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状态。

CEC 帧结构(数据块级) 块0: Header [7:4]目标 [3:0]源 块1: Opcode 操作码 块2..N 操作数 每个数据块内部位序(LSB first) [0:7]数据 | [8]EOM | ACK(独立) 逻辑1: 2.4ms高 + 0.6ms低 | 逻辑0: 0.6ms高 + 1.8ms低 地址映射: 0=TV, 1=Recording1, 2=Recording2, 3=Tuner1, 4=Playback1, 5=Audio, 6=Tuner2, 7=Tuner3, 8=Playback2, 9=Playback3, 10=Tuner4, 11=Playback4, 12=Reserved, 13=Reserved, 14=FreeUse, 15=Unregistered EOM=1: 帧结束 | ACK=0: 接收方应答 | 帧最大长度: 16数据块(1头+1操作码+14操作数)

解析器实现

以下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

验证方法

程序包含两组测试输入:

  1. Active Source帧:0x40 0x82 0x10 0x00,EOM标志为0,0,0,1。预期输出显示源地址为Playback1(0x4),目标为TV(0x0),操作码0x82对应"Active Source",操作数为物理地址0x1000。
  2. 轮询帧:单块0x40,EOM=1。预期输出显示无操作码,标记为轮询消息。

判定标准:程序输出与上述预期一致,且无内存错误(可用valgrind检查)。若要验证真实CEC总线数据,可将捕获的字节流按块填入data数组,并正确设置对应的eom和ack数组后重新编译运行。