资讯动态

STM32F103太阳能逐日系统实战:ADC、PWM与串口的完整闭环

发布时间:2026/10/27 23:33:28 来源:尧图企业网站定制
简介这是一套面向STM32F103Cortex-M3内核的太阳能逐日系统嵌入式软件工程适合单片机学习者、光伏控制开发工程师及电子竞赛备赛者参考覆盖太阳能板电压电流采集、MPPT最大功率点跟踪、储能电池充放电管理、过流过压保护与远程通信等功能。压缩包为rar格式共181个文件核心由43个h头文件和33个c源文件构成并包含Keil MDK工程文件uvprojx/uvoptx、标准外设库驱动以及axf、hex、map等编译输出文件整体约3.91MB可直接用Keil打开查看完整代码结构。目前已有51人学习下载。代码基于STM32F10x标准库编写展示了硬件抽象层、驱动层与应用层的分层设计思路可完整复现ADC采样、定时器中断、USART通信等工程级外设配置通过研读MPPT算法与电池管理逻辑能够快速上手太阳能追日系统的开发与调试也便于在现有框架上扩展新功能。整份资源结构紧凑适合作为嵌入式光伏控制方向的实战参考资料。1. 太阳能逐日系统为什么值得用STM32F103跑一次把ADC、PWM、串口的软硬件全部打通一块STM32F103配上两个光敏电阻和一个减速电机就能让太阳能板从早到晚追着太阳转这个系统的软件量不大但牵扯的外设比大部分入门教程都全ADC采光、定时器出PWM、串口打日志、GPIO读限位。很多做嵌入式的人学完cortex-M3内核的标准外设库之后下一步就卡在“怎么把这些外设串成一个真正干活的产品”上太阳能逐日系统恰好是这个跨度最小的实战项目——它不需要跑操作系统不需要复杂算法核心逻辑只有“比较两路光强、驱动电机转、别撞限位”三件事却能把一个完整闭环系统的坑全部踩一遍。这篇笔记写给已经会点Keil和标准库、想做一个能放在阳台或屋顶连续跑几天不出毛病的逐日控制器的人硬件成本控制在50元以内。2. 逐日系统硬件选型cortex-M3够不够用光敏采样和电机驱动怎么配2.1 用光敏电阻还是光电二极管ADC采样电路的分压计算逐日系统的传感器核心任务是判断“太阳在哪个方向”。最常用的方案是两个光敏电阻分别装在太阳能板两侧中间用一块不透明的隔板挡住。太阳偏左时左边光敏电阻照到更多光、阻值下降、分压点电压升高ADC读到的数值变大太阳偏右时右边数值变大太阳在正上方时两路接近相等。这个方案便宜、皮实光敏电阻5分钱一个缺点是响应速度慢大概几十毫秒但对逐日系统来说完全够用。光敏电阻不能直接接在STM32F103的ADC引脚上需要做一个分压电路。常见接法是光敏电阻一端接3.3V另一端接ADC引脚同时ADC引脚对地接一个10kΩ固定电阻。光敏电阻在强光下阻值大约5~10kΩ阴天能到50kΩ以上分压点的电压变化范围刚好能覆盖ADC的0~3.3V输入区间。如果你把手头模组换成光电二极管加运放线性度会更好但电路复杂度会上升而且运放的失调电压在弱光下反而容易让两路采样的基准不一致调试时更玄学。我的建议是第一版先把光敏电阻分压方案跑通后面再考虑精度问题。ADC参考电压直接用STM32F103的VDDA也就是3.3V电源轨。如果3.3V是AMS1117从5V转出来的纹波会直接影响采样精度这个坑后面专门讲。采样引脚我一般选择PA0和PA1这两个通道在ADC1的规则组里可以一轮采集完不需要切换注入组。2.2 电机带载选择的两个方案步进电机精度高直流减速电机成本低逐日系统的执行机构有两种主流选择。第一种是28BYJ-48步进电机加ULN2003驱动板精度高、扭矩大、可以开环精确控制角度缺点是转速慢、功耗高而且这个电机是5线制软件要写四相八拍的时序代码量会多几十行。第二种是直流减速电机加L298N或TB6612驱动转速快、控制简单正转反转加PWM调速缺点是停车靠机械限位或传感器反馈不能精确定位。对初学者我推荐第二种原因是太阳能逐日系统根本不需要高精度定位太阳在天空的角速度大约是每小时15度你只要保证每5分钟把板子对准一次就能拿到大部分能量直流减速电机加PWM调速已经完全够用。步进电机在这个项目里的精度优势体现不出来反而让控制逻辑变重——你需要在程序里维护一个“当前角度”变量还要处理丢步问题。做产品可以上步进做学习验证和初步样机直流减速电机是最省心的。驱动芯片TB6612比L298N好用得多逻辑电压兼容3.3V不需要额外电平转换直接拿STM32F103的PA6、PA7接PWM输入就行。2.3 供电和调试5V转3.3V电路与ST-Link的最小系统搭建STM32F103最小系统需要三样东西3.3V电源、8MHz晶振加两个20pF负载电容、SWD下载口。如果你买的是核心板这些都已经做好了如果你是画板子自己做电源部分要注意5V转3.3V电路不能直接拿AMS1117-3.3完事——AMS1117的最大压差约1.2V输入5V勉强够但如果你用的是电池供电电压掉到4.5V以下时输出就稳不住3.3V了。我踩过这个坑系统在室内调试一切正常拿到室外接太阳能板供电电压波动大就随机死机后来换成低压差LDO才解决。如果后续要加LCD或舵机3.3V的电流需求会涨到300mA以上AMS1117也得换成带散热焊盘的型号。下载调试用ST-Link最划算SWD模式只要接SWDIO、SWCLK、GND三根线。注意STM32F103的BOOT0引脚需要接地否则程序下载进去也跑不起来——这个是最常见的“为什么Keil提示下载成功但板子没反应”的原因。调试时还要预留串口我一般用USART1的PA9、PA10接一个CH340模块后面调试逐日逻辑全靠它打印数据。3. 用CubeMX初始化STM32F103ADC、定时器3、PWM和串口的参数设置3.1 安装pack包并新建Keil MDK工程STM32F103的工程搭建在Keil MDK里做第一步是给MDK安装对应的器件pack包。打开Keil的Pack Installer搜索STM32F1系列安装Keil::STM32F1xx_DFP最新版本。这一步不做的话新建工程时器件列表里找不到STM32F103C8T6后面写的所有代码都无法编译。工程搭建我建议直接用STM32CubeMX生成初始化代码虽然标准外设库StdPeriph也还能用但CubeMX生成的HAL库代码清晰、时钟配置不容易出错而且后续调整引脚分配比手写寄存器快。新建工程时选择STM32F103C8T6在Pinout视图里配置以下引脚PA0和PA1设为ADC1的IN0和IN1输入PA6和PA7设为TIM3的CH1和CH2输出PWMPC13设为GPIO输出接板载LEDPB0和PB1设为GPIO输入接左右限位开关PA9和PA10设为USART1的TX和RX。时钟树里把系统时钟设为72MHz这是cortex-M3内核在这个芯片上的最高主频ADC和PWM的时钟都由它分频而来。3.2 ADC1采样光敏电压采样时间与通道配置ADC的配置参数直接影响系统能不能分清“太阳偏了5度”和“阴天云层飘过”。在CubeMX的ADC1设置里规则组加入IN0和IN1两个通道对齐方式右对齐采样时间默认给的是14周期——这个值太短光敏电阻的采样源阻抗很高10kΩ量级采样时间不够时ADC内部采样电容充不满读到的值会随机跳变。我把采样时间改成55.5周期采样时钟分频设为6MHz左右这样单路采样大约耗时12微秒两路一起采样对系统性能的影响可以忽略。初始化代码由CubeMX生成后代码块长这样void MX_ADC1_Init(void) { ADC_ChannelConfTypeDef sConfig {0}; hadc1.Instance ADC1; hadc1.Init.ScanConvMode ENABLE; // 扫描模式一轮采多通道 hadc1.Init.ContinuousConvMode DISABLE; // 单次转换由软件触发 hadc1.Init.DiscontinuousConvMode DISABLE; hadc1.Init.ExternalTrigConv ADC_SOFTWARE_START; // 软件触发 hadc1.Init.DataAlign ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion 2; // 规则组里2个通道 HAL_ADC_Init(hadc1); sConfig.Channel ADC_CHANNEL_0; // PA0左侧光敏 sConfig.Rank ADC_REGULAR_RANK_1; sConfig.SamplingTime ADC_SAMPLETIME_55CYCLES_5; HAL_ADC_ConfigChannel(hadc1, sConfig); sConfig.Channel ADC_CHANNEL_1; // PA1右侧光敏 sConfig.Rank ADC_REGULAR_RANK_2; sConfig.SamplingTime ADC_SAMPLETIME_55CYCLES_5; HAL_ADC_ConfigChannel(hadc1, sConfig); }ScanConvMode打开表示一轮转换会依次采完两个通道ContinuousConvMode关闭表示每次转换需要软件再触发一次。每轮转换完成后读数据寄存器要区分当前读到的属于哪个通道按Rank值判断。这里最容易犯的错是只配置了通道没配扫描模式结果第二轮读到的还是第一个通道的数据——我调试时遇到过跟踪方向永远朝一边的情况最后就是发现ADC只返回了PA0的值。3.3 TIM3输出PWM驱动电机TIM2做控制周期定时器电机的调速用PWMSTM32F103的TIM3有4个通道我拿CH1和CH2分别控制电机的正转和反转。电机驱动芯片如果是TB6612它需要两个输入引脚控制方向一个PWM脚控制速度。简化做法是PA6TIM3_CH1控制正转PWMPA7TIM3_CH2控制反转PWM哪一路有输出电机就朝哪个方向转两路都不输出电机就停。这种接法多占一个定时器通道但逻辑非常简单方向切换时不用额外操作两个GPIO的时序。void MX_TIM3_Init(void) { TIM_OC_InitTypeDef sConfigOC {0}; htim3.Instance TIM3; htim3.Init.Prescaler 72 - 1; // 72MHz分频后为1MHz htim3.Init.Period 1000 - 1; // PWM频率1kHz htim3.Init.CounterMode TIM_COUNTERMODE_UP; HAL_TIM_PWM_Init(htim3); sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 0; // 初始占空比0电机停止 sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; HAL_TIM_PWM_ConfigChannel(htim3, sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_ConfigChannel(htim3, sConfigOC, TIM_CHANNEL_2); }Prescaler设72减1定时器计数频率变成1MHzPeriod设1000减1PWM输出频率就是1kHz。这个频率对直流减速电机合适太低电机会发出嗡嗡声而且转速抖动太高的话TB6612的开关损耗变大。占空比从0到100对应Pulse值0到999我实际调试时发现占空比到30%电机就能可靠启动到60%接近满速所以程序里会把最大占空比限制在700防止上电瞬间电流过大把3.3V电源拉垮。定时器2在这里的作用不是输出PWM而是产生一个100ms的中断作为逐日控制的主节拍。我不想在主循环里用HAL_Delay做延时因为电机转动期间如果同时要读ADC和限位开关延时会让响应变迟钝。TIM2配置成72MHz分频7200得到10kHz计数频率计数值1000就是100ms进一次中断在中断回调里置一个flag主循环看到flag就执行一次“采光、比较、决定是否转电机”的逻辑。3.4 串口打印关键变量看现场数据替代猜逐日系统写完之后最痛苦的问题是“它为什么不转”。这时候串口打印是唯一的救命稻草。初始化USART1为115200-8-N-1然后在主循环里周期性地把两路ADC值和当前的电机状态打出来。代码块如下void debug_print(uint16_t adc_left, uint16_t adc_right, uint8_t motor_state) { char buf[64]; sprintf(buf, L:%d R:%d S:%d\r\n, adc_left, adc_right, motor_state); HAL_UART_Transmit(huart1, (uint8_t*)buf, strlen(buf), 100); }motor_state用0表示停、1表示正转、2表示反转、3表示限位触发。把这行日志每秒打印一次接上串口助手看。如果L和R的值在光照不变时来回跳说明采样有问题如果光照变化但差值始终不超阈值说明阈值设太大如果差值超过了但电机不动说明PWM配置有误。这一步能排除掉80%的故障。4. 逐日控制软件怎么写从差分比较到带滞回的状态机4.1 差分比较法为什么够用死区与滞回是核心参数逐日控制最简算法是每次控制周期读两路ADC值算出差值diff left - right。diff大于某个正值就往左转小于某个负值就往右转否则停。这套逻辑看起来无懈可击但直接写出来会翻车阴天时两路光敏电阻的差值在0附近抖动电机会频繁启动、停止每分钟正反转十几次减速电机这样折腾两天就废了。解决方法是引入死区和滞回两个参数。死区deadband是一个差值区间比如±50以内认为“方向对准了”不动作。滞回hysteresis是另一个更关键的参数进入死区之后不是差值一出死区就立刻动作而是要让差值超过一个更大的阈值才动。比如启动阈值设±80死区阈值设±40这样电机一旦开始转动就要等到差值回落到40以内才停而不会在边界上来回抖动。滞回比较相当于给控制系统加了一点“惰性”在精度要求不高的逐日场景里这个惰性换来的是电机寿命。ADC是12位的满量程4096实际强光下两路光敏电压大约在2.5V附近对应ADC值3100左右。阴天时这个值可能掉到1500左右。所以死区和滞回的阈值不能用一个固定值覆盖所有天气——这个后面进阶章节细说第一版代码先用固定值把逻辑跑通。4.2 主循环状态机扫描、跟踪、保护三个模式把逐日逻辑写成一个简单的状态机会让程序结构清晰很多。三个状态分别是SCAN扫描、TRACK跟踪、PROTECT保护。上电后先进入SCAN控制电机从当前角度向一个方向连续转直到限位开关触发然后反向转回这个过程的目的是让系统知道“我的可动范围在哪”同时让两个光敏电阻都转过一遍避免太阳能板初始朝向完全背光导致永远找不到太阳。SCAN结束后停在光照比较强的位置进入TRACK。TRACK就是正常的逐日逻辑每100ms执行一次。核心代码块如下void tracking_control(int16_t diff) { if (motor_state MOTOR_STOP) { // 死区判断差值在正负40以内认为对准了不动 if (diff TRACK_HYSTERESIS) set_motor(MOTOR_LEFT, PWM_DUTY_MAX); else if (diff -TRACK_HYSTERESIS) set_motor(MOTOR_RIGHT, PWM_DUTY_MAX); } else { // 电机已在转动判断是否回到死区范围回则停 if (diff -TRACK_DEADBAND diff TRACK_DEADBAND) set_motor(MOTOR_STOP, 0); } }这段代码的逻辑是停止状态下差值超过正负80才启动电机转动状态下差值回到正负40以内才停。这样的不对称阈值就是刚才说的滞回。TRACK_HYSTERESIS我第一版设80TRACK_DEADBAND设40实际测试下来晴天没问题阴天偶尔还是会抖后来把采样做了滑动平均才彻底压住。PROTECT状态在限位开关触发时进入。电机不管正在朝哪边转先停下来然后只允许向反方向转。这个状态由GPIO中断或主循环轮询触发我用轮询每100ms读一次PB0和PB1读到低电平开关按下就强制切状态。4.3 限位开关与手动复位保护硬件的第一道防线限位开关在逐日系统里不是可选项是必选项。太阳能板的机械支架角度有限如果没有限位电机转到机械卡死位置还在使劲转轻则烧驱动芯片重则把支架结构掰弯。我见过一个没装限位的DIY系统连续运转两天后涡轮蜗杆直接卡死电机堵转电流把TB6612烧了。限位开关用机械式微动开关最可靠接法是一端接GND另一端接GPIO并启用内部上拉。开关未被压下时GPIO读到高电平被机械结构撞到后接通GND读到低电平。代码里要做消抖连续两次读到低电平间隔2个控制周期才确认限位触发不然开关触点弹跳会让程序误判。限位触发后的处理逻辑如下void limit_check(void) { if (HAL_GPIO_ReadPin(LIMIT_LEFT_GPIO_Port, LIMIT_LEFT_Pin) GPIO_PIN_RESET) { if (limit_left_count 2) // 连续两个周期确认 { motor_state MOTOR_STOP; limit_active LIMIT_LEFT; } } else { limit_left_count 0; } }limit_active用于在代码里记住“刚才撞到了左边”这样后续控制反转时有了依据不会一恢复控制又朝左边转。5. 避坑指南STM32F103逐日系统最常见的5个故障5.1 电机一启动ADC读数就乱跳现象电机转动瞬间串口打印的两路ADC值从3000突然掉到500或者出现几百的随机跳动。电机一停数据又恢复正常。原因有两个叠加一是电机启停的瞬态电流让5V电源跌落AMS1117输出的3.3V跟着波动二是电机的电刷产生电磁干扰耦合到ADC输入线上。解决方法是驱动板的地线单独走不要和光敏电阻的模拟地共用同一段线ADC引脚对地并联一个0.1μF电容滤高频噪声软件上做多次采样取中值比如每路连续采5次排序后取中间值而不是取平均值——平均值会被极端跳变值带偏中值对尖峰干扰免疫。5.2 阴天时电机频繁正反转现象太阳被云层遮住两路光敏电阻的读数来回抖动电机每隔几秒就反向转一次整个支架咯吱咯吱响。原因是死区太小。我第一版把死区设为40晴天时太阳移动产生的位置差足够小系统安静阴天时漫射光让两个光敏电阻的差异变得很不稳定差值在正负几十之间晃动频繁穿越死区边界。解决方法是把启动阈值提高到100并加一个时间条件同一方向的动作至少要持续3个控制周期才允许改变方向。也就是说程序记录上一次电机转向如果这次判定的方向不同先不执行等下一次判定仍然同向再切。这个方法对任何传感器噪声都有效比单纯放大阈值伤害更小。5.3 光敏电阻被太阳能板影子罩住系统转向错误现象系统在晴天午后突然反向转把板子转到完全朝西。排查后发现右侧光敏电阻被太阳能板自身的接线盒影子挡住了导致右边读数骤降系统误判“太阳在左边”。这类问题的根源是传感器安装位置太靠近板面影子的移动让传感器读到的不是阳光强度而是局部阴影。解决方法是把两个光敏电阻安装在板面之外的独立小支架上加高约5厘米并且每个传感器套一个不透明遮光筒筒长约2~3厘米内径1厘米这样只有来自前方一定角度范围的光线能照到传感器侧面的杂散光和部分阴影被挡在筒外。5.4 5V供电纹波导致跟踪精度飘移现象接直流电源适配器时系统工作正常换到12V蓄电池加降压模块供电后串口打印的ADC值出现周期性波动。原因是降压模块的纹波叠加到5V再传导到3.3VADC的参考电压不稳读数的绝对值在漂移。虽然系统用的是差分比较理论上两路光敏电阻同时漂移不影响差值但如果干扰源靠近某一根采样线就会产生不对称噪声。解决方法是ADC采样使用STM32F103的VREFINT内部参考电压做校准每次上电时读VREFINT通道的值把它和标准电压的比值作为一个修正系数对所有采样值做归一化。RM0008参考手册的13.4节有专门说明这个内部基准电压在出厂时校准过3.3V下的值能补偿大部分供电波动。5.5 Keil下载失败或ST-Link连接不稳定现象Keil提示“Error: Flash Download failed - Cortex-M3”或者ST-Link能识别但下载到一半断开。排查时发现两个常见原因一是pack包版本和芯片型号不匹配Keil报错里如果提到“No STM32 target found”先检查Pack Installer里ST32F1xx DFP是否完整安装二是SWD线太长我用过20厘米的杜邦线连接ST-Link下载大程序时经常失败换成10厘米以内的短线就稳定了。还有一个玄学问题目标板供电不足时ST-Link也会不正常因为SWD调试接口的参考电平被拉低。解决方法是ST-Link单独用一个口供电不要和电机共用电源线。6. 把逐日系统调得更准滞回参数整定与低功耗设计逐日系统的核心参数就三个采样次数、死区阈值、滞回阈值。它们之间互相牵制我调试久了总结出一个经验先调采样次数再看差值曲线最后定阈值。具体做法是把程序里阈值临时设成0让系统永远处于“方向不对就一直转”的状态然后观察串口打印的差值变化范围。在晴天中午连续观察5分钟差值波动范围大概在±30以内阴天这个范围会扩大到±150。那么死区设为最大波动值的一半滞回设为死区的1.5倍即阴天死区75、滞回110——这样既不会频繁启动也不会在云层突然散开时反应太慢。另一个想提的是低功耗设计。如果这个系统要做成户外独立供电每天的能耗大头是电机和驱动板待机电流STM32F103反而可以通过进入Stop模式省掉大部分功耗。做法是控制周期从100ms改成500ms每次执行完控制逻辑后调用HAL_PWR_EnterSTOPMode用TIM2的中断唤醒。注意进入Stop模式前要把ADC和串口都DeInit唤醒后再重新Init这个过程大约消耗几百毫秒对逐日系统的时间尺度来说完全可以接受。我实测过12V 10W的太阳能电池板配一块6安时的铅酸电池连续阴雨三天后系统仍然能正常启动。最后说一个我自己养成的习惯每次改完参数都用不干胶把当天的日期和阈值写在支架侧面。因为你会发现阴天调好的参数到晴天可能让电机过度频繁动作晴天调好的参数到阴天又会让系统反应迟钝。记录下每次调参时的天气、阈值、现象两周之后回头看你会对这套系统的“脾气”比任何人都熟悉。希望这篇笔记帮你在STM32F103的逐日系统上少走几趟弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