ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32电磁循迹小车完整代码解析:从电感采集到定圈停车

STM32电磁循迹小车完整代码解析:从电感采集到定圈停车

简介:这份资源是面向嵌入式初学者与竞赛选手的STM32电磁循迹小车完整工程代码,附带详尽中文注释,并集成蓝牙遥控、线路长度测量、定圈停止等扩展功能,可帮助读者理解传感器数据采集、电机PWM调速与转向控制、UART蓝牙通信及编码器里程计算等核心环节,适合作为课程设计或智能车比赛的参考方案。压缩包共249个文件,约9MB,以c源文件与h头文件为主体,配合Keil工程文件、编译中间产物及hex、axf等可执行输出,目录结构完整,便于直接打开工程对照注释逐模块研读。目前已有6729人学习下载,读者可从中获得循迹算法实现思路、蓝牙指令解析流程、圈数统计逻辑与调试排错经验,为后续嵌入式项目开发积累可复用的代码基础。

1. 从一堆能跑的代码说起:这套 STM32 电磁循迹小车到底解决了什么

拿到一份“能跑但看不懂”的单片机工程,比拿到一份跑不起来的工程更让人难受。这份 STM32 电磁循迹小车完整代码,最直接的价值不是“又一个循迹 Demo”,而是把电感采集、舵机打角、电机差速、蓝牙遥控、赛道长度累计、定圈停车这些散落在各处的功能,全部塞进一个带详细注释的工程里。它适合两类人:一类是刚接触 STM32 和电磁循迹、想照着接线就能让车动起来的新手;另一类是已经能跑基础循迹、但卡在“怎么加遥控、怎么算里程、怎么定圈停”的二次开发者。下面按“能干什么 → 怎么跑 → 怎么改”的顺序,把这份代码拆开讲。

2. 工程结构与技术选型:从电感信号到轮速输出的完整链路

2.1 硬件组成与信号流

这套小车的核心是“电磁感应 + 差速转向”。车头布置若干工字电感,赛道中心线通有特定频率的交流信号,电感感应出的电压幅值随车头偏离中心线的距离变化。主控用 STM32 的 ADC 采集这些电感信号,经过滤波和归一化后算出偏差,再通过 PID 或分段打角输出舵机 PWM,同时根据偏差和速度目标调整左右轮 PWM 占空比。蓝牙模块挂在 USART 上,用来接收遥控指令和回传状态;测线路长度靠编码器或电机霍尔脉冲累计;定圈停止则是在累计里程达到设定圈长后切断电机输出。

常见做法是把整个工程分成Core(主逻辑)、Hardware(外设初始化)、Algorithm(循迹与 PID)、App(蓝牙、里程、定圈)几层。这份代码的注释密度比较高,基本每个函数入口都写了“输入什么、输出什么、什么时候调用”,对新手比较友好。

2.2 目录结构速览

一个典型的工程目录大致如下,具体文件名可能略有差异,但分层思路一致:

Project/ ├── Core/ │ ├── main.c // 主循环、初始化调度 │ ├── stm32f1xx_it.c // 中断服务函数 │ └── system_stm32f1xx.c // 系统时钟 ├── Hardware/ │ ├── adc.c // 电感信号采集 │ ├── pwm.c // 舵机与电机 PWM │ ├── encoder.c // 编码器/霍尔测速 │ └── usart.c // 蓝牙串口 ├── Algorithm/ │ ├── filter.c // 滑动平均/中值滤波 │ ├── pid.c // 位置式/增量式 PID │ └── track.c // 偏差计算与打角策略 ├── App/ │ ├── bluetooth.c // 遥控指令解析 │ ├── odometer.c // 里程累计 │ └── lap_stop.c // 定圈停车判断 └── Drivers/ // 标准外设库或 HAL 库

提示:不同版本的工程可能用标准库也可能用 HAL 库,接线和引脚定义以自己板子的原理图为准,不要直接照搬引脚号。

2.3 关键外设初始化顺序

