STM32循迹小车项目实战:从硬件选型到PID算法全解析
2026/9/8 2:47:40 网站建设 项目流程

1. 从零到一:为什么你的第一个STM32项目应该是循迹小车?

如果你刚拿到一块STM32开发板,面对一堆外设和库函数感到无从下手,或者已经点了几遍LED灯、调通了串口,想找个综合性的项目把知识串起来,那循迹小车绝对是个完美的选择。这玩意儿听起来像玩具,但它几乎涵盖了嵌入式开发入门阶段你需要掌握的所有核心技能:GPIO控制、定时器PWM、外部中断、ADC采样,以及最关键的——如何将传感器数据转化为控制逻辑。更重要的是,它有一个明确、直观的反馈:小车能不能沿着黑线跑。成功与否,一目了然,这种即时正反馈对学习动力的提升是巨大的。

我见过太多初学者在裸机编程和RTOS之间反复横跳,或者沉迷于研究各种复杂的通信协议,结果越学越迷茫,最后板子吃灰。循迹小车这个项目,能帮你建立一个扎实的“手感”。你会真切地体会到,编程不是空中楼阁,每一行代码都对应着电机转速的变化和传感器的读数。当你调好PID参数,看着小车稳稳地压着黑线飞驰时,那种成就感是单纯点亮一个LED无法比拟的。接下来,我就以一个老电子爱好者的身份,带你完整地走一遍从元器件选型、电路搭建、代码编写到调试优化的全流程,避开那些我当年踩过的坑。

2. 硬件清单与核心模块选型:别在第一步就踩坑

做硬件项目,物料准备是基础。市面上循迹小车套件很多,但自己一件件采购、焊接,理解会更深刻。以下是核心清单和选型要点。

2.1 主控与电机驱动:动力与控制核心

1. STM32主控芯片对于循迹小车,STM32F103C8T6(俗称“蓝桥杯”或“最小系统板”)是性价比之王。它拥有足够的GPIO、定时器和ADC资源,价格亲民,社区资料海量。完全没必要一上来就追求F4、H7等高端系列,F103的性能对于循迹绰绰有余。

注意:购买开发板时,确认其引出了所有IO口,并且板载了ST-Link或留有SWD调试接口。自带Type-C供电和串口转换芯片的板子会方便很多。

2. 电机与驱动模块

  • 电机:常用的是TT减速电机(直流减速电机)。参数关注工作电压(常用3-6V)和减速比。减速比越大,扭矩越大,速度越慢,对于循迹小车,1:48或1:120的都比较合适。
  • 驱动芯片:最经典的是L298N或TB6612FNG。
    • L298N:老牌劲旅,驱动能力强,但发热量大,需要散热片,逻辑电压和驱动电压需分开供电,电路稍复杂。
    • TB6612FNG:更推荐。效率高、发热小,内置保护电路,支持PWM频率更高,逻辑电压与驱动电压可以相同,接线更简洁。它的一片芯片可以驱动两个电机。

选型建议:直接选择TB6612FNG电机驱动模块。它会让你的电源设计更简单,调试体验更好。

2.2 循迹传感器:小车的“眼睛”

这是循迹的核心。常见的有两种:

  • TCRT5000红外反射传感器模块:最常用、最经济。它由一个红外发射管和一个接收管组成。发射管发出红外光,照射到不同颜色的地面(如白色反射强,黑色吸收强),接收管接收到的反射光强度不同,从而输出不同的电平(通常白线输出低电平,黑线输出高电平,具体看模块设计)。模块上通常有一个电位器,可以调节灵敏度。
  • 灰度传感器:本质上也是红外反射原理,但有些型号输出的是模拟量(电压值),而非数字量(高低电平)。模拟量可以更精细地区分不同灰度,但需要主控的ADC来读取,增加了复杂度。

对于入门,强烈推荐使用4个或5个TCRT5000数字传感器模块一字排开。数字量直接用GPIO读取即可,简单可靠。5个传感器的布局可以提供更丰富的路径信息(如轻微偏左、严重偏右、十字路口等)。

