资讯动态

STM32循迹避障小车代码实战:从传感器采样到电机差速的完整工程

发布时间:2026/10/15 12:05:21 来源:尧图企业网站定制
简介这份资源是面向嵌入式初学者与电子竞赛选手的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计数频率1MHz1 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或2PWM2GPIO再加上串口调试基本把F103C8T6的引脚占满了。我一般会先画一张引脚表把定时器通道和GPIO对应关系列清楚避免写到一半发现某个定时器通道被两个外设抢了。功能外设引脚示例备注循迹传感器GPIO输入PA0~PA4上拉输入黑线拉低超声波TrigGPIO输出PB0推挽输出超声波EchoGPIO输入/TIM捕获PB1建议用输入捕获舵机PWMTIM3_CH1PA650Hz电机左PWMTIM3_CH2PA710kHz电机右PWMTIM3_CH3PB0注意和Trig冲突串口调试USART1PA9/PA10115200提示上表只是示例实际引脚以你手头板子的可用资源为准。如果引脚冲突优先保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 portSWD -w firmware.bin 0x08000000 -v -rst逻辑说明-c portSWD指定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。但这一步对新手来说先把开环跑稳再说闭环的调试复杂度至少翻倍。从那以后我每次拿到一套小车代码都强制先做三件事点灯确认最小系统、串口打印传感器数据、手挡传感器看电机方向。这三步走完后面调参才有意义。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