4. HDMI 消费电子控制(CEC)帧解析
任务说明
本案例实现HDMI CEC(消费电子控制)协议帧的解析器。CEC协议定义在HDMI 1.4及后续版本中,通过单线双向串行总线传输控制命令(如开机、关机、音量调节等)。输入为从CEC总线捕获的原始字节流(含起始位后的数据块),输出为解析后的帧结构:包含帧起始符、目标地址、源地址、操作码及操作数,并校验EOM(结束位)与ACK(应答位)状态。
CEC帧格式
CEC帧由起始位(低电平4.5ms)后跟10个数据块组成,每个数据块包含:1个起始位、8个数据位(LSB先发)、1个EOM位、1个ACK位。本解析器处理已去除物理层时序的字节级数据,输入为每个数据块的9位(8数据位+EOM位),ACK由发送方在接收方应答后决定,解析器仅标记ACK状态。
解析器实现
以下C语言实现接收CEC数据块数组(每块低8位为数据,第9位为EOM标志),输出解析后的帧信息。代码包含地址到设备名的映射、操作码到命令名的映射,并验证帧格式合法性。
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#define MAX_BLOCKS 16
#define HEADER_BLOCK 0
#define OPCODE_BLOCK 1
// 逻辑地址 -> 设备类型
const char* addr_to_device(uint8_t addr) {
static const char* names[] = {
"TV", "Recording1", "Recording2", "Tuner1",
"Playback1", "Audio", "Tuner2", "Tuner3",
"Playback2", "Playback3", "Tuner4", "Playback4",
"Reserved", "Reserved", "FreeUse", "Unregistered"
};
return (addr < 16) ? names[addr] : "Invalid";
}
// 操作码 -> 命令名(常用子集)
const char* opcode_to_name(uint8_t op) {
switch(op) {
case 0x04: return "Power On";
case 0x36: return "Standby";
case 0x44: return "Volume Up";
case 0x45: return "Volume Down";
case 0x46: return "Mute";
case 0x47: return "Play";
case 0x48: return "Pause";
case 0x49: return "Stop";
case 0x0D: return "Image View On";
case 0x32: return "Active Source";
case 0x70: return "System Info";
case 0x8A: return "Get Menu Language";
default: return "Unknown";
}
}
typedef struct {
uint8_t blocks[MAX_BLOCKS]; // 每块数据字节
uint8_t eom_flags[MAX_BLOCKS]; // 每块EOM位
uint8_t ack_flags[MAX_BLOCKS]; // 每块ACK位(0=应答)
uint8_t block_count;
uint8_t initiator; // 源地址
uint8_t destination; // 目标地址
uint8_t opcode;
int has_opcode;
int valid;
char error_msg[64];
} cec_frame_t;
// 解析CEC帧
int cec_parse_frame(const uint8_t* data_blocks, const uint8_t* eom_bits,
const uint8_t* ack_bits, uint8_t count, cec_frame_t* out) {
if (!out || !data_blocks || !eom_bits || !ack_bits) return -1;
if (count < 1 || count > MAX_BLOCKS) {
snprintf(out->error_msg, sizeof(out->error_msg), "Invalid block count: %d", count);
out->valid = 0;
return -1;
}
memset(out, 0, sizeof(*out));
out->block_count = count;
memcpy(out->blocks, data_blocks, count);
memcpy(out->eom_flags, eom_bits, count);
memcpy(out->ack_flags, ack_bits, count);
// 解析头部块
uint8_t header = data_blocks[0];
out->initiator = header & 0x0F;
out->destination = (header >> 4) & 0x0F;
// 检查EOM是否在头部提前结束(单块帧如轮询)
if (eom_bits[0]) {
out->has_opcode = 0;
out->opcode = 0;
out->valid = 1;
return 0;
}
// 解析操作码
if (count < 2) {
snprintf(out->error_msg, sizeof(out->error_msg), "Missing opcode block");
out->valid = 0;
return -1;
}
out->opcode = data_blocks[1];
out->has_opcode = 1;
// 验证EOM标志:除最后一块外EOM必须为0
for (int i = 0; i < count - 1; i++) {
if (eom_bits[i]) {
snprintf(out->error_msg, sizeof(out->error_msg),
"Premature EOM at block %d", i);
out->valid = 0;
return -1;
}
}
// 最后一块EOM必须为1
if (!eom_bits[count - 1]) {
snprintf(out->error_msg, sizeof(out->error_msg),
"Missing EOM on final block %d", count - 1);
out->valid = 0;
return -1;
}
out->valid = 1;
return 0;
}
// 打印解析结果
void cec_print_frame(const cec_frame_t* f) {
if (!f || !f->valid) {
printf("Invalid frame: %s\n", f ? f->error_msg : "NULL");
return;
}
printf("=== CEC Frame ===\n");
printf("Initiator : 0x%X (%s)\n", f->initiator, addr_to_device(f->initiator));
printf("Dest : 0x%X (%s)\n", f->destination, addr_to_device(f->destination));
if (f->has_opcode) {
printf("Opcode : 0x%02X (%s)\n", f->opcode, opcode_to_name(f->opcode));
} else {
printf("Opcode : (none - polling message)\n");
}
printf("Data blocks: %d\n", f->block_count);
for (int i = 0; i < f->block_count; i++) {
printf(" [%d] 0x%02X EOM=%d ACK=%d\n",
i, f->blocks[i], f->eom_flags[i], f->ack_flags[i]);
}
printf("================\n");
}
// 示例:解析一个"从Playback1到TV的Active Source"帧
int main() {
// 帧内容: 0x40 (目标TV=0, 源Playback1=4) | 0x82 (Active Source) | 0x10 0x00 (物理地址1.0.0.0)
uint8_t data[] = {0x40, 0x82, 0x10, 0x00};
uint8_t eom[] = {0, 0, 0, 1}; // 最后一块EOM=1
uint8_t ack[] = {0, 0, 0, 0}; // 全部ACK=0(正常应答)
cec_frame_t frame;
int ret = cec_parse_frame(data, eom, ack, 4, &frame);
if (ret == 0) {
cec_print_frame(&frame);
} else {
printf("Parse failed: %s\n", frame.error_msg);
}
// 测试轮询帧(无操作码)
printf("\n--- Polling message test ---\n");
uint8_t poll_data[] = {0x40}; // TV->Playback1 轮询
uint8_t poll_eom[] = {1};
uint8_t poll_ack[] = {0};
cec_frame_t poll_frame;
ret = cec_parse_frame(poll_data, poll_eom, poll_ack, 1, &poll_frame);
if (ret == 0) {
cec_print_frame(&poll_frame);
}
return 0;
}
编译与运行
将代码保存为cec_parser.c,使用任意C编译器编译(如gcc):
gcc -o cec_parser cec_parser.c
./cec_parser
验证方法
程序包含两组测试输入:
- Active Source帧:
0x40 0x82 0x10 0x00,EOM标志为0,0,0,1。预期输出显示源地址为Playback1(0x4),目标为TV(0x0),操作码0x82对应"Active Source",操作数为物理地址0x1000。 - 轮询帧:单块
0x40,EOM=1。预期输出显示无操作码,标记为轮询消息。
判定标准:程序输出与上述预期一致,且无内存错误(可用valgrind检查)。若要验证真实CEC总线数据,可将捕获的字节流按块填入data数组,并正确设置对应的eom和ack数组后重新编译运行。