• RP2040 数据手册(Datasheet) 中关于 PIO WAIT 指令的底层编码与行为说明(Section 3.4.3)。

1. WAIT 指令的二进制编码格式(Encoding)

Bit 位 15 14 13 12 11–9 8–5 4–0
0 0 1 0 Delay/Side-set Pol + Source Index

1.1 操作码 Opcode

  • 操作码0010(前四位,即十六进制 0x2),表示这是一条 WAIT 指令。

1.2 Pol 等待极性

  • Pol(Polarity):1 位,表示等待 高电平(1) 还是 低电平(0)

1.3 Source 信号源类型

  • 2 位,指定等待的信号源类型
    • 00GPIO(绝对引脚号,不受 in_base 影响)
    • 01PIN(相对于 in_base 的偏移,受 PIO 输入映射影响)
    • 10IRQ(中断标志)
    • 11 → 保留
Source 含义 实际效果
GPIO (00) 绝对 GPIO 编号 wait(1, gpio, 15) → 直接读取 GPIO15,无视 in_base
PIN (01) 相对于 in_base in_base=15, wait(1, pin, 0) → 读取 GPIO15in_base=10, wait(1, pin, 5) → 读取 GPIO15
IRQ (10) 等待 IRQ 标志 可用于 SM 之间同步(见下文)
1.3.1 IRQ 等待的特殊行为
  • wait(1, irq, n) 成功时,该 IRQ 标志会被自动清除(防止重复触发)。
  • 支持 相对 IRQ 编号:若 Index 最高位为 1(如 0x10 + x),则实际 IRQ = (x + sm_id) % 4
    • 例:SM2 执行 wait(1, irq, 0x11) → 等待 IRQ (1 + 2) % 4 = 3

⚠️ 警告:不要用 wait(1, irq, x) 等待那些已连接到 CPU 中断控制器的 IRQ,否则可能因 CPU 中断处理程序清除了标志而导致状态机永远等待。

1.4 Index :结合PIN类型使用的偏移量

  • Index:5 位,用于指定具体 GPIO 编号、引脚偏移或 IRQ 编号(0–31)。

在 MicroPython 的 @rp2.asm_pio 中,当写 wait(1, pin, 0)

  • pin 对应 Source = 01(PIN)
  • 0Index = 0
  • 实际等待的物理引脚 = in_base + 0

1.5 Delay/Side-set

1.5.1
是否启用 side-set bit 11–9 的作用
未启用 side-set 表示 延迟周期数(Delay),范围 0–7
启用了 side-set 高若干位用于 side-set 输出值,剩余低位用于 延迟
1.5.2 纯 delay
  • 模式 A:无 side-set → 纯 delay(最常见于接收类程序)

    • Delay = 0 到 7
    • 指令执行完成后,插入 N 个空闲时钟周期
    • 常用于:
      • 避免采样过快(如 UART 起始位后等待半个周期)
      • 控制循环速率
@rp2.asm_pio()
def example():
    wait(0, pin, 0)   # 无 delay
    nop() [3]         # 插入 3 个延迟周期,nop() [3] 编译后,其 `Delay/Side-set` 字段 = `0b011`(即 3)

💡 所有 PIO 指令都支持 [N] 语法来添加延迟,例如 jmp(...) [2]

