<?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/categories/%E8%8A%AF%E7%89%87%E4%B8%8E%E6%9E%B6%E6%9E%84/</link><description>Recent content in 芯片与架构 on Tech Snippets - 嵌入式技术笔记</description><generator>Hugo</generator><language>zh-cn</language><lastBuildDate>Wed, 07 Oct 2026 13:21:27 +0800</lastBuildDate><atom:link href="https://tech-snippets.xyz/categories/%E8%8A%AF%E7%89%87%E4%B8%8E%E6%9E%B6%E6%9E%84/index.xml" rel="self" type="application/rss+xml"/><item><title>MAX96745 技术详解：eDP/DP 转双通道 GMSL2 车规显示串行器（寄存器参考）</title><link>https://tech-snippets.xyz/posts/max96745-gmsl2-serializer-register-reference-2026/</link><pubDate>Wed, 07 Oct 2026 13:21:27 +0800</pubDate><guid>https://tech-snippets.xyz/posts/max96745-gmsl2-serializer-register-reference-2026/</guid><description>基于 Rockchip 内核与 U-Boot 开源驱动逆向整理的 MAX96745 技术参考：eDP/DP 转双通道 GMSL2 车规显示串行器的寄存器定义、初始化流程、GPIO 隧道、设备树配置与同系列对比。</description><content:encoded><![CDATA[<h2 id="写在前面">写在前面</h2>
<p>MAX96745 是 ADI（原 Maxim）的 <strong>eDP/DisplayPort 转 GMSL2 车规显示串行器</strong>，官方 datasheet 不公开（需 NDA）。本文档基于 Rockchip Linux 内核与 U-Boot 开源驱动（GPL-2.0）逆向整理，寄存器信息为芯片接口技术事实，<strong>非 ADI 官方文档</strong>；如需商用授权或官方规格，请联系 Analog Devices 申请。</p>
<blockquote>
<p><strong>来源声明</strong>：本文基于以下开源驱动整理，遵循 GPL-2.0 许可，转载需保留本声明与来源链接：</p>
<ul>
<li>U-Boot：<code>neggles/u-boot-quartz64</code> → <code>drivers/i2c/muxes/max96745.c</code></li>
<li>Linux 内核（kernel-5.10-rockchip）：<code>drivers/mfd/max96745.c</code>、<code>drivers/pinctrl/pinctrl-max96745.c</code>、<code>include/linux/mfd/max96745.h</code></li>
</ul>
</blockquote>
<hr>
<h2 id="1-芯片是什么">1. 芯片是什么</h2>
<p>MAX96745 把 SoC 的 <strong>eDP / DisplayPort</strong> 视频信号串行化成 <strong>GMSL2</strong> 链路，通过一根同轴电缆（FAKRA 连接器）或 STP 双绞线传输到远端显示器，同时通过同一条线反向隧道 I²C / UART / GPIO 控制信号，并支持 Power-over-Coax（PoC）。</p>
<table>
<thead>
<tr>
<th>项目</th>
<th>内容</th>
</tr>
</thead>
<tbody>
<tr>
<td>厂商</td>
<td>Analog Devices（原 Maxim Integrated）</td>
</tr>
<tr>
<td>类型</td>
<td>eDP/DP → GMSL2 串行器（Serializer）</td>
</tr>
<tr>
<td>输出</td>
<td>双通道 GMSL2（链路 A / 链路 B）</td>
</tr>
<tr>
<td>车规型号示例</td>
<td>MAX96745GTN/V+T（AEC-Q100，TQFN）</td>
</tr>
<tr>
<td>评估板</td>
<td>MAX96745-AAG-EVK#</td>
</tr>
<tr>
<td>同系列</td>
<td>MAX96755 / MAX96755F（同类 eDP→GMSL2）</td>
</tr>
<tr>
<td>寄存器寻址</td>
<td>16 位地址 / 8 位数据，最大寄存器 0x8000</td>
</tr>
<tr>
<td>设备树 compatible</td>
<td><code>maxim,max96745</code> / <code>maxim,max96745-pinctrl</code> / <code>maxim,max96745-bridge</code></td>
</tr>
</tbody>
</table>
<p>典型应用：车载信息娱乐显示、仪表盘、后排娱乐屏、工业显示网关——凡是&quot;SoC 的 eDP 输出要拉一根长线到屏幕&quot;的场景。</p>
<h2 id="2-架构总览">2. 架构总览</h2>
<figure>
<svg width="880" height="460" viewBox="0 0 880 460" xmlns="http://www.w3.org/2000/svg">
<defs><linearGradient id="m45-bg" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#111827"/><stop offset="100%" stop-color="#1f2937"/></linearGradient><marker id="m45-ar" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto"><polygon points="0 0,10 3.5,0 7" fill="#38bdf8"/></marker></defs>
<rect width="880" height="460" fill="url(#m45-bg)" rx="16"/>
<text x="440" y="32" text-anchor="middle" fill="#facc15" font-family="Arial,sans-serif" font-size="18" font-weight="bold">MAX96745 系统架构</text>
<rect x="40" y="70" width="180" height="150" rx="10" fill="#0f172a" stroke="#38bdf8" stroke-width="2"/>
<text x="130" y="100" text-anchor="middle" fill="#e5e7eb" font-family="Arial,sans-serif" font-size="14">SoC 主控</text>
<text x="130" y="122" text-anchor="middle" fill="#cbd5e1" font-family="Arial,sans-serif" font-size="12">eDP / DisplayPort</text>
<text x="130" y="142" text-anchor="middle" fill="#cbd5e1" font-family="Arial,sans-serif" font-size="12">视频输出</text>
<text x="130" y="176" text-anchor="middle" fill="#cbd5e1" font-family="Arial,sans-serif" font-size="12">I²C / UART</text>
<text x="130" y="196" text-anchor="middle" fill="#cbd5e1" font-family="Arial,sans-serif" font-size="12">GPIO 控制</text>
<rect x="330" y="55" width="220" height="230" rx="10" fill="#172554" stroke="#60a5fa" stroke-width="2"/>
<text x="440" y="85" text-anchor="middle" fill="#e5e7eb" font-family="Arial,sans-serif" font-size="15" font-weight="bold">MAX96745</text>
<line x1="350" y1="97" x2="530" y2="97" stroke="#334155" stroke-width="1"/>
<text x="440" y="120" text-anchor="middle" fill="#cbd5e1" font-family="Arial,sans-serif" font-size="12">eDP/DP RX（训练/车道/速率）</text>
<text x="440" y="140" text-anchor="middle" fill="#cbd5e1" font-family="Arial,sans-serif" font-size="12">视频处理 + FRC 帧率转换</text>
<text x="440" y="160" text-anchor="middle" fill="#cbd5e1" font-family="Arial,sans-serif" font-size="12">GMSL2 串行化（链路 A / B）</text>
<text x="440" y="180" text-anchor="middle" fill="#cbd5e1" font-family="Arial,sans-serif" font-size="12">Remote CC（I²C/UART 隧道）</text>
<text x="440" y="200" text-anchor="middle" fill="#cbd5e1" font-family="Arial,sans-serif" font-size="12">GPIO 隧道（MFP0~25）</text>
<text x="440" y="220" text-anchor="middle" fill="#cbd5e1" font-family="Arial,sans-serif" font-size="12">线故障监测（LF）</text>
<text x="440" y="260" text-anchor="middle" fill="#94a3b8" font-family="Arial,sans-serif" font-size="11">16 位寄存器 · 0x0000~0x8000</text>
<rect x="660" y="60" width="180" height="100" rx="10" fill="#0f172a" stroke="#38bdf8" stroke-width="2"/>
<text x="750" y="92" text-anchor="middle" fill="#e5e7eb" font-family="Arial,sans-serif" font-size="14">链路 A</text>
<text x="750" y="114" text-anchor="middle" fill="#cbd5e1" font-family="Arial,sans-serif" font-size="12">同轴 / STP</text>
<text x="750" y="134" text-anchor="middle" fill="#cbd5e1" font-family="Arial,sans-serif" font-size="12">远端显示器 1</text>
<rect x="660" y="200" width="180" height="100" rx="10" fill="#0f172a" stroke="#38bdf8" stroke-width="2"/>
<text x="750" y="232" text-anchor="middle" fill="#e5e7eb" font-family="Arial,sans-serif" font-size="14">链路 B</text>
<text x="750" y="254" text-anchor="middle" fill="#cbd5e1" font-family="Arial,sans-serif" font-size="12">同轴 / STP</text>
<text x="750" y="274" text-anchor="middle" fill="#cbd5e1" font-family="Arial,sans-serif" font-size="12">远端显示器 2</text>
<path d="M220 130 L330 130" stroke="#38bdf8" stroke-width="2" fill="none" marker-end="url(#m45-ar)"/>
<path d="M220 185 L330 160" stroke="#38bdf8" stroke-width="2" fill="none" marker-end="url(#m45-ar)"/>
<path d="M550 130 L660 110" stroke="#38bdf8" stroke-width="2" fill="none" marker-end="url(#m45-ar)"/>
<path d="M550 185 L660 250" stroke="#38bdf8" stroke-width="2" fill="none" marker-end="url(#m45-ar)"/>
<text x="440" y="330" text-anchor="middle" fill="#94a3b8" font-family="Arial,sans-serif" font-size="11">视频 + 双向控制信号共缆传输，支持 PoC 远端供电</text>
</svg>
<figcaption style="text-align:center;color:#888;font-size:12px;margin-top:8px;">图 1　MAX96745 系统架构：eDP 输入，双通道 GMSL2 输出，控制信号反向隧道。</figcaption>
</figure>
<h2 id="3-特性详解">3. 特性详解</h2>
<h3 id="31-edpdisplayport-接收端">3.1 eDP/DisplayPort 接收端</h3>
<p>从寄存器布局可以确认这是一颗<strong>显示方向</strong>的串行器（对比摄像头方向的 MAX96717 是 CSI-2 输入）：</p>
<table>
<thead>
<tr>
<th>寄存器</th>
<th>位</th>
<th>名称</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>0x641A</td>
<td>7:4</td>
<td>DPRX_TRAIN_STATE</td>
<td>DP RX 链路训练状态（LT 阶段）</td>
</tr>
<tr>
<td>0x7000</td>
<td>0</td>
<td>LINK_ENABLE</td>
<td>DP 链路使能</td>
</tr>
<tr>
<td>0x7070</td>
<td>7:0</td>
<td>MAX_LANE_COUNT</td>
<td>DP lane 数量</td>
</tr>
<tr>
<td>0x7074</td>
<td>7:0</td>
<td>MAX_LINK_RATE</td>
<td>DP 链路速率档位</td>
</tr>
<tr>
<td>0x0101</td>
<td>5:0</td>
<td>BPP</td>
<td>输入每像素位数</td>
</tr>
</tbody>
</table>
<p>DP RX 的 lane 数和链路速率决定了能传输的最大分辨率/刷新率组合（典型 4-lane HBR2/HBR3，具体以 ADI 官方规格为准）。</p>
<h3 id="32-双通道-gmsl2-输出链路-a--b">3.2 双通道 GMSL2 输出（链路 A / B）</h3>
<p>两颗远端显示器可以各挂一条链路，或做镜像/复制输出。链路 A/B 的寄存器完全对称：</p>
<table>
<thead>
<tr>
<th>功能</th>
<th>链路 A</th>
<th>链路 B</th>
</tr>
</thead>
<tbody>
<tr>
<td>链路使能</td>
<td>0x0028（LINK_EN）</td>
<td>0x0032（LINK_EN）</td>
</tr>
<tr>
<td>链路复位</td>
<td>0x0029（RESET_LINK / RESET_ONESHOT）</td>
<td>0x0033（同左）</td>
</tr>
<tr>
<td>链路锁定状态</td>
<td>0x002A（LINK_LOCKED）</td>
<td>0x0034（同左）</td>
</tr>
<tr>
<td>远程控制通道开关</td>
<td>0x0076（DIS_REM_CC）</td>
<td>0x0086（DIS_REM_CC）</td>
</tr>
<tr>
<td>视频发送激活</td>
<td>0x0107 bit6（VID_TX_ACTIVE_A）</td>
<td>0x0107 bit7（VID_TX_ACTIVE_B）</td>
</tr>
<tr>
<td>视频状态</td>
<td>0x0102（PCLKDET/DRIFT_ERR/OVERFLOW/FIFO_WARN）</td>
<td>0x0108（同左）</td>
</tr>
<tr>
<td>线故障状态</td>
<td>0x0026（LF_0/LF_1）</td>
<td>0x0027（LF_2/LF_3）</td>
</tr>
</tbody>
</table>
<p>链路使能后必须轮询 LINK_LOCKED，U-Boot 驱动里未锁定直接返回 <code>-ENODEV</code>——这是最常见的&quot;串行器不工作&quot;排查点。</p>
<h3 id="33-视频发送与状态监测">3.3 视频发送与状态监测</h3>
<ul>
<li><code>0x0100</code>：VID_TX_EN（发送使能）+ VID_LINK_SEL（选择输出到哪条链路）</li>
<li><code>0x0102/0x0108</code>：实时状态——PCLKDET（像素时钟检测）、DRIFT_ERR（时钟漂移）、OVERFLOW（FIFO 溢出）、FIFO_WARN</li>
<li><code>0x0107</code>：VID_TX_ACTIVE，驱动用它在 extcon 里上报&quot;视频输出已激活&quot;（<code>EXTCON_JACK_VIDEO_OUT</code>）</li>
</ul>
<h3 id="34-frc-帧率转换">3.4 FRC 帧率转换</h3>
<p><code>0x0750~0x0752</code> 是一组帧率转换寄存器（FRCEN、FRCGAIN、FRCSDEN、FRCDATAWIDTH、FRCASYNCEN、FRCHSPOL/FRCVSPOL、FRCDCMODE 等）。显示串行器常用它做帧率/时序适配（例如 60Hz 输入转 120Hz 输出给高刷屏），配置时要同步确认输入 BPP 与输出时序的匹配。</p>
<h3 id="35-gpio-隧道mfp0--mfp25">3.5 GPIO 隧道（MFP0 ~ MFP25）</h3>
<p>这是 GMSL 串行器最有价值的功能之一：把本地 GPIO 通过链路隧道到远端，反过来远端 GPIO 也能读回来。</p>
<ul>
<li>26 个多功能引脚 MFP0~MFP25</li>
<li>每引脚 4 个寄存器（间隔 8 字节）：
<pre tabindex="0"><code>GPIO_A_REG(gpio) = 0x0200 + gpio × 8   （方向/输出配置）
GPIO_B_REG(gpio) = 0x0201 + gpio × 8   （TX 映射 ID/上下拉）
GPIO_C_REG(gpio) = 0x0202 + gpio × 8   （RX 映射 ID/边沿速率）
GPIO_D_REG(gpio) = 0x0203 + gpio × 8   （A/B 链路收发使能）
</code></pre></li>
<li>每个引脚可配：GPIO_TX_A_x / GPIO_TX_B_x（发向链路 A/B 的远端引脚）、GPIO_RX_A_x / GPIO_RX_B_x（从链路 A/B 的远端引脚接收）、GPIO（本地直连）、I2C、UART（MFP3/MFP7）</li>
<li>GPIO TX/RX ID 范围 0~31，用于告诉远端&quot;这个 GPIO 对应远端第几号脚&quot;</li>
</ul>
<p>Linux pinctrl 驱动把这些封装成 pin function（如 <code>GPIO_TX_A_5</code>），设备树里直接指定 function 即可，不用手写寄存器。</p>
<h3 id="36-ic--uart-隧道">3.6 I²C / UART 隧道</h3>
<ul>
<li>芯片本身是 <strong>I²C MUX</strong>：本地 I²C 总线通过 Remote CC 通道访问链路远端器件</li>
<li>两个通道对应两条链路：通道 0 走链路 A（0x0076 DIS_REM_CC 控制）、通道 1 走链路 B（0x0086）</li>
<li>设备树里用 <code>i2c-mux-idle-disconnect</code> 属性控制空闲时是否断开</li>
<li>UART 复用 MFP3/MFP7</li>
</ul>
<h3 id="37-线故障监测line-fault-monitor">3.7 线故障监测（Line Fault Monitor）</h3>
<p>驱动实现了完整的线缆故障诊断（sysfs 接口 <code>line_fault_monitor</code>）：</p>
<ul>
<li>使能：<code>0x0005</code> 的 PU_LF0~PU_LF3（对应 LMN0~3 引脚）</li>
<li>状态：<code>0x0026</code>（LF_0/LF_1）、<code>0x0027</code>（LF_2/LF_3）</li>
</ul>
<table>
<thead>
<tr>
<th>状态编码</th>
<th>含义</th>
</tr>
</thead>
<tbody>
<tr>
<td>0b000</td>
<td>对电源短路</td>
</tr>
<tr>
<td>0b001</td>
<td>对地短路</td>
</tr>
<tr>
<td>0b010</td>
<td>正常</td>
</tr>
<tr>
<td>0b011</td>
<td>线路开路</td>
</tr>
<tr>
<td>0b1XX</td>
<td>线间短路</td>
</tr>
</tbody>
</table>
<p>这对车载线束排查非常实用——不用万用表就能知道哪根线断了还是短路了。</p>
<h3 id="38-复位电源与功耗管理">3.8 复位、电源与功耗管理</h3>
<ul>
<li><code>0x0010</code>：RESET_ALL（bit7）、SLEEP（bit3）</li>
<li>电源引脚：<code>enable-gpio</code>（高有效，上电后需 200ms）、<code>pwdnb-gpio</code>（低有效，30ms）</li>
<li>驱动用 regmap 缓存（REGCACHE_RBTREE）支持 suspend/resume，休眠时缓存寄存器、唤醒后一次性同步</li>
</ul>
<h2 id="4-引脚定义驱动确认">4. 引脚定义（驱动确认）</h2>
<blockquote>
<p>说明：MAX96745 的物理封装引脚（TQFN 具体引脚号、电源/地/功能矩阵）属于 ADI datasheet 内容，官方不公开。以下为从开源驱动逆向确认的<strong>逻辑/功能引脚</strong>，可指导软件配置；硬件设计请向 ADI 申请官方文档。</p>
</blockquote>
<h3 id="41-多功能引脚-mfp0--mfp2526-个">4.1 多功能引脚 MFP0 ~ MFP25（26 个）</h3>
<p>pinctrl 驱动确认，每个 MFP 引脚可配置为以下功能之一：</p>
<table>
<thead>
<tr>
<th>功能</th>
<th>方向</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>GPIO</td>
<td>本地</td>
<td>本地普通 GPIO（输入 / 输出 / 上下拉）</td>
</tr>
<tr>
<td>GPIO_TX_A_x</td>
<td>本地 → 链路 A</td>
<td>把本地 GPIO 状态隧道发送到链路 A 远端 x 号脚</td>
</tr>
<tr>
<td>GPIO_TX_B_x</td>
<td>本地 → 链路 B</td>
<td>同上，发往链路 B</td>
</tr>
<tr>
<td>GPIO_RX_A_x</td>
<td>链路 A → 本地</td>
<td>读取链路 A 远端 x 号脚状态</td>
</tr>
<tr>
<td>GPIO_RX_B_x</td>
<td>链路 B → 本地</td>
<td>读取链路 B 远端 x 号脚状态</td>
</tr>
<tr>
<td>I2C</td>
<td>本地</td>
<td>复用为 I2C（仅 MFP3 / MFP7）</td>
</tr>
<tr>
<td>UART</td>
<td>本地</td>
<td>复用为 UART（仅 MFP3 / MFP7）</td>
</tr>
</tbody>
</table>
<ul>
<li>x = 0~31（GPIO 隧道远端映射 ID，寄存器 GPIO_TX_ID / GPIO_RX_ID）</li>
<li>每个引脚 4 个配置寄存器（0x0200 + gpio×8 起），方向 / 隧道映射 / 上下拉独立可配</li>
</ul>
<p><strong>引脚清单</strong>（26 个多功能引脚）：</p>
<table>
<thead>
<tr>
<th>引脚</th>
<th>可用功能</th>
</tr>
</thead>
<tbody>
<tr>
<td>MFP0 ~ MFP2</td>
<td>GPIO / GPIO_TX_A_x / GPIO_TX_B_x / GPIO_RX_A_x / GPIO_RX_B_x</td>
</tr>
<tr>
<td><strong>MFP3</strong></td>
<td>上述功能 <strong>+ I2C + UART</strong></td>
</tr>
<tr>
<td>MFP4 ~ MFP6</td>
<td>GPIO / GPIO_TX_A_x / GPIO_TX_B_x / GPIO_RX_A_x / GPIO_RX_B_x</td>
</tr>
<tr>
<td><strong>MFP7</strong></td>
<td>上述功能 <strong>+ I2C + UART</strong></td>
</tr>
<tr>
<td>MFP8 ~ MFP25</td>
<td>GPIO / GPIO_TX_A_x / GPIO_TX_B_x / GPIO_RX_A_x / GPIO_RX_B_x</td>
</tr>
</tbody>
</table>
<h3 id="42-线故障监测引脚-lmn0--lmn34-个">4.2 线故障监测引脚 LMN0 ~ LMN3（4 个）</h3>
<p>从 0x0005 的 PU_LF0~PU_LF3 与 0x0026 / 0x0027 的 LF_0~LF_3 推断：</p>
<table>
<thead>
<tr>
<th>引脚</th>
<th>使能位</th>
<th>状态位</th>
<th>监测对象</th>
</tr>
</thead>
<tbody>
<tr>
<td>LMN0</td>
<td>PU_LF0</td>
<td>LF_0</td>
<td>链路通道 0</td>
</tr>
<tr>
<td>LMN1</td>
<td>PU_LF1</td>
<td>LF_1</td>
<td>链路通道 1</td>
</tr>
<tr>
<td>LMN2</td>
<td>PU_LF2</td>
<td>LF_2</td>
<td>链路通道 2</td>
</tr>
<tr>
<td>LMN3</td>
<td>PU_LF3</td>
<td>LF_3</td>
<td>链路通道 3</td>
</tr>
</tbody>
</table>
<h3 id="43-控制与电源引脚">4.3 控制与电源引脚</h3>
<p>从设备树属性确认：</p>
<table>
<thead>
<tr>
<th>引脚</th>
<th>设备树属性</th>
<th>极性 / 时序</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>EN（使能）</td>
<td>enable-gpios</td>
<td>高有效，200ms</td>
<td>芯片上电使能</td>
</tr>
<tr>
<td>PWDNB</td>
<td>pwdnb-gpios</td>
<td>低有效，30ms</td>
<td>掉电 / 复位控制（低 = 关闭）</td>
</tr>
</tbody>
</table>
<h3 id="44-接口引脚接口级推断">4.4 接口引脚（接口级，推断）</h3>
<p>以下为接口级引脚（具体封装引脚号需官方文档）：</p>
<table>
<thead>
<tr>
<th>接口</th>
<th>说明</th>
<th>依据</th>
</tr>
</thead>
<tbody>
<tr>
<td>eDP / DP 输入</td>
<td>多 lane（lane 数可配）+ DP AUX</td>
<td>0x7070 MAX_LANE_COUNT、0x641A 训练状态</td>
</tr>
<tr>
<td>GMSL2 输出 A</td>
<td>SIO+/SIO-（或同轴单端）</td>
<td>链路 A 寄存器组</td>
</tr>
<tr>
<td>GMSL2 输出 B</td>
<td>SIO+/SIO-（或同轴单端）</td>
<td>链路 B 寄存器组</td>
</tr>
<tr>
<td>本地 I2C</td>
<td>SCL / SDA（16 位寄存器配置总线）</td>
<td>i2c 驱动</td>
</tr>
<tr>
<td>电源</td>
<td>核心供电 + PoC（Power-over-Coax）</td>
<td>enable / pwdnb 时序 + PoC 支持</td>
</tr>
</tbody>
</table>
<blockquote>
<p>物理封装（TQFN）的完整引脚图与功能矩阵请向 ADI 申请官方 datasheet（NDA）。</p>
</blockquote>
<h2 id="5-寄存器速查总表">5. 寄存器速查总表</h2>
<h3 id="41-系统与复位">4.1 系统与复位</h3>
<table>
<thead>
<tr>
<th>地址</th>
<th>位</th>
<th>名称</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>0x0005</td>
<td>3:0</td>
<td>PU_LF3~PU_LF0</td>
<td>线故障监测使能（每 bit 对应 LMN0~3）</td>
</tr>
<tr>
<td>0x0010</td>
<td>7</td>
<td>RESET_ALL</td>
<td>全复位</td>
</tr>
<tr>
<td>0x0010</td>
<td>3</td>
<td>SLEEP</td>
<td>睡眠</td>
</tr>
<tr>
<td>0x0013</td>
<td>3</td>
<td>LOCKED</td>
<td>链路锁定标志</td>
</tr>
<tr>
<td>0x0013</td>
<td>2</td>
<td>ERROR</td>
<td>链路错误标志</td>
</tr>
<tr>
<td>0x0070</td>
<td>7:0</td>
<td>I2C_FAST_MODE</td>
<td>I2C 快速模式配置（驱动写 0x16）</td>
</tr>
</tbody>
</table>
<h3 id="42-链路-a--b">4.2 链路 A / B</h3>
<table>
<thead>
<tr>
<th>地址</th>
<th>位</th>
<th>名称</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>0x0026</td>
<td>2:0 / 6:4</td>
<td>LF_0 / LF_1</td>
<td>链路 A 侧通道 0/1 线故障</td>
</tr>
<tr>
<td>0x0027</td>
<td>2:0 / 6:4</td>
<td>LF_2 / LF_3</td>
<td>链路 B 侧通道 2/3 线故障</td>
</tr>
<tr>
<td>0x0028 / 0x0032</td>
<td>7</td>
<td>LINK_EN</td>
<td>链路 A / B 使能</td>
</tr>
<tr>
<td>0x0029 / 0x0033</td>
<td>0</td>
<td>RESET_LINK</td>
<td>链路 A / B 复位</td>
</tr>
<tr>
<td>0x0029 / 0x0033</td>
<td>1</td>
<td>RESET_ONESHOT</td>
<td>链路 A / B 一次性复位</td>
</tr>
<tr>
<td>0x002A / 0x0034</td>
<td>0</td>
<td>LINK_LOCKED</td>
<td>链路 A / B 锁定状态</td>
</tr>
<tr>
<td>0x0076 / 0x0086</td>
<td>7</td>
<td>DIS_REM_CC</td>
<td>禁用链路 A / B 远程控制通道</td>
</tr>
</tbody>
</table>
<h3 id="43-视频">4.3 视频</h3>
<table>
<thead>
<tr>
<th>地址</th>
<th>位</th>
<th>名称</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>0x0100</td>
<td>2:1</td>
<td>VID_LINK_SEL</td>
<td>视频输出链路选择</td>
</tr>
<tr>
<td>0x0100</td>
<td>0</td>
<td>VID_TX_EN</td>
<td>视频发送使能</td>
</tr>
<tr>
<td>0x0101</td>
<td>5:0</td>
<td>BPP</td>
<td>每像素位数</td>
</tr>
<tr>
<td>0x0102</td>
<td>7</td>
<td>PCLKDET_A</td>
<td>链路 A 像素时钟检测</td>
</tr>
<tr>
<td>0x0102</td>
<td>6</td>
<td>DRIFT_ERR_A</td>
<td>链路 A 时钟漂移错误</td>
</tr>
<tr>
<td>0x0102</td>
<td>5</td>
<td>OVERFLOW_A</td>
<td>链路 A FIFO 溢出</td>
</tr>
<tr>
<td>0x0102</td>
<td>4</td>
<td>FIFO_WARN_A</td>
<td>链路 A FIFO 警告</td>
</tr>
<tr>
<td>0x0102</td>
<td>2</td>
<td>LIM_HEART</td>
<td>心跳限制</td>
</tr>
<tr>
<td>0x0107</td>
<td>7:6</td>
<td>VID_TX_ACTIVE_B / _A</td>
<td>链路 B / A 视频发送激活</td>
</tr>
<tr>
<td>0x0108</td>
<td>7:4</td>
<td>PCLKDET_B / DRIFT_ERR_B / OVERFLOW_B / FIFO_WARN_B</td>
<td>链路 B 视频状态</td>
</tr>
</tbody>
</table>
<h3 id="44-gpio--mfpgpio--025">4.4 GPIO / MFP（gpio = 0~25）</h3>
<table>
<thead>
<tr>
<th>寄存器组</th>
<th>位</th>
<th>名称</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>A（0x0200+gpio×8）</td>
<td>7</td>
<td>RES_CFG</td>
<td>电阻配置</td>
</tr>
<tr>
<td>A</td>
<td>5</td>
<td>TX_COM_EN</td>
<td>发送公共端使能</td>
</tr>
<tr>
<td>A</td>
<td>4 / 3</td>
<td>GPIO_OUT / GPIO_IN</td>
<td>输出 / 输入</td>
</tr>
<tr>
<td>A</td>
<td>0</td>
<td>GPIO_OUT_DIS</td>
<td>输出禁用</td>
</tr>
<tr>
<td>B（0x0201+gpio×8）</td>
<td>7:6</td>
<td>PULL_UPDN_SEL</td>
<td>上下拉选择</td>
</tr>
<tr>
<td>B</td>
<td>5</td>
<td>OUT_TYPEC</td>
<td>输出类型</td>
</tr>
<tr>
<td>B</td>
<td>4:0</td>
<td>GPIO_TX_ID</td>
<td>GPIO TX 映射 ID（0~31）</td>
</tr>
<tr>
<td>C（0x0202+gpio×8）</td>
<td>7</td>
<td>OVR_RES_CFG</td>
<td>电阻配置覆盖</td>
</tr>
<tr>
<td>C</td>
<td>6:5</td>
<td>IO_EDGE_RATE</td>
<td>IO 边沿速率</td>
</tr>
<tr>
<td>C</td>
<td>4:0</td>
<td>GPIO_RX_ID</td>
<td>GPIO RX 映射 ID（0~31）</td>
</tr>
<tr>
<td>D（0x0203+gpio×8）</td>
<td>5</td>
<td>GPIO_IO_RX_EN</td>
<td>IO RX 使能</td>
</tr>
<tr>
<td>D</td>
<td>4</td>
<td>GPIO_OUT_LGC</td>
<td>输出逻辑</td>
</tr>
<tr>
<td>D</td>
<td>3 / 2</td>
<td>GPIO_RX_EN_B / GPIO_TX_EN_B</td>
<td>链路 B 收 / 发使能</td>
</tr>
<tr>
<td>D</td>
<td>1 / 0</td>
<td>GPIO_RX_EN_A / GPIO_TX_EN_A</td>
<td>链路 A 收 / 发使能</td>
</tr>
</tbody>
</table>
<h3 id="45-frc-帧率转换">4.5 FRC 帧率转换</h3>
<table>
<thead>
<tr>
<th>地址</th>
<th>位</th>
<th>名称</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>0x0750</td>
<td>7:6</td>
<td>FRCZEROPAD</td>
<td>零填充</td>
</tr>
<tr>
<td>0x0750</td>
<td>5</td>
<td>FRCZPEN</td>
<td>零填充使能</td>
</tr>
<tr>
<td>0x0750</td>
<td>4</td>
<td>FRCSDGAIN</td>
<td>增益（简化）</td>
</tr>
<tr>
<td>0x0750</td>
<td>3</td>
<td>FRCSDEN</td>
<td>FRC 使能（简化）</td>
</tr>
<tr>
<td>0x0750</td>
<td>2:1</td>
<td>FRCGAIN</td>
<td>FRC 增益</td>
</tr>
<tr>
<td>0x0750</td>
<td>0</td>
<td>FRCEN</td>
<td>FRC 使能</td>
</tr>
<tr>
<td>0x0751</td>
<td>3</td>
<td>FRCDATAWIDTH</td>
<td>数据宽度</td>
</tr>
<tr>
<td>0x0751</td>
<td>2</td>
<td>FRCASYNCEN</td>
<td>异步模式</td>
</tr>
<tr>
<td>0x0751</td>
<td>1 / 0</td>
<td>FRCHSPOL / FRCVSPOL</td>
<td>HS / VS 极性</td>
</tr>
<tr>
<td>0x0752</td>
<td>1:0</td>
<td>FRCDCMODE</td>
<td>DC 模式</td>
</tr>
</tbody>
</table>
<h3 id="46-dp-接收">4.6 DP 接收</h3>
<table>
<thead>
<tr>
<th>地址</th>
<th>位</th>
<th>名称</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>0x641A</td>
<td>7:4</td>
<td>DPRX_TRAIN_STATE</td>
<td>DP 链路训练状态</td>
</tr>
<tr>
<td>0x7000</td>
<td>0</td>
<td>LINK_ENABLE</td>
<td>DP 链路使能</td>
</tr>
<tr>
<td>0x7070</td>
<td>7:0</td>
<td>MAX_LANE_COUNT</td>
<td>lane 数量</td>
</tr>
<tr>
<td>0x7074</td>
<td>7:0</td>
<td>MAX_LINK_RATE</td>
<td>链路速率</td>
</tr>
</tbody>
</table>
<h2 id="6-初始化流程由驱动推导">6. 初始化流程（由驱动推导）</h2>
<pre tabindex="0"><code>1. 上电：enable GPIO = 1 → 等待 200ms → pwdnb GPIO = 0 → 等待 30ms
2. 全复位：0x0010 写 RESET_ALL=1 → 等待 100ms
3. I2C 快速模式：0x0070 = 0x16
4. （可选）线故障监测：0x0005 置位 PU_LFx
5. 链路使能：0x0028（A）/ 0x0032（B）写 LINK_EN=1
6. 等待锁定：轮询 0x002A（A）/ 0x0034（B）LINK_LOCKED=1
   → 超时未锁定说明链路物理层有问题（线序/供电/对端未上电）
7. 视频发送：0x0100 写 VID_TX_EN=1 + VID_LINK_SEL 选择链路
8. GPIO 隧道：按需配置 MFP 的 0x0200~0x0203
   （Linux 下直接用 pinctrl 的 function，如 GPIO_TX_A_5）
