<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>工业控制 on Tech Snippets - 嵌入式技术笔记</title>
    <link>https://tech-snippets.xyz/tags/%E5%B7%A5%E4%B8%9A%E6%8E%A7%E5%88%B6/</link>
    <description>Recent content in 工业控制 on Tech Snippets - 嵌入式技术笔记</description>
    <generator>Hugo</generator>
    <language>zh-cn</language>
    <lastBuildDate>Mon, 25 May 2026 19:00:00 +0800</lastBuildDate>
    <atom:link href="https://tech-snippets.xyz/tags/%E5%B7%A5%E4%B8%9A%E6%8E%A7%E5%88%B6/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Xenomai 实时操作系统深度实战指南：从双内核架构到工业级延迟优化</title>
      <link>https://tech-snippets.xyz/posts/xenomai-real-time-os-comprehensive-guide/</link>
      <pubDate>Mon, 25 May 2026 19:00:00 +0800</pubDate>
      <guid>https://tech-snippets.xyz/posts/xenomai-real-time-os-comprehensive-guide/</guid>
      <description>前言 如果你在工业自动化领域做过嵌入式开发，应该听过这样的抱怨：「Linux 什么都好，就是不够实时」。这句话背后藏着一个非常现实的困境——Linux 生态太强大了，驱动、网络、文件系统、调试工具应有尽有，但它天生就不是为了微秒级确定性设计的。当你的运动控制器需要在 100 µs 内响应编码器中断、当你的机器人关节需要每 1ms 完成一次 PID 闭环计算时，主线 Linux 的调度抖动可能让整个系统失控。
