<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Hexo</title>
  
  
  <link href="http://example.com/atom.xml" rel="self"/>
  
  <link href="http://example.com/"/>
  <updated>2025-06-23T19:21:30.106Z</updated>
  <id>http://example.com/</id>
  
  <author>
    <name>John Doe</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>ThingsBoard Docker 部署指南</title>
    <link href="http://example.com/2025/06/21/docker/Docker-ThingsBoard/"/>
    <id>http://example.com/2025/06/21/docker/Docker-ThingsBoard/</id>
    <published>2025-06-21T07:48:24.000Z</published>
    <updated>2025-06-23T19:21:30.106Z</updated>
    
    <content type="html"><![CDATA[<p>官网：<a class="link"   href="https://thingsboard.io/" >https://thingsboard.io<i class="fas fa-external-link-alt"></i></a><br>参考：<a class="link"   href="http://www.ithingsboard.com/docs/user-guide/attributes" >http://www.ithingsboard.com/docs/user-guide/attributes<i class="fas fa-external-link-alt"></i></a></p><p>应<strong>朋友</strong>的要求，写一篇关于 ThingsBoard 的文章，记录一下安装与使用的过程；</p><p>近年来，<strong>物联网平台（IoT Platform）</strong> 在智慧农业、工业监测、智能城市等应用中扮演着越来越关键的角色；随着传感器网络的普及，单一设备的数据采集和本地显示早已无法满足需求，<strong>集中式的远程管理、实时监控与可视化平台</strong> 成为构建现代物联网系统的重要组成部分；</p><p>在众多开源物联网平台中，<strong>ThingsBoard 凭借插件机制灵活、支持多种协议（如 MQTT、HTTP、CoAP），以及强大的仪表盘功能</strong>，成为中小型物联网项目中极具性价比的选择；</p><p>如果你正在寻找一个稳定易用的物联网平台，并满足以下需求：</p><ul><li>需要远程监控嵌入式&#x2F;传感器设备；</li><li>希望快速搭建一个支持 MQTT、HTTP、CoAP 的平台；</li><li>苦于其他平台太重或收费昂贵……</li></ul><p>那么，<strong>ThingsBoard</strong> 是你值得一试的开源解决方案；</p><p>本系列将基于实操视角，逐步记录我从零开始搭建 ThingsBoard 环境、连接设备、上传数据并实现可视化展示的过程；涉及内容包括但不限于：</p><ul><li>ThingsBoard 的快速部署（Docker 方式）；</li><li>基于 MQTT 协议的数据上报与遥控；</li><li>Dashboard 可视化配置；</li><li>与嵌入式终端（如 STM32、ESP32、树莓派等）的对接实践；</li></ul><h2 id="使用-Docker-部署-ThingsBoard"><a href="#使用-Docker-部署-ThingsBoard" class="headerlink" title="使用 Docker 部署 ThingsBoard"></a>使用 Docker 部署 ThingsBoard</h2><p>ThingsBoard 提供了多种安装方式，包括 Docker、Kubernetes、本地安装包等；考虑到 ThingsBoard 的 Docker 镜像已经非常完善，且易于部署，本文将采用 Docker 方式进行安装；</p><h3 id="环境依赖"><a href="#环境依赖" class="headerlink" title="环境依赖"></a>环境依赖</h3><p>在开始安装之前，请确保你的系统满足以下环境依赖：</p><ul><li><p><strong>Docker</strong>：ThingsBoard 需要 Docker 来运行；请确保你的系统已经安装了 Docker；如果没有安装，可以参考 Docker 官方文档进行安装；</p></li><li><p><strong>Docker Compose</strong>：ThingsBoard 使用 Docker Compose 来管理多个容器；请确保你的系统已经安装了 Docker Compose；如果没有安装，可以参考 Docker Compose 官方文档进行安装；</p></li></ul><h3 id="Docker-compose-配置文件"><a href="#Docker-compose-配置文件" class="headerlink" title="Docker compose 配置文件"></a>Docker compose 配置文件</h3><p>进入某一个目录（你自己定义，最好和其他任何软件不产生交集），编辑 docker-compose.yml 文件：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">nano docker-compose.yml</span><br></pre></td></tr></table></figure><p>在文件中添加：</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><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><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">postgres:</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">always</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">&quot;postgres:16&quot;</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;5432&quot;</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="attr">POSTGRES_DB:</span> <span class="string">thingsboard</span></span><br><span class="line">      <span class="attr">POSTGRES_PASSWORD:</span> <span class="string">postgres</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">postgres-data:/var/lib/postgresql/data</span></span><br><span class="line">  <span class="attr">thingsboard-ce:</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">always</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">&quot;thingsboard/tb-node:4.0.1.1&quot;</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;8080:8080&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;7070:7070&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;1883:1883&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;8883:8883&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;5683-5688:5683-5688/udp&quot;</span></span><br><span class="line">    <span class="attr">logging:</span></span><br><span class="line">      <span class="attr">driver:</span> <span class="string">&quot;json-file&quot;</span></span><br><span class="line">      <span class="attr">options:</span></span><br><span class="line">        <span class="attr">max-size:</span> <span class="string">&quot;100m&quot;</span></span><br><span class="line">        <span class="attr">max-file:</span> <span class="string">&quot;10&quot;</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="attr">TB_SERVICE_ID:</span> <span class="string">tb-ce-node</span></span><br><span class="line">      <span class="attr">SPRING_DATASOURCE_URL:</span> <span class="string">jdbc:postgresql://postgres:5432/thingsboard</span></span><br><span class="line">    <span class="attr">depends_on:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">postgres</span></span><br><span class="line"></span><br><span class="line"><span class="attr">volumes:</span></span><br><span class="line">  <span class="attr">postgres-data:</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">tb-postgres-data</span></span><br><span class="line">    <span class="attr">driver:</span> <span class="string">local</span></span><br></pre></td></tr></table></figure><p><strong>服务说明</strong>：</p><ul><li><p>postgres ：用于存储 ThingsBoard 的数据；</p><ul><li>image: postgres:16：使用官方维护的 PostgreSQL 16 镜像，稳定可靠；</li><li>ports: “5432”：数据库默认端口，未映射到宿主机，确保安全（可按需映射，可以通过 Docker 创建的网络使用容器名访问）；</li><li>environment：配置 PostgreSQL 的数据库信息；<ul><li>POSTGRES_DB&#x3D;thingsboard：初始化时创建名为 thingsboard 的数据库；</li><li>POSTGRES_PASSWORD&#x3D;postgres：数据库密码为 postgres，可自行修改；</li></ul></li><li>volumes：将数据库数据挂载到宿主机上的 tb-postgres-data 卷中，实现数据持久化，容器重启不会丢失数据（但不会直接在本地映射出来），建议定期备份 tb-postgres-data，以便灾难恢复；</li></ul></li><li><p>thingsboard-ce：ThingsBoard 的核心服务；</p><ul><li>image: thingsboard&#x2F;tb-node:4.0.1.1：指定 ThingsBoard CE 版本为 4.0.1.1，可根据需求更新为最新版本；</li><li>ports：映射到宿主机的端口，如下：<ul><li>8080:8080：Web UI 访问端口（默认使用 <a class="link"   href="http://localhost:8080/" >http://localhost:8080<i class="fas fa-external-link-alt"></i></a> 打开平台）</li><li>7070:7070：用于 WebSocket 和服务间通信</li><li>1883:1883：MQTT 协议端口（设备上报数据）</li><li>8883:8883：MQTT over TLS 加密端口</li><li>5683-5688:5683-5688&#x2F;udp：CoAP 协议端口（适用于轻量设备）</li></ul></li><li>logging：限制日志大小防止磁盘爆满，每个日志文件最大 100MB，最多保留 10 个文件；</li><li>environment：配置 ThingsBoard 的环境变量；<ul><li>TB_SERVICE_ID&#x3D;tb-ce-node：设置服务 ID，适用于集群或多节点部署；</li><li>SPRING_DATASOURCE_URL&#x3D;jdbc:postgresql:&#x2F;&#x2F;postgres:5432&#x2F;thingsboard：指定数据库连接字符串，注意这里的 postgres 为 Compose 内部服务名（自动 DNS），无需写成 IP 地址；</li></ul></li><li>depends_on: postgres：确保在启动 thingsboard-ce 之前，postgres 服务已经启动；</li></ul></li><li><p>volumes：数据持久化，用于保存 PostgreSQL 数据的宿主机挂载卷，避免容器重启后数据丢失；</p></li></ul><blockquote><p><strong>注意</strong>：端口配置中，冒号前面的是宿主机端口，后面的端口是容器内端口；如果需要修改端口，注意只能修改宿主机端口，容器内端口不能修改；<br>若设备运行在非 Docker 主机，请确保防火墙&#x2F;路由规则允许连接映射到的 MQTT&#x2F;CoAP&#x2F;Web 端口；</p></blockquote><h3 id="初始化数据库与加载系统资源"><a href="#初始化数据库与加载系统资源" class="headerlink" title="初始化数据库与加载系统资源"></a>初始化数据库与加载系统资源</h3><p>在首次启动 ThingsBoard 服务之前，我们需要先 <strong>初始化数据库的表结构</strong> 并 <strong>加载系统内置资源</strong>；这一步非常关键，否则平台无法正常运行；</p><p>ThingsBoard 官方镜像已经内置了初始化命令，可以通过一条 Docker Compose 命令完成：</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><span class="line">docker compose run --<span class="built_in">rm</span> \</span><br><span class="line">  -e INSTALL_TB=<span class="literal">true</span> \</span><br><span class="line">  -e LOAD_DEMO=<span class="literal">true</span> \</span><br><span class="line">  thingsboard-ce</span><br></pre></td></tr></table></figure><p>该命令会：</p><ul><li>启动一个临时的 thingsboard-ce 容器；</li><li>执行内置的数据库初始化流程；</li><li>结束后自动退出并删除临时容器（因为加了 –rm 参数）；</li></ul><h2 id="启动与登录平台"><a href="#启动与登录平台" class="headerlink" title="启动与登录平台"></a>启动与登录平台</h2><h3 id="启动平台"><a href="#启动平台" class="headerlink" title="启动平台"></a>启动平台</h3><p>完成数据库初始化后，我们即可正式启动 ThingsBoard 平台服务；</p><p>使用以下命令启动所有容器，并实时查看 ThingsBoard 的运行日志：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker compose up -d &amp;&amp; docker compose logs -f thingsboard-ce</span><br></pre></td></tr></table></figure><p>这条命令做了两件事：</p><ul><li>docker compose up -d：在后台启动所有定义在 docker-compose.yml 中的服务（包括 PostgreSQL 和 ThingsBoard 容器）；</li><li>docker compose logs -f thingsboard-ce：持续输出 thingsboard-ce 容器的运行日志，方便我们第一时间看到启动进度和是否成功；</li></ul><blockquote><p>如果你看到 Started ThingsBoardServerApplication 或类似提示，说明平台已经成功启动；<br>如果不想看日志，可以按下 Ctrl C 退出日志输出（容器本身不会停止，ThingsBoard 服务仍然会在后台运行）；<br>如果你想重新查看运行日志，可以随时使用 docker compose logs -f thingsboard-ce；</p></blockquote><h3 id="登录平台"><a href="#登录平台" class="headerlink" title="登录平台"></a>登录平台</h3><p>默认情况下，ThingsBoard 会监听本地的 8080 端口；你可以在浏览器中访问：</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">http://localhost:8080</span><br></pre></td></tr></table></figure><p>或者在非本地部署时，替换为你的主机 IP 地址：</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">http://&#123;your-host-ip&#125;:8080</span><br></pre></td></tr></table></figure><p>打开网页后，即可看到 ThingsBoard 的登录界面；</p><p>首次登录系统，你可以使用以下默认账户：</p><table><thead><tr><th>角色</th><th>用户名</th><th>密码</th></tr></thead><tbody><tr><td>系统管理员</td><td><a class="link"   href="mailto:&#x73;&#x79;&#115;&#97;&#x64;&#x6d;&#105;&#x6e;&#64;&#116;&#104;&#105;&#x6e;&#103;&#115;&#x62;&#x6f;&#97;&#x72;&#x64;&#46;&#111;&#x72;&#103;" >sysadmin@thingsboard.org<i class="fas fa-external-link-alt"></i></a></td><td>sysadmin</td></tr><tr><td>租户管理员</td><td><a class="link"   href="mailto:&#x74;&#x65;&#x6e;&#97;&#x6e;&#x74;&#64;&#x74;&#104;&#105;&#110;&#x67;&#115;&#98;&#x6f;&#x61;&#x72;&#100;&#x2e;&#x6f;&#x72;&#103;" >tenant@thingsboard.org<i class="fas fa-external-link-alt"></i></a></td><td>tenant</td></tr><tr><td>客户用户（演示）</td><td><a class="link"   href="mailto:&#x63;&#117;&#115;&#x74;&#x6f;&#109;&#x65;&#114;&#64;&#x74;&#x68;&#105;&#110;&#x67;&#x73;&#98;&#111;&#x61;&#x72;&#x64;&#x2e;&#x6f;&#x72;&#x67;" >customer@thingsboard.org<i class="fas fa-external-link-alt"></i></a></td><td>customer</td></tr></tbody></table><blockquote><p>建议登录后尽快修改各个账户的密码，以保证平台安全；</p></blockquote><h2 id="ThingsBoard-使用"><a href="#ThingsBoard-使用" class="headerlink" title="ThingsBoard 使用"></a>ThingsBoard 使用</h2><h3 id="添加设备"><a href="#添加设备" class="headerlink" title="添加设备"></a>添加设备</h3><p>其实我们第一步应该是创建租户和租户管理员账号的，但是平台默认提供了三个账号，我们直接使用即可；</p><p>登录租户管理员账号后，点击左侧导航栏的 <strong>设备</strong> 选项，进入设备管理页面即可添加设备；</p><center>   <img     src="https://picture.orangetime.top/i/2025/06/21/685691260679f.png"  ></center><center>   <img     src="https://picture.orangetime.top/i/2025/06/21/685692da81ca1.png"  ></center><p>可选择 default 或 thermostat 模板，这些模板预配置了 MQTT、HTTP 与 CoAP 通信协议，便于多种方式测试连通性（在我当前使用的版本中，MQTT 功能运行最稳定，CoAP反而无法正常使用）；</p><p>分配给客户，我选择了公开，这个决定了用户账户可以查看的设备；</p><center>   <img     src="https://picture.orangetime.top/i/2025/06/21/68569408ddfbd.png"  ></center><p>我们设置 MQTT 凭据，要注意，每一个设备的凭据不能相同，凭据是设备与平台通信的唯一标识；</p><h3 id="测试设备"><a href="#测试设备" class="headerlink" title="测试设备"></a>测试设备</h3><h4 id="连接平台"><a href="#连接平台" class="headerlink" title="连接平台"></a>连接平台</h4><p>我们可以使用客户端，或者 Linux 命令行工具，来测试设备与平台之间的连通性，这里我使用 MQTTX 工具进行测试；</p><center>   <img     src="https://picture.orangetime.top/i/2025/06/22/6857f09cde319.png"  ></center><p>选择你运行的 Docker ThingsBoard 的机器地址，端口选择 Docker 配置映射出来的端口（默认是 1883），填入在 ThingsBoard 中设备的凭据，然后点击连接即可连接到平台；</p><h4 id="订阅、发布与平台设备配置"><a href="#订阅、发布与平台设备配置" class="headerlink" title="订阅、发布与平台设备配置"></a>订阅、发布与平台设备配置</h4><p>在说订阅与发布之前，我们先来看一下 ThingsBoard 的属性，ThingsBoard 分为客户端属性、服务端属性和最新属性值，如下：</p><table><thead><tr><th>属性类型</th><th>来源方向</th><th>设备是否可写</th><th>平台是否可写</th><th>是否自动推送到设备</th><th>描述</th></tr></thead><tbody><tr><td>客户端属性</td><td>设备 → 平台</td><td>✅ 是</td><td>✅ 可初始化（仅首次）</td><td>❌ 否</td><td>设备主动上报的信息，例如设备型号、固件版本、运行状态，平台只做查看，不会主动推送</td></tr><tr><td>服务端属性</td><td>平台 → 平台侧应用</td><td>❌ 否</td><td>✅ 是</td><td>❌ 否</td><td>平台保存的属性集合，设备无法获取</td></tr><tr><td>共享属性</td><td>平台 → 设备</td><td>❌ 否</td><td>✅ 是</td><td>✅ 是</td><td>平台设置的共享配置项，支持平台→设备自动推送，也可由设备主动请求（如目标温度、模式等）</td></tr><tr><td>最新属性值（概念）</td><td>双向</td><td>✅ 是</td><td>✅ 是</td><td>❌ 否（需主动查询）</td><td>并非单独属性类型，而是平台维护的当前属性快照，用于展示或通过 API 获取</td></tr></tbody></table><p>在平台点击设备，进入设备详情页面，可以看到设备的属性，如下：</p><center>   <img     src="https://picture.orangetime.top/i/2025/06/23/68583b1340fff.png"  ></center><p>接下来我就根据上面的表格，来演示一下 ThingsBoard 的属性与设备之间的交互；</p><h5 id="服务端属性"><a href="#服务端属性" class="headerlink" title="服务端属性"></a>服务端属性</h5><center>   <img     src="https://picture.orangetime.top/i/2025/06/23/6858fdcfae2a3.png"  ></center><p>服务端属性通常是由平台维护、设备通过请求获取的一类静态或配置属性；它常用于存储平台侧记录的设备参数，例如部署位置、设备负责人、安装时间等；</p><ul><li>平台侧应用获取服务端属性</li></ul><p>因为服务端属性仅能通过平台侧应用进行读写，所以无法使用 MQTT 订阅的方式获取，只能通过 API 获取，如下：</p><ol><li>获取 JWT 令牌</li></ol><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl -X POST https://iot.140105.xyz/api/auth/login   -H <span class="string">&quot;Content-Type: application/json&quot;</span>   -d <span class="string">&#x27;&#123;&quot;username&quot;:&quot;tenant@thingsboard.org&quot;,&quot;password&quot;:&quot;tenant&quot;&#125;&#x27;</span> </span><br></pre></td></tr></table></figure><p>以我的平台为例，将地址替换为你的平台地址，用户名和密码替换为你的租户管理员账号和密码，即可获取 JWT 令牌，返回示例如下（返回的信息比较长，注意仔细比对）：</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></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;token&quot;</span><span class="punctuation">:</span> <span class="string">&quot;xxxxx&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;refreshToken&quot;</span><span class="punctuation">:</span> <span class="string">&quot;xxxxx&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><ol start="2"><li>获取设备服务端属性</li></ol><p>使用上一步获得的 JWT 令牌，通过下面的方法获取设备的服务端属性：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl -X GET <span class="string">&#x27;https://iot.140105.xyz/api/plugins/telemetry/DEVICE/4d453110-4e91-11f0-aa54-d1bcfb4dde7b/values/attributes/SERVER_SCOPE&#x27;</span>   -H <span class="string">&#x27;Content-Type: application/json&#x27;</span>   -H <span class="string">&#x27;X-Authorization: Bearer JWT 令牌&#x27;</span></span><br></pre></td></tr></table></figure><p>其中 DEVICE 后面的 4d453110-4e91-11f0-aa54-d1bcfb4dde7b 是设备的 ID，在平台网页点击设备，网址链接中可以看到，JWT 令牌就是上一步获取的 token，返回结果如下；</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><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></pre></td><td class="code"><pre><span class="line"><span class="punctuation">[</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;lastUpdateTs&quot;</span><span class="punctuation">:</span> <span class="number">1750619760822</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;key&quot;</span><span class="punctuation">:</span> <span class="string">&quot;lastConnectTime&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;value&quot;</span><span class="punctuation">:</span> <span class="number">1750619760822</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;lastUpdateTs&quot;</span><span class="punctuation">:</span> <span class="number">1750619852183</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;key&quot;</span><span class="punctuation">:</span> <span class="string">&quot;test&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;value&quot;</span><span class="punctuation">:</span> <span class="string">&quot;1&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;lastUpdateTs&quot;</span><span class="punctuation">:</span> <span class="number">1750621040988</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;key&quot;</span><span class="punctuation">:</span> <span class="string">&quot;lastDisconnectTime&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;value&quot;</span><span class="punctuation">:</span> <span class="number">1750621040987</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;lastUpdateTs&quot;</span><span class="punctuation">:</span> <span class="number">1750621041491</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;key&quot;</span><span class="punctuation">:</span> <span class="string">&quot;lastActivityTime&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;value&quot;</span><span class="punctuation">:</span> <span class="number">1750621040963</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;lastUpdateTs&quot;</span><span class="punctuation">:</span> <span class="number">1750621684069</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;key&quot;</span><span class="punctuation">:</span> <span class="string">&quot;inactivityAlarmTime&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;value&quot;</span><span class="punctuation">:</span> <span class="number">1750621684069</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;lastUpdateTs&quot;</span><span class="punctuation">:</span> <span class="number">1750621684069</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;key&quot;</span><span class="punctuation">:</span> <span class="string">&quot;active&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;value&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">]</span></span><br></pre></td></tr></table></figure><blockquote><p><strong>注意</strong>：token 是有有效期的，如果过期了，需要重新获取；<br>以上的演示方式，均在命令行中完成，可以使用其他编程语言，如 Python、Java、Go 等作为 curl 的替代；</p></blockquote><ul><li>平台侧应用设置服务端属性</li></ul><p>我们同样使用 JWT 令牌，通过下面的方法设置设备的服务端属性：‘</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><span class="line">curl -v <span class="string">&#x27;https://iot.140105.xyz/api/plugins/telemetry/DEVICE/4d453110-4e91-11f0-aa54-d1bcfb4dde7b/SERVER_SCOPE&#x27;</span>   -H <span class="string">&#x27;Content-Type: application/json&#x27;</span>   -H <span class="string">&#x27;X-Authorization: Bearer JWT 令牌&#x27;</span> \</span><br><span class="line">-H <span class="string">&#x27;content-type: application/json&#x27;</span> \</span><br><span class="line">--data-raw <span class="string">&#x27;&#123;&quot;test&quot;:&quot;2&quot;&#125;&#x27;</span></span><br></pre></td></tr></table></figure><p>其中最后一行的 test 是你要设置的属性键名，2 是你要设置的属性值，运行之后平台对应设备的服务端属性就会发生变化；</p><h5 id="客户端属性"><a href="#客户端属性" class="headerlink" title="客户端属性"></a>客户端属性</h5><center>   <img     src="https://picture.orangetime.top/i/2025/06/23/6858fa649f16b.png"  ></center><p>客户端属性常用于平台的分组管理、版本控制、规则引擎判断等场景，是设备与平台之间状态同步的基础结构之一；</p><ul><li>设备发送数据到平台</li></ul><p>客户端属性的主题是 v1&#x2F;devices&#x2F;me&#x2F;attributes，通过表格我们能看到，这个是由设备发送到平台的，所以我们使用 MQTTX 工具模拟设备，向平台发布一条消息，如下：</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></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;serialNumber&quot;</span><span class="punctuation">:</span> <span class="string">&quot;SN-001-ABC&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;firmwareVersion&quot;</span><span class="punctuation">:</span> <span class="string">&quot;v1.2.5&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;model&quot;</span><span class="punctuation">:</span> <span class="string">&quot;X200&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><center>   <img     src="https://picture.orangetime.top/i/2025/06/23/68583efd769a6.png"  ></center><p>平台可以根据设备上报的属性，在仪表盘中展示设备信息，也可以在规则链库中根据属性值进行判断，如下：</p><center>   <img     src="https://picture.orangetime.top/i/2025/06/23/6858414f20529.png"  ></center><p>如上图即为在仪表盘中显示设备信息；</p><center>   <img     src="https://picture.orangetime.top/i/2025/06/23/68584392e86b5.png"  ></center><p>如上图即为设置规则链库，当版本信息小于脚本中所配置的版本时，便会触发提醒；</p><ul><li>设备从平台获取数据</li></ul><p>设备可以主动请求平台侧的客户端属性，请求主题为 v1&#x2F;devices&#x2F;me&#x2F;attributes&#x2F;request&#x2F;&lt;request_id&gt;，其中 &lt;request_id&gt; 是请求 ID，可以任意指定，订阅主题可以是 v1&#x2F;devices&#x2F;me&#x2F;attributes&#x2F;request&#x2F;&lt;request_id&gt; 或者 v1&#x2F;devices&#x2F;me&#x2F;attributes&#x2F;response&#x2F;+，发送的数据如下：</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></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;clientKeys&quot;</span><span class="punctuation">:</span> <span class="string">&quot;firmwareVersion,serialNumber&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>其中， firmwareVersion 和 serialNumber 是设备在 ThingsBoard 中配置的客户端属性；</p><p>在主题 v1&#x2F;devices&#x2F;me&#x2F;attributes&#x2F;request&#x2F;1 或者 v1&#x2F;devices&#x2F;me&#x2F;attributes&#x2F;response&#x2F;+ 中会得到从平台返回的数据，如下：</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></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;client&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;serialNumber&quot;</span><span class="punctuation">:</span> <span class="string">&quot;SN-001-ABC&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;firmwareVersion&quot;</span><span class="punctuation">:</span> <span class="string">&quot;v1.2.5&quot;</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>对 json 进行解析，即可得到平台返回的属性值；</p><blockquote><p>在 ThingsBoard 中，每个设备都有唯一的连接凭据，因此平台能够识别出请求来源的具体设备；即使多个设备同时使用相同的 request_id 发起属性请求，平台也能正确分发并返回各自的响应结果，不会互相影响；<br>但如果你使用一个设备代理器，代理器会使用相同的凭据，那么平台会认为所有的请求都来自同一个设备，导致响应混乱，这时候就需要你对代理的设备进行额外的代码编写，给每个被代理的设备分配不同的请求 ID，实现设备之间的请求隔离；</p></blockquote><ul><li>平台侧应用获取数据</li></ul><p>平台侧可以和设备一样使用 MQTT 获取数据，也可以使用 API 获取数据，如下：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl -X GET <span class="string">&#x27;https://iot.140105.xyz/api/plugins/telemetry/DEVICE/4d453110-4e91-11f0-aa54-d1bcfb4dde7b/values/attributes/CLIENT_SCOPE&#x27;</span>   -H <span class="string">&#x27;Content-Type: application/json&#x27;</span>   -H <span class="string">&#x27;X-Authorization: Bearer JWT 令牌&#x27;</span></span><br></pre></td></tr></table></figure><p>和服务端属性获取的方式大同小异，只是将 SERVER_SCOPE 替换为 CLIENT_SCOPE 即可，运行之后结果如下：</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><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">[</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;lastUpdateTs&quot;</span><span class="punctuation">:</span> <span class="number">1750613652969</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;key&quot;</span><span class="punctuation">:</span> <span class="string">&quot;serialNumber&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;value&quot;</span><span class="punctuation">:</span> <span class="string">&quot;SN-001-ABC&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;lastUpdateTs&quot;</span><span class="punctuation">:</span> <span class="number">1750613652969</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;key&quot;</span><span class="punctuation">:</span> <span class="string">&quot;model&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;value&quot;</span><span class="punctuation">:</span> <span class="string">&quot;X200&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;lastUpdateTs&quot;</span><span class="punctuation">:</span> <span class="number">1750613652969</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;key&quot;</span><span class="punctuation">:</span> <span class="string">&quot;firmwareVersion&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;value&quot;</span><span class="punctuation">:</span> <span class="string">&quot;v1.2.5&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">]</span></span><br></pre></td></tr></table></figure><h5 id="共享属性"><a href="#共享属性" class="headerlink" title="共享属性"></a>共享属性</h5><center>   <img     src="https://picture.orangetime.top/i/2025/06/23/68593b5ac0ca6.png"  ></center><p>共享属性是平台侧应用给设备分发数据的一种方式，共享属性可以用于平台侧应用之间的数据共享（也可以用于平台向设备发送数据）；</p><blockquote><p>和客户端属性的区别是，客户端属性是设备主动请求平台返回数据，而共享属性是当属性发生变化或者新建属性时候，平台会主动向设备发送数据（当然，也可以主动请求）；</p></blockquote><ul><li>设备从平台请求数据</li></ul><p>和客户端属性一样，设备可以主动请求平台侧的共享属性，请求主题为 v1&#x2F;devices&#x2F;me&#x2F;attributes&#x2F;request&#x2F;&lt;request_id&gt;，其中 &lt;request_id&gt; 是请求 ID，可以任意指定，订阅主题可以是 v1&#x2F;devices&#x2F;me&#x2F;attributes&#x2F;request&#x2F;&lt;request_id&gt; 或者 v1&#x2F;devices&#x2F;me&#x2F;attributes&#x2F;response&#x2F;+，发送的数据如下：</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></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;sharedKeys&quot;</span><span class="punctuation">:</span> <span class="string">&quot;test&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>其中， test 是你要获取的属性键名；</p><p>在主题 v1&#x2F;devices&#x2F;me&#x2F;attributes&#x2F;request&#x2F;&lt;request_id&gt; 或者 v1&#x2F;devices&#x2F;me&#x2F;attributes&#x2F;response&#x2F;+ 中会得到从平台返回的数据，如下：</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></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;shared&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;test&quot;</span><span class="punctuation">:</span> <span class="string">&quot;1&quot;</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>对 json 进行解析，即可得到平台返回的共享属性值；</p><ul><li>属性修改&#x2F;变动自动推送</li></ul><p>无论在平台侧应用，还是在平台本身，我们对共享属性进行增、删、改操作，设备都会收到平台<strong>自动</strong>推送的属性值，如下：</p><ol><li>增和改</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></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;test&quot;</span><span class="punctuation">:</span> <span class="string">&quot;2&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><ol start="2"><li>删</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></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;deleted&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="string">&quot;test&quot;</span></span><br><span class="line">  <span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><ul><li>平台侧应用获取数据</li></ul><p>平台侧应用获取共享属性的方式和服务端属性大同小异，只是将 SERVER_SCOPE 替换为 SHARED_SCOPE 即可，如下所示,在此不过多赘述：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl -X GET <span class="string">&#x27;https://iot.140105.xyz/api/plugins/telemetry/DEVICE/4d453110-4e91-11f0-aa54-d1bcfb4dde7b/values/attributes/SHARED_SCOPE&#x27;</span>   -H <span class="string">&#x27;Content-Type: application/json&#x27;</span>   -H <span class="string">&#x27;X-Authorization: Bearer JWT 令牌&#x27;</span></span><br></pre></td></tr></table></figure><ul><li>平台侧应用设置数据</li></ul><p>平台侧应用设置共享属性的方式和服务端属性大同小异，只是将 SERVER_SCOPE 替换为 SHARED_SCOPE 即可，如下所示,在此不过多赘述：</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><span class="line">curl -v <span class="string">&#x27;https://iot.140105.xyz/api/plugins/telemetry/DEVICE/4d453110-4e91-11f0-aa54-d1bcfb4dde7b/SHARED_SCOPE&#x27;</span>   -H <span class="string">&#x27;Content-Type: application/json&#x27;</span>   -H <span class="string">&#x27;X-Authorization: Bearer JWT 令牌&#x27;</span> \</span><br><span class="line">-H <span class="string">&#x27;content-type: application/json&#x27;</span> \</span><br><span class="line">--data-raw <span class="string">&#x27;&#123;&quot;test&quot;:&quot;2&quot;&#125;&#x27;</span></span><br></pre></td></tr></table></figure><blockquote><p>用此方法进行数据的更新，设备也会自动收到平台推送的数据；<br>如果设备离线，可能会错过重要的数据更新，所以建议在设备启动之后订阅共享属性，并且每次连接后请求属性的最新值；</p></blockquote><blockquote><p>除了属性，ThingsBoard 还提供了遥测（Telemetry）数据，和属性的区别是遥测多了时间序列相关的功能，可以用于长时间的数据对比分析等，展开的话内容太多，在此不过多赘述，可以在这里查看：<a class="link"   href="http://www.ithingsboard.com/docs/user-guide/telemetry" >遥测数据<i class="fas fa-external-link-alt"></i></a>；</p></blockquote><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文记录了从零开始使用 Docker 快速部署 ThingsBoard 平台的全过程，并详细演示了平台与设备之间通过 MQTT 协议进行数据交互的方式，特别是客户端属性、服务端属性、共享属性三者的使用与差异；借助 ThingsBoard 丰富的功能，我们可以轻松实现设备的远程配置、状态监测与规则联动，为物联网系统的构建打下坚实基础；</p><p>如果你正在开发或运营一个嵌入式项目，或正在寻找一套支持 MQTT&#x2F;HTTP&#x2F;CoAP 且功能全面的物联网平台，相信 ThingsBoard 能为你提供一个清晰的起点；</p><p>后续我将继续记录更多实战经验，包括遥测数据上传与可视化、规则链自动化处理、集成邮件&#x2F;钉钉&#x2F;短信报警机制等内容，欢迎持续关注；</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;官网：&lt;a class=&quot;link&quot;   href=&quot;https://thingsboard.io/&quot; &gt;https://thingsboard.io&lt;i class=&quot;fas fa-external-link-alt&quot;&gt;&lt;/i&gt;&lt;/a&gt;&lt;br&gt;参考：&lt;a class=&quot;l</summary>
      
    
    
    
    <category term="docker" scheme="http://example.com/categories/docker/"/>
    
    
  </entry>
  
  <entry>
    <title>悲报</title>
    <link href="http://example.com/2025/05/17/bad/"/>
    <id>http://example.com/2025/05/17/bad/</id>
    <published>2025-05-17T05:51:27.000Z</published>
    <updated>2025-05-17T05:51:27.000Z</updated>
    
    <content type="html"><![CDATA[<p>硬盘坏了，数据全没了；<br>在慢慢恢复数据了，但是大部分应该是很难恢复了；</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;硬盘坏了，数据全没了；&lt;br&gt;在慢慢恢复数据了，但是大部分应该是很难恢复了；&lt;/p&gt;
</summary>
      
    
    
    
    
  </entry>
  
  <entry>
    <title>使用Hexo搭建个人博客</title>
    <link href="http://example.com/2025/02/22/other/Hexo-Blog/"/>
    <id>http://example.com/2025/02/22/other/Hexo-Blog/</id>
    <published>2025-02-22T01:47:18.000Z</published>
    <updated>2025-02-22T01:47:18.000Z</updated>
    
    <content type="html"><![CDATA[<p>在信息过载的时代，拥有一个属于自己的博客，不仅能整理思路、沉淀经验，还能在需要时快速复用曾经解决过的问题；而 Hexo，作为一款基于 Node.js 的轻量静态博客框架，以其部署简单、主题丰富、生成速度快的优势，成为了许多开发者的首选；</p><p>我最开始只是想记录一下技术笔记，后来发现 Markdown 写作 + Hexo 渲染这个组合用起来非常舒服：格式统一、可定制性强、部署自由（可以部署在 GitHub Pages、VPS、自建服务器等）；于是就干脆一步步搭建了一个完整的 Hexo 博客站点；</p><p>这篇文章就是整个过程的完整记录：从搭建环境、初始化项目、使用主题，到部署上线、开启 HTTPS、绑定域名等等，涵盖我实战中遇到的所有关键环节；希望能帮到你，也为自己留个备忘；</p><h2 id="环境准备（Ubuntu-系统）"><a href="#环境准备（Ubuntu-系统）" class="headerlink" title="环境准备（Ubuntu 系统）"></a>环境准备（Ubuntu 系统）</h2><p>在正式搭建 Hexo 博客之前，需要先安装一些基础环境工具；由于我使用了 <strong>FRP 实现内网穿透部署 Hexo</strong>，所以本篇不涉及 GitHub Pages 或 Git 相关内容，仅保留最必要的部分；</p><h3 id="安装-Node-js-和-npm"><a href="#安装-Node-js-和-npm" class="headerlink" title="安装 Node.js 和 npm"></a>安装 Node.js 和 npm</h3><p>Ubuntu 默认的软件源中 Node.js 版本可能比较老，推荐使用官方的 NodeSource 源进行安装；这里以 Node.js LTS 版本（长期支持版）为例：</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><span class="line"><span class="comment"># 安装 Node.js LTS 版本（包含 npm）</span></span><br><span class="line">curl -fsSL https://deb.nodesource.com/setup_lts.x | <span class="built_in">sudo</span> -E bash -</span><br><span class="line"><span class="built_in">sudo</span> apt install -y nodejs</span><br></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><span class="line">node -v    <span class="comment"># 输出版本号表示安装成功</span></span><br><span class="line">npm -v</span><br></pre></td></tr></table></figure><h3 id="安装-Hexo-命令行工具"><a href="#安装-Hexo-命令行工具" class="headerlink" title="安装 Hexo 命令行工具"></a>安装 Hexo 命令行工具</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> npm install -g hexo-cli</span><br></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><span class="line">hexo -v    <span class="comment"># 输出版本号表示安装成功</span></span><br></pre></td></tr></table></figure><h3 id="可选：Git（非必须）"><a href="#可选：Git（非必须）" class="headerlink" title="可选：Git（非必须）"></a>可选：Git（非必须）</h3><p>如果你后续打算将博客托管到 GitHub Pages，或使用 Git 做版本管理，也可以顺手安装 Git：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt install -y git</span><br></pre></td></tr></table></figure><p>但如果你和我一样是用 FRP 暴露本地服务到公网，那完全可以跳过这一步；</p><h2 id="Hexo-博客项目初始化"><a href="#Hexo-博客项目初始化" class="headerlink" title="Hexo 博客项目初始化"></a>Hexo 博客项目初始化</h2><p>在完成环境安装后，就可以开始初始化 Hexo 博客项目了；Hexo 会在指定目录生成整个博客所需的目录结构，包括文章源文件、配置文件和静态资源目录等；</p><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><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">mkdir</span> -p ~/my_blog</span><br><span class="line"><span class="built_in">cd</span> ~/my_blog</span><br></pre></td></tr></table></figure><h3 id="初始化博客项目"><a href="#初始化博客项目" class="headerlink" title="初始化博客项目"></a>初始化博客项目</h3><p>在项目目录中初始化 Hexo，会自动生成 scaffolds&#x2F;、source&#x2F;、themes&#x2F; 等基础目录：</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><span class="line">hexo init</span><br><span class="line">npm install</span><br></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><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><span class="line">my_blog/</span><br><span class="line">├── _config.yml        <span class="comment"># Hexo 配置文件</span></span><br><span class="line">├── package.json       <span class="comment"># 项目依赖描述</span></span><br><span class="line">├── scaffolds/         <span class="comment"># 文章模板</span></span><br><span class="line">├── <span class="built_in">source</span>/            <span class="comment"># 文章和静态资源存放目录</span></span><br><span class="line">├── themes/            <span class="comment"># 博客主题目录</span></span><br><span class="line">└── node_modules/      <span class="comment"># npm 安装的依赖包</span></span><br></pre></td></tr></table></figure><h3 id="启动本地服务器"><a href="#启动本地服务器" class="headerlink" title="启动本地服务器"></a>启动本地服务器</h3><p>完成初始化后，Hexo 自带的本地服务器就可以直接启动，用于查看博客内容是否正确渲染：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">hexo server</span><br></pre></td></tr></table></figure><p>启动成功后，浏览器访问 http<span></span>:&#x2F;&#x2F;localhost:4000，即可看到博客的默认页面；</p><blockquote><p>别急！别看到有端口就急着用FRP进行映射，因为hexo server 本质上只是一个轻量级本地预览服务，虽然可以通过 FRP 等工具映射到公网进行访问，但要注意以下几点：</p><ul><li>它不是为长期稳定服务设计的，性能和安全性不适合生产环境；</li><li>它的渲染速度比生产环境要慢很多，不适合用来做性能测试；<br>一旦关闭终端或重启机器，服务就会中断；<br>没有缓存机制、日志控制、访问控制等功能，暴露在公网风险较高；</li></ul><p>所以，在正式部署之前，还有一些操作要做；</p></blockquote><h2 id="使用-Hexo-生成静态博客-Caddy-反向代理"><a href="#使用-Hexo-生成静态博客-Caddy-反向代理" class="headerlink" title="使用 Hexo 生成静态博客 + Caddy 反向代理"></a>使用 Hexo 生成静态博客 + Caddy 反向代理</h2><p>当你完成博客内容撰写后，就可以使用 Hexo 将其生成静态页面，供 Caddy 托管：</p><h3 id="生成静态页面"><a href="#生成静态页面" class="headerlink" title="生成静态页面"></a>生成静态页面</h3><p>在 Hexo 项目的根目录下运行：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">hexo clean &amp;&amp; hexo generate</span><br></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><span class="line">hexo clean &amp;&amp; hexo g</span><br></pre></td></tr></table></figure><p>这会将博客的 HTML、CSS、JS 等静态资源统一输出到 public&#x2F; 目录中；这个目录就是你最终需要部署的网页内容；</p><h3 id="使用-Caddy-托管博客内容"><a href="#使用-Caddy-托管博客内容" class="headerlink" title="使用 Caddy 托管博客内容"></a>使用 Caddy 托管博客内容</h3><p>假设你希望通过 https<span></span>:&#x2F;&#x2F;blog.example.com 来访问这个博客，并且你的 public&#x2F; 目录路径为 &#x2F;home&#x2F;time&#x2F;hexo-blog&#x2F;public，Caddy 的配置大致如下（具体细节见我之前写的 <a href="/2024/08/19/linux/Caddy">Caddy 文章</a>）：</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><span class="line">blog.example.com &#123;</span><br><span class="line">    root * /home/time/hexo-blog/public</span><br><span class="line">    file_server</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>保存配置后，重新加载 Caddy 即可：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> caddy reload</span><br></pre></td></tr></table></figure><blockquote><p>注意：每次修改文章或主题后，记得重新执行 hexo g 生成静态文件；</p></blockquote><p>这样，就已经将博客反向代理到本机的443端口并且开启了https，绑定了域名；</p><h2 id="使用FRP实现公网访问"><a href="#使用FRP实现公网访问" class="headerlink" title="使用FRP实现公网访问"></a>使用FRP实现公网访问</h2><p>在某些情况下，我们的服务器部署在家里或内网环境中，并没有公网 IP；这时可以借助 FRP（Fast Reverse Proxy）进行内网穿透，让外部用户也能访问部署在本地的 Hexo 博客；</p><p>国内有很多提供FRP的服务，如果你有自己的公网服务器，也可以自己搭建FRP服务端（但如果有公网服务器，谁会在内网部署服务呢）；</p><blockquote><p>需要注意的是：我国通过自定义域名实现公网访问博客是需要备案的；根据工信部规定，域名用于网站服务必须完成 ICP 备案，否则将无法解析或被阻断；</p></blockquote><p>因为 FRP 平台和服务太多了，我也没法一一列举，这里只介绍一个通用的 FRP 映射配置方式：</p><h3 id="将本地博客服务映射到公网域名"><a href="#将本地博客服务映射到公网域名" class="headerlink" title="将本地博客服务映射到公网域名"></a>将本地博客服务映射到公网域名</h3><p>假设你本地的 Hexo 博客是通过 Caddy 启动的（前文所述），那么监听端口就是在 443 端口，我们希望通过公网访问 https<span></span>:&#x2F;&#x2F;blog.example.com；</p><p>那么配置大体如下：</p><figure class="highlight ini"><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><span class="line"><span class="section">[common]</span></span><br><span class="line"><span class="attr">server_addr</span> = your.server.ip  <span class="comment"># 替换成 FRP 服务端地址</span></span><br><span class="line"><span class="attr">server_port</span> = <span class="number">7000</span>            <span class="comment"># 替换成你服务端配置的端口</span></span><br><span class="line"></span><br><span class="line"><span class="section">[hexo-blog]</span></span><br><span class="line"><span class="attr">type</span> = tcp</span><br><span class="line"><span class="attr">local_port</span> = <span class="number">443</span></span><br><span class="line"><span class="attr">custom_domains</span> = blog.example.com</span><br></pre></td></tr></table></figure><p>然后将blog.example.com域名解析为 FRP 服务端地址，这样就可以实现公网的博客访问了；</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>至此，一个完整的 Hexo 博客就搭建完成了：从安装环境、初始化项目、选择部署方式，再到通过 Caddy 提供 HTTPS 支持，最后结合 FRP 将内网服务映射到公网访问，整个过程虽然步骤不少，但每一步其实都非常清晰；而一旦搭好，后续的维护只需要写好文章、执行 hexo g，几秒钟就能上线更新，非常高效；</p><p>当然，这只是一个最基础的博客框架，还有很多可以优化的地方，比如：</p><ul><li>主题定制：Hexo 主题丰富，你可以根据自己的喜好选择合适的主题，甚至自己动手定制主题；</li><li>文章分类：Hexo 默认没有分类功能，但可以通过插件实现；</li><li>评论系统：Hexo 默认没有评论功能，但可以通过插件实现；</li><li>SEO 优化：Hexo 默认没有 SEO 优化功能，但可以通过插件实现；</li></ul><p>博客就像是一个属于自己的技术“树洞”，<strong>写给别人看是传播，写给自己看是成长</strong>；希望你也能在这个过程中收获成就感，把更多思考沉淀为内容，持续构建自己的知识体系；</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;在信息过载的时代，拥有一个属于自己的博客，不仅能整理思路、沉淀经验，还能在需要时快速复用曾经解决过的问题；而 Hexo，作为一款基于 Node.js 的轻量静态博客框架，以其部署简单、主题丰富、生成速度快的优势，成为了许多开发者的首选；&lt;/p&gt;
&lt;p&gt;我最开始只是想记录一下</summary>
      
    
    
    
    <category term="other" scheme="http://example.com/categories/other/"/>
    
    
  </entry>
  
  <entry>
    <title>通过 init 系统实现开机自启（以 systemd 和 OpenRC 为例）</title>
    <link href="http://example.com/2025/02/12/linux/systemd-OpenRC/"/>
    <id>http://example.com/2025/02/12/linux/systemd-OpenRC/</id>
    <published>2025-02-12T09:10:28.000Z</published>
    <updated>2025-02-12T09:10:28.000Z</updated>
    
    <content type="html"><![CDATA[<p>在 Linux 系统中，<strong>“开机自启”</strong> 是我们几乎绕不开的一个话题：不管是启动一个自建的服务、挂载硬盘、跑一个守护进程，还是让脚本在系统启动后自动运行，<strong>归根结底都依赖系统使用的 init 系统</strong>；</p><p>你可能搜索过相关方法，结果却一脸懵逼：</p><ul><li>有人说往 &#x2F;etc&#x2F;rc.local 里加一行就行了；</li><li>有人建议写 .service 文件放进 systemd；</li><li>更有甚者，在 Alpine 这类轻量级系统里突然蹦出个 “OpenRC”——啥啊这是？</li></ul><p>更糟的是，你跟着教程配置了一堆，结果脚本根本没执行，日志里还一点提示都没有；</p><p>我也踩过这些坑，什么 systemctl、runlevel、依赖顺序、文件权限……搞得人头大；</p><hr><p>这篇博客将结合实际情况，总结 <strong>systemd</strong> 与 <strong>OpenRC</strong> 两种主流 init 系统下的开机自启方式：</p><ul><li>在现代 Linux 系统中，<strong>systemd</strong> 已成为最主流的初始化系统和服务管理器，被包括 <strong>Debian、Ubuntu、Fedora、CentOS 7+、Arch Linux</strong> 在内的大多数发行版所采用；它提供了统一的服务控制方式（如 systemctl 命令）、并行启动优化、日志管理（journald）等丰富功能，适合中大型服务器、桌面系统及容器环境；</li><li>相比之下，<strong>OpenRC</strong> 是一个轻量级的 init 系统，广泛用于如 <strong>Alpine Linux、Gentoo</strong> 等追求极致体积与灵活性的发行版；它不依赖 systemd，使用简单的 shell 脚本管理服务，启动快速，特别适合资源受限或需要最大程度自定义的场景，如<strong>容器、嵌入式设备和自定义 Linux 构建系统</strong>；</li></ul><p>目标是：<strong>看完后你能快速写出属于自己的启动脚本，并能“优雅地”开机自启，而不是一顿 chmod +x + debug</strong>；</p><h2 id="systemd"><a href="#systemd" class="headerlink" title="systemd"></a>systemd</h2><p>systemd是主流的 init 系统，被包括 Debian、Ubuntu、Fedora、CentOS 7+、Arch Linux 在内的大多数发行版所采用；</p><p>最近整了一个成都的LXC云服务器，7块包年，配置也“感人”：128M 内存 + 1G 硬盘；不过好在有25个可分配的V4端口，并且带宽高达1Gbps，非常适合用来跑frps以及虚拟组网等服务；</p><p>所以我就打算跑一些服务，让它们后台运行并且开机自启；但可能是因为性能或者别的一些问题，我使用 nohup + &amp; 或 screen 都不稳定，别说开机自启了，连后台稳定运行都做不到；</p><p>以下以 frps 为例，演示如何在 systemd 下实现开机自启；</p><h3 id="创建服务文件"><a href="#创建服务文件" class="headerlink" title="创建服务文件"></a>创建服务文件</h3><p>首先，在 &#x2F;etc&#x2F;systemd&#x2F;system&#x2F; 目录下创建一个以 .service 结尾的文件，如 frps.service；</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> vim /etc/systemd/system/frps.service</span><br></pre></td></tr></table></figure><p>建议以后统一使用比如 emtime_xxx.service 这样的命名方式，便于识别和管理；</p><p>以下是一个最小可用服务配置：</p><figure class="highlight ini"><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><span class="line"><span class="section">[Unit]</span></span><br><span class="line"><span class="attr">Description</span>=Frp Server Service</span><br><span class="line"><span class="attr">After</span>=network.target</span><br><span class="line"></span><br><span class="line"><span class="section">[Service]</span></span><br><span class="line"><span class="attr">Type</span>=simple</span><br><span class="line"><span class="attr">User</span>=root</span><br><span class="line"><span class="attr">ExecStart</span>=/usr/local/bin/frps -c /etc/frp/frps.toml</span><br><span class="line"><span class="attr">Restart</span>=<span class="literal">on</span>-failure</span><br><span class="line"><span class="attr">RestartSec</span>=<span class="number">5</span>s</span><br><span class="line"><span class="attr">StandardOutput</span>=file:/var/log/frps.log</span><br><span class="line"><span class="attr">StandardError</span>=file:/var/log/frps-error.log</span><br><span class="line"></span><br><span class="line"><span class="section">[Install]</span></span><br><span class="line"><span class="attr">WantedBy</span>=multi-user.target</span><br></pre></td></tr></table></figure><p><strong>说明：</strong></p><ul><li>After&#x3D; 可根据需求改成 network-online.target，必要时加上 Requires&#x3D;network-online.target，这样服务会在网络可用后启动；</li><li>User 可根据需求改成其他用户，但要注意权限问题，比如访问低于1024的端口或者调用需要 root 权限的命令，都需要使用 root 用户；</li><li>ExecStart 要使用绝对路径，不能写成 .&#x2F;xxx.sh；</li><li><strong>最后的命令不能中断或退出，否则服务会直接终止</strong>；</li><li>Restart&#x3D;always 可用于无条件重启；</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><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><span class="line"><span class="comment"># 重新加载 systemd 配置</span></span><br><span class="line"><span class="built_in">sudo</span> systemctl daemon-reload</span><br><span class="line"></span><br><span class="line"><span class="comment"># 启动服务（frps 是服务名）</span></span><br><span class="line"><span class="built_in">sudo</span> systemctl start frps</span><br><span class="line"></span><br><span class="line"><span class="comment"># 设置开机自启</span></span><br><span class="line"><span class="built_in">sudo</span> systemctl <span class="built_in">enable</span> frps</span><br></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><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><span class="line"><span class="comment"># 查看服务状态</span></span><br><span class="line"><span class="built_in">sudo</span> systemctl status frps</span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看运行输出</span></span><br><span class="line"><span class="built_in">tail</span> -f /var/log/frps.log</span><br><span class="line"><span class="built_in">tail</span> -f /var/log/frps-error.log</span><br><span class="line"></span><br><span class="line"><span class="comment"># 停止 / 重启服务</span></span><br><span class="line"><span class="built_in">sudo</span> systemctl stop frps</span><br><span class="line"><span class="built_in">sudo</span> systemctl restart frps</span><br><span class="line"></span><br><span class="line"><span class="comment"># 禁用开机自启</span></span><br><span class="line"><span class="built_in">sudo</span> systemctl <span class="built_in">disable</span> frps</span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看服务日志</span></span><br><span class="line"><span class="built_in">sudo</span> journalctl -u frps -f</span><br><span class="line"><span class="built_in">sudo</span> journalctl -u frps -n 50 --no-pager</span><br><span class="line"></span><br><span class="line"><span class="comment"># 验证服务文件语法</span></span><br><span class="line"><span class="built_in">sudo</span> systemd-analyze verify /etc/systemd/system/frps.service</span><br></pre></td></tr></table></figure><h2 id="OpenRC"><a href="#OpenRC" class="headerlink" title="OpenRC"></a>OpenRC</h2><p>OpenRC 是一个轻量级的 init 系统，广泛用于 Alpine Linux、Gentoo 等发行版；</p><p>还是以frps为例，演示如何在 OpenRC 系统（如 Alpine）中实现开机自启；</p><h3 id="安装-OpenRC（如未预装）"><a href="#安装-OpenRC（如未预装）" class="headerlink" title="安装 OpenRC（如未预装）"></a>安装 OpenRC（如未预装）</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">apk add openrc --no-cache</span><br></pre></td></tr></table></figure><h3 id="编写启动脚本"><a href="#编写启动脚本" class="headerlink" title="编写启动脚本"></a>编写启动脚本</h3><p>在 OpenRC 中，服务通过位于 &#x2F;etc&#x2F;init.d&#x2F; 的脚本来管理；我们可以手动编写一个脚本来启动 frps：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> nano /etc/init.d/frps</span><br></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><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><span class="line"><span class="meta">#!/sbin/openrc-run</span></span><br><span class="line"></span><br><span class="line">name=<span class="string">&quot;frps&quot;</span></span><br><span class="line">description=<span class="string">&quot;Frp server&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">command</span>=<span class="string">&quot;/home/alpine/app/frp/frps&quot;</span></span><br><span class="line">command_args=<span class="string">&quot;-c /home/alpine/app/frp/frps.toml&quot;</span></span><br><span class="line">pidfile=<span class="string">&quot;/run/<span class="variable">$&#123;RC_SVCNAME&#125;</span>.pid&quot;</span></span><br><span class="line"></span><br><span class="line">output_log=<span class="string">&quot;/var/log/frps.log&quot;</span></span><br><span class="line">error_log=<span class="string">&quot;/var/log/frps.err&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="title">depend</span></span>() &#123;</span><br><span class="line">    after sshd</span><br><span class="line">    need net</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="title">start_pre</span></span>() &#123;</span><br><span class="line">    <span class="comment"># 确保 /run 目录存在，并可写入 PID 文件</span></span><br><span class="line">    checkpath --directory /run --owner root:root</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 创建或检查日志文件</span></span><br><span class="line">    checkpath --file --mode 0644 /var/log/frps.log /var/log/frps.err</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">command_background=<span class="string">&quot;yes&quot;</span></span><br></pre></td></tr></table></figure><p><strong>说明：</strong></p><ul><li>command&#x3D; 和 command_args&#x3D; 指定要执行的命令及参数；</li><li>pidfile&#x3D; 指定 PID 文件的路径，通常建议放在 &#x2F;run；</li><li>output_log&#x3D; 和 error_log&#x3D; 可指定标准输出和错误输出文件路径；</li><li>depend() 中声明依赖项，如网络（need net）和 SSH；</li><li>start_pre() 在启动服务前执行一些准备工作，如创建所需目录或文件；</li><li>command_background&#x3D;”yes” 表示服务以后台方式运行；</li></ul><h3 id="添加可执行权限"><a href="#添加可执行权限" class="headerlink" title="添加可执行权限"></a>添加可执行权限</h3><p>OpenRC 启动脚本必须具有执行权限：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> <span class="built_in">chmod</span> +x /etc/init.d/frps</span><br></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><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><span class="line"><span class="comment"># 启动服务</span></span><br><span class="line"><span class="built_in">sudo</span> rc-service frps start</span><br><span class="line"></span><br><span class="line"><span class="comment"># 停止服务</span></span><br><span class="line"><span class="built_in">sudo</span> rc-service frps stop</span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看服务状态</span></span><br><span class="line"><span class="built_in">sudo</span> rc-service frps status</span><br><span class="line"></span><br><span class="line"><span class="comment"># 设置开机启动</span></span><br><span class="line"><span class="built_in">sudo</span> rc-update add frps</span><br><span class="line"></span><br><span class="line"><span class="comment"># 取消开机启动</span></span><br><span class="line"><span class="built_in">sudo</span> rc-update del frps</span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看所有开机自启服务</span></span><br><span class="line"><span class="built_in">sudo</span> rc-update show</span><br></pre></td></tr></table></figure><h3 id="提示与建议"><a href="#提示与建议" class="headerlink" title="提示与建议"></a>提示与建议</h3><ul><li><strong>日志调试</strong>：OpenRC 本身不会自动保存日志，建议你手动查看日志文件或将日志写入 &#x2F;var&#x2F;log 中的文件（如上所示），确保问题可定位；</li><li><strong>后台守护进程注意事项</strong>：确保你的命令不会自行退出（即不会直接跑完就结束），否则 OpenRC 会认为服务失败；</li><li><strong>目录权限问题</strong>：某些系统（特别是 Alpine）在 &#x2F;run 下的临时目录可能会重置，建议服务脚本中始终用 checkpath 创建并赋权；</li><li><strong>写 PID 的必要性</strong>：OpenRC 通过 pidfile 判断服务状态，漏写 PID 文件可能导致服务状态判断异常；</li></ul><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>无论是主流发行版中功能强大的 systemd，还是轻量简洁、广泛用于容器的 OpenRC，它们都承担着 Linux 系统中“启动与管理服务”的关键角色；</p><p>这篇文章从两个角度出发，分别介绍了如何：</p><ul><li>编写 systemd 的 .service 文件，实现服务注册、日志管理、异常重启等；</li><li>使用 OpenRC 编写 init 脚本，借助 checkpath 和 command_background 等机制实现后台运行和日志落地；</li></ul><blockquote><p>在实际使用中，你只需要记住：</p><ul><li>systemd 写的是配置文件，放在 &#x2F;etc&#x2F;systemd&#x2F;system&#x2F;；</li><li>OpenRC 写的是可执行脚本，放在 &#x2F;etc&#x2F;init.d&#x2F;；</li><li>二者都可以通过 enable 或 rc-update add 实现“开机自启”；</li></ul></blockquote><p>最后，无论你是跑一个简单的守护进程，还是想在嵌入式设备上部署自己的服务，只要搞清楚系统用的是什么 init 系统，<strong>再结合本文中的方法，一般都能跑通</strong>（别再满世界搜 &#x2F;etc&#x2F;rc.local 了，那已经是“老黄历”了）；</p><p>希望本文能帮你绕过“服务起不来没日志还没报错”的痛苦，优雅地把脚本跑起来；</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;在 Linux 系统中，&lt;strong&gt;“开机自启”&lt;/strong&gt; 是我们几乎绕不开的一个话题：不管是启动一个自建的服务、挂载硬盘、跑一个守护进程，还是让脚本在系统启动后自动运行，&lt;strong&gt;归根结底都依赖系统使用的 init 系统&lt;/strong&gt;；&lt;/p&gt;
&lt;p</summary>
      
    
    
    
    <category term="linux" scheme="http://example.com/categories/linux/"/>
    
    
  </entry>
  
  <entry>
    <title>SEGGER RTT：嵌入式调试的高效输出利器</title>
    <link href="http://example.com/2024/10/28/mcu/SEGGER-RTT/"/>
    <id>http://example.com/2024/10/28/mcu/SEGGER-RTT/</id>
    <published>2024-10-27T17:18:07.000Z</published>
    <updated>2024-10-27T17:18:07.000Z</updated>
    
    <content type="html"><![CDATA[<p>参考：<a class="link"   href="https://www.armbbs.cn/forum.php?mod=viewthread&tid=86177" >https://www.armbbs.cn/forum.php?mod=viewthread&amp;tid=86177<i class="fas fa-external-link-alt"></i></a></p><p>在嵌入式开发中，<strong>如何在不影响系统实时性的前提下，高效输出调试信息</strong>，一直是让人头疼的问题；传统的串口 printf 虽然简单易用，但在输出量大、串口速率较低时，就可能严重阻塞系统主流程，甚至引发时序异常；</p><p>为了解决这些问题，<strong>SEGGER 提供了 RTT（Real-Time Transfer）机制</strong>，它是一种利用 J-Link 仿真器在目标设备与主机之间进行高速内存通信的方案；它几乎不占用 CPU 时间、无需占用串口资源，而且速度快到令人惊喜，非常适合用于日志输出、数据查看甚至远程控制；</p><p>这篇博客将围绕 SEGGER RTT 展开，介绍简单原理、使用方法、常见配置及注意事项，帮助你在项目中轻松上手 RTT 输出，彻底告别“卡顿的串口 printf”；</p><h2 id="RTT简介"><a href="#RTT简介" class="headerlink" title="RTT简介"></a>RTT简介</h2><h3 id="什么是RTT"><a href="#什么是RTT" class="headerlink" title="什么是RTT"></a>什么是RTT</h3><p>RTT 是 SEGGER 提供的一种 <strong>通过调试接口进行双向通信</strong> 的机制，所有支持 J-Link 的调试器都可以使用这个功能；它的核心优势在于：</p><ul><li>高速传输：输出字符速度远超 SWO 和半主机模式；</li><li>非阻塞：不会影响目标程序的实时执行；</li><li>无需额外引脚：不依赖 SWO，引脚需求低；</li></ul><p>RTT 的传输是通过 SWD 或 JTAG 接口完成的，不需要像传统串口那样占用 UART 资源，也不需要额外的 IO；只要接好 SWDIO、SWCLK（必要时加 VCC 和 NRST），甚至用三线 JLINK-OB 也能正常使用；</p><h3 id="多通道-双向通信"><a href="#多通道-双向通信" class="headerlink" title="多通道+双向通信"></a>多通道+双向通信</h3><p>RTT 不只是一个 printf 替代品，它支持 多通道双向通信，这意味着你可以像使用多个串口终端一样，将不同类别的日志输出到不同的“虚拟终端”中；</p><p>SEGGER 官方提供的工具 RTT Viewer 支持多个终端窗口，可以将标准输出、错误信息、调试日志分流显示，条理清晰，非常适合调试复杂系统；</p><h3 id="RTT-vs-SWO-vs-半主机模式"><a href="#RTT-vs-SWO-vs-半主机模式" class="headerlink" title="RTT vs SWO vs 半主机模式"></a>RTT vs SWO vs 半主机模式</h3><p>官方做过一组速度测试（以 STM32F407、168MHz 为例）：</p><table><thead><tr><th>方式</th><th>输出82字符耗时</th></tr></thead><tbody><tr><td>RTT</td><td>1<strong>us</strong></td></tr><tr><td>SWO</td><td>120<strong>μs</strong></td></tr><tr><td>半主机模式</td><td>10700<strong>μs</strong></td></tr></tbody></table><p>可以看出，<strong>RTT 不只是快一点，而是快了几个数量级</strong>！如果你以前用的是 SWO 或半主机，这种性能差距足以让你考虑迁移到 RTT；</p><h3 id="RTT-与硬件兼容性"><a href="#RTT-与硬件兼容性" class="headerlink" title="RTT 与硬件兼容性"></a>RTT 与硬件兼容性</h3><p>RTT 不依赖 SWO 引脚，这对大多数小型开发板非常友好；基本上只要你能用 J-Link 下载程序，就能用 RTT 输出调试信息；</p><p>而且 RTT 支持多种接口速度的 J-Link 设备；在默认的 512 字节缓冲区配置下，普通速度的 J-Link 也可以跑到 0.5MB&#x2F;s，高端版甚至可以达到 1MB&#x2F;s，轻松应对大体量日志输出的需求；</p><h2 id="RTT-工作原理简单解析"><a href="#RTT-工作原理简单解析" class="headerlink" title="RTT 工作原理简单解析"></a>RTT 工作原理简单解析</h2><h3 id="核心结构"><a href="#核心结构" class="headerlink" title="核心结构"></a>核心结构</h3><p>RTT 的核心在于它在目标芯片的 RAM 中创建了一个称为 <strong>RTT 控制块（RTT Control Block）</strong> 的结构；这个控制块包含：</p><ul><li>一个 <strong>标识符（ID）</strong>：用于让 J-Link 调试器能快速在芯片内存中定位这个控制块；</li><li>多个 <strong>通道结构体</strong>：每个通道对应一个缓冲区，用来描述缓冲区地址、大小、读写指针等状态信息；</li></ul><p>RTT 支持 <strong>多个上行通道（Up Buffers）</strong> 和 <strong>多个下行通道（Down Buffers）</strong>，即从芯片上传到 PC 端，或者从 PC 发送数据到芯片；这使得 RTT 可以支持例如“标准输出”“错误日志”“调试日志”等多路并行通信；</p><p>通道数量和每个缓冲区的大小可以通过编译配置定义，也支持在运行时动态注册新的缓冲区；</p><h3 id="数据写入策略"><a href="#数据写入策略" class="headerlink" title="数据写入策略"></a>数据写入策略</h3><p>每个 RTT 通道可以配置为阻塞模式或非阻塞模式：</p><ul><li><strong>阻塞模式</strong>：当缓冲区满了，应用程序会<strong>等待</strong>，直到 J-Link 读出数据腾出空间；这保证了日志不会丢失，但有可能会阻塞当前任务执行；</li><li><strong>非阻塞模式</strong>：当缓冲区满时，多余的数据会<strong>直接丢弃</strong>，程序继续正常执行；这种模式下，即使调试器没有连接，也不会影响系统实时行为；</li></ul><h3 id="缓冲区工作"><a href="#缓冲区工作" class="headerlink" title="缓冲区工作"></a>缓冲区工作</h3><p>每个缓冲区本质上是一个环形队列，用一对指针进行数据管理：</p><ul><li><p>对于<strong>上行通道（芯片 → 主机）</strong>：</p><ul><li><strong>写入指针</strong>由应用程序维护（写入数据）；</li><li><strong>读取指针</strong>由 J-Link 维护（从芯片读数据）；</li></ul></li><li><p>对于<strong>下行通道（主机 → 芯片）</strong>：</p><ul><li><strong>写入指针</strong>由 J-Link 写；</li><li><strong>读取指针</strong>由芯片读取；</li></ul></li></ul><p>当读取指针和写入指针相等时，说明缓冲区为空；</p><p>这种设计方式确保了调试器和芯片之间的同步传输，不需要中断，也不依赖外设，从而最大限度地减少对系统性能的干扰；</p><h2 id="RTT移植"><a href="#RTT移植" class="headerlink" title="RTT移植"></a>RTT移植</h2><h3 id="准备-RTT-组件"><a href="#准备-RTT-组件" class="headerlink" title="准备 RTT 组件"></a>准备 RTT 组件</h3><p>RTT 随 J-Link 工具链一同提供，无需单独下载安装；你可以在默认安装路径下找到 RTT 示例代码，路径如下：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">C:\Program Files\SEGGER\JLink\Samples\RTT</span><br></pre></td></tr></table></figure><p>你可以直接把 RTT 目录下的所有文件打包带入你的工程中；</p><h3 id="关于-RTT-汇编加速文件（可选）"><a href="#关于-RTT-汇编加速文件（可选）" class="headerlink" title="关于 RTT 汇编加速文件（可选）"></a>关于 RTT 汇编加速文件（可选）</h3><p>新版本 RTT 文件夹中包含一个用于优化的汇编文件（如 SEGGER_RTT_ASM_ARM.S），作用是加速字符输出（通过优化内存访问方式），在高频率大数据量输出时能提高吞吐率；</p><p>不过由于不同编译器对汇编支持略有差异，<strong>如果你暂时用不上或不清楚用法，也可以先忽略这个文件</strong>，不会影响基本使用；</p><h3 id="缓冲区大小配置说明"><a href="#缓冲区大小配置说明" class="headerlink" title="缓冲区大小配置说明"></a>缓冲区大小配置说明</h3><p>RTT 的配置文件为 SEGGER_RTT_Conf.h，你需要关注三个重要参数：</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><span class="line"><span class="meta">#<span class="keyword">define</span> BUFFER_SIZE_UP                        1024  <span class="comment">// MCU -&gt; PC</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> BUFFER_SIZE_DOWN                      16    <span class="comment">// PC -&gt; MCU</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> SEGGER_RTT_PRINTF_BUFFER_SIZE         64    <span class="comment">// 格式化临时缓冲区</span></span></span><br></pre></td></tr></table></figure><blockquote><p>默认 BUFFER_SIZE_DOWN 是 16，不能随便改太大，否则 RTT Viewer 不一定能正常接收；</p></blockquote><p>缓冲区选型建议（根据传输频率和单次数据量）：</p><table><thead><tr><th>情况</th><th>建议缓冲区大小</th></tr></thead><tbody><tr><td>每秒发送 10 次 × 每次 1 字节</td><td>6 字节</td></tr><tr><td>每次 2 字节</td><td>11 字节</td></tr><tr><td>每次 5 字节</td><td>31 字节</td></tr><tr><td>每次 10 字节</td><td>61 字节</td></tr><tr><td>每次 50 字节</td><td>401 字节</td></tr><tr><td>每秒发送 1 次 × 每次 500 字节</td><td>501 字节</td></tr></tbody></table><p>暴力点来说，<strong>大多数项目配置 1024 字节基本是绰绰有余的</strong>，除非你是数据刷屏狂魔；</p><h3 id="RTT-最小使用代码"><a href="#RTT-最小使用代码" class="headerlink" title="RTT 最小使用代码"></a>RTT 最小使用代码</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></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;SEGGER_RTT.h&quot;</span></span></span><br><span class="line"></span><br><span class="line"><span class="type">uint8_t</span> RTT_Buffer_R[<span class="number">16</span>] = &#123;<span class="number">0</span>&#125;;</span><br><span class="line"><span class="type">uint32_t</span> RTT_Count_R = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">    SEGGER_RTT_ConfigUpBuffer(<span class="number">0</span>, <span class="string">&quot;RTTUP&quot;</span>, <span class="literal">NULL</span>, <span class="number">0</span>, SEGGER_RTT_MODE_NO_BLOCK_SKIP);</span><br><span class="line">    SEGGER_RTT_ConfigDownBuffer(<span class="number">0</span>, <span class="string">&quot;RTTDOWN&quot;</span>, <span class="literal">NULL</span>, <span class="number">0</span>, SEGGER_RTT_MODE_NO_BLOCK_SKIP);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">while</span> (<span class="number">1</span>)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="type">static</span> <span class="type">int</span> i = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 设置当前使用的终端为 0</span></span><br><span class="line">        SEGGER_RTT_SetTerminal(<span class="number">0</span>);</span><br><span class="line">        SEGGER_RTT_printf(<span class="number">0</span>, RTT_CTRL_TEXT_RED<span class="string">&quot;%d\r\n&quot;</span>, i++);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 设置当前使用的终端为 1</span></span><br><span class="line">        SEGGER_RTT_SetTerminal(<span class="number">1</span>);</span><br><span class="line">        SEGGER_RTT_printf(<span class="number">0</span>, <span class="string">&quot;%d\r\n&quot;</span>, i++);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 从 PC 端接收数据，默认最多只能接收 16 字节</span></span><br><span class="line">        RTT_Count_R = SEGGER_RTT_Read(<span class="number">0</span>, RTT_Buffer_R, <span class="number">16</span>);</span><br><span class="line">        <span class="keyword">if</span> (RTT_Count_R)</span><br><span class="line">        &#123;</span><br><span class="line">            RTT_Buffer_R[RTT_Count_R] = <span class="string">&#x27;\0&#x27;</span>;  <span class="comment">// 添加字符串终止符</span></span><br><span class="line">            SEGGER_RTT_printf(<span class="number">0</span>, RTT_CTRL_TEXT_YELLOW<span class="string">&quot;%s\r\n&quot;</span>, RTT_Buffer_R);</span><br><span class="line">            RTT_Count_R = <span class="number">0</span>;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>说明：</p><ul><li>SEGGER_RTT_SetTerminal(n) 设置的是在哪个虚拟终端窗口显示；</li><li>SEGGER_RTT_printf(0, …) 的第一个参数是 <strong>通道编号</strong>，默认为 0，固定使用；</li></ul><blockquote><p>注意：虚拟终端编号与通道编号并非一回事，通道编号决定数据流向的通道，而终端编号仅影响在 RTT Viewer 中显示的窗口；</p></blockquote><ul><li>接收缓冲区推荐用 16 字节，因为 RTT Viewer 默认一次只支持发送一个字符，除非启用了“Send on Enter”；</li></ul><h3 id="使用-J-Link-RTT-Viewer-工具"><a href="#使用-J-Link-RTT-Viewer-工具" class="headerlink" title="使用 J-Link RTT Viewer 工具"></a>使用 J-Link RTT Viewer 工具</h3><p>打开 J-Link RTT Viewer，选择如下参数：</p><ul><li>Interface：JTAG 或 SWD（视你的硬件连接而定）；</li><li>Speed：建议设置为中等速度，比如 1000kHz，太高可能不稳定；</li><li>连接不上时：点击菜单 File -&gt; Connect 尝试重连；</li><li>多终端支持：在工具右下角可以勾选多个终端进行输出区分；</li><li>输入设置：建议勾选 Send on Enter，这样输入多个字符后按下回车才会发送，方便处理字符串指令；</li></ul><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>SEGGER RTT 凭借其<strong>高速、非阻塞、零占用外设资源</strong>等优势，成为现代嵌入式开发中调试输出的理想方案；相比传统串口、SWO 或半主机模式，RTT 不仅性能更优，还具有更强的灵活性，支持多通道、双向通信，使日志输出更加有序高效；</p><p>通过本篇文章的介绍，我们了解了 RTT 的工作原理、配置方式、缓冲策略以及实际使用示例，并结合实际开发场景给出了配置建议；如果你在项目中苦于串口调试带来的性能瓶颈，不妨试试 RTT——它不仅能大幅提升调试效率，还能让你更专注于系统逻辑本身，而不是被调试工具“牵着鼻子走”；</p><p>此外，<strong>RTT 的下行通道支持从 PC 向目标设备发送数据</strong>，这为构建交互式调试终端提供了可能；例如，<strong>可以将 <a href="/2024/02/14/mcu/letter-shell1">Letter Shell</a> 的输入输出绑定到 RTT 通道上</strong>，实现无需串口、无需 IO 引脚的 Shell 控制台；配合 RTT Viewer 或自定义串口工具，你可以像操作普通终端一样通过 J-Link 实时操控设备，极大地拓展了嵌入式调试的边界；</p><p>未来在配合如 RTT Viewer、JScope、SystemView 等 SEGGER 工具时，RTT 还能拓展出更多实用功能，助力系统分析与性能评估；写代码的时候不卡壳，调试输出更清爽，从一步启用 RTT 开始；</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;参考：&lt;a class=&quot;link&quot;   href=&quot;https://www.armbbs.cn/forum.php?mod=viewthread&amp;tid=86177&quot; &gt;https://www.armbbs.cn/forum.php?mod=viewthread&amp;amp;</summary>
      
    
    
    
    <category term="mcu" scheme="http://example.com/categories/mcu/"/>
    
    
  </entry>
  
  <entry>
    <title>实现 USB Composite 设备：在同一接口上模拟多个设备（含 FatFS U盘）</title>
    <link href="http://example.com/2024/10/27/mcu/USB-Composite/"/>
    <id>http://example.com/2024/10/27/mcu/USB-Composite/</id>
    <published>2024-10-27T01:41:19.000Z</published>
    <updated>2024-10-27T01:41:19.000Z</updated>
    
    <content type="html"><![CDATA[<p>仓库：<a class="link"   href="https://github.com/alambe94/I-CUBE-USBD-Composite" >https://github.com/alambe94/I-CUBE-USBD-Composite<i class="fas fa-external-link-alt"></i></a><br>参考：<a class="link"   href="https://blog.csdn.net/u012936480/article/details/137411833" >https://blog.csdn.net/u012936480/article/details/137411833<i class="fas fa-external-link-alt"></i></a></p><p>在嵌入式开发中，USB 是一个非常重要的外设接口，它不仅可以用来调试、传输数据，还能为产品增加多样化的功能体验；而如果你希望在一个 USB 接口上同时实现多个设备功能，比如<strong>既能当串口调试工具、又能当 U 盘存储设备，甚至还能充当 HID 控制器</strong>，那你就需要掌握 USB Composite（复合设备） 的实现方式；</p><p>本篇博客将结合<a href="/2024/10/26/mcu/FatFS">前文实现的 FatFS 文件系统</a>功能，进一步介绍如何在 STM32 平台上实现 USB Composite 设备；我们将以 <strong>U盘 + 虚拟串口（CDC）</strong> 为例，完整讲解如何配置多个接口；通过这一实践，你将掌握一线多用的 USB 技巧，为你的嵌入式设备赋予更多“身份”和可能性；</p><p>本文采用了 GitHub 上一个开源项目（I-CUBE-USBD-Composite）作为基础，该项目已经实现了 USB Composite 的关键功能；我们基于它，以简单快捷的方式完成了 U 盘 + CDC 的复合设备实现；</p><h2 id="文件下载"><a href="#文件下载" class="headerlink" title="文件下载"></a>文件下载</h2><p>链接: <a class="link"   href="https://pan.baidu.com/s/10a2SehxSUwr8pd7I2zs45w?pwd=dbtc" >https://pan.baidu.com/s/10a2SehxSUwr8pd7I2zs45w?pwd=dbtc<i class="fas fa-external-link-alt"></i></a> 提取码: dbtc </p><p>链接中是一个 pack 文件，但是这个文件并不是给 MDK 使用的，而是给 STM32CubeMX 使用的，我们需要在 STM32CubeMX 中导入这个文件，作为组件库引入我们的工程中；</p><p>导入文件步骤如下：</p><center>   <img     src="https://picture.orangetime.top/i/2025/06/25/685ae35335f95.png"  ></center><center>   <img     src="https://picture.orangetime.top/i/2025/06/25/685ae36f1d0a5.png"  ></center><h2 id="工程引入"><a href="#工程引入" class="headerlink" title="工程引入"></a>工程引入</h2><p>导入完成之后，我们还需要在 CubeMX 中勾选这个组件库，选择所需的功能 ，如下：</p><center>   <img     src="https://picture.orangetime.top/i/2025/06/25/685ae3b35928d.png"  ></center><center>   <img     src="https://picture.orangetime.top/i/2025/07/04/6867acf5ea71a.png"  ></center><p>其中，Core 是必须勾选的，如果你要同时使用 CDC 和 U 盘，那么不仅 CDC_ACM 和 MSC_BOT 都需要勾选，而且还<strong>必须</strong>勾选 COMPISITE,否则 USB Composite 功能将无法正常使用；</p><p>在导入组件库之后，我们还需要在 CubeMX 中配置一下 USB 相关的配置，如下：</p><ul><li>配置 USB 外设的中断，否则 USB 外设将无法正常工作</li></ul><center>   <img     src="https://picture.orangetime.top/i/2025/06/25/685ae5085d7e9.png"  ></center><ul><li>配置组件库</li></ul><center>   <img     src="https://picture.orangetime.top/i/2025/06/25/685ae5bc20669.png"  ></center><p>博客使用 F103 作为例子，所以还需要选择最后一项；</p><h2 id="代码修改"><a href="#代码修改" class="headerlink" title="代码修改"></a>代码修改</h2><p>配置好之后，我们直接使用 CubeMX 生成代码即可，代码中需要修改的地方不多，主要是初始化和对应 USB 功能的逻辑，如下：</p><h3 id="引入工程"><a href="#引入工程" class="headerlink" title="引入工程"></a>引入工程</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></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;usb_device.h&quot;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;usbd_cdc_acm_if.h&quot;</span></span></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">    ...</span><br><span class="line">    MX_USB_DEVICE_Init();</span><br><span class="line">    ...</span><br><span class="line"></span><br><span class="line">    <span class="keyword">while</span>(<span class="number">1</span>)</span><br><span class="line">    &#123;</span><br><span class="line">        ...</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="功能逻辑"><a href="#功能逻辑" class="headerlink" title="功能逻辑"></a>功能逻辑</h3><p>每一个 USB 功能都对应一个文件，比如 CDC 功能对应的是 usbd_cdc_acm_if.c，我们只需要在对应的文件中添加我们需要的逻辑即可，因为在<a href="/2024/10/26/mcu/FatFS">之前的博客</a>中已经实现了 FatFS 文件系统，所以我们今天主要将重点放在 CDC 与 U 盘的处理逻辑上；</p><h4 id="CDC-串口"><a href="#CDC-串口" class="headerlink" title="CDC 串口"></a>CDC 串口</h4><p>CDC（Communication Device Class）是一种虚拟串口接口，库文件中已经封装好了基本的收发逻辑：发送数据时直接调用发送函数，接收到数据时则会自动调用接收回调函数；</p><blockquote><p>听起来一切都很完美，但这背后其实还隐藏着一个关键问题；</p></blockquote><p>答案是：<strong>理解 USB 的传输机制，特别是数据包的处理方式</strong>；</p><p>USB 在底层按“包（Packet）”传输，CDC 使用的传输类型为 Bulk（批量传输），**每个端点的最大数据包大小（Max Packet Size）**对于全速设备通常为 64 字节；因此，大于 64 字节的数据会被分为多个包发送；这意味着：</p><ul><li>如果我们发送的数据长度小于等于 64 字节，可以一包发完；</li><li>如果发送的数据长度大于 64 字节，则需要拆分为多个数据包进行分批发送；</li></ul><p><strong>在发送方面</strong>，库通常已经封装好了自动拆包的逻辑；我们调用发送函数时，只需要传入需要发送的缓冲区和长度，不必关心包的边界或分段细节；</p><p><strong>接收就没那么轻松了；</strong></p><p>虽然 USB 主机会自动将数据拆分为多个包发送，但在设备端的接收回调函数中，我们<strong>每次只能获取一包数据</strong>，这意味着：如果一条完整的数据大于 64 字节，必须<strong>手动将这些数据包拼接起来</strong>，然后再统一处理；</p><p>那么，<strong>我们该如何判断数据是否接收完毕</strong>？</p><p>我的做法是引入了一个“<strong>接收超时判断</strong>”机制；每当接收到一包数据时，就刷新一次超时计时器；如果在某个预设时间内没有再接收到新的数据包，就说明数据应该已经接收完毕，可以进行处理了（算是串口空闲的简单实现思路）；</p><p>这种方法简单实用，特别适合一些非实时、包长度不确定的串口协议；</p><p>如果你希望更稳妥地判断数据边界，也可以考虑使用协议头尾、长度字段等方式进行辅助判断；但在简单应用中，“超时判断法”是一个很实用的起点；</p><p>头文件 usbd_cdc_acm_if.h 中需要添加以下内容：</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 相关宏定义</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> CDC_RX_BUF_SIZE    512   <span class="comment">// 缓冲区大小，根据实际需求调整</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> CDC_TIMEOUT_MS     6     <span class="comment">// 超过 6ms 没数据，认为一帧结束，结合实际一包发送的时间调整</span></span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 相关函数声明</span></span><br><span class="line"><span class="type">void</span> <span class="title function_">process_usb_frame</span><span class="params">(<span class="type">uint8_t</span>* data, <span class="type">uint16_t</span> len)</span>;  <span class="comment">// 处理接收到的 USB 数据帧</span></span><br><span class="line"><span class="type">void</span> <span class="title function_">check_usb_timeout</span><span class="params">(<span class="type">void</span>)</span>;                         <span class="comment">// 检测 USB 接收超时，超时后调用 process_usb_frame</span></span><br></pre></td></tr></table></figure><p>源文件 usbd_cdc_acm_if.c 中需要添加以下内容：</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 相关变量定义</span></span><br><span class="line"><span class="type">uint8_t</span> cdc_rx_buffer[CDC_RX_BUF_SIZE];</span><br><span class="line"><span class="type">uint16_t</span> cdc_rx_index = <span class="number">0</span>;</span><br><span class="line"><span class="type">uint32_t</span> cdc_last_recv_tick = <span class="number">0</span>;</span><br><span class="line"><span class="type">uint8_t</span>  cdc_receiving = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 修改 CDC_Receive 函数</span></span><br><span class="line"><span class="type">static</span> <span class="type">int8_t</span> <span class="title function_">CDC_Receive</span><span class="params">(<span class="type">uint8_t</span> cdc_ch, <span class="type">uint8_t</span> *Buf, <span class="type">uint32_t</span> *Len)</span></span><br><span class="line">&#123;</span><br><span class="line">  <span class="comment">/* USER CODE BEGIN 6 */</span></span><br><span class="line">  <span class="comment">// 拼接到总缓冲区</span></span><br><span class="line">  <span class="keyword">if</span>(cdc_rx_index + *Len &lt; CDC_RX_BUF_SIZE)</span><br><span class="line">  &#123;</span><br><span class="line">      <span class="built_in">memcpy</span>(&amp;cdc_rx_buffer[cdc_rx_index], Buf, *Len);</span><br><span class="line">      cdc_rx_index += *Len;</span><br><span class="line">      cdc_last_recv_tick = HAL_GetTick(); <span class="comment">// 更新时间戳</span></span><br><span class="line">      cdc_receiving = <span class="number">1</span>;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">else</span></span><br><span class="line">  &#123;</span><br><span class="line">      <span class="comment">// 缓冲区溢出，复位</span></span><br><span class="line">      cdc_rx_index = <span class="number">0</span>;</span><br><span class="line">      cdc_receiving = <span class="number">0</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  USBD_CDC_SetRxBuffer(cdc_ch, &amp;hUsbDevice, &amp;Buf[<span class="number">0</span>]);</span><br><span class="line">  USBD_CDC_ReceivePacket(cdc_ch, &amp;hUsbDevice);</span><br><span class="line">  <span class="keyword">return</span> (USBD_OK);</span><br><span class="line">  <span class="comment">/* USER CODE END 6 */</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 处理 USB 数据帧</span></span><br><span class="line"><span class="type">void</span> <span class="title function_">process_usb_frame</span><span class="params">(<span class="type">uint8_t</span>* data, <span class="type">uint16_t</span> len)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="comment">// 举例：打印字符串帧</span></span><br><span class="line">    data[len] = <span class="string">&#x27;\0&#x27;</span>; <span class="comment">// 保证以 \0 结尾</span></span><br><span class="line">    CDC_Transmit(<span class="number">0</span>, data, len);  <span class="comment">// 回显</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 检测 USB 接收超时</span></span><br><span class="line"><span class="type">void</span> <span class="title function_">check_usb_timeout</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">if</span>(cdc_receiving)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">if</span>(HAL_GetTick() - cdc_last_recv_tick &gt; CDC_TIMEOUT_MS)</span><br><span class="line">        &#123;</span><br><span class="line">            <span class="comment">// 超时，处理一帧数据</span></span><br><span class="line">            process_usb_frame(cdc_rx_buffer, cdc_rx_index);</span><br><span class="line">            cdc_rx_index = <span class="number">0</span>;</span><br><span class="line">            cdc_receiving = <span class="number">0</span>;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在 main.c 的 while 循环中添加 check_usb_timeout 函数即可；</p><blockquote><p><strong>注意</strong>：</p><ul><li>这套库中的用户代码部分是没有作用的，你每次使用 CubeMX 生成代码都会覆盖掉你写的代码，无论你写在注释部分或者非注释部分，重新生成之后都会消失，所以可以找到 CubeMX 存储的位置，找到库文件，手动修改库文件，这样每次生成的代码都会是有处理逻辑的代码；<br>我的存储位置如下，在 C:\Users\EMTime\STM32Cube\Repository\Packs\AL94\I-CUBE-USBD-COMPOSITE\1.0.3\Middlewares\Third_Party\COMPOSITE\App 中即可找到对应的文件，打开进行修改即可；<br>放心，USB 的逻辑部分不像别的代码一样需要频繁修改，你写好之后，大部分情况可以直接使用；</li></ul></blockquote><center>   <img     src="https://picture.orangetime.top/i/2025/06/25/685bb2865f2ed.png"  ></center><blockquote><p><strong>注意</strong>：虽然虚拟串口数量可以在 CubeMX 中配置，但由于 I-CUBE-USBD-COMPOSITE 库在生成代码时会覆盖相关配置，这项设置在实际工程中并不会生效；真正起作用的是头文件 AL94.I-CUBE-USBD-COMPOSITE_conf.h 中的 _USBD_CDC_ACM_COUNT 宏定义，该值才是决定虚拟串口个数的关键参数；</p></blockquote><h4 id="MSC-U盘"><a href="#MSC-U盘" class="headerlink" title="MSC U盘"></a>MSC U盘</h4><p>因为之前已经实现了 FatFS 文件系统，所以关于文件系统的部分不过多赘述，USB 模拟 U 盘仅需实现 usbd_msc_if.c 中下面几个函数即可：</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></pre></td><td class="code"><pre><span class="line"><span class="type">int8_t</span> <span class="title function_">STORAGE_GetCapacity</span><span class="params">(<span class="type">uint8_t</span> lun, <span class="type">uint32_t</span> *block_num, <span class="type">uint16_t</span> *block_size)</span></span><br><span class="line">&#123;</span><br><span class="line">  <span class="comment">/* USER CODE BEGIN 3 */</span></span><br><span class="line"></span><br><span class="line">  <span class="comment">// 如果使用我之前博客中提供的代码，则这里可以不用自己计算扇区数和扇区大小，和我写一样的即可</span></span><br><span class="line"></span><br><span class="line">  <span class="comment">// 总扇区数，和存储容量有关，扇区数 = 总容量 / 每个扇区大小，比如 16MB SPI Flash 的扇区数 = 16M / 4096 = 16 * 1024 * 1024 / 4096 = 4096，那么这里就是4096</span></span><br><span class="line">  *block_num  = Flash_Sector_Count;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 每个扇区大小，和存储类型有关，如果是 SPI FLash，则是 4096，如果是内部 FLash 或者 SD 卡，则是 512</span></span><br><span class="line">  *block_size = Flash_Sector_Size;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (USBD_OK);</span><br><span class="line">  <span class="comment">/* USER CODE END 3 */</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">int8_t</span> <span class="title function_">STORAGE_Read</span><span class="params">(<span class="type">uint8_t</span> lun, <span class="type">uint8_t</span> *buf, <span class="type">uint32_t</span> blk_addr, <span class="type">uint16_t</span> blk_len)</span></span><br><span class="line">&#123;</span><br><span class="line">  <span class="comment">/* USER CODE BEGIN 6 */</span></span><br><span class="line"></span><br><span class="line">  <span class="comment">// 我之前博客中提供的读取函数如下所示，改成你自己的即可</span></span><br><span class="line">  FLASH_RD_Block_Start(blk_addr * <span class="number">4096</span>);</span><br><span class="line">  FLASH_RD_Block(buf, blk_len * <span class="number">4096</span>);</span><br><span class="line">  FLASH_RD_Block_End();</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (USBD_OK);</span><br><span class="line">  <span class="comment">/* USER CODE END 6 */</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">int8_t</span> <span class="title function_">STORAGE_Write</span><span class="params">(<span class="type">uint8_t</span> lun, <span class="type">uint8_t</span> *buf, <span class="type">uint32_t</span> blk_addr, <span class="type">uint16_t</span> blk_len)</span></span><br><span class="line">&#123;</span><br><span class="line">  <span class="comment">/* USER CODE BEGIN 7 */</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">for</span> (<span class="type">uint16_t</span> i = <span class="number">0</span>; i &lt; blk_len; i++)</span><br><span class="line">  &#123;</span><br><span class="line">    FLASH_Erase_Sector((blk_addr + i) * <span class="number">4096</span>);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  W25XXX_WR_Block((<span class="type">uint8_t</span>*)buf, blk_addr * <span class="number">4096</span>, blk_len * <span class="number">4096</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (USBD_OK);</span><br><span class="line">  <span class="comment">/* USER CODE END 7 */</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p><strong>注意</strong>：除了和前面一样的，需要自己手动去修改库文件之外，最好把 uint8_t MSC_Storage[32<em>1024]; 这个变量也删除掉，因为默认库中分配了 uint8_t MSC_Storage[32</em>1024]; 作为虚拟存储使用，但我们使用了外部存储器（如 SPI Flash），该数组将不再使用，建议释放该部分内存资源；</p></blockquote><p>至此，我们已经成功实现了一个包含 CDC 和 USB 存储的复合设备功能；如果你掌握了这部分内容，就可以自由组合 HID、Audio、WebUSB 等模块，为你的嵌入式项目打造更加丰富的功能形态；</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>这篇文章带你一步步实现了一个“多合一”的 USB 设备，让一个 USB 接口同时具备虚拟串口（CDC）和 U 盘（MSC）的功能；我们从组件库的导入、CubeMX 的配置，到代码逻辑的修改与实现，完整搭建了一个 USB Composite 设备的基础框架；</p><p>在过程中，你了解了 USB 是如何以数据包方式传输信息的，也学到了接收多包数据时如何“拼接”数据，以及如何判断一帧数据是否接收完成；通过简单的“超时判断”方法，我们就能实现比较可靠的数据处理逻辑；</p><p>另外，我们还结合了之前写好的 FatFS 文件系统，让 MCU 成功模拟出一个能读写的 U 盘，这对实际项目开发很有帮助；</p><p>掌握了这些内容之后，你就可以继续添加更多 USB 功能，比如 HID 键盘、音频设备，甚至是 WebUSB，让你的设备功能更加丰富；</p><p>如果你是第一次接触 USB Composite，这篇文章可以作为一个很好的起点；希望这次的实践能帮你打下基础，也欢迎你把这些经验应用到自己的项目中，做出更多好玩的嵌入式设备！</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;仓库：&lt;a class=&quot;link&quot;   href=&quot;https://github.com/alambe94/I-CUBE-USBD-Composite&quot; &gt;https://github.com/alambe94/I-CUBE-USBD-Composite&lt;i class=</summary>
      
    
    
    
    <category term="mcu" scheme="http://example.com/categories/mcu/"/>
    
    
  </entry>
  
  <entry>
    <title>从 0 搭建 SPI Flash 文件系统：驱动、FatFS、读写与坑点</title>
    <link href="http://example.com/2024/10/26/mcu/FatFS/"/>
    <id>http://example.com/2024/10/26/mcu/FatFS/</id>
    <published>2024-10-26T15:15:07.000Z</published>
    <updated>2024-10-26T15:15:07.000Z</updated>
    
    <content type="html"><![CDATA[<p>FatFS官网：<a class="link"   href="https://elm-chan.org/fsw/ff/00index_e.html" >https://elm-chan.org/fsw/ff/00index_e.html<i class="fas fa-external-link-alt"></i></a></p><p>当你用单片机做项目，代码调试靠串口、数据记录靠看屏幕、文件读写靠想象，久而久之，你会发现：<strong>没有文件系统，生活就像裸奔，哪都能跑，就是不太方便；</strong></p><p>尤其是在一些<strong>需要长时间运行、持续采集数据</strong>的应用场景中，比如环境监测、设备日志记录、传感器数据采集等，如果没有一个可靠的文件系统来进行<strong>数据持久化存储</strong>，不仅开发调试麻烦，维护和升级也会变得困难重重；你总不能每次都靠串口打印几十KB甚至几MB的数据吧？</p><p>这时候你可能听说了一个神器：<strong>FatFS</strong>，一个轻量级的 FAT 文件系统，专为嵌入式系统设计，小巧灵活，支持 SD 卡、SPI Flash，甚至 RAMDisk；不论你用的是 STM32、GD32，还是别的 MCU 平台，都能把它“嫁接”过去；</p><p>有了文件系统，不仅可以更方便地<strong>与电脑共享数据</strong>（比如通过 U 盘或 SD 卡读取设备日志），还能按时间归档、分类管理信息，甚至在设备<strong>意外断电或异常重启时保留关键数据</strong>，提升项目的健壮性和专业程度；</p><p>那么，这篇文章就是来讲一讲：<strong>如何在你的单片机上，成功移植 FatFS，让你的 MCU 拥有读写文件的能力；</strong></p><h2 id="FatFS-移植流程概览"><a href="#FatFS-移植流程概览" class="headerlink" title="FatFS 移植流程概览"></a>FatFS 移植流程概览</h2><p>FatFS 的移植主要包括以下几个步骤：</p><ol><li>准备底层存储驱动（如 SPI Flash 驱动）</li><li>实现 FatFS 所需的 diskio.c 接口函数</li><li>配置 ffconf.h 以满足你的文件系统需求</li><li>在主函数中初始化 FatFS，挂载文件系统</li><li>实现文件的读写操作测试</li></ol><p>接下来，我们从第一步开始，移植 SPI Flash 驱动；</p><h2 id="FatFS-文件系统移植到-SPI-Flash"><a href="#FatFS-文件系统移植到-SPI-Flash" class="headerlink" title="FatFS 文件系统移植到 SPI Flash"></a>FatFS 文件系统移植到 SPI Flash</h2><p>要让 FatFS 在单片机上正常工作，首先你得有一个“存储设备”能读能写；虽然 SD 卡是最常见的选择，但很多时候，SPI Flash 是更方便的一种方式：不需要外接卡座、不怕接触不良，容量也够用；</p><p>对应的驱动文件如下，将文件添加到你的工程中，驱动文件来自沁恒例程，做了一点点的补充与修改：</p><p>spi_flash.c</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><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><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br><span class="line">171</span><br><span class="line">172</span><br><span class="line">173</span><br><span class="line">174</span><br><span class="line">175</span><br><span class="line">176</span><br><span class="line">177</span><br><span class="line">178</span><br><span class="line">179</span><br><span class="line">180</span><br><span class="line">181</span><br><span class="line">182</span><br><span class="line">183</span><br><span class="line">184</span><br><span class="line">185</span><br><span class="line">186</span><br><span class="line">187</span><br><span class="line">188</span><br><span class="line">189</span><br><span class="line">190</span><br><span class="line">191</span><br><span class="line">192</span><br><span class="line">193</span><br><span class="line">194</span><br><span class="line">195</span><br><span class="line">196</span><br><span class="line">197</span><br><span class="line">198</span><br><span class="line">199</span><br><span class="line">200</span><br><span class="line">201</span><br><span class="line">202</span><br><span class="line">203</span><br><span class="line">204</span><br><span class="line">205</span><br><span class="line">206</span><br><span class="line">207</span><br><span class="line">208</span><br><span class="line">209</span><br><span class="line">210</span><br><span class="line">211</span><br><span class="line">212</span><br><span class="line">213</span><br><span class="line">214</span><br><span class="line">215</span><br><span class="line">216</span><br><span class="line">217</span><br><span class="line">218</span><br><span class="line">219</span><br><span class="line">220</span><br><span class="line">221</span><br><span class="line">222</span><br><span class="line">223</span><br><span class="line">224</span><br><span class="line">225</span><br><span class="line">226</span><br><span class="line">227</span><br><span class="line">228</span><br><span class="line">229</span><br><span class="line">230</span><br><span class="line">231</span><br><span class="line">232</span><br><span class="line">233</span><br><span class="line">234</span><br><span class="line">235</span><br><span class="line">236</span><br><span class="line">237</span><br><span class="line">238</span><br><span class="line">239</span><br><span class="line">240</span><br><span class="line">241</span><br><span class="line">242</span><br><span class="line">243</span><br><span class="line">244</span><br><span class="line">245</span><br><span class="line">246</span><br><span class="line">247</span><br><span class="line">248</span><br><span class="line">249</span><br><span class="line">250</span><br><span class="line">251</span><br><span class="line">252</span><br><span class="line">253</span><br><span class="line">254</span><br><span class="line">255</span><br><span class="line">256</span><br><span class="line">257</span><br><span class="line">258</span><br><span class="line">259</span><br><span class="line">260</span><br><span class="line">261</span><br><span class="line">262</span><br><span class="line">263</span><br><span class="line">264</span><br><span class="line">265</span><br><span class="line">266</span><br><span class="line">267</span><br><span class="line">268</span><br><span class="line">269</span><br><span class="line">270</span><br><span class="line">271</span><br><span class="line">272</span><br><span class="line">273</span><br><span class="line">274</span><br><span class="line">275</span><br><span class="line">276</span><br><span class="line">277</span><br><span class="line">278</span><br><span class="line">279</span><br><span class="line">280</span><br><span class="line">281</span><br><span class="line">282</span><br><span class="line">283</span><br><span class="line">284</span><br><span class="line">285</span><br><span class="line">286</span><br><span class="line">287</span><br><span class="line">288</span><br><span class="line">289</span><br><span class="line">290</span><br><span class="line">291</span><br><span class="line">292</span><br><span class="line">293</span><br><span class="line">294</span><br><span class="line">295</span><br><span class="line">296</span><br><span class="line">297</span><br><span class="line">298</span><br><span class="line">299</span><br><span class="line">300</span><br><span class="line">301</span><br><span class="line">302</span><br><span class="line">303</span><br><span class="line">304</span><br><span class="line">305</span><br><span class="line">306</span><br><span class="line">307</span><br><span class="line">308</span><br><span class="line">309</span><br><span class="line">310</span><br><span class="line">311</span><br><span class="line">312</span><br><span class="line">313</span><br><span class="line">314</span><br><span class="line">315</span><br><span class="line">316</span><br><span class="line">317</span><br><span class="line">318</span><br><span class="line">319</span><br><span class="line">320</span><br><span class="line">321</span><br><span class="line">322</span><br><span class="line">323</span><br><span class="line">324</span><br><span class="line">325</span><br><span class="line">326</span><br><span class="line">327</span><br><span class="line">328</span><br><span class="line">329</span><br><span class="line">330</span><br><span class="line">331</span><br><span class="line">332</span><br><span class="line">333</span><br><span class="line">334</span><br><span class="line">335</span><br><span class="line">336</span><br><span class="line">337</span><br><span class="line">338</span><br><span class="line">339</span><br><span class="line">340</span><br><span class="line">341</span><br><span class="line">342</span><br><span class="line">343</span><br><span class="line">344</span><br><span class="line">345</span><br><span class="line">346</span><br><span class="line">347</span><br><span class="line">348</span><br><span class="line">349</span><br><span class="line">350</span><br><span class="line">351</span><br><span class="line">352</span><br><span class="line">353</span><br><span class="line">354</span><br><span class="line">355</span><br><span class="line">356</span><br><span class="line">357</span><br><span class="line">358</span><br><span class="line">359</span><br><span class="line">360</span><br><span class="line">361</span><br><span class="line">362</span><br><span class="line">363</span><br><span class="line">364</span><br><span class="line">365</span><br><span class="line">366</span><br><span class="line">367</span><br><span class="line">368</span><br><span class="line">369</span><br><span class="line">370</span><br><span class="line">371</span><br><span class="line">372</span><br><span class="line">373</span><br><span class="line">374</span><br><span class="line">375</span><br><span class="line">376</span><br><span class="line">377</span><br><span class="line">378</span><br><span class="line">379</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;spi_flash.h&quot;</span></span></span><br><span class="line"></span><br><span class="line"><span class="keyword">volatile</span> <span class="type">uint8_t</span>  Flash_Type         = <span class="number">0x00</span>; <span class="comment">/* FLASH chip: 0: W25XXXseries */</span></span><br><span class="line"><span class="keyword">volatile</span> <span class="type">uint32_t</span> Flash_ID           = <span class="number">0x00</span>; <span class="comment">/* FLASH ID */</span></span><br><span class="line"><span class="keyword">volatile</span> <span class="type">uint32_t</span> Flash_Sector_Count = <span class="number">0x00</span>; <span class="comment">/* FLASH sector number */</span></span><br><span class="line"><span class="keyword">volatile</span> <span class="type">uint16_t</span> Flash_Sector_Size  = <span class="number">0x00</span>; <span class="comment">/* FLASH sector size */</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/*******************************************************************************</span></span><br><span class="line"><span class="comment"> * Function Name  : SPI_FLASH_SendByte</span></span><br><span class="line"><span class="comment"> * Description    : SPI send a byte</span></span><br><span class="line"><span class="comment"> * Input          : byte: byte to send</span></span><br><span class="line"><span class="comment"> * Output         : None</span></span><br><span class="line"><span class="comment"> * Return         : None</span></span><br><span class="line"><span class="comment"> *******************************************************************************/</span></span><br><span class="line"><span class="type">uint8_t</span> <span class="title function_">SPI_FLASH_SendByte</span> <span class="params">(<span class="type">uint8_t</span> byte)</span></span><br><span class="line">&#123;</span><br><span class="line">  <span class="keyword">return</span> User_SPI1_ReadWriteByte (byte);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/*******************************************************************************</span></span><br><span class="line"><span class="comment"> * Function Name  : SPI_FLASH_ReadByte</span></span><br><span class="line"><span class="comment"> * Description    : SPI receive a byte</span></span><br><span class="line"><span class="comment"> * Input          : None</span></span><br><span class="line"><span class="comment"> * Output         : None</span></span><br><span class="line"><span class="comment"> * Return         : byte received</span></span><br><span class="line"><span class="comment"> *******************************************************************************/</span></span><br><span class="line"><span class="type">uint8_t</span> <span class="title function_">SPI_FLASH_ReadByte</span> <span class="params">(<span class="type">void</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">  <span class="keyword">return</span> User_SPI1_ReadWriteByte (<span class="number">0xFF</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/*******************************************************************************</span></span><br><span class="line"><span class="comment"> * Function Name  : FLASH_ReadID</span></span><br><span class="line"><span class="comment"> * Description    : Read FLASH ID</span></span><br><span class="line"><span class="comment"> * Input          : None</span></span><br><span class="line"><span class="comment"> * Output         : None</span></span><br><span class="line"><span class="comment"> * Return         : chip id</span></span><br><span class="line"><span class="comment"> *******************************************************************************/</span></span><br><span class="line"><span class="type">uint32_t</span> <span class="title function_">FLASH_ReadID</span> <span class="params">(<span class="type">void</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">  <span class="type">uint32_t</span> dat;</span><br><span class="line">  PIN_FLASH_CS_LOW();</span><br><span class="line">  SPI_FLASH_SendByte (CMD_FLASH_JEDEC_ID);</span><br><span class="line">  dat = (<span class="type">uint32_t</span>)SPI_FLASH_SendByte (DEF_DUMMY_BYTE) &lt;&lt; <span class="number">16</span>;</span><br><span class="line">  dat |= (<span class="type">uint32_t</span>)SPI_FLASH_SendByte (DEF_DUMMY_BYTE) &lt;&lt; <span class="number">8</span>;</span><br><span class="line">  dat |= SPI_FLASH_SendByte (DEF_DUMMY_BYTE);</span><br><span class="line">  PIN_FLASH_CS_HIGH();</span><br><span class="line">  <span class="keyword">return</span> (dat);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/*******************************************************************************</span></span><br><span class="line"><span class="comment"> * Function Name  : FLASH_WriteEnable</span></span><br><span class="line"><span class="comment"> * Description    : FLASH Write Enable</span></span><br><span class="line"><span class="comment"> * Input          : None</span></span><br><span class="line"><span class="comment"> * Output         : None</span></span><br><span class="line"><span class="comment"> * Return         : None</span></span><br><span class="line"><span class="comment"> *******************************************************************************/</span></span><br><span class="line"><span class="type">void</span> <span class="title function_">FLASH_WriteEnable</span> <span class="params">(<span class="type">void</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">  PIN_FLASH_CS_LOW();</span><br><span class="line">  SPI_FLASH_SendByte (CMD_FLASH_WREN);</span><br><span class="line">  PIN_FLASH_CS_HIGH();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/*******************************************************************************</span></span><br><span class="line"><span class="comment"> * Function Name  : FLASH_WriteDisable</span></span><br><span class="line"><span class="comment"> * Description    : FLASH Write Disable</span></span><br><span class="line"><span class="comment"> * Input          : None</span></span><br><span class="line"><span class="comment"> * Output         : None</span></span><br><span class="line"><span class="comment"> * Return         : None</span></span><br><span class="line"><span class="comment"> *******************************************************************************/</span></span><br><span class="line"><span class="type">void</span> <span class="title function_">FLASH_WriteDisable</span> <span class="params">(<span class="type">void</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">  PIN_FLASH_CS_LOW();</span><br><span class="line">  SPI_FLASH_SendByte (CMD_FLASH_WRDI);</span><br><span class="line">  PIN_FLASH_CS_HIGH();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/*******************************************************************************</span></span><br><span class="line"><span class="comment"> * Function Name  : FLASH_ReadStatusReg</span></span><br><span class="line"><span class="comment"> * Description    : FLASH Read Status</span></span><br><span class="line"><span class="comment"> * Input          : None</span></span><br><span class="line"><span class="comment"> * Output         : None</span></span><br><span class="line"><span class="comment"> * Return         : status</span></span><br><span class="line"><span class="comment"> *******************************************************************************/</span></span><br><span class="line"><span class="type">uint8_t</span> <span class="title function_">FLASH_ReadStatusReg</span> <span class="params">(<span class="type">void</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">  <span class="type">uint8_t</span> status;</span><br><span class="line"></span><br><span class="line">  PIN_FLASH_CS_LOW();</span><br><span class="line">  SPI_FLASH_SendByte (CMD_FLASH_RDSR);</span><br><span class="line">  status = SPI_FLASH_ReadByte();</span><br><span class="line">  PIN_FLASH_CS_HIGH();</span><br><span class="line">  <span class="keyword">return</span> (status);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/*******************************************************************************</span></span><br><span class="line"><span class="comment"> * Function Name  : FLASH_Erase_Sector</span></span><br><span class="line"><span class="comment"> * Description    : FLASH Erase Sector</span></span><br><span class="line"><span class="comment"> * Input          : None</span></span><br><span class="line"><span class="comment"> * Output         : None</span></span><br><span class="line"><span class="comment"> * Return         : None</span></span><br><span class="line"><span class="comment"> *******************************************************************************/</span></span><br><span class="line"><span class="type">void</span> <span class="title function_">FLASH_Erase_Sector</span> <span class="params">(<span class="type">uint32_t</span> address)</span></span><br><span class="line">&#123;</span><br><span class="line">  <span class="type">uint8_t</span> temp;</span><br><span class="line">  FLASH_WriteEnable();</span><br><span class="line">  PIN_FLASH_CS_LOW();</span><br><span class="line">  SPI_FLASH_SendByte (CMD_FLASH_SECTOR_ERASE);</span><br><span class="line">  SPI_FLASH_SendByte ((<span class="type">uint8_t</span>)(address &gt;&gt; <span class="number">16</span>));</span><br><span class="line">  SPI_FLASH_SendByte ((<span class="type">uint8_t</span>)(address &gt;&gt; <span class="number">8</span>));</span><br><span class="line">  SPI_FLASH_SendByte ((<span class="type">uint8_t</span>)address);</span><br><span class="line">  PIN_FLASH_CS_HIGH();</span><br><span class="line">  <span class="keyword">do</span></span><br><span class="line">  &#123;</span><br><span class="line">    temp = FLASH_ReadStatusReg();</span><br><span class="line">  &#125; <span class="keyword">while</span> (temp &amp; <span class="number">0x01</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/*******************************************************************************</span></span><br><span class="line"><span class="comment"> * Function Name  : FLASH_RD_Block_Start</span></span><br><span class="line"><span class="comment"> * Description    : FLASH start block read</span></span><br><span class="line"><span class="comment"> * Input          : None</span></span><br><span class="line"><span class="comment"> * Output         : None</span></span><br><span class="line"><span class="comment"> * Return         : None</span></span><br><span class="line"><span class="comment"> *******************************************************************************/</span></span><br><span class="line"><span class="type">void</span> <span class="title function_">FLASH_RD_Block_Start</span> <span class="params">(<span class="type">uint32_t</span> address)</span></span><br><span class="line">&#123;</span><br><span class="line">  PIN_FLASH_CS_LOW();</span><br><span class="line">  SPI_FLASH_SendByte (CMD_FLASH_READ);</span><br><span class="line">  SPI_FLASH_SendByte ((<span class="type">uint8_t</span>)(address &gt;&gt; <span class="number">16</span>));</span><br><span class="line">  SPI_FLASH_SendByte ((<span class="type">uint8_t</span>)(address &gt;&gt; <span class="number">8</span>));</span><br><span class="line">  SPI_FLASH_SendByte ((<span class="type">uint8_t</span>)address);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/*******************************************************************************</span></span><br><span class="line"><span class="comment"> * Function Name  : FLASH_RD_Block</span></span><br><span class="line"><span class="comment"> * Description    : FLASH read block</span></span><br><span class="line"><span class="comment"> * Input          : None</span></span><br><span class="line"><span class="comment"> * Output         : None</span></span><br><span class="line"><span class="comment"> * Return         : None</span></span><br><span class="line"><span class="comment"> *******************************************************************************/</span></span><br><span class="line"><span class="type">void</span> <span class="title function_">FLASH_RD_Block</span> <span class="params">(<span class="type">uint8_t</span> *pbuf, <span class="type">uint32_t</span> len)</span></span><br><span class="line">&#123;</span><br><span class="line">  <span class="keyword">while</span> (len--)</span><br><span class="line">  &#123;</span><br><span class="line">    *pbuf++ = SPI_FLASH_ReadByte();</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/*******************************************************************************</span></span><br><span class="line"><span class="comment"> * Function Name  : FLASH_RD_Block_End</span></span><br><span class="line"><span class="comment"> * Description    : FLASH end block read</span></span><br><span class="line"><span class="comment"> * Input          : None</span></span><br><span class="line"><span class="comment"> * Output         : None</span></span><br><span class="line"><span class="comment"> * Return         : None</span></span><br><span class="line"><span class="comment"> *******************************************************************************/</span></span><br><span class="line"><span class="type">void</span> <span class="title function_">FLASH_RD_Block_End</span> <span class="params">(<span class="type">void</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">  PIN_FLASH_CS_HIGH();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/*******************************************************************************</span></span><br><span class="line"><span class="comment"> * Function Name  : W25XXX_WR_Page</span></span><br><span class="line"><span class="comment"> * Description    : Flash page program</span></span><br><span class="line"><span class="comment"> * Input          : address</span></span><br><span class="line"><span class="comment"> *                  len</span></span><br><span class="line"><span class="comment"> *                  *pbuf</span></span><br><span class="line"><span class="comment"> * Output         : None</span></span><br><span class="line"><span class="comment"> * Return         : None</span></span><br><span class="line"><span class="comment"> *******************************************************************************/</span></span><br><span class="line"><span class="type">void</span> <span class="title function_">W25XXX_WR_Page</span> <span class="params">(<span class="type">uint8_t</span> *pbuf, <span class="type">uint32_t</span> address, <span class="type">uint32_t</span> len)</span></span><br><span class="line">&#123;</span><br><span class="line">  <span class="type">uint8_t</span> temp;</span><br><span class="line">  FLASH_WriteEnable();</span><br><span class="line">  PIN_FLASH_CS_LOW();</span><br><span class="line">  SPI_FLASH_SendByte (CMD_FLASH_BYTE_PROG);</span><br><span class="line">  SPI_FLASH_SendByte ((<span class="type">uint8_t</span>)(address &gt;&gt; <span class="number">16</span>));</span><br><span class="line">  SPI_FLASH_SendByte ((<span class="type">uint8_t</span>)(address &gt;&gt; <span class="number">8</span>));</span><br><span class="line">  SPI_FLASH_SendByte ((<span class="type">uint8_t</span>)address);</span><br><span class="line">  <span class="keyword">if</span> (len &gt; SPI_FLASH_PerWritePageSize)</span><br><span class="line">  &#123;</span><br><span class="line">    len = SPI_FLASH_PerWritePageSize;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">while</span> (len--)</span><br><span class="line">  &#123;</span><br><span class="line">    SPI_FLASH_SendByte (*pbuf++);</span><br><span class="line">  &#125;</span><br><span class="line">  PIN_FLASH_CS_HIGH();</span><br><span class="line">  <span class="keyword">do</span></span><br><span class="line">  &#123;</span><br><span class="line">    temp = FLASH_ReadStatusReg();</span><br><span class="line">  &#125; <span class="keyword">while</span> (temp &amp; <span class="number">0x01</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/*******************************************************************************</span></span><br><span class="line"><span class="comment"> * Function Name  : W25XXX_WR_Block</span></span><br><span class="line"><span class="comment"> * Description    : W25XXX block write</span></span><br><span class="line"><span class="comment"> * Input          : address</span></span><br><span class="line"><span class="comment"> *                  len</span></span><br><span class="line"><span class="comment"> *                  *pbuf</span></span><br><span class="line"><span class="comment"> * Output         : None</span></span><br><span class="line"><span class="comment"> * Return         : None</span></span><br><span class="line"><span class="comment"> *******************************************************************************/</span></span><br><span class="line"><span class="type">void</span> <span class="title function_">W25XXX_WR_Block</span> <span class="params">(<span class="type">uint8_t</span> *pbuf, <span class="type">uint32_t</span> address, <span class="type">uint32_t</span> len)</span></span><br><span class="line">&#123;</span><br><span class="line">  <span class="type">uint8_t</span> NumOfPage = <span class="number">0</span>, NumOfSingle = <span class="number">0</span>, Addr = <span class="number">0</span>, count = <span class="number">0</span>, temp = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">  Addr        = address % SPI_FLASH_PageSize;</span><br><span class="line">  count       = SPI_FLASH_PageSize - Addr;</span><br><span class="line">  NumOfPage   = len / SPI_FLASH_PageSize;</span><br><span class="line">  NumOfSingle = len % SPI_FLASH_PageSize;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (Addr == <span class="number">0</span>)</span><br><span class="line">  &#123;</span><br><span class="line">    <span class="keyword">if</span> (NumOfPage == <span class="number">0</span>)</span><br><span class="line">    &#123;</span><br><span class="line">      W25XXX_WR_Page (pbuf, address, len);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">else</span></span><br><span class="line">    &#123;</span><br><span class="line">      <span class="keyword">while</span> (NumOfPage--)</span><br><span class="line">      &#123;</span><br><span class="line">        W25XXX_WR_Page (pbuf, address, SPI_FLASH_PageSize);</span><br><span class="line">        address += SPI_FLASH_PageSize;</span><br><span class="line">        pbuf += SPI_FLASH_PageSize;</span><br><span class="line">      &#125;</span><br><span class="line">      W25XXX_WR_Page (pbuf, address, NumOfSingle);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">else</span></span><br><span class="line">  &#123;</span><br><span class="line">    <span class="keyword">if</span> (NumOfPage == <span class="number">0</span>)</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="keyword">if</span> (NumOfSingle &gt; count)</span><br><span class="line">      &#123;</span><br><span class="line">        temp = NumOfSingle - count;</span><br><span class="line">        W25XXX_WR_Page (pbuf, address, count);</span><br><span class="line">        address += count;</span><br><span class="line">        pbuf += count;</span><br><span class="line">        W25XXX_WR_Page (pbuf, address, temp);</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">else</span></span><br><span class="line">      &#123;</span><br><span class="line">        W25XXX_WR_Page (pbuf, address, len);</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">else</span></span><br><span class="line">    &#123;</span><br><span class="line">      len -= count;</span><br><span class="line">      NumOfPage   = len / SPI_FLASH_PageSize;</span><br><span class="line">      NumOfSingle = len % SPI_FLASH_PageSize;</span><br><span class="line">      W25XXX_WR_Page (pbuf, address, count);</span><br><span class="line">      address += count;</span><br><span class="line">      pbuf += count;</span><br><span class="line">      <span class="keyword">while</span> (NumOfPage--)</span><br><span class="line">      &#123;</span><br><span class="line">        W25XXX_WR_Page (pbuf, address, SPI_FLASH_PageSize);</span><br><span class="line">        address += SPI_FLASH_PageSize;</span><br><span class="line">        pbuf += SPI_FLASH_PageSize;</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">if</span> (NumOfSingle != <span class="number">0</span>)</span><br><span class="line">      &#123;</span><br><span class="line">        W25XXX_WR_Page (pbuf, address, NumOfSingle);</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/*******************************************************************************</span></span><br><span class="line"><span class="comment"> * Function Name  : FLASH_IC_Check</span></span><br><span class="line"><span class="comment"> * Description    : check flash type</span></span><br><span class="line"><span class="comment"> * Input          : None</span></span><br><span class="line"><span class="comment"> * Output         : None</span></span><br><span class="line"><span class="comment"> * Return         : None</span></span><br><span class="line"><span class="comment"> *******************************************************************************/</span></span><br><span class="line"><span class="type">void</span> <span class="title function_">FLASH_IC_Check</span> <span class="params">(<span class="type">void</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">  <span class="type">uint32_t</span> count;</span><br><span class="line"></span><br><span class="line">  <span class="comment">/* Read FLASH ID */</span></span><br><span class="line">  Flash_ID = FLASH_ReadID();</span><br><span class="line"><span class="comment">//  printf (&quot;Flash_ID: %08x\n&quot;, (uint32_t)Flash_ID);</span></span><br><span class="line"></span><br><span class="line">  Flash_Type         = <span class="number">0x00</span>;</span><br><span class="line">  Flash_Sector_Count = <span class="number">0x00</span>;</span><br><span class="line">  Flash_Sector_Size  = <span class="number">0x00</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">switch</span> (Flash_ID)</span><br><span class="line">  &#123;</span><br><span class="line">    <span class="comment">/* W25XXX */</span></span><br><span class="line">    <span class="keyword">case</span> W25X10_FLASH_ID: <span class="comment">/* 0xEF3011-----1M bit */</span></span><br><span class="line">      count = <span class="number">1</span>;</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">case</span> W25X20_FLASH_ID: <span class="comment">/* 0xEF3012-----2M bit */</span></span><br><span class="line">      count = <span class="number">2</span>;</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">case</span> W25X40_FLASH_ID: <span class="comment">/* 0xEF3013-----4M bit */</span></span><br><span class="line">      count = <span class="number">4</span>;</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">case</span> W25X80_FLASH_ID: <span class="comment">/* 0xEF4014-----8M bit */</span></span><br><span class="line">      count = <span class="number">8</span>;</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">case</span> W25Q16_FLASH_ID1: <span class="comment">/* 0xEF3015-----16M bit */</span></span><br><span class="line">    <span class="keyword">case</span> W25Q16_FLASH_ID2: <span class="comment">/* 0xEF4015-----16M bit */</span></span><br><span class="line">      count = <span class="number">16</span>;</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">case</span> W25Q32_FLASH_ID1: <span class="comment">/* 0xEF4016-----32M bit */</span></span><br><span class="line">    <span class="keyword">case</span> W25Q32_FLASH_ID2: <span class="comment">/* 0xEF6016-----32M bit */</span></span><br><span class="line">      count = <span class="number">32</span>;</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">case</span> W25Q64_FLASH_ID1: <span class="comment">/* 0xEF4017-----64M bit */</span></span><br><span class="line">    <span class="keyword">case</span> W25Q64_FLASH_ID2: <span class="comment">/* 0xEF6017-----64M bit */</span></span><br><span class="line">      count = <span class="number">64</span>;</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">case</span> W25Q128_FLASH_ID1: <span class="comment">/* 0xEF4018-----128M bit */</span></span><br><span class="line">    <span class="keyword">case</span> W25Q128_FLASH_ID2: <span class="comment">/* 0xEF6018-----128M bit */</span></span><br><span class="line">      count = <span class="number">128</span>;</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">case</span> W25Q256_FLASH_ID1: <span class="comment">/* 0xEF4019-----256M bit */</span></span><br><span class="line">    <span class="keyword">case</span> W25Q256_FLASH_ID2: <span class="comment">/* 0xEF6019-----256M bit */</span></span><br><span class="line">      count = <span class="number">256</span>;</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line">    <span class="keyword">default</span>:</span><br><span class="line">      <span class="keyword">if</span> ((Flash_ID != <span class="number">0xFFFFFFFF</span>) &amp;&amp; (Flash_ID != <span class="number">0x00000000</span>))</span><br><span class="line">      &#123;</span><br><span class="line">        count = <span class="number">16</span>;</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">else</span></span><br><span class="line">      &#123;</span><br><span class="line">        count = <span class="number">0x00</span>;</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line">  &#125;</span><br><span class="line">  count = ((<span class="type">uint32_t</span>)count * <span class="number">1024</span>) * ((<span class="type">uint32_t</span>)<span class="number">1024</span> / <span class="number">8</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (count)</span><br><span class="line">  &#123;</span><br><span class="line">    <span class="comment">// 如果是内部,那么DEF_UDISK_SECTOR_SIZE是512,如果是外部,则DEF_SECTOR_SIZE是4096</span></span><br><span class="line">    Flash_Sector_Count = count / DEF_SECTOR_SIZE; <span class="comment">// DEF_SECTOR_SIZE;</span></span><br><span class="line">    Flash_Sector_Size  = DEF_SECTOR_SIZE;         <span class="comment">// DEF_SECTOR_SIZE;</span></span><br><span class="line"><span class="comment">//    printf (&quot;\r\nFlash_Sector_Count:%d\r\n&quot;, Flash_Sector_Count);</span></span><br><span class="line"><span class="comment">//    printf (&quot;Flash_Sector_Size:%d\r\n&quot;, Flash_Sector_Size);</span></span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">else</span></span><br><span class="line">  &#123;</span><br><span class="line"><span class="comment">//    printf (&quot;External Flash not connected\r\n&quot;);</span></span><br><span class="line">    <span class="comment">//  while(1);</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/*******************************************************************************</span></span><br><span class="line"><span class="comment"> * Function Name  : FLASH_Init</span></span><br><span class="line"><span class="comment"> * Description    : init spi flash</span></span><br><span class="line"><span class="comment"> * Input          : None</span></span><br><span class="line"><span class="comment"> * Output         : None</span></span><br><span class="line"><span class="comment"> * Return         : status</span></span><br><span class="line"><span class="comment"> *******************************************************************************/</span></span><br><span class="line"><span class="type">uint8_t</span> FLASH_Init(<span class="type">void</span>)</span><br><span class="line">&#123;</span><br><span class="line">  PIN_FLASH_CS_LOW();</span><br><span class="line"></span><br><span class="line">  SPI_FLASH_SendByte(CMD_FLASH_RESET_ENABLE);</span><br><span class="line">  SPI_FLASH_SendByte(CMD_FLASH_RESET_MEMORY);</span><br><span class="line"></span><br><span class="line">  PIN_FLASH_CS_HIGH();</span><br><span class="line"></span><br><span class="line">  FLASH_IC_Check();</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> FLASH_ReadStatusReg();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>spi_flash.h</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><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><span class="line">87</span><br><span class="line">88</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">ifndef</span> _SPI_FLASH_H_</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> _SPI_FLASH_H_</span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;spi.h&quot;</span></span></span><br><span class="line"></span><br><span class="line"><span class="comment">/* SPI Pins */</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> PIN_FLASH_CS_LOW()  SPI1_CS_LOW()</span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> PIN_FLASH_CS_HIGH() SPI1_CS_HIGH()</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/******************************************************************************/</span></span><br><span class="line"><span class="comment">/* SPI Serial Flash OPERATION INSTRUCTIONS */</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> CMD_FLASH_READ 0x03         <span class="comment">/* Read Memory at 25 MHz */</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> CMD_FLASH_SECTOR_ERASE 0x20 <span class="comment">/* Erase 4 KByte of memory array */</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> CMD_FLASH_BYTE_PROG 0x02    <span class="comment">/* To Program One Data Byte */</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> CMD_FLASH_RDSR 0x05         <span class="comment">/* Read-Status-Register */</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> CMD_FLASH_EWSR 0x50         <span class="comment">/* Enable-Write-Status-Register */</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> CMD_FLASH_WREN 0x06         <span class="comment">/* Write-Enable */</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> CMD_FLASH_WRDI 0x04         <span class="comment">/* Write-Disable */</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> CMD_FLASH_JEDEC_ID 0x9F     <span class="comment">/* JEDEC ID read */</span></span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> CMD_FLASH_RESET_ENABLE  0x66</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> CMD_FLASH_RESET_MEMORY  0x99</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/******************************************************************************/</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> DEF_DUMMY_BYTE 0xFF</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/******************************************************************************/</span></span><br><span class="line"><span class="comment">/* FLASH Parameter Definition */</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> SPI_FLASH_SectorSize 4096</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> SPI_FLASH_PageSize 256</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> SPI_FLASH_PerWritePageSize 256</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/******************************************************************************/</span></span><br><span class="line"><span class="comment">/* SPI FLASH Type */</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> DEF_TYPE_W25XXX 0 <span class="comment">/* W25XXX */</span></span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> DEF_SECTOR_SIZE 4096</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/* SPI FLASH STATUS */</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> SPI_FLASH_OK            ((uint8_t)0x00)</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> SPI_FLASH_ERROR         ((uint8_t)0x01)</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> SPI_FLASH_BUSY          ((uint8_t)0x02)</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> SPI_FLASH_TIMEOUT       ((uint8_t)0x03)</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/******************************************************************************/</span></span><br><span class="line"><span class="comment">/* SPI FLASH Type Define */</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/* W25XXX*/</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> W25X10_FLASH_ID 0xEF3011   <span class="comment">/* 1M bit */</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> W25X20_FLASH_ID 0xEF3012   <span class="comment">/* 2M bit */</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> W25X40_FLASH_ID 0xEF3013   <span class="comment">/* 4M bit */</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> W25X80_FLASH_ID 0xEF4014   <span class="comment">/* 8M bit */</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> W25Q16_FLASH_ID1 0xEF3015  <span class="comment">/* 16M bit */</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> W25Q16_FLASH_ID2 0xEF4015  <span class="comment">/* 16M bit */</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> W25Q32_FLASH_ID1 0xEF4016  <span class="comment">/* 32M bit */</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> W25Q32_FLASH_ID2 0xEF6016  <span class="comment">/* 32M bit */</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> W25Q64_FLASH_ID1 0xEF4017  <span class="comment">/* 64M bit */</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> W25Q64_FLASH_ID2 0xEF6017  <span class="comment">/* 64M bit */</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> W25Q128_FLASH_ID1 0xEF4018 <span class="comment">/* 128M bit */</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> W25Q128_FLASH_ID2 0xEF6018 <span class="comment">/* 128M bit */</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> W25Q256_FLASH_ID1 0xEF4019 <span class="comment">/* 256M bit */</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> W25Q256_FLASH_ID2 0xEF6019 <span class="comment">/* 256M bit */</span></span></span><br><span class="line"></span><br><span class="line"><span class="comment">/******************************************************************************/</span></span><br><span class="line"><span class="comment">/* Variable Definition */</span></span><br><span class="line"><span class="keyword">extern</span> <span class="keyword">volatile</span> <span class="type">uint8_t</span> Flash_Type;          <span class="comment">/* FLASH chip: 0: W25XXXseries   */</span></span><br><span class="line"><span class="keyword">extern</span> <span class="keyword">volatile</span> <span class="type">uint32_t</span> Flash_ID;           <span class="comment">/* FLASH ID */</span></span><br><span class="line"><span class="keyword">extern</span> <span class="keyword">volatile</span> <span class="type">uint32_t</span> Flash_Sector_Count; <span class="comment">/* FLASH sector number */</span></span><br><span class="line"><span class="keyword">extern</span> <span class="keyword">volatile</span> <span class="type">uint16_t</span> Flash_Sector_Size;  <span class="comment">/* FLASH sector size */</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/******************************************************************************/</span></span><br><span class="line"><span class="comment">/* external functions */</span></span><br><span class="line"><span class="keyword">extern</span> <span class="type">uint8_t</span> <span class="title function_">FLASH_Init</span><span class="params">(<span class="type">void</span>)</span>;</span><br><span class="line"><span class="keyword">extern</span> <span class="type">uint8_t</span> <span class="title function_">SPI_FLASH_SendByte</span> <span class="params">(<span class="type">uint8_t</span> byte)</span>;</span><br><span class="line"><span class="keyword">extern</span> <span class="type">uint8_t</span> <span class="title function_">SPI_FLASH_ReadByte</span> <span class="params">(<span class="type">void</span>)</span>;</span><br><span class="line"><span class="keyword">extern</span> <span class="type">uint32_t</span> <span class="title function_">FLASH_ReadID</span> <span class="params">(<span class="type">void</span>)</span>;</span><br><span class="line"><span class="keyword">extern</span> <span class="type">void</span> <span class="title function_">FLASH_WriteEnable</span> <span class="params">(<span class="type">void</span>)</span>;</span><br><span class="line"><span class="keyword">extern</span> <span class="type">void</span> <span class="title function_">FLASH_WriteDisable</span> <span class="params">(<span class="type">void</span>)</span>;</span><br><span class="line"><span class="keyword">extern</span> <span class="type">uint8_t</span> <span class="title function_">FLASH_ReadStatusReg</span> <span class="params">(<span class="type">void</span>)</span>;</span><br><span class="line"><span class="keyword">extern</span> <span class="type">void</span> <span class="title function_">FLASH_IC_Check</span> <span class="params">(<span class="type">void</span>)</span>;</span><br><span class="line"><span class="keyword">extern</span> <span class="type">void</span> <span class="title function_">FLASH_Erase_Sector</span> <span class="params">(<span class="type">uint32_t</span> address)</span>;</span><br><span class="line"><span class="keyword">extern</span> <span class="type">void</span> <span class="title function_">FLASH_RD_Block_Start</span> <span class="params">(<span class="type">uint32_t</span> address)</span>;</span><br><span class="line"><span class="keyword">extern</span> <span class="type">void</span> <span class="title function_">FLASH_RD_Block</span> <span class="params">(<span class="type">uint8_t</span> *pbuf, <span class="type">uint32_t</span> len)</span>;</span><br><span class="line"><span class="keyword">extern</span> <span class="type">void</span> <span class="title function_">FLASH_RD_Block_End</span> <span class="params">(<span class="type">void</span>)</span>;</span><br><span class="line"><span class="keyword">extern</span> <span class="type">void</span> <span class="title function_">W25XXX_WR_Block</span> <span class="params">(<span class="type">uint8_t</span> *pbuf, <span class="type">uint32_t</span> address, <span class="type">uint32_t</span> len)</span>;</span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">endif</span> <span class="comment">/* _SPI_FLASH_H_ */</span></span></span><br></pre></td></tr></table></figure><p>该驱动程序在SPI1上实现了对SPI FLASH的读写操作，包括初始化、读取ID、写入使能、写入禁用、读取状态寄存器、检查IC、擦除扇区、读取块、写入块等操作；</p><p>程序中涉及的 User_SPI1_ReadWriteByte 定义如下：</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><span class="line"><span class="type">uint8_t</span> <span class="title function_">User_SPI1_ReadWriteByte</span><span class="params">(<span class="type">uint8_t</span> TxData)</span></span><br><span class="line">&#123;</span><br><span class="line">  <span class="type">uint8_t</span> RxData = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">  HAL_SPI_TransmitReceive(&amp;hspi1, &amp;TxData, &amp;RxData, <span class="number">1</span>, <span class="number">0xFFFF</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> RxData;</span><br><span class="line">&#125;</span><br></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><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><span class="line"><span class="meta">#<span class="keyword">define</span> SPI1_CS_LOW() do \</span></span><br><span class="line"><span class="meta">&#123; \</span></span><br><span class="line"><span class="meta">  HAL_GPIO_WritePin(SPI1_CS_GPIO_Port, SPI1_CS_Pin, GPIO_PIN_RESET); \</span></span><br><span class="line"><span class="meta">&#125; \</span></span><br><span class="line"><span class="meta">while(0)</span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> SPI1_CS_HIGH() do \</span></span><br><span class="line"><span class="meta">&#123; \</span></span><br><span class="line"><span class="meta">  HAL_GPIO_WritePin(SPI1_CS_GPIO_Port, SPI1_CS_Pin, GPIO_PIN_SET); \</span></span><br><span class="line"><span class="meta">&#125; \</span></span><br><span class="line"><span class="meta">while(0)</span></span><br></pre></td></tr></table></figure><blockquote><p>执行FLASH_IC_Check函数之后，函数会根据返回的芯片 ID，设置Flash_Type、Flash_ID、Flash_Sector_Count、Flash_Sector_Size等变量，以便后续操作使用；</p></blockquote><h2 id="移植-FatFS"><a href="#移植-FatFS" class="headerlink" title="移植 FatFS"></a>移植 FatFS</h2><h3 id="实现-diskio-c-接口"><a href="#实现-diskio-c-接口" class="headerlink" title="实现 diskio.c 接口"></a>实现 diskio.c 接口</h3><p>主要就是需要编写 diskio.c 文件，实现以下函数（可以直接复制）：</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><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><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br><span class="line">171</span><br><span class="line">172</span><br><span class="line">173</span><br><span class="line">174</span><br><span class="line">175</span><br><span class="line">176</span><br><span class="line">177</span><br><span class="line">178</span><br><span class="line">179</span><br><span class="line">180</span><br><span class="line">181</span><br><span class="line">182</span><br><span class="line">183</span><br><span class="line">184</span><br><span class="line">185</span><br><span class="line">186</span><br><span class="line">187</span><br><span class="line">188</span><br><span class="line">189</span><br><span class="line">190</span><br><span class="line">191</span><br><span class="line">192</span><br><span class="line">193</span><br><span class="line">194</span><br><span class="line">195</span><br><span class="line">196</span><br><span class="line">197</span><br><span class="line">198</span><br><span class="line">199</span><br><span class="line">200</span><br><span class="line">201</span><br><span class="line">202</span><br><span class="line">203</span><br><span class="line">204</span><br><span class="line">205</span><br><span class="line">206</span><br><span class="line">207</span><br><span class="line">208</span><br><span class="line">209</span><br><span class="line">210</span><br><span class="line">211</span><br><span class="line">212</span><br><span class="line">213</span><br><span class="line">214</span><br><span class="line">215</span><br><span class="line">216</span><br><span class="line">217</span><br><span class="line">218</span><br><span class="line">219</span><br><span class="line">220</span><br><span class="line">221</span><br><span class="line">222</span><br><span class="line">223</span><br><span class="line">224</span><br><span class="line">225</span><br><span class="line">226</span><br><span class="line">227</span><br><span class="line">228</span><br><span class="line">229</span><br><span class="line">230</span><br><span class="line">231</span><br><span class="line">232</span><br><span class="line">233</span><br><span class="line">234</span><br><span class="line">235</span><br><span class="line">236</span><br><span class="line">237</span><br><span class="line">238</span><br><span class="line">239</span><br><span class="line">240</span><br><span class="line">241</span><br><span class="line">242</span><br><span class="line">243</span><br><span class="line">244</span><br><span class="line">245</span><br><span class="line">246</span><br><span class="line">247</span><br><span class="line">248</span><br><span class="line">249</span><br><span class="line">250</span><br><span class="line">251</span><br><span class="line">252</span><br><span class="line">253</span><br><span class="line">254</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/*-----------------------------------------------------------------------*/</span></span><br><span class="line"><span class="comment">/* Low level disk I/O module SKELETON for FatFs     (C)ChaN, 2019        */</span></span><br><span class="line"><span class="comment">/*-----------------------------------------------------------------------*/</span></span><br><span class="line"><span class="comment">/* If a working storage control module is available, it should be        */</span></span><br><span class="line"><span class="comment">/* attached to the FatFs via a glue function rather than modifying it.   */</span></span><br><span class="line"><span class="comment">/* This is an example of glue functions to attach various exsisting      */</span></span><br><span class="line"><span class="comment">/* storage control modules to the FatFs module with a defined API.       */</span></span><br><span class="line"><span class="comment">/*-----------------------------------------------------------------------*/</span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;ff.h&quot;</span><span class="comment">/* Obtains integer types */</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;diskio.h&quot;</span><span class="comment">/* Declarations of disk functions */</span></span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;spi_flash.h&quot;</span></span></span><br><span class="line"></span><br><span class="line"><span class="comment">/* Definitions of physical drive number for each drive */</span></span><br><span class="line"><span class="comment">//#define DEV_RAM0/* Example: Map Ramdisk to physical drive 0 */</span></span><br><span class="line"><span class="comment">//#define DEV_MMC1/* Example: Map MMC/SD card to physical drive 1 */</span></span><br><span class="line"><span class="comment">//#define DEV_USB2/* Example: Map USB MSD to physical drive 2 */</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> DEV_SPIFLASH 0</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/*-----------------------------------------------------------------------*/</span></span><br><span class="line"><span class="comment">/* Get Drive Status                                                      */</span></span><br><span class="line"><span class="comment">/*-----------------------------------------------------------------------*/</span></span><br><span class="line"></span><br><span class="line">DSTATUS <span class="title function_">disk_status</span><span class="params">(</span></span><br><span class="line"><span class="params">  BYTE pdrv<span class="comment">/* Physical drive nmuber to identify the drive */</span></span></span><br><span class="line"><span class="params">)</span></span><br><span class="line">&#123;</span><br><span class="line">  DSTATUS stat;</span><br><span class="line">  <span class="type">uint8_t</span> result;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">switch</span>(pdrv)</span><br><span class="line">  &#123;</span><br><span class="line">  <span class="keyword">case</span> DEV_SPIFLASH :</span><br><span class="line">    result = FLASH_ReadStatusReg();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// translate the reslut code here</span></span><br><span class="line">    <span class="keyword">switch</span>(result)</span><br><span class="line">    &#123;</span><br><span class="line">    <span class="keyword">case</span> SPI_FLASH_OK:</span><br><span class="line">      stat = STA_NOINIT &amp; (~STA_NOINIT);</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">case</span> SPI_FLASH_ERROR:</span><br><span class="line">      stat = STA_NOINIT;</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">case</span> SPI_FLASH_BUSY:</span><br><span class="line">      stat = STA_NOINIT;</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">case</span> SPI_FLASH_TIMEOUT:</span><br><span class="line">      stat = STA_NOINIT;</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> stat;</span><br><span class="line"></span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> STA_NOINIT;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">/*-----------------------------------------------------------------------*/</span></span><br><span class="line"><span class="comment">/* Inidialize a Drive                                                    */</span></span><br><span class="line"><span class="comment">/*-----------------------------------------------------------------------*/</span></span><br><span class="line"></span><br><span class="line">DSTATUS <span class="title function_">disk_initialize</span><span class="params">(</span></span><br><span class="line"><span class="params">  BYTE pdrv<span class="comment">/* Physical drive nmuber to identify the drive */</span></span></span><br><span class="line"><span class="params">)</span></span><br><span class="line">&#123;</span><br><span class="line">  DSTATUS stat;</span><br><span class="line">  <span class="type">uint8_t</span> result;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">switch</span>(pdrv)</span><br><span class="line">  &#123;</span><br><span class="line">  <span class="keyword">case</span> DEV_SPIFLASH :</span><br><span class="line">    result = FLASH_Init();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// translate the reslut code here</span></span><br><span class="line">    <span class="keyword">switch</span>(result)</span><br><span class="line">    &#123;</span><br><span class="line">    <span class="keyword">case</span> SPI_FLASH_OK:</span><br><span class="line">      stat = STA_NOINIT &amp; (~STA_NOINIT);</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">case</span> SPI_FLASH_ERROR:</span><br><span class="line">      stat = STA_NOINIT;</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">case</span> SPI_FLASH_BUSY:</span><br><span class="line">      stat = STA_NOINIT;</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">case</span> SPI_FLASH_TIMEOUT:</span><br><span class="line">      stat = STA_NOINIT;</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> stat;</span><br><span class="line"></span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> STA_NOINIT;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/*-----------------------------------------------------------------------*/</span></span><br><span class="line"><span class="comment">/* Read Sector(s)                                                        */</span></span><br><span class="line"><span class="comment">/*-----------------------------------------------------------------------*/</span></span><br><span class="line"></span><br><span class="line">DRESULT <span class="title function_">disk_read</span><span class="params">(</span></span><br><span class="line"><span class="params">  BYTE pdrv,<span class="comment">/* Physical drive nmuber to identify the drive */</span></span></span><br><span class="line"><span class="params">  BYTE* buff,<span class="comment">/* Data buffer to store read data */</span></span></span><br><span class="line"><span class="params">  LBA_t sector,<span class="comment">/* Start sector in LBA */</span></span></span><br><span class="line"><span class="params">  UINT count<span class="comment">/* Number of sectors to read */</span></span></span><br><span class="line"><span class="params">)</span></span><br><span class="line">&#123;</span><br><span class="line">  DRESULT res;</span><br><span class="line">  <span class="type">uint8_t</span> result;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">switch</span>(pdrv)</span><br><span class="line">  &#123;</span><br><span class="line">  <span class="keyword">case</span> DEV_SPIFLASH :</span><br><span class="line">    <span class="comment">// translate the arguments here</span></span><br><span class="line"></span><br><span class="line">    FLASH_RD_Block_Start(sector * <span class="number">4096</span>);</span><br><span class="line">    FLASH_RD_Block(buff, count * <span class="number">4096</span>);</span><br><span class="line">    FLASH_RD_Block_End();</span><br><span class="line"></span><br><span class="line">    result = FLASH_ReadStatusReg();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// translate the reslut code here</span></span><br><span class="line">    <span class="keyword">switch</span>(result)</span><br><span class="line">    &#123;</span><br><span class="line">    <span class="keyword">case</span> SPI_FLASH_OK:</span><br><span class="line">      res = RES_OK;</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">case</span> SPI_FLASH_ERROR:</span><br><span class="line">      res = RES_ERROR;</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">case</span> SPI_FLASH_BUSY:</span><br><span class="line">      res = RES_NOTRDY;</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">case</span> SPI_FLASH_TIMEOUT:</span><br><span class="line">      res = RES_ERROR;</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> res;</span><br><span class="line"></span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> RES_PARERR;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/*-----------------------------------------------------------------------*/</span></span><br><span class="line"><span class="comment">/* Write Sector(s)                                                       */</span></span><br><span class="line"><span class="comment">/*-----------------------------------------------------------------------*/</span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">if</span> FF_FS_READONLY == 0</span></span><br><span class="line"></span><br><span class="line">DRESULT <span class="title function_">disk_write</span><span class="params">(</span></span><br><span class="line"><span class="params">  BYTE pdrv,<span class="comment">/* Physical drive nmuber to identify the drive */</span></span></span><br><span class="line"><span class="params">  <span class="type">const</span> BYTE* buff,<span class="comment">/* Data to be written */</span></span></span><br><span class="line"><span class="params">  LBA_t sector,<span class="comment">/* Start sector in LBA */</span></span></span><br><span class="line"><span class="params">  UINT count<span class="comment">/* Number of sectors to write */</span></span></span><br><span class="line"><span class="params">)</span></span><br><span class="line">&#123;</span><br><span class="line">  DRESULT res;</span><br><span class="line">  <span class="type">int</span> result;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">switch</span>(pdrv)</span><br><span class="line">  &#123;</span><br><span class="line">  <span class="keyword">case</span> DEV_SPIFLASH :</span><br><span class="line">    <span class="comment">// translate the arguments here</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (UINT i = <span class="number">0</span>; i &lt; count; i++)</span><br><span class="line">    &#123;</span><br><span class="line">      FLASH_Erase_Sector((sector + i) * <span class="number">4096</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    W25XXX_WR_Block((<span class="type">uint8_t</span>*)buff, sector * <span class="number">4096</span>, count * <span class="number">4096</span>);</span><br><span class="line">    result = FLASH_ReadStatusReg();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// translate the reslut code here</span></span><br><span class="line">    <span class="keyword">switch</span>(result)</span><br><span class="line">    &#123;</span><br><span class="line">    <span class="keyword">case</span> SPI_FLASH_OK:</span><br><span class="line">      res = RES_OK;</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">case</span> SPI_FLASH_ERROR:</span><br><span class="line">      res = RES_ERROR;</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">case</span> SPI_FLASH_BUSY:</span><br><span class="line">      res = RES_NOTRDY;</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">case</span> SPI_FLASH_TIMEOUT:</span><br><span class="line">      res = RES_ERROR;</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> res;</span><br><span class="line"></span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> RES_PARERR;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">endif</span></span></span><br><span class="line"></span><br><span class="line"><span class="comment">/*-----------------------------------------------------------------------*/</span></span><br><span class="line"><span class="comment">/* Miscellaneous Functions                                               */</span></span><br><span class="line"><span class="comment">/*-----------------------------------------------------------------------*/</span></span><br><span class="line"></span><br><span class="line">DRESULT <span class="title function_">disk_ioctl</span><span class="params">(</span></span><br><span class="line"><span class="params">  BYTE pdrv,<span class="comment">/* Physical drive nmuber (0..) */</span></span></span><br><span class="line"><span class="params">  BYTE cmd,<span class="comment">/* Control code */</span></span></span><br><span class="line"><span class="params">  <span class="type">void</span>* buff<span class="comment">/* Buffer to send/receive control data */</span></span></span><br><span class="line"><span class="params">)</span></span><br><span class="line">&#123;</span><br><span class="line">  DRESULT res = RES_OK;</span><br><span class="line"><span class="comment">//  int result;</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">switch</span>(pdrv)</span><br><span class="line">  &#123;</span><br><span class="line">  <span class="keyword">case</span> DEV_SPIFLASH :</span><br><span class="line">    <span class="keyword">switch</span>(cmd)</span><br><span class="line">    &#123;</span><br><span class="line">    <span class="keyword">case</span> GET_SECTOR_COUNT:<span class="comment">//将驱动器上可用扇区的数目返回到buff指向的DWORD变量中</span></span><br><span class="line">    &#123;</span><br><span class="line">      <span class="comment">/* 扇区数量：1536*4096/1024/1024=6(MB) */</span></span><br><span class="line">      *(DWORD*)buff = Flash_Sector_Count; <span class="comment">// 1536; //要的是DWORD类型的数据,所以给这个形参返回这样的数据</span></span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">case</span> GET_SECTOR_SIZE:<span class="comment">//将媒体的扇区大小返回到buff指向的WORD变量中</span></span><br><span class="line">    &#123;</span><br><span class="line">      *(WORD*)buff = Flash_Sector_Size; <span class="comment">//类型是WORD的类型,每个扇区是4096的大小,这里同时还需要修改MAX_SS的值</span></span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">case</span> GET_BLOCK_SIZE:<span class="comment">//将闪存介质的擦除块大小(以扇区为单位)返回到buff指向的DWORD变量中</span></span><br><span class="line">    &#123;</span><br><span class="line">      *(DWORD*)buff = <span class="number">1</span>; <span class="comment">//每次擦除的大小是1个扇区,因为单位是扇区</span></span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// Process of the command for the RAM drive</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> res;</span><br><span class="line"></span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> RES_PARERR;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="修改-ffconf-h-配置"><a href="#修改-ffconf-h-配置" class="headerlink" title="修改 ffconf.h 配置"></a>修改 ffconf.h 配置</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><span class="line"><span class="meta">#<span class="keyword">define</span> FF_MAX_SS4096  <span class="comment">// 使用的是SPI FLash,所以这个需要修改为4096</span></span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> FF_USE_MKFS1     <span class="comment">// 这个需要修改为1启用格式化的功能</span></span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> FF_CODE_PAGE936   <span class="comment">// 可以设置成936,增加对中文的支持</span></span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> FF_FS_NORTC1     <span class="comment">// 这个需要设置成1,就是先不搞RTC相关的日期功能</span></span></span><br></pre></td></tr></table></figure><h2 id="使用"><a href="#使用" class="headerlink" title="使用"></a>使用</h2><p>main.c 文件中大体如下操作即可</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></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;ff.h&quot;</span></span></span><br><span class="line"></span><br><span class="line">FATFS FsObject;</span><br><span class="line"></span><br><span class="line">FIL fp;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> BYTE work_buffer[<span class="number">4096</span>];</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">  FRESULT result;</span><br><span class="line">  result=f_mount(&amp;FsObject,<span class="string">&quot;0:&quot;</span>,<span class="number">1</span>); <span class="comment">// 这个0:就是路径,和#define FF_VOLUMES 1有关,设定为1则路径只有0:</span></span><br><span class="line"></span><br><span class="line">  <span class="comment">// 如果是13,则表明没有格式化,我们进行格式化</span></span><br><span class="line">  <span class="comment">// 如果是11,则表明数量对不上,需要去改设备个数,也就是FF_VOLUMES</span></span><br><span class="line">  <span class="keyword">if</span>(result == <span class="number">13</span>)</span><br><span class="line">  &#123;</span><br><span class="line">    MKFS_PARM Format = &#123;FM_FAT32, <span class="number">0</span>, <span class="number">0</span>, <span class="number">0</span>, <span class="number">0</span>&#125;;  <span class="comment">// 为了兼容,可以改为FM_ANY,对16MB来说,FAT16最合适</span></span><br><span class="line">    result = f_mkfs(<span class="string">&quot;0:&quot;</span>, &amp;Format, work_buffer, <span class="keyword">sizeof</span>(work_buffer));</span><br><span class="line"></span><br><span class="line">    result=f_mount(&amp;FsObject,<span class="string">&quot;0:&quot;</span>,<span class="number">1</span>);  <span class="comment">// 再次挂载,其实还可以进行判断</span></span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">//  result = f_open(&amp;fp, &quot;0:test.txt&quot;, FA_OPEN_ALWAYS|FA_WRITE|FA_READ);</span></span><br><span class="line"></span><br><span class="line"><span class="comment">//  UINT test;</span></span><br><span class="line"><span class="comment">//  result = f_write(&amp;fp,&quot;test1234&quot;,sizeof(&quot;test1234&quot;),&amp;test);</span></span><br><span class="line"><span class="comment">//  f_close(&amp;fp);</span></span><br><span class="line"></span><br><span class="line">  </span><br><span class="line">  result = f_open(&amp;fp, <span class="string">&quot;0:test.txt&quot;</span>, FA_OPEN_ALWAYS|FA_WRITE|FA_READ);</span><br><span class="line"></span><br><span class="line">  UINT test;</span><br><span class="line">  <span class="type">uint8_t</span> read[<span class="number">20</span>];</span><br><span class="line">  result=f_read(&amp;fp, read, f_size(&amp;fp), &amp;test);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">while</span>(<span class="number">1</span>)</span><br><span class="line">  &#123;</span><br><span class="line">    </span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="常见问题与调试技巧"><a href="#常见问题与调试技巧" class="headerlink" title="常见问题与调试技巧"></a>常见问题与调试技巧</h2><h3 id="Flash-相关问题"><a href="#Flash-相关问题" class="headerlink" title="Flash 相关问题"></a>Flash 相关问题</h3><ul><li><p><strong>Q: FLASH_ReadID() 返回 0xFFFFFF 或 0x000000？</strong><br>A: SPI Flash 没有接好，或者 SPI 接口初始化未正确完成；建议检查：</p><ul><li>SPI 时钟、模式是否与 Flash 兼容；</li><li>Flash 供电是否稳定；</li><li>CS 引脚是否正确拉低后开始通信；</li></ul></li><li><p><strong>Q: Flash 容量识别不对？</strong><br>A: FLASH_IC_Check() 中只处理了常见型号，若你使用的是不在列表内的型号，请根据 datasheet 添加对应的 JEDEC ID 和容量；</p></li></ul><h3 id="挂载与格式化相关"><a href="#挂载与格式化相关" class="headerlink" title="挂载与格式化相关"></a>挂载与格式化相关</h3><ul><li><p><strong>Q: f_mount 返回 FR_NO_FILESYSTEM（13），怎么解决？</strong><br>A: 说明当前设备上没有可识别的文件系统；应使用 f_mkfs 对 Flash 进行格式化，完成后再调用 f_mount 重新挂载；</p></li><li><p><strong>Q: f_mount 返回 FR_INVALID_DRIVE（11）？</strong><br>A: 说明 FatFS 的卷编号配置有问题，请检查 ffconf.h 中 FF_VOLUMES 是否 &gt;&#x3D; 你的逻辑盘号，比如 f_mount(…, “0:”, 1) 表示你至少得设置 #define FF_VOLUMES 1；</p></li></ul><h3 id="FAT-文件系统格式相关"><a href="#FAT-文件系统格式相关" class="headerlink" title="FAT 文件系统格式相关"></a>FAT 文件系统格式相关</h3><ul><li><strong>Q: f_mkfs() 返回 FR_INVALID_PARAMETER？</strong><br>A: f_mkfs 参数设置不当，建议使用如下方式初始化：</li></ul><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><span class="line">MKFS_PARM fs_param = &#123;FM_ANY, <span class="number">0</span>, <span class="number">0</span>, <span class="number">0</span>, <span class="number">0</span>&#125;;</span><br><span class="line">f_mkfs(<span class="string">&quot;0:&quot;</span>, &amp;fs_param, work_buffer, <span class="keyword">sizeof</span>(work_buffer));</span><br></pre></td></tr></table></figure><ul><li><strong>Q: 格式化完文件系统容量很小（比如识别为1MB）？</strong><br>A: 可能是扇区大小未正确返回，检查 disk_ioctl() 中 GET_SECTOR_SIZE 和 GET_SECTOR_COUNT 是否准确计算，是否符合你实际 Flash 容量；</li></ul><h3 id="读写文件异常"><a href="#读写文件异常" class="headerlink" title="读写文件异常"></a>读写文件异常</h3><ul><li><p><strong>Q: 文件写入后读出来的数据不对，乱码或者全 0？</strong><br>A: 可能原因</p><ul><li>写入之前没有正确擦除扇区；</li><li>写操作未对齐页写入（W25系列对页写入有要求）；</li><li>写入数据后未调用 f_close() 或 f_sync()，导致未刷新缓存到 Flash；</li><li>diskio.c 中 FLASH_Erase_Sector() 和 W25XXX_WR_Block() 地址未正确计算；</li></ul></li><li><p><strong>Q: 写文件成功了，但再次打开文件内容变空？</strong><br>A: 注意写模式是否是 FA_CREATE_ALWAYS，该模式会每次打开都清空内容；如果想保留内容，改为 FA_OPEN_ALWAYS | FA_WRITE 并调用 f_lseek(&amp;fp, f_size(&amp;fp)) 跳到末尾再写；</p></li></ul><h3 id="文件系统行为与配置相关"><a href="#文件系统行为与配置相关" class="headerlink" title="文件系统行为与配置相关"></a>文件系统行为与配置相关</h3><ul><li><strong>Q: 文件名太长无法识别？</strong><br>A: 默认 FatFS 禁用长文件名（LFN），需在 ffconf.h 中配置：</li></ul><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><span class="line"><span class="meta">#<span class="keyword">define</span> FF_USE_LFN    1</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> FF_MAX_LFN    64</span></span><br></pre></td></tr></table></figure><ul><li><strong>Q: 中文文件名乱码？</strong><br>A: 请设置正确的代码页，例如：</li></ul><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">define</span> FF_CODE_PAGE 936  <span class="comment">// 简体中文 GBK</span></span></span><br></pre></td></tr></table></figure><ul><li><strong>Q: 同一个文件写入后再读读取不到内容？</strong><br>A: 若写入后未关闭文件或调用 f_sync()，FatFS 可能未刷新数据到底层 Flash，建议：</li></ul><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><span class="line">f_write(...);</span><br><span class="line">f_sync(&amp;fp);  <span class="comment">// 确保数据落盘</span></span><br></pre></td></tr></table></figure><h3 id="运行异常-稳定性问题"><a href="#运行异常-稳定性问题" class="headerlink" title="运行异常 &#x2F; 稳定性问题"></a>运行异常 &#x2F; 稳定性问题</h3><ul><li><p><strong>Q: 写入操作中系统卡死或死循环？</strong><br>A: Flash 的写入&#x2F;擦除是阻塞操作，部分芯片擦除单个扇区可能耗时几十毫秒；建议你：</p><ul><li>在写函数中加入 watchdog 喂狗机制；</li><li>考虑用非阻塞 Flash 驱动 + 文件系统缓存策略来优化；</li></ul></li><li><p><strong>Q: Flash 写入过程中掉电，数据损坏？</strong><br>A: 推荐使用 FatFS 的事务机制，例如在写入文件时增加 f_sync()，或使用 FAT 的备用区功能（需高级配置）；另外也可以借助 CRC 校验机制保证文件有效性；</p></li></ul><h3 id="调试技巧"><a href="#调试技巧" class="headerlink" title="调试技巧"></a>调试技巧</h3><ul><li><strong>建议在初始化完成后先跑一个简单的读写测试函数</strong>：</li></ul><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><span class="line">f_open(&amp;fp, <span class="string">&quot;test.txt&quot;</span>, FA_CREATE_ALWAYS | FA_WRITE);</span><br><span class="line">f_write(&amp;fp, <span class="string">&quot;hello&quot;</span>, <span class="number">5</span>, &amp;bw);</span><br><span class="line">f_close(&amp;fp);</span><br><span class="line"></span><br><span class="line">f_open(&amp;fp, <span class="string">&quot;test.txt&quot;</span>, FA_READ);</span><br><span class="line">f_read(&amp;fp, buf, <span class="number">5</span>, &amp;br);</span><br><span class="line">f_close(&amp;fp);</span><br></pre></td></tr></table></figure><ul><li><p><strong>使用串口或者 RTT 等工具打印中间步骤结果</strong>，比如挂载结果、读写返回值、实际读出的内容，有助于快速定位问题；</p></li><li><p><strong>调试期间建议将所有错误码都打印出来对应 FR_XXX 含义</strong>，便于对照 FatFS 源码中的错误枚举；</p></li></ul><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本篇博客详细介绍了如何将 FatFS 移植到 SPI Flash，并通过 W25Q128 实现文件读写功能；从驱动实现、FatFS 配置、文件操作到问题排查，整个流程强调的是「实用」与「稳定」，希望对你的嵌入式项目有所帮助；</p><p>值得注意的是，<strong>SPI Flash 天生具备“写前擦除”“页擦除&#x2F;块擦除”的特性，且写入寿命有限</strong>（一般每个扇区约 10 万次擦写）；这意味着在频繁写入场景下，Flash 容易出现写坏、性能衰减等问题；</p><p>为此，建议关注以下几点：</p><ul><li><strong>磨损均衡（Wear Leveling）</strong>：FatFS 本身不具备磨损均衡机制，如果使用 SPI Flash 存储频繁变更的数据（如日志、数据库），需要在应用层实现“循环覆盖”或“动态地址映射”来避免单点反复擦写；</li><li><strong>避免频繁格式化和 f_open&#x2F;f_write&#x2F;f_close 操作循环</strong>，应尽可能复用文件句柄，按需 flush 写入；</li><li><strong>设置合适的缓存机制</strong>，如启用 sector 缓冲，减少物理擦写次数；</li><li><strong>建议定期备份重要数据</strong>，并在系统初始化时进行 Flash 健康检查（可利用空闲位、标志位判断 Flash 是否写满或擦损）；</li><li><strong>日志&#x2F;配置文件等</strong> 建议使用固定格式（如简化版 TLV）写入，便于恢复和分析；</li></ul><p>最后，虽然 FatFS 的结构设计优雅轻量，但在用它搭配 SPI Flash 构建嵌入式文件系统时，我们仍需深入理解底层 Flash 的行为特性，并结合自身项目场景做出相应调整和优化；</p><p>下一步，我计划基于 <strong>USB Composite（复合设备）</strong> 实现 STM32 同时具备串口调试和 <strong>模拟U盘功能</strong>，通过 USB MSC 协议挂载 FatFS 文件系统，让用户能够在电脑端直接读写 SPI Flash 中的数据，这将进一步提升系统的易用性和可扩展性，敬请期待！</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;FatFS官网：&lt;a class=&quot;link&quot;   href=&quot;https://elm-chan.org/fsw/ff/00index_e.html&quot; &gt;https://elm-chan.org/fsw/ff/00index_e.html&lt;i class=&quot;fas fa-e</summary>
      
    
    
    
    <category term="mcu" scheme="http://example.com/categories/mcu/"/>
    
    
  </entry>
  
  <entry>
    <title>Docker部署MQTT服务器</title>
    <link href="http://example.com/2024/08/20/docker/Docker-EMQX/"/>
    <id>http://example.com/2024/08/20/docker/Docker-EMQX/</id>
    <published>2024-08-20T09:49:12.000Z</published>
    <updated>2024-08-20T09:49:12.000Z</updated>
    
    <content type="html"><![CDATA[<p>参考：</p><ul><li><a class="link"   href="https://blog.csdn.net/mftang/article/details/136585110" >https://blog.csdn.net/mftang/article/details/136585110<i class="fas fa-external-link-alt"></i></a></li><li><a class="link"   href="https://mftang.blog.csdn.net/article/details/136601795" >https://mftang.blog.csdn.net/article/details/136601795<i class="fas fa-external-link-alt"></i></a></li></ul><p>官网：<a class="link"   href="https://www.emqx.com/zh" >https://www.emqx.com/zh<i class="fas fa-external-link-alt"></i></a></p><p>在物联网系统中，消息队列是设备通信的核心，而 MQTT 作为轻量、高效的通信协议，已经成为行业默认标准；说到 MQTT Broker，大多数人第一时间想到的就是 EMQX：一款高性能、开源且支持企业级功能的 MQTT 消息服务器；</p><blockquote><p><strong>注意</strong>：企业级功能或者商用需要付费使用；</p></blockquote><p>不过 EMQX 的部署一直被人觉得“偏重”，各种配置繁琐、界面复杂，特别是初学者常常被一堆文档劝退；其实用 Docker 部署 EMQX，整个过程不仅轻量而且非常优雅；只要几条命令，就可以在本地快速跑起来，甚至连可视化管理界面都顺带搭好了；</p><p>本篇文章会一步步带你通过 Docker 部署 EMQX，适合刚接触 MQTT 或想自建消息服务的小伙伴；</p><blockquote><p>顺带一提，EMQ 还提供了一个更轻量的版本： <a class="link"   href="https://nanomq.io/zh" >NanoMQ<i class="fas fa-external-link-alt"></i></a>，专为嵌入式 Linux 设备设计，如果你后续在做边缘侧 MQTT 应用，也可以考虑这个版本；</p></blockquote><h2 id="Docker-部署-EMQX"><a href="#Docker-部署-EMQX" class="headerlink" title="Docker 部署 EMQX"></a>Docker 部署 EMQX</h2><p>部署方式选择的是官方提供的 开源版 + 自托管 + Docker 镜像：</p><p>下载地址（可查看所有平台选项）：<a class="link"   href="https://www.emqx.io/zh/downloads" >https://www.emqx.io/zh/downloads<i class="fas fa-external-link-alt"></i></a></p><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><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 拉取 EMQX 开源版镜像（版本 5.7.2）,如果想看别的版本可以去docker hub查看</span></span><br><span class="line">docker pull emqx/emqx:5.7.2</span><br><span class="line"></span><br><span class="line"><span class="comment"># 启动容器并映射常用端口</span></span><br><span class="line">docker run -d --name emqx \</span><br><span class="line">  -p 1883:1883 \</span><br><span class="line">  -p 8883:8883 \</span><br><span class="line">  -p 8083:8083 \</span><br><span class="line">  -p 8084:8084 \</span><br><span class="line">  -p 18083:18083 \</span><br><span class="line">  emqx/emqx:5.7.2</span><br></pre></td></tr></table></figure><h2 id="常用端口说明"><a href="#常用端口说明" class="headerlink" title="常用端口说明"></a>常用端口说明</h2><table><thead><tr><th>端口号</th><th>协议说明</th></tr></thead><tbody><tr><td>1883</td><td>MQTT over TCP（最常用端口）</td></tr><tr><td>8883</td><td>MQTT over SSL&#x2F;TLS（加密传输）</td></tr><tr><td>8083</td><td>MQTT over WebSocket</td></tr><tr><td>8084</td><td>MQTT over WSS（加密 WebSocket）</td></tr><tr><td>18083</td><td>HTTP Dashboard &amp; REST API</td></tr><tr><td>4370 &#x2F; 5370</td><td>集群通信端口（本次没用到）</td></tr><tr><td>5683</td><td>CoAP端口（本次没用到）</td></tr></tbody></table><h2 id="访问管理后台（Dashboard）"><a href="#访问管理后台（Dashboard）" class="headerlink" title="访问管理后台（Dashboard）"></a>访问管理后台（Dashboard）</h2><p>由于 EMQX 的网页管理后台需要通过 18083 端口访问，为了方便管理，我通过本地 Caddy 做了 HTTPS 反向代理，这样就可以通过我自己的域名访问了：mqtt.140105.xyz</p><blockquote><p>如果你没做 Caddy 代理，直接访问宿主机的 <a class="link"   href="http://localhost:18083/" >http://localhost:18083<i class="fas fa-external-link-alt"></i></a> 也是一样的；</p></blockquote><p>默认账号密码是：</p><ul><li>用户名：admin</li><li>密码：public</li></ul><p>首次登录建议立即修改密码，避免风险；</p><h2 id="客户端认证设置"><a href="#客户端认证设置" class="headerlink" title="客户端认证设置"></a>客户端认证设置</h2><p>在 EMQX 中，可以通过<strong>密码认证</strong>来控制哪些客户端可以连接；</p><p>操作路径：<br>Dashboard 面板 -&gt; <strong>访问控制</strong> -&gt; <strong>客户端认证</strong></p><p>配置流程如下：</p><ul><li><p>新建一个认证方式：</p><ul><li>类型选择：Password-Based</li><li>认证数据源：内置数据库</li><li>用户名类型：username</li><li>加密算法、加盐策略使用默认配置即可</li></ul></li><li><p>添加用户账号：</p><ul><li>用户名：emtime</li><li>密码：你自己的常用密码</li></ul></li></ul><h2 id="客户端连接测试"><a href="#客户端连接测试" class="headerlink" title="客户端连接测试"></a>客户端连接测试</h2><h3 id="桌面客户端推荐"><a href="#桌面客户端推荐" class="headerlink" title="桌面客户端推荐"></a>桌面客户端推荐</h3><p>建议使用官方出品的 MQTT 客户端：<a class="link"   href="https://mqttx.app/zh" >MQTTX<i class="fas fa-external-link-alt"></i></a></p><p>配置方式如下：</p><ul><li>地址填写你自己的域名或本机地址，如：mqtt.140105.xyz</li><li>端口填写 1883（TCP MQTT）</li><li>用户名 &#x2F; 密码 填写你刚才设置的认证信息</li></ul><p>点击连接后，右上角绿色表示连接成功，就可以自己进行订阅 &#x2F; 发布测试了；</p><h3 id="Web-客户端（可选）"><a href="#Web-客户端（可选）" class="headerlink" title="Web 客户端（可选）"></a>Web 客户端（可选）</h3><p>你也可以用官方提供的 Web 客户端工具：<a class="link"   href="https://mqttx.app/web-client" >https://mqttx.app/web-client<i class="fas fa-external-link-alt"></i></a></p><p>不过我个人测试的时候没连上，可能是网络或证书的问题，也可以使用 Dashboard 自带的测试功能尝试；</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>这套部署流程非常适合个人或小型项目使用，Docker 启动简单、配置界面直观，而且支持多种认证机制，非常灵活；后续如果你有更复杂的使用场景（如 TLS 加密、集群部署、ACL 访问控制等），EMQX 都能胜任；</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;参考：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class=&quot;link&quot;   href=&quot;https://blog.csdn.net/mftang/article/details/136585110&quot; &gt;https://blog.csdn.net/mftang/article/de</summary>
      
    
    
    
    <category term="docker" scheme="http://example.com/categories/docker/"/>
    
    
  </entry>
  
  <entry>
    <title>Docker部署Certd，实现HTTPS证书自动申请与更新</title>
    <link href="http://example.com/2024/08/19/docker/Docker-Certd/"/>
    <id>http://example.com/2024/08/19/docker/Docker-Certd/</id>
    <published>2024-08-19T14:19:47.000Z</published>
    <updated>2024-08-19T14:19:47.000Z</updated>
    
    <content type="html"><![CDATA[<p>Certd官网：<a class="link"   href="https://certd.docmirror.cn/" >https://certd.docmirror.cn<i class="fas fa-external-link-alt"></i></a></p><p>在现代网站服务中，HTTPS 已成为保障通信安全的基础组件；为了简化证书申请与更新流程，Certd 提供了自动化的证书管理能力，并支持多家主流证书颁发机构；</p><p>本文将详细介绍如何通过 Docker 快速部署 Certd，实现 HTTPS 证书的自动申请与更新；</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><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></pre></td><td class="code"><pre><span class="line"><span class="comment"># 创建用来存放Certd相关的</span></span><br><span class="line"><span class="built_in">mkdir</span> certd</span><br><span class="line"></span><br><span class="line"><span class="built_in">cd</span> certd</span><br><span class="line"></span><br><span class="line">wget https://gitee.com/certd/certd/raw/v2/docker/run/docker-compose.yaml</span><br><span class="line"></span><br><span class="line">nano docker-compose.yaml</span><br><span class="line"></span><br><span class="line">docker conmpose up -d</span><br><span class="line"></span><br><span class="line"><span class="comment"># 停止并删除容器，不用怕数据，数据已经挂载到宿主机了</span></span><br><span class="line"><span class="comment">#docker compose down</span></span><br></pre></td></tr></table></figure><p>我自己的docker-compose.yaml文件如下：</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><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><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">version:</span> <span class="string">&#x27;3.3&#x27;</span> <span class="comment"># 兼容旧版docker-compose</span></span><br><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">certd:</span></span><br><span class="line">    <span class="comment"># 镜像                                                  #  ↓↓↓↓↓ ---- 镜像版本号，建议改成固定版本号,例如：certd:1.29.0</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest</span></span><br><span class="line">    <span class="attr">container_name:</span> <span class="string">certd</span> <span class="comment"># 容器名</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">always</span> <span class="comment"># 自动重启</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="comment">#   ↓↓↓↓↓ -------------------------------------------------------- 数据库以及证书存储路径,默认存在宿主机的/data/certd/目录下，【您需要定时备份此目录，以保障数据容灾】</span></span><br><span class="line">      <span class="comment">#                                                                  只要修改冒号前面的，冒号后面的/app/data不要动</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">./:/app/data</span></span><br><span class="line">    <span class="attr">ports:</span> <span class="comment"># 端口映射</span></span><br><span class="line">      <span class="comment">#  ↓↓↓↓ ---------------------------------------------------------- 如果端口有冲突，可以修改第一个7001为其他不冲突的端口号，第二个7001不要动</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;7001:7001&quot;</span></span><br><span class="line">      <span class="comment">#  ↓↓↓↓ ---------------------------------------------------------- https端口，可以根据实际情况，是否暴露该端口</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;7002:7002&quot;</span></span><br><span class="line">    <span class="comment">#↓↓↓↓ -------------------------------------------------------------- 如果出现getaddrinfo ENOTFOUND错误，可以尝试设置dns</span></span><br><span class="line">    <span class="attr">dns:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="number">223.5</span><span class="number">.5</span><span class="number">.5</span></span><br><span class="line">      <span class="bullet">-</span> <span class="number">223.6</span><span class="number">.6</span><span class="number">.6</span></span><br><span class="line">    </span><br><span class="line"><span class="comment">#    dns:</span></span><br><span class="line"><span class="comment">#      - 223.5.5.5      # 阿里云公共dns</span></span><br><span class="line"><span class="comment">#      - 223.6.6.6</span></span><br><span class="line"><span class="comment">#       # ↓↓↓↓ --------------------------------------------------------- 如果你服务器在腾讯云，可以用这个替换上面阿里云的公共dns</span></span><br><span class="line"><span class="comment">#      - 119.29.29.29  # 腾讯云公共dns</span></span><br><span class="line"><span class="comment">#      - 182.254.116.116</span></span><br><span class="line"><span class="comment">#       # ↓↓↓↓ --------------------------------------------------------- 如果你服务器部署在国外，可以用这个替换上面阿里云的公共dns</span></span><br><span class="line"><span class="comment">#      - 8.8.8.8       # 谷歌公共dns</span></span><br><span class="line"><span class="comment">#      - 8.8.4.4</span></span><br><span class="line"><span class="comment">#    extra_hosts:</span></span><br><span class="line"><span class="comment">#        # ↓↓↓↓ -------------------------------------------------------- 这里可以配置自定义hosts，外网域名可以指向本地局域网ip地址</span></span><br><span class="line"><span class="comment">#      - &quot;localdomain.com:192.168.1.3&quot;</span></span><br><span class="line"><span class="comment">#        #         ↓↓↓↓ ------------------------------------------------ 直接使用主机的网络，如果网络问题实在找不到原因，可以尝试打开此参数</span></span><br><span class="line"><span class="comment">#    network_mode: host</span></span><br><span class="line">    <span class="attr">labels:</span></span><br><span class="line">      <span class="attr">com.centurylinklabs.watchtower.enable:</span> <span class="string">&quot;true&quot;</span></span><br><span class="line"><span class="comment">#    ↓↓↓↓ -------------------------------------------------------------- 启用ipv6网络，还需要把下面networks的注释放开</span></span><br><span class="line"><span class="comment">#    networks:</span></span><br><span class="line"><span class="comment">#      - ip6net</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line"><span class="comment">#     设置环境变量即可自定义certd配置</span></span><br><span class="line"><span class="comment">#     配置项见： packages/ui/certd-server/src/config/config.default.ts</span></span><br><span class="line"><span class="comment">#     配置规则： certd_ + 配置项, 点号用_代替</span></span><br><span class="line"><span class="comment">#                                    #↓↓↓↓ ----------------------------- 如果忘记管理员密码，可以设置为true，重启之后，管理员密码将改成123456，然后请及时修改回false</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">certd_system_resetAdminPasswd=false</span></span><br><span class="line"></span><br><span class="line"><span class="comment">#     默认使用sqlite文件数据库，如果需要使用其他数据库，请设置以下环境变量</span></span><br><span class="line"><span class="comment">#     注意： 选定使用一种数据库之后，不支持更换数据库；</span></span><br><span class="line"><span class="comment">#     数据库迁移方法：1、使用新数据库重新部署一套，然后将旧数据同步过去，注意flyway_history表的数据不要同步</span></span><br><span class="line"><span class="comment">#                                    #↓↓↓↓ ----------------------------- 使用postgresql数据库，需要提前创建数据库</span></span><br><span class="line"><span class="comment">#      - certd_flyway_scriptDir=./db/migration-pg                        # 升级脚本目录</span></span><br><span class="line"><span class="comment">#      - certd_typeorm_dataSource_default_type=postgres                  # 数据库类型</span></span><br><span class="line"><span class="comment">#      - certd_typeorm_dataSource_default_host=localhost                 # 数据库地址</span></span><br><span class="line"><span class="comment">#      - certd_typeorm_dataSource_default_port=5433                      # 数据库端口</span></span><br><span class="line"><span class="comment">#      - certd_typeorm_dataSource_default_username=postgres              # 用户名</span></span><br><span class="line"><span class="comment">#      - certd_typeorm_dataSource_default_password=yourpasswd            # 密码</span></span><br><span class="line"><span class="comment">#      - certd_typeorm_dataSource_default_database=certd                 # 数据库名</span></span><br><span class="line"></span><br><span class="line"><span class="comment">#                                    #↓↓↓↓ ----------------------------- 使用mysql数据库，需要提前创建数据库 charset=utf8mb4, collation=utf8mb4_bin</span></span><br><span class="line"><span class="comment">#      - certd_flyway_scriptDir=./db/migration-mysql                     # 升级脚本目录</span></span><br><span class="line"><span class="comment">#      - certd_typeorm_dataSource_default_type=mysql                     # 数据库类型， 或者 mariadb</span></span><br><span class="line"><span class="comment">#      - certd_typeorm_dataSource_default_host=localhost                 # 数据库地址</span></span><br><span class="line"><span class="comment">#      - certd_typeorm_dataSource_default_port=3306                      # 数据库端口</span></span><br><span class="line"><span class="comment">#      - certd_typeorm_dataSource_default_username=root                  # 用户名</span></span><br><span class="line"><span class="comment">#      - certd_typeorm_dataSource_default_password=yourpasswd            # 密码</span></span><br><span class="line"><span class="comment">#      - certd_typeorm_dataSource_default_database=certd                 # 数据库名</span></span><br><span class="line"></span><br><span class="line"><span class="comment">#         ↓↓↓↓ ---------------------------------------------------------  自动升级，上面certd的版本号要保持为latest</span></span><br><span class="line"><span class="comment">#  certd-updater:  # 添加 Watchtower 服务</span></span><br><span class="line"><span class="comment">#    image: containrrr/watchtower:latest</span></span><br><span class="line"><span class="comment">#    container_name: certd-updater</span></span><br><span class="line"><span class="comment">#    restart: unless-stopped</span></span><br><span class="line"><span class="comment">#    volumes:</span></span><br><span class="line"><span class="comment">#      - /var/run/docker.sock:/var/run/docker.sock</span></span><br><span class="line"><span class="comment">#    # 配置 自动更新</span></span><br><span class="line"><span class="comment">#    environment:</span></span><br><span class="line"><span class="comment">#      - WATCHTOWER_CLEANUP=true            # 自动清理旧版本容器</span></span><br><span class="line"><span class="comment">#      - WATCHTOWER_INCLUDE_STOPPED=false   # 不更新已停止的容器</span></span><br><span class="line"><span class="comment">#      - WATCHTOWER_LABEL_ENABLE=true       # 根据容器标签进行更新</span></span><br><span class="line"><span class="comment">#      - WATCHTOWER_POLL_INTERVAL=600       # 每 10 分钟检查一次更新</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">#    ↓↓↓↓ -------------------------------------------------------------- 启用ipv6网络，还需要把上面networks的注释放开</span></span><br><span class="line"><span class="comment">#networks:</span></span><br><span class="line"><span class="comment">#  ip6net:</span></span><br><span class="line"><span class="comment">#    enable_ipv6: true</span></span><br><span class="line"><span class="comment">#    ipam:</span></span><br><span class="line"><span class="comment">#      config:</span></span><br><span class="line"><span class="comment">#        - subnet: 2001:db8::/64</span></span><br></pre></td></tr></table></figure><h3 id="访问-Certd-面板"><a href="#访问-Certd-面板" class="headerlink" title="访问 Certd 面板"></a>访问 Certd 面板</h3><p>部署完成后，可通过以下地址访问 Certd：</p><ul><li><a class="link"   href="http://your_server_ip:7001/" >http://your_server_ip:7001<i class="fas fa-external-link-alt"></i></a></li><li><a class="link"   href="https://your_server_ip:7002/" >https://your_server_ip:7002<i class="fas fa-external-link-alt"></i></a></li></ul><figure class="highlight pgsql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">默认账号密码：</span><br><span class="line"><span class="keyword">admin</span>/<span class="number">123456</span></span><br></pre></td></tr></table></figure><p>首次登录后请务必修改管理员密码；</p><h3 id="配置自动签发证书"><a href="#配置自动签发证书" class="headerlink" title="配置自动签发证书"></a>配置自动签发证书</h3><p>Certd 支持多种 DNS API 接入方式（阿里云、腾讯云、Cloudflare等），根据你的域名服务商选择配置方法；详细配置方式请参考官方文档的 DNS API 配置指南；</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;Certd官网：&lt;a class=&quot;link&quot;   href=&quot;https://certd.docmirror.cn/&quot; &gt;https://certd.docmirror.cn&lt;i class=&quot;fas fa-external-link-alt&quot;&gt;&lt;/i&gt;&lt;/a&gt;&lt;/p&gt;
</summary>
      
    
    
    
    <category term="docker" scheme="http://example.com/categories/docker/"/>
    
    
  </entry>
  
  <entry>
    <title>Caddy反向代理</title>
    <link href="http://example.com/2024/08/19/linux/Caddy/"/>
    <id>http://example.com/2024/08/19/linux/Caddy/</id>
    <published>2024-08-19T14:19:03.000Z</published>
    <updated>2024-08-19T14:19:03.000Z</updated>
    
    <content type="html"><![CDATA[<p>Caddy官网：<a class="link"   href="https://caddyserver.com/" >https://caddyserver.com<i class="fas fa-external-link-alt"></i></a></p><h2 id="为什么我开始用-Caddy"><a href="#为什么我开始用-Caddy" class="headerlink" title="为什么我开始用 Caddy"></a>为什么我开始用 Caddy</h2><p>最初接触 Caddy，是因为它在社区中被广泛推荐为一款“自动 HTTPS 一把梭”的轻量级 Web 服务器；只要你有个域名、端口没被封，Caddy 理论上能自动申请并续签 HTTPS 证书，全程无需手动操作，真正实现开箱即用；</p><p>不过，<strong>实话实说，我并没能成功实现自动 HTTPS</strong>；<br>我这台小水管部署在没有公网 IP 的局域网环境中，Let’s Encrypt 验证域名所有权时无法访问到我的服务器，自然无法成功签发证书；翻了一些文档后确认，这种内网场景确实不适合自动签证；</p><p>但我使用 Caddy 的<strong>真正目的其实并不是自动 HTTPS</strong>，而是简洁地实现反向代理；我的需求是把一些局域网服务（比如 Web 控制面板、API 服务、博客页面）通过统一的域名访问，而不是记一堆不同端口；</p><p>相比 Nginx 那一堆配置、手动写 location、reload 重载，<strong>Caddy 用 Caddyfile 几行就搞定</strong>，还能自动帮我处理 header、压缩、缓存等常用设置；<br>虽然 HTTPS 自动签发失败，但 Caddy 依然凭借其优秀的反向代理能力和极简配置体验让我留下它；</p><h2 id="安装和基本使用"><a href="#安装和基本使用" class="headerlink" title="安装和基本使用"></a>安装和基本使用</h2><h3 id="安装-Caddy（推荐官方一键脚本）"><a href="#安装-Caddy（推荐官方一键脚本）" class="headerlink" title="安装 Caddy（推荐官方一键脚本）"></a>安装 Caddy（推荐官方一键脚本）</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl -fsSL https://get.caddyserver.com | bash -s personal</span><br></pre></td></tr></table></figure><p>或者使用包管理器，如在 Debian&#x2F;Ubuntu 下：</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><span class="line"><span class="built_in">sudo</span> apt install -y debian-keyring debian-archive-keyring apt-transport-https</span><br><span class="line">curl -1sLf <span class="string">&#x27;https://dl.cloudsmith.io/public/caddy/stable/gpg.key&#x27;</span> | <span class="built_in">sudo</span> gpg --dearmor -o /etc/apt/trusted.gpg.d/caddy.gpg</span><br><span class="line">curl -1sLf <span class="string">&#x27;https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt&#x27;</span> | <span class="built_in">sudo</span> <span class="built_in">tee</span> /etc/apt/sources.list.d/caddy.list</span><br><span class="line"><span class="built_in">sudo</span> apt update</span><br><span class="line"><span class="built_in">sudo</span> apt install caddy</span><br></pre></td></tr></table></figure><h3 id="编写配置文件"><a href="#编写配置文件" class="headerlink" title="编写配置文件"></a>编写配置文件</h3><p>默认配置文件路径为 &#x2F;etc&#x2F;caddy&#x2F;Caddyfile，也可以自己创建一个 Caddyfile 并通过命令指定；</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> nano /etc/caddy/Caddyfile</span><br></pre></td></tr></table></figure><h3 id="启动-Caddy"><a href="#启动-Caddy" class="headerlink" title="启动 Caddy"></a>启动 Caddy</h3><p>编辑完配置后，使用 systemd 启动 Caddy：</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><span class="line"><span class="built_in">sudo</span> systemctl restart caddy</span><br><span class="line"><span class="built_in">sudo</span> systemctl <span class="built_in">enable</span> caddy</span><br></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><span class="line">caddy run --config /path/to/Caddyfile</span><br></pre></td></tr></table></figure><h2 id="我的-Caddy-配置详解"><a href="#我的-Caddy-配置详解" class="headerlink" title="我的 Caddy 配置详解"></a>我的 Caddy 配置详解</h2><h3 id="配置监听端口（http-port）"><a href="#配置监听端口（http-port）" class="headerlink" title="配置监听端口（http_port）"></a>配置监听端口（http_port）</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><span class="line">&#123;</span><br><span class="line">http_port 8080</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>因为 80 端口经常被占用（尤其内网小主机上），我将默认 HTTP 监听端口改为了 8080，更灵活些；</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></pre></td><td class="code"><pre><span class="line">你的域名 &#123;</span><br><span class="line">    <span class="comment"># 你的域名证书</span></span><br><span class="line">tls cert.crt cert.key</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 本地服务</span></span><br><span class="line">reverse_proxy 127.0.0.1:34718</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个配置用于访问运行在本机 34718 端口的服务；</p><p>域名解析的是我在虚拟组网中的主机地址，因此组网内的设备都能通过域名来访问该服务；</p><p>这里的证书我是使用Certd申请的，详情见：<a href="/2024/08/19/docker/Docker-Certd">Docker部署Certd，实现HTTPS证书自动申请与更新</a>；</p><h3 id="反代局域网中的https服务"><a href="#反代局域网中的https服务" class="headerlink" title="反代局域网中的https服务"></a>反代局域网中的https服务</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><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">你的域名 &#123;</span><br><span class="line">    reverse_proxy https://192.168.0.1:1234 &#123;</span><br><span class="line">        transport http &#123;</span><br><span class="line">            <span class="comment"># 跳过 TLS 验证，避免证书错误导致Caddy反代失败</span></span><br><span class="line">            tls_insecure_skip_verify</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 你的证书</span></span><br><span class="line">    tls cert.crt cert.key</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里反向代理的是别的设备的https服务；</p><p>Caddy 默认验证目标服务的 TLS 证书，由于我这里是自签的，就使用 tls_insecure_skip_verify 跳过了验证，避免反代失败；</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><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></pre></td><td class="code"><pre><span class="line">:21824 &#123;</span><br><span class="line">tls emtime.crt emtime.key</span><br><span class="line">root * /home/time/doc/blog/public</span><br><span class="line">file_server</span><br><span class="line"></span><br><span class="line">handle_errors &#123;</span><br><span class="line">@404 &#123;</span><br><span class="line">expression &#123;http.error.status_code&#125; == 404</span><br><span class="line">&#125;</span><br><span class="line">rewrite @404 /404.html</span><br><span class="line">file_server</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这是一个纯静态服务配置，我用来部署 Hexo 生成的博客页面；</p><p>Caddy 会监听 21824 端口，并将 &#x2F;home&#x2F;time&#x2F;doc&#x2F;blog&#x2F;public 目录作为静态资源根目录；配置中还加了自定义的 404.html 页面处理逻辑；</p><p>因为没有公网 IP，这个页面更多是在局域网访问时使用；</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><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></pre></td><td class="code"><pre><span class="line">blog.emtime.top &#123;</span><br><span class="line">tls emtime.crt emtime.key</span><br><span class="line">root * /home/time/doc/blog/public</span><br><span class="line">file_server</span><br><span class="line"></span><br><span class="line">handle_errors &#123;</span><br><span class="line">@404 &#123;</span><br><span class="line">expression &#123;http.error.status_code&#125; == 404</span><br><span class="line">&#125;</span><br><span class="line">rewrite @404 /404.html</span><br><span class="line">file_server</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个配置和上面几乎一致，只是绑定了 blog.emtime.top 这个域名，可能正是<strong>你现在正在访问的这个博客</strong>的域名（我还有别的域名也同样部署了同样的博客）；</p><blockquote><p>因为我的小水管没有公网 IP，这个域名是通过 frp 穿透暴露出来的；<br>这里简单说一下，我使用的是frp的方法，创建HTTPS隧道，选择本机的443端口进行映射，域名通过cname的方法解析到frp的域名即可；</p></blockquote><h3 id="为什么我最终选择了-Caddy-而不是-Nginx？"><a href="#为什么我最终选择了-Caddy-而不是-Nginx？" class="headerlink" title="为什么我最终选择了 Caddy 而不是 Nginx？"></a>为什么我最终选择了 Caddy 而不是 Nginx？</h3><p>其实我也试过 Nginx，作为一个功能成熟的老牌服务器，它确实非常灵活；但对我这种轻量化内网部署场景来说，Caddy 的优势太明显了：</p><ul><li>Nginx 配置冗长，一写就是几十行；</li><li>Caddy 的配置语言天然支持反向代理，几行就能完成；</li><li>Caddy 默认自带 TLS（即使我没成功用自动签发）；</li><li>Caddy 自带静态文件服务、自动压缩、常用 header，非常适合个人小服务；</li><li>部署只需要一个二进制，不依赖太多系统组件，非常适合局域网小主机部署；</li></ul><p>虽然 Caddy 的自动 HTTPS 功能在我的场景下并没有成功使用，但它的反向代理能力和极低的学习成本，已经让我在日常部署中离不开它了；</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>主要就是说了说我如何在没有公网的局域网环境下使用 Caddy 来构建统一访问入口，反代各种服务、博客页面和管理面板；</p><p>如果你也有类似的需求，不妨试试 Caddy，即使不能一把梭 HTTPS，它依然是一个非常轻便好用的反代工具；</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;Caddy官网：&lt;a class=&quot;link&quot;   href=&quot;https://caddyserver.com/&quot; &gt;https://caddyserver.com&lt;i class=&quot;fas fa-external-link-alt&quot;&gt;&lt;/i&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2 id</summary>
      
    
    
    
    <category term="linux" scheme="http://example.com/categories/linux/"/>
    
    
  </entry>
  
  <entry>
    <title>Docker部署密码管理工具</title>
    <link href="http://example.com/2024/08/18/docker/Docker-Bitwarden/"/>
    <id>http://example.com/2024/08/18/docker/Docker-Bitwarden/</id>
    <published>2024-08-18T15:24:31.000Z</published>
    <updated>2024-08-20T09:49:01.000Z</updated>
    
    <content type="html"><![CDATA[<p>参考：<a class="link"   href="https://ohdmire.github.io/posts/bitwarden" >https://ohdmire.github.io/posts/bitwarden<i class="fas fa-external-link-alt"></i></a><br>仓库：<a class="link"   href="https://github.com/dani-garcia/vaultwarden" >https://github.com/dani-garcia/vaultwarden<i class="fas fa-external-link-alt"></i></a></p><p>在数字化生活中，密码管理已经不再是“记几个数字”那么简单；各种网站、应用动辄要求复杂度极高的密码，安全意识一提升，每个账户还不能用同一个密码；这时候，一个好用的密码管理工具就显得尤为重要；</p><p>Bitwarden 是一款非常优秀的开源密码管理器，而 Vaultwarden 则是它的轻量级“克隆版”——资源占用小，部署简单，并且还有一些特有的功能，非常适合自建；更重要的是，自部署意味着你的数据真正掌握在你自己手里；</p><p>这篇文章会带你一步步部署 Vaultwarden，构建一个属于你自己的私有密码管理系统，不依赖任何第三方服务，无需高性能服务器，即便是树莓派也能轻松运行；适合注重隐私、安全、掌控感的你；</p><h2 id="几个关键点先说清楚："><a href="#几个关键点先说清楚：" class="headerlink" title="几个关键点先说清楚："></a>几个关键点先说清楚：</h2><ul><li><strong>Docker 镜像不是官方的</strong>：使用的是 vaultwarden&#x2F;server，优点是轻量、解锁 Bitwarden 高级功能，适合个人部署使用；</li><li><strong>必须使用 HTTPS</strong>：Vaultwarden 不支持在 HTTP 下登录或注册，所以部署过程必须搭配 HTTPS 服务，最好申请证书；</li></ul><h2 id="Docker-部署过程"><a href="#Docker-部署过程" class="headerlink" title="Docker 部署过程"></a>Docker 部署过程</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><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></pre></td><td class="code"><pre><span class="line"><span class="comment"># 拉取镜像</span></span><br><span class="line">docker pull vaultwarden/server:latest</span><br><span class="line"></span><br><span class="line"><span class="comment"># 准备本地挂载目录，保存 Vaultwarden 的 SQLite 数据</span></span><br><span class="line"><span class="built_in">mkdir</span> -p /home/time/doc/codefile/password</span><br><span class="line"></span><br><span class="line"><span class="comment"># 启动容器</span></span><br><span class="line">docker run -d \</span><br><span class="line">  --name password \</span><br><span class="line">  -v /home/time/doc/codefile/password/:/data/ \</span><br><span class="line">  --restart unless-stopped \</span><br><span class="line">  -p 6468:80 \</span><br><span class="line">  vaultwarden/server:latest</span><br></pre></td></tr></table></figure><p>这个服务会监听在本地的 6468 端口，<strong>页面是能打开的，但不能登录或注册</strong>，因为没有 HTTPS；</p><h2 id="HTTPS-配置说明"><a href="#HTTPS-配置说明" class="headerlink" title="HTTPS 配置说明"></a>HTTPS 配置说明</h2><p>Vaultwarden 的前端 强制要求 HTTPS 访问，这是很多人部署时最容易卡壳的地方；我这边采用了 Caddy 来做反向代理，部署逻辑简单、证书配置灵活，比较适合个人自部署的场景；</p><ul><li><p>Caddy反向代理：我使用的是 Caddy，配置非常简洁；具体原理和部署过程可以参考我之前写的这篇博客：<a href="/2024/08/19/linux/Caddy">Caddy反向代理配置详解</a>；</p></li><li><p>Caddy 配置示例：</p></li></ul><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></pre></td><td class="code"><pre><span class="line">你的域名 &#123;</span><br><span class="line">    # 你的域名证书</span><br><span class="line">tls cert.crt cert.key</span><br><span class="line"></span><br><span class="line">    # 反向代理到 Vaultwarden 容器所在端口</span><br><span class="line">reverse_proxy 127.0.0.1:6468</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>证书来源：我没有使用 Caddy 自带的自动证书申请功能，而是通过 <a href="/2024/08/19/docker/Docker-Certd">Certd</a> 生成了泛域名证书，统一管理更方便；证书配置好后，Caddy 只需要加载即可；</li></ul><h2 id="密码导入和使用体验"><a href="#密码导入和使用体验" class="headerlink" title="密码导入和使用体验"></a>密码导入和使用体验</h2><p>Vaultwarden 兼容 Bitwarden 全套客户端，包括桌面应用、网页端和浏览器插件；首次访问你部署的网页服务后，注册一个账号并登录即可使用；</p><p>以下是浏览器插件界面截图：</p><center>   <img     src="https://picture.orangetime.top/i/2025/06/14/684c6e6229c19.png"  ></center><center>   <img     src="https://picture.orangetime.top/i/2025/06/14/684c6e815e796.png"  ></center><p>初次使用时，我直接将 Chrome 导出的 .csv 密码文件导入进去，整个流程没有踩坑，页面提示也很直观；导入成功后，插件会识别你常用的网站并提供填充密码功能，体验类似 Chrome 自带的密码管理，多了主密码保护等功能；</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>总的来说，Vaultwarden 是一个轻量、实用、易于自建的密码管理工具，适合追求数据自主和隐私保护的用户；虽然它在易用性和云同步体验上不如 1Password 或 Chrome 自带的密码管理器那么“顺手”，但它胜在部署自由、功能完整，尤其对于技术用户来说，掌控自己的数据就是最大的安心；</p><p>如果你已经有了自己的服务器资源（哪怕是树莓派），并具备基本的 Docker 和 HTTPS 配置能力，Vaultwarden 完全值得一试；配合 Caddy 做反向代理，搭配 Certd 管理泛域名证书，部署流程也没有想象中复杂；</p><p>也许它不够“傻瓜式”，但作为一名稍微动手能力强一点的用户，这种“折腾”背后的安全感和成就感，是任何云服务都给不了的；</p><p>如果你和我一样，厌倦了各种服务把账号密码握在手里，不妨给 Vaultwarden 一个机会；</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;参考：&lt;a class=&quot;link&quot;   href=&quot;https://ohdmire.github.io/posts/bitwarden&quot; &gt;https://ohdmire.github.io/posts/bitwarden&lt;i class=&quot;fas fa-external</summary>
      
    
    
    
    <category term="docker" scheme="http://example.com/categories/docker/"/>
    
    
  </entry>
  
  <entry>
    <title>Docker部署Gogs（个人GIthub）</title>
    <link href="http://example.com/2024/08/16/docker/Docker-Gogs/"/>
    <id>http://example.com/2024/08/16/docker/Docker-Gogs/</id>
    <published>2024-08-16T08:29:55.000Z</published>
    <updated>2024-08-19T14:40:08.000Z</updated>
    
    <content type="html"><![CDATA[<p>参考：</p><ul><li><a class="link"   href="https://blog.csdn.net/bbj12345678/article/details/122713883" >https://blog.csdn.net/bbj12345678/article/details/122713883<i class="fas fa-external-link-alt"></i></a></li><li><a class="link"   href="https://blog.csdn.net/yanzili/article/details/130615472" >https://blog.csdn.net/yanzili/article/details/130615472<i class="fas fa-external-link-alt"></i></a></li></ul><p>Gogs仓库：<a class="link"   href="https://github.com/gogs/gogs" >https://github.com/gogs/gogs<i class="fas fa-external-link-alt"></i></a></p><p>现在越来越多团队开始尝试搭建自己的 Git 服务，用于私有代码托管、版本控制以及团队协作；如果你觉得 GitLab 太重、Gitea 功能太多，那么 <strong>Gogs</strong> 这个轻量级的 Git 服务可能刚好适合你；</p><p>Gogs 的全名是「Go Git Service」，它由 Go 语言开发，体积小、部署快、占用资源极低，非常适合搭建在树莓派、低配机器或开发环境中自用；</p><p>这篇博客我就来分享下 <strong>如何用 Docker 部署 Gogs</strong>，整个流程简单高效，适合初学者和懒人党（比如我）参考；</p><h2 id="部署Gogs"><a href="#部署Gogs" class="headerlink" title="部署Gogs"></a>部署Gogs</h2><h3 id="拉取-Gogs-Docker-镜像"><a href="#拉取-Gogs-Docker-镜像" class="headerlink" title="拉取 Gogs Docker 镜像"></a>拉取 Gogs Docker 镜像</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker pull gogs/gogs</span><br></pre></td></tr></table></figure><p>Gogs 提供官方维护的镜像，持续更新，推荐使用；拉取速度可能因网络而异，可考虑配置镜像加速；</p><h3 id="创建挂载目录"><a href="#创建挂载目录" class="headerlink" title="创建挂载目录"></a>创建挂载目录</h3><p>为了数据持久化，我们提前创建挂载目录；这里以 &#x2F;home&#x2F;time&#x2F;docker_mount&#x2F;gogs_github 为例：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">mkdir</span> -p /home/time/docker_mount/gogs_github</span><br></pre></td></tr></table></figure><p>这个目录将映射到容器的 &#x2F;data 目录，Gogs 会把所有的配置、仓库文件、SSH 密钥等放在这里，重启、迁移都不影响；</p><h3 id="启动-Gogs-容器"><a href="#启动-Gogs-容器" class="headerlink" title="启动 Gogs 容器"></a>启动 Gogs 容器</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></pre></td><td class="code"><pre><span class="line">docker run -d \</span><br><span class="line">  --name my_github \</span><br><span class="line">  --restart=always \</span><br><span class="line">  -p 10022:22 \</span><br><span class="line">  -p 13000:3000 \</span><br><span class="line">  -v /home/time/docker_mount/gogs_github:/data \</span><br><span class="line">  gogs/gogs</span><br></pre></td></tr></table></figure><p>解释一下：</p><ul><li>-p 13000:3000：Web 界面访问端口；</li><li>-p 10022:22：Git SSH 克隆使用的端口；</li><li>-v：挂载持久化数据目录；</li><li>–name：容器名称为 my_github；</li></ul><h3 id="初始配置-Gogs"><a href="#初始配置-Gogs" class="headerlink" title="初始配置 Gogs"></a>初始配置 Gogs</h3><p>在浏览器中访问 http:&#x2F;&#x2F;运行服务的ip:13000，开始初始化设置；</p><p>配置项说明：</p><ul><li><strong>数据库类型</strong>：选择 SQLite3（默认即可）；</li><li><strong>应用名称</strong>：如 EMTime’s Private Repo，仅用于界面展示；</li><li><strong>SSH 端口</strong>：建议勾选“使用内置 SSH 服务器”；</li><li><strong>应用 URL</strong>：填写 http<span></span>:&#x2F;&#x2F;运行服务的ip:13000，注意不能加斜杠 &#x2F;；</li><li><strong>管理员账号</strong>：填写好用户名、密码和邮箱；</li><li><strong>点击“立即安装”</strong> 即可完成；</li></ul><blockquote><p>安装完成后请及时记下管理员账号密码，后续登录需要用到；</p></blockquote><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><span class="line">nano /home/time/docker_mount/gogs_github/gogs/conf/app.ini</span><br></pre></td></tr></table></figure><p>找到：</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[service]</span></span><br><span class="line"><span class="attr">DISABLE_REGISTRATION</span> = <span class="literal">false</span></span><br></pre></td></tr></table></figure><p>修改为：</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[service]</span></span><br><span class="line"><span class="attr">DISABLE_REGISTRATION</span> = <span class="literal">true</span></span><br></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><span class="line">docker restart my_github</span><br></pre></td></tr></table></figure><p>这样就只保留管理员创建用户的方式，更安全；</p><h3 id="使用自定义域名（可选）"><a href="#使用自定义域名（可选）" class="headerlink" title="使用自定义域名（可选）"></a>使用自定义域名（可选）</h3><p>比如你使用了反代（如 Caddy、Nginx、frp 等）并绑定了你的域名，就可以修改 Gogs 的显示地址：</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><span class="line">docker stop my_github</span><br><span class="line">vim /home/time/docker_mount/gogs_github/gogs/conf/app.ini</span><br></pre></td></tr></table></figure><p>找到：</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">APP_URL</span> = http://localhost:<span class="number">13000</span></span><br></pre></td></tr></table></figure><p>修改为：</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">APP_URL</span> = https://git.<span class="number">140105</span>.xyz</span><br></pre></td></tr></table></figure><p>这里用我自己的域名为例，注意不要加斜杠 &#x2F;；</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><span class="line">docker restart my_github</span><br></pre></td></tr></table></figure><h3 id="克隆仓库报-SSL-错误的解决办法（可选）"><a href="#克隆仓库报-SSL-错误的解决办法（可选）" class="headerlink" title="克隆仓库报 SSL 错误的解决办法（可选）"></a>克隆仓库报 SSL 错误的解决办法（可选）</h3><p>如果你的 HTTPS 使用的是自签名证书，Git 拉取可能提示 SSL 验证失败，可以关闭 SSL 验证（注意，仅建议在局域网或受信网络使用）：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git config --global http.sslVerify <span class="literal">false</span></span><br></pre></td></tr></table></figure><p>如果使用 Let’s Encrypt 的证书则无需担心，Git 会自动验证；</p><h2 id="为-Gogs-配置-SSH"><a href="#为-Gogs-配置-SSH" class="headerlink" title="为 Gogs 配置 SSH"></a>为 Gogs 配置 SSH</h2><p>在部署好 Gogs 之后，如果每次拉取和推送都输入账号密码，不仅麻烦还不安全；这时候我们可以通过 SSH 公钥认证 来实现更安全、无密码的 Git 操作；</p><p>这篇文章将一步步带你完成 Gogs 的 SSH 配置过程，适用于 Linux 和 Windows（使用 MobaXterm 等终端工具）环境；</p><h3 id="生成-SSH-密钥对"><a href="#生成-SSH-密钥对" class="headerlink" title="生成 SSH 密钥对"></a>生成 SSH 密钥对</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><span class="line">ssh-keygen -t rsa -C <span class="string">&quot;git@git.140105.xyz&quot;</span></span><br></pre></td></tr></table></figure><p>这里：</p><ul><li>-t rsa 表示使用 RSA 加密算法；</li><li>-C 根据你自己之前部署的情况进行修改，这里以我自己的域名为例；</li><li>默认会将密钥保存在 ~&#x2F;.ssh&#x2F;id_rsa，你也可以选择自定义，例如 id_rsa_gogs；</li></ul><p>Gogs 同样支持 ed25519 等更现代的算法，参考其界面提示或 GitHub 教程进行生成即可；</p><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><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">chmod</span> 600 ~/.ssh/id_rsa_gogs</span><br><span class="line"><span class="built_in">chmod</span> 644 ~/.ssh/id_rsa_gogs.pub</span><br></pre></td></tr></table></figure><h3 id="配置-SSH-连接"><a href="#配置-SSH-连接" class="headerlink" title="配置 SSH 连接"></a>配置 SSH 连接</h3><p>编辑 SSH 配置文件：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">nano ~/.ssh/config</span><br></pre></td></tr></table></figure><p>添加以下内容：</p><figure class="highlight ini"><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><span class="line">Host 随便取个名字</span><br><span class="line">    HostName git.140105.xyz</span><br><span class="line">    User git</span><br><span class="line">    Port 10022</span><br><span class="line">    IdentityFile ~/.ssh/id_rsa_gogs</span><br><span class="line">    PreferredAuthentications publickey</span><br></pre></td></tr></table></figure><p>解释一下这些配置：</p><ul><li>Host 是你之后在 git clone 命令中使用的别名；</li><li>HostName 是实际的服务器地址；</li><li>User 为固定的 git，Gogs 默认就是用这个账户名；</li><li>Port 是你容器映射出来的 SSH 端口（上文我们用的是 10022）；</li><li>IdentityFile 指向你刚刚生成的私钥；</li><li>PreferredAuthentications 指定只使用公钥认证；</li></ul><blockquote><p>如果你是在 Windows 使用 MobaXterm 或类似终端，~&#x2F;.ssh&#x2F;config 位置在 C:\Users&lt;用户名&gt;.ssh\config，内容相同；</p></blockquote><h3 id="添加-SSH-公钥到-Gogs"><a href="#添加-SSH-公钥到-Gogs" class="headerlink" title="添加 SSH 公钥到 Gogs"></a>添加 SSH 公钥到 Gogs</h3><p>登录 Gogs 网站 → 点击右上角头像 → 用户设置 → SSH 公钥 → 添加密钥；</p><ul><li>名称随意，例如 My laptop SSH key；</li><li>内容粘贴 id_rsa_gogs.pub 中的全部内容；</li></ul><p>这样 Gogs 就认识你的电脑了；</p><h3 id="测试-SSH-连接"><a href="#测试-SSH-连接" class="headerlink" title="测试 SSH 连接"></a>测试 SSH 连接</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><span class="line">ssh -T git@git.emtime.top -p 10022</span><br></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><span class="line">Hi EMTime! You<span class="string">&#x27;ve successfully authenticated, but Gogs does not provide shell access.</span></span><br></pre></td></tr></table></figure><p>表示你已经通过公钥成功认证了！</p><h3 id="克隆仓库的方式说明"><a href="#克隆仓库的方式说明" class="headerlink" title="克隆仓库的方式说明"></a>克隆仓库的方式说明</h3><p>使用 SSH 克隆仓库有两种方式：</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><span class="line"><span class="comment"># 推荐方式（更清晰直观）</span></span><br><span class="line">git <span class="built_in">clone</span> ssh://git@git.emtime.top:10022/EMTime/test.git</span><br><span class="line"></span><br><span class="line"><span class="comment"># 简写方式（Gogs 默认提供），这种写法虽然更短，但有些 Git 客户端或未配置 SSH 时可能会失败</span></span><br><span class="line">git@git.emtime.top:EMTime/test.git</span><br></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>部署一个轻量级的 Git 仓库服务其实非常简单，Gogs + Docker 就是最实用、门槛最低的选择之一：</p><ul><li>简单易用，不依赖外部数据库；</li><li>支持 SSH、Web、组织管理；</li><li>Docker 容器化部署，迁移方便；</li><li>可与 Caddy、Frp 等无缝结合，快速公网访问；</li></ul><p>非常适合用于自用项目托管、内网代码管理、小型团队开发协作等场景；</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;参考：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class=&quot;link&quot;   href=&quot;https://blog.csdn.net/bbj12345678/article/details/122713883&quot; &gt;https://blog.csdn.net/bbj12345678/</summary>
      
    
    
    
    <category term="docker" scheme="http://example.com/categories/docker/"/>
    
    
  </entry>
  
  <entry>
    <title>Docker运行OpenWRT</title>
    <link href="http://example.com/2024/07/25/docker/Docker-OpenWRT/"/>
    <id>http://example.com/2024/07/25/docker/Docker-OpenWRT/</id>
    <published>2024-07-25T10:26:51.000Z</published>
    <updated>2024-07-25T10:26:51.000Z</updated>
    
    <content type="html"><![CDATA[<p>在日常网络优化中，OpenWRT 作为强大灵活的软路由系统，非常适合作为旁路由运行；而使用 Docker 快速部署 OpenWRT，不仅便于管理，还能避免单独刷固件、物理接入的繁琐；本篇笔记记录了通过 Docker 安装并配置 OpenWRT 为旁路由的全过程；</p><blockquote><p>参考资料：<br><a class="link"   href="https://www.ilovn.com/2023/02/23/deploy-openwrt-with-docker/" >https://www.ilovn.com/2023/02/23/deploy-openwrt-with-docker/<i class="fas fa-external-link-alt"></i></a><br><a class="link"   href="https://openwrt.ai/docker%E7%89%88openwrt%E6%97%81%E8%B7%AF%E7%94%B1%E5%AE%89%E8%A3%85%E8%AE%BE%E7%BD%AE%E6%95%99%E7%A8%8B/" >https://openwrt.ai/docker%E7%89%88openwrt%E6%97%81%E8%B7%AF%E7%94%B1%E5%AE%89%E8%A3%85%E8%AE%BE%E7%BD%AE%E6%95%99%E7%A8%8B/<i class="fas fa-external-link-alt"></i></a></p></blockquote><h2 id="准备工作-开启网卡混杂模式"><a href="#准备工作-开启网卡混杂模式" class="headerlink" title="准备工作-开启网卡混杂模式"></a>准备工作-开启网卡混杂模式</h2><p>旁路由需要 Docker 网络能桥接到主机的物理网卡，因此必须开启混杂模式（Promiscuous Mode）；</p><p>使用 ifconfig 或 ip addr 查找你的有线网卡名，比如 enp1s0，然后执行：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> ip <span class="built_in">link</span> <span class="built_in">set</span> enp1s0 promisc on</span><br></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><span class="line">ip <span class="built_in">link</span> | grep PROMISC</span><br></pre></td></tr></table></figure><h2 id="创建-Macvlan-网络"><a href="#创建-Macvlan-网络" class="headerlink" title="创建 Macvlan 网络"></a>创建 Macvlan 网络</h2><p>创建一个 Macvlan 类型的网络，供 OpenWRT 容器使用；你可以根据自己的局域网网段修改 subnet 和 gateway 参数：</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><span class="line">docker network create -d macvlan \</span><br><span class="line">  --subnet=192.168.10.0/24 \</span><br><span class="line">  --gateway=192.168.10.1 \</span><br><span class="line">  -o parent=enp1s0 openwrt</span><br></pre></td></tr></table></figure><ul><li>–subnet: 指定 OpenWRT 所在的子网；</li><li>–gateway: 原有主路由地址；</li><li>-o parent: 指定物理网卡；</li><li>openwrt: 为网络命名；</li></ul><h2 id="获取并导入-OpenWRT-镜像"><a href="#获取并导入-OpenWRT-镜像" class="headerlink" title="获取并导入 OpenWRT 镜像"></a>获取并导入 OpenWRT 镜像</h2><p>从 <a class="link"   href="https://openwrt.ai/" >https://openwrt.ai<i class="fas fa-external-link-alt"></i></a> 下载适合自己设备架构的镜像，例如 x86_64 架构的 Generic 镜像；</p><p>下载后导入镜像，此命令会将镜像命名为 openwrt：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker import 文件名.tar.gz openwrt</span><br></pre></td></tr></table></figure><h2 id="运行-OpenWRT-容器"><a href="#运行-OpenWRT-容器" class="headerlink" title="运行 OpenWRT 容器"></a>运行 OpenWRT 容器</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><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">docker run -d \</span><br><span class="line">  --restart always \</span><br><span class="line">  --name openwrt \</span><br><span class="line">  --network openwrt --privileged=<span class="literal">true</span> \</span><br><span class="line">  openwrt /sbin/init</span><br></pre></td></tr></table></figure><h2 id="配置-OpenWRT-网络信息"><a href="#配置-OpenWRT-网络信息" class="headerlink" title="配置 OpenWRT 网络信息"></a>配置 OpenWRT 网络信息</h2><p>进入容器：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker <span class="built_in">exec</span> -it openwrt bash</span><br></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><span class="line">nano /etc/config/network</span><br></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><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><span class="line">config interface <span class="string">&#x27;lan&#x27;</span></span><br><span class="line">        option device <span class="string">&#x27;br-lan&#x27;</span></span><br><span class="line">        option proto <span class="string">&#x27;static&#x27;</span></span><br><span class="line">        option ipaddr <span class="string">&#x27;192.168.10.2&#x27;</span>     <span class="comment"># OpenWRT 地址</span></span><br><span class="line">        option netmask <span class="string">&#x27;255.255.255.0&#x27;</span></span><br><span class="line">        option gateway <span class="string">&#x27;192.168.10.1&#x27;</span>    <span class="comment"># 主路由地址</span></span><br><span class="line">        option peerdns <span class="string">&#x27;0&#x27;</span></span><br><span class="line">        list dns <span class="string">&#x27;223.5.5.5&#x27;</span></span><br></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><span class="line">/etc/init.d/network restart</span><br></pre></td></tr></table></figure><h2 id="进入-Web-管理页面"><a href="#进入-Web-管理页面" class="headerlink" title="进入 Web 管理页面"></a>进入 Web 管理页面</h2><p>此时你可以在浏览器访问 <a class="link"   href="http://192.168.10.2/" >http://192.168.10.2<i class="fas fa-external-link-alt"></i></a> 进入 OpenWRT 的 LuCI 管理界面，初始用户名密码均为 root；</p><p>进入系统后，可以通过“网络向导”配置旁路由模式，建议：</p><ul><li>IP 地址：192.168.10.2（与前文一致）</li><li>网关与 DNS：均填写主路由地址（如 192.168.10.1）</li></ul><h2 id="开启-DHCP-功能（可选）"><a href="#开启-DHCP-功能（可选）" class="headerlink" title="开启 DHCP 功能（可选）"></a>开启 DHCP 功能（可选）</h2><p>若希望 OpenWRT 自动分配 IP，则需要：</p><ul><li>关闭主路由的 DHCP 服务；</li><li>在 OpenWRT 中开启 DHCP 功能，并配置其 IP 分配范围；</li></ul><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>部署完成后，你的 OpenWRT 容器就具备完整的旁路由能力，可以实现流量分流、广告屏蔽等高级功能；同时也得益于 Docker 的管理便利，容器化的 OpenWRT 更便于升级、迁移和备份，推荐给动手能力强的朋友们试试；</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;在日常网络优化中，OpenWRT 作为强大灵活的软路由系统，非常适合作为旁路由运行；而使用 Docker 快速部署 OpenWRT，不仅便于管理，还能避免单独刷固件、物理接入的繁琐；本篇笔记记录了通过 Docker 安装并配置 OpenWRT 为旁路由的全过程；&lt;/p&gt;
&lt;</summary>
      
    
    
    
    <category term="docker" scheme="http://example.com/categories/docker/"/>
    
    
  </entry>
  
  <entry>
    <title>Basic OS：从任务定义到运行调度的全流程实践</title>
    <link href="http://example.com/2024/07/09/mcu/Basic-OS2/"/>
    <id>http://example.com/2024/07/09/mcu/Basic-OS2/</id>
    <published>2024-07-09T10:47:32.000Z</published>
    <updated>2024-07-09T10:47:32.000Z</updated>
    
    <content type="html"><![CDATA[<p>仓库：<a class="link"   href="https://gitee.com/event-os/basic-os" >https://gitee.com/event-os/basic-os<i class="fas fa-external-link-alt"></i></a></p><p>在上一篇中，我们从架构视角出发，了解了 Basic OS 的设计理念和核心机制：共享任务栈、协作式调度、基于 PendSV 的上下文切换……这些特性让它成为资源受限设备中，极具性价比的结构化选项；</p><p>但了解原理只是第一步；</p><p>真正的价值，来自它在实际项目中的表现；<strong>这篇文章，我们将从定义任务开始，带你完整走一遍 Basic OS 的应用流程</strong>：如何写任务？如何初始化调度器？任务之间如何协作与切换？</p><p>不需要复杂的配置，也无需外部依赖；你将看到的是一个纯净、高效、清晰可控的任务调度过程，真正让你从“裸机大 while(1)”走向结构化设计的实战落地；</p><h2 id="支持平台"><a href="#支持平台" class="headerlink" title="支持平台"></a>支持平台</h2><p>当前 Basic OS 默认只适配了 Cortex-M0 和 M3 内核（比如 STM32F103 或 F030），虽然不算通用，但架构非常清晰，完全可以参考其结构来适配其他内核或平台；如果你对汇编和异常向量表比较熟悉，实现并不困难；</p><h2 id="准备文件"><a href="#准备文件" class="headerlink" title="准备文件"></a>准备文件</h2><p>使用 Basic OS 时，最基本的工作就是把它的核心源码添加到你的工程中（文件可以去开头所说的仓库中下载）；至少需要以下几个文件：</p><table><thead><tr><th>文件名</th><th>说明</th></tr></thead><tbody><tr><td>basic_os.c</td><td>系统核心逻辑实现</td></tr><tr><td>basic_os.h</td><td>系统核心头文件</td></tr><tr><td>cpu.c</td><td>与平台无关的通用 CPU 支持</td></tr><tr><td>port_mdk.s</td><td>与平台有关的汇编端口（不同平台需对应更换）</td></tr></tbody></table><p>此外，还需要你自己实现部分<strong>平台相关的钩子函数、SysTick中断</strong>等逻辑，官方示例中以 cb_basic_os.c 命名此文件；你也可以照此命名，并加入自己的实现：</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></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;basic_os.h&quot;</span></span></span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">bos_port_assert</span><span class="params">(<span class="type">uint32_t</span> error_id)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">while</span> (<span class="number">1</span>)</span><br><span class="line">    &#123;</span><br><span class="line">        bos_delay_ms(<span class="number">10</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">uint32_t</span> count_idle = <span class="number">0</span>;</span><br><span class="line"><span class="type">void</span> <span class="title function_">bos_hook_idle</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">    count_idle++;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">bos_hook_start</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="comment">// 系统调度器启动后的钩子函数</span></span><br><span class="line">    <span class="comment">// 可选实现，用于调试或初始化用户资源</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">SysTick_Handler</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">    bos_tick(); <span class="comment">// 系统时基更新</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p> <strong>特别注意（针对 STM32 使用 CubeMX 的情况）</strong><br>CubeMX 默认会自动生成 SysTick_Handler 和 PendSV_Handler，而这两个函数已由 Basic OS 实现；请务必在 <strong>NVIC 配置页面 → Code Generation 设置中取消勾选它们的生成</strong>，避免函数重定义冲突；</p></blockquote><center>   <img     src="https://picture.orangetime.top/i/2025/06/14/684d873056671.png"  ></center><h2 id="初始化-Basic-OS"><a href="#初始化-Basic-OS" class="headerlink" title="初始化 Basic OS"></a>初始化 Basic OS</h2><p>在你的 main.c 中，初始化 Basic OS 的代码结构非常简单：</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></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;basic_os.h&quot;</span></span></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">static</span> <span class="type">uint8_t</span> <span class="built_in">stack</span>[<span class="number">4096</span>]; <span class="comment">// 为共享栈分配空间</span></span><br><span class="line"></span><br><span class="line">    bsp_init();                 <span class="comment">// 板级初始化（LED/GPIO/UART等）</span></span><br><span class="line">    basic_os_init(<span class="built_in">stack</span>, <span class="number">4096</span>); <span class="comment">// 初始化调度器</span></span><br><span class="line"></span><br><span class="line">    basic_os_run();            <span class="comment">// 启动调度器（不会返回）</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">while</span> (<span class="number">1</span>)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="comment">// 实际上，这一行代码永远不会被执行到</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>初始化的关键点是提供一段连续 RAM 区域作为共享任务栈，这里用了 4096 字节，具体大小依据任务栈深度和应用复杂度可调整；</p><blockquote><p>注意：栈区必须是静态分配（static&#x2F;global），不能是局部变量或动态分配，否则可能在函数退出后被破坏，导致调度器异常崩溃；</p></blockquote><h2 id="定义任务：结构化的多任务运行方式"><a href="#定义任务：结构化的多任务运行方式" class="headerlink" title="定义任务：结构化的多任务运行方式"></a>定义任务：结构化的多任务运行方式</h2><p>在 Basic OS 中，每个任务就是<strong>一个函数 + 一个注册宏</strong>；定义任务的方式非常直观，适合嵌入式初学者理解；</p><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></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;basic_os.h&quot;</span></span></span><br><span class="line"></span><br><span class="line"><span class="type">uint32_t</span> count_poll1 = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> _func_poll_1(<span class="type">void</span> *parameter)</span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">while</span> (<span class="number">1</span>)</span><br><span class="line">    &#123;</span><br><span class="line">        count_poll1++;</span><br><span class="line">        bos_delay_ms(<span class="number">10</span>); <span class="comment">// 每 10ms 执行一次</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">bos_task_export(poll1, _func_poll_1, <span class="number">2</span>, <span class="literal">NULL</span>);</span><br></pre></td></tr></table></figure><p>bos_task_export参数：</p><ul><li>第一个参数：任务名称（可用于调试）</li><li>第二个参数：函数指针（真正的任务执行体）</li><li>第三个参数：任务优先级（同优先级按注册顺序轮转）</li><li>第四个参数：任务参数（通常传 NULL）</li></ul><blockquote><p>bos_task_export() 本质上是通过宏将任务描述信息放入 .bos_tasks 特定段，系统初始化时会自动扫描这段数据进行任务注册；所以bos_task_export()并不需要被调用，只需要在代码中这样导出（注册）任务即可；<br>Basic OS 是协作式调度器，不支持抢占式调度；<strong>只有当当前任务主动 yield 或 delay 时，调度器才会切换到其他任务</strong>；优先级高的任务在有多个可运行任务时会优先执行，但<strong>不会打断正在运行的任务</strong>；</p></blockquote><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></pre></td><td class="code"><pre><span class="line"><span class="type">void</span> _func_poll_2(<span class="type">void</span> *parameter) &#123; <span class="keyword">while</span> (<span class="number">1</span>) &#123; count_poll2++; bos_task_yield(); &#125; &#125;</span><br><span class="line"><span class="type">void</span> _func_poll_3(<span class="type">void</span> *parameter) &#123; <span class="keyword">while</span> (<span class="number">1</span>) &#123; count_poll3++; bos_task_yield(); &#125; &#125;</span><br><span class="line"><span class="type">void</span> _func_poll_4(<span class="type">void</span> *parameter) &#123; <span class="keyword">while</span> (<span class="number">1</span>) &#123; count_poll4++; bos_task_yield(); &#125; &#125;</span><br><span class="line"><span class="type">void</span> _func_poll_5(<span class="type">void</span> *parameter) &#123; <span class="keyword">while</span> (<span class="number">1</span>) &#123; count_poll5++; bos_task_yield(); &#125; &#125;</span><br><span class="line"><span class="type">void</span> _func_poll_6(<span class="type">void</span> *parameter) &#123; <span class="keyword">while</span> (<span class="number">1</span>) &#123; count_poll6++; bos_delay_ms(<span class="number">10</span>); &#125; &#125;</span><br><span class="line"></span><br><span class="line">bos_task_export(poll2, _func_poll_2, <span class="number">2</span>, <span class="literal">NULL</span>);</span><br><span class="line">bos_task_export(poll3, _func_poll_3, <span class="number">2</span>, <span class="literal">NULL</span>);</span><br><span class="line">bos_task_export(poll4, _func_poll_4, <span class="number">2</span>, <span class="literal">NULL</span>);</span><br><span class="line">bos_task_export(poll5, _func_poll_5, <span class="number">2</span>, <span class="literal">NULL</span>);</span><br><span class="line">bos_task_export(poll6, _func_poll_6, <span class="number">2</span>, <span class="literal">NULL</span>);</span><br></pre></td></tr></table></figure><p>你还可以添加一些控制硬件的任务，例如通过 GPIO 控制 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></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;gpio_led.h&quot;</span></span></span><br><span class="line"></span><br><span class="line"><span class="type">void</span> _func_led1(<span class="type">void</span> *parameter)</span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">while</span> (<span class="number">1</span>)</span><br><span class="line">    &#123;</span><br><span class="line">        LED1_ON();</span><br><span class="line">        bos_delay_ms(<span class="number">500</span>);</span><br><span class="line">        LED1_OFF();</span><br><span class="line">        bos_delay_ms(<span class="number">500</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">bos_task_export(task_led1, _func_led1, <span class="number">2</span>, <span class="literal">NULL</span>);</span><br></pre></td></tr></table></figure><h2 id="软件定时器"><a href="#软件定时器" class="headerlink" title="软件定时器"></a>软件定时器</h2><p>在某些实际场景中，仅靠 bos_delay_ms() 实现任务延时是远远不够的；比如，我们希望任务可以“预约”一段时间后执行一次，或中途暂停、恢复某个定时行为，这时就需要更强大的软件定时器机制；</p><p>Basic OS 内置了轻量级的软件定时器接口，能够提供灵活、可控的定时事件触发逻辑，避免我们手动维护繁琐的计数器状态；</p><p>这一部分的使用非常简单，核心只需要掌握以下 5 个函数：</p><ul><li>bos_timer_export(_name, _func, _oneshoot, _para)：导出（注册）一个定时器<ul><li>_name：定时器名称</li><li>_func：定时器回调函数</li><li>_oneshoot：是否是一次性定时器，true 表示是一次性定时器，false 表示是周期性定时器</li><li>_para：定时器回调函数的参数</li></ul></li></ul><p>相当于给调度器声明“我有这么一个定时器”，必须在使用前调用一次，推荐在 main() 的初始化部分完成；</p><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></pre></td><td class="code"><pre><span class="line"><span class="type">void</span> _func_poll_7(<span class="type">void</span> *para);</span><br><span class="line"></span><br><span class="line">bos_timer_export(my_timer, _cb_timer_tick, <span class="literal">false</span>, <span class="literal">NULL</span>);</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> _func_poll_7(<span class="type">void</span> *para)</span><br><span class="line">&#123;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li><p>int16_t bos_timer_get_id(const char *name)：从软件定时器名称获取 ID，返回定时器 ID 或者错误代码</p></li><li><p>void bos_timer_start(uint16_t timer_id, uint32_t period)：启动一个定时器，period 单位是毫秒，可以和get_id配合使用</p></li><li><p>void bos_timer_stop(uint16_t timer_id)：停止一个定时器，可以和get_id配合使用</p></li><li><p>void bos_timer_reset(uint16_t timer_id, uint32_t period)：重置一个定时器，可以和get_id配合使用</p></li></ul><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>Basic OS 虽然定位轻量，但其设计并不简陋；从任务注册、共享栈调度，到钩子机制与软件定时器，它具备了一个结构化操作系统的核心要素；本文从创建工程出发，完整梳理了 Basic OS 的使用流程，让我们真正从裸机的 while(1) 循环中走出，迈入结构化的多任务世界；</p><p>当然，Basic OS 并非万能，它不支持抢占式调度，也没有复杂的同步机制，但也正是这种“克制”，让它在资源受限的嵌入式设备中成为一种高性价比的选择；</p><p>如果你正在开发一个不复杂但希望清晰可维护的项目，Basic OS 值得一试；下一步，我们可以探索它与外设协同、低功耗运行、任务通信等更高级的实战应用；</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;仓库：&lt;a class=&quot;link&quot;   href=&quot;https://gitee.com/event-os/basic-os&quot; &gt;https://gitee.com/event-os/basic-os&lt;i class=&quot;fas fa-external-link-alt&quot;&gt;&lt;</summary>
      
    
    
    
    <category term="mcu" scheme="http://example.com/categories/mcu/"/>
    
    
  </entry>
  
  <entry>
    <title>Basic OS：从裸机到结构化的第一步</title>
    <link href="http://example.com/2024/07/09/mcu/Basic-OS1/"/>
    <id>http://example.com/2024/07/09/mcu/Basic-OS1/</id>
    <published>2024-07-09T09:31:41.000Z</published>
    <updated>2024-07-09T09:31:41.000Z</updated>
    
    <content type="html"><![CDATA[<p>仓库：<a class="link"   href="https://gitee.com/event-os/basic-os" >https://gitee.com/event-os/basic-os<i class="fas fa-external-link-alt"></i></a></p><p>在嵌入式开发中，操作系统往往是被绕开的“高级选项”；</p><p>对很多开发者而言，裸机开发已经足够：主循环、定时中断、状态机……能跑、够用、成本低；然而，项目一旦变得复杂，系统结构很容易变得混乱不堪，任务交叉、逻辑耦合、维护困难，最终演变为“能改的都不敢动”；</p><p>这时候，一套<strong>结构清晰、运行可靠、资源占用极低</strong>的轻量操作系统就显得尤为重要；</p><p><strong>Basic OS</strong> 正是为此而生；它是一个基于事件驱动模型设计的微型嵌入式操作系统，拥有清晰的调度结构、极低的内存开销，以及良好的可读性与扩展性；哪怕在 M0 上，也能运行得游刃有余；</p><p>不追求复杂的线程抢占、不依赖高级硬件特性，Basic OS 关注的是对小型系统结构化管理的一种务实方案；</p><p>本篇文章将从整体架构出发，带你一步步了解 Basic OS 的设计理念、核心机制以及使用方法，帮助你在裸机开发和重量级 RTOS 之间，找到一个刚刚好的选择；</p><blockquote><p>顺带一提，作者还有一个更完整的项目叫 <strong>Event OS</strong>，之后如果用到了，我也会写成笔记记录下来；</p></blockquote><h2 id="设计理念"><a href="#设计理念" class="headerlink" title="设计理念"></a>设计理念</h2><h3 id="为小RAM-MCU而生"><a href="#为小RAM-MCU而生" class="headerlink" title="为小RAM MCU而生"></a>为小RAM MCU而生</h3><p>BasicOS 的设计出发点非常明确：<strong>针对 RAM 紧张的嵌入式系统（例如仅 8KB 的 Cortex-M0&#x2F;M3）</strong>；<br>传统 RTOS 在每个任务上都要分配独立栈空间，这在小资源芯片上是奢侈的；而 BasicOS 采用 <strong>“共享任务栈”</strong> 的方式，实现了任务切换与栈空间复用的兼顾，极大地压缩了内存开销，为低成本 MCU 系统量身定制；</p><blockquote><p>目标：让 RTOS 能在资源极度受限的 MCU 上运行自如；</p></blockquote><h3 id="极简协作式内核，不抢占也高效"><a href="#极简协作式内核，不抢占也高效" class="headerlink" title="极简协作式内核，不抢占也高效"></a>极简协作式内核，不抢占也高效</h3><p>BasicOS 明确定位为 <strong>协作式内核</strong>，不使用抢占调度，任务切换完全由用户控制；</p><p>协作式内核的优势在于：</p><ul><li>无隐式中断上下文切换</li><li>更低的调试成本</li><li>避免共享资源竞争</li><li>执行流程更加可预测、逻辑更清晰</li></ul><p>BasicOS 的设计理念认为：<strong>大多数嵌入式产品并不需要“毫秒级以下”的硬实时</strong>；对于 10ms 级别的任务响应，协作式足够胜任，而且更加稳定、易于维护；</p><h3 id="共享栈是技术核心与突破"><a href="#共享栈是技术核心与突破" class="headerlink" title="共享栈是技术核心与突破"></a>共享栈是技术核心与突破</h3><p>BasicOS 最引人注目的技术，是它引入了在 32 位 ARM 芯片中非常罕见的<strong>任务共享栈机制</strong>；</p><p>每次任务切换时，系统会将前一个任务的栈数据压缩保存，当前任务的栈重新展开；这种机制虽然带来一些上下文切换开销，但换来的，是大幅度降低的 RAM 消耗：</p><ul><li>每个任务只占用 16 字节元数据</li><li>共享同一个运行栈</li><li>显著提高 RAM 利用率</li></ul><blockquote><p>这是 BasicOS 区别于其他 RTOS 的最大技术特征和未来持续发展的主轴；</p></blockquote><h3 id="轻学习曲线，亲民易用"><a href="#轻学习曲线，亲民易用" class="headerlink" title="轻学习曲线，亲民易用"></a>轻学习曲线，亲民易用</h3><p>相比更先进但入门门槛更高的 EventOS Nano（引入面向对象、事件驱动、控制反转等概念），BasicOS 明显更具普适性：</p><ul><li>C语言风格直观</li><li>简单 API（例如 bos_delay_ms、bos_task_export）</li><li>明确的任务结构</li><li>清晰的调度逻辑</li></ul><p>这降低了嵌入式初学者使用 RTOS 的门槛，也便于项目快速落地；</p><h2 id="核心机制"><a href="#核心机制" class="headerlink" title="核心机制"></a>核心机制</h2><h3 id="PendSV-Handler"><a href="#PendSV-Handler" class="headerlink" title="PendSV_Handler"></a>PendSV_Handler</h3><p>这是 Cortex-M 处理器专用于 RTOS 上下文切换的异常向量，BasicOS 也采用它来完成任务切换；</p><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><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><span class="line">flowchart TD</span><br><span class="line">    A[PendSV_Handler触发] --&gt; B&#123;bos_current为NULL？&#125;</span><br><span class="line">    B -- 是 --&gt; E[跳过保存，直接恢复新任务]</span><br><span class="line">    B -- 否 --&gt; C[保存当前任务上下文&lt;br/&gt;压栈r4-r11]</span><br><span class="line">    C --&gt; D[继续切换流程]</span><br><span class="line">    E --&gt; D</span><br><span class="line">    D --&gt; F&#123;是否需要栈迁移？&#125;</span><br><span class="line">    F -- 否 --&gt; H[跳转恢复下一个任务]</span><br><span class="line">    F -- 是 --&gt; G[进行内存块复制（栈迁移）]</span><br><span class="line">    G --&gt; H</span><br><span class="line">    H --&gt; I[从 bos_next 恢复 SP]</span><br><span class="line">    I --&gt; J[设置 bos_current = bos_next]</span><br><span class="line">    J --&gt; K[弹出 r4-r11，恢复上下文]</span><br><span class="line">    K --&gt; L[返回下一个任务继续执行]</span><br></pre></td></tr></table></figure><blockquote><p>在 BasicOS 中，任务主动让出 CPU（例如调用 bos_yield()）会触发 PendSV 异常，从而进入上下文切换流程；</p></blockquote><h3 id="关键变量作用说明（从-s-文件可见）："><a href="#关键变量作用说明（从-s-文件可见）：" class="headerlink" title="关键变量作用说明（从 .s 文件可见）："></a>关键变量作用说明（从 .s 文件可见）：</h3><table><thead><tr><th>变量名</th><th>说明</th></tr></thead><tbody><tr><td>bos_current</td><td>当前运行任务的控制块指针</td></tr><tr><td>bos_next</td><td>准备运行的下一个任务的控制块指针</td></tr><tr><td>addr_source</td><td>当前任务栈需要迁移的数据源地址</td></tr><tr><td>addr_target</td><td>下一个任务栈的目标地址（即 SP 指针新位置）</td></tr><tr><td>copy_size</td><td>栈数据迁移的大小（以字节计）</td></tr></tbody></table><h3 id="任务上下文保存"><a href="#任务上下文保存" class="headerlink" title="任务上下文保存"></a>任务上下文保存</h3><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><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">PUSH &#123;r4-r7&#125;       ; 先保存低寄存器</span><br><span class="line">MOV  r4, r8</span><br><span class="line">MOV  r5, r9</span><br><span class="line">MOV  r6, r10</span><br><span class="line">MOV  r7, r11</span><br><span class="line">PUSH &#123;r4-r7&#125;       ; 再保存高寄存器</span><br></pre></td></tr></table></figure><p>保存的寄存器集合为：r4-r11，这是 ARM Cortex-M 在 RTOS 中典型的 <strong>callee-saved 寄存器集合</strong>；</p><h3 id="恢复任务上下文"><a href="#恢复任务上下文" class="headerlink" title="恢复任务上下文"></a>恢复任务上下文</h3><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><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">POP &#123;r4-r7&#125;</span><br><span class="line">MOV r8, r4</span><br><span class="line">MOV r9, r5</span><br><span class="line">MOV r10, r6</span><br><span class="line">MOV r11, r7</span><br><span class="line">POP &#123;r4-r7&#125;</span><br></pre></td></tr></table></figure><h3 id="共享栈的实现：运行时栈数据拷贝机制"><a href="#共享栈的实现：运行时栈数据拷贝机制" class="headerlink" title="共享栈的实现：运行时栈数据拷贝机制"></a>共享栈的实现：运行时栈数据拷贝机制</h3><p><strong>这是 BasicOS 与普通 RTOS 最大的不同</strong>：</p><ul><li>普通 RTOS 做法：每个任务独立分配一段栈空间，切换时只需切换 SP；</li><li>BasicOS 做法：所有任务共用一段物理栈空间，切换时必须将 上一个任务的栈数据搬走，并将下一个任务的栈内容 搬进当前栈位置；</li></ul><p><strong>实现方式：</strong></p><ul><li>判断 addr_target 和 addr_source 的关系，决定复制方向（前向或逆向）</li><li>以 word（4字节）为单位，逐字节移动任务的完整栈内容</li><li>Loop2_P：前向复制（正序）</li><li>Loop2_N：逆向复制（反向，避免内存重叠）</li></ul><h3 id="关键-API-实现简析"><a href="#关键-API-实现简析" class="headerlink" title="关键 API 实现简析"></a>关键 API 实现简析</h3><p>bos_critical_enter &#x2F; bos_critical_exit：用于进入&#x2F;退出临界区，避免任务切换导致的数据竞争；</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><span class="line">CPSID I  ; 禁用中断，进入临界区</span><br><span class="line">CPSIE I  ; 开启中断，退出临界区</span><br></pre></td></tr></table></figure><p>协作式内核也需要临界保护，防止任务间修改共享数据时出现竞态；</p><p>get_sp_value：获取当前任务的栈指针，可用于调试或任务切换前的状态保存；</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">MOV r0, sp ; 读取当前任务的栈顶</span><br></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>从裸机开发出发，BasicOS 提供了一种极具实用性的**“中间形态”**：它没有传统 RTOS 的复杂调度和庞大资源开销，也不再依赖“主循环 + 中断”这种高度耦合的代码结构，而是通过共享栈协作式调度的核心机制，在资源受限的 Cortex-M0&#x2F;M3 等平台上，达到了结构清晰、可维护性强、资源使用极小的理想状态；</p><p>它的设计哲学并不激进，但足够务实：以极简的结构解决系统管理问题，以极低的门槛带来结构化开发的好处，尤其适合那些希望从裸机过渡到更系统性开发方式的项目；</p><p>如果说 FreeRTOS 等RTOS更像是一座技术体系完备的高楼大厦，那 BasicOS 则是打下基础的一块块砖石；它让我们意识到：结构化的好，不一定非要以“重量”为代价；</p><p>下一篇，我们将通过一个具体的案例，看看如何在 BasicOS 上构建一个由多个任务组成的嵌入式应用系统，真正迈出从“裸机跑代码”到“结构清晰可控”的第一步；</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;仓库：&lt;a class=&quot;link&quot;   href=&quot;https://gitee.com/event-os/basic-os&quot; &gt;https://gitee.com/event-os/basic-os&lt;i class=&quot;fas fa-external-link-alt&quot;&gt;&lt;</summary>
      
    
    
    
    <category term="mcu" scheme="http://example.com/categories/mcu/"/>
    
    
  </entry>
  
  <entry>
    <title>Docker容器访问N系显卡</title>
    <link href="http://example.com/2024/06/13/docker/Docker-Nvidia/"/>
    <id>http://example.com/2024/06/13/docker/Docker-Nvidia/</id>
    <published>2024-06-13T12:32:26.000Z</published>
    <updated>2024-06-13T12:32:26.000Z</updated>
    
    <content type="html"><![CDATA[<p>参考文章：<a class="link"   href="https://www.cnblogs.com/linhaifeng/p/16108285.html" >https://www.cnblogs.com/linhaifeng/p/16108285.html<i class="fas fa-external-link-alt"></i></a></p><p>在进行深度学习、图像处理或视频编解码等计算密集型任务时，GPU 的加速能力几乎是刚需；而如果我们选择使用 Docker 部署这些应用，让容器能够直接访问宿主机的 GPU 就变得非常关键；</p><p>相比直接在物理机或虚拟环境中运行程序，通过 Docker 使用宿主机 GPU 有以下几大优势：</p><ul><li>环境隔离：每个容器都可以拥有独立的依赖、驱动版本和运行环境，不怕“装坏”系统；</li><li>更灵活的部署：GPU 资源可以按需挂载到不同容器，便于多项目并行运行；</li><li>启动快、迁移轻：容器镜像打包后，可快速迁移到其他支持 GPU 的机器上部署；</li><li>配合 nvidia-docker 使用更丝滑：NVIDIA 官方提供的 nvidia-container-toolkit 工具链，支持零配置挂载 GPU 驱动和 CUDA 库；</li><li>可通过 Docker 的参数控制容器能使用哪一块 GPU，避免资源争用；</li></ul><p>本篇文章将介绍如何让 Docker 容器访问宿主机的 GPU，并实际运行一个简单的测试案例验证 GPU 是否成功启用；</p><h2 id="安装-NVIDIA-Container-Toolkit"><a href="#安装-NVIDIA-Container-Toolkit" class="headerlink" title="安装 NVIDIA Container Toolkit"></a>安装 NVIDIA Container Toolkit</h2><p>如果你希望 Docker 容器也能像宿主机一样使用 GPU 进行加速计算，那么安装 NVIDIA 官方提供的 <a class="link"   href="https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html" >NVIDIA Container Toolkit<i class="fas fa-external-link-alt"></i></a> 是关键的一步；</p><p>这个工具链可以帮助我们将宿主机的 GPU 驱动和 CUDA 库映射进容器内部，使得容器可以像“原生运行”一样调用 GPU 资源；</p><p>首先，根据你的 Linux 系统版本添加 NVIDIA 的软件源并安装工具包：</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><span class="line">distribution=$(. /etc/os-release;<span class="built_in">echo</span> $ID<span class="variable">$VERSION_ID</span>)</span><br><span class="line">curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | <span class="built_in">sudo</span> apt-key add -</span><br><span class="line">curl -s -L https://nvidia.github.io/nvidia-docker/<span class="variable">$distribution</span>/nvidia-docker.list | <span class="built_in">sudo</span> <span class="built_in">tee</span> /etc/apt/sources.list.d/nvidia-docker.list</span><br><span class="line"></span><br><span class="line"><span class="built_in">sudo</span> apt update &amp;&amp; <span class="built_in">sudo</span> apt install -y nvidia-container-toolkit</span><br><span class="line"><span class="built_in">sudo</span> systemctl restart docker</span><br></pre></td></tr></table></figure><p>安装完成后，nvidia-container-toolkit 会自动配置 Docker，使其支持 –gpus 参数，用于将 GPU 挂载到容器中；</p><h2 id="验证安装是否成功"><a href="#验证安装是否成功" class="headerlink" title="验证安装是否成功"></a>验证安装是否成功</h2><p>要验证安装是否成功，可以通过以下命令检查 Docker 是否已识别 –gpus 参数支持：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker run --<span class="built_in">help</span> | grep -i gpus</span><br></pre></td></tr></table></figure><p>如果能看到类似 –gpus 的输出，说明 Toolkit 已成功生效；</p><p>你还可以运行 NVIDIA 提供的 CUDA 基准容器来确认 GPU 是否真的可用：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker run --<span class="built_in">rm</span> --gpus all nvidia/cuda:12.4.1-base-ubuntu22.04 nvidia-smi</span><br></pre></td></tr></table></figure><p>输出应该类似于宿主机运行 nvidia-smi 的结果，显示当前 GPU 的使用状态、驱动版本、CUDA 版本等信息；</p><h2 id="启动一个带-GPU-的容器"><a href="#启动一个带-GPU-的容器" class="headerlink" title="启动一个带 GPU 的容器"></a>启动一个带 GPU 的容器</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker run --gpus all -itd --name 容器名 镜像名 bash --privileged=<span class="literal">true</span></span><br></pre></td></tr></table></figure><ul><li>–gpus all：将所有可用的 GPU 设备挂载到容器中；</li><li>-itd：交互式后台运行容器；</li><li>–name my-gpu-container：指定容器名称；</li><li>–privileged&#x3D;true：容器具有更高权限（某些情况下处理硬件设备更稳定）；</li></ul><p>如果你只想绑定特定编号的 GPU（比如第 0 和第 2 块），也可以这样写：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker run --gpus <span class="string">&#x27;&quot;device=0,2&quot;&#x27;</span> ...</span><br></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>总的来说，通过 NVIDIA Container Toolkit 和 –gpus 参数，Docker 容器可以在保持环境隔离的同时，毫无障碍地访问宿主机的 GPU，这为部署 AI 模型训练、视频转码、科学计算等任务提供了极大的便利；</p><p>接下来，你可以尝试基于 GPU 的 Python 脚本、TensorFlow、PyTorch 或自定义镜像，在 Docker 容器中真正跑起来；</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;参考文章：&lt;a class=&quot;link&quot;   href=&quot;https://www.cnblogs.com/linhaifeng/p/16108285.html&quot; &gt;https://www.cnblogs.com/linhaifeng/p/16108285.html&lt;i cl</summary>
      
    
    
    
    <category term="docker" scheme="http://example.com/categories/docker/"/>
    
    
  </entry>
  
  <entry>
    <title>STM32MP157 移植笔记（一）：U-Boot 编译与启动调试</title>
    <link href="http://example.com/2024/04/26/linux-embedded/STM32MP157-1/"/>
    <id>http://example.com/2024/04/26/linux-embedded/STM32MP157-1/</id>
    <published>2024-04-26T14:36:11.000Z</published>
    <updated>2024-04-26T14:36:11.000Z</updated>
    
    <content type="html"><![CDATA[<p>近年来，<strong>异构多核 SoC</strong> 在嵌入式领域越来越常见，其中 STM32MP157 凭借其** Cortex-A7 + Cortex-M4 双核架构** 和良好的开源生态，成为许多开发者进行 Linux 系统移植和定制的理想平台；</p><p>相比传统的裸机 MCU 开发，STM32MP157 的启动流程更为复杂，涉及 <strong>BootROM → FSBL（如 TF-A）→ U-Boot → 内核 → 根文件系统</strong> 多个阶段，每一步都可能出现问题，每一步也都值得理解与掌控；</p><p>本系列文章将以实战为核心，记录我在 STM32MP157 平台上进行完整 Linux 移植的过程：包括 <strong>U-Boot 编译与调试、设备树配置、内核裁剪与驱动适配、根文件系统构建与挂载</strong> 等等；本篇作为第一篇，将重点放在 <strong>U-Boot 的获取、配置、编译、启动与串口调试</strong>，为后续系统启动打下基础；</p><p>本系列内容主要参考自 <strong>华清远见的嵌入式 Linux 教程</strong>，并结合我自己的实操经验进行补充、优化和归纳，力求贴近真实开发场景，便于读者实践与扩展；</p><p>如果你也在迈入 STM32MP1 系列的世界，希望本博客能帮你少走一些弯路，快速搭建起属于自己的 Linux 开发环境；</p><h2 id="环境准备"><a href="#环境准备" class="headerlink" title="环境准备"></a>环境准备</h2><p>本次移植所使用的环境基于 <strong>Ubuntu 18.04 LTS</strong>，配套的是<strong>较旧版本的 SDK 与源码（20 年）</strong>，即便版本偏旧，但在 STM32MP1 系列开发中仍具代表性，适合入门与学习；</p><blockquote><p>本文采用 Docker 容器方式搭建开发环境（Ubuntu 18.04 LTS），优点是可快速部署、便于还原；如果你不熟悉 Docker，也可以选择直接在虚拟机或实体机中安装 Ubuntu 18.04 环境；<br>容器采用 bind 挂载本地 SDK 和源码目录，便于调试与编译；建议通过 –privileged 启动容器，并开启 &#x2F;dev、&#x2F;proc 等挂载；</p></blockquote><p>在进行 U-Boot 编译之前，请先确保以下必要工具链已安装：</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><span class="line"><span class="built_in">sudo</span> apt update</span><br><span class="line"><span class="built_in">sudo</span> apt install -y file tar xz-utils findutils patch make gcc bc libssl-dev g++ cpio unzip device-tree-compiler python3 nano</span><br></pre></td></tr></table></figure><h2 id="TF-卡分区准备"><a href="#TF-卡分区准备" class="headerlink" title="TF 卡分区准备"></a>TF 卡分区准备</h2><p>为实现 TF 卡启动，我们需要手动对 TF 卡进行分区和格式化，确保各阶段引导程序能被正确识别与加载；</p><ul><li>清除原有分区表并建立新的 MSDOS 分区表：</li></ul><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> parted -s /dev/sdX mklabel msdos</span><br></pre></td></tr></table></figure><ul><li>使用 sgdisk 对 TF 卡重建分区表（含启动及根文件系统）：</li></ul><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><span class="line"><span class="built_in">sudo</span> sgdisk --resize-table=128 -a 1 \</span><br><span class="line">-n 1:34:545     -c 1:fsbl1 \</span><br><span class="line">-n 2:546:1057   -c 2:fsbl2 \</span><br><span class="line">-n 3:1058:5153  -c 3:ssbl \</span><br><span class="line">-n 4:5154:136225 -c 4:bootfs \</span><br><span class="line">-n 5:136226     -c 5:rootfs \</span><br><span class="line">-A 4:<span class="built_in">set</span>:2 -p /dev/sdX -g</span><br></pre></td></tr></table></figure><p>执行成功如下所示：</p><center>   <img     src="https://picture.orangetime.top/i/2025/06/20/68552dadccc1f.png"  ></center><blockquote><p>请将 &#x2F;dev&#x2F;sdX 替换为实际的 TF 卡设备名，如 &#x2F;dev&#x2F;sdb，确保避免误操作破坏其他磁盘数据；</p></blockquote><h2 id="SDK-与源码获取"><a href="#SDK-与源码获取" class="headerlink" title="SDK 与源码获取"></a>SDK 与源码获取</h2><p>链接: <a class="link"   href="https://pan.baidu.com/s/1PS7VjeD9nnNQvaiX0QkjDA?pwd=5mw4" >https://pan.baidu.com/s/1PS7VjeD9nnNQvaiX0QkjDA?pwd=5mw4<i class="fas fa-external-link-alt"></i></a> 提取码: 5mw4 </p><p>文件为 tar.xz 格式，请使用 tar -xvf 命令解压，解压之后，可进入 stm32mp1-openstlinux-5.4-dunfell-mp1-20-06-24 查看文件，其中 sdk 就是 SDK 安装文件所在文件夹，source 就是源码所在文件夹；</p><h3 id="SDK-安装"><a href="#SDK-安装" class="headerlink" title="SDK 安装"></a>SDK 安装</h3><p>该 SDK 是 ST 官方构建的交叉编译工具链（Toolchain），提供了用于构建 U-Boot、Linux 内核、Buildroot 系统和用户空间程序所需的编译器、头文件与库文件；<br>后续进行 STM32MP157 架构相关的编译工作时，均需依赖该 SDK 环境；</p><p>SDK 在解压时需要选择一个目录，作为 SDK 的安装目录，在这里我就把 sdk 文件夹改名为 sdk_installer ，然后新建一个名为 sdk 的文件夹，作为 SDK 的实际安装目录；</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">mv</span> sdk sdk_installer &amp;&amp; <span class="built_in">mkdir</span> sdk</span><br></pre></td></tr></table></figure><p>执行之后文件结构如下：</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></pre></td><td class="code"><pre><span class="line">stm32mp1-openstlinux-5.4-dunfell-mp1-20-06-24/</span><br><span class="line">├── sdk_installer/         # 安装器（.sh文件）</span><br><span class="line">├── sdk/                   # 安装完成的 SDK 目录</span><br><span class="line">└── source/                # 包含 u-boot、kernel、buildroot 等源码</span><br></pre></td></tr></table></figure><p>进入 sdk_installer 的目录，执行名为 st-image-weston-openstlinux-weston-stm32mp1-x86_64-toolchain-3.1-openstlinux-5.4-dunfell-mp1-20-06-24.sh 的文件，接下来会提示你输入安装目录，输入我们刚刚创建的 sdk 目录，继续根据提示输入 y ，然后等待安装完成即可，整个过程提示如下：</p><center>   <img     src="https://picture.orangetime.top/i/2025/06/19/685428c508bf6.png"  ></center><p>根据提示，我们只需要执行下面的命令，就可以使用 SDK 了：</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><span class="line">. /root/stm32mp1-openstlinux-5.4-dunfell-mp1-20-06-24/sdk/environment-setup-cortexa7t2hf-neon-vfpv4-ostl-linux-gnueabi</span><br><span class="line"></span><br><span class="line"><span class="comment"># 或者可以执行</span></span><br><span class="line"><span class="built_in">source</span> /root/stm32mp1-openstlinux-5.4-dunfell-mp1-20-06-24/sdk/environment-setup-cortexa7t2hf-neon-vfpv4-ostl-linux-gnueabi</span><br></pre></td></tr></table></figure><p>每次手动执行 source 命令略显繁琐，我们可以将它加入 .bashrc 文件，让终端在启动时自动加载，提升开发效率：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">nano ~/.bashrc</span><br></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><span class="line">. /root/stm32mp1-openstlinux-5.4-dunfell-mp1-20-06-24/sdk/environment-setup-cortexa7t2hf-neon-vfpv4-ostl-linux-gnueabi</span><br></pre></td></tr></table></figure><p>再执行 . ~&#x2F;.bashrc 命令，使配置生效；</p><p>配置成功之后，执行 echo $CC 和 echo $ARCH 命令，如果输出如下，则表示 SDK 配置成功：</p><center>   <img     src="https://picture.orangetime.top/i/2025/06/19/68542902445d8.png"  ></center><blockquote><p><strong>提示</strong>：安装 SDK 的意义不只是配置 $CC 和 $ARCH，更重要的是自动设置了交叉工具链、动态库路径、头文件路径等编译参数，使得我们可以一条命令完成 U-Boot、内核、应用程序等的 ARM 架构编译；<br>sdk_installer 以及 tar.xz 文件可以删除，后续编译过程中不再需要；</p></blockquote><h3 id="源码获取"><a href="#源码获取" class="headerlink" title="源码获取"></a>源码获取</h3><p>进入 source 目录，可以看到里面包含多个源码文件夹，其中 u-boot、kernel、buildroot 是我们移植过程中需要用到的，其他文件夹可以忽略；</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> sources/arm-ostl-linux-gnueabi &amp;&amp; <span class="built_in">ls</span></span><br></pre></td></tr></table></figure><p>可以看到，u-boot、kernel、buildroot 等源码文件夹都在这里：</p><center>   <img     src="https://picture.orangetime.top/i/2025/06/19/68542ca46796f.png"  ></center><h2 id="U-Boot-编译"><a href="#U-Boot-编译" class="headerlink" title="U-Boot 编译"></a>U-Boot 编译</h2><h3 id="源码准备"><a href="#源码准备" class="headerlink" title="源码准备"></a>源码准备</h3><p>上一步看到的源码仍然是压缩包，我们进入 U-Boot 的目录，在目录下能看到 README.HOW_TO.txt 文件，里面介绍了依赖的安装和源码的解压，我们按照文件操作即可；</p><h4 id="安装依赖"><a href="#安装依赖" class="headerlink" title="安装依赖"></a>安装依赖</h4><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt install libncurses5-dev libncursesw5-dev</span><br></pre></td></tr></table></figure><ul><li>SDK 环境加载，这一步和之前 SDK 安装时一样，不再赘述；</li></ul><h4 id="源码解压与补丁"><a href="#源码解压与补丁" class="headerlink" title="源码解压与补丁"></a>源码解压与补丁</h4><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><span class="line">tar xfz u-boot-stm32mp-2020.01-r0.tar.gz</span><br><span class="line"></span><br><span class="line"><span class="built_in">cd</span> u-boot-stm32mp-2020.01</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> p <span class="keyword">in</span> `<span class="built_in">ls</span> -1 ../*.patch`; <span class="keyword">do</span> patch -p1 &lt; <span class="variable">$p</span>; <span class="keyword">done</span></span><br></pre></td></tr></table></figure><h3 id="配置与编译"><a href="#配置与编译" class="headerlink" title="配置与编译"></a>配置与编译</h3><h4 id="复制默认配置文件"><a href="#复制默认配置文件" class="headerlink" title="复制默认配置文件"></a>复制默认配置文件</h4><p>首先，基于 ST 官方的 trusted 配置，我们复制一份作为我们板子的基础配置：</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><span class="line"><span class="built_in">cp</span> configs/stm32mp15_trusted_defconfig configs/stm32mp15_emtime_trusted_defconfig</span><br><span class="line"></span><br><span class="line">make stm32mp15_emtime_trusted_defconfig</span><br></pre></td></tr></table></figure><h4 id="板级设备树修改"><a href="#板级设备树修改" class="headerlink" title="板级设备树修改"></a>板级设备树修改</h4><p>ST 的默认设备树不包含我们自定义的板卡信息，因此需要在 arch&#x2F;arm&#x2F;dts&#x2F; 中添加我们自己的设备树文件，设备树可以参考 arch&#x2F;arm&#x2F;dts&#x2F; 目录下的其它配置，甚至可以先直接用官方的（只要你的硬件契合），主要涉及以下几个文件（以官方文件为例）：</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></pre></td><td class="code"><pre><span class="line">arch/arm/dts/stm32mp15xx-dkx.dtsi</span><br><span class="line">arch/arm/dts/stm32mp157a-dk1.dts</span><br><span class="line">arch/arm/dts/stm32mp157a-dk1-u-boot.dtsi</span><br></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><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cp</span> <span class="built_in">arch</span>/arm/dts/stm32mp15xx-dkx.dtsi <span class="built_in">arch</span>/arm/dts/stm32mp15xx-emtime.dtsi</span><br><span class="line"><span class="built_in">cp</span> <span class="built_in">arch</span>/arm/dts/stm32mp157a-dk1.dts <span class="built_in">arch</span>/arm/dts/stm32mp157a-emtime.dts</span><br><span class="line"><span class="built_in">cp</span> <span class="built_in">arch</span>/arm/dts/stm32mp157a-dk1-u-boot.dtsi <span class="built_in">arch</span>/arm/dts/stm32mp157a-emtime-u-boot.dtsi</span><br></pre></td></tr></table></figure><p>修改其中的文件，修改文件引用相关的信息，如下：</p><ul><li>将 arch&#x2F;arm&#x2F;dts&#x2F;stm32mp157a-emtime.dts 的 #include “stm32mp15xx-dkx.dtsi” 改为 #include “stm32mp15xx-emtime.dtsi”</li><li>修改 arch&#x2F;arm&#x2F;dts&#x2F;Makefile，在 stm32mp157 (dtb-$(CONFIG_STM32MP15x)) 中加入我们自己刚刚添加的文件，即加上 stm32mp157a-emtime.dtb \</li></ul><blockquote><p><strong>注意</strong>：这一部分必须按照你实际情况进行修改（比如修改电源配置、增加 SD 卡支持、增加 eMMC 驱动、添加网卡驱动等），否则会导致无法正常引导或者无法正常驱动的问题；</p></blockquote><blockquote><p>之后有机会，我会专门写一篇关于设备树的文章，介绍如何编写设备树；<br>虽然我现在还没有编写这一部分的文章，但是设备树是 Linux 内核中非常重要的一部分，也是嵌入式开发中非常重要的一部分，所以建议有时间的话，一定要去学习一下；</p></blockquote><h4 id="bootcmd-修改"><a href="#bootcmd-修改" class="headerlink" title="bootcmd 修改"></a>bootcmd 修改</h4><ul><li>若要进行bootcmd的修改，则可以修改 include&#x2F;configs&#x2F;stm32mp1.h 文件，在 #ifdef CONFIG_BOOTCOMMAND 之后添加如下代码：</li></ul><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><span class="line"><span class="meta">#<span class="keyword">ifdef</span> CONFIG_BOOTCOMMAND</span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">undef</span> CONFIG_BOOTCOMMAND</span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">endif</span></span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> CONFIG_BOOTCOMMAND \</span></span><br><span class="line"><span class="meta"><span class="string">&quot;if ext4ls mmc 1:2 /; then &quot;</span> \</span></span><br><span class="line"><span class="meta"><span class="string">&quot;setenv bootcmd ext4load mmc 1:2 c2000000 uImage; &quot;</span> \</span></span><br><span class="line"><span class="meta"><span class="string">&quot;setenv bootcmd \&quot;$&#123;bootcmd&#125;;ext4load mmc 1:2 c4000000 stm32mp157a-emtime.dtb\&quot;; &quot;</span> \</span></span><br><span class="line"><span class="meta"><span class="string">&quot;setenv bootcmd \&quot;$&#123;bootcmd&#125;;bootm c2000000 - c4000000\&quot;; &quot;</span> \</span></span><br><span class="line"><span class="meta"><span class="string">&quot;setenv bootargs rootwait rw console=ttySTM0,115200 root=/dev/mmcblk1p3; &quot;</span> \</span></span><br><span class="line"><span class="meta"><span class="string">&quot;saveenv; run bootcmd; &quot;</span> \</span></span><br><span class="line"><span class="meta"><span class="string">&quot;else run bootcmd_stm32mp; fi&quot;</span></span></span><br></pre></td></tr></table></figure><ul><li><p>这里采用了偷懒的方法，在判断 CONFIG_BOOTCOMMAND 宏定义的地方，直接将 CONFIG_BOOTCOMMAND 宏定义取消，然后重新定义了 CONFIG_BOOTCOMMAND 宏定义，这样在编译的时候，就会使用我们重新定义的宏定义，而不会使用默认的宏定义；</p></li><li><p>为什么我要重写 bootcmd？因为他们设计的这一版板子，boot 拨码开关有问题，无论怎么拨，都会进入 USB 烧录模式，但 如果是 USB 烧录的情况，那么是不会正常加载eMMC的，所以我使用了 ext4ls 命令判断 eMMC 是否正常挂载，如果正常挂载，则加载内核和设备树，然后启动，否则执行默认的 bootcmd_stm32mp 进入下载模式；</p></li></ul><blockquote><p>后续找到了 bootcmd 和 bootargs 的定义，在 include&#x2F;env_default.h 文件下，可以自行去修改；</p></blockquote><h4 id="U-Boot-配置"><a href="#U-Boot-配置" class="headerlink" title="U-Boot 配置"></a>U-Boot 配置</h4><p>执行 make menuconfig 命令，进入配置界面：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">make menuconfig</span><br></pre></td></tr></table></figure><center>   <img     src="https://picture.orangetime.top/i/2025/06/20/68552bee46980.png"  ></center><p>其实大部分我们都不用修改，因为我们复制的是官方的配置，官方的配置已经包含了我们需要的配置，而且因为是 U-Boot 的配置，并不需要配置太多驱动相关的东西，这里提一嘴 bootcmd value 吧，在这里我们也可以修改 bootcmd，如果这里修改了，那么上面代码中的操作是可以省略的；</p><p>配置完之后记得保存，然后就可以退出 menuconfig 界面了；</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><span class="line"><span class="built_in">cp</span> .config configs/stm32mp15_emtime_trusted_defconfig</span><br></pre></td></tr></table></figure><h4 id="编译"><a href="#编译" class="headerlink" title="编译"></a>编译</h4><p>修改上一个目录中的 Makefile 文件：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">nano ../Makefile.sdk</span><br></pre></td></tr></table></figure><p>对文件进行如下操作：</p><ul><li>在UBOOT_CONFIGS中添加stm32mp15_emtime_trusted_defconfig,trusted,u-boot.stm32</li><li>在DEVICE_TREE中添加stm32mp157a-emtime</li></ul><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></pre></td><td class="code"><pre><span class="line">make distclean</span><br><span class="line"></span><br><span class="line">make -f <span class="variable">$PWD</span>/../Makefile.sdk all UBOOT_CONFIGS=stm32mp15_emtime_trusted_defconfig,trusted,u-boot.stm32 -j$(<span class="built_in">nproc</span>) 2&gt; error.log</span><br></pre></td></tr></table></figure><p>可以通过 cat error.log 查看编译错误，如果编译成功，那么会在 ..&#x2F;build-trusted 目录下会生成对应的文件，比如我们这里就是如下图框住的 u-boot-stm32mp157a-emtime-trusted.stm32 文件；</p><center>   <img     src="https://picture.orangetime.top/i/2025/06/20/68556be481000.png"  ></center><p>使用 sudo dd if&#x3D;..&#x2F;build-trusted&#x2F;u-boot-stm32mp157a-emtime-trusted.stm32 of&#x3D;&#x2F;dev&#x2F;sdXY bs&#x3D;1024 seek&#x3D;8 conv&#x3D;fsync,notrunc 命令将生成的文件烧录到 SD 卡中，然后就可以正常引导了；</p><blockquote><p>未完待续…</p></blockquote>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;近年来，&lt;strong&gt;异构多核 SoC&lt;/strong&gt; 在嵌入式领域越来越常见，其中 STM32MP157 凭借其** Cortex-A7 + Cortex-M4 双核架构** 和良好的开源生态，成为许多开发者进行 Linux 系统移植和定制的理想平台；&lt;/p&gt;
&lt;p&gt;</summary>
      
    
    
    
    <category term="linux-embedded" scheme="http://example.com/categories/linux-embedded/"/>
    
    
  </entry>
  
  <entry>
    <title>Letter Shell：自定义函数参数解析</title>
    <link href="http://example.com/2024/02/15/mcu/letter-shell4/"/>
    <id>http://example.com/2024/02/15/mcu/letter-shell4/</id>
    <published>2024-02-15T02:50:19.000Z</published>
    <updated>2024-02-15T02:50:19.000Z</updated>
    
    <content type="html"><![CDATA[<p>仓库：<a class="link"   href="https://github.com/NevermindZZT/letter-shell" >https://github.com/NevermindZZT/letter-shell<i class="fas fa-external-link-alt"></i></a></p><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>在嵌入式项目中，将函数导出为 Shell 命令是一项非常实用的能力；然而，随着调试和控制需求的增加，我们往往希望调用的函数能够接受多个参数，甚至是不同类型的复杂参数，而不仅仅是简单地触发执行；</p><p>为了解决这一需求，Letter Shell 提供了“函数签名（Function Signature）”机制；通过为函数指定参数类型，Shell 能够在命令行中自动完成字符串到对应类型的转换，从而支持整型、浮点、字符串、指针等多种参数的解析；这个机制让 Shell 不再只是“命令触发器”，而是一个支持复杂交互的真正调试接口；</p><p>本节将介绍函数签名机制的使用方式、参数类型的定义方式以及注意事项，帮助你实现更强大、更灵活的 Shell 命令调用功能；</p><h2 id="函数签名引入"><a href="#函数签名引入" class="headerlink" title="函数签名引入"></a>函数签名引入</h2><p>从 3.2.x 版本开始，Letter Shell 引入了 <strong>函数签名（Function Signature）</strong> 的概念；以往 Shell 调用函数时，会根据输入参数“猜测”目标类型并尝试转换；这个机制在简单命令中还能应付，但在类型较复杂或参数较多的情况下，错误率明显升高，一旦类型猜错，轻则输出异常，重则直接跑飞；</p><p>于是，现在你可以选择开启一个新的宏：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SHELL_USING_FUNC_SIGNATURE</span><br></pre></td></tr></table></figure><p>开启后，Shell 会依据你提供的“函数签名”字符串，对参数进行明确且严格的类型解析，从而彻底摆脱以往“猜类型”的粗放方式；</p><p>一旦参数类型和签名不匹配，Shell 会自动停止命令执行，这也算是多了一层保护；毕竟，错误执行一个函数远比拒绝执行带来的代价大得多；</p><h3 id="基本类型支持"><a href="#基本类型支持" class="headerlink" title="基本类型支持"></a>基本类型支持</h3><p>函数签名是一个字符串，用来声明函数每个参数的类型；以下是当前版本支持的基本类型签名：</p><table><thead><tr><th>类型</th><th>签名</th></tr></thead><tbody><tr><td>char（字符）</td><td>c</td></tr><tr><td>char（数字）</td><td>q</td></tr><tr><td>short（数字）</td><td>h</td></tr><tr><td>int（数字）</td><td>i</td></tr><tr><td>char*（字符串）</td><td>s</td></tr><tr><td>void*（任意指针）</td><td>p</td></tr></tbody></table><p>函数的返回值不参与签名声明，仅声明参数类型即可；</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> <span class="title function_">func</span><span class="params">(<span class="type">int</span> a, <span class="type">char</span> *b, <span class="type">char</span> c)</span>;</span><br></pre></td></tr></table></figure><p>对应的函数签名是：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">&quot;isc&quot;</span></span><br></pre></td></tr></table></figure><h3 id="使用方法"><a href="#使用方法" class="headerlink" title="使用方法"></a>使用方法</h3><p>在使用 SHELL_EXPORT_CMD 宏导出命令时，只需要在最后添加 .data.cmd.signature &#x3D; “…” 即可，示例代码如下：</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><span class="line"><span class="type">void</span> <span class="title function_">shellFuncSignatureTest</span><span class="params">(<span class="type">int</span> a, <span class="type">char</span> *b, <span class="type">char</span> c)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">&quot;a = %d, b = %s, c = %c\r\n&quot;</span>, a, b, c);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">SHELL_EXPORT_CMD(SHELL_CMD_PERMISSION(<span class="number">0</span>)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_FUNC),</span><br><span class="line">funcSignatureTest, shellFuncSignatureTest, test function signature,</span><br><span class="line">.data.cmd.signature = <span class="string">&quot;isc&quot;</span>);</span><br></pre></td></tr></table></figure><p>就这么简单，你只需声明一次签名，Shell 便会在每次命令调用时，自动进行对应的参数解析与校验；</p><h3 id="自定义参数类型解析"><a href="#自定义参数类型解析" class="headerlink" title="自定义参数类型解析"></a>自定义参数类型解析</h3><p>函数签名不仅支持基础类型，还支持 结构体等自定义类型 的解析；自定义签名格式为：</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">L&lt;类型名&gt;<span class="comment">;</span></span><br></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><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> &#123;</span></span><br><span class="line">    <span class="type">int</span> a;</span><br><span class="line">    <span class="type">char</span> *b;</span><br><span class="line">&#125; TestStruct;</span><br></pre></td></tr></table></figure><p>函数签名如下：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">&quot;iLTestStruct;s&quot;</span></span><br></pre></td></tr></table></figure><p>则代表函数的参数为：int、TestStruct* 和 char*；</p><h3 id="注册解析器"><a href="#注册解析器" class="headerlink" title="注册解析器"></a>注册解析器</h3><p>要让 Shell 正确解析 TestStruct，我们需要写一个解析器函数，并通过宏注册：</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></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> <span class="title function_">testStructParser</span><span class="params">(<span class="type">char</span> *<span class="built_in">string</span>, <span class="type">void</span> **param)</span></span><br><span class="line">&#123;</span><br><span class="line">    TestStruct *data = <span class="built_in">malloc</span>(<span class="keyword">sizeof</span>(TestStruct));</span><br><span class="line">    data-&gt;b = <span class="built_in">malloc</span>(<span class="number">16</span>);</span><br><span class="line">    <span class="keyword">if</span> (<span class="built_in">sscanf</span>(<span class="built_in">string</span>, <span class="string">&quot;%d %s&quot;</span>, &amp;(data-&gt;a), data-&gt;b) == <span class="number">2</span>)</span><br><span class="line">    &#123;</span><br><span class="line">        *param = (<span class="type">void</span> *)data;</span><br><span class="line">        <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">-1</span>;</span><br><span class="line">&#125;</span><br></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><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><span class="line"><span class="type">int</span> <span class="title function_">testStructCleaner</span><span class="params">(<span class="type">void</span> *param)</span></span><br><span class="line">&#123;</span><br><span class="line">    TestStruct *data = (TestStruct *)param;</span><br><span class="line">    <span class="built_in">free</span>(data-&gt;b);</span><br><span class="line">    <span class="built_in">free</span>(data);</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>注册方式如下：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SHELL_EXPORT_PARAM_PARSER(<span class="number">0</span>, LTestStruct;, testStructParser, testStructCleaner);</span><br></pre></td></tr></table></figure><p>如果不需要清理（比如结构体全是值类型），可以把最后一个参数设为 NULL；</p><h3 id="函数导出-自定义参数组合示例"><a href="#函数导出-自定义参数组合示例" class="headerlink" title="函数导出 + 自定义参数组合示例"></a>函数导出 + 自定义参数组合示例</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><span class="line"><span class="type">void</span> <span class="title function_">shellParamParserTest</span><span class="params">(<span class="type">int</span> a, TestStruct *data, <span class="type">char</span> *c)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">uint8_t</span> temp[<span class="number">100</span>] = &#123;<span class="number">0</span>&#125;;</span><br><span class="line">    Shell *shell = shellGetCurrent();</span><br><span class="line"></span><br><span class="line">    <span class="built_in">sprintf</span>((<span class="type">char</span> *)temp, <span class="string">&quot;a = %d, data-&gt;a = %d, data-&gt;b = %s, c = %s\r\n&quot;</span>, a, data-&gt;a, data-&gt;b, c);</span><br><span class="line">    shellWriteString(shell, (<span class="type">const</span> <span class="type">char</span> *)temp);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">SHELL_EXPORT_CMD_SIGN(SHELL_CMD_PERMISSION(<span class="number">0</span>)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_FUNC),</span><br><span class="line">paramParserTest, shellParamParserTest, test function signature and param parser,</span><br><span class="line">iLTestStruct;s);</span><br></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><span class="line">paramParserTest 1 <span class="string">&quot;12 2a&quot;</span> <span class="string">&quot;apple&quot;</span></span><br></pre></td></tr></table></figure><p>解释如下：</p><ul><li>参数1：int → 1</li><li>参数2：TestStruct* → data-&gt;a &#x3D; 12, data-&gt;b &#x3D; “2a”</li><li>参数3：char* → “apple”</li></ul><p>这里用双引号包住结构体参数是为了清晰分割，但实际输入中引号可选（Shell 会智能处理）；</p><h2 id="数组参数解析引入"><a href="#数组参数解析引入" class="headerlink" title="数组参数解析引入"></a>数组参数解析引入</h2><p>从 Letter Shell 3.2.3 版本开始，函数签名机制进一步增强，新增了 <strong>数组参数</strong> 的支持，适用于如 int[]、char *[]、自定义结构体数组等情况；</p><h3 id="启用方法"><a href="#启用方法" class="headerlink" title="启用方法"></a>启用方法</h3><p>要启用数组参数解析功能，需要：</p><ol><li>开启宏开关：SHELL_SUPPORT_ARRAY_PARAM</li><li>配置内存分配函数：确保宏 SHELL_MALLOC 和 SHELL_FREE 被定义，指向你项目中的内存分配函数（如使用 FreeRTOS，可指向 pvPortMalloc 和 vPortFree，裸机系统下可绑定 malloc &#x2F; free 或其他实现）；</li></ol><h3 id="数组签名语法"><a href="#数组签名语法" class="headerlink" title="数组签名语法"></a>数组签名语法</h3><p>在原有类型签名前加 [ 表示这是一个数组类型参数；例如：</p><table><thead><tr><th>类型</th><th>签名</th></tr></thead><tbody><tr><td>int[]</td><td>[i</td></tr><tr><td>TestStruct*[]</td><td>[LTestStruct;</td></tr></tbody></table><p>完整签名可组合使用，如：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">&quot;i[i[LTestStruct;&quot;</span></span><br></pre></td></tr></table></figure><p>表示的类型分别为：</p><ul><li>int</li><li>int []（整型数组）</li><li>TestStruct*[]（结构体指针数组）</li></ul><h3 id="示例代码"><a href="#示例代码" class="headerlink" title="示例代码"></a>示例代码</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></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> <span class="title function_">shellArrayTest</span><span class="params">(<span class="type">int</span> a, <span class="type">int</span> *b, TestStruct **datas)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">int</span> i;</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">&quot;a = %d, b = %p, datas = %p\r\n&quot;</span>, a, b, datas);</span><br><span class="line">    <span class="keyword">for</span> (i = <span class="number">0</span>; i &lt; shellGetArrayParamSize(b); i++)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">&quot;b[%d] = %d\r\n&quot;</span>, i, b[i]);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">for</span> (i = <span class="number">0</span>; i &lt; shellGetArrayParamSize(datas); i++)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">&quot;datas[%d]-&gt;a = %d, datas[%d]-&gt;b = %s\r\n&quot;</span>, i, datas[i]-&gt;a, i, datas[i]-&gt;b);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">SHELL_EXPORT_CMD_SIGN(SHELL_CMD_PERMISSION(<span class="number">0</span>)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_FUNC),</span><br><span class="line">arrayTest, shellArrayTest, test <span class="built_in">array</span> param parser,</span><br><span class="line">i[i[LTestStruct;);</span><br></pre></td></tr></table></figure><p>使用示例，终端中输入命令如下：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">arrayTest <span class="number">12</span> [<span class="number">65</span>, <span class="number">89</span>, <span class="number">45</span>] [<span class="string">&quot;100 hello&quot;</span>, <span class="string">&quot;56 world&quot;</span>]</span><br></pre></td></tr></table></figure><p>含义解析如下：</p><ul><li>参数1：int → 12</li><li>参数2：int [] → {65, 89, 45}</li><li>参数3：TestStruct []（结构体数组）→<ul><li>datas[0]-&gt;a &#x3D; 100, datas[0]-&gt;b &#x3D; “hello”</li><li>datas[1]-&gt;a &#x3D; 56, datas[1]-&gt;b &#x3D; “world”</li></ul></li></ul><blockquote><p><strong>提示</strong>：数组用中括号括起来，元素之间用英文逗号分隔；结构体数组的每个元素用双引号包裹，格式与结构体解析规则一致；</p></blockquote><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>通过引入函数签名机制，Letter Shell 显著提升了命令行参数解析的可靠性与灵活性，使其不仅仅是一个简单的命令触发器，更是一个具备强交互能力的调试入口；无论是基础类型、指针类型，还是结构体乃至结构体数组，用户都可以借助清晰的签名字符串和自定义解析器，实现类型安全且表达力丰富的参数传递；</p><p>此外，函数签名机制还为 Shell 使用者提供了更明确的接口约束，有效避免了因类型错误导致的崩溃或异常；配合内存管理机制、解析器注册机制和数组支持，Letter Shell 已具备构建嵌入式调试框架所需的高级能力；</p><p>对于追求高可靠性和高可维护性的嵌入式项目来说，掌握并使用好函数签名功能，将为 Shell 接口的设计与扩展带来极大的便利和安全保障；</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;仓库：&lt;a class=&quot;link&quot;   href=&quot;https://github.com/NevermindZZT/letter-shell&quot; &gt;https://github.com/NevermindZZT/letter-shell&lt;i class=&quot;fas fa-ex</summary>
      
    
    
    
    <category term="mcu" scheme="http://example.com/categories/mcu/"/>
    
    
  </entry>
  
  <entry>
    <title>Letter Shell：自定义命令</title>
    <link href="http://example.com/2024/02/15/mcu/letter-shell3/"/>
    <id>http://example.com/2024/02/15/mcu/letter-shell3/</id>
    <published>2024-02-15T01:22:38.000Z</published>
    <updated>2024-02-15T01:22:38.000Z</updated>
    
    <content type="html"><![CDATA[<p>仓库：<a class="link"   href="https://github.com/NevermindZZT/letter-shell" >https://github.com/NevermindZZT/letter-shell<i class="fas fa-external-link-alt"></i></a></p><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>在完成基本交互功能和修复了一些常见问题之后，Letter Shell 已经能够稳定运行于我的嵌入式项目中；然而，项目开发中常常需要更多“定制化”的功能，比如：</p><ul><li>注册自己的命令，快速调试；</li><li>添加快捷键，提高开发效率；</li><li>让 Shell 能调用带多个复杂参数的函数（支持自动解析参数），避免为每个测试场景重复编写包装函数；</li></ul><p>好消息是，Letter Shell 本身就提供了完善的接口支持这些扩展；坏消息是，官方文档对这些内容略显简略，实际用起来还需要一点“摸索”；</p><p>这些扩展并非仅是“锦上添花”，而是将 Shell 从一个输入工具，变成一个真正实用的交互调试平台的关键；</p><p>Letter Shell 中，所有能被执行的“功能”：无论是你写的函数、绑定的快捷键、设置的变量，甚至是用户本身，在底层其实都被统一视为“命令项（Command）”；这些命令共享一套注册、查找与执行机制，使得 Shell 的扩展非常灵活；</p><p>不过在实际使用中，我们往往会将“自定义命令”专指 <strong>将 C 函数导出为 Shell 命令的行为</strong>，这是调试开发中最常用、最直接、最强大的手段之一；通过简单的宏定义，就可以将任意函数变成终端可调用的命令，极大提升调试效率和系统可观测性；</p><h2 id="写在前面"><a href="#写在前面" class="headerlink" title="写在前面"></a>写在前面</h2><p>Letter Shell 针对常用编译器都提供了宏支持，而为了保证导出的命令不会被优化掉，不同编译器还有一些额外的配置技巧：</p><ul><li>Keil MDK：建议在编译选项中添加 –keep shellCommand*，防止命令结构被链接器裁剪；</li><li>GCC：需要在链接脚本（ld 文件）中显式保留命令段：</li></ul><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></pre></td><td class="code"><pre><span class="line">_shell_command_start = .;</span><br><span class="line">KEEP (*(shellCommand))</span><br><span class="line">_shell_command_end = .;</span><br></pre></td></tr></table></figure><h2 id="函数命令导出"><a href="#函数命令导出" class="headerlink" title="函数命令导出"></a>函数命令导出</h2><p>函数导出是最常见的命令注册方式，适用于调试、信息打印、功能调用等场景；</p><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></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * @brief shell 命令定义</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * @param _attr 命令属性</span></span><br><span class="line"><span class="comment"> * @param _name 命令名</span></span><br><span class="line"><span class="comment"> * @param _func 命令函数</span></span><br><span class="line"><span class="comment"> * @param _desc 命令描述</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> SHELL_EXPORT_CMD(_attr, _name, _func, _desc) \</span></span><br><span class="line"><span class="meta">    const char shellCmd##_name[] = #_name; \</span></span><br><span class="line"><span class="meta">    const char shellDesc##_name[] = #_desc; \</span></span><br><span class="line"><span class="meta">    SHELL_USED const ShellCommand \</span></span><br><span class="line"><span class="meta">    shellCommand##_name SHELL_SECTION(<span class="string">&quot;shellCommand&quot;</span>) =  \</span></span><br><span class="line"><span class="meta">    &#123; \</span></span><br><span class="line"><span class="meta">        .attr.value = _attr, \</span></span><br><span class="line"><span class="meta">        .data.cmd.name = shellCmd##_name, \</span></span><br><span class="line"><span class="meta">        .data.cmd.function = (int (*)())_func, \</span></span><br><span class="line"><span class="meta">        .data.cmd.desc = shellDesc##_name \</span></span><br><span class="line"><span class="meta">    &#125;</span></span><br></pre></td></tr></table></figure><p>这行代码中将 _func 强转为 (int (*)()) 是为了兼容不同的函数签名（不管参数多少），但这样做会掩盖类型安全，有些 IDE 可能会报警告；</p><p>这种强制类型转换方式牺牲了类型检查，适合用于调试环境，但如果对类型安全有更严格要求，也可以配合包装函数使用；</p><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></pre></td><td class="code"><pre><span class="line"><span class="type">void</span> <span class="title function_">myShellTask</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">    Shell* shell = shellGetCurrent();</span><br><span class="line">    <span class="keyword">if</span>(shell)</span><br><span class="line">    &#123;</span><br><span class="line">        shellWriteString(shell, <span class="string">&quot;Shell自定义程序测试&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">SHELL_EXPORT_CMD(</span><br><span class="line">    SHELL_CMD_PERMISSION(<span class="number">0</span>) |</span><br><span class="line">    SHELL_CMD_TYPE(SHELL_TYPE_CMD_FUNC) |</span><br><span class="line">    SHELL_CMD_DISABLE_RETURN,</span><br><span class="line">    mytest, myShellTask, print message);</span><br></pre></td></tr></table></figure><p>使用 mytest 命令后，将输出 “Shell自定义程序测试”；这个机制可以非常方便地快速验证某个函数逻辑；</p><h2 id="变量导出"><a href="#变量导出" class="headerlink" title="变量导出"></a>变量导出</h2><p>除了函数，也可以将变量导出到 Shell 中进行读写；支持类型包括：char、short、int、字符串、指针等；</p><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></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * @brief shell 变量定义</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * @param _attr 变量属性</span></span><br><span class="line"><span class="comment"> * @param _name 变量名</span></span><br><span class="line"><span class="comment"> * @param _value 变量值（地址）</span></span><br><span class="line"><span class="comment"> * @param _desc 变量描述</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> SHELL_EXPORT_VAR(_attr, _name, _value, _desc) \</span></span><br><span class="line"><span class="meta">    const char shellCmd##_name[] = #_name; \</span></span><br><span class="line"><span class="meta">    const char shellDesc##_name[] = #_desc; \</span></span><br><span class="line"><span class="meta">    SHELL_USED const ShellCommand \</span></span><br><span class="line"><span class="meta">    shellVar##_name SHELL_SECTION(<span class="string">&quot;shellCommand&quot;</span>) =  \</span></span><br><span class="line"><span class="meta">    &#123; \</span></span><br><span class="line"><span class="meta">        .attr.value = _attr, \</span></span><br><span class="line"><span class="meta">        .data.var.name = shellCmd##_name, \</span></span><br><span class="line"><span class="meta">        .data.var.value = (void *)_value, \</span></span><br><span class="line"><span class="meta">        .data.var.desc = shellDesc##_name \</span></span><br><span class="line"><span class="meta">    &#125;</span></span><br></pre></td></tr></table></figure><blockquote><p><strong>注意</strong>：_value 必须是变量的地址，除非是字符串（数组本身即指针）；如果变量是只读的，可以加上 SHELL_CMD_READ_ONLY 属性；</p></blockquote><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></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> varInt = <span class="number">0</span>;</span><br><span class="line">SHELL_EXPORT_VAR(SHELL_CMD_PERMISSION(<span class="number">0</span>)|SHELL_CMD_TYPE(SHELL_TYPE_VAR_INT), varInt, &amp;varInt, test);</span><br><span class="line"></span><br><span class="line"><span class="type">char</span> str[] = <span class="string">&quot;test string&quot;</span>;</span><br><span class="line">SHELL_EXPORT_VAR(SHELL_CMD_PERMISSION(<span class="number">0</span>)|SHELL_CMD_TYPE(SHELL_TYPE_VAR_STRING), varStr, str, test);</span><br><span class="line"></span><br><span class="line">Log <span class="built_in">log</span>;</span><br><span class="line">SHELL_EXPORT_VAR(SHELL_CMD_PERMISSION(<span class="number">0</span>)|SHELL_CMD_TYPE(SHELL_TYPE_VAR_POINT), <span class="built_in">log</span>, &amp;<span class="built_in">log</span>, test);</span><br></pre></td></tr></table></figure><p>上述变量在终端中直接输入变量名（如 varInt），即可显示或修改值（除非是只读变量）；</p><h2 id="按键定义导出"><a href="#按键定义导出" class="headerlink" title="按键定义导出"></a>按键定义导出</h2><p>除了函数和变量，也可以绑定终端中的某些按键（如 Tab、Ctrl+C）来触发特定功能；</p><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></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * @brief shell 按键定义</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * @param _attr 按键属性</span></span><br><span class="line"><span class="comment"> * @param _value 按键键值（大端序）</span></span><br><span class="line"><span class="comment"> * @param _func 按键函数</span></span><br><span class="line"><span class="comment"> * @param _desc 描述信息</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> SHELL_EXPORT_KEY(_attr, _value, _func, _desc) \</span></span><br><span class="line"><span class="meta">    const char shellDesc##_value[] = #_desc; \</span></span><br><span class="line"><span class="meta">    SHELL_USED const ShellCommand \</span></span><br><span class="line"><span class="meta">    shellKey##_value SHELL_SECTION(<span class="string">&quot;shellCommand&quot;</span>) =  \</span></span><br><span class="line"><span class="meta">    &#123; \</span></span><br><span class="line"><span class="meta">        .attr.value = _attr|SHELL_CMD_TYPE(SHELL_TYPE_KEY), \</span></span><br><span class="line"><span class="meta">        .data.key.value = _value, \</span></span><br><span class="line"><span class="meta">        .data.key.function = (void (*)(Shell *))_func, \</span></span><br><span class="line"><span class="meta">        .data.key.desc = shellDesc##_value \</span></span><br><span class="line"><span class="meta">    &#125;</span></span><br></pre></td></tr></table></figure><p>按键值说明，按键值为按键发送的字节序列，按 <strong>大端模式</strong> 表示；例如：</p><ul><li>Tab 键发送 0x0B：键值为 0x0B000000</li><li>方向上箭头发送序列 0x1B 0x5B 0x41：键值为 0x1B5B4100</li></ul><p>使用示例（绑定 Ctrl+C）：</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></pre></td><td class="code"><pre><span class="line"><span class="type">uint8_t</span> is_CtrlC_Pressed = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> <span class="type">void</span> <span class="title function_">CtrlC_Interrupt</span><span class="params">(Shell* shell)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="comment">// 标记 Ctrl+C 被按下，用于其他程序中判断</span></span><br><span class="line">    is_CtrlC_Pressed = <span class="number">1</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 下面两行是标准行为：换行 + 触发下一轮命令解析</span></span><br><span class="line">    <span class="keyword">extern</span> <span class="type">void</span> <span class="title function_">myshellWritePrompt</span><span class="params">(Shell *shell, <span class="type">unsigned</span> <span class="type">char</span> newline)</span>;</span><br><span class="line">    myshellWritePrompt(shell, <span class="number">1</span>);</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">extern</span> <span class="type">void</span> <span class="title function_">myshellExec</span><span class="params">(Shell *shell)</span>;</span><br><span class="line">    myshellExec(shell);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">SHELL_EXPORT_KEY(SHELL_CMD_PERMISSION(<span class="number">0</span>), <span class="number">0x03000000</span>, CtrlC_Interrupt, Ctrl_C);</span><br></pre></td></tr></table></figure><p>这样在终端中按下 Ctrl+C 时，即可自动执行 CtrlC_Interrupt() 中定义的行为；</p><blockquote><p>提示：可以使用串口调试工具（如 SecureCRT）开启“十六进制查看模式”，按下目标键并记录其字节序列，以获取精确键值；</p></blockquote><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>Letter Shell 的命令导出机制设计得非常灵活统一，使用 .shellCommand 段 + 宏展开的方式，让用户可以零成本地将已有的函数、变量或按键操作暴露到终端；这一机制不仅极大提高了调试效率，也为后期维护和动态控制提供了便利；</p><p>只需根据不同平台配置保留 .shellCommand 段（如通过 –keep shellCommand* 或 KEEP(*(.shellCommand))），即可确保这些命令在最终镜像中生效；</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;仓库：&lt;a class=&quot;link&quot;   href=&quot;https://github.com/NevermindZZT/letter-shell&quot; &gt;https://github.com/NevermindZZT/letter-shell&lt;i class=&quot;fas fa-ex</summary>
      
    
    
    
    <category term="mcu" scheme="http://example.com/categories/mcu/"/>
    
    
  </entry>
  
  <entry>
    <title>Letter Shell：问题修复与功能扩展</title>
    <link href="http://example.com/2024/02/15/mcu/letter-shell2/"/>
    <id>http://example.com/2024/02/15/mcu/letter-shell2/</id>
    <published>2024-02-15T00:01:59.000Z</published>
    <updated>2024-02-15T00:01:59.000Z</updated>
    
    <content type="html"><![CDATA[<p>仓库：<a class="link"   href="https://github.com/NevermindZZT/letter-shell" >https://github.com/NevermindZZT/letter-shell<i class="fas fa-external-link-alt"></i></a></p><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>在之前的文章中，我介绍了 Letter Shell 的基本使用方法；在实际使用过程中，我发现 Letter Shell 存在一些小问题，也结合项目需求对其功能进行了扩展；本文将介绍这些问题的现象与修复方法，并分享新增功能的实现过程和实际用途；</p><h2 id="问题修复（部分修复）"><a href="#问题修复（部分修复）" class="headerlink" title="问题修复（部分修复）"></a>问题修复（部分修复）</h2><blockquote><p>现象：在 Letter Shell 中，串口输入时长按退格键或 Delete 键时，终端显示存在字符残留，虽然字符已无法继续删除，但视觉上仍未完全清除；</p></blockquote><h3 id="这背后的常见原因："><a href="#这背后的常见原因：" class="headerlink" title="这背后的常见原因："></a>这背后的常见原因：</h3><ol><li><p>终端行为：</p><ul><li>串口终端（如 SecureCRT、TeraTerm、MobaXterm 等）长按退格时会快速发出多个 0x08 字节；</li><li>这些字符本质是 “光标后退一格”，但 Letter Shell 要负责同步显示以及缓存处理；</li></ul></li><li><p>Letter Shell 内部处理问题：</p><ul><li>Letter Shell 会维护一个输入缓存；</li><li>如果没有正确处理 “删到头” 的情况，可能会出现：<ul><li>显示没擦干净（字符残留）；</li><li>缓存指针已到头，仍在处理退格；</li><li>没有回显正确的光标移动和空格清除字符；</li></ul></li></ul></li></ol><h3 id="修复方法要点"><a href="#修复方法要点" class="headerlink" title="修复方法要点"></a>修复方法要点</h3><ol><li><p>判断输入缓存是否为空：在 shellHandler() 中处理退格符（0x08）时，判断光标是否已在开头，防止“负向删除”（即光标越界到输入行左边界之外）;</p></li><li><p>对于连续退格，要保证：</p><ul><li>Shell 缓冲区更新正确；</li><li>串口输出内容与终端光标同步；</li><li>不要超出缓存边界；</li></ul></li></ol><h3 id="修改后的完整代码"><a href="#修改后的完整代码" class="headerlink" title="修改后的完整代码"></a>修改后的完整代码</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></pre></td><td class="code"><pre><span class="line"><span class="type">void</span> <span class="title function_">shellDeleteByte</span><span class="params">(Shell *shell, <span class="type">signed</span> <span class="type">char</span> direction)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">char</span> offset = (direction == <span class="number">-1</span>) ? <span class="number">1</span> : <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> ((shell-&gt;parser.cursor == <span class="number">0</span> &amp;&amp; direction == <span class="number">1</span>)</span><br><span class="line">        || (shell-&gt;parser.cursor == shell-&gt;parser.length &amp;&amp; direction == <span class="number">-1</span>))</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (shell-&gt;parser.cursor == shell-&gt;parser.length &amp;&amp; direction == <span class="number">1</span>)</span><br><span class="line">    &#123;</span><br><span class="line">        shell-&gt;parser.cursor--;</span><br><span class="line">        shell-&gt;parser.length--;</span><br><span class="line">        shell-&gt;parser.buffer[shell-&gt;parser.length] = <span class="number">0</span>;</span><br><span class="line">        shellDeleteCommandLine(shell, <span class="number">1</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">else</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">short</span> i = offset; i &lt; shell-&gt;parser.length - shell-&gt;parser.cursor; i++)</span><br><span class="line">        &#123;</span><br><span class="line">            shell-&gt;parser.buffer[shell-&gt;parser.cursor + i - <span class="number">1</span>] = </span><br><span class="line">                shell-&gt;parser.buffer[shell-&gt;parser.cursor + i];</span><br><span class="line">        &#125;</span><br><span class="line">        shell-&gt;parser.length--;</span><br><span class="line">        <span class="keyword">if</span> (!offset)</span><br><span class="line">        &#123;</span><br><span class="line">            shell-&gt;parser.cursor--;</span><br><span class="line">            shellWriteByte(shell, <span class="string">&#x27;\b&#x27;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        shell-&gt;parser.buffer[shell-&gt;parser.length] = <span class="number">0</span>;</span><br><span class="line">        <span class="comment">// 将光标移动到行首</span></span><br><span class="line">        <span class="keyword">for</span> (<span class="type">short</span> i = <span class="number">0</span>; i &lt; shell-&gt;parser.cursor + <span class="number">1</span>; i++)</span><br><span class="line">        &#123;</span><br><span class="line">            shellWriteByte(shell, <span class="string">&#x27;\b&#x27;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 重新打印整个命令行</span></span><br><span class="line">        <span class="keyword">for</span> (<span class="type">short</span> i = <span class="number">0</span>; i &lt; shell-&gt;parser.length; i++)</span><br><span class="line">        &#123;</span><br><span class="line">            shellWriteByte(shell, shell-&gt;parser.buffer[i]);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 打印一个空格，覆盖掉原来的最后一个字符</span></span><br><span class="line">        shellWriteByte(shell, <span class="string">&#x27; &#x27;</span>);</span><br><span class="line">        <span class="comment">// 将光标移动到正确的位置</span></span><br><span class="line">        <span class="keyword">for</span> (<span class="type">short</span> i = shell-&gt;parser.length; i &gt; shell-&gt;parser.cursor; i--)</span><br><span class="line">        &#123;</span><br><span class="line">            shellWriteByte(shell, <span class="string">&#x27;\b&#x27;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p>该函数通过整体刷新命令行缓冲区，并手动控制光标移动，确保删除行为与终端显示保持一致，尤其适用于字符中间删除的情形；<br>不过在实际使用中，仍会在某些输入场景下出现字符未被正确清除的情况，说明该方案不够完全可靠；如果你对此有更优的实现思路，欢迎联系我交流（联系方式已在主页提供）；</p></blockquote><h2 id="功能扩展"><a href="#功能扩展" class="headerlink" title="功能扩展"></a>功能扩展</h2><blockquote><p>在 Linux Shell 中，如果你在终端中键入了一长串命令，但突然不想执行，也不想一个一个退格删除，只需要轻轻按下 Ctrl+C，整行输入就会被取消，重新回到提示符；这种“强制销毁当前输入”的行为，对嵌入式调试来说也非常实用；<br>Letter Shell 默认的 Ctrl+C 实现主要用于中断长时间运行的命令，但并不具备清除当前行的效果；接下来，我们就来实现这一功能；</p></blockquote><h3 id="实现思路"><a href="#实现思路" class="headerlink" title="实现思路"></a>实现思路</h3><ol><li>使用 SHELL_EXPORT_KEY 注册一个 Ctrl+C 快捷键；</li><li>在回调函数中：<ul><li>将 is_CtrlC_Pressed 标记设为 1（供其他任务判断是否中断）；</li><li>重新输出提示符（myshellWritePrompt）；</li><li>清空当前行缓存（myshellExec 直接执行空行）；</li></ul></li><li>由于 Letter Shell 的内部函数是 static 的，我们自行实现 myshellWritePrompt 和 myshellExec 两个版本；</li></ol><h3 id="Ctrl-C-注册代码（port-文件中）"><a href="#Ctrl-C-注册代码（port-文件中）" class="headerlink" title="Ctrl+C 注册代码（port 文件中）"></a>Ctrl+C 注册代码（port 文件中）</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><span class="line"><span class="comment">/* Ctrl+C 注册，键值为 0x03 */</span></span><br><span class="line"><span class="type">uint8_t</span> is_CtrlC_Pressed = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> <span class="type">void</span> <span class="title function_">CtrlC_Interrupt</span><span class="params">(Shell* shell)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="comment">// 设置中断标志位，可供其他长运行命令判断</span></span><br><span class="line">    is_CtrlC_Pressed = <span class="number">1</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">extern</span> <span class="type">void</span> <span class="title function_">myshellWritePrompt</span><span class="params">(Shell *shell, <span class="type">unsigned</span> <span class="type">char</span> newline)</span>;</span><br><span class="line">    myshellWritePrompt(shell, <span class="number">1</span>); <span class="comment">// 输出新的一行提示符</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">extern</span> <span class="type">void</span> <span class="title function_">myshellExec</span><span class="params">(Shell *shell)</span>;</span><br><span class="line">    myshellExec(shell); <span class="comment">// 清除当前行输入</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">SHELL_EXPORT_KEY(SHELL_CMD_PERMISSION(<span class="number">0</span>), <span class="number">0x03000000</span>, CtrlC_Interrupt, Ctrl_C);</span><br></pre></td></tr></table></figure><h3 id="添加-myshellWritePrompt-与-myshellExec-函数（shell-c）"><a href="#添加-myshellWritePrompt-与-myshellExec-函数（shell-c）" class="headerlink" title="添加 myshellWritePrompt 与 myshellExec 函数（shell.c）"></a>添加 myshellWritePrompt 与 myshellExec 函数（shell.c）</h3><p>由于原函数是 static，我们避免直接修改 Letter Shell 源码，而是新建两个函数：</p><p>myshellWritePrompt：打印提示符</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><span class="line"><span class="type">void</span> <span class="title function_">myshellWritePrompt</span><span class="params">(Shell *shell, <span class="type">unsigned</span> <span class="type">char</span> newline)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">if</span> (shell-&gt;status.isChecked)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">if</span> (newline)</span><br><span class="line">        &#123;</span><br><span class="line">            shellWriteString(shell, <span class="string">&quot;\r\n&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        shellWriteString(shell, shell-&gt;info.user-&gt;data.user.name);</span><br><span class="line">        shellWriteString(shell, <span class="string">&quot;:&quot;</span>);</span><br><span class="line">        shellWriteString(shell, shell-&gt;info.path ? shell-&gt;info.path : <span class="string">&quot;/&quot;</span>);</span><br><span class="line">        shellWriteString(shell, <span class="string">&quot;$ &quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">else</span></span><br><span class="line">    &#123;</span><br><span class="line">        shellWriteString(shell, shellText[SHELL_TEXT_PASSWORD_HINT]);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>myshellExec：清空当前命令缓冲区</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></pre></td><td class="code"><pre><span class="line"><span class="type">void</span> <span class="title function_">myshellExec</span><span class="params">(Shell *shell)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">if</span> (shell-&gt;parser.length == <span class="number">0</span>)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    shell-&gt;parser.buffer[shell-&gt;parser.length] = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (shell-&gt;status.isChecked)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="comment">// 此处不添加到历史、不调用命令，仅清空缓冲区</span></span><br><span class="line">        shellParserParam(shell); </span><br><span class="line">        shell-&gt;parser.length = shell-&gt;parser.cursor = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (shell-&gt;parser.paramCount == <span class="number">0</span>)</span><br><span class="line">        &#123;</span><br><span class="line">            <span class="keyword">return</span>;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">else</span></span><br><span class="line">    &#123;</span><br><span class="line">        shellCheckPassword(shell);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p><strong>注意</strong>：该功能依赖串口工具正确发送 0x03（Ctrl+C）字符，建议使用支持该行为的终端，如 MobaXterm、TeraTerm 等；</p></blockquote><h3 id="扩展建议：基于-is-CtrlC-Pressed-的中断任务设计"><a href="#扩展建议：基于-is-CtrlC-Pressed-的中断任务设计" class="headerlink" title="扩展建议：基于 is_CtrlC_Pressed 的中断任务设计"></a>扩展建议：基于 is_CtrlC_Pressed 的中断任务设计</h3><blockquote><p>通过 is_CtrlC_Pressed 标志，用户可以在长时间运行的命令中主动检查该变量，实现软中断功能；例如：</p></blockquote><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><span class="line"><span class="keyword">while</span> (running) &#123;</span><br><span class="line">    <span class="keyword">if</span> (is_CtrlC_Pressed) &#123;</span><br><span class="line">        <span class="keyword">break</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    do_some_heavy_task();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p><strong>注意</strong>：此中断机制适用于循环任务或长延时任务的主动中断，不适用于系统层级的任务终止；</p></blockquote><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文基于 Letter Shell 在实际嵌入式项目中的使用经验，修复了输入过程中因退格处理不完整导致的字符残留问题，并新增 Ctrl+C 清除当前输入行的功能，提升了交互体验与调试效率；</p><p>在功能扩展的同时，尽量保持对原始源码的非侵入性，便于后续升级与维护；Letter Shell 作为一款轻量级命令行工具，其灵活的可扩展性值得深入挖掘；</p><p>如果你也在使用它，欢迎交流更多改进思路；</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;仓库：&lt;a class=&quot;link&quot;   href=&quot;https://github.com/NevermindZZT/letter-shell&quot; &gt;https://github.com/NevermindZZT/letter-shell&lt;i class=&quot;fas fa-ex</summary>
      
    
    
    
    <category term="mcu" scheme="http://example.com/categories/mcu/"/>
    
    
  </entry>
  
</feed>
