news 2026/10/17 7:22:02

STM32电磁循迹小车:从电感采集到PID控制的完整实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32电磁循迹小车:从电感采集到PID控制的完整实现

简介:面向恩智浦智能车竞赛与嵌入式控制学习者的电磁循迹小车完整工程包,围绕STM32四轮小车平台,详解电感采集、PID参数整定与路径跟踪实现。压缩包共192个文件,以C源码和头文件为主(47个.h、43个.c),便于模块化阅读与移植;另含编译生成的.o/.d中间文件、Keil工程配置、hex烧录文件及辅助脚本,整体仅5.76MB,在保留开发痕迹的同时便于快速二次开发。已有4687人学习下载,内容适合从底层驱动到上层算法的系统对照。包内不仅包含MPU6050姿态融合、定时器PWM输出等驱动代码,还提供完整PID调节逻辑与备赛调试记录,可帮助读者理解比例、积分、微分项对循迹稳定性的实际影响,掌握在复杂赛道中动态调整参数的方法,并针对舵机转向、电机速度的协同控制形成可复用的调参思路。

1. 电磁循迹小车:不是看赛道,是“摸”磁场

第一次拿到这套电磁循迹小车的工程时,我第一反应是“摄像头呢”——习惯了循迹车必须有图像输入,结果整个工程里只有一个ADC采集函数在反复读三个电感值。跑起来才明白,电磁循迹的底层逻辑跟视觉循迹完全不一样:赛道中心线底下埋着一根通交变电流的导线,小车靠车头一排电感去“摸”导线产生的磁场,从左右电感感应电压的差里算自己偏了多少,再交给PID调节去把偏差拉回来。整套工程的核心就落在两块:电感采集与差比和计算、PID方向环与差速输出。适合准备智能车竞赛、做STM32嵌入式控制入门,或者想研究经典PID怎么落到真实硬件上的开发者。

2. 整体架构:从磁场信号到车轮转速的完整链路

2.1 电磁循迹原理:交变磁场与感应电动势

电磁循迹赛道的标准做法,是在赛道中轴线下铺设一根导线,通以 100kHz 左右的正弦交变电流。交变电流会在导线周围激发交变磁场,车头安装的工字电感在磁场中会产生感应电动势,电动势的大小与电感所在位置的磁感应强度成正比。

离导线越近,感应电压越高;离得越远,电压越低。所以只要在车头左、中、右水平放置三个电感,当车跑偏时,左右两个电感的感应电压就会拉开差距。这个电压差就是小车“感知”到的赛道偏移量。

关键点在于,这套方案肉眼看不见赛道,完全依赖磁场强度梯度。这意味着它对电感的安装高度、左右对称性、以及信号采集的稳定性都非常敏感。稍后讲到的避坑章节里,很多问题根源就在这——电感增益不一致、电机磁场干扰,都会让整车表现像“玄学”一样难以捉摸。

2.2 硬件链路:电感、运放、MCU与电机驱动

从物理世界到数字世界的完整信号链是这样走的:

赛道导线(100kHz交变电流) → 工字电感感应 → 并联电容选频谐振 → 运放放大 → 二极管检波 → 直流电压 → STM32 ADC采集 → 软件滤波 → 差比和计算 → PID控制器 → PWM输出 → 电机驱动

这中间有一个容易被忽略的硬件细节:电感出来的信号是交流的,幅值可能只有几十毫伏到几百毫伏,如果直接送进STM32的ADC,分辨率会非常差。常见做法是让电感并联一个电容构成 LC 谐振回路,谐振点对准 100kHz,把有用信号“挑”出来,再经过一级运放把电压放大到 ADC 能分辨的范围(例如 0~3.3V)。

检波环节也值得注意。ADC 只能采正电压,而交流信号有正有负。一般方案会用工字电感检波电路,或者用运放做全波整流,把交流信号变成缓慢变化的直流电平,ADC 才能稳定读取。这套工程里的 adc.c 就是负责把这三个通道的直流电平读回来,变成可计算的原始数值。

