<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Wayson&#39;s Blog</title>
  
  <subtitle>C/C++ | RTOS | Linux 嵌入式开发博客</subtitle>
  <link href="https://w4ysonch.github.io/atom.xml" rel="self"/>
  
  <link href="https://w4ysonch.github.io/"/>
  <updated>2026-06-29T07:50:23.716Z</updated>
  <id>https://w4ysonch.github.io/</id>
  
  <author>
    <name>Wayson</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>从零拆解 embedmq：一个纯 C 实现的嵌入式线程间事件总线</title>
    <link href="https://w4ysonch.github.io/2026/06/20/embedmq-architecture/"/>
    <id>https://w4ysonch.github.io/2026/06/20/embedmq-architecture/</id>
    <published>2026-06-20T04:59:47.000Z</published>
    <updated>2026-06-29T07:50:23.716Z</updated>
    
    <content type="html"><![CDATA[<h3 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h3><p>嵌入式项目里，线程间通信是绕不开的问题。传感器线程读到数据，UI 线程要更新显示，网络线程要上报——它们之间怎么传消息？</p><p>最直接的做法是让模块互相持有对方的指针或队列句柄，但代价是强耦合：改一个模块，另一个也要跟着改。</p><p><strong><a href="https://github.com/w4ysonch/embedmq">embedmq</a></strong> 是我写的一个零依赖 C11 库，把线程间消息分发压缩成三个函数：<code>create</code>、<code>register</code>、<code>post</code>。本文从源码层面拆解它的每一个设计决策。</p><p><img src="/images/embedmq/arch.png" alt="架构图"></p><hr><h3 id="一、传统做法的痛点"><a href="#一、传统做法的痛点" class="headerlink" title="一、传统做法的痛点"></a>一、传统做法的痛点</h3><h4 id="裸机：flag-泛滥"><a href="#裸机：flag-泛滥" class="headerlink" title="裸机：flag 泛滥"></a>裸机：flag 泛滥</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">volatile</span> <span class="type">bool</span> g_uart_ready   = <span class="literal">false</span>;</span><br><span class="line"><span class="keyword">volatile</span> <span class="type">bool</span> g_sensor_ready = <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">main</span><span class="params">(<span class="type">void</span>)</span> &#123;</span><br><span class="line">    <span class="keyword">while</span> (<span class="number">1</span>) &#123;</span><br><span class="line">        <span class="keyword">if</span> (g_uart_ready)   &#123; process_uart();   g_uart_ready   = <span class="literal">false</span>; &#125;</span><br><span class="line">        <span class="keyword">if</span> (g_sensor_ready) &#123; update_display();  g_sensor_ready = <span class="literal">false</span>; &#125;</span><br><span class="line">        <span class="comment">// 每加一个功能，就要在这里加一个 if</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>功能少时还好，一旦 flag 超过十几个，<code>main.c</code> 就变成了垃圾桶。增删任何功能都要修改主循环。</p><h4 id="RTOS：Queue-句柄散落各处"><a href="#RTOS：Queue-句柄散落各处" class="headerlink" title="RTOS：Queue 句柄散落各处"></a>RTOS：Queue 句柄散落各处</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></pre></td><td class="code"><pre><span class="line"><span class="type">void</span> <span class="title function_">SensorTask</span><span class="params">(<span class="type">void</span> *p)</span> &#123;</span><br><span class="line">    <span class="type">sensor_data_t</span> data;</span><br><span class="line">    <span class="keyword">while</span> (<span class="number">1</span>) &#123;</span><br><span class="line">        ReadSensor(&amp;data);</span><br><span class="line">        xQueueSend(ui_queue,  &amp;data, <span class="number">0</span>);  <span class="comment">// 传感器任务必须知道 UI 队列</span></span><br><span class="line">        xQueueSend(log_queue, &amp;data, <span class="number">0</span>);  <span class="comment">// 还要知道日志队列</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>生产者和消费者互相知道对方的存在，增加一个消费者就要修改生产者代码，违反开闭原则。</p><h4 id="Linux：重复造轮子"><a href="#Linux：重复造轮子" class="headerlink" title="Linux：重复造轮子"></a>Linux：重复造轮子</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">pthread_mutex_lock(&amp;data_mutex);</span><br><span class="line">shared_data = new_value;</span><br><span class="line">pthread_cond_signal(&amp;data_cond);</span><br><span class="line">pthread_mutex_unlock(&amp;data_mutex);</span><br></pre></td></tr></table></figure><p>每个项目都在重写同样的 mutex + 条件变量组合，且容易死锁。</p><hr><h3 id="二、embedmq-的解法：发布-订阅解耦"><a href="#二、embedmq-的解法：发布-订阅解耦" class="headerlink" title="二、embedmq 的解法：发布-订阅解耦"></a>二、embedmq 的解法：发布-订阅解耦</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 生产者只管发，不知道谁在监听</span></span><br><span class="line">embedmq_post(q, <span class="string">&quot;sensor.temp&quot;</span>, &amp;data, <span class="keyword">sizeof</span>(data));</span><br><span class="line"></span><br><span class="line"><span class="comment">// 消费者只管注册，不知道谁在发</span></span><br><span class="line">embedmq_register(q, <span class="string">&quot;sensor.temp&quot;</span>, on_temp, <span class="literal">NULL</span>);</span><br></pre></td></tr></table></figure><p>两边通过<strong>事件名字符串</strong>约定，库负责中间的一切：哈希、队列、线程、派发。</p><p><strong>需要明确的一点</strong>：每个事件名只能绑定<strong>一个</strong> handler。embedmq 解决的是模块解耦，不是一对多广播。如果需要多个模块响应同一个事件，可以用不同的名字：</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">embedmq_register(q, <span class="string">&quot;sensor.temp.ui&quot;</span>,  on_ui,  <span class="literal">NULL</span>);</span><br><span class="line">embedmq_register(q, <span class="string">&quot;sensor.temp.log&quot;</span>, on_log, <span class="literal">NULL</span>);</span><br></pre></td></tr></table></figure><h4 id="一个完整的三线程例子"><a href="#一个完整的三线程例子" class="headerlink" title="一个完整的三线程例子"></a>一个完整的三线程例子</h4><p>用 embedmq 重写开头的问题场景，感受一下解耦的效果：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;embedmq.h&quot;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;pthread.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;stdio.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;unistd.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> &#123;</span> <span class="type">float</span> celsius; <span class="type">int</span> humidity; &#125; <span class="type">sensor_data_t</span>;</span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> &#123;</span> <span class="type">int</span> rssi; <span class="type">char</span> ssid[<span class="number">32</span>]; &#125; <span class="type">wifi_info_t</span>;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> <span class="type">embedmq_t</span> *g_bus;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* ── 消费者：UI 线程注册的 handler，启动时绑定一次 ── */</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_">on_sensor</span><span class="params">(<span class="type">const</span> <span class="type">void</span> *data, <span class="type">size_t</span> size, <span class="type">void</span> *ctx)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">const</span> <span class="type">sensor_data_t</span> *d = data;</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">&quot;[UI] 温度: %.1f°C  湿度: %d%%\n&quot;</span>, d-&gt;celsius, d-&gt;humidity);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> <span class="type">void</span> <span class="title function_">on_wifi_connected</span><span class="params">(<span class="type">const</span> <span class="type">void</span> *data, <span class="type">size_t</span> size, <span class="type">void</span> *ctx)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">const</span> <span class="type">wifi_info_t</span> *w = data;</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">&quot;[UI] WiFi 已连接: %s (RSSI=%d)\n&quot;</span>, w-&gt;ssid, w-&gt;rssi);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* ── 生产者：传感器线程，只管发，完全不知道 UI 的存在 ── */</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_">sensor_thread</span><span class="params">(<span class="type">void</span> *arg)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">sensor_data_t</span> d = &#123;<span class="number">0</span>&#125;;</span><br><span class="line">    <span class="keyword">while</span> (<span class="number">1</span>) &#123;</span><br><span class="line">        d.celsius  = <span class="number">25.0f</span> + (rand() % <span class="number">50</span>) * <span class="number">0.1f</span>;</span><br><span class="line">        d.humidity = <span class="number">55</span> + rand() % <span class="number">20</span>;</span><br><span class="line">        embedmq_post(g_bus, <span class="string">&quot;sensor.update&quot;</span>, &amp;d, <span class="keyword">sizeof</span>(d));</span><br><span class="line">        usleep(<span class="number">100000</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">NULL</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><br><span class="line"><span class="type">static</span> <span class="type">void</span> *<span class="title function_">network_thread</span><span class="params">(<span class="type">void</span> *arg)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">wifi_info_t</span> w = &#123; .rssi = <span class="number">-55</span> &#125;;</span><br><span class="line">    <span class="built_in">snprintf</span>(w.ssid, <span class="keyword">sizeof</span>(w.ssid), <span class="string">&quot;HomeNetwork&quot;</span>);</span><br><span class="line">    embedmq_post(g_bus, <span class="string">&quot;wifi.connected&quot;</span>, &amp;w, <span class="keyword">sizeof</span>(w));</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">&#125;</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">    g_bus = embedmq_create(<span class="literal">NULL</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">/* 启动阶段统一注册，两个生产者线程完全不需要知道这里 */</span></span><br><span class="line">    embedmq_register(g_bus, <span class="string">&quot;sensor.update&quot;</span>,  on_sensor,         <span class="literal">NULL</span>);</span><br><span class="line">    embedmq_register(g_bus, <span class="string">&quot;wifi.connected&quot;</span>, on_wifi_connected, <span class="literal">NULL</span>);</span><br><span class="line"></span><br><span class="line">    <span class="type">pthread_t</span> t1, t2;</span><br><span class="line">    pthread_create(&amp;t1, <span class="literal">NULL</span>, sensor_thread,  <span class="literal">NULL</span>);</span><br><span class="line">    pthread_create(&amp;t2, <span class="literal">NULL</span>, network_thread, <span class="literal">NULL</span>);</span><br><span class="line"></span><br><span class="line">    sleep(<span class="number">1</span>);</span><br><span class="line">    embedmq_destroy(g_bus);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>sensor_thread</code> 和 <code>network_thread</code> 里没有任何 UI 相关的引用，以后换掉 UI 层什么都不用改。</p><hr><h3 id="三、核心机制深度拆解"><a href="#三、核心机制深度拆解" class="headerlink" title="三、核心机制深度拆解"></a>三、核心机制深度拆解</h3><h4 id="3-1-一条消息的完整旅程"><a href="#3-1-一条消息的完整旅程" class="headerlink" title="3.1 一条消息的完整旅程"></a>3.1 一条消息的完整旅程</h4><p>这是理解整个库最重要的部分。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><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><br><span class="line">       │                                       │</span><br><span class="line">       │  embedmq_post(&quot;sensor.temp&quot;, &amp;d, n)  │</span><br><span class="line">       │                                       │</span><br><span class="line">       ▼                                       │</span><br><span class="line">  1. FNV-1a hash(&quot;sensor.temp&quot;) → UUID        │</span><br><span class="line">       │                                       │</span><br><span class="line">       ▼                                       │</span><br><span class="line">  2. mutex 加锁                               │</span><br><span class="line">       │                                       │</span><br><span class="line">       ▼                                       │</span><br><span class="line">  3. 写入 ring buffer                         │</span><br><span class="line">     [UUID 4B | len 2B | payload nB]          │</span><br><span class="line">       │                                       │</span><br><span class="line">       ▼                                       │</span><br><span class="line">  4. mutex 解锁                               │</span><br><span class="line">       │                                       │</span><br><span class="line">       ▼                                       │</span><br><span class="line">  5. sem_give() ──────────────────────────►  6. sem_take() 被唤醒</span><br><span class="line">                                               │</span><br><span class="line">                                               ▼</span><br><span class="line">                                           7. mutex 加锁</span><br><span class="line">                                           8. 从 ring buffer 读出消息</span><br><span class="line">                                           9. mutex 解锁（先解锁再调 handler）</span><br><span class="line">                                               │</span><br><span class="line">                                               ▼</span><br><span class="line">                                          10. 二分查找 handler 表</span><br><span class="line">                                          11. on_temp(data, size, ctx)</span><br></pre></td></tr></table></figure><p>两个值得注意的细节：</p><p><strong><code>post()</code> 非阻塞</strong>：消息写进 ring buffer 就立刻返回，不等 handler 执行完，handler 在消费者线程里异步执行。</p><p><strong>先解锁再调 handler</strong>：消费者线程读完消息后立刻释放 mutex，拿数据副本去调 handler。生产者不需要等 handler 执行完才能继续 post，吞吐量更高。</p><h4 id="3-2-ring-buffer：消息的存储格式"><a href="#3-2-ring-buffer：消息的存储格式" class="headerlink" title="3.2 ring buffer：消息的存储格式"></a>3.2 ring buffer：消息的存储格式</h4><p>每条消息在 buffer 里连续存三段：</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></pre></td><td class="code"><pre><span class="line">┌──────────────┬──────────────┬──────────────────────┐</span><br><span class="line">│  UUID  (4 B) │  长度  (2 B) │  payload（最多1KB）  │</span><br><span class="line">└──────────────┴──────────────┴──────────────────────┘</span><br></pre></td></tr></table></figure><p>6 字节固定 header，overhead 极低。buffer 用两个指针管理：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">head —— 读指针，只有消费者线程移动</span><br><span class="line">tail —— 写指针，只有生产者线程移动（受 mutex 保护）</span><br></pre></td></tr></table></figure><p>下面是 buffer 在不同状态下的样子（以 16 字节 buffer 为例，每条消息占 7 字节）：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line">初始（空）：head == tail</span><br><span class="line">[  ][  ][  ][  ][  ][  ][  ][  ][  ][  ][  ][  ][  ][  ][  ][  ]</span><br><span class="line"> ↑</span><br><span class="line">head/tail</span><br><span class="line"></span><br><span class="line">写入两条消息后：</span><br><span class="line">[M1][M1][M1][M1][M1][M1][M1][M2][M2][M2][M2][M2][M2][M2][  ][  ]</span><br><span class="line"> ↑                                                          ↑</span><br><span class="line">head                                                       tail</span><br><span class="line"></span><br><span class="line">消费者读取 M1 后：</span><br><span class="line">[  ][  ][  ][  ][  ][  ][  ][M2][M2][M2][M2][M2][M2][M2][  ][  ]</span><br><span class="line">                              ↑                              ↑</span><br><span class="line">                             head                          tail</span><br><span class="line"></span><br><span class="line">tail 逼近末尾，新消息放不下了（仅剩 2 格，消息需要 7 格）：</span><br><span class="line">[  ][  ][  ][  ][  ][  ][  ][M2][M2][M2][M2][M2][M2][M2][  ][  ]</span><br><span class="line">                              ↑                              ↑</span><br><span class="line">                             head                          tail</span><br><span class="line"></span><br><span class="line">分两次 memcpy 绕回写入 M3（5 字节写到末尾，2 字节绕回开头）：</span><br><span class="line">[M3][M3][  ][  ][  ][  ][  ][M2][M2][M2][M2][M2][M2][M2][M3][M3]</span><br><span class="line">      ↑                       ↑</span><br><span class="line">     tail                    head</span><br></pre></td></tr></table></figure><p>绕回对调用方完全透明，代码里用最多两次 <code>memcpy</code> 处理：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> <span class="type">void</span> <span class="title function_">ring_write_bytes</span><span class="params">(<span class="type">embedmq_t</span> *q, <span class="type">const</span> <span class="type">void</span> *src, <span class="type">size_t</span> n)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">size_t</span> end = q-&gt;buf_size - q-&gt;tail;  <span class="comment">// 尾部剩余空间</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (end &gt;= n) &#123;</span><br><span class="line">        <span class="built_in">memcpy</span>(q-&gt;buf + q-&gt;tail, src, n);        <span class="comment">// 一次搞定</span></span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="built_in">memcpy</span>(q-&gt;buf + q-&gt;tail, src, end);       <span class="comment">// 写到末尾</span></span><br><span class="line">        <span class="built_in">memcpy</span>(q-&gt;buf,           src + end, n - end); <span class="comment">// 剩余从头写</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="3-3-handler-表：有序插入-二分查找"><a href="#3-3-handler-表：有序插入-二分查找" class="headerlink" title="3.3 handler 表：有序插入 + 二分查找"></a>3.3 handler 表：有序插入 + 二分查找</h4><p><code>register()</code> 把 handler 存进一个<strong>按 UUID 排好序</strong>的数组。插入时找到正确位置，用 <code>memmove</code> 右移腾出空间：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">注册前：</span><br><span class="line">index  [0]               [1]               [2]</span><br><span class="line">uuid   0x1B00:on_button  0x5F00:on_wifi    0xF300:on_log</span><br><span class="line"></span><br><span class="line">注册 &quot;sensor.temp&quot;（UUID=0x8A00），应插在 [1] 和 [2] 之间：</span><br><span class="line">  → [2] 右移一格腾出位置</span><br><span class="line"></span><br><span class="line">注册后：</span><br><span class="line">index  [0]               [1]              [2]              [3]</span><br><span class="line">uuid   0x1B00:on_button  0x5F00:on_wifi   0x8A00:on_temp   0xF300:on_log</span><br></pre></td></tr></table></figure><p>注册只在启动时做一次，<code>memmove</code> 的开销不重要。换来的是 <code>post()</code> 时可以用<strong>二分查找</strong>，64 个 handler 最多比较 6 次（log₂64），比逐个遍历快得多。</p><h4 id="3-4-FNV-1a-hash：名字变数字"><a href="#3-4-FNV-1a-hash：名字变数字" class="headerlink" title="3.4 FNV-1a hash：名字变数字"></a>3.4 FNV-1a hash：名字变数字</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></pre></td><td class="code"><pre><span class="line"><span class="type">uint32_t</span> <span class="title function_">embedmq_uuid</span><span class="params">(<span class="type">const</span> <span class="type">char</span> *name)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">uint32_t</span> hash = <span class="number">0x811C9DC5</span>;          <span class="comment">// FNV offset basis</span></span><br><span class="line">    <span class="keyword">for</span> (<span class="type">const</span> <span class="type">unsigned</span> <span class="type">char</span> *p = name; *p; ++p) &#123;</span><br><span class="line">        hash ^= (<span class="type">uint32_t</span>)*p;</span><br><span class="line">        hash *= <span class="number">0x01000193</span>;              <span class="comment">// FNV prime</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> hash ? hash : <span class="number">1</span>;             <span class="comment">// 保证非零（0 用作无效哨兵）</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>单次遍历，平台无关，结果确定。注册时算一次，之后 <code>post()</code> 只比较整数，热路径上没有字符串操作。</p><p>如果连 <code>post()</code> 里的 hash 都想省掉，提前缓存 UUID：</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></pre></td><td class="code"><pre><span class="line"><span class="type">uint32_t</span> uuid = embedmq_uuid(<span class="string">&quot;sensor.temp&quot;</span>);  <span class="comment">// 启动时算一次</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">while</span> (<span class="number">1</span>) &#123;</span><br><span class="line">    <span class="type">sensor_t</span> d = read_sensor();</span><br><span class="line">    embedmq_post_id(q, uuid, &amp;d, <span class="keyword">sizeof</span>(d));   <span class="comment">// 直接用整数，跳过 hash</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="3-5-PAL：三个平台，一套接口"><a href="#3-5-PAL：三个平台，一套接口" class="headerlink" title="3.5 PAL：三个平台，一套接口"></a>3.5 PAL：三个平台，一套接口</h4><p><code>src/embedmq.c</code> 里没有一行平台相关代码，它只调用 <code>pal/embedmq_pal.h</code> 定义的 10 个函数：</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">embedmq_pal_sem_create / destroy / give / take      <span class="comment">// 信号量</span></span><br><span class="line">embedmq_pal_mutex_create / destroy / lock / unlock  <span class="comment">// 互斥锁</span></span><br><span class="line">embedmq_pal_thread_create / join                    <span class="comment">// 线程</span></span><br></pre></td></tr></table></figure><p>三个平台各自实现这 10 个函数，编译时选一个：</p><table><thead><tr><th>平台</th><th>信号量</th><th>互斥锁</th><th>线程</th></tr></thead><tbody><tr><td>Linux</td><td><code>sem_t</code> (POSIX)</td><td><code>pthread_mutex_t</code></td><td><code>pthread_t</code></td></tr><tr><td>FreeRTOS</td><td><code>SemaphoreHandle_t</code></td><td><code>SemaphoreHandle_t</code></td><td><code>TaskHandle_t</code> + done 信号量</td></tr><tr><td>裸机</td><td><code>atomic_int</code>（忙等）</td><td><code>atomic_flag</code>（自旋锁）</td><td>无（手动调 <code>poll()</code>）</td></tr></tbody></table><p>切换平台只需一行 CMake 参数，核心代码一字不改：</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">cmake -B build -DEMBEDMQ_PAL=freertos</span><br><span class="line">cmake -B build -DEMBEDMQ_PAL=none</span><br></pre></td></tr></table></figure><h4 id="3-6-静态模式的内存布局"><a href="#3-6-静态模式的内存布局" class="headerlink" title="3.6 静态模式的内存布局"></a>3.6 静态模式的内存布局</h4><p><code>embedmq_create_static()</code> 最有意思的地方是它只做一件事：<strong>把一整块内存切成四段，各自指向不同的数据结构</strong>。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">传入的 buf（一块连续内存）：</span><br><span class="line"></span><br><span class="line">┌──────────────────┬─────────────────────┬─────────────────┬──────────────────┐</span><br><span class="line">│  struct embedmq_s│  handler 表         │  ring buffer    │  dispatch 缓冲区 │</span><br><span class="line">│  （控制信息）    │  max_handlers × 12B │  queue_size 字节│  max_msg_size 字节│</span><br><span class="line">└──────────────────┴─────────────────────┴─────────────────┴──────────────────┘</span><br><span class="line">  ↑                  ↑                     ↑                  ↑</span><br><span class="line">  q                  q-&gt;handlers           q-&gt;buf             q-&gt;dispatch_buf</span><br></pre></td></tr></table></figure><p><code>embedmq_mem_size()</code> 就是把这四段的大小加起来，告诉你 buf 需要多大：</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></pre></td><td class="code"><pre><span class="line"><span class="type">size_t</span> <span class="title function_">embedmq_mem_size</span><span class="params">(<span class="type">const</span> <span class="type">embedmq_config_t</span> *cfg)</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">sizeof</span>(<span class="keyword">struct</span> embedmq_s)</span><br><span class="line">         + cfg-&gt;max_handlers * <span class="keyword">sizeof</span>(<span class="type">embedmq_handler_entry_t</span>)</span><br><span class="line">         + cfg-&gt;queue_size</span><br><span class="line">         + cfg-&gt;max_msg_size;   <span class="comment">// dispatch 缓冲区</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个设计的好处是：<strong>一次 <code>malloc</code>（或一块静态数组）拿到所有内存，生命周期统一管理，不产生碎片</strong>。对于禁止动态内存分配的 MCU 项目，只需要在 BSS 段声明一个数组：</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"><span class="type">static</span> <span class="type">embedmq_config_t</span> cfg = &#123; .queue_size = <span class="number">2048</span>, .max_handlers = <span class="number">8</span> &#125;;</span><br><span class="line"><span class="type">static</span> <span class="type">uint8_t</span> mq_buf[<span class="number">2048</span> + <span class="number">8</span>*<span class="number">12</span> + <span class="number">1024</span> + <span class="number">64</span>];  <span class="comment">// 或用 embedmq_mem_size 算</span></span><br></pre></td></tr></table></figure><p>之后整个库的运行不会再碰堆。</p><hr><h3 id="四、FreeRTOS-移植：两个坑的实战记录"><a href="#四、FreeRTOS-移植：两个坑的实战记录" class="headerlink" title="四、FreeRTOS 移植：两个坑的实战记录"></a>四、FreeRTOS 移植：两个坑的实战记录</h3><p>Linux 和裸机的后端很顺利，FreeRTOS 是真正把 PAL 抽象逼到极限的地方。</p><h4 id="坑一：FreeRTOS-task-不能-return"><a href="#坑一：FreeRTOS-task-不能-return" class="headerlink" title="坑一：FreeRTOS task 不能 return"></a>坑一：FreeRTOS task 不能 return</h4><p>embedmq 的消费者循环在收到退出信号后会 <code>break</code> 然后 <code>return</code>，在 pthreads 里完全正常。但 <strong>FreeRTOS 的 task 函数绝对不能 return</strong>——一旦 return 就是未定义行为，通常直接崩溃。task 必须自己调 <code>vTaskDelete(NULL)</code> 结束。</p><p>解决方案是在 FreeRTOS PAL 里加一层 trampoline：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> <span class="type">void</span> <span class="title function_">task_trampoline</span><span class="params">(<span class="type">void</span> *param)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">embedmq_pal_thread_t</span> *t = param;</span><br><span class="line">    t-&gt;fn(t-&gt;arg);             <span class="comment">// 运行消费者循环……</span></span><br><span class="line">    xSemaphoreGive(t-&gt;done);   <span class="comment">// ……通知外部&quot;我跑完了&quot;……</span></span><br><span class="line">    vTaskDelete(<span class="literal">NULL</span>);         <span class="comment">// ……然后正确地删除自己，永不 return</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="坑二：FreeRTOS-没有-pthread-join"><a href="#坑二：FreeRTOS-没有-pthread-join" class="headerlink" title="坑二：FreeRTOS 没有 pthread_join"></a>坑二：FreeRTOS 没有 pthread_join</h4><p><code>embedmq_destroy()</code> 必须等消费者线程真正退出后才能释放内存，否则线程还在跑，内存已经被 free，必然崩溃。pthreads 有 <code>pthread_join</code> 可以等。FreeRTOS 没有等价物。</p><p>解决方案是在线程句柄里放一个”done 信号量”：</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> &#123;</span></span><br><span class="line">    TaskHandle_t      handle;</span><br><span class="line">    SemaphoreHandle_t done;   <span class="comment">// task 退出前 give 这个信号量</span></span><br><span class="line">    <span class="type">void</span>            (*fn)(<span class="type">void</span> *);</span><br><span class="line">    <span class="type">void</span>             *arg;</span><br><span class="line">&#125; <span class="type">embedmq_pal_thread_t</span>;</span><br></pre></td></tr></table></figure><p><code>destroy()</code> 发退出信号后，等待 <code>done</code> 信号量：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">void</span> <span class="title function_">embedmq_pal_thread_join</span><span class="params">(<span class="type">embedmq_pal_thread_t</span> *t)</span></span><br><span class="line">&#123;</span><br><span class="line">    xSemaphoreTake(t-&gt;done, portMAX_DELAY);  <span class="comment">// 等 task 给信号</span></span><br><span class="line">    vSemaphoreDelete(t-&gt;done);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>task 在 <code>vTaskDelete</code> 之前 give <code>done</code>，join 随即返回。没有 <code>pthread_join</code>，用一个计数信号量模拟出了同样的语义。</p><h4 id="模拟器上的栈大小陷阱"><a href="#模拟器上的栈大小陷阱" class="headerlink" title="模拟器上的栈大小陷阱"></a>模拟器上的栈大小陷阱</h4><p>在 FreeRTOS POSIX 模拟器（GCC_POSIX 移植）上验证时遇到一个诡异的问题：程序启动后完全没有输出，测试 task 从未运行。</p><p>原因是 POSIX 移植里，FreeRTOS 的栈深度（<code>depth</code>）字段会被当作 <strong>pthread 栈大小的字节数</strong>。我把 <code>configMINIMAL_STACK_SIZE</code> 设成了 <code>PTHREAD_STACK_MIN</code>（现代 glibc 上约 16 KB），然后又乘了倍数，导致单个 task 请求超过 500 KB 的 pthread 栈。FreeRTOS heap 直接被撑爆，<code>xTaskCreate</code> 静默返回失败，调度器只剩 idle task 在空转。</p><p>修复方法是把深度和 pthread 栈字节数解耦：</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="comment">// 用字节数除以 StackType_t 大小，得到正确的 depth</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> configMINIMAL_STACK_SIZE \</span></span><br><span class="line"><span class="meta">    ((unsigned short)(PTHREAD_STACK_MIN / sizeof(unsigned long)))</span></span><br></pre></td></tr></table></figure><p><strong>教训</strong>：FreeRTOS 里”栈大小”这个数字，在 Cortex-M 上是实际栈的字（word）数，在 POSIX 移植里却同时影响 heap 分配和 pthread 栈字节数。同一个字段，完全不同的含义。</p><hr><h3 id="五、C-封装：RAII-Lambda"><a href="#五、C-封装：RAII-Lambda" class="headerlink" title="五、C++ 封装：RAII + Lambda"></a>五、C++ 封装：RAII + Lambda</h3><p>C API 足够用，但 C++ 封装让代码更简洁。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;embedmq.hpp&quot;</span></span></span><br><span class="line"></span><br><span class="line">embedmq::MQ q;  <span class="comment">// 构造时 create，析构时自动 destroy</span></span><br><span class="line"></span><br><span class="line">q.<span class="built_in">subscribe</span>(<span class="string">&quot;sensor.temp&quot;</span>, [&amp;](<span class="type">const</span> <span class="type">void</span> *data, <span class="type">size_t</span> size) &#123;</span><br><span class="line">    <span class="type">const</span> <span class="type">sensor_t</span> *s = <span class="built_in">static_cast</span>&lt;<span class="type">const</span> <span class="type">sensor_t</span> *&gt;(data);</span><br><span class="line">    display.<span class="built_in">update</span>(s-&gt;value);</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line">q.<span class="built_in">publish</span>(<span class="string">&quot;sensor.temp&quot;</span>, &amp;data, <span class="built_in">sizeof</span>(data));</span><br></pre></td></tr></table></figure><p><strong>lambda 是怎么桥接到 C 接口的？</strong></p><p>C 的 <code>embedmq_register()</code> 只接受普通函数指针，lambda 不是函数指针。C++ 封装用一个静态 trampoline 函数做桥：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 1. lambda 存在 entries_ 里，和 MQ 对象同生共死</span></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">HandlerEntry</span> &#123; <span class="type">uint32_t</span> uuid; Handler fn; &#125;;</span><br><span class="line">entries_.<span class="built_in">push_back</span>(&#123; uuid, std::<span class="built_in">move</span>(fn) &#125;);</span><br><span class="line"><span class="keyword">auto</span> *entry = &amp;entries_.<span class="built_in">back</span>();</span><br><span class="line"></span><br><span class="line"><span class="comment">// 2. 向 C 库注册一个普通函数，把 entry 指针作为 ctx 传进去</span></span><br><span class="line"><span class="built_in">embedmq_register</span>(q_, name.<span class="built_in">c_str</span>(), detail::trampoline, entry);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 3. trampoline：C 库调这个，它再转调 lambda</span></span><br><span class="line"><span class="function"><span class="type">static</span> <span class="type">void</span> <span class="title">trampoline</span><span class="params">(<span class="type">const</span> <span class="type">void</span> *data, <span class="type">size_t</span> size, <span class="type">void</span> *ctx)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">auto</span> *entry = <span class="built_in">static_cast</span>&lt;HandlerEntry *&gt;(ctx);</span><br><span class="line">    entry-&gt;<span class="built_in">fn</span>(data, size);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>调用链：消费者线程 → <code>trampoline()</code> → lambda。<code>entries_</code> 负责让 lambda 的内存一直活着，不会变成悬空指针。</p><hr><h3 id="六、快速上手"><a href="#六、快速上手" class="headerlink" title="六、快速上手"></a>六、快速上手</h3><h4 id="克隆和构建"><a href="#克隆和构建" class="headerlink" title="克隆和构建"></a>克隆和构建</h4><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><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">git <span class="built_in">clone</span> https://github.com/w4ysonch/embedmq.git</span><br><span class="line"><span class="built_in">cd</span> embedmq</span><br><span class="line"></span><br><span class="line"><span class="comment"># 构建（默认 Linux PAL，包含示例和测试）</span></span><br><span class="line">cmake -B build &amp;&amp; cmake --build build</span><br><span class="line"></span><br><span class="line"><span class="comment"># 运行示例</span></span><br><span class="line">./build/example_basic        <span class="comment"># C 示例</span></span><br><span class="line">./build/example_basic_cpp    <span class="comment"># C++ 示例</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 运行测试</span></span><br><span class="line">./build/test_embedmq</span><br><span class="line">./build/test_embedmq_cpp</span><br></pre></td></tr></table></figure><h4 id="静态分配模式（MCU-无堆环境）"><a href="#静态分配模式（MCU-无堆环境）" class="headerlink" title="静态分配模式（MCU &#x2F; 无堆环境）"></a>静态分配模式（MCU &#x2F; 无堆环境）</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></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> <span class="type">embedmq_config_t</span> cfg = &#123;</span><br><span class="line">    .queue_size   = <span class="number">2048</span>,</span><br><span class="line">    .max_msg_size = <span class="number">64</span>,</span><br><span class="line">    .max_handlers = <span class="number">8</span>,</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> <span class="type">uint8_t</span> mq_buf[<span class="number">4096</span>];  <span class="comment">// embedmq_mem_size(&amp;cfg) 算出确切大小</span></span><br><span class="line"><span class="type">static</span> <span class="type">embedmq_t</span> *q;</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">app_init</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">    q = embedmq_create_static(mq_buf, <span class="keyword">sizeof</span>(mq_buf), &amp;cfg);</span><br><span class="line">    embedmq_register(q, <span class="string">&quot;sensor.update&quot;</span>, on_sensor, <span class="literal">NULL</span>);</span><br><span class="line">    <span class="comment">// 之后不再碰堆</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="裸机-superloop-模式"><a href="#裸机-superloop-模式" class="headerlink" title="裸机 superloop 模式"></a>裸机 superloop 模式</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 编译：cmake -B build -DEMBEDMQ_PAL=none</span></span><br><span class="line"><span class="type">embedmq_t</span> *q = embedmq_create(&amp;cfg);</span><br><span class="line">embedmq_register(q, <span class="string">&quot;tick.10ms&quot;</span>, on_tick, <span class="literal">NULL</span>);</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">TIM_IRQHandler</span><span class="params">(<span class="type">void</span>)</span> &#123;</span><br><span class="line">    embedmq_post(q, <span class="string">&quot;tick.10ms&quot;</span>, <span class="literal">NULL</span>, <span class="number">0</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_">main</span><span class="params">(<span class="type">void</span>)</span> &#123;</span><br><span class="line">    <span class="keyword">while</span> (<span class="number">1</span>) &#123;</span><br><span class="line">        embedmq_poll(q);  <span class="comment">// 手动触发派发</span></span><br><span class="line">        __WFI();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><hr><h3 id="七、性能数据"><a href="#七、性能数据" class="headerlink" title="七、性能数据"></a>七、性能数据</h3><p>x86-64 Linux，Release 构建（<code>-O2</code>），单生产者 + 单消费者线程：</p><table><thead><tr><th>测试项</th><th>结果</th></tr></thead><tbody><tr><td><code>embedmq_post()</code> 吞吐量</td><td><strong>~2,966,716 条&#x2F;秒</strong></td></tr><tr><td><code>embedmq_post_id()</code> 吞吐量（UUID 预缓存）</td><td><strong>~3,377,094 条&#x2F;秒</strong></td></tr><tr><td>端到端延迟（post → handler），平均</td><td><strong>~22 µs</strong></td></tr><tr><td>端到端延迟，最短</td><td><strong>~2.6 µs</strong></td></tr><tr><td><code>embedmq_uuid()</code> hash 速度</td><td><strong>~131M 次&#x2F;秒</strong>（约 7.6 ns&#x2F;次）</td></tr></tbody></table><p>热路径上没有字符串操作、没有内存分配，只有一次整数比较（二分查找）、一次 <code>memcpy</code>（ring buffer 写入）、一次信号量操作。</p><p><img src="/images/embedmq/benchmark.png" alt="benchmark"></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></pre></td><td class="code"><pre><span class="line">cmake -B build -DCMAKE_BUILD_TYPE=Release &amp;&amp; cmake --build build --target benchmark</span><br><span class="line">./build/benchmark</span><br></pre></td></tr></table></figure><hr><h3 id="八、适用场景与局限性"><a href="#八、适用场景与局限性" class="headerlink" title="八、适用场景与局限性"></a>八、适用场景与局限性</h3><p><strong>适合用的场景：</strong></p><ul><li>嵌入式 Linux 多线程程序，模块间需要解耦通信</li><li>FreeRTOS 多任务，不想手写队列和任务间同步</li><li>裸机 superloop，想统一管理事件分发</li></ul><p><strong>不适合的场景：</strong></p><ul><li>跨进程通信（用 Unix socket &#x2F; mqueue）</li><li>跨网络通信（用 MQTT &#x2F; ZeroMQ）</li><li>需要一对多广播（embedmq 每个事件名只支持一个 handler）</li><li>handler 里有阻塞操作（会堵住消费者线程，后续消息排队等待）</li></ul><p><strong>几个容易踩的坑：</strong></p><ol><li><code>register()</code> 必须在所有 <code>post()</code> 之前完成，且不是线程安全的</li><li>handler 运行在消费者线程，<code>data</code> 指针只在调用期间有效，需要保留数据要自己拷贝</li><li><code>destroy()</code> 会阻塞等消费者线程退出，调用前要确保没有线程还在 <code>post()</code></li><li>裸机 PAL 的信号量是忙等，不适合对功耗敏感的场景</li></ol><hr><h3 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h3><p>embedmq 的核心取舍是：<strong>用注册时的一次性开销（hash + 有序插入）换取派发时的极低延迟（整数比较 + 二分查找）</strong>，以及<strong>一块连续内存管理所有内部状态</strong>带来的零碎片确定性。</p><p>适合事件种类固定、启动后不再变化的嵌入式场景。如果你的项目需要动态增删事件类型，这不是合适的工具。</p><p>项目采用 MIT 协议开源，欢迎试用和反馈。</p><p><strong>GitHub</strong>：<a href="https://github.com/w4ysonch/embedmq">https://github.com/w4ysonch/embedmq</a></p>]]></content>
    
    
      
      
    <summary type="html">&lt;h3 id=&quot;引言&quot;&gt;&lt;a href=&quot;#引言&quot; class=&quot;headerlink&quot; title=&quot;引言&quot;&gt;&lt;/a&gt;引言&lt;/h3&gt;&lt;p&gt;嵌入式项目里，线程间通信是绕不开的问题。传感器线程读到数据，UI 线程要更新显示，网络线程要上报——它们之间怎么传消息？&lt;/p&gt;
&lt;p&gt;最直</summary>
      
    
    
    
    <category term="嵌入式" scheme="https://w4ysonch.github.io/categories/%E5%B5%8C%E5%85%A5%E5%BC%8F/"/>
    
    
    <category term="C语言" scheme="https://w4ysonch.github.io/tags/C%E8%AF%AD%E8%A8%80/"/>
    
    <category term="架构设计" scheme="https://w4ysonch.github.io/tags/%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1/"/>
    
    <category term="开源" scheme="https://w4ysonch.github.io/tags/%E5%BC%80%E6%BA%90/"/>
    
    <category term="Linux" scheme="https://w4ysonch.github.io/tags/Linux/"/>
    
  </entry>
  
  <entry>
    <title>嵌入式开发者视角的 Google C++ Style Guide 实战解读</title>
    <link href="https://w4ysonch.github.io/2025/12/11/google-cpp-style-guide-embedded/"/>
    <id>https://w4ysonch.github.io/2025/12/11/google-cpp-style-guide-embedded/</id>
    <published>2025-12-11T04:00:00.000Z</published>
    <updated>2026-06-29T07:50:23.717Z</updated>
    
    <content type="html"><![CDATA[<h2 id="一、这东西到底有什么用？"><a href="#一、这东西到底有什么用？" class="headerlink" title="一、这东西到底有什么用？"></a>一、这东西到底有什么用？</h2><p>2013 年 Google 把内部 C++ 规范扔上了 GitHub。现在 38000+ Star，Chromium 在用，LLVM 在参考，国内大厂的规范里也多多少少能看到它的影子。</p><p>但它从一开始就没打算当”温和的建议”。它<strong>禁异常、禁 RTTI、禁 C 风格转型、禁全局变量、禁静态存储期对象</strong>。每一条单独拎出来都能在技术群里吵一个下午。</p><p>这些规则背后有一个简单的事实：这份规范是为 100M+ 行代码、上万工程师、维护几十年的代码库写的。这个场景跟嵌入式出奇地像——二进制要小、控制流要稳、出问题不能靠抛异常甩锅。</p><p>我不是来翻译官方文档的。下面从写了几十万行嵌入式 C&#x2F;C++ 的经验出发，拆哪些能直接用、哪些得改改、哪些 Google 自己也没那么认真。</p><p><img src="/images/google-cpp-style-guide-embedded/google-style-cover.png" alt="Google C++ Style Guide"></p><hr><h2 id="二、核心哲学：为什么偏要优化给”读者”看？"><a href="#二、核心哲学：为什么偏要优化给”读者”看？" class="headerlink" title="二、核心哲学：为什么偏要优化给”读者”看？"></a>二、核心哲学：为什么偏要优化给”读者”看？</h2><p>Google Style 的第一句话就能劝退不少人：</p><blockquote><p><strong>Optimize for the reader, not the writer.</strong></p></blockquote><p>说白了：写的时候多花 5 秒，让别人（以及三个月后的你自己）读的时候省 5 分钟。</p><h3 id="这在嵌入式项目里意味着什么？"><a href="#这在嵌入式项目里意味着什么？" class="headerlink" title="这在嵌入式项目里意味着什么？"></a>这在嵌入式项目里意味着什么？</h3><p>拿一段典型的裸机按键扫描代码对比：</p><p><strong>Before</strong></p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">uint8_t</span> k;</span><br><span class="line"><span class="type">uint8_t</span> p;</span><br><span class="line"><span class="type">uint8_t</span> s;</span><br><span class="line"><span class="type">int</span> c;</span><br><span class="line"><span class="type">void</span> <span class="title function_">scan</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="keyword">for</span> (k = <span class="number">0</span>; k &lt; <span class="number">8</span>; k++) &#123;</span><br><span class="line">        p = read(<span class="number">0x50</span> + k);</span><br><span class="line">        s = p ^ <span class="number">0xFF</span>;</span><br><span class="line">        <span class="keyword">if</span> (s != <span class="number">0</span> &amp;&amp; s != last[k]) &#123;</span><br><span class="line">            c = __builtin_ctz(s);</span><br><span class="line">            <span class="keyword">if</span> (s &gt; last[k])</span><br><span class="line">                cb_press(k, c);</span><br><span class="line">            <span class="keyword">else</span></span><br><span class="line">                cb_release(k, c);</span><br><span class="line">            last[k] = s;</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>这段代码是编译器能跑的，但三个月后的维护者在凌晨三点看到 <code>p = read(0x50 + k)</code> 的时候，心态是崩溃的：<code>0x50</code> 是什么寄存器？<code>s</code> 是什么？<code>cb_press</code> 的参数类型是什么？</p><p><strong>After — Google Style + 嵌入式惯例</strong></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></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">enum</span> &#123;</span></span><br><span class="line">    KEY_EVENT_PRESS = <span class="number">0</span>,</span><br><span class="line">    KEY_EVENT_RELEASE = <span class="number">1</span>,</span><br><span class="line">&#125; KeyEvent;</span><br><span class="line"></span><br><span class="line"><span class="keyword">typedef</span> <span class="title function_">void</span> <span class="params">(*KeyCallback)</span><span class="params">(<span class="type">uint8_t</span> row, <span class="type">uint8_t</span> col, KeyEvent event)</span>;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> <span class="type">constexpr</span> <span class="type">uint8_t</span> kKeyMatrixBaseAddr = <span class="number">0x50</span>;</span><br><span class="line"><span class="type">static</span> <span class="type">constexpr</span> <span class="type">uint8_t</span> kKeyMatrixRows = <span class="number">8</span>;</span><br><span class="line"><span class="type">static</span> <span class="type">constexpr</span> <span class="type">uint8_t</span> kKeyMatrixMask = <span class="number">0xFF</span>;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> <span class="type">uint8_t</span> g_key_last_state[kKeyMatrixRows];</span><br><span class="line"><span class="type">static</span> KeyCallback g_key_callbacks[kKeyMatrixRows][<span class="number">8</span>];</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">KeyMatrix_Scan</span><span class="params">(<span class="type">void</span>)</span> &#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">uint8_t</span> row = <span class="number">0</span>; row &lt; kKeyMatrixRows; row++) &#123;</span><br><span class="line">        <span class="type">const</span> <span class="type">uint8_t</span> raw_state = GPIO_ReadPort(kKeyMatrixBaseAddr + row);</span><br><span class="line">        <span class="type">const</span> <span class="type">uint8_t</span> inverted_state = raw_state ^ kKeyMatrixMask;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (inverted_state == g_key_last_state[row])</span><br><span class="line">            <span class="keyword">continue</span>;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">for</span> (<span class="type">uint8_t</span> col = <span class="number">0</span>; col &lt; <span class="number">8</span>; col++) &#123;</span><br><span class="line">            <span class="type">const</span> <span class="type">uint8_t</span> bit_mask = <span class="number">1U</span> &lt;&lt; col;</span><br><span class="line">            <span class="type">const</span> <span class="type">bool</span> was_pressed = g_key_last_state[row] &amp; bit_mask;</span><br><span class="line">            <span class="type">const</span> <span class="type">bool</span> is_pressed = inverted_state &amp; bit_mask;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">if</span> (is_pressed == was_pressed)</span><br><span class="line">                <span class="keyword">continue</span>;</span><br><span class="line"></span><br><span class="line">            <span class="type">const</span> KeyEvent event = is_pressed ? KEY_EVENT_PRESS : KEY_EVENT_RELEASE;</span><br><span class="line">            <span class="keyword">if</span> (g_key_callbacks[row][col] != <span class="literal">NULL</span>)</span><br><span class="line">                g_key_callbacks[row][col](row, col, event);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        g_key_last_state[row] = inverted_state;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>同样的功能，代码量多了，但任何一个 C 程序员打开都能在 10 秒内理解逻辑——这就是”为读者优化”的实战价值。</p><h3 id="三条黄金法则"><a href="#三条黄金法则" class="headerlink" title="三条黄金法则"></a>三条黄金法则</h3><table><thead><tr><th>原则</th><th>含义</th><th>嵌入式举例</th></tr></thead><tbody><tr><td>一致性压倒个人偏好</td><td>团队用一种风格，哪怕你不喜欢</td><td>别争论缩进用 2 格还是 4 格，定下来就别改</td></tr><tr><td>尽量避免”聪明”特性</td><td>禁止异常、RTTI、全局对象构造函数</td><td>STM32 启动阶段 CRTP 全局对象初始化顺序是 UB</td></tr><tr><td>自动化优先</td><td><code>clang-format</code> + <code>clang-tidy</code>，别手动查风格</td><td>CI 上挂一个 lint 检查，不通过不能合</td></tr></tbody></table><hr><h2 id="三、命名规范：代码即文档的第一公里"><a href="#三、命名规范：代码即文档的第一公里" class="headerlink" title="三、命名规范：代码即文档的第一公里"></a>三、命名规范：代码即文档的第一公里</h2><p>Google Style 的命名体系用视觉信号区分变量类型——扫一眼就知道是局部变量还是类成员、是函数还是常量。对于嵌入式 C 项目来说，这套规则能直接消灭最常见的命名问题。</p><h3 id="完整命名速查表"><a href="#完整命名速查表" class="headerlink" title="完整命名速查表"></a>完整命名速查表</h3><table><thead><tr><th>实体</th><th>风格</th><th>示例</th></tr></thead><tbody><tr><td>类 &#x2F; 结构体名</td><td>大驼峰</td><td><code>class AdcDriver;</code></td></tr><tr><td>枚举类型名</td><td>大驼峰</td><td><code>enum class SensorState;</code></td></tr><tr><td>函数 &#x2F; 方法名</td><td>大驼峰</td><td><code>void ReadSensorData();</code></td></tr><tr><td>普通变量（局部&#x2F;参数）</td><td>全小写下划线</td><td><code>int adc_value;</code></td></tr><tr><td>类成员变量</td><td>全小写下划线 + 尾部下划线</td><td><code>int buffer_size_;</code></td></tr><tr><td>结构体成员变量</td><td>全小写下划线，无后缀</td><td><code>std::string name;</code></td></tr><tr><td>常量（<code>constexpr</code> &#x2F; <code>const</code>）</td><td><code>k</code> + 大驼峰</td><td><code>const int kMaxBufferSize = 256;</code></td></tr><tr><td>枚举值</td><td><code>k</code> + 大驼峰</td><td><code>kErrorTimeout, kOk</code></td></tr><tr><td>宏</td><td>全大写 + 下划线</td><td><code>#define MYPROJECT_ROUND(x)</code></td></tr><tr><td>命名空间</td><td>全小写下划线</td><td><code>namespace sensor_driver {}</code></td></tr><tr><td>文件名</td><td>全小写下划线</td><td><code>adc_driver.h</code>, <code>adc_driver.cc</code></td></tr></tbody></table><h3 id="嵌入式实战中的几个关键点"><a href="#嵌入式实战中的几个关键点" class="headerlink" title="嵌入式实战中的几个关键点"></a>嵌入式实战中的几个关键点</h3><p><strong>1. 类成员变量 vs 结构体成员变量</strong></p><p>这个区别非常重要：类有不变式（invariant），数据成员必须私有，因此尾部加 <code>_</code> 提醒”这是类内部状态，外面别碰”；结构体只是数据容器，成员不加后缀。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">AdcDriver</span> &#123;</span><br><span class="line"> <span class="keyword">private</span>:</span><br><span class="line">    <span class="type">uint8_t</span> channel_;        <span class="comment">// 私有 — 尾部有 _</span></span><br><span class="line">    <span class="type">uint32_t</span> sample_rate_;   <span class="comment">// 私有 — 尾部有 _</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span>:</span><br><span class="line">    <span class="function"><span class="type">void</span> <span class="title">StartConversion</span><span class="params">()</span></span>;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">AdcConfig</span> &#123;</span><br><span class="line">    <span class="type">uint8_t</span> channel;         <span class="comment">// 公开 — 无后缀</span></span><br><span class="line">    <span class="type">uint32_t</span> sample_rate;    <span class="comment">// 公开 — 无后缀</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p><strong>2. 常量的 <code>k</code> 前缀</strong></p><p>很多嵌入式项目用 <code>#define</code> 或全大写常量来区分可变与不可变。<code>k</code> 前缀是一种更轻量的视觉提示：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 一眼区分：可变 vs 不可变</span></span><br><span class="line"><span class="type">int</span> retry_count = <span class="number">0</span>;                <span class="comment">// 普通变量</span></span><br><span class="line"><span class="keyword">constexpr</span> <span class="type">int</span> kMaxRetryCount = <span class="number">3</span>;   <span class="comment">// 编译期常量</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (retry_count &lt; kMaxRetryCount) &#123;</span><br><span class="line">    retry_count++;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>3. 宏必须全大写</strong></p><p>这几乎是所有规范的共识——宏不遵循作用域规则，必须用大写字母划清界限：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></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> EMBEDMQ_FNV_OFFSET_BASIS 0x811c9dc5U</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> EMBEDMQ_HASH(topic) FNV1a((topic), sizeof(topic) - 1)</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// ❌ 绝对禁止 — 和函数名完全混淆</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> hash(topic) FNV1a((topic), sizeof(topic) - 1)</span></span><br></pre></td></tr></table></figure><hr><h2 id="四、头文件管理：嵌入式编译速度的命门"><a href="#四、头文件管理：嵌入式编译速度的命门" class="headerlink" title="四、头文件管理：嵌入式编译速度的命门"></a>四、头文件管理：嵌入式编译速度的命门</h2><p>嵌入式项目编译慢的根源几乎永远是头文件依赖爆炸。一个 <code>.c</code> 文件 <code>#include &quot;main.h&quot;</code>，<code>main.h</code> 再拖着几十个 HAL 头文件——改一行宏，全项目重编。</p><p>Google Style 的头文件规则恰好对症下药。</p><h3 id="规则-1：头文件必须自给自足"><a href="#规则-1：头文件必须自给自足" class="headerlink" title="规则 1：头文件必须自给自足"></a>规则 1：头文件必须自给自足</h3><p>每个 <code>.h</code> 必须能<strong>独立编译</strong>——它自己 <code>#include</code> 它所依赖的一切。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// sensor_manager.h</span></span><br><span class="line"><span class="meta">#<span class="keyword">ifndef</span> SENSOR_MANAGER_H_</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> SENSOR_MANAGER_H_</span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;cstdint&gt;</span>          <span class="comment">// 用了 uint32_t，必须自己包含</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;adc_driver.h&quot;</span>     <span class="comment">// 用了 AdcDriver，必须自己包含</span></span></span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">SensorManager</span> &#123;</span><br><span class="line"> <span class="keyword">public</span>:</span><br><span class="line">    <span class="function"><span class="type">void</span> <span class="title">Initialize</span><span class="params">()</span></span>;</span><br><span class="line">    <span class="function"><span class="type">uint32_t</span> <span class="title">ReadTemperature</span><span class="params">(<span class="type">const</span> AdcDriver &amp;adc)</span></span>;</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 class="comment">// SENSOR_MANAGER_H_</span></span></span><br></pre></td></tr></table></figure><p>验证方法：写一个 <code>.cc</code> 文件，第一行只 <code>#include</code> 你自己的头文件，能编译通过就说明合格。</p><h3 id="规则-2：-include-顺序不是玄学"><a href="#规则-2：-include-顺序不是玄学" class="headerlink" title="规则 2：#include 顺序不是玄学"></a>规则 2：<code>#include</code> 顺序不是玄学</h3><p>标准顺序：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">1. 相关头文件（如 foo.cc 的 foo.h）</span><br><span class="line">2. （空行）</span><br><span class="line">3. C 标准库头文件</span><br><span class="line">4. （空行）  </span><br><span class="line">5. C++ 标准库头文件</span><br><span class="line">6. （空行）</span><br><span class="line">7. 其他第三方库</span><br><span class="line">8. （空行）</span><br><span class="line">9. 本项目头文件</span><br></pre></td></tr></table></figure><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// adc_manager.cc — Include 顺序示例</span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;adc_manager.h&quot;</span>        <span class="comment">// ① 对应头文件最先，充当自包含性检查</span></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 class="comment">// ② C 库</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;string.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;array&gt;</span>                <span class="comment">// ③ C++ 库</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;memory&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;stm32f4xx_hal.h&quot;</span>      <span class="comment">// ④ 平台/HAL 层</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;freertos/FreeRTOS.h&quot;</span></span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;project_config.h&quot;</span>     <span class="comment">// ⑤ 本项目头文件</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;utils/debug_log.h&quot;</span></span></span><br></pre></td></tr></table></figure><p>把对应头文件放在第一位是最聪明的设计——如果 <code>adc_manager.h</code> 漏掉了某个 <code>#include</code>，<code>adc_manager.cc</code> <strong>立刻报错</strong>。这是一种零成本的持续集成检查。</p><h3 id="规则-3：谨慎使用前置声明"><a href="#规则-3：谨慎使用前置声明" class="headerlink" title="规则 3：谨慎使用前置声明"></a>规则 3：谨慎使用前置声明</h3><p>Google 明确说：<strong>避免用前置声明代替 <code>#include</code></strong>。前置声明会让依赖关系不可见、可能导致对象布局错误、刷新代码时改变语义。</p><p>唯一的例外：你真的只需要声明指针&#x2F;引用类型，且头文件包含会引入巨大的编译依赖链。这种情况下在前置声明旁加注释说明原因。</p><h3 id="嵌入式特例：预编译头文件"><a href="#嵌入式特例：预编译头文件" class="headerlink" title="嵌入式特例：预编译头文件"></a>嵌入式特例：预编译头文件</h3><p>很多 MCU IDE（如 STM32CubeIDE、Keil）会自动把 <code>stm32f4xx_hal.h</code> 塞进每个源文件。但 Google Style 的世界里，<strong>每个文件应该只包含它真正需要的头文件</strong>。如果你用 CMake + GCC 构建嵌入式项目，建议：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ❌ — 拖慢编译</span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;hal_all.h&quot;</span>  <span class="comment">// 包含全部 HAL 模块，哪怕你只用 GPIO</span></span></span><br><span class="line"></span><br><span class="line"><span class="comment">// ✅ — 按需包含</span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;hal_gpio.h&quot;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;hal_uart.h&quot;</span></span></span><br></pre></td></tr></table></figure><hr><h2 id="五、类-vs-结构体：嵌入式-C-最需要搞清的界限"><a href="#五、类-vs-结构体：嵌入式-C-最需要搞清的界限" class="headerlink" title="五、类 vs 结构体：嵌入式 C++ 最需要搞清的界限"></a>五、类 vs 结构体：嵌入式 C++ 最需要搞清的界限</h2><p>Google Style 对 <code>class</code> vs <code>struct</code> 的定义非常清晰：</p><table><thead><tr><th></th><th><code>struct</code></th><th><code>class</code></th></tr></thead><tbody><tr><td>用途</td><td>被动数据载体（无不变式）</td><td>封装状态 + 行为</td></tr><tr><td>成员</td><td>全部 <code>public</code>，无后缀</td><td>全部 <code>private</code>，尾部 <code>_</code></td></tr><tr><td>方法</td><td>可以有：构造函数、<code>Reset()</code>、<code>IsValid()</code></td><td>所有业务逻辑</td></tr><tr><td>继承</td><td>基本不用</td><td>OK</td></tr></tbody></table><h3 id="嵌入式里的典型用法"><a href="#嵌入式里的典型用法" class="headerlink" title="嵌入式里的典型用法"></a>嵌入式里的典型用法</h3><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// struct — 纯数据，打包传给 ISR 或 DMA</span></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">AccelerometerSample</span> &#123;</span><br><span class="line">    <span class="type">int16_t</span> x;</span><br><span class="line">    <span class="type">int16_t</span> y;</span><br><span class="line">    <span class="type">int16_t</span> z;</span><br><span class="line">    <span class="type">uint32_t</span> timestamp_ms;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// class — 封装复杂的传感器驱动</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Mpu6050Driver</span> &#123;</span><br><span class="line"> <span class="keyword">public</span>:</span><br><span class="line">    <span class="function"><span class="type">bool</span> <span class="title">Init</span><span class="params">(I2C_HandleTypeDef *i2c)</span></span>;</span><br><span class="line">    <span class="function"><span class="type">bool</span> <span class="title">ReadAccel</span><span class="params">(AccelerometerSample *out)</span></span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span>:</span><br><span class="line">    I2C_HandleTypeDef *i2c_handle_;</span><br><span class="line">    <span class="type">uint8_t</span> device_addr_;</span><br><span class="line">    <span class="type">bool</span> initialized_;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>这条规则在嵌入式项目里尤其有用——它迫使你区分”数据”和”逻辑”，自然导向更清晰的模块边界。</p><h3 id="设计原则：组合-继承"><a href="#设计原则：组合-继承" class="headerlink" title="设计原则：组合 &gt; 继承"></a>设计原则：组合 &gt; 继承</h3><p>Google 强烈偏好组合而非继承。在嵌入式里这一点更加重要——多重继承在 MCU 上不仅浪费 ROM，还会带来 vtable 开销。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ❌ 为了一点点复用引入深层继承</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">TemperatureSensor</span> : <span class="keyword">public</span> I2cDevice,</span><br><span class="line">                           <span class="keyword">public</span> PollableDevice,</span><br><span class="line">                           <span class="keyword">public</span> CalibratableDevice &#123;</span><br><span class="line">    <span class="comment">// 调度器不确定，vtable 三份，调试地狱</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="keyword">class</span> <span class="title class_">TemperatureSensor</span> &#123;</span><br><span class="line"> <span class="keyword">public</span>:</span><br><span class="line">    <span class="function"><span class="type">void</span> <span class="title">Init</span><span class="params">(I2C_HandleTypeDef *i2c)</span> </span>&#123; i2c_device_.<span class="built_in">Init</span>(i2c, kAddr); &#125;</span><br><span class="line">    <span class="function"><span class="type">float</span> <span class="title">Read</span><span class="params">()</span> </span>&#123; <span class="keyword">return</span> <span class="built_in">Calibrate</span>(i2c_device_.<span class="built_in">ReadReg</span>(kRegTemp)); &#125;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span>:</span><br><span class="line">    I2cDevice i2c_device_;</span><br><span class="line">    <span class="function"><span class="type">float</span> <span class="title">Calibrate</span><span class="params">(<span class="type">uint16_t</span> raw)</span></span>;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><hr><h2 id="六、函数与参数：在栈上传递意图"><a href="#六、函数与参数：在栈上传递意图" class="headerlink" title="六、函数与参数：在栈上传递意图"></a>六、函数与参数：在栈上传递意图</h2><h3 id="传参约定——一张表就够了"><a href="#传参约定——一张表就够了" class="headerlink" title="传参约定——一张表就够了"></a>传参约定——一张表就够了</h3><table><thead><tr><th>意图</th><th>传入参数类型</th><th>返回值</th></tr></thead><tbody><tr><td>只读（无所有权）</td><td><code>const T&amp;</code> 或 <code>const T*</code></td><td><code>T</code> 或 <code>bool</code></td></tr><tr><td>要修改（无所有权）</td><td><code>T*</code>（非空）</td><td><code>void</code> 或 <code>bool</code></td></tr><tr><td>转移所有权</td><td><code>std::unique_ptr&lt;T&gt;</code></td><td>—</td></tr><tr><td>共享所有权</td><td><code>std::shared_ptr&lt;T&gt;</code></td><td>—</td></tr></tbody></table><h3 id="嵌入式里的参数传递"><a href="#嵌入式里的参数传递" class="headerlink" title="嵌入式里的参数传递"></a>嵌入式里的参数传递</h3><p>在 MCU 上，<code>std::unique_ptr</code> 和 <code>std::shared_ptr</code> 基本用不上——没有堆分配器。嵌入式 C++ 里的所有权几乎总是<strong>单例模式</strong>或<strong>栈上静态分配</strong>。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ✅ 嵌入式风格的&quot;所有权&quot;——编译期就定死了</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">MotorController</span> &#123;</span><br><span class="line"> <span class="keyword">public</span>:</span><br><span class="line">    <span class="comment">// 不拥有 i2c — 只是引用，由 HAL 层管理生命周期</span></span><br><span class="line">    <span class="function"><span class="type">void</span> <span class="title">Init</span><span class="params">(I2C_HandleTypeDef *i2c)</span> </span>&#123; i2c_ = i2c; &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 拥有 config — 内部拷贝一份</span></span><br><span class="line">    <span class="function"><span class="type">void</span> <span class="title">Configure</span><span class="params">(<span class="type">const</span> MotorConfig &amp;config)</span> </span>&#123; config_ = config; &#125;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span>:</span><br><span class="line">    I2C_HandleTypeDef *i2c_;   <span class="comment">// 不拥有</span></span><br><span class="line">    MotorConfig config_;        <span class="comment">// 拥有</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h3 id="函数声明注意事项"><a href="#函数声明注意事项" class="headerlink" title="函数声明注意事项"></a>函数声明注意事项</h3><ul><li><strong>短函数可以 inline</strong>（Google Style 限制 ≤ 10 行）。嵌入式里编译器 <code>__attribute__((always_inline))</code> 也很常见，但交给编译器决定更好。</li><li><strong>输出参数用指针而不是引用</strong>——这是 Google Style 的强烈建议，因为指针在调用处更显眼：</li></ul><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 调用处理后的返回值：status 是指针，调用处一眼可见会被修改</span></span><br><span class="line"><span class="function"><span class="type">bool</span> <span class="title">ProcessFrame</span><span class="params">(<span class="type">const</span> Frame &amp;input, Frame *output, Error *status)</span></span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 调用处：</span></span><br><span class="line"><span class="comment">// Frame output;</span></span><br><span class="line"><span class="comment">// Error err;</span></span><br><span class="line"><span class="comment">// ProcessFrame(input, &amp;output, &amp;err);  ← &amp; 提醒：会被修改</span></span><br></pre></td></tr></table></figure><hr><h2 id="七、禁止异常：嵌入式早就不玩了"><a href="#七、禁止异常：嵌入式早就不玩了" class="headerlink" title="七、禁止异常：嵌入式早就不玩了"></a>七、禁止异常：嵌入式早就不玩了</h2><p>Google Style 第一条严格限制就是<strong>彻底禁止 C++ 异常</strong>。原因不分平台：</p><ol><li>异常导致非局部控制流——代码里看不出哪里会”跳出来”</li><li>关闭异常（<code>-fno-exceptions</code>）后，二进制体积通常减少 15-20%</li><li>异常安全代码需要大量 RAII 包装，增加认知负担</li></ol><p>在嵌入式领域，禁止异常几乎是默认选项。大部分 MCU 工具链的 <code>libstdc++</code> 或 <code>libc++</code> 根本就不支持异常展开。如果你开启 <code>-fexceptions</code>，链接器会报一堆未定义符号。</p><h3 id="替代方案：错误码-工厂函数"><a href="#替代方案：错误码-工厂函数" class="headerlink" title="替代方案：错误码 + 工厂函数"></a>替代方案：错误码 + 工厂函数</h3><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 构造函数失败——不能用异常，用工厂函数</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">RingBuffer</span> &#123;</span><br><span class="line"> <span class="keyword">public</span>:</span><br><span class="line">    <span class="function"><span class="type">static</span> std::optional&lt;RingBuffer&gt; <span class="title">Create</span><span class="params">(<span class="type">size_t</span> size)</span> </span>&#123;</span><br><span class="line">        <span class="type">uint8_t</span> *buf = <span class="built_in">static_cast</span>&lt;<span class="type">uint8_t</span> *&gt;(<span class="built_in">malloc</span>(size));</span><br><span class="line">        <span class="keyword">if</span> (buf == <span class="literal">nullptr</span>) <span class="keyword">return</span> std::<span class="literal">nullopt</span>;</span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">RingBuffer</span>(buf, size);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span>:</span><br><span class="line">    <span class="built_in">RingBuffer</span>(<span class="type">uint8_t</span> *buf, <span class="type">size_t</span> size) : <span class="built_in">buf_</span>(buf), <span class="built_in">size_</span>(size) &#123;&#125;</span><br><span class="line">    <span class="type">uint8_t</span> *buf_;</span><br><span class="line">    <span class="type">size_t</span> size_;</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="keyword">auto</span> rb = RingBuffer::<span class="built_in">Create</span>(<span class="number">1024</span>);</span><br><span class="line"><span class="keyword">if</span> (!rb.<span class="built_in">has_value</span>()) &#123;</span><br><span class="line">    <span class="comment">// 处理分配失败</span></span><br><span class="line">    <span class="keyword">return</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在更裸的 MCU 环境（C++17 不可用），直接用 C 风格返回值：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">enum class</span> <span class="title class_">RingBufferError</span> &#123;</span><br><span class="line">    kOk = <span class="number">0</span>,</span><br><span class="line">    kNullPointer = <span class="number">1</span>,</span><br><span class="line">    kOutOfMemory = <span class="number">2</span>,</span><br><span class="line">    kFull = <span class="number">3</span>,</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="function">RingBufferError <span class="title">RingBuffer_Init</span><span class="params">(RingBuffer *rb, <span class="type">uint8_t</span> *buf, <span class="type">size_t</span> size)</span></span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 调用处</span></span><br><span class="line">RingBufferError err = <span class="built_in">RingBuffer_Init</span>(&amp;rb, buffer, <span class="built_in">sizeof</span>(buffer));</span><br><span class="line"><span class="keyword">if</span> (err != RingBufferError::kOk) &#123;</span><br><span class="line">    <span class="built_in">ErrorHandler</span>(err);</span><br><span class="line">    <span class="keyword">return</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><hr><h2 id="八、类型转换：别用括号硬搞"><a href="#八、类型转换：别用括号硬搞" class="headerlink" title="八、类型转换：别用括号硬搞"></a>八、类型转换：别用括号硬搞</h2><p>嵌入式的 HAL 层到处都是 <code>(uint8_t *)&amp;some_struct</code>、<code>(uint32_t)ptr</code>。Google Style 对类型转换的要求非常严格——但嵌入式有特例。</p><h3 id="Google-要求"><a href="#Google-要求" class="headerlink" title="Google 要求"></a>Google 要求</h3><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ❌ Google 禁止 C 风格转换</span></span><br><span class="line"><span class="type">int</span> y = (<span class="type">int</span>)x;</span><br><span class="line"><span class="type">char</span> *p = (<span class="type">char</span> *)buffer;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ✅ 使用 C++ 风格转换</span></span><br><span class="line"><span class="type">int</span> y = <span class="built_in">static_cast</span>&lt;<span class="type">int</span>&gt;(x);</span><br><span class="line"><span class="type">char</span> *p = <span class="built_in">reinterpret_cast</span>&lt;<span class="type">char</span> *&gt;(buffer);</span><br></pre></td></tr></table></figure><h3 id="嵌入式妥协"><a href="#嵌入式妥协" class="headerlink" title="嵌入式妥协"></a>嵌入式妥协</h3><p>在和外设寄存器、DMA 缓冲区、链接脚本符号打交道时，类型转换不可避免。我的建议是：</p><p><strong>在 HAL&#x2F;驱动层</strong>：允许 C 风格转换（ST HAL 库本身就大量使用），但加注释说明：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// OK — 硬件寄存器地址，必须强转</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> GPIOA_BASE ((uint32_t)0x40020000)     <span class="comment">// 地址常量</span></span></span><br><span class="line">GPIO_TypeDef *gpioa = (GPIO_TypeDef *)GPIOA_BASE;  <span class="comment">// 寄存器映射</span></span><br></pre></td></tr></table></figure><p><strong>在应用逻辑层</strong>：严格使用 C++ 风格转换：</p><figure class="highlight cpp"><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="keyword">auto</span> ticks = <span class="built_in">static_cast</span>&lt;TickType_t&gt;(timeout_ms / portTICK_PERIOD_MS);</span><br><span class="line"><span class="keyword">auto</span> *payload = <span class="built_in">reinterpret_cast</span>&lt;<span class="type">const</span> <span class="type">uint8_t</span> *&gt;(&amp;data);</span><br></pre></td></tr></table></figure><hr><h2 id="九、格式化：别用手工排版"><a href="#九、格式化：别用手工排版" class="headerlink" title="九、格式化：别用手工排版"></a>九、格式化：别用手工排版</h2><p>Google Style 的格式化规则用 <code>clang-format</code> 一键搞定。核心规则：</p><table><thead><tr><th>规则</th><th>值</th></tr></thead><tbody><tr><td>行宽</td><td>≤ 80 字符</td></tr><tr><td>缩进</td><td>2 空格（绝不用 Tab）</td></tr><tr><td>大括号</td><td>K&amp;R 变体 — 控制流同行，函数&#x2F;类另起行</td></tr><tr><td>指针&#x2F;引用</td><td><code>int* x;</code> 或 <code>int *x;</code>，文件内保持一致</td></tr></tbody></table><h3 id="clang-format-配置文件"><a href="#clang-format-配置文件" class="headerlink" title=".clang-format 配置文件"></a><code>.clang-format</code> 配置文件</h3><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></pre></td><td class="code"><pre><span class="line"><span class="attr">BasedOnStyle:</span> <span class="string">Google</span></span><br><span class="line"><span class="attr">ColumnLimit:</span> <span class="number">80</span></span><br><span class="line"><span class="attr">IndentWidth:</span> <span class="number">2</span></span><br><span class="line"><span class="attr">UseTab:</span> <span class="string">Never</span></span><br><span class="line"><span class="attr">AccessModifierOffset:</span> <span class="number">-1</span></span><br><span class="line"><span class="attr">AllowShortFunctionsOnASingleLine:</span> <span class="string">Inline</span></span><br></pre></td></tr></table></figure><h3 id="嵌入式-CI-集成"><a href="#嵌入式-CI-集成" class="headerlink" title="嵌入式 CI 集成"></a>嵌入式 CI 集成</h3><p>在 CI 脚本里加一行：</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">clang-format --dry-run --Werror <span class="built_in">source</span>/**/*.cc <span class="built_in">source</span>/**/*.h</span><br></pre></td></tr></table></figure><p>任何格式不合格的代码直接 <strong>-Werror</strong> 退出，不要等人手动检查。</p><hr><h2 id="十、宏：Google-说禁止，嵌入式说离不开"><a href="#十、宏：Google-说禁止，嵌入式说离不开" class="headerlink" title="十、宏：Google 说禁止，嵌入式说离不开"></a>十、宏：Google 说禁止，嵌入式说离不开</h2><p>这是 Google Style 和嵌入式最大的分歧点。Google 说”宏几乎总能被内联函数、<code>constexpr</code> 或枚举替代”——在 Linux 应用层确实如此。但嵌入式代码里：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></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> GPIO_SET_PIN(port, pin)   ((port)-&gt;BSRR = (1U &lt;&lt; (pin)))</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> GPIO_CLEAR_PIN(port, pin) ((port)-&gt;BRR  = (1U &lt;&lt; (pin)))</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 链接脚本符号 — 不是 C++ 类型系统能表达的</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> __VECT_TAB_BASE 0x08000000U</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> __STACK_TOP     0x20020000U</span></span><br></pre></td></tr></table></figure><h3 id="妥协策略"><a href="#妥协策略" class="headerlink" title="妥协策略"></a>妥协策略</h3><p><strong>可以继续用宏的场景：</strong></p><ul><li>寄存器位操作</li><li>链接脚本符号的外漏常量</li><li>硬件地址常量映射</li><li><code>#ifdef</code> 条件编译（不同 MCU 系列的差异化代码）</li></ul><p><strong>应该替换成 C++ 的场景：</strong></p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ❌ 功能宏 — 用 constexpr 替换</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> MAX(a, b) ((a) &gt; (b) ? (a) : (b))</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// ✅</span></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">typename</span> T&gt;</span><br><span class="line"><span class="function"><span class="keyword">constexpr</span> T <span class="title">Max</span><span class="params">(T a, T b)</span> </span>&#123; <span class="keyword">return</span> a &gt; b ? a : b; &#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ❌ 调试宏 — 用 constexpr 变量替换</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> DEBUG_UART_BAUDRATE 115200</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// ✅</span></span><br><span class="line"><span class="keyword">constexpr</span> <span class="type">uint32_t</span> kDebugUartBaudrate = <span class="number">115200</span>;</span><br></pre></td></tr></table></figure><p><strong>必须遵守的底线：</strong></p><ul><li>宏名称全部大写，加项目前缀</li><li>多语句宏必须 <code>do { ... } while (0)</code> 包裹</li><li>宏内参数用括号</li></ul><hr><h2 id="十一、全局变量：Google-说禁止，嵌入式确实要妥协"><a href="#十一、全局变量：Google-说禁止，嵌入式确实要妥协" class="headerlink" title="十一、全局变量：Google 说禁止，嵌入式确实要妥协"></a>十一、全局变量：Google 说禁止，嵌入式确实要妥协</h2><p>Google Style 对全局变量（包括 <code>static</code> 存储期对象）非常严格。但在裸机和 RTOS 环境下，全局状态是设计的一部分——任务通信、设备句柄、系统状态都必须跨函数存在。</p><h3 id="嵌入式里的”安全全局变量”"><a href="#嵌入式里的”安全全局变量”" class="headerlink" title="嵌入式里的”安全全局变量”"></a>嵌入式里的”安全全局变量”</h3><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ✅  文件作用域 static — 不对外可见</span></span><br><span class="line"><span class="type">static</span> AdcDriver g_adc1;</span><br><span class="line"><span class="type">static</span> SemaphoreHandle_t g_data_semaphore;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ✅  命名空间 + 访问控制 — 对外刻意暴露</span></span><br><span class="line"><span class="keyword">namespace</span> SystemState &#123;</span><br><span class="line">    <span class="function"><span class="type">bool</span> <span class="title">IsCalibrated</span><span class="params">()</span></span>;</span><br><span class="line">    <span class="function"><span class="type">void</span> <span class="title">SetCalibrated</span><span class="params">(<span class="type">bool</span> calibrated)</span></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="type">bool</span> g_system_calibrated;  <span class="comment">// 坏味道</span></span><br></pre></td></tr></table></figure><p><strong>核心原则</strong>：如果不得不使用全局变量，把它锁在最小作用域里——<code>static</code> 文件作用域或 <code>namespace</code> + 函数封装。</p><hr><h2 id="十二、纯-C-项目：Google-风格怎么落地？"><a href="#十二、纯-C-项目：Google-风格怎么落地？" class="headerlink" title="十二、纯 C 项目：Google 风格怎么落地？"></a>十二、纯 C 项目：Google 风格怎么落地？</h2><p>上面的讨论以 C++ 为主，但嵌入式圈有大量纯 C 项目——FreeRTOS、uC&#x2F;OS、contiki、各种 MCU BSP 全是 C。</p><p>Google 没有独立的 “C Style Guide”。C 代码在 Google 内部遵循同一份 C++ Guide，把类、异常、模板那堆 C++ 专用的规则摘掉就是。C 代码的命名、格式、头文件管理，跟 C++ 版完全相同。</p><p>但对于嵌入式 C 项目，有几个地方值得单独展开。</p><h3 id="C-的命名要不要加-g-前缀？"><a href="#C-的命名要不要加-g-前缀？" class="headerlink" title="C 的命名要不要加 g_ 前缀？"></a>C 的命名要不要加 <code>g_</code> 前缀？</h3><p>Google Style 没提 <code>g_</code>，但嵌入式 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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 匈牙利命名变体：g_ 全局、s_ 静态、p_ 指针</span></span><br><span class="line"><span class="type">static</span> <span class="type">uint8_t</span> s_key_last_state[<span class="number">8</span>];  <span class="comment">// 文件作用域 static</span></span><br><span class="line">UART_HandleTypeDef *g_huart1;        <span class="comment">// 全局可见</span></span><br></pre></td></tr></table></figure><p>这套命名法不是 Google 规范，但它在裸机 C 项目里很实用——没有命名空间，没有类，作用域全靠前缀区分。我的建议：团队内部统一就行，不必强求 Google 原版。</p><h3 id="C-没有命名空间怎么办？"><a href="#C-没有命名空间怎么办？" class="headerlink" title="C 没有命名空间怎么办？"></a>C 没有命名空间怎么办？</h3><p>命名空间是 Google Style 里最重要的隔离手段之一。纯 C 没有这个概念，替代方案是<strong>函数名前缀</strong>：</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// ❌ 裸函数名——链接时容易撞</span></span><br><span class="line"><span class="type">void</span> <span class="title function_">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_">Read</span><span class="params">(<span class="type">float</span> *out)</span>;</span><br><span class="line"><span class="type">void</span> <span class="title function_">Reset</span><span class="params">(<span class="type">void</span>)</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ✅ 模块前缀——C 的&quot;命名空间&quot;</span></span><br><span class="line"><span class="type">void</span> <span class="title function_">TempCtrl_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_">TempCtrl_Read</span><span class="params">(<span class="type">float</span> *out)</span>;</span><br><span class="line"><span class="type">void</span> <span class="title function_">TempCtrl_Reset</span><span class="params">(<span class="type">void</span>)</span>;</span><br></pre></td></tr></table></figure><p>对于每个模块，统一一个 2-4 字符的前缀或模块全名。别心疼那点打字时间，换来的是全局搜索时一眼定位。</p><h3 id="C-的-struct-怎么玩？"><a href="#C-的-struct-怎么玩？" class="headerlink" title="C 的 struct 怎么玩？"></a>C 的 <code>struct</code> 怎么玩？</h3><p>Google Style 下，C++ 的 struct 就是纯数据容器。C 语言里 struct 承担了更多角色——POD、接口注入、回调封装。命名上推荐：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// struct 名 — PascalCase</span></span><br><span class="line"><span class="comment">// 成员 — snake_case，无后缀</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> &#123;</span></span><br><span class="line">    <span class="type">float</span> proportional;</span><br><span class="line">    <span class="type">float</span> integral;</span><br><span class="line">    <span class="type">float</span> derivative;</span><br><span class="line">    <span class="type">float</span> output_max;</span><br><span class="line">&#125; PidParams;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 函数 — 模块前缀_PascalCase</span></span><br><span class="line"><span class="type">bool</span> <span class="title function_">Pid_Init</span><span class="params">(<span class="type">const</span> PidParams *params)</span>;</span><br><span class="line"><span class="type">float</span> <span class="title function_">Pid_Compute</span><span class="params">(<span class="type">float</span> setpoint, <span class="type">float</span> measured)</span>;</span><br></pre></td></tr></table></figure><h3 id="C-的枚举和宏"><a href="#C-的枚举和宏" class="headerlink" title="C 的枚举和宏"></a>C 的枚举和宏</h3><p>这是 C 和 C++ 分歧最大的地方。C++ 有 <code>enum class</code>——类型安全、作用域限定。C 只能裸 <code>enum</code>：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ❌ C enum — 全部漏到全局命名空间</span></span><br><span class="line"><span class="class"><span class="keyword">enum</span> &#123;</span> OK, ERROR, TIMEOUT &#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ✅ 加前缀隔离</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">enum</span> &#123;</span></span><br><span class="line">    RINGBUF_OK = <span class="number">0</span>,</span><br><span class="line">    RINGBUF_ERR_NULL = <span class="number">1</span>,</span><br><span class="line">    RINGBUF_ERR_FULL = <span class="number">2</span>,</span><br><span class="line">&#125; RingBuf_Error;</span><br></pre></td></tr></table></figure><p>宏方面，C 没有 <code>constexpr</code>，常量只能用 <code>#define</code> 或 <code>const</code>。规则不变：<strong>宏全大写，const 变量 snake_case</strong>：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">define</span> SENSOR_MAX_CHANNELS 8           <span class="comment">// 宏 — 全大写</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> SENSOR_SAMPLE_RATE_HZ 1000      <span class="comment">// 宏 — 全大写</span></span></span><br><span class="line"></span><br><span class="line"><span class="type">static</span> <span class="type">const</span> <span class="type">uint32_t</span> kPollIntervalMs = <span class="number">100</span>;  <span class="comment">// const — k前缀</span></span><br></pre></td></tr></table></figure><h3 id="C-和-C-混合项目"><a href="#C-和-C-混合项目" class="headerlink" title="C 和 C++ 混合项目"></a>C 和 C++ 混合项目</h3><p>如果你的项目是 C HAL 层 + C++ 应用逻辑（FreeRTOS + C++ 很常见）：</p><ul><li><code>extern &quot;C&quot;</code> 包裹所有 C 头文件接口</li><li>C 代码里不要用任何 C++ 特性（<code>bool</code> 除外——C23 前用 <code>&lt;stdbool.h&gt;</code>，C23 后内置）</li><li>编译选项里 C 和 C++ 分开设：<code>-std=c11</code> + <code>-std=c++17</code></li></ul><hr><h2 id="十三、工具链：别背规范，让机器人干"><a href="#十三、工具链：别背规范，让机器人干" class="headerlink" title="十三、工具链：别背规范，让机器人干"></a>十三、工具链：别背规范，让机器人干</h2><p>规范最烦的地方不是”记不住”，而是人工检查浪费时间。三件套搞定：</p><table><thead><tr><th>工具</th><th>作用</th><th>怎么装</th></tr></thead><tbody><tr><td><code>clang-format</code></td><td>自动格式化</td><td><code>apt install clang-format</code></td></tr><tr><td><code>cpplint</code></td><td>风格检查</td><td><code>pip install cpplint</code></td></tr><tr><td><code>clang-tidy</code></td><td>静态分析 + 风格</td><td><code>apt install clang-tidy</code></td></tr></tbody></table><h3 id="IDE-集成"><a href="#IDE-集成" class="headerlink" title="IDE 集成"></a>IDE 集成</h3><p>VS Code 里，<code>settings.json</code> 加：</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;C_Cpp.clang_format_style&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Google&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;editor.formatOnSave&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>保存文件时自动格式化。你只管写逻辑，格式交给工具。</p><h3 id="Pre-commit-Hook"><a href="#Pre-commit-Hook" class="headerlink" title="Pre-commit Hook"></a>Pre-commit Hook</h3><p>项目根目录 <code>.git/hooks/pre-commit</code>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#!/bin/bash</span></span><br><span class="line"><span class="comment"># 保证提交前代码格式不出问题</span></span><br><span class="line">clang-format --dry-run --Werror $(git diff --cached --name-only --diff-filter=ACMR | grep -E <span class="string">&#x27;\.(cc|h|cpp|hpp|c)$&#x27;</span>) \</span><br><span class="line">    || &#123; <span class="built_in">echo</span> <span class="string">&quot;格式不通过，请运行 clang-format -i 修正&quot;</span>; <span class="built_in">exit</span> 1; &#125;</span><br></pre></td></tr></table></figure><hr><h2 id="十四、完整案例：重构一个-200-行的温控模块"><a href="#十四、完整案例：重构一个-200-行的温控模块" class="headerlink" title="十四、完整案例：重构一个 200 行的温控模块"></a>十四、完整案例：重构一个 200 行的温控模块</h2><h3 id="重构前（200-行）"><a href="#重构前（200-行）" class="headerlink" title="重构前（200 行）"></a>重构前（200 行）</h3><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// tempctl.c — 典型无规范嵌入式文件</span></span><br><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;all_drivers.h&quot;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;freertos.h&quot;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;utils.h&quot;</span></span></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> tmp, set, mode, cnt, err;</span><br><span class="line"><span class="type">float</span> kp = <span class="number">1.5</span>, ki = <span class="number">0.1</span>, kd = <span class="number">0.05</span>;</span><br><span class="line"><span class="type">float</span> integ, prev_err;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">init</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    tmp = <span class="number">0</span>; set = <span class="number">250</span>; mode = <span class="number">1</span>; cnt = <span class="number">0</span>; err = <span class="number">0</span>;</span><br><span class="line">    integ = <span class="number">0</span>; prev_err = <span class="number">0</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">loop</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (mode == <span class="number">1</span>) &#123;</span><br><span class="line">        tmp = <span class="built_in">read_adc</span>(<span class="number">3</span>);</span><br><span class="line">        err = set - tmp;</span><br><span class="line">        integ += err * <span class="number">0.1</span>;</span><br><span class="line">        <span class="keyword">if</span> (integ &gt; <span class="number">100</span>) integ = <span class="number">100</span>;</span><br><span class="line">        <span class="keyword">if</span> (integ &lt; <span class="number">-100</span>) integ = <span class="number">-100</span>;</span><br><span class="line">        <span class="type">float</span> deriv = (err - prev_err) / <span class="number">0.1</span>;</span><br><span class="line">        <span class="type">float</span> out = kp * err + ki * integ + kd * deriv;</span><br><span class="line">        <span class="keyword">if</span> (out &gt; <span class="number">1000</span>) out = <span class="number">1000</span>;</span><br><span class="line">        <span class="keyword">if</span> (out &lt; <span class="number">0</span>) out = <span class="number">0</span>;</span><br><span class="line">        <span class="built_in">set_pwm</span>(<span class="number">1</span>, (<span class="type">int</span>)out);</span><br><span class="line">        prev_err = err;</span><br><span class="line">        cnt++;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>问题清单</strong>：无类型、无命名、无模块边界、魔法数字、PID 参数全局暴露、无错误处理、ISR 不可重入。</p><h3 id="重构后"><a href="#重构后" class="headerlink" title="重构后"></a>重构后</h3><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// temperature_controller.h</span></span><br><span class="line"><span class="meta">#<span class="keyword">ifndef</span> TEMPERATURE_CONTROLLER_H_</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> TEMPERATURE_CONTROLLER_H_</span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;cstdint&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> temperature_controller &#123;</span><br><span class="line"></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">PidParameters</span> &#123;</span><br><span class="line">    <span class="type">float</span> kp;</span><br><span class="line">    <span class="type">float</span> ki;</span><br><span class="line">    <span class="type">float</span> kd;</span><br><span class="line">    <span class="type">float</span> integral_limit;</span><br><span class="line">    <span class="type">float</span> output_max;</span><br><span class="line">    <span class="type">float</span> output_min;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">TemperatureController</span> &#123;</span><br><span class="line"> <span class="keyword">public</span>:</span><br><span class="line">    <span class="function"><span class="type">void</span> <span class="title">Init</span><span class="params">(<span class="type">const</span> PidParameters &amp;params, <span class="type">uint8_t</span> adc_channel,</span></span></span><br><span class="line"><span class="params"><span class="function">              <span class="type">uint8_t</span> pwm_channel)</span></span>;</span><br><span class="line">    <span class="function"><span class="type">void</span> <span class="title">Update</span><span class="params">()</span></span>;</span><br><span class="line">    <span class="function"><span class="type">bool</span> <span class="title">IsRunning</span><span class="params">()</span> <span class="type">const</span> </span>&#123; <span class="keyword">return</span> initialized_; &#125;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span>:</span><br><span class="line">    <span class="function"><span class="type">float</span> <span class="title">ReadTemperature</span><span class="params">()</span></span>;</span><br><span class="line">    <span class="function"><span class="type">void</span> <span class="title">SetHeaterOutput</span><span class="params">(<span class="type">float</span> duty_cycle)</span></span>;</span><br><span class="line">    <span class="function"><span class="type">float</span> <span class="title">ComputePid</span><span class="params">(<span class="type">float</span> setpoint, <span class="type">float</span> measured)</span></span>;</span><br><span class="line"></span><br><span class="line">    PidParameters params_;</span><br><span class="line">    <span class="type">uint8_t</span> adc_channel_;</span><br><span class="line">    <span class="type">uint8_t</span> pwm_channel_;</span><br><span class="line">    <span class="type">uint32_t</span> iteration_count_;</span><br><span class="line">    <span class="type">float</span> integral_;</span><br><span class="line">    <span class="type">float</span> prev_error_;</span><br><span class="line">    <span class="type">bool</span> initialized_;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">&#125;  <span class="comment">// namespace temperature_controller</span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">endif</span>  <span class="comment">// TEMPERATURE_CONTROLLER_H_</span></span></span><br></pre></td></tr></table></figure><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// temperature_controller.cc</span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;temperature_controller.h&quot;</span></span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;algorithm&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;adc_driver.h&quot;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;logger.h&quot;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;pwm_driver.h&quot;</span></span></span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> temperature_controller &#123;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> <span class="keyword">constexpr</span> <span class="type">float</span> kDefaultSetpointCelsius = <span class="number">250.0f</span>;</span><br><span class="line"><span class="type">static</span> <span class="keyword">constexpr</span> <span class="type">float</span> kUpdateIntervalSec = <span class="number">0.1f</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">TemperatureController::Init</span><span class="params">(<span class="type">const</span> PidParameters &amp;params,</span></span></span><br><span class="line"><span class="params"><span class="function">                                  <span class="type">uint8_t</span> adc_channel,</span></span></span><br><span class="line"><span class="params"><span class="function">                                  <span class="type">uint8_t</span> pwm_channel)</span> </span>&#123;</span><br><span class="line">    params_ = params;</span><br><span class="line">    adc_channel_ = adc_channel;</span><br><span class="line">    pwm_channel_ = pwm_channel;</span><br><span class="line">    integral_ = <span class="number">0.0f</span>;</span><br><span class="line">    prev_error_ = <span class="number">0.0f</span>;</span><br><span class="line">    iteration_count_ = <span class="number">0</span>;</span><br><span class="line">    initialized_ = <span class="literal">true</span>;</span><br><span class="line">    <span class="built_in">LOG_INFO</span>(<span class="string">&quot;TemperatureController initialized on ADC ch=%d, PWM ch=%d&quot;</span>,</span><br><span class="line">             adc_channel, pwm_channel);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">TemperatureController::Update</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (!initialized_) <span class="keyword">return</span>;</span><br><span class="line"></span><br><span class="line">    <span class="type">const</span> <span class="type">float</span> measured = <span class="built_in">ReadTemperature</span>();</span><br><span class="line">    <span class="type">const</span> <span class="type">float</span> output = <span class="built_in">ComputePid</span>(kDefaultSetpointCelsius, measured);</span><br><span class="line">    <span class="built_in">SetHeaterOutput</span>(output);</span><br><span class="line">    iteration_count_++;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">float</span> <span class="title">TemperatureController::ReadTemperature</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> AdcDriver::<span class="built_in">ReadVoltage</span>(adc_channel_) * <span class="number">100.0f</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">float</span> <span class="title">TemperatureController::ComputePid</span><span class="params">(<span class="type">float</span> setpoint, <span class="type">float</span> measured)</span> </span>&#123;</span><br><span class="line">    <span class="type">const</span> <span class="type">float</span> error = setpoint - measured;</span><br><span class="line"></span><br><span class="line">    integral_ += error * kUpdateIntervalSec;</span><br><span class="line">    integral_ = std::<span class="built_in">clamp</span>(integral_, -params_.integral_limit,</span><br><span class="line">                           params_.integral_limit);</span><br><span class="line"></span><br><span class="line">    <span class="type">const</span> <span class="type">float</span> derivative = (error - prev_error_) / kUpdateIntervalSec;</span><br><span class="line">    prev_error_ = error;</span><br><span class="line"></span><br><span class="line">    <span class="type">const</span> <span class="type">float</span> output = params_.kp * error + params_.ki * integral_ +</span><br><span class="line">                         params_.kd * derivative;</span><br><span class="line">    <span class="keyword">return</span> std::<span class="built_in">clamp</span>(output, params_.output_min, params_.output_max);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">TemperatureController::SetHeaterOutput</span><span class="params">(<span class="type">float</span> duty_cycle)</span> </span>&#123;</span><br><span class="line">    PwmDriver::<span class="built_in">SetDuty</span>(pwm_channel_, <span class="built_in">static_cast</span>&lt;<span class="type">uint32_t</span>&gt;(duty_cycle));</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">&#125;  <span class="comment">// namespace temperature_controller</span></span><br></pre></td></tr></table></figure><p>同样的 PID 温控逻辑，重构后：</p><ul><li>命名清楚：<code>integral_</code> 替代了 <code>integ</code></li><li><code>std::clamp</code> 替代了手动 <code>if</code> 限幅</li><li><code>LOG_INFO</code> 替代了 <code>printf</code></li><li>命名空间隔离了所有符号</li><li>类接口明确区分了公有&#x2F;私有</li><li><code>constexpr</code> 消除了魔法数字 <code>250</code>、<code>0.1</code></li></ul><hr><h2 id="十五、最后说两句"><a href="#十五、最后说两句" class="headerlink" title="十五、最后说两句"></a>十五、最后说两句</h2><p>规范这东西，争论起来没完没了——缩进用空格还是 Tab、大括号换不换行、变量名用驼峰还是下划线。但写嵌入式的人都知道一个更朴素的事实：<strong>三个月后凌晨两点调 bug 的时候，你不会关心当初写代码时省了 3 秒还是 5 秒。你会关心自己在不在骂那个人。</strong></p><p>Google C++ Style Guide 就是按这个标准设计的。</p><p>如果你团队现在就一个人，先做最简单的：命名统一、Include 顺序固定、装个 <code>clang-format</code> 保存时自动排版。这三件事没什么认知负担，但效果立竿见影。</p><p>如果你在带团队或者维护一个开源项目，再加一条：CI 上挂个 <code>clang-tidy</code>，不通过不能合。机器人来当坏人，比人当坏人轻松。</p><p>规范是给人看的，不是给编译器看的。编译器不关心你变量叫什么，但人关心。</p><hr><blockquote><p><strong>参考链接</strong></p><ul><li><a href="https://google.github.io/styleguide/cppguide.html">Google C++ Style Guide 官方</a></li><li><a href="https://github.com/cpplint/cpplint">cpplint — Google Style Checker</a></li><li><a href="https://clang.llvm.org/docs/ClangFormat.html">ClangFormat 官方文档</a></li></ul></blockquote>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;一、这东西到底有什么用？&quot;&gt;&lt;a href=&quot;#一、这东西到底有什么用？&quot; class=&quot;headerlink&quot; title=&quot;一、这东西到底有什么用？&quot;&gt;&lt;/a&gt;一、这东西到底有什么用？&lt;/h2&gt;&lt;p&gt;2013 年 Google 把内部 C++ 规范扔上了 Gi</summary>
      
    
    
    
    <category term="嵌入式" scheme="https://w4ysonch.github.io/categories/%E5%B5%8C%E5%85%A5%E5%BC%8F/"/>
    
    
    <category term="C++" scheme="https://w4ysonch.github.io/tags/C/"/>
    
    <category term="代码规范" scheme="https://w4ysonch.github.io/tags/%E4%BB%A3%E7%A0%81%E8%A7%84%E8%8C%83/"/>
    
    <category term="软件工程" scheme="https://w4ysonch.github.io/tags/%E8%BD%AF%E4%BB%B6%E5%B7%A5%E7%A8%8B/"/>
    
  </entry>
  
  <entry>
    <title>FreeRTOS 学习笔记（三）：信号量与互斥锁</title>
    <link href="https://w4ysonch.github.io/2025/08/15/freertos-semaphore-mutex/"/>
    <id>https://w4ysonch.github.io/2025/08/15/freertos-semaphore-mutex/</id>
    <published>2025-08-15T14:32:12.000Z</published>
    <updated>2026-06-29T07:50:23.717Z</updated>
    
    <content type="html"><![CDATA[<p>信号量和队列是亲戚——信号量本质上就是个不许传数据的队列。它不关心消息内容，只关心”有没有”。</p><hr><p><strong>二值信号量（Binary Semaphore）。</strong></p><p>就像一个只能放一个令牌的盒子。任务调用 <code>xSemaphoreTake()</code> 拿走令牌，盒子空了；另一个任务（或 ISR）调用 <code>xSemaphoreGive()</code> 放回令牌，任务被唤醒。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">SemaphoreHandle_t <span class="title function_">xSemaphoreCreateBinary</span><span class="params">(<span class="type">void</span>)</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 消费者</span></span><br><span class="line"><span class="keyword">if</span> (xSemaphoreTake(sem, portMAX_DELAY) == pdTRUE) &#123;</span><br><span class="line">    <span class="comment">// 拿到令牌，干活</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 生产者（任务或 ISR）</span></span><br><span class="line">xSemaphoreGive(sem);              <span class="comment">// 任务里用</span></span><br><span class="line">xSemaphoreGiveFromISR(sem, &amp;woken); <span class="comment">// ISR 里用</span></span><br></pre></td></tr></table></figure><p>最常见的场景：ISR 通知任务”数据准备好了”。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">SemaphoreHandle_t g_data_ready;</span><br><span class="line"></span><br><span class="line"><span class="comment">// UART 中断：数据收完，通知任务处理</span></span><br><span class="line"><span class="type">void</span> <span class="title function_">UART_RxComplete_IRQ</span><span class="params">(<span class="type">void</span>)</span> &#123;</span><br><span class="line">    BaseType_t woken = pdFALSE;</span><br><span class="line">    xSemaphoreGiveFromISR(g_data_ready, &amp;woken);</span><br><span class="line">    portYIELD_FROM_ISR(woken);</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_">vUARTProcessor</span><span class="params">(<span class="type">void</span> *pv)</span> &#123;</span><br><span class="line">    <span class="keyword">while</span> (<span class="number">1</span>) &#123;</span><br><span class="line">        xSemaphoreTake(g_data_ready, portMAX_DELAY);</span><br><span class="line">        ProcessReceivedData();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><hr><p><strong>计数信号量（Counting Semaphore）。</strong></p><p>和二进制一样，但令牌可以有多个。适合管理有限资源——比如 3 个 DMA 通道：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">SemaphoreHandle_t g_dma_sem = xSemaphoreCreateCounting(<span class="number">3</span>, <span class="number">3</span>);</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">vTask</span><span class="params">(<span class="type">void</span> *pv)</span> &#123;</span><br><span class="line">    <span class="comment">// 申请一个 DMA 通道</span></span><br><span class="line">    <span class="keyword">if</span> (xSemaphoreTake(g_dma_sem, pdMS_TO_TICKS(<span class="number">100</span>)) == pdTRUE) &#123;</span><br><span class="line">        UseDMA();</span><br><span class="line">        xSemaphoreGive(g_dma_sem); <span class="comment">// 用完归还</span></span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="comment">// 超时，三个通道都在忙</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>也适合”积累型”场景：ISR 每触发一次就给一个信号量，任务等到一定次数再处理。</p><hr><p><strong>互斥锁（Mutex）。</strong></p><p>看上去跟二值信号量一模一样——都是 Take&#x2F;Give。但互斥锁多了一个关键机制：<strong>优先级继承</strong>。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SemaphoreHandle_t <span class="title function_">xSemaphoreCreateMutex</span><span class="params">(<span class="type">void</span>)</span>;</span><br></pre></td></tr></table></figure><p>优先级继承是这样工作的：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">Task A (prio 1) 拿到 mutex    →  prio 1</span><br><span class="line">Task B (prio 3) 也想拿 mutex  →  prio 3 阻塞</span><br><span class="line">Task A 被临时提升到 prio 3     ← 这是优先级继承</span><br><span class="line">Task C (prio 2) 不会抢跑       ← 避免了优先级翻转</span><br><span class="line">Task A 释放 mutex，恢复 prio 1</span><br><span class="line">Task B 拿到 mutex</span><br></pre></td></tr></table></figure><p>如果没有继承机制，Task C（prio 2）会在 A 释放 mutex 之前抢跑，拖延 B 拿到锁的时间——这就是优先级翻转。</p><hr><p><strong>互斥锁和递归锁。</strong></p><p>标准互斥锁不能重入：一个任务已经持有它了，再 Take 一次会死锁。</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// ❌ 会死锁</span></span><br><span class="line"><span class="type">void</span> <span class="title function_">DoSomething</span><span class="params">(<span class="type">void</span>)</span> &#123;</span><br><span class="line">    xSemaphoreTake(mutex, portMAX_DELAY);</span><br><span class="line">    DoSomethingElse();  <span class="comment">// 里面又 Take 同一个 mutex</span></span><br><span class="line">    xSemaphoreGive(mutex);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如果确实需要递归（同一任务多次拿锁），用递归锁：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">SemaphoreHandle_t <span class="title function_">xSemaphoreCreateRecursiveMutex</span><span class="params">(<span class="type">void</span>)</span>;</span><br><span class="line">xSemaphoreTakeRecursive(mutex, portMAX_DELAY);    <span class="comment">// 可以多次调</span></span><br><span class="line">xSemaphoreGiveRecursive(mutex);                   <span class="comment">// 给几次拿几次必须对等</span></span><br></pre></td></tr></table></figure><p>递归锁的典型场景：一个模块的公有函数和私有函数都需要持锁，公有调私有时不会死锁。但尽量少用——需要递归锁通常意味着锁的粒度太大，该拆模块了。</p><hr><p>实际遇到的一次死锁。</p><p>系统里有一个 I2C 总线的 mutex。某天加了一个新功能：温度传感器任务持有 I2C mutex 去读温度，读数异常时调用日志模块打印告警，而日志模块内部也尝试拿 I2C mutex（因为日志输出到 OLED）。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">TempTask:  Take(I2C_mutex) → ReadTemp() → error → LogError() → Take(I2C_mutex) → 死锁</span><br></pre></td></tr></table></figure><p>解决方法不是换递归锁，而是把 I2C mutex 拆两层：底层驱动自己管理互斥，上层日志模块不需要知道总线的存在。锁的粒度越小，死锁概率越低。</p><p><code>configASSERT</code> 在排查时帮了大忙：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">define</span> configASSERT(x) <span class="keyword">if</span>(!(x)) &#123; taskDISABLE_INTERRUPTS(); for(;;); &#125;</span></span><br></pre></td></tr></table></figure><p>打开后，如果某个 API 返回了预期外的 <code>pdFALSE</code>，系统直接停住，调试器一看调用栈就知道死在哪。</p><hr><p><strong>信号量 vs 任务通知。</strong></p><p>这是 FreeRTOS 里一个常见的性能选择。任务通知能替代大部分二值信号量的场景，而且更快——通知直接操作 TCB 里的一个字段，不需要创建单独的内核对象。</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">// 任务通知版（比信号量快 3-5 倍）</span></span><br><span class="line">xTaskNotifyGive(handle);                  <span class="comment">// 发通知</span></span><br><span class="line">ulTaskNotifyTake(pdTRUE, portMAX_DELAY);  <span class="comment">// 等通知</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 信号量版</span></span><br><span class="line">xSemaphoreGive(sem);</span><br><span class="line">xSemaphoreTake(sem, portMAX_DELAY);</span><br></pre></td></tr></table></figure><p>但任务通知有几个限制：只能发给指定任务、不能广播、通知值是覆盖式的。当这些限制不构成问题时，直接用任务通知代替二值信号量。</p><hr><p><strong>什么时候用什么？</strong></p><ul><li>二值信号量：ISR → 任务同步，最简单</li><li>计数信号量：管理有限资源（DMA 通道、缓冲区槽位）</li><li>互斥锁：保护共享资源，需要优先级继承</li><li>递归锁：同一任务需多次拿锁，但尽量少用</li><li>任务通知：替代二值信号量，更快但有限制</li></ul>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;信号量和队列是亲戚——信号量本质上就是个不许传数据的队列。它不关心消息内容，只关心”有没有”。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;二值信号量（Binary Semaphore）。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;就像一个只能放一个令牌的盒子。任务调用 &lt;code&gt;xS</summary>
      
    
    
    
    <category term="学习笔记" scheme="https://w4ysonch.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="FreeRTOS" scheme="https://w4ysonch.github.io/tags/FreeRTOS/"/>
    
    <category term="RTOS" scheme="https://w4ysonch.github.io/tags/RTOS/"/>
    
    <category term="嵌入式" scheme="https://w4ysonch.github.io/tags/%E5%B5%8C%E5%85%A5%E5%BC%8F/"/>
    
  </entry>
  
  <entry>
    <title>FreeRTOS 学习笔记（二）：队列</title>
    <link href="https://w4ysonch.github.io/2025/08/12/freertos-queue/"/>
    <id>https://w4ysonch.github.io/2025/08/12/freertos-queue/</id>
    <published>2025-08-12T11:21:34.000Z</published>
    <updated>2026-06-29T07:50:23.717Z</updated>
    
    <content type="html"><![CDATA[<p>任务之间怎么传数据？最简单的办法是全局变量。但全局变量没有”阻塞等待”能力——消费者不知道数据什么时候准备好，只能轮询。</p><p>队列解决了这个问题：生产者往里面放，消费者从里面取。如果队列空了，消费者可以选择阻塞等待。</p><p>它本质上是个先进先出的缓冲区，但多了任务间同步的能力。</p><hr><p>创建队列：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">QueueHandle_t <span class="title function_">xQueueCreate</span><span class="params">(</span></span><br><span class="line"><span class="params">    UBaseType_t uxQueueLength,   <span class="comment">// 最多存几条消息</span></span></span><br><span class="line"><span class="params">    UBaseType_t uxItemSize       <span class="comment">// 每条消息多大</span></span></span><br><span class="line"><span class="params">)</span>;</span><br></pre></td></tr></table></figure><p>注意：<code>uxItemSize</code> 是<strong>每条</strong>消息的大小，不是总大小。队列的实际内存 &#x3D; <code>uxQueueLength * uxItemSize</code>，这块内存由 FreeRTOS 从堆上分配。</p><p>如果不想用堆，可以用 <code>xQueueCreateStatic()</code>，自己提供 <code>uint8_t</code> 缓冲区。</p><hr><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></pre></td><td class="code"><pre><span class="line">BaseType_t <span class="title function_">xQueueSend</span><span class="params">(</span></span><br><span class="line"><span class="params">    QueueHandle_t xQueue,</span></span><br><span class="line"><span class="params">    <span class="type">const</span> <span class="type">void</span> *  pvItemToQueue,</span></span><br><span class="line"><span class="params">    TickType_t    xTicksToWait   <span class="comment">// 满时最多等多久，0 = 不等，portMAX_DELAY = 死等</span></span></span><br><span class="line"><span class="params">)</span>;</span><br></pre></td></tr></table></figure><p>数据是<strong>拷贝进去</strong>的，不是传指针——队列把你传入的 <code>pvItemToQueue</code> 所指的内存内容 <code>memcpy</code> 到内部缓冲区。所以 <code>pvItemToQueue</code> 可以指向局部变量，不用担心作用域问题。</p><p>除了 <code>xQueueSend</code>，还有几个变体：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">xQueueSendToBack()   <span class="comment">// 跟 xQueueSend 一样，放队尾</span></span><br><span class="line">xQueueSendToFront()  <span class="comment">// 插队到队首（紧急消息用）</span></span><br><span class="line">xQueueOverwrite()    <span class="comment">// 覆盖式发送，即使队列满了也写（适合只有一条最新数据的场景）</span></span><br></pre></td></tr></table></figure><hr><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></pre></td><td class="code"><pre><span class="line">BaseType_t <span class="title function_">xQueueReceive</span><span class="params">(</span></span><br><span class="line"><span class="params">    QueueHandle_t xQueue,</span></span><br><span class="line"><span class="params">    <span class="type">void</span> *        pvBuffer,      <span class="comment">// 读出来的数据放这里</span></span></span><br><span class="line"><span class="params">    TickType_t    xTicksToWait   <span class="comment">// 空时最多等多久</span></span></span><br><span class="line"><span class="params">)</span>;</span><br></pre></td></tr></table></figure><p>读完后数据从队列里<strong>移除</strong>。如果想”只看不拿走”，用 <code>xQueuePeek()</code>：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">xQueuePeek(<span class="built_in">queue</span>, &amp;buf, timeout);  <span class="comment">// 看一眼，数据还在队列里</span></span><br></pre></td></tr></table></figure><hr><p>一个发一个收的例子：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 生产者任务：每 200ms 产生一个传感器数据</span></span><br><span class="line">QueueHandle_t g_sensor_queue;</span><br><span class="line"></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> &#123;</span></span><br><span class="line">    <span class="type">float</span> temperature;</span><br><span class="line">    <span class="type">int</span>   humidity;</span><br><span class="line">&#125; <span class="type">sensor_data_t</span>;</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">vSensorTask</span><span class="params">(<span class="type">void</span> *pv)</span> &#123;</span><br><span class="line">    <span class="type">sensor_data_t</span> data;</span><br><span class="line">    <span class="keyword">while</span> (<span class="number">1</span>) &#123;</span><br><span class="line">        data.temperature = <span class="number">25.0f</span> + (rand() % <span class="number">50</span>) * <span class="number">0.1f</span>;</span><br><span class="line">        data.humidity    = <span class="number">55</span> + rand() % <span class="number">20</span>;</span><br><span class="line">        xQueueSend(g_sensor_queue, &amp;data, portMAX_DELAY);</span><br><span class="line">        vTaskDelay(pdMS_TO_TICKS(<span class="number">200</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">void</span> <span class="title function_">vDisplayTask</span><span class="params">(<span class="type">void</span> *pv)</span> &#123;</span><br><span class="line">    <span class="type">sensor_data_t</span> data;</span><br><span class="line">    <span class="keyword">while</span> (<span class="number">1</span>) &#123;</span><br><span class="line">        <span class="keyword">if</span> (xQueueReceive(g_sensor_queue, &amp;data, portMAX_DELAY) == pdTRUE) &#123;</span><br><span class="line">            <span class="built_in">printf</span>(<span class="string">&quot;温度: %.1f°C  湿度: %d%%\r\n&quot;</span>,</span><br><span class="line">                   data.temperature, data.humidity);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">(<span class="type">void</span>)</span> &#123;</span><br><span class="line">    g_sensor_queue = xQueueCreate(<span class="number">10</span>, <span class="keyword">sizeof</span>(<span class="type">sensor_data_t</span>));</span><br><span class="line">    xTaskCreate(vSensorTask,  <span class="string">&quot;Sensor&quot;</span>,  <span class="number">256</span>, <span class="literal">NULL</span>, <span class="number">2</span>, <span class="literal">NULL</span>);</span><br><span class="line">    xTaskCreate(vDisplayTask, <span class="string">&quot;Display&quot;</span>, <span class="number">256</span>, <span class="literal">NULL</span>, <span class="number">1</span>, <span class="literal">NULL</span>);</span><br><span class="line">    vTaskStartScheduler();</span><br><span class="line">    <span class="keyword">while</span> (<span class="number">1</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>队列里有 10 个坑位，生产速度 200ms，消费能力足够快就不会满。</p><hr><p><strong>队列满了怎么办？</strong></p><p>取决于业务：</p><ul><li>如果旧数据没意义（传感器读数），用 <code>xQueueOverwrite()</code>，只保留最新的</li><li>如果数据不能丢（日志、命令），增加队列长度或者提高消费者优先级</li><li>如果偶尔丢几帧可以接受，用 <code>xQueueSend()</code> + 超时 0，满了直接返回 <code>errQUEUE_FULL</code>，跳过这次发送</li></ul><hr><p><strong>ISR 里发队列的坑。</strong></p><p>在中断服务函数里不能调 <code>xQueueSend</code>——因为它可能阻塞，而 ISR 里不能阻塞。必须用 FromISR 版本：</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">BaseType_t <span class="title function_">xQueueSendFromISR</span><span class="params">(</span></span><br><span class="line"><span class="params">    QueueHandle_t xQueue,</span></span><br><span class="line"><span class="params">    <span class="type">const</span> <span class="type">void</span> *  pvItemToQueue,</span></span><br><span class="line"><span class="params">    BaseType_t *  pxHigherPriorityTaskWoken  <span class="comment">// 关键参数</span></span></span><br><span class="line"><span class="params">)</span>;</span><br></pre></td></tr></table></figure><p><code>pxHigherPriorityTaskWoken</code> 是个标志位——如果发送后唤醒了一个更高优先级的任务，它会被设为 <code>pdTRUE</code>。然后你在 ISR 末尾调用 <code>portYIELD_FROM_ISR()</code> 触发一次上下文切换：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// UART 接收中断</span></span><br><span class="line"><span class="type">void</span> <span class="title function_">UART_Rx_IRQHandler</span><span class="params">(<span class="type">void</span>)</span> &#123;</span><br><span class="line">    BaseType_t xHigherPriorityTaskWoken = pdFALSE;</span><br><span class="line">    <span class="type">uint8_t</span> byte = UART-&gt;DR;</span><br><span class="line"></span><br><span class="line">    xQueueSendFromISR(g_uart_queue, &amp;byte, &amp;xHigherPriorityTaskWoken);</span><br><span class="line"></span><br><span class="line">    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>忘了检查这个标志位，结果是：消息发出去了，但消费者要等到下一个 tick 才会被调度——延迟一个 tick（1ms~10ms），对高实时场景可能刚好超时。</p><hr><p><strong>队列 vs 全局数组的实测对比。</strong></p><p>在 STM32F407（168MHz）上做了个简单对比，单字节消息、发 10000 次：</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></pre></td><td class="code"><pre><span class="line">全局数组（轮询）:  平均 0.3μs/次，无阻塞能力，消费者忙等</span><br><span class="line">队列（1个坑位）:   平均 2.1μs/次，支持阻塞等待</span><br><span class="line">队列（32个坑位）:  平均 2.8μs/次，队列越长开销越大（拷贝 + 索引计算）</span><br></pre></td></tr></table></figure><p>队列慢了一个数量级，但这几微秒换来了阻塞等待能力和任务解耦——值不值取决于场景。传感器数据轮询够用了，网络协议栈就必须上队列。</p><p>另外队列创建时的 <code>uxItemSize</code> 越小越好。大结构体优先传指针：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ❌ 大结构体拷贝</span></span><br><span class="line">xQueueCreate(<span class="number">8</span>, <span class="keyword">sizeof</span>(<span class="type">net_packet_t</span>));  <span class="comment">// 一条消息 516 字节</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// ✅ 只传指针</span></span><br><span class="line">xQueueCreate(<span class="number">8</span>, <span class="keyword">sizeof</span>(<span class="type">net_packet_t</span> *));  <span class="comment">// 一条消息 4 字节</span></span><br></pre></td></tr></table></figure><p>传指针的话要自己管理 <code>net_packet_t</code> 的生命周期——被消费之前不能释放。通常用内存池配合队列，消费者取走指针、用完归还。</p><hr><p><strong>阻塞超时的小细节。</strong></p><p><code>portMAX_DELAY</code> 的意思是”等到天荒地老”。但如果用了 <code>vTaskSuspendAll()</code> 关了调度器，即使队列里有数据也不会被唤醒——调度器关了，任务切换不生效。</p><p>所以调试时如果发现任务卡在某个 <code>portMAX_DELAY</code> 上永远不动了，先检查是不是哪里关了调度器忘了开。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;任务之间怎么传数据？最简单的办法是全局变量。但全局变量没有”阻塞等待”能力——消费者不知道数据什么时候准备好，只能轮询。&lt;/p&gt;
&lt;p&gt;队列解决了这个问题：生产者往里面放，消费者从里面取。如果队列空了，消费者可以选择阻塞等待。&lt;/p&gt;
&lt;p&gt;它本质上是个先进先出的缓冲区，但</summary>
      
    
    
    
    <category term="学习笔记" scheme="https://w4ysonch.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="FreeRTOS" scheme="https://w4ysonch.github.io/tags/FreeRTOS/"/>
    
    <category term="RTOS" scheme="https://w4ysonch.github.io/tags/RTOS/"/>
    
    <category term="嵌入式" scheme="https://w4ysonch.github.io/tags/%E5%B5%8C%E5%85%A5%E5%BC%8F/"/>
    
  </entry>
  
  <entry>
    <title>FreeRTOS 学习笔记（一）：任务管理</title>
    <link href="https://w4ysonch.github.io/2025/08/10/freertos-task-management/"/>
    <id>https://w4ysonch.github.io/2025/08/10/freertos-task-management/</id>
    <published>2025-08-10T14:00:00.000Z</published>
    <updated>2026-06-29T07:50:23.717Z</updated>
    
    <content type="html"><![CDATA[<p>一个 FreeRTOS 任务就是一个永不返回的 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></pre></td><td class="code"><pre><span class="line"><span class="type">void</span> <span class="title function_">MyTask</span><span class="params">(<span class="type">void</span> *pvParameters)</span> &#123;</span><br><span class="line">    <span class="keyword">while</span> (<span class="number">1</span>) &#123;</span><br><span class="line">        <span class="comment">// 干点什么</span></span><br><span class="line">        vTaskDelay(pdMS_TO_TICKS(<span class="number">100</span>));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>任务在五种状态之间切换：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><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></pre></td><td class="code"><pre><span class="line">             xTaskCreate()</span><br><span class="line">                  │</span><br><span class="line">                  ▼</span><br><span class="line">┌──────────── 就绪态 ◄────────────────┐</span><br><span class="line">│                │                    │</span><br><span class="line">│          调度器选中               时间片到/被抢占</span><br><span class="line">│                │                    │</span><br><span class="line">│                ▼                    │</span><br><span class="line">│         运行态 ──────────────────────┘</span><br><span class="line">│                │</span><br><span class="line">│   vTaskDelay() │  │ 等队列/信号量</span><br><span class="line">│                │</span><br><span class="line">│                ▼</span><br><span class="line">│         阻塞态 ─── 事件到达/超时</span><br><span class="line">│                │</span><br><span class="line">└────────────────┘</span><br><span class="line">                  </span><br><span class="line">          vTaskSuspend()</span><br><span class="line">                 │</span><br><span class="line">                 ▼</span><br><span class="line">           挂起态 (只能被别的任务 vTaskResume() 拉回来)</span><br></pre></td></tr></table></figure><p>调度器决定下一个跑谁。FreeRTOS 默认<strong>抢占式 + 时间片轮转</strong>：高优先级就绪立刻抢占，同优先级轮流执行。</p><p>以下情况不会发生任务切换：</p><ol><li>在临界区里（<code>taskENTER_CRITICAL()</code> … <code>taskEXIT_CRITICAL()</code>）</li><li>关了调度器（<code>vTaskSuspendAll()</code> … <code>xTaskResumeAll()</code>）</li><li>正在 ISR 里（中断退出时才切）</li></ol><p>这三个是调 FreeRTOS 时”我的任务为什么没跑”的标准答案。</p><hr><p>创建任务：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">BaseType_t <span class="title function_">xTaskCreate</span><span class="params">(</span></span><br><span class="line"><span class="params">    TaskFunction_t   pvTaskCode,       <span class="comment">// 函数指针</span></span></span><br><span class="line"><span class="params">    <span class="type">const</span> <span class="type">char</span> *     pcName,           <span class="comment">// 调试用，别太长</span></span></span><br><span class="line"><span class="params">    configSTACK_DEPTH_TYPE usStackDepth, <span class="comment">// 堆栈，单位是 word 不是 byte</span></span></span><br><span class="line"><span class="params">    <span class="type">void</span> *           pvParameters,     <span class="comment">// 传参</span></span></span><br><span class="line"><span class="params">    UBaseType_t      uxPriority,       <span class="comment">// 数字越大越高</span></span></span><br><span class="line"><span class="params">    TaskHandle_t *   pxCreatedTask     <span class="comment">// 句柄</span></span></span><br><span class="line"><span class="params">)</span>;</span><br></pre></td></tr></table></figure><p>删任务：<code>vTaskDelete(NULL)</code> 删自己，idle 任务会回收 TCB 和堆栈。</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></pre></td><td class="code"><pre><span class="line"><span class="type">void</span> <span class="title function_">vTaskDelay</span><span class="params">(TickType_t xTicksToDelay)</span>;                    <span class="comment">// 相对延时</span></span><br><span class="line"><span class="type">void</span> <span class="title function_">vTaskDelayUntil</span><span class="params">(TickType_t *pxWakeTime, TickType_t inc)</span>; <span class="comment">// 绝对延时</span></span><br></pre></td></tr></table></figure><p><code>vTaskDelay(100ms)</code>：从现在起等 100ms。但任务自己跑了 3ms 才调它，实际间隔就是 103ms。</p><p><code>vTaskDelayUntil(&amp;lastWake, 100ms)</code>：以上次醒来为基准加 100ms，亏掉的时间下次补回来。</p><p>需要固定频率执行的场合（10ms 读一次传感器、20ms 刷一次屏），必须用 <code>vTaskDelayUntil</code>。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ❌ 实际周期 = 10ms + 执行时间</span></span><br><span class="line"><span class="keyword">while</span> (<span class="number">1</span>) &#123;</span><br><span class="line">    ReadSensor();</span><br><span class="line">    vTaskDelay(pdMS_TO_TICKS(<span class="number">10</span>));</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ✅ 严格 10ms</span></span><br><span class="line">TickType_t last = xTaskGetTickCount();</span><br><span class="line"><span class="keyword">while</span> (<span class="number">1</span>) &#123;</span><br><span class="line">    ReadSensor();</span><br><span class="line">    vTaskDelayUntil(&amp;last, pdMS_TO_TICKS(<span class="number">10</span>));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>其他 API：</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">vTaskPrioritySet(handle, prio);      <span class="comment">// 改优先级</span></span><br><span class="line">uxTaskPriorityGet(handle);           <span class="comment">// 查优先级</span></span><br><span class="line">vTaskSuspend(handle);                <span class="comment">// 挂起</span></span><br><span class="line">vTaskResume(handle);                 <span class="comment">// 恢复（任务上下文）</span></span><br><span class="line">xTaskResumeFromISR(handle);          <span class="comment">// 恢复（ISR 中）</span></span><br><span class="line">xTaskGetTickCount();                 <span class="comment">// 启动以来的 tick 数</span></span><br><span class="line">uxTaskGetNumberOfTasks();            <span class="comment">// 当前任务数</span></span><br><span class="line">pcTaskGetName(handle);               <span class="comment">// 任务名（调试用）</span></span><br><span class="line">vTaskSuspendAll();                   <span class="comment">// 关调度（ISR 仍可触发）</span></span><br><span class="line">xTaskResumeAll();                    <span class="comment">// 开调度</span></span><br></pre></td></tr></table></figure><hr><p>下面是一个跑得通的三任务例子：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;FreeRTOS.h&quot;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;task.h&quot;</span></span></span><br><span class="line"></span><br><span class="line"><span class="comment">// LED 闪烁，500ms 一次</span></span><br><span class="line"><span class="type">void</span> <span class="title function_">vLEDTask</span><span class="params">(<span class="type">void</span> *pv)</span> &#123;</span><br><span class="line">    <span class="keyword">while</span> (<span class="number">1</span>) &#123;</span><br><span class="line">        HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);</span><br><span class="line">        vTaskDelay(pdMS_TO_TICKS(<span class="number">500</span>));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 串口打印，严格 1 秒周期</span></span><br><span class="line"><span class="type">void</span> <span class="title function_">vLogTask</span><span class="params">(<span class="type">void</span> *pv)</span> &#123;</span><br><span class="line">    TickType_t last = xTaskGetTickCount();</span><br><span class="line">    <span class="keyword">while</span> (<span class="number">1</span>) &#123;</span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">&quot;Uptime: %lu ms\r\n&quot;</span>,</span><br><span class="line">               xTaskGetTickCount() * portTICK_PERIOD_MS);</span><br><span class="line">        vTaskDelayUntil(&amp;last, pdMS_TO_TICKS(<span class="number">1000</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">// 按键扫描，20ms 轮询</span></span><br><span class="line"><span class="type">void</span> <span class="title function_">vButtonTask</span><span class="params">(<span class="type">void</span> *pv)</span> &#123;</span><br><span class="line">    <span class="keyword">while</span> (<span class="number">1</span>) &#123;</span><br><span class="line">        <span class="keyword">if</span> (HAL_GPIO_ReadPin(BUTTON_GPIO_Port, BUTTON_Pin)) &#123;</span><br><span class="line">            <span class="built_in">printf</span>(<span class="string">&quot;Button pressed!\r\n&quot;</span>);</span><br><span class="line">            vTaskDelay(pdMS_TO_TICKS(<span class="number">200</span>)); <span class="comment">// 去抖</span></span><br><span class="line">        &#125;</span><br><span class="line">        vTaskDelay(pdMS_TO_TICKS(<span class="number">20</span>));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">(<span class="type">void</span>)</span> &#123;</span><br><span class="line">    HAL_Init();</span><br><span class="line">    SystemClock_Config();</span><br><span class="line"></span><br><span class="line">    xTaskCreate(vLEDTask,    <span class="string">&quot;LED&quot;</span>,    <span class="number">128</span>, <span class="literal">NULL</span>, <span class="number">1</span>, <span class="literal">NULL</span>);</span><br><span class="line">    xTaskCreate(vLogTask,    <span class="string">&quot;Logger&quot;</span>, <span class="number">256</span>, <span class="literal">NULL</span>, <span class="number">2</span>, <span class="literal">NULL</span>);</span><br><span class="line">    xTaskCreate(vButtonTask, <span class="string">&quot;Button&quot;</span>, <span class="number">128</span>, <span class="literal">NULL</span>, <span class="number">3</span>, <span class="literal">NULL</span>);</span><br><span class="line"></span><br><span class="line">    vTaskStartScheduler();</span><br><span class="line">    <span class="keyword">while</span> (<span class="number">1</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>优先级 Button(3) &gt; Logger(2) &gt; LED(1)，按键总能最快响应。</p><p>如果用 CMSIS-RTOS v2（STM32CubeMX 默认生成），底层也是 <code>xTaskCreate</code>，区别是堆栈单位变成 byte：</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></pre></td><td class="code"><pre><span class="line"><span class="type">const</span> osThreadAttr_t attr = &#123;</span><br><span class="line">    .name       = <span class="string">&quot;LED&quot;</span>,</span><br><span class="line">    .stack_size = <span class="number">512</span>,  <span class="comment">// 注意：byte，不是 word</span></span><br><span class="line">    .priority   = osPriorityNormal,</span><br><span class="line">&#125;;</span><br><span class="line">osThreadNew(vLEDTask, <span class="literal">NULL</span>, &amp;attr);</span><br></pre></td></tr></table></figure><hr><p><strong>堆栈怎么估算？</strong></p><p>新手的噩梦。没公式，土办法：</p><ol><li>先设大（比如 512 words）</li><li>跑几个小时后看 <code>uxTaskGetStackHighWaterMark(handle)</code>，返回剩余堆栈</li><li>实际用量 ≈ 配置值 - high water mark，留 1.5x 余量</li></ol><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 空闲钩子里每 60 秒打印一次堆栈使用</span></span><br><span class="line"><span class="type">void</span> <span class="title function_">vApplicationIdleHook</span><span class="params">(<span class="type">void</span>)</span> &#123;</span><br><span class="line">    <span class="type">static</span> <span class="type">uint32_t</span> count = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">if</span> (++count % <span class="number">60000</span> == <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">&quot;LED stack free: %lu words\r\n&quot;</span>,</span><br><span class="line">               uxTaskGetStackHighWaterMark(xLEDHandle));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>另外 <code>FreeRTOSConfig.h</code> 里打开溢出检测：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">define</span> configCHECK_FOR_STACK_OVERFLOW 2</span></span><br></pre></td></tr></table></figure><p>方案 2 在创建任务时用 <code>0xA5</code> 填满堆栈，溢出时 canary 被破坏，下次切换时检测到。开销极小。</p><hr><p><strong>空闲任务能干什么？</strong></p><p><code>vTaskStartScheduler()</code> 自动建了个优先级 0 的空闲任务。它只做一件事：回收被删除任务的 TCB 和堆栈。</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></pre></td><td class="code"><pre><span class="line"><span class="type">void</span> <span class="title function_">vApplicationIdleHook</span><span class="params">(<span class="type">void</span>)</span> &#123;</span><br><span class="line">    <span class="comment">// ✅ 低功耗：配合 configUSE_TICKLESS_IDLE 进入 sleep</span></span><br><span class="line">    <span class="comment">// ✅ 喂狗：前提是保证所有任务阻塞后 idle 能及时喂</span></span><br><span class="line">    <span class="comment">// ✅ 调试：打印堆栈使用量</span></span><br><span class="line">    <span class="comment">// ✅ 性能：递增一个计数器，vTaskGetRunTimeStats() 可以看到 idle 跑了多少时间</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>不能干的事：</strong> 调任何会阻塞的 API——<code>vTaskDelay</code>、<code>xQueueReceive</code>、<code>xSemaphoreTake</code>——idle 是系统最后的救命稻草，它被阻塞系统就挂了。</p><hr><p><strong>不要靠调优先级修 bug。</strong></p><p>刚上手容易犯的错：任务不够及时 → 优先级 +1 → 另一个任务又不够了 → 再 +1 → 所有任务都在高优先级打架，跟没上 RTOS 一样。</p><p>任务没按时跑，先排查：是不是临界区太长关了中断？是不是有高优先级任务一直在跑没 block？是不是 configTICK_RATE_HZ 太低分辨率不够？优先级是最后的调整手段。</p><hr><p><strong>configTICK_RATE_HZ 怎么选？</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">1000Hz → 1ms tick   → 实时性好，功耗高（每秒 1000 次 SysTick 中断）</span><br><span class="line">100Hz  → 10ms tick  → 功耗低，vTaskDelay 最小分辨率 10ms</span><br></pre></td></tr></table></figure><p>电池供电的设备降到 100Hz 省电很明显。大多数应用不需要 1ms 精度。选好之后别忘了 <code>pdMS_TO_TICKS()</code> 会自动换算，代码不用改。</p><hr>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;一个 FreeRTOS 任务就是一个永不返回的 C 函数：&lt;/p&gt;
&lt;figure class=&quot;highlight c&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span cla</summary>
      
    
    
    
    <category term="学习笔记" scheme="https://w4ysonch.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="FreeRTOS" scheme="https://w4ysonch.github.io/tags/FreeRTOS/"/>
    
    <category term="RTOS" scheme="https://w4ysonch.github.io/tags/RTOS/"/>
    
    <category term="嵌入式" scheme="https://w4ysonch.github.io/tags/%E5%B5%8C%E5%85%A5%E5%BC%8F/"/>
    
  </entry>
  
</feed>
