1. 从一次花屏事故说起:为什么要折腾单帧多slice
去年帮一个做安防设备的朋友调一块Hi3559A的板子,场景是8路1080P视频同时解码上墙。硬件规格书上写得明明白白,H.265解码能力绰绰有余,但实际跑起来就是不对劲——单路播放没问题,一旦多路并发,画面就开始出现横向撕裂,严重的时候直接花屏,音频倒是正常走。查了一圈内存带宽、DDR频率、VPSS绑定关系,都没找到根因。
后来抓码流分析才发现,问题出在码流本身的slice结构上。那批码流是编码端为了降低延迟,把一帧切成了多个slice并行编码的。Hi3559A的VDEC默认按整帧粒度调度,遇到多slice帧时,解码器内部缓冲和输出时序对不齐,多路并发下就崩了。解决方向很明确:让VDEC按slice粒度去解,把单帧多slice的解码路径打通。这就是今天要聊的核心——Hi3559A单帧多slice解码的方法配置。
这篇文章适合正在用Hi3559A做视频解码、遇到多slice码流花屏或卡顿的嵌入式开发者。我会把整个配置思路、参数含义、踩过的坑都摊开讲,代码片段可以直接参考。如果你手上正好有这块芯片,或者在做类似的多路解码项目,这篇内容应该能帮你省下不少调试时间。
2. 先搞懂Hi3559A的VDEC到底怎么工作
2.1 VDEC模块的层级结构
Hi3559A的VDEC不是一个大黑盒,它内部是有明确分层的。从上层到下层大致是:通道(Channel)→ 解码器实例 → 帧缓冲管理 → slice解析单元。每个通道可以独立配置码流类型、分辨率、参考帧数量等参数。关键点在于,slice解析单元是否启用、以什么粒度工作,直接决定了多slice码流能不能被正确还原。
默认配置下,VDEC走的是"整帧提交、整帧输出"的模式。编码端如果按整帧编码,这套逻辑没问题。但一旦编码端做了slice切分,VDEC在收到第一个slice时就开始等后续slice,等待过程中如果缓冲区管理没配好,就会出现帧序错乱。表现出来就是花屏或者画面卡住不动。
2.2 多slice码流的两种常见形态
实际项目里遇到的多slice码流,基本逃不出这两种:
- 条带式slice:一帧画面按水平方向切成若干条,每条独立编码。这种最常见,编码端做低延迟推流时几乎必用。
- 矩形slice:一帧切成多个矩形区域,每个区域独立编码。这种在特定行业应用里会出现,比如某些需要区域独立刷新的场景。
Hi3559A对这两种形态的支持程度不一样。条带式slice的支持相对成熟,矩形slice需要额外注意宏块对齐问题。配置的时候如果没区分清楚,很容易出现"能解但画面错位"的情况。
2.3 为什么默认配置会花屏
根本原因在于帧缓冲的分配策略和slice输出时序不匹配。默认配置假设一帧的所有slice会在极短时间内连续到达,缓冲区的读写指针按整帧推进。但实际网络传输中,slice之间可能有几十毫秒的间隔,这时候如果缓冲区管理没做slice级别的隔离,后一个slice的数据就可能覆盖前一个slice还没被消费的缓冲,导致解码器读到脏数据。
另一个隐性问题是参考帧管理。多slice帧在解码时,每个slice可能引用不同的参考帧区域。如果参考帧标记(reference frame marking)没按slice粒度更新,解码器会用错误的参考帧去重建画面,结果就是大面积花屏。
3. 单帧多slice解码的核心配置项拆解
3.1 通道属性里的slice相关参数
在Hi3559A的MPP框架下,配置VDEC通道时需要关注这几个参数:
| 参数名 | 默认值 | 建议值 | 作用 |
|---|---|---|---|
enSliceDecode | 0 | 1 | 是否启用slice级解码 |
u32MaxSliceNum | 1 | 实际最大slice数 | 单帧最大slice数量 |
enFrameBufRecycle | 1 | 0 | 是否整帧回收缓冲 |
u32RefFrameNum | 动态 | 按码流实际 | 参考帧数量 |
enSliceDecode是总开关,不开这个后面都白搭。u32MaxSliceNum要设成码流里实际出现的最大slice数,设小了会丢slice,设大了浪费内存。enFrameBufRecycle这个参数很关键,默认是整帧回收,多slice场景下要改成按slice回收,否则就会出现前面说的缓冲区覆盖问题。
3.2 码流发送方式的调整
配置改完只是第一步,码流怎么送给VDEC同样重要。默认的发送接口是HI_MPI_VDEC_SendStream,它按帧为单位提交数据。多slice场景下,建议改用HI_MPI_VDEC_SendStream的slice模式,或者手动控制每次发送的数据量,确保一个slice一个slice地送。
具体做法是:在解析码流时,先识别出每个slice的起始码(H.265是00 00 01后的NAL单元类型),然后按slice边界切分数据包。每个数据包单独调用发送接口,并在结构体里标记bSliceEnd字段。这样VDEC就知道当前送的是一个完整的slice,可以立即开始解码,不用等整帧数据到齐。
3.3 输出缓冲区的对齐要求
Hi3559A的VDEC输出缓冲区有对齐要求,多slice解码时这个要求更严格。宽度必须按16像素对齐,高度按2像素对齐。如果码流分辨率不是对齐的,需要在配置通道时手动指定u32PicWidth和u32PicHeight为对齐后的值,然后在VPSS或VO阶段做裁剪。
举个例子,码流是1920x1080,宽度1920是16的倍数没问题,高度1080不是2的倍数吗?1080/2=540,是整数,所以没问题。但如果遇到1920x1081这种奇怪分辨率,高度就要补到1082。这个细节不注意,解码器会直接报错或者输出花屏。
4. 完整配置流程与实操步骤
4.1 系统初始化与VDEC通道创建
先走标准流程:HI_MPI_SYS_Init初始化系统,然后设置VB池。VB池的配置要多留一些余量,因为多slice解码时每个slice可能占用独立的缓冲块。
VB_CONFIG_S stVbConf; memset(&stVbConf, 0, sizeof(VB_CONFIG_S)); stVbConf.u32MaxPoolCnt = 3; stVbConf.astCommPool[0].u32BlkSize = 1920 * 1080 * 2; // 参考帧 stVbConf.astCommPool[0].u32BlkCnt = 8; stVbConf.astCommPool[1].u32BlkSize = 1920 * 1080 / 2; // slice缓冲 stVbConf.astCommPool[1].u32BlkCnt = 16; HI_MPI_VB_SetConf(&stVbConf); HI_MPI_VB_Init();注意第二个池子,专门给slice缓冲用,块数要给够。我一般按"最大slice数 × 并发通道数 × 2"来估算,留一倍余量。
创建VDEC通道时,通道属性这样填:
VDEC_CHN_ATTR_S stChnAttr; stChnAttr.enType = PT_H265; stChnAttr.enMode = VIDEO_MODE_FRAME; stChnAttr.u32PicWidth = 1920; stChnAttr.u32PicHeight = 1080; stChnAttr.u32StreamBufSize = 1920 * 1080 * 2; stChnAttr.u32FrameBufCnt = 8; stChnAttr.u32Priority = 0; stChnAttr.bSliceDecode = HI_TRUE; // 关键:开启slice解码 stChnAttr.u32MaxSliceNum = 8; // 按实际码流设置 stChnAttr.bFrameBufRecycle = HI_FALSE; // 按slice回收 HI_MPI_VDEC_CreateChn(ChnId, &stChnAttr);4.2 码流解析与slice切分
这一步是整个流程里最需要耐心的。H.265的slice边界识别靠NAL单元类型,类型1到31是VCL单元,其中类型1是TRAIL_R,类型19是IDR_W_RADL,这些都可能包含slice数据。实际解析时,我一般用状态机扫起始码,遇到00 00 01就检查下一个字节的高几位。
int find_slice_boundary(unsigned char *buf, int len) { for (int i = 0; i < len - 3; i++) { if (buf[i] == 0x00 && buf[i+1] == 0x00 && buf[i+2] == 0x01) { int nal_type = (buf[i+3] >> 1) & 0x3F; if (nal_type >= 0 && nal_type <= 31) { return i; // 找到slice起始位置 } } } return -1; }找到边界后,把数据按slice切分,每个slice单独构造VDEC_STREAM_S结构体发送。结构体里的bEndOfSlice字段要置为HI_TRUE,告诉VDEC这是一个完整的slice。
4.3 发送码流与获取解码结果
发送的时候有个细节:不要一次性把整帧所有slice都塞进去。我的做法是维护一个发送队列,每次取一个slice发送,等VDEC返回可接收状态后再发下一个。这样能避免内部缓冲溢出。
VDEC_STREAM_S stStream; stStream.pu8Addr = slice_data; stStream.u32Len = slice_len; stStream.u64PTS = pts; stStream.bEndOfFrame = is_last_slice ? HI_TRUE : HI_FALSE; stStream.bEndOfSlice = HI_TRUE; stStream.bDisplay = HI_TRUE; while (HI_MPI_VDEC_SendStream(ChnId, &stStream, 0) != HI_SUCCESS) { usleep(1000); // 缓冲满,等1ms重试 }获取解码结果用HI_MPI_VDEC_GetFrame,注意要判断返回的帧是否完整。多slice场景下,有时候会先返回一个部分解码的帧,这时候不能直接送去显示,要等所有slice都解完再取。
4.4 与VPSS和VO的对接
解码出来的帧要送到VPSS做缩放或格式转换,再送VO显示。这里的关键是帧的时序标记。多slice解码时,同一帧的多个slice可能在不同时间点解完,但对外必须表现为一帧完整的画面。我的做法是在VDEC输出时打上帧序号,VPSS按序号组帧,确保同一帧的所有slice数据都到齐了再往下送。
VPSS的配置相对标准,注意把u32MaxW和u32MaxH设成实际分辨率,不要用默认值。VO那边如果做多路拼接,要确保每路的时间戳对齐,否则会出现画面撕裂。
5. 调试过程中遇到的典型问题与排查方法
5.1 花屏但音频正常
这是最典型的症状。音频走的是独立通道,不受VDEC影响,所以音频正常说明系统整体没崩,问题局限在视频解码路径。排查顺序建议这样:
- 先确认
bSliceDecode是否真的生效了,可以读寄存器或者看VDEC的调试信息。 - 检查
u32MaxSliceNum是否小于实际slice数,这个设小了会丢slice,表现就是花屏。 - 抓码流分析slice结构,确认编码端是不是真的做了slice切分。有时候编码端配置改了但没生效,码流还是整帧的,这时候开slice解码反而会出问题。
5.2 解码卡顿、帧率上不去
卡顿通常是缓冲不够或者发送节奏不对。先看VB池的块数够不够,多slice解码对缓冲的需求比整帧解码大不少。然后检查发送逻辑,如果一次性把整帧所有slice都发进去,VDEC内部排队会很长,表现出来就是延迟大、帧率低。
我的经验是控制发送速率,每发一个slice等一小会儿,让VDEC有时间处理。具体等多久可以实测,一般1到2毫秒就够。另外,u32Priority可以适当调高,让VDEC线程优先调度。
5.3 多路并发时性能骤降
单路正常、多路就崩,这个问题我遇到过好几次。根因往往是DDR带宽争抢。多slice解码时,每个slice都要独立读写缓冲,DDR访问次数比整帧解码多得多。如果多路同时跑,带宽就不够了。
解决办法有两个方向:一是降低单路的缓冲需求,比如把参考帧数量从默认的4降到2;二是错开各路的解码时间,不要让所有通道同时处于解码高峰。我一般会在通道创建时给不同的u32Priority,让高优先级的通道先解,低优先级的等一等。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决措施 |
|---|---|---|---|
| 花屏 | slice数超限 | 查u32MaxSliceNum | 调大到实际值 |
| 花屏 | 缓冲覆盖 | 查bFrameBufRecycle | 改为按slice回收 |
| 卡顿 | 缓冲不足 | 查VB池块数 | 增加slice缓冲块 |
| 卡顿 | 发送过快 | 查发送逻辑 | 加延时控制 |
| 多路崩 | 带宽不足 | 查DDR占用 | 降参考帧数或错峰 |
| 画面错位 | 对齐问题 | 查分辨率 | 按16x2对齐 |
6. 几个容易被忽略的细节和实操心得
6.1 参考帧标记要按slice更新
这个坑我踩过。H.265的参考帧管理比H.264复杂,多slice场景下,每个slice可能引用不同的参考帧集合。如果参考帧标记没按slice粒度更新,解码器会用错误的参考帧去重建,结果就是画面局部花屏。解决办法是在解析码流时,把每个slice的参考帧信息提取出来,通过私有接口传给VDEC。Hi3559A的MPP里有对应的扩展接口,具体可以查hi_mpi_vdec.h里的HI_MPI_VDEC_SetSliceRefInfo。
6.2 PTS和DTS的处理
多slice解码时,PTS和DTS的处理要格外小心。同一帧的多个slice应该共享同一个PTS,但DTS可能不同。如果PTS处理错了,显示端会出现画面顺序错乱。我的做法是在发送第一个slice时带上PTS,后续slice的PTS置为无效值,让VDEC自己处理。
6.3 内存对齐的隐藏要求
除了前面说的宽高对齐,VDEC的输入缓冲地址也有对齐要求,一般是128字节对齐。如果自己管理码流缓冲,分配内存时要用HI_MPI_SYS_MmzAlloc,它会自动做对齐。用malloc分配的内存可能不对齐,导致解码器读数据时出错。
6.4 调试信息的利用
Hi3559A的VDEC有调试信息输出,可以通过/proc/mpp/vdec查看。里面会显示每个通道的解码状态、缓冲使用情况、slice计数等。调试花屏问题时,我习惯先看这个文件,确认slice是否被正确识别和计数。如果slice计数和实际码流对不上,那问题就在解析层,不用往下查了。
6.5 版本兼容性
不同版本的MPP对slice解码的支持程度不一样。我用的那版SDK里,bSliceDecode这个字段是后来加的,早期版本没有。如果你的SDK里找不到这个字段,要么升级SDK,要么用私有接口绕过。升级前记得确认新版本和现有代码的兼容性,MPP的接口变动有时候挺大的。
7. 性能调优的进阶思路
7.1 动态调整slice缓冲
固定分配slice缓冲有时候会浪费内存,特别是多路场景下。可以做一个简单的动态调整:监控每路的实际slice数,如果某路长期只用2个slice,就把它的缓冲块数降下来,把省出的内存给其他路。这个逻辑可以在应用层实现,定期调用HI_MPI_VDEC_SetChnAttr调整参数。
7.2 利用硬件解码器的并行能力
Hi3559A的VDEC内部有多个解码核,多slice帧天然适合并行解码。配置的时候可以指定u32DecodeCoreNum,让不同的slice分配到不同的核上。这个参数默认是1,改成2或4能明显提升解码速度,但要注意核间同步的开销。实测下来,slice数大于4的时候开多核效果比较明显。
7.3 与编码端的协同优化
如果编码端也是自己可控的,建议在编码时就考虑解码端的处理能力。比如控制单帧的slice数不要太多,一般4到8个比较合适。slice太多会增加解码端的调度开销,太少又起不到低延迟的效果。另外,编码端可以定期插入整帧编码的I帧,让解码端有机会做参考帧刷新,减少花屏的累积效应。
8. 一个完整的配置示例
把前面的内容串起来,给一个可以直接参考的配置流程。假设场景是4路1080P H.265解码,码流每帧8个slice。
// 1. 系统初始化 HI_MPI_SYS_Init(); HI_MPI_VB_Init(); // 2. 配置VB池 VB_CONFIG_S stVbConf; stVbConf.u32MaxPoolCnt = 3; stVbConf.astCommPool[0].u32BlkSize = 1920*1080*2; stVbConf.astCommPool[0].u32BlkCnt = 32; // 4路 x 8参考帧 stVbConf.astCommPool[1].u32BlkSize = 1920*1080/4; stVbConf.astCommPool[1].u32BlkCnt = 64; // 4路 x 8slice x 2 HI_MPI_VB_SetConf(&stVbConf); // 3. 创建4个VDEC通道 for (int i = 0; i < 4; i++) { VDEC_CHN_ATTR_S stAttr; stAttr.enType = PT_H265; stAttr.enMode = VIDEO_MODE_FRAME; stAttr.u32PicWidth = 1920; stAttr.u32PicHeight = 1080; stAttr.u32StreamBufSize = 1920*1080; stAttr.u32FrameBufCnt = 8; stAttr.bSliceDecode = HI_TRUE; stAttr.u32MaxSliceNum = 8; stAttr.bFrameBufRecycle = HI_FALSE; stAttr.u32Priority = i; // 错开优先级 HI_MPI_VDEC_CreateChn(i, &stAttr); } // 4. 码流发送(伪代码) while (1) { for (int chn = 0; chn < 4; chn++) { slice = get_next_slice(chn); VDEC_STREAM_S stStream; stStream.pu8Addr = slice.data; stStream.u32Len = slice.len; stStream.bEndOfSlice = HI_TRUE; stStream.bEndOfFrame = slice.is_last; stStream.u64PTS = slice.pts; HI_MPI_VDEC_SendStream(chn, &stStream, 0); usleep(1000); // 控制节奏 } }这个配置在我这边跑4路1080P@30fps是稳的,CPU占用大概在40%左右,DDR带宽占用约60%。如果路数更多,需要相应调整VB池和优先级。
9. 最后聊几句实际体会
多slice解码这个事,说难不难,说简单也不简单。核心就那几行配置,但背后的时序、缓冲、对齐问题一大堆。我最大的体会是:不要迷信默认配置。Hi3559A的默认参数是按通用场景调的,遇到多slice码流必须手动改。另外,调试的时候一定要抓码流分析,不要凭感觉猜。很多时候花屏不是解码器的问题,而是码流本身就不规范。
还有一点,多slice解码对系统资源的消耗比整帧解码大,做方案设计的时候要提前算好带宽和内存。我见过有人按整帧解码的规格选DDR,结果多slice一开就崩,最后只能降分辨率。这个坑提前避开,能省不少事。
如果你在配置过程中遇到奇怪的问题,欢迎一起交流。这块芯片的文档不算特别详细,很多细节得靠实测积累。我这边踩过的坑基本都写出来了,希望能帮你少走点弯路。