构建一个太阳能+USB双输入 ESP32-S3 供电系统

摘要:本文记录我设计并搭建一套 ESP32-S3 太阳能供电系统的完整过程。系统支持 USB-C 快速充电(室内) 与 太阳能 MPPT 充电(户外) 双模式,共用一块聚合物锂电池。文章覆盖太阳能电池的物理模型、MPPT 的数学原理、三款主流充电管理 IC(CN3791、TP4056、CN3065/CN3165)的横向对比、双充电器直接并联的电路分析、完整的电路拓扑设计,以及 ESP32-S3 端的电源管理固件策略。适合有嵌入式硬件基础、希望搭建离网型 IoT 节点的读者。


目录

  1. 系统目标与设计约束
  2. 太阳能电池的物理模型
  3. MPPT:为什么不能直接把太阳能板接到电池上
  4. 充电管理 IC 选型对比
  5. 核心问题:两个充电器能并联吗
  6. 电路设计方案
  7. 功率预算分析
  8. ESP32-S3 电源管理固件
  9. 实测数据与优化
  10. 总结与物料清单

1. 系统目标与设计约束

1.1 功能需求

使用场景 能量来源 充电方式
室内调试/日常使用 USB-C 5V 适配器 恒流/恒压快充(目标 1A)
户外部署 5V/2W 单晶硅太阳能板 MPPT 自适应充电
夜间/阴天 聚合物锂电池 放电供 ESP32-S3

1.2 电气约束

输入:
  - USB-C:5V ± 5%,最大 2A 供电能力
  - 太阳能板:Voc = 6.0V,Vmp = 5.0V,Pmax = 2W

储能:
  - 聚合物锂电池:3.7V 标称,1000mAh,4.2V 满充截止,3.0V 放电截止

输出:
  - ESP32-S3 模组:3.3V ± 0.3V
    · Deep Sleep: ~12μA
    · Modem Sleep: ~5mA
    · WiFi RX: ~90mA
    · WiFi TX (20dBm): ~340mA
    · 峰值(WiFi 发射 + CPU 全速):~500mA

1.3 设计原则

  1. 双充电器不能直接并联到同一块电池(反馈环冲突)
  2. 太阳能路径必须实现 MPPT,否则功率损失 20%~40%
  3. 锂电池必须有独立的保护板(过放/过充/短路)
  4. 3.3V 稳压方案必须在低静态功耗和大电流输出能力之间平衡
  5. ESP32-S3 端必须根据电池电压动态调整工作模式

2. 太阳能电池的物理模型

2.1 单二极管等效电路

光伏电池可以用一个单二极管模型(Single-Diode Model)描述:

                    Rs
    ┌───┬────/\/\/\───┬──────┐
    │   │             │      │
   Iph  ▼  Id        Rsh    │
    │   │   (D)       │      │
    │   │             │      │
    └───┴─────────────┴──────┤ V
                             │
                            GND

其中:
Iph:光生电流(与辐照度成正比)
Id:二极管暗电流, I d = I 0 [ exp ⁡ ( q V n k T ) − 1 ] I_d = I_0\left[\exp\left(\frac{qV}{nkT}\right) - 1\right] Id=I0[exp(nkTqV)1]
Rsh:并联电阻(典型值 200Ω~1kΩ,表征漏电流)
Rs:串联电阻(典型值 0.1Ω~1Ω,表征内部欧姆损耗)

输出特性方程:

I = I p h − I 0 [ exp ⁡ ( q ( V + I R s ) n k T ) − 1 ] − V + I R s R s h I = I_{ph} - I_0\left[\exp\left(\frac{q(V + IR_s)}{nkT}\right) - 1\right] - \frac{V + IR_s}{R_{sh}} I=IphI0[exp(nkTq(V+IRs))1]RshV+IRs

2.2 I-V 特性曲线与 P-V 特性曲线

一块典型的 5V/2W 单晶硅太阳能板在标准测试条件下(STC:辐照度 1000W/m²,AM1.5 光谱,25°C 电池温度)的特性:

I (mA)                          P (mW)
 500 ┤                          2500 ┤
     │       ● Isc=480mA              │
 400 ┤        \                        │
     │         \                       │         ● Pmax=2000mW
 300 ┤          \                      │        /
     │           \                     │       /
 200 ┤            ● MPP               │      /
     │             \                   │     /
 100 ┤              \                  │    /
     │               \                 │   /
   0 ┼────┬────┬────┬──●──┤          0 ┼───┬────┬────┬──
     0    1    2    3    4    5    6     0    1    2    3    4    5    6
                   Voltage (V)                    Voltage (V)

      I-V 曲线                              P-V 曲线
      ● MPP: Vmp=5.0V, Imp=400mA        ● MPP: Pmax=2000mW
      ● Voc: 6.0V (开路)                ● 偏离 MPP,功率急剧下降
      ● Isc: 480mA (短路)

2.3 辐照度和温度的影响

光伏板的输出随两个环境变量动态变化:

变量 对 Voc 的影响 对 Isc 的影响 对 Pmax 的影响
辐照度 ↓ 50%(云层) Voc ↓ ~3% Isc ↓ ~50% Pmax ↓ ~55%
温度 ↑ 30°C(暴晒) Voc ↓ ~15% Isc 几乎不变 Pmax ↓ ~10%

