简介:面向具备Linux与嵌入式基础的研发人员,这份PDF围绕全志V3S芯片,完整记录了嵌入式音视频开发从环境搭建到模块落地的全过程。文档以linux-5.4.31与buildroot-2021.02.1为例,先后讲解交叉编译器配置、U-boot设备树调整、内核编译和根文件系统制作,并逐步驱动2.4寸TFT屏幕、按键、声卡与摄像头,所有关键命令和配置点均按操作顺序展开,便于直接跟随验证。资源包为1个PDF文档,大小约12.26MB,内容组织紧凑,适合边查边做。当前已有326人学习。后半部分聚焦SDL2与QT5的移植,以及基于QT5的音乐播放器、图片显示、视频播放和摄像头模块设计,同时引入FFmpeg完成音视频解码,为后续GUI与多媒体功能的扩展提供了可复用的思路和排错依据。

1. 全志v3s音视频开发:先把芯片能力与常见误区对齐

如果你手里拿着《全志v3s音视频开发.pdf》这份资料,说明你已经跨过了“板子能不能开机”这个阶段,接下来是让摄像头画面变成H.264码流、让声音变成AAC,最后推送到RTMP或RTSP服务器。全志V3s是一颗很特别的SoC:CPU是单核ARM Cortex-A7,最高1.2GHz,内置64MB DDR2,片上带硬件H.264编码器,但内存控制器带宽不高。很多人从树莓派或桌面Linux转过来,第一反应是编译一个完整FFmpeg,然后 -c:v libx264 直接软编。这个思路在720p@30下会把CPU跑满,帧率掉到十几帧。真正把V3s音视频开发做起来,要先把数据通路理顺:sensor通过8位DVP进CSI,CSI输出NV12给V4L2;音频走I2S进内置codec;之后视频进全志VE硬件编码器或者软编,音频交给AAC编码器,最后一起封装推流。下面按环境、采集、编码、同步四步展开,每步都给可复现的命令和参数。

2. 搭建全志v3s音视频开发环境:工具链、内核与SD卡启动

2.1 Buildroot构建工具链与根文件系统

V3s没有一个官方统一的SDK下载入口,很多开发板厂商给的BSP也版本混乱。我一般不用全志自带工具链直接编rootfs,而是用Buildroot生成arm-linux-gnueabihf交叉工具链、根文件系统和小尺寸镜像。Buildroot的好处是能把工具链、内核头文件、libc版本锁定在一起,避免Linaro工具链和BSP内核版本错位。

clone https://gitlab.com/buildroot.org/buildroot.git
cd buildroot
make menuconfig

menuconfig里做三处关键选择:Target options中把Target Architecture设为ARM,Target Architecture Variant设为Cortex-A7,Target ABI设为EABIhf;Filesystem images中勾选ext2/3/4 root filesystem和tarball;Toolchain类型选择Buildroot toolchain,C library用glibc,调试阶段报错信息更完整。选完执行 make -j$(nproc) ,把 output/images/rootfs.ext4 和 zImage 留下备用。

这套Buildroot配置只决定系统能跑,音视频相关模块必须在Linux内核配置里单独打开。V3s的CSI驱动在Device Drivers → Media support → V4L platform devices下面,选项名是 SUN6I_CSI ;音频控制器在 SUN8I_CODEC 。用make menuconfig勾上,或者直接写进defconfig:

CONFIG_VIDEO_SUN6I_CSI=y
CONFIG_SND_SUN8I_CODEC=y
CONFIG_SND_SUN8I_I2S=y

注意 =y 和 =m 的区别。rootfs是ext4,驱动可以模块化加载,但V3s开发初期强烈建议直接编进内核,否则 dmesg 里会出现固件找不到或fallback到用户空间加载的错误,排查起来多绕一圈。

2.2 内核与设备树里音视频相关节点的配置

V3s音视频开发最耗时间的不是写C语言逻辑,而是设备树没有把CSI、I2S、codec节点打开。烧进系统后先看 /sys/firmware/devicetree/base ,如果 csi 或 i2s0 的status属性是disabled,后面V4L2或ALSA必然报“cannot open device”。当你满怀信心执行 v4l2-ctl --list-devices ,看到的却只有 /dev/video0 不存在时,十有八九就是设备树状态没改对。

2.2.1 CSI摄像头节点配置

摄像头端我用OV5640最多,因为V3s硬件参考设计里就是这颗sensor。设备树要同时改i2c、csi和pinctrl三处:

&i2c0 {
    pinctrl-names = "default";
    pinctrl-0 = <&i2c0_pins>;
    status = "okay";

    ov5640: ov5640@3c {
        compatible = "ovti,ov5640";
        reg = <0x3c>;
        clocks = <&clks CLK_CSI_MCLK>;
        assigned-clocks = <&clks CLK_CSI_MCLK>;
        assigned-clock-rates = <24000000>;
        reset-gpios = <&pio 1 9 GPIO_ACTIVE_LOW>;
        pwdn-gpios = <&pio 1 7 GPIO_ACTIVE_LOW>;
        port {
            ov5640_ep: endpoint {
                remote-endpoint = <&csi_ep>;
                bus-width = <8>;
                hsync-active = <1>;
                vsync-active = <1>;
            };
        };
    };
};

&csi {
    pinctrl-names = "default";
    pinctrl-0 = <&csi_pins>;
    status = "okay";

    port {
        csi_ep: endpoint {
            remote-endpoint = <&ov5640_ep>;
            bus-width = <8>;
        };
    };
};

这段配置里最关键的是 assigned-clock-rates = <24000000> 。OV5640的MCLK来自CSI模块的24MHz时钟,如果不显式指定,内核可能把MCLK设到其他频率,sensor直接不出图。 bus-width = <8> 表示走8位DVP接口,V3s的CSI还支持MIPI-CSI,但低成本OV5640模组几乎都是DVP。 reset-gpios 和 pwdn-gpios 的引脚号随板子改,接反了会出现sensor上电后I2C地址探测不到。如果用的是mainline内核,clock节点要换成 &ccu CLK_CSI_MCLK ,属性名不变,写法相同。

2.2.2 I2S和音频codec节点

音频部分先打开I2S0。V3s内置一个音频codec,I2S0是数字侧接口,codec是模拟侧接口:

&i2s0 {
    pinctrl-names = "default";
    pinctrl-0 = <&i2s0_pins>;
    status = "okay";
};

&codec {
    status = "okay";
};

有些底板把音频codec挂在I2S1上,这时要改成 &i2s1 ,同时检查BCLK、LRCLK、DIN/DOUT三根线是否和被占用的UART引脚复用。音频出问题的排查顺序是:先 cat /proc/asound/cards 看有没有声卡,再 cat /proc/asound/card0/pcm0p/sub0/hw_params 看当前采样率和格式。这条路径能省掉很多怀疑驱动的无用功。

2.3 用全志v3s串口验证启动和驱动加载

调试阶段建议保留全志v3s串口,也就是UART0,BootROM固定从这里打印。烧SD卡时先分两个分区,第一个FAT分区放zImage和设备树,第二个分区放rootfs.ext4,然后用dd写rootfs:

sudo dd if=./rootfs.ext4 of=/dev/sdX bs=4M conv=fsync

串口参数如下表:

参数 取值 说明
波特率 115200 全志V系列BootROM固定
数据位 8 标准UART
停止位 1 标准UART
流控 无 调试口不启用RTS/CTS

接上USB转串口后执行 picocom -b 115200 /dev/ttyUSB0 ,系统启动后用 dmesg | grep -E "csi|i2s|codec|v4l2" 。看到 v4l2: registered 或 asoc-simple-card 相关信息,才代表设备树和驱动都生效。一个容易误判的坑:设备树填好但驱动没编进内核,日志里同样看不到 /dev/video0 。所以2.1节里的 CONFIG_VIDEO_SUN6I_CSI=y 必须和设备树节点同时满足,两个地方全部对齐,摄像头才能被 v4l2-ctl --list-devices 看到。

3. 用V4L2和ALSA把V3s的音视频原始数据采下来

3.1 V4L2采集YUV帧,并用v4l2-ctl确认像素格式

V3s的CSI控制器不感知图像内容,像素格式由sensor决定。先用v4l2-ctl把能力试探出来:

v4l2-ctl -d /dev/video0 --list-formats-ext
v4l2-ctl -d /dev/video0 --set-fmt-video=width=1280,height=720,pixelformat=NV12
v4l2-ctl -d /dev/video0 --stream-mmap --stream-count=30 --stream-to=/tmp/cap.yuv

--stream-mmap 表示用mmap方式申请缓冲区,这是V3s上最稳的路径。 --stream-to 把内存映射后的frame连续写文件,方便离线验证。拿到 /tmp/cap.yuv 后,用ffplay检查图像:

ffplay -f rawvideo -pixel_format nv12 -video_size 1280x720 /tmp/cap.yuv

如果画面是绿的或者花掉,优先看 --list-formats-ext 输出,确认sensor到底输出什么格式。OV5640原生输出YUYV,但CSI内部可以转成NV12;V3s的CEU支持NV12,建议直接用NV12,省掉编码器前面的格式转换。这里不要尝试让CSI输出RGB,V3s的CSI转为RGB的路径和编码器不直接兼容,多一次转换就多吃一份内存带宽。

3.2 ALSA采集PCM,并强制设置采样率

V3s内置codec的采样率上限通常到48kHz,但有的底板codec只支持44.1kHz。用arecord验证最直接:

arecord -D plughw:0,0 -f S16_LE -r 48000 -c 2 -d 10 /tmp/cap.wav
aplay -D plughw:0,0 /tmp/cap.wav

plughw:0,0 前面的 plug 表示允许ALSA做采样率转换。如果用 hw:0,0 ,应用必须把格式设置得和声卡硬件参数完全一致,否则返回参数错误。追求低延迟时要改成 hw ,并用 snd_pcm_hw_params_set_rate_near 去匹配实际采样率。声道数也要看原理图,很多V3s底板只接了单路MIC,你用 -c 2 录出来的右声道是空的,这不是驱动bug,而是硬件只有一路模拟输入。

3.3 双线程读取与缓存处理

命令行验证通过后,需要一个能长期跑的读取框架。我一般开两个线程,一个读视频,一个读音频,各自放进独立队列:

static void *capture_video(void *arg)
{
    struct v4l2_buffer buf;

    while (!stop) {
        memset(&buf, 0, sizeof(buf));
        buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;
        ioctl(fd, VIDIOC_DQBUF, &buf);
        copy_frame_to_queue(&video_queue, buffers[buf.index].start,
                            length[buf.index]);
        ioctl(fd, VIDIOC_QBUF, &buf);
    }
    return NULL;
}

VIDIOC_DQBUF 把驱动里已经填好的buffer取出来, VIDIOC_QBUF 把用完的buffer还回去。 copy_frame_to_queue 里必须是深拷贝,不能只保存指针。原因是V3s的CSI缓冲区数量通常只有4个,编码线程稍慢就会覆盖;深拷贝虽然多一次内存复制,但能让采集和编码解耦。V3s内存带宽有限,更合理的做法是维护一个3到4帧的队列,队列满就丢帧,而不是让编码线程无限等下去。

音视频采集参数建议按下面这张表:

参数 推荐值 说明
视频输入 YUYV→NV12 交给CSI转换,不要在ARM里用libyuv
视频分辨率 1280x720 再高编码压力大,内存也紧张
视频帧率 25 PAL/NTSC均可,V3s常见用法
音频采样率 48000 与AAC编码器对齐
音频格式 S16_LE V3s内置codec默认,不需要浮点PCM

4. 把原始音视频编码成H.264/AAC:软编与全志硬编怎么选

4.1 FFmpeg软编码:适合原型验证,不要指望720p@30

V3s的Cortex-A7在720p30下做libx264软编基本到了临界点。快速验证整条链路时,FFmpeg可以直接读取设备节点:

ffmpeg -f v4l2 -input_format nv12 -video_size 1280x720 -r 25 -i /dev/video0 \
       -f alsa -ar 48000 -ac 2 -i hw:0,0 \
       -c:v libx264 -preset ultrafast -tune zerolatency -b:v 1500k \
       -c:a aac -b:a 96k -f flv rtmp://192.168.1.100/live/v3s

-preset ultrafast 不是为了画质,而是让CPU少跑运动搜索。 -tune zerolatency 会关闭B帧并压低编码器缓冲,是低延迟推流的关键。 -b:v 1500k 对1280x720室内场景足够,户外运动场景要提到2000k。V3s上跑这条命令,CPU占用会到80%以上,音频线程不稳定,所以我只在原型验证用,不做量产方案。量产必须走硬件编码器。

4.2 全志VE硬编码:BSP里的编码库才是正路

全志V3s的Video Engine模块支持H.264编码,但mainline内核没有完整的编码驱动。最常见的做法是用全志BSP内核,配VE编码库。接口风格随BSP版本不同,流程固定:

void *enc = cedar_encode_open("h264");
cedar_encode_setconfig(enc, 1280, 720, 25, CEDAR_H264_HIGH);

