4、HDMI音频数据包打包:音频数据包格式、音频样本打包、通道状态位设置

好,咱们今天聊点实在的。HDMI音频传输,说白了就是把数字音频塞进HDMI的数据岛里,然后发出去。很多工程师一听到「打包」两个字就觉得头大,其实没那么玄乎。我刚开始做HDMI驱动的时候,也被那些文档绕得晕头转向。后来自己动手抓了几次总线数据,才真正搞明白。

嗯,咱们今天就把这个事拆开揉碎了讲。

4.1 音频数据包格式

HDMI传输音频,用的是I2S的变种。但注意,它不直接传I2S帧。它把音频样本打包成一个个「音频数据包」,塞进HDMI的TMDS通道里。

每个音频数据包长什么样?我直接给你看结构:

音频数据包结构(4个样本,每个样本32位):
+--------+--------+--------+--------+
| 样本1  | 样本2  | 样本3  | 样本4  |
| (32位) | (32位) | (32位) | (32位) |
+--------+--------+--------+--------+
| 包头   | 子包1  | 子包2  | 子包3  |
| (8位)  | (32位) | (32位) | (32位) |
+--------+--------+--------+--------+

这里有个关键点:每个音频数据包包含4个音频样本。每个样本是32位宽。但实际音频位深可能是16位、20位、24位。那多出来的位怎么办?

我告诉你,多余的位填0。但填0的位置有讲究。举个例子,如果是24位音频,样本放在高24位,低8位填0。如果是16位,放在高16位,低16位填0。这个对齐方式,我在项目里踩过坑——有一次客户说声音有爆音,查了半天,发现是样本对齐方式搞反了。

音频数据包在HDMI帧里怎么放?看这个时序:

HDMI帧区域 用途 说明
数据岛(Data Island) 传输音频数据包 每个数据岛周期传输1个音频包
视频消隐区 音频包插入时机 只在消隐区传输,不影响视频
包间隔 包与包之间留空 至少32个TMDS时钟间隔

我个人习惯,在驱动里用一个环形缓冲区来管理音频包。每次从DMA拿数据,按32位对齐打包,然后塞进发送队列。这样不容易丢包。

4.2 音频样本打包

样本打包,说白了就是把PCM数据切成32位的块。但这里有个坑——通道映射。

HDMI支持最多8个通道(7.1声道)。每个音频数据包里的4个样本,可以是同一个通道的,也可以是不同通道的。怎么区分?靠包头里的「通道号」字段。

我举个例子,2通道立体声:

音频数据包1:
  样本1: 左声道 (通道0)
  样本2: 右声道 (通道1)
  样本3: 左声道 (通道0)
  样本4: 右声道 (通道1)

音频数据包2:
  样本1: 左声道 (通道0)
  样本2: 右声道 (通道1)
  样本3: 左声道 (通道0)
  样本4: 右声道 (通道1)
  ...

你看,每个包交替放左右声道。这样接收端拿到包后,按通道号重新组装,就能还原出完整的音频流。

那如果是5.1声道呢?我建议这样打包:

  • 包1:左前、右前、中置、低音
  • 包2:左后、右后、左前、右前
  • 包3:中置、低音、左后、右后
  • ...循环

注意,每个包里的样本顺序,必须和HDMI规范里的「通道分配表」一致。我曾经犯过一个错——把左后和右后搞反了,结果用户听环绕声时,声音从左边跑到右边去了。嗯,这种问题很难排查,因为单声道测试根本听不出来。

我的小技巧: 调试时先用1kHz正弦波单声道测试。如果每个通道都能正确输出,再切到多声道。这样能快速定位通道映射问题。

4.3 通道状态位设置

通道状态位,这是HDMI音频里最容易忽略但又最关键的部分。它告诉接收端:你收到的音频是什么格式、多少位深、多少采样率。

通道状态位是一个192位的块,分成24个字节。每个字节有特定含义。我挑几个最重要的说:

字节 位 含义 常见值
0 0-1 音频格式 00=PCM, 01=非PCM
0 2-3 预加重 00=无
1 0-3 通道数 0001=2通道, 0011=6通道
2 0-3 样本位深 0000=16位, 0001=20位, 0010=24位
3 0-3 采样率 0011=48kHz, 0100=96kHz

这里有个容易踩的坑:字节0的位0-1,如果设成01(非PCM),接收端会认为你传的是压缩音频(比如Dolby Digital)。这时候它不会做PCM解码,而是直接透传。我遇到过有人传PCM数据但设成了非PCM标志,结果接收端不出声——因为它以为数据是压缩的,没做解码。

通道状态位的更新时机也很重要。我建议:

  • 音频流开始时:一次性设置好所有通道状态位
  • 音频流中间:不要修改,除非采样率或格式变了
  • 格式切换时:先发一个「音频静音」包,再更新状态位,再恢复音频

为什么要先静音?因为接收端解析状态位需要时间。如果你边发数据边改状态位,接收端可能读到一半的状态,导致解码错误。嗯,这个我在项目里吃过亏——切换采样率时没做静音,结果听到一声「啪」的爆音。

注意: 通道状态位是逐帧更新的。每个音频帧(192个音频样本)只更新1位。所以整个192位的状态块,需要192帧才能传完。接收端在收到完整块之前,会使用上一次的有效值。所以不要频繁修改状态位,否则接收端可能永远收不到完整块。

4.4 实战中的坑与建议

好,理论讲完了,我分享几个实战经验。

第一个坑:样本对齐。 我遇到过HDMI接收端对32位样本的位对齐要求很严格。有些芯片要求样本必须左对齐,有些要求右对齐。解决办法很简单——看芯片手册的「音频输入格式」章节。如果手册没写,就两种都试一下,抓I2S波形看哪个能正确解码。

第二个坑:包间隔。 HDMI规范要求音频包之间至少间隔32个TMDS时钟。但有些便宜的接收端芯片,要求更长的间隔。我建议在驱动里把间隔设成64个时钟,兼容性更好。反正多等几十个时钟,音频延迟增加不到1微秒,听不出来。

第三个坑:通道状态位的校验和。 通道状态块的第23个字节是校验和。很多工程师会忽略它,但接收端会校验。如果校验和不对,接收端可能丢弃整个状态块。我建议用简单的异或校验:把前23个字节异或一遍,结果放在第24字节。

// 计算通道状态校验和
uint8_t calc_channel_status_checksum(uint8_t *status, int len)
{
    uint8_t checksum = 0;
    for (int i = 0; i < len - 1; i++) {
        checksum ^= status[i];
    }
    return checksum;
}

嗯,这个函数我用了好多年,从来没出过问题。

最后说一句:HDMI音频打包,说难不难,说简单也不简单。关键是把规范吃透,然后多动手抓波形验证。我每次写完驱动,都会用逻辑分析仪抓一下音频数据包,确认每个样本的位对齐、通道号、状态位都正确。这一步省不了,真的省不了。