简介:面向STM32入门学习者的智能小车综合实验包,聚焦黑线循迹与红外避障两大基础功能,适合课程设计、个人练手及电子竞赛准备。资源以由浅入深、注释详尽为特色,基于定时器配置电机,并封装了前进、刹车、左转、右转、后退及原地旋转等底层运动函数,方便初学者快速理解循迹避障逻辑与电机控制流程。整个压缩包共192个文件,以C源码、H头文件、编译生成的O/CRF目标文件及uvprojx工程文件为主,附带HEX烧录文件,可直接编译验证,适合对照学习定时器PWM输出、GPIO输入检测与传感器反馈的综合应用。压缩包体积仅6.49MB,内容精炼,目前已有1186人浏览学习,可作为STM32智能车入门的实用参考资料。
1. 拿到 ZYSTM32-A1 压缩包后先搞清楚:这其实是两个实验合为一个工程
刚拿到 “ZYSTM32-A1 智能小车黑线循迹&红外避障综合实验.zip” 这个压缩包时,我习惯先不看代码,而是先想明白它到底要我做什么:让小车沿着黑线跑,同时遇到障碍物自动让开。听起来是两件事,但塞进同一个工程后,传感器、电机、定时器全挤在一起,最容易翻车的地方反而不是代码跑不起来,而是你分不清哪一段逻辑在管循迹、哪一段在管避障,最后小车要么绕着黑线画圈,要么对障碍视而不见。这类综合实验包在课程设计和入门比赛里非常常见,适合刚学完 GPIO 和定时器、想上手一个小完整系统的人。这篇文章按“接线 → 初始化 → 功能逻辑 → 排查”的顺序,把涉及的板级资源、传感器电平逻辑和状态机设计一次讲透,让你不仅能跑通,还能按自己的赛道和障碍布局改参数。
2. 拆开实验包的硬件拼图:先接线,再谈代码
2.1 一套标准智能小车的硬件清单:你手里应该有哪些东西
综合实验包之所以叫“综合”,是因为它把循迹和避障两套外设同时挂到了一块 STM32 主控板上。我拿到这类套件后,第一步永远是把模块从车架上拆下来逐一点清楚。一个典型的配置是:主控板采用 STM32F103C8T6 最小系统板,车底装 4 路红外循迹传感器(常见的是 TCRT5000 方案),车头左右各装一个红外避障传感器,驱动部分用 L298N 模块带 4 个直流减速电机,电源通常是 7.4V 锂电池或两节 18650。
这里有个容易忽略的点:4 路循迹和 2 路避障加起来一共 6 个传感器输出,加上 L298N 需要的 6 个控制引脚(IN1~IN4 和 ENA/ENB),一个 F103C8T6 的引脚是够用的,但如果你用的是 2 路循迹传感器,循迹精度会差很多。2 路循迹只能判断“偏左还是偏右”,遇到稍微倾斜的交叉线就会丢失;4 路可以分辨“居中、微偏、大偏、出线”四种状态,循迹曲线会平滑得多。所以看到标题里是黑线循迹,我个人建议至少用 4 路模块。
2.2 传感器接线表:STM32 引脚分配与共地细节
以下是我在类似实验包里最常用的一套引脚分配,和出厂例程的思路基本一致,你可以对照自己板子的丝印微调:
| 功能模块 | 信号线 | 接到 STM32 引脚 | 说明 |
|---|---|---|---|
| 循迹传感器 4 路 | OUT1~OUT4 | PB0 / PB1 / PB2 / PB3 | 数字量输出,检测到黑线电平变化 |
| 避障传感器 左 / 右 | OUT_L / OUT_R | PB4 / PB5 | 数字量输出,检测到障碍电平变化 |
| L298N 方向控制 | IN1 / IN2 / IN3 / IN4 | PA1 / PA2 / PA3 / PA4 | 控制两路电机正反转 |
| L298N 使能调速 | ENA / ENB | PA6 / PA7 | 接 STM32 定时器 PWM 输出 |
| 板载 LED | LED | PC13 | 烧录验证、状态指示 |
接线的核心不是把线插上,而是把共地做好。主控板、传感器模块、L298N 的 GND 必须全部连到一起。很多新手只接了信号线和电源线,电机死活不转,原因就是主控和电机驱动没有共地,PWM 信号没有一个统一的参考电平。
循迹模块的 VCC 一般接 5V,避障模块也是 5V,但两者的输出电平大多是 TTL 5V,而 STM32 的 GPIO 容忍 5V 输入,可以直接接。如果你用的是 3.3V 供电的传感器版本,输出高电平只有 3.3V,那更要保证共地,否则信号判定会不稳定。
2.3 供电方案与电机驱动:L298N 的接线优先级
供电是这套系统里最“玄学”的部分。我常用的做法是:锂电池正负极接 L298N 的 12V 输入端子(7.4V 电池完全没问题),L298N 板载 5V 输出给 STM32 主控板供电,STM32 再用自己的 3.3V 稳压给传感器电路供电。但这里有一个血泪教训:如果 L298N 的 5V 输出同时驱动了舵机或者大功率模块,电压会被拉塌,STM32 会反复重启,表现就是小车跑几秒就“抽搐”一下。
还有一点要注意:L298N 的 ENA/ENB 跳帽。出厂默认两个跳帽都插着,表示电机全速转。做 PWM 调速时必须把跳帽拔掉,否则你给 PA6/PA7 的 PWM 信号根本进不去,电机始终保持最大转速,这时候任何差速逻辑都失效,循迹必然冲出赛道。
避障模块的安装高度也要在接线时一并确认。红外避障传感器的检测距离和安装角度关系很大,一般离地 3~5 厘米,探头略微朝下前方。装得太高,小车会撞上矮障碍物;装得太低,遇到路面的轻微凸起也会误判成障碍。装好后,用一张白纸在传感器前方 10 厘米处晃动,看模块上的指示灯有没有对应变化。
3. 工程结构与初始化代码:编译烧录前先看懂三段代码
3.1 解包后工程文件怎么看:先找 main.c 和硬件初始化文件
解压 ZYSTM32-A1 实验包后,典型的工程里会有 MDK-ARM 工程目录、User 目录(main.c、stm32f10x_it.c 等)、硬件驱动目录(GPIO、定时器、串口的初始化文件)和 Doc 目录。我拿到陌生工程的第一件事不是点编译,而是打开 main.c 看主循环里调了几个任务函数,再看初始化函数里配置了哪几个外设的时钟。这样能快速判断这个工程是“裸机轮询”还是“定时器中断调度”。
这类教学实验包绝大多数是裸机轮询。主循环大概长这样:
int main(void) { SystemInit(); GPIO_Config(); // 配置传感器输入和电机方向引脚 PWM_Config(); // 配置 TIM3 输出两路 PWM UART1_Config(); // 配置串口1,调试用 Delay_Init(); // 初始化 SysTick 延时 while(1) { Tracking_Task(); // 黑线循迹任务 Avoid_Task(); // 红外避障任务 Delay_MS(10); // 10ms 调度周期 } }逻辑说明:这个主循环把两个任务都放在 while(1) 里轮询,Delay_MS(10) 不是为了让系统“慢下来”,而是给传感器电平变化一个稳定的采样窗口,同时给电机一个换向缓冲时间。10ms 是我在类似项目里比较常用的调度周期,太快了避障抖动容易误判,太慢了循迹转向会迟钝。
参数说明:Delay_MS(10) 的数值不是拍脑袋定的。循迹时车速在 30% 占空比左右,10ms 对应小车前进大概 1~2mm,这个距离足够传感器分辨黑线边界,又不会因为采样过密把细微抖动放大。
3.2 GPIO 初始化:方向引脚必须推挽输出,传感器输入不要乱加上拉
看懂了主循环结构,就去看 GPIO 初始化。传感器输入引脚的配置有个细节经常被忽略:循迹传感器模块板载了上拉电阻和比较器,输出是强推挽的数字信号,所以 STM32 这边配成浮空输入就行;但避障模块的输出情况各家不一样,配成上拉输入更保险,防止模块未供电时引脚悬空导致误判。
void GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC, ENABLE); // 循迹传感器输入 PB0-PB3,浮空输入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOB, &GPIO_InitStructure); // 避障传感器输入 PB4-PB5,上拉输入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_Init(GPIOB, &GPIO_InitStructure); // L298N 方向控制引脚 PA1-PA4,推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // 板载 LED PC13,推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOC, &GPIO_InitStructure); }逻辑说明:这段代码把 6 个传感器引脚和 5 个输出引脚一次性配置完成。方向控制引脚必须用推挽输出,因为 L298N 的 IN 脚需要明确的 0V / 3.3V 电平,不能是开漏加上拉的弱驱动。LED 引脚配成推挽是为了烧录后能亮灯验证。
参数说明:这里对循迹和避障输入分别用了浮空和上拉,是经验做法。如果你的避障模块输出端集成了强下拉电阻,上拉输入反而会和模块内部电阻分压,导致电平读不准。遇到这种情况,把 PB4/PB5 改成浮空输入即可。
3.3 定时器 PWM 配置:频率选 1kHz,占空比决定电机转速
电机转速控制靠 TIM3 的 CH1/CH2 输出 PWM 到 L298N 的 ENA/ENB。我一般把 PWM 频率设在 1kHz 左右,这个频率下电机线圈电流连续,噪音小,L298N 的开关损耗也可接受。频率太低(比如 100Hz)电机会发出尖锐啸叫,转速还会一跳一跳;频率太高(比如 50kHz)L298N 的开关损耗变大,模块发热明显。
void PWM_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); // PA6、PA7 复用为 TIM3_CH1、TIM3_CH2 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // 72MHz 主频,预分频 72,计数 1000,得到 1kHz TIM_TimeBaseStructure.TIM_Prescaler = 72 - 1; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_Period = 1000 - 1; TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); // PWM 模式 1,输出极性高,初始占空比 0 TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = 0; TIM_OC1Init(TIM3, &TIM_OCInitStructure); TIM_OC2Init(TIM3, &TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM3, ENABLE); TIM_Cmd(TIM3, ENABLE); }逻辑说明:预分频 72 和计数周期 1000 的组合,把 72MHz 系统时钟分频成 72MHz / 72 / 1000 = 1kHz。TIM_OCInitStructure.TIM_Pulse 是初始占空比比较值,后续通过修改 TIM3->CCR1 和 CCR2 来调速。
参数说明:这里有个容易踩坑的地方——TIM3 的通道引脚默认不一定在 PA6/PA7。部分开发板会把 PWM 引脚重映射到 PB4/PB5,如果你发现 PA6/PA7 没有波形,检查一下工程里是否调用了 GPIO_PinRemapConfig(GPIO_Remap_TIM3, ENABLE)。重映射后定时器通道和普通 IO 复用了,两者只能取其一。
编译烧录时,我用 ST-LINK 下载器接 SWD 接口,在 Keil 的 Flash Download 里选好对应芯片型号。烧录前把工程里 Use MicroLIB 勾上,否则串口重定向 printf 会跑飞。烧进去之后先不急着跑小车,把板子拿在手里转一下轮子,确认两个电机能分别调速,再放到赛道上。
4. 黑线循迹与红外避障的逻辑融合:状态机是主程序的灵魂
4.1 循迹逻辑:电平组合对照表,差速转向比想象中更讲究
循迹的原理很简单:TCRT5000 发射红外光,黑线吸收红外光,白色地面反射红外光,模块上的比较器把反射强度转成高低电平。我用的模块约定“检测到黑线输出低电平,检测到白色地面输出高电平”,你的模块如果相反,后面所有判断都要取反。
4 路传感器从车底左侧到右侧依次接 PB0、PB1、PB2、PB3,把电平读出来拼成一个 4 位值,然后查表决定怎么打方向盘。这里的“方向盘”其实就是左右轮差速:
#define SPEED 600 // 基础速度,占空比 = 600/1000 = 60% #define TURN_DELTA 200 // 差速量 void Set_Motor(int motor_L, int motor_R) { // 正数前进,负数后退,数值就是 CCR 比较值 if (motor_L >= 0) { GPIO_SetBits(GPIOA, GPIO_Pin_1); GPIO_ResetBits(GPIOA, GPIO_Pin_2); TIM3->CCR1 = motor_L; } else { GPIO_ResetBits(GPIOA, GPIO_Pin_1); GPIO_SetBits(GPIOA, GPIO_Pin_2); TIM3->CCR1 = -motor_L; } // 右轮同理,用 PA3/PA4 和 CCR2 if (motor_R >= 0) { GPIO_SetBits(GPIOA, GPIO_Pin_3); GPIO_ResetBits(GPIOA, GPIO_Pin_4); TIM3->CCR2 = motor_R; } else { GPIO_ResetBits(GPIOA, GPIO_Pin_3); GPIO_SetBits(GPIOA, GPIO_Pin_4); TIM3->CCR2 = -motor_R; } } uint8_t Read_Tracking_Sensor(void) { uint8_t val = 0; if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == Bit_RESET) val |= 0x01; // 最左路压黑线 if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == Bit_RESET) val |= 0x02; if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_2) == Bit_RESET) val |= 0x04; if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_3) == Bit_RESET) val |= 0x08; // 最右路压黑线 return val; } void Tracking_Task(void) { uint8_t sensor = Read_Tracking_Sensor(); switch(sensor) { case 0x06: // 0b0110 中间两路压线,直行 Set_Motor(SPEED, SPEED); break; case 0x0E: // 0b1110 线偏右,左轮快右轮慢,向右修正 Set_Motor(SPEED, SPEED - TURN_DELTA); break; case 0x03: // 0b0011 线偏左,右轮快左轮慢,向左修正 Set_Motor(SPEED - TURN_DELTA, SPEED); break; case 0x0F: // 0b1111 十字交叉或全黑,直行 case 0x00: // 0b0000 全白出线,先按直行兜底 Set_Motor(SPEED, SPEED); break; default: // 其他组合按偏向方向补一个修正 if (sensor & 0x08) Set_Motor(SPEED, SPEED - TURN_DELTA); else if (sensor & 0x01) Set_Motor(SPEED - TURN_DELTA, SPEED); else Set_Motor(SPEED - TURN_DELTA / 2, SPEED + TURN_DELTA / 2); break; } }逻辑说明:核心是查表转向。0x06 是完美居中的状态,两个内侧传感器压线;0x0E 说明黑线偏向了小车右侧,此时外侧左轮保持高速、右轮减速,小车向右偏转;0x03 反向操作。default 分支兜底所有中间状态,防止出现“传感器悬在两线之间”时的死区。
参数说明:SPEED 和 TURN_DELTA 是这组逻辑里最需要调的参数。占空比 60% 适合桌面级小车起步,如果赛道有斜坡,SPEED 适当提高到 700~750;TURN_DELTA 是差速量,转弯半径不够就增大到 250~300,但太大容易甩尾,尤其是车上电池靠近尾部时重心偏后,差速过猛会让后轮离地。
4.2 避障逻辑:红外传感器的判定边界与防抖设计
红外避障模块的判定原理同样是反射式红外,但它没有黑白的“线”概念,只判断前方有没有物体。模块上有一个电位器调检测距离,我一般调到 20~30cm 生效,太远会把赛道栏杆也当障碍,太近则来不及转向。
避障传感器的输出电平各家不太统一,我按“检测到障碍输出低电平”来写代码逻辑。左右两个传感器分别接 PB4 和 PB5,读回一个 2 位值。处理上有一个关键点:避障不能像循迹那样“读到障碍立刻转向”,否则小车在障碍边缘犹豫,会左右抖个不停。必须有消抖机制。
#define OBSTACLE_NONE 0 #define OBSTACLE_LEFT 0x01 // 左边检测到障碍 #define OBSTACLE_RIGHT 0x02 // 右边检测到障碍 uint8_t Read_Obstacle_Raw(void) { uint8_t val = 0; if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_4) == Bit_RESET) val |= OBSTACLE_LEFT; if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_5) == Bit_RESET) val |= OBSTACLE_RIGHT; return val; } void Avoid_Task(void) { static uint8_t clear_count = 0; uint8_t obs = Read_Obstacle_Raw(); if (obs == OBSTACLE_NONE) { if (clear_count < 5) clear_count++; if (clear_count >= 5) state = STATE_TRACKING; return; } clear_count = 0; switch(obs) { case OBSTACLE_LEFT: Set_Motor(SPEED + 100, SPEED - 260); // 右转 break; case OBSTACLE_RIGHT: Set_Motor(SPEED - 260, SPEED + 100); // 左转 break; case 0x03: Set_Motor(-SPEED, -SPEED); // 两边都有,后退 200ms Delay_MS(200); Set_Motor(SPEED, -SPEED); // 原地右转 90 度 Delay_MS(260); break; } }逻辑说明:clear_count 是连续“无障碍”的计数,只有连续 5 次(约 50ms)没检测到障碍,才会把状态切回循迹。这个设计避免了小车刚绕过障碍、传感器还没有对准黑线时就被强行拉回的抖动。双向障碍时先后退再转向,防止被夹在障碍和赛道边缘之间。
参数说明:右转差速 100 和 260 不是对称的,这是因为电机左右存在个体差异。我习惯把外侧轮只加一点速、内侧轮大幅减速,这样转弯半径小且不容易甩尾。两个数值后期要按你的小车实际表现校准,我见过有人把内侧减速调到 400,转弯是快了,但每次转向都会把车尾甩到线外。
4.3 状态机融合:为什么避障优先级必须高于循迹
综合实验和单一循迹实验最大的区别,就是两个任务抢电机。我的处理方式是定义一个全局状态枚举,主循环里先读避障,再读循迹,避障一旦触发立刻抢占控制权:
typedef enum { STATE_TRACKING, STATE_AVOID } SystemState; SystemState state = STATE_TRACKING; int main(void) { SystemInit(); GPIO_Config(); PWM_Config(); Delay_Init(); while(1) { if (Read_Obstacle_Raw() != OBSTACLE_NONE) { state = STATE_AVOID; // 避障优先级高于循迹 } if (state == STATE_AVOID) { Avoid_Task(); // 内部会把 state 切回 TRACKING } else { Tracking_Task(); } Delay_MS(10); } }逻辑说明:这个写法的关键是“障碍判定直接使用原始读取值,不走消抖”。为什么不消抖?因为 Avoid_Task 内部有消抖,那是为了“退出避障”的;而“进入避障”必须用原始值,否则传感器已经检测到障碍了,还要等 50ms 消抖确认,以这个车速早就撞上去了。进入要快,退出要稳。
状态机做完之后,我一般会在 PC13 上做指示:循迹状态亮 LED,避障状态灭 LED。手拿小车靠近障碍物,观察 LED 切换是否干脆,能快速判断状态机逻辑有没有写对。这一步比直接放赛道省时间很多。
5. 避坑排查:五个让小车“翻车”的真实原因
5.1 一上电就原地打转:传感器电平逻辑反了
现象:小车放在赛道上,轮子自己左右乱摆,没有规律地原地打转,抬起来看传感器指示灯忽亮忽灭。
原因:最常见的是循迹模块“检测到黑线输出高电平”,代码却按低电平来判断。模块背面丝印和实际逻辑不一致,或者跳帽改变了几路传感器的输出极性,导致 Read_Tracking_Sensor 拼出来的状态全部取反。
解决:把小车放在黑线上,用万用表量 PB0 引脚,黑线时是高还是低,白线时是高还是低,确认后把 Read_Tracking_Sensor 里的 Bit_RESET 判断改成对应的电平。这比改代码去猜要快得多。
5.2 电机不转或只有一边转:L298N 没共地,ENA 跳帽没拔
现象:烧录成功、指示灯正常,但四个轮子纹丝不动,或者只有一侧轮子转。
原因:两个原因叠加最常见。第一是主控板 GND 和 L298N 的 GND 没接一起,PWM 信号没有参考地;第二是 ENA/ENB 跳帽没拔,PWM 信号进不了使能端。
解决:先量 L298N 逻辑电源指示灯有没有亮,再量电机电源两端有没有电池电压,最后拿一根杜邦线把两块板的 GND 短接。确认共地后拔掉两个跳帽,用万用表测 PA6 引脚,烧录时应该能看到约 0~3.3V 的方波。我调试时习惯先把 Speed 设成固定值 500,电机能转再调逻辑。
5.3 循迹在强光下失效:环境光干扰和阈值漂移
现象:晚上在客厅跑得好好的小车,白天拉到窗边就冲出赛道,传感器指示灯在黑白交界处闪烁不定。
原因:TCRT5000 的红外反射信号会被太阳光和室内灯光干扰,模块比较器的阈值是固定的,环境光强度变化后,黑白反射量的分界线跟着漂移。
解决:把小车放在赛道黑白交界处,用小螺丝刀慢慢旋转模块上的电位器,直到 LED 刚好翻转。注意要模拟实际使用环境的光照来调。我习惯调完后把电位器用热熔胶封一下,防止震动跑位。实在不行,给传感器加一个遮光罩,这是最直接的办法。
5.4 遇到障碍不转向:避障传感器误触和安装高度翻车
现象:小车直直地撞向障碍物,或者离障碍还有半米就开始绕路。
原因:检测距离调得太远,把赛道围栏当成障碍;或者传感器安装角度朝上,白色墙面反射强,被当成地面,检测不到障碍。
解决:先用白纸在传感器前方反复移动,找到一个 20cm 左右的距离让指示灯稳定翻转。然后检查安装高度,让探头略微朝下,对准障碍物中下部。浅色障碍物反射强,检测距离会比深色远,所以实际距离阈值要按最浅颜色的障碍来标定。
5.5 电池电压下降后,小车过弯冲线
现象:满电时循迹完美,跑了十分钟后突然开始冲出赛道,转弯明显迟钝。
原因:直流减速电机的转速和供电电压直接相关。满电 8.4V,占空比 60% 对应一个高转速;电压掉到 7.0V 时,同样的占空比转速下降,转向差速的实际效果变弱,循迹逻辑里的固定 TURN_DELTA 就失效了。
解决:用稳定电源给系统供电来调参,不要用快没电的电池调。如果必须用电池,把 SPEED 和 TURN_DELTA 的数值设成电压在 7.4V 时仍然够用的值,或者增加一个电压检测,低于某阈值后整体提高占空比补偿转速下降。这不是代码玄学,而是直流电机的物理特性,提前理解了,后面调参会有底气得多。
6. 从“能跑”到“跑得稳”:循迹转弯的三个进阶技巧
把状态机跑通只是第一步,很多参赛选手卡在“小车能走但走不快”这个阶段。我分享三个亲测有效的技巧,它们不需要改硬件,只改逻辑。
第一个是速度斜坡。启动瞬间给 60% 占空比会让电机电流猛增,电池电压被瞬间拉低,更容易触发复位。我习惯把 SPEED 从 300 起步,每 10ms 增加 30,直到目标速度。同样,转向结束后也做减速斜坡,避免循迹修正过头造成来回摆动。
第二个是比例转向。前面的查表转向只有“直行、小转、大转”三档,其实可以更平滑:根据黑线偏离中心的程度计算差速量。偏离越大,TURN_DELTA 越大,而不是固定 200。
void Tracking_PID_Task(void) { uint8_t sensor = Read_Tracking_Sensor(); int l = SPEED, r = SPEED; int err = 0; // 简单比例:中间两路对应0权重,越靠边权重越大 if (sensor & 0x01) err += 3; if (sensor & 0x02) err += 1; if (sensor & 0x04) err -= 1; if (sensor & 0x08) err -= 3; l = SPEED - err * 80; // err 为负说明偏右,左轮提速 r = SPEED + err * 80; if (l < 0) l = 0; if (r < 0) r = 0; if (l > 900) l = 900; if (r > 900) r = 900; Set_Motor(l, r); }逻辑说明:err 就是“黑线相对小车中心的偏差”,把 PB0 到 PB3 的权重设为 3、1、-1、-3,偏差值乘以 80 就是差速量。这个写法比查表平滑,特别适合弯道半径变化大的赛道。80 这个系数我一般从 50 开始试,弯道转不过去就加,直道发飘就减。
第三个技巧是传感器滤波。传感器经过黑线边缘时,电平会在几毫秒内抖动多次。我在读 GPIO 时做连续三次采样,两次一致才算有效。用位移寄存器的方式不需要延时,代码量也小。
uint8_t Read_Tracking_Filtered(void) { uint8_t val = 0; uint8_t s1 = Read_Tracking_Sensor(); uint8_t s2 = Read_Tracking_Sensor(); uint8_t s3 = Read_Tracking_Sensor(); // 两两与/或,滤掉单次抖动 val = (s1 & s2) | (s2 & s3) | (s1 & s3); return val; }逻辑说明:连续读三次,取任意两次相同的电平结果。这个方法在循迹和避障都适用,代价是每次任务多花几十微秒,对 10ms 调度周期来说完全可忽略。
这三个技巧做完,小车在直线段可以跑得又快又稳,弯道也不会再频繁冲出黑线。我自己调试这类综合实验时,最后一步永远是把电池充满再跑三圈,记录每圈耗时和冲出赛道次数,而不是只看“有没有跑通一次”。养成这个习惯之后,你会发现自己调任何小车控制逻辑都有了方向感。希望帮到你。
本文还有配套的精品资源,点击获取