<?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/%e8%bd%af%e4%bb%b6%e6%9e%b6%e6%9e%84/feed/" rel="self" type="application/rss+xml" />
	<link>https://cloudlay.cn</link>
	<description>life</description>
	<lastBuildDate>Sun, 14 Jun 2026 17:08:33 +0000</lastBuildDate>
	<language>zh-Hans</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.0</generator>

<image>
	<url>https://cloudlay.cn/wp-content/uploads/2026/01/avatar.ico</url>
	<title>软件架构 &#8211; Cloudlay</title>
	<link>https://cloudlay.cn</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>从0到1带你打电赛·小车电控篇(十一)：把一切串起来——状态机、控制环时序与整车软件架构</title>
		<link>https://cloudlay.cn/nuedc-car-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-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>
