硬件平台: ESP32
编程语言: MicroPython
外设元件: 10kΩ 电阻、1kΩ 电阻、100nF 电容
PD 诱骗芯片: CH224K
供电接口: USB Type-C (16P)

 

引言

在现代电子设备中,支持 USB Power Delivery(PD)协议的电源系统越来越普及。通过 PD 协议,设备可以在 5V、9V、12V、15V 和 20V 等多个电压档位之间动态切换,极大提升了供电灵活性。

本文介绍如何使用 ESP32 搭载 CH224K PD 诱骗芯片,结合简单的分压电路与软件滤波算法,在 MicroPython 环境下构建一个高精度、可校准的电压测量系统。该系统不仅能实时监测当前电压,还支持基于 PD 供电环境的自动/手动校准,适用于电源模块测试、快充适配器验证等场景。


一.硬件设计

1.分压电路原理图

2.分压电路原理

ESP32 内置 ADC 的最大输入电压约为 3.3V(启用衰减后可达 ~2V),无法直接测量 PD 输出的高电压(最高 20V)。因此采用 电阻分压电路

  • R1 = 10kΩ(接高压端)
  • R2 = 1kΩ(接地)
  • ADC 采样点位于 R1 与 R2 之间

分压比计算如下:

Vout = Vin × (R2 / (R1 + R2)) = Vin × (1k / 11k) ≈ Vin / 11

即:输入电压被衰减约 11 倍后送入 ADC。

理论分压比:11:1

3.滤波设计

为抑制高频噪声和开关电源纹波,在分压节点与地之间并联一个 100nF 陶瓷电容,构成 RC 低通滤波器,提升 ADC 采样的稳定性。

4.CH224K接线原理图

5.CH224K数据手册原理图

        

 

6.PD 电压控制:CH224K 芯片

本系统使用 CH224K 作为 PD 诱骗芯片,通过控制其 CFG1、CFG2、CFG3 引脚电平,可主动请求电源端输出指定电压档位(5V、9V、12V、15V、20V)。

ESP32 的 GPIO25、GPIO32、GPIO27 分别连接 CH224K 的 CFG 引脚,实现电压档位切换:

self.voltage = {
    20: (0, 1, 0),
    15: (0, 1, 1),
    12: (0, 0, 1),
    9:  (0, 0, 0),
    5:  (1, 0, 0)
}

⚙️ 工作流程: ESP32 设置 GPIO → CH224K 发送 PD 请求 → 电源调整输出电压


二.软件架构概述

整个系统基于 MicroPython 实现,主要类如下:

功能

File

JSON 配置文件读写,持久化存储校准参数

ADCFilter

滑动窗口 + 去除极值的均值滤波

TimeMs

非阻塞毫秒级定时器

PowerManagement

控制 CH224K 设置 PD 输出电压

VoltageDetection

主电压检测类,集成采样、滤波、校准、匹配逻辑

VoltageCalibration

提供自动/手动校准功能


三.核心代码解析

1. 配置文件管理:File 类

        系统使用 set.json(可以更改) 存储最大电压和校准系数:

file = File('set.json')  # 可以更改文件路径
if not file:
    data = {
        'Vmaximum': 20,
        'Vcalibration': {
            '20': 11.10553,
            '15': 11.3803,
            '12': 11.63826,
            '9': 12.09389,
            '5': 13.40413
        },
    }
    file.update(data)
    file.write()

⚠️ 实际分压比因电阻误差、ADC 偏移等需通过校准获得。


2. ADC 滤波:ADCFilter 类

采用 滑动窗口 + 去除最大最小值 的滤波策略,有效抑制异常脉冲干扰:

def get(self):
    if len(self._data) <= 2:
        return sum(self._data) / len(self._data)
    sorted_data = sorted(self._data)
    return sum(sorted_data[1:-1]) / (len(sorted_data) - 2)

3. 电压检测主类:VoltageDetection

核心功能包括:

  • 初始化 ADC(ADC.ATTN_6DB,量程 ~2V)
  • 实时采样与滤波
  • 动态匹配当前电压档位
  • 加载对应校准系数
