资讯动态

STM32智能小车循迹避障实战:从硬件配置到状态机与PID调试

发布时间:2026/9/17 4:21:41 来源:尧图企业网站定制
简介这是一份基于STM32的智能循迹避障小车完整项目资源适合自动化、电子信息、计算机等相关专业学生用于毕业设计、课程设计或大作业。压缩包内共176个文件以C源码和头文件为主涵盖STM32标准外设库常用模块如定时器、串口、ADC、I2C、CAN等同时包含工程配置文件、编译输出文件和说明文档可支撑从代码阅读到烧录调试的全过程。资源包大小约565KB整体轻量便于快速部署。目前已有233人学习浏览项目曾获导师认可并取得95分答辩成绩说明方案完整度较高。内容包含可运行的工程源码、编译器生成文件及辅助文档适合直接用于演示或在此基础上扩展避障、调速、通信等功能对正在筹备课设或毕设的读者有较好的参考价值。1. 入手前先看懂小车项目的基本盘把一块 STM32 主控板、一路电机驱动、一排红外传感器和一个超声波模块拼成能自己跑的小车是嵌入式学习里反复出现的练习但真正让人卡住的往往不是电机转不转而是循迹和避障两个任务同时跑时的调度逻辑。这个项目标题里最值钱的部分不是“源码.zip”而是“源码 文档 资料”这个组合它说明作者已经把硬件接线、控制逻辑、标定步骤和故障排查整理成了可复现的工程而不是随手丢一个 main.c。适合看这篇文章的人有两类一类是刚开始接触 STM32 的在校生想把毕设或课设从“能跑”做到“讲得清楚”另一类是转嵌入式开发、想快速熟悉 GPIO、定时器 PWM、串口调试和外设驱动搭配的工程师。后者往往已经有 C 语言和单片机基础缺的是把零散外设串成一个状态机并处理真实地面、电机死区和传感器误触发这些“实验室里看不见的问题”。下面按我通常会做的顺序展开先把硬件层的引脚和定时器配置定清楚再写状态机控制逻辑最后做标定和整理资料。这样当你拿到任何一份小车源码时都能用它自己的接线表快速确认工程结构。2. 硬件驱动与 GPIO/定时器配置的要害2.1 电机驱动、循迹传感器和超声波模块的选型与接线围绕 STM32 做智能小车硬件方案已经相当固定常见做法是 STM32F103C8T6 最小系统板 TB6612FNG或 L298N电机驱动 TCRT5000 灰度循迹模块 HC-SR04 超声波模块。F103C8T6 的主频 72MHz片上 Flash 标称 64KB实际多数型号能读到 128KBRAM 20KB跑一个五路循迹加避障状态机绰绰有余。选型上不要把注意力放在“哪个主控更强”而是放在驱动模块和传感器的电平匹配上。TB6612FNG 的典型优势是导通压降小、不需要额外散热片电机低速控制比 L298N 平稳L298N 虽然便宜但在电池电压偏低时压降会吃掉一格电转速会突然掉下去。循迹模块优先选带五个探头的版本五路在计算中线偏差时比三路多一个样本直道和 S 弯的鲁棒性不是一个量级。HC-SR04 的 Echo 引脚是 5V 电平F103 的 GPIO 耐压是 5V 容忍直接接大多数情况下能工作但为了长期稳定建议串一个 1k 到 2k 的电阻限流。下面是一套我常用的引脚分配表格中的标注适合直接画接线图模块引脚对应 STM32说明电机驱动 TB6612PWMA / AIN1 / AIN2PA0(TIM2_CH1) / PB12 / PB13左电机电机驱动 TB6612PWMB / BIN1 / BIN2PA1(TIM2_CH2) / PB14 / PB15右电机循迹模块OUT1 到 OUT5PC0 到 PC4五路数字输出HC-SR04Trig / EchoPA6 / PA7避障测距舵机 SG90信号线PA8(TIM1_CH1)用于转动超声波探头可选串口TX / RXPA9 / PA10调试输出需要注意 PB12 到 PB15 这几个引脚在 JTAG 模式中是默认的 JTAG 调试口直接当 GPIO 用会不生效必须在 CubeMX 里选择 SWD 四线调试模式或者干脆关闭 JTAG 只留 SWDIO/SWCLK。很多新手把程序下载进去之后发现电机引脚没反应十有八九是没处理这个冲突。2.2 用 CubeMX 把 GPIO、PWM 和定时器先配到能跑拿到源码后最忌讳打开 Keil 直接编译下载。先看原作者的 CubeMX 工程或引脚定义是否与你的开发板一致尤其是外部晶振如果开发板是 8MHz HSE时钟树就要按 8MHz 输入来配如果是 25MHz 晶振不重新配分频系数就会导致串口波特率偏 30% 以上。这部分被跳过的代价是整个项目一开机就静默失败。在 STM32CubeMX 里配置这套系统操作顺序是先选择 MCU 型号 STM32F103C8Tx勾选 SYS 调试口为 Serial Wire。RCC 里把 HSE 设为 Crystal/Ceramic Resonator时钟树让 SYSCLK 达到 72MHz。然后逐项打开用到的外设TIM2 做电机 PWM、TIM1 做舵机 PWM、USART1 做调试串口GPIO 里把 PC0 到 PC4 设为输入、PB12 到 PB15 设为输出。循迹传感器的数字输出是开漏还是推挽取决于模块里的比较器电路通常设 GPIO 输入时把上拉打开。初始化代码自动生成后裸机主循环里至少要保证定时器输出是正常工作的。下面是 TIM2 通道 1 的 PWM 初始化配置可以直接抄到自己的工程里对照htim2.Instance TIM2; htim2.Init.Prescaler 0; // 不分频72MHz 计数 htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.Period 3599; // 重装值决定频率 htim2.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; HAL_TIM_PWM_Init(htim2); TIM_OC_InitTypeDef sConfigOC {0}; sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 0; // 初始占空比为 0 sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; HAL_TIM_PWM_ConfigChannel(htim2, sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1);这里的关键是 Prescaler 和 Period 一起决定了 PWM 频率频率 72MHz / ((Prescaler 1) * (Period 1))。Prescaler 设为 0、Period 设为 3599得到 20kHz。脉冲比较值 Pulse 的范围是 0 到 3599也就是每 1% 占空比对应约 36 个计数单位。为什么要这个精度因为小车轮径小、负载轻速度差异往往只有 2% 到 3% 的占空比如果 Resolution 不够修正差值会被取整吞掉导致循迹直线跑不出来。舵机的 PWM 频率需要单独看SG90 是 50Hz 周期对应 Period 为 7199高电平脉宽 0.5ms 到 2.5ms 对应角度 0 到 180 度。如果要带舵机换向扫描测距就用 TIM1_CH1配置频率和 TIM2 不一样别直接复制电机的参数。2.3 给电机调速的 PWM 参数要这样设电机驱动作为一个线性器件PWM 参数的设置比 GPIO 电平复杂因为它同时涉及机械震动、电流噪声和死区。先给一组我实际跑过 15cm 直径小车底盘的经验值参数参考值说明PWM 频率20kHz高于人耳可听范围减少尖锐噪声ARR 重装值359912 位分辨率最小启动占空比8% 到 12%克服电机静摩擦力所需最小电压刹车时的行为占空比置 0 并拉低方向引脚避免反向电动势串扰最小启动占空比不是固定值它跟电池电压和电机电刷阻力有关。测试方法是让车空载从 0 开始每 0.5% 增加占空比当轮子刚刚连续转动记录此时的值。比如测得 10%那速度下限就定在 12% 到 15%留一点裕度。方向切换时不要在同一行把方向引脚反向否则电机的惯性会让电流瞬间反向冲击驱动芯片。我通常的做法是先把 PWM 置 0延迟 30ms再切换方向最后重新赋速度值。PWM 频率也不宜乱加超过 25kHz 后TB6612 的 MOS 管开关损耗明显上升芯片会发烫低于 5kHz 则一方面有噪声另一方面电机电流纹波增大红外循迹传感器会收到明显的电源噪声干扰。20kHz 是一个不需要在芯片手册里翻半天就能得到的稳定的中庸值。3. 循迹与避障控制逻辑的源码骨架3.1 状态机结构用有限状态机把循迹和避障串起来源码里最值得读的部分不是传感器读取函数而是主循环里那个状态机。很多初版小车把循迹和避障写成 if 嵌套三四个条件叠在一起之后只要某个分支里少了一个 return整车就会在某段赛道疯转。用状态机管理这类任务是一种更稳的工程做法每个时刻系统只有一个明确状态状态迁移条件集中在同一处出问题时也容易通过串口打印状态号来定位。现阶段我需要定义四个状态ST_LINE 表示正常循迹ST_AVOID 表示检测到障碍正在执行绕行ST_RECOVER 表示短暂丢线后的寻回ST_STOP 表示异常停车。优先级关系是避障最高、循迹其次、丢线恢复最低。闲话少说主循环的骨架可以直接写成这样typedef enum { ST_LINE, ST_AVOID, ST_RECOVER, ST_STOP } car_state_t; car_state_t g_state ST_LINE; uint32_t g_state_start 0; void state_enter(car_state_t next) { g_state next; g_state_start HAL_GetTick(); } void car_run_loop(void) { switch (g_state) { case ST_LINE: state_line(); if (ultrasonic_cm() 25) { state_enter(ST_AVOID); } break; case ST_AVOID: state_avoid(); break; case ST_RECOVER: state_recover(); break; case ST_STOP: motor_set(0, 0, MOTOR_STOP); break; default: state_enter(ST_STOP); break; } }state_enter 用一个统一入口记录进入时间g_state_start在后面做超时控制时非常有用。比如在 ST_AVOID 里如果超声波持续测到障碍说明绕行路径被挡死了需要在 timeout 后进行反方向尝试。这个时间戳比在每个分支里单独定义 start 变量整洁得多。状态机的迁移规则应当写在状态处理函数里而不是在一个大 switch 里堆条件。这样做的直接好处是后续如果你想把避障从“固定方向绕行”升级成“动态决策”只需要改 state_avoid 一整个函数不影响循迹和恢复逻辑。3.2 灰度循迹与中线偏差计算五路 TCRT5000 的数字输出在白色地面通常是高电平检测到黑线时输出低电平具体高低逻辑取决于模块上的电压比较器接法。读回来的五路信号可以打包成一个 5 位掩码每一位对应一个传感器。计算偏差时最简单可靠的方法是加权平均法给每个传感器位置一个权重比如左到右依次是 -2, -1, 0, 1, 2再把触发传感器权重相加后除以触发数量就得到当前黑线中心相对车体中心的偏移。这个算法在代码里长这样int8_t line_get_error(uint8_t mask) { int16_t sum 0; int8_t cnt 0; int8_t weight[5] {-2, -1, 0, 1, 2}; for (int8_t i 0; i 5; i) { if (mask (1 i)) { sum weight[i]; cnt; } } if (cnt 0) { return 127; // 丢线标志 } if (mask 0b00000) { return 127; } return sum / cnt; }注意 mask 为 0 时返回的是 127 这个特殊值不能返回 0因为 0 表示车体正好压在线上。返回 127 之后上层状态机能区分“偏离很小”和“完全丢线”继而进入 ST_RECOVER 而不是当作直线继续直行。如果只返回普通偏差丢线时 PID 会按一个较大的误差处理通常结果是小车直接冲出赛道。还要处理一种情况当五个传感器里有两到三个同时触发时说明车头骑在线上或者线宽过宽。加权平均法天然能处理这个因为 sum/cnt 会把多个触发位置拉回到几何中心。不该做的事是只取最左边触发位或最右边触发位当作偏差这在弯道里会造成严重的滞后。3.3 超声波避障触发与转向角度处理超声波测距的逻辑分成两步发出 Trig 脉冲再测量 Echo 高电平持续的时间。用阻塞轮询方式读 Echo 会让整个主循环卡住最长几十毫秒对循迹状态机来说不可接受所以实践中要么用输入捕获中断要么把测距放到循环里非阻塞地扫描。下面是一个输入捕获实现的基本流程void hc_sr04_start(void) { HAL_GPIO_WritePin(TRIG_PORT, TRIG_PIN, GPIO_PIN_RESET); delay_us(2); HAL_GPIO_WritePin(TRIG_PORT, TRIG_PIN, GPIO_PIN_SET); delay_us(10); HAL_GPIO_WritePin(TRIG_PORT, TRIG_PIN, GPIO_PIN_RESET); } uint32_t echo_time_us(void) { uint32_t start 0, end 0, timeout 50000; while (!Echo_high() timeout--) ; // 等待上升沿 start DWT-CYCCNT / 72; // 换成微秒 timeout 50000; while (Echo_high() timeout--) ; // 等待下降沿 end DWT-CYCCNT / 72; return (end start) ? (end - start) : 0; }距离计算公式是 distance_cm echo_us * 0.017。比如 Echo 持续 2ms就是 2000us距离为 34cm。这里建议在主频 72MHz 下用 DWT-CYCCNT 做微秒计时精度比 HAL_GetTick 高很多而且不需要额外初始化性价比极高。避障的决策不要等小车已经怼到障碍物 5cm 才开始25cm 是常见触发距离。检测到障碍后先记下障碍出现在车头左侧还是右侧原则是若左侧有障碍就右转绕行。如果舵机装上了可以做一个两段扫描第一段先让舵机转左测距再转右测距选择距离较大的一侧作为绕行方向。绕行动作本身用固定时间控制一般 500ms 到 900ms绕行结束后重新检测前方距离若小于阈值则继续绕否则切回 ST_RECOVER 寻找黑线。这里的时间参数请结合车速标定不能照抄网上任何一串数字。3.4 PID 巡线最少参数表循迹控制不写 PID 也能跑但速度超过 0.5m/s 时会出现明显的弯道甩尾。PID 控制里P 项负责对偏差作出直接反比例修正D 项负责抑制修正过度I 项在循迹场景基本不需要因为系统本身是低速连续的不会存在需要积分才能消除的稳态误差。最小可用的 PID 只需要 P 和 D。下面的增量式 PID 非常适合小车轮速控制int8_t last_error 0; int16_t pid_speed(int8_t current_error) { int16_t p_out KP * current_error; int16_t d_out KD * (current_error - last_error); last_error current_error; return p_out d_out; }调用时把基础速度 BASE_SPEED 和 PID 输出相加分别赋给左右电机。车体往右偏误差为正值那左侧轮子加速、右侧减速形成一个顺时针修正力矩。注意 PID 输出可能超出 PWM 上限在写入占空比前一定要做饱和限制。参数表给出典型起点值实际还得以你的底盘和电池电压为准参数起点调高后的影响KP0.006回复越快过高则摇头KD0.02抑制摇头过高则响应迟缓BASE_SPEED1200 / 3599 满占空比约 33%越高越难收敛把 KP 和 KD 写成浮点嵌入式里没有大问题但更快的做法是放大整型将 KP*100 存为整数输出前除以 100。这个习惯在代码里会显得更工程化也避开无意义的浮点运算。4. 参数标定、串口调试与常见翻车点4.1 传感器阈值要在实际地面标定TCRT5000 模块上的电位器可以调节比较器阈值但每个模块之间的器件离散性可能导致同一电位器位置上不同模块灵敏度不一致。如果你手里这版源码用的是 ADC 采样而不是数字输出那就需要在代码里做自动标定。标定过程可以复用串口开机提示“开始标定”把传感器依次放在黑线和白地上采集最大最小值算出中间值作为判断阈值。源码里可以预留一个标定模式void sensor_calibration(void) { for (int i 0; i 5; i) { printf(校准传感器%d...\r\n, i); HAL_Delay(2000); uint16_t black adc_read(i); // 黑线采样 HAL_Delay(500); uint16_t white adc_read(i); // 白色地面采样 sensor_th[i] (black white) / 2; printf(阈值%d\r\n, sensor_th[i]); } }如果模块输出是数字量没有 ADC 通道那就只能靠电位器配合串口打印状态来判断把传感器按到黑线上看输出是否从高变低。这里容易犯的错是只在一块地板上标定换到浅色木纹桌面或瓷砖反光地面上就完全失效。因此标定动作一定要在最终比赛或演示用的地面上做。标定完成后的阈值可以固化到 Flash或者写死在头文件里。不要每次开机都标定那样到了比赛现场反而可能把初始状态搞乱。4.2 串口调试数据格式与解析串口是这个项目里最快的排错手段比 J-Link 断点还要直接因为小车在跑你不可能一边追着车一边打断点。调试输出要有固定格式我用的是类似 Key:Value 的表格形式方便在串口助手或自写上位机里做列对齐。具体输出格式建议包含四个关键量状态号 state、前方距离 distance、偏差 error、左右电机占空比 dutyL/dutyR。一次循环打印一行的代码如下printf(S:%d D:%dcm E:%d ML:%d MR:%d\r\n, g_state, (int)distance_cm, error, duty_left, duty_right);输出的数据按 100ms 周期打印一次不要每个主循环都打否则串口缓冲区很快塞满程序会倒过来等 printf。用 HAL_Delay 或者按 tick 计数来控制打印节奏。判断“避障触发是否过早”直接看状态号从 ST_LINE 变成 ST_AVOID 时的距离值和障碍物实际位置对比就完成一次验算。Keil 里让 printf 走串口需要重写 fputc下面是标准做法#include stdio.h int fputc(int ch, FILE *f) { (void)f; while ((USART1-SR USART_FLAG_TXE) 0); USART1-DR (uint8_t)ch; return ch; }检查串口输出前先确认波特率一致。CubeMX 里设置 115200-8-N-1串口助手也设 115200。如果输出乱码优先查晶振配置而不是查波特率计算。芯片买到手都在跑 HSE但 CubeMX 工程如果默认选了 HSI 8MHz串口波特率会产生系统性偏移。4.3 电机死区、跑偏和误触发的处理小车跑偏是另一个高频问题而且它不一定来自控制算法而是机械结构。两个电机的减速箱摩擦系数几乎不可能完全一致新电池下速度差小电池电压下降到 6.8V 之后差异会放大。先不要动 PID 参数先把左右轮对调接线如果原来偏左变成了偏右确认是机械差异用软件转速校准来处理。软件校准的常见做法是在代码里加一个左右速度系数这比硬调占空比更优雅int16_t duty_left (int16_t)(BASE_SPEED pid_out) * 1000; int16_t duty_right (int16_t)(BASE_SPEED - pid_out) / 1000; duty_left duty_left * LEFT_TRIM; // LEFT_TRIM 默认 1000可调整整数比例 duty_right duty_right * RIGHT_TRIM;车还有可能在避障动作结束后一直找不到线卡死在 ST_RECOVER。常见原因是避障转过的角度不足导致车头正对的方向和原循迹方向差了一个小角度传感器刚好落在线的外侧。解决方法是让 ST_RECOVER 状态下做小幅来回摆头扫描而不是原地等待超时。调试中常见的故障和对应方案整理成表现象可能原因检查路径电机不转但有 PWM 波形电机驱动 EN 脚没使能或方向引脚冲突先量驱动模块 AIN/BIN 电压循迹正常但避障不触发Echo 没有正确匹配电平或触发距离设得过小串口打印距离值看是否在变化速度稍高就冲出赛道KD 过小或传感器安装高度偏高降低总体速度先调 KP锂电池供电一开电机就复位VM 端没有大容量储能电容在驱动模块电源端并 1000uF 电解电容下载程序后 GPIO 上电电平乱跳外部晶振接触不良或 BOOT0 悬空硬件上拉 BOOT0 为 0检查 SWD 线5. 把源码和资料整理成可复现项目的收尾技巧到这里一个功能上可跑的循迹避障小车已经完成你想把它从“自己机器上能跑”变成“别人解压后也能复现”这才是“优秀项目”四个字的含金量所在。源码压缩包里如果没有接线表、引脚定义说明和标定步骤下一个接手的人只能逐行读 main.c 去猜那这个项目在简历上就失去了说服力。我建议的项目目录结构分成四块Firmware、Hardware、Docs、Tools。Firmware 里放 Keil / CubeMX 工程以及源码Hardware 放原理图 PDF、引脚接线表和底盘照片Docs 放一份 README.md开头第一张表写明引脚映射、第二张表写明电池电压范围第三部分写标定步骤Tools 放串口调试助手使用截图或制作脚本。目录结构直接写到源码包里这个动作成本极低收益却是让人打开 zip 后两分钟内就能判断项目是否可靠。在固件层面还有一个提高搬运性的独家技巧在 main 初始化完成后加入一段上电自检自检通过后蜂鸣器短鸣一声。自检内容不需要完整跑算法只检查三类事五个循迹传感器是否都能读到有效值、超声波模块能不能收到回波、左右电机驱动使能后 PWM 是否正确输出。自检逻辑里允许出错打印错误码错误码通过 LED 闪烁次数区分。这样新板子拿过来烧录固件后用户不需要接线对标只看自检结果就能知道是哪个环节没接对。最后补充一个小优化就是把当前的 PID 参数、传感器阈值、左右轮速度系数都放到单独的 config.h 里并注释清楚它们各自的调整方向和限制范围。这样一来即便资料里没有我的精调记录使用者也敢自己动手改参数不至于怕把车调飞。这套方法放在任何依赖外设参数的嵌入式项目里都通用也是我从调试智能小车开始沿用到现在的工作习惯。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价