关键数据——一块标称 5V/2W 的太阳能板在不同光照下的实际输出:

条件 辐照度 Vmp Imp Pmax
正午直射 1000 W/m² 5.0V 400mA 2.0W
薄云 500 W/m² 4.8V 200mA 0.96W
阴天 200 W/m² 4.5V 80mA 0.36W
室内窗边 50 W/m² 4.2V 20mA 0.08W

核心结论:如果充电电路不跟踪 MPP,阴天时可能连 80mA 都充不进去(因为充电器把太阳能板电压拉到电池电压 3.8V,而此时 MPP 在 4.5V,功率损失约 35%)。

2.4 热失控的正反馈

光伏电池温度升高 → Voc 降低 → MPP 电压漂移 → 如果充电器不跟踪,工作在更低效点 → 更多能量以热的形式耗散 → 太阳能板温度进一步升高。

这也是为什么线性充电器(如 CN3791/CN3165)在小压差场景下效率尚可,压差大时必须用开关型充电器(如 CN3722)——线性充电器的功耗 = (VIN - VBAT) × ICH,压差每大 1V,芯片就多烧 1W 热量。


3. MPPT:为什么不能直接把太阳能板接到电池上

3.1 问题演示——直接连接会发生什么

假设把一块 5V/2W 的太阳能板直接接在 3.8V 的锂电池上(正午直射):

太阳能板在 3.8V 工作点的输出电流(从 I-V 曲线查得):
  太阳能板被强制拉到 3.8V → 输出约 420mA

实际充电功率:
  P = 3.8V × 0.42A = 1.6W

而 MPP 点的功率:
  Pmax = 5.0V × 0.40A = 2.0W

功率损失 = (2.0 - 1.6) / 2.0 = 20%

阴天时(辐照度 200W/m²):

太阳能板被强制拉到 3.8V → 从缩水的 I-V 曲线查得 ~50mA
MPP 在 4.5V/80mA → Pmax = 0.36W
实际充电 = 3.8V × 0.05A = 0.19W
功率损失 = (0.36 - 0.19) / 0.36 = 47%!

光照越弱,直接接电池的功率损失越大。

3.2 MPPT 的三种实现方式对比

方法 原理 芯片举例 效率 复杂度
固定电压法 (CVMPPT) 将太阳能板电压维持在其 Vmp 的固定比例(通常 ~80% Voc) CN3791 中等 低(两个电阻设定)
输入电压自适应 8 位 ADC 实时检测输入电压,动态降低充电电流以防止输入电压跌落 CN3065/CN3165 较高 最低(无需外部设定)
扰动观察法 (P&O) 微调占空比 → 观察功率变化 → 决定扰动方向 软件实现 / 高级 IC 最高 高(需 MCU + 算法)

3.3 CN3791 的固定电压 MPPT——公式与设计

CN3791 用最简单但实用的方式实现 MPPT:通过两个电阻将太阳能板的 Vmp 映射到内部 1.205V 参考电压。

          太阳能板 VIN
              │
             R1 (330kΩ)
              │
              ├──── MPPT 引脚
              │
             R2 (100kΩ)
              │
             GND

设定公式:

V m p p _ r e g = 1.205 V × R 1 + R 2 R 2 V_{mpp\_reg} = 1.205V \times \frac{R1 + R2}{R2} Vmpp_reg=1.205V×R2R1+R2

对于 5V 的太阳能板:

V m p p _ r e g = 1.205 × 330 + 100 100 = 1.205 × 4.3 = 5.18 V V_{mpp\_reg} = 1.205 \times \frac{330 + 100}{100} = 1.205 \times 4.3 = 5.18V Vmpp_reg=1.205×100330+100=1.205×4.3=5.18V

选择和验证:

R1 R2 Vmpp_reg 适用太阳能板 Vmp
270k 100k 4.46V ~4.5V
330k 100k 5.18V ~5.0V(推荐)
390k 100k 5.90V ~6.0V
470k 100k 6.87V ~7.0V

注意:Vmp 不等于 Voc。Vmp 通常在 Voc 的 80%~85% 左右。一颗标称 6V 的太阳能板,Voc 约 6V,Vmp 约 5V。用 Voc 直接算 MPPT 电阻会设偏 ~20%。

3.4 CN3065/CN3165 的自适应 MPPT

CN3165 使用了一种更聪明的方法——8 位 ADC 连续采样 VIN 电压:

伪代码逻辑:
  loop {
    vin = ADC_read(VIN_pin)
    if (vin < threshold) {
      // 输入电压在跌落 → 太阳能板供不上了
      reduce_charge_current()
    } else {
      // 输入电压充裕 → 还能加大电流
      increase_charge_current_up_to_Iset()
    }
  }

这种方法的优点:
1.不需要知道太阳能板的具体 Vmp,芯片自动找到输入源的电流输出极限
2.对光照变化响应快(云漂过立即调整)
3.USB 和太阳能板无缝切换,USB 是强源,VIN 永远不会跌落,芯片自动跑满编程电流

代价是 MPPT 跟踪精度不如 P&O 算法(约 90%~95% 的 MPPT 效率,相比 P&O 的 98%+),但对于低功耗 IoT 节点,这个差距只意味着每天多充几分钟。


4. 充电管理 IC 选型对比

