STM32电磁循迹小车从原理到实战:完整代码与PID调试经验
2026/9/9 19:20:12 网站建设 项目流程

简介:这份STM32工程是针对电磁循迹小车赛题的完整代码包,适合嵌入式初学者及电赛备赛者参考。工程以库函数版例程为基础,覆盖电机驱动、电磁传感器数据读取、PWM调速、转向控制等核心模块,并额外集成蓝牙遥控、线路长度测量与定圈停止功能。代码中带有详细注释,便于理解STM32初始化、UART通信、中断服务程序及编码器脉冲计数等关键逻辑。压缩包约9MB,共249个文件,主要包括46个h头文件、44个c源文件、45个o目标文件以及hex、uvprojx、map等工程配置文件,可直接在Keil环境中查看或烧录。已有6710人学习下载,资源目录层次清晰,从硬件配置到功能实现均有对应代码模块。通过研读该例程,读者不仅可以掌握电磁循迹小车的基本控制思路,还能学习蓝牙遥控指令解析、里程计算和自动停圈设计,为后续嵌入式项目开发提供可直接复用的代码框架。 STM32电磁循迹小车我陆陆续续做了大半年,中间推倒重来了三轮,才把代码和硬件折腾到能稳定跑完一圈的程度。最近整理资料时发现这套“带详细注释的最终完整代码”包含了蓝牙遥控、测线路长度、定圈停止这些附加功能,不少同学私下找我要,索性把整个设计思路、代码逻辑和调试过程中踩过的坑一次性写清楚。这篇文章不止给你代码,还会讲清楚每个模块“为什么这样写”、硬件上“为什么这样接”,以及我在调参和排错时积累的实战经验。

先说清楚这套代码适合谁。如果你是刚接触STM32、想做智能车竞赛或者课程设计,这套代码能帮你省掉大量查资料和试错的时间。如果你已经有一辆能跑的车但总是不稳定,这篇文章里的归一化处理、PID参数整定和滤波设计思路也会对你有用。代码基于STM32F103C8T6,使用标准库开发,兼容性好,移植到其他型号也不难。

1. 电磁循迹的核心原理:为什么用8个电感而不是摄像头

先讲透电磁循迹的原理,否则你拿着代码也调不好。

电磁循迹的基本思路是:赛道中心线下方铺设通有20kHz交变电流的导线,导线周围会产生交变磁场。小车前后左右布置电感线圈,根据法拉第电磁感应定律,线圈中会感应出与距离相关的电动势,通过分析多个电感的感应强度差值,就能计算出小车相对于赛道中心线的偏移量。

很多新手会问:现在视觉循迹这么成熟,为什么还要做电磁?原因有三点:一是电磁方案不受光照影响,室内室外、白天晚上都能跑,摄像头在强光和弱光环境下表现差异极大;二是电磁传感器成本极低,几个工字电感加运放滤波电路就能搞定,摄像头模块动辄几十上百元;三是电磁方案的数据率可以做得很高,ADC采样速度远高于图像处理帧率,控制环路可以跑得更快。

电感布局我最终采用的是“水平八字+垂直检测”的方案:车头前方左右两侧各放一个水平电感(检测左右偏差),车头中央靠后位置放一个垂直电感(检测是否在赛道正上方)。左右水平电感之间的电压差就是偏差信号,垂直电感用于辅助判断和过十字弯道。

这里要特别提醒一个新手常犯的错误:不要把电感放得太靠后。我第一版把电感装在车尾附近,结果小车过弯时电感还没到达弯道入口,转向已经晚了,车头冲出赛道才知道拐弯。后来把电感移到车头最前端,过弯响应明显及时。电感离地高度也有讲究,我实测下来8-15mm效果最好,离地太低容易刮到赛道表面,太高信号幅值衰减严重。

信号调理电路方面,20kHz的交流感应信号不能直接进ADC,需要经过检波和滤波。我用的是经典的“二极管检波+RC低通滤波”方案:感应电压经过1N4148半波整流,再用10uF电容和10k电阻组成低通滤波,把20kHz交流分量滤掉,留下平滑的直流电平送入STM32的ADC引脚。这个电路简单可靠,唯一需要注意的是RC时间常数,我取的是10k*10uF=100ms,这个值不能太大,否则信号响应太慢,小车高速入弯时会因为信号滞后而冲出赛道。

