<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>一念</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <id>https://bubao.github.io/</id>
  <link href="https://bubao.github.io/" rel="alternate"/>
  <link href="https://bubao.github.io/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, 一念</rights>
  <subtitle>因为有了危机感，所以会义无反顾。</subtitle>
  <title>瓦猫杂货铺</title>
  <updated>2026-10-31T20:37:58.469Z</updated>
  <entry>
    <author>
      <name>一念</name>
    </author>
    <category term="Linux" scheme="https://bubao.github.io/categories/Linux/"/>
    <category term="运维" scheme="https://bubao.github.io/categories/Linux/%E8%BF%90%E7%BB%B4/"/>
    <category term="Ubuntu" scheme="https://bubao.github.io/tags/Ubuntu/"/>
    <category term="SSH" scheme="https://bubao.github.io/tags/SSH/"/>
    <category term="OpenSSH" scheme="https://bubao.github.io/tags/OpenSSH/"/>
    <category term="安全" scheme="https://bubao.github.io/tags/%E5%AE%89%E5%85%A8/"/>
    <category term="运维" scheme="https://bubao.github.io/tags/%E8%BF%90%E7%BB%B4/"/>
    <content>
      <![CDATA[<p>在多用户或嵌入式边缘设备（如开发板、跳板机、内部测试服务器）管理中，传统的 SSH 免密登录通常将公钥放置在每个用户的家目录 <code>~/.ssh/authorized_keys</code> 中。然而这种做法在集中运维和安全管控时存在显著痛点：</p><ol><li><strong>权限管控难</strong>：普通用户可以随时登入机器随意修改、清空或追加自己的 <code>authorized_keys</code>，管理员难以进行公钥生命周期管理；</li><li><strong>初始化繁琐</strong>：新创建的用户尚未生成家目录或尚未初始化 <code>.ssh</code> 时，自动化运维脚本部署公钥较为别扭。</li></ol><p>通过配置 OpenSSH 的 <code>AuthorizedKeysFile</code>，我们可以将公钥统一集中存放至 <code>/etc/ssh/authorized_keys/%u</code>，由 root 集中管控，同时支持回退兜底家目录配置。</p><p>但在配置集中公钥时，<strong>几乎所有人都会踩进一个因 OpenSSH 权限机制引发的隐蔽大坑</strong>。本文将从底层工作原理出发，提供完整配置方案、排障调试指南以及 AppArmor 拦截处理。</p><span id="more"></span><hr><h2 id="💡-核心原理与权限暗坑"><a href="#💡-核心原理与权限暗坑" class="headerlink" title="💡 核心原理与权限暗坑"></a>💡 核心原理与权限暗坑</h2><p>配置的核心是 <code>sshd_config</code> 中的这一行指令：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs sshconfig">AuthorizedKeysFile /etc/ssh/authorized_keys/%u .ssh/authorized_keys<br></code></pre></td></tr></table></figure><ul><li>**<code>%u</code>**：OpenSSH 的内置占位符，代表当前发起登录请求的用户名。当用户 <code>neons</code> 尝试登录时，路径自动展开为 <code>/etc/ssh/authorized_keys/neons</code>；</li><li><strong>双路径兜底机制</strong>：sshd 会优先查找集中目录 <code>/etc/ssh/authorized_keys/neons</code>，若该文件不存在，则自动 fallback 到用户家目录下的 <code>~neons/.ssh/authorized_keys</code>。</li></ul><h3 id="⚠️-致命陷阱：temporarily-use-uid-机制"><a href="#⚠️-致命陷阱：temporarily-use-uid-机制" class="headerlink" title="⚠️ 致命陷阱：temporarily_use_uid 机制"></a>⚠️ 致命陷阱：temporarily_use_uid 机制</h3><p>很多运维习惯性地认为：“<code>/etc/ssh</code> 是管理员目录，权限越严格越好”，于是对集中目录执行了 <code>chmod 700</code>、密钥文件执行了 <code>chmod 600</code>。</p><p><strong>结果导致：客户端提交公钥时服务端直接拒绝，静默回退要求输入密码！</strong></p><h4 id="底层原理解析："><a href="#底层原理解析：" class="headerlink" title="底层原理解析："></a>底层原理解析：</h4><p>OpenSSH 在进行公钥校验时，处于极度严格的安全设计考量，<strong>sshd 守护进程会临时降低权限，切换为目标登录用户的 UID（<code>temporarily_use_uid</code>）去打开并读取公钥文件</strong>！</p><ul><li>如果目录被设为 <code>700</code>（所有者 root），登录用户（普通用户 UID）根本<strong>无权进入该目录（无法 <code>chdir</code>）</strong>；</li><li>如果文件被设为 <code>600</code>（仅 root 可读写），普通用户 UID 同样<strong>无权读取该文件内容</strong>；</li><li>sshd 发现“文件读不到”，出于安全考虑既不给详细报错，直接当作“无可用公钥”，在日志中静默记录 <code>ED25519 key is not allowed</code>。</li></ul><h4 id="正确的权限设定法则："><a href="#正确的权限设定法则：" class="headerlink" title="正确的权限设定法则："></a>正确的权限设定法则：</h4><table><thead><tr><th align="left">存放位置</th><th align="left">目录推荐权限</th><th align="left">密钥文件推荐权限</th><th align="left">属主与所有权</th></tr></thead><tbody><tr><td align="left"><strong>集中目录方案</strong>（<code>/etc/ssh/authorized_keys/%u</code>）</td><td align="left"><strong><code>755</code></strong> (<code>rwxr-xr-x</code>)</td><td align="left"><strong><code>644</code></strong> (<code>rw-r--r--</code>)</td><td align="left"><code>root:root</code>（关键：防普通用户篡改，但允许读）</td></tr><tr><td align="left"><strong>传统家目录方案</strong>（<code>~/.ssh/authorized_keys</code>）</td><td align="left"><strong><code>700</code></strong> (<code>rwx------</code>)</td><td align="left"><strong><code>600</code></strong> (<code>rw-------</code>)</td><td align="left"><code>&lt;user&gt;:&lt;user&gt;</code>（关键：严格防其他用户窥视）</td></tr></tbody></table><hr><h2 id="📝-完整配置实战步骤"><a href="#📝-完整配置实战步骤" class="headerlink" title="📝 完整配置实战步骤"></a>📝 完整配置实战步骤</h2><h3 id="1-客户端生成现代-Ed25519-密钥（本地-Mac-Linux）"><a href="#1-客户端生成现代-Ed25519-密钥（本地-Mac-Linux）" class="headerlink" title="1. 客户端生成现代 Ed25519 密钥（本地 Mac &#x2F; Linux）"></a>1. 客户端生成现代 Ed25519 密钥（本地 Mac &#x2F; Linux）</h3><p>推荐优先采用 <code>Ed25519</code> 算法（椭圆曲线加密，相比 RSA 体积更小、抗碰撞更强），并建议通过 <code>-f</code> 显式命名以隔离不同主机：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 生成密钥，指定注释与保存路径</span><br>ssh-keygen -t ed25519 -C <span class="hljs-string">&quot;admin-tspi&quot;</span> -f ~/.ssh/id_ed25519_tspi<br></code></pre></td></tr></table></figure><p>生成产物：</p><ul><li><code>~/.ssh/id_ed25519_tspi</code>：私钥（严禁外泄）；</li><li><code>~/.ssh/id_ed25519_tspi.pub</code>：公钥（用于写入服务端）。</li></ul><p>复制公钥内容：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">cat</span> ~/.ssh/id_ed25519_tspi.pub<br></code></pre></td></tr></table></figure><h3 id="2-服务器创建集中公钥目录"><a href="#2-服务器创建集中公钥目录" class="headerlink" title="2. 服务器创建集中公钥目录"></a>2. 服务器创建集中公钥目录</h3><p>以 root 或 sudo 权限在服务器上创建集中存储目录：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">mkdir</span> -p /etc/ssh/authorized_keys<br></code></pre></td></tr></table></figure><h3 id="3-写入用户公钥"><a href="#3-写入用户公钥" class="headerlink" title="3. 写入用户公钥"></a>3. 写入用户公钥</h3><p>以目标登录用户名 <code>neons</code> 为文件名，将客户端公钥<strong>单行</strong>完整写入：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">tee</span> /etc/ssh/authorized_keys/neons &lt;&lt;<span class="hljs-string">&#x27;EOF&#x27;</span><br>ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClK7l3OUBXKMg9shQtgZi5TlgveIuqLMECvCdKD189x admin-tspi<br>EOF<br></code></pre></td></tr></table></figure><blockquote><p><strong>注意</strong>：公钥必须保持为完整的一整行，中间不得包含多余的换行符或头尾截断。</p></blockquote><h3 id="4-关键步骤：规范权限与属主"><a href="#4-关键步骤：规范权限与属主" class="headerlink" title="4. 关键步骤：规范权限与属主"></a>4. 关键步骤：规范权限与属主</h3><p>严格按集中式目录要求赋予权限：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 目录 755：允许普通用户 uid 穿透目录查找文件，root 独占修改权</span><br><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">chmod</span> 755 /etc/ssh/authorized_keys<br><br><span class="hljs-comment"># 公钥文件 644：允许所有用户只读，仅 root 能够增删改</span><br><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">chmod</span> 644 /etc/ssh/authorized_keys/neons<br><br><span class="hljs-comment"># 属主必须锁定为 root:root</span><br><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">chown</span> root:root /etc/ssh/authorized_keys /etc/ssh/authorized_keys/neons<br></code></pre></td></tr></table></figure><h3 id="5-修改-sshd-服务端配置"><a href="#5-修改-sshd-服务端配置" class="headerlink" title="5. 修改 sshd 服务端配置"></a>5. 修改 sshd 服务端配置</h3><p>编辑主配置文件 <code>/etc/ssh/sshd_config</code>（或放入 <code>/etc/ssh/sshd_config.d/</code>）：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> nano /etc/ssh/sshd_config<br></code></pre></td></tr></table></figure><p>确保开启公钥认证并指定双路径：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sshconfig">PubkeyAuthentication yes<br>AuthorizedKeysFile /etc/ssh/authorized_keys/%u .ssh/authorized_keys<br></code></pre></td></tr></table></figure><h3 id="6-配置语法校验与平滑重载"><a href="#6-配置语法校验与平滑重载" class="headerlink" title="6. 配置语法校验与平滑重载"></a>6. 配置语法校验与平滑重载</h3><p>修改完成后，切记<strong>不要盲目直接重启服务</strong>，先做语法验证：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 验证配置语法，若无任何输出表示语法正常</span><br><span class="hljs-built_in">sudo</span> sshd -t<br><br><span class="hljs-comment"># 平滑重载 sshd（不会中断当前已连接的 SSH 会话）</span><br><span class="hljs-built_in">sudo</span> systemctl reload ssh<br><span class="hljs-comment"># 部分 CentOS/RHEL 发行版命令为：sudo systemctl reload sshd</span><br></code></pre></td></tr></table></figure><h3 id="7-本地客户端-ssh-config-快捷配置"><a href="#7-本地客户端-ssh-config-快捷配置" class="headerlink" title="7. 本地客户端 ~/.ssh/config 快捷配置"></a>7. 本地客户端 <code>~/.ssh/config</code> 快捷配置</h3><p>在本地电脑上配置 Host 别名，免去每次输入 IP 与参数：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs sshconfig">Host tspi<br>    HostName 10.0.0.123<br>    User neons<br>    IdentityFile ~/.ssh/id_ed25519_tspi<br></code></pre></td></tr></table></figure><h3 id="8-测试登录"><a href="#8-测试登录" class="headerlink" title="8. 测试登录"></a>8. 测试登录</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">ssh tspi<br></code></pre></td></tr></table></figure><p>无需输入用户密码即可直接登入。</p><hr><h2 id="🚨-深度故障排查与调试（调试神器）"><a href="#🚨-深度故障排查与调试（调试神器）" class="headerlink" title="🚨 深度故障排查与调试（调试神器）"></a>🚨 深度故障排查与调试（调试神器）</h2><p>如果配置后客户端 <code>ssh -v tspi</code> 显示发送了公钥（<code>Offering public key</code>），但服务器依然提示输入密码，说明服务端判定该密钥不可用。</p><h3 id="1-启动独立临时端口调试（推荐，不影响现存服务）"><a href="#1-启动独立临时端口调试（推荐，不影响现存服务）" class="headerlink" title="1. 启动独立临时端口调试（推荐，不影响现存服务）"></a>1. 启动独立临时端口调试（推荐，不影响现存服务）</h3><p>千万不要在生产机器上盲目调高主进程日志。在服务器上开一个新终端，运行<strong>独立调试端口</strong>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> /usr/sbin/sshd -ddd -p 2222<br></code></pre></td></tr></table></figure><ul><li><code>-ddd</code>：最高详细级别的调试输出，日志直接实时打在终端屏幕上；</li><li><code>-p 2222</code>：指定独立测试端口，原 22 端口服务不受任何干扰。</li></ul><p>本地客户端指定端口连接测试：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">ssh -p 2222 -v tspi<br></code></pre></td></tr></table></figure><h3 id="2-核心报错特征与对照排查"><a href="#2-核心报错特征与对照排查" class="headerlink" title="2. 核心报错特征与对照排查"></a>2. 核心报错特征与对照排查</h3><p>观察服务器调试终端的输出，若出现以下错误：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs text">debug1: temporarily_use_uid: 1000/1000 (e=0/0)<br>debug1: trying public key file /etc/ssh/authorized_keys/neons<br>debug3: secure_filename: checking &#x27;/etc/ssh/authorized_keys&#x27;<br>...<br>mm_answer_keyallowed: publickey authentication test: ED25519 key is not allowed<br>Failed publickey for neons from 10.0.0.50 port 54321 ssh2<br></code></pre></td></tr></table></figure><p><strong>排查清单</strong>：</p><ol><li><strong>检查目录权限</strong>：再次确认 <code>/etc/ssh/authorized_keys</code> 是否为 <code>755</code>，误设为 <code>700</code> 会直接导致 <code>Permission denied</code>；</li><li><strong>检查文件权限</strong>：再次确认公钥文件是否为 <code>644</code>；</li><li><strong>检查公钥格式</strong>：检查文件内是否有多余空行、被拆分成多行、或结尾被截断。</li></ol><h3 id="3-Ubuntu-24-04-AppArmor-拦截排查"><a href="#3-Ubuntu-24-04-AppArmor-拦截排查" class="headerlink" title="3. Ubuntu 24.04+ AppArmor 拦截排查"></a>3. Ubuntu 24.04+ AppArmor 拦截排查</h3><p>在新版 Ubuntu（如 Ubuntu 24.04 LTS &#x2F; 26.04）上，OpenSSH 默认受到 <strong>AppArmor</strong> 安全沙箱保护。默认规则可能仅允许 sshd 读取 <code>/etc/ssh/*</code> 下的常规文件，而阻止深入读取子目录。</p><p>若调试日志中明确打印底层 <code>Permission denied</code>，可为 AppArmor 添加局部放行规则：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 编辑本地补充规则</span><br><span class="hljs-built_in">sudo</span> nano /etc/apparmor.d/local/usr.sbin.sshd<br></code></pre></td></tr></table></figure><p>追加允许规则：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">/etc/ssh/authorized_keys/** r,<br></code></pre></td></tr></table></figure><p>重新加载 AppArmor 策略：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> apparmor_parser -r /etc/apparmor.d/usr.sbin.sshd<br></code></pre></td></tr></table></figure><hr><h2 id="📌-进阶选型对比：三种公钥管理方案"><a href="#📌-进阶选型对比：三种公钥管理方案" class="headerlink" title="📌 进阶选型对比：三种公钥管理方案"></a>📌 进阶选型对比：三种公钥管理方案</h2><table><thead><tr><th align="left">方案</th><th align="left">配置方式</th><th align="left">优点</th><th align="left">缺点 &#x2F; 适用场景</th></tr></thead><tbody><tr><td align="left"><strong>集中目录静态文件（本文方案）</strong></td><td align="left"><code>AuthorizedKeysFile /etc/ssh/authorized_keys/%u</code></td><td align="left">简单易懂，纯原生无外部依赖；由管理员统一下发，普通用户无法篡改。</td><td align="left">公钥文件需设为 <code>644</code>，所有系统用户均可读取公钥内容（公钥本身是公开的，无泄密风险）。适合小微集群与开发板运维。</td></tr><tr><td align="left"><strong>传统家目录方案</strong></td><td align="left"><code>AuthorizedKeysFile .ssh/authorized_keys</code></td><td align="left">符合个人使用习惯，权限严格隔离（<code>700/600</code>）。</td><td align="left">管理员无法防范用户私自增删；新用户无家目录时无法预置公钥。</td></tr><tr><td align="left"><strong>动态集中认证（企业级）</strong></td><td align="left"><code>AuthorizedKeysCommand /usr/local/bin/get_keys.sh</code></td><td align="left">权限最高。由 root 进程调用脚本从 LDAP、Vault 或数据库实时查询公钥，本地不落盘。</td><td align="left">部署与维护复杂度高，外部查询服务若宕机可能导致全网无法登录。适合大规模云原生机房。</td></tr></tbody></table><hr><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>配置 OpenSSH 集中公钥认证的核心关键在于<strong>理解 <code>temporarily_use_uid</code> 的权限回退机制</strong>：</p><ul><li>集中目录需打破惯性思维，目录采用 <code>755</code>、公钥文件采用 <code>644</code>；</li><li>利用 <code>AuthorizedKeysFile /etc/ssh/authorized_keys/%u .ssh/authorized_keys</code> 建立“集中管理 + 家目录兜底”的双保险；</li><li>善用 <code>sshd -ddd -p 2222</code> 独立调试端口，秒级定位公钥协商失败的根本诱因。</li></ul>]]>
    </content>
    <id>https://bubao.github.io/posts/fe62447c.html</id>
    <link href="https://bubao.github.io/posts/fe62447c.html"/>
    <published>2026-11-01T13:00:00.000Z</published>
    <summary>详解 OpenSSH 集中式公钥认证（AuthorizedKeysFile /etc/ssh/authorized_keys/%u）的配置方案，深入剖析 temporarily_use_uid 机制导致的权限拒绝大坑、Ubuntu AppArmor 拦截排查，以及 sshd -ddd 独立端口调试技巧。</summary>
    <title>SSH 集中式公钥免密登录配置与权限排坑指南</title>
    <updated>2026-10-31T20:37:58.469Z</updated>
  </entry>
  <entry>
    <author>
      <name>一念</name>
    </author>
    <category term="嵌入式" scheme="https://bubao.github.io/categories/%E5%B5%8C%E5%85%A5%E5%BC%8F/"/>
    <category term="教程" scheme="https://bubao.github.io/categories/%E5%B5%8C%E5%85%A5%E5%BC%8F/%E6%95%99%E7%A8%8B/"/>
    <category term="RK3566" scheme="https://bubao.github.io/tags/RK3566/"/>
    <category term="Python" scheme="https://bubao.github.io/tags/Python/"/>
    <category term="泰山派" scheme="https://bubao.github.io/tags/%E6%B3%B0%E5%B1%B1%E6%B4%BE/"/>
    <category term="OCR" scheme="https://bubao.github.io/tags/OCR/"/>
    <category term="NCNN" scheme="https://bubao.github.io/tags/NCNN/"/>
    <category term="模型转换" scheme="https://bubao.github.io/tags/%E6%A8%A1%E5%9E%8B%E8%BD%AC%E6%8D%A2/"/>
    <category term="ARM" scheme="https://bubao.github.io/tags/ARM/"/>
    <content>
      <![CDATA[<p>在立创·泰山派（RK3566, 4 核 Cortex-A55, 2GB 内存）这类嵌入式 Linux 开发板上部署深度学习模型时，<strong>CPU 算力与内存带宽往往是最大掣肘</strong>。</p><p>很多开发者默认使用 ONNX Runtime CPU 模式作为落地兜底方案，但在泰山派上运行标准的 PP-OCRv4 ONNX 模型时，耗时通常长达 <strong>1.5 ~ 2 秒</strong>，且模型总体量达 <strong>90MB</strong>，内存占用极高。</p><p>而在开源项目 <a href="https://github.com/bubao/tspi-ocr-web"><strong>tspi-ocr-web</strong></a> 的研发过程中，我引入了腾讯开源的高性能移动端推理框架 <a href="https://github.com/Tencent/ncnn"><strong>Tencent&#x2F;ncnn</strong></a> 与百度 <a href="https://github.com/PaddlePaddle/PaddleOCR"><strong>PaddlePaddle&#x2F;PaddleOCR</strong></a> 移动端轻量模型。通过开启 <strong>ARM NEON FP16 汇编加速</strong> 与 <strong>pnnx 动态尺寸图优化</strong>，将整图 OCR 识别速度直接提升至 <strong>120~180ms（性能提升近 10 倍）</strong>，同时模型体量骤降至 <strong>11MB</strong>！</p><p>本文提取项目中关于 NCNN 的核心实践，系统整理“<strong>架构优势</strong> $\to$ <strong>模型来源与下载</strong> $\to$ <strong>模型转换 (pnnx)</strong> $\to$ <strong>Python 板端推理</strong> $\to$ <strong>性能对比与避坑</strong>”的完整落地方法论。</p><span id="more"></span><hr><h2 id="一、为什么选择-NCNN？"><a href="#一、为什么选择-NCNN？" class="headerlink" title="一、为什么选择 NCNN？"></a>一、为什么选择 NCNN？</h2><p>在边缘 ARM 嵌入式设备上，主流推理框架主要有 ONNX Runtime、TFLite 以及硬件专用的 NPU 工具链（如 RKNN）。但在实际工程落地中，NCNN 展现出了极其独特的不可替代性：</p><table><thead><tr><th align="left">评估维度</th><th align="left">ONNX Runtime (CPU)</th><th align="left">瑞芯微 RKNN (NPU)</th><th align="left">腾讯 NCNN (ARM CPU)</th></tr></thead><tbody><tr><td align="left"><strong>依赖与体积</strong></td><td align="left">依赖较重（动态库庞大）</td><td align="left">强依赖专有 NPU 内核驱动</td><td align="left">**极简纯 C++**，零第三方库依赖</td></tr><tr><td align="left"><strong>模型算子兼容</strong></td><td align="left">兼容性极强</td><td align="left">需模型量化，部分动态算子受限</td><td align="left">算子全面，图优化成熟</td></tr><tr><td align="left"><strong>模型体积 (PP-OCR)</strong></td><td align="left">约 <strong>90MB</strong></td><td align="left">约 <strong>15MB</strong></td><td align="left"><strong>仅 11MB（FP16 压缩）</strong></td></tr><tr><td align="left"><strong>RK3566 整图耗时</strong></td><td align="left">~1500ms（慢）</td><td align="left">~80ms（极快）</td><td align="left"><strong>~150ms（极快，接近 NPU）</strong></td></tr><tr><td align="left"><strong>跨硬件通用性</strong></td><td align="left">通用</td><td align="left">仅限瑞芯微指定芯片</td><td align="left"><strong>通用</strong>（任意 ARMv8&#x2F;A55&#x2F;A72&#x2F;A76）</td></tr></tbody></table><h3 id="核心加速秘密：ARMv8-2-A-NEON-FP16-指令"><a href="#核心加速秘密：ARMv8-2-A-NEON-FP16-指令" class="headerlink" title="核心加速秘密：ARMv8.2-A NEON FP16 指令"></a>核心加速秘密：ARMv8.2-A NEON FP16 指令</h3><p>RK3566 的 CPU 核心基于 ARM Cortex-A55 架构，支持 <strong>ARMv8.2-A 指令集中的 FP16 矢量运算</strong>。</p><ul><li>传统 ONNX Runtime 大多运行在 FP32 单精度下，不仅计算量翻倍，更严重的是在 2GB 开发板上受限于 L2&#x2F;L3 缓存与 DDR4 带宽瓶颈；</li><li>NCNN 针对 ARM NEON 手写了深度的汇编优化内核（Kernel），计算时以 16 位半精度浮点（FP16）直接加载并执行 FMA（乘加）向量指令；</li><li><strong>双重红利</strong>：内存带宽占用直接减半，浮点吞吐率翻倍，彻底释放 4 核 A55 的硬件潜能。</li></ul><hr><h2 id="二、模型来源与资源清单（Hugging-Face-开源仓库）"><a href="#二、模型来源与资源清单（Hugging-Face-开源仓库）" class="headerlink" title="二、模型来源与资源清单（Hugging Face 开源仓库）"></a>二、模型来源与资源清单（Hugging Face 开源仓库）</h2><p>项目中所采用的 NCNN 与 ONNX 模型均来自于开源社区并经过轻量化验证，详细开源出处与文件清单如下：</p><h3 id="2-1-PP-OCRv5-NCNN-优化版（开箱即用推荐，总计约-11MB）"><a href="#2-1-PP-OCRv5-NCNN-优化版（开箱即用推荐，总计约-11MB）" class="headerlink" title="2.1 PP-OCRv5 NCNN 优化版（开箱即用推荐，总计约 11MB）"></a>2.1 PP-OCRv5 NCNN 优化版（开箱即用推荐，总计约 11MB）</h3><ul><li><strong>开源仓库出处</strong>：Hugging Face 开源项目 <a href="https://huggingface.co/qefro/pp-OCRv5-6-ncnn"><strong>qefro&#x2F;pp-OCRv5-6-ncnn</strong></a></li><li><strong>国内镜像加速</strong>：<a href="https://hf-mirror.com/qefro/pp-OCRv5-6-ncnn">https://hf-mirror.com/qefro/pp-OCRv5-6-ncnn</a></li><li><strong>包含文件与映射</strong>：<ul><li><strong>文本检测</strong>：<code>PP_OCRv5_mobile_det.param</code> &#x2F; <code>PP_OCRv5_mobile_det.bin</code>（约 3.0 MB）</li><li><strong>文本识别</strong>：<code>PP_OCRv5_mobile_rec.param</code> &#x2F; <code>PP_OCRv5_mobile_rec.bin</code>（约 7.9 MB）</li><li><strong>方向分类器</strong>：<code>PP_LCNet_x0_25_textline_ori.param</code> &#x2F; <code>PP_LCNet_x0_25_textline_ori.bin</code>（约 0.3 MB）</li><li><strong>字典文件</strong>：<code>ppocr_keys_v5.txt</code>（映射为 <code>dict.txt</code>）</li></ul></li></ul><p>该套模型是针对移动端与嵌入式平台优化后的 PP-OCRv5 结构，相比 v4 识别精度提升的同时参数量进一步压缩，完美适配 NCNN 的 NEON 汇编计算核心。</p><h3 id="2-2-PP-OCRv4-ONNX-原版（对比基准与转换源模型，约-90MB）"><a href="#2-2-PP-OCRv4-ONNX-原版（对比基准与转换源模型，约-90MB）" class="headerlink" title="2.2 PP-OCRv4 ONNX 原版（对比基准与转换源模型，约 90MB）"></a>2.2 PP-OCRv4 ONNX 原版（对比基准与转换源模型，约 90MB）</h3><ul><li><strong>开源仓库出处</strong>：Hugging Face 仓库 <a href="https://huggingface.co/monkt/paddleocr-onnx"><strong>monkt&#x2F;paddleocr-onnx</strong></a></li><li><strong>国内镜像加速</strong>：<a href="https://hf-mirror.com/monkt/paddleocr-onnx">https://hf-mirror.com/monkt/paddleocr-onnx</a></li><li><strong>包含文件</strong>：<ul><li><code>detection/v3/det.onnx</code></li><li><code>languages/chinese/rec.onnx</code></li><li><code>preprocessing/textline-orientation/PP-LCNet_x1_0_textline_ori.onnx</code></li></ul></li></ul><h3 id="2-3-自动化下载脚本"><a href="#2-3-自动化下载脚本" class="headerlink" title="2.3 自动化下载脚本"></a>2.3 自动化下载脚本</h3><p>在 <code>tspi-ocr-web</code> 项目中，可通过内置脚本 <a href="https://github.com/bubao/tspi-ocr-web/blob/master/scripts/download_models.py"><code>scripts/download_models.py</code></a> 一键拉取上述模型（支持自动走 <code>hf-mirror.com</code> 国内加速）：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 自动下载 PP-OCRv5 NCNN 推荐模型到 models/ocr/ppocrv5-ncnn/</span><br>python scripts/download_models.py --ncnn<br></code></pre></td></tr></table></figure><hr><h2 id="三、模型转换实战：ONNX-longrightarrow-NCNN-基于-pnnx"><a href="#三、模型转换实战：ONNX-longrightarrow-NCNN-基于-pnnx" class="headerlink" title="三、模型转换实战：ONNX $\longrightarrow$ NCNN (基于 pnnx)"></a>三、模型转换实战：ONNX $\longrightarrow$ NCNN (基于 pnnx)</h2><p>NCNN 早期使用的是 <code>onnx2ncnn</code> 工具，而目前官方主力推荐的是经过彻底重构的 <strong><a href="https://github.com/pnnx/pnnx">pnnx</a>（PyTorch&#x2F;ONNX Neural Network eXchange）</strong>。pnnx 能自动完成算子融合、冗余消除以及动态尺寸定义。</p><p>在 <code>tspi-ocr-web</code> 项目中，转换逻辑封装在 <a href="https://github.com/bubao/tspi-ocr-web/blob/master/utils/ncnn_converter.py"><code>utils/ncnn_converter.py</code></a> 模块中。</p><h3 id="3-1-安装转换工具链（建议在-PC-端执行）"><a href="#3-1-安装转换工具链（建议在-PC-端执行）" class="headerlink" title="3.1 安装转换工具链（建议在 PC 端执行）"></a>3.1 安装转换工具链（建议在 PC 端执行）</h3><p>虽然 pnnx 可以通过 pip 安装，但官方的 <code>pip install pnnx</code> 往往会强行绑定安装 500MB+ 的 PyTorch。为了保持环境纯净，可从 <a href="https://github.com/pnnx/pnnx/releases">pnnx GitHub Releases</a> 直接下载独立的 C++ 预编译二进制文件。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 也可通过 pip 在独立虚拟环境中安装：</span><br>pip install pnnx<br></code></pre></td></tr></table></figure><h3 id="3-2-文本检测模型（DBNet）转换"><a href="#3-2-文本检测模型（DBNet）转换" class="headerlink" title="3.2 文本检测模型（DBNet）转换"></a>3.2 文本检测模型（DBNet）转换</h3><p>文本检测模型输入通常为动态大图，若未指定输入维度，转换工具往往无法正确推导动态算子形状。需使用 <code>inputshape</code> 与 <code>inputshape2</code> 显式预设多级动态分辨率：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs bash">pnnx det.onnx \<br>  inputshape=[1,3,640,640] \<br>  inputshape2=[1,3,960,960] \<br>  optlevel=2 \<br>  fp16=1<br></code></pre></td></tr></table></figure><ul><li><code>optlevel=2</code>：启用全局算子融合与图结构裁剪；</li><li><code>fp16=1</code>：权重大幅压缩至原大小的 50%，并在运行时开启半精度推理；</li><li><strong>产物</strong>：生成 <code>det.ncnn.param</code>（网络拓扑）与 <code>det.ncnn.bin</code>（权重参数）。</li></ul><h3 id="3-3-文本识别模型（CRNN-SVTR-LCNet）转换"><a href="#3-3-文本识别模型（CRNN-SVTR-LCNet）转换" class="headerlink" title="3.3 文本识别模型（CRNN &#x2F; SVTR &#x2F; LCNet）转换"></a>3.3 文本识别模型（CRNN &#x2F; SVTR &#x2F; LCNet）转换</h3><p>文本识别模型的特点是：<strong>图像高度固定（如 48px），而文本行宽度随字符数量动态可变</strong>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs bash">pnnx rec.onnx \<br>  inputshape=[1,3,48,320] \<br>  inputshape2=[1,3,48,640] \<br>  optlevel=2 \<br>  fp16=1<br></code></pre></td></tr></table></figure><p>通过指定不同宽度的 <code>inputshape</code>，pnnx 能够推导出任意步长的动态宽度拓扑，使得模型在板端能够直接接收变长文本切片推理。</p><hr><h2 id="四、板端-Python-极速推理实战"><a href="#四、板端-Python-极速推理实战" class="headerlink" title="四、板端 Python 极速推理实战"></a>四、板端 Python 极速推理实战</h2><p>在 RK3566 开发板端（建议使用此前搭建的 <a href="/posts/929df792.html">Python 3.12 虚拟环境</a>），安装编译好的 Python 绑定：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">pip install ncnn<br></code></pre></td></tr></table></figure><h3 id="4-1-初始化与网络加载"><a href="#4-1-初始化与网络加载" class="headerlink" title="4.1 初始化与网络加载"></a>4.1 初始化与网络加载</h3><p>基于 <code>tspi-ocr-web</code> 中 <code>PPOCRv5NCNNEngine</code> 的核心实现，网络初始化与线程调优如下：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">import</span> os<br><span class="hljs-keyword">import</span> ncnn<br><span class="hljs-keyword">import</span> numpy <span class="hljs-keyword">as</span> np<br><br><span class="hljs-comment"># 1. 实例化 NCNN 网络</span><br>net = ncnn.Net()<br><br><span class="hljs-comment"># 2. 核心性能参数调优（RK3566 为 4 核 A55，设置为 4 线程最大化吞吐）</span><br>net.opt.num_threads = <span class="hljs-number">4</span><br>net.opt.use_vulkan_compute = <span class="hljs-literal">False</span>  <span class="hljs-comment"># 嵌入式无头环境禁用 Vulkan</span><br>net.opt.use_fp16_packed = <span class="hljs-literal">True</span>     <span class="hljs-comment"># 启用紧凑 FP16</span><br>net.opt.use_fp16_storage = <span class="hljs-literal">True</span>    <span class="hljs-comment"># 启用 FP16 权重存储</span><br>net.opt.use_fp16_arithmetic = <span class="hljs-literal">True</span> <span class="hljs-comment"># 启用硬件 NEON FP16 浮点计算</span><br><br><span class="hljs-comment"># 3. 加载拓扑结构与权重</span><br>net.load_param(<span class="hljs-string">&quot;models/ocr/ppocrv5-ncnn/rec.param&quot;</span>)<br>net.load_model(<span class="hljs-string">&quot;models/ocr/ppocrv5-ncnn/rec.bin&quot;</span>)<br></code></pre></td></tr></table></figure><h3 id="4-2-动态-Blob-名称自适应"><a href="#4-2-动态-Blob-名称自适应" class="headerlink" title="4.2 动态 Blob 名称自适应"></a>4.2 动态 Blob 名称自适应</h3><p>经过 pnnx 优化后的网络，输入输出 Blob 节点名有时会由 <code>input/output</code> 重命名为 <code>in0/out0</code>。为了增强兼容性，可在加载后动态自省匹配：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs python">blobs = &#123;b.name <span class="hljs-keyword">for</span> b <span class="hljs-keyword">in</span> net.blobs()&#125;<br>in_name = <span class="hljs-string">&quot;in0&quot;</span> <span class="hljs-keyword">if</span> <span class="hljs-string">&quot;in0&quot;</span> <span class="hljs-keyword">in</span> blobs <span class="hljs-keyword">else</span> <span class="hljs-string">&quot;input&quot;</span><br>out_name = <span class="hljs-string">&quot;out0&quot;</span> <span class="hljs-keyword">if</span> <span class="hljs-string">&quot;out0&quot;</span> <span class="hljs-keyword">in</span> blobs <span class="hljs-keyword">else</span> <span class="hljs-string">&quot;output&quot;</span><br></code></pre></td></tr></table></figure><h3 id="4-3-执行前向推理（Extractor）"><a href="#4-3-执行前向推理（Extractor）" class="headerlink" title="4.3 执行前向推理（Extractor）"></a>4.3 执行前向推理（Extractor）</h3><p>NCNN 的设计思想是<strong>无状态的网络拓扑（Net）搭配有状态的提取器（Extractor）</strong>，Extractor 负责单次前向的数据绑定与计算执行：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">def</span> <span class="hljs-title function_">run_inference</span>(<span class="hljs-params">net: ncnn.Net, img_blob: np.ndarray, in_name: <span class="hljs-built_in">str</span>, out_name: <span class="hljs-built_in">str</span></span>) -&gt; np.ndarray:<br>    <span class="hljs-string">&quot;&quot;&quot;</span><br><span class="hljs-string">    img_blob: 经过预处理归一化后的浮点数组，形状 [C, H, W]</span><br><span class="hljs-string">    &quot;&quot;&quot;</span><br>    <span class="hljs-comment"># 1. 确保内存连续 (Contiguous)</span><br>    data = np.ascontiguousarray(img_blob, dtype=np.float32)<br>    <br>    <span class="hljs-comment"># 2. 转换为 ncnn.Mat</span><br>    in_mat = ncnn.Mat(data)<br>    <br>    <span class="hljs-comment"># 3. 创建提取器并绑定输入</span><br>    ex = net.create_extractor()<br>    ex.<span class="hljs-built_in">input</span>(in_name, in_mat)<br>    <br>    <span class="hljs-comment"># 4. 执行前向推理并提取输出</span><br>    ret, out_mat = ex.extract(out_name)<br>    <span class="hljs-keyword">if</span> ret != <span class="hljs-number">0</span>:<br>        <span class="hljs-keyword">raise</span> RuntimeError(<span class="hljs-string">f&quot;NCNN 推理提取失败, ret=<span class="hljs-subst">&#123;ret&#125;</span>&quot;</span>)<br>    <br>    <span class="hljs-comment"># 5. 转回 NumPy 供后处理解码 (添加 Batch 维度)</span><br>    output_np = np.array(out_mat)[np.newaxis, ...]<br>    <span class="hljs-keyword">return</span> output_np<br></code></pre></td></tr></table></figure><h3 id="4-4-资源释放"><a href="#4-4-资源释放" class="headerlink" title="4.4 资源释放"></a>4.4 资源释放</h3><p>在长周期驻留的服务中，当网络不再使用或需要重载时，显式释放底层 C++ 显存与权重：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs python">net.clear()<br></code></pre></td></tr></table></figure><hr><h2 id="五、RK3566-真实性能对比基准"><a href="#五、RK3566-真实性能对比基准" class="headerlink" title="五、RK3566 真实性能对比基准"></a>五、RK3566 真实性能对比基准</h2><p>在立创·泰山派（RK3566 4核 1.8GHz, 2GB LPDDR4, Ubuntu 26.04）上，对一张标准发票图片（包含 18 行文字）进行端到端识别测试：</p><table><thead><tr><th align="left">推理引擎</th><th align="left">计算单元</th><th align="left">模型体量 (Det + Rec)</th><th align="left">进程常驻内存</th><th align="left">整图端到端延迟</th></tr></thead><tbody><tr><td align="left"><strong>ONNX Runtime (PP-OCRv4)</strong></td><td align="left">CPU 4-Thread (FP32)</td><td align="left">90.4 MB</td><td align="left">~380 MB</td><td align="left"><strong>1520 ms</strong></td></tr><tr><td align="left"><strong>NCNN (PP-OCRv5)</strong></td><td align="left"><strong>CPU 4-Thread (NEON FP16)</strong></td><td align="left"><strong>11.2 MB</strong></td><td align="left"><strong>~85 MB</strong></td><td align="left"><strong>158 ms</strong> ⭐</td></tr><tr><td align="left"><strong>RKNN (PP-OCRv4)</strong></td><td align="left">NPU (0.8 TOPS INT8)</td><td align="left">14.8 MB</td><td align="left">~110 MB</td><td align="left"><strong>78 ms</strong></td></tr></tbody></table><p>从测试数据可以看出：</p><ol><li><strong>对比 ONNX Runtime</strong>：NCNN 速度直接提升近 <strong>10 倍</strong>，内存节省超过 **75%**，使得在 2GB 内存板卡上跑复杂视觉服务完全不会发生 OOM；</li><li><strong>对比 RKNN NPU</strong>：NCNN 仅通过 CPU 纯算力就跑出了 150ms 的优异成绩，且<strong>完全避开了 RKNN 的 NPU 量化精度损失以及专有内核驱动门槛</strong>，非常适合快速落地交付。</li></ol><hr><h2 id="六、关键避坑指南"><a href="#六、关键避坑指南" class="headerlink" title="六、关键避坑指南"></a>六、关键避坑指南</h2><ol><li><strong>NumPy 数组连续性问题（C-Contiguous）</strong>：<br>在向 <code>ncnn.Mat</code> 喂入切片数据（如从检测框裁剪文本小图）时，NumPy 切片通常是非连续内存。**必须调用 <code>np.ascontiguousarray(...)</code>**，否则可能导致底层 C++ 内存指针越界直接 Core Dump。</li><li><strong>多线程参数设置（num_threads）</strong>：<br>嵌入式多核 CPU 并非线程越多越好。在泰山派 4 核 A55 上，设置为 <code>num_threads = 4</code> 是吞吐与延迟的最佳平衡点；若设置过高，频繁的线程上下文切换反而会导致性能劣化。</li><li><strong>输入输出尺寸必须符合网络步长</strong>：<br>检测网络通常包含多级下采样（如 32 倍），输入的宽和高必须预处理为 32 的整数倍，否则在经过 pnnx 算子时会触发维度不匹配错误。</li></ol><hr><h2 id="结语与开源资源导航"><a href="#结语与开源资源导航" class="headerlink" title="结语与开源资源导航"></a>结语与开源资源导航</h2><p>NCNN 凭借纯粹的底层工程优化与对 ARM 架构的极致压榨，为边缘嵌入式设备提供了一条在 CPU 上实现准实时 AI 推理的高效通路。</p><p>本文涉及的所有项目源码、模型与依赖均已开源，欢迎查阅与体验：</p><h3 id="1-项目与代码仓库"><a href="#1-项目与代码仓库" class="headerlink" title="1. 项目与代码仓库"></a>1. 项目与代码仓库</h3><ul><li>🔗 <strong>tspi-ocr-web 完整项目</strong>：<a href="https://github.com/bubao/tspi-ocr-web">https://github.com/bubao/tspi-ocr-web</a></li><li>📂 <strong>模型转换源码</strong>：<a href="https://github.com/bubao/tspi-ocr-web/blob/master/utils/ncnn_converter.py"><code>utils/ncnn_converter.py</code></a></li><li>📂 <strong>推理引擎封装</strong>：<a href="https://github.com/bubao/tspi-ocr-web/blob/master/services/ocr_service.py"><code>services/ocr_service.py</code></a></li><li>📂 <strong>模型自动下载脚本</strong>：<a href="https://github.com/bubao/tspi-ocr-web/blob/master/scripts/download_models.py"><code>scripts/download_models.py</code></a></li></ul><h3 id="2-开源框架与模型出处"><a href="#2-开源框架与模型出处" class="headerlink" title="2. 开源框架与模型出处"></a>2. 开源框架与模型出处</h3><ul><li>🚀 <strong>腾讯 NCNN 官方仓库</strong>：<a href="https://github.com/Tencent/ncnn">https://github.com/Tencent/ncnn</a></li><li>🛠️ <strong>PNNX 模型转换工具链</strong>：<a href="https://github.com/pnnx/pnnx">https://github.com/pnnx/pnnx</a></li><li>📖 <strong>百度官方 PaddleOCR 项目</strong>：<a href="https://github.com/PaddlePaddle/PaddleOCR">https://github.com/PaddlePaddle/PaddleOCR</a></li><li>🤗 <strong>Hugging Face PP-OCRv5 NCNN 权重</strong>：<a href="https://huggingface.co/qefro/pp-OCRv5-6-ncnn">qefro&#x2F;pp-OCRv5-6-ncnn</a>（国内镜像：<a href="https://hf-mirror.com/qefro/pp-OCRv5-6-ncnn">hf-mirror.com&#x2F;qefro&#x2F;pp-OCRv5-6-ncnn</a>）</li><li>🤗 <strong>Hugging Face PP-OCRv4 ONNX 权重</strong>：<a href="https://huggingface.co/monkt/paddleocr-onnx">monkt&#x2F;paddleocr-onnx</a>（国内镜像：<a href="https://hf-mirror.com/monkt/paddleocr-onnx">hf-mirror.com&#x2F;monkt&#x2F;paddleocr-onnx</a>）</li></ul>]]>
    </content>
    <id>https://bubao.github.io/posts/80a659d.html</id>
    <link href="https://bubao.github.io/posts/80a659d.html"/>
    <published>2026-11-01T10:20:00.000Z</published>
    <summary>提取自 tspi-ocr-web 开源项目的 NCNN 实战指南：深入剖析腾讯 NCNN 在 ARM Cortex-A55 架构上的 NEON FP16 汇编加速优势，详解使用 pnnx 工具链将 ONNX 转换为支持动态尺寸的 NCNN 模型，并基于 Python 实现毫秒级端侧极速推理。</summary>
    <title>泰山派 RK3566 部署 NCNN 极速推理实践</title>
    <updated>2026-10-31T20:37:58.478Z</updated>
  </entry>
  <entry>
    <author>
      <name>一念</name>
    </author>
    <category term="嵌入式" scheme="https://bubao.github.io/categories/%E5%B5%8C%E5%85%A5%E5%BC%8F/"/>
    <category term="开源项目" scheme="https://bubao.github.io/categories/%E5%B5%8C%E5%85%A5%E5%BC%8F/%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE/"/>
    <category term="RK3566" scheme="https://bubao.github.io/tags/RK3566/"/>
    <category term="Python" scheme="https://bubao.github.io/tags/Python/"/>
    <category term="泰山派" scheme="https://bubao.github.io/tags/%E6%B3%B0%E5%B1%B1%E6%B4%BE/"/>
    <category term="RKNN" scheme="https://bubao.github.io/tags/RKNN/"/>
    <category term="RGA" scheme="https://bubao.github.io/tags/RGA/"/>
    <category term="OCR" scheme="https://bubao.github.io/tags/OCR/"/>
    <category term="NCNN" scheme="https://bubao.github.io/tags/NCNN/"/>
    <content>
      <![CDATA[<p>在边缘嵌入式开发板上部署深度学习应用时，往往受限于<strong>CPU 算力弱、内存容量小（通常仅 1GB ~ 2GB）、无独立显卡</strong>等瓶颈。如果在 RK3566 这类入门级嵌入式板卡上直接运行未经优化的大型 OCR 模型，往往会遭遇延迟高达数秒、内存频频 OOM、甚至导致板载系统假死的窘境。</p><p>为了解决低功耗嵌入式平台上的本地文字识别痛点，我开发并开源了针对立创·泰山派打造的高性能离线 OCR 服务项目：<a href="https://github.com/bubao/tspi-ocr-web"><strong>tspi-ocr-web</strong></a>。</p><p>本项目充分挖掘了 RK3566 芯片的硬件潜能，实现了 <strong>多引擎自动分级切换（RKNN NPU &#x2F; NCNN NEON &#x2F; ONNX）</strong>，结合 <strong>Rockchip RGA 2D 硬件图形加速</strong> 与 <strong>零服务端绘图开销的纯前端标注</strong> 架构，整图识别耗时直接从数秒级压缩至 <strong>~150ms</strong>。</p><span id="more"></span><hr><h2 id="💡-架构设计与核心思路"><a href="#💡-架构设计与核心思路" class="headerlink" title="💡 架构设计与核心思路"></a>💡 架构设计与核心思路</h2><figure class="highlight dns"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs dns">                            ┌──&gt; [检测到 RKNN 模型] ──&gt; NPU 硬件加速推理 <span class="hljs-punctuation">(</span><span class="hljs-number">0</span>.<span class="hljs-number">8</span> TOPS RKNNLite, ~<span class="hljs-number">80m</span>s<span class="hljs-punctuation">)</span><br>                            │<br>上传图片 ──&gt; 启动自动检测 ──┼──&gt; [检测到 NCNN 模型] ──&gt; NCNN 极速引擎 <span class="hljs-punctuation">(</span>PP-OCRv5 + NEON FP16, ~<span class="hljs-number">150m</span>s<span class="hljs-punctuation">)</span> ⭐推荐<br>                            │<br>                            └──&gt; [回退 ONNX 引擎] ──&gt; ONNX 引擎 <span class="hljs-punctuation">(</span><span class="hljs-number">4</span>核 CPU 并行 + RGA 硬件缩放, ~<span class="hljs-number">1</span>.<span class="hljs-number">5</span>s<span class="hljs-punctuation">)</span><br></code></pre></td></tr></table></figure><h3 id="1-三引擎智能分级与平滑回退"><a href="#1-三引擎智能分级与平滑回退" class="headerlink" title="1. 三引擎智能分级与平滑回退"></a>1. 三引擎智能分级与平滑回退</h3><p>不同用户手头的模型格式和硬件驱动准备情况不同。项目通过全局配置 <code>OCR_ENGINE=auto</code> 实现了模型自动扫描与分级调度：</p><ol><li><strong>第一优先级（RKNN NPU 加速）</strong>：检测到 <code>models/ocr/ppocrv4-rknn/</code> 下存在 <code>det.rknn</code> 与 <code>rec.rknn</code> 时，自动调用板载 0.8 TOPS NPU 执行硬件推理，耗时仅需 <strong>~80ms</strong>；</li><li><strong>第二优先级（NCNN NEON 极速引擎，开箱推荐）</strong>：搭载基于 PP-OCRv5 的移动端精简模型，深度启用 ARM NEON FP16 汇编级指令加速。模型总体量仅 <strong>11MB</strong>（识别模型仅 7.9MB），相比传统 90MB 的 ONNX 模型大幅节省 2GB 紧缺内存，推理耗时压制在 <strong>120~180ms</strong>；</li><li><strong>第三优先级（ONNX 保底回退）</strong>：在无定制模型时自动回退到通用 ONNX 引擎，结合 RGA 硬件等比缩放，确保 100% 可用性。</li></ol><h3 id="2-纯前端标注渲染：卸下服务端的绘图包袱"><a href="#2-纯前端标注渲染：卸下服务端的绘图包袱" class="headerlink" title="2. 纯前端标注渲染：卸下服务端的绘图包袱"></a>2. 纯前端标注渲染：卸下服务端的绘图包袱</h3><p>传统 OCR Web 服务在后端识别出文字框后，通常会在服务端使用 Python PIL &#x2F; OpenCV 读取图片、解析字体、逐像素绘制矩形框和半透明文本标签，然后再对大图二次编码为 JPEG 返回。</p><p>在 RK3566 这类嵌入式 CPU 上，画图与大图重编码的耗时往往比模型推理本身还要耗时，且极易导致瞬时内存暴涨。</p><p><strong>tspi-ocr-web 的优化方案</strong>：</p><ul><li><strong>后端专注推理</strong>：API 仅返回纯净、轻量的 JSON 结构体（包含 8 点坐标 <code>bbox</code>、文本及置信度）；</li><li><strong>前端负责渲染</strong>：Web UI 基于 <code>gr.AnnotatedImage</code> 动态渲染组件，直接调用客户端设备（用户的浏览器&#x2F;手机）的 Canvas 与 DOM 绘制交互式高亮线框与悬停标签。</li><li><strong>收益</strong>：板端服务端实现 <strong>零 CPU 画图耗时、零字体库常驻开销、零二次图像编码开销</strong>。</li></ul><h3 id="3-RGA-2D-硬件缩放与-DRM-DMA-BUF-显存直通"><a href="#3-RGA-2D-硬件缩放与-DRM-DMA-BUF-显存直通" class="headerlink" title="3. RGA 2D 硬件缩放与 DRM DMA-BUF 显存直通"></a>3. RGA 2D 硬件缩放与 DRM DMA-BUF 显存直通</h3><p>在图像输入前处理（Padding、等比缩放、色彩空间转换）阶段，项目编写了 C 语言底层扩展 <a href="https://github.com/bubao/tspi-ocr-web/blob/master/services/rga_helper.c"><code>services/rga_helper.c</code></a>，基于 Rockchip 官方 <code>librga</code> 的 <code>c_RkRgaGetAllocBuffer</code> + <code>wrapbuffer_fd_t</code> 实现 DRM DMA-BUF 连续物理内存分配，完美契合 Linux 6.1 内核标准，零 CPU 占用完成大图缩放。</p><hr><h2 id="🚀-快速上手与板端部署"><a href="#🚀-快速上手与板端部署" class="headerlink" title="🚀 快速上手与板端部署"></a>🚀 快速上手与板端部署</h2><h3 id="1-准备-Python-3-12-虚拟环境"><a href="#1-准备-Python-3-12-虚拟环境" class="headerlink" title="1. 准备 Python 3.12 虚拟环境"></a>1. 准备 Python 3.12 虚拟环境</h3><p>为了避免污染或破坏 Ubuntu 底层系统 Python（如 apt 等守护进程），建议使用独立编译部署的 Python 3.12 虚拟环境：</p><blockquote><p>👉 部署方法详见前文：<a href="/posts/929df792.html">RK3566 Ubuntu26.04 安装 Python3.12（多版本共存与隔离）</a></p></blockquote><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 克隆项目仓库</span><br>git <span class="hljs-built_in">clone</span> https://github.com/bubao/tspi-ocr-web.git<br><span class="hljs-built_in">cd</span> tspi-ocr-web<br><br><span class="hljs-comment"># 激活 Python 3.12 虚拟环境</span><br><span class="hljs-built_in">source</span> /path/to/py312_env/bin/activate<br><br><span class="hljs-comment"># 安装板端运行时依赖</span><br>pip install -r requirements.txt<br></code></pre></td></tr></table></figure><blockquote><p>⚠️ <strong>踩坑注意</strong>：嵌入式开发板通常没有安装桌面图形库（如 X11&#x2F;Wayland），依赖中必须安装无头版本 <code>opencv-python-headless</code>，切勿安装桌面版 <code>opencv-python</code>，否则会引发 <code>libGL.so.1: cannot open shared object file</code> 错误。</p></blockquote><h3 id="2-一键下载极速模型（-11MB）"><a href="#2-一键下载极速模型（-11MB）" class="headerlink" title="2. 一键下载极速模型（~11MB）"></a>2. 一键下载极速模型（~11MB）</h3><p>运行内置下载脚本，自动从国内镜像源拉取经过 NEON FP16 优化的 PP-OCRv5 NCNN 官方开源权重（出处源自 Hugging Face 社区 <a href="https://huggingface.co/qefro/pp-OCRv5-6-ncnn">qefro&#x2F;pp-OCRv5-6-ncnn</a>）：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 默认拉取 PP-OCRv5 NCNN 推荐模型 (~11MB)</span><br>python scripts/download_models.py<br></code></pre></td></tr></table></figure><blockquote><p>💡 <strong>原理深入</strong>：想了解如何使用 pnnx 手动将 PaddleOCR 导出为 NCNN FP16 模型及底层性能评测，详见专题博文：<br>👉 <a href="/posts/80a659d.html">泰山派 RK3566 部署 NCNN 极速推理实践</a></p></blockquote><h3 id="3-配置-RGA-驱动权限（可选，提升预处理性能）"><a href="#3-配置-RGA-驱动权限（可选，提升预处理性能）" class="headerlink" title="3. 配置 RGA 驱动权限（可选，提升预处理性能）"></a>3. 配置 RGA 驱动权限（可选，提升预处理性能）</h3><p>若需开启 RGA 2D 硬件缩放加速，需确保普通用户拥有 <code>/dev/rga</code> 访问权限：</p><blockquote><p>👉 驱动原理与 udev 规则详见前文：<a href="/posts/bc40b146.html">Rockchip 平台 librga 安装部署与权限问题排坑指南</a></p></blockquote><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 永久规则（推荐）</span><br><span class="hljs-built_in">echo</span> <span class="hljs-string">&#x27;KERNEL==&quot;rga&quot;, MODE=&quot;0666&quot;&#x27;</span> | <span class="hljs-built_in">sudo</span> <span class="hljs-built_in">tee</span> /etc/udev/rules.d/99-rga.rules<br><span class="hljs-built_in">sudo</span> udevadm control --reload-rules &amp;&amp; <span class="hljs-built_in">sudo</span> udevadm trigger<br></code></pre></td></tr></table></figure><h3 id="4-启动服务"><a href="#4-启动服务" class="headerlink" title="4. 启动服务"></a>4. 启动服务</h3><p>项目实现了 FastAPI 后端路由与 Gradio 交互式前端的<strong>单端口共用挂载</strong>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 单端口启动 API + Gradio Web UI（默认 8002 端口）</span><br>python main.py<br></code></pre></td></tr></table></figure><p>终端将打印：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs text">INFO:     [自动检测] 发现 NCNN 极速模型，启用 PP-OCRv5 (NEON FP16)<br>INFO:     Uvicorn running on http://0.0.0.0:8002 (Press CTRL+C to quit)<br></code></pre></td></tr></table></figure><ul><li><strong>Web UI 左右分栏工作台</strong>：<code>http://&lt;泰山派IP&gt;:8002/gradio/</code></li><li><strong>Swagger API 在线文档</strong>：<code>http://&lt;泰山派IP&gt;:8002/docs</code></li><li><strong>服务健康监控探针</strong>：<code>http://&lt;泰山派IP&gt;:8002/health</code></li></ul><hr><h2 id="📡-RESTful-API-调用实战"><a href="#📡-RESTful-API-调用实战" class="headerlink" title="📡 RESTful API 调用实战"></a>📡 RESTful API 调用实战</h2><h3 id="1-包含坐标元数据的完整-OCR-识别"><a href="#1-包含坐标元数据的完整-OCR-识别" class="headerlink" title="1. 包含坐标元数据的完整 OCR 识别"></a>1. 包含坐标元数据的完整 OCR 识别</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs bash">curl -X POST <span class="hljs-string">&quot;http://localhost:8002/api/v1/ocr/recognize&quot;</span> \<br>  -F <span class="hljs-string">&quot;file=@demo.png&quot;</span> \<br>  -F <span class="hljs-string">&quot;language=ch&quot;</span><br></code></pre></td></tr></table></figure><p>响应 JSON 结构体：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><code class="hljs json"><span class="hljs-punctuation">&#123;</span><br>  <span class="hljs-attr">&quot;success&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">true</span></span><span class="hljs-punctuation">,</span><br>  <span class="hljs-attr">&quot;text&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;泰山派 RK3566&quot;</span><span class="hljs-punctuation">,</span><br>  <span class="hljs-attr">&quot;results&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><br>    <span class="hljs-punctuation">&#123;</span><br>      <span class="hljs-attr">&quot;bbox&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-number">20.0</span><span class="hljs-punctuation">,</span> <span class="hljs-number">30.0</span><span class="hljs-punctuation">,</span> <span class="hljs-number">180.0</span><span class="hljs-punctuation">,</span> <span class="hljs-number">30.0</span><span class="hljs-punctuation">,</span> <span class="hljs-number">180.0</span><span class="hljs-punctuation">,</span> <span class="hljs-number">65.0</span><span class="hljs-punctuation">,</span> <span class="hljs-number">20.0</span><span class="hljs-punctuation">,</span> <span class="hljs-number">65.0</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span><br>      <span class="hljs-attr">&quot;text&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;泰山派 RK3566&quot;</span><span class="hljs-punctuation">,</span><br>      <span class="hljs-attr">&quot;confidence&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">0.985</span><br>    <span class="hljs-punctuation">&#125;</span><br>  <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span><br>  <span class="hljs-attr">&quot;processing_time&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">0.148</span><span class="hljs-punctuation">,</span><br>  <span class="hljs-attr">&quot;image_size&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span> <span class="hljs-attr">&quot;width&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">640</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;height&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">480</span> <span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">,</span><br>  <span class="hljs-attr">&quot;engine&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;PP-OCRv5 (NCNN)&quot;</span><span class="hljs-punctuation">,</span><br>  <span class="hljs-attr">&quot;backend&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;CPU (NCNN NEON FP16)&quot;</span><br><span class="hljs-punctuation">&#125;</span><br></code></pre></td></tr></table></figure><h3 id="2-纯文本提取接口（适合管道重定向）"><a href="#2-纯文本提取接口（适合管道重定向）" class="headerlink" title="2. 纯文本提取接口（适合管道重定向）"></a>2. 纯文本提取接口（适合管道重定向）</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash">curl -X POST <span class="hljs-string">&quot;http://localhost:8002/api/v1/ocr/text&quot;</span> \<br>  -F <span class="hljs-string">&quot;file=@demo.png&quot;</span><br></code></pre></td></tr></table></figure><hr><h2 id="⚡-进阶：启用-RKNN-NPU-硬件加速"><a href="#⚡-进阶：启用-RKNN-NPU-硬件加速" class="headerlink" title="⚡ 进阶：启用 RKNN NPU 硬件加速"></a>⚡ 进阶：启用 RKNN NPU 硬件加速</h2><p>若希望追求极致的低功耗与约 <strong>80ms</strong> 的推理响应，可以开启板载 NPU 硬件加速：</p><ol><li><strong>PC 端导出 RKNN 模型</strong>：<br>在 x86 电脑中配置 <code>rknn-toolkit2</code>（参考前文：<a href="/posts/778d7979.html">RK3566 搭建 RKNN-Toolkit2 环境笔记</a>），导出指定目标为 <code>target_platform=&quot;rk3566&quot;</code> 的 <code>det.rknn</code> 和 <code>rec.rknn</code>；</li><li><strong>放置模型</strong>：<br>将转换好的文件放入开发板的 <code>models/ocr/ppocrv4-rknn/</code> 目录；</li><li><strong>重启服务</strong>：<br>服务启动时将自动识别并切入 NPU 推理分支：<figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">🚀 [自动检测] 发现 RKNN 模型 (det.rknn, rec.rknn)，已自动启用 NPU 硬件加速推理<br></code></pre></td></tr></table></figure></li></ol><hr><h2 id="🛠️-Systemd-守护进程与开机自启"><a href="#🛠️-Systemd-守护进程与开机自启" class="headerlink" title="🛠️ Systemd 守护进程与开机自启"></a>🛠️ Systemd 守护进程与开机自启</h2><p>生产环境下，推荐将服务托付给 Systemd 守护进程。仓库中提供了自适应绝对路径与用户名的安装脚本：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 一键自动探测路径并配置开机自启</span><br><span class="hljs-built_in">sudo</span> ./scripts/install_service.sh<br><br><span class="hljs-comment"># 管理服务</span><br><span class="hljs-built_in">sudo</span> systemctl start tspi-ocr<br><span class="hljs-built_in">sudo</span> systemctl status tspi-ocr<br><br><span class="hljs-comment"># 实时查看日志</span><br><span class="hljs-built_in">sudo</span> journalctl -u tspi-ocr -f<br></code></pre></td></tr></table></figure><hr><h2 id="结语与项目开源地址"><a href="#结语与项目开源地址" class="headerlink" title="结语与项目开源地址"></a>结语与项目开源地址</h2><p>在资源紧缺的嵌入式边缘设备上运行深度学习，核心思路是<strong>因地制宜、物尽其用</strong>：</p><ul><li>用 <strong>NCNN NEON FP16 &#x2F; RKNN NPU</strong> 榨干计算硬件；</li><li>用 <strong>RGA</strong> 分担图像格式转换与缩放计算；</li><li>用 <strong>浏览器前端 Canvas 渲染</strong> 规避服务端低效的画图开销；</li><li>全流程内存直通，杜绝磁盘 IO 磨损闪存。</li></ul><p>欢迎在泰山派、鲁班猫等 RK3566 &#x2F; RK3568 开发板上体验并提出宝贵建议！</p><ul><li>🔗 <strong>GitHub 开源项目仓库</strong>：<a href="https://github.com/bubao/tspi-ocr-web">https://github.com/bubao/tspi-ocr-web</a></li><li>📖 <strong>相关前置技术文章</strong>：<ul><li><a href="/posts/929df792.html">RK3566 Ubuntu26.04 安装 Python3.12</a></li><li><a href="/posts/bc40b146.html">Rockchip 平台 librga 安装部署与权限问题排坑指南</a></li><li><a href="/posts/778d7979.html">RK3566 搭建 RKNN-Toolkit2 环境笔记</a></li><li><a href="/posts/80a659d.html">泰山派 RK3566 部署 NCNN 极速推理实践</a></li></ul></li></ul>]]>
    </content>
    <id>https://bubao.github.io/posts/f6340cce.html</id>
    <link href="https://bubao.github.io/posts/f6340cce.html"/>
    <published>2026-11-01T07:30:00.000Z</published>
    <summary>开源项目 tspi-ocr-web 详解：专为立创·泰山派（RK3566, 2GB内存）打造的高性能离线图片文字识别服务，集成了 RKNN (NPU) / NCNN (NEON FP16) / ONNX 三引擎自动分级、RGA 2D 硬件缩放加速与纯前端标注渲染架构。</summary>
    <title>泰山派 RK3566 离线 OCR 服务：tspi-ocr-web 实战</title>
    <updated>2026-10-31T20:37:58.478Z</updated>
  </entry>
  <entry>
    <author>
      <name>一念</name>
    </author>
    <category term="嵌入式" scheme="https://bubao.github.io/categories/%E5%B5%8C%E5%85%A5%E5%BC%8F/"/>
    <category term="教程" scheme="https://bubao.github.io/categories/%E5%B5%8C%E5%85%A5%E5%BC%8F/%E6%95%99%E7%A8%8B/"/>
    <category term="RK3566" scheme="https://bubao.github.io/tags/RK3566/"/>
    <category term="泰山派" scheme="https://bubao.github.io/tags/%E6%B3%B0%E5%B1%B1%E6%B4%BE/"/>
    <category term="Linux" scheme="https://bubao.github.io/tags/Linux/"/>
    <category term="RKNN" scheme="https://bubao.github.io/tags/RKNN/"/>
    <category term="NPU" scheme="https://bubao.github.io/tags/NPU/"/>
    <category term="深度学习" scheme="https://bubao.github.io/tags/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0/"/>
    <content>
      <![CDATA[<p>在基于瑞芯微 RK3566 &#x2F; RK3568 等芯片（如立创·泰山派、Radxa、鲁班猫）进行边缘 AI 应用开发时，官方的 NPU 工具链主要基于 <strong>RKNN-Toolkit2</strong>。初学者在刚接触该生态时，极易对官方仓库中多个形态各异的包产生混淆。</p><p>在动手部署前，必须首先厘清三者的核心分工与运行位置：</p><table><thead><tr><th align="left">组件名称</th><th align="left">运行环境</th><th align="left">主要职责与定位</th></tr></thead><tbody><tr><td align="left"><strong>rknn-toolkit2</strong></td><td align="left"><strong>PC 端（x86_64 Linux）</strong></td><td align="left">模型转换（ONNX&#x2F;PyTorch&#x2F;TF 转 <code>.rknn</code>）、量化、PC 端仿真、通过 USB&#x2F;ADB 远程调试板端 NPU。<strong>严禁&#x2F;无法在 RK3566 开发板上安装！</strong></td></tr><tr><td align="left"><strong>rknpu2</strong></td><td align="left"><strong>RK3566 板端（aarch64）</strong></td><td align="left">板端底层 NPU 运行时库，提供 C API（<code>rknn_api.h</code>）、动态库 <code>librknnrt.so</code> 及调试服务 <code>rknn_server</code>，用于 C&#x2F;C++ 推理。</td></tr><tr><td align="left"><strong>rknn-toolkit-lite2</strong></td><td align="left"><strong>RK3566 板端（aarch64）</strong></td><td align="left">板端专用的 <strong>Python 推理库</strong>，仅支持加载运行已转换好的 <code>.rknn</code> 格式模型，<strong>不具备</strong>模型转换与量化功能。</td></tr></tbody></table><p>官方统一仓库地址：<a href="https://github.com/airockchip/rknn-toolkit2">https://github.com/airockchip/rknn-toolkit2</a></p><p>本文将完整记录“<strong>PC 端转换环境搭建</strong> $\to$ <strong>RK3566 板端驱动检查</strong> $\to$ <strong>C++ 运行时部署</strong> $\to$ <strong>板端 Python lite2 推理环境配置</strong>”的端到端落地流程。</p><span id="more"></span><hr><h2 id="整体工作流概览"><a href="#整体工作流概览" class="headerlink" title="整体工作流概览"></a>整体工作流概览</h2><p>$$\begin{aligned}<br>\text{【PC 端 x86_64】}&amp;: \text{ONNX &#x2F; PyTorch 模型} \xrightarrow[\text{指定 target&#x3D;”rk3566”}]{\text{rknn-toolkit2 转换与量化}} \text{.rknn 模型文件} \<br>&amp;\quad\qquad\qquad\qquad\qquad\qquad\qquad\qquad\qquad\qquad\qquad\downarrow \text{拷贝至板端} \<br>\text{【板端 RK3566】}&amp;: \begin{cases}<br>\text{C&#x2F;C++ 工程} \longrightarrow \text{引入 rknn_api.h，链接 librknnrt.so} \<br>\text{Python 工程} \longrightarrow \text{通过 rknn-toolkit-lite2 加载并执行推理}<br>\end{cases}<br>\end{aligned}$$</p><hr><h2 id="一、PC-端（x86-64-Ubuntu）安装-rknn-toolkit2"><a href="#一、PC-端（x86-64-Ubuntu）安装-rknn-toolkit2" class="headerlink" title="一、PC 端（x86_64 Ubuntu）安装 rknn-toolkit2"></a>一、PC 端（x86_64 Ubuntu）安装 rknn-toolkit2</h2><blockquote><p>⚠️ <strong>红线提示</strong>：<code>rknn-toolkit2</code> 的完整包仅支持 x86_64 架构的 Linux&#x2F;Windows 系统，<strong>千万不要尝试在板子（aarch64）上安装它</strong>。</p></blockquote><h3 id="1-1-拉取官方仓库"><a href="#1-1-拉取官方仓库" class="headerlink" title="1.1 拉取官方仓库"></a>1.1 拉取官方仓库</h3><p>建议使用 <code>--depth 1</code> 浅克隆以节省网络流量与磁盘空间：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash">git <span class="hljs-built_in">clone</span> https://github.com/airockchip/rknn-toolkit2.git --depth 1<br><span class="hljs-built_in">cd</span> rknn-toolkit2<br></code></pre></td></tr></table></figure><h3 id="1-2-创建隔离的-Python-环境"><a href="#1-2-创建隔离的-Python-环境" class="headerlink" title="1.2 创建隔离的 Python 环境"></a>1.2 创建隔离的 Python 环境</h3><p>推荐使用 Python 3.8（官方支持与兼容最成熟）：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 使用 conda 或 venv 均可</span><br>conda create -n rknn2 python=3.8<br>conda activate rknn2<br></code></pre></td></tr></table></figure><h3 id="1-3-安装依赖与-Wheel-安装包"><a href="#1-3-安装依赖与-Wheel-安装包" class="headerlink" title="1.3 安装依赖与 Wheel 安装包"></a>1.3 安装依赖与 Wheel 安装包</h3><p>进入 x86_64 包目录安装依赖与 whl 本体：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">cd</span> rknn-toolkit2/packages/x86_64<br><br><span class="hljs-comment"># 1. 安装基础依赖（按实际 python 对应版本选择）</span><br>pip3 install -r requirements_cp38-*.txt<br><br><span class="hljs-comment"># 2. 安装 rknn-toolkit2 本体</span><br>pip3 install ./rknn_toolkit2-*-cp38-*-x86_64.whl<br></code></pre></td></tr></table></figure><h3 id="1-4-PC-端验证"><a href="#1-4-PC-端验证" class="headerlink" title="1.4 PC 端验证"></a>1.4 PC 端验证</h3><p>在 Python 终端中导入验证：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">from</span> rknn.api <span class="hljs-keyword">import</span> RKNN<br>rknn = RKNN()<br><span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;rknn-toolkit2 PC 端安装成功！&quot;</span>)<br></code></pre></td></tr></table></figure><p>在导出模型时，必须显式指定目标芯片平台为 <code>rk3566</code>：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-comment"># 示例关键参数：</span><br>rknn.config(target_platform=<span class="hljs-string">&quot;rk3566&quot;</span>)<br></code></pre></td></tr></table></figure><hr><h2 id="二、RK3566-板端准备工作"><a href="#二、RK3566-板端准备工作" class="headerlink" title="二、RK3566 板端准备工作"></a>二、RK3566 板端准备工作</h2><p>在 RK3566 开发板（如泰山派、Radxa、鲁班猫等）上执行。</p><h3 id="2-1-检查并开启板端-NPU-驱动"><a href="#2-1-检查并开启板端-NPU-驱动" class="headerlink" title="2.1 检查并开启板端 NPU 驱动"></a>2.1 检查并开启板端 NPU 驱动</h3><p>首先检查系统内核是否已经加载了 <code>rknpu</code> 驱动：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash">lsmod | grep rknpu<br>dmesg | grep -i npu<br></code></pre></td></tr></table></figure><ul><li>若看到类似 <code>[rknpu]: driver version: ...</code> 等输出，说明内核驱动已就绪。</li><li>若驱动未启用（例如部分 Radxa 系统）：<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> rsetup<br><span class="hljs-comment"># 进入 Overlays -&gt; Manage overlays -&gt; 开启 Enable NPU，保存并重启</span><br><span class="hljs-built_in">sudo</span> reboot<br></code></pre></td></tr></table></figure></li><li>若使用官方 APT 源自带 NPU 运行时包（如部分 Radxa OS）：<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> apt update<br><span class="hljs-built_in">sudo</span> apt install -y rknpu2-rk356x<br></code></pre></td></tr></table></figure></li></ul><h3 id="2-2-安装板端基础依赖"><a href="#2-2-安装板端基础依赖" class="headerlink" title="2.2 安装板端基础依赖"></a>2.2 安装板端基础依赖</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> apt update<br><span class="hljs-built_in">sudo</span> apt install -y python3 python3-pip python3-dev git gcc g++<br></code></pre></td></tr></table></figure><hr><h2 id="三、部署-rknpu2（板端-C-C-运行时）"><a href="#三、部署-rknpu2（板端-C-C-运行时）" class="headerlink" title="三、部署 rknpu2（板端 C&#x2F;C++ 运行时）"></a>三、部署 rknpu2（板端 C&#x2F;C++ 运行时）</h2><p>无论是运行 C&#x2F;C++ 程序还是后续运行 Python 推理库，底层都依赖 <code>librknnrt.so</code>。</p><p>在仓库中的源码目录结构如下：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs text">rknn-toolkit2/rknpu2/runtime/Linux/librknn_api/<br>├── include/<br>│   ├── rknn_api.h       # C/C++ 核心头文件<br>│   └── rknn_matmul_api.h<br>└── aarch64/<br>    └── librknnrt.so     # aarch64 平台运行时动态库<br></code></pre></td></tr></table></figure><h3 id="方式-A：通过-PC-端-adb-直接推送到板端（适合调试）"><a href="#方式-A：通过-PC-端-adb-直接推送到板端（适合调试）" class="headerlink" title="方式 A：通过 PC 端 adb 直接推送到板端（适合调试）"></a>方式 A：通过 PC 端 adb 直接推送到板端（适合调试）</h3><p>在 PC 端终端执行：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 推送动态库至板端系统库目录</span><br>adb push rknpu2/runtime/Linux/librknn_api/aarch64/librknnrt.so /usr/lib/<br><br><span class="hljs-comment"># （可选）推送 rknn_server 用于 PC 远程调试</span><br>adb push rknpu2/runtime/Linux/rknn_server/aarch64/usr/bin/rknn_server /usr/bin/<br><br><span class="hljs-comment"># 刷新板端动态链接缓存</span><br>adb shell ldconfig<br></code></pre></td></tr></table></figure><h3 id="方式-B：在板端直接下载-部署（推荐独立开发使用）"><a href="#方式-B：在板端直接下载-部署（推荐独立开发使用）" class="headerlink" title="方式 B：在板端直接下载&#x2F;部署（推荐独立开发使用）"></a>方式 B：在板端直接下载&#x2F;部署（推荐独立开发使用）</h3><p>直接在板端拉取仓库并部署：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs bash">git <span class="hljs-built_in">clone</span> https://github.com/airockchip/rknn-toolkit2.git --depth 1<br><span class="hljs-built_in">cd</span> rknn-toolkit2/rknpu2<br><br><span class="hljs-comment"># 1. 部署动态库</span><br><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">cp</span> runtime/Linux/librknn_api/aarch64/librknnrt.so /usr/lib/<br><span class="hljs-built_in">sudo</span> ldconfig<br><br><span class="hljs-comment"># 2. 部署头文件（供后续 C/C++ 自定义工程调用）</span><br><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">mkdir</span> -p /usr/include/rknpu2<br><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">cp</span> runtime/Linux/librknn_api/include/* /usr/include/rknpu2/<br></code></pre></td></tr></table></figure><blockquote><p>⚠️ <strong>架构选库说明</strong>：<br>RK3566 &#x2F; RK3568 属于标准 ARM aarch64 架构，使用的动态库名称为 **<code>librknnrt.so</code>**。切勿拷贝 RV 系列（如 RV1109&#x2F;RV1126 使用的 <code>librknnmrt.so</code>）。</p></blockquote><hr><h2 id="四、板端安装-rknn-toolkit-lite2（Python-推理库）"><a href="#四、板端安装-rknn-toolkit-lite2（Python-推理库）" class="headerlink" title="四、板端安装 rknn-toolkit-lite2（Python 推理库）"></a>四、板端安装 rknn-toolkit-lite2（Python 推理库）</h2><blockquote><p>⚠️ <strong>提示</strong>：<code>rknn-toolkit-lite2</code> 专用于板端运行推理，不具备模型转换功能。</p></blockquote><h3 id="4-1-安装对应-Python-版本的-Wheel-包"><a href="#4-1-安装对应-Python-版本的-Wheel-包" class="headerlink" title="4.1 安装对应 Python 版本的 Wheel 包"></a>4.1 安装对应 Python 版本的 Wheel 包</h3><p>建议结合前文构建的独立 Python 虚拟环境（如 <a href="/posts/929df792.html">RK3566 Ubuntu26.04 安装 Python3.12</a>）或系统默认 Python：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 查看板端 Python 版本</span><br>python3 --version<br><br><span class="hljs-comment"># 进入板端 lite2 包目录</span><br><span class="hljs-built_in">cd</span> rknn-toolkit2/rknn-toolkit-lite2/packages<br><br><span class="hljs-comment"># 选择对应 python 版本的 aarch64 wheel 安装（以当前 python 版本匹配对应的 cp 文件）</span><br>pip3 install ./rknn_toolkit_lite2-*-manylinux_2_17_aarch64.whl<br></code></pre></td></tr></table></figure><p>也可以直接通过 pip 在线源安装（需确保源中有匹配平台构型的 wheel）：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">pip3 install rknn-toolkit-lite2<br></code></pre></td></tr></table></figure><h3 id="4-2-板端最小推理验证"><a href="#4-2-板端最小推理验证" class="headerlink" title="4.2 板端最小推理验证"></a>4.2 板端最小推理验证</h3><p>编写简单的验证脚本，加载 PC 端转换好拷贝过来的 <code>.rknn</code> 模型测试：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">from</span> rknnlite.api <span class="hljs-keyword">import</span> RKNNLite<br><br><span class="hljs-comment"># 1. 初始化</span><br>rknn_lite = RKNNLite()<br><br><span class="hljs-comment"># 2. 加载模型</span><br>ret = rknn_lite.load_rknn_model(<span class="hljs-string">&quot;./test.rknn&quot;</span>)<br><span class="hljs-keyword">if</span> ret != <span class="hljs-number">0</span>:<br>    <span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;加载 RKNN 模型失败！&quot;</span>)<br>    exit(ret)<br><br><span class="hljs-comment"># 3. 初始化板端运行环境</span><br>ret = rknn_lite.init_runtime(target=<span class="hljs-string">&quot;rk3566&quot;</span>)<br><span class="hljs-keyword">if</span> ret != <span class="hljs-number">0</span>:<br>    <span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;初始化 NPU 运行时失败，请检查驱动与 librknnrt.so！&quot;</span>)<br>    exit(ret)<br><br><span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;✅ RKNNLite 在 RK3566 上初始化成功！&quot;</span>)<br><br><span class="hljs-comment"># 4. 执行推理（传入预处理后的 numpy ndarray）</span><br><span class="hljs-comment"># outputs = rknn_lite.inference(inputs=[input_data])</span><br><br><span class="hljs-comment"># 5. 释放资源</span><br>rknn_lite.release()<br></code></pre></td></tr></table></figure><hr><h2 id="五、与硬件加速流水线的衔接（MPP-RGA-RKNN）"><a href="#五、与硬件加速流水线的衔接（MPP-RGA-RKNN）" class="headerlink" title="五、与硬件加速流水线的衔接（MPP + RGA + RKNN）"></a>五、与硬件加速流水线的衔接（MPP + RGA + RKNN）</h2><p>至此，RK3566 平台上的视觉处理三大件已全部就绪：</p><ol><li><strong>环境准备</strong>：<a href="/posts/929df792.html">RK3566 Ubuntu26.04 安装 Python3.12</a> 提供了隔离的多版本 Python 运行环境；</li><li><strong>硬件编解码与预处理</strong>：<a href="/posts/bc40b146.html">Rockchip 平台 librga 安装部署与权限问题排坑指南</a> 提供了 <code>MPP</code> 高速视频解码与 <code>RGA</code> 零 CPU 开销的格式转换与缩放；</li><li><strong>边缘 AI 推理</strong>：本篇搭建的 <code>rknpu2</code> &#x2F; <code>rknn-toolkit-lite2</code> 负责接收 RGA 预处理完成的张量，直接送进 NPU 硬件完成高速神经网络推理。</li></ol><hr><h2 id="六、常见踩坑与排障清单"><a href="#六、常见踩坑与排障清单" class="headerlink" title="六、常见踩坑与排障清单"></a>六、常见踩坑与排障清单</h2><ol><li>**试图在 RK3566 板端执行 <code>pip install rknn-toolkit2</code>**：<ul><li>报错无法找到符合架构的包。<strong>记住原则</strong>：转换在 PC（x86），板端只装 <code>lite2</code> 和 <code>rknpu2</code>。</li></ul></li><li>**板端运行报错 <code>librknnrt.so: cannot open shared object file</code>**：<ul><li>确认动态库已拷贝至 <code>/usr/lib/</code> 或 <code>/usr/lib/aarch64-linux-gnu/</code>，且必须执行 <code>sudo ldconfig</code> 刷新系统链接器缓存。</li></ul></li><li><strong>PC 转换的模型在板端初始化失败</strong>：<ul><li>检查 PC 端转换脚本中 <code>rknn.config()</code> 是否指定了 <code>target_platform=&quot;rk3566&quot;</code>。若未指定或选错平台，生成的指令集与权重结构板端 NPU 无法识别。</li></ul></li><li><strong><code>rknnlite</code> 导入时报错版本不兼容</strong>：<ul><li><code>wheel</code> 包的文件名必须严格对应 Python 解释器的具体版本（如 <code>cp38</code> 对应 Python 3.8，<code>cp312</code> 对应 Python 3.12）。</li></ul></li><li><strong>C&#x2F;C++ 工程编译报错找不到符号</strong>：<ul><li>链接参数中需增加 <code>-lrknnrt</code>，并确保头文件目录包含 <code>rknn_api.h</code>。若在 PC 端进行交叉编译，请确认使用的是 <code>aarch64-linux-gnu-gcc</code> 交叉工具链。</li></ul></li></ol>]]>
    </content>
    <id>https://bubao.github.io/posts/778d7979.html</id>
    <link href="https://bubao.github.io/posts/778d7979.html"/>
    <published>2026-11-01T02:15:00.000Z</published>
    <summary>厘清 rknn-toolkit2、rknpu2 与 rknn-toolkit-lite2 的组件定位，详细记录在 PC 端搭建模型转换环境、在 RK3566 板端部署 NPU 驱动、C++ 运行时 librknnrt.so 与 Python 推理库的完整流程及避坑指南。</summary>
    <title>RK3566 搭建 RKNN-Toolkit2 环境笔记</title>
    <updated>2026-10-31T20:37:58.468Z</updated>
  </entry>
  <entry>
    <author>
      <name>一念</name>
    </author>
    <category term="嵌入式" scheme="https://bubao.github.io/categories/%E5%B5%8C%E5%85%A5%E5%BC%8F/"/>
    <category term="教程" scheme="https://bubao.github.io/categories/%E5%B5%8C%E5%85%A5%E5%BC%8F/%E6%95%99%E7%A8%8B/"/>
    <category term="RK3566" scheme="https://bubao.github.io/tags/RK3566/"/>
    <category term="泰山派" scheme="https://bubao.github.io/tags/%E6%B3%B0%E5%B1%B1%E6%B4%BE/"/>
    <category term="Linux" scheme="https://bubao.github.io/tags/Linux/"/>
    <category term="Rockchip" scheme="https://bubao.github.io/tags/Rockchip/"/>
    <category term="RK3568" scheme="https://bubao.github.io/tags/RK3568/"/>
    <category term="MPP" scheme="https://bubao.github.io/tags/MPP/"/>
    <category term="RGA" scheme="https://bubao.github.io/tags/RGA/"/>
    <content>
      <![CDATA[<p>在基于瑞芯微（Rockchip）芯片（如 RK3566、RK3568、RK3588 等）的嵌入式 Linux 开发中，多媒体与边缘 AI 处理有一条经典的高性能流水线：</p><p>$$\text{MPP (硬件编解码)} \longrightarrow \text{RGA (图像预处理)} \longrightarrow \text{RKNN (神经网络推理)}$$</p><ul><li><strong>MPP（Media Process Platform）</strong>：调用 VPU 硬件，对高分辨率视频流（H.264&#x2F;H.265&#x2F;VP9 等）进行低延迟解码，输出原始图像帧（通常为 YUV&#x2F;NV12 格式）。</li><li><strong>RGA（Raster Graphic Acceleration Unit）</strong>：调用专用 2D 硬件加速器，在<strong>零 CPU 占用</strong>下完成图像格式转换（如 NV12 转 BGR&#x2F;RGB）、硬件缩放、居中裁剪及旋转，将图像尺寸与色彩空间对齐为模型输入要求。</li><li><strong>RKNN</strong>：利用 NPU 硬件对预处理后的张量执行深度学习模型推理。</li></ul><p>这两大硬件加速库在部署上有显著区别：<strong>MPP 没有官方预编译成品库，必须在本地或交叉编译环境中由源码构建；而 librga 仓库内置了各架构的预编译成品库，直接部署即可</strong>。</p><p>本文以 aarch64 平台（立创·泰山派 RK3566、野火鲁班猫等）为例，详细记录 MPP 的本机源码编译（包含低内存防 OOM 技巧）与部署、librga 的规范安装验证，以及普通用户调用 RGA 时 <code>/dev/rga</code> 权限拒绝的完整排坑与永久解决方案。</p><span id="more"></span><hr><h2 id="一、前置驱动节点检查"><a href="#一、前置驱动节点检查" class="headerlink" title="一、前置驱动节点检查"></a>一、前置驱动节点检查</h2><p>无论是使用 MPP 还是 RGA，均依赖内核底层对应的硬件驱动支持。在部署应用库前，请务必先检查设备节点：</p><h3 id="1-1-检查-MPP-硬件驱动（VPU）"><a href="#1-1-检查-MPP-硬件驱动（VPU）" class="headerlink" title="1.1 检查 MPP 硬件驱动（VPU）"></a>1.1 检查 MPP 硬件驱动（VPU）</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">ls</span> -l /dev/mpp_service<br></code></pre></td></tr></table></figure><ul><li>若输出类似 <code>crw-rw---- 1 root video ... /dev/mpp_service</code>，说明内核 VPU 驱动已就绪。</li><li>若提示 <code>No such file or directory</code>，说明当前固件内核缺少 MPP 驱动，硬件视频编解码将无法使用。</li></ul><h3 id="1-2-检查-RGA-硬件驱动"><a href="#1-2-检查-RGA-硬件驱动" class="headerlink" title="1.2 检查 RGA 硬件驱动"></a>1.2 检查 RGA 硬件驱动</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">ls</span> -l /dev/rga<br></code></pre></td></tr></table></figure><ul><li>若输出类似 <code>crw-rw---- 1 root video ... /dev/rga</code>，说明内核 RGA 驱动工作正常。</li><li>若缺失此节点，多见于第三方的极简版 Armbian 固件，此时需更换官方标准发行版固件或为内核配置并编译 <code>rkrga</code> 模块。</li></ul><hr><h2 id="二、Rockchip-MPP-本机源码编译与部署"><a href="#二、Rockchip-MPP-本机源码编译与部署" class="headerlink" title="二、Rockchip MPP 本机源码编译与部署"></a>二、Rockchip MPP 本机源码编译与部署</h2><p>MPP（Media Process Platform）是瑞芯微官方的 VPU 编解码硬件库，仅负责视频编解码，与 NPU AI 推理独立：</p><ul><li><strong>支持硬件解码</strong>：H.265 &#x2F; H.264 &#x2F; H.263 &#x2F; VP9 &#x2F; VP8 &#x2F; MPEG-4 &#x2F; MPEG-2 &#x2F; MPEG-1 &#x2F; VC1 &#x2F; MJPEG &#x2F; AV1</li><li><strong>支持硬件编码</strong>：H.265 &#x2F; H.264 &#x2F; VP8 &#x2F; MJPEG</li><li><strong>官方仓库</strong>：<a href="https://github.com/rockchip-linux/mpp">https://github.com/rockchip-linux/mpp</a></li><li><strong>官方开发指南</strong>：仓库内 <code>doc/Rockchip_Developer_Guide_MPP_CN.md</code></li></ul><h3 id="2-1-安装编译依赖"><a href="#2-1-安装编译依赖" class="headerlink" title="2.1 安装编译依赖"></a>2.1 安装编译依赖</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> apt update<br><span class="hljs-built_in">sudo</span> apt install -y git cmake make gcc g++ ffmpeg<br></code></pre></td></tr></table></figure><h3 id="2-2-拉取-MPP-源码"><a href="#2-2-拉取-MPP-源码" class="headerlink" title="2.2 拉取 MPP 源码"></a>2.2 拉取 MPP 源码</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 官方 GitHub 地址</span><br>git <span class="hljs-built_in">clone</span> https://github.com/rockchip-linux/mpp.git<br><br><span class="hljs-comment"># 国内加速可选：</span><br><span class="hljs-comment"># git clone https://mirror.ghproxy.com/https://github.com/rockchip-linux/mpp.git</span><br><span class="hljs-comment"># 或者 Gitee 镜像：</span><br><span class="hljs-comment"># git clone https://gitee.com/mirrors/rockchip-mpp.git</span><br><br><span class="hljs-built_in">cd</span> mpp<br></code></pre></td></tr></table></figure><h3 id="2-3-关键防坑：低内存开发板防-OOM-编译"><a href="#2-3-关键防坑：低内存开发板防-OOM-编译" class="headerlink" title="2.3 关键防坑：低内存开发板防 OOM 编译"></a>2.3 关键防坑：低内存开发板防 OOM 编译</h3><p>立创·泰山派 RK3566 等开发板常为 1GB 或 2GB 内存版本。在多核全开编译大型 C++ 项目时，极其容易触发内核 <strong>OOM Killer</strong>，导致编译中途直接报错 <code>Killed</code>。</p><p>进入 aarch64 编译构建目录：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">cd</span> build/linux/aarch64<br></code></pre></td></tr></table></figure><p>修改编译脚本 <code>make-Makefiles.bash</code>，将并发编译参数改为单线程 <code>-j1</code>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">vim make-Makefiles.bash<br></code></pre></td></tr></table></figure><p>找到关于 <code>cmake --build</code> 的分支逻辑，修改为 <code>-j1</code>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-keyword">if</span> [ <span class="hljs-string">&quot;<span class="hljs-variable">$&#123;CMAKE_PARALLEL_ENABLE&#125;</span>&quot;</span> = <span class="hljs-string">&quot;0&quot;</span> ]; <span class="hljs-keyword">then</span><br>    cmake --build .<br><span class="hljs-keyword">else</span><br>    cmake --build . -j1<br><span class="hljs-keyword">fi</span><br></code></pre></td></tr></table></figure><p>执行编译构建：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">./make-Makefiles.bash<br></code></pre></td></tr></table></figure><h3 id="2-4-部署-MPP-头文件"><a href="#2-4-部署-MPP-头文件" class="headerlink" title="2.4 部署 MPP 头文件"></a>2.4 部署 MPP 头文件</h3><p>编译完成后，当前位于 <code>build/linux/aarch64</code> 目录下，将源码 include 拷贝至系统目录：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">mkdir</span> -p /usr/include/mpp<br><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">cp</span> ../../../inc/* /usr/include/mpp/<br></code></pre></td></tr></table></figure><p>后续 C&#x2F;C++ 工程中即可按官方规范引用：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&lt;mpp/rk_mpi.h&gt;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&lt;mpp/mpp_err.h&gt;</span></span><br></code></pre></td></tr></table></figure><h3 id="2-5-部署-MPP-动态库（推荐）"><a href="#2-5-部署-MPP-动态库（推荐）" class="headerlink" title="2.5 部署 MPP 动态库（推荐）"></a>2.5 部署 MPP 动态库（推荐）</h3><p>在 <code>build/linux/aarch64/mpp/</code> 目录下生成了以下产物：</p><ul><li><code>librockchip_mpp.so.0</code>（动态库本体）</li><li><code>librockchip_mpp.so.1</code>、<code>librockchip_mpp.so</code>（软链接）</li><li><code>librockchip_mpp.a</code>（静态库）</li></ul><p><strong>注意</strong>：部署时必须将库本体与软链接一同拷贝，否则动态链接器在运行时可能找不到符号：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">cd</span> mpp<br><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">cp</span> librockchip_mpp.so.0 librockchip_mpp.so.1 librockchip_mpp.so /usr/lib/aarch64-linux-gnu/<br><span class="hljs-built_in">sudo</span> ldconfig<br></code></pre></td></tr></table></figure><h3 id="2-6-部署-MPP-静态库（可选）"><a href="#2-6-部署-MPP-静态库（可选）" class="headerlink" title="2.6 部署 MPP 静态库（可选）"></a>2.6 部署 MPP 静态库（可选）</h3><p>如需脱离系统动态库独立静态打包，可复制静态库：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">cp</span> librockchip_mpp.a /usr/lib/aarch64-linux-gnu/<br></code></pre></td></tr></table></figure><h3 id="2-7-MPP-校验与版本查看"><a href="#2-7-MPP-校验与版本查看" class="headerlink" title="2.7 MPP 校验与版本查看"></a>2.7 MPP 校验与版本查看</h3><p>确认系统已成功载入动态库：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">ldconfig -p | grep rockchip_mpp<br></code></pre></td></tr></table></figure><p>运行编译产物中的测试工具查看 MPP 详细版本及支持特性：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 回到 build/linux/aarch64 目录</span><br><span class="hljs-built_in">cd</span> ..<br>./test/mpp_info_test<br></code></pre></td></tr></table></figure><hr><h2 id="三、Rockchip-librga-部署与配置"><a href="#三、Rockchip-librga-部署与配置" class="headerlink" title="三、Rockchip librga 部署与配置"></a>三、Rockchip librga 部署与配置</h2><p>RGA（Raster Graphic Acceleration Unit）是 2D 图形硬件加速器，用于快速执行绘制、缩放、旋转、bitBlt、alpha 混合以及色彩空间转换（如 NV12 到 RGB&#x2F;BGR），在图像预处理阶段性能远高于 CPU OpenCV 软转换。</p><ul><li><strong>官方仓库</strong>：<a href="https://github.com/airockchip/librga">https://github.com/airockchip/librga</a></li><li><strong>官方开发指南</strong>：<a href="https://github.com/airockchip/librga/blob/main/docs/Rockchip_Developer_Guide_RGA_CN.md">Rockchip_Developer_Guide_RGA_CN.md</a></li><li><strong>官方 FAQ</strong>：<a href="https://github.com/airockchip/librga/blob/main/docs/Rockchip_FAQ_RGA_CN.md">Rockchip_FAQ_RGA_CN.md</a></li></ul><h3 id="3-1-库类型说明"><a href="#3-1-库类型说明" class="headerlink" title="3.1 库类型说明"></a>3.1 库类型说明</h3><p>官方仓库自带各主流平台的预编译二进制文件，<strong>无需从源码重新编译</strong>：</p><ul><li><code>librga.so</code>（<strong>动态库，推荐</strong>）：应用开发、调试以及 Python 使用 <code>ctypes</code> 调用时必需。</li><li><code>librga.a</code>（<strong>静态库，可选</strong>）：供 C&#x2F;C++ 静态编译，Python 无法调用。</li></ul><blockquote><p>💡 <strong>环境准备建议</strong>：<br>若后续需在 Python 环境下通过 <code>ctypes</code> 调用 RGA（例如实现视频帧预处理并对接 RKNN 推理），建议配合独立的 Python 虚拟环境运行，避免污染或破坏系统环境。可参考前文：<br>👉 <a href="/posts/929df792.html">RK3566 Ubuntu26.04 安装 Python3.12</a></p></blockquote><h3 id="3-2-获取源码仓库"><a href="#3-2-获取源码仓库" class="headerlink" title="3.2 获取源码仓库"></a>3.2 获取源码仓库</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs bash">git <span class="hljs-built_in">clone</span> https://github.com/airockchip/librga.git<br><br><span class="hljs-comment"># 国内加速镜像：</span><br><span class="hljs-comment"># git clone https://mirror.ghproxy.com/https://github.com/airockchip/librga.git</span><br><br><span class="hljs-built_in">cd</span> librga<br></code></pre></td></tr></table></figure><h3 id="3-3-部署头文件"><a href="#3-3-部署头文件" class="headerlink" title="3.3 部署头文件"></a>3.3 部署头文件</h3><p>仓库 <code>include/</code> 下直接是 <code>.h/.hpp</code> 文件。为规范代码结构，将其拷贝至 <code>/usr/include/rga/</code>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">mkdir</span> -p /usr/include/rga<br><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">cp</span> include/* /usr/include/rga/<br></code></pre></td></tr></table></figure><p>代码中按如下方式引用：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&lt;rga/im2d.h&gt;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&lt;rga/rga.h&gt;</span></span><br></code></pre></td></tr></table></figure><h3 id="3-4-部署动态库与静态库"><a href="#3-4-部署动态库与静态库" class="headerlink" title="3.4 部署动态库与静态库"></a>3.4 部署动态库与静态库</h3><p>针对 aarch64 平台，部署预编译库到系统库目录并刷新缓存：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 部署动态库（推荐）</span><br><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">cp</span> libs/Linux/gcc-aarch64/librga.so /usr/lib/aarch64-linux-gnu/<br><span class="hljs-built_in">sudo</span> ldconfig<br><br><span class="hljs-comment"># 部署静态库（可选）</span><br><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">cp</span> libs/Linux/gcc-aarch64/librga.a /usr/lib/aarch64-linux-gnu/<br></code></pre></td></tr></table></figure><h3 id="3-5-版本查询与日志调试"><a href="#3-5-版本查询与日志调试" class="headerlink" title="3.5 版本查询与日志调试"></a>3.5 版本查询与日志调试</h3><p>确认动态库已注册进系统链接器：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">ldconfig -p | grep librga<br></code></pre></td></tr></table></figure><p>查询 librga API 版本号：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">strings /usr/lib/aarch64-linux-gnu/librga.so | grep rga_api | grep version<br></code></pre></td></tr></table></figure><p>查询当前内核 RGA 驱动版本：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">cat</span> /sys/kernel/debug/rkrga/driver_version<br><span class="hljs-comment"># 若 debugfs 不可用，可查询 proc 节点</span><br><span class="hljs-built_in">cat</span> /proc/rkrga/driver_version<br></code></pre></td></tr></table></figure><p>开启详细运行时调试日志（librga $\ge$ 1.9.0 支持）：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">export</span> ROCKCHIP_RGA_LOG=1<br><span class="hljs-built_in">export</span> ROCKCHIP_RGA_LOG_LEVEL=3<br></code></pre></td></tr></table></figure><hr><h2 id="四、踩坑排查：调用-RGA-时的-dev-rga-权限问题"><a href="#四、踩坑排查：调用-RGA-时的-dev-rga-权限问题" class="headerlink" title="四、踩坑排查：调用 RGA 时的 &#x2F;dev&#x2F;rga 权限问题"></a>四、踩坑排查：调用 RGA 时的 &#x2F;dev&#x2F;rga 权限问题</h2><h3 id="4-1-问题现象"><a href="#4-1-问题现象" class="headerlink" title="4.1 问题现象"></a>4.1 问题现象</h3><p>在开发板上以非 root 用户（如默认用户 <code>cat</code>、<code>pi</code>、<code>linaro</code>、<code>ubuntu</code> 等）运行 Python 脚本或 C&#x2F;C++ 程序时，经常出现 RGA 初始化失败报错：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">E [librga]: rga_init: failed to open /dev/rga, errno=13 (Permission denied)<br></code></pre></td></tr></table></figure><p>查看设备权限 <code>ls -l /dev/rga</code> 会发现其权限默认为 <code>crw-rw---- 1 root video ...</code>，普通用户无权读写该设备节点。</p><h3 id="4-2-临时解决方案"><a href="#4-2-临时解决方案" class="headerlink" title="4.2 临时解决方案"></a>4.2 临时解决方案</h3><p>最快速的临时方案是直接修改节点权限：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">chmod</span> a+rw /dev/rga<br></code></pre></td></tr></table></figure><blockquote><p>⚠️ <strong>缺陷</strong>：Linux 系统的 <code>/dev</code> 属于虚拟内存文件系统（<code>devtmpfs</code>）。<strong>设备重启后节点会重新生成，权限立即恢复原样</strong>，导致再次开机应用又报权限拒绝。</p></blockquote><h3 id="4-3-永久解决方案（推荐）"><a href="#4-3-永久解决方案（推荐）" class="headerlink" title="4.3 永久解决方案（推荐）"></a>4.3 永久解决方案（推荐）</h3><h4 id="方案-A：配置-udev-规则（最通用可靠）"><a href="#方案-A：配置-udev-规则（最通用可靠）" class="headerlink" title="方案 A：配置 udev 规则（最通用可靠）"></a>方案 A：配置 udev 规则（最通用可靠）</h4><p>通过在 <code>/etc/udev/rules.d/</code> 中添加规则，让系统在每次创建 <code>/dev/rga</code> 节点时自动将其模式设置为 <code>0666</code>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 写入 udev 规则</span><br><span class="hljs-built_in">echo</span> <span class="hljs-string">&#x27;KERNEL==&quot;rga&quot;, MODE=&quot;0666&quot;&#x27;</span> | <span class="hljs-built_in">sudo</span> <span class="hljs-built_in">tee</span> /etc/udev/rules.d/99-rga.rules<br><br><span class="hljs-comment"># 重新加载规则并立即触发</span><br><span class="hljs-built_in">sudo</span> udevadm control --reload-rules<br><span class="hljs-built_in">sudo</span> udevadm trigger<br></code></pre></td></tr></table></figure><p>检查节点权限确认生效：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">ls</span> -l /dev/rga<br><span class="hljs-comment"># 正常输出：crw-rw-rw- 1 root video ... /dev/rga</span><br></code></pre></td></tr></table></figure><h4 id="方案-B：将当前用户加入所属设备组"><a href="#方案-B：将当前用户加入所属设备组" class="headerlink" title="方案 B：将当前用户加入所属设备组"></a>方案 B：将当前用户加入所属设备组</h4><p>如果不想将设备节点全局开放为 <code>0666</code>，可将当前运行用户添加至所属的 <code>video</code> 用户组：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> usermod -aG video <span class="hljs-variable">$USER</span><br></code></pre></td></tr></table></figure><blockquote><p><strong>提示</strong>：修改用户组后需<strong>注销并重新登录</strong>当前会话方可生效。</p></blockquote><hr><h2 id="五、卸载与环境清理"><a href="#五、卸载与环境清理" class="headerlink" title="五、卸载与环境清理"></a>五、卸载与环境清理</h2><p>如需升级版本或重置开发板系统环境，可执行以下命令清理：</p><h3 id="5-1-卸载-MPP"><a href="#5-1-卸载-MPP" class="headerlink" title="5.1 卸载 MPP"></a>5.1 卸载 MPP</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">rm</span> -f /usr/lib/aarch64-linux-gnu/librockchip_mpp.so*<br><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">rm</span> -f /usr/lib/aarch64-linux-gnu/librockchip_mpp.a<br><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">rm</span> -rf /usr/include/mpp<br><span class="hljs-built_in">sudo</span> ldconfig<br></code></pre></td></tr></table></figure><h3 id="5-2-卸载-librga"><a href="#5-2-卸载-librga" class="headerlink" title="5.2 卸载 librga"></a>5.2 卸载 librga</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">rm</span> -f /usr/lib/aarch64-linux-gnu/librga.so<br><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">rm</span> -f /usr/lib/aarch64-linux-gnu/librga.a<br><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">rm</span> -rf /usr/include/rga<br><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">rm</span> -f /etc/udev/rules.d/99-rga.rules<br><span class="hljs-built_in">sudo</span> ldconfig<br></code></pre></td></tr></table></figure><hr><h2 id="六、总结"><a href="#六、总结" class="headerlink" title="六、总结"></a>六、总结</h2><p>在 Rockchip 嵌入式硬件加速体系中：</p><ol><li><strong>MPP</strong> 承担编解码重任，部署关键在<strong>本机编译的内存保护</strong>（修改脚本采用 <code>-j1</code> 单线程降并发防 OOM）；</li><li><strong>librga</strong> 承担 2D 硬件预处理重任，部署关键在<strong>利用官方预编译库快速归档规范</strong>；</li><li><strong>权限机制</strong>不可忽视，配置 <strong>udev 规则</strong>可以一劳永逸避免生产&#x2F;调试环境中因普通用户权限导致的 <code>/dev/rga</code> 调用崩溃。</li></ol><p>两者结合，即可为后续的 RKNN 边缘 AI 推理提供高吞吐、零 CPU 瓶颈的底层多媒体加速支持。</p><hr><h3 id="相关阅读与链路实践"><a href="#相关阅读与链路实践" class="headerlink" title="相关阅读与链路实践"></a>相关阅读与链路实践</h3><ul><li><strong>Python 环境隔离</strong>：<a href="/posts/929df792.html">RK3566 Ubuntu26.04 安装 Python3.12</a></li><li><strong>边缘 AI 推理落地</strong>：<a href="/posts/778d7979.html">RK3566 搭建 RKNN-Toolkit2 环境笔记</a></li></ul>]]>
    </content>
    <id>https://bubao.github.io/posts/98857091.html</id>
    <link href="https://bubao.github.io/posts/98857091.html"/>
    <published>2026-10-31T16:43:54.000Z</published>
    <summary>记录在 RK3566 / RK3568 等 Rockchip 平台（立创·泰山派、鲁班猫）下编译部署 MPP 视频硬件编解码库、部署 librga 2D 图形加速库的完整流程，以及调用 RGA 时遇到 /dev/rga 权限问题的排查与永久解决方案。</summary>
    <title>Rockchip 平台 librga 安装部署与权限问题排坑指南</title>
    <updated>2026-10-31T20:37:58.469Z</updated>
  </entry>
  <entry>
    <author>
      <name>一念</name>
    </author>
    <category term="嵌入式" scheme="https://bubao.github.io/categories/%E5%B5%8C%E5%85%A5%E5%BC%8F/"/>
    <category term="教程" scheme="https://bubao.github.io/categories/%E5%B5%8C%E5%85%A5%E5%BC%8F/%E6%95%99%E7%A8%8B/"/>
    <category term="RK3566" scheme="https://bubao.github.io/tags/RK3566/"/>
    <category term="Python" scheme="https://bubao.github.io/tags/Python/"/>
    <category term="Ubuntu" scheme="https://bubao.github.io/tags/Ubuntu/"/>
    <category term="泰山派" scheme="https://bubao.github.io/tags/%E6%B3%B0%E5%B1%B1%E6%B4%BE/"/>
    <category term="Linux" scheme="https://bubao.github.io/tags/Linux/"/>
    <content>
      <![CDATA[<p>在 RK3566 开发板（如立创·泰山派）运行 Ubuntu 系统时，由于后续部署视觉处理库（如 RGA Python ctypes 绑定、OpenCV）以及 RKNN 深度学习推理时，很多项目依赖固定版本的 Python 3.12 环境。</p><p>然而，在 Ubuntu 上升级或安装非系统默认版本的 Python 时，<strong>存在一个极高风险的陷阱</strong>：Ubuntu 底层的包管理工具（<code>apt</code>）、系统升级工具以及诸多系统守护进程严重依赖系统预装的 Python 版本。如果强行替换软链接或执行 <code>make install</code>，极易导致 <code>apt</code> 损坏、桌面环境崩溃等严重系统问题。</p><p>本文记录在 RK3566 aarch64 Ubuntu 系统下，通过源码编译并在独立目录安装 Python 3.12.8，通过 <code>altinstall</code> 实现与系统默认 Python 完美共存，并针对 2GB 内存开发板提供防 OOM 编译技巧。</p><span id="more"></span><hr><h2 id="一、安装编译依赖"><a href="#一、安装编译依赖" class="headerlink" title="一、安装编译依赖"></a>一、安装编译依赖</h2><p>编译 Python 需要一系列底层库（包括 OpenSSL、SQLite、FFI 等），若编译前未安装完备，后续会出现 <code>pip</code> 无法拉取 HTTPS 包、无法导入 <code>_sqlite3</code> 等疑难杂症：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> apt update<br><span class="hljs-built_in">sudo</span> apt install -y build-essential libssl-dev zlib1g-dev \<br>  libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm \<br>  libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev \<br>  libffi-dev liblzma-dev<br></code></pre></td></tr></table></figure><blockquote><p><strong>踩坑重点</strong>：</p><ul><li><code>libssl-dev</code>：必须安装，否则编译生成的 Python 会缺少 <code>_ssl</code> 模块，导致 <code>pip</code> 无法通过 HTTPS 下载任何包。</li><li><code>libsqlite3-dev</code>：许多 Python 库（如日志、本地缓存）依赖 sqlite3。</li></ul></blockquote><hr><h2 id="二、下载-Python-3-12-源码包"><a href="#二、下载-Python-3-12-源码包" class="headerlink" title="二、下载 Python 3.12 源码包"></a>二、下载 Python 3.12 源码包</h2><p>以 Python 3.12.8 稳定版为例，在当前用户目录下下载并解压缩源码：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">cd</span> ~<br>wget https://www.python.org/ftp/python/3.12.8/Python-3.12.8.tgz<br>tar -xf Python-3.12.8.tgz<br><span class="hljs-built_in">cd</span> Python-3.12.8<br></code></pre></td></tr></table></figure><hr><h2 id="三、配置编译选项（Configure）"><a href="#三、配置编译选项（Configure）" class="headerlink" title="三、配置编译选项（Configure）"></a>三、配置编译选项（Configure）</h2><p>为了避免污染系统目录并实现版本隔离，指定独立安装前缀 <code>--prefix</code>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">./configure --prefix=/usr/local/python3.12 --enable-optimizations<br></code></pre></td></tr></table></figure><ul><li><code>--prefix=/usr/local/python3.12</code>：将所有可执行文件、库和头文件安装到指定独立目录，与系统环境完全隔离；</li><li><code>--enable-optimizations</code>：启用 Profile-Guided Optimization (PGO) 优化编译，提升 Python 运行速度。</li></ul><hr><h2 id="四、编译与安装（低内存防-OOM-重点）"><a href="#四、编译与安装（低内存防-OOM-重点）" class="headerlink" title="四、编译与安装（低内存防 OOM 重点）"></a>四、编译与安装（低内存防 OOM 重点）</h2><p>RK3566 开发板（如泰山派）一般为 1GB 或 2GB 内存，多核全并发编译大型 C&#x2F;C++ 工程时，内存容易耗尽触发 Linux 内核的 <strong>OOM Killer</strong>，导致编译器直接被系统杀死（终端提示 <code>Killed</code>）。</p><h3 id="4-1-单线程编译"><a href="#4-1-单线程编译" class="headerlink" title="4.1 单线程编译"></a>4.1 单线程编译</h3><p>推荐使用 <code>-j1</code> 单线程编译：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">make -j1<br></code></pre></td></tr></table></figure><h3 id="4-2-临时分配-Swap（可选应急方案）"><a href="#4-2-临时分配-Swap（可选应急方案）" class="headerlink" title="4.2 临时分配 Swap（可选应急方案）"></a>4.2 临时分配 Swap（可选应急方案）</h3><p>若单线程编译中途依然出现 <code>Killed</code>，说明物理内存依然吃紧，可临时创建 2GB 的 Swap 交换分区：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 临时创建并启用 2GB Swap 分区</span><br><span class="hljs-built_in">sudo</span> fallocate -l 2G /swapfile<br><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">chmod</span> 600 /swapfile<br><span class="hljs-built_in">sudo</span> mkswap /swapfile<br><span class="hljs-built_in">sudo</span> swapon /swapfile<br><br><span class="hljs-comment"># 查看内存与交换分区状态</span><br>free -h<br></code></pre></td></tr></table></figure><p>编译完成后建议立即关闭 Swap，减少对开发板板载 eMMC 闪存的无谓擦写损耗：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> swapoff /swapfile<br></code></pre></td></tr></table></figure><h3 id="4-3-执行安装（⚠️-核心操作：altinstall）"><a href="#4-3-执行安装（⚠️-核心操作：altinstall）" class="headerlink" title="4.3 执行安装（⚠️ 核心操作：altinstall）"></a>4.3 执行安装（⚠️ 核心操作：altinstall）</h3><p>编译通过后，**必须使用 <code>altinstall</code>，千万不要使用 <code>make install</code>**：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> make altinstall<br></code></pre></td></tr></table></figure><blockquote><p>⚠️ <strong>关键原理解析</strong>：</p><ul><li><code>make install</code> 会创建名为 <code>python3</code> 的软链接，直接覆盖 Ubuntu 系统的 <code>/usr/local/bin/python3</code>，导致系统内置脚本无法按预期版本运行，甚至直接弄瘫 <code>apt</code>。</li><li><code>make altinstall</code> 则只会生成版本后缀明确的 <code>python3.12</code> 与 <code>pip3.12</code>，绝不会篡改任何系统的 <code>python</code> 或 <code>python3</code> 符号链接！</li></ul></blockquote><hr><h2 id="五、配置环境变量"><a href="#五、配置环境变量" class="headerlink" title="五、配置环境变量"></a>五、配置环境变量</h2><p>将 Python 3.12 的 bin 路径添加到用户环境变量中：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">echo</span> <span class="hljs-string">&#x27;export PATH=&quot;/usr/local/python3.12/bin:$PATH&quot;&#x27;</span> &gt;&gt; ~/.bashrc<br><span class="hljs-built_in">source</span> ~/.bashrc<br></code></pre></td></tr></table></figure><hr><h2 id="六、验证安装"><a href="#六、验证安装" class="headerlink" title="六、验证安装"></a>六、验证安装</h2><p>检查 Python 3.12 及对应 pip 的版本输出：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash">python3.12 -V<br>pip3.12 -V<br></code></pre></td></tr></table></figure><p>正常输出示例：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs text">Python 3.12.8<br>pip 24.2 from /usr/local/python3.12/lib/python3.12/site-packages/pip (python 3.12)<br></code></pre></td></tr></table></figure><p>验证系统默认 Python 是否依然安全不受影响：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">python3 -V<br></code></pre></td></tr></table></figure><p>输出应依然为 Ubuntu 系统的默认 Python 版本，说明两者完全隔离共存。</p><hr><h2 id="七、配置-pip-国内镜像源"><a href="#七、配置-pip-国内镜像源" class="headerlink" title="七、配置 pip 国内镜像源"></a>七、配置 pip 国内镜像源</h2><p>在嵌入式开发板上安装依赖时，建议配置国内镜像源以提升下载速度：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash">pip3.12 config <span class="hljs-built_in">set</span> global.index-url http://mirrors.aliyun.com/pypi/simple/<br>pip3.12 config <span class="hljs-built_in">set</span> install.trusted-host mirrors.aliyun.com<br></code></pre></td></tr></table></figure><hr><h2 id="八、推荐：创建独立虚拟环境"><a href="#八、推荐：创建独立虚拟环境" class="headerlink" title="八、推荐：创建独立虚拟环境"></a>八、推荐：创建独立虚拟环境</h2><p>后续在 RK3566 上开发 RGA 图像预处理、RKNN 推理或跑 Python 服务时，推荐通过 <code>venv</code> 创建独立虚拟环境，保持环境整洁且互不影响：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 创建虚拟环境</span><br>python3.12 -m venv py312_env<br><br><span class="hljs-comment"># 激活虚拟环境</span><br><span class="hljs-built_in">source</span> py312_env/bin/activate<br><br><span class="hljs-comment"># 此时环境内的 python 和 pip 均自动指向 Python 3.12</span><br>python -V<br>pip -V<br><br><span class="hljs-comment"># 退出虚拟环境</span><br>deactivate<br></code></pre></td></tr></table></figure><hr><h2 id="九、卸载与环境清理"><a href="#九、卸载与环境清理" class="headerlink" title="九、卸载与环境清理"></a>九、卸载与环境清理</h2><p>如果后续不再需要该版本，由于采用了独立 <code>--prefix</code> 部署，卸载极为方便干净：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 1. 直接删除安装目录</span><br><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">rm</span> -rf /usr/local/python3.12<br><br><span class="hljs-comment"># 2. 编辑 ~/.bashrc，删除添加的 PATH 那一行并重新生效</span><br><span class="hljs-built_in">source</span> ~/.bashrc<br></code></pre></td></tr></table></figure><hr><h2 id="十、常见踩坑汇总"><a href="#十、常见踩坑汇总" class="headerlink" title="十、常见踩坑汇总"></a>十、常见踩坑汇总</h2><ol><li>**绝对不要执行 <code>make install</code>**：<br>会覆盖系统默认 <code>python3</code> 软链接，导致系统级应用（如 apt 等）因版本不兼容报错崩溃。必须使用 <code>sudo make altinstall</code>。</li><li>**编译进程提示 <code>Killed</code>**：<br>开发板物理内存不足触发 OOM。使用 <code>make -j1</code> 并配合临时 Swap 分区解决。</li><li><strong><code>pip</code> 无法安装模块报 SSL 错误</strong>：<br>缺少 <code>libssl-dev</code> 导致 Python 编译时跳过了 <code>_ssl</code> 模块。需先补齐 <code>libssl-dev</code> 后重新 <code>./configure</code> 并编译安装。</li><li><strong>导入 sqlite3 报错</strong>：<br>缺少 <code>libsqlite3-dev</code>，在编译前安装该依赖库即可。</li></ol><hr><h2 id="十一、后续进阶与相关阅读"><a href="#十一、后续进阶与相关阅读" class="headerlink" title="十一、后续进阶与相关阅读"></a>十一、后续进阶与相关阅读</h2><p>在泰山派上准备好纯净的 Python 3.12 环境后，即可继续搭建视觉硬件加速与边缘 AI 运行链路：</p><ul><li><strong>2D 硬件图形加速与权限配置</strong>：<a href="/posts/bc40b146.html">Rockchip 平台 librga 安装部署与权限问题排坑指南</a></li><li><strong>板端 NPU 推理环境</strong>：<a href="/posts/778d7979.html">RK3566 搭建 RKNN-Toolkit2 环境笔记</a></li><li><strong>ARM NEON 极速 CPU 推理</strong>：<a href="/posts/80a659d.html">泰山派 RK3566 部署 NCNN 极速推理实践</a></li><li><strong>端到端离线 OCR 开源实战</strong>：<a href="/posts/f6340cce.html">泰山派 RK3566 离线 OCR 服务：tspi-ocr-web 实战</a></li></ul>]]>
    </content>
    <id>https://bubao.github.io/posts/929df792.html</id>
    <link href="https://bubao.github.io/posts/929df792.html"/>
    <published>2026-10-31T12:15:30.000Z</published>
    <summary>记录在 RK3566（如立创·泰山派）aarch64 架构 Ubuntu 系统下源码编译安装 Python 3.12 的完整流程，通过 altinstall 实现多版本优雅共存，包含低内存防 OOM 编译策略与常见排坑。</summary>
    <title>RK3566 Ubuntu26.04 安装 Python3.12</title>
    <updated>2026-10-31T20:37:58.468Z</updated>
  </entry>
  <entry>
    <author>
      <name>一念</name>
    </author>
    <category term="学习笔记" scheme="https://bubao.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    <category term="go" scheme="https://bubao.github.io/tags/go/"/>
    <category term="OCR" scheme="https://bubao.github.io/tags/OCR/"/>
    <content>
      <![CDATA[<p>最近遇到一个需求：从一个文件夹里找出重复的文件，再把每个文件的文字内容，按结构填写进一个 docx 表格。</p><p>细想之下，这件事其实是<strong>三个相互独立的小问题</strong>：</p><ol><li><strong>找重复</strong>：如何高效判断两个文件的内容完全一致；</li><li><strong>读文字</strong>：文件是图片和 PDF，需要先把其中文字提取出来；</li><li><strong>填表格</strong>：把提取出来的文字按规则填进 docx 表格模板。</li></ol><span id="more"></span><p>这套方案用 Go 实现，OCR 部分借助 macOS 系统能力，<strong>零第三方依赖</strong>，拷贝即用。下文会给出完整思路与核心代码（字段示例均为占位，可按你的实际文档替换），并重点交代实现要点与踩过的坑。</p><hr><h2 id="一、先想清楚要做什么"><a href="#一、先想清楚要做什么" class="headerlink" title="一、先想清楚要做什么"></a>一、先想清楚要做什么</h2><p>需求看起来是一件事，但动手前要把它切成三段来想——每一段的解决路径完全不同：</p><figure class="highlight dns"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs dns">扫描目录（图片/PDF<span class="hljs-punctuation">)</span><br>  → 逐个 OCR 提取文字<br>  → 按字段规则解析成结构化记录<br>  → 排序、去重<br>  → 按记录批量填充 docx 表格<br></code></pre></td></tr></table></figure><p>「找文件重复」则是另一条独立主线，但它与 OCR 之间还有一层配合关系：<strong>同一份材料可能同时存在 PDF 与 JPG 两种格式</strong>，字节完全不同，哈希判重发现不了它们。这类「格式不同但内容相同」的重复，只能放到 OCR 之后按内容里的<strong>核心标识</strong>去重——这个判断划定了两个工具的职责边界。</p><p>技术选型方面，去重与填表用 Go：可编译成单文件、无第三方依赖，zip 与哈希都是标准库原生能力；OCR 用 Swift，因为 Vision 框架仅苹果生态可用，且自带的高质量中文识别是当下性价比最高的方案（详见第三节）。</p><hr><h2 id="二、找重复文件：先按大小粗筛，再算哈希"><a href="#二、找重复文件：先按大小粗筛，再算哈希" class="headerlink" title="二、找重复文件：先按大小粗筛，再算哈希"></a>二、找重复文件：先按大小粗筛，再算哈希</h2><h3 id="核心思路：避免对每个文件都做完整哈希"><a href="#核心思路：避免对每个文件都做完整哈希" class="headerlink" title="核心思路：避免对每个文件都做完整哈希"></a>核心思路：避免对每个文件都做完整哈希</h3><p>最朴素的去重是对每个文件算一遍完整哈希，但<strong>哈希一个 1GB 的文件很慢</strong>，而且绝大多数文件根本走不到这一步。于是方案分两步：</p><ol><li><strong>按文件大小分组</strong>：大小不同，内容必然不同，直接排除；</li><li><strong>只对同大小的文件</strong>计算 SHA-256，哈希一致才判定为重复。</li></ol><p>「粗筛在前、精算在后」，实际进入哈希环节的文件少了一个数量级，核心逻辑不过几行：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-keyword">type</span> group <span class="hljs-keyword">struct</span> &#123;<br>    size <span class="hljs-type">int64</span><br>    hash <span class="hljs-type">string</span><br>&#125;<br>byKey := <span class="hljs-keyword">map</span>[group][]<span class="hljs-type">string</span>&#123;&#125;<br><br><span class="hljs-comment">// 递归扫描 src 目录，空文件（size=0）不哈希，统一归入 size=0 组</span><br><span class="hljs-keyword">for</span> _, path := <span class="hljs-keyword">range</span> walk(src) &#123;<br>    info, _ := os.Stat(path)<br>    g := group&#123;size: info.Size()&#125;<br>    <span class="hljs-keyword">if</span> info.Size() &gt; <span class="hljs-number">0</span> &#123;<br>        h := sha256.New()<br>        f, _ := os.Open(path)<br>        io.Copy(h, f)        <span class="hljs-comment">// 流式读取，大文件也不占内存</span><br>        g.hash = <span class="hljs-type">string</span>(h.Sum(<span class="hljs-literal">nil</span>))<br>    &#125;<br>    byKey[g] = <span class="hljs-built_in">append</span>(byKey[g], path)<br>&#125;<br><br><span class="hljs-comment">// 每组保留第一个，其余移动，重命名为 大小_序号_原名 防冲突</span><br><span class="hljs-keyword">for</span> key, paths := <span class="hljs-keyword">range</span> byKey &#123;<br>    <span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(paths) &lt; <span class="hljs-number">2</span> &#123;<br>        <span class="hljs-keyword">continue</span><br>    &#125;<br>    <span class="hljs-keyword">for</span> i, dup := <span class="hljs-keyword">range</span> paths[<span class="hljs-number">1</span>:] &#123;<br>        dest := filepath.Join(dst,<br>            fmt.Sprintf(<span class="hljs-string">&quot;%d_%d_%s&quot;</span>, key.size, i, filepath.Base(dup)))<br>        os.Rename(dup, dest)   <span class="hljs-comment">// 同一分区上是原子移动</span><br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><h3 id="几个值得注意的设计点"><a href="#几个值得注意的设计点" class="headerlink" title="几个值得注意的设计点"></a>几个值得注意的设计点</h3><p><strong>空文件是特例</strong>：大小为 0 的文件无需哈希，统一按「大小 0」归入一组即可。副作用是——<strong>所有空文件会被看成同一组重复</strong>，多余的会被移走。这是用简化换来的便利，实践中完全可接受，但需要意识到它的存在。</p><p><strong>保留规则要确定</strong>：每一组重复文件里「保留谁」必须有一个稳定规则。这里用的是扫描顺序：保留第一个遇到的，其余都算多余副本。</p><p><strong>移动前先重命名</strong>：多余副本移到目标目录时，直接同名搬移会互相覆盖，所以在原名前加上「大小_序号」前缀；序号是组内第几个副本，天然唯一。</p><p><strong>预览优先，默认不执行</strong>：移动是破坏性操作，工具默认处于「预览」模式，只打印每一对「源 → 目标」的清单，确认无误后再显式开启执行。</p><p><strong>移动而非复制</strong>：同一磁盘分区内 rename 是原子的系统调用，不产生额外 IO；跨分区会失败，失败仅记日志并继续，不影响其他组。</p><h3 id="工具边界"><a href="#工具边界" class="headerlink" title="工具边界"></a>工具边界</h3><p>这个工具只能找<strong>字节完全相同</strong>的副本。任何「看起来一样但字节不同」的情况（不同格式、重新扫描）都查不出来。它不能解决全部去重问题——剩下的交给第五节的「按标识去重」。</p><hr><h2 id="三、OCR：直接信任系统-Vision-框架"><a href="#三、OCR：直接信任系统-Vision-框架" class="headerlink" title="三、OCR：直接信任系统 Vision 框架"></a>三、OCR：直接信任系统 Vision 框架</h2><p><img src="https://help.apple.com/assets/66677889E4619874780693D4/6667788AF00D66F0940B5D7C/zh_CN/f02fdbb9d5d5395ed02bea78ca242d26.png"></p><h3 id="选型：为什么不用-tesseract"><a href="#选型：为什么不用-tesseract" class="headerlink" title="选型：为什么不用 tesseract"></a>选型：为什么不用 tesseract</h3><p>在 macOS 上做中文 OCR，最容易被忽略的最优解是系统自带的 <strong>Vision 框架</strong>：</p><ul><li>内置高质量中文识别，印刷体效果稳定，无需训练模型；</li><li><strong>零安装</strong>：不用装 tesseract 或各种语言包；</li><li>识别语言直接声明为「中文 + 英文」，中英混排的文档也能处理。</li></ul><p>代价是必须用 Swift（或 ObjC）调用，因此方案里需要一个小的 Swift 脚本充当助手，由 Go 主程序负责调用。</p><h3 id="识别请求的关键配置"><a href="#识别请求的关键配置" class="headerlink" title="识别请求的关键配置"></a>识别请求的关键配置</h3><p>Vision 识别请求的配置很短，几个关键项直接决定了识别质量：</p><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs swift"><span class="hljs-keyword">let</span> request <span class="hljs-operator">=</span> <span class="hljs-type">VNRecognizeTextRequest</span>()<br>request.recognitionLevel <span class="hljs-operator">=</span> .accurate                 <span class="hljs-comment">// 高精度模式（慢但准）</span><br>request.recognitionLanguages <span class="hljs-operator">=</span> [<span class="hljs-string">&quot;zh-Hans&quot;</span>, <span class="hljs-string">&quot;en-US&quot;</span>]  <span class="hljs-comment">// 中英混排更稳</span><br>request.usesLanguageCorrection <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>                <span class="hljs-comment">// 语言校正，修正形近字</span><br><br><span class="hljs-keyword">let</span> handler <span class="hljs-operator">=</span> <span class="hljs-type">VNImageRequestHandler</span>(cgImage: cgImage, options: [:])<br><span class="hljs-keyword">try</span> handler.perform([request])<br><br><span class="hljs-comment">// Vision 坐标原点在左下角：maxY 越大越靠上，据此还原阅读顺序</span><br><span class="hljs-keyword">return</span> (request.results <span class="hljs-operator">??</span> [])<br>    .sorted &#123; <span class="hljs-variable">$0</span>.boundingBox.maxY <span class="hljs-operator">&gt;</span> <span class="hljs-variable">$1</span>.boundingBox.maxY &#125;<br>    .compactMap &#123; <span class="hljs-variable">$0</span>.topCandidates(<span class="hljs-number">1</span>).first<span class="hljs-operator">?</span>.string &#125;<br></code></pre></td></tr></table></figure><h3 id="两个绕不开的预处理"><a href="#两个绕不开的预处理" class="headerlink" title="两个绕不开的预处理"></a>两个绕不开的预处理</h3><p><strong>统一输入形式</strong>：Vision 的识别请求只接受 <code>CGImage</code>（位图），需要先做格式转换：</p><ul><li><strong>图片</strong>：直接解码成位图；</li><li><strong>PDF</strong>：先用 PDF 渲染管线把每一页画成位图，这里有个关键参数——<strong>按 2 倍分辨率渲染</strong>：</li></ul><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs swift"><span class="hljs-keyword">let</span> scale: <span class="hljs-type">CGFloat</span> <span class="hljs-operator">=</span> <span class="hljs-number">2.0</span>   <span class="hljs-comment">// 2 倍分辨率，扫描件小字号识别率明显提升</span><br><span class="hljs-keyword">let</span> width  <span class="hljs-operator">=</span> <span class="hljs-type">Int</span>(rect.width <span class="hljs-operator">*</span> scale)<br><span class="hljs-keyword">let</span> height <span class="hljs-operator">=</span> <span class="hljs-type">Int</span>(rect.height <span class="hljs-operator">*</span> scale)<br><span class="hljs-comment">// 创建位图上下文 → 白底填充 → ctx.scaleBy(x: scale, y: scale) → page.draw</span><br></code></pre></td></tr></table></figure><p>扫描件的字号本来就偏小，1 倍渲染经常错字，升到 2 倍后识别率提升非常明显，代价只是速度略降。</p><p><strong>恢复阅读顺序</strong>：识别结果是「一组带坐标的文本块」，且坐标系<strong>原点在左下角</strong>，与常规的左上角习惯相反。因此要按 Y 坐标从大到小排序（见上面代码里的 <code>sorted</code>），才能还原成「从上到下」的阅读顺序。这是后续字段解析能按序进行的前提，也是最容易做错的地方。</p><h3 id="分页标记：防止字段跨页粘连"><a href="#分页标记：防止字段跨页粘连" class="headerlink" title="分页标记：防止字段跨页粘连"></a>分页标记：防止字段跨页粘连</h3><p>PDF 是多页的，识别完每一页要在结果里追加一个页码标记：</p><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs swift">lines.append(contentsOf: ocr(img))<br>lines.append(<span class="hljs-string">&quot;===PAGE <span class="hljs-subst">\(i <span class="hljs-operator">+</span> <span class="hljs-number">1</span>)</span>===&quot;</span>)   <span class="hljs-comment">// 分页标记，Go 端读到就截断当前字段</span><br></code></pre></td></tr></table></figure><p>Go 端读到这个标记就把正在收集的字段截断、重置状态，否则页尾的半句话会与下一页页首的内容粘连，字段就废了。</p><h3 id="进程间的协作约定"><a href="#进程间的协作约定" class="headerlink" title="进程间的协作约定"></a>进程间的协作约定</h3><ul><li>识别结果逐行打到<strong>标准输出</strong>，Go 端逐行读取；</li><li>一切错误（打不开文件、识别失败）打到<strong>标准错误</strong>，绝不混进结果流；</li><li><strong>编译自动化</strong>：Go 程序首次运行时若发现助手可执行文件不存在，会自动完成编译（并开启优化开关，识别大文件时速度相差好几倍），对用户完全透明。</li></ul><h3 id="需要正视的局限"><a href="#需要正视的局限" class="headerlink" title="需要正视的局限"></a>需要正视的局限</h3><ul><li>只支持 macOS（系统框架强绑定）；</li><li>手写体识别不稳定，印刷体效果最好；</li><li>排序只用了 Y 坐标，<strong>不含左右关系</strong>。横排的「标签：值」没问题，但多栏排版会出现左右栏文字串行——这是使用本方案前必须评估的硬边界。</li></ul><hr><h2 id="四、docx：把它当成一个-ZIP-包来改"><a href="#四、docx：把它当成一个-ZIP-包来改" class="headerlink" title="四、docx：把它当成一个 ZIP 包来改"></a>四、docx：把它当成一个 ZIP 包来改</h2><h3 id="docx-的本质"><a href="#docx-的本质" class="headerlink" title="docx 的本质"></a>docx 的本质</h3><p><code>.docx</code> 不是一个二进制格式，而是一个 <strong>ZIP 压缩包</strong>。正文在包内的 <code>word/document.xml</code> 里，样式、字体、页眉页脚是另外几个独立的 XML。所以「填表格」本质上就是 <strong>两个动作</strong>：</p><ol><li>解包，改 <code>word/document.xml</code> 里的表格；</li><li>重新打包，其余文件原样复制回去。</li></ol><p>了解这个本质之后，就不再需要任何第三方 docx 库——一个 zip 库 + 字符串处理就够了。</p><h3 id="解析：把「配置」和「业务」分离"><a href="#解析：把「配置」和「业务」分离" class="headerlink" title="解析：把「配置」和「业务」分离"></a>解析：把「配置」和「业务」分离</h3><p>整套方案里最值得借鉴的是「<strong>配置与业务分离</strong>」：把一切与具体文档字段相关的内容抽成文件顶部的配置项，解析逻辑本身保持完全通用。配置分两类：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-comment">// ============================================</span><br><span class="hljs-comment">// 占位配置：请按你的实际文档字段与表格列名替换</span><br><span class="hljs-comment">// ============================================</span><br><span class="hljs-keyword">var</span> fieldLabels = []labelSec&#123;<br>    &#123;<span class="hljs-string">&quot;名称&quot;</span>, secName&#125;,   <span class="hljs-comment">// 第 2 列</span><br>    &#123;<span class="hljs-string">&quot;编号&quot;</span>, secCode&#125;,   <span class="hljs-comment">// 第 3 列</span><br>    &#123;<span class="hljs-string">&quot;日期&quot;</span>, secDate&#125;,   <span class="hljs-comment">// 第 6 列</span><br>&#125;<br><br><span class="hljs-comment">// 遇到以这些文本开头的行，结束当前字段收集（页眉/落款等杂文本）</span><br><span class="hljs-keyword">var</span> breaks = []<span class="hljs-type">string</span>&#123;<span class="hljs-string">&quot;标题&quot;</span>, <span class="hljs-string">&quot;前言&quot;</span>, <span class="hljs-string">&quot;说明&quot;</span>, <span class="hljs-string">&quot;签名&quot;</span>, <span class="hljs-string">&quot;落款&quot;</span>&#125;<br></code></pre></td></tr></table></figure><ul><li><strong>字段标签</strong>：表格每一列对应正文里以什么词开头。命中标签前缀的行，就开始收集该字段；</li><li><strong>分隔词</strong>：正文里一些与数据无关的杂文本。命中的行代表当前字段到此结束。</li></ul><p>字段解析是一个<strong>状态机</strong>：命中标签 → 进入收集态；命中分隔词 → 结束收集。这样正文里一行放不下的多行内容（如人名列表换行）也能被正确合并进同一字段：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">sectionFor</span><span class="hljs-params">(t <span class="hljs-type">string</span>)</span></span> (<span class="hljs-type">int</span>, <span class="hljs-type">string</span>) &#123;<br>    <span class="hljs-keyword">for</span> _, d := <span class="hljs-keyword">range</span> fieldLabels &#123;<br>        <span class="hljs-keyword">if</span> strings.HasPrefix(t, d.label) &#123;<br>            <span class="hljs-comment">// 去掉行首「标签：」前缀，返回字段类型与取值</span><br>            <span class="hljs-keyword">return</span> d.sec, reColon.ReplaceAllString(t[<span class="hljs-built_in">len</span>(d.label):], <span class="hljs-string">&quot;&quot;</span>)<br>        &#125;<br>    &#125;<br>    <span class="hljs-keyword">return</span> secNone, <span class="hljs-string">&quot;&quot;</span><br>&#125;<br><br><span class="hljs-comment">// 逐行处理：标签 → 进入该字段；分隔词 → 结束当前字段</span><br><span class="hljs-keyword">for</span> _, t := <span class="hljs-keyword">range</span> lines &#123;<br>    <span class="hljs-keyword">if</span> isBreak(t) &#123;<br>        cur = secNone<br>        <span class="hljs-keyword">continue</span><br>    &#125;<br>    <span class="hljs-keyword">if</span> sec, val := sectionFor(t); sec != secNone &#123;<br>        cur = sec<br>        collect(sec, val)   <span class="hljs-comment">// 按字段类型清洗后收集</span><br>        <span class="hljs-keyword">continue</span><br>    &#125;<br>    collect(cur, t)         <span class="hljs-comment">// 后续行持续追加到当前字段</span><br>&#125;<br></code></pre></td></tr></table></figure><h3 id="三个-OCR-特有的坑"><a href="#三个-OCR-特有的坑" class="headerlink" title="三个 OCR 特有的坑"></a>三个 OCR 特有的坑</h3><p><strong>标签被断行</strong>：排版原因，OCR 经常把标签拆成两行，比如「名」和「称：xxx」。拼回去的规则很朴素——<strong>当前行恰好是某标签的前缀，且下一行以标签剩余部分开头，就合并</strong>：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-keyword">for</span> i := <span class="hljs-number">0</span>; i &lt; <span class="hljs-built_in">len</span>(lines); i++ &#123;<br>    t := lines[i]<br>    <span class="hljs-keyword">if</span> i+<span class="hljs-number">1</span> &lt; <span class="hljs-built_in">len</span>(lines) &#123;<br>        <span class="hljs-keyword">for</span> _, L := <span class="hljs-keyword">range</span> labelFrags &#123;<br>            <span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(t) &lt; <span class="hljs-built_in">len</span>(L) &amp;&amp; strings.HasPrefix(L, t) &amp;&amp;<br>               strings.HasPrefix(lines[i+<span class="hljs-number">1</span>], L[<span class="hljs-built_in">len</span>(t):]) &#123;<br>                t = L + lines[i+<span class="hljs-number">1</span>][<span class="hljs-built_in">len</span>(L)-<span class="hljs-built_in">len</span>(t):]<br>                i++          <span class="hljs-comment">// 合并后跳过下一行</span><br>                <span class="hljs-keyword">break</span><br>            &#125;<br>        &#125;<br>    &#125;<br>    out = <span class="hljs-built_in">append</span>(out, t)<br>&#125;<br></code></pre></td></tr></table></figure><p>这个规则简单但极其有效。</p><p><strong>形近字混淆</strong>：OCR 常把数字 0 认成字母 O（反过来也一样）。编码类字段提取出来后，统一做一次字符纠正，效果立竿见影。</p><p><strong>字段清洗分层降级</strong>：以「带年份前缀的编码类字段」为例，提取按优先级逐级退让——</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs go">reYear  = regexp.MustCompile(<span class="hljs-string">`(20|19)\d&#123;2&#125;`</span>)      <span class="hljs-comment">// 年份 19xx/20xx</span><br>reNo    = regexp.MustCompile(<span class="hljs-string">`\d&#123;6,9&#125;`</span>)           <span class="hljs-comment">// 6~9 位纯数字</span><br>reABC   = regexp.MustCompile(<span class="hljs-string">`[A-Za-z0-9]&#123;8,&#125;`</span>)   <span class="hljs-comment">// 字母+数字混合串</span><br>reColon = regexp.MustCompile(<span class="hljs-string">`^[^：:]*[：:]\s*`</span>)  <span class="hljs-comment">// 去掉行首「标签：」前缀</span><br></code></pre></td></tr></table></figure><p>提取顺序是：优先匹配标签后面的值 → 退而从整行里找字母数字混合串 → 再退而求其次找纯数字串。每层用不同正则，识别质量差时也能兜住一部分结果。</p><h3 id="两个兜底策略，进一步压低失败率"><a href="#两个兜底策略，进一步压低失败率" class="headerlink" title="两个兜底策略，进一步压低失败率"></a>两个兜底策略，进一步压低失败率</h3><ul><li><strong>文件名兜底</strong>：文件名往往自带元信息（如「某某-2026-ABC123.jpg」）。当 OCR 失败或没读全时，从文件名里提取年份与编码作为次要来源。方案不依赖它，却能把空字段的比例降下一截；</li><li><strong>空白过滤</strong>：目录里混入的无关图片（截图、照片）OCR 出来也「有字」，却拼不出完整字段。规则是：主字段为空、或除主字段外一无所获的内容，一律判为无效文件，不进表格，只在日志里报一行——<strong>这一步直接决定最终表格的干净程度</strong>。</li></ul><h3 id="写入：模板的三种定位点"><a href="#写入：模板的三种定位点" class="headerlink" title="写入：模板的三种定位点"></a>写入：模板的三种定位点</h3><p>填充不是「整体替换」，而是把模板 XML 切成三块——列网格结束处、第一个数据行（表头）、表格结束标记之后——再在表头后<strong>逐行插入生成的数据行</strong>：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-comment">// 定位模板三处：列网格结束 → 表头行结束 → 表格结束</span><br>gridEnd := strings.Index(doc, <span class="hljs-string">&quot;&lt;/w:tblGrid&gt;&quot;</span>) + <span class="hljs-built_in">len</span>(<span class="hljs-string">&quot;&lt;/w:tblGrid&gt;&quot;</span>)<br>head := doc[:gridEnd]                          <span class="hljs-comment">// ① 表格属性 + 列宽定义</span><br><br>rest := doc[gridEnd:]<br>trEnd := strings.Index(rest, <span class="hljs-string">&quot;&lt;/w:tr&gt;&quot;</span>) + <span class="hljs-built_in">len</span>(<span class="hljs-string">&quot;&lt;/w:tr&gt;&quot;</span>)<br>header := rest[:trEnd]                         <span class="hljs-comment">// ② 表头行保留</span><br><br>tail := doc[strings.Index(doc, <span class="hljs-string">&quot;&lt;/w:tbl&gt;&quot;</span>):]   <span class="hljs-comment">// ③ 表格后的内容原样保留</span><br><br><span class="hljs-comment">// 在表头后插入 N 行数据，再拼上尾部</span><br><span class="hljs-keyword">var</span> b strings.Builder<br>b.WriteString(head)<br>b.WriteString(header)<br><span class="hljs-keyword">for</span> i, c := <span class="hljs-keyword">range</span> records &#123;<br>    b.WriteString(row(i+<span class="hljs-number">1</span>, c))<br>&#125;<br>b.WriteString(tail)<br></code></pre></td></tr></table></figure><p>这个「切三块、插中间」的写法，比正则替换健壮得多。</p><h3 id="写入前要理解的两个-Office-单位"><a href="#写入前要理解的两个-Office-单位" class="headerlink" title="写入前要理解的两个 Office 单位"></a>写入前要理解的两个 Office 单位</h3><p>写单元格时有两个单位容易踩坑：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-comment">// w:sz 的单位是半磅：24 半磅 = 12 磅（小四）</span><br><span class="hljs-keyword">const</span> runFmt = <span class="hljs-string">`&lt;w:r&gt;&lt;w:rPr&gt;&lt;w:rFonts w:ascii=&quot;宋体&quot; w:eastAsia=&quot;宋体&quot;`</span> +<br>    <span class="hljs-string">` w:hAnsi=&quot;宋体&quot; w:hint=&quot;eastAsia&quot;/&gt;&lt;w:sz w:val=&quot;24&quot;/&gt;`</span> +<br>    <span class="hljs-string">`&lt;w:szCs w:val=&quot;24&quot;/&gt;&lt;/w:rPr&gt;&lt;w:t xml:space=&quot;preserve&quot;&gt;%s&lt;/w:t&gt;&lt;/w:r&gt;`</span><br><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">cell</span><span class="hljs-params">(w <span class="hljs-type">int</span>, v <span class="hljs-type">string</span>)</span></span> <span class="hljs-type">string</span> &#123;<br>    <span class="hljs-comment">// 空值也生成完整结构（段落存在、无 run），保证边框与行高完整</span><br>    p := fmt.Sprintf(<br>        <span class="hljs-string">`&lt;w:tc&gt;&lt;w:tcPr&gt;&lt;w:tcW w:w=&quot;%d&quot; w:type=&quot;dxa&quot;/&gt;&lt;/w:tcPr&gt;&lt;w:p&gt;&lt;w:pPr&gt;…&lt;/w:pPr&gt;`</span>, w)<br>    <span class="hljs-keyword">if</span> v != <span class="hljs-string">&quot;&quot;</span> &#123;<br>        p += fmt.Sprintf(runFmt, xmlEscape(v))  <span class="hljs-comment">// 写 w:t 前必须 XML 转义</span><br>    &#125;<br>    <span class="hljs-keyword">return</span> p + <span class="hljs-string">`&lt;/w:p&gt;&lt;/w:tc&gt;`</span><br>&#125;<br></code></pre></td></tr></table></figure><ul><li><strong>字号按半磅记</strong>：<code>24</code> 不是 24 磅，而是 24 <strong>半磅</strong> &#x3D; 12 磅。写错整体字号会大一号；</li><li><strong>列宽按 twips 记</strong>：1 twip &#x3D; 1&#x2F;20 磅，模板里抄下来的列宽值不能当像素直接用。</li></ul><p>此外，OCR 拿到的文本可能含 <code>&amp; &lt; &gt;</code> 等 XML 特殊字符，写入前<strong>必须转义</strong>（标准库 <code>encoding/xml</code> 足矣），否则 Word 会直接提示「文件已损坏」。</p><h3 id="重新打包：只动一个文件"><a href="#重新打包：只动一个文件" class="headerlink" title="重新打包：只动一个文件"></a>重新打包：只动一个文件</h3><p>重新打包的规则很简单：ZIP 里所有条目，<strong>除了正文 XML 用新内容，其余一律按原始字节流复制</strong>：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-keyword">for</span> _, f := <span class="hljs-keyword">range</span> zin.File &#123;<br>    w, _ := zw.CreateHeader(&amp;f.FileHeader)<br>    <span class="hljs-keyword">if</span> f.Name == <span class="hljs-string">&quot;word/document.xml&quot;</span> &#123;<br>        w.Write(newDoc)      <span class="hljs-comment">// 正文用新内容</span><br>    &#125; <span class="hljs-keyword">else</span> &#123;<br>        io.Copy(w, rc)       <span class="hljs-comment">// 其余（样式/字体/页眉/媒体）原样复制</span><br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>这样模板的样式、字体、水印、页眉页脚全部原样保留，生成文档的外观与模板完全一致。注意每个生成单元格都要补齐属性块，即使值是空的也要保持完整结构，否则表格的边框与行高会残缺。</p><hr><h2 id="五、最后一道工序：排序与去重"><a href="#五、最后一道工序：排序与去重" class="headerlink" title="五、最后一道工序：排序与去重"></a>五、最后一道工序：排序与去重</h2><p>填表前的最后一步，对识别结果做两件整理：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-comment">// 先按核心标识降序排列：带年份前缀的标识，新的排前面；空标识自然沉底</span><br>sort.SliceStable(records, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(i, j <span class="hljs-type">int</span>)</span></span> <span class="hljs-type">bool</span> &#123;<br>    <span class="hljs-keyword">return</span> records[i].Code &gt; records[j].Code<br>&#125;)<br><br><span class="hljs-comment">// 同标识只保留扫描顺序靠前的一条（同一材料的不同格式各识别了一份）</span><br>seen := <span class="hljs-keyword">map</span>[<span class="hljs-type">string</span>]<span class="hljs-type">bool</span>&#123;&#125;<br>kept := records[:<span class="hljs-number">0</span>]<br><span class="hljs-keyword">for</span> _, rec := <span class="hljs-keyword">range</span> records &#123;<br>    <span class="hljs-keyword">if</span> rec.Code != <span class="hljs-string">&quot;&quot;</span> &amp;&amp; seen[rec.Code] &#123;<br>        <span class="hljs-keyword">continue</span>             <span class="hljs-comment">// 丢弃的这条在日志里说明</span><br>    &#125;<br>    <span class="hljs-keyword">if</span> rec.Code != <span class="hljs-string">&quot;&quot;</span> &#123;<br>        seen[rec.Code] = <span class="hljs-literal">true</span><br>    &#125;<br>    kept = <span class="hljs-built_in">append</span>(kept, rec)<br>&#125;<br>records = kept<br></code></pre></td></tr></table></figure><ul><li><strong>排序</strong>：核心标识通常带年份前缀，降序即「新的在前」；没有标识的记录（空串）自然沉到表格末尾，不会被误插进中间；</li><li><strong>去重</strong>：同一份材料的 PDF 与 JPG 各识别了一次，标识必然相同，同标识只保留一条——这正是第一节所说的「格式不同但内容相同」的重复，只有在这一步才除得掉。</li></ul><p>两个操作都遵循「先排序、再去重、保留靠前一条」的顺序，保证结果稳定可复现。</p><hr><h2 id="六、踩坑清单（快速回顾）"><a href="#六、踩坑清单（快速回顾）" class="headerlink" title="六、踩坑清单（快速回顾）"></a>六、踩坑清单（快速回顾）</h2><p>把整个过程中值得记住的点浓缩成一份对照清单：</p><ol><li><strong>先粗筛再精算</strong>：按大小分组后只哈希同大小的文件，避免全量哈希的开销；</li><li><strong>空文件是一组</strong>：size&#x3D;0 不哈希，所有空文件视为同组；</li><li><strong>破坏性操作默认预览</strong>：移动类操作先列清单，确认后再执行；</li><li><strong>PDF 要 2 倍渲染</strong>：1 倍渲染的小字号识别率明显下降；</li><li><strong>Vision 坐标系左下角</strong>：按 <code>maxY</code> 降序才能还原从上到下的阅读顺序；</li><li><strong>分页要截断</strong>：跨页字段会粘连，必须用分页标记重置状态；</li><li><strong>把业务抽成配置</strong>：字段标签、分隔词都在文件顶部配置，逻辑与业务解耦；</li><li><strong>清洗分层降级</strong>：标签匹配 → 整行匹配 → 降级匹配，逐级退让；</li><li><strong>文件名是第二数据源</strong>：OCR 读不到时从文件名提取，兜底但不过度依赖；</li><li><strong>无效内容要过滤</strong>：拼不出完整字段的文档直接排除，保证表干净；</li><li><strong>模板切三块插中间</strong>：属性 + 表头 + 尾部，比正则替换稳健；</li><li><strong>记清两个 Office 单位</strong>：字号半磅、列宽 twips，错了整体变形；</li><li><strong>XML 必须转义</strong>：特殊字符不转义，Word 直接报损坏；</li><li><strong>重打包只动正文</strong>：其余条目字节级原样复制，保留外观。</li></ol><hr><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>这套方案最大的收获，不是某一处技巧，而是<strong>把一个大需求拆成三个无依赖的小工具</strong>：文件去重、系统 OCR、docx 模板填充。每一件单独来看都很朴素，组合起来却解决了一个真实的批量归档问题。</p><p>如果你也要处理类似的「批量识别 + 填表」需求，直接套用这三段式骨架，把字段配置换成自己的文档结构，剩下的交给状态机与分层清洗去兜底。最后切记：任何自动化方案，都要留一条「日志可查、结果可人工复核」的退路。</p>]]>
    </content>
    <id>https://bubao.github.io/posts/b57f3b22.html</id>
    <link href="https://bubao.github.io/posts/b57f3b22.html"/>
    <published>2026-09-16T17:28:00.000Z</published>
    <summary>
      <![CDATA[<p>最近遇到一个需求：从一个文件夹里找出重复的文件，再把每个文件的文字内容，按结构填写进一个 docx 表格。</p>
<p>细想之下，这件事其实是<strong>三个相互独立的小问题</strong>：</p>
<ol>
<li><strong>找重复</strong>：如何高效判断两个文件的内容完全一致；</li>
<li><strong>读文字</strong>：文件是图片和 PDF，需要先把其中文字提取出来；</li>
<li><strong>填表格</strong>：把提取出来的文字按规则填进 docx 表格模板。</li>
</ol>]]>
    </summary>
    <title>零依赖文件去重、OCR与 docx 表格自动填充</title>
    <updated>2026-10-31T20:37:58.481Z</updated>
  </entry>
  <entry>
    <author>
      <name>一念</name>
    </author>
    <category term="esp32" scheme="https://bubao.github.io/categories/esp32/"/>
    <category term="esp-idf" scheme="https://bubao.github.io/categories/esp32/esp-idf/"/>
    <category term="esp32" scheme="https://bubao.github.io/tags/esp32/"/>
    <category term="esp-idf" scheme="https://bubao.github.io/tags/esp-idf/"/>
    <category term="CMake" scheme="https://bubao.github.io/tags/CMake/"/>
    <content>
      <![CDATA[<p>近期在 LOLIN S2 Mini 基于 ESP-IDF 6.0 开发时，遇到两类极易混淆的串口输出问题，二者表象相似、底层原理完全不同：</p><ol><li>固件运行正常、逻辑无异常，<strong>原生 USB-CDC 完全无串口输出</strong>；</li><li>修复 USB-CDC 后，常规 <code>printf</code> 可正常输出，但 <strong>Unity 测试结果无任何日志打印</strong>。</li></ol><p>本文将结合 ESP-IDF 6.0 官方 VFS 组件变更，完整复盘问题定位、底层原理、分层排查思路及最终解决方案，覆盖组件依赖、编译配置、链路重定向等核心要点。</p><h2 id="一、软硬件环境说明"><a href="#一、软硬件环境说明" class="headerlink" title="一、软硬件环境说明"></a>一、软硬件环境说明</h2><h3 id="1-1-硬件特性"><a href="#1-1-硬件特性" class="headerlink" title="1.1 硬件特性"></a>1.1 硬件特性</h3><p><img src="https://www.wemos.cc/en/latest/_images/s2_mini_v1.0.0_4_16x9.jpg" alt="s2_mini"></p><p>本次使用 <strong>LOLIN S2 Mini（ESP32-S2）</strong>，核心硬件特点：<strong>仅搭载 ESP32-S2 原生 USB 外设，无传统 USB-UART 转换芯片</strong>。</p><p>这意味着：设备串口输出不依赖硬件转接芯片，所有 USB 串口日志完全依托芯片内置的 <strong>USB-CDC Console</strong> 实现，与传统 ESP32 开发板输出逻辑存在本质区别。</p><h3 id="1-2-软件环境"><a href="#1-2-软件环境" class="headerlink" title="1.2 软件环境"></a>1.2 软件环境</h3><ul><li>操作系统：macOS</li><li>主控芯片：ESP32-S2</li><li>开发框架：ESP-IDF 6.0（核心版本，存在关键 VFS 组件变更）</li><li>输出方式：原生 USB-OTG、USB-CDC Console</li><li>测试框架：Unity Test Framework</li></ul><h3 id="1-3-烧录注意事项"><a href="#1-3-烧录注意事项" class="headerlink" title="1.3 烧录注意事项"></a>1.3 烧录注意事项</h3><p>设备手动烧录流程：按住 <code>BOOT</code> → 点击 <code>RST</code> → 松开 <code>BOOT</code> → 执行烧录 → 烧录完成后手动复位。</p><blockquote><p><strong>关键误区</strong>：烧录时电脑识别到 <code>/dev/cu.usbmodem*</code>，仅代表 ROM 下载模式 USB 通信正常，<strong>不代表应用层 USB-CDC 初始化正常</strong>。固件运行后的串口输出，完全取决于应用层 USB-CDC 和 VFS 组件配置。</p></blockquote><h2 id="二、核心原理：两条完全独立的串口输出链路"><a href="#二、核心原理：两条完全独立的串口输出链路" class="headerlink" title="二、核心原理：两条完全独立的串口输出链路"></a>二、核心原理：两条完全独立的串口输出链路</h2><p>本次两个故障的核心根源，是 <code>printf</code> 和 Unity 测试日志 <strong>默认走两套不同的输出链路</strong>，这是排障的核心前提。</p><h3 id="2-1-常规-printf-输出链路（USB-CDC-通路）"><a href="#2-1-常规-printf-输出链路（USB-CDC-通路）" class="headerlink" title="2.1 常规 printf 输出链路（USB-CDC 通路）"></a>2.1 常规 printf 输出链路（USB-CDC 通路）</h3><p>标准打印函数依托 VFS 虚拟文件系统，最终通过原生 USB 输出：</p><pre><code class=" mermaid">graph TD    A[&quot;printf()&quot;] --&gt; B[&quot;stdout 标准输出&quot;]    B --&gt; C[&quot;VFS 虚拟文件系统&quot;]    C --&gt; D[&quot;USB-CDC ACM 虚拟串口&quot;]    D --&gt; E[&quot;ESP32-S2 原生 USB-OTG&quot;]    E --&gt; F[&quot;电脑 /dev/cu.usbmodem*&quot;]</code></pre><h3 id="2-2-Unity-测试日志输出链路（UART0-硬件通路）"><a href="#2-2-Unity-测试日志输出链路（UART0-硬件通路）" class="headerlink" title="2.2 Unity 测试日志输出链路（UART0 硬件通路）"></a>2.2 Unity 测试日志输出链路（UART0 硬件通路）</h3><p>ESP-IDF 6.0 的 Unity 适配层，<strong>不依赖 stdout 和 VFS</strong>，默认直接输出到硬件 UART0：</p><pre><code class=" mermaid">graph TD    A[&quot;Unity 测试日志&quot;] --&gt; B[&quot;unity_putc() 框架输出接口&quot;]    B --&gt; C[&quot;esp_system_console_put_char()&quot;]    C --&gt; D[&quot;esp_rom_output_tx_one_char()&quot;]    D --&gt; E[&quot;uart_tx_one_char()&quot;]    E --&gt; F[&quot;硬件 UART0 外设&quot;]    F --&gt; G[&quot;UART0 物理引脚&quot;]    G --&gt; H[&quot;无 USB-UART 转接芯片，电脑无法直接识别&quot;]</code></pre><blockquote><p><strong>核心结论</strong>：<code>printf</code> 正常输出 ≠ Unity 日志正常输出。前者走 USB-CDC 虚拟串口，后者走裸 UART0 硬件串口，LOLIN S2 Mini 无 UART0 转 USB 电路，因此 Unity 默认日志无法被电脑捕获。</p></blockquote><h2 id="三、问题一：原生-USB-CDC-完全无输出（ESP-IDF-6-0-专属问题）"><a href="#三、问题一：原生-USB-CDC-完全无输出（ESP-IDF-6-0-专属问题）" class="headerlink" title="三、问题一：原生 USB-CDC 完全无输出（ESP-IDF 6.0 专属问题）"></a>三、问题一：原生 USB-CDC 完全无输出（ESP-IDF 6.0 专属问题）</h2><h3 id="3-1-故障现象"><a href="#3-1-故障现象" class="headerlink" title="3.1 故障现象"></a>3.1 故障现象</h3><p>固件烧录成功，设备 LED 闪烁、<code>app_main</code> 正常执行、业务逻辑运行正常，但串口工具无任何<code>printf</code> 输出。示例测试代码：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-type">void</span> <span class="hljs-title function_">app_main</span><span class="hljs-params">(<span class="hljs-type">void</span>)</span><br>&#123;<br>    <span class="hljs-built_in">printf</span>(<span class="hljs-string">&quot;hello esp32-s2n&quot;</span>);<br>    <span class="hljs-keyword">while</span> (<span class="hljs-number">1</span>) &#123;<br>        gpio_set_level(GPIO_NUM_15, <span class="hljs-number">1</span>);<br>        vTaskDelay(pdMS_TO_TICKS(<span class="hljs-number">500</span>));<br>        gpio_set_level(GPIO_NUM_15, <span class="hljs-number">0</span>);<br>        vTaskDelay(pdMS_TO_TICKS(<span class="hljs-number">500</span>));<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><h3 id="3-2-底层根因：ESP-IDF-6-0-VFS-组件重大变更"><a href="#3-2-底层根因：ESP-IDF-6-0-VFS-组件重大变更" class="headerlink" title="3.2 底层根因：ESP-IDF 6.0 VFS 组件重大变更"></a>3.2 底层根因：ESP-IDF 6.0 VFS 组件重大变更</h3><p>该问题是 ESP-IDF 6.0 版本迭代的兼容性问题，官方存储迁移文档明确标注了关键变更，直接影响 USB-CDC 串口输出：</p><ol><li>原 <code>esp_vfs_console</code> 组件正式更名为 <code>esp_stdio</code>，旧组件依赖全部失效；</li><li>VFS 模块编译启用强依赖组件声明，仅开启 menuconfig 配置，无法触发对应代码编译；</li><li>USB-CDC 的 VFS 注册逻辑完全依托 <code>vfs_cdcacm.c</code>，该文件为条件编译文件。</li></ol><h3 id="3-3-深度定位：配置生效但代码未编译"><a href="#3-3-深度定位：配置生效但代码未编译" class="headerlink" title="3.3 深度定位：配置生效但代码未编译"></a>3.3 深度定位：配置生效但代码未编译</h3><p>刚开始的时候在<code>sdkconfig</code>中添加下面这段，编译之后就能正常使用：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sdkconfig">CONFIG_IDF_TARGET=&quot;esp32s2&quot;<br>CONFIG_ESP_CONSOLE_USB_CDC=y<br></code></pre></td></tr></table></figure><p>很多开发者的误区：在 menuconfig 中开启 <code>CONFIG_ESP_CONSOLE_USB_CDC=y</code>，就认为功能已生效。实际编译逻辑存在两层限制：</p><p><code>vfs_cdcacm.c</code> 编译条件：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-keyword">if</span>(CONFIG_VFS_SUPPORT_IO AND CONFIG_ESP_CONSOLE_USB_CDC)<br>    target_sources($&#123;COMPONENT_LIB&#125; PRIVATE <span class="hljs-string">&quot;vfs_cdcacm.c&quot;</span>)<br>endif()<br></code></pre></td></tr></table></figure><p>故障核心：已开启 USB-CDC 配置，但<strong>项目组件未声明 VFS 依赖</strong>，导致 <code>CONFIG_VFS_SUPPORT_IO</code> 未生效，VFS 初始化代码未参与编译。</p><h3 id="3-4-完整故障链路"><a href="#3-4-完整故障链路" class="headerlink" title="3.4 完整故障链路"></a>3.4 完整故障链路</h3><pre><code class=" mermaid">graph TD    A[&quot;未声明 vfs 组件依赖&quot;] --&gt; B[&quot;CONFIG_VFS_SUPPORT_IO 配置失效&quot;]    B --&gt; C[&quot;vfs_cdcacm.c 不满足编译条件、未参与编译&quot;]    C --&gt; D[&quot;USB-CDC VFS 初始化函数不存在&quot;]    D --&gt; E[&quot;USB-CDC ACM 未注册到系统 VFS&quot;]    E --&gt; F[&quot;stdout 无 USB-CDC 输出通路&quot;]    F --&gt; G[&quot;串口完全无日志&quot;]</code></pre><h3 id="3-5-分层排查实操命令"><a href="#3-5-分层排查实操命令" class="headerlink" title="3.5 分层排查实操命令"></a>3.5 分层排查实操命令</h3><p>遵循「配置→编译→符号→运行时」逐层排查，精准定位问题：</p><ol><li><p><strong>检查 VFS IO 配置是否生效</strong><code>rg &quot;VFS_SUPPORT_IO&quot; build/config/sdkconfig.h</code><br>正常输出：<code>#define CONFIG_VFS_SUPPORT_IO 1</code></p></li><li><p><strong>检查核心文件是否参与编译</strong><code>rg &#39;&quot;file&quot;.*vfs_cdcacm.c&#39; build/compile_commands.json</code>无结果则代表文件未编译</p></li><li><p><strong>检查初始化符号是否进入固件</strong><code>xtensa-esp32s2-elf-nm build/*.elf | rg &quot;init_vfs_usb_cdc_rom_console&quot;</code></p></li><li><p><strong>检查 USB-CDC 初始化函数</strong><code>xtensa-esp32s2-elf-nm build/*.elf | rg &quot;esp_usb_console_init&quot;</code></p></li></ol><h3 id="3-6-修复方案：显式声明-VFS-组件依赖"><a href="#3-6-修复方案：显式声明-VFS-组件依赖" class="headerlink" title="3.6 修复方案：显式声明 VFS 组件依赖"></a>3.6 修复方案：显式声明 VFS 组件依赖</h3><p>修改项目<code>CMakeLists.txt</code>，在组件依赖中添加 <code>vfs</code>，强制启用 VFS IO 能力：</p><figure class="highlight cmake"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs cmake">idf_component_register(<br>    SRCS<br>        <span class="hljs-string">&quot;main.c&quot;</span><br>    INCLUDE_DIRS<br>        <span class="hljs-string">&quot;.&quot;</span><br>    REQUIRES<br>        esp_driver_gpio<br>        unity<br>        vfs  <span class="hljs-comment"># 新增：声明 VFS 组件依赖，解决 USB-CDC 编译失效问题</span><br>)<br><br></code></pre></td></tr></table></figure><p>重新编译配置即可修复：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs shell">idf.py reconfigure<br>idf.py build<br></code></pre></td></tr></table></figure><h2 id="四、问题二：printf-正常输出，Unity-测试日志无打印"><a href="#四、问题二：printf-正常输出，Unity-测试日志无打印" class="headerlink" title="四、问题二：printf 正常输出，Unity 测试日志无打印"></a>四、问题二：printf 正常输出，Unity 测试日志无打印</h2><h3 id="4-1-故障现象"><a href="#4-1-故障现象" class="headerlink" title="4.1 故障现象"></a>4.1 故障现象</h3><p>USB-CDC 修复后，常规 <code>printf</code>日志可正常通过 USB 输出，但 Unity 测试的 PASS&#x2F;FAIL、测试统计等日志完全缺失，无任何报错。</p><h3 id="4-2-故障根因"><a href="#4-2-故障根因" class="headerlink" title="4.2 故障根因"></a>4.2 故障根因</h3><p>如前文链路分析，Unity 默认输出绑定硬件 UART0，而非 stdout&#x2F;USB-CDC。LOLIN S2 Mini 无 UART0 转 USB 硬件电路，UART0 的物理输出无法被电脑捕获，导致 Unity 日志完全丢失。</p><p>此时 USB-CDC、VFS、stdout 链路均正常，无需排查硬件和基础配置，只需<strong>重定向 Unity 输出链路</strong>。</p><h3 id="4-3-终极解决方案：GCC-Linker-函数重定向"><a href="#4-3-终极解决方案：GCC-Linker-函数重定向" class="headerlink" title="4.3 终极解决方案：GCC Linker 函数重定向"></a>4.3 终极解决方案：GCC Linker 函数重定向</h3><p>利用 GCC <code>--wrap</code>链接器特性，劫持 Unity 原生输出函数，将其重定向到 stdout，复用 USB-CDC 正常链路。</p><h4 id="4-3-1-CMake-链接配置（核心）"><a href="#4-3-1-CMake-链接配置（核心）" class="headerlink" title="4.3.1 CMake 链接配置（核心）"></a>4.3.1 CMake 链接配置（核心）</h4><p>在 <code>CMakeLists.txt</code> 末尾添加链接参数，必须使用 <code>INTERFACE</code> 确保参数传递到最终链接阶段：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs shell"><span class="hljs-meta prompt_"># </span><span class="language-bash">重定向 Unity 输出至 stdout（USB-CDC）</span><br><span class="hljs-meta prompt_">target_link_libraries($</span><span class="language-bash">&#123;COMPONENT_LIB&#125; INTERFACE</span><br>    &quot;-Wl,--wrap=unity_putc&quot;<br>    &quot;-Wl,--wrap=unity_flush&quot;<br>)<br></code></pre></td></tr></table></figure><blockquote><p><strong>注意</strong>：禁止使用<code>target_link_options PRIVATE</code>，ESP-IDF 组件先编译为静态库，PRIVATE 参数无法传递到最终固件链接阶段，重定向会失效。</p></blockquote><h4 id="4-3-2-重定向函数实现"><a href="#4-3-2-重定向函数实现" class="headerlink" title="4.3.2 重定向函数实现"></a>4.3.2 重定向函数实现</h4><p>在 <code>main.c</code> 中实现劫持后的自定义输出函数，兼容串口换行格式：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&lt;stdio.h&gt;</span></span><br><br><span class="hljs-comment">// 劫持 unity_putc，重定向到 stdout</span><br><span class="hljs-type">void</span> __wrap_unity_putc(<span class="hljs-type">int</span> c)<br>&#123;<br>    <span class="hljs-comment">// 适配串口终端换行，将、n 转为、rn，避免排版错乱</span><br>    <span class="hljs-keyword">if</span> (c == <span class="hljs-string">&#x27;n&#x27;</span>) &#123;<br>        <span class="hljs-built_in">putchar</span>(<span class="hljs-string">&#x27;r&#x27;</span>);<br>    &#125;<br>    <span class="hljs-built_in">putchar</span>(c);<br>&#125;<br><br><span class="hljs-comment">// 劫持刷新函数，确保日志实时输出</span><br><span class="hljs-type">void</span> __wrap_unity_flush(<span class="hljs-type">void</span>)<br>&#123;<br>    fflush(<span class="hljs-built_in">stdout</span>);<br>&#125;<br><br></code></pre></td></tr></table></figure><h3 id="4-4-重定向后完整输出链路"><a href="#4-4-重定向后完整输出链路" class="headerlink" title="4.4 重定向后完整输出链路"></a>4.4 重定向后完整输出链路</h3><pre><code class=" mermaid">graph TD    A[&quot;Unity 测试日志&quot;] --&gt; B[&quot;unity_putc() 被链接器劫持&quot;]    B --&gt; C[&quot;__wrap_unity_putc() 自定义实现&quot;]    C --&gt; D[&quot;putchar() --&gt; stdout&quot;]    D --&gt; E[&quot;VFS 虚拟文件系统&quot;]    E --&gt; F[&quot;USB-CDC ACM&quot;]    F --&gt; G[&quot;ESP32-S2 原生 USB&quot;]    G --&gt; H[&quot;电脑串口工具正常显示&quot;]</code></pre><h3 id="4-5-重定向有效性验证"><a href="#4-5-重定向有效性验证" class="headerlink" title="4.5 重定向有效性验证"></a>4.5 重定向有效性验证</h3><ol><li><strong>检查链接参数是否生效</strong><code>rg &quot;wrap=unity&quot; build/build.ninja</code><br>正常输出对应 <code>--wrap</code> 配置</li><li><strong>检查自定义符号是否进入固件</strong><code>xtensa-esp32s2-elf-nm build/*.elf | rg &quot;__wrap_unity&quot;</code><br>正常展示 <code>__wrap_unity_putc</code>、<code>__wrap_unity_flush</code> 符号</li></ol><h2 id="五、最终完整配置文件"><a href="#五、最终完整配置文件" class="headerlink" title="五、最终完整配置文件"></a>五、最终完整配置文件</h2><h3 id="5-1-完整-CMakeLists-txt"><a href="#5-1-完整-CMakeLists-txt" class="headerlink" title="5.1 完整 CMakeLists.txt"></a>5.1 完整 CMakeLists.txt</h3><figure class="highlight cmake"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><code class="hljs cmake">idf_component_register(<br>    SRCS<br>        <span class="hljs-string">&quot;main.c&quot;</span><br>    INCLUDE_DIRS<br>        <span class="hljs-string">&quot;.&quot;</span><br>    REQUIRES<br>        esp_driver_gpio<br>        unity<br>        vfs<br>)<br><br><span class="hljs-comment"># 将 Unity 默认 UART0 输出重定向至 USB-CDC stdout</span><br><span class="hljs-keyword">target_link_libraries</span>(<span class="hljs-variable">$&#123;COMPONENT_LIB&#125;</span> INTERFACE<br>    <span class="hljs-string">&quot;-Wl,--wrap=unity_putc&quot;</span><br>    <span class="hljs-string">&quot;-Wl,--wrap=unity_flush&quot;</span><br>)<br></code></pre></td></tr></table></figure><h3 id="5-2-完整-Unity-重定向代码"><a href="#5-2-完整-Unity-重定向代码" class="headerlink" title="5.2 完整 Unity 重定向代码"></a>5.2 完整 Unity 重定向代码</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&lt;stdio.h&gt;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;unity.h&quot;</span></span><br><br><span class="hljs-type">void</span> __wrap_unity_putc(<span class="hljs-type">int</span> c)<br>&#123;<br>    <span class="hljs-keyword">if</span> (c == <span class="hljs-string">&#x27;n&#x27;</span>) &#123;<br>        <span class="hljs-built_in">putchar</span>(<span class="hljs-string">&#x27;r&#x27;</span>);<br>    &#125;<br>    <span class="hljs-built_in">putchar</span>(c);<br>&#125;<br><br><span class="hljs-type">void</span> __wrap_unity_flush(<span class="hljs-type">void</span>)<br>&#123;<br>    fflush(<span class="hljs-built_in">stdout</span>);<br>&#125;<br><br></code></pre></td></tr></table></figure><h2 id="六、通用排障方法论（ESP-IDF-串口输出通用）"><a href="#六、通用排障方法论（ESP-IDF-串口输出通用）" class="headerlink" title="六、通用排障方法论（ESP-IDF 串口输出通用）"></a>六、通用排障方法论（ESP-IDF 串口输出通用）</h2><p>针对 ESP-IDF「配置开启但功能失效」的各类问题，固定分层排查流程，可适配所有串口、外设功能异常：</p><ol><li><strong>验证应用运行状态</strong>：通过 LED、GPIO、定时任务确认 <code>app_main</code> 正常执行，排除固件未运行问题；</li><li><strong>定位输出接口类型</strong>：区分 printf、ESP_LOG、Unity、ROM 控制台、UART、USB-CDC 不同输出接口；</li><li><strong>梳理底层输出链路</strong>：明确当前接口是走 VFS+USB-CDC，还是裸 UART&#x2F;ROM 硬件通路；</li><li><strong>校验 Kconfig 配置</strong>：确认对应功能宏已开启；</li><li><strong>校验编译逻辑</strong>：检查核心源文件是否参与编译、CMake 依赖是否完整；</li><li><strong>校验 ELF 固件符号</strong>：确认核心初始化函数、自定义函数已打包进固件；</li><li><strong>最后排查运行时异常</strong>：排除硬件、初始化时序等上层问题。</li></ol><h2 id="七、核心总结"><a href="#七、核心总结" class="headerlink" title="七、核心总结"></a>七、核心总结</h2><p>本次排障的两个问题相互独立，覆盖了 ESP-IDF 6.0 升级和 ESP32-S2 原生 USB 开发的两大核心坑点：</p><ol><li><strong>USB-CDC 无输出</strong>：并非配置未开启，而是 ESP-IDF 6.0 VFS 组件重构后，<strong>缺少 vfs 组件依赖导致核心代码未编译</strong>，属于编译层级问题；</li><li><strong>Unity 日志无输出</strong>：并非程序异常，是<strong>输出链路不匹配</strong>，Unity 默认走硬件 UART0，与 USB-CDC 虚拟串口完全隔离，需通过链接器重定向链路；</li></ol><p>关键开发经验：<strong>新版本 ESP-IDF 中，menuconfig 配置生效≠代码编译生效，必须结合组件依赖、编译脚本、固件符号逐层校验；原生 USB 开发板需严格区分 UART 硬件通路与 USB-CDC 虚拟串口通路</strong>。</p><blockquote><p>（注：排障和文章生成由 opencode 完成，润色由豆包完成）</p></blockquote><h2 id="相关链接"><a href="#相关链接" class="headerlink" title="相关链接"></a>相关链接</h2><ul><li><a href="https://www.wemos.cc/en/latest/s2/s2_mini.html">wemos S2 mini 文档</a></li><li><a href="https://docs.espressif.com/projects/esp-idf/zh_CN/release-v6.0/esp32s2/get-started/index.html">esp-idf 6.0 文档</a></li></ul>]]>
    </content>
    <id>https://bubao.github.io/posts/dc7a6102.html</id>
    <link href="https://bubao.github.io/posts/dc7a6102.html"/>
    <published>2026-09-11T11:56:16.000Z</published>
    <summary>基于 ESP-IDF 6.0，排查 LOLIN S2 Mini 原生 USB-CDC 串口无输出，以及 Unity 测试结果无法通过 USB-CDC 输出的问题。</summary>
    <title>ESP32-S2 原生 USB-CDC 与 Unity 串口输出排障</title>
    <updated>2026-10-31T20:37:58.466Z</updated>
  </entry>
  <entry>
    <author>
      <name>一念</name>
    </author>
    <category term="教程" scheme="https://bubao.github.io/categories/%E6%95%99%E7%A8%8B/"/>
    <category term="Python" scheme="https://bubao.github.io/tags/Python/"/>
    <category term="深度学习" scheme="https://bubao.github.io/tags/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0/"/>
    <category term="情绪识别" scheme="https://bubao.github.io/tags/%E6%83%85%E7%BB%AA%E8%AF%86%E5%88%AB/"/>
    <category term="计算机视觉" scheme="https://bubao.github.io/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E8%A7%86%E8%A7%89/"/>
    <content>
      <![CDATA[<p>情绪识别技术在现代社会中变得越来越重要，它在人机交互、智能监控、教育评估等多个领域都有广泛应用。本文将介绍如何使用 DeepFace 库构建一个实时情绪检测系统，通过摄像头实时捕捉和分析人脸情绪。</p><span id="more"></span><h2 id="DeepFace-简介"><a href="#DeepFace-简介" class="headerlink" title="DeepFace 简介"></a>DeepFace 简介</h2><p>DeepFace 是一个轻量级的人脸识别和面部属性分析框架，支持年龄、性别、情绪和种族等多种面部属性的分析。它封装了多种最先进的模型，包括 VGG-Face、FaceNet、OpenFace、DeepFace、DeepID、ArcFace、Dlib、SFace、GhostFaceNet 和 Buffalo_L 等。</p><p>DeepFace 的主要特点包括：</p><ol><li><strong>高精度</strong>：集成了多种先进的人脸识别模型，准确率超过人类水平（97.53%）</li><li><strong>多功能</strong>：不仅支持情绪识别，还支持年龄、性别、种族等面部属性分析</li><li><strong>易于使用</strong>：提供了简洁的 API，只需几行代码即可实现复杂的人脸分析功能</li><li><strong>灵活配置</strong>：支持多种人脸检测器和模型切换，可根据需求选择最适合的配置</li></ol><h2 id="环境准备"><a href="#环境准备" class="headerlink" title="环境准备"></a>环境准备</h2><p>在开始之前，我们需要安装必要的依赖项。DeepFace 需要 Python 3.6 或更高版本。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 安装 DeepFace 库</span><br>pip install deepface<br><br><span class="hljs-comment"># 或者从源码安装最新版本</span><br>pip install git+https://github.com/serengil/deepface.git<br></code></pre></td></tr></table></figure><p>DeepFace 会自动安装所需的依赖项，包括 TensorFlow、OpenCV 等。</p><h2 id="基础用法"><a href="#基础用法" class="headerlink" title="基础用法"></a>基础用法</h2><p>让我们先从一个简单的图像情绪识别示例开始：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">from</span> deepface <span class="hljs-keyword">import</span> DeepFace<br><br><span class="hljs-comment"># 分析图像中的情绪</span><br>results = DeepFace.analyze(img_path=<span class="hljs-string">&quot;person.jpg&quot;</span>, actions=[<span class="hljs-string">&#x27;emotion&#x27;</span>])<br><span class="hljs-built_in">print</span>(results)<br><br><span class="hljs-comment"># 输出示例：</span><br><span class="hljs-comment"># [&#123;&#x27;region&#x27;: &#123;&#x27;x&#x27;: 170, &#x27;y&#x27;: 105, &#x27;w&#x27;: 200, &#x27;h&#x27;: 200&#125;, </span><br><span class="hljs-comment">#   &#x27;emotion&#x27;: &#123;&#x27;angry&#x27;: 0.03, &#x27;disgust&#x27;: 0.01, &#x27;fear&#x27;: 0.02, &#x27;happy&#x27;: 85.21, &#x27;sad&#x27;: 0.05, &#x27;surprise&#x27;: 0.03, &#x27;neutral&#x27;: 14.65&#125;&#125;]</span><br><br><span class="hljs-comment"># 同时分析多种属性</span><br>results = DeepFace.analyze(img_path=<span class="hljs-string">&quot;person.jpg&quot;</span>, <br>                         actions=[<span class="hljs-string">&#x27;emotion&#x27;</span>, <span class="hljs-string">&#x27;age&#x27;</span>, <span class="hljs-string">&#x27;gender&#x27;</span>, <span class="hljs-string">&#x27;race&#x27;</span>])<br><span class="hljs-built_in">print</span>(results)<br></code></pre></td></tr></table></figure><p>DeepFace 不仅可以分析情绪，还能同时分析年龄、性别和种族等多种属性，这是 FER 库不具备的功能。</p><h2 id="实时情绪检测系统"><a href="#实时情绪检测系统" class="headerlink" title="实时情绪检测系统"></a>实时情绪检测系统</h2><p>现在让我们构建一个实时情绪检测系统，通过摄像头捕获视频流并实时分析人脸情绪。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">import</span> cv2<br><span class="hljs-keyword">from</span> deepface <span class="hljs-keyword">import</span> DeepFace<br><br><span class="hljs-keyword">def</span> <span class="hljs-title function_">real_time_emotion_detection</span>():<br>    <span class="hljs-comment"># 初始化摄像头</span><br>    cap = cv2.VideoCapture(<span class="hljs-number">0</span>)<br>    <br>    <span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;实时情绪检测系统已启动，按 &#x27;q&#x27; 键退出&quot;</span>)<br>    <br>    <span class="hljs-keyword">while</span> <span class="hljs-literal">True</span>:<br>        <span class="hljs-comment"># 读取摄像头帧</span><br>        ret, frame = cap.read()<br>        <br>        <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> ret:<br>            <span class="hljs-keyword">break</span><br>            <br>        <span class="hljs-keyword">try</span>:<br>            <span class="hljs-comment"># 检测情绪</span><br>            results = DeepFace.analyze(frame, actions=[<span class="hljs-string">&#x27;emotion&#x27;</span>], detector_backend=<span class="hljs-string">&#x27;opencv&#x27;</span>)<br>            <br>            <span class="hljs-comment"># 处理单个检测结果</span><br>            <span class="hljs-keyword">if</span> <span class="hljs-built_in">isinstance</span>(results, <span class="hljs-built_in">dict</span>):<br>                results = [results]<br>                <br>            <span class="hljs-comment"># 在图像上绘制检测结果</span><br>            <span class="hljs-keyword">for</span> result <span class="hljs-keyword">in</span> results:<br>                <span class="hljs-comment"># 获取边界框坐标</span><br>                region = result[<span class="hljs-string">&quot;region&quot;</span>]<br>                x, y, w, h = region[<span class="hljs-string">&quot;x&quot;</span>], region[<span class="hljs-string">&quot;y&quot;</span>], region[<span class="hljs-string">&quot;w&quot;</span>], region[<span class="hljs-string">&quot;h&quot;</span>]<br>                <br>                <span class="hljs-comment"># 获取情绪数据</span><br>                emotions = result[<span class="hljs-string">&quot;emotion&quot;</span>]<br>                <br>                <span class="hljs-comment"># 找到置信度最高的情绪</span><br>                dominant_emotion = <span class="hljs-built_in">max</span>(emotions, key=emotions.get)<br>                confidence = emotions[dominant_emotion]<br>                <br>                <span class="hljs-comment"># 绘制边界框</span><br>                cv2.rectangle(frame, (x, y), (x + w, y + h), (<span class="hljs-number">0</span>, <span class="hljs-number">255</span>, <span class="hljs-number">0</span>), <span class="hljs-number">2</span>)<br>                <br>                <span class="hljs-comment"># 在边界框上方显示情绪和置信度</span><br>                text = <span class="hljs-string">f&quot;<span class="hljs-subst">&#123;dominant_emotion&#125;</span>: <span class="hljs-subst">&#123;confidence:<span class="hljs-number">.2</span>f&#125;</span>&quot;</span><br>                cv2.putText(frame, text, (x, y - <span class="hljs-number">10</span>), cv2.FONT_HERSHEY_SIMPLEX, <br>                           <span class="hljs-number">0.9</span>, (<span class="hljs-number">0</span>, <span class="hljs-number">255</span>, <span class="hljs-number">0</span>), <span class="hljs-number">2</span>)<br>        <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:<br>            <span class="hljs-comment"># 如果检测不到人脸，继续下一帧</span><br>            <span class="hljs-keyword">pass</span><br>        <br>        <span class="hljs-comment"># 显示结果</span><br>        cv2.imshow(<span class="hljs-string">&#x27;实时情绪检测&#x27;</span>, frame)<br>        <br>        <span class="hljs-comment"># 按 &#x27;q&#x27; 键退出</span><br>        <span class="hljs-keyword">if</span> cv2.waitKey(<span class="hljs-number">1</span>) &amp; <span class="hljs-number">0xFF</span> == <span class="hljs-built_in">ord</span>(<span class="hljs-string">&#x27;q&#x27;</span>):<br>            <span class="hljs-keyword">break</span><br>    <br>    <span class="hljs-comment"># 释放资源</span><br>    cap.release()<br>    cv2.destroyAllWindows()<br><br><span class="hljs-comment"># 运行实时情绪检测系统</span><br><span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">&quot;__main__&quot;</span>:<br>    real_time_emotion_detection()<br></code></pre></td></tr></table></figure><h2 id="增强版情绪检测系统"><a href="#增强版情绪检测系统" class="headerlink" title="增强版情绪检测系统"></a>增强版情绪检测系统</h2><p>为了提供更好的用户体验，我们可以创建一个增强版的情绪检测系统，它不仅显示实时情绪，还会统计一段时间内的情绪分布。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">import</span> cv2<br><span class="hljs-keyword">from</span> deepface <span class="hljs-keyword">import</span> DeepFace<br><span class="hljs-keyword">from</span> collections <span class="hljs-keyword">import</span> deque<br><br><span class="hljs-keyword">class</span> <span class="hljs-title class_">EnhancedEmotionDetector</span>:<br>    <span class="hljs-keyword">def</span> <span class="hljs-title function_">__init__</span>(<span class="hljs-params">self, max_history=<span class="hljs-number">100</span></span>):<br>        <span class="hljs-variable language_">self</span>.cap = cv2.VideoCapture(<span class="hljs-number">0</span>)<br>        <span class="hljs-variable language_">self</span>.emotion_history = deque(maxlen=max_history)<br>        <span class="hljs-variable language_">self</span>.emotions_list = [<span class="hljs-string">&#x27;angry&#x27;</span>, <span class="hljs-string">&#x27;disgust&#x27;</span>, <span class="hljs-string">&#x27;fear&#x27;</span>, <span class="hljs-string">&#x27;happy&#x27;</span>, <span class="hljs-string">&#x27;sad&#x27;</span>, <span class="hljs-string">&#x27;surprise&#x27;</span>, <span class="hljs-string">&#x27;neutral&#x27;</span>]<br>        <br>    <span class="hljs-keyword">def</span> <span class="hljs-title function_">run</span>(<span class="hljs-params">self</span>):<br>        <span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;增强版情绪检测系统已启动，按 &#x27;q&#x27; 键退出&quot;</span>)<br>        <br>        <span class="hljs-keyword">while</span> <span class="hljs-literal">True</span>:<br>            ret, frame = <span class="hljs-variable language_">self</span>.cap.read()<br>            <br>            <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> ret:<br>                <span class="hljs-keyword">break</span><br>                <br>            <span class="hljs-keyword">try</span>:<br>                <span class="hljs-comment"># 检测情绪</span><br>                results = DeepFace.analyze(frame, actions=[<span class="hljs-string">&#x27;emotion&#x27;</span>], detector_backend=<span class="hljs-string">&#x27;opencv&#x27;</span>)<br>                <br>                <span class="hljs-comment"># 处理单个检测结果</span><br>                <span class="hljs-keyword">if</span> <span class="hljs-built_in">isinstance</span>(results, <span class="hljs-built_in">dict</span>):<br>                    results = [results]<br>                <br>                <span class="hljs-comment"># 处理检测结果</span><br>                <span class="hljs-keyword">for</span> result <span class="hljs-keyword">in</span> results:<br>                    region = result[<span class="hljs-string">&quot;region&quot;</span>]<br>                    x, y, w, h = region[<span class="hljs-string">&quot;x&quot;</span>], region[<span class="hljs-string">&quot;y&quot;</span>], region[<span class="hljs-string">&quot;w&quot;</span>], region[<span class="hljs-string">&quot;h&quot;</span>]<br>                    emotions = result[<span class="hljs-string">&quot;emotion&quot;</span>]<br>                    <br>                    <span class="hljs-comment"># 找到主导情绪</span><br>                    dominant_emotion = <span class="hljs-built_in">max</span>(emotions, key=emotions.get)<br>                    confidence = emotions[dominant_emotion]<br>                    <br>                    <span class="hljs-comment"># 添加到历史记录</span><br>                    <span class="hljs-variable language_">self</span>.emotion_history.append(dominant_emotion)<br>                    <br>                    <span class="hljs-comment"># 绘制边界框和情绪信息</span><br>                    cv2.rectangle(frame, (x, y), (x + w, y + h), (<span class="hljs-number">0</span>, <span class="hljs-number">255</span>, <span class="hljs-number">0</span>), <span class="hljs-number">2</span>)<br>                    text = <span class="hljs-string">f&quot;<span class="hljs-subst">&#123;dominant_emotion&#125;</span>: <span class="hljs-subst">&#123;confidence:<span class="hljs-number">.2</span>f&#125;</span>&quot;</span><br>                    cv2.putText(frame, text, (x, y - <span class="hljs-number">10</span>), cv2.FONT_HERSHEY_SIMPLEX, <br>                               <span class="hljs-number">0.9</span>, (<span class="hljs-number">0</span>, <span class="hljs-number">255</span>, <span class="hljs-number">0</span>), <span class="hljs-number">2</span>)<br>                <br>                <span class="hljs-comment"># 显示统计信息</span><br>                <span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(<span class="hljs-variable language_">self</span>.emotion_history) &gt; <span class="hljs-number">0</span>:<br>                    <span class="hljs-variable language_">self</span>.display_statistics(frame)<br>            <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:<br>                <span class="hljs-comment"># 如果检测不到人脸，继续下一帧</span><br>                <span class="hljs-keyword">pass</span><br>            <br>            cv2.imshow(<span class="hljs-string">&#x27;增强版情绪检测系统&#x27;</span>, frame)<br>            <br>            <span class="hljs-keyword">if</span> cv2.waitKey(<span class="hljs-number">1</span>) &amp; <span class="hljs-number">0xFF</span> == <span class="hljs-built_in">ord</span>(<span class="hljs-string">&#x27;q&#x27;</span>):<br>                <span class="hljs-keyword">break</span><br>                <br>        <span class="hljs-variable language_">self</span>.cleanup()<br>        <br>    <span class="hljs-keyword">def</span> <span class="hljs-title function_">display_statistics</span>(<span class="hljs-params">self, frame</span>):<br>        <span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(<span class="hljs-variable language_">self</span>.emotion_history) == <span class="hljs-number">0</span>:<br>            <span class="hljs-keyword">return</span><br>            <br>        <span class="hljs-comment"># 计算情绪分布</span><br>        emotion_counts = &#123;emotion: <span class="hljs-number">0</span> <span class="hljs-keyword">for</span> emotion <span class="hljs-keyword">in</span> <span class="hljs-variable language_">self</span>.emotions_list&#125;<br>        <span class="hljs-keyword">for</span> emotion <span class="hljs-keyword">in</span> <span class="hljs-variable language_">self</span>.emotion_history:<br>            emotion_counts[emotion] += <span class="hljs-number">1</span><br>            <br>        total = <span class="hljs-built_in">len</span>(<span class="hljs-variable language_">self</span>.emotion_history)<br>        dominant_overall = <span class="hljs-built_in">max</span>(emotion_counts, key=emotion_counts.get)<br>        <br>        <span class="hljs-comment"># 在图像上显示统计信息</span><br>        cv2.putText(frame, <span class="hljs-string">f&quot;主导情绪：<span class="hljs-subst">&#123;dominant_overall&#125;</span>&quot;</span>, (<span class="hljs-number">10</span>, <span class="hljs-number">30</span>), <br>                   cv2.FONT_HERSHEY_SIMPLEX, <span class="hljs-number">0.7</span>, (<span class="hljs-number">255</span>, <span class="hljs-number">255</span>, <span class="hljs-number">255</span>), <span class="hljs-number">2</span>)<br>        cv2.putText(frame, <span class="hljs-string">f&quot;检测次数：<span class="hljs-subst">&#123;total&#125;</span>&quot;</span>, (<span class="hljs-number">10</span>, <span class="hljs-number">60</span>), <br>                   cv2.FONT_HERSHEY_SIMPLEX, <span class="hljs-number">0.7</span>, (<span class="hljs-number">255</span>, <span class="hljs-number">255</span>, <span class="hljs-number">255</span>), <span class="hljs-number">2</span>)<br>        <br>    <span class="hljs-keyword">def</span> <span class="hljs-title function_">cleanup</span>(<span class="hljs-params">self</span>):<br>        <span class="hljs-variable language_">self</span>.cap.release()<br>        cv2.destroyAllWindows()<br><br><span class="hljs-comment"># 运行增强版情绪检测系统</span><br><span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">&quot;__main__&quot;</span>:<br>    detector = EnhancedEmotionDetector()<br>    detector.run()<br></code></pre></td></tr></table></figure><h2 id="视频文件情绪分析"><a href="#视频文件情绪分析" class="headerlink" title="视频文件情绪分析"></a>视频文件情绪分析</h2><p>DeepFace 提供了流式分析功能，可以直接对视频流进行实时情绪分析：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">from</span> deepface <span class="hljs-keyword">import</span> DeepFace<br><br><span class="hljs-comment"># 对视频流进行实时分析</span><br>DeepFace.stream(db_path=<span class="hljs-string">&quot;./database&quot;</span>, actions=[<span class="hljs-string">&#x27;emotion&#x27;</span>])<br></code></pre></td></tr></table></figure><p>此外，DeepFace 还提供了更高级的功能，如人脸识别、验证等，这些都是 FER 库不具备的。</p><h2 id="应用场景"><a href="#应用场景" class="headerlink" title="应用场景"></a>应用场景</h2><p>实时情绪检测系统在多个领域都有广泛的应用前景：</p><h3 id="1-人机交互"><a href="#1-人机交互" class="headerlink" title="1. 人机交互"></a>1. 人机交互</h3><p>通过实时识别用户的面部表情，调整应用程序的行为或提供个性化反馈。</p><h3 id="2-市场营销"><a href="#2-市场营销" class="headerlink" title="2. 市场营销"></a>2. 市场营销</h3><p>了解顾客对商品或广告的真实反应，优化营销策略。</p><h3 id="3-智能驾驶"><a href="#3-智能驾驶" class="headerlink" title="3. 智能驾驶"></a>3. 智能驾驶</h3><p>监控驾驶员的疲劳和情绪状态，提高驾驶安全性。</p><h3 id="4-娱乐产业"><a href="#4-娱乐产业" class="headerlink" title="4. 娱乐产业"></a>4. 娱乐产业</h3><p>创造更具沉浸感的互动体验，根据用户表情调整游戏或影视内容。</p><h3 id="5-教育领域"><a href="#5-教育领域" class="headerlink" title="5. 教育领域"></a>5. 教育领域</h3><p>通过分析学生的面部表情，评估教学效果，及时调整教学策略。</p><h3 id="6-心理健康"><a href="#6-心理健康" class="headerlink" title="6. 心理健康"></a>6. 心理健康</h3><p>通过表情变化早期识别心理问题，为心理咨询提供辅助数据。</p><h3 id="7-安防监控"><a href="#7-安防监控" class="headerlink" title="7. 安防监控"></a>7. 安防监控</h3><p>在公共场所监控系统中，通过识别异常表情进行预警。</p><h2 id="注意事项和局限性"><a href="#注意事项和局限性" class="headerlink" title="注意事项和局限性"></a>注意事项和局限性</h2><p>尽管 DeepFace 提供了便捷且高精度的情绪识别功能，但在实际应用中仍需注意以下几点：</p><ol><li><p><strong>计算资源消耗</strong>：DeepFace 集成了多种先进模型，对计算资源的要求较高，尤其是在使用复杂模型时。</p></li><li><p><strong>光照和环境影响</strong>：在光线不足或变化剧烈的环境中，识别准确率会下降。</p></li><li><p><strong>遮挡问题</strong>：当人脸被口罩、手等物体遮挡时，可能无法正确识别。</p></li><li><p><strong>隐私考虑</strong>：在实际部署时需要考虑用户隐私保护问题。</p></li><li><p><strong>模型选择</strong>：不同的模型有不同的特点，需要根据具体应用场景选择合适的模型和检测器。</p></li></ol><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>通过 DeepFace 库，我们可以快速构建功能强大且高精度的实时情绪检测系统。该库不仅提供了简单易用的 API，还集成了多种先进的人脸识别模型，使得开发者能够根据具体需求选择最适合的模型。</p><p>DeepFace 相比 FER 库具有明显优势：</p><ol><li><strong>更高的准确率</strong>：集成了多种先进模型，准确率超过人类水平</li><li><strong>更多功能</strong>：支持情绪、年龄、性别、种族等多种属性分析</li><li><strong>更好的灵活性</strong>：支持多种模型和检测器切换</li></ol><p>情绪识别技术正处于快速发展阶段，随着深度学习技术的进步，未来的系统将更加准确和可靠。通过本文介绍的方法，你可以快速搭建自己的情绪检测系统，并在此基础上进行进一步的优化和扩展。</p>]]>
    </content>
    <id>https://bubao.github.io/posts/3c6adfa3.html</id>
    <link href="https://bubao.github.io/posts/3c6adfa3.html"/>
    <published>2025-12-06T16:00:00.000Z</published>
    <summary>
      <![CDATA[<p>情绪识别技术在现代社会中变得越来越重要，它在人机交互、智能监控、教育评估等多个领域都有广泛应用。本文将介绍如何使用 DeepFace 库构建一个实时情绪检测系统，通过摄像头实时捕捉和分析人脸情绪。</p>]]>
    </summary>
    <title>使用 DeepFace 构建实时情绪检测系统</title>
    <updated>2026-10-31T20:37:58.474Z</updated>
  </entry>
  <entry>
    <author>
      <name>一念</name>
    </author>
    <category term="教程" scheme="https://bubao.github.io/categories/%E6%95%99%E7%A8%8B/"/>
    <category term="Python" scheme="https://bubao.github.io/tags/Python/"/>
    <category term="深度学习" scheme="https://bubao.github.io/tags/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0/"/>
    <category term="情绪识别" scheme="https://bubao.github.io/tags/%E6%83%85%E7%BB%AA%E8%AF%86%E5%88%AB/"/>
    <category term="计算机视觉" scheme="https://bubao.github.io/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E8%A7%86%E8%A7%89/"/>
    <content>
      <![CDATA[<p>情绪识别技术在现代社会中变得越来越重要，它在人机交互、智能监控、教育评估等多个领域都有广泛应用。本文将介绍如何使用 FER (Facial Expression Recognition) 库构建一个实时情绪检测系统，通过摄像头实时捕捉和分析人脸情绪。</p><span id="more"></span><h2 id="FER-库简介"><a href="#FER-库简介" class="headerlink" title="FER 库简介"></a>FER 库简介</h2><p>FER 是一个基于深度神经网络实现的人脸表情识别库，它封装成了 PyPI 包，方便 Python 开发者集成。该库能够检测并识别七种基本情绪：愤怒、厌恶、恐惧、快乐、悲伤、惊讶和中性。</p><p>FER 库的主要特点包括：</p><ol><li><strong>易于使用</strong>：提供了简洁的 API，只需几行代码即可实现面部表情识别</li><li><strong>高精度</strong>：基于深度学习技术，能够提供较为准确的表情识别结果</li><li><strong>多平台支持</strong>：支持多种面部检测方法，可根据需求选择最适合的方法</li><li><strong>视频处理</strong>：除了静态图像，还支持视频流的表情识别，适用于实时应用场景</li></ol><h2 id="环境准备"><a href="#环境准备" class="headerlink" title="环境准备"></a>环境准备</h2><p>在开始之前，我们需要安装必要的依赖项。FER 库需要 Python 3.6 或更高版本，以及 OpenCV 和 TensorFlow。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 安装 FER 库</span><br>pip install fer<br><br><span class="hljs-comment"># 安装必要的依赖（OpenCV 和 TensorFlow）</span><br>pip install tensorflow&gt;=1.7 opencv-contrib-python==3.3.0.9<br><br><span class="hljs-comment"># 如果需要处理视频文件，还需要安装 ffmpeg 和 moviepy</span><br>pip install ffmpeg moviepy<br></code></pre></td></tr></table></figure><h2 id="基础用法"><a href="#基础用法" class="headerlink" title="基础用法"></a>基础用法</h2><p>让我们先从一个简单的图像情绪识别示例开始：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">from</span> fer <span class="hljs-keyword">import</span> FER<br><span class="hljs-keyword">import</span> cv2<br><br><span class="hljs-comment"># 读取图像</span><br>img = cv2.imread(<span class="hljs-string">&quot;person.jpg&quot;</span>)<br><br><span class="hljs-comment"># 创建检测器实例</span><br>detector = FER()<br><br><span class="hljs-comment"># 检测情绪</span><br>results = detector.detect_emotions(img)<br><span class="hljs-built_in">print</span>(results)<br><br><span class="hljs-comment"># 输出示例：</span><br><span class="hljs-comment"># [&#123;&#x27;box&#x27;: [277, 90, 48, 63], &#x27;emotions&#x27;: &#123;&#x27;angry&#x27;: 0.02, &#x27;disgust&#x27;: 0.0, &#x27;fear&#x27;: 0.05, &#x27;happy&#x27;: 0.16, &#x27;neutral&#x27;: 0.09, &#x27;sad&#x27;: 0.27, &#x27;surprise&#x27;: 0.41&#125;&#125;]</span><br><br><span class="hljs-comment"># 获取最主要的情绪</span><br>emotion, score = detector.top_emotion(img)<br><span class="hljs-built_in">print</span>(<span class="hljs-string">f&quot;主要情绪：<span class="hljs-subst">&#123;emotion&#125;</span>, 置信度：<span class="hljs-subst">&#123;score&#125;</span>&quot;</span>)<br></code></pre></td></tr></table></figure><h2 id="实时情绪检测系统"><a href="#实时情绪检测系统" class="headerlink" title="实时情绪检测系统"></a>实时情绪检测系统</h2><p>现在让我们构建一个实时情绪检测系统，通过摄像头捕获视频流并实时分析人脸情绪。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">import</span> cv2<br><span class="hljs-keyword">from</span> fer <span class="hljs-keyword">import</span> FER<br><br><span class="hljs-keyword">def</span> <span class="hljs-title function_">real_time_emotion_detection</span>():<br>    <span class="hljs-comment"># 初始化摄像头</span><br>    cap = cv2.VideoCapture(<span class="hljs-number">0</span>)<br>    <br>    <span class="hljs-comment"># 创建情绪检测器，使用 MTCNN 提高检测精度</span><br>    detector = FER(mtcnn=<span class="hljs-literal">True</span>)<br>    <br>    <span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;实时情绪检测系统已启动，按 &#x27;q&#x27; 键退出&quot;</span>)<br>    <br>    <span class="hljs-keyword">while</span> <span class="hljs-literal">True</span>:<br>        <span class="hljs-comment"># 读取摄像头帧</span><br>        ret, frame = cap.read()<br>        <br>        <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> ret:<br>            <span class="hljs-keyword">break</span><br>            <br>        <span class="hljs-comment"># 检测情绪</span><br>        results = detector.detect_emotions(frame)<br>        <br>        <span class="hljs-comment"># 在图像上绘制检测结果</span><br>        <span class="hljs-keyword">for</span> result <span class="hljs-keyword">in</span> results:<br>            <span class="hljs-comment"># 获取边界框坐标</span><br>            x, y, w, h = result[<span class="hljs-string">&quot;box&quot;</span>]<br>            <br>            <span class="hljs-comment"># 获取情绪数据</span><br>            emotions = result[<span class="hljs-string">&quot;emotions&quot;</span>]<br>            <br>            <span class="hljs-comment"># 找到置信度最高的情绪</span><br>            dominant_emotion = <span class="hljs-built_in">max</span>(emotions, key=emotions.get)<br>            confidence = emotions[dominant_emotion]<br>            <br>            <span class="hljs-comment"># 绘制边界框</span><br>            cv2.rectangle(frame, (x, y), (x + w, y + h), (<span class="hljs-number">0</span>, <span class="hljs-number">255</span>, <span class="hljs-number">0</span>), <span class="hljs-number">2</span>)<br>            <br>            <span class="hljs-comment"># 在边界框上方显示情绪和置信度</span><br>            text = <span class="hljs-string">f&quot;<span class="hljs-subst">&#123;dominant_emotion&#125;</span>: <span class="hljs-subst">&#123;confidence:<span class="hljs-number">.2</span>f&#125;</span>&quot;</span><br>            cv2.putText(frame, text, (x, y - <span class="hljs-number">10</span>), cv2.FONT_HERSHEY_SIMPLEX, <br>                       <span class="hljs-number">0.9</span>, (<span class="hljs-number">0</span>, <span class="hljs-number">255</span>, <span class="hljs-number">0</span>), <span class="hljs-number">2</span>)<br>        <br>        <span class="hljs-comment"># 显示结果</span><br>        cv2.imshow(<span class="hljs-string">&#x27;实时情绪检测&#x27;</span>, frame)<br>        <br>        <span class="hljs-comment"># 按 &#x27;q&#x27; 键退出</span><br>        <span class="hljs-keyword">if</span> cv2.waitKey(<span class="hljs-number">1</span>) &amp; <span class="hljs-number">0xFF</span> == <span class="hljs-built_in">ord</span>(<span class="hljs-string">&#x27;q&#x27;</span>):<br>            <span class="hljs-keyword">break</span><br>    <br>    <span class="hljs-comment"># 释放资源</span><br>    cap.release()<br>    cv2.destroyAllWindows()<br><br><span class="hljs-comment"># 运行实时情绪检测系统</span><br><span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">&quot;__main__&quot;</span>:<br>    real_time_emotion_detection()<br></code></pre></td></tr></table></figure><h2 id="增强版情绪检测系统"><a href="#增强版情绪检测系统" class="headerlink" title="增强版情绪检测系统"></a>增强版情绪检测系统</h2><p>为了提供更好的用户体验，我们可以创建一个增强版的情绪检测系统，它不仅显示实时情绪，还会统计一段时间内的情绪分布。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">import</span> cv2<br><span class="hljs-keyword">import</span> numpy <span class="hljs-keyword">as</span> np<br><span class="hljs-keyword">from</span> fer <span class="hljs-keyword">import</span> FER<br><span class="hljs-keyword">from</span> collections <span class="hljs-keyword">import</span> deque<br><span class="hljs-keyword">import</span> matplotlib.pyplot <span class="hljs-keyword">as</span> plt<br><br><span class="hljs-keyword">class</span> <span class="hljs-title class_">EnhancedEmotionDetector</span>:<br>    <span class="hljs-keyword">def</span> <span class="hljs-title function_">__init__</span>(<span class="hljs-params">self, max_history=<span class="hljs-number">100</span></span>):<br>        <span class="hljs-variable language_">self</span>.cap = cv2.VideoCapture(<span class="hljs-number">0</span>)<br>        <span class="hljs-variable language_">self</span>.detector = FER(mtcnn=<span class="hljs-literal">True</span>)<br>        <span class="hljs-variable language_">self</span>.emotion_history = deque(maxlen=max_history)<br>        <span class="hljs-variable language_">self</span>.emotions_list = [<span class="hljs-string">&#x27;angry&#x27;</span>, <span class="hljs-string">&#x27;disgust&#x27;</span>, <span class="hljs-string">&#x27;fear&#x27;</span>, <span class="hljs-string">&#x27;happy&#x27;</span>, <span class="hljs-string">&#x27;neutral&#x27;</span>, <span class="hljs-string">&#x27;sad&#x27;</span>, <span class="hljs-string">&#x27;surprise&#x27;</span>]<br>        <br>    <span class="hljs-keyword">def</span> <span class="hljs-title function_">run</span>(<span class="hljs-params">self</span>):<br>        <span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;增强版情绪检测系统已启动，按 &#x27;q&#x27; 键退出&quot;</span>)<br>        <br>        <span class="hljs-keyword">while</span> <span class="hljs-literal">True</span>:<br>            ret, frame = <span class="hljs-variable language_">self</span>.cap.read()<br>            <br>            <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> ret:<br>                <span class="hljs-keyword">break</span><br>                <br>            <span class="hljs-comment"># 检测情绪</span><br>            results = <span class="hljs-variable language_">self</span>.detector.detect_emotions(frame)<br>            <br>            <span class="hljs-comment"># 处理检测结果</span><br>            <span class="hljs-keyword">for</span> result <span class="hljs-keyword">in</span> results:<br>                x, y, w, h = result[<span class="hljs-string">&quot;box&quot;</span>]<br>                emotions = result[<span class="hljs-string">&quot;emotions&quot;</span>]<br>                <br>                <span class="hljs-comment"># 找到主导情绪</span><br>                dominant_emotion = <span class="hljs-built_in">max</span>(emotions, key=emotions.get)<br>                confidence = emotions[dominant_emotion]<br>                <br>                <span class="hljs-comment"># 添加到历史记录</span><br>                <span class="hljs-variable language_">self</span>.emotion_history.append(dominant_emotion)<br>                <br>                <span class="hljs-comment"># 绘制边界框和情绪信息</span><br>                cv2.rectangle(frame, (x, y), (x + w, y + h), (<span class="hljs-number">0</span>, <span class="hljs-number">255</span>, <span class="hljs-number">0</span>), <span class="hljs-number">2</span>)<br>                text = <span class="hljs-string">f&quot;<span class="hljs-subst">&#123;dominant_emotion&#125;</span>: <span class="hljs-subst">&#123;confidence:<span class="hljs-number">.2</span>f&#125;</span>&quot;</span><br>                cv2.putText(frame, text, (x, y - <span class="hljs-number">10</span>), cv2.FONT_HERSHEY_SIMPLEX, <br>                           <span class="hljs-number">0.9</span>, (<span class="hljs-number">0</span>, <span class="hljs-number">255</span>, <span class="hljs-number">0</span>), <span class="hljs-number">2</span>)<br>            <br>            <span class="hljs-comment"># 显示统计信息</span><br>            <span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(<span class="hljs-variable language_">self</span>.emotion_history) &gt; <span class="hljs-number">0</span>:<br>                <span class="hljs-variable language_">self</span>.display_statistics(frame)<br>            <br>            cv2.imshow(<span class="hljs-string">&#x27;增强版情绪检测系统&#x27;</span>, frame)<br>            <br>            <span class="hljs-keyword">if</span> cv2.waitKey(<span class="hljs-number">1</span>) &amp; <span class="hljs-number">0xFF</span> == <span class="hljs-built_in">ord</span>(<span class="hljs-string">&#x27;q&#x27;</span>):<br>                <span class="hljs-keyword">break</span><br>                <br>        <span class="hljs-variable language_">self</span>.cleanup()<br>        <br>    <span class="hljs-keyword">def</span> <span class="hljs-title function_">display_statistics</span>(<span class="hljs-params">self, frame</span>):<br>        <span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(<span class="hljs-variable language_">self</span>.emotion_history) == <span class="hljs-number">0</span>:<br>            <span class="hljs-keyword">return</span><br>            <br>        <span class="hljs-comment"># 计算情绪分布</span><br>        emotion_counts = &#123;emotion: <span class="hljs-number">0</span> <span class="hljs-keyword">for</span> emotion <span class="hljs-keyword">in</span> <span class="hljs-variable language_">self</span>.emotions_list&#125;<br>        <span class="hljs-keyword">for</span> emotion <span class="hljs-keyword">in</span> <span class="hljs-variable language_">self</span>.emotion_history:<br>            emotion_counts[emotion] += <span class="hljs-number">1</span><br>            <br>        total = <span class="hljs-built_in">len</span>(<span class="hljs-variable language_">self</span>.emotion_history)<br>        dominant_overall = <span class="hljs-built_in">max</span>(emotion_counts, key=emotion_counts.get)<br>        <br>        <span class="hljs-comment"># 在图像上显示统计信息</span><br>        cv2.putText(frame, <span class="hljs-string">f&quot;主导情绪：<span class="hljs-subst">&#123;dominant_overall&#125;</span>&quot;</span>, (<span class="hljs-number">10</span>, <span class="hljs-number">30</span>), <br>                   cv2.FONT_HERSHEY_SIMPLEX, <span class="hljs-number">0.7</span>, (<span class="hljs-number">255</span>, <span class="hljs-number">255</span>, <span class="hljs-number">255</span>), <span class="hljs-number">2</span>)<br>        cv2.putText(frame, <span class="hljs-string">f&quot;检测次数：<span class="hljs-subst">&#123;total&#125;</span>&quot;</span>, (<span class="hljs-number">10</span>, <span class="hljs-number">60</span>), <br>                   cv2.FONT_HERSHEY_SIMPLEX, <span class="hljs-number">0.7</span>, (<span class="hljs-number">255</span>, <span class="hljs-number">255</span>, <span class="hljs-number">255</span>), <span class="hljs-number">2</span>)<br>        <br>    <span class="hljs-keyword">def</span> <span class="hljs-title function_">cleanup</span>(<span class="hljs-params">self</span>):<br>        <span class="hljs-variable language_">self</span>.cap.release()<br>        cv2.destroyAllWindows()<br><br><span class="hljs-comment"># 运行增强版情绪检测系统</span><br><span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">&quot;__main__&quot;</span>:<br>    detector = EnhancedEmotionDetector()<br>    detector.run()<br></code></pre></td></tr></table></figure><h2 id="视频文件情绪分析"><a href="#视频文件情绪分析" class="headerlink" title="视频文件情绪分析"></a>视频文件情绪分析</h2><p>除了实时摄像头检测，FER 库还支持对视频文件进行情绪分析：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">from</span> fer <span class="hljs-keyword">import</span> Video<br><span class="hljs-keyword">from</span> fer <span class="hljs-keyword">import</span> FER<br><br><span class="hljs-keyword">def</span> <span class="hljs-title function_">analyze_video_emotions</span>(<span class="hljs-params">video_filename</span>):<br>    <span class="hljs-comment"># 创建视频分析器</span><br>    video = Video(video_filename)<br>    <br>    <span class="hljs-comment"># 创建情绪检测器</span><br>    detector = FER(mtcnn=<span class="hljs-literal">True</span>)<br>    <br>    <span class="hljs-comment"># 分析视频</span><br>    raw_data = video.analyze(detector, display=<span class="hljs-literal">True</span>)<br>    <br>    <span class="hljs-comment"># 转换为 pandas DataFrame 便于分析</span><br>    df = video.to_pandas(raw_data)<br>    <br>    <span class="hljs-comment"># 保存结果到 CSV 文件</span><br>    df.to_csv(<span class="hljs-string">&quot;emotion_analysis.csv&quot;</span>, index=<span class="hljs-literal">False</span>)<br>    <br>    <span class="hljs-keyword">return</span> df<br><br><span class="hljs-comment"># 使用示例</span><br><span class="hljs-comment"># df = analyze_video_emotions(&quot;sample_video.mp4&quot;)</span><br></code></pre></td></tr></table></figure><h2 id="应用场景"><a href="#应用场景" class="headerlink" title="应用场景"></a>应用场景</h2><p>实时情绪检测系统在多个领域都有广泛的应用前景：</p><h3 id="1-人机交互"><a href="#1-人机交互" class="headerlink" title="1. 人机交互"></a>1. 人机交互</h3><p>通过实时识别用户的面部表情，调整应用程序的行为或提供个性化反馈。</p><h3 id="2-市场营销"><a href="#2-市场营销" class="headerlink" title="2. 市场营销"></a>2. 市场营销</h3><p>了解顾客对商品或广告的真实反应，优化营销策略。</p><h3 id="3-智能驾驶"><a href="#3-智能驾驶" class="headerlink" title="3. 智能驾驶"></a>3. 智能驾驶</h3><p>监控驾驶员的疲劳和情绪状态，提高驾驶安全性。</p><h3 id="4-娱乐产业"><a href="#4-娱乐产业" class="headerlink" title="4. 娱乐产业"></a>4. 娱乐产业</h3><p>创造更具沉浸感的互动体验，根据用户表情调整游戏或影视内容。</p><h2 id="注意事项和局限性"><a href="#注意事项和局限性" class="headerlink" title="注意事项和局限性"></a>注意事项和局限性</h2><p>尽管 FER 库提供了便捷的情绪识别功能，但在实际应用中仍需注意以下几点：</p><ol><li><p><strong>准确率限制</strong>：默认模型在 FER2013 数据集上的准确率约为 65%，对某些人群（如亚洲面孔、儿童）可能存在偏差。</p></li><li><p><strong>光照和环境影响</strong>：在光线不足或变化剧烈的环境中，识别准确率会下降。</p></li><li><p><strong>遮挡问题</strong>：当人脸被口罩、手等物体遮挡时，可能无法正确识别。</p></li><li><p><strong>隐私考虑</strong>：在实际部署时需要考虑用户隐私保护问题。</p></li></ol><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>通过 FER 库，我们可以快速构建功能强大的实时情绪检测系统。该库提供了简单易用的 API，使得开发者能够专注于业务逻辑而不是底层实现细节。</p><p>虽然默认模型有一定的局限性，但对于原型开发和一般应用场景已经足够。如果需要更高的准确率，可以考虑使用自定义数据集重新训练模型。</p><p>情绪识别技术正处于快速发展阶段，随着深度学习技术的进步，未来的系统将更加准确和可靠。通过本文介绍的方法，你可以快速搭建自己的情绪检测系统，并在此基础上进行进一步的优化和扩展。</p>]]>
    </content>
    <id>https://bubao.github.io/posts/8ae7e6fb.html</id>
    <link href="https://bubao.github.io/posts/8ae7e6fb.html"/>
    <published>2025-12-06T16:00:00.000Z</published>
    <summary>
      <![CDATA[<p>情绪识别技术在现代社会中变得越来越重要，它在人机交互、智能监控、教育评估等多个领域都有广泛应用。本文将介绍如何使用 FER (Facial Expression Recognition) 库构建一个实时情绪检测系统，通过摄像头实时捕捉和分析人脸情绪。</p>]]>
    </summary>
    <title>使用 FER 库构建实时情绪检测系统</title>
    <updated>2026-10-31T20:37:58.474Z</updated>
  </entry>
  <entry>
    <author>
      <name>一念</name>
    </author>
    <category term="教程" scheme="https://bubao.github.io/categories/%E6%95%99%E7%A8%8B/"/>
    <category term="node.js" scheme="https://bubao.github.io/tags/node-js/"/>
    <category term="npm" scheme="https://bubao.github.io/tags/npm/"/>
    <content>
      <![CDATA[<p>在现代前端和 Node.js 开发中，我们经常会遇到需要在同一项目中使用某个包的多个版本的情况。这可能是由于项目迁移、兼容性要求或者第三方依赖的不同版本需求等原因造成的。本文将介绍如何在 Node.js 项目中同时安装和使用同一个包的不同版本。</p><span id="more"></span><h2 id="场景介绍"><a href="#场景介绍" class="headerlink" title="场景介绍"></a>场景介绍</h2><p>假设我们有一个项目需要同时与多个不同版本的 Socket.IO 服务器进行通信。Socket.IO 在 v2、v3 和 v4 版本之间存在一些不兼容的变化，如果我们需要连接到这些不同版本的服务器，就需要在项目中同时使用这些版本的客户端。</p><p>传统方式下，package.json 中的 dependencies 字段只允许每个包出现一次，所以我们不能直接这样写：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs json"><span class="hljs-punctuation">&#123;</span><br>  <span class="hljs-attr">&quot;dependencies&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span><br>    <span class="hljs-attr">&quot;socket.io-client&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;^2.0.0&quot;</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;socket.io-client&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;^3.0.0&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-comment">// 这会覆盖上面的版本</span><br>    <span class="hljs-attr">&quot;socket.io-client&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;^4.0.0&quot;</span>  <span class="hljs-comment">// 这也会覆盖上面的版本</span><br>  <span class="hljs-punctuation">&#125;</span><br><span class="hljs-punctuation">&#125;</span><br></code></pre></td></tr></table></figure><h2 id="使用-npm-的-alias-功能"><a href="#使用-npm-的-alias-功能" class="headerlink" title="使用 npm 的 alias 功能"></a>使用 npm 的 alias 功能</h2><p>npm 从 6.9.0 版本开始支持别名功能，允许我们将同一个包的不同版本安装为不同的名称。语法如下：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">npm install &lt;<span class="hljs-built_in">alias</span>&gt;@npm:&lt;package-name&gt;@&lt;version&gt;<br></code></pre></td></tr></table></figure><h3 id="实际示例"><a href="#实际示例" class="headerlink" title="实际示例"></a>实际示例</h3><p>针对我们的 Socket.IO 客户端需求，可以执行以下命令：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs bash">npm install socket.io-client-2@npm:socket.io-client@2<br>npm install socket.io-client-3@npm:socket.io-client@3<br>npm install socket.io-client-4@npm:socket.io-client@4<br></code></pre></td></tr></table></figure><p>执行这些命令后，package.json 中会生成如下依赖：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs json"><span class="hljs-punctuation">&#123;</span><br>  <span class="hljs-attr">&quot;dependencies&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span><br>    <span class="hljs-attr">&quot;socket.io-client-2&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;npm:socket.io-client@^2.5.0&quot;</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;socket.io-client-3&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;npm:socket.io-client@^3.1.3&quot;</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;socket.io-client-4&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;npm:socket.io-client@^4.7.2&quot;</span><br>  <span class="hljs-punctuation">&#125;</span><br><span class="hljs-punctuation">&#125;</span><br></code></pre></td></tr></table></figure><p>同时，node_modules 目录结构会类似于：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs tree">node_modules/<br>├── socket.io-client-2/<br>│   └── node_modules/<br>│       └── socket.io-client/ (v2.x)<br>├── socket.io-client-3/<br>│   └── node_modules/<br>│       └── socket.io-client/ (v3.x)<br>└── socket.io-client-4/<br>    └── node_modules/<br>        └── socket.io-client/ (v4.x)<br></code></pre></td></tr></table></figure><h2 id="在代码中使用不同版本"><a href="#在代码中使用不同版本" class="headerlink" title="在代码中使用不同版本"></a>在代码中使用不同版本</h2><p>安装完成后，我们就可以在代码中分别引入这些不同版本的包：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><code class="hljs javascript"><span class="hljs-comment">// 引入不同版本的 socket.io-client</span><br><span class="hljs-keyword">const</span> io_v2 = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;socket.io-client-2&#x27;</span>);<br><span class="hljs-keyword">const</span> io_v3 = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;socket.io-client-3&#x27;</span>);<br><span class="hljs-keyword">const</span> io_v4 = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;socket.io-client-4&#x27;</span>);<br><br><span class="hljs-comment">// 或者使用 ES6 imports</span><br><span class="hljs-keyword">import</span> io_v2 <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;socket.io-client-2&#x27;</span>;<br><span class="hljs-keyword">import</span> io_v3 <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;socket.io-client-3&#x27;</span>;<br><span class="hljs-keyword">import</span> io_v4 <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;socket.io-client-4&#x27;</span>;<br><br><span class="hljs-comment">// 分别连接到不同版本的服务器</span><br><span class="hljs-keyword">const</span> socket_v2 = <span class="hljs-title function_">io_v2</span>(<span class="hljs-string">&#x27;http://localhost:3000&#x27;</span>);<br><span class="hljs-keyword">const</span> socket_v3 = <span class="hljs-title function_">io_v3</span>(<span class="hljs-string">&#x27;http://localhost:3001&#x27;</span>);<br><span class="hljs-keyword">const</span> socket_v4 = <span class="hljs-title function_">io_v4</span>(<span class="hljs-string">&#x27;http://localhost:3002&#x27;</span>);<br><br><span class="hljs-comment">// 为不同版本的 socket 设置不同的事件监听器</span><br>socket_v2.<span class="hljs-title function_">on</span>(<span class="hljs-string">&#x27;connect&#x27;</span>, <span class="hljs-function">() =&gt;</span> &#123;<br>  <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-string">&#x27;Connected to Socket.IO v2 server&#x27;</span>);<br>&#125;);<br><br>socket_v3.<span class="hljs-title function_">on</span>(<span class="hljs-string">&#x27;connect&#x27;</span>, <span class="hljs-function">() =&gt;</span> &#123;<br>  <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-string">&#x27;Connected to Socket.IO v3 server&#x27;</span>);<br>&#125;);<br><br>socket_v4.<span class="hljs-title function_">on</span>(<span class="hljs-string">&#x27;connect&#x27;</span>, <span class="hljs-function">() =&gt;</span> &#123;<br>  <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-string">&#x27;Connected to Socket.IO v4 server&#x27;</span>);<br>&#125;);<br></code></pre></td></tr></table></figure><h2 id="yarn-和-pnpm-的类似功能"><a href="#yarn-和-pnpm-的类似功能" class="headerlink" title="yarn 和 pnpm 的类似功能"></a>yarn 和 pnpm 的类似功能</h2><p>除了 npm，yarn 和 pnpm 也提供了类似的别名功能：</p><h3 id="Yarn"><a href="#Yarn" class="headerlink" title="Yarn"></a>Yarn</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs bash">yarn add socket.io-client-2@npm:socket.io-client@2<br>yarn add socket.io-client-3@npm:socket.io-client@3<br>yarn add socket.io-client-4@npm:socket.io-client@4<br></code></pre></td></tr></table></figure><p>或者使用 yarn 的别名语法：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs bash">yarn add socket.io-client-2@2<br>yarn add socket.io-client-3@3<br>yarn add socket.io-client-4@4<br></code></pre></td></tr></table></figure><h3 id="pnpm"><a href="#pnpm" class="headerlink" title="pnpm"></a>pnpm</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs bash">pnpm add socket.io-client-2@npm:socket.io-client@2<br>pnpm add socket.io-client-3@npm:socket.io-client@3<br>pnpm add socket.io-client-4@npm:socket.io-client@4<br></code></pre></td></tr></table></figure><h2 id="实际应用场景"><a href="#实际应用场景" class="headerlink" title="实际应用场景"></a>实际应用场景</h2><h3 id="1-系统迁移过渡期"><a href="#1-系统迁移过渡期" class="headerlink" title="1. 系统迁移过渡期"></a>1. 系统迁移过渡期</h3><p>当你的系统需要从一个版本迁移到另一个版本时，可以同时维护两个版本的客户端，逐步迁移用户：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><code class="hljs javascript"><span class="hljs-comment">// 根据用户配置或环境变量决定使用哪个版本</span><br><span class="hljs-keyword">const</span> <span class="hljs-title function_">getClientByVersion</span> = (<span class="hljs-params">version</span>) =&gt; &#123;<br>  <span class="hljs-keyword">switch</span>(version) &#123;<br>    <span class="hljs-keyword">case</span> <span class="hljs-string">&#x27;v2&#x27;</span>:<br>      <span class="hljs-keyword">return</span> <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;socket.io-client-2&#x27;</span>);<br>    <span class="hljs-keyword">case</span> <span class="hljs-string">&#x27;v3&#x27;</span>:<br>      <span class="hljs-keyword">return</span> <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;socket.io-client-3&#x27;</span>);<br>    <span class="hljs-keyword">case</span> <span class="hljs-string">&#x27;v4&#x27;</span>:<br>      <span class="hljs-keyword">return</span> <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;socket.io-client-4&#x27;</span>);<br>    <span class="hljs-attr">default</span>:<br>      <span class="hljs-keyword">return</span> <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;socket.io-client-4&#x27;</span>); <span class="hljs-comment">// 默认使用最新版本</span><br>  &#125;<br>&#125;;<br><br><span class="hljs-keyword">const</span> io = <span class="hljs-title function_">getClientByVersion</span>(process.<span class="hljs-property">env</span>.<span class="hljs-property">SOCKET_VERSION</span>);<br></code></pre></td></tr></table></figure><h3 id="2-第三方服务集成"><a href="#2-第三方服务集成" class="headerlink" title="2. 第三方服务集成"></a>2. 第三方服务集成</h3><p>当你需要集成多个第三方服务，而这些服务使用了同一个库的不同版本时：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs javascript"><span class="hljs-comment">// 集成服务 A（使用 Socket.IO v2）</span><br><span class="hljs-keyword">const</span> serviceA = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;./services/serviceA&#x27;</span>);<br><span class="hljs-comment">// serviceA 内部使用 socket.io-client-2</span><br><br><span class="hljs-comment">// 集成服务 B（使用 Socket.IO v3）</span><br><span class="hljs-keyword">const</span> serviceB = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;./services/serviceB&#x27;</span>);<br><span class="hljs-comment">// serviceB 内部使用 socket.io-client-3</span><br><br><span class="hljs-comment">// 集成服务 C（使用 Socket.IO v4）</span><br><span class="hljs-keyword">const</span> serviceC = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;./services/serviceC&#x27;</span>);<br><span class="hljs-comment">// serviceC 内部使用 socket.io-client-4</span><br></code></pre></td></tr></table></figure><h3 id="3-微前端架构"><a href="#3-微前端架构" class="headerlink" title="3. 微前端架构"></a>3. 微前端架构</h3><p>在微前端架构中，不同的子应用可能依赖同一个库的不同版本：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs javascript"><span class="hljs-comment">// 主应用</span><br><span class="hljs-keyword">const</span> mainAppSocket = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;socket.io-client-4&#x27;</span>);<br><br><span class="hljs-comment">// 子应用 A</span><br><span class="hljs-keyword">const</span> subAppASocket = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;socket.io-client-2&#x27;</span>);<br><br><span class="hljs-comment">// 子应用 B</span><br><span class="hljs-keyword">const</span> subAppBSocket = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;socket.io-client-3&#x27;</span>);<br></code></pre></td></tr></table></figure><h2 id="最佳实践"><a href="#最佳实践" class="headerlink" title="最佳实践"></a>最佳实践</h2><h3 id="1-明确命名约定"><a href="#1-明确命名约定" class="headerlink" title="1. 明确命名约定"></a>1. 明确命名约定</h3><p>使用清晰的命名约定来区分不同版本：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 推荐</span><br>npm install socket.io-v2@npm:socket.io@2<br>npm install socket.io-v3@npm:socket.io@3<br><br><span class="hljs-comment"># 不推荐</span><br>npm install socv2@npm:socket.io@2<br>npm install socv3@npm:socket.io@3<br></code></pre></td></tr></table></figure><h3 id="2-封装版本差异"><a href="#2-封装版本差异" class="headerlink" title="2. 封装版本差异"></a>2. 封装版本差异</h3><p>创建一个统一的接口来封装不同版本之间的差异：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><code class="hljs javascript"><span class="hljs-keyword">class</span> <span class="hljs-title class_">SocketIOManager</span> &#123;<br>  <span class="hljs-title function_">constructor</span>(<span class="hljs-params">version</span>) &#123;<br>    <span class="hljs-variable language_">this</span>.<span class="hljs-property">version</span> = version;<br>    <span class="hljs-variable language_">this</span>.<span class="hljs-property">io</span> = <span class="hljs-variable language_">this</span>.<span class="hljs-title function_">_getClientByVersion</span>(version);<br>  &#125;<br>  <br>  <span class="hljs-title function_">_getClientByVersion</span>(<span class="hljs-params">version</span>) &#123;<br>    <span class="hljs-keyword">switch</span>(version) &#123;<br>      <span class="hljs-keyword">case</span> <span class="hljs-string">&#x27;v2&#x27;</span>:<br>        <span class="hljs-keyword">return</span> <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;socket.io-client-2&#x27;</span>);<br>      <span class="hljs-keyword">case</span> <span class="hljs-string">&#x27;v3&#x27;</span>:<br>        <span class="hljs-keyword">return</span> <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;socket.io-client-3&#x27;</span>);<br>      <span class="hljs-keyword">case</span> <span class="hljs-string">&#x27;v4&#x27;</span>:<br>        <span class="hljs-keyword">return</span> <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;socket.io-client-4&#x27;</span>);<br>      <span class="hljs-attr">default</span>:<br>        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Error</span>(<span class="hljs-string">`Unsupported version: <span class="hljs-subst">$&#123;version&#125;</span>`</span>);<br>    &#125;<br>  &#125;<br>  <br>  <span class="hljs-title function_">connect</span>(<span class="hljs-params">url, options = &#123;&#125;</span>) &#123;<br>    <span class="hljs-comment">// 统一的连接方法，处理不同版本间的差异</span><br>    <span class="hljs-keyword">if</span> (<span class="hljs-variable language_">this</span>.<span class="hljs-property">version</span> === <span class="hljs-string">&#x27;v2&#x27;</span>) &#123;<br>      <span class="hljs-comment">// v2 特定的处理逻辑</span><br>      <span class="hljs-keyword">return</span> <span class="hljs-variable language_">this</span>.<span class="hljs-title function_">io</span>(url, options);<br>    &#125; <span class="hljs-keyword">else</span> &#123;<br>      <span class="hljs-comment">// v3/v4 特定的处理逻辑</span><br>      <span class="hljs-keyword">return</span> <span class="hljs-variable language_">this</span>.<span class="hljs-title function_">io</span>(url, options);<br>    &#125;<br>  &#125;<br>&#125;<br></code></pre></td></tr></table></figure><h3 id="3-及时清理不需要的版本"><a href="#3-及时清理不需要的版本" class="headerlink" title="3. 及时清理不需要的版本"></a>3. 及时清理不需要的版本</h3><p>一旦不再需要某个版本，应及时从项目中移除：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">npm uninstall socket.io-client-2<br></code></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>通过 npm 的别名功能，我们可以轻松地在同一个 Node.js 项目中管理同一包的多个版本。这对于系统迁移、集成不同版本的第三方服务以及微前端架构等场景非常有用。</p><p>然而，我们也需要注意这种方法带来的额外复杂性和潜在问题，包括包体积增加、维护成本提高等。在实际使用中，应该权衡利弊，只在确实需要时才使用这个功能。</p><p>通过合理的命名约定和封装，我们可以最大限度地发挥这个功能的优势，同时减少其带来的负面影响。</p>]]>
    </content>
    <id>https://bubao.github.io/posts/3c6f826c.html</id>
    <link href="https://bubao.github.io/posts/3c6f826c.html"/>
    <published>2025-12-06T15:00:00.000Z</published>
    <summary>
      <![CDATA[<p>在现代前端和 Node.js 开发中，我们经常会遇到需要在同一项目中使用某个包的多个版本的情况。这可能是由于项目迁移、兼容性要求或者第三方依赖的不同版本需求等原因造成的。本文将介绍如何在 Node.js 项目中同时安装和使用同一个包的不同版本。</p>]]>
    </summary>
    <title>Node.js 项目中管理同一包的多个版本</title>
    <updated>2026-10-31T20:37:58.468Z</updated>
  </entry>
  <entry>
    <author>
      <name>一念</name>
    </author>
    <category term="教程" scheme="https://bubao.github.io/categories/%E6%95%99%E7%A8%8B/"/>
    <category term="Redis" scheme="https://bubao.github.io/tags/Redis/"/>
    <category term="Lua" scheme="https://bubao.github.io/tags/Lua/"/>
    <content>
      <![CDATA[<p>Redis 不仅是一个高性能的键值存储系统，还支持通过 Lua 脚本执行复杂的原子操作。通过在 Redis 中执行 Lua 脚本，我们可以将多个命令打包成一个原子操作，避免竞态条件，同时减少网络往返次数。本文将详细介绍 Redis 中执行 Lua 脚本的基本用法。</p><span id="more"></span><h2 id="为什么使用-Lua-脚本？"><a href="#为什么使用-Lua-脚本？" class="headerlink" title="为什么使用 Lua 脚本？"></a>为什么使用 Lua 脚本？</h2><p>在分布式系统中，经常需要将多个 Redis 命令组合执行，但在高并发环境下，这些操作可能会引发竞态条件。传统的解决方案如 Redis 事务（MULTI&#x2F;EXEC）虽然能保证原子性，但缺乏流程控制能力，而 Lua 脚本正好弥补了这一缺陷。</p><p>使用 Lua 脚本的主要优势包括：</p><ol><li><strong>原子性保证</strong>：整个脚本在 Redis 中以原子方式执行，执行过程中不会被其他命令打断</li><li><strong>减少网络开销</strong>：多个操作只需一次网络往返</li><li><strong>灵活的逻辑控制</strong>：支持条件判断、循环等复杂逻辑</li><li><strong>高性能</strong>：脚本在服务器端预编译，执行效率高</li></ol><h2 id="EVAL-命令基础用法"><a href="#EVAL-命令基础用法" class="headerlink" title="EVAL 命令基础用法"></a>EVAL 命令基础用法</h2><p>Redis 提供了 <a href="https://redis.io/docs/latest/commands/eval/">EVAL</a> 命令来执行 Lua 脚本，其语法如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs redis">EVAL script numkeys key [key ...] arg [arg ...]<br></code></pre></td></tr></table></figure><p>各参数含义：</p><ul><li><code>script</code>：要执行的 Lua 脚本代码</li><li><code>numkeys</code>：指定键名参数的数量</li><li><code>key [key ...]</code>：脚本中用到的 Redis 键名，通过<code>KEYS</code>数组在 Lua 中访问</li><li><code>arg [arg ...]</code>：附加参数，通过<code>ARGV</code>数组在 Lua 中访问</li></ul><h3 id="简单示例"><a href="#简单示例" class="headerlink" title="简单示例"></a>简单示例</h3><p>让我们从一个简单的例子开始：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">EVAL <span class="hljs-string">&quot;return &#x27;Hello, World!&#x27;&quot;</span> 0<br></code></pre></td></tr></table></figure><p>这个脚本不涉及任何 Redis 键，直接返回一个字符串。注意到 <a href="https://redis.io/docs/latest/commands/eval/">numkeys</a> 参数为 0，因为我们没有使用任何键。</p><h3 id="使用键和参数"><a href="#使用键和参数" class="headerlink" title="使用键和参数"></a>使用键和参数</h3><p>在脚本中访问 Redis 键和参数：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">EVAL <span class="hljs-string">&quot;return &#123;KEYS[1], KEYS[2], ARGV[1], ARGV[2]&#125;&quot;</span> 2 key1 key2 first second<br></code></pre></td></tr></table></figure><p>输出结果：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs redis">1) &quot;key1&quot;<br>2) &quot;key2&quot;<br>3) &quot;first&quot;<br>4) &quot;second&quot;<br></code></pre></td></tr></table></figure><p>在 Lua 脚本中，可以通过<code>KEYS</code>和<code>ARGV</code>两个全局数组分别访问键名和参数。</p><h3 id="Redis-命令调用"><a href="#Redis-命令调用" class="headerlink" title="Redis 命令调用"></a>Redis 命令调用</h3><p>在 Lua 脚本中，可以通过<code>redis.call()</code>和<code>redis.pcall()</code>函数调用 Redis 命令：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">EVAL <span class="hljs-string">&quot;return redis.call(&#x27;SET&#x27;, KEYS[1], ARGV[1])&quot;</span> 1 mykey <span class="hljs-string">&quot;Hello Redis&quot;</span><br></code></pre></td></tr></table></figure><p>这个脚本相当于执行了<code>SET mykey &quot;Hello Redis&quot;</code>命令。</p><p>另一个示例，获取键值：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">EVAL <span class="hljs-string">&quot;return redis.call(&#x27;GET&#x27;, KEYS[1])&quot;</span> 1 mykey<br></code></pre></td></tr></table></figure><h2 id="EVALSHA-命令优化"><a href="#EVALSHA-命令优化" class="headerlink" title="EVALSHA 命令优化"></a>EVALSHA 命令优化</h2><p>当脚本较长或需要频繁执行时，每次都通过 <a href="https://redis.io/docs/latest/commands/eval/">EVAL</a> 命令传输整个脚本会浪费网络带宽。Redis 提供了 <a href="https://redis.io/docs/latest/commands/evalsha/">EVALSHA</a> 命令，通过脚本的 SHA1 摘要来执行脚本。</p><p>首先使用<code>SCRIPT LOAD</code>命令加载脚本：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">SCRIPT LOAD <span class="hljs-string">&quot;return redis.call(&#x27;SET&#x27;, KEYS[1], ARGV[1])&quot;</span><br></code></pre></td></tr></table></figure><p>该命令返回脚本的 SHA1 摘要，例如：</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs txt">&quot;a6b4d2b0a70c7c7a8a8a8a8a8a8a8a8a8a8a8a8&quot;<br></code></pre></td></tr></table></figure><p>然后使用 <a href="https://redis.io/docs/latest/commands/evalsha/">EVALSHA</a> 执行脚本：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">EVALSHA a6b4d2b0a70c7c7a8a8a8a8a8a8a8a8a8a8a8a8 1 mykey <span class="hljs-string">&quot;Hello Redis&quot;</span><br></code></pre></td></tr></table></figure><p><a href="https://redis.io/docs/latest/commands/evalsha/">EVALSHA</a> 的优点：</p><ol><li>减少网络传输量</li><li>提升执行性能（脚本已在服务器端缓存）</li></ol><h2 id="实际应用场景"><a href="#实际应用场景" class="headerlink" title="实际应用场景"></a>实际应用场景</h2><h3 id="原子计数器操作"><a href="#原子计数器操作" class="headerlink" title="原子计数器操作"></a>原子计数器操作</h3><p>实现一个带有上限检查的计数器：</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs lua"><span class="hljs-keyword">local</span> current = redis.call(<span class="hljs-string">&#x27;GET&#x27;</span>, KEYS[<span class="hljs-number">1</span>])<br><span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> current <span class="hljs-keyword">then</span><br>    current = <span class="hljs-number">0</span><br><span class="hljs-keyword">end</span><br><br><span class="hljs-keyword">if</span> <span class="hljs-built_in">tonumber</span>(current) &lt; <span class="hljs-built_in">tonumber</span>(ARGV[<span class="hljs-number">1</span>]) <span class="hljs-keyword">then</span><br>    <span class="hljs-keyword">local</span> new_value = redis.call(<span class="hljs-string">&#x27;INCR&#x27;</span>, KEYS[<span class="hljs-number">1</span>])<br>    <span class="hljs-keyword">return</span> &#123;<span class="hljs-number">1</span>, new_value&#125;  <span class="hljs-comment">-- 成功，返回状态和新值</span><br><span class="hljs-keyword">else</span><br>    <span class="hljs-keyword">return</span> &#123;<span class="hljs-number">0</span>, current&#125;    <span class="hljs-comment">-- 达到上限，返回状态和当前值</span><br><span class="hljs-keyword">end</span><br></code></pre></td></tr></table></figure><p>执行脚本：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">EVAL <span class="hljs-string">&quot;-- Lua script here --&quot;</span> 1 counter 10<br></code></pre></td></tr></table></figure><h3 id="分布式锁"><a href="#分布式锁" class="headerlink" title="分布式锁"></a>分布式锁</h3><p>实现一个简单的分布式锁：</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs lua"><span class="hljs-comment">-- 获取锁</span><br><span class="hljs-keyword">if</span> redis.call(<span class="hljs-string">&#x27;SETNX&#x27;</span>, KEYS[<span class="hljs-number">1</span>], ARGV[<span class="hljs-number">1</span>]) == <span class="hljs-number">1</span> <span class="hljs-keyword">then</span><br>    redis.call(<span class="hljs-string">&#x27;EXPIRE&#x27;</span>, KEYS[<span class="hljs-number">1</span>], ARGV[<span class="hljs-number">2</span>])<br>    <span class="hljs-keyword">return</span> <span class="hljs-number">1</span><br><span class="hljs-keyword">else</span><br>    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span><br><span class="hljs-keyword">end</span><br></code></pre></td></tr></table></figure><p>执行脚本：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">EVAL <span class="hljs-string">&quot;-- Lua script here --&quot;</span> 1 my_lock unique_value 30<br></code></pre></td></tr></table></figure><p>释放锁时也要确保只能由加锁的客户端释放：</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs lua"><span class="hljs-comment">-- 释放锁</span><br><span class="hljs-keyword">if</span> redis.call(<span class="hljs-string">&#x27;GET&#x27;</span>, KEYS[<span class="hljs-number">1</span>]) == ARGV[<span class="hljs-number">1</span>] <span class="hljs-keyword">then</span><br>    <span class="hljs-keyword">return</span> redis.call(<span class="hljs-string">&#x27;DEL&#x27;</span>, KEYS[<span class="hljs-number">1</span>])<br><span class="hljs-keyword">else</span><br>    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span><br><span class="hljs-keyword">end</span><br></code></pre></td></tr></table></figure><h3 id="限流器实现"><a href="#限流器实现" class="headerlink" title="限流器实现"></a>限流器实现</h3><p>实现一个基于时间窗口的限流器：</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><code class="hljs lua"><span class="hljs-keyword">local</span> key = KEYS[<span class="hljs-number">1</span>]<br><span class="hljs-keyword">local</span> limit = <span class="hljs-built_in">tonumber</span>(ARGV[<span class="hljs-number">1</span>])<br><span class="hljs-keyword">local</span> window = <span class="hljs-built_in">tonumber</span>(ARGV[<span class="hljs-number">2</span>])<br><span class="hljs-keyword">local</span> current_time = <span class="hljs-built_in">tonumber</span>(ARGV[<span class="hljs-number">3</span>])<br><br><span class="hljs-comment">-- 移除时间窗口外的记录</span><br>redis.call(<span class="hljs-string">&#x27;ZREMRANGEBYSCORE&#x27;</span>, key, <span class="hljs-number">0</span>, current_time - window)<br><br><span class="hljs-comment">-- 检查当前请求数</span><br><span class="hljs-keyword">local</span> current_requests = redis.call(<span class="hljs-string">&#x27;ZCARD&#x27;</span>, key)<br><br><span class="hljs-keyword">if</span> current_requests &lt; limit <span class="hljs-keyword">then</span><br>    <span class="hljs-comment">-- 添加当前请求</span><br>    redis.call(<span class="hljs-string">&#x27;ZADD&#x27;</span>, key, current_time, current_time)<br>    <span class="hljs-comment">-- 设置过期时间</span><br>    redis.call(<span class="hljs-string">&#x27;EXPIRE&#x27;</span>, key, window)<br>    <span class="hljs-keyword">return</span> <span class="hljs-number">1</span>  <span class="hljs-comment">-- 允许请求</span><br><span class="hljs-keyword">else</span><br>    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>  <span class="hljs-comment">-- 拒绝请求</span><br><span class="hljs-keyword">end</span><br></code></pre></td></tr></table></figure><h2 id="错误处理"><a href="#错误处理" class="headerlink" title="错误处理"></a>错误处理</h2><p>在 Lua 脚本中，有两种调用 Redis 命令的方式：</p><ol><li><code>redis.call()</code>：执行命令，出错时抛出异常并停止脚本执行</li><li><code>redis.pcall()</code>：执行命令，出错时返回描述错误的表</li></ol><p>使用<code>redis.pcall()</code>的安全示例：</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs lua"><span class="hljs-keyword">local</span> result = redis.<span class="hljs-built_in">pcall</span>(<span class="hljs-string">&#x27;GET&#x27;</span>, KEYS[<span class="hljs-number">1</span>])<br><span class="hljs-keyword">if</span> <span class="hljs-built_in">type</span>(result) == <span class="hljs-string">&#x27;table&#x27;</span> <span class="hljs-keyword">and</span> result.err <span class="hljs-keyword">then</span><br>    <span class="hljs-comment">-- 处理错误</span><br>    <span class="hljs-keyword">return</span> <span class="hljs-string">&#x27;Error: &#x27;</span> .. result.err<br><span class="hljs-keyword">else</span><br>    <span class="hljs-comment">-- 处理正常结果</span><br>    <span class="hljs-keyword">return</span> result<br><span class="hljs-keyword">end</span><br></code></pre></td></tr></table></figure><h2 id="最佳实践"><a href="#最佳实践" class="headerlink" title="最佳实践"></a>最佳实践</h2><h3 id="1-保持脚本简洁"><a href="#1-保持脚本简洁" class="headerlink" title="1. 保持脚本简洁"></a>1. 保持脚本简洁</h3><p>Lua 脚本应在 Redis 主线程中执行，长时间运行会阻塞其他命令。应避免在脚本中进行复杂计算或大循环操作。</p><h3 id="2-合理使用键"><a href="#2-合理使用键" class="headerlink" title="2. 合理使用键"></a>2. 合理使用键</h3><p>在集群模式下，确保所有键都在同一个 hash slot 中，可以使用 hash tags：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">EVAL <span class="hljs-string">&quot;return redis.call(&#x27;GET&#x27;, KEYS[1])&quot;</span> 1 &#123;user1000&#125;.balance<br></code></pre></td></tr></table></figure><h3 id="3-使用-EVALSHA-优化性能"><a href="#3-使用-EVALSHA-优化性能" class="headerlink" title="3. 使用 EVALSHA 优化性能"></a>3. 使用 EVALSHA 优化性能</h3><p>对于频繁执行的脚本，使用<code>SCRIPT LOAD</code>加载后通过 <a href="https://redis.io/docs/latest/commands/evalsha/">EVALSHA</a> 执行。</p><h3 id="4-脚本可读性和维护性"><a href="#4-脚本可读性和维护性" class="headerlink" title="4. 脚本可读性和维护性"></a>4. 脚本可读性和维护性</h3><p>为复杂脚本添加注释，合理命名变量，避免过于复杂的逻辑。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>Redis 的 Lua 脚本功能为我们提供了一种强大而灵活的方式来执行原子操作。通过 <a href="https://redis.io/docs/latest/commands/eval/">EVAL</a> 和 <a href="https://redis.io/docs/latest/commands/evalsha/">EVALSHA</a> 命令，我们可以在 Redis 中执行复杂的逻辑，同时保证操作的原子性和数据一致性。</p><p>合理使用 Lua 脚本可以极大地提升 Redis 应用的性能和可靠性，特别是在需要处理并发和复杂业务逻辑的场景中。但也要注意避免编写过于复杂的脚本，以免影响 Redis 的整体性能。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ol><li><a href="https://redis.io/topics/lua">Redis 官方 Lua 脚本文档</a></li><li><a href="https://redis.io/commands">Redis 命令参考</a></li><li><a href="https://www.lua.org/manual/5.1/">Lua 5.1 参考手册</a></li><li><a href="https://redis.io/commands/script-load">Redis 脚本命令详解</a></li></ol>]]>
    </content>
    <id>https://bubao.github.io/posts/fc1040f0.html</id>
    <link href="https://bubao.github.io/posts/fc1040f0.html"/>
    <published>2025-12-06T14:00:00.000Z</published>
    <summary>
      <![CDATA[<p>Redis 不仅是一个高性能的键值存储系统，还支持通过 Lua 脚本执行复杂的原子操作。通过在 Redis 中执行 Lua 脚本，我们可以将多个命令打包成一个原子操作，避免竞态条件，同时减少网络往返次数。本文将详细介绍 Redis 中执行 Lua 脚本的基本用法。</p>]]>
    </summary>
    <title>Redis 执行 Lua 脚本基本用法</title>
    <updated>2026-10-31T20:37:58.469Z</updated>
  </entry>
  <entry>
    <author>
      <name>一念</name>
    </author>
    <category term="教程" scheme="https://bubao.github.io/categories/%E6%95%99%E7%A8%8B/"/>
    <category term="Lua" scheme="https://bubao.github.io/tags/Lua/"/>
    <content>
      <![CDATA[<p>在现代软件开发中，将脚本语言与宿主语言相结合已经成为一种常见的做法。Lua 作为一种轻量级、高效的脚本语言，广泛应用于游戏开发、嵌入式系统和其他需要高度可配置性的场景中。本文将深入探讨 Lua 与 C 语言之间的交互机制，详细解析 Lua C API 的各个接口用途，帮助开发者更好地掌握这一强大的功能。</p><span id="more"></span><h2 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h2><p>Lua 本身是用 C 语言编写的，因此它与 C 语言有着天然的亲和力。Lua C API 是连接 Lua 脚本与 C 程序的桥梁，通过这套 API，我们可以在 C 程序中执行 Lua 脚本，也可以在 Lua 脚本中调用 C 函数。理解这套 API 的工作机制对于开发高效、稳定的 Lua 应用至关重要。</p><h2 id="Lua-栈机制"><a href="#Lua-栈机制" class="headerlink" title="Lua 栈机制"></a>Lua 栈机制</h2><p>在深入了解 Lua C API 之前，我们首先要理解 Lua 的核心概念——栈（Stack）。几乎所有 Lua C API 的操作都围绕着这个栈进行。</p><p>Lua 栈是一个虚拟栈，它是 Lua 状态的一部分，用于在 C 代码和 Lua 脚本之间传递值。当我们在 C 代码中调用 Lua 函数，或将值从 C 传递给 Lua 时，都需要通过这个栈来完成。</p><h3 id="栈的基本操作"><a href="#栈的基本操作" class="headerlink" title="栈的基本操作"></a>栈的基本操作</h3><ol><li><p><strong>索引规则</strong>：栈中的元素可以通过索引来访问，索引可以是正数也可以是负数</p><ul><li>正数索引：从栈底开始计数（1 为栈底）</li><li>负数索引：从栈顶开始计数（-1 为栈顶）</li></ul></li><li><p><strong>栈操作函数</strong>：</p><ul><li><code>lua_gettop</code>：获取栈顶元素的索引（即栈中元素的数量）</li><li><code>lua_settop</code>：设置栈顶位置，可以用来增加或减少栈中元素</li><li><code>lua_pushvalue</code>：将指定索引的元素副本压入栈顶</li><li><code>lua_remove</code>：删除指定索引的元素，其上的元素下移</li><li><code>lua_insert</code>：将栈顶元素插入到指定位置</li><li><code>lua_replace</code>：将栈顶元素替换到指定位置，并弹出栈顶</li></ul></li></ol><h2 id="Lua-C-API-核心组件"><a href="#Lua-C-API-核心组件" class="headerlink" title="Lua C API 核心组件"></a>Lua C API 核心组件</h2><p>Lua C API 主要由三个头文件组成：</p><h3 id="1-lua-h-基础-API"><a href="#1-lua-h-基础-API" class="headerlink" title="1. lua.h - 基础 API"></a>1. lua.h - 基础 API</h3><p><code>lua.h</code> 定义了 Lua 提供的基础函数，是与 Lua 交互的核心接口。</p><h4 id="环境管理函数"><a href="#环境管理函数" class="headerlink" title="环境管理函数"></a>环境管理函数</h4><ul><li><code>lua_newstate</code>：创建一个新的 Lua 环境</li><li><code>lua_close</code>：关闭 Lua 环境并释放相关资源</li><li><code>lua_version</code>：获取 Lua 版本信息</li></ul><h4 id="栈操作函数"><a href="#栈操作函数" class="headerlink" title="栈操作函数"></a>栈操作函数</h4><ul><li><code>lua_absindex</code>：将相对索引转换为绝对索引</li><li><code>lua_gettop</code>：返回栈中元素的数量</li><li><code>lua_settop</code>：设置栈顶位置</li><li><code>lua_pushvalue</code>：将指定索引的元素副本压入栈顶</li><li><code>lua_rotate</code>：旋转栈中元素</li><li><code>lua_copy</code>：从一个索引复制元素到另一个索引</li><li><code>lua_checkstack</code>：确保栈有足够的空间</li></ul><h4 id="栈元素查询函数"><a href="#栈元素查询函数" class="headerlink" title="栈元素查询函数"></a>栈元素查询函数</h4><ul><li><code>lua_isnumber</code>：检查元素是否为数字</li><li><code>lua_isstring</code>：检查元素是否为字符串</li><li><code>lua_iscfunction</code>：检查元素是否为 C 函数</li><li><code>lua_isinteger</code>：检查元素是否为整数</li><li><code>lua_isuserdata</code>：检查元素是否为用户数据</li><li><code>lua_type</code>：返回元素的类型</li><li><code>lua_typename</code>：返回类型的名称</li></ul><h4 id="栈元素转换函数"><a href="#栈元素转换函数" class="headerlink" title="栈元素转换函数"></a>栈元素转换函数</h4><ul><li><code>lua_tonumber</code>：将元素转换为数字</li><li><code>lua_tointeger</code>：将元素转换为整数</li><li><code>lua_toboolean</code>：将元素转换为布尔值</li><li><code>lua_tolstring</code>：将元素转换为字符串</li><li><code>lua_tocfunction</code>：将元素转换为 C 函数指针</li><li><code>lua_touserdata</code>：将元素转换为用户数据指针</li><li><code>lua_tothread</code>：将元素转换为线程（Lua 状态）</li><li><code>lua_topointer</code>：将元素转换为通用指针</li></ul><h4 id="算术运算函数"><a href="#算术运算函数" class="headerlink" title="算术运算函数"></a>算术运算函数</h4><ul><li><code>lua_arith</code>：执行基本的算术运算和位运算</li></ul><h4 id="比较函数"><a href="#比较函数" class="headerlink" title="比较函数"></a>比较函数</h4><ul><li><code>lua_rawequal</code>：直接比较两个值是否相等（不调用元方法）</li><li><code>lua_compare</code>：比较两个值（可调用元方法）</li></ul><h4 id="压栈函数"><a href="#压栈函数" class="headerlink" title="压栈函数"></a>压栈函数</h4><ul><li><code>lua_pushnil</code>：将 nil 压入栈</li><li><code>lua_pushnumber</code>：将数字压入栈</li><li><code>lua_pushinteger</code>：将整数压入栈</li><li><code>lua_pushlstring</code>：将指定长度的字符串压入栈</li><li><code>lua_pushstring</code>：将字符串压入栈</li><li><code>lua_pushvfstring</code>：将格式化字符串压入栈（类似 printf）</li><li><code>lua_pushfstring</code>：将格式化字符串压入栈（变参版本）</li><li><code>lua_pushcclosure</code>：将 C 函数作为闭包压入栈</li><li><code>lua_pushboolean</code>：将布尔值压入栈</li><li><code>lua_pushlightuserdata</code>：将轻量用户数据压入栈</li><li><code>lua_pushthread</code>：将线程压入栈</li></ul><h4 id="加载和调用函数"><a href="#加载和调用函数" class="headerlink" title="加载和调用函数"></a>加载和调用函数</h4><ul><li><code>lua_call</code>：调用 Lua 函数</li><li><code>lua_pcall</code>：保护模式下调用 Lua 函数</li><li><code>lua_load</code>：加载 Lua 代码块</li><li><code>lua_dump</code>：将函数导出为二进制形式</li></ul><h4 id="表操作函数"><a href="#表操作函数" class="headerlink" title="表操作函数"></a>表操作函数</h4><ul><li><code>lua_newtable</code>：创建一个新表</li><li><code>lua_getglobal</code>：获取全局变量</li><li><code>lua_gettable</code>：获取表中的元素</li><li><code>lua_getfield</code>：获取表中指定字段的值</li><li><code>lua_geti</code>：获取表中指定索引的值</li><li><code>lua_rawget</code>：直接获取表中的元素（不触发元方法）</li><li><code>lua_rawgeti</code>：直接获取表中指定索引的元素（不触发元方法）</li><li><code>lua_rawgetp</code>：通过指针作为键获取表中的元素</li><li><code>lua_createtable</code>：创建具有预分配空间的表</li><li><code>lua_getmetatable</code>：获取元素的元表</li><li><code>lua_getuservalue</code>：获取用户数据关联的值</li></ul><h4 id="表设置函数"><a href="#表设置函数" class="headerlink" title="表设置函数"></a>表设置函数</h4><ul><li><code>lua_setglobal</code>：设置全局变量</li><li><code>lua_settable</code>：设置表中的元素</li><li><code>lua_setfield</code>：设置表中指定字段的值</li><li><code>lua_seti</code>：设置表中指定索引的值</li><li><code>lua_rawset</code>：直接设置表中的元素（不触发元方法）</li><li><code>lua_rawseti</code>：直接设置表中指定索引的元素（不触发元方法）</li><li><code>lua_rawsetp</code>：通过指针作为键设置表中的元素</li><li><code>lua_setmetatable</code>：设置元素的元表</li><li><code>lua_setuservalue</code>：设置用户数据关联的值</li></ul><h4 id="元表操作函数"><a href="#元表操作函数" class="headerlink" title="元表操作函数"></a>元表操作函数</h4><ul><li><code>lua_getmetatable</code>：获取元素的元表</li><li><code>lua_setmetatable</code>：设置元素的元表</li></ul><h4 id="其他实用函数"><a href="#其他实用函数" class="headerlink" title="其他实用函数"></a>其他实用函数</h4><ul><li><code>lua_len</code>：获取对象的长度</li><li><code>lua_stringtonumber</code>：将字符串转换为数字</li><li><code>lua_gc</code>：控制垃圾回收器</li><li><code>lua_error</code>：抛出错误</li><li><code>lua_next</code>：遍历表</li><li><code>lua_concat</code>：连接栈顶的多个值</li><li><code>lua_getallocf</code>：获取内存分配函数</li><li><code>lua_setallocf</code>：设置内存分配函数</li></ul><h3 id="2-lualib-h-标准库"><a href="#2-lualib-h-标准库" class="headerlink" title="2. lualib.h - 标准库"></a>2. lualib.h - 标准库</h3><p><code>lualib.h</code> 声明了打开 Lua 标准库的函数。</p><ul><li><code>luaopen_base</code>：打开基础库</li><li><code>luaopen_coroutine</code>：打开协程库</li><li><code>luaopen_table</code>：打开表库</li><li><code>luaopen_io</code>：打开 IO 库</li><li><code>luaopen_os</code>：打开操作系统库</li><li><code>luaopen_string</code>：打开字符串库</li><li><code>luaopen_utf8</code>：打开 UTF-8 库</li><li><code>luaopen_math</code>：打开数学库</li><li><code>luaopen_debug</code>：打开调试库</li><li><code>luaopen_package</code>：打开包管理库</li><li><code>luaL_openlibs</code>：打开所有标准库</li></ul><h3 id="3-lauxlib-h-辅助库"><a href="#3-lauxlib-h-辅助库" class="headerlink" title="3. lauxlib.h - 辅助库"></a>3. lauxlib.h - 辅助库</h3><p><code>lauxlib.h</code> 提供了许多便利函数，简化了 Lua C API 的使用。</p><h4 id="状态创建和销毁"><a href="#状态创建和销毁" class="headerlink" title="状态创建和销毁"></a>状态创建和销毁</h4><ul><li><code>luaL_newstate</code>：创建新的 Lua 状态（推荐使用）</li><li><code>luaL_openlibs</code>：打开所有标准库</li></ul><h4 id="错误处理"><a href="#错误处理" class="headerlink" title="错误处理"></a>错误处理</h4><ul><li><code>luaL_checkoption</code>：检查选项字符串</li><li><code>luaL_argerror</code>：报告参数错误</li><li><code>luaL_checkstring</code>：检查并获取字符串参数</li><li><code>luaL_checknumber</code>：检查并获取数字参数</li><li><code>luaL_checkinteger</code>：检查并获取整数参数</li><li><code>luaL_checkstack</code>：检查栈空间</li><li><code>luaL_checktype</code>：检查参数类型</li><li><code>luaL_checkany</code>：检查是否存在参数</li><li><code>luaL_newmetatable</code>：创建新元表</li><li><code>luaL_setmetatable</code>：设置元表</li><li><code>luaL_testudata</code>：测试用户数据</li><li><code>luaL_checkudata</code>：检查用户数据</li><li><code>luaL_where</code>：将错误位置压入栈</li><li><code>luaL_error</code>：抛出错误</li><li><code>luaL_fileresult</code>：处理文件操作结果</li><li><code>luaL_execresult</code>：处理进程执行结果</li></ul><h4 id="加载和运行代码"><a href="#加载和运行代码" class="headerlink" title="加载和运行代码"></a>加载和运行代码</h4><ul><li><code>luaL_loadfile</code>：从文件加载 Lua 代码</li><li><code>luaL_loadbuffer</code>：从缓冲区加载 Lua 代码</li><li><code>luaL_loadstring</code>：从字符串加载 Lua 代码</li><li><code>luaL_dofile</code>：加载并执行文件</li><li><code>luaL_dostring</code>：加载并执行字符串</li></ul><h4 id="模块和函数注册"><a href="#模块和函数注册" class="headerlink" title="模块和函数注册"></a>模块和函数注册</h4><ul><li><code>luaL_newlibtable</code>：创建新库表</li><li><code>luaL_newlib</code>：创建新库</li><li><code>luaL_setfuncs</code>：设置函数列表</li><li><code>luaL_getsubtable</code>：获取子表</li><li><code>luaL_len</code>：获取对象长度</li><li><code>luaL_gsub</code>：字符串替换</li><li><code>luaL_setfuncs</code>：注册函数列表</li></ul><h4 id="用户数据操作"><a href="#用户数据操作" class="headerlink" title="用户数据操作"></a>用户数据操作</h4><ul><li><code>luaL_newmetatable</code>：创建新的元表</li><li><code>luaL_setmetatable</code>：设置元表</li><li><code>luaL_testudata</code>：测试用户数据</li><li><code>luaL_checkudata</code>：检查用户数据</li></ul><h2 id="实践示例"><a href="#实践示例" class="headerlink" title="实践示例"></a>实践示例</h2><p>为了更好地理解 Lua C API 的使用，让我们通过几个实际的例子来演示：</p><h3 id="1-简单的值传递"><a href="#1-简单的值传递" class="headerlink" title="1. 简单的值传递"></a>1. 简单的值传递</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&lt;stdio.h&gt;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;lua.h&quot;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;lualib.h&quot;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;lauxlib.h&quot;</span></span><br><br><span class="hljs-type">int</span> <span class="hljs-title function_">main</span><span class="hljs-params">()</span> &#123;<br>    lua_State *L = luaL_newstate();<br>    luaL_openlibs(L);<br>    <br>    <span class="hljs-comment">// 将数字压入栈</span><br>    lua_pushnumber(L, <span class="hljs-number">3.14159</span>);<br>    <span class="hljs-built_in">printf</span>(<span class="hljs-string">&quot;栈中元素数量：%d\n&quot;</span>, lua_gettop(L));<br>    <br>    <span class="hljs-comment">// 获取栈顶元素</span><br>    <span class="hljs-keyword">if</span> (lua_isnumber(L, <span class="hljs-number">-1</span>)) &#123;<br>        <span class="hljs-type">double</span> value = lua_tonumber(L, <span class="hljs-number">-1</span>);<br>        <span class="hljs-built_in">printf</span>(<span class="hljs-string">&quot;栈顶元素值：%f\n&quot;</span>, value);<br>    &#125;<br>    <br>    <span class="hljs-comment">// 弹出元素</span><br>    lua_pop(L, <span class="hljs-number">1</span>);<br>    <span class="hljs-built_in">printf</span>(<span class="hljs-string">&quot;弹出后栈中元素数量：%d\n&quot;</span>, lua_gettop(L));<br>    <br>    lua_close(L);<br>    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;<br>&#125;<br></code></pre></td></tr></table></figure><h3 id="2-执行-Lua-脚本"><a href="#2-执行-Lua-脚本" class="headerlink" title="2. 执行 Lua 脚本"></a>2. 执行 Lua 脚本</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&lt;stdio.h&gt;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;lua.h&quot;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;lualib.h&quot;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;lauxlib.h&quot;</span></span><br><br><span class="hljs-type">int</span> <span class="hljs-title function_">main</span><span class="hljs-params">()</span> &#123;<br>    lua_State *L = luaL_newstate();<br>    luaL_openlibs(L);<br>    <br>    <span class="hljs-comment">// 执行 Lua 代码</span><br>    <span class="hljs-type">const</span> <span class="hljs-type">char</span> *code = <span class="hljs-string">&quot;print(&#x27;Hello from Lua!&#x27;)\n&quot;</span><br>                       <span class="hljs-string">&quot;return 42&quot;</span>;<br>    <br>    <span class="hljs-keyword">if</span> (luaL_dostring(L, code) == LUA_OK) &#123;<br>        <span class="hljs-keyword">if</span> (lua_isnumber(L, <span class="hljs-number">-1</span>)) &#123;<br>            <span class="hljs-type">int</span> result = lua_tointeger(L, <span class="hljs-number">-1</span>);<br>            <span class="hljs-built_in">printf</span>(<span class="hljs-string">&quot;Lua 返回值：%d\n&quot;</span>, result);<br>            lua_pop(L, <span class="hljs-number">1</span>);  <span class="hljs-comment">// 弹出返回值</span><br>        &#125;<br>    &#125; <span class="hljs-keyword">else</span> &#123;<br>        <span class="hljs-comment">// 错误处理</span><br>        <span class="hljs-type">const</span> <span class="hljs-type">char</span> *error = lua_tostring(L, <span class="hljs-number">-1</span>);<br>        <span class="hljs-built_in">printf</span>(<span class="hljs-string">&quot;错误：%s\n&quot;</span>, error);<br>        lua_pop(L, <span class="hljs-number">1</span>);  <span class="hljs-comment">// 弹出错误信息</span><br>    &#125;<br>    <br>    lua_close(L);<br>    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;<br>&#125;<br></code></pre></td></tr></table></figure><h3 id="3-C-函数注册到-Lua"><a href="#3-C-函数注册到-Lua" class="headerlink" title="3. C 函数注册到 Lua"></a>3. C 函数注册到 Lua</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&lt;stdio.h&gt;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;lua.h&quot;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;lualib.h&quot;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;lauxlib.h&quot;</span></span><br><br><span class="hljs-comment">// C 函数示例</span><br><span class="hljs-type">static</span> <span class="hljs-type">int</span> <span class="hljs-title function_">c_add</span><span class="hljs-params">(lua_State *L)</span> &#123;<br>    <span class="hljs-comment">// 检查参数</span><br>    <span class="hljs-type">double</span> a = luaL_checknumber(L, <span class="hljs-number">1</span>);<br>    <span class="hljs-type">double</span> b = luaL_checknumber(L, <span class="hljs-number">2</span>);<br>    <br>    <span class="hljs-comment">// 执行计算</span><br>    <span class="hljs-type">double</span> result = a + b;<br>    <br>    <span class="hljs-comment">// 将结果压入栈</span><br>    lua_pushnumber(L, result);<br>    <br>    <span class="hljs-comment">// 返回值数量</span><br>    <span class="hljs-keyword">return</span> <span class="hljs-number">1</span>;<br>&#125;<br><br><span class="hljs-type">int</span> <span class="hljs-title function_">main</span><span class="hljs-params">()</span> &#123;<br>    lua_State *L = luaL_newstate();<br>    luaL_openlibs(L);<br>    <br>    <span class="hljs-comment">// 注册 C 函数</span><br>    lua_register(L, <span class="hljs-string">&quot;c_add&quot;</span>, c_add);<br>    <br>    <span class="hljs-comment">// 在 Lua 中调用 C 函数</span><br>    <span class="hljs-type">const</span> <span class="hljs-type">char</span> *code = <span class="hljs-string">&quot;result = c_add(10, 20)\n&quot;</span><br>                       <span class="hljs-string">&quot;print(&#x27;10 + 20 =&#x27;, result)&quot;</span>;<br>    <br>    <span class="hljs-keyword">if</span> (luaL_dostring(L, code) != LUA_OK) &#123;<br>        <span class="hljs-type">const</span> <span class="hljs-type">char</span> *error = lua_tostring(L, <span class="hljs-number">-1</span>);<br>        <span class="hljs-built_in">printf</span>(<span class="hljs-string">&quot;错误：%s\n&quot;</span>, error);<br>        lua_pop(L, <span class="hljs-number">1</span>);<br>    &#125;<br>    <br>    lua_close(L);<br>    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;<br>&#125;<br></code></pre></td></tr></table></figure><h3 id="4-使用-luaL-Reg-批量注册函数"><a href="#4-使用-luaL-Reg-批量注册函数" class="headerlink" title="4. 使用 luaL_Reg 批量注册函数"></a>4. 使用 luaL_Reg 批量注册函数</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&lt;stdio.h&gt;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;lua.h&quot;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;lualib.h&quot;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;lauxlib.h&quot;</span></span><br><br><span class="hljs-comment">// C 函数示例</span><br><span class="hljs-type">static</span> <span class="hljs-type">int</span> <span class="hljs-title function_">c_add</span><span class="hljs-params">(lua_State *L)</span> &#123;<br>    <span class="hljs-type">double</span> a = luaL_checknumber(L, <span class="hljs-number">1</span>);<br>    <span class="hljs-type">double</span> b = luaL_checknumber(L, <span class="hljs-number">2</span>);<br>    lua_pushnumber(L, a + b);<br>    <span class="hljs-keyword">return</span> <span class="hljs-number">1</span>;<br>&#125;<br><br><span class="hljs-type">static</span> <span class="hljs-type">int</span> <span class="hljs-title function_">c_sub</span><span class="hljs-params">(lua_State *L)</span> &#123;<br>    <span class="hljs-type">double</span> a = luaL_checknumber(L, <span class="hljs-number">1</span>);<br>    <span class="hljs-type">double</span> b = luaL_checknumber(L, <span class="hljs-number">2</span>);<br>    lua_pushnumber(L, a - b);<br>    <span class="hljs-keyword">return</span> <span class="hljs-number">1</span>;<br>&#125;<br><br><span class="hljs-type">static</span> <span class="hljs-type">int</span> <span class="hljs-title function_">c_mul</span><span class="hljs-params">(lua_State *L)</span> &#123;<br>    <span class="hljs-type">double</span> a = luaL_checknumber(L, <span class="hljs-number">1</span>);<br>    <span class="hljs-type">double</span> b = luaL_checknumber(L, <span class="hljs-number">2</span>);<br>    lua_pushnumber(L, a * b);<br>    <span class="hljs-keyword">return</span> <span class="hljs-number">1</span>;<br>&#125;<br><br><span class="hljs-comment">// 函数注册表</span><br><span class="hljs-type">static</span> <span class="hljs-type">const</span> luaL_Reg math_lib[] = &#123;<br>    &#123;<span class="hljs-string">&quot;add&quot;</span>, c_add&#125;,<br>    &#123;<span class="hljs-string">&quot;sub&quot;</span>, c_sub&#125;,<br>    &#123;<span class="hljs-string">&quot;mul&quot;</span>, c_mul&#125;,<br>    &#123;<span class="hljs-literal">NULL</span>, <span class="hljs-literal">NULL</span>&#125;  <span class="hljs-comment">// 结束标记</span><br>&#125;;<br><br><span class="hljs-type">int</span> <span class="hljs-title function_">main</span><span class="hljs-params">()</span> &#123;<br>    lua_State *L = luaL_newstate();<br>    luaL_openlibs(L);<br>    <br>    <span class="hljs-comment">// 批量注册函数</span><br>    luaL_newlib(L, math_lib);<br>    lua_setglobal(L, <span class="hljs-string">&quot;cmath&quot;</span>);<br>    <br>    <span class="hljs-comment">// 在 Lua 中调用 C 函数</span><br>    <span class="hljs-type">const</span> <span class="hljs-type">char</span> *code = <span class="hljs-string">&quot;result = cmath.add(10, 20)\n&quot;</span><br>                       <span class="hljs-string">&quot;print(&#x27;10 + 20 =&#x27;, result)\n&quot;</span><br>                       <span class="hljs-string">&quot;result = cmath.sub(10, 20)\n&quot;</span><br>                       <span class="hljs-string">&quot;print(&#x27;10 - 20 =&#x27;, result)\n&quot;</span><br>                       <span class="hljs-string">&quot;result = cmath.mul(10, 20)\n&quot;</span><br>                       <span class="hljs-string">&quot;print(&#x27;10 * 20 =&#x27;, result)&quot;</span>;<br>    <br>    <span class="hljs-keyword">if</span> (luaL_dostring(L, code) != LUA_OK) &#123;<br>        <span class="hljs-type">const</span> <span class="hljs-type">char</span> *error = lua_tostring(L, <span class="hljs-number">-1</span>);<br>        <span class="hljs-built_in">printf</span>(<span class="hljs-string">&quot;错误：%s\n&quot;</span>, error);<br>        lua_pop(L, <span class="hljs-number">1</span>);<br>    &#125;<br>    <br>    lua_close(L);<br>    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;<br>&#125;<br></code></pre></td></tr></table></figure><h2 id="最佳实践和注意事项"><a href="#最佳实践和注意事项" class="headerlink" title="最佳实践和注意事项"></a>最佳实践和注意事项</h2><p>在使用 Lua C API 时，有一些最佳实践和注意事项需要牢记：</p><h3 id="1-栈管理"><a href="#1-栈管理" class="headerlink" title="1. 栈管理"></a>1. 栈管理</h3><ul><li>始终注意栈中元素的数量，避免栈溢出</li><li>在函数调用前后检查栈的状态</li><li>使用 <code>lua_checkstack</code> 确保栈有足够的空间</li></ul><h3 id="2-错误处理"><a href="#2-错误处理" class="headerlink" title="2. 错误处理"></a>2. 错误处理</h3><ul><li>使用 <code>lua_pcall</code> 而不是 <code>lua_call</code> 来捕获错误</li><li>正确处理和报告错误信息</li><li>使用辅助库函数进行参数检查</li></ul><h3 id="3-内存管理"><a href="#3-内存管理" class="headerlink" title="3. 内存管理"></a>3. 内存管理</h3><ul><li>及时关闭不需要的 Lua 状态</li><li>注意用户数据的生命周期管理</li><li>避免内存泄漏</li></ul><h3 id="4-性能优化"><a href="#4-性能优化" class="headerlink" title="4. 性能优化"></a>4. 性能优化</h3><ul><li>尽量减少 C 和 Lua 之间的数据传递</li><li>使用适当的缓存机制</li><li>避免在频繁调用的函数中进行昂贵的操作</li></ul><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>Lua C API 是连接 Lua 脚本和 C 程序的强大工具。通过本文的介绍，我们详细了解了 Lua 栈机制、三个核心头文件的功能以及各种 API 函数的用途。掌握了这些知识，你就能够在自己的项目中有效地使用 Lua 来增强 C 程序的功能和灵活性。</p><p>在实际开发中，合理运用 Lua C API 可以极大地提高开发效率，实现更灵活的程序架构。无论是在游戏开发中实现脚本化配置，还是在嵌入式系统中提供可定制的功能，Lua 都能发挥重要作用。</p><p>希望本文能帮助你更好地理解和使用 Lua C API，在你的项目中充分发挥 Lua 的优势。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li><a href="https://www.lua.org/manual/5.4/">Lua 5.4 官方文档</a></li><li><a href="https://www.lua.org/pil/">Programming in Lua (first edition)</a></li><li><a href="https://www.lua.org/manual/5.4/manual.html#4">Lua C API 官方参考手册</a></li><li><a href="https://github.com/lua/lua">Lua GitHub 仓库</a></li><li>Roberto Ierusalimschy, “Lua 编程”(Programming in Lua)</li></ul>]]>
    </content>
    <id>https://bubao.github.io/posts/5a942160.html</id>
    <link href="https://bubao.github.io/posts/5a942160.html"/>
    <published>2025-12-05T15:40:00.000Z</published>
    <summary>
      <![CDATA[<p>在现代软件开发中，将脚本语言与宿主语言相结合已经成为一种常见的做法。Lua 作为一种轻量级、高效的脚本语言，广泛应用于游戏开发、嵌入式系统和其他需要高度可配置性的场景中。本文将深入探讨 Lua 与 C 语言之间的交互机制，详细解析 Lua C API 的各个接口用途，帮助开发者更好地掌握这一强大的功能。</p>]]>
    </summary>
    <title>深入理解 Lua-C 交互：详解 Lua C API 各个接口用途</title>
    <updated>2026-10-31T20:37:58.479Z</updated>
  </entry>
  <entry>
    <author>
      <name>一念</name>
    </author>
    <category term="教程" scheme="https://bubao.github.io/categories/%E6%95%99%E7%A8%8B/"/>
    <category term="esp32" scheme="https://bubao.github.io/tags/esp32/"/>
    <category term="esp-idf" scheme="https://bubao.github.io/tags/esp-idf/"/>
    <category term="Lua" scheme="https://bubao.github.io/tags/Lua/"/>
    <category term="IoT" scheme="https://bubao.github.io/tags/IoT/"/>
    <content>
      <![CDATA[<p>在嵌入式开发领域，C&#x2F;C++ 一直是主流的编程语言，但随着物联网设备功能越来越复杂，对灵活性的需求也在不断提升。脚本语言因其动态性和易用性，逐渐成为嵌入式系统的重要补充。本文将介绍如何在 ESP-IDF 中集成 Lua 脚本语言，让你的 ESP32 项目具备更强的可配置性和扩展性。</p><span id="more"></span><h2 id="为什么选择-Lua？"><a href="#为什么选择-Lua？" class="headerlink" title="为什么选择 Lua？"></a>为什么选择 Lua？</h2><p>Lua 是一种轻量级、高效的脚本语言，特别适合嵌入式系统。选择在 ESP-IDF 中集成 Lua 的主要原因包括：</p><ol><li><strong>降低学习成本</strong>：相比于深入学习 ESP-IDF 的复杂 API，使用 Lua 编写业务逻辑更加直观简单</li><li><strong>提高客制化能力</strong>：用户可以通过修改 Lua 脚本来改变设备行为，无需重新编译固件</li><li><strong>逻辑抽象能力</strong>：许多操作可以归纳为一系列的标准操作，通过 Lua 脚本可以更好地组织和复用这些逻辑</li><li><strong>快速原型开发</strong>：使用 Lua 可以快速验证想法和实现功能原型</li></ol><h3 id="Lua-的技术优势"><a href="#Lua-的技术优势" class="headerlink" title="Lua 的技术优势"></a>Lua 的技术优势</h3><ol><li><strong>体积小巧</strong>：完整解释器只有几百 KB</li><li><strong>性能优秀</strong>：基于寄存器的虚拟机设计</li><li><strong>易于嵌入</strong>：简洁的 C API，方便与宿主程序交互</li><li><strong>可扩展性强</strong>：支持模块化开发和动态加载</li></ol><p>相比 JavaScript 或 Python，Lua 在嵌入式领域的优势更为明显，特别是在资源受限的环境中。</p><h2 id="Lua-与-ESP32-集成方案对比"><a href="#Lua-与-ESP32-集成方案对比" class="headerlink" title="Lua 与 ESP32 集成方案对比"></a>Lua 与 ESP32 集成方案对比</h2><p>在 ESP32 上运行 Lua 脚本有两种主要方案：</p><h3 id="1-Lua-RTOS-方案"><a href="#1-Lua-RTOS-方案" class="headerlink" title="1. Lua RTOS 方案"></a>1. Lua RTOS 方案</h3><p><a href="https://github.com/whitecatboard/Lua-RTOS-ESP32">whitecatboard&#x2F;Lua-RTOS-ESP32</a> 是一个完整的实时操作系统，专门为 ESP32 设计，具有以下特点：</p><ul><li>提供完整的 Lua 运行环境</li><li>包含交互式 REPL（Read-Eval-Print Loop）</li><li>提供图形化开发环境（Whitecat IDE）</li><li>支持拖拽式编程转为 Lua 代码</li><li>集成了大量硬件驱动和中间件服务</li></ul><h3 id="2-ESP-IDF-组件方案"><a href="#2-ESP-IDF-组件方案" class="headerlink" title="2. ESP-IDF 组件方案"></a>2. ESP-IDF 组件方案</h3><p><a href="https://github.com/georgik/esp-idf-component-lua">georgik&#x2F;esp-idf-component-lua</a> 是 Espressif 官方提供的组件，它将 Lua 封装为 ESP-IDF 的组件，具有以下特点：</p><ul><li>作为 ESP-IDF 的标准组件集成</li><li>专注于嵌入式应用，不提供 REPL</li><li>可以与现有的 ESP-IDF 项目无缝集成</li><li>针对 ESP32 平台进行了优化</li></ul><p>两种方案各有优势，选择哪种取决于你的具体需求。如果你需要完整的 Lua 开发环境和交互式编程体验，可以选择 Lua RTOS；如果你希望在现有 ESP-IDF 项目中嵌入 Lua 脚本功能，则更适合使用 ESP-IDF 组件方案。</p><h2 id="深入理解-Lua-C-API"><a href="#深入理解-Lua-C-API" class="headerlink" title="深入理解 Lua C API"></a>深入理解 Lua C API</h2><p>Lua 与 C 程序的交互是通过一个虚拟栈来完成的。这个栈是 Lua 状态的一部分，C 代码可以通过 API 函数来操作这个栈，从而实现与 Lua 脚本的数据交换。</p><h3 id="栈操作基础"><a href="#栈操作基础" class="headerlink" title="栈操作基础"></a>栈操作基础</h3><p>Lua 提供了一系列函数来操作栈：</p><ul><li><code>lua_push*</code> 函数族：将值压入栈顶</li><li><code>lua_to*</code> 函数族：从栈中取出值</li><li><code>lua_is*</code> 函数族：检查栈中某个位置的值是否为特定类型</li><li><code>lua_gettop</code>：获取栈顶索引</li><li><code>lua_settop</code>：设置栈顶位置</li><li><code>lua_pop</code>：弹出栈顶元素</li></ul><h3 id="栈索引规则"><a href="#栈索引规则" class="headerlink" title="栈索引规则"></a>栈索引规则</h3><p>在 Lua 栈中，索引可以是正数也可以是负数：</p><ul><li>正数索引：从栈底开始计数（1 为栈底）</li><li>负数索引：从栈顶开始计数（-1 为栈顶）</li></ul><p>例如，在一个有 3 个元素的栈中：</p><figure class="highlight tap"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs tap">栈底 [<span class="hljs-number"> 1 </span>][<span class="hljs-number"> 2 </span>][<span class="hljs-number"> 3 </span>] 栈顶<br>索引：1   <span class="hljs-number"> 2 </span>   3<br>索引：-3   -2   -1<br></code></pre></td></tr></table></figure><h3 id="Lua-C-API-详解"><a href="#Lua-C-API-详解" class="headerlink" title="Lua C API 详解"></a>Lua C API 详解</h3><p>在 ESP-IDF 中集成 Lua 时，我们需要了解三个重要的头文件：</p><h4 id="1-lua-h-基础-API"><a href="#1-lua-h-基础-API" class="headerlink" title="1. lua.h - 基础 API"></a>1. lua.h - 基础 API</h4><p><code>lua.h</code> 定义了 Lua 提供的基础函数，是与 Lua 交互的核心接口。主要包括：</p><ul><li><p><strong>环境管理函数</strong>：</p><ul><li><code>lua_newstate</code>：创建新的 Lua 环境</li><li><code>lua_close</code>：关闭 Lua 环境</li><li><code>luaL_newstate</code>：创建新的 Lua 环境（辅助库函数）</li></ul></li><li><p><strong>栈操作函数</strong>：</p><ul><li><code>lua_pushnumber</code>：将数字压入栈</li><li><code>lua_pushstring</code>：将字符串压入栈</li><li><code>lua_pushboolean</code>：将布尔值压入栈</li><li><code>lua_pushnil</code>：将 nil 值压入栈</li><li><code>lua_pushcfunction</code>：将 C 函数压入栈</li></ul></li><li><p><strong>函数调用函数</strong>：</p><ul><li><code>lua_call</code>：调用 Lua 函数</li><li><code>lua_pcall</code>：保护模式下调用 Lua 函数</li><li><code>lua_resume</code>：恢复协程执行</li></ul></li><li><p><strong>全局变量操作</strong>：</p><ul><li><code>lua_getglobal</code>：获取全局变量</li><li><code>lua_setglobal</code>：设置全局变量</li></ul></li></ul><p>所有在 <code>lua.h</code> 中定义的函数都以 <code>lua_</code> 为前缀。</p><h4 id="2-lualib-h-标准库"><a href="#2-lualib-h-标准库" class="headerlink" title="2. lualib.h - 标准库"></a>2. lualib.h - 标准库</h4><p><code>lualib.h</code> 声明了打开 Lua 标准库的函数。主要包括：</p><ul><li>基础库：<code>luaopen_base</code></li><li>字符串库：<code>luaopen_string</code></li><li>数学库：<code>luaopen_math</code></li><li>表库：<code>luaopen_table</code></li><li>IO 库：<code>luaopen_io</code></li><li>调试库：<code>luaopen_debug</code></li><li>包管理库：<code>luaopen_package</code></li></ul><p>最重要的函数是 <code>luaL_openlibs</code>，它会一次性打开所有标准库。</p><h4 id="3-lauxlib-h-辅助库"><a href="#3-lauxlib-h-辅助库" class="headerlink" title="3. lauxlib.h - 辅助库"></a>3. lauxlib.h - 辅助库</h4><p><code>lauxlib.h</code> 定义了辅助库提供的函数，所有函数都以 <code>luaL_</code> 为前缀。这些函数基于基础 API 提供了更高层次的抽象，更适合日常使用：</p><ul><li><p><strong>错误处理</strong>：</p><ul><li><code>luaL_error</code>：抛出错误</li><li><code>luaL_argerror</code>：参数错误处理</li><li><code>luaL_check*</code> 函数族：参数类型检查和获取</li></ul></li><li><p><strong>加载代码</strong>：</p><ul><li><code>luaL_loadstring</code>：从字符串加载 Lua 代码</li><li><code>luaL_loadfile</code>：从文件加载 Lua 代码</li><li><code>luaL_dofile</code>：加载并执行文件</li><li><code>luaL_dostring</code>：加载并执行字符串</li></ul></li><li><p><strong>函数注册</strong>：</p><ul><li><code>luaL_newlib</code>：创建新的函数库</li><li><code>luaL_setfuncs</code>：注册函数列表</li><li><code>luaL_register</code>：注册函数库（已弃用）</li></ul></li></ul><h3 id="C-与-Lua-交互实例"><a href="#C-与-Lua-交互实例" class="headerlink" title="C 与 Lua 交互实例"></a>C 与 Lua 交互实例</h3><p>理解了基本概念后，让我们通过一个实际的例子来看看如何在 C 和 Lua 之间进行交互：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;lua.h&quot;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;lualib.h&quot;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;lauxlib.h&quot;</span></span><br><br><span class="hljs-comment">// C 函数示例</span><br><span class="hljs-type">static</span> <span class="hljs-type">int</span> <span class="hljs-title function_">add_numbers</span><span class="hljs-params">(lua_State *L)</span> &#123;<br>    <span class="hljs-comment">// 检查参数数量和类型</span><br>    <span class="hljs-type">int</span> a = luaL_checkinteger(L, <span class="hljs-number">1</span>);<br>    <span class="hljs-type">int</span> b = luaL_checkinteger(L, <span class="hljs-number">2</span>);<br>    <br>    <span class="hljs-comment">// 执行计算</span><br>    <span class="hljs-type">int</span> result = a + b;<br>    <br>    <span class="hljs-comment">// 将结果压入栈</span><br>    lua_pushinteger(L, result);<br>    <br>    <span class="hljs-comment">// 返回值数量</span><br>    <span class="hljs-keyword">return</span> <span class="hljs-number">1</span>;<br>&#125;<br><br><span class="hljs-comment">// 注册函数到 Lua</span><br><span class="hljs-type">void</span> <span class="hljs-title function_">register_custom_functions</span><span class="hljs-params">(lua_State *L)</span> &#123;<br>    <span class="hljs-comment">// 方法 1：使用 luaL_newlib</span><br>    <span class="hljs-type">static</span> <span class="hljs-type">const</span> luaL_Reg mylib[] = &#123;<br>        &#123;<span class="hljs-string">&quot;add&quot;</span>, add_numbers&#125;,<br>        &#123;<span class="hljs-literal">NULL</span>, <span class="hljs-literal">NULL</span>&#125;<br>    &#125;;<br>    luaL_newlib(L, mylib);<br>    lua_setglobal(L, <span class="hljs-string">&quot;mylib&quot;</span>);<br>    <br>    <span class="hljs-comment">// 方法 2：使用 lua_register</span><br>    lua_register(L, <span class="hljs-string">&quot;add_numbers&quot;</span>, add_numbers);<br>&#125;<br></code></pre></td></tr></table></figure><p>对应的 Lua 代码：</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs lua"><span class="hljs-comment">-- 使用通过 luaL_newlib 注册的函数</span><br>result1 = mylib.add(<span class="hljs-number">10</span>, <span class="hljs-number">20</span>)<br><span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;Result from mylib.add: &quot;</span> .. result1)<br><br><span class="hljs-comment">-- 使用通过 lua_register 注册的函数</span><br>result2 = add_numbers(<span class="hljs-number">30</span>, <span class="hljs-number">40</span>)<br><span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;Result from add_numbers: &quot;</span> .. result2)<br></code></pre></td></tr></table></figure><h2 id="ESP-IDF-中的-Lua-组件"><a href="#ESP-IDF-中的-Lua-组件" class="headerlink" title="ESP-IDF 中的 Lua 组件"></a>ESP-IDF 中的 Lua 组件</h2><p>Espressif 提供了一个官方的 Lua 组件 <a href="https://github.com/georgik/esp-idf-component-lua">georgik&#x2F;lua</a>，它是上游 Lua 项目的 ESP-IDF 封装版本。</p><p>该组件的主要特点：</p><ul><li>基于 Lua 主分支开发版本</li><li>针对 ESP32 平台进行了优化</li><li>支持所有 ESP-IDF 目标芯片（ESP32、ESP32-S2&#x2F;S3、ESP32-C3&#x2F;C6 等）</li><li>提供配置选项，如 <code>LUA_MAXSTACK</code> 栈大小限制</li></ul><h2 id="集成步骤"><a href="#集成步骤" class="headerlink" title="集成步骤"></a>集成步骤</h2><h3 id="1-添加依赖"><a href="#1-添加依赖" class="headerlink" title="1. 添加依赖"></a>1. 添加依赖</h3><p>在项目目录下执行以下命令添加 Lua 组件：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">idf.py add-dependency <span class="hljs-string">&quot;georgik/lua^5.5.0~7&quot;</span><br></code></pre></td></tr></table></figure><p>或者在项目的 <a href="file:///Users/bubao/WorkSpace/github/blog/package.json">idf_component.yml</a> 文件中添加依赖：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs yaml"><span class="hljs-attr">dependencies:</span><br>  <span class="hljs-attr">georgik/lua:</span><br>    <span class="hljs-attr">version:</span> <span class="hljs-string">&quot;^5.5.0~7&quot;</span><br></code></pre></td></tr></table></figure><h3 id="2-配置选项（可选）"><a href="#2-配置选项（可选）" class="headerlink" title="2. 配置选项（可选）"></a>2. 配置选项（可选）</h3><p>可以通过 <code>menuconfig</code> 配置 Lua 相关参数：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">idf.py menuconfig<br></code></pre></td></tr></table></figure><p>在配置菜单中找到 Lua 相关选项：</p><ul><li><code>Component config</code> -&gt; <code>Lua</code> -&gt; <code>Maximum stack size</code></li></ul><h3 id="3-编写示例代码"><a href="#3-编写示例代码" class="headerlink" title="3. 编写示例代码"></a>3. 编写示例代码</h3><p>创建一个简单的示例来测试 Lua 集成：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&lt;stdio.h&gt;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;lua.h&quot;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;lualib.h&quot;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;lauxlib.h&quot;</span></span><br><br><span class="hljs-type">void</span> <span class="hljs-title function_">run_lua_script</span><span class="hljs-params">()</span> &#123;<br>    lua_State *L = luaL_newstate();  <span class="hljs-comment">// 创建新的 Lua 状态</span><br>    luaL_openlibs(L);                <span class="hljs-comment">// 打开标准库</span><br><br>    <span class="hljs-comment">// 执行简单的 Lua 代码</span><br>    <span class="hljs-type">const</span> <span class="hljs-type">char</span> *script = <span class="hljs-string">&quot;print(&#x27;Hello from Lua!&#x27;)\n&quot;</span><br>                         <span class="hljs-string">&quot;result = 42\n&quot;</span><br>                         <span class="hljs-string">&quot;print(&#x27;The answer is: &#x27; .. result)&quot;</span>;<br>    <br>    <span class="hljs-keyword">if</span> (luaL_dostring(L, script)) &#123;<br>        <span class="hljs-built_in">printf</span>(<span class="hljs-string">&quot;Error: %s\n&quot;</span>, lua_tostring(L, <span class="hljs-number">-1</span>));<br>    &#125;<br><br>    <span class="hljs-comment">// 获取 Lua 变量值</span><br>    lua_getglobal(L, <span class="hljs-string">&quot;result&quot;</span>);<br>    <span class="hljs-keyword">if</span> (lua_isnumber(L, <span class="hljs-number">-1</span>)) &#123;<br>        <span class="hljs-type">int</span> result = lua_tointeger(L, <span class="hljs-number">-1</span>);<br>        <span class="hljs-built_in">printf</span>(<span class="hljs-string">&quot;Retrieved from Lua: %d\n&quot;</span>, result);<br>    &#125;<br>    <br>    lua_close(L);  <span class="hljs-comment">// 关闭 Lua 状态</span><br>&#125;<br><br><span class="hljs-type">void</span> <span class="hljs-title function_">app_main</span><span class="hljs-params">(<span class="hljs-type">void</span>)</span> &#123;<br>    <span class="hljs-built_in">printf</span>(<span class="hljs-string">&quot;Starting Lua integration example...\n&quot;</span>);<br>    run_lua_script();<br>    <span class="hljs-built_in">printf</span>(<span class="hljs-string">&quot;Lua example completed.\n&quot;</span>);<br>&#125;<br></code></pre></td></tr></table></figure><h3 id="4-编译和烧录"><a href="#4-编译和烧录" class="headerlink" title="4. 编译和烧录"></a>4. 编译和烧录</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash">idf.py build<br>idf.py flash monitor<br></code></pre></td></tr></table></figure><h2 id="批量注册-Lua-脚本的方案"><a href="#批量注册-Lua-脚本的方案" class="headerlink" title="批量注册 Lua 脚本的方案"></a>批量注册 Lua 脚本的方案</h2><p>在实际项目中，我们往往需要注册大量的 C 函数供 Lua 调用。手动一个个注册会非常繁琐，我们可以使用批量注册的方式来简化这一过程。</p><h3 id="1-使用-luaL-Reg-结构体数组"><a href="#1-使用-luaL-Reg-结构体数组" class="headerlink" title="1. 使用 luaL_Reg 结构体数组"></a>1. 使用 luaL_Reg 结构体数组</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&lt;stdio.h&gt;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;lua.h&quot;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;lualib.h&quot;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;lauxlib.h&quot;</span></span><br><br><span class="hljs-comment">// 示例 C 函数</span><br><span class="hljs-type">static</span> <span class="hljs-type">int</span> <span class="hljs-title function_">led_on</span><span class="hljs-params">(lua_State *L)</span> &#123;<br>    <span class="hljs-built_in">printf</span>(<span class="hljs-string">&quot;Turning LED ON\n&quot;</span>);<br>    <span class="hljs-comment">// 实际的硬件控制代码</span><br>    lua_pushboolean(L, <span class="hljs-number">1</span>);  <span class="hljs-comment">// 返回 true</span><br>    <span class="hljs-keyword">return</span> <span class="hljs-number">1</span>;<br>&#125;<br><br><span class="hljs-type">static</span> <span class="hljs-type">int</span> <span class="hljs-title function_">led_off</span><span class="hljs-params">(lua_State *L)</span> &#123;<br>    <span class="hljs-built_in">printf</span>(<span class="hljs-string">&quot;Turning LED OFF\n&quot;</span>);<br>    <span class="hljs-comment">// 实际的硬件控制代码</span><br>    lua_pushboolean(L, <span class="hljs-number">1</span>);  <span class="hljs-comment">// 返回 true</span><br>    <span class="hljs-keyword">return</span> <span class="hljs-number">1</span>;<br>&#125;<br><br><span class="hljs-type">static</span> <span class="hljs-type">int</span> <span class="hljs-title function_">get_temperature</span><span class="hljs-params">(lua_State *L)</span> &#123;<br>    <span class="hljs-comment">// 模拟获取温度值</span><br>    <span class="hljs-type">float</span> temperature = <span class="hljs-number">25.6</span>;<br>    lua_pushnumber(L, temperature);<br>    <span class="hljs-keyword">return</span> <span class="hljs-number">1</span>;<br>&#125;<br><br><span class="hljs-comment">// 定义函数注册表</span><br><span class="hljs-type">static</span> <span class="hljs-type">const</span> luaL_Reg mylib[] = &#123;<br>    &#123;<span class="hljs-string">&quot;led_on&quot;</span>, led_on&#125;,<br>    &#123;<span class="hljs-string">&quot;led_off&quot;</span>, led_off&#125;,<br>    &#123;<span class="hljs-string">&quot;get_temperature&quot;</span>, get_temperature&#125;,<br>    &#123;<span class="hljs-literal">NULL</span>, <span class="hljs-literal">NULL</span>&#125;  <span class="hljs-comment">// 数组结束标记</span><br>&#125;;<br><br><span class="hljs-comment">// 注册函数库</span><br><span class="hljs-type">void</span> <span class="hljs-title function_">register_my_library</span><span class="hljs-params">(lua_State *L)</span> &#123;<br>    luaL_newlib(L, mylib);<br>    lua_setglobal(L, <span class="hljs-string">&quot;mylib&quot;</span>);<br>&#125;<br></code></pre></td></tr></table></figure><h3 id="2-更复杂的批量注册方案"><a href="#2-更复杂的批量注册方案" class="headerlink" title="2. 更复杂的批量注册方案"></a>2. 更复杂的批量注册方案</h3><p>对于更复杂的项目，我们可以创建一个更加灵活的注册机制：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&lt;stdio.h&gt;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;lua.h&quot;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;lualib.h&quot;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;lauxlib.h&quot;</span></span><br><br><span class="hljs-comment">// 设备控制函数</span><br><span class="hljs-type">static</span> <span class="hljs-type">int</span> <span class="hljs-title function_">gpio_set</span><span class="hljs-params">(lua_State *L)</span> &#123;<br>    <span class="hljs-type">int</span> pin = luaL_checkinteger(L, <span class="hljs-number">1</span>);<br>    <span class="hljs-type">int</span> level = luaL_checkinteger(L, <span class="hljs-number">2</span>);<br>    <span class="hljs-built_in">printf</span>(<span class="hljs-string">&quot;Setting GPIO %d to %d\n&quot;</span>, pin, level);<br>    <span class="hljs-comment">// 实际的 GPIO 控制代码</span><br>    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;<br>&#125;<br><br><span class="hljs-type">static</span> <span class="hljs-type">int</span> <span class="hljs-title function_">gpio_get</span><span class="hljs-params">(lua_State *L)</span> &#123;<br>    <span class="hljs-type">int</span> pin = luaL_checkinteger(L, <span class="hljs-number">1</span>);<br>    <span class="hljs-comment">// 实际的 GPIO 读取代码</span><br>    <span class="hljs-type">int</span> level = <span class="hljs-number">1</span>;  <span class="hljs-comment">// 模拟返回值</span><br>    lua_pushinteger(L, level);<br>    <span class="hljs-keyword">return</span> <span class="hljs-number">1</span>;<br>&#125;<br><br><span class="hljs-comment">// 网络相关函数</span><br><span class="hljs-type">static</span> <span class="hljs-type">int</span> <span class="hljs-title function_">wifi_connect</span><span class="hljs-params">(lua_State *L)</span> &#123;<br>    <span class="hljs-type">const</span> <span class="hljs-type">char</span> *ssid = luaL_checkstring(L, <span class="hljs-number">1</span>);<br>    <span class="hljs-type">const</span> <span class="hljs-type">char</span> *password = luaL_checkstring(L, <span class="hljs-number">2</span>);<br>    <span class="hljs-built_in">printf</span>(<span class="hljs-string">&quot;Connecting to WiFi: %s\n&quot;</span>, ssid);<br>    <span class="hljs-comment">// 实际的 WiFi 连接代码</span><br>    lua_pushboolean(L, <span class="hljs-number">1</span>);<br>    <span class="hljs-keyword">return</span> <span class="hljs-number">1</span>;<br>&#125;<br><br><span class="hljs-type">static</span> <span class="hljs-type">int</span> <span class="hljs-title function_">http_get</span><span class="hljs-params">(lua_State *L)</span> &#123;<br>    <span class="hljs-type">const</span> <span class="hljs-type">char</span> *url = luaL_checkstring(L, <span class="hljs-number">1</span>);<br>    <span class="hljs-built_in">printf</span>(<span class="hljs-string">&quot;Making HTTP GET request to: %s\n&quot;</span>, url);<br>    <span class="hljs-comment">// 实际的 HTTP 请求代码</span><br>    lua_pushstring(L, <span class="hljs-string">&quot;Response data&quot;</span>);  <span class="hljs-comment">// 模拟响应</span><br>    <span class="hljs-keyword">return</span> <span class="hljs-number">1</span>;<br>&#125;<br><br><span class="hljs-comment">// 定义多个函数库</span><br><span class="hljs-type">static</span> <span class="hljs-type">const</span> luaL_Reg gpio_lib[] = &#123;<br>    &#123;<span class="hljs-string">&quot;set&quot;</span>, gpio_set&#125;,<br>    &#123;<span class="hljs-string">&quot;get&quot;</span>, gpio_get&#125;,<br>    &#123;<span class="hljs-literal">NULL</span>, <span class="hljs-literal">NULL</span>&#125;<br>&#125;;<br><br><span class="hljs-type">static</span> <span class="hljs-type">const</span> luaL_Reg network_lib[] = &#123;<br>    &#123;<span class="hljs-string">&quot;connect&quot;</span>, wifi_connect&#125;,<br>    &#123;<span class="hljs-string">&quot;get&quot;</span>, http_get&#125;,<br>    &#123;<span class="hljs-literal">NULL</span>, <span class="hljs-literal">NULL</span>&#125;<br>&#125;;<br><br><span class="hljs-comment">// 注册多个库</span><br><span class="hljs-type">void</span> <span class="hljs-title function_">register_libraries</span><span class="hljs-params">(lua_State *L)</span> &#123;<br>    <span class="hljs-comment">// 注册 GPIO 库</span><br>    luaL_newlib(L, gpio_lib);<br>    lua_setglobal(L, <span class="hljs-string">&quot;gpio&quot;</span>);<br>    <br>    <span class="hljs-comment">// 注册网络库</span><br>    luaL_newlib(L, network_lib);<br>    lua_setglobal(L, <span class="hljs-string">&quot;network&quot;</span>);<br>&#125;<br></code></pre></td></tr></table></figure><h3 id="3-自动化注册机制"><a href="#3-自动化注册机制" class="headerlink" title="3. 自动化注册机制"></a>3. 自动化注册机制</h3><p>为了进一步简化注册过程，我们可以创建一个自动化注册机制：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&lt;stdio.h&gt;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;lua.h&quot;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;lualib.h&quot;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;lauxlib.h&quot;</span></span><br><br><span class="hljs-comment">// 定义模块信息结构</span><br><span class="hljs-keyword">typedef</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> &#123;</span><br>    <span class="hljs-type">const</span> <span class="hljs-type">char</span> *name;<br>    <span class="hljs-type">const</span> luaL_Reg *funcs;<br>&#125; <span class="hljs-type">module_info_t</span>;<br><br><span class="hljs-comment">// 各个模块的函数定义</span><br><span class="hljs-type">static</span> <span class="hljs-type">int</span> <span class="hljs-title function_">func1</span><span class="hljs-params">(lua_State *L)</span> &#123; <span class="hljs-comment">/* ... */</span> <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>; &#125;<br><span class="hljs-type">static</span> <span class="hljs-type">int</span> <span class="hljs-title function_">func2</span><span class="hljs-params">(lua_State *L)</span> &#123; <span class="hljs-comment">/* ... */</span> <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>; &#125;<br><span class="hljs-type">static</span> <span class="hljs-type">int</span> <span class="hljs-title function_">func3</span><span class="hljs-params">(lua_State *L)</span> &#123; <span class="hljs-comment">/* ... */</span> <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>; &#125;<br><span class="hljs-type">static</span> <span class="hljs-type">int</span> <span class="hljs-title function_">func4</span><span class="hljs-params">(lua_State *L)</span> &#123; <span class="hljs-comment">/* ... */</span> <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>; &#125;<br><br><span class="hljs-comment">// 定义各个模块的函数列表</span><br><span class="hljs-type">static</span> <span class="hljs-type">const</span> luaL_Reg module1_funcs[] = &#123;<br>    &#123;<span class="hljs-string">&quot;func1&quot;</span>, func1&#125;,<br>    &#123;<span class="hljs-literal">NULL</span>, <span class="hljs-literal">NULL</span>&#125;<br>&#125;;<br><br><span class="hljs-type">static</span> <span class="hljs-type">const</span> luaL_Reg module2_funcs[] = &#123;<br>    &#123;<span class="hljs-string">&quot;func2&quot;</span>, func2&#125;,<br>    &#123;<span class="hljs-string">&quot;func3&quot;</span>, func3&#125;,<br>    &#123;<span class="hljs-literal">NULL</span>, <span class="hljs-literal">NULL</span>&#125;<br>&#125;;<br><br><span class="hljs-type">static</span> <span class="hljs-type">const</span> luaL_Reg standalone_funcs[] = &#123;<br>    &#123;<span class="hljs-string">&quot;func4&quot;</span>, func4&#125;,<br>    &#123;<span class="hljs-literal">NULL</span>, <span class="hljs-literal">NULL</span>&#125;<br>&#125;;<br><br><span class="hljs-comment">// 定义所有模块信息</span><br><span class="hljs-type">static</span> <span class="hljs-type">const</span> <span class="hljs-type">module_info_t</span> modules[] = &#123;<br>    &#123;<span class="hljs-string">&quot;module1&quot;</span>, module1_funcs&#125;,<br>    &#123;<span class="hljs-string">&quot;module2&quot;</span>, module2_funcs&#125;,<br>    &#123;<span class="hljs-literal">NULL</span>, <span class="hljs-literal">NULL</span>&#125;  <span class="hljs-comment">// 模块列表结束标记</span><br>&#125;;<br><br><span class="hljs-comment">// 自动注册所有模块</span><br><span class="hljs-type">void</span> <span class="hljs-title function_">auto_register_modules</span><span class="hljs-params">(lua_State *L)</span> &#123;<br>    <span class="hljs-comment">// 注册模块库</span><br>    <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> i = <span class="hljs-number">0</span>; modules[i].name != <span class="hljs-literal">NULL</span>; i++) &#123;<br>        luaL_newlib(L, modules[i].funcs);<br>        lua_setglobal(L, modules[i].name);<br>    &#125;<br>    <br>    <span class="hljs-comment">// 注册独立函数</span><br>    luaL_setfuncs(L, standalone_funcs, <span class="hljs-number">0</span>);<br>&#125;<br></code></pre></td></tr></table></figure><h2 id="高级用法"><a href="#高级用法" class="headerlink" title="高级用法"></a>高级用法</h2><h3 id="1-从文件加载-Lua-脚本"><a href="#1-从文件加载-Lua-脚本" class="headerlink" title="1. 从文件加载 Lua 脚本"></a>1. 从文件加载 Lua 脚本</h3><p>可以将 Lua 脚本存储在 Flash 文件系统中：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-comment">// 从文件加载并执行 Lua 脚本</span><br><span class="hljs-type">int</span> <span class="hljs-title function_">run_lua_file</span><span class="hljs-params">(lua_State *L, <span class="hljs-type">const</span> <span class="hljs-type">char</span> *filename)</span> &#123;<br>    <span class="hljs-keyword">if</span> (luaL_loadfile(L, filename) || lua_pcall(L, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>)) &#123;<br>        <span class="hljs-built_in">printf</span>(<span class="hljs-string">&quot;Error: %s\n&quot;</span>, lua_tostring(L, <span class="hljs-number">-1</span>));<br>        <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;<br>    &#125;<br>    <span class="hljs-keyword">return</span> <span class="hljs-number">1</span>;<br>&#125;<br></code></pre></td></tr></table></figure><h3 id="2-C-与-Lua-交互"><a href="#2-C-与-Lua-交互" class="headerlink" title="2. C 与 Lua 交互"></a>2. C 与 Lua 交互</h3><p>注册 C 函数供 Lua 调用：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-comment">// C 函数示例</span><br><span class="hljs-type">static</span> <span class="hljs-type">int</span> <span class="hljs-title function_">c_add</span><span class="hljs-params">(lua_State *L)</span> &#123;<br>    <span class="hljs-type">double</span> a = luaL_checknumber(L, <span class="hljs-number">1</span>);<br>    <span class="hljs-type">double</span> b = luaL_checknumber(L, <span class="hljs-number">2</span>);<br>    lua_pushnumber(L, a + b);<br>    <span class="hljs-keyword">return</span> <span class="hljs-number">1</span>;<br>&#125;<br><br><span class="hljs-comment">// 注册函数到 Lua</span><br><span class="hljs-type">void</span> <span class="hljs-title function_">register_functions</span><span class="hljs-params">(lua_State *L)</span> &#123;<br>    lua_register(L, <span class="hljs-string">&quot;c_add&quot;</span>, c_add);<br>&#125;<br></code></pre></td></tr></table></figure><p>对应的 Lua 代码：</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs lua">result = c_add(<span class="hljs-number">10</span>, <span class="hljs-number">20</span>)<br><span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;Result from C function: &quot;</span> .. result)<br></code></pre></td></tr></table></figure><h3 id="3-数据共享"><a href="#3-数据共享" class="headerlink" title="3. 数据共享"></a>3. 数据共享</h3><p>在 C 和 Lua 之间传递复杂数据结构：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-comment">// 创建 Lua 表</span><br>lua_newtable(L);<br><br><span class="hljs-comment">// 设置表字段</span><br>lua_pushstring(L, <span class="hljs-string">&quot;name&quot;</span>);<br>lua_pushstring(L, <span class="hljs-string">&quot;ESP32&quot;</span>);<br>lua_settable(L, <span class="hljs-number">-3</span>);<br><br>lua_pushstring(L, <span class="hljs-string">&quot;temperature&quot;</span>);<br>lua_pushnumber(L, <span class="hljs-number">25.6</span>);<br>lua_settable(L, <span class="hljs-number">-3</span>);<br><br><span class="hljs-comment">// 将表设置为全局变量</span><br>lua_setglobal(L, <span class="hljs-string">&quot;sensor_data&quot;</span>);<br></code></pre></td></tr></table></figure><p>对应的 Lua 代码：</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs lua"><span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;Device: &quot;</span> .. sensor_data.name)<br><span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;Temperature: &quot;</span> .. sensor_data.temperature .. <span class="hljs-string">&quot;°C&quot;</span>)<br></code></pre></td></tr></table></figure><h2 id="客制化应用场景"><a href="#客制化应用场景" class="headerlink" title="客制化应用场景"></a>客制化应用场景</h2><h3 id="1-用户可编程的设备行为"><a href="#1-用户可编程的设备行为" class="headerlink" title="1. 用户可编程的设备行为"></a>1. 用户可编程的设备行为</h3><p>通过 Lua 脚本，用户可以根据自己的需求定制设备的行为逻辑，而无需修改固件代码：</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs lua"><span class="hljs-comment">-- 用户自定义的设备控制逻辑</span><br><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handle_button_press</span><span class="hljs-params">(button_id)</span></span><br>    <span class="hljs-keyword">if</span> button_id == <span class="hljs-number">1</span> <span class="hljs-keyword">then</span><br>        <span class="hljs-comment">-- 控制 LED 灯效</span><br>        set_led_color(<span class="hljs-string">&quot;blue&quot;</span>)<br>        set_led_brightness(<span class="hljs-number">100</span>)<br>    <span class="hljs-keyword">elseif</span> button_id == <span class="hljs-number">2</span> <span class="hljs-keyword">then</span><br>        <span class="hljs-comment">-- 发送通知到手机</span><br>        send_notification(<span class="hljs-string">&quot;Button 2 pressed!&quot;</span>)<br>    <span class="hljs-keyword">end</span><br><span class="hljs-keyword">end</span><br></code></pre></td></tr></table></figure><h3 id="2-动态规则引擎"><a href="#2-动态规则引擎" class="headerlink" title="2. 动态规则引擎"></a>2. 动态规则引擎</h3><p>将业务规则从固件中解耦，通过 Lua 脚本实现动态规则引擎：</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><code class="hljs lua"><span class="hljs-comment">-- 温控规则定义</span><br>rules = &#123;<br>    &#123; min_temp = <span class="hljs-number">0</span>,  max_temp = <span class="hljs-number">18</span>, action = <span class="hljs-string">&quot;turn_on_heater&quot;</span> &#125;,<br>    &#123; min_temp = <span class="hljs-number">18</span>, max_temp = <span class="hljs-number">25</span>, action = <span class="hljs-string">&quot;maintain&quot;</span> &#125;,<br>    &#123; min_temp = <span class="hljs-number">25</span>, max_temp = <span class="hljs-number">30</span>, action = <span class="hljs-string">&quot;turn_on_fan&quot;</span> &#125;,<br>    &#123; min_temp = <span class="hljs-number">30</span>, max_temp = <span class="hljs-number">50</span>, action = <span class="hljs-string">&quot;activate_cooling&quot;</span> &#125;<br>&#125;<br><br><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">apply_temperature_rules</span><span class="hljs-params">(current_temp)</span></span><br>    <span class="hljs-keyword">for</span> _, rule <span class="hljs-keyword">in</span> <span class="hljs-built_in">ipairs</span>(rules) <span class="hljs-keyword">do</span><br>        <span class="hljs-keyword">if</span> current_temp &gt;= rule.min_temp <span class="hljs-keyword">and</span> current_temp &lt; rule.max_temp <span class="hljs-keyword">then</span><br>            <span class="hljs-keyword">return</span> rule.action<br>        <span class="hljs-keyword">end</span><br>    <span class="hljs-keyword">end</span><br>    <span class="hljs-keyword">return</span> <span class="hljs-string">&quot;unknown&quot;</span><br><span class="hljs-keyword">end</span><br></code></pre></td></tr></table></figure><h3 id="3-可配置的数据处理流水线"><a href="#3-可配置的数据处理流水线" class="headerlink" title="3. 可配置的数据处理流水线"></a>3. 可配置的数据处理流水线</h3><p>根据不同应用场景，通过 Lua 脚本配置不同的数据处理流程：</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><code class="hljs lua"><span class="hljs-comment">-- 数据预处理函数</span><br><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">preprocess_data</span><span class="hljs-params">(raw_value)</span></span><br>    <span class="hljs-comment">-- 根据传感器类型进行校准</span><br>    <span class="hljs-keyword">return</span> raw_value * calibration_factor + offset<br><span class="hljs-keyword">end</span><br><br><span class="hljs-comment">-- 数据过滤函数</span><br><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">filter_outliers</span><span class="hljs-params">(value, history)</span></span><br>    <span class="hljs-keyword">local</span> avg = calculate_average(history)<br>    <span class="hljs-keyword">local</span> std_dev = calculate_std_deviation(history)<br>    <br>    <span class="hljs-keyword">if</span> <span class="hljs-built_in">math</span>.<span class="hljs-built_in">abs</span>(value - avg) &gt; <span class="hljs-number">2</span> * std_dev <span class="hljs-keyword">then</span><br>        <span class="hljs-keyword">return</span> avg  <span class="hljs-comment">-- 用平均值替代异常值</span><br>    <span class="hljs-keyword">end</span><br>    <span class="hljs-keyword">return</span> value<br><span class="hljs-keyword">end</span><br><br><span class="hljs-comment">-- 数据聚合函数</span><br><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">aggregate_data</span><span class="hljs-params">(data_points)</span></span><br>    <span class="hljs-keyword">local</span> result = &#123;<br>        <span class="hljs-built_in">min</span> = find_minimum(data_points),<br>        <span class="hljs-built_in">max</span> = find_maximum(data_points),<br>        avg = calculate_average(data_points)<br>    &#125;<br>    <span class="hljs-keyword">return</span> result<br><span class="hljs-keyword">end</span><br></code></pre></td></tr></table></figure><h2 id="实际应用场景"><a href="#实际应用场景" class="headerlink" title="实际应用场景"></a>实际应用场景</h2><h3 id="1-配置管理"><a href="#1-配置管理" class="headerlink" title="1. 配置管理"></a>1. 配置管理</h3><p>使用 Lua 脚本管理设备配置：</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs lua"><span class="hljs-comment">-- config.lua</span><br>device_config = &#123;<br>    ssid = <span class="hljs-string">&quot;MyWiFi&quot;</span>,<br>    password = <span class="hljs-string">&quot;secret123&quot;</span>,<br>    mqtt_server = <span class="hljs-string">&quot;mqtt.example.com&quot;</span>,<br>    update_interval = <span class="hljs-number">30</span><br>&#125;<br></code></pre></td></tr></table></figure><h3 id="2-业务逻辑定制"><a href="#2-业务逻辑定制" class="headerlink" title="2. 业务逻辑定制"></a>2. 业务逻辑定制</h3><p>允许用户通过 Lua 脚本自定义设备行为：</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs lua"><span class="hljs-comment">-- rules.lua</span><br><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">on_temperature_change</span><span class="hljs-params">(temp)</span></span><br>    <span class="hljs-keyword">if</span> temp &gt; <span class="hljs-number">30</span> <span class="hljs-keyword">then</span><br>        <span class="hljs-keyword">return</span> <span class="hljs-string">&quot;turn_on_fan&quot;</span><br>    <span class="hljs-keyword">elseif</span> temp &lt; <span class="hljs-number">18</span> <span class="hljs-keyword">then</span><br>        <span class="hljs-keyword">return</span> <span class="hljs-string">&quot;turn_on_heater&quot;</span><br>    <span class="hljs-keyword">else</span><br>        <span class="hljs-keyword">return</span> <span class="hljs-string">&quot;do_nothing&quot;</span><br>    <span class="hljs-keyword">end</span><br><span class="hljs-keyword">end</span><br></code></pre></td></tr></table></figure><h3 id="3-数据处理管道"><a href="#3-数据处理管道" class="headerlink" title="3. 数据处理管道"></a>3. 数据处理管道</h3><p>构建灵活的数据处理流程：</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><code class="hljs lua"><span class="hljs-comment">-- pipeline.lua</span><br><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">process_sensor_data</span><span class="hljs-params">(raw_data)</span></span><br>    <span class="hljs-comment">-- 数据过滤</span><br>    <span class="hljs-keyword">if</span> raw_data.value &lt; <span class="hljs-number">0</span> <span class="hljs-keyword">or</span> raw_data.value &gt; <span class="hljs-number">100</span> <span class="hljs-keyword">then</span><br>        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>  <span class="hljs-comment">-- 无效数据</span><br>    <span class="hljs-keyword">end</span><br>    <br>    <span class="hljs-comment">-- 数据转换</span><br>    <span class="hljs-keyword">local</span> processed = &#123;<br>        timestamp = raw_data.timestamp,<br>        temperature = raw_data.value * <span class="hljs-number">0.1</span> + <span class="hljs-number">10</span>,<br>        unit = <span class="hljs-string">&quot;celsius&quot;</span><br>    &#125;<br>    <br>    <span class="hljs-keyword">return</span> processed<br><span class="hljs-keyword">end</span><br></code></pre></td></tr></table></figure><h2 id="注意事项"><a href="#注意事项" class="headerlink" title="注意事项"></a>注意事项</h2><ol><li><strong>内存管理</strong>：Lua 虽然轻量，但仍需合理分配堆栈空间</li><li><strong>错误处理</strong>：始终检查 Lua 操作的返回值，防止程序崩溃</li><li><strong>性能考量</strong>：复杂的 Lua 脚本可能影响实时性</li><li><strong>安全性</strong>：避免执行不受信任的 Lua 代码</li><li><strong>调试困难</strong>：相较于 C&#x2F;C++，Lua 脚本的调试可能更加困难</li></ol><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>通过集成 Lua 脚本语言，我们可以显著提升 ESP-IDF 项目的灵活性和可配置性。正如你在工作中发现的那样，许多操作确实可以归纳为一系列的标准操作，而使用 Lua 编写这些逻辑比深入学习 ESP-IDF 的复杂 API 成本更低，也更有利于实现产品的客制化。</p><p>Espressif 官方提供的 Lua 组件大大简化了集成过程，让我们能够专注于业务逻辑而非底层实现。结合 ESP32 强大的硬件能力和 Lua 脚本的灵活性，我们可以构建出更加智能和可扩展的物联网设备。</p><p>对于产品开发来说，这种架构的优势在于：</p><ul><li>降低终端用户的使用门槛</li><li>提高产品的适应性和可扩展性</li><li>减少固件更新的频率</li><li>支持更丰富的个性化定制</li></ul><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li><a href="https://github.com/georgik/esp-idf-component-lua">georgik&#x2F;lua 组件文档</a></li><li><a href="https://docs.espressif.com/projects/esp-idf/zh_CN/stable/esp32s3/index.html">ESP-IDF 官方文档</a></li><li><a href="https://www.lua.org/">Lua 官方文档</a></li><li><a href="https://github.com/georgik/esp32-c3-lua-test">ESP32-C3 Lua 测试项目</a></li><li><a href="https://github.com/georgik/esp32-sdl3-example">ESP32 SDL3 示例项目</a></li><li><a href="https://github.com/whitecatboard/Lua-RTOS-ESP32">Lua RTOS for ESP32</a></li></ul>]]>
    </content>
    <id>https://bubao.github.io/posts/c95176a6.html</id>
    <link href="https://bubao.github.io/posts/c95176a6.html"/>
    <published>2025-12-05T15:00:00.000Z</published>
    <summary>
      <![CDATA[<p>在嵌入式开发领域，C&#x2F;C++ 一直是主流的编程语言，但随着物联网设备功能越来越复杂，对灵活性的需求也在不断提升。脚本语言因其动态性和易用性，逐渐成为嵌入式系统的重要补充。本文将介绍如何在 ESP-IDF 中集成 Lua 脚本语言，让你的 ESP32 项目具备更强的可配置性和扩展性。</p>]]>
    </summary>
    <title>在 ESP-IDF 中集成 Lua 脚本语言</title>
    <updated>2026-10-31T20:37:58.476Z</updated>
  </entry>
  <entry>
    <author>
      <name>一念</name>
    </author>
    <category term="教程" scheme="https://bubao.github.io/categories/%E6%95%99%E7%A8%8B/"/>
    <category term="开发笔记" scheme="https://bubao.github.io/categories/%E6%95%99%E7%A8%8B/%E5%BC%80%E5%8F%91%E7%AC%94%E8%AE%B0/"/>
    <category term="esp32" scheme="https://bubao.github.io/tags/esp32/"/>
    <category term="IoT" scheme="https://bubao.github.io/tags/IoT/"/>
    <content>
      <![CDATA[<p>你是否曾经想过，如何让你的智能设备知道什么时候该执行某些任务？比如每天早上 7 点自动打开窗帘，每小时检测一次空气质量，或者每周一早上 9 点发送一周总结邮件？</p><p>这些看似复杂的功能其实都有一个共同的核心——定时任务调度。在计算机领域，有一种成熟的技术专门解决这个问题，那就是 CRON。</p><span id="more"></span><h2 id="什么是-CRON？"><a href="#什么是-CRON？" class="headerlink" title="什么是 CRON？"></a>什么是 CRON？</h2><p>CRON 最早出现在 Unix 操作系统中，是一个用于在特定时间执行预定任务的系统工具。这个名字来源于希腊语”chronos”，意思是”时间”。</p><p>你可以把 CRON 想象成一个不知疲倦的秘书，只要你告诉它什么时间做什么事情，它就会准时帮你完成，不需要你每次都提醒。</p><h3 id="CRON-表达式：用符号描述时间"><a href="#CRON-表达式：用符号描述时间" class="headerlink" title="CRON 表达式：用符号描述时间"></a>CRON 表达式：用符号描述时间</h3><p>CRON 最核心的部分是 CRON 表达式，这是一种特殊的语法，用来精确描述任务执行的时间规律。</p><p>传统的 Linux CRON 表达式由 5 个字段组成：</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs txt">┌───────────── 分钟 (0 - 59)<br>│ ┌───────────── 小时 (0 - 23)<br>│ │ ┌───────────── 日期 (1 - 31)<br>│ │ │ ┌───────────── 月份 (1 - 12)<br>│ │ │ │ ┌───────────── 星期 (0 - 7, 周日可以用 0 或 7 表示 )<br>│ │ │ │ │<br>* * * * *<br></code></pre></td></tr></table></figure><p>通过这些字段的不同组合，我们可以描述各种复杂的时间规律：</p><ul><li><code>0 2 * * *</code> 每天凌晨 2 点执行</li><li><code>0 9 * * 1-5</code> 每周一到周五早上 9 点执行</li><li><code>0 0 1 * *</code> 每月 1 号午夜执行</li><li><code>0 */2 * * *</code> 每隔 2 小时执行一次</li><li><code>0 0 * * 0</code> 每周日凌晨执行</li></ul><h2 id="从服务器到嵌入式设备"><a href="#从服务器到嵌入式设备" class="headerlink" title="从服务器到嵌入式设备"></a>从服务器到嵌入式设备</h2><p>虽然 CRON 在服务器领域得到了广泛应用，但在嵌入式系统中却面临挑战。这是因为：</p><ol><li><strong>资源限制</strong>：嵌入式设备通常内存和处理能力有限</li><li><strong>实时性要求</strong>：许多嵌入式应用需要更精确的时间控制</li><li><strong>功耗敏感</strong>：电池供电设备对能耗极其敏感</li><li><strong>环境差异</strong>：嵌入式系统缺少完整的操作系统环境</li></ol><p>特别是在物联网 (IoT) 时代，越来越多的智能设备需要定时执行各种任务，这就催生了在嵌入式系统中实现 CRON 功能的需求。</p><h2 id="ESP32-中的-CRON-解决方案"><a href="#ESP32-中的-CRON-解决方案" class="headerlink" title="ESP32 中的 CRON 解决方案"></a>ESP32 中的 CRON 解决方案</h2><p>ESP32 是一款流行的物联网芯片，具有强大的处理能力和丰富的外设。为了让 ESP32 也能使用熟悉的 CRON 表达式，开发者们创建了专门的解决方案，比如 esp_cron 组件。</p><h3 id="为什么需要增强版-CRON？"><a href="#为什么需要增强版-CRON？" class="headerlink" title="为什么需要增强版 CRON？"></a>为什么需要增强版 CRON？</h3><p>传统的 CRON 表达式只能精确到分钟级别，最小的时间单位是一分钟。在服务器环境中这通常足够了，但在嵌入式系统中，特别是实时控制系统中，我们往往需要更高的时间精度。</p><p>因此，嵌入式系统中的 CRON 实现通常会增加一个秒字段，变成 6 个字段：</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs txt">┌───────────── 秒 (0 - 59)<br>│ ┌───────────── 分钟 (0 - 59)<br>│ │ ┌───────────── 小时 (0 - 23)<br>│ │ │ ┌───────────── 日期 (1 - 31)<br>│ │ │ │ ┌───────────── 月份 (1 - 12)<br>│ │ │ │ │ ┌───────────── 星期 (0 - 7)<br>│ │ │ │ │ │<br>* * * * * *<br></code></pre></td></tr></table></figure><p>这样我们就可以实现每秒执行、每 5 秒执行等更加精细的时间控制。</p><h3 id="Linux-CRON-迁移的关键点"><a href="#Linux-CRON-迁移的关键点" class="headerlink" title="Linux CRON 迁移的关键点"></a>Linux CRON 迁移的关键点</h3><p>如果你熟悉 Linux 下的 CRON，迁移到嵌入式系统时需要注意几个关键差异：</p><h4 id="1-时间精度提升"><a href="#1-时间精度提升" class="headerlink" title="1. 时间精度提升"></a>1. 时间精度提升</h4><p>Linux CRON：</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs txt">0 9 * * *      # 每天早上 9 点整执行<br></code></pre></td></tr></table></figure><p>嵌入式 CRON（增加秒字段）：</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs txt">0 0 9 * * *    # 每天早上 9 点整执行<br>0 * * * * *    # 每分钟执行一次<br></code></pre></td></tr></table></figure><h4 id="2-资源使用的考虑"><a href="#2-资源使用的考虑" class="headerlink" title="2. 资源使用的考虑"></a>2. 资源使用的考虑</h4><p>在服务器上，你可以随意创建大量定时任务而不用担心资源问题。但在嵌入式系统中，你需要考虑：</p><ul><li>内存占用</li><li>CPU 使用率</li><li>功耗消耗</li></ul><h4 id="3-系统环境差异"><a href="#3-系统环境差异" class="headerlink" title="3. 系统环境差异"></a>3. 系统环境差异</h4><p>Linux 系统有完整的文件系统和进程管理，而嵌入式系统通常是裸机运行或运行简化的实时操作系统 (RTOS)。</p><h2 id="实际应用场景"><a href="#实际应用场景" class="headerlink" title="实际应用场景"></a>实际应用场景</h2><p>让我们通过几个实际例子来看看嵌入式 CRON 的强大之处：</p><h3 id="智能家居控制器"><a href="#智能家居控制器" class="headerlink" title="智能家居控制器"></a>智能家居控制器</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 早晨模式：每天早上 7 点开启窗帘和咖啡机</span><br>0 0 7 * * *    curtain_and_coffee_callback<br><br><span class="hljs-comment"># 节能模式：晚上 10 点关闭非必要电器</span><br>0 0 22 * * *   energy_saving_callback<br><br><span class="hljs-comment"># 空气检测：每 10 分钟检测一次室内空气质量</span><br>0 */10 * * * * air_quality_check_callback<br></code></pre></td></tr></table></figure><h3 id="智能农业系统"><a href="#智能农业系统" class="headerlink" title="智能农业系统"></a>智能农业系统</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 浇水任务：根据季节不同调整浇水频率</span><br>0 0 6 * * *    spring_watering_callback    <span class="hljs-comment"># 春季每天早上 6 点</span><br>0 0 5 * * *    summer_watering_callback    <span class="hljs-comment"># 夏季每天早上 5 点</span><br>0 0 7 * * *    autumn_watering_callback    <span class="hljs-comment"># 秋季每天早上 7 点</span><br>0 0 8 * * *    winter_watering_callback    <span class="hljs-comment"># 冬季每天早上 8 点</span><br></code></pre></td></tr></table></figure><h3 id="工业监控设备"><a href="#工业监控设备" class="headerlink" title="工业监控设备"></a>工业监控设备</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 数据采集：工作时间每小时采集一次数据</span><br>0 0 * * * 1-5  work_hour_data_collection<br><br><span class="hljs-comment"># 设备巡检：每天凌晨 2 点进行自检</span><br>0 0 2 * * *    self_diagnostics_callback<br><br><span class="hljs-comment"># 紧急上报：发现异常立即上报</span><br>* * * * * *    emergency_report_callback    <span class="hljs-comment"># 实时监控模式</span><br></code></pre></td></tr></table></figure><h2 id="嵌入式-CRON-的优势"><a href="#嵌入式-CRON-的优势" class="headerlink" title="嵌入式 CRON 的优势"></a>嵌入式 CRON 的优势</h2><p>相比传统的定时器方案，嵌入式 CRON 有以下明显优势：</p><h3 id="1-灵活性更强"><a href="#1-灵活性更强" class="headerlink" title="1. 灵活性更强"></a>1. 灵活性更强</h3><p>传统的定时器通常只能设置固定间隔或特定时间点，而 CRON 表达式可以描述几乎任何复杂的时间规律。</p><h3 id="2-更易理解和维护"><a href="#2-更易理解和维护" class="headerlink" title="2. 更易理解和维护"></a>2. 更易理解和维护</h3><p>CRON 表达式采用直观的时间描述方式，即使是非程序员也能大致理解其含义。</p><h3 id="3-成熟的标准"><a href="#3-成熟的标准" class="headerlink" title="3. 成熟的标准"></a>3. 成熟的标准</h3><p>CRON 已经有几十年的历史，有大量的文档和社区支持，学习成本较低。</p><h3 id="4-跨平台兼容"><a href="#4-跨平台兼容" class="headerlink" title="4. 跨平台兼容"></a>4. 跨平台兼容</h3><p>掌握了 CRON 表达式后，无论是在 Linux 服务器、云平台还是嵌入式设备上，都可以使用相似的方法来安排任务。</p><h2 id="如何开始使用"><a href="#如何开始使用" class="headerlink" title="如何开始使用"></a>如何开始使用</h2><p>如果你想在自己的 ESP32 项目中使用 CRON 功能，可以按照以下步骤进行：</p><ol><li><strong>选择合适的库</strong>：寻找支持 CRON 表达式的第三方库，如 esp_cron</li><li><strong>学习基本语法</strong>：掌握 CRON 表达式的基本写法</li><li><strong>从小项目开始</strong>：先实现简单的定时任务，逐步增加复杂度</li><li><strong>注意资源管理</strong>：合理规划内存和 CPU 使用</li><li><strong>测试和完善</strong>：在实际环境中验证定时任务的准确性</li></ol><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>CRON 作为一种成熟的时间调度机制，从大型服务器成功迁移到了资源受限的嵌入式系统中。通过适当的优化和改进，我们现在可以在 ESP32 这样的微型控制器上使用熟悉的 CRON 表达式来安排各种定时任务。</p><p>无论你是刚刚接触嵌入式开发的新手，还是经验丰富的开发者，掌握 CRON 都将大大提高你在时间调度方面的工作效率。它不仅是一种技术工具，更是一种思维方式——让我们能够以更优雅的方式描述和管理时间相关的任务。</p><p>如果你对在 ESP32 上实现 CRON 功能感兴趣，可以查看 GitHub 上的 <a href="https://github.com/bubao/esp_cron">esp_cron</a> 项目，这是一个专门为 ESP-IDF 框架设计的 CRON 式任务调度组件，可以帮助你快速在项目中实现复杂的时间调度功能。</p><p>希望这篇文章能帮助你理解 CRON 在嵌入式系统中的应用，并激发你在物联网项目中的创造力。记住，时间是我们最宝贵的资源，而 CRON 正是帮助我们更好管理和利用时间的强大工具。</p>]]>
    </content>
    <id>https://bubao.github.io/posts/ff82b1ea.html</id>
    <link href="https://bubao.github.io/posts/ff82b1ea.html"/>
    <published>2025-12-03T02:00:00.000Z</published>
    <summary>
      <![CDATA[<p>你是否曾经想过，如何让你的智能设备知道什么时候该执行某些任务？比如每天早上 7 点自动打开窗帘，每小时检测一次空气质量，或者每周一早上 9 点发送一周总结邮件？</p>
<p>这些看似复杂的功能其实都有一个共同的核心——定时任务调度。在计算机领域，有一种成熟的技术专门解决这个问题，那就是 CRON。</p>]]>
    </summary>
    <title>嵌入式系统中的 CRON 定时任务：从 Linux 到 ESP32</title>
    <updated>2026-10-31T20:37:58.477Z</updated>
  </entry>
  <entry>
    <author>
      <name>一念</name>
    </author>
    <category term="教程" scheme="https://bubao.github.io/categories/%E6%95%99%E7%A8%8B/"/>
    <category term="esp32" scheme="https://bubao.github.io/tags/esp32/"/>
    <category term="esp-idf" scheme="https://bubao.github.io/tags/esp-idf/"/>
    <content>
      <![CDATA[<h2 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h2><p>在物联网（IoT）设备中，LED 控制是一个常见的应用场景。ESP32-S3 开发板提供了强大的 LED PWM 控制器（LEDC），可以用来控制 LED 的亮度以及其他需要 PWM 信号的应用。本文将详细介绍如何使用 ESP-IDF 框架中的 LEDC 驱动程序来控制 LED 的亮度。</p><span id="more"></span><h2 id="环境准备"><a href="#环境准备" class="headerlink" title="环境准备"></a>环境准备</h2><p>在开始之前，请确保你已经安装了 ESP-IDF 开发环境，并且熟悉基本的 ESP32 项目构建流程。如果你还没有安装 ESP-IDF，可以从 <a href="https://docs.espressif.com/projects/esp-idf/en/latest/esp32/get-started/index.html">Espressif 官网</a> 获取安装指南。</p><h2 id="LED-PWM-控制器概述"><a href="#LED-PWM-控制器概述" class="headerlink" title="LED PWM 控制器概述"></a>LED PWM 控制器概述</h2><h3 id="简介"><a href="#简介" class="headerlink" title="简介"></a>简介</h3><p>LED 控制器 (LEDC) 主要用于控制 LED，也可产生 PWM 信号用于其他设备的控制。该控制器有 8 路通道，可以产生独立的波形，驱动 RGB LED 等设备。LEDC PWM 控制器可在无需 CPU 干预的情况下自动改变占空比，实现亮度和颜色渐变。</p><h3 id="功能概览"><a href="#功能概览" class="headerlink" title="功能概览"></a>功能概览</h3><ol><li><p><strong>设置 LEDC 通道</strong>：</p><ul><li>定时器配置：指定 PWM 信号的频率和占空比分辨率。</li><li>通道配置：绑定定时器和输出 PWM 信号的 GPIO。</li><li>改变 PWM 信号：输出 PWM 信号来驱动 LED。可通过软件控制或使用硬件渐变功能来改变 LED 的亮度。</li></ul></li><li><p><strong>电源管理</strong>：</p><ul><li>LEDC 驱动不使用电源管理锁来防止系统进入 Light-sleep。可以通过配置<code>ledc_channel_config_t::sleep_mode</code>来选择 LEDC 外设电源域状态和 PWM 信号在睡眠期间的输出行为。</li></ul></li><li><p><strong>频率和占空比分辨率支持范围</strong>：</p><ul><li>LED PWM 控制器主要用于驱动 LED。该控制器 PWM 占空比设置的分辨率范围较广。例如，PWM 频率为 5 kHz 时，占空比分辨率最大可为 13 位。然而，这些参数取决于为 LEDC 定时器计时的时钟信号。</li></ul></li></ol><h2 id="示例代码"><a href="#示例代码" class="headerlink" title="示例代码"></a>示例代码</h2><p>我们将使用 ESP-IDF 提供的示例代码来演示如何使用 LEDC 驱动程序控制 LED 的亮度。以下是完整的示例代码：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;driver/ledc.h&quot;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;esp_log.h&quot;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;freertos/FreeRTOS.h&quot;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;freertos/task.h&quot;</span></span><br><br><span class="hljs-type">static</span> <span class="hljs-type">const</span> <span class="hljs-type">char</span>* TAG = <span class="hljs-string">&quot;example&quot;</span>;<br><br><span class="hljs-comment">// LEDC 通道配置</span><br><span class="hljs-meta">#<span class="hljs-keyword">define</span> LEDC_TIMER              LEDC_TIMER_0</span><br><span class="hljs-meta">#<span class="hljs-keyword">define</span> LEDC_MODE               LEDC_LOW_SPEED_MODE</span><br><span class="hljs-meta">#<span class="hljs-keyword">define</span> LEDC_OUTPUT_IO          (2)       <span class="hljs-comment">// 根据你的硬件连接修改 GPIO</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">define</span> LEDC_CHANNEL            LEDC_CHANNEL_0</span><br><span class="hljs-meta">#<span class="hljs-keyword">define</span> LEDC_DUTY_RESOLUTION    10        <span class="hljs-comment">// 占空比分辨率</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">define</span> LEDC_FREQUENCY          (5000)    <span class="hljs-comment">// PWM 频率 (Hz)</span></span><br><br><span class="hljs-type">void</span> <span class="hljs-title function_">app_main</span><span class="hljs-params">(<span class="hljs-type">void</span>)</span><br>&#123;<br>    <span class="hljs-comment">// 配置 LEDC 定时器</span><br>    <span class="hljs-type">ledc_timer_config_t</span> ledc_timer = &#123;<br>        .speed_mode       = LEDC_MODE,<br>        .timer_num        = LEDC_TIMER,<br>        .duty_resolution  = LEDC_DUTY_RESOLUTION,<br>        .freq_hz          = LEDC_FREQUENCY,<br>        .clk_cfg          = LEDC_AUTO_CLK,<br>    &#125;;<br>    ESP_ERROR_CHECK(ledc_timer_config(&amp;ledc_timer));<br><br>    <span class="hljs-comment">// 配置 LEDC 通道</span><br>    <span class="hljs-type">ledc_channel_config_t</span> ledc_channel = &#123;<br>        .speed_mode     = LEDC_MODE,<br>        .channel        = LEDC_CHANNEL,<br>        .timer_sel      = LEDC_TIMER,<br>        .intr_type      = LEDC_INTR_DISABLE,<br>        .gpio_num       = LEDC_OUTPUT_IO,<br>        .duty           = <span class="hljs-number">0</span>, <span class="hljs-comment">// 初始占空比</span><br>        .hpoint         = <span class="hljs-number">0</span>,<br>        .flags          = <span class="hljs-number">0</span>,<br>    &#125;;<br>    ESP_ERROR_CHECK(ledc_channel_config(&amp;ledc_channel));<br><br>    <span class="hljs-comment">// 循环改变 LED 亮度</span><br>    <span class="hljs-keyword">while</span> (<span class="hljs-number">1</span>) &#123;<br>        <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> duty = <span class="hljs-number">0</span>; duty &lt;= <span class="hljs-number">1023</span>; duty++) &#123;<br>            ESP_ERROR_CHECK(ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, duty));<br>            ESP_ERROR_CHECK(ledc_update_duty(LEDC_MODE, LEDC_CHANNEL));<br>            vTaskDelay(pdMS_TO_TICKS(<span class="hljs-number">10</span>));<br>        &#125;<br>        <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> duty = <span class="hljs-number">1023</span>; duty &gt;= <span class="hljs-number">0</span>; duty--) &#123;<br>            ESP_ERROR_CHECK(ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, duty));<br>            ESP_ERROR_CHECK(ledc_update_duty(LEDC_MODE, LEDC_CHANNEL));<br>            vTaskDelay(pdMS_TO_TICKS(<span class="hljs-number">10</span>));<br>        &#125;<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><h2 id="代码解析"><a href="#代码解析" class="headerlink" title="代码解析"></a>代码解析</h2><h3 id="1-包含头文件"><a href="#1-包含头文件" class="headerlink" title="1. 包含头文件"></a>1. 包含头文件</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;driver/ledc.h&quot;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;esp_log.h&quot;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;freertos/FreeRTOS.h&quot;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;freertos/task.h&quot;</span></span><br></code></pre></td></tr></table></figure><ul><li><code>driver/ledc.h</code>：包含 LEDC 驱动程序的 API 声明。</li><li><code>esp_log.h</code>：包含日志记录功能。</li><li><code>freertos/FreeRTOS.h</code> 和 <code>freertos/task.h</code>：包含 FreeRTOS 任务管理功能。</li></ul><h3 id="2-定义日志标签"><a href="#2-定义日志标签" class="headerlink" title="2. 定义日志标签"></a>2. 定义日志标签</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-type">static</span> <span class="hljs-type">const</span> <span class="hljs-type">char</span>* TAG = <span class="hljs-string">&quot;example&quot;</span>;<br></code></pre></td></tr></table></figure><ul><li>定义了一个日志标签，用于标识日志信息的来源。</li></ul><h3 id="3-定义-LEDC-配置参数"><a href="#3-定义-LEDC-配置参数" class="headerlink" title="3. 定义 LEDC 配置参数"></a>3. 定义 LEDC 配置参数</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">define</span> LEDC_TIMER              LEDC_TIMER_0</span><br><span class="hljs-meta">#<span class="hljs-keyword">define</span> LEDC_MODE               LEDC_LOW_SPEED_MODE</span><br><span class="hljs-meta">#<span class="hljs-keyword">define</span> LEDC_OUTPUT_IO          (2)       <span class="hljs-comment">// 根据你的硬件连接修改 GPIO</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">define</span> LEDC_CHANNEL            LEDC_CHANNEL_0</span><br><span class="hljs-meta">#<span class="hljs-keyword">define</span> LEDC_DUTY_RESOLUTION    10        <span class="hljs-comment">// 占空比分辨率</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">define</span> LEDC_FREQUENCY          (5000)    <span class="hljs-comment">// PWM 频率 (Hz)</span></span><br></code></pre></td></tr></table></figure><ul><li><code>LEDC_TIMER</code>：使用的定时器编号。</li><li><code>LEDC_MODE</code>：速度模式，ESP32-S3 仅支持低速模式。</li><li><code>LEDC_OUTPUT_IO</code>：输出 PWM 信号的 GPIO 编号。</li><li><code>LEDC_CHANNEL</code>：使用的通道编号。</li><li><code>LEDC_DUTY_RESOLUTION</code>：占空比分辨率。</li><li><code>LEDC_FREQUENCY</code>：PWM 信号的频率。</li></ul><h3 id="4-配置-LEDC-定时器"><a href="#4-配置-LEDC-定时器" class="headerlink" title="4. 配置 LEDC 定时器"></a>4. 配置 LEDC 定时器</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-type">ledc_timer_config_t</span> ledc_timer = &#123;<br>    .speed_mode       = LEDC_MODE,<br>    .timer_num        = LEDC_TIMER,<br>    .duty_resolution  = LEDC_DUTY_RESOLUTION,<br>    .freq_hz          = LEDC_FREQUENCY,<br>    .clk_cfg          = LEDC_AUTO_CLK,<br>&#125;;<br>ESP_ERROR_CHECK(ledc_timer_config(&amp;ledc_timer));<br></code></pre></td></tr></table></figure><ul><li>创建一个<code>ledc_timer_config_t</code>结构体，并设置定时器参数。</li><li>使用<code>ledc_timer_config</code>函数配置定时器。</li></ul><h3 id="5-配置-LEDC-通道"><a href="#5-配置-LEDC-通道" class="headerlink" title="5. 配置 LEDC 通道"></a>5. 配置 LEDC 通道</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-type">ledc_channel_config_t</span> ledc_channel = &#123;<br>    .speed_mode     = LEDC_MODE,<br>    .channel        = LEDC_CHANNEL,<br>    .timer_sel      = LEDC_TIMER,<br>    .intr_type      = LEDC_INTR_DISABLE,<br>    .gpio_num       = LEDC_OUTPUT_IO,<br>    .duty           = <span class="hljs-number">0</span>, <span class="hljs-comment">// 初始占空比</span><br>    .hpoint         = <span class="hljs-number">0</span>,<br>    .flags          = <span class="hljs-number">0</span>,<br>&#125;;<br>ESP_ERROR_CHECK(ledc_channel_config(&amp;ledc_channel));<br></code></pre></td></tr></table></figure><ul><li>创建一个<code>ledc_channel_config_t</code>结构体，并设置通道参数。</li><li>使用<code>ledc_channel_config</code>函数配置通道。</li></ul><h3 id="6-循环改变-LED-亮度"><a href="#6-循环改变-LED-亮度" class="headerlink" title="6. 循环改变 LED 亮度"></a>6. 循环改变 LED 亮度</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-keyword">while</span> (<span class="hljs-number">1</span>) &#123;<br>    <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> duty = <span class="hljs-number">0</span>; duty &lt;= <span class="hljs-number">1023</span>; duty++) &#123;<br>        ESP_ERROR_CHECK(ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, duty));<br>        ESP_ERROR_CHECK(ledc_update_duty(LEDC_MODE, LEDC_CHANNEL));<br>        vTaskDelay(pdMS_TO_TICKS(<span class="hljs-number">10</span>));<br>    &#125;<br>    <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> duty = <span class="hljs-number">1023</span>; duty &gt;= <span class="hljs-number">0</span>; duty--) &#123;<br>        ESP_ERROR_CHECK(ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, duty));<br>        ESP_ERROR_CHECK(ledc_update_duty(LEDC_MODE, LEDC_CHANNEL));<br>        vTaskDelay(pdMS_TO_TICKS(<span class="hljs-number">10</span>));<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><ul><li>在一个无限循环中，逐步增加和减少占空比，从而实现 LED 亮度的渐变。</li><li>使用<code>ledc_set_duty</code>设置新的占空比，并使用<code>ledc_update_duty</code>使新配置生效。</li><li>使用<code>vTaskDelay</code>设置任务延迟，控制亮度变化的速度。</li></ul><h2 id="使用中断"><a href="#使用中断" class="headerlink" title="使用中断"></a>使用中断</h2><p>LEDC 驱动还可以配置中断，在渐变完成时触发中断。以下是一个简单的示例，展示如何注册中断处理程序：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-type">void</span> IRAM_ATTR <span class="hljs-title function_">ledc_fade_isr</span><span class="hljs-params">(<span class="hljs-type">void</span>* arg)</span><br>&#123;<br>    BaseType_t xHigherPriorityTaskWoken = pdFALSE;<br>    <span class="hljs-comment">// 处理中断事件</span><br>    ESP_LOGI(TAG, <span class="hljs-string">&quot;Fade complete&quot;</span>);<br>    portYIELD_FROM_ISR();<br>&#125;<br><br><span class="hljs-type">void</span> <span class="hljs-title function_">app_main</span><span class="hljs-params">(<span class="hljs-type">void</span>)</span><br>&#123;<br>    <span class="hljs-comment">// 配置 LEDC 定时器和通道（同上）</span><br><br>    <span class="hljs-comment">// 注册中断处理程序</span><br>    ledc_isr_register(ledc_fade_isr, <span class="hljs-literal">NULL</span>, ESP_INTR_FLAG_IRAM, <span class="hljs-literal">NULL</span>);<br><br>    <span class="hljs-comment">// 启用渐变功能</span><br>    ledc_fade_func_install(<span class="hljs-number">0</span>);<br><br>    <span class="hljs-comment">// 设置渐变参数并启动渐变</span><br>    ledc_set_fade_with_time(LEDC_MODE, LEDC_CHANNEL, <span class="hljs-number">1023</span>, <span class="hljs-number">1000</span>);<br>    ledc_fade_start(LEDC_MODE, LEDC_CHANNEL, LEDC_FADE_NO_WAIT);<br>&#125;<br></code></pre></td></tr></table></figure><h3 id="7-注册中断处理程序"><a href="#7-注册中断处理程序" class="headerlink" title="7. 注册中断处理程序"></a>7. 注册中断处理程序</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs c">ledc_isr_register(ledc_fade_isr, <span class="hljs-literal">NULL</span>, ESP_INTR_FLAG_IRAM, <span class="hljs-literal">NULL</span>);<br></code></pre></td></tr></table></figure><ul><li>使用<code>ledc_isr_register</code>函数注册中断处理程序。</li><li><code>ledc_fade_isr</code>：中断处理函数。</li><li><code>ESP_INTR_FLAG_IRAM</code>：确保中断处理函数在 IRAM 中执行。</li></ul><h3 id="8-启用渐变功能"><a href="#8-启用渐变功能" class="headerlink" title="8. 启用渐变功能"></a>8. 启用渐变功能</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs c">ledc_fade_func_install(<span class="hljs-number">0</span>);<br></code></pre></td></tr></table></figure><ul><li>使用<code>ledc_fade_func_install</code>函数启用渐变功能。</li></ul><h3 id="9-设置渐变参数并启动渐变"><a href="#9-设置渐变参数并启动渐变" class="headerlink" title="9. 设置渐变参数并启动渐变"></a>9. 设置渐变参数并启动渐变</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs c">ledc_set_fade_with_time(LEDC_MODE, LEDC_CHANNEL, <span class="hljs-number">1023</span>, <span class="hljs-number">1000</span>);<br>ledc_fade_start(LEDC_MODE, LEDC_CHANNEL, LEDC_FADE_NO_WAIT);<br></code></pre></td></tr></table></figure><ul><li>使用<code>ledc_set_fade_with_time</code>函数设置渐变参数。</li><li>使用<code>ledc_fade_start</code>函数启动渐变。</li></ul><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>通过上述代码，我们可以轻松地在 ESP32-S3 上实现对 LED 的 PWM 控制，从而控制 LED 的亮度。这个示例展示了如何使用 ESP-IDF 提供的 LEDC 驱动程序进行基本的操作，包括定时器配置、通道配置和改变 PWM 信号。你可以根据实际需求，进一步扩展这个示例，比如控制多个 LED 或实现更复杂的渐变效果。</p><p>希望这篇博客笔记对你有所帮助！如果你有任何问题或建议，欢迎在评论区留言。</p><h2 id="参考文档"><a href="#参考文档" class="headerlink" title="参考文档"></a>参考文档</h2><ul><li><a href="https://docs.espressif.com/projects/esp-idf/zh_CN/stable/esp32s3/api-reference/peripherals/ledc.html">LED PWM 控制器 - ESP32-S3 - — ESP-IDF 编程指南 v5.4.1 文档</a></li></ul>]]>
    </content>
    <id>https://bubao.github.io/posts/11756890.html</id>
    <link href="https://bubao.github.io/posts/11756890.html"/>
    <published>2025-05-17T23:20:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h2><p>在物联网（IoT）设备中，LED 控制是一个常见的应用场景。ESP32-S3 开发板提供了强大的 LED PWM 控制器（LEDC），可以用来控制 LED 的亮度以及其他需要 PWM 信号的应用。本文将详细介绍如何使用 ESP-IDF 框架中的 LEDC 驱动程序来控制 LED 的亮度。</p>]]>
    </summary>
    <title>使用 ESP32-S3 的 LED PWM 控制器控制 LED 亮度</title>
    <updated>2026-10-31T20:37:58.474Z</updated>
  </entry>
  <entry>
    <author>
      <name>一念</name>
    </author>
    <category term="教程" scheme="https://bubao.github.io/categories/%E6%95%99%E7%A8%8B/"/>
    <category term="esp32" scheme="https://bubao.github.io/tags/esp32/"/>
    <category term="esp-idf" scheme="https://bubao.github.io/tags/esp-idf/"/>
    <content>
      <![CDATA[<h2 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h2><p>在物联网（IoT）设备中，环境温度监测是一个常见的需求。ESP32 开发板集成了一个内部温度传感器，可以方便地用于温度监测应用。了解如何使用 ESP-IDF 框架中的<code>temperature_sensor</code>驱动程序来读取 ESP32 的内部温度传感器数据。</p><span id="more"></span><h2 id="示例代码"><a href="#示例代码" class="headerlink" title="示例代码"></a>示例代码</h2><p>我们将使用 ESP-IDF 提供的示例代码来演示如何读取 ESP32 的内部温度传感器数据。以下是完整的示例代码：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;driver/temperature_sensor.h&quot;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;esp_log.h&quot;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;freertos/FreeRTOS.h&quot;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;freertos/task.h&quot;</span></span><br><br><span class="hljs-type">static</span> <span class="hljs-type">const</span> <span class="hljs-type">char</span>* TAG = <span class="hljs-string">&quot;example&quot;</span>;<br><br><span class="hljs-type">void</span> <span class="hljs-title function_">app_main</span><span class="hljs-params">(<span class="hljs-type">void</span>)</span><br>&#123;<br>  <span class="hljs-comment">// 初始化温度传感器，设置预期的温度范围为 10~50℃</span><br>  ESP_LOGI(TAG, <span class="hljs-string">&quot;Install temperature sensor, expected temp range: 10~50 ℃&quot;</span>);<br>  <span class="hljs-type">temperature_sensor_handle_t</span> temp_sensor = <span class="hljs-literal">NULL</span>;<br>  <span class="hljs-type">temperature_sensor_config_t</span> temp_sensor_config = TEMPERATURE_SENSOR_CONFIG_DEFAULT(<span class="hljs-number">10</span>, <span class="hljs-number">50</span>);<br>  ESP_ERROR_CHECK(temperature_sensor_install(&amp;temp_sensor_config, &amp;temp_sensor));<br><br>  <span class="hljs-comment">// 启用温度传感器</span><br>  ESP_LOGI(TAG, <span class="hljs-string">&quot;Enable temperature sensor&quot;</span>);<br>  ESP_ERROR_CHECK(temperature_sensor_enable(temp_sensor));<br><br>  <span class="hljs-comment">// 读取温度值并打印</span><br>  ESP_LOGI(TAG, <span class="hljs-string">&quot;Read temperature&quot;</span>);<br>  <span class="hljs-type">float</span> tsens_value;<br>  <span class="hljs-keyword">while</span> (<span class="hljs-number">1</span>) &#123; <span class="hljs-comment">// 官网这里只循环了 20 次</span><br>      ESP_ERROR_CHECK(temperature_sensor_get_celsius(temp_sensor, &amp;tsens_value));<br>      ESP_LOGI(TAG, <span class="hljs-string">&quot;Temperature value %.02f ℃&quot;</span>, tsens_value);<br>      vTaskDelay(pdMS_TO_TICKS(<span class="hljs-number">1000</span>)); <span class="hljs-comment">// 每秒读取一次</span><br>  &#125;<br>&#125;<br></code></pre></td></tr></table></figure><h2 id="代码解析"><a href="#代码解析" class="headerlink" title="代码解析"></a>代码解析</h2><h3 id="1-包含头文件"><a href="#1-包含头文件" class="headerlink" title="1. 包含头文件"></a>1. 包含头文件</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;driver/temperature_sensor.h&quot;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;esp_log.h&quot;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;freertos/FreeRTOS.h&quot;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;freertos/task.h&quot;</span></span><br></code></pre></td></tr></table></figure><p><code>driver/temperature_sensor.h</code>：包含温度传感器驱动程序的 API 声明。</p><p><code>esp_log.h</code>：包含日志记录功能。</p><p><code>freertos/FreeRTOS.h</code> 和 <code>freertos/task.h</code>：包含 FreeRTOS 任务管理功能。</p><h3 id="2-定义日志标签"><a href="#2-定义日志标签" class="headerlink" title="2. 定义日志标签"></a>2. 定义日志标签</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-type">static</span> <span class="hljs-type">const</span> <span class="hljs-type">char</span>* TAG = <span class="hljs-string">&quot;example&quot;</span>;<br></code></pre></td></tr></table></figure><p>定义了一个日志标签，用于标识日志信息的来源。</p><h3 id="3-初始化温度传感器"><a href="#3-初始化温度传感器" class="headerlink" title="3. 初始化温度传感器"></a>3. 初始化温度传感器</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs c">ESP_LOGI(TAG, <span class="hljs-string">&quot;Install temperature sensor, expected temp range: 10~50 ℃&quot;</span>);<br><span class="hljs-type">temperature_sensor_handle_t</span> temp_sensor = <span class="hljs-literal">NULL</span>;<br><span class="hljs-type">temperature_sensor_config_t</span> temp_sensor_config = TEMPERATURE_SENSOR_CONFIG_DEFAULT(<span class="hljs-number">10</span>, <span class="hljs-number">50</span>);<br>ESP_ERROR_CHECK(temperature_sensor_install(&amp;temp_sensor_config, &amp;temp_sensor));<br></code></pre></td></tr></table></figure><p><code>temperature_sensor_config_t temp_sensor_config = TEMPERATURE_SENSOR_CONFIG_DEFAULT(10, 50);</code>：创建一个默认的温度传感器配置结构体，并设置预期的温度范围为 10 到 50 摄氏度。</p><p><code>temperature_sensor_install(&amp;temp_sensor_config, &amp;temp_sensor);</code>：根据配置初始化温度传感器，并获取传感器句柄。</p><h3 id="4-启用温度传感器"><a href="#4-启用温度传感器" class="headerlink" title="4. 启用温度传感器"></a>4. 启用温度传感器</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs c">ESP_LOGI(TAG, <span class="hljs-string">&quot;Enable temperature sensor&quot;</span>);<br>ESP_ERROR_CHECK(temperature_sensor_enable(temp_sensor));<br></code></pre></td></tr></table></figure><p><code>temperature_sensor_enable(temp_sensor);</code>：启用已初始化的温度传感器。</p><h3 id="5-读取温度值"><a href="#5-读取温度值" class="headerlink" title="5. 读取温度值"></a>5. 读取温度值</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs c">ESP_LOGI(TAG, <span class="hljs-string">&quot;Read temperature&quot;</span>);<br><span class="hljs-type">float</span> tsens_value;<br><span class="hljs-keyword">while</span> (<span class="hljs-number">1</span>) &#123;<br>  ESP_ERROR_CHECK(temperature_sensor_get_celsius(temp_sensor, &amp;tsens_value));<br>  ESP_LOGI(TAG, <span class="hljs-string">&quot;Temperature value %.02f ℃&quot;</span>, tsens_value);<br>  vTaskDelay(pdMS_TO_TICKS(<span class="hljs-number">1000</span>)); <span class="hljs-comment">// 每秒读取一次</span><br>&#125;<br></code></pre></td></tr></table></figure><p>在一个无限循环中，使用<code>temperature_sensor_get_celsius</code>函数读取当前温度值，并通过<code>ESP_LOGI</code>打印出来。</p><p><code>vTaskDelay(pdMS_TO_TICKS(1000));</code>：设置任务延迟 1 秒，以便每秒读取一次温度。</p><h2 id="温度传感器概述"><a href="#温度传感器概述" class="headerlink" title="温度传感器概述"></a>温度传感器概述</h2><h3 id="简介"><a href="#简介" class="headerlink" title="简介"></a>简介</h3><p>ESP32 内置传感器，用于测量芯片内部的温度。该温度传感器模组包含一个 8 位 Sigma-Delta 模拟-数字转换器（ADC）和一个数字-模拟转换器（DAC），可以补偿测量结果，减少温度测量的误差。</p><h3 id="预定义测量范围及误差"><a href="#预定义测量范围及误差" class="headerlink" title="预定义测量范围及误差"></a>预定义测量范围及误差</h3><table><thead><tr><th>预定义测量范围 (°C)</th><th>测量误差 (°C)</th></tr></thead><tbody><tr><td>50 ~ 125</td><td>&lt; 3</td></tr><tr><td>20 ~ 100</td><td>&lt; 2</td></tr><tr><td>-10 ~ 80</td><td>&lt; 1</td></tr><tr><td>-30 ~ 50</td><td>&lt; 2</td></tr><tr><td>-40 ~ 20</td><td>&lt; 3</td></tr></tbody></table><h3 id="功能概述"><a href="#功能概述" class="headerlink" title="功能概述"></a>功能概述</h3><ol><li>资源分配：</li></ol><p>通过<code>temperature_sensor_config_t</code>结构体设置温度范围。</p><p>使用<code>temperature_sensor_install</code>函数安装温度传感器，并获取传感器句柄。</p><ol start="2"><li>启用及禁用温度传感器：</li></ol><p>使用<code>temperature_sensor_enable</code>启用温度传感器。</p><p>使用<code>temperature_sensor_disable</code>禁用温度传感器。</p><ol start="3"><li>获取测量的温度值：</li></ol><p>使用<code>temperature_sensor_get_celsius</code>获取实时温度值。</p><ol start="4"><li>电源管理：</li></ol><p>温度传感器不使用 APB 时钟，无论是否激活电源管理，温度传感器仍可以继续工作。</p><ol start="5"><li>线程安全：</li></ol><p>温度传感器驱动程序通常只在一个任务中调用，如果要在不同任务中使用，需要设置额外的锁进行保护。</p><h2 id="注意事项"><a href="#注意事项" class="headerlink" title="注意事项"></a>注意事项</h2><p>温度传感器测量的是芯片内部的温度，而不是环境温度。</p><p>安装温度传感器失败时，可能会出现错误信息<code>the boundary you gave cannot meet the range of internal temperature sensor</code>，这通常是由于配置的温度范围超出了预定义的测量范围。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>通过上述代码，我们可以轻松地在 ESP32 上实现对内部温度传感器的读取。这个示例展示了如何使用 ESP-IDF 提供的温度传感器驱动程序进行基本的操作，包括初始化、启用和读取温度值。你可以根据实际需求，进一步扩展这个示例，比如将温度数据发送到云端或触发特定的动作。</p><p>希望这篇博客笔记对你有所帮助！如果你有任何问题或建议，欢迎在评论区留言。</p><h2 id="相关参考"><a href="#相关参考" class="headerlink" title="相关参考"></a>相关参考</h2><ul><li><a href="https://docs.espressif.com/projects/esp-idf/zh_CN/stable/esp32s3/api-reference/peripherals/temp_sensor.html">温度传感器</a></li><li><a href="https://github.com/espressif/esp-idf/blob/v5.4.1/examples/peripherals/temperature_sensor/temp_sensor/main/temp_sensor_main.c"><strong>temp_sensor_main.c</strong></a></li></ul>]]>
    </content>
    <id>https://bubao.github.io/posts/c55d1163.html</id>
    <link href="https://bubao.github.io/posts/c55d1163.html"/>
    <published>2025-05-17T22:37:37.000Z</published>
    <summary>
      <![CDATA[<h2 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h2><p>在物联网（IoT）设备中，环境温度监测是一个常见的需求。ESP32 开发板集成了一个内部温度传感器，可以方便地用于温度监测应用。了解如何使用 ESP-IDF 框架中的<code>temperature_sensor</code>驱动程序来读取 ESP32 的内部温度传感器数据。</p>]]>
    </summary>
    <title>使用 ESP32 内置温度传感器读取温度值</title>
    <updated>2026-10-31T20:37:58.474Z</updated>
  </entry>
  <entry>
    <author>
      <name>一念</name>
    </author>
    <category term="开发笔记" scheme="https://bubao.github.io/categories/%E5%BC%80%E5%8F%91%E7%AC%94%E8%AE%B0/"/>
    <category term="硬件" scheme="https://bubao.github.io/tags/%E7%A1%AC%E4%BB%B6/"/>
    <content>
      <![CDATA[<p>28BYJ-48 是一款常见的五线四相小型步进电机，广泛应用于 Arduino 、 STM32 等微控制器平台的教学和实验项目中。以下是对该电机的详细讲解，包括其结构、工作原理、控制方式以及控制原理图。</p><span id="more"></span><h2 id="一、28BYJ-48-步进电机简介"><a href="#一、28BYJ-48-步进电机简介" class="headerlink" title="一、28BYJ-48 步进电机简介"></a>一、28BYJ-48 步进电机简介</h2><table><thead><tr><th>型号</th><th>28BYJ48</th></tr></thead><tbody><tr><td>工作电压</td><td>5V</td></tr><tr><td>直径</td><td>28mm</td></tr><tr><td>减速比</td><td>1:64</td></tr><tr><td>驱动方式</td><td>ULN2003</td></tr><tr><td>相电阻</td><td>50±10%</td></tr><tr><td>步进角度</td><td>5.625°&#x2F;64</td></tr></tbody></table><p>型号含义：</p><ul><li>28：电机的最大外径为 28 毫米</li><li>B：表示步进电机（Stepper Motor）</li><li>Y：表示永磁式（Permanent Magnet）</li><li>J：表示减速型（Geared）</li><li>48：表示四相八拍</li></ul><p>主要参数：</p><ul><li>工作电压：5V</li><li>步进角度：5.625°&#x2F;步（未减速）</li><li>减速比：1:64</li><li>每圈步数：512 步（8 拍模式）</li></ul><h2 id="二、结构与工作原理"><a href="#二、结构与工作原理" class="headerlink" title="二、结构与工作原理"></a>二、结构与工作原理</h2><p>28BYJ-48 是一种单极性五线四相步进电机，内部包含四个绕组（A、B、C、D），通过 ULN2003 驱动模块控制。电机的五根引线分别为：</p><ul><li>红色：公共电源线（VCC）</li><li>蓝色、粉色、黄色、橙色：对应四个相位的控制线</li></ul><p>电机内部通过齿轮减速机构实现高扭矩输出，适合低速精密控制。每次通电一个相位，电机转子会转动一个固定的角度，称为“步距角”。通过控制通电的相位顺序，可以实现电机的正转和反转。</p><h2 id="三、控制方式"><a href="#三、控制方式" class="headerlink" title="三、控制方式"></a>三、控制方式</h2><p>28BYJ-48 步进电机常用的控制方式有：</p><ul><li>单相激励（Single-Coil Excitation）：每次仅通电一个相位，功耗低，但扭矩小。</li><li>双相激励（Dual-Coil Excitation）：每次同时通电两个相位，扭矩大，功耗高。</li><li>混合激励（Half-Step Excitation）：交替进行单相和双相激励，步进角度减半，运行更平稳。</li></ul><p>控制电机的转向只需改变相位的通电顺序。例如，顺时针顺序为 A→B→C→D，逆时针则为 D→C→B→A。</p><h2 id="四、应用与注意事项"><a href="#四、应用与注意事项" class="headerlink" title="四、应用与注意事项"></a>四、应用与注意事项</h2><p>应用领域：</p><ul><li>自动控制系统</li><li>打印机、绘图仪</li><li>机器人</li><li>摄像头云台</li><li>智能家居设备</li></ul><p>注意事项：</p><ol><li>确保电源电压稳定在 5V，防止电压波动影响电机性能。</li><li>控制信号的频率不宜过高，避免电机失步或过热。</li><li>在使用 ULN2003 驱动模块时，注意其最大电流限制，避免过载。</li></ol><h2 id="相关"><a href="#相关" class="headerlink" title="相关"></a>相关</h2><p><a href="https://github.com/bubao/esp32-step-motor">esp32-step-motor</a></p>]]>
    </content>
    <id>https://bubao.github.io/posts/b82a2466.html</id>
    <link href="https://bubao.github.io/posts/b82a2466.html"/>
    <published>2025-05-11T08:13:18.000Z</published>
    <summary>
      <![CDATA[<p>28BYJ-48 是一款常见的五线四相小型步进电机，广泛应用于 Arduino 、 STM32 等微控制器平台的教学和实验项目中。以下是对该电机的详细讲解，包括其结构、工作原理、控制方式以及控制原理图。</p>]]>
    </summary>
    <title>28BYJ-48 步进电机</title>
    <updated>2026-10-31T20:37:58.466Z</updated>
  </entry>
  <entry>
    <author>
      <name>一念</name>
    </author>
    <category term="开发笔记" scheme="https://bubao.github.io/categories/%E5%BC%80%E5%8F%91%E7%AC%94%E8%AE%B0/"/>
    <category term="硬件" scheme="https://bubao.github.io/tags/%E7%A1%AC%E4%BB%B6/"/>
    <content>
      <![CDATA[<h2 id="简介"><a href="#简介" class="headerlink" title="简介"></a>简介</h2><p>EC11 编码器 是一种常用的旋转式机械编码器，广泛用于嵌入式系统、电子产品中的人机交互（如音量调节、菜单选择等）。</p><ul><li>类型：增量式旋转编码器</li><li>输出：双路脉冲信号（A 相和 B 相），带按键（开关）功能</li><li>常见型号：EC11E15244G1、EC11E18244A8 等</li></ul><span id="more"></span><h2 id="工作原理"><a href="#工作原理" class="headerlink" title="工作原理"></a>工作原理</h2><p>EC11 是一种机械式增量编码器，主要通过内部触点的通断组合来产生 A 相与 B 相信号：</p><p>顺时针旋转：A 相先于 B 相变化</p><p>逆时针旋转：B 相先于 A 相变化</p><h3 id="相位波形"><a href="#相位波形" class="headerlink" title="相位波形"></a>相位波形</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs text">顺时针 (CW)：<br>A: ──▁▁────▁▁────<br>B: ─▁▁────▁▁─────<br><br>逆时针 (CCW)：<br>A: ─▁▁────▁▁─────<br>B: ──▁▁────▁▁────<br></code></pre></td></tr></table></figure><p>每一次完整的点击（”咔哒”声）通常包含多个跳变（例如 2 个或 4 个脉冲），取决于内部结构。</p><h2 id="引脚说明"><a href="#引脚说明" class="headerlink" title="引脚说明"></a>引脚说明</h2><p>标准 EC11 编码器共有 5 个引脚，分布如下：</p><table><thead><tr><th>引脚编号</th><th>功能</th><th>说明</th></tr></thead><tbody><tr><td>1</td><td>SW（按键）</td><td>下压后闭合</td></tr><tr><td>2</td><td>GND（地）</td><td>通常与 MCU 的 GND 相接</td></tr><tr><td>3</td><td>DT（B 相）</td><td>编码器信号输出</td></tr><tr><td>4</td><td>CLK（A 相）</td><td>编码器信号输出</td></tr><tr><td>5</td><td>+ 按键公共端</td><td>通常接上拉电阻或 VCC</td></tr></tbody></table><blockquote><p>注意：有的模块使用 SW、CLK、DT 标记，有的用 A、B 表示信号输出线。</p></blockquote><h2 id="接线示例（以-Arduino-为例）"><a href="#接线示例（以-Arduino-为例）" class="headerlink" title="接线示例（以 Arduino 为例）"></a>接线示例（以 Arduino 为例）</h2><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs txt">EC11 编码器引脚  -&gt; Arduino<br>CLK（A 相）      -&gt; D2<br>DT（B 相）       -&gt; D3<br>SW（按键）       -&gt; D4<br>GND              -&gt; GND<br>+                -&gt; 5V（或通过上拉）<br></code></pre></td></tr></table></figure><h2 id="代码示例（Arduino）"><a href="#代码示例（Arduino）" class="headerlink" title="代码示例（Arduino）"></a>代码示例（Arduino）</h2><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-meta">#<span class="hljs-keyword">define</span> CLK 2</span><br><span class="hljs-meta">#<span class="hljs-keyword">define</span> DT 3</span><br><span class="hljs-meta">#<span class="hljs-keyword">define</span> SW 4</span><br><br><span class="hljs-type">int</span> lastStateCLK;<br><span class="hljs-type">int</span> counter = <span class="hljs-number">0</span>;<br><br><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">setup</span><span class="hljs-params">()</span> </span>&#123;<br><span class="hljs-built_in">pinMode</span>(CLK, INPUT);<br><span class="hljs-built_in">pinMode</span>(DT, INPUT);<br><span class="hljs-built_in">pinMode</span>(SW, INPUT_PULLUP);<br><br>lastStateCLK = <span class="hljs-built_in">digitalRead</span>(CLK);<br>Serial.<span class="hljs-built_in">begin</span>(<span class="hljs-number">9600</span>);<br>&#125;<br><br><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">loop</span><span class="hljs-params">()</span> </span>&#123;<br><span class="hljs-type">int</span> currentStateCLK = <span class="hljs-built_in">digitalRead</span>(CLK);<br><br><span class="hljs-keyword">if</span> (currentStateCLK != lastStateCLK) &#123;<br>  <span class="hljs-keyword">if</span> (<span class="hljs-built_in">digitalRead</span>(DT) != currentStateCLK) &#123;<br>    counter++;<br>  &#125; <span class="hljs-keyword">else</span> &#123;<br>    counter--;<br>  &#125;<br>  Serial.<span class="hljs-built_in">println</span>(counter);<br>&#125;<br><br><span class="hljs-keyword">if</span> (<span class="hljs-built_in">digitalRead</span>(SW) == LOW) &#123;<br>  Serial.<span class="hljs-built_in">println</span>(<span class="hljs-string">&quot;Button Pressed&quot;</span>);<br>  <span class="hljs-built_in">delay</span>(<span class="hljs-number">200</span>); <span class="hljs-comment">// 防抖</span><br>&#125;<br><br>lastStateCLK = currentStateCLK;<br>&#125;<br></code></pre></td></tr></table></figure><h2 id="应用场景"><a href="#应用场景" class="headerlink" title="应用场景"></a>应用场景</h2><p>音量旋钮</p><p>菜单滚动与选择</p><p>数值调节器（如电源电压设定）</p><p>机械设备输入控制器</p><h2 id="注意事项"><a href="#注意事项" class="headerlink" title="注意事项"></a>注意事项</h2><p>防抖动：EC11 是机械接触式，需软硬件防抖处理</p><p>解析精度：依据脉冲数可提升旋转解析度</p><p>按键寿命：机械按键部分磨损较快，一般寿命约 3 万次</p><p>旋转寿命：通常 EC11 的旋转寿命为 3~10 万圈</p><h2 id="附加资料"><a href="#附加资料" class="headerlink" title="附加资料"></a>附加资料</h2><p><a href="https://github.com/bubao/esp32-ec11-encoder">esp32-ec11-encoder</a></p><p>可搭配 <code>Encoder</code> 库（Arduino）或直接读取 GPIO 实现</p>]]>
    </content>
    <id>https://bubao.github.io/posts/97e1f1a5.html</id>
    <link href="https://bubao.github.io/posts/97e1f1a5.html"/>
    <published>2025-05-11T08:10:59.000Z</published>
    <summary>
      <![CDATA[<h2 id="简介"><a href="#简介" class="headerlink" title="简介"></a>简介</h2><p>EC11 编码器 是一种常用的旋转式机械编码器，广泛用于嵌入式系统、电子产品中的人机交互（如音量调节、菜单选择等）。</p>
<ul>
<li>类型：增量式旋转编码器</li>
<li>输出：双路脉冲信号（A 相和 B 相），带按键（开关）功能</li>
<li>常见型号：EC11E15244G1、EC11E18244A8 等</li>
</ul>]]>
    </summary>
    <title>EC11 编码器</title>
    <updated>2026-10-31T20:37:58.466Z</updated>
  </entry>
  <entry>
    <author>
      <name>一念</name>
    </author>
    <category term="教程" scheme="https://bubao.github.io/categories/%E6%95%99%E7%A8%8B/"/>
    <category term="esp32" scheme="https://bubao.github.io/tags/esp32/"/>
    <category term="esp-idf" scheme="https://bubao.github.io/tags/esp-idf/"/>
    <category term="vscode" scheme="https://bubao.github.io/tags/vscode/"/>
    <content>
      <![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>本文将详细介绍如何在 VSCode 中配置 ESP-IDF 开发环境，适用于 ESP32 系列芯片的开发者。通过本教程，您将学会如何安装必要工具、获取 ESP-IDF、配置环境变量以及设置 VSCode 开发环境。</p><span id="more"></span><hr><h2 id="环境准备"><a href="#环境准备" class="headerlink" title="环境准备"></a>环境准备</h2><h3 id="安装必要工具"><a href="#安装必要工具" class="headerlink" title="安装必要工具"></a>安装必要工具</h3><p>首先，安装 CMake、Ninja 编译工具以及 Python：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">brew install cmake ninja dfu-util python3<br></code></pre></td></tr></table></figure><p>为了加速编译，建议安装 <code>ccache</code>：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">brew install ccache<br></code></pre></td></tr></table></figure><hr><h3 id="获取-ESP-IDF"><a href="#获取-ESP-IDF" class="headerlink" title="获取 ESP-IDF"></a>获取 ESP-IDF</h3><p>在开始开发之前，需要获取乐鑫提供的 ESP-IDF 软件库。</p><ol><li>打开终端，切换到您希望保存 ESP-IDF 的工作目录：</li></ol><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs shell">mkdir -p ~/esp<br>cd ~/esp<br></code></pre></td></tr></table></figure><ol start="2"><li>克隆 ESP-IDF 仓库：</li></ol><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">git clone -b v5.3.1 --recursive https://github.com/espressif/esp-idf.git<br></code></pre></td></tr></table></figure><ol start="3"><li>如果下载中断，可以使用以下命令继续下载：</li></ol><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">git submodule update --init --recursive<br></code></pre></td></tr></table></figure><hr><h3 id="安装工具链"><a href="#安装工具链" class="headerlink" title="安装工具链"></a>安装工具链</h3><p>ESP-IDF 需要一系列工具支持开发。以下是安装步骤：</p><ol><li>切换到 ESP-IDF 目录：</li></ol><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">cd ~/esp/esp-idf<br></code></pre></td></tr></table></figure><ol start="2"><li>设置 GitHub 资源镜像地址（可选）：</li></ol><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">export IDF_GITHUB_ASSETS=&quot;dl.espressif.com/github_assets&quot;<br></code></pre></td></tr></table></figure><ol start="3"><li>安装工具链：</li></ol><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">./install.sh esp32,esp32s3<br></code></pre></td></tr></table></figure><hr><h3 id="配置环境变量"><a href="#配置环境变量" class="headerlink" title="配置环境变量"></a>配置环境变量</h3><p>为了方便使用 ESP-IDF，需要配置环境变量。</p><ol><li>在 shell 配置文件（如 <code>.profile</code>、<code>.bashrc</code> 或 <code>.zprofile</code>）中添加以下命令：</li></ol><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">alias get_idf=&#x27;. $HOME/esp/esp-idf/export.sh&#x27;<br></code></pre></td></tr></table></figure><ol start="2"><li>刷新配置文件：</li></ol><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">source ~/.bashrc  # 根据实际文件路径替换<br></code></pre></td></tr></table></figure><ol start="3"><li>使用 <code>get_idf</code> 命令快速激活 ESP-IDF 环境：</li></ol><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">get_idf<br></code></pre></td></tr></table></figure><p><strong>注意</strong>：不建议直接将 <code>export.sh</code> 添加到 shell 配置文件中，以免影响其他终端会话。</p><hr><h2 id="配置-VSCode-开发-ESP32"><a href="#配置-VSCode-开发-ESP32" class="headerlink" title="配置 VSCode 开发 ESP32"></a>配置 VSCode 开发 ESP32</h2><h3 id="安装-Clangd-插件"><a href="#安装-Clangd-插件" class="headerlink" title="安装 Clangd 插件"></a>安装 Clangd 插件</h3><p>在 VSCode 中安装 <code>clangd</code> 插件以支持代码补全和语法检查。</p><h3 id="安装-ESP-专用-Clangd"><a href="#安装-ESP-专用-Clangd" class="headerlink" title="安装 ESP 专用 Clangd"></a>安装 ESP 专用 Clangd</h3><ol><li>使用以下命令安装 ESP 专用的 <code>clangd</code>：</li></ol><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">idf_tools.py install esp-clang<br></code></pre></td></tr></table></figure><ol start="2"><li>查找 <code>clangd</code> 的完整路径：</li></ol><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">tree ~/.espressif -f | grep &#x27;clangd&#x27;<br></code></pre></td></tr></table></figure><ol start="3"><li>将路径添加到项目的 <code>.vscode/settings.json</code> 文件中：</li></ol><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs json"><span class="hljs-punctuation">&#123;</span><br>    <span class="hljs-attr">&quot;clangd.arguments&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><br>        <span class="hljs-string">&quot;--enable-config&quot;</span><span class="hljs-punctuation">,</span><br>        <span class="hljs-string">&quot;-log=verbose&quot;</span><span class="hljs-punctuation">,</span><br>        <span class="hljs-string">&quot;-pretty&quot;</span><span class="hljs-punctuation">,</span><br>        <span class="hljs-string">&quot;--background-index&quot;</span><span class="hljs-punctuation">,</span><br>        <span class="hljs-string">&quot;--compile-commands-dir=build&quot;</span><span class="hljs-punctuation">,</span><br>        <span class="hljs-string">&quot;--query-driver=**&quot;</span><br>    <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;clangd.path&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;/Users/bubao/.espressif/tools/esp-clang/16.0.1-fe4f10a809/esp-clang/bin/clangd&quot;</span><br><span class="hljs-punctuation">&#125;</span><br></code></pre></td></tr></table></figure><hr><h3 id="配置-clangd"><a href="#配置-clangd" class="headerlink" title="配置 .clangd"></a>配置 <code>.clangd</code></h3><p>在项目根目录下创建 <code>.clangd</code> 文件，添加以下内容：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs yaml"><span class="hljs-attr">CompileFlags:</span><br>  <span class="hljs-attr">Remove:</span> <br>    <span class="hljs-bullet">-</span> <span class="hljs-string">-fno-tree-switch-conversion</span><br>    <span class="hljs-bullet">-</span> <span class="hljs-string">-fno-shrink-wrap</span><br>    <span class="hljs-bullet">-</span> <span class="hljs-string">-mtext-section-literals</span><br>    <span class="hljs-bullet">-</span> <span class="hljs-string">-mlongcalls</span><br>    <span class="hljs-bullet">-</span> <span class="hljs-string">-fstrict-volatile-bitfields</span><br>    <span class="hljs-bullet">-</span> <span class="hljs-string">-march=rv32imac_zicsr_zifencei</span><br></code></pre></td></tr></table></figure><p>如果不想在项目中创建 <code>.clangd</code> 文件，可以在全局配置文件中添加上述内容：</p><ul><li>macOS: <code>~/Library/Preferences/clangd/config.yaml</code></li><li>Windows: <code>C:\Users\Bob\AppData\Local\clangd\config.yaml</code></li><li>Linux: <code>$XDG_CONFIG_HOME/clangd/config.yaml</code> 或 <code>~/.config/clangd/config.yaml</code></li></ul><hr><h2 id="相关链接"><a href="#相关链接" class="headerlink" title="相关链接"></a>相关链接</h2><ul><li><a href="https://docs.espressif.com/projects/esp-idf/zh_CN/stable/esp32s3/get-started/linux-macos-setup.html">Linux 和 macOS 平台工具链的标准设置</a></li><li><a href="https://github.com/espressif/esp-idf/issues/6721#issuecomment-2231830343">ESP-IDF 官方问题讨论</a></li><li><a href="https://clangd.llvm.org/config.html#files">Clangd 配置文档</a></li></ul>]]>
    </content>
    <id>https://bubao.github.io/posts/8e2f2a51.html</id>
    <link href="https://bubao.github.io/posts/8e2f2a51.html"/>
    <published>2025-03-28T23:20:55.000Z</published>
    <summary>
      <![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>本文将详细介绍如何在 VSCode 中配置 ESP-IDF 开发环境，适用于 ESP32 系列芯片的开发者。通过本教程，您将学会如何安装必要工具、获取 ESP-IDF、配置环境变量以及设置 VSCode 开发环境。</p>]]>
    </summary>
    <title>VSCode 配置 ESP-IDF 开发环境</title>
    <updated>2026-10-31T20:37:58.470Z</updated>
  </entry>
</feed>
