1. 第三期开工循迹、蓝牙和控制逻辑的合体前两期把车体搭起来、电机转起来之后很多同学会卡在一个尴尬的节点循迹能跑蓝牙能连但两套功能各自为政一合体就各种毛病——传感器误判、蓝牙指令丢失、电机响应迟钝、车跑起来画龙。这一期就是把它们真正捏到一块儿解决能跑到跑得稳、控得住这个阶段的问题。我会把我在这个项目上踩过的坑、试过的方案、最后稳定下来的配置全部摊开讲。内容主要覆盖三块TCRT5000循迹传感器的硬件接法与校准、HC-05蓝牙模块与Android端通信协议的设计、以及循迹算法和双模式切换的落地实现。项目主控还是老朋友STM32F103C8T6驱动用L298N传感器我用了5路的TCRT5000排成一排蓝牙模块是HC-05。这套组合在课程设计和DIY圈子里非常常见所以这篇的经验几乎可以无缝移植到你的车上。2. 循迹传感器接线容易校准才是真功夫2.1 TCRT5000的选型与布局思路TCRT5000是反射式红外传感器原理很简单红外发射管发出光线接收管根据反射光强度输出模拟电压再通过模块上的LM393比较器转成数字电平。黑线吸光、白色赛道反光强所以传感器在黑线上方输出高电平在白色区域输出低电平这就是最基础的判黑依据。我用的5路传感器排布左右间距大约1.5cm整体宽度控制在赛道黑线宽度附近。这样做的目的是让车在赛道中央时中间一路传感器正好压线两侧传感器悬在白色区域外一旦车体偏移对应侧的传感器就会先压到黑线给控制逻辑足够的提前量。2.2 接线方式与供电注意事项5路TCRT5000的接线很统一每路都是VCC、GND、DO三个引脚。注意这里有个劝退无数新手的细节传感器的VCC不能直接接STM32的3.3V排针除非你确认模块上的比较器芯片兼容3.3V电平。很多TCRT5000模块用的是LM393它支持宽电压但为了稳妥我直接给传感器统一供5V输出脚DO接到单片机的GPIO上。STM32F103的GPIO大部分是5V容忍的所以DO直接接PA0~PA4没有问题。但如果你用的是某些3.3V-only的MCU就必须加电平转换或者用分压电阻这一点在选型的时候要提前想清楚不然后期烧引脚特别麻烦。实际接线表自己对照着检查组件引脚接STM32引脚备注TCRT5000 左1VCC/DO5V / PA0最外侧左TCRT5000 左2VCC/DO5V / PA1左内TCRT5000 中VCC/DO5V / PA2中间基准TCRT5000 右2VCC/DO5V / PA3右内TCRT5000 右1VCC/DO5V / PA4最外侧右HC-05TX/RXPA9/PA10USART1L298N IN1~IN4INPB0~PB3PWM通道L298N ENA/ENBENPA6/PA7定时器PWM2.3 校准的完整流程与判断逻辑校准是整个循迹系统里最容易被低估的环节。很多人第一次把传感器焊好上电直接跑发现车完全不听使唤第一反应是算法写得有问题其实多半是传感器阈值根本没设对。我用的校准流程是这样的先把车放在白色赛道上用串口把五路传感器的DO电平值读出来同时用万用表测模块上电位器分压处的电压。然后旋转电位器直到白色区域下DO输出确定电平翻转。判断标准就是模块上的指示灯在白色区域亮、在黑线上灭。每一路都要单独调因为每一路红外管的衰减程度会有细微差别直接套用同一个电位器位置会导致个别传感器灵敏度不足。更细致的做法是用ADC模式读取传感器的AO引脚在白色区域和黑色线上各采样50次取平均值作为该路的动态阈值。这样即使环境光变化或者赛道反光率不同阈值的设置也比固定电平要可靠得多。我在后期把5路全改成了ADC采样加动态阈值误判率明显下降尤其在强光环境下改善很大。提示不要在直射阳光下校准传感器红外接收管对阳光中的红外成分非常敏感日光直射会让白色和黑色的电平差大幅缩小校出来的阈值在室内灯光下反而不准。3. 蓝牙控制的核心UART指令协议与Android端对接3.1 HC-05的配置与连接细节HC-05默认是AT模式还是通信模式取决于按键是否按住再上电。这个模块有个很坑的地方默认波特率是9600但有些卖家发的固件版本默认38400所以在PC上用USB转TTL调试的时候如果串口助手收到的都是乱码分别试一下这两个波特率基本能解决。我习惯把HC-05接到USART1也就是PA9和PA10理由很简单默认的串口调试信息也在USART1这样调试阶段用一根USB转TTL接电脑跑起来直接切换成蓝牙代码不用大改。注意HC-05是3.3V逻辑电平虽然很多模块自带稳压和电平转换但如果你用的是裸模块TX引脚输出3.3V直接接STM32没问题而HC-05的RX接STM32的TX两边电平也要匹配供电用5V时注意看模块规格。其次配对密码默认是1234名称默认是HC-05。这些都应该在AT模式下改掉不然多组同学在同一实验室调试时互相连错模块是常有的事。用AT指令改名称和密码只需要几条命令ATNAMECar_BT ATPSWD8888 ATUART9600,0,0 ATROLE0配置完成后重新上电模块进入通信模式。这个时候STM32侧的串口配置必须和模块一致我用的配置是9600波特率、8位数据、无校验、1位停止位。3.2 通信协议的设计别用裸字符传输很多人在蓝牙控制这里犯的第一个错误是直接通过串口发送字符比如发1表示前进、2表示后退。这样在简单demo里没问题但一旦涉及多字节数据、方向速度和模式切换的组合指令裸字符传输很容易出乱子。我的做法是设计一个固定长度的指令帧字节内容说明00xAA帧头10x55帧头校验2指令类型0x01速度控制 / 0x02模式切换3数据段速度值0-100或模式编号4校验和前4字节求和取低8位Android端每次发送5个字节STM32在串口接收中断里做状态机解析。这样做的核心优势是对齐不容易错位即使某一字节丢了下一帧的帧头还能重新同步。3.3 Android端开发要点Android端我用的是Java写的原生App核心就三件事蓝牙权限申请、蓝牙Socket连接、数据收发线程。蓝牙权限这块要注意Android 6.0以上需要动态申请权限Android 12以上还需要BLUETOOTH_CONNECT权限而且这几个权限不是同一个漏一个就会在连接的时候直接崩掉。我当时的处理是在Activity里一次性申请所有相关权限并且处理了用户拒绝的情况。连接SPP设备用的UUID是固定的写成private static final UUID SPP_UUID UUID.fromString(00001101-0000-1000-8000-00805F9B34FB);拿到BluetoothDevice之后调用createRfcommSocketToServiceRecord(SPP_UUID)创建socket然后connect()。注意connect()必须放在子线程否则会直接ANR。数据发送用socket.getOutputStream().write(frame)发送的帧就是上面协议里定义的5个字节。接收数据不是必须的但是强烈建议把串口返回的数据读出来。我在STM32端每收到一帧有效指令就回发一个确认字节0x6FAndroid端用一个独立的读线程解析返回数据这样能在界面上显示当前连接是否正常、指令是否被正确解析。这个小设计帮我解决了一大堆为什么车不动的排查问题因为你能立刻区分是蓝牙没连上、指令没发出、还是单片机没执行。界面方面我用的是一个简单的方向盘布局加两个模式切换按钮方向键发速度控制指令模式按钮发模式切换指令。需要注意的是按键的按下和松开要分别发送指令按下发前进80松开发前进0这样车才能有松手即停的响应而不是像玩具车那样一直冲出去。4. 循迹算法从逻辑判断到PID4.1 三段式方案的对比与选择循迹算法网上能搜到一大堆但归纳起来就三类。第一类是最土的五路if-else逻辑判断根据哪一路传感器压线来决定转向。第二类是加权偏差计算给每一路传感器赋予一个权重系数算出当前车体相对于黑线的偏移量再用这个偏移量来决定左右轮速差。第三类是PID闭环在加权偏差的基础上用比例、积分、微分对转向量做平滑控制。我的建议是直接从第二类开始写第一类只用来验证硬件是否正常。因为if-else的逻辑会让转向非常突兀车在直道上也会左右摇摆完全体现不出循迹的平滑感。而第三类是在第二类基础上加PID壳子只要第二类的偏差算得对PID的调参就是顺理成章的事。4.2 加权偏差的具体实现五路传感器我给它们的权重是这样定的从最左到最右分别是-4、-2、0、2、4。每次读取五路DO值把压到黑线的传感器权重加起来就是当前偏差error。比如中间一路压到黑线error0最左一路压到黑线error-4最左和左内同时压到error-6。这个偏差值代表了车体中心相对于黑线的偏移方向和大小。得到error之后基础的控制逻辑是int base_speed 60; int turn error * 8; // 比例系数 motor_left base_speed turn; motor_right base_speed - turn;这个比例系数8不是随便拍的是我根据车的轴距、轮径和赛道转弯半径估算出来的。我的车底盘轴距大约18cm轮径6.5cm直角弯道的内半径大约10cm要让车在弯道里不至于冲出赛道左右轮差至少要达到速度值的20%到30%。先取速度60那么turn的基本范围就是12到18对应error范围1.5到2.25再乘上一个裕量因子最后定在8左右。数字这东西在不同车上是完全不同的哪怕同型号的车架电池电压不同、轮胎磨损不同效果都会变。我给的参数只是一个起点实际调的时候应该在直道上观察车的摆头幅度turn大了车会画龙小了车过弯会冲出去。注意电机速度不是线性对应PWM占空比的尤其是L298N在低占空比下存在死区PWM值低于30左右电机根本不动。所以在base_speed60的情况下最大转向量最多给到30也就是左右轮速差达到60和90的差距才能弯道有效果。4.3 PID调参过程与参数记录在基础的P控制之上我加了I和D。I项用来消除稳态偏差比如车长时间贴着一侧跑而不是居中D项用来抑制超调让转向响应更平滑。但我调参的时候发现I项参数稍大就会在弯道里积分过头导致出弯后车还在继续转向。所以我在循迹模式下I项的积分只在error绝对值小于2的时候才累计防止弯道里错误积分。实际的PID代码框架是这样的float kp 1.2f, ki 0.05f, kd 2.0f; float last_error 0, integral 0; int pid_control(int error) { if (abs(error) 3) integral error; else integral 0; float derivative error - last_error; last_error error; float output kp * error ki * integral kd * derivative; return (int)output; }调参顺序我先只开P把kp从0.5开始一点一点加直到车在直道上能走稳但过弯不冲线。然后加D把kd从0.5开始加观察车过弯后的回正过程是否干脆。最后加Iki从小往大调每次只加0.01直到车能稳定贴着赛道中线跑。整个过程大概花了一个多小时每调一次参数就要在赛道上跑一圈看效果不要想着一口气调好这是纯经验活。4.4 五种传感器状态的兜底处理循迹算法里最容易翻车的是传感器状态全白和全黑。全白代表车已经完全冲出赛道全黑代表五路全部压线通常是车横在黑线上或者到了起跑线。我的处理策略是全白的时候不执行PID输出而是直接让车以固定速度后退一小段然后原地左转重新搜索黑线全黑的情况则根据前一次的有效error方向继续转弯因为车横在黑线上时需要知道从哪边脱离这个方向就是上一次正常循迹时的error极性。这两类兜底逻辑尤其重要如果只写正常的PID路径车一旦冲出赛道就会一直直行然后卡死。很多人的demo车在实际跑的时候撞墙或者飞出去一部分就是没处理全白状态。5. 双模式共存循迹与蓝牙控制的切换逻辑5.1 模式管理的架构设计循迹和蓝牙遥控在同一辆车上共存不是简单地把两段代码拼在一起而是需要一套清晰的状态机。我用的是一个全局变量current_mode取值MODE_LINE_TRACKING和MODE_BLUETOOTH主循环里先判断当前模式再分发到对应的控制逻辑。这个状态机的关键在于模式切换的瞬间要做状态清理。从循迹切到蓝牙时必须把PID的积分项清零、把上一次的error清空不然切回来的时候PID会带着旧的状态继续跑出现一个突兀的转向。从蓝牙切到循迹时要重置左右电机的PWM输出因为蓝牙遥控的最后一个速度指令可能还驻留在PWM寄存器里。模式切换指令我放在蓝牙协议里Android端发0x02类型的数据帧数据段为0就是循迹模式1就是蓝牙遥控模式。同时在车身上加一个物理拨码开关作为备用切换手段一旦蓝牙断连卡死还能手动强制切回遥控模式。5.2 速度映射与方向指令的实现蓝牙遥控模式下Android端发送的方向是前进、后退、左转、右转、停止五类。我在STM32端把这五类指令映射成具体的电机输出void bt_handle_command(uint8_t *frame) { uint8_t type frame[2]; uint8_t data frame[3]; if (type 0x01) { // 速度控制 switch(data) { case 0x01: motor_set(80, 80); break; // 前进 case 0x02: motor_set(-80, -80); break; // 后退 case 0x03: motor_set(50, 90); break; // 左转 case 0x04: motor_set(90, 50); break; // 右转 case 0x00: motor_set(0, 0); break; // 停止 } } ... }这里的左转我特意没有做成原地转向而是设计成差速转向左轮50右轮90。原因很简单原地转向在静止状态下对L298N和电机的冲击很大而且车在转向过程中几乎没有前进速度在比赛或者演示场合显得特别笨拙。差速转向的好处是车一边转一边往前走轨迹更流畅也更好控制。5.3 定时器与PWM通道的规划在系统整合阶段我把定时器资源重新梳理了一遍。L298N的ENA和ENB接在PA6和PA7上对应TIM3的通道1和通道2用PWM模式控制两个电机的转速。IN1~IN4接普通GPIO控制转向。这个分配不是随便选的TIM3在STM32F103上可以直接输出两路PWM而且APB1的时钟配置比较简单不容易在初始化的时候踩坑。PWM的初始化有一个常见的大坑很多人忘记配置TIM3的ARR和PSC的关系结果PWM频率太高或者太低。我设定的PWM频率是10kHz这在电机驱动上是比较合适的值既不会产生可闻的噪声又能提供足够的分辨率。ARR设置为1999PSC设置为71在72MHz的系统时钟下就是72MHz / (72 * 2000) 1kHz不对这里要算准确。72MHz主频TIM3挂在APB1上APB1分频为2的时候定时器时钟是72MHz所以PWM频率 72MHz / (PSC1) / (ARR1)。要让频率为10kHz我取PSC71、ARR1999计算结果是72000000 / 72 / 2000 500Hz明显不对。所以实际我用的参数是PSC71、ARR9972000000 / 72 / 100 10kHz占空比通过CCR值0到99来精细调节。这个细节特别值得写出来因为教程里照搬的PSC和ARR到了不同主频的板子上需要重新推算很多人的PWM输出频率不对就是没做这一步换算。5.4 主循环的实时性优化循迹和蓝牙共存的另外一个隐形问题就是主循环的执行频率。循迹需要尽量快的控制周期而蓝牙串口接收又要及时响应。我的主循环用一个简单的标志位控制周期内部通过DWT计数器实现精确的5ms循环。每个周期里做的事情顺序是先读五路传感器的ADC值然后处理串口接收缓冲区的指令帧再根据当前模式执行控制逻辑最后更新PWM。串口接收是放在中断里的中断里只做一件事把字节丢进环形缓冲区。主循环里再解析缓冲区里的帧。这个中断放数据、主循环取数据的模式是嵌入式开发的基础素养但很多新手喜欢在中断里做协议解析我一开始也这样干过后果就是一旦指令帧密集到来中断处理时间过长主循环的执行周期被严重拉长循迹的控制频率就会断断续续。6. 实测中的典型故障与排查记录6.1 故障速查表整个调试过程中我遇到的高频问题大概有七个这里用表格直接列出来方便其他同学对照排查现象可能原因检查与解决办法蓝牙连接后车无反应串口波特率不匹配确认HC-05与STM32串口均为9600另外检查帧校验和是否正确蓝牙连上但发送指令偶尔失灵供电电压跌落导致HC-05重启用万用表测HC-05 VCC电压蓝牙发射瞬间电压低于3.3V时需加1000μF电容稳压循迹车画龙严重PID的P参数过大逐步减小kp观察直道摆头幅度过弯冲出赛道转向输出饱和过早增大转向差值同时降低base_speed传感器在校准黑线上输出不稳定环境光干扰或电位器未调准重新在暗光下校准或者改用ADC动态阈值电机抖动不转L298N使能脚未接PWM确认ENA/ENB引脚确实接到了定时器PWM通道而非普通GPIO车跑一会儿突然乱跑电池电压下降影响传感器阈值改用ADC动态阈值或增加低压报警逻辑6.2 最坑的电压问题蓝牙模块反复重启这个坑我必须重点说因为它排查了我整整一个晚上。现象是蓝牙能连上手机但每次发送速度指令车要么没反应要么整个系统重启。用万用表一测才发现HC-05的VCC在蓝牙发射数据的瞬间电压会掉到2.8V左右模块直接复位了。原因是电池经过L298N给整个系统供电电机的PWM切换瞬间电流很大导致母线电压被拉低。解决办法是在HC-05的VCC和GND之间并一个1000μF的电解电容再并一个0.1μF的陶瓷电容去高频噪声。同时电机电源和控制电路要从L298N的供电点分开走线避免控制电路和电机驱动共用同一段细导线。加了电容之后蓝牙发射瞬间的电压跌落从0.8V降到了0.1V以内这个问题彻底消失。6.3 蓝牙数据丢帧的定位过程还有一个问题Android端连上蓝牙后发送30条指令车只能执行大概25条左右平均每几条就丢一个。一开始我怀疑是无线干扰后来用串口助手在PC端模拟Android发送同样的数据帧发现STM32端接收到的帧头经常错位。排查到最后发现问题出在Android端的主线程和发送线程竞争同一个OutputStream。我在UI的onTouch事件里直接调用了socket.getOutputStream().write()但手指快速连续点击按钮时多个事件在同一个线程里排队发送期间如果有一个线程正在写一半另一个线程就开始写下一帧两个帧在串口线上交叠了。解决方式是用一个专门的发送队列所有UI事件只往队列里放数据一个独立线程负责从队列取数据并通过串口发送。这样从源头上杜绝了并发写串口的问题丢帧率从15%降到了0。6.4 传感器受环境光干扰的实战测试我的调试场地在实验室靠窗的位置下午阳光斜射进来的时候循迹表现明显变差数据上看是部分传感器持续输出压线信号。这是因为阳光里有大量红外成分直接把接收管打饱和了。我试过加遮光罩效果有一点但不能根治。真正有效的方案是把五路传感器全部改接AO模拟输出然后在代码里每秒动态刷新一次白色基准值。具体做法是默认认为赛道上白色区域占绝大多数每200ms采样一次所有路的ADC值取最低值作为当前的白色基准然后把这个基准乘以0.8作为判黑阈值。这样即使环境光变化阈值也会跟着漂移系统始终能分辨黑色和白色。uint16_t white_floor[5]; uint16_t adc[5]; void update_threshold() { for (int i 0; i 5; i) { if (adc[i] white_floor[i]) { white_floor[i] adc[i] * 0.8; // 渐近更新 } } }这个动态阈值方案实测在强光和弱光下都可以稳定跑完全程强烈推荐。7. 一套完整的参数清单与调试顺序7.1 我最终定稿的配置经过两天的反复调试我最后稳定下来的参数组在这里直接给出来供参考但一定要结合自己的车架重新验证项目参数值说明PWM频率10kHzTIM3PSC71ARR99循迹基准速度55PWM占空比55%PID参数kp1.1ki0.03kd1.8以error权重-4~4为输入最大转向输出±30限制左右轮速差不超30蓝牙波特率9600HC-05与STM32一致指令帧长度5字节帧头0xAA55类型数据校验动态阈值更新周期200ms传感器白色基准刷新主循环周期5ms200Hz控制频率这套参数在我的车上直线部分几乎不摆头直角弯能稳定通过S弯稍微减速也不会冲出赛道。如果你照抄之后发现过弯冲出优先把基准速度降到45而不是去动PID速度是所有参数里影响最大的一个。7.2 建议的调试顺序调试顺序比参数本身更重要。我的经验是严格按这个顺序来能省下一半以上的时间第一步先不做循迹只做蓝牙遥控。确认连接、通信、电机转动方向全部正确通过Android端让车前后左右动起来。第二步蓝牙遥控通过后再测传感器。把车手动放在黑线左边、中间、右边观察五路LED对应的串口输出是否正常。第三步在直道上跑纯P控制的循迹不接蓝牙只观察车的直道表现把P调稳。第四步加入D和I在直道和S弯上反复验证确定PID三参数。第五步最后才合并蓝牙模式切换逻辑在完整赛道上跑双模式测试。这个顺序的核心逻辑是逐步增加系统的复杂度每一步出现问题都能快速定位到是新引入的部分还是之前的老问题。我见过很多同学一上来就把所有功能全写完再联调结果出了问题根本无法判断是哪个环节造成的最后只能全部推翻重来。8. 最后一公里的经验总结这个项目做完我最大的体会是循迹小车和蓝牙小车分开来做都不难难的是系统整合时暴露出来的电源、时序和通信细节问题。尤其是电源部分三伏电池同时给电机和蓝牙供电时电机起步瞬间的冲击会直接影响蓝牙模块的稳定性这个问题在教程里几乎没人提到但实际项目里它比算法本身更容易让你卡进度。另外就是调试工具的投入。我花了十几块钱买了一个逻辑分析仪把蓝牙UART的波形抓出来一帧一帧看通信问题的定位速度提升了一大截。如果你手头只有万用表和示波器那串口回发确认帧这个设计一定要加它能帮你区分没收到指令和收到没执行这两个完全不同的故障域。最后提醒一点别迷信参数抄作业。同一套PID参数在不同电池电压下表现差异很大锂聚合物电池从满电4.2V掉到3.7V的过程中电机的输出特性会明显变化。我的做法是在电池电压高于3.9V时用高速参数组低于3.9V时自动切换到低速参数组这个简单的小逻辑让小车在整场演示中都保持了稳定的表现。这个思路也可以扩展到你的项目里毕竟稳定压倒一切车跑得再花哨半路趴窝就全完了。
阅读完成 · 觉得有帮助?