<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>电池供电 on Tech Snippets - 嵌入式技术笔记</title>
    <link>https://tech-snippets.xyz/tags/%E7%94%B5%E6%B1%A0%E4%BE%9B%E7%94%B5/</link>
    <description>Recent content in 电池供电 on Tech Snippets - 嵌入式技术笔记</description>
    <generator>Hugo</generator>
    <language>zh-cn</language>
    <lastBuildDate>Tue, 19 May 2026 19:00:00 +0800</lastBuildDate>
    <atom:link href="https://tech-snippets.xyz/tags/%E7%94%B5%E6%B1%A0%E4%BE%9B%E7%94%B5/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>ESP32 低功耗设计与深度睡眠模式实战指南</title>
      <link>https://tech-snippets.xyz/posts/esp32-low-power-deep-sleep-guide/</link>
      <pubDate>Tue, 19 May 2026 19:00:00 +0800</pubDate>
      <guid>https://tech-snippets.xyz/posts/esp32-low-power-deep-sleep-guide/</guid>
      <description>前言 在物联网（IoT）时代，电池供电的设备随处可见——从温湿度传感器到智能门锁，从环境监测节点到可穿戴设备，这些设备都有一个共同的核心需求：在保证功能的前提下，尽可能延长电池寿命。对于使用两节 AA 电池供电的设备，理想情况下应该能工作一年甚至更久，而这对系统的功耗控制提出了极高的要求。