2.3 电源系统:稳定大于一切

电源混乱是项目失败的一大元凶。你需要一个两路供电方案:

  1. 电机驱动供电:直接使用一块7.4V 2S锂电池。这是动力电源,接在TB6612FNG的VM引脚上。锂电池电量足、放电电流大,能保证电机瞬间提速时有劲。
  2. 控制系统供电:从锂电池引出电压,通过一个降压模块(如LM2596)降到5V。这个5V给STM32开发板、传感器模块和TB6612FNG的逻辑部分(VCC引脚)供电。

核心避坑点:绝对不要试图用USB线或者一堆碱性电池同时给电机和控制系统供电。电机启停的瞬间会产生巨大的电流波动和电压跌落,足以导致STM32复位或传感器误触发。物理上隔离动力电源和控制电源,是保证系统稳定的基石。

3. 电路连接与机械结构搭建:把想法变成实物

有了所有零件,就像有了乐高积木,现在需要把它们正确拼装起来。

3.1 电气连接图与接线表

下图清晰地展示了各模块间的连接关系:

flowchart TD subgraph P [电源部分] A[7.4V锂电池] --> B[DC降压模块 LM2596] B --> C[输出稳定5V] end subgraph Ctl [控制核心] D[STM32F103C8T6] end subgraph Sen [传感器阵列] E[TCRT5000 x5] end subgraph Drv [电机驱动] F[TB6612FNG模块] end subgraph Mot [执行机构] G[左轮电机] H[右轮电机] end C --> D C --> E C --> F[VCC逻辑供电] A --> F[VM电机供电] E -- GPIO输入 --> D D -- PWM & 方向控制GPIO --> F F --> G F --> H

根据图示,具体接线可参考下表:

连接线起点终点说明
电源正极(红)锂电池+TB6612 VM引脚、降压模块IN+动力总输入
电源负极(黑)锂电池-TB6612 GND、降压模块IN-、STM32 GND共地!
5V输出(红)降压模块OUT+STM32 5V/VIN、TB6612 VCC、传感器VCC控制逻辑供电
5V地(黑)降压模块OUT-STM32 GND、TB6612 GND、传感器GND控制逻辑共地
传感器信号线传感器1 DOSTM32 PA0根据你的布局定义引脚
传感器2 DOSTM32 PA1
传感器3 DOSTM32 PA2中间传感器
传感器4 DOSTM32 PA3
传感器5 DOSTM32 PA4
电机控制线STM32 PB6 (PWM)TB6612 PWMA左电机PWM
STM32 PB7 (DIR)TB6612 AIN1左电机方向
STM32 PB8 (DIR)TB6612 AIN2左电机方向
STM32 PB9 (PWM)TB6612 PWMB右电机PWM
STM32 PB10 (DIR)TB6612 BIN1右电机方向
STM32 PB11 (DIR)TB6612 BIN2右电机方向
电机动力线TB6612 A01/A02左电机两极极性决定正反转
TB6612 B01/B02右电机两极

接线关键提示

  1. 共地!共地!共地!锂电池的GND、降压模块的GND、STM32的GND、驱动模块的GND、传感器的GND,必须全部连接在一起。这是电路正常工作的前提。
  2. 电机线先不接:在调试软件前,可以先不接电机线,避免程序错误导致电机乱转损坏设备。
  3. 传感器布局:将5个传感器模块紧密地一字排开,固定在小车底盘最前端。传感器间距应略小于黑线宽度,以确保任何时候至少有1个传感器能检测到黑线。

3.2 机械结构与装配心得

车架可以选择亚克力板、铝合金或者直接购买现成的智能小车底盘。装配时注意:

  • 重心要低:电池等重物尽量放在底盘下方,降低重心,小车转弯时更稳,不易侧翻。
  • 传感器高度可调:不要将传感器死死固定。最好能用铜柱、螺丝等让它的高度可调。传感器离地面太高,检测不到;太低,容易刮擦地面。最佳距离通常是1-2厘米,需要通过实验确定。
  • 线材整理:用扎带或胶带将电线捆扎好,避免缠绕进车轮或齿轮。混乱的布线是后期调试的噩梦。

