01 · 点亮 STM32MP25 的硬件视频编解码:VPU 与那些“安静“的坑

本系列第 1 篇 | 2026-05 | 标签:STM32MP2、GStreamer、V4L2、硬件编解码

一切从最小验证开始:不写业务逻辑,先用 GStreamer 把 STM32MP25 上的

Verisilicon/Hantro VPU 跑通 ------ 能编码、能解码、能封装成文件。

这一篇记录跑通过程,以及三个不报错、只是结果不对 的安静坑:

裸流 VLC 打不开、NV12 带 padding、"JPEG 那条没问题"推不出"H264 也没问题"。

硬件形态:两个 V4L2 设备

VPU 驱动在内核侧是 drivers/media/platform/verisilicon/,对外呈现为两个独立的

V4L2 mem2mem 实体:

实体 方向 输入 → 输出
venc(编码) 原始帧 → 码流 NV12/YUY2/UYVY/I420/RGB16/BGRx → H264/VP8/JPEG
vdec(解码) 码流 → 帧 H264/VP8/JPEG → NV12/NV16,上限 1080p(JPEG/编码可 4K)

它们各占一个 /dev/videoN 和一个 /dev/mediaN。注意:号不是固定的 ,

每次开机都可能变 ------ 这是下一篇的主题,本篇先按下不表。

关键点:H264 走的是 stateless (无状态)接口,码流格式是"Parsed Slice Data"

------ pipeline 里必须h264parse 把码流整理成驱动要的格式。漏掉它不会报

"缺元件",而是协商直接失败。

第一条编码管线:先不写代码

300 帧彩条、720p、CBR 2Mbps、GOP=30:

sh 复制代码
gst-launch-1.0 -e videotestsrc num-buffers=300 ! \
    video/x-raw,width=1280,height=720,format=NV12,framerate=30/1 ! \
    v4l2slh264enc bitrate=2000000 rate-control=cbr keyframe-interval=30 ! \
    h264parse ! mp4mux ! filesink location=/tmp/test.mp4

v4l2slh264enc(stateless encoder)的关键属性全走 GStreamer 属性,没有

extra-controls:

  • bitrate(bit/s)、rate-control=cbr|cqp
  • keyframe-interval(GOP,帧数)
  • 输入 caps 原生接受 RGB16

最后一条在本项目里价值千金,后面几篇会反复用到:DCMIPP 摄像头主路只输出 RGB
(RGB565/RGB888),不出 YUV;而编码器原生吃 RGB16
------ 所以摄像头可以直喂编码器,

不需要 videoconvert,省掉一次整帧 CPU 颜色转换。在双核 A35 上,"省一次 640×480

的逐像素转换"就是省出一个核的三分之一。

接摄像头:第一条真实管线

sh 复制代码
# 设备号按实体名查(为什么不能写死,见第 2 篇)
M=$(for m in /dev/media*; do media-ctl -d $m -p 2>/dev/null | grep -q 'driver  *dcmipp' && echo $m; done | head -1)
CAM_NODE=$(media-ctl -d $M -e dcmipp_main_capture)

gst-launch-1.0 -e v4l2src device=$CAM_NODE io-mode=dmabuf ! \
  video/x-raw,format=RGB16,width=640,height=480,framerate=30/1 ! \
  v4l2slh264enc bitrate=2000000 ! h264parse config-interval=1 ! mpegtsmux ! \
  filesink location=/tmp/cam.ts

三个细节:

  1. 必须显式给 caps 。不给的话协商出非法分辨率,报 Wrong width or height
    / Invalid format ------ 这算报错的,不算安静坑。
  2. io-mode=dmabuf:见下一节。
  3. h264parse config-interval=1:让 SPS/PPS 随关键帧重复发送。第 6 篇会讲丢掉它
    引发的"中途接入黑屏",这里先记住:只要有人可能中途开始看流,参数集就必须
    周期性出现

dmabuf:零拷贝不是玄学,是少两次 memcpy

io-mode=mmap 时数据路径是:DCMIPP 的 DMA buffer → 拷到用户态 → 再拷进 VPU

输入队列。640×480@30 下每帧 ~600KB × 2 次 memcpy,白烧 CPU。

io-mode=dmabuf:DCMIPP 的 DMA buffer 以 dmabuf fd 直接挂到 VPU 输入队列,

零拷贝。这个思想后面还会以另一种形式出现(第 5 篇:共享内存环里传 fd 引用而不是

像素,CPU 从 47.5% 干到 1.7%)。

安静坑一:裸流 .h264,VLC 打不开

把编码输出直接写文件(.h264,Annex-B 裸流),VLC 报"无法解复用"或秒退。

第一反应往往是怀疑码流坏了 ------ 其实码流完全合法,可以用三重方式证实:

od 看 NAL 起始码、手解 SPS 头、板上硬解回放。

真正原因:.h264 是裸 ES,无容器无时间戳,VLC 对 raw-ES 的兜底探测很弱。

解法任选:

sh 复制代码
mux 封 TS 给 VLC 直接播        # 推荐,MPEG-TS 兼容性最好
ffplay /tmp/cam.h264           # 或用 ffplay 播裸流

教训:验证码流要用对工具,别让播放器的探测能力背锅

应用骨架:appsink 收尾

