☰
基于STM32F103的太阳能逐日系统设计与实现
2026/10/27 7:39:23 网站建设 项目流程

简介:一套采用Cortex-M3内核的STM32F103主控芯片的太阳能逐日系统嵌入式软件工程,面向嵌入式开发者、电子爱好者和新能源控制学习者,覆盖太阳能板电压电流采集、最大功率点跟踪、储能电池充放电管理及系统保护等核心控制逻辑。rar压缩包内共181个文件,整体约3.91MB;文件以C源文件与头文件为主,另有Keil工程配置文件、预编译的库文件以及axf和hex等可执行产物,可支持直接打开查看、重新编译和烧录到开发板验证。目前已有51人学习或下载,数据规模不算大,但因资源包内同时包含源码层次结构、驱动实现和工程配置,仍适合作为入门或课程设计的参考资料。工程采用STM32标准外设库组织驱动层,包含模数转换、定时器、I2C、CAN、USART等外设接口,完整展示从硬件抽象到应用层控制逻辑的编写方式;拿到压缩包后可基于Keil直接打开工程,结合源码理解逐日系统如何实现数据采集、最大功率点跟踪、电池保护等功能,减少从零搭建的重复工作。

1. 太阳能逐日系统为什么绕不开STM32F103:从双轴跟踪到Cortex-M3选型

做逐日系统的第一反应是把太阳能板固定住,但光伏发电的收益大头在春秋两季的早晚时段——太阳斜射角度变化快,固定倾角板在早晨和下午的发电量掉得厉害。双轴逐日系统就是让板子始终正对太阳,实测能比固定支架多收 20% 到 35% 的发电量。这个项目用 STM32F103(Cortex-M3 内核)做主控,是因为它在 8 位单片机和带浮点单元的 M4 之间卡得刚好:算视日轨道、读光敏采样、输出两路 PWM 控制电机,资源富余但不浪费,开发资料也多到让人安心。这套东西适合刚把 32 位机玩熟的工程师,也适合给光伏电站做低成本跟踪控制器——不依赖云端,本地闭环就能干活。

2. 先立住控制逻辑:从光敏传感器到逐日算法的完整链路

2.1 逐日系统两种常见方案:光电跟踪与视日轨迹跟踪

逐日跟踪的核心是怎么知道太阳在哪。业内最常见的是两条路:光电闭环和视日轨迹开环。

光电跟踪的做法是在一块隔光板上装三到四只光敏电阻或光电二极管,板子朝向太阳时各路光照均衡,一旦偏了,某一路的电压明显高于另一边,控制器拿这个偏差去驱动电机追平。优点是反馈直接,阴天多云时也能大致朝最亮方向转;缺点是会被反射光干扰,比如旁边有水面或白色建筑时容易追着反光跑。

视日轨迹跟踪则是用经纬度和时间算出太阳高度角与方位角,按公式推电机要走的步数,属于开环。优点是全天稳定,不受环境光干扰;缺点是安装时要标定水平角和正北方向,而且云层遮光时它仍然按当前的轨迹走,有时候会对着乌云发呆。

我一般推荐混合策略:晴天用视日轨迹做粗定位,每 15 分钟校准一次;光电传感器做细纠偏,只在偏差超过回差阈值时才启动电机。这样既避免光电方案在正午附近反复震荡,又补上了轨迹算法因安装倾斜产生的累计误差。

2.2 三路光敏传感器怎么接、怎么读:ADC采样与比较逻辑

逐日控制器最少需要三路光敏信号:东、西、南(或上、下、左、右,看你的支架自由度)。如果只做水平方位角跟踪,两路东西向就够了;做双轴的话,还需要仰角通道。我用的是贴片光敏电阻配合 10kΩ 分压电阻,一端接 3.3V,另一端串联电阻到 GND,中间抽头进 STM32F103 的 ADC 引脚。

