资讯动态

STM32多功能小车实战:循迹避障灭火遥控全解析

发布时间:2026/10/13 15:56:51 来源:尧图企业网站定制
简介面向嵌入式入门者与电子设计竞赛参赛者基于STM32单片机的智能遥控循迹避障灭火小车资源包覆盖遥控、循迹、避障、灭火等完整功能可直接复现为课程设计或科创项目的参考模板。压缩包共197个文件约6.9MB内部以C语言源文件、H头文件、Keil工程文件uvprojx、uvoptx、sct和编译中间产物o、d、crf、axf、map、lst为主并附有清理脚本便于在Keil MDK下直接打开、编译与烧录。已有446人学习适合需要快速落地STM32多功能小车的开发者。借助其中完整的工程源码与外围驱动读者可了解GPIO、定时器、ADC、I2C、CAN等外设的开发思路学习从外设初始化到控制逻辑的代码组织方式并通过循迹避障、无线遥控与电机驱动的联调过程掌握多功能小车落地的关键方法节省从零搭建硬件和编写驱动的时间。1. 从遥控到自动灭火STM32小车到底在做什么一个能循迹、能避障、能灭火、还能遥控的STM32小车听上去像把好几个独立课程设计拼在一起但真正动手后会发现它不是在堆功能而是把所有外设和中断资源逼到极限。很多开发者第一次做完这个项目后最大的收获不是“跑起来了”而是终于理解了芯片手册上那些寄存器为什么按那样的顺序配置。这个项目适合两类人一类是刚学完STM32标准外设、想找一个能覆盖GPIO、定时器、PWM、串口、中断的综合练手题另一类是准备电赛或毕设、需要快速搭建一个能演示的多功能移动平台。它不像单模块实验那样跑通就结束而是要处理传感器互斥、电源噪声、逻辑冲突这些真正工程里才会遇到的问题。我接触过不少做这个题目的同学真正拉开差距的从来不是代码量而是对“状态优先级”和“响应时序”的理解。接下来这篇文章我会按自己做这套系统的顺序来拆先讲硬件选型怎么定再讲循迹、避障、灭火三个模块怎么一步步落地最后把联调时最容易翻车的问题列出来。你拿到的不只是一堆代码片段而是一条可复现的完整调试路径。2. 硬件选型与系统架构先定主控、传感器和驱动再写代码2.1 为什么选STM32F103C8T6性价比与生态的平衡点做这个题目主控选择没有太多悬念。STM32F103C8T6这颗芯片几乎是国内嵌入式学习路径上绕不开的型号72MHz主频、64KB Flash、20KB SRAM对于循迹避障灭火这种负载来说绰绰有余。更重要的是它的外设资源分布很友好有3个USART、4个16位定时器、37个GPIO刚好够把遥控接收、超声波触发、红外循迹、PWM电机驱动全部挂上去而不用抢引脚。有人会问用F103C8T6是不是太老了换成F407或者G431会不会更好我的看法是这个项目的瓶颈不在算力而在传感器布局和机械结构。F103的定时器输出4路PWM驱动两路电机完全没问题串口波特率跑到115200也轻松。除非你后续要加摄像头做视觉循迹才需要考虑换H7系列否则F103C8T6是性价比最稳的选择。常见的购买渠道上一块最小系统板加下载器大概几十块钱配套的传感器模块也都是通用型号坏了直接换新成本很低。这对新手来说是个很实际的考量——调试过程中烧坏几个传感器是很正常的事如果主控板太贵心理压力会大很多。2.2 传感器选型清单红外循迹、超声波避障、火焰探测怎么配循迹模块我一般推荐用TCRT5000红外反射传感器这是做循迹小车最经典的方案。它本质是一个红外发射管加一个光电晶体管黑色地面反射率低白色地面反射率高通过比较器输出高低电平ADC都用不上。一套模块上通常集成两路或四路探头做成寻迹线的排布。这里有个关键参数传感器离地高度要控制在1cm到1.5cm之间太高会误判环境光太低容易被地面不平卡住。避障模块推荐HC-SR04超声波测距模块测距范围2cm到400cm精度在3mm左右响应速度完全够用。它的工作原理是给Trig引脚一个10us以上的高电平脉冲模块内部自动发射8个40kHz的超声波脉冲然后Echo引脚输出与飞行时间成正比的高电平。主控只需要用定时器测量Echo高电平持续时间除以58就能得到厘米数。灭火模块的核心是火焰传感器常见做法是用一个对红外光敏感的二极管加比较器电路做成模块检测火焰发出的红外光谱输出数字信号或者模拟电压。更稳妥的方案是把火焰传感器模块的输出接到ADC引脚通过设定阈值来判断火焰方向和距离。执行机构通常有两种选择一个是直流风机加继电器适合扑灭蜡烛火焰一个是微型水泵适合演示场景但需要带水箱。电机驱动方面两驱小车配一个TB6612FNG驱动模块就够了它比L298N体积小、效率高峰值电流1.2A支持PWM调速和正反转控制。注意TB6612的逻辑电源和电机电源要分开接电机电源用两节18650锂电池串联也就是7.4V逻辑电源用5V共地必须连上不然PWM波形会乱掉。模块型号/方案关键参数购买注意点循迹TCRT5000×3检测距离1~1.5cm选带电位器可调阈值的版本避障HC-SR042~400cm精度3mmTrig和Echo直连GPIO注意电平5V兼容火焰红外火焰传感器检测角度60°~90°选模拟输出版本用ADC读更准驱动TB6612FNG峰值1.2A/通道芯片本身发热正常需焊散热铜箔主控STM32F103C8T672MHz/64KB Flash买带串口下载电路的最小系统板2.3 电源设计为什么单独给电机供电是关键这个项目90%的诡异问题都出在电源上。常见的错误做法是电机驱动模块、单片机、传感器全部从同一个5V电源取电结果电机一启动单片机立刻复位或者超声波测距数值乱跳。原因是直流电机的启动电流可以达到正常工作电流的5倍以上瞬间拉低母线电压单片机供电低于3.3V就会触发掉电复位。我常用的电源架构是双电源方案两节18650锂电池串联输出7.4V直接接到TB6612的VM引脚给电机供电同时7.4V经过一个AMS1117-5.0降压到5V再经过AMS1117-3.3降压到3.3V给单片机和传感器供电。这样电机电源和逻辑电源在物理上分开只在TB6612模块上共地。市售的降压模块很多如果怕AMS1117发热可以用MP1584这种DC-DC降压模块效率高很多。还有一个细节容易被忽略电池电压不是恒定的。18650充满电是4.2V两节串联就是8.4V放电截止一般是3.0V两节是6.0V。这意味着电机驱动输入的电压范围在6.0V到8.4V之间波动。如果用一个固定5V输出的降压模块需要确认模块输入电压下限低于6.0V否则电池没电时模块先进入欠压保护小车还没跑就没劲了。选择输入范围4.5V到12V的降压模块可以留出余量。电源接线的顺序也有讲究。我习惯先把电机电源接好确认驱动模块亮灯再接单片机电源最后接传感器电源。这样每一级都可以单独判断是否供电正常。如果一上电全部亮但小车不动就可以按这个顺序排查是哪一级出了问题。2.4 引脚分配一个不冲突的GPIO规划表在写代码之前先花十分钟做引脚分配能省掉后面至少两小时的重复改线时间。STM32F103C8T6虽然有37个GPIO但要注意USART1的TX/RX默认接在PA9/PA10上下载器烧录时要占用这两个引脚。开关机或者下载程序时如果传感器正好接在这两个引脚上可能会造成电平冲突。以下是我常用的一个引脚规划方案PA0超声波Trig输出PA1超声波Echo输入配置为浮空输入PA2火焰传感器模拟输入ADC1_IN1PA3循迹传感器左路输入PA4循迹传感器中路输入PA5循迹传感器右路输入PA6TB6612 PWMA左电机PWMPA7TB6612 PWMB右电机PWMPB0TB6612 AIN1PB1TB6612 AIN2PB10TB6612 BIN1PB11TB6612 BIN2PB12继电器控制脚灭火风机PB13遥控信号输入如果有独立遥控接收头PA9/PA10保留给串口用来调试输出这个分配方案的考虑是把PWM相关的引脚放在PA6/PA7因为这两个引脚是TIM3的通道1和通道2可以复用为定时器输出不用额外映射。ADC引脚集中在PA通道配置ADC1时方便。火焰传感器和超声波放在PA0到PA2这三个引脚在F103上属于GPIOAADC和TIM2_ETR有复用但我们的用法互不冲突。需要注意的是如果你用的是四路循迹模块PA3到PA5可能不够用可以扩展PB3、PB4、PB5这三个引脚。PC13到PC15这三个引脚在F103上比较特殊它们要驱动板载晶振或LED的话不能随便作普通输入输出除非你把对应的RCC配置改掉。建议不要用这三个引脚接传感器否则配置起来很麻烦容易踩坑。3. 循迹与避障模块从GPIO读到电机动作的最小实现3.1 循迹的基本原理三个红外探头如何判断行驶方向循迹小车的逻辑本质上是一个三输入的单输出决策系统三个红外探头分别检测左、中、右三条路径上的地面反射情况然后决定电机左转、右转还是直行。正常黑线宽度在2cm到3cm小车居中行驶时中间探头在黑线上方输出低电平左右探头在地面上方输出高电平。一旦车身偏向一侧相应位置的探头就会读到黑线输出状态改变。TCRT5000模块的输出通常是数字电平板上已经集成比较器可以通过电位器调节触发阈值。接线时只用一个GPIO读高低电平就行不需要ADC。三个探头的间距是核心设计参数间距太大小车在弯道上容易丢失黑线间距太小转向动作太敏感容易左右摆动。我用过最顺手的间距是1.5cm到2cm这个距离在标准的白色赛道胶带宽2cm上表现稳定。循迹控制的核心代码就是一个状态判断函数根据三个探头的组合决定电机输出。这里要注意一个反直觉的点循迹轮子差速转向时并不是转向侧电机减速或者停止而是转向侧电机反转或者刹车会更激进让车头快速扭过来。但如果没有PID控制反转容易过冲所以我通常选择减速加少量反转的组合具体系数在调试时定。uint8_t sensor_left, sensor_mid, sensor_right; void track_read(void) { sensor_left GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_3); sensor_mid GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_4); sensor_right GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_5); } void track_control(void) { track_read(); if (sensor_mid 0 sensor_left 1 sensor_right 1) { motor_set(LEFT_SPEED, RIGHT_SPEED); // 直线行驶 } else if (sensor_left 0) { motor_set(LEFT_SPEED - TURN_DELTA, RIGHT_SPEED TURN_DELTA); // 左偏往右修 } else if (sensor_right 0) { motor_set(LEFT_SPEED TURN_DELTA, RIGHT_SPEED - TURN_DELTA); // 右偏往左修 } else if (sensor_left 0 sensor_right 0) { motor_set(0, 0); // 三路全部离线或到达十字路口停车 } }这段代码的逻辑是优先判断是否直行再判断是否偏离。有一个细节值得注意最后那个传感器全黑的判断——当小车行驶到十字路口或者全部探头都脱离黑线时需要手动指定一种策略比如停车或者按上次方向继续直行。我一般选择停车加蜂鸣器提示避免小车冲出赛道。参数说明LEFT_SPEED和RIGHT_SPEED是基础速度值建议初始设置在400到500之间对应PWM的占空比满值是999。TURN_DELTA是修正量初始值建议100。如果小车出现蛇形摆动说明TURN_DELTA太大或者控制周期太长如果弯道转不过去说明TURN_DELTA太小。控制周期建议设在10ms到20ms即主循环每10ms调用一次track_control()。3.2 超声波避障定时器输入捕获的正确打开方式超声波避障最关键的技术点不是发射而是接收回波的时序测量。HC-SR04的Echo引脚输出一个与声波往返时间成正比的高电平脉宽需要精确测量这个脉宽。常见做法有两种一种是阻塞式延时轮询就是不停地读Echo引脚电平直到变低用SysTick记录耗时另一种是定时器输入捕获通过硬件捕获上升沿和下降沿自动记录计数值精度更高且不阻塞主循环。阻塞式延时在低速循迹小车里也能用但它有一个问题在等待Echo返回的期间主控无法处理循迹和遥控指令。超声波测距一次最长等待时间约24ms对应400cm如果主循环恰好卡在这里电机PWM输出会仍然保持上次的值小车可能会出现危险动作。所以我推荐用定时器输入捕获或者至少把测距放在主循环的最后一步避免阻塞关键控制逻辑。用TIM2做输入捕获的配置步骤比较标准把TIM2的CH1映射到PA1配置为上升沿和下降沿都捕获开启捕获中断。上升沿到来时记录第一个计数值下降沿到来时记录第二个计数值两者之差乘以定时器时钟周期再除以58就是距离厘米数。这种方案的难点在配置代码较长但一旦写好后续读取距离就变成读变量的事非常省心。uint32_t distance_cm, cap_val_rise, cap_val_fall; void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_CC1) ! RESET) { if (TIM_GetCapture1(TIM2) cap_val_rise) { cap_val_fall TIM_GetCapture1(TIM2); distance_cm (cap_val_fall - cap_val_rise) / 58; TIM_ClearITPendingBit(TIM2, TIM_IT_CC1); } else { cap_val_rise TIM_GetCapture1(TIM2); cap_val_fall 0; TIM_ClearITPendingBit(TIM2, TIM_IT_CC1); } } }这段代码里通过比较当前捕获值是否大于上次的上升沿值来判断是上升沿还是下降沿。这里有个实际的坑当超声波回波非常近的时候Echo高电平时间非常短如果中断响应不及时两次捕获可能合并成一次读到的distance_cm会是一个异常大的数。解决办法是配置一个超时计时器如果500ms内没有收到下降沿就把distance_cm置为最大值999表示前方无障碍。参数说明TIM2的预分频系数建议设为72-1这样计数频率是1MHz计数器每个刻度代表1us。声速340m/s时往返距离1cm需要约58us所以(计数差值)/58就得到厘米。这个公式在温度变化时会有些误差但在室内环境下足够使用。3.3 避障策略右边优先还是直接刹车超声波拿到距离数据之后下一步不是立刻打方向而是先制定一个决策策略。最简单的策略是前方距离小于30cm时停车然后右转如果右侧也有障碍物就左转。这个策略在实验室环境里表现不错但有明显的限制——如果小车在墙角它会左右反复转向出不去。稍微改进一点的策略是记录最近几次的转向方向如果连续两次向同一个方向转仍然无法脱困就切换为倒车加转向的组合动作。这个逻辑有点像一个简易的迷失恢复机制代码量不大但能让小车看起来“聪明”很多。我见过有同学直接用随机转向来脱困偶尔也能成功但在演示场合很不稳定不建议。避障优先级在代码里要高于循迹。理由是循迹依赖地面而避障依赖前方空间当前方有障碍物时继续循迹大概率会撞上去。所以主循环里我一般先测距距离小于阈值就进入避障状态不执行循迹逻辑距离大于阈值才走循迹分支。主循环调度 1. 读取传感器状态循迹、超声波、火焰 2. 如果火焰传感器报警 - 进入灭火状态最高优先级 3. 如果前方障碍距离 30cm - 进入避障状态 4. 否则 - 循迹状态 5. 刷新电机PWM输出4. 灭火与遥控联动PWM调速、状态机与无线指令解析4.1 火焰传感器ADC读取要比数字输出靠谱得多火焰传感器市面上有两种输出一种是数字输出板上比较器直接输出高低电平另一种是模拟输出输出电压随火焰强度变化。强烈建议选模拟输出版本因为数字输出模块的阈值是固定电位器调好的对环境和火光强度的适应性很差。用ADC读模拟电压的话可以在程序里设定动态阈值甚至根据电压大小粗略判断火焰远近。火焰传感器模块的核心是红外接收二极管它对波长在760nm到1100nm的红外光敏感蜡烛火焰在这个波段有很强的辐射峰。但这里有个阴影区阳光和普通白炽灯也包含大量红外成分会导致误报。解决方案有两个一是让传感器朝下或者侧下方安装避开天花板的照明直射二是在代码里要求连续多次超过阈值才确认火焰比如连续5次采样都超过阈值才触发灭火。STMF103的ADC配置比较直接使用ADC1的通道1对应PA2引脚。采样时间建议设置为55.5个周期以上因为火焰传感器的内阻不是固定的太短的采样时间会导致采样电容充不满读出来的数值偏小。uint16_t flame_value; void flame_init(void) { GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_2; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; GPIO_Init(GPIOA, GPIO_InitStructure); ADC_InitStructure.ADC_Mode ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode DISABLE; ADC_InitStructure.ADC_ContinuousConvMode ENABLE; ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel 1; ADC_Init(ADC1, ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_55Cycles5); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1)) ; ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1)) ; } uint16_t flame_read(void) { ADC_SoftwareStartConvCmd(ADC1, ENABLE); while (!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)) ; return ADC_GetConversionValue(ADC1); }逻辑说明flame_init()里面做了一次ADC校准这是F103芯片手册里明确要求的操作很多人忘了做会读出一个固定的0或满量程值。配置ContinuousConvMode为ENABLE后flame_read()只需要启动一次转换并等待EOC标志位返回的就是当前转换值。初始化完成后调用flame_read()的返回值和环境光线有关正常情况下室内白炽灯下约在1500到2500之间蜡烛在20cm处能读到3500以上。参数说明ADC_SampleTime_55Cycles5这个采样时间约合1.2usADC时钟14MHz时对于火焰传感器这类高内阻源来说刚好。如果你读到的数值跳动幅度超过±200说明采样时间过短可以改到239.5周期。4.2 灭火执行机构风机还是水泵继电器怎么接线灭火执行机构是这个项目里最需要提前规划的部分因为电流和阀门控制都可能出问题。最常见的方案是用一个5V或者12V的直流风机对准火焰吹灭。选风机时要看风量和尺寸5V风机虽然省事但风量偏小离火焰20cm以上就吹不灭了12V风机风力足但要额外一路12V电源或者用升压模块从7.4V电池升压。继电器的接线有三点要注意第一继电器的线圈驱动电流约70mA左右不能直接连STM32引脚必须加一个三极管或者用ULN2003来驱动第二继电器线圈两端要反向并联一个1N4007二极管不然断开瞬间的反电动势容易打坏单片机引脚第三继电器触点额定电流要大于风机的启动电流一般选10A规格会比较稳。void extinguish_start(void) { GPIO_SetBits(GPIOB, GPIO_Pin_12); // 继电器吸合风机开始工作 motor_set(0, 0); // 立刻停车防止小车冲过火焰 } void extinguish_stop(void) { GPIO_ResetBits(GPIOB, GPIO_Pin_12); // 继电器断开风机停止 }逻辑说明extinguish_start()做的事很简单——先停车再打开风机。停车放前面是刻意的小车带着速度冲向火焰时风机会把火焰吹向后方如果不停车火焰可能会被吹到小车后方设备上。motor_set(0,0)是电机控制的统一接口传入两个摩托速度0就是完全停止。这是我在这个项目里坚持的原则任何紧急动作前先刹车再执行后续动作。如果你做的是水泵方案代码逻辑完全一样只是把继电器控制的负载换成水泵。水泵方案需要额外注意水路布局和水箱重量重心变动会影响循迹稳定性。我做过的两个项目一个用风机一个用水泵综合体验是风机更好——不用补水不会漏水维护简单。4.3 遥控功能用无线模块还是手机蓝牙指令解析怎么写遥控功能在这个项目里的定位是“人工接管”也就是在赛道太复杂或者传感器失灵时让人通过无线手柄或者手机APP直接控制小车运动。最简单的方案是用一个2.4G无线模块接收端接在STM32的串口上。发射端用一个配套的遥控手柄按按键直接发出对应指令码。蓝牙方案也很常见用HC-05蓝牙模块连手机通过串口发送字符指令。遥控指令格式我自己常用一种简单的协议一个字节的帧头0xAA加一个字节的指令码再加一个字节的校验和。指令码定义0x01前进、0x02后退、0x03左转、0x04右转、0x00停车。接收状态机在串口中断里解析解析出完整指令后置一个全局标志位主循环里处理这个标志位并执行对应的电机动作。#define FRAME_HEAD 0xAA volatile uint8_t remote_cmd 0x00; uint8_t rx_buffer[3]; uint8_t rx_index 0; void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { uint8_t data USART_ReceiveData(USART1); if (rx_index 0 data ! FRAME_HEAD) { return; // 帧头不对丢弃 } rx_buffer[rx_index] data; if (rx_index 3) { if ((rx_buffer[0] rx_buffer[1]) rx_buffer[2]) { // 校验和 remote_cmd rx_buffer[1]; } rx_index 0; } } } void remote_apply(void) { switch (remote_cmd) { case 0x01: motor_run(FORWARD); break; case 0x02: motor_run(BACKWARD); break; case 0x03: motor_run(TURN_LEFT); break; case 0x04: motor_run(TURN_RIGHT); break; case 0x00: motor_stop(); break; default: break; } }参数说明串口波特率设为9600还是115200都行取决于遥感手柄的配置。我用过的一款无线模块默认9600里面有1个起始位、8个数据位、1个停止位。校验和算法简单实用帧头和指令码相加低字节作为校验值。如果校验失败直接丢弃整帧不会产生误动作。remote_cmd用了volatile修饰符这是C语言里告诉编译器该变量可能在中断里被修改主循环里读取时不要做优化缓存。很多人忘了加这个关键字结果主循环里读到的指令永远是旧值浪费了大量时间排查。4.4 状态机设计如何优雅地处理多模式切换到这一步小车已经有四种工作模式遥控模式、循迹模式、避障模式、灭火模式。如果不用状态机代码会变成一坨if-else嵌套而且模式切换的边界条件会越来越混乱。我见过有同学用单个全局标志位表示当前模式结果遥控和循迹同时工作时电机转速指令打架小车原地抽搐。正确做法是定义一个枚举类型的系统状态主循环里用一个switch语句来分发。状态之间通过触发条件迁移每个状态内部只处理自己的逻辑。这样做的核心收益是同一个时刻只有一个状态在执行不会出现两种模式抢控制权的问题。typedef enum { MODE_REMOTE, MODE_TRACK, MODE_AVOID, MODE_EXTINGUISH } SysMode; SysMode current_mode MODE_REMOTE; uint32_t mode_switch_time 0; // 用于防抖 void mode_update(void) { // 火焰检测优先级最高任何模式下都直接切换 if (flame_continuous_count 5) { current_mode MODE_EXTINGUISH; mode_switch_time get_tick(); } else { // 遥控指令有效则进入遥控模式并清零超时计数 if (remote_cmd ! 0x00 remote_cmd ! 0xFF) { current_mode MODE_REMOTE; remote_cmd 0x00; // 指令只执行一次 } else if (current_mode MODE_REMOTE) { // 遥控超时检测5秒无指令自动回到循迹模式 if (get_tick() - mode_switch_time 5000) { current_mode MODE_TRACK; } } else if (current_mode MODE_TRACK) { if (obstacle_distance 30) current_mode MODE_AVOID; } else if (current_mode MODE_AVOID) { if (obstacle_distance 40) current_mode MODE_TRACK; } // 灭火结束条件 if (current_mode MODE_EXTINGUISH extinguish_timeout 3000) { current_mode MODE_TRACK; extinguish_stop(); } } } void motor_control_dispatch(void) { switch (current_mode) { case MODE_REMOTE: remote_apply(); break; case MODE_TRACK: track_control(); break; case MODE_AVOID: avoid_control(); break; case MODE_EXTINGUISH: extinguish_start(); break; default: break; } }这个状态机的巧妙之处在于灭火状态优先级最高从任何模式都能进入遥控模式不抢优先级只在主动收到指令时介入遥控超时5秒后自动切回循迹防止人走开小车失去控制。mode_update()必须在主循环里以固定周期调用周期建议在10ms左右这样模式切换的响应时间在几十毫秒内人的体感是“瞬间切换”。参数说明flame_continuous_count是火焰连续计数器每次flame_read()超过阈值就加1低于阈值就清零。设置成5次连续检测是防误触的关键传感器在火焰旁有红外闪烁单次读数波动很大。extinguish_timeout是个定时器灭火开始后计数超过3000ms认为火已经扑灭自动退出。这个超时值需要根据现场火焰大小调整蜡烛火一般1到2秒就能扑灭留3秒余量是为了防止复燃。5. 联调中的5个典型翻车现场现象、原因与修复5.1 电机只转一边PWM通道配置遗漏现象上电后小车左电机正常转动右电机纹丝不动用手推一下右轮又能转了。原因右电机的PWM通道对应的TIM3_CH2没有配置复用功能。很多人初始化了GPIO但忘了配置GPIO_PinAFConfig导致引脚是普通的推挽输出而不是定时器复用输出。用手推一下能动是因为驱动芯片收到了方向信号只是没有PWM调速信号。解决检查GPIO_InitTypeDef里的GPIO_Mode是不是GPIO_Mode_AF_PP复用推挽输出并且确认定时器时钟有没有使能。调试时先用示波器或者万用表量一下PA7引脚有没有1kHz左右的方波如果没有多半是复用配置不对。5.2 超声波测距值跳动巨大电源噪声现象小车静止时超声波读数稳定在30cm电机一启动读数变成10cm、80cm乱跳完全不可信。原因电机转动时碳刷产生火花在电源线上耦合出高频噪声直接串进HC-SR04的供电或Echo引脚。HC-SR04模块的内部芯片对电源纹波比较敏感特别是给模块供电的那路5V如果和电机电源共用噪声会直接干扰回波检测电路。解决首先确认供电方案是分离的如果已经把电机和逻辑电源分开就在HC-SR04的电源引脚旁边加一个100uF电解电容加一个100nF陶瓷电容滤掉低频和高频噪声。还有一个软方法主控对测距结果做一个中位滤波连续测3次取中间值能滤掉绝大多数偶发跳变。另外给Echo引脚对地并联一个1k电阻可以增强抗干扰但这个不是必须的。5.3 循迹小车出赛道检测周期太长现象小车在直线段跑得很好一进弯道就直接冲出黑色胶带完全没看到任何转向动作。原因主循环的执行周期太长传感器状态更新不及时。超声波测距的阻塞式延时如果和循迹逻辑串行一次完整循环可能要50ms以上也就是1秒只做20次循迹判断对于高速运行的小车来说这个频率远远不够。解决把超声波测距改到中断或者定时器里做主循环只负责读最近一次的距离结果。同时控制循迹状态机的调用频率在10ms级别。另外检查是不是用了HAL_Delay()或者systick延时阻塞了主循环这些延时在循迹时要尽量避免。还有一点红外探头的响应时间本身有几百微秒如果调用频率太高也没意义10ms到20ms是最甜的区间。5.4 火焰误触发晴天窗边直接报警现象在阳光充足的窗边做实验小车一上电就进入灭火模式风机一直转蜂鸣器响个不停。原因太阳光里的红外成分很强火焰传感器的红外接收二极管在不做滤光的情况下会把阳光识别成火焰。此时传感器视野朝天或者朝着窗帘方向误检不可避免。解决第一个办法是改变传感器安装角度让它面向地面只检测前方和下方45度范围的火焰。第二个办法是程序中加连续采样确认逻辑阳光是稳定的强红外火焰是有闪烁特性的可以通过判断ADC读数的波动范围来区分——火焰的ADC读数通常在短时间内有明显波动阳光的读数相对稳定。第三个办法是在硬件上给传感器加一个红外窄带滤光片但这需要额外定制一般实验室不好实现。最有效的还是软件逻辑要求5次采样中至少有3次超过阈值才触发。5.5 遥控指令丢失串口中断被更高优先级阻塞现象遥控手柄按半天没反应偶尔动一下串口助手上能看到乱码。原因串口中断的优先级配置不当。如果某一个外设中断比如TIM2的输入捕获中断优先级高于串口中断且频繁触发串口接收的中断响应会被不断打断收到的数据就出现了丢字节或者错位。特别是超声波测距的捕获中断触发频率很高每个回波都要触发两次。解决把串口中断优先级调低USART1的抢占优先级设为1子优先级设为0把超声波捕获中断设为抢占优先级2。同时确保串口接收缓冲区足够大如果接发的频率很高可以把缓冲区做成一个环形队列而不是简单的一个字节接收一个字节处理。如果乱码严重检查波特率是否有偏差HC-05蓝牙模块如果从9600改成115200芯片内部时钟误差会变大可能造成连续丢字节。6. 让小车跑得更稳调试顺序、优先级参数与验证清单这个项目最后能不能落地取决于你在调试时是否有清晰的先后顺序。我习惯的顺序是先电源后传感器再单模块动作最后联调状态切换。电源不上电先不接传感器用万用表量每一级电压是否正常然后单独调循迹让小车的速度降到最低确认三个探头在赛道上的输出状态和预期一致再单独调避障用手在车前方晃动看小车是否能在20cm处停车最后才把遥控和灭火接进来。几个关键参数我给出一个可复现的初始值方便你照着跑起来再微调基础速度LEFT_SPEED450RIGHT_SPEED450转向修正量TURN_DELTA120超声波避障阈值初始设定为30cm脱困距离40cm火焰触发阈值根据环境光线标定建议找一根蜡烛在20cm处读取ADC值取该值的70%作为触发阈值灭火超时设定为3秒。控制周期统一用10ms用TIM4做时基中断主循环所有模块的调度都响应该中断的Tick。还有一个容易忽略的验证方法找一个空旷地面用遥控模式让小车分别执行前进、后退、左转、右转各1秒观察两个电机转速差是否一致。如果左转时小车明显往右偏说明两个电机的机械特性不一致可能存在齿轮箱摩擦差异。这个差异在循迹模式下会被定向放大表现为小车不断往一侧修正。解决办法是在代码里加一个速度补偿系数给慢速那一侧额外增加5%的PWM。调参时有一个玄学规律但确实靠谱如果小车在弯道出现抖动优先减小TURN_DELTA而不是减小基础速度。基础速度降低了小车虽然稳定但过弯时间太长火焰检测和避障的反应窗口也会变长。我一般保持基础速度尽量高通过调节修正量的梯度来兼顾直线稳定性和弯道通过性。最后建议你把串口调试输出留到最后再关特别是遥控指令解析的状态机一直把解析日志打到串口上联调时会发现大量“你以为发出去但实际没发出去”的指令这些直接改代码比瞎猜快得多。这个项目做完之后我最大的感受是功能越多越要控制每个模块对共享资源的占用。一次只让一个“决策者”控制电机比让所有策略同时竞争PWM输出要稳定得多。希望这篇笔记能帮你少走几段弯路。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