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规范里的「通道分配表」一致。我曾经犯过一个错——把左后和右后搞反了,结果用户听环绕声时,声音从左边跑到右边去了。嗯,这种问题很难排查,因为单声道测试根本听不出来。
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标志,结果接收端不出声——因为它以为数据是压缩的,没做解码。
通道状态位的更新时机也很重要。我建议:
- 音频流开始时:一次性设置好所有通道状态位
- 音频流中间:不要修改,除非采样率或格式变了
- 格式切换时:先发一个「音频静音」包,再更新状态位,再恢复音频
为什么要先静音?因为接收端解析状态位需要时间。如果你边发数据边改状态位,接收端可能读到一半的状态,导致解码错误。嗯,这个我在项目里吃过亏——切换采样率时没做静音,结果听到一声「啪」的爆音。
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音频打包,说难不难,说简单也不简单。关键是把规范吃透,然后多动手抓波形验证。我每次写完驱动,都会用逻辑分析仪抓一下音频数据包,确认每个样本的位对齐、通道号、状态位都正确。这一步省不了,真的省不了。