简介:这份PPT课件面向音视频接口开发、嵌入式系统及智能家居控制领域的学习者与工程人员,系统梳理HDMI-CEC技术标准与设备交互原理。包内仅1个pptx文件,约428KB,内容精炼,适合作为培训提纲或自学索引。课件从HDMI的TMDS差分传输及DDC、CEC、HPD通道讲起,说明CEC定时格式、开始位与数据位定义,以及Header、Opcode、Operand块和EOM、ACK控制位;同时梳理逻辑地址分配、Normal与Standby功能,并涉及一键播放、系统待机、遥控透传、音频控制等命令场景。已有1063人学习,可帮助读者快速建立从物理层到应用层的CEC知识框架,并指导实际命令应用。
1. 一根线控制所有设备:HDMI-CEC 真正解决的尴尬
买回一台电视和一台播放器,接上 HDMI 线,本指望一个遥控器全搞定,结果按电视关机键,播放器照旧亮着。线是通的,接口是对的,问题常常出在这根线里最不起眼的一根:承载 HDMI-CEC 的那条单线。这条通道的位速率只有约 400 bit/s,比二十年前的串口还慢两个数量级;它之所以能在这么低的速率下完成“一按全关”,靠的是用 4 bit 逻辑地址建立起来的身份体系,和一套所有设备都能听懂的命令号。搞懂这套地址怎么被分配、10 bit 数据块怎么拼、命令到了应用层怎么被解释,才能解释为什么有些设备组合一插就通,有些组合无论怎么调都显得靠运气。
2. HDMI 的物理底座:CEC 为什么能利用 TMDS 的通道
2.1 四组差分线与三条控制线的分工
要理解 CEC,得先看它依附于什么。HDMI 的线束里一共有四组差分对:三组 TMDS 数据通道加一组 TMDS 时钟通道。每个时钟周期内,每条数据通道传 10 bit,也就是常说的 8b/10b 编码——把 8 bit 的有效载荷映射成 10 bit 的线路码,用冗余位换取直流平衡和足够的跳变密度,接收端才能从数据流里恢复时钟。这套编码的代价是 20% 的带宽开销,换来的是不需要独立同步信号也能稳定采样,这笔账在长线缆和高分辨率下是划算的。
视频信号被拆成 R、G、B、H、V 五路。三个 TMDS 数据通道分别承载 R、G、B 三原色,行场同步信号 H、V 被编码进 B 通道一起传输,而 R、G 通道在消隐期的空余位置被拿去塞音频采样——这就是为什么音频不需要额外走一根线。理解这个映射关系很关键:它意味着视频、音频、控制信息在物理上是彻底分离的三套体系,TMDS 只管把像素和声音搬过去,一个字节的控制信息都不负责。
于是才有了那几条控制线各自分工。DDC(Display Data Channel,显示数据通道)用于读取接收端的配置信息;CEC(Consumer Electronics Control,消费电子控制通道)负责设备之间的交互与控制;HPD(Hot Plug Detect,热插拔检测)决定源端什么时候可以去读 DDC。三者不是并列关系,而是一条有先后依赖的链条:HPD 拉高告诉源端“对面插上了”,源端才通过 DDC 去读 EDID,读到 EDID 才知道对面是什么设备、物理上挂在树里的哪个位置,最后才轮到 CEC 拿着这个位置去申请自己的身份。这个链条里任何一环出问题,表现出来的都是 CEC 层的异常症状,排查时容易被引到错误的方向。
2.2 DDC 与 E-EDID:物理地址是“读”出来的
DDC 底层就是一路 I²C,两根线,时钟和数据。接收端的 EDID 数据一般挂在 I²C 的 0x50 这个地址上,第一个 128 字节块是基础 EDID,描述厂商、支持的分辨率时序等;后面跟着的扩展块里,CEA-861 扩展中的厂商自定义数据块保存着 HDMI 特有的信息,其中就包括 4 位十六进制的物理地址(Physical Address)。这个地址不是设备自己编的,是拓扑位置决定的:直接接在电视上的设备拿到的是 1.0.0.0、2.0.0.0 这种一级地址,串在它后面的设备拿到 1.1.0.0 这种二级地址,电视自己固定是 0.0.0.0,F.F.F.F 则表示“尚未分配”。
物理地址的价值在于它描述的是“插在哪个口”,而不是“我是谁”。两个完全一样的播放器插在电视的两个 HDMI 口上,物理地址天然不同,这就给上层提供了区分同一型号多台设备的唯一依据。很多 CEC 的“抢屏”“串控”问题,根子就在这里:物理地址错了,后面所有的路由判断都会跟着错。
这里有一个容易被忽略的时序细节:HPD 不只是“插上了”的信号,它还是源端读取 EDID 的许可。HPD 拉低期间,规范上源端不应该去读 DDC,因为此时接收端可能正在重新初始化 EDID 内容。某些电视在切换输入源时会短暂拉低 HPD,如果源端固件不遵守这个约束、在 HPD 低电平期间硬读,读回来的可能是一段残缺或过期的 EDID,物理地址解析成 F.F.F.F,设备就“消失”了。这类问题的现象是偶发性的,重启一次就好,很难复现,但根因非常确定。
2.3 4 bit 逻辑地址:一张有配额的分配表
物理地址解决了“插在哪”,但 CEC 总线上的消息头只有 4 bit 空间放地址,装不下 16 bit 的物理地址。所以 CEC 又引入了一层逻辑地址:4 bit,取值 0 到 15,每个值对应一类设备功能,而不是某个具体产品。这套分配是规范里固定约束的,数量有限,某些值还被特定类型独占:
| 逻辑地址 | 对应设备类型 | 说明 |
|---|---|---|
| 0 | TV | 固定给电视,也是总线上唯一的总控角色 |
| 1 / 2 / 9 | 录制设备 | 三类录像类功能,可同时存在 |
| 3 / 6 / 7 / 10 | 调谐器 | 四类 Tuner 功能 |
| 4 / 8 / 11 | 播放设备 | 三类播放类功能 |
| 5 | 音响系统 | 音频回传、音量控制的落点 |
| 12 / 13 | 保留 | 规范未分配 |
| 14 | 特定用途 | 如未注册的播放设备 |
| 15 | 未注册 / 广播 | 目的地址为此值表示广播 |
分配流程是一个典型的“先探测、再声明”。设备拿到非 F.F.F.F 的物理地址后,先向自己期望的逻辑地址发一条只有消息头的 Polling Message,如果总线上已有设备占用该地址,对方会在这个消息的应答位拉低电平作为回应,发起方就知道被占了,换下一个候选;如果没有任何回应,说明地址空闲,设备就占用它,然后向全网广播 Report Physical Address,把“物理地址 ↔ 逻辑地址”的绑定关系告诉所有设备。这个广播是整条总线上地址映射表的唯一来源,任何一个设备都可以借此建立一张完整的拓扑表。
一台物理设备如果包含多种功能,就必须为每种功能各占一个逻辑地址——一台带播放功能又内置功放的设备,可能同时占着 4 和 5。这解释了为什么有些设备的 CEC 行为格外复杂:它同时在扮演多个角色,收到的广播要按不同身份分别处理。逻辑地址只有 16 个而设备类型有十来种,配额是紧张的,这也是 CEC 网络里设备一多就容易出问题的结构性原因。
3. 位定时与数据块:从开始位到 160 bit 的最大帧
3.1 开始位与数据位的时间刻度
CEC 只在一根线上传数据,这根线是双向的、开漏结构的,所有设备共享。没有独立的时钟线,全部靠时间长度来区分信息,所以定时精度是这套协议的基础。一个信号单元只包含两部分:一个开始位,和一串数据位。开始位的判定同时约束两个量:低电平持续时间 a 和整个位的总时长 b。规范给出的标称值是 a 约 3.7 ms、b 约 4.5 ms,容差在 ±0.2 ms 量级。为什么要同时卡两个量而不是只看低电平宽度?因为单看一个量无法区分“一个长低电平的开始位”和“一个低电平占比较高的数据位”,双约束把误判概率压到最低。
数据位同样是时间编码,但总时长被统一在 2.4 ms 左右,靠低电平占的比例来区分逻辑值:逻辑 0 的低电平约 1.5 ms、高电平约 0.9 ms;逻辑 1 的低电平约 0.6 ms、高电平约 1.8 ms。也就是说,低电平“宽”代表 0,低电平“窄”代表 1。这个约定的直接后果是:多个设备同时发送时,低电平更宽的信号会在总线上占据优势——这正是后面仲裁机制的基础。
还有一个细节很容易被忽略:当前数据位的最后一位如果后面还有数据,它从高到低的跳变就直接充当下一数据位的开始边沿,最后一位传完之后,CEC 线保持高电平。也就是说接收端不需要在每一位之间重新找同步头,边沿本身就是同步点。这让协议在硬件实现上非常省——一个定时器加一个边沿捕获中断就能解码,不需要 PLL,不需要过采样。
3.2 逻辑 0 和 1 的判定窗口
实际固件里,判定通常不是“算精确值”,而是“落到窗口里”。下面这段是把边沿捕获到的低电平和总时长映射成位值的典型写法:
# 时间常量单位 ms,取规范标称值并向两侧留出采样抖动余量 START_LOW = (3.5, 3.9) # 开始位低电平窗口 START_TOTAL = (4.3, 4.7) # 开始位总时长窗口 BIT_TOTAL = (2.05, 2.75) # 任意数据位的总时长窗口 LOGIC0_LOW = (1.3, 1.7) # 逻辑 0 的低电平窗口 LOGIC1_LOW = (0.5, 0.7) # 逻辑 1 的低电平窗口 def classify(low_ms, total_ms): """根据一次跳变测得的低电平和总时长,判定位类型与位值。""" if START_LOW[0] <= low_ms <= START_LOW[1] and \ START_TOTAL[0] <= total_ms <= START_TOTAL[1]: return "start" # 总线空闲后的第一个下降沿,必须是开始位 if not (BIT_TOTAL[0] <= total_ms <= BIT_TOTAL[1]): return "invalid" # 总时长偏离太多,多半是毛刺或中断延迟 if LOGIC0_LOW[0] <= low_ms <= LOGIC0_LOW[1]: return 0 if LOGIC1_LOW[0] <= low_ms <= LOGIC1_LOW[1]: return 1 return "invalid"参数含义很直白:low_ms 是从下降沿到上升沿的实测宽度,total_ms 是相邻两个下降沿之间的间隔。之所以要有 invalid 这个返回值,是因为真实总线上一定会有毛刺、有设备热插拔引起的电平抖动、有中断被更高优先级任务推迟导致的采样偏移。把非法值显式返回,比默默当成 0 或 1 要好得多——前者会让上层丢弃整帧并等待总线空闲,后者会解出一条看似合法实则错乱的消息,故障现象会变得很难解释。
注意这里没有做“平均”“滤波”之类的处理。CEC 的位时长在毫秒量级,对现代 MCU 来说时间分辨率绰绰有余,窗口判定已经足够;真正需要小心的是中断延迟,而不是采样精度。这也是为什么很多方案里 CEC 的收发要用独立的定时器通道,而不是挂在通用 GPIO 中断上——后者一旦被别的中断遮挡几毫秒,整帧就废了。
3.3 Header Block 的 4+4 布局与 EOM/ACK
每一条 CEC 消息由若干个 10 bit 的块组成,块与块的结构完全一致:8 个信息位,加 EOM 和 ACK 两个控制位,最低位先发。第一块是 Header Block,它的 8 个信息位不是数据,而是地址:1 到 4 bit 是发起方(Initiator)的逻辑地址,5 到 8 bit 是接收方(Destination)的逻辑地址。目的地址填 15 就是广播,全总线所有设备都要收。Header 之后是 Opcode Block(操作码),再之后是若干 Operand Block(操作数)。
两个控制位的分工非常清晰。EOM(End Of Message)声明当前块是不是最后一块:0 表示后面还有块,1 表示消息到此结束。ACK(Acknowledge)由接收方驱动:发起方在发送时总是把它置 1,接收方如果正确收到并愿意接收,就在这一位的时间内把总线拉低,发起方读回 0 就知道对方收到了;读回 1 则表示无人应答。这个设计与 I²C 的 ACK 是同一个思路——把应答做成总线电平层面的握手,而不是靠上层再发一条确认消息,省掉了一整个往返。
帧长有硬上限:Header + Opcode + Operand 加起来最多 16 个块,也就是 160 bit。这个数字不是随便定的,它对应着最坏情况下总线的占用时间——16 块乘以 2.4 ms 再加开始位,接近 40 ms。CEC 总线上同时可能有多台设备要说话,帧太长会让响应变得不可接受,所以规范把上限卡死在这里,超长的信息必须由厂商拆成多条消息自己拼。做固件的人如果按“想发多少发多少”来写,做到后面一定会撞上这个天花板。
3.4 把一条命令拼成块序列
从命令到线上波形,中间这一步是把结构化参数摊平成块。下面这段是拼帧的典型逻辑:
MAX_BLOCKS = 16 def build_frame(initiator, destination, opcode, operands=()): """把一条 CEC 命令拼成线路上依次发送的 10 bit 块序列。""" payload = [opcode, *operands] total = 1 + len(payload) if total > MAX_BLOCKS: raise ValueError(f"帧长 {total} 块,超过 {MAX_BLOCKS} 块上限") raw = [((initiator & 0x0F) << 4) | (destination & 0x0F)] + payload blocks = [] for i, byte in enumerate(raw): eom = 1 if i == total - 1 else 0 # 只把最后一块的 EOM 置 1 ack = 1 # 发送端先置 1,接收端拉低才算数 blocks.append((byte << 2) | (eom << 1) | ack) return blocks四个参数分别是源逻辑地址、目的逻辑地址、操作码字节、操作数序列。返回值是已经带好控制位的 10 bit 整数列表,发送时按最低位优先逐位输出即可。这里有两个地方值得停一下:一是 header 的构造方式,把源地址放高 4 位、目的地址放低 4 位,是因为在线路上低位先发,目的地址会先于源地址被对端解析——接收方可以在收完前 4 bit 时就开始判断“这条消息是不是给我的”,从而决定要不要参与后面的应答,这是一种降低延迟的小心思。二是 ack 位的初值恒为 1,因为开漏总线上“不驱动”天然就是高,发送端不需要主动拉高,只要不拉低就行。
接收侧的解析是逆过程,但要额外处理一件事:ACK 位读回来的值决定了这条消息的后续处理。如果一条定向消息的 ACK 读回 1,说明目的地址那台设备不在线或者没空,发起方通常要重试若干次再放弃,而不是当成成功。很多“命令发出去了但没反应”的问题,如果日志里显示 ACK 一直是 1,那就是地址或设备状态的问题,跟命令内容无关。
3.5 仲裁:线与总线上的抢答机制
单线双向的总线上,两台设备同时开口是必然会发生的。CEC 用的是一个很朴素的机制:总线是线与结构,任何一方拉低,整条线就是低。两个设备同时发,一个发逻辑 0(低电平 1.5 ms),一个发逻辑 1(低电平 0.6 ms),那么窄的那一方在 0.6 ms 后想把线放开,却发现线仍然是低的——因为另一方还在拉。此时它就知道自己输了,立刻停止发送,退出竞争,把总线让出去,等这一帧结束后再重试。
这个机制的妙处在于它不需要任何额外的仲裁线或令牌,冲突检测就是“读回自己写的值”这么简单。而且它天然带优先级:地址数值小、低电平位多的设备更容易赢。发起方地址是 header 的高 4 位,逻辑地址 0 的电视永远是最后赢家——这符合电视在 CEC 网络里的总控地位,不是靠规范规定“电视优先”,而是靠电气特性自然形成的。
代价是重试。仲裁失败的设备必须完整地等当前帧结束(总线空闲若干毫秒)才能重新发起,如果总线上设备多、流量大,一个低优先级设备的命令可能被推迟相当久。这是设计上的取舍:用简单的电气仲裁换取实现成本极低,代价是实时性没有保证。理解了这一点,就不会对“偶尔慢半拍”感到意外。
4. 应用层命令链路:One Touch Play、Routing 与 Standby 怎么跑起来
4.1 身份先行:Report Physical Address 是前提
应用层的一切交互,都建立在“我知道你是谁、你插在哪个口”这个前提上。所以上电后的第一件事不是发控制命令,而是完成身份注册:拿到物理地址、抢占逻辑地址、广播 Report Physical Address。这条广播携带两个信息——设备的物理地址和它的设备类型,收到的一方据此更新自己的地址映射表。同时配套的还有 Give Physical Address,用来在设备错过广播时主动索要。
System Information 这一族命令就是围绕这个目标存在的:Get CEC Version / CEC Version 协商协议版本,避免新旧设备用不同版本的命令语义互相误解;Get Menu Language / Set Menu Language 同步菜单语言;Polling Message 单独用来探测某个逻辑地址是否已被占用。这七条命令看起来琐碎,实际上是整个 CEC 网络的“入网握手”,任何一条缺失或实现错误,后面的功能都会表现得不稳定。
注意:版本协商不是可选项。低版本设备收到高版本才有的操作码时,按规范应该忽略而不是报错,但现实中不少固件会直接把整帧丢弃,连后面的合法命令一起丢掉,表现出来的就是“某台设备一接上,其他功能也开始抽风”。
4.2 One Touch Play:三条命令的配合
One Touch Play 是 CEC 认证里强制要求实现的功能,说白了就是“我这边开始播了,你把电视切过来”。它由三条命令配合完成:设备发送 Active Source 声明自己成为当前信源,同时携带自己的物理地址;再发 Text View On 或 Image View On 请求电视唤醒并显示。Text View On 和 Image View On 的区别在于前者用于文本类显示场景,后者用于图像类场景,很多实现里两者行为一致,但规范上是分开的。
这三条命令的时序有讲究。Active Source 通常以广播形式发出,因为它要更新的是全网的路由状态;而 Text/Image View On 是定向发给电视(逻辑地址 0)的。如果顺序反了,先请求显示再声明信源,电视可能会先切过去又发现没有有效信源,导致画面闪一下黑屏——这是联调里非常典型的现象,用抓包工具一看命令顺序就能定位。
4.3 Routing Control:谁在显示,谁在调整输入源
Routing 这一族管的是“电视该显示哪一路输入”。核心命令包括 Active Source、Inactive Source、Request Active Source、Set Stream Path、Routing Change 和 Routing Information。Active Source 是主动宣告“我是当前源”;Inactive Source 是宣告“我不再是当前源”,让电视回退;Request Active Source 是电视主动问“现在谁是源”,用于设备上电或输入切换后重建状态;Set Stream Path 是电视指定“我要看某个物理地址上的设备”,被点名的设备收到后要变成 Active Source;Routing Change 和 Routing Information 则用于多层拓扑下,当中间某个切换器改变内部路由时,逐级通报变化。
这一族是 CEC 里最容易出问题的部分,因为涉及多台设备对“当前源”这个共享状态的并发修改。两台设备同时发 Active Source,电视只能认一个,另一台如果不处理被拒的情况,就会陷入反复重发的循环。合理的实现是在发 Active Source 之后等待一段时间,看有没有被后来的消息覆盖,而不是无脑定时重发。
4.4 Standby:一条广播能关掉所有设备
System Standby 只靠一条 Standby 命令实现,但它有两种发送方式,语义完全不同。目的地址填 15 是广播,总线上所有设备收到都要进入待机;填具体逻辑地址是定向,只有那台设备响应。电视遥控器按下电源键时,电视通常发的是广播 Standby,于是播放器、功放一起关掉——这就是“一个键关全屋”的实现方式,简单到只有一条命令。
问题也出在这个简单上。广播 Standby 没有任何确认机制,发出去就完了,谁收到了谁没收到发起方并不知道。如果某台设备因为仲裁失败没收到,或者收到了但因为正在录制而选择忽略(规范允许录制设备在录制期间不响应 Standby),就会出现“关了三个、剩一个还亮着”的现象。更麻烦的是反向场景:设备从待机唤醒时发的唤醒命令被电视误判,导致刚关掉又自己打开。这类问题不是协议缺陷,而是广播语义在高可靠性场景下的固有代价。
与待机紧密相关的是 Give Device Power Status / Report Power Status 这一对命令:前者询问对方当前的电源状态,后者回复 Power On 或 Standby。规范里提到,这对命令的响应通常在 Standby 相关功能里实现——也就是说,设备即使在待机状态下,CEC 收发模块也必须保持供电和监听,否则无法回应状态查询,也就无法被唤醒。这是硬件设计上的一个硬要求:CEC 那条线的接收电路不能跟着主芯片一起断电。
4.5 其余功能族:厂商自定义与界面控制
Vendor Specific 这一族给了厂商自定义命令的空间,包括 Device Vendor ID、Give Device Vendor ID、Vendor Command、Vendor Command With ID、Vendor Remote Button Down 和 Vendor Remote Button Up。前两条用来交换厂商标识,后四条是留给厂商自己定义语义的通道。这是 CEC 最灵活也最混乱的部分:同一组厂商命令在不同品牌之间完全不通用,跨品牌联调时这部分基本可以当作不存在。
Device OSD Name Transfer 由 Give OSD Name 和 Set OSD Name 两条命令构成,作用是让设备在电视的输入源列表里显示一个可读名字,而不是干巴巴的“HDMI 2”。Device Menu Control 由 Menu Request 和 Menu Status 组成,前者带 Active、Deactive、Query 三个参数,后者只能回 Active 或 Deactive,用于让电视遥控器操作外接设备的菜单。Remote Control Pass Through 靠 User Control Pressed 和 User Control Released 携带不同的按键码,把电视遥控器的按键透传给外接设备,实现“一个遥控器控两台”。这几族功能的共同点是都需要双向交互,比单向的 Standby 复杂得多,也更容易因为一方实现不完整而表现异常。
5. CEC 联调常见问题:从“按了没反应”到“关了又自动开”的排查路径
5.1 现象一:设备在输入源列表里时有时无
现象是同一台设备,有时电视能识别到名字和状态,重启一次就找不到了,换根线又好了。原因通常落在 HPD 和 EDID 读取的时序上:电视切换输入源或从待机唤醒时会短暂拉低 HPD,如果设备固件在 HPD 低电平期间去读 DDC,读回来的 EDID 可能不完整,解析出的物理地址变成 F.F.F.F,后续的逻辑地址申请和 Report Physical Address 全部失效,设备在网络里就“隐身”了。
解决办法是把读 EDID 的动作严格挂在 HPD 上升沿之后,并加一段延迟再读,读到物理地址为 F.F.F.F 时不要直接采用,而是等下一次 HPD 变化重试。同时检查设备是否在物理地址变化后重新走了一遍逻辑地址申请流程——物理地址变了但逻辑地址没更新,等于用了别人的身份在说话,后面所有路由判断都会错。
5.2 现象二:关机后过几秒自己又开机了
现象是按电视电源键,所有设备都关了,几秒后播放器自己亮起来。原因多半是关机流程里的消息竞争:电视发广播 Standby 的同时,播放器可能正在发 Active Source 或 Image View On,电视收到后按“有新信源”处理,又把自己唤醒,然后反过来把播放器也唤醒。两台设备谁都没错,错在两条方向相反的消息撞在了一起。
解决需要在应用层加状态机约束:设备收到 Standby 后进入一个短暂的抑制窗口,窗口内不再发起任何唤醒类命令;电视侧在发出 Standby 后的一段时间内忽略新收到的 Active Source。这个窗口的时长没有规范规定,一般取几百毫秒到一秒,具体值要按实际设备的响应时间调。这个坑很典型,光看单条命令的收发日志看不出问题,必须把两台设备的时间轴对齐了看。
5.3 现象三:厂商自定义命令发出去对方完全不理
现象是自家两条产品线之间用 Vendor Command 通信,一条通一条不通。原因通常有三个层次:一是对方根本没有实现这一族命令,收到未知操作码直接丢弃;二是 Device Vendor ID 没有先交换,对方不知道这条自定义命令来自哪个厂商,按规范拒绝处理;三是操作数长度超过了对方缓冲区,被截断后校验失败。
排查顺序建议从收到没收到开始:先确认 ACK 位是否被拉低,ACK 为 1 说明对方物理层就没接;ACK 为 0 但没反应,再查对方是否实现了该操作码;都正常的话,抓包看操作数长度是否和对方文档一致。跨品牌场景下这条路径基本走不通,因为厂商命令的语义本来就不公开,这时候应该退回用标准命令实现功能,而不是硬啃厂商通道。
5.4 现象四:长消息偶发失败,短消息一切正常
现象是短命令稳定,一旦操作数多几个就开始丢帧或者对方解析出错。原因是 16 块的上限和总线仲裁的交互:帧越长,占用总线的时间越久,被高优先级设备打断的概率越大,仲裁失败后整帧作废重发,如果重发逻辑没有正确复位状态机,就会出现“发了一半”的残帧,接收方按 EOM 判断消息没结束却等不到后续,超时后丢弃。
解决要两头做:发送侧在仲裁失败后必须完整复位发送状态机,等总线空闲足够长时间再重发,且重发次数要有上限;接收侧要设帧间超时,超过若干毫秒没收到下一块就丢弃当前帧并复位。另外尽量把长消息拆成多条短消息,用操作数里的序号自己拼,能显著降低失败率。
6. 设计取舍:CEC 为什么宁可慢、宁可“靠运气”,也不做带内控制
把 CEC 和同期的其他控制方案放在一起看,会发现它的每个设计选择都在往“便宜”这一端压:单线双向、开漏结构、时间编码无时钟线、4 bit 逻辑地址、最多 16 块的帧长、广播式 Standby 无确认。这些选择单独看都有更优解,但组合起来指向同一个目标:让一颗几毛钱的 MCU 加一个定时器就能实现,且不需要在主 SoC 里占用任何带宽。
| 设计选择 | 换来的好处 | 付出的代价 |
|---|---|---|
| 时间编码、无独立时钟线 | 硬件成本极低,边沿捕获即可解码 | 对中断延迟敏感,位定时容差要求严 |
| 单线开漏、线与仲裁 | 无需令牌,冲突自然消解 | 低优先级设备实时性无保证,需重试 |
| 4 bit 逻辑地址 | 消息头极短,一帧能塞更多操作数 | 地址配额固定且稀缺,多设备易冲突 |
| 最多 16 块帧长 | 单帧占用总线时间可控 | 长信息必须上层拆分,增加复杂度 |
| 广播 Standby 无确认 | 一条命令关全屋,交互最简 | 无法保证送达,也无从知道谁没关 |
如果换成带内控制,也就是把控制命令塞进 TMDS 的消隐期随视频一起传,理论上能拿到高得多的带宽和天然的时序同步,但代价是控制通路必须依赖视频链路处于活动状态——设备待机时视频链路是断的,恰恰是最需要控制的时候没有通路。CEC 单独走一根线,主链路断电它还能工作,这是它能承担“唤醒”职责的根本原因。所以那 400 bit/s 不是妥协的产物,而是刻意留出的最小可用通道:慢到可以用最便宜的电路实现,又足够传完一条几字节的命令。
真要说这套设计最大的问题,不在电气层,而在应用层的语义没有强制约束。规范定义了命令的格式和大致语义,但对“收到 Active Source 之后多久必须响应”“广播 Standby 没收到要不要重试”这类行为没有硬性规定,各家实现自由发挥,跨品牌组合起来就变成了“靠运气”。这不是协议的失败,而是它选择了“可实现性优先于互操作性”的必然结果。
我早年做一个小项目时踩过这个坑:当时觉得 ACK 位只是个形式,发送端置 1 之后就没再读回,结果对方设备根本没在线,命令全发进了空气里,日志上一切正常。后来养成一个习惯,每次调试 CEC 相关的东西,第一件事就是把收发两侧的原始块序列打出来,逐块核对源地址、目的地址、EOM 和 ACK,而不是只看最终功能有没有生效。这套笨办法救过我很多次,也让我真正理解了那句话——协议文档写的是应该怎么工作,抓包看到的是实际怎么工作,两者之间的差距就是所有“玄学”的来源。希望帮到你。
本文还有配套的精品资源,点击获取