9. I2C 隧道：通过 0x0076/0x0086 的 DIS_REM_CC 切换通道访问远端器件
</code></pre><h2 id="7-设备树配置示例">7. 设备树配置示例</h2>
<p>以下设备树基于驱动代码推断（I2C 地址和 GPIO 编号按实际硬件调整）：</p>
<pre tabindex="0"><code class="language-dts" data-lang="dts">&amp;i2c4 {
    max96745: max96745@48 {            /* I2C 地址按硬件实际修改 */
        compatible = &#34;maxim,max96745&#34;;
        reg = &lt;0x48&gt;;
        enable-gpios = &lt;&amp;gpio2 5 GPIO_ACTIVE_HIGH&gt;;   /* 上电使能 */
        pwdnb-gpios  = &lt;&amp;gpio2 6 GPIO_ACTIVE_LOW&gt;;    /* 低有效 */
        i2c-mux-idle-disconnect;                       /* 空闲断开 MUX */
        line-fault-monitor = &lt;0&gt;;                      /* 使能 LMN0 监测 */

        #address-cells = &lt;1&gt;;
        #size-cells = &lt;0&gt;;

        /* 链路 A 远端器件（通过 Remote CC 隧道访问） */
        link_a_dev: link_a@50 {
            reg = &lt;0x50&gt;;                              /* 远端器件 I2C 地址 */
        };

        /* 链路 B 远端器件 */
        link_b_dev: link_b@51 {
            reg = &lt;0x51&gt;;
        };
    };
};
</code></pre><blockquote>
<p>注：以上 I2C 地址 0x48 为常见默认值，<strong>需以实际硬件/ADI 确认为准</strong>；GPIO 编号按你的板卡原理图修改。</p>
</blockquote>
<h2 id="8-与同系列芯片对比">8. 与同系列芯片对比</h2>
<table>
<thead>
<tr>
<th>芯片</th>
<th>方向</th>
<th>输入</th>
<th>输出</th>
<th>用途</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>MAX96745</strong></td>
<td>串行器</td>
<td>eDP / DisplayPort</td>
<td>双 GMSL2</td>
<td>车载/工业显示长线传输</td>
</tr>
<tr>
<td>MAX96755 / F</td>
<td>串行器</td>
<td>eDP</td>
<td>GMSL2</td>
<td>同类显示串行器</td>
</tr>
<tr>
<td>MAX96717</td>
<td>串行器</td>
<td>MIPI CSI-2</td>
<td>GMSL2</td>
<td>摄像头信号上行车</td>
</tr>
<tr>
<td>MAX96789</td>
<td>串行器</td>
<td>多路</td>
<td>GMSL2</td>
<td>摄像头聚合（寄存器手册已另文整理）</td>
</tr>
<tr>
<td>MAX96724</td>
<td>解串器</td>
<td>4× GMSL2/1</td>
<td>CSI-2</td>
<td>摄像头信号聚合接收</td>
</tr>
</tbody>
</table>
<p>选型要点：<strong>显示方向选 MAX96745/96755，摄像头方向选 MAX96717/96789</strong>；需要多路输入聚合选解串器（MAX96724 等）。</p>
<h2 id="9-参考资料">9. 参考资料</h2>
<h3 id="驱动源码">驱动源码</h3>
<table>
<thead>
<tr>
<th>来源</th>
<th>仓库</th>
<th>文件</th>
</tr>
</thead>
<tbody>
<tr>
<td>U-Boot</td>
<td><a href="https://github.com/neggles/u-boot-quartz64">neggles/u-boot-quartz64</a></td>
<td><code>drivers/i2c/muxes/max96745.c</code></td>
</tr>
<tr>
<td>Linux MFD</td>
<td>kernel-5.10-rockchip</td>
<td><code>drivers/mfd/max96745.c</code></td>
</tr>
<tr>
<td>Linux Pinctrl</td>
<td>同上</td>
<td><code>drivers/pinctrl/pinctrl-max96745.c</code></td>
</tr>
<tr>
<td>Linux 头文件</td>
<td>同上</td>
<td><code>include/linux/mfd/max96745.h</code></td>
</tr>
</tbody>
</table>
<h3 id="官方资料">官方资料</h3>
<ul>
<li>ADI 官方产品页与 datasheet 不公开（需 NDA），可通过 ADI 官网联系 FAE 申请</li>
<li>ADI 官方 GMSL 开源入口：https://github.com/analogdevicesinc/gmsl</li>
<li>ADI GMSL2 硬件设计与验证指南（公开 PDF）：<code>Public GMSL2 Hardware Design and Validation Guide</code></li>
</ul>
<hr>
<p><em>本文档由开源驱动逆向整理，寄存器信息为芯片接口技术事实，仅供工程参考，不构成 ADI 官方规格承诺。</em></p>
]]></content:encoded></item><item><title>ARM TrustZone-M 安全启动与 OTA 实战：从分区、密钥到回滚防护</title><link>https://tech-snippets.xyz/posts/arm-trustzone-m-secure-boot-ota-guide/</link><pubDate>Thu, 11 Jun 2026 19:00:00 +0800</pubDate><guid>https://tech-snippets.xyz/posts/arm-trustzone-m-secure-boot-ota-guide/</guid><description>前言：为什么 MCU 也需要可信启动链 过去做单片机项目，很多团队把安全问题理解成“通信加个 TLS”“升级包做个 CRC”或者“调试口量产时关掉”。这些措施当然有价值，但如果设备会联网、会远程升级、会保存业务密钥，真正的风险往往出现在更早的启动阶段：攻击者能不能替换 Bootloader？能不能刷入旧版本固件重新打开已经修复的漏洞？能不能通过普通应用区读出密钥？能不能在升级断电后把设备卡成砖？
ARM Cortex-M23、Cortex-M33、Cortex-M35P、Cortex-M55 等内核引入的 TrustZone-M，正是为这些问题准备的一套硬件隔离基础设施。它不像服务器上的虚拟化那样厚重，也不是简单的软件权限判断，而是把整个地址空间、外设访问、中断入口和函数调用边界都划分成 Secure 与 Non-secure 两个世界。安全世界负责根信任、密钥、签名校验、回滚计数和少量可信服务；普通世界继续运行原有业务逻辑，例如传感器采集、协议栈、UI、云端连接和控制算法。
这篇文章不追求把 ARM 架构手册逐页复述，而是从一个真实产品视角出发，设计一条可落地的 TrustZone-M 安全启动与 OTA 链路。目标设备可以是带 Cortex-M33 的无线 MCU，也可以是安全要求较高的工业控制板。我们会讨论分区怎么切、密钥放在哪里、启动时校验什么、升级包如何设计、回滚防护如何做，以及调试和量产阶段最容易踩的坑。
一、TrustZone-M 的基本模型 TrustZone-M 的核心是“安全属性”。CPU 取指、读写内存、访问外设、响应中断时，硬件都会判断当前访问属于 Secure 还是 Non-secure。这个属性不是单靠软件变量维护，而是由 SAU（Security Attribution Unit）、IDAU（Implementation Defined Attribution Unit）以及厂商外设安全控制器共同决定。简单说，SAU 更像内核侧的安全地址表，IDAU 则是芯片厂商预先定义的安全属性，例如某些系统寄存器或 OTP 区域天然只能由 Secure 访问。
典型工程会把 Flash 分成 Secure Bootloader、Secure Service、Non-secure App、OTA Slot、Scratch 或 Trailer 几类区域。Secure Bootloader 最先运行，负责配置时钟、最小外设、安全属性和镜像验证；Secure Service 提供少量可被普通应用调用的安全接口，例如读取设备证书摘要、发起签名验签、获取随机数、更新回滚计数；Non-secure App 是业务主程序，绝大部分代码都放在这里；OTA Slot 保存待升级镜像；Scratch 或 Trailer 用来记录升级状态、断电恢复信息和镜像确认标志。
与普通 MPU 的区别在于，TrustZone-M 不只是“禁止某段代码访问某段内存”。它还定义了跨世界调用方式。Secure 代码可以主动跳转到 Non-secure；Non-secure 如果要调用安全服务，只能进入被标记为 Non-secure Callable 的小入口，也就是 NSC veneer。这个入口通常非常薄，只做参数检查和转发，真正的密钥操作仍在 Secure 内部完成。这样做的好处是普通应用拿不到安全函数的任意入口地址，也无法随意跳进安全代码中间执行。</description><content:encoded><![CDATA[<h2 id="前言为什么-mcu-也需要可信启动链">前言：为什么 MCU 也需要可信启动链</h2>
<p>过去做单片机项目，很多团队把安全问题理解成“通信加个 TLS”“升级包做个 CRC”或者“调试口量产时关掉”。这些措施当然有价值，但如果设备会联网、会远程升级、会保存业务密钥，真正的风险往往出现在更早的启动阶段：攻击者能不能替换 Bootloader？能不能刷入旧版本固件重新打开已经修复的漏洞？能不能通过普通应用区读出密钥？能不能在升级断电后把设备卡成砖？</p>
<p>ARM Cortex-M23、Cortex-M33、Cortex-M35P、Cortex-M55 等内核引入的 TrustZone-M，正是为这些问题准备的一套硬件隔离基础设施。它不像服务器上的虚拟化那样厚重，也不是简单的软件权限判断，而是把整个地址空间、外设访问、中断入口和函数调用边界都划分成 Secure 与 Non-secure 两个世界。安全世界负责根信任、密钥、签名校验、回滚计数和少量可信服务；普通世界继续运行原有业务逻辑，例如传感器采集、协议栈、UI、云端连接和控制算法。</p>
<p>这篇文章不追求把 ARM 架构手册逐页复述，而是从一个真实产品视角出发，设计一条可落地的 TrustZone-M 安全启动与 OTA 链路。目标设备可以是带 Cortex-M33 的无线 MCU，也可以是安全要求较高的工业控制板。我们会讨论分区怎么切、密钥放在哪里、启动时校验什么、升级包如何设计、回滚防护如何做，以及调试和量产阶段最容易踩的坑。</p>
<p><img alt="TrustZone-M 安全启动与 OTA 架构" loading="lazy" src="/images/arm-trustzone-m-secure-boot-ota.svg"></p>
<h2 id="一trustzone-m-的基本模型">一、TrustZone-M 的基本模型</h2>
<p>TrustZone-M 的核心是“安全属性”。CPU 取指、读写内存、访问外设、响应中断时，硬件都会判断当前访问属于 Secure 还是 Non-secure。这个属性不是单靠软件变量维护，而是由 SAU（Security Attribution Unit）、IDAU（Implementation Defined Attribution Unit）以及厂商外设安全控制器共同决定。简单说，SAU 更像内核侧的安全地址表，IDAU 则是芯片厂商预先定义的安全属性，例如某些系统寄存器或 OTP 区域天然只能由 Secure 访问。</p>
<p>典型工程会把 Flash 分成 Secure Bootloader、Secure Service、Non-secure App、OTA Slot、Scratch 或 Trailer 几类区域。Secure Bootloader 最先运行，负责配置时钟、最小外设、安全属性和镜像验证；Secure Service 提供少量可被普通应用调用的安全接口，例如读取设备证书摘要、发起签名验签、获取随机数、更新回滚计数；Non-secure App 是业务主程序，绝大部分代码都放在这里；OTA Slot 保存待升级镜像；Scratch 或 Trailer 用来记录升级状态、断电恢复信息和镜像确认标志。</p>
<p>与普通 MPU 的区别在于，TrustZone-M 不只是“禁止某段代码访问某段内存”。它还定义了跨世界调用方式。Secure 代码可以主动跳转到 Non-secure；Non-secure 如果要调用安全服务，只能进入被标记为 Non-secure Callable 的小入口，也就是 NSC veneer。这个入口通常非常薄，只做参数检查和转发，真正的密钥操作仍在 Secure 内部完成。这样做的好处是普通应用拿不到安全函数的任意入口地址，也无法随意跳进安全代码中间执行。</p>
<p>一个最小化设计原则是：Secure 世界越小越好。很多项目一开始把协议栈、文件系统甚至业务算法都塞进 Secure，结果不仅调试困难，还扩大了可信计算基。更稳妥的做法是把 Secure 当成“保险柜和门卫”：保险柜保存密钥与安全计数器，门卫判断镜像是否可信、请求是否合法。至于复杂业务，仍然放在 Non-secure 中，出问题也不应影响根信任。</p>
<h2 id="二启动链路从-rom-到业务应用">二、启动链路：从 ROM 到业务应用</h2>
<p>安全启动不是在应用入口前做一次哈希这么简单。它是一条链：芯片上电后先执行片上 ROM，ROM 根据厂商规则加载一级 Bootloader；一级 Bootloader 校验二级 Bootloader 或安全固件；安全固件再校验普通业务镜像；业务镜像运行后仍要通过“确认”机制告诉 Bootloader 新版本健康。链上任意一环没有验证，攻击者都可能在那里插入代码。</p>
<p>在 MCU 项目里，ROM 阶段通常由芯片厂商实现，我们能配置的内容包括启动源、调试权限、读保护等级、密钥哈希、生命周期状态和安全启动开关。有些芯片支持把公钥哈希写入 OTP，有些只能把根公钥放在安全 Flash，再用读保护保护。前者安全性更强，因为 OTP 一旦锁定，攻击者很难替换根公钥；后者实现更灵活，但必须认真处理安全区域擦写权限。</p>
<p>二级 Bootloader 可以选择 MCUboot、TF-M Bootloader，也可以自研。除非业务有非常特殊的镜像格式，建议优先基于成熟实现改造。成熟 Bootloader 已经处理了镜像头、签名 TLV、版本号、Swap 状态、断电恢复等细节，自研看起来不难，实际最容易在异常路径出问题。</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="kt">int</span> <span class="nf">secure_boot_main</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nf">early_clock_init</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="nf">configure_sau_and_mpc</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="nf">lock_debug_if_production</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kt">boot_state_t</span> <span class="n">st</span> <span class="o">=</span> <span class="nf">read_boot_state</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="kt">image_t</span> <span class="n">candidate</span> <span class="o">=</span> <span class="nf">select_candidate_image</span><span class="p">(</span><span class="n">st</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nf">verify_header</span><span class="p">(</span><span class="n">candidate</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">goto</span> <span class="n">boot_recovery</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="o">!</span><span class="nf">verify_signature</span><span class="p">(</span><span class="n">candidate</span><span class="p">,</span> <span class="nf">root_public_key_hash</span><span class="p">()))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">goto</span> <span class="n">boot_recovery</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="o">!</span><span class="nf">check_security_counter</span><span class="p">(</span><span class="n">candidate</span><span class="p">.</span><span class="n">version</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">goto</span> <span class="n">boot_recovery</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="o">!</span><span class="nf">verify_image_hash</span><span class="p">(</span><span class="n">candidate</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">goto</span> <span class="n">boot_recovery</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">configure_non_secure_vector</span><span class="p">(</span><span class="n">candidate</span><span class="p">.</span><span class="n">vector_table</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="nf">jump_to_non_secure</span><span class="p">(</span><span class="n">candidate</span><span class="p">.</span><span class="n">reset_handler</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nl">boot_recovery</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="nf">enter_recovery_mode_or_previous_image</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>这里有两个容易被忽略的点。第一，签名校验和哈希校验都要做。签名证明镜像来自可信发布者，哈希证明镜像内容未被修改。很多镜像格式会把哈希放进被签名的元数据里，Bootloader 校验签名后再计算整包哈希。第二，版本检查必须在启动阶段完成，而不是应用自己检查。应用已经被攻击者替换时，它当然可以谎报版本。</p>
<p>（第一部分完，约 2100 字）</p>
<h2 id="三flash-分区先画边界再写代码">三、Flash 分区：先画边界，再写代码</h2>
<p>TrustZone-M 项目的成败，很大程度取决于分区设计。分区不是简单把 Flash 平均切两半，而是要同时考虑擦除粒度、升级策略、镜像大小增长、安全属性、写保护、日志磨损和量产工具链。一个常见的 2MB Flash 设备可以这样规划：前 128KB 放 Secure Bootloader，接着 256KB 放 Secure Firmware 与 NSC veneer，之后 640KB 放 Non-secure Active Image，再放 640KB 的 Non-secure OTA Slot，剩余空间用于 NVM、升级状态、证书和工厂参数。</p>
<p>如果安全固件也需要 OTA，分区会更复杂。安全世界升级风险更高，因为升级失败可能直接破坏启动链。工程上通常有三种策略：第一，Secure Firmware 很小且极少升级，只允许工厂刷写；第二，为 Secure 与 Non-secure 分别设置双槽，Bootloader 同时验证两者兼容性；第三，把安全服务做成可追加的版本表，Bootloader 固定不变，服务接口保持向后兼容。消费级设备常采用第一种，工业网关和长期维护设备更适合第二种或第三种。</p>
<p>分区表最好由单一配置文件生成，避免链接脚本、Bootloader 配置、烧录脚本和文档各写一份。下面是一个 YAML 风格的示例：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">flash</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">base</span><span class="p">:</span><span class="w"> </span><span class="m">0x08000000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">size</span><span class="p">:</span><span class="w"> </span><span class="m">0x00200000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">erase_size</span><span class="p">:</span><span class="w"> </span><span class="m">0x00002000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">regions</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">secure_boot</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">offset</span><span class="p">:</span><span class="w"> </span><span class="m">0x00000000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">size</span><span class="p">:</span><span class="w"> </span><span class="m">0x00020000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">attr</span><span class="p">:</span><span class="w"> </span><span class="l">secure_rx</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">write</span><span class="p">:</span><span class="w"> </span><span class="l">factory_only</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">secure_fw</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">offset</span><span class="p">:</span><span class="w"> </span><span class="m">0x00020000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">size</span><span class="p">:</span><span class="w"> </span><span class="m">0x00040000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">attr</span><span class="p">:</span><span class="w"> </span><span class="l">secure_rx</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">write</span><span class="p">:</span><span class="w"> </span><span class="l">bootloader</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">nsc_veneer</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">offset</span><span class="p">:</span><span class="w"> </span><span class="l">0x0005F000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">size</span><span class="p">:</span><span class="w"> </span><span class="m">0x00001000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">attr</span><span class="p">:</span><span class="w"> </span><span class="l">nsc_rx</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">ns_app</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">offset</span><span class="p">:</span><span class="w"> </span><span class="m">0x00060000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">size</span><span class="p">:</span><span class="w"> </span><span class="l">0x000A0000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">attr</span><span class="p">:</span><span class="w"> </span><span class="l">non_secure_rx</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">ota_slot</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">offset</span><span class="p">:</span><span class="w"> </span><span class="m">0x00100000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">size</span><span class="p">:</span><span class="w"> </span><span class="l">0x000A0000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">attr</span><span class="p">:</span><span class="w"> </span><span class="l">non_secure_rw</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">kv_store</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">offset</span><span class="p">:</span><span class="w"> </span><span class="l">0x001A0000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">size</span><span class="p">:</span><span class="w"> </span><span class="m">0x00020000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">attr</span><span class="p">:</span><span class="w"> </span><span class="l">secure_rw</span><span class="w">
</span></span></span></code></pre></div><p>这类配置可以生成 linker script 片段、C 头文件、Python 打包脚本参数和产线烧录描述。团队里只要有一处手工同步，就迟早会出现“Bootloader 以为镜像从 A 地址开始，链接脚本却把向量表放在 B 地址”的事故。</p>
<h2 id="四密钥体系设备密钥和发布密钥不要混用">四、密钥体系：设备密钥和发布密钥不要混用</h2>
<p>安全启动至少涉及两类密钥：发布密钥和设备密钥。发布密钥用于签名固件，私钥必须保存在离线环境、HSM 或严格受控的 CI 机密系统中；设备端只保存公钥或公钥哈希。设备密钥用于设备身份、会话加密、数据封装或云端认证，它的私钥属于单台设备，不能出厂后被读取。二者绝对不要混用。把同一把密钥既用于固件签名又用于 TLS 客户端证书，是非常危险的设计。</p>
<p>固件签名建议使用 ECDSA P-256 或 Ed25519。许多 MCU 安全库和 PSA Crypto 对 P-256 支持更成熟，硬件加速也更常见；Ed25519 实现简洁，但要确认 Bootloader 和安全库的支持状态。哈希一般用 SHA-256。对资源很紧的设备，不要盲目追求复杂算法，验证路径的可靠性比算法名词更重要。</p>
<p>根公钥的更新也是一个实际问题。如果根公钥永远不能换，一旦私钥泄露，产品线就会非常被动；如果根公钥可以随意更新，攻击者又可能把自己的公钥写进去。常见做法是预置多个公钥哈希，或者用根密钥签发中间发布密钥。Bootloader 信任根公钥，日常发布使用中间密钥；需要轮换时发布一个被根密钥签名的密钥更新包。根密钥只在极少数情况下使用，放在更严格的离线环境。</p>
<p>设备密钥最好由芯片硬件唯一密钥派生，或者在安全产线注入后写入只读安全存储。对于支持 PSA Protected Storage 或 Internal Trusted Storage 的平台，可以把证书、私钥句柄和安全计数器放入安全存储，由 Secure Service 封装访问。普通应用只拿到句柄，不能读出明文私钥。</p>
<h2 id="五ota-包格式让-bootloader-能独立判断">五、OTA 包格式：让 Bootloader 能独立判断</h2>
<p>OTA 包不能依赖云端“说它合法”。设备离线、代理缓存、网络被劫持、升级包被拷贝到本地调试工具时，Bootloader 仍必须能独立判断镜像是否可信。因此升级包需要自描述：目标硬件、镜像类型、版本号、安全计数、长度、哈希、签名、压缩方式、加密信息和兼容性范围都应该在包里。</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">struct</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">header_version</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint16_t</span> <span class="n">image_type</span><span class="p">;</span>      <span class="c1">// secure, non-secure, radio, data
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="kt">uint32_t</span> <span class="n">load_address</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint32_t</span> <span class="n">image_size</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint32_t</span> <span class="n">version_major</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint32_t</span> <span class="n">version_minor</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint32_t</span> <span class="n">security_counter</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint8_t</span>  <span class="n">hw_model</span><span class="p">[</span><span class="mi">16</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint8_t</span>  <span class="n">dependency</span><span class="p">[</span><span class="mi">32</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint8_t</span>  <span class="n">image_hash</span><span class="p">[</span><span class="mi">32</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint32_t</span> <span class="n">tlv_offset</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="kt">image_header_t</span><span class="p">;</span>
</span></span></code></pre></div><p>签名覆盖范围应包括镜像头中除签名本身以外的关键字段以及镜像内容哈希。硬件型号字段可以阻止把 A 型号固件刷到 B 型号；dependency 可以声明“这个 Non-secure App 至少需要 Secure Service 2.3”。安全计数 security_counter 用于回滚防护，它不一定等于语义化版本号，但必须单调递增。</p>
<p>如果 OTA 包需要加密，建议采用“先签名后加密”还是“先加密后签名”要根据威胁模型定。很多 MCU 项目采用外层签名、内层加密或签名元数据加密载荷的组合。重点是 Bootloader 必须能在不泄露明文的情况下确认包来自可信发布者，并且只有目标设备或目标产品族能解密。加密不是安全启动的替代品，签名才是防止恶意固件的关键。</p>
<h2 id="六non-secure-callable-接口少即是多">六、Non-secure Callable 接口：少即是多</h2>
<p>NSC 接口是 Non-secure 世界进入 Secure 世界的门。接口越多，攻击面越大；参数越复杂，漏洞越难查。建议一开始只提供非常克制的一组能力：获取随机数、读取设备身份摘要、执行签名或解密操作、查询安全版本、提交升级确认、读取受保护配置。不要把“任意读安全存储”“任意写安全计数器”“传入函数指针回调”这类接口暴露出去。</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="nf">__attribute__</span><span class="p">((</span><span class="n">cmse_nonsecure_entry</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="kt">int</span> <span class="nf">secure_sign_digest</span><span class="p">(</span><span class="kt">uint32_t</span> <span class="n">key_id</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                       <span class="k">const</span> <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">ns_digest</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                       <span class="kt">size_t</span> <span class="n">digest_len</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                       <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">ns_sig</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                       <span class="kt">size_t</span> <span class="o">*</span><span class="n">ns_sig_len</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="o">!</span><span class="nf">cmse_check_address_range</span><span class="p">(</span><span class="n">ns_digest</span><span class="p">,</span> <span class="n">digest_len</span><span class="p">,</span> <span class="n">CMSE_NONSECURE</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">PSA_ERROR_INVALID_ARGUMENT</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="o">!</span><span class="nf">cmse_check_address_range</span><span class="p">(</span><span class="n">ns_sig</span><span class="p">,</span> <span class="o">*</span><span class="n">ns_sig_len</span><span class="p">,</span> <span class="n">CMSE_NONSECURE</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">PSA_ERROR_INVALID_ARGUMENT</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="o">!</span><span class="nf">is_key_allowed_for_non_secure</span><span class="p">(</span><span class="n">key_id</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">PSA_ERROR_NOT_PERMITTED</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">uint8_t</span> <span class="n">local_digest</span><span class="p">[</span><span class="mi">32</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">    <span class="nf">memcpy</span><span class="p">(</span><span class="n">local_digest</span><span class="p">,</span> <span class="n">ns_digest</span><span class="p">,</span> <span class="mi">32</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nf">psa_sign_hash</span><span class="p">(</span><span class="nf">key_handle_from_id</span><span class="p">(</span><span class="n">key_id</span><span class="p">),</span> <span class="nf">PSA_ALG_ECDSA</span><span class="p">(</span><span class="n">PSA_ALG_SHA_256</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                         <span class="n">local_digest</span><span class="p">,</span> <span class="mi">32</span><span class="p">,</span> <span class="n">ns_sig</span><span class="p">,</span> <span class="o">*</span><span class="n">ns_sig_len</span><span class="p">,</span> <span class="n">ns_sig_len</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>两个检查很重要：第一，所有来自 Non-secure 的指针都要用 CMSE 辅助函数验证，防止普通应用传入 Secure 地址诱导安全服务读写；第二，参数复制到 Secure 本地缓冲区后再使用，避免校验后调用前被 Non-secure 中断修改，也就是常说的 TOCTOU 问题。对较长数据可以分块处理，但状态机必须保存在 Secure 侧。</p>
<p>（第二部分完，约 2300 字）</p>
<h2 id="七回滚防护版本号不可信单调计数才可信">七、回滚防护：版本号不可信，单调计数才可信</h2>
<p>回滚攻击很常见：攻击者不需要破解最新固件，只要把设备刷回存在漏洞的旧版本，就可能绕过认证、恢复默认口令或重新打开调试接口。解决办法是在安全存储中维护单调递增的安全计数器。Bootloader 只启动 security_counter 不小于已记录值的镜像；当新镜像通过试运行并确认健康后，再把计数器提升到新值。</p>
<p>计数器保存位置要抗篡改。理想情况是 OTP、 eFuse 单调计数器或安全元件；如果只有 Flash，就要用安全区域、冗余记录、写入序号和校验码降低掉电损坏概率。Flash 计数器不能无限擦写，因此不要每次启动都写，只在版本确认时写。对于有频繁灰度回退需求的产品，可以把“业务版本回退”和“安全计数回退”区分开：允许同一安全计数范围内的小版本切换，但不允许退到已经废弃的安全基线之前。</p>
<p>升级确认机制也不能省。新固件第一次启动时处于 pending 状态，应用完成自检、联网、关键外设初始化后调用安全服务 <code>confirm_image()</code>。如果设备在确认前复位多次，Bootloader 应自动回退到旧镜像或进入恢复模式。这样可以避免升级包能通过签名校验，却因为驱动 bug、配置不兼容或电源异常导致大面积变砖。</p>
<h2 id="八调试口生命周期与量产">八、调试口、生命周期与量产</h2>
<p>安全启动在实验室跑通不代表量产安全。很多事故发生在生命周期配置上：开发板调试口全开，量产脚本忘了切换保护级别；固件签名私钥放在工程目录；产线为了方便保留了全擦写权限；返修模式没有认证就允许刷机。这些问题不属于算法漏洞，却更容易被利用。</p>
<p>建议把设备生命周期分成 Development、Provisioning、Production、RMA 四个状态。Development 允许调试和未签名固件，但只能用于样机；Provisioning 注入设备证书、写入公钥哈希、配置安全区域；Production 关闭普通调试入口，只保留经过认证的维护能力；RMA 需要云端授权或挑战响应，进入后可以导出诊断日志，但不能导出密钥。</p>
<p>量产脚本要可审计。每台设备应生成一份简短的生产记录：芯片唯一 ID、公钥哈希版本、证书序列号、安全启动开关状态、调试锁状态、固件版本和写入时间。记录不需要包含敏感密钥，但要能在售后定位“这台设备到底有没有完成安全配置”。如果条件允许，产线工具应先读回关键锁定位确认，再打印合格标签。</p>
<h2 id="九断电恢复与双槽策略">九、断电恢复与双槽策略</h2>
<p>OTA 最怕升级到一半断电。双槽 A/B 是最容易解释也最稳的策略：当前运行镜像在 Active Slot，下载的新镜像写入 Secondary Slot；写完后校验哈希和签名，设置 pending 标志；下次启动由 Bootloader 执行 swap 或直接从新槽启动；应用确认后标记 permanent。A/B 的代价是 Flash 占用翻倍，但对联网设备非常值得。</p>
<p>如果 Flash 不够，可以选择覆盖式升级加小 Scratch 区，但实现复杂度明显上升。Bootloader 需要按擦除块搬移数据，并在每一步写入状态，确保任意时刻断电都能继续或回退。不要只在 RAM 中保存升级进度，掉电后 RAM 没了，设备就不知道哪些块有效。</p>
<p>升级状态记录建议采用“追加写 + 魔数 + CRC + 序号”的小日志，而不是反复覆盖同一个结构体。启动时扫描最后一条有效记录即可恢复状态。这样既能提升掉电可靠性，也能减少单个 Flash 字段磨损。</p>
<h2 id="十在-zephyr--tf-m-项目中的落地路径">十、在 Zephyr / TF-M 项目中的落地路径</h2>
<p>如果项目基于 Zephyr，TrustZone-M 通常与 MCUboot、TF-M、Partition Manager、devicetree 和 Kconfig 一起使用。开发顺序建议是：先跑通官方安全样例，确认 Secure 与 Non-secure 镜像能启动；再固定分区和链接地址；然后引入 MCUboot 签名；接着增加自定义 NSC 服务；最后才接入云端 OTA。不要一上来同时改 Bootloader、TF-M、应用和云端协议，否则排查会非常痛苦。</p>
<p>常用检查命令包括查看镜像头、确认签名 TLV、检查 map 文件和反汇编向量表：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">west build -b your_board_ns app
</span></span><span class="line"><span class="cl">imgtool verify --key root_pub.pem build/zephyr/zephyr.signed.bin
</span></span><span class="line"><span class="cl">arm-none-eabi-readelf -S build/tfm/secure_fw.elf
</span></span><span class="line"><span class="cl">arm-none-eabi-nm build/zephyr/zephyr.elf <span class="p">|</span> grep -E <span class="s2">&#34;__Vectors|veneer|cmse&#34;</span>
</span></span></code></pre></div><p>如果是裸机或 RTOS 自研工程，也可以借鉴同样的层次：Bootloader 单独工程，Secure Service 单独链接，Non-secure App 单独链接，三者通过生成的分区头文件共享地址。CI 中至少做三件事：未签名镜像必须启动失败；低安全计数镜像必须被拒绝；损坏一个字节的镜像必须被拒绝。安全功能没有自动化测试，后期很容易被“临时调试开关”破坏。</p>
<h2 id="十一常见故障排查清单">十一、常见故障排查清单</h2>
<p>第一类问题是跳转 Non-secure 后 HardFault。优先检查 Non-secure 向量表地址是否对齐，MSP_NS 是否落在 Non-secure SRAM，Reset Handler 最低位是否为 1，SAU 是否把应用 Flash 和 SRAM 标成 Non-secure。还要确认 Secure 侧在跳转前关闭或重新归属不必要的中断。</p>
<p>第二类问题是 NSC 调用失败。检查 veneer 区域是否被标记为 NSC，链接脚本是否把 cmse veneer 放到预期地址，Non-secure 工程使用的导入库是否与 Secure 固件同版本。Secure 接口改了参数但 Non-secure 仍用旧头文件，是很常见的隐蔽错误。</p>
<p>第三类问题是签名验证在 Bootloader 里失败，但 PC 工具验证成功。通常是镜像覆盖范围、填充字节、端序、TLV 偏移或压缩后再签名的顺序不一致。建议把 Bootloader 实际计算出的哈希通过安全日志输出一次，与 PC 端对比；确认后关闭这类详细日志，避免泄露内部布局。</p>
<p>第四类问题是量产后无法调试。安全项目必须预留受控诊断方案，例如安全日志缓冲区、错误码持久化、RMA 授权流程和最小恢复固件。完全没有诊断能力的“安全”设备，现场维护成本会非常高。</p>
<h2 id="十二工程化测试把攻击样例放进-ci">十二、工程化测试：把攻击样例放进 CI</h2>
<p>安全启动和 OTA 最需要自动化测试，因为它们平时很少被业务同事主动触碰，却会在发布脚本、分区调整、编译选项变化时被意外破坏。建议在 CI 中保留一组“坏镜像”：签名字段被清空的镜像、签名正确但内容被改一个字节的镜像、安全计数低于当前设备计数的镜像、目标硬件型号不匹配的镜像、依赖 Secure Service 版本过高的镜像，以及下载过程中截断的镜像。每次 Bootloader 或打包工具变更后，都要确认这些镜像不能启动。</p>
<p>板级自动化也很有必要。用继电器或可控电源在升级写 Flash、swap、首次启动确认前后随机断电，重复几百次，观察设备最终是否能回到旧版本、新版本或恢复模式之一。只要出现“既不是旧版本也不是新版本，串口也没有错误码”的状态，就说明状态机还不够稳。很多 OTA 事故不是验签失败，而是异常路径没有被真正跑过。</p>
<p>日志策略要平衡诊断和泄露。开发阶段可以打印镜像哈希、分区地址和错误细节；量产版本至少要降级为错误码和阶段码，例如 <code>BOOT_E_SIG</code>、<code>BOOT_E_COUNTER</code>、<code>BOOT_E_SWAP_STATE</code>。这些错误码应写入安全或半安全日志区，方便售后读取，但不要把公钥、证书私钥、解密后的载荷或随机数种子打印出来。</p>
<h2 id="十三工程化建议与总结">十三、工程化建议与总结</h2>
<p>TrustZone-M 的价值不在于把产品宣传成“用了硬件安全”，而在于把几个关键事实变成硬约束：普通应用读不到密钥，未签名固件不能启动，旧漏洞版本不能回滚，升级断电可以恢复，量产状态可以审计。做到这些之后，TLS、设备证书、云端策略和应用权限才有可靠基础。</p>
<p>落地时可以按优先级推进。第一阶段，完成 Bootloader 签名校验和基本 A/B OTA；第二阶段，引入安全计数器与升级确认；第三阶段，把设备私钥迁移到 Secure Service，并收敛 NSC 接口；第四阶段，完善生命周期、产线审计和 RMA 流程。每个阶段都要有自动化测试和失败样例，不要只测试“正常升级成功”。</p>
<p>最后提醒一句：安全设计最怕“差不多”。CRC 差不多能发现传输错误，但不能抵抗恶意修改；读保护差不多能挡住普通读取，但挡不住被替换的 Bootloader；版本字符串差不多能显示新旧，但不能防回滚。TrustZone-M 给了 MCU 一套很好的隔离工具，但它只是一把刀，真正决定产品安全性的，是启动链、密钥体系、升级协议和量产流程能不能闭环。</p>
<p>（全文完，约 7300 字）</p>
]]></content:encoded></item><item><title>Cortex-M Cache、MPU 与 DMA 一致性实战：把 STM32H7 这类高性能 MCU 跑稳跑快</title><link>https://tech-snippets.xyz/posts/arm-cortex-m-cache-mpu-dma-coherency-guide/</link><pubDate>Mon, 08 Jun 2026 19:00:00 +0800</pubDate><guid>https://tech-snippets.xyz/posts/arm-cortex-m-cache-mpu-dma-coherency-guide/</guid><description>前言：高性能 MCU 最隐蔽的坑，不是算力不够，而是数据“不一致” 很多人第一次从 Cortex-M3 / Cortex-M4 迁移到 Cortex-M7，感受非常直接：主频更高了，FPU 更强了，片上 SRAM 更大了，外设带宽也上来了。以 STM32H7、NXP i.MX RT、部分国产高性能 MCU 为例，系统里开始出现 I-Cache、D-Cache、AXI SRAM、多级总线矩阵、MDMA、ETH、SDMMC、DCMI、LTDC 这类过去在小 MCU 上不太需要认真处理的模块。代码还是 C，外设还是 DMA，调试器还是能单步，但一旦项目进入图像采集、以太网、文件系统、音频流或者屏幕刷新，问题会变得很诡异：
DMA 明明已经写完了缓冲区，CPU 读到的还是旧数据； CPU 明明把发送包填好了，以太网 DMA 发出去的却是上一帧； 关掉 D-Cache 后系统稳定了，但吞吐掉了一大截； 加了一句 SCB_CleanDCache_by_Addr() 后偶尔好、偶尔坏； 同样的代码 Debug 版本正常，Release 版本或者换了优化等级就出错； 缓冲区长度不是 32 字节倍数时，旁边的变量被“莫名其妙”污染。 这些现象的根源通常不是外设驱动写错，也不是编译器“玄学”，而是 CPU、Cache、MPU、DMA 对同一段内存的理解不一致。Cortex-M7 的 D-Cache 提升了 CPU 访问速度，但 DMA 控制器通常不会经过 D-Cache，它直接从 SRAM 或外部 RAM 读写。于是同一个地址，在 CPU 看来可能是 Cache Line 里的新数据，在 DMA 看来却是内存里的旧数据；反过来，DMA 已经把新数据写入内存，CPU 仍然命中旧的 Cache Line。</description><content:encoded><![CDATA[<h2 id="前言高性能-mcu-最隐蔽的坑不是算力不够而是数据不一致">前言：高性能 MCU 最隐蔽的坑，不是算力不够，而是数据“不一致”</h2>
<p>很多人第一次从 Cortex-M3 / Cortex-M4 迁移到 Cortex-M7，感受非常直接：主频更高了，FPU 更强了，片上 SRAM 更大了，外设带宽也上来了。以 STM32H7、NXP i.MX RT、部分国产高性能 MCU 为例，系统里开始出现 I-Cache、D-Cache、AXI SRAM、多级总线矩阵、MDMA、ETH、SDMMC、DCMI、LTDC 这类过去在小 MCU 上不太需要认真处理的模块。代码还是 C，外设还是 DMA，调试器还是能单步，但一旦项目进入图像采集、以太网、文件系统、音频流或者屏幕刷新，问题会变得很诡异：</p>
<ul>
<li>DMA 明明已经写完了缓冲区，CPU 读到的还是旧数据；</li>
<li>CPU 明明把发送包填好了，以太网 DMA 发出去的却是上一帧；</li>
<li>关掉 D-Cache 后系统稳定了，但吞吐掉了一大截；</li>
<li>加了一句 <code>SCB_CleanDCache_by_Addr()</code> 后偶尔好、偶尔坏；</li>
<li>同样的代码 Debug 版本正常，Release 版本或者换了优化等级就出错；</li>
<li>缓冲区长度不是 32 字节倍数时，旁边的变量被“莫名其妙”污染。</li>
</ul>
<p>这些现象的根源通常不是外设驱动写错，也不是编译器“玄学”，而是 CPU、Cache、MPU、DMA 对同一段内存的理解不一致。Cortex-M7 的 D-Cache 提升了 CPU 访问速度，但 DMA 控制器通常不会经过 D-Cache，它直接从 SRAM 或外部 RAM 读写。于是同一个地址，在 CPU 看来可能是 Cache Line 里的新数据，在 DMA 看来却是内存里的旧数据；反过来，DMA 已经把新数据写入内存，CPU 仍然命中旧的 Cache Line。</p>
<p>本文不把重点放在寄存器手册逐位翻译上，而是从工程落地角度讲清楚三个问题：第一，Cache、MPU、DMA 为什么会互相影响；第二，如何设计可维护的内存区域和缓冲区策略；第三，怎样写出能在网络、摄像头、SD 卡、屏幕刷新场景里长期稳定运行的代码。文章示例偏向 STM32H7 / Cortex-M7，但方法同样适用于其他带 D-Cache、MPU 和 DMA 的高性能 MCU。</p>
<p><img alt="Cortex-M Cache、MPU 与 DMA 一致性关系" loading="lazy" src="/images/arm-cortex-m-cache-mpu-dma-coherency.svg"></p>
<h2 id="一先建立一个基本模型cpu-看-cachedma-看内存">一、先建立一个基本模型：CPU 看 Cache，DMA 看内存</h2>
<p>在传统 Cortex-M0 / M3 / M4 项目里，我们常常把“地址”和“数据”简单绑定：某个指针指向 SRAM，CPU 写了什么，DMA 就能读到什么；DMA 写了什么，CPU 再读就能看到什么。这个模型在没有 D-Cache 的系统里基本成立，最多需要考虑 <code>volatile</code>、中断竞争和总线带宽。</p>
<p>到了 Cortex-M7，模型必须改成：CPU 访问某个地址时，可能先访问 D-Cache；DMA 访问某个地址时，通常直接访问 SRAM、AXI SRAM、DTCM 或外部 SDRAM。D-Cache 的常见 Cache Line 大小是 32 字节，CPU 写入一个变量时，可能只是把对应 Cache Line 标记为 dirty，还没真正写回内存。CPU 读取一个地址时，如果 Cache 命中，也可能根本不去内存取 DMA 刚写入的新内容。</p>
<p>这就是所谓 Cache 一致性问题。桌面 CPU 或高端 SoC 往往有硬件一致性协议，多个核心、DMA、外设通过 ACE、CHI 等协议维持一致。但很多 MCU 的 DMA 并不参与 D-Cache 一致性协议，维护责任就落到了软件身上。</p>
<h3 id="三类常见内存的差异">三类常见内存的差异</h3>
<p>以 STM32H7 为例，开发者经常会遇到 DTCM RAM、AXI SRAM、SRAM1 / SRAM2 / SRAM3、外部 SDRAM 等区域。它们不只是容量不同，访问路径也不同：</p>
<ol>
<li><strong>DTCM RAM</strong>：CPU 访问非常快，适合栈、实时控制变量、算法中间状态。但不少 DMA 外设无法访问 DTCM，所以把 DMA 缓冲区放在 DTCM 里会直接失败，表现为 DMA 不搬运或外设无数据。</li>
<li><strong>AXI SRAM</strong>：挂在 AXI 总线矩阵上，CPU 和很多 DMA 都能访问，是大块 DMA Buffer 的常见选择。配合 Cache 后 CPU 处理速度好，但必须做一致性维护。</li>
<li><strong>外部 SDRAM / PSRAM</strong>：容量大，适合帧缓冲、神经网络中间张量、文件缓存，但延迟和仲裁复杂，更需要 Cache，也更容易暴露刷新、对齐、带宽瓶颈。</li>
</ol>
<p>所以工程里的第一个原则是：不要只问“这段内存够不够大”，还要问“谁会访问它、通过哪条总线访问、是否经过 Cache、是否允许 DMA 访问”。</p>
<h2 id="二mpu-的价值把内存使用约定固化为硬件属性">二、MPU 的价值：把“内存使用约定”固化为硬件属性</h2>
<p>MPU（Memory Protection Unit）在很多裸机项目里被忽略，大家觉得它只是 RTOS 做任务隔离、权限保护时才需要。实际上在带 D-Cache 的 MCU 上，MPU 最常用的价值是定义内存属性：某段区域是否可缓存、是否 Bufferable、是否 Shareable、是否允许执行、读写权限如何。</p>
<p>如果没有 MPU，很多库会用默认内存属性启动 Cache。默认属性对普通代码和数据可能没问题，但对 DMA 共享缓冲区未必合适。一个成熟的高性能 MCU 项目，通常会把内存划分成几类：</p>
<ul>
<li>普通代码和常规数据：可缓存，追求 CPU 性能；</li>
<li>DMA 描述符：不可缓存或严格手动维护，追求确定性；</li>
<li>大块 DMA 数据缓冲区：可缓存但按方向 Clean / Invalidate，兼顾吞吐；</li>
<li>外设寄存器区域：Device 类型，不允许乱序和缓存；</li>
<li>帧缓冲：根据 LTDC / DMA2D / CPU 绘制比例选择 Write-through、Write-back 或 Non-cacheable。</li>
</ul>
<p>这种划分看起来麻烦，但它能把“约定”变成系统启动时的硬件配置，而不是散落在驱动代码里的注释。</p>
<h3 id="一个典型的-mpu-区域规划">一个典型的 MPU 区域规划</h3>
<p>下面是一个简化的规划，不直接对应某一块芯片的完整地址表，但足够说明思路：</p>
<table>
<thead>
<tr>
<th>区域</th>
<th>用途</th>
<th>属性建议</th>
<th>原因</th>
</tr>
</thead>
<tbody>
<tr>
<td>Flash</td>
<td>代码、只读表</td>
<td>Normal、Cacheable、Executable</td>
<td>提高取指和查表速度</td>
</tr>
<tr>
<td>DTCM</td>
<td>栈、控制变量、实时算法</td>
<td>Normal、Non-cacheable 或 TCM 默认</td>
<td>CPU 低延迟访问，不给 DMA 用</td>
</tr>
<tr>
<td>AXI SRAM 普通段</td>
<td>堆、算法 Buffer</td>
<td>Normal、Write-back Cacheable</td>
<td>提高 CPU 处理吞吐</td>
</tr>
<tr>
<td>AXI SRAM DMA 描述符段</td>
<td>ETH、SDMMC、USB 描述符</td>
<td>Normal、Non-cacheable</td>
<td>避免描述符状态不同步</td>
</tr>
<tr>
<td>AXI SRAM DMA 数据段</td>
<td>网络包、图像块、音频块</td>
<td>Normal、Cacheable + 手动维护</td>
<td>大块数据用 Cache 提速</td>
</tr>
<tr>
<td>外设寄存器</td>
<td>GPIO、DMA、ETH 等</td>
<td>Device、Non-cacheable</td>
<td>禁止缓存和不合适的重排</td>
</tr>
</tbody>
</table>
<p>需要注意的是，MPU Region 的大小和基地址一般有对齐要求，且 Region 数量有限。不要为每个小数组都单独建 Region，而应把共享 Buffer 放到统一的链接段里，例如 <code>.dma_desc</code>、<code>.dma_buffer</code>，再用链接脚本保证对齐和边界。</p>
<h2 id="三cache-维护动作cleaninvalidate-和-cleaninvalidate-的边界">三、Cache 维护动作：Clean、Invalidate 和 CleanInvalidate 的边界</h2>
<p>Cache 维护 API 名字很像，很多 bug 就出在调用时机反了。可以用一句话记住：</p>
<ul>
<li><strong>CPU 写、DMA 读</strong>：启动 DMA 前要 Clean，让 CPU 写在 Cache 里的脏数据写回内存；</li>
<li><strong>DMA 写、CPU 读</strong>：DMA 完成后要 Invalidate，让 CPU 丢掉旧 Cache Line，下次从内存取新数据；</li>
<li><strong>双向或状态不确定</strong>：谨慎使用 CleanInvalidate，但要确认不会把 DMA 新写的数据被旧脏 Cache 覆盖。</li>
</ul>
<p>以发送网络包为例，CPU 先构造以太网帧，然后 ETH DMA 从内存读取并发送。如果 CPU 的帧内容还停留在 D-Cache，ETH DMA 读到的就是旧内存。正确动作是在把描述符交给 DMA 前，对数据 Buffer 执行 Clean。</p>
<p>以接收网络包为例，ETH DMA 把数据写入内存，然后中断通知 CPU。CPU 如果之前读过这个 Buffer，对应 Cache Line 可能还在 D-Cache 里。正确动作是在 CPU 解析包之前，对 Buffer 执行 Invalidate。</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"></span>
</span></span><span class="line"><span class="cl"><span class="k">static</span> <span class="kr">inline</span> <span class="kt">uintptr_t</span> <span class="nf">align_down</span><span class="p">(</span><span class="kt">uintptr_t</span> <span class="n">addr</span><span class="p">,</span> <span class="kt">uintptr_t</span> <span class="n">align</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">return</span> <span class="n">addr</span> <span class="o">&amp;</span> <span class="o">~</span><span class="p">(</span><span class="n">align</span> <span class="o">-</span> <span class="mi">1U</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">static</span> <span class="kr">inline</span> <span class="kt">uintptr_t</span> <span class="nf">align_up</span><span class="p">(</span><span class="kt">uintptr_t</span> <span class="n">addr</span><span class="p">,</span> <span class="kt">uintptr_t</span> <span class="n">align</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">return</span> <span class="p">(</span><span class="n">addr</span> <span class="o">+</span> <span class="n">align</span> <span class="o">-</span> <span class="mi">1U</span><span class="p">)</span> <span class="o">&amp;</span> <span class="o">~</span><span class="p">(</span><span class="n">align</span> <span class="o">-</span> <span class="mi">1U</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">dma_clean_range</span><span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="n">addr</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="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uintptr_t</span> <span class="n">start</span> <span class="o">=</span> <span class="nf">align_down</span><span class="p">((</span><span class="kt">uintptr_t</span><span class="p">)</span><span class="n">addr</span><span class="p">,</span> <span class="n">CACHE_LINE_SIZE</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uintptr_t</span> <span class="n">end</span>   <span class="o">=</span> <span class="nf">align_up</span><span class="p">((</span><span class="kt">uintptr_t</span><span class="p">)</span><span class="n">addr</span> <span class="o">+</span> <span class="n">len</span><span class="p">,</span> <span class="n">CACHE_LINE_SIZE</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="nf">SCB_CleanDCache_by_Addr</span><span class="p">((</span><span class="kt">uint32_t</span> <span class="o">*</span><span class="p">)</span><span class="n">start</span><span class="p">,</span> <span class="p">(</span><span class="kt">int32_t</span><span class="p">)(</span><span class="n">end</span> <span class="o">-</span> <span class="n">start</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">    <span class="nf">__DSB</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">dma_invalidate_range</span><span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="n">addr</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="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uintptr_t</span> <span class="n">start</span> <span class="o">=</span> <span class="nf">align_down</span><span class="p">((</span><span class="kt">uintptr_t</span><span class="p">)</span><span class="n">addr</span><span class="p">,</span> <span class="n">CACHE_LINE_SIZE</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uintptr_t</span> <span class="n">end</span>   <span class="o">=</span> <span class="nf">align_up</span><span class="p">((</span><span class="kt">uintptr_t</span><span class="p">)</span><span class="n">addr</span> <span class="o">+</span> <span class="n">len</span><span class="p">,</span> <span class="n">CACHE_LINE_SIZE</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="nf">SCB_InvalidateDCache_by_Addr</span><span class="p">((</span><span class="kt">uint32_t</span> <span class="o">*</span><span class="p">)</span><span class="n">start</span><span class="p">,</span> <span class="p">(</span><span class="kt">int32_t</span><span class="p">)(</span><span class="n">end</span> <span class="o">-</span> <span class="n">start</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">    <span class="nf">__DSB</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>这里最关键的细节是地址和长度按 Cache Line 对齐。很多 HAL 示例只传原始地址和长度，看似能跑，实则在非对齐缓冲区上风险很高。因为硬件维护的是整条 Cache Line，不是 C 语言对象。如果一个 20 字节 DMA Buffer 与旁边变量共享同一条 32 字节 Cache Line，Invalidate 时可能把旁边变量在 Cache 里的修改也丢掉；Clean 时也可能把不该提交的数据写回。</p>
<p>（第一部分完，约 2400 字）</p>
<h2 id="四链接脚本不要让-dma-buffer-和普通变量混住">四、链接脚本：不要让 DMA Buffer 和普通变量混住</h2>
<p>只靠 <code>__attribute__((aligned(32)))</code> 能解决一部分问题，但不够系统。对齐解决的是起点问题，不能保证链接器不会把其他变量放在同一条 Cache Line 附近，也不能保证整段区域落在 DMA 可访问的 SRAM。更稳妥的做法是为 DMA 描述符和 DMA 数据建立专门的链接段。</p>
<p>以 GCC 链接脚本为例，可以在 AXI SRAM 中划出两个区域：一个不可缓存的描述符区，一个可缓存但手动维护的数据区。实际地址需要根据芯片手册调整，这里只展示结构。</p>
<pre tabindex="0"><code class="language-ld" data-lang="ld">MEMORY
{
  FLASH   (rx)  : ORIGIN = 0x08000000, LENGTH = 2048K
  DTCMRAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
  AXIRAM  (xrw) : ORIGIN = 0x24000000, LENGTH = 512K
}

SECTIONS
{
  .dma_desc (NOLOAD) : ALIGN(32)
  {
    __dma_desc_start__ = .;
    *(.dma_desc*)
    . = ALIGN(32);
    __dma_desc_end__ = .;
  } &gt; AXIRAM

  .dma_buffer (NOLOAD) : ALIGN(32)
  {
    __dma_buffer_start__ = .;
    *(.dma_buffer*)
    . = ALIGN(32);
    __dma_buffer_end__ = .;
  } &gt; AXIRAM
}
</code></pre><p>在 C 代码中可以这样声明：</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 DMA_ALIGN __attribute__((aligned(32)))
</span></span></span><span class="line"><span class="cl"><span class="cp">#define DMA_DESC  __attribute__((section(&#34;.dma_desc&#34;), aligned(32)))
</span></span></span><span class="line"><span class="cl"><span class="cp">#define DMA_BUF   __attribute__((section(&#34;.dma_buffer&#34;), aligned(32)))
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl"><span class="n">DMA_DESC</span> <span class="n">ETH_DMADescTypeDef</span> <span class="n">eth_rx_desc</span><span class="p">[</span><span class="n">ETH_RX_DESC_CNT</span><span class="p">];</span>
</span></span><span class="line"><span class="cl"><span class="n">DMA_DESC</span> <span class="n">ETH_DMADescTypeDef</span> <span class="n">eth_tx_desc</span><span class="p">[</span><span class="n">ETH_TX_DESC_CNT</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">DMA_BUF</span> <span class="kt">uint8_t</span> <span class="n">eth_rx_pool</span><span class="p">[</span><span class="n">ETH_RX_DESC_CNT</span><span class="p">][</span><span class="mi">1536</span><span class="p">];</span>
</span></span><span class="line"><span class="cl"><span class="n">DMA_BUF</span> <span class="kt">uint8_t</span> <span class="n">eth_tx_pool</span><span class="p">[</span><span class="n">ETH_TX_DESC_CNT</span><span class="p">][</span><span class="mi">1536</span><span class="p">];</span>
</span></span></code></pre></div><p>这样做有几个好处。第一，审查 map 文件时一眼能看到 DMA 资源放在哪里；第二，MPU 可以对整段 <code>.dma_desc</code> 设置 Non-cacheable；第三，数据 Buffer 至少不会和普通全局变量混在同一条 Cache Line 里；第四，换芯片或换板子时迁移成本更低。</p>
<h2 id="五mpu-初始化顺序先配置属性再打开-cache">五、MPU 初始化顺序：先配置属性，再打开 Cache</h2>
<p>MPU 和 Cache 的初始化顺序很重要。通常建议在系统启动早期完成：关闭 MPU，配置各 Region，开启 MPU，然后再开启 I-Cache / D-Cache。若系统已经跑起来再修改某段内存属性，就要非常小心先清理、失效相关 Cache，否则会留下难以复现的状态。</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="kt">void</span> <span class="nf">system_memory_attr_init</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="cm">/* AXI SRAM: default write-back cacheable */</span>
</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_NUMBER0</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="mh">0x24000000</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_512KB</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">IsBufferable</span>     <span class="o">=</span> <span class="n">MPU_ACCESS_BUFFERABLE</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_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">IsShareable</span>      <span class="o">=</span> <span class="n">MPU_ACCESS_NOT_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">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">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">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="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></span><span class="line"><span class="cl">    <span class="cm">/* DMA descriptor window: non-cacheable */</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_NUMBER1</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="mh">0x24070000</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_16KB</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 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">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="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></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></span><span class="line"><span class="cl">    <span class="nf">SCB_EnableICache</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="nf">SCB_EnableDCache</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>这段代码不能原样复制到所有项目，因为不同芯片的 HAL 宏、地址、Region 大小和默认属性会不同。真正需要复制的是原则：外设寄存器保持 Device 属性；普通内存尽量 Cacheable；描述符区优先 Non-cacheable；大块数据区如果追求吞吐，就 Cacheable 加维护函数；所有区域基址和大小满足 MPU 规则。</p>
<h2 id="六发送方向案例cpu-生产数据dma-消费数据">六、发送方向案例：CPU 生产数据，DMA 消费数据</h2>
<p>发送方向是最容易讲清楚的场景。假设我们用 SPI DMA 发送一段采样数据，或者用 ETH DMA 发送网络帧。数据流是：CPU 写 Buffer，DMA 读 Buffer，外设发送。</p>
<p>正确流程应该是：</p>
<ol>
<li>CPU 填写 Buffer；</li>
<li>如果 Buffer 位于 Cacheable 区域，执行 Clean；</li>
<li>设置 DMA 源地址、长度、方向；</li>
<li>必要时执行内存屏障；</li>
<li>启动 DMA；</li>
<li>DMA 完成后释放 Buffer 或进入下一轮。</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="kt">int</span> <span class="nf">eth_send_frame</span><span class="p">(</span><span class="kt">uint8_t</span> <span class="o">*</span><span class="n">buf</span><span class="p">,</span> <span class="kt">uint16_t</span> <span class="n">len</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">==</span> <span class="mi">0</span> <span class="o">||</span> <span class="n">len</span> <span class="o">&gt;</span> <span class="mi">1536</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="o">-</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="cm">/* CPU 已经把以太网帧写入 buf。启动 DMA 前必须写回内存。 */</span>
</span></span><span class="line"><span class="cl">    <span class="nf">dma_clean_range</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span> <span class="n">len</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nf">__DMB</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="cm">/* 下面是假想接口：把 buf 交给 DMA 描述符 */</span>
</span></span><span class="line"><span class="cl">    <span class="nf">eth_tx_desc_prepare</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span> <span class="n">len</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="nf">eth_tx_kick_dma</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>这里有一个容易被忽视的问题：如果描述符本身也在 Cacheable 区域，除了 Clean 数据 Buffer，还要 Clean 描述符。很多以太网驱动会把描述符设为 Non-cacheable，这样 CPU 改描述符后 DMA 立刻能看到，驱动逻辑更简单。代价是 CPU 访问描述符略慢，但描述符很小，这点开销通常可以接受。</p>
<p>对于 SDMMC 写卡、QSPI DMA 写 Flash、SAI / I2S 播放音频，道理完全一样。只要方向是 CPU 先写、DMA 后读，核心动作就是 Clean。</p>
<h2 id="七接收方向案例dma-生产数据cpu-消费数据">七、接收方向案例：DMA 生产数据，CPU 消费数据</h2>
<p>接收方向更容易出错，因为 CPU 可能在 DMA 完成前无意中读过 Buffer。例如网络栈初始化时清零、调试打印、协议栈预取，都会让对应 Cache Line 进入 D-Cache。DMA 完成后如果不 Invalidate，CPU 解析的仍然可能是旧内容。</p>
<p>正确流程应该是：</p>
<ol>
<li>准备空 Buffer；</li>
<li>如果 Buffer 曾被 CPU 写过，启动 DMA 前可根据情况 Clean 或 CleanInvalidate；</li>
<li>启动 DMA，让外设写入内存；</li>
<li>等待完成中断或轮询完成标志；</li>
<li>CPU 读取前执行 Invalidate；</li>
<li>解析 Buffer。</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="kt">int</span> <span class="nf">camera_capture_one</span><span class="p">(</span><span class="kt">uint8_t</span> <span class="o">*</span><span class="n">frame</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="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="cm">/* 如果之前 CPU 清过 frame，且该区域可缓存，启动前先清理到内存。 */</span>
</span></span><span class="line"><span class="cl">    <span class="nf">dma_clean_range</span><span class="p">(</span><span class="n">frame</span><span class="p">,</span> <span class="n">len</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nf">dcmi_start_dma</span><span class="p">(</span><span class="n">frame</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">if</span> <span class="p">(</span><span class="o">!</span><span class="nf">wait_frame_done</span><span class="p">(</span><span class="mi">100</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nf">dcmi_stop_dma</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="o">-</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="cm">/* DMA 已写完，CPU 读取前丢弃旧 Cache Line。 */</span>
</span></span><span class="line"><span class="cl">    <span class="nf">dma_invalidate_range</span><span class="p">(</span><span class="n">frame</span><span class="p">,</span> <span class="n">len</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>有些资料会建议“接收前 Invalidate 一次，接收后再 Invalidate 一次”。这并非完全没有道理：接收前 Invalidate 可以避免某条 dirty Cache Line 在后续被替换时写回，覆盖 DMA 新数据；接收后 Invalidate 则确保 CPU 看到 DMA 结果。但工程上更推荐避免 CPU 在 DMA 运行期间访问同一 Buffer，并通过 Buffer 状态机明确所有权。如果所有权混乱，再多维护函数也只是降低概率，不是根治。</p>
<h2 id="八双缓冲与环形队列用所有权模型替代到处加维护函数">八、双缓冲与环形队列：用所有权模型替代“到处加维护函数”</h2>
<p>高吞吐外设通常不会只用一个 Buffer。摄像头有帧缓冲，音频有 ping-pong Buffer，网络有 RX / TX Descriptor Ring，SD 卡有块缓存。此时最重要的不是多调用几个 Cache API，而是建立清晰的 Buffer 所有权模型。</p>
<p>可以把每个 Buffer 的状态定义为：</p>
<ul>
<li><code>FREE</code>：CPU 可以填充或交给外设；</li>
<li><code>DMA_OWNED</code>：DMA 正在读写，CPU 禁止访问；</li>
<li><code>CPU_OWNED</code>：DMA 已完成，CPU 可以解析或修改；</li>
<li><code>QUEUED</code>：已经放入协议栈或应用队列，等待消费。</li>
</ul>
<p>发送方向中，Buffer 从 <code>CPU_OWNED</code> 变成 <code>DMA_OWNED</code> 的瞬间执行 Clean；接收方向中，Buffer 从 <code>DMA_OWNED</code> 变成 <code>CPU_OWNED</code> 的瞬间执行 Invalidate。维护动作绑定在状态转换点，而不是散落在业务代码里。</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">BUF_FREE</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">BUF_CPU_OWNED</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">BUF_DMA_OWNED</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">BUF_QUEUED</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="kt">buf_state_t</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><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">uint8_t</span> <span class="o">*</span><span class="n">addr</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint32_t</span> <span class="n">len</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">volatile</span> <span class="kt">buf_state_t</span> <span class="n">state</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="kt">dma_buf_t</span><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">rx_dma_complete_isr</span><span class="p">(</span><span class="kt">dma_buf_t</span> <span class="o">*</span><span class="n">b</span><span class="p">,</span> <span class="kt">uint32_t</span> <span class="n">actual_len</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">b</span><span class="o">-&gt;</span><span class="n">len</span> <span class="o">=</span> <span class="n">actual_len</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="nf">dma_invalidate_range</span><span class="p">(</span><span class="n">b</span><span class="o">-&gt;</span><span class="n">addr</span><span class="p">,</span> <span class="n">actual_len</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="n">b</span><span class="o">-&gt;</span><span class="n">state</span> <span class="o">=</span> <span class="n">BUF_CPU_OWNED</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">tx_submit</span><span class="p">(</span><span class="kt">dma_buf_t</span> <span class="o">*</span><span class="n">b</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">dma_clean_range</span><span class="p">(</span><span class="n">b</span><span class="o">-&gt;</span><span class="n">addr</span><span class="p">,</span> <span class="n">b</span><span class="o">-&gt;</span><span class="n">len</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="n">b</span><span class="o">-&gt;</span><span class="n">state</span> <span class="o">=</span> <span class="n">BUF_DMA_OWNED</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="nf">dma_submit_to_hw</span><span class="p">(</span><span class="n">b</span><span class="o">-&gt;</span><span class="n">addr</span><span class="p">,</span> <span class="n">b</span><span class="o">-&gt;</span><span class="n">len</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>这种写法还有一个额外好处：后续如果你把某些 Buffer 改成 Non-cacheable，只需要调整 <code>dma_clean_range()</code> / <code>dma_invalidate_range()</code> 的实现或内存属性，不必到业务层逐个删代码。</p>
<p>（第二部分完，约 2600 字）</p>
<h2 id="九什么时候应该把-dma-区域设为-non-cacheable">九、什么时候应该把 DMA 区域设为 Non-cacheable？</h2>
<p>既然手动维护 Cache 这么容易出错，那是不是把所有 DMA Buffer 都设成 Non-cacheable 就好？答案是：可以，但要看数据规模和访问模式。</p>
<p>适合 Non-cacheable 的对象通常有三类。第一类是描述符、状态字、门铃寄存器镜像这类小对象，它们频繁被 CPU 和 DMA 交替访问，数据量小，追求确定性大于追求 Cache 命中。第二类是低速外设的小包 Buffer，例如 UART DMA 收几十字节命令，Cache 带来的收益有限。第三类是调试阶段，为了尽快排除一致性问题，可以临时把共享区设为 Non-cacheable，确认问题是否由 Cache 引起。</p>
<p>不适合 Non-cacheable 的对象也很典型：摄像头整帧、LCD Framebuffer、神经网络输入输出张量、网络大吞吐数据池、文件系统块缓存。如果 CPU 会对这些数据做大量扫描、拷贝、颜色转换、校验或协议解析，完全关闭 Cache 往往会让性能掉到不可接受。比如 800×480 的 RGB565 帧缓冲接近 750KB，CPU 绘制 UI 时如果每次都直接打到外部 SDRAM，刷新率和响应都会很难看。</p>
<p>因此更实用的策略是：<strong>描述符 Non-cacheable，大数据 Cacheable；维护动作集中封装；所有 Buffer 32 字节对齐；禁止 CPU 和 DMA 同时拥有同一 Buffer。</strong></p>
<h2 id="十性能调优一致性正确只是第一步">十、性能调优：一致性正确只是第一步</h2>
<p>很多项目修完 Cache bug 后才发现，性能仍然不理想。原因是 Cache 一致性解决的是“数据对不对”，而吞吐还受总线仲裁、内存 Bank、突发长度、访问模式影响。</p>
<h3 id="1-减少无意义的整帧-clean--invalidate">1. 减少无意义的整帧 Clean / Invalidate</h3>
<p>维护 Cache 也有成本。对一个几百 KB 的帧缓冲整帧 Invalidate，会占用明显时间。如果 DMA 实际只写了一个 ROI 区域，就只维护对应范围；如果网络包实际长度是 300 字节，不要对整个 1536 字节池做维护。</p>
<h3 id="2-避免-cpu-在-dma-期间扫描同一区域">2. 避免 CPU 在 DMA 期间扫描同一区域</h3>
<p>有些代码为了显示进度或做调试，会在 DMA 接收过程中读取 Buffer 的前几个字节。这会让 Cache 状态复杂化，也可能造成总线竞争。更好的做法是用单独的状态变量或 DMA 中断标志表示进度，不要偷看正在被 DMA 拥有的数据区。</p>
<h3 id="3-对齐不仅是-cache-line也是总线突发">3. 对齐不仅是 Cache Line，也是总线突发</h3>
<p>32 字节对齐解决 Cache Line 问题，但某些 DMA 或总线对 64 字节、128 字节边界更友好。图像和音频这类连续大块数据，尽量让行跨度、块大小、环形 Buffer 节点都接近硬件推荐的突发长度，减少跨 Bank 和非对齐传输。</p>
<h3 id="4-用-dwt-或-etm-做真实测量">4. 用 DWT 或 ETM 做真实测量</h3>
<p>不要只靠主观感觉判断优化效果。Cortex-M 提供 DWT cycle counter，可以很方便地测量 Clean、Invalidate、memcpy、协议解析耗时。</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="kr">inline</span> <span class="kt">void</span> <span class="nf">dwt_init</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">CoreDebug</span><span class="o">-&gt;</span><span class="n">DEMCR</span> <span class="o">|=</span> <span class="n">CoreDebug_DEMCR_TRCENA_Msk</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">DWT</span><span class="o">-&gt;</span><span class="n">CYCCNT</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">DWT</span><span class="o">-&gt;</span><span class="n">CTRL</span> <span class="o">|=</span> <span class="n">DWT_CTRL_CYCCNTENA_Msk</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">uint32_t</span> <span class="nf">measure_invalidate_cycles</span><span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="n">addr</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="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint32_t</span> <span class="n">start</span> <span class="o">=</span> <span class="n">DWT</span><span class="o">-&gt;</span><span class="n">CYCCNT</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="nf">dma_invalidate_range</span><span class="p">(</span><span class="n">addr</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">DWT</span><span class="o">-&gt;</span><span class="n">CYCCNT</span> <span class="o">-</span> <span class="n">start</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>把测量结果打印成表格，往往比猜测有效得多。你会看到维护 64 字节、1500 字节、64KB、整帧图像的成本完全不同，也会发现某些“为了保险”的全局维护动作其实非常昂贵。</p>
<h2 id="十一常见故障排查清单">十一、常见故障排查清单</h2>
<p>遇到 DMA + Cache 相关问题时，可以按下面顺序排查，效率会高很多。</p>
<ol>
<li><strong>确认 DMA 能访问该内存区域</strong>：很多 DMA 不能访问 DTCM，先查总线矩阵和参考手册，不要只看地址是 SRAM 就默认可以。</li>
<li><strong>确认 Buffer 地址和长度对齐</strong>：地址至少 32 字节对齐，长度向上补齐，最好让链接段边界也对齐。</li>
<li><strong>确认方向对应的维护动作</strong>：CPU 写 DMA 读用 Clean；DMA 写 CPU 读用 Invalidate；描述符也要考虑。</li>
<li><strong>确认维护时机</strong>：Clean 必须在启动 DMA 前；Invalidate 必须在 DMA 完成后、CPU 读取前。</li>
<li><strong>确认没有并发访问</strong>：DMA 拥有 Buffer 期间，CPU 不要读写同一范围。</li>
<li><strong>确认 MPU 属性符合预期</strong>：读回 MPU 配置或在启动日志打印 Region 表，不要只相信 CubeMX 或默认启动文件。</li>
<li><strong>确认优化等级下屏障仍然存在</strong>：关键状态切换处加 <code>__DMB()</code> / <code>__DSB()</code>，尤其是描述符交接和中断处理路径。</li>
<li><strong>临时关闭 D-Cache 做 A/B 测试</strong>：如果关闭后问题消失，基本可以把方向锁定在一致性维护或 MPU 属性。</li>
<li><strong>检查 map 文件</strong>：确认 Buffer 没被链接到错误区域，也没有因为拼写错误导致 section 属性失效。</li>
<li><strong>检查库代码隐藏访问</strong>：协议栈、文件系统、图形库可能提前读写 Buffer，需要把所有权规则延伸到库接口边界。</li>
</ol>
<h2 id="十二一个更完整的工程封装思路">十二、一个更完整的工程封装思路</h2>
<p>实际项目里，不建议业务代码直接调用 <code>SCB_CleanDCache_by_Addr()</code>。可以建立一个很薄的 <code>dma_mem</code> 模块，统一处理对齐、属性、统计和调试断言。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cm">/* dma_mem.h */</span>
</span></span><span class="line"><span class="cl"><span class="cp">#pragma once
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;stddef.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;stdint.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 DMA_CACHE_LINE 32U
</span></span></span><span class="line"><span class="cl"><span class="cp">#define DMA_SECTION __attribute__((section(&#34;.dma_buffer&#34;), aligned(DMA_CACHE_LINE)))
</span></span></span><span class="line"><span class="cl"><span class="cp">#define DMA_DESC_SECTION __attribute__((section(&#34;.dma_desc&#34;), aligned(DMA_CACHE_LINE)))
</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="nf">dma_mem_init</span><span class="p">(</span><span class="kt">void</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="nf">dma_mem_clean</span><span class="p">(</span><span class="k">const</span> <span class="kt">void</span> <span class="o">*</span><span class="n">addr</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">void</span> <span class="nf">dma_mem_invalidate</span><span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="n">addr</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">void</span> <span class="nf">dma_mem_check_range</span><span class="p">(</span><span class="k">const</span> <span class="kt">void</span> <span class="o">*</span><span class="n">addr</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">len</span><span class="p">);</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cm">/* dma_mem.c */</span>
</span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&#34;dma_mem.h&#34;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&#34;cmsis_gcc.h&#34;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&#34;core_cm7.h&#34;</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="k">static</span> <span class="kt">uintptr_t</span> <span class="nf">down</span><span class="p">(</span><span class="kt">uintptr_t</span> <span class="n">v</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">return</span> <span class="n">v</span> <span class="o">&amp;</span> <span class="o">~</span><span class="p">(</span><span class="kt">uintptr_t</span><span class="p">)(</span><span class="n">DMA_CACHE_LINE</span> <span class="o">-</span> <span class="mi">1U</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">static</span> <span class="kt">uintptr_t</span> <span class="nf">up</span><span class="p">(</span><span class="kt">uintptr_t</span> <span class="n">v</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">return</span> <span class="p">(</span><span class="n">v</span> <span class="o">+</span> <span class="n">DMA_CACHE_LINE</span> <span class="o">-</span> <span class="mi">1U</span><span class="p">)</span> <span class="o">&amp;</span> <span class="o">~</span><span class="p">(</span><span class="kt">uintptr_t</span><span class="p">)(</span><span class="n">DMA_CACHE_LINE</span> <span class="o">-</span> <span class="mi">1U</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">dma_mem_clean</span><span class="p">(</span><span class="k">const</span> <span class="kt">void</span> <span class="o">*</span><span class="n">addr</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="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">==</span> <span class="mi">0</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uintptr_t</span> <span class="n">s</span> <span class="o">=</span> <span class="nf">down</span><span class="p">((</span><span class="kt">uintptr_t</span><span class="p">)</span><span class="n">addr</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uintptr_t</span> <span class="n">e</span> <span class="o">=</span> <span class="nf">up</span><span class="p">((</span><span class="kt">uintptr_t</span><span class="p">)</span><span class="n">addr</span> <span class="o">+</span> <span class="n">len</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="nf">SCB_CleanDCache_by_Addr</span><span class="p">((</span><span class="kt">uint32_t</span> <span class="o">*</span><span class="p">)</span><span class="n">s</span><span class="p">,</span> <span class="p">(</span><span class="kt">int32_t</span><span class="p">)(</span><span class="n">e</span> <span class="o">-</span> <span class="n">s</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">    <span class="nf">__DSB</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">dma_mem_invalidate</span><span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="n">addr</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="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">==</span> <span class="mi">0</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uintptr_t</span> <span class="n">s</span> <span class="o">=</span> <span class="nf">down</span><span class="p">((</span><span class="kt">uintptr_t</span><span class="p">)</span><span class="n">addr</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uintptr_t</span> <span class="n">e</span> <span class="o">=</span> <span class="nf">up</span><span class="p">((</span><span class="kt">uintptr_t</span><span class="p">)</span><span class="n">addr</span> <span class="o">+</span> <span class="n">len</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="nf">SCB_InvalidateDCache_by_Addr</span><span class="p">((</span><span class="kt">uint32_t</span> <span class="o">*</span><span class="p">)</span><span class="n">s</span><span class="p">,</span> <span class="p">(</span><span class="kt">int32_t</span><span class="p">)(</span><span class="n">e</span> <span class="o">-</span> <span class="n">s</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">    <span class="nf">__DSB</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>在 Debug 版本中，<code>dma_mem_check_range()</code> 可以检查地址是否落在允许 DMA 的内存段，长度是否超过 Buffer 边界，地址是否按 32 字节对齐。这样问题会在开发阶段暴露，而不是在客户现场变成偶发死机。</p>
<h2 id="十三把经验落到团队规范里">十三、把经验落到团队规范里</h2>
<p>Cache 一致性不是某个驱动工程师的个人习惯，而应该进入团队编码规范。建议至少写下以下约定：</p>
<ul>
<li>所有 DMA Buffer 必须使用统一宏声明，不允许随手 <code>static uint8_t buf[1024]</code>；</li>
<li>所有 DMA 描述符必须放在 <code>.dma_desc</code>；</li>
<li>所有 DMA 数据池必须放在 <code>.dma_buffer</code> 或明确标注 Non-cacheable；</li>
<li>驱动提交 Buffer 前后必须通过 <code>dma_mem_*</code> 接口维护；</li>
<li>DMA 拥有 Buffer 期间 CPU 禁止访问，调试打印也不例外；</li>
<li>新增外设驱动时必须在评审中说明内存区域、方向、维护动作和对齐策略；</li>
<li>每次修改链接脚本、MPU 初始化、外部 RAM 配置后必须跑 DMA 回归测试。</li>
</ul>
<p>回归测试可以设计得很朴素：构造不同长度、不同偏移、不同填充值的 Buffer，让 DMA 做内存到内存搬运或外设环回；CPU 在前后计算 CRC；同时测试 1 字节、31 字节、32 字节、33 字节、1500 字节、4096 字节等边界长度。很多隐藏问题会在 31 / 33 字节这种非整 Cache Line 长度上暴露。</p>
<h2 id="十四总结高性能-mcu-要用-soc-思维来写">十四、总结：高性能 MCU 要用 SoC 思维来写</h2>
<p>Cortex-M7 这类高性能 MCU 仍然保留了单片机的开发体验，但它的内存系统已经接近小型 SoC：Cache、MPU、多总线、多 SRAM 域、外部 RAM、多个 DMA 主设备同时存在。如果继续用“所有地址都等价”的旧模型写代码，项目越接近量产，越容易遇到偶发、难复现、难定位的问题。</p>
<p>把系统跑稳的关键并不复杂：先画清楚 CPU、DMA 和内存之间的访问路径；用 MPU 固化内存属性；用链接脚本隔离描述符和数据池；按方向正确执行 Clean / Invalidate；把维护动作绑定到 Buffer 所有权转换点；最后用 DWT、CRC、边界长度测试验证性能和正确性。</p>
<p>经验上，最可靠的方案往往不是“全局关闭 Cache”，也不是“到处补一行 Invalidate”，而是工程化地管理内存。描述符小而关键，可以 Non-cacheable；大块数据需要吞吐，就让它 Cacheable，但必须有严格的对齐、所有权和维护封装。这样既能保住 Cortex-M7 的性能，也能让 ETH、SDMMC、DCMI、LTDC、DMA2D 这些高带宽外设稳定工作。</p>
<p>如果你正在做 STM32H7 摄像头网关、工业以太网节点、带屏 UI 控制器、音频采集设备或端侧 AI 推理板卡，建议尽早把本文这套规范放进工程模板。越早建立内存区域和 DMA Buffer 的纪律，后期排查“偶现脏数据”的时间就越少，系统的可维护性也会高很多。</p>
<h2 id="十五附录一套最小回归用例">十五、附录：一套最小回归用例</h2>
<p>最后给一个简单但很有效的回归思路。准备一块 4KB 的 DMA 测试区，分别从偏移 0、1、15、31、32、33 字节开始测试，再覆盖长度 1、16、31、32、33、127、256、1500 字节。每次测试前由 CPU 写入递增模式，Clean 后让 DMA 搬运到另一块区域；DMA 完成后对目标区 Invalidate，再由 CPU 计算 CRC。随后反过来，让 DMA 写入源区，CPU 在 Invalidate 后校验。这个测试不依赖复杂外设，很多芯片可以用内存到内存 DMA 完成，适合作为板级 bring-up 的第一项检查。</p>
<p>如果这套边界测试能在开启 D-Cache、最高优化等级、RTOS 任务切换和中断压力同时存在的情况下连续运行数小时，说明内存属性、链接脚本、对齐封装和维护时机基本可信。反之，只要 31 字节、33 字节或非零偏移用例失败，就不要急着怀疑协议栈，先回到 Cache Line、所有权和 MPU Region 这三件事上排查。</p>
<p>（全文完，约 8000 字）</p>
]]></content:encoded></item><item><title>嵌入式 NPU 架构与算子优化实战：从内存带宽到 INT8 部署</title><link>https://tech-snippets.xyz/posts/embedded-npu-architecture-operator-optimization-guide/</link><pubDate>Fri, 05 Jun 2026 19:00:00 +0800</pubDate><guid>https://tech-snippets.xyz/posts/embedded-npu-architecture-operator-optimization-guide/</guid><description>前言：为什么同一个模型在不同 NPU 上差距很大？ 做嵌入式 AI 部署时，很多人第一次拿到 NPU 板卡都会有一个误解：只要芯片宣传页写着 1TOPS、6TOPS 或 10TOPS，模型就应该按照这个数字线性变快。实际项目里经常不是这样。同样一个 YOLO、MobileNet 或语音关键词模型，在 A 芯片上跑得很顺，在 B 芯片上却卡在某几个算子；同样是 INT8 量化，有的模型精度几乎不掉，有的模型会出现明显误检；同样是官方转换工具，有的网络一键通过，有的网络需要反复改 ONNX 图、替换算子、拆分子图。
这些问题并不神秘，本质上是 NPU 的计算阵列、片上 SRAM、DMA、数据布局、编译器和运行时之间存在非常强的耦合。CPU 代码慢了，我们通常先看热点函数；GPU 程序慢了，会看 kernel occupancy、显存访问和线程块；NPU 部署慢了，也要有类似的分析框架：先判断瓶颈是算力、带宽、算子支持、量化误差，还是 CPU/NPU 之间的调度开销。
本文从工程视角拆解嵌入式 NPU 的典型架构，并围绕一个真实部署流程展开：模型导出、图优化、量化校准、算子映射、内存规划、运行时流水线和性能排查。文章不绑定某一家芯片，但会覆盖 RK、Amlogic、Kendryte、寒武纪边缘模块以及很多 MCU 级 NPU 都会遇到的共性问题。读完后，你应该能判断一个模型为什么没有跑满 NPU，也能知道该从哪里下手优化。
一、先把 TOPS 的含义说清楚 TOPS 是每秒万亿次操作数，通常用于描述 INT8 乘加能力。例如一个 2TOPS 的 NPU，理论上每秒可以完成 2 万亿次 8 bit 整数运算。问题在于，这个数字往往是理想条件下的峰值：输入输出都在合适的数据布局中，算子可以完全映射到矩阵乘阵列，片上缓存命中率足够高，DMA 搬运没有拖后腿，调度器没有频繁切换任务。
在实际模型里，真正能高效利用 NPU 的通常是卷积、深度卷积、全连接、矩阵乘、部分池化和激活函数。很多看起来不起眼的操作，例如 Reshape、Transpose、Slice、Gather、Resize、NonMaxSuppression，如果不能被 NPU 原生支持，就可能回退到 CPU。一次 CPU 回退不仅带来计算时间，还可能带来缓存同步、数据格式转换和内存拷贝。模型中只要有几个这样的“断点”，端到端延迟就会明显变差。
评估 NPU 时，比 TOPS 更有价值的是下面几个指标：</description><content:encoded><![CDATA[<h2 id="前言为什么同一个模型在不同-npu-上差距很大">前言：为什么同一个模型在不同 NPU 上差距很大？</h2>
<p>做嵌入式 AI 部署时，很多人第一次拿到 NPU 板卡都会有一个误解：只要芯片宣传页写着 1TOPS、6TOPS 或 10TOPS，模型就应该按照这个数字线性变快。实际项目里经常不是这样。同样一个 YOLO、MobileNet 或语音关键词模型，在 A 芯片上跑得很顺，在 B 芯片上却卡在某几个算子；同样是 INT8 量化，有的模型精度几乎不掉，有的模型会出现明显误检；同样是官方转换工具，有的网络一键通过，有的网络需要反复改 ONNX 图、替换算子、拆分子图。</p>
<p>这些问题并不神秘，本质上是 NPU 的计算阵列、片上 SRAM、DMA、数据布局、编译器和运行时之间存在非常强的耦合。CPU 代码慢了，我们通常先看热点函数；GPU 程序慢了，会看 kernel occupancy、显存访问和线程块；NPU 部署慢了，也要有类似的分析框架：先判断瓶颈是算力、带宽、算子支持、量化误差，还是 CPU/NPU 之间的调度开销。</p>
<p>本文从工程视角拆解嵌入式 NPU 的典型架构，并围绕一个真实部署流程展开：模型导出、图优化、量化校准、算子映射、内存规划、运行时流水线和性能排查。文章不绑定某一家芯片，但会覆盖 RK、Amlogic、Kendryte、寒武纪边缘模块以及很多 MCU 级 NPU 都会遇到的共性问题。读完后，你应该能判断一个模型为什么没有跑满 NPU，也能知道该从哪里下手优化。</p>
<p><img alt="嵌入式 NPU 算子执行流水线" loading="lazy" src="/images/embedded-npu-operator-pipeline.svg"></p>
<h2 id="一先把-tops-的含义说清楚">一、先把 TOPS 的含义说清楚</h2>
<p>TOPS 是每秒万亿次操作数，通常用于描述 INT8 乘加能力。例如一个 2TOPS 的 NPU，理论上每秒可以完成 2 万亿次 8 bit 整数运算。问题在于，这个数字往往是理想条件下的峰值：输入输出都在合适的数据布局中，算子可以完全映射到矩阵乘阵列，片上缓存命中率足够高，DMA 搬运没有拖后腿，调度器没有频繁切换任务。</p>
<p>在实际模型里，真正能高效利用 NPU 的通常是卷积、深度卷积、全连接、矩阵乘、部分池化和激活函数。很多看起来不起眼的操作，例如 <code>Reshape</code>、<code>Transpose</code>、<code>Slice</code>、<code>Gather</code>、<code>Resize</code>、<code>NonMaxSuppression</code>，如果不能被 NPU 原生支持，就可能回退到 CPU。一次 CPU 回退不仅带来计算时间，还可能带来缓存同步、数据格式转换和内存拷贝。模型中只要有几个这样的“断点”，端到端延迟就会明显变差。</p>
<p>评估 NPU 时，比 TOPS 更有价值的是下面几个指标：</p>
<ol>
<li><strong>端到端延迟</strong>：从图像采集或音频帧输入，到最终结果输出的总耗时。</li>
<li><strong>NPU 子图覆盖率</strong>：模型中有多少算子真正被 NPU 执行。</li>
<li><strong>DDR 带宽占用</strong>：输入、输出、中间特征图是否频繁进出外部内存。</li>
<li><strong>Batch 与分辨率敏感性</strong>：嵌入式场景多为 batch=1，很多服务端优化不适用。</li>
<li><strong>量化后精度</strong>：INT8 的 mAP、Top-1、误唤醒率是否满足业务要求。</li>
<li><strong>功耗与温升</strong>：持续推理 30 分钟后频率是否降档。</li>
</ol>
<p>如果只看峰值 TOPS，很容易把问题归因到“芯片不行”。但很多时候，真正的问题是模型图不适合该 NPU，或者预处理和后处理拖慢了整条流水线。</p>
<h2 id="二嵌入式-npu-的典型硬件结构">二、嵌入式 NPU 的典型硬件结构</h2>
<p>不同厂商的实现细节不同，但嵌入式 NPU 通常可以抽象成几个模块：MAC 阵列、片上 SRAM、DMA 控制器、指令调度器、数据重排单元和外部 DDR 接口。</p>
<p>MAC 阵列负责核心乘加。卷积在编译阶段会被转换为矩阵乘或滑窗乘加任务，再切成多个 tile 放入阵列。片上 SRAM 保存权重块、输入块和输出块，避免每次乘加都访问 DDR。DMA 负责在 DDR 和 SRAM 之间搬运数据。数据重排单元负责处理 NCHW、NHWC、NC1HWC2 等布局转换。指令调度器则把编译器生成的命令流按照依赖关系送入硬件。</p>
<p>一个简化的卷积执行过程如下：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">DDR 中的输入特征图 -&gt; DMA 搬入片上 SRAM
</span></span><span class="line"><span class="cl">DDR 中的权重块     -&gt; DMA 搬入片上 SRAM
</span></span><span class="line"><span class="cl">MAC 阵列执行 tile 卷积
</span></span><span class="line"><span class="cl">局部输出写回 SRAM
</span></span><span class="line"><span class="cl">必要时做激活/量化/累加
</span></span><span class="line"><span class="cl">最终输出通过 DMA 写回 DDR
</span></span></code></pre></div><p>这里的关键是“tile”。片上 SRAM 容量有限，不可能一次放下完整的高分辨率特征图和全部权重。编译器需要根据 SRAM 大小、阵列形状、数据类型和算子参数，把一个大算子切成许多小块。tile 切得太小，DMA 和调度开销变大；tile 切得太大，SRAM 放不下或复用率下降。很多 NPU 的性能差异，表面看是 TOPS 不同，深层其实是 tile 策略、数据布局和内存层次做得好不好。</p>
<h2 id="三从模型图看-npu-友好程度">三、从模型图看 NPU 友好程度</h2>
<p>在部署前，建议先用 Netron、ONNX GraphSurgeon 或厂商工具查看模型图。一个 NPU 友好的模型通常具备这些特征：主干网络由 Conv、BN、ReLU/SiLU、Pooling、MatMul 等常见算子组成；分支结构不太复杂；动态 shape 很少；后处理可以拆到 CPU，并且数据量已经足够小；输入分辨率固定；没有大量 <code>Transpose</code> 和 <code>Gather</code>。</p>
<p>以目标检测模型为例，主干和 neck 往往很好映射到 NPU，但 decode 和 NMS 经常是麻烦点。很多模型导出 ONNX 后，会把网格生成、坐标变换、阈值过滤和 NMS 都留在图里。这样虽然在 PC 上用 ONNX Runtime 很方便，但在嵌入式 NPU 上可能导致大量 CPU 回退。更稳妥的做法是让 NPU 输出三个尺度的特征图，后处理在 C/C++ 中单独实现。</p>
<p>另一个常见问题是激活函数。传统 ReLU 对 NPU 友好，但 Swish、GELU、HardSwish 的支持情况因芯片而异。有些 NPU 可以融合 <code>Conv + BN + ReLU</code>，但不能很好地融合 <code>Conv + BN + SiLU</code>。如果模型允许，训练阶段就应考虑部署端约束，而不是等模型训练完再强行适配。</p>
<p>（第一部分完，约2300字）</p>
<h2 id="四模型转换前的图优化少一个-transpose-就少一次搬运">四、模型转换前的图优化：少一个 Transpose 就少一次搬运</h2>
<p>NPU 编译器通常会做常量折叠、算子融合、死节点删除和布局传播，但不要指望它解决所有问题。工程上更可靠的做法，是在导出 ONNX 后主动清理模型图。尤其是 PyTorch 导出的图，经常会因为框架表达方式留下多余的 <code>Unsqueeze</code>、<code>Concat</code>、<code>Slice</code>、<code>Transpose</code>，这些节点在桌面端不明显，在 NPU 上却可能成为性能断点。</p>
<p>下面是一个简单的 ONNX 检查脚本，用来统计算子类型和可疑节点：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">onnx</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">collections</span> <span class="kn">import</span> <span class="n">Counter</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">model</span> <span class="o">=</span> <span class="n">onnx</span><span class="o">.</span><span class="n">load</span><span class="p">(</span><span class="s2">&#34;model.onnx&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">ops</span> <span class="o">=</span> <span class="n">Counter</span><span class="p">(</span><span class="n">node</span><span class="o">.</span><span class="n">op_type</span> <span class="k">for</span> <span class="n">node</span> <span class="ow">in</span> <span class="n">model</span><span class="o">.</span><span class="n">graph</span><span class="o">.</span><span class="n">node</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">op</span><span class="p">,</span> <span class="n">count</span> <span class="ow">in</span> <span class="n">ops</span><span class="o">.</span><span class="n">most_common</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">op</span><span class="si">:</span><span class="s2">20s</span><span class="si">}</span><span class="s2"> </span><span class="si">{</span><span class="n">count</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">suspect</span> <span class="o">=</span> <span class="p">{</span><span class="s2">&#34;Transpose&#34;</span><span class="p">,</span> <span class="s2">&#34;Gather&#34;</span><span class="p">,</span> <span class="s2">&#34;Slice&#34;</span><span class="p">,</span> <span class="s2">&#34;Resize&#34;</span><span class="p">,</span> <span class="s2">&#34;NonMaxSuppression&#34;</span><span class="p">,</span> <span class="s2">&#34;Shape&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="s2">&#34;</span><span class="se">\n</span><span class="s2">可疑算子：&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">node</span> <span class="ow">in</span> <span class="n">model</span><span class="o">.</span><span class="n">graph</span><span class="o">.</span><span class="n">node</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">node</span><span class="o">.</span><span class="n">op_type</span> <span class="ow">in</span> <span class="n">suspect</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="n">node</span><span class="o">.</span><span class="n">name</span><span class="p">,</span> <span class="n">node</span><span class="o">.</span><span class="n">op_type</span><span class="p">,</span> <span class="p">[</span><span class="n">i</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="n">node</span><span class="o">.</span><span class="n">input</span><span class="p">],</span> <span class="s2">&#34;-&gt;&#34;</span><span class="p">,</span> <span class="p">[</span><span class="n">o</span> <span class="k">for</span> <span class="n">o</span> <span class="ow">in</span> <span class="n">node</span><span class="o">.</span><span class="n">output</span><span class="p">])</span>
</span></span></code></pre></div><p>如果发现 <code>Transpose</code> 数量很多，要进一步看它们是否只是为了在 NCHW 和 NHWC 之间来回切换。有些转换工具要求固定输入布局，导出时如果设置错了，就会在图首和图尾插入额外重排。重排操作本身不做复杂计算，却会读写完整特征图；对于 640×640 的检测模型，中间层特征图可能有数 MB，几次重排就足以吞掉大量带宽。</p>
<p>图优化的基本原则有三条：</p>
<ul>
<li><strong>能融合就融合</strong>：Conv + BN + Activation 应尽量在转换前或编译器中融合。</li>
<li><strong>能静态就静态</strong>：固定输入尺寸、固定 batch，避免动态 shape 进入 NPU 子图。</li>
<li><strong>能移出就移出</strong>：NMS、字符串处理、复杂索引等不适合 NPU 的逻辑放到 CPU 后处理。</li>
</ul>
<p>以 BatchNorm 融合为例，推理阶段 BN 可以合并进卷积权重和偏置。公式并不复杂：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># W, b: 原卷积权重和偏置</span>
</span></span><span class="line"><span class="cl"><span class="c1"># mean, var, gamma, beta: BN 参数</span>
</span></span><span class="line"><span class="cl"><span class="c1"># eps: BN epsilon</span>
</span></span><span class="line"><span class="cl"><span class="n">scale</span> <span class="o">=</span> <span class="n">gamma</span> <span class="o">/</span> <span class="n">np</span><span class="o">.</span><span class="n">sqrt</span><span class="p">(</span><span class="n">var</span> <span class="o">+</span> <span class="n">eps</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">W_fused</span> <span class="o">=</span> <span class="n">W</span> <span class="o">*</span> <span class="n">scale</span><span class="o">.</span><span class="n">reshape</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">b_fused</span> <span class="o">=</span> <span class="n">beta</span> <span class="o">+</span> <span class="p">(</span><span class="n">b</span> <span class="o">-</span> <span class="n">mean</span><span class="p">)</span> <span class="o">*</span> <span class="n">scale</span>
</span></span></code></pre></div><p>融合后，运行时少了一个 BN 算子，也减少了一次中间特征图读写。对大模型来说，这种看似基础的优化非常重要，因为嵌入式推理经常不是算力不够，而是内存访问太贵。</p>
<h2 id="五int8-量化性能提升背后的精度账">五、INT8 量化：性能提升背后的精度账</h2>
<p>多数嵌入式 NPU 的高性能路径是 INT8。FP16 或 BF16 支持正在变多，但在低功耗设备上，INT8 仍是性价比最高的部署方式。量化的核心是把浮点张量映射到整数范围：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">real_value ≈ scale × (int_value - zero_point)
</span></span></code></pre></div><p>对于权重量化，常见做法是 per-channel；对于激活量化，常见做法是 per-tensor。per-channel 能显著改善卷积权重量化精度，因为不同输出通道的权重分布可能差异很大。激活则依赖校准数据集，用一批代表性输入统计每层的数值范围。</p>
<p>校准集不是随便找几十张图就行。它应该覆盖真实场景中的亮度、角度、背景、目标尺度和噪声。如果是工业检测，要包含正常品、轻微缺陷、严重缺陷和空载图；如果是语音唤醒，要覆盖不同说话人、距离、噪声和麦克风增益。校准集偏了，量化 scale 就会偏，最终表现为某些场景下误差突然变大。</p>
<p>下面是一个用于挑选图像校准集的小脚本思路：从原始数据中按亮度和边缘密度分桶，避免校准样本都集中在同一种光照条件。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">cv2</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="nn">np</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">pathlib</span> <span class="kn">import</span> <span class="n">Path</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">items</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">Path</span><span class="p">(</span><span class="s2">&#34;calib_raw&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">glob</span><span class="p">(</span><span class="s2">&#34;*.jpg&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">img</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">imread</span><span class="p">(</span><span class="nb">str</span><span class="p">(</span><span class="n">p</span><span class="p">),</span> <span class="n">cv2</span><span class="o">.</span><span class="n">IMREAD_GRAYSCALE</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">img</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">continue</span>
</span></span><span class="line"><span class="cl">    <span class="n">brightness</span> <span class="o">=</span> <span class="nb">float</span><span class="p">(</span><span class="n">img</span><span class="o">.</span><span class="n">mean</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">    <span class="n">edges</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">Canny</span><span class="p">(</span><span class="n">img</span><span class="p">,</span> <span class="mi">80</span><span class="p">,</span> <span class="mi">160</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">edge_density</span> <span class="o">=</span> <span class="nb">float</span><span class="p">((</span><span class="n">edges</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span><span class="o">.</span><span class="n">mean</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">    <span class="n">items</span><span class="o">.</span><span class="n">append</span><span class="p">((</span><span class="n">p</span><span class="p">,</span> <span class="n">brightness</span><span class="p">,</span> <span class="n">edge_density</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 简单分桶：亮度 5 桶，纹理 5 桶，每桶最多取 4 张</span>
</span></span><span class="line"><span class="cl"><span class="n">selected</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl"><span class="n">buckets</span> <span class="o">=</span> <span class="p">{}</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">p</span><span class="p">,</span> <span class="n">b</span><span class="p">,</span> <span class="n">e</span> <span class="ow">in</span> <span class="n">items</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">key</span> <span class="o">=</span> <span class="p">(</span><span class="nb">min</span><span class="p">(</span><span class="nb">int</span><span class="p">(</span><span class="n">b</span> <span class="o">/</span> <span class="mi">51</span><span class="p">),</span> <span class="mi">4</span><span class="p">),</span> <span class="nb">min</span><span class="p">(</span><span class="nb">int</span><span class="p">(</span><span class="n">e</span> <span class="o">/</span> <span class="mf">0.04</span><span class="p">),</span> <span class="mi">4</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="n">buckets</span><span class="o">.</span><span class="n">setdefault</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="p">[])</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">p</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">key</span><span class="p">,</span> <span class="n">paths</span> <span class="ow">in</span> <span class="n">buckets</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">selected</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">paths</span><span class="p">[:</span><span class="mi">4</span><span class="p">])</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="s2">&#34;selected&#34;</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">selected</span><span class="p">))</span>
</span></span></code></pre></div><p>量化后一定要做逐层对比。只看最终 mAP 或准确率，很难定位问题。更有效的方式是让浮点模型和 INT8 模型输出若干关键层，计算余弦相似度、均方误差和最大误差。如果某一层开始相似度骤降，就重点检查该层前后的激活范围、是否有异常 outlier、是否有不适合量化的操作。</p>
<h2 id="六算子映射让模型顺着硬件走">六、算子映射：让模型顺着硬件走</h2>
<p>NPU 编译器会把模型图拆成一个或多个子图。连续可支持的算子会进入 NPU 子图，不支持的算子留给 CPU 或 DSP。性能优化的目标，就是让大的计算段尽量连续地留在 NPU 上。</p>
<p>举个常见例子：<code>Conv -&gt; BN -&gt; SiLU -&gt; Add</code>。如果某个 NPU 不支持 SiLU 融合，编译器可能把 <code>Conv + BN</code> 放进 NPU，把 SiLU 放到 CPU，再把 Add 放回 NPU。这样中间特征图要从 NPU 内存写到 DDR，再由 CPU 读取处理，然后又写回给 NPU，代价非常高。此时可以考虑把 SiLU 替换为 HardSwish 或 ReLU6，或者在训练时使用部署端支持更好的激活函数。</p>
<p>再比如深度可分离卷积。它理论上计算量低，但对某些 NPU 并不一定更快，因为 depthwise conv 的数据复用率不如普通卷积，容易变成带宽瓶颈。移动端网络里常见的 1×1 pointwise conv 反而更容易跑满矩阵阵列。所以模型结构选择不能只看 FLOPs，还要结合目标硬件的算子效率表。</p>
<p>实践中可以建立一张“算子白名单”：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">preferred_ops</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">Conv</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">Relu</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">LeakyRelu</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">Add</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">Mul</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">MaxPool</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">AveragePool</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">GlobalAveragePool</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">MatMul</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">avoid_ops</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">NonMaxSuppression</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">DynamicQuantizeLinear</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">GatherND</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">GridSample</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">Resize(mode=cubic)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">Transpose(large_feature_map)</span><span class="w">
</span></span></span></code></pre></div><p>这张表不是固定的，要根据具体芯片 SDK 更新。每次 SDK 升级后，都建议重新跑一遍模型转换报告，看看原本回退 CPU 的节点是否已经支持，或者原本支持的融合是否发生变化。</p>
<p>（第二部分完，约2500字）</p>
<h2 id="七内存规划嵌入式-npu-最容易被低估的战场">七、内存规划：嵌入式 NPU 最容易被低估的战场</h2>
<p>很多模型理论计算量不大，却在板子上跑不快，根因是内存。嵌入式 SoC 的 DDR 同时服务 CPU、GPU、ISP、VPU、NPU 和显示控制器。摄像头预览、视频编码、神经网络推理如果同时进行，DDR 带宽会被迅速吃满。NPU 峰值算力再高，数据喂不上去也只能空转。</p>
<p>优化内存时，先看三类数据：输入输出、中间特征图、权重。权重通常可以常驻内存，有些平台还能预加载到专用区域；输入输出与业务流水线有关，例如摄像头 NV12 数据是否需要转 RGB、是否需要 resize、是否能使用 zero-copy；中间特征图由编译器规划，但模型结构会影响峰值内存。</p>
<p>对视觉模型来说，预处理经常被忽略。一个 1080p 摄像头输入，如果每帧都由 CPU 做 NV12 到 RGB、resize、归一化，再拷贝到 NPU 输入缓冲区，可能预处理就花掉 8 到 15ms。更好的方式是使用 ISP/RGA/GPU 做颜色转换和缩放，或者让 NPU runtime 接受硬件缓冲区句柄，减少内存拷贝。</p>
<p>典型的零拷贝流水线如下：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Camera DMA buffer
</span></span><span class="line"><span class="cl">  -&gt; 硬件缩放/颜色转换
</span></span><span class="line"><span class="cl">  -&gt; NPU 输入 buffer
</span></span><span class="line"><span class="cl">  -&gt; NPU 推理
</span></span><span class="line"><span class="cl">  -&gt; CPU 读取小尺寸输出
</span></span><span class="line"><span class="cl">  -&gt; 后处理与业务逻辑
</span></span></code></pre></div><p>如果平台支持 ION、DMA-BUF 或类似机制，尽量让视频帧在硬件模块之间传递句柄，而不是在用户态反复 <code>memcpy</code>。这类优化写起来不如改模型显眼，但对端到端延迟和功耗非常有效。</p>
<h2 id="八运行时流水线不要让-npu-等-cpu">八、运行时流水线：不要让 NPU 等 CPU</h2>
<p>单帧推理流程通常包括采集、预处理、推理、后处理和显示/通信。如果这些步骤串行执行，总延迟是所有步骤相加。实际产品中可以用流水线并行：CPU 后处理第 N 帧时，NPU 推理第 N+1 帧，ISP 准备第 N+2 帧。这样单帧延迟没有消失，但系统吞吐会明显提升。</p>
<p>下面是一个简化的 C++ 伪代码，展示三线程流水线的结构：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-cpp" data-lang="cpp"><span class="line"><span class="cl"><span class="k">struct</span> <span class="nc">FrameJob</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kt">int</span> <span class="n">id</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">Buffer</span> <span class="n">input</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">Buffer</span> <span class="n">tensor</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">NpuOutput</span> <span class="n">output</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">BlockingQueue</span><span class="o">&lt;</span><span class="n">FrameJob</span><span class="o">&gt;</span> <span class="n">q_pre</span><span class="p">,</span> <span class="n">q_infer</span><span class="p">,</span> <span class="n">q_post</span><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">capture_thread</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kt">int</span> <span class="n">id</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">while</span> <span class="p">(</span><span class="n">running</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">FrameJob</span> <span class="n">job</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="n">job</span><span class="p">.</span><span class="n">id</span> <span class="o">=</span> <span class="n">id</span><span class="o">++</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="n">job</span><span class="p">.</span><span class="n">input</span> <span class="o">=</span> <span class="n">camera_dequeue</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">        <span class="n">q_pre</span><span class="p">.</span><span class="n">push</span><span class="p">(</span><span class="n">job</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="kt">void</span> <span class="nf">preprocess_thread</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">while</span> <span class="p">(</span><span class="n">running</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">auto</span> <span class="n">job</span> <span class="o">=</span> <span class="n">q_pre</span><span class="p">.</span><span class="n">pop</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">        <span class="n">job</span><span class="p">.</span><span class="n">tensor</span> <span class="o">=</span> <span class="n">hw_resize_color_convert</span><span class="p">(</span><span class="n">job</span><span class="p">.</span><span class="n">input</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="n">q_infer</span><span class="p">.</span><span class="n">push</span><span class="p">(</span><span class="n">job</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="kt">void</span> <span class="nf">infer_thread</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">while</span> <span class="p">(</span><span class="n">running</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">auto</span> <span class="n">job</span> <span class="o">=</span> <span class="n">q_infer</span><span class="p">.</span><span class="n">pop</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">        <span class="n">job</span><span class="p">.</span><span class="n">output</span> <span class="o">=</span> <span class="n">npu_run</span><span class="p">(</span><span class="n">job</span><span class="p">.</span><span class="n">tensor</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="n">q_post</span><span class="p">.</span><span class="n">push</span><span class="p">(</span><span class="n">job</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="kt">void</span> <span class="nf">postprocess_thread</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">while</span> <span class="p">(</span><span class="n">running</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">auto</span> <span class="n">job</span> <span class="o">=</span> <span class="n">q_post</span><span class="p">.</span><span class="n">pop</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">        <span class="k">auto</span> <span class="n">result</span> <span class="o">=</span> <span class="n">decode_and_nms</span><span class="p">(</span><span class="n">job</span><span class="p">.</span><span class="n">output</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="n">publish_result</span><span class="p">(</span><span class="n">job</span><span class="p">.</span><span class="n">id</span><span class="p">,</span> <span class="n">result</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="n">camera_release</span><span class="p">(</span><span class="n">job</span><span class="p">.</span><span class="n">input</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>这里有几个工程细节：队列长度不能无限增长，否则实时系统会堆积旧帧；如果业务只关心最新结果，可以在队列满时丢弃旧帧；NPU 输入输出 buffer 应该复用，避免每帧申请释放；多线程之间要注意 cache flush/invalidate，特别是 CPU 和 NPU 共享物理内存时。</p>
<h2 id="九性能排查按层耗时比总耗时更重要">九、性能排查：按层耗时比总耗时更重要</h2>
<p>当模型跑得慢时，不要先猜。第一步是拿到转换报告和 profiler。大多数 NPU SDK 都能输出每层耗时、子图划分、内存使用和 CPU fallback 信息。如果工具不够完善，也可以在业务侧用时间戳包住预处理、推理和后处理，至少先拆出大方向。</p>
<p>建议建立如下排查顺序：</p>
<ol>
<li><strong>确认频率和温度</strong>：是否因为供电、散热或 governor 导致降频。</li>
<li><strong>确认模型输入尺寸</strong>：是否误用了更大分辨率或动态 shape。</li>
<li><strong>查看 NPU 子图覆盖率</strong>：是否存在 CPU fallback。</li>
<li><strong>查看最慢的前 10 层</strong>：是大卷积、Resize、Transpose，还是后处理。</li>
<li><strong>检查预处理耗时</strong>：是否被 CPU resize 和 memcpy 拖慢。</li>
<li><strong>检查量化模式</strong>：是否有部分层保持 FP32，导致混合执行成本增加。</li>
<li><strong>对比官方 benchmark</strong>：用厂商示例确认环境没有系统性问题。</li>
</ol>
<p>如果某一层卷积异常慢，可以尝试改变输入尺寸、通道数或模型结构。NPU 对通道对齐很敏感，有些硬件喜欢通道数是 8、16 或 32 的倍数。如果网络中出现 3、5、7 这类不规则通道，编译器可能需要 padding，导致实际计算和内存都增加。训练模型时适当让通道数对齐硬件粒度，通常比部署后硬优化更划算。</p>
<h2 id="十一个可落地的部署检查清单">十、一个可落地的部署检查清单</h2>
<p>下面这份清单适合放进项目的 <code>docs/deployment.md</code>，每次模型升级时按步骤检查：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">[模型导出]
</span></span><span class="line"><span class="cl">- 固定 input shape 和 batch=1
</span></span><span class="line"><span class="cl">- 关闭训练态节点，确认 BN 已进入 eval 模式
</span></span><span class="line"><span class="cl">- 导出 ONNX 后用 onnx.checker 校验
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">[图优化]
</span></span><span class="line"><span class="cl">- 统计算子类型，标记 Transpose / Gather / Slice / Resize
</span></span><span class="line"><span class="cl">- 融合 Conv + BN + Activation
</span></span><span class="line"><span class="cl">- 将 NMS、decode 等后处理移出 NPU 图
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">[量化]
</span></span><span class="line"><span class="cl">- 校准集覆盖真实场景
</span></span><span class="line"><span class="cl">- 使用 per-channel weight quantization
</span></span><span class="line"><span class="cl">- 做逐层余弦相似度对比
</span></span><span class="line"><span class="cl">- 对关键指标做回归测试
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">[编译]
</span></span><span class="line"><span class="cl">- 查看 NPU 子图覆盖率
</span></span><span class="line"><span class="cl">- 保存编译报告和 SDK 版本
</span></span><span class="line"><span class="cl">- 记录输入布局、量化参数和 runtime 配置
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">[运行]
</span></span><span class="line"><span class="cl">- 预分配输入输出 buffer
</span></span><span class="line"><span class="cl">- 使用硬件预处理或 zero-copy
</span></span><span class="line"><span class="cl">- 分离采集、推理、后处理线程
</span></span><span class="line"><span class="cl">- 记录端到端延迟、P95、P99 和温度
</span></span></code></pre></div><p>这份清单的价值在于让部署过程可复现。很多团队的问题不是没有优化能力，而是每次模型、SDK、板级系统一起变化，最后不知道性能变化来自哪里。只要把转换命令、校准数据版本、编译报告和测试结果保存下来，排查效率会高很多。</p>
<h2 id="十一常见坑与解决办法">十一、常见坑与解决办法</h2>
<p><strong>1. PC 上 ONNX 推理正确，NPU 结果明显偏移。</strong> 先检查输入归一化顺序、RGB/BGR、NHWC/NCHW 和量化 scale。很多精度问题并不是 NPU 算错，而是预处理和训练时不一致。</p>
<p><strong>2. 转换工具提示某个算子不支持。</strong> 不要急着换芯片，先看该算子是否可以常量折叠、替换或移到后处理。比如检测模型里的 <code>NonMaxSuppression</code> 通常没必要放在 NPU 图里。</p>
<p><strong>3. INT8 精度下降很大。</strong> 增加校准集多样性，检查异常激活层，尝试 per-channel、混合精度或量化感知训练。对于检测头、注意力模块等敏感部分，可以保留 FP16，前提是硬件支持且性能可接受。</p>
<p><strong>4. 单独 benchmark 很快，业务程序很慢。</strong> 重点看预处理、后处理、线程同步、日志打印和内存拷贝。厂商 benchmark 往往只测 NPU 子图，不包含摄像头和业务逻辑。</p>
<p><strong>5. 长时间运行后变慢。</strong> 检查温度、供电、内存泄漏和 buffer 队列堆积。嵌入式设备的持续性能比冷启动成绩更重要。</p>
<h2 id="十二总结npu-优化是模型编译器和系统工程的合题">十二、总结：NPU 优化是模型、编译器和系统工程的合题</h2>
<p>嵌入式 NPU 部署不是把模型丢进转换工具那么简单。一个稳定高效的方案，需要模型结构顺着硬件走，量化校准贴近真实数据，编译报告能解释每个子图，运行时流水线减少等待，系统层面控制带宽、功耗和温度。</p>
<p>如果把经验压缩成一句话：先保证算子连续落在 NPU 上，再减少大特征图搬运，最后才追求单个算子的极限优化。很多项目真正的性能提升，来自删掉几个多余的 <code>Transpose</code>、把 NMS 移出图、使用硬件 resize、复用 DMA buffer，而不是盲目追逐更大的 TOPS。</p>
<p>面向未来，边缘 NPU 会继续向更强的混合精度、更好的 Transformer 支持和更完善的软件栈发展。但在可预见的几年里，嵌入式工程师仍然需要理解硬件约束。懂模型的人写出的网络更容易部署，懂系统的人能把 NPU 放进真实产品流水线，懂架构的人则能在性能、功耗和成本之间做出正确取舍。</p>
<p>（全文完，约7200字）</p>
]]></content:encoded></item><item><title>DDR 内存带宽调优实战：从 AXI 总线到 Cache Miss 的 SoC 性能优化指南</title><link>https://tech-snippets.xyz/posts/ddr-memory-controller-bandwidth-optimization-guide/</link><pubDate>Mon, 01 Jun 2026 19:00:00 +0800</pubDate><guid>https://tech-snippets.xyz/posts/ddr-memory-controller-bandwidth-optimization-guide/</guid><description>前言 做嵌入式 Linux 或边缘 AI 项目时，很多性能问题最后都会绕回一个朴素但容易被低估的事实：算力不等于吞吐，CPU、NPU、GPU 跑得再快，只要数据喂不上去，整机性能就会被内存系统卡住。
我第一次真正意识到 DDR 带宽的重要性，是在一块多核 ARM SoC 上做 4 路摄像头视频分析。算法同事看 NPU 利用率只有 40% 左右，以为模型还可以继续加大；系统同事看 CPU 使用率也不高，以为瓶颈不在软件。直到我们把 ISP、RGA、NPU、VPU 同时压起来，再去读 DDR 控制器计数器，才发现内存读写已经接近平台可持续带宽的上限。那一刻，所谓“还有很多算力没用上”，其实只是“大家都在等内存”。
这篇文章想把这个问题讲透一点：DDR 带宽不是一个孤立参数，它贯穿了 CPU Cache、AXI/NoC 互联、DMA burst、内存控制器调度、DRAM Bank 冲突、刷新开销以及 Linux 调度策略。很多项目里大家会直接跑一个 memcpy 或 stream，看到数字不错就认为内存没问题；但真实业务往往不是连续大块搬运，而是多个主设备同时访问、读写混合、缓存命中率波动、实时任务和后台任务互相抢总线。
本文会从 SoC 视角出发，拆解一条内存访问路径，并给出一套可以落地的排查和优化方法。示例代码以 Linux 用户态为主，兼顾裸机/RTOS 下的思路。目标不是把每个 DDR 时序参数都背下来，而是建立一个工程上有用的判断框架：什么时候该看 Cache Miss，什么时候该看 AXI outstanding，什么时候该怀疑 DDR controller 的 page policy，什么时候该从数据布局和 DMA burst 入手。
一、先把“带宽”这件事说清楚 DDR 厂商手册里常见的理论带宽计算很简单：
理论带宽 = 数据总线宽度 / 8 × 数据传输速率 例如 32-bit LPDDR4X，数据速率 4266 MT/s，理论峰值约为：</description><content:encoded><![CDATA[<h2 id="前言">前言</h2>
<p>做嵌入式 Linux 或边缘 AI 项目时，很多性能问题最后都会绕回一个朴素但容易被低估的事实：算力不等于吞吐，CPU、NPU、GPU 跑得再快，只要数据喂不上去，整机性能就会被内存系统卡住。</p>
<p>我第一次真正意识到 DDR 带宽的重要性，是在一块多核 ARM SoC 上做 4 路摄像头视频分析。算法同事看 NPU 利用率只有 40% 左右，以为模型还可以继续加大；系统同事看 CPU 使用率也不高，以为瓶颈不在软件。直到我们把 ISP、RGA、NPU、VPU 同时压起来，再去读 DDR 控制器计数器，才发现内存读写已经接近平台可持续带宽的上限。那一刻，所谓“还有很多算力没用上”，其实只是“大家都在等内存”。</p>
<p>这篇文章想把这个问题讲透一点：DDR 带宽不是一个孤立参数，它贯穿了 CPU Cache、AXI/NoC 互联、DMA burst、内存控制器调度、DRAM Bank 冲突、刷新开销以及 Linux 调度策略。很多项目里大家会直接跑一个 <code>memcpy</code> 或 <code>stream</code>，看到数字不错就认为内存没问题；但真实业务往往不是连续大块搬运，而是多个主设备同时访问、读写混合、缓存命中率波动、实时任务和后台任务互相抢总线。</p>
<p>本文会从 SoC 视角出发，拆解一条内存访问路径，并给出一套可以落地的排查和优化方法。示例代码以 Linux 用户态为主，兼顾裸机/RTOS 下的思路。目标不是把每个 DDR 时序参数都背下来，而是建立一个工程上有用的判断框架：什么时候该看 Cache Miss，什么时候该看 AXI outstanding，什么时候该怀疑 DDR controller 的 page policy，什么时候该从数据布局和 DMA burst 入手。</p>
<p><img alt="SoC DDR 带宽路径与调优观测点" loading="lazy" src="/images/ddr-memory-bandwidth-architecture.svg"></p>
<h2 id="一先把带宽这件事说清楚">一、先把“带宽”这件事说清楚</h2>
<p>DDR 厂商手册里常见的理论带宽计算很简单：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">理论带宽 = 数据总线宽度 / 8 × 数据传输速率
</span></span></code></pre></div><p>例如 32-bit LPDDR4X，数据速率 4266 MT/s，理论峰值约为：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">32 / 8 × 4266 = 17064 MB/s ≈ 17 GB/s
</span></span></code></pre></div><p>这个数字看起来很漂亮，但工程上最容易踩的坑，就是把理论峰值当作业务可用带宽。实际系统里至少有几类损耗：</p>
<ol>
<li><strong>协议和控制开销</strong>：DRAM 不是一个无限快的 SRAM，行打开、预充电、刷新、读写切换都会消耗周期。</li>
<li><strong>访问模式损耗</strong>：连续访问和随机访问差别巨大，同一 Row 命中和频繁换 Row 的效率完全不同。</li>
<li><strong>多主设备竞争</strong>：CPU、GPU、NPU、ISP、VPU、显示控制器、PCIe、USB 都可能通过 AXI/NoC 访问 DDR。</li>
<li><strong>Cache 行粒度放大</strong>：CPU 读一个 4 字节整数，如果它不在 Cache 里，通常会拉回一整条 Cache Line。</li>
<li><strong>软件栈额外拷贝</strong>：视频帧、网络包、AI tensor 如果在多个模块之间来回复制，带宽会被悄悄吃掉。</li>
</ol>
<p>所以我更喜欢把带宽分成三个层次：</p>
<ul>
<li><strong>理论峰值带宽</strong>：由 DDR 类型、频率、位宽决定，用于判断上限。</li>
<li><strong>平台可持续带宽</strong>：在稳定温度、电压、频率下，通过基准测试长期跑出来的数字。</li>
<li><strong>业务有效带宽</strong>：真实业务中，真正转化为有效计算的数据吞吐。</li>
</ul>
<p>优化时最重要的是第三个。一个平台跑 STREAM 能到 12 GB/s，不代表你的视觉 pipeline 就能用到 12 GB/s。如果算法访问模式很差，或者多媒体 DMA 和 CPU 同时抢总线，业务有效带宽可能只有几 GB/s，甚至更低。</p>
<h2 id="二一次内存访问到底经过了哪里">二、一次内存访问到底经过了哪里</h2>
<p>从 CPU 角度看，一行 C 代码可能只是：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="n">sum</span> <span class="o">+=</span> <span class="n">buffer</span><span class="p">[</span><span class="n">i</span><span class="p">];</span>
</span></span></code></pre></div><p>但在 SoC 内部，这次读取可能经历下面的路径：</p>
<ol>
<li>CPU 先查 L1 Data Cache；</li>
<li>L1 miss 后查 L2；</li>
<li>L2 miss 后查 LLC 或系统级缓存；</li>
<li>仍然 miss，就通过 ACE/CHI/AXI 接口发起读事务；</li>
<li>请求进入 NoC 或 AXI interconnect，和其他 master 仲裁；</li>
<li>DDR controller 接收请求，决定访问哪个 channel、rank、bank、row；</li>
<li>如果目标 row 已打开，直接读；否则需要 precharge/activate；</li>
<li>数据经过 PHY 回来，再沿互联返回 CPU；</li>
<li>Cache line 被填入，CPU 才能继续执行依赖这份数据的指令。</li>
</ol>
<p>这个路径里任何一段都可能成为瓶颈。CPU 侧看到的是 <code>cache-misses</code>、<code>stalled-cycles</code>、IPC 下降；互联侧看到的是 outstanding 堆积、QoS 延迟变大；DDR 控制器侧看到的是读写队列拥塞、page miss 增加、refresh 周期影响；业务侧看到的则是帧率下降、推理延迟抖动、实时线程偶发超时。</p>
<p>如果只盯着一个指标，很容易误判。例如 CPU Cache Miss 高，不一定表示 DDR 频率不够，也可能是数据结构布局导致空间局部性太差；DDR 带宽占用高，也不一定要提高频率，可能是多了一次无意义的内存拷贝。</p>
<h2 id="三建立第一组基准连续带宽随机延迟和业务混压">三、建立第一组基准：连续带宽、随机延迟和业务混压</h2>
<p>调优前必须先建立基线。我的习惯是至少跑三类测试：</p>
<h3 id="31-连续读写带宽">3.1 连续读写带宽</h3>
<p>连续带宽可以用 STREAM，也可以写一个简化版测试。下面这个程序不如专业工具严谨，但适合快速确认不同 buffer 大小、不同线程数下的变化趋势：</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;stdint.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;string.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="k">static</span> <span class="kt">double</span> <span class="nf">now_sec</span><span class="p">(</span><span class="kt">void</span><span class="p">)</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">ts</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="nf">clock_gettime</span><span class="p">(</span><span class="n">CLOCK_MONOTONIC</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">ts</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">ts</span><span class="p">.</span><span class="n">tv_sec</span> <span class="o">+</span> <span class="n">ts</span><span class="p">.</span><span class="n">tv_nsec</span> <span class="o">/</span> <span class="mf">1000000000.0</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 class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kt">size_t</span> <span class="n">mb</span> <span class="o">=</span> <span class="n">argc</span> <span class="o">&gt;</span> <span class="mi">1</span> <span class="o">?</span> <span class="nf">strtoull</span><span class="p">(</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="nb">NULL</span><span class="p">,</span> <span class="mi">10</span><span class="p">)</span> <span class="o">:</span> <span class="mi">512</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">size_t</span> <span class="n">bytes</span> <span class="o">=</span> <span class="n">mb</span> <span class="o">*</span> <span class="mi">1024ULL</span> <span class="o">*</span> <span class="mi">1024ULL</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">src</span><span class="p">,</span> <span class="o">*</span><span class="n">dst</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nf">posix_memalign</span><span class="p">((</span><span class="kt">void</span> <span class="o">**</span><span class="p">)</span><span class="o">&amp;</span><span class="n">src</span><span class="p">,</span> <span class="mi">64</span><span class="p">,</span> <span class="n">bytes</span><span class="p">)</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">)</span> <span class="k">return</span> <span class="mi">1</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">posix_memalign</span><span class="p">((</span><span class="kt">void</span> <span class="o">**</span><span class="p">)</span><span class="o">&amp;</span><span class="n">dst</span><span class="p">,</span> <span class="mi">64</span><span class="p">,</span> <span class="n">bytes</span><span class="p">)</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">)</span> <span class="k">return</span> <span class="mi">1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="nf">memset</span><span class="p">(</span><span class="n">src</span><span class="p">,</span> <span class="mh">0x5a</span><span class="p">,</span> <span class="n">bytes</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="nf">memset</span><span class="p">(</span><span class="n">dst</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span> <span class="n">bytes</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kt">double</span> <span class="n">t0</span> <span class="o">=</span> <span class="nf">now_sec</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">r</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">r</span> <span class="o">&lt;</span> <span class="mi">20</span><span class="p">;</span> <span class="o">++</span><span class="n">r</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nf">memcpy</span><span class="p">(</span><span class="n">dst</span><span class="p">,</span> <span class="n">src</span><span class="p">,</span> <span class="n">bytes</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">double</span> <span class="n">t1</span> <span class="o">=</span> <span class="nf">now_sec</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kt">double</span> <span class="n">gb</span> <span class="o">=</span> <span class="p">(</span><span class="kt">double</span><span class="p">)</span><span class="n">bytes</span> <span class="o">*</span> <span class="mf">20.0</span> <span class="o">/</span> <span class="mi">1024</span> <span class="o">/</span> <span class="mi">1024</span> <span class="o">/</span> <span class="mi">1024</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;copy bandwidth: %.2f GB/s</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">,</span> <span class="n">gb</span> <span class="o">/</span> <span class="p">(</span><span class="n">t1</span> <span class="o">-</span> <span class="n">t0</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">    <span class="nf">free</span><span class="p">(</span><span class="n">src</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="nf">free</span><span class="p">(</span><span class="n">dst</span><span class="p">);</span>
</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 -O3 -march<span class="o">=</span>native memcopy_bw.c -o memcopy_bw
</span></span><span class="line"><span class="cl">./memcopy_bw <span class="m">1024</span>
</span></span></code></pre></div><p>注意，这个结果主要反映大块连续拷贝能力，还受到 libc <code>memcpy</code> 实现、CPU 预取、Cache 策略影响。它适合做“平台状态是否正常”的健康检查，但不能代表所有业务。</p>
<h3 id="32-随机访问延迟">3.2 随机访问延迟</h3>
<p>很多控制类、图结构、稀疏张量、数据库索引类负载不是带宽优先，而是延迟优先。连续带宽高的平台，如果随机访问延迟很差，业务一样会慢。可以用 <code>lmbench</code> 的 <code>lat_mem_rd</code>，也可以自己构造链表追指针测试。核心思路是让下一次访问依赖上一次读取结果，破坏 CPU 预取器的发挥空间。</p>
<h3 id="33-混压测试">3.3 混压测试</h3>
<p>真实 SoC 里最关键的是混压：CPU 跑内存测试的同时，让摄像头采集、显示刷新、NPU 推理、视频编码一起工作。很多问题只有在混压下出现，因为 DDR controller 和 NoC 仲裁策略这时才真正被打满。</p>
<p>我通常会记录三组数据：</p>
<ul>
<li>空载下的连续带宽和随机延迟；</li>
<li>业务单独运行时的 DDR 计数器和帧率；</li>
<li>基准测试与业务同时运行时的延迟抖动。</li>
</ul>
<p>如果单独测试都很好，一混压就抖，优先查 QoS、DMA burst、内存拷贝和任务绑核，而不是急着改 DDR 时序。</p>
<p>（第一部分完，约 2200 字）</p>
<h2 id="四用-perf-先判断是不是-cache-问题">四、用 perf 先判断是不是 Cache 问题</h2>
<p>在 Linux 上，第一步通常不是直接看 DDR controller，而是先看 CPU 的硬件性能计数器。因为很多所谓“内存带宽不够”，根因其实是 Cache 使用方式太差。</p>
<p>常用命令如下：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">perf stat -e cycles,instructions,cache-references,cache-misses,LLC-loads,LLC-load-misses ./your_app
</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">perf stat -e stalled-cycles-frontend,stalled-cycles-backend,branch-misses,dTLB-load-misses ./your_app
</span></span></code></pre></div><p>几个经验判断：</p>
<ul>
<li><strong>IPC 很低，backend stall 很高</strong>：CPU 大概率在等内存或执行单元资源。</li>
<li><strong>LLC miss 比例高</strong>：数据工作集超出缓存，或者访问局部性差。</li>
<li><strong>dTLB miss 高</strong>：大数组随机访问、页表压力大，可以考虑 hugepage 或改善布局。</li>
<li><strong>cache-misses 高但 DDR 带宽不高</strong>：可能是随机小访问导致延迟瓶颈，而不是带宽瓶颈。</li>
</ul>
<p>一个非常典型的例子是 AoS（Array of Structs）和 SoA（Struct of Arrays）的差异。假设我们只需要处理像素的亮度 <code>y</code>，但数据结构却把多个字段混在一起：</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">uint8_t</span> <span class="n">y</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint8_t</span> <span class="n">u</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint8_t</span> <span class="n">v</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint8_t</span> <span class="n">flag</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint32_t</span> <span class="n">timestamp</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="n">PixelMeta</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kt">uint64_t</span> <span class="nf">sum_y_aos</span><span class="p">(</span><span class="n">PixelMeta</span> <span class="o">*</span><span class="n">p</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">n</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint64_t</span> <span class="n">s</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">n</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">s</span> <span class="o">+=</span> <span class="n">p</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">y</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">return</span> <span class="n">s</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>CPU 每次拉回 Cache Line，里面包含很多当前循环不需要的字段。改成 SoA 后，访问会连续得多：</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">uint8_t</span> <span class="o">*</span><span class="n">y</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">u</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">v</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">flag</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint32_t</span> <span class="o">*</span><span class="n">timestamp</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="n">PixelPlane</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kt">uint64_t</span> <span class="nf">sum_y_soa</span><span class="p">(</span><span class="n">PixelPlane</span> <span class="o">*</span><span class="n">p</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">n</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint64_t</span> <span class="n">s</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">n</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">s</span> <span class="o">+=</span> <span class="n">p</span><span class="o">-&gt;</span><span class="n">y</span><span class="p">[</span><span class="n">i</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">return</span> <span class="n">s</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>这类优化没有改 DDR 频率，也没有碰内核，却能显著减少无效带宽。尤其在图像处理、传感器融合、推理前后处理里，数据布局往往比单纯“加线程”更重要。</p>
<h2 id="五axinoc-层别让-master-互相踩脚">五、AXI/NoC 层：别让 master 互相踩脚</h2>
<p>SoC 里的 DDR 不是 CPU 独占资源。摄像头 ISP 可能持续写入帧缓冲，显示控制器周期性读取 framebuffer，NPU 读取权重和 feature map，VPU 编码器读写码流和参考帧。它们通常都通过 AXI 或片上 NoC 进入内存系统。</p>
<p>AXI 层常见的几个调优点包括：</p>
<h3 id="51-burst-长度">5.1 Burst 长度</h3>
<p>DDR 喜欢连续访问，AXI burst 太短会带来额外命令开销。对 DMA 来说，尽量让传输地址连续、长度对齐、burst 足够长。比如图像一行 stride 如果没有按 64/128 字节对齐，DMA 可能被迫拆成更多事务。</p>
<p>在驱动里申请 DMA buffer 时，至少要确认：</p>
<ul>
<li>起始地址是否满足硬件对齐；</li>
<li>每行 stride 是否满足模块要求；</li>
<li>buffer 是否跨越硬件不支持的边界；</li>
<li>是否发生了 cache sync 导致额外拷贝或刷写。</li>
</ul>
<h3 id="52-outstanding-能力">5.2 Outstanding 能力</h3>
<p>AXI master 可以同时挂起多个未完成事务。Outstanding 太小，延迟无法被隐藏；太大，又可能挤压其他实时 master。NPU/GPU 这类吞吐型设备通常需要较大的 outstanding，显示、音频、某些实时采集链路则更关心延迟上限。</p>
<p>如果芯片手册提供 NoC 或 DDR port 的 outstanding 配置，建议不要盲目拉满，而是按业务分组测试：</p>
<ul>
<li>单 NPU 推理吞吐；</li>
<li>NPU + ISP；</li>
<li>NPU + ISP + VPU；</li>
<li>加入 CPU 后处理线程。</li>
</ul>
<p>看的是整体帧率和 P99 延迟，而不是某一个模块的峰值。</p>
<h3 id="53-qos-优先级">5.3 QoS 优先级</h3>
<p>很多 SoC 的 AXI port 有 QoS 字段或内部仲裁权重。显示控制器、音频、摄像头输入这类实时流，一旦饿死就会花屏、爆音或丢帧；AI 推理慢一点通常只是延迟增加。因此，QoS 的目标不是让所有模块“公平”，而是让实时链路有确定性，让吞吐型模块吃剩余带宽。</p>
<p>一个实用策略是：</p>
<ol>
<li>先保证显示/采集链路不丢；</li>
<li>再给编码器、NPU 设置中等优先级；</li>
<li>CPU 后台任务、日志、文件 IO 降低优先级；</li>
<li>对吞吐型 DMA 使用大 burst，但限制 outstanding，避免长时间占住通道。</li>
</ol>
<h2 id="六ddr-controllerrow-hit读写切换和刷新">六、DDR Controller：Row Hit、读写切换和刷新</h2>
<p>到了 DDR controller 层，问题会变得更“硬件”。这里的核心是调度器如何把上层来的请求转换成 DRAM 命令序列。</p>
<p>DRAM 内部按 bank、row、column 组织。访问已经打开的 row，叫 row hit；如果要访问另一个 row，就需要 precharge 当前 row，再 activate 新 row，开销明显更大。因此连续访问、按行访问、减少跨 bank/row 的随机跳转，通常能提升效率。</p>
<p>控制器还要处理读写切换。读和写在总线上方向不同，频繁切换会产生 turnaround penalty。很多 controller 会倾向于攒一批读或一批写再切换，以提高总线效率；但如果攒得太久，实时写入或读取的延迟可能变差。</p>
<p>刷新也是不可忽略的因素。DRAM 需要周期性 refresh，温度越高，刷新压力可能越大。某些项目在高温箱里出现周期性延迟尖峰，最后发现不是 CPU 调度问题，而是内存刷新和业务峰值叠加。</p>
<p>如果平台暴露 DDR controller 计数器，建议关注：</p>
<ul>
<li>read/write command 数量；</li>
<li>row hit / row miss；</li>
<li>bank conflict；</li>
<li>refresh 周期；</li>
<li>port busy 或 queue full；</li>
<li>各 master 的带宽占比。</li>
</ul>
<p>不同厂商接口差异很大，有的在 <code>/sys</code>，有的通过 <code>devfreq</code>，有的要读寄存器。不要只看一个总带宽数字，最好能按 master 或 port 拆开，否则很难知道谁在消耗带宽。</p>
<h2 id="七linux-侧常见的隐藏带宽消耗">七、Linux 侧常见的隐藏带宽消耗</h2>
<p>在应用层，最浪费 DDR 的通常不是计算，而是“搬来搬去”。视频和 AI pipeline 里尤其明显：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Camera -&gt; ISP -&gt; 内存 -&gt; CPU memcpy -&gt; NPU input -&gt; 内存 -&gt; CPU 后处理 -&gt; VPU/Display
</span></span></code></pre></div><p>如果每个箭头都落 DDR，每个模块之间再做一次格式转换，带宽很快就被吃光。优化方向包括：</p>
<ul>
<li>使用 DMA-BUF 在模块之间共享 buffer；</li>
<li>尽量让 ISP/RGA/NPU/VPU 直接处理物理连续或 IOMMU 映射后的 buffer；</li>
<li>减少 CPU 参与大块图像拷贝；</li>
<li>统一颜色格式，避免 NV12/RGB/BGR 来回转换；</li>
<li>对只读权重、查找表使用合适的缓存策略；</li>
<li>对一次性 DMA buffer 避免不必要的 cache invalidate/clean。</li>
</ul>
<p>很多时候，把两次 <code>memcpy</code> 去掉，比把 DDR 频率从 3200 提到 4266 更有效，也更省电。</p>
<p>（第二部分完，约 2400 字）</p>
<h2 id="八一个可复用的带宽排查脚本">八、一个可复用的带宽排查脚本</h2>
<p>下面这个 Python 脚本用于自动跑不同 buffer 大小下的拷贝测试，并记录 <code>perf stat</code> 的关键指标。实际项目里我会把它放进 bring-up 工具箱，每次改 DDR 频率、内核版本、驱动 DMA 策略后都跑一遍。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="ch">#!/usr/bin/env python3</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">csv</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">re</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">subprocess</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">pathlib</span> <span class="kn">import</span> <span class="n">Path</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">SIZES_MB</span> <span class="o">=</span> <span class="p">[</span><span class="mi">64</span><span class="p">,</span> <span class="mi">128</span><span class="p">,</span> <span class="mi">256</span><span class="p">,</span> <span class="mi">512</span><span class="p">,</span> <span class="mi">1024</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="n">EVENTS</span> <span class="o">=</span> <span class="s2">&#34;cycles,instructions,cache-references,cache-misses,LLC-loads,LLC-load-misses&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">BIN</span> <span class="o">=</span> <span class="s2">&#34;./memcopy_bw&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">perf_re</span> <span class="o">=</span> <span class="n">re</span><span class="o">.</span><span class="n">compile</span><span class="p">(</span><span class="sa">r</span><span class="s2">&#34;^\s*([0-9,]+)\s+([A-Za-z0-9_-]+)&#34;</span><span class="p">,</span> <span class="n">re</span><span class="o">.</span><span class="n">M</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">bw_re</span> <span class="o">=</span> <span class="n">re</span><span class="o">.</span><span class="n">compile</span><span class="p">(</span><span class="sa">r</span><span class="s2">&#34;copy bandwidth:\s+([0-9.]+)\s+GB/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="k">def</span> <span class="nf">run_one</span><span class="p">(</span><span class="n">size_mb</span><span class="p">:</span> <span class="nb">int</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">cmd</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;perf&#34;</span><span class="p">,</span> <span class="s2">&#34;stat&#34;</span><span class="p">,</span> <span class="s2">&#34;-e&#34;</span><span class="p">,</span> <span class="n">EVENTS</span><span class="p">,</span> <span class="n">BIN</span><span class="p">,</span> <span class="nb">str</span><span class="p">(</span><span class="n">size_mb</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl">    <span class="n">p</span> <span class="o">=</span> <span class="n">subprocess</span><span class="o">.</span><span class="n">run</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">text</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">stdout</span><span class="o">=</span><span class="n">subprocess</span><span class="o">.</span><span class="n">PIPE</span><span class="p">,</span> <span class="n">stderr</span><span class="o">=</span><span class="n">subprocess</span><span class="o">.</span><span class="n">PIPE</span><span class="p">,</span> <span class="n">check</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">out</span> <span class="o">=</span> <span class="n">p</span><span class="o">.</span><span class="n">stdout</span> <span class="o">+</span> <span class="s2">&#34;</span><span class="se">\n</span><span class="s2">&#34;</span> <span class="o">+</span> <span class="n">p</span><span class="o">.</span><span class="n">stderr</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">row</span> <span class="o">=</span> <span class="p">{</span><span class="s2">&#34;size_mb&#34;</span><span class="p">:</span> <span class="n">size_mb</span><span class="p">,</span> <span class="s2">&#34;bandwidth_gbps&#34;</span><span class="p">:</span> <span class="kc">None</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="n">m</span> <span class="o">=</span> <span class="n">bw_re</span><span class="o">.</span><span class="n">search</span><span class="p">(</span><span class="n">out</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">m</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">row</span><span class="p">[</span><span class="s2">&#34;bandwidth_gbps&#34;</span><span class="p">]</span> <span class="o">=</span> <span class="nb">float</span><span class="p">(</span><span class="n">m</span><span class="o">.</span><span class="n">group</span><span class="p">(</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">for</span> <span class="n">value</span><span class="p">,</span> <span class="n">name</span> <span class="ow">in</span> <span class="n">perf_re</span><span class="o">.</span><span class="n">findall</span><span class="p">(</span><span class="n">out</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="n">row</span><span class="p">[</span><span class="n">name</span><span class="p">]</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">value</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">&#34;,&#34;</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">row</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">rows</span> <span class="o">=</span> <span class="p">[</span><span class="n">run_one</span><span class="p">(</span><span class="n">s</span><span class="p">)</span> <span class="k">for</span> <span class="n">s</span> <span class="ow">in</span> <span class="n">SIZES_MB</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="n">keys</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">({</span><span class="n">k</span> <span class="k">for</span> <span class="n">r</span> <span class="ow">in</span> <span class="n">rows</span> <span class="k">for</span> <span class="n">k</span> <span class="ow">in</span> <span class="n">r</span><span class="o">.</span><span class="n">keys</span><span class="p">()})</span>
</span></span><span class="line"><span class="cl"><span class="n">Path</span><span class="p">(</span><span class="s2">&#34;results&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">mkdir</span><span class="p">(</span><span class="n">exist_ok</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="s2">&#34;results/memory_perf.csv&#34;</span><span class="p">,</span> <span class="s2">&#34;w&#34;</span><span class="p">,</span> <span class="n">newline</span><span class="o">=</span><span class="s2">&#34;&#34;</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">writer</span> <span class="o">=</span> <span class="n">csv</span><span class="o">.</span><span class="n">DictWriter</span><span class="p">(</span><span class="n">f</span><span class="p">,</span> <span class="n">fieldnames</span><span class="o">=</span><span class="n">keys</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">writer</span><span class="o">.</span><span class="n">writeheader</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">writer</span><span class="o">.</span><span class="n">writerows</span><span class="p">(</span><span class="n">rows</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">r</span> <span class="ow">in</span> <span class="n">rows</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">miss</span> <span class="o">=</span> <span class="n">r</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;cache-misses&#34;</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">ref</span> <span class="o">=</span> <span class="n">r</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;cache-references&#34;</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">miss_rate</span> <span class="o">=</span> <span class="n">miss</span> <span class="o">/</span> <span class="n">ref</span> <span class="o">*</span> <span class="mi">100</span> <span class="k">if</span> <span class="n">ref</span> <span class="k">else</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">r</span><span class="p">[</span><span class="s1">&#39;size_mb&#39;</span><span class="p">]</span><span class="si">:</span><span class="s2">4d</span><span class="si">}</span><span class="s2"> MB  </span><span class="si">{</span><span class="n">r</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">&#39;bandwidth_gbps&#39;</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span><span class="si">:</span><span class="s2">6.2f</span><span class="si">}</span><span class="s2"> GB/s  cache miss </span><span class="si">{</span><span class="n">miss_rate</span><span class="si">:</span><span class="s2">5.2f</span><span class="si">}</span><span class="s2">%&#34;</span><span class="p">)</span>
</span></span></code></pre></div><p>这个脚本不能替代专业测试，但有两个好处：第一，它让每次优化都有数据记录；第二，它能快速发现“改动看似无关，内存行为却变了”的情况。例如某次驱动修改把 buffer 从 cacheable 改成 non-cacheable，CPU 后处理性能会立刻掉下来；某次设备树改错 DDR devfreq 档位，连续带宽也会明显变化。</p>
<h2 id="九实战调优顺序不要一上来就改-ddr-参数">九、实战调优顺序：不要一上来就改 DDR 参数</h2>
<p>DDR 参数很诱人，因为它看起来离瓶颈最近。但在量产项目里，随意修改 DDR training、ODT、时序、频率，风险远高于收益。我的建议顺序是：</p>
<h3 id="91-先确认频率和工作模式">9.1 先确认频率和工作模式</h3>
<p>检查 DDR 是否跑在预期频率，devfreq 是否被省电策略压低，双通道是否都启用，位宽是否符合硬件设计。很多“性能问题”最后只是设备树频点、bootloader 初始化或电源模式配置不对。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cat /sys/class/devfreq/*/cur_freq 2&gt;/dev/null
</span></span><span class="line"><span class="cl">cat /sys/class/devfreq/*/available_frequencies 2&gt;/dev/null
</span></span></code></pre></div><p>不同平台节点名称不一样，上面命令只是示意。关键是把空载、业务运行、混压时的频率都记录下来。</p>
<h3 id="92-再减少无效流量">9.2 再减少无效流量</h3>
<p>优先去掉重复拷贝、格式来回转换、日志大吞吐写盘、调试 overlay、无意义的 buffer 清零。尤其是图像类项目，<code>memset</code> 和 <code>memcpy</code> 经常藏在看起来不起眼的封装函数里。</p>
<p>可以临时用 <code>LD_PRELOAD</code> 包装 <code>memcpy</code> 做统计，也可以在代码里对大块拷贝加 trace。不要凭感觉判断，很多团队最后会惊讶地发现，CPU 每帧搬运的数据量比原始图像大几倍。</p>
<h3 id="93-然后优化访问局部性">9.3 然后优化访问局部性</h3>
<p>包括数据结构从 AoS 改 SoA、循环顺序调整、tile/block 处理、预取、对齐、减少随机访问。矩阵和图像算法里，分块往往非常有效：让工作集留在 L1/L2 里，而不是每一步都回 DDR。</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="c1">// 差：按列访问，跨行 stride 大，Cache 利用率低
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">x</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">x</span> <span class="o">&lt;</span> <span class="n">width</span><span class="p">;</span> <span class="o">++</span><span class="n">x</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">y</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">y</span> <span class="o">&lt;</span> <span class="n">height</span><span class="p">;</span> <span class="o">++</span><span class="n">y</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">acc</span> <span class="o">+=</span> <span class="n">img</span><span class="p">[</span><span class="n">y</span> <span class="o">*</span> <span class="n">stride</span> <span class="o">+</span> <span class="n">x</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="c1">// 好：按行访问，连续读取
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">y</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">y</span> <span class="o">&lt;</span> <span class="n">height</span><span class="p">;</span> <span class="o">++</span><span class="n">y</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">const</span> <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">row</span> <span class="o">=</span> <span class="n">img</span> <span class="o">+</span> <span class="n">y</span> <span class="o">*</span> <span class="n">stride</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">x</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">x</span> <span class="o">&lt;</span> <span class="n">width</span><span class="p">;</span> <span class="o">++</span><span class="n">x</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">acc</span> <span class="o">+=</span> <span class="n">row</span><span class="p">[</span><span class="n">x</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><h3 id="94-最后再碰-qos-和-ddr-控制器">9.4 最后再碰 QoS 和 DDR 控制器</h3>
<p>当确认无效流量已经压下去、访问模式也合理，但混压下仍然抖动，再去调 QoS、outstanding、读写队列、水线、DDR devfreq governor。每次只改一个变量，并记录平均值、P95、P99，而不是只看峰值。</p>
<h2 id="十几个常见故障现象和定位方向">十、几个常见故障现象和定位方向</h2>
<h3 id="101-单测很快业务很慢">10.1 单测很快，业务很慢</h3>
<p>优先怀疑多 master 竞争、额外拷贝和 Cache 同步。连续内存测试无法复现业务的读写混合和实时约束，必须做混压。</p>
<h3 id="102-平均帧率够偶发掉帧">10.2 平均帧率够，偶发掉帧</h3>
<p>看 P99 延迟、调度抢占、DDR refresh、QoS、水温/温度导致的降频。显示、摄像头、音频类问题尤其要关注最坏情况，而不是平均值。</p>
<h3 id="103-cpu-占用不高但程序慢">10.3 CPU 占用不高但程序慢</h3>
<p>可能是 backend stall，线程在等内存。用 <code>perf stat</code> 看 IPC、cache miss、dTLB miss，再结合火焰图看热点是不是大数组随机访问。</p>
<h3 id="104-npu-利用率上不去">10.4 NPU 利用率上不去</h3>
<p>不一定是模型小，也可能是输入预处理、tensor layout 转换、权重读取、NPU 与 CPU/NPU 共享 DDR 造成等待。检查是否支持零拷贝输入，是否每帧都做了不必要的 NHWC/NCHW 转换。</p>
<h3 id="105-高温后性能下降">10.5 高温后性能下降</h3>
<p>检查 DDR devfreq、CPU/GPU/NPU 降频、DRAM refresh、PMIC 限流。高温问题不要只盯 CPU 温度，内存和电源策略也会影响吞吐。</p>
<h2 id="十一量产项目里的建议清单">十一、量产项目里的建议清单</h2>
<p>最后给一份我在项目评审里常用的 checklist：</p>
<ul>
<li>DDR 频率、位宽、通道数是否和硬件设计一致；</li>
<li>bootloader 与内核里的 DDR/devfreq 配置是否一致；</li>
<li>是否有 STREAM、随机延迟、业务混压三类基线数据；</li>
<li>是否记录了 CPU PMU、DDR controller、NoC/AXI port 计数器；</li>
<li>视频/AI pipeline 是否做到 DMA-BUF 或等价的零拷贝；</li>
<li>大 buffer 是否对齐，stride 是否满足 DMA burst；</li>
<li>CPU 是否存在大块 <code>memcpy</code>、<code>memset</code>、格式转换；</li>
<li>热点数据结构是否有良好的空间局部性；</li>
<li>实时 master 的 QoS 是否高于吞吐型后台任务；</li>
<li>是否用 P95/P99 延迟评估，而不是只看平均吞吐；</li>
<li>高温、低电压、省电模式下是否重复验证。</li>
</ul>
<h2 id="总结">总结</h2>
<p>DDR 带宽调优不是单点优化，而是一条链路的系统工程。CPU 看到的是 Cache Miss，DMA 看到的是 burst 和对齐，NoC 看到的是仲裁和 outstanding，DDR controller 看到的是 row hit、读写切换和刷新，业务最终看到的是帧率、延迟和稳定性。</p>
<p>真正有效的调优顺序应该是：先建立基线，再确认频率和硬件配置；先减少无效拷贝，再改善数据局部性；先通过 perf 和业务 trace 定位瓶颈，再去调整 QoS、NoC 和 DDR controller。除非你已经有充分数据证明瓶颈在内存控制器，否则不要一上来就改 DDR 时序。</p>
<p>如果只能记住一句话，那就是：<strong>内存系统优化的目标不是跑出最高的 GB/s，而是在真实业务混压下，把有效数据稳定、可预测地送到需要它的计算单元。</strong> 这也是芯片架构和嵌入式软件最有意思的交界处——硬件给了你上限，软件决定你能接近多少。</p>
<p>（全文完，约 6800 字）</p>
]]></content:encoded></item><item><title>ARMv8-A AArch64 架构深度解析与汇编编程实战指南</title><link>https://tech-snippets.xyz/posts/armv8-aarch64-assembly-guide/</link><pubDate>Tue, 26 May 2026 19:00:00 +0800</pubDate><guid>https://tech-snippets.xyz/posts/armv8-aarch64-assembly-guide/</guid><description>前言 如果你是一名嵌入式开发者或者系统程序员，可能会有这样的困惑：「我都用上 C/C++ 甚至 Rust 了，为什么还要学汇编语言？」
这个问题我被问过很多次。2018 年我在做一个手机相机 HAL 层的性能优化项目时，算法团队用 NEON intrinsics 写的图像处理函数在骁龙 845 上跑 12ms，离 8ms 的目标还差很远。我花了三天时间，把核心循环改成纯 AArch64 汇编，最终跑到了 6.8ms——这就是汇编的力量：你完全掌控了 CPU 的每一个周期、每一个寄存器。
今天，AArch64 已经无处不在：从你的智能手机到树莓派 4/5，从 AWS Graviton 服务器到 NVIDIA Jetson 开发板，甚至苹果 M 系列芯片本质上也是 AArch64 兼容架构。理解 AArch64 架构，不仅能让你写出更快的代码，更能让你真正理解现代 CPU 是如何工作的。
这篇文章不会教你「Hello World」级别的汇编入门。我要做的是：从架构设计哲学讲起，深入寄存器模型、指令集、寻址模式、函数调用约定，最后用实战项目教你如何写出高性能的 AArch64 汇编代码。
一、ARMv8-A 架构：从 32 位到 64 位的革命 1.1 ARM 架构演进简史 在深入 AArch64 之前，我们先快速回顾一下 ARM 架构的演进路线：
ARMv4/v5 (1990s): 经典 ARM，32 位 ARM 状态 + 16 位 Thumb 状态 ARMv6 (2001): 引入 SIMD 媒体处理扩展、未对齐内存访问 ARMv7-A (2005): Cortex-A 系列诞生，NEON SIMD、虚拟化扩展 ARMv8-A (2011): 革命性的 64 位架构，AArch64 执行状态 ARMv8.</description><content:encoded><![CDATA[<h2 id="前言">前言</h2>
<p>如果你是一名嵌入式开发者或者系统程序员，可能会有这样的困惑：「我都用上 C/C++ 甚至 Rust 了，为什么还要学汇编语言？」</p>
<p>这个问题我被问过很多次。2018 年我在做一个手机相机 HAL 层的性能优化项目时，算法团队用 NEON  intrinsics 写的图像处理函数在骁龙 845 上跑 12ms，离 8ms 的目标还差很远。我花了三天时间，把核心循环改成纯 AArch64 汇编，最终跑到了 6.8ms——这就是汇编的力量：你完全掌控了 CPU 的每一个周期、每一个寄存器。</p>
<p>今天，AArch64 已经无处不在：从你的智能手机到树莓派 4/5，从 AWS Graviton 服务器到 NVIDIA Jetson 开发板，甚至苹果 M 系列芯片本质上也是 AArch64 兼容架构。理解 AArch64 架构，不仅能让你写出更快的代码，更能让你真正理解现代 CPU 是如何工作的。</p>
<p>这篇文章不会教你「Hello World」级别的汇编入门。我要做的是：从架构设计哲学讲起，深入寄存器模型、指令集、寻址模式、函数调用约定，最后用实战项目教你如何写出高性能的 AArch64 汇编代码。</p>
<h2 id="一armv8-a-架构从-32-位到-64-位的革命">一、ARMv8-A 架构：从 32 位到 64 位的革命</h2>
<h3 id="11-arm-架构演进简史">1.1 ARM 架构演进简史</h3>
<p>在深入 AArch64 之前，我们先快速回顾一下 ARM 架构的演进路线：</p>
<ul>
<li><strong>ARMv4/v5</strong> (1990s): 经典 ARM，32 位 ARM 状态 + 16 位 Thumb 状态</li>
<li><strong>ARMv6</strong> (2001): 引入 SIMD 媒体处理扩展、未对齐内存访问</li>
<li><strong>ARMv7-A</strong> (2005): Cortex-A 系列诞生，NEON SIMD、虚拟化扩展</li>
<li><strong>ARMv8-A</strong> (2011): 革命性的 64 位架构，AArch64 执行状态</li>
<li><strong>ARMv8.1-A</strong> ~ <strong>ARMv8.5-A</strong>: 持续增强，加入 SVE、指针认证、内存标记等</li>
<li><strong>ARMv9-A</strong> (2021): SVE2、机密计算架构 (CCA)、更多安全增强</li>
</ul>
<p>很多人误以为 ARMv8-A 「就是 64 位的 ARMv7」——这是完全错误的。ARMv8-A 不是在 ARMv7 基础上「加了几根地址线」，而是<strong>几乎重新设计了整个指令集架构</strong>。</p>
<h3 id="12-两个执行状态aarch32-与-aarch64">1.2 两个执行状态：AArch32 与 AArch64</h3>
<p>ARMv8-A 最独特的设计就是支持<strong>两个独立的执行状态</strong>：</p>
<table>
<thead>
<tr>
<th>特性</th>
<th>AArch32</th>
<th>AArch64</th>
</tr>
</thead>
<tbody>
<tr>
<td>通用寄存器</td>
<td>15 个 (R0-R14)</td>
<td>31 个 (X0-X30)</td>
</tr>
<tr>
<td>寄存器宽度</td>
<td>32 位</td>
<td>64 位</td>
</tr>
<tr>
<td>指令集</td>
<td>ARM (32位) / Thumb (16位)</td>
<td>A64 (固定 32 位)</td>
</tr>
<tr>
<td>异常级别</td>
<td>PL0/PL1/PL2</td>
<td>EL0/EL1/EL2/EL3</td>
</tr>
<tr>
<td>栈指针</td>
<td>R13 (共用)</td>
<td>SP_EL0 / SP_EL1 / SP_EL2 / SP_EL3</td>
</tr>
<tr>
<td>程序计数器</td>
<td>R15</td>
<td>PC (不直接作为通用寄存器)</td>
</tr>
</tbody>
</table>
<p>这里有个非常重要的设计决策：<strong>AArch64 状态下的程序计数器 (PC) 不再是通用寄存器</strong>。在 ARMv7 中，你可以直接对 PC 进行算术运算、用 PC 做基址寻址——这种灵活性带来了很多巧妙的编程技巧，但也严重限制了 CPU 的乱序执行优化。ARMv8 的设计者果断砍掉了这个特性，换来了更大的性能提升空间。</p>
<h3 id="13-aarch64-的设计哲学">1.3 AArch64 的设计哲学</h3>
<p>如果你仔细对比 AArch64 和 x86-64，会发现它们走了完全不同的路线：</p>
<p><strong>x86-64 的路线</strong>：向后兼容到极致。32 位 x86 程序可以直接在 64 位模式下运行，甚至可以在同一个进程中混合 32 位和 64 位代码。代价是：指令编码极其复杂，解码器占用大量芯片面积。</p>
<p><strong>AArch64 的路线</strong>：干净利落地切断包袱。AArch32 和 AArch64 是两个几乎独立的世界，异常级别之间的切换是唯一的桥梁。代价是：旧的 32 位应用必须重新编译才能运行。好处是：指令集可以从零开始优化，没有历史包袱。</p>
<p>这就是为什么 AArch64 汇编看起来如此「干净」——没有 x86 那些奇奇怪怪的指令后缀，没有复杂的 ModR/M 编码，所有指令都是固定 32 位宽度，字段位置高度规律。</p>
<h3 id="14-armv8-a-异常级别模型">1.4 ARMv8-A 异常级别模型</h3>
<p>ARMv8-A 用 <strong>异常级别 (Exception Level, EL)</strong> 取代了传统的特权级模型，这是一个非常重要的架构概念：</p>
<ul>
<li><strong>EL0</strong>: 用户态 (User)，运行应用程序</li>
<li><strong>EL1</strong>: 内核态 (Kernel)，运行操作系统内核</li>
<li><strong>EL2</strong>: 虚拟机监控器 (Hypervisor)，运行虚拟化管理程序</li>
<li><strong>EL3</strong>: 安全监控器 (Secure Monitor)，负责安全世界和普通世界切换</li>
</ul>
<p>这个模型的巧妙之处在于：<strong>级别编号越高，特权越大</strong>。这和很多人直觉中的「Level 0 最高」正好相反。ARM 设计者的理由是：异常发生时，CPU 总是进入更高的 EL，从 0 往上加很自然。</p>
<p>每个 EL 都有自己独立的栈指针 (SP_ELn)、异常向量表、以及一整套系统寄存器。这种隔离设计是 ARMv8-A 安全架构的基石。</p>
<h2 id="二aarch64-寄存器模型31-个寄存器的艺术">二、AArch64 寄存器模型：31 个寄存器的艺术</h2>
<h3 id="21-通用寄存器组">2.1 通用寄存器组</h3>
<p>AArch64 有 <strong>31 个通用寄存器</strong>，编号 X0 到 X30。每个寄存器可以被当作 64 位使用 (Xn)，或者当作 32 位使用 (Wn)。</p>
<pre tabindex="0"><code>X0  (W0)   X8  (W8)    X16 (W16)   X24 (W24)
X1  (W1)   X9  (W9)    X17 (W17)   X25 (W25)
X2  (W2)   X10 (W10)   X18 (W18)   X26 (W26)
X3  (W3)   X11 (W11)   X19 (W19)   X27 (W27)
X4  (W4)   X12 (W12)   X20 (W20)   X28 (W28)
X5  (W5)   X13 (W13)   X21 (W21)   X29 (W29)   FP (帧指针)
X6  (W6)   X14 (W14)   X22 (W22)   X30 (W30)   LR (链接寄存器)
X7  (W7)   X15 (W15)   X23 (W23)
</code></pre><p><strong>关键特性</strong>：</p>
<ol>
<li><strong>部分寄存器访问</strong>：当你写 Wn 寄存器时，<strong>高 32 位会被自动清零</strong>。这和 x86-64 完全不同（x86-64 写 32 位寄存器也会清零高 32 位，但这是后来加的）。</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-asm" data-lang="asm"><span class="line"><span class="cl"><span class="nf">mov</span>     <span class="no">w0</span><span class="p">,</span> <span class="c1">#0x1234     // X0 = 0x0000000000001234
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nf">mov</span>     <span class="no">x0</span><span class="p">,</span> <span class="c1">#0x5678     // X0 = 0x0000000000005678
</span></span></span></code></pre></div><ol start="2">
<li><strong>零寄存器 XZR/WZR</strong>：这是 AArch64 最巧妙的设计之一——寄存器编号 31，根据上下文不同，它可以是栈指针 (SP)，也可以是永远返回 0 的零寄存器。</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-asm" data-lang="asm"><span class="line"><span class="cl"><span class="nf">mov</span>     <span class="no">x0</span><span class="p">,</span> <span class="no">xzr</span>         <span class="c1">// X0 = 0，比 mov x0, #0 更高效
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nf">add</span>     <span class="no">x0</span><span class="p">,</span> <span class="no">x1</span><span class="p">,</span> <span class="no">xzr</span>     <span class="c1">// X0 = X1，变相的 mov 指令
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nf">str</span>     <span class="no">xzr</span><span class="p">,</span> <span class="p">[</span><span class="no">x2</span><span class="p">]</span>       <span class="c1">// 向地址 X2 写入 0
</span></span></span></code></pre></div><p>零寄存器让很多指令变得更加简洁。比如你不需要专门的 <code>nop</code> 指令——<code>mov x0, x0</code> 就是 nop，而 <code>orr x0, xzr, xzr</code> 也是 nop。</p>
<ol start="3">
<li><strong>程序计数器 (PC)</strong>：PC 不再是通用寄存器，你不能像 ARMv7 那样 <code>add pc, pc, #4</code>。但 PC 仍然可以被某些寻址模式隐式引用，比如 ADR 指令和字面量加载。</li>
</ol>
<h3 id="22-特殊寄存器">2.2 特殊寄存器</h3>
<p><strong>栈指针 SP</strong>：每个异常级别有独立的栈指针。AArch64 的栈总是 16 字节对齐的——这是硬性要求，不对齐的 SP 访问会触发异常。</p>
<p><strong>链接寄存器 LR (X30)</strong>：保存函数返回地址。<code>BL</code> 指令会自动把返回地址写入 LR。</p>
<p><strong>帧指针 FP (X29)</strong>：虽然 AArch64 没有强制要求帧指针，但 GCC、Clang 等编译器在非优化模式下都会用 X29 作为 FP。</p>
<p><strong>过程调用结果寄存器 X8</strong>：用于间接调用的返回地址，以及某些特殊的系统调用。</p>
<h3 id="23-浮点与-neon-寄存器">2.3 浮点与 NEON 寄存器</h3>
<p>AArch64 的浮点/SIMD 寄存器组也是一大亮点：<strong>32 个 128 位寄存器 V0-V31</strong>。</p>
<p>每个寄存器可以被访问为：</p>
<ul>
<li><strong>Bn</strong>: 8 位 (字节)</li>
<li><strong>Hn</strong>: 16 位 (半精度浮点)</li>
<li><strong>Sn</strong>: 32 位 (单精度浮点)</li>
<li><strong>Dn</strong>: 64 位 (双精度浮点)</li>
<li><strong>Qn</strong>: 128 位 (NEON 向量)</li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-asm" data-lang="asm"><span class="line"><span class="cl"><span class="nf">fadd</span>    <span class="no">s0</span><span class="p">,</span> <span class="no">s1</span><span class="p">,</span> <span class="no">s2</span>      <span class="c1">// 单精度浮点加法
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nf">fadd</span>    <span class="no">d0</span><span class="p">,</span> <span class="no">d1</span><span class="p">,</span> <span class="no">d2</span>      <span class="c1">// 双精度浮点加法
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nf">add</span>     <span class="no">v0.4s</span><span class="p">,</span> <span class="no">v1.4s</span><span class="p">,</span> <span class="no">v2.4s</span>  <span class="c1">// NEON：4个单精度同时相加
</span></span></span></code></pre></div><p>32 个 128 位寄存器意味着什么？你可以在寄存器里放下 32 个双精度数，或者 128 个单精度数——对于很多内核级算法来说，这意味着整个循环的中间数据都可以放在寄存器里，完全不需要访问内存。</p>
<p>（第一部分完，约2400字）</p>
]]></content:encoded></item><item><title>Mali GPU 架构原理与嵌入式图形计算深度优化指南</title><link>https://tech-snippets.xyz/posts/mali-gpu-architecture-optimization-guide/</link><pubDate>Sun, 17 May 2026 19:00:00 +0800</pubDate><guid>https://tech-snippets.xyz/posts/mali-gpu-architecture-optimization-guide/</guid><description>前言 在嵌入式系统飞速发展的今天，GPU 早已不再仅仅是&amp;quot;游戏显卡&amp;quot;的代名词。从智能手机的流畅 UI 渲染，到车载娱乐系统的 3D 导航，从边缘设备的 AI 推理加速，到 AR/VR 设备的实时渲染，GPU 已经成为现代嵌入式 SoC 中不可或缺的核心组件。而在这个领域，ARM Mali GPU 无疑是占据统治地位的存在——全球超过 70% 的 Android 设备都搭载了 Mali GPU，从入门级的 Mali-G52 到旗舰级的 Mali-G720，Mali 架构覆盖了从低端到高端的完整产品线。
然而，尽管 Mali GPU 如此普及，真正深入理解其架构原理的开发者却并不多。大多数嵌入式工程师习惯于 CPU 的线性编程模型，面对 GPU 的并行计算架构和独特的渲染流水线时，往往感到无从下手。更重要的是，Mali GPU 采用的基于分片（Tile-Based）的渲染架构，与桌面端 NVIDIA/AMD 的立即模式渲染有着本质区别，如果不理解这种差异，写出的着色器代码往往会出现严重的性能问题。
我曾见过太多这样的案例：一个在 PC 上运行流畅的 OpenGL ES 应用，移植到嵌入式平台后帧率暴跌；一份看似合理的着色器代码，却在 Mali GPU 上出现了难以解释的带宽瓶颈；一个经过精心优化的渲染流程，实际性能却只有理论值的三分之一。这些问题的根源，往往都在于对 Mali GPU 架构的理解不够深入。
本文将从硬件架构出发，系统地讲解 Mali GPU 的工作原理。我们会从最基础的 Tiler 分片渲染机制讲起，深入到着色器核心的执行模型，分析内存层次结构的设计考量，最后给出一套完整的性能优化方法论。无论你是正在开发嵌入式图形应用的工程师，还是对 GPU 架构感兴趣的技术爱好者，这篇文章都能为你揭开 Mali GPU 的神秘面纱。
一、为什么嵌入式 GPU 需要不同的架构设计？ 在深入 Mali GPU 的具体架构之前，我们首先要回答一个根本性的问题：为什么嵌入式 GPU 不能直接沿用桌面 GPU 的设计？答案可以用三个关键词来概括：功耗、带宽、面积。</description><content:encoded><![CDATA[<h2 id="前言">前言</h2>
<p>在嵌入式系统飞速发展的今天，GPU 早已不再仅仅是&quot;游戏显卡&quot;的代名词。从智能手机的流畅 UI 渲染，到车载娱乐系统的 3D 导航，从边缘设备的 AI 推理加速，到 AR/VR 设备的实时渲染，GPU 已经成为现代嵌入式 SoC 中不可或缺的核心组件。而在这个领域，ARM Mali GPU 无疑是占据统治地位的存在——全球超过 70% 的 Android 设备都搭载了 Mali GPU，从入门级的 Mali-G52 到旗舰级的 Mali-G720，Mali 架构覆盖了从低端到高端的完整产品线。</p>
<p>然而，尽管 Mali GPU 如此普及，真正深入理解其架构原理的开发者却并不多。大多数嵌入式工程师习惯于 CPU 的线性编程模型，面对 GPU 的并行计算架构和独特的渲染流水线时，往往感到无从下手。更重要的是，Mali GPU 采用的基于分片（Tile-Based）的渲染架构，与桌面端 NVIDIA/AMD 的立即模式渲染有着本质区别，如果不理解这种差异，写出的着色器代码往往会出现严重的性能问题。</p>
<p>我曾见过太多这样的案例：一个在 PC 上运行流畅的 OpenGL ES 应用，移植到嵌入式平台后帧率暴跌；一份看似合理的着色器代码，却在 Mali GPU 上出现了难以解释的带宽瓶颈；一个经过精心优化的渲染流程，实际性能却只有理论值的三分之一。这些问题的根源，往往都在于对 Mali GPU 架构的理解不够深入。</p>
<p>本文将从硬件架构出发，系统地讲解 Mali GPU 的工作原理。我们会从最基础的 Tiler 分片渲染机制讲起，深入到着色器核心的执行模型，分析内存层次结构的设计考量，最后给出一套完整的性能优化方法论。无论你是正在开发嵌入式图形应用的工程师，还是对 GPU 架构感兴趣的技术爱好者，这篇文章都能为你揭开 Mali GPU 的神秘面纱。</p>
<p><img alt="Mali GPU 架构示意图" loading="lazy" src="/images/mali-gpu-architecture-overview.svg"></p>
<h2 id="一为什么嵌入式-gpu-需要不同的架构设计">一、为什么嵌入式 GPU 需要不同的架构设计？</h2>
<p>在深入 Mali GPU 的具体架构之前，我们首先要回答一个根本性的问题：为什么嵌入式 GPU 不能直接沿用桌面 GPU 的设计？答案可以用三个关键词来概括：功耗、带宽、面积。</p>
<p>桌面 GPU 的设计哲学是&quot;性能优先&quot;——只要能跑出更高的帧率，功耗高一点、芯片面积大一点都无所谓。一块高端 NVIDIA 显卡的功耗可以达到 400W 以上，芯片面积超过 600mm²，显存带宽超过 1TB/s。这种设计在桌面环境下完全可行，但在嵌入式场景中却是不可接受的。</p>
<p>嵌入式设备面临着完全不同的约束：</p>
<ul>
<li><strong>功耗预算极其有限</strong>：手机 SoC 的整体功耗通常在 5-10W，GPU 分到的预算只有 2-3W；即使是车载 SoC，GPU 功耗也很少超过 15W</li>
<li><strong>内存带宽严重受限</strong>：嵌入式系统使用的 LPDDR5 带宽通常在 30-50GB/s，不到高端桌面显卡的 5%</li>
<li><strong>芯片面积寸土寸金</strong>：每增加 1mm² 的芯片面积，都意味着数百万台设备的成本上升</li>
<li><strong>散热条件恶劣</strong>：没有风扇，只能靠被动散热，温度过高还会触发降频</li>
</ul>
<p>在这些约束下，嵌入式 GPU 必须找到一条完全不同的技术路线。如果直接把桌面 GPU 的架构缩小放到嵌入式芯片中，结果必然是功耗超标、带宽不足、性能低下。正是这种现实的约束，催生了 Mali GPU 独特的架构设计。</p>
<p>Mali GPU 的设计哲学可以总结为三句话：<strong>计算尽量本地化，最大限度重用数据，减少不必要的内存访问</strong>。整个架构的每一个设计决策，从 Tiler 分片渲染到片上缓存，从执行引擎设计到内存控制器，都是围绕这三个原则展开的。</p>
<h2 id="二tiler-分片渲染mali-架构的灵魂">二、Tiler 分片渲染：Mali 架构的灵魂</h2>
<p>理解 Tiler 分片渲染（Tile-Based Rendering, TBR）是理解 Mali GPU 的第一步。这也是 Mali GPU 与桌面 GPU 最本质的区别。</p>
<h3 id="21-传统立即模式渲染的问题">2.1 传统立即模式渲染的问题</h3>
<p>让我们先看看桌面 GPU 采用的立即模式渲染（Immediate Mode Rendering, IMR）是如何工作的：</p>
<ol>
<li>应用提交一个绘制命令，包含一组图元（三角形）</li>
<li>GPU 立即处理这些图元，执行顶点变换、光栅化、片元着色</li>
<li>处理完成的像素立即写入帧缓冲内存</li>
</ol>
<p>这个流程看起来简单直接，但存在一个致命的问题：<strong>像素被反复读写的次数可能非常多</strong>。</p>
<p>想象一个复杂的 3D 场景，同一个像素位置可能被十几个三角形覆盖。在立即模式渲染中，每个三角形处理时都会读取这个像素的当前值，进行混合计算，然后写回内存。如果场景中有大量重叠的几何体，同一个像素位置可能被读写几十次。每次读写都意味着一次显存访问，每次访问都消耗宝贵的带宽和能量。</p>
<p>在桌面 GPU 上，这个问题被&quot;暴力解决&quot;了——使用超高速的 GDDR 显存和巨大的带宽。但在嵌入式平台上，这种方案根本行不通。</p>
<h3 id="22-tbr-的核心思想先分类后渲染">2.2 TBR 的核心思想：先分类，后渲染</h3>
<p>分片渲染的思想非常朴素：既然同一个像素会被多次访问，那为什么不把这些计算都放到高速的片上缓存中完成，最后只往内存写一次呢？</p>
<p>TBR 的工作流程分为两个独立的阶段：</p>
<p><strong>第一阶段：几何处理与分片（Tiling）</strong></p>
<ol>
<li>处理所有顶点，完成坐标变换和裁剪</li>
<li>将整个屏幕划分为多个小 Tile（通常是 16×16 或 32×32 像素）</li>
<li>对每个图元（三角形），计算它覆盖了哪些 Tile</li>
<li>为每个 Tile 维护一个多边形列表，记录覆盖它的所有图元</li>
</ol>
<p><strong>第二阶段：逐 Tile 渲染（Rendering）</strong></p>
<ol>
<li>依次处理每个 Tile</li>
<li>将该 Tile 对应的多边形列表中的所有图元都加载到片上缓存</li>
<li>在片上缓存中完成该 Tile 的所有光栅化、着色、深度测试、混合等操作</li>
<li>整个 Tile 处理完成后，一次性将最终结果写回系统内存</li>
</ol>
<p>这个流程最关键的优势在于：<strong>每个像素位置只需要往内存写一次</strong>，中间的所有计算都在高速的片上 SRAM 中完成。即使一个像素被 100 个三角形覆盖，这些重叠计算都不会产生额外的内存带宽开销。</p>
<p><img alt="Mali Tiler 分片渲染工作流程" loading="lazy" src="/images/mali-tiler-rendering-pipeline.svg"></p>
<h3 id="23-性能收益的量化分析">2.3 性能收益的量化分析</h3>
<p>让我们用具体的数字来说明 TBR 的优势：</p>
<p>假设我们有一个 1080p 分辨率的场景，平均每个像素被 5 个三角形覆盖（过度采样率为 5）。</p>
<p><strong>立即模式渲染（IMR）的带宽消耗：</strong></p>
<ul>
<li>每个像素的颜色值：4 字节（RGBA8）</li>
<li>每个像素的深度值：4 字节（Depth32）</li>
<li>每个三角形读写一次：(4 + 4) × 2 = 16 字节/像素/三角形</li>
<li>总带宽：1920 × 1080 × 5 × 16 = 165.888 MB/帧</li>
<li>60fps 下：约 9.95 GB/s</li>
</ul>
<p><strong>分片渲染（TBR）的带宽消耗：</strong></p>
<ul>
<li>几何阶段：多边形列表写入，约 5-10 MB/帧</li>
<li>渲染阶段：每个像素只写一次，中间计算在片上</li>
<li>最终写出：1920 × 1080 × 8 = 16.5888 MB/帧</li>
<li>60fps 下：约 1.0 GB/s</li>
</ul>
<p>这就是 TBR 架构最惊人的优势：在典型场景下，内存带宽消耗可以降低 <strong>70%-90%</strong>！对于带宽极度受限的嵌入式平台来说，这种节省是决定性的。</p>
<p>当然，天下没有免费的午餐。TBR 也有自己的代价：</p>
<ol>
<li><strong>额外的几何处理开销</strong>：需要对所有图元进行分类，维护多边形列表</li>
<li><strong>延迟增加</strong>：必须等所有几何处理完成后才能开始渲染</li>
<li><strong>特殊的内存开销</strong>：需要存储多边形列表，大场景下可能达到几 MB</li>
</ol>
<p>但对于绝大多数嵌入式场景来说，这些代价都是完全值得的。带宽节省带来的功耗降低和性能提升，远远超过了这些额外开销。</p>
<h3 id="24-mali-tiler-的硬件实现">2.4 Mali Tiler 的硬件实现</h3>
<p>Mali GPU 的 Tiler 单元是一个专门的硬件模块，独立于着色器核心。它的工作流程可以进一步细化为：</p>
<p><strong>顶点处理阶段：</strong></p>
<ul>
<li>顶点着色器执行（可以由着色器核心完成）</li>
<li>图元装配：点、线、三角形</li>
<li>裁剪：移除视口外的图元</li>
<li>剔除：背面剔除、视锥体剔除</li>
</ul>
<p><strong>分片阶段：</strong></p>
<ul>
<li>包围盒测试：计算每个三角形的屏幕空间包围盒</li>
<li>Tile 分配：确定三角形覆盖哪些 Tile</li>
<li>多边形列表构建：将三角形索引添加到对应 Tile 的列表中</li>
<li>溢出处理：如果某个 Tile 的多边形列表太大，切换到&quot;超额渲染&quot;模式</li>
</ul>
<p>这里有一个容易被忽视的细节：Mali 的 Tiler 使用了**分层分片（Hierarchical Tiling）**技术。对于非常大的三角形，不会直接遍历所有可能覆盖的 Tile，而是在更高层级上进行快速排除。这种优化可以显著减少分片阶段的计算量。</p>
<p>另外一个重要特性是<strong>多边形列表的压缩</strong>。Mali GPU 会对多边形列表进行实时压缩存储，进一步降低内存带宽消耗。这种压缩是完全硬件实现的，对开发者透明，但理解它的存在有助于解释某些性能现象。</p>
<h2 id="三理解-mali-的渲染流水线">三、理解 Mali 的渲染流水线</h2>
<p>现在我们已经理解了 TBR 的基本概念，接下来让我们看看整个渲染流水线是如何组织的。</p>
<h3 id="31-命令流处理器job-manager">3.1 命令流处理器（Job Manager）</h3>
<p>渲染的起点是命令流处理器。它负责从 CPU 接收渲染命令，解析命令序列，然后将工作分发给各个硬件单元。</p>
<p>Mali 的命令流处理器采用了**基于作业（Job-based）**的调度模型，而不是传统的固定功能流水线。这种设计有几个重要优势：</p>
<ul>
<li><strong>灵活的任务调度</strong>：不同类型的作业可以并行执行，几何处理和着色计算可以同时进行</li>
<li><strong>动态负载均衡</strong>：根据当前工作负载自动调整资源分配</li>
<li><strong>容错能力</strong>：某个作业出错不会影响整个流水线</li>
</ul>
<p>命令流处理器支持多种作业类型：</p>
<ul>
<li>几何作业：顶点处理、分片</li>
<li>分片作业：逐 Tile 渲染</li>
<li>计算作业：OpenCL / Vulkan Compute</li>
<li>写出作业：像素合并、帧缓冲输出</li>
</ul>
<p>这种基于作业的架构也是现代 GPU 的共同特征，无论是 Mali、Adreno 还是最新的 NVIDIA 架构，都采用了类似的设计。</p>
<h3 id="32-几何处理流水线">3.2 几何处理流水线</h3>
<p>几何处理是渲染的第一个阶段，负责将 3D 世界坐标转换为 2D 屏幕坐标。</p>
<p>Mali 的几何流水线包含以下阶段：</p>
<ol>
<li><strong>顶点获取</strong>：从内存读取顶点数据（位置、法向、纹理坐标等）</li>
<li><strong>顶点着色</strong>：执行顶点着色器，进行坐标变换</li>
<li><strong>曲面细分</strong>（可选）：Tessellation 着色器执行</li>
<li><strong>几何着色</strong>（可选）：生成或丢弃图元</li>
<li><strong>图元装配</strong>：组装成点、线、三角形</li>
<li><strong>裁剪</strong>：移除视口外的部分</li>
<li><strong>剔除</strong>：背面剔除、视锥体剔除</li>
<li><strong>屏幕坐标变换</strong>：转换到像素坐标空间</li>
</ol>
<p>一个重要的优化点是：<strong>Mali 会在几何阶段尽可能早地剔除不可见的图元</strong>。如果一个三角形完全在视口外，或者是背面，它会被直接丢弃，不会进入后续的分片阶段。这种提前剔除可以显著减少分片的工作量。</p>
<p>这也是为什么我们常说&quot;几何阶段的优化往往收益最大&quot;——在流水线早期减少工作量，可以避免后续所有阶段的无效计算。</p>
<h3 id="33-分片与渲染的解耦">3.3 分片与渲染的解耦</h3>
<p>在 TBR 架构中，几何处理和像素渲染是两个完全解耦的阶段。这意味着：</p>
<ul>
<li>可以先完成所有几何处理，生成完整的多边形列表，然后再开始渲染</li>
<li>也可以采用流式处理，一边进行几何分片，一边开始渲染已经完成的 Tile</li>
</ul>
<p>Mali GPU 实际上采用了<strong>混合模式</strong>：对于简单场景，会等所有几何处理完成后再渲染；对于复杂场景，会分批次处理，避免多边形列表占用过多内存。</p>
<p>这种解耦带来了一个有趣的性能特征：在 Mali GPU 上，几何负载和像素负载的性能瓶颈是相对独立的。如果你看到几何处理占用了大量时间，那么优化顶点着色器、减少 Draw Call 数量会是最有效的手段；如果瓶颈在渲染阶段，那么优化片元着色器、降低分辨率会更有效果。</p>
<p>（第一部分完，约 2700 字）</p>
<h2 id="四着色器核心架构从-midgard-到-valhall">四、着色器核心架构：从 Midgard 到 Valhall</h2>
<p>如果说 Tiler 是 Mali GPU 的骨架，那么着色器核心就是它的心脏。着色器核心的设计直接决定了 GPU 的计算能力和能效比。Mali GPU 的着色器架构经历了三代重大演进，每一代都代表了设计理念的重要转变。</p>
<h3 id="41-midgard-架构mali-t6xxt7xxt8xx">4.1 Midgard 架构（Mali-T6xx/T7xx/T8xx）</h3>
<p>Midgard 是 Mali 的第一代统一着色器架构，于 2012 年随 Mali-T604 发布。它的核心设计是**&ldquo;四向 VLIW + 四线程&rdquo;**的执行模型。</p>
<p>每个 Midgard 执行引擎包含：</p>
<ul>
<li>4 个算术管线（ALU Pipe）</li>
<li>1 个加载/存储管线（Load/Store Pipe）</li>
<li>1 个纹理管线（Texture Pipe）</li>
<li>4 个硬件线程槽，每个线程可以独立调度</li>
</ul>
<p>VLIW（超长指令字）的设计思想是让编译器在编译时就把多个操作打包到一条指令中，硬件不需要复杂的动态调度逻辑。这种设计的优点是硬件简单、功耗低，但缺点是对编译器要求极高，如果编译器无法找到足够的并行性，硬件利用率就会很低。</p>
<p>在实际应用中，Midgard 架构的问题逐渐暴露：</p>
<ul>
<li>着色器代码中往往存在大量的数据依赖，很难填满 4 个 ALU 槽</li>
<li>分支密集的代码几乎无法利用 VLIW 并行性</li>
<li>不同类型的工作负载（图形 vs 计算）利用率差异巨大</li>
</ul>
<p>这些问题促使 ARM 开始了下一代架构的研发。</p>
<h3 id="42-bifrost-架构mali-g3xg5xg7x">4.2 Bifrost 架构（Mali-G3x/G5x/G7x）</h3>
<p>Bifrost 架构于 2016 年随 Mali-G71 发布，标志着 Mali 从 VLIW 转向了**四向 SIMD + 波浪前端（Warp Frontend）**的设计。</p>
<p>Bifrost 的核心改进包括：</p>
<ul>
<li>抛弃 VLIW，采用更灵活的 SIMD 执行模型</li>
<li>引入波浪（Warp）调度：16 个线程组成一个 Wave，同步执行</li>
<li>统一的指令集：图形和计算使用相同的 ISA</li>
<li>四倍的寄存器文件：每个线程可用的寄存器数量翻倍</li>
</ul>
<p>Bifrost 架构最大的优势是<strong>硬件利用率显著提升</strong>。SIMD 模型不需要编译器找到指令级并行，只要有足够的数据并行（这在图形和计算工作负载中非常普遍），就能填满硬件管线。而且波浪调度可以很好地隐藏内存延迟——当一个 Wave 等待内存时，硬件可以立即切换到另一个 Wave 执行。</p>
<p>从 Bifrost 开始，Mali GPU 的通用计算能力有了质的飞跃。OpenCL 工作负载的性能相比 Midgard 提升了 2-3 倍，这也是为什么 Mali GPU 开始被广泛用于边缘 AI 推理加速。</p>
<h3 id="43-valhall-架构mali-g57g68g78g710g715g720">4.3 Valhall 架构（Mali-G57/G68/G78/G710/G715/G720）</h3>
<p>Valhall 是 Mali 的最新架构，首次亮相于 2019 年的 Mali-G57。Valhall 代表了 Mali 架构的一次全面重塑，核心设计理念是**&ldquo;可扩展的执行引擎&rdquo;**。</p>
<p>Valhall 的关键改进：</p>
<p><strong>1. 解耦的执行管线</strong></p>
<ul>
<li>FMA（乘加）单元和 SFU（特殊功能单元）完全独立调度</li>
<li>不再强制绑定到固定大小的 Wave</li>
<li>每个执行单元可以独立接受工作分配</li>
</ul>
<p><strong>2. 矩阵计算引擎</strong></p>
<ul>
<li>从 Mali-G710 开始引入专用的矩阵乘法单元</li>
<li>支持 FP16/BF16/INT8 矩阵运算</li>
<li>AI 推理性能提升 2-4 倍</li>
</ul>
<p><strong>3. 可变速率着色（VRS）</strong></p>
<ul>
<li>硬件支持每 Tile 的着色率调节</li>
<li>对于细节不敏感的区域可以降低着色频率</li>
<li>性能提升 20-30%，视觉质量几乎不受影响</li>
</ul>
<p><strong>4. 光线追踪加速</strong></p>
<ul>
<li>Mali-G715 首次引入硬件光线追踪单元</li>
<li>支持 BVH 遍历、相交测试</li>
<li>实时光线追踪在移动端成为可能</li>
</ul>
<p>Valhall 架构最值得关注的变化是<strong>图形和计算的深度融合</strong>。在 Valhall 之前，图形管线和计算管线虽然共享硬件，但仍然是相对独立的编程模型。而在 Valhall 架构中，整个 GPU 被设计为一个统一的计算平台——渲染本质上只是一种特殊形式的计算。这种设计理念的转变，为 Mali GPU 在 AI、科学计算等领域的应用打开了更广阔的空间。</p>
<h2 id="五执行模型理解-wavefront-与隐藏延迟">五、执行模型：理解 Wavefront 与隐藏延迟</h2>
<p>要写出高效的 Mali 着色器代码，首先必须理解它的执行模型。很多从 CPU 开发转向 GPU 开发的工程师，最容易犯的错误就是用 CPU 的思维方式来写 GPU 代码。</p>
<h3 id="51-simt单指令多线程">5.1 SIMT：单指令多线程</h3>
<p>Mali GPU 采用的是 **SIMT（单指令多线程）**执行模型。这是一种介于 SIMD 和 MIMD 之间的模型，也是现代 GPU 的标准执行模型。</p>
<p>SIMT 的核心思想是：</p>
<ul>
<li>大量线程以&quot;波浪&quot;（Wave / Warp / Wavefront）为单位组织</li>
<li>同一个 Wave 中的所有线程执行完全相同的指令</li>
<li>但每个线程有自己独立的寄存器和程序计数器</li>
<li>线程可以有不同的执行路径（分支）</li>
</ul>
<p>在 Mali Bifrost 和 Valhall 架构中，一个 Wave 包含 <strong>16 个线程</strong>。这意味着当你写一个片元着色器时，实际上它会同时在 16 个像素上并行执行。</p>
<p>这种执行模型带来了几个重要的推论：</p>
<p><strong>推论 1：分支是性能杀手，但不是绝对的</strong></p>
<p>如果同一个 Wave 中的不同线程走了不同的分支路径，GPU 会串行执行所有分支，然后丢弃不需要的结果。这被称为&quot;分支发散&quot;（Branch Divergence），会导致硬件利用率下降。</p>
<p>但分支发散的影响是<strong>概率性的</strong>：如果分支条件在 Wave 内部是一致的（比如整个 Wave 的所有像素都满足条件，或者都不满足），那么就不会有发散惩罚。这也是为什么基于 Tile 的分支通常性能很好——同一个 Tile 内的像素往往有相似的特征。</p>
<p><strong>推论 2：内存延迟通过多线程隐藏，而不是通过缓存</strong></p>
<p>在 CPU 上，我们依赖缓存来隐藏内存延迟。如果缓存命中率高，内存访问只需要几个时钟周期；如果缓存不命中，CPU 会停顿几百个周期等待数据。</p>
<p>而在 GPU 上，隐藏内存延迟的主要机制是<strong>快速的上下文切换</strong>。当一个 Wave 发起内存请求后，硬件会立即切换到另一个就绪的 Wave 执行，而不是等待数据返回。只要有足够多的就绪 Wave，GPU 就能一直保持忙碌，内存延迟就被完全隐藏了。</p>
<p>这就是为什么 GPU 需要如此多的寄存器——每个硬件线程都需要自己的寄存器文件。Mali GPU 的每个执行引擎通常支持 128-256 个并发硬件线程，这意味着即使 90% 的线程都在等待内存，仍然有足够的线程可以保持硬件满载。</p>
<p><strong>推论 3：寄存器使用量决定了最大并行度</strong></p>
<p>这里有一个重要的权衡：着色器使用的寄存器越多，能同时驻留的 Wave 数量就越少。如果 Wave 数量太少，就无法有效隐藏内存延迟，GPU 会出现&quot;气泡&quot;（空闲周期）。</p>
<p>这就是为什么在 Mali GPU 上，<strong>减少寄存器使用量往往比减少指令数量更能提升性能</strong>。一个使用 32 个寄存器的着色器可能比一个使用 64 个寄存器但指令数少 20% 的着色器运行得更快。</p>
<h3 id="52-mali-的指令调度机制">5.2 Mali 的指令调度机制</h3>
<p>让我们更具体地看看 Valhall 架构的指令调度是如何工作的。</p>
<p>每个 Valhall 执行引擎有多个独立的功能单元：</p>
<ul>
<li><strong>FMA 单元</strong>：执行乘加、加减、比较等基础算术运算</li>
<li><strong>SFU 单元</strong>：执行超越函数（sin、cos、exp、log 等）和特殊操作</li>
<li><strong>Load/Store 单元</strong>：处理内存读写</li>
<li><strong>Texture 单元</strong>：处理纹理采样</li>
</ul>
<p>这些单元是完全解耦的，可以并行操作。调度器会在每个时钟周期检查所有就绪的指令，然后分配到对应的功能单元。这种设计被称为**&ldquo;乱序执行的超标量架构&rdquo;**，但与 CPU 的乱序执行不同，GPU 的调度粒度是 Wave 而不是单个指令。</p>
<p>这种架构的一个有趣特性是：<strong>不同类型的指令可以完全重叠执行</strong>。例如，一个 Wave 可以在 FMA 单元执行算术运算的同时，另一个 Wave 在 SFU 单元计算三角函数，第三个 Wave 在等待纹理采样返回。只要有足够多的独立工作，所有功能单元都可以同时保持忙碌。</p>
<p>这也是为什么 GPU 的理论峰值 FLOPS 很难达到——你需要足够多样化的工作负载来同时填满所有功能单元。纯算术的工作负载只能利用 FMA 单元，纯纹理采样的工作负载只能利用纹理单元，只有平衡的工作负载才能接近理论峰值。</p>
<h2 id="六内存层次结构从寄存器到-ddr">六、内存层次结构：从寄存器到 DDR</h2>
<p>内存系统是 GPU 性能最关键的瓶颈之一。Mali GPU 的内存层次结构经过精心设计，每一级都有明确的性能目标和使用场景。</p>
<h3 id="61-寄存器文件最快的存储">6.1 寄存器文件：最快的存储</h3>
<p>寄存器是 GPU 中最快的存储，访问延迟只有 1-2 个时钟周期。Mali GPU 的每个执行引擎都有一个巨大的寄存器文件——通常是 128KB 到 512KB，比很多 CPU 的 L1 缓存还大。</p>
<p>寄存器的分配是<strong>静态的</strong>——在着色器编译时就确定了每个线程需要多少寄存器，硬件在 Wave 启动时一次性分配。这种静态分配的优点是不需要复杂的缓存一致性逻辑，但缺点是寄存器使用量直接影响并发度。</p>
<p>一个重要的优化技巧是<strong>寄存器溢出检测</strong>。如果着色器使用的寄存器超过了硬件限制，编译器会把多余的寄存器&quot;溢出&quot;到栈内存中。这会导致严重的性能下降——本来应该是寄存器访问变成了内存访问。在 Mali Performance Counters 中可以找到<code>Registers Spilled</code>计数器，如果这个值大于零，就说明需要优化寄存器使用。</p>
<h3 id="62-l1slc-缓存着色器核心本地缓存">6.2 L1/SLC 缓存：着色器核心本地缓存</h3>
<p>Mali GPU 的每个着色器核心都有自己的 L1 缓存（在 Valhall 架构中称为系统级缓存 SLC）。典型大小是 64KB 或 128KB 每个核心。</p>
<p>L1 缓存的特点：</p>
<ul>
<li><strong>读写延迟低</strong>：约 10-20 个时钟周期</li>
<li><strong>核心私有</strong>：每个核心的缓存是独立的，不与其他核心共享</li>
<li><strong>一致性弱</strong>：不需要维持多核一致性，所以效率很高</li>
<li><strong>优先加载纹理</strong>：纹理采样会优先使用 L1 缓存</li>
</ul>
<p>L1 缓存对于 Mali GPU 的性能非常关键。因为纹理采样通常是带宽最大的操作，良好的纹理局部性可以显著降低对 L2 缓存和主存的压力。</p>
<h3 id="63-l2-统一缓存全-gpu-共享">6.3 L2 统一缓存：全 GPU 共享</h3>
<p>所有 Mali GPU 都有一个统一的 L2 缓存，由所有着色器核心共享。L2 缓存的大小从入门级的 256KB 到旗舰级的 4MB 不等。</p>
<p>L2 缓存的设计有几个重要特点：</p>
<p><strong>1. 包含式设计</strong></p>
<ul>
<li>L2 缓存包含了所有 L1 缓存的数据</li>
<li>这简化了一致性协议，减少了冗余存储</li>
</ul>
<p><strong>2. 带宽优先级</strong></p>
<ul>
<li>纹理读取 &gt; 着色器读写 &gt; 帧缓冲写出</li>
<li>这种优先级设计保证了最关键的操作能优先获得带宽</li>
</ul>
<p><strong>3. 压缩支持</strong></p>
<ul>
<li>L2 缓存支持多种压缩格式</li>
<li>颜色缓冲压缩、深度缓冲压缩、纹理压缩</li>
<li>可以有效减少实际的内存传输量</li>
</ul>
<p>L2 缓存是 Mali GPU 内存层次结构中最重要的一级。大部分工作负载的性能瓶颈都在 L2 缓存的命中率和带宽上。优化 L2 命中率的常见方法包括：</p>
<ul>
<li>改善数据访问的空间局部性</li>
<li>减少不必要的全局内存读写</li>
<li>使用合适的纹理压缩格式</li>
<li>避免随机内存访问模式</li>
</ul>
<h3 id="64-主存最终的瓶颈">6.4 主存：最终的瓶颈</h3>
<p>主存（通常是 LPDDR4X 或 LPDDR5）是内存层次结构的最后一级，也是最慢的一级。访问主存的延迟通常在 200-400 个时钟周期，是寄存器访问的几百倍。</p>
<p>但更重要的是<strong>带宽限制</strong>。即使是最新的 LPDDR5-6400，理论带宽也只有 51.2 GB/s，实际可用带宽往往只有理论值的 60-70%。而且系统中的其他单元（CPU、NPU、显示控制器）也在竞争这部分带宽。</p>
<p>这就是为什么 TBR 架构如此重要——它从根本上减少了需要访问主存的数据量。如果没有 TBR，移动 GPU 的性能会下降 5-10 倍。</p>
<p>一个常见的误区是认为&quot;只要优化了 ALU 指令数就能提升性能&quot;。实际上，在大多数嵌入式图形应用中，<strong>70% 以上的性能瓶颈都在内存系统上</strong>。同样的渲染效果，一个内存友好的实现可能比一个计算优化但内存模式糟糕的实现快好几倍。</p>
<h3 id="65-内存一致性模型">6.5 内存一致性模型</h3>
<p>最后一个需要理解的概念是内存一致性。与 CPU 的强一致性模型不同，GPU 的内存一致性模型要弱得多，这对多线程编程有重要影响。</p>
<p>在 Mali GPU 上：</p>
<ul>
<li><strong>同一个 Wave 内的线程</strong>：可以安全地通过共享内存通信，但需要屏障（barrier）</li>
<li><strong>同一个核心内的 Wave</strong>：L1 缓存是一致的，但仍然需要显式同步</li>
<li><strong>不同核心的 Wave</strong>：L2 缓存是一致的，但需要更昂贵的同步操作</li>
<li><strong>CPU 和 GPU 之间</strong>：需要显式的内存屏障和缓存刷新</li>
</ul>
<p>理解内存一致性模型对于编写正确的计算着色器和多阶段渲染流程至关重要。很多难以调试的图形 Bug，根源都在于对内存一致性的错误假设。</p>
<p>（第二部分完，约 3200 字）</p>
<h2 id="七性能优化方法论从架构理解到代码实践">七、性能优化方法论：从架构理解到代码实践</h2>
<p>理解 Mali GPU 架构的最终目的是为了写出更高效的代码。在这一部分，我们将介绍一套系统的性能优化方法论，结合具体的代码示例，帮助你将架构知识转化为实际的性能提升。</p>
<h3 id="71-性能分析的基本流程">7.1 性能分析的基本流程</h3>
<p>优化的第一步永远是<strong>测量</strong>，而不是猜测。Mali GPU 提供了丰富的性能计数器（Performance Counters），可以精确地告诉你瓶颈在哪里。</p>
<p>标准的优化流程应该是：</p>
<ol>
<li><strong>建立性能基线</strong>：记录当前的帧率、渲染时间、带宽使用等指标</li>
<li><strong>使用性能计数器定位瓶颈</strong>：是几何阶段、分片阶段、还是着色阶段？</li>
<li><strong>分析具体原因</strong>：是 ALU 瓶颈、带宽瓶颈、还是分支发散？</li>
<li><strong>实施针对性优化</strong>：根据瓶颈类型选择合适的优化策略</li>
<li><strong>验证优化效果</strong>：对比优化前后的性能指标，确认收益</li>
<li><strong>迭代</strong>：回到步骤 2，寻找下一个瓶颈</li>
</ol>
<p>最常见的错误是上来就盲目优化，比如把所有着色器都改成半精度，或者无差别地减少 Draw Call。这样的优化往往收效甚微，甚至可能引入新的问题。</p>
<h3 id="72-tbr-友好的渲染实践">7.2 TBR 友好的渲染实践</h3>
<p>既然 TBR 是 Mali 架构的核心，那么最有效的优化自然是让你的渲染流程尽可能&quot;TBR 友好&quot;。</p>
<p><strong>实践 1：避免逐帧的帧缓冲读回</strong></p>
<p>在立即模式渲染中，读取帧缓冲数据可能只是一次内存访问；但在 TBR 架构中，这会强制整个渲染流水线刷新——所有正在处理的 Tile 都必须立即写出到内存，然后才能开始读取。这个刷新操作的开销可能高达几毫秒。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-glsl" data-lang="glsl"><span class="line"><span class="cl"><span class="c1">// ❌ 不好的做法：每帧多次读取帧缓冲</span>
</span></span><span class="line"><span class="cl"><span class="k">vec4</span> <span class="n">currentPixel</span> <span class="o">=</span> <span class="n">texture2D</span><span class="p">(</span><span class="n">framebufferTex</span><span class="p">,</span> <span class="n">v_texCoord</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></code></pre></div><p>如果你确实需要读-修改-写操作，考虑使用<code>glCopyTexImage2D</code>而不是<code>glReadPixels</code>，前者可以在 GPU 内部完成数据传输，不需要经过 CPU。</p>
<p><strong>实践 2：合理控制多边形数量</strong></p>
<p>虽然 TBR 可以很好地处理过度绘制，但多边形数量过多仍然会增加分片阶段的负担。每个三角形都需要进行包围盒计算、Tile 分配、写入多边形列表等操作。</p>
<p>一个实用的经验法则是：</p>
<ul>
<li>移动端 3D 应用：每帧 5-10 万个三角形是比较合理的上限</li>
<li>2D UI 应用：每帧 1-2 万个三角形</li>
</ul>
<p>超过这个数量，分片阶段就可能成为瓶颈。</p>
<p><strong>实践 3：谨慎使用 discard 和 alpha to coverage</strong></p>
<p><code>discard</code> 语句（或者 <code>alpha_test</code>）会强制 Mali GPU 禁用早期深度测试（Early-Z）。因为 GPU 无法在着色器执行前知道哪个片元会被丢弃，深度测试必须推迟到着色器执行之后。这会导致大量无效的着色器执行。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-glsl" data-lang="glsl"><span class="line"><span class="cl"><span class="c1">// ❌ 性能代价高：强制禁用 Early-Z</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="n">alpha</span> <span class="o">&lt;</span> <span class="mf">0.5</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">discard</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">// ✅ 更好的做法：使用 alpha blending，或者重新设计渲染顺序</span>
</span></span></code></pre></div><p>如果确实需要透明度裁剪，尽量把这些物体放在最后渲染，这样它们不会挡住其他物体，Early-Z 仍然可以对其他物体生效。</p>
<p><strong>实践 4：避免渲染区域的频繁变化</strong></p>
<p>每次改变渲染区域（<code>glScissor</code>、改变帧缓冲大小）都可能导致 Tiler 重置内部状态。如果必须使用裁剪区域，尽量保持裁剪区域的大小和位置相对稳定。</p>
<h3 id="73-着色器优化实战">7.3 着色器优化实战</h3>
<p>着色器优化是 Mali GPU 优化中最复杂也最有技巧性的部分。让我们通过具体的例子来说明关键的优化原则。</p>
<p><strong>原则 1：优先使用 mediump 精度</strong></p>
<p>Mali GPU 的 ALU 设计是为 16 位浮点运算优化的。使用 <code>mediump</code> 不仅可以减少寄存器使用量（增加并发 Wave 数量），还可以直接提升 ALU 的吞吐量。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-glsl" data-lang="glsl"><span class="line"><span class="cl"><span class="c1">// ❌ 不必要的高精度</span>
</span></span><span class="line"><span class="cl"><span class="k">highp</span> <span class="k">vec3</span> <span class="n">lightDir</span> <span class="o">=</span> <span class="n">normalize</span><span class="p">(</span><span class="n">u_lightPos</span> <span class="o">-</span> <span class="n">v_worldPos</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="k">highp</span> <span class="k">float</span> <span class="n">diff</span> <span class="o">=</span> <span class="n">max</span><span class="p">(</span><span class="n">dot</span><span class="p">(</span><span class="n">normal</span><span class="p">,</span> <span class="n">lightDir</span><span class="p">),</span> <span class="mf">0.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">// ✅ 大多数光照计算用 mediump 足够</span>
</span></span><span class="line"><span class="cl"><span class="k">mediump</span> <span class="k">vec3</span> <span class="n">lightDir</span> <span class="o">=</span> <span class="n">normalize</span><span class="p">(</span><span class="n">u_lightPos</span> <span class="o">-</span> <span class="n">v_worldPos</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="k">mediump</span> <span class="k">float</span> <span class="n">diff</span> <span class="o">=</span> <span class="n">max</span><span class="p">(</span><span class="n">dot</span><span class="p">(</span><span class="n">normal</span><span class="p">,</span> <span class="n">lightDir</span><span class="p">),</span> <span class="mf">0.0</span><span class="p">);</span>
</span></span></code></pre></div><p>什么情况必须用 <code>highp</code>：</p>
<ul>
<li>顶点位置计算（特别是大场景的坐标变换）</li>
<li>纹理坐标插值（大纹理时）</li>
<li>深度值计算</li>
<li>需要超过 10 位精度的标量计算</li>
</ul>
<p>什么情况用 <code>mediump</code> 足够：</p>
<ul>
<li>颜色计算</li>
<li>大多数光照计算</li>
<li>法线变换</li>
<li>纹理采样后的颜色处理</li>
</ul>
<p>一个典型的片元着色器，全部使用 <code>mediump</code> 可能比全部使用 <code>highp</code> 快 30-50%。</p>
<p><strong>原则 2：减少寄存器使用量</strong></p>
<p>如前所述，寄存器使用量直接影响能同时驻留的 Wave 数量。减少寄存器的方法包括：</p>
<ul>
<li>复用变量，避免声明不必要的临时变量</li>
<li>把复杂计算拆分成更小的函数（函数返回值可以立即使用，不需要存储到寄存器）</li>
<li>使用 <code>const</code> 修饰编译期常量</li>
<li>避免展开大循环（循环展开会增加寄存器压力）</li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-glsl" data-lang="glsl"><span class="line"><span class="cl"><span class="c1">// ❌ 寄存器压力大</span>
</span></span><span class="line"><span class="cl"><span class="k">vec4</span> <span class="n">temp1</span> <span class="o">=</span> <span class="n">texture2D</span><span class="p">(</span><span class="n">u_tex1</span><span class="p">,</span> <span class="n">uv</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="k">vec4</span> <span class="n">temp2</span> <span class="o">=</span> <span class="n">texture2D</span><span class="p">(</span><span class="n">u_tex2</span><span class="p">,</span> <span class="n">uv</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="k">vec4</span> <span class="n">temp3</span> <span class="o">=</span> <span class="n">texture2D</span><span class="p">(</span><span class="n">u_tex3</span><span class="p">,</span> <span class="n">uv</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="k">vec4</span> <span class="n">result</span> <span class="o">=</span> <span class="n">temp1</span> <span class="o">*</span> <span class="mf">0.3</span> <span class="o">+</span> <span class="n">temp2</span> <span class="o">*</span> <span class="mf">0.5</span> <span class="o">+</span> <span class="n">temp3</span> <span class="o">*</span> <span class="mf">0.2</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="k">vec4</span> <span class="n">result</span> <span class="o">=</span> <span class="n">texture2D</span><span class="p">(</span><span class="n">u_tex1</span><span class="p">,</span> <span class="n">uv</span><span class="p">)</span> <span class="o">*</span> <span class="mf">0.3</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="n">result</span> <span class="o">+=</span> <span class="n">texture2D</span><span class="p">(</span><span class="n">u_tex2</span><span class="p">,</span> <span class="n">uv</span><span class="p">)</span> <span class="o">*</span> <span class="mf">0.5</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="n">result</span> <span class="o">+=</span> <span class="n">texture2D</span><span class="p">(</span><span class="n">u_tex3</span><span class="p">,</span> <span class="n">uv</span><span class="p">)</span> <span class="o">*</span> <span class="mf">0.2</span><span class="p">;</span>
</span></span></code></pre></div><p><strong>原则 3：明智地使用分支</strong></p>
<p>分支不一定是坏事，关键是分支条件的一致性。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-glsl" data-lang="glsl"><span class="line"><span class="cl"><span class="c1">// ✅ 好的分支：整个 Wave 的条件很可能一致</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="n">u_effectEnabled</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="p">}</span> <span class="k">else</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="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="k">if</span> <span class="p">(</span><span class="n">distance</span><span class="p">(</span><span class="n">uv</span><span class="p">,</span> <span class="n">center</span><span class="p">)</span> <span class="o">&lt;</span> <span class="n">radius</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="p">}</span> <span class="k">else</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="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="k">if</span> <span class="p">(</span><span class="n">randomValue</span> <span class="o">&gt;</span> <span class="mf">0.5</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">doSomethingA</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">doSomethingB</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>如果分支确实不可避免，可以考虑使用混合（blending）来替代分支：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-glsl" data-lang="glsl"><span class="line"><span class="cl"><span class="c1">// 使用线性插值替代分支</span>
</span></span><span class="line"><span class="cl"><span class="k">float</span> <span class="n">factor</span> <span class="o">=</span> <span class="n">step</span><span class="p">(</span><span class="mf">0.5</span><span class="p">,</span> <span class="n">value</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="n">color</span> <span class="o">=</span> <span class="n">mix</span><span class="p">(</span><span class="n">colorA</span><span class="p">,</span> <span class="n">colorB</span><span class="p">,</span> <span class="n">factor</span><span class="p">);</span>
</span></span></code></pre></div><p><code>mix</code> 函数在 Mali GPU 上是单周期指令，而且不会导致分支发散，在很多情况下比 <code>if-else</code> 更快。</p>
<p><strong>原则 4：优化纹理访问模式</strong></p>
<p>纹理采样是 Mali GPU 上最大的带宽消耗者。优化纹理访问的关键是改善空间局部性。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-glsl" data-lang="glsl"><span class="line"><span class="cl"><span class="c1">// ❌ 不好的访问模式：随机采样多个不相关的纹理坐标</span>
</span></span><span class="line"><span class="cl"><span class="k">vec4</span> <span class="n">c1</span> <span class="o">=</span> <span class="n">texture2D</span><span class="p">(</span><span class="n">u_tex</span><span class="p">,</span> <span class="n">uv1</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="k">vec4</span> <span class="n">c2</span> <span class="o">=</span> <span class="n">texture2D</span><span class="p">(</span><span class="n">u_tex</span><span class="p">,</span> <span class="n">uv2</span><span class="p">);</span>  <span class="c1">// uv2 与 uv1 可能相差很远</span>
</span></span><span class="line"><span class="cl"><span class="k">vec4</span> <span class="n">c3</span> <span class="o">=</span> <span class="n">texture2D</span><span class="p">(</span><span class="n">u_tex</span><span class="p">,</span> <span class="n">uv3</span><span class="p">);</span>  <span class="c1">// uv3 可能又在另一个地方</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></span><span class="line"><span class="cl"><span class="k">vec4</span> <span class="n">color</span> <span class="o">=</span> <span class="n">texture2D</span><span class="p">(</span><span class="n">u_tex</span><span class="p">,</span> <span class="n">v_texCoord</span><span class="p">);</span>
</span></span></code></pre></div><p>其他纹理优化技巧：</p>
<ul>
<li>使用纹理压缩（ASTC、ETC2），不仅节省存储空间，还减少带宽</li>
<li>启用 mipmapping，不仅改善画质，还能减少纹理采样的带宽</li>
<li>避免过大的纹理（移动端单张纹理最好不要超过 2048×2048）</li>
<li>合理设置纹理过滤方式，<code>GL_LINEAR_MIPMAP_NEAREST</code> 通常是画质和性能的最佳平衡点</li>
</ul>
<h3 id="74-api-层面的优化">7.4 API 层面的优化</h3>
<p>最后，让我们看看 OpenGL ES/Vulkan API 层面的常见优化点。</p>
<p><strong>Draw Call 批处理</strong></p>
<p>虽然 Mali GPU 的 Draw Call 开销相对较低，但过多的 Draw Call 仍然会增加 CPU 开销，并影响 GPU 的并行度。</p>
<p>目标是每帧 100-300 个 Draw Call，超过 500 个就应该考虑优化：</p>
<ul>
<li>使用实例化渲染（<code>glDrawArraysInstanced</code>）</li>
<li>合并几何体，使用纹理图集</li>
<li>利用状态排序，减少状态切换</li>
</ul>
<p><strong>帧缓冲对象（FBO）优化</strong></p>
<p>每个 FBO 切换都会导致流水线刷新，应尽量减少 FBO 切换次数：</p>
<ul>
<li>把需要相同 FBO 的渲染批处理在一起</li>
<li>考虑重新设计渲染流程，减少渲染阶段数量</li>
<li>使用多渲染目标（MRT）减少绘制次数</li>
</ul>
<p><strong>缓冲对象管理</strong></p>
<ul>
<li>使用 <code>GL_STATIC_DRAW</code> 标记真正静态的数据</li>
<li>对于动态更新的缓冲，使用 <code>glMapBufferRange</code> 而不是 <code>glBufferSubData</code></li>
<li>避免在渲染过程中频繁分配和释放缓冲对象</li>
</ul>
<h2 id="八常见性能陷阱与解决方案">八、常见性能陷阱与解决方案</h2>
<p>在多年的 Mali GPU 开发经验中，我总结了一些最常见的性能陷阱。了解这些陷阱可以帮助你避免很多不必要的性能损失。</p>
<h3 id="陷阱-1只要减少三角形数量就能提升性能">陷阱 1：&ldquo;只要减少三角形数量就能提升性能&rdquo;</h3>
<p>很多人本能地认为，减少一半三角形数量，性能就应该提升一倍。但在 Mali GPU 上，这往往不成立。</p>
<p>原因：</p>
<ul>
<li>如果瓶颈是片元着色器（像素太多），减少三角形数量几乎没有效果</li>
<li>如果三角形已经很小（小于一个 Tile），减少数量确实有帮助</li>
<li>如果瓶颈在 Tiler，减少三角形数量会有帮助</li>
</ul>
<p><strong>解决方案</strong>：首先用性能计数器确定瓶颈在哪里，然后再针对性优化。如果确实是几何瓶颈，除了减少三角形，还可以考虑：</p>
<ul>
<li>使用更简单的顶点着色器</li>
<li>启用硬件剔除（背面剔除、视锥体剔除）</li>
<li>使用 LOD 技术，远距离物体使用更简单的模型</li>
</ul>
<h3 id="陷阱-2early-z-会自动处理过度绘制">陷阱 2：&ldquo;Early-Z 会自动处理过度绘制&rdquo;</h3>
<p>很多人认为只要从前往后渲染，Early-Z 就会自动消除过度绘制。但在 Mali GPU 上，情况更复杂。</p>
<p>Mali 的 Early-Z 是在<strong>分片阶段</strong>和<strong>着色阶段</strong>之间工作的。对于被完全遮挡的 Tile，Early-Z 可以完全跳过着色；但对于部分遮挡的 Tile，仍然需要执行着色器来确定可见性。</p>
<p>更重要的是，以下操作会<strong>禁用 Early-Z</strong>：</p>
<ul>
<li>使用 <code>discard</code> 语句</li>
<li>修改深度值（<code>gl_FragDepth</code>）</li>
<li>alpha to coverage 启用</li>
<li>某些混合模式</li>
</ul>
<p><strong>解决方案</strong>：</p>
<ul>
<li>尽量避免使用会禁用 Early-Z 的功能</li>
<li>如果必须使用，把这些物体放在渲染顺序的最后</li>
<li>考虑使用 Pre-Pass 技术：先用一个简单的着色器渲染深度，然后再渲染颜色</li>
</ul>
<h3 id="陷阱-3计算着色器和图形着色器一样快">陷阱 3：&ldquo;计算着色器和图形着色器一样快&rdquo;</h3>
<p>从 Valhall 架构开始，图形和计算确实共享同一个硬件。但这并不意味着计算着色器和图形着色器的性能是一样的。</p>
<p>图形流水线有很多专门的硬件优化：</p>
<ul>
<li>硬件光栅化器比软件光栅化快 10-100 倍</li>
<li>内置的深度测试和混合是硬件加速的</li>
<li>纹理采样器有专门的过滤硬件</li>
<li>顶点和片元的插值是专用硬件处理的</li>
</ul>
<p><strong>解决方案</strong>：不要为了&quot;统一&quot;而强行把图形任务改成计算着色器。计算着色器适合不规则的并行计算，图形流水线适合规则的光栅化任务。选择正确的工具比写聪明的代码更重要。</p>
<h3 id="陷阱-4更多的核心--线性的性能提升">陷阱 4：&ldquo;更多的核心 = 线性的性能提升&rdquo;</h3>
<p>很多旗舰 SoC 宣传自己有 10 核、14 核甚至 20 核的 Mali GPU。但实际性能提升往往不是线性的。</p>
<p>原因：</p>
<ul>
<li>Amdahl 定律：并行部分的加速受限于串行部分的比例</li>
<li>内存墙：核心越多，每个核心分到的带宽比例越低</li>
<li>Tiler 瓶颈：Tiler 单元通常是单核，成为扩展的瓶颈</li>
<li>功耗墙：核心太多会导致过热降频</li>
</ul>
<p><strong>解决方案</strong>：不要指望增加核心数量能解决所有性能问题。对于大多数嵌入式应用，4-8 个核心是性价比最好的区间。超过这个数量，应该更多地考虑算法优化，而不是堆硬件。</p>
<h2 id="九进阶方向从图形到通用计算">九、进阶方向：从图形到通用计算</h2>
<p>随着 Mali GPU 架构的演进，它正在从&quot;图形处理器&quot;转变为&quot;通用并行处理器&quot;。对于嵌入式开发者来说，这打开了很多新的可能性。</p>
<h3 id="91-边缘-ai-推理加速">9.1 边缘 AI 推理加速</h3>
<p>Mali GPU 已经成为边缘 AI 推理的重要算力来源。Valhall 架构引入的矩阵计算引擎，使得 Mali GPU 的 AI 性能可以与专用 NPU 相媲美。</p>
<p>常见的优化技术：</p>
<ul>
<li>使用 FP16 或 INT8 量化，充分利用硬件加速</li>
<li>优化内存布局，使用 NHWC 格式</li>
<li>算子融合，减少中间结果的内存读写</li>
<li>利用 Mali 的性能计数器指导优化</li>
</ul>
<p>现在主流的深度学习框架（TensorFlow Lite、ONNX Runtime、TNN）都已经针对 Mali GPU 做了深度优化。对于大多数常见的 CNN 模型，在 Mali GPU 上的推理性能可以达到 CPU 的 5-10 倍。</p>
<h3 id="92-通用计算gpgpu在嵌入式中的应用">9.2 通用计算（GPGPU）在嵌入式中的应用</h3>
<p>除了 AI，Mali GPU 还非常适合以下类型的通用计算任务：</p>
<p><strong>图像处理</strong>：</p>
<ul>
<li>图像滤波、边缘检测</li>
<li>颜色空间转换</li>
<li>图像缩放和旋转</li>
<li>特征点检测和匹配</li>
</ul>
<p><strong>信号处理</strong>：</p>
<ul>
<li>FFT（快速傅里叶变换）</li>
<li>数字滤波</li>
<li>雷达信号处理</li>
<li>音频处理</li>
</ul>
<p><strong>物理模拟</strong>：</p>
<ul>
<li>粒子系统</li>
<li>流体模拟</li>
<li>刚体动力学</li>
<li>布料模拟</li>
</ul>
<p>这些任务的共同特点是：高度数据并行，计算密集，控制流简单。正是 Mali GPU 最擅长的工作负载类型。</p>
<h3 id="93-vulkan-和新一代图形-api">9.3 Vulkan 和新一代图形 API</h3>
<p>Vulkan 代表了图形 API 的未来方向，它给了开发者更多的控制权，也为 Mali GPU 优化打开了新的可能性。</p>
<p>Vulkan 在 Mali 上的关键优势：</p>
<ul>
<li>更可预测的性能：没有驱动的&quot;魔法&quot;黑盒</li>
<li>多线程命令缓冲生成：更好地利用多核 CPU</li>
<li>显式的内存管理：可以精确控制数据布局和访问模式</li>
<li>渲染通道（Render Pass）：让驱动更好地了解渲染流程，优化 Tiler 行为</li>
</ul>
<p>特别是 Render Pass 机制，它与 Mali 的 TBR 架构天然契合。正确使用 Render Pass 可以让 Mali 驱动自动应用很多高级优化，比如 Tile 本地存储、负载/存储操作优化等。</p>
<h2 id="总结">总结</h2>
<p>Mali GPU 是一个极其精妙的架构设计，它在严格的功耗、带宽、面积约束下，实现了令人惊叹的性能和灵活性。从 Tiler 分片渲染到 Valhall 可扩展执行引擎，从 SIMT 执行模型到分层内存层次结构，每一个设计决策都体现了嵌入式系统特有的设计哲学。</p>
<p>回顾本文的核心要点：</p>
<ol>
<li><strong>理解 TBR 架构是基础</strong>：分片渲染是 Mali 区别于桌面 GPU 的最核心特征，所有优化都应该从理解 TBR 开始</li>
<li><strong>内存比计算更重要</strong>：大多数性能瓶颈在内存系统，优化数据访问模式往往比减少指令数更有效</li>
<li><strong>寄存器使用量决定并发度</strong>：减少寄存器压力可以增加 Wave 数量，更好地隐藏内存延迟</li>
<li><strong>先测量，再优化</strong>：使用性能计数器精确定位瓶颈，避免盲目优化</li>
<li><strong>图形和计算正在融合</strong>：Mali GPU 正在成为通用并行处理器，为嵌入式 AI 和高性能计算提供强大算力</li>
</ol>
<p>作为嵌入式开发者，深入理解 Mali GPU 架构不仅能帮助你写出更快的图形应用，还能让你掌握一种全新的并行编程范式。在边缘计算和 AI 加速越来越重要的今天，这种能力的价值只会不断增加。</p>
<p>Mali GPU 的故事还在继续。每一代新架构都带来了更强大的算力、更灵活的编程模型、更高效的功耗控制。作为开发者，我们需要不断学习、不断适应，才能充分利用这些强大的硬件，创造出更好的产品。</p>
<p>（全文完，约 7800 字）</p>
]]></content:encoded></item><item><title>RISC-V 架构深度解析与指令集编程实战指南</title><link>https://tech-snippets.xyz/posts/riscv-architecture-programming-guide/</link><pubDate>Mon, 11 May 2026 19:00:00 +0800</pubDate><guid>https://tech-snippets.xyz/posts/riscv-architecture-programming-guide/</guid><description>前言 2026 年，芯片架构领域正在经历一场前所未有的变革。
过去三十年，ARM 架构凭借其低功耗、高效率的设计哲学，几乎垄断了整个移动设备和嵌入式市场。而 x86 架构则凭借强大的生态和软件兼容性，牢牢占据着桌面和服务器市场的主导地位。对于普通开发者来说，芯片架构似乎是一个遥不可及的话题——那是 Intel、ARM、高通这些巨头的游戏。
但一切都在 2010 年发生了改变。
加州大学伯克利分校的 Krste Asanović 教授带领他的团队，启动了一个看似疯狂的项目：从零开始设计一个全新的指令集架构（ISA），并且将其完全开源、免费授权。这个项目就是 RISC-V，读作 &amp;ldquo;Risk-Five&amp;rdquo;。
十五年后的今天，RISC-V 已经从一个学术项目成长为撼动整个芯片产业的力量。从最低端的 IoT 微控制器，到高性能服务器 CPU；从消费电子的 AI 加速卡，到超级计算机的计算节点——RISC-V 的身影无处不在。NVIDIA 的下一代 GPU 将集成 RISC-V 核心，Intel 的代工业务正在为客户生产 RISC-V 芯片，甚至连 ARM 自己都开始布局 RISC-V 相关业务。
为什么一个开源的指令集能够引发如此巨大的产业震动？RISC-V 到底解决了什么问题？它的技术优势在哪里？普通开发者又如何参与到这场架构革命中来？
本文将带你从零开始，深入解析 RISC-V 架构的设计哲学、指令集细节、汇编编程、特权机制，最终实现一个简单的操作系统内核。无论你是嵌入式工程师、系统程序员，还是对芯片架构感兴趣的普通开发者，读完这篇文章，你都将对 RISC-V 有一个全面而深刻的理解。
一、为什么 RISC-V 能够撼动 ARM 的地位？ 在深入技术细节之前，我们需要先理解一个问题：在 ARM 和 x86 已经如此成熟的今天，为什么整个产业还要费心费力地去拥抱一个全新的架构？
答案藏在三个关键词里：成本、控制权、创新。
1.1 授权费用的天壤之别 让我们先算一笔账。如果你想设计一款基于 ARM 架构的芯片，你需要支付哪些费用？
首先是 架构授权费（Architecture License），这是使用 ARM 指令集本身的入门费。根据公开资料，ARM v8/v9 的架构授权费大约在 1000 万到 5000 万美元之间。注意，这只是让你&amp;quot;有权&amp;quot;设计兼容 ARM 指令集的 CPU，你还没有拿到任何实际的 CPU 设计。</description><content:encoded><![CDATA[<h2 id="前言">前言</h2>
<p>2026 年，芯片架构领域正在经历一场前所未有的变革。</p>
<p>过去三十年，ARM 架构凭借其低功耗、高效率的设计哲学，几乎垄断了整个移动设备和嵌入式市场。而 x86 架构则凭借强大的生态和软件兼容性，牢牢占据着桌面和服务器市场的主导地位。对于普通开发者来说，芯片架构似乎是一个遥不可及的话题——那是 Intel、ARM、高通这些巨头的游戏。</p>
<p>但一切都在 2010 年发生了改变。</p>
<p>加州大学伯克利分校的 Krste Asanović 教授带领他的团队，启动了一个看似疯狂的项目：从零开始设计一个全新的指令集架构（ISA），并且将其完全开源、免费授权。这个项目就是 RISC-V，读作 &ldquo;Risk-Five&rdquo;。</p>
<p>十五年后的今天，RISC-V 已经从一个学术项目成长为撼动整个芯片产业的力量。从最低端的 IoT 微控制器，到高性能服务器 CPU；从消费电子的 AI 加速卡，到超级计算机的计算节点——RISC-V 的身影无处不在。NVIDIA 的下一代 GPU 将集成 RISC-V 核心，Intel 的代工业务正在为客户生产 RISC-V 芯片，甚至连 ARM 自己都开始布局 RISC-V 相关业务。</p>
<p>为什么一个开源的指令集能够引发如此巨大的产业震动？RISC-V 到底解决了什么问题？它的技术优势在哪里？普通开发者又如何参与到这场架构革命中来？</p>
<p>本文将带你从零开始，深入解析 RISC-V 架构的设计哲学、指令集细节、汇编编程、特权机制，最终实现一个简单的操作系统内核。无论你是嵌入式工程师、系统程序员，还是对芯片架构感兴趣的普通开发者，读完这篇文章，你都将对 RISC-V 有一个全面而深刻的理解。</p>
<p><img alt="RISC-V 指令集架构分层设计" loading="lazy" src="/images/riscv-architecture-overview.svg"></p>
<h2 id="一为什么-risc-v-能够撼动-arm-的地位">一、为什么 RISC-V 能够撼动 ARM 的地位？</h2>
<p>在深入技术细节之前，我们需要先理解一个问题：在 ARM 和 x86 已经如此成熟的今天，为什么整个产业还要费心费力地去拥抱一个全新的架构？</p>
<p>答案藏在三个关键词里：<strong>成本、控制权、创新</strong>。</p>
<h3 id="11-授权费用的天壤之别">1.1 授权费用的天壤之别</h3>
<p>让我们先算一笔账。如果你想设计一款基于 ARM 架构的芯片，你需要支付哪些费用？</p>
<p>首先是 <strong>架构授权费（Architecture License）</strong>，这是使用 ARM 指令集本身的入门费。根据公开资料，ARM v8/v9 的架构授权费大约在 1000 万到 5000 万美元之间。注意，这只是让你&quot;有权&quot;设计兼容 ARM 指令集的 CPU，你还没有拿到任何实际的 CPU 设计。</p>
<p>如果你不想自己设计 CPU 核，而是直接使用 ARM 设计好的 Cortex-A 系列、Cortex-M 系列，你还需要支付 <strong>IP 授权费</strong>。一颗 Cortex-A76 的授权费大约在几百万美元级别。</p>
<p>最后，每卖出一颗芯片，你还要按芯片售价支付 <strong>版税（Royalty）</strong>，通常是 1% 到 2%。对于销量巨大的消费电子芯片来说，这可能是比授权费更可怕的支出。</p>
<p>粗略估算，一款中高端 ARM SoC 从设计到量产，仅 ARM 的相关费用就可能超过 1 亿美元。这还不包括你自己的研发、流片、测试成本。</p>
<p>而 RISC-V 呢？</p>
<p><strong>零授权费，零版税。</strong></p>
<p>你可以免费下载 RISC-V 的指令集规范，免费设计自己的 RISC-V CPU，免费生产和销售 RISC-V 芯片，一分钱都不用给任何人。对于芯片厂商来说，这意味着每颗芯片至少节省了几美元的成本——在毛利率只有百分之十几的芯片行业，这足以决定生死。</p>
<h3 id="12-从黑盒到白盒的控制权转移">1.2 从&quot;黑盒&quot;到&quot;白盒&quot;的控制权转移</h3>
<p>使用 ARM IP 的另一个问题是：你拿到的是一个&quot;黑盒&quot;。</p>
<p>ARM 会给你一个经过验证的 CPU 核网表，你可以把它集成到你的 SoC 里，可以调整一些缓存大小、总线宽度之类的参数，但你无法修改 CPU 内部的设计。你不知道流水线是怎么排布的，不知道分支预测器的具体算法，不知道指令解码器的细节——ARM 不会告诉你这些。</p>
<p>这对于大多数公司来说可能不是问题。但对于那些有差异化竞争需求的公司来说，这就是致命的缺陷。如果你想给 CPU 加一条专用的 AI 指令，如果你想优化异常处理的 latency，如果你想调整缓存策略——对不起，ARM 不允许你这么做。</p>
<p>RISC-V 则完全不同。你拥有 100% 的控制权。你可以从零开始设计 CPU 核，可以任意添加自定义指令，可以深度定制微架构的每一个细节。华为的香山处理器、阿里的玄铁系列、中科院的香山处理器，都是基于 RISC-V 进行深度定制的产物。</p>
<p>这种控制权的转移，对于中国芯片产业来说意义尤为重大。在美国持续加码的芯片出口管制下，RISC-V 成了不受制于人的技术路线。这也是为什么中国成为了 RISC-V 最坚定的支持者和最大的贡献者。</p>
<h3 id="13-模块化设计带来的创新加速度">1.3 模块化设计带来的创新加速度</h3>
<p>RISC-V 最具革命性的设计理念，是它的 <strong>模块化扩展机制</strong>。</p>
<p>传统的指令集架构是&quot;全有或全无&quot;的。如果你声称兼容 ARMv8，那么你必须实现 ARMv8 规范定义的所有几百条指令——不管你用不用得上。一个简单的 IoT 微控制器也必须支持浮点数、SIMD、虚拟化这些它永远不会用到的功能，平白无故地增加了芯片面积和功耗。</p>
<p>RISC-V 采取了完全不同的思路。它定义了一个极小的 <strong>基础指令集（RV32I 或 RV64I）</strong>，只有 40 多条指令。任何 RISC-V 兼容的处理器都必须实现这个基础指令集。在此之上，你可以根据需要选择性地实现各种扩展：</p>
<ul>
<li><strong>M</strong> 扩展：整数乘除法指令</li>
<li><strong>A</strong> 扩展：原子操作指令</li>
<li><strong>F</strong> 扩展：单精度浮点指令</li>
<li><strong>D</strong> 扩展：双精度浮点指令</li>
<li><strong>C</strong> 扩展：压缩指令（16位指令编码）</li>
<li><strong>V</strong> 扩展：向量运算指令</li>
</ul>
<p>一个 IoT 微控制器可能只需要实现 RV32I + M + C，总共不到 60 条指令。而一个高性能应用处理器则可以实现完整的 RV64GCV（G = I + M + A + F + D）。</p>
<p>这种模块化设计带来了两个巨大的优势：</p>
<p>第一，<strong>小芯片可以真正做小</strong>。一个 RV32I 的 CPU 核用 Verilog 实现只需要几千行代码，在 28nm 工艺下占用的面积不到 0.01 平方毫米。这意味着你可以在一颗芯片里集成几十甚至上百个这样的小核心，用于 IoT、传感器网络、边缘计算等场景。</p>
<p>第二，<strong>创新的门槛被极大降低</strong>。在 RISC-V 出现之前，如果你想实验一种新的指令集扩展，你需要说服 ARM 把它加入规范，或者花费数千万美元获得架构授权自己改。而现在，你只需要修改你的 RISC-V CPU 核的 Verilog 代码，重新编译一下就能实验——整个过程可能只需要几天时间。</p>
<p>正是这种开放、自由、低成本的创新环境，吸引了全世界的开发者和公司投入到 RISC-V 生态的建设中。大学用它来教授计算机体系结构课程，创业公司用它来设计专用的 AI 加速芯片，大公司用它来摆脱对 ARM 的依赖。</p>
<p>这就是 RISC-V 真正的力量：它不是一个比 ARM 更快、更高效的架构（至少现在还不是），而是一个比 ARM 更开放、更民主、更能激发创新的平台。</p>
<h2 id="二risc-v-指令集架构的核心设计哲学">二、RISC-V 指令集架构的核心设计哲学</h2>
<p>理解了 RISC-V 出现的历史背景之后，让我们深入到技术层面，看看 RISC-V 的设计到底有哪些独到之处。</p>
<p>RISC-V 的设计者们在一开始就确立了几条核心的设计原则。这些原则贯穿了整个指令集的设计，是理解 RISC-V 的关键。</p>
<h3 id="21-简单性是最高优先级">2.1 简单性是最高优先级</h3>
<p>RISC-V 的第一条设计原则是：<strong>简单性高于一切。</strong></p>
<p>让我们用一个具体的例子来说明。所有的现代指令集都有条件分支指令，用来实现 if-else、for、while 这些程序结构。让我们看看不同架构是如何设计条件分支的：</p>
<p><strong>ARM AArch64</strong> 有大约 30 条不同的分支指令：</p>
<ul>
<li>B.cond：通用条件分支（EQ、NE、LT、GT、LE、GE&hellip;）</li>
<li>CBZ/CBNZ：比较零然后分支</li>
<li>TBZ/TBNZ：测试位然后分支</li>
<li>B：无条件分支</li>
<li>BL：带链接的分支（函数调用）</li>
<li>BR：寄存器间接分支</li>
<li>RET：函数返回</li>
</ul>
<p><strong>x86-64</strong> 更是夸张，光是条件跳转指令就有几十条，还有各种基于标志位的复杂组合。</p>
<p><strong>RISC-V</strong> 有多少条分支指令？<strong>6 条。</strong> 仅此而已：</p>
<ul>
<li>BEQ：等于则分支</li>
<li>BNE：不等于则分支</li>
<li>BLT：小于则分支（有符号）</li>
<li>BGE：大于等于则分支（有符号）</li>
<li>BLTU：小于则分支（无符号）</li>
<li>BGEU：大于等于则分支（无符号）</li>
</ul>
<p>就这么多。没有&quot;测试位分支&quot;，没有&quot;比较零分支&quot;，没有各种花哨的变体。所有的条件判断都被简化为&quot;比较两个寄存器，然后根据结果分支&quot;。</p>
<p>这种简化带来的好处是巨大的。CPU 的分支预测单元只需要处理 6 种不同的分支指令格式，而不是几十种。指令解码器的逻辑大大简化。编译器的后端也更容易实现。</p>
<p>当然，这种简化是有代价的。在某些情况下，RISC-V 需要两条指令来完成 ARM 一条指令就能完成的事情。但是 RISC-V 的设计者们认为：<strong>硬件简单带来的整体收益，远远超过了个别指令带来的微小性能损失。</strong></p>
<p>这是一个非常重要的权衡。过去几十年，CPU 设计者们一直在往指令集里加各种复杂的指令，希望通过一条指令做更多的事情来提升性能。但 RISC-V 的设计者们反其道而行之：他们把指令集做的尽可能简单，把复杂度留给软件（编译器），而不是硬件。</p>
<h3 id="22-避免过度优化为未来留下空间">2.2 避免过度优化，为未来留下空间</h3>
<p>RISC-V 的第二条设计原则是：<strong>不要为了微小的优化而牺牲长期的扩展性。</strong></p>
<p>一个经典的例子是 <strong>延迟槽（Delay Slot）</strong>。延迟槽是早期 RISC 架构（MIPS、SPARC、PowerPC）为了解决流水线冲突而引入的一个技巧。简单来说，分支指令后面的那条指令总是会被执行，不管分支跳不跳转。这样编译器就可以在分支延迟槽里填充一条有用的指令，从而掩盖分支带来的流水线气泡。</p>
<p>在 1980 年代，这是一个非常聪明的优化，能带来大约 10% 的性能提升。但是到了今天，CPU 的分支预测器已经非常先进，深流水线、乱序执行技术让延迟槽的优势几乎消失殆尽。更糟糕的是，延迟槽成了指令集里一个永远无法去掉的包袱——因为所有的旧软件都依赖这个行为。</p>
<p>RISC-V 的设计者们从一开始就决定：<strong>不使用延迟槽。</strong> 虽然在简单的顺序执行 CPU 上这会损失一点点性能，但对于更复杂的高性能 CPU 来说，这实际上是一个更好的设计选择。</p>
<p>另一个例子是 <strong>条件指令（Predication）</strong>。ARM 架构有一个非常有特色的功能：几乎所有的指令都可以条件执行。你可以写 <code>ADDEQ r0, r1, r2</code>，意思是&quot;如果前一条比较的结果是相等，那么执行加法&quot;。这样可以消除一些短小的分支，提升流水线效率。</p>
<p>但是条件执行也有代价：每条指令都需要额外的 4 位来编码条件。而且对于乱序执行 CPU 来说，条件指令实际上会增加硬件的复杂度——CPU 需要跟踪每条指令的执行条件。</p>
<p>RISC-V 同样没有实现条件指令。设计者们认为：在乱序执行 CPU 上，好的分支预测器已经能很好地处理短小分支，条件指令带来的收益不足以抵消它增加的复杂度。</p>
<p>这种设计哲学可以总结为一句话：<strong>如果一个特性在今天有 5% 的好处，但在未来会限制 50% 的可能性，那么就不要做。</strong></p>
<h3 id="23-统一的指令编码格式">2.3 统一的指令编码格式</h3>
<p>RISC-V 的第三条设计原则是：<strong>尽可能保持指令编码格式的统一。</strong></p>
<p>所有的 RISC-V 基础指令都是 <strong>32 位固定长度</strong>，并且在内存中自然对齐。这和 ARM 的 Thumb 指令集、x86 的变长指令集形成了鲜明对比。</p>
<p>更重要的是，RISC-V 的指令编码有非常强的规律性。所有的 R 型指令（寄存器-寄存器操作）都有相同的格式：</p>
<pre tabindex="0"><code>| funct7 (7) | rs2 (5) | rs1 (5) | funct3 (3) | rd (5) | opcode (7) |
</code></pre><p>所有的 I 型指令（立即数操作、加载指令）都有相同的格式：</p>
<pre tabindex="0"><code>| immediate[11:0] (12) | rs1 (5) | funct3 (3) | rd (5) | opcode (7) |
</code></pre><p>所有的 S 型指令（存储指令）都有相同的格式：</p>
<pre tabindex="0"><code>| imm[11:5] (7) | rs2 (5) | rs1 (5) | funct3 (3) | imm[4:0] (5) | opcode (7) |
</code></pre><p>这种规律性对于硬件实现来说是一个巨大的礼物。指令解码器不需要先判断指令类型，再用不同的逻辑去解析各个字段。它可以在一个时钟周期内，并行地解码出 rd、rs1、rs2、立即数、操作码——所有的字段位置都是固定的。</p>
<p>这也是为什么 RISC-V CPU 的前端设计相对简单的原因之一。在 x86 上，指令解码器可能需要几个时钟周期才能确定一条指令的长度和所有操作数，因为指令长度从 1 字节到 15 字节不等，各种前缀、后缀、操作数的组合千变万化。</p>
<p>（第一部分完，约 2400 字）</p>
]]></content:encoded></item><item><title>RISC-V 向量扩展 (RVV) 原理与实战优化指南</title><link>https://tech-snippets.xyz/posts/riscv-rvv-principle-optimization-guide/</link><pubDate>Wed, 06 May 2026 19:00:00 +0800</pubDate><guid>https://tech-snippets.xyz/posts/riscv-rvv-principle-optimization-guide/</guid><description>前言 2020 年代，AI 算力的需求呈现出爆炸式增长。从大语言模型的推理，到计算机视觉的实时处理，再到科学计算的海量数据处理，计算领域对数据并行处理能力的需求从未如此迫切。传统的标量 CPU 虽然通用，但面对海量重复运算时显得力不从心；GPU 虽然并行能力强大，但功耗和延迟问题使其难以在嵌入式和端侧场景中广泛应用。
正是在这样的背景下，RISC-V 向量扩展（RISC-V Vector Extension，简称 RVV） 应运而生。作为 RISC-V 指令集架构的官方标准扩展，RVV 提供了一套灵活、可扩展的向量处理机制，能够以远低于 GPU 的功耗和延迟，实现高效的数据并行计算。从低功耗的 IoT 设备，到高性能的服务器 CPU，RVV 正在成为 RISC-V 生态中最具变革性的技术之一。
RVV 的设计哲学与传统的 SIMD 扩展（如 x86 的 SSE/AVX、ARM 的 NEON/SVE）有着本质的不同。它不是简单地固定宽度的向量寄存器堆，而是引入了运行时可配置向量长度、向量寄存器分组、掩码操作等一系列创新设计，使得同一份 RVV 代码能够在不同硬件实现上高效运行，真正实现了&amp;quot;一次编写，处处加速&amp;quot;。
本文将从底层原理出发，带你深入理解 RVV 1.0 规范的设计精髓，通过完整的代码示例，手把手教你掌握 RVV 编程和优化技巧。无论你是芯片架构师、系统工程师，还是想要在 RISC-V 平台上优化算法性能的开发者，这篇文章都会为你提供完整的知识体系和实战指南。
一、为什么我们需要向量扩展？ 在深入探讨 RVV 的具体细节之前，让我们先回答一个最基本的问题：为什么 CPU 需要向量扩展？
1.1 数据级并行的本质 现代计算任务中，绝大多数密集运算都具有一个共同的特征：对大量数据执行相同的操作。例如：
图像卷积：对每个像素点执行相同的乘加运算 矩阵乘法：大量的元素级乘累加操作 神经网络推理：张量之间的批量运算 信号处理：FFT、滤波等时域频域变换 这种&amp;quot;单指令，多数据&amp;quot;的模式，正是向量计算能够发挥巨大优势的场景。如果用传统的标量指令来处理这些任务，每个数据元素都需要取指、译码、执行一次，这会造成巨大的指令开销和控制开销。而向量指令可以在一条指令中处理数十甚至上百个数据元素，将指令吞吐量提升一个数量级。
1.2 传统 SIMD 的局限性 在 RVV 出现之前，主流 CPU 架构都有自己的 SIMD 扩展：
x86: SSE、AVX、AVX2、AVX-512，向量宽度从 128 位逐步增加到 512 位 ARM: NEON（128 位固定宽度）、SVE（可伸缩向量） MIPS: MSA（128 位向量） 这些传统 SIMD 扩展虽然在特定场景下表现出色，但普遍存在几个问题：</description><content:encoded><![CDATA[<h2 id="前言">前言</h2>
<p>2020 年代，AI 算力的需求呈现出爆炸式增长。从大语言模型的推理，到计算机视觉的实时处理，再到科学计算的海量数据处理，计算领域对<strong>数据并行处理能力</strong>的需求从未如此迫切。传统的标量 CPU 虽然通用，但面对海量重复运算时显得力不从心；GPU 虽然并行能力强大，但功耗和延迟问题使其难以在嵌入式和端侧场景中广泛应用。</p>
<p>正是在这样的背景下，<strong>RISC-V 向量扩展（RISC-V Vector Extension，简称 RVV）</strong> 应运而生。作为 RISC-V 指令集架构的官方标准扩展，RVV 提供了一套灵活、可扩展的向量处理机制，能够以远低于 GPU 的功耗和延迟，实现高效的数据并行计算。从低功耗的 IoT 设备，到高性能的服务器 CPU，RVV 正在成为 RISC-V 生态中最具变革性的技术之一。</p>
<p>RVV 的设计哲学与传统的 SIMD 扩展（如 x86 的 SSE/AVX、ARM 的 NEON/SVE）有着本质的不同。它不是简单地固定宽度的向量寄存器堆，而是引入了<strong>运行时可配置向量长度</strong>、<strong>向量寄存器分组</strong>、<strong>掩码操作</strong>等一系列创新设计，使得同一份 RVV 代码能够在不同硬件实现上高效运行，真正实现了&quot;一次编写，处处加速&quot;。</p>
<p>本文将从底层原理出发，带你深入理解 RVV 1.0 规范的设计精髓，通过完整的代码示例，手把手教你掌握 RVV 编程和优化技巧。无论你是芯片架构师、系统工程师，还是想要在 RISC-V 平台上优化算法性能的开发者，这篇文章都会为你提供完整的知识体系和实战指南。</p>
<p><img alt="RVV 架构概览" loading="lazy" src="/images/rvv-architecture-overview.svg"></p>
<h2 id="一为什么我们需要向量扩展">一、为什么我们需要向量扩展？</h2>
<p>在深入探讨 RVV 的具体细节之前，让我们先回答一个最基本的问题：<strong>为什么 CPU 需要向量扩展？</strong></p>
<h3 id="11-数据级并行的本质">1.1 数据级并行的本质</h3>
<p>现代计算任务中，绝大多数密集运算都具有一个共同的特征：<strong>对大量数据执行相同的操作</strong>。例如：</p>
<ul>
<li>图像卷积：对每个像素点执行相同的乘加运算</li>
<li>矩阵乘法：大量的元素级乘累加操作</li>
<li>神经网络推理：张量之间的批量运算</li>
<li>信号处理：FFT、滤波等时域频域变换</li>
</ul>
<p>这种&quot;单指令，多数据&quot;的模式，正是向量计算能够发挥巨大优势的场景。如果用传统的标量指令来处理这些任务，每个数据元素都需要取指、译码、执行一次，这会造成巨大的指令开销和控制开销。而向量指令可以在一条指令中处理数十甚至上百个数据元素，将指令吞吐量提升一个数量级。</p>
<h3 id="12-传统-simd-的局限性">1.2 传统 SIMD 的局限性</h3>
<p>在 RVV 出现之前，主流 CPU 架构都有自己的 SIMD 扩展：</p>
<ul>
<li><strong>x86</strong>: SSE、AVX、AVX2、AVX-512，向量宽度从 128 位逐步增加到 512 位</li>
<li><strong>ARM</strong>: NEON（128 位固定宽度）、SVE（可伸缩向量）</li>
<li><strong>MIPS</strong>: MSA（128 位向量）</li>
</ul>
<p>这些传统 SIMD 扩展虽然在特定场景下表现出色，但普遍存在几个问题：</p>
<p><strong>固定向量宽度的耦合</strong>：传统 SIMD 的向量宽度是硬件定义的，软件必须针对特定宽度编写代码。当硬件升级（如从 AVX2 升级到 AVX-512）时，软件需要重新编写才能利用更大的向量宽度。</p>
<p><strong>寄存器资源浪费</strong>：对于较小的数据类型（如 8 位、16 位），固定宽度的向量寄存器虽然能存放更多元素，但指令集往往缺乏灵活的类型转换和操作支持。</p>
<p><strong>代码可移植性差</strong>：不同架构、甚至同一架构的不同代之间，SIMD 指令集往往不兼容。为了在多平台上获得最佳性能，开发者需要维护多个版本的 SIMD 代码。</p>
<h3 id="13-rvv-的设计突破">1.3 RVV 的设计突破</h3>
<p>RVV 的设计者们深刻认识到了传统 SIMD 的这些问题，提出了一系列创新的设计理念：</p>
<ul>
<li>
<p><strong>运行时可配置向量长度</strong>：软件在运行时查询硬件支持的最大向量长度（VLEN），然后根据实际需求设置当前操作的向量长度（vl）。同一份代码在不同 VLEN 的硬件上都能正确运行。</p>
</li>
<li>
<p><strong>灵活的数据类型支持</strong>：向量元素宽度（SEW）可以在运行时配置，支持 8、16、32、64 位的整数和浮点数。</p>
</li>
<li>
<p><strong>向量寄存器分组（LMUL）</strong>：通过配置 LMUL 参数，可以将多个物理寄存器组合成一个逻辑寄存器组，在需要更多元素时动态扩展向量长度。</p>
</li>
<li>
<p><strong>内置掩码支持</strong>：每条向量指令都支持掩码操作，可以只处理向量中的部分元素，避免了传统 SIMD 中复杂的掩码处理逻辑。</p>
</li>
</ul>
<p>这些设计使得 RVV 兼具了<strong>高性能</strong>、<strong>灵活性</strong>和<strong>可移植性</strong>，代表了下一代向量处理架构的发展方向。</p>
<h2 id="二rvv-10-规范的设计哲学">二、RVV 1.0 规范的设计哲学</h2>
<p>RVV 1.0 规范于 2021 年正式冻结，标志着 RISC-V 向量扩展进入了稳定可用的阶段。理解 RVV 的设计哲学，是掌握 RVV 编程的第一步。</p>
<h3 id="21-向量长度的解耦vlen-vs-vl">2.1 向量长度的解耦：VLEN vs vl</h3>
<p>RVV 中最核心的创新，就是将<strong>硬件实现的向量长度</strong>与<strong>软件使用的向量长度</strong>完全解耦。</p>
<p><strong>VLEN（Vector Length）</strong>：硬件实现的每个向量寄存器的位数，是一个硬件参数，在不同芯片上可以有不同的取值。RVV 规范要求 VLEN ≥ 128 位，且必须是 2 的幂。常见的 VLEN 配置有 128、256、512、1024 位等。</p>
<p><strong>vl（vector length 寄存器）</strong>：软件在运行时设置的当前向量操作的元素个数。每次向量指令执行时，只会处理前 <code>vl</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="c1">// 查询硬件支持的最大向量长度（以位为单位）
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kt">uint32_t</span> <span class="n">vlen</span> <span class="o">=</span> <span class="nf">__riscv_vlenb</span><span class="p">()</span> <span class="o">*</span> <span class="mi">8</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 设置当前向量长度为 16 个元素
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nf">vsetvl</span><span class="p">(</span><span class="mi">16</span><span class="p">,</span> <span class="n">SEW_32</span><span class="p">,</span> <span class="n">LMUL_1</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 这条指令只会处理 16 个元素
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="n">vadd</span><span class="p">.</span><span class="nf">vv</span><span class="p">(</span><span class="n">v0</span><span class="p">,</span> <span class="n">v1</span><span class="p">,</span> <span class="n">v2</span><span class="p">);</span>
</span></span></code></pre></div><p>当软件设置的 <code>vl</code> 超过硬件能够支持的最大值时，硬件会自动将其截断为最大支持值。这意味着同一份代码在 VLEN=128 的芯片上可能每次处理 4 个 32 位元素，在 VLEN=512 的芯片上可能每次处理 16 个 32 位元素，但代码逻辑完全不需要修改。</p>
<h3 id="22-向量类型配置vtype-寄存器">2.2 向量类型配置：vtype 寄存器</h3>
<p>除了向量长度 <code>vl</code>，RVV 还通过 <code>vtype</code> 向量类型寄存器来控制向量操作的行为。<code>vtype</code> 包含以下关键字段：</p>
<ul>
<li><strong>SEW（Standard Element Width）</strong>：标准元素宽度，指定向量元素的大小，可以是 8、16、32、64 位</li>
<li><strong>LMUL（Vector Register Grouping Multiplier）</strong>：向量寄存器分组倍数，决定每个逻辑向量使用多少个物理寄存器</li>
<li><strong>vediv</strong>：向量元素宽度除数，用于窄化操作</li>
<li><strong>vta（Vector Tail Agnostic）</strong>：向量尾元素策略，指定超过 <code>vl</code> 的元素是否可以被硬件任意修改</li>
<li><strong>vma（Vector Mask Agnostic）</strong>：向量掩码策略，指定被掩码屏蔽的元素是否可以被硬件任意修改</li>
</ul>
<p>每次执行向量指令前，都需要通过 <code>vsetvl</code> 或 <code>vsetivli</code> 指令配置这些参数。这看起来增加了一条额外的指令，但实际上这为编译器和硬件提供了巨大的优化空间。</p>
<h3 id="23-向量寄存器堆v0---v31">2.3 向量寄存器堆：v0 - v31</h3>
<p>RVV 定义了 32 个向量寄存器 <code>v0</code> 到 <code>v31</code>，每个寄存器的宽度为 VLEN 位。这些向量寄存器在逻辑上是独立的，但通过 LMUL 参数可以组合成更大的逻辑寄存器组。</p>
<p>值得注意的是，<code>v0</code> 具有特殊的地位——它是默认的掩码寄存器。当一条向量指令需要使用掩码时，如果没有指定其他掩码寄存器，就会默认使用 <code>v0</code> 中的掩码值。</p>
<h2 id="三向量寄存器分组lmul机制详解">三、向量寄存器分组（LMUL）机制详解</h2>
<p>向量寄存器分组（LMUL）是 RVV 中最巧妙、也是最容易让人困惑的设计之一。理解 LMUL 的工作原理，是掌握 RVV 性能优化的关键。</p>
<p><img alt="RVV 向量寄存器分组" loading="lazy" src="/images/rvv-register-grouping.svg"></p>
<h3 id="31-lmul-的基本概念">3.1 LMUL 的基本概念</h3>
<p>LMUL（Vector Register Grouping Multiplier）决定了每个逻辑向量使用多少个物理向量寄存器。RVV 1.0 支持以下 LMUL 值：</p>
<ul>
<li><strong>分数 LMUL</strong>：1/8、1/4、1/2 —— 一个物理寄存器可以容纳多个逻辑向量</li>
<li><strong>整数 LMUL</strong>：1、2、4、8 —— 一个逻辑向量占用多个物理寄存器</li>
</ul>
<p>为什么需要这样的设计？这要从向量操作的两类需求说起：</p>
<ol>
<li><strong>需要更多元素</strong>：当处理大量相同类型的数据时，我们希望每次向量指令能处理尽可能多的元素，以摊薄指令开销。</li>
<li><strong>需要更多临时变量</strong>：当实现复杂算法时，我们需要更多的向量寄存器来存放中间结果，避免频繁的寄存器溢出。</li>
</ol>
<p>LMUL 机制正是为了在这两类需求之间取得平衡。</p>
<h3 id="32-vlmax-的计算">3.2 VLMAX 的计算</h3>
<p>对于给定的 SEW 和 LMUL，硬件能够支持的最大向量元素个数 VLMAX 的计算公式为：</p>
<pre tabindex="0"><code>VLMAX = (VLEN × LMUL) / SEW
</code></pre><p>让我们通过几个例子来理解这个公式（假设 VLEN = 256 位）：</p>
<table>
<thead>
<tr>
<th>LMUL</th>
<th>SEW</th>
<th>VLMAX 计算</th>
<th>结果</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>32 位</td>
<td>(256 × 1) / 32</td>
<td>8 元素</td>
<td>每个逻辑向量使用 1 个物理寄存器</td>
</tr>
<tr>
<td>4</td>
<td>32 位</td>
<td>(256 × 4) / 32</td>
<td>32 元素</td>
<td>每个逻辑向量使用 4 个物理寄存器</td>
</tr>
<tr>
<td>1/8</td>
<td>32 位</td>
<td>(256 × 1/8) / 32</td>
<td>1 元素</td>
<td>8 个逻辑向量共享 1 个物理寄存器</td>
</tr>
</tbody>
</table>
<p>可以看到，增大 LMUL 可以增加每次向量操作的元素个数，但代价是减少了可用的逻辑向量寄存器数量。例如，当 LMUL = 8 时，32 个物理寄存器只能提供 4 个逻辑向量寄存器（v0, v8, v16, v24）。</p>
<h3 id="33-lmul-的选择策略">3.3 LMUL 的选择策略</h3>
<p>在实际编程中，如何选择合适的 LMUL 值？这里有几个基本原则：</p>
<p><strong>优先使用 LMUL = 1</strong>：这是最通用的配置，既能获得合理的向量长度，又能保留完整的 32 个向量寄存器用于算法实现。</p>
<p><strong>计算密集型内核用大 LMUL</strong>：如果你的算法非常简单（如向量加法、点积），只需要很少的临时变量，那么使用 LMUL = 2 或 LMUL = 4 可以增加每次处理的元素数，提升计算吞吐量。</p>
<p><strong>复杂算法用 LMUL = 1 或分数 LMUL</strong>：如果算法需要很多中间结果，或者有很多依赖关系，那么保留更多的向量寄存器比增加向量长度更重要。分数 LMUL 特别适合处理需要大量临时变量的场景。</p>
<p><strong>根据数据类型调整</strong>：对于较小的数据类型（如 8 位），LMUL = 1 可能已经提供了足够的元素个数（VLEN=256 时是 32 个元素），不需要更大的 LMUL。</p>
<p>（第一部分完，约2300字）</p>
<h2 id="四rvv-指令集分类详解">四、RVV 指令集分类详解</h2>
<p>RVV 1.0 规范定义了超过 150 条向量指令，涵盖了向量计算的各个方面。按照功能分类，我们可以将这些指令分为以下几大类。</p>
<h3 id="41-向量配置指令">4.1 向量配置指令</h3>
<p>向量配置指令用于设置 <code>vl</code> 和 <code>vtype</code> 寄存器，是执行任何向量操作前的&quot;开场白&quot;。</p>
<p><strong>vsetvl（Vector Set Vector Length）</strong>：</p>
<pre tabindex="0"><code class="language-assembly" data-lang="assembly">vsetvl rd, rs1, vtypei
</code></pre><p>这条指令根据 <code>rs1</code> 中的请求长度和 <code>vtypei</code> 编码的向量类型，计算实际可用的向量长度，并将其写入 <code>rd</code> 和 <code>vl</code> 寄存器。</p>
<p>在 C intrinsic 中，对应的函数是：</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">size_t</span> <span class="nf">vsetvl_e32m1</span><span class="p">(</span><span class="kt">size_t</span> <span class="n">avl</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nf">__riscv_vsetvl_e32m1</span><span class="p">(</span><span class="n">avl</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p><strong>vsetivli（Vector Set Immediate Vector Length）</strong>：</p>
<pre tabindex="0"><code class="language-assembly" data-lang="assembly">vsetivli rd, uimm, vtypei
</code></pre><p>这是 vsetvl 的立即数版本，适合请求长度是编译期常量的情况。</p>
<h3 id="42-向量-loadstore-指令">4.2 向量 Load/Store 指令</h3>
<p>RVV 的访存指令是其灵活性的重要体现，支持多种访存模式：</p>
<p><strong>单位步长访存（Unit-Stride）</strong>：</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="kt">vint32m1_t</span> <span class="nf">vle32_v_i32m1</span><span class="p">(</span><span class="k">const</span> <span class="kt">int32_t</span> <span class="o">*</span><span class="n">base</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">vl</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="kt">void</span> <span class="nf">vse32_v_i32m1</span><span class="p">(</span><span class="kt">int32_t</span> <span class="o">*</span><span class="n">base</span><span class="p">,</span> <span class="kt">vint32m1_t</span> <span class="n">value</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">vl</span><span class="p">);</span>
</span></span></code></pre></div><p>这是最常用的访存模式，相当于传统 SIMD 的连续加载/存储。</p>
<p><strong>跨步访存（Strided）</strong>：</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">// 每隔 stride 个字节加载一个元素
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kt">vint32m1_t</span> <span class="nf">vlse32_v_i32m1</span><span class="p">(</span><span class="k">const</span> <span class="kt">int32_t</span> <span class="o">*</span><span class="n">base</span><span class="p">,</span> <span class="kt">ptrdiff_t</span> <span class="n">stride</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">vl</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 每隔 stride 个字节存储一个元素
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="nf">vsse32_v_i32m1</span><span class="p">(</span><span class="kt">int32_t</span> <span class="o">*</span><span class="n">base</span><span class="p">,</span> <span class="kt">ptrdiff_t</span> <span class="n">stride</span><span class="p">,</span> <span class="kt">vint32m1_t</span> <span class="n">value</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">vl</span><span class="p">);</span>
</span></span></code></pre></div><p>跨步访存非常适合处理矩阵的列访问，或者数组中特定间隔的元素。</p>
<p><strong>索引访存（Indexed）</strong>：</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="kt">vint32m1_t</span> <span class="nf">vluxei32_v_i32m1</span><span class="p">(</span><span class="k">const</span> <span class="kt">int32_t</span> <span class="o">*</span><span class="n">base</span><span class="p">,</span> <span class="kt">vuint32m1_t</span> <span class="n">bindex</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">vl</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="kt">void</span> <span class="nf">vsuxei32_v_i32m1</span><span class="p">(</span><span class="kt">int32_t</span> <span class="o">*</span><span class="n">base</span><span class="p">,</span> <span class="kt">vuint32m1_t</span> <span class="n">bindex</span><span class="p">,</span> <span class="kt">vint32m1_t</span> <span class="n">value</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">vl</span><span class="p">);</span>
</span></span></code></pre></div><p>索引访存支持对任意地址的数据进行 gather/scatter 操作，这是传统 SIMD 很难高效实现的功能。</p>
<p><strong>段访存（Segment）</strong>：</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">// 加载结构体数组（AoS）格式的数据
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="nf">vlsseg3e32_v_i32m1</span><span class="p">(</span><span class="kt">vint32m1_t</span> <span class="o">*</span><span class="n">v0</span><span class="p">,</span> <span class="kt">vint32m1_t</span> <span class="o">*</span><span class="n">v1</span><span class="p">,</span> <span class="kt">vint32m1_t</span> <span class="o">*</span><span class="n">v2</span><span class="p">,</span> 
</span></span><span class="line"><span class="cl">                         <span class="k">const</span> <span class="kt">int32_t</span> <span class="o">*</span><span class="n">base</span><span class="p">,</span> <span class="kt">ptrdiff_t</span> <span class="n">stride</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">vl</span><span class="p">);</span>
</span></span></code></pre></div><p>段访存指令可以一次性加载多个字段，完美支持结构体数组（AoS）的访问模式，避免了复杂的数据重排。</p>
<h3 id="43-向量算术运算指令">4.3 向量算术运算指令</h3>
<p>RVV 提供了完整的算术运算指令，支持整数和浮点数：</p>
<p><strong>向量-向量运算</strong>：</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="kt">vint32m1_t</span> <span class="nf">vadd_vv_i32m1</span><span class="p">(</span><span class="kt">vint32m1_t</span> <span class="n">op1</span><span class="p">,</span> <span class="kt">vint32m1_t</span> <span class="n">op2</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">vl</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="kt">vint32m1_t</span> <span class="nf">vmul_vv_i32m1</span><span class="p">(</span><span class="kt">vint32m1_t</span> <span class="n">op1</span><span class="p">,</span> <span class="kt">vint32m1_t</span> <span class="n">op2</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">vl</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="kt">vint32m1_t</span> <span class="nf">vmacc_vv_i32m1</span><span class="p">(</span><span class="kt">vint32m1_t</span> <span class="n">acc</span><span class="p">,</span> <span class="kt">vint32m1_t</span> <span class="n">op1</span><span class="p">,</span> <span class="kt">vint32m1_t</span> <span class="n">op2</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">vl</span><span class="p">);</span>
</span></span></code></pre></div><p><strong>向量-标量运算</strong>：</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="kt">vint32m1_t</span> <span class="nf">vadd_vx_i32m1</span><span class="p">(</span><span class="kt">vint32m1_t</span> <span class="n">op1</span><span class="p">,</span> <span class="kt">int32_t</span> <span class="n">op2</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">vl</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="kt">vint32m1_t</span> <span class="nf">vmul_vx_i32m1</span><span class="p">(</span><span class="kt">vint32m1_t</span> <span class="n">op1</span><span class="p">,</span> <span class="kt">int32_t</span> <span class="n">op2</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">vl</span><span class="p">);</span>
</span></span></code></pre></div><p><strong>饱和运算</strong>：</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="kt">vint8m1_t</span> <span class="nf">vadd_sat_vv_i8m1</span><span class="p">(</span><span class="kt">vint8m1_t</span> <span class="n">op1</span><span class="p">,</span> <span class="kt">vint8m1_t</span> <span class="n">op2</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">vl</span><span class="p">);</span>
</span></span></code></pre></div><p>饱和运算在图像处理和信号处理中非常有用，可以避免溢出导致的 artifacts。</p>
<h3 id="44-向量比较与掩码指令">4.4 向量比较与掩码指令</h3>
<p>RVV 的每条指令都支持掩码操作，这是其重要特性之一。</p>
<p><strong>向量比较</strong>：</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="kt">vbool8_t</span> <span class="nf">vmseq_vv_i32m1_b8</span><span class="p">(</span><span class="kt">vint32m1_t</span> <span class="n">op1</span><span class="p">,</span> <span class="kt">vint32m1_t</span> <span class="n">op2</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">vl</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="kt">vbool8_t</span> <span class="nf">vmsgt_vv_i32m1_b8</span><span class="p">(</span><span class="kt">vint32m1_t</span> <span class="n">op1</span><span class="p">,</span> <span class="kt">vint32m1_t</span> <span class="n">op2</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">vl</span><span class="p">);</span>
</span></span></code></pre></div><p>比较指令的结果是一个掩码向量，每个元素是一个布尔值。</p>
<p><strong>掩码化操作</strong>：</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="kt">vint32m1_t</span> <span class="nf">vadd_vv_i32m1_m</span><span class="p">(</span><span class="kt">vbool8_t</span> <span class="n">mask</span><span class="p">,</span> <span class="kt">vint32m1_t</span> <span class="n">maskedoff</span><span class="p">,</span> 
</span></span><span class="line"><span class="cl">                           <span class="kt">vint32m1_t</span> <span class="n">op1</span><span class="p">,</span> <span class="kt">vint32m1_t</span> <span class="n">op2</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">vl</span><span class="p">);</span>
</span></span></code></pre></div><p>当 <code>mask</code> 中的元素为 true 时，执行对应的运算；为 false 时，保留 <code>maskedoff</code> 中的值（或根据 vma 策略处理）。</p>
<h3 id="45-向量置换与重排指令">4.5 向量置换与重排指令</h3>
<p>向量置换指令用于重新排列向量中的元素，这是很多算法中的关键操作。</p>
<p><strong>滑动窗口（Slide）</strong>：</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="kt">vint32m1_t</span> <span class="nf">vslide1up_vx_i32m1</span><span class="p">(</span><span class="kt">vint32m1_t</span> <span class="n">dest</span><span class="p">,</span> <span class="kt">int32_t</span> <span class="n">src</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">vl</span><span class="p">);</span>
</span></span></code></pre></div><p>滑动指令在实现 FIR 滤波器、滑动窗口求和等算法时非常高效。</p>
<p><strong>收集（Gather）</strong>：</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="kt">vint32m1_t</span> <span class="nf">vrgather_vv_i32m1</span><span class="p">(</span><span class="kt">vint32m1_t</span> <span class="n">op1</span><span class="p">,</span> <span class="kt">vuint32m1_t</span> <span class="n">op2</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">vl</span><span class="p">);</span>
</span></span></code></pre></div><p><strong>压缩（Compress）</strong>：</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="kt">vint32m1_t</span> <span class="nf">vcompress_vm_i32m1</span><span class="p">(</span><span class="kt">vbool8_t</span> <span class="n">mask</span><span class="p">,</span> <span class="kt">vint32m1_t</span> <span class="n">dest</span><span class="p">,</span> 
</span></span><span class="line"><span class="cl">                              <span class="kt">vint32m1_t</span> <span class="n">src</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">vl</span><span class="p">);</span>
</span></span></code></pre></div><p>压缩指令在实现条件过滤等操作时非常有用。</p>
<h3 id="46-向量归约指令">4.6 向量归约指令</h3>
<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="c1">// 向量求和归约
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kt">int32_t</span> <span class="nf">vredsum_vs_i32m1_i32</span><span class="p">(</span><span class="kt">int32_t</span> <span class="n">dst</span><span class="p">,</span> <span class="kt">vint32m1_t</span> <span class="n">vector</span><span class="p">,</span> 
</span></span><span class="line"><span class="cl">                              <span class="kt">int32_t</span> <span class="n">scalar</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">vl</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="kt">int32_t</span> <span class="nf">vredmax_vs_i32m1_i32</span><span class="p">(</span><span class="kt">int32_t</span> <span class="n">dst</span><span class="p">,</span> <span class="kt">vint32m1_t</span> <span class="n">vector</span><span class="p">,</span> 
</span></span><span class="line"><span class="cl">                              <span class="kt">int32_t</span> <span class="n">scalar</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">vl</span><span class="p">);</span>
</span></span></code></pre></div><p>归约指令在实现点积、求和、找最值等操作时非常高效。</p>
<h2 id="五rvv-编程入门从汇编到-intrinsic">五、RVV 编程入门：从汇编到 Intrinsic</h2>
<p>理解了 RVV 的原理和指令集后，现在让我们通过实际的代码示例来学习 RVV 编程。</p>
<h3 id="51-环境准备">5.1 环境准备</h3>
<p>要编译 RVV 代码，你需要支持 RVV 1.0 的工具链。推荐使用：</p>
<ul>
<li><strong>GCC 13+</strong> 或 <strong>Clang 17+</strong>：编译器需要支持 <code>-march=rv64gcv</code> 或 <code>-march=rv32gcv</code></li>
<li><strong>QEMU 7+</strong>：用于在 x86 机器上模拟 RISC-V 环境</li>
<li><strong>Spike</strong>：RISC-V 官方指令集模拟器</li>
</ul>
<p>编译命令示例：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">riscv64-linux-gnu-gcc -march<span class="o">=</span>rv64gcv -O2 -o <span class="nb">test</span> test.c
</span></span></code></pre></div><h3 id="52-第一个-rvv-程序向量加法">5.2 第一个 RVV 程序：向量加法</h3>
<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">#include</span> <span class="cpf">&lt;riscv_vector.h&gt;</span><span class="cp">
</span></span></span><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"></span>
</span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="nf">vector_add</span><span class="p">(</span><span class="kt">int32_t</span> <span class="o">*</span><span class="n">a</span><span class="p">,</span> <span class="kt">int32_t</span> <span class="o">*</span><span class="n">b</span><span class="p">,</span> <span class="kt">int32_t</span> <span class="o">*</span><span class="n">c</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">n</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">while</span> <span class="p">(</span><span class="n">i</span> <span class="o">&lt;</span> <span class="n">n</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="kt">size_t</span> <span class="n">vl</span> <span class="o">=</span> <span class="nf">__riscv_vsetvl_e32m1</span><span class="p">(</span><span class="n">n</span> <span class="o">-</span> <span class="n">i</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="kt">vint32m1_t</span> <span class="n">va</span> <span class="o">=</span> <span class="nf">__riscv_vle32_v_i32m1</span><span class="p">(</span><span class="o">&amp;</span><span class="n">a</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">vl</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="kt">vint32m1_t</span> <span class="n">vb</span> <span class="o">=</span> <span class="nf">__riscv_vle32_v_i32m1</span><span class="p">(</span><span class="o">&amp;</span><span class="n">b</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">vl</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="kt">vint32m1_t</span> <span class="n">vc</span> <span class="o">=</span> <span class="nf">__riscv_vadd_vv_i32m1</span><span class="p">(</span><span class="n">va</span><span class="p">,</span> <span class="n">vb</span><span class="p">,</span> <span class="n">vl</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">__riscv_vse32_v_i32m1</span><span class="p">(</span><span class="o">&amp;</span><span class="n">c</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">vc</span><span class="p">,</span> <span class="n">vl</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="n">i</span> <span class="o">+=</span> <span class="n">vl</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="kt">int</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kt">int32_t</span> <span class="n">a</span><span class="p">[]</span> <span class="o">=</span> <span class="p">{</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">6</span><span class="p">,</span> <span class="mi">7</span><span class="p">,</span> <span class="mi">8</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="mi">10</span><span class="p">};</span>
</span></span><span class="line"><span class="cl">    <span class="kt">int32_t</span> <span class="n">b</span><span class="p">[]</span> <span class="o">=</span> <span class="p">{</span><span class="mi">10</span><span class="p">,</span> <span class="mi">20</span><span class="p">,</span> <span class="mi">30</span><span class="p">,</span> <span class="mi">40</span><span class="p">,</span> <span class="mi">50</span><span class="p">,</span> <span class="mi">60</span><span class="p">,</span> <span class="mi">70</span><span class="p">,</span> <span class="mi">80</span><span class="p">,</span> <span class="mi">90</span><span class="p">,</span> <span class="mi">100</span><span class="p">};</span>
</span></span><span class="line"><span class="cl">    <span class="kt">int32_t</span> <span class="n">c</span><span class="p">[</span><span class="mi">10</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="nf">vector_add</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">,</span> <span class="n">c</span><span class="p">,</span> <span class="mi">10</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="mi">10</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;%d &#34;</span><span class="p">,</span> <span class="n">c</span><span class="p">[</span><span class="n">i</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">printf</span><span class="p">(</span><span class="s">&#34;</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="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>这个程序的核心是一个循环，每次循环处理 <code>vl</code> 个元素。<code>vl</code> 的值由硬件在运行时决定——在 VLEN=128 位的硬件上，<code>vl</code> 最多是 4（128 / 32）；在 VLEN=512 位的硬件上，<code>vl</code> 最多是 16。但无论硬件如何，代码逻辑都不需要修改。</p>
<h3 id="53-汇编代码分析">5.3 汇编代码分析</h3>
<p>让我们看看编译器为 <code>vector_add</code> 函数生成的汇编代码（简化版）：</p>
<pre tabindex="0"><code class="language-assembly" data-lang="assembly">vector_add:
    li      a4,0
    j       .L2
.L3:
    slli    a5,a4,2
    add     a5,a0,a5
    vsetvli a5, a5, e32, m1, ta, ma  # 设置向量长度和类型
    vle32.v v1,0(a5)                  # 加载向量 a
    add     a5,a1,a4,slli 2
    vle32.v v2,0(a5)                  # 加载向量 b
    vadd.vv v1,v1,v2                  # 向量加法
    add     a5,a2,a4,slli 2
    vse32.v v1,0(a5)                  # 存储结果
    add     a4,a4,a3                  # 更新索引
.L2:
    bltu    a4,a3,.L3                 # 检查是否完成
    ret
</code></pre><p>可以看到，核心的向量操作只有几条指令，但这几条指令可以一次性处理多个元素。</p>
<h3 id="54-条件操作掩码的使用">5.4 条件操作：掩码的使用</h3>
<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="kt">void</span> <span class="nf">vector_add_even</span><span class="p">(</span><span class="kt">int32_t</span> <span class="o">*</span><span class="n">a</span><span class="p">,</span> <span class="kt">int32_t</span> <span class="o">*</span><span class="n">b</span><span class="p">,</span> <span class="kt">int32_t</span> <span class="o">*</span><span class="n">c</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">n</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">while</span> <span class="p">(</span><span class="n">i</span> <span class="o">&lt;</span> <span class="n">n</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kt">size_t</span> <span class="n">vl</span> <span class="o">=</span> <span class="nf">__riscv_vsetvl_e32m1</span><span class="p">(</span><span class="n">n</span> <span class="o">-</span> <span class="n">i</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="kt">vint32m1_t</span> <span class="n">va</span> <span class="o">=</span> <span class="nf">__riscv_vle32_v_i32m1</span><span class="p">(</span><span class="o">&amp;</span><span class="n">a</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">vl</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="kt">vint32m1_t</span> <span class="n">vb</span> <span class="o">=</span> <span class="nf">__riscv_vle32_v_i32m1</span><span class="p">(</span><span class="o">&amp;</span><span class="n">b</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">vl</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="kt">vuint32m1_t</span> <span class="n">vid</span> <span class="o">=</span> <span class="nf">__riscv_vid_v_u32m1</span><span class="p">(</span><span class="n">vl</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="kt">vuint32m1_t</span> <span class="n">offset</span> <span class="o">=</span> <span class="nf">__riscv_vadd_vx_u32m1</span><span class="p">(</span><span class="n">vid</span><span class="p">,</span> <span class="n">i</span><span class="p">,</span> <span class="n">vl</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="kt">vuint32m1_t</span> <span class="n">mod</span> <span class="o">=</span> <span class="nf">__riscv_vand_vx_u32m1</span><span class="p">(</span><span class="n">offset</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="n">vl</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="kt">vbool8_t</span> <span class="n">mask</span> <span class="o">=</span> <span class="nf">__riscv_vmseq_vx_u32m1_b8</span><span class="p">(</span><span class="n">mod</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">vl</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="kt">vint32m1_t</span> <span class="n">vc</span> <span class="o">=</span> <span class="nf">__riscv_vadd_vv_i32m1_m</span><span class="p">(</span><span class="n">mask</span><span class="p">,</span> <span class="n">va</span><span class="p">,</span> <span class="n">va</span><span class="p">,</span> <span class="n">vb</span><span class="p">,</span> <span class="n">vl</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="nf">__riscv_vse32_v_i32m1</span><span class="p">(</span><span class="o">&amp;</span><span class="n">c</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">vc</span><span class="p">,</span> <span class="n">vl</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="n">i</span> <span class="o">+=</span> <span class="n">vl</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>vid</code> 指令生成元素索引，然后通过位运算和比较生成掩码，最后在掩码的控制下执行加法。这展示了 RVV 灵活的条件处理能力。</p>
<p>（第二部分完，约2400字）</p>
]]></content:encoded></item></channel></rss>