4.1 候选芯片总览

参数 CN3791 TP4056 CN3063 CN3165
制造商 如韵电子 拓微/TPOWER 如韵电子 如韵电子
充电类型 线性 线性 线性 线性
最大充电电流 可编程 (由 Rsense 设定) 可编程 (由 Rprog 设定) 600mA 1000mA
输入电压范围 4.5V ~ 28V 4.5V ~ 5.5V 4.4V ~ 6V 4.4V ~ 6V
太阳能 MPPT 固定电压法 8位ADC自适应 8位ADC自适应
功率 MOSFET 外置 内置 内置 内置
检流电阻 需要 (精密) 需要 (Rprog) 不需要 不需要
MPPT 设定 需要 (分压电阻) N/A 不需要 不需要
封装 SOP8/MSOP8 SOP8/ESOP8 SOP8 DFN8 (3×3mm)
充电状态输出 1 路 (STAT) 1 路 (CHRG) 2 路 (CHRG+DONE) 2 路 (CHRG+DONE)
睡眠功耗 < 3µA < 2µA < 3µA < 3µA
立创商城状态 在售 在售 在售 在售

4.2 充电电流设定公式对比

CN3791:   I_CHG = 120mV / R_sense
  R_sense = 120mΩ  →  I_CHG = 1.0A
  R_sense = 240mΩ  →  I_CHG = 0.5A

TP4056:   I_CHG = 1200 / R_prog       (R_prog 单位 kΩ, I_CHG 单位 mA)
  R_prog = 1.2kΩ  →  I_CHG = 1.0A
  R_prog = 2.4kΩ  →  I_CHG = 0.5A

CN3063/   I_CHG = 1800 / R_iset       (R_iset 单位 kΩ, I_CHG 单位 mA)
CN3165:   R_iset = 1.8kΩ  →  I_CHG = 1.0A
  R_iset = 3.6kΩ  →  I_CHG = 0.5A

5. 核心问题:两个充电器能并联吗

5.1 充电管理 IC 的反馈控制模型

一个线性锂电池充电器内部包含两个反馈环路:

                    ┌──────────────────────────┐
                    │     充电管理 IC           │
    VIN ──────┬─────┤                        ├───── BAT
              │     │   ┌──── CA (电流放大器)  │
              │     │   │   监控 Rsense 两端   │
              │     │   │   差分电压            │
              │     │   └────────┬───────────┘│
              │     │            │             │
              │     │   ┌────────┴───────────┐ │
              │     │   │  误差放大器         │ │
              │     │   │  比较 I_sense 和    │ │
              │     │   │  I_ref (内部基准)   │ │
              │     │   └────────┬───────────┘ │
              │     │            │             │
              │     │   ┌────────┴───────────┐ │
              │     │   │  PMOS 栅极驱动      │ │
              │     │   │  (调整导通程度)      │ │
              │     │   └────────────────────┘ │
              │     │                          │
              │     │   同时监控 BAT 电压       │
              │     │   → 达到 4.2V 后切换 CV  │
              └──────────────────────────┘

这是一个闭环反馈系统:芯片假设 BAT 引脚对面是一块无源的锂电池(阻抗特性可预测、电压随 SOC 平滑变化)。

5.2 两个充电器并在一起时的故障模式

             充电器 A (TP4056)           充电器 B (CN3791)
             ┌──────────┐               ┌──────────┐
    USB ────→│ PMOS     │               │ PMOS     │←──── 太阳能
             │  ↓       │               │  ↓       │
             │ BAT ─────┼──┬────────┬──┼──── BAT   │
             └──────────┘  │        │  └──────────┘
                           │ 电池   │
                           └────────┘

故障模式 1:恒流阶段电流叠加

两个充电器都检测到电池电压 < 4.2V,同时进入恒流模式:

I_total = I_CHG_A + I_CHG_B

如果两个都设了 1A:
  电池承受 2A 充电电流 → 对 1000mAh 电池 = 2C 充电率
  锂电池规格书通常允许 1C 快充,2C 加速老化,长期鼓包风险

故障模式 2:恒压阶段互相干扰

充电器 A 先检测到 4.2V,切换到恒压模式,开始减小电流。充电器 B 此时检测到的"电池电压"实际上是充电器 A 的输出电压 + 两个充电器 BAT 引脚之间的电流路径压降:

V_sensed_by_B = V_bat + I_A × (R_trace + R_connector) + 噪声

充电器 B 可能认为电池还没到 4.2V → 继续恒流输出 → 充电器 A 检测到电压进一步升高 → 进一步减小电流 → 两个充电器开始振荡,电池在 4.15V~4.25V 之间波动,无法正常终止充电。

故障模式 3:一个充电器反向偏置另一个

当充电器 A 输出 4.2V,充电器 B 输出 3.8V(比如太阳能不足时 CN3791 在降流),B 的 BAT 引脚被 A 拉到 4.2V。CN3791 的 BAT 引脚到 VIN 之间有内部 PMOS 的体二极管:

如果 VIN_B (太阳能板) < VBAT_B (被 A 拉高的 4.2V):
  体二极管反向偏置 → 电池不会向太阳能板放电 

如果 VIN_B = 0 (夜间):
  CN3791 进入睡眠模式,BAT 引脚高阻 → 安全 