选择 ADC 引脚时要注意:STM32F103 的内部 ADC 是 12 位的,转换结果存在 16 位寄存器里,左对齐还是右对齐要心里有数。我用的是右对齐,读取时直接ADC1->DR & 0x0FFF就行。光敏电阻的阻值随光照变化不是线性的,所以代码里不需要把 ADC 值换算成物理照度,直接比较三路的原始数值即可。

比较逻辑不能做成“哪边亮就往哪转”的死逻辑,否则正午时东西两路的差值很小,电机会频繁启停。我要在代码里加一个回差阈值,比如亮度差大于 200 个 ADC 码才动作,小于 50 就停机等待。这个阈值既避免电机抖动,也省电。

2.3 电机与限位:步进电机或直流减速电机怎么选

逐日系统的执行机构有两种主流选择。步进电机配合细分驱动器适合精密跟踪,能精确控制每次转动的步数,但价格高、功耗大,户外长期工作还要考虑散热。直流减速电机便宜皮实,用 H 桥配合 PWM 控制转速,靠限位开关做终点保护,是我在小型太阳能逐日板上用得最多的方案。

选电机时主要看扭矩和自锁能力。太阳能板在风大时会对传动机构形成很大的反向力矩,直流减速电机如果蜗轮蜗杆结构自带自锁最理想;如果是普通齿轮箱,控制器断电后板子可能被风吹转。我用的是 12V 直流减速电机,带 1:60 减速箱,配合两路继电器或 H 桥芯片,驱动电流 1A 级别,STM32F103 的 GPIO 直接推不动,必须加三极管或者 MOS 管放大。电机供电和逻辑供电要分开,我吃过一次亏:直接共用一个 5V 电源,电机一启动,ADC 采集值就跳得一塌糊涂。

限位开关是必须的,不仅防止机械撞限位,还能在断电重启时让控制器“归零”。软件里要记录当前角度,但掉电后丢失,所以每次上电先让电机慢速转到限位开关触发位置,以此为原点。这个归零动作看起来简单,布线和防水却要专门处理,不然户外几个月后微动开关就锈死了。

3. 用STM32F103跑通逐日控制:最小系统、CubeMX工程与定时器PWM

3.1 最小系统三板斧:时钟、复位、供电(5V转3.3V别踩坑)

STM32F103 最小系统看着简单,但少了任何一个元件都会让板子“薛定谔地工作”。电源部分我直接用 AMS1117-3.3 把 5V 转 3.3V,注意输入输出电容——输入 10uF 钽电容,输出 22uF 陶瓷电容,离芯片引脚越近越好。有人问为什么不用 1117 的典型 1uF 输出电容,实测在电机启停的浪涌下容易复位,大一点稳妥。

复位电路用 10kΩ 上拉电阻加 0.1uF 电容到地即可,NRST 引脚内部已有上拉,但外部再补一个更抗干扰。BOOT0 必须接 10kΩ 下拉到 GND,BOOT1 可以悬空——很多第一次画板的人把 BOOT0 悬空,烧录时正常,复位后却偶尔进不了主程序。

时钟电路是经常翻车的地方:8MHz 晶振配两个 20pF 负载电容,走线要尽量短且对称。用内部 HSI 时钟虽然能省两颗电容,但串口波特率会偏,PWM 频率也不准,所以我坚持外部晶振。用 Keil MDK 做 ST-Link 调试时,如果程序跑飞或进不了调试,先量晶振引脚波形,八成是电容虚焊或晶振体质量问题。

5V 转 3.3V 这个环节,还要注意 ADC 的参考电压。STM32F103 的 VDDA 和 VREF- 引脚如果不接,内部会默认供电,但电机开启时电压跌落会导致采样值漂移。我建议把 VDDA 单独用一个 LC 滤波接 3.3V,VREF+ 接一个 100nF 电容到地,这样 ADC 数值稳定得多。

3.2 CubeMX定时器3生成PWM:频率与占空比怎么设

