6. CEC内核支持

CEC框架为HDMI CEC硬件提供了一个统一的内核接口。它旨在处理多种类型的硬件(接收器、发射器、USB加密狗)。该框架还允许选择在内核驱动程序中执行哪些操作,以及哪些操作应由用户空间应用程序处理。此外,它还将远程控制直通功能集成到内核的远程控制框架中。

6.1. CEC协议

CEC协议使消费电子设备能够通过HDMI连接相互通信。该协议在通信中使用逻辑地址。逻辑地址与设备提供的功能严格相关。作为通信枢纽的电视始终分配地址0。物理地址由设备之间的物理连接确定。

此处描述的CEC框架与CEC 2.0规范保持同步。它在HDMI 1.4规范中进行了文档化,新的2.0位在HDMI 2.0规范中进行了文档化。但对于大多数功能而言,可免费获得的HDMI 1.3a规范就足够了。

https://www.hdmi.org/spec/index

6.2. CEC适配器接口

struct cec_adapter表示CEC适配器硬件。它是通过调用cec_allocate_adapter()创建的,并通过调用cec_delete_adapter()删除的

struct cec_adapter *cec_allocate_adapter(const struct cec_adap_ops *ops, void *priv, const char *name, u32 caps, u8 available_las);
void cec_delete_adapter(struct cec_adapter *adap);

要创建适配器,您需要传递以下信息

ops

适配器操作,由CEC框架调用,您必须实现这些操作。

priv

将存储在adap->priv中,并且可以被适配器操作使用。使用cec_get_drvdata(adap)获取priv指针。

name

CEC适配器的名称。注意:此名称将被复制。

caps

CEC适配器的功能。这些功能决定了硬件的功能以及哪些部分由用户空间处理,哪些部分由内核空间处理。这些功能由CEC_ADAP_G_CAPS返回。

available_las

此适配器可以同时处理的逻辑地址的数量。必须为 1 <= available_las <= CEC_MAX_LOG_ADDRS。

要获取priv指针,请使用此辅助函数

void *cec_get_drvdata(const struct cec_adapter *adap);

要注册/dev/cecX设备节点和远程控制设备(如果设置了CEC_CAP_RC),请调用

int cec_register_adapter(struct cec_adapter *adap, struct device *parent);

其中parent是父设备。

要取消注册设备,请调用

void cec_unregister_adapter(struct cec_adapter *adap);

注意:如果cec_register_adapter()失败,则调用cec_delete_adapter()进行清理。但是,如果cec_register_adapter()成功,则仅调用cec_unregister_adapter()进行清理,切勿调用cec_delete_adapter()。一旦/dev/cecX设备的最后一个用户关闭其文件句柄,取消注册功能将自动删除适配器。

6.3. 实现低级CEC适配器

以下低级适配器操作必须在您的驱动程序中实现

struct cec_adap_ops
struct cec_adap_ops
{
        /* Low-level callbacks */
        int (*adap_enable)(struct cec_adapter *adap, bool enable);
        int (*adap_monitor_all_enable)(struct cec_adapter *adap, bool enable);
        int (*adap_monitor_pin_enable)(struct cec_adapter *adap, bool enable);
        int (*adap_log_addr)(struct cec_adapter *adap, u8 logical_addr);
        void (*adap_unconfigured)(struct cec_adapter *adap);
        int (*adap_transmit)(struct cec_adapter *adap, u8 attempts,
                              u32 signal_free_time, struct cec_msg *msg);
        void (*adap_nb_transmit_canceled)(struct cec_adapter *adap,
                                          const struct cec_msg *msg);
        void (*adap_status)(struct cec_adapter *adap, struct seq_file *file);
        void (*adap_free)(struct cec_adapter *adap);

        /* Error injection callbacks */
        ...

        /* High-level callback */
        ...
};

这些低级操作处理控制CEC适配器硬件的各个方面。它们都是在互斥锁adap->lock被持有的情况下调用的。