2.3 软件模块划分:采集、滤波、控制、执行四层

拿到压缩包解压之后,工程内部的代码组织方式并不复杂,但分层非常清晰。如果按功能划分,大致可以分成四个模块,对应四个.c文件:

project/ ├── Core/ # 标准外设库或HAL库的时钟、中断配置 ├── Electromagnetic/ │ ├── adc.c # 三路电感ADC采集,DMA循环搬运 │ ├── filter.c # 滑动平均滤波,消除信号毛刺 │ ├── pid.c # 位置式PID控制器,转向环与速度环共用 │ ├── motor.c # PWM输出映射,左右轮差速控制 │ └── control.c # 主控制逻辑:采集→滤波→算偏差→PID→输出 ├── MDK-ARM/ # MDK工程文件入口 └── USER/main.c # 初始化与主循环调度

这种分层的价值在于,每一层都可以独立调试。采集层只负责把 ADC 原始值变成可靠的电压数据,控制层只负责吃一个规范化误差值、吐一个转向量,执行层只负责把转向量映射成 PWM 占空比。以后想换传感器(比如从三电感换成五电感),或者想换执行器(比如从普通直流电机换成带编码器的电机),都只需要替换对应模块,不需要把 main 函数重写一遍。

3. 核心代码拆解:从ADC原始值到PWM占空比的完整计算链

3.1 电感采集:多通道ADC扫描与滑动平均滤波

三个电感的电压信号分别接到了STM32的三个ADC通道上。最常见的接法是左电感接ADC1_IN0,中电感接ADC1_IN1,右电感接ADC1_IN2,对应的引脚是 PA0、PA1、PA2。

#define ADC_CH_NUM 3 #define ADC_BUF_SIZE 8 uint16_t raw_adc[ADC_CH_NUM]; // 原始采集值 uint16_t smooth_adc[ADC_CH_NUM]; // 滤波后的值 void Adc_Init(void) { // 使能ADC1和GPIOA时钟 // PA0/PA1/PA2配置为模拟输入模式 // ADC1配置为:独立模式、规则通道、右对齐、采样周期拉到最大 // 开启ADC1的DMA循环模式,每次采集完三个通道自动搬运到raw_adc } void Adc_Update(void) { // 等待DMA传输标志位 // 将raw_adc拷贝到smooth_adc前,先过一层滑动平均 for (int ch = 0; ch < ADC_CH_NUM; ch++) { uint32_t sum = 0; for (int i = 0; i < ADC_BUF_SIZE; i++) { sum += raw_adc[ch]; } smooth_adc[ch] = sum / ADC_BUF_SIZE; } }

这里有两个值得说明的参数:第一,ADC 采样周期要尽可能拉长,常规配置里采样周期取 239.5 个 ADC 时钟周期。电磁信号的频率是100kHz,如果采样太快反而会把信号波形上的高频细节采进来,而这个采样周期配 DMA 循环模式,能在硬件层面保证三路数据的时间一致性。第二,滑动平均窗口大小决定了响应速度,窗口越大,数据越平稳,但循迹延迟也越大。我这边的经验是 8 次平均比较稳妥,既压得住赛道接缝处的毛刺,又不会让小车在入弯时明显迟钝。

3.2 差比和归一化:把电压差变成赛道偏离量

拿到三个电感的滤波值之后,下一个关键步骤是把电压值转换成“偏了多少”的误差量。这里最常踩的方案是用左右电感的差值直接当误差,比如error = left - right。但在实际跑车时,这个做法会让控制量随电感绝对幅值浮动——车离导线远了,左右电压同时变小,同样的物理偏移产生的差值也会变小,导致远处和近处的灵敏度不一致。

更好的做法是差比和归一化,公式是:

float Calculate_Error(void) { float left = (float)smooth_adc[0]; // 左电感 float mid = (float)smooth_adc[1]; // 中电感 float right = (float)smooth_adc[2]; // 右电感 float error = (left - right) / (left + right + 1e-6f); return error; // 范围约在 [-1, 1] }

把差值除以左右电感的和,相当于做了一个自动增益控制:当车距离导线较远、整体电压偏低时,除法会把误差归一化回一个稳定的量级,这样 PID 参数在不同的赛道高度、不同的电感离地距离下都能复用。代码里的1e-6f是防止左右电感同时为零时出现除零错误。

中间电感在这个公式里没有直接参与误差计算,但它的值有另外一个用途——判断小车是否已经严重偏离赛道。如果中电感的电压掉到了某一个阈值以下,说明车头已经远离中心导线,这时候可以强制让转向输出加大,或者切到“扫描找回”状态,防止直接冲出赛道。

3.3 PID控制器:位置式PD转向环与限幅

这套工程的PID实现是经典的位置式PID,结构体里把比例、积分、微分三项系数和内部状态打包在一起,可读性很好,也方便后续做参数动态切换。

typedef struct { float kp; // 比例系数 float ki; // 积分系数 float kd; // 微分系数 float integral; // 积分累计值 float last_error; // 上一次误差,用于计算微分项 float out_max; // 输出限幅 float integral_max;// 积分限幅 } PID_t; float PID_Calc(PID_t *pid, float error) { // 积分项累计,并做限幅,防止积分饱和 pid->integral += error; if (pid->integral > pid->integral_max) pid->integral = pid->integral_max; if (pid->integral < -pid->integral_max) pid->integral = -pid->integral_max; // 微分项:当前误差 - 上一次误差 float diff = error - pid->last_error; pid->last_error = error; // 位置式PID输出 float out = pid->kp * error + pid->ki * pid->integral + pid->kd * diff; // 输出限幅 if (out > pid->out_max) out = pid->out_max; if (out < -pid->out_max) out = -pid->out_max; return out; }

对于电磁循迹这种场景,转向环通常只用 PD,不用积分。原因是积分项对持续存在的偏差会不断累积,而在弯道里,偏差本来就长期不为零,积分项会越积越大,导致出弯时转向输出还保持一个很大的残留值,车就直接推头冲出赛道了。如果确实需要积分来消除静态误差,那积分限幅一定要给一个很小的值,比如输出限幅的十分之一。

微分项的作用是“预测”。偏差在快速增大时,微分项会产生一个反向的输出,相当于给转向加了阻尼,减少车头左右摆动。在实调中,Kd 往往要比 Kp 大三到五倍才能看到明显的稳定性提升。

3.4 差速输出:把PID输出映射成左右轮PWM

转向环算出来的输出值,本身是一个“转向量”,正负号表示方向,绝对值大小表示转向强度。要让小车真正转向,最简单的办法是差速——左轮和右轮给不同的PWM占空比,让两侧轮速不一致,车身就会转向。

void Motor_SetSpeed(float base_speed, float steer) { float left_pwm = base_speed - steer; // 左轮 = 基础速度 - 转向量 float right_pwm = base_speed + steer; // 右轮 = 基础速度 + 转向量 // 边界限幅,防止PWM超过定时器比较值上限 if (left_pwm > PWM_MAX) left_pwm = PWM_MAX; if (right_pwm > PWM_MAX) right_pwm = PWM_MAX; if (left_pwm < 0) left_pwm = 0; if (right_pwm < 0) right_pwm = 0; // 写入定时器比较寄存器,更新占空比 TIM_SetCompare1(TIM1, (uint16_t)left_pwm); TIM_SetCompare2(TIM1, (uint16_t)right_pwm); }

这个函数是执行层的核心。注意两个参数的含义:base_speed是你想要的直道速度,steer是PID计算出的转向修正量。当车向左偏时,误差为正,PID输出为正,那么左轮减速、右轮加速,车头就会向右修正回赛道中心,这是典型的负反馈闭环。

PWM频率也需要特别关注。直流电机驱动芯片对PWM频率有要求,一般设在 10kHz 到 20kHz 之间。低于这个范围,电机会发出明显的啸叫声,而且转速波动大;高于这个范围,驱动芯片的开关损耗又会显著上升。工程里默认用 15kHz 左右,这个值在大多数驱动芯片的数据手册推荐范围内。

4. 工程运行:编译、烧录与PID参数整定的完整流程

4.1 拿到工程先看什么:三个必须确认的配置点

打开压缩包内的 MDK-ARM 目录,双击工程文件直接进入MDK开发环境。我不建议拿到工程就直接点编译,先花五分钟确认三个关键配置,能省掉后面一大半的调试时间。

第一是芯片型号。工程默认配置的是 STM32F103 系列,如果手里实际使用的是不同型号,需要打开魔术棒(Options for Target)的 Device 选项卡重新选择,同时确认 Flash 容量配置。第二是调试器。在 Debug 选项卡里选择自己手上的调试器型号,并在 Settings 里确认能识别到芯片ID,这一步出问题最常见的原因是调试器没接好或者目标板没上电。第三是宏定义,检查 C/C++ 选项卡里是不是定义了USE_STDPERIPH_DRIVER和STM32F10X_MD,这两个宏直接影响外设库的编译范围。

4.2 编译烧录与串口观察:让小车“开口说话”

配置完成之后,先编译一下确认零错误零警告,然后点击下载按钮把程序烧进芯片。下载成功之后,不要急着把车放上赛道,先用串口把内部数据拉出来看。工程默认配置了 USART1,波特率 115200,PA9 是 TX,PA10 是 RX。

// main.c 主循环中的调试输出片段 while (1) { Adc_Update(); float error = Calculate_Error(); // 串口打印三个电感原始值和误差,格式方便用串口助手画波形 printf("L:%d M:%d R:%d E:%d\r\n", smooth_adc[0], smooth_adc[1], smooth_adc[2], (int)(error * 1000)); // 控制运算放在定时器中断里执行 delay_ms(20); }

把车拿在手里,靠近导线中轴,左右平移,观察串口数据的变化:靠近中心时左右电感值应该接近相等、误差接近零;偏向左边时误差应为正值;偏向右边时误差应为负值。如果符号反了,说明左右通道接反了,一种处理是把左右电感的ADC通道号对调,另一种更快的办法是把差比和公式改成(right - left) / (left + right)。

串口打印的间隔和内容也值得根据调试阶段调整。在整定PID时,只打印误差值E就够了,画出来的波形更干净;在验证传感器硬件时,打印完整的 L/M/R 三项;等进入稳定运行阶段,可以把串口打印关掉或者降频,避免调试串口占用太多CPU时间。

4.3 PID参数整定:先P后D再I的三步法

PID参数没有一步到位的公式,但对于电磁循迹这种场景,有一套行之有效的三步整定法,按顺序来,通常十分钟之内能让车走起来。

步骤做什么具体操作判断标准
第一步只调Kp把Kd和Ki先设为零,Kp从0.3开始慢慢加小车能沿直线走,但会有明显的摆头
第二步加KdKp保持不变,Kd从Kp的三倍开始试摆头明显减弱,直道变稳
第三步视情况加Ki如果过十字弯或大弧弯时转向响应不足,加很小的Ki弯道跟线更贴,但注意出弯不能冲出去

提示:第二步里如果 Kd 加得太大,车会出现高频抖动,听起来像电机在快速点刹。这时候先减小 Kd,不要动 Kp,否则很容易把两个参数一起调乱。

还有一个方向修正的细节,部分车的转向逻辑需要把舵量反向。如果发现加 Kp 之后车越跑越偏,往右偏的时候误差在增大,但转向输出反而让它偏得更快,说明反馈方向反了。不要改PID代码,直接调Motor_SetSpeed函数里的差速符号,把steer的加减位置互换即可。

4.4 整定实战:从抖到稳的一次完整记录

我自己的实际经验是,Kp 从 0.4 起步,车能走但是画龙;Kp 加到 0.8,直道好一点,但入弯时甩尾;把 Kd 加到 2.5,弯道明显贴线,偶尔还有一点过冲;Kd 加到 4.0,无论直道还是弯道都稳了,误差曲线在零点附近的波动控制在很小的范围之内。这一组参数在不同赛道上会有差异,但你会在调参过程中逐渐建立起感觉——什么现象对应哪个参数没调好,比背十遍PID公式都管用。

5. 调试避坑记录:五个让小车“原地去世”的坑

5.1 采集与信号端的三个坑

坑一:电感增益不一致导致“画龙”。现象是车明明摆正在赛道中轴线上,误差值却不是零,小车跑起来走 S 形。原因是左右两个电感本身感应系数有细微差异,或者两路运放放大倍数不一样,造成差比和公式里存在一个固定偏置。解决方法是做“静态校准”:上电后让车静置在赛道中心,采集此时左右电感的输出电压,把差值记录下来作为校准偏置,之后每次计算误差时先减去这个偏置。

坑二:ADC读数“发飘”,未上赛道时数据乱跳。现象是把车拿在手里,串口打印的电感值在几十个数之间波动,放上赛道后误差波动也很大。原因大概率是ADC采样时间配置不合理,或者采集引脚悬空。解决方法是把ADC采样周期拉到最大,同时把中电感对应的那个通道引脚在硬件上加一个下拉电阻,保证悬空时读到的是确定值。软件侧给采集值加上滑动平均滤波,双管齐下基本能压住。

坑三:电机一加油门,电感值整体抬升。现象是静止放在赛道上误差值正常,驱动电机加速后误差突然偏一边,车速越高越明显。原因是电机换向时产生的高频辐射磁场耦合进了工字电感,跟赛道信号叠加了。这个坑需要在机械和软件两端同时处理:机械上把电感支架尽量远离电机本体,有条件的在电感和电机之间加一层屏蔽;软件上把滤波窗口缩短,或者对采集值做一次基于车轮脉动的动态补偿。

5.2 控制与执行端的两个坑

坑四:过弯太猛出弯直接冲出去。现象是连续弯道跑得很顺,但每次出弯进入大直道时车头没有回正,直接偏离赛道冲出去。这个问题根源在积分项。弯道里误差持续非零,积分项不断累积输出,等出弯时误差已经变小,积分项却还保持着弯道里积累的大数值,转向输出退不下来。解决方法是转向环不用积分项,只用 PD,或者在必须保留积分的情况下把积分限幅压到输出限幅的十分之一以下。速度环可以用PI,但转向环用PD几乎成了这类工程的标准做法。

坑五:低速起步时车轮卡死不转,推一下反而猛冲。现象是给定很小的PWM占空比时电机纹丝不动,稍微加一点占空比又突然高速转动。原因有两个层次:一是驱动芯片的MOS管存在导通死区,占空比太低时输出端根本不足以驱动电机克服静摩擦;二是PWM占空比与电机输出扭矩在小占空比区间并不是线性关系。解决方法是做软件死区补偿——当计算出的PWM小于某个阈值时,直接把输出提到阈值之上,比如占空比低于10%时直接给10%。这个阈值需要根据电机和驱动的实际组合去试,常见范围在 8% 到 15% 之间。

6. 二次开发思路:从固定赛道到自适应循迹

6.1 三电感升级五电感:识别十字弯和环岛

基础三电感方案只能给出左右偏移量,遇到十字路口和环岛时容易丢失方向参考。一个低成本升级方案是增加两个外侧电感,变成五电感排布。误差计算改成加权差比和:

float error = (L2*w2 + L3*w3 - L1*w1 - L4*w4) / (L1+L2+L3+L4+1e-6f);

权重参数的意义是让靠外的电感在靠近赛道边缘时贡献更大的引导作用。这套工程的控制层不需要改动,只需替换Calculate_Error函数的内部实现,再增加两个ADC通道即可。

6.2 动态PID参数切换:查表替代固定参数

固定PID参数在上届赛场跑得稳,换个场地可能又抖又冲。我后来习惯把参数表做成二维查找表,按照当前偏差的绝对值切档:偏差小于 0.2 用低参数保证直道稳定,偏差大于 0.5 用高参数保证入弯响应。代价只是多几十行代码,收益是让车在不同曲率赛道上都有可用的折中表现。

6.3 闭环化改造与参数持久化

如果车模上带了编码器,可以把开环PWM输出改成带速度反馈的PI闭环,让左右轮实际转速跟随目标值。方向环保持PD,速度环用PI,两个环分层运作。最后把整定好的PID参数写入Flash,重启不丢失,避免每次上电都手动输入一遍。

那次比赛前夜我改了Kp忘写下电,第二天试车直接在第一个弯甩了出去——从那以后我每次整完参数都会跑一遍完整的直道加弯道验证流程,确认无误才烧录固化。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 7:21:54

AI辅助学术专著写作全流程:从大纲到初稿的五周实战

1. 为什么说学术专著写作正在被AI重写规则一本几十万字的学术专著&#xff0c;放在三年前基本是按年起步的周期&#xff0c;现在真的可以按周出初稿。这句话放在去年我是不信的&#xff0c;但当我用AI配合一套完整流程跑完一部技术专著的初稿后&#xff0c;我确信这是当下最被低…

作者头像 李华
网站建设 2026/10/17 7:21:50

终端AI编程助手Super Code实战:从安装配置到高效提效与避坑指南

1. 为什么要在终端里塞一个 AI 编程助手终端对开发者来说&#xff0c;是每天停留时间最长的地方之一。编译、跑测试、查日志、翻 git 历史、连远程机器&#xff0c;几乎所有关键动作都在这个黑框里完成。但过去这些年&#xff0c;终端本身其实没怎么变聪明——它只是个忠实的执…

作者头像 李华
网站建设 2026/10/17 7:21:26

多轮对话上下文截断策略:从滑动窗口到Token控制的工程实践

1. 问题本质与截断的必要性最早接触这个需求&#xff0c;是给一个智能客服机器人做多轮对话优化。用户在会话里连续问了十几个问题&#xff0c;客服机器人回复到后面明显开始“答非所问”——不是模型变笨了&#xff0c;而是历史消息把上下文窗口挤满了&#xff0c;早期真正关键…

作者头像 李华
网站建设 2026/10/17 7:21:08

C#调用C++ QT DLL:基于HWND的跨语言界面嵌入方案

简介&#xff1a;面向C#与C/Qt混合编程需求的开发者&#xff0c;示例包演示了C#控制台程序如何调用由Qt编译生成的外部界面UI DLL。包内基于Visual Studio解决方案组织&#xff0c;包含C#源码、C/Qt生成的DLL组件、可执行文件及配置文档&#xff0c;覆盖从工程配置、路径设定到…

作者头像 李华
网站建设 2026/10/17 7:21:04

Windows上通过WSL安装OpenClaw:从环境初始化到协同使用全指南

先说一句&#xff1a;如果你跟我一样&#xff0c;主力机是 Windows&#xff0c;但日常要跑各种命令行工具、脚本、AI agent 类应用&#xff0c;那我非常建议把这类工具放进 WSL 里&#xff0c;而不是强行在原生 Windows 环境里折腾。这篇记录的是我在 Windows 上安装 OpenClaw …

作者头像 李华
网站建设 2026/10/17 7:20:50

PB级大数据平台Alluxio数据编排层实践:架构、调优与性能提升

1. 项目缘起与整体架构思路大数据平台做到一定规模之后&#xff0c;存储和计算之间的那道墙就会变得特别扎眼。我所在的团队负责维护一套日处理量在PB级别的离线与近线混合计算平台&#xff0c;底层存储横跨三个机房、两代硬件、四种不同的存储介质。早期大家各写各的Spark任务…

作者头像 李华