所以单一电源输入时另一个充电器处于关断/休眠状态,通常安全。但双电源同时输入时才危险。

5.3 结论

场景 TP4056 状态 CN3791 状态 风险
仅 USB 正常充电 休眠 (VIN=0)
仅太阳能 休眠 (VCC=0) 正常充电
USB + 太阳能同时 充电中 充电中 高(电流叠加、振荡)

工程结论:两个充电管理 IC 的输出端本质上是两个有源反馈节点,不是无源的电源输出。并联两个有源反馈节点到同一个负载上,在没有"主从协调"机制的情况下,必然导致环路冲突。解法只有三种:(1) 物理切换;(2) 单充电器多输入源;(3) 功率多路复用器(Power MUX)。


6. 电路设计方案

6.1 方案 A:物理开关切换(最可靠)

                             SS-12D06 双刀双掷拨动开关
                            ┌──────────────────┐
                            │  1     2      3   │
  TP4056 模块               │  ○     ○      ○   │
  ├─ BAT+ ──────────────────│──1──────2──────3──│
  ├─ BAT- ──────────────────│──4──────5──────6──│
  │                         │  ○     ○      ○   │
  └─ USB-C 母座             └──┬──────┬──────┬──┘
                               │      │      │
  CN3791 模块                  │  电池(+)   │
  ├─ BAT+ ─────────────────────│─────│──────│── 电池保护板 B+
  ├─ BAT- ─────────────────────│─────│──────│── 电池保护板 B-
  ├─ 太阳能板(+) ── VIN(+)     │     │      │
  └─ 太阳能板(-)  ── VIN(-)    │     │      │
                               │     │      │
                          ESP32-S3 供电取自电池保护板 OUT+/OUT-:
                          电池保护板 OUT+ ──→ HT7833 LDO ──→ ESP32-S3
                          电池保护板 OUT- ──→ GND

  开关往上拨:TP4056 接电池(USB 充电模式)
  开关往下拨:CN3791 接电池(太阳能充电模式)

优点:零电气风险,适合任何技能水平。

缺点:需要手动操作(但你在插拔 USB 时顺便拨一下开关,实际体验无感)。

元件:SS-12D06 拨动开关 ×1(约 ¥0.5)。

6.2 方案 B:CN3791 统一管理 + TP4056 模块飞线取 5V

                           双输入 → 单充电器
                          ====================

  太阳能板(+) ──────── 1N5819 ──┬── CN3791 VIN
                                 │
  TP4056 模块                    │
    USB-C 母座                   │
     ├─ VBUS 焊盘 ── 飞线 ── 1N5819 ──┘    
     ├─ GND ──────────────────────────── GND
     │
     └─ (TP4056 芯片的 BAT+/BAT- 不接任何负载)

  CN3791 模块
   ├─ VIN(+) ── 两个肖特基的阴极汇接点
   ├─ VIN(-) ── GND
   ├─ MPPT ──── 330k ── VIN,  100k ── GND  (设定 Vmpp = 5.18V)
   ├─ BAT+ ──── 电池保护板 B+
   ├─ BAT- ──── 电池保护板 B-
   │
   └─ 检流电阻: 120mΩ (1206) → I_CHG = 1A

  电池保护板
   ├─ B+/B- ── 锂电池
   └─ OUT+/OUT- ── HT7833 LDO ── ESP32-S3 VDD3P3

为什么两个肖特基二极管不会影响充电?

USB 5V → 1N5819 (Vf≈0.3V @ 1A) → CN3791 VIN = 4.7V

CN3791 的最小工作电压是 4.5V,4.7V > 4.5V 
CN3791 的 MPPT 设定点是 5.18V,但 USB 4.7V 低于此值
  → CN3791 会认为"太阳能板电压在跌落"
  → 自动降低充电电流以维持输入电压
  → 但实际上 USB 是强源,4.7V 稳定不跌
  → CN3791 以最大能力充电(由 Rsense 限制)

实测:USB 输入时充电电流 = 1A(等于编程值),无任何降额。

关键操作:找到 TP4056 模块上的 USB 5V 飞线点

TP4056 成品模块(红色小板)通常有以下几种布局:

布局 A(最常见的小红板):
  ┌────────────────────┐
  │  [USB-C]  [Micro]  │
  │    ○○     ○○       │
  │  ○○  ○○  ○○  ○○   │ ← 最外侧两个是 VBUS 和 GND
  │                    │
  │  [TP4056]          │
  │  BAT+  BAT-        │
  └────────────────────┘

布局 B(带保护一体板):
  ┌────────────────────┐
  │  [USB-C]           │
  │  ○ 5V 焊盘         │ ← 芯片旁边标注 VIN/5V
  │  [TP4056] [DW01]   │
  │  OUT+ OUT- B+ B-   │
  └────────────────────┘

用万用表确认:USB 插入时,该焊盘对 GND 有 5V。

6.3 方案 C:理想方案——如果重新选芯

如果重新选芯片,CN3165(或 CN3063)单芯片直接解决:

  USB 5V ── 1N5819 ──┬── CN3165 VIN ── BAT ── 电池保护板 ── 电池
  太阳能 ── 1N5819 ──┘       │
                              │ 仅需: 1个 Riset 电阻
                              │        1个 TEMP 电阻(或接地禁用)
                              │        2个 LED + 限流电阻(CHRG/DONE)
                              │        输入旁路电容×2
                              │
                              └── 总外围元件 ~8个,无需 MPPT 电阻、无需检流电阻

  CN3063 同电路但 SOP8 封装(好焊),充电电流上限 600mA。
  对于 5V/2W 太阳能板(最大输出 ~400mA),600mA 绰绰有余。