2. 硬件选型与系统架构:从最小系统到整车的完整清单

硬件方案决定了代码能跑多稳,这里给出我最终定型的整车硬件配置和选型理由。

主控芯片选用STM32F103C8T6,就是俗称的“蓝丸”核心板。选择理由很直接:主频72MHz足够跑PID控制和蓝牙协议解析,内置12位ADC可以直接采集电感信号,3路USART分别给蓝牙、调试串口和预留通讯,而且资料生态极其丰富,出任何问题都能搜到解决方案。成本十几块钱,坏了也不心疼。

电机驱动我用了TB6612FNG,相比老式L298N,它的内部压降更小,发热更低,PWM频率可以跑到20kHz以上不衰减。配合两颗N20微型减速电机(减速比30:1)和两路正交编码器,可以实现闭环速度控制。编码器每圈输出的脉冲数不多,但对于智能车这种应用完全够用。

蓝牙模块选用HC-05,工作在从机模式,通过串口3与STM32通信。蓝牙遥控这个附加功能做成“双模式切换”:当蓝牙发送特定指令时,小车从“循迹模式”切换到“遥控模式”,此时电机控制权交给手机APP方向键;再发送指令切回循迹模式。这个设计在调试时特别有用,车跑偏了可以立刻切回手动控制把车开回赛道,不用每次跑去车头捡车。

供电系统是很多人忽略的坑。我的方案是:7.4V 2S锂电池直接给电机驱动供电,经AMS1117-3.3降压给STM32和传感器供电,蓝牙模块单独用5V供电(从电池经LM2596降压)。注意数字地和模拟地要单点连接,否则ADC采样值会抖动得让你怀疑人生。

整车系统框图其实非常简单清晰:电感传感器阵列 -> 检波滤波电路 -> STM32 ADC采样 -> 软件滤波+归一化 -> PID计算 -> PWM输出 -> TB6612 -> 直流电机。编码器脉冲 -> STM32定时器正交解码 -> 速度PID -> 叠加到PWM输出。蓝牙 -> 串口中断 -> 命令解析 -> 模式切换/目标速度设定。

3. 代码架构与核心算法:ADC采样、滤波、归一化、PID一网打尽

这是整篇文章的硬核部分。代码整体采用前后台架构,主循环里跑循迹算法和PID控制,定时器中断里跑编码器测速和速度闭环。

ADC采样我使用ADC1的通道0-2,分别对应左电感、右电感、垂直电感。采样方式采用DMA连续采样,每次采集20个样本然后取平均值,这样可以滤除高频噪声。这里有个经验值:DMA采集要设置足够长的转换时间,我使用ADC采样周期为55.5个时钟周期(ADC_SAMPLETIME_55CYCLES_5),加上软件平均,ADC读数非常稳定。

软件滤波我做了两层:第一层是限幅滤波,如果某次采样值与前一次差值的绝对值超过阈值,就丢弃这次采样——这能有效剔除电火花等干扰造成的异常尖峰;第二层是一阶低通滤波,公式是filtered_value = 0.7 * new_sample + 0.3 * previous_filtered。这个0.7和0.3的比例就是滤波系数,系数太大响应快但噪声大,系数太小平滑但迟钝,你可以根据实测波形微调。

归一化处理是电磁循迹算法里最重要的一个环节,没有之一。原始ADC值是绝对电压,它受电池电压波动、环境电磁干扰、赛道不同区段的导线深度差异影响很大。如果你直接拿绝对差值做PID输入,就会出现“同一辆车、同一个偏差、不同时间输出电压不同”的诡异现象。归一化的做法是实时记录左右电感ADC的最大值和最小值,然后计算:

normalized_left = (left_adc - min_left) / (max_left - min_left) normalized_right = (right_adc - min_right) / (max_right - min_right) error = normalized_right - normalized_left

这样得到的error值在-1到+1之间,是一个无量纲的相对偏差量。无论电池电压从8.4V掉到7.0V,还是赛道上电感读数整体漂移,归一化后的误差值都能保持稳定。这个处理让我省掉了大量重新调参的功夫,强烈建议你务必实现。

PID控制部分,转向环我用的是PD控制,公式是:

steering_output = Kp * error + Kd * (error - previous_error)

