首页 / 资讯中心 / 文章详情

电磁循迹小车PID整定:电感信号链、舵机PD与增量式PID

电磁循迹小车PID整定:电感信号链、舵机PD与增量式PID ★ FEATURED ARTICLE
简介面向电磁循迹与恩智浦智能车竞赛的PID调节工程包适合具备STM32基础、正在攻克电感采集与舵机/电机闭环调试的参赛者与自动化学习者。压缩包共192个文件、约5.76MB以47个C头文件、43个C源文件为核心辅以Keil工程配置、编译输出与少量说明文档覆盖定时器、Flash、LCD、MPU6050姿态解算及DMP驱动等模块可用于对照理解工程组织与底层驱动写法。已有4694人学习。读者可从中获取电磁循迹小车的完整代码框架、传感器数据处理思路和PID参数整定参考结合赛道条件反复试验比例、积分、微分项观察舵机角度与后轮转速的响应进而排查振荡、迟滞与静态误差以及从硬件连接到软件实现的综合调试路径。工程保留中间产物与批处理脚本也便于复现编译环境和定位常见构建问题。1. 电磁循迹小车 PID 调节为什么同一套代码换条赛道就跑歪车在直道上跑得笔直一进弯就贴着内道切出去把 Kp 调大一点又变成左右画龙——这大概是电磁循迹小车调试里最常见的一幕。这份资源给的就是一套能直接编译的电磁循迹小车 PID 调节工程电感采集、归一化、差比和算偏差、舵机 PD 打角、电机增量式 PID 差速参数集中在一张表里改完烧进去就能试。适合正在做循迹小车、卡在“参数到底怎么调”这一步的开发者也适合想把控制环拆开单独验证的人。下面按信号链、控制层、工程结构、排查、二次开发的顺序拆照着能先把车跑起来再动手改。2. 信号链拆解从电感谐振到一路可信的偏差量整车的控制效果上限其实在信号链这一层就定死了。后面 PID 调得再花只要喂进去的偏差量本身带跳变、带零点漂移舵机就会跟着抖。所以先把电感怎么选、怎么采、怎么变成 -1 到 1 的偏差讲清楚再谈控制。2.1 电感布局与谐振采集电路赛道导线里通的是交变电流周围产生同频交变磁场。车头的工字电感与并联电容组成 LC 并联谐振回路谐振频率对准赛道电流频率时回路阻抗最大、感应电压最高相当于把微弱磁场信号从环境噪声里“挑”出来。常见做法是电感取 10mH 量级电容按 f 1/(2π√(LC)) 反推几百 nF 到 1nF 之间对准 20kHz 左右。检波有两条路一条是二极管检波加 RC 滤波直接得到直流量电路简单、响应快但二极管压降会吃掉小信号温度一变零点就飘另一条是让 ADC 在谐振周期内多次采样取峰值或有效值或者用运放做精密整流动态范围更好代价是要多花一点 CPU 和采样时序设计。工程里用的是后者采样窗口由定时器触发和 PWM 开关沿错开。电感布局决定了偏差量的“形状”。常见的是四电感方案左右两个横置电感对横向偏移敏感左右两个竖直电感对弯道方向敏感也有加中间电感的五电感方案用来判断是否正压在导线上。通道安装位置主要作用典型权重IND_L车头左侧横置反映左偏量1.0IND_R车头右侧横置反映右偏量1.0IND_VL车头左侧竖直判弯道方向、曲率0.3 左右IND_VR车头右侧竖直判弯道方向、曲率0.3 左右注意竖直电感的权重不要一上来就给大。它的信号在直道上差值很小一旦权重过高直道会被噪声推着走表现出来就是“直道画龙”。2.2 归一化与差比和把电压变成可信的偏差原始 ADC 值不能直接用。电感离地高度变一点、电池电压掉一点、赛道电流源换一个绝对值全变昨天调好的 Kp 今天就不对了。所以第一步是归一化把每个通道压到 0.0 到 1.0 之间靠的是实测标定出来的底噪和峰值两个基准。#define IND_NUM 4 #define IND_MIN 30 /* 电感远离导线时的底噪实测标定 */ #define IND_MAX 3800 /* 电感正对导线时的峰值实测标定 */ static uint16_t adc_raw[IND_NUM]; static float ind_norm[IND_NUM]; /* 把原始 ADC 值压到 0.0 ~ 1.0消掉高度、供电、赛道电流带来的整体缩放 */ void Ind_Normalize(void) { for (int i 0; i IND_NUM; i) { int32_t v (int32_t)adc_raw[i] - IND_MIN; if (v 0) v 0; if (v (IND_MAX - IND_MIN)) v IND_MAX - IND_MIN; ind_norm[i] (float)v / (float)(IND_MAX - IND_MIN); } } /* 差比和左右横电感之差 ÷ 左右之和输出 -1.0 ~ 1.0 的偏差 */ float Ind_GetError(void) { static float last_err 0.0f; float l ind_norm[IND_L]; float r ind_norm[IND_R]; float sum l r; if (sum 0.05f) { /* 两边都没信号基本等于脱线 */ return last_err; /* 保持上一次偏差交给状态机处理 */ } last_err (l - r) / sum; return last_err; }IND_MIN和IND_MAX是拿着车在赛道上实测出来的把车放在导线正上方读一次峰值把车横移到完全没信号的位置读一次底噪多测几个点取平均。sum 0.05f这个阈值是脱线判据太小会误判太大会漏判一般取满量程的 3% 到 5%。差比和的好处在于分母是 lr天然带归一化车正好压在中间时 l≈r输出接近 0信号整体变弱时分子分母同步缩小输出基本不变。这比直接用 (l-r) 稳得多。竖直电感要单独融合进来用来提前感知弯道/* 横竖融合横电感管偏移竖电感管弯道方向 */ float Ind_GetErrorFused(void) { float err_h Ind_GetError(); float err_v (ind_norm[IND_VL] - ind_norm[IND_VR]) / (ind_norm[IND_VL] ind_norm[IND_VR] 0.05f); return 0.8f * err_h 0.2f * err_v; /* 权重按赛道曲率调 */ }分母加 0.05f 是防止两边都接近零时除出个巨大的数这是血泪经验——不加这一项直道上偶尔会蹦出一个 ±5 的偏差舵机当场打满。进控制环之前还要过一道滤波。电感信号里混着 PWM 开关噪声和电源纹波不滤直接送进微分项Kd 会被放大成抖动/* 一阶低通alpha 越小越平滑但相位滞后越大 */ static float Lpf(float in, float *state, float alpha) { *state alpha * (in - *state); return *state; }采样率 100Hz 时 alpha 取 0.2 到 0.4 比较合适。alpha 再小弯道入口的相位滞后会让车“慢半拍”表现出来就是进弯晚、出弯冲。3. 控制层拆解舵机 PD 和电机 PID 到底各管什么很多人调车调不明白是把舵机环和速度环当成一个东西在调。实际上舵机是位置伺服给什么角度转什么角度属于快环电机是速度伺服有加减速惯性属于慢环。两个环的周期、参数意义、限幅方式都不一样混在一起调必然翻车。3.1 舵机 PD输出的是打角不是速度舵机接收的是 PWM 占空比对应的目标角度所以用位置式 PD不需要积分项——加了积分反而会在长直道上累积出弯时猛地一甩。微分项用偏差的差分近似因为控制周期固定差分本身就等价于变化率不用再除 dt。typedef struct { float kp; float kd; float kff; /* 陀螺仪前馈系数 */ float out_limit; /* 打角限幅单位是 PWM 计数值 */ float last_err; float out; } SteerPD; float Steer_Calc(SteerPD *s, float err, float gyro_z) { float diff err - s-last_err; /* 周期固定差分即变化率 */ s-last_err err; float out s-kp * err s-kd * diff s-kff * gyro_z; /* 前馈车头已经开始转提前打角 */ if (out s-out_limit) out s-out_limit; if (out -s-out_limit) out -s-out_limit; s-out out; return out; }参数怎么起手kp先给 60 到 120对应舵机 PWM 计数值的量级具体看你的舵机行程标定把kd和kff都置零推车走直道看回正力度回正够了但进弯过冲再往上加kd一次加 1 到 2等kd加到开始出现高频抖动就往回退两档这是kd的实际上限。kff最后加配合陀螺仪 Z 轴角速度能让车在弯道入口提前打角观感上就是“车知道要转弯了”。还有两个容易被忽略的细节舵机中值必须单独标定机械安装误差会让中值偏离 1500写死会一直往一边跑打角限幅要留余量打满 100% 的舵机在弯道里没有调整空间遇到突发偏差只能硬顶。3.2 电机增量式 PID抗饱和、切换无扰电机这边用的是增量式 PID输出的是 PWM 增量而不是绝对占空比typedef struct { float kp, ki, kd; float e1, e2; /* e(k-1), e(k-2) */ float out; float out_max; } IncPID; float IncPID_Calc(IncPID *p, float target, float feedback) { float e0 target - feedback; /* 增量式标准形式Δu Kp·(e0-e1) Ki·e0 Kd·(e0-2e1e2) */ float du p-kp * (e0 - p-e1) p-ki * e0 p-kd * (e0 - 2.0f * p-e1 p-e2); p-e2 p-e1; p-e1 e0; p-out du; if (p-out p-out_max) p-out p-out_max; if (p-out -p-out_max) p-out -p-out_max; return p-out; }选增量式而不是位置式主要图两点一是切换目标速度时没有积分累积要清理从 0 直接跳到 2m/s 也不会猛冲二是限幅后out被钳住下一拍du是在钳位值上继续累加天然带一点抗饱和的意味。但要注意out被限幅后必须回写进结构体不写回去积分项照样在内部涨这就是典型的积分饱和。速度环周期一般取 20ms比舵机环慢四倍。太快的速度环会因为编码器分辨率不够而抖太慢又跟不上弯道的速度规划。差速分配放在速度环外面/* 差速基础速度 ± 转向修正转向修正由打角或偏差给出 */ void Motor_Output(void) { float base g_param.base_speed; float steer steer.out / steer.out_limit; /* 归一化到 -1 ~ 1 */ float diff g_param.diff_gain * steer; /* 弯道降速偏差越大基础速度压得越低 */ base * (1.0f - g_param.curve_slow * fabsf(steer)); float target_l base - diff; float target_r base diff; int16_t pwm_l (int16_t)IncPID_Calc(pid_l, target_l, Encoder_GetL()); int16_t pwm_r (int16_t)IncPID_Calc(pid_r, target_r, Encoder_GetR()); Motor_SetPwm(pwm_l, pwm_r); }diff_gain决定转向的“力度”curve_slow决定弯道降速比例一般 0.2 到 0.4。这两个参数是替代“调 Kp 调不出来”的关键——很多车进弯冲出去不是舵机 Kp 不够而是弯道速度没降下来。3.3 两个环的时序关系舵机环每 5ms 跑一次速度环每 20ms 跑一次都在同一个定时器节拍里按计数分频执行。舵机环的快是为了让打角能跟上偏差的瞬时变化速度环的慢是因为电机本身有惯性跑太快只会让 PWM 抖成噪声。提示控制周期一旦定下来就不要随手改。Kd 项的物理意义依赖固定周期改了周期不改参数等于把调好的微分作用整体缩放了一遍之前的整定全部作废。4. 工程结构与调度定时器节拍、状态机和参数表代码能跑和代码能改是两件事。这套工程在结构上把“采样—计算—输出”和“参数—状态—存储”分开了好处是调参不用翻控制代码改控制不用动参数表。4.1 目录结构与模块职责EMC_TrackCar/ ├── Core/ 芯片启动、时钟配置、中断向量表 ├── Drivers/ 外设驱动ADC、PWM、编码器、UART、陀螺仪 ├── App/ │ ├── sensor/ 电感采样、归一化、滤波、偏差融合 │ ├── control/ 舵机 PD、电机增量式 PID │ ├── chassis/ 差速分配、速度规划 │ └── app_task.c 定时节拍调度、状态机 ├── Param/ │ └── param.c 参数结构体、默认值、Flash 读写 └── Tools/ 串口波形脚本、参数下发工具模块关键文件职责调用周期sensorind.c/filter.cADC 取值、归一化、差比和5mscontrolsteer_pd.c/speed_pid.c打角计算、速度闭环5ms / 20mschassisdiff.c差速分配、弯道降速20msapp_taskapp_task.c节拍调度、状态机5msparamparam.c参数集中管理、掉电保存按需分层的边界要守住control层不允许直接读 ADC 寄存器也不允许直接写 PWM 寄存器全部走sensor和Drivers的接口。这样换一块板子只需要改Drivers控制逻辑一行不动。4.2 定时节拍与状态机节拍由定时器更新中断驱动中断里只做计数和置标志实际计算放在主循环或者低优先级任务里避免中断里跑浮点运算拖长响应/* 5ms 节拍定时器更新中断 */ void TIM3_IRQHandler(void) { if (TIM3-SR TIM_SR_UIF) { TIM3-SR ~TIM_SR_UIF; g_tick_5ms 1; /* 只置标志不算浮点 */ } } /* 主循环里消费节拍 */ void App_Loop(void) { static uint8_t cnt 0; if (!g_tick_5ms) return; g_tick_5ms 0; Ind_Sample(); /* 采样 归一化 滤波 */ float err Ind_GetErrorFused(); Steer_Calc(steer, err, Gyro_GetZ()); /* 舵机环每拍都跑 */ App_StateMachine(); if (cnt 4) { /* 20ms 一次速度环 */ cnt 0; Speed_Loop(); } Motor_Output(); }状态机负责处理脱线、发车、停车这些边界情况别小看它——脱线保护写不好车冲出赛道后会满速撞墙typedef enum { ST_IDLE, ST_RUN, ST_LOST, ST_STOP } CarState; void App_StateMachine(void) { static uint16_t lost_ms 0; switch (state) { case ST_RUN: if (Ind_AllDark()) { state ST_LOST; lost_ms 0; } break; case ST_LOST: /* 用最后一次有效偏差方向低速找线超时则停车 */ lost_ms 5; if (Ind_AnyValid()) { state ST_RUN; lost_ms 0; } else if (lost_ms 300) { state ST_STOP; } break; case ST_STOP: Motor_SetPwm(0, 0); break; default: break; } }lost_ms 300就是 1.5 秒的找线窗口太短会在颠簸处误停车太长会让车带着错误方向跑很远。5. 常见问题排查实车上那几种“看着像玄学”的抖动调车调到怀疑人生的时候问题多半不在 PID 参数上而在信号或者时序。下面这几条是反复踩过的按“现象 → 原因 → 解决”记下来比盲调参数省时间。5.1 信号层的问题现象一静止时电感值纹丝不动电机一转就乱跳。原因是电机 PWM 的开关噪声通过电源和地串进了 ADC采样时刻正好落在 MOS 开关沿上。解决方式是把 ADC 触发源改成定时器触发采样窗口对齐到 PWM 中心避开开关沿电感地和功率地单点连接别在板上随便铺输入端加一级 RC截止频率压在几 kHz。现象二换一块场地直道偏差就不为零了。原因是归一化基准IND_MIN/IND_MAX是写死的而赛道电流、环境磁场、电感离地高度都变了。解决办法是上电自标定发车前让车停在赛道中央静止 200ms采一段峰值再把车横移到无信号位置采一段底噪把两个值写进 RAM 的基准变量归一化函数读这两个变量而不是宏。现象三直道好好的远处弯道提前“看到”车提前切内道。原因是电感前瞻太远或者竖直电感权重给大了。竖直电感在直道上的差值本来就小权重一大噪声被放大成假弯道。做法是把err_v的权重从 0.2 往下压到 0.1或者干脆只在横电感差值超过阈值时才启用竖直分量。5.2 控制层的问题现象四直道上小幅高频抖肉眼看着像车在“颤”。原因通常是kd太大或者微分项之前没滤波。偏差本身带 10 到 30 个 ADC 码的噪声差分之后放大十倍舵机就在中值附近来回打。解法是先对diff单独做一阶低通再乘kd或者把kd直接砍半用前馈补回响应。现象五进弯冲出去出弯甩尾。这个最容易被误判成舵机 Kp 不够。实际原因多半是两个一是弯道没有降速速度环目标速度恒定舵机打到限幅也拉不回来二是没有前馈纯反馈永远慢半拍。做法是加曲率前馈或者陀螺仪前馈同时用curve_slow把弯道基础速度压下来 20% 到 40%。注意排查顺序永远是“先看信号再看时序最后看参数”。偏差量本身在跳参数怎么调都是白搭这也是我一般建议在工程里留一路串口波形的原因——把err、steer.out、pwm三条曲线打出来问题基本一眼就能看出来。6. 二次开发把整定流程做成可复现的验证方法参数调通一次不难难的是换车、换赛道、换电池之后还能快速复现。我的做法是把参数从代码里彻底拔出来再把整定过程变成一套固定的验证动作而不是靠手感。先做参数表外置。所有可调量收进一个结构体带 magic 校验上电从 Flash 读读失败就落回默认值串口可以单条下发typedef struct { uint32_t magic; /* 0x5A5A1234用来判断是否已初始化 */ float steer_kp, steer_kd, steer_kff; float speed_kp, speed_ki, speed_kd; float base_speed, diff_gain, curve_slow; } CarParam; CarParam g_param; void Param_Load(void) { /* 从 Flash 最后一页读结构体magic 不匹配就用编译期默认值 */ if (*(uint32_t *)PARAM_ADDR ! PARAM_MAGIC) { Param_SetDefault(); Param_Save(); } else { memcpy(g_param, (void *)PARAM_ADDR, sizeof(g_param)); } } /* 串口命令SET steer_kp 85解析后直接改 g_param */ void Param_OnCommand(const char *name, float val) { if (!strcmp(name, steer_kp)) g_param.steer_kp val; if (!strcmp(name, steer_kd)) g_param.steer_kd val; if (!strcmp(name, base_speed))g_param.base_speed val; /* 其余字段同理注意加范围钳位 */ }验证方法我固定成三组动作每次改完参数都跑一遍静态阶跃——把车架空手动把偏差从 0 跳到 0.5看打角响应有没有超调、稳定时间多少方波跟踪——用串口以 1Hz 下发 ±0.3 的偏差方波看打角能不能跟上、有没有滞后实车直线——直道全速跑看偏差曲线是不是贴着零线走。三组都过了再上弯道能省掉大量“改一个参数跑一圈”的时间。一个具体技巧kd的整定不要看车看波形。把err和steer.out同时打出来理想的曲线是打角略超前于偏差变化如果打角曲线和偏差曲线完全同相说明kd还没起作用如果打角在偏差反向之后还在往原方向走就是kd过头了。从那以后我每次改控制参数都强制先跑一遍静态阶跃再上车哪怕只改了一个数。省下来的不是调试时间是舵机和齿轮的钱。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站