简介:这是基于STM32的智能路灯控制系统完整设计文档,源于某大学电气工程学院电子设计大赛D题,适合电子竞赛参赛者、嵌入式初学者及STM32开发者参考。文档围绕低成本、高性能、易操作的设计目标,从硬件与软件两方面展开:硬件涵盖单片机最小系统、红外检测执行电路、LED驱动电路、数码管显示及CH340g串口通信模块;软件包含路灯控制、光电检测、时间显示等程序模块,并给出STM32F103RCT6外部中断与定时器实现思路。方案对比中详细比较了STC89C52、STM32F103RB与STM32F103RCT6的优劣,有助于读者理解选型逻辑。压缩包内含1个docx文档,约183KB,内容包含系统方案论证、电路原理图、控制流程图、Proteus仿真测试结果及主要程序代码,结构完整且便于直接查阅。已有1134人学习下载,适合需要快速了解智能路灯系统设计流程或准备电子竞赛方案的人群。
1. 从课设题目到可复制方案:这盏路灯控制系统的真正价值
做电子设计竞赛或者课程设计的人,大概率都遇到过这类题目:要做一个能感知环境亮度、检测行人通过、自动控制灯亮灭的系统。市面上很多方案用STC89C52或者纯逻辑电路堆功能,做完能跑就算赢,但真要加定时开关、串口调参、速度显示这些功能,代码和电路就开始失控。这套完整的智能路灯控制系统,主控选的是STM32F103RCT6,硬件上有红外检测、LED驱动、数码管显示和LCD1602字符屏,软件上把外部中断、定时器、串口指令解析和状态机调度串在一条主循环里;物体经过时靠红外下降沿打断主程序,串口下发"N1 20:30:00"这类指令就能定点开灯,比单纯的光控路灯多了一层可以远程设置的灵活性。这份文档适合三类人:正在做竞赛题、想把课程设计做成实物、或者想了解STM32工程里中断与定时器怎么配合的从业者,下文我会把工程结构、关键代码和调试坑一次拆透。
2. 系统选型与整体设计:为什么是STM32F103RCT6而不是更便宜的51
这个系统的核心矛盾是"响应要及时、逻辑要能扩展"。题目要求检测物体通过后立刻动作,同时还要能显示时间、速度和接收上位机指令,这些需求决定了主控的选型方向,也决定了整个工程的代码组织方式。
2.1 单片机选型对比与最终结论
文档列了三套方案,我把它整理成一张对比表,方便你理解作者的选择逻辑:
| 对比项 | STC89C52 | STM32F103RB | STM32F103RCT6 |
|---|---|---|---|
| 主频 | 12MHz左右 | 72MHz | 72MHz |
| 中断资源 | 6个中断源 | 丰富 | 丰富,支持外部中断EXTI |
| 定时器 | 2个16位 | 3个 | 4个16位定时器 |
| 存储 | 8KB Flash | 128KB Flash | 256KB Flash,48KB RAM |
| 串口 | 1个 | 2个 | 2个USART |
| 适合场景 | 简单逻辑 | 中小系统 | 多外设+中断+指令解析 |
STC89C52周期慢是致命伤,红外检测触发中断后还要快速处理速度计算和时间记录,51在中断里做太多事会导致主循环卡顿;STM32F103RB虽然速度快,但Flash偏小,上LCD1602、数码管、串口指令解析、定时器中断这套组合下来很容易捉襟见肘;F103RCT6的256KB Flash给了很宽裕的余量,而且它的外部中断可以做到每个引脚独立触发,这正好契合红外对射检测的下降沿触发场景。选型结论:在低成本范围内,RCT6是这类多外设、多中断、还要带显示和通信的设备里最稳的选择。
2.2 红外检测模块的工作原理
红外感应部分用的是对射式红外检测方案,而不是热释电人体感应。热释电传感器只能感知人体移动,没法精确给出"物体经过"的时刻,但对射式红外不一样:发射管持续发射红外线,接收管正常收到光时输出高电平;物体从中间穿过时红外线被遮挡,接收端输出立刻跳变产生下降沿。这个下降沿接到单片机的EXTI引脚上,就能触发外部中断。
设计上有一个关键联动:红外检测模块的供电由LED驱动电路控制。环境亮度低的时候给红外模块供电,此时有人经过就能触发开灯;环境亮度高的时候直接切断红外模块电源进入省电模式,这时候无论怎么遮挡红外对射管,路灯也不会响应。这个逻辑避免了白天有人经过时误触发亮灯的问题,同时也省掉了单独测光的传感器成本,用环境光敏电路本身去管理整个检测通道的供电。这类设计在真实路灯节能场景里很常见,相当于把"光控"和"人控"两级闸门串在了一起。
2.3 LED驱动与双路控制逻辑
路灯负载部分设计成两路LED灯组,每路LED的亮灭由两个条件共同决定:时间条件和明暗条件。只有当前时刻落在用户设定的时间窗口内,并且环境光线低于阈值,LED才会点亮。这个逻辑在代码里体现为Judge函数和Control函数的配合,状态标志位State_Flag1、State_Flag2分别记录两路LED是否处于定时开启状态,Control_Flag1、Control_Flag2标记是否进入了可控制区间。
两路LED的好处是可以模拟真实双车道或者人行道双侧路灯的场景,同时也能分别设置不同的定时开启时间。比如1号灯从晚上18:30开启,2号灯从19:00开启,这就是文档里Fixed_time模块干的事。主程序循环里调用Control()完成所有条件判断,这种双路设计对后续扩展很有帮助,要加第三路路灯只需要复制一组标志位和判断逻辑。
3. 硬件电路与引脚分配:从原理图到GPIO的对应关系
很多人拿到这类工程源码,最大的障碍是不知道每个外设接在哪个引脚上。这里我按原工程的头文件和初始化代码,把整个引脚分配表完整梳理出来。
3.1 单片机最小系统与电源设计
STM32F103RCT6属于LQFP64封装,电源部分需要3.3V供电,典型做法是用AMS1117-3.3从5V降压。最小系统电路包含8MHz主晶振、32.768KHz的RTC晶振(如果做时钟)、复位电路(10K电阻+104电容+按键)、以及BOOT0下拉到GND让芯片从Flash启动。文档里提到用CH340g串口通信,CH340g负责USB转TTL,把电脑USB信号转成单片机可识别的UART电平。
这里有一个常见做法值得注意:CH340g模块的TXD接单片机USART1的RX(PA10),RXD接PA9(TX),共地一定要接,否则串口收发会不稳定。烧录时把BOOT0跳线接到1进入ISP模式,写完程序再跳回0,也可以直接用SWD下载器省去跳线操作。
3.2 全工程引脚分配表
根据原工程GPIO初始化代码,引脚分配如下:
| 外设 | 引脚 | 模式 | 说明 |
|---|---|---|---|
| LCD1602数据线 | PA0-PA7 | 推挽输出 | 8位并口数据 |
| LCD1602控制线 | PB5(RS)、PB6(EN)、PB7(RW) | 推挽输出 | 读写控制 |
| 数码管段码 | PC0-PC7 | 推挽输出 | 共阳极段码 |
| 数码管位选1 | PB14 | 推挽输出 | 最低位 |
| 数码管位选2 | PB15 | 推挽输出 | 第二位 |
| 数码管位选3 | PB8 | 推挽输出 | 第三位 |
| 数码管位选4 | PA12 | 推挽输出 | 最高位 |
| 红外输入1 | PC10 | 浮空输入 | 对射管1下降沿 |
| 红外输入2 | PC11 | 浮空输入 | 对射管2下降沿 |
| 红外输入3 | PC13 | 浮空输入 | 备用检测点 |
| 红外输入4 | PC14 | 浮空输入 | 备用检测点 |
| 红外输入5 | PC15 | 浮空输入 | 备用检测点 |
| LED1控制 | PB10 | 推挽输出 | 1号路灯 |
| LED2控制 | PB11 | 推挽输出 | 2号路灯 |
需要特别说明的是PC13、PC14、PC15这三个引脚在F103上默认用于RTC,但在做普通GPIO输入时也是可用的,前提是关闭RTC相关功能。这个工程用它们做了额外的红外检测输入,说明作者对引脚复用处理得干净,没有让RTC和EXTI抢占同一资源。
3.3 红外对射管的硬件接法
红外发射管串联一个100Ω限流电阻后接3.3V或5V电源,注意不同供电电压要重新计算限流电阻阻值。红外接收管(光电三极管)接法更讲究:集电极接3.3V,发射极接一个10K下拉电阻到地,输出信号从发射极引出。正常情况下接收管导通,输出为高电平;红外被遮挡时接收管截止,输出被下拉到低电平,产生下降沿。这个接法决定了信号跳变的方向,正好匹配EXTI配置的下降沿触发模式。
有个细节容易忽略:红外发射管和接收管之间距离拉长后,输出信号会出现毛刺抖动,物体边缘不完全遮挡时可能产生多次跳变。常见做法是在接收管输出端加一个100nF滤波电容,同时在代码里做200ms左右的消抖延时。工程代码里专门写了EXTI_PC10_Config这类初始化函数,如果中断里加了消抖逻辑,那么处理速度测量时要考虑消抖延时会吃掉一部分时间窗口。
4. 程序设计核心:主循环、外部中断、串口指令的协作机制
这套系统的软件架构可以从主函数入口拆开看。主函数干了几件事:配置各外设GPIO、初始化LCD和数码管、设置外部中断、串口初始化为9600波特率、配置TIM3定时器作为时间基准,然后进入一个while(1)大循环。中断负责"感知",定时器负责"计时",串口负责"设置",主循环负责"执行",分工很清晰。
4.1 主函数骨架与状态机
主函数原文较长,我整理成一个精简可读的版本,逻辑等价:
int main(void) { u8 t, len; u8 Time_Set[] = "Set"; u8 Time_Set_Flag = 0; u8 Fix_Flag = 0; u8 *P_Fix_Flag = &Fix_Flag; /* 1. 外设初始化 */ LCD_Data_GPIO_Config(); // LCD数据口 PA0-PA7 LCD_Com_GPIO_Config(); // LCD控制口 PB5/PB6/PB7 Digital_LED_GPIO_Init(); // 数码管段码+位选 LED_GPIO_Config(); // LED1/LED2控制口 LCD1602_Init(); // LCD初始化 delay_init(); // 延时函数初始化 NVIC_Configuration_sys(); // 中断优先级分组 Alarm_Config(); // 报警相关初始化 /* 2. 外部中断初始化:红外对射下降沿 */ EXTI_PC10_Config(); EXTI_PC11_Config(); EXTI_PC13_Config(); EXTI_PC14_Config(); EXTI_PC15_Config(); /* 3. 串口 9600 波特率 */ uart_init(9600); /* 4. TIM3 定时器做时间基准 */ Timer_Init(); Clock_Init(); // 给时钟赋初始时间 LED1(OFF); LED2(OFF); /* 5. 主循环 */ while(1) { Judge(); // 判断环境亮度与时间条件 if(USART_RX_STA & 0x8000) // 串口收到一帧完整数据 { len = USART_RX_STA & 0x3fff; printf("..."); for(t = 0; t < len; t++) { USART1->DR = USART_RX_BUF[t]; while((USART1->SR & 0X40) == 0); } USART_RX_STA = 0; Time_Set_Flag = 1; Fix_Flag = 1; Control_Flag1 = 1; Control_Flag2 = 1; } /* 解析 "Set HH:MM:SS" 时间设置指令 */ if(Strcmp(USART_RX_BUF, Time_Set) > 0 && USART_RX_BUF[0] == 'S') if(Time_Set_Flag) { hour = (USART_RX_BUF[3]-48)*10 + (USART_RX_BUF[4]-48); min = (USART_RX_BUF[6]-48)*10 + (USART_RX_BUF[7]-48); sec = (USART_RX_BUF[9]-48)*10 + (USART_RX_BUF[10]-48) - 1; Time_Set_Flag = 0; } Control(); // 根据标志位执行开灯/关灯 Fix(P_Fix_Flag); // 处理 N1/F1/N2/F2 定时指令 Dis_time(); // 显示时间 Fixed_Show(); // 显示定时时间参数 } }这段代码有两个设计点值得学习。第一,串口接收采用中断接收方式,USART_RX_STA的最高位表示一帧数据接收完成,主循环检测到这个标志后才去解析数据,不会阻塞主循环的执行。第二,所有的标志位在串口收到指令后统一置位,Control()和Fix()函数再根据标志位执行具体动作,这是一种简单的"命令-状态-动作"模式,虽然没有用状态机表,但逻辑划分是清晰的。
4.2 外部中断配置与事件响应
红外对射检测依赖下降沿中断,以PC10为例,标准配置是这样的:
void EXTI_PC10_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; EXTI_InitTypeDef EXTI_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; /* 使能 GPIOC 和 AFIO 时钟 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC | RCC_APB2Periph_AFIO, ENABLE); /* PC10 配置为浮空输入 */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOC, &GPIO_InitStructure); /* 选择 PC10 对应的外部中断线 */ GPIO_EXTILineConfig(GPIO_PortSourceGPIOC, GPIO_PinSource10); EXTI_InitStructure.EXTI_Line = EXTI_Line10; EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; EXTI_InitStructure.EXTI_LineCmd = ENABLE; EXTI_Init(&EXTI_InitStructure); /* NVIC 配置 */ NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); }注意一个容易踩坑的点:PC10、PC11、PC13、PC14、PC15这五个引脚在F103上各自映射到不同的EXTI线,中断服务函数的入口也不同。PC10和PC11会共用EXTI15_10_IRQHandler这个中断入口,而PC13、PC14、PC15分别有独立的EXTI15_10或EXTI9_5入口。你在中断函数里第一件事就是判断EXTI_GetITStatus,对每个引脚分别处理,否则会误触发。文档里每个引脚单独写了Config函数,这种写法虽然看起来重复,但可读性很高,调试的时候也容易定位。
4.3 速度测量与数码管显示
物体通过时经过两个红外对射点,两个下降沿中断分别记录TIM3的计数值,两者相减得到时间差,固定间距除以时间差就是平均速度。数码管显示单位是mm,最大值9999,代表999.9cm,也就是10米以内。
void Digital_LED_Write(u32 Speed) // Speed 单位 mm { if(Speed <= 9999) { /* 千位非零,显示4位 */ if(Speed / 1000) { Dig_1_On; DATA_OUT_DIG = Digital_LED_Table[Speed / 1000]; Delay(0x000fef); Dig_1_Off; Dig_2_On; DATA_OUT_DIG = Digital_LED_Table[(Speed - (Speed/1000)*1000) / 100]; Delay(0x000fef); Dig_2_Off; Dig_3_On; DATA_OUT_DIG = Digital_LED_Table[(Speed - (Speed/100)*100) / 10]; Delay(0x000fef); DATA_OUT_DIG = 0x7f; // 点亮小数点 Delay(0x000fef); Dig_3_Off; Dig_4_On; DATA_OUT_DIG = Digital_LED_Table[Speed - (Speed/10)*10]; Delay(0x000fef); Dig_4_Off; } /* 百位非零,显示3位,最高位熄灭 */ else if(Speed / 100) { Dig_1_Off; Dig_2_On; DATA_OUT_DIG = Digital_LED_Table[Speed / 100]; Delay(0x000fef); Dig_2_Off; /* 其余位类推 */ } /* 十位和个位的分支类似,不足10时显示0.0 */ } }这段代码就是一个典型的数码管动态扫描,4位共阳极数码管在同一时刻只有一位在显示,通过快速轮换产生视觉残留。段码表Digital_LED_Table用的是共阳极编码:0xC0是数字0,0xF9是数字1,0xA4是数字2,依次到0x90是数字9。小数点显示通过段码值0x7f,它只点亮DP段,不影响数字段。Delay(0x000fef)大约几十微秒,扫描频率足够让眼睛看不出闪烁。
这里的逻辑本质上是用"分段判断+除法取余"来做位拆分,虽然是逐位显示的经典写法,但有个性能隐患:Delay用的是空循环,如果主循环里同时还要做LCD刷新和串口解析,数码管扫描会占用大量CPU时间。后面调试到响应变慢时,可以把Delay换成定时器中断里的刷新标志,但这套工程规模不大,空循环也能接受。
5. 联调避坑:五个容易翻车的细节与排查方法
从Proteus仿真到实物联调,这个系统有五个高频坑位,每一个我都见过有人踩进去出不来。下面按"现象→原因→解决"写清楚。
5.1 数码管亮度不均匀,低位明显偏暗
现象:4位数码管显示速度数值时,最右边一位明显比左边暗,拍照时能看到闪烁条纹。
原因:位选扫描顺序是Dig_1到Dig_4逐个导通,每一位点亮的时间占比只有25%。前面几位刚熄灭马上轮到后面,视觉残留时间不足;而且空循环Delay在不同位数之间耗时不均,低位的显示时间更短。
解决:把Delay值统一加大到0x002000以上,保证每一位都有足够的点亮时间;更稳的做法是把扫描从"逐位点亮"改成"每2ms刷新一位",用TIM3的Update中断里置一个标志位,主循环检测到标志位后刷新一位,这样每一位的显示时间完全均匀,亮度就一致了。
5.2 红外对射长时间工作后温度漂移
现象:傍晚刚通电时红外检测灵敏,人一过就触发;运行一两个小时后,需要凑得很近才触发,甚至不触发。
原因:红外发射管长时间导通会发热,发射功率随温度上升而衰减,接收端的信号裕量变小。尤其是电路板靠近LED驱动部分时,热量传导让漂移更明显。
解决:发射管串联电阻适当减小,典型值从100Ω改到82Ω增加发射电流;接收管的10K下拉电阻可以换成4.7K,提高输出电平的驱动能力;另外检查PCB布局,把红外发射和接收部分与LED功率电阻拉开距离,加一点通风空间。
5.3 串口下发时间指令后,时间没有更新
现象:用串口助手发送"Set 20:30:00"后,LCD1602上的时间没有变化,回显数据正常但系统不响应。
原因:指令解析代码依赖USART_RX_BUF[0]=='S'的判断,但串口接收中断会在收到回车换行后把USART_RX_STA置位。如果串口助手发送时带了额外的\r\n,USART_RX_BUF数组里第一个字节不是'S',而是'\r'或空格,解析就被拦住了。
解决:发送端确保只发纯指令不加回车,或者在接收中断处理逻辑里先过滤掉\r和\n字符。我在调试时一般先用串口助手发送不含回车的指令测试,确认功能后再决定是否要在固件里加过滤。
5.4 两路LED灯同时点亮,无法独立控制
现象:设置N1定时开启1号灯后,2号灯也跟着亮了;关掉1号灯,2号灯也一起灭。
原因:Control()函数里两路LED的判断条件共用了一组环境亮度标志位和全局标志,没有在逻辑上把两路完全隔开;另一个可能是LED驱动电路里两路继电器或三极管共用了一个基极电阻,电气层面已经并联了。
解决:代码层面检查State_Flag1和State_Flag2是否在Fix()函数里被串扰赋值,确保LED1只操作PB10,LED2只操作PB11;硬件层面用万用表量PB10和PB11的实际电平,如果两路引脚电平始终一致,说明驱动电路短路或者GPIO配置重复了。
5.5 数码管显示速度永远是0,物体经过没反应
现象:红外对射传感器正常,手挡一下数码管仍然显示0.0,但LED灯会正常点亮。
原因:速度计算依赖两个红外检测点的时间差,如果PC10或PC11对应的EXTI中断没有正确配置,时间差算不出来;也可能是两个对射管的间距在代码里写死成0,导致除数为零的数据被过滤掉了。
解决:先串口打印两个中断点的计数值,确认两个中断都触发了。如果只有一路触发,检查EXTI_PC10_Config和EXTI_PC11_Config的引脚号是否写对,PC10和PC11共用一个中断入口EXTI15_10_IRQHandler,很容易漏判断其中一路。间距参数在Speed_Cal模块里确认是否和实际安装距离一致。
6. 验证方法与进阶思路:从Proteus仿真到串口协议扩展
这套系统做完不是终点,怎么证明它工作可靠才是关键。文档里的测试思路我整理成一个可以在自己项目里直接套用的验证流程。
第一步是Proteus仿真。仿真时重点观察的是逻辑链路而不是电流参数:红外遮挡信号下降沿是否能触发LED状态翻转、定时时间到是否能自动开灯、数码管数值是否按预期跳动。因为Proteus里看不到真实的红外光学特性,所以仿真通过只代表逻辑正确。
第二步是硬件分模块测试。把电源、最小系统、红外检测、LED驱动、数码管显示分成五个独立模块,分别供电测试。电源模块看3.3V输出纹波,红外检测模块用示波器看输出信号的下降沿是否干净,LED驱动模块量继电器动作时是否有拉低系统电压的现象。每个模块独立跑通后再合到一起,出现问题时能快速缩小范围。
第三步是串口联调。接上CH340g后,用串口助手以9600波特率分别发送这几类指令做验证:
| 指令 | 预期行为 | 验证点 |
|---|---|---|
| Set 18:30:00 | LCD时间跳转到18:30:00 | 时间解析与赋值 |
| N1 19:00:00 | 19:00时1号灯自动点亮 | 定时开灯逻辑 |
| F1 | 1号灯立即关闭 | 强制关闭指令 |
| N2 20:00:00 | 20:00时2号灯自动点亮 | 双路独立定时 |
串口指令协议本身也有扩展空间。现有指令集覆盖了时间设置和两路灯的定时开关,没有覆盖手动强制点亮和查询状态。我一般会加一条"Q"指令,让系统返回当前环境亮度状态、两路灯的开关状态和内部时钟值,调试时能少猜很多问题。另外一个实用的改动是把固定间距值做成可配置项,通过串口指令修改,这样改变两个红外检测点的安装间距时不用重新编译烧录。
还有一个容易被忽视的指标是功耗。系统白天让LED驱动电路切断红外模块供电,这个省电设计是好的,但主控本身一直在跑,LCD1602和数码管常亮也耗电。如果要做电池供电的升级版本,可以在数码管进入待机模式后只保留1Hz刷新的时分显示,红外检测模块在环境亮度高时用MOS管完全断电,主控进入Stop模式,用RTC闹钟唤醒做周期性巡检。STM32的Stop模式电流可以降到微安级别,比现在的全速运行省得多。
这套工程我从头跟到尾之后,最大的习惯变化是:再做任何带外部中断和串口指令的板子,我都会先画一张引脚占用表和指令协议表,贴在调试台前面。中断入口重复、引脚复用冲突、指令带回车解析失败,这三类问题在这套工程里全遇见过,与其靠记忆硬扛,不如让表和代码保持同步。希望这篇拆解能帮你在做类似系统时少走这几段弯路。
本文还有配套的精品资源,点击获取