上电后的初始化顺序很关键,顺序错了会出现“电机先转、电感还没稳定”的翻车现场。常见顺序是:时钟 → GPIO → ADC(含校准)→ PWM 定时器 → 编码器定时器 → USART → 中断优先级 → 使能输出。下面是一段简化的初始化骨架,注释里标了每一步的作用:

int main(void) { SystemClock_Config(); // 配置系统时钟,决定后续定时器频率 MX_GPIO_Init(); // 引脚复用与上下拉 MX_ADC1_Init(); // 电感采集通道 MX_TIM1_Init(); // 舵机 PWM,通常 50Hz MX_TIM2_Init(); // 电机 PWM,频率可高一些 MX_TIM3_Init(); // 编码器接口模式 MX_USART1_UART_Init(); // 蓝牙串口,常见 9600 或 115200 ADC_Calibrate(); // ADC 自校准,减少零点漂移 Motor_Stop(); // 先确保电机不动 Bluetooth_SendString("READY\r\n"); while (1) { Track_Task(); // 采集、算偏差、打角 Speed_Task(); // 测速与差速调整 App_Task(); // 蓝牙解析、里程、定圈判断 } }

这段代码的逻辑是“先让所有外设进入确定状态,再进主循环”。ADC_Calibrate()不能省,电磁信号本身幅值不大,零点漂移会直接让车往一边偏。Motor_Stop()放在初始化末尾,是为了避免定时器刚启动时占空比寄存器残留值导致车突然窜出去。主循环里三个任务按固定节奏调用,实际工程里常用定时器中断打标志位来保证采样周期稳定,而不是靠while空转。

3. 核心模块拆解:电感采集、偏差计算与舵机打角

3.1 电感信号采集与滤波

电感信号是整套循迹的“眼睛”。ADC 采回来的原始值会随电源波动、赛道信号强度变化而抖动,直接拿去算偏差,车会像喝多了一样左右摆。常见做法是每个通道连续采多次,去掉最大最小后取平均,再做一阶低通。下面是一段可抄的采集函数:

#define SAMPLE_NUM 8 uint16_t ADC_Read_Filtered(uint8_t ch) { uint16_t buf[SAMPLE_NUM]; uint32_t sum = 0; uint16_t max = 0, min = 0xFFFF; for (uint8_t i = 0; i < SAMPLE_NUM; i++) { buf[i] = ADC_Read_Channel(ch); // 单通道单次转换 sum += buf[i]; if (buf[i] > max) max = buf[i]; if (buf[i] < min) min = buf[i]; } // 去掉一个最大值和一个最小值,抑制尖峰干扰 return (uint16_t)((sum - max - min) / (SAMPLE_NUM - 2)); }

SAMPLE_NUM取 8 是采样速度和稳定性的折中,取太大主循环周期会被拉长,取太小滤波效果有限。ADC_Read_Channel内部要保证采样时间足够,电感信号经过运放后输出阻抗不低,采样时间太短会采不准。滤波后的值再送进归一化:用“当前值减最小值除以最大值减最小值”,把不同通道拉到同一量纲,否则左右电感灵敏度不一致,偏差永远算不对。

3.2 偏差计算与打角策略

偏差计算常见有两种:一种是“左右电感差值除以和值”,得到 -1 到 1 之间的归一化偏差;另一种是多电感加权,适合电感数量多、赛道曲率变化大的场景。这份代码里用的是差值归一化加分段打角,逻辑直观,调参也容易。核心片段如下:

float Get_Track_Error(void) { uint16_t l = ADC_Read_Filtered(ADC_CH_LEFT); uint16_t r = ADC_Read_Filtered(ADC_CH_RIGHT); float err; if ((l + r) < 100) { // 两个电感都没信号,可能脱线 return last_error; // 保持上一次偏差,避免猛打角 } err = ((float)l - (float)r) / ((float)l + (float)r); last_error = err; return err; } void Track_Task(void) { float err = Get_Track_Error(); int16_t angle = PID_Calc(&steer_pid, err); // 输出舵机打角增量 Servo_Set_Angle(angle); }

Get_Track_Error里那个“信号太弱就保持上次偏差”的判断很关键,脱线瞬间如果直接按 0 处理,舵机会回正,车反而冲出去。PID_Calc的输入是归一化偏差,输出限幅到舵机机械行程内,常见是 -30 到 30 度。PID 参数我一般先只上 P,让车能大致跟线,再加 D 抑制摆动,I 项在循迹里用得少,除非赛道有持续单边坡度。调参时把车架空,手动把车头往左往右偏,看舵机响应是否跟手,比直接下地跑效率高得多。

3.3 电机差速与速度闭环

舵机负责方向,电机负责速度。差速的常见做法是“基础速度 + 偏差修正”:偏差为正时左轮减速右轮加速,偏差为负时反过来。如果只做开环占空比,电池电压一下降车就变慢,循迹手感会飘。所以工程里通常还会用编码器做速度闭环,下面是一个增量式 PID 的速度环片段:

int16_t Speed_PID(int16_t target, int16_t actual) { int16_t err = target - actual; static int16_t last_err = 0; int16_t p = KP * err; int16_t d = KD * (err - last_err); last_err = err; return p + d; // 输出叠加到基础占空比上 }

target来自“基础速度 + 偏差修正”,actual来自编码器在固定周期内读到的脉冲数。KP、KD的量纲要和脉冲数量级匹配,直接抄别人的参数大概率翻车,因为编码器线数和采样周期不同。实际调试时先把KD置 0,调KP到轮速能跟上目标且不持续震荡,再加KD压超调。

4. 附加功能实现:蓝牙遥控、里程累计与定圈停车

4.1 蓝牙串口指令解析

蓝牙模块一般透传,单片机收到的就是一串字符。常见协议是单字符指令,比如F前进、B后退、L左转、R右转、S停止、A切自动循迹。解析时不要在主循环里死等,用中断接收加环形缓冲更稳。下面是一个简化的解析函数:

void Bluetooth_Parse(uint8_t ch) { switch (ch) { case 'F': mode = MODE_MANUAL; manual_speed = 600; break; case 'B': mode = MODE_MANUAL; manual_speed = -600; break; case 'L': manual_steer = -20; break; case 'R': manual_steer = 20; break; case 'S': mode = MODE_STOP; manual_speed = 0; break; case 'A': mode = MODE_AUTO; break; // 切回自动循迹 default: break; } }

mode是全局状态机,手动模式下循迹任务不输出舵机,避免两边抢控制权。manual_speed的正负代表方向,实际写进 PWM 时要取绝对值并设置方向引脚。串口中断里只做“收字节进缓冲”,解析放到主循环,这样不会因为解析耗时导致丢字节。

4.2 里程累计与定圈判断

测线路长度有两种常见来源:编码器脉冲累计,或者按“速度 × 时间”估算。前者准但需要编码器,后者简单但受打滑影响。代码里如果是编码器方案,通常在一个固定周期中断里把左右轮脉冲累加,再乘以轮周长换算成距离:

void Odometer_Update(int16_t left_pulse, int16_t right_pulse) { float dist_l = left_pulse * MM_PER_PULSE; float dist_r = right_pulse * MM_PER_PULSE; total_distance += (dist_l + dist_r) * 0.5f; // 取平均,抵消差速 } uint8_t Lap_Check(float lap_length) { if (total_distance >= lap_length * lap_count) { lap_count++; return 1; // 到达一圈 } return 0; }

MM_PER_PULSE等于轮周长除以每圈脉冲数,这个值必须实测:让车直线走一段已知距离,读累计脉冲反推,比按理论值算靠谱。Lap_Check返回 1 时,上层把电机占空比清零并进停车状态。定圈数lap_count的目标值建议做成宏或可配置变量,方便赛前改。

4.3 状态机把功能串起来

蓝牙、循迹、里程、定圈这几个功能如果各写各的if,很容易互相打架。常见做法是用一个顶层状态机:IDLE、MANUAL、AUTO、STOP。自动模式下才跑循迹和里程累计,手动模式下只响应遥控,停车状态屏蔽所有电机输出。状态切换集中在蓝牙解析和定圈判断两处,逻辑清晰,也方便后面加“暂停”“重新计圈”之类的功能。

5. 避坑与常见问题:调试现场最容易翻车的几件事

5.1 车上电就猛冲

现象:一给电,电机直接满速转。原因多半是 PWM 定时器初始化后比较寄存器默认值不为 0,或者方向引脚初始电平不对。解决:初始化最后统一调用Motor_Stop(),把占空比写 0、方向引脚拉低,再进主循环。

5.2 循迹左右摆个不停

现象:车能跟线但持续画龙。原因通常是滤波不足、PID 的 D 项过大,或者左右电感灵敏度不一致。解决:先确认归一化做了没有,再把 D 项减半试跑,最后检查两个电感在中心线正上方时读数是否接近。

5.3 蓝牙遥控时好时坏

现象:按了没反应,或者连按几次才动。原因常见是串口中断里做了耗时解析导致丢字节,或者蓝牙模块波特率和代码不一致。解决:中断只收字节进环形缓冲,解析放主循环;用串口助手先确认模块波特率,再对代码里的BaudRate。

5.4 里程和实际距离对不上

现象:定圈总是提前或延后停。原因多是MM_PER_PULSE用了理论值,或者差速时只取了单侧脉冲。解决:实测标定该系数,累计时取左右平均;如果赛道有打滑,适当留一点余量。

5.5 脱线后车不减速反而冲出去

现象:车头偏出赛道,速度不降。原因是没有做“信号丢失”判断,偏差保持逻辑让舵机维持旧角度。解决:在Get_Track_Error里加信号强度阈值,连续多次低于阈值就进保护状态,降速或停车。

6. 二次开发怎么改:从参数标定到功能扩展的实操思路

拿到这份代码后,我建议先别急着加功能,先把三个基础量标定准:电感归一化区间、MM_PER_PULSE、舵机中位与限幅。这三个不准,后面加什么功能都是空中楼阁。标定电感时把车放在赛道中心线上,记录各通道读数,再分别把车头左偏、右偏到极限位置记录读数,用这些值做归一化上下限。舵机中位用“给固定角度看轮子是否回正”的方式确认,限幅要留机械余量,别让舵机顶死发出嗡嗡声。

功能扩展上,比较有价值的改法有三个方向。第一,把分段打角换成完整 PID 加前馈,弯道提前打角,直道更稳;第二,把定圈停车扩展成“分段限速”,比如第一圈慢速热身、第二圈提速,用一个圈数计数器驱动目标速度表;第三,把蓝牙协议从单字符升级成带校验的短帧,比如$F,600#,方便上位机调参。改协议时注意接收缓冲要能容纳整帧,解析用状态机而不是strstr硬找。

验证方法上,我习惯分三层:架空测舵机和电机响应,低速下地测循迹,再逐步提速测定圈。每改一个参数只动一个变量,改完记录一次现象,不然出了问题根本不知道是哪次改动引入的。下面这张表是我常用的调试记录格式,可以直接抄:

改动项改动前改动后现象结论
舵机 KP812直道更跟手,弯道略抖保留,补 D
速度 KP35起步更冲回退到 4
滤波窗口48摆动减小,响应变慢保留

最后说个血泪经验:这份代码注释再全,也别跳过“先架空、再低速、后提速”的顺序。我有一次图省事直接满速下地,结果脱线保护没生效,车一头撞上桌腿,舵机齿轮当场扫齿。从那以后我每次改完参数都强制走一遍架空验证,确认舵机和电机响应正常才放地上跑。希望这份拆解能帮你少走几个弯路,把这份代码真正跑成自己的车。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进