用 STM32CubeMX 配置定时器 3 生成两路 PWM 是最直接的路子。打开 CubeMX,选芯片型号 STM32F103C8T6,在 Pinout 视图里把定时器3 的通道1(PA6)和通道2(PA7)设为 PWM Generation CH1/CH2。注意定时器3 挂载在 APB1 总线上,时钟树默认是 72MHz 主频三分频后得到 36MHz。

常见做法是让 PWM 频率落在 500Hz 到 2kHz 之间,太高了电机驱动芯片开关损耗大,太低了大功率电机线圈会啸叫。计算方式很简单:定时器时钟 36MHz,分频系数 PSC 设为 72-1=71,那么计数器时钟就是 500kHz;自动重装载值 ARR 设为 500-1=499,这样 PWM 频率就是 500000/500=1kHz。占空比通过 CCR 寄存器控制,CCR=250 时占空比 50%。

在 CubeMX 里配置的参数就是这三个:Prescaler=71,Counter Period=499,Pulse(即初始 CCR)设成 0。生成代码后,在主循环里用__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, pwm_value)就能动态改占空比。

注意:定时器3 的通道引脚 PA6 和 PA7 默认复用为 SPI 功能,CubeMX 里选择 PWM 后它会自动配置 GPIO 为复用推挽输出,但在老版本固件库里,需要手动调GPIO_PinAFConfig。如果你是从寄存器裸奔过来的,留意别把 PA6 当普通 GPIO 操作了。

3.3 串口打印调试:裸机状态机与日志

逐日系统调试时看不到电机转速,也看不到 ADC 数值,串口打印是唯一的“眼睛”。STM32F103 的 USART1 挂在 APB2 总线上(72MHz),配置波特率 115200、8 位数据、无校验、1 停止位。CubeMX 生成代码后,重定向fputc到 USART1,然后就能用 printf 打印日志。

我通常会定义一个简单的状态机日志函数,把当前工作状态、ADC 三路原始值、PWM 占空比、电机方向都打出来,格式用逗号分隔,方便直接用串口助手画波形或导入 Excel 分析。比如:

printf("state=%d, adc_e=%d, adc_w=%d, adc_s=%d, pwm=%d, dir=%d\r\n", state, adc_east, adc_west, adc_south, pwm_value, motor_dir);

打印的时间点放在主循环末尾,加一个软件延时——用DWT->CYCCNT或者定时器计时,控制日志间隔 200ms 一条。别用HAL_Delay来测量精确时刻,它本身会阻塞。

串口打印的坑主要是重定向问题。GCC 环境下要自己实现_write,Keil 下要勾选 MicroLIB,否则 printf 编译过了但串口没输出。我一般建议直接写一个UART_SendString,绕开半主机模式,这样不管哪个工具链都稳定。

4. 代码骨架:ADC采集、PID/比较算法与电机驱动

4.1 ADC多通道采样与滑动滤波

ADC 是逐日系统的感官入口,但光敏电阻信号本身就带噪,电机 PWM 一开,电源上的纹波直接串进采样端。所以我不用单次采样,而是用循环采样加滑动滤波。

在 CubeMX 里把 ADC1 的 IN0、IN1、IN2 三个通道配置为 Scan Mode、Continuous Conversion Mode,采样时间设成 55.5 周期(最慢档),这样抗噪性能最好。生成代码后,启动 DMA 循环采集三个通道的数据到数组里:

#define ADC_BUF_SIZE 16 uint16_t adc_raw[3][ADC_BUF_SIZE]; // 三路各存16个历史值 HAL_ADC_Start_DMA(&hadc1, (uint32_t *)dma_buf, 48); // 3通道x16次

注意 DMA 缓冲长度必须等于通道数乘采样次数,我在第一次跑的时候只给了 3,结果 DMA 直接数组越界,程序跑飞。DMA 传输完成后在回调里做一下数据处理,取最新 16 次的中位值再平均,不是简单的算术平均——中位值能干掉电机尖峰引起的离群点。