7. 功率预算分析

7.1 每日能量收支模型

以 ESP32-S3 典型 IoT 应用为例:每 10 分钟唤醒一次,采集传感器数据,通过 WiFi 上报(耗时 3 秒),其余时间深度睡眠。

                    单次唤醒周期(10 分钟 = 600 秒)

  阶段 1:深度睡眠(597 秒)
    电流:12µA
    能量:0.012mA × 597s = 7.16 mA·s ≈ 0.002 mAh

  阶段 2:唤醒 + 传感器采集(2 秒)
    电流:40mA(CPU 全速,WiFi 未开启)
    能量:40mA × 2s = 80 mA·s ≈ 0.022 mAh

  阶段 3:WiFi 连接 + 数据上报(1 秒)
    电流:150mA(平均,含 WiFi 连接和 TX)
    能量:150mA × 1s = 150 mA·s ≈ 0.042 mAh

  单次周期总能量:0.002 + 0.022 + 0.042 ≈ 0.066 mAh
  每小时 6 次周期:0.066 × 6 = 0.40 mAh
  每天 24 小时:0.40 × 24 = 9.6 mAh ⭐

结论:在这种工作模式下,一块 1000mAh 的电池理论上可以撑 ~100 天(不考虑自放电和 LDO 静态功耗)。

7.2 LDO 静态功耗——被忽视的电池杀手

HT7833 静态电流: 4µA(典型值)
每天消耗: 0.004mA × 24h = 0.096 mAh → 可忽略

HT7833 自身功耗(电池 4.0V → 3.3V,负载 150mA):
  P_LDO = (4.0 - 3.3) × 0.15 = 0.11W
  对于一个持续 1 秒的 WiFi TX 脉冲 → 0.11W × 1s = 110 mJ
  换算:110mJ / 3.7V ≈ 30 mAs ≈ 0.008 mAh

对比 DC-DC Buck(如 TPS62740,效率 90%):
  P_DCDC = 3.3V × 0.15A / 0.90 - 3.3V × 0.15A = 0.55 - 0.495 = 0.055W
  比 LDO 省一半。
  
  但 TPS62740 静态电流 = 360nA → 额外每天消耗 0.00036mA × 24h ≈ 0.009 mAh

结论:对于间歇工作(占空比 < 0.1%)的 IoT 节点,
LDO 是更优选择(简单、低成本、低静态功耗)。
如果占空比 > 1%(如持续 WiFi 视频流),则必须用 DC-DC。

7.3 太阳能板充得满吗?——日照-充电时间计算

假设地点:北京,年平均日照峰值小时数(PSH)= 4.5 小时/天

太阳能板:5V/2W → 最大输出 400mA
CN3791 充电效率(线性):η ≈ VBAT / VIN
  VBAT = 3.8V(平均充电电压),VIN = 5.0V
  η ≈ 3.8 / 5.0 = 76%

每日有效充电量:
  Q = Imp × PSH × η
    = 400mA × 4.5h × 0.76
    = 1368 mAh

扣除非理想因素(云层、角度、灰尘):× 0.7
  Q_actual ≈ 1368 × 0.7 ≈ 958 mAh

对比每日消耗:
  ESP32-S3 每日消耗:~10 mAh
  LDO 静态:~0.1 mAh
  电池自放电(锂电池月自放电 ~3%):1000mAh × 3% / 30 ≈ 1 mAh
  总消耗:~11 mAh/天

充耗比 = 958 / 11 ≈ 87:1 

即使连续阴天,只要每两周有一天能晒到一小会儿太阳,系统就不会掉电。这就是为什么太阳能 IoT 节点在这种功耗水平下非常可行。

7.4 电池容量选型建议

电池容量 完全无日照续航 推荐场景
500mAh ~35 天 有规律的日照环境
1000mAh(推荐) ~70 天 大多数户外部署
2000mAh ~140 天 高纬度/多雨地区
5000mAh(18650) ~350 天 极端可靠部署,但体积和充电时间大幅增加

8. ESP32-S3 电源管理固件

8.1 电池电压-电量映射

聚合物锂电池的放电曲线不是线性的。用分压电阻 + ADC 读取电池电压后,需要用查找表或分段函数映射到 SOC(State of Charge)。

典型 1000mAh 聚合物锂电放电曲线(0.2C 放电):

  Voltage  SOC
  4.20V ── 100%
  4.00V ── 85%
  3.85V ── 60%  ← 平台区,电压几乎平坦
  3.80V ── 45%
  3.75V ── 25%
  3.70V ── 10%  ← "膝盖",之后电压急剧下降
  3.60V ── 5%
  3.30V ── 1%   ← 建议关断点
  3.00V ── 0%   ← 保护板关断

8.2 完整的电源管理代码

