LIRC (Linux Infrared remote control,Linux红外线遥控)
LIRC (Linux Infrared remote control,Linux红外线遥控)是一个开放源代码的包。这个包可以让你的Linux系统能够接收及发送红外线信号。
Microsoft Windows上有个功能相同的软件叫WinLIRC。
利用LIRC和红外线接收器,你就几乎可以用所有的红外线遥控器(例如:电视遥控器)来控制你的电脑。举例来说,你可以用遥控器来控制DVD或音乐的播放。
LIRC有个具有图形用户接口的前端,叫做KDELirc。它是用KDE的库编译的。
http://www.lirc.org/
Linux Infrared Remote
Control System
修订历史
版本 |
日期 |
作者 |
描述 |
0.1 |
2011-10-12 |
杜昌彬 |
初稿 |
|
|
|
|
1.红外遥控基础知识
1.1 红外遥控简介
红外遥控协议有有很多,比如RC-5,RC-6,NEC,SIRC等,不过协议都比较简单,基本上都是以脉冲宽度或脉冲间隔来编码。
当遥控器上按下按键时,遥控器逻辑单元会产生一个完整的逻辑脉冲波形,这个波形上包含了遥控命令的信息,他是红外传输的基带信号。这个波形被送到遥控器的调制单元,经调制单元调制成高频的红外电磁波信号,并由发光二极管发射出去。如下图的左边模块。
红外遥控的信号的产生和接收
红外电磁波信号现在一般使用一体化接收头接收,接收头同时完成了信号的解调和放大,其输出信号就是红外的基带脉冲信号。解调后的信号可直接送入信号处理器中由处理器对脉冲波形进行解码,也就是将经编码的脉冲信号翻译成逻辑数字。根据不同的控制协议,解码方式不同。
红外接收头,一跟线用于输出脉冲信号,其他两根是电源线和地线
1.2 红外遥控协议
下面已sony的SIRC协议为列说明。
编码
SIRC协议使用脉冲宽度对每一比特位进行编码,编码规则如下:
SIRC协议编码
首先,每一个脉冲后跟一个固定宽为600微秒的间隔,而每一个脉冲便是一个逻辑数字,并由脉冲的宽度决定是0还是1:脉冲宽度1200微秒表示逻辑1,宽度600微秒表示逻辑0.
幀格式
当按下遥控器上的按键时,遥控器会发送一个命令信号,这个信号就是一个幀,它包含了命令字段和地址(设备)字段,以及扩展字段。当按住按键不放时,遥控器会不断的发送这一命令信号,直到松开。
SIRC协议的幀格式有12位、15位、20位三种,如下所示:
一个幀以一个起始标志(图中的红色)开始,它是一个2400微秒的脉冲并跟一个间隔。之后是7字节的命令字段(图中的橙色),这个字段用于识别按下了遥控器上的哪个按键;然后是地址字段(图中的蓝色),用于识别控制的是什么类型的设备;对于20位宽格式,还有一个扩展字段,用于传输其他信息。
址值和设备类型映射表
一种命令值和具体按键的映射表
示例:Sony电视遥控器上“音量-”按键对应的脉冲波形
对于脉冲波形的解码,一般用一个专门的硬件单元完成,也可以在CPU中利用如GPIO等检测接收器输出的波形然后使用软件的方式解码,但这种方式效率显然很低。
2.Linux 对红外遥控的支持
Linux上通过LIRC子系统对红外控制提供支持,它包含几个部分:lirc核心、协议原始脉冲解码器、按键映射表、红外输入设备驱动。LIRC代码在:
drivers/media/IR
2.1. 协议原始脉冲解码器模块
解码器模块实现用软件的方法对原始脉冲进行解码。解码器用一个ir_raw_handler结构表示。
struct ir_raw_handler
{
struct list_head list;
int (*decode)(struct input_dev input_dev, struct ir_raw_event event); /* 解码函数 */
int (*raw_register)(struct input_dev input_dev); /* 注册函数 */
int (*raw_unregister)(struct input_dev input_dev); /* 卸载函数 */
};
解码器通过注册和卸载函数:
int ir_raw_handler_register(struct ir_raw_handler *ir_raw_handler)
void ir_raw_handler_unregister(struct ir_raw_handler *ir_raw_handler)
当注册解码器时, ir_raw_handler的 raw_register函数被调用,所以可在其进行一些解码器初始化工作。相应的,卸载时 raw_unregister函数被调用。所有注册的解码器放在一个全局链表ir_raw_handler_list中,lirc会便利每个解码器对报告的波形进行解码,注意,此时当任何一个解码器返回一个错误,后面的解码器不会被执行,所以不要将不使用的解码器模块同时加载到内核中。
解码器的主体就是 decode函数,lirc核心会将驱动报告的每个脉冲一次一次的传递到decode 函数,而decode 函数的实现就是一个状态机,每一次输入导致进入下一状态,直到一次解码完成,然后返回起始状态进行下次解码。
再LIRC中,每个脉冲(包括脉冲间隔)用一个ir_raw_event结构表示:
struct ir_raw_event
{
unsigned pulse:1; /* 是脉冲还是间隔 */
unsigned duration:31; /* 宽度,以ns为单位,一个0ns的脉冲表示重新开始解码 */
};
lirc中对于脉冲宽度的比较使用eq_margin()、geq_margin()函数,它允许宽度值在二分之一单元上下波动。
bool geq_margin(unsigned d1, unsigned d2, unsigned margin)
bool eq_margin(unsigned d1, unsigned d2, unsigned margin)
解码器的完整实现可参考sony sirc解码器实现:ir-sony-decoder.c
2.2. 按键映射表模块
按键映射模块都放在keymaps目录下。
不同的遥控器有不同的按键映射,按键映射模块的作用就是将扫描码与Linux input系统标准事件对应起来。
映射表的注册和卸载:
int ir_register_map(struct rc_keymap *map)
void ir_unregister_map(struct rc_keymap *map)
按键映射模块的主体就是一个ir_scancode结构数组,每个元素是一对按键映射。
2.3. 红外输入设备驱动
红外输入设备驱动负责向LIRC核心报告脉冲或直接报告扫描码事件。
红外输入设备用ir_input_dev结构描述:
struct ir_input_dev
{
struct device dev; /* device */
char *driver_name; /* Name of the driver module */
struct ir_scancode_table rc_tab; /* scan/key table */
unsigned long devno; /* device number */
const struct ir_dev_props *props; /* Device properties */
struct ir_raw_event_ctrl *raw; /* for raw pulse/space events */
struct input_dev *input_dev; /* the input device associated with this device */
/* key info - needed by IR keycode handlers */
spinlock_t keylock; /* protects the below members */
bool keypressed; /* current state */
unsigned long keyup_jiffies; /* when should the current keypress be released? */
struct timer_list timer_keyup; /* timer for releasing a keypress */
u32 last_keycode; /* keycode of last command */
u32 last_scancode; /* scancode of last command */
u8 last_toggle; /* toggle of last command */
};
设备注册和卸载:
int ir_input_register(struct input_dev *dev,const char *map_name,
const struct ir_dev_props *props,const char *driver_name)
void ir_input_unregister(struct input_dev *input_dev)
注册流程:
(1) 分配一个input设备,input_allocate_device();
(2) 对input设备进行一些初始化设置,但事件掩码不需要设置;
(3) 将分配的input设备结构的地址作为参数调用ir_input_register(),以后与lirc核心的交互都是通过这个input设备结构的地址进行的。
ir_input_register ()函数的map参数指定要使用的按键映射表,所有映射表定义在rc-map.h中,比如RC_MAP_RC5_TV。
props参数可以为NULL,但若要使用解码器模块对原始脉冲解码(比如无法直接从硬件获得扫描码时),则要设置。比如:
static struct ir_dev_props irc_props = {
.driver_type = RC_DRIVER_IR_RAW, /* 指定需要软件解码 */
.allowed_protos = IR_TYPE_SONY,
};
input_dev = input_allocate_device();
if (!irc_input_dev) {
ret = -ENOMEM;
goto err_input_allocate_device;
}
input_dev->name = "IRC";
ret = ir_input_register(input_dev, RC_MAP_RC5_TV, &irc_props, NULL);
if (ret) {
goto err_ir_input_register;
}
input_dev->rep[REP_DELAY] = 400;
input_dev->rep[REP_PERIOD] = 33;
对于可直接从硬件读取到扫描码的设备,可用以下函数包括扫描码事件:
void ir_keydown(struct input_dev *dev, int scancode, u8 toggle)
对于只能获得原始脉冲的设备,需先调用下面函数报告每个脉冲和脉冲间隔:
int ir_raw_event_store(struct input_dev *input_dev, struct ir_raw_event *ev);
int ir_raw_event_store_edge(struct input_dev *input_dev, enum raw_event_type type)
第一个函数要求驱动自己填充 ir_raw_event结构,并且在报告第一个脉冲前,需要调用ir_raw_event_reset()函数重置解码器,当驱动认为一个完成的波形已报告完毕后,调用ir_raw_event_handle()启动解码;第二个函数自动生成一个 ir_raw_event结构,脉冲宽度根据前后两次调用ir_raw_event_store_edge()函数的时间间隔字段计算,并且会自动调用ir_raw_event_reset()和ir_raw_event_handle()函数。 type参数指定是何脉冲。
enum raw_event_type
{
IR_SPACE = (1 << 0),
IR_PULSE = (1 << 1),
IR_START_EVENT = (1 << 2),
IR_STOP_EVENT = (1 << 3),
};
其他两个驱动常用的接口:
void ir_repeat(struct input_dev dev) /* 重复上次按键 */
u32 ir_g_keycode_from_table(struct input_dev input_dev, u32 scancode) /* 获得扫描码对应的按键*/
2.4. LIRC对按住按键时重复事件的处理
首先,重复是自动的,它使用了input子系统的REP功能。当第一次向lirc报一个扫描码事件时,lirc会向input子系统报告相应的按键事件,并启动一个定时器,该定时器在超时后会上报对应的按键松开事件;之后若在250ms(这个值等于input子系统的rep延时的默认值)内该设备又报告了同一个扫描码,这时只是将定时器再推后250ms,并不报告新的按键事件,也就是说按键的重复由input系统处理。这种设计主要是考虑到遥控器的限制,有的遥控器没有按键按下和松开之分(虽然在按下和松开时都有脉冲,但没有区分字段,而且有时可能会丢失信号)。
3.附 使用GPIO接收遥控命令驱动代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>
#include <linux/platform_device.h>
#include <linux/workqueue.h>
#include <linux/time.h>
#include <linux/irq.h>
#include <linux/spinlock.h>
#include <media/ir-core.h>
#include <asm/irq.h>
#define GPIO_IRC_MAP RC_MAP_SONY_SIRC12_TV
//#define DEBUG
static struct input_dev *irc_input_dev;
static struct timer_list irc_timer;
static atomic_t f_restart;
extern u64 gpt_get_cycles(void);
extern unsigned long gpt_delta_to_ns(u64 delta);
static void gpio_irc_raw_event_reset()
{
ir_raw_event_reset(irc_input_dev);
atomic_set(&f_restart,1);
}
static void gpio_irc_timer_func(unsigned long data)
{
struct ir_raw_event ev;
/* end pulse */
ev.pulse = false;
ev.duration = ~0u;
ir_raw_event_store(irc_input_dev,&ev);
ir_raw_event_handle(irc_input_dev);
gpio_irc_raw_event_reset();
#ifdef DEBUG
printk("gpio_irc restart event\n");
#endif
}
static irqreturn_t gpio_irc_rx_irq(int irq, void *dev_id)
{
static bool triger_falling = 1;
static unsigned long last;
struct ir_raw_event ev;
if (atomic_read(&f_restart)) {
atomic_set(&f_restart,0);
} else {
ev.pulse = !triger_falling;
ev.duration = gpt_delta_to_ns((unsigned long)gpt_get_cycles() - last);
ir_raw_event_store(irc_input_dev,&ev);
}
last = (unsigned long)gpt_get_cycles();
if (triger_falling)
set_irq_type(irq,IRQF_TRIGGER_RISING);
else
set_irq_type(irq,IRQF_TRIGGER_FALLING);
triger_falling = !triger_falling;
mod_timer(&irc_timer,jiffies + HZ/50);
return IRQ_HANDLED;
}
static struct ir_dev_props gpio_irc_props = {
.driver_type = RC_DRIVER_IR_RAW,
.allowed_protos = IR_TYPE_SONY,
};
static int gpio_irc_probe(struct platform_device *pdev)
{
int gpio,gpio_irq;
int ret;
gpio = pdev->resource[0].start;
gpio_irq = gpio_to_irq(gpio);
if (gpio_request(gpio,"gpio_irc")) {
dev_err(&pdev->dev, "rx pin not available\n");
return -1;
}
gpio_direction_input(gpio);
setup_timer(&irc_timer,gpio_irc_timer_func,gpio_irq);
irc_input_dev = input_allocate_device();
if (!irc_input_dev) {
ret = -ENOMEM;
goto err_input_allocate_device;
}
irc_input_dev->name = "MXC GPIO IR receiver";
ret = ir_input_register(irc_input_dev, GPIO_IRC_MAP, &gpio_irc_props, NULL);
if (ret) {
pr_err("gpio_irc:ir_input_register() failed\n");
goto err_ir_input_register;
}
irc_input_dev->rep[REP_DELAY] = 400;
irc_input_dev->rep[REP_PERIOD] = 33;
gpio_irc_raw_event_reset();
ret = request_irq(gpio_irq,gpio_irc_rx_irq,0,"gpio_irc",pdev);
if (ret) {
dev_err(&pdev->dev,"unabale to request irq\n");
goto err_request_irq;
}
set_irq_type(gpio_irq,IRQF_TRIGGER_FALLING);
return 0;
err_request_irq:
ir_input_unregister(irc_input_dev);
err_ir_input_register:
input_free_device(irc_input_dev);
err_input_allocate_device:
gpio_free(gpio);
return ret;
}
static int gpio_irc_remove(struct platform_device *pdev)
{
int gpio,gpio_irq;
del_timer_sync(&irc_timer);
gpio = pdev->resource[0].start;
gpio_irq = gpio_to_irq(gpio);
free_irq(gpio_irq,NULL);
gpio_free(gpio);
ir_input_unregister(irc_input_dev);
input_free_device(irc_input_dev);
return 0;
}
static int gpio_irc_suspend(struct platform_device *pdev, pm_message_t state)
{
return 0;
}
static int gpio_irc_resume(struct platform_device *pdev)
{
return 0;
}
static struct platform_driver irc_driver = {
.driver = {
.name = "gpio_irc",
},
.probe = gpio_irc_probe,
.remove = gpio_irc_remove,
.suspend = gpio_irc_suspend,
.resume = gpio_irc_resume,
};
static int gpio_irc_init(void)
{
return platform_driver_register(&irc_driver);
}
static void gpio_irc_exit(void)
{
platform_driver_unregister(&irc_driver);
}
late_initcall_sync(gpio_irc_init);
module_exit(gpio_irc_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("infrared remote control through gpio");