启用/禁用硬件

int (*adap_enable)(struct cec_adapter *adap, bool enable);

此回调启用或禁用CEC硬件。启用CEC硬件意味着将其上电,使其处于未声明任何逻辑地址的状态。如果设置了CEC_CAP_NEEDS_HPD,则物理地址始终有效。如果未设置该功能,则在启用CEC硬件时,物理地址可能会更改。CEC驱动程序不应设置CEC_CAP_NEEDS_HPD,除非硬件设计要求这样做,因为这将使其无法唤醒在待机模式下将HPD拉低的显示器。调用cec_allocate_adapter()后,CEC适配器的初始状态为禁用。

请注意,如果enable为false,则adap_enable必须返回0。

启用/禁用“监视所有”模式

int (*adap_monitor_all_enable)(struct cec_adapter *adap, bool enable);

如果启用,则应将适配器置于一种模式,以便监视不属于我们的消息。并非所有硬件都支持此功能,并且仅当设置了CEC_CAP_MONITOR_ALL功能时才调用此函数。此回调是可选的(某些硬件可能始终处于“监视所有”模式)。

请注意,如果enable为false,则adap_monitor_all_enable必须返回0。

启用/禁用“监视引脚”模式

int (*adap_monitor_pin_enable)(struct cec_adapter *adap, bool enable);

如果启用,则应将适配器置于一种模式,以便监视CEC引脚变化。并非所有硬件都支持此功能,并且仅当设置了CEC_CAP_MONITOR_PIN功能时才调用此函数。此回调是可选的(某些硬件可能始终处于“监视引脚”模式)。

请注意,如果enable为false,则adap_monitor_pin_enable必须返回0。

要编程一个新的逻辑地址

int (*adap_log_addr)(struct cec_adapter *adap, u8 logical_addr);

如果 logical_addr == CEC_LOG_ADDR_INVALID,则应擦除所有已编程的逻辑地址。否则,应编程给定的逻辑地址。如果超过了可用的最大逻辑地址数,则应返回 -ENXIO。一旦编程了逻辑地址,CEC 硬件就可以接收发送到该地址的定向消息。

请注意,如果 logical_addr 是 CEC_LOG_ADDR_INVALID,则 adap_log_addr 必须返回 0。

在适配器未配置时调用

void (*adap_unconfigured)(struct cec_adapter *adap);

适配器已未配置。如果驱动程序在未配置后必须采取特定操作,则可以通过此可选回调完成。

要发送一条新消息

int (*adap_transmit)(struct cec_adapter *adap, u8 attempts,
                     u32 signal_free_time, struct cec_msg *msg);

这将发送一条新消息。attempts 参数是建议的传输尝试次数。

signal_free_time 是适配器在尝试发送消息之前,当线路空闲时应等待的数据位周期数。此值取决于此传输是重试、来自新发起者的消息还是同一发起者的新消息。大多数硬件将自动处理此问题,但在某些情况下,需要此信息。

可以使用 CEC_FREE_TIME_TO_USEC 宏将 signal_free_time 转换为微秒(一个数据位周期为 2.4 毫秒)。

要传递取消的非阻塞传输的结果

void (*adap_nb_transmit_canceled)(struct cec_adapter *adap,
                                  const struct cec_msg *msg);

可以使用此可选回调来获取具有序列号 msg->sequence 的已取消的非阻塞传输的结果。如果传输中止、传输超时(即硬件从未发出传输完成信号)或者传输成功,但等待预期回复被中止或超时,则会调用此回调。

记录当前 CEC 硬件状态

void (*adap_status)(struct cec_adapter *adap, struct seq_file *file);

可以使用此可选回调来显示 CEC 硬件的状态。该状态可通过 debugfs 获取:cat /sys/kernel/debug/cec/cecX/status

在删除适配器时释放任何资源

void (*adap_free)(struct cec_adapter *adap);