4. 软件开发环境与工程框架搭建

硬件准备就绪,我们进入软件世界。这里以Keil MDK(Keil5)和标准外设库为例。

4.1 创建工程与库文件管理

  1. 安装Keil MDK及STM32芯片包:确保你的Keil5安装了Keil.STM32F1xx_DFP这样的设备支持包。
  2. 新建工程:选择芯片型号STM32F103C8
  3. 管理工程文件夹:不要把所有文件都堆在根目录。建议建立如下结构的文件夹:
    Project/ ├── USER/ // 存放main.c, stm32f10x_it.c(中断文件), 等用户文件 ├── CORE/ // 存放CMSIS核心文件(如core_cm3.c, startup_stm32f10x_md.s) ├── FWLIB/ // 存放STM32标准外设库的所有.c和.h文件 ├── HARDWARE/ // 自己编写的模块驱动代码(如motor.c, sensor.c) └── SYSTEM/ // 一些公用代码(如sys.c, usart.c, delay.c)
  4. 添加文件与头文件路径:在Keil的工程管理窗口中,将这些文件夹下的.c文件添加到对应的组里。在Options for Target -> C/C++ -> Include Paths中,添加所有这些文件夹的路径。

4.2 基础驱动编写:GPIO、定时器与PWM

HARDWARE文件夹下,我们创建两个基础驱动文件。

sensor.c- 红外传感器读取

#include "sensor.h" // 假设5个传感器接在PA0~PA4 #define SENSOR1_PIN GPIO_Pin_0 #define SENSOR2_PIN GPIO_Pin_1 #define SENSOR3_PIN GPIO_Pin_2 #define SENSOR4_PIN GPIO_Pin_3 #define SENSOR5_PIN GPIO_Pin_4 #define SENSOR_PORT GPIOA void Sensor_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin = SENSOR1_PIN | SENSOR2_PIN | SENSOR3_PIN | SENSOR4_PIN | SENSOR5_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入模式 GPIO_Init(SENSOR_PORT, &GPIO_InitStructure); } // 读取传感器状态,返回一个5位的值,每一位代表一个传感器(1:检测到黑线,0:未检测到) u8 Sensor_Read(void) { u8 sensor_value = 0; if(GPIO_ReadInputDataBit(SENSOR_PORT, SENSOR1_PIN) == Bit_SET) sensor_value |= 0x01; if(GPIO_ReadInputDataBit(SENSOR_PORT, SENSOR2_PIN) == Bit_SET) sensor_value |= 0x02; if(GPIO_ReadInputDataBit(SENSOR_PORT, SENSOR3_PIN) == Bit_SET) sensor_value |= 0x04; if(GPIO_ReadInputDataBit(SENSOR_PORT, SENSOR4_PIN) == Bit_SET) sensor_value |= 0x08; if(GPIO_ReadInputDataBit(SENSOR_PORT, SENSOR5_PIN) == Bit_SET) sensor_value |= 0x10; return sensor_value; }

这里使用上拉输入模式,当传感器未检测到黑线(白底)时,模块输出低电平,被上拉电阻拉高,读回Bit_RESET;检测到黑线时,模块输出高电平,读回Bit_SET。这样Sensor_Read()返回值的位为1就代表对应传感器在黑线上。

motor.c- TB6612电机PWM驱动

