尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于STM32的交通灯识别与避障小车设计实现

基于STM32的交通灯识别与避障小车设计实现 简介这套基于STM32的交通信号识别智能小车项目以STM32C8T6为主控融合超声波避障、编码器测速、OLED显示、限速与红绿灯识别等功能适合毕业设计、课程设计、工程实训及各类单片机竞赛使用对初学嵌入式开发并希望完整复刻项目的读者同样友好。资源包含完整源码、工程文件与使用说明共419个文件涵盖C源文件、头文件、Keil工程配置、编译调试生成文件以及可直接烧录的HEX固件等类型整体10.34MB目录结构清晰便于按模块查看和重新编译。该项目在CSDN已有211人学习浏览代码与配置经过严格测试拿到后即可复现也可在其基础上扩展蓝牙控制、循迹或更多传感器功能。对硬件设计不熟悉的读者资源也提供了绕开PCB绘制的折中思路为不同基础的开发者降低复刻门槛。1. 交通信号识别小车内核其实是三条链路把“识别红绿灯”当成智能小车的主线是不少课设翻车的起点。真正跑得稳的交通信号识别小车主控就是STM32F103C8T6这颗72MHz的Cortex-M3它干的事情拆开看只有三条链路颜色传感器判断灯态、超声波测量前方距离、编码器闭环限制速度。三条链路互相独立再通过一个优先级仲裁机制串起来车才能在该停的地方停、该慢的地方慢。这套工程我完整拆过一遍代码基于标准库压缩包里连keilkill.bat这种清理脚本都带着适合做毕设、课设、大创也适合想把手头STM32玩出完整功能闭环的人。下文按硬件接线、颜色识别、速度闭环、调试技巧四个层面展开所有参数都给可复现的具体值。2. 硬件选型与引脚映射为什么红绿灯识别用TCS3200而不是摄像头2.1 主控与识别方案权衡F103C8T6只有72MHz主频、20KB RAM跑摄像头做实时颜色识别不是不行但代价很高。OV7670这类摄像头要处理RGB565数据、FIFO读写时序、DMA搬运一套下来光驱动就要啃一两周而且光照一变阈值全废。我在这个工程里用的是TCS3200颜色传感器它本质上是把颜色光强转换成方波频率红绿蓝三通道频率由S2、S3引脚选择输出。主控只需要做一件事测频率。C8T6的定时器和外部中断资源完全够用稳定性也比摄像头方案高一截。这套工程能在课设答辩现场稳定演示恰恰是因为识别环节够简单可靠。摄像头方案不是不能做而是对“拿到就能复现”的项目来说TCS3200的容错空间大得多。等你跑通整个闭环再回头把TCS3200换成OV7670颜色判断逻辑仍然可以复用。2.2 工程接线表拿到压缩包后先别急着烧录先核对引脚。每个版本的工程引脚宏可能微调下面是我测试时用的映射表全部在main.h里通过宏定义集中管理改线时只改一处模块引脚说明TCS3200 S0 / S1PB0 / PB1输出频率缩放比例选择TCS3200 S2 / S3PB2 / PB10颜色通道选择红绿蓝/无滤波TCS3200 OUTPB12方波输出接外部中断EXTI12HC-SR04 TrigPA2超声波触发脉冲HC-SR04 EchoPA3回波输入接外部中断EXTI3OLED SCL / SDAPB8 / PB9软件模拟I2C不占硬件I2C外设左轮编码器A/BPA0 / PA1TIM2编码器模式4倍频右轮编码器A/BPA6 / PA7TIM3编码器模式4倍频电机PWMPB6 / PB7TIM4 CH1 / CH2频率10kHz注意GPIO复用关系。PA0、PA1同时是TIM2的CH1、CH2PA6、PA7是TIM3的CH1、CH2这正是编码器模式需要的。OLED用软件模拟I2C是想避开标准库硬件I2C的坑PB8、PB9复用成普通推挽输出即可。电机驱动我用的是TB6612没有硬件I2C和引脚复用冲突比L298N的压降和发热都友好。2.3 Keil5工程结构和启动顺序解压后你会看到USART.axf、stm32f10x_tim.c、stm32f10x_flash.c、fonts.c和keilkill.bat。USART.axf是编译产物说明工程本身在Keil5里能直接编译不是半成品。keilkill.bat用来清理调试中间文件我习惯在发别人代码前跑一下几十MB能压到几百KB。stm32f10x_tim.c是定时器驱动编码器测速、PWM输出、超声波计时都靠它stm32f10x_flash.c是把颜色阈值写进Flash用的防止每次上电重新标定。main函数里的初始化顺序值得照抄外设初始化顺序错了后面查问题会非常痛苦int main(void) { delay_init(); // 系统滴答延时先于外设 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 抢占1位子优先级3位 USART1_Config(115200); // 调试串口打印颜色频率原始值 OLED_Init(); // I2C OLED显示车速和信号灯状态 TCS3200_Init(); // 配置PB12外部中断下降沿计数 Ultrasonic_Init(); // 配置PA2、PA3Echo中断计时 Encoder_Init(); // TIM2/TIM3编码器模式4倍频 Motor_Init(); // TIM4 PWM输出 方向GPIO Flash_ReadCalibData(); // 读取上次保存的RGB阈值 while (1) { TCS3200_ReadAllChannels(); // 轮流读RGB频率 TrafficLight_Decide(); // 判断红/绿/黄 Ultrasonic_Update(); // 更新距离 Speed_Control(); // 限速PID OLED_Display(); // 刷新显示 delay_ms(100); } }USART1放在最前面初始化是有原因的。USB转串口插上去任何一步卡住都能通过串口打印定位到具体外设。Flash读取放在电机初始化之后是因为阈值结构体里存了判断信号灯用的RGB基准值读不到就用默认值保证裸板也能跑。整个while循环的周期压到100ms颜色通道切换需要时间这个节奏刚好匹配交通灯的切换速度。3. 交通灯颜色识别频率采集、白平衡与阈值判断3.1 TCS3200输出频率与S0/S1/S2/S3的关系TCS3200内部有光电二极管阵列按颜色分组。S2、S3的组合决定哪组二极管接入电流-频率转换器S0、S1决定输出频率的缩放比例S2S3通道LL红色HH绿色LH蓝色HL无滤波Clear缩放比默认设成20%也就是S0接高电平、S1接低电平。20%这个档位对室内和室外光环境适应性最好100%档在强光下会饱和2%档在弱光下分辨率不够。引脚配置就是一组GPIO这里不贴了重点在看怎么把频率读准。读频率的经典做法是外部中断计数配合定时器闸门控制。PB12接TCS3200的OUT引脚配置成下降沿触发每来一个脉冲进一次中断计数值加一。另一个定时器产生100ms周期中断每个周期读取计数值后清零换算成Hz。这样CPU不需要死等脉冲PWM输出和超声波测距不受影响。3.2 100ms频率采样代码通道切换后不能立刻读数内部光电二极管和频率转换器需要稳定时间。实测S2/S3切换后至少等10ms频率输出才稳定。我把这个延时直接放在通道切换函数注释里#define S2_GPIO GPIOB #define S2_PIN GPIO_Pin_2 #define S3_GPIO GPIOB #define S3_PIN GPIO_Pin_10 volatile uint32_t tcs_pulse_cnt 0; uint16_t freq_r 0, freq_g 0, freq_b 0; void EXTI15_10_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line12) ! RESET) { tcs_pulse_cnt; // 每个下降沿累计一次 EXTI_ClearITPendingBit(EXTI_Line12); } } void TCS3200_ReadChannel(uint8_t s2, uint8_t s3) { GPIO_WriteBit(S2_GPIO, S2_PIN, s2 ? Bit_SET : Bit_RESET); GPIO_WriteBit(S3_GPIO, S3_PIN, s3 ? Bit_SET : Bit_RESET); delay_ms(10); // 等待内部频率稳定不能省 tcs_pulse_cnt 0; TIM_SetCounter(TIM1, 0); // 用TIM1做100ms闸门 while (TIM_GetCounter(TIM1) 9000); // 72MHz主频9000计数值10ms freq_temp tcs_pulse_cnt; // 10ms内的脉冲数 freq_hz freq_temp * 100; // 换算成Hz }这里有个工程技巧100ms闸门没必要用定时器中断轮询TIM1计数器到9000次就是10ms配合TCS3200输出范围正好。S0/S1设成20%缩放比时典型输出在10kHz到100kHz之间10ms采样窗口能拿到稳定的计数。换到100%档位时脉冲数会饱和到定时器溢出所以不建议。3.3 红绿灯判断阈值表与判断代码拿到RGB三个频率后不能直接用绝对频率判断。环境光强度、传感器与灯的距离都会让绝对频率整体偏移。我用的方法是归一化比值判断把R通道频率除以G、B通道的均值突出红色分量比例。#define RATIO_RED 1.6f // 红色占比高 - 红灯 #define RATIO_GREEN 1.5f // 绿色占比高 - 绿灯 uint8_t TrafficLight_Decide(void) { float ratio_r (float)freq_r / ((float)freq_g 1.0f); float ratio_g (float)freq_g / ((float)freq_r 1.0f); if (ratio_r RATIO_RED freq_r 20000) return LIGHT_RED; if (ratio_g RATIO_GREEN freq_g 20000) return LIGHT_GREEN; return LIGHT_UNKNOWN; }归一化比值的优势在于抗光照。中午太阳直射和傍晚弱光下RGB三个通道的频率会同比例下降但比值变化不大。加一个绝对频率下限是为了避免全黑场景下噪声干扰造成的误判。阈值表参考值如下但这组数值必须在实际场景里重新标定场景红灯R/G比绿灯G/R比黄灯特征室内荧光灯 1.8 1.6R和G都偏高室外阳光 1.5 1.4需要Clear通道校正逆光环境 1.3 1.3建议加遮光罩3.4 光环境干扰处理最容易踩的坑有三个。第一个是LED反光黑色亚克力板表面会出现镜面反射把环境光直接反射进传感器解决方法是传感器前端加10mm高的遮光筒只留正面进光。第二个是白平衡漂移白纸在两个场景下标定出的RGB基准值能差30%所以工程里把校准值存进Flash每次换场地重新标定一次而不是烧一次程序用到底。第三个是黄灯判断TCS3200测黄灯时R、G两个通道都会升高靠单一阈值分不开我这里用的策略是R和G同时高于某个绝对值时判黄并给出停车或低速通过的决策交给上层仲裁。4. 避障、编码器测速与限速联动控制仲裁和速度闭环4.1 超声波测距用输入捕获而不是delayHC-SR04的标准驱动是Trig拉高10us然后死等Echo拉高再用delay测量高电平持续时间。这种阻塞式写法在只有测距的程序里没问题但放在这个小车里Echo等待期间电机PWM和颜色采样全部卡死速度闭环直接失效。工程里改成中断捕获方式Trig发脉冲Echo上升沿打开TIM1下降沿读取计数值。uint32_t echo_time 0; void EXTI3_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line3) ! RESET) { if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_3) Bit_SET) { TIM_SetCounter(TIM1, 0); TIM_Cmd(TIM1, ENABLE); // 上升沿开始计时 } else { TIM_Cmd(TIM1, DISABLE); // 下降沿停止 echo_time TIM_GetCounter(TIM1); } EXTI_ClearITPendingBit(EXTI_Line3); } } uint16_t Ultrasonic_GetDistance(void) { return echo_time / (72 * 58); // 单位cm }TIM1挂在APB2总线上时钟72MHz计数器每计数一次代表1/72微秒。声速340m/s时往返距离和时间的换算系数是58us/cm。echo_time除以(72*58)直接把计数值换算成厘米。注释里写清楚后面换芯片时不用再推导一遍。60ms没收到回波就认为是超量程返回一个标志位避免障碍物太近导致Echo一直高电平卡死程序。4.2 编码器4倍频测速与速度换算测速部分我用的TIM2、TIM3编码器模式。霍尔编码器A、B相接定时器的CH1、CH2配置成TI12模式后定时器在A、B相的每个边沿都计数也就是4倍频。标准库配置很固定void Encoder_Init_TIM2(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructInit(TIM_TimeBaseStructure); TIM_TimeBaseStructure.TIM_Prescaler 0; TIM_TimeBaseStructure.TIM_Period 0xFFFF; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); TIM_EncoderInterfaceConfig(TIM2, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); TIM_Cmd(TIM2, ENABLE); }A、B相输入浮空是标准做法编码器输出是开漏形态STM32内部带上拉才能读到稳定电平。速度换算参数我整理成了表方便换电机时核对参数数值说明编码器线数13线/圈霍尔码盘刻线减速比1:30电机输出轴到车轮车轮直径65mm常规TT电机轮4倍频后每圈计数4 × 13 × 30 1560TIM计数值/圈换算成米每秒公式v (当前计数值 - 上次计数值) / 1560 × π × 0.065 / Δt。我用一个1Hz的定时器中断做速度采样计数值差值直接乘以0.2就是m/s误差在±3%以内对限速控制完全够用。4.3 控制优先级与限速规则三条链路的数据汇聚后不能简单地和在一起做PID。我的仲裁逻辑定成这张表优先级从上到下递减优先级条件动作1超声波距离 20cmPWM立即拉0刹车2识别到红灯PWM拉0等待绿灯3识别到黄灯速度减半4编码器速度 限速值减小PWM比例调节5正常巡航按目标速度做PID闭环这个顺序非常关键。如果先做速度PID再做避障20cm的紧急距离被PID的积分项抵消车会顶着障碍物向前冲。限速逻辑不能靠if硬切那样车速会一顿一顿的我在这里用一个增量式PID做速度环输出直接叠加到PWM基值上void Speed_Control(void) { static float target_speed 1.2f; // m/s巡航限速 float cur_speed Get_Speed_Mps(); if (ultrasonic_dist 20.0f || light_state LIGHT_RED) { Motor_SetPWM(0); pid_err_last 0; // 清积分防止重启时过冲 return; } float err target_speed - cur_speed; pid_integral err * 0.1f; pid_integral LIMIT(pid_integral, -1.0f, 1.0f); // 积分限幅 float out 50.0f pid_kp * err pid_ki * pid_integral; out LIMIT(out, 0.0f, 100.0f); Motor_SetPWM((uint8_t)out); }积分限幅是增量式PID最容易漏的一环。小车被红灯拦停时误差一直为负积分项会持续累计到很大绿灯一亮车就猛然窜出去。我在红灯分支里把积分清零还不够正常运行时的积分也限到±1.0的占空比范围保证起步平滑。PID参数我调的是kp0.8ki0.15100ms控制周期电池电压不同时这个参数要微调。5. OLED实时显示与三处容易被忽略的调试点5.1 白平衡一键标定换场地后交通灯颜色判断失效90%是因为没重新标定。我在小车侧面加了一个按键长按1秒进入标定模式把白纸放在传感器前方程序自动采集当前RGB频率作为基准值调整归一化阈值。void Calib_KeyScan(void) { if (KEY_LongPress()) // 连续按下1秒 { calib_r_base TCS3200_ReadChannel(0, 0); calib_g_base TCS3200_ReadChannel(1, 1); Flash_SaveCalibData(); // 写进Flash最后4页 OLED_ShowString(0, 3, Calib OK); } }标定值保存用stm32f10x_flash.c里的库函数擦除、写入都在运行期完成。注意F103C8T6的Flash按页擦除一页1KB每次标定前先把阈值结构体整体擦掉再写避免新旧数据交错。5.2 USART看原始频率值判断阈值调不好时先别改代码里的常量。用串口把RGB三个通道的原始频率打出来看数据是整体漂移还是单通道异常int fputc(int ch, FILE *f) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); USART_SendData(USART1, (uint8_t)ch); return ch; }printf重定向到USART1后在TrafficLight_Decide前面加一行printf(R:%d G:%d B:%d\n, freq_r, freq_g, freq_b)频率和归一化比值一眼就能看出问题。绿灯误判成红灯时看打印里的R/G比如果低于1.5说明阈值偏紧直接改RATIO_RED常量就行。5.3 摄像头方案扩展TCS3200跑通后想升级摄像头颜色判断部分不要推翻重写。换成OV7670后把RGB888采出来仍然走归一化比值这条路ratio R / (G 1)。只要阈值表重新标定仲裁逻辑、避障、限速全部原样保留。工程里stm32f10x_tim.c和stm32f10x_flash.c这两个文件到时候才知道有多值——Flash里存的不只是颜色阈值我把PID参数和限速值也放进去了上位机改参数不用重新烧程序。学习这套工程时最先改的应该是S0/S1的频率缩放比如果串口打印的频率值全在50000Hz以上说明100%档位太高把S1从低电平改成高电平切到20%档整条识别链路马上稳定。本文还有配套的精品资源点击获取
返回列表