代码里我维护一个环形缓冲,只保留最近 16 个点。中位值平均滤波的好处是既能滤掉脉冲干扰,又不会像纯滑动平均那样让阶跃响应变慢。太阳被云挡住时,ADC 值变化是缓慢的;电机刹车时,瞬间跳变是剧烈的。这两类信号在中位值滤波器面前分得很清楚。

4.2 偏差方向判断与回差控制

逐日算法不复杂,本质是比大小。水平轴:东和西两路 ADC 值相减,差值为正说明东边亮,板子要往东转;差值为负往西转。垂直轴:南和北或上和下相减,决定仰角电机方向。

但直接比大小会引发严重的抖动问题。太阳在正午前后 1 个小时内,东西方向照度差不大,ADC 值在阈值附近反复横跳,电机跟着频繁换向,齿轮箱很快就磨损了。我的解决方法是引入回差控制(滞回比较):

#define HYSTERESIS 200 // 回差阈值,单位ADC码 #define DEAD_ZONE 50 // 死区,避免微小波动触发 int diff_east_west = adc_east - adc_west; if (diff_east_west > HYSTERESIS && last_dir != EAST) { motor_drive(EAST, PWM_LOW); last_dir = EAST; } else if (diff_east_west < -HYSTERESIS && last_dir != WEST) { motor_drive(WEST, PWM_LOW); last_dir = WEST; } else if (abs(diff_east_west) < DEAD_ZONE && motor_running) { motor_stop(); last_dir = IDLE; }

逻辑说明:last_dir记录上一次动作方向,防止电机已经在朝东转时,diff 轻微回落就立刻停转,这样能保证每次动作有最小持续时间。PWM_LOW是低速档,比如占空比 30%,电机慢转时惯性小,到了目标位置更容易停准。

为什么不用 PID?因为被控对象是电机转速和角度,系统是积分型对象,纯 P 控制就会有振荡,PID 参数调起来很玄学,而在户外的光照条件下对象特性还会变。回差加死区虽然粗糙,但户外现场可靠性远高于花哨的算法。如果真要做到精细,可以用位置式 PD,输出叠加到 PWM 占空比上,但至少我做过三个项目,简单回差控制就够了。

4.3 电机驱动:PWM输出与方向引脚

直流减速电机的驱动,我用的是双 H 桥芯片(比如 L298N 或 TB6612),STM32F103 的 PA6/PA7 输出 PWM 接到使能引脚,方向用两个 GPIO 控制。TB6612 比 L298N 的压降小,适合 12V 电机,不需要额外散热片,我推荐用它。

驱动代码最好封装成一个函数,内部做互斥逻辑,防止两个方向同时使能:

void motor_control(uint8_t ch, uint8_t dir, uint8_t duty) { if (dir == EAST) { HAL_GPIO_WritePin(IN1_GPIO_Port, IN1_Pin, GPIO_PIN_SET); HAL_GPIO_WritePin(IN2_GPIO_Port, IN2_Pin, GPIO_PIN_RESET); } else if (dir == WEST) { HAL_GPIO_WritePin(IN1_GPIO_Port, IN1_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(IN2_GPIO_Port, IN2_Pin, GPIO_PIN_SET); } else { HAL_GPIO_WritePin(IN1_GPIO_Port, IN1_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(IN2_GPIO_Port, IN2_Pin, GPIO_PIN_RESET); } __HAL_TIM_SET_COMPARE(&htim3, ch, duty); }

命令除了方向还有 PWM 占空比,这个 duty 要加斜坡。我见过直接给 100% 占空比的板子,电机轴瞬间大扭矩,太阳能板的安装支架都会被扭变形。正确做法是 500ms 内从 0 慢慢加到目标占空比,每 10ms 加 2 个点。

电机停止时最好加 100ms 的反向刹车脉冲。直流电机惯性大,直接停 PWM 后板子会因惯性冲过头,撞击限位或产生跟踪过冲。刹车脉冲就是把方向反过来,占空比 30%,持续 100ms,然后再发停止。

5. 避坑/常见问题:STM32F103逐日系统实战排查记录

5.1 现象:ADC值漂移,太阳不动电机乱转

户外实测时遇到过最诡异的问题:板子朝东固定不动,但日志里 ADC 值一直在缓慢爬升,电机转一会在西边停,过一会又往东追。用万用表量光敏电阻两端电压,看起来正常,但单片机的采样值却逐步走高。

原因有两个:一是板子发热后,AMS1117 的输出电压会从 3.30V 漂到 3.28V,而光敏电阻的分压参考是同一个 3.3V,理论上不会变,但 ADC 的参考电压 VREF+ 直接连的 VDDA,纹波一大,采样值就跟着晃。二是更隐蔽的:电机驱动继电器的线圈电源和逻辑电源共地,继电器吸合瞬间,地线上出现几十毫伏的压降,ADC 的 VSSA 与主地不同。

解决:把继电器供电换成单独 5V 隔离电源,或者用固态继电器替代机械继电器;VREF+ 加一级 RC 滤波,R=10Ω,C=10uF,截止频率约 1.6kHz,能把高频纹波滤掉大半。之后 ADC 漂移量从 ±80 码降到了 ±10 码。

5.2 现象:定时器3 PWM没输出,万用表测PA6始终低电平

CubeMX 生成的工程编译下载后,逻辑应该没问题,但 PA6 量不出波形。查了很久发现是 CubeMX 里虽然选了定时器3 的 PWM,但定时器3 的时钟源没有使能——在 Clock Configuration 页面里,APB1 定时器时钟被设置为默认的 36MHz,但固件库初始化时并没有强制开定时器3 的时钟。

更深层的原因是 CubeMX 版本差异:新版本自动调用__HAL_RCC_TIM3_CLK_ENABLE(),老版本则需要手动在MX_TIM3_Init()前面加上这一句。排查方法是读 RCC 寄存器,或直接看生成的代码里有没有。我遇到过芯片本身 PA6 复用被另一个外设抢占了,比如 SPI1 默认也映射在这两个引脚上,CubeMX 里要确认没有其他外设同时占用。

解决:检查tim.c里的MX_TIM3_Init()是否被调用,检查main.c里是否调用了HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1)。PWM 配置了但没启动,输出就是低电平,这个最容易漏。

5.3 现象:一上电电机就转到底,连板子都没启动

有一次给客户装了一套,每次上电,电机就像疯了一样往一个方向转,直到限位开关咔嗒一声才停。当时怀疑是代码逻辑问题,但单独给 STM32F103 供电时控制信号正常,电机就是不动;合上电机电源后立刻乱转。

定位到是 GPIO 初始化之前的电平问题:STM32F103 在复位期间所有 GPIO 是浮空输入状态,而驱动板 IN1/IN2 引脚内部没有下拉电阻。此时IN1和IN2悬空,H 桥芯片内部逻辑电平不确定,有一半的概率会把电机驱动起来。STM32F103 程序加载完成还要几十毫秒,这期间电机已经转了。

解决:硬件上在 IN1/IN2 对地各加一个 10kΩ 下拉电阻;软件上在系统初始化第一件事,也就是 GPIO 还没配置前,先禁止所有外设时钟,把用到的引脚尽早初始化为推挽输出并输出低电平。另外驱动板的 PWM 使能引脚也要加下拉,防止复位期间 PWM 输出高电平。

5.4 现象:串口打印乱码,全是“烫烫烫”一类

串口助手收到的数据是乱码,但示波器看波形和数据正常。这类问题多半是波特率不匹配,而波特率不匹配的根源在时钟配置。STM32F103 的 USART1 挂在 APB2 上,如果固件里把系统时钟从 72MHz 调成了 8MHz(比如误用 HSI),CubeMX 生成的波特率参数是按 72MHz 算的,实际 8MHz 下的 115200 就偏了 8 倍左右,成了 14400,串口当然乱。

另外 STM32F103 的 PLL 倍频系数在system_stm32f10x.c里定义,如果是从旧工程拷贝过来,可能还是 9 倍频的旧代码,与 CubeMX 生成的配置不一致。解决:直接用 ST-Link 的 SWD 接口读出当前运行频率,或者在代码里打印SystemCoreClock的数值,看是不是 72000000。如果不对,检查外部晶振是否是 8MHz,以及RCC_CFGR的 PLLSRC 和 PLLMUL 设置。

还有一个冷门坑:串口工具选择的“解码方式”如果是十六进制显示,文本就全变乱码。我调试逐日系统时习惯先发一个固定的hello字符串,用示波器量波特率,确认无误再谈别的。

5.5 现象:Keil MDK 工程打不开,STM32F103 芯片选不到

在 Keil MDK 里新建工程,芯片列表找不到 STM32F103C8T6,这是 pack 包没装好。STM32F103 属于 STM32F1 系列,需要安装 Keil.STM32F1xx_DFP 这个 pack。老版本 Keil MDK4 不需要 pack,但 MDK5 以后必须在线安装或手动导入。

我推荐手动下载 pack 包,在 Keil 的 Pack Installer 里选择 ST 目录,找到 F1 系列最新版,安装后重启 Keil。如果在线装不上,去 ST 官网或 Keil 官网下离线包,双击即可。装完后还要注意 MDK 版本,太老的 MDK(比如 5.20 之前)对较新 pack 兼容性差,建议至少 5.30 以上。

调试时 ST-Link 识别不到芯片,除了接线问题,还要检查 Keil 的 Flash Download 选项里有没有选中正确的编程算法(STM32F10x Medium-density Flash),如果没有,下载时报错 No Algorithm found。这是很多人第一次用 ST-Link 时被卡住的地方。

6. 最后一块板子:从样机到长期运行的几个实用技巧

样机在台架上转了两周没问题,真装到屋顶才发现问题多出在没人想的地方。

第一件事是给 ADC 采样加看门狗。独立看门狗不太适合逐日系统——电机正常转动时单圈耗时可能超过喂狗周期,设计不当会让系统误复位。我改用窗口看门狗或简单的软件看门狗:在主循环里维护一个时间戳,如果超过 5 秒没有成功执行完一次“采样-决策-驱动”循环,就强制重启。这能有效防止程序卡死在某个电机驱动的死循环里。

第二件事是断电记忆。STM32F103 没有 EEPROM,但内部 Flash 可以用来存角度数据。我会在每次电机停止后把当前角度写进 Flash 的最后一个扇区,上电时读回来,配合限位开关归零。频繁擦写会损耗 Flash,所以只在角度变化大于 1 度时才写,不要每循环都写。这个习惯帮我躲过了无数次户外调试时断电丢角度的返工。

第三件事是看 PWM 占空比对电机温度的影响。低速大扭矩时电机发热很厉害,我调试时发现占空比 30% 持续 10 分钟,电机外壳就能到 70 度。后来把低速档改成 20% 加间歇驱动——转 900ms 停 300ms,发热降了一半,跟踪精度没受影响。户外设备上可靠比速度更值钱。

这套系统的验证方法其实很土:拿一个手电筒模拟太阳,从东往西慢慢移动,看板子是否平滑跟随、有没有来回震荡。手电筒光强变化梯度大,能暴露出 ADC 阈值设置不合理的毛病。晚上再用天空背景光测试,确认不会出现“乱追亮处”的情况——周围环境光不均匀时,传感器阵列偶尔会把路灯也当成太阳。

我自己的教训是:永远不要省限位开关电路里的那两颗电阻,永远不要直接用板载 LED 来判断 PWM 输出——LED 在高频 PWM 下几乎看不出明暗,必须用示波器或逻辑分析仪确认波形。逐日系统本质上是一套慢速位置的闭环,调试它最需要的耐心是等,等太阳,也等电机停下来。希望这些踩坑和参数能帮你少走几趟弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询