4、红外信号发射与捕获:从原始码到空调控制
红外遥控,说白了就是咱们空调遥控器里那套老本行。我刚开始做这个项目时,觉得红外协议挺简单的——不就是发光管闪一闪嘛。结果真上手才发现,坑多着呢。
这一章,咱们要搞定三件事:发射红外信号、捕获空调遥控器的原始码、以及解析和存储这些码。说白了,就是让咱们的板子能“听懂”空调遥控器在说什么,然后自己也能说同样的话。
核心思路:先捕获空调遥控器发出的红外波形,解析出其中的协议和数据,然后把这些数据存起来。需要控制空调时,直接调取数据发射就行。
4.1 红外接收头:捕获遥控器的“耳朵”
先说说硬件。红外接收头,常见的有 VS1838B、TSOP38238 这些型号。它们内部集成了光电二极管、放大器和解调电路,输出的是解调后的数字信号。
我习惯用 VS1838B,便宜又好买。它的引脚定义很简单:
| 引脚 | 名称 | 说明 |
|---|---|---|
| 1 | OUT | 信号输出,接 GPIO |
| 2 | GND | 接地 |
| 3 | VCC | 3.3V 或 5V 供电 |
小技巧:接收头输出是低电平有效。没信号时输出高电平,收到红外载波时输出低电平。这个特性在捕获时要注意。
4.2 使用 pulseio 捕获原始码
CircuitPython 的 pulseio 模块,专门用来捕获脉冲宽度。说白了,就是记录信号高低电平的持续时间。
我给大家写个捕获函数:
import board
import pulseio
import array
def capture_ir(pin, timeout=0.1, max_pulses=200):
"""
捕获红外信号,返回脉冲列表
每个元素是 (duration, level) 元组
duration 单位:微秒
level: True=高电平, False=低电平
"""
with pulseio.PulseIn(pin, maxlen=max_pulses, idle_state=True) as pulses:
# 等待信号开始
pulses.clear()
pulses.resume()
# 等待超时或缓冲区满
while len(pulses) < max_pulses:
if pulses.paused:
break
if pulses[-1] > timeout * 1000000: # 超时判断
break
# 转换为列表
result = []
for i in range(len(pulses)):
result.append((pulses[i], i % 2 == 0))
return result
嗯,这里要注意几个点:
- idle_state=True:表示空闲时是高电平。因为接收头没信号时输出高。
- 超时判断:红外信号结束后,接收头会一直输出高电平。我设置 100ms 超时,够用了。
- 缓冲区大小:空调遥控器的码一般不超过 200 个脉冲,设 200 足够。
避坑指南:我曾经遇到过捕获到的脉冲数量忽多忽少,后来发现是遥控器电池电量不足,导致信号不稳定。换新电池后问题解决。捕获时尽量用新电池。
4.3 原始码解析:从脉冲到数据
捕获到的是脉冲宽度列表,但空调遥控器用的是某种红外协议。常见的空调协议有 NEC、SONY、RC-5 等。不过说实话,很多空调厂商会魔改协议,尤其是美的、格力这些。
解析的核心思路:
- 识别引导码:通常是一段较长的低电平 + 一段较长的高电平。
- 识别数据位:0 和 1 的脉冲宽度不同。比如 NEC 协议,0 是 560us 低 + 560us 高,1 是 560us 低 + 1690us 高。
- 提取数据:按位解析出地址码、命令码等。
我写了个简单的解析器,针对 NEC 协议:
def parse_nec(pulses):
"""
解析 NEC 协议的红外脉冲
返回 (address, command) 元组
"""
if len(pulses) < 68: # NEC 最少 68 个脉冲
return None
# 跳过引导码(前两个脉冲)
data_pulses = pulses[2:]
# 提取数据位
bits = []
for i in range(0, len(data_pulses), 2):
if i + 1 >= len(data_pulses):
break
low_time = data_pulses[i][0]
high_time = data_pulses[i + 1][0]
# 判断是 0 还是 1
if high_time > 1000: # 高电平 > 1ms 认为是 1
bits.append(1)
else:
bits.append(0)
if len(bits) >= 32: # NEC 是 32 位数据
break
if len(bits) < 32:
return None
# 组装成 4 个字节
address = (bits[0] << 7) | (bits[1] << 6) | ... # 简化处理
command = (bits[16] << 7) | (bits[17] << 6) | ...
return (address, command)
实际项目经验:我遇到过一款空调,它的协议和标准 NEC 不完全一样——引导码长度不同,数据位顺序也反了。这种情况下,我建议直接存储原始脉冲序列,而不是解析后的数据。发射时原样重放,省去协议适配的麻烦。
4.4 存储方案:把码保存下来
解析后的数据,或者原始脉冲序列,总得存起来。我推荐用 JSON 格式,方便读写,也方便调试。
存储结构示例:
{
"protocol": "nec",
"carrier_freq": 38000,
"pulses": [
{"duration": 9000, "level": false},
{"duration": 4500, "level": true},
{"duration": 560, "level": false},
...
],
"address": 0x00,
"command": 0xBF
}
存成文件后,下次要用直接读取。我习惯把每个按键的码存成一个文件,文件名用按键名命名,比如 power_on.json、temp_up.json。
小建议:如果板子有文件系统(比如 SD 卡或 SPI Flash),直接存文件。如果内存紧张,可以存成字节数组,用 struct 模块打包,能省不少空间。
4.5 发射红外信号:把码“说”出去
发射比捕获简单。用 pulseio.PulseOut 配合红外发射管就行。注意发射管要串联一个限流电阻,一般 100Ω 左右。
发射代码:
import board
import pulseio
import array
def send_ir(pin, pulses, carrier_freq=38000):
"""
发射红外信号
pulses: 脉冲列表,每个元素是 (duration, level)
carrier_freq: 载波频率,默认 38KHz
"""
# 将脉冲转换为 PulseOut 需要的格式
# 注意:PulseOut 只接受高电平脉冲,低电平用 0 表示
pwm_pulses = array.array('H')
for duration, level in pulses:
if level:
pwm_pulses.append(int(duration))
else:
pwm_pulses.append(0) # 低电平不发射载波
with pulseio.PulseOut(pin, frequency=carrier_freq, duty_cycle=2**15) as pulse:
pulse.send(pwm_pulses)
重要提醒:发射时,红外发射管的正极接 GPIO,负极通过电阻接地。GPIO 的驱动能力有限,如果距离远,建议加三极管放大。我曾经试过直接用 GPIO 驱动,结果 1 米外就控制不了空调了。
4.6 实战:捕获空调遥控器的“开”键
咱们来走一遍完整流程。假设你要捕获空调遥控器的“开机”键:
- 连接红外接收头到板子(OUT 接 GPIO,GND 接地,VCC 接 3.3V)。
- 运行捕获脚本,按下遥控器的“开”键。
- 查看捕获到的脉冲序列,确认长度合理(一般 100-200 个脉冲)。
- 解析协议,或者直接存储原始脉冲。
- 用发射脚本测试,看空调是否有反应。
我给大家一个完整的测试脚本:
import board
import pulseio
import time
import json
# 捕获
def capture_and_save(pin, filename):
pulses = capture_ir(pin)
if pulses:
data = {
"protocol": "raw",
"carrier_freq": 38000,
"pulses": [{"d": p[0], "l": p[1]} for p in pulses]
}
with open(filename, 'w') as f:
json.dump(data, f)
print(f"已保存 {len(pulses)} 个脉冲到 {filename}")
else:
print("捕获失败,请重试")
# 发射
def load_and_send(pin, filename):
with open(filename, 'r') as f:
data = json.load(f)
pulses = [(p['d'], p['l']) for p in data['pulses']]
send_ir(pin, pulses, data['carrier_freq'])
print(f"已发射 {len(pulses)} 个脉冲")
# 使用示例
pin = board.GP16 # 接收头接 GP16
capture_and_save(pin, "power_on.json")
time.sleep(1)
pin_tx = board.GP17 # 发射管接 GP17
load_and_send(pin_tx, "power_on.json")
我的经验:第一次测试时,建议把空调的电源插头拔掉再插上,确保空调处于待机状态。然后发射捕获到的码,看空调是否开机。如果没反应,检查脉冲序列是否完整,或者载波频率是否匹配。有些空调用 38KHz,有些用 40KHz,甚至 56KHz。