while (frame_wait(&frame)) {
    if (cedar_encode_frame(enc, &frame) < 0) {
        frame_drop(&frame);
        continue;
    }
    while (cedar_get_packet(enc, &nalu) == 0) {
        write_rtsp_packet(nalu);
        cedar_put_packet(enc);
    }
}
cedar_encode_close(enc);

这里的 cedar_* 是占位风格API,实际头文件以BSP包内为准。要注意:别在mainline内核上找这个接口,V3s的VE编码在mainline里是不完整的。V3s硬编有个独特约束,输入buffer必须是物理连续内存。用普通malloc出来的buffer,编码器会报地址对齐错误。解决方法是V4L2申请MMAP buffer后,把fd用DMA_BUF导出,再映射成编码库需要的地址。常见做法是在Init阶段一次性申请,之后循环复用。

软编和硬编的取舍,直接决定你后面投入多少时间:

方向 软编 FFmpeg 硬编 VE
720p30 表现 周期性掉帧 可稳定30fps
编码延迟 80-150ms 30ms左右
依赖 FFmpeg BSP内核 + VE库
调试难度 低,日志多 高,库不开源
适用场景 原型、低分辨率 产品、低延迟

如果你目标是VGA(640x480),软编可以到30fps,不一定非要碰VE库;如果必须1080p,V3s的DDR2带宽会先卡死,CPU根本不是主要瓶颈。

4.3 用FFmpeg做MP4/FLV封装和RTSP/RTMP推流

编码拿到H.264和AAC裸流后,封装交给FFmpeg的muxer,不必自己写MP4 box。常见做法是把裸流写成文件再推出去:

ffmpeg -f h264 -i /tmp/video.h264 -f aac -i /tmp/audio.aac \
       -c:v copy -c:a copy -f flv rtmp://192.168.1.100/live/v3s

-c:v copy -c:a copy 表示不重新编码,直接封装。硬编码器输出的H.264通常不带PTS,所以4.2伪代码里专门用 write_rtsp_packet(nalu) 而不是直接写文件。真正接RTSP服务器时,建议开启 -use_wallclock_as_timestamps 1 ,让FFmpeg用墙上时钟生成时间戳;如果走RTSP TCP,再加上 -rtsp_transport tcp ,避免UDP丢包导致画面花块。

5. V3s音视频开发中的时间戳、丢帧与内存回收技巧

5.1 用单调时钟给编码帧打PTS

硬编码器不负责维护时间戳,它只告诉驱动“这一帧处理完了”。PTS必须由采集端生成,而且要保证视频PTS和音频PTS用同一个时钟源:

struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
uint64_t pts = (uint64_t)ts.tv_sec * 90000 + ts.tv_nsec / 11111;

90000是H.264和AAC通用的90kHz时间基。用CLOCK_MONOTONIC而不用gettimeofday,可以避免NTP校正或者人工改系统时间导致的时间跳变。音频侧ALSA的 snd_pcm_htimestamp 也要用同一时钟,否则推流后会出现音画不同步。

5.2 丢帧丢在采集端,不丢在编码端

V3s的VE编码队列通常只有几十帧深度。如果每个V4L2 buffer出来都塞给编码器,编码线程一旦跟不上,V4L2会卡在DQBUF上,导致采集线程阻塞。更好的策略是采集线程里检查视频队列长度,超过5帧时直接丢弃当前帧。丢帧丢在采集端,只是丢掉了那几毫秒的画面;丢在编码端,会造成H.264参考帧错乱,后面的帧全部花掉。

5.3 用内存分配和日志判断瓶颈

编码期间不要频繁malloc。V3s总共就64MB内存,rootfs和占用大的应用占掉一半后,编码buffer每帧NV12需要3MB,30帧轮转就是90MB,根本不够。所以buffer必须在编码线程启动前一次性分配,之后只做memcpy。最后用“三档验证”定位瓶颈:摄像头单独用v4l2-ctl连续采集2分钟,不丢帧说明CSI和DMA没问题;把采集到的YUV文件单独交给软编或硬编跑,不延时说明编码器正常;二者合跑才丢帧,说明DDR带宽或线程调度是瓶颈。VE缓冲区一直卡住时,看BSP提供的debug节点,或直接观察 /proc/interrupts 里编码器中断是否持续增长,这比猜代码省时间。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

Logo

邀请您加入社区

更多推荐