编程 MicroPython esp32 模块笔记:PCNT 4X 正交解码、OTA 回滚与 deep sleep 唤醒

2026-10-05 00:05:14

MicroPython esp32 模块笔记:PCNT 4X 正交解码、OTA 回滚与 deep sleep 唤醒

项目信息

  • 官网:
  • 文档:
  • GitHub:

esp32 模块与 machine 模块的分工

machine 是通用可移植硬件抽象层——Pin、I2C、SPI、Timer 这些,换到别的芯片也长得差不多。esp32 模块走的是另一条路:暴露芯片专有能力,成员按编译期 SOC_* 宏条件编译。结果是同一份代码在不同 ESP32 型号上可用的 API 集合并不一样,比如 ULP 只编译进 ESP32/S2/S3,LDO 只有 ESP32-P4 才有。跨芯片代码里最好把这类调用包起来,或者按芯片型号分支。

睡眠与唤醒

唤醒源的配置函数按硬件能力划分:

  • esp32.wake_on_touch(wake):wake 为 bool,只有带触摸传感器的板子可用。
  • esp32.wake_on_ulp(wake):只有带 ULP 的板子可用。
  • esp32.wake_on_ext0(pin, level):pin 可以是 None 或一个合法 Pin。
  • esp32.wake_on_ext1(pins, level):pins 可以是 None,或者 Pin 的 tuple/list。
  • esp32.wake_on_gpio(pins, level):部分板子不支持从 deep sleep 用 GPIO 唤醒,在这些板子上,这里设置的引脚只能用于 light sleep 唤醒。

后三个函数的 level 都取 esp32.WAKEUP_ALL_LOW 或 esp32.WAKEUP_ANY_HIGH,没有别的取值。

esp32.gpio_deep_sleep_hold(enable) 控制非 RTC GPIO 引脚的配置在 deep-sleep 期间对 held pads 是否保持。用 GPIO 唤醒时如果发现引脚状态在深睡里丢了,先看这个。

内存与任务诊断

esp32.idf_heap_info(capabilities) 返回 ESP-IDF 堆内存区域信息。capabilities 对应 ESP-IDF 的 MALLOC_CAP_XXX 值,预定义了 esp32.HEAP_DATA(数据堆)和 esp32.HEAP_EXEC(可执行区域,供 native code emitter 用)。返回是一个 4 元组列表,每个 4 元组对应一个堆:total bytes、free bytes、largest free block、minimum free seen over time。

启动后的官方示例输出:

>>> import esp32; esp32.idf_heap_info(esp32.HEAP_DATA)
[(240, 0, 0, 0), (7288, 0, 0, 0), (16648, 4, 4, 4), (79912, 35712, 35512, 35108), (15072, 15036, 15036, 15036), (113840, 0, 0, 0)]

HEAP_DATA 区域的空闲 IDF 堆内存可以被自动加入 MicroPython 堆,用来避免分配失败。但除此之外,这些信息对排查 Python 分配失败没有用——该用 micropython.mem_info() 和 gc.mem_free()。micropython.mem_info() 输出里的 max new split 对应可被按需自动加入 MicroPython 堆的 ESP-IDF 堆最大空闲块;gc.mem_free() 返回的是当前 free 与 max new split 之和。

esp32.idf_task_info() 返回正在运行的 ESP-IDF/FreeRTOS 任务信息,MicroPython 线程也在内。它需要在板卡配置里设 CONFIG_FREERTOS_USE_TRACE_FACILITY=y 才可用;建议同时配 CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y 和 CONFIG_FREERTOS_VTASKLIST_INCLUDE_COREID=y,才能拿到总/单任务运行时间和 core ID。返回 2 元组:第一个是总运行时间,第二个是任务列表。每个任务是 7 元组——任务 ID、名字、当前状态、优先级、运行时间、栈 high water mark、运行所在 core ID。对应的 FreeRTOS 选项没开时,runtime 和 core ID 为 None。想要类似 Unix top 的实时概览,可以用 utop 库。

Flash 分区与 OTA

class esp32.Partition(id, block_size=4096, /) 创建代表分区的对象。id 可以是分区标签字符串,也可以是常量 BOOT 或 RUNNING;block_size 指定单个块的字节数。

