【树莓派 003】 RP2040 PIO WAIT 指令 : 二进制编码格式+语法说明+实例代码(DHT22/DHT11温湿度传感器数据读取)
【树莓派 003】 RP2040 PIO WAIT 指令 : 二进制编码格式+语法说明+实例代码(DHT22/DHT11温湿度传感器数据读取)
- 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 位,指定等待的信号源类型:
00→ GPIO(绝对引脚号,不受in_base影响)01→ PIN(相对于in_base的偏移,受 PIO 输入映射影响)10→ IRQ(中断标志)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
- 例:SM2 执行
⚠️ 警告:不要用
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)0是 Index = 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)。rp2和asm_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μs → bit = 0
- ~70μs → bit = 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
- 每位数据:
- 等待 E(低电平开始)
- 进入 F(高电平)
- 延迟 40μs(此时若仍为高,则是 “1”;若已变低,则是 “0”)
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

更多推荐



所有评论(0)