5.30 Infrared红外设备
5.30 Infrared红外设备
📚 本节导读
学习时长: 约 30 分钟
难度级别: ⭐⭐⭐☆☆
前置知识: C 语言基础、红外协议基础知识、5.1 设备驱动框架
本节讲解 OneOS 的 Infrared 红外设备。它基于 NEC 红外遥控协议,通过 GPIO 中断检测红外接收头输出的脉冲宽度来解码遥控信号,并通过 GPIO 输出 38kHz 载波来发送遥控信号。红外驱动使用 6 态有限状态机(FSM) 处理接收,用定时器超时检测判断一帧数据结束。
本节会带你:
- 理解 NEC 协议的帧格式(引导码、地址/数据、重复码)与"用脉冲宽度编码 0/1"的原理;
- 拆解接收状态机的 6 个状态(IDLE/SYNC/ADDR/DATA/REPEAT0/REPEAT1)如何流转;
- 拆解
infrared_send如何发送一帧 NEC 数据(含 38kHz 载波调制); - 掌握
os_infrared_register_device的注册与_infrared_read/_infrared_write的收发机制; - 掌握红外收发设备的完整使用流程。
说明:本节所有源码均取自项目
drivers/infrared/infrared.c,与项目源码逐字一致(含原始英文注释)。
🎯 学习目标
- 理解 NEC 协议帧格式与脉冲宽度编码原理
- 理解红外接收状态机的 6 个状态与流转
- 逐个拆解
infrared_send/_infrared_read/_infrared_write的完整源码 - 掌握
os_infrared_register_device的注册流程 - 掌握红外收发设备的使用方法
一、概述
1.1 Infrared 解决什么问题
一句话核心:Infrared 设备适用于通过 GPIO 接收/发送 NEC 红外遥控信号的场景。它让你"接好红外接收/发射引脚,就能解码遥控器按键、或发射遥控码去控制其他设备"。
为什么需要状态机驱动:
- 红外接收头把光信号转成数字脉冲(引脚电平跳变),遥控信号就是由不同宽度的脉冲序列编码的。
- 解码这些脉冲需要用状态机跟踪"现在在收哪一段"(引导码?地址?数据?),并根据脉冲宽度判断 bit 是 0 还是 1。
- OneOS 用 GPIO 上升/下降沿中断捕捉每个跳变时间点,用状态机逐步解析。
1.2 NEC 协议基础(帧格式)
NEC 协议的一帧数据(从低电平开始)结构:
引导码 地址码(8bit)+反码 数据码(8bit)+反码
┌──────────┐ ┌────────────────┐ ┌────────────────┐
│ 9ms 载波 │ │ addr+~addr │ │ data+~data │
│ 4.5ms 空 │ │ (16 bit) │ │ (16 bit) │
└──────────┘ └────────────────┘ └────────────────┘关键理解 —— 用脉冲宽度编码 0/1:
| 逻辑位 | 载波持续时间 | 间隔 | 总时长 |
|---|---|---|---|
| 0 | 560 µs | 550 µs | ~1.1 ms |
| 1 | 560 µs | 1680 µs | ~2.2 ms |
源码
infrared_rx_addr/infrared_rx_data中用usec > 1200 && usec < 2000判断"高电平时长在 1.2~2ms"则为 bit 1;否则为 bit 0。
重复码(长按):按住按键不放会周期性发送重复码:
重复码
┌──────────┐
│ 9ms 载波 │
│ 2.25ms 空│ ← 与普通引导码的 4.5ms 区分
└──────────┘1.3 接收状态机(6 态)
红外接收用 rx_status 记录当前状态,共 6 态:
| 状态宏 | 值 | 含义 |
|---|---|---|
INFRARED_RX_STATUS_IDLE | 0 | 空闲,等待引导码 |
INFRARED_RX_STATUS_SYNC | 1 | 已收到引导码,进入同步 |
INFRARED_RX_STATUS_ADDR | 2 | 正在接收地址码(16 bit) |
INFRARED_RX_STATUS_DATA | 3 | 正在接收数据码(16 bit) |
INFRARED_RX_STATUS_REPEAT0 | 4 | 收到重复码引导(9ms/2.25ms) |
INFRARED_RX_STATUS_REPEAT1 | 5 | 重复码间隔等待 |
关键理解 —— 状态流转:
IDLE → 检测到引导码(低电平) → SYNC
SYNC → 下一跳变 → ADDR
ADDR → 收满 16bit → DATA
DATA → 收满 16bit → 校验 → 入队 → IDLE
IDLE → 检测到 3.6ms(重复引导) → REPEAT0
REPEAT0 → 间隔判断 → REPEAT1 / IDLE
REPEAT1 → 收到重复 → REPEAT0 / IDLE1.4 Infrared 使用场景
1.4.1 一句话核心
Infrared 设备适用于通过 GPIO 解码 NEC 遥控信号(接收)或发射 NEC 遥控信号(发送)的场景。应用用 os_device_read 收、os_device_write 发。
1.4.2 典型应用场景
场景一:接收遥控器按键
遥控器按键 → 红外接收头 → GPIO 中断 → 状态机解码 → 环形队列 → os_device_read应用循环 os_device_read 读取 os_infrared_info(addr/data/times),判断按键。
为什么用 Infrared:解码、校验、去抖全由驱动完成,应用只需读取解析好的 addr/data。
场景二:发射红外遥控码
os_device_write → infrared_send(addr, data, times) → 38kHz 载波 → 红外发射管应用把要发送的地址码/数据码/重复次数通过 os_device_write 发给驱动。
为什么用 Infrared:发送逻辑(引导码、0/1 脉冲、重复码)由驱动封装,应用只给地址和数据。
1.4.3 选型边界表(何时不该用它)
| 场景 | 该用什么 | 为什么 |
|---|---|---|
| 需要复杂红外交互(多协议) | 专用红外协议栈 | 当前驱动只支持 NEC 协议 |
| 不需要收发、只要读 GPIO 电平 | GPIO 设备(5.2) | 简单电平检测无需红外状态机 |
| 通信距离远、需硬件编码器 | 硬件红外编码器芯片 | GPIO 软件调制受中断延迟影响 |
二、核心数据结构
2.1 红外消息结构体 os_infrared_info
来源:drivers/infrared/infrared.h
struct os_infrared_info
{
uint8_t addr;
uint8_t data;
uint8_t times;
};字段详解(源码无注释,下表补充说明,不属于源码):
| 字段 | 说明 |
|---|---|
addr | 解码出的 8 位地址码 |
data | 解码出的 8 位数据码(按键值) |
times | 重复次数(长按时递增) |
2.2 红外设备结构体
来源:drivers/infrared/infrared.h
struct os_infrared_device
{
struct os_device parent;
int tx_pin;
int rx_pin;
int rx_status;
uint64_t rx_time_stamp;
uint16_t rx_addr, rx_addr_offset;
uint16_t rx_data, rx_data_offset;
int info_head, info_tail;
struct os_infrared_info info[8];
os_list_node_t list;
};字段详解(源码无注释,下表补充说明,不属于源码):
| 字段 | 说明 |
|---|---|
parent | 首成员继承 os_device_t(os_infrared_register_device 里 OS_DEVICE(device) 取它) |
tx_pin/rx_pin | 发送/接收引脚(tx_pin 为 OS_INFRARED_INVALIDE_PIN(-1) 时不初始化发送) |
rx_status | 状态机当前状态(IDLE/SYNC/ADDR/DATA/REPEAT0/REPEAT1) |
rx_time_stamp | 上一次跳变的时间戳,用于计算脉冲宽度 |
rx_addr / rx_data | 正在累加的地址/数据(16bit,含反码) |
rx_addr_offset / rx_data_offset | 已接收的 bit 数 |
info_head / info_tail | 环形队列读写指针 |
info[8] | 8 条消息的环形缓冲 |
list | 链表节点,挂到 infrared_list(供定时器超时检测遍历) |
2.3 状态机状态宏
#define INFRARED_RX_STATUS_IDLE (0)
#define INFRARED_RX_STATUS_SYNC (1)
#define INFRARED_RX_STATUS_ADDR (2)
#define INFRARED_RX_STATUS_DATA (3)
#define INFRARED_RX_STATUS_REPEAT0 (4)
#define INFRARED_RX_STATUS_REPEAT1 (5)三、核心源码详解
说明:以下源码均取自
drivers/infrared/infrared.c,与项目源码逐字一致(含原始英文注释)。
3.1 接收入口 infrared_rx_pin_callback
一句话定位:GPIO 上升/下降沿中断回调,读取当前电平与时间戳,按状态机状态分发给对应处理函数。
完整源码(drivers/infrared/infrared.c,与项目一致):
static void infrared_rx_pin_callback(void *param)
{
struct os_infrared_device *infrared = param;
int rx_pin_status;
uint64_t now;
rx_pin_status = os_pin_read(BSP_USING_RMT_CTL_ATK_RX_PIN);
now = os_clocksource_gettime();
switch (infrared->rx_status)
{
case INFRARED_RX_STATUS_IDLE:
infrared_rx_idle(infrared, rx_pin_status, now);
break;
case INFRARED_RX_STATUS_SYNC:
infrared_rx_sync(infrared, rx_pin_status, now);
break;
case INFRARED_RX_STATUS_ADDR:
infrared_rx_addr(infrared, rx_pin_status, now);
break;
case INFRARED_RX_STATUS_DATA:
infrared_rx_data(infrared, rx_pin_status, now);
break;
case INFRARED_RX_STATUS_REPEAT0:
infrared_rx_repeat0(infrared, rx_pin_status, now);
break;
case INFRARED_RX_STATUS_REPEAT1:
infrared_rx_repeat1(infrared, rx_pin_status, now);
break;
default:
os_kprintf("infrared rx(%d) invalid\r\n", infrared->rx_status);
break;
}
infrared->rx_time_stamp = now;
}分步详解:
第 1 步:读取电平与时间戳(无条件执行)
rx_pin_status = os_pin_read(BSP_USING_RMT_CTL_ATK_RX_PIN);
now = os_clocksource_gettime();详细解释:os_pin_read 读红外接收引脚当前电平(PIN_HIGH/PIN_LOW)。os_clocksource_gettime() 取当前时间戳(纳秒),用于计算与前一次跳变的时间差(脉冲宽度)。
第 2 步:按状态机分发(条件 当前状态)
switch (infrared->rx_status)
{
case INFRARED_RX_STATUS_IDLE:
infrared_rx_idle(infrared, rx_pin_status, now);
break;
case INFRARED_RX_STATUS_SYNC:
infrared_rx_sync(infrared, rx_pin_status, now);
break;
case INFRARED_RX_STATUS_ADDR:
infrared_rx_addr(infrared, rx_pin_status, now);
break;
case INFRARED_RX_STATUS_DATA:
infrared_rx_data(infrared, rx_pin_status, now);
break;
case INFRARED_RX_STATUS_REPEAT0:
infrared_rx_repeat0(infrared, rx_pin_status, now);
break;
case INFRARED_RX_STATUS_REPEAT1:
infrared_rx_repeat1(infrared, rx_pin_status, now);
break;
default:
os_kprintf("infrared rx(%d) invalid\r\n", infrared->rx_status);
break;
}详细解释:根据 rx_status 调用对应处理函数。每个处理函数都接收 rx_pin_status(当前电平)和 now(时间戳)。
第 3 步:更新时间戳(无条件执行)
infrared->rx_time_stamp = now;
}详细解释:记录本次跳变时间,供下一次计算脉冲宽度使用。
3.2 空闲态 infrared_rx_idle
完整源码(drivers/infrared/infrared.c,与项目一致):
static void infrared_rx_idle(struct os_infrared_device *infrared, int rx_pin_status, uint64_t now)
{
int32_t usec = (now - infrared->rx_time_stamp) / NSEC_PER_USEC;
LOG_D(DBG_EXT_TAG,
"%d.%09d, %d, %d",
(int32_t)(now / NSEC_PER_SEC),
(int32_t)(now % NSEC_PER_SEC),
usec,
rx_pin_status);
if (rx_pin_status == PIN_LOW)
{
/* 3.6ms repeat */
if (usec > 35000 && usec < 45000)
{
infrared->rx_status = INFRARED_RX_STATUS_REPEAT0;
}
else
{
infrared->rx_addr = 0;
infrared->rx_addr_offset = 0;
infrared->rx_data = 0;
infrared->rx_data_offset = 0;
infrared->rx_status = INFRARED_RX_STATUS_SYNC;
}
}
}分步详解:
第 1 步:计算空闲时长(无条件执行)
int32_t usec = (now - infrared->rx_time_stamp) / NSEC_PER_USEC;详细解释:now - rx_time_stamp 是自上次跳变以来的时间(纳秒),除以 NSEC_PER_USEC 转成微秒 usec。这决定了如何区分引导码和重复码。
第 2 步:判断重复码或引导码(条件 rx_pin_status == PIN_LOW)
if (rx_pin_status == PIN_LOW)
{
/* 3.6ms repeat */
if (usec > 35000 && usec < 45000)
{
infrared->rx_status = INFRARED_RX_STATUS_REPEAT0;
}
else
{
infrared->rx_addr = 0;
infrared->rx_addr_offset = 0;
infrared->rx_data = 0;
infrared->rx_data_offset = 0;
infrared->rx_status = INFRARED_RX_STATUS_SYNC;
}
}
}详细解释:
- 若空闲时长为
35~45ms(usec > 35000 && usec < 45000),认为是重复码引导(9ms 载波 + 2.25ms 间隔 ≈ 11.25ms,但加上接收头的低电平保持,实测 3.6ms 段),进入REPEAT0。 - 否则认为是普通引导码(9ms 载波 + 4.5ms 间隔),清零地址/数据缓冲(
rx_addr/rx_data及偏移量),进入SYNC(开始收地址)。
infrared_rx_idle() 条件走向总结:
infrared_rx_idle() 条件走向总结:
├── rx_pin_status == PIN_LOW
│ ├── usec 在 35~45ms(重复引导)→ 状态 = REPEAT0
│ └── 否则(普通引导)→ 清零缓冲 + 状态 = SYNC
└── rx_pin_status != PIN_LOW → 什么都不做3.3 地址/数据接收 infrared_rx_addr / infrared_rx_data
完整源码(drivers/infrared/infrared.c,与项目一致):
static void infrared_rx_addr(struct os_infrared_device *infrared, int rx_pin_status, uint64_t now)
{
int32_t usec = (now - infrared->rx_time_stamp) / NSEC_PER_USEC;
LOG_D(DBG_EXT_TAG,
"%d.%09d, %d, %d",
(int32_t)(now / NSEC_PER_SEC),
(int32_t)(now % NSEC_PER_SEC),
usec,
rx_pin_status);
if (rx_pin_status == PIN_HIGH)
{
return;
}
infrared->rx_addr >>= 1;
if (usec > 1200 && usec < 2000)
{
infrared->rx_addr |= 1 << 15;
}
if (++infrared->rx_addr_offset < 16)
{
return;
}
infrared->rx_status = INFRARED_RX_STATUS_DATA;
}分步详解:
第 1 步:低电平才处理(条件 rx_pin_status == PIN_HIGH 直接返回)
if (rx_pin_status == PIN_HIGH)
{
return;
}详细解释:关键。红外接收头输出低电平代表"载波脉冲段"的结束(一个 bit 的载波脉冲结束)。只在下降沿(低电平)时处理,上升沿忽略。
第 2 步:移位累加并判断 bit(无条件执行)
infrared->rx_addr >>= 1;
if (usec > 1200 && usec < 2000)
{
infrared->rx_addr |= 1 << 15;
}
if (++infrared->rx_addr_offset < 16)
{
return;
}
infrared->rx_status = INFRARED_RX_STATUS_DATA;
}详细解释:
rx_addr >>= 1:右移一位,为新 bit 腾出最高位。usec > 1200 && usec < 2000:上一个高电平时长在 1.2~2ms,说明是"长间隔" = bit 1,置最高位(1 << 15);否则为 bit 0。rx_addr_offset计数到 16(收满 16 bit:地址 + 反码),转入DATA状态收数据。
infrared_rx_data 结构与 rx_addr 完全对称(用 rx_data/rx_data_offset),且收满 16 bit 后做反码校验:
if (++infrared->rx_data_offset < 16)
{
return;
}
if ((infrared->rx_addr >> 8 == (~infrared->rx_addr & 0xff)) &&
(infrared->rx_data >> 8 == (~infrared->rx_data & 0xff)))
{
infrared_rx_data_enqueue(infrared);
}
infrared->rx_status = INFRARED_RX_STATUS_IDLE;
}关键理解 —— 反码校验:NEC 协议发送"地址+地址反码、数据+数据反码"。校验条件 addr >> 8 == ~addr & 0xff 检查高 8 位(反码)是否等于低 8 位(真值)取反。校验通过才入队,否则丢弃(抗干扰)。收完后回到 IDLE。
3.4 入队 infrared_rx_data_enqueue
完整源码(drivers/infrared/infrared.c,与项目一致):
static void infrared_rx_data_enqueue(struct os_infrared_device *infrared)
{
int next_head = (infrared->info_head + 1) % ARRAY_SIZE(infrared->info);
if (next_head != infrared->info_tail)
{
infrared->info[infrared->info_head].addr = infrared->rx_addr;
infrared->info[infrared->info_head].data = infrared->rx_data;
infrared->info_head = next_head;
infrared->parent.rx_count++;
os_device_recv_notify(&infrared->parent);
}
}分步详解:
第 1 步:计算下一个写位置(无条件执行)
int next_head = (infrared->info_head + 1) % ARRAY_SIZE(infrared->info);详细解释:环形队列,(head + 1) % 8 是下一个写位置。
第 2 步:队列未满则入队(条件 next_head != info_tail)
if (next_head != infrared->info_tail)
{
infrared->info[infrared->info_head].addr = infrared->rx_addr;
infrared->info[infrared->info_head].data = infrared->rx_data;
infrared->info_head = next_head;
infrared->parent.rx_count++;
os_device_recv_notify(&infrared->parent);
}详细解释:next_head != info_tail 表示队列未满。把解码出的 rx_addr(取 8 位)和 rx_data 存入队首,更新 info_head,递增 rx_count,并调用 os_device_recv_notify 通知有数据可读。队列满则丢弃新数据(防溢出)。
3.5 发送 infrared_send
一句话定位:按 NEC 协议发送一帧数据(引导码 + 32 bit 地址/数据 + 重复码),通过 irk_on_us/irk_off_us 调制 38kHz 载波。
完整源码(drivers/infrared/infrared.c,与项目一致):
static void irk_on_us(struct os_infrared_device *infrared, uint32_t us)
{
uint32_t count;
uint64_t end;
end = os_clocksource_gettime() + us * NSEC_PER_USEC;
count = us / 24;
for (uint32_t i = 0; i < count; i++)
{
EMISSION_PIN(infrared, INFRARED_TX_ACTIVE);
infrared_udelay(6);
EMISSION_PIN(infrared, INFRARED_TX_DEACTIVE);
infrared_udelay(18);
if (os_clocksource_gettime() > end)
break;
}
}
static void irk_off_us(struct os_infrared_device *infrared, uint32_t us)
{
infrared_udelay(us);
}
void infrared_send(struct os_infrared_device *infrared, uint8_t addrcode, uint8_t keycode, int times)
{
int index;
uint32_t data;
data = ((~keycode & 0xFF) << 24) + ((keycode & 0xFF) << 16) + ((~addrcode & 0xff) << 8) + addrcode;
irk_on_us(infrared, 9000);
irk_off_us(infrared, 4500);
for (index = 0; index < 32; index++)
{
irk_on_us(infrared, 560);
if (((data >> index) & 0x00000001) == 0)
{
irk_off_us(infrared, 550);
}
else
{
irk_off_us(infrared, 1680);
}
}
irk_on_us(infrared, 560);
irk_off_us(infrared, 40000);
for (index = 0; index < times; index++)
{
irk_on_us(infrared, 9000);
irk_off_us(infrared, 2250);
irk_on_us(infrared, 560);
irk_off_us(infrared, 96000);
}
}分步详解:
第 1 步:组织 32 bit 数据(无条件执行)
data = ((~keycode & 0xFF) << 24) + ((keycode & 0xFF) << 16) + ((~addrcode & 0xff) << 8) + addrcode;详细解释:把"数据反码 + 数据 + 地址反码 + 地址"拼成 32 bit:
数值演算(addrcode=0x12, keycode=0x34):
| 字节 | 代入计算 | 值 |
|---|---|---|
| 数据反码 | (~0x34 & 0xFF) << 24 | 0xCB000000 |
| 数据 | (0x34 & 0xFF) << 16 | 0x00340000 |
| 地址反码 | (~0x12 & 0xFF) << 8 | 0x0000ED00 |
| 地址 | 0x12 | 0x00000012 |
| 合成 | 上述相加 | 0xCB34ED12 |
第 2 步:发送引导码(无条件执行)
irk_on_us(infrared, 9000);
irk_off_us(infrared, 4500);详细解释:irk_on_us(9000) 发送 9ms 载波(引导码起始),irk_off_us(4500) 停 4.5ms(引导码间隔)。
关键理解 —— 载波调制 irk_on_us:
count = us / 24;
for (i = 0; i < count; i++)
{
EMISSION_PIN(infrared, INFRARED_TX_ACTIVE); /* 高电平 */
infrared_udelay(6); /* 6 µs */
EMISSION_PIN(infrared, INFRARED_TX_DEACTIVE); /* 低电平 */
infrared_udelay(18); /* 18 µs */
if (os_clocksource_gettime() > end)
break;
}irk_on_us 不是简单拉高引脚,而是每 24 µs 一个周期(6µs 高 + 18µs 低),产生约 1/24µs ≈ 38kHz 的载波。红外接收头需要 38kHz 载波才能识别。
第 3 步:发送 32 bit 数据(无条件执行)
for (index = 0; index < 32; index++)
{
irk_on_us(infrared, 560);
if (((data >> index) & 0x00000001) == 0)
{
irk_off_us(infrared, 550); /* bit 0:短间隔 */
}
else
{
irk_off_us(infrared, 1680); /* bit 1:长间隔 */
}
}详细解释:逐位发送。每位先 irk_on_us(560) 发 560µs 载波,然后根据 bit 决定间隔:bit 0 → 550µs,bit 1 → 1680µs。这与接收端 usec > 1200 && usec < 2000 判断 bit 1 对应。
第 4 步:发送结束位和重复码(条件 times)
irk_on_us(infrared, 560);
irk_off_us(infrared, 40000);
for (index = 0; index < times; index++)
{
irk_on_us(infrared, 9000);
irk_off_us(infrared, 2250);
irk_on_us(infrared, 560);
irk_off_us(infrared, 96000);
}
}详细解释:先发结束位(560µs 载波 + 40ms 空闲)。若 times > 0,发 times 个重复码(9ms 载波 + 2.25ms + 560µs + 96ms)。
infrared_send() 条件走向总结:
infrared_send() 条件走向总结:
├── 拼接 32bit data(反码+真值)
├── 引导码:9ms 载波 + 4.5ms 间隔
├── for 32 bit:
│ ├── 560µs 载波
│ ├── bit 0 → 550µs;bit 1 → 1680µs 间隔
├── 结束位:560µs 载波 + 40ms
└── for times:重复码(9ms + 2.25ms + 560µs + 96ms)3.6 设备操作 _infrared_read / _infrared_write
完整源码(drivers/infrared/infrared.c,与项目一致):
static os_ssize_t _infrared_read(os_device_t *dev, os_off_t pos, void *buffer, os_size_t size)
{
os_ubase_t level;
struct os_infrared_info *info;
struct os_infrared_device *infrared = (struct os_infrared_device *)dev;
OS_ASSERT(buffer);
OS_ASSERT(sizeof(struct os_infrared_info) == size);
info = (struct os_infrared_info *)buffer;
os_spin_lock_irqsave(&gs_device_lock, &level);
if (dev->rx_count <= 0)
{
os_spin_unlock_irqrestore(&gs_device_lock, level);
return 0;
}
*info = infrared->info[infrared->info_tail];
infrared->info_tail = (infrared->info_tail + 1) % ARRAY_SIZE(infrared->info);
dev->rx_count--;
os_spin_unlock_irqrestore(&gs_device_lock, level);
return size;
}分步详解:
第 1 步:断言参数(无条件执行)
OS_ASSERT(buffer);
OS_ASSERT(sizeof(struct os_infrared_info) == size);详细解释:缓冲区非空,且读取长度必须是 sizeof(os_infrared_info)。
第 2 步:队列空则返回 0(条件 rx_count <= 0)
if (dev->rx_count <= 0)
{
os_spin_unlock_irqrestore(&gs_device_lock, level);
return 0;
}详细解释:没有解码数据时返回 0(应用应稍后重试或阻塞)。
第 3 步:取队尾数据并推进(无条件执行)
*info = infrared->info[infrared->info_tail];
infrared->info_tail = (infrared->info_tail + 1) % ARRAY_SIZE(infrared->info);
dev->rx_count--;
os_spin_unlock_irqrestore(&gs_device_lock, level);
return size;
}详细解释:从 info_tail 取一条 os_infrared_info(addr/data)给调用者,info_tail 前进(环形),rx_count 减一,返回 size(读成功)。
_infrared_write(发送):
static os_ssize_t _infrared_write(os_device_t *dev, os_off_t pos, const void *buffer, os_size_t size)
{
os_ubase_t level;
struct os_infrared_info *info;
struct os_infrared_device *infrared = (struct os_infrared_device *)dev;
OS_ASSERT(buffer);
OS_ASSERT(sizeof(struct os_infrared_info) == size);
os_spin_lock_irqsave(&gs_device_lock, &level);
if (dev->tx_count == dev->tx_size)
{
os_spin_unlock_irqrestore(&gs_device_lock, level);
return 0;
}
dev->tx_count++;
os_spin_unlock_irqrestore(&gs_device_lock, level);
info = (struct os_infrared_info *)buffer;
infrared_send(infrared, info->addr, info->data, info->times);
os_spin_lock_irqsave(&gs_device_lock, &level);
dev->tx_count--;
os_spin_unlock_irqrestore(&gs_device_lock, level);
return size;
}分步详解:
第 1 步:断言参数(无条件执行)
OS_ASSERT(buffer);
OS_ASSERT(sizeof(struct os_infrared_info) == size);详细解释:缓冲区非空,且 size 必须是 sizeof(os_infrared_info)。
第 2 步:检查发送计数(条件 tx_count == tx_size)
os_spin_lock_irqsave(&gs_device_lock, &level);
if (dev->tx_count == dev->tx_size)
{
os_spin_unlock_irqrestore(&gs_device_lock, level);
return 0;
}
dev->tx_count++;
os_spin_unlock_irqrestore(&gs_device_lock, level);详细解释:发送互斥。tx_count == tx_size(tx_size 初始为 1,即同时只能发一帧)说明上一帧还在发,返回 0(应用稍后重试)。未满则 tx_count++ 占用发送通道(加锁保护),解锁后再实际发送。
第 3 步:发送一帧(无条件执行)
info = (struct os_infrared_info *)buffer;
infrared_send(infrared, info->addr, info->data, info->times);
os_spin_lock_irqsave(&gs_device_lock, &level);
dev->tx_count--;
os_spin_unlock_irqrestore(&gs_device_lock, level);
return size;
}详细解释:从 buffer 取出 os_infrared_info(addr/data/times),调用 infrared_send(见 3.5)发送完整 NEC 帧(引导码 + 32bit + 重复码)。发送是阻塞的(infrared_send 里 irk_on_us/irk_off_us 用 infrared_udelay 忙等),发完 tx_count-- 释放通道,返回 size。
3.7 注册 os_infrared_register_device
完整源码(drivers/infrared/infrared.c,与项目一致):
const static struct os_device_ops infrared_ops = {
.init = _infrared_init,
.deinit = _infrared_deinit,
.read = _infrared_read,
.write = _infrared_write,
.control = OS_NULL,
};
os_err_t os_infrared_register_device(const char *name, struct os_infrared_device *device)
{
os_device_t *dev;
dev = OS_DEVICE(device);
OS_ASSERT(dev != OS_NULL);
/* set device class and generic device interface */
dev->type = OS_DEVICE_TYPE_INFRARED;
dev->ops = &infrared_ops;
return os_device_register(dev, name);
}分步详解:
第 1 步:设置设备类型与操作表(无条件执行)
dev->type = OS_DEVICE_TYPE_INFRARED;
dev->ops = &infrared_ops;详细解释:设置类型 OS_DEVICE_TYPE_INFRARED 和操作表 infrared_ops(init/deinit/read/write)。
第 2 步:注册设备(无条件执行)
return os_device_register(dev, name);
}详细解释:用 name 注册到设备框架。打开设备时 _infrared_init 会配置 TX/RX 引脚并加入 infrared_list(供定时器超时检测遍历)。
四、完整使用示例
示例:接收并打印遥控按键
#include <infrared/infrared.h>
#include <device.h>
#include <os_stddef.h>
void infrared_recv_task(void *arg)
{
os_device_t *dev;
struct os_infrared_info info;
/* 1. 打开红外设备 */
dev = os_device_open("infrared0");
if (dev == OS_NULL)
{
os_kprintf("infrared0 open failed\r\n");
return;
}
/* 2. 循环读取遥控数据(驱动自动解码 NEC,入环形队列) */
while (1)
{
os_size_t len = os_device_read(dev, 0, &info, sizeof(info));
if (len == sizeof(info))
{
os_kprintf("IR: addr=0x%02x, data=0x%02x\r\n",
info.addr, info.data);
}
os_task_msleep(10);
}
}示例:发送红外遥控码
#include <infrared/infrared.h>
#include <device.h>
#include <os_stddef.h>
void infrared_send_demo(void)
{
os_device_t *dev;
struct os_infrared_info info;
dev = os_device_open("infrared0");
if (dev == OS_NULL)
{
os_kprintf("infrared0 open failed\r\n");
return;
}
/* 构造要发送的遥控码:地址 0x12、数据 0x34、发送 3 次重复 */
info.addr = 0x12;
info.data = 0x34;
info.times = 3;
os_device_write(dev, 0, &info, sizeof(info));
}说明:示例展示红外收发标准用法。实际效果:接收时驱动自动解码 NEC 帧并入环形队列,
os_device_read取出addr/data;发送时os_device_write传入os_infrared_info,驱动生成引导码 + 32bit 数据 + 重复码。
注意:
os_infrared_info的大小必须等于sizeof(struct os_infrared_info),否则OS_ASSERT失败。发送前要确保tx_pin已配置(_infrared_init中非OS_INFRARED_INVALIDE_PIN才会初始化发送)。接收为中断驱动,应用需轮询read(非阻塞)。
五、Infrared 的适配(接入新平台的 GPIO 引脚)
5.1 先认识适配层:红外驱动怎么"长"到新平台上
drivers/infrared/infrared.c 是跨平台统一框架(NEC 状态机解码 + 38kHz 载波调制 + 收发机制),不碰具体寄存器——它通过 os_pin_*(GPIO)和 os_clocksource_gettime/os_timer 等通用接口工作。接入新平台,只需在 BSP 里配置 TX/RX 引脚 + 注册设备,框架不用改。
适配全景图:
┌──────────────────────────────────────────────────────────┐
│ 应用层(os_device_read/write) │
└──────────────────────────────┬───────────────────────────┘
▼
┌──────────────────────────────────────────────────────────┐
│ Infrared 框架层(drivers/infrared/infrared.c,通用不改)│
│ NEC 状态机 + 38kHz 载波 + 环形队列 + 超时检测 │
└───────────────┬──────────────────────────┬────────────────┘
│ ①配置 TX/RX 引脚 │ ②os_infrared_register_device
▼ ▼
┌──────────────────────────────────────────────────────────┐
│ BSP 适配层(board.c / infrared_cfg)★需实现★ │
│ 定义引脚宏、初始化 GPIO、声明 os_infrared_device 并注册 │
└───────────────┬──────────────────────────┬────────────────┘
│ os_pin_* / os_clocksource│
▼ ▼
┌──────────────────────────────────────────────────────────┐
│ 硬件(红外接收头 + 发射管 + GPIO) │
└──────────────────────────────────────────────────────────┘适配的关键洞察:接入新平台只需做三件事:① 配置 RX 引脚为中断输入、TX 引脚为输出(os_pin_*);② 分配 os_infrared_device 填 tx_pin/rx_pin;③ os_infrared_register_device 注册。框架层不用改。红外驱动依赖 os_pin、os_clocksource 两个子系统(须已启用)。
5.2 接口契约:接入红外需要提供什么
| 层 | 内容 | 职责 | 说明 |
|---|---|---|---|
| GPIO | RX 引脚中断输入 | 接收头输出接 GPIO 中断 | _infrared_init 配 PIN_MODE_INPUT_PULLUP + 双边沿中断 |
| GPIO | TX 引脚输出 | 38kHz 载波输出 | tx_pin 非 OS_INFRARED_INVALIDE_PIN 才初始化 |
| 注册 | os_infrared_register_device | 注册为 OS_DEVICE_TYPE_INFRARED 设备 | 打开时 _infrared_init 配引脚、加链表 |
| 定时器 | infrared_timer_init | 超时检测(区分帧结束) | OS_INIT_CALL 设备层自动初始化 |
5.3 接入新平台的标准流程
第 1 步:确认硬件——红外接收头输出接哪只 GPIO(RX),发射管驱动接哪只 GPIO(TX)。
第 2 步:配置引脚——BSP 里确认 RX 引脚支持中断输入、TX 支持推挽输出;_infrared_init 会配好,但需确认引脚号正确。
第 3 步:分配并注册设备——定义 static struct os_infrared_device,填 tx_pin/rx_pin,os_infrared_register_device("infrared0", &device)。
第 4 步:确认依赖子系统——启用 os_pin、os_clocksource(os_clocksource_gettime),且 infrared_timer_init 被 OS_INIT_CALL 调用。
第 5 步:测试——接收:按遥控器看 os_device_read 是否出 addr/data;发送:os_device_write 后接收头能否收到。
5.4 适配检查清单(快速自查)
| 检查项 | 说明 |
|---|---|
| RX 引脚支持中断? | 不能中断则收不到跳变、状态机不流转 |
tx_pin/rx_pin 填对? | 填错则收/发不到信号 |
依赖 os_pin/os_clocksource 启用? | 未启用则 os_pin_*/os_clocksource_gettime 不可用 |
infrared_timer_init 被调用? | 未初始化超时定时器则帧结束检测失效 |
| 接收头极性对? | 不同接收头输出极性可能相反,PIN_LOW 判断需匹配 |
核心要点回顾
本节围绕 Infrared 红外设备展开,核心要点如下:
1. NEC 协议
- 帧 = 引导码(9ms 载波 + 4.5ms)+ 32bit(地址+反码+数据+反码)+ 结束位。
- bit 0 = 短间隔(550µs),bit 1 = 长间隔(1680µs)。
- 接收端用
usec > 1200 && usec < 2000判断 bit 1。
2. 6 态接收状态机
| 状态 | 含义 |
|---|---|
| IDLE | 空闲,等引导码 |
| SYNC | 已同步 |
| ADDR | 收 16bit 地址 |
| DATA | 收 16bit 数据 + 反码校验 |
| REPEAT0/REPEAT1 | 重复码处理 |
3. 载波调制
irk_on_us用 6µs 高 + 18µs 低(周期 24µs)产生 38kHz 载波。
4. 收发机制
- 接收:GPIO 中断 → 状态机解码 → 反码校验 → 环形队列 →
read。 - 发送:
write传os_infrared_info→infrared_send生成 NEC 帧。
5. 硬件适配
- 接入新平台 = 配置 TX/RX GPIO + 填
tx_pin/rx_pin+os_infrared_register_device,框架不用改。 - 依赖
os_pin、os_clocksource子系统,且infrared_timer_init需被OS_INIT_CALL调用。
关键易错点速查
| 易错点 | 正确做法 |
|---|---|
read/write 长度不对 | size 必须是 sizeof(struct os_infrared_info),否则 OS_ASSERT 失败 |
| 忽略非阻塞读 | read 无数据返回 0,应用需循环/阻塞轮询,不要假设一次就读到 |
| 发送引脚未配置 | 只有 tx_pin != OS_INFRARED_INVALIDE_PIN 才初始化发送,否则发送无效 |
| 误解载波 | 发送必须调 irk_on_us 产生 38kHz 载波,直接拉高电平无效 |
| 忽略重复码 | 长按按键会触发重复码(REPEAT0/REPEAT1),应用应根据 times 处理 |
| 队列满丢数据 | 环形队列 8 条,满了会丢新数据,应用应及时 read 清空 |
| RX 引脚不能中断 | RX 引脚必须是中断输入,否则收不到跳变、状态机不流转 |
| 依赖子系统未启用 | 未启用 os_pin/os_clocksource 则 os_pin_*/os_clocksource_gettime 不可用 |
下一步
掌握了 Infrared 红外设备后,接下来学习 SN 序列号设备:
- 5.31 SN序列号设备:了解 OneOS 通过 FAL 分区持久化存储设备唯一序列号。