这次我们来看一个非常适合电赛和嵌入式入门实战的开源项目:STM32 智能小车资料包。
项目核心不复杂,但覆盖面很完整:PID 循迹、超声波避障、距离跟随、蓝牙遥控、HCSR04 测距、TB6612 电机驱动、SG90 舵机控制全部集成在同一套小车平台上。如果你正在备赛、上单片机课程,或者想把手里的 STM32 开发板变成一辆能跑、能躲、能跟、能遥控的小车,这套资料可以直接作为起点。
先说最值得关注的几个点:整份资料以开源形式发布,包含工程源码、接线说明和功能测试思路;控制逻辑不是硬编码“死走”,而是用 PID 做速度和方向闭环,后期调参数是重点;硬件选型都是淘宝能直接买到的常用模块,成本可控;功能之间通过状态机切换,循迹、避障、跟随、蓝牙可以共存在一套主程序里,不需要改接线就能切换模式。
本文不会只贴一个“下载链接”就结束。我会把这套小车的功能拆开讲清楚:每个模块解决什么问题、接线怎么排、驱动层怎么写、核心逻辑怎么实现、调 PID 时看哪些数据、遇到车不走直或者超声波读数异常时怎么排查。文章末尾会给出电赛场景下的工程化建议。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 项目类型 | STM32 智能小车嵌入式实战项目 |
| 控制芯片 | STM32F103 系列为主,具体型号以资料标注为准 |
| 主要功能 | PID 循迹、超声波避障、距离跟随、蓝牙控制、HCSR04 测距、SG90 舵机控制 |
| 电机驱动 | TB6612,双路 H 桥,支持 PWM 调速 |
| 测距传感器 | HCSR04 超声波模块 |
| 舵机 | SG90,用于超声波探头扫描方向 |
| 循迹传感器 | 常见三路/五路红外循迹模块,五路在复杂赛道更稳 |
| 通信方式 | 串口/蓝牙模块,常见 HC-05/HC-06 方案 |
| 开发环境 | Keil MDK + STM32 标准外设库/固件库,配合 ST-Link 下载调试 |
| 上位机调参 | 支持串口打印配合 Vofa 等工具观察 PID 波形 |
| 适合场景 | 电赛备赛、STM32 学习、智能车入门、毕业设计 |
需要说明一点:这套资料的源码是基于轮式差速小车写的。底盘是两个电机加一个万向轮的结构,转向靠左右轮转速差实现,这也是多数电赛智能小车和入门循迹小车最通用的结构。
2. 适用场景与使用边界
这个项目适合三类人:
第一类是电赛备赛队伍。智能小车类是电赛常见题型方向,资料里踩过的坑、写好的驱动、PID 调参思路能省很多前期时间,你只需要根据自己的赛道和传感器布局去改参数。
第二类是正在学 STM32 的在校学生。HCSR04、TB6612、SG90、蓝牙这些模块是嵌入式基础外设的集合作业,把每个模块驱动读懂,比自己从头啃芯片手册轻松很多。
第三类是做课程设计或毕业设计的同学。题目往往是“基于 STM32 的智能循迹避障小车”,这套资料基本可以覆盖题目的功能要求,再补一些显示、调速、状态指示就能成稿。
使用边界也很明确:这套资料不是拿来就能用的商业成品。它需要你自己去熔焊接线、烧录程序、根据实际赛道调 PID 参数。不同底盘、轮胎摩擦系数、电机减速比会直接影响循迹效果,换一块板子或换一套轮子之后,原来的 PID 参数大概率要做调整。
还有一条合规和安全边界必须强调:智能小车在实验和比赛场景下使用没有问题,但改装遥控模型、在公共道路上测试或让他人不明所以地被小车跟随,都不合适。调试时注意防止电机堵转过热,锂电池充电和放电要按规范操作,飞线多的地方先断电再改接线,避免短路。
3. 硬件平台与环境准备
3.1 硬件清单
一套完整可跑的最小系统至少需要这些:
| 模块 | 型号/规格 | 用途 |
|---|---|---|
| 主控板 | STM32F103C8T6 / F103RCT6 最小系统板 | 逻辑控制 |
| 电机驱动 | TB6612FNG | 驱动左右两路直流电机 |
| 直流电机 | 带减速箱的 TT 电机或类似 6V 电机 | 提供动力 |
| 超声波模块 | HCSR04 | 测距、避障、跟随 |
| 舵机 | SG90 180 度 | 控制超声波探头朝向 |
| 循迹模块 | 三路或五路红外循迹传感器 | 检测黑线 |
| 蓝牙模块 | HC-05/HC-06 | 手机遥控 |
| 降压模块 | 如 MP1584 或 AMS1117 组合 | 给主控和传感器供稳定电压 |
| 电池 | 2S 锂电池 7.4V 或 4 节 AA 电池盒 | 整车供电 |
| 下载器 | ST-Link V2 | 烧录程序 |
资料中的电源方案建议把电机电源和逻辑电源分开处理。TB6612 的 VM 接电池电压,VCC 接 3.3V 或 5V,逻辑部分和电机部分共地,这样电机启动瞬间的压降不会把一个单片机直接拉复位。
3.2 软件环境
开发环境推荐用 Keil MDK 5,配合 ST-Link 驱动使用。工程建议基于 STM32 标准外设库建立,也可以直接使用 STM32CubeMX 生成 HAL 库工程再移植驱动代码。无论用哪种方式,都需要确认三件事:
- Keil 里安装了对应芯片型号的 Device Pack;
- ST-Link 驱动已经装好,下载器能识别目标板;
- 工程里已经正确添加标准外设库或 HAL 库文件。
常见问题是新装 Keil 之后没有安装 STM32F1 系列的 Pack,编译时报 "cannot open source file" 或者烧录时无法识别芯片。解决方法是到 Keil 的 Pack Installer 里选择 STM32F1xx 系列安装,或者按“STM32 芯片包官网下载流程”手动下载离线包再导入。
3.3 工程结构建议
建议把代码按模块拆分,不要所有逻辑堆在一个main.c里。推荐结构如下:
Project/ |-- USER/ // main.c, stm32f10x_it.c |-- HARDWARE/ | |-- motor.c/h // TB6612 电机驱动 | |-- ultrasonic.c/h // HCSR04 测距 | |-- servo.c/h // SG90 舵机控制 | |-- tracking.c/h // 循迹传感器读取 | |-- bluetooth.c/h // 蓝牙串口命令解析 |-- SYSTEM/ // 时钟、延时等基础文件 |-- PID/ | |-- pid.c/h // PID 控制器 |-- CORE/ |-- OBJ/这样的好处是调试时只改一个模块,不用全局找代码;换传感器型号时也只替换对应驱动文件。
4. 核心模块接线参考
接线是新手最容易翻车的环节。下面给出一套常见接线参考,具体引脚以你手里的资料和开发板丝印为准。
4.1 TB6612 电机驱动接线
| TB6612 引脚 | 接 STM32 |
|---|---|
| PWMA | TIM 通道输出引脚,控制左电机速度 |
| AIN1 / AIN2 | 普通 GPIO,控制左电机方向 |
| PWMB | TIM 通道输出引脚,控制右电机速度 |
| BIN1 / BIN2 | 普通 GPIO,控制右电机方向 |
| VM | 电池正极 |
| VCC | 3.3V 或 5V 逻辑电源 |
| GND | 公共地 |
| STBY | 接 STM32 GPIO 高电平,或直接接 VCC,禁用低电平 |
TB6612 比传统 L298N 好的地方是压降小、发热低,适合两节锂电池直接供电。注意 STBY 引脚如果被拉低,整个驱动芯片会进入待机状态,电机完全不动。排查“电机不转”时要先量这个引脚电压。
4.2 HCSR04 超声波接线
| HCSR04 引脚 | 接 STM32 |
|---|---|
| VCC | 5V |
| GND | 公共地 |
| TRIG | 触发引脚,普通 GPIO 输出 |
| ECHO | 回波引脚,需要支持输入捕获或用 GPIO 模拟测宽 |
HCSR04 的 ECHO 引脚返回的是 5V 高电平脉冲,如果 STM32 引脚不是 5V 容忍引脚,建议串联电阻分压后再接,避免长期使用损坏引脚。
4.3 SG90 舵机接线
| SG90 引脚 | 接 STM32 |
|---|---|
| 红线 | 5V |
| 棕线 | GND |
| 橙线 | TIM 通道输出引脚,50Hz PWM |
SG90 对 PWM 周期敏感,需要 50Hz 的脉宽调制信号,0.5ms 到 2.5ms 的脉宽对应 0 度到 180 度。大多数资料会让超声波探头固定在舵机臂上,需要避障时舵机摆动扫描左、中、右三个方向。
4.4 蓝牙模块接线
| 蓝牙模块引脚 | 接 STM32 |
|---|---|
| VCC | 3.3V 或按模块要求接 5V |
| GND | 公共地 |
| TXD | 接 STM32 的 RX 引脚 |
| RXD | 接 STM32 的 TX 引脚 |
交叉连接是重点:蓝牙的 TX 接单片机的 RX,蓝牙的 RX 接单片机的 TX。如果接成同向,串口数据完全收不到。HC-05 和 HC-06 的注意点还不太一样,HC-05 有 AT 模式可以改名字和主从角色,HC-06 一般只能做从机,手机直接配对使用。
5. 基础驱动层实现
资料里所有功能到最后都落到底层驱动上。先用标准外设库风格写几个关键驱动代码,帮助你理解框架,具体引脚宏定义需要按你的资料替换。
5.1 电机 PWM 驱动
TB6612 需要两路 PWM 和一个方向控制引脚。以 TIM2 输出 PWM 为例:
#include "stm32f10x.h" #define MOTOR_PWM_GPIO GPIOA #define MOTOR_PWM_PIN GPIO_Pin_1 #define MOTOR_AIN1_GPIO GPIOB #define MOTOR_AIN1_PIN GPIO_Pin_0 #define MOTOR_AIN2_GPIO GPIOB #define MOTOR_AIN2_PIN GPIO_Pin_1 void Motor_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Pin = MOTOR_PWM_PIN; GPIO_Init(MOTOR_PWM_GPIO, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Pin = MOTOR_AIN1_PIN | MOTOR_AIN2_PIN; GPIO_Init(MOTOR_AIN1_GPIO, &GPIO_InitStructure); } void Motor_PWM_Init(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; OCInitTypeDef OC_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); TIM_TimeBaseStructure.TIM_Period = 999; TIM_TimeBaseStructure.TIM_Prescaler = 71; TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); OC_InitStructure.TIM_OCMode = TIM_OCMode_PWM1; OC_InitStructure.TIM_OutputState = TIM_OutputState_Enable; OC_InitStructure.TIM_Pulse = 0; OC_InitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM2, &OC_InitStructure); TIM_Cmd(TIM2, ENABLE); } void Motor_SetSpeed(int16_t speed) { if (speed > 0) { GPIO_SetBits(MOTOR_AIN1_GPIO, MOTOR_AIN1_PIN); GPIO_ResetBits(MOTOR_AIN2_GPIO, MOTOR_AIN2_PIN); } else if (speed < 0) { GPIO_ResetBits(MOTOR_AIN1_GPIO, MOTOR_AIN1_PIN); GPIO_SetBits(MOTOR_AIN2_GPIO, MOTOR_AIN2_PIN); speed = -speed; } else { GPIO_ResetBits(MOTOR_AIN1_GPIO, MOTOR_AIN1_PIN); GPIO_ResetBits(MOTOR_AIN2_GPIO, MOTOR_AIN2_PIN); } if (speed > 1000) speed = 1000; TIM_SetCompare1(TIM2, (uint16_t)speed); }代码里TIM_Period设成 999,Prescaler设成 71,在 72MHz 主频下 PWM 频率是 1kHz。这样的频率驱动普通直流减速电机够用。左右两个电机需要各自初始化一个 TIM 通道,调方向时注意左右电机安装方向可能相反,一个用正逻辑一个用反逻辑是常见操作。
5.2 HCSR04 超声波测距
HCSR04 的时序很简单:TRIG 拉高 10us 以上,模块自动发八个 40kHz 的脉冲,ECHO 输出高电平时间就是声音往返一次的时间。高电平时间除以 58,单位是厘米。
用 GPIO 模拟测量 ECHO 高电平时长:
#include "stm32f10x.h" #define TRIG_GPIO GPIOA #define TRIG_PIN GPIO_Pin_6 #define ECHO_GPIO GPIOA #define ECHO_PIN GPIO_Pin_7 void Ultrasonic_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Pin = TRIG_PIN; GPIO_Init(TRIG_GPIO, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_InitStructure.GPIO_Pin = ECHO_PIN; GPIO_Init(ECHO_GPIO, &GPIO_InitStructure); } float Ultrasonic_GetDistance(void) { uint32_t timeout = 0; GPIO_SetBits(TRIG_GPIO, TRIG_PIN); Delay_us(15); GPIO_ResetBits(TRIG_GPIO, TRIG_PIN); while (GPIO_ReadInputDataBit(ECHO_GPIO, ECHO_PIN) == 0) { if (++timeout > 100000) return -1.0f; } TIM3->CNT = 0; TIM_Cmd(TIM3, ENABLE); while (GPIO_ReadInputDataBit(ECHO_GPIO, ECHO_PIN) == 1) { if (TIM3->CNT > 30000) { TIM_Cmd(TIM3, DISABLE); return -1.0f; } } TIM_Cmd(TIM3, DISABLE); return (float)TIM3->CNT * 0.034f / 2.0f; }使用定时器计数来测量高电平宽度,比纯软件延时更准确。超声波模块测距原理是固定的,但实际使用会有测量盲区、声速受温度影响、不同物体反射率不同、长时间连续测距发热导致漂移等问题。建议连续测量时每次间隔 50ms,并做简单的均值滤波或滑动平均。
5.3 SG90 舵机控制
舵机角度控制用 50Hz PWM,脉宽从 0.5ms 到 2.5ms。如果 PWM 周期是 20ms,占空比 2.5% 到 12.5%。在标准库下可以配置 TIM 重载值为 19999,比较值到 500 到 2500 对应 0 到 180 度:
void Servo_SetAngle(uint16_t angle) { uint16_t compare = 500 + (uint16_t)(angle * 2000.0f / 180.0f); TIM_SetCompare2(TIM4, compare); }调用Servo_SetAngle(90)可以让舵机转至中间位置。避障模式下,可以先把舵机转到 90 度,测前方距离;再转到 30 度或 150 度,分别测左右两个方向,然后根据三组距离判断往哪边转。舵机扫到边缘位置后要注意加一点延时,让舵机舵盘稳定后再触发超声波测量,否则读数会偏大或偶尔为 0。
6. 核心功能实现与分析
6.1 PID 循迹
循迹是这套资料的技术核心。红外循迹模块输出的是一排数字信号:在黑线上输出一种电平,在白色地面上输出另一种电平。因为每个传感器返回的是“是黑还是白”的状态,所以直接用状态跳变来转弯会非常生硬,表现为车在线上左右横摆。
更好的做法是把传感器信息换算成一个“位置偏差值”。以五路循迹为例,假设五个传感器的权重是-2, -1, 0, 1, 2,把当前传感器的输出值读出来,计算加权和。如果只有中间传感器压线,偏差为 0;如果车偏左,右边传感器压线,偏差为正;如果车偏右,左边传感器压线,偏差为负。
偏差计算出来之后,用增量式 PID 输出修正量,叠加到左右轮基础速度上:
typedef struct { float Kp; float Ki; float Kd; float error; float last_error; float integral; } PID_t; int16_t PID_Increment(PID_t *pid, float error) { float P = 0, I = 0, D = 0; float delta; P = error - pid->last_error; I = error; D = error - 2.0f * pid->last_error + pid->last_error; pid->integral += error; delta = pid->Kp * P + pid->Ki * I + pid->Kd * D; pid->last_error = error; return (int16_t)delta; }左右轮速度的计算方式:
#define BASE_SPEED 450 #define MAX_SPEED 850 void Tracking_Control(PID_t *pid, int16_t error) { int16_t correction = PID_Increment(pid, error); int16_t left_speed = BASE_SPEED + correction; int16_t right_speed = BASE_SPEED - correction; if (left_speed > MAX_SPEED) left_speed = MAX_SPEED; if (left_speed < -MAX_SPEED) left_speed = -MAX_SPEED; if (right_speed > MAX_SPEED) right_speed = MAX_SPEED; if (right_speed < -MAX_SPEED) right_speed = -MAX_SPEED; Motor_SetLeftSpeed(left_speed); Motor_SetRightSpeed(right_speed); }这里BASE_SPEED是基础速度,correction是 PID 修正量。PID 修正量加到左侧、右侧减掉后,车会朝偏差的反方向回中。增量式 PID 的好处是只输出偏差变化量,不会因为积分项累积导致系统失控。
调参顺序是固定的:先调 Kp,Kp 太小会过弯拉不回来,太大则车在直线上来回震荡;再调 Kd,Kd 提供阻尼,能有效减少直线抖动;最后才是 Ki,通常在存在稳态误差时才加。循迹调试还有个技巧:把 PID 修正量和传感器偏差通过串口打印出来,用 Vofa 上位机看波形。如果偏差曲线快速收敛,说明参数方向是对的;如果始终等幅震荡,说明 Kp 偏大。
6.2 超声波避障
避障逻辑本质上是一个状态决策机。主循环周期性地获取超声波距离,根据距离判断当前状态:
- 距离大于 30cm,认为前方安全,直行。
- 距离小于 30cm 大于 15cm,说明前方有障碍物,减速。
- 距离小于 15cm,紧急停车,开始绕行。
绕行时先让舵机摆动测左右两个方向的距离,选较远的一侧转向,然后前进一段,再回到直行状态。状态机可以用简单的 switch-case 实现:
void Avoid_Process(void) { float front = Ultrasonic_GetDistance(); if (front > 30.0f) { Motor_SetLeftSpeed(BASE_SPEED); Motor_SetRightSpeed(BASE_SPEED); return; } if (front < 15.0f) { Motor_Stop(); Servo_SetAngle(150); Delay_ms(200); float left = Ultrasonic_GetDistance(); Servo_SetAngle(30); Delay_ms(200); float right = Ultrasonic_GetDistance(); Servo_SetAngle(90); Delay_ms(200); if (left >= right) { Motor_SetLeftSpeed(-BASE_SPEED / 2); Motor_SetRightSpeed(BASE_SPEED / 2); } else { Motor_SetLeftSpeed(BASE_SPEED / 2); Motor_SetRightSpeed(-BASE_SPEED / 2); } Delay_ms(500); } else { Motor_SetLeftSpeed(BASE_SPEED - 100); Motor_SetRightSpeed(BASE_SPEED); } }避障调试点主要集中在两个地方:一是超声波每次测距后要加延时,不要连续触发,否则回波可能串扰;二是舵机扫描后要等舵机到位,再测距,否则测量方向不对。从实际经验看,很多“避障失败”不是逻辑问题,而是测距方向滞后导致的误判。
6.3 距离跟随
跟随功能看着高级,本质还是超声波数据的使用。把跟随目标保持在一定距离范围内,比如 20cm 到 35cm:
- 距离大于 35cm,说明目标太远,当前方向前进,同时提高速度。
- 距离在 20cm 到 35cm 之间,保持当前速度或停止,等待。
- 距离小于 20cm,说明离目标太近,后退或者减速。
跟随的 PID 控制和循迹不太一样。循迹的输入是传感器偏差,跟随的输入是目标距离和实测距离的差值:
void Follow_Process(float target_distance) { float current_distance = Ultrasonic_GetDistance(); float error = current_distance - target_distance; int16_t correction = PID_Increment(&follow_pid, error); if (correction > 0) { Motor_SetLeftSpeed(correction); Motor_SetRightSpeed(correction); } else { Motor_SetLeftSpeed(correction); Motor_SetRightSpeed(correction); } }跟随的调参思路和循迹一致,先减小基础速度,再逐步增大 Kp。如果车在目标点来回震荡,说明 Kp 偏大或目标死区太小。更稳妥的做法是设置一段“不灵敏区”:误差在正负 3cm 以内时不更新速度,避免在临界距离反复启动和停车。
6.4 蓝牙遥控
蓝牙遥控是这套小车里最容易出效果的功能。模块和手机连接后,手机发送字符,STM32 串口中断接收,主循环解析后执行对应动作。
串口接收初始化之后,在中断里做一个简单的命令缓存:
uint8_t cmd_buffer = 0; void USART2_IRQHandler(void) { if (USART_GetITStatus(USART2, USART_IT_RXNE) != RESET) { cmd_buffer = USART_ReceiveData(USART2); } }主循环里根据收到的字符执行动作:
void Bluetooth_CommandProcess(uint8_t cmd) { switch (cmd) { case 'F': Motor_SetLeftSpeed(650); Motor_SetRightSpeed(650); break; case 'B': Motor_SetLeftSpeed(-650); Motor_SetRightSpeed(-650); break; case 'L': Motor_SetLeftSpeed(-200); Motor_SetRightSpeed(650); break; case 'R': Motor_SetLeftSpeed(650); Motor_SetRightSpeed(-200); break; case 'S': Motor_Stop(); break; default: break; } }蓝牙控制使用 9600 或 115200 波特率,具体以模块默认配置为准。手机端可以用通用的蓝牙串口助手 APP,也可以自己做上位机。如果发现发送指令后小车没有反应,先检查串口数据能不能收到:把蓝牙模块断开,直接用 USB 转 TTL 接 STM32 的串口,发同样的字符看小车是否动作,这样就能定位是串口问题还是蓝牙模块问题。
7. 调试与效果验证
嵌入式项目不能只看代码理论,必须有一套可以复现的验证方法。
7.1 基础功能验证顺序
先把复杂功能拆开,逐个验证:
| 验证步骤 | 操作 | 通过标准 |
|---|---|---|
| 第 1 步 | 下载电机驱动程序 | 左右电机能正转、反转、调速 |
| 第 2 步 | 下载超声波程序 | 串口打印距离值,用手挡在探头前数值连续变化 |
| 第 3 步 | 下载舵机程序 | SG90 能转到 0、90、180 三个位置 |
| 第 4 步 | 下载蓝牙程序 | 手机发送 F/B/L/R/S 小车响应正确 |
| 第 5 步 | 下载循迹程序 | 车能沿黑线从直道进入弯道并回正 |
| 第 6 步 | 避障与跟随 | 用静止物体做目标,小车能避开或保持距离 |
每完成一步就把对应代码备份一次,不要让未知问题叠加。比如避障失败时,如果超声波测距本身不准,就会很难判断是传感器问题还是决策逻辑问题。
7.2 用串口和 Vofa 观察 PID 波形
PID 调参最怕“看不见数据”。调试时把传感器偏差和 PID 输出通过串口打印出来,格式可以按error:%.2f,output:%d的文本形式打印。Vofa 上位机还支持 JustFloat 协议,一次传输多个 float 数据,适合观察多条曲线的实时变化。
在循迹测试时,把偏差值和修正量同时发出来。如果修正量方向始终和偏差方向相反,说明控制方向反了,需要交换左右轮逻辑或改 PID 符号。如果修正量在快速正负振荡,说明 Kp 过大。如果修正量收敛很慢,车在入弯后拉不回来,说明 Kp 过小或基础速度太快。
7.3 性能与资源占用观察
STM32 智能小车不强求 GPU、显存这类资源,但有几个指标值得关注:
- 主循环执行周期。超声波测距、舵机运动、PID 计算都在一个循环里,循环周期越短,控制响应越快。常见问题是加了
Delay_ms延时后,主循环周期长到 50ms 以上,导致循迹反应迟钝。 - CPU 占用。用调试器在线运行,观察代码块耗时,确认没有死循环阻塞。
- 电池电压波动。TB6612 的 VM 端电压跌落会导致 PWM 输出脉宽对应的实际电压变低,表现为速度漂移和 PID 输出不稳定。建议在电池电压低于 3.7V/节时及时充电,不要在低压下调参,否则调出来的参数到满电时又会偏激。
8. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 编译报缺少 .h 文件 | 标准库或固件库路径没添加 | 检查 Keil 的 Include Paths 设置 | 在 C/C++ 选项卡中加入工程目录路径 |
| ST-Link 下载失败 | 驱动未装好 | 查看设备管理器是否识别下载器 | 重新安装 ST-Link 驱动,检查连接线 SWDIO/SWCLK |
| 电机完全不转 | TB6612 STBY 引脚为低 | 用万用表量 STBY 引脚电压 | 拉高 STBY 或直接接 VCC |
| 电机只转一个方向 | 另一路 PWM 没初始化 | 检查 TIM 通道是否使能 | 确认两路 PWM 都配置并开启输出 |
| 电机转速最低时仍太快 | 基础 PWM 值设置过高 | 串口打印当前 PWM 值 | 降低 BASE_SPEED,或调整 PWM 频率 |
| 车沿黑线左右剧烈摆动 | 循迹逻辑只做开关判断,没有 PID | 观察车能否直线回正 | 改用偏差计算 + PID 修正 |
| 车入弯后拉不回来 | Kp 太小或基础速度太高 | 用 Vofa 观察偏差收敛过程 | 增大 Kp 或降低 BASE_SPEED |
| 超声波距离固定不变 | ECHO 引脚高电平检测失效 | 量 ECHO 引脚电压变化 | 检查 GPIO 配置和模块供电电压 |
| 超声波距离跳动大 | 连续测距间隔太短 | 加延时,打印连续 10 次数据 | 每次测距间隔至少 50ms |
| 蓝牙手机搜不到模块 | 模块处于 AT 模式或未上电 | 检查模块指示灯状态 | 重新上电,进入配对模式 |
| 蓝牙发送指令无反应 | TX/RX 接反 | 检查串口 RX/TX 方向 | 交叉连接 TX-RX、RX-TX |
| 小车跑一段后失控 | 电池电压跌落 | 测量电池空载和带载电压 | 使用锂电池组和低压报警模块 |
| SG90 舵机抖动 | PWM 频率不是 50Hz | 用示波器确认 PWM 波形周期 | 重新配置 TIM 周期为 20ms |
| 避障模式频繁急停 | 舵机扫描方向和测距时序不同步 | 代码中扫描后未加延时 | 舵机到位后再触发超声波测距 |
排查原则是“从底层往上验证”。硬件不动时先不怀疑程序,先用万用表和示波器确认供电、信号、电平是否正常;程序出现行为异常时,先在关键位置加串口打印,缩小问题范围,不要靠猜。
9. 电赛实践与工程化建议
如果你是把这套资料用于电赛备赛,有几个建议值得注意。
第一,不要比赛前夜才第一次调 PID。循迹参数和环境光、赛道材质、电机损耗都有关,必须提前至少三天在模拟赛道上反复测试。准备一组保守参数和一组激进参数,赛前根据现场光线临时切换。
第二,模块化测试数据要留档。每套速度、PID 参数、循迹传感器布局都对应一组实测表现,建议用表格记录:日期、基础速度、Kp、Ki、Kd、直线表现、弯道表现、备注。节省重复调参时间。
第三,硬件留冗余。比赛现场最容易坏的是电机和杜邦线。备件里准备两台电机、一套 TB6612 模块、至少 20 根不同颜色的杜邦线,比准备很多大件更有效。
第四,胶水和扎带要用对。传感器支架、电池、主板要固定牢靠,但也要方便快速拆装。强烈推荐用铜柱加尼龙扎带的方式固定主要模块,避免热熔胶在震动下脱落,也方便换传感器位置。
第五,合规使用器件和代码。写论文、做作品展示时,引用别人的开源代码、接线图、算法思路要标注来源。比赛规则明确禁止抄袭源码和未授权使用的方案,这一点要提前查清楚。
10. 总结与下一步
这套 STM32 智能小车资料最有价值的地方在于把嵌入式学习的核心模块串成了完整项目。B 站和论坛里讲 HCSR04、TB6612、PID 的零散教程很多,但能把这些功能集成到一台可在赛道上跑起来的小车,并给出完整开源工程和接线逻辑的资料,才是真正值得收藏的。
拿到资料后,建议按照这个顺序推进:先跑通电机驱动,这是所有功能的基础;再让超声波测距和舵机扫描工作,验证距离数据稳定;接着用蓝牙做遥控,确认整车控制链路没问题;最后才上 PID 循迹和避障逻辑。最容易踩的坑不是代码看不懂,而是电机方向搞反、超声波时序不对、PID 参数给得太大导致车直接原地振荡。
下一步可以继续扩展的方向很多:加 OLED 显示当前距离和模式、增加按键切换运行模式、用 MPU6050 做姿态解算站上平衡车、把摄像头接入做视觉循迹、通过 ESP8266 模块接入 WiFi 实现局域网内控制和数据回传。对一个已经能把小车跑顺的开发者来说,这些扩展方向都建立在同样的电机、传感器和 PID 控制基础上,上手会非常快。
建议先把资料保存好,再从第一行代码开始,亲手把小车转起来。