
做这个小车项目的人不少但大多数教程要么只给单一传感器的示例要么代码贴出来写了等于没写。这个基于STM32F103的项目把红外循迹、红外避障和超声波测距揉在一起做成了一套既能沿着黑线跑、又能自动躲障碍的小车我前后改了三版才跑得像个样子这里把方案选型、接线、代码实现和踩过的坑一次性整理清楚。不管你是拿它当课程设计还是想入门嵌入式或者准备参加小车类比赛这篇都能帮你少走不少弯路。1. 项目概览与方案选型1.1 为什么选STM32F103C8T6三个理由和一个对比先解决一个问题为什么不用Arduino、不用树莓派偏偏选STM32F103C8T6这块“老古董”F103C8T6是Cortex-M3内核主频最高72MHzFlash 64KBRAM 20KB。跑一个超声波的测量时序加上两路红外信号处理和差速转向逻辑资源绰绰有余价格却只要几块钱。对于学生党和个人项目来说这是典型的够用就好。三方面考量比较实在第一资料齐全。F103是ST出货量最大的系列之一寄存器手册、HAL库例程、各种开发板的原理图到处都是遇到问题搜索一下基本都有答案第二学习曲线平滑。从GPIO操作到定时器PWM再从外部中断到串口调试整个项目涉及的知识点刚好覆盖嵌入式入门需要掌握的核心模块做完这个小车再去做其他单片机项目心里就有底了。第三5V容忍的IO比较多。F103系列绝大部分普通GPIO都标了FT5V容忍意味着可以直接接5V逻辑的传感器模块不需要额外买电平转换芯片省事也省钱。有人会问树莓派或者ESP32不是更强吗树莓派跑Linux确实能干更复杂的视觉导航但电源需求高、启动慢、成本翻好几倍ESP32主频更高还有WiFi但做这种硬实时的传感器轮询F103的裸机中断响应反而是优势。做小车实时性比算力更宝贵。对于传感器融合这个需求F103的性能刚好卡在“紧张但够用”的甜点上。1.2 红外与超声波怎么分工才不算重复造轮子很多人做避障小车只装一个超声波跑起来会发现一个很尴尬的问题超声波模块的探测盲区通常是2到3厘米等到车头距离障碍物只有两厘米读回来一个乱跳的数转向逻辑还没来得及执行就已经撞上了。另一个问题是超声波测距一次要来回十几个毫秒连续测三次再做滤波响应速度会明显变慢。红外传感器正好补齐这两个短板。红外收发对管是近距离检测的能手车身两侧装两个红外避障模块专门负责20厘米以内的近距离感知车底再装两个红外循迹模块用于黑线检测。这就是整个项目的融合思路超声波管远红外管近各自在最擅长的范围内工作再通过决策逻辑把信息汇总而不是简单粗暴地把几个传感器的数据加在一起。逻辑上分成两个模式来跑。循迹模式下车底的TCRT5000是主输入负责跟着黑线走同时超声波实时检测前方发现障碍临时避让绕过去之后继续寻线纯避障模式下超声波做前向测距主决策左右红外做近距离判断组成分级避让策略。这种“远中近三层感知双模式复用”的结构比单独用任何一类传感器都可靠。1.3 整车架构划分整个系统可以拆成五个部分感知层、控制层、执行层、供电层和调试层。感知层包括一个超声波模块和四个红外模块控制层是STM32F103C8T6最小系统板执行层是两路电机驱动加两个直流减速电机供电层用两节18650锂电池串联经过降压模块输出稳定电压调试层用OLED屏幕实时显示距离和模式状态同时通过串口把内部变量打出来。讲道理这五个部分单独做都不难真正考验耐心的是它们组合在一起时出现的各种工程问题——供电干扰、电平不匹配、传感器安装角度、机械结构的重心偏移每一个坑都能让车跑不起来。下面分章节把这些细节展开说。2. 传感器选型与硬件接线细节2.1 TCRT5000循迹模块原理、接线与电平确认TCRT5000是光电反射式传感器模块上有一颗发射管、一颗接收管外加一个LM393比较器输出数字电平。发射管持续发射红外光红外光打到地面上会反射回来一部分接收管收到的反射光强度不同输出端电平就不同。黑色表面会吸收红外光反射少白色表面反射多所以它天生就能区分黑线和白底。接线非常简单VCC接3.3V或5V都可以GND接地DO接单片机GPIO。AO口是模拟输出在有特殊需要时可以用ADC读反射强度实现灰线检测或阈值自定义。顺带说一句如果你想更精准地检测不同灰度的线建议把AO引出来接ADC后面可以做比例控制而不是简单的开和关。重点不同厂家生产的TCRT5000模块对“黑线”输出的电平定义可能相反。有的黑线输出高电平有的黑线输出低电平。接完线先不要写算法用串口打印读到的GPIO电平拿一张黑纸在传感器下面划过确认一下你想要的“检测到黑线”到底对应的哪个值。这个花两分钟确认的动作能避免后面调试逻辑时怀疑人生。模块上有一个蓝色电位器用来调节比较器的参考阈值。顺时针拧到底和逆时针拧到底探测灵敏度差异非常大调节原则是把传感器放在白色地面调到输出刚好为白地电平再放到黑线上确认输出翻转反复几次找到一个临界点。2.2 HC-SR04测距时序与电平注意事项HC-SR04工作原理很简单给TRIG引脚一个10微秒以上的高电平脉冲模块内部的电路会发出8个40kHz的超声波脉冲同时ECHO引脚输出一个高电平高电平持续的时间就是超声波从发射到接收的往返时间。距离等于时间乘以声速再除以2。声速在20摄氏度空气里大约每秒340米换算一下就是0.034厘米每微秒而超声波是往返的所以时间要除以2最终系数是0.017。也就是距离厘米 回波高电平时间微秒× 0.017。这个换算关系我刚开始总记错特别提醒各位0.034是单程系数0.017才是往返之后的结果。HC-SR04用5V供电回波ECHO输出的高电平最高是5V。F103的GPIO标注为5V容忍所以直接接到单片机的PB口或者PA口一般没问题。但我建议宁可多一分小心在ECHO引脚和单片机引脚之间串联一个1k电阻做限流完全不影响信号读取就算模块电平特性有差异也不会烧引脚。安装位置对测量结果影响很大。模块有两个像眼睛一样的探头发射探头和接收探头要在同一水平线上正对前方。不要贴地安装超声波打到地面会乱反射推荐安装在车头离地5到10厘米的位置稍微向上仰一点角度大约10度到15度这样既能测到前方的障碍物又不会频繁被地面触发。2.3 电机驱动与引脚分配电机驱动有三种常见选择L298N模块、TB6612模块和DRV8833模块我实际对比过三者的表现。L298N是老牌方案功率大能驱动大电流电机但压降大内部的三极管导通损耗让5V稳压输出不稳定供电久了芯片烫手TB6612是东芝的MOS管驱动压降低、体积小我后期主力用它DRV8833和TB6612类似双路驱动体积更小适合超迷你小车。对学生项目来说TB6612是性价比和稳定性最平衡的选择如果你手里只有L298N也不是不能用注意别把单片机的5V从L298N上取电机瞬间抽流会导致单片机复位。电机控制需要两路PWM调速和四个方向GPIO控制正反转。方向引脚接H桥的IN1/IN2逻辑是IN1高IN2低为正转IN1低IN2高为反转IN1和IN2同高同低都算刹车。调速用PWM接STBY或者PWMA/PWMB注意TB6612有一个STBY引脚必须拉高芯片才工作有些人接完线车不动查半天发现STBY悬空。我使用的引脚分配表如下供参考模块信号单片机引脚说明超声波TRIGPA0推挽输出10us高电平触发超声波ECHOPA1输入测量高电平时间左电机PWMPA8TIM1_CH110kHz左电机DIR1/DIR2PB0/PB1高低电平控制方向右电机PWMPA9TIM1_CH210kHz右电机DIR1/DIR2PB2/PB10高低电平控制方向红外循迹LDOPB3注意默认是JTAG引脚红外循迹RDOPB4注意默认是JTAG引脚红外避障LDOPB5数字电平红外避障RDOPB6数字电平模式按键KEYPC14低电平有效串口TX/RXPA9/PA10调试基线若与PWM冲突可改用PB6/PB7的USART1重映射一是PB3和PB4默认被JTAG占用要用它们做普通IO得先关闭JTAG复用功能这个细节在下一章展开二是OLED如果加的话用PB6/PB7挂I2C1或者用软件I2C随便挑两个IO都行。供电部分是整个项目最容易出问题的地方。两个18650锂电池串联标称7.4V经过一个低压差降压芯片转成5V给单片机和传感器供电。电机的电源直接接电池正极但地线与单片机系统共地否则逻辑电平没有参考点。共地这事怎么强调都不为过模块地和单片机地必须是同一个地否则传感器读回来的电平全是乱的。3. 核心代码实现从读传感器到自主决策3.1 初始化必须处理的两个坑JTAG复用与微秒延时初始化阶段有两件事要处理干净不然后面全部白搭。第一件事是PB3/PB4的复用问题。STM32F103上电后PB3、PB4和PA15默认功能是JTAG引脚如果直接当普通IO用读回来的电平始终不对。解决办法是在GPIO初始化之前把JTAG的调试口关掉保留SWD下载功能代码如下__HAL_AFIO_REMAP_SWJ_NOJTAG();这行代码执行后PB3和PB4才能被当作普通IO使用同时PA13/PA14两个SWD引脚依然保留可以继续用调试器下载程序。不写这行的话你在PB3上焊个传感器读到的数据永远像在跳大神。第二件事是微秒级延时。超声波的TRIG引脚要求10微秒的高电平脉冲回波时间测量精度直接影响距离精度。用HAL_Delay这种毫秒级延时远远不够必须用微秒级延时。最可靠的办法是用Cortex-M3内核自带的DWT循环计数器不需要额外占用定时器void DWT_Init(void) { CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; } void delay_us(uint32_t us) { DWT-CYCCNT 0; while (DWT-CYCCNT us * 72); // 系统时钟72MHz1us 72个周期 }第一次用DWT的时候我确认过CYCCNT是32位计数器溢出周期大约59.6秒做单次微秒延时完全不用担心溢出问题。如果你不想用DWT用SysTick做微秒延时也不是不行但代码会比这个啰嗦一倍。GPIO初始化用标准的HAL库函数方向引脚配成推挽输出循迹和避障引脚配成上拉输入PWM输出引脚在MX配置里设置好复用功能。系统时钟通过SystemClock_Config配成72MHz这行配置可以直接用CubeMX生成建议别手写时钟树容易配错导致外设频率不对。3.2 超声波测距代码加上滤波才能真正用一次完整的超声波测距流程可以拆成四个步骤TL;DR就是拉低TRIG、拉高10us、拉低、等ECHO高电平计时。写成函数如下float Get_Ultrasonic_CM(void) { // 1. 触发脉冲 HAL_GPIO_WritePin(TRIG_PORT, TRIG_PIN, GPIO_PIN_RESET); delay_us(5); HAL_GPIO_WritePin(TRIG_PORT, TRIG_PIN, GPIO_PIN_SET); delay_us(12); HAL_GPIO_WritePin(TRIG_PORT, TRIG_PIN, GPIO_PIN_RESET); // 2. 等待ECHO上升沿超时保护 uint32_t timeout 0; while (HAL_GPIO_ReadPin(ECHO_PORT, ECHO_PIN) GPIO_PIN_RESET) { if (timeout 30000) return -1; } // 3. 测量高电平持续时间 uint32_t start DWT-CYCCNT / 72; // 转为微秒 timeout 0; while (HAL_GPIO_ReadPin(ECHO_PORT, ECHO_PIN) GPIO_PIN_SET) { if (timeout 60000) { start 0; break; } } uint32_t dt DWT-CYCCNT / 72 - start; // 4. 超时判断与距离换算 if (dt 60000) return -1; float dist_cm dt * 0.017f; return dist_cm; }这里有两个关键工程细节。第一等待ECHO上升沿的过程必须加超时保护。当超声波模块前面没有障碍物或者反射面太远超出量程上限ECHO会一直保持低电平如果没有超时保护程序会卡死在while循环里小车当场死机。第二回声高电平时间上限是60毫秒对应距离大约是1020厘米但HC-SR04实际量程只有400厘米左右超过量程的读数直接丢弃后续逻辑不要使用。实际跑起来还会发现一个问题单次测量出来的距离会在原地抖动同一个静态障碍物连续测五次可能一次16厘米一次19厘米这种抖动会让转向逻辑来回犹豫小车蛇形前进。解决方法是加一个三样本中位值滤波取三次连续测量的中间值作为有效距离float Read_Distance_Filtered(void) { float a Get_Ultrasonic_CM(); delay_us(80); float b Get_Ultrasonic_CM(); delay_us(80); float c Get_Ultrasonic_CM(); if (a 0) a 999; if (b 0) b 999; if (c 0) c 999; // 简单中位三个数排序取中间 if (a b b c) return b; if (a c c b) return c; if (b a a c) return a; if (b c c a) return c; if (c a a b) return a; return b; }注意三次测量之间要有短暂间隔连续快速触发会让模块内部的电容充放电来不及恢复导致测量值系统性偏大。间隔80微秒到1毫秒都行实测差别不大。3.3 循迹与避障的决策逻辑先画流程再写代码写决策之前先把逻辑在纸上画清楚不然代码写出来一堆if套娃后面改一个阈值都不知道影响哪个分支。循迹模式的核心判断是四个组合对应四种情况左黑右白、左白右黑、双黑、双白。双黑说明两个传感器都压线也就是车和线的偏差很小直行单边压线说明车偏了差速转弯修回来双白是最头疼的丢线状态。循迹控制代码如下void Follow_Line(uint8_t L, uint8_t R, float dist) { if (dist 0 dist 20) { // 循迹中前方突然出现障碍临时左转避让 Set_Speed(80, 200); return; } if (L BLACK R BLACK) { Set_Speed(150, 150); // 正常直线行驶 } else if (L BLACK R WHITE) { Set_Speed(80, 180); // 线偏左向左修正右轮快 } else if (L WHITE R BLACK) { Set_Speed(180, 80); // 线偏右向右修正左轮快 } else { Set_Speed(120, -120); // 双白丢线原地旋转找线 } }再说一次方向逻辑Set_Speed(左轮速度右轮速度)速度范围 -255到255正数前进负数后退。左轮慢右轮快车的转弯方向是向左所以左传感器压线线在左边用左慢右快的差速修正这里的方向很容易搞反我当时就是反了一次之后把所有注释都标好才记住。避障模式采用三级决策核心是优先级判断前方安全距离大于30厘米直线行驶前方20到30厘米减速警戒前方小于20厘米结合左右红外判断往哪边转。判断原则是选择有空间的一侧转弯两边都有障碍就原地掉头void Avoid_Obstacle(float dist, uint8_t obsL, uint8_t obsR) { if (dist 0) { Set_Speed(0, 0); return; } if (dist 15) { if (obsL 0 obsR 1) { Set_Speed(120, 220); // 右侧堵左侧有空左转 } else if (obsL 1 obsR 0) { Set_Speed(220, 120); // 左侧堵右侧有空右转 } else if (obsL 0 obsR 0) { Set_Speed(150, 200); // 两边都空优先左转绕行 } else { Set_Speed(200, -200); // 两侧都堵原地掉头 } } else if (dist 30) { Set_Speed(100, 100); // 警戒距离减速直行 } else { if (obsL 0 obsR 1) { Set_Speed(180, 200); // 左侧障碍轻微右偏 } else if (obsL 1 obsR 0) { Set_Speed(200, 180); // 右侧障碍轻微左偏 } else { Set_Speed(200, 200); // 安全全速直行 } } }这里obsL和obsR指的是左右红外避障模块的检测结果检测到障碍物定义为1没检测到定义为0。具体电平取决于你的模块有的模块障碍物输出低电平串口一测就知道了。注意15厘米这个阈值和车身宽度相关如果小车比较宽要提前到20厘米不然转向过程中车身侧边会蹭到障碍物。主循环里所有逻辑每50毫秒执行一次这个时间比超声波单次测量周期大约20到30毫秒稍长既保证响应及时又不会因为频繁切换方向让电机抖动。主循环代码结构如下while (1) { float dist Read_Distance_Filtered(); uint8_t L Read_Line_Left(); uint8_t R Read_Line_Right(); uint8_t obsL Read_IR_Left(); uint8_t obsR Read_IR_Right(); if (mode LINE_MODE) { Follow_Line(L, R, dist); } else { Avoid_Obstacle(dist, obsL, obsR); } // OLED刷新 串口调试数据 HAL_Delay(50); }3.4 模式切换和OLED调试让状态看得见两种模式之间通过一个按键切换按键接PC14配置为外部中断按下时翻转模式变量。如果不用外部中断在主循环里做一个按键消抖轮询也可以就是响应会慢一点点。代码如下void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin KEY_PIN) { delay_us(20000); // 简单消抖实际建议改用定时器计数消抖 while (HAL_GPIO_ReadPin(KEY_PORT, KEY_PIN) GPIO_PIN_RESET); mode !mode; OLED_Show_String(0, 0, mode ? Avoid Mode : Line Mode); } }调试信息输出我强烈建议加一块OLED屏幕。OLED0.96寸屏幕用软件模拟I2C驱动只需要两根IO线实时显示当前距离和传感器状态。调传感器位置、调电位器阈值的时候不用每次插线连电脑看串口低头看屏幕就够。串口还是要保留作为第二通道打印详细变量比如三个红外逐次读到的电平变化方便分析传感器是不是时好时坏。我的调试界面简单到不能再简单第一行显示模式第二行显示距离值和滤波前后的对比第三行显示四个红外电平第四行显示左右电机的PWM占空比。看到数据再调参数效率比改一行编译一次高不知道多少。4. 实测调参与常见问题排查4.1 红外阈值标定用串口打印代替瞎猜电位器调红外传感器的阈值电位器是最容易烦的环节。一开始我都是凭感觉拧拧几圈之后发现黑线和白地识别混乱要么全黑要么全白。后来学乖了先把传感器固定好接好线在串口里打印数字输出然后拿一块黑色电工胶带贴在地面传感器在黑白交界处来回移动眼睛盯着串口输出手动调电位器直到边界翻转干净利落。有一个好用的判断标准白色地面输出一个稳定电平黑色胶带输出另一个稳定电平两者边界切换时没有抖动和毛刺。如果边界处电平跳来跳去出现几十毫秒的抖动说明阈值太接近边界值了。往前调一点让白地更稳定别让黑线稍微脏一点就触发误判。循迹传感器安装间距也有讲究。两个传感器之间的距离要略小于黑线的宽度安装高度保持1到2厘米太远反射光衰减厉害太近容易被地面划伤。实际装车之后建议跑几次直线观察小车是不是偏航如果偏航就微调左右两个传感器的安装位置把机械偏差消除掉。4.2 常见故障速查表复现最多的问题汇总我把这个项目调试过程中遇到的问题整理成一个表格后面做类似项目的朋友可以对照排查故障现象可能原因排查方法超声波距离一直显示0或-1TRIG引脚没接对ECHO电平超出容忍范围模块损坏先用万用表测TRIG有没有10us脉冲再测ECHO有没有高电平超声波距离值跳变超过5cm供电不稳模块前方有遮挡物干扰滤波没加加中值滤波检查电池电压把模块和电机电源分离小车直线跑偏两个电机转速不一致轮胎规格偏差用手悬空测试两个轮空转速度程序里给慢的一侧补偿循迹时小车蛇形走差速幅度太大主循环周期太短降低差速比例适当增加HAL_Delay延时循迹丢线找不回双白丢线后旋转速度不对线宽与传感器间距不匹配调双白状态的旋转速度检查传感器间距按下避障键小车没反应模式变量没切换OLED显示更新失败串口打印mode值先排除程序问题电机嗡嗡响但不转PWM频率太低STBY没拉高占空比太小PWM频率提到10kHz以上检查STBY引脚电池电量充足但单片机复位电机启动瞬间拉低电源轨共地不良电机电源独立降压模块输出并联大电容加TVS红外传感器白天地面误判环境红外光干扰阈值太灵敏调低阈值给传感器加遮光罩换用带调制频率的模块第四个和第五个问题在循迹小车里出现频率最高。蛇形走和丢线基本都是差速参数和安装间距不对。我的经验是先调机械再调软件最后调参数。顺序错了会越调越乱。4.3 下一步优化从开关量到连续量的融合这个项目做完之后如果想继续进阶有几个明确的方向。第一个方向是循迹从开关量升级到连续量。TCRT5000的AO口输出模拟电压通过ADC读回可以得到传感器距离黑线的偏移量然后把这个偏移量作为比例项输入PWM控制转向就会平滑很多不再是一档一档的开关切换。第二个方向是给超声波加舵机云台。用一个SG90舵机带动超声波模块左右转动测量前方三个角度的距离分别是左45度、正前方、右45度。避障决策从简单的左右二选一变成三方向最优选择小车能绕开一些更复杂的障碍布局。第三个方向是引入闭环速度控制。在电机后面加装霍尔编码器读取实际转速通过PID控制器让两个轮子的速度精确匹配。循迹和避障的性能都会上一个台阶但这个改动会明显增加代码复杂度建议先把开环逻辑吃透再上闭环。第四个方向是状态机重构。把循迹中、避障中、脱困中、停止作为四个显式状态通过事件触发状态迁移而不是在函数里层层嵌套if。状态机的优点是每个状态内逻辑清晰出问题好定位。适合在代码规模超过200行之后做。从传感器融合的角度讲还可以尝试加权融合。比如近距离数据以红外为主超声波为辅远距离反过来。两种传感器测同一方向时按可信度加权平均。对于F103来说这点计算量完全不是问题。最后再分享一个习惯这个项目真正做到最后我最深的体会是传感器不是拿来就能用的每个模块都有它的性格。HC-SR04怕盲区怕干扰TCRT5000怕环境光怕安装位置偏电机怕供电波动每个坑都是单独跑才能发现。做硬件项目最忌讳的就是直接上完整逻辑然后发现跑不起来不知道从哪查起。我后来的做法是先让每个传感器单独工作串口打印原始值确认数据和手册对上了再开始写融合逻辑。这个过程多花了一个晚上但之后几乎没遇到找不出原因的毛病。做智能小车这个项目最有价值的不是小车最后跑得有多顺而是把“数据采集-数据处理-执行控制”这条链路的每一步都在真实物理环境里验证过一遍。这套思路放到任何单片机项目里都通用。