做嵌入式的人应该都有印象,路灯这个题目在毕业设计和电子竞赛里几乎年年出现。以前常见的做法就是光控加时控:天黑开灯、天亮关灯,最多再加一个时钟控制深夜半功率运行。但这两年题目风向变了,“基于 STM32 的智能低碳路灯控制系统的设计”这类题目频繁出现,核心不再只是“能亮”,而是“会省”——用传感器感知环境,让路灯该亮的时候亮、该暗的时候暗、有人经过的时候才全功率输出,把每一度电花在刀刃上。这篇文章就是基于我自己做这类项目的完整过程,从硬件选型、电路设计、PWM 调光算法到现场调试和踩坑记录,一次性讲透。
这套系统适合谁来参考?如果你正在做 STM32 相关的毕业设计、智能硬件课程设计,或者想给老家院子、社区小道做一套低成本的自动照明,这篇内容可以直接拿来用。我会把方案选型逻辑、关键参数计算、STM32CubeMX 配置步骤和实际调试中遇到的坑都写出来,读完你不只会搭一套能跑的原型,还能理解每个环节为什么要这么做。
1. 项目概述与整体设计思路
1.1 需求拆解:“低碳”到底体现在哪
拿到题目别急着画原理图,先想清楚“智能”和“低碳”这两个词分别对应什么功能。我见过不少同学一上来就堆传感器,超声波、红外、雨滴、温湿度全往上挂,最后代码逻辑混乱,调试到崩溃。实际上智能路灯的核心需求可以拆成三块:
第一是“按需照明”。路灯不需要始终以 100% 功率运行。夜间车流量、人流量低时,保持一个较低的基础亮度(比如 20%~30%),当检测到行人或车辆靠近时,自动提高到 100%,人走之后再延时降回。这就是低碳的关键——用传感器代替“24 小时全功率”的粗暴方式,实测节电率能达到 40%~60%,取决于使用场景。
第二是“环境自适应”。天亮自动关灯、天黑自动开灯、阴雨天提前开灯,这些都需要环境光照采集。单纯用光敏电阻做阈值判断也能跑,但要做好滤波和滞回,否则傍晚光线稍微波动灯就会频繁闪烁。
第三是“可靠和可维护”。系统需要具备基本的异常保护(过流、过热、传感器失效)和现场调试手段。否则灯坏了自己不知道,维护成本比省下来的电费还高。把这些需求列成表格,硬件选型和软件架构就清晰了:
| 需求模块 | 功能描述 | 典型实现方案 |
|---|---|---|
| 环境感知 | 检测环境光照、判断白天/黑夜 | 光敏电阻 + ADC 或 BH1750 数字光照传感器 |
| 人体感应 | 检测行人/车辆接近 | 热释电红外(PIR)、微波雷达、超声波 |
| 照明驱动 | 调节 LED 亮度 | PWM 控制 MOS 管实现恒流调光 |
| 状态显示 | 本地显示工作状态/光照值 | LCD1602/OLED 显示屏 |
| 调试接口 | 参数修改、数据监控 | 串口 / 蓝牙模块 |
| 电源管理 | AC-DC 降压、MCU 供电 | 隔离电源模块 + 线性稳压 |
1.2 主控选型:为什么用 STM32 而不是 51 或 Arduino
主控是这套系统的决策中心。用 51 单片机也能做,但当你需要多个定时器同时输出 PWM、ADC 多通道采集、串口调试,再加上后面可能要扩展无线通信模块,51 的资源就很紧张了。Arduino 开发快,但脱离裸机寄存器层面的理解,做不了深度的低功耗设计,而且在论文和答辩里技术含金量也偏低。
STM32 的理由很清楚:外设丰富、价格便宜、生态成熟。以最常见的 STM32F103C8T6 为例,主频 72MHz,三个定时器都能输出 PWM,两个 ADC 各有 10 个通道,串口、I2C、SPI 齐了,价格十几块钱,资料满天飞。整个系统跑下来 Flash 占用不到 40KB,RAM 占用不到 10KB,绰绰有余。如果你手头只有 STM32F407 或者 G4 系列,也可以直接迁移,核心逻辑不变,只是外设初始化函数名略有差异。
选型时还要考虑一点:需要不需要 RTOS?我的看法是,如果只有光敏检测、PIR 检测、PWM 调光这三件事,裸机状态机足够,不需要上 FreeRTOS。上系统反而引入任务调度、优先级分配的复杂度。但如果你要同时跑 ESP8266 联网、LCD 刷新、多个传感器轮询,未来还要做物联网网关,那用 FreeRTOS 划分任务会省心很多。建议先裸机把逻辑调通,再考虑移植 RTOS,不要一上来就追求架构的“高级感”。
1.3 整体架构:从墙面到灯珠的完整链路
这套系统的数据流可以这样理解:光照传感器和人体感应模块把环境信息变成电信号,STM32 的 ADC 和 GPIO 中断负责采集;MCU 根据当前亮度、时间和感应状态,决定输出多大的 PWM 占空比;PWM 信号驱动 MOS 管,控制流过 LED 灯串的电流大小,从而改变亮度。整个过程形成一个闭环,只不过人眼是最终评判者。
电路拓扑上,我推荐模块化设计。电源板、驱动板、传感板、主控板分开,中间用排针或端子连接。这样调试时哪儿坏了拆哪儿,不用整块重新打样。很多同学喜欢把原理图画成一个“大杂烩”,看着很完整,其实出了故障根本无从下手。模块化的代价只是多几根线,收益是排查效率成倍提升。
2. 硬件电路设计与关键模块
2.1 电源架构:别让 MCU 和 LED 抢电
路灯系统里最容易被忽视的是电源设计。LED 灯串功率通常 10W~30W,工作电压 12V 或 24V,而 MCU 只需要 3.3V。如果一个电源同时给 LED 和 MCU 供电,LED 启动瞬间的电流冲击会导致 3.3V 电压跌落,轻则屏幕乱码,重则 MCU 死机重启。
正确的做法是两级供电:输入端用 AC-DC 隔离电源模块(比如 220V 转 24V,功率按 LED 功耗的 1.5 倍选型),24V 直接供给 LED 驱动部分;同时 24V 经过 DC-DC 降压到 5V,再通过 LDO(如 AMS1117-3.3)降到 3.3V 给 MCU 和传感器。DC-DC 选型上,MP1584 和 MP2307 都便宜好用,注意纹波控制在 50mV 以内,否则 ADC 采光敏电压时会引入周期性干扰。
电源还有一个细节:LED 驱动部分要加 LC 滤波或大电解电容,否则 PWM 的开关纹波会沿着地线耦合到 MCU 的 ADC 参考电压。我在调试时遇到过光照值每隔几秒跳变一次,最后发现是 PWM 频率恰好和 ADC 采样周期产生差拍干扰,给驱动电路的地单独走线、单点接地后问题消失。模拟地和数字地分开布,是这类系统稳定运行的基础。
2.2 调光驱动:PWM 频率怎么选
LED 调光有两种常见路线:线性调光和 PWM 调光。线性调光通过改变 MOS 管的导通程度来控制电流,电路简单,但 MOS 管上压降大,发热严重,效率低。PWM 调光通过高频开关,让 LED 以一定的占空比导通,平均电流决定亮度,MOS 管工作在开关状态,损耗很小,是低碳方案的首选。
PWM 频率的选择有两个约束。低于 1kHz 时人眼能感知频闪,长时间在这样的灯光下眼睛容易疲劳;高于 20kHz 时驱动电路里的电感电容会产生音频啸叫,而且 MOS 管的开关损耗随频率上升而增加。我实际测试下来,10kHz 到 15kHz 是比较舒服的范围。用 STM32 定时器产生这个频率很简单:比如 72MHz 时钟,预分频 72,计数器周期 100,输出频率就是 72MHz / 72 / 100 = 10kHz,分辨率 1% 步进,够用了。
驱动电路拓扑上,我推荐低边 N-MOS 驱动。N-MOS 价格低、导通电阻小,放在 LED 灯串的负极侧,源极接地,栅极由 STM32 的 PWM 信号直接驱动(加一个 10Ω 电阻限流),漏极接 LED 负极。这种接法不需要电平转换,MCU 的 3.3V 就能完全开通 MOS。选 MOS 管时注意三件事:Vgs 阈值要低于 3.3V(比如 AO3400,Vgs(th) 典型 1.5V)、导通电阻尽量小(mΩ 级别)、散热要跟上。如果驱动 1A 以上的电流,建议用两个 MOS 并联降低发热,或者在栅极加一个三极管缓冲级。
2.3 光照采集:光敏电阻方案还是数字传感器方案
光照检测是“环境自适应”的基础。最简单的方案是光敏电阻(光敏电阻又称光依赖电阻,阻值随光照强度变化),配合一个 10kΩ 固定电阻组成分压电路,分压点接 STM32 的 ADC 引脚。光照强时阻值小,ADC 读数高;光照弱时阻值大,ADC 读数低。
光敏电阻方案的优点是便宜(几分钱一个)、电路简单,缺点是非线性明显、一致性差、响应速度慢,而且不同批次的产品差异很大,导致每块板子的阈值都要单独标定。如果要做单台原型,光敏电阻完全够用;如果要批量装配,我建议直接上 BH1750 数字光照传感器。BH1750 通过 I2C 接口输出 1~65535 lx 的照度值,内部自带校正,精度一致性都好很多,价格也就两三元。I2C 上拉电阻用 4.7kΩ,地址引脚拉高或拉低选地址,接线就 SCL、SDA、VCC、GND 四根线。
无论用哪种方案,ADC 采集都要做软件滤波。我常用的方法是连续采 10 次去掉最大最小值取平均,同时配合一个 50ms 的采样间隔。这样能滤掉大部分电源纹波和光照瞬时波动,又不至于让响应变得太迟钝。环境光在黄昏时变化速率很快(每秒钟可能下降几十 lux),平均滤波不会影响开灯判断的即时性。
2.4 人体感应:PIR 的预热和误触发必须处理
人体感应用得最多的是热释电红外传感器(PIR),常见的型号是 HC-SR501。它能检测人体发出的红外辐射,探测距离 3~7 米,角度约 110 度,价格十来块钱,非常适合路灯应用。
PIR 有几个必须知道的特性。第一是上电后需要 30~60 秒的预热时间,期间输出信号不稳定,容易误触发,软件上要做一个“启动忽略期”。第二是感应有盲区,人静止不动时 PIR 不会输出,因为热释电传感器只对温度变化敏感;如果用来检测排队的人群,效果不好,但路灯场景检测走动的人流完全没有问题。第三是灵敏度调节和延时调节各有一个旋钮,出厂默认延时可能长达几十秒,做路灯时要把延时调到最短(几秒左右),把“人走了之后多久降亮度”的决策权交给 MCU,而不是交给模块本身的硬件延时。
如果你对感应可靠性要求更高,可以换成微波雷达模块(比如 RCWL-0516,灵敏度比 PIR 高,不受热源干扰),或者用 HC-SR04 超声波模块做近距离检测。超声波的好处是可以测出距离,配合 PWM 实现“越近越亮”的渐进式调光,但超声波在户外受温度、风噪影响较大,我个人的建议是:路灯场景 PIR 或微波雷达够用,追求精确距离控制再上超声波。
3. 软件逻辑与核心算法
3.1 用状态机管理整个运行流程
软件架构是整个系统“智能”程度的体现。我见过很多同学把所有的判断逻辑堆在 while(1) 里,写着写着就成了一坨分不清先后次序的 if-else。路灯这种运行场景非常适合用状态机:白天的状态、黄昏状态、夜间正常状态、夜间有人状态、异常保护状态,状态之间只有有限的转换条件,逻辑清晰,也方便扩展。
比如我的状态定义如下:
typedef enum { STATE_DAY = 0, // 白天待机,灯灭 STATE_DUSK = 1, // 黄昏过渡,灯基础亮度 STATE_NIGHT = 2, // 夜间正常运行,无人低亮 STATE_NIGHT_ACT = 3, // 检测到人,全亮度 STATE_FAULT = 4 // 传感器异常,按时间策略运行 } SysState_t;状态转换条件:ADC 光照值低于“开灯阈值”且维持 3 秒,从 STATE_DAY 切到 STATE_DUSK;光照值高于“关灯阈值”且维持 5 秒,从其他状态切回 STATE_DAY;夜间状态检测到 PIR 高电平,进入 STATE_NIGHT_ACT;STATE_NIGHT_ACT 状态下 PIR 连续 2 分钟无触发,回到 STATE_NIGHT。每个状态的持续时间用“累计时间”而不是“延时函数卡死”来管理,这一点非常重要。
为什么强调用状态机而不是简单 if 判断?因为路灯的运行模式不是线性的,白天可能因为乌云短暂变暗触发开灯,夜间 PIR 可能误触发,传感器可能失效,单一的“判断-执行”逻辑处理不了这些交错情况。状态机把每个时刻系统只处于一个确定状态,任何外设变化只是“事件”,由状态机决定是否响应,鲁棒性直接上一个档次。
3.2 光照阈值与滞回防抖
直接用单阈值判断开灯/关灯会有一个经典问题:当环境光照在阈值附近波动时,系统会在开和关之间反复切换,LED 就像在跳迪斯科,既伤灯又费电。解决办法是使用滞回比较:设置两个阈值,开灯阈值低(比如 50 lux),关灯阈值高(比如 100 lux),中间存在一个“保持区”。当光照从亮变暗,降到 50 lux 以下才开灯;从暗变亮,要升到 100 lux 以上才关灯。这样 50 到 100 之间的任何波动都不会引起状态翻转。
阈值怎么定?要根据安装位置的实际情况。路灯正下方的光照和树荫下的光照差异巨大,不能照搬网上的默认值。我建议用串口把实时照度值打印出来,在傍晚记录一整条变化曲线,再根据曲线选择合理的开灯阈值和关灯阈值。以我实测的一个小区角落为例,傍晚 6 点光照还在 8000 lux,6 点半降到 300 lux,7 点降到 30 lux,所以开灯阈值选 50 lux 合适,而早晨关灯阈值选 100 lux,可以保证清晨天亮了就关掉。
数字滤波也不能少。我用了滑动窗口均值滤波,窗口长度 10,配合 100ms 采样周期,效果很好:
uint32_t readLightLx(void) { uint32_t sum = 0; uint16_t min = 0xFFFF, max = 0; for (int i = 0; i < 10; i++) { uint16_t val = readAdcAverage(ADC_CH_LIGHT); sum += val; if (val < min) min = val; if (val > max) max = val; HAL_Delay(10); } return (sum - min - max) / 8; }3.3 调光策略:从“突变”到“渐变”
确定了状态,接下来是最有意思的部分:PWM 亮度控制策略。最简单的方案是有人 100%、无人 30%,直接切换。但我实际做下来发现这种突变有两个问题:一是人眼在黑暗中突然被强光刺激很不舒服,二是 LED 驱动电路瞬间电流冲击大,影响寿命。
更好的方案是渐变调光:状态切换时不直接赋值占空比,而是用一个“目标占空比 + 步进逼近”的机制。比如目标占空比是 100%,当前是 30%,每个控制周期(50ms)占空比增加 2%,大约 1.75 秒平滑过渡到全亮;反之亦然。人走近时灯缓缓亮起,人走后灯慢慢暗下去,体验好很多。
这里还可以做更精细的控制——根据距离调光。如果用了超声波测距,可以把距离和亮度映射成反比关系:2 米外 40% 亮度,1 米内 100% 亮度。这种“按需供能”的思路才是真正的节能核心,也让答辩时比较有亮点。以下是一个简化的渐变调光代码:
void dimToTarget(uint8_t targetPct) { while (currentPct != targetPct) { if (currentPct < targetPct) { currentPct += 2; if (currentPct > targetPct) currentPct = targetPct; } else { currentPct -= 2; if (currentPct < targetPct) currentPct = targetPct; } __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, currentPct * 10); HAL_Delay(50); } }3.4 可靠性设计:看门狗、低功耗和参数存储
路灯是无人值守设备,可靠性比功能丰富更重要。软件上至少要加两重保障:独立看门狗(IWDG)和关键参数掉电存储。
看门狗的作用是防止程序跑飞后系统“假死”。路灯如果死机,表现可能是灯一直亮着——这既浪费电又严重影响寿命,或者是灯一直不亮——道路安全风险。我配置 IWDG 的超时时间为 2 秒,主循环里必须在 2 秒内至少喂狗一次。注意喂狗的位置必须在主循环的“心跳”处,不能在某个延迟函数内部喂,否则程序卡在死循环里时,看门狗依然被喂饱,失去保护意义。
参数存储方面,我建议把开灯阈值、关灯阈值、夜间基础亮度、渐变步进这些参数固化到 Flash 的末尾扇区,上电时读取校验。 STM32F103 的 Flash 写入需要先擦除整个扇区(1KB 或 2KB),写之前要关中断防止写入被打断(这块在调试时踩过坑,后面详说)。一个简单的做法是把参数打包成结构体,加上 CRC 校验值,写入时先擦扇区再写,读取时校验 CRC,如果校验失败就恢复默认值。
低功耗设计要看使用场景。如果是市电供电,MCU 那点功耗可以忽略,不需要进入 Sleep 模式。如果是太阳能电池供电,就必须考虑低功耗:白天让 MCU 进入 Stop 模式,用 RTC 定时唤醒采样光照,光照低于阈值才全速运行。STM32F103 的 Stop 模式整机功耗可以压到 10µA 级别,配合太阳能板可以做到真正免维护。但注意 Stop 模式下串口下载会失效,调试时要先改配置再下载,否则容易“锁死”。
3.5 如果上 FreeRTOS:任务怎么划分
如果你后面要加 WiFi 模块、远程控制、日志记录等复杂功能,裸机状态机就不够灵活了,换 FreeRTOS 是合理的。任务划分我的建议是:
- ControlTask(最高优先级):定频 100ms 处理状态机和调光逻辑,这是系统的核心,不能被其他任务阻塞;
- SensorTask:周期性采集光照、PIR 状态,通过队列发送给 ControlTask;
- CommTask:处理串口/蓝牙数据,解析远程指令,比如手动设置亮度、修改阈值;
- DisplayTask(最低优先级):刷新 LCD 显示,慢一点没关系。
任务间通信用 FreeRTOS 队列,不要用全局变量加标志位的方式,不然任务多了你会发现数据竞争问题防不胜防。这是一个“以后可能会用”的架构建议,当前项目如果只有 3 个功能点,老老实实裸机就好,不要为了用而用。
4. 实操过程与核心环节实现
4.1 用 STM32CubeMX 初始化工程
新建工程的第一步是用 STM32CubeMX 把外设时钟和引脚配置好。我的引脚分配如下:PA0 接光敏分压点,作为 ADC1_IN0;PB0 接 PIR 传感器输出,作为输入引脚;PA1(TIM2_CH2)输出 PWM 控制 MOS; 串口 1(PA9/PA10)作为调试接口;PA8 接 LCD1602 的 I2C 时钟,PC9 接 I2C 数据(也可以用标准 I2C 引脚 PB6/PB7,看方便)。
时钟树配置:外部 8MHz 晶振,PLL 倍频到 72MHz。ADC 时钟分频到 12MHz 以内(APB2 时钟 72MHz,分频 6 倍就是 12MHz),采样周期设为 55.5 周期,这样 ADC 采样精度比较稳定。
关键是 PWM 配置:选择 TIM2,时钟源内部时钟,通道 2 输出 PWM。预分频 PSC 设为 71,这样定时器时钟是 1MHz;自动重载值 ARR 设为 100,输出频率 10kHz,占空比调节精度 1%。代码里设置占空比用__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, duty);,duty 范围 0~100。注意 ARR 的寄存器是 16 位的,最大 65535,所以这个配置对 10kHz 没问题。
4.2 关键代码段拆解
主循环里我保持一个 10ms 的“心跳”节奏,每次循环做以下几件事:采集光照值、读取 PIR 状态、调用状态机处理函数、执行 PWM 渐变控制、通过串口打印调试信息(只在调试模式打印,正常运行时关掉打印,避免阻塞)。
状态机处理函数是最核心的部分,我用一个processState()函数实现:
void processState(void) { uint16_t lux = readLightLx(); uint8_t pirActive = HAL_GPIO_ReadPin(PIR_GPIO_Port, PIR_Pin); switch (state) { case STATE_DAY: if (lux < OPEN_THRESHOLD_LX && checkHoldTime(3, &dayHoldTimer)) { state = STATE_DUSK; dimToTarget(NIGHT_BASE_PCT); } break; case STATE_DUSK: if (lux > CLOSE_THRESHOLD_LX) { state = STATE_DAY; dimToTarget(0); } else if (lux < OPEN_THRESHOLD_LX) { state = STATE_NIGHT; } break; case STATE_NIGHT: if (lux > CLOSE_THRESHOLD_LX && checkHoldTime(5, &nightHoldTimer)) { state = STATE_DAY; dimToTarget(0); } else if (pirActive) { state = STATE_NIGHT_ACT; dimToTarget(100); } break; case STATE_NIGHT_ACT: if (!pirActive && checkHoldTime(120, &noPersonTimer)) { state = STATE_NIGHT; dimToTarget(NIGHT_BASE_PCT); } if (lux > CLOSE_THRESHOLD_LX) { state = STATE_DAY; dimToTarget(0); } break; } }这段代码里的checkHoldTime是一个很重要的工具函数。它用 HAL 的 tick 计数做计时,不用 delay 阻塞,这样在等待状态转换期间,系统还能响应其他事件(比如串口指令)。实现思路是:第一次调用时记录当前 tick,后续调用判断是否已经过了指定秒数。
4.3 编译下载和串口调试技巧
编译环境我用的 Keil MDK,安装好 STM32F1 系列支持包之后直接编译。如果这一步出现 “No ULINK device found” 或 “Cannot load flash programming algorithm” ,先检查 ST-Link 驱动,再检查工程里 Debug 设置里的 Flash Download 选项是否选对了 64KB 容量。用 ST-Link 下载时,注意如果之前配置过禁用 JTAG/SWD 引脚(某些教程会教你关掉 JTAG 复用),下载器会连不上,解决办法是按住复位键保持在复位状态再点下载,松开复位键让程序趁空隙写入。
串口调试是这套系统最重要的排错手段。我用串口 1 重定向 printf,波特率 115200。实现重定向注意关键的两行:
int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF); return ch; }把这个函数加到工程里,printf就能直接往串口输出。调试时我每隔 1 秒打印一次当前光照值、状态、PIR 状态和目标占空比,看起来像这样:
Lux=234 | State=DAY | PIR=0 | PWM= 0% Lux=87 | State=DUSK | PIR=0 | PWM=30% Lux=43 | State=NIGHT | PIR=1 | PWM=100%通过串口日志,你可以直观看到状态机是否正确流转,阈值设得合不合理一目了然。如果不想一直插线,可以加一个 HC-05 蓝牙模块,把同样的信息发到手机蓝牙调试助手,调参更方便。蓝牙模块在调试阶段非常推荐,因为路灯安装高度通常比较高,拉串口线很麻烦。
4.4 现场测试和参数标定流程
硬件焊好后,先在桌面做功能测试,再把系统装到真实路灯杆上测试。我的测试顺序是:先测 PWM 输出波形(用示波器看占空比是否随代码改变)、再测光照采集值是否正确变化、然后测 PIR 触发响应、最后整体跑状态机。
参数标定部分,重点说开灯和关灯阈值的标定方法。把串口日志打开,站在安装位置附近,从下午 5 点开始记录光照变化,记录到晚上 8 点,得到一条光照-时间曲线。开灯阈值选在“道路上明显需要照明”的时刻对应的光照值,关灯阈值选在“自然光已经足够”的时刻的光照值。我实测小区道路的数据是:开灯阈值定在 50 lux,关灯阈值定在 120 lux,中间 70 lux 的滞回区间足够防止抖动。
PIR 的触发测试也有讲究。人从远处走向传感器,观察串口里 PIR 从 0 变 1 的距离是否满足需求。HC-SR501 模块上的灵敏度旋钮逆时针调降低灵敏度、顺时针提高,很多模块出厂调得过于灵敏,晚上猫狗经过都会触发全亮,这时候要把灵敏度适当调低。延时旋钮要调到最小,让模块自身的输出高电平持续时间尽可能短,这样“多久降亮度”完全交给 MCU 的 2 分钟延时逻辑处理。
4.5 户外安装的防水和散热处理
做桌面原型很简单,但拿到户外会发现很多问题。首先是防水:路灯控制盒至少需要 IP65 防护等级,接线端子要用防水端子,传感器探头要避开积水区域。光敏传感器如果装在金属灯杆内部,测到的是“灯杆内的黑暗”,永远也不会关灯,必须把探头伸到灯杆外部,同时加一个半球形透光罩防止雨水遮挡。
散热方面,MOS 管和 LED 灯珠是主要热源。MOS 管要加足够的散热面积,或者贴在控制盒的金属底座上。连续工作一小时,用手摸 MOS 和 LED 铝基板的温度,如果超过 80°C 说明散热不足,需要加大散热片或者降低基础亮度。我曾经有一版电路用了贴片 MOS 没加散热,在 30W LED 下连续运行半小时后 MOS 温度飙到 90°C,后来换成 DPAK 封装的 AO3400 并贴到铝板上,温度稳定在 55°C,问题才解决。
5. 常见问题与排查技巧实录
5.1 PWM 调光时 LED 出现频闪
现象:用手机摄像头对着 LED 能看到明显的滚动条纹,人眼虽然不觉得闪烁但总觉得灯光“不干净”。
原因排查:先用示波器看 PWM 波形,如果频率低于 1kHz,人眼虽然基本不感知,但手机摄像头因为采样率和 PWM 频率的拍频,就会拍出条纹。我测试时把频率从 1kHz 调到 10kHz,手机摄像头下的条纹就消失了。
还有一个隐蔽的原因:PWM 的占空比调节代码每 50ms 才更新一次比较值,如果更新次数不规律,等效频率会下漂。解决方法是定时器中断里只更新比较寄存器,不做其它耗时操作。另外检查 LED 驱动电路里有没有加电容滤波,过大的电容会让 PWM 波形严重变缓,LED 变成了“模拟调光”,失去 PWM 高频开关的效率优势。
5.2 ADC 采集的光照值跳变、不稳定
现象:串口打印的光照值在傍晚稳定环境下波动超过 ±20%,甚至周期性跳变。
排查思路分三步。第一步检查电源纹波,用示波器测 ADC 参考电压引脚(VDDA)和 3.3V,纹波超过 50mV 就要在电源输出端加 10µF 陶瓷电容和 100µF 电解电容。第二步检查采样时间,ADC 采样周期太短(低于 13.5 周期)会导致内部采样电容充不满,读数偏低且不稳定,我直接用 55.5 周期。第三步检查地线,LED 功率地如果有大电流脉冲,会把噪声耦合到 MCU 地平面,光照值就会随亮度变化而波动,这就是前面说过的“模拟地和数字地单点接地”问题。
5.3 PIR 频繁误触发
现象:晚上没有人经过,串口显示 PIR 频繁变 1,路灯频繁全亮。
常见原因有三个。第一个是 PIR 的灵敏度调太高,轻微的热气流、温度变化都会触发,把灵敏度旋钮逆时针调低一档。第二个是 PIR 的安装位置不对,正对着空调外机、路灯自身的散热孔等热源,需要调整朝向或加装遮光罩。第三个是 PIR 模块的电源纹波干扰,HC-SR501 对电源噪声比较敏感,给它加一个 100µF 电解电容和一个 0.1µF 陶瓷电容并联去耦,误触发概率下降非常明显。
软件上还有一道防线:PIR 高电平必须持续超过 200ms 才认为有效,小于这个时间的脉冲直接忽略。这个去抖逻辑加在状态机入口前,可以滤掉大部分瞬态干扰。
5.4 环境光传感器被路灯灯光自己干扰
现象:系统在夜间运行正常,但路灯亮起来之后,光敏传感器检测到的“环境亮度”反而升高了,导致系统误判为白天关灯,灯灭了环境又变暗,又开灯——恶性循环。
这是路灯项目的经典问题。解决办法有几个:一是光敏探头加装遮光罩,只能看到天空方向的光线,不能看到灯具本身发出的光;二是软件上区分“环境光照”和“自身灯光”的时段逻辑,在夜间状态强制不再通过光照阈值判断“是否天亮”,而是通过时间窗口或 RTC 来判定清晨。我的做法是:夜间 22:00 到凌晨 5:00 之间,即使光照值升高也忽略,天亮判定只允许在 5:00 之后生效。这个方法在路灯控制器里叫“时间窗口封锁”,很实用。
5.5 Flash 写参数时系统卡死或参数丢失
现象:修改阈值参数后断电重启,参数没有保存,或者保存过程会导致程序卡死。
这是一次深刻的教训。STM32F103 的 Flash 写入前需要擦除整个扇区,擦除期间 Flash 不可读,如果此时发生中断(比如某个外设中断里访问了 Flash 里的常量),程序就会卡死。解决办法:写 Flash 前关全局中断,写完再打开。另外,擦除要放在最后,数据先暂存在 RAM,避免在擦除的毫秒级窗口里丢数据。配置参考:
__disable_irq(); HAL_FLASH_Unlock(); FLASH_ErasePage(parameterPageAddr); HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, parameterPageAddr, (uint32_t)¶ms); HAL_FLASH_Lock(); __enable_irq();5.6 问题排查速查表
| 现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| LED 频闪 | PWM 频率过低 | 示波器看频率 | 调到 10kHz 以上 |
| ADC 读数乱跳 | 电源纹波大、地线干扰 | 示波器测 3.3V | 加去耦电容、单点接地 |
| PIR 乱触发 | 灵敏度高、热源干扰 | 短时间置 PIR 断开测试 | 调灵敏度、加延时去抖 |
| 灯一开就关、一关就开 | 光敏被自身灯光干扰 | 观察状态切换时间点 | 加遮光罩、时间窗口封锁 |
| 参数保存后重启丢失 | Flash 写入被打断 | 检查中断开关状态 | 写 Flash 前关中断 |
| 串口打印乱码 | 波特率不匹配 | 检查串口助手设置 | 统一 115200,8N1 |
6. 扩展方向与个人经验总结
6.1 从单灯到组网:RS485 与集中控制器
如果你想把这个项目从“单灯智能”升级成“智慧路灯系统”,第一件要做的不是上云,而是组网。路灯通常是沿道路一字排开,距离几百米,最成熟的方案是 RS485 总线。每个路灯控制盒里加一个 RS485 收发芯片(如 MAX3485),通过双绞线串联,主机轮询读取每一盏灯的状态、发送调光指令。这种情况下,每盏路灯就有了一个独立的设备地址,通过 Modbus RTU 协议通信。STM32 的 USART 做 485 通信非常容易,只是注意方向控制引脚要在发送时拉高、发送完拉低。
更进一步可以用 LoRa 无线模块做远距离星型组网,一盏网关可以管理几十盏灯,免布线,单灯控制效果很惊艳。如果你学校里做物联网创新项目,可以把这套路灯接入一个简单的 MQTT 服务器,用手机 App 远程控制,技术栈从嵌入式延伸到物联网,亮点直接拉满。
6.2 太阳能供电是低碳路线的最终形态
题目既然强调“低碳”,太阳能供电是绕不开的加分项。把路灯做成独立离网系统:太阳能板 + 锂电池 + 充放电管理 + 智能调光。核心选型逻辑是能量守恒:电池容量要能撑过连续阴雨天(比如 3 天),太阳能板功率按日均发电量估算。以 12V 系统、负载 15W、夜间工作 10 小时为例,每晚耗电 150Wh,考虑 0.8 的效率,电池至少需要 200Wh,按 12V 折算就是 16.7Ah。以日均有效日照 4 小时计算,太阳能板功率需要 150Wh / 4h / 0.8 ≈ 47W,取 50W 规格。这套参数是可以直接套用的。
调光策略在电池供电场景下要更激进:深夜无人时段基础亮度可以降到 10%,太阳能板的发电效率在阴雨天只有晴天的 20%~30%,必须靠调光保续航。这个场景下,STM32 的低功耗 Stop 模式就派上用场了。
6.3 我踩过的几个坑和最终体会
这个项目前后迭代了三个版本。第一版在硬件上栽了跟头,LED 驱动和 MCU 共地共电源,结果一开灯 MCU 就复位;第二版软件逻辑太“线性”,环境光照一波动,路灯就频繁开关,后来狠狠心把状态机重写了一遍,稳定性才上来;第三版加入了渐变调光和串口日志,整个系统的“手感”才对了——晚上看路灯慢慢变亮而不是“啪”地猛开,那种体验上的区别非常明显。
如果要给正在做类似项目的朋友一个建议,我会说:先在桌面上把每一路外设单独测通,再合体。不要试图一次性把整块板子跑起来。PWM 波形单独测、ADC 值单独看、PIR 触发单独试,每个模块都可靠之后,再写状态机。硬件调试拼的不是智商,是耐心和方法。
另外,坚持用串口日志记录一轮完整的“傍晚→夜间→清晨”过程。这个数据是后续所有阈值设定和调优的基础,也是你写论文、做答辩展示时最有说服力的素材。路灯这种系统,边界条件多、运行时间长,只有基于真实数据才能把参数调得合理,靠拍脑袋是调不出好效果的。