Partition.find(type=TYPE_APP, subtype=0xff, label=None, block_size=4096) 按 type、subtype、label 查找,返回(可能为空的)Partition 对象列表。subtype=0xff 匹配任意 subtype,label=None 匹配任意 label。

  • Partition.info() → 6 元组 (type, subtype, addr, size, label, encrypted)
  • Partition.readblocks(block_num, buf[, offset])、writeblocks(...)、ioctl(cmd, arg):实现 vfs.AbstractBlockDev 的 simple 和 extended 块协议
  • Partition.set_boot():把该分区设为启动分区
  • Partition.get_next_update():获取此分区之后的下一个更新分区,返回新的 Partition 对象,典型用法 Partition(Partition.RUNNING).get_next_update()
  • classmethod Partition.mark_app_valid_cancel_rollback():标识当前启动视为成功

set_boot() 之后有一个容易忽略的时序问题:改完 OTA 启动分区,不要在没有先做硬复位或断电的情况下进入 deepsleep,要让 bootloader 在启动前有机会校验新镜像。

mark_app_valid_cancel_rollback() 对新分区首次启动是必须的,不调用的话下次启动会自动回滚。它依赖 ESP-IDF 的 app rollback 特性和 CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE;在未启用该特性的固件上调用会抛 OSError(-261)。每次启动都调用没问题,用 esptool 加载的固件不需要调用。

相关常量:Partition.BOOT / RUNNING / TYPE_APP / TYPE_DATA,以及 esp32.HEAP_DATA / HEAP_EXEC。TYPE_APP 用于可启动固件分区(通常标注 factory、ota_0、ota_1),TYPE_DATA 用于 nvs、otadata、phy_init、vfs 等其他分区。

PCNT 脉冲计数

PCNT 提供对硬件脉冲计数单元的访问,共 8 个单元,id 0..7。想要更简单的可移植抽象,看 machine.Counter 和 machine.Encoder,它们是围绕 PCNT 的薄 Python shim。

class esp32.PCNT(id, *, ...) 返回给定单元 id 的 PCNT 单例,关键字参数直接传给 init()。

PCNT.init(*, ...) 支持的关键字参数:

  • channel:通道号
  • pin:监听的输入引脚
  • rising / falling:上升沿/下降沿动作,取 PCNT.INCREMENT、PCNT.DECREMENT 或 PCNT.IGNORE,默认 IGNORE
  • mode_pin:监测第二个引脚,按其电平改变计数器行为
  • mode_low / mode_high:取 PCNT.HOLD 或 PCNT.REVERSE,在 mode_pin 为低/高时暂停计数或反转方向
  • filter:1..1023,以 80MHz 时钟 tick 为单位启用脉宽过滤
  • min:递减时计数器最小值,-32768..-1,0 禁用
  • max:递增时计数器最大值,1..32767,0 禁用
  • threshold0 / threshold1:设置 IRQ_THRESHOLD0/IRQ_THRESHOLD1 事件的计数值
  • value:设为 0 可复位计数值

硬件初始化分阶段,部分关键字可以分组或单独使用,用于部分重配单元。每个单元支持两个 channel(0 和 1),各自监测不同引脚、不同计数逻辑,但更新同一个计数值。用 channel=1 配合相应关键字配置第二通道。第二通道可用于单单元 4X 正交解码:

pin_a = Pin(2, Pin.INPUT, pull=Pin.PULL_UP)
pin_b = Pin(3, Pin.INPUT, pull=Pin.PULL_UP)
rotary = PCNT(0, min=-32000, max=32000)
rotary.init(channel=0, pin=pin_a, falling=PCNT.INCREMENT, rising=PCNT.DECREMENT, mode_pin=pin_b, mode_low=PCNT.REVERSE)
rotary.init(channel=1, pin=pin_b, falling=PCNT.DECREMENT, rising=PCNT.INCREMENT, mode_pin=pin_a, mode_low=PCNT.REVERSE)
rotary.start()

PCNT.value([value]) 无参时返回当前计数值;value 为 0 时复位计数器,但返回的是复位之前的值。读取并复位不是原子操作,中间可能漏掉一个脉冲。传任何非 0 值都会报错。

