简介:《全志v3s音视频开发.pdf》是一份面向具备一定Linux与嵌入式基础的研发人员的实战型技术文档,围绕全志V3S平台,系统梳理从交叉编译环境、U-boot与Linux内核配置,到Buildroot根文件系统制作的完整开发链路。文档进一步展开2.4寸TFT-LED屏幕、按键、声卡与摄像头的驱动配置,并讲解SDL2、QT5及图形库、音频视频支持的移植方法。价值上,读者可跟随章节完成音乐播放器、图片显示、视频播放器和摄像头模块的QT5界面设计与实现,理解FFmpeg音视频解码处理及外设联调思路,适合按顺序学习并逐步搭建可运行的嵌入式音视频原型。资源包为1个PDF文件,大小约12.26MB,结构完整、便于检索;目前已有329人学习,可作为Linux移植与C++ Qt音视频开发的参考手册。
1. 一块小板子扛起音视频:V3S 这套方案到底解决了什么
全志 V3S 音视频开发这条链路上,最容易卡住的从来不是写代码,而是环境——工具链版本、设备树节点、根文件系统三者对不上,板子就停在串口那一行日志里不动。这份文档做的事情很具体:把 V3S 从 Linaro 6.5 交叉编译器、U-Boot 网络改造、Linux 5.4.31 内核裁剪,一路推到 Buildroot 根文件系统,再落到 TFT 屏、按键、声卡、摄像头四类外设,最后接到 SDL2 和 Qt5 的音视频界面上。它不是知识点罗列,是一条能照着敲完的施工顺序。
适合谁看?手上有 V3S 核心板、想把「能启动」推进到「能出声出画」的人;也适合应用层写得挺熟、但一碰设备树和根文件系统就发怵的开发者。V3S 片内集成 DDR、成本低、接口够用,做一台带屏的离线播放器或者摄像头终端,硬件门槛很低,真正的门槛全在软件栈的拼装顺序上。
2. 三段式搭建:工具链、U-Boot 与内核的落地顺序
2.1 先把目录和源码包摆正
接手一块新板子,我做的第一件事不是敲命令,而是把目录切干净。V3S 这套链路里「源码在哪、编译器在哪、产物去哪」三件事一旦混着放,后面反复改内核和设备树会非常难受。文档里的做法是建一个工作目录,下面三个平级子目录:driver放自己写的应用(音频播放器、摄像头 demo),tools放 U-Boot、交叉编译器、Buildroot 以及后面要单独交叉编译的 alsa-lib/alsa-utils,kernel只放内核源码。三个目录职责互不重叠,出问题时能立刻判断是「编译器不对」还是「内核配置不对」。
# 工作根目录,路径随个人习惯,后面所有路径都基于它 mkdir -p ~/qz/driver mkdir -p ~/qz/tools mkdir -p ~/qz/kernel # 把下载好的源码包统一丢进 tools 或 kernel cd ~/qz/tools tar -xvf gcc-linaro-6.5.0-2018.12-x86_64_arm-linux-gnueabihf.tar.xz tar -xvf buildroot-2021.02.1.tar.xz unzip u-boot-v3s-current.zip # 内核单独放,避免和 tools 下的东西混在一起 tar -xvf linux-5.4.31.tar.gz -C ~/qz/kernel-C指定解压目标目录,省得解压完再 mv 一遍。这一步看着琐碎,但后面写CROSS_COMPILE、写 Buildroot 的外部工具链路径、写 Qt 的 mkspecs,全都要引用绝对路径,目录定得越早越省事。
整套源码包的角色分工,最好一开始就心里有数:
| 文件 | 作用 | 建议位置 |
|---|---|---|
| gcc-linaro-6.5.0-...-arm-linux-gnueabihf.tar.xz | 交叉编译器,全流程共用 | qz/tools |
| u-boot-v3s-current.zip | 引导加载,负责把内核拉起来 | qz/tools |
| linux-5.4.31.tar.gz | 内核源码,含驱动与设备树 | qz/kernel |
| buildroot-2021.02.1.tar.xz | 生成根文件系统 | qz/tools |
| alsa-lib-1.2.2 / alsa-utils-1.2.2 | 音频库与 aplay/amixer 工具 | qz/tools |
| qt-opensource-linux-x64-5.14.2.run | 主机侧 Qt,主要用来取 mkspecs 和 qmake 参考 | qz/tools |
这里有个容易误解的点:qt-opensource-linux-x64-5.14.2.run是 x86_64 主机版,装出来的 qmake 生成的是主机程序,不能直接跑在板子上。它的价值在于提供完整的 mkspecs 目录结构和 configure 选项参考,真正上板的那份 Qt 必须用交叉编译器重新编一遍。把主机版当交叉版用,是新手最常见的翻车点之一。
2.2 交叉编译器:Linaro 6.5 的安装与 PATH 落地
交叉编译器的本质是「在 x86_64 上跑,产出 ARM 机器码」。选 Linaro 6.5-2018.12 的arm-linux-gnueabihf这套,是因为它和内核 5.4.31、Buildroot 2021.02.1 的 glibc 版本能对上;换更新的 GCC 也能编,但 glibc 版本一高,根文件系统里的动态库就得跟着换,反而增加变量。这套工具链的 ABI 是硬浮点(hf 后缀),后面 Buildroot 里 Target ABI 必须选 EABIhf,浮点策略选 VFPv3,三处必须一致,否则编出来的程序一上板就报Illegal instruction或者No such file or directory(其实是动态链接器路径不对)。
解压完先拿到 bin 目录的绝对路径,这一步别凭记忆写:
cd ~/qz/tools/gcc-linaro-6.5.0-2018.12-x86_64_arm-linux-gnueabihf/bin pwd # 输出类似 /home/<user>/qz/tools/gcc-linaro-6.5.0-2018.12-x86_64_arm-linux-gnueabihf/bin把pwd的结果写进 shell 配置文件,而不是每次开终端手动 export:
vim ~/.bashrc # 在文件末尾追加,路径换成上一步 pwd 的真实输出 export PATH=$PATH:/home/<user>/qz/tools/gcc-linaro-6.5.0-2018.12-x86_64_arm-linux-gnueabihf/bin source ~/.bashrc验证是否生效,看两件事:编译器能不能找到,目标架构对不对。
arm-linux-gnueabihf-gcc -v # 关注输出里的 Target: arm-linux-gnueabihf 和 gcc version 6.5.0 # 编一个最小程序确认能产出 ARM 可执行文件 echo 'int main(void){return 0;}' > /tmp/t.c arm-linux-gnueabihf-gcc /tmp/t.c -o /tmp/t file /tmp/t # 期望看到 ELF 32-bit LSB executable, ARM, EABI5file的输出是判断工具链有没有真正生效的最快方式。如果显示的是 x86-64,说明PATH里排在前面的还是主机 gcc,或者你敲的是gcc而不是带前缀的arm-linux-gnueabihf-gcc。交叉编译时所有命令都必须带这个前缀,包括gcc、ld、objcopy、strip,少一个就会混进主机产物,链接阶段才报错,排查起来很浪费时间。
另外提醒一句:CROSS_COMPILE这个变量的值末尾一定要带短横线,写成arm-linux-gnueabihf-,因为 Makefile 是拿它直接拼$(CROSS_COMPILE)gcc的。漏掉横线,报的错是arm-linux-gnueabihfgcc: not found,看着像路径问题,其实是拼写问题。
2.3 U-Boot:从 defconfig 到能 ping 通
U-Boot 阶段的目标不是「能启动」,而是「网口能用」。原因很现实:后面内核和设备树要反复改,如果每次都拔卡烧录,一天下来光插拔就够呛。U-Boot 里把网络打通,就能用 tftp 从主机直接拉 uImage 和 dtb,改一次内核只要重启一次。
先出默认配置,再动设备树:
cd ~/qz/tools/u-boot-v3s-current make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- LicheePi_Zero_defconfigARCH=arm决定走arch/arm那套构建规则,CROSS_COMPILE决定用哪套工具链,LicheePi_Zero_defconfig是板级默认配置,会生成.config。终端打印configuration written to .config就说明这一步过了,没过通常是 defconfig 名字打错或者源码目录不完整。
默认配置里网口是关着的,得手动补两处。第一处是板级 dts,把 emac 节点打开并指定 PHY:
/ { aliases { serial0 = &uart0; ethernet0 = &emac; /* 让 U-Boot 的网络设备能按别名找到控制器 */ }; }; &emac { phy = <&phy0>; phy-mode = "mii"; allwinner,use-internal-phy; /* V3S 内置 PHY,不加这句驱动会去外面找 */ allwinner,leds-active-low; status = "okay"; phy0: ethernet-phy@0 { reg = <1>; /* PHY 地址,和硬件 strapping 一致 */ }; };第二处是 SoC 级 dtsi,在soc {}里补 pinctrl 和控制器节点。这一步不能省,因为板级 dts 只是「引用」,真正的寄存器地址、中断号、时钟都在 dtsi 里定义。文档给的 emac 节点里compatible = "allwinner,sun8i-h3-emac"、reg = <0x01c30000 0x104>、中断<GIC_SPI 82 ...>,这几个值照抄即可,改错一个就是「网口灯亮但不通」。
引脚复用那组emac_rgmii_pins列了 PD0 到 PD17 的一串引脚,作用是告诉 pinctrl 把这些脚切到 EMAC 功能。MII 模式实际用不到这么多,但全列上不会出错,少列反而可能漏掉 RX/TX 的某一位导致丢包。
节点补完,进 menuconfig 打开驱动:
make ARCH=arm menuconfig # Device Drivers ---> # [*] Network device support ---> # [*] Ethernet driver support ---> # <*> Allwinner sun8i GMAC support然后编译。U-Boot 的编译产物里,真正要烧的是u-boot-sunxi-with-spl.bin——它把 SPL、U-Boot 主体和设备树打包在一起,BROM 从 SD 卡的固定偏移读它。
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- # 烧录前务必用 lsblk 确认设备名,写错盘会直接毁掉主机系统 lsblk sudo dd if=u-boot-sunxi-with-spl.bin of=/dev/sdX bs=1024 seek=8 syncseek=8配合bs=1024就是跳过前 8KB。这个偏移是 sunxi 平台 BROM 的约定,SPL 必须落在第 8KB 开始的位置,写成seek=8 bs=512就变成 4KB,板子直接不启动。sync别省,拔卡前不刷缓存,烧进去的内容可能只写了一半。
上电进 U-Boot 命令行,配好网络再 ping:
=> setenv ipaddr 192.168.10.202 # 板子自己的地址 => setenv gatewayip 192.168.10.1 # 网关 => setenv serverip 192.168.10.201 # 主机(tftp 服务器)地址 => setenv netmask 255.255.255.0 => saveenv # 写进 MMC,下次上电还在 => ping 192.168.10.201 Using ethernet@1c30000 device host 192.168.10.201 is alive看到is alive才算真的通了。这一步失败,九成不是 U-Boot 的问题,而是主机防火墙挡了 ICMP,或者板子和主机不在同一网段。我一般先把主机ping板子反过来试一次,双向都不通就先查网线和 IP,别急着回去改设备树。
2.4 内核:sunxi_defconfig 之上补 NFS 与 GMAC
内核这一段的思路和 U-Boot 一样:先拿厂商/社区默认配置当底,再补自己需要的那几项。V3S 用sunxi_defconfig,它已经包含了大部分片上外设,缺的是网络驱动细节和 NFS。
先改 Makefile 里的架构和编译器前缀,这样后面敲make就不用每次带一长串参数:
# linux-5.4.31/Makefile 顶部 ARCH ?= arm CROSS_COMPILE ?= arm-linux-gnueabihf-用?=而不是=,是为了保留命令行覆盖的能力,后面交叉编译外部模块时还能临时改。
cd ~/qz/kernel/linux-5.4.31 make sunxi_defconfig make ARCH=arm menuconfigmenuconfig 里要动的就三块,路径记不住没关系,用/搜关键词更快:
Device Drivers -> Network device support -> Ethernet driver support下打开STMMAC Platform bus support、Generic driver for DWMAC、Allwinner GMAC support、Allwinner sun8i GMAC support。V3S 的 MAC 是 dwmac 派生的,只开 Allwinner 那一项而不开 STMMAC 平台总线,驱动根本注册不上。Networking support -> Networking options下打开NFS client support以及 v2/v3/v4,最后勾上Root file system on NFS。这几个是给调试期用的:根文件系统直接从主机 NFS 挂,改一个文件不用重新打包镜像。- 顺带确认
Enable MMU之类的选项由 defconfig 保证,不用手动碰。
设备树这边比 U-Boot 多一层:V3S 的 PHY 是内置的,但内核的 mdio 需要经过一个 mux 才能访问到内部 PHY,所以节点结构比 U-Boot 里复杂。
/* sun8i-v3s-licheepi-zero.dts 末尾 */ &emac { phy-handle = <&int_mii_phy>; /* 指向内部 PHY,不是外部 phy0 */ phy-mode = "mii"; allwinner,leds-active-low; status = "okay"; };/* sun8i-v3s.dtsi 的 soc {} 内 */ emac: ethernet@1c30000 { compatible = "allwinner,sun8i-v3s-emac"; syscon = <&syscon>; reg = <0x01c30000 0x10000>; interrupts = <GIC_SPI 82 IRQ_TYPE_LEVEL_HIGH>; interrupt-names = "macirq"; resets = <&ccu RST_BUS_EMAC>; reset-names = "stmmaceth"; clocks = <&ccu CLK_BUS_EMAC>; clock-names = "stmmaceth"; phy-handle = <&int_mii_phy>; phy-mode = "mii"; status = "disabled"; mdio: mdio { compatible = "snps,dwmac-mdio"; }; mdio_mux: mdio-mux { compatible = "allwinner,sun8i-h3-mdio-mux"; mdio-parent-bus = <&mdio>; internal_mdio: mdio@1 { compatible = "allwinner,sun8i-h3-mdio-internal"; reg = <1>; int_mii_phy: ethernet-phy@1 { compatible = "ethernet-phy-ieee802.3-c22"; reg = <1>; clocks = <&ccu CLK_BUS_EPHY>; resets = <&ccu RST_BUS_EPHY>; }; }; }; };mdio-mux这一层是关键。V3S 的 MDIO 总线既能接内部 PHY 也能接外部 PHY,同一时刻只能用一路,所以内核用 mux 做切换。少了这一层,phy-handle指向的节点不存在,网卡会以no phy found收场。reg = <1>在两个层级里的含义不同:mux 节点里是「第 1 路」,PHY 节点里是 PHY 地址,别混。
编译时LOADADDR要和 U-Boot 的加载地址对齐:
make ARCH=arm -j12 uImage LOADADDR=0x41000040 make ARCH=arm dtbs cp arch/arm/boot/uImage /tftpboot/ cp arch/arm/boot/dts/sun8i-v3s-licheepi-zero.dtb /tftpboot/-j12按主机核数调整,V3S 内核代码量不大,编译时间主要花在驱动上。LOADADDR=0x41000040是 uImage 头的加载地址,得和 U-Boot 里bootm用的地址、以及内核自身的TEXT_OFFSET对得上;三者错开,现象是 U-Boot 显示Starting kernel ...之后就没下文了,连 earlyprintk 都出不来,这种时候别怀疑串口线,先回去核对地址。
3. 根文件系统:Buildroot 里把外部工具链喂对
3.1 Target options:Cortex-A7 与 EABIhf 必须对齐
根文件系统这条线,Buildroot 的坑集中在「它自己也想编一套工具链」。默认行为是内部构建,从零编 gcc 和 glibc,一编就是两三个小时,而且版本未必和你在用的 Linaro 6.5 一致。既然交叉编译器已经装好了,就该让 Buildroot 复用它,这样内核、应用、根文件系统三边的 ABI 才完全一致。
前提是 Target options 得先配对。V3S 是 Cortex-A7 单核,ARMv7-A 指令集,硬浮点。这几项配错,编出来的库在板子上跑不动,而且报错信息往往很误导。
cd ~/qz/tools/buildroot-2021.02.1 make ARCH=arm menuconfigTarget options ---> Target Architecture (ARM (little endian)) Target Binary Format (ELF) Target Architecture Variant (cortex-A7) Target ABI (EABIhf) Floating point strategy (VFPv3) ARM instruction set (ARM)EABIhf的 hf 就是 hard float,和工具链前缀里的gnueabihf是同一个意思,这两处必须成对出现。如果这里选了EABI(软浮点),编出来的动态库调用约定和工具链不一致,程序一启动就Segmentation fault,而且 gdb 栈回溯也是乱的,很难定位到「是浮点 ABI 不匹配」。
VFPv3对应 Cortex-A7 的浮点单元。选VFPv4也能跑(A7 支持 v4),但没必要,反而让编译产物对老一点的核不兼容。ARM instruction set选ARM而不是Thumb2,是因为部分音视频库(尤其是带汇编优化的解码器)对 Thumb 模式支持不完整,选 ARM 更省事。
3.2 外部工具链的三处路径与版本号
Toolchain 这一页是整份配置里最容易写错的地方,因为它是「字符串匹配」,写错了不报错,只在编译后期炸。
Toolchain ---> Toolchain type (External toolchain) Toolchain (Custom toolchain) Toolchain origin (Pre-installed toolchain) (/home/<user>/qz/tools/gcc-linaro-6.5.0-2018.12-x86_64_arm-linux-gnueabihf) Toolchain path (arm-linux-gnueabihf) Toolchain prefix External toolchain gcc version (6.x) External toolchain kernel headers series (5.4.x) External toolchain C library (glibc/eglibc) [*] Toolchain has SSP support? [*] Toolchain has RPC support? [*] Toolchain has C++ support? [*] Enable MMU support四组值要逐字核对:
Toolchain path是工具链的根目录,不是bin目录。写成.../bin的话,Buildroot 会去.../bin/arm-linux-gnueabihf-gcc找编译器,路径变成.../bin/bin/...,直接失败。Toolchain prefix写arm-linux-gnueabihf,不带末尾横线。Buildroot 自己会拼横线,和内核 Makefile 的CROSS_COMPILE规则正好相反,这里特别容易搞混。gcc version选6.x,kernel headers series选5.4.x。这两个是给 Buildroot 做特性判断用的,选高了它会尝试用工具链不支持的特性。C++ support必须勾上,后面 Qt5 全靠它。SSP和RPC按工具链实际能力勾,Linaro 这版都支持。
有一个隐性依赖值得说清楚:kernel headers series要和工具链自带的头文件版本一致。Linaro 6.5-2018.12 配的头文件大概是 4.20 上下,如果这里硬选 5.4.x,Buildroot 会认为工具链支持某些新的 syscall,编出来的 glibc 调用时可能返回ENOSYS。稳妥做法是选比实际略低的版本,宁可保守。
3.3 出包、烧录、首启验证
配置存盘之后直接make。第一次会下载一堆包源码,网络不好的话建议先挂个本地缓存或者提前把 dl 目录填好,不然卡在Downloading上很久。
make -j12 # 产物在 output/images 下 ls output/images/ # rootfs.tar rootfs.ext2 rootfs.ext4 sdcard.img ...rootfs.tar是根文件系统的原始打包,sdcard.img是带分区表的整卡镜像(如果开了Board options里的 genimage 配置)。两种用法:
一种是整卡烧录,适合第一次上板:
sudo dd if=output/images/sdcard.img of=/dev/sdX bs=4M conv=fsyncconv=fsync会在结束时刷盘,比单独再敲一次sync更省心。bs=4M对 SD 卡来说比默认的 512 字节快一个数量级。
另一种是只更新根文件系统分区,适合调试期频繁改应用。把rootfs.ext4直接dd到对应的分区节点(比如/dev/sdX2),不动前面的 U-Boot 和内核区。这种方式要求你事先用fdisk看清楚分区布局,写错分区号会把内核冲掉。
首启验证按顺序看三件事:串口有没有打到Please press Enter to activate this console、ifconfig -a能不能看到eth0、cat /proc/cpuinfo里的 CPU part 是不是0xc07(Cortex-A7)。第三条最直接地验证了根文件系统是不是真的按 Cortex-A7 编的——如果这里显示的是别的型号,说明 Target options 选错了,后面所有应用都得重编。
4. 外设打通:屏幕、按键、声卡、摄像头的四条链路
4.1 2.4 寸 TFT:时序参数与 fb0
屏幕是音视频项目里第一个必须拿下的外设,因为它决定了你能不能看到后面所有东西。V3S 的显示走 DE(Display Engine),内核里对应的是sun4i-drm或者更早的sun4ifb,最终呈现为/dev/fb0。
真正的难点不在驱动,在时序。2.4 寸 TFT 常见的驱动 IC 是 ILI9341 一类,需要提供clock-frequency、hactive、vactive、hfront-porch、hback-porch、hsync-len、vfront-porch、vback-porch、vsync-len这一整套参数。这些值必须从屏的规格书里抄,抄错一个的表现是「背光亮但花屏」或者「图像整体偏移」。
常见做法是在板级 dts 里加一个 panel 节点,用panel-dpi这类通用绑定:
panel: panel { compatible = "panel-dpi"; backlight = <&backlight>; power-supply = <®_vcc3v3>; port { panel_in: endpoint { remote-endpoint = <&tcon0_out>; }; }; };然后在panel-timing子节点里填时序。这一块没有通用值,只能按屏手册改。我一般先把clock-frequency按手册给的典型值填,把前后肩和同步宽度也照抄,跑起来之后再微调——有些屏的hback-porch要加几个像素才能让图像居中,属于玄学范畴,调不出来就换个刷新率试。
验证方式很土但有效:往/dev/fb0里写满一屏纯色,看是不是整屏变色。
# 屏幕分辨率 320x240,32bpp,一帧约 300KB head -c 307200 /dev/urandom > /dev/fb0 # 随机噪点,能看出有没有花屏 dd if=/dev/zero of=/dev/fb0 bs=307200 count=1 # 全黑,能看出有没有残留/dev/urandom那一步如果出现规律的斜纹或者色块错位,基本可以断定时序不对;如果整屏干净,只是颜色偏,那是 RGB 通道顺序或者bpp的问题。
4.2 按键:input 子系统与 evtest
按键看起来最简单,实际上很容易被忽略成「板载几个键,接上就行」。V3S 的 GPIO 按键走的是gpio-keys驱动,最终在/dev/input/eventX上产生标准 input 事件。把这层配好,应用层就完全不用碰 GPIO 寄存器。
gpio-keys { compatible = "gpio-keys"; autorepeat; key-enter { label = "enter"; gpios = <&pio 4 14 GPIO_ACTIVE_LOW>; /* PE14,bank 4 = PE */ linux,code = <KEY_ENTER>; }; key-next { label = "next"; gpios = <&pio 4 15 GPIO_ACTIVE_LOW>; /* PE15 */ linux,code = <KEY_NEXTSONG>; }; };&pio 4 14里的4是 GPIO bank 编号,PA=0、PB=1、PC=2、PD=3、PE=4,14是 bank 内的序号。这两个数抄错,按键要么完全无响应,要么触发的是别的键。GPIO_ACTIVE_LOW表示按下时是低电平,绝大多数开发板的按键都是这么接的,但如果你自己画的板子上拉,就得改成GPIO_ACTIVE_HIGH。
linux,code用标准键值(KEY_ENTER、KEY_NEXTSONG这些),好处是上层 Qt 或者 SDL 能直接识别成「确认」「下一首」,不用自己定义私有编码。设成私有值的话,应用里还得做一层映射,多此一举。
验证用evtest,Buildroot 里把alsa-utils和evtest一起选上就行:
evtest /dev/input/event0 # 按下按键,看到类似: # Event: time 1234.567, type 1 (EV_KEY), code 28 (KEY_ENTER), value 1只看到value 1没有value 0,说明按键松开没被识别,通常是没加上拉电阻或者 dts 里的极性反了。这种情况在应用里表现为「按一次触发好几次」,是血泪经验。
4.3 声卡:ALSA 库交叉编译与 aplay 验证
音频这条链路分两层:内核里的 codec 驱动,和用户态的 alsa-lib / alsa-utils。V3S 片内有 audio codec,dts 里对应的节点打开、配上simple-audio-card之后,/proc/asound/cards里应该能看到声卡。
Buildroot 里可以直接勾alsa-lib和alsa-utils,但如果版本不满足需求(比如要 1.2.2 这个特定版本),就得手动交叉编译。手动编的步骤和普通 autotools 项目一样,关键是--host:
cd ~/qz/tools/alsa-lib-1.2.2 ./configure \ --host=arm-linux-gnueabihf \ --prefix=/usr \ --enable-shared \ --disable-python \ --with-alsa-devdir=/dev/snd make -j12 make install DESTDIR=$PWD/../alsa-stage--host告诉 configure 这是交叉编译,它会去找arm-linux-gnueabihf-gcc;--prefix=/usr是目标板上的安装路径,不是主机的,DESTDIR才是主机上的暂存目录。这两个路径搞混,编完的东西会装到主机的/usr里去,很麻烦。
--disable-python省掉 python 绑定,板子上用不到,编它纯属浪费。--with-alsa-devdir=/dev/snd指定设备节点目录,默认就是这个值,写上只是明确一下。
alsa-utils 依赖 alsa-lib,configure 时要指过去:
cd ~/qz/tools/alsa-utils-1.2.2 ./configure \ --host=arm-linux-gnueabihf \ --prefix=/usr \ --with-alsa-inc-prefix=$PWD/../alsa-stage/usr/include \ --with-alsa-prefix=$PWD/../alsa-stage/usr/lib make -j12--with-alsa-inc-prefix和--with-alsa-prefix分别指向 alsa-lib 的头文件和库,不指的话 configure 会去找主机的 alsa-lib,链接出来的aplay是 x86 版本,拷到板子上直接Exec format error。
上板后先看设备,再放声音:
cat /proc/asound/cards # 应该有一张 card 0 aplay -l # 列出 playback 设备 amixer sset 'Headphone' 80% # 音量,控件名按实际 codec 改 aplay -D hw:0,0 /tmp/test.wav # 直接走硬件,绕开 dmix 插件直接用hw:0,0而不是default,是为了排除 dmix、pulseaudio 这些中间层的干扰。如果hw:0,0能出声而default不出,问题在 asound.conf,不在驱动。
4.4 摄像头:V4L2 取流的最小闭环
摄像头是这套方案里最「吃带宽」的部分。V3S 的 CSI 接口配合 OV 系列 sensor,走 V4L2 框架,应用层拿到的是/dev/video0。验证驱动有没有起来,用v4l2-ctl最快:
v4l2-ctl --list-devices v4l2-ctl -d /dev/video0 --list-formats-ext--list-formats-ext会列出支持的分辨率和像素格式。如果只列出YUYV没有MJPEG,说明 sensor 的驱动裁剪过,抓高分辨率时会因为带宽不够掉帧,这时候要么降分辨率,要么在应用层做 JPEG 解码。
一个最小的取帧循环,用 mmap 方式,代码不长但每一步都不能少:
#include <linux/videodev2.h> #include <sys/ioctl.h> #include <sys/mman.h> #include <fcntl.h> #include <unistd.h> #include <string.h> #define WIDTH 640 #define HEIGHT 480 #define BUFCNT 4 int main(void) { int fd = open("/dev/video0", O_RDWR); if (fd < 0) return -1; /* 1. 设置格式:驱动会返回实际生效的宽高和 stride */ struct v4l2_format fmt = {0}; fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; fmt.fmt.pix.width = WIDTH; fmt.fmt.pix.height = HEIGHT; fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV; fmt.fmt.pix.field = V4L2_FIELD_NONE; ioctl(fd, VIDIOC_S_FMT, &fmt); /* 2. 申请缓冲区:用 MMAP 避免每帧 memcpy */ struct v4l2_requestbuffers req = {0}; req.count = BUFCNT; req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; req.memory = V4L2_MEMORY_MMAP; ioctl(fd, VIDIOC_REQBUFS, &req); void *buf[BUFCNT]; for (int i = 0; i < BUFCNT; i++) { struct v4l2_buffer b = {0}; b.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; b.memory = V4L2_MEMORY_MMAP; b.index = i; ioctl(fd, VIDIOC_QUERYBUF, &b); buf[i] = mmap(NULL, b.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, b.m.offset); ioctl(fd, VIDIOC_QBUF, &b); /* 先入队,再开流 */ } /* 3. 开流,进入循环取帧 */ enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE; ioctl(fd, VIDIOC_STREAMON, &type); for (int n = 0; n < 100; n++) { struct v4l2_buffer b = {0}; b.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; b.memory = V4L2_MEMORY_MMAP; ioctl(fd, VIDIOC_DQBUF, &b); /* 阻塞直到一帧就绪 */ /* buf[b.index] 里就是这一帧,fmt.fmt.pix.bytesperline 是行宽 */ ioctl(fd, VIDIOC_QBUF, &b); /* 用完立刻还给驱动 */ } ioctl(fd, VIDIOC_STREAMOFF, &type); close(fd); return 0; }三个地方最容易翻车。一是VIDIOC_S_FMT之后必须回读fmt,驱动的bytesperline可能不等于width * 2(有对齐填充),按width * 2算地址会得到斜着的图像。二是缓冲区必须先全部 QBUF 再 STREAMON,顺序反了DQBUF会一直阻塞。三是DQBUF和QBUF要成对,忘了还缓冲区,循环跑到第四帧就卡死——缓冲区总共就 4 个。
编译用交叉工具链,链接时不需要额外的库,V4L2 全是 ioctl:
arm-linux-gnueabihf-gcc v4l2_demo.c -o v4l2_demo5. 避坑排查:黑屏、无声、网口不通的现场记录
5.1 启动与网络链路的三个坑
坑一:dd烧完 U-Boot,串口一点输出都没有。
现象是上电后串口助手完全静默,连 BROM 的乱码都没有。原因通常是seek偏移写错,或者烧到了错误的分区节点——很多人把of=写成/dev/sdX1而不是整盘/dev/sdX,SPL 落在了分区里,BROM 按固定偏移去读自然读不到。解决办法是先lsblk看清整盘和分区的区别,of=一律用整盘设备名,seek=8 bs=1024两个参数一起核对。烧完sync再拔卡,别图快。
坑二:U-Boot 里ping不通,但网口灯是亮的。
灯亮只说明 PHY 完成了自协商,不代表数据通路通了。常见原因有三个:allwinner,use-internal-phy这句没加,驱动去外部总线找 PHY;PHY 地址reg和硬件 strapping 不一致;主机侧防火墙挡了 ICMP。排查顺序是从近到远——先在 U-Boot 里mii info看能不能读到 PHY 寄存器,读不到就是设备树问题;能读到但 ping 不通,再查 IP 和防火墙。我一般还会在主机上tcpdump -i any icmp抓一下,看请求到底有没有到主机。
坑三:内核启动到Starting kernel ...就停住。
这个现象几乎全部指向LOADADDR或设备树地址不匹配。uImage 的加载地址、U-Boot 的bootm地址、内核编译时的TEXT_OFFSET,三者必须能对上。还有一个容易被忽略的原因:dtb 和内核版本不匹配,比如用旧 dtb 配新内核,earlycon初始化时读不到/chosen节点就静默死掉。解决办法是把uImage和sun8i-v3s-licheepi-zero.dtb一起重新编译、一起拷贝,别只更新其中一个。串口参数也要确认是115200n8,波特率错了看到的全是乱码,容易误判成内核挂了。
5.2 显示与音频链路的两个坑
坑四:屏幕背光亮,但全是白屏或者规律花屏。
背光亮说明供电和 backlight 没问题,问题在时序或者像素格式。规律花屏(比如每隔几行错位)基本是bytesperline和实际 stride 不匹配;整屏偏移或者边缘有黑边,是hback-porch/vback-porch差了几个像素。做法是拿屏的规格书逐项对照panel-timing,特别是clock-frequency——这个值差 5% 就可能出不来图。还有一个隐蔽原因:dts 里 panel 的remote-endpoint没指到tcon0_out,显示管线没连上,内核日志里会有一句failed to bind之类的报错,用dmesg | grep -i drm能翻出来。
坑五:aplay不报错但没声音。
先分清是「没声音」还是「声音极小」。amixer把Headphone和Playback两个控件都拉满试试,V3S 的 codec 有好几级增益,只调一个往往听不见。如果拉满还是没声,用aplay -D hw:0,0绕开default设备,能出声说明是 asound.conf 里的 dmix 配置有问题;还不出声就cat /proc/asound/card0/pcm0p/sub0/hw_params看硬件参数有没有真的生效——有时候应用请求 44100Hz 但 codec 只支持 48000Hz,ALSA 会静默降级或者干脆不出声。另一个常见原因是耳机检测脚没配,驱动认为耳机没插入,把输出路由到了不存在的扬声器上。
6. 二次开发:Qt5 音视频界面在板子上的改造切入点
把这套环境跑通之后,真正的工作才开始:界面怎么改、播放器怎么接。Qt5 上板的关键不在写界面,而在交叉编译那份 Qt 本身,以及选对多媒体后端。
交叉编译 Qt 的核心是 mkspecs。主机版qt-opensource-linux-x64-5.14.2装好之后,把mkspecs/linux-arm-gnueabi-g++拷到交叉版源码里,改qmake.conf的三个变量:
# qtbase/mkspecs/linux-arm-gnueabi-g++/qmake.conf MAKEFILE_GENERATOR = UNIX QMAKE_CC = arm-linux-gnueabihf-gcc QMAKE_CXX = arm-linux-gnueabihf-g++ QMAKE_LINK = arm-linux-gnueabihf-g++ QMAKE_AR = arm-linux-gnueabihf-ar cqs QMAKE_STRIP = arm-linux-gnueabihf-stripconfigure 的时候按板子能力裁剪,V3S 这种单核 A7 带不动 OpenGL,直接关掉:
./configure -release -opensource -confirm-license \ -prefix /usr \ -xplatform linux-arm-gnueabi-g++ \ -no-opengl -no-eglfs \ -qt-zlib -qt-libpng -qt-libjpeg \ -alsa -skip qtwebengine -skip qt3d-no-opengl省掉一整套 EGL 依赖,-skip qtwebengine是必须的,那个模块在 64MB 内存的板子上根本编不过。-alsa打开音频后端,-qt-libjpeg之类的内置库避免再去交叉编译第三方依赖。
界面和播放器的分层,我建议这么切:
| 层 | 职责 | 改造自由度 |
|---|---|---|
| QWidget 主框架 | 页面切换、按键事件分发 | 高,纯 UI 代码 |
| 媒体后端 | 音乐/视频解码输出 | 中,受后端能力限制 |
| 硬件抽象 | fb0、/dev/video0、ALSA 设备 | 低,跟着驱动走 |
音视频播放这块有个取舍:Qt5 自带的QMediaPlayer在嵌入式上依赖 gstreamer,交叉编译 gstreamer 及其插件链的工作量比写播放器本身还大。更实际的做法是自己用 FFmpeg 解码,把 YUV 数据直接写/dev/fb0或者转成QImage交给QLabel,音频部分用 ALSA 的snd_pcm_writei推帧。这样整条链路的每一环都在自己手里,掉帧了、音画不同步了,能立刻定位是解码慢还是写屏慢。
验证方法很简单:先不管界面,写一个只有main的控制台程序,解码一个 10 秒的测试视频,把每帧的 PTS 和实际写屏时间打出来。如果平均耗时超过 33ms,说明 640x480 的软解在 V3S 上跑不满 30fps,得降分辨率或者在 dts 里开硬件解码节点。这一步不做,等界面做完再发现卡,改起来就是伤筋动骨。
我踩过最深的一次坑,是花了一整天调 Qt 界面闪烁,最后发现是/dev/fb0的双缓冲没开,每帧都在直接改显存。从那以后我每次接新板子,都强制先跑一遍「纯色刷屏 + 帧率打印」的最小验证,再往上堆 UI。希望帮到你。
本文还有配套的精品资源,点击获取