
简介基于STM32的自动追光系统完整源码资料包面向毕业设计、课程设计及嵌入式方向学习者。系统通过光敏电阻采集环境光强经STM32处理后控制步进电机转动实现对光源的自动追踪可扩展应用于太阳能发电、智能照明等场景。压缩包共139个文件约5.39MB以C源码、H头文件、Keil工程配置及hex烧录文件为主同时包含部分文档与辅助文件便于直接编译、烧录与二次开发。工程结构清晰涵盖光敏采集、电机驱动、主控逻辑等模块读者可对照代码理解ADC采样、GPIO控制及电机调速实现并参考硬件设计完成整体搭建。目前已有217人学习下载适合需要完整方案参考、快速完成项目或进行功能扩展的开发者。1. 自动追光系统为什么 STM32 是这套方案里最难绕开的控制核心一块固定的太阳能板一天下来有效发电时长往往只有正午前后那两三个小时换成自动追光之后累计发电量能提升 30% 到 45%。这个数字靠的不是高端光伏材料而是让板子始终对着太阳。自动追光系统的核心逻辑并不复杂检测光源方向驱动电机转动直到传感器读数达到平衡。难点在于传感器怎么布、数据怎么读、电机怎么控、系统怎么在户外长期稳定跑。STM32 之所以成为这类项目的主流选择是因为它同时具备多通道 ADC、硬件定时器 PWM、丰富的中断和足够便宜的开发成本一块几个引脚的单片机就能把检测、决策、驱动三件事全部包下。这篇内容适合正在做 STM32 项目或基于 STM32 的毕业设计的人也适合想把手头固定光伏板改成追光结构的嵌入式开发者。下面按“硬件结构 → STM32 采样与控制 → 完整系统联调 → 离线验证技巧”的顺序把一套能落地的自动追光方案拆开讲。2. 自动追光系统的结构设计与光敏元件选型在打开 Keil 写代码之前先把机械结构和传感器布局定下来。很多 STM32 自动追光项目做到一半推倒重来原因往往不是程序跑不通而是光敏传感器装的位置不对导致追光算法无论怎么写都无法稳定收敛。2.1 双轴还是单轴先看应用场景再定自由度常见的自动追光系统结构有两类。单轴追光只在一个方向上旋转通常是东西方向跟踪太阳的每日运动轨迹适合纬度较低、光照时间长的地区双轴追光在水平和俯仰两个方向各有一个电机能够同时补偿太阳高度角和方位角的变化发电增益更明显但机械结构、控制逻辑和功耗都成倍增加。我在实际项目中的选型原则是如果板子面积小于 0.5 平方米、供电能力有限优先做单轴如果要展示完整的 STM32 项目源码或者做毕业设计双轴更容易体现系统设计能力。双轴系统的传感器布局和算法可以完全向下兼容单轴所以下面以双轴为例展开。2.2 光敏传感器的三种选型路线追光系统必须有“感知光源方向”的手段常见方案有三种各有各的适用边界。第一种是光敏电阻分压采样。光敏电阻的阻值随光照强度变化配合固定电阻组成分压电路将光照变化转换为电压变化送入 STM32 的 ADC 引脚。优点是便宜、电路简单、替换方便缺点是响应速度慢、一致性差同一批次的电阻阻值离散度高需要逐个标定。第二种是光电二极管或光敏三极管。响应速度快、线性度好适合需要快速响应的场景但需要运算放大器配合电路复杂度上升。第三种是太阳能板开路电压法。利用太阳能板本身作为光传感器开路电压与光照强度近似线性相关通过分压电阻采样光伏板正极电压即可判断光照方向。这种方法不需要额外光敏元件但在弱光环境下开路电压变化不明显容易导致追光误判。我一般优先推荐光敏电阻方案四路光敏电阻加上四个分压电阻就能组成完整的传感器阵列成本在几块钱以内正好匹配 STM32F103 系列自带的多通道 ADC 资源。2.3 传感器遮光结构与采样电路参数传感器布局是整个系统成败的关键。四路光敏电阻应该以光伏板的中心为原点分布在东南、东北、西南、西北四个方向每两个相邻传感器之间用不透明的隔板隔开。隔板高度需要根据传感器间距计算目的是让光线从某个方向射入时只有朝向该方向的传感器能直接接收到光照其余传感器被隔板阴影遮挡。这样的结构能够让四路 ADC 采样值产生明显的差异控制算法才能根据差异判断光源偏移方向。以我常用的电路参数为例光敏电阻选用 5mm 型光敏电阻亮阻 5 到 10 千欧暗阻 1 兆欧以上固定分压电阻选 10 千欧。光敏电阻一端接 3.3V另一端接固定电阻到 GND中间抽头接 STM32 的 ADC 引脚。这样在强光环境下分压点电压接近 2V 左右在弱光环境下接近 0V可以充分利用 STM32 的 0 到 3.3V 采样范围。参数项推荐值说明光敏电阻亮阻5kΩ ~ 10kΩ1000Lux 光照下测量固定分压电阻10kΩ与亮阻接近分压灵敏度最高采样电压范围0V ~ 3.3V匹配 STM32 VDDA 供电ADC 分辨率12 位0~4095对应电压分辨率约 0.8mV隔板高度8mm ~ 15mm根据传感器间距调整避免互相串光3. STM32 实现追光ADC 采样、比较策略与 PWM 舵机控制结构定了之后进入 STM32 项目开发的核心环节。无论是基于标准外设库还是 HAL 库整个软件部分可以拆成三个模块多通道 ADC 采样模块、追光判断算法模块、PWM 电机驱动模块。3.1 用 STM32CubeMX 配置 ADC 多通道采样的关键步骤在 STM32 开发环境中新建工程时我建议直接用 STM32CubeMX 生成初始化代码重点配置以下几个地方。ADC 引脚配置为模拟输入开启 ADC1 的四个通道对应同一组 ADC 外设。扫描模式Scan Mode必须使能连续转换模式Continuous Mode按需开启因为追光系统不需要极高的采样率但需要四路数据几乎同时获取所以建议开启扫描模式转换为单次模式软件触发一轮四通道转换后读取四个结果。ADC 采样时间建议配置为 55.5 或 239.5 个周期。采样时间越长采样电容充电越充分结果越稳定光敏电阻分压电路的输出阻抗较高采样时间太短会导致读数偏低。我的做法是设置 ADC 时钟为 12MHz采样时间选择 239.5 周期四位采样值基本不跳动。/* 以 HAL 库为例ADC 四通道扫描采样的核心读取逻辑 */ uint16_t adc_values[4] {0}; void ADC_Scan_4Channel(void) { ADC_ChannelConfTypeDef sConfig {0}; for (int ch 0; ch 4; ch) { sConfig.Channel ADC_CHANNEL_0 ch; /* 根据实际引脚映射调整 */ sConfig.Rank ch 1; /* 扫描顺序 1~4 */ sConfig.SamplingTime ADC_SAMPLETIME_239CYCLES_5; /* 高阻抗源需要长采样时间 */ HAL_ADC_ConfigChannel(hadc1, sConfig); } HAL_ADC_Start(hadc1); HAL_ADC_PollForConversion(hadc1, 10); /* 等待第一通道转换完成 */ adc_values[0] HAL_ADC_GetValue(hadc1); HAL_ADC_PollForConversion(hadc1, 10); adc_values[1] HAL_ADC_GetValue(hadc1); HAL_ADC_PollForConversion(hadc1, 10); adc_values[2] HAL_ADC_GetValue(hadc1); HAL_ADC_PollForConversion(hadc1, 10); adc_values[3] HAL_ADC_GetValue(hadc1); }这段代码的执行逻辑是在每次扫描转换前把四个通道的参数重新配置一次确保通道顺序正确然后启动一次 ADC 转换序列。每次调用HAL_ADC_PollForConversion等待当前通道转换完成再用HAL_ADC_GetValue取出寄存器的转换结果。四路数据存放在adc_values数组中顺序与通道配置一致。由于扫描模式下 DMA 是更高效的方式如果 ADC 数据还需要用于其他任务建议开启 DMA 搬运避免 CPU 反复查询造成阻塞。这里使用循环轮询的方式实现起来最直观在追光这种低频控制场景下完全够用。3.2 追光判断策略差值式比较与动态阈值有了四路光照值接下来要回答“向哪个方向转”。最直接的思路是找最大值让板子朝光照最强的方向移动。但这种算法有个典型问题在云遮挡或散射光环境下四个方向的 ADC 值差异很小最大值法会频繁改变方向造成电机来回振荡功耗增加且机械结构容易疲劳损坏。我常用的策略是差值式逼近法。设东、西、南、北四路传感器采样值分别为 E、W、S、N水平方向偏差由 E 和 W 做差得到俯仰方向偏差由 S 和 N 做差得到。定义一个死区阈值dead_zone当两路差值绝对值小于该阈值时认为当前方向已经对准光源电机停止超过阈值才启动电机朝数值较大的一侧转动。死区阈值的大小需要根据 ADC 采样噪声和传感器差异来定。阈值太大追光精度不够阈值太小电机频繁启停。经验值是取满量程的 2% 到 5%例如 12 位 ADC 满量程 4095死区阈值取 100 到 200 比较合适。/* 四路传感器差值式追光判断输出电机方向控制标志 */ #define DEAD_ZONE 150 /* 死区阈值ADC原始值单位 */ #define MOVE_STEP 15 /* 单次移动步数控制舵机转角的增量 */ typedef struct { int8_t horizontal; /* 水平方向-1 左转0 停1 右转 */ int8_t vertical; /* 俯仰方向-1 下转0 停1 上转 */ } TrackCommand; TrackCommand track_control(uint16_t E, uint16_t W, uint16_t S, uint16_t N) { TrackCommand cmd {0, 0}; int16_t diff_h (int16_t)E - (int16_t)W; int16_t diff_v (int16_t)N - (int16_t)S; if (diff_h DEAD_ZONE) { cmd.horizontal 1; /* 东侧光照明显强向右转 */ } else if (diff_h -DEAD_ZONE) { cmd.horizontal -1; /* 西侧光照强向左转 */ } if (diff_v DEAD_ZONE) { cmd.vertical 1; /* 北侧光照强向上转 */ } else if (diff_v -DEAD_ZONE) { cmd.vertical -1; /* 南侧光照强向下转 */ } return cmd; }差值式逼近法比最大值法多了一层“先判差异、再判方向”的逻辑不仅滤掉了传感器绝对值的个体差异也天然实现了死区控制。判断逻辑中用符号变量作为运动方向标志而不是直接输出 PWM 占空比是为了把“判断”和“驱动”解耦方便后续换不同功率的电机时只改驱动层。3.3 PWM 驱动舵机的频率与占空比计算追光系统常用的执行机构有两种舵机和步进电机。舵机通过 50Hz 的 PWM 方波信号控制转角SG90 这类小舵机的脉宽和角度关系是 0.5ms 对应 0 度、1.5ms 对应 90 度、2.5ms 对应 180 度。STM32 定时器的 PWM 模式比较寄存器值由定时器时钟、预分频值和自动重装载值共同决定。以 STM32F103 的 TIM2 为例APB1 定时器时钟为 72MHz设置预分频器 PSC 为 71计数器频率变成 1MHz即 1us 计数一次。舵机需要 20ms 周期对应的 PWM 频率 50Hz所以自动重装载值 ARR 设为 19999这样计数周期就是 20ms。占空比 2.5% 对应 0.5ms 脉宽也就是比较寄存器 CCR 值为 50012.5% 对应 2.5ms 脉宽CCR 值为 2500。/* 初始化 50Hz PWM用于控制追光舵机 */ void Servo_PWM_Init(void) { TIM_OC_InitTypeDef sConfigOC {0}; sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 1500; /* 初始脉宽 1.5ms对应 90 度中间位置 */ sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; HAL_TIM_PWM_ConfigChannel(htim2, sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1); } void Servo_Set_Angle(uint8_t channel, uint16_t pulse_us) { if (pulse_us 500) pulse_us 500; /* 限制最小脉宽 0.5ms */ if (pulse_us 2500) pulse_us 2500; /* 限制最大脉宽 2.5ms */ __HAL_TIM_SET_COMPARE(htim2, channel, pulse_us); }PWM 控制参数中的关键点有两个。一是定时器时基要计算准确预分频和重装载值必须保证输出频率精确在 50Hz 附近误差过大舵机会发出尖锐啸叫并抖动二是每次在角度边界做限幅防止程序跑飞时脉宽超出舵机允许范围烧毁电位器或齿轮。4. 从最小原型到完整源码状态机、限位与系统联调前面的代码解决了“怎么读和怎么转”但完整系统源码真正体现价值的地方在整体逻辑编排。STM32 追光项目中最容易出的问题不是单个模块而是模块之间的状态转换启动时光伏板应该停在什么位置阴天怎么处理电机堵转了怎么办4.1 系统状态机设计我把自动追光系统的运行流程抽象成四个状态初始化态、搜索态、跟踪态和休眠态。初始化态负责硬件自检和舵机归位搜索态在大范围内扫描找光跟踪态进入 3.2 节的差值逼近循环休眠态在光照低于阈值时关闭电机驱动只保留定时唤醒。状态机的好处是每个状态下的行为边界清晰异常处理也容易定位。例如搜索态下如果连续 30 秒没有找到有效光源自动切入休眠态跟踪态下如果传感器读数突然整体下降先判断是否遮云连续低照度超过 5 分钟才切回搜索态避免云层一过就乱转。/* 追光主循环的简化状态机实现 */ typedef enum { STATE_INIT 0, STATE_SEARCH, STATE_TRACK, STATE_SLEEP } TrackState; void Track_Task_Handler(void) { static TrackState state STATE_INIT; uint16_t light_sum; uint32_t low_light_cnt 0; switch (state) { case STATE_INIT: Servo_Set_Angle(TIM_CHANNEL_1, 1500); /* 水平回中 */ Servo_Set_Angle(TIM_CHANNEL_2, 1500); /* 俯仰回中 */ state STATE_SEARCH; break; case STATE_SEARCH: light_sum adc_values[0] adc_values[1] adc_values[2] adc_values[3]; if (light_sum SEARCH_THRESHOLD) { state STATE_TRACK; } else if (low_light_cnt 300) { state STATE_SLEEP; /* 连续低照度进入休眠 */ } break; case STATE_TRACK: if (is_light_stable()) { low_light_cnt 0; exec_track_command(); /* 调用差值逼近控制函数 */ } else { if (low_light_cnt 3000) /* 5 分钟低照度(100ms周期) */ { state STATE_SLEEP; } } break; case STATE_SLEEP: __WFI(); /* 进入休眠由定时器唤醒 */ break; } }调试这份源码时建议把每个状态切换的触发条件先写在注释里再对照代码检查。例如SEARCH_THRESHOLD需要根据实际环境白天和黑夜的 ADC 总量来标定不能随便给一个固定值。状态机实现里还有一个隐含要求is_light_stable函数需要返回一个综合判断结果而不仅仅依赖某一路传感器避免单路被遮挡时误判为异常。4.2 限位开关与零点校准舵机累积误差的消除方法舵机是一个闭环系统内部有反馈电位器转角和指令脉宽一一对应不像步进电机存在开环丢步的问题。但它没有“绝对零点”的概念断电重新上电后STM32 并不知道舵机当前的真实角度只能默认发送初始脉宽对应的角度。如果机械结构在断电时被外力拨动过重新上电后光伏板的实际角度和程序认为的角度就不一致了。解决这个问题最可靠的方法是加限位开关。在水平和俯仰方向的最大行程两端各装一个微动开关初始化时让电机朝一个方向转直到限位开关触发把这个位置记录为机械零位再朝目标方向转指定角度。/* 使用 GPIO 中断或轮询方式检测限位开关触发并校准零位 */ #define LIMIT_SW_GPIO_PIN GPIO_PIN_5 #define LIMIT_SW_GPIO_PORT GPIOB uint8_t Limit_Switch_IsPressed(void) { return (HAL_GPIO_ReadPin(LIMIT_SW_GPIO_PORT, LIMIT_SW_GPIO_PIN) GPIO_PIN_RESET); } void Servo_Calibrate_Zero(void) { while (!Limit_Switch_IsPressed()) { Servo_Set_Angle(TIM_CHANNEL_1, 1000); /* 朝一个方向慢速转动 */ HAL_Delay(20); } Servo_Set_Angle(TIM_CHANNEL_1, 1500); /* 回到机械中点作为控制参考零位 */ HAL_Delay(1000); }注意限位开关一般使用常闭触点串联在电机供电回路同时在 GPIO 上加上拉电阻读取状态。常闭触点的好处是限位开关断线时表现为触发状态系统不会朝限位方向继续驱动安全性更高。4.3 完整源码工程中的目录划分拿到一份完整的 STM32 自动追光系统源码包第一次打开工程时先看目录结构能省很多时间。常见做法是分为 App、Driver、BSP、Middlewares 四层。App 存放状态机和追光算法Driver 存放舵机和 ADC 的硬件驱动文件BSP 存放板级初始化函数Middlewares 放置操作系统或通信协议库。如果源码包里的工程没有做分层建议自己按这个结构整理后面加功能会轻松得多。4.4 双舵机联调的常见问题排查双轴联调时经常出现以下状况。水平和俯仰两个舵机同时转动时电源电压瞬间跌落导致 STM32 复位这是因为舵机启动电流峰值较高普通 USB 供电无法满足瞬态电流需求。解决方案是在舵机电源两端并联一个 470uF 以上的电解电容或使用独立的舵机电源模块。另一个典型的坑是两路 PWM 信号共用地线。舵机驱动板和 STM32 开发板必须共地否则 PWM 信号的电平参考不一致舵机会随机抖动而且这种抖动用示波器都不一定能稳定复现。联调阶段的排查建议按表格顺序进行避免在多个因素叠加时盲目修改参数。故障现象优先检查项处理方案舵机完全不转PWM 输出引脚、电源电压示波器测 PWM 波形确认频率 50Hz两个舵机互相干扰共地、电源容量舵机电源独立供电并联大电容追光方向反了电机接线或传感器编号交换对应通道的 ADC 索引或 PWM 方向阴天反复乱转死区阈值过小根据 ADC 噪声实测值调大死区光照强但不动ADC 采样时间过短增大采样周期检查参考电压配置5. 离线调试点用串口日志验证 STM32 自动追光系统的判断逻辑追光系统在户外跑起来之后很难直观判断算法的每一次决策是否正确因为光照变化是连续的人眼观察到的“板子转没转”和程序内部的“为什么转”之间存在信息断层。我的做法是给系统加一路串口日志输出把关键变量和状态转换实时打印出来再用手电筒和遮挡物在离线状态模拟光照变化验证程序的判断逻辑是否符合预期。5.1 串口打印的设计传感器原始值、差值计算、状态机当前的组合输出串口输出不能只是一堆数字要设计成方便观察的文本协议。每条日志建议包含四路 ADC 原始值、水平差值、俯仰差值、死区阈值和当前状态。这样在追踪问题时只用看一条日志就能判断传感器、算法、状态机三个环节的关系。/* 串口日志输出示例用于追光判断逻辑的离线验证 */ void Debug_Log_TrackData(TrackState state, TrackCommand cmd) { char buf[128]; snprintf(buf, sizeof(buf), [%lu] state%d E%u W%u S%u N%u dh%d dv%d cmd_h%d cmd_v%d\r\n, HAL_GetTick(), state, adc_values[0], adc_values[1], adc_values[2], adc_values[3], (int16_t)adc_values[0] - (int16_t)adc_values[1], (int16_t)adc_values[3] - (int16_t)adc_values[2], cmd.horizontal, cmd.vertical); HAL_UART_Transmit(huart1, (uint8_t *)buf, strlen(buf), 100); }日志中添加时间戳是必要的HAL_GetTick()返回毫秒级系统时间能够还原追光系统在时间轴上的行为序列定位“先乱转后停住”这类问题时就靠它判断各事件发生的先后顺序。5.2 手电筒标定法三个典型测试用例离线验证追光逻辑时我用一个强光手电筒模拟太阳分别做三个测试用例。第一个用例是正前方光照测试。手电筒从正上方照射传感器阵列理想情况下东西和南北两组差值都应该落在死区范围内系统应输出停转指令。如果系统仍然转动说明死区阈值设置得比传感器静态偏差还小需要统计十次静态读数的最大偏差将死区设置在该值的 1.5 倍以上。第二个用例是单侧光源测试。手电筒从东侧斜射用挡板遮住其余三侧传感器此时东侧 ADC 值应该明显高于西侧水平方向应输出右转指令。若没有输出正确方向检查 ADC 通道映射顺序是否和实际电路板丝印一致这个错误在自行焊接的板子上尤其常见。第三个用例是渐变光测试。手电筒从东侧缓慢移动到西侧观察日志中的差值曲线是否平滑过渡有没有跳变。如果差值变化不平滑检查传感器是否受到隔板的边缘散射影响必要时加长隔板高度。5.3 从原型走向长期运行阈值标定与日志归档系统调到能正常工作以后最后一步是阈值标定的固化。AD 原始值的范围会随季节、天气和安装环境变化因此不能把死区阈值写死建议在初始化阶段做一次自动校准。程序启动后让系统静止 10 秒连续采样记录四路 ADC 值的最大值和最小值用这个波动范围自动计算死区阈值。日志输出也应该分级管理。调试阶段打开详细日志正式运行阶段只保留状态切换和异常报警事件避免长时间运行后日志文件膨胀。考虑到 STM32 内部 Flash 有限日志数据直接输出到 SD 卡或通过 Wi-Fi 模块上报上位机比存本地更实用。追光系统的离线验证做扎实了搬到真实环境之后剩下的问题基本只剩天气适应性和机械耐久性而这些已经超出单片机软件代码能解决的范畴。调试串口接口不要拆除保留在最终样板上的好处是系统在户外跑几个月后随时可以插上线观察传感器衰减情况重新校准阈值参数。本文还有配套的精品资源点击获取