为什么不用积分项?因为电磁循迹是一个稳态误差本来就接近零的系统——车正对赛道中心线时,左右电感对称,误差天然为零。引入积分项反而会造成积分饱和,车子在长直道上会左右震荡。如果你实在想要积分来消除静差,一定要做积分限幅,把积分项限制在输出量的20%以内。

速度环我单独做了一个PI控制器,目标速度由模式决定:循迹模式默认设定为1.5m/s(通过编码器测速换算成脉冲频率),入弯检测到误差绝对值大于阈值时自动降速到0.8m/s,出弯恢复。方向PD输出和速度PI输出叠加后限制在PWM死区以上、满占空比以下,再分别配置到左右两个电机的PWM寄存器。注意左右电机的安装方向是镜像的,所以其中一个电机的占空比方向和另一个相反,代码中要对编码器方向符号做处理,否则速度闭环会飞车。

4. 完整代码走读:主循环、寻迹算法、PID输出的逐行注释

这一节我直接贴核心代码并逐段解释。完整工程代码比较长,我在这里挑最关键的三段:主循环逻辑、偏差计算函数、PID输出函数。这些代码都经过实际跑车验证,可以直接移植。

主循环的逻辑非常简单,核心就三步:采样、算偏差、算输出。

int main(void) { /* 初始化各类外设 */ SystemInit(); Delay_Init(); ADC_DMA_Init(); // ADC + DMA 采样初始化 TIM_Encoder_Init(); // 定时器编码器模式初始化 TIM_PWM_Init(); // PWM输出初始化 USART_Init(); // 调试串口和蓝牙串口初始化 PID_Init(); while(1) { /* 1. 从DMA缓冲区取ADC原始值 */ uint16_t left_adc_raw = adc_buf[0]; uint16_t right_adc_raw = adc_buf[1]; uint16_t vertical_adc_raw = adc_buf[2]; /* 2. 执行限幅滤波 + 一阶低通滤波 */ Left_ADC = LowPass_Filter(left_adc_raw, &Left_Last, LEFT_LIMIT); Right_ADC = LowPass_Filter(right_adc_raw, &Right_Last, RIGHT_LIMIT); Vertical_ADC = LowPass_Filter(vertical_adc_raw, &Vertical_Last, VERTICAL_LIMIT); /* 3. 标准化/归一化处理,得到-1到1之间的无单位偏差 */ float error = Calculate_Error(Left_ADC, Right_ADC); /* 4. 根据偏差计算方向PD输出 */ float steering = PD_Calculate(error); /* 5. 检测弯道,决定是否降速 */ if (fabs(error) > 0.5f) { target_speed = SLOW_SPEED; // 入弯降速 } else { target_speed = FAST_SPEED; // 直道加速 } /* 6. 速度PI控制(在定时器中断中周期性执行),输出左右PWM */ Set_Motor_Speed(steering); /* 7. 处理蓝牙命令和屏幕调试信息(如果有) */ Process_Bluetooth_Command(); Debug_Print_Status(); Delay_Ms(5); // 主循环周期约5ms,即控制率200Hz } }

上面这段代码里,Delay_Ms(5)把主循环频率控制在200Hz。为什么要这个频率?因为电磁信号的物理带宽和多旋翼控制一样,200Hz已经足够覆盖赛道变化率,同时又不会因为控制周期太短导致微分项放大噪声。如果你发现车在直道上蛇形,多半是PD的D项过大,或者主循环周期不稳定。

偏差计算函数是核心中的核心,我这里写成独立函数方便你调试时加打印输出:

/** * @brief 归一化并计算循迹偏差 * @param left_raw 左电感滤波后的ADC值 * @param right_raw 右电感滤波后的ADC值 * @retval 偏差值,范围约[-1, +1] * * 逻辑说明: * 1. 记录历史最大值/最小值,用于实时归一化 * 2. 用 min-max 公式把原始ADC值映射到[0,1] * 3. 计算 right - left,车偏右时 right 小 left 大,误差为负 * 车偏左时 right 大 left 小,误差为正 */ float Calculate_Error(uint16_t left_raw, uint16_t right_raw) { // 动态更新最大值和最小值,初始值和上下限需要根据实际环境微调 static uint16_t left_max = 3500, left_min = 0; static uint16_t right_max = 3500, right_min = 0; if (left_raw > left_max) left_max = left_raw; if (left_raw < left_min) left_min = left_raw; if (right_raw > right_max) right_max = right_raw; if (right_raw < right_min) right_min = right_raw; // 防止除零 if ((left_max - left_min) < 100) left_max = left_min + 100; if ((right_max - right_min) < 100) right_max = right_min + 100; // 归一化到0~1 float left_norm = (float)(left_raw - left_min) / (left_max - left_min); float right_norm = (float)(right_raw - right_min) / (right_max - right_min); // 限幅到[0,1] if (left_norm > 1.0f) left_norm = 1.0f; if (left_norm < 0.0f) left_norm = 0.0f; if (right_norm > 1.0f) right_norm = 1.0f; if (right_norm < 0.0f) right_norm = 0.0f; return right_norm - left_norm; }

这段代码里的动态最大值最小值更新有个隐患:如果小车长时间停在起跑线,电感信号几乎为零,left_min会慢慢被更新成0,而left_max保持一个较大的值,这没问题。但如果小车被拿起来断电后on赛道又不在导线上方,电感信号只有噪声,left_min会逼近left_max,归一化后会出现随机跳变。我的解决办法是加一个判定:如果左右电感的原始值都低于某个阈值(比如200),就认为小车不在赛道上,此时强制error=0并输出零转向,让小车直行“找回”赛道。

PD输出函数同样独立,方便你直接在串口监视器里看到PD的输出变化:

/** * @brief 位置式PD控制器,用于转向环 * @param err 归一化偏差 * @retval 转向输出,范围[-100, +100],正值表示向右转 * * 调参经验: * Kp 先从小往大调,直到直道稳定不蛇形。 * Kd 用于抑制过冲,弯道出口如果左右摆动,先加Kd。 */ float PD_Calculate(float err) { static float last_err = 0.0f; float derivative = err - last_err; last_err = err; // 这两个参数是我在1.5m/s速度下的整定值,仅供参考 float Kp = 35.0f; float Kd = 8.0f; float output = Kp * err + Kd * derivative; // 输出限幅,防止电机瞬间满转 if (output > 100.0f) output = 100.0f; if (output < -100.0f) output = -100.0f; return output; }

速度PID你可以在网上找到很多成熟代码,我这里只强调一个工程细节:编码器数据要放在定时器更新中断里读取,而不是在主循环里轮询。因为主循环时不时会被蓝牙命令处理阻塞几十毫秒,在这期间编码器脉冲会被漏数,导致速度估计偏低。我把定时器配置为100Hz中断,每次中断累加当前脉冲数,然后清零硬件计数寄存器,这样即使主循环卡了,速度环依然稳定更新。

5. 附加功能实现:蓝牙遥控状态机、测线路长度、定圈自动停止

这节讲标题里提到的三个“小功能”,每一个的实现我都给出关键代码和设计思路。

蓝牙遥控功能我实现成完整的状态机,而不是简单的if-else。为什么用状态机?因为遥控模式和小车循迹模式之间切换时机很关键:如果小车正在循迹高速行驶,突然切换成遥控模式,此时方向盘输出必须立即归零,否则飞车。反过来,从遥控切回循迹时,需要先让车停稳再启动循迹。状态机可以清晰地表达这种约束关系。

状态机定义三个状态:MODE_LINE_FOLLOW(循迹模式)、MODE_REMOTE_CTRL(遥控模式)、MODE_STOP(急停模式)。蓝牙指令F进入循迹模式,R进入遥控模式,S进入急停模式。每个模式进入时都会执行各自的初始化动作,比如遥控模式进入时立即清零PID历史值、设置PWM为0。

测线路长度功能用编码器累计值除以每圈脉冲数再乘以轮胎周长实现。关键在于精度:N20电机自带编码器一般是每圈11个脉冲,配合减速比30:1,输出轴每转约330个脉冲。轮胎直径我量的是65mm,周长约204mm,所以每个脉冲对应的行驶距离是0.62mm。每次进入循迹模式时清零累计值,在循迹模式下每100ms读取一次脉冲差值,累加进总里程,同时在OLED屏上实时显示。实测跑标准一圈(约50米)误差在20厘米以内,精度足够。

定圈停止功能需要和测长度配合,我增加一个过圈检测机制:用垂直电感检测起始线。垂直电感在正常赛道上信号较强,而在起跑线十字交叉处信号会急剧增大到正常值的两倍以上,利用这个特征可以判定“过线”。核心逻辑如下:

  • 定义状态:NOT_COUNTED(未开始计数)、COUNTING(正在计数)、FINISHED(已完成)。
  • 入圈前发送蓝牙指令G进入定圈模式,此时系统记录当前过线标志。
  • 每当垂直电感信号超过阈值时,认为过了一次起跑线,圈数加1。
  • 当圈数达到设定的目标值(比如测试三圈),立即执行刹车程序:PWM输出归零并拉高刹车引脚,车停下后通过蓝牙发送完成提示,同时点亮LED。

这个功能的难点不是代码,而是过线检测的阈值标定。垂直电感的ADC值在不同赛道上差异很大,我这里的做法是动态跟踪垂直电感的“普通区平均值”,然后设定阈值为average * 1.8。如果1.8倍触发不了,就降为1.5倍;如果正常行驶时经常误触发,就调高到2.0倍。你根据实际打印出来的数据灵活调整。

6. 调试方法总结:串口波形观察、参数整定顺序、常见故障案例

最后这节分享调试经验,这部分内容常规教程很少写,但对你能不能把车调好至关重要。

我强烈建议在调试阶段用串口把原始ADC值、归一化误差值、PD输出值这些中间量以CSV格式发到上位机,然后在电脑上用串口绘图工具(我用的是VOFA+)观察变化曲线。很多时候车跑不好,你用眼睛看不出问题,但打开曲线图一眼就能发现:是不是采样值噪声太大?是不是归一化后误差在零点附近震荡?是不是PD输出饱和了?这些都能从波形上直接判读。

参数整定顺序遵循“先内环后外环、先转向后速度”的原则。需要强调一点:调试PD参数时,最好让速度固定在一个较低值,否则速度变化会干扰你对转向参数的判断。先用直道调Kp,把Kp从小到大增加,直到直道上车不再明显蛇形;然后过弯调Kd,Kd从小往大加,观察弯道出口是否出现左右摇摆。如果出口摇摆是过冲,加大Kd;如果入弯反应慢,减小Kp(因为Kp过大反而会导致弯道内转向过猛,车头甩出去)。

我调试过程中遇到过一个非常经典的故障:小车在直道上每隔两秒就会突然向左猛拐一下。排查了很久,最后发现是蓝牙模块的天线靠近了左电感,蓝牙2.4GHz的射频信号对20kHz的感应信号产生了间歇性干扰,导致左电感ADC偶尔出现异常尖峰。限幅滤波虽然能拦住大部分尖峰,但蓝牙跳频时产生的干扰幅度特别大,超过了限幅阈值。解决办法很简单:把蓝牙模块移到车尾,远离所有电感,并且给电感加了铝箔屏蔽罩。这里提醒大家,电磁循迹对电磁干扰非常敏感,无线模块的位置摆放要慎重,最好一开始就布局在车尾。

还有一个常见的坑是ADC参考电压不稳。STM32的VREF+引脚如果直接接3.3V而电池电压波动,ADC采集值就会跟着漂。我用的是核心板,VREF已经接好了,无法单独供电,所以归一化处理尤为重要。如果后续你想提高精度,可以外接一个精密基准电压源(比如TL431)给VREF+,这样做完归一化之后,线性度会更好。

关于代码注释——原工程注释非常充足,每一个函数、每一行关键逻辑都有注释说明,包括为什么这样写、修改某个参数的后果等。很多同学下载代码后不知道怎么改参数,我在每个可调参数处都加上了注释,比如#define Kp 35.0f // 比例系数,直道蛇形时减小,弯道迟钝时增大。建议你先原封不动烧录跑通,再逐步调整参数,体验每个参数对车的影响。

最后分享一个经验:电磁循迹小车调好的秘诀不在于代码本身有多复杂,而在于你对每个环节的理解有多深。把“为什么用归一化”“为什么用PD不用PID”“为什么电感要放在车头最前端”这些问题想透,出了问题才能快速定位是硬件问题还是算法问题。我这份代码不是终点,而是一个可靠的起点,希望你在此基础上做出更极致的小车。如果调试过程中遇到解不开的坑,欢迎带着数据和我交流,实测中我把左右电感的距离从15cm改到20cm,转向PD参数就完全变了——这类具体问题,光靠公式是算不出来的。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询