封装好容器后不落盘、交给程序处理的最小骨架(appsink 回调示意):

c 复制代码
static GstFlowReturn on_new_sample(GstAppSink *sink, gpointer user) {
    GstSample *s = gst_app_sink_pull_sample(sink);
    GstBuffer *buf = gst_sample_get_buffer(s);
    GstMapInfo map;
    if (gst_buffer_map(buf, &map, GST_MAP_READ)) {
        fwrite(map.data, 1, map.size, out_fp);   /* 这里换成推流/转发即可 */
        gst_buffer_unmap(buf, &map);
    }
    gst_sample_unref(s);
    return GST_FLOW_OK;
}

要接 RTMP/WebRTC/RTSP,替换回调里这一处就行 ------ 第 4 篇的 WebRTC 推流正是从

这里长出来的。

解码与最阴险的坑:NV12 带 padding

解码管线对称简单,输入容器按扩展名分流(.mp4→qtdemux、.ts→tsdemux、

.h264→直接 h264parse):

sh 复制代码
gst-launch-1.0 filesrc location=/tmp/cam.ts ! tsdemux ! h264parse ! \
    v4l2slh264dec ! videoconvert ! waylandsink     # 硬解上屏

然后是这个项目踩过最安静的坑。我们想当然地按紧凑公式 w*h*3/2 切解码输出的

NV12 帧,存出来的文件用 ffplay -f rawvideo -s 1280x720 播,花屏

实测对比(720p):

字节数
紧凑值 1280×720×3/2 1382400 B
v4l2slh264dec 实际每帧 1612832 B(+230432 B)

原因:v4l2slh264dec 的输出 buffer 带 stride / 高度对齐填充。按紧凑公式切帧,

拿到的是一张"斜图"。

更阴险的地方在于:同一颗 VPU 上,另一个解码器行为不一样 ------ 我们此前用

v4l2jpegdec 做过 JPEG 解码验证,输出就是精确的紧凑 1382400 B。于是形成了

错误归纳:"这条链路这么写没问题"。正确结论是:

"JPEG 解码器输出紧凑" 推不出 "H264 解码器也紧凑"。

换一个 IP/驱动路径,所有关于内存布局的假设都要重新验证。

正确姿势二选一:

sh 复制代码
# ① 让 GStreamer 自己处理(肉眼确认内容,最省事):
... ! v4l2slh264dec ! videoconvert ! jpegenc ! multifilesink location=/tmp/dec_%03d.jpg

# ② 自己取紧凑数据:gst_buffer_get_video_meta() 按 stride[]/offset[] 逐行拷

⚠ 用 ① 时别把慢当成硬解性能:videoconvert + jpegenc 是软件元件,686 帧在 A35 上

跑了 7 分半(≈1.5 fps),慢的是软 JPEG 编码,硬解本身毫秒级。

新板子到手的第一件事:能力探测

写一个探测脚本做两件事:枚举编解码设备能力 + 摄像头各格式直采测试,输出一份

info 文件。思路核心:

sh 复制代码
# 枚举编码器能力(解码同理)
v4l2-ctl -d /dev/videoN --list-formats-ext

# 摄像头某格式能不能直采?开 10 帧试试,成功与否都记下来
v4l2-ctl -d $CAM_NODE --set-fmt-video=width=640,height=480,pixelformat=RGBP \
    --stream-mmap --stream-count=10

后来排查过的很多问题(下一篇的格式钉错节点、aux/dump 开反)都能在这份探测输出里

提前发现。新硬件先摸清能力边界,再写业务代码 ------ 这条纪律贯穿整个系列。

小结

  • stateless 编码器必须配 h264parse;中途接入的场景必须 config-interval
  • 编码器吃 RGB16,摄像头直喂,免一次整帧软转
  • dmabuf 零拷贝 = 省两次整帧 memcpy
  • 裸流打不开先怪工具再怪码流
  • 解码输出的内存布局要实测,同类设备的经验不可平移

下一篇:02 · 摄像头这条路坑更多

------ 设备号每次开机都变、media link 默认关闭、STREAMON 报 -22 但所有 S_FMT 全成功。

相关推荐
欧叶冲冲冲1 小时前
uv Python 环境管理笔记
笔记·python·uv
自小吃多1 小时前
Capture软件原理图PDF输出笔记
笔记·嵌入式硬件
zbyyd1 小时前
深入理解 Linux 文件 IO 与目录 IO:系统调用与库函数的本质区别
linux·c语言
存在morning1 小时前
【PySpark 学习笔记 四】DataFrame 进阶:窗口函数、高级聚合与复杂类型
笔记·学习
Wang's Blog1 小时前
PostgreSQL笔记62: 分区表维护最佳实践——默认分区、锁策略与性能调优
数据库·笔记·postgresql
电化学仪器白超2 小时前
梅特勒-托利多自动滴定管产品线全览
网络·python·单片机
SUNNYSPY0012 小时前
30N06NF-ASEMI高耐压功率器件选型30N06NF
单片机
花月mmc2 小时前
Arduino UNO R4 ——电压电流数据显示
嵌入式硬件·mcu·物联网
末代iOS程序员华仔2 小时前
Codex + Figma 生成 Objective‑C (UIKit) 完整工作流
c语言·开发语言·figma