野火电子论坛

 找回密码
 注册

QQ登录

只需一步,快速开始

查看: 18|回复: 0

[经验\资料] 鲁班猫A0(全志H618)V4L2 M2M 硬件编码器驱动结构设计

[复制链接]
发表于 昨天 20:44 | 显示全部楼层 |阅读模式
本文描述 kernel/ 下 sunxi_venc.ko(模块名 sunxi_venc,设备 /dev/videoN)的 分层结构:格式表、队列、控件、编码核心。目标是「V4L2 外壳 + 编码核心」两部分, 编码核心不认识 V4L2,从而可以脱离 vb2 单独测试、复用,格式/控件/队列的扩展 互不牵连。

1. 分层与依赖方向
        user (v4l2-ctl / gst / ffmpeg)
                    │  ioctl
        ┌───────────▼────────────┐
        │ venc_main.c            │  设备生命周期 / video_device / fops / ioctl 表
        └───┬────────┬───────┬───┘
            │        │       │
  ┌─────────▼──┐ ┌───▼─────┐ ┌▼────────────┐
  │ venc_fmt.c │ │venc_vb2 │ │ venc_ctrl.c │
  │ 格式表      │ │ .c 队列 │ │ 控件         │
  └─────────┬──┘ └───┬─────┘ └┬────────────┘
            │        │        │  (只写参数)
            └────────┴───┬────┘
                         │
                 ┌───────▼────────┐
                 │ venc_enc.c     │  寄存器 / SPS·PPS / 码控 / 复位与重试
                 │ 编码核心        │  ← 只依赖 linux/io、dma-mapping
                 └────────────────┘
依赖是单向的:venc_enc.c 不 include 任何 media/ 头文件,也不出现 vb2_*、 v4l2_* 符号;venc_drv.h 是唯一的共享私有头。这条纪律可以用一行命令守住:

grep -nE 'v4l2_|vb2_|videodev2|media/' kernel/venc_enc.c   # 必须无输出(注释除外)
文件        职责        关键导出
venc_main.c        找 VE 平台设备、取 regs/clk/IOMMU、注册 video_device;ioctl 转发;open/release/mmap/poll        venc_fops / venc_ioctl_ops
venc_fmt.c        格式表 + ENUM/G/TRY/S_FMT、ENUM_FRAMESIZES、G/S_PARM        venc_fmt_find/get/default/align/fill、venc_enum_fmt…
venc_vb2.c        两条 vb2 队列、buffer 链表、配对并编码的 kthread、EOS        venc_vb2_queue_init/start_job/stop_job/flush
venc_ctrl.c        控件注册与 S_CTRL → venc_enc_set_* 直写        venc_ctrl_init/free
venc_enc.c        编码核心:VE 寄存器、比特写入、SPS/PPS/slice、码控、复位与重试        venc_enc_start/stop/frame
venc_drv.h        共享结构、寄存器偏移、格式表类型、各层原型        —
驱动不绑定 VE 的 DT 节点:BSP 的 sunxi-cedar 已经占了它(/dev/cedar_dev), 本驱动用 bus_find_device() 找到该平台设备,复用它的寄存器窗口、时钟和 IOMMU 域,再对外暴露一个 V4L2 形态的第二个接口。复位线归 BSP 驱动独占,只能借 IOCTL_RESET_VE(0x104)用(§5.6)。

2. 格式表
格式的全部知识集中在一张表里,队列层只读取 S_FMT 时缓存进 ctx 的 in_size / cap_size / out_bytesperline,因此新增格式不需要改 venc_vb2.c。

struct venc_format {
    u32  pixelformat;      /* V4L2 fourcc */
    const char *name;
    u32  type;             /* OUTPUT / CAPTURE */
    u32  flags;            /* 回报给 ENUM_FMT,如 COMPRESSED */
    u16  h_align, v_align; /* 硬件/码流要求的对齐 */
    u32  (*bytesperline)(const struct venc_format *, u32 width);
    u32  (*sizeimage)(const struct venc_format *, u32 w, u32 h, u32 bpl);
};
当前条目(只列已上板验证的两种):

