<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Hexo</title>
  
  
  <link href="https://blog.orangetime.top/atom.xml" rel="self"/>
  
  <link href="https://blog.orangetime.top/"/>
  <updated>2026-01-02T15:53:39.010Z</updated>
  <id>https://blog.orangetime.top/</id>
  
  <author>
    <name>John Doe</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>STM32H7开发笔记(七):MPU引入与讲解</title>
    <link href="https://blog.orangetime.top/2026/01/02/mcu/h7-mpu-basics/"/>
    <id>https://blog.orangetime.top/2026/01/02/mcu/h7-mpu-basics/</id>
    <published>2026-01-01T19:03:16.024Z</published>
    <updated>2026-01-02T15:53:39.010Z</updated>
    
    <content type="html"><![CDATA[<p>其实接下来是打算直接写基础外设的，比如先写写串口的收发，但是想到要写外设的话，肯定要写 DMA，而在 H7 上使用 DMA 就必须先了解 MPU 和 Cache，所以还是先写写 MPU 吧。</p><blockquote><p>对于之前没有接触过类似概念的小伙伴来说，MPU 和 Cache 都是比较难理解的概念，我尽量把这一部分写的简单易懂一些。</p></blockquote><p>在之前的系列中，比如 F1、F4 甚至是 F7 中，我们都很少接触到 MPU 这个概念，F7 中虽然有，但是即使 F7 不用 MPU，也可以正常使用下去，但是到了 H7，MPU 从<strong>可选进阶</strong>变为了<strong>基础建设</strong>，所以必须先了解一下。</p><p>MPU 就是 Memory Protection Unit，内存保护单元，单纯看名字，可能会以为是什么防止内存被攻击的东西，其实不然，MPU 的主要作用并不是安全，而是“<strong>给内存立规矩</strong>”，告诉 CPU 这块内存<strong>应该如何使用</strong>。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;其实接下来是打算直接写基础外设的，比如先写写串口的收发，但是想到要写外设的话，肯定要写 DMA，而在 H7 上使用 DMA 就必须先了解 MPU 和 Cache，所以还是先写写 MPU 吧。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;对于之前没有接触过类似概念的小伙伴来说，M</summary>
      
    
    
    
    <category term="mcu" scheme="https://blog.orangetime.top/categories/mcu/"/>
    
    
  </entry>
  
  <entry>
    <title>STM32H7开发笔记(六):GPIO-输入处理-libopencm3库实现</title>
    <link href="https://blog.orangetime.top/2025/11/29/mcu/h7-gpio-lib-libopencm3/"/>
    <id>https://blog.orangetime.top/2025/11/29/mcu/h7-gpio-lib-libopencm3/</id>
    <published>2025-11-29T11:40:56.730Z</published>
    <updated>2025-12-29T11:01:16.689Z</updated>
    
    <content type="html"><![CDATA[<p>在上一篇中，我们使用 HAL 库实现了 easy_button 的按键处理，这一篇我们使用 libopencm3 库实现同样的功能。</p><p>大体上思路和 HAL 库实现是一样的，只是使用 libopencm3 库的 API 来实现。</p><p>本文代码仓库：<a class="link"   href="https://git.orangetime.top/EMTime/stm32h7-libopencm3" >stm32h7-libopencm3<i class="fas fa-external-link-alt"></i></a>，不想看我啰嗦的，可以直接拉代码，目录结构清晰，还写了详细注释。</p><blockquote><p>本文对应于仓库中的 2gpio-lib 文件夹</p></blockquote><h2 id="工程导入"><a href="#工程导入" class="headerlink" title="工程导入"></a>工程导入</h2><p>在这一次的代码中，我进行了一些管理上的变更，也算是一个新的工程，所以可以新建一个文件夹，我来引导大家一步一步来。</p><p>文件夹还是需要创建在和 stm32h7-libopencm3 同级的目录下，我们可以从之前的工程中复制 cortex-m-generic.ld 以及 user 文件夹到当前工程下，这些都是和之前保持一致的。</p><h3 id="下载-easy-button"><a href="#下载-easy-button" class="headerlink" title="下载 easy_button"></a>下载 easy_button</h3><ul><li>Github仓库：<a class="link"   href="https://github.com/bobwenstudy/easy_button" >https://github.com/bobwenstudy/easy_button<i class="fas fa-external-link-alt"></i></a></li></ul><blockquote><p>考虑到国内网络问题，部分读者可能无法访问 Github，所以我自己部署了 Gitea，将 easy_button 仓库同步到了我的 Gitea 服务器上，地址：<a class="link"   href="https://git.orangetime.top/EMTime/easy_button" >https://git.orangetime.top/EMTime/easy_button<i class="fas fa-external-link-alt"></i></a></p></blockquote><h3 id="添加-easy-button-文件"><a href="#添加-easy-button-文件" class="headerlink" title="添加 easy_button 文件"></a>添加 easy_button 文件</h3><p>我们要新增第三方库，这些库的源码，我建议单独放到一个文件夹中，这样方便管理，所以新建一个 lib 文件夹，在里面再创建 ebtn 文件夹，我们将下载的 easy_button 仓库中的所有文件复制到这个文件夹中。</p><h3 id="xmake-配置"><a href="#xmake-配置" class="headerlink" title="xmake 配置"></a>xmake 配置</h3><p>我主要修改了规则的位置和对于库的依赖，整体的配置如下：</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><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></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 工程名</span></span><br><span class="line">set_project(<span class="string">&quot;stm32h7&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 定义工具链，确定位置和名称</span></span><br><span class="line">toolchain(<span class="string">&quot;arm-none-eabi&quot;</span>)</span><br><span class="line">    set_kind(<span class="string">&quot;standalone&quot;</span>)</span><br><span class="line">    set_sdkdir(<span class="string">&quot;/home/time/doc/mybin/arm-none-eabi&quot;</span>)</span><br><span class="line">toolchain_end()</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 让工程使用我们定义的工具链</span></span><br><span class="line">set_toolchains(<span class="string">&quot;arm-none-eabi&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 设置平台与架构</span></span><br><span class="line">set_plat(<span class="string">&quot;MCU&quot;</span>)</span><br><span class="line">set_arch(<span class="string">&quot;ARM Cortex-M7&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 设置编译优化等级与编译模式</span></span><br><span class="line">set_optimize(<span class="string">&quot;none&quot;</span>)</span><br><span class="line">set_symbols(<span class="string">&quot;debug&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 设置架构 &amp; FPU</span></span><br><span class="line">add_cxflags(<span class="string">&quot;-mthumb&quot;</span>, <span class="string">&quot;-mcpu=cortex-m7&quot;</span>, <span class="string">&quot;-mfpu=fpv5-d16&quot;</span>, <span class="string">&quot;-mfloat-abi=hard&quot;</span>, &#123;force = <span class="literal">true</span>&#125;)</span><br><span class="line">add_asflags(<span class="string">&quot;-mthumb&quot;</span>, <span class="string">&quot;-mcpu=cortex-m7&quot;</span>, <span class="string">&quot;-mfpu=fpv5-d16&quot;</span>, <span class="string">&quot;-mfloat-abi=hard&quot;</span>, &#123;force = <span class="literal">true</span>&#125;)</span><br><span class="line">add_ldflags(<span class="string">&quot;-mthumb&quot;</span>, <span class="string">&quot;-mcpu=cortex-m7&quot;</span>, <span class="string">&quot;-mfpu=fpv5-d16&quot;</span>, <span class="string">&quot;-mfloat-abi=hard&quot;</span>, &#123;force = <span class="literal">true</span>&#125;)</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 设置全局头文件路径位置 (libopencm3)</span></span><br><span class="line">add_includedirs(<span class="string">&quot;../../libopencm3/include&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 设置全局链接库 (libopencm3)</span></span><br><span class="line">add_linkdirs(<span class="string">&quot;../../libopencm3/lib&quot;</span>)</span><br><span class="line">add_links(<span class="string">&quot;opencm3_stm32h7&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 设置全局额外链接选项</span></span><br><span class="line">add_ldflags(<span class="string">&quot;-T./cortex-m-generic.ld&quot;</span>, <span class="string">&quot;--static&quot;</span>, <span class="string">&quot;-nostartfiles&quot;</span>, <span class="string">&quot;-Wl,--gc-sections&quot;</span>, &#123;force = <span class="literal">true</span>&#125;)</span><br><span class="line">add_syslinks(<span class="string">&quot;c&quot;</span>, <span class="string">&quot;gcc&quot;</span>, <span class="string">&quot;nosys&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 设置全局宏定义</span></span><br><span class="line">add_defines(<span class="string">&quot;STM32H7&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 设置全局用户头文件搜索路径</span></span><br><span class="line">add_includedirs(<span class="string">&quot;user/inc&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 目标程序</span></span><br><span class="line">target(<span class="string">&quot;gpio-lib&quot;</span>)</span><br><span class="line">    <span class="comment">-- 设置目标类型为二进制，就是编译输出可执行文件</span></span><br><span class="line">    set_kind(<span class="string">&quot;binary&quot;</span>)</span><br><span class="line">    add_files(<span class="string">&quot;user/src/*.c&quot;</span>)</span><br><span class="line">    set_targetdir(<span class="string">&quot;$(projectdir)/bin&quot;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="comment">-- 依赖于 lib 目标，这个目标在后面定义</span></span><br><span class="line">    add_deps(<span class="string">&quot;lib&quot;</span>)</span><br><span class="line"></span><br><span class="line">    on_load(<span class="function"><span class="keyword">function</span> <span class="params">(target)</span></span></span><br><span class="line">        target:set(<span class="string">&quot;filename&quot;</span>, target:name() .. <span class="string">&quot;.elf&quot;</span>)</span><br><span class="line">        <span class="comment">-- 生成 map 文件并带 cref，显示 cross reference</span></span><br><span class="line">        target:add(<span class="string">&quot;ldflags&quot;</span>,</span><br><span class="line">            <span class="string">&quot;-Wl,-Map=&quot;</span> .. <span class="built_in">path</span>.join(target:targetdir(), target:name() .. <span class="string">&quot;.map&quot;</span>) .. <span class="string">&quot;,-cref&quot;</span>,</span><br><span class="line">            &#123;force = <span class="literal">true</span>&#125;</span><br><span class="line">        )</span><br><span class="line">    <span class="keyword">end</span>)</span><br><span class="line"></span><br><span class="line">    <span class="comment">-- 生成额外文件</span></span><br><span class="line">    after_build(<span class="function"><span class="keyword">function</span> <span class="params">(target)</span></span></span><br><span class="line">        <span class="keyword">local</span> elf = target:targetfile()</span><br><span class="line">        <span class="keyword">local</span> bindir = target:targetdir()</span><br><span class="line">        <span class="keyword">local</span> name = target:name()</span><br><span class="line"></span><br><span class="line">        <span class="comment">-- 常见格式</span></span><br><span class="line">        <span class="built_in">os</span>.execv(<span class="string">&quot;arm-none-eabi-objcopy&quot;</span>, &#123;<span class="string">&quot;-Obinary&quot;</span>, elf, <span class="built_in">path</span>.join(bindir, name .. <span class="string">&quot;.bin&quot;</span>)&#125;)</span><br><span class="line">        <span class="built_in">os</span>.execv(<span class="string">&quot;arm-none-eabi-objcopy&quot;</span>, &#123;<span class="string">&quot;-Oihex&quot;</span>,   elf, <span class="built_in">path</span>.join(bindir, name .. <span class="string">&quot;.hex&quot;</span>)&#125;)</span><br><span class="line">        <span class="built_in">os</span>.execv(<span class="string">&quot;arm-none-eabi-objcopy&quot;</span>, &#123;<span class="string">&quot;-Osrec&quot;</span>,   elf, <span class="built_in">path</span>.join(bindir, name .. <span class="string">&quot;.srec&quot;</span>)&#125;)</span><br><span class="line">        <span class="built_in">os</span>.execv(<span class="string">&quot;arm-none-eabi-objdump&quot;</span>, &#123;<span class="string">&quot;-S&quot;</span>, elf&#125;, &#123;<span class="built_in">stdout</span> = <span class="built_in">path</span>.join(bindir, name .. <span class="string">&quot;.list&quot;</span>)&#125;)</span><br><span class="line"></span><br><span class="line">        <span class="comment">-- 🔥 符号表 (对应 MDK Image Symbol Table)</span></span><br><span class="line">        <span class="built_in">os</span>.execv(<span class="string">&quot;arm-none-eabi-nm&quot;</span>, &#123;<span class="string">&quot;-n&quot;</span>, elf&#125;, &#123;<span class="built_in">stdout</span> = <span class="built_in">path</span>.join(bindir, name .. <span class="string">&quot;.sym&quot;</span>)&#125;)</span><br><span class="line"></span><br><span class="line">        <span class="comment">-- 🔥 段大小统计 (对应 MDK Image component sizes)</span></span><br><span class="line">        <span class="built_in">os</span>.execv(<span class="string">&quot;arm-none-eabi-size&quot;</span>, &#123;<span class="string">&quot;-B&quot;</span>, elf&#125;, &#123;<span class="built_in">stdout</span> = <span class="built_in">path</span>.join(bindir, name .. <span class="string">&quot;.size&quot;</span>)&#125;)</span><br><span class="line">    <span class="keyword">end</span>)</span><br><span class="line"></span><br><span class="line">    on_clean(<span class="function"><span class="keyword">function</span> <span class="params">(target)</span></span></span><br><span class="line">        <span class="built_in">os</span>.rm(<span class="string">&quot;build&quot;</span>)</span><br><span class="line">        <span class="built_in">os</span>.rm(<span class="string">&quot;bin&quot;</span>)</span><br><span class="line">    <span class="keyword">end</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">-- lib 目标，负责整合所有第三方库 (libopencm3除外)</span></span><br><span class="line">target(<span class="string">&quot;lib&quot;</span>)</span><br><span class="line">    <span class="comment">-- 设置目标类型为phony，即伪目标，不会生成文件，仅负责将不同的目标整合到一起</span></span><br><span class="line">    set_kind(<span class="string">&quot;phony&quot;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="comment">-- 依赖于 easybutton 目标，之后有新的目标，都可以添加到这里</span></span><br><span class="line">    add_deps(<span class="string">&quot;lib-ebtn&quot;</span>)</span><br><span class="line"></span><br><span class="line">target(<span class="string">&quot;lib-ebtn&quot;</span>)</span><br><span class="line">    <span class="comment">-- 设置目标类型为静态库，依赖于他的目标会链接这个库</span></span><br><span class="line">    set_kind(<span class="string">&quot;static&quot;</span>)</span><br><span class="line">    add_includedirs(<span class="string">&quot;lib/ebtn&quot;</span>, &#123;public = <span class="literal">true</span>&#125;)</span><br><span class="line">    add_files(<span class="string">&quot;lib/ebtn/*.c&quot;</span>)</span><br></pre></td></tr></table></figure><h2 id="代码实现"><a href="#代码实现" class="headerlink" title="代码实现"></a>代码实现</h2><p>为了方便管理，我在 ebtn.c 同路径下创建了 ebtn_cb.c 和 ebtn_cb.h 文件，用于存放 easy_button 的具体实现函数。</p><h3 id="初始化"><a href="#初始化" class="headerlink" title="初始化"></a>初始化</h3><p>easy_button 的初始化主要包含：</p><ul><li>按键的时间配置参数（如消抖时间、长按时间等）</li><li>按键定义（区分按键）</li><li>实现按键状态读取函数</li><li>获取系统时间函数</li><li>实现事件回调函数</li></ul><h4 id="时间配置"><a href="#时间配置" class="headerlink" title="时间配置"></a>时间配置</h4><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></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> <span class="type">const</span> <span class="type">ebtn_btn_param_t</span> default_param = EBTN_PARAMS_INIT</span><br><span class="line">    (</span><br><span class="line">      <span class="number">20</span>,   <span class="comment">// 按下去抖时间(ms)</span></span><br><span class="line">      <span class="number">20</span>,   <span class="comment">// 释放去抖时间(ms)</span></span><br><span class="line">      <span class="number">20</span>,   <span class="comment">// 点击最短时间(ms)</span></span><br><span class="line">      <span class="number">300</span>,  <span class="comment">// 点击最长时间(ms)</span></span><br><span class="line">      <span class="number">200</span>,  <span class="comment">// 连击间隔最大值(ms)</span></span><br><span class="line">      <span class="number">500</span>,  <span class="comment">// 长按KEEPALIVE间隔(ms)</span></span><br><span class="line">      <span class="number">10</span>    <span class="comment">// 最大连续点击次数</span></span><br><span class="line">    );</span><br></pre></td></tr></table></figure><p>easy_button 可以给每一个不同的按键设置不同的时间参数，这里我就定义了一个变量 default_param，之后所有的按键都使用这个参数。</p><h4 id="按键定义"><a href="#按键定义" class="headerlink" title="按键定义"></a>按键定义</h4><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">enum</span></span></span><br><span class="line"><span class="class">&#123;</span></span><br><span class="line">  USER_BUTTON1 = <span class="number">0</span>,</span><br><span class="line">  USER_BUTTON_MAX,</span><br><span class="line">&#125; <span class="type">user_button_t</span>;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> <span class="type">ebtn_btn_t</span> btns[] =</span><br><span class="line">&#123;</span><br><span class="line">  EBTN_BUTTON_INIT(USER_BUTTON1, &amp;default_param),</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>用枚举来给我们的按键定义一个编号，实际上你不用枚举也是可以的，不过这样更方便管理。<br>然后使用 EBTN_BUTTON_INIT 宏来初始化我们的按键，第一个参数是按键编号，第二个参数是之前定义的时间参数。</p><h4 id="按键状态读取函数"><a href="#按键状态读取函数" class="headerlink" title="按键状态读取函数"></a>按键状态读取函数</h4><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> <span class="type">uint8_t</span> <span class="title function_">prv_btn_get_state</span><span class="params">(<span class="keyword">struct</span> ebtn_btn* btn)</span></span><br><span class="line">&#123;</span><br><span class="line">  <span class="keyword">switch</span>(btn-&gt;key_id)</span><br><span class="line">  &#123;</span><br><span class="line">  <span class="keyword">case</span> USER_BUTTON1:</span><br><span class="line">    <span class="keyword">return</span> gpio_get(GPIOC, GPIO13) == GPIO13;</span><br><span class="line">  <span class="keyword">default</span>:</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">&#125;</span><br></pre></td></tr></table></figure><p>我们自己实现一个参数为 struct ebtn_btn* btn 的函数，根据按键编号来读取按键状态，我的按键按下时为高电平，所以我们判断是否为高电平，然后返回 1（表示true，按下了按键） 或者 0（表示false，按键没有被按下）。</p><h4 id="获取系统时间函数"><a href="#获取系统时间函数" class="headerlink" title="获取系统时间函数"></a>获取系统时间函数</h4><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="type">static</span> <span class="type">uint32_t</span> <span class="title function_">ebtn_user_get_tick</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> systick;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在前面 libopencm3 的文章中，我们初始化了 SysTick，并且实现了对应的延时功能，在这里我们包含 systick.h 就可以直接使用 systick 变量，获取系统运行的时间（毫秒）。</p><h4 id="实现事件回调函数"><a href="#实现事件回调函数" class="headerlink" title="实现事件回调函数"></a>实现事件回调函数</h4><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> <span class="type">void</span> <span class="title function_">prv_btn_event</span><span class="params">(<span class="keyword">struct</span> ebtn_btn* btn, <span class="type">ebtn_evt_t</span> evt)</span></span><br><span class="line">&#123;</span><br><span class="line">  <span class="keyword">switch</span>(evt)</span><br><span class="line">  &#123;</span><br><span class="line">  <span class="keyword">case</span> EBTN_EVT_ONPRESS:</span><br><span class="line"></span><br><span class="line">    <span class="keyword">break</span>;</span><br><span class="line">  <span class="keyword">case</span> EBTN_EVT_ONRELEASE:</span><br><span class="line"></span><br><span class="line">    <span class="keyword">break</span>;</span><br><span class="line">  <span class="keyword">case</span> EBTN_EVT_ONCLICK:</span><br><span class="line">    <span class="keyword">if</span>((ebtn_click_get_count(btn) == <span class="number">2</span>) &amp;&amp; (btn-&gt;key_id == USER_BUTTON1))</span><br><span class="line">    &#123;</span><br><span class="line">      gpio_toggle(GPIOE, GPIO3);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//printf(&quot;[BTN %d] Clicked, count=%d\r\n&quot;, btn-&gt;key_id, ebtn_click_get_count(btn));</span></span><br><span class="line">    <span class="keyword">break</span>;</span><br><span class="line">  <span class="keyword">case</span> EBTN_EVT_KEEPALIVE:</span><br><span class="line">    <span class="keyword">if</span>(btn-&gt;key_id == USER_BUTTON1)</span><br><span class="line">    &#123;</span><br><span class="line">      gpio_toggle(GPIOE, GPIO3);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//printf(&quot;[BTN %d] Keepalive, cnt=%d\r\n&quot;, btn-&gt;key_id, ebtn_keepalive_get_count(btn));</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">break</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们自己实现一个参数为 struct ebtn_btn* btn 和 ebtn_evt_t evt 的函数，我们根据事件类型来处理不同的按键事件，在不同的事件中通过判断按键编号来执行不同的操作。</p><p>其中：</p><ul><li>EBTN_EVT_ONPRESS：按键按下事件，当按键按下时触发。</li><li>EBTN_EVT_ONRELEASE：按键释放事件，当按键释放时触发。</li><li>EBTN_EVT_ONCLICK：按键点击事件，当按键被点击时触发，easy_button 将单击和多击进行了合并，统一都叫 EBTN_EVT_ONCLICK，在代码中可以看到，我们可以通过使用 ebtn_click_get_count() 函数来获取连续点击的次数，从而区分单击和双击以及更多的点击次数。</li><li>EBTN_EVT_KEEPALIVE：按键长按事件，当按键被长按时持续触发，执行的周期和时间参数中的 KEEPALIVE 间隔一致，我千面写的是500，那么长按期间，每隔500ms就会触发一次 EBTN_EVT_KEEPALIVE 事件。<ul><li>那么如果想实现长按后只执行一次操作，应该怎么处理呢？在代码中其实你也看到了，注释掉的部分有一个函数 ebtn_keepalive_get_count，通过这个函数，我们可以获取到长按期间，keepalive 事件触发的次数，当这个次数为1时，表示长按后第一次触发 keepalive 事件，那么我们就可以执行我们想要执行的操作，之后值为2，3，4…，表示 keepalive 事件被多次触发，那么我们简单的通过 if 判断就可以不再执行长按的操作了。</li></ul></li></ul><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></pre></td><td class="code"><pre><span class="line"><span class="type">void</span> <span class="title function_">ebtn_user_init</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">  ebtn_init(btns,</span><br><span class="line">            EBTN_ARRAY_SIZE(btns),</span><br><span class="line">            <span class="literal">NULL</span>, <span class="number">0</span>,                    <span class="comment">// 无组合键</span></span><br><span class="line">            prv_btn_get_state,</span><br><span class="line">            prv_btn_event);</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_">ebtn_user_process</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">  ebtn_process(ebtn_user_get_tick());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>ebtn_user_init 就是 easy_button 的初始化函数，第一个参数是之前定义的按键数组，包含了按键 ID和按键时间参数，第二个参数是按键数组的长度，第三个参数是组合键数组，第四个参数是组合键数组的长度，这里我们不需要组合键，所以都设置为 NULL 和 0，第五个参数是按键状态读取函数，第六个参数是事件回调函数。</p><p>ebtn_user_process 是用于周期性调用的函数，进行按键的状态读取、消抖、状态判断、事件执行等，它可以在定时器中断中调用，也可以在主循环中调用，如果你使用 RTOS，那么还可以单独开一个线程，调用这个函数，我这里就放在主循环中调用了。</p><h3 id="预期效果"><a href="#预期效果" class="headerlink" title="预期效果"></a>预期效果</h3><p>对于已经定义的 USER_BUTTON1，我通过按键状态读取函数将 PC11 与其关联起来，然后在事件回调函数中，实现了点击事件和长按事件，当双击按钮时，LED 灯会闪烁，当长按按钮时，LED 灯会每隔500ms闪烁一次，直到松开按键。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>通过使用 easy_button 库，我们可以很方便的实现按键的点击、长按、连击等功能，并且可以很方便的扩展到多个按键，只需要在 ebtn_btn_t 数组中添加按键对象即可，非常方便。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;在上一篇中，我们使用 HAL 库实现了 easy_button 的按键处理，这一篇我们使用 libopencm3 库实现同样的功能。&lt;/p&gt;
&lt;p&gt;大体上思路和 HAL 库实现是一样的，只是使用 libopencm3 库的 API 来实现。&lt;/p&gt;
&lt;p&gt;本文代码仓库：&lt;a</summary>
      
    
    
    
    <category term="mcu" scheme="https://blog.orangetime.top/categories/mcu/"/>
    
    
  </entry>
  
  <entry>
    <title>STM32H7开发笔记(五):GPIO-输入处理-HAL库实现</title>
    <link href="https://blog.orangetime.top/2025/11/29/mcu/h7-gpio-lib-hal/"/>
    <id>https://blog.orangetime.top/2025/11/29/mcu/h7-gpio-lib-hal/</id>
    <published>2025-11-28T18:47:38.369Z</published>
    <updated>2025-11-29T16:55:40.309Z</updated>
    
    <content type="html"><![CDATA[<p>在上一篇中，我简单介绍了一下 easy_button，这一篇，我们使用 HAL 库结合 easy_button 来实现一个简单的按键输入例子。</p><h2 id="工程导入"><a href="#工程导入" class="headerlink" title="工程导入"></a>工程导入</h2><blockquote><p>在这里我们依然沿用之前的工程，如果你打算新建工程，或者说没有看前面的文章，请先看 <a href="/2025/09/24/mcu/h7-gpio-hal/">STM32H7开发笔记(二):GPIO-HAL库实现</a></p></blockquote><h3 id="下载-easy-button"><a href="#下载-easy-button" class="headerlink" title="下载 easy_button"></a>下载 easy_button</h3><ul><li>Github仓库：<a class="link"   href="https://github.com/bobwenstudy/easy_button" >https://github.com/bobwenstudy/easy_button<i class="fas fa-external-link-alt"></i></a></li></ul><blockquote><p>考虑到国内网络问题，部分读者可能无法访问 Github，所以我自己部署了 Gitea，将 easy_button 仓库同步到了我的 Gitea 服务器上，地址：<a class="link"   href="https://git.orangetime.top/EMTime/easy_button" >https://git.orangetime.top/EMTime/easy_button<i class="fas fa-external-link-alt"></i></a></p></blockquote><h3 id="添加-easy-button-文件"><a href="#添加-easy-button-文件" class="headerlink" title="添加 easy_button 文件"></a>添加 easy_button 文件</h3><p>进入下载好的 easy_button 路径，进入 ebtn 文件夹，将其中的所有文件复制到我们的工程目录中，在 MDK 软件中添加对应的源码文件，同时设置好头文件搜索路径，如下图：</p><center>   <img     src="https://emtime-picture.cn-nb1.rains3.com/2025/11/29/6929f6af7874b.png"  ></center><h2 id="代码实现"><a href="#代码实现" class="headerlink" title="代码实现"></a>代码实现</h2><p>为了方便管理，我在 ebtn.c 同路径下创建了 ebtn_cb.c 和 ebtn_cb.h 文件，用于存放 easy_button 的具体实现函数。</p><h3 id="初始化"><a href="#初始化" class="headerlink" title="初始化"></a>初始化</h3><p>easy_button 的初始化主要包含：</p><ul><li>按键的时间配置参数（如消抖时间、长按时间等）</li><li>按键定义（区分按键）</li><li>实现按键状态读取函数</li><li>获取系统时间函数</li><li>实现事件回调函数</li></ul><h4 id="时间配置"><a href="#时间配置" class="headerlink" title="时间配置"></a>时间配置</h4><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></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> <span class="type">const</span> <span class="type">ebtn_btn_param_t</span> default_param = EBTN_PARAMS_INIT</span><br><span class="line">    (</span><br><span class="line">      <span class="number">20</span>,   <span class="comment">// 按下去抖时间(ms)</span></span><br><span class="line">      <span class="number">20</span>,   <span class="comment">// 释放去抖时间(ms)</span></span><br><span class="line">      <span class="number">20</span>,   <span class="comment">// 点击最短时间(ms)</span></span><br><span class="line">      <span class="number">300</span>,  <span class="comment">// 点击最长时间(ms)</span></span><br><span class="line">      <span class="number">200</span>,  <span class="comment">// 连击间隔最大值(ms)</span></span><br><span class="line">      <span class="number">500</span>,  <span class="comment">// 长按KEEPALIVE间隔(ms)</span></span><br><span class="line">      <span class="number">10</span>    <span class="comment">// 最大连续点击次数</span></span><br><span class="line">    );</span><br></pre></td></tr></table></figure><p>easy_button 可以给每一个不同的按键设置不同的时间参数，这里我就定义了一个变量 default_param，之后所有的按键都使用这个参数。</p><h4 id="按键定义"><a href="#按键定义" class="headerlink" title="按键定义"></a>按键定义</h4><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">enum</span></span></span><br><span class="line"><span class="class">&#123;</span></span><br><span class="line">  USER_BUTTON1 = <span class="number">0</span>,</span><br><span class="line">  USER_BUTTON_MAX,</span><br><span class="line">&#125; <span class="type">user_button_t</span>;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> <span class="type">ebtn_btn_t</span> btns[] =</span><br><span class="line">&#123;</span><br><span class="line">  EBTN_BUTTON_INIT(USER_BUTTON1, &amp;default_param),</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>用枚举来给我们的按键定义一个编号，实际上你不用枚举也是可以的，不过这样更方便管理。<br>然后使用 EBTN_BUTTON_INIT 宏来初始化我们的按键，第一个参数是按键编号，第二个参数是之前定义的时间参数。</p><h4 id="按键状态读取函数"><a href="#按键状态读取函数" class="headerlink" title="按键状态读取函数"></a>按键状态读取函数</h4><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> <span class="type">uint8_t</span> <span class="title function_">prv_btn_get_state</span><span class="params">(<span class="keyword">struct</span> ebtn_btn* btn)</span></span><br><span class="line">&#123;</span><br><span class="line">  <span class="keyword">switch</span>(btn-&gt;key_id)</span><br><span class="line">  &#123;</span><br><span class="line">  <span class="keyword">case</span> USER_BUTTON1:</span><br><span class="line">    <span class="keyword">return</span> HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) == GPIO_PIN_SET;</span><br><span class="line">  <span class="keyword">default</span>:</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">&#125;</span><br></pre></td></tr></table></figure><p>我们自己实现一个参数为 struct ebtn_btn* btn 的函数，根据按键编号来读取按键状态，我的按键按下时为高电平，所以我们判断是否为高电平，然后返回 1（表示true，按下了按键） 或者 0（表示false，按键没有被按下）。</p><h4 id="获取系统时间函数"><a href="#获取系统时间函数" class="headerlink" title="获取系统时间函数"></a>获取系统时间函数</h4><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="type">static</span> <span class="type">uint32_t</span> <span class="title function_">ebtn_user_get_tick</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> HAL_GetTick();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个函数很简单，直接调用 HAL 库提供的 HAL_GetTick() 函数即可，或者也可以直接获取 uwTick这个值，看你个人的习惯。</p><h4 id="实现事件回调函数"><a href="#实现事件回调函数" class="headerlink" title="实现事件回调函数"></a>实现事件回调函数</h4><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> <span class="type">void</span> <span class="title function_">prv_btn_event</span><span class="params">(<span class="keyword">struct</span> ebtn_btn* btn, <span class="type">ebtn_evt_t</span> evt)</span></span><br><span class="line">&#123;</span><br><span class="line">  <span class="keyword">switch</span>(evt)</span><br><span class="line">  &#123;</span><br><span class="line">  <span class="keyword">case</span> EBTN_EVT_ONPRESS:</span><br><span class="line"></span><br><span class="line">    <span class="keyword">break</span>;</span><br><span class="line">  <span class="keyword">case</span> EBTN_EVT_ONRELEASE:</span><br><span class="line"></span><br><span class="line">    <span class="keyword">break</span>;</span><br><span class="line">  <span class="keyword">case</span> EBTN_EVT_ONCLICK:</span><br><span class="line">    <span class="keyword">if</span>((ebtn_click_get_count(btn) == <span class="number">2</span>) &amp;&amp; (btn-&gt;key_id == USER_BUTTON1))</span><br><span class="line">    &#123;</span><br><span class="line">      HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//printf(&quot;[BTN %d] Clicked, count=%d\r\n&quot;, btn-&gt;key_id, ebtn_click_get_count(btn));</span></span><br><span class="line">    <span class="keyword">break</span>;</span><br><span class="line">  <span class="keyword">case</span> EBTN_EVT_KEEPALIVE:</span><br><span class="line">    <span class="keyword">if</span>(btn-&gt;key_id == USER_BUTTON1)</span><br><span class="line">    &#123;</span><br><span class="line">      HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//printf(&quot;[BTN %d] Keepalive, cnt=%d\r\n&quot;, btn-&gt;key_id, ebtn_keepalive_get_count(btn));</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">break</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们自己实现一个参数为 struct ebtn_btn* btn 和 ebtn_evt_t evt 的函数，我们根据事件类型来处理不同的按键事件，在不同的事件中通过判断按键编号来执行不同的操作。</p><p>其中：</p><ul><li>EBTN_EVT_ONPRESS：按键按下事件，当按键按下时触发。</li><li>EBTN_EVT_ONRELEASE：按键释放事件，当按键释放时触发。</li><li>EBTN_EVT_ONCLICK：按键点击事件，当按键被点击时触发，easy_button 将单击和多击进行了合并，统一都叫 EBTN_EVT_ONCLICK，在代码中可以看到，我们可以通过使用 ebtn_click_get_count() 函数来获取连续点击的次数，从而区分单击和双击以及更多的点击次数。</li><li>EBTN_EVT_KEEPALIVE：按键长按事件，当按键被长按时持续触发，执行的周期和时间参数中的 KEEPALIVE 间隔一致，我千面写的是500，那么长按期间，每隔500ms就会触发一次 EBTN_EVT_KEEPALIVE 事件。<ul><li>那么如果想实现长按后只执行一次操作，应该怎么处理呢？在代码中其实你也看到了，注释掉的部分有一个函数 ebtn_keepalive_get_count，通过这个函数，我们可以获取到长按期间，keepalive 事件触发的次数，当这个次数为1时，表示长按后第一次触发 keepalive 事件，那么我们就可以执行我们想要执行的操作，之后值为2，3，4…，表示 keepalive 事件被多次触发，那么我们简单的通过 if 判断就可以不再执行长按的操作了。</li></ul></li></ul><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></pre></td><td class="code"><pre><span class="line"><span class="type">void</span> <span class="title function_">ebtn_user_init</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">  ebtn_init(btns,</span><br><span class="line">            EBTN_ARRAY_SIZE(btns),</span><br><span class="line">            <span class="literal">NULL</span>, <span class="number">0</span>,                    <span class="comment">// 无组合键</span></span><br><span class="line">            prv_btn_get_state,</span><br><span class="line">            prv_btn_event);</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_">ebtn_user_process</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">  ebtn_process(ebtn_user_get_tick());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>ebtn_user_init 就是 easy_button 的初始化函数，第一个参数是之前定义的按键数组，包含了按键 ID和按键时间参数，第二个参数是按键数组的长度，第三个参数是组合键数组，第四个参数是组合键数组的长度，这里我们不需要组合键，所以都设置为 NULL 和 0，第五个参数是按键状态读取函数，第六个参数是事件回调函数。</p><p>ebtn_user_process 是用于周期性调用的函数，进行按键的状态读取、消抖、状态判断、事件执行等，它可以在定时器中断中调用，也可以在主循环中调用，如果你使用 RTOS，那么还可以单独开一个线程，调用这个函数，我这里就放在主循环中调用了。</p><h3 id="预期效果"><a href="#预期效果" class="headerlink" title="预期效果"></a>预期效果</h3><p>对于已经定义的 USER_BUTTON1，我通过按键状态读取函数将 PC11 与其关联起来，然后在事件回调函数中，实现了点击事件和长按事件，当双击按钮时，LED 灯会闪烁，当长按按钮时，LED 灯会每隔500ms闪烁一次，直到松开按键。</p><h2 id="完整代码"><a href="#完整代码" class="headerlink" title="完整代码"></a>完整代码</h2><h3 id="ebtn-cb-c"><a href="#ebtn-cb-c" class="headerlink" title="ebtn_cb.c"></a>ebtn_cb.c</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><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></pre></td><td class="code"><pre><span class="line">include <span class="string">&quot;ebtn_cb.h&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/* ---------------- 按钮参数配置 ---------------- */</span></span><br><span class="line"><span class="type">static</span> <span class="type">const</span> <span class="type">ebtn_btn_param_t</span> default_param = EBTN_PARAMS_INIT</span><br><span class="line">    (</span><br><span class="line">      <span class="number">20</span>,   <span class="comment">// 按下去抖时间(ms)</span></span><br><span class="line">      <span class="number">20</span>,   <span class="comment">// 释放去抖时间(ms)</span></span><br><span class="line">      <span class="number">20</span>,   <span class="comment">// 点击最短时间(ms)</span></span><br><span class="line">      <span class="number">300</span>,  <span class="comment">// 点击最长时间(ms)</span></span><br><span class="line">      <span class="number">200</span>,  <span class="comment">// 连击间隔最大值(ms)</span></span><br><span class="line">      <span class="number">500</span>,  <span class="comment">// 长按KEEPALIVE间隔(ms)</span></span><br><span class="line">      <span class="number">10</span>    <span class="comment">// 最大连续点击次数</span></span><br><span class="line">    );</span><br><span class="line"></span><br><span class="line"><span class="comment">/* ---------------- 按钮ID定义 ---------------- */</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">enum</span></span></span><br><span class="line"><span class="class">&#123;</span></span><br><span class="line">  USER_BUTTON1 = <span class="number">0</span>,</span><br><span class="line">  USER_BUTTON_MAX,</span><br><span class="line">&#125; <span class="type">user_button_t</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* ---------------- 按钮对象 ---------------- */</span></span><br><span class="line"><span class="type">static</span> <span class="type">ebtn_btn_t</span> btns[] =</span><br><span class="line">&#123;</span><br><span class="line">  EBTN_BUTTON_INIT(USER_BUTTON1, &amp;default_param),</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="type">static</span> <span class="type">uint8_t</span> <span class="title function_">prv_btn_get_state</span><span class="params">(<span class="keyword">struct</span> ebtn_btn* btn)</span></span><br><span class="line">&#123;</span><br><span class="line">  <span class="keyword">switch</span>(btn-&gt;key_id)</span><br><span class="line">  &#123;</span><br><span class="line">  <span class="keyword">case</span> USER_BUTTON1:</span><br><span class="line">    <span class="comment">// 如果按下时为高电平</span></span><br><span class="line">    <span class="keyword">return</span> HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) == GPIO_PIN_SET;</span><br><span class="line">  <span class="keyword">default</span>:</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">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* ---------------- 事件回调函数 ---------------- */</span></span><br><span class="line"><span class="type">static</span> <span class="type">void</span> <span class="title function_">prv_btn_event</span><span class="params">(<span class="keyword">struct</span> ebtn_btn* btn, <span class="type">ebtn_evt_t</span> evt)</span></span><br><span class="line">&#123;</span><br><span class="line">  <span class="keyword">switch</span>(evt)</span><br><span class="line">  &#123;</span><br><span class="line">  <span class="keyword">case</span> EBTN_EVT_ONPRESS:</span><br><span class="line"></span><br><span class="line">    <span class="keyword">break</span>;</span><br><span class="line">  <span class="keyword">case</span> EBTN_EVT_ONRELEASE:</span><br><span class="line"></span><br><span class="line">    <span class="keyword">break</span>;</span><br><span class="line">  <span class="keyword">case</span> EBTN_EVT_ONCLICK:</span><br><span class="line">    <span class="keyword">if</span>((ebtn_click_get_count(btn) == <span class="number">2</span>) &amp;&amp; (btn-&gt;key_id == USER_BUTTON1))</span><br><span class="line">    &#123;</span><br><span class="line">      HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//printf(&quot;[BTN %d] Clicked, count=%d\r\n&quot;, btn-&gt;key_id, ebtn_click_get_count(btn));</span></span><br><span class="line">    <span class="keyword">break</span>;</span><br><span class="line">  <span class="keyword">case</span> EBTN_EVT_KEEPALIVE:</span><br><span class="line">    <span class="keyword">if</span>(btn-&gt;key_id == USER_BUTTON1)</span><br><span class="line">    &#123;</span><br><span class="line">      HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//printf(&quot;[BTN %d] Keepalive, cnt=%d\r\n&quot;, btn-&gt;key_id, ebtn_keepalive_get_count(btn));</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">break</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="type">static</span> <span class="type">uint32_t</span> <span class="title function_">ebtn_user_get_tick</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> HAL_GetTick();</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="type">void</span> <span class="title function_">ebtn_user_init</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">  ebtn_init(btns,</span><br><span class="line">            EBTN_ARRAY_SIZE(btns),</span><br><span class="line">            <span class="literal">NULL</span>, <span class="number">0</span>,                    <span class="comment">// 无组合键</span></span><br><span class="line">            prv_btn_get_state,</span><br><span class="line">            prv_btn_event);</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="type">void</span> <span class="title function_">ebtn_user_process</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">  ebtn_process(ebtn_user_get_tick());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="ebtn-cb-h"><a href="#ebtn-cb-h" class="headerlink" title="ebtn_cb.h"></a>ebtn_cb.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><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="meta">#<span class="keyword">ifndef</span> __EBTN_CB_H_</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> __EBTN_CB_H_</span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;gpio.h&quot;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;ebtn.h&quot;</span></span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">ifdef</span> __cplusplus</span></span><br><span class="line"><span class="keyword">extern</span> <span class="string">&quot;C&quot;</span> &#123;</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="type">void</span> <span class="title function_">ebtn_user_init</span><span class="params">(<span class="type">void</span>)</span>;</span><br><span class="line"><span class="type">void</span> <span class="title function_">ebtn_user_process</span><span class="params">(<span class="type">void</span>)</span>;</span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">ifdef</span> __cplusplus</span></span><br><span class="line">&#125;</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">endif</span></span></span><br></pre></td></tr></table></figure><h3 id="main-c"><a href="#main-c" class="headerlink" title="main.c"></a>main.c</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></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">...</span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;ebtn_cb.h&quot;</span></span></span><br><span class="line">...</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">  ebtn_user_init();</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">    ebtn_user_process();</span><br><span class="line">    HAL_Delay(<span class="number">5</span>);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>通过使用 easy_button 库，我们可以很方便的实现按键的点击、长按、连击等功能，并且可以很方便的扩展到多个按键，只需要在 ebtn_btn_t 数组中添加按键对象即可，非常方便。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;在上一篇中，我简单介绍了一下 easy_button，这一篇，我们使用 HAL 库结合 easy_button 来实现一个简单的按键输入例子。&lt;/p&gt;
&lt;h2 id=&quot;工程导入&quot;&gt;&lt;a href=&quot;#工程导入&quot; class=&quot;headerlink&quot; title=&quot;工程导入&quot;</summary>
      
    
    
    
    <category term="mcu" scheme="https://blog.orangetime.top/categories/mcu/"/>
    
    
  </entry>
  
  <entry>
    <title>Docker 部署个人网盘</title>
    <link href="https://blog.orangetime.top/2025/10/15/docker/Docker-Storage/"/>
    <id>https://blog.orangetime.top/2025/10/15/docker/Docker-Storage/</id>
    <published>2025-10-15T09:06:26.682Z</published>
    <updated>2025-10-24T19:43:16.503Z</updated>
    
    <content type="html"><![CDATA[<p>可道云官网：<a class="link"   href="https://kodcloud.com/" >https://kodcloud.com<i class="fas fa-external-link-alt"></i></a><br>小兔互联官网：<a class="link"   href="https://moebun.com/aff/INILUDUF" >https://moebun.com/aff/INILUDUF<i class="fas fa-external-link-alt"></i></a></p><p>平时写博客的时候，我经常会遇到一个小麻烦：<br>有时候想给大家分享一些工具、项目资源或者其它一些文件，但这些文件要么太大不方便放在仓库里，要么托管在网盘上又容易过期或者限速。久而久之，我就萌生了一个想法——干脆自己搭一个网盘吧！</p><p>不过我又不想再去折腾复杂的文件系统，于是想到用<strong>对象存储</strong>来做底层存储，既省心又稳定；再配合一个界面好看、功能齐全的文件管理面板，比如<strong>可道云（KodCloud）</strong>，就能轻松实现上传、预览、分享等功能。</p><p>这篇文章就来记录一下我用 <strong>Docker 部署可道云 + 对象存储</strong>的全过程，让博客文件分享这件小事变得优雅又高效。</p><h2 id="为什么选择可道云-对象存储的组合"><a href="#为什么选择可道云-对象存储的组合" class="headerlink" title="为什么选择可道云 + 对象存储的组合"></a>为什么选择可道云 + 对象存储的组合</h2><p>其实在写这个文章之前，或者说有了文件分享的需求之后，我其实已经尝试过很多种方案了，比如：</p><ul><li>国内的网盘：百度网盘、阿里云盘、腾讯微云等，这些网盘都非常省心，但是缺点也很明显，就是<strong>限速、限流量、限空间</strong>，以及可能会对部分文件进行审查，导致文件时不时就会失效。此外，网盘并不统一，可能你用的网盘是百度网盘，但是别人用的网盘是阿里云盘，这就导致了分享文件的时候，需要将文件上传到多个网盘，然后再下载下来，非常麻烦。</li><li>自建网盘：比如 Nextcloud、Owncloud 以及可道云等，这些网盘都是开源的，可以自己搭建，自由度比较高，并且服务掌握在自己手里，非常灵活，但是缺点也很明显，就是文件存储于本地，如果<strong>服务器挂了，那么文件也就丢失了</strong>。此外，直接这样分享的话，文件传输速度也会受到服务器带宽的限制（我的小水管只有不到 100 Mbps），如果多个人同时下载，那么速度就会非常慢，严重影响用户体验。</li></ul><p>所以，我最终选择了可道云 + 对象存储的组合，既省心又稳定。</p><ul><li>可道云可以像网盘一样进行文件的共享，其他人可以通过分享链接进行文件的下载，不用在意自己用的什么网盘，只要有安装浏览器，就可以直接下载。</li><li>对象存储通过分布式存储，保证了文件的安全与稳定性，即使服务器挂了，文件也不会丢失，而且<strong>文件传输速度也不会受到服务器带宽的限制</strong>，可以支持高并发的下载，因为文件存储于对象存储中，文件传输速度是由对象存储的带宽决定的，而不是服务器带宽决定的（一般来说，对象存储的带宽都比较高）。</li></ul><blockquote><p>在本文中，将会使用<strong>小兔互联的对象存储</strong>和 <strong>CloudFlare R2 存储</strong>为例子进行说明。</p><ul><li>小兔互联的对象存储是收费的（价格还是比较实惠的），其上游是雨云，服务器位于国内，无论是访问速度还是稳定性都很高，并且默认只会使用你所选择的套餐内的流量，不开启弹性计费的情况下，即使流量被刷干了，也不会产生额外费用，只是会暂停服务，到下个月刷新（不过一般也不会有人去刷流量吧）。</li><li>CloudFlare R2 存储是<strong>免费</strong>的，每个月拥有 10G 的空间（一个月每天占用的最大空间之和的平均数 &lt; 10G 就是免费的，超出会收取额外费用），也有上传和下载的次数限制，其速度在国内也是不错的，这些限制对于个人使用来说，基本上是够用的。</li></ul></blockquote><h2 id="部署可道云"><a href="#部署可道云" class="headerlink" title="部署可道云"></a>部署可道云</h2><p>官方文档：<a class="link"   href="https://docs.kodcloud.com/setup/docker" >https://docs.kodcloud.com/setup/docker<i class="fas fa-external-link-alt"></i></a></p><p>我们按照官方文档的说明，进行以下的操作：</p><ul><li>创建目录，作为可道云容器的映射目录，用于映射可道云的配置文件、数据文件、日志文件以及数据库等。</li></ul><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="comment"># 注意，这里是我自己服务器对应的目录，大家可以根据自己的需求进行修改</span></span><br><span class="line"><span class="built_in">mkdir</span> -p ~/doc/docker/kodcloud</span><br></pre></td></tr></table></figure><ul><li>创建数据库环境变量配置文件 db.env，用于配置可道云的数据库信息，其中内容如下：</li></ul><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">MYSQL_PASSWORD=0YXl7Q^kBGFO*&amp;g8p96^</span><br><span class="line">MYSQL_DATABASE=kodbox</span><br><span class="line">MYSQL_USER=kodbox</span><br></pre></td></tr></table></figure><blockquote><p>MYSQL_PASSWORD 是数据库密码，我生成了一个随机密码，大家可以根据自己的需求进行修改。</p></blockquote><ul><li>创建 compose.yml 文件，用于配置可道云的容器信息，其中内容如下：</li></ul><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></pre></td><td class="code"><pre><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">db:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">mariadb:lts</span></span><br><span class="line">    <span class="attr">command:</span> <span class="string">--transaction-isolation=READ-COMMITTED</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">always</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;./db:/var/lib/mysql&quot;</span>       <span class="comment">#./db是数据库持久化目录，可以修改</span></span><br><span class="line">      <span class="comment"># - &quot;./etc/mysql/conf.d:/etc/mysql/conf.d&quot;       #增加自定义mysql配置</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">MYSQL_ROOT_PASSWORD=0YXl7Q^kBGFO*&amp;g8p96^</span> <span class="comment"># 和上面的MYSQL_PASSWORD保持一致</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">MARIADB_AUTO_UPGRADE=1</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">MARIADB_DISABLE_UPGRADE_BACKUP=1</span></span><br><span class="line">    <span class="attr">env_file:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">db.env</span></span><br><span class="line">      </span><br><span class="line">  <span class="attr">app:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">kodcloud/kodbox</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">always</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="number">80</span><span class="string">:80</span>                       <span class="comment">#左边80是映射到宿主机的端口，可以修改并且建议修改，因为80端口可能被其它服务占用</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;./site:/var/www/html&quot;</span>      <span class="comment">#./site是站点目录位置，可以修改</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">MYSQL_HOST=db</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">REDIS_HOST=redis</span></span><br><span class="line">    <span class="attr">env_file:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">db.env</span></span><br><span class="line">    <span class="attr">depends_on:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">db</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">redis</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">redis:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">redis:alpine</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">always</span></span><br></pre></td></tr></table></figure><blockquote><ul><li>MYSQL_ROOT_PASSWORD 和上面的数据库密码保持一致。</li><li>ports 中的 80 端口可以根据自己的需求进行修改，因为 80 端口可能被其它服务占用，这时候因为端口为非标准的 http 端口，所以需要配置 nginx 或其它反向代理服务器，才能正常访问。</li></ul></blockquote><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 compose up -d</span><br></pre></td></tr></table></figure><h2 id="Caddy-配置反向代理"><a href="#Caddy-配置反向代理" class="headerlink" title="Caddy 配置反向代理"></a>Caddy 配置反向代理</h2><p>我使用的是 caddy 进行了反向代理，具体可以参考我的另一篇文章：<a href="/2024/08/19/linux/Caddy">Caddy反向代理</a>。<br>在这里，我们只需要在 Caddyfile 中增加以下配置即可：</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">你的域名或者端口 &#123;</span><br><span class="line">        tls 证书.crt 证书.key</span><br><span class="line">        reverse_proxy 127.0.0.1:21773</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><ul><li>“你的域名或者端口”需要替换成你自己的域名或者端口。</li><li>21773 是上面 compose.yml 文件中 app 服务的端口，也就是刚刚强调过的可以修改的 80 端口，我的compose.yml 文件中已经修改成了 21773。</li></ul></blockquote><p>配置完之后，重启 caddy 即可生效，使用配置的域名或者端口即可正常访问可道云。</p><h2 id="进入可道云并配置对象存储"><a href="#进入可道云并配置对象存储" class="headerlink" title="进入可道云并配置对象存储"></a>进入可道云并配置对象存储</h2><p>使用配置的域名或者端口访问可道云，进入可道云后，首先需要配置管理员账号和密码，这一部分就不用我过多赘述了，你自己配置好管理员账号和密码后，登录即可。</p><p>进入可道云后，首先需要配置对象存储，因为可道云默认使用的是本地存储，我们需要将其修改为对象存储，才能实现文件的存储和分享。</p><h3 id="小兔互联对象存储配置"><a href="#小兔互联对象存储配置" class="headerlink" title="小兔互联对象存储配置"></a>小兔互联对象存储配置</h3><ol><li>获取访问凭证信息</li></ol><p>进入小兔互联的对象存储控制台，选择我们购买的对象存储实例，点击管理即可看到相关信息，如下：</p><center>   <img     src="https://emtime-picture.cn-nb1.rains3.com/2025/10/15/68ef94526aacf.png"  ></center><ol start="2"><li>创建存储桶并添加网络挂载</li></ol><p>我们点击创建存储桶，自己取一个名字（只能是字母数字和 ‘-’，不能是其它字符），比如我取的是 emtime，所属实例选择我们购买的实例，点击创建即可创建好我们对应的存储桶。</p><p>回到可道云，我们点击左侧的文件管理，然后点击网络挂载，点击新增网络挂载，存储类型选择 <strong>MinIO</strong>。</p><center>   <img     src="https://emtime-picture.cn-nb1.rains3.com/2025/10/15/68ef969b76c3d.png"  ></center><ul><li>名称：自定义，比如我这里填写的是小兔互联。</li><li>空间大小：根据购买的套餐填写，比如我这里填写的是 10，表示 10GB。</li><li><strong>Access Key ID</strong>：对应访问凭证信息图片中 Access Key。</li><li><strong>Access Key Secret</strong>：对应访问凭证信息图片中 Secret Key。</li><li><strong>Bucket 名称</strong>：就是我们自己创建的存储桶名称，比如我这里填写的是 emtime。</li><li><strong>地域节点</strong>：对应访问凭证信息图片中的 API 端点。</li><li>存储区域：无需填写。</li><li>存储目录：默认即可。</li><li>允许文件缩略图：按自己的需求选择，我这里没有开启。</li><li><strong>服务器中转</strong>：如果开启，文件传输会先经过可道云服务器，然后通过服务器再传输到对象存储，一般会占用服务器带宽。除非网络不通或直连不稳定，否则建议关闭，让客户端直接访问对象存储效率会更高。</li><li>设为默认：按自己的需求选择，我这里将小兔存储设置为了默认存储。</li></ul><p>这样我们的对象存储就配置好了，接下来我们就可以上传文件了，上传的文件会自动存储到我们配置的对象存储中。</p><h3 id="CloudFlare-R2-配置"><a href="#CloudFlare-R2-配置" class="headerlink" title="CloudFlare R2 配置"></a>CloudFlare R2 配置</h3><p>说来 CloudFlare 也算是“赛博善人”了，免费功能非常多：<strong>CDN 加速、虚拟组网、内网穿透（Cloudflare Tunnel）</strong>，甚至还有 <strong>SSL 证书自动签发与续期</strong>。对个人开发者或小项目来说，这些工具几乎能解决大部分对外访问和安全的烦恼，省钱又省心。不过比较可惜的是，<strong>在国内用的话体验没那么理想</strong>，偶尔会遇到访问慢或者节点不太稳定的情况。</p><ol><li>订阅 R2 存储并添加存储桶</li></ol><p>如图所示，我们在对应菜单中点击 R2 对象存储，然后在界面的右上角点击创建存储桶，根据图片进行设置即可。</p><center>   <img     src="https://emtime-picture.cn-nb1.rains3.com/2025/10/15/68ef9bf82e610.png"  ></center><center>   <img     src="https://emtime-picture.cn-nb1.rains3.com/2025/10/15/68ef9b0a49542.png"  ></center><ol start="2"><li>获取 API</li></ol><p>CloudFlare 的对象存储提供了<strong>兼容 S3 的 API</strong>，而可道云也支持 S3，所以我们直接创建 S3 API 即可。按照我图片中的步骤进行操作即可。</p><center>   <img     src="https://emtime-picture.cn-nb1.rains3.com/2025/10/15/68ef9c6bf2f30.png"  ></center><center>   <img     src="https://emtime-picture.cn-nb1.rains3.com/2025/10/15/68ef9ca4c9421.png"  ></center><center>   <img     src="https://emtime-picture.cn-nb1.rains3.com/2025/10/15/68ef9de2e5fac.png"  ></center><center>   <img     src="https://emtime-picture.cn-nb1.rains3.com/2025/10/15/68ef9e337e7ad.png"  ></center><p><strong>最后这个图片是我们创建的 API 的信息，将图片中的信息最好全都复制出来，后面配置可道云的时候需要用到。</strong></p><center>   <img     src="https://emtime-picture.cn-nb1.rains3.com/2025/10/15/68ef9e95a9ed4.png"  ></center><ol start="3"><li>可道云添加网络挂载</li></ol><p>和前面差不多，回到可道云，我们点击左侧的文件管理，然后点击网络挂载，点击新增网络挂载，存储类型选择 <strong>S3 存储</strong>。</p><center>   <img     src="https://emtime-picture.cn-nb1.rains3.com/2025/10/15/68efa3c4e7828.png"  ></center><ul><li>名称：自定义，比如我这里填写的是 Cloudflare R2。</li><li>空间大小：CloudFlare R2 对象存储，免费版本也是 10GB，所以这里填写 10。</li><li><strong>Access Key ID</strong>：对应 API 信息中的访问密钥 ID。</li><li><strong>Access Key Secret</strong>：对应 API 信息中的机密访问密钥。</li><li><strong>Bucket 名称</strong>：创建存储桶时填写的名称。</li><li><strong>地域节点</strong>：对应 API 信息中的为 S3 客户端使用管辖权地特定的终结点。</li><li>存储区域：无需填写。</li><li>存储目录：默认即可。</li><li>签名版本：默认 V4 即可。</li><li>允许文件缩略图：按自己的需求选择，我这里没有开启。</li><li><strong>服务器中转</strong>：和之前一样，除非网络不通或直连不稳定，否则建议关闭，让客户端直接访问对象存储效率会更高。</li><li>设为默认：按自己的需求选择，因为我设置了小兔互联的对象存储为默认存储，所以这里就不勾选了。</li></ul><p>这样我们的 CloudFlare R2 对象存储就配置好了，接下来我们就可以上传文件了，上传的文件会自动存储到我们配置的对象存储中。</p><h2 id="测试使用"><a href="#测试使用" class="headerlink" title="测试使用"></a>测试使用</h2><p>配置完成后，我们就可以像使用普通网盘一样上传、下载和分享文件了。<br>上传一个文件后，直接生成分享链接，其他人点击即可下载，整个传输过程都会通过对象存储完成，不再占用你服务器的带宽。</p><center>   <img     src="https://emtime-picture.cn-nb1.rains3.com/2025/10/15/68ef987033751.png"  ></center><p>我分别测试了小兔互联和 Cloudflare R2 两个对象存储的下载速度（本地带宽为 1000 Mbps）：</p><p>小兔互联：</p><center>   <img     src="https://emtime-picture.cn-nb1.rains3.com/2025/10/16/68f0daffbd362.png"  ></center><p>可以看到，小兔互联的下载速度还是很快的，而且完全不占用服务器流量，<strong>体验非常好</strong>。</p><p>CloudFlare R2：</p><center>   <img     src="https://emtime-picture.cn-nb1.rains3.com/2025/10/16/68f0e1fe70dab.png"  ></center><p>Cloudflare R2 的速度也很不错，只是由于节点主要在国外，在国内的访问速度和稳定性稍差一些。<br>不过对于个人使用来说，它依然是一个非常有性价比（甚至可以说“<strong>白嫖友好</strong>”）的方案。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>整体来说，这套 “可道云 + 对象存储” 的组合，<strong>既能拥有类似网盘的便捷体验，又不必担心限速等问题</strong>。</p><p>如果你手上也有一台闲置的服务器，不妨试试这套方案。<br>搭建过程不复杂，但带来的便利却很明显——让博客文件分享更轻、更快，也更优雅。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;可道云官网：&lt;a class=&quot;link&quot;   href=&quot;https://kodcloud.com/&quot; &gt;https://kodcloud.com&lt;i class=&quot;fas fa-external-link-alt&quot;&gt;&lt;/i&gt;&lt;/a&gt;&lt;br&gt;小兔互联官网：&lt;a class</summary>
      
    
    
    
    <category term="docker" scheme="https://blog.orangetime.top/categories/docker/"/>
    
    
  </entry>
  
  <entry>
    <title>STM32H7开发笔记(四):GPIO-按键处理引入</title>
    <link href="https://blog.orangetime.top/2025/10/02/mcu/h7-gpio-lib/"/>
    <id>https://blog.orangetime.top/2025/10/02/mcu/h7-gpio-lib/</id>
    <published>2025-10-02T07:35:51.192Z</published>
    <updated>2025-11-28T17:56:34.449Z</updated>
    
    <content type="html"><![CDATA[<p>在前面的文章中，我们实现了一个最简单的功能：按下按键，LED 灯亮；松开按键，LED 灭。</p><p>这个小实验虽然验证了 GPIO 已经可以工作，但如果把它用到真正的项目里，很快就会遇到新需求——</p><p>比如，我们希望一个按键可以支持：</p><ul><li>单击控制 LED</li><li>双击切换让单片机进入低功耗模式</li><li>长按触发期间，LED 灯闪烁</li></ul><p>很明显，按键不仅仅是“开关”这么简单，它可能承载多种操作事件。为了让按键在项目中表现得灵活而可靠，我们就需要对按键进行更系统的处理。</p><p>这些库的功能大同小异，都是对按键进行消抖、长按、双击等处理，本文使用了 Github 中开源的 <a class="link"   href="https://github.com/bobwenstudy/easy_button" >easy_button<i class="fas fa-external-link-alt"></i></a> 库进行按键的处理。</p><blockquote><p>和之前一样，仓库都是在 Github 中，如果你访问 Github 没有那么顺畅，可以使用我提供的链接，我将仓库同步到了我自部署的 Git 服务器上：<a class="link"   href="https://git.orangetime.top/EMTime/easy_button" >https://git.orangetime.top/EMTime/easy_button<i class="fas fa-external-link-alt"></i></a></p></blockquote><p>easy_button 是一个轻量级但功能丰富的按键处理库，作者人家也说了，核心的按键管理机制是借鉴的 <a class="link"   href="https://github.com/MaJerle/lwbtn" >lwbtn<i class="fas fa-external-link-alt"></i></a>，具备以下的特点：</p><ul><li>多按键支持：理论上按键数量无限制</li><li>灵活事件机制：支持单击、双击、多击、长按、超长按</li><li>组合按键支持：基于 bit_array 实现组合逻辑，无需重复扫描逻辑</li><li>静态&#x2F;动态注册按键：可按需选择，节省代码空间</li><li>可配置时间参数：每个按键可独立配置消抖、长按、双击间隔等参数</li></ul><p>基于作者个人的角度进行横向对比，结果如下：</p><table><thead><tr><th align="left"></th><th align="left">easy_button</th><th align="left">FlexibleButton</th><th align="left">MultiButton</th><th align="left">lwbtn</th></tr></thead><tbody><tr><td align="left">最大支持按键数</td><td align="left">无限</td><td align="left">32</td><td align="left">无限</td><td align="left">无限</td></tr><tr><td align="left">按键时间参数独立配置</td><td align="left">支持</td><td align="left">支持</td><td align="left">部分支持</td><td align="left">支持</td></tr><tr><td align="left">单个按键RAM Size(Bytes)</td><td align="left">20(ebtn_btn_t)</td><td align="left">28(flex_button_t)</td><td align="left">44(Button)</td><td align="left">48(lwbtn_btn_t)</td></tr><tr><td align="left">支持组合按键</td><td align="left">支持</td><td align="left">不支持</td><td align="left">不支持</td><td align="left">不支持</td></tr><tr><td align="left">支持静态注册(可以省 Code Size)</td><td align="left">支持</td><td align="left">不支持</td><td align="left">不支持</td><td align="left">支持</td></tr><tr><td align="left">支持动态注册</td><td align="left">支持</td><td align="left">支持</td><td align="left">支持</td><td align="left">不支持</td></tr><tr><td align="left">点击最大次数</td><td align="left">无限</td><td align="left">无限</td><td align="left">2</td><td align="left">无限</td></tr><tr><td align="left">长按种类</td><td align="left">无限</td><td align="left">1</td><td align="left">1</td><td align="left">无限</td></tr><tr><td align="left">批量扫描支持</td><td align="left">支持</td><td align="left">不支持</td><td align="left">不支持</td><td align="left">不支持</td></tr></tbody></table><p>可以看到，easy_button 在保证了代码体积最小化的同时，还提供了非常全面且灵活的按键处理功能，因此本文选择它作为按键处理的方案。</p><h2 id="主要代码介绍"><a href="#主要代码介绍" class="headerlink" title="主要代码介绍"></a>主要代码介绍</h2><blockquote><p>本来想着写一些源码解释，以及相关的一些说明的，但是发现我写出来之后，只有我能看懂，在我表述的过程中，又丢失了很多细节，所以还是主要介绍一下用到的部分，然后直接上代码吧。</p></blockquote><h3 id="事件类型"><a href="#事件类型" class="headerlink" title="事件类型"></a>事件类型</h3><p>easy_button 仅保留了 4 种核心类型，可以满足大部分按键需求：</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="keyword">typedef</span> <span class="class"><span class="keyword">enum</span></span></span><br><span class="line"><span class="class">&#123;</span></span><br><span class="line">    EBTN_EVT_ONPRESS = <span class="number">0x00</span>,</span><br><span class="line">    EBTN_EVT_ONRELEASE,</span><br><span class="line">    EBTN_EVT_ONCLICK,</span><br><span class="line">    EBTN_EVT_KEEPALIVE,</span><br><span class="line">&#125; <span class="type">ebtn_evt_t</span>;</span><br></pre></td></tr></table></figure><p>其中：</p><ul><li>EBTN_EVT_ONPRESS 为按下事件，当按键按下的时候就会触发</li><li>EBTN_EVT_ONRELEASE 为抬起事件，当按键松开的时候就会触发</li><li>EBTN_EVT_ONCLICK 为点击事件，easy_button 将单击和双击以及更多次点击事件合并为一种类型，只要你是按下并抬起，就会记录为一次点击事件，至于如何区分，这个放在应用的时候再讲</li><li>EBTN_EVT_KEEPALIVE 为长按事件，当按键按下并持续一段时间，就会触发，easy_button 也将长按做了和点击事件同样的处理，根据配置的时间，会记录长按的次数；结合记录的次数，我们可以独立实现进入长按时的功能，和持续长按时的功能</li></ul><h3 id="按键时长配置"><a href="#按键时长配置" class="headerlink" title="按键时长配置"></a>按键时长配置</h3><p>easy_button 可以为每个按键提供不同的时间配置，基于这些配置，easy_button 不仅实现了软件消抖，同时还提供了更灵活的长按&#x2F;多击自定义方案，每个按键都可以根据应用场景和物理电路特性配置为不同的参数：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">ebtn_btn_param</span></span></span><br><span class="line"><span class="class">&#123;</span></span><br><span class="line">    <span class="type">uint16_t</span> time_debounce;</span><br><span class="line">    <span class="type">uint16_t</span> time_debounce_release;</span><br><span class="line">    <span class="type">uint16_t</span> time_click_pressed_min;</span><br><span class="line">    <span class="type">uint16_t</span> time_click_pressed_max;</span><br><span class="line">    <span class="type">uint16_t</span> time_click_multi_max;</span><br><span class="line">    <span class="type">uint16_t</span> time_keepalive_period;</span><br><span class="line">    <span class="type">uint16_t</span> max_consecutive;</span><br><span class="line">&#125; <span class="type">ebtn_btn_param_t</span>;</span><br></pre></td></tr></table></figure><p>其中：</p><ul><li>time_debounce 为按下消抖时间，防止按下时的按键抖动</li><li>time_debounce_release 为松开消抖时间，防止松开时的按键抖动</li><li>time_click_pressed_min 为单击按下最小时间，高于这个时间，才能算一次有效点击</li><li>time_click_pressed_max 为单击按下最大时间，高于这个时间，通常会视为长按</li><li>time_click_multi_max 为多击最大时间间隔，用于检测双击、三击等多击事件</li><li>time_keepalive_period 为长按事件触发间隔，按键持续按下时，周期性触发长按事件的间隔时间</li><li>max_consecutive 为连续点击最大次数，也就是多击上限，比如最多支持 5 次连续点击，超出 5 次后，系统会立即通知应用层进行处理</li></ul><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>easy_button 的使用其实非常的简单，接下来两篇文章，我会分别根据 HAL 库和 libopencm3 库，分别使用 easy_button 实现一个按键处理示例，通过这个示例，你可以了解到 easy_button 的使用方法，以及如何结合按键处理实现一些实际的功能。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;在前面的文章中，我们实现了一个最简单的功能：按下按键，LED 灯亮；松开按键，LED 灭。&lt;/p&gt;
&lt;p&gt;这个小实验虽然验证了 GPIO 已经可以工作，但如果把它用到真正的项目里，很快就会遇到新需求——&lt;/p&gt;
&lt;p&gt;比如，我们希望一个按键可以支持：&lt;/p&gt;
&lt;ul&gt;
&lt;l</summary>
      
    
    
    
    <category term="mcu" scheme="https://blog.orangetime.top/categories/mcu/"/>
    
    
  </entry>
  
  <entry>
    <title>STM32H7开发笔记(三):GPIO-libopencm3库实现</title>
    <link href="https://blog.orangetime.top/2025/09/25/mcu/h7-gpio-libopencm3/"/>
    <id>https://blog.orangetime.top/2025/09/25/mcu/h7-gpio-libopencm3/</id>
    <published>2025-09-25T07:38:59.339Z</published>
    <updated>2025-12-09T09:54:31.199Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>以为 H7 的 libopencm3 开发也和 F1、F4 用起来一样简单，结果发现还是有些不一样的，自己先小折腾了半天，差点刚开坑就结束了。</p></blockquote><p>在上一篇文章中我们使用 HAL 库实现了 STM32H7 的 GPIO 控制功能，本文我们使用 libopencm3 库来实现同样的功能，有关于 libopencm3 的介绍可以参考我前面写的<a href="/2023/07/20/mcu/libopencm3">libopencm3 开发STM32体验笔记</a>。</p><blockquote><p>强烈推荐大家看一下这篇文章，才好理解 libopencm3 的使用方式，否则可能很难快速上手。</p></blockquote><p>本文代码仓库：<a class="link"   href="https://git.orangetime.top/EMTime/stm32h7-libopencm3" >stm32h7-libopencm3<i class="fas fa-external-link-alt"></i></a>，不想看我啰嗦的，可以直接拉代码，目录结构清晰，还写了详细注释。</p><blockquote><p>本文对应于仓库中的 1gpio 文件夹</p></blockquote><h2 id="开发环境简单说明"><a href="#开发环境简单说明" class="headerlink" title="开发环境简单说明"></a>开发环境简单说明</h2><ul><li><strong>系统</strong>：Ubuntu 24.04 LTS（没啥特别的，装好 git、make 等基础工具即可）。</li><li><strong>开发工具链</strong>：arm-none-eabi-gcc<ul><li>apt 里有老版本。</li><li>ARM 官网可下最新版：<a class="link"   href="https://developer.arm.com/downloads/-/arm-gnu-toolchain-downloads" >ARM GNU Toolchain<i class="fas fa-external-link-alt"></i></a>。</li><li>我用的是 14.3 版本。装完记得把路径加到环境变量。</li></ul></li><li><strong>构建工具</strong>：<a class="link"   href="https://xmake.io/" >xmake<i class="fas fa-external-link-alt"></i></a><ul><li>上手比 cmake 直观，能生成 makefile、ninja 等多种配置。</li><li>我这边用 xmake 生成了一份 makefile，习惯 make 的同学也能直接用。</li></ul></li><li><strong>调试工具、烧录工具等</strong>：参考我之前的那篇 libopencm3 笔记。</li></ul><h2 id="工程结构"><a href="#工程结构" class="headerlink" title="工程结构"></a>工程结构</h2><p>工程目录结构如下：</p><h3 id="工程与-libopencm3-的结构"><a href="#工程与-libopencm3-的结构" class="headerlink" title="工程与 libopencm3 的结构"></a>工程与 libopencm3 的结构</h3><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">./</span><br><span class="line">├── libopencm3/           # 库源码</span><br><span class="line">├── libopencm3-examples/  # 官方示例</span><br><span class="line">└── stm32h7/              # 我自己的项目</span><br><span class="line">    └── 1gpio</span><br></pre></td></tr></table></figure><ul><li>libopencm3&#x2F;：库源码，首次拉取后要进目录 make 一下，生成库文件和头文件。</li><li>libopencm3-examples&#x2F;：示例仓库，可惜目前还没 STM32H7 的例子。</li><li>stm32h7&#x2F;：我自己的代码。建议你要玩 F1&#x2F;F4 的话，也建个同级目录，比如 stm32f1&#x2F;，方便区分不同型号。</li></ul><blockquote><p>libopencm3 与 libopencm3-examples 两个目录是 libopencm3 的官方仓库，为了方便大家拉取，我也将这两个仓库同步到了我自己部署的 Gitea 中。<br>地址分别是：</p><ul><li><a class="link"   href="https://git.orangetime.top/EMTime/libopencm3" >libopencm3<i class="fas fa-external-link-alt"></i></a></li><li><a class="link"   href="https://git.orangetime.top/EMTime/libopencm3-examples" >libopencm3-examples<i class="fas fa-external-link-alt"></i></a>。</li></ul></blockquote><h3 id="工程内部目录结构"><a href="#工程内部目录结构" class="headerlink" title="工程内部目录结构"></a>工程内部目录结构</h3><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><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><br><span class="line">├── bin/                # 编译产物（elf、hex、bin）</span><br><span class="line">├── build/              # 临时文件</span><br><span class="line">├── user/               # 用户代码</span><br><span class="line">│   ├── inc/            # 头文件</span><br><span class="line">│   │   ├── gpio.h</span><br><span class="line">│   │   ├── main.h</span><br><span class="line">│   │   └── systick.h</span><br><span class="line">│   └── src/            # 源文件</span><br><span class="line">│       ├── gpio.c</span><br><span class="line">│       ├── main.c</span><br><span class="line">│       └── systick.c</span><br><span class="line">├── cortex-m-generic.ld # 链接脚本</span><br><span class="line">├── makefile</span><br><span class="line">└── xmake.lua</span><br></pre></td></tr></table></figure><ul><li>bin&#x2F; 目录用于存放编译生成的可执行文件，如果使用 xmake，则会一并生成 hex、bin 文件。</li><li>build&#x2F; 目录用于存放编译生成的临时文件。</li></ul><blockquote><p>保持源代码和生成物分离是一个好习惯，能避免 Git 仓库污染。</p></blockquote><ul><li>user&#x2F; 目录用于存放用户代码，我按照 HAL 的目录结构，将代码分成了 inc 和 src 两个目录，其中 inc 目录用于存放头文件，src 目录用于存放源文件。之后随着功能的增多，我还会增加 lib 等目录，分别用于存放相关的库文件。</li><li>cortex-m-generic.ld 是链接脚本，用于指定程序的内存布局。</li><li>makefile 是 make 的构建文件，这里我是使用 xmake 生成的，可以编译生成 elf 文件，但没有添加 hex、bin 文件的生成，如果需要生成 hex、bin 文件，可以自行添加。</li><li>xmake.lua 是 xmake 的构建文件，用于指定项目的构建规则，我的工程也是使用 xmake 进行构建的。</li></ul><h2 id="功能实现"><a href="#功能实现" class="headerlink" title="功能实现"></a>功能实现</h2><p>用 libopencm3 写 GPIO，需要先准备几个“地基”：</p><ul><li>系统时钟配置</li><li>SysTick 延时</li><li>GPIO 初始化与操作</li></ul><p>顺序差不多就是这样的，我们一步一步来。</p><h3 id="时钟配置"><a href="#时钟配置" class="headerlink" title="时钟配置"></a>时钟配置</h3><p>我的核心板外部晶振为 <strong>25 MHz</strong>，芯片是 <strong>STM32H743VIT6</strong>，最大主频为 480 MHz，需要配置 PLL 把晶振倍频到 480 MHz，再分频给各个总线和外设。</p><p>CubeMX 的时钟树很直观，所以我通常用它来进行对照，libopencm3 的结构体配置和 CubeMX 的参数差不多是一一对应的：</p><center>   <img     src="https://emtime-picture.cn-nb1.rains3.com/2025/09/28/68d81189b6f1d.png"  ></center><p>在 libopencm3 中，有一个结构体定义如下，分别对应了我们图片中的倍频、分频因子：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">rcc_pll_config</span> &#123;</span></span><br><span class="line"><span class="class"><span class="keyword">enum</span> <span class="title">rcc_osc</span> <span class="title">sysclock_source</span>;</span>     <span class="comment">/**&lt; SYSCLK source input selection. */</span></span><br><span class="line"><span class="type">uint8_t</span> pll_source;               <span class="comment">/**&lt; RCC_PLLCKSELR_PLLSRC_xxx value. */</span></span><br><span class="line"><span class="type">uint32_t</span> hse_frequency;           <span class="comment">/**&lt; User specified HSE frequency, 0 if none. */</span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">pll_config</span> &#123;</span></span><br><span class="line"><span class="type">uint8_t</span> divm;                   <span class="comment">/**&lt; Pre-divider value for each PLL. 0-64 integers. */</span></span><br><span class="line"><span class="type">uint16_t</span> divn;                  <span class="comment">/**&lt; Multiplier, 0-512 integer. */</span></span><br><span class="line"><span class="type">uint8_t</span> divp;                   <span class="comment">/**&lt; Post divider for PLLP clock. */</span></span><br><span class="line"><span class="type">uint8_t</span> divq;                   <span class="comment">/**&lt; Post divider for PLLQ clock. */</span></span><br><span class="line"><span class="type">uint8_t</span> divr;                   <span class="comment">/**&lt; Post divider for PLLR clock. */</span></span><br><span class="line">&#125; pll1, pll2, pll3;               <span class="comment">/**&lt; PLL1-PLL3 configurations. */</span></span><br><span class="line"><span class="type">uint8_t</span> core_pre;                 <span class="comment">/**&lt; Core prescaler  note: domain 1. */</span></span><br><span class="line"><span class="type">uint8_t</span> hpre;                     <span class="comment">/**&lt; HCLK3 prescaler note: domain 1. */</span></span><br><span class="line"><span class="type">uint8_t</span> ppre1;                    <span class="comment">/**&lt; APB1 Peripheral prescaler note: domain 2. */</span></span><br><span class="line"><span class="type">uint8_t</span> ppre2;                    <span class="comment">/**&lt; APB2 Peripheral prescaler note: domain 2. */</span></span><br><span class="line"><span class="type">uint8_t</span> ppre3;                    <span class="comment">/**&lt; APB3 Peripheral prescaler note: domain 1. */</span></span><br><span class="line"><span class="type">uint8_t</span> ppre4;                    <span class="comment">/**&lt; APB4 Peripheral prescaler note: domain 3. */</span></span><br><span class="line"><span class="type">uint8_t</span> flash_waitstates;         <span class="comment">/**&lt; Latency Value to set for flahs. */</span></span><br><span class="line"><span class="class"><span class="keyword">enum</span> <span class="title">pwr_vos_scale</span> <span class="title">voltage_scale</span>;</span> <span class="comment">/**&lt; LDO/SMPS Voltage scale used for this frequency. */</span></span><br><span class="line"><span class="class"><span class="keyword">enum</span> <span class="title">pwr_sys_mode</span> <span class="title">power_mode</span>;</span>     <span class="comment">/**&lt; LDO/SMPS configuration for device. */</span></span><br><span class="line"><span class="type">uint8_t</span> smps_level;               <span class="comment">/**&lt; If using SMPS, voltage level to set. */</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>我们对其进行逐项解释：</p><ul><li><p>sysclock_source：系统时钟源选择，对应上面图片中的 System Clock Mux 部分，一般来说，我们都是选择 PLL 作为系统时钟源。可选参数如下：</p><ul><li>RCC_PLL：选择 PLL 作为系统时钟源，这个是常用的配置。</li><li>RCC_HSE：选择 HSE 作为系统时钟源。</li><li>RCC_HSI：选择 HSI 作为系统时钟源。</li><li>其实还有一些参数，但我觉得不能用在系统时钟源的选择上，所以就不一一列举了。</li></ul></li><li><p>pll_source：PLL 时钟源选择，对应图片中的 PLL Clock Mux 部分，我们通常选择 HSE 作为 PLL 时钟源，可选参数如下：</p><ul><li>RCC_PLLCKSELR_PLLSRC_HSE：选择 HSE 作为 PLL 时钟源，有外部晶振的情况下，我们通常选择这个配置。</li><li>RCC_PLLCKSELR_PLLSRC_CSI：选择 CSI 作为 PLL 时钟源，CSI 是比较新的单片机（如 H7、U5）引入的 4 MHz 的内部时钟源，功耗低，但精度不高，如果说有低功耗方面的需求，可以考虑这个配置。</li><li>RCC_PLLCKSELR_PLLSRC_HSI：选择 HSI 作为 PLL 时钟源，HSI 是一个 64 MHz 的内部时钟源，单片机上电默认就是这个时钟源，在没有外部晶振的情况下，我们通常选择这个配置（精度也不是很高）。</li></ul></li><li><p>hse_frequency：HSE 频率，是一个 uint32_t 的变量，在配置的时候我们直接填写 HSE 的频率即可（如 25000000U），如果选择的是 HSI，则填写 0。</p></li><li><p>pll1、pll2、pll3：PLL 配置，对应图片中 PLL Source Mux 出来之后分叉出来的三条线，这三条线大同小异，我们以 pll1 为例，逐项解释：</p><ul><li>divm：PLL 预分频因子，对应图片中的 DIVM1，图片里面是几，我们赋值的值就是几，比如图片中是 5，我们赋值 5 即可。</li><li>divn：PLL 倍频因子，对应图片中的 DIVN1，图片里面是几，同理，图片中是 192，我们就是赋值 192 即可。</li><li>divp、divq、divr：PLL 后分频因子，根据 pqr 即可在锁相环上输出三个不同的时钟，对应图片中的 DIVP1、DIVQ1、DIVR1，图片里面是几，我们赋值几即可。<ul><li>divp：该通道的输出常用于系统时钟。</li><li>divq：该通道的输出常用于 USB、SDMMC、ETH 等外设。</li><li>divr：该通道的输出常用于 SPI、ADC、DAC 等外设。</li></ul></li></ul></li><li><p>core_pre：核心时钟预分频因子，对应图片中的 D1CPRE Prescaler，决定了核心时钟的频率，可选参数如下：</p><ul><li>RCC_D1CFGR_D1CPRE_BYP：不进行分频，对应于图片中的 1 分频。</li><li>RCC_D1CFGR_D1CPRE_DIV2：2 分频。</li><li>RCC_D1CFGR_D1CPRE_DIV4：4 分频。</li><li>RCC_D1CFGR_D1CPRE_DIV8：8 分频。</li><li>RCC_D1CFGR_D1CPRE_DIV16：16 分频。</li><li>…：其他分频因子。</li></ul></li><li><p>hpre、ppre1、ppre2、ppre3、ppre4：和核心时钟预分频因子类似，其意义在上面展示结构体 rcc_pll_config 的代码中标有注释，在图片中都分别有对应的配置可以参考，要注意图片中类似于 D2PPRE1 这样的名字，在 libopencm3 中，对应的参数是 RCC_D2CFGR_D2PPRE_DIV2，要注意区分。</p></li><li><p>flash_waitstates：闪存等待周期，这个在图片中还真没对应的，可以生成一份同样配置的 MDK 工程，在 SystemClock_Config 函数中，有如下代码：</p></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></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (HAL_RCC_ClockConfig(&amp;RCC_ClkInitStruct, FLASH_LATENCY_4) != HAL_OK)</span><br><span class="line">&#123;</span><br><span class="line">  Error_Handler();</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>可以看到函数的参数是 FLASH_LATENCY_4，这个参数对应于 libopencm3 中的 FLASH_ACR_LATENCY_4WS，所以我们赋值为 FLASH_ACR_LATENCY_4WS 即可。</p><ul><li>voltage_scale：电压缩放，也是新系列的单片机中出现的东西，缩放影响内核电压与最高主频，0 为高档位，档位越高，功耗越大，但主频越高。同样在 SystemClock_Config 函数中，有如下代码：</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></pre></td><td class="code"><pre><span class="line">__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE0);</span><br><span class="line"></span><br><span class="line"><span class="keyword">while</span>(!__HAL_PWR_GET_FLAG(PWR_FLAG_VOSRDY)) &#123;&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>可以看到函数的参数是 PWR_REGULATOR_VOLTAGE_SCALE0，这个参数对应于 libopencm3 中的 PWR_VOS_SCALE_0，所以我们赋值为 PWR_VOS_SCALE_0 即可。</p><ul><li>power_mode：电源模式，同样是新系列带来的新功能，可以选择 LDO 或者 SMPS，LDO 简单易用，但功耗较大，SMPS 功耗低，但需要外部电路支持。同样在 SystemClock_Config 函数中，有如下代码：</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">HAL_PWREx_ConfigSupply(PWR_LDO_SUPPLY);</span><br></pre></td></tr></table></figure><p>我们和 MDK 工程保持一致，所以也是选择 LDO 的供电方式，对应于 libopencm3 中的 PWR_SYS_LDO，所以我们赋值为 PWR_SYS_LDO 即可。</p><ul><li>smps_level：如果选择的是 SMPS，则需要设置电压等级，libopencm3 也提供了对应的选项，如 PWR_CR3_SMPSLEVEL_VOS，我没没有使用 SMPS，所以这里设置为什么都没关系。</li></ul><p>在配置完参数之后，我们调用 rcc_clock_setup_pll(&amp;pll_config); 即可进行系统时钟的初始化，该函数会根据我们配置的参数，设置到寄存器中，完成系统时钟的初始化。</p><blockquote><p>对于时钟的初始化来说，执行顺序其实是一个值得考虑的事情，先初始化什么，后初始化什么，都是有考量的，不过 libopencm3 已经做好这一步了，如果你对执行的顺序有所疑惑，可以查看 libopencm3 源码，在libopencm3&#x2F;lib&#x2F;stm32&#x2F;h7&#x2F;rcc.c中，可以看到 rcc_clock_setup_pll 函数的执行顺序，对于理解时钟的初始化过程，有很大的帮助。</p></blockquote><blockquote><p>使用第三方库，需要多看源码，多找定义，多翻阅文档，多动手实践，才能熟练使用。</p></blockquote><h3 id="SysTick-延时"><a href="#SysTick-延时" class="headerlink" title="SysTick 延时"></a>SysTick 延时</h3><p>众所周知，HAL 库的 HAL_Delay 是基于 SysTick 实现的，延时&#x2F;计时对于单片机程序来说，是一个非常重要的功能，所以 SysTick 定时器的初始化，也是我们必须要做的。</p><p>因为 SysTick 是 ARM 内核提供的，并不是 STM32 系列单片机独有的，所以在初始化思路上来说，你可以借鉴任何一个 ARM 内核处理器的相关代码，我这里还是以 CubeMX 生成的代码作为参考，进行滴答定时器的初始化。</p><p>在 HAL_Init 这个函数中，有如下代码：</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span>(HAL_InitTick(TICK_INT_PRIORITY) != HAL_OK)</span><br><span class="line">&#123;</span><br><span class="line">  <span class="keyword">return</span> HAL_ERROR;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>这个函数实现了 SysTick 的初始化，主要包含了如下几步：</p><ul><li>SysTick 时钟源选择。</li><li>SysTick 重装值设置。</li><li>SysTick 计数值清零。</li><li>SysTick 中断优先级设置。</li><li>SysTick 中断使能。</li><li>SysTick 使能。</li></ul><p>对应于 libopencm3 中的代码如下：</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_">systick_init</span><span class="params">(<span class="type">uint32_t</span> ticks)</span></span><br><span class="line">&#123;</span><br><span class="line">  systick_set_clocksource(STK_CSR_CLKSOURCE_AHB);</span><br><span class="line"></span><br><span class="line">  systick_set_reload((rcc_get_bus_clk_freq(RCC_CPUCLK) / ticks) - <span class="number">1UL</span>);</span><br><span class="line"></span><br><span class="line">  systick_clear();</span><br><span class="line"></span><br><span class="line">  nvic_set_priority(NVIC_SYSTICK_IRQ, <span class="number">15</span>);</span><br><span class="line"></span><br><span class="line">  systick_interrupt_enable();</span><br><span class="line">  systick_counter_enable();</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>SysTick 可以正常工作之后，我们写好中断和延时函数：</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">volatile</span> <span class="type">uint32_t</span> systick = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 进入低功耗模式</span></span><br><span class="line"><span class="type">static</span> <span class="keyword">inline</span> __attribute__((always_inline)) <span class="type">void</span> __WFI(<span class="type">void</span>)</span><br><span class="line">&#123;</span><br><span class="line">  __asm <span class="title function_">volatile</span><span class="params">(<span class="string">&quot;wfi&quot;</span>)</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_">user_delay_ms</span><span class="params">(<span class="type">uint32_t</span> ms)</span></span><br><span class="line">&#123;</span><br><span class="line">  <span class="type">uint32_t</span> start = systick;</span><br><span class="line">  <span class="keyword">while</span> (systick -  start &lt; ms)</span><br><span class="line">  &#123;</span><br><span class="line">    __WFI();</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">// SysTick 定时器中断处理函数</span></span><br><span class="line"><span class="type">void</span> <span class="title function_">sys_tick_handler</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">  systick++;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>这样就能愉快用 user_delay_ms() 进行毫秒级延时了。</p><h3 id="补充：延时时间展示"><a href="#补充：延时时间展示" class="headerlink" title="补充：延时时间展示"></a>补充：延时时间展示</h3><p>那么我们这样初始化之后，到底对不对呢，能不能实现延时的功能呢？我写了一个简单的程序，在无限循环中，每隔 100ms，翻转一次 PE3 引脚，如下：</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">while</span> (<span class="number">1</span>)</span><br><span class="line">&#123;</span><br><span class="line">  user_delay_ms(<span class="number">100</span>);</span><br><span class="line">  gpio_toggle(GPIOE, GPIO3);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>然后我们使用逻辑分析仪，采集 PE3 引脚的电平，如下：</p><center>   <img     src="https://emtime-picture.cn-nb1.rains3.com/2025/11/08/690f09e4d4fb9.png"  ></center><p>可以看到，PE3 引脚高电平+低电平的持续时间为200.021 ms，结合逻辑分析仪和定时器本身的误差，我们可以认为，这个延时是准确的。</p><h2 id="GPIO"><a href="#GPIO" class="headerlink" title="GPIO"></a>GPIO</h2><h3 id="GPIO-初始化"><a href="#GPIO-初始化" class="headerlink" title="GPIO 初始化"></a>GPIO 初始化</h3><p>绕了一大圈才回到主题，接下来的事情反而很简单，我们只需要像标准库一样，进行时钟使能，然后配置引脚的模式即可，libopencm3 中，GPIO 的配置，需要分两步进行，第一步是配置引脚的模式，第二步是配置引脚的属性。</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></pre></td><td class="code"><pre><span class="line"><span class="type">void</span> <span class="title function_">user_gpio_setup</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">  rcc_periph_clock_enable(RCC_GPIOE);</span><br><span class="line">  gpio_mode_setup(GPIOE, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO3);</span><br><span class="line">  gpio_set_output_options(GPIOE, GPIO_OTYPE_PP, GPIO_OSPEED_2MHZ, GPIO3);</span><br><span class="line"></span><br><span class="line">  gpio_clear(GPIOE, GPIO3);</span><br><span class="line"></span><br><span class="line">  PWR_CR1 |= PWR_CR1_DBP;</span><br><span class="line">  rcc_periph_clock_enable(RCC_GPIOC);</span><br><span class="line">  gpio_mode_setup(GPIOC, GPIO_MODE_INPUT, GPIO_PUPD_PULLDOWN, GPIO13);</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>基本思路就是使能时钟-&gt;配置引脚模式-&gt;配置引脚属性，然后就可以使用对应的引脚了。</p><p>但是，在 STM32 中，PC13 - PC15 这三个引脚是比较特殊的，有以下一些需要注意的地方：</p><ul><li>他们属于低速I&#x2F;O，最大只能工作在 2MHz。</li><li>驱动能力弱，一般用作输入或者低速输出（所以核心板用这个引脚作为按键还是有一定合理性的）。</li><li>在 STM32H7 中，这三个引脚属于备份域（Backup Domain），为了防止程序误操作，也为了保持主电源掉电之后的可靠性，单片机在默认上电的时候是禁止访问备份域的，所以我们需要在初始化的时候，先使能备份域的访问权限，也就是在代码中，先执行 PWR_CR1 |&#x3D; PWR_CR1_DBP; 这一行代码。</li></ul><blockquote><p>可能你会问，为什么在使用 HAL 库的时候没有这样的问题，因为 HAL 库在 SystemClock_Config 函数中，调用了 HAL_RCC_OscConfig，而这个函数中进行了备份域的写使能：PWR-&gt;CR1 |&#x3D; PWR_CR1_DBP;，所以在使用 HAL 库的时候，不需要手动写使能备份域的代码。</p></blockquote><h3 id="GPIO-读写"><a href="#GPIO-读写" class="headerlink" title="GPIO 读写"></a>GPIO 读写</h3><p>和上一篇的代码逻辑一致，当按键按下的时候，进行 LED 的翻转，代码如下：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><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"></span><br><span class="line">  user_gpio_setup();</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="keyword">if</span>(gpio_get(GPIOC, GPIO13))</span><br><span class="line">    &#123;</span><br><span class="line">      user_delay_ms(<span class="number">20</span>);</span><br><span class="line">      <span class="keyword">if</span>(gpio_get(GPIOC, GPIO13))</span><br><span class="line">      &#123;</span><br><span class="line">        gpio_toggle(GPIOE, GPIO3);</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">while</span>(gpio_get(GPIOC, GPIO13));</span><br><span class="line">    &#125;</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><span class="line"></span><br></pre></td></tr></table></figure><p>如果要直接控制电平，可以使用 gpio_set 和 gpio_clear 函数，如 gpio_set(GPIOE, GPIO3); 和 gpio_clear(GPIOE, GPIO3);。</p><p>对于读取函数来说，虽然看起来没有什么问题，但是要注意 gpio_get 函数返回的并不是电平的逻辑值，而是包含所选引脚状态的位掩码。例如：</p><ul><li>当 PC13 引脚为高电平时，gpio_get(GPIOC, GPIO13) 返回 0x2000（即 1 &lt;&lt; 13），而不是单纯的 1。</li><li>当 PC13 为低电平时，返回值才是 0。</li></ul><p>因此，在读取引脚电平的时候，应当判断它是不是 0，才能保证引脚电平的正确性。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>这一篇文章还是比较啰嗦了，本来只是想点亮一个灯，结果为了把地基打稳，写了大半篇。</p><p>用 libopencm3 开发 STM32H7，最大的心得是：</p><ul><li>要习惯自己写初始化，不像 HAL 那样一口气帮你包好。</li><li>多看源码，多对照 CubeMX，少走弯路。</li><li>习惯把工程目录分层，方便后面扩展。</li></ul>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;以为 H7 的 libopencm3 开发也和 F1、F4 用起来一样简单，结果发现还是有些不一样的，自己先小折腾了半天，差点刚开坑就结束了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在上一篇文章中我们使用 HAL 库实现了 STM32H7 的 </summary>
      
    
    
    
    <category term="mcu" scheme="https://blog.orangetime.top/categories/mcu/"/>
    
    
  </entry>
  
  <entry>
    <title>STM32H7开发笔记(二):GPIO-HAL库实现</title>
    <link href="https://blog.orangetime.top/2025/09/24/mcu/h7-gpio-hal/"/>
    <id>https://blog.orangetime.top/2025/09/24/mcu/h7-gpio-hal/</id>
    <published>2025-09-24T11:44:43.701Z</published>
    <updated>2025-10-24T19:43:16.490Z</updated>
    
    <content type="html"><![CDATA[<p>在单片机的世界里，“点亮一盏 LED”几乎是所有开发的第一步。它不仅是传统意义上的“Hello World”，更是验证开发环境是否正确搭建、工具链能否正常工作的基本手段。</p><p>在 STM32H7 上，GPIO（通用输入输出口）依然是最基础、最常用的外设。无论是后续的串口通信、外设控制，还是复杂的总线接口，几乎都离不开 GPIO 的支撑。</p><p>本文将从 <strong>HAL 库</strong> 的角度出发，介绍如何完成 GPIO 的初始化与配置，并通过点亮开发板上的 LED 来完成第一个小实验。这也是我们深入 STM32H7 开发的起点。</p><h2 id="CubeMX-配置"><a href="#CubeMX-配置" class="headerlink" title="CubeMX 配置"></a>CubeMX 配置</h2><p>意法半导体为 STM32H7 提供了两套官方开发库：<strong>HAL 库</strong> 和 <strong>LL 库</strong>。 </p><ul><li>HAL 库：官方推荐，封装更高，开发效率更快；</li><li>LL 库：更接近寄存器，性能可控，但门槛更高。</li></ul><p>本文使用 HAL 库来实现 GPIO 的配置和控制。</p><blockquote><p>顺带一提，HAL 和 LL 库是可以混用的，但需要你有一定的寄存器基础，否则很容易出问题。我后续还会写 libopencm3 的使用笔记，它和 LL 思路类似，可以作为参考。</p></blockquote><p>新建一个 CubeMX 工程，选择 <strong>STM32H743VIT6</strong> 作为目标芯片。创建时会提示 MPU（内存保护单元）的配置，暂时不用管，直接点击 “Yes” 即可。</p><center>   <img     src="https://emtime-picture.cn-nb1.rains3.com/2025/09/25/68d4269cc8360.png"  ></center><h3 id="配置时钟"><a href="#配置时钟" class="headerlink" title="配置时钟"></a>配置时钟</h3><p>在左侧的 <strong>System Core → RCC</strong> 中进入时钟配置，选择合适的时钟源，并配置相应的时钟，如下所示：</p><center>   <img     src="https://emtime-picture.cn-nb1.rains3.com/2025/09/25/68d427d955624.png"  ></center><center>   <img     src="https://emtime-picture.cn-nb1.rains3.com/2025/09/25/68d42823e4531.png"  ></center><h3 id="配置-GPIO"><a href="#配置-GPIO" class="headerlink" title="配置 GPIO"></a>配置 GPIO</h3><p>在本篇文章中，我们会同时演示 GPIO <strong>输出</strong>（点亮 LED）和 <strong>输入</strong>（读取按键）。</p><p>在我的核心板上：</p><ul><li><strong>LED</strong> 接在 PE3</li><li><strong>按键</strong> 接在 PC13</li></ul><p>对应的原理图如下：</p><center>   <img     src="https://emtime-picture.cn-nb1.rains3.com/2025/09/25/68d42a776216b.png"  ></center><p>这里 PE3 并不是直接接 LED，而是通过一个三极管来驱动。简单来说：</p><ul><li>当 PE3 输出高电平时，三极管导通，LED 亮；</li><li>当 PE3 输出低电平时，三极管截止，LED 灭。</li></ul><p>配置效果如下图所示：</p><center>   <img     src="https://emtime-picture.cn-nb1.rains3.com/2025/09/25/68d4cb6bbee29.png"  ></center><blockquote><p>小技巧：看到这种三极管驱动电路时，不用慌，直接看三极管箭头方向——引脚电流和箭头方向一致时，三极管导通，LED 就会亮。</p></blockquote><p>在 CubeMX 中还可以给引脚加上 <strong>User Label</strong>，这样在生成代码时会有 LED_GPIO_Port 和 LED_Pin 这样的宏，方便后续编程。</p><p>对于输入引脚 PC13，电路中串了一个电阻和 ESD 保护器件。ESD 可以忽略，把它当作导线即可。限流电阻保证安全。<br>但需要注意的是：<strong>按键松开时，PC13 会悬空（电平不确定）</strong>，所以我们在 CubeMX 里要把它配置为 <strong>下拉输入</strong>。</p><p>配置效果如下：</p><center>   <img     src="https://emtime-picture.cn-nb1.rains3.com/2025/09/25/68d4cbd832ff9.png"  ></center><h2 id="代码编写"><a href="#代码编写" class="headerlink" title="代码编写"></a>代码编写</h2><p>配置完外设后，生成对应的工程。我这里使用 <strong>MDK</strong>，你也可以选择其他 IDE。</p><p>在 main.c 中，我们添加简单的按键检测和 LED 控制逻辑：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;main.h&quot;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;gpio.h&quot;</span></span></span><br><span class="line"></span><br><span class="line">...</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"></span><br><span class="line">  <span class="comment">/* USER CODE BEGIN WHILE */</span></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="keyword">if</span>(HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_SET)</span><br><span class="line">    &#123;</span><br><span class="line">      HAL_Delay(<span class="number">20</span>);</span><br><span class="line">      <span class="keyword">if</span>(HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_SET)</span><br><span class="line">      &#123;</span><br><span class="line">        HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">while</span>(HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_SET);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">/* USER CODE END WHILE */</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">/* USER CODE BEGIN 3 */</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这段代码实现了一个最经典的“按键控制 LED”功能：</p><ul><li>按下按键 → LED 状态反转（亮 ↔ 灭）</li><li>松开按键后才能继续检测（避免长按一直触发）</li></ul><p>当然，如果需要直接控制电平，也可以用 HAL_GPIO_WritePin：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET);   <span class="comment">// 点亮 LED</span></span><br><span class="line">HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET); <span class="comment">// 熄灭 LED</span></span><br></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>GPIO 是单片机开发的起点，也是后续一切外设驱动的基础。</p><ul><li>输出方面：常见用法是点灯，比如在多线程或异步场景下，周期性闪烁的 LED 就能作为“心跳指示灯”，帮助我们快速判断设备是否运行正常。</li><li>输入方面：最常见的就是按键，不仅能作为人机交互入口，还可以用外部中断的方式捕获事件，这部分我会在后续文章中单独展开。</li></ul>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;在单片机的世界里，“点亮一盏 LED”几乎是所有开发的第一步。它不仅是传统意义上的“Hello World”，更是验证开发环境是否正确搭建、工具链能否正常工作的基本手段。&lt;/p&gt;
&lt;p&gt;在 STM32H7 上，GPIO（通用输入输出口）依然是最基础、最常用的外设。无论是后续</summary>
      
    
    
    
    <category term="mcu" scheme="https://blog.orangetime.top/categories/mcu/"/>
    
    
  </entry>
  
  <entry>
    <title>STM32H7开发笔记(一):前言</title>
    <link href="https://blog.orangetime.top/2025/09/24/mcu/h7/"/>
    <id>https://blog.orangetime.top/2025/09/24/mcu/h7/</id>
    <published>2025-09-24T03:25:00.265Z</published>
    <updated>2025-09-24T17:29:52.436Z</updated>
    
    <content type="html"><![CDATA[<p>随着嵌入式应用需求的不断提升，<strong>STM32H7 系列</strong>凭借高主频、强大的外设接口和丰富的存储特性，逐渐成为许多高性能场景的理想选择。无论是高速通信、大容量存储，还是复杂的多任务控制，H7 都能提供比 F1&#x2F;F4 系列更宽裕的性能空间。</p><p>为了更好地梳理和沉淀开发经验，我决定开启一个新的系列：<strong>STM32H7 开发笔记</strong>。<br>这个系列将系统性地记录 H7 平台的常用功能与应用实践，帮助有类似需求的朋友快速上手并深入理解。</p><h2 id="系列定位"><a href="#系列定位" class="headerlink" title="系列定位"></a>系列定位</h2><ul><li><strong>覆盖范围广</strong>：从最基础的 GPIO、串口，到较为复杂的 USB、SDIO 等常用外设；</li><li><strong>注重实践</strong>：结合实际应用场景，给出可运行的示例代码与配置方法；</li><li><strong>对比不同库</strong>：既会基于 ST 官方的 HAL 库实现功能，也会尝试使用 libopencm3 实现相同功能，从而更直观地理解二者的差异和特点。</li></ul><blockquote><p><strong>注意</strong>：本系列并不是零基础入门教程，阅读这些笔记的前提是你已经具备一定的嵌入式开发经验，了解基本的 C 语言编程和单片机外设的使用。如果你已经有一定的 STM32 或其他 MCU 开发背景，那么这些内容会帮助你快速上手并熟悉 STM32H7 的特性；但如果你完全没有嵌入式开发经验，本系列可能并不是最合适的起点。</p></blockquote><blockquote><p>说到起点，之后可能会出入门系列的手把手教程，可能是51，也可能是32，敬请期待。</p></blockquote><h2 id="使用的库"><a href="#使用的库" class="headerlink" title="使用的库"></a>使用的库</h2><ol><li><p><strong>HAL 库</strong><br> ST 官方提供的硬件抽象层库，功能全面，封装完善，能够快速实现大部分外设功能，非常适合快速搭建和验证项目。<br> 系列将以 HAL 为主，确保功能完整性和可移植性。</p></li><li><p><strong>libopencm3</strong><br> 一个轻量、开源的外设驱动库，更贴近底层寄存器操作，结构简洁清晰。<br> 在掌握 HAL 基础上，我会尝试使用 libopencm3，展示更灵活、更贴近标准库的实现方式，并通过对比加深对外设和底层机制的理解。</p></li></ol><h2 id="系列安排"><a href="#系列安排" class="headerlink" title="系列安排"></a>系列安排</h2><p>本系列的内容会按照由浅入深的方式展开，主要包括以下方面：</p><ul><li>工程环境与工具链配置</li><li>GPIO 控制与外设基础</li><li>串口通信（UART&#x2F;USART）</li><li>I2C &#x2F; SPI 总线应用</li><li>DMA 高速数据传输</li><li>USB 设备与主机模式</li><li>SDIO&#x2F;SD 卡文件系统</li><li>RTOS 在 H7 平台上的使用（比如nuttx、FreeRTOS、RT-Thread等）</li></ul><p>在每个主题中，都会给出相应的代码示例和注意事项，帮助快速定位问题、验证功能。</p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>这套笔记并不是简单的“功能演示”，而是希望成为一份<strong>系统化的开发记录</strong>。<br>无论你是刚接触 STM32H7，还是已经在项目中使用它，都能在这里找到一些实用的方法和思路。</p><p>让我们从这里开始，一起探索 <strong>STM32H7 的开发之旅</strong> 🚀。</p><blockquote><p>教程使用的是淘宝店铺 WeAct 售卖的 STM32H743VIT6 的核心板，如果引脚资源和你手上的不同，请自行调整代码。</p></blockquote>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;随着嵌入式应用需求的不断提升，&lt;strong&gt;STM32H7 系列&lt;/strong&gt;凭借高主频、强大的外设接口和丰富的存储特性，逐渐成为许多高性能场景的理想选择。无论是高速通信、大容量存储，还是复杂的多任务控制，H7 都能提供比 F1&amp;#x2F;F4 系列更宽裕的性能空间。</summary>
      
    
    
    
    <category term="mcu" scheme="https://blog.orangetime.top/categories/mcu/"/>
    
    
  </entry>
  
  <entry>
    <title>ThingsBoard Docker 部署指南</title>
    <link href="https://blog.orangetime.top/2025/06/21/docker/Docker-ThingsBoard/"/>
    <id>https://blog.orangetime.top/2025/06/21/docker/Docker-ThingsBoard/</id>
    <published>2025-06-21T07:48:24.000Z</published>
    <updated>2025-10-24T19:43:16.498Z</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:&#115;&#x79;&#x73;&#97;&#x64;&#109;&#105;&#x6e;&#64;&#x74;&#104;&#105;&#x6e;&#103;&#115;&#x62;&#x6f;&#x61;&#114;&#x64;&#46;&#x6f;&#114;&#x67;" >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:&#116;&#101;&#110;&#97;&#110;&#116;&#64;&#x74;&#104;&#x69;&#110;&#x67;&#x73;&#x62;&#111;&#x61;&#x72;&#x64;&#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:&#99;&#117;&#115;&#116;&#111;&#x6d;&#101;&#114;&#64;&#x74;&#x68;&#x69;&#110;&#103;&#115;&#98;&#111;&#x61;&#114;&#x64;&#x2e;&#x6f;&#114;&#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://emtime-picture.cn-nb1.rains3.com/2025/06/21/685691260679f.png"  ></center><center>   <img     src="https://emtime-picture.cn-nb1.rains3.com/2025/06/21/685692da81ca1.png"  ></center><p>可选择 default 或 thermostat 模板，这些模板预配置了 MQTT、HTTP 与 CoAP 通信协议，便于多种方式测试连通性（在我当前使用的版本中，MQTT 功能运行最稳定，CoAP反而无法正常使用）；</p><p>分配给客户，我选择了公开，这个决定了用户账户可以查看的设备；</p><center>   <img     src="https://emtime-picture.cn-nb1.rains3.com/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://emtime-picture.cn-nb1.rains3.com/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://emtime-picture.cn-nb1.rains3.com/2025/06/23/68583b1340fff.png"  ></center><p>接下来我就根据上面的表格，来演示一下 ThingsBoard 的属性与设备之间的交互；</p><h5 id="服务端属性"><a href="#服务端属性" class="headerlink" title="服务端属性"></a>服务端属性</h5><center>   <img     src="https://emtime-picture.cn-nb1.rains3.com/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://emtime-picture.cn-nb1.rains3.com/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://emtime-picture.cn-nb1.rains3.com/2025/06/23/68583efd769a6.png"  ></center><p>平台可以根据设备上报的属性，在仪表盘中展示设备信息，也可以在规则链库中根据属性值进行判断，如下：</p><center>   <img     src="https://emtime-picture.cn-nb1.rains3.com/2025/06/23/6858414f20529.png"  ></center><p>如上图即为在仪表盘中显示设备信息；</p><center>   <img     src="https://emtime-picture.cn-nb1.rains3.com/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://emtime-picture.cn-nb1.rains3.com/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="https://blog.orangetime.top/categories/docker/"/>
    
    
  </entry>
  
  <entry>
    <title>悲报</title>
    <link href="https://blog.orangetime.top/2025/05/17/bad/"/>
    <id>https://blog.orangetime.top/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="https://blog.orangetime.top/2025/02/22/other/Hexo-Blog/"/>
    <id>https://blog.orangetime.top/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="https://blog.orangetime.top/categories/other/"/>
    
    
  </entry>
  
  <entry>
    <title>通过 init 系统实现开机自启（以 systemd 和 OpenRC 为例）</title>
    <link href="https://blog.orangetime.top/2025/02/12/linux/systemd-OpenRC/"/>
    <id>https://blog.orangetime.top/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="https://blog.orangetime.top/categories/linux/"/>
    
    
  </entry>
  
  <entry>
    <title>SEGGER RTT：嵌入式调试的高效输出利器</title>
    <link href="https://blog.orangetime.top/2024/10/28/mcu/SEGGER-RTT/"/>
    <id>https://blog.orangetime.top/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="https://blog.orangetime.top/categories/mcu/"/>
    
    
  </entry>
  
  <entry>
    <title>实现 USB Composite 设备：在同一接口上模拟多个设备（含 FatFS U盘）</title>
    <link href="https://blog.orangetime.top/2024/10/27/mcu/USB-Composite/"/>
    <id>https://blog.orangetime.top/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://webdisk.orangetime.top/#s/EAMFFqgT" >AL94.I-CUBE-USBD-COMPOSITE.1.0.3.pack<i class="fas fa-external-link-alt"></i></a><br><strong>提取密码: zKxR3</strong></p><p>链接中是一个 pack 文件，但是这个文件并不是给 MDK 使用的，而是给 STM32CubeMX 使用的，我们需要在 STM32CubeMX 中导入这个文件，作为组件库引入我们的工程中；</p><p>导入文件步骤如下：</p><center>   <img     src="https://emtime-picture.cn-nb1.rains3.com/2025/06/25/685ae35335f95.png"  ></center><center>   <img     src="https://emtime-picture.cn-nb1.rains3.com/2025/06/25/685ae36f1d0a5.png"  ></center><h2 id="工程引入"><a href="#工程引入" class="headerlink" title="工程引入"></a>工程引入</h2><p>导入完成之后，我们还需要在 CubeMX 中勾选这个组件库，选择所需的功能 ，如下：</p><center>   <img     src="https://emtime-picture.cn-nb1.rains3.com/2025/06/25/685ae3b35928d.png"  ></center><center>   <img     src="https://emtime-picture.cn-nb1.rains3.com/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://emtime-picture.cn-nb1.rains3.com/2025/06/25/685ae5085d7e9.png"  ></center><ul><li>配置组件库</li></ul><center>   <img     src="https://emtime-picture.cn-nb1.rains3.com/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://emtime-picture.cn-nb1.rains3.com/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="https://blog.orangetime.top/categories/mcu/"/>
    
    
  </entry>
  
  <entry>
    <title>从 0 搭建 SPI Flash 文件系统：驱动、FatFS、读写与坑点</title>
    <link href="https://blog.orangetime.top/2024/10/26/mcu/FatFS/"/>
    <id>https://blog.orangetime.top/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>W25Qxx.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><span class="line">380</span><br><span class="line">381</span><br><span class="line">382</span><br><span class="line">383</span><br><span class="line">384</span><br><span class="line">385</span><br><span class="line">386</span><br><span class="line">387</span><br><span class="line">388</span><br><span class="line">389</span><br><span class="line">390</span><br><span class="line">391</span><br><span class="line">392</span><br><span class="line">393</span><br><span class="line">394</span><br><span class="line">395</span><br><span class="line">396</span><br><span class="line">397</span><br><span class="line">398</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;W25Qxx.h&quot;</span></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_">W25Qxx_Reset</span><span class="params">(<span class="type">void</span>)</span>;</span><br><span class="line"></span><br><span class="line">W25Qxx_Info_t W25Qxx_Info = &#123;<span class="number">0</span>&#125;;</span><br><span class="line"></span><br><span class="line">__weak <span class="type">void</span> <span class="title function_">W25Qxx_CS_Enable</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">&#125;</span><br><span class="line"></span><br><span class="line">__weak <span class="type">void</span> <span class="title function_">W25Qxx_CS_Disable</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">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 返回1是正常</span></span><br><span class="line">__weak <span class="type">uint8_t</span> <span class="title function_">W25Qxx_ReadByte</span><span class="params">(<span class="type">uint8_t</span>* RxData, <span class="type">uint16_t</span> Size)</span></span><br><span class="line">&#123;</span><br><span class="line">  (<span class="type">void</span>)RxData;</span><br><span class="line">  (<span class="type">void</span>)Size;</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><span class="line"></span><br><span class="line"><span class="comment">// 返回1是正常</span></span><br><span class="line">__weak <span class="type">uint8_t</span> <span class="title function_">W25Qxx_WriteByte</span><span class="params">(<span class="type">uint8_t</span>* TxData, <span class="type">uint16_t</span> Size)</span></span><br><span class="line">&#123;</span><br><span class="line">  (<span class="type">void</span>)TxData;</span><br><span class="line">  (<span class="type">void</span>)Size;</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><span class="line"></span><br><span class="line">__weak <span class="type">uint32_t</span> <span class="title function_">W25Qxx_GetTick</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> <span class="number">0</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">  * @brief  Initializes the W25Q128FV interface.</span></span><br><span class="line"><span class="comment">  * @retval 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_">W25Qxx_Init</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">  <span class="comment">/* Reset W25Qxx */</span></span><br><span class="line">  W25Qxx_Reset();</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> W25Qxx_GetStatus();</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">  * @brief  This function reset the W25Qx.</span></span><br><span class="line"><span class="comment">  * @retval None</span></span><br><span class="line"><span class="comment">  */</span></span><br><span class="line"><span class="type">static</span> <span class="type">void</span> <span class="title function_">W25Qxx_Reset</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> cmd[<span class="number">2</span>] = &#123;RESET_ENABLE_CMD, RESET_MEMORY_CMD&#125;;</span><br><span class="line"></span><br><span class="line">  W25Qxx_CS_Enable();</span><br><span class="line">  <span class="comment">/* Send the reset command */</span></span><br><span class="line">  W25Qxx_WriteByte(cmd, <span class="number">2</span>);</span><br><span class="line">  W25Qxx_CS_Disable();</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">  * @brief  Reads current status of the W25Q128FV.</span></span><br><span class="line"><span class="comment">  * @retval W25Q128FV memory 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_">W25Qxx_GetStatus</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> cmd[] = &#123;READ_STATUS_REG1_CMD&#125;;</span><br><span class="line">  <span class="type">uint8_t</span> status;</span><br><span class="line"></span><br><span class="line">  W25Qxx_CS_Enable();</span><br><span class="line">  <span class="comment">/* Send the read status command */</span></span><br><span class="line">  W25Qxx_WriteByte(cmd, <span class="number">1</span>);</span><br><span class="line">  <span class="comment">/* Reception of the data */</span></span><br><span class="line">  W25Qxx_ReadByte(&amp;status, <span class="number">1</span>);</span><br><span class="line">  W25Qxx_CS_Disable();</span><br><span class="line"></span><br><span class="line">  <span class="comment">/* Check the value of the register */</span></span><br><span class="line">  <span class="keyword">if</span>((status &amp; W25QXX_FSR_BUSY) != <span class="number">0</span>)</span><br><span class="line">  &#123;</span><br><span class="line">    <span class="keyword">return</span> W25QXX_BUSY;</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">return</span> W25QXX_OK;</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">  * @brief  This function send a Write Enable and wait it is effective.</span></span><br><span class="line"><span class="comment">  * @retval 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_">W25Qxx_WriteEnable</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> cmd[] = &#123;WRITE_ENABLE_CMD&#125;;</span><br><span class="line">  <span class="type">uint32_t</span> tickstart = W25Qxx_GetTick();</span><br><span class="line"></span><br><span class="line">  <span class="comment">/*Select the FLASH: Chip Select low */</span></span><br><span class="line">  W25Qxx_CS_Enable();</span><br><span class="line">  <span class="comment">/* Send the read ID command */</span></span><br><span class="line">  W25Qxx_WriteByte(cmd, <span class="number">1</span>);</span><br><span class="line">  <span class="comment">/*Deselect the FLASH: Chip Select high */</span></span><br><span class="line">  W25Qxx_CS_Disable();</span><br><span class="line"></span><br><span class="line">  <span class="comment">/* Wait the end of Flash writing */</span></span><br><span class="line">  <span class="keyword">while</span>(W25Qxx_GetStatus() == W25QXX_BUSY)</span><br><span class="line">  &#123;</span><br><span class="line">    <span class="comment">/* Check for the Timeout */</span></span><br><span class="line">    <span class="keyword">if</span>((W25Qxx_GetTick() - tickstart) &gt; W25QXX_TIMEOUT_VALUE)</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="keyword">return</span> W25QXX_TIMEOUT;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> W25QXX_OK;</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">  * @brief  Read Manufacture/Device ID.</span></span><br><span class="line"><span class="comment">  * @param  return value address</span></span><br><span class="line"><span class="comment">  * @retval 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_">W25Qxx_Read_ID</span><span class="params">(<span class="type">uint8_t</span>* ID)</span></span><br><span class="line">&#123;</span><br><span class="line">  <span class="type">uint8_t</span> cmd[<span class="number">4</span>] = &#123;READ_JEDEC_ID_CMD, <span class="number">0x00</span>, <span class="number">0x00</span>, <span class="number">0x00</span>&#125;;</span><br><span class="line"></span><br><span class="line">  W25Qxx_CS_Enable();</span><br><span class="line">  <span class="comment">/* Send the read ID command */</span></span><br><span class="line">  W25Qxx_WriteByte(cmd, <span class="number">1</span>);</span><br><span class="line">  <span class="comment">/* Reception of the data */</span></span><br><span class="line">  W25Qxx_ReadByte(ID, <span class="number">3</span>);</span><br><span class="line">  W25Qxx_CS_Disable();</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_">W25Qxx_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="type">uint8_t</span> temp_id[<span class="number">3</span>];</span><br><span class="line"></span><br><span class="line">  <span class="comment">/* Read FLASH ID */</span></span><br><span class="line">  W25Qxx_Read_ID(temp_id);</span><br><span class="line"></span><br><span class="line">  W25Qxx_Info.Flash_ID = ((<span class="type">uint32_t</span>)temp_id[<span class="number">0</span>] &lt;&lt; <span class="number">16</span>) | </span><br><span class="line">                         ((<span class="type">uint32_t</span>)temp_id[<span class="number">1</span>] &lt;&lt; <span class="number">8</span>)  | </span><br><span class="line">                         ((<span class="type">uint32_t</span>)temp_id[<span class="number">2</span>]);</span><br><span class="line"></span><br><span class="line">  W25Qxx_Info.Flash_Sector_Count = <span class="number">0x00</span>;</span><br><span class="line">  W25Qxx_Info.Flash_Sector_Size  = <span class="number">0x00</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">switch</span>(W25Qxx_Info.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>((W25Qxx_Info.Flash_ID != <span class="number">0xFFFFFFFF</span>) &amp;&amp; (W25Qxx_Info.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">    W25Qxx_Info.Flash_Sector_Count = count / DEF_SECTOR_SIZE; <span class="comment">// DEF_SECTOR_SIZE;</span></span><br><span class="line">    W25Qxx_Info.Flash_Sector_Size  = DEF_SECTOR_SIZE;         <span class="comment">// DEF_SECTOR_SIZE;</span></span><br><span class="line">    W25Qxx_Info.Flash_Page_Size = <span class="number">256</span>;                        <span class="comment">// 全系列固定的</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">  * @brief  Reads an amount of data from the QSPI memory.</span></span><br><span class="line"><span class="comment">  * @param  pData: Pointer to data to be read</span></span><br><span class="line"><span class="comment">  * @param  ReadAddr: Read start address</span></span><br><span class="line"><span class="comment">  * @param  Size: Size of data to read</span></span><br><span class="line"><span class="comment">  * @retval QSPI memory status</span></span><br><span class="line"><span class="comment">  */</span></span><br><span class="line"><span class="comment">// TODO</span></span><br><span class="line"><span class="type">uint8_t</span> W25Qxx_Read(<span class="type">uint8_t</span>* pData, <span class="type">uint32_t</span> ReadAddr, <span class="type">uint32_t</span> Size)</span><br><span class="line">&#123;</span><br><span class="line">  <span class="type">uint8_t</span> cmd[<span class="number">4</span>];</span><br><span class="line"></span><br><span class="line">  <span class="comment">/* Configure the command */</span></span><br><span class="line">  cmd[<span class="number">0</span>] = READ_CMD;</span><br><span class="line">  cmd[<span class="number">1</span>] = (<span class="type">uint8_t</span>)(ReadAddr &gt;&gt; <span class="number">16</span>);</span><br><span class="line">  cmd[<span class="number">2</span>] = (<span class="type">uint8_t</span>)(ReadAddr &gt;&gt; <span class="number">8</span>);</span><br><span class="line">  cmd[<span class="number">3</span>] = (<span class="type">uint8_t</span>)(ReadAddr);</span><br><span class="line"></span><br><span class="line">  W25Qxx_CS_Enable();</span><br><span class="line">  <span class="comment">/* Send the read ID command */</span></span><br><span class="line">  W25Qxx_WriteByte(cmd, <span class="number">4</span>);</span><br><span class="line">  <span class="comment">/* Reception of the data */</span></span><br><span class="line">  <span class="keyword">if</span>(W25Qxx_ReadByte(pData, Size) == <span class="number">0</span>)</span><br><span class="line">  &#123;</span><br><span class="line">    <span class="keyword">return</span> W25QXX_ERROR;</span><br><span class="line">  &#125;</span><br><span class="line">  W25Qxx_CS_Disable();</span><br><span class="line">  <span class="keyword">return</span> W25QXX_OK;</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">  * @brief  Writes an amount of data to the QSPI memory.</span></span><br><span class="line"><span class="comment">  * @param  pData: Pointer to data to be written</span></span><br><span class="line"><span class="comment">  * @param  WriteAddr: Write start address</span></span><br><span class="line"><span class="comment">  * @param  Size: Size of data to write,No more than 256byte.</span></span><br><span class="line"><span class="comment">  * @retval QSPI memory 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_">W25Qxx_Write</span><span class="params">(<span class="type">uint8_t</span>* pData, <span class="type">uint32_t</span> WriteAddr, <span class="type">uint32_t</span> Size)</span></span><br><span class="line">&#123;</span><br><span class="line">  <span class="type">uint8_t</span> cmd[<span class="number">4</span>];</span><br><span class="line">  <span class="type">uint32_t</span> end_addr, current_size, current_addr;</span><br><span class="line">  <span class="type">uint32_t</span> tickstart = W25Qxx_GetTick();</span><br><span class="line"></span><br><span class="line">  <span class="comment">/* Calculation of the size between the write address and the end of the page */</span></span><br><span class="line">  current_addr = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">while</span>(current_addr &lt;= WriteAddr)</span><br><span class="line">  &#123;</span><br><span class="line">    current_addr += W25Qxx_Info.Flash_Page_Size;</span><br><span class="line">  &#125;</span><br><span class="line">  current_size = current_addr - WriteAddr;</span><br><span class="line"></span><br><span class="line">  <span class="comment">/* Check if the size of the data is less than the remaining place in the page */</span></span><br><span class="line">  <span class="keyword">if</span>(current_size &gt; Size)</span><br><span class="line">  &#123;</span><br><span class="line">    current_size = Size;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">/* Initialize the adress variables */</span></span><br><span class="line">  current_addr = WriteAddr;</span><br><span class="line">  end_addr = WriteAddr + Size;</span><br><span class="line"></span><br><span class="line">  <span class="comment">/* Perform the write page by page */</span></span><br><span class="line">  <span class="keyword">do</span></span><br><span class="line">  &#123;</span><br><span class="line">    <span class="comment">/* Configure the command */</span></span><br><span class="line">    cmd[<span class="number">0</span>] = PAGE_PROG_CMD;</span><br><span class="line">    cmd[<span class="number">1</span>] = (<span class="type">uint8_t</span>)(current_addr &gt;&gt; <span class="number">16</span>);</span><br><span class="line">    cmd[<span class="number">2</span>] = (<span class="type">uint8_t</span>)(current_addr &gt;&gt; <span class="number">8</span>);</span><br><span class="line">    cmd[<span class="number">3</span>] = (<span class="type">uint8_t</span>)(current_addr);</span><br><span class="line"></span><br><span class="line">    <span class="comment">/* Enable write operations */</span></span><br><span class="line">    W25Qxx_WriteEnable();</span><br><span class="line"></span><br><span class="line">    W25Qxx_CS_Enable();</span><br><span class="line">    <span class="comment">/* Send the command */</span></span><br><span class="line">    <span class="keyword">if</span>(W25Qxx_WriteByte(cmd, <span class="number">4</span>) == <span class="number">0</span>)</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="keyword">return</span> W25QXX_ERROR;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/* Transmission of the data */</span></span><br><span class="line">    <span class="keyword">if</span>(W25Qxx_WriteByte(pData, current_size) == <span class="number">0</span>)</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="keyword">return</span> W25QXX_ERROR;</span><br><span class="line">    &#125;</span><br><span class="line">    W25Qxx_CS_Disable();</span><br><span class="line">    <span class="comment">/* Wait the end of Flash writing */</span></span><br><span class="line">    <span class="keyword">while</span>(W25Qxx_GetStatus() == W25QXX_BUSY)</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="comment">/* Check for the Timeout */</span></span><br><span class="line">      <span class="keyword">if</span>((W25Qxx_GetTick() - tickstart) &gt; W25QXX_TIMEOUT_VALUE)</span><br><span class="line">      &#123;</span><br><span class="line">        <span class="keyword">return</span> W25QXX_TIMEOUT;</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">/* Update the address and size variables for next page programming */</span></span><br><span class="line">    current_addr += current_size;</span><br><span class="line">    pData += current_size;</span><br><span class="line">    current_size = ((current_addr + W25Qxx_Info.Flash_Page_Size) &gt; end_addr) ? (end_addr - current_addr) : W25Qxx_Info.Flash_Page_Size;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">while</span>(current_addr &lt; end_addr);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> W25QXX_OK;</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">  * @brief  Erases the specified block of the QSPI memory.</span></span><br><span class="line"><span class="comment">  * @param  BlockAddress: Block address to erase</span></span><br><span class="line"><span class="comment">  * @retval QSPI memory 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_">W25Qxx_Erase_Block</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> cmd[<span class="number">4</span>];</span><br><span class="line">  <span class="type">uint32_t</span> tickstart = W25Qxx_GetTick();</span><br><span class="line">  cmd[<span class="number">0</span>] = SECTOR_ERASE_CMD;</span><br><span class="line">  cmd[<span class="number">1</span>] = (<span class="type">uint8_t</span>)(Address &gt;&gt; <span class="number">16</span>);</span><br><span class="line">  cmd[<span class="number">2</span>] = (<span class="type">uint8_t</span>)(Address &gt;&gt; <span class="number">8</span>);</span><br><span class="line">  cmd[<span class="number">3</span>] = (<span class="type">uint8_t</span>)(Address);</span><br><span class="line"></span><br><span class="line">  <span class="comment">/* Enable write operations */</span></span><br><span class="line">  W25Qxx_WriteEnable();</span><br><span class="line"></span><br><span class="line">  <span class="comment">/*Select the FLASH: Chip Select low */</span></span><br><span class="line">  W25Qxx_CS_Enable();</span><br><span class="line">  <span class="comment">/* Send the read ID command */</span></span><br><span class="line">  W25Qxx_WriteByte(cmd, <span class="number">4</span>);</span><br><span class="line">  <span class="comment">/*Deselect the FLASH: Chip Select high */</span></span><br><span class="line">  W25Qxx_CS_Disable();</span><br><span class="line"></span><br><span class="line">  <span class="comment">/* Wait the end of Flash writing */</span></span><br><span class="line">  <span class="keyword">while</span>(W25Qxx_GetStatus() == W25QXX_BUSY)</span><br><span class="line">  &#123;</span><br><span class="line">    <span class="comment">/* Check for the Timeout */</span></span><br><span class="line">    <span class="keyword">if</span>((W25Qxx_GetTick() - tickstart) &gt; W25QXX_SECTOR_ERASE_MAX_TIME)</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="keyword">return</span> W25QXX_TIMEOUT;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> W25QXX_OK;</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">  * @brief  Erases the entire QSPI memory.This function will take a very long time.</span></span><br><span class="line"><span class="comment">  * @retval QSPI memory 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_">W25Qxx_Erase_Chip</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> cmd[<span class="number">4</span>];</span><br><span class="line">  <span class="type">uint32_t</span> tickstart = W25Qxx_GetTick();</span><br><span class="line">  cmd[<span class="number">0</span>] = CHIP_ERASE_CMD;</span><br><span class="line"></span><br><span class="line">  <span class="comment">/* Enable write operations */</span></span><br><span class="line">  W25Qxx_WriteEnable();</span><br><span class="line"></span><br><span class="line">  <span class="comment">/*Select the FLASH: Chip Select low */</span></span><br><span class="line">  W25Qxx_CS_Enable();</span><br><span class="line">  <span class="comment">/* Send the read ID command */</span></span><br><span class="line">  W25Qxx_WriteByte(cmd, <span class="number">1</span>);</span><br><span class="line">  <span class="comment">/*Deselect the FLASH: Chip Select high */</span></span><br><span class="line">  W25Qxx_CS_Disable();</span><br><span class="line"></span><br><span class="line">  <span class="comment">/* Wait the end of Flash writing */</span></span><br><span class="line">  <span class="keyword">while</span>(W25Qxx_GetStatus() != W25QXX_BUSY)</span><br><span class="line">  &#123;</span><br><span class="line">    <span class="comment">/* Check for the Timeout */</span></span><br><span class="line">    <span class="keyword">if</span>((W25Qxx_GetTick() - tickstart) &gt; W25QXX_BULK_ERASE_MAX_TIME)</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="keyword">return</span> W25QXX_TIMEOUT;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> W25QXX_OK;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>W25Qxx.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><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></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">ifndef</span> __W25QXX_H_</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> __W25QXX_H_</span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;stdint.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> W25QXX_BULK_ERASE_MAX_TIME         250000</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> W25QXX_SECTOR_ERASE_MAX_TIME       3000</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> W25QXX_SUBSECTOR_ERASE_MAX_TIME    800</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> W25QXX_TIMEOUT_VALUE 1000</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="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">/* Reset Operations */</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> RESET_ENABLE_CMD          0x66</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> RESET_MEMORY_CMD          0x99</span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> ENTER_QPI_MODE_CMD        0x38</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> EXIT_QPI_MODE_CMD         0xFF</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/* Identification Operations */</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> READ_ID_CMD               0x90</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> DUAL_READ_ID_CMD          0x92</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> QUAD_READ_ID_CMD          0x94</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> READ_JEDEC_ID_CMD         0x9F</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/* Read Operations */</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> READ_CMD                  0x03</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> FAST_READ_CMD             0x0B</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> DUAL_OUT_FAST_READ_CMD    0x3B</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> DUAL_INOUT_FAST_READ_CMD  0xBB</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> QUAD_OUT_FAST_READ_CMD    0x6B</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> QUAD_INOUT_FAST_READ_CMD  0xEB</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/* Write Operations */</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> WRITE_ENABLE_CMD          0x06</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> WRITE_DISABLE_CMD         0x04</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/* Register Operations */</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> READ_STATUS_REG1_CMD      0x05</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> READ_STATUS_REG2_CMD      0x35</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> READ_STATUS_REG3_CMD      0x15</span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> WRITE_STATUS_REG1_CMD     0x01</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> WRITE_STATUS_REG2_CMD     0x31</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> WRITE_STATUS_REG3_CMD     0x11</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/* Program Operations */</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> PAGE_PROG_CMD             0x02</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> QUAD_INPUT_PAGE_PROG_CMD  0x32</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/* Erase Operations */</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> SECTOR_ERASE_CMD          0x20</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> CHIP_ERASE_CMD            0xC7</span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> PROG_ERASE_RESUME_CMD     0x7A</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> PROG_ERASE_SUSPEND_CMD    0x75</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/* Flag Status Register */</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> W25QXX_FSR_BUSY            ((uint8_t)0x01)    <span class="comment">/*!&lt; busy */</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> W25QXX_FSR_WREN            ((uint8_t)0x02)    <span class="comment">/*!&lt; write enable */</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> W25QXX_FSR_QE              ((uint8_t)0x02)    <span class="comment">/*!&lt; quad enable */</span></span></span><br><span class="line"></span><br><span class="line"><span class="comment">/* Status */</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> W25QXX_OK            ((uint8_t)0x00)</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> W25QXX_ERROR         ((uint8_t)0x01)</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> W25QXX_BUSY          ((uint8_t)0x02)</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> W25QXX_TIMEOUT       ((uint8_t)0x03)</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span></span></span><br><span class="line"><span class="class">&#123;</span></span><br><span class="line">  <span class="type">uint32_t</span> Flash_ID;</span><br><span class="line">  <span class="type">uint32_t</span> Flash_Sector_Count;</span><br><span class="line">  <span class="type">uint32_t</span> Flash_Page_Size;</span><br><span class="line">  <span class="type">uint16_t</span> Flash_Sector_Size;</span><br><span class="line">&#125;W25Qxx_Info_t;</span><br><span class="line"></span><br><span class="line"><span class="keyword">extern</span> W25Qxx_Info_t W25Qxx_Info;</span><br><span class="line"></span><br><span class="line"><span class="type">uint8_t</span> <span class="title function_">W25Qxx_Init</span><span class="params">(<span class="type">void</span>)</span>;  <span class="comment">// 必须执行</span></span><br><span class="line"><span class="type">uint8_t</span> <span class="title function_">W25Qxx_GetStatus</span><span class="params">(<span class="type">void</span>)</span>;</span><br><span class="line"><span class="type">uint8_t</span> <span class="title function_">W25Qxx_WriteEnable</span><span class="params">(<span class="type">void</span>)</span>;</span><br><span class="line"><span class="type">void</span> <span class="title function_">W25Qxx_Read_ID</span><span class="params">(<span class="type">uint8_t</span>* ID)</span>;</span><br><span class="line"><span class="type">void</span> <span class="title function_">W25Qxx_IC_Check</span><span class="params">(<span class="type">void</span>)</span>; <span class="comment">// 必须执行</span></span><br><span class="line"><span class="type">uint8_t</span> <span class="title function_">W25Qxx_Read</span><span class="params">(<span class="type">uint8_t</span>* pData, <span class="type">uint32_t</span> ReadAddr, <span class="type">uint32_t</span> Size)</span>;</span><br><span class="line"><span class="type">uint8_t</span> <span class="title function_">W25Qxx_Write</span><span class="params">(<span class="type">uint8_t</span>* pData, <span class="type">uint32_t</span> WriteAddr, <span class="type">uint32_t</span> Size)</span>;</span><br><span class="line"><span class="type">uint8_t</span> <span class="title function_">W25Qxx_Erase_Block</span><span class="params">(<span class="type">uint32_t</span> Address)</span>;</span><br><span class="line"><span class="type">uint8_t</span> <span class="title function_">W25Qxx_Erase_Chip</span><span class="params">(<span class="type">void</span>)</span>;</span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">endif</span></span></span><br></pre></td></tr></table></figure><p>该驱动程序在SPI1上实现了对SPI FLASH的读写操作，包括初始化、读取ID、写入使能、写入禁用、读取状态寄存器、检查IC、擦除扇区、读取块、写入块等操作。</p><p>我将与单片机硬件相关的函数都使用弱定义进行了声明，这样在移植的时候，只需要在别的文件中实现硬件操作即可，不需要修改其他文件。比如，你可以在 spi.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="type">void</span> <span class="title function_">W25Qxx_CS_Enable</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">  HAL_GPIO_WritePin(SPI1_CS_GPIO_Port, SPI1_CS_Pin, GPIO_PIN_RESET);</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_">W25Qxx_CS_Disable</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">  HAL_GPIO_WritePin(SPI1_CS_GPIO_Port, SPI1_CS_Pin, GPIO_PIN_SET);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 返回1是正常</span></span><br><span class="line"><span class="type">uint8_t</span> <span class="title function_">W25Qxx_ReadByte</span><span class="params">(<span class="type">uint8_t</span>* RxData, <span class="type">uint16_t</span> Size)</span></span><br><span class="line">&#123;</span><br><span class="line">  <span class="keyword">return</span> (HAL_SPI_Receive(&amp;hspi1, RxData, Size, <span class="number">0xFF</span>) == HAL_OK);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 返回1是正常</span></span><br><span class="line"><span class="type">uint8_t</span> <span class="title function_">W25Qxx_WriteByte</span><span class="params">(<span class="type">uint8_t</span>* TxData, <span class="type">uint16_t</span> Size)</span></span><br><span class="line">&#123;</span><br><span class="line">  <span class="keyword">return</span> (HAL_SPI_Transmit(&amp;hspi1, TxData, Size, <span class="number">0xFF</span>) == HAL_OK);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">uint32_t</span> <span class="title function_">W25Qxx_GetTick</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> HAL_GetTick();</span><br><span class="line">&#125;</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></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;W25Qxx.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="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 = W25Qxx_GetStatus();</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> W25QXX_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> W25QXX_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> W25QXX_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> W25QXX_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 = W25Qxx_Init();</span><br><span class="line">    W25Qxx_IC_Check();</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> W25QXX_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> W25QXX_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> W25QXX_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> W25QXX_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">/* 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">    result = W25Qxx_Read(buff, sector * W25Qxx_Info.Flash_Sector_Size, count * W25Qxx_Info.Flash_Sector_Size);</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> W25QXX_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> W25QXX_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> W25QXX_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> W25QXX_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><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 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">      W25Qxx_Erase_Block((sector + i) * W25Qxx_Info.Flash_Sector_Size);</span><br><span class="line">    &#125;</span><br><span class="line">    result = W25Qxx_Write((<span class="type">uint8_t</span>*)buff, sector * W25Qxx_Info.Flash_Sector_Size, count * W25Qxx_Info.Flash_Sector_Size);</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> W25QXX_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> W25QXX_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> W25QXX_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> W25QXX_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><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><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">      *(DWORD*)buff = W25Qxx_Info.Flash_Sector_Count;</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 = W25Qxx_Info.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="https://blog.orangetime.top/categories/mcu/"/>
    
    
  </entry>
  
  <entry>
    <title>Docker部署MQTT服务器</title>
    <link href="https://blog.orangetime.top/2024/08/20/docker/Docker-EMQX/"/>
    <id>https://blog.orangetime.top/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="https://blog.orangetime.top/categories/docker/"/>
    
    
  </entry>
  
  <entry>
    <title>Docker部署Certd，实现HTTPS证书自动申请与更新</title>
    <link href="https://blog.orangetime.top/2024/08/19/docker/Docker-Certd/"/>
    <id>https://blog.orangetime.top/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="https://blog.orangetime.top/categories/docker/"/>
    
    
  </entry>
  
  <entry>
    <title>Caddy 反向代理</title>
    <link href="https://blog.orangetime.top/2024/08/19/linux/Caddy/"/>
    <id>https://blog.orangetime.top/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="https://blog.orangetime.top/categories/linux/"/>
    
    
  </entry>
  
  <entry>
    <title>Docker部署密码管理工具</title>
    <link href="https://blog.orangetime.top/2024/08/18/docker/Docker-Bitwarden/"/>
    <id>https://blog.orangetime.top/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://emtime-picture.cn-nb1.rains3.com/2025/06/14/684c6e6229c19.png"  ></center><center>   <img     src="https://emtime-picture.cn-nb1.rains3.com/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="https://blog.orangetime.top/categories/docker/"/>
    
    
  </entry>
  
  <entry>
    <title>Docker部署Gogs（个人GIthub）</title>
    <link href="https://blog.orangetime.top/2024/08/16/docker/Docker-Gogs/"/>
    <id>https://blog.orangetime.top/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="https://blog.orangetime.top/categories/docker/"/>
    
    
  </entry>
  
</feed>