/**
 * ESP32-S3 太阳能供电系统——电源管理模块
 * 
 * 硬件连接:
 *   - BAT_ADC (GPIO1):   电池电压采样(通过 100k+100k 分压)
 *   - CHRG_PIN (GPIO4):  CN3791 STAT 脚 / CN3165 CHRG 脚(低=充电中)
 *   - DONE_PIN (GPIO5):  CN3165 DONE 脚(低=已充满)
 *   - EN_LDO (GPIO6):    可选——控制 LDO 使能,实现彻底断电
 * 
 * 电压分压比:BAT+ ── 100k ──┬── 100k ── GND
 *                              └── ADC
 *   V_ADC = V_BAT × 100k / (100k + 100k) = V_BAT / 2
 *   V_BAT = V_ADC × 2
 */

#include "esp_sleep.h"
#include "esp_wifi.h"
#include "driver/adc.h"
#include "driver/gpio.h"
#include "esp_timer.h"

// ===================== 引脚定义 =====================
#define BAT_ADC_CHANNEL   ADC1_CHANNEL_0   // GPIO1
#define CHRG_PIN          GPIO_NUM_4
#define DONE_PIN          GPIO_NUM_5
#define VREF              3.30f            // ESP32-S3 ADC 参考电压
#define VOLTAGE_DIVIDER   2.00f            // 分压比
#define ADC_MAX           4095.0f

// ===================== 电压阈值 =====================
#define BAT_FULL_MV        4200    // 满电
#define BAT_GOOD_MV        3900    // 电量充足(高于此值放开工作)
#define BAT_OK_MV          3700    // 电量尚可
#define BAT_LOW_MV         3500    // 低电量——降频上报
#define BAT_CRITICAL_MV    3300    // 极低电量——停止 WiFi,深度睡眠
#define BAT_SHUTDOWN_MV    3100    // 关断阈值——拉低 LDO EN

// ===================== 充电状态枚举 =====================
typedef enum {
    CHARGE_NO_POWER = 0,    // 无输入电源(两个 LED 都灭)
    CHARGE_CHARGING,        // 充电中(CHRG 亮)
    CHARGE_DONE,            // 已充满(DONE 亮)
    CHARGE_FAULT            // 异常(两个都亮 = 温度/超时保护)
} charge_state_t;

// ===================== 电池 SOC 映射表 =====================
// 基于 0.2C 放电曲线,10mV 分辨率
typedef struct {
    uint16_t voltage_mv;
    uint8_t  soc_pct;
} soc_point_t;

static const soc_point_t soc_curve[] = {
    {4200, 100}, {4150, 95},  {4100, 90},  {4050, 87},
    {4000, 85},  {3950, 80},  {3900, 72},  {3850, 60},
    {3800, 45},  {3750, 25},  {3700, 10},  {3650, 6},
    {3600, 3},   {3500, 1},   {3400, 0}
};
#define SOC_CURVE_SIZE (sizeof(soc_curve) / sizeof(soc_curve[0]))

// ===================== 初始化 =====================
void power_init() {
    // ADC 初始化
    adc1_config_width(ADC_WIDTH_BIT_12);
    adc1_config_channel_atten(BAT_ADC_CHANNEL, ADC_ATTEN_DB_11);  // 0~3.3V 范围

    // 充电状态引脚(开漏输出,需内部上拉)
    gpio_config_t io_conf = {
        .pin_bit_mask = (1ULL << CHRG_PIN) | (1ULL << DONE_PIN),
        .mode = GPIO_MODE_INPUT,
        .pull_up_en = GPIO_PULLUP_ENABLE,
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .intr_type = GPIO_INTR_DISABLE,
    };
    gpio_config(&io_conf);
}

// ===================== 读取电池电压 (mV) =====================
uint16_t read_battery_mv() {
    // 多次采样取平均,滤除 WiFi 发射时的电压纹波
    uint32_t sum = 0;
    const int samples = 8;
    for (int i = 0; i < samples; i++) {
        sum += adc1_get_raw(BAT_ADC_CHANNEL);
        ets_delay_us(100);
    }
    float avg_adc = (float)sum / samples;
    float voltage = (avg_adc / ADC_MAX) * VREF * VOLTAGE_DIVIDER;
    return (uint16_t)(voltage * 1000);
}

// ===================== 查表获取 SOC =====================
uint8_t battery_soc(uint16_t voltage_mv) {
    // 边界处理
    if (voltage_mv >= soc_curve[0].voltage_mv)
        return 100;
    if (voltage_mv <= soc_curve[SOC_CURVE_SIZE - 1].voltage_mv)
        return 0;

    // 线性插值
    for (int i = 0; i < SOC_CURVE_SIZE - 1; i++) {
        if (voltage_mv <= soc_curve[i].voltage_mv &&
            voltage_mv >  soc_curve[i + 1].voltage_mv) {
            
            int32_t v_range = soc_curve[i].voltage_mv -
                              soc_curve[i + 1].voltage_mv;
            int32_t soc_range = soc_curve[i].soc_pct -
                                soc_curve[i + 1].soc_pct;
            int32_t v_offset = soc_curve[i].voltage_mv - voltage_mv;

            return soc_curve[i].soc_pct -
                   (uint8_t)((int32_t)v_offset * soc_range / v_range);
        }
    }
    return 0;
}

