<?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/tag/%e7%94%b5%e8%b5%9b/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带你打电赛·小车电控篇(六)：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带你打电赛·小车电控篇(七)：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 调参实战——一套能照抄的调参流程 + 看波形治百病</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-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带你打电赛·小车电控篇(十)：视觉与通信——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带你打电赛·小车电控篇(十一)：把一切串起来——状态机、控制环时序与整车软件架构</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带你打电赛·小车电控篇(十二)：现场作战手册——四天三夜怎么打、调试避坑与精选开源</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-01-how-to-score/</link>
					<comments>https://cloudlay.cn/nuedc-car-01-how-to-score/#respond</comments>
		
		<dc:creator><![CDATA[云间辞]]></dc:creator>
		<pubDate>Sun, 14 Jun 2026 17:08:32 +0000</pubDate>
				<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-01-how-to-score/</guid>

					<description><![CDATA[📚 本文是 「从 0 到 1 带你打电赛 · 小车电控篇」 系列（共 12 篇）第 1 篇。 第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 篇）第 1 篇。</p>
<ol style="margin:.2em 0 0;padding-left:1.4em">
<li style="margin:.15em 0"><strong>第1篇 · 拿奖逻辑：把比赛拆成小目标（本篇）</strong></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"><a href="https://cloudlay.cn/nuedc-car-12-field-manual/">第12篇 · 现场作战+避坑+开源</a></li>
</ol>
</div>
<p>先问你一个问题：你觉得电赛拿奖，最该练的是什么？是把代码写得多漂亮，还是把报告写得多厚？</p>
<p>如果你脑子里第一反应是&#8221;报告要写好、PPT 要做炫&#8221;，那这篇就是来&#8221;劝退&#8221;这个想法的。我们这群拿过奖、也翻过车的人复盘下来，结论很一致：<strong>电赛小车这条赛道，拿分逻辑跟你想的可能不太一样。</strong> 这一篇不讲任何代码、不调任何参数，只干一件事——把&#8221;拿奖&#8221;这个听起来很玄的目标，拆成一条你接下来能照着练的清单。</p>
<p>把地图先摊开，后面十一篇你才知道每一步是在往哪走。</p>
<h2>先用一分钟说清楚：这比赛到底是个啥</h2>
<p>全国大学生电子设计竞赛（简称<strong>电赛</strong>，英文缩写 NUEDC），由教育部和工信部共同发起，是国内电子信息类规模最大、含金量最高的学科竞赛，一年差不多四万人参赛。它有个让新人很懵的&#8221;大小年&#8221;规律：</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;" /> 大年 vs 小年</p>
<ul>
<li><strong>奇数年</strong>＝国赛（&#8221;大年&#8221;），全国统一命题，规模最大；</li>
<li><strong>偶数年</strong>＝省赛 ＋ TI 杯模拟电子系统设计专题赛（&#8221;小年&#8221;），专题赛的初赛和双数年省赛合并进行。</li>
</ul>
<p>所以你能不能赶上一次完整的国赛，跟你大几入学有点关系——这是组队前就该算清楚的事。</p>
</div>
<p>报名规则很简单：<strong>本校三个人自愿组一队</strong>，国赛名额通常要靠省赛推荐（一般是省一，部分省份可推优秀省二）。控制类（也就是我们这个&#8221;小车/平衡车/云台追踪&#8221;赛道）是电赛的核心方向之一。</p>
<p>最有&#8221;仪式感&#8221;的是赛程：<strong>四天三夜封闭。</strong> 一般 8 月初开赛，第一天早上 8 点前发题，最后一天晚上 8 点开始装箱封箱，中间禁止老师指导，元器件由学校提供。这四天三夜，你大概率是要熬夜的——所以&#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;几乎完全重合：同样是 STM32/TI 单片机 ＋ 编码器测速 ＋ 电机 H 桥驱动 ＋ 串级 PID ＋ 各种传感器（红外阵列/电磁电感/摄像头/IMU）。这意味着：<strong>智能车队每年公开的技术报告，是你备赛时白捡的金矿。</strong> 那些报告把电磁归一化、差比和、模糊 PID、姿态解算、调参曲线写得极透，比电赛赛后零散经验系统得多。这点我们在《赛题进化史》和《现场作战手册》两篇会反复用到。</p>
</div>
<h2>重头戏：这比赛的分，到底是怎么给的</h2>
<p>这是全篇最值钱的一节，请慢点读。</p>
<p>很多人对评分有个&#8221;祖传印象&#8221;：<strong>总分 150，报告占 1/3（50 分）。</strong> 你在不少老资料、老学长口口相传里都会听到这个说法。</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;" /> 先纠正一个流传很广的过时印象</p>
<p>&#8220;总分 150、报告占 1/3&#8221; 是 2019 年前的老格式了。你真正要面对的是<strong>每道具体赛题自带的官方评分表</strong>——而近年真题（2021、2024）的评分表，统统是 <strong>120 分制，报告只占 20 分</strong>。 换句话说：按单题官方评分表，报告占 <strong>20/120 ≈ 1/6</strong>，远低于传说中的 1/3。<strong>别再按&#8221;报告占三分之一&#8221;去分配时间了。</strong></p>
</div>
<p>我们直接看两份官方真题的评分表，硬碰硬：</p>
<table>
<thead>
<tr>
<th>赛题</th>
<th>报告</th>
<th>客观测试</th>
<th>总分</th>
<th>报告占比</th>
</tr>
</thead>
<tbody>
<tr>
<td>2024 H 题·自动行驶小车</td>
<td>20</td>
<td>要求(1)20 ＋ (2)20 ＋ (3)30 ＋ (4)30 ＝ 100</td>
<td>120</td>
<td>≈ 17%</td>
</tr>
<tr>
<td>2021 F 题·智能送药小车</td>
<td>20</td>
<td>基本要求 50（12/18/20）＋ 发挥部分 50（23/21/其他6）</td>
<td>120</td>
<td>≈ 17%</td>
</tr>
</tbody>
</table>
<p>两道题、隔了三年，结构却惊人一致：<strong>报告恒为 20 分，剩下 100 分全是能拿尺子量、能掐秒表算的客观测试。</strong> 客观测试占了约 <strong>83%</strong>。</p>
<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> 客观测试是排名的绝对主导。一份漂亮报告救不了一辆跑不动的车；但一辆能稳稳跑完的车，配上一份&#8221;按点填空&#8221;的及格报告，就足够你冲奖了。</p>
</div>
<p>打个比方：<strong>客观测试 vs 报告，就像高考的&#8221;笔试分&#8221;和&#8221;卷面整洁分&#8221;。</strong> 小车真能跑、跑得准、跑得快，那是 120 分里的 100 分；报告写得漂亮，只值 20 分。字再工整，题做错了也白搭——<strong>先把题做对，再把卷面写干净。</strong></p>
<h3>比&#8221;少拿分&#8221;可怕一万倍的事：失败即 0 分</h3>
<p>如果说&#8221;报告占比&#8221;是认知误区，那下面这个才是真正会让你前功尽弃的东西——<strong>红线。</strong></p>
<p>客观测试里埋了一大堆&#8221;踩了就该项归零、甚至整场归零&#8221;的硬规则。以 2024 H 题为例，官方&#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;" /> 2024 H 题的硬红线（踩一条就废一项）</p>
<ul>
<li>小车尺寸必须 ≤ <strong>25（长）× 15（宽）× 15（高）cm</strong>，超标不予测试；</li>
<li>必须是轮式，<strong>不得用履带、不得用麦克纳姆轮</strong>；</li>
<li><strong>只能前进，不得后退</strong>；</li>
<li><strong>必须用 TI MSPM0 系列单片机</strong>，不得用别的 MCU，而且控制板要把芯片<strong>暴露出来便于查验</strong>；</li>
<li><strong>不得安装摄像头</strong>；</li>
<li>小车在地面的投影<strong>脱离规定圆弧，该项 0 分</strong>；</li>
<li>完成时间<strong>超过规定一倍以上，该项 0 分</strong>。</li>
</ul>
</div>
<p>2021 F 题同样狠：发挥部分<strong>超过 60 秒直接计 0</strong>；投影<strong>连续落在黑线上超过 30cm</strong>、或整车越过黑实线、或两车连续接触超过 5 秒，<strong>该测试项直接 0 分</strong>。</p>
<p>这些红线为什么比&#8221;少拿几分发挥部分&#8221;严重得多？因为它们是&#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><strong>失败保护，就像考科目二&#8221;压线即挂&#8221;、像答题卡&#8221;涂错位&#8221;。</strong> 哪怕你每道题都会做，只要答题卡涂错了位（冲出赛道 / 超时 / 用错芯片 / 尺寸超标），整道题直接 0 分。所以&#8221;别犯低级错误归零&#8221;这件事，比你多攻一道难题划算得多。 也像打游戏——<strong>先保命，再刷分。</strong> 血都没了，输出再高也没意义。</p>
</div>
<p>关于&#8221;测试不过会怎样&#8221;，网上有种很流行的强表述：&#8221;测试不通过 = 报告不参与评分 = 直接 0 分&#8221;。这里要给你一个更准确的版本，免得你被吓得方向跑偏：</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>硬件不合规会被直接取消测试资格（该项不测试＝0 分）</strong>；最终按&#8221;赛区测评分 ＋ 报告评审&#8221;综合排名。但&#8221;测试不过则报告一律不计分&#8221;作为一条绝对规则，官方实施细则<strong>并没有这么写</strong>——那是二手资料的经验化解读。 准确的说法是：<strong>客观测试是排名的绝对主导，且设有大量&#8221;失败即 0 分 / 取消资格&#8221;的红线。</strong> 方向不变——避免归零是第一要务，只是别把它绝对化成&#8221;报告白写&#8221;。</p>
</div>
<h3>还有两个你迟早要面对的关卡</h3>
<p>光赛区跑得好还不够，想进国奖，得知道后面这两关：</p>
<ul>
<li><strong>综合测评（满分 30 分）：</strong> 这是冲国奖队伍的&#8221;复测确认&#8221;关，俗称&#8221;关小黑屋&#8221;。统一在 <strong>8 月 10 日 8:00–15:00</strong> 单独一天闭卷进行，<strong>不能上网、不能用手机和电脑，只能带纸质资料</strong>，由至少 3 名专家共同签字记录。<strong>只有超过专家组划定的最低分数线，作品才计入全国评审总分</strong>（这条线不公开）。说白了，就是把你三个人单独带进小房间限时做一道题，验证作品到底是不是你们自己做的、基本功扎不扎实。</li>
<li><strong>获奖比例（你得知道自己在跟多少人抢）：</strong> 全国奖总数 ≤ 参赛队的 <strong>8%</strong>（国一、国二大约三七开）；赛区一二三等总获奖 ≤ <strong>40%</strong>；赛区把 ≤ <strong>10%</strong> 的优秀队材料上报全国。还有个细则：<strong>同一所学校、同一道题，推荐参加全国评奖的总队数 ≤ 4 个。</strong></li>
</ul>
<p>把这两关串起来看，结论很冷酷也很清晰：<strong>想进国奖，你得先在赛区前 10%，而且测试硬实力必须真的够。</strong> 报告里的数据还不能造假——综合测评和复评是可能要你<strong>现场复测</strong>的，数据对不上直接出局。</p>
<h2>把&#8221;拿奖&#8221;拆成能训练的小目标</h2>
<p>好，现在我们手里有了评分逻辑这把尺子。接下来反过来想：<strong>评委按什么给分，我就该按什么顺序练。</strong> 这条能力链，是这一篇最想塞给你的&#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/614ae61ace78.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>
<p><strong>① 稳——不冲出赛道、不挂零。</strong> 这是地基，对应红线里那一堆&#8221;0 分&#8221;项。能力上靠的是<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;" /> &quot;禁后退&quot;逼出来的一个细节</p>
<p>H 题只能前进、不能后退。这意味着循迹一旦冲出去，你<strong>没法靠倒车找回来</strong>。所以丢线保护必须靠转向：全丢线时<strong>锁住上一次的偏差方向、把方向打死继续找线</strong>，同时把目标速度降到很低甚至点动。这点不做，禁后退题型就是直接出界 0 分。具体怎么写在《感知层》和《状态机与整车软件》两篇细讲。</p>
</div>
<p><strong>② 准——定点停车，投影覆盖顶点。</strong> 光会跑还不够，题目往往要求停在指定位置（比如投影要盖住 A/B/C/D 顶点）。这靠的是<strong>距离环 ＋ 到点判定逻辑</strong>，让车知道&#8221;我到了，该停了&#8221;。</p>
<p><strong>③ 快——发挥部分拼用时。</strong> H 题要求(4)明确写着&#8221;用时越少越好&#8221;，这就是发挥分。但提速会放大循迹和惯性误差，一不小心就冲出去（又回到红线 0 分）。所以这一步的原则是——</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> 现场只测有限几次，挂零的代价极大。正确做法是分档提速：用一个&#8221;目标速度&#8221;全局变量，每加一档跑 3–5 次确认稳定再加；过弯时联动降速、直道再抢时间。另外，电池从满电到半电，同样的输出下车速会明显往下掉，标定时记得用接近现场的电量。具体调法见《PID 调参实战》一篇。</p>
</div>
<p><strong>④ 报告闭环——误差分析 ＋ 实测数据。</strong> 报告虽只占 20 分，但这 20 分是&#8221;按点给分的填空题&#8221;，性价比其实不低。以 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>误差来源分析（车为什么会偏）</td>
</tr>
<tr>
<td>电路与程序设计</td>
<td>5</td>
<td>关键电路图 ＋ 控制流程</td>
</tr>
<tr>
<td>测试方案与结果</td>
<td>4</td>
<td>实测数据表 ＋ 结果分析</td>
</tr>
<tr>
<td>报告结构规范性</td>
<td>3</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;" /> 报告就是&quot;实验报告&quot;</p>
<p>评委要看的是一条<strong>&#8220;测了 → 有数据 → 会分析&#8221;的闭环证据链</strong>。光说&#8221;我设计了一辆循迹小车&#8221;没用；要有系统框图（怎么做）、误差来源分析（为什么会偏）、实测数据表 ＋ 结果分析（到底跑得多准）。光&#8221;理论分析 5 分 ＋ 测试结果 4 分&#8221;这 9 分，就是直接奖励你&#8221;会做实验&#8221;的。所以——<strong>报告随做随写、数据随测随存</strong>，别留到最后一晚硬赶。</p>
</div>
<h3>一个绕不开的策略问题：基本要求 vs 发挥部分</h3>
<p>赛题一般分&#8221;基本要求&#8221;和&#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;" /> 主线任务 vs 支线任务</p>
<p><strong>基本要求像游戏主线，发挥部分像支线。</strong> 主线必须通关才有资格评奖，稳拿；支线分高但难、容易翻车，等主线稳定通关后再去刷，刷不动就别强求——免得连主线进度都丢了。</p>
</div>
<p>以 H 题为例：要求(1)(2)各 20 分共 40 分，难度低、能 100% 稳过；要求(3)(4)各 30 分但难度高、风险大。理性的打法是：<strong>先把 40 分稳稳吃满，再去冲 (3)；与其赌难度最高的 (4)，不如先确保前面不掉链子。</strong> 立创 wiki 里有个很经典的案例：某队卡在&#8221;波形显示 +10 分&#8221;做不出来，被建议&#8221;波形做不出，就把 UI 设计的 +10 分拿满&#8221;——<a href="https://wiki.lckfb.com/zh-hans/lspi/competition/competition-guide.html" 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>现场<strong>不能稳定复现的功能，宁可不做。</strong> 因为一个不稳定的发挥功能，可能正好在测试那一次把车送出赛道，触发 0 分红线，反而把名次拉低。这就是为什么&#8221;稳&#8221;永远排在能力链第一位。</p>
</div>
<h2>来自前辈的几条血泪教训</h2>
<p>光讲道理太干，给你几个真实翻车现场，提前感受一下&#8221;坑&#8221;长什么样（细节和处置我们留到对应篇章展开）：</p>
<ul>
<li>
<p><strong>方案越复杂越烧时间。</strong> 有队伍上了&#8221;双摄像头&#8221;方案，整场都在优化图像处理，赛后才发现单摄像头改个朝向就能实现同样功能。更致命的是没做版本管理——第一版能跑的程序后来&#8221;复现不出来&#8221;，第三天又过度自信去研究高精度模型，最后回不到能用的版本。<strong>教训：先用最简方案跑通拿保底分；每个能跑的版本立刻打包备份，&#8221;能复现&#8221;比&#8221;更优&#8221;重要得多。</strong>（<a href="https://www.cnblogs.com/sosoeeee/p/16820505.html" target="_blank" rel="noopener noreferrer">来源</a>）</p>
</li>
<li>
<p><strong>硬件 / 机械问题，软件救不了。</strong> 同一台车在瓷砖地能跑 2m/s，换到木地板却加速不上去，根因是编码器机械摩擦 ＋ 电机 PID 失配；还有激光因虚焊间歇失效，最后靠焊死才解决。<strong>教训：关键信号线要焊接、别用杜邦线；机械和电源前两天就定型，后期只动参数。</strong></p>
</li>
<li>
<p><strong>车模和电机要冗余前置。</strong> 有人现场才发现车模尺寸超标、或电机扭矩不够，已经来不及了。<strong>教训：多备车模 / 电机 / 轮子 / 驱动 / 传感器；开赛先按题目尺寸和约束选定平台，别等软件调好才发现硬件违规。</strong></p>
</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>
</div>
<h2>本系列的学习地图</h2>
<p>这一篇是&#8221;为什么&#8221;，接下来的十一篇是&#8221;怎么做&#8221;。给你一张全图，知道每篇在能力链的哪个位置：</p>
<table>
<thead>
<tr>
<th>篇</th>
<th>主题</th>
<th>解决能力链的哪一环</th>
</tr>
</thead>
<tbody>
<tr>
<td>二</td>
<td>赛题进化史与押题</td>
<td>看懂出题套路</td>
</tr>
<tr>
<td>三</td>
<td>整车搭建与代码框架</td>
<td>0 阶段·分工与解耦</td>
</tr>
<tr>
<td>四</td>
<td>电机驱动与电源地基</td>
<td>稳·硬件地基</td>
</tr>
<tr>
<td>五</td>
<td>感知：灰度/电磁/编码器/IMU</td>
<td>稳 ＋ 准·反馈信号</td>
</tr>
<tr>
<td>六</td>
<td>PID 入门（搞懂 P/I/D）</td>
<td>稳·控制入门</td>
</tr>
<tr>
<td>七</td>
<td>PID 进阶（串级 ＋ 工程补丁）</td>
<td>稳·不翻车</td>
</tr>
<tr>
<td>八</td>
<td>PID 调参实战（核心）</td>
<td>准 ＋ 快·看波形治百病</td>
</tr>
<tr>
<td>九</td>
<td>进阶控制（几时该上）</td>
<td>快·锦上添花</td>
</tr>
<tr>
<td>十</td>
<td>K230 视觉与通信</td>
<td>准·更高级的眼睛</td>
</tr>
<tr>
<td>十一</td>
<td>状态机与整车软件</td>
<td>把一切串起来</td>
</tr>
<tr>
<td>十二</td>
<td>现场作战 ＋ 避坑 ＋ 开源</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>
<ol>
<li><strong>客观测试约 83%、报告约 17%</strong>——别再按&#8221;报告占 1/3&#8243;分配精力；</li>
<li><strong>&#8220;失败即 0 分&#8221;的红线，比少拿几分发挥部分可怕得多</strong>——保命第一；</li>
<li>按 <strong>稳 → 准 → 快 → 报告闭环</strong> 这条链去练，每一步都对得上评分表。</li>
</ol>
</div>
<p>把方向定好了，下一步自然是搞清楚&#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 篇）第 1 篇。</p>
<ol style="margin:.2em 0 0;padding-left:1.4em">
<li style="margin:.15em 0"><strong>第1篇 · 拿奖逻辑：把比赛拆成小目标（本篇）</strong></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"><a href="https://cloudlay.cn/nuedc-car-12-field-manual/">第12篇 · 现场作战+避坑+开源</a></li>
</ol>
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://cloudlay.cn/nuedc-car-01-how-to-score/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>从0到1带你打电赛·小车电控篇(二)：历年小车赛题进化史——看懂出题套路，押对方向</title>
		<link>https://cloudlay.cn/nuedc-car-02-history/</link>
					<comments>https://cloudlay.cn/nuedc-car-02-history/#respond</comments>
		
		<dc:creator><![CDATA[云间辞]]></dc:creator>
		<pubDate>Sun, 14 Jun 2026 17:08:32 +0000</pubDate>
				<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-02-history/</guid>

					<description><![CDATA[📚 本文是 「从 0 到 1 带你打电赛 · 小车电控篇」 系列（共 12 篇）第 2 篇。 第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 篇）第 2 篇。</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"><strong>第2篇 · 赛题进化史与押题（本篇）</strong></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"><a href="https://cloudlay.cn/nuedc-car-12-field-manual/">第12篇 · 现场作战+避坑+开源</a></li>
</ol>
</div>
<p>上一篇我们把&#8221;怎么拿奖&#8221;拆成了一条能力链：稳→准→快→报告。但还有一个问题没解决——你到底该练哪种&#8221;稳准快&#8221;？循迹？测距？视觉识别？双车通信？这些技能学起来都不便宜，四天三夜里全练一遍根本不够。</p>
<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;" /> 先记住一句话</p>
<p>看赛题进化，就像看一个人&#8221;从学会走，到学会开车&#8221;的过程：早期只要求小车能往返跑准距离（走路），后来要会循迹过弯（骑车），再后来要看摄像头认路、认数字（看着路标开车），现在是多车配合 + 精确停进车位（自动驾驶 + 自动泊车）。每一步都在前一步上加东西，从不凭空跳。</p>
</div>
<h2>先扫掉一个最常见的误区：电赛不是年年都有国赛</h2>
<p>很多新人一上来就栽在赛制上。这件事先说清楚，否则你押题的年份都会对错。</p>
<p>全国大学生电子设计竞赛（NUEDC，大家都叫它&#8221;电赛&#8221;）是<strong>两年一个周期</strong>：</p>
<ul>
<li><strong>奇数年（2021、2023、2025…）= 国赛</strong>，全国统一命题、规模最大，圈里叫&#8221;大年&#8221;；</li>
<li><strong>偶数年（2022、2024…）= 省级赛 + TI 杯模拟电子系统设计专题赛</strong>，圈里叫&#8221;小年&#8221;。</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;" /> 别把&quot;2022 双车跟随&quot;&quot;2024 自动行驶小车&quot;当成国赛题</p>
<p>网上资料经常笼统地把所有题都叫&#8221;电赛&#8221;，于是你会看到&#8221;2022 双车跟随&#8221;&#8221;2024 自动行驶小车&#8221;这种说法。严格讲，2022、2024 是<strong>偶数年的省赛 / TI 杯</strong>，不是国赛。这一点对押题很关键：<strong>省赛题往往是上一届国赛方向的简化或延伸</strong>，难度和时限通常更宽松。换句话说，研究国赛主线能帮你押省赛，反过来不一定成立。</p>
</div>
<p>这个&#8221;大年小年交替&#8221;，你可以想成奥运会和省运会：国赛像奥运会，奇数年全国总动员、最隆重；省赛 + TI 杯像省运会加专项邀请赛，偶数年小一号、按地区比。</p>
<p>赛制的更多细节（四天三夜怎么打、三人怎么分工）后面《整车搭建与代码框架》和《现场作战手册》会专门展开，这里不重复。我们直接进入正题——看题。</p>
<h2>第一阶段·早期（2003~2011）：把&#8221;机械控制&#8221;做准</h2>
<p>最早的小车题，核心诉求就一个字：<strong>准</strong>。让车按要求跑、停得准、算得对，根本还没有&#8221;看路&#8221;这回事。</p>
<p>奠基性的一题是 <strong>2003 年 E 题「简易智能电动车」</strong>。官方题面要求：</p>
<ul>
<li>直道行驶时，存储并显示每个薄铁片（路面中心标志）到起跑线的距离；</li>
<li>进入停车区，准确驶入车库，车身要完全进去；</li>
<li>停车后，准确显示全程行驶时间。</li>
</ul>
<p>听着挺朴素，但它把后面二十年小车题的骨架都立好了：<strong>测距、定点停车、全程计时</strong>。当年北京科技大学的队伍靠这题拿了国一。</p>
<p>那它的电控核心是什么？很简单：</p>
<ul>
<li><strong>传感器</strong>：在车轮上贴黑白条纹，用光电传感器（或霍尔开关）做&#8221;里程计&#8221;——轮子转一圈，反射光就产生固定个数的脉冲；</li>
<li><strong>算法</strong>：单片机数脉冲，就能算出走了多远、走得多快；</li>
<li><strong>闭环</strong>：一个简单的速度闭环，让车别忽快忽慢。</li>
</ul>
<p>到 <strong>2011 年</strong>，出现了一个很有意思的题：<strong>双车超车</strong>。两辆车在赛道上，用光电管寻迹、用红外开关测两车距离，控制车距实现超车。这是&#8221;多车协同&#8221;主线最早的雏形——日后 2021 送药双车、2022 双车跟随，思想都从这儿来：<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;都还没弄明白，那不管赛题怎么进化，地基都没打好。编码器和速度环的细节，《感知》和《PID 入门》两篇会手把手教。</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>
<ul>
<li>车体尺寸限制一脉相承：2001/2003 是&#8221;电动玩具车&#8221;（长 ≤ 35cm、宽 ≤ 15cm），2007 起改&#8221;小型电动车&#8221;（最大 300mm×200mm，高不限），到 2024 年量级压到约 25cm×15cm×15cm。<strong>尺寸卡得越来越死，直接影响你能塞多大的视觉模块</strong>——这也是后来大家偏爱紧凑型摄像头的原因之一。</li>
<li>网上常把&#8221;2001 自动往返&#8221;当起点。更准确地说，这条主线对应的是 2003 E 题；2001 本科组主线是&#8221;电动玩具车&#8221;，具体题面记载比较弱。押题别拿 2001 当锚点。</li>
</ul>
</div>
<h2>第二阶段·中期（2016~2022）：循迹 + 测距 + 双车，控制升级到&#8221;串级&#8221;</h2>
<p>车开始要&#8221;会看路&#8221;和&#8221;会配合&#8221;了。这一阶段的关键词是：<strong>多路循迹、超声测距、双车通信、串级 PID</strong>。</p>
<p>代表题包括 2016 年无线充电车、2017/2022 自动泊车，以及 <strong>2022 双车跟随</strong>。技术栈明显比早期厚了一层：</p>
<ul>
<li><strong>循迹</strong>：从 1 路红外，升级到多路红外 / 灰度阵列。车不再只会直线跑，能沿着地面引导线过弯；</li>
<li><strong>测距</strong>：超声波测距上场，用来控制车距、避障、入库；</li>
<li><strong>通信</strong>：双车任务需要稳定的车间通信，最常用的是<strong>蓝牙串口模块 HC-05</strong>（配置简单、成本低）；如果是跨楼宇的远距离任务，才会考虑 LoRa；</li>
<li><strong>控制</strong>：单环已经不够用了，<strong>串级 PID（双环）</strong> 成为标配。</li>
</ul>
<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;" /> 串级 / 双环 PID，像&quot;公司里的老板和员工&quot;</p>
<p>外环（老板）只负责定目标，比如&#8221;车速给我到 1m/s&#8221;；内环（员工）负责具体怎么给油门，把这个目标真正落地。规矩是：<strong>先把员工训练靠谱（内环调稳），老板下指令才有意义（再调外环）</strong>。反过来，员工还站不住，老板喊破嗓子也白搭。</p>
</div>
<p>这条规矩——<strong>双环永远从内往外调</strong>——是贯穿整个 PID 体系的铁律，后面《PID 进阶》和《PID 调参实战》会反复强调。这里你先有个印象就行：到了中期，调参不再是调一个环，而是调两个环之间的配合。</p>
<h2>第三阶段·近期（2021~2024）：视觉 + 决策被推到舞台中央</h2>
<p>这是变化最剧烈的阶段。车不光要看引导线了，它要<strong>看懂世界</strong>——认数字、认靶标、追光斑——然后<strong>自己做决策</strong>。我们挑三道有代表性的题细看。</p>
<h3>2021 F 题「智能送药小车」：视觉融合的里程碑</h3>
<p>场景设定得很生活化：医院走廊地面上有一条红线，墙上 / 地上用黑色数字纸贴着病房号。任务是——单车在规定时间内把药送到指定的近距 / 中距病房并返回药房；进阶部分还要两车协同，一起把药送到同一个中距病房。</p>
<p>电控核心一下子丰富起来：</p>
<ul>
<li>摄像头做<strong>阈值化红线循迹</strong>；</li>
<li>摄像头做<strong>黑色数字识别</strong>（认病房号）；</li>
<li>陀螺仪测角度控制转向；</li>
<li>光电编码器测里程；</li>
<li>单片机跑 PID。</li>
</ul>
<p>主控通常是 STM32F407，视觉用 OpenMV 或 K210，车体是舵机四驱小车。</p>
<p>这题最值得讲的，是一段真实的国一战例——它告诉你&#8221;桌面调通&#8221;和&#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>一支 2021 F 题国一队，一开始用 OpenMV 的<strong>模板匹配</strong>来识别病房数字。桌面上调得好好的，一到真实走廊，光照、角度一变，<strong>识别率根本不可靠</strong>。他们临场把方案切到 K210 神经网络，在<strong>固定 20~25cm 的拍摄距离</strong>重新采集数据集、训练 tflite 模型，才把识别率拉到 <strong>80%~90%+</strong>。</p>
<p>还有两个救命的小招： 1. <strong>用置信度阈值过滤误判</strong>（数字 7 最容易被认错）； 2. <strong>用一个 8 元素数组做多帧投票</strong>——不是看一帧就定结果，而是累计多帧检测再排序定结论。</p>
<p>后期他们甚至上了<strong>双 K210</strong>，实现&#8221;行进中不停车识别&#8221;。</p>
<p>教训：<strong>视觉识别一定要在真实的拍摄距离和光照下采数据、训练</strong>，别信桌面上调通的模板匹配；多帧投票 + 置信度阈值，是去抖动最廉价、最好用的招。</p>
</div>
<p>同一支队还有个惨痛的硬件插曲：<strong>第二辆车在临赛前 UART 和 IO 引脚损坏</strong>，被迫整片换主控芯片，在封箱前最后几小时才把两车功能补完。所以关键引脚要早做冗余验证、备好替换板，别把最后几小时排满。</p>
<p>这段经验，到《视觉与通信》那篇会接着深挖——尤其是&#8221;视觉模块只回传结论、不回传整张图&#8221;的设计思想。这里你先记住一个判断：<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>115200 波特率就开始乱码</strong>，把波特率降到 <strong>9600</strong> 才稳。这个坑在 2024 H 题里也有队伍踩过，后面还会再遇到它。</p>
</div>
<h3>2023 E 题「运动目标控制与自动追踪」：毫米级定位的残酷一课</h3>
<p>这题严格说不是地面小车，而是<strong>二轴舵机云台 + 激光笔 + OpenMV 视觉</strong>的组合，但它属于控制类主线，而且给所有人上了一堂关于&#8221;机械精度&#8221;的课，非常值得看。</p>
<p>系统是：OpenMV 视觉 + STM32 + 二轴云台 + 激光笔，做闭环。功能包括复位、让激光沿屏幕边 / A4 纸边运动、自动追踪一个光斑。题目建议<strong>红色激光开环、绿色激光闭环</strong>（闭环那部分需要 PID 跟踪）。</p>
<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;" /> 战例：1° 的误差，在 1 米外就是 1.7 厘米</p>
<p>激光打到 1 米外的屏幕上。普通 PWM 舵机有回差，<strong>1° 的误差，在 1 米距离上就放大成约 1.7cm 的偏移</strong>。你要做的是毫米级定位，普通舵机的分辨率和回差根本压不住。</p>
<p>雪上加霜：K210 在 320×240 分辨率下，1 米外的黑胶带不足 10 个像素——<strong>连&#8221;看清&#8221;都困难</strong>。</p>
<p>各队最后的解法：换<strong>高精度 / 总线舵机</strong>（带编码器），或者<strong>步进电机 + 齿轮减速</strong>，或者普通舵机配 2:1、3:1 减速；同时做两套坐标标定——云台角 <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;" /> 屏幕坐标、相机像素 <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;" /> 屏幕坐标（类似电阻屏标定，控制激光打到几个已知点，建一个变换矩阵）。</p>
<p>教训：<strong>精密指向任务，先算&#8221;角度误差 → 末端位移&#8221;这笔灵敏度账。机械精度不够，再好的 PID 也救不回来；视觉分辨率不够，就靠近拍或换高分辨率模块。</strong></p>
</div>
<p>这就是那个著名的&#8221;激光笔杠杆效应&#8221;——手腕（舵机）动一点点，1 米外墙上的红点就跑老远；指得越远，手越要稳。所以精密指向必须用&#8221;更细的手腕&#8221;。</p>
<h3>2024 H 题「自动行驶小车」：回到纯传感器，但把双环 PID 玩到极致</h3>
<p>有意思的是，2024 H 题反而&#8221;退&#8221;了一步——<strong>不许用摄像头</strong>，强制用 <strong>TI 的 MSPM0（M0G3507）平台</strong>，靠<strong>灰度循迹 + 陀螺仪</strong>。但它把控制做到了很硬核的程度。</p>
<p>场地是这样的：≥ 220cm×120cm，两段对称的半圆弧，半径 40cm，黑弧线宽约 1.8cm。四个任务由易到难：</p>
<table>
<thead>
<tr>
<th>任务</th>
<th>路径</th>
<th>时限</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>A → B</td>
<td>≤ 15s</td>
</tr>
<tr>
<td>2</td>
<td>A→B→弧→C→D→弧→A 一圈</td>
<td>≤ 30s</td>
</tr>
<tr>
<td>3</td>
<td>A→C→弧→B→D→弧→A 一圈</td>
<td>≤ 40s</td>
</tr>
<tr>
<td>4</td>
<td>按路径跑 4 圈</td>
<td>越快越好</td>
</tr>
</tbody>
</table>
<p>控制方案是<strong>七 / 八路灰度识线 + 维特陀螺仪测偏航</strong>，跑<strong>姿态环 + 寻迹环双环 PID</strong>：直线 / 斜线段靠姿态环，过弯靠寻迹环。控制周期 <strong>10ms</strong>（定时器中断，TIM2）。</p>
<p>实战里有两个流派，你可以都参考：</p>
<ul>
<li><strong>流派一（陀螺主控向）</strong>：用位置式 PID，直接以陀螺的<strong>绝对偏航角</strong>控制直行和转向，把算出来的 PWM 增 / 减量，叠加到左右轮的基础占空比上。逻辑是&#8221;遇到黑线开寻迹环，离开黑线开转向环&#8221;；</li>
<li><strong>流派二（分段动态 PID）</strong>：直线段用大 Kp、Kd=0 求快；弯道段用更大的 Kp + 大 Kd 求稳，按陀螺角速度或灰度命中宽度来切换。</li>
</ul>
<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;的时候有效，一旦车在直线段、或者短暂丢线，灰度就抓瞎了。这时候靠<strong>陀螺积分出来的偏航角（yaw）保持航向</strong>最可靠：记录起始 yaw 当基准，用 PD 控制偏差，叠加到左右占空比上。这是 2024 H 题的主流方案。</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;" /> 一组 PID 参数别想通吃直线和弯道</p>
<p>直道求快需要小 Kd，弯道求稳需要大 Kp 大 Kd，单组参数顾此失彼。国奖队普遍用<strong>分段动态切换</strong>。注意切换点要加&#8221;迟滞&#8221;（hysteresis），别在临界点反复横跳；切换瞬间可能有跳变，最好做平滑过渡。</p>
</div>
<p>具体到怎么把这几条揉进一份能跑的代码、怎么对着波形一步步整定，是《PID 调参实战》整篇要解决的事，这里点到为止。下面给一段差速控向的骨架，让你先有个手感——不管误差是来自陀螺偏航角还是灰度加权，套路都是&#8221;误差 → PID → 叠加到左右轮&#8221;：</p>
<pre><code class="language-c">// 控制周期 10ms（定时器中断里调用）
// err 可以来自：陀螺偏航角偏差，或灰度加权偏差
float turn = Kp * err + Kd * (err - err_prev);
err_prev  = err;

int left  = base_speed - turn;   // 左轮 = 基础速度 - 转向量
int right = base_speed + turn;   // 右轮 = 基础速度 + 转向量
// 符号按你自己的传感器朝向 / 电机接线定，调反了会朝偏差方向越冲越远</code></pre>
<p>但 2024 H 题也暴露了纯灰度 + 陀螺方案的两个致命弱点，这两个坑你一定要提前防：</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>，导致依赖&#8221;检测到黑线才切状态&#8221;的整套程序逻辑节点全部后移，最后只跑了三圈半。</p>
<p>根因之一是灰度的&#8221;死缝&#8221;问题：<strong>黑线正好卡在两路灰度中间时，两路都返回 0</strong>——车明明压着线，程序却判成&#8221;丢线&#8221;。</p>
<p>这队还顺带踩了一串小坑：某 GPIO 输入异常（换个引脚解决）、轮子和电机轴打滑（上胶固定）、115200 串口乱码（降到 9600）。</p>
<p>教训：<strong>纯灰度状态机一定要有兜底</strong>——用编码器里程计做后备，在&#8221;按理走了这么远还没见到线&#8221;时强制切状态 / 转向；传感器间距要小到不会让线漏在缝里；到了现场，必须重新标定灰度的电位器去适配当天的光照。</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>多份 2024 H 题复盘都指出：陀螺仪的偏航角有<strong>非固定的零漂</strong>，跑多圈之后航向会缓慢偏移，只能靠人工修正每圈的转向角硬凑，仍然消不干净。</p>
<p>对策：<strong>上电先静置标定零偏 + 做温补</strong>，并且用外部基准（灰度线 / 里程计）周期性地校正 yaw，给陀螺上&#8221;双保险&#8221;。</p>
<p>你可以把陀螺零漂想成&#8221;戴久了会走偏的旧手表&#8221;：你以为它读的是真角度，其实它每分钟都悄悄快一点点，跑几圈方向就全错了——所以要定时拿&#8221;标准钟&#8221;（灰度线 / 里程）对一下表。顺带一提，MPU6050 的零漂明显比维特智能（WT61 / HWT101 等，约 0.05° 级）这类高精度陀螺要大。</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;" /> 一定要纠正的一个流传错误：2019 没有地面循迹小车</p>
<p>网上经常把 <strong>2019 B 题「巡线机器人」当成地面小车</strong>来准备——它其实是<strong>四旋翼无人机绕杆巡检</strong>：从距 A 杆 1m 内起飞，1m 定高绕杆巡检（起飞 → A 杆 → 线缆 → 绕 B 杆 → 线缆 → A 杆 → 降落），≤150s，发现黄色异物在 30cm 内声光提示。2019 真正的<strong>地面控制车是 A 题「动态无线充电系统」</strong>（车边走边接收地面线圈的电）。押题、复盘别记错这一笔。</p>
</div>
<h2>把这条线拎出来：四个清晰的演进方向</h2>
<p>把三个阶段叠在一起看，趋势其实非常干净。我用一张表收一下：</p>
<table>
<thead>
<tr>
<th>维度</th>
<th>早期（2003~2011）</th>
<th>中期（2016~2022）</th>
<th>近期（2021~2024）</th>
<th>趋势预测</th>
</tr>
</thead>
<tbody>
<tr>
<td>传感器</td>
<td>单路红外 / 码盘</td>
<td>多路灰度阵列 + 超声</td>
<td>摄像头 + AI 芯片</td>
<td>激光雷达 / 深度相机</td>
</tr>
<tr>
<td>控制</td>
<td>单环速度 PID</td>
<td>串级双环 PID</td>
<td>多模态融合（+卡尔曼）</td>
<td>融合 + 自适应</td>
</tr>
<tr>
<td>协同</td>
<td>单车</td>
<td>双车</td>
<td>双车 / 多车</td>
<td>多车 + V2X</td>
</tr>
<tr>
<td>指标</td>
<td>&#8220;到达&#8221;</td>
<td>精确入库</td>
<td>厘米 / 毫米级停靠</td>
<td>精度继续抬高</td>
</tr>
<tr>
<td>主控</td>
<td>51 / STM32</td>
<td>STM32</td>
<td>STM32 → 强制 TI MSPM0</td>
<td>延续 TI MSPM0</td>
</tr>
</tbody>
</table>
<p>四句话总结出题人的脑回路：</p>
<ol>
<li><strong>视觉权重一路走高</strong>：从&#8221;无视觉&#8221;，到 OpenMV/K210，再到 K230/MaixCam；</li>
<li><strong>精确停靠越来越变态</strong>：从&#8221;到了就行&#8221;，到厘米级，再到毫米级；</li>
<li><strong>多车协同成为常客</strong>：单车 → 双车 → 多车，背后都是&#8221;通信 + 相对定位&#8221;；</li>
<li><strong>决策状态机不可或缺</strong>：题目里越来越多&#8221;识别完再决定往哪走&#8221;的环节，背后必然是一套状态机（这套东西《状态机与整车软件》会专门讲）。</li>
</ol>
<h2>那 2025 / 省赛，该往哪押？</h2>
<p>押题这事，没人能打包票，但顺着上面这条线，方向是可以判断的。多份器件清单分析和复盘给出的<strong>较高概率方向</strong>是：</p>
<ul>
<li><strong>复杂小车系统</strong>：小车 + 摄像头 + 云台 + 激光笔 + 感光纸绘图这类复合系统；或者&#8221;视觉循迹 + 精确停靠 + 双车 / 多车协同&#8221;的组合；</li>
<li><strong>主控大概率延续 TI MSPM0 系列</strong>（M0G3507）。2024 起电赛主推 TI 平台，这个惯性短期内不会变；</li>
<li><strong>视觉模块偏好紧凑型</strong>：因为车体尺寸卡得严，MaixCam 这类小模块更吃香；</li>
<li><strong>可能融合更多模态</strong>：摄像头 + 超声波 + 雷达；目标跟踪场景会用到卡尔曼滤波去抖。</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;，不如反过来想：上面这些方向，<strong>公共的电控能力</strong>是哪几样？答案很集中—— &#8211; <strong>多路灰度 / 电磁循迹</strong>（差速控向）； &#8211; <strong>串级双环 PID</strong> + 一身&#8221;工程补丁&#8221;（积分限幅、抗饱和、分段切换）； &#8211; <strong>陀螺 / 编码器融合</strong>保航向、做里程兜底； &#8211; <strong>视觉模块只回传结论 + 帧协议通信</strong>； &#8211; <strong>一套能管&#8221;待命 / 直道 / 弯道 / 识别 / 停靠 / 丢线找回 / 急停&#8221;的状态机</strong>。</p>
<p>把这五样练扎实，不管今年出哪道题，你都接得住。这也正是本系列后面每一篇要带你逐个攻克的东西。</p>
</div>
<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;的主线上稳步加码。看懂这条线，你的备赛就有了主心骨：地基（电机、电源、感知）必须先稳，PID 必须吃透，视觉和状态机按方向补齐。</p>
</div>
<p>押对了方向，下一步就是动手搭车。三个人怎么分工、主控为什么选 TI MSPM0、怎么搭一套&#8221;算法和芯片解耦&#8221;的代码框架，让你在 STM32 上练通的东西能整段平移过去——这些就是整车搭建与代码框架那一篇要解决的事。</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 篇）第 2 篇。</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"><strong>第2篇 · 赛题进化史与押题（本篇）</strong></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"><a href="https://cloudlay.cn/nuedc-car-12-field-manual/">第12篇 · 现场作战+避坑+开源</a></li>
</ol>
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://cloudlay.cn/nuedc-car-02-history/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>