#include "motor.h" #include "stm32f10x_tim.h" // 引脚定义:左电机PWM-PB6, 方向-PB7/PB8;右电机PWM-PB9, 方向-PB10/PB11 void Motor_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; // 初始化方向控制引脚为推挽输出 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7 | GPIO_Pin_8 | GPIO_Pin_10 | GPIO_Pin_11; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); // 初始化PWM引脚为复用推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出 GPIO_Init(GPIOB, &GPIO_InitStructure); } void Motor_PWM_Init(u16 arr, u16 psc) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; // 开启TIM4时钟(PB6/PB7/PB8/PB9是TIM4的通道1/2) RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE); // 定时器基础设置 TIM_TimeBaseStructure.TIM_Period = arr; // 自动重装载值 TIM_TimeBaseStructure.TIM_Prescaler = psc; // 预分频系数 TIM_TimeBaseStructure.TIM_ClockDivision = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStructure); // PWM模式设置 TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; // 通道1 (PB6 - 左电机) TIM_OC1Init(TIM4, &TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM4, TIM_OCPreload_Enable); // 通道4 (PB9 - 右电机) 注意:PB9是TIM4的通道4 TIM_OC4Init(TIM4, &TIM_OCInitStructure); TIM_OC4PreloadConfig(TIM4, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM4, ENABLE); TIM_Cmd(TIM4, ENABLE); } // 设置左电机速度和方向 void Motor_Left_Set(int speed) { if(speed >= 0) { // 正转 GPIO_SetBits(GPIOB, GPIO_Pin_7); GPIO_ResetBits(GPIOB, GPIO_Pin_8); TIM_SetCompare1(TIM4, speed); // 设置占空比 } else { // 反转 GPIO_ResetBits(GPIOB, GPIO_Pin_7); GPIO_SetBits(GPIOB, GPIO_Pin_8); TIM_SetCompare1(TIM4, -speed); } } // 设置右电机速度和方向 void Motor_Right_Set(int speed) { if(speed >= 0) { GPIO_SetBits(GPIOB, GPIO_Pin_10); GPIO_ResetBits(GPIOB, GPIO_Pin_11); TIM_SetCompare4(TIM4, speed); } else { GPIO_ResetBits(GPIOB, GPIO_Pin_10); GPIO_SetBits(GPIOB, GPIO_Pin_11); TIM_SetCompare4(TIM4, -speed); } }

Motor_PWM_Init函数中,arrpsc决定了PWM的频率。PWM频率 = 定时器时钟 / ( (arr+1) * (psc+1) )。对于直流电机,通常选择5kHz到20kHz的频率。频率太低电机会啸叫,太高则驱动芯片可能响应不了。假设系统时钟72MHz,设置arr=7199psc=0,则PWM频率=72MHz / (7200 * 1) = 10kHz,这是一个常用值。

5. 循迹核心算法:从简单逻辑到比例控制

有了传感器数据和电机控制能力,现在需要编写“大脑”——循迹算法。

5.1 基础状态机与开关量控制

最直观的方法是根据5个传感器的状态,定义一个“位置误差”。

// 定义传感器位置权重,中间为0,两边对称 const int sensor_weight[5] = {-2, -1, 0, 1, 2}; int Calculate_Error(u8 sensor_val) { int error = 0; int active_sensor_count = 0; for(int i=0; i<5; i++) { if(sensor_val & (1<<i)) { // 如果第i个传感器检测到黑线 error += sensor_weight[i]; active_sensor_count++; } } if(active_sensor_count == 0) { // 所有传感器都没检测到黑线,可能是脱线了 return 100; // 返回一个很大的误差,触发特殊处理 } // 返回平均误差,使误差值归一化 return error / active_sensor_count; }

这个Calculate_Error函数会根据哪几个传感器压线,计算出一个误差值。例如,只有最左边的传感器压线(二进制00001),误差为-2;只有中间传感器压线(00100),误差为0;右边两个传感器压线(11000),误差为(1+2)/2=1.5取整后为1

有了误差,最简单的控制就是“bang-bang”控制(开关控制):

