4. CEC驱动开发

好,咱们今天来聊聊CEC驱动开发。说实话,这部分内容在HDMI整个体系中属于比较“硬核”的环节。很多工程师做上层应用做得风生水起,一碰到驱动层就头疼。我当年刚接触CEC时,也踩了不少坑。

先给大家画个知识地图,看看我们今天要聊什么。

CEC驱动开发知识体系 Linux CEC 驱动 驱动框架 cec-core 子系统 cec_adapter 结构体 cec_ops 回调函数 消息收发 API cec_transmit_msg() cec_receive_msg() 消息队列管理 中断与轮询 中断处理流程 轮询模式实现 性能对比分析 目标:实现稳定可靠的 CEC 通信

4.1 Linux CEC 驱动框架

Linux内核从4.10版本开始,正式引入了CEC驱动框架。说白了,就是内核帮你搭好了架子,你只需要往里面填具体硬件的实现代码就行。

我个人习惯把CEC驱动框架分成三层来看:

  • 核心层(cec-core):内核提供的通用框架,负责设备注册、消息路由、状态管理
  • 适配层(cec_adapter):硬件抽象接口,你写的驱动主要就是实现这个结构体
  • 硬件层(具体驱动):操作I2C、GPIO、中断控制器等底层硬件

来看一个典型的cec_adapter结构体定义:

struct cec_adapter {
    struct device *dev;           /* 关联的设备 */
    const struct cec_ops *ops;    /* 操作函数集 */
    u32 capabilities;             /* 硬件能力标志 */
    u32 available_log_addrs;      /* 可用逻辑地址 */
    struct cec_msg rx_msg;        /* 接收消息缓冲区 */
    struct mutex lock;            /* 互斥锁 */
    /* ... 还有其他成员 */
};

关键点:cec_ops是你必须实现的回调函数集。包括发送消息、接收消息、设置逻辑地址、电源管理等。我见过不少新手把ops实现得乱七八糟,结果消息收发总出问题。

4.2 CEC消息收发API

消息收发是CEC驱动的核心功能。你想想看,整个CEC协议就是靠消息来沟通的,消息收发搞不定,其他都是白搭。

内核提供了几个关键的API:

API函数 功能说明 使用场景
cec_transmit_msg() 发送一条CEC消息 应用层下发指令时调用
cec_receive_msg() 接收一条CEC消息 硬件收到消息后回调
cec_queue_msg() 将消息放入队列 中断上下文中使用
cec_transmit_done() 通知发送完成 发送中断处理中调用

我记得有一次调试一个CEC驱动,发现发送消息总是超时。查了半天,原来是cec_transmit_done()没在正确的地方调用。内核框架等着这个回调来释放发送锁,你不调用,它就一直卡着。

来看一个发送消息的典型实现:

static int my_cec_transmit(struct cec_adapter *adap, u8 attempts, u32 signal_free_time, struct cec_msg *msg)
{
    struct my_cec_dev *cec = adap->priv;
    unsigned long flags;
    int ret = 0;

    /* 检查硬件是否空闲 */
    if (cec->tx_busy) {
        dev_err(cec->dev, "发送忙,请稍后重试\n");
        return -EBUSY;
    }

    /* 将消息写入硬件发送寄存器 */
    spin_lock_irqsave(&cec->lock, flags);
    writeb(msg->msg[0], cec->regs + CEC_TX_DATA0);
    writeb(msg->len, cec->regs + CEC_TX_LEN);
    writeb(CEC_TX_START, cec->regs + CEC_TX_CTRL);
    cec->tx_busy = true;
    spin_unlock_irqrestore(&cec->lock, flags);

    return ret;
}

小技巧:发送消息时,建议用spinlock保护硬件寄存器访问。我曾经用mutex导致中断上下文死锁,排查了一整天。中断处理里千万别用mutex!

4.3 中断处理与轮询模式

嗯,这里要注意。CEC驱动的消息接收有两种模式:中断模式和轮询模式。选哪个?得看你的硬件支持和实时性要求。

