简介:这是一套面向STM32F103(Cortex-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初始化STM32F103:ADC、定时器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输出(PWM);PC13设为GPIO输出接板载LED;PB0和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做控制周期定时器
电机的调速用PWM,STM32F103的TIM3有4个通道,我拿CH1和CH2分别控制电机的正转和反转。电机驱动芯片如果是TB6612,它需要两个输入引脚控制方向,一个PWM脚控制速度。简化做法是:PA6(TIM3_CH1)控制正转PWM,PA7(TIM3_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,定时器计数频率变成1MHz,Period设1000减1,PWM输出频率就是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我第一版设80,TRACK_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.3V,ADC的参考电压不稳,读数的绝对值在漂移。虽然系统用的是差分比较,理论上两路光敏电阻同时漂移不影响差值,但如果干扰源靠近某一根采样线,就会产生不对称噪声。解决方法是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的逐日系统上少走几趟弯路。
本文还有配套的精品资源,点击获取