Linux之IR驱动

背景

在光谱中波长自760nm至400um的电磁波称为红外线,它是一种不可见光。红外遥控成本很低,以前广泛应用在电视,空调等电器的控制上面,现在随着蓝牙遥控器慢慢普及,红外遥控越来越少,但在某些场景,还保留着红外通信

ir

红外属于media子系统里面的rc(remote control)模块,所以相关驱动代码目录为 drivers/media/rc/

相关内核文档:

  • Documentation/devicetree/bindings/media/gpio-ir-receiver.txt
  • Documentation/devicetree/bindings/media/rc.yaml

下面就从红外的接收、发送和编解码协议简单记录下

接收

红外接收的处理有很多种方式,有些soc有专门的硬件模块,有些使用的是通用的gpio,这里以常见的gpio为例,其他的都大同小异。

用通用的GPIO来接收红外原理比较简单,代码实现主要在:

  • drivers/media/rc/gpio-ir-recv.c
  • drivers/media/rc/rc-main.c
  • drivers/media/rc/rc-ir-raw.c

主要流程

probe函数:
    --> devm_rc_register_device 注册rc设备
    --> devm_request_irq 申请gpio中断(上升沿和下降沿)

中断处理函数 gpio_ir_recv_irq:
    --> gpiod_get_value 获取gpio状态
    --> ir_raw_event_store_edge 保存边沿事件数据

重点的是这里的 devm_rc_register_device()ir_raw_event_store_edge() 函数,下面分开具体来看

devm_rc_register_device()函数

devm_rc_register_device
    -> rc_register_device
        -> ir_raw_event_prepare
            -> timer_setup ir_raw_edge_handle: 设置一个定时器
            -> INIT_KFIFO: 初始化一个fifo,用来保存ir数据
        -> rc_prepare_rx_device: rc_map,keymap相关处理
        -> lirc_register: 注册lirc,后面转发keycodes数据给用户空间
        -> rc_setup_rx_device: 注册input设备
        -> ir_raw_event_register
            -> kthread_run ir_raw_event_thread: 运行一个内核线程,
                -> kfifo_out: 从上面fifo里拿数据
                -> decode: 根据协议解码数据
                -> lirc_raw_event: 通过 lirc 转发到用户空间

注:

  • LIRC(Linux Infrared Remote Control), 主要提供与核外的交互接口,核外有一个对应的开源软件包,这里对 lirc 就不展开了
  • 关于keymap协议相关处理
  • 关于decode解码在后面的部分专门来说明

ir_raw_event_store_edge()函数

ir_raw_event_store_edge: 保存这次的电平pulse及上次边沿到这次边沿的时长duration
    -> ir_raw_event_store_with_timeout: 
        -> ktime_get: 保存这次的时间,为last_event
        -> ir_raw_event_store 
            -> kfifo_put: 保存包含pulse和duration数据(struct ir_raw_event结构)到上面的fifo中
        -> timer 设置timeout 默认15ms

定时器回调函数ir_raw_edge_handle()里面的处理:

ir_raw_edge_handle
    -> 判断时间间隔,ir_raw_event_store 保存超时事件
    -> ir_raw_event_handle 
        -> wake_up_process(dev->raw->thread) 唤醒处理线程

判断时间间隔说明:
如果从上次边沿触发到这次定时器触发的间隔时间interval大于 dev->timeout[这里gpio的方式默认为 IR_DEFAULT_TIMEOUT(125ms)] 就保存一个超时事件 timeout event,否则修改定时器的超时时间为 dev->timeout - ktime_to_us(interval)

发送

发送是接收的逆过程,红外发送主要有通用gpio、pwm等实现方式,主要代码在:

  • drivers/media/rc/gpio-ir-tx.c
  • drivers/media/rc/pwm-ir-tx.c
  • drivers/media/rc/rc-ir-raw.c
  • drivers/media/rc/lirc_dev.c

这里主要记录下常用的pwm方式,原理比较简单:

probe 函数: devm_rc_register_device: 注册rc 设备,重要的代码如下:

rcdev->priv = pwm_ir;
rcdev->driver_name = DRIVER_NAME;
rcdev->device_name = DEVICE_NAME;
rcdev->tx_ir = pwm_ir_tx;
rcdev->s_tx_duty_cycle = pwm_ir_set_duty_cycle;
rcdev->s_tx_carrier = pwm_ir_set_carrier;

rc = devm_rc_register_device(&pdev->dev, rcdev);
if (rc < 0)
    dev_err(&pdev->dev, "failed to register rc device\n");