4.3.1 中断模式

中断模式是首选方案。硬件收到CEC消息后,会触发一个中断信号,驱动在中断处理函数中读取消息。这样做的好处是CPU利用率高,响应快。

中断处理流程大致如下:

  1. 硬件检测到CEC总线上的起始条件
  2. 硬件接收完整消息,触发中断
  3. 中断处理函数读取消息内容
  4. 调用cec_receive_msg()将消息上报给核心层
  5. 核心层唤醒等待的用户空间进程

我曾经在一个项目里,中断处理函数写得过于复杂,在里面做了消息解析和日志打印。结果导致中断响应时间过长,丢消息了。记住,中断处理要快进快出。

static irqreturn_t my_cec_irq_handler(int irq, void *dev_id)
{
    struct my_cec_dev *cec = dev_id;
    u8 status;
    struct cec_msg msg;

    status = readb(cec->regs + CEC_INT_STATUS);

    if (status & CEC_INT_RX) {
        /* 读取接收到的消息 */
        msg.len = readb(cec->regs + CEC_RX_LEN);
        for (int i = 0; i < msg.len; i++)
            msg.msg[i] = readb(cec->regs + CEC_RX_DATA0 + i);

        /* 上报给核心层 */
        cec_receive_msg(cec->adap, &msg);
    }

    if (status & CEC_INT_TX_DONE) {
        /* 发送完成,释放发送锁 */
        cec->tx_busy = false;
        cec_transmit_done(cec->adap);
    }

    /* 清除中断标志 */
    writeb(status, cec->regs + CEC_INT_CLEAR);

    return IRQ_HANDLED;
}

4.3.2 轮询模式

轮询模式就简单粗暴了。驱动定时去检查硬件寄存器,看看有没有新消息。适合那些不支持中断的廉价硬件。

但说实话,轮询模式效率低。你想想看,CPU要不停地去读寄存器,浪费算力。而且轮询间隔不好把握——间隔太短浪费CPU,间隔太长可能丢消息。

避坑指南:我曾经在一个低功耗项目里用了轮询模式,结果设备待机功耗超标。后来改成中断模式,功耗降了30%。如果你的设备对功耗敏感,尽量用中断。

轮询模式的实现一般用内核定时器:

static void my_cec_poll_timer(struct timer_list *t)
{
    struct my_cec_dev *cec = from_timer(cec, t, poll_timer);
    u8 rx_pending;

    rx_pending = readb(cec->regs + CEC_RX_PENDING);
    if (rx_pending) {
        struct cec_msg msg;
        msg.len = readb(cec->regs + CEC_RX_LEN);
        for (int i = 0; i < msg.len; i++)
            msg.msg[i] = readb(cec->regs + CEC_RX_DATA0 + i);
        cec_receive_msg(cec->adap, &msg);
    }

    /* 重新启动定时器 */
    mod_timer(&cec->poll_timer, jiffies + msecs_to_jiffies(10));
}

4.4 两种模式的对比

我整理了一个对比表格,方便大家选型时参考:

对比项 中断模式 轮询模式
CPU占用率 低(事件触发) 高(持续占用)
响应延迟 微秒级 取决于轮询间隔
功耗 低 较高
实现复杂度 中等 简单
硬件要求 需要中断控制器 无特殊要求
适用场景 主流方案,推荐使用 低成本、低复杂度场景

我的建议:除非你的硬件实在不支持中断,否则一律用中断模式。轮询模式只适合做调试或者极低成本的方案。我在实际项目中,99%的情况都用中断。

好了,CEC驱动开发的核心内容就这些。驱动框架是骨架,消息API是血肉,中断和轮询是心跳。把这三块搞明白,写一个稳定的CEC驱动就不难了。

记住,驱动开发最怕想当然。每行代码都要想清楚:如果硬件出错了怎么办?如果中断丢失了怎么办?多问自己几个为什么,代码质量自然就上去了。


可可精品资料库