def get_ADC(self):
    return self._filter.update(self._ADC.read()) * (2.0 / 4095)

def get(self):
    return round(self.get_ADC() * self._calibration, 1)

4. 电压校准机制(基于 PD 供电)

        由于硬件差异,理想分压比(11)不准确。系统提供两种校准方式:

自动校准(不推荐)

VC=VoltageCalibration()
VC.automatic()

原理:

        系统依次切换到 5V、9V、12V、15V、20V 档位,读取当前电压并直接输入当前电压的值,(如:获取5V电压,直接输入5)自动记录校准值。

手动校准(推荐)

VC = VoltageCalibration()
VC.manual()

流程:

  1. 系统通过 CH224K 切换到目标电压档位(如 12V)
  2. 用户使用万用表测量实际输出电压
  3. 输入实测值(如 12.05V)
  4. 系统自动计算并保存真实分压比

校准公式: calibration = 实际电压 / ADC 测量电压

注意:关于校准如果不需要太高精度,电阻R1,R2可以使用高精度±0.1的碳膜电阻,就不需要在进行首次运行校准。


5. 高效主循环设计

系统主循环运行效率极高,单次 PS.update() 执行时间约为 1ms,确保实时性:

Time = TimeMs(1000, 1000)
while True:
    PS.update()           # ~1ms
    if Time.update():     # 每秒触发一次
        print(PS.get())   # 输出电压
    time.sleep_ms(1)      # 轻量延时,避免 CPU 占用过高

✅ 非阻塞设计,不影响其他任务运行。


四.使用说明

1:首次运行与校准

VC = VoltageCalibration()
VC.manual()  # 推荐方式

依次设置 5V、9V、12V、15V、20V 档位,输入万用表实测值完成校准。

🔋 供电要求: 最好使用支持 PD 20V 输出的 Type-C 充电器。

2:正常运行

PS = VoltageDetection()
while True:
    PS.update()
    if timer.update():
        print("Voltage:", PS.get(), "V")
    time.sleep_ms(1)

3.电压调整

        通过VoltageDetection的Restart方法来调整 15为要设置的电压支持(5,9,12,15,20)

PS = VoltageDetection()
PS.Restart(15)  # 设置电压重启

        注意:设置新的电压使用PS.Restart方法,不要使用PS.PM.set(PS.PM.set会出现电压缓步增加或减少的输出)方法。

五.系统总结

1.优点

  • ✅ 支持 5V~20V 多档位 PD 电压测量
  • ✅ 基于 CH224K 实现主动电压切换
  • ✅ 分压电路简单,成本低
  • ✅ 软件滤波提升稳定性
  • ✅ 支持手动校准,精度高
  • ✅ 非阻塞设计,实时性强(~1ms/次)
  • ✅ 配置持久化,断电不丢失

 


2.局限性

尽管本系统已具备较高实用性,但仍存在以下局限性,未来可进一步优化:

1. ADC 精度限制

  • ESP32 的 ADC 存在非线性、温漂等问题,尤其在低电压段(如 5V)误差较明显。(可以使用外置高精度的ADC)

2. 电阻温漂影响

  • 普通贴片电阻在温度变化时阻值漂移,影响分压比稳定性。(可以使用低温漂的金属膜电阻,或高精度电阻±0.1%)

六.结语

        本文实现了一套基于 ESP32、CH224K 和 MicroPython 的 PD 电压测量系统,具备高精度、易校准、响应快等优点,特别适用于 PD 电源开发与测试场景。

        通过合理的软硬件协同设计,即使是资源有限的 MicroPython 平台,也能完成复杂的电源监测任务。未来可通过引入外部 ADC、温补算法等方式进一步提升系统性能。

七.源码

from machine import ADC, Pin, Timer
from File import file
from ADCFilter import ADCFilter
import time,json

