构建一个太阳能+USB双输入 ESP32-S3 供电系统
构建一个太阳能+USB双输入 ESP32-S3 供电系统
摘要:本文记录我设计并搭建一套 ESP32-S3 太阳能供电系统的完整过程。系统支持 USB-C 快速充电(室内) 与 太阳能 MPPT 充电(户外) 双模式,共用一块聚合物锂电池。文章覆盖太阳能电池的物理模型、MPPT 的数学原理、三款主流充电管理 IC(CN3791、TP4056、CN3065/CN3165)的横向对比、双充电器直接并联的电路分析、完整的电路拓扑设计,以及 ESP32-S3 端的电源管理固件策略。适合有嵌入式硬件基础、希望搭建离网型 IoT 节点的读者。
目录
- 系统目标与设计约束
- 太阳能电池的物理模型
- MPPT:为什么不能直接把太阳能板接到电池上
- 充电管理 IC 选型对比
- 核心问题:两个充电器能并联吗
- 电路设计方案
- 功率预算分析
- ESP32-S3 电源管理固件
- 实测数据与优化
- 总结与物料清单
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 设计原则
- 双充电器不能直接并联到同一块电池(反馈环冲突)
- 太阳能路径必须实现 MPPT,否则功率损失 20%~40%
- 锂电池必须有独立的保护板(过放/过充/短路)
- 3.3V 稳压方案必须在低静态功耗和大电流输出能力之间平衡
- 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=Iph−I0[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 倍。
环境能量预测:根据历史日照数据和天气预报,动态调整上报频率和数据精度——天晴时高频全量采集,阴雨前提早进入节电模式。
更多推荐



所有评论(0)