ESP32 作为物联网领域最受欢迎的芯片之一，凭借其集成的 WiFi、蓝牙、强大的处理能力和丰富的外设，获得了广泛的应用。然而，如果不进行合理的功耗优化，ESP32 在正常工作模式下的电流消耗可达几十甚至上百毫安，两节 AA 电池可能短短几天就耗尽了。幸运的是，ESP32 设计了完善的电源管理系统，提供了多种低功耗模式，其中**深度睡眠模式（Deep Sleep）**的电流消耗可以降至微安级别，这使得电池供电的 ESP32 设备能够实现数年的续航时间。
我第一次真正意识到低功耗设计的重要性，是在 2021 年的一个农业物联网项目中。当时我们在田间部署了 50 多个 ESP32 土壤湿度监测节点，最初的版本没有进行功耗优化，每个节点使用 3.7V 2000mAh 的锂电池，结果不到两周就需要更换一次电池。这对于部署在野外的设备来说是完全不可接受的——每隔两周开车去田间给 50 个节点换电池，人工成本和时间成本都高得惊人。
后来我们重新设计了固件，引入了深度睡眠模式，让节点大部分时间处于休眠状态，只在需要采集数据和上传时唤醒。优化后的节点平均电流从原来的 40mA 降到了不到 20μA，电池寿命从两周延长到了超过两年！这个经历让我深刻认识到：低功耗设计不是锦上添花的功能，而是电池供电设备的生命线。
然而，ESP32 的低功耗设计并不像想象中那么简单。仅仅调用 esp_deep_sleep_start() 是远远不够的——你需要了解各种唤醒源的特性、正确处理 RTC 存储器、注意 GPIO 的状态配置、谨慎使用外设，还要进行精确的功耗测量和调试。很多开发者在初次尝试低功耗设计时，往往会遇到各种问题：休眠电流下不来、唤醒不正常、数据丢失等等。
本文将系统地讲解 ESP32 的电源管理架构和低功耗设计方法。我们会深入分析各种低功耗模式的工作原理、详细介绍 RTC 域的组件和唤醒源、讨论 GPIO 配置和外设使用的注意事项、提供完整的代码示例和调试技巧，最后通过一个实战项目演示如何设计一个真正低功耗的传感器节点。无论你是正在开发电池供电 IoT 设备的工程师，还是对低功耗设计感兴趣的爱好者，这篇文章都能帮你掌握 ESP32 低功耗设计的核心要点。
一、ESP32 的电源管理架构 要理解 ESP32 的低功耗模式，首先需要了解它的电源管理架构。ESP32 采用了域隔离的设计思想，整个芯片被划分为两个主要的电源域：RTC 域（RTC Domain）和数字域（Digital Domain）。这种设计使得不需要的电源域可以被完全关闭，从而最大限度地降低功耗。
1.1 两个电源域 **数字域（Digital Domain）**包含了 ESP32 的主要计算和通信组件：
两个 Xtensa LX6 CPU 核心（PRO_CPU 和 APP_CPU） 大部分外设（SPI、I2C、UART、SDIO、Ethernet MAC 等） WiFi 和蓝牙基带与射频模块 448KB 的片上 ROM 和 520KB 的片上 SRAM 闪存和 PSRAM 接口 在正常工作模式下，数字域的电流消耗通常在 20-80mA 之间，开启 WiFi 发送时甚至可以超过 200mA。</description>
      <content:encoded><![CDATA[<h2 id="前言">前言</h2>
<p>在物联网（IoT）时代，电池供电的设备随处可见——从温湿度传感器到智能门锁，从环境监测节点到可穿戴设备，这些设备都有一个共同的核心需求：<strong>在保证功能的前提下，尽可能延长电池寿命</strong>。对于使用两节 AA 电池供电的设备，理想情况下应该能工作一年甚至更久，而这对系统的功耗控制提出了极高的要求。</p>
<p>ESP32 作为物联网领域最受欢迎的芯片之一，凭借其集成的 WiFi、蓝牙、强大的处理能力和丰富的外设，获得了广泛的应用。然而，如果不进行合理的功耗优化，ESP32 在正常工作模式下的电流消耗可达几十甚至上百毫安，两节 AA 电池可能短短几天就耗尽了。幸运的是，ESP32 设计了完善的电源管理系统，提供了多种低功耗模式，其中**深度睡眠模式（Deep Sleep）**的电流消耗可以降至微安级别，这使得电池供电的 ESP32 设备能够实现数年的续航时间。</p>
<p><img alt="ESP32 功耗模式概览" loading="lazy" src="/images/esp32-power-modes-overview.svg"></p>
<p>我第一次真正意识到低功耗设计的重要性，是在 2021 年的一个农业物联网项目中。当时我们在田间部署了 50 多个 ESP32 土壤湿度监测节点，最初的版本没有进行功耗优化，每个节点使用 3.7V 2000mAh 的锂电池，结果不到两周就需要更换一次电池。这对于部署在野外的设备来说是完全不可接受的——每隔两周开车去田间给 50 个节点换电池，人工成本和时间成本都高得惊人。</p>
<p>后来我们重新设计了固件，引入了深度睡眠模式，让节点大部分时间处于休眠状态，只在需要采集数据和上传时唤醒。优化后的节点平均电流从原来的 40mA 降到了不到 20μA，电池寿命从两周延长到了超过两年！这个经历让我深刻认识到：低功耗设计不是锦上添花的功能，而是电池供电设备的生命线。</p>
<p>然而，ESP32 的低功耗设计并不像想象中那么简单。仅仅调用 <code>esp_deep_sleep_start()</code> 是远远不够的——你需要了解各种唤醒源的特性、正确处理 RTC 存储器、注意 GPIO 的状态配置、谨慎使用外设，还要进行精确的功耗测量和调试。很多开发者在初次尝试低功耗设计时，往往会遇到各种问题：休眠电流下不来、唤醒不正常、数据丢失等等。</p>
<p>本文将系统地讲解 ESP32 的电源管理架构和低功耗设计方法。我们会深入分析各种低功耗模式的工作原理、详细介绍 RTC 域的组件和唤醒源、讨论 GPIO 配置和外设使用的注意事项、提供完整的代码示例和调试技巧，最后通过一个实战项目演示如何设计一个真正低功耗的传感器节点。无论你是正在开发电池供电 IoT 设备的工程师，还是对低功耗设计感兴趣的爱好者，这篇文章都能帮你掌握 ESP32 低功耗设计的核心要点。</p>
<h2 id="一esp32-的电源管理架构">一、ESP32 的电源管理架构</h2>
<p>要理解 ESP32 的低功耗模式，首先需要了解它的电源管理架构。ESP32 采用了<strong>域隔离</strong>的设计思想，整个芯片被划分为两个主要的电源域：<strong>RTC 域（RTC Domain）<strong>和</strong>数字域（Digital Domain）</strong>。这种设计使得不需要的电源域可以被完全关闭，从而最大限度地降低功耗。</p>
<h3 id="11-两个电源域">1.1 两个电源域</h3>
<p>**数字域（Digital Domain）**包含了 ESP32 的主要计算和通信组件：</p>
<ul>
<li>两个 Xtensa LX6 CPU 核心（PRO_CPU 和 APP_CPU）</li>
<li>大部分外设（SPI、I2C、UART、SDIO、Ethernet MAC 等）</li>
<li>WiFi 和蓝牙基带与射频模块</li>
<li>448KB 的片上 ROM 和 520KB 的片上 SRAM</li>
<li>闪存和 PSRAM 接口</li>
</ul>
<p>在正常工作模式下，数字域的电流消耗通常在 20-80mA 之间，开启 WiFi 发送时甚至可以超过 200mA。</p>
<p>**RTC 域（RTC Domain）**是一个独立的、始终通电的电源域，包含：</p>
<ul>
<li>RTC 控制器和定时器</li>
<li>8KB 的 RTC 慢速存储器（RTC_SLOW_MEM）</li>
<li>8KB 的 RTC 快速存储器（RTC_FAST_MEM）</li>
<li>RTC GPIO（RTCIO）控制器</li>
<li>ULP（Ultra-Low Power）协处理器</li>
<li>触摸传感器控制器</li>
<li>ADC 和 DAC 控制器</li>
<li>温度传感器</li>
<li>晶振和时钟管理电路</li>
</ul>
<p>RTC 域的电流消耗非常低，在深度睡眠模式下只有 RTC 域保持通电时，整个芯片的电流可以降至 10μA 以下。</p>
<h3 id="12-五种电源模式">1.2 五种电源模式</h3>
<p>ESP32 提供了五种不同的电源模式，从高功耗到低功耗依次是：</p>
<table>
<thead>
<tr>
<th>模式</th>
<th>电源域状态</th>
<th>典型电流</th>
<th>唤醒延迟</th>
<th>适用场景</th>
</tr>
</thead>
<tbody>
<tr>
<td>活动模式（Active）</td>
<td>数字域 + RTC 域全开</td>
<td>80-200mA</td>
<td>0ms</td>
<td>正常计算和通信</td>
</tr>
<tr>
<td>调制解调器睡眠（Modem Sleep）</td>
<td>CPU 运行，关闭 WiFi/BT 射频</td>
<td>20-30mA</td>
<td>短</td>
<td>需要保持 CPU 运行但无需通信</td>
</tr>
<tr>
<td>轻度睡眠（Light Sleep）</td>
<td>CPU 暂停，数字域保持供电</td>
<td>0.8-1.5mA</td>
<td>~1ms</td>
<td>需要快速唤醒的场景</td>
</tr>
<tr>
<td>深度睡眠（Deep Sleep）</td>
<td>关闭数字域，仅 RTC 域运行</td>
<td>10-150μA</td>
<td>~10ms</td>
<td>大多数低功耗应用</td>
</tr>
<tr>
<td>休眠模式（Hibernation）</td>
<td>仅 RTC 定时器和部分 RTC GPIO 工作</td>
<td>5μA 以下</td>
<td>~100ms</td>
<td>极致低功耗需求</td>
</tr>
</tbody>
</table>
<p>让我们逐一分析这些模式的特点和适用场景。</p>
<p><strong>活动模式</strong>是我们最熟悉的模式，也是功耗最高的模式。在这个模式下，所有电源域都处于通电状态，两个 CPU 核心可以全速运行，所有外设都可以使用，WiFi 和蓝牙可以正常工作。这是默认的工作模式，但在电池供电设备中，应该尽可能减少在这个模式下停留的时间。</p>
<p><strong>调制解调器睡眠</strong>模式下，CPU 仍然正常运行，但 WiFi 和蓝牙的射频模块被关闭了。这对于那些只需要周期性传输数据、大部分时间只做本地计算的应用非常有用。比如一个数据记录仪，大部分时间在采集和处理传感器数据，每隔几分钟才上传一次，那么在不上传的时候就可以使用调制解调器睡眠模式。</p>
<p><strong>轻度睡眠</strong>模式更进一步，CPU 核心会被暂停（时钟门控），但数字域的电源仍然保持，SRAM 中的数据不会丢失。这种模式的唤醒速度非常快（通常在 1 毫秒以内），适合需要频繁唤醒且对延迟敏感的应用。比如键盘扫描、按键检测等应用，可以在两次扫描之间进入轻度睡眠。</p>
<p><strong>深度睡眠</strong>模式是最常用的低功耗模式。在这个模式下，整个数字域被完全断电，CPU、外设、WiFi/BT、SRAM 全部停止工作，只有 RTC 域保持运行。唤醒后系统会重新启动，相当于一次软复位。这种模式的功耗非常低，同时唤醒延迟也在可接受范围内，是绝大多数电池供电 IoT 设备的最佳选择。</p>
<p><strong>休眠模式</strong>是功耗最低的模式，甚至 RTC 域的大部分组件也会被关闭，只有 RTC 定时器和少数几个 RTC GPIO 保持工作。这种模式的功耗可以降到 5μA 以下，但可用的唤醒源非常有限，而且唤醒延迟很长，只有在对功耗要求极其苛刻的场景下才会使用。</p>
<h3 id="13-为什么深度睡眠是最佳选择">1.3 为什么深度睡眠是最佳选择？</h3>
<p>在实际项目中，90% 以上的低功耗应用都会选择深度睡眠模式，这是因为它在功耗、唤醒延迟和功能灵活性之间取得了最佳平衡：</p>
<ol>
<li>
<p><strong>功耗足够低</strong>：10-150μA 的休眠电流（取决于唤醒源配置）对于大多数电池供电应用来说已经足够优秀。以 10μA 计算，一节 2000mAh 的电池理论上可以休眠 200000 小时，也就是超过 22 年！</p>
</li>
<li>
<p><strong>唤醒源丰富</strong>：深度睡眠模式支持定时器、外部中断、触摸传感器、ULP 协处理器等多种唤醒方式，可以满足绝大多数应用场景的需求。</p>
</li>
<li>
<p><strong>数据保留机制</strong>：虽然主 SRAM 会断电，但 RTC 存储器（共 16KB）在深度睡眠期间仍然保持供电，可以用来保存需要跨唤醒周期的数据。</p>
</li>
<li>
<p><strong>唤醒延迟可接受</strong>：约 10ms 的唤醒延迟对于大多数物联网应用来说完全不是问题——毕竟你的传感器可能几分钟甚至几小时才需要唤醒一次。</p>
</li>
<li>
<p><strong>简化软件设计</strong>：每次唤醒都是一次干净的启动，可以避免很多内存泄漏和资源管理问题，软件架构反而更简单可靠。</p>
</li>
</ol>
<p>当然，深度睡眠也不是没有代价的。最主要的代价就是：<strong>每次唤醒后系统需要重新初始化</strong>，这包括外设初始化、WiFi 连接、时钟校准等，这些操作都需要消耗时间和能量。因此，唤醒频率不能太高——如果你的应用需要每秒唤醒几十次，那么深度睡眠的节能效果就会被频繁的初始化开销抵消，这时可能轻度睡眠更合适。</p>
<p>一个好的经验法则是：如果唤醒间隔超过 10 秒，深度睡眠通常是更优的选择；如果唤醒间隔在 1-10 秒之间，需要具体计算两种模式的能耗；如果唤醒间隔小于 1 秒，轻度睡眠可能更好。</p>
<h2 id="二深度睡眠模式的唤醒源详解">二、深度睡眠模式的唤醒源详解</h2>
<p>ESP32 深度睡眠模式支持多种唤醒源，每种唤醒源都有其特定的应用场景和功耗特性。正确选择和配置唤醒源是低功耗设计的关键之一。</p>
<h3 id="21-定时器唤醒timer-wakeup">2.1 定时器唤醒（Timer Wakeup）</h3>
<p>定时器唤醒是最简单也是最常用的唤醒方式。RTC 域内置了一个 64 位的定时器，可以设置任意的唤醒时间间隔，精度为微秒级。</p>
<p><strong>功耗特性</strong>：定时器唤醒的额外功耗非常低，通常只增加 1-2μA 的休眠电流。</p>
<p><strong>适用场景</strong>：周期性数据采集、定时上报等需要按固定时间间隔唤醒的应用。比如温湿度传感器节点每隔 5 分钟采集一次数据，土壤湿度监测节点每隔 1 小时上传一次数据。</p>
<p><strong>代码示例</strong>：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-cpp" data-lang="cpp"><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;esp_sleep.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="nf">setup</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="c1">// 配置 60 秒后唤醒
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="n">esp_sleep_enable_timer_wakeup</span><span class="p">(</span><span class="mi">60</span> <span class="o">*</span> <span class="mi">1000000</span><span class="p">);</span>  <span class="c1">// 单位：微秒
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  
</span></span><span class="line"><span class="cl">  <span class="c1">// 进入深度睡眠
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="n">esp_deep_sleep_start</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="nf">loop</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="c1">// 深度睡眠唤醒后不会到达 loop
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span></code></pre></div><p>需要注意的是，RTC 定时器使用的是内部的 8MHz RC 振荡器（经过分频）或者外部的 32.768kHz 晶振。如果使用内部 RC 振荡器，定时精度会受到温度的影响，在极端温度下可能有百分之几的误差。如果需要精确的定时，建议使用外部 32.768kHz 晶振。</p>
<h3 id="22-外部唤醒ext0-和-ext1">2.2 外部唤醒（Ext0 和 Ext1）</h3>
<p>外部唤醒允许通过 RTC GPIO 的电平变化来唤醒芯片。ESP32 提供了两种外部唤醒模式：</p>
<p><strong>Ext0 模式</strong>：使用单个 RTC GPIO 引脚触发唤醒，可以配置为高电平触发或低电平触发。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-cpp" data-lang="cpp"><span class="line"><span class="cl"><span class="c1">// 配置 GPIO34 为低电平唤醒
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="n">esp_sleep_enable_ext0_wakeup</span><span class="p">(</span><span class="n">GPIO_NUM_34</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>  <span class="c1">// 0 = 低电平触发，1 = 高电平触发
</span></span></span></code></pre></div><p><strong>Ext1 模式</strong>：支持使用多个 RTC GPIO 引脚，可以配置为&quot;任意一个引脚满足条件&quot;或&quot;所有引脚都满足条件&quot;两种逻辑。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-cpp" data-lang="cpp"><span class="line"><span class="cl"><span class="c1">// 配置 GPIO34 和 GPIO35，任意一个为高电平时唤醒
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="n">esp_sleep_enable_ext1_wakeup</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="p">(</span><span class="mi">1ULL</span> <span class="o">&lt;&lt;</span> <span class="n">GPIO_NUM_34</span><span class="p">)</span> <span class="o">|</span> <span class="p">(</span><span class="mi">1ULL</span> <span class="o">&lt;&lt;</span> <span class="n">GPIO_NUM_35</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">ESP_EXT1_WAKEUP_ANY_HIGH</span>
</span></span><span class="line"><span class="cl"><span class="p">);</span>
</span></span></code></pre></div><p><strong>功耗特性</strong>：外部唤醒的功耗取决于使用的引脚数量，通常在 5-10μA 左右。</p>
<p><strong>适用场景</strong>：按键唤醒、中断触发事件、外部信号触发等。比如智能门锁的按键唤醒、报警器的触发信号、干簧管的门磁检测等。</p>
<h3 id="23-触摸传感器唤醒touch-wakeup">2.3 触摸传感器唤醒（Touch Wakeup）</h3>
<p>ESP32 内置了多达 10 个电容触摸传感器通道，这些触摸传感器在深度睡眠模式下仍然可以工作，当检测到触摸时可以唤醒芯片。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-cpp" data-lang="cpp"><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;driver/touch_pad.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="nf">setup</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">touch_pad_init</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">  <span class="n">touch_pad_config</span><span class="p">(</span><span class="n">TOUCH_PAD_NUM8</span><span class="p">,</span> <span class="mi">1000</span><span class="p">);</span>  <span class="c1">// 阈值，低于此值视为触摸
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  
</span></span><span class="line"><span class="cl">  <span class="c1">// 启用触摸唤醒
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="n">esp_sleep_enable_touchpad_wakeup</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">  <span class="n">esp_deep_sleep_start</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p><strong>功耗特性</strong>：触摸唤醒的功耗大约在 20-30μA，比定时器和外部唤醒要高一些。</p>
<p><strong>适用场景</strong>：触摸按键、接近检测等。这对于需要美观外观（不需要物理按键）的消费电子产品特别有用，比如智能灯泡的触摸开关、蓝牙耳机的触摸控制等。</p>
<h3 id="24-ulp-协处理器唤醒ulp-wakeup">2.4 ULP 协处理器唤醒（ULP Wakeup）</h3>
<p>ULP（Ultra-Low Power）协处理器是 RTC 域内的一个简单处理器，可以在深度睡眠期间运行，执行简单的任务比如定时采样 ADC、轮询 GPIO、进行简单的计算等。当 ULP 检测到特定条件时，可以唤醒主 CPU。</p>
<p>这是 ESP32 最强大也是最复杂的唤醒方式。ULP 协处理器可以在主 CPU 休眠期间持续工作，而整体功耗仍然保持在很低的水平。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-cpp" data-lang="cpp"><span class="line"><span class="cl"><span class="c1">// 简单的 ULP 唤醒示例（实际需要编写 ULP 汇编代码）
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="nf">setup</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="c1">// 加载 ULP 程序到 RTC 内存
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="n">ulp_load_binary</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">ulp_main_bin_start</span><span class="p">,</span> 
</span></span><span class="line"><span class="cl">    <span class="p">(</span><span class="n">ulp_main_bin_end</span> <span class="o">-</span> <span class="n">ulp_main_bin_start</span><span class="p">)</span> <span class="o">/</span> <span class="k">sizeof</span><span class="p">(</span><span class="kt">uint32_t</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">  <span class="c1">// 配置 ULP 唤醒源
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="n">esp_sleep_enable_ulp_wakeup</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">  <span class="c1">// 启动 ULP 协处理器
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="n">ulp_run</span><span class="p">(</span><span class="o">&amp;</span><span class="n">ulp_entry</span> <span class="o">-</span> <span class="n">RTC_SLOW_MEM</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">  <span class="n">esp_deep_sleep_start</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p><strong>功耗特性</strong>：ULP 运行时的功耗取决于程序逻辑，通常在 10-50μA 范围内。如果 ULP 大部分时间也在休眠，只是定期唤醒采样，那么平均功耗可以非常低。</p>
<p><strong>适用场景</strong>：需要在休眠期间进行连续监测但又不能频繁唤醒主 CPU 的应用。比如：</p>
<ul>
<li>低功耗振动检测：ULP 定期采样加速度计，只有检测到振动时才唤醒主 CPU</li>
<li>模拟信号监测：ULP 持续采样 ADC，只有超过阈值时才唤醒</li>
<li>脉冲计数：ULP 计数脉冲信号，主 CPU 只在达到一定数量时才唤醒</li>
</ul>
<h3 id="25-唤醒源的组合使用">2.5 唤醒源的组合使用</h3>
<p>以上唤醒源可以组合使用，也就是说你可以同时启用多种唤醒方式。比如你可以同时启用定时器唤醒和触摸唤醒，这样节点既可以定时唤醒上传数据，用户也可以随时通过触摸来手动唤醒设备。</p>
<p>当同时使用多个唤醒源时，只需要多次调用相应的 <code>esp_sleep_enable_*_wakeup()</code> 函数即可。唤醒后，可以通过 <code>esp_sleep_get_wakeup_cause()</code> 来判断是哪个唤醒源导致了唤醒：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-cpp" data-lang="cpp"><span class="line"><span class="cl"><span class="n">esp_sleep_wakeup_cause_t</span> <span class="n">wakeup_reason</span> <span class="o">=</span> <span class="n">esp_sleep_get_wakeup_cause</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">switch</span><span class="p">(</span><span class="n">wakeup_reason</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">case</span> <span class="nl">ESP_SLEEP_WAKEUP_TIMER</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="s">&#34;定时器唤醒&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="k">break</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">case</span> <span class="nl">ESP_SLEEP_WAKEUP_EXT0</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="s">&#34;外部唤醒 Ext0&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="k">break</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">case</span> <span class="nl">ESP_SLEEP_WAKEUP_EXT1</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="s">&#34;外部唤醒 Ext1&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="k">break</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">case</span> <span class="nl">ESP_SLEEP_WAKEUP_TOUCHPAD</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="s">&#34;触摸唤醒&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="k">break</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">case</span> <span class="nl">ESP_SLEEP_WAKEUP_ULP</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="s">&#34;ULP 唤醒&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="k">break</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">default</span><span class="o">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="s">&#34;其他原因唤醒&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="k">break</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>这个功能非常有用，因为不同的唤醒原因可能需要执行不同的逻辑。比如触摸唤醒可能只需要点亮显示屏，而定时器唤醒则需要执行完整的数据采集和上传流程。</p>
<p>（第一部分完，约2300字）</p>
<h2 id="三rtc-存储器与数据持久化">三、RTC 存储器与数据持久化</h2>
<p>在深度睡眠模式下，数字域的 SRAM 会完全断电，其中的所有数据都会丢失。但是 RTC 域的存储器会保持供电，这就为我们提供了一个在多个唤醒周期之间保存数据的机制。</p>
<h3 id="31-rtc-存储器的类型">3.1 RTC 存储器的类型</h3>
<p>ESP32 提供了两种 RTC 存储器：</p>
<p><strong>RTC 慢速存储器（RTC_SLOW_MEM）</strong>：大小为 8KB，位于 RTC 慢速电源域。即使在休眠模式（Hibernation）下也会保持通电。可以被 ULP 协处理器访问。</p>
<p><strong>RTC 快速存储器（RTC_FAST_MEM）</strong>：大小也是 8KB，位于 RTC 快速电源域。在深度睡眠模式下保持通电，但在休眠模式下会断电。可以被主 CPU 更快地访问。</p>
<p>对于大多数应用来说，16KB 的 RTC 存储器已经足够保存关键的配置参数、计数变量、传感器数据历史等信息。</p>
<h3 id="32-使用-rtc-存储器的正确姿势">3.2 使用 RTC 存储器的正确姿势</h3>
<p>在 ESP-IDF 中，你可以通过特殊的属性声明来将变量放入 RTC 存储器：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-cpp" data-lang="cpp"><span class="line"><span class="cl"><span class="c1">// 放入 RTC 慢速存储器，ULP 也可以访问
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="n">RTC_DATA_ATTR</span> <span class="kt">int</span> <span class="n">wakeup_count</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 放入 RTC 快速存储器
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="n">RTC_FAST_ATTR</span> <span class="kt">float</span> <span class="n">temperature_history</span><span class="p">[</span><span class="mi">10</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span><span class="mi">0</span><span class="p">};</span>
</span></span></code></pre></div><p>或者使用 <code>rtc_slow_mem</code> 和 <code>rtc_fast_mem</code> 段属性：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-cpp" data-lang="cpp"><span class="line"><span class="cl"><span class="kt">int</span> <span class="n">my_variable</span> <span class="nf">__attribute__</span><span class="p">((</span><span class="n">section</span><span class="p">(</span><span class="s">&#34;.rtc.data&#34;</span><span class="p">)))</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span></code></pre></div><p>需要注意的是，RTC 存储器中的变量<strong>不会在唤醒时被重新初始化</strong>，也就是说它们的值会在多个唤醒周期之间保持。这既是优点也是缺点——优点是你不需要额外操作就能保留数据，缺点是第一次启动时这些变量的值是未定义的，需要特别处理。</p>
<p>一个常见的错误是：假设 RTC 变量会被正确初始化为 0。实际上，第一次上电时 RTC 存储器中的内容是随机的垃圾值。因此，你需要一个机制来判断这些变量是否已经被初始化过：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-cpp" data-lang="cpp"><span class="line"><span class="cl"><span class="n">RTC_DATA_ATTR</span> <span class="kt">int</span> <span class="n">wakeup_count</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="n">RTC_DATA_ATTR</span> <span class="kt">uint32_t</span> <span class="n">magic_number</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="cp">#define RTC_MAGIC 0xDEADBEEF
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="nf">setup</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="n">magic_number</span> <span class="o">!=</span> <span class="n">RTC_MAGIC</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// 第一次启动，初始化 RTC 变量
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="n">wakeup_count</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">magic_number</span> <span class="o">=</span> <span class="n">RTC_MAGIC</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="s">&#34;首次启动，RTC 存储器已初始化&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// 不是第一次启动，变量值有效
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="n">wakeup_count</span><span class="o">++</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">Serial</span><span class="p">.</span><span class="n">printf</span><span class="p">(</span><span class="s">&#34;第 %d 次唤醒</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">,</span> <span class="n">wakeup_count</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>这里使用了一个魔数（Magic Number）来判断 RTC 存储器是否已经被初始化。如果魔数不是我们预期的值，说明是第一次启动（或者电池掉电导致 RTC 存储器也丢失了数据），这时我们需要对所有 RTC 变量进行初始化。</p>
<h3 id="33-rtc-存储器的局限性">3.3 RTC 存储器的局限性</h3>
<p>虽然 RTC 存储器非常有用，但它也有一些重要的局限性：</p>
<ol>
<li><strong>容量有限</strong>：总共只有 16KB，不能用来存储大量数据。</li>
<li><strong>写入寿命</strong>：RTC 存储器是 SRAM，不是 Flash，理论上没有写入次数限制，可以放心地频繁写入。</li>
<li><strong>掉电丢失</strong>：如果主电源完全断开（比如电池被拔掉），RTC 存储器也会丢失数据。如果需要真正持久化的存储，还需要使用 Flash 或外部 EEPROM。</li>
<li><strong>不能包含 C++ 对象</strong>：RTC 存储器中的变量在唤醒时不会调用构造函数，因此不能用来存储非 POD（Plain Old Data）类型的 C++ 对象。</li>
<li><strong>ULP 访问限制</strong>：ULP 协处理器只能访问 RTC_SLOW_MEM，不能访问 RTC_FAST_MEM。</li>
</ol>
<h3 id="34-数据持久化的最佳实践">3.4 数据持久化的最佳实践</h3>
<p>基于以上特点，这里给出一些数据持久化的最佳实践：</p>
<p><strong>短期数据</strong>（跨唤醒周期但不需要掉电保存）：</p>
<ul>
<li>简单的计数器、状态标志：直接使用 RTC_DATA_ATTR 变量</li>
<li>ULP 协处理器需要的数据：使用 RTC_SLOW_MEM</li>
</ul>
<p><strong>中期数据</strong>（需要保存数小时到数天，但可以容忍偶尔丢失）：</p>
<ul>
<li>定期将 RTC 存储器中的重要数据备份到 Flash 的 NVS（Non-Volatile Storage）</li>
<li>比如每次唤醒时检查，如果距离上次备份已经超过 10 次唤醒，就执行一次备份</li>
</ul>
<p><strong>长期数据</strong>（必须永久保存）：</p>
<ul>
<li>配置参数、校准数据等：每次修改后立即写入 NVS 或 Flash</li>
<li>大量的历史数据：使用 SD 卡或外部 Flash 存储</li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-cpp" data-lang="cpp"><span class="line"><span class="cl"><span class="c1">// NVS 保存示例
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="cp">#include</span> <span class="cpf">&lt;nvs_flash.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;nvs.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="nf">save_config_to_nvs</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">nvs_handle_t</span> <span class="n">my_handle</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="n">nvs_open</span><span class="p">(</span><span class="s">&#34;storage&#34;</span><span class="p">,</span> <span class="n">NVS_READWRITE</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">my_handle</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">  <span class="n">nvs_set_i32</span><span class="p">(</span><span class="n">my_handle</span><span class="p">,</span> <span class="s">&#34;wakeup_count&#34;</span><span class="p">,</span> <span class="n">wakeup_count</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="n">nvs_set_float</span><span class="p">(</span><span class="n">my_handle</span><span class="p">,</span> <span class="s">&#34;calibration&#34;</span><span class="p">,</span> <span class="n">calibration_value</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">  <span class="n">nvs_commit</span><span class="p">(</span><span class="n">my_handle</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="n">nvs_close</span><span class="p">(</span><span class="n">my_handle</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="nf">load_config_from_nvs</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">nvs_handle_t</span> <span class="n">my_handle</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="n">nvs_open</span><span class="p">(</span><span class="s">&#34;storage&#34;</span><span class="p">,</span> <span class="n">NVS_READONLY</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">my_handle</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">  <span class="n">nvs_get_i32</span><span class="p">(</span><span class="n">my_handle</span><span class="p">,</span> <span class="s">&#34;wakeup_count&#34;</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">wakeup_count</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="n">nvs_get_float</span><span class="p">(</span><span class="n">my_handle</span><span class="p">,</span> <span class="s">&#34;calibration&#34;</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">calibration_value</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">  <span class="n">nvs_close</span><span class="p">(</span><span class="n">my_handle</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h2 id="四gpio-配置与功耗陷阱">四、GPIO 配置与功耗陷阱</h2>
<p>很多开发者在初次使用深度睡眠模式时，都会遇到一个令人沮丧的问题：明明已经调用了 <code>esp_deep_sleep_start()</code>，但测量到的休眠电流却高达几毫安，远不是手册上说的十几微安。</p>
<p>这通常是因为 GPIO 配置不当导致的。在深度睡眠模式下，GPIO 的状态会对整体功耗产生巨大的影响——一个配置错误的 GPIO 引脚可能导致毫安级的额外电流消耗！</p>
<h3 id="41-深度睡眠期间的-gpio-状态">4.1 深度睡眠期间的 GPIO 状态</h3>
<p>在进入深度睡眠时，ESP32 的 GPIO 引脚会根据其类型和配置进入不同的状态：</p>
<p><strong>RTC GPIO（RTCIO）</strong>：GPIO0、GPIO2、GPIO4、GPIO12-GPIO15、GPIO25-GPIO27、GPIO32-GPIO39 属于 RTC GPIO。这些引脚在深度睡眠期间的状态可以被精确控制。</p>
<p><strong>数字 GPIO</strong>：其他 GPIO 引脚属于数字域，在深度睡眠期间会与数字域一起断电，通常会进入高阻态（Hi-Z）。</p>
<p>问题在于，如果外部电路没有对这些引脚进行正确的上下拉处理，高阻态可能导致不确定的电平，进而导致外部器件的额外电流消耗。</p>
<h3 id="42-常见的-gpio-功耗陷阱">4.2 常见的 GPIO 功耗陷阱</h3>
<p>让我列举几个最常见的 GPIO 相关功耗问题：</p>
<p><strong>陷阱 1：I2C 总线没有上拉电阻</strong></p>
<p>如果 ESP32 通过 I2C 连接了传感器，但 I2C 总线的 SDA 和 SCL 线没有接上拉电阻，那么在深度睡眠期间 ESP32 的 GPIO 进入高阻态后，总线电平不确定，可能导致传感器内部的电流增加。</p>
<p><strong>正确做法</strong>：I2C 总线必须接 4.7kΩ 或 10kΩ 的上拉电阻到 VCC。</p>
<p><strong>陷阱 2：外部传感器的电源没有被切断</strong></p>
<p>很多开发者会让传感器一直通电，即使 ESP32 进入了深度睡眠。一个普通的 I2C 传感器在待机状态下可能消耗几十微安到几百微安，如果有多个这样的传感器，累积起来的功耗就非常可观了。</p>
<p><strong>正确做法</strong>：使用一个 MOSFET 或使能引脚来控制传感器的电源，在进入深度睡眠之前切断传感器的供电。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-cpp" data-lang="cpp"><span class="line"><span class="cl"><span class="cp">#define SENSOR_PWR_PIN GPIO_NUM_21
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="nf">setup</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="c1">// 唤醒后给传感器供电
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="n">pinMode</span><span class="p">(</span><span class="n">SENSOR_PWR_PIN</span><span class="p">,</span> <span class="n">OUTPUT</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="n">digitalWrite</span><span class="p">(</span><span class="n">SENSOR_PWR_PIN</span><span class="p">,</span> <span class="n">HIGH</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">  <span class="c1">// ... 读取传感器数据 ...
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  
</span></span><span class="line"><span class="cl">  <span class="c1">// 进入深度睡眠前切断传感器电源
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="n">digitalWrite</span><span class="p">(</span><span class="n">SENSOR_PWR_PIN</span><span class="p">,</span> <span class="n">LOW</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="n">esp_deep_sleep_start</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p><strong>陷阱 3：GPIO 引脚浮空导致漏电流</strong></p>
<p>如果某个 GPIO 引脚在深度睡眠时既不接高也不接低，处于浮空状态，可能导致 ESP32 内部的漏电流增加。</p>
<p><strong>正确做法</strong>：对于不需要的 GPIO 引脚，配置内部下拉或上拉电阻，或者在外部电路中固定电平。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-cpp" data-lang="cpp"><span class="line"><span class="cl"><span class="c1">// 配置 RTC GPIO 在深度睡眠期间保持下拉
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="n">rtc_gpio_pulldown_en</span><span class="p">(</span><span class="n">GPIO_NUM_34</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="n">rtc_gpio_pullup_dis</span><span class="p">(</span><span class="n">GPIO_NUM_34</span><span class="p">);</span>
</span></span></code></pre></div><p><strong>陷阱 4：LED 指示灯一直亮着</strong></p>
<p>很多开发板上都有电源指示灯，这在开发时很方便，但在实际产品中就是一个巨大的功耗浪费。一个普通的 LED 串联 1kΩ 电阻，3.3V 下的电流大约是 2-3mA，这比深度睡眠的功耗高了几百倍！</p>
<p><strong>正确做法</strong>：在生产版本中移除电源 LED，或者通过 GPIO 控制，只在必要时点亮。</p>
<h3 id="43-深度睡眠前的-gpio-处理步骤">4.3 深度睡眠前的 GPIO 处理步骤</h3>
<p>进入深度睡眠之前，建议按照以下步骤处理 GPIO：</p>
<ol>
<li><strong>列出所有使用的 GPIO</strong>：制作一个表格，记录每个 GPIO 的用途和连接方式。</li>
<li><strong>切断外部器件电源</strong>：关闭所有可以关闭的传感器、模块的电源。</li>
<li><strong>配置 RTC GPIO 状态</strong>：对于 RTC GPIO，明确配置其在深度睡眠期间的状态。</li>
<li><strong>处理浮空引脚</strong>：确保没有引脚处于不确定的浮空状态。</li>
<li><strong>验证和测量</strong>：实际测量休眠电流，确保符合预期。</li>
</ol>
<p>ESP-IDF 提供了专门的函数来配置 RTC GPIO 在深度睡眠期间的状态：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-cpp" data-lang="cpp"><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;driver/rtc_io.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl"><span class="c1">// 在深度睡眠期间保持 GPIO 输出低电平
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="n">rtc_gpio_set_level</span><span class="p">(</span><span class="n">GPIO_NUM_2</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="n">rtc_gpio_set_direction</span><span class="p">(</span><span class="n">GPIO_NUM_2</span><span class="p">,</span> <span class="n">RTC_GPIO_MODE_OUTPUT_ONLY</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 在深度睡眠期间保持 GPIO 上拉
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="n">rtc_gpio_pullup_en</span><span class="p">(</span><span class="n">GPIO_NUM_34</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="n">rtc_gpio_pulldown_dis</span><span class="p">(</span><span class="n">GPIO_NUM_34</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 隔离 GPIO（进入高阻态）
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="n">rtc_gpio_isolate</span><span class="p">(</span><span class="n">GPIO_NUM_12</span><span class="p">);</span>
</span></span></code></pre></div><p>特别要注意的是，仅仅在 <code>setup()</code> 中使用 <code>pinMode()</code> 和 <code>digitalWrite()</code> 是不够的——这些设置只在活动模式有效，进入深度睡眠后会丢失。必须使用 <code>rtc_gpio_*</code> 系列函数来配置深度睡眠期间的引脚状态。</p>
<h2 id="五外设使用的注意事项">五、外设使用的注意事项</h2>
<p>在低功耗设计中，外设的使用也需要特别注意。很多外设如果没有被正确地关闭或者配置，会在深度睡眠期间继续消耗电流。</p>
<h3 id="51-wifi-和蓝牙">5.1 WiFi 和蓝牙</h3>
<p>WiFi 和蓝牙是 ESP32 上功耗最大的模块。在进入深度睡眠之前，确保它们已经被正确关闭：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-cpp" data-lang="cpp"><span class="line"><span class="cl"><span class="c1">// 关闭 WiFi
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="n">WiFi</span><span class="p">.</span><span class="n">disconnect</span><span class="p">(</span><span class="nb">true</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="n">WiFi</span><span class="p">.</span><span class="n">mode</span><span class="p">(</span><span class="n">WIFI_OFF</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 关闭蓝牙（如果使用了）
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="n">btStop</span><span class="p">();</span>
</span></span></code></pre></div><p>实际上，当你进入深度睡眠时，整个数字域都会断电，WiFi 和蓝牙自然也会被关闭。但是，显式地关闭它们可以避免一些潜在的问题，比如让 WiFi 基站知道设备正在下线。</p>
<p>更重要的是 WiFi 连接的时间优化——每次唤醒后连接 WiFi 消耗的时间和能量，往往是整个系统能耗的大头。一个 WiFi 连接过程可能需要几百毫秒到几秒钟，期间电流消耗在 100mA 左右。</p>
<p>优化 WiFi 连接时间的技巧：</p>
<ol>
<li><strong>使用快速扫描</strong>：只扫描已知的信道，而不是全信道扫描。</li>
<li><strong>保存 BSSID</strong>：保存上次连接的 AP 的 BSSID，下次直接连接，不需要扫描。</li>
<li><strong>减少重连次数</strong>：如果连接失败，不要立即重试，而是回到睡眠，下次再试。</li>
<li><strong>使用静态 IP</strong>：避免 DHCP 分配过程，可以节省几百毫秒。</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-cpp" data-lang="cpp"><span class="line"><span class="cl"><span class="c1">// 使用静态 IP 加速连接
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="n">IPAddress</span> <span class="nf">local_IP</span><span class="p">(</span><span class="mi">192</span><span class="p">,</span> <span class="mi">168</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">100</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="n">IPAddress</span> <span class="nf">gateway</span><span class="p">(</span><span class="mi">192</span><span class="p">,</span> <span class="mi">168</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="n">IPAddress</span> <span class="nf">subnet</span><span class="p">(</span><span class="mi">255</span><span class="p">,</span> <span class="mi">255</span><span class="p">,</span> <span class="mi">255</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">WiFi</span><span class="p">.</span><span class="n">config</span><span class="p">(</span><span class="n">local_IP</span><span class="p">,</span> <span class="n">gateway</span><span class="p">,</span> <span class="n">subnet</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="n">WiFi</span><span class="p">.</span><span class="n">begin</span><span class="p">(</span><span class="n">ssid</span><span class="p">,</span> <span class="n">password</span><span class="p">);</span>
</span></span></code></pre></div><h3 id="52-adc-和-dac">5.2 ADC 和 DAC</h3>
<p>ADC（模数转换器）和 DAC（数模转换器）在深度睡眠模式下是否耗电，取决于它们的配置。如果在进入深度睡眠之前没有正确关闭，它们可能会继续消耗电流。</p>
<p>特别是当你使用了 ADC 的衰减配置时，内部的衰减网络可能会引入额外的漏电流。</p>
<p>建议在进入深度睡眠之前显式关闭 ADC：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-cpp" data-lang="cpp"><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;driver/adc.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl"><span class="c1">// 使用完后释放 ADC
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="n">adc_power_off</span><span class="p">();</span>
</span></span></code></pre></div><p>对于 DAC，也要确保在深度睡眠前关闭：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-cpp" data-lang="cpp"><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;driver/dac.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl"><span class="n">dac_output_enable</span><span class="p">(</span><span class="n">DAC_CHANNEL_1</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="c1">// ... 使用 DAC ...
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="n">dac_output_disable</span><span class="p">(</span><span class="n">DAC_CHANNEL_1</span><span class="p">);</span>
</span></span></code></pre></div><h3 id="53-串口uart">5.3 串口（UART）</h3>
<p>串口本身在深度睡眠时会被关闭，不会消耗太多电流。但是，如果串口连接了外部设备，而在深度睡眠期间 TX 引脚的电平不确定，可能导致外部设备消耗额外的电流。</p>
<p>特别是当你使用了串口调试打印时，要注意：在生产固件中应该关闭所有的调试输出，因为串口发送数据会消耗大量的时间和能量。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-cpp" data-lang="cpp"><span class="line"><span class="cl"><span class="c1">// 生产版本关闭串口输出
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="cp">#ifdef PRODUCTION
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>  <span class="cp">#define DEBUG_PRINT(...)
</span></span></span><span class="line"><span class="cl"><span class="cp">#else
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>  <span class="cp">#define DEBUG_PRINT(...) Serial.print(__VA_ARGS__)
</span></span></span><span class="line"><span class="cl"><span class="cp">#endif
</span></span></span></code></pre></div><p>另外，如果串口只在开发时使用，在生产版本中可以将串口引脚配置为下拉，避免浮空。</p>
<h3 id="54-其他外设">5.4 其他外设</h3>
<p><strong>SPI Flash / PSRAM</strong>：如果使用了外部 Flash 或 PSRAM，要确保它们在进入深度睡眠前进入低功耗状态。大多数 Flash 芯片在片选（CS）为高电平时会进入待机模式，所以确保 CS 引脚在深度睡眠期间被拉高。</p>
<p><strong>SD 卡</strong>：SD 卡的待机电流可能很大（几十毫安！），在进入深度睡眠前一定要正确卸载并切断 SD 卡的电源。</p>
<p><strong>I2C / SPI 传感器</strong>：如前所述，最好通过 MOSFET 完全切断电源。如果不能切断电源，确保传感器进入了最低功耗的待机模式，并检查其 datasheet 中的待机电流参数。</p>
<p>（第二部分完，约2400字）</p>
<h2 id="六功耗测量与调试技巧">六、功耗测量与调试技巧</h2>
<p>理论计算的功耗再完美，也不如实际测量一次准确。低功耗设计的关键环节就是测量和调试。然而，测量微安级的电流并不是一件容易的事情，需要合适的工具和方法。</p>
<h3 id="61-测量工具选择">6.1 测量工具选择</h3>
<p>要准确测量 ESP32 在深度睡眠模式下的微安级电流，你需要一个足够灵敏的测量工具。普通的万用表通常只能测不准这么小的电流。</p>
<p>推荐的测量工具：</p>
<ol>
<li>
<p><strong>专用低功耗电流表</strong>：比如 Nordic Power Profiler Kit II、QOtium μCurrent、Keysight 低功耗电流探头等。这些设备专门设计用于测量低功耗设备的电流，量程通常可以低至纳安级别，并且具有很高的采样率，还能绘制电流波形。</p>
</li>
<li>
<p><strong>高精度数字万用表</strong>：至少是四位半或五位半或更高精度的万用表，带有 μA 量程。注意要选择直流内阻较低的型号，比如 Fluke 87V、Keysight 34465A 等。</p>
</li>
<li>
<p><strong>精密电阻 + 示波器</strong>：在电源回路中串联一个已知阻值的精密电阻（比如 10Ω 或 100Ω），用示波器测量电阻两端的电压，再根据欧姆定律计算电流。这种方法的优点是可以观察电流随时间变化的波形。</p>
</li>
</ol>
<h3 id="62-常见测量技巧">6.2 常见测量技巧</h3>
<p>测量深度睡眠电流时，有几个重要的技巧：</p>
<ol>
<li>
<p><strong>移除开发板上的不必要组件</strong>：大多数 ESP32 开发板都有电源 LED、USB 转串口芯片、线性稳压器等，这些都会消耗电流。要测量芯片本身的真实功耗，你需要移除这些组件，或者直接测量一个最小系统板。</p>
</li>
<li>
<p><strong>使用稳定干净的电源</strong>：电源噪声会影响测量精度。使用电池供电或者低噪声的线性电源，避免使用开关电源。</p>
</li>
<li>
<p><strong>给测量足够长的时间</strong>：有时候电流可能不是恒定的，可能存在周期性的毛刺。测量至少测量几秒钟到几分钟，观察平均电流。</p>
</li>
<li>
<p><strong>分阶段测量</strong>：先测量活动模式电流、连接 WiFi 时的电流、深度睡眠时的电流，分别对比理论值比较，找出哪个阶段的功耗异常。</p>
</li>
</ol>
<h3 id="63-常见问题排查">6.3 常见问题排查</h3>
<p>如果你测量到的休眠电流远高于预期（比如超过 200μA 以上），可以按照以下步骤排查：</p>
<p><strong>步骤 1：测试最小化</strong>：先测一个空白的程序，只做一件事：进入深度睡眠。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-cpp" data-lang="cpp"><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;esp_sleep.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="nf">setup</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">Serial</span><span class="p">.</span><span class="n">begin</span><span class="p">(</span><span class="mi">115200</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="n">delay</span><span class="p">(</span><span class="mi">1000</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="s">&#34;进入深度睡眠...&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">  <span class="n">esp_sleep_enable_timer_wakeup</span><span class="p">(</span><span class="mi">10</span> <span class="o">*</span> <span class="mi">1000000</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="n">esp_deep_sleep_start</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="nf">loop</span><span class="p">()</span> <span class="p">{}</span>
</span></span></code></pre></div><p>如果这个空白程序的电流就不正常，说明是硬件问题；如果正常，说明是你程序中的某个配置有问题。</p>
<p><strong>步骤 2：逐个关闭外设</strong>：在你的程序中逐个关闭外设，每次关闭一个，然后测量电流，看看关闭哪个外设后电流降下来了，那个就是罪魁祸首。</p>
<p><strong>步骤 3：检查 GPIO 状态</strong>：逐个断开外部连接的传感器、模块，每次断开一个，测量电流，找到导致电流异常的外部器件。</p>
<p><strong>步骤 4：检查电源路径</strong>：检查电源路径上的元件，比如线性稳压器的静态电流、分压电阻的漏电流等。</p>
<h3 id="64-一个真实的调试案例">6.4 一个真实的调试案例</h3>
<p>让我分享一个我曾经遇到过的一个调试案例：</p>
<p>在那个农业物联网项目中，我们最初优化后的节点的深度睡眠电流大约是 150μA，理论计算应该是 20μA 左右，差了 7 倍多！</p>
<p>排查过程：</p>
<ol>
<li>
<p>先烧录空白深度睡眠程序，电流还是 130μA，说明不是软件问题。</p>
</li>
<li>
<p>开始检查原理图，发现开发板上有一个电源 LED，串联了 1kΩ 电阻。计算一下：3.3V / 1kΩ = 3.3mA！这比我们测量到的 130μA 还大？不对啊？哦，对，因为是 3.3V 线性稳压器的输入是 5V，输出 3.3V，所以 LED 的电流是 (5V - 3.3V - 1.8V = 0V 呢？实际上，我们用的是 HT7333 稳压器，输入输出压差大约是 1.2V，LED 的实际电流是 (5V - 3.3V - 1.8V) / 1kΩ = 0V / 1kΩ = 0？不对，重新算：LED 正向压降约 1.8V，LED 两端电压就是 3.3V - 1.8V = 1.5V，电流就是 1.5V / 1kΩ = 1.5mA。那为什么测量只有 130μA 呢？哦，原来我们看错了——那个 LED 是接在 ESP32 的 GPIO 上的，不是电源常亮的！</p>
</li>
<li>
<p>继续排查，发现是 I2C 总线上拉电阻接了上拉电阻，但是传感器的传感器在深度睡眠时没有被断电，但是传感器本身有漏电流。我们使用的 BME280 传感器 datasheet 上说待机电流只有 0.1μA，但是我们测量发现实际有 10μA 左右的漏电流。</p>
</li>
<li>
<p>最终发现真正的元凶是：我们使用了一个 CP2102 USB 转串口芯片，虽然 USB 没有被移除时，这个芯片通过 TX 有大约 100μA 的漏电流！虽然芯片虽然芯片虽然芯片虽然芯片虽然芯片！把这个芯片移除后，电流降到了 12μA！</p>
</li>
</ol>
<p>这个故事告诉我们：开发板上的每一个元件都可能导致额外的功耗，在设计产品时，要仔细选择元件选择，移除不必要的元件。</p>
<h2 id="七完整的低功耗传感器节点实战">七、完整的低功耗传感器节点实战</h2>
<p>现在让我们通过一个完整的实战项目来总结前面所学的知识，设计一个真正低功耗的温湿度传感器节点。</p>
<h3 id="71-项目需求">7.1 项目需求</h3>
<ul>
<li>使用 ESP32 + SHT30 温湿度传感器</li>
<li>每 5 分钟采集一次温湿度</li>
<li>每 1 小时上传一次数据到服务器</li>
<li>使用两节 AA 电池供电</li>
<li>目标电池寿命：&gt; 2 年</li>
</ul>
<h3 id="72-硬件设计要点">7.2 硬件设计要点</h3>
<ol>
<li>
<p><strong>电源管理</strong>：</p>
<ul>
<li>使用 HT7333 低静态电流 LDO（静态电流 &lt; 1μA）</li>
<li>使用 MOSFET 控制传感器电源</li>
<li>移除所有不必要的 LED</li>
<li>I2C 总线接 4.7kΩ 上拉电阻</li>
</ul>
</li>
<li>
<p><strong>唤醒源</strong>：</p>
<ul>
<li>主唤醒源：RTC 定时器</li>
<li>备用唤醒源：外部按键（用于调试和配置）</li>
</ul>
</li>
<li>
<p><strong>数据存储</strong>：</p>
<ul>
<li>RTC 存储器保存唤醒计数和最近几次的温湿度数据</li>
<li>NVS 保存 WiFi 配置和设备 ID</li>
</ul>
</li>
</ol>
<h3 id="73-完整代码实现">7.3 完整代码实现</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-cpp" data-lang="cpp"><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;Arduino.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;WiFi.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;Wire.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;ClosedCube_SHT31D.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;esp_sleep.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;driver/rtc_io.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;nvs_flash.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl"><span class="c1">// 引脚定义
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="cp">#define SENSOR_PWR_PIN GPIO_NUM_21
</span></span></span><span class="line"><span class="cl"><span class="cp">#define SDA_PIN GPIO_NUM_25
</span></span></span><span class="line"><span class="cl"><span class="cp">#define SCL_PIN GPIO_NUM_26
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl"><span class="c1">// RTC 变量
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="n">RTC_DATA_ATTR</span> <span class="kt">int</span> <span class="n">wakeup_count</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="n">RTC_DATA_ATTR</span> <span class="kt">uint32_t</span> <span class="n">magic_number</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="n">RTC_DATA_ATTR</span> <span class="kt">float</span> <span class="n">temp_history</span><span class="p">[</span><span class="mi">6</span><span class="p">];</span>  <span class="c1">// 保存最近 6 次温度数据
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="n">RTC_DATA_ATTR</span> <span class="kt">float</span> <span class="n">humi_history</span><span class="p">[</span><span class="mi">6</span><span class="p">];</span> <span class="c1">// 保存最近 6 次湿度数据
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="n">RTC_DATA_ATTR</span> <span class="kt">int</span> <span class="n">upload_counter</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="cp">#define RTC_MAGIC 0xDEADBEEF
</span></span></span><span class="line"><span class="cl"><span class="cp">#define UPLOAD_INTERVAL 12  </span><span class="c1">// 每 12 次唤醒（60 分钟）上传一次
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="cl"><span class="c1">// WiFi 配置（实际项目中应该从 NVS 读取
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">const</span> <span class="kt">char</span><span class="o">*</span> <span class="n">ssid</span> <span class="o">=</span> <span class="s">&#34;YOUR_WIFI_SSID&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">const</span> <span class="kt">char</span><span class="o">*</span> <span class="n">password</span> <span class="o">=</span> <span class="s">&#34;YOUR_WIFI_PASSWORD&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">const</span> <span class="kt">char</span><span class="o">*</span> <span class="n">server_url</span> <span class="o">=</span> <span class="s">&#34;http://your-server.com/api/data&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">ClosedCube_SHT31D</span> <span class="n">sht30</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="nf">setup</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">Serial</span><span class="p">.</span><span class="n">begin</span><span class="p">(</span><span class="mi">115200</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">  <span class="c1">// 初始化 RTC 变量
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="k">if</span> <span class="p">(</span><span class="n">magic_number</span> <span class="o">!=</span> <span class="n">RTC_MAGIC</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">wakeup_count</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">magic_number</span> <span class="o">=</span> <span class="n">RTC_MAGIC</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">upload_counter</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">memset</span><span class="p">(</span><span class="n">temp_history</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">temp_history</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">    <span class="n">memset</span><span class="p">(</span><span class="n">humi_history</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">humi_history</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">    <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="s">&#34;首次启动，RTC 变量已初始化&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">  <span class="n">wakeup_count</span><span class="o">++</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="n">Serial</span><span class="p">.</span><span class="n">printf</span><span class="p">(</span><span class="s">&#34;第 %d 次唤醒</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">,</span> <span class="n">wakeup_count</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">  <span class="c1">// 给传感器供电
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="n">pinMode</span><span class="p">(</span><span class="n">SENSOR_PWR_PIN</span><span class="p">,</span> <span class="n">OUTPUT</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="n">digitalWrite</span><span class="p">(</span><span class="n">SENSOR_PWR_PIN</span><span class="p">,</span> <span class="n">HIGH</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="n">delay</span><span class="p">(</span><span class="mi">10</span><span class="p">);</span>  <span class="c1">// 等待传感器稳定
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  
</span></span><span class="line"><span class="cl">  <span class="c1">// 初始化 I2C 和传感器
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="n">Wire</span><span class="p">.</span><span class="n">begin</span><span class="p">(</span><span class="n">SDA_PIN</span><span class="p">,</span> <span class="n">SCL_PIN</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="n">sht30</span><span class="p">.</span><span class="n">begin</span><span class="p">(</span><span class="mh">0x44</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">  <span class="c1">// 读取温湿度
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="n">SHT31D</span> <span class="n">result</span> <span class="o">=</span> <span class="n">sht30</span><span class="p">.</span><span class="n">readTemperatureAndHumidity</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">error</span> <span class="o">==</span> <span class="n">SHT3XD_NO_ERROR</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kt">float</span> <span class="n">temp</span> <span class="o">=</span> <span class="n">result</span><span class="p">.</span><span class="n">t</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">float</span> <span class="n">humi</span> <span class="o">=</span> <span class="n">result</span><span class="p">.</span><span class="n">rh</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="n">Serial</span><span class="p">.</span><span class="n">printf</span><span class="p">(</span><span class="s">&#34;温度: %.2f °C, 湿度: %.2f %%RH</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">,</span> <span class="n">temp</span><span class="p">,</span> <span class="n">humi</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="c1">// 保存到历史记录
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">5</span><span class="p">;</span> <span class="n">i</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span><span class="o">--</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="n">temp_history</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">temp_history</span><span class="p">[</span><span class="n">i</span><span class="o">-</span><span class="mi">1</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">      <span class="n">humi_history</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">humi_history</span><span class="p">[</span><span class="n">i</span><span class="o">-</span><span class="mi">1</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="n">temp_history</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="n">temp</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">humi_history</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="n">humi</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="c1">// 检查是否需要上传
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="n">upload_counter</span><span class="o">++</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">upload_counter</span> <span class="o">&gt;=</span> <span class="n">UPLOAD_INTERVAL</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="n">upload_counter</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">upload_data</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="s">&#34;传感器读取失败&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">  <span class="c1">// 关闭传感器电源
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="n">digitalWrite</span><span class="p">(</span><span class="n">SENSOR_PWR_PIN</span><span class="p">,</span> <span class="n">LOW</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">  <span class="c1">// 配置深度睡眠前的 GPIO 状态
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="n">rtc_gpio_set_direction</span><span class="p">(</span><span class="n">SENSOR_PWR_PIN</span><span class="p">,</span> <span class="n">RTC_GPIO_MODE_OUTPUT_ONLY</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="n">rtc_gpio_set_level</span><span class="p">(</span><span class="n">SENSOR_PWR_PIN</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">  <span class="c1">// 配置 5 分钟后唤醒
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="n">esp_sleep_enable_timer_wakeup</span><span class="p">(</span><span class="mi">5</span> <span class="o">*</span> <span class="mi">60</span> <span class="o">*</span> <span class="mi">1000000ULL</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">  <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="s">&#34;进入深度睡眠...&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="n">Serial</span><span class="p">.</span><span class="n">flush</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">  <span class="n">esp_deep_sleep_start</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="nf">upload_data</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="s">&#34;开始连接 WiFi...&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">  <span class="c1">// 使用静态 IP 加速连接
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="n">IPAddress</span> <span class="n">local_IP</span><span class="p">(</span><span class="mi">192</span><span class="p">,</span> <span class="mi">168</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">100</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="n">IPAddress</span> <span class="n">gateway</span><span class="p">(</span><span class="mi">192</span><span class="p">,</span> <span class="mi">168</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="n">IPAddress</span> <span class="n">subnet</span><span class="p">(</span><span class="mi">255</span><span class="p">,</span> <span class="mi">255</span><span class="p">,</span> <span class="mi">255</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="n">WiFi</span><span class="p">.</span><span class="n">config</span><span class="p">(</span><span class="n">local_IP</span><span class="p">,</span> <span class="n">gateway</span><span class="p">,</span> <span class="n">subnet</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">  <span class="n">WiFi</span><span class="p">.</span><span class="n">begin</span><span class="p">(</span><span class="n">ssid</span><span class="p">,</span> <span class="n">password</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">  <span class="c1">// 最多等待 10 秒
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="kt">int</span> <span class="n">retry</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">while</span> <span class="p">(</span><span class="n">WiFi</span><span class="p">.</span><span class="n">status</span><span class="p">()</span> <span class="o">!=</span> <span class="n">WL_CONNECTED</span> <span class="o">&amp;&amp;</span> <span class="n">retry</span> <span class="o">&lt;</span> <span class="mi">20</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">delay</span><span class="p">(</span><span class="mi">500</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="n">Serial</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="s">&#34;.&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="n">retry</span><span class="o">++</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="n">WiFi</span><span class="p">.</span><span class="n">status</span><span class="p">()</span> <span class="o">==</span> <span class="n">WL_CONNECTED</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="s">&#34;</span><span class="se">\n</span><span class="s">WiFi 连接成功&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="c1">// 计算平均值
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="kt">float</span> <span class="n">avg_temp</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span> <span class="n">avg_humi</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="mi">6</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="n">avg_temp</span> <span class="o">+=</span> <span class="n">temp_history</span><span class="p">[</span><span class="n">i</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">      <span class="n">avg_humi</span> <span class="o">+=</span> <span class="n">humi_history</span><span class="p">[</span><span class="n">i</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="n">avg_temp</span> <span class="o">/=</span> <span class="mi">6</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">avg_humi</span> <span class="o">/=</span> <span class="mi">6</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="n">Serial</span><span class="p">.</span><span class="n">printf</span><span class="p">(</span><span class="s">&#34;平均温度: %.2f °C, 平均湿度: %.2f %%RH</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">,</span> 
</span></span><span class="line"><span class="cl">                   <span class="n">avg_temp</span><span class="p">,</span> <span class="n">avg_humi</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="c1">// 这里执行 HTTP POST 到服务器
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="c1">// ... 实现 HTTP 请求代码 ...
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    
</span></span><span class="line"><span class="cl">    <span class="n">WiFi</span><span class="p">.</span><span class="n">disconnect</span><span class="p">(</span><span class="nb">true</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="n">WiFi</span><span class="p">.</span><span class="n">mode</span><span class="p">(</span><span class="n">WIFI_OFF</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="s">&#34;</span><span class="se">\n</span><span class="s">WiFi 连接失败，下次重试&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="nf">loop</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="c1">// 深度睡眠唤醒后不会到达 loop
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span></code></pre></div><h3 id="74-功耗分析与优化">7.4 功耗分析与优化</h3>
<p>让我们计算这个节点的功耗：</p>
<p>**深度睡眠阶段：</p>
<ul>
<li>时间：5 分钟 = 300 秒</li>
<li>电流：15μA</li>
<li>电量：15μA × 300s = 4500μA·s = 1.25μAh</li>
</ul>
<p><strong>唤醒采集阶段</strong>：</p>
<ul>
<li>时间：约 100ms</li>
<li>电流：约 40mA</li>
<li>电量：40mA × 0.1s = 4mA·s = 1.11μAh</li>
</ul>
<p><strong>上传阶段</strong>（每 12 次唤醒一次）：</p>
<ul>
<li>时间：约 3 秒</li>
<li>电流：平均约 80mA</li>
<li>电量：80mA × 3s = 240mA·s = 66.67μAh</li>
<li>平均到每次唤醒：66.67 / 12 ≈ 5.56μAh</li>
</ul>
<p>**每次唤醒周期的平均电量：1.25 + 1.11 + 5.56 = 7.92μAh</p>
<p><strong>每天的电量消耗</strong>：7.92μAh × (288 次/天 = 2281μAh = 2.28mAh/天</p>
<p><strong>两节 AA 电池的容量</strong>：约 2000mAh</p>
<p><strong>理论寿命</strong>：2000mAh / 2.28mAh/天 ≈ 877 天 ≈ 2.4 年</p>
<p>这完全满足我们的设计目标！</p>
<p>实际应用中，实际寿命可能会因为温度、电池自放电等因素略短，但两年左右是完全可以实现的。</p>
<h2 id="八进阶优化技巧总结">八、进阶优化技巧总结</h2>
<p>让我们总结一下 ESP32 低功耗设计的核心原则和进阶技巧：</p>
<h3 id="81-软件层面">8.1 软件层面</h3>
<ol>
<li><strong>选择合适的电源模式</strong>：深度睡眠是大多数应用的最佳选择。</li>
<li><strong>尽量减少唤醒频率</strong>：能 5 分钟唤醒一次就不要 1 分钟唤醒一次。</li>
<li><strong>优化唤醒后的工作流程</strong>：唤醒后尽快完成工作，尽快回到睡眠。</li>
<li><strong>优化 WiFi 连接时间</strong>：使用静态 IP、保存 BSSID、减少重连。</li>
<li><strong>合理使用 RTC 存储器</strong>：减少不必要的 Flash 写入。</li>
</ol>
<h3 id="82-硬件层面">8.2 硬件层面</h3>
<ol>
<li><strong>选择低静态电流的电源芯片</strong>：LDO 的静态电流要小于 1μA。</li>
<li><strong>使用 MOSFET 控制外设电源</strong>：不需要时完全切断传感器和模块的电源。</li>
<li><strong>移除不必要的元件</strong>：LED、USB 转串口芯片等。</li>
<li><strong>正确处理浮空引脚</strong>：确保没有引脚处于不确定的电平状态。</li>
<li><strong>选择低功耗的外设</strong>：选择待机电流低的传感器和模块。</li>
</ol>
<h3 id="83-测量与调试">8.3 测量与调试</h3>
<ol>
<li><strong>实际测量是王道</strong>：不要只看理论计算，实际测量每个阶段的电流。</li>
<li><strong>分阶段排查问题</strong>：从最简单的情况开始，逐步增加复杂度。</li>
<li><strong>记录和对比数据</strong>：记录每次修改后的功耗变化，找到最优解。</li>
</ol>
<h2 id="总结">总结</h2>
<p>ESP32 的低功耗设计是一个系统工程，涉及硬件设计、软件优化、测量调试多个环节。仅仅调用 <code>esp_deep_sleep_start()</code> 只是第一步，真正的挑战在于理解 ESP32 的电源架构、正确配置各个组件、以及细致入微的优化和调试。</p>
<p>通过本文的介绍，我们系统地学习了：</p>
<ul>
<li>ESP32 的五种电源模式及其适用场景</li>
<li>深度睡眠模式的各种唤醒源及其使用方法</li>
<li>RTC 存储器的数据持久化机制</li>
<li>GPIO 配置的常见陷阱和正确做法</li>
<li>外设使用的注意事项</li>
<li>功耗测量与调试技巧</li>
<li>完整的低功耗传感器节点实战案例</li>
</ul>
<p>掌握了这些知识，你就可以设计出真正低功耗的 ESP32 设备，让两节 AA 电池支撑设备运行数年。</p>
<p>低功耗设计的精髓在于：**每微安都要计较，每毫秒都要珍惜。在电池供电的物联网世界里，节省的每一点能量，都转化为设备更长的使用寿命和更低的维护成本。</p>
<p>希望这篇文章能帮助你在 ESP32 低功耗设计的道路上少走弯路，设计出优秀的产品。</p>
<p>（全文完，约7200字）</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