1.5.3 启用 side-set
  • 模式 B:启用 side-set → 同时输出控制信号 + 可选 delay

    • 当需要 在执行指令的同时控制 GPIO 输出(如红外发射、LED 控制),就要用到 side-set

    • @rp2.asm_pio() 装饰器中指定:

    @rp2.asm_pio(
        sideset_init=rp2.PIO.OUT_HIGH,  # 初始化 side-set 引脚为高
        out_shiftdir=rp2.PIO.SHIFT_RIGHT,
        autopull=True,
        pull_thresh=24,
        sideset_width=2   # ← 关键:使用 2 个 side-set 位
    )
    
  • 此时 Delay/Side-set 字段被拆分:
    假设 sideset_width = N,则:

    • 高 N 位 → side-set 输出值(控制 GPIO)
    • 低 (3−N) 位 → delay 周期数(最大 2^(3−N) − 1
sideset_width Side-set 位 Delay 位 最大 Delay
0 3 位 7
1 1 位 2 位 3
2 2 位 1 位 1
3 3 位 0 位 0

⚠️ 如果 sideset_width ≥ 3无法再添加任何延迟

1.5.3.1 示例:WS2812 驱动片段(使用 side-set 控制数据线)
@rp2.asm_pio(sideset_init=rp2.PIO.OUT_LOW, out_init=rp2.PIO.OUT_LOW, sideset_width=1)
def ws2812():
    T1 = 2; T2 = 5; T3 = 3
    label("bitloop")
    out(x, 1)               .side(1)    # 输出1,同时拉高数据线
    jmp(not_x, "do_zero")   [T3 - 1]    # delay 由 [ ] 指定
    jmp("bitloop")          [T1 - 1]
    label("do_zero")
    nop()                   .side(0)    [T2 - 1]  # 拉低,delay=T2-1
  • .side(1) → 将 side-set 位设为 1(数据线高)
  • [T2 - 1] → 利用剩余 delay 位插入精确延时

2. CODE

2.1. 代码简介

  • https://github.com/danjperron/PicoDHT22/blob/main/PicoDHT22.py 代码是一个 MicroPython 程序,用于在 Raspberry Pi Pico(基于 RP2040 芯片) 上通过 PIO(Programmable I/O)硬件模块 读取 DHT22(或 DHT11)温湿度传感器 的数据。
  • 该代码采用 MIT 开源许可证。作者是 Daniel Perron(2021 年)。

2.2. 导入必要的模块

import utime
import rp2
from rp2 import PIO, asm_pio
from machine import Pin
  • utime:提供延时函数(如 sleep_ms)。
  • rp2asm_pio:用于定义 PIO 汇编程序(底层硬件控制)。
  • Pin:用于操作 GPIO 引脚。

2.3. DHT22 通信协议

  • 单总线协议(1-Wire Protocol) 是一种由 Maxim Integrated(原 Dallas Semiconductor) 开发的半双工、主从式、低速串行通信协议,其最大特点是:仅用一根数据线(加地线)即可实现供电和数据通信。
  • 标准单总线协议(1-Wire):是 Maxim 的完整通信标准,支持多设备、唯一 ID、寄生供电。
  • DHT22 的协议:简化的“单线”私有协议:
    在这里插入图片描述
#     A     B   C   D   E   F
#          ___     ___     ...
#     ____/   \___/   \___/   \
#
#     A = start pulse (> 1ms )
#     B = 2-40 us 
#     C = 80 us
#     D = 80 us
#
#     E and F are  data clock
#
#     E = 50 us
#     F =  26-28 us => 0    70 us => 1

这是 DHT22 的通信时序图:

  • A:主机拉低总线 >1ms,启动通信。
  • B:传感器拉高 20–40μs(表示开始响应)。
  • C:传感器拉低约 80μs。
  • D:传感器拉高约 80μs(准备发送数据)。
  • E + F:每个数据位由两个脉冲组成:
    • E:固定 ~50μs 低电平(同步段)。
    • F:高电平持续时间决定 bit 值:
      • 26–28μsbit = 0
      • ~70μsbit = 1

DHT22 发送 40 位数据(5 字节):
[湿度高8][湿度低8][温度高8][温度低8][校验和]

2.4. PIO 汇编程序:DHT22_PIO

  • 使用 @asm_pio 装饰器定义一个 PIO 状态机程序,运行在 RP2040 的专用硬件上,实现精确的微秒级时序控制。
@asm_pio(set_init=(PIO.OUT_HIGH),autopush=True, push_thresh=8)
def DHT22_PIO():
    # clock set at 500Khz  Cycle is 2us
    # drive output low for at least 20ms
    set(y,1)                    # 0
    pull()                      # 1
    mov(x,osr)                  # 2
    set(pindirs,1)              # 3 set pin to output
    set(pins,0)                 # 4 set pin low
    label ('waitx')
    jmp(x_dec,'waitx')          # 5 decrement x reg every 32 cycles
    set(pindirs,0)              # 6 set pin to input 
    # STATE A. Wait for high at least 80us. max should be  very short
    set(x,31)                   # 7 
    label('loopA')
    jmp(pin,'got_B')            # 8
    jmp(x_dec,'loopA')          # 9
    label('Error')
    in_(y,1)                    # 10
    jmp('Error')                # 11  Infinity loop error

    # STATE B. Get HIGH pulse. max should be 40us
    label('got_B')
    set(x,31)                   # 12
    label('loop_B')
    jmp(x_dec,'check_B')        # 13
    jmp('Error')                # 14
    label('check_B') 
    jmp(pin,'loop_B')           # 15
 
    # STATE C. Get LOW pulse. max should be 80us
    set(x,31)                   # 16
    label('loop_C')
    jmp(pin,'got_D')            # 17     
    jmp(x_dec,'loop_C')         # 18
    jmp('Error')                # 19
    
    # STATE D. Get HIGH pulse. max should be 80us
    label('got_D')
    set(x,31)                   # 20
    label('loop_D')
    jmp(x_dec,'check_D')        # 21
    jmp('Error')                # 22
    label('check_D')
    jmp(pin,'loop_D')           # 23
    
    # STATE E. Get Low pulse delay. should be around 50us
    set(x,31)                   # 24
    label('loop_E')
    jmp(pin,'got_F')            # 25
    jmp(x_dec,'loop_E')         # 26
    jmp('Error')                # 27
   
    # STATE F.
    # wait 40 us
    label('got_F')              
    nop() [20]                  # 28
    in_(pins,1)                 # 29
    # now wait for low pulse
    set(x,31)                   # 30
    jmp('loop_D')               # 31    
关键参数:
@asm_pio(set_init=(PIO.OUT_HIGH), autopush=True, push_thresh=8)
  • set_init=PIO.OUT_HIGH:初始化引脚为输出高电平。
  • autopush=True, push_thresh=8:每收到 8 个 bit 自动推入 FIFO(供 CPU 读取)。
程序流程详解:
初始化 & 主机启动信号(拉低 >1ms)
pull()                      # 从 FIFO 读取延时值(如 1000)
mov(x, osr)                 # 将值存入 x 寄存器
set(pindirs, 1)             # 设置引脚为输出
set(pins, 0)                # 拉低
label('waitx')
jmp(x_dec, 'waitx')         # 循环减 x,实现延时(每个循环约 32 cycles)
set(pindirs, 0)             # 切回输入(释放总线)
  • 主机拉低总线,启动传感器。
  • 延时由 sm.put(1000) 控制(对应约 1ms @ 500kHz)。
等待传感器响应(State A)
set(x, 31)                  # 最多等待 31*32*2us ≈ 2ms(实际只需 80us)
label('loopA')
jmp(pin, 'got_B')           # 如果引脚变高,进入 B 阶段
jmp(x_dec, 'loopA')
jmp('Error')                # 超时则死循环报错
读取 B、C、D 阶段(验证响应脉冲)
  • B:高电平(20–40μs)→ 必须在 64μs 内结束(31 cycles × 2μs = 62μs)。
  • C:低电平(≈80μs)。
  • D:高电平(≈80μs)。
  • 每个阶段都设超时,防止卡死。
读取 40 位数据(E + F 阶段)
# E: 等待低电平(≈50us)
# F: 等待高电平,然后延迟 40us 后采样
nop()[20]        # 延迟 40us(20 × 2us)
in_(pins, 1)     # 读取当前引脚状态(0 或 1),存入 shift register
  • 每位数据:
    1. 等待 E(低电平开始)
    2. 进入 F(高电平)
    3. 延迟 40μs(此时若仍为高,则是 “1”;若已变低,则是 “0”)
    4. in_(pins,1) 采样当前电平 → 存入 FIFO

注意:最后 jmp('loop_D') 实际是跳回 D 阶段的入口,形成循环读取 40 位(5 字节)。

2.5. PicoDHT22 类

init
  • 初始化引脚,并创建 StateMachine 对象(未启动)。
def __init__(self, dataPin, powerPin=None, dht11=False, smID=1):
  • dataPin:连接 DHT22 的数据引脚。
  • powerPin(可选):用 GPIO 给传感器供电(省电用)。
  • dht11:是否为 DHT11(精度较低,整数型)。
  • smID:使用哪个 PIO 状态机(0–3)。
read_array():读取原始 5 字节
def read_array(self):
    if self.powerPin: power on + delay
    sleep 200ms
    init state machine at 500kHz
    sm.put(1000)  # DHT22 启动延时(1000 cycles ≈ 2ms? 实际代码写 1000,但注释说 >1ms)
    sm.active(1)
    for i in range(5): value.append(sm.get())  # 从 FIFO 读 5 字节
    sm.active(0)
    power off if needed
    return value
  • 启动状态机,自动采集 40 位(5 字节)并推入 FIFO。
  • 主程序循环读取 5 次 sm.get() 获取字节(注意:PIO 是 LSB 先传,但 DHT 是 MSB 先传?需看实现)

实际上,由于 in_(pins,1) 每次只进 1 bit,且 autopush 在 8 bits 后推入 FIFO,顺序是 bit0 到 bit7 构成一个字节。而 DHT 是 MSB 先发,所以 PIO 收到的是 反序 bit,但因为每次 in_ 是左移还是右移?

  • 关键点:默认 in_左移(shift left),所以先收到的 MSB 会放在字节的高位 —— 正好匹配 DHT 协议!因此无需反转。
read():解析数据 + 校验
def read(self):
    value = self.read_array()  # [H_H, H_L, T_H, T_L, checksum]
    sumV = value[0]+value[1]+value[2]+value[3]
    if (sumV & 0xFF) == value[4]:  # 校验成功
        if dht11:
            humidity = value[0]
            temperature = value[2]
        else:
            humidity = (value[0]<<8 | value[1]) / 10.0
            temperature = ((value[2]&0x7F)<<8 | value[3]) / 10.0
            if value[2] & 0x80: temperature = -temperature
        return T, H
    else:
        return None, None
  • DHT22 数据是 16 位整数 × 0.1
  • 温度最高位(bit7 of byte2)为符号位。

2.6. 主程序

if __name__ == "__main__":
    dht_data = Pin(15, Pin.IN, Pin.PULL_UP)
    dht_sensor = PicoDHT22(dht_data, Pin(14, Pin.OUT), dht11=False)
    while True:
        T, H = dht_sensor.read()
        if T is None: print("sensor error")
        else: print("{:3.1f}'C  {:3.1f}%".format(T,H))
        utime.sleep_ms(500)
  • 使用 GPIO15 作为数据线,GPIO14 供电。
  • 每 500ms 读一次(DHT22 建议最小间隔 2s,但 500ms 有时也能工作)。

3.CG

  • https://pip-assets.raspberrypi.com/categories/814-rp2040/documents/RP-008371-DS-1-rp2040-datasheet.pdf?disposition=inline#page=362
    在这里插入图片描述
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