class File(dict):
    '''文件字典'''
    def __init__(self, path, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._path = path
        self._update()

    def _update(self):
        '''更新文件数据'''
        self.clear()
        try:
            with open(self._path, 'r', encoding='utf-8') as f:
                self.update(json.load(f))
        except:
            self.write()

    def write(self):
        '''数据写入'''
        with open(self._path, 'w', encoding='utf-8') as f:
            json.dump(self, f)


file = File('set.json')

if not file:
    data = {
        'Vmaximum': 20,
        'Vcalibration': {  # 电压校准
            '20': 11.10553,
            '15': 11.3803,
            '12': 11.63826,
            '9': 12.09389,
            '5': 13.40413
        },
    }
    file.update(data)
    file.write()


class ADCFilter:
    """ADC 滤波器"""
    def __init__(self, length=10):
        '''
        初始化
        :param length: 滤波器长度
        '''
        self._length = length
        self._data = []

    def update(self, data):
        """
        更新数据
        :param data:  输入数据
        :return:  滤波后的数据
        """
        self._data.append(data)
        if len(self._data) > self._length:
            self._data.pop(0)
        return self.get()

    def get(self):
        """
        获取数据
        :return:滤波后的数据|去除最大最小值
        """
        if len(self._data) <= self._length:
            return sum(self._data) / len(self._data)
        sorted_data = sorted(self._data)  # 排序
        return sum(sorted_data[1:-1]) / (len(sorted_data) - 2)  # 去除高低各1个极值

    def clear(self):
        '''清空原有数据 用于预防突变'''
        self._data = []

class TimeMs:
    '''时间控制器'''
    def __init__(self, val: int, start=0):
        '''
        :param val: 时间间隔 毫秒
        :param start: 开始时间的起始位置-start 毫秒
        '''
        self._interval = val  # 时间间隔
        self._start = time.ticks_ms() - start  # 开始时间

    def change(self, val: int):
        '''改变时间间隔'''
        self._interval = val

    def reset(self):
        '''重置计时'''
        self._start = time.ticks_ms()  # 开始时间

    def update(self):
        '''更新时间'''
        t1 = time.ticks_ms()
        if t1 - self._start >= self._interval:
            self._start = t1
            return True
        return False

class VoltageCalibration:
    '''电压校准
    前提条件: 请确保设备由PD快充充电器供电 最好支持20V'''
    def _get_maximum_voltage(self):
        '''获取最大电压列表'''
        print('正在获取最大电压')
        # 如果配置电压大于20将不重启电压
        if not file.get('Vmaximum') >= 20:
            PS.Restart(20)  # 设置最高电压 并 重启
        timeMs = TimeMs(3000)  # 等待3秒
        while True:
            if timeMs.update():
                max_v = PS.get()  # 获取当前的电压
                return [x for x in [5, 9, 12, 15, 20] if x <= max_v + 1]
            PS.update()  # 更新电压
            time.sleep_ms(5)

    def automatic(self):
        '''自动校准'''
        max_v = self._get_maximum_voltage()  # 获取最大电压以下可以操作的列表
        for v in max_v:
            print('正在校准' + str(v) + '伏')
            timeMs = TimeMs(3000)  # 等待3秒
            PS.Restart(v)  # 设置电压重启
            while True:
                if timeMs.update():
                    PS.calibrate(v, v)
                    break
                PS.update()  # 更新电压
                time.sleep_ms(5)

        file.write()  # 保存数据
        PS.Restart()  # 设置默认电压 并 重启
        print('校准完成')

    def manual(self):
        '''手动校准'''
        max_v = self._get_maximum_voltage()  # 获取最大电压以下可以操作的列表
        for v in max_v:
            print('校准电压' + str(v) + '伏')
            timeMs = TimeMs(3000)  # 等待3秒
            PS.Restart(v)  # 设置电压重启
            while True:
                if timeMs.update():
                    PS.calibrate(v, float(input('请输入当前电压值:')))
                    break
                time.sleep_ms(1)
            PS.update()  # 更新电压
        file.write()  # 保存数据
        PS.Restart()  # 设置默认电压 并 重启
        print('校准完成')


def getmin(key, val):
    '''获取最接近的key'''
    return min(key, key=lambda x: abs(x - val))

class PowerManagement:
    """电源管理"""
    def __init__(self):
        # 创建电源引脚
        self.CFG1 = Pin(25, Pin.OUT)
        self.CFG2 = Pin(32, Pin.OUT)
        self.CFG3 = Pin(27, Pin.OUT)
        self.voltage = {
            20: (0, 1, 0),
            15: (0, 1, 1),
            12: (0, 0, 1),
            9: (0, 0, 0),
            5: (1, 0, 0)}  # 创建CH224K相对应的匹配项

    def set(self, value):
        """
        设置电压 20,15,12,9,5
        """
        if value not in self.voltage:
            return False
        cfg1, cfg2, cfg3 = self.voltage[value]
        self.CFG1.value(cfg1)
        self.CFG2.value(cfg2)
        self.CFG3.value(cfg3)
        return True


class VoltageDetection:
    '''电压检测'''
    #  创建电源管理 并设置当前电压
    PM = PowerManagement()
    PM.set(file['Vmaximum'])

    def __init__(self, pin=33):
        # 创建均值滤波器
        self._filter = ADCFilter(50)
        # 创建ADC
        self._ADC = ADC(Pin(pin))
        self._ADC.atten(ADC.ATTN_6DB)

        self._configure = 2.0  # ATTEN_6DB对应0-2V量程
        self._calibration = 11.0  # 默认分压比,通过校准调整

        # 获取60个样本进行填充
        self.dataFill()

        self._Time = TimeMs(600, 600)  # 用于控制每次匹配电压的时间
        self._state = None
        self.adjust()  # 调整电压策略

    def dataFill(self):
        '''数据填充'''
        self._filter.clear()
        for _ in range(60):
            self.get()

    def get_ADC(self):
        '''
        :return: 获取的 ADC 采样电压
        '''
        return self._filter.update(self._ADC.read()) * (self._configure / 4095)

    def get(self):
        '''
        转换电压值
        因为self.get返回的是经过10k电阻和1k电阻之间的电压值,所以要乘以 self._calibration
        :return: 电压值
        '''
        return round(self.get_ADC() * self._calibration, 1)

    def calibrate(self, key, voltage):
        """
        已知电压校准
        :param key:  更改的电压值
        :param voltage:  当前电压
        """
        self.dataFill()
        readings = [self.get_ADC() for _ in range(60)]  # 获取新60个样本
        file['Vcalibration'][str(key)] = self._calibration = voltage / (sum(readings) / len(readings))

    def adjust(self):
        '''电压调整'''
        self._calibration = file.get('Vcalibration')[str(self.matching())]

    def matching(self):
        '''
        :return: 匹配相近电压值
        '''
        return getmin([5, 9, 12, 15, 20], self.get())  # 获取当前电压并匹配,最接近的key的值

    def Restart(self, val: int | bool = False):
        '''
        准备重启电源,设置电压=》更新电压并在1秒后重置对象状态
        :param val: 是否设置电压
        '''
        # 设置电压
        self.PM.set(val)

        def timer_callback(timer):
            timer.deinit()  # 先释放资源
            self.__init__()  # 重新初始化
        # 创建并启动定时器
        timer = Timer(1)
        timer.init(mode=Timer.ONE_SHOT, period=1000, callback=timer_callback)

    def update(self):
        '''更新'''
        if self._Time.update():
            voltage = self.matching()
            if self._state != voltage:  # 如果当前电压和以前不同则调整电压策略
                self._state = voltage
                self.adjust()
        return True


PS = VoltageDetection()
PS.Restart()

'''
#自动校准
VC=VoltageCalibration()
VC.automatic()
#手动校准 
VC=VoltageCalibration()
VC.manual()

建议用’手动校准‘
'''
if __name__ == '__main__':
    Time = TimeMs(1000, 1000)
    while True:
        PS.update()
        if Time.update():
            print(PS.get())
        time.sleep_ms(1)

 

 

Logo

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

更多推荐