可以使用此可选回调来释放驱动程序可能已分配的任何资源。它从 cec_delete_adapter 调用。

您的适配器驱动程序还必须通过在以下情况下调用框架来响应事件(通常是中断驱动的)

当传输完成时(成功或其他)

void cec_transmit_done(struct cec_adapter *adap, u8 status,
                       u8 arb_lost_cnt,  u8 nack_cnt, u8 low_drive_cnt,
                       u8 error_cnt);

或者

void cec_transmit_attempt_done(struct cec_adapter *adap, u8 status);

状态可以是以下之一

CEC_TX_STATUS_OK

传输成功。

CEC_TX_STATUS_ARB_LOST

仲裁丢失:另一个 CEC 发起者控制了 CEC 线路,并且您失去了仲裁。

CEC_TX_STATUS_NACK

消息被否定确认(对于定向消息)或确认(对于广播消息)。需要重新传输。

CEC_TX_STATUS_LOW_DRIVE

在 CEC 总线上检测到低驱动。这表示从机检测到总线上有错误,并请求重新传输。

CEC_TX_STATUS_ERROR

发生了一些未指定的错误:如果硬件无法区分,则可以是 ARB_LOST 或 LOW_DRIVE,或完全是其他错误。某些硬件仅支持 OK 和 FAIL 作为传输结果,即无法区分不同的可能错误。在这种情况下,将 FAIL 映射到 CEC_TX_STATUS_NACK,而不是 CEC_TX_STATUS_ERROR。

CEC_TX_STATUS_MAX_RETRIES

多次尝试后无法传输消息。只有当驱动程序具有硬件支持来重试消息时,才应由驱动程序设置。如果设置了此项,则框架会假设它不必再次尝试传输消息,因为硬件已经这样做了。

硬件必须能够区分 OK、NACK 和“其他情况”。

*_cnt 参数是看到的错误条件数。如果没有可用信息,则此值可能为 0。不支持硬件重试的驱动程序可以将与传输错误对应的计数器设置为 1,如果硬件支持重试,则可以将这些计数器设置为 0(如果硬件不提供发生哪些错误以及发生多少次的反馈),或者填写硬件报告的正确值。

请注意,如果在队列中有一个挂起的传输,则调用这些函数会立即开始新的传输。因此,请确保硬件处于可以启动新传输的状态,然后再调用这些函数。

cec_transmit_attempt_done() 函数是一个助手函数,适用于硬件从不重试的情况,因此传输始终只进行一次尝试。它将反过来调用 cec_transmit_done(),为与状态对应的 count 参数填充 1。如果状态为 OK,则全部填充 0。

当接收到 CEC 消息时

void cec_received_msg(struct cec_adapter *adap, struct cec_msg *msg);

不言自明。

6.4. 实现中断处理程序

通常,CEC 硬件会提供中断,以指示传输何时完成以及是否成功,并且在接收到 CEC 消息时提供中断。

CEC 驱动程序应始终先处理传输中断,然后再处理接收中断。框架希望在 cec_received_msg 调用之前看到 cec_transmit_done 调用,否则如果接收到的消息是对发送的消息的回复,则会感到困惑。

6.5. 可选:实现错误注入支持

如果 CEC 适配器支持错误注入功能,则可以通过错误注入回调公开该功能

struct cec_adap_ops {
        /* Low-level callbacks */
        ...

        /* Error injection callbacks */
        int (*error_inj_show)(struct cec_adapter *adap, struct seq_file *sf);
        bool (*error_inj_parse_line)(struct cec_adapter *adap, char *line);

        /* High-level CEC message callback */
        ...
};

如果同时设置了这两个回调,则 error-inj 文件将出现在 debugfs 中。基本语法如下

忽略前导空格/制表符。如果下一个字符是 # 或到达行尾,则忽略整行。否则,会需要一个命令。

