STM32交通灯项目实战:从状态机到FreeRTOS的嵌入式系统设计 1. 项目缘起从十字路口的等待到开发板的闪烁每次在十字路口等红灯看着红黄绿三色灯有规律地切换你有没有想过这背后的控制逻辑是怎样的如果让你用一块小小的单片机来复现这个系统你会怎么做这不仅仅是电子爱好者入门时的一个经典练手项目更是理解嵌入式系统中状态机、定时器、多任务调度等核心概念的绝佳实践。我选择使用STM32来完成这个四向十字路口交通灯项目原因很简单它性能足够强大生态极其丰富从简单的GPIO控制到复杂的实时操作系统都能找到对应的实现路径非常适合用来深入探究一个看似简单实则内涵丰富的系统。这个项目远不止是点亮几个LED灯那么简单。一个真实的十字路口交通灯系统需要考虑东西向与南北向的通行权交替、黄灯缓冲时间、行人过街信号联动、紧急车辆优先通行等诸多复杂场景。用STM32来实现我们可以从最基础的定时器中断控制灯态切换开始逐步引入按键中断模拟行人请求再到使用状态机框架来清晰管理所有灯态最终甚至可以尝试在FreeRTOS上构建多个并发的控制任务。整个过程就像搭积木从简到繁每一步都能加深对嵌入式开发的理解。无论你是刚接触STM32的新手想通过一个有趣的项目巩固GPIO和定时器的使用还是有一定经验的开发者希望借此梳理状态机设计或RTOS应用这个项目都能提供足够的探索空间。接下来我将分享如何从零开始构建一个功能完备、代码清晰、易于扩展的STM32四向交通灯系统。2. 核心需求分析与系统设计在动手写代码之前我们必须把十字路口交通灯的工作逻辑彻底厘清。一个标准的四向十字路口通常分为东西EW和南北SN两个主方向。每个方向上有三组灯红灯Red、黄灯Yellow、绿灯Green。其基本的工作周期Cycle一般遵循以下顺序相位A南北绿灯东西红灯南北方向车辆可通行东西方向车辆禁止通行。此相位持续时间最长例如30秒。相位过渡A南北黄灯东西红灯南北绿灯熄灭黄灯亮起提示南北方向车辆准备停止东西方向仍为红灯。此为缓冲时间例如3秒。相位B南北红灯东西绿灯南北方向变为红灯东西方向变为绿灯通行权切换。持续时间同样为30秒。相位过渡B南北红灯东西黄灯东西方向绿灯熄灭黄灯亮起南北方向保持红灯。缓冲3秒后重新回到相位A如此循环。这构成了一个最基础的四相位状态机。但实际需求往往更复杂行人请求路口设有行人过街按钮。当行人按下按钮后系统需要在当前车辆通行相位结束后插入一个全方向车辆红灯同时点亮行人通行绿灯的相位。紧急模式例如消防车、救护车通过时需要强制切换为全方向红灯或特定方向绿灯以确保安全。夜间模式在车流量极少的深夜交通灯可能切换为黄灯慢闪提示司机注意观察后通过。基于以上分析我们的STM32系统需要具备以下能力精准计时每个相位的持续时间必须精确可控。这依赖于STM32的定时器Timer模块。多路输出控制至少需要控制12个LED4个方向 × 3种颜色。我们将使用GPIO推挽输出模式直接驱动LED为简化电路通常会在GPIO口和LED之间串联一个限流电阻如220Ω。外部事件响应响应行人按钮外部中断和紧急模式切换信号。清晰的状态管理使用状态机State Machine来管理“南北绿灯”、“南北黄灯”、“东西绿灯”、“东西黄灯”、“全红”等状态及其转换条件这是整个系统逻辑清晰的核心。可扩展的架构为未来添加倒计时显示、联网控制等功能预留接口。硬件设计上除了STM32最小系统板如STM32F103C8T6核心板我们还需要12个LED灯红、黄、绿各4个及对应的限流电阻。2个轻触按键一个模拟行人请求一个模拟紧急模式切换。电源部分USB供电或外部5V/3.3V电源。可选为了更直观可以使用4个三色LED共阴模块来简化布线。软件层面我们将采用基于时间片轮询的状态机作为初版实现方案。它避免了复杂RTOS带来的开销又能很好地体现状态机的思想。后续我们可以将其平滑迁移到FreeRTOS的任务中。3. 硬件电路设计与关键外设配置硬件是软件的基石一个稳定可靠的硬件连接方案能避免很多调试时的灵异问题。我们以最常见的STM32F103C8T6核心板为例进行设计。3.1 GPIO分配与LED驱动电路STM32F103C8T6有丰富的GPIO我们为其分配如下具体引脚可根据你的板子调整南北方向红灯PC13黄灯PC14绿灯PC15东西方向红灯PB12黄灯PB13绿灯PB14行人通行灯假设设置在东西向PB15绿灯、PA8红灯按键行人请求按键PA0配置为上拉输入下降沿中断紧急模式按键PA1配置为上拉输入下降沿中断LED驱动采用最简单的STM32 GPIO直接驱动方式。STM32的GPIO在推挽输出模式下可以提供或吸收最大20mA的电流驱动普通LED绰绰有余。电路连接为3.3V - LED阳极 - 220Ω限流电阻 - GPIO引脚。当GPIO输出低电平0时LED两端形成压差而点亮输出高电平1时熄灭。这种“低电平点亮”的方式是单片机驱动LED的常见做法。3.2 定时器TIM配置系统的心跳精准的定时是交通灯的灵魂。我们将使用STM32的一个基本定时器如TIM2来产生一个周期性的中断作为整个系统的“心跳”或“时基”。假设我们希望系统时基为10毫秒ms。对于STM32F103如果系统时钟SYSCLK是72MHz我们可以这样配置TIM2预分频器PSC设置为7200-1。这样定时器时钟 72MHz / 7200 10KHz。自动重装载寄存器ARR设置为100-1。这样定时器每计数100次产生一次更新中断中断周期 (1 / 10KHz) * 100 10ms。在10ms的中断服务函数里我们不去直接处理灯的状态切换而是维护几个关键的软件计数器。例如我们定义一个全局变量phase_timer在每个10ms中断里递减。当phase_timer减到0时在主循环的状态机中才会触发状态切换。这种“中断标记主循环处理”的模式能保持中断服务函数的简短避免在中断中处理复杂逻辑。3.3 外部中断EXTI配置响应外部事件行人按钮和紧急按钮需要即时响应适合使用外部中断。将PA0和PA1配置为上拉输入模式内部上拉电阻使能默认高电平。配置EXTI线路将PA0连接到EXTI0PA1连接到EXTI1。配置中断触发方式为下降沿触发按键按下时引脚从高电平变为低电平。在NVIC中使能对应的EXTI中断。在中断服务函数中同样遵循“短平快”原则仅设置事件标志位。例如在EXTI0中断中设置pedestrian_request_flag 1在EXTI1中断中切换emergency_mode_flag。具体的处理逻辑放在主循环的状态机中判断这些标志位来执行。注意按键消抖必须在软件中处理。一种简单可靠的方法是在中断中设置标志位后在主循环中检测到标志位时先延时10-20ms可以用定时器时基实现非阻塞延时再次读取按键引脚状态如果仍是低电平才确认是有效按键。切忌在中断服务函数中进行延时操作。4. 状态机State Machine的软件实现状态机是控制系统的核心大脑它定义了系统可能处于的所有状态以及事件发生时如何从一个状态转换到另一个状态。对于我们的交通灯状态定义如下typedef enum { STATE_EW_GREEN_SN_RED, // 东西绿南北红 STATE_EW_YELLOW_SN_RED, // 东西黄南北红 STATE_EW_RED_SN_GREEN, // 东西红南北绿 STATE_EW_RED_SN_YELLOW, // 东西红南北黄 STATE_ALL_RED, // 全红行人通行或紧急状态 STATE_BLINK_YELLOW // 黄灯闪烁夜间模式 } TrafficLightState_t;每个状态需要关联两个信息该状态下各个LED的输出值和该状态的持续时间。我们可以用一个结构体数组查表法来定义typedef struct { TrafficLightState_t state; uint32_t duration_ms; // 状态持续时间单位ms struct { uint8_t ew_red : 1; uint8_t ew_yellow : 1; uint8_t ew_green : 1; uint8_t sn_red : 1; uint8_t sn_yellow : 1; uint8_t sn_green : 1; } output; // 使用位域紧凑表示输出1点亮0熄灭 } StateTable_t; const StateTable_t state_table[] { {STATE_EW_GREEN_SN_RED, 30000, {1,0,0, 0,0,1}}, // 东西红南北绿 {STATE_EW_YELLOW_SN_RED, 3000, {1,1,0, 0,0,1}}, // 东西红黄南北绿 等等这里需要仔细核对 // ... 其他状态 };注意上面的输出位域定义和赋值示例需要根据“低电平点亮”的硬件连接来仔细定义。例如ew_red:1可能代表“东西红灯点亮”。建议为每个LED定义一个清晰的宏如#define EW_RED_ON() HAL_GPIO_WritePin(EW_RED_GPIO_Port, EW_RED_Pin, GPIO_PIN_RESET)。4.1 主循环与状态机调度主循环的核心是一个巨大的switch-case语句它根据当前状态current_state执行相应的操作并检查转换条件。// 全局变量 volatile uint32_t phase_countdown 0; // 由定时器中断递减 TrafficLightState_t current_state STATE_EW_GREEN_SN_RED; uint8_t pedestrian_request_flag 0; uint8_t emergency_mode_flag 0; int main(void) { // 硬件初始化时钟、GPIO、定时器、中断... phase_countdown state_table[current_state].duration_ms / 10; // 转换为10ms的 ticks set_led_output(state_table[current_state].output); // 设置初始灯态 while (1) { // 1. 检查并处理按键事件消抖后 handle_button_events(); // 2. 状态机核心 switch (current_state) { case STATE_EW_GREEN_SN_RED: if (phase_countdown 0) { // 时间到正常转换到下一状态 current_state STATE_EW_YELLOW_SN_RED; phase_countdown state_table[current_state].duration_ms / 10; set_led_output(state_table[current_state].output); } if (pedestrian_request_flag) { // 行人请求插入全红状态 pedestrian_request_flag 0; current_state STATE_ALL_RED; phase_countdown 15000 / 10; // 行人通行时间15秒 set_led_output(ALL_RED_OUTPUT); // 所有车行灯为红行人灯为绿 } if (emergency_mode_flag) { // 紧急模式立即切换为全红 current_state STATE_ALL_RED; phase_countdown 0xFFFFFFFF; // 持续直到紧急模式取消 set_led_output(ALL_RED_OUTPUT); } break; case STATE_EW_YELLOW_SN_RED: if (phase_countdown 0) { current_state STATE_EW_RED_SN_GREEN; phase_countdown state_table[current_state].duration_ms / 10; set_led_output(state_table[current_state].output); } // 黄灯期间通常不响应行人请求 break; // ... 处理其他状态 case STATE_ALL_RED: if (phase_countdown 0) { // 行人通行时间结束回到中断前的状态或默认状态 current_state get_next_state_after_all_red(); // 一个函数决定返回哪个车行状态 phase_countdown state_table[current_state].duration_ms / 10; set_led_output(state_table[current_state].output); } if (!emergency_mode_flag current_state STATE_ALL_RED) { // 紧急模式取消恢复 current_state STATE_EW_GREEN_SN_RED; phase_countdown state_table[current_state].duration_ms / 10; set_led_output(state_table[current_state].output); } break; } // 3. 其他后台任务如串口调试信息打印非阻塞方式 // ... } }4.2 定时器中断服务函数void TIM2_IRQHandler(void) { if (__HAL_TIM_GET_FLAG(htim2, TIM_FLAG_UPDATE) ! RESET) { __HAL_TIM_CLEAR_FLAG(htim2, TIM_FLAG_UPDATE); // 系统时基每10ms一次 static uint32_t sys_tick 0; sys_tick; // 相位倒计时如果未暂停 if (phase_countdown 0) { phase_countdown--; } // 按键扫描消抖计时略 // ... } }这种查表法结合状态机的实现将状态、时长、输出紧密绑定逻辑清晰添加或修改状态非常方便。当需要增加“左转绿灯”等更复杂的相位时只需在状态枚举和状态表中添加新条目并在主循环的switch-case中补充处理逻辑即可。5. 功能扩展与进阶优化实现基础的四相位控制后我们可以让这个系统变得更智能、更健壮。5.1 添加倒计时显示为了提升用户体验可以增加数码管或LCD显示屏来显示当前绿灯或红灯的剩余秒数。这需要硬件连接一个四位一体共阴数码管模块使用TM1650等驱动芯片可简化编程或一块I2C/SPI接口的OLED屏幕。软件在状态切换时将phase_countdown单位是10ms的tick转换为秒数并驱动显示。例如在定时器中断中每秒更新一次显示数据。注意倒计时显示应与灯态同步在黄灯、全红等状态时显示相应的固定数字或符号。5.2 引入实时操作系统FreeRTOS当功能越来越多倒计时显示、串口命令控制、车流量检测模拟时轮询式状态机主循环会变得臃肿且难以维护。引入FreeRTOS可以将不同功能模块解耦为独立的任务Task_TrafficLightSM高优先级任务纯粹的状态机只负责根据时间和事件更新current_state和phase_countdown。Task_LEDDriver中优先级任务周期性地如每100ms读取current_state并调用set_led_output函数更新实际GPIO输出。这实现了控制与驱动分离。Task_Display低优先级任务负责更新倒计时显示。Task_CmdParser低优先级任务监听串口接收调试命令如修改相位时长、切换模式。任务间通过队列Queue或事件标志组Event Group进行通信。例如按键中断服务函数中释放一个信号量SemaphoreTask_TrafficLightSM等待这个信号量并处理事件。这样系统响应性更好结构也更清晰。5.3 模拟车流量检测与自适应配时这是一个更高级的拓展。我们可以用几个GPIO引脚连接红外对管或超声波模块来模拟车辆检测或者简单用随机数模拟。根据检测到的“车流量”动态调整绿灯相位时长。在每个绿灯相位结束前一段时间如最后5秒启动“检测窗口”。在窗口期内如果有车辆持续通过检测到信号则绿灯相位延长一个固定单位如5秒但设置一个最大延长时间上限。如果窗口期内无车辆则按时切换到黄灯。 这实现了一个简单的自适应交通控制算法虽然离真正的智能交通系统很远但作为学习传感器反馈控制的概念验证非常有趣。5.4 通过串口进行调试与参数配置利用STM32的USART我们可以实现一个简单的命令行接口CLI。例如在PC端使用串口助手发送命令set ew_green 40设置东西绿灯时间为40秒。get state获取当前状态和剩余时间。mode night切换到夜间黄灯闪烁模式。 在主程序中解析这些命令并修改state_table中的时长或直接切换current_state这极大方便了系统调试和参数整定。6. 调试技巧与常见问题排查在实际制作和编程过程中你肯定会遇到各种问题。以下是一些典型的踩坑点和解决思路6.1 LED灯不亮或亮度异常检查硬件连接这是第一步也是最常见的一步。确认LED极性是否正确长脚为正限流电阻是否接上杜邦线是否松动。确认GPIO模式驱动LED应设置为推挽输出GPIO_MODE_OUTPUT_PP而不是开漏输出。开漏输出无法直接驱动LED至高亮。确认输出电平逻辑根据你的电路是“高电平点亮”还是“低电平点亮”在代码中确认GPIO_PIN_SET和GPIO_PIN_RESET与预期是否相符。可以用万用表测量引脚电压。检查代码初始化顺序确保在配置GPIO前已经使能了对应的外设时钟__HAL_RCC_GPIOx_CLK_ENABLE()。6.2 状态切换混乱或时间不准定时器中断配置错误重点检查定时器的预分频器PSC和自动重装载值ARR计算是否正确。使用示波器或逻辑分析仪测量一个GPIO引脚在中断函数里翻转产生的波形可以精确测量中断周期。中断服务函数过长确保中断函数里只做最简单的标志位设置或计数器递减。复杂的处理必须放到主循环。长时间的中断会阻塞其他中断导致系统响应变慢甚至死机。变量共享问题在主循环和中断中都会访问的全局变量如phase_countdown,pedestrian_request_flag必须使用volatile关键字声明防止编译器优化导致数据不同步。对于更复杂的结构可能需要关中断进行保护。状态机逻辑错误仔细绘制状态转换图并在每个case中打印调试信息通过串口确认状态转换的条件和顺序是否符合预期。常见的错误是在一个状态里错误地判断了多个转换条件导致跳转异常。6.3 按键响应不灵或连击未消抖这是按键问题之首。务必在主循环中实现软件消抖参考前面提到的方法。中断配置问题确认EXTI中断线是否正确映射到GPIO引脚中断优先级NVIC是否使能中断服务函数名是否正确在启动文件startup_stm32f103xe.s中有定义。中断标志未清除在EXTI中断服务函数末尾必须调用__HAL_GPIO_EXTI_CLEAR_IT()或类似函数来清除挂起的中断标志否则会连续进入中断。6.4 系统运行一段时间后死机堆栈溢出如果使用了FreeRTOS检查任务分配的堆栈空间是否足够。可以在FreeRTOSConfig.h中启用configUSE_TRACE_FACILITY和configCHECK_FOR_STACK_OVERFLOW来辅助诊断。中断嵌套与优先级如果使用了多个中断要合理配置它们的抢占优先级和子优先级。错误的优先级设置可能导致高优先级中断持续打断低优先级中断使后者无法完成。看门狗IWDG/WWDG对于要求高可靠性的系统可以启用独立看门狗。在主循环中定期“喂狗”。如果程序跑飞看门狗超时复位可以快速恢复。从点亮第一个LED到状态机有条不紊地运行再到加入各种高级功能完成一个STM32交通灯项目的全过程是对嵌入式开发基本功的一次全面锻炼。它强迫你去思考硬件与软件的接口、时间的精确管理、复杂逻辑的组织方式以及系统的可扩展性。当你看到自己编写的代码控制着红绿灯按预想的节奏闪烁时那种成就感是无可替代的。这个项目就像一个微缩的工业控制系统其中的设计模式如状态机、前后台系统和问题排查方法在你未来面对更复杂的嵌入式系统时依然会非常有用。