YAOTU INSIGHTS

STM32F103摄像头循迹小车硬件二值化与PID控制方案

STM32F103摄像头循迹小车硬件二值化与PID控制方案
简介这份基于STM32F103的摄像头循迹小车工程面向学习嵌入式控制、图像处理与智能硬件实践的学生和开发者重点解决OV7670图像采集、二值化道路识别、循迹方向控制及超声波避障的协同实现问题。压缩包共166个文件约751KB以72个C源文件、77个头文件为主另有启动汇编、Keil工程配置、HEX下载文件和少量图片素材便于直接编译、对照模块划分与移植裁剪。已有2341人学习下载说明该案例在STM32入门与综合项目练习中具有一定参考价值。代码围绕摄像头驱动、图像阈值处理、边缘/线条检测、电机与舵机控制、超声波测距等环节展开读者可从中梳理外设初始化、任务调度和避障逻辑的组织方式并借鉴其工程目录与调试思路适合作为课程设计、智能小车竞赛或自主循迹项目的起步框架。1. 摄像头循迹为什么不用红外对管一套 STM32F103 小车的方案定位摄像头循迹在 STM32F103 上运行多数人的第一反应是性价比太低——72MHz 主频、20KB SRAM连一帧 320×240 灰度图都存不下。但我看过不少做智能小车的学生项目真正卡住的从来不是算力而是把摄像头当成“图像传感器”而不是“赛道传感器”来用。这套方案要解决的问题很具体让一台基于 STM32F103 的两驱小车靠一颗俯视摄像头识别黑线并稳定跑完全程同时挂一路超声波测距做近距避障遇到障碍能减速、绕行、再自动回线。适合正在做毕设或课设、手里已经有一块 F103 最小系统板和一辆裸车、需要一套既能跑通又能写进设计报告的结构的人。下面按硬件分配、图像流水线、控制环、避坑、验收的顺序展开每一段都对应交付物里的一章。2. 硬件与资源分配F103 的定时器、GPIO 与 DMA 怎么切在把摄像头接入 F103 前需要先明确一个前提这颗芯片没有 DCMI 摄像头接口SRAM 只有 20KB也没有硬件图像流水线。所有“摄像头循迹”的方案差异本质上都是在回答同一个问题——怎么用最少的 CPU 时间和最少的 RAM把赛道信息从一帧图像里提取出来。这个问题的答案会直接决定后面所有代码的写法所以在选型阶段就要确定不能拖到调车时再回头改。2.1 摄像头接口选型为什么放弃整帧搬运改用硬件二值化先算一笔账。320×240 的灰度图是 76800 字节F103C8T6 的 20KB SRAM 装不下即使换成 64KB 的增强型型号也只是勉强塞下一帧还得留出栈、缓冲区和 PID 状态。更要命的是时间以 1MHz 的采样率把 19200 字节80×240搬进内存需要接近 20ms而整车的方向控制周期希望压到 5ms 以内——搬一帧图像的时间相当于四五个控制周期车早就冲出去了。所以这类项目通常走两条路。第一条是 OV7670 加 FIFOAL422B整帧缓存MCU 在场消隐期间慢慢读读完做软件二值化第二条是直接选带硬件二值化输出的摄像头模块模块内部用比较器把 Y 分量和一个可调阈值比出 0/1MCU 只负责按行采样。我一般会选第二条原因不在“省事”而在采样量只采图像中间 80 列、60 行一帧 4800 个点按位打包后正好 600 字节连同行缓冲和边线数组总共不到 2KB剩下的 RAM 全留给控制和调试缓冲。采样动作交给 DMACPU 占用接近零。代价是二值化阈值一旦固定换场地、换光照就要重新调。这一点在第 5 章会展开这里先记住硬件二值化省下来的是算力代价是把一部分鲁棒性挪到了标定环节。2.2 定时器与外设分配表一个 72MHz 的芯片要同时干四件事F103 的中断和外设资源不算宽裕分配不好就会出现“电机一转、图像就花”的现象。我通常按下面的方式切分把对时序最敏感的任务固定到独立外设上避免它们互相阻塞流程。资源用途关键配置与理由TIM1 CH1~CH4左右电机 PWM20kHz 载波避开人耳可闻频段也高于电机电感响应TIM2左轮编码器接口编码器模式 TI12四倍频计数10ms 读一次TIM4右轮编码器接口同上两个轮子必须用独立定时器TIM3图像采样节拍 DMA1_Ch3ETR 引脚接分频后的 PCLK每个 update 事件触发一次 DMASysTick5ms 控制周期只置标志位不做运算运算放在主循环EXTI0超声波 ECHO 双沿配 DWT 周期计数器换算微秒USART1调参数据回传1152008N1跑车时关闭以保证时序这里有一个容易忽略的细节TIM3 用外部时钟ETR驱动后它的计数器频率等于采样率不再是 1MHz所以不能拿它当通用计时器用。超声波测距因此改用 EXTI 双沿加 DWT 的周期计数成本几乎为零。/* TIM3_UP 在 DMA1 的通道 3 上用它把 GPIO 输入寄存器一次次搬进缓冲 */ #define LINE_POINTS 80 static uint32_t line_raw[LINE_POINTS]; /* 每项是一次 GPIOA-IDR 的快照 */ void camera_dma_init(void) { RCC-AHBENR | RCC_AHBENR_DMA1EN; RCC-APB1ENR | RCC_APB1ENR_TIM3EN; /* PA0 作为二值化数据输入上拉避免悬空时读出一片随机值 */ GPIOA-CRL ~(0xFU 0); GPIOA-CRL | (0x8U 0); /* 上拉/下拉输入 */ GPIOA-ODR | (1U 0); /* 上拉 */ DMA1_Channel3-CPAR (uint32_t)GPIOA-IDR; DMA1_Channel3-CMAR (uint32_t)line_raw; DMA1_Channel3-CNDTR LINE_POINTS; DMA1_Channel3-CCR DMA_CCR_MSIZE_1 | DMA_CCR_PSIZE_1 /* 32 位宽 */ | DMA_CCR_MINC /* 内存地址自增 */ | DMA_CCR_PL_1; /* 高优先级 */ /* 不置 CIRC每行由 HREF 中断重新装填避免半行数据被覆盖 */ } /* 行同步下降沿调用把这一行的 80 个采样点转成字节数组 */ static inline void extract_bits(uint8_t *dst, const uint32_t *src) { for (uint8_t i 0; i LINE_POINTS; i) dst[i] (src[i] 0) 0x01; /* 数据线接 PA0取 bit0 */ }这段代码的关键不在 DMA 本身而在“每行重新装填”这个选择。如果开循环模式DMA 会在整帧期间不停地覆盖同一块缓冲行与行之间就分不清边界图像会呈现明显的斜向撕裂。改成每行由 HREF 中断重启一次 DMA配合一个行计数器就能保证第 i 行数据一定落在 img[i] 里。2.3 电源与地线电机一启动就复位的老毛病调车现场最常见的一种“玄学”是车架空转一切正常一落地电机带载MCU 随机复位或者图像突然全黑。九成以上不是程序问题是电源。动力电和逻辑电必须分开走。两节 18650 串联给电机驱动直接供电中间经一片降压模块降到 5V 给舵机和摄像头再经 LDO 降到 3.3V 给 MCU。三路地最后只在一个点汇合——通常是电源入口的滤波电容负极不要出现“电机电流从 MCU 的地线上流回去”的路径。每个电机两端并联一个 0.1μF 瓷片电容和一只续流二极管能明显降低换向瞬间的尖峰。摄像头排线旁边再挂一组 LC 滤波因为它的模拟输出对电源纹波极其敏感纹波一大二值化结果就会整片翻转。提示判断是不是电源问题有一个很快的方法——把车架起来用手按住轮子让电机堵转同时看串口回传的 offset 值是否出现大幅跳变。跳变就是电源问题不跳再去看算法。3. 图像流水线从 80×60 二值图到一条能用的中线摄像头出来的东西只是一堆 0 和 1控制环需要的是一个带符号的偏差量单位是像素。中间这一步做得好不好直接决定车是“贴着线走”还是“画龙”。整个流水线我习惯拆成四段定帧边界、提单行边线、加权算中线、处理丢线每段都可以单独拿串口数据验证。3.1 场同步与行同步先把一帧的边界定死VSYNC 的下降沿表示新的一帧开始HREF 的高电平表示这一行的像素有效。这两个信号必须用中断处理而且优先级要高于控制周期中断——图像采到一半被控制中断打断回来时 HREF 已经过去好几行行号就错位了。具体做法是VSYNC 下降沿把行计数器清零、置“采集中”标志HREF 下降沿把行计数器加一如果落在采样窗口内比如第 30 到 89 行就启动一次 DMA。整帧结束时把“新帧就绪”标志置上主循环看到标志才做图像处理处理完再清标志。这样处理带来一个副产品控制环可以知道“这一帧是哪一刻的”对后面做图像滞后补偿很有用。还有一个细节是采样窗口。俯视摄像头装得再高图像顶部和底部也有一部分是地面之外的内容车头、远处背景全部采样只是浪费。实测下来只取中间 60 行、左右各留 20 列的边缘区域不采效果最好既保证了前瞻距离又避开了广角镜头的畸变区。3.2 边线提取从中间向两侧找跳变每一行单独看就是一个一维的二值序列。理论上黑线在中间左右各有一个从黑到白的跳变找到这两个跳变中线就出来了。但真实赛道上有反光、有接缝、有阴影直接从两边往中间扫很容易被干扰所以正确顺序是先找种子点再从种子点往外扩。#define ROWS 60 #define COLS 80 #define BLACK 1 #define WHITE 0 typedef struct { int16_t left[ROWS]; int16_t right[ROWS]; int16_t center[ROWS]; int16_t offset; /* 中线相对图像中心的偏差右偏为正 */ int16_t last_offset; /* 上一帧有效偏差用于丢线外推 */ uint8_t valid_rows; uint8_t lost; } TrackInfo; static TrackInfo track; static void scan_row(const uint8_t *row, int16_t *l, int16_t *r) { const int16_t mid COLS 1; int16_t i, seed -1; *l -1; *r -1; /* 弯道时车头可能已经偏出黑线中心像素未必是黑先向两侧找种子 */ for (i 0; i mid; i) { if (row[mid - i] BLACK) { seed mid - i; break; } if (row[mid i] BLACK) { seed mid i; break; } } if (seed 0) return; /* 整行无黑点交给丢线逻辑 */ /* 从种子向左找第一个 黑-白 跳变作为左边线 */ for (i seed; i 0; i--) if (row[i] BLACK row[i - 1] WHITE) { *l i; break; } /* 从种子向右找第一个 黑-白 跳变作为右边线 */ for (i seed; i COLS - 1; i) if (row[i] BLACK row[i 1] WHITE) { *r i; break; } }“先找种子”这个动作是必须经历的。早期版本直接从 mid 往两边扫结果在弯道上出现一个奇怪现象车头偏出线外时中间像素是白的程序认为这一行没线丢线计数一路涨最后按最右侧的路径偏出赛道。加上种子查找后同样的弯道就能稳稳地跟住。3.3 中线加权与赛道宽度校验每行拿到左右边线后先做一道宽度校验再算中线。宽度小于 18 像素基本是地面的接缝或反光噪点大于 76 像素多半是十字路口或者整片阴影。这两种情况下的中线都不可信直接把该行标记为无效比强行使用这个中点来算要好得多。加权的方式决定了这辆车的“前瞻感”。图像第 0 行是最远的一行第 59 行是最近的一行如果所有行等权平均算出来的偏差严重滞后——等车头真的偏了远处早就弯了。我一般用线性权重越远权重越大。#define LINE_MIN 18 #define LINE_MAX 76 void track_process(const uint8_t img[ROWS][COLS]) { int32_t sum 0, wsum 0; uint8_t valid 0; for (uint8_t i 0; i ROWS; i) { int16_t l, r, w; scan_row(img[i], l, r); track.left[i] l; track.right[i] r; if (l 0 || r 0) { track.center[i] -1; continue; } w r - l; if (w LINE_MIN || w LINE_MAX) { /* 宽度异常本行不参与中线计算 */ track.center[i] -1; continue; } track.center[i] (l r) 1; /* i 越小越远权重 ROWS-i 越大让弯道提前反映到 offset 上 */ int16_t weight ROWS - i; sum (int32_t)weight * (track.center[i] - (COLS 1)); wsum weight; valid; } track.valid_rows valid; track.offset wsum ? (int16_t)(sum / wsum) : track.last_offset; }参数上有一点值得说权重是线性还是平方对直道影响不大对 S 弯影响很明显。线性权重下车在连续反向弯里会略微“切弯”平方权重更贴近远端但噪声放大了三倍容易抖。比赛车我倾向线性教学演示用平方更能看出效果。3.4 丢线、十字与断路补线策略和它的边界丢线是必然会发生的关键不在于“永不丢线”而在于丢线之后车的行为是否可预期。我的处理分三档。第一档有效行数少于 8 行但还没到零认为是被局部阴影干扰直接用上一帧的 offset 顶替同时把基础速度降一档。第二档连续丢线超过 15 帧按最后一次有效偏差的方向把差速加到最大让车转向回线同时速度降到最低。第三档连续丢线超过 60 帧大约 1 秒判定出界停车等人工拾取——这一档不是为了跑分是为了让调试时的现象可复现而不是车无目的地乱冲。#define LOST_HOLD 15 #define LOST_STOP 60 #define OFFSET_MAX 35 void track_lost_handle(void) { if (track.valid_rows 8) { track.lost 0; track.last_offset track.offset; return; } if (track.lost LOST_HOLD) { /* 短暂丢线沿用上一帧避免控制量突变成 0 */ track.offset track.last_offset; } else if (track.lost LOST_STOP) { /* 持续丢线按记忆方向打满同时主循环会把速度降到最低档 */ track.offset (track.last_offset 0) ? OFFSET_MAX : -OFFSET_MAX; } else { track.offset 0; motor_stop(); /* 判定出界停车保护车体和场地 */ } }十字路口是另一类问题。宽度校验能把整行全黑的行剔掉但十字的特点是“左右边界突然外扩”——中间一段照样是黑的宽度却翻倍。所以我把 LINE_MAX 设成 76图像宽度的 95%让这类行直接失效靠前后有效行的插值补出中线。这个方法在直角交叉上表现稳定唯一会失效的是“十字加急弯”的组合遇到这种情况通常需要额外加一个“宽度突变计数”这里先不展开。4. 控制环5ms 方向环、10ms 速度环与避障状态机图像每 40ms 才更新一次控制周期却想做到 5ms这中间的矛盾贯穿整个控制层的设计。有两种思路一种是让控制环跟着图像走新帧到了才算一次另一种是控制环固定节拍运行图像没更新就沿用上一次的偏差靠固定节拍保证电机 PWM 的连续性。我选后者因为电机的机械惯性和 PWM 的刷新率是绑定在一起的控制周期一抖电机就会发出明显的啸叫。4.1 串级结构与控制周期外层是方向环输入是 offset输出是差速量内层是速度环输入是目标速度输出是 PWM 占空比。方向环 5ms 一次速度环 10ms 一次速度环之所以慢一倍是因为编码器在低速下 5ms 只能计到几个脉冲做差商噪声极大10ms 刚好。图像滞后必须显式补偿。摄像头从曝光到数据搬进内存大约 20~30ms加上处理时间实际拿到手的 offset 对应的是三四十毫秒之前的位置。车速 1.5m/s 时40ms 就是 6cm接近赛道宽度的一半。补偿的办法有两个一是把图像偏移按当前角速度做一阶外推二是直接把差速输出加一个基于上次差速的预测项。我用第一种实现简单效果足够。4.2 PID 实现积分限幅与不完全微分摄像头输入天然带量化噪声offset 是整像素抖动 ±1 属于常态。纯微分项会把这个抖动放大成剧烈的舵机摆动所以微分必须先低通。同时积分项要限幅否则车在弯道里持续偏一侧积分会积累到非常大。下回进直道时需要很久才把这部分积分释放表现为“直道画龙”。typedef struct { float kp, ki, kd; float err, err_last; float integral, i_max, out_max; float d_filt; /* 微分低通状态 */ } PID; float pid_calc(PID *p, float target, float actual) { float d; p-err target - actual; p-integral p-err; if (p-integral p-i_max) p-integral p-i_max; if (p-integral -p-i_max) p-integral -p-i_max; /* 不完全微分一阶低通把整像素量化噪声压掉系数越小越平滑 */ d p-err - p-err_last; p-d_filt 0.3f * (d - p-d_filt); p-err_last p-err; float out p-kp * p-err p-ki * p-integral p-kd * p-d_filt; if (out p-out_max) out p-out_max; if (out -p-out_max) out -p-out_max; return out; }整定顺序是固定的先把 ki、kd 置零只加 kp 到车能在直道上跟住线但轻微摆动再加 kd 把摆动压下去最后加一点点 ki 消除稳态偏差。很多同学一上来三个参数一起调越调越乱。经验值是 kp 在 8~15offset 单位是像素输出是 PWM 千分比kd 是 kp 的 3~6 倍ki 不超过 kp 的 0.05 倍。4.3 差速分配与速度环差速分配看起来只是加减法实际上有几个坑。第一输出必须限幅否则内侧轮会算出负值电机反转车直接打转。第二限幅之后要做一次“等比缩放”如果只对超限的那一侧裁剪左右轮的速比就变了车会跑出弧线。第三低速时内侧轮占空比太低电机根本转不起来死区需要设一个最小占空比补偿。#define PWM_MAX 1000 #define PWM_MIN 120 /* 电机死区补偿低于此值轮子不转 */ static int16_t clamp_pwm(int16_t v) { if (v PWM_MAX) return PWM_MAX; if (v -PWM_MAX) return -PWM_MAX; return v; } void motor_set(int16_t base, int16_t steer) { int16_t l base - steer; int16_t r base steer; /* 死区补偿绝对值小于 PWM_MIN 时按符号抬到最小值 */ if (l 0 l PWM_MIN) l PWM_MIN; if (l 0 l -PWM_MIN) l -PWM_MIN; if (r 0 r PWM_MIN) r PWM_MIN; if (r 0 r -PWM_MIN) r -PWM_MIN; pwm_set(MOTOR_L, clamp_pwm(l)); pwm_set(MOTOR_R, clamp_pwm(r)); }速度环用编码器做反馈10ms 读一次计数差值。这里有个细节计数器是 16 位的正反转时会在 0 和 65535 之间跳直接相减会得到一个巨大的数。所以要在读取时判断符号或者干脆用 32 位的软件累加器每次读取后把增量并进去。4.4 超声波测距与避障状态机超声波在这套方案里不是主角它的作用是让车在赛道上有障碍时能减速绕行而不是撞上去。测距部分用 EXTI 双沿加 DWT 周期计数精度足够也不占定时器。/* DWT 需要在 DEMCR 里先打开 TRCENA否则 CYCCNT 不走 */ static volatile uint32_t t_start; static volatile uint8_t echo_rise; void EXTI0_IRQHandler(void) { if (EXTI-PR EXTI_PR_PR0) { EXTI-PR EXTI_PR_PR0; /* 写 1 清标志 */ if (!echo_rise) { t_start DWT-CYCCNT; echo_rise 1; } else { uint32_t ticks DWT-CYCCNT - t_start; uint32_t us ticks / (SystemCoreClock / 1000000U); echo_rise 0; /* 有效窗口约 1.7cm ~ 4.3m超出范围一律丢弃不参与滤波 */ if (us 100 us 25000) push_distance((uint16_t)(us / 58)); /* 声速换算us/58 即为 cm */ } } }滤波用“中值 一阶低通”两级。中值窗口取 5专门对付偶发的跳变值比如回波打到地面反射回来的错误距离一阶低通系数取 0.3把剩下的高频噪声压平。这一步不能省否则车会在障碍物前反复启停。状态机设计成四个状态转换条件写死避免出现“边避障边循迹”的混乱逻辑。状态进入条件动作退出条件TRACK默认正常循迹基础速度 100%距离 40cmSLOW距离 25~40cm基础速度降到 50%继续循迹距离 25cm 或回升到 45cm 以上AVOID距离 25cm按障碍物所在侧反向打差速速度 30%连续 800ms 内距离 35cmRECOVERAVOID 结束以最后一次有效 offset 为起点重新找线有效行数 ≥ 20AVOID 阶段有一个必须注意的点摄像头此时很可能看不到线车头已经偏出如果不屏蔽循迹输出两个控制量会互相冲突车会原地抖动。所以避障期间方向输出直接接管图像只用来做“是否已经回到线上”的判断。5. 避坑与排查调车现场最常见的五类翻车调车这件事绝大多数时间花在排查而不是编写代码上。下面五类问题我几乎每次都遇到按“现象—原因—解决”记下来能省下不少时间。第一类电机一转MCU 就复位或图像全黑。现象是架空正常、落地就出问题串口偶尔打印出乱码。原因是电机换向瞬间的电压尖峰顺着电源和地线窜进 MCU或者摄像头供电被拉低到复位阈值以下。解决方式是先查地线拓扑动力地和逻辑地必须单点汇合不能形成环流再查电机两端有没有并续流二极管和 0.1μF 电容最后在摄像头电源脚旁边就近加一组 LC 滤波。做完这三步九成复位问题会消失。第二类换个场地图像整片全白或全黑。现象是同一套代码在实验室跑得好好的到走廊就完全找不到线。原因是硬件二值化的阈值是固定的而走廊的灯光色温和照度跟实验室差很多黑线在图像里的灰度值变了。解决方式是引入自适应阈值每隔若干帧统计一次整幅图的黑白比例如果白点比例超过 85% 或低于 15%就说明阈值偏了用软件 DAC 或数字电位器把比较器阈值往反方向挪一档。这一步值得做因为比赛和答辩的场地往往跟调试场地不是同一个。第三类直道上车画龙越调 PID 越抖。现象是直道左右摆动加大 kd 之后摆得更快。原因通常不是 PID 参数而是控制周期和图像更新不同步——图像 40ms 更新一次控制环 5ms 跑一次8 次里有 7 次用的是同一个 offset微分项算出来全是 0第 8 次突然跳一个值微分项爆掉。解决办法是把微分改成对“图像帧序号变化”才更新或者在两次图像更新之间对 offset 做线性插值让控制环拿到的是连续变化的量而不是阶梯信号。第四类弯道丢线后车直接冲出去。现象是直角弯和急弯丢线车按最后方向打满但打满的方向可能是错的。原因是 last_offset 记录的是最后一次“有效”的偏差而在丢线前的那几帧宽度校验已经把真正有效的信息剔掉了留下的其实是一个噪声值。解决办法是记录“最后一次有效行数 ≥ 20 的帧”的 offset而不是最后一帧同时在丢线计数达到 LOST_HOLD 时用最近 5 帧 offset 的滑动平均作为方向记忆比单帧值稳定得多。第五类超声波偶尔读到 0 或者 400cm 以上。现象是车在空旷处突然刹车。原因是多次触发间隔太短导致回波串扰或者舵机转向时探头指向了吸音材料。解决办法是三次测量取中值、触发间隔不小于 60ms、把超范围值直接丢弃而不是当成有效距离参与滤波。如果车上装了舵机云台转弯期间干脆暂停测距等云台回正再采。注意排查顺序建议固定为“先电源、再时序、后算法”。电源问题会伪装成算法问题时序问题会伪装成参数问题反过来查一定绕远路。6. 验收与答辩拿一组可复现的数据说话车能跑完一圈不算交付能说清楚“为什么这么调”才算。答辩时最容易被问住的不是 PID 公式而是“你怎么证明这个参数是对的”。我的做法是在设计报告里固定放一张验收表每一项都有测量方法和判据让评委能顺着表复现。指标测量方法合格判据单圈用时光电门或秒表连续 5 次取均值波动不超过 8%脱线次数全程录像人工计数10 圈内不超过 2 次中线偏差 RMS串口回传 offset上位机统计直道 3 像素弯道 10 像素图像处理耗时GPIO 翻转 示波器单帧 6ms避障响应距离卷尺 5 次重复25±3cm 内开始减速连续运行稳定性不间断跑 10 分钟无复位、无丢帧累积测中线偏差这一步最容易被跳过但它恰恰是最有说服力的一项。做法很简单在 USART1 上按固定格式输出offset, steer, speed, valid_rows波特率 115200跑车时用上位机存成 CSV跑完画一条曲线。直道上那条线应该贴着 0 走如果出现明显的周期性正弦波说明方向环的相位裕度不够是 kd 给大了如果是缓慢漂移后突然修正说明积分饱和了。这些结论靠肉眼看车是看不出来的但曲线上一目了然。答辩陈述我建议按“需求—选型—实现—验证”四段讲重点放在两处一是为什么不用整帧图像处理把 20KB SRAM 和 72MHz 这两个硬约束摆出来评委立刻能理解你的取舍二是主动讲一个失败案例比如前面提到的“直道画龙其实是图像更新不同步”这种细节比背 PID 公式更能体现你真调过车。手边最好准备一份“黑匣子”日志把某一次翻车的 offset 曲线打印出来指着图说清楚当时改了什么、改完曲线怎么变的。这份材料比任何 PPT 都有用。我自己的习惯是每次改参数前先存一份日志命名带上日期和改动内容调完对比两条曲线。这个习惯来自一次血泪教训某次连续改了三个参数车突然跑得特别好结果第二天怎么都复现不出来因为已经不记得是哪三个参数、按什么顺序改的。从那以后我每次动 PID 之前都强制走一遍“存日志—改一个本文还有配套的精品资源点击获取