此基本解析在 CEC 框架中完成。由驱动程序决定要实现哪些命令。唯一的要求是必须实现没有参数的命令 clear,并且它将删除所有当前的错误注入命令。

这确保您可以始终执行 echo clear >error-inj 来清除任何错误注入,而无需了解特定于驱动程序的命令的详细信息。

请注意,error-inj 的输出应作为 error-inj 的输入有效。因此,此操作必须有效

$ cat error-inj >einj.txt
$ cat einj.txt >error-inj

读取此文件时会调用第一个回调,它应显示当前错误注入状态

int (*error_inj_show)(struct cec_adapter *adap, struct seq_file *sf);

建议它以包含基本用法信息的注释块开头。如果成功,则返回 0;否则,返回错误。

第二个回调将解析写入 error-inj 文件的命令

bool (*error_inj_parse_line)(struct cec_adapter *adap, char *line);

line 参数指向命令的开头。已跳过任何前导空格或制表符。它仅是一行(因此没有嵌入的换行符),并且以 0 结尾。回调可以自由修改缓冲区的内容。仅对包含命令的行调用此回调,因此不会对空行或注释行调用此回调。

如果命令有效,则返回 true;如果有语法错误,则返回 false。

6.6. 实现高级 CEC 适配器

底层操作驱动硬件,高级操作由 CEC 协议驱动。在不持有 adap->lock 互斥锁的情况下调用高级回调。可以使用以下高级回调

struct cec_adap_ops {
        /* Low-level callbacks */
        ...

        /* Error injection callbacks */
        ...

        /* High-level CEC message callback */
        void (*configured)(struct cec_adapter *adap);
        int (*received)(struct cec_adapter *adap, struct cec_msg *msg);
};

在配置适配器时调用

void (*configured)(struct cec_adapter *adap);

适配器已完全配置,即已成功声明所有逻辑地址。如果驱动程序在配置后必须采取特定操作,则可以通过此可选回调完成。

received() 回调允许驱动程序选择处理新接收到的 CEC 消息

int (*received)(struct cec_adapter *adap, struct cec_msg *msg);

如果驱动程序想要处理 CEC 消息,则它可以实现此回调。如果它不想处理此消息,则应返回 -ENOMSG,否则 CEC 框架会假设它已处理此消息,并且不会对其进行任何操作。

6.7. CEC 框架函数

CEC 适配器驱动程序可以调用以下 CEC 框架函数

int cec_transmit_msg(struct cec_adapter *adap, struct cec_msg *msg, bool block);

发送 CEC 消息。如果 block 为 true,则等待直到消息已发送;否则,只需将其排队并返回。

void cec_s_phys_addr(struct cec_adapter *adap, u16 phys_addr, bool block);

更改物理地址。此函数将设置 adap->phys_addr,并在其更改时发送事件。如果已调用 cec_s_log_addrs() 且物理地址已变为有效,则 CEC 框架将开始声明逻辑地址。如果 block 为 true,则此函数将不会返回,直到此过程完成。

当物理地址设置为有效值时,CEC 适配器将被启用(请参阅 adap_enable 操作)。当设置为 CEC_PHYS_ADDR_INVALID 时,CEC 适配器将被禁用。如果您将一个有效的物理地址更改为另一个有效的物理地址,则此函数将首先将地址设置为 CEC_PHYS_ADDR_INVALID,然后再启用新的物理地址。

void cec_s_phys_addr_from_edid(struct cec_adapter *adap, const struct edid *edid);

一个辅助函数,它从 edid 结构中提取物理地址,并使用该地址调用 cec_s_phys_addr(),如果 EDID 未包含物理地址或 edid 是 NULL 指针,则调用 CEC_PHYS_ADDR_INVALID。

int cec_s_log_addrs(struct cec_adapter *adap, struct cec_log_addrs *log_addrs, bool block);