void Track_BangBang(int error) { int base_speed = 3000; // 基础速度,对应PWM比较寄存器的值 int turn_speed = 1000; // 转弯时的速度差 if(error == 100) { // 脱线处理:停车或原地旋转找线 Motor_Left_Set(0); Motor_Right_Set(0); return; } if(error < 0) { // 偏左,右转:左轮加速,右轮减速 Motor_Left_Set(base_speed + turn_speed); Motor_Right_Set(base_speed - turn_speed); } else if(error > 0) { // 偏右,左转:左轮减速,右轮加速 Motor_Left_Set(base_speed - turn_speed); Motor_Right_Set(base_speed + turn_speed); } else { // 居中,直行 Motor_Left_Set(base_speed); Motor_Right_Set(base_speed); } }

这种方法实现简单,但小车运行起来会“画龙”,在黑白线边缘来回摆动,不流畅。

5.2 引入比例(P)控制实现平滑循迹

要让小车跑得稳,必须引入比例控制。核心思想是:控制量(左右轮速度差)与误差(偏离中心的程度)成正比

void Track_P_Control(int error) { float Kp = 15.0; // 比例系数,需要调试 int base_speed = 3000; int turn_adjustment; // 转弯调整量 if(error == 100) { // 脱线处理策略可以更智能,比如记忆最后一次误差方向,向那个方向旋转 Motor_Left_Set(-1000); Motor_Right_Set(1000); delay_ms(100); // 旋转一下 return; } // 核心公式:调整量 = 误差 * 比例系数 turn_adjustment = (int)(Kp * error); // 限制调整量,防止过大 if(turn_adjustment > 2000) turn_adjustment = 2000; if(turn_adjustment < -2000) turn_adjustment = -2000; // 应用调整:左轮速度 = 基础速度 - 调整量;右轮速度 = 基础速度 + 调整量 // 当error为负(偏左),turn_adjustment为负,则左轮速度增加,右轮速度减少,实现右转 Motor_Left_Set(base_speed - turn_adjustment); Motor_Right_Set(base_speed + turn_adjustment); }

比例系数Kp的调试技巧

  1. 先将Kp设为0,让小车直行,观察它偏离黑线时,误差error的变化范围(比如在-2到2之间)。
  2. 从小值开始(如Kp=5),逐渐增大。Kp太小,小车反应迟钝,拐弯时容易冲出去;Kp太大,小车会剧烈振荡,在直道上也左右摇摆。
  3. 理想的Kp是能让小车在直道上轻微、高频地微调,在弯道上能平滑地拐过去,没有明显的超调和振荡。

5.3 进阶:PID控制与误差处理优化

单纯的P控制可能存在静差(在弯道上无法完全回到中心线)或过冲。可以引入积分(I)和微分(D)项,构成完整的PID控制器。但对于很多场地,一个精心调校的P控制器或者PD控制器已经足够优秀。

更健壮的误差处理: 上面的简单平均法在传感器部分被遮挡或个别传感器损坏时不稳定。可以采用“加权重心法”:

int Calculate_Error_Robust(u8 sensor_val) { long weighted_sum = 0; int sensor_count = 0; // 传感器位置坐标,假设间距为10个单位 const int sensor_pos[5] = {-20, -10, 0, 10, 20}; for(int i=0; i<5; i++) { if(sensor_val & (1<<i)) { weighted_sum += sensor_pos[i]; sensor_count++; } } if(sensor_count == 0) { return 1000; // 脱线标志 } // 返回黑线位置的“重心” return (int)(weighted_sum / sensor_count); }

这种方法计算出的误差是连续的坐标值,更精细。对应的Kp值也需要重新调整。

6. 系统集成、调试与性能优化

将各个模块组合起来,并在main.c中构建主循环。

6.1 主程序框架与状态管理

#include "stm32f10x.h" #include "sensor.h" #include "motor.h" #include "delay.h" #include "usart.h" // 用于调试打印 int main(void) { u8 sensor_state; int line_error; delay_init(); // 初始化延时函数 USART1_Init(115200); // 初始化串口,用于调试 Sensor_Init(); Motor_GPIO_Init(); Motor_PWM_Init(7199, 0); // 10kHz PWM printf("STM32 Tracking Car Start!\r\n"); while(1) { sensor_state = Sensor_Read(); line_error = Calculate_Error_Robust(sensor_state); // 打印调试信息(调试完成后可注释掉) printf("Sensor: 0x%02X, Error: %d\r\n", sensor_state, line_error); Track_P_Control(line_error); delay_ms(10); // 控制周期,10ms一次,即100Hz } }

6.2 调试流程与常见问题排查

调试务必分步进行,不要一上来就期望小车完美运行。

第一步:传感器测试

  1. 将小车抬起,用手在传感器下方移动黑白纸片。
  2. 通过串口打印Sensor_Read()的返回值。确认每个传感器都能正确输出0和1,并且灵敏度合适(电位器调节到在白纸上稳定输出0,在黑胶带上稳定输出1)。
  3. 常见问题:传感器输出不稳定,在白纸上也偶尔跳变。这通常是环境光干扰或传感器离地面太远/太近。可以尝试用热熔胶或遮光材料在传感器周围做一个“遮光罩”。

第二步:电机测试

  1. 将小车架空,轮子悬空。
  2. 编写一个测试函数,让两个电机分别正转、反转、停止。观察电机转向是否正确。特别注意:如果电机转向与预期相反,只需交换接到驱动模块A01/A02或B01/B02的两根线即可。
  3. 测试PWM调速是否平滑,从低速到高速。

第三步:开环调试(不循迹,只走固定路径)

  1. 编写一段程序,让小车直行3秒,左转1秒,右转1秒。
  2. 将小车放在地上,观察其行走路径是否笔直。如果不直,说明两个电机的机械性能或驱动有细微差异。可以通过在代码中给两个电机设置一个微小的“补偿值”来修正。
// 在设置速度时加入补偿 #define LEFT_COMPENSATE -50 // 左电机比右电机快,就给它减点速 Motor_Left_Set(target_speed + LEFT_COMPENSATE);

第四步:闭环调试(正式循迹)

  1. 先让小车以很低的基础速度(如base_speed=1000)运行,Kp设一个较小值(如5)。
  2. 观察小车在直道和弯道的行为。如果拐弯时冲出去,缓慢增大Kp;如果直道上左右摆动厉害,则减小Kp。
  3. 调整base_speed。速度越快,对控制器的响应要求越高,需要更大的Kp或更快的控制周期。

6.3 性能优化与扩展思路

当基础循迹稳定后,可以尝试以下优化:

  • 控制周期:尝试将delay_ms(10)改为更短的时间(如5ms),可以提高系统响应速度。注意传感器读取和电机控制函数本身耗时很短,主要瓶颈在delay_ms
  • 动态速度:在直道上加速,进入弯道前减速。可以通过预测来实现:如果连续多次误差都很小(在直道上),则缓慢提高base_speed;如果检测到误差突然变大(可能遇到急弯),则提前减速。
  • 应对复杂路径
    • 十字路口:当检测到多个传感器(如中间三个)同时长时间压线,可以判断为十字路口,执行预设的直行或转弯逻辑。
    • 断续线:如果黑线是断续的,需要在脱线状态(error==1000)时,让小车按照脱线前的最后一个有效误差方向继续前进一小段距离,并加大找线的旋转幅度。
  • 使用中断:可以将传感器检测放到外部中断中,或者使用定时器中断来严格定时执行控制算法,使系统时序更加精确。

循迹小车作为一个入门项目,其内涵可以非常深。从最基础的GPIO操作到闭环控制算法,从简单的状态机到可以应对复杂场面的决策逻辑,每一步的深入都能带来新的收获。当你调通第一版,看着它笨拙但坚定地跟着黑线前进时,恭喜你,你已经跨过了嵌入式开发实践的第一道重要门槛。接下来,你可以尝试给它加上蓝牙遥控、超声波避障、摄像头视觉,甚至移植到RTOS上,让它在更复杂的场景中游刃有余。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询