做循迹小车最容易翻车的不是直道跑不了直线而是丢线以后小车当场发懵——传感器全黑或全白既不知道该往哪边转也不知道该停还是该冲。MSPM0G3507 这颗芯片在这个项目里其实被严重低估了Cortex-M0 跑到 80MHzGPIO 翻转、PWM 输出、定时器全是现成的配合 TI 的 SysConfig 图形化配置十几分钟就能把工程环境搭起来。今天这篇分享的是我这个系列的第一篇代码主题就一个强制寻黑线。所谓强制就是不做温柔的渐变修正一旦检测到偏差或丢线直接打满方向把车头甩回线上。这套逻辑特别适合直道稳、弯道急、黑线偶尔断线的入门赛道也适合刚接触 MSPM0 系列、想搞懂循迹状态机怎么写的人参考。我不会把这篇写成 SDK 文档的翻译直接讲思路、贴代码、说坑。后面跟着的配置、接线、调试记录都是我实际跑过车之后总结的照着做基本能复现。1. 为什么用 MSPM0G3507 做循迹小车芯片选型与硬件准备1.1 MSPM0G3507 到底强在哪很多新手一上来选 Arduino UNO理由很简单例程多、资料全。但 Arduino 那颗 AVR 主频只有 16MHz跑两路 PWM 加两路 GPIO 中断就已经很吃力了更不用说以后想在这套代码上加 PID 速度环、编码器采集、无线遥控这些功能。MSPM0G3507 在同价位的板子里外设和性能属于非常能打的Arm Cortex-M0 内核最高 80MHz 主频处理几个传感器输入完全不是瓶颈128KB Flash、32KB SRAM写完整套循迹逻辑还绰绰有余内置多路定时器可以非常方便地输出多路 PWM16 位 ADC、UART、I2C、SPI后续扩展电磁循迹、OLED 显示、蓝牙遥控都留了接口官方 LaunchPad 板载 XDS110 调试器插一根 USB 线就能在线仿真、看变量、打断点。关键是这颗芯片的开发流程和 STM32 很像但配置方式更省心。TI 自己的 SysConfig 工具会用图形化界面让你勾选引脚、时钟、外设然后自动生成初始化代码大部分引脚复用问题根本不需要去翻数据手册。我实际用下来从新建工程到点亮一颗 LED大概十分钟出头。对于循迹小车这种需要反复调整 GPIO 和定时器的项目这个效率优势太明显了。1.2 一套能跑起来的硬件清单与接线方案硬件不用买多贵的除非你要跑竞速级比赛。我这次的配置就很简单MSPM0G3507 LaunchPad 开发板一块TB6612FNG 双路电机驱动模块一个TCRT5000 红外反射式循迹传感器模块两个TT 减速电机带轮子两个搭配亚克力小车底盘7.4V 锂电池给电机驱动供电一个降压模块从电池降压到 5V 给 LaunchPad 供电若干杜邦线和螺纹铜柱。接线的时候重点记住一个原则所有模块必须共地。传感器、驱动模块、MSPM0 的地如果没有连在一起数字电平参考点不一致GPIO 读到的高低电平就是乱的表现是小车要么不动、要么乱转。我把实际接法整理如下信号接到 MSPM0G3507说明左传感器 DOPA0GPIO 输入检测黑线右传感器 DOPA1GPIO 输入检测黑线左电机 PWMPA10TIMG0 通道 0 输出右电机 PWMPA11TIMG0 通道 1 输出TB6612 AIN1PB0左电机方向控制TB6612 AIN2PB1左电机方向控制TB6612 BIN1PB2右电机方向控制TB6612 BIN2PB3右电机方向控制TB6612 STBY接 3.3V必须拉高否则电机不转这里再次提醒一句很多人的车一点反应都没有最后查出来是 TB6612 的 STBY 引脚浮空或者接低了。STBY 是待机控制高电平才让驱动芯片正常工作。这个细节在模块说明书里容易被忽略但现场调试翻车概率极高。电机电源和逻辑电源如果 LaunchPad 用 USB 供电、电池只给电机供电那电机转动瞬间的压降会干扰逻辑电路建议电池经过降压模块给整板供电或者至少保证电源输入和电机驱动的地线在同一个点上避免形成地环流。2. 理解强制寻黑线传感器电平、偏差表与状态机2.1 灰度传感器的输出电平到底是高还是低这个坑我建议所有人在写代码前先花五分钟确认不要凭经验猜。TCRT5000 模块上面有一个电位器可以调阈值不同批次、不同供电电压、不同地面材质下DO 引脚在黑线上的输出电平完全可能不一样。我手头两个模块一个检测到黑线输出高电平另一个输出低电平。如果代码里写死了一种电平另一个模块插上去就是完全反向的行为。确认方法很简单模块接好 3.3V 和 GND用万用表量 DO 引脚对 GND 的电压。把传感器对着白纸记录电压再用手遮住传感器模拟黑线再记录一次。电压从 0 跳到 3.3V说明黑线是高电平反过来就是低电平。记下这个结果写代码时用一个宏来控制// 假设实测结果为检测到黑线 DO 输出低电平 #define LEFT_SENSOR_ON_LINE() (DL_GPIO_readPins(GPIOA, LEFT_SENSOR_PIN) 0) #define RIGHT_SENSOR_ON_LINE() (DL_GPIO_readPins(GPIOA, RIGHT_SENSOR_PIN) 0)如果实测是黑线高电平把宏里的 0改成! 0就行。这样后面所有逻辑都统一为检测到黑线为真不需要到处取反。2.2 双传感器循迹的四种偏差组合两个传感器装在车头左右间距略小于黑线宽度这样正常骑线的时候两个传感器都能压住黑线。然后把传感器状态抽象成组合左传感器右传感器车辆位置判断控制动作00丢线或位于大片空白区域强制搜索01车偏右线在车头左侧强制左转10车偏左线在车头右侧强制右转11正好骑线或横跨黑线直行可能有人不理解为什么要这样对应如果车整体向右侧偏移那么在车头视角里右侧传感器仍然压在线上左侧传感器已经悬空到白地上了。此时如果不赶紧把车头往左压回来再往前冲几十厘米就彻底冲出赛道。所以左离线、右在线的动作为强制左转。同理左在线、右离线就强制右转。这个映射关系如果搞反车的表现会非常诡异——它会在黑线边缘画圈而且越画越远。另外一个容易忽略的问题黑线宽度。比赛的胶带一般宽 2cm 左右两个传感器的间距最好不要超过 1.5cm否则两个传感器永远无法同时压在黑线上代码里(1,1)直行分支基本每次都会跳过车头就会左右摇摆、永远无法稳定。间距太小也不行两个传感器几乎叠在一起和单传感器没区别偏差方向判断不出来。我调过几次的经验是让两个传感器的中心距约为黑线宽度的 60%70%。2.3 丢线之后怎么强制找回来搜索状态机设计强制寻黑线和普通寻线的最大区别在于对丢线状态的处理。很多基础例程丢线后直接原地停车这在比赛场合就是判死刑。强制搜索的核心方案是摆头进入丢线状态后小车先在原地向左旋转一段时间如果没找到线再向右旋转一段时间如此往复。传感器相当于一个探测头车身旋转带动传感器扫过一个扇形区域总有一个时刻能扫到黑线。我用的搜索状态机很简单只有两个状态SEARCH_LEFT左轮反转、右轮正转车身原地左转SEARCH_RIGHT右轮反转、左轮正转车身原地右转。状态切换以时间为基准每个状态持续 150ms到时间就切到另一个状态。这里的核心思想是不要停下来、不要慢慢试探反正已经丢线了与其犹豫不如快速搜索。实测下来这种原地转而不是边向前边转弯的搜索方式找回黑线的概率最高。因为边向前边转弯会让传感器扫描路径变成螺旋线容易越扫越偏原地旋转时车头扫过的区域固定黑线一旦在扫描范围内传感器就能迅速捕获。选择先左后右的顺序也是有讲究的。大多数赛道急弯是有连续性的比如连续左弯后丢线黑线大概率在车头的左侧。先向左搜索往往第一拍就能找回线省掉一个搜索周期。当然这个顺序不是绝对的如果你的赛道以右弯为主把两个状态调换顺序即可。代码上就是调换一下枚举值完全不需要改其他逻辑。3. 核心代码逐段拆解GPIO、PWM 与强制搜索逻辑3.1 SysConfig 生成工程引脚、时钟、定时器新建 MSPM0G3507 工程的时候我建议直接基于 SDK 自带的 empty 工程修改避免从零搭建一堆底层配置。在 SysConfig 里主要做这几件事时钟配置选择 SYSPLLMCLK 设为 80MHz。循迹任务里不需要高频外设但主频高一点对 PWM 分辨率和中断响应都有好处GPIO 配置把 PA0、PA1 设为输入模式对应两个传感器PB0PB3 设为输出对应 TB6612 的方向控制PA10、PA11 复用为 TIMG0 的 C0、C1 通道作为 PWM 输出TIMG0 配置选择 PWM 模式设置 period 为 2000prescale 为 4。这样 PWM 频率约为 80MHz / 4 / 2000 10kHz电机运行声音在人耳可接受的范围内也不容易触发电机电感的尖锐啸叫UART0 配置可选如果你习惯用串口打印调试信息可以把 PA8、PA9 复用为 UART0波特率 115200。SysConfig 保存并生成代码后main 里一定要先调用SYSCFG_DL_init();这个函数会完成引脚功能复用、时钟使能、定时器初始化等全部底层工作。如果发现某个外设不工作第一步先确认这个初始化函数有没有被调用这是最基础也最容易被忽略的坑。3.2 主循环里的四种处理分支先把头文件和宏定义摆出来#include ti_msp_dl_config.h #define LEFT_SENSOR_PIN GPIO_PIN_0 #define RIGHT_SENSOR_PIN GPIO_PIN_1 // 电平方向依据 2.1 节实测结果填写 #define LEFT_ON_LINE() (DL_GPIO_readPins(GPIOA, LEFT_SENSOR_PIN) 0) #define RIGHT_ON_LINE() (DL_GPIO_readPins(GPIOA, RIGHT_SENSOR_PIN) 0) #define MOTOR_FORWARD 1 #define MOTOR_BACKWARD 0 #define PWM_STRAIGHT 1400 #define PWM_TURN 1200 #define PWM_SEARCH 900 void motor_ctrl(uint8_t ch, uint8_t dir, uint16_t speed) { if (ch 0) { if (dir MOTOR_FORWARD) { DL_GPIO_setPins(GPIOB, GPIO_PIN_1); DL_GPIO_clearPins(GPIOB, GPIO_PIN_0); } else { DL_GPIO_setPins(GPIOB, GPIO_PIN_0); DL_GPIO_clearPins(GPIOB, GPIO_PIN_1); } DL_TimerG_setCaptureCompareValue(TIMG0, speed, DL_TIMER_CC_0_CC_O); } else { if (dir MOTOR_FORWARD) { DL_GPIO_setPins(GPIOB, GPIO_PIN_3); DL_GPIO_clearPins(GPIOB, GPIO_PIN_2); } else { DL_GPIO_setPins(GPIOB, GPIO_PIN_2); DL_GPIO_clearPins(GPIOB, GPIO_PIN_3); } DL_TimerG_setCaptureCompareValue(TIMG0, speed, DL_TIMER_CC_1_CC_O); } }这里 GPIO_PIN_0、GPIO_PIN_1 在驱动上的语义是 AIN1、AIN2GPIO_PIN_2、GPIO_PIN_3 对应 BIN1、BIN2。TB6612 的方向逻辑是AIN10、AIN21 时左电机正转AIN11、AIN20 时左电机反转。同理 BIN1、BIN2 控制右电机注意电源极性接法不同实际方向可能和你预期相反这需要在整车上电后单独验证一次。主循环才是整个程序的核心int main(void) { SYSCFG_DL_init(); SysTick_Config(80000); // 80MHz 主频每毫秒中断一次 while (1) { if (LEFT_ON_LINE() RIGHT_ON_LINE()) { // 骑线状态直行 motor_ctrl(0, MOTOR_FORWARD, PWM_STRAIGHT); motor_ctrl(1, MOTOR_FORWARD, PWM_STRAIGHT); } else if (!LEFT_ON_LINE() RIGHT_ON_LINE()) { // 车偏右强制左转 motor_ctrl(0, MOTOR_BACKWARD, PWM_TURN); motor_ctrl(1, MOTOR_FORWARD, PWM_TURN); } else if (LEFT_ON_LINE() !RIGHT_ON_LINE()) { // 车偏左强制右转 motor_ctrl(0, MOTOR_FORWARD, PWM_TURN); motor_ctrl(1, MOTOR_BACKWARD, PWM_TURN); } else { // 两个都不在线丢线进入强制搜索 search_line(); } } }这段逻辑可以说一眼就能看懂但实际运行起来有几个性能关键点。第一DL_GPIO_readPins返回的是一个带引脚的掩码值不是单纯的 0 或 1所以判断条件里用 0判断引脚为低这是没问题的。第二每个循环周期内四个条件都会被依次判断但因为if/else if结构实际只会执行其中一个分支不会出现同时执行多个动作的情况。第三我把鼠标放在直行电机速度上时PWM 占空比是 1400/2000 70%接近 TT 电机的舒适工作区不要上来就给到 90% 以上否则第一次丢线搜索就可能因为车速过快而直接冲出桌面。3.3 强制寻黑线状态机的完整实现搜索函数需要一个毫秒级时间基准我直接用 SysTick 产生volatile uint32_t g_tick_ms 0; void SysTick_Handler(void) { g_tick_ms; } uint32_t get_tick_ms(void) { return g_tick_ms; }注意SysTick_Config(80000)这里的参数是 80000因为主频 80MHz每计数器加一对应 1/80MHz 秒计数到 80000 就是 1ms。如果实际主频不是 80MHz这个参数要相应改否则搜索状态机的换向时间就全错了。搜索函数的实现如下typedef enum { SEARCH_LEFT 0, SEARCH_RIGHT } SearchDir; void search_line(void) { static SearchDir dir SEARCH_LEFT; static uint32_t start_ms 0; uint32_t now get_tick_ms(); // 每 150ms 切换一次搜索方向 if (now - start_ms 150) { dir (dir SEARCH_LEFT) ? SEARCH_RIGHT : SEARCH_LEFT; start_ms now; } if (dir SEARCH_LEFT) { // 左轮反转、右轮正转车身原地左旋 motor_ctrl(0, MOTOR_BACKWARD, PWM_SEARCH); motor_ctrl(1, MOTOR_FORWARD, PWM_SEARCH); } else { // 右轮反转、左轮正转车身原地右旋 motor_ctrl(0, MOTOR_FORWARD, PWM_SEARCH); motor_ctrl(1, MOTOR_BACKWARD, PWM_SEARCH); } }这个函数的核心设计是时间驱动而不是循环计数驱动。用get_tick_ms()做时间基准不管主循环跑得快慢搜线方向切换的时间都是稳定的 150ms。如果直接累加循环次数一旦你在其他代码段里加了耗时操作搜索节奏就会完全乱掉。这是我调试过程中踩过的坑最开始用for循环做延时主循环里还要串口打印结果打印耗时不一样搜索方向切换速度时快时慢小车行为毫无规律。后来全部改成时间戳判断问题立刻消失。还有一个细节now - start_ms的运算在uint32_t下可以自然处理定时器回绕即使系统连续运行 49 天也不会出错这一点对于嵌入式代码来说很实用不用额外防御。3.4 为什么强制要采用原地差速旋转我在 3.2 的代码里左右转分支用的是左轮反转、右轮正转的原地旋转方式。相比常见的左轮减速、右轮加速的差速转向这种方式转向角速度大得多。以我们这个底盘举例差速转向时转向角速度约 100°/s原地差速旋转能到 300°/s 以上。高速冲进急弯时车头在极短时间内从切线方向偏到道路外侧留给转向的时间窗口可能只有一两百毫秒。如果转向角速度不够传感器还没扫到黑线车已经冲出赛道了。所以强制的本质是在响应速度上留足余量宁可在直道上经常微调震荡也不能在急弯里丢线。当然原地旋转也有代价内侧轮反转会增加轮胎磨损地面不够粗糙时还会打滑而且对电机电流冲击比较大。所以我把搜索时的 PWM 设得比正常转向低一档900 而不是 1200减少轮子空转的概率。如果你的底盘轮子抓地力很好可以适当把 PWM_SEARCH 提高到 1000 左右找回速度会更快。4. 上电前的最后检查编译、下载与常见坑4.1 编译环境和程序烧录MSPM0G3507 在 Keil、CCS、IAR 里都能开发。如果你用 Keil需要先装 MSPM0 的 Device Family Pack在 Pack Installer 里搜索 MSPM0G1X0G3X0 并安装如果你用 CCS导入 SDK 例程后 SysConfig 会自动参与编译工程配置很短。下载和调试直接用 LaunchPad 板载的 XDS110在 Keil 里打开 Debug 设置选择 TI XDS110 Debug Probe然后点击 Load 即可。CCS 里更简单直接按 F11 进仿真。烧录后如果小车没反应用万用表量一下板上 3.3V 是否正常再量一下 PA0 和 PA1 的电平是否随传感器状态翻转。这样能快速确认是硬件问题还是软件问题。有一点必须提醒如果你在 SysConfig 里重新分配了引脚Keil 工程需要重新编译生成代码不是保存 .syscfg 就行。经常有人改了引脚配置结果实际烧录的还是旧代码车怎么都不按新配置工作。CCS 里一般会自动触发生成Keil 里要注意编译前勾选 SysConfig 生成步骤。4.2 现场调试最常翻车的 5 个问题我这次调试花了大约一个晚上遇到的问题基本都集中在这几类问题一传感器电平搞反。现象是小车在线外疯狂加速越冲越远。原因就是黑线电平判断反了把在线当成离线。解决办法是用 2.1 节的方法实测后改宏定义不要试图在逻辑里到处取反改宏最稳妥。问题二左右电机方向接反。现象是直行变倒车或者程序让它左转它右转。我一般先把小车架空运行一个只让左轮正转的函数确认左轮确实正转再测右轮最后让左右轮同时反转确认是原地左旋还是右旋。把方向真值表打印出来贴在工作台上后面接线和调试都能省事。问题三电机堵转导致 MCU 复位。TT 电机启动和堵转瞬间电流非常大如果电池电量不足或电源走线太细瞬间压降会让 MSPM0G3507 掉电复位。现象是车一启动就重启或者跑几圈突然从头开始执行程序。解决办法是在电池输出端并联一个 470uF1000uF 的电解电容再并联一个 0.1uF 陶瓷电容滤除高频噪声。如果还是复位就要分成两路供电电机一路高电压逻辑电路一路降压后的稳定电源。问题四传感器离地高度太高。传感器距离地面每升高 1mm红外反射光的散落范围就大一圈阈值边界变得模糊容易在深色地面纹路上误判成黑线。建议传感器底面离地 0.81.2cm用铜柱固定。太低也不行地面起伏时容易刮蹭传感器。问题五搜索方向反复横跳找不到线。如果小车原地旋转了五六拍还没找到黑线大概率是搜索时间与 PWM 不匹配。搜索时间太长车身每次旋转超过 180°线从车头左侧扫到右侧传感器只在极短的瞬间检测到信号主循环还没来得及响应就错过了。把 SEARCH_INTERVAL_MS 从 150 改到 80100或者把 PWM_SEARCH 从 900 降到 700让每次扫描的角速度小一点寻找成功的概率会明显上升。5. 实测记录不同车速下的寻线表现与参数调整思路5.1 低速、中速、高速三组对比这套代码本质上是一个带丢失找回的开关量控制器速度参数对整体表现影响很大。我用三组速度分别跑过一圈约 6 米的测试赛道赛道包含直线、直角弯和一个 S 弯黑线是标准 2cm 电工胶带。PWM 基准直行速度表现需要改动的地方100050%偏慢稳定每个弯都能流畅找回几乎无震荡无需改动140070%中等直线快速稳定急弯时丢线 12 拍后找回搜索间隔保持 150msPWM_SEARCH 设 900180090%偏快直线表现很好S 弯和直角弯容易冲出去搜索间隔缩短到 100msPWM_SEARCH 提高到 1100这个表格直观地说明了车速越快转向动作需要越激进搜索频率需要越高。但速度也不是越高越好超过 1800 后即使搜索再频繁车身的机械惯性也会让传感器在遮挡区域出现假在线的现象也就是明明线还没进入传感器视场由于车身侧滑瞬时扫描到的是上一帧的残影。对于大多数新手赛赛道我建议先用 1400 作为模板跑顺后再逐步提高。你可以在代码里把声音调速旋钮映射到一个 ADC 通道上做成可调速度这样赛道现场边跑边调不需要反复下载程序。5.2 搜索延时的整定经验搜索延时 150ms 这个值不是凭空拍的。它是基于两个约束算出来的传感器模块的响应时间TCRT5000 数字输出的跳变时间大约在几百微秒到几毫秒这个量级远小于 150ms所以主要瓶颈不在传感器车身旋转角速度和扫描覆盖率原地旋转时车头的扫描角速度约为 300°/s那么 150ms 内车头会扫过约 45°。两个传感器总宽度大约覆盖车头前方 5cm 范围在黑线宽度 2cm 的情况下45° 的扫描步进能保证线出现在扫描扇区内时传感器至少有 1020ms 的窗口可以检测到黑线。如果这个窗口太短主循环的DL_GPIO_readPins会因为相位问题漏掉黑线信号。所以我给出的经验范围是扫描间隔 120200ms 之间具体取多少取决于你的车转向角速度。一个简单的整定方法把车抬起来用手指快速拨动驱动轮模拟丢线状态观察轮子是否按预定的左-右-左顺序切换再落地放在赛道外侧 10cm计时多长时间能找回黑线。超过 1 秒找不到就把搜索 PWM 降一档或者把间隔缩短一点。我最终调的参数是SEARCH_INTERVAL_MS 150PWM_SEARCH 900PWM_TURN 1200PWM_STRAIGHT 1400。这套参数在普通教室地面和 PVC 地胶上都跑得不错如果你换到深色木地板可能需要把传感器灵敏度电位器重新拧一拧代码参数基本不用动。关于这套强制寻黑线代码我个人在实际调车中的体会是它最大的价值不在于有多么高深的控制算法而在于把丢线这个最常见的故障变成了一种主动策略。很多新手把时间花在追求复杂的 PID 参数上结果连最基础的传感器电平都没确认清楚车当然跑不好。这篇是第一版代码后面我还会在这个工程基础上继续分享三路传感器仲裁、编码器测速和 PID 闭环的版本。建议你先把手头的板子按这篇文章跑通再去折腾更高级的控制。如果调试过程遇到问题把你实测的传感器电平、PWM 参数和赛道照片整理一下发在评论区我看到了会回。