声明 CEC 逻辑地址。如果设置了 CEC_CAP_LOG_ADDRS,则永远不应调用此函数。如果 block 为 true,则等待直到逻辑地址被声明,否则仅将其排队并返回。要取消配置所有逻辑地址,请调用此函数,并将 log_addrs 设置为 NULL 或将 log_addrs->num_log_addrs 设置为 0。取消配置时将忽略 block 参数。如果物理地址无效,则此函数将直接返回。一旦物理地址变为有效,框架将尝试声明这些逻辑地址。

6.8. CEC 引脚框架

大多数 CEC 硬件都基于完整的 CEC 消息运行,其中软件提供消息,硬件处理低级 CEC 协议。但是,某些硬件仅驱动 CEC 引脚,而软件必须处理低级 CEC 协议。创建 CEC 引脚框架是为了处理此类设备。

请注意,由于接近实时的要求,永远无法保证 100% 工作。此框架在内部使用高分辨率定时器,但是如果定时器延迟超过 300 微秒,则可能会发生错误的结果。实际上,它似乎相当可靠。

这种低级实现的一个优点是,它可以作为廉价的 CEC 分析器使用,尤其是在可以使用中断来检测 CEC 引脚从低电平到高电平或反向的转换时。

struct cec_pin_ops

低级 CEC 引脚操作

定义:

struct cec_pin_ops {
    int (*read)(struct cec_adapter *adap);
    void (*low)(struct cec_adapter *adap);
    void (*high)(struct cec_adapter *adap);
    bool (*enable_irq)(struct cec_adapter *adap);
    void (*disable_irq)(struct cec_adapter *adap);
    void (*free)(struct cec_adapter *adap);
    void (*status)(struct cec_adapter *adap, struct seq_file *file);
    int (*read_hpd)(struct cec_adapter *adap);
    int (*read_5v)(struct cec_adapter *adap);
    int (*received)(struct cec_adapter *adap, struct cec_msg *msg);
};

成员

read

读取 CEC 引脚。如果为高电平,则返回 > 0,如果为低电平,则返回 0,如果出现错误,则返回负数。

low

将 CEC 引脚驱动为低电平。

high

停止驱动 CEC 引脚。上拉电阻会将引脚驱动为高电平,除非其他设备正在将引脚驱动为低电平。

enable_irq

可选,启用中断以检测引脚电压变化。

disable_irq

可选,禁用中断。

free

可选。释放所有已分配的资源。在删除适配器时调用。

status

可选,记录状态信息。

read_hpd

可选。读取 HPD 引脚。如果为高电平,则返回 > 0,如果为低电平,则返回 0,如果出现错误,则返回负数。

read_5v

可选。读取 5V 引脚。如果为高电平,则返回 > 0,如果为低电平,则返回 0,如果出现错误,则返回负数。

received

可选。高级 CEC 消息回调。允许驱动程序处理 CEC 消息。

描述

这些操作(除了 received 操作之外)由 CEC 引脚框架用于操作 CEC 引脚。

void cec_pin_changed(struct cec_adapter *adap, bool value)

从中断更新引脚状态

参数

struct cec_adapter *adap

指向 cec 适配器的指针

bool value

当为 true 时,引脚为高电平,否则为低电平

描述

如果通过中断检测到 CEC 电压的变化,则会使用新值从中断调用 cec_pin_changed。

struct cec_adapter *cec_pin_allocate_adapter(const struct cec_pin_ops *pin_ops, void *priv, const char *name, u32 caps)

分配一个基于引脚的 cec 适配器

参数

const struct cec_pin_ops *pin_ops

低级引脚操作

void *priv

将存储在adap->priv中,并且可以被适配器操作使用。使用cec_get_drvdata(adap)获取priv指针。

const char *name

CEC适配器的名称。注意:此名称将被复制。

u32 caps

CEC 适配器的功能。这将与 CEC_CAP_MONITOR_ALL 和 CEC_CAP_MONITOR_PIN 进行 OR 运算。

描述

使用 cec 引脚框架分配一个 cec 适配器。

返回