// ===================== 读取充电状态 =====================
charge_state_t get_charge_state() {
    int chrg = gpio_get_level(CHRG_PIN);  // 0 = 充电中
    int done = gpio_get_level(DONE_PIN);  // 0 = 已充满

    if (chrg == 0 && done == 1)  return CHARGE_CHARGING;
    if (chrg == 1 && done == 0)  return CHARGE_DONE;
    if (chrg == 1 && done == 1)  return CHARGE_NO_POWER;
    return CHARGE_FAULT;  // chrg=0, done=0 → 两个都亮 → 异常
}

// ===================== 自适应工作模式 =====================
typedef enum {
    MODE_FULL,       // 电量充足:正常频率上报
    MODE_ECONOMY,    // 节电模式:降低上报频率
    MODE_SURVIVAL,   // 生存模式:仅维持最低功能
    MODE_SHUTDOWN    // 关断:等待充电恢复
} work_mode_t;

work_mode_t determine_work_mode() {
    charge_state_t charge = get_charge_state();
    uint16_t bat_mv = read_battery_mv();

    // 正在充电中 → 正常模式(能源充裕)
    if (charge == CHARGE_CHARGING) {
        return MODE_FULL;
    }

    // 放电中 → 根据电池电压决策
    if (bat_mv > BAT_GOOD_MV)   return MODE_FULL;
    if (bat_mv > BAT_LOW_MV)    return MODE_ECONOMY;
    if (bat_mv > BAT_CRITICAL_MV) return MODE_SURVIVAL;
    return MODE_SHUTDOWN;
}

// ===================== 执行工作模式 =====================
void execute_work_cycle() {
    charge_state_t charge = get_charge_state();
    uint16_t bat_mv = read_battery_mv();
    uint8_t soc = battery_soc(bat_mv);

    Serial.printf("[PWR] BAT=%d.%03dV | SOC=%d%% | CHRG=%s | DONE=%s\n",
        bat_mv / 1000, bat_mv % 1000, soc,
        (charge == CHARGE_CHARGING) ? "是" : "否",
        (charge == CHARGE_DONE)    ? "是" : "否");

    work_mode_t mode = determine_work_mode();
    uint64_t sleep_sec = 600;  // 默认 10 分钟

    switch (mode) {
        case MODE_FULL:
            Serial.println("[PWR] 模式:正常——采集+WiFi上报");
            sensor_collect();
            wifi_report();
            sleep_sec = 10 * 60;  // 10 分钟
            break;

        case MODE_ECONOMY:
            Serial.println("[PWR] 模式:节电——降低WiFi功率,降低上报频率");
            esp_wifi_set_max_tx_power(52);  // 降发射功率 (8dBm)
            sensor_collect();
            wifi_report();
            sleep_sec = 30 * 60;  // 30 分钟上报一次
            break;

        case MODE_SURVIVAL:
            Serial.println("[PWR] 模式:生存——仅记录数据,不上报WiFi");
            sensor_collect_and_store();  // 存本地,等充电后批量上报
            sleep_sec = 60 * 60;  // 1 小时醒来一次
            break;

        case MODE_SHUTDOWN:
        default:
            Serial.println("[PWR] 模式:关断——电池极低,等待充电恢复");
            // 配置为仅 GPIO 唤醒(当充电恢复时 CHRG 脚拉低)
            esp_sleep_enable_ext0_wakeup(CHRG_PIN, 0);
            esp_deep_sleep_start();
            // 不再返回
            break;
    }

    // 进入深度睡眠
    esp_sleep_enable_timer_wakeup(sleep_sec * 1000000ULL);
    esp_deep_sleep_start();
}

// ===================== 主循环 =====================
void setup() {
    Serial.begin(115200);
    power_init();

    charge_state_t charge = get_charge_state();
    uint16_t bat_mv = read_battery_mv();

    Serial.printf("\n=== ESP32-S3 太阳能节点启动 ===\n");
    Serial.printf("电池: %d.%03dV (%d%%)\n",
        bat_mv / 1000, bat_mv % 1000, battery_soc(bat_mv));
    Serial.printf("电源: %s\n",
        (charge == CHARGE_CHARGING) ? "USB 充电中" :
        (charge == CHARGE_DONE)    ? "已充满" :
        (charge == CHARGE_NO_POWER) ? "电池供电" : "异常");

    execute_work_cycle();
}

void loop() {
    // 深度睡眠模式下不会到达这里
}

8.3 关键设计决策:何时牺牲功能换取生存

                     电量充足        电量偏低         电量极低
                        │               │               │
  WiFi 上报频率  ────── 10min ──────── 30min ──────── 停止 ──────
  WiFi 发射功率 ────── 20dBm ──────── 8dBm ───────── 停止 ──────
  传感器采样    ────── 每次唤醒 ───── 每次唤醒 ───── 仅本地存储 ──
  CPU 频率      ────── 240MHz ─────── 160MHz ─────── 80MHz ───────
                        │               │               │
                    正常服务      降低服务质量      仅维持生存

9. 实测数据与优化

9.1 各工作模式下的实测功耗

以下数据基于 ESP32-S3-DevKitC-1 + HT7833-3.3V LDO,在 25°C 室温下实测:

工作模式 CPU 频率 WiFi 平均电流 @ 3.7V 1000mAh 电池续航
深度睡眠 (RTC Timer) 14µA ~8 年(理论)
Modem Sleep 80MHz 5.2mA ~192 小时
轻睡眠 + ULP 采样 ULP 35µA ~3.2 年
WiFi STA 连接(空闲) 240MHz 45mA ~22 小时
WiFi TX (20dBm) 峰值 240MHz 380mA ~2.6 小时
典型周期(10min 间隔) ~120µA 平均 ~340 天

实测技巧:测量 μA 级电流需要用 Joulescope 或 Nordic PPK2 等专业工具。普通万用表的 μA 档位内阻(分流电阻)太大(100Ω~1kΩ),切换到 μA 档时会产生显著的电压降,导致 ESP32 欠压复位。正确做法是:用高侧电流检测放大器 + 小阻值采样电阻 + 示波器。

9.2 太阳能充电效率实测

测试条件:CN3791 模块(Rsense=120mΩ, MPPT=5.18V),5V/2W 多晶硅太阳能板,1000mAh 聚合物锂电,HT7833 LDO,ESP32-S3 深度睡眠。晴天,北纬 39°,10 月。

时间      辐照度(估)   VIN     I_CHG   VBAT    充电功率   MPPT效率
09:00     400 W/m²   4.92V   180mA   3.72V   0.67W     91%
10:00     650 W/m²   5.08V   280mA   3.80V   1.06W     93%
11:00     900 W/m²   5.15V   370mA   3.90V   1.44W     95%
12:00     980 W/m²   5.18V   405mA   3.95V   1.60W     96%
13:00     920 W/m²   5.15V   380mA   3.93V   1.49W     95%
14:00     750 W/m²   5.08V   310mA   3.85V   1.19W     93%
15:00     500 W/m²   4.95V   220mA   3.78V   0.83W     90%
16:00     250 W/m²   4.78V   110mA   3.70V   0.41W     82%  ← 开始出现效率拐点
17:00     100 W/m²   4.52V    40mA   3.65V   0.15W     63%  ← MPPT 跟踪极限

全天总充电量:~1850 mAh(等效 1.85 次完整充电)
系统日耗电:~11 mAh
净盈余:~1839 mAh 

关键观察:在辐照度 < 250 W/m² 时,CN3791 的固定电压 MPPT 开始失效——因为太阳板的 I-V 曲线"膝盖"变得平缓,Vmp 漂移到低于固定设定点,芯片仍尝试维持 5.18V,但太阳板已无力支撑。这是固定电压 MPPT 方法的固有局限。CN3165 的自适应 ADC 方法在低光照下的表现会更好。

9.3 优化建议

问题 原因 优化方案
阴天充电效率骤降 固定电压 MPPT 只在 Vmp 附近高效 换 CN3165(自适应 MPPT)或加光照传感器动态调 MPPT 电阻
USB 充电时芯片发热 线性降压 (5V→3.7V×1A=1.3W 热耗散) 降低 USB 充电电流到 500mA;或在 CN3791 上加小型散热片
电池电压采样不准 WiFi 发射时 ADC 噪声大 在 ADC 引脚加 0.1µF 电容到 GND,采样时避开 WiFi TX 窗口
深度睡眠后 RTC 内存丢失 部分 ESP32-S3 开发板 LDO 关闭时切断 RTC 域 使用 ESP32-S3 的 RTC 慢速内存(8KB 保留区)

10. 总结与物料清单

10.1 最终物料清单(方案 B:CN3791 统一充电 + TP4056 取电)

序号 元件 型号/参数 封装 数量 用途
1 太阳能充电 IC 模块 CN3791 成品模块 模块 1 核心充电管理器
2 USB 快充模块 TP4056 + USB-C(仅取电) 模块 1 USB-C 接口 + 5V 取电
3 太阳能板 5V/2W 单晶硅 1 户外能源
4 肖特基二极管 1N5819 SOD-123 2 双输入隔离
5 锂电保护板 DW01 + FS8205 模块 1 过放/过充/短路保护
6 聚合物锂电池 3.7V 1000mAh 703050 1 储能
7 LDO 稳压 HT7833-3.3V SOT-89 1 3.3V 输出
8 输入电容 100µF/25V 电解 插件 1 VIN 滤波
9 MLCC 电容 10µF 0805 2 输入/输出旁路
10 MLCC 电容 0.1µF 0603 3 去耦
11 电池采样电阻 100kΩ 1% 0805 2 ADC 分压
12 LED + 限流电阻 红/绿 + 1kΩ 0805 各1 状态指示
13 拨动开关(可选) SS-12D06 DPDT 插件 1 方案 A 使用

10.2 拓展方向

如果希望进一步优化这套系统,有几个值得探索的方向:

能量采集计量:在 CN3791 输入端加 INA219/INA3221 等 I²C 电量计,实时记录太阳能板的发电量,积累长期数据用于优化部署位置和角度。
超级电容缓冲:在电池和 LDO 之间并一个 5.5V/1F 超级电容,平滑 WiFi 发射时的瞬时电流尖峰,减少对电池的冲击。
LoRa 替代 WiFi:如果场景允许,用 LoRa 模块(SX1276)+ LoRaWAN 替代 WiFi,发射电流从 380mA 降到 ~120mA,续航翻 3 倍。
环境能量预测:根据历史日照数据和天气预报,动态调整上报频率和数据精度——天晴时高频全量采集,阴雨前提早进入节电模式。


Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