最主要是下面三个函数:

  • pwm_ir_tx() 发送最重要的函数,负责控制pwm来发送红外,用户空间通过lirc最后会调用到这里
  • pwm_ir_set_duty_cycle(): 设置pwm的占空比
  • pwm_ir_set_carrier(): 设置pwm载波的频率,默认的是38000,即38K

pwm_ir_tx()函数说明

代码如下,

static int pwm_ir_tx(struct rc_dev *dev, unsigned int *txbuf,
             unsigned int count)
{
    struct pwm_ir *pwm_ir = dev->priv;
    struct pwm_device *pwm = pwm_ir->pwm;
    int i, duty, period;
    ktime_t edge;
    long delta;

    period = DIV_ROUND_CLOSEST(NSEC_PER_SEC, pwm_ir->carrier);
    duty = DIV_ROUND_CLOSEST(pwm_ir->duty_cycle * period, 100);

    pwm_config(pwm, duty, period);

    edge = ktime_get();

    for (i = 0; i < count; i++) {
        if (i % 2) // space
            pwm_disable(pwm);
        else
            pwm_enable(pwm);

        edge = ktime_add_us(edge, txbuf[i]);
        delta = ktime_us_delta(edge, ktime_get());
        if (delta > 0)
            usleep_range(delta, delta + 10);
    }

    pwm_disable(pwm);

    return count;
}

为啥发送这里没有协议编码相关的呢?

上面已经说了,tx_ir函数即这里的pwm_ir_tx()最后会被lirc调用到来发送,所以相关协议编码主要在lirc代码里,即lirc_transmit()里的ir_raw_encode_scancode()函数,调用流程如下:

用户空间调用lirc的write函数
    -> lirc_transmit()
        -> ir_raw_encode_scancode()
            -> encode(): 协议编码
        -> tx_ir(): 发送函数
            -> pwm_ir_tx(): 这里pwm就对应此函数

编解码协议

这里主要记录下最常见最常用的协议之一—NEC协议

NEC协议介绍

NEC协议是众多红外线协议中的一种,以前广泛用在电视机,投影仪设备里,之前的万能电视遥控器就是走的NEC协议

​NEC协议的特征: ​

  1. 8位地址码和8位命令码长度;
  2. 单次传输主要分为5部分(不算重复码): 引导码+地址码+地址反码+命令码+命令反码,地址和命令两次传输,提高准确性;
  3. 停止码主要起隔离作用,一般不进行判断
  4. 载波频率为38KHz
  5. 脉冲时间间隔调制
  6. 位时间为1.125ms和2.25ms,具体见下面说明

NEC码位的定义:一个脉冲对应562.5us的连续载波,一个逻辑1传输需要2.25ms(562.5us脉冲+1687.5us低电平),一个逻辑0的传输需要1.125ms(562.5us脉冲+562.5us低电平)。
​而遥控接收头在收到脉冲时为低电平​,在没有收到脉冲时为高电平,因此, 我们在接收头端收到的信号为:逻辑1应该是562.5us低+1687.5us高,逻辑0应该是562.5us低+562.5us高。

对于接收方:

  • 引导码: 9ms的低电平 + 4.5ms的高电平
  • 逻辑0: 562.5us低电平 + 562.5us高电平
  • 逻辑1: 562.5us低电平 + 1687.5us高电平
  • 重复码:9ms的低电平 + 2.25ms的高电平

对于发送方:
如果我们规定1拍是562.5us载波脉冲, 那么:

  • 引导码: 16拍的红外发射 + 8拍的空闲
  • 逻辑0: 1拍的发射 + 1拍的空闲
  • 逻辑1: 1拍的发射 + 3拍的空闲
  • 重复码:16拍的红外发射 + 4拍的空闲
  • 结束码:1拍的发射

ir_nec

NEC相关代码

内核中nec协议相关的源码实现在: drivers/media/rc/ir-nec-decoder.c
主要提供上面接收和发送时编解码相关的接口和参数,如decode, encode函数,carrier参数等

static struct ir_raw_handler nec_handler = {
    .protocols  = RC_PROTO_BIT_NEC | RC_PROTO_BIT_NECX |
                            RC_PROTO_BIT_NEC32,
    .decode     = ir_nec_decode,
    .encode     = ir_nec_encode,
    .carrier    = 38000,
    .min_timeout    = NEC_TRAILER_SPACE,
};

具体代码实现这里就不展开了,感兴趣可自行参看源码

参考