Arduino循迹小车源码拆解:从PID控制到TB6612调参实战

Arduino循迹小车源码拆解:从PID控制到TB6612调参实战 简介Arduino循迹小车源码详解资料包面向嵌入式入门学习者、Arduino爱好者以及准备参与智能车项目的学生可系统解决循迹小车在路径识别、转向控制、速度调节与故障排查中的常见问题。压缩包共3个文件体积仅6KB包含HTML说明页、inscode项目配置及Git忽略文件结构精简既方便在线查看也便于本地导入运行目前已有448人学习下载。内容提供完整可运行源码深入解读循迹模块将传感器模拟信号转换为数字信号、通过左右轮差速实现小左转、小右转、大左转、大右转等不同转向的判断逻辑同时介绍基于PWM的调速代码编写方法说明如何通过改变输出脉冲宽度来控制电机转速与小车运动状态。针对调试阶段容易遇到的传感器信号不稳定、电机响应不及时、小车偏离路线等问题也逐一给出滤波优化、参数调整、传感器位置重设等解决思路。整体以实践为主线帮助读者在自主调试中理解原理、积累经验并可进一步扩展遥控、避障等功能。1. 项目概述与整体设计思路Arduino循迹小车几乎是每个嵌入式爱好者的入门必修课也是各类电子设计竞赛、智能车竞赛中最常见的基础载体。很多同学拿到一套小车套件或者从网上扒到一份源码烧进去之后发现小车要么原地打转要么冲出场外然后就开始怀疑硬件坏了。其实大部分问题不在于硬件而是代码逻辑和硬件结构没对上。这篇文章我就从源码角度完整拆解一个可实际跑通的循迹小车代码结合硬件接线、控制逻辑和调参过程把每一段代码为什么这么写讲清楚。这套方案的目标很明确让小车能够沿着一条黑色引导线通常是黑色胶带贴在浅色地面上自主行驶在弯道、十字交叉口等常见赛道元素上保持稳定不丢线。代码以Arduino Uno/Nano为控制核心配合5路红外灰度传感器和TB6612电机驱动模块实现差速转向控制。适合刚接触Arduino和智能车的学生、DIY玩家以及正在备战校内赛或省赛的竞赛队伍。1.1 为什么用Arduino而不是STM32我用Arduino做循迹小车核心原因只有一个快。Arduino的生态让你把注意力全部放在控制逻辑上而不是花两三天去配置寄存器、调试时钟树和烧录器。循迹小车本身对算力要求极低8位AVR芯片的16MHz主频完全够用传感器采样周期在毫秒级电机响应时间在几十毫秒级Arduino的实时性没有任何瓶颈。等你有一次完整的车模调试经验之后再考虑换STM32或者ESP32去卷更高性能的玩法比如Micro-ROS、视觉循迹、SLAM那时候你才会真正理解为什么竞赛老手总说换主控不是优化的起点。说白了Arduino是帮你把车能跑先解决的问题解决掉后面再谈车跑得好。1.2 硬件选型与底盘方案我这里用的配置是市面上最常见的入门四轮底盘主控Arduino NanoUno也可以代码完全兼容Nano体积小更好布线传感器5路TCRT5000红外反射式灰度传感器模块蓝板那种电机驱动TB6612FNG模块比L298N轻、压降小、逻辑电平兼容性好电机TT马达带减速箱的直流电机带编码器版本更好不带也能跑电源两节18650锂电池串联7.4V供电通过降压模块给Arduino供5V选5路传感器而不是3路是因为3路只能做简单的看到线就转向弯道稍急就会丢线5路可以识别出更细的偏差等级配合PID控制稳定性和过弯速度都有质的提升。TB6612比L298N的优势在于体积小、发热低而且PWM频率响应更快适合频繁调速的场景。底盘方面前轮转向的阿克曼结构虽然更接近真车但循迹效果远不如四轮差速驱动灵活——差速驱动可以通过左右轮反向旋转实现原地转向在窄弯里优势明显。这也是绝大多数竞赛循迹车的选择。2. 循迹原理与硬件接线细节2.1 红外对管的工作逻辑TCRT5000的工作原理不复杂模块上一个红外发射管持续发射红外光红外光照射到地面后反射回来被旁边的光电晶体管接收。关键在地面的反射率差异——黑胶带吸光能力很强反射回来的红外光少浅色地面反射率高接收到的信号强。模块上的比较器通常是LM393会根据反射强度输出高电平或低电平。这里面有个容易踩的坑TCRT5000的检测距离大概在1cm到3cm之间太近会饱和太远信号弱。实际安装时传感器底部离地面保持在1.5cm到2cm之间最合适。而且深色桌面、黑色跑道材质、阳光直射都会干扰判断比赛前一定要先做表面测试。模块上还有一个可调电位器用来调节灵敏度阈值。把小车放在白色地面上顺时针旋到指示灯刚好亮起放在黑线上逆时针旋到灯刚好熄灭。反复微调几次这个阈值基本就准了。这一步如果偷懒不做后面代码写再好都没用。2.2 5路灰度传感器的接线方式数字输出型的循迹模块一般是TTL电平输出接线非常简单传感器引脚Arduino Nano引脚说明VCC5V模块供电GNDGND共地OUT1最左D8数字输入OUT2左中D9数字输入OUT3中间D10数字输入OUT4右中D11数字输入OUT5最右D12数字输入电机驱动PWMAD5左电机PWM电机驱动AIN1/AIN2D6/D7左电机方向电机驱动PWMBD3右电机PWM电机驱动BIN1/BIN2D4/D2右电机方向如果你用的是带模拟输出的传感器模块没有LM393比较器那就把OUT引脚接到A0到A4模拟口用analogRead读取0到1023的灰度值。模拟方案的好处是能拿到连续信号方便后续做更精细的PID控制但处理逻辑也更复杂。本文代码以数字量方案为主后面第4节我会补充模拟量的处理思路。3. 核心源码逐段拆解3.1 引脚定义与初始化源码的核心结构其实非常简单任何循迹小车都逃不出这个框架读取传感器状态 → 计算位置偏差 → 计算左右轮速度差 → 输出PWM。下面这段初始化代码我用了几年可靠性很高// 循迹小车源码 - 基于Arduino Nano/TB6612/5路红外灰度传感器 // 按钮启动版避免上电瞬间小车冲出桌面 #define SENSOR_LEFT 8 #define SENSOR_MID_L 9 #define SENSOR_MID 10 #define SENSOR_MID_R 11 #define SENSOR_RIGHT 12 // TB6612电机驱动引脚 #define AIN1 6 #define AIN2 7 #define PWMA 5 #define BIN1 4 #define BIN2 2 #define PWMB 3 // 启动按钮 #define START_BTN 13 void setup() { pinMode(SENSOR_LEFT, INPUT); pinMode(SENSOR_MID_L, INPUT); pinMode(SENSOR_MID, INPUT); pinMode(SENSOR_MID_R, INPUT); pinMode(SENSOR_RIGHT, INPUT); pinMode(AIN1, OUTPUT); pinMode(AIN2, OUTPUT); pinMode(PWMA, OUTPUT); pinMode(BIN1, OUTPUT); pinMode(BIN2, OUTPUT); pinMode(PWMB, OUTPUT); pinMode(START_BTN, INPUT_PULLUP); // 初始状态电机停止 digitalWrite(AIN1, LOW); digitalWrite(AIN2, LOW); digitalWrite(BIN1, LOW); digitalWrite(BIN2, LOW); analogWrite(PWMA, 0); analogWrite(PWMB, 0); Serial.begin(9600); }每个引脚定义都对应硬件接线表中的一一行这一步做错了后面全乱。一个经验是电机驱动的方向引脚AIN1/AIN2、BIN1/BIN2千万不要悬空必须初始化为LOW否则上电瞬间电机可能自己转起来。启动按钮用INPUT_PULLUP按钮一端接GND另一端接D13按下时读取到LOW。3.2 PID偏差计算与转向输出这是整个代码的灵魂部分也是最容易理解偏差的地方。5路传感器从左到右排列我给它定义一个位置权重最左测到黑线位置值为-4左中测到黑线位置值为-2中间测到黑线位置值为0右中测到黑线位置值为2最右测到黑线位置值为4实际计算时不是简单读取哪一路亮了而是先读取所有传感器的状态然后计算加权平均值得到一个连续的偏差值error。这个地方初学者最容易犯一个错误直接把位置权值当作固定档位来做开关控制比如左边亮了就向左打死。这样做的结果是车子左右疯狂摆动过弯全靠运气。正确做法是把偏差值映射成转向输出量并且加入比例和微分控制int readError() { int s1 digitalRead(SENSOR_LEFT); int s2 digitalRead(SENSOR_MID_L); int s3 digitalRead(SENSOR_MID); int s4 digitalRead(SENSOR_MID_R); int s5 digitalRead(SENSOR_RIGHT); // 数字量输出黑线检测到为LOW视模块而定某些模块反相 // 这里以传感器输出LOW表示检测到黑线为例 int error 0; if (s1 LOW) error -4; if (s2 LOW) error -2; if (s3 LOW) error 0; if (s4 LOW) error 2; if (s5 LOW) error 4; return error; }3.3 PID控制器与电机输出有了偏差值error之后用PD控制计算左右轮的速度差。为什么不用完整的PID因为积分项在循迹场景下不仅没有帮助反而会导致过弯时攒了一堆误差然后冲出去。P项负责快速响应偏差D项负责抑制超调D项在过弯时能提前减速和收紧转向这一点非常重要。// PD参数 - 调参从这里开始 double Kp 1.8; double Kd 3.5; int lastError 0; int baseSpeed 160; // 基础速度 0-255 void loop() { // 等待按钮启动防止上电乱跑 while (digitalRead(START_BTN) HIGH) { motorStop(); } delay(100); // 消抖 int errorVal readError(); // 计算PD输出 int P (int)(Kp * errorVal); int D (int)(Kd * (errorVal - lastError)); int correction P D; // 计算左右轮速度 int leftSpeed baseSpeed correction; int rightSpeed baseSpeed - correction; // 速度限幅 leftSpeed constrain(leftSpeed, -255, 255); rightSpeed constrain(rightSpeed, -255, 255); // 驱动电机 motorDrive(leftSpeed, rightSpeed); // 更新上一次偏差 lastError errorVal; // 可选串口打印调试 Serial.print(E:); Serial.print(errorVal); Serial.print( L:); Serial.print(leftSpeed); Serial.print( R:); Serial.println(rightSpeed); }这套代码的思路是当车偏左时error为负correction为负左轮减速、右轮加速车头自然向右修正。当车偏右时反过来。D项的作用在于如果车子正在快速向左偏转的过程中即便当前偏差已经接近零D项也会先给一个反向修正避免过冲。调参时Kp从小到大加车不够灵敏就加大Kp车来回摆动就加大Kd这个原则百试不爽。3.4 电机驱动函数与完整源码motorDrive函数负责把速度值转换成电机动作。TB6612的逻辑很简单AIN1和AIN2是高/低组合控制电机方向PWMA控制速度。这里要注意Arduino的analogWrite输出PWM范围是0到255而刹车和反转需要特殊处理void motorDrive(int leftSpeed, int rightSpeed) { // 左电机 if (leftSpeed 0) { digitalWrite(AIN1, HIGH); digitalWrite(AIN2, LOW); analogWrite(PWMA, leftSpeed); } else { digitalWrite(AIN1, LOW); digitalWrite(AIN2, HIGH); analogWrite(PWMA, -leftSpeed); } // 右电机 - 注意两个电机安装方向相反控制极性要反过来 if (rightSpeed 0) { digitalWrite(BIN1, LOW); digitalWrite(BIN2, HIGH); analogWrite(PWMB, rightSpeed); } else { digitalWrite(BIN1, HIGH); digitalWrite(BIN2, LOW); analogWrite(PWMB, -rightSpeed); } } void motorStop() { analogWrite(PWMA, 0); analogWrite(PWMB, 0); digitalWrite(AIN1, LOW); digitalWrite(AIN2, LOW); digitalWrite(BIN1, LOW); digitalWrite(BIN2, LOW); }这里最坑的地方是右电机的极性。由于左右电机在底盘上对称安装要让两个轮子同时向前转驱动信号的极性必须相反。如果你发现一个轮子向前一个向后不要怀疑接线把右电机的两个方向引脚交换一下就行。另外TB6612的STBY引脚必须接高电平5V否则芯片一直处于待机状态电机完全不动——这个坑我当年排查了整整一个晚上。4. 从代码到跑通调参与优化实战4.1 传感器标定与阈值处理代码烧录进去之后第一步不是急着上车跑而是先做传感器标定。打开串口监视器把小车放在白色地面上观察每一路的输出状态然后放到黑线上再观察一次。正常情况下两种状态应该是明确相反的。如果某一路上电后状态不对先调模块上的电位器调完还不行检查是不是线接错了或者模块坏了。数字量方案有一个天然缺陷阈值是固定的环境光变化或者电池电压下降都会影响判断。更稳的方案是改用模拟量读取每次上电先做一个自动标定程序让小车记住白地和黑线的ADC值范围然后在中间取阈值。这样在不同场地之间切换不用反复手动调电位器。模拟量标定的参考代码逻辑大致是上电后蜂鸣器响两声用户把车先放在白地上按一下按键采样10次取平均再把车放在黑线上按一下按键采样取平均两个平均值取中值作为阈值存到EEPROM里。竞赛队伍换场地调车的时候这个功能简直救命。4.2 电机死区补偿与直道修正TT马达的个体差异远比想象中大。两个电机标称转速一样实际装上车之后由于齿轮摩擦、轮胎磨损、轴承阻力不同直道上即使给定完全相同的PWM值车也会跑偏。这时候就轮到右轮微调系数上场了。给代码增加一个直道修正系数// 右轮微调系数典型值0.85-1.15 // 若车向右偏右轮偏快加大这个值若车向左偏减小这个值 double rightTrim 1.0;在输出速度的时候右轮速度乘以rightTrimint rightFinal constrain((int)(rightSpeed * rightTrim), -255, 255); motorDrive(leftSpeed, rightFinal);实际测试方法把车放在长直道上拔掉循迹逻辑直接让两个电机以固定PWM空转观察车的走向。如果向右偏就增大rightTrim让右轮更快一点或者反方向逻辑取决于你的车架朝向。每次调整0.05反复测试直到直道100米内不跑偏为止。4.3 转向强度标定与速度分级搞定了直道接下来要解决弯道。PD参数不是一次就调好的我建议按下面的顺序来第一步先设定一个较低的baseSpeed比如120放车上赛道跑一圈观察弯道表现。如果车能过弯但转向不足冲出赛道先加大Kp。每加0.2测试一次。第二步当Kp大到一定程度时车在直道上会开始小幅摆动这就是超调信号。此时加大Kd从2开始每次加0.5直到直道恢复稳定。第三步逐步提高baseSpeed每提高20再调一遍Kp和Kd。你会发现速度越高Kd的作用越重要。有一个技巧在弯道入口前主动减速比在弯道中加大转向更有效。实现起来很简单当中间三路传感器全部检测到黑线说明车处于近似直线状态时全速跑一旦只有单侧传感器检测到线说明进入弯道就把基础速度降下来30%左右同时加大转向输出。这样在急弯里的表现会脱胎换骨。5. 竞赛向进阶方向5.1 电磁循迹与传感器升级如果赛道是白色底没有黑线比如某些电磁组比赛红外循迹就彻底没戏了。电磁循迹的原理完全不同赛道中心铺设一根通有20kHz交变电流的导线车上的电感感应到磁场强度通过运放放大后送入ADC根据左右电感感应值差值判断车相对导线中心的位置。电磁循迹在Arduino上也能实现但需要额外的前置运放电路通常是LMV358或者OPA4376把电感的微弱感应信号放大到ADC能识别的范围。代码上和红外循迹最大的区别在于误差值是连续的模拟量而不是离散的数字量PID调参逻辑反而更顺滑。但电磁组的赛道布线、运放增益调节、频率抗干扰这些细节比红外方案复杂了一个量级。5.2 视觉循迹与OpenMV方案用OpenMV摄像头替代红外传感器是目前竞赛中最热门的玩法之一。OpenMV可以输出画面中心到黑色引导线的像素偏移量直接通过串口发给ArduinoArduino解析为偏差值喂给PID。视觉方案的最大优势是前瞻量大——摄像头看到的是前方十厘米甚至更远的赛道控制提前量比红外大得多高速过弯极限高很多。但视觉方案对代码能力的要求也陡增ROI区域设置、二值化阈值、图像滤波、帧率优化每一样都是独立的知识点。我个人的建议是先把红外方案跑熟理解差速控制和PID的本质再往视觉方向升级否则排查问题的时候你根本分不清是控制问题还是图像问题。5.3 无线调试与仿真验证调车过程中最痛苦的事情就是每改一个参数都要插上USB线重新烧录。强烈建议在车上预留一个蓝牙串口模块或者nRF24L01无线透传模块的接口把Kp、Kd、baseSpeed这几个参数做成串口指令实时修改不用反复烧录。Arduino端解析串口数据的代码很简单if (Serial.available()) { char c Serial.read(); if (c p) Kp Serial.parseFloat(); if (c d) Kd Serial.parseFloat(); if (c s) baseSpeed Serial.parseInt(); }另外如果你手头没有实物硬件Wokwi这个在线仿真平台支持Arduino Uno L298N 灰度传感器的组合可以在浏览器里先把代码逻辑跑通至少验证传感器读取、偏差计算、电机输出这些核心逻辑是否正常。仿真环境里没有物理惯性所以PID参数不能直接照搬到实车但作为写代码前的逻辑验证工具它免费又好用。6. 常见问题与排查技巧实录6.1 传感器读数异常现象串口打印的传感器状态一直不变或者白地和黑线的状态反了。排查顺序先看模块上的电源指示灯是否亮供电问题再用万用表测传感器输出引脚电平是否变化模块损坏最后检查模块上电位器是否被拧到极限位置阈值不对。数字量模块如果输出逻辑和代码假设相反直接在readError函数里把LOW换成HIGH就行不用纠结为什么反了——不同厂家模块的输出逻辑本来就不统一定义。还有一个容易被忽略的点传感器装得太高或太低都会导致全白或全黑的假象。装上支架后手压着传感器降到黑线上方观察输出变化是否可以稳定触发否则就调整安装高度。6.2 电机不转或转速不一致现象通电后电机完全不动或者只有一个轮子转。先查TB6612的STBY引脚它接到5V之后芯片才解禁。然后查方向引脚组合AIN1H、AIN2L是正转反过来是反转如果两个都是L或者都是H电机就是刹停状态。最后查PWM引脚是否真的输出了波形——用示波器或者万用表频率档测D5/D3引脚。一个我在现实中碰到过的经典案例电机转动时有明显咔咔声但不顺畅原因是电机的焊线虚焊或者杜邦线接触不良。TT马达转速低但电流不小细杜邦线在某些情况下会电发热导致电流供应不足。6.3 小车冲出赛道或原地打转现象放上赛道后小车要么直线冲出去要么原地逆时针打转。原地打转通常意味着左右轮速度差太大但误差计算逻辑本身错了。最常见的原因右电机的极性配置反了导致右轮向后转车速差等于两个轮子的速度之和车当然原地打转。把motorDrive的右电机方向引脚换一下问题立刻消失。冲出赛道的情况要区分是直道冲还是弯道冲。直道冲出去大概率是传感器阈值没标定好车根本没看到线弯道冲出去先降低baseSpeed再加大Kp。如果弯道中小车表现为进入弯道后要先抖动一下才转向说明D项太强把Kd降一档。最后再分享一个我自己调试了非常多车之后的体会循迹小车的代码从来不是一次写对的而是写框架 → 跑起来 → 发现问题 → 按逻辑修反复迭代出来的。不要怕改参数每改一次参数带着串口数据去对应实际运动表现你会慢慢建立对这个系统最直观的感知。希望这份源码和踩坑记录能让你少走弯路很快跑出第一条完整的赛道。本文还有配套的精品资源点击获取