PCNT.irq(handler=None, trigger=PCNT.IRQ_ZERO) 支持的事件有 IRQ_ZERO(计数器回零)、IRQ_MIN、IRQ_MAX、IRQ_THRESHOLD0、IRQ_THRESHOLD1,trigger 是所需事件按位或的掩码。handler 接收一个参数,即触发事件的 PCNT 实例。方法返回 callback 对象,可以用 pcnt.irq().flags() 取未处理事件的位掩码。

有几个点要注意:访问 irq.flags() 会清除标志,所以每次 handler 调用只能读一次。handler 由 MicroPython 调度器调用,在中断之后某个时间点执行;如果在 handler 被调用前又发生中断,事件会合并成一次调用,位掩码指示所有已发生的事件。为避免竞态,value() 会在返回当前值(可能是复位值)之前强制执行所有挂起事件。每个单元只能有一个 handler,设为 None 即禁用。

还有一个反直觉的地方:ESP32 脉冲计数器到达最小值或最大值时会归零,所以 IRQ_ZERO 在这些事件发生时也会被触发。

RMT

RMT(Remote Control)模块最初用于收发红外遥控信号,但因为脉冲生成足够灵活(低至 12.5ns),也可以收发很多其他数字信号。输入时钟为 80MHz(目前固定),resolution_hz 决定 RMT 通道分辨率,write_pulses 中的数字乘以分辨率定义脉冲长度。

import esp32
from machine import Pin
r = esp32.RMT(pin=Pin(18), resolution_hz=10000000)
r = esp32.RMT(pin=Pin(18), resolution_hz=10000000, tx_carrier=(38000, 50, 1))
r.write_pulses((1, 20, 2, 40), 0)  # 发 0 持续 100ns,1 持续 2000ns,0 持续 200ns,1 持续 4000ns

class esp32.RMT(channel, *, pin=None, resolution_hz=10000000, clock_div=None, idle_level=False, num_symbols=48|64, tx_carrier=None) 访问八个 RMT 通道之一。channel 可选,保留是为向后兼容;pin 必填。num_symbols 指定为该通道分配的 RMT 缓冲区(最小 48 或 64,视芯片),来自所有通道共享的小符号池(192 到 512,视芯片);这个缓冲不限制可发送的脉冲序列大小,但更大的缓冲能降低 CPU 负载,并减少毛刺和脉冲长度不精确的风险。tx_carrier 是三元组:载波频率、占空百分比(0..100)、施加载波的电平。

RMT.write_pulses(duration, data=True) 有三种模式:

  1. duration 是时长 list/tuple,data 指定初始电平,输出电平在每个时长后翻转
  2. duration 为正整数、data 为电平 list/tuple,每段固定时长
  3. duration 与 data 等长的 list/tuple,逐段指定时长与电平

时长以通道分辨率的整数为单位,范围 1 到 PULSE_MAX。

其余方法:RMT.wait_done(*, timeout=0)、RMT.loop(enable_loop)、RMT.loop_count(n)、RMT.active([boolean])、RMT.deinit()。静态方法 RMT.bitstream_rmt([value]) 配置 machine.bitstream 的实现是否使用 RMT,默认 True。常量有 RMT.PULSE_MAX。

当前 MicroPython 的 RMT 实现缺少部分特性,最明显的是不支持接收脉冲。RMT 应视为 beta 特性,接口未来可能变更——锁死版本的量产固件里用它要留心。

ULP、NVS、LDO

class esp32.ULP 提供对 ESP32、ESP32-S2、ESP32-S3 芯片上 ULP 协处理器的访问。它不提供对 ESP32-S2/S3 上 RISCV ULP 协处理器的访问,也只在这些芯片上编译(CONFIG_IDF_TARGET_ESP32/S2/S3)。

NVS 的键是字符串,值可以是各种整数类型、字符串和二进制 blob。修改后必须 commit(),否则复位后丢失。

LDO 提供对 ESP32-P4 上低压差稳压器的访问,仅 ESP32-P4 可用(受 SOC_GP_LDO_SUPPORTED 保护),且仅支持 RTC 域引脚。

温度读取分芯片:esp32.raw_temperature() 仅原始 ESP32 可用,返回内部温度传感器原始值的整数;其他芯片使用 mcu_temperature()。

各型号的具体可用 API 与参数范围以官方文档为准。

推荐文章

程序员茄子在线接单