于是就有了三条路：第一条路是彻底放弃 Linux，改用纯 RTOS——VxWorks、QNX、或者 FreeRTOS，但代价是你得放弃整个 Linux 生态；第二条路是 PREEMPT_RT——给 Linux 内核打上实时补丁，这是我们之前详细讨论过的方案；第三条路就是今天的主角：Xenomai——它不走「改造 Linux」的路线，而是走「与 Linux 共存」的双内核架构路线。
我第一次接触 Xenomai 是在一个六轴机械臂项目上。当时客户要求关节控制周期 1ms，最大抖动不能超过 50 µs。我们先用了 PREEMPT_RT，在隔离 CPU、关中断、线程优先级拉满的情况下，最坏情况抖动还是冲到了 120 µs，偶尔还会有 200 µs 的尖刺。后来换成 Xenomai 3 Cobalt 内核，同样的硬件，最坏情况抖动稳定在 15 µs 以内，而且应用层的代码改动量不到 20%。
写这篇文章的目的，不是要争论 Xenomai 和 PREEMPT_RT 谁更好——它们有各自的适用场景。我想做的是把 Xenomai 的技术本质讲清楚，从双内核架构的设计哲学讲起，到实际的环境搭建、应用开发、延迟测量与调优，最后给出我在多个工业项目中验证过的最佳实践。
一、为什么需要 Xenomai？PREEMPT_RT 的极限在哪里？ 在深入 Xenomai 之前，我们得先搞清楚一个问题：既然 PREEMPT_RT 能让 Linux 变成实时系统，为什么还需要 Xenomai？
1.1 PREEMPT_RT 的本质：把 Linux 尽量改得「更实时」 PREEMPT_RT 的核心思路是最大化 Linux 内核的可抢占性：</description>
      <content:encoded><![CDATA[<h2 id="前言">前言</h2>
<p>如果你在工业自动化领域做过嵌入式开发，应该听过这样的抱怨：「Linux 什么都好，就是不够实时」。这句话背后藏着一个非常现实的困境——Linux 生态太强大了，驱动、网络、文件系统、调试工具应有尽有，但它天生就不是为了微秒级确定性设计的。当你的运动控制器需要在 100 µs 内响应编码器中断、当你的机器人关节需要每 1ms 完成一次 PID 闭环计算时，主线 Linux 的调度抖动可能让整个系统失控。</p>
<p>于是就有了三条路：第一条路是彻底放弃 Linux，改用纯 RTOS——VxWorks、QNX、或者 FreeRTOS，但代价是你得放弃整个 Linux 生态；第二条路是 PREEMPT_RT——给 Linux 内核打上实时补丁，这是我们之前详细讨论过的方案；第三条路就是今天的主角：<strong>Xenomai</strong>——它不走「改造 Linux」的路线，而是走「与 Linux 共存」的双内核架构路线。</p>
<p>我第一次接触 Xenomai 是在一个六轴机械臂项目上。当时客户要求关节控制周期 1ms，最大抖动不能超过 50 µs。我们先用了 PREEMPT_RT，在隔离 CPU、关中断、线程优先级拉满的情况下，最坏情况抖动还是冲到了 120 µs，偶尔还会有 200 µs 的尖刺。后来换成 Xenomai 3 Cobalt 内核，同样的硬件，最坏情况抖动稳定在 15 µs 以内，而且应用层的代码改动量不到 20%。</p>
<p>写这篇文章的目的，不是要争论 Xenomai 和 PREEMPT_RT 谁更好——它们有各自的适用场景。我想做的是把 Xenomai 的技术本质讲清楚，从双内核架构的设计哲学讲起，到实际的环境搭建、应用开发、延迟测量与调优，最后给出我在多个工业项目中验证过的最佳实践。</p>
<h2 id="一为什么需要-xenomaipreempt_rt-的极限在哪里">一、为什么需要 Xenomai？PREEMPT_RT 的极限在哪里？</h2>
<p>在深入 Xenomai 之前，我们得先搞清楚一个问题：既然 PREEMPT_RT 能让 Linux 变成实时系统，为什么还需要 Xenomai？</p>
<h3 id="11-preempt_rt-的本质把-linux-尽量改得更实时">1.1 PREEMPT_RT 的本质：把 Linux 尽量改得「更实时」</h3>
<p>PREEMPT_RT 的核心思路是<strong>最大化 Linux 内核的可抢占性</strong>：</p>
<ul>
<li>把所有 spinlock 改成可抢占的 mutex</li>
<li>把中断处理线程化，让高优先级任务可以抢占中断</li>
<li>引入优先级继承，解决优先级反转问题</li>
<li>优化调度器，减少调度延迟</li>
</ul>
<p>但不管怎么改，PREEMPT_RT 依然是「Linux 内核的一部分」。它跑在同一个地址空间，共享同一个调度器，使用同一个内存管理子系统。这意味着它永远摆脱不了 Linux 的一些结构性开销：</p>
<ul>
<li><strong>页错误处理</strong>：即使是实时线程，访问未映射的内存页依然会触发缺页异常，这可能需要几百微秒甚至几毫秒</li>
<li><strong>RCU 回调</strong>：虽然有 RCU priority boosting，但极端情况下依然可能造成阻塞</li>
<li><strong>内存回收</strong>：即使是 mlockall 锁定的内存，在极端内存压力下依然可能有意外</li>
<li><strong>各种子系统的不可预知性</strong>：网络、存储、驱动层都可能引入意料之外的延迟</li>
</ul>
<h3 id="12-xenomai-的哲学让实时内核和-linux-内核平起平坐">1.2 Xenomai 的哲学：让实时内核和 Linux 内核平起平坐</h3>
<p>Xenomai 走了一条完全不同的路——它不是「给 Linux 打补丁」，而是<strong>在硬件之上同时运行两个内核</strong>：</p>
<ul>
<li><strong>Cobalt 内核</strong>（实时内核）：专门处理实时任务，极简设计，只有几千行代码，没有复杂的内存管理和调度开销</li>
<li><strong>Linux 内核</strong>（通用内核）：处理所有非实时任务，完整的 Linux 生态</li>
</ul>
<p>这两个内核通过一个叫做 <strong>IPE（Inter-Pipeline Execution）</strong> 的机制协调工作。当中断到来时，Cobalt 内核首先检查这个中断是否是实时中断。如果是，直接由实时内核处理；如果不是，再转发给 Linux 内核。</p>
<p>关键在于：<strong>Cobalt 内核永远不会被 Linux 内核抢占</strong>。不管 Linux 里面在干什么——不管是在做大规模内存回收，还是在处理网络拥塞，还是在触发 kernel panic——实时任务的调度都不受影响。</p>
<h3 id="13-延迟数据对比preempt_rt-vs-xenomai">1.3 延迟数据对比：PREEMPT_RT vs Xenomai</h3>
<p>这是在同一台硬件（i.MX6 Quad，1GHz）上测试的 1ms 周期任务延迟数据：</p>
<table>
<thead>
<tr>
<th>指标</th>
<th>PREEMPT_RT（优化后）</th>
<th>Xenomai 3 Cobalt</th>
</tr>
</thead>
<tbody>
<tr>
<td>平均延迟</td>
<td>8 µs</td>
<td>5 µs</td>
</tr>
<tr>
<td>99% 延迟</td>
<td>35 µs</td>
<td>10 µs</td>
</tr>
<tr>
<td>最坏情况延迟</td>
<td>120 µs</td>
<td>14 µs</td>
</tr>
<tr>
<td>运行时间</td>
<td>24 小时</td>
<td>24 小时</td>
</tr>
</tbody>
</table>
<p>可以看到，平均延迟差距不大，但在最坏情况延迟上，Xenomai 的优势是压倒性的。这就是为什么在工业控制、机器人、运动控制这些对最坏情况延迟有严格要求的领域，Xenomai 依然是首选方案。</p>
<h3 id="14-什么时候该用-xenomai什么时候该用-preempt_rt">1.4 什么时候该用 Xenomai，什么时候该用 PREEMPT_RT？</h3>
<p><strong>选择 Xenomai 的场景：</strong></p>
<ul>
<li>最坏情况延迟要求 &lt; 50 µs</li>
<li>周期任务频率 &gt; 1kHz</li>
<li>系统需要长时间（数月甚至数年）稳定运行，不能有任何延迟尖刺</li>
<li>可以接受一定程度的应用层代码修改</li>
</ul>
<p><strong>选择 PREEMPT_RT 的场景：</strong></p>
<ul>
<li>延迟要求在几百微秒量级即可</li>
<li>希望应用代码 100% 兼容标准 POSIX</li>
<li>需要充分利用 Linux 生态的所有功能</li>
<li>团队没有实时系统专业经验</li>
</ul>
<h2 id="二xenomai-双内核架构深度解析">二、Xenomai 双内核架构深度解析</h2>
<p>理解 Xenomai 的第一步，是搞清楚它的架构到底是怎么工作的。</p>
<h3 id="21-整体架构概览">2.1 整体架构概览</h3>
<p><img alt="Xenomai 双内核架构示意图" loading="lazy" src="/images/posts/2026/05/xenomai-dual-kernel-architecture.svg"></p>
<p>从下到上，Xenomai 的架构分为四层：</p>
<p><strong>第一层：硬件层</strong></p>
<ul>
<li>CPU、中断控制器、定时器</li>
<li>Cobalt 和 Linux 共享同一套硬件</li>
</ul>
<p><strong>第二层：Dovetail 层</strong></p>
<ul>
<li>这是 Xenomai 3.1+ 引入的中断流水线机制</li>
<li>取代了之前的 Adeos（Adaptive Domain Environment for Operating Systems）</li>
<li>负责中断分发和内核间的协调</li>
</ul>
<p><strong>第三层：双内核层</strong></p>
<ul>
<li><strong>Cobalt 内核</strong>：实时内核，调度实时线程</li>
<li><strong>Linux 内核</strong>：通用内核，调度普通线程</li>
</ul>
<p><strong>第四层：应用层</strong></p>
<ul>
<li><strong>实时应用</strong>：链接 libcobalt，调用 Xenomai 原生 API 或 POSIX API</li>
<li><strong>普通应用</strong>：标准 Linux 应用</li>
</ul>
<h3 id="22-dovetail-中断流水线是如何工作的">2.2 Dovetail 中断流水线是如何工作的？</h3>
<p>Dovetail 是整个双内核架构的核心。它不是一个 hypervisor，也不是一个虚拟机。它是一个非常轻量级的中断管道（interrupt pipeline）。</p>
<p>当中断从硬件产生时，它首先到达 Dovetail，然后 Dovetail 按照注册顺序把中断分发给各个内核：</p>
<pre tabindex="0"><code>硬件中断 → Dovetail → Cobalt 内核（先检查）
                      → 如果是实时中断，处理，不转发给 Linux
                      → 如果不是实时中断，转发给 Linux 内核
</code></pre><p>这个顺序是关键——Cobalt 永远排在 Linux 前面。所以实时中断永远会被优先处理，Linux 只能拿到 Cobalt 不要的中断。</p>
<p>这就解释了为什么 Xenomai 的延迟那么稳定——不管 Linux 里面在干什么，实时中断永远第一时间被 Cobalt 处理。</p>
<h3 id="23-cobalt-调度器为什么它这么快">2.3 Cobalt 调度器：为什么它这么快？</h3>
<p>Cobalt 调度器和 Linux CFS 调度器相比，简直简单到了极致：</p>
<ul>
<li>只有优先级调度，没有 CFS 的公平调度</li>
<li>每个优先级一个链表，总共 256 个优先级（0-255，数值越大优先级越高）</li>
<li>调度时直接找最高优先级的非空链表，取第一个任务</li>
<li>整个调度过程是 O(1) 的时间复杂度</li>
<li>没有负载均衡，没有组调度，没有各种复杂的统计</li>
</ul>
<p>而且 Cobalt 调度器是<strong>完全不可抢占的</strong>吗？不，它是<strong>可抢占的，但只被更高优先级的任务抢占</strong>。一旦一个实时任务开始运行，它会一直运行直到：</p>
<ol>
<li>它主动让出 CPU</li>
<li>它调用了阻塞的系统调用</li>
<li>有更高优先级的任务就绪</li>
</ol>
<p>这种简单粗暴的设计，换来的是确定性。</p>
<h3 id="24-内存模型实时任务的内存是怎么管理的">2.4 内存模型：实时任务的内存是怎么管理的？</h3>
<p>这是 Xenomai 另一个聪明的设计：实时任务的内存不是由 Cobalt 管理的，而是<strong>由 Linux 预先分配好，然后锁定在物理内存中</strong>。</p>
<p>具体流程是：</p>
<ol>
<li>实时应用在 Linux 上下文中分配内存（malloc/mmap）</li>
<li>调用 mlockall() 把所有内存锁定，防止换出</li>
<li>切换到 Cobalt 上下文执行</li>
<li>执行过程中，Cobalt 不会做任何内存分配或释放</li>
</ol>
<p>换句话说，Cobalt 内核本身几乎不做内存管理。所有复杂的内存管理工作都交给 Linux 在非实时阶段完成。实时阶段只使用预先锁定好的内存，完全避免了页错误和内存回收的开销。</p>
<h3 id="25-两个内核如何通信">2.5 两个内核如何通信？</h3>
<p>Cobalt 和 Linux 之间需要通信，比如实时任务需要打印日志、需要读写文件、需要发送网络数据包。</p>
<p>Xenomai 提供了几种通信机制：</p>
<p><strong>1. 代理系统调用（Proxy syscalls）</strong></p>
<ul>
<li>实时任务调用 Linux 系统调用时，会通过代理转发给 Linux 执行</li>
<li>这个过程是异步的，不会阻塞实时任务</li>
<li>但代价是有一定的延迟，所以不建议在实时路径中频繁使用</li>
</ul>
<p><strong>2. 共享内存（Shared memory）</strong></p>
<ul>
<li>Cobalt 和 Linux 共享同一块物理内存</li>
<li>可以通过共享内存交换大量数据</li>
<li>需要自己实现同步机制</li>
</ul>
<p><strong>3. XDDP（Xenomai Data Distribution Protocol）</strong></p>
<ul>
<li>Xenomai 提供的实时套接字机制</li>
<li>支持实时任务之间、实时任务和 Linux 任务之间的通信</li>
<li>延迟低，确定性好</li>
</ul>
<h2 id="三环境搭建从零开始部署-xenomai-3">三、环境搭建：从零开始部署 Xenomai 3</h2>
<p>现在我们开始实战，第一步是搭建 Xenomai 开发环境。</p>
<h3 id="31-硬件选择">3.1 硬件选择</h3>
<p>Xenomai 支持的架构非常广泛：x86_64、ARM32、ARM64、PowerPC 等等。但要获得最好的实时性能，你需要注意：</p>
<p><strong>推荐的开发板：</strong></p>
<ul>
<li>Raspberry Pi 4（ARM64）：性价比高，社区支持好，实测最坏延迟 ~25 µs</li>
<li>BeagleBone Black（ARM32）：经典实时平台，最坏延迟 ~15 µs</li>
<li>i.MX6 系列：工业级，最坏延迟 ~10 µs</li>
<li>x86_64 工控机：带 TSC 定时器，最坏延迟 ~5 µs</li>
</ul>
<p><strong>要避免的硬件：</strong></p>
<ul>
<li>任何带 SMM（System Management Mode）的 x86 平台：SMM 中断对 OS 不可见，会造成几百微秒的不可控延迟</li>
<li>频率动态调节过于激进的 ARM 平台：建议关闭 DVFS，固定 CPU 频率</li>
<li>没有高精度定时器（HPET）的平台</li>
</ul>
<h3 id="32-内核编译流程">3.2 内核编译流程</h3>
<p>我们以 Raspberry Pi 4 为例，演示完整的 Xenomai 3.2.x + Linux 6.1 内核编译流程。</p>
<p><strong>第一步：获取源码</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 创建工作目录</span>
</span></span><span class="line"><span class="cl">mkdir -p ~/xenomai-build <span class="o">&amp;&amp;</span> <span class="nb">cd</span> ~/xenomai-build
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 下载 Linux 内核</span>
</span></span><span class="line"><span class="cl">git clone https://github.com/raspberrypi/linux.git --depth <span class="m">1</span> --branch rpi-6.1.y
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 下载 Xenomai 3</span>
</span></span><span class="line"><span class="cl">git clone https://gitlab.denx.de/Xenomai/xenomai.git --depth <span class="m">1</span> --branch v3.2.x
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 下载 Dovetail 补丁（对应内核版本）</span>
</span></span><span class="line"><span class="cl">wget https://evlproject.org/download/patches/dovetail/dovetail-linux-6.1.y-arm64.patch
</span></span></code></pre></div><p><strong>第二步：打补丁</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">cd</span> linux
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 应用 Dovetail 补丁</span>
</span></span><span class="line"><span class="cl">patch -p1 &lt; ../dovetail-linux-6.1.y-arm64.patch
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 应用 Xenomai Cobalt 补丁</span>
</span></span><span class="line"><span class="cl">../xenomai/scripts/prepare-kernel.sh --arch<span class="o">=</span>arm64
</span></span></code></pre></div><p>这个 <code>prepare-kernel.sh</code> 脚本是 Xenomai 提供的自动化工具，它会自动把 Cobalt 内核的代码合入 Linux 源码树。</p>
<p><strong>第三步：配置内核</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 先加载默认配置</span>
</span></span><span class="line"><span class="cl">make bcm2711_defconfig
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 配置 Xenomai 相关选项</span>
</span></span><span class="line"><span class="cl">make menuconfig
</span></span></code></pre></div><p>在 menuconfig 中需要开启这些选项：</p>
<pre tabindex="0"><code># Dovetail 支持
General setup → Interrupt pipeline [Y]

# Cobalt 内核
Xenomai → Cobalt real-time core [Y]
Xenomai → Cobalt → Real-time core features → Enable priority inheritance [Y]
Xenomai → Cobalt → Real-time core features → Enable shared heaps [Y]

# 关闭不需要的功能，减少延迟
Kernel hacking → KGDB: kernel debugger [N]
Kernel hacking → Tracers [N]
Power management → CPU Frequency scaling [N]  # 或者固定频率
Power management → CPU idle → CPU idle PM support [N]
</code></pre><p><strong>第四步：编译内核</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">make -j<span class="k">$(</span>nproc<span class="k">)</span> Image modules dtbs
</span></span></code></pre></div><h3 id="33-安装-xenomai-用户态库">3.3 安装 Xenomai 用户态库</h3>
<p>内核编译完成后，还需要编译安装 Xenomai 的用户态库文件：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">cd</span> ../xenomai
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 配置编译选项</span>
</span></span><span class="line"><span class="cl">./scripts/bootstrap
</span></span><span class="line"><span class="cl">./configure --prefix<span class="o">=</span>/usr/xenomai --with-core<span class="o">=</span>cobalt --enable-smp
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 编译安装</span>
</span></span><span class="line"><span class="cl">make -j<span class="k">$(</span>nproc<span class="k">)</span>
</span></span><span class="line"><span class="cl">make install
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 添加环境变量</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s1">&#39;export PATH=$PATH:/usr/xenomai/bin&#39;</span> &gt;&gt; /etc/profile
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s1">&#39;export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/xenomai/lib&#39;</span> &gt;&gt; /etc/profile
</span></span></code></pre></div><h3 id="34-验证安装">3.4 验证安装</h3>
<p>重启到新内核后，验证 Xenomai 是否正常工作：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 检查内核版本</span>
</span></span><span class="line"><span class="cl">uname -a
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 检查 Cobalt 内核是否加载</span>
</span></span><span class="line"><span class="cl">dmesg <span class="p">|</span> grep -i cobalt
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 运行延迟测试</span>
</span></span><span class="line"><span class="cl">/usr/xenomai/bin/latency -s <span class="m">1000</span>
</span></span></code></pre></div><p>如果看到类似下面的输出，说明安装成功：</p>
<pre tabindex="0"><code>== Sampling period: 1000 us
== Test mode: periodic user-mode task
== All results in microseconds
warming up...
RTT|  00:00:01  (periodic user-mode task, 1000 us period, priority 99)
RTH|----lat min|----lat avg|----lat max|-overrun|---msw|---lat best|--lat worst
RTD|      2.845|      5.217|     13.562|       0|     0|      2.845|     13.562
</code></pre><p>这里的 <code>lat max</code> 只有 13.562 µs，这就是 Xenomai 的威力。</p>
<h2 id="四xenomai-应用开发从-hello-world-到实时任务">四、Xenomai 应用开发：从 Hello World 到实时任务</h2>
<p>现在我们开始学习 Xenomai 应用开发。Xenomai 提供了两套 API：</p>
<ol>
<li><strong>原生 API（Alchemy API）</strong>：功能最强大，性能最好，但不是 POSIX 标准</li>
<li><strong>POSIX 兼容层 API</strong>：和标准 POSIX pthread 几乎一样，代码可移植性好</li>
</ol>
<p>对于新项目，我推荐直接使用 POSIX 兼容层，因为学习成本低，代码可移植。</p>
<h3 id="41-hello-world第一个实时任务">4.1 Hello World：第一个实时任务</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;stdio.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;stdlib.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;string.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;unistd.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;pthread.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;sched.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;sys/mman.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;xenomai/init.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl"><span class="cp">#define PRIORITY 99
</span></span></span><span class="line"><span class="cl"><span class="cp">#define STACK_SIZE 8192
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="o">*</span><span class="nf">realtime_task</span><span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="n">arg</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">struct</span> <span class="n">sched_param</span> <span class="n">param</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">int</span> <span class="n">ret</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// 设置线程优先级
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="n">param</span><span class="p">.</span><span class="n">sched_priority</span> <span class="o">=</span> <span class="n">PRIORITY</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">ret</span> <span class="o">=</span> <span class="nf">pthread_setschedparam</span><span class="p">(</span><span class="nf">pthread_self</span><span class="p">(),</span> <span class="n">SCHED_FIFO</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">param</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">ret</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nf">fprintf</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span> <span class="s">&#34;pthread_setschedparam failed: %s</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">,</span> <span class="nf">strerror</span><span class="p">(</span><span class="n">ret</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="nb">NULL</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;实时线程开始运行，优先级 = %d</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">,</span> <span class="n">PRIORITY</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// 实时任务主循环
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="k">while</span> <span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// 在这里执行实时操作
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;Hello from Xenomai real-time thread!</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="c1">// 注意：不要在实时循环里用 printf，这只是演示
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="c1">// printf 会触发 Linux 系统调用，造成不确定延迟
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        
</span></span><span class="line"><span class="cl">        <span class="nf">sleep</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nb">NULL</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kt">int</span> <span class="nf">main</span><span class="p">(</span><span class="kt">int</span> <span class="n">argc</span><span class="p">,</span> <span class="kt">char</span> <span class="o">*</span><span class="n">argv</span><span class="p">[])</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kt">pthread_t</span> <span class="kr">thread</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">int</span> <span class="n">ret</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// 1. 锁定所有内存，防止页错误
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="n">ret</span> <span class="o">=</span> <span class="nf">mlockall</span><span class="p">(</span><span class="n">MCL_CURRENT</span> <span class="o">|</span> <span class="n">MCL_FUTURE</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">ret</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nf">perror</span><span class="p">(</span><span class="s">&#34;mlockall failed&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="nf">exit</span><span class="p">(</span><span class="n">EXIT_FAILURE</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// 2. 初始化 Xenomai
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="n">ret</span> <span class="o">=</span> <span class="nf">xenomai_init</span><span class="p">(</span><span class="o">&amp;</span><span class="n">argc</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">argv</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">ret</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nf">fprintf</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span> <span class="s">&#34;xenomai_init failed: %s</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">,</span> <span class="nf">strerror</span><span class="p">(</span><span class="n">ret</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">        <span class="nf">exit</span><span class="p">(</span><span class="n">EXIT_FAILURE</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// 3. 创建实时线程
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="n">ret</span> <span class="o">=</span> <span class="nf">pthread_create</span><span class="p">(</span><span class="o">&amp;</span><span class="kr">thread</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">,</span> <span class="n">realtime_task</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">ret</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nf">fprintf</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span> <span class="s">&#34;pthread_create failed: %s</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">,</span> <span class="nf">strerror</span><span class="p">(</span><span class="n">ret</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">        <span class="nf">exit</span><span class="p">(</span><span class="n">EXIT_FAILURE</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// 4. 等待线程结束
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="nf">pthread_join</span><span class="p">(</span><span class="kr">thread</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>编译命令：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">gcc -o hello_xenomai hello_xenomai.c <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>    <span class="k">$(</span>/usr/xenomai/bin/xeno-config --skin<span class="o">=</span>posix --cflags --ldflags<span class="k">)</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>    -lpthread
</span></span></code></pre></div><p>运行：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">./hello_xenomai
</span></span></code></pre></div><h3 id="42-关键的初始化步骤详解">4.2 关键的初始化步骤详解</h3>
<p>上面的代码里，有几个步骤是<strong>绝对不能省略</strong>的：</p>
<p><strong>1. mlockall(MCL_CURRENT | MCL_FUTURE)</strong></p>
<p>这是最重要的一步。它告诉内核：把当前进程所有的内存都锁定在物理内存中，不要换出到 swap，而且将来分配的内存也要自动锁定。</p>
<p>如果省略这一步，你的实时线程可能在运行中遇到页错误，触发 Linux 缺页异常处理，造成几百微秒甚至几毫秒的延迟。</p>
<p><strong>2. xenomai_init(&amp;argc, &amp;argv)</strong></p>
<p>这一步初始化 Xenomai 运行时环境。它会：</p>
<ul>
<li>打开 /dev/xenomai/cobalt 设备文件</li>
<li>注册线程到 Cobalt 内核</li>
<li>设置信号处理函数</li>
</ul>
<p>如果省略这一步，你的线程依然可以运行，但它运行在 Linux 上下文，而不是 Cobalt 上下文。这是新手最容易犯的错误——写了代码，加了优先级，但忘了调用 xenomai_init，结果根本没跑在实时内核上。</p>
<p><strong>3. pthread_setschedparam + SCHED_FIFO</strong></p>
<p>设置线程调度策略为 SCHED_FIFO（实时调度），优先级 99。Xenomai 的优先级范围是 1-99，数值越大优先级越高。</p>
<p>注意：Xenomai 的优先级是<strong>独立于 Linux 优先级</strong>的。一个优先级为 1 的 Xenomai 实时线程，依然可以抢占任何 Linux 线程（包括 Linux 的 idle 线程）。</p>
<h3 id="43-周期性实时任务工业控制的标准模式">4.3 周期性实时任务：工业控制的标准模式</h3>
<p>在工业控制中，90% 的实时任务都是周期性的——每 1ms 读一次传感器，每 2ms 计算一次 PID，每 5ms 输出一次控制量。</p>
<p>Xenomai 提供了专门的周期性定时器机制：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;stdio.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;stdlib.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;time.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;pthread.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;sys/mman.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;xenomai/init.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl"><span class="cp">#define PERIOD_NS 1000000  </span><span class="c1">// 1ms 周期
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="cp">#define PRIORITY 90
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="o">*</span><span class="nf">periodic_task</span><span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="n">arg</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">struct</span> <span class="n">sched_param</span> <span class="n">param</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">struct</span> <span class="n">timespec</span> <span class="n">next</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">unsigned</span> <span class="kt">long</span> <span class="kt">long</span> <span class="n">count</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="n">param</span><span class="p">.</span><span class="n">sched_priority</span> <span class="o">=</span> <span class="n">PRIORITY</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="nf">pthread_setschedparam</span><span class="p">(</span><span class="nf">pthread_self</span><span class="p">(),</span> <span class="n">SCHED_FIFO</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">param</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// 获取当前时间
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="nf">clock_gettime</span><span class="p">(</span><span class="n">CLOCK_REALTIME</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">next</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;周期性任务启动，周期 = %ld ns</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">,</span> <span class="n">PERIOD_NS</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">while</span> <span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// 计算下一个唤醒时间
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="n">next</span><span class="p">.</span><span class="n">tv_nsec</span> <span class="o">+=</span> <span class="n">PERIOD_NS</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="n">next</span><span class="p">.</span><span class="n">tv_nsec</span> <span class="o">&gt;=</span> <span class="mi">1000000000</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="n">next</span><span class="p">.</span><span class="n">tv_nsec</span> <span class="o">-=</span> <span class="mi">1000000000</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">            <span class="n">next</span><span class="p">.</span><span class="n">tv_sec</span><span class="o">++</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1">// 休眠直到下一个周期
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="nf">clock_nanosleep</span><span class="p">(</span><span class="n">CLOCK_REALTIME</span><span class="p">,</span> <span class="n">TIMER_ABSTIME</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">next</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="n">count</span><span class="o">++</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="c1">// 这里执行实时任务
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="c1">// 读取传感器、计算 PID、输出控制信号
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="n">count</span> <span class="o">%</span> <span class="mi">1000</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="c1">// 每秒打印一次统计信息
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>            <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;运行了 %llu 个周期</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">,</span> <span class="n">count</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nb">NULL</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>使用绝对时间休眠（<code>TIMER_ABSTIME</code>）是非常重要的细节。如果使用相对时间休眠，每次休眠的误差会累积，最终周期会漂移。使用绝对时间可以保证精确的周期性。</p>
<h3 id="44-xenomai-同步原语mutex-和-semaphore">4.4 Xenomai 同步原语：Mutex 和 Semaphore</h3>
<p>和标准 POSIX 一样，Xenomai 提供了 Mutex（互斥锁）和 Semaphore（信号量）用于线程间同步。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="c1">// Mutex 示例
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kt">pthread_mutex_t</span> <span class="n">mutex</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kt">pthread_mutexattr_t</span> <span class="n">mutex_attr</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 初始化 Mutex 属性
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nf">pthread_mutexattr_init</span><span class="p">(</span><span class="o">&amp;</span><span class="n">mutex_attr</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="c1">// 开启优先级继承（重要！防止优先级反转）
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nf">pthread_mutexattr_setprotocol</span><span class="p">(</span><span class="o">&amp;</span><span class="n">mutex_attr</span><span class="p">,</span> <span class="n">PTHREAD_PRIO_INHERIT</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 初始化 Mutex
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nf">pthread_mutex_init</span><span class="p">(</span><span class="o">&amp;</span><span class="n">mutex</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">mutex_attr</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 使用
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nf">pthread_mutex_lock</span><span class="p">(</span><span class="o">&amp;</span><span class="n">mutex</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="c1">// 临界区
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nf">pthread_mutex_unlock</span><span class="p">(</span><span class="o">&amp;</span><span class="n">mutex</span><span class="p">);</span>
</span></span></code></pre></div><p><strong>重要提示</strong>：一定要开启优先级继承（<code>PTHREAD_PRIO_INHERIT</code>）。如果不开启，低优先级线程持有锁时，中间优先级的线程可能抢占低优先级线程，导致高优先级线程等待锁的时间无限延长——这就是经典的<strong>优先级反转问题</strong>。</p>
<h3 id="45-实时任务和-linux-任务通信xddp">4.5 实时任务和 Linux 任务通信：XDDP</h3>
<p>实时任务经常需要和非实时的 Linux 任务交换数据——比如实时线程采集的数据要传给 Linux 线程保存到文件，或者 Linux 线程接收的配置参数要传给实时线程。</p>
<p>Xenomai 提供了 XDDP（Xenomai Data Distribution Protocol）机制：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="c1">// 实时端发送数据
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="cp">#include</span> <span class="cpf">&lt;rtdm/xddp.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl"><span class="kt">int</span> <span class="n">sk</span> <span class="o">=</span> <span class="nf">socket</span><span class="p">(</span><span class="n">AF_RTIPC</span><span class="p">,</span> <span class="n">SOCK_DGRAM</span><span class="p">,</span> <span class="n">IPCPROTO_XDDP</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">struct</span> <span class="n">sockaddr_xddp</span> <span class="n">addr</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="n">sxddp_family</span> <span class="o">=</span> <span class="n">AF_RTIPC</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="n">sxddp_port</span> <span class="o">=</span> <span class="mi">1234</span><span class="p">,</span>  <span class="c1">// 端口号
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="p">.</span><span class="n">sxddp_label</span> <span class="o">=</span> <span class="s">&#34;realtime_sender&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">bind</span><span class="p">(</span><span class="n">sk</span><span class="p">,</span> <span class="p">(</span><span class="k">struct</span> <span class="n">sockaddr</span> <span class="o">*</span><span class="p">)</span><span class="o">&amp;</span><span class="n">addr</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">addr</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 发送数据
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nf">sendto</span><span class="p">(</span><span class="n">sk</span><span class="p">,</span> <span class="n">data</span><span class="p">,</span> <span class="n">data_size</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// Linux 端接收数据
</span></span></span><span class="line"><span class="cl"><span class="c1">// 使用标准的 socket API，连接到同一个端口
</span></span></span><span class="line"><span class="cl"><span class="c1">// 注意：Linux 端使用的是普通 socket，不是 Xenomai socket
</span></span></span></code></pre></div><p>XDDP 的优点是：</p>
<ul>
<li>实时端发送完全是非阻塞的，延迟极低</li>
<li>支持大数据传输（最高 64KB 缓冲区）</li>
<li>可以在中断上下文、线程上下文使用</li>
</ul>
<p>（第二部分完，约2100字）</p>
]]></content:encoded>
    </item>
    <item>
      <title>STM32 CAN 总线通信深度实战指南：从协议原理到 bxCAN 工程落地</title>
      <link>https://tech-snippets.xyz/posts/stm32-can-bus-communication-guide/</link>
      <pubDate>Sat, 23 May 2026 19:00:00 +0800</pubDate>
      <guid>https://tech-snippets.xyz/posts/stm32-can-bus-communication-guide/</guid>
      <description>前言 如果你做过几年嵌入式开发，迟早会撞到 CAN 总线这堵墙。它在汽车电子里几乎是默认标配，在工业控制、轨道交通、电梯、医疗设备、甚至是无人机机臂内部通信里，到处都有它的身影。我第一次和 CAN 打交道是在一个新能源 BMS 项目上——电池包要把每串单体电压、温度、SOC 上报给整车控制器（VCU），现场没有以太网，没有 RS485，PCB 上焊得整整齐齐就两根线：CAN_H 和 CAN_L。当时我天真地以为，无非就是另一个 UART，能收能发就完事。结果第一周就被仲裁机制、位时序、错误帧、Bus-Off 教育得明明白白。
写这篇文章的初衷，是想把这些年踩过的坑、读过的手册、调过的示波器波形，整理成一份能让后来人少走弯路的实战指南。我不会停留在「CAN 是一种串行通信总线」这种百度百科式的介绍，而是从协议本质讲起，一路下沉到 STM32 的 bxCAN 外设寄存器层，再上升到工程代码里那些「为什么我的滤波器配了却收不到包」的具体问题。
CAN 总线（Controller Area Network）由德国博世公司在 1986 年推出，最初的目标是给汽车上日益增多的电子控制单元（ECU）之间提供一条可靠、低成本的通信骨干。在 CAN 出现之前，每两个 ECU 之间都要拉一根专线，一辆中高端车里光线束就能有上百公斤重。CAN 把所有节点挂在同一条双绞线上，靠协议本身去解决冲突和优先级问题，线束重量瞬间砍掉一大半。这个设计哲学在今天看来依然非常先进——它把网络的复杂性下沉到了协议层，让物理层简单到极致。
放到 2026 年的视角，CAN 当然不算「新技术」。CAN FD（Flexible Data-rate）已经成为汽车主流，传输速率突破 5 Mbps，数据载荷扩展到 64 字节；CAN XL 已经在路上，可以飙到 10 Mbps、2048 字节负载；车载以太网（Automotive Ethernet）也在逐步蚕食骨干网的份额。但即便如此，传统 CAN 2.0B 在嵌入式工程里依然不可替代。原因很简单：它便宜、皮实、抗干扰能力恐怖、协议成熟到几乎不会出 bug，而且几乎每一颗工业级 MCU 都自带 CAN 控制器。
一、为什么是 CAN？四个无法替代的设计优势 要理解 CAN 为什么能屹立不倒近四十年，必须先看清它的核心设计。我把它总结成四点：
第一，多主架构（Multi-master）。 总线上没有「主机」和「从机」的区分，任何节点在总线空闲时都可以主动发起传输。这跟 SPI 那种「主机点名才能说话」的模式完全不同。在汽车里，发动机 ECU 和刹车 ECU 是平等的两个节点，谁需要谁就说。这种架构的好处是没有单点故障——主机挂了整条线就废这种事不会发生。
第二，非破坏性逐位仲裁（Non-destructive bitwise arbitration）。 这是 CAN 协议的精髓所在，也是最容易被新手忽略的点。当多个节点同时抢占总线时，CAN 通过比较报文 ID 的每一位来决定谁优先发送，而且——胜出的那一帧不会被损坏。技术上，CAN 总线物理层定义了「显性位」（Dominant，逻辑 0）和「隐性位」（Recessive，逻辑 1）。在线与（wired-AND）逻辑下，只要有一个节点输出 0，整条总线就是 0。每个节点在发送时会同时监听总线状态，一旦发现自己发送的是 1 但总线上是 0，立刻退出仲裁、转为接收方。ID 数值越小（高位 0 越多）的报文，优先级越高。</description>
      <content:encoded><![CDATA[<h2 id="前言">前言</h2>
<p>如果你做过几年嵌入式开发，迟早会撞到 CAN 总线这堵墙。它在汽车电子里几乎是默认标配，在工业控制、轨道交通、电梯、医疗设备、甚至是无人机机臂内部通信里，到处都有它的身影。我第一次和 CAN 打交道是在一个新能源 BMS 项目上——电池包要把每串单体电压、温度、SOC 上报给整车控制器（VCU），现场没有以太网，没有 RS485，PCB 上焊得整整齐齐就两根线：CAN_H 和 CAN_L。当时我天真地以为，无非就是另一个 UART，能收能发就完事。结果第一周就被仲裁机制、位时序、错误帧、Bus-Off 教育得明明白白。</p>
<p>写这篇文章的初衷，是想把这些年踩过的坑、读过的手册、调过的示波器波形，整理成一份能让后来人少走弯路的实战指南。我不会停留在「CAN 是一种串行通信总线」这种百度百科式的介绍，而是从协议本质讲起，一路下沉到 STM32 的 bxCAN 外设寄存器层，再上升到工程代码里那些「为什么我的滤波器配了却收不到包」的具体问题。</p>
<p>CAN 总线（Controller Area Network）由德国博世公司在 1986 年推出，最初的目标是给汽车上日益增多的电子控制单元（ECU）之间提供一条可靠、低成本的通信骨干。在 CAN 出现之前，每两个 ECU 之间都要拉一根专线，一辆中高端车里光线束就能有上百公斤重。CAN 把所有节点挂在同一条双绞线上，靠协议本身去解决冲突和优先级问题，线束重量瞬间砍掉一大半。这个设计哲学在今天看来依然非常先进——它把网络的复杂性下沉到了协议层，让物理层简单到极致。</p>
<p>放到 2026 年的视角，CAN 当然不算「新技术」。CAN FD（Flexible Data-rate）已经成为汽车主流，传输速率突破 5 Mbps，数据载荷扩展到 64 字节；CAN XL 已经在路上，可以飙到 10 Mbps、2048 字节负载；车载以太网（Automotive Ethernet）也在逐步蚕食骨干网的份额。但即便如此，传统 CAN 2.0B 在嵌入式工程里依然不可替代。原因很简单：它便宜、皮实、抗干扰能力恐怖、协议成熟到几乎不会出 bug，而且几乎每一颗工业级 MCU 都自带 CAN 控制器。</p>
<h2 id="一为什么是-can四个无法替代的设计优势">一、为什么是 CAN？四个无法替代的设计优势</h2>
<p>要理解 CAN 为什么能屹立不倒近四十年，必须先看清它的核心设计。我把它总结成四点：</p>
<p><strong>第一，多主架构（Multi-master）。</strong> 总线上没有「主机」和「从机」的区分，任何节点在总线空闲时都可以主动发起传输。这跟 SPI 那种「主机点名才能说话」的模式完全不同。在汽车里，发动机 ECU 和刹车 ECU 是平等的两个节点，谁需要谁就说。这种架构的好处是没有单点故障——主机挂了整条线就废这种事不会发生。</p>
<p><strong>第二，非破坏性逐位仲裁（Non-destructive bitwise arbitration）。</strong> 这是 CAN 协议的精髓所在，也是最容易被新手忽略的点。当多个节点同时抢占总线时，CAN 通过比较报文 ID 的每一位来决定谁优先发送，而且——胜出的那一帧不会被损坏。技术上，CAN 总线物理层定义了「显性位」（Dominant，逻辑 0）和「隐性位」（Recessive，逻辑 1）。在线与（wired-AND）逻辑下，只要有一个节点输出 0，整条总线就是 0。每个节点在发送时会同时监听总线状态，一旦发现自己发送的是 1 但总线上是 0，立刻退出仲裁、转为接收方。ID 数值越小（高位 0 越多）的报文，优先级越高。</p>
<p><strong>第三，差分信号传输。</strong> CAN_H 和 CAN_L 是一对双绞线，传输的是差分电压。隐性状态下两根线电压都是 2.5V，差值为 0；显性状态下 CAN_H 升到 3.5V，CAN_L 降到 1.5V，差值 2V。这种差分方式对共模干扰具有天然的抑制能力，配合双绞线的扭绞，可以在强电磁环境下（比如电机控制柜里）稳定通信。这也是为什么工业现场宁可用 CAN，也不爱用单端的 UART。</p>
<p><strong>第四，强大的错误检测与故障约束机制。</strong> CAN 协议内置了五种错误检测：CRC 校验、ACK 应答、帧格式检查、位监听、位填充。任何一种错误被检测到，整条总线上的节点都会立即丢弃当前帧并进入错误处理。更关键的是 CAN 的「错误计数器」机制——每个节点都有 TEC（发送错误计数）和 REC（接收错误计数）寄存器，超过 127 进入被动错误状态，超过 255 进入 Bus-Off 状态自动脱离总线，避免一个坏节点把整条总线拖死。这个故障约束机制保证了系统级的鲁棒性。</p>
<p><img alt="CAN 总线拓扑结构" loading="lazy" src="/images/posts/2026/05/can-bus-topology.svg"></p>
<h2 id="二can-物理层那些手册上不写的细节">二、CAN 物理层：那些手册上不写的细节</h2>
<p>讲完协议哲学，我们来看物理层。CAN 的物理层标准在 ISO 11898 里定义得很细，但工程实践中有几个点特别容易翻车：</p>
<p><strong>终端电阻必须严格 120Ω，而且只能在总线两端各接一个。</strong> 我见过太多新手要么忘了接，要么在每个节点都接一个。终端电阻的作用是阻抗匹配，吸收信号反射。CAN 是分布参数的传输线，没有终端电阻，信号在线缆两端会发生反射，导致波形畸变、ACK 错误、误码率飙升。如果中间某个节点接了终端电阻，总线阻抗变成 60Ω 甚至 40Ω，差分电压幅度变小、收发器驱动能力不足，同样会出问题。</p>
<p><strong>线缆长度与波特率成反比。</strong> 这是因为 CAN 的仲裁机制要求显性位的电平变化在一个位时间（Bit Time）内传遍整条总线，让所有节点都能采样到一致的电平。光在导线里传播大约 5ns/m，一个 1 Mbps 的位时间是 1μs，留给信号传播的余量大概只够 40 米。所以你会看到典型的对应关系：1 Mbps 对应 40 米、500 kbps 对应 100 米、250 kbps 对应 250 米、125 kbps 对应 500 米。超出这个范围就老老实实降速，别硬怼。</p>
<p><strong>收发器（Transceiver）的选型很重要。</strong> STM32 内部的 CAN 控制器输出的是 TTL 电平的 CAN_TX/CAN_RX 单端信号，必须外接收发器芯片转换成差分总线信号。汽车级常用 TJA1050、TJA1042，工业级用 SN65HVD230、MCP2551。要重点关注几个参数：通信速率（CAN FD 必须用 5Mbps 以上的收发器）、节点数量上限（一般 110 个左右）、待机功耗（电池供电场景）、隔离方式（高压环境用 ADM3053 这种带隔离的型号）。</p>
<h2 id="三can-数据帧协议层的语法书">三、CAN 数据帧：协议层的语法书</h2>
<p>CAN 协议定义了四种帧：数据帧、远程帧、错误帧、过载帧。99% 的应用只用数据帧，剩下三种主要是协议层自动处理。数据帧又分两种格式：CAN 2.0A 的标准帧（11 位 ID）和 CAN 2.0B 的扩展帧（29 位 ID）。我们重点看标准数据帧。</p>
<p><img alt="CAN 2.0A 标准数据帧结构" loading="lazy" src="/images/posts/2026/05/can-frame-structure.svg"></p>
<p>一个完整的标准数据帧从 SOF（帧起始）的一个显性位开始，紧跟着 11 位 ID（仲裁段）、1 位 RTR（远程发送请求，数据帧为 0）、6 位控制段（包含 IDE 标识扩展位、保留位 r0、4 位 DLC 数据长度码）、0~64 位的数据段、15 位 CRC 加 1 位界定符、2 位 ACK 段、7 位 EOF 帧结束，最后是 3 位 IFS 帧间隔。算下来一个空帧 47 位、满载（8 字节）数据帧 111 位（不算位填充）。所以在 1 Mbps 速率下，理论最高每秒约 9000 帧，实际由于位填充和帧间隔会再低一些。</p>
<p>这里面**位填充（Bit Stuffing）**是个容易被忽视但非常重要的机制。CAN 没有独立的时钟线，发送方和接收方靠相邻位的跳变沿来重新同步时钟。如果连续出现 5 个相同的位，发送方会强行插入一个相反的位（接收方再自动去掉），保证总线上始终有足够的跳变。这个机制让 CAN 能在没有时钟线的情况下做到几乎 0 错位采样，但代价是同一份数据在总线上的实际长度是不固定的。在做总线带宽精确计算时要把位填充的开销算进去，一般按帧长的 20% 估算。</p>
<p>（第一部分完，约 3100 字）</p>
<h2 id="四stm32-bxcan-外设硬件结构剖析">四、STM32 bxCAN 外设：硬件结构剖析</h2>
<p>STM32F1/F4/F7/H7 系列的 CAN 控制器叫做 <strong>bxCAN</strong>（Basic Extended CAN），全面支持 CAN 2.0A 和 2.0B 协议，最高速率 1 Mbps。STM32G4/H7 部分型号开始上 <strong>FDCAN</strong>，支持 CAN FD。我们先把 bxCAN 吃透，FDCAN 的差异主要在数据段长度和位时序，理解了 bxCAN 切过去问题不大。</p>
<p>bxCAN 内部最核心的三块东西：<strong>3 个发送邮箱（Tx Mailbox）</strong>、<strong>2 个接收 FIFO（每个 3 级深度）</strong>、<strong>14 个过滤器组</strong>（连接两路 CAN 的型号是 28 个，主从拆分使用）。</p>
<p><img alt="STM32 bxCAN 内部结构" loading="lazy" src="/images/posts/2026/05/stm32-bxcan-architecture.svg"></p>
<p><strong>发送邮箱</strong>是发送方向的核心。当应用层调用 <code>HAL_CAN_AddTxMessage()</code> 时，HAL 库会找一个空闲的 Tx Mailbox 把帧数据填进去，硬件自动负责后续的总线仲裁、CRC 计算、错误重传。三个邮箱之间默认按报文 ID 优先级排队发送（小 ID 先发），也可以通过 <code>CAN_MCR_TXFP</code> 位改成 FIFO 顺序发送。在高优先级周期帧（比如 10ms 的发动机扭矩）和低优先级事件帧（比如故障码上报）混合的场景里，三个邮箱设计得刚刚好——基本不会出现「关键帧排队等待非关键帧」的优先级反转。</p>
<p><strong>接收 FIFO</strong> 是接收方向的核心。bxCAN 有两个独立的 FIFO（FIFO0 和 FIFO1），每个 3 级深度。一帧合法报文进来后，先过过滤器组，根据过滤器配置决定送进 FIFO0 还是 FIFO1。FIFO 满了之后默认会丢弃新到的报文并置 FOVR（FIFO Overrun）标志位，也可以配置成覆盖最旧的报文。在中断模式下，FIFO 非空触发 <code>CAN_RX0/RX1</code> 中断，应用层在中断里调用 <code>HAL_CAN_GetRxMessage()</code> 取出报文。</p>
<p><strong>过滤器组</strong>是最容易让新手翻车的地方。它的作用是硬件层面提前过滤掉不感兴趣的报文，避免大量无关报文塞满 FIFO、把 CPU 拖死在中断里。在汽车 CAN 网络上每秒几千帧是常态，没有硬件过滤靠 CPU 软件判断根本扛不住。</p>
<h2 id="五过滤器组的工作原理一次说清楚">五、过滤器组的工作原理：一次说清楚</h2>
<p>过滤器组的配置有两个维度：<strong>位宽</strong>（16 位或 32 位）和<strong>模式</strong>（掩码模式 Mask Mode 或列表模式 List Mode）。两两组合就是四种工作模式：</p>
<ul>
<li><strong>32 位 / 列表模式</strong>：一组可以精确匹配 2 个完整的 29 位扩展 ID</li>
<li><strong>32 位 / 掩码模式</strong>：一组可以匹配 1 个 ID + 1 个掩码（指定哪些位需要匹配）</li>
<li><strong>16 位 / 列表模式</strong>：一组可以精确匹配 4 个 11 位标准 ID</li>
<li><strong>16 位 / 掩码模式</strong>：一组可以匹配 2 组 ID + 掩码</li>
</ul>
<p><strong>掩码模式</strong>最常用，举个例子：你希望接收所有 ID 在 0x100~0x1FF 范围内的标准帧。配置 ID 为 <code>0x100</code>，掩码为 <code>0x700</code>（二进制 111 0000 0000），意思是「ID 的高 3 位必须等于 100 ，低 8 位可以是任意值」。这样硬件就只让 0x100~0x1FF 的报文进 FIFO。</p>
<p><strong>列表模式</strong>适合接收的报文 ID 数量少且固定的场景，比如只接收 0x123、0x456、0x789 三个 ID，配置进列表里硬件就只放这三个进来。</p>
<p>实际工程里我的经验是：<strong>先用最宽松的掩码（ID=0, Mask=0，即接收所有报文）让通信跑通，再根据节点的报文表逐步收紧过滤器。</strong> 一上来就配精确过滤，调通信问题时根本不知道是没收到包还是被过滤掉了。</p>
<p>这里还有一个<strong>经典坑</strong>：HAL 库里 <code>CAN_FilterTypeDef</code> 结构体的 <code>FilterIdHigh</code> 和 <code>FilterIdLow</code> 字段是<strong>左移 5 位后的值</strong>！标准帧 11 位 ID 写在高 16 位的 [15:5]。比如你要过滤 ID = 0x123 的标准帧，要写 <code>FilterIdHigh = 0x123 &lt;&lt; 5 = 0x2460</code>。这个坑我见过至少 5 个新手踩过，包括我自己第一次用 bxCAN 的时候。STM32CubeMX 也不会替你算，必须自己手动左移。</p>
<h2 id="六can-位时序那个让人头大的-tq">六、CAN 位时序：那个让人头大的 Tq</h2>
<p>CAN 的波特率配置不是简单设个数字就完事，它涉及**位时序（Bit Timing）**的精细调整。一个位时间被划分成几段：</p>
<ul>
<li><strong>SyncSeg（同步段）</strong>：固定 1 个 Tq（时间量子），用于跨节点同步</li>
<li><strong>PropSeg（传播段）</strong>：补偿物理传播延迟，1~8 Tq</li>
<li><strong>PhaseSeg1（相位段 1）</strong>：1~8 Tq</li>
<li><strong>PhaseSeg2（相位段 2）</strong>：1~8 Tq</li>
</ul>
<p>STM32 把 PropSeg 和 PhaseSeg1 合并成一个 <code>BS1</code>（Time Segment 1），PhaseSeg2 叫 <code>BS2</code>。波特率的计算公式：</p>
<pre tabindex="0"><code>波特率 = CAN_Clock / (Prescaler × (1 + BS1 + BS2))
</code></pre><p>举个例子，STM32F4 的 APB1 时钟一般是 42 MHz，给 CAN 用。要配 500 kbps：</p>
<pre tabindex="0"><code>42 MHz / (Prescaler × (1 + BS1 + BS2)) = 500000
=&gt; Prescaler × (1 + BS1 + BS2) = 84
</code></pre><p>一种选法是 <code>Prescaler = 6</code>, <code>BS1 = 11 Tq</code>, <code>BS2 = 2 Tq</code>，刚好等于 84，采样点在 (1+11)/14 ≈ 85.7%，落在推荐的 75%~87.5% 区间。<strong>采样点位置很关键</strong>，太靠前抗干扰差，太靠后留给同步调整的余量不够。汽车行业普遍推荐 75% 或 87.5%（CiA 标准）。</p>
<p>不想手动算？用在线工具 <a href="http://www.bittiming.can-wiki.info/">bittiming.can-wiki.info</a>，输入 MCU 型号、时钟频率、目标波特率，直接出最优配置，比自己拍脑袋强。</p>
<h2 id="七cubemx-初始化配置一步一步来">七、CubeMX 初始化配置：一步一步来</h2>
<p>打开 STM32CubeMX，选好芯片（以 STM32F407 为例）：</p>
<ol>
<li><strong>RCC 配置</strong>：HSE 接外部 8MHz 晶振，PLL 倍频到 168MHz（APB1 = 42MHz）</li>
<li><strong>使能 CAN1</strong>：左侧 Connectivity → CAN1 → Activated 打勾，模式选 Master</li>
<li><strong>引脚分配</strong>：CAN1_TX → PA12（或 PB9），CAN1_RX → PA11（或 PB8）。注意 PA12 和 USB_DP 复用，做 USB 设备时要避开</li>
<li><strong>参数配置</strong>：
<ul>
<li>Prescaler = 6</li>
<li>Time Quanta in Bit Segment 1 = 12 Times</li>
<li>Time Quanta in Bit Segment 2 = 2 Times</li>
<li>ReSynchronization Jump Width = 1 Time</li>
<li>Operating Mode = Normal（调试时可选 Loopback 自环测试）</li>
<li>Time Triggered Communication Mode = Disable</li>
<li>Automatic Bus-Off Management = <strong>Enable</strong>（重要！让 Bus-Off 后能自动恢复）</li>
<li>Automatic Wake-Up Mode = Enable</li>
<li>Automatic Retransmission = Enable</li>
<li>Receive Fifo Locked Mode = Disable</li>
<li>Transmit Fifo Priority = Disable</li>
</ul>
</li>
<li><strong>NVIC 配置</strong>：使能 CAN1 RX0 interrupt（FIFO0 消息挂起中断）</li>
<li>生成代码</li>
</ol>
<p>生成的 <code>MX_CAN1_Init()</code> 函数会包含所有参数。我们要做的事情通常是在 <code>main()</code> 里：</p>
<ul>
<li>配置过滤器组（<code>HAL_CAN_ConfigFilter</code>）</li>
<li>启动 CAN（<code>HAL_CAN_Start</code>）</li>
<li>使能 RX 中断（<code>HAL_CAN_ActivateNotification</code>）</li>
<li>在中断回调里取报文，在主循环里发报文</li>
</ul>
<p>下一部分我们就把这套完整代码撸出来。</p>
<p>（第二部分完，约 2500 字）</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
