<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>云间辞 &#8211; Cloudlay</title>
	<atom:link href="https://cloudlay.cn/author/189532159/feed/" rel="self" type="application/rss+xml" />
	<link>https://cloudlay.cn</link>
	<description>life</description>
	<lastBuildDate>Sun, 14 Jun 2026 17:08:33 +0000</lastBuildDate>
	<language>zh-Hans</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.0</generator>

<image>
	<url>https://cloudlay.cn/wp-content/uploads/2026/01/avatar.ico</url>
	<title>云间辞 &#8211; Cloudlay</title>
	<link>https://cloudlay.cn</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>从0到1带你打电赛·小车电控篇(十二)：现场作战手册——四天三夜怎么打、调试避坑与精选开源</title>
		<link>https://cloudlay.cn/nuedc-car-12-field-manual/</link>
					<comments>https://cloudlay.cn/nuedc-car-12-field-manual/#respond</comments>
		
		<dc:creator><![CDATA[云间辞]]></dc:creator>
		<pubDate>Sun, 14 Jun 2026 17:08:33 +0000</pubDate>
				<category><![CDATA[嵌入式]]></category>
		<category><![CDATA[开源项目]]></category>
		<category><![CDATA[智能小车]]></category>
		<category><![CDATA[比赛经验]]></category>
		<category><![CDATA[电赛]]></category>
		<category><![CDATA[调试技巧]]></category>
		<category><![CDATA[避坑]]></category>
		<guid isPermaLink="false">https://cloudlay.cn/nuedc-car-12-field-manual/</guid>

					<description><![CDATA[📚 本文是 「从 0 到 1 带你打电赛 · 小车电控篇」 系列（共 12 篇）第 12 篇。 第1篇 · 拿 [&#8230;]]]></description>
										<content:encoded><![CDATA[<div class="ds-series" style="border:1px solid #4488ff33;background:#4488ff0d;border-radius:8px;padding:.8em 1.1em;margin:1.2em 0">
<p style="margin:0 0 .5em"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4da.png" alt="📚" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 本文是 <strong>「从 0 到 1 带你打电赛 · 小车电控篇」</strong> 系列（共 12 篇）第 12 篇。</p>
<ol style="margin:.2em 0 0;padding-left:1.4em">
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-01-how-to-score/">第1篇 · 拿奖逻辑：把比赛拆成小目标</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-02-history/">第2篇 · 赛题进化史与押题</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-03-build-and-architecture/">第3篇 · 整车搭建与代码框架</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-04-motor-power/">第4篇 · 电机驱动与电源地基</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-05-sensing/">第5篇 · 感知：灰度/电磁/编码器/IMU</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-06-pid-basics/">第6篇 · PID 入门：搞懂 P/I/D</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-07-pid-advanced/">第7篇 · PID 进阶：串级+工程补丁</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-08-pid-tuning/">第8篇 · PID 调参实战(核心)</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-09-advanced-control/">第9篇 · 进阶控制：几时该上</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-10-vision-comm/">第10篇 · K230 视觉与通信协议</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-11-architecture-fsm/">第11篇 · 状态机与整车软件</a></li>
<li style="margin:.15em 0"><strong>第12篇 · 现场作战+避坑+开源（本篇）</strong></li>
</ol>
</div>
<p>前面十一篇，我们把一辆电赛小车从里到外拆了个遍：从拿分逻辑、赛题套路，到电机电源、感知、PID、串级双环、视觉通信、状态机……该有的零件都齐了。但你会发现一个残酷的事实——<strong>赛场上翻车的队伍，十有八九不是输在&#8221;不会&#8221;，而是输在&#8221;现场没扛住&#8221;</strong>。明明实验室里跑得好好的车，一到测评台就画龙、就冲出、就死机；明明算法都对，却因为没备份、没睡觉、最后一刻手贱改了个参数，把到手的奖弄丢了。</p>
<p>这一篇是整个系列的收官，也是最接地气的一篇。我们不讲新算法，只讲一件事：<strong>怎么把四天三夜活着打完，并且把已经会的东西，稳稳地变成台上的分数。</strong> 内容分四块：四天三夜的作战节奏、一张能照着排查的故障速查表、几条保命拿分的现场细节，最后是一份精选开源仓库清单。</p>
<h2>一、四天三夜，到底是怎么个打法</h2>
<p>电赛全国赛是<strong>四天三夜、约 84 小时连续作战</strong>（一般从第 1 天早上 8 点，到第 4 天晚上 20 点封箱上交）。这不是一场冲刺，是一场马拉松——而且是你自己一边造跑鞋一边跑的马拉松。</p>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 一个贯穿全程的比喻</p>
<p>把这四天想成跑一场&#8221;自己造跑鞋的马拉松&#8221;：第 1 天做鞋（搭机械、焊板子），第 2 天试跑（联调），第 3 天调配速、写日记、还得补个觉，第 4 天上午把装备定死、然后稳稳跑到终点——<strong>中途大改方案，约等于弃赛。</strong></p>
</div>
<p>队伍标准配置是 <strong>2 软 1 硬</strong>（两人写软件、一人管硬件），但分工不是各扫门前雪，而是有节奏地交替。多支国奖队伍的复盘高度一致，节奏大致是这样：</p>
<figure class="ds-diagram" style="text-align:center;margin:1.3em 0"><img decoding="async" src="https://cloudlay.cn/wp-content/uploads/dianseiche/0534e6c9e984.png" alt="电赛小车电控 · 示意图" loading="lazy" style="max-width:100%;height:auto;background:#fff;border-radius:8px;padding:6px;box-shadow:0 1px 6px rgba(0,0,0,.25)"></figure>
<p><strong>第 1 天：定平台、搭机械、焊板子。</strong> 这是最忙也最关键的一天。先按题目的硬约束把平台选死（尺寸、主控、传感器都要合规，下文细说），然后机械组全力把车搭出来。</p>
<div class="ds-callout ds-callout-important" style="border-left:4px solid #00bcd4;background:#00bcd414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bcd4"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2757.png" alt="❗" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 机械搭得好，算法轻松十倍</p>
<p>这几乎是所有国奖选手复盘里的原话。机械松垮、重心歪、左右两个电机转速差太大，软件再怎么调都救不回一条直线——有位作者就吃过亏：<a href="https://github.com/hongfeiyucode/find_way_car" target="_blank" rel="noopener noreferrer">两个电机转速差太大，不在程序里补偿就根本走不直</a>。所以记住一句话：<strong>车跑偏，先怀疑机械，再怀疑代码</strong>，别一上来就猛加积分。</p>
</div>
<p><strong>第 2 天：模块联调。</strong> 机械稳定后，把感知、电机、PID、通信一块块接起来跑通。这一天的目标是&#8221;全程能慢速跑完&#8221;，不求快。</p>
<p><strong>第 3 天：核心调试 + 开始写报告。</strong> 这是分水岭。两个软件队员轮流——一个调车，一个写报告，<strong>报告随做随存、不要攒到最后赶</strong>。</p>
<div class="ds-callout ds-callout-danger" style="border-left:4px solid #ff1744;background:#ff174414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff1744"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 第 3 天晚上，必须睡 1~2 小时</p>
<p>这是用血泪换来的铁律。多支国奖队伍复盘一致：第 3 晚通宵，会直接导致第 4 天上午定稿、测评时判断力崩盘，反而出错。带个折叠床或睡垫，排班保证至少一人清醒值守。很多卡死的问题，<strong>睡一觉醒来常常迎刃而解</strong>；硬熬下去，边际收益极低。</p>
</div>
<p><strong>第 4 天上午：定稿封箱。</strong> 不再改结构，最后一小时彻底停手，把能跑的版本稳稳交上去。</p>
<h3>报告：边做边写，对着评分表填空</h3>
<p>很多人误以为报告要憋到最后熬夜写。错。报告应该是<strong>第 3 天就开始、随做随存</strong>——因为它本质上是一道&#8221;按点给分&#8221;的填空题。</p>
<p>回顾我们在《这比赛到底在比什么》一篇说过的结论：现在的小车题<strong>总分 120 = 客观测试 100 + 设计报告 20</strong>，报告只占约 1/6。</p>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 别再信&quot;报告占 1/3&quot;的老说法</p>
<p>网上很多二手资料还在说&#8221;总分 150、报告 50、占三分之一&#8221;。这是 2019 年前的旧格式。按近年官方单题评分表（2021 F 题送药小车、2024 H 题自动行驶小车），实测都是<strong>总分 120、报告 20，约占 1/6</strong>。<strong>别按 1/3 去规划报告投入</strong>——把时间砸在让车真跑起来上，性价比高得多。</p>
</div>
<p>报告 20 分怎么分？以 2024 H 题官方评分表为例：</p>
<table>
<thead>
<tr>
<th>评分小项</th>
<th>分值</th>
<th>你该填什么</th>
</tr>
</thead>
<tbody>
<tr>
<td>系统方案</td>
<td>3</td>
<td>整车系统框图、方案比较与选型理由</td>
</tr>
<tr>
<td>理论分析与计算</td>
<td>5</td>
<td>误差来源分析、PID 推导、关键参数计算</td>
</tr>
<tr>
<td>电路与程序设计</td>
<td>5</td>
<td>电路框图、核心流程图 / 状态机图</td>
</tr>
<tr>
<td>测试方案与结果</td>
<td>4</td>
<td>实测数据表 + 结果分析（最值钱的&#8221;闭环&#8221;证据）</td>
</tr>
<tr>
<td>报告结构规范性</td>
<td>3</td>
<td>摘要、图表编号、格式整洁</td>
</tr>
</tbody>
</table>
<p>写报告的诀窍就一句：<strong>对着这张表，一项一项填。</strong> 其中体现&#8221;测了 → 有数据 → 会分析&#8221;这条闭环最值钱。还要注意一点：<strong>测试数据必须真实可复现</strong>。因为拟获一等奖的队伍要过&#8221;综合测评&#8221;这一关（满分 30 分，统一在 8 月 10 日 8:00–15:00 闭卷制作，禁网、禁手机、禁电脑，只能带纸质资料，至少 3 名专家共同记录签字）。编造数据、或报告与实物对不上，是大忌。</p>
<h2>二、调试与避坑：分模块自测 + 故障速查表</h2>
<p>调试最忌讳&#8221;啥都接上、一把上电、然后对着不动的车发呆&#8221;。正确姿势是<strong>分模块自测，从下往上一层层确认</strong>，每层 OK 了再叠下一层：</p>
<ul>
<li>☐ <strong>电源</strong>：上电先量各路电压（电机轨、5V、3.3V），别让 MCU 欠压复位</li>
<li>☐ <strong>电机方向</strong>：低 PWM 单独给每个电机，确认正转 / 反转方向对、没接反</li>
<li>☐ <strong>编码器</strong>：手动转轮子，看脉冲计数有没有变、正反方向符号对不对</li>
<li>☐ <strong>传感器</strong>：灰度看黑白阈值、IMU 看 Yaw 静止漂不漂、视觉看回传数据</li>
<li>☐ <strong>速度环（内环）</strong>：给固定目标速度，看能不能稳准跟随</li>
<li>☐ <strong>方向 / 循迹环（外环）</strong>：内环稳了再叠外环，先慢速循迹</li>
<li>☐ <strong>状态机 + 保护</strong>：最后把丢线找回、出界急停、视觉超时降级接上</li>
</ul>
<p>下面这张速查表，建议直接打印贴在工位上。绝大多数现场故障都能在里面对号入座：</p>
<table>
<thead>
<tr>
<th>症状</th>
<th>最可能的原因</th>
<th>怎么治</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>画龙 / 来回振荡</strong></td>
<td>方向环 Kp 太大；或转向环乱加了 I</td>
<td>减小 Kp；转向 / 循迹环<strong>只用 PD，别加 I</strong>（积分会引入延迟，弯道拖沓画龙）</td>
</tr>
<tr>
<td><strong>老往一边偏（走不直）</strong></td>
<td>机械不对称、左右电机转速差</td>
<td><strong>加固定前馈 offset</strong>（如某省一队给右轮 offset=30），而不是无限加 Ki</td>
</tr>
<tr>
<td><strong>只嗡嗡响、电机不转</strong></td>
<td>PWM 太低没过启动死区；驱动没使能；只接了方向脚没给 PWM</td>
<td>加死区补偿、查使能脚、确认 PWM 输入有信号（详见《让电机听话》）</td>
</tr>
<tr>
<td><strong>一加速就复位 / 死机</strong></td>
<td>电源跌落、VM 没并大电容；或多串口 / 多中断在 80MHz 下抢资源卡死</td>
<td>驱动 VM 并大电解电容、电机地与控制地单点共地；理顺中断优先级</td>
</tr>
<tr>
<td><strong>视觉时好时坏</strong></td>
<td>串口丢字节、没做超时保护，吃了过期数据</td>
<td>DMA + IDLE 空闲中断收包，&gt;100ms 超时就降级到灰度 / IMU（详见《视觉与通信》）</td>
</tr>
<tr>
<td><strong>Yaw 越跑越偏</strong></td>
<td>陀螺仪零漂；纯里程 / 纯陀螺没有绝对参考</td>
<td>开机静止采零偏；用灰度过点 / 到点标志定期校正航向</td>
</tr>
<tr>
<td><strong>响应慢 / 回复慢</strong></td>
<td>P 偏小，或速度环 I 作用不够</td>
<td>适当加 P；静差大就加 I（速度环）</td>
</tr>
<tr>
<td><strong>超调大、冲过头</strong></td>
<td>D 不够，或 P 太猛</td>
<td>加 D 抑制超调；理想阶跃响应是 <strong>4:1 衰减</strong>（前高后低两个波）</td>
</tr>
</tbody>
</table>
<div class="ds-callout ds-callout-note" style="border-left:4px solid #448aff;background:#448aff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#448aff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4dd.png" alt="📝" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 看波形治百病</p>
<p>表里这些症状，最高效的判断方式是用 <code>printf</code> 把&#8221;目标速度, 实际速度&#8221;两路发到 VOFA+ 画曲线，对着波形对症下药。这套&#8221;波形症状 → 调哪个参数&#8221;的诊断方法是调参的核心功夫，完整流程见《PID 调参实战》一篇，这里不展开。</p>
</div>
<h3>几个最容易踩的坑，单独拎出来强调</h3>
<div class="ds-callout ds-callout-danger" style="border-left:4px solid #ff1744;background:#ff174414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff1744"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> PID 没加限幅 = 定时炸弹</p>
<p>位置式 PID 不给积分限幅，长时间偏差后积分项会<strong>爆掉（积分饱和）</strong>：误差反向了还久久退不出来，表现为大幅过冲、长时间画龙；输出不限幅则直接顶满 PWM。<strong>积分项要单独限幅，总输出也要限幅，两道都不能少。</strong> 速度环改用增量式 PI 能天然抗饱和，更适合电机调速。</p>
</div>
<div class="ds-callout ds-callout-danger" style="border-left:4px solid #ff1744;background:#ff174414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff1744"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 全白丢线时的&quot;除零陷阱&quot;</p>
<p>灰度循迹算偏差时，分母（各路灰度之和）一旦接近 0（全白、冲出赛道）就会除零或乱转。<strong>必须做丢线保护</strong>：沿用上一次的偏差方向继续打死转向，或切回角度闭环、减速找线。尤其是&#8221;只能前进不能后退&#8221;的题型（如 2024 H 题），冲出去没法倒车找线，全靠转向纠回，这个保护不做就是直接出界 0 分。</p>
</div>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 控制周期被阻塞拖慢</p>
<p>别在定时中断里写阻塞式 <code>delay</code>，也别在中断里刷 OLED、发一长串串口。中断里<strong>只做&#8221;读传感器 + 算 PID + 写 PWM&#8221;</strong>，耗时的打印 / 显示放主循环。否则控制周期被拖得忽快忽慢，$K_i$、$K_d$ 的等效系数会随周期乱跳，波形抖成一团。</p>
</div>
<h2>三、保命拿分：现场细节决定生死</h2>
<p>讲完技术，再讲几条&#8221;不写代码但能救命&#8221;的现场策略。这些每一条背后都有人摔过跟头。</p>
<h3>求稳，永远优于求全</h3>
<p>这是整个系列反复强调、也是现场最值钱的一条心法。回顾《这比赛到底在比什么》：客观测试约占 83%，而且有一堆<strong>&#8220;失败即 0 分&#8221;的红线</strong>——投影脱离弧线、整车越黑线、超时达规定 1 倍以上、用错主控、装了禁用的摄像头、尺寸超标、用了禁用的麦轮 / 履带……任何一条中招，该项甚至全场归零，比少拿几分发挥部分严重得多。</p>
<p>所以拿分顺序应该是：<strong>先吃满低门槛的基本要求 → 再啃高分项 → 发挥部分按&#8221;能不能稳定复现&#8221;来取舍。</strong></p>
<div class="ds-callout ds-callout-example" style="border-left:4px solid #7c4dff;background:#7c4dff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#7c4dff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 两个真实的反面教材</p>
<ul>
<li>某国奖队省赛已经稳稳拿下前 4 个基本要求，被胜利冲昏头脑去强冲第 5 个发挥要求，结果<strong>把装置烧了</strong>，最终只 2 项达标，从国一掉到国二。（<a href="https://blog.csdn.net/qq_32666555/article/details/79312413" target="_blank" rel="noopener noreferrer">来源</a>）</li>
<li>2024 年某队在 30 分钟综合测评里，<strong>第 19 分钟还在改 PID 参数</strong>，系统当场振荡，失去全部剩余测试机会。（<a href="https://aihardware.csdn.net/69bcfb9a54b52172bc62d7f1.html" target="_blank" rel="noopener noreferrer">来源</a>）</li>
</ul>
</div>
<p>两个故事一个结论：<strong>冲高风险的发挥项之前，先问自己——万一失败，会不会把已经到手的分一起赔进去？</strong> 不稳定的功能，宁可不做。</p>
<h3>测评现场和定稿后，绝不大改硬件和 PID 参数</h3>
<p>现场测评一般有 <strong>30 分钟准备时间</strong>（电路恢复、虚焊重焊、换普通元件都行；超时则每超 1 分钟扣 1 分），但<strong>封箱后硬件不可改</strong>。第 4 天上午定稿之后，更要把&#8221;求稳&#8221;刻进 DNA：</p>
<div class="ds-callout ds-callout-danger" style="border-left:4px solid #ff1744;background:#ff174414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff1744"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 最后时刻改参数，是性价比最低的赌博</p>
<p>后期人疲劳、脑子冲动，改参数的收益远小于翻车的风险。确实要微调，也<strong>只改数值不改结构</strong>，改之前先 <code>git tag</code> 备份一个能跑的版本，留足回滚时间。上面那个&#8221;第 19 分钟改参数全盘皆输&#8221;的故事，就是没留回滚时间的典型。</p>
</div>
<h3>备件全双份，且要趁早囤</h3>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 赛题发布时，常用器件可能已经断货</p>
<p>立创官方提醒：常用器件清单往往赛前一周才发布，等赛题正式发布，很多器件<strong>早就被抢断货了</strong>。所以经费允许的话，<strong>买最好的，而且多买几个</strong>：主控、电机、驱动、传感器都囤 2~3 份。（<a href="https://wiki.lckfb.com/zh-hans/lspi/competition/competition-guide.html" target="_blank" rel="noopener noreferrer">来源</a>）</p>
</div>
<p>别只盯着电子元件。备件清单里还要有：<strong>笔（报告全程手写，至少 2 支）、热熔胶、封箱用的木板 / PVC 板</strong>。有人现场才发现没带够笔——细节翻车，最冤。另外建议赛前先把 2~3 道历年题完整做一遍，把循迹、测距、视觉这些算法模块当&#8221;半成品&#8221;储备好，现场直接调用。</p>
<h3>用一份问题清单，把疲劳期的脑子&#8221;外包&#8221;出去</h3>
<p>国奖队的另一个习惯：全程维护一份 <strong>Bug 清单（随手记下遇到的问题和解法）</strong>。等到第 3、4 天人困马乏时，靠这份清单快速定位&#8221;这个坑我之前是怎么填的&#8221;。本质上就是<strong>把疲劳期的认知负担，前置到清醒的时候</strong>——和&#8221;报告随做随存&#8221;是同一个道理。</p>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 现场行动准则三连</p>
<ul>
<li>一个问题卡超过 10 分钟没思路，就换路子或求助队友 / 资料，别死磕（4 天里时间是最贵的资源）。</li>
<li>每个能跑的版本，立刻打包备份；&#8221;能复现&#8221;比&#8221;更优&#8221;重要得多。</li>
<li>心态上：你的目标是稳稳拿到该拿的分，不是做出最炫的车。</li>
</ul>
</div>
<h2>四、精选开源仓库与学习资源</h2>
<p>最后送上一份&#8221;站在巨人肩膀上&#8221;的清单。这些都是社区里口碑不错的开源项目，逐个评注它们是什么、能借鉴多少。</p>
<div class="ds-callout ds-callout-important" style="border-left:4px solid #00bcd4;background:#00bcd414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bcd4"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2757.png" alt="❗" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 一句话用法</p>
<p><strong>先看懂、再借鉴，别整包照抄——尤其是 PID 参数，必须按你自己的车重、电机、地面重新调。</strong> 看懂别人&#8221;为什么这么写&#8221;，比抄下来重要一百倍。</p>
</div>
<h3>MSPM0 路线（2024 起电赛官方主推）</h3>
<table>
<thead>
<tr>
<th>仓库</th>
<th>是什么</th>
<th>可复用度</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://github.com/ZhijianLi2003/ZLC_MSPM0_Peripheral_Library" target="_blank" rel="noopener noreferrer">ZhijianLi2003/ZLC_MSPM0_Peripheral_Library</a></td>
<td>山大（威海）ZLC 队 2024 H 题<strong>国一</strong>作品：完整外设库 + 串级 PID（方向 PD + 左右轮增量式 PI）+ 1024 线编码器 + 8 路灰度 + IMU + DRV8701E，控制周期 20ms；含设计报告 PDF、B 站讲解、立创 EDA 扩展板，还带&#8221;无线急停开关&#8221;做失败保护</td>
<td><strong>极高</strong>：可直接当 MSPM0 小车的代码骨架和报告范本，参数与控制周期都能借鉴</td>
</tr>
<tr>
<td><a href="https://github.com/menoking/PIDCarTemplate-MSPM0G3507" target="_blank" rel="noopener noreferrer">menoking/PIDCarTemplate-MSPM0G3507</a></td>
<td>电赛 MSPM0 两轮 / 四轮 PID 小车模板：封装好延时 / 编码器 / 电机 / 按键（短按长按双击）/ MPU6050 / OLED / 蓝牙 / 灰度 API，速度 / 转向 / 距离 / 陀螺仪多环参数初值齐全，克隆即用</td>
<td><strong>高</strong>：新车起步脚手架，省下大量外设移植时间</td>
</tr>
<tr>
<td><a href="https://github.com/abcuer/2024-NUEDC-H-TI_CAR" target="_blank" rel="noopener noreferrer">abcuer/2024-NUEDC-H-TI_CAR</a></td>
<td>24H 题省一惯导 + 巡线小车：分层状态机，<code>pid.c</code> 增量 / 位置双模、<code>track.c</code> 灰度加权、<code>angle.c</code>、<code>dist.c</code> 代码清晰，含实测参数、查表补偿、EDA 开源、选型清单</td>
<td><strong>很高</strong>：代码可读 + 真实可抄参数，循迹小车最佳学习样本之一</td>
</tr>
<tr>
<td><a href="https://github.com/Torris-Yin/mspm0-modules" target="_blank" rel="noopener noreferrer">Torris-Yin/mspm0-modules</a></td>
<td>MSPM0 常用模块驱动合集，库内人气最高，配 B 站课程（注意：它要求其他中断优先级都低于 SysTick）</td>
<td><strong>高</strong>：模块多 + 教学视频，按需取用单个驱动</td>
</tr>
<tr>
<td><a href="https://github.com/danshoujieyi/TI-MSPM0G3507" target="_blank" rel="noopener noreferrer">danshoujieyi/TI-MSPM0G3507</a></td>
<td>电赛指定板工程模板三件套：裸机 / FreeRTOS / RT-Thread，两套 RTOS 都移植好了</td>
<td><strong>高</strong>（想上 RTOS 的队）：带操作系统的工程骨架</td>
</tr>
<tr>
<td><a href="https://github.com/TexasInstruments/mspm0-sdk" target="_blank" rel="noopener noreferrer">TexasInstruments/mspm0-sdk</a></td>
<td>TI 官方 MSPM0 SDK（DriverLib + SysConfig），近年控制类题常指定 MSPM0，官方一手资料</td>
<td><strong>高</strong>：官方底层驱动权威来源，但需配合 SysConfig 图形配置</td>
</tr>
</tbody>
</table>
<h3>STM32 / 跨平台路线（练手和借鉴思路）</h3>
<table>
<thead>
<tr>
<th>仓库</th>
<th>是什么</th>
<th>可复用度</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://github.com/Pansamic/CarOS" target="_blank" rel="noopener noreferrer">Pansamic/CarOS</a></td>
<td>电赛小车底层驱动 + 应用接口框架，STM32 / MSP432 跨平台，封装电机驱动 / 速度环 / 运动学闭环（需配 FreeRTOS）</td>
<td><strong>中高</strong>：架构规范、可移植；非 MSPM0 需自适配</td>
</tr>
<tr>
<td><a href="https://github.com/we-worker/STM32f407Zgt6-project-temp" target="_blank" rel="noopener noreferrer">we-worker/STM32f407Zgt6-project-temp</a></td>
<td>启明欣欣 STM32F407 框架，多分支含 23H 题 / FFT / ADC / TIM / DMA 采样 / DAC-DMA / GUI</td>
<td><strong>中高</strong>（STM32 路线）：信号处理与采样模板丰富</td>
</tr>
<tr>
<td><a href="https://github.com/hongfeiyucode/find_way_car" target="_blank" rel="noopener noreferrer">hongfeiyucode/find_way_car</a></td>
<td>自称&#8221;最快&#8221;的 STM32 循迹小车，含循迹 / 寻光 / 切换 / 安卓遥控；作者复盘&#8221;两电机转速差必须程序补偿才走直&#8221;</td>
<td><strong>中</strong>：入门循迹 + 真实踩坑记录</td>
</tr>
</tbody>
</table>
<h3>题库与官方文档（必备底料）</h3>
<ul>
<li><a href="https://github.com/CCBP/NUEDC_Topic" target="_blank" rel="noopener noreferrer">CCBP/NUEDC_Topic</a>：历年全国电赛真题 PDF 合集，每题含官方评分标准表。<strong>研究评分逻辑、看清&#8221;基本要求 vs 发挥部分&#8221;分值的第一手权威来源</strong>，做题型预测和能力反推必备。</li>
<li><a href="https://wiki.lckfb.com/zh-hans/lspi/competition/competition-guide.html" target="_blank" rel="noopener noreferrer">立创开发板 lspi 竞赛指南 wiki</a>：备赛流程、器件采购、按分值优先级拿分等实操建议，前面引用的&#8221;器件易断货&#8221;&#8221;按分值优先级取舍&#8221;都出自这里。</li>
</ul>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 怎么把这些仓库串起来用</p>
<p>推荐路线：先用 ZLC 或 PIDCarTemplate 把框架立起来，再拿 abcuer 当&#8221;答案&#8221;对照着读懂循迹与状态机怎么写，最后翻 CCBP 的真题评分表，反推自己这届该把火力集中在哪。</p>
</div>
<h2>写在最后</h2>
<p>到这里，&#8221;从 0 到 1 带你打电赛·小车电控篇&#8221;十二篇就全部讲完了。我们从&#8221;这比赛在比什么&#8221;出发，一路走过赛题进化、整车搭建、电机电源、感知、PID 入门到进阶、调参实战、进阶算法、视觉通信、状态机，最后落到今天这份现场作战手册。</p>
<p>如果只让我留一句话给即将上场的你，那就是开篇就埋下、今天又反复出现的那条主线——</p>
<div class="ds-callout ds-callout-success" style="border-left:4px solid #00c853;background:#00c85314;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00c853"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 稳 → 准 → 快</p>
<p>先让车<strong>不冲出、不挂零</strong>，再让它<strong>定点停准</strong>，最后才拼<strong>用时更快</strong>。能力是一层层叠上去的，分数也是。把基础做扎实、把保护做齐全、把能跑的版本备份好，剩下的，交给那 84 个小时里你和队友的默契。</p>
</div>
<p>电赛不只是一场比赛，它会逼你在极限压力下，把书本上的电机、PID、状态机真正变成手里能跑的东西。那种&#8221;它终于稳稳停在终点&#8221;的瞬间，值得你熬的每一个夜。</p>
<p>去吧，把车跑稳。祝你拿奖。</p>
<div class="ds-series" style="border:1px solid #4488ff33;background:#4488ff0d;border-radius:8px;padding:.8em 1.1em;margin:1.2em 0">
<p style="margin:0 0 .5em"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4da.png" alt="📚" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 本文是 <strong>「从 0 到 1 带你打电赛 · 小车电控篇」</strong> 系列（共 12 篇）第 12 篇。</p>
<ol style="margin:.2em 0 0;padding-left:1.4em">
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-01-how-to-score/">第1篇 · 拿奖逻辑：把比赛拆成小目标</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-02-history/">第2篇 · 赛题进化史与押题</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-03-build-and-architecture/">第3篇 · 整车搭建与代码框架</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-04-motor-power/">第4篇 · 电机驱动与电源地基</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-05-sensing/">第5篇 · 感知：灰度/电磁/编码器/IMU</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-06-pid-basics/">第6篇 · PID 入门：搞懂 P/I/D</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-07-pid-advanced/">第7篇 · PID 进阶：串级+工程补丁</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-08-pid-tuning/">第8篇 · PID 调参实战(核心)</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-09-advanced-control/">第9篇 · 进阶控制：几时该上</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-10-vision-comm/">第10篇 · K230 视觉与通信协议</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-11-architecture-fsm/">第11篇 · 状态机与整车软件</a></li>
<li style="margin:.15em 0"><strong>第12篇 · 现场作战+避坑+开源（本篇）</strong></li>
</ol>
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://cloudlay.cn/nuedc-car-12-field-manual/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>从0到1带你打电赛·小车电控篇(十一)：把一切串起来——状态机、控制环时序与整车软件架构</title>
		<link>https://cloudlay.cn/nuedc-car-11-architecture-fsm/</link>
					<comments>https://cloudlay.cn/nuedc-car-11-architecture-fsm/#respond</comments>
		
		<dc:creator><![CDATA[云间辞]]></dc:creator>
		<pubDate>Sun, 14 Jun 2026 17:08:33 +0000</pubDate>
				<category><![CDATA[嵌入式]]></category>
		<category><![CDATA[FreeRTOS]]></category>
		<category><![CDATA[实时控制]]></category>
		<category><![CDATA[智能小车]]></category>
		<category><![CDATA[状态机]]></category>
		<category><![CDATA[电赛]]></category>
		<category><![CDATA[软件架构]]></category>
		<guid isPermaLink="false">https://cloudlay.cn/nuedc-car-11-architecture-fsm/</guid>

					<description><![CDATA[📚 本文是 「从 0 到 1 带你打电赛 · 小车电控篇」 系列（共 12 篇）第 11 篇。 第1篇 · 拿 [&#8230;]]]></description>
										<content:encoded><![CDATA[<div class="ds-series" style="border:1px solid #4488ff33;background:#4488ff0d;border-radius:8px;padding:.8em 1.1em;margin:1.2em 0">
<p style="margin:0 0 .5em"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4da.png" alt="📚" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 本文是 <strong>「从 0 到 1 带你打电赛 · 小车电控篇」</strong> 系列（共 12 篇）第 11 篇。</p>
<ol style="margin:.2em 0 0;padding-left:1.4em">
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-01-how-to-score/">第1篇 · 拿奖逻辑：把比赛拆成小目标</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-02-history/">第2篇 · 赛题进化史与押题</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-03-build-and-architecture/">第3篇 · 整车搭建与代码框架</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-04-motor-power/">第4篇 · 电机驱动与电源地基</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-05-sensing/">第5篇 · 感知：灰度/电磁/编码器/IMU</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-06-pid-basics/">第6篇 · PID 入门：搞懂 P/I/D</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-07-pid-advanced/">第7篇 · PID 进阶：串级+工程补丁</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-08-pid-tuning/">第8篇 · PID 调参实战(核心)</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-09-advanced-control/">第9篇 · 进阶控制：几时该上</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-10-vision-comm/">第10篇 · K230 视觉与通信协议</a></li>
<li style="margin:.15em 0"><strong>第11篇 · 状态机与整车软件（本篇）</strong></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-12-field-manual/">第12篇 · 现场作战+避坑+开源</a></li>
</ol>
</div>
<p>电赛小车系列前面十篇，我们把电控的零件一个个磨出来了：电机能听话转、传感器看得见路、PID 能把速度和方向调稳、视觉和主控也能对上话。但这些都还只是&#8221;零件&#8221;。这一篇，我们要做的是把所有零件焊成一台真正能在赛道上跑的车。</p>
<p>你会发现一个有意思的现象：很多队伍每个模块单测都好好的——电机转得欢、灰度读得准、PID 波形漂亮——可一旦合到一起跑整车，就开始抽风：要么卡死不动，要么时灵时不灵，要么过个十字就冲出去了。问题几乎都不在某个模块本身，而在&#8221;怎么把它们组织起来、谁先跑谁后跑、出了岔子怎么兜底&#8221;。这就是整车软件架构要解决的事。</p>
<p>这一篇我们讲清四件事：<strong>分层</strong>（代码怎么摆放）、<strong>调度</strong>（谁在什么时候跑）、<strong>控制环时序</strong>（一拍里按什么顺序干活）、<strong>状态机</strong>（整车行为怎么管），最后再钉一遍贯穿全系列的保护逻辑和那条&#8221;机械定上限、软件定下限&#8221;的硬道理。吃透这几件事，你的车就从&#8221;一堆能动的零件&#8221;变成&#8221;一台能比赛的车&#8221;。</p>
<h2>一、分层架构：让你的算法&#8221;换芯片不换脑子&#8221;</h2>
<p>先讲个大白话比喻。写整车软件像点外卖：</p>
<div class="ds-callout ds-callout-example" style="border-left:4px solid #7c4dff;background:#7c4dff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#7c4dff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 分层就是各司其职</p>
<p>你（<strong>应用层 App</strong>）只管对着 App 下单，从不冲进后厨。App（<strong>中间件层</strong>）把你的订单翻译成&#8221;做一份番茄炒蛋、骑手送到 3 号楼&#8221;。真正干脏活累活的是商家灶台和骑手（<strong>驱动层 / 硬件抽象层</strong>）。 妙处在于：你换个城市（换芯片），点外卖的方式一点不变，变的只是当地的骑手和店家（驱动层）。</p>
</div>
<p>这正是逐飞、恩智浦那套智能车开源库多年沉淀下来的标准做法。把代码分成四层，上层依赖下层、下层不知道上层的存在：</p>
<figure class="ds-diagram" style="text-align:center;margin:1.3em 0"><img decoding="async" src="https://cloudlay.cn/wp-content/uploads/dianseiche/38c16a37cfcf.png" alt="电赛小车电控 · 示意图" loading="lazy" style="max-width:100%;height:auto;background:#fff;border-radius:8px;padding:6px;box-shadow:0 1px 6px rgba(0,0,0,.25)"></figure>
<p>以逐飞 SeekFree 库为例，它的真实分层是这样的：</p>
<table>
<thead>
<tr>
<th>层</th>
<th>逐飞里叫什么</th>
<th>放什么</th>
<th>芯片相关吗</th>
</tr>
</thead>
<tbody>
<tr>
<td>公共层</td>
<td><code>zf_common</code></td>
<td>时钟、中断、调试配置</td>
<td>弱相关</td>
</tr>
<tr>
<td>驱动层</td>
<td><code>zf_driver</code></td>
<td>PWM、PIT 定时中断、编码器、ADC、GPIO、UART、SPI</td>
<td><strong>强相关</strong></td>
</tr>
<tr>
<td>设备层</td>
<td><code>zf_device</code></td>
<td>摄像头、陀螺、编码器、屏的初始化和应用函数</td>
<td>弱相关</td>
</tr>
<tr>
<td>应用层</td>
<td>你自己写的 <code>code</code></td>
<td>状态机、循迹决策、串级 PID 调用</td>
<td>无关</td>
</tr>
</tbody>
</table>
<p>中间件层（PID、滤波、协议、元素识别）这些纯算法，本质上和芯片没有任何关系——它们只是数学，搬到任何平台上都一样跑。</p>
<h3>一条必须刻进脑子的铁律</h3>
<div class="ds-callout ds-callout-danger" style="border-left:4px solid #ff1744;background:#ff174414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff1744"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 上层只调下层，下层绝不反调；设备层禁止直接碰芯片 API</p>
<p>设备层（<code>zf_device</code>）只能调驱动层（<code>zf_driver</code>）的接口，<strong>绝不能直接去调 <code>HAL_GPIO_WritePin</code> 这种最底层的芯片 API</strong>。逐飞为了彻底解耦，连设备层都全部通过驱动层拿接口，连 <code>extern</code> 都不用。</p>
</div>
<p>这条铁律还是用打比方理解最快：</p>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 像公司里的汇报关系</p>
<p>下属向上级汇报（对外暴露接口），老板不会替下属干活，下属也别越级跑去机房乱动设备（直接调 HAL）。一旦有人绕过流程，整个公司就乱套了——你换个机房（换芯片），就得满地找哪儿被人偷偷动过。</p>
</div>
<p><strong>为什么值得花这个力气分层？</strong> 因为它直接决定了你前面那些功夫能不能&#8221;白嫖&#8221;过来。整车搭建那一篇讲过&#8221;STM32 练手、MSPM0 比赛&#8221;的策略，分层就是这套策略能成立的根。看一组实打实的证据：同一套逐飞库，社区已经移植到了 RT1064、英飞凌 TC264、STC32G144K，甚至 2025 年 TI 板电赛用的 <a href="https://github.com/woai66/SeekFree_MSPM0G3507_Opensource_Library" target="_blank" rel="noopener noreferrer">MSPM0G3507</a>。这些版本的 App 层和算法层<strong>几乎一字不改</strong>，差异全部集中在底层驱动。</p>
<div class="ds-callout ds-callout-success" style="border-left:4px solid #00c853;background:#00c85314;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00c853"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 这意味着什么</p>
<p>你在 STM32 上调通的串级 PID、写好的状态机、磨好的丢线找回逻辑，换到 TI MSPM0 比赛时，<strong>只动驱动层就能整段平移过去</strong>。不分层，换芯片就是重写；分层做好，换芯片只是换驱动。</p>
</div>
<p>想找范本直接抄结构的，推荐 <a href="https://github.com/Sakuramdd/SeekFree_RT1064_Opensource_Library" target="_blank" rel="noopener noreferrer">Sakuramdd/SeekFree_RT1064_Opensource_Library</a>，标准三层加上 <code>code</code> 目录（image / control / init），是研究分层与解耦最干净的样本。STM32 与 MSPM0 之间具体怎么对应 API、平移时要注意什么，整车搭建那一篇已经给过对照心法，这里不展开。</p>
<h2>二、调度：电赛首选&#8221;前后台裸机&#8221;，别动不动就上 RTOS</h2>
<p>代码摆好了，下一个问题是：这么多任务——读传感器、跑 PID、刷屏、收视觉数据——谁在什么时候跑？这叫<strong>调度</strong>。常见的有三档，我们从简单到复杂排一下。</p>
<h3>第一档：前后台裸机（电赛首选）</h3>
<p>这是最常见、最可预测的方案，也是绝大多数电赛车的选择。结构极简：</p>
<div class="ds-callout ds-callout-example" style="border-left:4px solid #7c4dff;background:#7c4dff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#7c4dff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 值班大爷和门铃</p>
<p><strong>主循环</strong>是&#8221;后台&#8221;，像值班大爷慢悠悠扫地、刷屏、收通信，干那些不急的慢活。<strong>定时器中断</strong>是&#8221;前台&#8221;，像门铃——一响立刻放下扫帚去开门（跑控制环），开完门接着扫地。 关键纪律：门铃要<strong>快进快出</strong>，开个门别在门口跟人唠嗑（中断服务函数别太长），不然下一个客人（下一个控制周期）就堵在门口了。</p>
</div>
<p>把这套结构画出来就是：</p>
<figure class="ds-diagram" style="text-align:center;margin:1.3em 0"><img decoding="async" src="https://cloudlay.cn/wp-content/uploads/dianseiche/b4d08705fbeb.png" alt="电赛小车电控 · 示意图" loading="lazy" style="max-width:100%;height:auto;background:#fff;border-radius:8px;padding:6px;box-shadow:0 1px 6px rgba(0,0,0,.25)"></figure>
<h3>第二档：时间片轮询</h3>
<p>如果你有好几个不同周期的任务（控制环 5ms、读视觉 20ms、刷屏 100ms），可以用一个简单的时间片调度器：用一个 1ms 的定时器统一递减计时，到点就给任务打个&#8221;该跑了&#8221;的标记，主循环看到标记再去执行。</p>
<pre><code class="language-c">// 时间片轮询调度器：一个 1ms 的 tick 管多周期任务
typedef struct { uint16_t period, timer; uint8_t run; void(*task)(void); } Task_t;

Task_t tasks[] = {
    {5,   5,   0, Control_Loop},  // 5ms：  控制环
    {20,  20,  0, Read_Vision},   // 20ms： 收视觉
    {100, 100, 0, Update_OLED},   // 100ms：刷屏
};
#define N (sizeof(tasks) / sizeof(tasks[0]))

void SysTick_Handler(void) {          // 1ms tick
    for (int i = 0; i &lt; N; i++) {
        if (tasks[i].timer == 0) { tasks[i].timer = tasks[i].period; tasks[i].run = 1; }
        else tasks[i].timer--;
    }
}

int main(void) {
    Init();
    while (1)
        for (int i = 0; i &lt; N; i++)
            if (tasks[i].run) { tasks[i].run = 0; tasks[i].task(); }
}</code></pre>
<p>这其实是前后台的&#8221;加强版&#8221;，介于裸机和 RTOS 之间——能管多个不同周期的任务，但仍然透明可控、没有调度器的黑盒。</p>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 一个小提醒</p>
<p>上面这种把任务&#8221;标记+主循环执行&#8221;的写法，慢任务（比如刷屏）拖久了会顺延后面的任务。真正死磕时序的控制环，最稳的还是直接放进定时器中断里跑（也就是第一档的&#8221;前台&#8221;），别和慢任务挤在主循环里抢时间。</p>
</div>
<h3>第三档：RTOS（FreeRTOS / RT-Thread）</h3>
<p>只有当任务真的多、真的需要并发的时候才上 RTOS。它的代价不小：</p>
<table>
<thead>
<tr>
<th>RTOS</th>
<th>内核占用</th>
<th>特点</th>
</tr>
</thead>
<tbody>
<tr>
<td>FreeRTOS</td>
<td>4~9 KB</td>
<td>抢占/协作/时间片可选，支持 30+ 架构，tickless 低功耗</td>
</tr>
<tr>
<td>RT-Thread</td>
<td>最小 3K ROM / 1K RAM</td>
<td>基于优先级全抢占、256 优先级 O(1) 调度、自带 TCP/IP</td>
</tr>
<tr>
<td>µC/OS-III</td>
<td>6~24K 代码 + 1K 数据</td>
<td>任务数无限、文档全</td>
</tr>
</tbody>
</table>
<p>电赛如果真要上 RTOS，主流就是 FreeRTOS 和 RT-Thread 这两个。</p>
<h3>那到底该不该上 RTOS？</h3>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 出门用不用导航</p>
<p>去楼下小卖部（三五个任务）凭记忆走最快；跨城多点配送（多任务真并发）才值得开导航（RTOS）。但导航本身也耗油（几 KB 开销）。任务就三五个，硬上 RTOS 纯属给自己添堵。</p>
</div>
<p>更具体地说，<strong>宁可用周期性裸机也不上 RTOS</strong> 的理由有三条，都很实在：</p>
<ul>
<li><strong>确定性</strong>：定时器中断直接管任务，没有 RTOS 那套调度算法和优先级反转，固定周期任务给你严格的时序保证，最坏执行时间也好分析。</li>
<li><strong>低开销</strong>：省掉内核那几 KB 的 ROM/RAM，省掉每个任务的控制块（TCB）。</li>
<li><strong>易验证</strong>：任务少（三五个）时，状态机比多线程透明得多，不会有死锁、没有同步 bug。</li>
</ul>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 一个很常见的坑</p>
<p>任务才三五个还非要上 FreeRTOS，徒增几 KB 开销，引入优先级反转和同步 bug，时序还更难验证。<strong>用最小的复杂度完成需求</strong>，这是工程上的硬道理。比赛时一个莫名其妙的死锁，能让你整夜睡不着还找不到原因。</p>
</div>
<p>记住一句话：RTOS 是给&#8221;任务多到管不过来&#8221;准备的解药，不是给整车软件撑场面的装饰品。</p>
<h2>三、控制环时序：5ms 一拍，按死顺序干活</h2>
<p>调度定好了，现在聚焦最核心的那个&#8221;前台&#8221;——控制环。它跑在定时器中断里，是整车的心跳。</p>
<div class="ds-callout ds-callout-example" style="border-left:4px solid #7c4dff;background:#7c4dff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#7c4dff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 5ms 控制周期 = 机器人的心跳</p>
<p>每跳一次，完整做一遍：看路（采集）→ 判断（融合）→ 打方向、踩油门（串级 PID + 输出）→ 检查安全（保护）。心跳必须稳，<strong>每一拍的活必须在一拍之内干完</strong>，绝不能拖到下一拍。</p>
</div>
<p>为什么是 5ms（也就是 200Hz）？这是智能车社区多年试出来的甜点：够快，能保证控制平滑；又不至于太快，给中断里的计算留足时间。背后有个定量依据——<strong>采样定理（Nyquist）</strong>：采样频率要大于被控信号变化频率的 2 倍，才不会&#8221;漏看&#8221;信号的变化。小车的动态没那么快，5ms 一拍绰绰有余。</p>
<div class="ds-callout ds-callout-note" style="border-left:4px solid #448aff;background:#448aff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#448aff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4dd.png" alt="📝" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 哈工大紫丁香三队的实测节拍</p>
<p>电磁/舵机处理 2.2ms（约 454Hz）、舵机输出 5ms、电机控制 5ms、陀螺采样 5ms，部分队伍把编码器读取放到 10ms。你看，核心控制基本都压在 5ms 这个量级。</p>
</div>
<p>STM32 上配一个 5ms 中断很简单：在 72MHz 主频下，预分频 <code>PSC = 72</code>（把计数时钟降到 1MHz，即 1µs 一格）、重装载 <code>ARR = 5000</code>（数 5000 格就是 5ms），就是精确的 5ms。</p>
<h3>一拍里的标准流程</h3>
<p>中断一进来，按这个顺序走，一步都不能乱：</p>
<figure class="ds-diagram" style="text-align:center;margin:1.3em 0"><img decoding="async" src="https://cloudlay.cn/wp-content/uploads/dianseiche/df9e9ae0bbc5.png" alt="电赛小车电控 · 示意图" loading="lazy" style="max-width:100%;height:auto;background:#fff;border-radius:8px;padding:6px;box-shadow:0 1px 6px rgba(0,0,0,.25)"></figure>
<p>落成代码骨架就是这样（前后台架构里的&#8221;前台&#8221;）：</p>
<pre><code class="language-c">// PIT / TIM 5ms 中断 —— 固定周期控制环（前台）
void TIM3_IRQHandler(void) {
    if (TIM_GetITStatus(TIM3, TIM_IT_Update)) {
        TIM_ClearITPendingBit(TIM3, TIM_IT_Update);

        // 1) 采集：读编码器测速，读完立刻清零
        int16_t el = Read_Encoder(L); Clear_Encoder(L);
        int16_t er = Read_Encoder(R); Clear_Encoder(R);
        IMU_Read(&amp;gyro, &amp;acc);

        // 2) 融合：互补滤波出姿态角
        float ang = Complementary_Filter(acc, gyro, 0.005f);

        // 3) 串级 PID：外环速度 → 内环转向
        int16_t ps = Speed_PI(target, (el + er) / 2);   // 外环：速度
        int16_t pt = Turn_PD(line_err, gyro.z);         // 内环：转向

        // 4) 输出：差速 + 限幅后写 PWM
        Set_Motor(LIMIT(ps - pt, -7200, 7200),
                  LIMIT(ps + pt, -7200, 7200));

        // 5) 保护：视觉超时则降级，绝不盲冲
        if (++vision_timeout &gt; VTO) Set_Motor_Safe();
    }
}</code></pre>
<h3>内环必须比外环快</h3>
<p>讲串级 PID 时埋过一个伏笔，这里再钉一遍：</p>
<div class="ds-callout ds-callout-example" style="border-left:4px solid #7c4dff;background:#7c4dff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#7c4dff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 串级 PID 像开车</p>
<p>外环（大脑）决定&#8221;我要开 60 码&#8221;，内环（脚）精确控制油门把速度顶上去。大脑别管太细，<strong>脚的反应必须更快</strong>。摄像头循迹车里，方向环用位置式 PD、速度环用增量式 PI，直道加速、弯道减速。</p>
</div>
<p>所以控制环里的铁律是：<strong>内环周期 ≤ 外环周期，且内环先调稳</strong>。外环的输出是内环的目标，内环都晃晃悠悠，外环再准也没用。具体的调参顺序（先内后外、先 P 后 I 再 D、看波形治百病）在 PID 入门、PID 进阶和 PID 调参实战那几篇已经讲透了，这里只强调时序关系。</p>
<h3>最容易踩的两个时序坑</h3>
<div class="ds-callout ds-callout-danger" style="border-left:4px solid #ff1744;background:#ff174414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff1744"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 坑一：把耗时操作塞进控制中断</p>
<p>在 5ms 控制中断里塞显示刷新、串口打印、图像处理这种耗时操作，会直接挤爆控制周期、破坏固定时序，严重时触发看门狗复位。<strong>控制环里只留：采集 + PID + 输出 + 保护。</strong> 显示、打印、通信、图像处理一律甩到主循环或单独的低频中断。</p>
<p>有个队伍踩过的真实坑：边处理图像边在屏幕上画点，结果屏幕一直闪烁、还拖慢了节拍。正确做法是所有显示函数集中到图像处理完成后一次性执行——显示永远别插进控制热路径。</p>
</div>
<div class="ds-callout ds-callout-danger" style="border-left:4px solid #ff1744;background:#ff174414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff1744"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 坑二：把控制周期和 PWM 频率混为一谈</p>
<p>这俩根本不是一回事： &#8211; <strong>PWM 频率</strong>是 20kHz 级——为了超过人耳听觉、避免电机啸叫（电机驱动那一篇讲过）。 &#8211; <strong>控制环频率</strong>是 200Hz 级（5ms 一拍）——这是你算 PID 的节奏。</p>
<p>一个是&#8221;电机驱动信号有多细腻&#8221;，一个是&#8221;你多久重新决策一次&#8221;，别搞混。</p>
</div>
<h3>顺手补一个：传感器进 PID 前先滤波</h3>
<p>采集到的编码器、陀螺数据有高频噪声，直接喂给 PID 的 D 项会被 Kd 放大成持续抖动。一个又便宜又好用的办法是一阶低通：</p>
<pre><code class="language-c">// 一阶低通：新值占 0.3，旧值占 0.7，平滑又便宜
Encoder = Encoder * 0.7f + new_value * 0.3f;</code></pre>
<p>融合姿态角同理。互补滤波是工程上最常用的：</p>
<pre><code class="language-c">// dt = 0.005s；0.98 越大 =&gt; 越信陀螺（高通）
float Complementary_Filter(float acc_ang, float gyro_rate, float dt) {
    static float angle = 0;
    angle = 0.98f * (angle + gyro_rate * dt) + 0.02f * acc_ang;
    return angle;
}</code></pre>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 陀螺和加速度计是天生互补的一对</p>
<p>陀螺像短跑选手——短期很准，但越跑越偏（零漂会随时间累积）；加速度计像指南针——长期方向对，但一抖一抖的（受振动干扰）。互补滤波把两人的优点加权拼起来：短期信陀螺、长期信加计，得到又稳又准的姿态。精度要求更高时上卡尔曼，原理一样是融合两者，进阶控制那一篇有更细的取舍判断。</p>
</div>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 滤波也有代价</p>
<p>滤波会引入相位滞后，系数下得太狠会拖慢响应、削弱 D 的效果，需要折中。</p>
</div>
<h2>四、状态机：把整车行为拆成&#8221;一次只演一个角色&#8221;</h2>
<p>控制环管的是&#8221;这一拍怎么打方向&#8221;，但整车在赛道上还有更宏观的行为切换：在起跑线待命、起步加速、跑直道、过弯道、到了十字路口要决策、到终点要精确停靠、丢线了要找回、出意外要急停……这些行为怎么管？答案是<strong>有限状态机（FSM）</strong>。</p>
<div class="ds-callout ds-callout-example" style="border-left:4px solid #7c4dff;background:#7c4dff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#7c4dff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 状态机像剧本杀</p>
<p>车在任何时刻只演<strong>一个</strong>角色（待命 / 直道 / 弯道 / 十字 / 丢线 / 急停），剧情（条件）触发了才换角色。每段戏只管自己的台词，出了 bug 一眼就能定位是哪个角色演砸了，不会一锅乱炖。</p>
</div>
<p>这就是状态机最大的价值：<strong>降耦合、易调试</strong>。把复杂行为拆成独立状态，每个状态只管单一功能。智能车国赛代码多年都用状态机管理直道、弯道、十字、丢线、急停；自动驾驶的行为规划也用分层有限状态机（HFSM），状态名都很像，比如 <code>FORWARD_DRIVE</code>、<code>STOP_SIGN_WAIT</code>、<code>CROSS_INTERSECTION</code>。</p>
<h3>整车主状态机骨架</h3>
<figure class="ds-diagram" style="text-align:center;margin:1.3em 0"><img decoding="async" src="https://cloudlay.cn/wp-content/uploads/dianseiche/618fc94ce74a.png" alt="电赛小车电控 · 示意图" loading="lazy" style="max-width:100%;height:auto;background:#fff;border-radius:8px;padding:6px;box-shadow:0 1px 6px rgba(0,0,0,.25)"></figure>
<p>注意急停（ESTOP）能从<strong>任何</strong>状态进入——这是最高优先级，下面代码里会体现（图里只画了几条代表线，实际是所有状态都能转）：</p>
<pre><code class="language-c">typedef enum {
    ST_IDLE, ST_START, ST_STRAIGHT, ST_CURVE,
    ST_CROSS, ST_PARK, ST_LOST, ST_ESTOP
} CarState;

CarState st = ST_IDLE;

void State_Machine(void) {
    if (estop_flag) { st = ST_ESTOP; }   // 急停最高优先级，先判
    switch (st) {
        case ST_IDLE:     if (start_btn) st = ST_START;       break;
        case ST_START:    if (moving)    st = ST_STRAIGHT;    break;
        case ST_STRAIGHT: if (is_cross())      st = ST_CROSS;
                          else if (is_curve()) st = ST_CURVE;
                          else if (line_lost)  st = ST_LOST;  break;
        case ST_CURVE:    if (!is_curve())     st = ST_STRAIGHT;
                          else if (line_lost)  st = ST_LOST;  break;
        case ST_CROSS:    Cross_FillLine();
                          if (passed) st = ST_STRAIGHT;       break;
        case ST_LOST:     Recover_ByLastError();
                          if (line_found) st = ST_STRAIGHT;   break;
        case ST_PARK:     Precise_Stop();                     break;
        case ST_ESTOP:    Set_Motor(0, 0);                    break;
    }
}</code></pre>
<div class="ds-callout ds-callout-note" style="border-left:4px solid #448aff;background:#448aff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#448aff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4dd.png" alt="📝" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 状态机放在哪跑？</p>
<p>状态机本身不是高频热路径，它做的是&#8221;宏观决策&#8221;，可以放在主循环里、或者用比控制环慢一档的节奏跑（比如配合视觉数据的更新频率）。真正每 5ms 死磕的是控制环；状态机只负责告诉控制环&#8221;现在该用什么目标速度、什么循迹策略&#8221;。</p>
</div>
<h3>状态机最容易写错的地方</h3>
<div class="ds-callout ds-callout-danger" style="border-left:4px solid #ff1744;background:#ff174414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff1744"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 迁移条件写得耦合或有遗漏</p>
<p>两个高发 bug： 1. <strong>急停不是最高优先级</strong>——藏在某个 case 里，结果某些状态下急停根本触发不了。一定要像上面那样，在 switch 之前先判 <code>estop_flag</code>。 2. <strong>丢线找回没有超时退出</strong>——车进了 <code>ST_LOST</code> 状态，找半天找不回来又没有退出条件，就卡死在那儿原地打转。每个&#8221;等待型&#8221;状态（丢线找回、十字通过、精确停靠）都该配一个超时兜底，超时了就降级或急停。</p>
</div>
<h2>五、保护逻辑：能拿分的车，先得是&#8221;摔不死&#8221;的车</h2>
<p>开篇那一篇讲过电赛的拿分逻辑——有一堆&#8221;失败即 0 分&#8221;的红线（冲出赛道、超时等等）。所以整车软件里，保护逻辑不是锦上添花，而是保命。前面控制环代码里那行 <code>if (++vision_timeout &gt; VTO) Set_Motor_Safe();</code> 就是保护段的入口。这里展开两个最关键的。</p>
<h3>视觉超时降级：信号断了别闭眼猛踩</h3>
<p>这是新手最容易忽略、又最容易翻车的地方。注意一个事实：<strong>摄像头帧率（60~100Hz）远低于控制环（200Hz）</strong>。也就是说，控制环跑两三拍，视觉才更新一帧。如果某一拍没有新帧，或者连续丢线超过阈值了，你绝不能拿过期的、无效的图像去盲控。</p>
<div class="ds-callout ds-callout-example" style="border-left:4px solid #7c4dff;background:#7c4dff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#7c4dff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 进隧道信号没了</p>
<p>开车进隧道，导航（摄像头）突然黑屏。聪明的做法是：松油门、按记忆方向慢慢直行，等信号恢复；而不是闭着眼按原速猛踩。</p>
</div>
<p>具体做法：设一个看门狗 / 超时计数器，每来一帧新数据就清零，控制环每拍 +1。超过阈值就降级——保持上一个有效打角、减速或限速直行；严重时直接急停。阈值各队按自己车的特性定。视觉那一篇也讲过同款思路：通信超时（比如超过 100ms 没收到包）就降级到灰度或 IMU 兜底，两处其实是一回事。</p>
<div class="ds-callout ds-callout-danger" style="border-left:4px solid #ff1744;background:#ff174414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff1744"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 真实教训</p>
<p>摄像头帧率低于控制环却不做帧同步、不做超时处理，用过期或无效图像盲控，丢线那一刻直接冲出去——这是社区里反复出现的&#8221;冲出赛道&#8221;原因之一。</p>
</div>
<h3>丢线找回：记住&#8221;最后看见路的方向&#8221;</h3>
<p>丢线之后怎么回到赛道？核心思路是<strong>记忆最后一次有效偏差</strong>：</p>
<ul>
<li>设一个 <code>last_error</code>，没丢线时持续更新它；</li>
<li>一旦判定丢线，就<strong>冻结</strong>这个值；</li>
<li>用冻结的偏差判断车是往哪边出去的，据此朝相反方向打角，把车拽回赛道。</li>
</ul>
<p>判丢线常用&#8221;全行扫描&#8221;：从图像底部往上扫，一直扫到最上面一行还找不到赛道边界，就判为丢线。</p>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 丢线时千万别让偏差归零</p>
<p>如果丢线瞬间偏差归零或乱跳，车就彻底失去方向感，原地乱转或直接飞出去。一定要冻结 <code>last_error</code>，靠它把车找回来。条件允许的话，视觉 + 电磁双传感器互补，丢线时切到电磁兜底也是好招。</p>
</div>
<h2>六、一条贯穿始终的硬道理：机械定上限，软件定下限</h2>
<p>最后，讲一个比任何代码都重要的认知，来自哈工大紫丁香队的血泪经验：</p>
<div class="ds-callout ds-callout-important" style="border-left:4px solid #00bcd4;background:#00bcd414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bcd4"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2757.png" alt="❗" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 机械影响上限，软件影响下限</p>
<p>机械没做好，车速会被死死限制——高速甩尾侧滑对机械要求极高，主销后倾/内倾、前束、底盘高度（降重心）直接影响回正稳定性。<strong>软件再强，也救不了垃圾机械。</strong></p>
</div>
<p>他们有两个特别值得记的故事：</p>
<div class="ds-callout ds-callout-example" style="border-left:4px solid #7c4dff;background:#7c4dff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#7c4dff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 故事一：别迷信单一高级算法</p>
<p>他们的电磁越野组一开始用纯神经网络控制，低速效果好，但高速抖动过大、甚至直接冲出赛道。事后复盘出两个根因：一是偏差范围太大，量化成整型时损失了精度；二是用来做时序预测的网络（TCN）预测值偏小，还出现了&#8221;提前预测&#8221;的现象，导致打角时机不对。最终方案是混合控制——用神经网络提取与时间无关的信息，叠加传统 PID 处理时间相关的部分，按预测角度动态切换两者权重，才稳下来。</p>
<p><strong>教训</strong>：高速控制对反馈精度和动态响应极其敏感；经典 PID 常常是高速稳定的&#8221;压舱石&#8221;。进阶算法该不该上、怎么上，进阶控制那一篇有更细的判断。</p>
</div>
<div class="ds-callout ds-callout-example" style="border-left:4px solid #7c4dff;background:#7c4dff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#7c4dff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 故事二：调参撞天花板，先回看机械</p>
<p>同一支队伍发现车速一高就明显甩尾侧滑，软件怎么调都压不住，最后被迫回去改机械——调主销后倾内倾、前束、降低底盘重心，问题才解决。</p>
<p><strong>教训</strong>：调参遇到天花板，别在软件里死磕，先回头看看是不是机械到极限了。</p>
</div>
<p>还有一条赛场铁律，宁可多说一遍：</p>
<div class="ds-callout ds-callout-danger" style="border-left:4px solid #ff1744;background:#ff174414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff1744"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 传感器上场前必须标定、自检</p>
<p>线上比赛真有队伍电磁没标定就莫名其妙冲出赛道——而国赛赛道边缘是黑海绵条，是否冲出由人工判罚，赛场上还无从复现。<strong>起跑前一定留一个标定 / 自检环节</strong>，别带着一台没标定的车上场。</p>
</div>
<h2>想找完整工程当范本？</h2>
<p>光看片段不过瘾，下面几个开源仓库可以拿来研究整车是怎么组织的：</p>
<ul>
<li><a href="https://github.com/Sakuramdd/SeekFree_RT1064_Opensource_Library" target="_blank" rel="noopener noreferrer">Sakuramdd/SeekFree_RT1064_Opensource_Library</a>：逐飞 RT1064 视觉组开源库，标准三层 + <code>code</code>(image/control/init)，研究分层与解耦的最佳范本。</li>
<li><a href="https://github.com/woai66/SeekFree_MSPM0G3507_Opensource_Library" target="_blank" rel="noopener noreferrer">woai66/SeekFree_MSPM0G3507_Opensource_Library</a>：逐飞库移植到 TI MSPM0G3507，直接对应 TI 赛道，是芯片解耦平移的活样本。</li>
<li><a href="https://github.com/Jerrysupreme/XJTU-VISON_SMARTCAR_2025" target="_blank" rel="noopener noreferrer">Jerrysupreme/XJTU-VISON_SMARTCAR_2025</a>：西安交大 2025 智能视觉组完整参赛级工程，底盘 RT1064 + 摄像头，学整车怎么组织。</li>
<li><a href="https://github.com/Blight001/SmartCarCameraTrackingSimulation" target="_blank" rel="noopener noreferrer">Blight001/SmartCarCameraTrackingSimulation</a>：Unity 做的赛道仿真，10 款赛道含国赛道，还能逐帧回溯复盘&#8221;为什么冲出赛道&#8221;——没硬件时调循迹、调参、复盘的神器。</li>
<li><a href="https://github.com/hxk55668/PID_CAR-STM32-FREERTOS-" target="_blank" rel="noopener noreferrer">hxk55668/PID_CAR-STM32-FREERTOS-</a>：想用 FreeRTOS 组织控制/通信/显示任务的，可以参考这个。</li>
</ul>
<p>更全的开源清单和现场调试速查表，留到下一篇集中给。</p>
<h2>小结：把这一篇钉进脑子</h2>
<p>到这里，整车软件的骨架就立起来了。回顾一下这几条心法：</p>
<ul>
<li>☐ <strong>分层</strong>：App / 中间件 / Driver / HAL 四层，上层只调下层、下层不反调，设备层禁碰芯片 API——换芯片只改驱动层。</li>
<li>☐ <strong>调度</strong>：电赛首选前后台裸机（主循环跑慢任务 + 定时器中断跑控制环），任务真多了再考虑 RTOS。</li>
<li>☐ <strong>控制环</strong>：5ms 一拍（200Hz），按&#8221;采集→融合→串级 PID→输出→保护&#8221;死顺序走，内环快于外环，ISR 只做核心、耗时操作甩主循环。</li>
<li>☐ <strong>状态机</strong>：把整车行为拆成独立状态，急停最高优先级、等待型状态都要超时退出。</li>
<li>☐ <strong>保护</strong>：视觉超时降级、丢线记忆找回、出界急停——保命优先。</li>
<li>☐ <strong>认知</strong>：机械定上限、软件定下限；传感器上场前必标定。</li>
</ul>
<p>软件框架搭好了，但真正决定你能不能拿奖的，是四天三夜里在赛场上把它跑稳、调好、不翻车。下一篇现场作战手册，我们就聊聊四天三夜怎么安排、调试怎么避坑、以及哪些开源仓库值得你现在就 star。</p>
<div class="ds-series" style="border:1px solid #4488ff33;background:#4488ff0d;border-radius:8px;padding:.8em 1.1em;margin:1.2em 0">
<p style="margin:0 0 .5em"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4da.png" alt="📚" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 本文是 <strong>「从 0 到 1 带你打电赛 · 小车电控篇」</strong> 系列（共 12 篇）第 11 篇。</p>
<ol style="margin:.2em 0 0;padding-left:1.4em">
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-01-how-to-score/">第1篇 · 拿奖逻辑：把比赛拆成小目标</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-02-history/">第2篇 · 赛题进化史与押题</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-03-build-and-architecture/">第3篇 · 整车搭建与代码框架</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-04-motor-power/">第4篇 · 电机驱动与电源地基</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-05-sensing/">第5篇 · 感知：灰度/电磁/编码器/IMU</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-06-pid-basics/">第6篇 · PID 入门：搞懂 P/I/D</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-07-pid-advanced/">第7篇 · PID 进阶：串级+工程补丁</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-08-pid-tuning/">第8篇 · PID 调参实战(核心)</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-09-advanced-control/">第9篇 · 进阶控制：几时该上</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-10-vision-comm/">第10篇 · K230 视觉与通信协议</a></li>
<li style="margin:.15em 0"><strong>第11篇 · 状态机与整车软件（本篇）</strong></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-12-field-manual/">第12篇 · 现场作战+避坑+开源</a></li>
</ol>
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://cloudlay.cn/nuedc-car-11-architecture-fsm/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>从0到1带你打电赛·小车电控篇(十)：视觉与通信——K230 怎么用、怎么和主控对上话</title>
		<link>https://cloudlay.cn/nuedc-car-10-vision-comm/</link>
					<comments>https://cloudlay.cn/nuedc-car-10-vision-comm/#respond</comments>
		
		<dc:creator><![CDATA[云间辞]]></dc:creator>
		<pubDate>Sun, 14 Jun 2026 17:08:33 +0000</pubDate>
				<category><![CDATA[嵌入式]]></category>
		<category><![CDATA[CanMV]]></category>
		<category><![CDATA[K230]]></category>
		<category><![CDATA[OpenMV]]></category>
		<category><![CDATA[UART]]></category>
		<category><![CDATA[智能小车]]></category>
		<category><![CDATA[电赛]]></category>
		<category><![CDATA[通信协议]]></category>
		<guid isPermaLink="false">https://cloudlay.cn/nuedc-car-10-vision-comm/</guid>

					<description><![CDATA[📚 本文是 「从 0 到 1 带你打电赛 · 小车电控篇」 系列（共 12 篇）第 10 篇。 第1篇 · 拿 [&#8230;]]]></description>
										<content:encoded><![CDATA[<div class="ds-series" style="border:1px solid #4488ff33;background:#4488ff0d;border-radius:8px;padding:.8em 1.1em;margin:1.2em 0">
<p style="margin:0 0 .5em"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4da.png" alt="📚" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 本文是 <strong>「从 0 到 1 带你打电赛 · 小车电控篇」</strong> 系列（共 12 篇）第 10 篇。</p>
<ol style="margin:.2em 0 0;padding-left:1.4em">
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-01-how-to-score/">第1篇 · 拿奖逻辑：把比赛拆成小目标</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-02-history/">第2篇 · 赛题进化史与押题</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-03-build-and-architecture/">第3篇 · 整车搭建与代码框架</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-04-motor-power/">第4篇 · 电机驱动与电源地基</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-05-sensing/">第5篇 · 感知：灰度/电磁/编码器/IMU</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-06-pid-basics/">第6篇 · PID 入门：搞懂 P/I/D</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-07-pid-advanced/">第7篇 · PID 进阶：串级+工程补丁</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-08-pid-tuning/">第8篇 · PID 调参实战(核心)</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-09-advanced-control/">第9篇 · 进阶控制：几时该上</a></li>
<li style="margin:.15em 0"><strong>第10篇 · K230 视觉与通信协议（本篇）</strong></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-11-architecture-fsm/">第11篇 · 状态机与整车软件</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-12-field-manual/">第12篇 · 现场作战+避坑+开源</a></li>
</ol>
</div>
<p>前面九篇，我们把&#8221;小车自己能跑稳、跑准&#8221;这件事讲透了：电机、电源、各种传感器，PID 从入门到串级再到调参实战。但你有没有发现，到现在为止小车的&#8221;眼睛&#8221;其实很弱——灰度传感器只能看脚下那条线，编码器只知道轮子转了几圈，IMU 只管自己歪没歪。要让小车看懂&#8221;前面有个二维码&#8221;&#8221;那边是个 2 号路牌&#8221;&#8221;这条线往左拐了 30 度&#8221;，就得请出真正的视觉模块。</p>
<p>这一篇聊两件事：一是 K230 这类视觉模块到底能干什么、怎么用；二是它怎么跟主控&#8221;对上话&#8221;——也就是通信协议。这两件事其实是一件事：视觉负责&#8221;看&#8221;，主控负责&#8221;动&#8221;，中间靠一根串口线把&#8221;看到的结论&#8221;传过去。把这条链路打通，你的小车才真正有了眼睛。</p>
<p>先把整条链路画出来，心里有个全局：</p>
<figure class="ds-diagram" style="text-align:center;margin:1.3em 0"><img decoding="async" src="https://cloudlay.cn/wp-content/uploads/dianseiche/1506e2ef5953.png" alt="电赛小车电控 · 示意图" loading="lazy" style="max-width:100%;height:auto;background:#fff;border-radius:8px;padding:6px;box-shadow:0 1px 6px rgba(0,0,0,.25)"></figure>
<h2>一、先想清楚：视觉模块在整车里是什么角色</h2>
<p>新手最容易犯的一个错，是把视觉模块当&#8221;主脑&#8221;，想让它又看图、又算决策、又控电机。结果发现 K230 上用 Python 跑控制环又慢又难调，主控那边还拿不到干净数据，两头别扭。</p>
<p>正确的分工是这样的：</p>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 一句话记住分工</p>
<p>视觉模块负责&#8221;看&#8221;，主控负责&#8221;动&#8221;，中间只传&#8221;结论&#8221;。</p>
</div>
<p>打个比方：视觉模块就像副驾上的导航员。副驾不会把整块挡风玻璃的画面塞给开车的你，他只会喊一句&#8221;左偏 10 度&#8221;&#8221;前方是 2 号路牌&#8221;。你（主控）拿到这一句话就够打方向了。</p>
<p>所以<strong>视觉端只回传几个数</strong>——比如 <code>line_error</code>（线偏了多少）、<code>line_angle</code>（线斜了多少度）、<code>flag</code>（看到没看到）、<code>id</code>（二维码/路牌的编号）——绝不把整张图像通过串口发过来。</p>
<div class="ds-callout ds-callout-danger" style="border-left:4px solid #ff1744;background:#ff174414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff1744"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 头号大坑：千万别传图</p>
<p>K230 千万别把整幅图像或大数组通过 UART 发给单片机。几百 KB 的图片走 115200 的串口要好几秒，带宽和主控算力都扛不住，延迟大到小车早就冲出去了。主控其实只需要 <code>line_error</code>/<code>line_angle</code> 这几个数就能闭环。真要传整幅图/点云那是极罕见的赛题，才考虑 SPI 或网口。</p>
</div>
<p>这种&#8221;把看的活和算的活分开&#8221;的设计，好处是两个人可以各写各的：视觉同学专心调他的巡线和识别，电控同学专心调 PID，只要事先把&#8221;传哪几个数、什么格式&#8221;约定死，最后联调时插上线就能跑。</p>
<h2>二、K230/CanMV 能干什么</h2>
<p>K230 是嘉楠（Kendryte）出的一颗带 6 TOPS NPU（神经网络算力）的视觉芯片，常见开发板有庐山派、01Studio CanMV 等。它跑的固件叫 <strong>CanMV</strong>，写法是 MicroPython，API 跟大家熟悉的 OpenMV 几乎同源——所以网上 OpenMV 的巡线、色块例程，K230 上稍改就能用。</p>
<div class="ds-callout ds-callout-note" style="border-left:4px solid #448aff;background:#448aff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#448aff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4dd.png" alt="📝" class="wp-smiley" style="height: 1em; max-height: 1em;" /> CanMV 是什么</p>
<p>CanMV = 在 K230 上跑的 MicroPython 视觉固件。你用 Python 写&#8221;找黑线&#8221;&#8221;找色块&#8221;，它底层调硬件帮你算。会写几行 Python 就能上手，不用碰 C。</p>
</div>
<p>对电赛小车来说，常用的有这么几样活：</p>
<table>
<thead>
<tr>
<th>任务</th>
<th>用的 API</th>
<th>回传给主控的&#8221;结论&#8221;</th>
</tr>
</thead>
<tbody>
<tr>
<td>巡线</td>
<td><code>find_blobs</code> 加权质心 / <code>get_regression</code> 线性回归</td>
<td><code>line_error</code>、<code>line_angle</code></td>
</tr>
<tr>
<td>找色块</td>
<td><code>find_blobs([色彩阈值])</code></td>
<td>色块中心坐标、颜色编号</td>
</tr>
<tr>
<td>二维码</td>
<td><code>find_qrcodes()</code></td>
<td><code>.payload()</code> 内容字符串</td>
</tr>
<tr>
<td>机器码 AprilTag</td>
<td><code>find_apriltags()</code></td>
<td><code>.id()</code>、中心坐标、旋转角</td>
</tr>
<tr>
<td>数字/文字 OCR</td>
<td>OCR kmodel</td>
<td>识别出的数字/字符串</td>
</tr>
</tbody>
</table>
<h3>巡线的两条路线</h3>
<p>巡线是小车赛最高频的视觉任务，K230 上有两条主流做法，都只把&#8221;一个偏差 + 一个角度&#8221;发给主控。</p>
<p><strong>路线一：灰度加权质心法。</strong> 把画面横向切成几条 ROI（感兴趣区域），近处权重大、远处权重小，每条 ROI 里用 <code>find_blobs</code> 找黑线最大的色块，加权平均出线的中心位置，再算出偏角。官方例程里近/中/远的权重是 <strong>0.7 / 0.3 / 0.1</strong>——理解一下：脚下的线最该听（权重大），远处的线只是&#8221;预判方向&#8221;（权重小），这其实就是我们在感知篇讲过的&#8221;前瞻&#8221;。</p>
<p><strong>路线二：线性回归 <code>get_regression</code>。</strong> 直接对整幅二值图做一次直线拟合，返回一个 line 对象，里面有 <code>theta()</code>（角度）、<code>rho()</code>（到原点的距离，反映横向偏移），还有一个很有用的 <code>magnitude()</code>。</p>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> magnitude 是&quot;线靠不靠谱&quot;的置信度</p>
<p><code>get_regression</code> 的 magnitude 范围是 0 到正无穷，越大说明拟合出来的越像一条直线、越可信；接近 0 说明这堆点更像一团乱（圆），多半是噪点。就像你看一行字，写得越直越能确信&#8221;这是一条线&#8221;而不是一团花。可以拿它当门槛：magnitude 太小就认为&#8221;这帧没看清&#8221;，直接告诉主控&#8221;我没线&#8221;。</p>
</div>
<h3>二维码、AprilTag、OCR——直接出&#8221;结论&#8221;</h3>
<p>这三样的共同点是：K230 直接帮你把结果算成字符串或编号，你只要把那几个字节塞进串口发出去。</p>
<ul>
<li><strong>二维码</strong> <code>find_qrcodes()</code>：返回对象的 <code>.payload()</code> 就是二维码里的内容字符串。</li>
<li><strong>AprilTag</strong>（一种黑白方块机器码，专为机器识别设计）<code>find_apriltags()</code>：返回 <code>.id()</code>、中心坐标 <code>.cx()/.cy()</code>、旋转角。默认用 <strong>TAG36H11</strong> 这一族（6×6 方块，误识率低）；还有 TAG16H5（4×4，看得更远但更容易认错）。</li>
<li><strong>OCR / 数字识别</strong>：用官方的 OCR kmodel，能识别中英文和数字；纯手写数字也可以走 MNIST 模型分类。</li>
</ul>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 一个容易白调的坑</p>
<p>二维码识别率低时，常常要加 <code>set_hmirror</code>/<code>set_vflip</code> 做镜像校正（因为摄像头朝向可能把图照反了）；但 <strong>AprilTag 不需要镜像校正</strong>。两者搞混会让你白白折腾半天，还以为是阈值问题。</p>
</div>
<h3>分辨率怎么选</h3>
<p>一句口诀：<strong>巡线用小图保帧率，识别用大图保精度。</strong></p>
<p>巡线是几何任务、要的是控制环快（帧率就是控制频率的上限），所以常用 160×120（QQVGA）甚至更小的灰度图；二维码官方例程用 640×480 灰度；OCR/AI 识别最吃算力，但 K230 有 NPU 撑着，可以慢一点换清晰。K230 还支持多通道，甚至能一路给屏幕显示、一路给算法跑。</p>
<h2>三、视觉端代码：把巡线结果发出去</h2>
<p>下面给一段可以直接改的灰度加权质心巡线代码（K230 / MicroPython），算出偏差和角度后调 <code>send_line</code> 发给主控。发送函数 <code>send_line</code>/<code>send_frame</code> 的帧格式我们在第五节细讲，这里先把&#8221;算结论&#8221;的逻辑看清楚。</p>
<pre><code class="language-python">import math

GRAYSCALE_THRESHOLD = [(0, 64)]              # 黑线的灰度阈值(现场要重标!)
ROIS = [ (0, 100, 160, 20, 0.7),            # 近(权重大)
         (0,  50, 160, 20, 0.3),            # 中
         (0,   0, 160, 20, 0.1) ]           # 远
weight_sum = sum(r[4] for r in ROIS)
IMG_W, IMG_H = 160, 120

while True:
    img = sensor.snapshot()
    centroid_sum, found = 0, 0
    for r in ROIS:
        blobs = img.find_blobs(GRAYSCALE_THRESHOLD, roi=r[0:4], merge=True)
        if blobs:
            b = max(blobs, key=lambda x: x.pixels())   # 取最大的黑块
            centroid_sum += b.cx() * r[4]; found += 1
    if found:
        center_pos = centroid_sum / weight_sum
        line_error = center_pos - IMG_W/2              # 居中=0,正负代表左右偏
        angle = math.degrees(-math.atan((center_pos - IMG_W/2) / (IMG_H/2)))
        send_line(line_error, angle)                   # 发给主控
    else:
        send_frame(0x00, b'')                          # 一条线都没找到 -&gt; 告诉主控</code></pre>
<p>如果走线性回归路线，核心就这几行：</p>
<pre><code class="language-python">img = sensor.snapshot().binary([(0, 64)])     # 先二值化
line = img.get_regression([(255, 255)], robust=True)
if line and line.magnitude() &gt; 8:             # 阈值自调,过滤不可信的拟合
    rho_err = abs(line.rho()) - img.width()/2          # 横向偏移
    theta = line.theta()
    theta_err = theta - 180 if theta &gt; 90 else theta   # 角度,&gt;90度要修正
    send_frame(0x01, struct.pack('&lt;hh', int(rho_err), int(theta_err * 10)))
else:
    send_frame(0x00, b'')                              # 没拟合到可信的线</code></pre>
<p>注意那行 <code>theta - 180 if theta &gt; 90</code>：<code>get_regression</code> 返回的角度有时会跳到 90 度以上，需要减 180 折回来，否则角度会突变、车会猛打一下方向。这是 <a href="https://book.openmv.cc/project/follow-lines.html" target="_blank" rel="noopener noreferrer">OpenMV 官方巡线工程</a> 的标准处理。</p>
<div class="ds-callout ds-callout-important" style="border-left:4px solid #00bcd4;background:#00bcd414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bcd4"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2757.png" alt="❗" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 阈值和 ROI 必须现场重标，不能照抄</p>
<p>上面的灰度阈值 <code>[(0,64)]</code> 和 ROI 是按别人的赛道、别人的光照定的。换个场地、换个灯，黑白的灰度值就变了。到了赛场第一件事就是对着真实赛道重新标阈值，否则&#8221;明明代码是对的，就是巡不到线&#8221;——多半就是阈值没标。</p>
</div>
<p>如果你用线性回归 + 主控双 PID 的方案，<a href="https://book.openmv.cc/project/follow-lines.html" target="_blank" rel="noopener noreferrer">OpenMV 官方的经验</a> 值得记住：横向偏移 <code>rho</code> 配一个 PID（经验 p ≈ 0.4），角度 <code>theta</code> 配另一个 PID（经验 p ≈ 0.001）。注意这俩 p 值差了两个数量级，<strong>必须分开调</strong>——不少人想用一个 PID 硬扛，结果怎么都不稳。具体怎么把这两路偏差喂进方向环、怎么整定，方向环和串级的细节见《PID 进阶：方向环、串级双环与工程补丁》一篇。</p>
<h2>四、通信三选一：为什么是 UART</h2>
<p>视觉端算好了结论，怎么送到主控？常见有三条总线：UART、SPI、I2C。对&#8221;只回传几个数&#8221;这个场景，结论很干脆——<strong>UART 几乎是唯一正确答案</strong>。</p>
<p>打个比方理解三者的取舍：</p>
<ul>
<li><strong>UART</strong> 像乡间双车道：两根线（TX/RX）加地线，点对点，够用又省事。</li>
<li><strong>SPI</strong> 像高速公路：多车道全双工、速率最高（&gt;10 Mbps），适合传图/大带宽，但要修匝道（每个从机一根 CS）、走线多、长线容易出错。</li>
<li><strong>I2C</strong> 像公交线：一条总线上挂一串站点，省 GPIO，但速度慢、要排班寻址（地址管理 + 上拉电阻），抗噪和距离最差。</li>
</ul>
<p>给小车回传几个数，走&#8221;乡间双车道&#8221;最划算。详细对比：</p>
<table>
<thead>
<tr>
<th></th>
<th>UART（主推）</th>
<th>SPI</th>
<th>I2C</th>
</tr>
</thead>
<tbody>
<tr>
<td>线数</td>
<td>2（TX/RX）+ 地</td>
<td>4（CLK/MOSI/MISO/CS）</td>
<td>2（SCL/SDA）+ 地</td>
</tr>
<tr>
<td>方式</td>
<td>异步全双工，点对点</td>
<td>同步全双工，主从</td>
<td>同步半双工，总线多从</td>
</tr>
<tr>
<td>速率</td>
<td>中（115200 够用）</td>
<td>最高（&gt;10 Mbps）</td>
<td>低（100k~400k）</td>
</tr>
<tr>
<td>适合</td>
<td>回传几个结论</td>
<td>传图/高带宽</td>
<td>一条总线挂多传感器</td>
</tr>
<tr>
<td>配套</td>
<td>DMA+IDLE 零成本收整帧，库最成熟</td>
<td>走线多、CS 管理麻烦</td>
<td>上拉+地址管理麻烦、抗噪差</td>
</tr>
</tbody>
</table>
<div class="ds-callout ds-callout-success" style="border-left:4px solid #00c853;background:#00c85314;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00c853"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 选型口诀</p>
<p>K230 只回传几个数 → <strong>UART</strong>；要回传整幅图/点云 → 才考虑 SPI；同一总线上挂多个传感器 → 才用 I2C。99% 的小车赛用 UART。</p>
</div>
<h3>接线：3.3V 直连，别加多余的转换板</h3>
<p>K230 所有 IO 都是 3.3V，STM32F4 的 IO 也是 3.3V，所以 <strong>TX 接对方 RX 交叉接、共地，就能直连</strong>，不需要电平转换芯片。波特率约定 115200、8 位数据、无校验、1 位停止（也就是常说的 8N1）即可。</p>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 接线两个高频翻车点</p>
<ul>
<li><strong>忘了共地</strong>：K230 和主控不共地 → 电平没有共同基准 → 全是乱码。两块板之间一定要拉一根 GND。</li>
<li><strong>5V 主控直连</strong>：如果你的主控是 5V 系统（某些老板子、Arduino），它的 TX 是 5V，直接接到 K230 的 RX 可能把 K230 打坏。这种情况 K230 的 RX 要分压或加电平转换。STM32 是 3.3V 就没这问题。</li>
</ul>
</div>
<p>K230 用串口前还有一步<strong>软件上的坑</strong>：必须先用 FPIOA 做引脚复用，纯软件 <code>UART()</code> 初始化是不够的。另外 K230 一共 5 个串口，<strong>UART0（小核 SH）和 UART3（大核 SH）常被系统占用</strong>，别选；用户可用 UART1/2/4，<strong>主推 UART2</strong>（GPIO11 = TX，GPIO12 = RX，对应庐山派的 GH1.25 接口①）。</p>
<pre><code class="language-python">from machine import UART, FPIOA

# 关键:必须先做引脚复用,否则口不通!
fpioa = FPIOA()
fpioa.set_function(11, FPIOA.UART2_TXD)
fpioa.set_function(12, FPIOA.UART2_RXD)
uart = UART(UART.UART2, baudrate=115200, bits=UART.EIGHTBITS,
            parity=UART.PARITY_NONE, stop=UART.STOPBITS_ONE)</code></pre>
<p>引脚、API、电平这些硬事实，以 <a href="https://wiki.lckfb.com/zh-hans/lushan-pi-k230/" target="_blank" rel="noopener noreferrer">立创庐山派 K230 wiki</a> 为准最权威。</p>
<h2>五、帧协议：给数据装个&#8221;快递包裹&#8221;</h2>
<p>确定了用 UART，下一个问题是：数据在线上是一串字节流，主控怎么知道&#8221;一帧从哪开始、到哪结束、有没有传错&#8221;？这就需要设计<strong>帧协议</strong>。</p>
<p>打个比方，一帧数据就像一个寄出去的快递包裹：</p>
<div class="ds-callout ds-callout-example" style="border-left:4px solid #7c4dff;background:#7c4dff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#7c4dff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 帧协议 = 寄快递</p>
<ul>
<li><strong>帧头</strong>（封口贴）：告诉收件人&#8221;包裹从这开始了&#8221;</li>
<li><strong>类型</strong>：里面装的是巡线结果，还是二维码 id</li>
<li><strong>长度</strong>（运单上写几件）：数据区有多少字节</li>
<li><strong>数据</strong>：真正的货物（那几个数）</li>
<li><strong>校验</strong>（防伪码）：确认路上没被掉包/传错</li>
<li><strong>帧尾</strong>（封箱胶带）：到此结束</li>
</ul>
<p>少了哪样，收件人都可能拆错包。</p>
</div>
<p>一个经过实战检验、可以直接用的格式：</p>
<pre><code class="language-plaintext">帧头 0xAA 0x55 | 类型 1B | 长度 1B | 数据 N字节 | 校验 1B | 帧尾 0x0D 0x0A</code></pre>
<p>几个设计点说一下：</p>
<ul>
<li><strong>帧头用双字节 0xAA 0x55</strong>，而不是单字节。因为在二进制数据流里，单个 0xAA 太容易被数据区里恰好相同的字节&#8221;撞上&#8221;导致错帧；双字节 + 长度 + 校验三重确认，误同步概率就压到可以忽略。顺便一提，0x55 = 01010101 在线上是个 50% 占空比的方波，最容易用示波器对齐，所以常被选作帧头。</li>
<li><strong>长度字段</strong>让你能收变长数据（1 字节最大 255，扣掉校验后净载荷还有 200 多字节，对小车绰绰有余）。</li>
<li><strong>校验</strong>可以用最轻量的累加和，也可以用更强的 CRC-8 / CRC-16。新手先用累加和就够。</li>
<li><strong>帧尾 0x0D 0x0A</strong>（也就是 <code>\r\n</code>）的好处是：用串口助手或日志直接打印时能自动换行，调试时一眼能看清每一帧。</li>
</ul>
<div class="ds-callout ds-callout-danger" style="border-left:4px solid #ff1744;background:#ff174414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff1744"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 帧头/帧尾可能撞上数据</p>
<p>如果数据区里恰好出现了 0x0D 0x0A，而你又只靠帧尾来断帧，就会被误判成&#8221;帧结束了&#8221;。两种解法二选一：① 像本方案这样<strong>靠&#8221;帧头 + 长度 + 校验&#8221;整体判定</strong>，根本不依赖帧尾找边界；② 做<strong>转义</strong>（约定一个转义字符如 0x7D，遇到冲突字节就改写）。新手用方案①最省事。</p>
</div>
<h3>视觉端发送代码</h3>
<pre><code class="language-python">import struct

FRAME_HEAD = b'\xAA\x55'
FRAME_TAIL = b'\x0D\x0A'

# type: 0x01=巡线  0x02=二维码  0x03=AprilTag  0x04=数字  0x00=无目标
def send_frame(msg_type, payload: bytes):
    length = len(payload)
    body = bytes([msg_type, length]) + payload    # 类型 + 长度 + 数据
    checksum = 0
    for b in body:
        checksum = (checksum + b) &amp; 0xFF           # 累加和(可换成 CRC)
    uart.write(FRAME_HEAD + body + bytes([checksum]) + FRAME_TAIL)

# 巡线 -&gt; 回传 line_error 和 line_angle(放大10倍发,保留0.1度精度)
def send_line(line_error, line_angle_deg):
    payload = struct.pack('&lt;hh', int(line_error), int(line_angle_deg * 10))
    send_frame(0x01, payload)

# 二维码/AprilTag/数字 -&gt; 只回一个 id
def send_id(msg_type, id_val):
    send_frame(msg_type, bytes([id_val &amp; 0xFF]))</code></pre>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 大小端要两边对上</p>
<p>上面用 <code>struct.pack('&lt;hh', ...)</code> 发的是<strong>小端</strong>（低字节在前）。主控那边拼字节时也必须按小端还原（见下面 C 代码里的 <code>data[0] | (data[1]&lt;&lt;8)</code>）。两边约定不一致，解出来就是乱值。</p>
</div>
<h2>六、主控收包：DMA + 串口空闲中断的黄金组合</h2>
<p>主控这边怎么收？新手常见的错法是：在串口中断里一个字节一个字节地收，还顺手把解析、甚至 PID 都塞进中断里。这样中断被占用太久，控制环都会被拖卡。</p>
<p>正确姿势是 <strong>DMA + 串口 IDLE（空闲）中断</strong>：</p>
<div class="ds-callout ds-callout-example" style="border-left:4px solid #7c4dff;background:#7c4dff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#7c4dff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> DMA + IDLE = 传送带 + 到货铃</p>
<p>DMA 是后台传送带，把串口来的字节一个个搬进缓冲区，全程不用 CPU 管；当一整帧到齐、线路空闲下来时，IDLE 中断就像&#8221;货到齐了按一下铃&#8221;，CPU 这才过来清点。平时 CPU 根本不用守着串口。</p>
</div>
<p>这套组合的好处是：<strong>零 CPU 占用 + 硬件自动断帧（天然支持变长收包）</strong>。现代 HAL 库写起来很省心，一个回调搞定：</p>
<pre><code class="language-c">#define RX_BUF_SIZE 64
static uint8_t rx_buf[RX_BUF_SIZE];
volatile uint8_t  frame_ready = 0;
volatile uint16_t frame_len   = 0;
static   uint8_t  frame_copy[RX_BUF_SIZE];

// 初始化时调用一次(放在 DMA/UART Init 之后)
void Vision_UART_Start(UART_HandleTypeDef *huart){
    HAL_UARTEx_ReceiveToIdle_DMA(huart, rx_buf, RX_BUF_SIZE);
    __HAL_DMA_DISABLE_IT(huart-&gt;hdmarx, DMA_IT_HT);  // 关半满中断,只要IDLE/收满
}

// 整帧到达回调:Size = 本帧字节数(收到指定长度或线路空闲触发)
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size){
    if (Size &gt; 0 &amp;&amp; Size &lt;= RX_BUF_SIZE){
        memcpy(frame_copy, rx_buf, Size);
        frame_len = Size; frame_ready = 1;            // 只置标志,解析放主循环
    }
    HAL_UARTEx_ReceiveToIdle_DMA(huart, rx_buf, RX_BUF_SIZE);  // 重启接收
    __HAL_DMA_DISABLE_IT(huart-&gt;hdmarx, DMA_IT_HT);
}</code></pre>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> DMA+IDLE 方案的第一坑：IDLE 中断要手动开</p>
<p>很多人用 <code>HAL_UARTEx_ReceiveToIdle_DMA</code> 却发现空闲中断死活不触发。原因是 <strong>IDLE 中断不像 DMA 完成中断那样自动开启</strong>，必须确认开了 UART 全局中断，且 DMA 配成 Normal 模式配合。如果你用老写法，记得 <code>__HAL_UART_ENABLE_IT(&amp;huart, UART_IT_IDLE)</code> 手动使能；老写法里算本帧长度用 <code>rx_len = BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&amp;hdma)</code>。</p>
</div>
<h3>解析：一道道闸机的状态机</h3>
<p>收到一整块字节后，怎么验证它是合法的一帧？用一个<strong>逐字节状态机</strong>最清晰。</p>
<div class="ds-callout ds-callout-example" style="border-left:4px solid #7c4dff;background:#7c4dff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#7c4dff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 状态机 = 过安检的闸机</p>
<p>一道道关卡：等帧头 → 读类型 → 读长度 → 收数据 → 验校验 → 等帧尾。每过一关进下一状态，任何一关不对就打回起点重新排队。最后放行的，一定是合规的完整帧。</p>
</div>
<pre><code class="language-c">typedef enum {S_H1,S_H2,S_TYPE,S_LEN,S_DATA,S_SUM,S_T1,S_T2} PState;
typedef struct { uint8_t type; uint8_t len; uint8_t data[32]; } Frame;

// 逐字节喂入,返回 1=完整有效帧, -1=出错, 0=进行中
int frame_feed(uint8_t byte, Frame *out){
    static PState st = S_H1; static uint8_t idx=0, sum=0; static Frame f;
    switch(st){
      case S_H1: if(byte==0xAA) st=S_H2; break;
      case S_H2: st = (byte==0x55)? S_TYPE : S_H1; break;     // 双字节帧头
      case S_TYPE: f.type=byte; sum=byte; st=S_LEN; break;
      case S_LEN:  f.len =byte; if(f.len&gt;32){st=S_H1;break;}  // 越界保护
                   sum+=byte; idx=0; st = f.len? S_DATA : S_SUM; break;
      case S_DATA: f.data[idx++]=byte; sum+=byte;
                   if(idx&gt;=f.len) st=S_SUM; break;
      case S_SUM:  if((sum&amp;0xFF)!=byte){ st=S_H1; return -1; } // 校验失败,丢
                   st=S_T1; break;
      case S_T1:   st = (byte==0x0D)? S_T2 : S_H1; break;
      case S_T2:   st=S_H1;
                   if(byte==0x0A){ *out=f; return 1; }        // 完整有效帧!
                   return -1;
    }
    return 0;  // 进行中
}</code></pre>
<div class="ds-callout ds-callout-note" style="border-left:4px solid #448aff;background:#448aff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#448aff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4dd.png" alt="📝" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 另一种常见写法：环形缓冲</p>
<p>如果你更习惯&#8221;DMA 整块搬进来、再批量找帧&#8221;，可以用环形缓冲区：循环找 0xAA 帧头、读长度、累加和校验，校验过才把整帧搬出、读指针前移。庐山派配套的 <code>command.c</code> 就是这套典型的嵌入式风格，<a href="https://github.com/humanfirework/K230-Study-Reference" target="_blank" rel="noopener noreferrer">K230-Study-Reference</a> 里有现成代码。想要工业级带 CRC-8 和转义的 C 解析库，可以参考 <a href="https://github.com/mohamedAziz-bousbih/uart-frame-parser" target="_blank" rel="noopener noreferrer">uart-frame-parser</a>。两种范式选一种用即可。</p>
</div>
<h2>七、保命：超时降级，绝不让小车失控</h2>
<p>视觉模块再好，赛场上也会掉链子——强光、反光、被手挡一下、线松了，丢帧是常态。这时候如果主控还傻等视觉数据，或者拿着一张过期的旧照片硬冲，小车就直接冲出赛道掉大分了。</p>
<p>所以必须有<strong>超时降级</strong>这道保命机制：</p>
<div class="ds-callout ds-callout-example" style="border-left:4px solid #7c4dff;background:#7c4dff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#7c4dff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 超时降级 = 备用降落伞</p>
<p>视觉是主伞（首选），但你一定要背个备用伞（灰度/IMU）。100ms 没收到有效数据就立刻开备用伞——切回灰度巡线，或用 IMU 锁住当前航向直行/缓停。宁可降落慢一点，也绝不自由落体（失控）。</p>
</div>
<pre><code class="language-c">volatile uint32_t last_valid_ms = 0;
int16_t line_error=0, line_angle=0; uint8_t vision_ok=0;

void Control_Loop(void){          // 固定周期调用(如 5ms)
    if(frame_ready){
        frame_ready=0; Frame f;
        for(uint16_t i=0;i&lt;frame_len;i++){
            int r = frame_feed(frame_copy[i], &amp;f);
            if(r==1){                                  // 只有校验通过的整帧
                last_valid_ms = HAL_GetTick();         // 才刷新超时计时!
                if(f.type==0x01){                       // 巡线
                    line_error = (int16_t)(f.data[0]|(f.data[1]&lt;&lt;8));
                    line_angle = (int16_t)(f.data[2]|(f.data[3]&lt;&lt;8));
                    vision_ok = 1;
                } else if(f.type==0x00){ vision_ok = 0; } // 视觉报告:没线
                // else 0x02/0x03/0x04: 二维码/Tag/数字 id = f.data[0]
            }
        }
    }
    if(HAL_GetTick() - last_valid_ms &gt; 100){            // &gt;100ms 超时
        vision_ok = 0;
        Fallback_GrayscaleOrIMU();                      // 降级:灰度巡线/IMU航向
    } else if(vision_ok){
        Motor_PID(line_error, line_angle);              // 正常:视觉闭环
    }
}</code></pre>
<p>这段里藏着几条容易被忽略、但赛场上很要命的细节：</p>
<div class="ds-callout ds-callout-important" style="border-left:4px solid #00bcd4;background:#00bcd414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bcd4"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2757.png" alt="❗" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 降级机制的三条铁律</p>
<ol>
<li><strong>只有校验通过的帧才刷新超时计时。</strong> 坏帧（校验错、残帧）一律丢弃、不刷新——否则坏数据会维持&#8221;视觉还活着&#8221;的假象，该降级时不降级。</li>
<li><strong>超时阈值要留余量。</strong> 100ms 大约是正常帧间隔的 2~3 倍，给抖动留空间，不会因为偶尔晚到一帧就误降级。</li>
<li><strong>降级和恢复之间加迟滞（hysteresis）。</strong> 视觉时断时续会让小车在&#8221;正常<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2194.png" alt="↔" class="wp-smiley" style="height: 1em; max-height: 1em;" />降级&#8221;之间来回抖动切换。可以设成：连续丢 N 帧才降级，连续收到 M 帧好数据才切回视觉。</li>
</ol>
</div>
<p>要让降级真正靠谱，前提是<strong>灰度、IMU 这些备选方案本身就已经能独立巡线/锁航向</strong>。这就回到了一条贯穿整个系列的最佳实践：</p>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 先各模块单独调通，再合并</p>
<p>视觉、灰度、IMU、驱动、通信都先各自单独验证能跑，再合并到主控。2024 电赛 H 题的获奖方案就反复强调这种模块化——隐藏好处正是：视觉一掉线，主控能无缝切到&#8221;已经独立验证过&#8221;的灰度/IMU。赛前一定要专门做一次<strong>故障注入测试：故意拔掉视觉线</strong>，看小车会不会乖乖降级，而不是只测正常路径。</p>
</div>
<h2>八、联调心法与一份避坑速查</h2>
<p>最后给一句这一篇最重要的工作方法：</p>
<div class="ds-callout ds-callout-success" style="border-left:4px solid #00c853;background:#00c85314;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00c853"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 先把协议定死，双方各自开发，最后联调</p>
<p>视觉同学和电控同学一开始就坐下来，把&#8221;传哪几个数、什么类型、什么格式、大小端、波特率&#8221;全部写在一张纸上钉死。然后各写各的——视觉端发完整帧（哪怕暂时发假数据），电控端按协议解析。等两边都自测通过，插上线联调时往往一次就通。最怕的是边写边改协议，两头永远对不齐。</p>
</div>
<p>随手附一份本篇的避坑速查，赛前过一遍：</p>
<ul>
<li>☐ 视觉端只发&#8221;结论&#8221;几个数，绝不发整图</li>
<li>☐ 帧头用双字节 0xAA 0x55 + 长度 + 校验三重确认</li>
<li>☐ K230 用串口前先 FPIOA 配引脚复用，且别用 UART0/UART3</li>
<li>☐ K230 与 STM32 都是 3.3V，TX-RX 交叉接、<strong>务必共地</strong></li>
<li>☐ 主控收包走 DMA + IDLE 中断，IDLE 中断记得手动使能</li>
<li>☐ 中断里只置标志，解析和控制放主循环</li>
<li>☐ 大小端两边对齐（视觉 <code>&lt;hh</code> 小端 → 主控低字节在前还原）</li>
<li>☐ 只有校验通过的帧才刷新超时计时，坏帧只丢弃</li>
<li>☐ &gt;100ms 无有效帧立即降级到灰度/IMU，并加迟滞防抖动</li>
<li>☐ 巡线阈值和 ROI 到现场必须重新标定，别照抄例程</li>
<li>☐ 赛前做一次&#8221;拔视觉线&#8221;的故障注入，验证降级真的会触发</li>
</ul>
<p>到这里，小车的&#8221;眼睛&#8221;和&#8221;神经&#8221;就接通了：视觉看到东西、算出结论、打包成帧发出去，主控收包、校验、闭环，还带着超时降级的保命绳。但你可能已经发现，主控这边的活越来越多了——既要跑控制环、解析串口，又要处理起步、过弯、停靠、丢线找回这些不同阶段的逻辑。这些怎么有条不紊地组织起来、各自按什么节奏跑，就是下一篇要解决的事：状态机与整车软件架构。</p>
<div class="ds-series" style="border:1px solid #4488ff33;background:#4488ff0d;border-radius:8px;padding:.8em 1.1em;margin:1.2em 0">
<p style="margin:0 0 .5em"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4da.png" alt="📚" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 本文是 <strong>「从 0 到 1 带你打电赛 · 小车电控篇」</strong> 系列（共 12 篇）第 10 篇。</p>
<ol style="margin:.2em 0 0;padding-left:1.4em">
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-01-how-to-score/">第1篇 · 拿奖逻辑：把比赛拆成小目标</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-02-history/">第2篇 · 赛题进化史与押题</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-03-build-and-architecture/">第3篇 · 整车搭建与代码框架</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-04-motor-power/">第4篇 · 电机驱动与电源地基</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-05-sensing/">第5篇 · 感知：灰度/电磁/编码器/IMU</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-06-pid-basics/">第6篇 · PID 入门：搞懂 P/I/D</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-07-pid-advanced/">第7篇 · PID 进阶：串级+工程补丁</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-08-pid-tuning/">第8篇 · PID 调参实战(核心)</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-09-advanced-control/">第9篇 · 进阶控制：几时该上</a></li>
<li style="margin:.15em 0"><strong>第10篇 · K230 视觉与通信协议（本篇）</strong></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-11-architecture-fsm/">第11篇 · 状态机与整车软件</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-12-field-manual/">第12篇 · 现场作战+避坑+开源</a></li>
</ol>
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://cloudlay.cn/nuedc-car-10-vision-comm/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>从0到1带你打电赛·小车电控篇(九)：进阶控制算法——前馈、模糊PID、卡尔曼，什么时候才值得上</title>
		<link>https://cloudlay.cn/nuedc-car-09-advanced-control/</link>
					<comments>https://cloudlay.cn/nuedc-car-09-advanced-control/#respond</comments>
		
		<dc:creator><![CDATA[云间辞]]></dc:creator>
		<pubDate>Sun, 14 Jun 2026 17:08:33 +0000</pubDate>
				<category><![CDATA[嵌入式]]></category>
		<category><![CDATA[Bang-Bang]]></category>
		<category><![CDATA[前馈控制]]></category>
		<category><![CDATA[卡尔曼滤波]]></category>
		<category><![CDATA[智能小车]]></category>
		<category><![CDATA[模糊PID]]></category>
		<category><![CDATA[电赛]]></category>
		<guid isPermaLink="false">https://cloudlay.cn/nuedc-car-09-advanced-control/</guid>

					<description><![CDATA[📚 本文是 「从 0 到 1 带你打电赛 · 小车电控篇」 系列（共 12 篇）第 9 篇。 第1篇 · 拿奖 [&#8230;]]]></description>
										<content:encoded><![CDATA[<div class="ds-series" style="border:1px solid #4488ff33;background:#4488ff0d;border-radius:8px;padding:.8em 1.1em;margin:1.2em 0">
<p style="margin:0 0 .5em"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4da.png" alt="📚" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 本文是 <strong>「从 0 到 1 带你打电赛 · 小车电控篇」</strong> 系列（共 12 篇）第 9 篇。</p>
<ol style="margin:.2em 0 0;padding-left:1.4em">
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-01-how-to-score/">第1篇 · 拿奖逻辑：把比赛拆成小目标</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-02-history/">第2篇 · 赛题进化史与押题</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-03-build-and-architecture/">第3篇 · 整车搭建与代码框架</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-04-motor-power/">第4篇 · 电机驱动与电源地基</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-05-sensing/">第5篇 · 感知：灰度/电磁/编码器/IMU</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-06-pid-basics/">第6篇 · PID 入门：搞懂 P/I/D</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-07-pid-advanced/">第7篇 · PID 进阶：串级+工程补丁</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-08-pid-tuning/">第8篇 · PID 调参实战(核心)</a></li>
<li style="margin:.15em 0"><strong>第9篇 · 进阶控制：几时该上（本篇）</strong></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-10-vision-comm/">第10篇 · K230 视觉与通信协议</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-11-architecture-fsm/">第11篇 · 状态机与整车软件</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-12-field-manual/">第12篇 · 现场作战+避坑+开源</a></li>
</ol>
</div>
<p>到了这一篇，你手里应该已经有一辆能跑的车了：电机听话转、编码器能测速、串级 PID 调好以后能稳稳跟线、过弯也不冲出去。这时候很多同学会开始心痒——网上铺天盖地的&#8221;模糊 PID&#8221;&#8221;卡尔曼滤波&#8221;&#8221;前馈控制&#8221;&#8221;自抗扰 ADRC&#8221;，名字一个比一个唬人，是不是把这些都堆上去，车就能从&#8221;能跑&#8221;变成&#8221;封神&#8221;？</p>
<p>先把话撂在这儿：<strong>90% 的省赛小车，把基础 PID 加串级双环调扎实，就足够拿奖了。</strong> 这一篇要讲的进阶算法，定位是&#8221;锦上添花 + 报告亮点&#8221;，不是&#8221;不上就完蛋&#8221;的必需品。本篇的真正价值，不是教你把每个算法都写一遍，而是帮你判断：<strong>哪个算法解决什么问题、什么时候才值得上、性价比到底高不高</strong>——免得你把宝贵的四天三夜耗在调一个收益微薄的高级算法上，最后基础反而没跑稳。</p>
<div class="ds-callout ds-callout-important" style="border-left:4px solid #00bcd4;background:#00bcd414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bcd4"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2757.png" alt="❗" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 一句话定调</p>
<p>进阶算法是&#8221;装修&#8221;，基础 PID 是&#8221;地基&#8221;。地基没夯实就急着装修，房子只会塌得更难看。先把机械中值、编码器测速、PWM 频率、控制周期、串级 PID 这些做扎实，再按需逐级往上加——每加一个，都留一份对比数据。</p>
</div>
<h2>先认清这条&#8221;逐级升级&#8221;的路线</h2>
<p>进阶算法不是平行摆在货架上让你随便挑的，它们有明显的&#8221;性价比梯度&#8221;：越往后，调试成本越高、边际收益越小。一个稳妥的升级顺序长这样：</p>
<figure class="ds-diagram" style="text-align:center;margin:1.3em 0"><img decoding="async" src="https://cloudlay.cn/wp-content/uploads/dianseiche/15b54c7c8a54.png" alt="电赛小车电控 · 示意图" loading="lazy" style="max-width:100%;height:auto;background:#fff;border-radius:8px;padding:6px;box-shadow:0 1px 6px rgba(0,0,0,.25)"></figure>
<p>为什么是这个顺序？因为越靠左的，改动小、收益直接、不容易引入新 bug；越靠右的（尤其模糊 PID、卡尔曼），自由度多、调试耗时，稍有不慎反而把好不容易调稳的车搞乱。<strong>省赛时间紧，跑不稳就果断回退到上一级。</strong> 下面我们一个一个拆。</p>
<div class="ds-callout ds-callout-note" style="border-left:4px solid #448aff;background:#448aff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#448aff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4dd.png" alt="📝" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 一句话交代前置</p>
<p>这一篇默认你已经把单环 PID、串级双环和那一套&#8221;工程补丁&#8221;调通了——位置式/增量式怎么选、方向环为什么用 PD、抗积分饱和怎么做，分别在《PID 入门》《PID 进阶》两篇里讲过；具体怎么看波形对症下药，见《PID 调参实战》。这里只聊&#8221;在基础之上还能再加什么&#8221;。</p>
</div>
<h2>变速积分 / 积分分离：性价比最高，优先上</h2>
<p>这是进阶项里<strong>性价比最高</strong>的一个：简单、收益直接，建议第一个上。</p>
<p>普通 PID 的积分系数是恒定的，全程积分增量都一样。但实际需求是矛盾的——误差大的时候，积分项疯狂累加，等误差快归零时这股劲儿泄不掉，造成严重超调；误差小的时候又需要积分稳稳消掉静差。</p>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 攒钱补差距的比喻</p>
<p>把积分想象成攒钱补差距。离目标还远（误差大）时<strong>先别急着存钱</strong>，免得攒过头冲出去；快到目标（误差小）了，再慢慢攒，把最后那一点点差距精确补平。</p>
</div>
<p>最直接的办法是三段式<strong>积分分离</strong>：</p>
<table>
<thead>
<tr>
<th>误差大小</th>
<th>积分系数</th>
<th>含义</th>
</tr>
</thead>
<tbody>
<tr>
<td>$\lvert e\rvert > 50$</td>
<td>0</td>
<td>误差大，切断积分</td>
</tr>
<tr>
<td>$30 < \lvert e\rvert < 50$</td>
<td>0.6</td>
<td>过渡区，弱化积分</td>
</tr>
<tr>
<td>$\lvert e\rvert < 30$</td>
<td>1.0</td>
<td>误差小，全额积分</td>
</tr>
</tbody>
</table>
<p>但阶跃式分段在临界点会有突变。更平滑的是<strong>变速积分</strong>，用一个线性过渡系数：</p>
<pre><code class="language-c">if (pid-&gt;errABS &gt; lErr) {            // 误差大: 不积分
    kiIndex = 0.000f;
} else if (pid-&gt;errABS &lt; sErr) {     // 误差小: 全额积分
    kiIndex = 1.000f;
} else {                             // 中间: 线性过渡
    kiIndex = (lErr - pid-&gt;errABS) / (lErr - sErr);
}
pid-&gt;outPut = pid-&gt;outPutP + pid-&gt;outPutI * kiIndex + pid-&gt;outPutD;</code></pre>
<p>经验取值 <code>lErr ≈ 50</code>、<code>sErr ≈ 30</code>。</p>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 这些阈值不是普适常数</p>
<p>50/30 这组数字是某位作者在他那套车上的具体工程取值（来源见 <a href="https://ittuann.github.io/2021/08/29/CarPID.html" target="_blank" rel="noopener noreferrer">智能车常规 PID 及改进式 PID</a>）。一旦你换了被控量的量纲/单位，这两个阈值就得重新定。能照抄的是&#8221;误差大弱化、误差小全额&#8221;这个<strong>思想</strong>，不是这两个数。</p>
</div>
<p><strong>怎么选？</strong> 匀速车选变速积分（积分主导稳态精度，平滑过渡最好）；有明显加减速的车，更该重视下面这个&#8221;遇限削弱积分&#8221;。</p>
<div class="ds-callout ds-callout-important" style="border-left:4px solid #00bcd4;background:#00bcd414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bcd4"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2757.png" alt="❗" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 补一句：抗积分饱和是底线，不是进阶</p>
<p>积分饱和（输出顶到限幅了积分还在傻傻累加，反向时半天泄不掉，造成大超调甚至震荡）是 PID 翻车的<strong>头号杀手</strong>。&#8221;遇限削弱积分 + 积分限幅 + 输出限幅 + 模式切换清零积分&#8221;这一套，属于<strong>基础工程补丁</strong>，《PID 进阶》一篇已经讲透，这里不重复——只强调一点：<strong>它比上面任何进阶算法都更必须做。</strong> 顺带提一个工业上更平滑的进阶玩法叫 <strong>back-calculation（反算抗饱和）</strong>：把饱和被砍掉的那部分输出，按一个增益回灌到积分器里，退饱和比简单的&#8221;条件积分&#8221;更柔和。省赛用条件积分够了，这个留作了解。</p>
</div>
<h2>Bang-Bang + PID：电机弱的车靠它&#8221;榨提速&#8221;</h2>
<h3>思路：大误差全力冲，小误差精细停</h3>
<p>Bang-Bang（起停 / 砰砰控制）让输出在两个极端之间切换——要么全速，要么全刹。控制理论里有个结论：当哈密顿量对控制量是线性的时候，Bang-Bang 就是<strong>最短时间最优解</strong>。说人话就是：想在最短时间内从 A 跑到 B，&#8221;地板油加速 + 急刹&#8221;往往就是最快的。</p>
<p>但纯 Bang-Bang 在目标附近会疯狂抖动，所以智能车里用的是组合拳——<strong>用 Bang-Bang 控力度、用 PID 控精度</strong>：</p>
<pre><code class="language-c">if (e &gt;  eps)       u = Umax;       // 大正偏差: 全速
else if (e &lt; -eps)  u = -Umax;      // 大负偏差: 全刹/反向
else                u = PID(e);     // 阈值内: 切 PID 精调
// eps(阈值)务必带迟滞, 避免阈值附近来回抖动(chattering)</code></pre>
<div class="ds-callout ds-callout-example" style="border-left:4px solid #7c4dff;background:#7c4dff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#7c4dff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 油门到底冲 + 到点轻踩</p>
<p>离目标速度还远，就一脚地板油（全速）把动态榨出来；快到了，松开油门、踩点刹车，交给 PID 精细停准。电机扭矩不足的车型，靠这&#8221;一脚地板油&#8221;能明显提速。</p>
</div>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 选电机阶段就该量化测试</p>
<p>有支飞思卡尔队伍的 C 车模电机偏弱，他们用恒流源逐一测对应电压下的电流和转速来选电机，再用&#8221;Bang-Bang 控力度 + PID 控精度&#8221;的组合榨出加速性能（<a href="https://www.cnblogs.com/pang123hui/archive/2010/08/16/2309974.html" target="_blank" rel="noopener noreferrer">飞思卡尔智能车电机 PID</a>）。<strong>电机弱的时候别死磕 PID 调速</strong>：Bang-Bang 负责大误差段全速冲、PID 负责小误差精调，是性价比极高的提速手段。</p>
</div>
<h3>它的坑：抖动（chattering）</h3>
<p>Bang-Bang 最大的毛病是切换不连续。如果阈值 $\varepsilon$ 不带<strong>迟滞</strong>，误差在阈值附近来回时，输出就在两个极值之间高频跳变——既伤电机，又让车发抖。</p>
<p>解决办法是给阈值加一个迟滞带（施密特触发器的思路）：进入用一个阈值、退出用另一个略小的阈值，中间留一段缓冲。</p>
<div class="ds-callout ds-callout-note" style="border-left:4px solid #448aff;background:#448aff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#448aff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4dd.png" alt="📝" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 理论上的&quot;瞬时全速/全刹&quot;做不到</p>
<p>补一句严谨的：纯 Bang-Bang 的&#8221;最短时间最优&#8221;建立在&#8221;控制能瞬时切换&#8221;上，而且对一个 $n$ 阶线性系统，切换次数不超过系统阶数（二阶系统最多切一次）。但实际电机有电气时间常数、执行器有带宽，全速 / 全刹不可能一瞬间完成。这也正是为什么一定要带 PID 精调段 + 迟滞，而不是指望纯 Bang-Bang 一招鲜。</p>
</div>
<h2>前馈控制：从&#8221;亡羊补牢&#8221;到&#8221;未雨绸缪&#8221;</h2>
<h3>它到底解决什么问题</h3>
<p>PID 是典型的<strong>反馈</strong>控制——它盯着&#8221;已经发生的偏差&#8221;去纠正。问题是，等偏差出现了你才动作，永远慢半拍。高速过弯的时候，等你看到车已经偏出赛道（误差变大）才猛打方向，往往已经来不及了。</p>
<p>前馈（feedforward）反过来：它不等误差出现，而是<strong>根据已知的扰动或模型提前补偿</strong>。</p>
<div class="ds-callout ds-callout-example" style="border-left:4px solid #7c4dff;background:#7c4dff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#7c4dff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 老司机过弯的比喻</p>
<p>反馈 PID = 亡羊补牢：羊跑了（偏差出现）才去补圈。 前馈 = 未雨绸缪：老司机看到前方是个弯（已知曲率），方向盘提前就开始打了，根本不等车冲出去。 实战里两者一定是搭配用的——<strong>前馈预判、反馈兜底</strong>。前馈把大头先补上，剩下模型不准、路面打滑这些没料到的误差，交给 PID 收尾。</p>
</div>
<p>它的威力在于：在扰动影响输出<strong>之前</strong>就预补偿，响应能快很多，动态误差明显减小，能耗和磨损也更低。但它有三个硬性前提：</p>
<ul>
<li>必须有一个<strong>比较准的被控对象模型</strong>（前馈控制器本质上就是被控对象模型的&#8221;倒数&#8221;）；</li>
<li>扰动必须<strong>可测 / 可预知</strong>——对没料到的扰动它无能为力；</li>
<li><strong>每个系统的前馈控制器都不一样，是专用的</strong>，换车、换赛道就得重算。</li>
</ul>
<p>所以前馈不能单用，铁律是和 PID 复合：</p>
<p class="ds-math" style="overflow-x:auto">$$U(k) = U_{pid}(k) + U_{ff}(k)$$</p>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 前馈滥用是个坑</p>
<p>前馈只补&#8221;可测扰动&#8221;，而且依赖较准的模型。模型不准的前馈反而帮倒忙——你以为提前打了正确的舵，结果模型错了，等于主动把车往沟里带。所以宁可前馈量保守一点、让反馈多兜底，也别让一个不靠谱的模型主导输出。</p>
</div>
<h3>智能车里前馈的两个典型落地</h3>
<p><strong>第一个：曲率前馈打舵。</strong> 侧向控制量 = 前馈 + 反馈。前馈那一份由参考路径的曲率和车辆动力学模型直接算出来（舵机转角公式里含轮距 $L$、曲率 $\rho$、不足转向梯度）。曲率怎么求？一个实用做法是用赛道上连续三个点：曲率等于这三点外接圆半径的倒数，而拐向用三点的叉乘面积判——<strong>右拐为负、左拐为正</strong>。高速时舵机响应慢，在输出里提前加入这个&#8221;路径趋势分量&#8221;，舵机就能预先打舵，而不是等偏差累积。</p>
<p><strong>第二个：速度突变前馈 PWM。</strong> 速度环里，当目标速度突然变化（比如起步、出弯加速）时，光靠 PID 慢慢追会有明显滞后。这时候直接前馈一段 PWM，电机响应立刻跟上。一个常见的离散前馈实现，是对设定值做一阶 + 二阶差分加权：</p>
<pre><code class="language-plaintext">前馈输出: result = α*(rin - lastRin) + β*(rin - 2*lastRin + prevRin)
         其中 α = A/(B*T), β = 1/(B*T^2)   (A、B 为对象模型参数, T 为采样周期)
复合控制: U(k) = Upid(k) + Uff(k)   // 位置式或增量式 PID 都行</code></pre>
<p>第一项是&#8221;速度前馈&#8221;（设定值变化的快慢），第二项是&#8221;加速度前馈&#8221;（设定值变化的变化）。$\alpha$、$\beta$ 由你的电机模型参数和采样周期决定，没有万能值，得自己标。</p>
<h2>死区补偿：一个不起眼但特别实用的小补丁</h2>
<p>这个严格说属于《PID 进阶》里的&#8221;工程补丁&#8221;，但太常用、又特别容易被漏掉，这里单独点一下。</p>
<p>直流电机有个&#8221;死区电压&#8221;：大约是额定的 10%~15%（比如一个 12V 电机，低于约 1.5~2V 根本不转）。PID 算出来的小 PWM 如果落在死区里，电机纹丝不动——结果就是低速爬行、静差死活消不掉、起步迟滞。</p>
<p>解决办法很简单，输出非零时叠加一个死区偏置，把电机直接推出静摩擦区：</p>
<pre><code class="language-c">// 输出不为零时, 补一个死区偏置, 把电机踹出死区
if (pwm_out &gt; 0)      pwm_out += PWM_DEADZONE;
else if (pwm_out &lt; 0) pwm_out -= PWM_DEADZONE;</code></pre>
<div class="ds-callout ds-callout-note" style="border-left:4px solid #448aff;background:#448aff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#448aff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4dd.png" alt="📝" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 别和&quot;死区时间&quot;搞混</p>
<p>这里说的&#8221;死区补偿&#8221;是补电机的死区电压。它和 H 桥 PWM 互补输出里的&#8221;死区时间（dead-time，防上下桥臂直通烧管子）&#8221;是<strong>完全不同的两个概念</strong>，名字像但别混。</p>
</div>
<h2>模糊 PID：报告里的&#8221;高级感&#8221;，赛场上的&#8221;性价比陷阱&#8221;</h2>
<h3>它解决&#8221;一套参数不够用&#8221;</h3>
<p>普通 PID 全程就一套 Kp/Ki/Kd。但智能车是个<strong>时变、非线性</strong>的系统——高速时需要一套参数，低速时需要另一套，直道和急弯的需求完全不同。一套死参数顾此失彼。</p>
<p>模糊 PID 的做法是：输入当前偏差 $e$ 和偏差变化率 $ec$，经过模糊化（量化因子 $K_e = N/e_{max}$ 把 $e$ 映射到论域）→ 查一张 7×7 的模糊规则表（模糊子集 NB/NM/NS/ZO/PS/PM/PB）→ 模糊推理 → 解模糊（缩放因子 $K_u = u_{max}/N$）→ 在线输出 $\Delta K_p / \Delta K_i / \Delta K_d$，实时微调 PID 三个参数。</p>
<div class="ds-callout ds-callout-example" style="border-left:4px solid #7c4dff;background:#7c4dff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#7c4dff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 请了个老师傅在旁边随时拧旋钮</p>
<p>你给老师傅看：现在差多少（$e$）、差距是在变快还是变慢（$ec$）。他凭经验（规则表）实时帮你把 Kp/Ki/Kd 拧大拧小，而不是让你全程守着一套死参数。本质上是&#8221;模仿熟练驾驶员的推理&#8221;去协调横向偏角和纵向速度。</p>
</div>
<h3>残酷的现实：省赛性价比偏低</h3>
<p>模糊 PID 听起来很美，但它的调试自由度多得吓人——规则表、隶属度函数、量化因子、缩放因子，每一个都要调。</p>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 花两天调模糊 PID，可能还不如认真调好的串级 PID</p>
<p>这是社区里相当普遍的反馈。模糊 PID 的实测收益常常<strong>不如</strong>把串级 PID 老老实实调扎实，但它会实实在在地吃掉你两天时间，容易拖垮整体进度。</p>
<p>那它的价值在哪？<strong>报告和答辩的创新点。</strong> 在论文里能体现&#8221;仿照熟练驾驶员推理、协调横向偏角与纵向速度&#8221;，这是实打实的加分项。</p>
</div>
<p>所以建议很明确：<strong>车先用普通 / 串级 PID 跑稳，确实有余力了再上模糊 PID，并且一定要在报告里给出对比曲线</strong>——&#8221;加了模糊 PID 之后，过弯误差从 X 降到 Y&#8221;，这种带数据的论证，比光堆一个&#8221;我们用了模糊 PID&#8221;的名词强一百倍。想找现成起点的话，<a href="https://github.com/FlameAlpha/fuzzy-pid" target="_blank" rel="noopener noreferrer">FlameAlpha/fuzzy-pid</a>（纯 C 实现）和 <a href="https://github.com/the-fenrir/Fuzzy-PID" target="_blank" rel="noopener noreferrer">the-fenrir/Fuzzy-PID</a>（直接面向 STM32 电机控制）都可以拿来改。</p>
<h2>卡尔曼 vs 互补滤波：姿态融合该选哪个</h2>
<p>这一节主要是给做<strong>直立车 / 平衡车</strong>的同学——姿态解算是直立车的命根子。四轮循迹车其实多数时候用陀螺 Z 轴 + 简单滤波锁航向就够了。</p>
<h3>为什么要融合</h3>
<p>MPU6050 这类 6 轴 IMU 有两个传感器，各有各的毛病：</p>
<ul>
<li><strong>加速度计</strong>算出的 Pitch/Roll 是<strong>绝对</strong>的（长期不漂），但噪声大、一晃就乱跳；</li>
<li><strong>陀螺仪</strong>积分出的角度<strong>平滑</strong>（短期准），但会随时间<strong>漂移</strong>。</li>
</ul>
<div class="ds-callout ds-callout-example" style="border-left:4px solid #7c4dff;background:#7c4dff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#7c4dff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 两个证人互相补台</p>
<p>陀螺仪：短期说得准，但时间一长爱吹牛（漂移）。加速度计：长期靠谱，但一受震动就慌（噪声）。各取所长——<strong>短期信陀螺仪、长期信加速度计</strong>，拼出真相。</p>
</div>
<p>一阶互补滤波的公式和代码，《感知：灰度/电磁/编码器/IMU》一篇已经给过（$angle = a\cdot(angle + gyro\cdot dt) + (1-a)\cdot acc$，$a$ 取 0.98）。这里不重复推导，只把&#8221;两条路线怎么选&#8221;讲清楚。</p>
<h3>互补滤波：简单、够用，省赛首选</h3>
<p>互补滤波就一行核心公式，优点是计算量小、易于嵌入式实现，平衡车直立环完全够用。它的两个系数 $a$ 与 $1-a$ 之和必须等于 1，$a$ 的物理含义是 $a = \tau/(\tau+dt)$，$\tau$ 越大越信陀螺仪、跟随越慢。</p>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 几个能直接用的数</p>
<ul>
<li>100Hz（$dt=0.01$s）、$a=0.98$ 时，时间常数 $\tau = a\cdot dt/(1-a) = 0.49$s。</li>
<li>控制周期 5ms、想要 $\tau=1$s 时，$a \approx 0.995$。</li>
<li>调参从 <strong>$a=0.98$</strong> 起步，这是社区验证过的安全起点。$a$ 越大越平滑但跟随慢，越小越跟手但越抖。</li>
</ul>
</div>
<p>它的局限是对陀螺仪零漂的抑制有限、初始收敛慢。</p>
<h3>卡尔曼滤波：精度更高，但要交&#8221;学费&#8221;</h3>
<p>卡尔曼把陀螺仪角速度（当先验/预测）和加速度计倾角（当观测）做最优融合，还能在线估计陀螺零偏，精度比互补滤波更高。代价是：计算量大、要懂点统计、还得调那两个噪声参数 $Q$ 和 $R$。</p>
<div class="ds-callout ds-callout-example" style="border-left:4px solid #7c4dff;background:#7c4dff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#7c4dff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 会算概率的精明法官</p>
<p>卡尔曼不只听两个证人，还根据各自&#8221;平时多靠谱&#8221;（$Q$/$R$ 噪声）<strong>动态决定</strong>这一刻该多信谁。判得更准，但你得先摸清两个证人的脾气（调 $Q$/$R$），挺费脑子。其实卡尔曼可以理解成&#8221;增益会自适应的互补滤波&#8221;。</p>
</div>
<p>$Q$/$R$ 这两个参数的方向特别容易记反，这里给准确说法：</p>
<ul>
<li><strong>$Q$（过程噪声）</strong>：越小越信模型/预测，越大越信测量。</li>
<li><strong>$R$（测量噪声）</strong>：越大响应越慢、越信预测；越小收敛越快，但<strong>过小会震荡</strong>。</li>
</ul>
<p>调法：$R$ 的初值可以这样估——让陀螺仪静止采一段数据（近似正态分布），按 $3\sigma$ 原则取 $(3\sigma)^2$ 作 $R$ 初值；然后 <strong>$Q$ 由小到大、$R$ 由大到小</strong>慢慢试。平衡车常用的一套经典参数（TKJ 版）是 <code>Q_angle=0.001, Q_bias=0.003, R_measure=0.03, dt=0.005~0.01</code>。</p>
<div class="ds-callout ds-callout-danger" style="border-left:4px solid #ff1744;background:#ff174414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff1744"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> $Q$/$R$ 乱调是无底洞</p>
<p>$R$ 太大响应变慢、$R$ 太小震荡，$Q$/$R$ 不匹配会让滤波结果要么保留运动噪声、要么受零漂影响。<strong>没有方法地瞎试，能浪费掉你一整天。</strong> 有方法地按上面的套路逼近。</p>
</div>
<h3>结论：省赛优先互补，精度敏感再上卡尔曼</h3>
<div class="ds-callout ds-callout-success" style="border-left:4px solid #00c853;background:#00c85314;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00c853"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 一句话决策</p>
<p>多位做姿态融合的开发者都直言&#8221;卡尔曼太复杂了&#8221;，转头选了互补滤波（<a href="https://fireholder.github.io/posts/hubu_filter" target="_blank" rel="noopener noreferrer">互补滤波概述</a>）。<strong>省赛优先用互补滤波把车站稳，别为了&#8221;高级&#8221;硬上卡尔曼把时间耗在调 $Q$/$R$ 上；真到了对漂移/精度敏感的程度，再升级。</strong> MPU6050 内置的 DMP 也是个省事选项（硬件直接输出姿态、不占主控算力），缺点是灵活性差、移植驱动繁琐。</p>
</div>
<p>如果你做的是多轴/全姿态（不只直立环单轴），还有两条比手写卡尔曼更省心的路：<strong>带零偏在线估计的二阶互补滤波</strong>（专治一阶互补&#8221;零漂抑制有限&#8221;的毛病），以及无人机/AHRS 圈广泛用的 <strong>Madgwick/Mahony 四元数互补滤波</strong>（计算量介于一阶互补和卡尔曼之间）。想直接抄卡尔曼的，<a href="https://github.com/Mattral/Kalman-Filter-mpu6050" target="_blank" rel="noopener noreferrer">Mattral/Kalman-Filter-mpu6050</a> 是即拿即用的姿态卡尔曼实现。</p>
<h2>一张表：到底什么时候该上什么</h2>
<p>把上面这些拢成一张决策表，照着对号入座就行：</p>
<table>
<thead>
<tr>
<th>算法</th>
<th>解决什么</th>
<th>何时值得上</th>
<th>性价比</th>
<th>调试成本</th>
</tr>
</thead>
<tbody>
<tr>
<td>变速积分/积分分离</td>
<td>积分饱和、超调</td>
<td>匀速车，几乎都该上</td>
<td>很高</td>
<td>低</td>
</tr>
<tr>
<td>抗积分饱和(遇限削弱)</td>
<td>饱和退不掉→大超调</td>
<td><strong>凡带积分的环必做</strong></td>
<td>很高</td>
<td>低</td>
</tr>
<tr>
<td>死区补偿</td>
<td>低速爬行、静差消不掉</td>
<td>电机有明显死区</td>
<td>高</td>
<td>很低</td>
</tr>
<tr>
<td>Bang-Bang+PID</td>
<td>电机弱、想提速</td>
<td>扭矩不足、追用时</td>
<td>高</td>
<td>中</td>
</tr>
<tr>
<td>前馈</td>
<td>响应慢半拍、动态误差</td>
<td>高速过弯、目标突变</td>
<td>中高</td>
<td>中(要模型)</td>
</tr>
<tr>
<td>模糊 PID</td>
<td>一套参数不够用</td>
<td>余力 + 想要报告亮点</td>
<td>低(赛场)/高(报告)</td>
<td>高</td>
</tr>
<tr>
<td>卡尔曼</td>
<td>姿态精度/漂移</td>
<td>直立车且互补不够</td>
<td>中</td>
<td>高</td>
</tr>
</tbody>
</table>
<h2>几条压箱底的忠告</h2>
<p>进阶算法这块，翻车的往往不是&#8221;算法没学会&#8221;，而是&#8221;用错了地方&#8221;。最后几条，记牢：</p>
<ul>
<li><strong>别本末倒置。</strong> 机械中值没找准、编码器测速不稳、PWM 频率/控制周期没固定，就急着堆模糊 PID/卡尔曼——地基不稳，高级算法只会<strong>放大问题、引入更多 bug</strong>。</li>
<li><strong>控制周期必须固定且已知。</strong> Ki、Kd、互补滤波的 $a$、卡尔曼的 $Q$/$R$，<strong>全都隐含依赖采样周期 $T$</strong>。一定要用硬件定时器中断跑控制环来保证周期恒定，把打印、无线发送这些放低优先级或降频。有队伍踩过坑：调 PID 时发现效果不只受 PID 数值影响，还明显受无线发数间隔、编码器测速间隔的影响——<strong>控制周期不稳是隐形杀手</strong>。</li>
<li><strong>逐级加、留对比数据。</strong> 先用基础/串级 PID 跑稳跑完整圈，再按&#8221;变速积分 → Bang-Bang → 前馈 → 模糊 PID&#8221;逐级加，每加一个都留一份对比曲线。这既是科学的工程方法，也是答辩时最有说服力的素材。</li>
<li><strong>报告别只堆名词。</strong> 论文/答辩里只写&#8221;我们用了模糊 PID/卡尔曼&#8221;却不讲&#8221;为什么需要 + 对比数据&#8221;，反而显得为用而用。<strong>把一个算法讲透 + 给曲线</strong>，远比罗列一堆名词加分。</li>
</ul>
<div class="ds-callout ds-callout-success" style="border-left:4px solid #00c853;background:#00c85314;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00c853"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 收个尾</p>
<p>这一篇你不用全记住，记住一个判断标准就够了：<strong>这个算法解决的痛点，是不是我的车现在真正卡住的地方？</strong> 是，就上，并且用数据证明它有用；不是，就先放着——把时间还给基础。90% 的省赛车，赢在&#8221;基础调得比别人扎实&#8221;，而不是&#8221;算法堆得比别人多&#8221;。</p>
</div>
<p>讲到这里，电控的&#8221;控制&#8221;部分基本拼齐了。但小车要真正变聪明——看得懂赛道、认得出数字和色块、和主控对得上话——还差一双&#8221;眼睛&#8221;。下一篇我们就来聊 K230 视觉模块怎么用、怎么设计一套稳的通信协议，把视觉结论稳稳喂给主控。</p>
<div class="ds-series" style="border:1px solid #4488ff33;background:#4488ff0d;border-radius:8px;padding:.8em 1.1em;margin:1.2em 0">
<p style="margin:0 0 .5em"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4da.png" alt="📚" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 本文是 <strong>「从 0 到 1 带你打电赛 · 小车电控篇」</strong> 系列（共 12 篇）第 9 篇。</p>
<ol style="margin:.2em 0 0;padding-left:1.4em">
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-01-how-to-score/">第1篇 · 拿奖逻辑：把比赛拆成小目标</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-02-history/">第2篇 · 赛题进化史与押题</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-03-build-and-architecture/">第3篇 · 整车搭建与代码框架</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-04-motor-power/">第4篇 · 电机驱动与电源地基</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-05-sensing/">第5篇 · 感知：灰度/电磁/编码器/IMU</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-06-pid-basics/">第6篇 · PID 入门：搞懂 P/I/D</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-07-pid-advanced/">第7篇 · PID 进阶：串级+工程补丁</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-08-pid-tuning/">第8篇 · PID 调参实战(核心)</a></li>
<li style="margin:.15em 0"><strong>第9篇 · 进阶控制：几时该上（本篇）</strong></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-10-vision-comm/">第10篇 · K230 视觉与通信协议</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-11-architecture-fsm/">第11篇 · 状态机与整车软件</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-12-field-manual/">第12篇 · 现场作战+避坑+开源</a></li>
</ol>
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://cloudlay.cn/nuedc-car-09-advanced-control/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>从0到1带你打电赛·小车电控篇(八)：PID 调参实战——一套能照抄的调参流程 + 看波形治百病</title>
		<link>https://cloudlay.cn/nuedc-car-08-pid-tuning/</link>
					<comments>https://cloudlay.cn/nuedc-car-08-pid-tuning/#respond</comments>
		
		<dc:creator><![CDATA[云间辞]]></dc:creator>
		<pubDate>Sun, 14 Jun 2026 17:08:33 +0000</pubDate>
				<category><![CDATA[嵌入式]]></category>
		<category><![CDATA[PID调参]]></category>
		<category><![CDATA[VOFA]]></category>
		<category><![CDATA[Ziegler-Nichols]]></category>
		<category><![CDATA[智能小车]]></category>
		<category><![CDATA[波形诊断]]></category>
		<category><![CDATA[电赛]]></category>
		<guid isPermaLink="false">https://cloudlay.cn/nuedc-car-08-pid-tuning/</guid>

					<description><![CDATA[📚 本文是 「从 0 到 1 带你打电赛 · 小车电控篇」 系列（共 12 篇）第 8 篇。 第1篇 · 拿奖 [&#8230;]]]></description>
										<content:encoded><![CDATA[<div class="ds-series" style="border:1px solid #4488ff33;background:#4488ff0d;border-radius:8px;padding:.8em 1.1em;margin:1.2em 0">
<p style="margin:0 0 .5em"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4da.png" alt="📚" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 本文是 <strong>「从 0 到 1 带你打电赛 · 小车电控篇」</strong> 系列（共 12 篇）第 8 篇。</p>
<ol style="margin:.2em 0 0;padding-left:1.4em">
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-01-how-to-score/">第1篇 · 拿奖逻辑：把比赛拆成小目标</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-02-history/">第2篇 · 赛题进化史与押题</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-03-build-and-architecture/">第3篇 · 整车搭建与代码框架</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-04-motor-power/">第4篇 · 电机驱动与电源地基</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-05-sensing/">第5篇 · 感知：灰度/电磁/编码器/IMU</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-06-pid-basics/">第6篇 · PID 入门：搞懂 P/I/D</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-07-pid-advanced/">第7篇 · PID 进阶：串级+工程补丁</a></li>
<li style="margin:.15em 0"><strong>第8篇 · PID 调参实战(核心)（本篇）</strong></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-09-advanced-control/">第9篇 · 进阶控制：几时该上</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-10-vision-comm/">第10篇 · K230 视觉与通信协议</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-11-architecture-fsm/">第11篇 · 状态机与整车软件</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-12-field-manual/">第12篇 · 现场作战+避坑+开源</a></li>
</ol>
</div>
<p>到这里，前面几篇我们已经把单环、串级、还有那一堆&#8221;不做就翻车&#8221;的工程补丁都讲过了。但你大概率会有个困惑：道理我都懂，可参数到底怎么调出来？P 设多少？I 加到哪算够？车在地上画龙的时候，我该动哪个数？</p>
<p>这一篇就是来解决&#8221;动手&#8221;这件事的。它是整个系列里最该被你打印出来贴在调试桌上的一篇——一套从简单到难、可以照抄的调参流程，外加一张&#8221;看波形治百病&#8221;的诊断表。调参这事说穿了不玄学，它有固定套路，也有趁手工具。我们一步步来。</p>
<h2>先把心态摆正：调参是&#8221;有顺序地试出来&#8221;，不是&#8221;算出来&#8221;</h2>
<p>很多新手一上来就想找一组&#8221;标准参数&#8221;抄进去，结果发现别人的 Kp=500 抄到自己车上直接飞出去。</p>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 第一条铁律：别人的 PID 数值不能直接抄</p>
<p>Kp/Ki/Kd 的具体数值，强依赖你的车模、编码器线数、减速比、供电电压、控制周期，还有&#8221;偏差用什么单位算的&#8221;。同样是方向环，偏差用像素算 Kp 可能要 500~900，偏差归一化到小范围 Kp 可能就是个位数——能差好几个数量级。所以网上任何参数（包括本篇后面给的表）都只能当<strong>起点量级</strong>，必须在你自己的车上现场重调。</p>
</div>
<p>那调参靠什么？靠一套固定的顺序，配上眼睛看波形。这就像考驾照练科目二：不是背一个&#8221;方向盘打几圈&#8221;的万能数字，而是练出&#8221;看到库角到哪个位置就开始回方向&#8221;的手感。手感从哪来？从一遍遍试、一遍遍看结果来。我们要做的，就是把&#8221;试&#8221;这件事变得有章法、不瞎试。</p>
<p>整套流程可以浓缩成一句口诀：<strong>先内环后外环，先 P 后 I 后 D，参数从小往大加。</strong> 下面拆开讲。</p>
<h2>调参总流程：四步走，照着做</h2>
<figure class="ds-diagram" style="text-align:center;margin:1.3em 0"><img decoding="async" src="https://cloudlay.cn/wp-content/uploads/dianseiche/8ac2a75741c5.png" alt="电赛小车电控 · 示意图" loading="lazy" style="max-width:100%;height:auto;background:#fff;border-radius:8px;padding:6px;box-shadow:0 1px 6px rgba(0,0,0,.25)"></figure>
<h3>第 0 步：先验机械和极性，别让控制环背锅</h3>
<p>这一步最容易被跳过，但跳过它你后面会调到怀疑人生。</p>
<p>很多&#8221;PID 怎么调都不对&#8221;的情况，根子根本不在参数上，而在机械和反馈信号本身就是错的：编码器联轴器松了导致测速跳变、电机没固定牢一加速就晃、舵机中值没标定导致你以为在走直线其实车在跑偏。还有一种很坑的情况——低速时一切正常，一提速或者一换地面，机械松动才暴露出来。所以宁可一开始就把它排查干净。</p>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 这就像配眼镜前先验光</p>
<p>微分项怕噪声，就像近视眼盯着一行抖动的字看——传感器一抖，D 项就以为出大事了猛踩刹车。你得先把&#8221;眼镜擦干净&#8221;（机械稳、信号干净），再让 PID 上岗。</p>
</div>
<p>还有一个必做动作：<strong>确认反馈极性</strong>。方法很土但极其有效——用手去转车（或拨动轮子），如果你给的是正 Kp，车应该&#8221;反抗&#8221;你这个动作（这才是负反馈）；如果车顺着你的手越转越欢，说明极性反了，那就是正反馈，再怎么调参数车都会原地打转或直接失控。极性反了，把符号取反，或者把电机两根线对调即可。</p>
<div class="ds-callout ds-callout-danger" style="border-left:4px solid #ff1744;background:#ff174414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff1744"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 极性接反 = 正反馈 = 必然失控</p>
<p>这是新手最隐蔽的坑。换了电机接线、或者改了陀螺仪的安装方向之后，一定要重新验一遍极性。极性不对，参数调得再漂亮都没用。</p>
</div>
<p>舵机车还要先标定中值、左右极限并限幅（脉宽夹在安全范围内）。标准 RC 舵机周期 20ms（50Hz），脉宽常用范围约 0.5~2.5ms，1.5ms 附近是中位。机械没装好、舵机没标好，PID 再准也救不回来。</p>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 闭环之前，先把开环标定做完</p>
<p>在套闭环之前，先开环测一遍：电机的中值和死区在哪、占空比和转速大致是什么关系、左右两个电机是不是不一致。这些零点附近的特性如果没摸清，闭环时车会在零点附近来回跳变，看起来像 PID 没调好，其实是机械/标定的锅。左右电机往往不完全一样，最好左右各一套速度环参数分别整定。</p>
</div>
<h3>第 1 步：先单独调好速度内环</h3>
<p>串级系统里，<strong>永远先调内环</strong>。原因很直白：外环（方向/位置）看到的&#8221;被控对象&#8221;其实是&#8221;内环 + 电机&#8221;这个整体。如果内环本身就晃晃悠悠不稳定，你在它外面套外环，调出来的参数换个工况就废了。</p>
<div class="ds-callout ds-callout-example" style="border-left:4px solid #7c4dff;background:#7c4dff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#7c4dff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 串级双环像公司管理</p>
<p>外环（方向/位置）是老板，定目标&#8221;到那个位置去&#8221;；内环（速度/电流）是员工，负责执行&#8221;踩多大油门&#8221;。老板不能管太细，得先让员工把执行这件事做稳——所以先调内环（员工），再调外环（老板）。员工还毛手毛脚的时候，老板定再清晰的目标也落不了地。</p>
</div>
<p>具体怎么调（速度环一般用<strong>增量式 PID</strong>）：</p>
<ol>
<li>把车架空或低速直线跑，给一个固定的目标速度。</li>
<li><strong>先给一个很小的 I</strong>，让车能慢慢爬到目标速度（增量式里乘当前误差 $e$ 的那个系数承担&#8221;积分&#8221;角色，它负责消除稳态转速误差）。</li>
<li><strong>再缓慢加 P</strong>，让响应变快——目标速度一变，实际速度要快速跟上去。</li>
<li>P 一直加到编码器波形开始出现小幅高频抖动，就<strong>回退一点点</strong>。</li>
<li>D 一般很小或者干脆不加（编码器测速本身就有噪声，D 会把它放大成抖动）。</li>
</ol>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 增量式的系数不能照搬位置式</p>
<p>位置式是 $u = K_p e + K_i \sum e + K_d \Delta e$，增量式是 $\Delta u = K_p \Delta e + K_i e + K_d \Delta^2 e$。两者里 $K_i$、$K_d$ 的<strong>量纲完全不同</strong>：增量式里乘 $e$ 的那个系数对应的是连续 $K_i$ 再乘上采样周期，跟位置式那个直接累加的 $K_i$ 不是一回事，数值能差好几个数量级。很多博客把两种式子的系数混着贴，你照抄必崩。换算要么自己推，要么干脆重新整定。</p>
</div>
<p>增量式速度环只需要做<strong>输出限幅</strong>（不像位置式还得专门做积分限幅），因为它本身不显式累加积分，天然抗积分饱和、误动作影响小——算错一次只是微微抖一下，而不是猛地一窜。</p>
<div class="ds-callout ds-callout-example" style="border-left:4px solid #7c4dff;background:#7c4dff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#7c4dff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 增量式 vs 位置式像踩油门</p>
<p>位置式是每次直接报&#8221;油门踩到几成&#8221;（绝对量），算错一次车就猛地一冲；增量式是每次说&#8221;油门多给一点 / 少给一点&#8221;（增量），算错一次只是轻轻一颤，更安全。这就是电机速度环爱用增量式的原因。</p>
</div>
<h3>第 2 步：再调方向 / 位置外环</h3>
<p>内环稳了，外环此时看到的就是一个&#8221;快速、稳定&#8221;的等效环节，可以像调普通单环 PID 那样慢慢来。</p>
<p>方向环一般用<strong>位置式 PD</strong>（多数情况不加 I）：</p>
<ol>
<li>从 D=0、只加 P 开始。P 慢慢加大到车能基本跟住线 / 跟住目标方向，但过弯时略冲、直道有点画龙。</li>
<li>再加 D，压住过冲和直道的蛇形抖动。</li>
</ol>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 方向环口诀：入弯靠 D，弯中靠 P</p>
<p>入弯的瞬间，偏差变化率很大，主要靠 D 项产生&#8221;预判性打角&#8221;，提前把方向打出去；车进入弯道中段后偏差基本不变，D 项失效，由 P 项维持住位置。所以 P 决定&#8221;跟得准不准&#8221;，D 决定&#8221;打得圆不圆滑、抖不抖&#8221;。</p>
</div>
<p>关于方向环要不要加 I，这里要说句更准确的话：</p>
<div class="ds-callout ds-callout-note" style="border-left:4px solid #448aff;background:#448aff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#448aff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4dd.png" alt="📝" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 方向环以 PD 为主，但&quot;绝不加 I&quot;说得太满</p>
<p>高速动态循迹时，I 项容易引起入弯滞后、过弯后画龙摆尾，所以常常弱化或省略。但如果你的车存在固定的机械/装配偏置（比如直道上总是恒定地偏一点点），加一个<strong>很小、且带积分分离和限幅</strong>的 I 来纠正这个系统性偏差，反而是有道理的——自动驾驶里的转向 PID 也常用一个小 I 去吃掉这种 steering drift。结论：以 PD 为主导，I 看场景按需加一点点，别一概而论。</p>
</div>
<h3>第 3 步：提速，然后到赛场复调</h3>
<p>低速时系统更宽容、容易看清振荡，所以前面都是先慢速调通。调稳之后再逐步把基础速度提上去。</p>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 提速后参数往往要回调</p>
<p>速度和转向是耦合的：提速后原来的方向环参数常常偏大，需要回调；高速过弯还得配合弯道减速策略（按曲率或偏差大小压低基础速度）。这也是为什么很多队伍会准备<strong>两套甚至三套</strong>参数：直线一套（快）、弯道一套（稳），现场用按键 + OLED 切换。</p>
</div>
<p>实战里就有人这么干：某 2024 电赛 H 题循迹分享里，直线用 Kp=500 / Kd=0，弯道单独用 Kp=900 / Kd=200，两套动态参数切换跑（<a href="https://blog.csdn.net/AzureMeadow65/article/details/154388185" target="_blank" rel="noopener noreferrer">来源</a>）。</p>
<div class="ds-callout ds-callout-danger" style="border-left:4px solid #ff1744;background:#ff174414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff1744"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 实验室调好 ≠ 赛场能跑</p>
<p>赛场的光照、地面摩擦、电池电量都和你实验室不一样。灰度/电感的阈值受环境光影响极大，PID 也会跟着变。一定要赶到现场，用赛道真实材质和光照重新标定阈值、复调参数，并且留大约 20% 的参数余量。满电调好的车，亏电时同一个 PWM 转速会掉，赛前务必在&#8221;比赛会用到的电量水平&#8221;下再复测一遍。</p>
</div>
<h2>从临界振荡起步：试凑法标准步骤</h2>
<p>上面是&#8221;流程顺序&#8221;，那每一个环具体怎么把 P 加上去？这里有个经典的标准动作。</p>
<p><strong>试凑法（先 P 后 I 后 D）：</strong></p>
<ol>
<li>把 I、D 都去掉（只留 P）。</li>
<li>把 Kp 从小往大加，一直加到系统出现<strong>等幅振荡</strong>（不收敛也不发散，匀速来回晃）。</li>
<li>把 Kp 减小到振荡刚好消失，再取这个值的 <strong>60%~70%</strong>（或者干脆乘 0.6）作为最终 P。</li>
<li>需要消静差就加 I：积分时间 Ti 先设一个大初值，逐步往小减，减到出现振荡，再回头把它加大到 <strong>150%~180%</strong>。</li>
<li>需要压超调就加 D：Td 一般取 0，或者取不振荡时的约 <strong>30%</strong>。</li>
</ol>
<div class="ds-callout ds-callout-example" style="border-left:4px solid #7c4dff;background:#7c4dff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#7c4dff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> &quot;先 P 到振荡再退一点&quot;像调水龙头水温</p>
<p>先把热水开到烫手（临界振荡点），确认这是上限，再往回拧一点点到刚好舒服（退到 60%~70%）。临界点是稳定边界，退一档是为了在&#8221;反应快&#8221;和&#8221;不失稳&#8221;之间留余量。</p>
</div>
<div class="ds-callout ds-callout-danger" style="border-left:4px solid #ff1744;background:#ff174414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff1744"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 找&quot;等幅振荡&quot;时小心车冲出去</p>
<p>把 Kp 往上加到振荡的过程中，车随时可能失控冲出赛道甚至翻车。建议先低速、把轮子架空、或者用手扶着测。另外电机有死区——Kp 太小时电机可能压根不转，表现为&#8221;调不动&#8221;，这时要先克服死区（做死区补偿）。</p>
</div>
<h2>想要个起步估计：Ziegler-Nichols 临界比例度法</h2>
<p>试凑法靠手感，如果你想要一个能&#8221;算出起点&#8221;的方法，可以用经典的 Ziegler-Nichols（临界比例度法）。</p>
<p>做法：把 I、D 去掉，逐渐加大 Kp 到系统首次出现等幅振荡，记下此时的<strong>临界增益 $K_u$</strong> 和<strong>临界振荡周期 $T_u$</strong>，然后查表。</p>
<p>标准版（最常用，会产生约 25% 超调、四分之一衰减）：</p>
<p class="ds-math" style="overflow-x:auto">$$K_p = 0.6 K_u, \quad T_i = 0.5 T_u, \quad T_d = 0.125 T_u$$</p>
<p>换算成常见的并联形式就是 $K_i = 1.2 K_u / T_u$、$K_d = 0.075 K_u T_u$。</p>
<p>野火文档里给的查表版本（数值上和标准版接近）：</p>
<table>
<thead>
<tr>
<th>控制器</th>
<th>Kp</th>
<th>Ki / Kd</th>
</tr>
</thead>
<tbody>
<tr>
<td>仅 P</td>
<td>$K_u/2$</td>
<td>—</td>
</tr>
<tr>
<td>PI</td>
<td>$K_u/2.2$</td>
<td>$K_i = K_p/(0.833\,T_u)$</td>
</tr>
<tr>
<td>PID</td>
<td>$K_u/1.7$</td>
<td>$K_i = K_p/(0.5\,T_u)$，$K_d = 0.125\,T_u\,K_p$</td>
</tr>
</tbody>
</table>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 几个容易被网上资料带歪的点，务必记清楚</p>
<ul>
<li>$K_p = 0.45 K_u$ 是 <strong>PI 控制器</strong>的 Kp，<strong>不是</strong>什么&#8221;PID 降超调版&#8221;。真正的 PID 低超调变体是 $K_p = 0.2 K_u$、$T_d = 0.333 T_u$（no overshoot），或 $K_p = 0.333 K_u$（some overshoot）。</li>
<li>网上有种说法是&#8221;PI 段把 Kp 设成临界 Kp 的 5/6&#8243;，<strong>这是错的</strong>。5/6 指的是 PI 的积分时间 $T_i \approx 0.833\,T_u$（临界周期的 5/6），不是比例系数。相对 PID 的 Kp，PI 的 Kp 其实是 $0.45/0.6 = 3/4$。别把积分时间的系数张冠李戴安到 Kp 上。</li>
<li>Z-N 整定结果普遍偏激进（约 25% 超调）。实战常需要在它给的起点上再保守化；要小超调就直接用上面的 no-overshoot 变体。对那种滞后大、特别容易振荡的对象，还可以换成更稳健的 Tyreus-Luyben 整定（$K_p = K_u/3.2$、$T_i = 2.2\,T_u$、$T_d = T_u/6.3$）。</li>
</ul>
</div>
<h2>重头戏：看波形治百病</h2>
<p>如果说前面是&#8221;流程&#8221;，那这一节是这一篇的灵魂。<strong>你能不能调好 PID，很大程度取决于你能不能把车的状态&#8221;看见&#8221;。</strong> 盲调和看着波形调，效率差一个数量级。</p>
<h3>工具：VOFA+ 串口示波器</h3>
<p>最主流、免费的工具是 <strong>VOFA+</strong>。思路非常简单：在控制环里用 printf 同时发出&#8221;目标值&#8221;和&#8221;实际值&#8221;两路数据，VOFA+ 把字节流翻译成实时曲线画出来。你一眼就能看到实际值是怎么去追目标值的——是慢慢爬上去、还是冲过头来回荡、还是死活差一截。需要的话还能多发几路（比如 PID 的分项、PWM 输出）一起看。</p>
<p>VOFA+ 有三种数据协议，按你的通道数和频率选：</p>
<table>
<thead>
<tr>
<th>协议</th>
<th>格式</th>
<th>适用</th>
<th>注意</th>
</tr>
</thead>
<tbody>
<tr>
<td>FireWater</td>
<td>逗号分隔的文本 + 换行</td>
<td>通道少、频率低（3~4 路、&lt;100Hz）</td>
<td><strong>末尾必须有换行</strong>，否则不解析、画不出波形</td>
</tr>
<tr>
<td>JustFloat</td>
<td>小端 float 数组 + 帧尾</td>
<td>多通道高频首选</td>
<td>帧尾固定 <code>0x00 0x00 0x80 0x7F</code>；接收区<strong>必须勾&#8221;十六进制&#8221;</strong>；缺帧尾会卡死</td>
</tr>
<tr>
<td>RawData</td>
<td>原始字节按通道切</td>
<td>特殊场景</td>
<td>—</td>
</tr>
</tbody>
</table>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 通道多、频率高就用 JustFloat</p>
<p>JustFloat 那个帧尾 <code>0x00 0x00 0x80 0x7F</code> 其实就是单精度浮点的正无穷（+inf），正常数据不会出现这个值，所以拿它当帧的分隔标志最稳。波特率常用 115200，要发很多通道高频数据时上 460800 / 921600。注意 51 单片机的 float 是大端，用 JustFloat 要先翻转字节序。</p>
<p>带宽够不够可以粗算一下：JustFloat 每通道每帧 4 字节，加 4 字节帧尾，n 通道、f Hz 大约需要 $(4n+4)\cdot f$ 字节/秒。比如 6 通道、1kHz 约 28 KB/s，而 115200 波特（8N1）只有约 11.5 KB/s，根本不够——这时要么提波特率，要么降通道、降回传频率。</p>
</div>
<h3>诊断表：波形长什么样 → 该动哪个参数</h3>
<p>这是最可操作的产出，建议直接背下来：</p>
<table>
<thead>
<tr>
<th>波形症状</th>
<th>病因</th>
<th>处方</th>
</tr>
</thead>
<tbody>
<tr>
<td>实际值上得很慢，半天追不上目标</td>
<td>响应不够</td>
<td><strong>加大 P</strong>（或加 I 作用）</td>
</tr>
<tr>
<td>频繁来回振荡、等幅晃</td>
<td>P 太大</td>
<td><strong>减小 P</strong></td>
</tr>
<tr>
<td>一下子冲过头、超调大</td>
<td>阻尼不够</td>
<td><strong>加 D</strong>（或减小 P/I）</td>
</tr>
<tr>
<td>稳定后总差一截、贴不到目标线</td>
<td>静差</td>
<td><strong>加 I</strong></td>
</tr>
<tr>
<td>波形发散、越晃越大</td>
<td>P 严重过大</td>
<td><strong>立刻减小 P</strong></td>
</tr>
<tr>
<td>加了 I 之后新出现超调</td>
<td>I 太猛</td>
<td>加 D 或减 I</td>
</tr>
<tr>
<td>高频毛刺、整体其实跟得上</td>
<td>噪声 / D 太大</td>
<td><strong>减 D 或加滤波</strong>（别误当振荡去减 P！）</td>
</tr>
</tbody>
</table>
<p>一句口诀帮你记住分工：<strong>P 主管响应、I 消除静差、D 抑制振荡。</strong></p>
<p>理想的响应曲线长什么样？老一辈工控人有个判据叫&#8221;<strong>两个波，前高后低四比一</strong>&#8220;——也就是超调后回落，相邻两个波峰幅值约 4:1 衰减比，既反应快又稳得住。</p>
<div class="ds-callout ds-callout-example" style="border-left:4px solid #7c4dff;background:#7c4dff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#7c4dff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> D 项怕噪声，像近视眼盯着抖动的字</p>
<p>表里那条&#8221;高频毛刺别误当振荡&#8221;特别重要。微分看的是&#8221;变化快不快&#8221;，传感器信号稍微一抖，D 就以为出大事了猛打方向。所以遇到高频抖，第一反应应该是<strong>减 D 或给反馈加低通滤波</strong>，而不是去减 P。这也是为什么工程上常用&#8221;不完全微分&#8221;（给 D 串一个一阶低通）和&#8221;微分先行&#8221;（只对反馈测量值求导、不对误差求导，避免目标突变时产生微分冲击）。具体这些补丁怎么写，《PID 进阶：串级+工程补丁》一篇里都给过代码，这里就不重复了。</p>
</div>
<p>下面这张图把整个看波形的判断逻辑串起来：</p>
<figure class="ds-diagram" style="text-align:center;margin:1.3em 0"><img decoding="async" src="https://cloudlay.cn/wp-content/uploads/dianseiche/d29647dc4dc5.png" alt="电赛小车电控 · 示意图" loading="lazy" style="max-width:100%;height:auto;background:#fff;border-radius:8px;padding:6px;box-shadow:0 1px 6px rgba(0,0,0,.25)"></figure>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 有一类&quot;调不好&quot;，加大 D 也救不回</p>
<p>如果你的反馈本身有明显延迟——比如图像处理耗时、传感器采集到 PID 执行之间隔了很久——这相当于给系统加了纯滞后，会让本来稳定的 Kp 突然开始振荡。这种问题加 D 没用，得从源头缩短&#8221;采集→计算→执行&#8221;这条链路，或者上更高级的预估补偿。所以波形怎么调都不对的时候，回头看看是不是时序出了问题。</p>
</div>
<h2>在线调参：别每改一个数就重新烧录</h2>
<p>调参是个反复试的活。如果每改一次参数都要重新编译、烧录、复位、再跑，一天调不了几轮。高名次队伍的共同点之一，就是把&#8221;改参数&#8221;这件事做成在线的。三种手段：</p>
<p><strong>1. 板载按键 + OLED（现场最稳，比赛桌上首选）</strong></p>
<p>把 P/I/D 存成数组，用按键选择当前要改哪个参数、加减键步进调整，OLED 实时显示。不用连电脑、不用重新下载、抗 PC 干扰。整定满意后把参数写进 Flash 掉电保存。</p>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 比赛现场优先用按键 + OLED</p>
<p>赛场电磁环境复杂、桌子上一堆人挤，连着电脑反而是累赘。一块板载 OLED + 几个按键，独立、稳、快，是封箱前最后整定的标配。记得改完一定要存进 Flash，否则一断电参数全丢，现场重调哭都来不及。</p>
</div>
<p><strong>2. 蓝牙 / 2.4G 无线透传（跑动中调参）</strong></p>
<p>平衡车、高速车这种&#8221;必须跑起来才能看出参数好坏&#8221;的场景，用 HC-05 蓝牙（或 NRF24L01 这类 2.4G）做无线串口透传。在 VOFA+ 或手机上发命令帧改参数，车继续跑，同时把波形实时回传回来。</p>
<p>下位机解析命令帧的通用做法：定好帧头帧尾，用 DMA + 串口空闲（IDLE）中断收变长包，收到后用 <code>strstr</code> 定位关键字、<code>atof</code> 把字符串转成 float。</p>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 无线调参必须同时回传波形，否则等于盲调</p>
<p>无线的全部价值在于&#8221;车在跑，你在看&#8221;。如果只发参数下去、不把波形传回来，你根本不知道改完变好还是变坏，那无线就白用了。还有，蓝牙带宽有限，多通道高频回传会丢包——这时候降通道数或者把回传频率降到 50~100Hz。</p>
</div>
<p><strong>3. MATLAB 系统辨识（算个起点）</strong></p>
<p>进阶玩法：开环给电机一个恒定 PWM，记录转速响应，拟合出传递函数，再用 PID Tuner 算出一组起步参数。拟合率（fit）大于 80% 基本就能用。注意 PID Tuner 算出的连续 I 项要乘以采样周期才是离散位置式的 Ki。这套适合给速度环找起点，但最终还是得回到车上微调。</p>
<h2>一个能照抄的 PID 实现（含输出限幅）</h2>
<p>理论说够了，给一份电赛 H 题国奖工程里用过的通用 PID 实现——同一个结构体支持位置式和增量式两种模式，并且带上必做的输出限幅（<a href="https://github.com/abcuer/2024-NUEDC-H-TI_CAR" target="_blank" rel="noopener noreferrer">来源</a>）：</p>
<pre><code class="language-c">// 通用 PID：一个结构体两种模式（来自 2024 电赛 H 题开源工程）
void PID_Calculate(PID_Struct *pid){
    pid-&gt;error[0] = pid-&gt;target - pid-&gt;now;   // 当前偏差
    if(pid-&gt;mode == DELTA_PID){               // 增量式（电机速度环常用）
        pid-&gt;pout = pid-&gt;p * (pid-&gt;error[0] - pid-&gt;error[1]);
        pid-&gt;iout = pid-&gt;i *  pid-&gt;error[0];
        pid-&gt;dout = pid-&gt;d * (pid-&gt;error[0] - 2*pid-&gt;error[1] + pid-&gt;error[2]);
        pid-&gt;out += pid-&gt;pout + pid-&gt;iout + pid-&gt;dout;   // 注意是累加增量
    } else if(pid-&gt;mode == POSITION_PID){     // 位置式（方向/角度环常用）
        pid-&gt;pout = pid-&gt;p *  pid-&gt;error[0];
        pid-&gt;iout = pid-&gt;i *  pid-&gt;error[0];
        pid-&gt;dout = pid-&gt;d * (pid-&gt;error[0] - pid-&gt;error[1]);
        pid-&gt;out  = pid-&gt;pout + pid-&gt;iout + pid-&gt;dout;    // 直接赋值
    }
    pid-&gt;error[2] = pid-&gt;error[1];            // 历史偏差移位
    pid-&gt;error[1] = pid-&gt;error[0];
}

// 输出限幅：位置式/增量式都必须做，否则容易烧电机或舵机
void PID_OutPutLimit(PID_Struct *pid, float duty){
    if(pid-&gt;out &gt;=  duty) pid-&gt;out =  duty;
    if(pid-&gt;out &lt;= -duty) pid-&gt;out = -duty;
}</code></pre>
<div class="ds-callout ds-callout-danger" style="border-left:4px solid #ff1744;background:#ff174414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff1744"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 限幅是必做项，不是可选项</p>
<p>位置式还要额外做积分限幅和积分分离（偏差超过某个阈值时干脆不累加 I），防止积分饱和。积分饱和是什么概念——上电瞬间或大偏差时误差很大，I 项会迅速攒到饱和，等误差反向了它还在用力过猛，结果就是巨大超调甚至冲出赛道。这就像憋了一肚子气：你越攒越气，等对方道歉了你还在继续发飙，所以得&#8221;积分分离&#8221;——大事化小前先别记仇。这些工程补丁的完整写法，前一篇《PID 进阶：串级+工程补丁》里成套讲过。</p>
</div>
<p>循迹车把方向环输出叠加到左右轮基础速度上，实现差速转向：</p>
<pre><code class="language-c">// 七路灰度加权求中线偏差，target=0（车正好压在中线）
track_pid.now    = weighted_sum / sum;
track_pid.target = 0;
PID_Calculate(&amp;track_pid);
PID_OutPutLimit(&amp;track_pid, 800);          // 方向环输出限幅 ±800
Motor_LeftCtrl (basespeed - track_pid.out); // 左轮 = 基础速度 - 转向量
Motor_RightCtrl(basespeed + track_pid.out); // 右轮 = 基础速度 + 转向量</code></pre>
<p>差速运动学的本质就是这两行：左右轮一个减一个加，差出来的速度差就是转向。对应公式 $v_L = v &#8211; \frac{\omega L}{2}$、$v_R = v + \frac{\omega L}{2}$（$v$ 线速度、$\omega$ 角速度、$L$ 轮距）。左右到底是加还是减，取决于你的电机安装方向，跑一下不对就互换。</p>
<h2>各场景 PID 经验起点数值表</h2>
<p>再强调一遍：<strong>下面全是起点量级，不是答案，必须现场重调。</strong> 给它们是为了让你不至于从零开始瞎蒙，知道&#8221;大概在哪个数量级&#8221;。</p>
<table>
<thead>
<tr>
<th>场景</th>
<th>控制方式</th>
<th>参考起点（仅量级）</th>
<th>备注</th>
</tr>
</thead>
<tbody>
<tr>
<td>电机速度环</td>
<td>增量式</td>
<td>Kp ≈ 0.5~6，Ki ≈ 0.008~8.5，Kd≈0</td>
<td>增量式 Ki 与位置式量纲不同</td>
</tr>
<tr>
<td>方向环（偏差归一化）</td>
<td>位置式 PD</td>
<td>Kp ≈ 7~8，Kd ≈ -0.1</td>
<td>H 题角度环量级</td>
</tr>
<tr>
<td>方向环（偏差用像素/电感差）</td>
<td>位置式 PD</td>
<td>直线 Kp≈500，弯道 Kp≈900/Kd≈200</td>
<td>偏差量纲大故 Kp 大</td>
</tr>
<tr>
<td>送药车速度环（串级位置式）</td>
<td>位置式</td>
<td>Kp=6.0，Ki=8.5，Kd=0</td>
<td>car_template 实测</td>
</tr>
<tr>
<td>送药车位置外环</td>
<td>位置式</td>
<td>Kp=0.3，Ki=0，Kd=0</td>
<td>car_template 实测</td>
</tr>
<tr>
<td>控制 / 采样周期</td>
<td>—</td>
<td>速度内环 1~10ms（100~1000Hz）</td>
<td>内环要快于外环</td>
</tr>
<tr>
<td>电机 PWM 频率</td>
<td>—</td>
<td>17~24kHz</td>
<td>见下方说明</td>
</tr>
<tr>
<td>舵机 PWM</td>
<td>—</td>
<td>50Hz / 周期 20ms，脉宽 0.5~2.5ms</td>
<td>1.5ms 附近中位</td>
</tr>
</tbody>
</table>
<p>关于控制周期和 PWM 频率，初学者最容易混成一件事，这里务必分清：</p>
<div class="ds-callout ds-callout-important" style="border-left:4px solid #00bcd4;background:#00bcd414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bcd4"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2757.png" alt="❗" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 控制周期 ≠ PWM 频率，两者差几十上百倍</p>
<ul>
<li><strong>PWM 频率</strong>（电机驱动的载波）：决定电机转得平不平滑、吵不吵。</li>
<li><strong>PID 控制周期</strong>（定时器中断里跑一次 PID 的间隔）：通常 1~10ms，也就是 100~1000Hz。2024 电赛 H 题很多队伍就是 10ms 定时器中断里跑整套 PID。</li>
</ul>
<p>串级时内环周期要明显小于外环（量级上内环带宽是外环的 3~10 倍），代码上用一个计数器实现：内环每次中断都算，外环每隔 N 次中断才算一次。</p>
</div>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 内外环分频，别忘了&quot;周期&quot;也跟着变</p>
<p>外环每 N 次中断才算一次，那它 PID 公式里的积分/微分用的就该是外环的真实周期 $N\cdot T$，而不是基本中断周期 $T$。很多人这里偷懒直接用 T，结果 Ki/Kd 的物理意义被分频系数悄悄改掉，调出来的参数换个分频就不对——这是很隐蔽的 bug。</p>
</div>
<p>关于 PWM 频率到底取多少，有个常被网上资料说错的点：</p>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> &quot;10kHz 就能避开啸叫&quot;是不准确的</p>
<p>人耳上限大约 20kHz，所以 <strong>10kHz、17kHz 其实大多数人还是听得到的</strong>（尤其年轻人，10kHz 会是明显的尖啸）。要真正做到&#8221;超声静音&#8221;，PWM 要提到 <strong>20kHz 以上</strong>。代价是开关损耗增大、对驱动 MOS 要求更高，而且在固定定时器时钟下频率越高、占空比分辨率越低。所以 17~24kHz 是&#8221;避人耳&#8221;和&#8221;保住分辨率/控制损耗&#8221;之间的折中，24kHz 更稳妥。10kHz 常被选用主要是图省开关损耗，不是因为它真的安静。</p>
</div>
<h2>串级调参顺序与现场快速整定</h2>
<p>把前面的拼起来，串级双环的完整调参顺序是：</p>
<ul>
<li>☐ <strong>第 0 步</strong>：验机械、验编码器、验极性，舵机标中值限幅，开环标定好中值/死区。</li>
<li>☐ <strong>第 1 步</strong>：屏蔽外环，单独调速度内环（增量式）。给固定目标速度，调到&#8221;目标一变实际能快速平滑跟上、受扰不振荡&#8221;。</li>
<li>☐ <strong>第 2 步</strong>：套上方向/位置外环（位置式 PD）。先加 P 到能跟线，再加 D 压超调，方向环一般不加或只加极小 I。</li>
<li>☐ <strong>第 3 步</strong>：逐步提速，弯道单独降速；准备 2~3 套参数现场切换。</li>
<li>☐ <strong>现场</strong>：到赛道真实材质/光照下重标阈值、复调参数，留 20% 余量，在比赛电量水平下复测。</li>
</ul>
<div class="ds-callout ds-callout-success" style="border-left:4px solid #00c853;background:#00c85314;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00c853"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 现场快速整定的三个救命动作</p>
<ol>
<li>先用按键 + OLED 把当前波形/偏差显示出来，确认信号正常。</li>
<li>速度先压一半，把车调到&#8221;稳&#8221;，确认不冲出、不画龙，再慢慢提速度找&#8221;快&#8221;。</li>
<li>任何一次改参后，立刻让车在赛道上跑一遍看实际表现，并把满意的参数存进 Flash。封箱前最后一次整定，一定在比赛要用的那块电池电量下做。</li>
</ol>
</div>
<h2>几个反复要人命的坑，最后再钉一遍</h2>
<div class="ds-callout ds-callout-danger" style="border-left:4px solid #ff1744;background:#ff174414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff1744"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 调参翻车排行榜</p>
<ul>
<li><strong>忘记限幅</strong>：位置式/增量式都要做输出限幅；位置式还要积分限幅 + 积分分离。不做会积分饱和、超调冲出，甚至烧电机舵机。</li>
<li><strong>照抄别人数值</strong>：换车、换编码器、换电压、换控制周期，参数全得重调。</li>
<li><strong>改了控制周期忘重调</strong>：多数代码把采样周期 T 合进了系数里，周期一变等效增益全变。更稳的做法是把控制环放在硬件定时器中断里保证 T 恒定，或者在公式里用实测 dt 动态算积分/微分。</li>
<li><strong>增量式系数当位置式用</strong>：量纲不同，照抄差几个数量级。</li>
<li><strong>printf 阻塞主循环</strong>：高频发波形如果用阻塞式 <code>HAL_UART_Transmit</code>，发上千字节要几十毫秒，会把控制周期搅乱、车直接抖到失控。必须改 DMA + 环形缓冲非阻塞发送。</li>
<li><strong>低速测速噪声大</strong>：编码器用&#8221;单位时间计数&#8221;测速时，低速下每周期脉冲数少、量化噪声大，D 一放大就抖。这种情况低速宜改用&#8221;测脉冲间隔&#8221;的算法，并对速度反馈先滤波再进 PID，比一味减小 Kd 更治本。</li>
<li><strong>MPU6050 零飘</strong>：上电要静置校准零偏。注意一个常见误传——MPU6050 的 Yaw 漂移并没有&#8221;每秒 2 度&#8221;那么夸张，做过一次性零偏校准后裸积分大约是<strong>每分钟几度</strong>的量级，配上带零偏修正的姿态融合能压到约 0.5°/min。结论不变：航向类赛题预算允许就上 HWT101 / JY901 这类带融合的专用航向模块。</li>
</ul>
</div>
<p>调参这一关过了，你的车就有了&#8221;稳和准&#8221;的底子。但你可能还会问：前馈、模糊 PID、卡尔曼这些听起来很高级的东西，到底要不要上、什么时候上才不算本末倒置？这正是进阶控制算法那一篇要替你算的那笔性价比账。</p>
<div class="ds-series" style="border:1px solid #4488ff33;background:#4488ff0d;border-radius:8px;padding:.8em 1.1em;margin:1.2em 0">
<p style="margin:0 0 .5em"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4da.png" alt="📚" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 本文是 <strong>「从 0 到 1 带你打电赛 · 小车电控篇」</strong> 系列（共 12 篇）第 8 篇。</p>
<ol style="margin:.2em 0 0;padding-left:1.4em">
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-01-how-to-score/">第1篇 · 拿奖逻辑：把比赛拆成小目标</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-02-history/">第2篇 · 赛题进化史与押题</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-03-build-and-architecture/">第3篇 · 整车搭建与代码框架</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-04-motor-power/">第4篇 · 电机驱动与电源地基</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-05-sensing/">第5篇 · 感知：灰度/电磁/编码器/IMU</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-06-pid-basics/">第6篇 · PID 入门：搞懂 P/I/D</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-07-pid-advanced/">第7篇 · PID 进阶：串级+工程补丁</a></li>
<li style="margin:.15em 0"><strong>第8篇 · PID 调参实战(核心)（本篇）</strong></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-09-advanced-control/">第9篇 · 进阶控制：几时该上</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-10-vision-comm/">第10篇 · K230 视觉与通信协议</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-11-architecture-fsm/">第11篇 · 状态机与整车软件</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-12-field-manual/">第12篇 · 现场作战+避坑+开源</a></li>
</ol>
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://cloudlay.cn/nuedc-car-08-pid-tuning/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>从0到1带你打电赛·小车电控篇(七)：PID 进阶——方向环、串级双环，和「不做就翻车」的工程补丁</title>
		<link>https://cloudlay.cn/nuedc-car-07-pid-advanced/</link>
					<comments>https://cloudlay.cn/nuedc-car-07-pid-advanced/#respond</comments>
		
		<dc:creator><![CDATA[云间辞]]></dc:creator>
		<pubDate>Sun, 14 Jun 2026 17:08:33 +0000</pubDate>
				<category><![CDATA[嵌入式]]></category>
		<category><![CDATA[串级PID]]></category>
		<category><![CDATA[工程实现]]></category>
		<category><![CDATA[抗积分饱和]]></category>
		<category><![CDATA[方向环]]></category>
		<category><![CDATA[智能小车]]></category>
		<category><![CDATA[电赛]]></category>
		<guid isPermaLink="false">https://cloudlay.cn/nuedc-car-07-pid-advanced/</guid>

					<description><![CDATA[📚 本文是 「从 0 到 1 带你打电赛 · 小车电控篇」 系列（共 12 篇）第 7 篇。 第1篇 · 拿奖 [&#8230;]]]></description>
										<content:encoded><![CDATA[<div class="ds-series" style="border:1px solid #4488ff33;background:#4488ff0d;border-radius:8px;padding:.8em 1.1em;margin:1.2em 0">
<p style="margin:0 0 .5em"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4da.png" alt="📚" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 本文是 <strong>「从 0 到 1 带你打电赛 · 小车电控篇」</strong> 系列（共 12 篇）第 7 篇。</p>
<ol style="margin:.2em 0 0;padding-left:1.4em">
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-01-how-to-score/">第1篇 · 拿奖逻辑：把比赛拆成小目标</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-02-history/">第2篇 · 赛题进化史与押题</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-03-build-and-architecture/">第3篇 · 整车搭建与代码框架</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-04-motor-power/">第4篇 · 电机驱动与电源地基</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-05-sensing/">第5篇 · 感知：灰度/电磁/编码器/IMU</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-06-pid-basics/">第6篇 · PID 入门：搞懂 P/I/D</a></li>
<li style="margin:.15em 0"><strong>第7篇 · PID 进阶：串级+工程补丁（本篇）</strong></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-08-pid-tuning/">第8篇 · PID 调参实战(核心)</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-09-advanced-control/">第9篇 · 进阶控制：几时该上</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-10-vision-comm/">第10篇 · K230 视觉与通信协议</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-11-architecture-fsm/">第11篇 · 状态机与整车软件</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-12-field-manual/">第12篇 · 现场作战+避坑+开源</a></li>
</ol>
</div>
<p>上一篇《PID 入门》里，我们把一个单电机的速度环从只有 P 开始，一步步调到能稳稳跟住目标速度。如果你已经跑通了那一步，恭喜——你已经摸到了控制的门。但单环只能让&#8221;一个量&#8221;听话，真正上赛道的车要同时管两件事：<strong>跑多快</strong>和<strong>往哪拐</strong>。这一篇，我们把单环升级成能上场的完整控制：先讲方向环，再讲串级双环，最后讲一套&#8221;不打就翻车&#8221;的工程补丁。</p>
<p>先说一句大实话：很多人调车调到崩溃，以为是 Kp/Ki/Kd 没拧到位，其实十有八九是少打了某个补丁。<strong>这些补丁往往比纠结那三个参数更决定成败。</strong> 这篇就是带你把它们一次配齐。</p>
<h2>方向环：偏差怎么变成&#8221;左右轮速度差&#8221;</h2>
<p>小车靠两个轮子转向。想右转，就让右轮慢一点、左轮快一点，车头自然就偏过去了——跟划船一个道理。</p>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 大白话比喻</p>
<p>差速转向像划船：想往右转，就右手轻划(右轮慢)、左手猛划(左轮快)，船头就往右偏。方向环要算的，就是&#8221;该往哪边偏、偏多少力气&#8221;，然后左桨减、右桨加。</p>
</div>
<p>那&#8221;该偏多少&#8221;从哪来？来自一个<strong>偏差</strong>。这个偏差可以是： &#8211; <strong>循迹题</strong>：摄像头看到的赛道中线偏差，或灰度/电感传感器的左右差(差比和)； &#8211; <strong>方向保持题</strong>：陀螺仪测到的偏航角(Yaw)误差，比如&#8221;我想走直线，结果车头偏了 5 度&#8221;。</p>
<p>把这个偏差喂给一个 PID(方向环实际上多数只用 PD，下面会解释)，算出一个转向量 <code>turn</code>，再分配到左右轮：</p>
<p class="ds-math" style="overflow-x:auto">$$\text{左轮目标} = \text{base} &#8211; \text{turn}, \qquad \text{右轮目标} = \text{base} + \text{turn}$$</p>
<p><code>base</code> 是直行基础速度。这个加减号谁正谁负，取决于你电机和传感器的安装方向，调的时候试一下就知道。</p>
<p>它背后是差速车的运动学(给定线速度 $v$、角速度 $w$、轮距 $L$)：</p>
<p class="ds-math" style="overflow-x:auto">$$v_R = v + \tfrac{1}{2} w L, \qquad v_L = v &#8211; \tfrac{1}{2} w L$$</p>
<div class="ds-callout ds-callout-note" style="border-left:4px solid #448aff;background:#448aff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#448aff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4dd.png" alt="📝" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 别把轮距搞混</p>
<p>这里的 $L$ 是<strong>轮距</strong>(左右轮中心间距)，不是前后轴距。代公式时别拿错尺寸。</p>
</div>
<p>平衡车里有个更直接的经典写法，是在 PWM 层直接把几路输出叠加，可以借鉴它的&#8221;共模/差模&#8221;思路：</p>
<pre><code class="language-c">// 直立项 + 速度项(共模,左右一样) ± 转向项(差模,左右相反)
Moto_Left  = Balance_Pwm + Velocity_Pwm - Turn_Pwm;   // 左轮
Moto_Right = Balance_Pwm + Velocity_Pwm + Turn_Pwm;   // 右轮

// 纯差速循迹小车去掉 Balance,在"目标速度层"做混合:
// Left_Speed_Goal  = base_speed - turn;
// Right_Speed_Goal = base_speed + turn;</code></pre>
<h3>方向环为什么常常只用 PD，不加 I？</h3>
<p>方向环里那个 <code>turn</code> 一般用<strong>位置式 PD</strong>(位置式、增量式的区别《PID 入门》讲过了)。入弯时偏差会突然变大，这一下主要靠 <strong>D 项</strong>来响应——D 看的是&#8221;偏差变化率&#8221;，偏差一跳它最敏感，能让车提前打方向。</p>
<p>那 I 呢？社区里有很强的共识：方向环<strong>通常弱化甚至不加 I</strong>。原因有两个：陀螺仪有零飘、车轮会打滑，数据本身就不太准，靠 I 去&#8221;消静差&#8221;意义不大；而且 I 会引入延迟，过弯后车身&#8221;拖泥带水&#8221;半天回不到中线。</p>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 这条不要绝对化</p>
<p>&#8220;方向环绝对不能加 I&#8221;是个<strong>偏教条</strong>的说法。准确讲：方向环<strong>以 PD 为主导</strong>；但如果你的车存在固定的机械/装配偏置——比如装得不正、走直线总是恒定地偏一点点角度——这时加一个<strong>很小、并且带积分分离和限幅</strong>的 I 来纠正这个恒定偏角，是合理且有用的(自动驾驶 PID 理论里，I 项正是用来消除转向系统的恒定偏置/steering drift)。结论是：默认 PD，需要纠恒定偏置时再谨慎补一点小 I；但高速动态循迹下 I 过大确实会带来滞后和超调。</p>
</div>
<p>这里有个真实的坑：有人调方向环手痒加了积分，结果回正迟钝、过弯后车身拖泥带水回不到中线；去掉 I 只留 PD 后转向立刻干脆了。(<a href="https://blog.csdn.net/qq_63306197/article/details/143493535" target="_blank" rel="noopener noreferrer">来源</a>)</p>
<h3>调方向环之前，先验极性！</h3>
<p>这是新手最容易栽的地方，而且一栽就是&#8221;参数怎么调都没用&#8221;。</p>
<div class="ds-callout ds-callout-danger" style="border-left:4px solid #ff1744;background:#ff174414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff1744"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 极性接反 = 正反馈 = 直接失控</p>
<p>方向环极性接反，就像方向盘装反了：你想往左打，车却往右拐，而且越打越歪。这是正反馈，再完美的参数也救不了。</p>
</div>
<p>验极性的方法极简单：给一个正的 Kp，然后<strong>用手去转车</strong>。 &#8211; 车<strong>反抗</strong>你、想转回来 → 负反馈，极性对 <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> &#8211; 车<strong>顺着</strong>你越转越欢 → 正反馈，极性反了 <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" />，把符号取反，或交换电机线/陀螺仪安装方向</p>
<div class="ds-callout ds-callout-important" style="border-left:4px solid #00bcd4;background:#00bcd414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bcd4"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2757.png" alt="❗" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 极性和增益要分两步调</p>
<p>先确认极性(符号)对，<strong>再</strong>调大小(增益)。把这两件事混在一起，你会把&#8221;符号错&#8221;当成&#8221;参数不对&#8221;在那瞎拧。换了电机接线或陀螺仪安装方向后，必须重新验一遍极性。</p>
</div>
<p>调 P 和 D 的口诀和单环一样：从 D=0、只加 P 开始，P 加到走直线/循迹基本能跟线、但弯道略冲；再加 D 抑制过冲和振荡。<strong>P 太大直道会画龙(蛇形摆)，D 太大对噪声敏感会高频抖舵。</strong> 一套能照抄的完整方向环调参步骤，留到《PID 调参实战》一篇细讲。</p>
<h2>串级双环：老板与员工</h2>
<p>单环 PID 自稳性还凑合，但一遇到负载变化——上坡、打滑、电池掉压——跟随就会滞后甚至失稳。串级(双反馈)PID 是控制赛道的标准答案，它的核心思想一句话：</p>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 串级 PID 像老板和员工</p>
<p>外环是老板，只说&#8221;这个月业绩做到 100 万&#8221;(定目标)，不亲自跑业务；内环是员工，负责&#8221;今天这一单怎么签下来&#8221;(快速执行)。老板看大方向(慢、稳)，员工干具体活(快、勤)。<strong>老板的指令，就是员工的工作目标——这就是&#8221;外环输出 = 内环目标值&#8221;。</strong></p>
</div>
<p>具体到小车： &#8211; <strong>外环</strong>(方向/位置/角度)：负责&#8221;对不对得准、到没到位&#8221;。它的输出<strong>不直接驱动电机</strong>，而是作为内环的目标值。 &#8211; <strong>内环</strong>(速度/角速度)：负责让实际转速<strong>快速精确</strong>跟上外环给的目标，输出才真正变成 PWM。</p>
<p>偏差大时，外环给一个大目标速度让车快速逼近；快到了，外环把目标速度收小，避免冲过头。</p>
<p>为什么这样就更抗造？举个上坡的例子：上坡瞬间电机实际转速被拖下来，<strong>内环立刻发现&#8221;我没跟上目标&#8221;，马上加大 PWM 顶上去</strong>(粗调、快)，外环再慢慢保证最终到位(细调)。所以串级在变负载、高速时明显比单环稳，提速时不甩尾。(<a href="https://blog.csdn.net/dpc03/article/details/132001414" target="_blank" rel="noopener noreferrer">来源</a>)</p>
<figure class="ds-diagram" style="text-align:center;margin:1.3em 0"><img decoding="async" src="https://cloudlay.cn/wp-content/uploads/dianseiche/649838f28ae1.png" alt="电赛小车电控 · 示意图" loading="lazy" style="max-width:100%;height:auto;background:#fff;border-radius:8px;padding:6px;box-shadow:0 1px 6px rgba(0,0,0,.25)"></figure>
<h3>分工：速度内环用增量式 PI，方向外环用位置式 PD</h3>
<p>多数车队的搭配是： &#8211; <strong>速度内环 → 增量式 PI</strong>：输出是增量累加，电机不容易在正反转之间突跳，天然好处理饱和，只需输出限幅。 &#8211; <strong>方向外环 → 位置式 PD</strong>：偏差直观、响应快，不加(或只加极小)I。</p>
<p>速度内环的标准写法(左轮为例，偏差 = 目标 − 编码器)：</p>
<pre><code class="language-c">void Left_Speed_Control(void)
{
    float erro = Speed_Goal - Left_Encoder;            // 偏差 = 目标 - 编码器读数
    // 增量式: Δu = P*(e-e1) + I*e + D*(e-2*e1+e2)
    Left_Speed_PID.OUT += ( Left_Speed_PID.P * (erro - Left_Speed_Lasterro)
                          + Left_Speed_PID.I *  erro
                          + Left_Speed_PID.D * (erro - 2*Left_Speed_Lasterro + Left_Speed_Preverro) );
    Left_Speed_OUT = Range_protect((int)Left_Speed_PID.OUT, -9000, 9000); // 输出限幅
    Left_Speed_Preverro = Left_Speed_Lasterro;
    Left_Speed_Lasterro = erro;
}</code></pre>
<div class="ds-callout ds-callout-note" style="border-left:4px solid #448aff;background:#448aff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#448aff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4dd.png" alt="📝" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 一个省事的小技巧</p>
<p>速度内环的反馈<strong>直接用编码器原始计数就行</strong>，不必费劲换算成&#8221;米/秒&#8221;这种真实物理速度——中间只差一个固定比例系数，会被 Kp 自动吸收掉。别为换算真实速度浪费精力。</p>
</div>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 上面代码里的 ±9000 不是&quot;万能经验值&quot;</p>
<p>这个限幅值强依赖你的定时器满量程(ARR)。正确的姿势是：<strong>输出限幅取 PWM 满量程的某个比例(比如 90%)</strong>，具体数字随你的 ARR 而定。同理，网上各种&#8221;方向环 Kp = 0.6&#8243;之类的数字，都和反馈量纲、陀螺量程(±250/500/2000 dps)强相关，<strong>只能当某次实现的参考，不能跨平台照搬</strong>。换了平台必须从小到大重新整定。</p>
</div>
<h3>内环要比外环跑得快得多</h3>
<p>这是串级的命门：<strong>内环周期必须远小于外环周期。</strong></p>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 比喻：手速要快，战略可以慢</p>
<p>内环快外环慢，像打游戏：微操(补刀、走位)必须每秒几十次；大局观(什么时候推塔)几秒决策一次就够。要是战略也每秒变几十次，只会自己乱了阵脚。</p>
</div>
<p>经验上，<strong>内环周期取外环的 1/3 到 1/10</strong>(等价于内环带宽是外环的 3~10 倍)。这是跨多个来源一致的核心规律——<strong>量级上</strong>把握住就行，不必死抠精确数字。代码实现很简单，用一个计数器分频：内环每次中断都算，外环每隔 N 次才算一次。</p>
<pre><code class="language-c">// 在同一个定时器中断(周期 T)里:
current_loop();                       // 最内环:每次都算
if (outer_ring_timer % 2 == 0)        // 速度环:每 2T 算一次
    speed_loop();
if (outer_ring_timer % 3 == 0)        // 位置/方向外环:每 3T 算一次
    position_loop();
outer_ring_timer++;</code></pre>
<p>野火的有刷电机三环例程就是这么干的：电流内环每 T、速度环每 2T、位置环每 3T。平衡车的经典分层大致是直立/角度环最快(约 5~10ms)、速度外环为它的若干倍周期。</p>
<div class="ds-callout ds-callout-note" style="border-left:4px solid #448aff;background:#448aff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#448aff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4dd.png" alt="📝" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 别把这组数字背死</p>
<p>&#8220;平衡车一定是角速度 2ms / 角度 10ms / 速度 50~100ms&#8221;这种说法<strong>偏死板</strong>。很多实现把速度环和直立环放同一个中断、只做几倍分频。真正重要的不是这组具体数字，而是<strong>保持内环明显快于外环的频率分离</strong>。2024 电赛 H 题里很多队伍直接用一个 10ms 定时器中断(100Hz)跑完整套 PID，也照样跑得很好。</p>
</div>
<div class="ds-callout ds-callout-danger" style="border-left:4px solid #ff1744;background:#ff174414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff1744"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 一个常见的认知混乱：控制周期 ≠ PWM 频率</p>
<p>这俩差着几十上百倍，初学者最容易搞混： &#8211; <strong>PWM 载波频率</strong>(电机驱动)：10~20kHz，决定电机平滑度和噪声。 &#8211; <strong>PID 控制周期</strong>(定时器中断里跑 PID)：1~10ms，也就是 100~1000Hz。</p>
<p>这里要破一个常见误解：10kHz <strong>并没有真正&#8221;避开可听噪声&#8221;</strong>——人耳上限约 20kHz，10kHz 反而是明显的尖啸。10kHz 只是兼顾电机平滑度、开关损耗(发热)与噪声的一个<strong>够用的折中下限</strong>。<strong>要真正消除可听啸叫，得把 PWM 提到 20kHz 以上</strong>(超出人耳上限)，代价是开关损耗变大、对驱动 MOS 要求更高。所以更稳妥的做法是直接上 20kHz 左右。(选驱动芯片和具体频率取舍见《电机驱动与电源地基》一篇。)</p>
</div>
<h3>调参铁律：先调内环，后调外环</h3>
<div class="ds-callout ds-callout-important" style="border-left:4px solid #00bcd4;background:#00bcd414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bcd4"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2757.png" alt="❗" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 这是新手最大的坑：千万别先调外环</p>
<p>调外环时，外环&#8221;看到&#8221;的对象其实是<strong>内环 + 电机这个整体</strong>。如果内环本身不稳，这个整体特性就是飘的，你在它上面调出来的外环参数毫无意义，一换工况就废。</p>
</div>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 比喻：先教会司机开车，再规划路线</p>
<p>如果司机(内环)连油门刹车都控制不稳，你给他再完美的导航(外环)，车也开得东倒西歪。必须先把司机训练到指哪开哪、稳准快，路线规划才有意义。</p>
</div>
<p>正确流程： 1. <strong>先屏蔽外环</strong>(把外环运算注释掉，或给内环一个固定目标速度)。 2. 单独调内环：让车架空或直线低速跑，给一个突变的目标速度，看编码器波形——调到能<strong>快速、平滑、几乎不超调</strong>地跟上，受扰后不振荡。 3. 内环稳了，对外环来说它就近似成了一个&#8221;稳定快速的等效环节&#8221;。这时再像调普通 PID 那样调外环：先加 P，再加 D。</p>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 调某一环时，把其它环的运算先注释/置零</p>
<p>一个真实教训(2020 全国智能车直立组)：串级&#8221;一环套一环，出了问题根本分不清是哪个环&#8221;，整定极其耗时。这位选手三次整定分别花了 5~6 小时、2~3 小时、1 小时——熟能生巧没有捷径。所以一定要<strong>分环隔离、逐个整定</strong>，绝不要三个环一起调。(<a href="https://blog.csdn.net/weixin_44065323/article/details/107926155" target="_blank" rel="noopener noreferrer">来源</a>)</p>
</div>
<h2>工程补丁八件套：不打就翻车</h2>
<p>终于到了这一篇真正的&#8221;硬核私货&#8221;。下面这八个补丁，每一个都对应一类&#8221;调参怎么都调不好&#8221;的诡异现象。把它们配齐，你的车才算从&#8221;实验室能跑&#8221;进化到&#8221;赛场能扛&#8221;。</p>
<h3>补丁 1：积分限幅 + 补丁 7：输出限幅</h3>
<p>这俩是一对，一起讲。<strong>输出限幅</strong>是把最终 PWM 夹到 <code>[-Max, Max]</code>；<strong>积分限幅</strong>是单独给积分累加项设上下界。</p>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 最常见的错误：只限输出，不限积分</p>
<p>很多人以为夹了输出就万事大吉。错。如果你只限输出不管积分，积分项还会<strong>偷偷涨到非常大</strong>，等误差反向时它迟迟泄不掉，车冲过头才肯回头。</p>
</div>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 比喻</p>
<p>输出限幅是&#8221;限制方向盘最多打到底&#8221;，积分限幅是&#8221;限制你心里那股较劲的劲儿别攒太多&#8221;。只管方向盘、不管心里的劲，松手时车还是会猛回弹。</p>
</div>
<p>顺序也是固定的：<strong>先有输出限幅，anti-windup 才有&#8221;是否饱和&#8221;可判断。</strong> 国一开源 Enterprise_E 里就是电机环积分限幅 <code>i_Max=1000</code>、转向环 <code>i_Max=400</code>，和输出限幅各自独立。</p>
<h3>补丁 2：积分分离</h3>
<p>专治起步/大阶跃时的猛烈超调。思路：<strong>误差大时关掉积分(只用 PD 快速逼近)，误差进入小范围才投入积分消静差。</strong></p>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 比喻：考试策略</p>
<p>离正确答案还很远时(大误差)别抠细节，先大刀阔斧往对的方向赶(只用 PD)；快接近了(小误差)再用积分慢慢抠掉最后那点零头(静差)。</p>
</div>
<p>可以做成三段式平滑过渡：</p>
<pre><code class="language-c">if (pid-&gt;errABS &gt; 50)         kiIndex = 0.000f;   // 大误差:关积分,纯 PD 冲
else if (pid-&gt;errABS &gt; 30)    kiIndex = 0.600f;   // 中误差:半投
else                          kiIndex = 1.000f;   // 小误差:全投,消静差
pid-&gt;outPut = pid-&gt;outPutP + pid-&gt;outPutI * kiIndex + pid-&gt;outPutD;</code></pre>
<p>实战反馈：智能车里<strong>积分分离比变速积分更直接好用、更干脆</strong>。(<a href="https://ittuann.github.io/2021/08/29/CarPID.html" target="_blank" rel="noopener noreferrer">来源</a>)</p>
<h3>补丁 3：抗积分饱和(anti-windup)</h3>
<p>这是积分类问题的总根源。<strong>积分饱和(windup)</strong>是指：误差长期同号 → 积分越累越大 → 输出顶到饱和 → 等误差反向时，积分要很久才泄完 → 造成大滞后、大超调。</p>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 比喻：上长坡一直深踩油门</p>
<p>坡(误差)一直在，你脚越踩越深(积分越累越大)；到了坡顶该松油门减速了(误差反向)，可你脚踩得太深，要抬好久车才肯减速——这段&#8221;反应不过来&#8221;就是饱和带来的大滞后。anti-windup 就是&#8221;看到油门踩到底了，就别再使劲往下踩&#8221;。</p>
</div>
<p>有两大流派。</p>
<p><strong>流派 A：遇限削弱(条件积分 / clamping)</strong>——输出已饱和时，只允许&#8221;会让积分往回退&#8221;的累加：</p>
<pre><code class="language-c">if (ABS(pid-&gt;outPutLast) &gt; outPutLimit) {
    // 仅当积分会被"反向"削弱时才累加(误差与积分异号才累加)
    if ((pid-&gt;errSum &gt; 0 &amp;&amp; pid-&gt;errNow &lt; 0)
     || (pid-&gt;errSum &lt; 0 &amp;&amp; pid-&gt;errNow &gt; 0)) {
        pid-&gt;errSum += pid-&gt;errNow;
    }
    // 同号(会让积分更饱和) → 本次不累加
} else {
    pid-&gt;errSum += pid-&gt;errNow;   // 未饱和,正常累加
}</code></pre>
<p><strong>流派 B：反计算(back-calculation)</strong>——饱和时把&#8221;饱和差&#8221;按一定速率反喂给积分器，主动把它拉回来。离散实现很干脆：</p>
<pre><code class="language-python"># 先算未饱和输出 op
if op[i] &gt; op_hi:        # 触上限
    op[i] = op_hi
    ie[i] = ie[i] - e[i] * delta_t    # 抵消本次积分增量
if op[i] &lt; op_lo:        # 触下限
    op[i] = op_lo
    ie[i] = ie[i] - e[i] * delta_t</code></pre>
<p>反计算有个跟踪时间常数 $T_t$，经验取 $T_t = \sqrt{T_i \cdot T_d}$，且满足 $T_d < T_t < T_i$；$T_t$ 越小，积分泄得越快。</p>
<div class="ds-callout ds-callout-note" style="border-left:4px solid #448aff;background:#448aff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#448aff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4dd.png" alt="📝" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 增量式 PID 算不算&quot;天生免疫&quot;饱和？</p>
<p>入门篇说过增量式&#8221;天然规避积分饱和&#8221;——这话<strong>基本对，但别理解得太绝对</strong>。增量式确实没有&#8221;显式累加 Σe 导致数值无界膨胀&#8221;那种经典 windup；但当 <code>u(k)=u(k-1)+Δu</code> 的输出被限幅(比如 PWM 顶到 100%)时，<strong>仍会出现类 windup 的滞后/退饱和延迟</strong>。严重时增量式照样需要反算抗饱和。准确说法是&#8221;不易/天然规避显式积分饱和&#8221;，不是&#8221;绝对不会饱和&#8221;。</p>
</div>
<h3>补丁 4：微分先行 / 对测量值微分</h3>
<p>专治&#8221;设定值突变&#8221;。如果微分项是对<strong>误差</strong>做差分，那么设定值一阶跃(比如目标速度从 0 突然变 100)，误差瞬间巨变，微分项会蹦出一个巨大的尖峰冲击执行器——这就是 <strong>derivative kick(微分踢)</strong>。</p>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 比喻</p>
<p>微分踢像你正稳稳走路，突然有人把终点瞬移到很远——如果你对&#8221;目标距离的变化&#8221;做反应，会被吓得猛地一蹿。改成只盯<strong>自己脚步</strong>的变化(对测量值微分)，就不会被目标瞬移惊到。</p>
</div>
<p>解法：微分项改用 $-(PV_k &#8211; PV_{k-1})$，作用在测量值上而非误差上：</p>
<pre><code class="language-c">// 微分项用 -(PV变化) 代替 (误差变化),二阶形式同时抑制噪声
u_k = u_k_1
    + KP*(e_k - e_k_1)
    + KI*e_k*dt
    - KD*(PV_k - 2*PV_k_1 + PV_k_2)/dt;</code></pre>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 这只消了&quot;微分踢&quot;,没消&quot;比例踢&quot;</p>
<p>上面这个式子里，比例项还是 <code>KP*(e_k - e_k_1)</code>，设定值阶跃时它仍会让输出瞬间跳一下 <code>KP*Δsp</code>——这叫<strong>比例踢(proportional kick)</strong>。如果你的设定值会频繁大幅跳变、想把它也驯服，需要用<strong>设定值加权 / I-PD(二自由度)结构</strong>：让比例项也作用于测量值(或加权 <code>KP*(β*sp - PV)</code>)，只让积分项作用于误差。记住：仅对 PV 微分<strong>只解决 derivative kick</strong>，比例踢得另行处理。</p>
</div>
<h3>补丁 5：微分低通滤波(不完全微分)</h3>
<p>专治&#8221;加大 D 就抖/啸叫&#8221;。纯微分会把编码器、陀螺仪、ADC 的高频噪声放大成抖动甚至尖啸。</p>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 比喻：戴降噪耳机听节奏</p>
<p>不滤波时，旁边一点杂音(传感器噪声)都被你当成节拍剧烈摇头(抖动)；滤波后只跟真正的鼓点(真实趋势)走。</p>
</div>
<p>做法是给微分项串一个一阶低通：</p>
<pre><code class="language-c">thisError = setpoint - processValue;
integral += thisError;
// 关键: (1-α)*新微分 + α*上次微分
thisDev = Kd*(1.0f - alpha)*(thisError - lasterror) + alpha*lastdev;
result  = Kp*thisError + Ki*integral + thisDev;
lasterror = thisError;
lastdev   = thisDev;</code></pre>
<p>系数 $\alpha \in (0,1)$：$\alpha=0$ 无滤波、$\alpha=1$ 等于没微分。等价于工业界的微分滤波系数 $N$(典型 8~20，越小滤得越狠)。</p>
<div class="ds-callout ds-callout-important" style="border-left:4px solid #00bcd4;background:#00bcd414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bcd4"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2757.png" alt="❗" class="wp-smiley" style="height: 1em; max-height: 1em;" /> D 项的两大坑，要一起堵</p>
<p>derivative kick(对误差微分)和噪声放大(不滤波)是 D 项的两大坑，分别用<strong>对测量微分</strong>和<strong>低通滤波</strong>解决。配齐这两个补丁，你的 Kd 才敢用得起来、敢加大。一个常见误判：Kd 过大引起的高频抖动，经常被当成&#8221;机械松动&#8221;——先确认是不是 D 没滤波，再去拧螺丝。</p>
</div>
<h3>补丁 6：电机死区补偿</h3>
<p>专治&#8221;低速/小误差时车原地哒哒抖，或干脆趴窝不动&#8221;。静摩擦加上 H 桥死区，会让占空比低于某个阈值(常见 5%~10%，或绝对值如 &lt; 65/1000)时电机根本不转——这时控制增益在死区内突然变 0，系统就极限环抖动或卡死。</p>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 比喻：太松的自行车链条</p>
<p>你蹬了一点(小占空比)链条还在空档没咬上，车不动；得先蹬过那段空档(死区补偿)，后面才线性使上劲。</p>
</div>
<p>补偿就是输出非零时，叠加一个符号相关的偏置把死区&#8221;跨过去&#8221;：</p>
<pre><code class="language-c">if (out &gt; 0)      out += DEAD_ZONE;   // 正向跨死区
else if (out &lt; 0) out -= DEAD_ZONE;   // 反向跨死区
// out == 0 时保持 0,避免零点附近来回跳</code></pre>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 别过补偿</p>
<p>死区补偿标定要准。补多了会在零点附近反复正反跳变，产生<strong>新的</strong>抖动。建议实测电机刚启动的最小占空比，再略加一点余量。另外，上闭环前先把电机中值/死区标定好——否则零点附近反复跳变，看起来像 PID 没调好，其实是死区在作怪。</p>
</div>
<h3>补丁 8：电池电压补偿</h3>
<p>这是&#8221;实验室调好、上场翻车&#8221;的隐形元凶。同一个占空比下，电机力矩正比于母线电压；电池从满电掉到欠压，等效 Kp 就<strong>悄悄变小</strong>了，高速时车会变软、摆头甚至失控。</p>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 比喻：跑步时的体力</p>
<p>满电(刚出发)同样力气跑得快，没电(快到终点)同样力气跑得慢。补偿就是&#8221;累了就多使点劲&#8221;，让全程速度感一致——你练习时(满电)定的节奏，比赛后半程(亏电)也还成立。</p>
</div>
<p>补偿公式简单，但要 ADC 实测电池电压：</p>
<pre><code class="language-c">// V_NOMINAL 为标称/调参时电压, v_batt 由 ADC 实测
pwm_out = pid_out * (V_NOMINAL / v_batt);</code></pre>
<div class="ds-callout ds-callout-note" style="border-left:4px solid #448aff;background:#448aff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#448aff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4dd.png" alt="📝" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 两个注意点</p>
<p>一是电压采样要滤波，否则电压噪声会反灌进 PWM；二是低于欠压保护阈值时应该<strong>限速</strong>而不是死命补偿。</p>
</div>
<p>一个真实战例：高速行驶时车突然失控摆头，排查半天发现是电池电量下降导致同占空比下力矩变小、等效 Kp 变软。加上电压补偿后，从满电到亏电表现终于一致了。(<a href="https://blog.csdn.net/2301_80317247/article/details/145358194" target="_blank" rel="noopener noreferrer">来源</a>)</p>
<h2>一张表，把八件套对号入座</h2>
<table>
<thead>
<tr>
<th>你看到的症状</th>
<th>八成是缺了哪个补丁</th>
</tr>
</thead>
<tbody>
<tr>
<td>起步给大目标，车猛冲过头(大超调)</td>
<td>积分分离 + 积分限幅 + anti-windup</td>
</tr>
<tr>
<td>误差反向后车迟迟不回头、退饱和慢</td>
<td>anti-windup(遇限削弱 / 反计算)</td>
</tr>
<tr>
<td>一改目标值，输出就猛地蹿一下</td>
<td>微分先行(对测量值微分)</td>
</tr>
<tr>
<td>一加大 Kd，车就高频抖/啸叫</td>
<td>微分低通滤波</td>
</tr>
<tr>
<td>低速/小误差原地哒哒抖或趴窝</td>
<td>死区补偿</td>
</tr>
<tr>
<td>满电调好上场，后半程变软/摆头</td>
<td>电池电压补偿</td>
</tr>
<tr>
<td>反向时积分泄不掉、冲过头</td>
<td>积分限幅(只限输出不够)</td>
</tr>
<tr>
<td>输出超过 PWM 量程</td>
<td>输出限幅(也是所有抗饱和的前提)</td>
</tr>
</tbody>
</table>
<h2>别忘了陀螺仪零飘</h2>
<p>方向环如果用陀螺仪 Yaw 做反馈，零飘是绕不开的。一个真实战例：2024 电赛 H 题某队原计划用编码器 PID 调速，却发现两个轮子性能不一致、达到恒速的时间都不一样，被逼着改用陀螺仪 Yaw 检测偏航角控直行；结果省赛前夜先误烧了十轴模块，又发现 MPU6050 的 Yaw 在漂，最后比赛前 6 小时才把应急模块移植成功。(<a href="https://blog.csdn.net/m0_74800695/article/details/140869823" target="_blank" rel="noopener noreferrer">来源</a>)</p>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 一个被广泛误传的数字</p>
<p>网上常说&#8221;MPU6050 的 Yaw 一秒漂 2 度&#8221;——这个<strong>严重高估</strong>了。经过一次性零偏校准后，MPU6050 的 Yaw 裸积分漂移大约是<strong>每分钟几度</strong>的量级；如果配上带零偏修正的姿态融合(如 Madgwick)，能降到约 <strong>0.5°/分钟</strong>(≈0.008°/s)。&#8221;每秒 2 度&#8221;更像是完全没校零偏时的瞬时表现。</p>
<p>但<strong>结论方向不变</strong>：Yaw 漂移确实需要处理。要么上电静止时采一次零偏并做软件纠偏，要么干脆换 HWT101/JY901 这类带磁/融合的专用航向模块。不处理的话，直行越走越偏、定角度转弯越转越不准。(零偏校准与互补滤波的具体做法见《感知：灰度/电磁/编码器/IMU》一篇。)</p>
</div>
<p>另外，左右电机/编码器往往天生不一致，<strong>最好左右各用一套速度 PID 参数分别整定</strong>，或在差速混合里加补偿，别指望一套参数包打天下。</p>
<h2>收个尾：补丁配齐，参数才有意义</h2>
<p>把这一篇捋一遍，你应该建立起这样一条心智：</p>
<figure class="ds-diagram" style="text-align:center;margin:1.3em 0"><img decoding="async" src="https://cloudlay.cn/wp-content/uploads/dianseiche/5f54c7d17132.png" alt="电赛小车电控 · 示意图" loading="lazy" style="max-width:100%;height:auto;background:#fff;border-radius:8px;padding:6px;box-shadow:0 1px 6px rgba(0,0,0,.25)"></figure>
<div class="ds-callout ds-callout-success" style="border-left:4px solid #00c853;background:#00c85314;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00c853"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 这一篇的核心就三句话</p>
<ol>
<li><strong>先调内环、后调外环</strong>，分环隔离逐个整定，绝不三环一起上。</li>
<li><strong>内环周期远小于外环</strong>(1/3~1/10)，控制周期和 PWM 频率是两码事。</li>
<li><strong>八件套补丁往往比 Kp/Ki/Kd 更决定成败</strong>——它们才是&#8221;调好了上场不翻车&#8221;的真正保险。</li>
</ol>
</div>
<p>想看这套东西在真实比赛代码里长什么样，强烈推荐通读第十六届智能车国一开源 <a href="https://github.com/ittuann/Enterprise_E" target="_blank" rel="noopener noreferrer">ittuann/Enterprise_E</a>：它的 <code>pid.c</code> 把误差限幅、积分限幅、积分分离、专家 PID、前馈补偿全配齐了，注释也到位，是工程补丁落地的最佳样板；配套<a href="https://ittuann.github.io/2021/08/29/CarPID.html" target="_blank" rel="noopener noreferrer">博客</a>更是把抗饱和、积分分离、不完全微分讲透了。串级落地代码可以参考 STM32 的 <a href="https://gitee.com/xxpcb/stm32-motor-pid" target="_blank" rel="noopener noreferrer">xxpcb/stm32-motor-pid</a>、TI MSPM0 赛道(对口 2024 H 题)的 <a href="https://github.com/Szturin/M0G3507-TI-CAR" target="_blank" rel="noopener noreferrer">Szturin/M0G3507-TI-CAR</a>，原理教程则首推<a href="https://doc.embedfire.com/motor/motor_tutorial/zh/latest/improve_part/dc_motor_multi_loop_control.html" target="_blank" rel="noopener noreferrer">野火有刷电机多环控制</a>。</p>
<p>到这里，方向环、串级、补丁你都备齐了，但参数到底怎么从 0 一步步拧出来、波形该看哪些症状下哪些药——那才是真正的硬功夫。具体调参流程，我们留到下一篇 PID 调参实战，用一套能照抄的步骤和一张&#8221;波形症状→该调哪个参数&#8221;的诊断表，带你把这些参数一个个调到位。</p>
<div class="ds-series" style="border:1px solid #4488ff33;background:#4488ff0d;border-radius:8px;padding:.8em 1.1em;margin:1.2em 0">
<p style="margin:0 0 .5em"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4da.png" alt="📚" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 本文是 <strong>「从 0 到 1 带你打电赛 · 小车电控篇」</strong> 系列（共 12 篇）第 7 篇。</p>
<ol style="margin:.2em 0 0;padding-left:1.4em">
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-01-how-to-score/">第1篇 · 拿奖逻辑：把比赛拆成小目标</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-02-history/">第2篇 · 赛题进化史与押题</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-03-build-and-architecture/">第3篇 · 整车搭建与代码框架</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-04-motor-power/">第4篇 · 电机驱动与电源地基</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-05-sensing/">第5篇 · 感知：灰度/电磁/编码器/IMU</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-06-pid-basics/">第6篇 · PID 入门：搞懂 P/I/D</a></li>
<li style="margin:.15em 0"><strong>第7篇 · PID 进阶：串级+工程补丁（本篇）</strong></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-08-pid-tuning/">第8篇 · PID 调参实战(核心)</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-09-advanced-control/">第9篇 · 进阶控制：几时该上</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-10-vision-comm/">第10篇 · K230 视觉与通信协议</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-11-architecture-fsm/">第11篇 · 状态机与整车软件</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-12-field-manual/">第12篇 · 现场作战+避坑+开源</a></li>
</ol>
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://cloudlay.cn/nuedc-car-07-pid-advanced/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>从0到1带你打电赛·小车电控篇(六)：PID 入门——从「把速度调稳」开始，一次搞懂 P、I、D</title>
		<link>https://cloudlay.cn/nuedc-car-06-pid-basics/</link>
					<comments>https://cloudlay.cn/nuedc-car-06-pid-basics/#respond</comments>
		
		<dc:creator><![CDATA[云间辞]]></dc:creator>
		<pubDate>Sun, 14 Jun 2026 17:08:33 +0000</pubDate>
				<category><![CDATA[嵌入式]]></category>
		<category><![CDATA[PID]]></category>
		<category><![CDATA[控制算法]]></category>
		<category><![CDATA[智能小车]]></category>
		<category><![CDATA[电赛]]></category>
		<category><![CDATA[调参]]></category>
		<category><![CDATA[速度环]]></category>
		<guid isPermaLink="false">https://cloudlay.cn/nuedc-car-06-pid-basics/</guid>

					<description><![CDATA[📚 本文是 「从 0 到 1 带你打电赛 · 小车电控篇」 系列（共 12 篇）第 6 篇。 第1篇 · 拿奖 [&#8230;]]]></description>
										<content:encoded><![CDATA[<div class="ds-series" style="border:1px solid #4488ff33;background:#4488ff0d;border-radius:8px;padding:.8em 1.1em;margin:1.2em 0">
<p style="margin:0 0 .5em"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4da.png" alt="📚" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 本文是 <strong>「从 0 到 1 带你打电赛 · 小车电控篇」</strong> 系列（共 12 篇）第 6 篇。</p>
<ol style="margin:.2em 0 0;padding-left:1.4em">
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-01-how-to-score/">第1篇 · 拿奖逻辑：把比赛拆成小目标</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-02-history/">第2篇 · 赛题进化史与押题</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-03-build-and-architecture/">第3篇 · 整车搭建与代码框架</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-04-motor-power/">第4篇 · 电机驱动与电源地基</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-05-sensing/">第5篇 · 感知：灰度/电磁/编码器/IMU</a></li>
<li style="margin:.15em 0"><strong>第6篇 · PID 入门：搞懂 P/I/D（本篇）</strong></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-07-pid-advanced/">第7篇 · PID 进阶：串级+工程补丁</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-08-pid-tuning/">第8篇 · PID 调参实战(核心)</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-09-advanced-control/">第9篇 · 进阶控制：几时该上</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-10-vision-comm/">第10篇 · K230 视觉与通信协议</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-11-architecture-fsm/">第11篇 · 状态机与整车软件</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-12-field-manual/">第12篇 · 现场作战+避坑+开源</a></li>
</ol>
</div>
<p>到这一篇，我们终于要碰电控里&#8221;听起来最玄、其实最值钱&#8221;的东西了——PID。</p>
<p>前面几篇，我们给小车装好了电机、电源（电机驱动与电源地基那篇），又装好了灰度、编码器、IMU 这些&#8221;感官&#8221;（感知那篇）。现在小车能动、也能&#8221;知道自己现在多快、偏了多少&#8221;，可它还差最后一口气：不会<strong>自己根据偏差去纠正</strong>。这一步，就是 PID 干的活。</p>
<p>很多人对 PID 有两种相反的误解。一种把它当高深算法，看见公式里的积分微分就头大，绕道走；另一种觉得&#8221;不就三个数嘛，乱试呗&#8221;，结果调了一下午，车还在地上画龙。这篇我们专治这两种病：先用大白话把 P、I、D 三个字母到底在干嘛讲透，建立直觉；再讲清两种最常见的写法（位置式和增量式）到底差在哪、怎么选；最后手把手带你做&#8221;人生第一次调参&#8221;——只调一个电机的速度环，从只有 P 开始，一步步加到能稳稳跟住目标速度。</p>
<div class="ds-callout ds-callout-note" style="border-left:4px solid #448aff;background:#448aff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#448aff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4dd.png" alt="📝" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 本篇只打地基</p>
<p>这一篇我们只做<strong>单环</strong>（一个电机、一个速度环），目的是建立直觉、跑通流程。真正比赛用的&#8221;方向环 + 速度环串级双环&#8221;，还有那一堆&#8221;不做就翻车&#8221;的工程补丁（积分限幅、抗饱和、微分滤波……），留到 PID 进阶那一篇专门讲。这里别贪多，先把一个环调明白，比啥都强。</p>
</div>
<h2>先搞懂一件事：什么是&#8221;负反馈&#8221;</h2>
<p>在拆 P、I、D 之前，得先有个总画面。PID 本质上是一套<strong>负反馈</strong>的纠错逻辑，一句话概括：</p>
<blockquote>
<p>拿&#8221;你想要的目标&#8221;减去&#8221;传感器测到的实际值&#8221;，得到一个<strong>偏差</strong>；再根据这个偏差算出一个<strong>控制量</strong>去纠正，让实际值往目标靠。偏差越大纠得越狠，偏差归零就收手。</p>
</blockquote>
<p>它一直在转圈：测量 → 算偏差 → 输出 → 再测量……画成图就是这样一个闭环：</p>
<figure class="ds-diagram" style="text-align:center;margin:1.3em 0"><img decoding="async" src="https://cloudlay.cn/wp-content/uploads/dianseiche/54bccefdbae0.png" alt="电赛小车电控 · 示意图" loading="lazy" style="max-width:100%;height:auto;background:#fff;border-radius:8px;padding:6px;box-shadow:0 1px 6px rgba(0,0,0,.25)"></figure>
<p>给它起几个固定名字，后面一直用：</p>
<ul>
<li><strong>目标值（setpoint）</strong>：你希望它达到的值。比如&#8221;电机转速 = 100&#8243;。</li>
<li><strong>实际值（反馈）</strong>：传感器测到的当前值。比如编码器测出来现在只有 60。</li>
<li><strong>偏差（error）</strong>：$e = \text{目标} &#8211; \text{实际}$。上面这例子 $e = 100 &#8211; 60 = 40$。</li>
<li><strong>控制量（output）</strong>：算出来要给执行机构的指令。对电机来说，就是 PWM 占空比。</li>
</ul>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 一个贯穿全篇的比喻：开车去停车线</p>
<p>你要把车停在前面一条线上。<strong>目标</strong> = 那条线的位置，<strong>实际</strong> = 你现在的位置，<strong>偏差</strong> = 还差多远，你脚下的油门/刹车 = <strong>控制量</strong>。离得远就猛踩油门，快到了松油门、点点刹车——你大脑里其实一直在跑一个&#8221;负反馈&#8221;。PID，就是把这套人脑里的本能，写成了三行可以照抄的公式。</p>
</div>
<p>而 P、I、D，就是从三个不同的时间视角去看同一个偏差：<strong>P 看现在、I 看过去、D 看未来</strong>。下面一个个拆。</p>
<h2>P：看现在——偏差多大就纠多狠（管&#8221;快&#8221;）</h2>
<p>P（Proportional，比例）是最直接的一项：<strong>当前偏差有多大，就按比例给多大的纠正力度</strong>。公式就一行：</p>
<p class="ds-math" style="overflow-x:auto">$$u_P = K_p \cdot e$$</p>
<p>$K_p$ 是你要调的比例系数。偏差大、纠正猛；偏差小、纠正轻；偏差归零，这一项也归零。</p>
<div class="ds-callout ds-callout-example" style="border-left:4px solid #7c4dff;background:#7c4dff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#7c4dff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 开车的 P</p>
<p>离停车线还有 100 米，你油门踩得很深；还剩 5 米，你只轻轻给一点。&#8221;差得越多、踩得越狠&#8221;，这就是 P。$K_p$ 就是你的&#8221;急脾气程度&#8221;——同样差 100 米，急脾气的人一脚油门到底，慢性子的人才慢慢加速。</p>
</div>
<p>P 决定了系统<strong>反应有多快、有多&#8221;硬&#8221;</strong>。这就是为什么常说 <strong>P 管&#8221;快&#8221;</strong>。</p>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> P 不是越大越好</p>
<p>$K_p$ 太小：车反应迟钝、半天到不了目标，慢吞吞。 $K_p$ 太大：纠过头。还没到目标就冲过去了，冲过去又被拉回来，来回<strong>振荡</strong>，甚至越晃越大直接<strong>发散</strong>（车失控冲出去）。 而且，光靠 P 往往会留一条尾巴——<strong>稳态误差</strong>：车最后总是差那么一丢丢，停不到线上。原因是偏差一旦变小，$K_p \cdot e$ 这个力度也跟着变小，小到刚好抵消阻力（摩擦、坡度、风），系统就&#8221;卡&#8221;在那个差一点的地方不动了。这条尾巴，得靠下面的 I 来收拾。</p>
</div>
<h2>I：看过去——把欠的账一点点补上（管&#8221;准&#8221;）</h2>
<p>I（Integral，积分）专治 P 留下的那个&#8221;差一丢丢&#8221;的尾巴。它的逻辑是：<strong>把过去每一拍的残余偏差都累加起来，攒到一定程度，逼着系统把这点欠账补上</strong>。</p>
<p class="ds-math" style="overflow-x:auto">$$u_I = K_i \cdot \sum e$$</p>
<p>只要还有偏差没消掉，这个累加和 $\sum e$ 就一直在涨，对应的纠正力度也越来越大，直到偏差真正归零它才停止增长。所以 I 是专门<strong>消除稳态误差</strong>的，决定了系统最终能不能<strong>精确命中目标</strong>——这就是 <strong>I 管&#8221;准&#8221;</strong>。</p>
<div class="ds-callout ds-callout-example" style="border-left:4px solid #7c4dff;background:#7c4dff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#7c4dff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 洗澡调水温的 I</p>
<p>你想要 40 度，水管有热损耗，光靠 P（凉了就开大热水）总是卡在 38 度上不去。这时你心里那个&#8221;老差 2 度&#8221;的不爽会一点点累积，最后促使你&#8221;再多拧一点热水&#8221;，把这 2 度补回来。这个&#8221;把长期的小遗憾累加起来、最后一并补上&#8221;的劲儿，就是 I。</p>
</div>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> I 调大的后果</p>
<p>$K_i$ 太小：欠账补得太慢，稳态误差迟迟消不掉。 $K_i$ 太大：补过头！因为它会&#8221;闷头攒&#8221;——等偏差终于归零时，前面攒下的那一大坨累加量一时撒不掉，结果<strong>冲过头、超调变大、来回振荡</strong>，整个系统反而<strong>变慢、变得不稳</strong>。 还有一个更隐蔽的坑叫<strong>积分饱和（windup）</strong>：如果偏差长期消不掉（比如车被卡住了、或者 PWM 已经顶到 100% 还不够），这个累加和会一路涨到天上去；等情况好转该收手时，这一肚子&#8221;攒下的火气&#8221;撒不掉，猛地冲过头。这个坑的解法（积分限幅、抗饱和）属于工程补丁，放到 PID 进阶那一篇细讲。本篇你只要记住一句：&#8221;<strong>I 不能贪大，攒过头会反噬</strong>&#8220;。</p>
</div>
<h2>D：看未来——看趋势提前刹车（管&#8221;稳&#8221;）</h2>
<p>D（Derivative，微分）是三项里最&#8221;聪明&#8221;的一项。它不看偏差本身有多大，而看<strong>偏差正在以多快的速度变化</strong>（这一拍的偏差减上一拍的偏差）：</p>
<p class="ds-math" style="overflow-x:auto">$$u_D = K_d \cdot (e_k &#8211; e_{k-1})$$</p>
<p>如果偏差正在飞快缩小，说明你正猛冲向目标，再不收手就要冲过头了——D 这时给一个反向的力，相当于<strong>提前点刹车</strong>，专门用来<strong>抑制超调、增加阻尼、让系统更稳</strong>。所以 <strong>D 管&#8221;稳&#8221;</strong>。</p>
<div class="ds-callout ds-callout-example" style="border-left:4px solid #7c4dff;background:#7c4dff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#7c4dff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 骑自行车的 D</p>
<p>老司机的口诀是&#8221;<strong>车身往哪倒，龙头往哪拐</strong>&#8220;。注意——你不是等真摔倒了才去救，而是看到车身<strong>正在往左倒</strong>（偏差在往一个方向变化）就提前往左打方向。这种&#8221;盯着趋势、提前动手&#8221;的预判，就是 D 的精髓。</p>
</div>
<div class="ds-callout ds-callout-danger" style="border-left:4px solid #ff1744;background:#ff174414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff1744"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> D 的最大副作用：放大噪声</p>
<p>$K_d$ 太大，会出大问题。因为 D 是对&#8221;变化量&#8221;做反应，而传感器的噪声（编码器测速的毛刺、陀螺仪角度的抖动）本身就是高频的剧烈变化——D 会把这些噪声<strong>当成真实趋势，放大成控制量的剧烈抖动</strong>，电机/舵机跟着发抖，曲线长满毛刺，越调越糟。 所以 D 一定要<strong>从 0 缓慢往上加</strong>，一旦看到曲线开始长毛刺、电机发抖，立刻停手并回调 10%~20%。如果噪声实在大，正确做法是给 D 项加个低通滤波（叫&#8221;不完全微分&#8221;），而不是一味硬压小 $K_d$——这个技巧同样留到进阶篇。 另外记牢一句：<strong>D 治不了稳态误差</strong>。偏差不变时（$e_k = e_{k-1}$），D 输出为 0，它对&#8221;卡在那不动的尾巴&#8221;完全无能为力，那是 I 的活。</p>
</div>
<h2>三个一起上：完整的 PID</h2>
<p>把三项加起来，就是完整的 PID：</p>
<p class="ds-math" style="overflow-x:auto">$$u(k) = K_p \cdot e(k) + K_i \cdot \sum e(i) + K_d \cdot [e(k) &#8211; e(k-1)]$$</p>
<p>用三个性格来记，特别好用：</p>
<table>
<thead>
<tr>
<th>项</th>
<th>看的是</th>
<th>管什么</th>
<th>性格比喻</th>
<th>调大了会</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>P</strong></td>
<td>现在的偏差</td>
<td><strong>快</strong>（响应速度、刚度）</td>
<td>急性子：差多少补多少，反应快但容易用力过猛</td>
<td>振荡、发散</td>
</tr>
<tr>
<td><strong>I</strong></td>
<td>过去的累计偏差</td>
<td><strong>准</strong>（消稳态误差）</td>
<td>记仇的人：每笔小账都记着，死磕到&#8221;一丝不差&#8221;</td>
<td>超调、积分饱和、变慢</td>
</tr>
<tr>
<td><strong>D</strong></td>
<td>偏差变化的趋势</td>
<td><strong>稳</strong>（抑制超调、阻尼）</td>
<td>预言家：看你冲太快就提前喊停</td>
<td>放大噪声、电机发抖</td>
</tr>
</tbody>
</table>
<div class="ds-callout ds-callout-important" style="border-left:4px solid #00bcd4;background:#00bcd414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bcd4"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2757.png" alt="❗" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 务必记牢：&quot;P 快、I 准、D 稳&quot;</p>
<p>网上有些资料把它写成&#8221;P 管稳、D 管快&#8221;——这是<strong>说反了</strong>。权威控制教材（密歇根 CTMS、Caltech AM08 等）的结论是统一的：增大 $K_p$ 会<strong>加快</strong>响应（减小上升时间），所以 P 管&#8221;快&#8221;；$K_d$ 增加阻尼、抑制超调、改善稳定性，所以 D 管&#8221;稳&#8221;。只有 <strong>I 管&#8221;准&#8221;</strong> 这一条三方都没争议。照 <strong>P 快 / I 准 / D 稳</strong> 来记，别被对调的版本带偏。</p>
</div>
<h2>两种写法：位置式 vs 增量式</h2>
<p>上面那个公式，是<strong>位置式</strong>的写法。工程上还有一种<strong>增量式</strong>，两者长得不一样、用途也不一样，是新手最容易混的地方。务必搞清楚，否则照抄别人代码会出大问题。</p>
<h3>位置式：直接算出&#8221;绝对的&#8221;控制量</h3>
<p>就是上面那条完整公式，它算出来的 $u(k)$ 是一个<strong>绝对值</strong>——比如&#8221;舵机应该转到 37 度&#8221;。</p>
<p class="ds-math" style="overflow-x:auto">$$u(k) = K_p \cdot e(k) + K_i \sum_{i} e(i) + K_d \cdot [e(k) &#8211; e(k-1)]$$</p>
<p>特点： &#8211; 输出直接对应执行机构的绝对位置/角度，<strong>符合&#8221;我要它在哪&#8221;这种直觉</strong>。 &#8211; 含一个<strong>全历史误差累加</strong> $\sum e$，所以容易<strong>积分饱和</strong>，必须配积分限幅。 &#8211; 一旦某次算错、或者程序重启，因为输出是绝对值，执行机构会直接<strong>跳变</strong>到那个错误位置，有点危险。</p>
<h3>增量式：只算&#8221;在上次基础上加减多少&#8221;</h3>
<p>增量式算的不是绝对值，而是<strong>这一拍相对上一拍要变化多少</strong>（增量 $\Delta u$），最后累加回去：</p>
<p class="ds-math" style="overflow-x:auto">$$\Delta u(k) = K_p[e(k)-e(k-1)] + K_i \cdot e(k) + K_d[e(k)-2e(k-1)+e(k-2)]$$</p>
<p class="ds-math" style="overflow-x:auto">$$u(k) = u(k-1) + \Delta u(k)$$</p>
<p>它只跟<strong>最近 3 拍</strong>的误差有关，不显式累加全部历史。特点： &#8211; <strong>不显式累加 $\sum e$，所以天然规避了那种&#8221;积分项无限膨胀&#8221;的经典饱和</strong>。 &#8211; 某次算错只影响一个增量，<strong>误动作影响小、更安全</strong>，也便于&#8221;手动/自动&#8221;无扰切换。 &#8211; 缺点：靠累加 $\Delta u$ 间接实现积分，可能残留一点稳态误差；而且 3 拍差分对噪声更敏感。</p>
<div class="ds-callout ds-callout-note" style="border-left:4px solid #448aff;background:#448aff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#448aff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4dd.png" alt="📝" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 别误会成&quot;绝对不会饱和&quot;</p>
<p>严格说，增量式只是<strong>不易</strong>发生显式的积分饱和，不是&#8221;绝对免疫&#8221;。当 $u(k)=u(k-1)+\Delta u$ 这个输出本身被限幅（比如 PWM 顶到 100%）时，照样会出现类似饱和的退出延迟，真较真起来工程上还是要做抗饱和处理。所以准确的说法是&#8221;<strong>天然规避显式积分饱和</strong>&#8220;，而不是&#8221;天生绝不饱和&#8221;。</p>
</div>
<h3>怎么选？</h3>
<p>记住这个比喻就够了：</p>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 油门 vs 方向盘</p>
<p><strong>位置式像报&#8221;油门踩到第几格&#8221;的绝对值</strong>——你要的就是一个绝对位置（舵机要转到某个绝对角度），适合它；但算错一次或重启就直接跳到错误档位（突变危险），还得记一路的总账（容易饱和）。 <strong>增量式像每次只说&#8221;油门再多/再少一点点&#8221;</strong>——说错一次只差一点点（影响小），不用记总账（不易饱和），对电机 PWM 做&#8221;增/减&#8221;最自然。</p>
<p>所以工程惯例是： &#8211; <strong>电机速度环 → 用增量式</strong>（通常是 PI，测速噪声大一般不加 D），对 PWM 做 <code>pwm += Δu</code> 最顺手； &#8211; <strong>舵机方向环 / 角度环 → 用位置式</strong>（通常是 PD，转向加 I 会延迟、弯道画龙，一般不加 I）。</p>
</div>
<p>这个&#8221;速度环增量式、方向环位置式&#8221;的搭配，是几乎所有智能车队的标配。本篇先把增量式速度环跑通；方向环长啥样、两者怎么串成两级，是 PID 进阶那一篇的主题。</p>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 两种写法的系数不能直接照抄</p>
<p>位置式里乘 $\sum e$ 的 $K_i$，和增量式里乘 $e(k)$ 的 $K_i$，<strong>量纲和数值完全不同</strong>（增量式里乘 $e(k)$ 的那个系数其实承担了&#8221;积分&#8221;的角色）。网上很多博客把两种式子的系数混着贴，你照抄过来会差出十万八千里。<strong>换写法 = 重新整定</strong>。</p>
</div>
<h2>上代码：两种 PID 的最小实现</h2>
<p>光看公式没感觉，看代码就清楚了。先是位置式（最容易懂的入门写法，已经带上积分限幅和输出限幅这两道保险——为什么要加、怎么加得更好，进阶篇讲，这里先照抄）：</p>
<pre><code class="language-c">// 位置式 PID：输出当作绝对量(如舵机角度)
float Position_PID(float target, float measure){
    static float integral = 0, last_err = 0;
    float err = target - measure;          // 偏差 = 目标 - 实际

    integral += err;                       // I：累加历史偏差
    // 积分限幅(防止积分饱和，进阶篇细讲)
    if(integral &gt;  INT_MAX) integral =  INT_MAX;
    if(integral &lt; -INT_MAX) integral = -INT_MAX;

    float out = Kp*err                     // P：看现在
              + Ki*integral                // I：看过去
              + Kd*(err - last_err);       // D：看未来(偏差变化)
    last_err = err;                        // 记住这次偏差，下次算 D 用

    // 输出限幅(必做!否则可能烧电机/舵机)
    if(out &gt;  OUT_MAX) out =  OUT_MAX;
    if(out &lt; -OUT_MAX) out = -OUT_MAX;
    return out;
}</code></pre>
<p>增量式（速度环典型，直接对 PWM 做增减）：</p>
<pre><code class="language-c">// 增量式 PID：输出累加到 PWM 上，适合电机速度环
float Incremental_PID(float target, float measure){
    static float e0=0, e1=0, e2=0;         // 最近三拍误差：这次/上次/上上次
    static float pwm = 0;

    e0 = target - measure;
    float dU = Kp*(e0 - e1)                 // P：这拍与上拍偏差之差
             + Ki*e0                        // I：本拍偏差
             + Kd*(e0 - 2*e1 + e2);         // D：二阶差分
    pwm += dU;                              // 在上次基础上增减

    if(pwm &gt;  PWM_MAX) pwm =  PWM_MAX;      // 输出限幅
    if(pwm &lt; -PWM_MAX) pwm = -PWM_MAX;

    e2 = e1; e1 = e0;                       // 移位，准备下一拍
    return pwm;
}</code></pre>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <code>static</code> 是关键</p>
<p>注意那几个 <code>static</code> 变量——<code>integral</code>、<code>last_err</code>、<code>e0/e1/e2</code>、<code>pwm</code>。PID 靠&#8221;记住上一次的状态&#8221;工作，这些变量必须<strong>跨函数调用保留</strong>，所以用 <code>static</code>（或者更规范地塞进一个结构体里）。新手最常犯的错就是把它们写成普通局部变量，每次进函数都清零，PID 直接废掉。</p>
</div>
<h2>第一次调参：把一个电机的速度环调稳</h2>
<p>理论讲完，最爽的来了——亲手调一次。目标极其朴素：<strong>让一个电机稳稳地跟住一个目标转速</strong>。不管你设 100 还是 200，它都能快速、平稳、不抖地达到并保持。</p>
<h3>准备工作</h3>
<ol>
<li><strong>架空轮子</strong>。把车架起来或拿一个电机单测，别让它在地上乱窜。第一次调参，安全第一，手边随时能断电。</li>
<li><strong>接通编码器测速</strong>（感知那一篇讲过：用定时器编码器模式，把&#8221;单位周期的脉冲数&#8221;当速度反馈）。</li>
<li><strong>定好控制周期</strong>。用<strong>定时器中断</strong>固定周期跑 PID，不要用 <code>delay</code> 凑！速度环常用 <strong>5~10ms</strong>（100~200Hz）。周期必须严格恒定，否则等效参数会乱漂。</li>
<li><strong>把数据发出来看</strong>。这是调参的眼睛——用 <code>printf</code> 同时发&#8221;目标值&#8221;和&#8221;实际值&#8221;两路，丢给 <a href="https://www.vofa.plus/" target="_blank" rel="noopener noreferrer">VOFA+</a> 或串口示波器画成曲线。<strong>看不到波形，调参就是瞎子摸象。</strong> 具体怎么&#8221;看波形治百病&#8221;，是 PID 调参实战那一篇的核心内容，这里你先把曲线显示出来就行。</li>
</ol>
<div class="ds-callout ds-callout-important" style="border-left:4px solid #00bcd4;background:#00bcd414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bcd4"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2757.png" alt="❗" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 控制周期决定参数量级</p>
<p>提前打个预防针：PID 系数的大小<strong>强烈依赖控制周期</strong>。粗略地说，采样 1ms 时 $K_p$ 可能在&#8221;1&#8243;这个量级，采样 500ms 时 $K_p$ 可能在&#8221;0.01&#8243;量级——差出 100 倍。所以<strong>别人的参数你不能直接照抄</strong>，只能当个&#8221;数量级参考&#8221;。下面给的数值同理，是起点不是答案。</p>
</div>
<h3>调参顺序：先 P、再 I、最后 D</h3>
<p>有句流传几十年的工程口诀，背下来准没错：</p>
<blockquote>
<p><strong>参数整定找最佳，从小到大顺序查；先是比例后积分，最后再把微分加。</strong></p>
</blockquote>
<p>为什么是这个顺序？因为 P 决定基本响应、必须先立起来；I 是在 P 的基础上去消那条残余尾巴；D 最后用来压超调和抖动。顺序乱了，三者互相干扰，你根本判断不出是谁在捣乱。</p>
<div class="ds-callout ds-callout-note" style="border-left:4px solid #448aff;background:#448aff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#448aff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4dd.png" alt="📝" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 速度环常常只用 PI</p>
<p>提前说一句：<strong>电机速度环因为测速噪声大，很多时候只用 P 和 I，不加 D</strong>（加了 D 容易把测速毛刺放大成抖动）。所以下面的流程，重点是 P 和 I；D 你可以先不上，或者最后象征性地试一点点。</p>
</div>
<h4>第一步：只调 P，找到&#8221;临界振荡&#8221;再退一档</h4>
<p>把 $K_i$、$K_d$ 都设为 <strong>0</strong>，只留 P。给一个固定目标速度（比如目标 = 100）。</p>
<ul>
<li>从一个很小的 $K_p$ 开始，<strong>逐步加大</strong>。</li>
<li>你会看到：$K_p$ 小的时候，实际转速慢吞吞地往目标爬，还到不了（有稳态误差）；</li>
<li>$K_p$ 加大，响应变快，但开始有超调（冲过头再回来）；</li>
<li>继续加大到某个值，曲线开始<strong>等幅振荡</strong>（绕着目标值上下来回、幅度既不收敛也不发散）——这个点叫<strong>临界点</strong>，记下此时的 $K_p$；</li>
<li>然后把 $K_p$ <strong>退回到临界值的 60%~70%</strong>（或者干脆乘 0.6）。这就是你的 P 起点。</li>
</ul>
<p>这套&#8221;加到临界振荡再退一档&#8221;的做法，其实就是经典的<strong>临界比例度法（Ziegler-Nichols）</strong>的核心思想——它给你一个量级正确的起点，省得你从 0 瞎试。完整的 Z-N 换算表和起步估计，放到 PID 调参实战那一篇。</p>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 往临界振荡推是有风险的</p>
<p>真把车推到等幅振荡，电机/舵机有炸机风险。所以：先低速、先架空轮子、手边随时能断电。&#8221;减半/打六折&#8221;只是个起点，后面还要配合 I、D 微调。</p>
</div>
<h4>第二步：加 I，把稳态误差那条尾巴收掉</h4>
<p>P 调好后，你的转速大概率还差一点点到不了目标（稳态误差）。现在从一个很小的 $K_i$ 开始往上加：</p>
<ul>
<li>经验起点：$K_i$ 大约取 $K_p$ 的 <strong>1/10 ~ 1/5</strong> 起步试。</li>
<li>加 I 之后观察曲线：稳态误差应该慢慢被&#8221;顶&#8221;上去，最终精确贴到目标线上。</li>
<li>$K_i$ 太小：尾巴消得太慢，半天贴不上目标。</li>
<li>$K_i$ 太大：开始超调、甚至振荡——这时往回退。</li>
</ul>
<h4>第三步（可选）：加 D，压一压超调</h4>
<p>如果加了 I 之后超调还是有点大，可以试着加一点点 D（从 0 缓慢加）来增加阻尼、压住超调。但前面说过，速度环测速噪声大，<strong>一旦看到曲线长毛刺、电机发抖，立刻停手回调</strong>。很多速度环这一步直接跳过。</p>
<p>调到这一步，你大概会在曲线上看到下面这几种典型现象，对照着调就行：</p>
<table>
<thead>
<tr>
<th>曲线现象</th>
<th>大概率是谁</th>
<th>怎么动</th>
</tr>
</thead>
<tbody>
<tr>
<td>半天爬不到目标、慢吞吞</td>
<td>$K_p$ 太小</td>
<td>加大 $K_p$</td>
</tr>
<tr>
<td>来回振荡 / 越晃越大</td>
<td>$K_p$ 太大</td>
<td>减小 $K_p$</td>
</tr>
<tr>
<td>稳稳停住，但总差一丢丢（静差）</td>
<td>缺 I / $K_i$ 太小</td>
<td>加大 $K_i$</td>
</tr>
<tr>
<td>冲过头超调大、回不来</td>
<td>$K_i$ 太大</td>
<td>减小 $K_i$，或补一点 D</td>
</tr>
<tr>
<td>曲线长毛刺、电机发抖</td>
<td>$K_d$ 太大（放大噪声）</td>
<td>减小 $K_d$，或给 D 加滤波</td>
</tr>
</tbody>
</table>
<h3>一组真实的电赛参数（仅供感受量级）</h3>
<p>光说&#8221;经验起点&#8221;太抽象，给你看一组 2024 年电赛巡线小车的<strong>真实实测参数</strong>，感受一下数量级（再强调一遍：这是<strong>别人车上的个案值，不能照抄，只能当参照</strong>）：</p>
<table>
<thead>
<tr>
<th>项目</th>
<th>取值</th>
</tr>
</thead>
<tbody>
<tr>
<td>$K_p$</td>
<td>96</td>
</tr>
<tr>
<td>$K_i$</td>
<td>0.01</td>
</tr>
<tr>
<td>$K_d$</td>
<td>1.5</td>
</tr>
<tr>
<td>主循环周期</td>
<td>20ms</td>
</tr>
<tr>
<td>PWM 频率</td>
<td>约 36kHz</td>
</tr>
<tr>
<td>基础速度</td>
<td>260</td>
</tr>
<tr>
<td>速度限幅</td>
<td>±550</td>
</tr>
</tbody>
</table>
<div class="ds-callout ds-callout-example" style="border-left:4px solid #7c4dff;background:#7c4dff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#7c4dff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 从这组数读出的门道</p>
<p>注意 $K_i$ 被压到 <strong>0.01</strong> 这么小——这说明这辆车的环路<strong>主要靠 P 和 D</strong>，积分作用极弱。原因是巡线/转向场景里，积分稍微大一点，过弯时偏差就会被累加放大，把车顶出赛道。这正好印证了前面那句：&#8221;<strong>转向/方向环里积分要非常克制，甚至不用</strong>。&#8221; （来源：<a href="https://www.cnblogs.com/laideblog/p/18842942" target="_blank" rel="noopener noreferrer">2024 电赛小车记</a>）</p>
</div>
<h3>一个调不出来时的保命心态</h3>
<p>最后讲个真事。有位作者做摄像头跟踪云台（X/Y 双轴 PID，以画面中心为目标、小球坐标为反馈），因为系统太复杂，比赛现场参数怎么都收敛不了，最后他承认&#8221;<strong>开环也未尝不可</strong>&#8220;——退一步用开环或分段控制保底，反而把任务完成了。</p>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> PID 不是万能的</p>
<p>比赛就那么四天三夜，<strong>别在调参上耗死自己</strong>。如果一个闭环死活调不收敛，退一步用更简单的开环/查表/分段控制保底，往往比硬磕一个不收敛的 PID 更靠谱。先能跑、能拿分，再谈优雅。（<a href="https://blog.csdn.net/cyaya6/article/details/132174286" target="_blank" rel="noopener noreferrer">经验来源</a>）</p>
</div>
<h2>几个新手必踩的坑（先记下，进阶篇细讲）</h2>
<p>这篇是入门，下面这些坑你<strong>先有个印象</strong>就好，真正的解法在 PID 进阶和 PID 调参实战两篇里：</p>
<ul>
<li><strong><code>static</code> 状态变量被清零</strong> → PID 失效。这是最低级也最高频的错。</li>
<li><strong>极性搞反</strong>（偏差符号或电机方向反了）→ 不是负反馈而是正反馈，车越偏越猛、直接发散。上电先用很小的增益验一下极性。</li>
<li><strong>用 <code>delay</code> 凑控制周期、或周期被中断打乱</strong> → 相当于采样忽快忽慢，等效参数乱漂，怎么调都不对。一定用定时器固定周期。</li>
<li><strong>照抄别人参数却换了周期/电机/电压</strong> → 量级完全不对。</li>
<li><strong>位置式忘了给积分限幅</strong> → 积分饱和，超调到天上。</li>
<li><strong>D 直接加大</strong> → 放大测速噪声，电机发抖。</li>
</ul>
<h2>小结</h2>
<p>这一篇，我们把 PID 从&#8221;玄学&#8221;拉回了&#8221;直觉&#8221;：</p>
<ul>
<li>它是一套<strong>负反馈纠错</strong>：目标减实际得偏差，再用三路力度去纠。</li>
<li><strong>P 看现在（管快）、I 看过去（管准）、D 看未来（管稳）</strong>——记牢是&#8221;P 快、I 准、D 稳&#8221;，别被对调的版本骗了。</li>
<li><strong>位置式</strong>输出绝对量、适合舵机/方向环；<strong>增量式</strong>输出增量、适合电机速度环，且天然规避显式积分饱和。</li>
<li>调参顺序铁律：<strong>先 P、再 I、最后 D，参数从小到大</strong>。第一次就拿一个电机的速度环练手，把曲线发到上位机上盯着调。</li>
</ul>
<p>你现在已经能让<strong>一个电机</strong>稳稳听话了。但一辆车要跑起来，光有速度还不够——它还得知道&#8221;往哪偏了、该怎么转&#8221;，这就要把<strong>方向环</strong>也加进来，让它和速度环<strong>串成两级</strong>协同工作；而真正让车在赛道上不翻不冲，还得靠一整套工程补丁。这些，我们下一篇 PID 进阶接着聊。</p>
<div class="ds-series" style="border:1px solid #4488ff33;background:#4488ff0d;border-radius:8px;padding:.8em 1.1em;margin:1.2em 0">
<p style="margin:0 0 .5em"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4da.png" alt="📚" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 本文是 <strong>「从 0 到 1 带你打电赛 · 小车电控篇」</strong> 系列（共 12 篇）第 6 篇。</p>
<ol style="margin:.2em 0 0;padding-left:1.4em">
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-01-how-to-score/">第1篇 · 拿奖逻辑：把比赛拆成小目标</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-02-history/">第2篇 · 赛题进化史与押题</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-03-build-and-architecture/">第3篇 · 整车搭建与代码框架</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-04-motor-power/">第4篇 · 电机驱动与电源地基</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-05-sensing/">第5篇 · 感知：灰度/电磁/编码器/IMU</a></li>
<li style="margin:.15em 0"><strong>第6篇 · PID 入门：搞懂 P/I/D（本篇）</strong></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-07-pid-advanced/">第7篇 · PID 进阶：串级+工程补丁</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-08-pid-tuning/">第8篇 · PID 调参实战(核心)</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-09-advanced-control/">第9篇 · 进阶控制：几时该上</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-10-vision-comm/">第10篇 · K230 视觉与通信协议</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-11-architecture-fsm/">第11篇 · 状态机与整车软件</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-12-field-manual/">第12篇 · 现场作战+避坑+开源</a></li>
</ol>
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://cloudlay.cn/nuedc-car-06-pid-basics/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>从0到1带你打电赛·小车电控篇(五)：给小车装眼睛和平衡感——灰度、电磁、编码器与 IMU</title>
		<link>https://cloudlay.cn/nuedc-car-05-sensing/</link>
					<comments>https://cloudlay.cn/nuedc-car-05-sensing/#respond</comments>
		
		<dc:creator><![CDATA[云间辞]]></dc:creator>
		<pubDate>Sun, 14 Jun 2026 17:08:32 +0000</pubDate>
				<category><![CDATA[嵌入式]]></category>
		<category><![CDATA[IMU]]></category>
		<category><![CDATA[互补滤波]]></category>
		<category><![CDATA[智能小车]]></category>
		<category><![CDATA[灰度循迹]]></category>
		<category><![CDATA[电磁循迹]]></category>
		<category><![CDATA[电赛]]></category>
		<category><![CDATA[编码器]]></category>
		<guid isPermaLink="false">https://cloudlay.cn/nuedc-car-05-sensing/</guid>

					<description><![CDATA[📚 本文是 「从 0 到 1 带你打电赛 · 小车电控篇」 系列（共 12 篇）第 5 篇。 第1篇 · 拿奖 [&#8230;]]]></description>
										<content:encoded><![CDATA[<div class="ds-series" style="border:1px solid #4488ff33;background:#4488ff0d;border-radius:8px;padding:.8em 1.1em;margin:1.2em 0">
<p style="margin:0 0 .5em"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4da.png" alt="📚" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 本文是 <strong>「从 0 到 1 带你打电赛 · 小车电控篇」</strong> 系列（共 12 篇）第 5 篇。</p>
<ol style="margin:.2em 0 0;padding-left:1.4em">
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-01-how-to-score/">第1篇 · 拿奖逻辑：把比赛拆成小目标</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-02-history/">第2篇 · 赛题进化史与押题</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-03-build-and-architecture/">第3篇 · 整车搭建与代码框架</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-04-motor-power/">第4篇 · 电机驱动与电源地基</a></li>
<li style="margin:.15em 0"><strong>第5篇 · 感知：灰度/电磁/编码器/IMU（本篇）</strong></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-06-pid-basics/">第6篇 · PID 入门：搞懂 P/I/D</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-07-pid-advanced/">第7篇 · PID 进阶：串级+工程补丁</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-08-pid-tuning/">第8篇 · PID 调参实战(核心)</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-09-advanced-control/">第9篇 · 进阶控制：几时该上</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-10-vision-comm/">第10篇 · K230 视觉与通信协议</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-11-architecture-fsm/">第11篇 · 状态机与整车软件</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-12-field-manual/">第12篇 · 现场作战+避坑+开源</a></li>
</ol>
</div>
<p>上一篇《电机驱动与电源地基》解决的是&#8221;小车的腿能不能听话地动起来&#8221;。可问题来了：腿动得再稳，要是脑子不知道&#8221;我现在偏左了还是偏右了&#8221;&#8221;我跑多快&#8221;&#8221;车头朝哪个方向&#8221;，那也只是一辆瞎跑的车。</p>
<p>这一篇我们就来给小车装上四样&#8221;感官&#8221;——灰度、电磁、编码器、IMU。它们的共同任务只有一个：<strong>给后面的控制环(PID)送上一份又干净又准确的反馈信号</strong>。打个比方，PID 是司机，感知层就是司机的眼睛、耳朵和平衡感。眼睛进了沙子，再老练的司机也得撞墙。所以这一篇虽然不讲控制算法，却是整车成败的地基里非常硬核的一块。</p>
<p>我们分四块讲：循迹的两条路线(灰度 / 电磁)、测速度的编码器、测姿态的 IMU。每一块都给你能照抄的算法和代码，最后说一句&#8221;路上怎么在它们之间切换&#8221;。</p>
<h2>一、循迹路线一：灰度——小车最常用的&#8221;看路&#8221;方式</h2>
<p>灰度循迹是绝大多数电赛/智能车小车的第一选择：在车头底部横排一组朝下的光电/灰度传感器，地面是白底黑线，传感器看到黑线就报告&#8221;我这儿压线了&#8221;。把所有传感器的信息汇总，就能算出&#8221;黑线现在偏车身中线多远&#8221;。这个偏差，就是喂给转向 PID 的核心输入。</p>
<h3>数字量阵列：加权位置法(行业标准算法)</h3>
<p>最经典、被全网开源项目反复抄的算法，叫<strong>加权位置法</strong>，来自 Pololu 的 QTR 传感器库 <a href="https://www.pololu.com/docs/0J19/3" target="_blank" rel="noopener noreferrer">readLine 算法</a>。它的思路特别好懂：</p>
<div class="ds-callout ds-callout-example" style="border-left:4px solid #7c4dff;background:#7c4dff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#7c4dff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 比喻：同学按学号站一排报到</p>
<p>把 8 个传感器想象成班里 8 个同学，按学号(0~7)站成一排。谁看到黑线谁&#8221;举手&#8221;，举手的力度(传感器读数)当权重。我们把&#8221;学号 × 力度&#8221;全加起来，再除以&#8221;总力度&#8221;，得到的就是一个平均学号——黑线现在大概压在第几号同学附近。比起只数&#8221;有没有人举手&#8221;，这能算出黑线卡在两个同学<strong>中间</strong>的位置，精细得多。</p>
</div>
<p>写成公式就是：</p>
<p class="ds-math" style="overflow-x:auto">$$\text{position} = \frac{\sum_{i} (i \times 1000) \times value_i}{\sum_{i} value_i}$$</p>
<p>其中 $i$ 是传感器索引。对 8 路传感器，这个值的范围是 $0 \sim 7000$，正中心就是 $(8-1)\times500 = 3500$。这个数随黑线左右移动单调变化，<strong>直接拿来当 PID 输入</strong>：设定值 setpoint = 3500，误差 error = position − 3500。</p>
<p>下面是改写自 QTR 库的核心代码，数字量、模拟量阵列都能用：</p>
<pre><code class="language-c">// N 路灰度阵列加权位置法，返回 0~(N-1)*1000，中心 (N-1)*500
// value[i]: 第 i 路校准后读数(压线=高值)，已做归一化
uint16_t readLinePosition(uint16_t *value, uint8_t N) {
    uint32_t avg = 0;   // 加权和 Σ(i*1000*value)
    uint32_t sum = 0;   // 权重和 Σ(value)
    static uint16_t lastPos = 0;
    bool onLine = false;
    for (uint8_t i = 0; i &lt; N; i++) {
        uint16_t v = value[i];
        if (v &gt; 200) onLine = true;        // 判定压线
        if (v &gt; 50) {                       // 噪声门限，&gt;50 才计入加权
            avg += (uint32_t)v * (i * 1000);
            sum += v;
        }
    }
    if (!onLine) {                          // 全丢线：朝上次方向找线(下面细讲)
        if (lastPos &lt; (uint32_t)(N - 1) * 1000 / 2) return 0;
        else return (N - 1) * 1000;
    }
    lastPos = avg / sum;
    return lastPos;                         // PID 输入：error = lastPos - (N-1)*500
}</code></pre>
<p>注意里面两个门限：读数 <code>&gt;50</code> 才计入加权和(滤掉噪声)，<code>&gt;200</code> 才算&#8221;真的压线了&#8221;。这是 QTR 库的经验值，能让位置计算更干净。</p>
<h3>数字量 vs 模拟量：要不要那么精细？</h3>
<p>刚才代码里的 <code>value[i]</code> 可以是两种来源：</p>
<table>
<thead>
<tr>
<th>类型</th>
<th>怎么读</th>
<th>优点</th>
<th>缺点</th>
</tr>
</thead>
<tbody>
<tr>
<td>数字量</td>
<td>每路经比较器输出 0/1(压线/没压)</td>
<td>简单、直接读 IO、抗环境光稍好、运算量小</td>
<td>分辨率低，偏差呈阶梯状</td>
</tr>
<tr>
<td>模拟量</td>
<td>直接读 ADC 原值</td>
<td>分辨率高，能算出黑线在两路中间的&#8221;亚像素&#8221;位置</td>
<td>运算量大，更怕环境光和反光</td>
</tr>
</tbody>
</table>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 比喻</p>
<p>数字量像只问&#8221;踩到线了吗？是/否&#8221;；模拟量像问&#8221;你离线多近？打个 0~100 分&#8221;。后者能算出更细的偏差，但也更容易被路灯、反光骗到。</p>
</div>
<p>模拟量有个必做的预处理：<strong>归一化</strong>。每一路传感器单独记下自己在纯黑、纯白时的最大最小值，运行时把读数折算到 0~100。这能消除各路传感器的个体差异，换场地也只要重测一遍。</p>
<pre><code class="language-c">// 标定阶段：扫黑白记录每路 min[i]/max[i]
// 运行阶段：归一化到 0~100
uint8_t normalize(uint16_t raw, uint16_t mn, uint16_t mx) {
    if (raw &lt;= mn) return 0;
    if (raw &gt;= mx) return 100;
    return (uint8_t)((uint32_t)(raw - mn) * 100 / (mx - mn));
}</code></pre>
<h3>工程四件套：&#8221;实验室能跑、赛场翻车&#8221;的分水岭</h3>
<p>灰度循迹真正拉开差距的，不是那个加权公式，而是下面这四件事。</p>
<p><strong>① 黑白阈值，一定要现场标定。</strong> 别用上次的、别用实验室的。赛场的光照、地面材质、纸张反光都和你家不一样，固定阈值一换场就误判。</p>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 2022 年那届的&quot;翻车现场&quot;</p>
<p>那年好几支队伍的小车在赛场识别/循迹翻车——实验室明明能跑，一到赛场就不行。根因几乎都是同一个：赛场环境和实验室差太多，参数没现场重标。(<a href="https://blog.csdn.net/qq_47652105/article/details/127036370" target="_blank" rel="noopener noreferrer">来源</a>)</p>
<p>基础做法：上电后让阵列扫过真实赛道的纯黑线和纯白底，采集每路 min/max，阈值取中值。</p>
</div>
<p>进阶一点，可以上 <strong>OTSU 大津法</strong>做动态阈值，免去手动标定：</p>
<pre><code class="language-c">// 非线性映射，增强黑线区分度(减小传感器个体差异)
adc[a] = pow((adc[a] / 10.0), 3) / 15000;
// OTSU：遍历灰度级找"最大类间方差"对应的灰度作阈值
// 类间方差 = p0 * p1 * (m0 - m1)^2   (p0/p1 两类概率，m0/m1 两类均值)
// 阈值限幅 100 &lt;= threshold &lt;= 800
output[i] = (input[i] &lt;= threshold) ? 1 : 0;     // 二值化</code></pre>
<p>有作者用 OTSU + 三次方非线性映射，在 <strong>9 片不同材质、不同光照、带污渍褶皱的场地</strong>上测试全部正常，还省掉了上电黑白标定这一步(<a href="https://blog.csdn.net/2301_78814366/article/details/150100225" target="_blank" rel="noopener noreferrer">来源</a>)。</p>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 比喻：标定就像考试前定及格线</p>
<p>不同试卷(场地)难度不同，你得先看看这次黑题白题大概多少分，把及格线划在中间，而不是死守上次的 60 分。OTSU 就是自动帮你找&#8221;最能把黑、白分成两堆&#8221;的那条线。</p>
</div>
<p><strong>② 全丢线，绝不能把 error 清零。</strong> 这是新手最容易踩的命案。</p>
<div class="ds-callout ds-callout-danger" style="border-left:4px solid #ff1744;background:#ff174414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff1744"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 全丢线清零 = 直冲出弯</p>
<p>丢线大多发生在急弯切出赛道的那一瞬间。此时最后那个偏差方向，恰恰就是你该回去找线的方向。如果把 error 归零，车就以为&#8221;前方笔直&#8221;，一头冲出去。</p>
<p>正确做法(就是上面代码里那段)：丢线时<strong>记住上一周期的偏差极性</strong>，朝原来弯道的方向打死方向去重新抓线。再补一条超时保护：长时间持续丢线就减速甚至停车倒找，别让车原地打转。</p>
</div>
<div class="ds-callout ds-callout-example" style="border-left:4px solid #7c4dff;background:#7c4dff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#7c4dff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 比喻</p>
<p>好比你过弯时眼睛突然闭了一下，正确做法是&#8221;刚才在往左拐就继续往左找路&#8221;，而不是&#8221;看不见了就直着冲&#8221;——直冲必然冲出赛道。</p>
</div>
<p><strong>③ 十字/直角路口，必须特判。</strong> 经过十字时多路传感器会同时压线，加权位置会突然跳变、把偏差算飞，车就猛打一下方向。处理办法：8 路里有 <strong>≥3 路同时压线</strong>就判为路口，进入&#8221;路口模式&#8221;——这时候不用常规偏差了，而是靠状态机让车直行/盲走固定一段(配合编码器或惯导计程)穿过去，过完再恢复巡线。一位省赛一等奖选手就是这么干的，并强调&#8221;换赛道、换车都得重标重调，没有万能参数&#8221;(<a href="https://blog.csdn.net/zhengnianli/article/details/106684237" target="_blank" rel="noopener noreferrer">来源</a>)。</p>
<p><strong>④ 安装高度与前瞻，是个三方妥协。</strong> 前瞻 = 传感器有效检测点离车头多远。</p>
<ul>
<li>前瞻<strong>远</strong> → 高速直道更稳，能提前预判弯道，但急弯容易提前切弯、切到弯内；</li>
<li>前瞻<strong>近</strong> → 急弯跟得紧，但高速时容易发抖。</li>
</ul>
<p>经验做法：<strong>直道用远前瞻，弯道用近前瞻</strong>，可以动态切换(灰度阵列靠&#8221;用哪几路算偏差&#8221;来实现切换，切换点要带迟滞，否则在直弯交界来回切会抖)。</p>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 恩智浦智能车光电组的纠结</p>
<p>一份技术报告里，底盘高度反复纠结：太低上坡蹭赛道、过障被顶住；太高重心高、高速过弯侧翻；前瞻调太远又有噪点。最后只能在&#8221;不蹭赛道&#8221;的前提下尽量压低，再配俯仰角控制前瞻。(<a href="https://bj.bcebos.com/cdstm-hyetecforthesmartcar-bucket/d641c155ba4444649c662287c241d3a9.pdf" target="_blank" rel="noopener noreferrer">来源</a>)</p>
<p>一句话：高度和前瞻都不是越大越好，是<strong>重心稳定性、过障能力、识别准确率</strong>三者的折中。</p>
</div>
<h3>关于 TCRT5000 红外光电</h3>
<p>入门玩具车上最常见的就是 TCRT5000 这种离散红外反射光电。它是红外发射二极管(波长 950nm) + 光敏三极管，集成了日光阻断滤光片(<a href="https://www.vishay.com/docs/83760/tcrt5000.pdf" target="_blank" rel="noopener noreferrer">Vishay 官方手册</a>)。关键参数：</p>
<ul>
<li>峰值工作距离 <strong>2.5mm</strong>，有效范围 0.2~15mm，最佳 0.2~6.5mm；</li>
<li>供电 3.3~5V，配 LM393 比较器整形输出数字量。</li>
</ul>
<div class="ds-callout ds-callout-danger" style="border-left:4px solid #ff1744;background:#ff174414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff1744"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> TCRT5000 必须贴地，这是它最大的硬约束</p>
<p>离地太远，反射光变弱，会把白地误判成&#8221;黑&#8221;。所以循迹时模块要贴到 <strong>2~8mm</strong>(贴近峰值 2.5mm 最好)。调试口诀：<strong>没遇黑线时指示灯应长亮，一旦灯灭就是遇到黑线了。</strong> 安装高度不一致会导致各路灵敏度不同，要逐路调电位器。</p>
</div>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 比喻</p>
<p>TCRT5000 就像用手电照地面看反光，离太远光散了，白地黑地都看不清，必须凑很近(几毫米)才分得清黑白。</p>
</div>
<p><strong>选型小结</strong>：TCRT5000 离散红外便宜、易上手，但单点少、贴地要求严、抗环境光差，适合入门；集成灰度阵列(如 8 路 IIC 数字阵列、模拟量阵列)一致性好、分辨率高、部分带自标定，是电赛/智能车的主流；摄像头方案前瞻最远、能识别十字数字等复杂元素，但算力和调试成本高(留到《K230 视觉与通信协议》一篇展开)。<strong>2024/2025 电赛 H 题(自动行驶小车)的主流一等奖方案，就是 8 路灰度 + 惯导(主控 MSPM0G3507)</strong>，不是电磁，这点后面会再强调。</p>
<h3>灰度循迹的调参口诀(转向环)</h3>
<p>偏差信号算出来后，喂给转向 PID。这里先给经验，完整的调参流程见《PID 调参实战》一篇。</p>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 灰度转向调参铁律</p>
<ol>
<li><strong>先把基础速度降到很低</strong>(最大的 30~50%)，Ki = Kd = 0，只加 Kp 直到能跟线；</li>
<li>升速，加 Kd 抑制振荡和过弯超调；</li>
<li><strong>Ki 在循迹转向环通常保持 0</strong>——转向是&#8221;位置跟随&#8221;，P 就能消静差，Ki 多余且容易引发振荡和过冲。</li>
</ol>
</div>
<p>有个 Arduino 5 路 PD 循迹的实测起点可以参考：Kp=15、Ki=0、Kd=200、基础速度 200(PWM 0~255)，5.14m 的赛道跑约 5 秒(<a href="https://znzcqy.github.io/post/I9lb3ilye/" target="_blank" rel="noopener noreferrer">来源</a>)。</p>
<p>还有一条容易被忽视：<strong>偏差信号要先低通滤波再算微分</strong>。灰度原始读数有抖动，直接微分会被 Kd 放大，车就发抖。经验做法：</p>
<pre><code class="language-c">adc_err = 0.4 * err + 0.6 * adc_err;   // 一阶低通，新值权重 α≈0.3~0.5</code></pre>
<p>注意滤波别太狠：α 太小会引入滞后，让弯道响应变慢——这是前瞻和滤波之间的又一个权衡。</p>
<div class="ds-callout ds-callout-danger" style="border-left:4px solid #ff1744;background:#ff174414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff1744"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 灰度循迹必查的几个坑</p>
<ul>
<li><strong>偏差极性 / 电机加减号接反</strong> → 车不回正反而越跑越偏，直接冲出去。上电第一件事：手推车体，确认 error 符号和 left/right 混合方向匹配。</li>
<li>用实验室/上次的阈值上赛场 → 误判。每次新场地重标或用 OTSU。</li>
<li>十字不特判 → 偏差算飞，急打方向。</li>
<li>偏差不滤波直接微分 → 高速发抖。</li>
<li>Ki 盲目加在转向环 → 过弯过冲、画龙。</li>
</ul>
</div>
<h2>二、循迹路线二：电磁——蒙眼跟着 20kHz 小调走</h2>
<p>有些赛题(尤其智能车竞赛电磁组)不是白底黑线，而是在赛道中心埋一根漆包线，通上 <strong>20kHz、约 100mA</strong> 的正弦交变电流，产生交变磁场。车上用工字电感去感应这个磁场——离导线越近，感应出的电压越大。</p>
<div class="ds-callout ds-callout-example" style="border-left:4px solid #7c4dff;background:#7c4dff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#7c4dff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 比喻：蒙眼跟着哼歌的人走</p>
<p>地上那根线一直&#8221;哼着&#8221;20kHz 的歌，车的两只&#8221;耳朵&#8221;(左右电感)谁听得响就说明离谁近，靠左右耳的响度差，就能判断车往哪边偏了。</p>
</div>
<p>电磁循迹在电赛里用得不算多(主要是智能车竞赛电磁组的常态)，但思路很经典，而且 TI MSPM0 芯片在这上面有独特优势，值得了解。</p>
<h3>信号链：从 30mV 微弱交流到一个能用的数</h3>
<p>单个 10mH 工字电感在离线上方 15~20cm 处感应出的原始信号，只有大约 <strong>30mV 的 20kHz 小交流</strong>——这么弱、这么高频，单片机根本没法直接读。必须走完整的四级信号链：</p>
<figure class="ds-diagram" style="text-align:center;margin:1.3em 0"><img decoding="async" src="https://cloudlay.cn/wp-content/uploads/dianseiche/49e1d9cd30f7.png" alt="电赛小车电控 · 示意图" loading="lazy" style="max-width:100%;height:auto;background:#fff;border-radius:8px;padding:6px;box-shadow:0 1px 6px rgba(0,0,0,.25)"></figure>
<ol>
<li><strong>LC 并联谐振选频</strong>：只放大 20kHz，抑制其它频率的干扰。10mH 电感配 6.8nF 电容，谐振频率约 19.3kHz(理论上 20kHz 对应 6.33nF，工程上取就近标称值 6.8nF，刻意略低一点适配寄生电容与公差)。</li>
<li><strong>运放放大约 100 倍</strong>：把 30mV 抬到伏级(STM32 不超 3.3V)。</li>
<li><strong>二极管检波</strong>(常用肖特基 BAT54S)：把 20kHz 交流的包络/峰值整流成直流。</li>
<li><strong>RC 低通滤波</strong>：平滑后送 ADC。</li>
</ol>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 两个比喻串起来</p>
<ul>
<li><strong>LC 谐振选频像调收音机台</strong>：周围一堆杂音(各种磁场)，LC 只把 20kHz 这个台调清楚放大，别的拧小声。</li>
<li><strong>检波整流像把抖动的水面读成水位</strong>：电感信号是 20kHz 高速抖动的交流(像水波纹)，二极管 + 电容只看波纹有多高(包络)，变成稳定的&#8221;水位&#8221;给单片机读。</li>
</ul>
</div>
<h3>核心算法：差比和 (L−R)/(L+R)</h3>
<p>电感读到值之后，最关键、最该记住的算法就是<strong>差比和</strong>：</p>
<p class="ds-math" style="overflow-x:auto">$$\text{偏差} = \frac{L &#8211; R}{L + R}$$</p>
<p>为什么不直接用 L−R？因为整体场强会漂移——电流波动、不同赛道，会让所有电感的读数整体涨 10%，这时候位置没变，L−R 却跟着变了。而差比和的分子分母同比例缩放，比值不变，所以偏差只反映<strong>位置</strong>，不受幅值影响。</p>
<div class="ds-callout ds-callout-example" style="border-left:4px solid #7c4dff;background:#7c4dff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#7c4dff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 比喻：按比例打分，而不是绝对分</p>
<p>全班整体多考了 10 分(信号变强)，但你比同桌高出的相对名次没变。除以总分看相对位置，就不会被整体高低带偏——这就是抗幅值漂移。</p>
</div>
<p>工程上还会先把每个通道<strong>归一化到 0~100</strong>(value/max×100)，方便统一设阈值、识别环岛/十字等特殊元素。下面是真实电磁组代码里的双重差比和：</p>
<pre><code class="language-c">// 归一化(每通道 value/max*100)
Left_Adc  = (adc_deal_last[LEFT_1]*100) / adc_max[0];
Right_Adc = (adc_deal_last[RIGHT_1]*100) / adc_max[1];
// 双重差比和求偏差 AD_Bias
AD_Bias = ((Left_Adc - Middle_Adc)*100/(Left_Adc + Middle_Adc))
        - ((Right_Adc - Middle_Adc)*100/(Right_Adc + Middle_Adc));</code></pre>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 标定别贪满幅</p>
<p>12 位 ADC(0~4096)实测一般不超 3600。标定时让电感最大值落在量程的 <strong>70~90%</strong>，<strong>绝不要调到满幅</strong>——满幅会削顶饱和、丢分辨率，还给特殊元素的极值留不出空间。</p>
</div>
<h3>电感布局：成绩的隐形变量</h3>
<p>电感怎么摆，直接影响直道稳不稳、弯道灵不灵：</p>
<table>
<thead>
<tr>
<th>布局</th>
<th>特点</th>
</tr>
</thead>
<tbody>
<tr>
<td>一字横排</td>
<td>简单、直道稳，但对弯道/特殊元素弱</td>
</tr>
<tr>
<td>八字向外 V</td>
<td>两端差异大，对弯道敏感，外侧可取极值识别元素</td>
</tr>
<tr>
<td>双横 + 竖直(双 T)</td>
<td>综合直弯，竖直电感对正上方磁场敏感，用于环岛/坡道识别</td>
</tr>
<tr>
<td>双 45°</td>
<td>对称性好，但要求左右运放/电感一致性极高，否则跑偏</td>
</tr>
</tbody>
</table>
<p>布局要点：相邻电感间距 <strong>≥2cm</strong>，避免互感串扰。</p>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 第 16 届电磁越野组的教训</p>
<p>木地板测试时编码器脉冲不稳，根因竟是机械连杆张力过大 + 电机振动没约束——软件再调参也救不回，这是硬件缺陷。该队还放弃了 45° 布局，因为硬件对称性不足、左右运放灵敏度不一致导致左右不对称。(<a href="https://www.cnblogs.com/Tayoou/p/15257616.html" target="_blank" rel="noopener noreferrer">来源</a>)</p>
<p>一句话：机械固定、左右对称性、硬件一致性是地基。先把电感装牢、左右配对一致，再谈调参。</p>
</div>
<h3>MSPM0 的独门优势：片上运放</h3>
<p>前面那条信号链里，&#8221;运放放大&#8221;通常要在板外焊 LM358/OPA4377 之类的运放。但 <strong>TI MSPM0 芯片内部就自带运放</strong>——比如 MSPM0G3507 含 2 个 OPA，MSPM0L13xx 含 2 个零漂斩波运放 + 1 个 GPAMP，可编程增益(PGA)1~32 倍(单级不够可两级 cascade)，输出能直连片上高速 12 位 ADC。</p>
<div class="ds-callout ds-callout-example" style="border-left:4px solid #7c4dff;background:#7c4dff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#7c4dff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 比喻</p>
<p>MSPM0 的片上运放就像芯片自带放大镜：本来要在板外焊运放当放大镜放大微弱信号，现在 MCU 内部就有 2 个高质量(零漂、抗噪)放大镜，信号不出芯片就放大完直接给 ADC——省料、省地方，还更干净。</p>
</div>
<p>这意味着电磁前端的选频信号放大可以做进芯片里，省掉外置运放，减 BOM、减焊接、减板上噪声。零漂斩波运放尤其适合这种微弱信号(低失调、低 1/f 噪声)。配置用 TI 的 SysConfig 图形化完成，官方有 <code>opa_signal_chain_to_adc</code> 等例程。不过要注意：MSPM0 跑纯电磁的完整开源还比较少(多为灰度)，建议自己验证片内运放增益够不够，不够就级联或再外置一级。</p>
<div class="ds-callout ds-callout-important" style="border-left:4px solid #00bcd4;background:#00bcd414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bcd4"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2757.png" alt="❗" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 别把 2024 电赛 H 题当电磁准备</p>
<p>H 题&#8221;自动行驶小车&#8221;限定用 MSPM0(常见 G3507)，<strong>主流一等奖方案是 8 路灰度 + MPU6050 + 位置式 PID，不是电磁循迹。</strong> 电磁 + 差比和 + 工字电感主要是智能汽车竞赛电磁组的玩法，别搞混了准备方向。</p>
</div>
<h3>电磁的几个高频坑</h3>
<div class="ds-callout ds-callout-danger" style="border-left:4px solid #ff1744;background:#ff174414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff1744"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 危险</p>
<ul>
<li><strong>电机 PWM 频率别落在 20kHz 附近</strong>——开关噪声会被 LC 选频电路一并放大，污染信号。取 13~19kHz 错开，或给模拟前端独立 LDO 供电 + 屏蔽 + 物理远离电机。</li>
<li><strong>不做归一化直接用 L−R</strong> → 信号涨落时偏差漂移，不同赛道表现不一(差比和就是解决这个)。</li>
<li><strong>相邻电感太近(&lt;2cm)</strong> → 互感串扰。</li>
<li><strong>舵机环加大 I</strong> → 积分饱和、弯道滞后画龙；微分项不滤波会放大 ADC 噪声抖舵。</li>
</ul>
</div>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 排查电磁链路：用示波器自下而上</p>
<p>很多&#8221;算法跑不动&#8221;其实是前端没出波形(电感虚焊/谐振没对/运放没起振)。正确顺序是：先确认信号源和示波器正常，把电感贴近电磁线量 10mH 引脚看有没有 20kHz 正弦，再逐级往后量放大、检波的输出。<strong>先怀疑硬件波形，别一上来怀疑代码。</strong> (<a href="https://blog.csdn.net/weixin_45523734/article/details/111141670" target="_blank" rel="noopener noreferrer">来源</a>)</p>
</div>
<h2>三、编码器：知道自己跑多快</h2>
<p>循迹解决了&#8221;往哪走&#8221;，编码器解决&#8221;跑多快&#8221;。增量编码器输出 A、B 两路正交方波(相位差 90°)，转得越快、脉冲越密；正转反转，A、B 的超前关系相反。</p>
<h3>首选读法：定时器编码器模式</h3>
<p>STM32 的定时器有个硬件&#8221;编码器模式&#8221;，<strong>强烈推荐用它</strong>：硬件自动加减计数、自动判方向，CPU 几乎零开销。</p>
<p>在 CubeMX 里把 TIMx 的 Encoder Mode 设成 <strong>&#8220;TI1 and TI2&#8221;</strong>(即 4 倍频，Encoder Mode 3)，Prescaler = 0，ARR 设 65535(16 位)或 0xFFFFFFFF(32 位定时器，<strong>强烈推荐用 32 位的 TIM2/TIM5 防溢出</strong>)。</p>
<div class="ds-callout ds-callout-example" style="border-left:4px solid #7c4dff;background:#7c4dff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#7c4dff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 比喻：4 倍频就像数火车数得更细</p>
<p>只数车头(2 倍频 = A、B 各数一边)不如车头车尾都数(4 倍频 = A、B 的上升沿 + 下降沿都数)。同样一列火车，你数出 4 倍的格子，测得更精细。</p>
</div>
<p>读速度的代码非常简洁——每个固定控制周期(常用 5ms 或 10ms)读一次计数差值：</p>
<pre><code class="language-c">// CubeMX: TIMx -&gt; Encoder Mode = "TI1 and TI2"(4倍频), Prescaler=0, ARR=65535/0xFFFFFFFF
HAL_TIM_Encoder_Start(&amp;htim2, TIM_CHANNEL_ALL);   // 启动编码器接口

// 每个固定控制周期(5/10ms)调用一次，返回"单位周期脉冲数"作速度反馈
int16_t Encoder_GetSpeed(void) {
    static int16_t last = 0;
    int16_t cur   = (int16_t)__HAL_TIM_GET_COUNTER(&amp;htim2); // 读计数
    int16_t delta = cur - last;   // signed 差值自动吃掉 65535/0 回绕(单周期增量&lt;32767)
    last = cur;
    return delta;                 // 正/负即正反转，数值即速度反馈
}</code></pre>
<h3>关键心法：单位周期脉冲数，直接当速度用</h3>
<p>很多新手卡在&#8221;怎么把脉冲换算成 m/s&#8221;。其实<strong>不用换算</strong>——那个 <code>delta</code>(单位周期内的脉冲数)本身就能直接当速度反馈喂进 PID。</p>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 比喻：数电线杆</p>
<p>不用换算成公里每小时，就像你每 10 秒数一次窗外过了多少根电线杆，杆数本身就代表快慢——数越多车越快，直接拿这个数去踩油门/松油门就行。</p>
</div>
<p>如果报告里需要物理速度，再换算也不迟：</p>
<p class="ds-math" style="overflow-x:auto">$$\text{RPM} = \frac{\Delta CNT \times (1000 / \text{period\_ms})}{\text{PPR} \times 4 \times \text{减速比}}$$</p>
<p>线速度再乘轮周长即可。采样周期是个权衡：太短，ΔCNT 太小，量化噪声大；太长，响应慢。智能车常用 5ms 或 10ms(弱一点的电机推荐 10ms)。</p>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 16 位定时器溢出，靠 signed 差值法自动解决</p>
<p>CNT 到 65535 会回绕到 0。代码里 <code>int16_t delta = cur - last;</code> 这一句，利用了无符号回绕的特性，即便跨越 65535/0 边界也能给出正确的带符号增量——<strong>只要单周期增量不超过 ±32767。</strong> 长距离里程累加要用 int32/int64。</p>
</div>
<p>还有一种读法是用 EXTI 外部中断在 A/B 边沿软件计数(M 法数脉冲 / T 法测间隔)，更灵活，但占 CPU、高速易丢脉冲。<strong>结论：有硬件编码器接口时优先用定时器模式</strong>，EXTI 只在引脚/定时器不够时备选。</p>
<div class="ds-callout ds-callout-danger" style="border-left:4px solid #ff1744;background:#ff174414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff1744"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 编码器必查</p>
<ul>
<li><strong>A/B 接反或电机正负接反</strong> → 反馈方向和控制方向相反，闭环变正反馈，越控越偏。先开环验证：手转轮子正转，读数应增大。</li>
<li><strong>dt 不固定</strong>(用 HAL_Delay 或主循环计时) → 速度 = Δ脉冲/dt 和积分项全失真，PID 怎么调都不稳。<strong>编码器采样和 PID 控制周期，要用同一个硬件定时中断严格定时。</strong> 中断里只采集 + 置标志，别做浮点/打印，计算放主循环。</li>
</ul>
</div>
<h2>四、IMU：给小车装上平衡感和方向感</h2>
<p>IMU(常用 MPU6050)是六轴传感器：三轴加速度计 + 三轴陀螺仪。它能告诉小车两件事——<strong>姿态</strong>(Pitch/Roll，平衡车直立要用)和<strong>航向</strong>(Yaw，走直线锁方向要用)。但这两件事的难度天差地别。</p>
<h3>姿态融合：互补滤波(必会)</h3>
<p>加速度计和陀螺仪各有各的毛病，得互补着用：</p>
<ul>
<li><strong>加速度计</strong>：能算出绝对的倾角，但噪声大、抖；</li>
<li><strong>陀螺仪</strong>：积分出来的角度很平滑，但会慢慢漂移。</li>
</ul>
<div class="ds-callout ds-callout-example" style="border-left:4px solid #7c4dff;background:#7c4dff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#7c4dff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 比喻：开会时该多听谁</p>
<p>加速度计像个诚实但话痨手抖的人(说的方向对，但一直抖)；陀螺仪像个嗓门稳但会慢慢跑题的人(短期稳，但越说越偏)。互补滤波就是：开会时多听那个稳的人(权重 0.98)，偶尔用诚实的人把话题拉回正轨(0.02)，综合出又稳又准的答案。</p>
</div>
<p>互补滤波公式：</p>
<p class="ds-math" style="overflow-x:auto">$$\text{angle} = \alpha \times (\text{angle} + \text{gyro} \times dt) + (1-\alpha) \times \text{acc\_angle}$$</p>
<p>$\alpha$ 取 0.95~0.98(陀螺权重高)。加速度算角：$\text{pitch} = \text{atan2}(a_y, \sqrt{a_x^2 + a_z^2})$。代码：</p>
<pre><code class="language-c">// 一阶互补滤波：融合加速度(绝对但抖)与陀螺(平滑但漂)，求 Pitch/Roll
// alpha=0.98(陀螺权重)，dt=控制周期(秒)
float comp_pitch = 0;
void IMU_Update(float ax, float ay, float az, float gy, float dt) {
    float acc_pitch = atan2f(ay, sqrtf(ax*ax + az*az)) * 57.2958f; // 加速度算角(度)
    comp_pitch = 0.98f*(comp_pitch + gy*dt) + 0.02f*acc_pitch;     // 互补融合
}</code></pre>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> α 怎么取</p>
<p>α 越大越信任陀螺(更平滑但跟随慢)，越小越信任加速度(更跟手但更抖)。小车机械振动大就适当加大 α 滤掉加速度噪声。一般直接取 0.95~0.98 就行。<strong>dt 千万记得用秒(0.005)，别用 ms 或采样次数</strong>，否则融合比例完全错。</p>
</div>
<p>进阶可以上<strong>一阶卡尔曼滤波</strong>(平衡车经典 TKJ 版)，效果更平滑，标准参数 Q_angle=0.001、Q_bias=0.003、R_measure=0.03、dt=0.005~0.01。它本质上就是&#8221;会自动调权重的互补滤波&#8221;——发现加速度计这会儿可信就多听它，发现它在抖就少听它。代价是计算量大。对资源受限的单片机，互补滤波通常已经够用，追求极致平滑再上卡尔曼。</p>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 卡尔曼 = 自适应增益的互补滤波</p>
<p>两者本质相似。互补滤波省、收敛快、对小车足够；卡尔曼平滑但费算力。别一上来就堆卡尔曼，先把互补滤波吃透。&#8221;什么时候才值得上进阶滤波&#8221;，留到《进阶控制算法》一篇细聊。</p>
</div>
<h3>Yaw(航向)：六轴 IMU 的一个大坑</h3>
<p>姿态(Pitch/Roll)有加速度计当绝对参考，能纠偏。但 <strong>Yaw 不行</strong>——水平面内的旋转，加速度计提供不了任何参考(重力始终竖直)。所以 Yaw 只能靠陀螺 Z 轴积分：<code>yaw += gyro_z * dt</code>，<strong>必然随时间漂移</strong>。这是六轴 IMU 的固有局限(想要绝对航向，得上九轴加磁力计，或者用外部基准纠偏)。</p>
<div class="ds-callout ds-callout-example" style="border-left:4px solid #7c4dff;background:#7c4dff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#7c4dff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 比喻：闭着眼睛数步子估方向</p>
<p>陀螺仪测的是转得多快(角速度)，要靠不停累加才知道现在朝哪。就像闭着眼睛靠数步子估自己面朝哪个方向，走久了一点点误差攒起来，最后以为朝北其实朝东北——这就是漂移。</p>
</div>
<p>对策有两步。</p>
<p><strong>第一步：开机静止采零偏。</strong> 陀螺仪静止时本该读 0，但实际总有个固定的偏心读数。上电后让车<strong>绝对静止</strong>，采 N 次(常 500~2000，1000 次很常见)陀螺 Z 读数求平均当零偏，运行时每个读数都减掉它。</p>
<div class="ds-callout ds-callout-example" style="border-left:4px solid #7c4dff;background:#7c4dff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#7c4dff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 比喻</p>
<p>就像体重秤没人站时也显示 0.3kg。你先记住这个 0.3，以后每次读数都减掉它，才是真实角速度。</p>
</div>
<pre><code class="language-c">// 开机静止采陀螺 Z 轴零偏 + 运行时积分 Yaw(相对航向)
float gz_bias = 0, yaw = 0;
void Gyro_CalibZ(void) {            // 上电后车保持静止时调用
    float sum = 0;
    for (int i = 0; i &lt; 1000; i++){ sum += MPU_GetGyroZ(); HAL_Delay(1); }
    gz_bias = sum / 1000.0f;        // 零偏 = 静止读数均值
}
void Yaw_Update(float dt){          // 每个控制周期调用
    float gz = MPU_GetGyroZ() - gz_bias;   // 去零偏
    yaw += gz * dt;                 // 积分得相对航向(短时漂移可忽略)
}</code></pre>
<div class="ds-callout ds-callout-danger" style="border-left:4px solid #ff1744;background:#ff174414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff1744"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 采零偏时车必须绝对静止</p>
<p>有人手扶、地面震动，都会把运动量误算进零偏，运行时车就持续缓慢自转/跑偏。而且采一次零偏<strong>也只能减小、不能消除</strong>漂移(温漂、随机游走仍在)，长时间运行 Yaw 仍会缓慢漂。</p>
</div>
<p><strong>第二步：走直线用&#8221;相对航向&#8221;，随时重置。</strong> 既然绝对 Yaw 会漂，那就不依赖它的绝对值——每段直线起步时把当前 yaw <strong>清零</strong>当目标，航向误差 err = 0 − yaw 做 PID，输出叠加到左右轮差速：</p>
<pre><code class="language-c">// 走直线锁航向：起步时 yaw=0(清零当目标)，err = 0 - yaw，差速纠偏
// out = Kp*err - Kd*gz;   pwm_L = base + out;   pwm_R = base - out;</code></pre>
<p>注意上面那行的 D 项直接用了陀螺角速度 <code>gz</code>(对测量微分，比对误差微分更干净、抗扰)。转 90°：把目标航向加/减 90，闭环自动转到位后再清零，进入下一段直线。再高级一点可以做串级：外环用航向角锁朝向、内环用角速度求跟手——不过对入门小车，上面这套单环 PD 已经够稳。</p>
<div class="ds-callout ds-callout-example" style="border-left:4px solid #7c4dff;background:#7c4dff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#7c4dff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 比喻</p>
<p>不管罗盘(绝对 Yaw)准不准，起步时直接把现在朝向定为 0°当基准，之后谁偏了就往回掰——就像走路时盯着正前方一个目标走，而不是依赖会跑偏的指南针。</p>
</div>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 2024 电赛 H 题的航向实战</p>
<p>团队发现单靠 MPU6050 积分航向 + 编码器里程，长距离会累积误差，温漂还让 Yaw 越漂越大。解决：用实时温度算比例系数做<strong>动态温漂补偿</strong>，把漂移压到 ±1 以内，并用 8 路灰度纠偏兜底。有的队甚至换成维特智能陀螺仪(约 0.05° 精度)替代 MPU6050(约 2°/s 零漂)来提升转向精度。(<a href="https://blog.csdn.net/weixin_60991529/article/details/141832409" target="_blank" rel="noopener noreferrer">来源</a>)</p>
<p>一句话：纯惯性导航(IMU + 编码器)长距离必然累积误差，要么加外部基准(灰度/视觉)纠偏，要么温漂补偿 + 定期重置，别指望积分一路准到底。</p>
</div>
<div class="ds-callout ds-callout-danger" style="border-left:4px solid #ff1744;background:#ff174414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff1744"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> IMU 必查</p>
<ul>
<li><strong>零偏采集没静止</strong> → 运行时持续自转。</li>
<li><strong>以为采一次零偏就永久消漂</strong> → 温漂仍在，必须相对航向 + 重置。</li>
<li><strong>dt 单位忘换成秒</strong> → 融合/积分比例全错。</li>
<li><strong>方向环只用绝对 Yaw 单环且不重置</strong> → 漂移被当成真实偏差，车被慢慢带偏。</li>
</ul>
</div>
<h2>五、把感官接到一起：分工、串级与切换</h2>
<p>四样感官各管一摊，整车里它们这样配合：</p>
<figure class="ds-diagram" style="text-align:center;margin:1.3em 0"><img decoding="async" src="https://cloudlay.cn/wp-content/uploads/dianseiche/c2cc35986a38.png" alt="电赛小车电控 · 示意图" loading="lazy" style="max-width:100%;height:auto;background:#fff;border-radius:8px;padding:6px;box-shadow:0 1px 6px rgba(0,0,0,.25)"></figure>
<p>记住两条分工原则(串级双环的落地代码与调参顺序，见《PID 进阶》和《状态机与整车软件》两篇)：</p>
<ul>
<li><strong>速度环(编码器反馈)用增量式 PID</strong>——只输出 ΔPWM、自带积分、无饱和切换冲击，适合连续调速；</li>
<li><strong>方向/航向环(灰度/电磁/IMU 反馈)用位置式 PID(实践中多为 PD)</strong>——适合&#8221;锁住某个朝向/位置&#8221;。</li>
</ul>
<div class="ds-callout ds-callout-example" style="border-left:4px solid #7c4dff;background:#7c4dff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#7c4dff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 比喻：开车时手脚分工</p>
<p>方向环是你的手(管方向、外环、慢一点)，速度环是你的脚(管油门、内环、快一点)。手脚分工又配合，车才能又快又直。</p>
</div>
<p>至于&#8221;什么路段用哪个感官&#8221;，核心思路是<strong>分工 + 兜底</strong>：</p>
<ul>
<li><strong>巡线</strong>主要靠灰度/电磁给偏差；</li>
<li><strong>直道锁航向</strong>用 IMU 的相对航向，防止灰度细微抖动累积成跑偏；</li>
<li><strong>十字/丢线</strong>时灰度偏差不可信，切到 IMU + 编码器&#8221;盲走&#8221;几格穿过去；</li>
<li>一旦灰度长时间丢线，<strong>有外部基准(重新抓到的灰度线)就纠偏、没有就靠惯导兜底</strong>——绝不让 error 清零直冲。</li>
</ul>
<div class="ds-callout ds-callout-success" style="border-left:4px solid #00c853;background:#00c85314;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00c853"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 这一篇你该带走的</p>
<ul>
<li>灰度循迹核心 = <strong>加权位置法</strong> + 工程四件套(现场标定 / 全丢线记忆方向 / 十字特判 / 前瞻高度妥协)。</li>
<li>电磁循迹核心 = <strong>LC 选频→放大→检波→ADC</strong> 信号链 + <strong>差比和 (L−R)/(L+R)</strong> 抗幅值漂移；MSPM0 片上运放能省外置前端。</li>
<li>编码器 = <strong>定时器编码器模式 4 倍频</strong>，单位周期脉冲数直接当速度，signed 差值吃掉溢出。</li>
<li>IMU = 加速度 + 陀螺<strong>互补滤波</strong>求姿态；<strong>Yaw 必漂</strong>，靠开机采零偏 + 相对航向 + 随时重置来锁直线。</li>
<li>不管哪种感官，<strong>dt 要严格定时、极性先开环验证、参数现场重标并留约 20% 裕度</strong>——这是&#8221;实验室能跑、赛场也能跑&#8221;的分水岭。</li>
</ul>
</div>
<p>感官齐了、反馈干净了，下一步就是真正让控制环动起来——从最基础的&#8221;把一个电机的速度调稳&#8221;开始，一次搞懂 P、I、D 各自在管什么。我们下一篇 PID 入门见。</p>
<div class="ds-series" style="border:1px solid #4488ff33;background:#4488ff0d;border-radius:8px;padding:.8em 1.1em;margin:1.2em 0">
<p style="margin:0 0 .5em"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4da.png" alt="📚" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 本文是 <strong>「从 0 到 1 带你打电赛 · 小车电控篇」</strong> 系列（共 12 篇）第 5 篇。</p>
<ol style="margin:.2em 0 0;padding-left:1.4em">
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-01-how-to-score/">第1篇 · 拿奖逻辑：把比赛拆成小目标</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-02-history/">第2篇 · 赛题进化史与押题</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-03-build-and-architecture/">第3篇 · 整车搭建与代码框架</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-04-motor-power/">第4篇 · 电机驱动与电源地基</a></li>
<li style="margin:.15em 0"><strong>第5篇 · 感知：灰度/电磁/编码器/IMU（本篇）</strong></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-06-pid-basics/">第6篇 · PID 入门：搞懂 P/I/D</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-07-pid-advanced/">第7篇 · PID 进阶：串级+工程补丁</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-08-pid-tuning/">第8篇 · PID 调参实战(核心)</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-09-advanced-control/">第9篇 · 进阶控制：几时该上</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-10-vision-comm/">第10篇 · K230 视觉与通信协议</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-11-architecture-fsm/">第11篇 · 状态机与整车软件</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-12-field-manual/">第12篇 · 现场作战+避坑+开源</a></li>
</ol>
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://cloudlay.cn/nuedc-car-05-sensing/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>从0到1带你打电赛·小车电控篇(四)：让电机听话——电机驱动、PWM 与电源（电控地基）</title>
		<link>https://cloudlay.cn/nuedc-car-04-motor-power/</link>
					<comments>https://cloudlay.cn/nuedc-car-04-motor-power/#respond</comments>
		
		<dc:creator><![CDATA[云间辞]]></dc:creator>
		<pubDate>Sun, 14 Jun 2026 17:08:32 +0000</pubDate>
				<category><![CDATA[嵌入式]]></category>
		<category><![CDATA[DRV8701]]></category>
		<category><![CDATA[PWM]]></category>
		<category><![CDATA[TB6612]]></category>
		<category><![CDATA[智能小车]]></category>
		<category><![CDATA[电机驱动]]></category>
		<category><![CDATA[电源]]></category>
		<category><![CDATA[电赛]]></category>
		<guid isPermaLink="false">https://cloudlay.cn/nuedc-car-04-motor-power/</guid>

					<description><![CDATA[📚 本文是 「从 0 到 1 带你打电赛 · 小车电控篇」 系列（共 12 篇）第 4 篇。 第1篇 · 拿奖 [&#8230;]]]></description>
										<content:encoded><![CDATA[<div class="ds-series" style="border:1px solid #4488ff33;background:#4488ff0d;border-radius:8px;padding:.8em 1.1em;margin:1.2em 0">
<p style="margin:0 0 .5em"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4da.png" alt="📚" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 本文是 <strong>「从 0 到 1 带你打电赛 · 小车电控篇」</strong> 系列（共 12 篇）第 4 篇。</p>
<ol style="margin:.2em 0 0;padding-left:1.4em">
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-01-how-to-score/">第1篇 · 拿奖逻辑：把比赛拆成小目标</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-02-history/">第2篇 · 赛题进化史与押题</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-03-build-and-architecture/">第3篇 · 整车搭建与代码框架</a></li>
<li style="margin:.15em 0"><strong>第4篇 · 电机驱动与电源地基（本篇）</strong></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-05-sensing/">第5篇 · 感知：灰度/电磁/编码器/IMU</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-06-pid-basics/">第6篇 · PID 入门：搞懂 P/I/D</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-07-pid-advanced/">第7篇 · PID 进阶：串级+工程补丁</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-08-pid-tuning/">第8篇 · PID 调参实战(核心)</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-09-advanced-control/">第9篇 · 进阶控制：几时该上</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-10-vision-comm/">第10篇 · K230 视觉与通信协议</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-11-architecture-fsm/">第11篇 · 状态机与整车软件</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-12-field-manual/">第12篇 · 现场作战+避坑+开源</a></li>
</ol>
</div>
<div class="ds-callout ds-callout-note" style="border-left:4px solid #448aff;background:#448aff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#448aff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4dd.png" alt="📝" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 系列说明</p>
<p>这是「从0到1带你打电赛·小车电控篇」第四篇。前三篇我们聊了拿奖逻辑、赛题套路和整车框架，从这篇开始动真格——先把地基打牢。</p>
</div>
<p>你有没有遇到过这种情况：代码逻辑明明没问题，PID 也照着教程抄了，可车一上场要么嗡嗡乱叫不走直线，要么跑着跑着自己复位重启，再要么瓷砖地上飞快、一到赛场木地板就趴窝？</p>
<p>十有八九，问题不在你的算法，而在脚下这块&#8221;地基&#8221;——电机、驱动和电源。</p>
<p>这是赛后总结里被反复念叨的一句话：<strong>硬件不过关，软件再强也救不回来</strong>。控制算法是建在反馈信号和稳定供电之上的，地基一塌，上面盖得再漂亮也是空中楼阁。所以这一篇我们不碰高深的控制理论，专心把三件事讲透：<strong>电机怎么选、驱动怎么接、电源怎么供</strong>。</p>
<h2>先认识小车的&#8221;肌肉&#8221;：电机怎么选</h2>
<p>电机就是小车的肌肉。肌肉不行，神经系统（你的控制算法）指挥得再精妙也白搭。电赛小车上最常见的是两类带编码器的直流减速电机。</p>
<h3>N20 和 GA12-N20：两个常客</h3>
<p><strong>N20</strong> 是最小巧的那一类，微型直流减速电机，适合轻量级小车。</p>
<ul>
<li>额定 3-6V（工作范围 2-9V），体积小、重量轻</li>
<li>带霍尔编码器，电机轴典型 <strong>7 PPR</strong>（每转 7 个脉冲）</li>
<li>通常是 6 线制：电机两根（M+ / M-）、编码器供电两根（VCC / GND）、编码器信号两根（A / B 两相）</li>
</ul>
<p>这里有个<strong>新手最容易接错的地方</strong>：编码器的供电（3.3-5V 逻辑电平）和电机的供电（走功率轨）是<strong>分开</strong>的两路，别图省事接到一起。</p>
<p>举个具体型号让你有数：Waveshare 那款 12V/200rpm 的 N20，减速比 1:150，电机轴 7 PPR，那么输出轴每圈就是 $7 \times 150 = 1050$ 个脉冲，再经过后面要讲的四倍频，每圈能数到 4200 个计数。这个分辨率对测速来说相当够用了。</p>
<p><strong>GA12-N20</strong> 是它的&#8221;健身版&#8221;——同样是 12mm 直径，但换成了金属齿轮箱，更耐用、扭矩更大。</p>
<ul>
<li>减速比可选范围极广，1:3 到 1:1000 都有，常用的是 1:30 / 1:50 / 1:100 / 1:150</li>
<li>拿 GA12-N20-0100 举例：DC 12V、空载 100rpm、负载 80rpm、额定力矩 2kg·cm、额定电流 300mA</li>
<li>同款电压下转速参考：6V 约 95rpm，3V 约 47rpm（基本随电压线性变化）</li>
</ul>
<p>N20 塑齿轻便但扭矩弱，GA12 金属齿耐造扭矩大。需要爬坡、负载重、或者怕齿轮磨损的车，优先 GA12。</p>
<h3>减速比怎么选：像自行车换挡</h3>
<p>减速比这个参数最让人犯迷糊，其实一个比喻就懂了。</p>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 减速比 = 自行车的变速挡位</p>
<p>大减速比 = 低速挡：蹬起来轻松（扭矩大），能爬坡，但跑不快； 小减速比 = 高速挡：平路飞快（转速高），但起步费劲、爬坡没劲。</p>
<p>减速比越大，输出轴转速越低、扭矩越大，运转还越静音。</p>
</div>
<p>选型的思路是<strong>从你的目标反推</strong>，分两路算：</p>
<ol>
<li><strong>看顶速</strong>：目标线速度 = 轮子周长 × 输出轴每秒转数。先定下你想要的最大线速度（比如 1.5 m/s），再用轮径折算出需要的输出轴 rpm，倒推减速比。</li>
<li><strong>看扭矩</strong>：车有多重、要不要爬坡、起步要不要猛，决定了你需要多大扭矩。负载重、要爬坡就往大减速比走。</li>
</ol>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 减速比是个权衡，两头都别走极端</p>
<p>比太小：转速够了但扭矩不足，爬不动坡、起步无力； 比太大：扭矩管够但顶速上不去，到了拼用时的发挥部分就吃亏。 实在拿不准，宁可让动力<strong>留点余量</strong>——下面那个&#8221;瓷砖地能跑、木地板趴窝&#8221;的惨案，根子就是动力没余量。</p>
</div>
<h3>编码器：电机的&#8221;码表&#8221;，靠它测速</h3>
<p>编码器是装在电机尾部的转速传感器，相当于自行车的码表。它输出 A、B 两相方波，相位差 90°。我们用 STM32 定时器的<strong>编码器模式</strong>来读它，并且做<strong>正交四倍频</strong>。</p>
<p>四倍频是什么意思？AB 两相每个周期里有 4 个跳变沿（A 上升、B 上升、A 下降、B 下降），我们每个沿都数一次，分辨率就翻了 4 倍。所以：</p>
<p class="ds-math" style="overflow-x:auto">$$\text{每圈计数} = \text{PPR} \times \text{减速比} \times 4$$</p>
<p>测速的办法很直接：在固定周期的定时器中断里，读一次 <code>TIMx-&gt;CNT</code>，算出这段时间里计数增加了多少（Δ脉冲），就是当前转速的反馈值。低速场景脉冲太少不准，可以用 M/T 法改进，但入门阶段固定周期测增量就够了。</p>
<div class="ds-callout ds-callout-danger" style="border-left:4px solid #ff1744;background:#ff174414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff1744"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 一个真实的翻车案例：问题根本不在软件</p>
<p>某队省赛失利。车在实验室瓷砖地上能跑到 2m/s，一到赛场木地板就跑不起来，十字路口冲不出去。队员死磕软件、反复改 PID，毫无效果。</p>
<p>最后扒出来的根因是<strong>机械</strong>：连接杆插得太紧，产生了过盈摩擦；电机没固定牢，一跑起来就抖。结果编码器脉冲抖动严重，反馈信号全是噪声，PID 自然整不稳。</p>
<p>教训有三条，条条用血换的： 1. 软件很难弥补硬件短板，编码器的机械安装（同心、牢固、无过盈摩擦）是闭环精度的前提； 2. 一定要在<strong>比赛同款地面</strong>上调参，别只在瓷砖上测； 3. 选电机要给动力留余量，别按光滑地面的极限去卡。</p>
<p>来源：<a href="https://www.cnblogs.com/Tayoou/p/15257616.html" target="_blank" rel="noopener noreferrer">Tayoou 的调车总结</a></p>
</div>
<h2>驱动芯片：给肌肉接上&#8221;功放&#8221;</h2>
<p>MCU 的 GPIO 引脚只能输出几毫安的小电流，电机一启动就要几百毫安甚至几安培——直接连上去，引脚瞬间烧掉。中间必须有个&#8221;功率放大器&#8221;，这就是驱动芯片。</p>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 驱动芯片像音响里的功放</p>
<p>你的手机（MCU）输出的是微弱的音频信号，推不动大音箱。中间要接一个功放（驱动芯片），把小信号放大成能推动音箱（电机）的大功率。功放的好坏，直接决定声音（动力）干不干净、够不够劲。</p>
</div>
<p>驱动芯片内部的核心结构是 <strong>H 桥</strong>——四个开关搭成的电路。</p>
<div class="ds-callout ds-callout-example" style="border-left:4px solid #7c4dff;background:#7c4dff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#7c4dff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> H 桥像四个开关搭的跷跷板</p>
<p>对角的两个开关闭合，电流从一个方向流过电机，正转；切换到另一对开关，电流反向，反转。靠&#8221;哪两个开关导通&#8221;来控制方向，靠&#8221;开关闭合时间的占比&#8221;来控制速度。</p>
</div>
<h3>选型：按电机的堵转电流分档</h3>
<p>选驱动芯片，最关键的指标是<strong>电机堵转电流</strong>（电机被卡死时的最大电流，远大于正常工作电流）。芯片的电流能力必须扛得住堵转，否则一卡就烧。下面这张表是电赛圈里的主流选择：</p>
<table>
<thead>
<tr>
<th>芯片</th>
<th>电压范围</th>
<th>电流能力</th>
<th>特点</th>
<th>适用场景</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>TB6612FNG</strong></td>
<td>VM 2.5~13.5V</td>
<td>连续 1.2A / 峰值 3.2A</td>
<td>双 H 桥 MOS，集成度高，自带过热/欠压保护</td>
<td>小电机、入门首选</td>
</tr>
<tr>
<td><strong>DRV8701</strong></td>
<td>6.5~45V</td>
<td>取决于外置 MOS</td>
<td>栅极驱动 + 外置 4 颗 N-MOS，宽压免升压</td>
<td>大电流、大电机、智能车主流</td>
</tr>
<tr>
<td><strong>A4950</strong></td>
<td>8~40V</td>
<td>峰值 3.5A</td>
<td>单 H 桥，两个电机要两片</td>
<td>中大电流</td>
</tr>
<tr>
<td><strong>DRV8833</strong></td>
<td>2.7~10.8V</td>
<td>1.5A RMS / 2A 峰值</td>
<td>TB6612 平替，电流偏弱</td>
<td>小功率</td>
</tr>
<tr>
<td><strong>L298N</strong></td>
<td>3~48V</td>
<td>连续 2A / 峰值 3A</td>
<td>BJT 老工艺，发热严重</td>
<td><strong>不推荐</strong></td>
</tr>
</tbody>
</table>
<p>缺货时，国产的 GC8871、BDR 系列可以做 TB6612/同类的平替。</p>
<p><strong>小电机选 TB6612，大电机选 DRV8701</strong>，这是两条最常走的路。下面分别说说它俩。</p>
<h3>TB6612：小电机的省事之选</h3>
<p>TB6612 把&#8221;指挥&#8221;和&#8221;开关&#8221;都集成在一颗芯片里，外围简单，特别适合 N20 这类小电机。引脚和真值表必须记牢：</p>
<ul>
<li><strong>PWMA / PWMB</strong>：调速（接 PWM 信号）</li>
<li><strong>AIN1 / AIN2 / BIN1 / BIN2</strong>：控制方向</li>
<li><strong>STBY</strong>：待机脚，<strong>必须置高</strong>芯片才工作</li>
<li><strong>VM</strong>：电机电源；<strong>VCC</strong>：逻辑电源（2.7~5.5V）</li>
</ul>
<p>真值表（以 A 通道为例）：</p>
<table>
<thead>
<tr>
<th>AIN1</th>
<th>AIN2</th>
<th>PWMA</th>
<th>状态</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>0</td>
<td>给占空比</td>
<td>正转</td>
</tr>
<tr>
<td>0</td>
<td>1</td>
<td>给占空比</td>
<td>反转</td>
</tr>
<tr>
<td>0</td>
<td>0</td>
<td>—</td>
<td>短路刹车（急停）</td>
</tr>
<tr>
<td>1</td>
<td>1</td>
<td>—</td>
<td>滑行（自由停）</td>
</tr>
</tbody>
</table>
<div class="ds-callout ds-callout-danger" style="border-left:4px solid #ff1744;background:#ff174414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff1744"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 新手最常见的坑：只给方向不给 PWM，电机纹丝不动</p>
<p>TB6612 的输出 AO1/AO2 <strong>必须有 PWM 输入才有效</strong>。很多人只接了 AIN1/AIN2 设好方向，PWM 占空比却忘了给（或者给了 0），然后对着不动的电机怀疑人生。记住：方向脚定方向，PWM 脚给动力，<strong>两者缺一不可</strong>。</p>
<p>顺带一个接地建议：GND 最好一路接电源地、一路接单片机地，做&#8221;单点共地&#8221;（后面电源章节细讲）。</p>
</div>
<p>下面是 STM32 HAL 库下 TB6612 调速 + 换向的核心代码，PWM 配成 24kHz：</p>
<pre><code class="language-c">// 占空比 = (Pulse+1)/(ARR+1)
// 72MHz 主频, Prescaler=2, ARR=999  -&gt;  PWM 频率 = 72M/3/1000 = 24kHz
HAL_TIM_PWM_Start(&amp;htim3, TIM_CHANNEL_2);          // 启动 PWM 通道

// 设置转向：BIN1=1, BIN2=0 -&gt; 正转
HAL_GPIO_WritePin(BIN1_GPIO_Port, BIN1_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(BIN2_GPIO_Port, BIN2_Pin, GPIO_PIN_RESET);

// 改占空比就是调速：pulse 越大越快（0 ~ ARR）
__HAL_TIM_SET_COMPARE(&amp;htim3, TIM_CHANNEL_2, pulse);</code></pre>
<p>代码思路参考 <a href="https://c.miaowlabs.com/A25.html" target="_blank" rel="noopener noreferrer">喵呜实验室的 TB6612 教程</a>。</p>
<p>这里引出一个非常好用的编程抽象：<strong>带符号速度</strong>。我们希望上层 PID 算出一个数，正数就前进、负数就后退、绝对值就是快慢，不用每次都手动管方向。封装一下：</p>
<pre><code class="language-c">#define ARR_MAX  999      // 与 PWM 的 ARR 对应, 占空比上限

// speed: 带符号速度, 正前进负后退, 范围 [-ARR_MAX, ARR_MAX]
void Motor_SetSpeed(int16_t speed)
{
    if (speed &gt;= 0) {
        // 正转
        HAL_GPIO_WritePin(BIN1_GPIO_Port, BIN1_Pin, GPIO_PIN_SET);
        HAL_GPIO_WritePin(BIN2_GPIO_Port, BIN2_Pin, GPIO_PIN_RESET);
    } else {
        // 反转
        HAL_GPIO_WritePin(BIN1_GPIO_Port, BIN1_Pin, GPIO_PIN_RESET);
        HAL_GPIO_WritePin(BIN2_GPIO_Port, BIN2_Pin, GPIO_PIN_SET);
        speed = -speed;                               // 取绝对值当占空比
    }
    if (speed &gt; ARR_MAX) speed = ARR_MAX;             // 输出限幅, 别超 ARR
    __HAL_TIM_SET_COMPARE(&amp;htim3, TIM_CHANNEL_2, speed);
}</code></pre>
<p><strong>方向 GPIO + 占空比 = 带符号速度</strong>，这个封装一做，后面写 PID 的人就只管甩一个数过来，世界清静了。</p>
<h3>DRV8701：大电流场景的指挥官</h3>
<p>当你用 RS380 这类大电机、或者要跑高速大扭矩时，TB6612 那点电流就不够看了。DRV8701 是另一种思路——它本身<strong>不直接驱动电机</strong>，而是一个&#8221;栅极驱动器&#8221;：</p>
<div class="ds-callout ds-callout-example" style="border-left:4px solid #7c4dff;background:#7c4dff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#7c4dff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> DRV8701 是指挥，MOS 是干活的大力士</p>
<p>TB6612 把指挥（控制逻辑）和小开关（MOS）都塞进一颗芯片，省事但电流小。DRV8701 只当&#8221;指挥官&#8221;，负责发号施令（驱动栅极），真正扛大电流的四颗 N 沟道 MOS 管放在芯片外面。开关分离出去，所以能扛远比集成芯片大的电流。</p>
</div>
<p>它的几个亮点：</p>
<ul>
<li><strong>宽压 6.5~45V，免升压</strong>：直接吃高压电池，不用额外的 BOOST 升压电路，也就避免了升压带来的干扰（不同手册标注略有出入，这里取保守的 6.5~45V）</li>
<li><strong>内置电流采样</strong>，放大增益 $A_v = 20\,\text{V/V}$</li>
<li><strong>定时关断斩波限流</strong>：能给电机设一个电流上限，堵转时自动斩波保护</li>
<li>支持 100% 占空比</li>
</ul>
<p>它有两种工作模式，接线时要分清：</p>
<ul>
<li><strong>E 型</strong>：PH/EN 模式。EN 进 PWM（调速），PH 给方向。一路 PWM 一路电平，省 IO。</li>
<li><strong>P 型</strong>：双 PWM 模式。IN1/IN2 各接一路 PWM。</li>
</ul>
<p>斩波限流的电流上限怎么算：</p>
<p class="ds-math" style="overflow-x:auto">$$I_{chop} = \frac{V_{REF}}{A_v \times R_{sense}}$$</p>
<p>其中 $A_v = 20\,\text{V/V}$ 是内部放大增益，$R_{sense}$ 是你外接的采样电阻，$V_{REF}$ 是你设的参考电压。</p>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> DRV8701 的采样电阻和 VREF 别配错</p>
<p>如果 $R_{sense}$ 或 $V_{REF}$ 配错，斩波限流点会被设得过低，结果就是电机带不动、或者动不动就触发限流。算的时候把 $A_v = 20$ 这个增益代准。另外 VM 引脚就近并一个 0.1uF 陶瓷电容 + 大 bulk 电容（下一章讲为什么）。</p>
</div>
<p>立创开源上有一个现成的参考设计 <a href="https://oshwhub.com/yy28/smart-car-drv8701-dual-drive-ope" target="_blank" rel="noopener noreferrer">smart-car-drv8701-dual-drive（yy28）</a>，6-45V 宽压、堵转仍工作、信号与地平面隔离，可以克隆工程改板用（注意页面暂无 BOM，MOS 和采样电阻要自己核对）。</p>
<h3>L298N 为什么劝退</h3>
<p>你可能在很多老教程里见过 L298N，但<strong>正式比赛真心不推荐</strong>：</p>
<ul>
<li>它是 <strong>BJT 老工艺</strong>，导通时饱和压降大，发热严重，必须挂大散热片</li>
<li><strong>没有寄生体二极管</strong>，得外接 8 个续流二极管（电机是感性负载，关断时会产生反向电压尖峰，需要二极管给它一条泄放路径），电路又乱又占地方</li>
<li>效率低，同样的电池跑不了多久</li>
</ul>
<p>相比之下，TB6612、DRV8833、A4950 这些 MOS 方案导通损耗低、自带续流、效率高。L298N 只在你手头恰好有现成模块、临时凑合时用用，别当主力。</p>
<div class="ds-callout ds-callout-danger" style="border-left:4px solid #ff1744;background:#ff174414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff1744"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> MOS 冒烟的排查故事</p>
<p>有参赛队遇到驱动 MOS 下管冒烟。他们没有盲目重焊，而是拿示波器<strong>逐级测信号链</strong>：单片机 PWM 口 → 栅极驱动输入 → 栅极驱动输出 → MOS 的 Vgs，一段段往下看信号在哪儿断的，迅速定位了故障。</p>
<p>经验：驱动故障要顺着信号链逐级排查；MOS 要选内阻低、性能稳的；用栅极驱动芯片把 MCU 和 MOS 隔离开，<strong>防止 MOS 击穿后大电流倒灌烧掉单片机</strong>；电机端加 TVS 抑制换向尖峰。来源：<a href="https://blog.csdn.net/qq_41954556/article/details/123611226" target="_blank" rel="noopener noreferrer">电磁组调车记录</a></p>
</div>
<h2>PWM 调速：为什么频率定在 15~20kHz</h2>
<p>我们一直说&#8221;占空比调速&#8221;，到底什么原理？</p>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> PWM 调速像快速开关灯</p>
<p>灯一直开着太亮（全速）。你飞快地开-关-开-关，并改变&#8221;开&#8221;占的时间比例（占空比），眼睛感受到的是平均亮度。电机也一样——你高频地通断电源，它感受到的是<strong>平均电压</strong>。占空比 50%，等效于给了它一半的电压，转一半的速。</p>
</div>
<p>占空比的定义很简单：</p>
<p class="ds-math" style="overflow-x:auto">$$\text{占空比} = \frac{T_{on}}{T_{on} + T_{off}}$$</p>
<p>那频率（一秒钟开关多少次）选多少？这里有个非常实际的考量——<strong>人耳的听觉范围是 20Hz ~ 20kHz</strong>。</p>
<p>如果你的 PWM 频率落在这个范围内（比如常见的几 kHz），电机线圈会跟着这个频率机械振动，你就听到了刺耳的&#8221;嗡嗡&#8221;啸叫声。把频率推到 20kHz 以上，超出人耳听觉，世界瞬间安静。</p>
<div class="ds-callout ds-callout-success" style="border-left:4px solid #00c853;background:#00c85314;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00c853"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 实测对照，非常直观</p>
<p>有同学实测：PWM 设 <strong>15kHz 明显啸叫</strong>，提到 <strong>22kHz 啸叫消失、电机静音</strong>。这就是为什么默认推荐把频率定在 <strong>15~20kHz</strong>，尤其 18~20kHz 是个甜点——刚好踩在可听范围的边缘外。</p>
</div>
<p>但也不是越高越好：</p>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 频率太高的代价</p>
<p>频率越高，MOS 的开关次数越多，<strong>开关损耗增大</strong>（发热），同时<strong>EMI（电磁干扰）</strong>也更强，会干扰旁边的传感器。所以 15~20kHz 是个三方平衡：避开啸叫、控制损耗、压住 EMI。</p>
<p>另外，频率还要<strong>避开车身的机械共振点</strong>，否则某个频率下整车会异常抖动。</p>
</div>
<p>一个特别要注意的特殊情况，留给电磁循迹组：</p>
<div class="ds-callout ds-callout-danger" style="border-left:4px solid #ff1744;background:#ff174414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff1744"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 电磁组：PWM 会干扰你的电感传感器</p>
<p>有电磁循迹队伍发现，电机 PWM 对采集磁场的电感传感器产生了明显的电磁干扰，信号脏得没法用。他们把 PWM 频率压到 <strong>13-19kHz</strong> 折中后，干扰明显减小。</p>
<p>教训：传感器密集的车，要让 PWM 频率和传感器采集&#8221;错开&#8221;，频率不是越高越好，得兼顾啸叫、损耗、EMI 三者。来源：<a href="https://blog.csdn.net/qq_41954556/article/details/123611226" target="_blank" rel="noopener noreferrer">电磁组记录</a></p>
</div>
<p>如果你用的是<strong>互补 PWM</strong>（上下桥臂用一对互补信号驱动），还有一条铁律：</p>
<div class="ds-callout ds-callout-danger" style="border-left:4px solid #ff1744;background:#ff174414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff1744"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 互补 PWM 必加死区，否则炸管</p>
<p>上下桥臂的 MOS 一个开一个关。如果切换时上管还没完全关、下管就开了，两个管子同时导通，电源直接对地短路（叫&#8221;直通 / shoot-through&#8221;），瞬间大电流炸 MOS。</p>
<p>解决办法是加<strong>死区时间</strong>——在上管关、下管开之间留一小段两个都不导通的空档，数百 ns 到 1us 量级，随 MOS 开关速度定。注意：相同死区在不同载波频率下，能达到的最大占空比不一样，所以死区要和频率配套设。</p>
</div>
<h2>电源树：给整车&#8221;供血&#8221;</h2>
<p>到这儿肌肉（电机）和功放（驱动）都有了，最后一块地基是供电。电源问题最隐蔽——它不会让你的代码报错，而是让你的车在最关键的时候<strong>莫名其妙复位、串口乱码、ADC 跳变</strong>，查到怀疑人生。</p>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 电源树像自来水系统</p>
<ul>
<li><strong>电池 = 水库</strong>：高压大流量，直接供给电机这个&#8221;大用户&#8221;；</li>
<li><strong>DC-DC（Buck 降压）= 小区水泵房</strong>：把电压降到家用的 5V；</li>
<li><strong>LDO = 水龙头前的精滤</strong>：把电（水）滤干净，降到 3.3V 给娇贵的 MCU 喝。</li>
</ul>
<p>关键是：<strong>给 MCU 单独接一根管子</strong>，别让洗衣机（电机）用水时把你喝水的水流冲乱。</p>
</div>
<h3>电源树的标准结构</h3>
<p>一条典型的供电链路是这样分级的：</p>
<figure class="ds-diagram" style="text-align:center;margin:1.3em 0"><img decoding="async" src="https://cloudlay.cn/wp-content/uploads/dianseiche/d68e05afcb0c.png" alt="电赛小车电控 · 示意图" loading="lazy" style="max-width:100%;height:auto;background:#fff;border-radius:8px;padding:6px;box-shadow:0 1px 6px rgba(0,0,0,.25)"></figure>
<p>几条要点：</p>
<ol>
<li><strong>电池直供电机功率轨（VM）</strong>：电机要大电流，让它直接吃电池，别经过 DC-DC（DC-DC 带不动那么大瞬时电流）。</li>
<li><strong>DC-DC（Buck）降到 5V</strong>：给舵机、5V 传感器用。TPS 系列是常见选择，效率高。</li>
<li><strong>MCU 务必走独立 LDO 降到 3.3V</strong>。LDO 噪声低、外围简单、输出干净，特别适合给敏感的 MCU 和 ADC 基准供电（缺点是压差大时效率低、发热，所以只用它做最后一级精滤，不要拿它扛大电流降压）。</li>
</ol>
<div class="ds-callout ds-callout-danger" style="border-left:4px solid #ff1744;background:#ff174414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff1744"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 为什么 MCU 一定要独立 LDO</p>
<p>如果图省事让 MCU 和电机/驱动共用一路电源，电机的电流噪声会顺着电源线灌进 MCU，导致<strong>死机、串口乱码、ADC 采样不准</strong>。这种问题极其难查，因为代码完全正常。一路独立 LDO 也就几毛钱，能省掉你几个小时的 debug。</p>
</div>
<h3>地线规划：单点共地</h3>
<p>电源里最玄学、也最容易翻车的，是<strong>地线</strong>。</p>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 单点共地像班里只设一个&quot;班长&quot;当基准</p>
<p>全班都以班长为统一基准（共地），但<strong>只能有一条主线</strong>连到班长那里。不能你拉我、我拉他绕成一个圈（地环路），否则电机这个&#8221;大嗓门&#8221;一吼，MCU 这个&#8221;听写的&#8221;就把数听错了。</p>
</div>
<p>具体怎么做：<strong>电机功率地</strong>和 <strong>MCU/传感器控制地</strong>各自单独走线，最后在电源入口附近用<strong>一个点</strong>（0Ω 电阻或磁珠）汇接到一起。</p>
<figure class="ds-diagram" style="text-align:center;margin:1.3em 0"><img decoding="async" src="https://cloudlay.cn/wp-content/uploads/dianseiche/00cf06516928.png" alt="电赛小车电控 · 示意图" loading="lazy" style="max-width:100%;height:auto;background:#fff;border-radius:8px;padding:6px;box-shadow:0 1px 6px rgba(0,0,0,.25)"></figure>
<div class="ds-callout ds-callout-danger" style="border-left:4px solid #ff1744;background:#ff174414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff1744"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 地线两个极端都会翻车</p>
<ul>
<li><strong>多点连接 → 地环路 / 地弹</strong>：电机的大电流在地线上产生压降，如果和控制地共用走线，会污染 ADC 基准，引起 MCU 复位、采样跳变、死机。</li>
<li><strong>完全不共地</strong>：信号没有公共参考，更不行。</li>
</ul>
<p>正确姿势：<strong>必须共地，但只能一个点连接</strong>，不能多点形成环路。</p>
</div>
<h3>VM 并大电容：给电机配个&#8221;水箱&#8221;</h3>
<p>电机是感性负载，启动、堵转、换向的瞬间电流会猛地飙升，把 VM 电压拽下去一大截。</p>
<div class="ds-callout ds-callout-example" style="border-left:4px solid #7c4dff;background:#7c4dff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#7c4dff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> VM 并大电容像水箱</p>
<p>电机猛地一启动/堵转，就像有人突然开大水龙头，管道压力瞬间掉。<strong>大电解电容（水箱）先顶上</strong>，把电流补给它，撑到电源反应过来。<strong>小陶瓷电容（滤网）</strong>则滤掉电里的高频毛刺（小气泡）。</p>
</div>
<p>所以驱动 VM 引脚要并两种电容：</p>
<ul>
<li><strong>大电解电容（bulk，数十~数百 uF）</strong>：抗电机启动/换向时的电压跌落，做能量缓冲。选<strong>低 ESR</strong> 的，就近放在驱动 VM 引脚旁。</li>
<li><strong>小陶瓷电容（0.1uF）</strong>：就近滤高频毛刺。</li>
</ul>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 只放小陶瓷电容滤不住低频跌落</p>
<p>如果 VM 只并了 0.1uF 陶瓷电容、没有大电解电容，电机启动/堵转瞬间的电压大幅跌落会<strong>拉垮整车电源，甚至复位 MCU</strong>。小电容只能滤高频，低频的能量缺口得靠大电容补。</p>
<p>TI 手册的建议：VM 就近接低 ESR 陶瓷 0.1uF，再配足够的 bulk 电容，具体容值要靠系统级测试来定。实战里还会在<strong>电机两端并 TVS（瞬态抑制二极管）</strong>，进一步压制换向时的电压尖峰。</p>
</div>
<h2>低电量电压补偿：让车后半程不掉速</h2>
<p>最后一个常被忽略、却能直接拉开名次的细节。</p>
<p>锂电池从满电到亏电，电压能跌 20% 以上。问题来了：你的 PID 输出的是<strong>占空比</strong>，占空比固定时，电压越低、给电机的实际平均电压就越低、转速就越慢。</p>
<div class="ds-callout ds-callout-example" style="border-left:4px solid #7c4dff;background:#7c4dff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#7c4dff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 低电量补偿像跑步配速</p>
<p>体力（电量）下降后，同样&#8221;用力&#8221;（占空比）实际跑得更慢。聪明的做法是：体力越差，越加大用力比例，保持配速（速度）恒定，直到实在跑不动为止。</p>
</div>
<div class="ds-callout ds-callout-danger" style="border-left:4px solid #ff1744;background:#ff174414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff1744"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 不补偿的后果：后半程跑偏、参数失配</p>
<p>有队伍的目标是&#8221;不同电压下匀速前进&#8221;，结果实测电机速度确实随电池压降变慢，<strong>后半程跑偏、速度不一致</strong>。同一套 PID 参数，满电时调好的，到亏电就表现漂移。来源：<a href="https://www.cnblogs.com/Tayoou/p/15257616.html" target="_blank" rel="noopener noreferrer">Tayoou 调车总结</a></p>
</div>
<p>做法很简单——用 ADC 实时采电池电压，把占空比按比例放大：</p>
<p class="ds-math" style="overflow-x:auto">$$\text{实际占空比} = \text{期望占空比} \times \frac{V_{\text{标称}}}{V_{\text{实测}}}$$</p>
<p>电池越亏（$V_{实测}$ 越小），补偿系数越大，等效给电机的平均电压就恒定了。</p>
<pre><code class="language-c">// V_nominal: 标称电压(常数); V_batt: ADC 实测电池电压(已滤波)
float comp = V_nominal / V_batt;       // 补偿系数, 电压越低系数越大
duty_out = duty_target * comp;
if (duty_out &gt; DUTY_MAX) duty_out = DUTY_MAX;   // 限幅, 别超 100%</code></pre>
<p>代码思路参考 <a href="https://zhuanlan.zhihu.com/p/513969803" target="_blank" rel="noopener noreferrer">电压补偿的实现</a>。</p>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 两个补偿的坑</p>
<ol>
<li><strong>ADC 采电池电压必须滤波</strong>：电机噪声大，不滤波的话补偿系数会乱跳，反而加剧速度波动。</li>
<li><strong>电压过低时补偿会顶到 100% 仍达不到目标</strong>：这时候应该触发低压报警、适当降速，保护电池，别硬怼。</li>
</ol>
<p>注意这是一条经验性做法——它能改善低电量掉速，但要真正做到速度恒定，还得靠<strong>速度闭环 PID</strong>，补偿只是给闭环减负的前置手段，两者配合才稳。</p>
</div>
<h2>一点点剧透：地基之上是 PID</h2>
<p>讲到电压补偿，已经摸到控制环的门口了。这里先埋个伏笔，把和地基直接相关的几条结论给你（详细的原理和调参后面几篇专门讲，这里不展开）：</p>
<ul>
<li><strong>速度环用增量式 PID</strong>：它只输出&#8221;控制增量&#8221;（油门加一点/松一点），正反向切换平顺、对积分天然限幅，适合电机连续调速。</li>
<li><strong>方向环/位置环用位置式 PID</strong>：直接算绝对量（方向盘打到 30 度这个位置），精度高，适合一步到位的定位。</li>
<li><strong>串级（双环）</strong>：外环管方向/位置，内环管速度，外环输出作为内环目标，外环周期比内环慢 2~5 倍。</li>
<li><strong>控制周期</strong>：速度环 5~10ms（100~200Hz）起步，方向/外环 10~50ms，用定时器中断保证周期严格恒定。</li>
<li><strong>铁律</strong>：必做积分限幅 + 输出限幅；切换 PID 模式或电机换向时，<strong>一定要清积分项</strong>，否则积分饱和（windup）会让输出冲顶、响应迟滞。</li>
<li><strong>调参口诀</strong>：先 P 后 I 再 D。</li>
</ul>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 这只是预告</p>
<p>上面这些是&#8221;地基决定的约束&#8221;，具体怎么理解 P/I/D、怎么从零调出第一个能跟随目标速度的电机，是《PID 入门》那一篇手把手带你做的事；串级、抗饱和那些&#8221;工程补丁&#8221;放在《PID 进阶》；看波形治百病的调参实战在《PID 调参实战》。这里点到为止。</p>
</div>
<h2>收尾：地基自检清单</h2>
<p>照着下面这几条逐项过一遍，你的电控地基基本就立住了：</p>
<ul>
<li>☐ 电机减速比按&#8221;目标线速度 + 爬坡扭矩&#8221;反推，动力留余量</li>
<li>☐ 编码器和电机供电分两路；机械安装同心、固定牢、无过盈摩擦</li>
<li>☐ 驱动芯片按堵转电流选型：小电机 TB6612，大电流 DRV8701，别用 L298N 当主力</li>
<li>☐ TB6612 记得给 PWM、STBY 置高；把方向 + 占空比封装成&#8221;带符号速度&#8221;</li>
<li>☐ PWM 频率 18~20kHz 起步，电磁组干扰严重就压到 13-15kHz；互补 PWM 必加死区</li>
<li>☐ 电源树分级：电池直供 VM，DC-DC 降 5V，MCU 走独立 LDO 降 3.3V</li>
<li>☐ 功率地与控制地分开走线，单点共地，别绕成地环路</li>
<li>☐ 驱动 VM 并大电解（低 ESR）+ 0.1uF 陶瓷，电机端加 TVS</li>
<li>☐ 上电池电压补偿（ADC 滤波 + 占空比限幅），低压报警降速</li>
<li>☐ <strong>务必在赛场同款地面上调参</strong></li>
</ul>
<p>地基稳了，软件才有发挥的余地。但光有&#8221;肌肉&#8221;还不够——小车还得有&#8221;眼睛&#8221;和&#8221;平衡感&#8221;，才知道自己在哪、有没有跑偏。下一篇我们就来给小车装上感知系统：灰度、电磁、编码器和 IMU。</p>
<div class="ds-series" style="border:1px solid #4488ff33;background:#4488ff0d;border-radius:8px;padding:.8em 1.1em;margin:1.2em 0">
<p style="margin:0 0 .5em"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4da.png" alt="📚" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 本文是 <strong>「从 0 到 1 带你打电赛 · 小车电控篇」</strong> 系列（共 12 篇）第 4 篇。</p>
<ol style="margin:.2em 0 0;padding-left:1.4em">
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-01-how-to-score/">第1篇 · 拿奖逻辑：把比赛拆成小目标</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-02-history/">第2篇 · 赛题进化史与押题</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-03-build-and-architecture/">第3篇 · 整车搭建与代码框架</a></li>
<li style="margin:.15em 0"><strong>第4篇 · 电机驱动与电源地基（本篇）</strong></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-05-sensing/">第5篇 · 感知：灰度/电磁/编码器/IMU</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-06-pid-basics/">第6篇 · PID 入门：搞懂 P/I/D</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-07-pid-advanced/">第7篇 · PID 进阶：串级+工程补丁</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-08-pid-tuning/">第8篇 · PID 调参实战(核心)</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-09-advanced-control/">第9篇 · 进阶控制：几时该上</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-10-vision-comm/">第10篇 · K230 视觉与通信协议</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-11-architecture-fsm/">第11篇 · 状态机与整车软件</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-12-field-manual/">第12篇 · 现场作战+避坑+开源</a></li>
</ol>
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://cloudlay.cn/nuedc-car-04-motor-power/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>从0到1带你打电赛·小车电控篇(三)：整车怎么搭——三人分工、硬件选型与「算法和芯片解耦」的代码框架</title>
		<link>https://cloudlay.cn/nuedc-car-03-build-and-architecture/</link>
					<comments>https://cloudlay.cn/nuedc-car-03-build-and-architecture/#respond</comments>
		
		<dc:creator><![CDATA[云间辞]]></dc:creator>
		<pubDate>Sun, 14 Jun 2026 17:08:32 +0000</pubDate>
				<category><![CDATA[嵌入式]]></category>
		<category><![CDATA[MSPM0G3507]]></category>
		<category><![CDATA[STM32]]></category>
		<category><![CDATA[团队分工]]></category>
		<category><![CDATA[智能小车]]></category>
		<category><![CDATA[电赛]]></category>
		<category><![CDATA[软件架构]]></category>
		<guid isPermaLink="false">https://cloudlay.cn/nuedc-car-03-build-and-architecture/</guid>

					<description><![CDATA[📚 本文是 「从 0 到 1 带你打电赛 · 小车电控篇」 系列（共 12 篇）第 3 篇。 第1篇 · 拿奖 [&#8230;]]]></description>
										<content:encoded><![CDATA[<div class="ds-series" style="border:1px solid #4488ff33;background:#4488ff0d;border-radius:8px;padding:.8em 1.1em;margin:1.2em 0">
<p style="margin:0 0 .5em"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4da.png" alt="📚" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 本文是 <strong>「从 0 到 1 带你打电赛 · 小车电控篇」</strong> 系列（共 12 篇）第 3 篇。</p>
<ol style="margin:.2em 0 0;padding-left:1.4em">
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-01-how-to-score/">第1篇 · 拿奖逻辑：把比赛拆成小目标</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-02-history/">第2篇 · 赛题进化史与押题</a></li>
<li style="margin:.15em 0"><strong>第3篇 · 整车搭建与代码框架（本篇）</strong></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-04-motor-power/">第4篇 · 电机驱动与电源地基</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-05-sensing/">第5篇 · 感知：灰度/电磁/编码器/IMU</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-06-pid-basics/">第6篇 · PID 入门：搞懂 P/I/D</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-07-pid-advanced/">第7篇 · PID 进阶：串级+工程补丁</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-08-pid-tuning/">第8篇 · PID 调参实战(核心)</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-09-advanced-control/">第9篇 · 进阶控制：几时该上</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-10-vision-comm/">第10篇 · K230 视觉与通信协议</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-11-architecture-fsm/">第11篇 · 状态机与整车软件</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-12-field-manual/">第12篇 · 现场作战+避坑+开源</a></li>
</ol>
</div>
<p>前两篇我们把&#8221;这比赛在比什么&#8221;&#8221;出题人爱出什么&#8221;聊清楚了。从这一篇开始，我们正式动手——但先别急着掏出电烙铁。一支队伍的上限，往往不是某个调参的瞬间决定的，而是开赛前那个&#8221;0 阶段&#8221;就埋下的：人怎么分工、主控选谁、代码骨架怎么搭。</p>
<p>这就好比盖楼。地基和承重结构得在挖第一锹土之前想清楚，等楼盖到一半才发现柱子位置不对，那不是修修补补能解决的，是要拆了重来的。而电赛只有四天三夜，没有&#8221;重来&#8221;的预算。这一篇我们就把这套地基打牢，让你后面写算法、调参数的时候，能做到&#8221;现场只改参数，不改架构&#8221;。</p>
<h2>三个人，怎么分这摊活</h2>
<p>小车赛道的队伍通常是三个人。最舒服、也最常见的分工是这样切的：</p>
<ul>
<li><strong>硬件 / 机械</strong>：负责选电机、画 PCB 或飞线、电源、机械结构、传感器布板与焊接。这个人手要稳、要懂模拟电路，是整车能不能&#8221;通电不冒烟&#8221;的第一道关。</li>
<li><strong>电控软件</strong>：负责主控固件——电机驱动、PID、循迹、状态机、各路通信的整合。这是把所有零件&#8221;捏成一辆会跑的车&#8221;的人。</li>
<li><strong>视觉算法</strong>：负责摄像头那一摊——巡线、识别色块/二维码/数字，把图像处理成一个个&#8221;结论&#8221;发给主控。</li>
</ul>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 分工不是切三块互不相干的地</p>
<p>三块活之间咬合得非常紧。硬件选了什么电机，直接决定电控能不能跑闭环；视觉发什么格式的数据，电控就得按什么格式去解析。所以<strong>接口要在开工第一天就定死</strong>：电机驱动用哪个芯片、编码器多少线、视觉和主控之间的通信帧长什么样。接口一旦钉死，三个人就能各干各的，最后再联调——而不是天天互相等。</p>
</div>
<p>这里先埋个伏笔：视觉和主控之间&#8221;只传结论不传整张图&#8221;的协议怎么设计，是《视觉与通信》一篇的重头戏，这里点到为止。</p>
<h2>主控选谁：为什么 2024 年起电赛主推 TI 的 MSPM0</h2>
<p>如果你是 2023 年以前打电赛，主控基本就是 STM32。但从 2024 年开始，电赛官方主推的是 <strong>TI 的 MSPM0G3507</strong>，尤其是智能小车这类题（比如 2024 年 H 题&#8221;自动行驶小车&#8221;）。很多人第一反应是&#8221;我 STM32 还没玩明白，又要换芯片？&#8221;——别慌。这俩的关系，我们后面会讲透：STM32 是你练手的&#8221;训练场&#8221;，MSPM0 是你上场的&#8221;主战场&#8221;，而且<strong>练通的算法能整段搬过去</strong>。</p>
<p>先看 MSPM0G3507 这颗芯片本身。它是一颗 Arm Cortex-M0+ 内核、主频 80MHz 的&#8221;混合信号&#8221;单片机。所谓&#8221;混合信号&#8221;，意思是它不只擅长跑数字逻辑，模拟外设也特别强——这正好戳中小车的需求。核心规格：</p>
<table>
<thead>
<tr>
<th>资源</th>
<th>MSPM0G3507</th>
<th>对小车意味着什么</th>
</tr>
</thead>
<tbody>
<tr>
<td>内核 / 主频</td>
<td>Cortex-M0+ @ 80MHz</td>
<td>跑 PID、状态机绰绰有余</td>
</tr>
<tr>
<td>Flash / SRAM</td>
<td>128KB / 32KB</td>
<td>装得下整车固件</td>
</tr>
<tr>
<td>ADC</td>
<td>2 个 12 位、4Msps、最多 17~27 通道、带 FIFO</td>
<td>可同步采样、一次扫完 8 路灰度</td>
</tr>
<tr>
<td>片上运放 OPA</td>
<td>2 个零漂斩波、可编程增益最高 32 倍</td>
<td>灰度/微弱信号免外挂运放</td>
</tr>
<tr>
<td>定时器</td>
<td>7 个（2 个高级 TIMA + 5 个 TIMG）、最多 22 路 PWM</td>
<td>出 PWM 驱动电机、测编码器</td>
</tr>
<tr>
<td>通信</td>
<td>4×UART、2×SPI、2×I2C、1×CAN-FD</td>
<td>接屏、蓝牙、陀螺仪、上位机</td>
</tr>
</tbody>
</table>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 片上运放是个隐藏福利</p>
<p>MSPM0 自带的 2 个运放（OPA），可以理解成<strong>手机自带的美颜相机</strong>：以前你采灰度这种微弱信号，得在板子上外挂一颗运放芯片来放大，费板子、费焊盘；现在芯片里就带了能放大信号的&#8221;内置镜头&#8221;，放大完直接喂给 ADC。STM32 的 G0 系列没有集成运放，从 STM32 迁到 MSPM0，正好能用片上 OPA 把外部分立运放替掉。</p>
</div>
<h3>一个必须提前知道的&#8221;反直觉硬伤&#8221;：硬件编码器接口只有一路</h3>
<p>这是 MSPM0 相对 STM32 最大的坑，<strong>选型和布线之前必须想清楚</strong>，否则到现场才发现就晚了。</p>
<p>STM32 几乎每个定时器都能配成编码器接口模式，你想测两个电机的转速，随便挑两个定时器就行。但 MSPM0G <strong>全芯片只有一个定时器（常说的 TIMG8）硬件支持 QEI 正交编码器解码</strong>。也就是说，硬件层面你只能&#8221;白嫖&#8221;一路编码器。</p>
<p>那双电机怎么办？两条路：</p>
<ul>
<li><strong>路线 A（硬件 QEI）</strong>：用那唯一一路 TIMG8，硬件自动计数 + 判方向，几乎不占 CPU。这是首选。</li>
<li><strong>路线 B（GPIO 外部中断）</strong>：A/B 两相各接一个 GPIO，上升沿触发中断，在中断里读另一相的电平判方向，再用一个 20ms 周期的定时器去数脉冲算速度。代价是高转速时<strong>容易漏脉冲、把中断占满</strong>。</li>
</ul>
<p>所以双电机的典型方案是：一路走硬件 QEI，另一路退回 GPIO 中断（给它最高优先级），或者上一颗外部计数芯片。</p>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 真实踩坑</p>
<p>很多新手在 MSPM0 上想给两个电机各配一路硬件编码器，结果发现只有一个定时器支持 QEI，第二路只能退回 GPIO 中断，高速时丢脉冲、速度测不准，车跑起来一边快一边慢。这个坑的根源不是代码写错，而是<strong>选型时没数清楚硬件资源</strong>。</p>
</div>
<p>编码器测速本身的原理很直白，就像<strong>数脉冲过了几个</strong>：车每跑一段，电机转一点，编码器吐出一串脉冲；你每隔固定时间（比如 20ms）数一次这段时间内新增了多少脉冲，数得越多说明跑得越快。A、B 相脉冲的先后顺序，还能告诉你是前进还是后退。具体的编码器接法和 IMU 融合，我们留到《感知：灰度/电磁/编码器/IMU》一篇细讲。</p>
<h2>开发环境：SysConfig、IDE 和两种下载方式</h2>
<h3>SysConfig：TI 版的&#8221;装修图纸软件&#8221;</h3>
<p>TI 给 MSPM0 配了一个图形化配置工具叫 <strong>SysConfig</strong>，作用相当于 STM32 玩家熟悉的 CubeMX。</p>
<p>它就像<strong>装修前用图纸软件拖拽布局</strong>：你在图上点哪个插座（引脚）接什么电器（外设），软件自动帮你把&#8221;水电图&#8221;（一个叫 <code>ti_msp_dl_config.h</code> 的头文件，里面有 <code>SYSCFG_DL_init()</code> 初始化函数）画好，你不用自己一根根去配寄存器。配 PWM 在 TIMER-PWM 分类下、配编码器在 TIMER-QEI、配 ADC 在 ADC12、配运放在 OPA。配完它会生成一堆实例宏，比如 <code>PWM_0_INST</code>、<code>QEI_0_INST</code>、<code>UART_0_INST</code>，你代码里直接用这些名字就行。</p>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> SysConfig 第一坑：改完一定要保存</p>
<p>SysConfig 改完配置如果<strong>不保存</strong>，生成的 <code>ti_msp_dl_config.h</code> 不会更新，你编译出来的还是旧配置——明明改了引脚却没生效，能让你查半天。每次改完务必 Ctrl+S。</p>
</div>
<p>配 PWM 的时候，有个关系要记牢：</p>
<p class="ds-math" style="overflow-x:auto">$$\text{频率}=\frac{\text{时钟}}{\text{分频}\cdot(\text{period}+1)}$$</p>
<p>向上计数时，占空比 $=\frac{\text{CCR}}{\text{period}+1}$。立创开发板的电机驱动例程用 80MHz 时钟、period 设 10000，实测出来约 8kHz。运行时动态改占空比就一行：</p>
<pre><code class="language-c">// 设置占空比 (向上计数: duty = CCR/(period+1))
DL_TimerG_setCaptureCompareValue(PWM_0_INST, 1000, DL_TIMER_CC_1_INDEX);
// 如果 SysConfig 里没勾 "Start Timer"，需要手动启动一次
DL_TimerG_startCounter(PWM_0_INST);
// 读编码器 QEI 计数
uint32_t cnt = DL_Timer_getTimerCount(QEI_0_INST);</code></pre>
<p>注意 MSPM0 的 API 全是 <code>DL_</code> 前缀（DriverLib 的意思），这和 STM32 的 <code>HAL_</code>/<code>LL_</code> 不一样。再记两个小口诀：PWM 占空比建议<strong>别超过 95%</strong>；改 PWM 频率（改 period）会同时改变占空比的分辨率（period 越小，占空比能分的档越粗），要兼顾。PWM 频率电赛常用 8kHz 到 32kHz 这个区间，低于 20kHz 左右电机会有可闻的&#8221;啸叫&#8221;，想安静点就往高了选。</p>
<h3>IDE：CCS / Keil / IAR 怎么挑</h3>
<ul>
<li><strong>CCS</strong>：TI 官方的，免费，基于 Eclipse，<strong>内置 SysConfig</strong>，配套最全。新手最省心。</li>
<li><strong>Keil MDK</strong>：电赛圈最主流，因为大家生态熟、学校积累多。但要注意版本——<strong>需要 uVision v5.38a 以上 + AC6 编译器 v6.16 以上</strong>，版本太旧装不上 MSPM0 的支持包。</li>
<li><strong>IAR</strong>：也支持，用的人相对少。</li>
<li><strong>VSCode</strong>：它本身不是官方 IDE，但社区常用 VSCode + Keil（靠插件）或 VSCode + CMake + GCC 来获得更现代的编辑体验，编译下载还是靠 Keil 或命令行。</li>
</ul>
<p>一句话：图省心、从零起步用 CCS；要沿用学校积累、跟队友保持一致就用 Keil。</p>
<h3>下载：SWD 仿真器 vs BSL 串口</h3>
<p>这俩的区别，可以类比成上网方式：</p>
<ul>
<li><strong>SWD（专线宽带）</strong>：用板载的 XDS110 或外接 DAP-Link / J-Link，四根线（SWCLK / SWDIO / GND / 3V3）。不仅能下载，还能<strong>实时看程序在干嘛、打断点单步调试</strong>。开发期首选。</li>
<li><strong>BSL 串口（手机热点应急）</strong>：用 TI 的 UniFlash 工具，通过 USB 转串口把程序灌进去，没仿真器也能用，但<strong>只能灌不能调</strong>。</li>
</ul>
<p>BSL 这条路有两个非常容易翻车的细节：</p>
<div class="ds-callout ds-callout-danger" style="border-left:4px solid #ff1744;background:#ff174414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff1744"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> BSL 两大坑</p>
<ol>
<li><strong>只认 <code>.txt</code> / <code>.hex</code> 文件，绝不认 CCS 生成的 <code>.out</code></strong>。有人拿 <code>.out</code> 去烧，UniFlash 死活不认，还以为是工具坏了。</li>
<li><strong>按键时序要掐准</strong>：同时按住 BSL + RST 约 5 秒 → 松开 RST → 3 秒内点 Load Image → 烧完（约 10~20 秒）松开 BSL → 再按一次 RST。时序没掐准就反复连不上。</li>
</ol>
</div>
<p>结论很简单：开发期老老实实用板载 XDS110 或 DAP-Link 走 SWD，能调试又省事；BSL 留作&#8221;手头没仿真器&#8221;时的应急手段。</p>
<h2>重头戏：让&#8221;算法和芯片解耦&#8221;的分层框架</h2>
<p>到这里，才是这一篇真正的核心。前面铺垫了这么多，就是为了引出一个问题：<strong>STM32 练手、MSPM0 上场，凭什么练通的算法能整段搬过去？</strong></p>
<p>答案不在芯片，在<strong>代码架构</strong>。</p>
<h3>为什么要分层</h3>
<p>设想一下，如果你的 PID 函数里直接写满了 <code>HAL_GPIO_WritePin(...)</code> 这种 STM32 专属的芯片 API，那换到 MSPM0 的时候，整个 PID 函数都得跟着改——因为它和具体芯片&#8221;焊死&#8221;在一起了。这就是没分层的下场：换芯片 = 满地找改 = 平移变重写。</p>
<p>分层架构的思路，像<strong>点外卖</strong>：</p>
<ul>
<li><strong>你（App 应用层）</strong>：只对着 App 下单，决定&#8221;我要吃啥、车要怎么跑&#8221;，从不进后厨。这里放状态机和决策。</li>
<li><strong>App 平台（中间件层）</strong>：把订单翻译给骑手和商家，承载 PID、滤波、通信协议、元素识别这些<strong>和芯片无关</strong>的算法。</li>
<li><strong>骑手 / 灶台（驱动层 + HAL）</strong>：干脏活累活，真正去配寄存器、读写引脚。</li>
</ul>
<p>换城市（换芯片）时，你点外卖的方式（算法）一字不改，变的只是当地的骑手和店家（驱动层）。这就是&#8221;算法和芯片解耦&#8221;的本质。</p>
<p>这套范式不是我们瞎编的，它直接借鉴了智能车圈久经考验的<strong>逐飞（SeekFree）开源库</strong>的分层结构：</p>
<figure class="ds-diagram" style="text-align:center;margin:1.3em 0"><img decoding="async" src="https://cloudlay.cn/wp-content/uploads/dianseiche/5aa6d106b13e.png" alt="电赛小车电控 · 示意图" loading="lazy" style="max-width:100%;height:auto;background:#fff;border-radius:8px;padding:6px;box-shadow:0 1px 6px rgba(0,0,0,.25)"></figure>
<h3>一条不能破的铁律</h3>
<p>分层能不能起作用，全看一条铁律守不守得住：</p>
<div class="ds-callout ds-callout-important" style="border-left:4px solid #00bcd4;background:#00bcd414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bcd4"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2757.png" alt="❗" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 铁律：上层只调下层、下层绝不反调；上层禁止直接调芯片 API</p>
<p>这就像公司汇报：下属向上汇报（对上提供接口），老板不会替下属干活，下属也别绕过流程直接跑去动机房（直接调 <code>HAL_GPIO_WritePin</code> 这类最底层 API）。一旦中间件、应用层里混进了芯片专属 API，分层就破了，换芯片时你得满地找它们、一个个改，平移直接变成重写。</p>
</div>
<p>逐飞库就是这么干的：设备层（管摄像头/陀螺/屏的初始化）<strong>只允许调驱动层的接口</strong>，不准碰最底层的芯片 API。这样换 MCU 只改驱动层，中间件和 App 原样平移。</p>
<p>这套库有多能打？同一份逐飞库已经移植到了 RT1064、英飞凌 TC264、STC32，以及 <strong>TI 的 MSPM0G3507（对应 2025 TI 板电赛）</strong>——上层算法共用，差异全集中在底层驱动。这就是分层解耦价值的活证据。</p>
<h3>一个可照抄的目录骨架</h3>
<p>落到实处，你的工程目录可以这么切：</p>
<pre><code class="language-plaintext">project/
├── App/                 # 应用层：你写的核心逻辑
│   ├── fsm.c/h          #   状态机：待命/起步/直道/弯道/路口/停靠/找回/急停
│   └── control_task.c   #   控制环调度：采集→融合→串级PID→输出→保护
├── Middleware/          # 中间件层：芯片无关的算法
│   ├── pid.c/h          #   PID（位置式/增量式）
│   ├── filter.c/h       #   滤波（一阶低通/互补）
│   └── protocol.c/h     #   通信帧解析
├── Driver/              # 驱动层：换芯片主要改这里
│   ├── motor.c/h        #   电机：把"带符号速度"翻译成PWM+方向
│   ├── encoder.c/h      #   编码器读速
│   ├── gray.c/h         #   灰度采集
│   └── uart.c/h         #   串口收发
└── HAL/                 # 芯片相关：DriverLib / 寄存器
    └── (SysConfig 生成的 ti_msp_dl_config.h 等)</code></pre>
<p>关键在于<strong>接口的方向</strong>：App 调中间件和驱动，中间件和驱动里<strong>绝不出现任何芯片专属 API</strong>。比如电机驱动这一层，对上只暴露一个&#8221;给我一个带符号的速度&#8221;的接口，符号决定正反转、绝对值决定 PWM 大小，至于底下怎么配 PWM 寄存器，全藏在驱动层里：</p>
<pre><code class="language-c">// 中间件层：PID 算出一个带符号输出，完全不知道芯片是谁
int pwm = (int)pid_calc(&amp;speed_pid, target_speed, get_encoder_count());

// 驱动层：把带符号输出翻译成具体的正反转 + PWM（这层才碰芯片）
void motor_set(int pwm) {
    if (pwm &gt; 0)       set_motor(0, pwm);   // 正转
    else if (pwm &lt; 0)  set_motor(-pwm, 0);  // 反转（取绝对值）
    else               stop_motor();
}</code></pre>
<p><code>pid_calc</code> 这个函数里只有数学，没有一行芯片代码，所以它在 STM32 上调通了，搬到 MSPM0 上一个字都不用改。这就是&#8221;练通的算法整段平移&#8221;的底气。</p>
<h2>STM32 <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2194.png" alt="↔" class="wp-smiley" style="height: 1em; max-height: 1em;" /> MSPM0 平移心法</h2>
<p>很多人迁移时的第一反应是&#8221;把每一个 <code>HAL_xxx</code> 逐行翻译成 <code>DL_xxx</code>&#8220;。<strong>这是最痛苦、最容易错的做法，千万别这么干。</strong></p>
<p>TI 官方在《从 STM32 到 MSPM0 迁移指南》（文档号 ZHCABX9B，能搜到）里给出的标准心法是：<strong>先把你的应用逻辑理解透，然后拿一份最接近的 MSPM0 官方例程当骨架，把你的 PID、循迹这些算法层原样搬进去，只重写外设的初始化和读写。</strong></p>
<div class="ds-callout ds-callout-example" style="border-left:4px solid #7c4dff;background:#7c4dff14;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#7c4dff"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 一个好懂的比喻</p>
<p>把算法从 STM32 平移到 MSPM0，好比把同一道菜从燃气灶搬到电磁炉。<strong>菜谱（算法逻辑）一字不改</strong>，只是把&#8221;开燃气阀&#8221;的动作换成&#8221;按电磁炉按钮&#8221;（HAL 换成 DriverLib）。聪明的做法不是把每个燃气动作硬翻译，而是直接拿一份电磁炉的现成菜谱当模板，把你的料倒进去。</p>
</div>
<p>官方给的工具链对照表，迁移时贴在显示器边上很有用：</p>
<table>
<thead>
<tr>
<th>STM32（ST 工具）</th>
<th>MSPM0（TI 工具）</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>CubeIDE</td>
<td>CCS</td>
<td>IDE</td>
</tr>
<tr>
<td>CubeMX</td>
<td>SysConfig</td>
<td>图形化配置</td>
</tr>
<tr>
<td>CubeProgrammer</td>
<td>UniFlash</td>
<td>烧录</td>
</tr>
<tr>
<td>CubeMonitor</td>
<td>GuiComposer</td>
<td>上位机监控</td>
</tr>
<tr>
<td>HAL 库</td>
<td>TI Driver</td>
<td>高层驱动</td>
</tr>
<tr>
<td>LL 库</td>
<td>DriverLib（<code>DL_</code> 前缀）</td>
<td>底层驱动（TI 建议大多数人直接用这个，省内存、性能好）</td>
</tr>
</tbody>
</table>
<p>有几个外设层面的差异，是平移时最容易&#8221;反咬&#8221;算法层假设的地方，列成清单逐条核对：</p>
<ul>
<li>☐ <strong>GPIO 拆成了两块</strong>：STM32 一个 GPIO 概念，MSPM0 拆成 GPIO（读写 / 中断）+ IOMUX（引脚复用），配引脚和读写是分开的。</li>
<li>☐ <strong>硬件编码器接口只有一路 QEI</strong>（前面强调过的硬伤），双电机方案要提前定好。</li>
<li>☐ <strong>UART 的 FIFO 只有 4 字节</strong>（STM32 是 8 字节），高速收发更容易溢出，而且不支持自动波特率检测。</li>
<li>☐ <strong>UART 中断里不要照搬习惯去调 <code>NVIC_ClearPendingIRQ()</code></strong>——官方教程明确提醒过，照搬 STM32 写法会出问题。</li>
<li>☐ <strong>时钟树、NVIC、中断模型</strong>和 STM32 不一样，从官方 example 起步能帮你绕开大部分坑。</li>
</ul>
<p>官方迁移指南把整个流程总结成五步，照着走就行：① 对照产品系列表选定 MSPM0 器件 → ② 订 LaunchPad 开发板（LP-MSPM0G3507）→ ③ 装 IDE + SDK → ④ 软件移植（从最接近的官方例程改起，<strong>不是逐行替换 API</strong>）→ ⑤ 调试验证。</p>
<h2>有哪些现成的模板和库可以站在肩膀上</h2>
<p>完全从零搭框架很费时间。下面这几个仓库都是经过电赛验证的，按你的处境挑一个当起点：</p>
<ul>
<li><strong><a href="https://github.com/TexasInstruments/mspm0-sdk" target="_blank" rel="noopener noreferrer">TI 官方 mspm0-sdk</a></strong>：DriverLib + 海量外设例程（GPIO / Timer / PWM / ADC / UART / DMA / OPA 全都有）。这是所有上层方案的底座，也是&#8221;从最接近的 example 改起&#8221;那个 example 的来源。一手权威，必装。</li>
<li><strong><a href="https://github.com/ZhijianLi2003/ZLC_MSPM0_Peripheral_Library" target="_blank" rel="noopener noreferrer">ZLC_MSPM0_Peripheral_Library</a></strong>：2024 电赛 H 题一等奖（山大威海）的外设库，编码器、电机（DRV8701E 双路）、舵机 PWM、8 路灰度、IMU、无线串口一应俱全，还附了设计报告和环境配置文档。<strong>想看一等奖的完整车软件栈长什么样，看这个。</strong></li>
<li><strong><a href="https://github.com/menoking/PIDCarTemplate-MSPM0G3507" target="_blank" rel="noopener noreferrer">PIDCarTemplate-MSPM0G3507</a></strong>：两轮 / 四轮 PID 小车模板，封装好了 Delay / Encoder / Motor / 按键（短按长按双击）/ MPU6050 / OLED / 蓝牙 / 灰度，clone 下来调 API 就能开发。<strong>从零起步直接套模板的首选。</strong></li>
<li><strong><a href="https://github.com/woai66/SeekFree_MSPM0G3507_Opensource_Library" target="_blank" rel="noopener noreferrer">woai66 的逐飞 MSPM0 移植库</a></strong>：把上面讲的逐飞分层库移植到了 MSPM0，对应 2025 TI 板电赛。<strong>想要&#8221;芯片解耦平移&#8221;那套完整范本，看它。</strong></li>
<li><strong><a href="https://github.com/danshoujieyi/TI-MSPM0G3507" target="_blank" rel="noopener noreferrer">danshoujieyi/TI-MSPM0G3507</a></strong>：电赛指定板模板，裸机版 + FreeRTOS 版 + RT-Thread 版都有（Keil5 + VSCode）。想上 RTOS 或要 VSCode 工作流的直接用。</li>
<li><strong><a href="https://github.com/zhzhongshi/MSPM0-CMAKE-GCC-Template" target="_blank" rel="noopener noreferrer">MSPM0-CMAKE-GCC-Template</a></strong>：GCC + CMake 模板，想脱离 Keil、用 VSCode + 命令行现代工作流的首选。</li>
</ul>
<p>更全的开源仓库点评（含视觉车、平衡车、仿真工具），我们放在收官的《现场作战手册》一篇里集中评注，这里先给够你起步用的几个。</p>
<h2>一些选型和架构上的经验之谈</h2>
<p>最后几条是过来人趟出来的，写代码之前先记在心里。</p>
<div class="ds-callout ds-callout-tip" style="border-left:4px solid #00bfa6;background:#00bfa614;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bfa6"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 电机选型，先于算法</p>
<p>一等奖队伍（ZLC）选的是带霍尔编码器的减速直流电机，而不是步进或空心杯——<strong>步进体积大、空心杯没法测速</strong>，只有带编码器的减速直流电机才能跑闭环 PID。换句话说，<strong>要做速度 / 位置闭环，选型阶段就必须锁定带编码器的电机</strong>，减速比还会影响测速分辨率和扭矩。算法再强，没编码器也没法闭环。</p>
</div>
<div class="ds-callout ds-callout-warning" style="border-left:4px solid #ff9100;background:#ff910014;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#ff9100"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> MPU6050 的温漂是头号坑</p>
<p>2024 H 题有队伍用 MPU6050 做无指示线直行，发现陀螺仪温漂严重——偏航角随温度漂移，车越跑越偏，直道走不直。消费级 MPU6050 精度有限，<strong>上电要静置做零偏校准</strong>，长时间跑要考虑温补或换更高级的 IMU（如 ICM、IMU660ra），直行最好<strong>融合编码器双反馈</strong>，而不是只信陀螺仪。陀螺仪具体怎么校准、怎么融合，留到《感知：灰度/电磁/编码器/IMU》一篇。</p>
</div>
<div class="ds-callout ds-callout-important" style="border-left:4px solid #00bcd4;background:#00bcd414;padding:.6em 1em;margin:1.2em 0;border-radius:6px">
<p style="margin:0 0 .45em;font-weight:700;color:#00bcd4"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2757.png" alt="❗" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 调度方式：电赛首选前后台裸机，别盲目上 RTOS</p>
<p>整车软件的调度，<strong>主流且最稳的选择是&#8221;前后台裸机&#8221;</strong>：主循环（后台）跑显示、通信这些慢任务，定时器中断（前台）跑固定周期的控制环。它时序确定、开销极小、容易验证。</p>
<p>要不要上 FreeRTOS / RT-Thread，像<strong>出门用不用导航</strong>：去楼下小卖部（任务才 3~5 个）凭记忆最快；只有跨城多点配送（任务多、要真并发）才值得开导航——而导航本身也耗油（RTOS 内核要占几 KB，还可能引入优先级反转、同步 bug）。任务少时硬上 RTOS，纯属给自己加难度。</p>
</div>
<p>调度、控制环时序、状态机怎么把所有模块串成一辆会跑的车，是《状态机与整车软件》那一篇的主题，到时候我们会给出完整的主控制环伪代码和状态图。</p>
<p>把这一篇的地基打牢，你就有了一套&#8221;算法写一次、芯片换着用&#8221;的框架。但框架终究是个空壳，真正让车动起来的第一步，是让电机听话——电机驱动、PWM 和电源这套电控地基，我们下一篇见。</p>
<div class="ds-series" style="border:1px solid #4488ff33;background:#4488ff0d;border-radius:8px;padding:.8em 1.1em;margin:1.2em 0">
<p style="margin:0 0 .5em"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4da.png" alt="📚" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 本文是 <strong>「从 0 到 1 带你打电赛 · 小车电控篇」</strong> 系列（共 12 篇）第 3 篇。</p>
<ol style="margin:.2em 0 0;padding-left:1.4em">
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-01-how-to-score/">第1篇 · 拿奖逻辑：把比赛拆成小目标</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-02-history/">第2篇 · 赛题进化史与押题</a></li>
<li style="margin:.15em 0"><strong>第3篇 · 整车搭建与代码框架（本篇）</strong></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-04-motor-power/">第4篇 · 电机驱动与电源地基</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-05-sensing/">第5篇 · 感知：灰度/电磁/编码器/IMU</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-06-pid-basics/">第6篇 · PID 入门：搞懂 P/I/D</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-07-pid-advanced/">第7篇 · PID 进阶：串级+工程补丁</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-08-pid-tuning/">第8篇 · PID 调参实战(核心)</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-09-advanced-control/">第9篇 · 进阶控制：几时该上</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-10-vision-comm/">第10篇 · K230 视觉与通信协议</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-11-architecture-fsm/">第11篇 · 状态机与整车软件</a></li>
<li style="margin:.15em 0"><a href="https://cloudlay.cn/nuedc-car-12-field-manual/">第12篇 · 现场作战+避坑+开源</a></li>
</ol>
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://cloudlay.cn/nuedc-car-03-build-and-architecture/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
