简介:这份资源是面向嵌入式初学者与电子竞赛选手的STM32循迹避障小车完整工程代码,围绕路径跟踪与障碍物避障两大核心功能展开,适合已掌握C语言基础、希望从外设驱动进阶到综合项目实战的开发者。压缩包共223个文件,约7.83MB,以c与h源码文件为主,配合Keil工程文件、编译中间产物及hex烧录文件,覆盖GPIO、定时器、PWM、串口等外设驱动与主控逻辑。代码涉及红外或磁感应循迹、超声波测距避障、PID闭环调速、电机H桥驱动等模块,并采用模块化分层设计,便于按初始化、传感器处理、电机控制、避障算法逐块阅读与移植。目前已有4987人学习下载,可作为课程设计、毕业设计或智能车竞赛的参考模板,帮助读者快速理解STM32工程组织方式与调试流程。
1. 从一堆“能跑但不敢改”的代码说起:STM32循迹避障小车到底交付了什么
如果你搜到这份资源,大概率是两种情况:一种是课程设计或比赛临近,需要一套能直接烧录、能跑通循迹和避障的STM32小车代码;另一种是手头已经有车,但代码是东拼西凑的,能跑却不敢动,一改就翻车。这份“基于STM32单片机的循迹避障小车代码”解决的正是后者——它不是从零讲GPIO怎么配,而是把循迹、避障、电机控制、传感器采样这几块整合成一套可复现的工程骨架。适合谁?适合已经会点C、能看懂HAL库或标准库初始化、但缺一个完整项目参照的嵌入式新手,也适合熟手拿来当快速验证平台。它不承诺“一键智能”,但能让你把红外对管、超声波、PWM调速这些模块串起来,知道每一行代码在干什么。
2. 循迹与避障的代码骨架:从传感器采样到电机差速的完整链路
2.1 循迹模块:五路红外对管的阈值判断与偏差计算
循迹的核心不是“看到黑线”,而是把多路红外对管的数字量转成一个带方向的偏差值。常见做法是五路传感器一字排开,中间一路为基准,左右各两路。代码里一般会先读GPIO,再根据阈值判断哪几路压线。但这里有个容易忽略的点:数字量直接做差速,车会像“抽搐”一样左右猛打,因为偏差是跳变的。我一般会先把五路状态映射成位置权重,比如从左到右给-2、-1、0、1、2,再算加权平均,得到一个连续偏差。这样PID或比例控制才有意义。
// 五路红外循迹:读取GPIO并计算偏差 // 假设传感器从左到右接在PA0~PA4,黑线输出0,白底输出1 #define SENSOR_NUM 5 int16_t weight[SENSOR_NUM] = {-2, -1, 0, 1, 2}; // 位置权重 int16_t read_line_error(void) { int16_t sum = 0; int16_t active = 0; for (int i = 0; i < SENSOR_NUM; i++) { if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0 << i) == GPIO_PIN_RESET) { // 检测到黑线,累加权重 sum += weight[i]; active++; } } if (active == 0) { return 0; // 丢线,保持上一次偏差或进入寻线策略 } return sum / active; // 返回平均偏差,范围约-2~2 }这段代码的逻辑说明:weight数组把物理位置转成数值,active统计压线路数。如果全白(丢线),返回0会让车直行,实际更稳妥的做法是返回上一次偏差或触发丢线保护。参数说明:权重值可以根据传感器间距调整,间距大就加大两端权重;GPIO_PIN_0 << i这种写法依赖引脚连续,如果实际布线不连续,得改成查表。常见坑是红外对管受环境光影响大,室内日光灯下阈值漂移,建议上电时做一次动态阈值校准,或者改用模拟量输出传感器。
2.2 避障模块:超声波测距与舵机扫描的时序配合
避障部分通常用HC-SR04超声波加一个舵机,让超声波左右转头扫描。代码上分两块:触发测距和舵机角度控制。HC-SR04的时序是Trig给10us高电平,然后Echo变高,高电平持续时间就是距离。很多人用HAL_Delay测Echo,结果误差大得离谱,因为HAL_Delay是毫秒级,而Echo高电平可能只有几百微秒。正确做法是用定时器输入捕获,或者用__HAL_TIM_GET_COUNTER轮询。舵机控制则是50Hz PWM,脉宽0.5ms~2.5ms对应0~180度。
// 超声波测距:定时器计数法,避免HAL_Delay精度不足 // 假设TIM2时钟72MHz,预分频72-1,计数频率1MHz,1 tick = 1us float get_distance_cm(void) { uint32_t start, end; HAL_GPIO_WritePin(TRIG_PORT, TRIG_PIN, GPIO_PIN_SET); __NOP(); __NOP(); __NOP(); __NOP(); __NOP(); // 约10us,主频72M时 HAL_GPIO_WritePin(TRIG_PORT, TRIG_PIN, GPIO_PIN_RESET); while (HAL_GPIO_ReadPin(ECHO_PORT, ECHO_PIN) == GPIO_PIN_RESET); // 等回波变高 start = __HAL_TIM_GET_COUNTER(&htim2); while (HAL_GPIO_ReadPin(ECHO_PORT, ECHO_PIN) == GPIO_PIN_SET); // 等回波变低 end = __HAL_TIM_GET_COUNTER(&htim2); if (end < start) end += 0xFFFF; // 处理计数器溢出 return (end - start) * 0.034 / 2.0; // 声速340m/s,除以2是往返 }逻辑说明:__NOP()凑10us触发信号,实际项目建议用定时器输出比较更稳。start和end抓取计数器值,差值就是Echo高电平时间(微秒)。参数说明:声速取0.034cm/us,温度变化大时得补偿。避障策略上,常见做法是舵机转0度、90度、180度三次测距,取最大值方向转向。这里有个时序坑:舵机转动需要时间,测距前得延时200ms等它停稳,否则测到的是转动中的距离,数据跳得厉害。
2.3 电机控制:PWM差速与死区补偿
电机驱动一般用L298N或TB6612,代码上就是两组PWM加方向GPIO。循迹和避障的输出最终都变成左右轮速度差。但直流电机有死区:PWM低于某个占空比时电机不转,代码里算出的微小差速根本执行不了。我一般会在PWM输出前加一个死区补偿,比如最小占空比设15%,低于这个值要么给0要么给15%。另外,差速公式别写反:左轮速度 = 基础速度 + 转向修正,右轮速度 = 基础速度 - 转向修正,具体加减看电机安装方向。
// 电机差速控制:带死区补偿 #define BASE_SPEED 50 // 基础PWM百分比 #define DEAD_ZONE 15 // 死区阈值 void set_motor(int16_t left, int16_t right) { // 死区补偿:非零但低于死区的值拉到死区 if (left > 0 && left < DEAD_ZONE) left = DEAD_ZONE; if (left < 0 && left > -DEAD_ZONE) left = -DEAD_ZONE; if (right > 0 && right < DEAD_ZONE) right = DEAD_ZONE; if (right < 0 && right > -DEAD_ZONE) right = -DEAD_ZONE; // 限幅 left = (left > 100) ? 100 : (left < -100) ? -100 : left; right = (right > 100) ? 100 : (right < -100) ? -100 : right; // 设置方向和PWM,具体引脚按实际驱动板改 __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, abs(left)); __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, abs(right)); HAL_GPIO_WritePin(MOTOR_DIR_PORT, LEFT_DIR_PIN, left >= 0 ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(MOTOR_DIR_PORT, RIGHT_DIR_PIN, right >= 0 ? GPIO_PIN_SET : GPIO_PIN_RESET); }逻辑说明:先做死区补偿,再做限幅,最后写PWM和方向。参数说明:BASE_SPEED和DEAD_ZONE得根据实际电机和电池电压调,电池从满电到快没电,同样的PWM转速差很多,所以进阶做法是加编码器做闭环。这段代码的坑在于:如果驱动板是L298N,方向引脚和PWM引脚是分开的,但有些模块是IN1/IN2两路PWM决定正反转,那就不能这么写,得改成两路PWM互补输出。
3. 把代码烧进板子:工程配置、引脚映射与调试手段
3.1 时钟树与引脚分配:先定外设再写逻辑
拿到代码别急着编译,先看时钟树。STM32F103常见配置是外部8MHz晶振,PLL倍频到72MHz。如果代码里用的是内部HSI,串口波特率会飘,超声波计时也不准。引脚分配上,循迹传感器占5个GPIO,超声波占2个,舵机占1个PWM,电机占4个PWM或2PWM+2GPIO,再加上串口调试,基本把F103C8T6的引脚占满了。我一般会先画一张引脚表,把定时器通道和GPIO对应关系列清楚,避免写到一半发现某个定时器通道被两个外设抢了。
| 功能 | 外设 | 引脚示例 | 备注 |
|---|---|---|---|
| 循迹传感器 | GPIO输入 | PA0~PA4 | 上拉输入,黑线拉低 |
| 超声波Trig | GPIO输出 | PB0 | 推挽输出 |
| 超声波Echo | GPIO输入/TIM捕获 | PB1 | 建议用输入捕获 |
| 舵机PWM | TIM3_CH1 | PA6 | 50Hz |
| 电机左PWM | TIM3_CH2 | PA7 | 10kHz |
| 电机右PWM | TIM3_CH3 | PB0 | 注意和Trig冲突 |
| 串口调试 | USART1 | PA9/PA10 | 115200 |
提示:上表只是示例,实际引脚以你手头板子的可用资源为准。如果引脚冲突,优先保PWM和输入捕获,普通GPIO可以随便挪。
3.2 编译与烧录:Keil、CubeMX和标准库的取舍
这份代码大概率是标准库或HAL库工程。如果是CubeMX生成的HAL工程,注意.ioc文件别丢,改引脚得在CubeMX里改完重新生成,直接改main.c里的初始化代码,下次生成就被覆盖。编译时常见报错是“未定义HAL_GPIO_ReadPin”,那是没加对应头文件或库文件没编进来。烧录用ST-Link的话,Keil里选SWD模式,注意复位方式选“Connect under reset”,否则偶尔连不上。烧完第一次跑,先别放地上,拿手挡住传感器看电机转不转,确认逻辑方向对了再落地。
# 用STM32CubeProgrammer命令行烧录示例(假设已生成bin) STM32_Programmer_CLI -c port=SWD -w firmware.bin 0x08000000 -v -rst逻辑说明:-c port=SWD指定SWD接口,-w写文件到Flash起始地址,-v校验,-rst复位运行。参数说明:如果用的是hex文件,地址可以省略,工具会自己解析。这条命令适合批量烧录或脚本化,日常调试还是IDE里点下载方便。
3.3 串口调试:把偏差和距离打出来看
小车跑得不顺,第一反应不应该是改PID,而是把传感器数据打出来。串口每隔100ms打印五路循迹状态、计算出的偏差、超声波距离、左右轮PWM值。这样一眼就能看出是传感器没读到,还是偏差算反了,还是电机没执行。我习惯用printf重定向到USART1,注意在Keil里勾选“Use MicroLIB”,否则printf会卡死。打印格式用逗号分隔,方便丢进Excel画曲线。
// 串口重定向,Keil下需勾选Use MicroLIB #include <stdio.h> int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 10); return ch; } // 在主循环里打印 printf("line:%d,%d,%d,%d,%d,err:%d,dist:%.1f,L:%d,R:%d\r\n", s0, s1, s2, s3, s4, error, distance, left_pwm, right_pwm);逻辑说明:fputc把标准输出重定向到串口,之后就能用printf。参数说明:HAL_UART_Transmit的超时给10ms,太短会丢数据。打印频率别太高,115200波特率下每行几十字节,100ms一次足够,再快会阻塞主循环影响控制周期。
4. 避坑与排查:循迹抖动、避障误判、电机不转的常见原因
4.1 循迹车在直线上左右画龙
现象:车能沿线走,但走不直,像喝醉一样左右摆。原因:偏差计算用了跳变的数字量,或者比例系数太大,系统在过冲和修正之间振荡。解决:先把五路状态映射成连续偏差(参考2.1),再把比例系数从0.1开始慢慢加,加到刚好不摆为止。如果还摆,加一点微分项,但微分对传感器噪声敏感,得先做滤波,比如连续三次采样取中间值。
4.2 超声波避障时好时坏,偶尔撞墙
现象:大部分时候能停,偶尔直接撞上去。原因:超声波测距被舵机转动干扰,或者测距周期太短,上一次回波还没处理完就发下一次触发。解决:舵机转到位后延时200ms再测距,测距间隔至少60ms,因为超声波最大量程4米时往返时间约24ms,加上余量得留够。另外,Echo引脚如果没接输入捕获,用轮询时得加超时,否则丢一次回波就死循环。
4.3 电机嗡嗡响但不转,或者一边转一边不转
现象:PWM给了,电机有声音但轮子不动,或者只有一边动。原因:PWM频率不对,或者死区没补偿,或者方向引脚接反。解决:先确认PWM频率在5kHz~20kHz之间,太低会听到啸叫,太高驱动板可能响应不了。再用万用表量电机两端电压,如果电压有但电机不转,可能是电流不够,检查电池电压和驱动板供电。一边不转就交换左右电机插头,判断是代码问题还是硬件问题。
4.4 烧录后程序不跑,或者跑一下就停
现象:烧录成功但板子没反应,或者跑几秒就死机。原因:时钟配置错误导致主频不对,或者中断优先级冲突,或者栈溢出。解决:先点灯测试,确认最小系统能跑。再看启动文件里的栈大小,循迹避障代码里如果用了浮点运算和printf,栈得调到0x800以上。中断优先级上,SysTick和定时器中断别设成同优先级,否则会互相打断。
4.5 电池电量下降后行为异常
现象:满电跑得好好的,跑十分钟后开始乱窜。原因:电机供电和单片机供电共用电池,电机电流大导致电压跌落,单片机复位或传感器阈值漂移。解决:电机和单片机分开供电,或者加一个大电容缓冲。代码上可以做低压检测,电压低于阈值时降速或报警。这个坑在比赛现场特别常见,血泪经验就是多带一块电池。
5. 进阶调参:用PID替代比例控制,以及编码器闭环的接入思路
比例控制能让车跑起来,但速度一变或者赛道摩擦不同,偏差就压不住。这时候得上PID。位置式PID的公式不复杂,关键是参数整定。我一般先整P,让车能快速响应但不振荡;再加D,抑制过冲;最后加一点点I,消除稳态误差。但循迹的I不能大,否则丢线后积分饱和,车会猛打方向。代码上,PID输出限幅和积分限幅都得加,不然翻车是迟早的事。
// 位置式PID:循迹偏差控制 typedef struct { float Kp, Ki, Kd; float integral; float prev_error; float out_min, out_max; } PID; float pid_calc(PID *pid, float error) { pid->integral += error; // 积分限幅,防止饱和 if (pid->integral > 100) pid->integral = 100; if (pid->integral < -100) pid->integral = -100; float derivative = error - pid->prev_error; pid->prev_error = error; float output = pid->Kp * error + pid->Ki * pid->integral + pid->Kd * derivative; // 输出限幅 if (output > pid->out_max) output = pid->out_max; if (output < pid->out_min) output = pid->out_min; return output; }逻辑说明:积分累加后限幅,微分用差分代替。参数说明:Kp从0.5开始试,Kd从0.1开始,Ki给0.01级别。整定顺序是P→D→I,每次只调一个。如果车在直线上高频抖动,是Kd太大或传感器噪声大;如果过弯反应慢,是Kp太小。
再进一步就是编码器闭环。直流电机加霍尔编码器,测左右轮实际转速,把PID从位置控制改成速度控制。这样电池电压下降时,代码会自动加大PWM维持转速。接入思路是:定时器编码器模式读脉冲,算出单位时间转速,再和设定速度做PID。但这一步对新手来说,先把开环跑稳再说,闭环的调试复杂度至少翻倍。
从那以后我每次拿到一套小车代码,都强制先做三件事:点灯确认最小系统、串口打印传感器数据、手挡传感器看电机方向。这三步走完,后面调参才有意义。希望帮到你。
本文还有配套的精品资源,点击获取