<?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>RPMsg on Tech Snippets - 嵌入式技术笔记</title>
    <link>https://tech-snippets.xyz/tags/rpmsg/</link>
    <description>Recent content in RPMsg on Tech Snippets - 嵌入式技术笔记</description>
    <generator>Hugo</generator>
    <language>zh-cn</language>
    <lastBuildDate>Fri, 05 Jun 2026 03:00:00 +0800</lastBuildDate>
    <atom:link href="https://tech-snippets.xyz/tags/rpmsg/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>STM32H7 双核通信实战：用 OpenAMP 与 RPMsg 打通 Cortex-M7 / Cortex-M4</title>
      <link>https://tech-snippets.xyz/posts/stm32h7-openamp-rpmsg-dual-core-guide/</link>
      <pubDate>Fri, 05 Jun 2026 03:00:00 +0800</pubDate>
      <guid>https://tech-snippets.xyz/posts/stm32h7-openamp-rpmsg-dual-core-guide/</guid>
      <description>从内存分区、启动顺序、resource table、RPMsg 协议到 Cache 一致性，完整讲解 STM32H7 双核工程的通信设计与调试方法。</description>
      <content:encoded><![CDATA[<h2 id="引言双核-mcu-的难点不在多一个核而在边界设计">引言：双核 MCU 的难点不在“多一个核”，而在边界设计</h2>
<p>STM32H745、STM32H747、STM32H755、STM32H757 这类双核 MCU 看起来很诱人：一个 Cortex-M7 跑到几百 MHz，带 I-Cache、D-Cache、FPU 和丰富高速外设；另一个 Cortex-M4 更适合处理中断、采样、控制环和低抖动任务。理论上，把 UI、网络、文件系统、机器视觉前处理放到 M7，把电机控制、ADC 采样、CAN 通信、保护逻辑放到 M4，就能同时得到吞吐量和实时性。</p>
<p>但真正做项目时，问题往往不是“两个核能不能同时跑起来”，而是：谁负责启动谁？共享内存放哪里？消息格式怎么演进？M7 打开 D-Cache 后 M4 为什么收不到新数据？M4 卡死后 M7 如何降级？量产后如何定位一条跨核消息到底丢在哪个阶段？这些问题如果没有在架构阶段想清楚，后面会变成非常难排查的随机故障。</p>
<p>本文以 STM32H7 双核系列为背景，讲一套比较稳妥的 OpenAMP / RPMsg 通信方案。OpenAMP 原本常见于 Linux + MCU 的异构多核系统，在 STM32H7 上也可以作为 Cortex-M7 与 Cortex-M4 之间的消息层。它的价值不是让代码看起来“高级”，而是把共享内存、vring、endpoint、resource table、通知中断这些细节收敛成一套可维护的模型。</p>
<p><img alt="STM32H7 OpenAMP RPMsg 架构" loading="lazy" src="/images/stm32h7-openamp-rpmsg-architecture.svg"></p>
<p>这篇文章不会停留在概念层面。我们会从芯片启动模型讲起，逐步进入内存布局、CubeMX 配置、resource table、RPMsg 端点设计、Cache 一致性、协议封装、调试手段和常见故障。文中的代码偏向工程骨架，目的是让你知道每个模块应该放在哪里，以及哪些地方必须根据具体板卡调整。</p>
<h2 id="一先给两个核心分工m7-管复杂m4-管确定">一、先给两个核心分工：M7 管复杂，M4 管确定</h2>
<p>双核 MCU 最容易犯的错误，是把它当成“两个单片机焊在一起”。如果 M7 和 M4 都直接操作同一批外设、都可以改同一段共享变量、都能决定系统状态，那通信层迟早会变成一团乱麻。比较可控的做法是先明确边界：M7 负责复杂业务，M4 负责确定性任务。</p>
<p>例如一个带触摸屏和电机的设备，可以这样拆分：</p>
<ul>
<li>M7：图形界面、参数管理、以太网或 Wi-Fi 网关、日志、文件系统、OTA、上位机协议解析；</li>
<li>M4：PWM 输出、编码器采样、ADC 采样、过流保护、实时状态机、CAN 或 RS485 的周期帧；</li>
<li>双核通信：M7 下发参数和控制命令，M4 上报状态、故障码和采样摘要。</li>
</ul>
<p>这种分工的好处是业务语义清晰。M7 可以处理复杂但不那么确定的任务，偶尔因为文件系统或网络协议阻塞几十毫秒，也不会直接影响 M4 的控制环。M4 则尽量不做字符串解析、大块内存申请和复杂协议栈，只保证实时任务稳定运行。</p>
<p>如果某个外设必须两个核都知道状态，也尽量采用“单拥有者 + 消息镜像”的方式。比如 ADC DMA 缓冲区由 M4 拥有，M7 不直接读 DMA 正在写的原始缓冲，而是让 M4 周期性整理出摘要或复制一份快照，再通过 RPMsg 发给 M7。这样看似多了一次复制，实际换来了边界清楚和调试方便。</p>
<h2 id="二stm32h7-双核启动模型谁先活谁释放谁">二、STM32H7 双核启动模型：谁先活，谁释放谁</h2>
<p>在 STM32H7 双核设备里，M7 通常作为主核先启动，M4 可以处于保持状态，等待 M7 完成系统时钟、内存、外设和共享区初始化后再释放。不同工程可以选择不同启动策略，但对于 OpenAMP 通信来说，最推荐的方式是：M7 负责系统级初始化，然后启动 M4；M4 启动后初始化自己的 OpenAMP 端点并进入消息循环。</p>
<p>典型启动流程如下：</p>
<ol>
<li>复位后 M7 从 Flash 启动；</li>
<li>M7 配置系统时钟、电源域、MPU、Cache 和必要的共享内存区域；</li>
<li>M7 初始化 OpenAMP 框架，准备 vring 和 resource table 所需的共享区；</li>
<li>M7 通过 HAL_RCCEx_EnableBootCore 或相关机制释放 M4；</li>
<li>M4 从自己的向量表地址启动，初始化 HAL、外设、OpenAMP；</li>
<li>双方创建 RPMsg endpoint，交换握手消息；</li>
<li>握手完成后，应用层才允许发送控制命令。</li>
</ol>
<p>实际项目中，不建议上电后马上发送业务消息。两个核的启动时间不完全一致，某些板卡还会因为外部晶振、外设复位、电源时序造成波动。最好定义一个 <code>HELLO / READY / VERSION</code> 握手过程，在状态没有进入 <code>LINK_READY</code> 前，业务层只缓存关键命令或直接返回“从核未就绪”。</p>
<p>一个简化的状态机可以这样设计：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="k">typedef</span> <span class="k">enum</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">IPC_DOWN</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">IPC_BOOTING</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">IPC_ENDPOINT_CREATED</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">IPC_WAIT_REMOTE_READY</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">IPC_READY</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">IPC_FAULT</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="kt">ipc_state_t</span><span class="p">;</span>
</span></span></code></pre></div><p>M7 侧不要只依赖“函数返回成功”判断链路可用，而应该用明确的远端消息确认。OpenAMP 初始化成功，只说明本地数据结构准备好了；真正能不能收发，还要看 M4 是否完成 endpoint 创建、是否能响应版本查询、是否能处理 ring buffer。</p>
<h2 id="三共享内存布局最怕差不多能用">三、共享内存布局：最怕“差不多能用”</h2>
<p>双核通信的底层一定离不开共享内存。OpenAMP / RPMsg 常用的结构包括 resource table、两个 vring、消息 buffer 以及可能的私有共享数据区。这里最重要的原则是：地址固定、两核一致、MPU 属性明确、不要和堆栈抢空间。</p>
<p>一个工程化的链接脚本布局大概会预留如下区域：</p>
<pre tabindex="0"><code class="language-ld" data-lang="ld">/* 示例地址需要按具体 STM32H7 型号和 SRAM 分区调整 */
RAM_D2 (xrw)      : ORIGIN = 0x30000000, LENGTH = 256K
SHM_IPC (xrw)     : ORIGIN = 0x30040000, LENGTH = 64K
</code></pre><p>在 M7 和 M4 的工程里，都必须以同样的地址理解 <code>SHM_IPC</code>。如果 M7 认为 vring 在 <code>0x30040000</code>，M4 却因为链接脚本或宏定义不同把它放到 <code>0x30020000</code>，现象往往不是直接报错，而是偶发 HardFault、消息错乱或初始化超时。</p>
<p>建议把共享内存配置集中放在一个头文件里，并让 M7、M4 两个工程都引用同一份定义：</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">#define IPC_SHM_BASE        0x30040000UL
</span></span></span><span class="line"><span class="cl"><span class="cp">#define IPC_SHM_SIZE        0x00010000UL
</span></span></span><span class="line"><span class="cl"><span class="cp">#define IPC_VRING_SIZE      0x00001000UL
</span></span></span><span class="line"><span class="cl"><span class="cp">#define IPC_BUFFER_COUNT    16U
</span></span></span><span class="line"><span class="cl"><span class="cp">#define IPC_BUFFER_SIZE     512U
</span></span></span></code></pre></div><p>同时要注意 STM32H7 的内存域。D1、D2、D3 SRAM 的访问权限、总线连接和性能不同。M7 访问某些区域带 Cache，M4 访问同一区域时不经过 M7 的 D-Cache。如果没有正确配置 MPU 或手动清理 / 失效 Cache，M7 写入的内容可能还停留在自己的 Cache line 中，M4 读到的仍然是旧值。这是双核通信里最典型、也最容易误判的软件 bug。</p>
<p>（第一部分完，约2100字）</p>
<h2 id="四cubemx-配置要点不要只勾-openamp">四、CubeMX 配置要点：不要只勾 OpenAMP</h2>
<p>很多人第一次用 STM32CubeMX 配 OpenAMP，会以为勾选中间件就结束了。实际上 CubeMX 只是生成一部分框架代码，工程能不能稳定运行，还取决于内存、启动、HSEM、中断和 Cache 配置。</p>
<p>首先要确认工程是双核工程，分别生成 CM7 和 CM4 两个目标。M7 工程一般包含系统时钟和全局初始化，M4 工程只初始化自己需要的外设。外设归属尽量在设计阶段确定，不要两个核都初始化同一个外设实例。比如 TIM1 如果由 M4 做 PWM，就不要在 M7 工程里也生成 TIM1 初始化代码。</p>
<p>其次是 HSEM。STM32H7 提供硬件信号量，可以用于核间同步和事件通知。OpenAMP 端口通常会使用 HSEM 或 IPCC 类似机制来触发对方处理消息。即便应用层不直接调用 HSEM，也要理解它的存在：如果中断优先级被错误配置、HSEM 时钟未打开、或者中断处理函数没有正确调用 OpenAMP poll，RPMsg 可能会表现为“发送成功但对方不处理”。</p>
<p>第三是内存区域。对于共享内存，最好通过 MPU 把它设置成 non-cacheable，或者在每次访问前后严格执行 cache clean / invalidate。为了减少出错概率，很多控制类项目会选择把 IPC 共享区配置为非缓存区域。性能上会损失一点，但消息包通常不大，换来的确定性更值钱。</p>
<p>M7 侧 MPU 示例思路如下，具体 API 名称和参数以 HAL 版本为准：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="k">static</span> <span class="kt">void</span> <span class="nf">MPU_Config_IPC</span><span class="p">(</span><span class="kt">void</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="n">MPU_Region_InitTypeDef</span> <span class="n">MPU_InitStruct</span> <span class="o">=</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="nf">HAL_MPU_Disable</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">MPU_InitStruct</span><span class="p">.</span><span class="n">Enable</span> <span class="o">=</span> <span class="n">MPU_REGION_ENABLE</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">MPU_InitStruct</span><span class="p">.</span><span class="n">Number</span> <span class="o">=</span> <span class="n">MPU_REGION_NUMBER2</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">MPU_InitStruct</span><span class="p">.</span><span class="n">BaseAddress</span> <span class="o">=</span> <span class="n">IPC_SHM_BASE</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">MPU_InitStruct</span><span class="p">.</span><span class="n">Size</span> <span class="o">=</span> <span class="n">MPU_REGION_SIZE_64KB</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">MPU_InitStruct</span><span class="p">.</span><span class="n">SubRegionDisable</span> <span class="o">=</span> <span class="mh">0x00</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">MPU_InitStruct</span><span class="p">.</span><span class="n">TypeExtField</span> <span class="o">=</span> <span class="n">MPU_TEX_LEVEL1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">MPU_InitStruct</span><span class="p">.</span><span class="n">AccessPermission</span> <span class="o">=</span> <span class="n">MPU_REGION_FULL_ACCESS</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">MPU_InitStruct</span><span class="p">.</span><span class="n">DisableExec</span> <span class="o">=</span> <span class="n">MPU_INSTRUCTION_ACCESS_DISABLE</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">MPU_InitStruct</span><span class="p">.</span><span class="n">IsShareable</span> <span class="o">=</span> <span class="n">MPU_ACCESS_SHAREABLE</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">MPU_InitStruct</span><span class="p">.</span><span class="n">IsCacheable</span> <span class="o">=</span> <span class="n">MPU_ACCESS_NOT_CACHEABLE</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">MPU_InitStruct</span><span class="p">.</span><span class="n">IsBufferable</span> <span class="o">=</span> <span class="n">MPU_ACCESS_NOT_BUFFERABLE</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nf">HAL_MPU_ConfigRegion</span><span class="p">(</span><span class="o">&amp;</span><span class="n">MPU_InitStruct</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="nf">HAL_MPU_Enable</span><span class="p">(</span><span class="n">MPU_PRIVILEGED_DEFAULT</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>如果你选择保留 Cache，则必须把 RPMsg buffer 对齐到 Cache line，并在发送前 <code>SCB_CleanDCache_by_Addr</code>，接收前后 <code>SCB_InvalidateDCache_by_Addr</code>。这条路对性能更友好，但对团队纪律要求更高。只要某个新同事在共享区里新增一个结构体却忘了对齐，就可能引入难以复现的问题。</p>
<h2 id="五resource-table让两边说清楚共享资源在哪里">五、resource table：让两边说清楚“共享资源在哪里”</h2>
<p>OpenAMP 里的 resource table 可以理解为远端处理器描述自己需要哪些共享资源的表。它通常包含 vring 地址、大小、对齐方式、设备特征等信息。在 STM32H7 这种双 MCU 核场景里，resource table 不一定像 Linux remoteproc 那样完整复杂，但它仍然是双方理解 RPMsg 设备的重要依据。</p>
<p>你可以把 resource table 看成一份低层契约：</p>
<ul>
<li>vring0 和 vring1 的地址在哪里；</li>
<li>每个 ring 有多少 descriptor；</li>
<li>buffer 区域从哪里开始；</li>
<li>对齐边界是多少；</li>
<li>通知机制和特征位如何设置。</li>
</ul>
<p>不要在多个文件里散落这些地址。建议把地址定义、resource table 和链接脚本三者一起评审。尤其在项目后期，开发者很容易为了“临时加一个 DMA 缓冲区”挪动 SRAM，结果忘了同步 OpenAMP 地址，导致双核通信莫名其妙失效。</p>
<p>可以用编译期断言减少这类错误：</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">#define STATIC_ASSERT(COND, MSG) typedef char static_assertion_##MSG[(COND)?1:-1]
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl"><span class="nf">STATIC_ASSERT</span><span class="p">((</span><span class="n">IPC_SHM_BASE</span> <span class="o">%</span> <span class="mh">0x1000</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">,</span> <span class="n">ipc_base_must_4k_aligned</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="nf">STATIC_ASSERT</span><span class="p">(</span><span class="n">IPC_SHM_SIZE</span> <span class="o">&gt;=</span> <span class="mh">0x10000</span><span class="p">,</span> <span class="n">ipc_shm_too_small</span><span class="p">);</span>
</span></span></code></pre></div><p>另外，resource table 和共享 buffer 最好不要放在会被 C 运行库初始化清零的普通 <code>.bss</code> 区里。两核启动顺序不同，一方刚初始化好的 ring，另一方如果随后执行清零动作，就会把 ring 状态破坏掉。工程中应明确哪些段由谁初始化，哪些段是 noinit，哪些段在复位后必须重建。</p>
<h2 id="六rpmsg-endpoint不要把消息通道设计成全局变量垃圾桶">六、RPMsg endpoint：不要把消息通道设计成全局变量垃圾桶</h2>
<p>RPMsg 的 endpoint 提供了类似“命名端口”的抽象。一个 endpoint 可以有名称、地址和回调函数。很多示例工程只创建一个 endpoint，然后所有消息都塞进去，通过第一个字节区分类型。这种做法在 demo 里没问题，但项目复杂后会变成维护灾难。</p>
<p>更推荐的方式是按业务域拆分 endpoint 或至少拆分协议模块。例如：</p>
<ul>
<li><code>rpmsg-ctrl</code>：启动握手、版本查询、心跳、复位请求；</li>
<li><code>rpmsg-param</code>：参数读写、参数校验、参数持久化通知；</li>
<li><code>rpmsg-telemetry</code>：状态上报、采样摘要、故障码；</li>
<li><code>rpmsg-debug</code>：诊断命令、日志抓取、性能统计。</li>
</ul>
<p>如果底层端口限制或资源较少，也可以只建一个 endpoint，但应用层协议必须模块化，不能让每个业务文件都直接调用 <code>OPENAMP_send</code>。建议封装一个 <code>ipc_send()</code>，统一处理超时、序列号、统计和错误码。</p>
<p>一个简单消息头可以这样定义：</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">#define IPC_MAGIC 0x48495043u  </span><span class="cm">/* &#34;HIPC&#34; */</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#define IPC_VERSION 1u
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl"><span class="k">typedef</span> <span class="k">struct</span> <span class="nf">__attribute__</span><span class="p">((</span><span class="n">packed</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint32_t</span> <span class="n">magic</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint16_t</span> <span class="n">version</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint16_t</span> <span class="n">type</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint32_t</span> <span class="n">seq</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint32_t</span> <span class="n">timestamp_ms</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint16_t</span> <span class="n">payload_len</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint16_t</span> <span class="n">flags</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="kt">ipc_msg_header_t</span><span class="p">;</span>
</span></span></code></pre></div><p>每条消息带上 <code>magic</code> 和 <code>version</code>，可以避免 M7、M4 固件版本不一致时互相误解。<code>seq</code> 用于请求响应匹配和丢包统计，<code>timestamp_ms</code> 用于分析延迟。<code>flags</code> 可以标记是否需要 ACK、是否为分片、是否为错误响应。</p>
<p>发送函数不要只返回 HAL 状态，而要返回业务可理解的错误：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="k">typedef</span> <span class="k">enum</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">IPC_OK</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">IPC_ERR_NOT_READY</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">IPC_ERR_TIMEOUT</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">IPC_ERR_TOO_LARGE</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">IPC_ERR_BUSY</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">IPC_ERR_BAD_ARG</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="kt">ipc_result_t</span><span class="p">;</span>
</span></span></code></pre></div><p>这样上层代码才知道是链路没准备好、消息过大、ring 满了，还是等待 ACK 超时。对于需要安全保护的设备，M7 下发关键控制命令后必须等待 M4 确认，不能假设“发送成功”等于“执行成功”。</p>
<p>（第二部分完，约2300字）</p>
<h2 id="七m7-侧工程骨架主控路由和超时">七、M7 侧工程骨架：主控、路由和超时</h2>
<p>M7 侧一般承担主控角色。它需要负责启动 M4、初始化 OpenAMP、创建 endpoint、发送握手，并维护链路状态。下面是一个简化骨架，省略了具体 HAL 初始化细节：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="k">static</span> <span class="k">volatile</span> <span class="kt">ipc_state_t</span> <span class="n">g_ipc_state</span> <span class="o">=</span> <span class="n">IPC_DOWN</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">static</span> <span class="k">struct</span> <span class="n">rpmsg_endpoint</span> <span class="n">g_ctrl_ept</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">static</span> <span class="kt">uint32_t</span> <span class="n">g_seq</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">static</span> <span class="kt">int</span> <span class="nf">ctrl_rx_cb</span><span class="p">(</span><span class="k">struct</span> <span class="n">rpmsg_endpoint</span> <span class="o">*</span><span class="n">ept</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                      <span class="kt">void</span> <span class="o">*</span><span class="n">data</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">len</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                      <span class="kt">uint32_t</span> <span class="n">src</span><span class="p">,</span> <span class="kt">void</span> <span class="o">*</span><span class="n">priv</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">if</span> <span class="p">(</span><span class="n">len</span> <span class="o">&lt;</span> <span class="k">sizeof</span><span class="p">(</span><span class="kt">ipc_msg_header_t</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">RPMSG_SUCCESS</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">const</span> <span class="kt">ipc_msg_header_t</span> <span class="o">*</span><span class="n">hdr</span> <span class="o">=</span> <span class="p">(</span><span class="k">const</span> <span class="kt">ipc_msg_header_t</span> <span class="o">*</span><span class="p">)</span><span class="n">data</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">hdr</span><span class="o">-&gt;</span><span class="n">magic</span> <span class="o">!=</span> <span class="n">IPC_MAGIC</span> <span class="o">||</span> <span class="n">hdr</span><span class="o">-&gt;</span><span class="n">version</span> <span class="o">!=</span> <span class="n">IPC_VERSION</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">RPMSG_SUCCESS</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">switch</span> <span class="p">(</span><span class="n">hdr</span><span class="o">-&gt;</span><span class="n">type</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">case</span> <span class="nl">IPC_MSG_M4_READY</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">g_ipc_state</span> <span class="o">=</span> <span class="n">IPC_READY</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="k">break</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">case</span> <span class="nl">IPC_MSG_TELEMETRY</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="nf">telemetry_handle</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="n">len</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="k">break</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">case</span> <span class="nl">IPC_MSG_FAULT</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="nf">fault_handle_from_m4</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="n">len</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="k">break</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">default</span><span class="o">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">break</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="n">RPMSG_SUCCESS</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">void</span> <span class="nf">ipc_m7_task</span><span class="p">(</span><span class="kt">void</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="nf">ipc_lowlevel_init</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="nf">ipc_boot_m4</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="n">g_ipc_state</span> <span class="o">=</span> <span class="n">IPC_BOOTING</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="nf">OPENAMP_create_endpoint</span><span class="p">(</span><span class="o">&amp;</span><span class="n">g_ctrl_ept</span><span class="p">,</span> <span class="s">&#34;rpmsg-ctrl&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">           <span class="n">RPMSG_ADDR_ANY</span><span class="p">,</span> <span class="n">ctrl_rx_cb</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">)</span> <span class="o">!=</span> <span class="n">RPMSG_SUCCESS</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nf">OPENAMP_check_for_message</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="n">g_ipc_state</span> <span class="o">=</span> <span class="n">IPC_WAIT_REMOTE_READY</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="nf">OPENAMP_check_for_message</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">        <span class="nf">ipc_periodic_heartbeat</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">        <span class="nf">ipc_check_timeouts</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">        <span class="nf">osDelay</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 class="p">}</span>
</span></span></code></pre></div><p>实际项目中，<code>OPENAMP_check_for_message()</code> 可以放在专门任务里，也可以由中断通知后释放信号量再处理。重点是不要在高优先级中断里做复杂协议解析。中断里只做最小通知，解析放到任务上下文，避免影响实时性。</p>
<p>M7 下发参数时，建议采用“影子参数 + 提交”的两阶段方式。比如先把一组 PID 参数写入 M4 的待生效区，M4 校验范围、单位和互斥关系后返回 <code>PARAM_ACCEPTED</code>，最后 M7 再发送 <code>PARAM_COMMIT</code>。这样可以避免一半参数已生效、一半参数未生效导致控制状态不一致。</p>
<h2 id="八m4-侧工程骨架实时任务优先通信任务次之">八、M4 侧工程骨架：实时任务优先，通信任务次之</h2>
<p>M4 的第一原则是不要让通信破坏实时任务。OpenAMP 回调里不要做耗时计算，不要直接改控制环正在使用的结构体，更不要在回调里写 Flash。比较稳妥的做法是把收到的消息放入一个小队列，由低优先级任务处理；实时控制任务只在安全点读取已经验证过的参数快照。</p>
<p>M4 侧可以这样组织：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="k">static</span> <span class="k">struct</span> <span class="n">rpmsg_endpoint</span> <span class="n">g_ctrl_ept</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">static</span> <span class="k">volatile</span> <span class="kt">bool</span> <span class="n">g_link_ready</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">static</span> <span class="kt">int</span> <span class="nf">m4_ctrl_rx_cb</span><span class="p">(</span><span class="k">struct</span> <span class="n">rpmsg_endpoint</span> <span class="o">*</span><span class="n">ept</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                         <span class="kt">void</span> <span class="o">*</span><span class="n">data</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">len</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                         <span class="kt">uint32_t</span> <span class="n">src</span><span class="p">,</span> <span class="kt">void</span> <span class="o">*</span><span class="n">priv</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="nf">ipc_queue_push_from_callback</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="n">len</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">RPMSG_SUCCESS</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">void</span> <span class="nf">ipc_m4_task</span><span class="p">(</span><span class="kt">void</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="nf">MX_OPENAMP_Init</span><span class="p">(</span><span class="n">RPMSG_REMOTE</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="nf">OPENAMP_create_endpoint</span><span class="p">(</span><span class="o">&amp;</span><span class="n">g_ctrl_ept</span><span class="p">,</span> <span class="s">&#34;rpmsg-ctrl&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                            <span class="n">RPMSG_ADDR_ANY</span><span class="p">,</span> <span class="n">m4_ctrl_rx_cb</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="nf">ipc_send_ready</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="n">g_link_ready</span> <span class="o">=</span> <span class="nb">true</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="nf">OPENAMP_check_for_message</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">        <span class="nf">ipc_process_queued_messages</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">        <span class="nf">ipc_send_telemetry_if_due</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">        <span class="nf">osDelay</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 class="p">}</span>
</span></span></code></pre></div><p>控制环任务则完全不依赖 RPMsg 的即时响应：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="kt">void</span> <span class="nf">motor_control_task</span><span class="p">(</span><span class="kt">void</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">control_param_t</span> <span class="n">local_param</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="k">if</span> <span class="p">(</span><span class="nf">param_has_pending_commit</span><span class="p">())</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nf">taskENTER_CRITICAL</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">            <span class="n">local_param</span> <span class="o">=</span> <span class="n">g_committed_param</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">            <span class="nf">taskEXIT_CRITICAL</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">adc_sample_update</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">        <span class="nf">foc_control_step</span><span class="p">(</span><span class="o">&amp;</span><span class="n">local_param</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="nf">pwm_update</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">        <span class="nf">wait_next_period</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></code></pre></div><p>这种结构的关键是“通信带来新信息，但不直接打断实时节奏”。如果 M7 突然连续发很多参数或诊断命令，M4 可以丢弃低优先级消息、延迟处理日志请求，但不能牺牲控制周期。</p>
<h2 id="九cache-一致性十个双核疑难杂症七个和它有关">九、Cache 一致性：十个双核疑难杂症，七个和它有关</h2>
<p>STM32H7 的 M7 带 D-Cache，这是性能来源，也是双核通信的坑。常见现象包括：M7 明明写了消息，M4 读到旧内容；M4 写了状态，M7 偶尔看不到更新；调试模式单步正常，全速运行就失败；关闭 Cache 后问题消失但系统变慢。</p>
<p>根因很简单：M7 的 Cache 和 M4 看到的 SRAM 不是同一个“时间点”。如果共享内存是 cacheable，M7 对内存的写入可能先进入 Cache，尚未写回 SRAM。M4 没有经过 M7 的 Cache，自然读不到新值。反过来，M4 修改了 SRAM，M7 如果 Cache line 里还有旧值，也可能继续读旧数据。</p>
<p>有三种处理策略：</p>
<ol>
<li>把 OpenAMP 共享区设为 non-cacheable；</li>
<li>保持 cacheable，但所有共享 buffer 严格做 clean / invalidate；</li>
<li>消息区 non-cacheable，大块数据区 cacheable 并手动维护。</li>
</ol>
<p>对于多数中小型项目，第一种最省心。RPMsg 消息通常只有几十到几百字节，不值得为了这点性能冒一致性风险。对于摄像头图像、音频块、波形缓存这类大数据，可以单独设计共享数据区，使用显式所有权转移：写方填充完成后 clean，发送消息通知；读方收到消息后 invalidate，再读取数据；读完后通过消息归还 buffer。</p>
<p>还要注意对齐。Cache 维护函数通常要求地址和长度按 Cache line 对齐，否则可能影响相邻数据。建议共享结构体使用 32 字节对齐：</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">#define CACHE_LINE_SIZE 32U
</span></span></span><span class="line"><span class="cl"><span class="cp">#define ALIGN_CACHE __attribute__((aligned(CACHE_LINE_SIZE)))
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl"><span class="k">typedef</span> <span class="k">struct</span> <span class="n">ALIGN_CACHE</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint8_t</span> <span class="n">data</span><span class="p">[</span><span class="mi">512</span><span class="p">];</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="kt">ipc_aligned_buffer_t</span><span class="p">;</span>
</span></span></code></pre></div><p>如果团队里有人说“加个 delay 就好了”，基本可以优先怀疑 Cache 或同步问题。delay 只是把竞态窗口遮住，并没有解决根因。</p>
<h2 id="十协议设计把能收发升级成能维护">十、协议设计：把“能收发”升级成“能维护”</h2>
<p>项目早期，双方能互相打印一句 hello 就很有成就感。但量产项目真正需要的是可维护协议。至少要考虑以下字段：消息类型、版本、长度、序列号、时间戳、错误码和可选校验。</p>
<p>一个参数写入流程可以这样定义：</p>
<ol>
<li>M7 发送 <code>PARAM_SET_REQ</code>，包含参数 ID、长度和值；</li>
<li>M4 检查参数 ID 是否存在、长度是否匹配、值域是否合法；</li>
<li>M4 返回 <code>PARAM_SET_RSP</code>，携带 <code>OK</code> 或具体错误码；</li>
<li>M7 收到所有参数设置成功后发送 <code>PARAM_COMMIT_REQ</code>；</li>
<li>M4 在控制环安全点切换参数并返回 <code>PARAM_COMMIT_RSP</code>。</li>
</ol>
<p>错误码不要只用一个 <code>FAIL</code>。建议至少区分：未知命令、版本不兼容、参数不存在、长度错误、值越界、当前状态不允许、内部忙、执行超时。这样现场日志才有分析价值。</p>
<p>心跳也很重要。M7 可以每 100 ms 或 500 ms 发送 heartbeat，M4 回应自己的运行计数、控制周期最大耗时、故障状态和消息队列深度。M7 如果连续多次收不到心跳，应进入降级策略：禁止新的危险命令、提示用户检查从核、必要时尝试重启 M4。</p>
<p>一个心跳 payload 示例：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="k">typedef</span> <span class="k">struct</span> <span class="nf">__attribute__</span><span class="p">((</span><span class="n">packed</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint32_t</span> <span class="n">uptime_ms</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint32_t</span> <span class="n">control_loop_count</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint16_t</span> <span class="n">max_loop_us</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint16_t</span> <span class="n">queue_depth</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint32_t</span> <span class="n">fault_bitmap</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="kt">ipc_heartbeat_payload_t</span><span class="p">;</span>
</span></span></code></pre></div><p>这些字段在开发阶段也很有用。比如 max_loop_us 偶尔跳高，说明 M4 实时任务被打断或某个临界区太长；queue_depth 持续升高，说明 M7 发得太快或 M4 处理不过来；fault_bitmap 可以让 M7 UI 直接显示底层保护状态。</p>
<h2 id="十一调试方法不要只盯串口打印">十一、调试方法：不要只盯串口打印</h2>
<p>双核系统最怕“两个核都在打印，但你不知道先后顺序”。串口打印本身可能阻塞，还会改变时序。建议从一开始就做轻量级事件追踪。每个核维护一个环形 trace buffer，记录事件 ID、时间戳和参数，必要时由 M7 拉取并导出。</p>
<p>事件可以包括：</p>
<ul>
<li>M7 释放 M4；</li>
<li>endpoint 创建成功；</li>
<li>收到远端 READY；</li>
<li>发送消息失败；</li>
<li>ring buffer 满；</li>
<li>心跳超时；</li>
<li>参数提交成功；</li>
<li>M4 控制周期超限。</li>
</ul>
<p>示例结构：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="k">typedef</span> <span class="k">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint32_t</span> <span class="n">tick</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint16_t</span> <span class="n">event</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint16_t</span> <span class="n">arg0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint32_t</span> <span class="n">arg1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="kt">trace_item_t</span><span class="p">;</span>
</span></span></code></pre></div><p>调试时还可以准备一个“回环命令”：M7 发送指定长度的数据，M4 原样返回，M7 统计往返延迟和失败次数。这个命令简单但非常有效，能快速判断链路、Cache、ring 深度和任务调度是否健康。</p>
<p>如果通信偶发卡死，排查顺序建议如下：</p>
<ol>
<li>两个核是否都还在运行，心跳计数是否增长；</li>
<li>HSEM 或通知中断是否触发；</li>
<li>vring descriptor 是否被消耗但未释放；</li>
<li>共享内存地址是否被其他模块覆盖；</li>
<li>Cache 属性是否与预期一致；</li>
<li>是否在回调中执行了阻塞操作；</li>
<li>是否存在两个核同时写同一个结构体的情况。</li>
</ol>
<p>用 J-Link 或 ST-LINK 同时调两个核时，要留意断点会改变时序。M7 停住时 M4 可能继续跑，导致心跳超时；M4 停住时 M7 可能不断重试，ring 被填满。因此调试模式下可以放宽超时时间，或者加入 <code>debug_freeze</code> 开关。</p>
<h2 id="十二常见坑与解决方案">十二、常见坑与解决方案</h2>
<p><strong>1. M7 能启动，M4 没反应。</strong> 先检查 M4 的 boot 地址、选项字节、工程向量表和 M7 释放 M4 的代码。确认 M4 工程确实被烧录到正确地址。很多问题不是 OpenAMP，而是 M4 根本没有启动。</p>
<p><strong>2. endpoint 创建成功，但收不到消息。</strong> 检查 HSEM / 中断回调是否接入 OpenAMP，<code>OPENAMP_check_for_message()</code> 是否被周期调用，远端 endpoint 名称是否一致。还要确认两个核使用的 OpenAMP 配置和 resource table 地址一致。</p>
<p><strong>3. 小消息正常，大消息失败。</strong> 检查 RPMsg buffer 大小、payload 最大长度和消息头开销。不要假设 512 字节 buffer 可以发送 512 字节业务数据，因为还要扣除 RPMsg 和自定义头部。</p>
<p><strong>4. Debug 正常，Release 异常。</strong> 优先怀疑优化导致的竞态、volatile 缺失、内存屏障和 Cache 一致性。共享状态标志必须使用 <code>volatile</code> 或 RTOS 同步原语，跨核共享数据需要明确所有权。</p>
<p><strong>5. 跑一段时间后 ring 满。</strong> 可能是接收方没有及时 poll，或者某条错误路径没有释放 buffer。给发送失败、接收回调、队列满都加计数器，不要只打印一次错误。</p>
<p><strong>6. M4 实时任务抖动增加。</strong> 检查 OpenAMP 处理任务优先级是否过高，回调是否做了耗时工作，M7 是否发送了过于频繁的诊断请求。通信任务应该服务实时任务，而不是压制实时任务。</p>
<h2 id="十三一个推荐的工程目录结构">十三、一个推荐的工程目录结构</h2>
<p>为了让双核工程长期可维护，目录结构也要有边界。可以参考下面这种组织：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">project/
</span></span><span class="line"><span class="cl">  CM7/
</span></span><span class="line"><span class="cl">    Core/
</span></span><span class="line"><span class="cl">    App/
</span></span><span class="line"><span class="cl">      ipc_master.c
</span></span><span class="line"><span class="cl">      ipc_protocol.c
</span></span><span class="line"><span class="cl">      telemetry_view.c
</span></span><span class="line"><span class="cl">  CM4/
</span></span><span class="line"><span class="cl">    Core/
</span></span><span class="line"><span class="cl">    App/
</span></span><span class="line"><span class="cl">      ipc_remote.c
</span></span><span class="line"><span class="cl">      motor_control.c
</span></span><span class="line"><span class="cl">      fault_manager.c
</span></span><span class="line"><span class="cl">  Shared/
</span></span><span class="line"><span class="cl">    ipc_config.h
</span></span><span class="line"><span class="cl">    ipc_protocol.h
</span></span><span class="line"><span class="cl">    ipc_trace.h
</span></span><span class="line"><span class="cl">    memory_map.h
</span></span></code></pre></div><p><code>Shared</code> 目录只放纯头文件或两核都能编译的轻量逻辑，不要放依赖某个核外设的代码。<code>ipc_protocol.h</code> 中定义消息类型、结构体和错误码，确保两边协议一致。每次修改协议，都应更新版本号，并保留必要的兼容处理。</p>
<p>如果项目使用 CI，可以增加一个小脚本检查 M7、M4 是否引用同一份协议头，甚至把协议结构体大小打印出来做静态验证。嵌入式项目不一定要复杂的自动化，但对双核协议这种关键边界，自动检查非常值得。</p>
<h2 id="十四什么时候不该用-openamp">十四、什么时候不该用 OpenAMP？</h2>
<p>OpenAMP 不是银弹。如果你的双核通信只是几个状态位，频率很低，且团队对 OpenAMP 不熟，直接使用 HSEM + 共享结构体也可以。但前提是结构体有明确所有权、版本和同步机制。反过来，如果项目需要多种消息类型、请求响应、远端状态管理、后续可能迁移到 Linux + M4 或更复杂 SoC，那么 OpenAMP / RPMsg 的抽象就更有价值。</p>
<p>还有一种情况要谨慎：超高频小包通信。RPMsg 有通用性，也有开销。如果你试图每个控制周期都通过 RPMsg 发送几十个小包，可能会把系统拖进无意义的调度和拷贝中。对于高频数据，应该使用共享环形缓冲区或双缓冲，RPMsg 只发送“新数据块已准备好”的通知。</p>
<h2 id="总结双核系统的稳定性来自清晰契约">总结：双核系统的稳定性来自清晰契约</h2>
<p>STM32H7 双核开发的核心，不是把 M7 和 M4 都点亮，也不是让 OpenAMP 示例跑起来，而是建立一套清晰、可验证、可调试的跨核契约。这个契约至少包括：启动顺序、共享内存地址、Cache 属性、endpoint 命名、消息头格式、错误码、超时策略、参数提交流程和故障降级方案。</p>
<p>工程上可以记住几个原则：第一，M7 管复杂业务，M4 管确定性任务；第二，共享内存宁可保守，也不要含糊；第三，Cache 一致性要么通过 MPU 规避，要么通过严格封装维护；第四，RPMsg 上层必须有协议，不要把它当成随手发送字节数组的管道；第五，调试信息要结构化，不能只依赖串口打印。</p>
<p>如果按这些原则设计，STM32H7 双核架构会非常好用。M7 可以放心承载界面、网络、文件和复杂算法，M4 则保持控制环和采样任务的稳定节奏。两者通过 OpenAMP / RPMsg 交换经过版本化和校验的消息，既能提升系统能力，也不会把实时性和可维护性牺牲掉。</p>
<p>（全文完，约6900字）</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
