免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

STM32F103交通灯控制系统:状态机与定时器驱动的工程实践

STM32F103交通灯控制系统:状态机与定时器驱动的工程实践 简介基于STM32F103单片机设计的交通灯控制系统开发资料面向课程设计与单片机实践解决红绿灯状态切换与定时控制问题。压缩包共88个文件约1.73MB含34个C源文件、38个H头文件以及Keil工程、Proteus仿真、PDF设计报告、HEX烧录文件等。代码内含main函数中东西与南北方向绿黄灯顺序切换的延时控制逻辑并封装系统时钟、GPIO、TIMER、OLED等模块附带STM32标准外设库启动文件便于理解工程结构。Proteus仿真工程可直观演示信号灯流程报告补充整体方案与设计思路。已有850人学习适合需快速完成课设或进行红绿灯逻辑验证的读者。模块化代码清晰易读解压后可直接用Keil打开节省搭建时间。1. 交通灯控制系统不是流水灯核心在状态机与安全时序很多人在拿到基于STM32F103单片机设计的交通灯控制系统这类题目时第一反应是点灯红灯亮几秒、绿灯亮几秒、黄灯闪烁几次然后循环。如果只是这样用51单片机加延时函数就够了根本不需要STM32F103。真正让这个题目有分量的地方在于它把单片机课程设计里最经典的两个问题揉在了一起状态机建模和时序可靠性。交通灯不是简单的周期性翻转IO它要处理倒计时显示的精确性、紧急车辆通行时的状态抢占、行人按钮的异步触发以及断电重启后如何安全恢复而不是直接跳到绿灯。这个项目最适合两类人一类是正在做单片机课程设计或毕业设计的学生需要把能跑升级成能讲清楚另一类是刚接触STM32标准外设库的工程师想找一个比流水灯复杂一个量级、又比FreeRTOS移植简单得多的练手对象。用STM32F103做这件事的优势在于它有足够的定时器资源让倒计时和LED驱动互不干扰有外部中断处理按键和传感器输入而且标准库v3.50这一代资料极其丰富遇到问题几乎都能查到解决方案。当然它也引入了51单片机时代不太在意的坑比如GPIO配置模式、定时器重装载值的边界计算、中断优先级抢占这些恰恰是这个项目真正的学习价值所在。下面这套方案以状态机为核心、定时器为骨架、按键和传感器为扰动输入的常见做法展开从最小系统搭建一路讲到逻辑分析仪验证时序全程可以照着抄也能理解每一条配置为什么这么写。2. 状态机建模与延时方案选型先定状态表再写代码2.1 交通灯状态不是时间表而是转移图标准十字路口交通灯不考虑左转专用相位可以抽象为4个主状态南北直行绿灯、南北左转绿灯、东西直行绿灯、东西左转绿灯每个状态之间用黄灯过渡。如果再加入行人按钮和紧急车辆传感器状态数会增加到7到8个。这里有一个关键的设计决策状态转移只能由定时事件驱动不能由延时函数内部的休眠驱动。普通延时法即HAL_Delay或Delay循环的问题是当单片机停在延时循环里时它无法响应按键中断、无法刷新数码管倒计时、无法处理紧急传感器输入。这在课程设计答辩演示时特别尴尬因为老师按下行人按钮灯要等当前相位走完才反应看起来就像卡住了。正确的建模方式是把每个状态定义成持续时间 转移条件的结构体数组。我一般这样定义typedef struct { uint8_t state_id; // 状态编号用于状态机跳转 uint8_t north_south_led; // 南北方向灯组状态0灭 1红 2绿 3黄 uint8_t east_west_led; // 东西方向灯组状态 uint16_t duration_ms; // 本状态持续时长 uint8_t next_state; // 超时后跳转的目标状态 } TrafficLightState;参数说明state_id是枚举值比如0代表南北绿灯、1代表南北黄灯、2代表东西绿灯、3代表东西黄灯north_south_led和east_west_led各自独立控制一个方向的灯色这样南北和东西可以用同一个结构体但方向字段互不相同。duration_ms是这个状态持续的最短时间一般绿灯给20到30秒黄灯固定3秒行人按钮触发后给15秒左右。next_state让状态机在超时后知道去哪里同时这个转移表也为后面加入紧急模式预留位置。很多人会把黄灯闪烁单独处理成一个状态这是对的。在标准交通灯逻辑里黄灯不是随便闪两下而是固定频率的闪烁比如1Hz也就是500毫秒亮、500毫秒灭。如果把这个闪烁逻辑塞进状态机主循环会让状态机的刷新频率和闪烁频率耦合在一起后面加倒计时数码管显示时会非常痛苦。好的做法是黄灯状态本身只负责设置LED为黄色且进入闪烁模式闪烁的亮灭交给定时器中断去翻转。2.2 为什么课程设计首选定时器时基主循环轮询而不是裸机延时STM32F103有不少于4个定时器对于交通灯这种低速外设我们其实用不了那么多。最稳妥的架构是TIM2产生1毫秒的时基中断在中断里做计时累加主循环里做状态判断和LED刷新。这个方案的核心代码如下volatile uint32_t g_tick_ms 0; void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); g_tick_ms; // 每进入一次中断就是1ms过去了 } } uint32_t get_tick_ms(void) { return g_tick_ms; }为什么不在中断里直接跑状态机因为状态机的处理逻辑包含多个LED的赋值、数码管的段码刷新、按键状态读取如果全部放进中断中断服务函数会变得很长容易超过下一次中断的到来时间。常见的做法是中断只负责维护时间基准主循环负责业务逻辑while (1) { uint32_t now get_tick_ms(); // 当前状态剩余时间 状态预定时间 - 已运行时间 if (now - state_start_time current_state-duration_ms) { current_state state_table[current_state-next_state]; state_start_time now; apply_led_state(current_state); // 把状态结构体里的LED值刷到GPIO } // 更新倒计时数码管每200ms刷新一次避免肉眼可见闪烁 if (now - display_last_update 200) { display_last_update now; display_remaining_time(current_state-duration_ms - (now - state_start_time), now); } }这里有三个需要重点说明的参数和逻辑第一state_start_time必须是无符号32位变量记录状态机进入当前状态的时刻。计算剩余时间时用now - state_start_time的差值而不是先取一次时间再自己累加这样可以避免在中断里做除法或取余运算。第二g_tick_ms用volatile修饰极其重要。STM32F103是单核M3内核虽然主循环和中断共享这个变量时大多数情况下能正常工作但编译器优化后可能把g_tick_ms缓存在寄存器里导致主循环读到旧值。加volatile是告诉编译器每次都从内存地址读取。如果后续使用GCC的-O2以上优化级别这个关键字的缺失会引发程序跑几分钟才跳变一次的诡异现象。第三倒计时显示的刷新频率取200毫秒不是随便定的。数码管是动态扫描显示的每个位需要轮流点亮刷新太快会占用过多CPU时间刷新太慢则会明显看到数字跳动。200毫秒对于秒为单位的倒计时来说完全够用而且给人眼留了足够的观测窗口。2.3 状态表参数设计的工程经验值下面这张表是常见的课程设计默认参数实际调试时可以直接抄也可以按路口车流量调整状态南北LED东西LED持续时间说明S0绿灯红灯25秒南北直行放行S1黄灯 1Hz 闪烁红灯3秒南北清空路口S2红灯绿灯25秒东西直行放行S3红灯黄灯 1Hz 闪烁3秒东西清空路口S4全红灯全红灯2秒全红清空用于安全间隔全红间隔这个2秒是很多新手容易漏掉的状态。中国国标GB 14886-2016明确规定了信号灯的转换时序虽然没有强制全红间隔的秒数但实际工程中会保留1到3秒全红时间确保上一个方向的车辆完全驶出路口后才放行下一个方向。在课程设计答辩时主动解释为什么在绿灯切红灯之间加了全红状态往往比代码本身更得分。另外如果路口有左转专用相位状态表会变成6行以上此时需要增加南北左转绿灯和东西左转绿灯两个状态每个左转相位持续15到20秒并且左转与直行之间也要加黄灯过渡。这个扩展非常容易只需要往state_table数组里加结构体元素并修改next_state指针即可状态机框架本身完全不用动。3. 从原理图到GPIO最小系统之外交通灯控制板还要接什么3.1 STM32F103最小系统的必要组件与常见翻车点网上搜stm32f103最小系统原理图能找到大量参考但很多图纸抄来抄去少了关键电容或者复位电路电阻值不对。一个能稳定跑交通灯代码的最小系统至少包含以下部分8MHz晶振 两个20pF负载电容给HSE提供时钟源BOOT0下拉到GND10K电阻BOOT1随意确保从Flash启动NRST复位引脚接10K上拉电阻到3.3V再串100nF电容到地VDDA引脚接3.3V并加1uF和100nF两级滤波电容VDD每个引脚都要有100nF去耦电容且尽量靠近引脚放置稳压部分课题常见两种选择如果是纯3.3V供电比如用ST-Link的3.3V输出不需要额外稳压如果输入是5V USB供电则需要AMS1117-3.3稳压。这里有个很多人踩过的坑AMS1117的压差是1V左右输入5V输出3.3V没问题但要确保输入电容不小于10uF否则上电瞬间可能触发STM32的BOR欠压复位。LED驱动部分交通灯模块通常采用共阳极接法即LED的正极接3.3V或5V负极经过限流电阻接到STM32的GPIO。STM32F103的GPIO在推挽输出模式下灌电流能力比拉电流强虽然数据手册标称都是±25mA但实际灌电流时饱和压降更低。用共阳极GPIO输出低电平点亮LED是大多数开发板的设计选择。限流电阻取330欧姆到1K欧姆取决于LED的额定电流和供电电压。如果是3.3V供电、红色LED压降2V330欧姆电阻限流约3.3-2)/3303.9mA亮度适中。四位数码管做倒计时显示推荐用共阴或共阳的动态扫描方式。这里不需要外接驱动芯片用STM32的GPIO直接驱动即可但每一段的限流电阻不能省。如果使用的是四位一体数码管位选引脚需要加PNP三极管共阳时或NPN三极管共阴时来增强驱动能力因为STM32单个GPIO的灌电流/拉电流不足以同时点亮四位所有段。3.2 GPIO初始化代码推挽输出与上拉输入一个都不能错void GPIO_InitConfig(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC, ENABLE); // 交通灯LEDPA0-PA5推挽输出复用推挽也OK但这里用通用推挽即可 GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_2MHz; // 低速外设2MHz足够 GPIO_Init(GPIOA, GPIO_InitStructure); // 按键输入PA6-PA7上拉输入按下为低电平 GPIO_InitStructure.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOA, GPIO_InitStructure); }参数说明里有三个容易被忽视的细节一是GPIO_Speed_2MHz。很多人习惯把所有引脚都配成GPIO_Speed_50MHz在高频SPI或PWM输出时这是对的但LED和按键这种低速应用2MHz已经足够。关键是更高的翻转速率会带来更大的EMI辐射和电源噪声在面包板上搭电路时这个问题会导致数码管显示乱码或按键误触发。二是共阳极LED输出低电平点亮意味着初始化后GPIO输出高电平才让所有灯灭。如果初始化后忘记把LED引脚拉高上电瞬间所有灯会亮一下给人系统不受控的坏印象。在GPIO_InitConfig()末尾执行GPIO_SetBits(GPIOA, GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5);三是按键的GPIO速度不需要配因为输入模式下速度配置无效但GPIO_Mode_IPU必须显式配置。STM32F103的GPIO默认状态是浮空输入不加上拉电阻的话按键未按下时引脚电平不确定轻则按键失灵重则频繁触发外部中断。3.3 按键处理轮询 消抖不要用阻塞延时交通灯系统至少有两个按键一个行人按钮按下后尽快切换到行人绿灯一个是紧急模式切换模拟消防车或救护车通过时全路口红灯。按键消抖有几种方案最不推荐的是在主循环里Delay(10ms)——这又回到了阻塞式延时和状态机架构冲突。常见做法是定时器时基驱动按键扫描每20ms扫描一次连续两次读到相同电平才认为有效typedef struct { uint8_t stable_state; // 稳定电平1为未按下0为按下 uint8_t last_state; uint8_t count; } KeyState; void KeyScan(void) { // 每20ms调用一次 uint8_t pedal GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6); if (pedal key.last_state) { if (key.count 3) key.count; if (key.count 2) key.stable_state pedal; } else { key.last_state pedal; key.count 0; } }这段代码的逻辑是连续3次即60ms读到相同电平才更新稳定状态比简单的延迟10ms再读一次更能滤除机械抖动。更重要的是这个扫描函数在while(1)主循环里调用也可以在20ms定时中断里调用无论哪种都不阻塞状态机运行。当key.stable_state 0时触发行人绿灯抢断逻辑。抢断不是马上跳进某个状态而是把当前状态的duration_ms覆盖为一个较小值。我常用的做法是定义一个全局变量g_shorten_remaining_ms在主循环计算剩余时间时检测if (key.stable_state 0 current_state-state_id S0) { // 当前状态剩余时间超过3秒才截断否则忽略 uint32_t remain current_state-duration_ms - (now - state_start_time); if (remain 3000) state_start_time now - (current_state-duration_ms - 3000); }这里改state_start_time而不是直接改duration_ms好处是不破坏状态表的原始参数在显示和后续状态恢复时逻辑一致。3秒的保护时间是为了避免行人按键被连续按下时状态机在S0和行人状态之间来回跳。4. 倒计时显示与LED刷新动态扫描的时序竞争与解决4.1 数码管动态扫描的基本原理段选与位选四位数码管显示倒计时本质上是一个时分复用问题。四位共阳数码管有8个段选引脚a到dp和4个位选引脚digit1到digit4。动态扫描的做法是在任何时刻只点亮一个位点亮一段极短时间后切换到下一位由于视觉暂留效应人眼看到的是所有位同时亮。关键参数是扫描频率。每个位至少需要50Hz的刷新率才不会闪烁四位合计就是200Hz即每个位的刷新周期最长5毫秒。反过来看单次放置时间不能太长否则人的眼睛会捕捉到一个数字比另一个亮的亮度不均。void Display_Update(uint8_t digit1, uint8_t digit2, uint8_t digit3, uint8_t digit4) { static uint8_t current_digit 0; uint8_t code; // 先关闭所有位选防止拖影 GPIO_SetBits(GPIOC, GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3); // 切换位选 switch (current_digit) { case 0: code digit1; GPIO_ResetBits(GPIOC, GPIO_Pin_0); break; case 1: code digit2; GPIO_ResetBits(GPIOC, GPIO_Pin_1); break; case 2: code digit3; GPIO_ResetBits(GPIOC, GPIO_Pin_2); break; case 3: code digit4; GPIO_ResetBits(GPIOC, GPIO_Pin_3); break; } // 输出段码共阳极数码管低电平点亮段 GPIO_Write(GPIOB, Segment_Table[code]); current_digit (current_digit 1) % 4; }Segment_Table是共阳数码管的段码数组比如数字0对应0xC0数字1对应0xF9。这里要注意一个细节GPIO_Write会一次性写入16位数据如果GPIOB上还接了其他外设会干扰它们的状态。稳妥的做法是只操作低8位先读出当前ODR再修改uint16_t temp GPIO_ReadOutputData(GPIOB); temp (temp 0xFF00) | Segment_Table[code]; GPIO_Write(GPIOB, temp);调用Display_Update的时机放在TIM2中断里每2毫秒切换一位。这样四位完整扫描周期是8毫秒每位刷新频率125Hz高于无闪烁阈值。但要注意如果Display_Update在中断里执行它的执行时间不能超过2毫秒本身——实际上段码查表和GPIO操作只需要几十微秒完全没有风险。4.2 黄灯闪烁与倒计时显示的并发问题当状态机进入黄灯闪烁状态时黄灯要1Hz闪烁同时数码管要显示剩余秒数。如果两个逻辑都在主循环里顺序执行会出现一个优先级问题LED的闪烁翻转频率是2Hz周期而数码管刷新是毫秒级——如果主循环比较慢LED灯会跳变迟缓。解决方案是把LED闪烁的时序判断和数码管刷新放在同一个1ms时基上LED每500次tick翻转一次数码管每2ms刷新一位。这样LED的翻转不会因为主循环的耗时而被推迟。void TIM2_IRQHandler(void) { static uint16_t led_flip_counter 0; static uint16_t display_digit_counter 0; g_tick_ms; // 黄灯闪烁500ms翻转一次 if (current_state-state_id S1 || current_state-state_id S3) { led_flip_counter; if (led_flip_counter 500) { led_flip_counter 0; GPIO_WriteBit(GPIOA, GPIO_Pin_1, (BitAction)(1 - GPIO_ReadOutputDataBit(GPIOA, GPIO_Pin_1))); } } // 数码管2ms切换一位 display_digit_counter; if (display_digit_counter 2) { display_digit_counter 0; // 把剩余秒数拆成4位显示 // 这里用状态机剩余时间 / 1000 得到秒数 } }这个设计让所有时间敏感的操作都集中到了定时器中断主循环只需要处理状态转移和按键扫描。代价是中断服务函数变得更长但STM32F103的Cortex-M3内核在72MHz主频下执行完这段逻辑几个比较、赋值、GPIO操作需要不到10微秒占1ms时基的1%远没有达到需要优化的程度。如果后续要加紧急车辆传感器比如红外对射检测只需在中断或主循环中增加一个IO_ReadInputDataBit判断当检测到高优先级车辆时置一个标志位主循环检测到标志位后立即切换到全红保持状态。全红保持时间建议固定为5秒确保路口内车辆清空然后恢复默认循环。4.3 主循环架构的最终形态综合以上设计主循环的逻辑可以归结为扫描按键、检查紧急标志、检查状态是否超时、更新倒计时显示数据。伪代码如下while (1) { KeyScan(); if (g_emergency_flag) { current_state state_table[STATE_ALL_RED]; state_start_time get_tick_ms(); g_emergency_flag 0; } uint32_t now get_tick_ms(); if (now - state_start_time current_state-duration_ms) { current_state state_table[current_state-next_state]; state_start_time now; apply_led_state(current_state); } if (now - display_refresh_time 500) { display_refresh_time now; uint16_t remain_sec (current_state-duration_ms - (now - state_start_time)) / 1000; // 拆分个位、十位送入显示缓冲区 } }这里没有用RTOS是因为交通灯的逻辑本身是线性状态机用FreeRTOS反而会引入任务间通信和临界区保护这些额外复杂度。只有当你要在这个项目上叠加多路传感器采集、LCD屏显示、远程通信时才值得考虑上FreeRTOS——那时裸机主循环的实时性才会成为瓶颈。5. 从理论到实物Proteus仿真与硬件调试的对照验证5.1 仿真先行Proteus中搭建交通灯系统的要点很多人在Proteus中画的交通灯原理图能通过仿真但移植到实板上完全不工作典型原因是仿真模型忽略了GPIO的驱动能力限制。在Proteus中你可以直接让STM32F103的引脚点亮LED但在实板上单个GPIO引脚灌入20mA以上电流时输出低电平会从0V升到0.5V甚至更高导致LED亮度降低或逻辑混乱。一个稳妥的仿真方案是用74HC245或ULN2003作为LED驱动芯片STM32的GPIO只输出逻辑信号由驱动芯片提供电流。ULN2003是达林顿管阵列灌电流能力500mA非常适合驱动共阳极LED灯组。但要注意ULN2003是反相的输入高电平则输出导通到地LED点亮逻辑需要反转。在Proteus中进行时间验证时有一个技巧把TIM2的预分频系数调小让1ms时基变成0.1ms甚至更短状态机的25秒绿灯就会在2.5秒内跑完。这样你不需要等完整的60秒周期就能验证整个状态机循环。等仿真确认无误后再把预分频系数改回去。这个参数就是TIM_Prescaler——72MHz时钟下预分频71得到1MHz计数频率自动重装载999得到1ms中断周期。5.2 硬件调试三板斧逻辑分析仪、串口打印和LED指示灯实物调试中最常见的故障现象是状态机不跳转或数码管显示乱码。我的排错顺序如下先用逻辑分析仪挂到状态机切换的GPIO引脚上。比如在每次状态转移时翻转PA8引脚调试专用状态每次跳转都会产生一个电平跳变逻辑分析仪的边沿间隔就是每个状态的持续时间。如果发现某个状态持续时间异常长说明duration_ms参数或state_start_time计算有误。其次是串口打印。用USART1重定向printf在每个状态切换时打印一行日志格式如[t12345ms] S0-S1, remain25000ms。如果连printf都卡住多半是fputc没有重定向到串口。STM32标准库中需要在文件中加入int fputc(int ch, FILE *f) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); USART_SendData(USART1, (uint8_t)ch); return ch; }第三板斧是在可疑位置加LED闪烁指示灯。比如在主循环每循环一次翻转PB0上的LED如果LED不闪说明主循环卡死了如果LED闪但状态不跳说明问题在定时器中断。一个容易被忽略的硬件坑是启动文件选择。STM32F103有多个启动文件startup_stm32f10x_hd.s对应高密度器件Flash大于256KBstartup_stm32f10x_md.s对应中密度64KB到128KB Flash。如果你的工程是STM32F103C8T6必须用md版本用错启动文件会导致上电后程序不运行表现为所有LED无反应、调试器能连接但无法复位。这个问题在从网上下载别人的工程模板时经常遇到因为对方用的是ZET6或RCT6启动文件不兼容。6. 状态机可视化与边界参数验证让答辩或交付有理有据6.1 用状态转移表自检而不是靠眼睛看灯课程设计最怕的是看起来一切正常一追问逻辑就答不上来。我建议在项目收尾时打印或手绘一张完整的状态转移表把每个状态的进入条件、持续时间、退出条件、对行人按钮的响应方式都写清楚。以本文前面的5状态模型为基础扩展成含行人抢断的8状态表状态进入条件持续时间退出条件行人按钮响应S0 南北绿灯上电初始/状态完成25s超时剩余3s忽略否则截断至3s后进入S1S1 南北黄灯S0超时3s超时忽略S2 东西绿灯S1超时25s超时剩余3s忽略否则截断至3s后进入S3S3 东西黄灯S2超时3s超时忽略S4 全红清空S3超时2s超时忽略S5 行人南北按钮按下且当前为S0/S115s超时忽略S6 紧急全红紧急传感器触发保持按钮解除忽略S7 恢复过渡S6退出5s超时忽略这张表的价值不只是用来答辩它也是代码review的检查清单。比如你发现S0被行人截断后直接跳S1那S5行人绿灯之后应该跳回哪个状态如果跳回S2那么行人过马路的时间只有3秒黄灯2秒全红共5秒对于15米宽的路口显然不够。正确的做法是S5结束后跳回S2的起点重新给东西方向25秒绿灯。这种逻辑漏洞在纯看代码时不容易发现但画成转移表一目了然。6.2 参数边界验证清单测试代码改一个宏就能跑为了让自己和审阅者确信系统可靠我通常会在工程里加一个#define TEST_MODE 1的开关。开启后状态机的时间参数按1/10缩放25秒变2.5秒3秒变0.3秒1Hz闪烁变10Hz闪烁。这样可以在30秒内跑完全部状态组合并用逻辑分析仪录制完整波形。验证项包括正常循环100次不出错行人按钮在绿灯任意剩余时间按下都能正确截断到3秒紧急模式下全红保持时间稳定在5秒断电重启后从全红状态开始而不是从南北绿灯开始。最后一点需要额外说明STM32上电后进入主函数的初始状态我定义的是STATE_ALL_RED保持2秒后自动进S0。这样电源恢复时不会出现直接放行的危险场景。添加这个逻辑只需要两行代码current_state state_table[STATE_ALL_RED]; state_start_time get_tick_ms();6.3 把裸机逻辑整理成可移植的模块化工程当你把交通灯系统做完后自然的进阶方向是把功能拆成bsp_led.c、bsp_key.c、bsp_display.c、app_traffic.c四个模块。app_traffic.c对外只暴露三个接口Traffic_Init、Traffic_Tick_1ms在定时器中断中调用、Traffic_Loop在主循环中调用。这样无论后续移植到STM32F103C8T6还是STM32F407底层BSP换掉应用层状态机完全不用动。如果还想要更进阶的玩法可以把状态机的数据结构改用函数指针数组实现每个状态对应一个void (*state_handler)(void)回调函数。这种写法在状态数量超过10个时优势明显因为每个状态的处理逻辑可能不同比如S0需要读取车流量传感器S5需要启动行人倒计时显示。函数指针数组的跳转代价只有一次间接调用在72MHz主频下几乎可以忽略但它让代码的可读性和可维护性提升了一个档次。这正是从课程设计代码走向嵌入式工程代码的关键一步也是这个项目的终极价值所在。本文还有配套的精品资源点击获取
返回列表