type        fourcc        flags        对齐        bytesperline        sizeimage
OUTPUT        V4L2_PIX_FMT_NV12        —        16x16        ALIGN(w,16)        bpl * h * 3 / 2
CAPTURE        V4L2_PIX_FMT_H264        COMPRESSED        16x16        0(压缩)        w*h > 1280*720 ? 4 MiB : 1 MiB
venc_fmt_fill() 是唯一的「格式 → v4l2_pix_format」出口,同时固定 field = NONE、colorspace = REC709。

2.1 ioctl 契约
ioctl        行为
ENUM_FMT        按 type 过滤表项,index 越界返回 -EINVAL
TRY_FMT        未知 fourcc 不报错,回落该队列默认格式;只保留(clamp + 对齐)请求的宽高
S_FMT        先走 TRY_FMT;`streaming_out
G_FMT        直接回填缓存(out_bytesperline / in_size / cap_size),不重算
ENUM_FRAMESIZES        只支持 index == 0,CONTINUOUS,64..3840 × 64..2160,步进 16
尺寸 clamp 全部走 venc_fmt_align():先 clamp 到 [VENC_MIN, VENC_MAX],再 ALIGN_UP,最后防止对齐后越界(max & ~(a-1))。

2.2 S_FMT(OUTPUT) 是几何的唯一入口
S_FMT(OUTPUT) ──► ctx->in_fmt / width / height / out_bytesperline / in_size
                        │
                        └─► venc_enc_set_geometry():pad_w/pad_h/stride/mbw/mbh
S_FMT(CAPTURE) ─► ctx->out_fmt / cap_size          (不碰几何)
由此得到核心零拷贝所依赖的不变式:

ctx->out_bytesperline == enc->stride。核心把 OUTPUT buffer 的 DMA 地址直接 交给 ISP,按 stride = ALIGN_UP(w,16) 逐行读;两者不一致会读错行。
geometry 只在 S_FMT 且未开流时改变,venc_enc_start() 之后被冻结(池按它分配)。
新增格式(NV21 / NV16 / MPLANE)需要:表里加一行 + enc_program_frame() 的 0xA08(输入排列)+ 必要时 venc_enc_frame() 的色度偏移约定。venc_vb2.c 不用改。

3. 队列
不使用 v4l2-mem2mem 框架:硬件单实例、一次只能跑一帧,v4l2_m2m 的调度抽象 带来的复杂度大于收益。两条普通 vb2 队列 + 一个配对线程已经足够,并且和 VIDIOC_ENCODER_CMD 的语义可以直接对上。

OUTPUT(NV12) out_q ──out_list──┐
                               ├─► [sunxi-venc kthread] ─► venc_enc_frame()
CAPTURE(H264) cap_q ──cap_list─┘                              │
        ▲                                                     ▼
        └──── vb2_buffer_done(两路各自完成) ◄──── venc_do_encode()
3.1 vb2 配置
项        值        原因
io_modes        VB2_MMAP        未做 DMABUF import
mem_ops        vb2_dma_contig_memops        分配即物理/IOVA 连续,可直挂 IOMMU
q->dev / alloc_devs[0]        VE 平台设备        buffer 落在 VE 的 IOMMU 域,核心才能拿到 dma_addr_t
buf_struct_size        sizeof(struct venc_buf)        带 queued_ts 延迟探针
timestamp_flags        TIMESTAMP_COPY        驱动自己把 OUTPUT 时间戳拷到 CAPTURE
q->lock        &ctx->qlock        两条队列共用一把串行化锁
queue_setup        单 plane,size = 缓存的 in_size/cap_size        格式表决定,队列层不重算
buf_prepare        plane_size < need → -EINVAL        防止 S_FMT 变小后复用旧 buffer
3.2 锁
锁        保护        谁持有
ctx->qlock        vb2 队列状态、S_FMT 与开/停流的串行化        ioctl 与 vb2 核心
ctx->list_lock(spinlock)        out_list / cap_list        kthread、buf_queue、flush
enc->run_lock(mutex)        venc_enc_start/stop/frame 互斥,等在飞帧结束        kthread、start/stop_streaming
enc->lock(spinlock, irqsave)        每帧参数快照与序列/码控状态        venc_enc_set_*、frame
规则:

kthread 只持 list_lock 摘 buffer、持 enc->run_lock 编码,绝不持 qlock。
stop_streaming(OUTPUT) 顺序固定:venc_enc_stop()(run_lock 等在飞帧结束)→ venc_vb2_flush()(链表中未编的 buffer 以 ERROR 归还)。这样释放池时 kthread 已经不可能再触碰它。
vb2_buffer_done() 在无 qlock 时调用,是有意为之:stop_streaming 持 qlock 等 run_lock,若 kthread 编码完再回头抢 qlock 归还 buffer 就会死锁。 代价是不满足 vb2 文档「持队列锁调用」的建议,属于已知权衡(§8)。
3.3 流生命周期
open()           单实例检查(第二次 -EBUSY)→ kzalloc ctx → 默认格式/几何
                 → venc_ctrl_init(含向核心 seed 默认值)→ 两条 vb2 队列
                 → kthread_run("sunxi-venc")
STREAMON(OUTPUT) venc_enc_start():分配 DMA 池 + 真 VE 复位;清 draining/last_flag_sent
STREAMON(CAP)    仅置 streaming_cap(buffer 已由 vb2 备好)
QBUF             记 queued_ts → 入链表 → wake_up
kthread          两链表都非空 → 各取队首 → venc_enc_frame() → 两路 done
STREAMOFF(OUT)   venc_enc_stop()(等在飞帧)→ flush 本队列链表
release()        stop kthread → venc_enc_stop → vb2_queue_release ×2 → fh/ctrl 释放
DMA 池的生命周期因此从「open→release」收紧到「OUTPUT streamon→streamoff」。

3.4 EOS:同时用两种语言表达
V4L2_ENC_CMD_STOP 置 draining 并唤醒线程;START / 重新 STREAMON 清除。 收尾有两条路径,因为消费者不同:

场景        表达
消费掉最后一个 OUTPUT buffer 的那一帧        该 CAPTURE buffer 带 V4L2_BUF_FLAG_LAST(venc_do_encode() 里判断 draining && out_list 空)
STOP 到达时已无待编帧        venc_finish_drain() 取一个 CAPTURE buffer,0 字节 payload + LAST 立即归还
两种之后        venc_send_eos() 投 V4L2_EVENT_EOS,清 draining/last_flag_sent
ffmpeg 的 v4l2_m2m 只认 LAST、不看 V4L2_EVENT_EOS,缺了它会永远阻塞在 DQBUF;GStreamer 的 v4l2videoenc 则订阅事件。last_flag_sent 保证一次 drain 只发一次 LAST。

其他队列语义:

IDR 帧的 CAPTURE buffer 带 V4L2_BUF_FLAG_KEYFRAME。
venc_enc_frame() 返回错误时:输入已消费(DONE),输出 buffer 以 ERROR 归还,调用方看到的是「这一帧没有码流」而不是整个流失败。
CAPTURE 的 mmap offset 加 1 << 30(VENC_DST_QUEUE_OFF_BASE),mmap() 按 vm_pgoff 高位选队列,两队列 offset 不冲突。
4. 控件
原则:每个控件都必须一对一映射到核心真的能做的事,没有装饰性控件。

CID        范围/默认        生效方式
V4L2_CID_MPEG_VIDEO_BITRATE        0..100 Mbit/s,步进 1 k,默认 4 Mbit/s        venc_enc_set_bitrate();0 = 关码控、固定 QP
V4L2_CID_MPEG_VIDEO_GOP_SIZE        1..600,默认 30        venc_enc_set_gop(),下一帧起按新 GOP 判 IDR
V4L2_CID_MPEG_VIDEO_H264_I_FRAME_QP        1..51,默认 26        venc_enc_set_qp();码控开时作为起点
V4L2_CID_MPEG_VIDEO_FORCE_KEY_FRAME        BUTTON        venc_enc_force_key(),下一帧强制 IDR
全部走 s_ctrl 回调直写核心,允许流中修改:核心在每帧开始时于 enc->lock 内快照 qp/gop/bitrate/force_idr,因此 slice header 里的 QP 与 寄存器 0xB08 永远一致,不会出现「头用旧 QP、寄存器用新 QP」的撕裂。
FORCE_KEY_FRAME 必须用 V4L2_CTRL_TYPE_BUTTON(v4l2_ctrl_new_custom)。 若按普通整型创建(max=0),用户写 1 会被 clamp 成 0,控件永久失效——原单文件 实现里的隐藏 bug,拆分时一并修正。
venc_ctrl_init() 结束时把三个标量控件的当前值 seed 进核心,避免「控件显示 4 Mbit/s、核心还是编译默认值」;q->lock 尚无人持有,seed 不受并发影响。
每帧预算 = bitrate * fps_num / fps_den(timeperframe = 秒/帧),时间基来自 S_PARM,改 S_PARM 即时重算。
未暴露:BITRATE_MODE(只有「每帧固定预算 + 积分修正」一个控制器,勉强算 CBR, 不宣称 VBR)、PROFILE/LEVEL(固定 Baseline / 按分辨率推导 level)、 H264_MIN/MAX_QP(码控内部已 clamp 1..51)。宁缺毋滥。
5. 编码核心下沉
venc_enc.c 是把用户态 src/venc_ve_h264.c 的编码逻辑下沉进内核的产物。接口参数 全是原始指针 / DMA 地址 / 几何,没有任何 V4L2 类型:

int  venc_enc_init(struct venc_enc *e, struct device *dma_dev, void __iomem *regs);
void venc_enc_set_geometry(struct venc_enc *e, u32 width, u32 height);
int  venc_enc_start(struct venc_enc *e);      /* 分配 DMA 池 + 真复位 */
void venc_enc_stop(struct venc_enc *e);       /* 等在飞帧结束 + 释放池 */
void venc_enc_set_qp/gop/bitrate/fps/force_key(...);

int  venc_enc_frame(struct venc_enc *e,
                    dma_addr_t src_dma,        /* OUTPUT buffer,NV12,stride */
                    void      *dst_vaddr,      /* CAPTURE buffer,可 NULL(只测吞吐)*/
                    dma_addr_t dst_dma, size_t dst_size,
                    size_t *dst_len, bool *is_idr);
输入不做 CPU 拷贝:src_dma 就是 vb2-dma-contig 给 OUTPUT buffer 的 IOVA, ISP 直接按 0xA78/0xA7C 读;色度平面起始 = stride * height(可见高度,不是 padding 高度)。这是拆分后的关键简化——旧版本往内核池里逐行拷贝。

5.1 核心自有的状态
类别        成员
几何        width/height(可见)、stride、pad_w/pad_h、mbw/mbh
DMA 池        pool_vaddr/pool_dma/pool_size + 各段偏移(§5.3)
序列        frame_num、poc_global、frames_in、rec_flip、since_reset
码控        rc_on、rc_target_bits、rc_deficit、当前 qp
回填        nal_len[]、fix_n(每帧被 VENC 剥掉的起始码位置)
5.2 一次 venc_enc_frame() 的流程
run_lock:与 start/stop 互斥;started == false 直接 -ENODEV;
enc->lock 内快照 qp/gop/rc 并判 IDR(frames_in % gop == 0 || force_idr);
enc_program_frame() 开头:since_reset >= reset_every 则预防性真复位(§5.6) 并清零计数,然后整块重写 0xB00..0xBFC、0xA2C/0xA00/0xA04/0xA08、VLE 地址/长度、REC/REF 双缓冲、subpixel/MV 工作区;
write_header_bits():内存模式先算 SPS+PPS+slice header 的字节流与起始码位置, 再用 PUTBITSDATA(24 bit + ACK 轮询)写进硬件;
udelay(pre_us)(默认 150 µs)settle 后 TRIG_START,然后按 §5.4 等完成;
校验:FINISH 位、VLE 长度 > header、VE_MODE.ENC_EN 仍在、输出不越界; 再过 enc_slice_truncated()(中段被 VLE stall 截断的帧)与起始码回填;
enc->lock 内更新码控、frame_num、rec_flip、frames_in。
失败(步骤 6)→ enc_ve_reset + enc_top_init + enc_rearm 后重放,最多 3 次尝试, 仍失败返回 -EIO(V4L2 层把该帧的 CAPTURE buffer 标 ERROR,输入照常 DONE)。

5.3 DMA 池布局
一次 dma_alloc_coherent(),按 4 KiB 对齐切段(dma_set_mask_and_coherent 限制到 30 bit,让 IOVA 落在与 native/vendor 一致的低地址区):

段        大小        用途
off_input        ALIGN_UP(stride*pad_h*3/2, 4096)        历史遗留:零拷贝改造后已不使用,保留只为布局稳定
off_rec / off_ref        各 ALIGN_UP(luma+chroma, 4096),luma = ALIGN(bw,32)*ALIGN(bh,32),chroma = ALIGN(bw,32)*ALIGN(bh/2,32)        重建/参考帧双缓冲,rec_flip 轮换
off_rec_sl / off_ref_sl        各 ALIGN_UP(luma, 4096)        slice 级重建/参考
off_mb_info        ALIGN_UP(mbw*32 + 0x40000, 4096)        宏块信息(clear_scratch 每帧清零)
off_subpix_a/b        各 ALIGN_UP(sw*sh, 4096),sw = ((mbw+47)*2/3 & ~31) + ALIGN(mbw,32)*2,sh = (mbh*16+72)/8        亚像素工作区,双缓冲
off_unk        ALIGN_UP(ALIGN(mbw,4)*mbh*8, 4096)        引擎要求的未命名工作区
off_mv        ALIGN_UP(mbw*mbh*64 + 65536, 4096)        运动矢量工作区
1080p 池 ≈ 14 MiB,随 pad_w/pad_h 变化;venc_enc_start() 分配,stop() 释放。

5.4 完成判据(三级)
判据        含义
0xB1C bit0(FINISH)        首选。W1C 闩锁:enc_program_frame() 写的 0xf 把它清掉,引擎真正结束才置位,比「计数器不再增长」早约 5 ms
cur == after_hdr && quiet >= dead_polls        一个 slice bit 都没写 → 引擎根本没启动,立即失败(~1 ms)
quiet >= quiet_ms(默认 8 次轮询)        兜底:FINISH 永不来的坏帧
spent >= VENC_STATUS_MS(400 ms)        最后防线
0xB1C 的 bit31/bit15 是编码过程中的瞬态忙标志(正常帧中途就读到 0x80002260),不能单独当错误判据。收尾时按 W1C 写回清除。

5.5 三个必须保住的硬件不变式
VE_MODE[7] 每帧重新置位,坏帧恢复也靠它;
0xA2C(= 0xA00 = 0xB00)的宽高以 8 像素为单位;
VENC 地址寄存器写 addr >> 8,ISP 输入地址写原始地址;
pic_order_cnt_lsb 跨 IDR 必须单调(用 frames_in 而不是 frame_num);
输出 buffer 里的起始码由驱动按记录的 nal_len[] 回填(VENC 会吞掉部分 00 00 00 01)。
5.6 复位策略(三条路)
引擎只有在真复位之后才会置起 VETOP 握手位(0x004/0x01C,手工写不生效), 否则收下 header 却不去取画面。复位线归 BSP 驱动独占,所以统一走 filp_open("/dev/cedar_dev") + ioctl(IOCTL_RESET_VE):

时机        动作        代价
venc_enc_start()        enc_recover():真复位 + VETOP 重放 + 0x04 bit16 脉冲 + 时钟门 0→1        一次性
每 reset_every=24 帧        同上精简版(复位 + VETOP + rearm)        2.5 ms / 24 帧 ≈ 0.3%
单帧失败重试        同上,且 since_reset 清零        只发生在已失败时
为什么是 24:引擎在「距上次复位约 32 张」时会拒收下一张(该帧 vle == after_hdr、 qp 不变,每 32 帧周期性出现,与 GOP 无关)。24 留了余量,且拒帧兜底仍在。

5.7 模块参数(诊断面)
参数        默认        说明
use_finish        Y        用 0xB1C bit0 结束等待
quiet_ms        8        兜底判据:计数器不动几次算停
dead_polls        2        一个 slice bit 都没写就早退
pre_us        150        编程 → 触发之间的 settle(75 µs 会拒帧,100 µs 起干净)
reset_every        24        每 N 帧预防性复位(0 = 关)
stat_attempts/refused/truncated/patchfail        0        计数器,写 0 清零;量拒帧率只能看这里
trace_frame        N        每帧一行(含 fin= 与 wait=)
trace_poll        N        拒帧时 dump 轮询历史
dump_regs        0        dump 前 N 帧的 VENC/VETOP 寄存器
trace_latency(vb2)        N        qbuf->start / prevdone->start / enc 三段耗时
这些是设计与排障面的正式组成部分,不是临时调试代码:docs/HANDOFF.md 里 「量拒帧率看 stat_refused,不要数 dmesg」就是它们的用法。

6. 并发、生命周期与错误码
事件        处理
第二次 open()        -EBUSY(VE 只有一套寄存器与工作区)
S_FMT 在流中        -EBUSY
venc_enc_frame() 失败        -EIO → 该帧 CAPTURE ERROR,输入 DONE,流继续
池分配失败        STREAMON 直接返回错误,V4L2 层不进入 streaming
release()        停 kthread → venc_enc_stop → 释放两条队列 → 释放 ctrl/fh/ctx
venc_mmap() / venc_poll() 用 dev->ctx 而不是 file->private_data,因为它们 在单实例模型下等价,而 vb2 的 queue 需要 ctx 指针。

7. 构建与自测
cd kernel && make module                           # 产出 vebuf.ko + sunxi_venc.ko
sudo insmod /abs/path/kernel/sunxi_venc.ko         # 打印 "venc: registered as /dev/videoN"
不依赖 v4l2-ctl 的自检客户端(仓库根目录):

make v4l2_test
build/venc_v4l2_test -d /dev/videoN -w 320 -h 240 -n 30 -o /tmp/v.h264
板端一键验收(含格式/控件枚举、300 帧编解码、dmesg 检查):

sudo deploy/verify_v4l2.sh        # 期望 RESULT: PASS

8. 已知边界 / 后续
单实例、仅 VB2_MMAP、仅 NV12、仅 Baseline/CAVLC/单 slice/1 参考帧。
未做 V4L2_MEMORY_DMABUF(vb2_dma_contig 支持,补 get_dmabuf + alloc_devs 即可零拷贝接采集管线)。
码控是「每帧固定预算 + 积分修正」,无 VBV,因此不暴露 BITRATE_MODE。
无 G/S_SELECTION(裁剪)、无 V4L2_EVENT_SOURCE_CHANGE。
对齐后宽度 < 208 的帧被引擎拒绝(0x80008260),驱动不做 padding 规避。
vb2_buffer_done() 未持 qlock(§3.2 的死锁权衡);若将来引入多队列锁, 需要重新审视。
DMA 池里的 off_input 段是零拷贝改造后的死区(每帧白占 ~3 MiB @1080p), 删掉会改变所有段偏移,留待有回归测试时再动。
与 venc_cli --backend native 共享同一块 VE,没有跨路径互斥:同一时刻只应有 一个消费者。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

联系站长|手机版|野火电子官网|野火淘宝店铺|野火电子论坛 ( 粤ICP备14069197号 ) 大学生ARM嵌入式2群

GMT+8, 2026-11-2 17:39 , Processed in 0.254489 second(s), 26 queries , Gzip On.

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表