指向 cec 适配器的指针或错误指针

6.9. CEC 通知器框架

大多数 drm HDMI 实现都具有集成的 CEC 实现,不需要通知器支持。但是,某些实现具有独立的 CEC 实现,这些实现有自己的驱动程序。这可以是 SoC 的 IP 块,也可以是处理 CEC 引脚的完全独立的芯片。对于这些情况,drm 驱动程序可以安装一个通知器,并使用该通知器来通知 CEC 驱动程序物理地址的更改。

struct cec_notifier *cec_notifier_conn_register(struct device *hdmi_dev, const char *port_name, const struct cec_connector_info *conn_info)

查找或为给定的 HDMI 设备和连接器元组创建一个新的 cec_notifier。

参数

struct device *hdmi_dev

发送事件的 HDMI 设备。

const char *port_name

发生事件的连接器名称。如果 HDMI 设备始终只创建一个 HDMI 连接器,则可以为 NULL。

const struct cec_connector_info *conn_info

发生事件的连接器信息(可以为 NULL)

描述

如果设备 dev 和连接器 port_name 的通知器已存在,则增加 refcount 并返回该通知器。

如果不存在,则分配一个新的通知器结构并返回指向该新结构的指针。

如果无法分配内存,则返回 NULL。

void cec_notifier_conn_unregister(struct cec_notifier *n)

减少 refcount,当 refcount 达到 0 时删除。

参数

struct cec_notifier *n

通知器。如果为 NULL,则此函数不执行任何操作。

struct cec_notifier *cec_notifier_cec_adap_register(struct device *hdmi_dev, const char *port_name, struct cec_adapter *adap)

查找或为给定设备创建一个新的 cec_notifier。

参数

struct device *hdmi_dev

发送事件的 HDMI 设备。

const char *port_name

发生事件的连接器名称。如果 HDMI 设备始终只创建一个 HDMI 连接器,则可以为 NULL。

struct cec_adapter *adap

注册此通知器的 cec 适配器。

描述

如果设备 dev 和连接器 port_name 的通知器已存在,则增加 refcount 并返回该通知器。

如果不存在,则分配一个新的通知器结构并返回指向该新结构的指针。

如果无法分配内存,则返回 NULL。

void cec_notifier_cec_adap_unregister(struct cec_notifier *n, struct cec_adapter *adap)

减少 refcount,当 refcount 达到 0 时删除。

参数

struct cec_notifier *n

通知器。如果为 NULL,则此函数不执行任何操作。

struct cec_adapter *adap

注册此通知器的 cec 适配器。

void cec_notifier_set_phys_addr(struct cec_notifier *n, u16 pa)

设置一个新的物理地址。

参数

struct cec_notifier *n

CEC 通知器

u16 pa

CEC 物理地址

描述

设置一个新的 CEC 物理地址。如果 n == NULL,则不执行任何操作。

void cec_notifier_set_phys_addr_from_edid(struct cec_notifier *n, const struct edid *edid)

从 EDID 解析 PA。

参数

struct cec_notifier *n

CEC 通知器

const struct edid *edid

edid 结构体指针

描述

解析 EDID 以获取新的 CEC 物理地址并进行设置。如果 n == NULL,则不执行任何操作。

struct device *cec_notifier_parse_hdmi_phandle(struct device *dev)

从“hdmi-phandle”查找 HDMI 设备

参数

struct device *dev

具有“hdmi-phandle”设备树属性的设备

描述

返回由“hdmi-phandle”属性引用的设备指针。 请注意,返回的设备的引用计数不会递增。此设备指针仅用作通知器列表中的键值,但 CEC 驱动程序永远不会访问它。

void cec_notifier_phys_addr_invalidate(struct cec_notifier *n)

将物理地址设置为无效。

参数

struct cec_notifier *n

CEC 通知器

描述

这是一个简单的辅助函数,用于使物理地址无效。如果 n == NULL,则不执行任何操作。