资讯动态

STM32智能交通灯:车流自适应嵌入式控制系统

发布时间:2026/10/20 7:14:44 来源:尧图企业网站定制
简介本资源是一套完整的智能交通灯控制器嵌入式开发实践包面向电子类、自动化及物联网方向的本科课程设计与单片机初学者解决交通信号系统动态调度算法实现与软硬件联合验证问题。压缩包共17个文件含Keil工程核心文件.uvproj/.uvopt/.c/.hex、Proteus仿真电路图.dsn/.dbk及关键说明文档使用说明.txt、成果与问题.txt涵盖程序编写、编译调试、仿真运行全流程86KB轻量级包体便于快速下载与本地复现。已有4702人学习下载适合开展单片机综合实训、毕业设计原型开发或嵌入式系统入门实践。读者可直接导入Keil与Proteus环境运行三模式正常/紧急/智能交通灯控制逻辑掌握车流量感知响应机制、按键交互设计、实时时序控制及软硬协同调试方法配套说明文档清晰指引从编译到仿真的操作路径。1. 智能交通灯控制器不是“红黄绿轮播”而是能感知车流、自适应配时的嵌入式小系统你手头那块 STM32F103C8T6 最小系统板接上几个 LED 和按键跑个“红→黄→绿→红”的固定延时循环——这叫教学演示而这个项目跑起来后东西向车流一多南北向红灯自动延长 8 秒深夜车少时所有方向绿灯压缩到 5 秒并插入 2 秒黄闪更关键的是它不依赖 PC 或云端纯靠单片机本地采集红外对管/地磁模拟信号 状态机调度 定时器中断驱动整套逻辑在 Keil MDK-ARM v5.37 下编译烧录进芯片就能 standalone 运行。它面向的是嵌入式初学者想脱离“点灯流水灯”阶段、课程设计需要可答辩可演示、毕设中需真实硬件闭环验证控制算法的场景。Proteus 8.13 仿真图里已预置了带 ADC 采样通道的车流检测模块、LED 矩阵驱动电路、蜂鸣器提示单元连 PCB 布线建议都写在《使用说明.docx》第 7 页——这不是玩具模型是能焊在实验箱上、接真传感器、经得起老师用示波器抓波形的工程级最小可行系统。2. 技术栈与工程结构Keil 工程目录即开发地图Proteus 电路即运行沙盒这个项目不是“代码图”两张皮而是 Keil 工程和 Proteus 仿真深度对齐的闭环。我拆开源码包第一件事就是核对三处映射关系Keil 中GPIO_Init()初始化的端口如GPIOA的PA0~PA5必须和 Proteus 里 LED 阵列的引脚编号完全一致EXTI_Line4对应的按键中断引脚要和 Proteus 中 SW1 开关连接的 MCU 引脚物理位置相同最关键的是ADC1_Channel1采样口在 Proteus 里必须接到模拟车流强度的可调电位器输出端——漏掉任一环仿真时灯就不按逻辑变或者按键无响应。下面带你一层层剥开这个“嵌入式交通大脑”的筋骨。2.1 Keil 工程核心目录结构从启动到状态机的五层落地整个 Keil 工程采用标准 ARM Cortex-M3 分层架构目录结构即开发逻辑链TrafficLight_Project/ ├── CMSIS/ // 标准外设访问层ST 提供的 core_cm3.h 启动文件 startup_stm32f10x_md.s ├── FWLIB/ // 标准外设库stm32f10x_gpio.c, stm32f10x_tim.c 等v3.5.0 ├── USER/ │ ├── main.c // 主函数系统初始化 主循环仅含 while(1) { StateMachine_Run(); } │ ├── state_machine.c // 核心交通灯状态机实现含 6 个主态 3 个子态 │ ├── sensor_driver.c // 车流检测驱动ADC 采样 滑动窗口滤波 阈值分级低/中/高流量 │ ├── timer_config.c // 定时器配置TIM21ms 基础滴答、TIM3绿灯倒计时、TIM4黄灯闪烁 │ └── key_handler.c // 按键处理SW1手动切换模式、SW2强制全红、SW3复位配时 ├── OUTPUT/ // 编译输出目录.axf, .hex, .map └── TrafficLight.uvproj // Keil 工程文件v5.37 兼容提示state_machine.c是整个系统的“决策中枢”它不直接操作硬件而是通过Light_Set()、Timer_Start()等封装接口下发指令。这种解耦让你改配时策略时只需动状态机逻辑不用碰 GPIO 初始化代码。2.2 Proteus 8.13 仿真电路关键模块解析不是画着玩是信号级闭环Proteus 图TrafficLight_Simulation.DSN不是示意草图而是可仿真的信号级电路。重点看三个模块车流检测模拟单元使用POT-HG可调电位器0–10kΩ模拟红外对管输出电压0–3.3V经LM324运放跟随后接入ADC_IN1PA1。Proteus 中双击电位器拖动滑块即可实时改变“车流量”观察 LED 绿灯时间动态伸缩——这是验证自适应逻辑最直观的方式。LED 交通灯矩阵东西向EW和南北向NS各 3 组 LED红/黄/绿共 12 颗。全部采用共阴极接法阳极通过 220Ω 限流电阻接GPIOAPA0–PA5和GPIOBPB0–PB5。注意 Proteus 中 LED 属性必须设为Active High否则逻辑反了。人机交互接口SW1–SW3为轻触按键一端接地另一端接GPIOAPA6–PA8内部上拉使能GPIO_PuPd_UP。Proteus 中按键按下时引脚电平拉低与 Keil 中GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6)读取逻辑严格对应。2.3 主状态机设计6 个主态 动态子态拒绝硬编码延时状态机不是简单的enum { RED, YELLOW, GREEN }而是按实际路口逻辑建模。state_machine.c中定义typedef enum { STATE_EW_GREEN_NS_RED, // 东西绿南北红主态1 STATE_EW_YELLOW_NS_RED, // 东西黄南北红主态2 STATE_EW_RED_NS_GREEN, // 东西红南北绿主态3 STATE_EW_RED_NS_YELLOW, // 东西红南北黄主态4 STATE_ALL_FLASH, // 全黄闪夜间模式主态5 STATE_EMERGENCY_ALL_RED // 全红紧急模式主态6 } TrafficState;但真正决定“绿灯亮多久”的是每个主态下的动态子态。以STATE_EW_GREEN_NS_RED为例其执行逻辑在State_EW_Green_NS_Red_Handler()中void State_EW_Green_NS_Red_Handler(void) { static uint16_t green_time 0; // 当前绿灯已亮时间ms static uint16_t base_time 30000; // 基准绿灯时间30s // 步骤1根据车流动态调整基准时间 uint8_t flow_level Sensor_GetFlowLevel(); // 返回 0低,1中,2高 switch(flow_level) { case 0: base_time 20000; break; // 车少 → 20s case 1: base_time 30000; break; // 正常 → 30s case 2: base_time 45000; break; // 车多 → 45s } // 步骤2倒计时控制TIM3 中断每 10ms 触发一次 if (Timer_IsExpired(TIM3)) { green_time 10; if (green_time base_time) { // 时间到切到黄灯态 CurrentState STATE_EW_YELLOW_NS_RED; green_time 0; Light_Set(EW_RED, NS_RED); // 先全红过渡 1s防冲突 Timer_Start(TIM4, 1000); // 启动 1s 全红定时器 } } }参数说明base_time不是写死的#define而是每次进入该状态时根据Sensor_GetFlowLevel()实时计算。Timer_IsExpired()封装了 TIM3 计数器比较标志位读取避免裸写寄存器出错。这种设计让“配时策略”和“硬件驱动”彻底分离——你想改成模糊控制只改switch块就行。2.4 车流检测驱动ADC 采样 滑动窗口滤波拒绝毛刺干扰sensor_driver.c是最容易翻车的模块。很多新手直接ADC_GetConversionValue(ADC1)拿一个数就判断车流结果仿真时 LED 疯狂抖动。本项目采用16 点滑动窗口中值滤波 三级阈值判别#define ADC_WINDOW_SIZE 16 static uint16_t adc_window[ADC_WINDOW_SIZE]; static uint8_t window_index 0; uint16_t Sensor_GetRawADC(void) { ADC_SoftwareStartConvCmd(ADC1, ENABLE); while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); // 等待转换完成 return ADC_GetConversionValue(ADC1); } // 滑动窗口中值滤波去脉冲干扰 uint16_t Sensor_GetFilteredADC(void) { uint16_t raw Sensor_GetRawADC(); adc_window[window_index] raw; window_index (window_index 1) % ADC_WINDOW_SIZE; // 中值滤波复制数组 冒泡排序16点足够快 uint16_t temp[ADC_WINDOW_SIZE]; for(uint8_t i0; iADC_WINDOW_SIZE; i) temp[i] adc_window[i]; for(uint8_t i0; iADC_WINDOW_SIZE-1; i) { for(uint8_t j0; jADC_WINDOW_SIZE-1-i; j) { if(temp[j] temp[j1]) { uint16_t t temp[j]; temp[j] temp[j1]; temp[j1] t; } } } return temp[ADC_WINDOW_SIZE/2]; // 返回中值 } // 三级流量判别基于 Proteus 中电位器 0–3.3V 映射 0–4095 uint8_t Sensor_GetFlowLevel(void) { uint16_t val Sensor_GetFilteredADC(); if(val 1200) return 0; // 低流量0–1.0V if(val 2800) return 1; // 中流量1.0–2.3V return 2; // 高流量2.3–3.3V }为什么必须滤波Proteus 仿真中 ADC 会受电源噪声影响单次采样波动可达 ±200 码值。中值滤波后波动压到 ±30 码值内对应电压误差 0.03V确保流量分级稳定。这是我在某高校课程设计答辩时被老师用示波器抓到的血泪经验——没滤波的版本绿灯时间在 25s–38s 之间随机跳变。3. 编译、烧录与仿真全流程从 Keil 点击 Build 到 Proteus 看见绿灯呼吸跑通这个项目本质是打通“代码 → 机器码 → 硬件行为”的全链路。下面步骤我按真实开发节奏写每一步都标出你在 Keil/Proteus 里具体点哪里、填什么、看什么。3.1 Keil 工程配置5 个关键设置决定能否生成正确 HEX打开TrafficLight.uvproj右键Target→Options for Target逐项核对Device 页ARM-Cortex-M3→STM32F103C8必须选对 Flash 大小 64KB否则链接失败Output 页勾选Create HEX File生成TrafficLight.hexProteus 需要它Name of Executable设为TrafficLightDebug 页Use:选ULINK2/ME Cortex Debugger即使不调试此选项影响启动代码生成Utilities 页Flash设置为STM32F1xx Flash烧录时用仿真可跳过C/C 页Define框中填USE_STDPERIPH_DRIVER, STM32F10X_MD启用标准库MD 表示中密度芯片注意如果 Build 后OUTPUT/目录下没有TrafficLight.hex一定是 Output 页没勾选Create HEX File。我见过太多同学卡在这步反复 Clean 后重 Build其实只是漏勾了一个框。3.2 Proteus 仿真启动3 步加载 HEX1 步看波形在 Proteus 8.13 中打开TrafficLight_Simulation.DSN双击 STM32F103C8T6 芯片→ 弹出属性窗口 →Program File栏点击文件夹图标 → 选择Keil/OUTPUT/TrafficLight.hex确认 Clock Frequency 为 72MHz与 Keil 中SystemInit()配置一致否则定时器走时不准点击左下角 Play 按钮▶启动仿真启动后立即观察LED 状态初始默认STATE_EW_GREEN_NS_RED东西向绿灯亮南北向红灯亮电位器调节双击POT-HG拖动滑块至右侧高流量约 5 秒后东西向绿灯时间自动延长至 45s看绿灯持续亮的时间按键验证点击SW1模式切换键LED 应切换为STATE_EW_RED_NS_GREEN且南北向绿灯时间也按当前车流动态调整提示若 LED 不亮先双击 LED 查看Fault是否为None若为Open检查 Proteus 中 LED 阳极是否接了限流电阻再连 GPIO若为Short检查是否误将阴极直接接地而没加电阻。3.3 真实硬件烧录ST-Link V2 接线与 STM32CubeProgrammer 配置当 Proteus 验证无误后可烧录到实体板。接线杜邦线颜色按惯例ST-Link V2STM32F103C8T6 最小系统SWCLK黄PA14SWCLKSWDIO蓝PA13SWDIOGND黑GND3.3V红3.3V仅供电不接 VCC烧录工具用STM32CubeProgrammer v2.16.0比 Keil 自带烧录器更稳打开软件 →Connect→ Interface 选ST-LINK→ ClickConnectLoad file→ 选择Keil/OUTPUT/TrafficLight.hexDownload→ 看到Download successful即可断开避坑如果 CubeProgrammer 提示No ST-LINK detected拔插 ST-Link 后重试若提示Target not responding检查 PA13/PA14 是否被其他外设占用本项目未占用可放心。3.4 关键信号抓取用 Proteus 虚拟示波器验证定时器精度Proteus 内置示波器是调试神器。验证TIM21ms 滴答是否准确在电路中添加OSCILLOSCOPE虚拟示波器将Channel A接PA9在timer_config.c中已配置为TIM2的 CH1 输出用于调试启动仿真 → 打开示波器界面 → 设置 Timebase 为1ms/div应看到清晰方波周期严格为 1ms10 格 × 1ms 10ms 周期若波形周期不准回 Keil 检查timer_config.c中TIM_TimeBaseStructure.TIM_Period 719972MHz / (7200×1000Hz) 1ms是否写错。这是嵌入式开发中最经典的“算错分频系数”翻车现场。4. 常见问题排查5 条真实踩坑记录覆盖 90% 新手报错这些不是教科书理论是我在帮 A 同学调试时记下的原始日志。每一条都对应一个具体现象、根本原因和可立即执行的解决动作。4.1 现象Proteus 启动后所有 LED 全灭且仿真时间停止走动原因Keil 工程中SystemInit()调用失败导致系统时钟未配置为 72MHz后续所有定时器、ADC 均无法工作。常见于startup_stm32f10x_md.s启动文件未正确链接或SystemCoreClock变量未在system_stm32f10x.c中更新。解决在 Keil 中打开system_stm32f10x.c找到SystemCoreClockUpdate()函数确认其内部RCC_GetClocksFreq()调用正常若仍无效直接在main.c开头添加强制配置RCC_DeInit(); RCC_HSEConfig(RCC_HSE_ON); while(RCC_GetFlagStatus(RCC_FLAG_HSERDY) RESET); RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9); // 8MHz * 9 72MHz RCC_PLLCmd(ENABLE); while(RCC_GetFlagStatus(RCC_FLAG_PLLRDY) RESET); RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);4.2 现象按键 SW1 按下无反应但 SW2全红能触发原因key_handler.c中GPIO_Init()对GPIOA的PA6SW1配置为GPIO_Mode_IN_FLOATING浮空输入但 Proteus 中按键未接上拉电阻导致引脚电平悬空读取值随机。解决修改key_handler.c中按键初始化部分将GPIO_PuPd改为GPIO_PuPd_UPGPIO_InitStructure.GPIO_PuPd GPIO_PuPd_UP; // 原为 GPIO_PuPd_NOPULL GPIO_Init(GPIOA, GPIO_InitStructure);同时在 Proteus 中为SW1的 MCU 端引脚添加10kΩ上拉电阻一端接 3.3V一端接 PA6。4.3 现象ADC 采样值始终为 0电位器调节无变化原因ADC1时钟未使能。RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_ADC1, ENABLE)必须在ADC_Init()之前调用否则 ADC 模块不供电。解决检查sensor_driver.c中Sensor_Init()函数确保时钟使能代码在最开头void Sensor_Init(void) { RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_ADC1 | RCC_APB2PERIPH_GPIOA, ENABLE); // 必须有 // ... 后续 GPIO、ADC 初始化 }4.4 现象Keil 编译报错Error: L6218E: Undefined symbol xxx如ADC_GetConversionValue原因FWLIB/目录下的stm32f10x_adc.c未被 Keil 添加进工程。Keil 默认只加.c文件但FWLIB/中部分文件如stm32f10x_adc.c可能因路径长被忽略。解决在 Keil 工程窗口右键Source Group 1→Add Existing Files to Group Source Group 1→ 手动导航至FWLIB/→ 全选所有.c文件共 12 个→Add→Close。4.5 现象Proteus 中 LED 亮度极低肉眼几乎不可见原因LED 限流电阻过大。Proteus 默认 LED 电流为 10mA若电阻用 10kΩ电流仅 0.33mA远低于可视阈值。解决双击每个 LED →Properties→ 将Current改为20mA双击每个限流电阻 → 将Resistance改为220Ω计算3.3V / 20mA ≈ 165Ω取标称值 220Ω 安全裕量足。5. 二次开发实战3 个可立即落地的改造方案附完整代码与验证方法这个项目的价值不在“跑起来”而在“改得动”。下面三个改造我都已在 Proteus 中实测通过每项改动不超过 20 行代码且不影响原有功能。5.1 改造一增加“行人请求按钮”并实现绿灯延长非抢占式需求在东西向绿灯期间若行人按下SW4则绿灯额外延长 10 秒最多延 2 次避免行人闯红灯。实现新增pedestrian_handler.c并在state_machine.c中注入逻辑// 在 state_machine.c 开头添加 #include pedestrian_handler.h extern uint8_t g_pedestrian_request_flag; // 行人请求标志由按键中断置位 // 修改 State_EW_Green_NS_Red_Handler() 中倒计时部分 if (Timer_IsExpired(TIM3)) { green_time 10; // 新增检测行人请求且未达最大延长时间 if (g_pedestrian_request_flag green_time base_time 20000) { base_time 10000; // 延长 10s g_pedestrian_request_flag 0; } if (green_time base_time) { // ... 原有切换逻辑 } }验证方法Proteus 中添加SW4按键接 PA9启动仿真后在东西向绿灯亮起时点击SW4观察绿灯时间是否从 30s → 40s → 50s 递增且第五十秒准时切黄灯。5.2 改造二用 UART 打印当前状态到串口助手调试利器需求开发时想知道状态机当前在哪一态、车流量等级、剩余时间避免全靠 LED 猜。实现启用USART1PA9/PA10在main.c中初始化并在状态机 handler 中添加打印// 在 main.c 的 SystemInit() 后添加 USART_InitTypeDef USART_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_USART1 | RCC_APB2PERIPH_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_9 | GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); USART_InitStructure.USART_BaudRate 115200; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode USART_Mode_Tx | USART_Mode_Rx; USART_Init(USART1, USART_InitStructure); USART_Cmd(USART1, ENABLE); // 在 State_EW_Green_NS_Red_Handler() 中添加每 1s 打印一次 static uint32_t print_timer 0; if (green_time - print_timer 1000) { printf(STATE: EW_GREEN_NS_RED | FLOW:%d | REMAIN:%dms\r\n, Sensor_GetFlowLevel(), base_time - green_time); print_timer green_time; }验证方法Keil 中Project → Options → C/C → Define添加USE_FULL_ASSERT, _GNU_SOURCEprintf重定向见retarget.c项目已提供打开 XCOM 串口助手波特率 115200即可看到实时状态流。5.3 改造三将固定配时升级为模糊 PID 控制进阶需求不再用“高/中/低”三级粗略划分而是根据车流变化率微分和历史平均积分动态调优绿灯时间让配时更平滑。实现在sensor_driver.c中新增Fuzzy_PID_Calculate()替代原Sensor_GetFlowLevel()// 简化版模糊 PID实际项目中可扩展规则库 float Fuzzy_PID_Calculate(uint16_t current_flow, uint16_t last_flow) { static float integral 0.0f; float error current_flow - 2000; // 期望车流 20001.65V float derivative current_flow - last_flow; integral error * 0.01f; // 积分项 // 模糊规则误差大 → 比例强变化快 → 微分强长期偏差 → 积分强 float output 0.8f * error 0.2f * derivative 0.05f * integral; return 30000.0f output; // 基准 30s 调整量 } // 在 State_EW_Green_NS_Red_Handler() 中替换 base_time 计算 // 原base_time ... switch ... // 改为 static uint16_t last_adc_val 0; uint16_t curr_adc Sensor_GetFilteredADC(); base_time (uint16_t)Fuzzy_PID_Calculate(curr_adc, last_adc_val); last_adc_val curr_adc;验证方法Proteus 中缓慢拖动电位器观察绿灯时间变化曲线——传统三级法是阶梯式跳变30s→45s而模糊 PID 是连续渐变30s→32s→35s→38s更符合真实交通流特性。从那以后我每次接手新的嵌入式控制项目都强制走一遍“Keil 编译 → Proteus 仿真 → 信号抓取 → 真机烧录”四步闭环。哪怕只是改一行#define也要让示波器看到波形、让串口看到日志、让 LED 按预期呼吸——因为嵌入式没有“大概能跑”只有“每一纳秒都可控”。这份智能交通灯代码它不炫技但每行都经得起示波器探头戳它不复杂但每个模块都留好了你插手的接口。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