跳到主要内容

OpenixSuit全志开源烧写工具

· 阅读需 2 分钟

软件介绍

OpenixSuit是一款开源支持全志系列芯片的系统固件烧写工具,集成 全志Tina系统固件烧写,指定扇区位置烧写,社区版本通用固件烧写,固件解析解包,EFEL裸机程序测试运行,ADB文件浏览。

目前测试D1 T113 T153 T527 A133 A537 等都没问题。其他全志系列芯片需要自行测试研究。

image-20260304172909756

获取工具

参考如下软件下载链接,获取你的电脑系统对应的软件安装包。

WindowsLinuxmacOS (Apple Silicon)
image-20260304174628403image-20260304174638502image-20260304174650440

工具介绍

image-20260304191719575

烧写系统

FEL固件烧写

FEL固件主要是指全志Tina系统编译出来的固件镜像,比如Tina4 Tina5 longan 等全志原厂SDK 生成的镜像,原用 PhoenixSuit 所烧写的镜像文件。

如下图所示,烧写T113s3系统,参考如下步骤,但是需要确保你的电脑已经安装过全志的USB烧录驱动,安装驱动方式请参考网页:https://dshanpi.100ask.org/docs/T113s4-SdNand/part1/03-1_FlashSystem

image-20260306160452438

  • 支持的硬件
芯片名称镜像名称镜像描述
T113s3

通用固件烧写

通用固件指的是使用 社区主线Linux内核 编译出来的系统,一般有armbian ubuntu openwrt等,这里以 Avaota-A1 T527为例演示如何配置烧录。

高级用法

扇区固件烧写

EFEL工具箱

更多玩法

固件解析提取

image-20260306161953526

2026-03-06_162101

ADB文件浏览

image-20260306173148352

扩展更新

语言设置

支持中英文 两种语言

image-20260306173410539

版本更新

image-20260305112305639

OPENWRT系统1. A1主板介绍与开发环境搭建

· 阅读需 11 分钟
DShanPl-A1 Education专为人工智能教育及项目开发深度优化,基于瑞芯微的RK3576处理器设计,集成了4个Cortex-A72和4个Cortex-A53及支持NEON指令集,支持8K@30fps的H.265,VP9AVS2 和 AV1解码器,4k@60fps的H.264解码器和4K@60fps的AV1解码器;还支持4K@60fps的H.264和H.265编码器。内置3D GPU,能够完全兼容OpenGl ES1.1/2.0/3.2、0penCL2.0和Vulkan 1.1。内嵌的NPU算力高达6TopS,支持INT4/INT8/INT16/FP16混合运算。

板子有丰富的外设接口,板载SOC性能强劲,可以提供中高端性能 SBC(Single Board Computer-卡片电脑)体验,智能路由器等,下面是一些对应的应用场景示例:

  • 智能单机小电脑,具备办公,教育,编程开发,嵌入式开发等功能
  • 个人git仓库,服务器,nas,软路由,私有云
  • 机器人,无人机等项目
  • 电视机盒子,智能家居中枢,家庭安防监控,智能音箱等智能设备

OpenWrt介绍

OpenWrt 是一个基于 Linux 的开源嵌入式操作系统,主要用于路由器等网络设备。与传统路由器固件相比,OpenWrt 不是固定功能的固件,而是可自由扩展的软件平台,用户可以通过 opkg 软件包系统安装各种组件,实现路由、防火墙、VPN、NAS、内网穿透等多种功能。它提供 SSH 命令行与 LuCI Web 界面,配置灵活,支持 VLAN、IPv6、QoS、多 WAN 等高级网络特性。OpenWrt 结构清晰、模块化,核心包括 UCI 配置系统、netifd 网络管理、dnsmasq、hostapd 和防火墙框架等。凭借高度可定制性和强大的社区支持,OpenWrt 适用于家庭、企业网络以及二次开发,是打造高功能路由器和网络应用平台的理想选择。 Lean 的 OpenWrt LEDE 仓库是一个由 Lean 所维护的开源项目,旨在为 OpenWrt 系统提供稳定、高效且功能丰富的支持。作为 OpenWrt 和 LEDE 项目的结合,Lean 版本为广泛的路由器和嵌入式设备提供了优化的固件及增强功能,广泛应用于家庭、企业及实验室环境。Lean 仓库包含了来自全球开源社区的众多补丁、优化、驱动程序以及各种第三方应用,极大地提升了 OpenWrt 系统的可定制性和性能。

本次项目的目标是基于 DShanPl-A1 Education 单板构建一个轻量化 NAS(轻NAS)应用。最佳实现路径是采用成熟且高度可扩展的开源路由系统 OpenWrt LEDE。借助 OpenWrt 完整的 Linux 环境与丰富的生态插件,我们能够在系统中按需安装存储服务、网络服务、内网穿透、安全访问等功能模块。通过这些插件之间的协作配置,再结合 OpenWrt 强大的网络管理能力,即可构建一个基于软路由架构的轻NAS 方案。

开发环境

环境说明

编译openwrt官方推荐使用原生 GNU/Linux environment,但是也支持使用Windows WSL的模式进行编译。在Windows上使用WSL的开发环境,不用去配置虚拟机环境,在某些限制按照vmware的环境下也可以使用,估笔者的编译和开发环境大多数优先使用WSL进行。

WSL(Windows Subsystem for Linux)在 Windows 上提供原生级 Linux 环境,适合开发者进行跨平台或 Linux 相关项目开发。其主要优点包括:

  1. 轻量快速:无需虚拟机或双系统,启动和运行几乎与原生 Linux 一样快,资源占用低。
  2. 无缝集成 Windows:可以直接访问 Windows 文件系统、使用 Windows 工具(如 VSCode、浏览器)与 Linux 工具协同工作。
  3. 原生 Linux 体验:支持大多数 Linux 命令、包管理器、构建工具,可直接进行编译、调试、运行服务。
  4. 易于安装维护:从 Microsoft Store 一键安装,系统更新、环境切换都非常方便。
  5. 优秀的开发体验:支持 Docker(WSL2)、Git、Python、Node.js、C/C++ 等主流开发环境,适合嵌入式、服务器、网络、AI 等领域。
  6. 跨平台兼容性好:能在 Windows 上构建 Linux 可运行的软件,如编译 OpenWrt、构建驱动、生成交叉编译包等。
总体来说,WSL 让开发者在 Windows 下以最小成本获得接近原生的 Linux 能力,大大提升效率与灵活性,我们采用VSCode远程访问的方式,很方便的可以完成开发工作。 下面是一些需要在编译前需要配置的WSL环境,把这部分复制到

环境变量配置

参考官方文档:Build system setup WSL

在WSL环境下面,编译用户的.bashrc里面,按照下面说明增加对应的配置信息,解决默认环境下,Windows的环境变量也会在WSL里面默认生效的问题,这样设置过后,基本上环境和原生的GNU/LINUX环境保持一致,不会用WSL的机制导入的问题。

# GO编译配置,如果编译不了打开这个
#export GO111MODULE=on
#export GOPROXY=https://goproxy.cn

# proxy,这里替换成自己的Windows环境的代理服务IP:PORT
export http_proxy=http://192.168.31.50:6080
export https_proxy=http://192.168.31.50:6080

export REPO_URL='https://mirrors.tuna.tsinghua.edu.cn/git/git-repo'

# Filter Windows PATH stuff
export PATH=$(echo $PATH | sed -e 's|:[^:]*WindowsApps[^:]*||g')
export PATH=$(echo $PATH | tr ':' '\n' | grep -v NVIDIA | tr '\n' ':')
export PATH=$(echo $PATH | tr ':' '\n' | grep -v 'Files' | paste -sd ':' -)
export PATH=$(echo $PATH | tr ':' '\n' | grep -v 'VS' | paste -sd ':' -)
export PATH=$(echo $PATH | tr ':' '\n' | grep -v '/mnt/' | paste -sd ':' -)

WSL网络代理设置

针对有的工具包在github上,默认网络下载可能会经常失败,我们可以选择在Windows上运行对应的代理软件,然后开启允许其它设备连接,然后在WSL中配置对应的http_proxy和https_proxy环境变量,就可以很方便的对github访问进行加速了。

下面是一些配置示例:

  1. 代理软件使能局域网设备连接

  1. WSL Settings中设置网络模式为Mirrored

更多说明请查阅微软的官方稳文档:使用 WSL 访问网络应用程序 - 镜像模式网络

  1. 配置好后,先wsl --shutdown,然后再重新启动wsl ubuntu
  2. 检查环境变量WSL_PAC_URL是否已经配置成功,成功的示例如下:

  1. 配置终端http和https代理,自动PAC过滤
export http_proxy=$WSL_PAC_URL
export https_proxy=$WSL_PAC_URL

Tips: 可以直接写入当前用户的.bashrc里面,这样不用每次都执行一遍代理设置。

刷机测试方法

这里介绍刷入LEDE镜像的基础方法,需要提前掌握刷入镜像的方法,下面是详细步骤介绍。

硬件连接

烧录系统镜像,除了dshanpi-a1板子,还需要准备 TypeC USB线 、30W PD电源适配器 (建议韦东山店铺购买),如下所示:

安装驱动和刷机软件

需要下载的工具包和镜像文件如下:

在前面下载的资料里找到驱动安装工具包 DriverAssitant_v5.1.1.zip,解压 ,然后打开启动下载程序 DriverInstall.exe ,点击驱动安装即,如下:

解压前面下载链接下载的烧录工具RKDevTool_Release_v3.32.zip,然后直接双击RKDevTool.exe即可。

开始烧录

准备工作完成后,按照下面的步骤操作,使设备进如MASKROM烧录模式:

① 接上 usb2.0/3.0 otg 线(也即type-c烧录数据线,数据线另一端接电脑的 USB2.0/3.0 蓝色接口);

② 按住 **<font style={{color: 'rgb(28, 30, 33)', backgroundColor: 'rgb(246, 247, 248)'}}>MASKROM</font>** 按键,先不松开

③ 再接上电源,dshanpi-a1 就会进入 **<font style={{color: 'rgb(28, 30, 33)', backgroundColor: 'rgb(246, 247, 248)'}}>MASKROM</font>** 烧录模式;

打开烧录工具,按照下面的选择界面参数,配置烧录镜像和参数,然后点击执行,等待下载完成,在刷入完成后,会自动重启单板,然后出现LEDE的像素LOGO,即刷入完成。

OpenWrt刷写EMMC的参数配置示例

系统启动后shell提示

注意:在OpenWrt编译完成后,可以刷写的镜像会被压缩成zip格式文件,需要先执行解压操作,然后才能做为烧录工具的刷机镜像,示例如下:
jason@ubuntu24:~/LEDE/bin/targets/rockchip/armv8$ gunzip -k openwrt-rockchip-armv8-100ask_dshanpia1-squashfs-sysupgrade.img.gz -f
gzip: openwrt-rockchip-armv8-100ask_dshanpia1-squashfs-sysupgrade.img.gz: decompression OK, trailing garbage ignored

# 待输入img镜像文件
jason@ubuntu24:~/LEDE/bin/targets/rockchip/armv8$ ls -lh openwrt-rockchip-armv8-100ask_dshanpia1-squashfs-sysupgrade.img
-rw-r--r-- 1 jason jason 640M Nov 28 02:24 openwrt-rockchip-armv8-100ask_dshanpia1-squashfs-sysupgrade.img

参考文档

OPENWRT系统 2.构建自定义系统

· 阅读需 10 分钟

序言

本文档旨在为开发者和爱好者提供一份清晰、简洁的 OpenWrt(LEDE)固件编译指南,专用于 DshanPi A1(基于瑞芯微 RK3576 平台) 开发板。通过本流程,您将完成从源码获取、依赖更新、配置定制(包括 OP 域与 Kernel 域)、到最终固件生成的完整编译过程。文档同时涵盖最小化配置生成方法、常见问题提示及镜像输出说明,帮助用户高效构建适配硬件的稳定固件,为后续开发、调试或部署奠定基础。无论您是初次接触 OpenWrt 编译,还是希望针对 RK3576 平台进行深度定制,本文均可作为实用参考。

配置编译环境

如果是基于WSL编译的,请参考《一、单板介绍与开发环境搭建 - 开发环境》章节,配置好WSL的基础环境。

然后按照仓库readme安装编译需要的编译工具链,安装好编译相关,示例如下:

sudo apt install -y ack antlr3 asciidoc autoconf automake autopoint binutils bison build-essential \
bzip2 ccache clang cmake cpio curl device-tree-compiler flex gawk gcc-multilib g++-multilib gettext \
genisoimage git gperf haveged help2man intltool libc6-dev-i386 libelf-dev libfuse-dev libglib2.0-dev \
libgmp3-dev libltdl-dev libmpc-dev libmpfr-dev libncurses5-dev libncursesw5-dev libpython3-dev \
libreadline-dev libssl-dev libtool llvm lrzsz msmtp ninja-build p7zip p7zip-full patch pkgconf \
python3 python3-pyelftools python3-setuptools qemu-utils rsync scons squashfs-tools subversion \
swig texinfo uglifyjs upx-ucl unzip vim wget xmlto xxd zlib1g-dev

获取源码

直接git clone百问网的仓库即可,注意有些环境可能github访问受限,克隆不下来,可以参考《一、单板介绍与开发环境搭建 - 3.3 WSL网络代理设置》中的内容,配置http/https终端代理即可。

git clone https://github.com/dshanpi/RK3576-DshanPiA1_LEDE.git

在下载完源码后,更新feeds,下载对应的包:

cd RK3576-DshanPiA1_LEDE
./scripts/feeds update -a
./scripts/feeds install -a

自定义选项

在更新feeds完成后,我们可以先使用默认的minimal配置做为基础,然后在上面做自定义配置即可,示例如下:

cp minimal.config .config
make defconfig # 会自动补齐缺失的配置项,使其成为可编译的完整配置

后续的配置可以保存为defconfig,加入版本管理中,具体方法查看本文 《4.5 保存配置》章节。

生成了基础配置后,就可以进行自定义配置了,自定义配置的命令为:

make menuconfig

系统的配置,简单划分的话,可以主要分为以下几个部分:

  • busybox

这部分是基础系统的特性配置,openwrt的基础系统是使用的busybox。

  • app

这部分对应一些命令或者luci-xxx这类待页面的应用,主要靠这部分扩展路由器的功能,和暴露易用的配置界面。

  • libs

这部分主要是配置openwrt的系统里面集成的库,可以按需增加,常规情况下是选中某些命令或者luci类的app的时候,会自动选中对应的库。

  • kernel

这部分主要是配置kernel,包含一些内核功能和驱动,在有新的外设支持的时候,需要配置到。

配置编译选项

这部分主要配置编译目标文件时候的一些优化参数和编译工具链的一些选项,按照下面的方法打开:

  1. 首先使能 Advanced configuration options (for developers):

  1. 使能 Target Options,并填入目标优化的GCC编译参数:

目标优化编译参数配置

  1. 使能Toolchain Options,配置工具链选项:

警告:此处C库实现要选择musl,不然刷机完进如系统后,通过Opkg下载的包全都无法使用!(因为默认的包都是musl c库)!

配置busybox选项

在有些场景,openwrt的非busybox类的配置无法覆盖要求,需要通过busybox里面的包来实现,这个时候就需要自定义busybox的选项。可以参考下面的配置方法:

  1. 选中 Base System -> Customize busybox options

其中Settings是一些额外的参数配置,如编译选项等;Applets是一些命令行工具的配置,我们按需进行相应的配置即可。

需要注意的是,当openwrt的包能提供对应的功能的时候,我们不应在busybox的配置里面再提供,不然在编译的最后环节,会提示已经提供但是Busybox里面也有的错误提示。

配置应用

openwrt包含丰富的应用集,可以极大地丰富路由器的功能,包含各种各样的库,命令行工具,带界面的APP(常称为插件)等,这里只对配置一些常见的APP做一些配置示例。

大部分包都按照分类,有序的按照字母顺序排列在各个大项下面,如我们想使能一下sftp-server,那么可以在:Network -> SSH 页面下找到,然后选中即可,示例如下:

还有一种比较快捷的方法,可以通过menuconfig的配置项搜索功能,快速定位到需要配置的页面去选中,下面已sftp-server为目标,搜索打开的方法示例:

  1. 在menuconfig主界面,输入/,进如搜索页面,然后在搜索页面输入sftp-server

  1. 根据搜索结果页面中(N)对应的数字索引,可以快速跳转到某条结果,如此处只有一个结果,那么索引是1,直接输入1,会直接跳转到对应的页面。

  1. 可以看到目前搜索页面对应的包是=n,未使能的;输入索引值跳转过去后发现确实是未使能,相对应,这个时候我们只需要输入y使能即可。

  1. 针对有多条搜索结果的情况,我们可以通过输入空格键实现整页翻页查看结果,也可以通过输入上下键实现按行翻页查看结果,通过查看搜索页面的详细信息,确定是否是自己要找的包。

说明:推荐两种方法都灵活使用,可以大大加快开发效率。

配置kernel选项

kernel的配置选项和其他不同,不是make menuconfig配置的,而是有单独的make目标,示例如下:

make kernel_menuconfig

在未编译过整个工程的情况下,执行上述命令会自动去先编译依赖的工具链,可能比较耗时,推荐先整个工程编译一次,然后再自定义配置kernel选项。

因为第一次整个编译工程的时候,会下载依赖的所有源码包,并编译对应的工具链,比较耗时。

保存配置

针对OP域的配置,使用下面命令,生成最小配置文件:

./scripts/diffconfig.sh > defconfig

针对kernel域的配置,在配置kernel的时候,会自动更新配置到target下面的config文件里面,不用手动保存。

编译

流程

首先执行下载操作,解决完下载过程中可能遇到的问题,然后再执行编译流程。

避免默认的编译过程中下载,可能会某个包失败了后,再编译的时候,会挨个检查之前的包是否下载和编译完成了,这样不利于调试,执行命令如下:

# 当下载失败的时候,使用-j1查看具体的失败信息
# 下载的源码包都存放在工程根目录的dl目录下
make download -j$(nproc)

#第一次编译推荐用单线程,测试多线程编译会失败!
make V=s -j1

二次编译的时候,可以执行:

make V=s -j$(nproc)

如果需要重新配置,按照下面流程执行:

rm -rf .config
make menuconfig
make V=s -j$(nproc)

常见编译错误

有些程序编译的时候会失败,这个时候我们需要重新使用make V=s -j1的方式重新跑一遍,才能较好的看到编译过程中的错误,比较场景的有未定义或者库找不到的错误,或者Werror导致的报错,下面是一个简单的解决方式示例。

如最开始使用glibc进行编译,发现mbedtls和vlmcsd一直编不过,增加如下修改可以编过:

diff --git a/package/libs/mbedtls/Makefile b/package/libs/mbedtls/Makefile
index 4e0a4a034..54a0b2d45 100644
--- a/package/libs/mbedtls/Makefile
+++ b/package/libs/mbedtls/Makefile
@@ -121,7 +121,7 @@ This package contains mbedtls helper programs for private key and
CSR generation (gen_key, cert_req)
endef

-TARGET_CFLAGS += -ffunction-sections -fdata-sections
+TARGET_CFLAGS += -ffunction-sections -fdata-sections -Wno-error=stringop-overflow
TARGET_CFLAGS := $(filter-out -O%,$(TARGET_CFLAGS))

CMAKE_OPTIONS +=
--- Makefile.orig 2025-11-14 01:58:43.376952312 +0800
+++ Makefile 2025-11-14 01:53:50.865983762 +0800
@@ -37,4 +37,6 @@
$(INSTALL_BIN) ./files/vlmcsd.ini $(1)/etc/vlmcsd/vlmcsd.ini
endef

+TARGET_LDFLAGS += -lresolv -lpthread
+
$(eval $(call BuildPackage,vlmcsd))

更多编译过程中的错误,灵活使用AI工具和搜索引擎,基本都能解决编译中遇到的问题。

烧写

编译完成后,会在对应的bin/target/xxxx目录生成两个类型的镜像包,一个是ext4一个是squashfs的,如果有恢复默认配置需求,需要使用squashfs的镜像包。

注意:在OpenWrt编译完成后,可以刷写的镜像会被压缩成zip格式文件,需要先执行解压操作,然后才能做为烧录工具的刷机镜像,示例如下:
jason@ubuntu24:~/LEDE/bin/targets/rockchip/armv8$ gunzip -k openwrt-rockchip-armv8-100ask_dshanpia1-squashfs-sysupgrade.img.gz -f
gzip: openwrt-rockchip-armv8-100ask_dshanpia1-squashfs-sysupgrade.img.gz: decompression OK, trailing garbage ignored

解压得到img镜像后,参考《单板介绍与开发环境搭建 - 4.3开始烧录》内容,进行刷机。

openwrt系统自带的在线刷机功能使用的时候有问题,参考《现有功能优化 - 1.3 sysupgrade镜像无法使用》章节进行适配,适配后便可以直接通过web的方式直接刷机,示例如下:

注意:web页面在线升级选择的为压缩后的镜像包!

2026-1/OPENWRT系统 3.现有功能优化/现有功能优化

· 阅读需 14 分钟

在下载百问网官方适配后的Openwrt源码后,发现使用上有很多功能没有完全适配,出现部分使用过程中体验不好的问题,下面是针对笔者使用中发现的问题的记录与解决办法分析,希望给读者遇到类似问题后,一些解决问题的思路。因为本人知识有限,有什么错误的地方,欢迎交流讨论。

安装第三方ipk无法使用

默认的云端mirror库的libc使用musl,使用glibc后烧进去,发现安装的程序都用不了,默认openwrt都是用的musl libc。

安装第三方的fdisk无法使用

默认的busybox配置,很多工具都没有,fdisk通过opkg安装后某个提示库不存在,需要手动配置Busybox的选项,我们把打开自定义busybox选项,然后配置fdisk使能。

然后重新编译镜像,然后刷入,测试便可以发现fdisk可以使用了。

sysupgrade镜像无法使用

使用默认生成的sysupgrade镜像,在界面【系统-备份与升级-刷写新的固件】里面选择了编出来的固件的时候,发现无法使用,有如下错误打印信息:

Tue Dec 2 17:27:21 2025 user.info upgrade: Device 100ask,dshanpi-a1 not supported by this image
Tue Dec 2 17:27:21 2025 user.info upgrade: Supported devices: 100ask,dshanpia1
Tue Dec 2 17:27:21 2025 user.info upgrade: Reading partition table from bootdisk...
Tue Dec 2 17:27:22 2025 user.info upgrade: Reading partition table from image...
Tue Dec 2 17:27:22 2025 user.info upgrade: Device 100ask,dshanpi-a1 not supported by this image
Tue Dec 2 17:27:22 2025 user.info upgrade: Supported devices: 100ask,dshanpia1
Tue Dec 2 17:27:22 2025 user.info upgrade: Reading partition table from bootdisk...
Tue Dec 2 17:27:22 2025 user.info upgrade: Reading partition table from image..

检查发现是armv8.mk里面定义的设备名称,和dts中的compatible不一致, 致 sysupgrade 拒绝刷机。

OpenWrt sysupgrade 会读取:

  1. 当前运行设备的标识:

来自:

  • /proc/device-tree/compatible
  • /etc/board.json
  1. 固件中 embedded 的 supported_devices 列表

二者任意一个字符不匹配就报:Device XXX not supported by this image

这里知道问题所在了,修改就比较简单了,按照如下方式修改:

  1. 修改 target/linux/rockchip/image/armv8.mk,保持和dts中的一致

  1. 重新执行make menuconfig,选择target,会自动更新.config文件

  1. 重新执行make V=s -j8,进行镜像编译即可。

sftp无法使用

默认的使用的dropbear做为ssh server,没有sftp功能,这里我们修改配置,关掉dropbear,然后在Network -> SSH下面打开openssh,如下图所示。

编译无法通过,openssh-sk-helper编译依赖libfido2,我们手动使能这个库,选中为y,然后重新编译即可。

注意:openssh-server和openssh-server-pam无法同时使能,我们打开没有PAM支持的编译即可。

rootfs空间太小

我们优先刷的squashfs格式的镜像,可以比较方便的恢复出厂配置,因为squashfs格式的rom是基于overlayfs的,更新的配置不会直接改到rom里面的内容。但是我们可以发现默认配置的rootfs的大小比较小,而板载的EMMC有58G,我们可以将rootfs空间扩大到8G,剩下的空间单独分配一个分区。

默认配置的rootfs分区为512M,如下所示:

修改配置,默认rootfs分区大小为2G,修改 Target Images下的Rootfs分区大小配置,如下所示:

usb设备无法识别

当我们只简单的在设备树使能usb0和usb1过后,会发现能识别到U盘了,但是usb驱动probe期间会打印dr_mode强制设置为host的打印,查找代码发现是默认配置的otg,但是没有otg对应的配置,并且drd相关的代码都未编译。

需要修改设备树文件,使能usb0和usb1控制器,默认角色为host。使能DRD ROLE SWITCH功能,然后就可以动态配置控制器角色,然后还可以指定默认角色。

必须要打开了 USB Gadget才能是能双角色功能,那么我们打开,然后在Mode Selection里面选中Dual Role mode,这样会在内核生成usb_role的sysfs节点,可以动态配置成host或者peripheral,在dshan pi a1上,usb1固定为host,usb0可以配置为双角色,可以切换,那么我们做如下dts配置:

--- a/target/linux/rockchip/files/arch/arm64/boot/dts/rockchip/rk3576-100ask-dshanpi-a1.dts
+++ b/target/linux/rockchip/files/arch/arm64/boot/dts/rockchip/rk3576-100ask-dshanpi-a1.dts
@@ -770,6 +770,20 @@
status = "okay";
};

+// usb0 as type-c port, can be host or peripheral.
+&usb_drd0_dwc3 {
+ status = "okay";
+ usb-role-switch;
+ role-switch-default-mode ="host";
+};
+
+// usb1 as type-a port, fixed to host
+&usb_drd1_dwc3 {
+ status = "okay";
+ usb-role-switch;
+ role-switch-default-mode ="host";
+};
+
&uart0 {
pinctrl-0 = <&uart0m0_xfer>;
status = "okay";

配置后,usb1可以使用,但是usb0无法使用,并且板载Hynetek HUSB311 Type-C 芯片,可以提供USB PD和USB Type-C的功能。发现默认6.12内核版本的驱动,没有该芯片的支持,查看rockip官方仓库,有这款芯片的支持,需要移植过赖,我们暂时不需要DP功能,屏蔽掉。

PWM风扇一直最大风速

上电后,风扇一直以最大转速工作,声音比较大,需要修改下,支持按照温度自动调整转速,这样更符合常见的应用场景。排查记录如下:

查硬件

风扇使用的树莓派5的4针风扇,是标准的4针JST插口,实物图和原理图如下:

风扇插口为1mm间距JST SH插座,有四个引脚:
PIN序号功能
1+5V
2PWM
3GND
4转速

查看了手册,2脚连接的PWM1,4脚连接的风扇转速口,说明不支持读取转速,只能PWM控制转速。看原理图是PWM1_CH0,dts里面也有对应的配置,根据compatible字段"pwm-fan"到驱动里面搜索,发现存在linux-6.12.43/drivers/hwmon/pwm-fan.c这个文件没有编译,那么就是KCONFIG没有选中,没有编译pwm-fan的驱动。

使能PWM驱动

那么我们直接make kernel_menuconfig,搜索COFNIG_SENSORS_PWM_FAN,然后输入搜索结果对应的序号,然后就会直接跳转到配置对应的地方,直接输入Y使能即可。

编译刷写后,启动过程中发现pwm-fan驱动启动失败,有如下打印:

可以得出,没有找到上游的PWM设备,搜索引用的节点的compatible,发现对应的驱动未打开,在单独的目录下:drivers/soc/rockchip

打开的驱动:

打开后,会出现编译问题,我们修改如下:

修改如下:

--- a/include/soc/rockchip/utils.h 2025-11-23 03:35:07.695086227 +0800
+++ b/include/soc/rockchip/utils.h 2025-11-23 03:34:43.946599951 +0800
@@ -50,6 +50,7 @@
*
* Return: the value, shifted into place, with the required write-enable bits
*/
+#if 0
#define REG_UPDATE_WE(_val, _low, _high) ( \
BUILD_BUG_ON_ZERO(const_true((_low) > (_high))) + \
BUILD_BUG_ON_ZERO(const_true((_high) > 15)) + \
@@ -57,6 +58,11 @@
BUILD_BUG_ON_ZERO(const_true((u64) (_val) > U16_MAX)) + \
((_val & GENMASK((_high) - (_low), 0)) << (_low) | \
(GENMASK((_high), (_low)) << 16)))
+#else
+#define REG_UPDATE_WE(_val, _low, _high) ( \
+ ((_val & GENMASK((_high) - (_low), 0)) << (_low) | \
+ (GENMASK((_high), (_low)) << 16)))
+#endif

/**
* REG_UPDATE_BIT_WE - update a bit with a write-enable mask
@@ -68,9 +74,14 @@
*
* Return: a value with bit @__bit set to @__val and @__bit << 16 set to ``1``
*/
+#if 0
#define REG_UPDATE_BIT_WE(__val, __bit) ( \
BUILD_BUG_ON_ZERO(const_true((__val) > 1)) + \
BUILD_BUG_ON_ZERO(const_true((__val) < 0)) + \
REG_UPDATE_WE((__val), (__bit), (__bit)))
+#else
+#define REG_UPDATE_BIT_WE(__val, __bit) ( \
+ REG_UPDATE_WE((__val), (__bit), (__bit)))
+#endif

#endif /* __SOC_ROCKCHIP_UTILS_H__ */

配置转速方法

手动配置pwm-fan的方法,在sysfs下面查找pwm-fan的目录,进入/sys/class/hwmon/hwmon0/下,即可看到两个文件pwm1_enable和pwm1,先配置pwm1为100,观察风扇声音是否减小,试验发现确实变小了,说明PWM控制生效了。

root@LEDE:~# ls /sys/class/hwmon/hwmon0/
device of_node pwm1 subsystem
name power pwm1_enable uevent
root@LEDE:~# echo 100 > /sys/class/hwmon/hwmon0/pwm1

kernel文档关于pwm_fan的sysfs节点说明

参考文档:

  • Documentation/hwmon/pwm-fan.rst
  • Documentation/devicetree/bindings/hwmon/pwm-fan.yaml
  • Documentation/driver-api/thermal/sysfs-api.rst

也可以根据thermal框架,下面的cooling_device找到pwm-fan。在thermal cooling device框架下有注册的sysfs接口,对应绑定pwm_fan驱动提供的ops,将state数值对应dts中配置的cooling_levels数组索引,进而可以通过配置0..max_state的数值,来配置pwm驱动里面对应的相对速度,在shell里面直接。

static const struct thermal_cooling_device_ops pwm_fan_cooling_ops = {
.get_max_state = pwm_fan_get_max_state,
.get_cur_state = pwm_fan_get_cur_state,
.set_cur_state = pwm_fan_set_cur_state,
};

//dts
fan: pwm-fan {
status = "okay";
compatible = "pwm-fan";
#cooling-cells = <2>;
pwms = <&pwm1_6ch_0 0 50000 1>;

// 这里对应state从0..5
cooling-levels = <0 100 125 150 200 255>;

// 这里配置的trips,没有生效。我们需要将它放到thermal框架里去
rockchip,temp-trips = <
40000 1
50000 2
60000 3
65000 4
70000 5
>;
};
root@LEDE:~# ls /sys/class/thermal/cooling_device0/
cur_state max_state power subsystem type uevent
root@LEDE:~# cat /sys/class/thermal/cooling_device0/max_state
5
# 这里等同于 echo 100 > /sys/class/hwmon/hwmon0/pwm1
root@LEDE:~# echo 1 > /sys/class/thermal/cooling_device0/cur_state

增加自动调整转速功能

自动调整转速,依赖于thermal系统,主要有theraml_zone,cooling_device,trip_point三个组成。

参考文章:Linux Thermal 框架解析-CSDN博客

在原本的dts里面配置的thermal_zones节点下新增对应的trip。

单独编译dts 失败

make target/linux/prepare V=s
make target/linux/compile DTBS=1 V=s

然后你就可以直接找到生成的:

build_dir/target-*/linux-*/linux-*/arch/arm64/boot/dts/*.dtb

配置pwm1或者cur_state=0的时候,风扇转速最大。根据kernel文档关于pwm_fan的sysfs节点说明,可以由pwm1_enable文件进行配置当pwm1=0的时候的具体行为。默认的值为1,即disable pwm, keep regulator enabled。所以设置成0的时候,会直接拉满。

所以这里我们配置cooling-levels数组的时候,第0个元素的值设置为10,以较低速度转动。然后pwm1_enable设置为2,即pwn=0的时候,pwm和regulator都还有输出,即占空比0。

板载LED灯没有驱动

这是一个 由单线串行控制的 RGB LED 灯带链路

  • 芯片:WS2812C-2020
  • 灯的数量:4 个(RUN × 4)
  • 每个 LED 既是 RGB LED,又集成了驱动芯片
  • 只需要 1 根 GPIO 数字信号线 就能控制一串灯

典型用途:

  • 状态灯
  • 跑马灯
  • 主板灯光
  • 工控指示灯
  • 路由器/电视盒子的呼吸灯

WS2812 是 单线 800kHz NRZ 协议,不是普通 PWM。Linux 内核无法直接 bit-bang 得够快,必须使用能产生精确的波形才能驱动。

通过搜索源码,发现工程里面有提供的两种驱动,一个是leds-ws2812b,一个是ws2812-pio-rp1。仔细检查发现一个是基于spi,一个是基于pio扩展芯片的方式,我们原理图里面只能使用spi hacking的方式。

配置方法参考redmi的配置,适配到dshanpi即可。

原理图里面的PIN脚没有MOSI功能,无法使用SPI HACKING,咨询得到,需要使用PWM HACKING方式。

MMC驱动经常打印报错

驱动报错信息:

[ 1344.988178] mmc0: Timeout waiting for hardware interrupt.
[ 1344.988672] mmc0: sdhci: ============ SDHCI REGISTER DUMP ===========
[ 1344.989235] mmc0: sdhci: Sys addr: 0x00000002 | Version: 0x00000005
[ 1344.989800] mmc0: sdhci: Blk size: 0x00007200 | Blk cnt: 0x00000002
[ 1344.990364] mmc0: sdhci: Argument: 0x00069e72 | Trn mode: 0x0000003f
[ 1344.990929] mmc0: sdhci: Present: 0x03f700f1 | Host ctl: 0x00000035
[ 1344.991493] mmc0: sdhci: Power: 0x0000000d | Blk gap: 0x00000000
[ 1344.992057] mmc0: sdhci: Wake-up: 0x00000000 | Clock: 0x0000030f
[ 1344.992621] mmc0: sdhci: Timeout: 0x0000000e | Int stat: 0x00000000
[ 1344.993185] mmc0: sdhci: Int enab: 0x03ff000b | Sig enab: 0x03ff000b
[ 1344.993749] mmc0: sdhci: ACmd stat: 0x00000000 | Slot int: 0x00000000
[ 1344.994313] mmc0: sdhci: Caps: 0x3a6dc881 | Caps_1: 0x08000007
[ 1344.994876] mmc0: sdhci: Cmd: 0x0000123a | Max curr: 0x00000000
[ 1344.995439] mmc0: sdhci: Resp[0]: 0x00000900 | Resp[1]: 0xfff6dbff
[ 1344.996003] mmc0: sdhci: Resp[2]: 0x320f5903 | Resp[3]: 0x00009001
[ 1344.996566] mmc0: sdhci: Host ctl2: 0x0000380f
[ 1344.996957] mmc0: sdhci: ADMA Err: 0x00000060 | ADMA Ptr: 0x00000000fc300210
[ 1344.997581] mmc0: sdhci: ============================================

dmesg里面probe的信息:

[ 0.438699] mmc0: SDHCI controller on 2a330000.mmc [2a330000.mmc] using ADMA 64-bit
[ 0.499319] mmc0: new HS400 Enhanced strobe MMC card at address 0001
[ 0.500422] mmcblk0: mmc0:0001 CJNB4R 58.2 GiB
[ 0.502009] mmcblk0: p1 p2 p3
[ 0.502655] mmcblk0boot0: mmc0:0001 CJNB4R 4.00 MiB
[ 0.503828] mmcblk0boot1: mmc0:0001 CJNB4R 4.00 MiB
[ 0.504873] mmcblk0rpmb: mmc0:0001 CJNB4R 4.00 MiB, chardev (247:0)

根据AI搜索和DTS里面的信息, 看得出来 eMMC 在上电初始化阶段是 完全正常工作的, 控制器寄存器/时钟/复位/pinctrl 基本没问题,否则根本不会成功切到 HS400 ES、识别分区 。

,可以尝试降档到HS200或者降低频率的方式,来进行测试,找一个稳定工作的版本,本地测试了两种方法:

  1. 降档到HS200,不修改频率
mmc-hs200-1_8v;
//mmc-hs400-1_8v;
//mmc-hs400-enhanced-strobe;

测试后发现,HS200工作没问题,启动打印:

[ 0.439179] mmc0: SDHCI controller on 2a330000.mmc [2a330000.mmc] using ADMA 64-bit
[ 0.492249] mmc0: new HS200 MMC card at address 0001
[ 0.493187] mmcblk0: mmc0:0001 CJNB4R 58.2 GiB
[ 0.494771] mmcblk0: p1 p2 p3
[ 0.495416] mmcblk0boot0: mmc0:0001 CJNB4R 4.00 MiB
[ 0.496586] mmcblk0boot1: mmc0:0001 CJNB4R 4.00 MiB
[ 0.497629] mmcblk0rpmb: mmc0:0001 CJNB4R 4.00 MiB, chardev (247:0)

  1. 保持HS400不变,降低频率到100M
//max-frequency = <200000000>;
max-frequency = <100000000>; //work perfect on 100M

测试后发现,也能正常工作,启动打印:

[ 0.439264] mmc0: SDHCI controller on 2a330000.mmc [2a330000.mmc] using ADMA 64-bit
[ 0.492185] mmc0: new HS400 Enhanced strobe MMC card at address 0001
[ 0.493275] mmcblk0: mmc0:0001 CJNB4R 58.2 GiB
[ 0.494696] mmcblk0: p1 p2 p3
[ 0.495331] mmcblk0boot0: mmc0:0001 CJNB4R 4.00 MiB
[ 0.496498] mmcblk0boot1: mmc0:0001 CJNB4R 4.00 MiB
[ 0.497546] mmcblk0rpmb: mmc0:0001 CJNB4R 4.00 MiB, chardev (247:0)

但是发现使用docker的时候会有各种错误打印:

就现使用最小修改的方法,设备树删除HS400模式配置,设置HS200模式,测试稳定运行,就先用这种。

OPENWRT系统 4.轻NAS玩法介绍

· 阅读需 10 分钟

方案介绍与选择

让 OpenWrt 上的 轻NAS(局域网文件共享)能在外网访问,也就是说,从任何地方都能安全地访问你的家用存储。 这是最简单的视线自建私有云的方式,只需要开发板要USB接口,3.0接口更佳,就可以接入USB移动硬盘,化身具有轻NAS功能的OpenWrt设备,下面是实现的步骤分析。

文件共享

我们首先需要实现局域网文件共享功能,下面是常见的局域网共享场景推荐的方法,我们这里选择samba4,FileBrowser,webDAV,三种常见的共享方式都支持上,下面是具体的场景推荐的协议,可以根据自己场景灵活选择。

场景推荐协议
Windows + Linux 通用文件共享Samba4
Linux 服务器挂载(如 Docker/K8s)NFS
外网访问 NAS(配合 frp/Tailscale)FileBrowser(Web)
最安全的传输(需要加密)SFTP
iPhone/macOS 挂载网盘WebDAV
媒体播放器(电视、DLNA)Samba4 或 NFS

内网访问

在完成局域网网络共享后,要想实现轻NAS,还有个关键的功能,就是可以远程随时查看家里共享的文件,那么我们就需要实现内网穿透,下面是常见的内网穿透方法,及以对应的优缺点,我们选择使用frp(自建中转服务器)和现在比较流行的方法DDNSTO,前者需要自己有一个公网的服务器来做数据转发,后者操作简便只需要安装对应的插件,然后在易有云平台绑定自己的设备即可,由易有云服务商的服务器来做数据转发。

下面是常见的内网访问的方案对比:

方案优点缺点安全性
公网 IP + 端口映射简单直连,速度快需要公网 IP(电信一般不给)较低(需防火墙)
🌐 DDNS + 公网 IP适合动态 IP 用户同样需要公网访问权限中等
🔐 ZeroTier / Tailscale VPN无需公网 IP,自动穿透 NAT需第三方 VPN 控制平面
☁️ frp / Cloudflare Tunnel自建隧道,无需公网 IP依赖中间服务器
DDNSTO路由远程简单操作,无需公网IP依赖第三方服务商

这里我们优先使用DDNSTO插件+插件提供商的云服务来实现轻NAS应用的内网穿透,也可在自己的VPS上自建frp云服务来实现轻NAS应用的内网穿透(适合高阶用户,需要配置很多参数和一些网络知识,当然也可以问AI来生成对应的配置),可根据自己的实际情况选择适合自己的方案。

DDNSTO插件实现轻NAS应用

挂载硬盘

首先,我们需要将移动硬盘插入DshanPi A1的USB TYPE-A口上,然后配置对应的挂载目录,并且设置为每次开机自动挂载。这里使用U盘进行测试示例,移动硬盘配置方法完全一样。

首先,在 **系统 -> 挂载点 **页面,配置磁盘自动挂载的目录,并启用。

配置好后,重启设备,观察配置是否断电也有效,生效的话可以看到下面的打印:

/media的默认权限:

使能文件共享服务

Samba共享

Samba是在Linux系统上实现SMB协议的一个免费软件,我们可以使用支持SMB协议的终端设备, 来实现局域网内的文件共享。

安装Samba

首先,我们需要在编译前选中luci-app-samba4,或者刷机后通过在线安装ipk的方式安装samba4服务端程序到系统内。安装完成后,在页面: 服务 -> 网络共享 可以看到对应的配置,如下图:

创建Samba用户

在进行网络共享时,我们应该避免使用root用户来登录samba服务器。 为此,我们单独创建一个用户来用于samba服务器的访问,并为它赋予文件夹的访问权限。

打开 **服务 -> 终端 **,执行下面的命令,创建用户,并给用户开启共享目录的访问权限。

#添加名为samba的用户
useradd samba

#为用户samba创建smb服务的密码,这个和用户名的密码是单独的,可以设置不同
smbpasswd -a samba

#使用户samba获得共享目录的权限
#注意:只有ext4的文件系统才能修改权限,根据自己磁盘格式做对应调整
chown -R samba:samba /media/

修改/etc/passwd,配置samba用户无法登陆,下面是示例:

修改samba4配置

打开 服务 -> 网络共享 进行参数的配置。

选择接口为lan,可以使内网设备访问。勾选 允许旧协议与身份验证。

点击 新增 一个条目。

  • 名称:共享时显示的文件夹名称,可随意设置,这里设置为media
  • 路径:将要共享的文件夹路径,这里设置为上一章节挂载的目录<font style={{color: 'rgb(64, 64, 64)', backgroundColor: 'rgb(252, 252, 252)'}}>/media</font>
  • 允许用户:具有访问权限的用户,这里设置为刚刚创建的用户samba。
主要设置就是这些,保存并应用这些配置,其他的设置可自行探索其他高阶配置。

SFTP共享

安装SFTP server

Dropbear 不支持 SFTP,但它支持 调用外部 sftp-server。

OpenWrt 已提供独立的 openssh-sftp-server 包:

opkg update
opkg install openssh-sftp-server

安装好后,sftp-server会放在:/usr/lib/sftp-server,这种方案适合需要有界面配置ssh秘钥的功能,但是也需要sftp server功能的场景,如果全部替换成openssh的全家桶,会因为OpenSSH 在 OpenWrt 中无官方 LuCI 配置界面,导致所以配置都要通过终端来完成。

配置SFTP

在安装好后,不用做其他配置,都可以直接使用,例如用Xftp直接连接,就能看到系统内的文件。

WebDAV共享

在安装完DDNSTO插件后,内部自带了一个轻量的webdav服务,不用再单独安装,直接使用即可。

配置内网穿透

首先登录DDNSTO控制台,注册登录后,记录用户Token,然后在板端配置DDNSTO远程控制页面,配置对应的参数,示例如下:

配置Samba远程访问

登录DDNSTO控制台,在文件管理栏下,点击添加文件管理,增加Samba协议的文件管理服务,填入对应参数,示例如下:

  1. 添加配置

  1. 点击连接
  2. 输入Samba4配置中的用户和密码
  3. 连接成功,可以看到对应目录下的文件,如下所示:

配置SFTP远程访问

登录DDNSTO控制台,在文件管理栏下,点击添加文件管理,增加Sftp协议的文件管理服务,填入对应参数,示例如下:

  1. 添加配置

  1. 点击访问

  1. 输入ssh可以登录的用户名和密码,这里输入root对应的密码
  2. 连接成功,可以看到对应目录下的文件,如下所示:

配置WebDAV远程访问

登录DDNSTO控制台,在文件管理栏下,点击添加文件管理,增加webdav协议的文件管理服务,填入对应参数,示例如下:

  1. 添加配置

  1. 点击访问

  1. 输入路由器系统里面DDSNTO插件中,填写的授权用户名和密码到登录页面中

  1. 连接成功,可以看到对应目录下的文件,如下所示:

配置远程访问路由器后台

登录DDNSTO控制台,选择外网域名栏,然后点击添加域名,按照下面示例填写配置:

配置完成后,点击外网域名栏,可以直接跳转到外网域名页面,这样就可以在任何地方远程配置局域网内的路由器了。

总结一下:DDNSTO插件把很多远程场景都整合起来了,轻度使用的话,付费用4Mbps的就可以了,延迟低,省去了各种复杂的环境搭建过程,和自建VPS的繁琐流程,推荐!

自建Frp云服务实现轻NAS应用

挂载硬盘并使能文件共享服务

自建方案中的挂载硬盘并使能文件共享服务,与使用DDNSTO插件方式完全一致,详细步骤轻查阅上一章节中的内容,此处不再累述。

配置内网穿透服务

这里需要配置的参数较多,并且需要考虑安全,需要涉及的配置项和证书等步骤较多,限于篇幅影响,这里不再详细描述,更多的请查阅frp的官方文档,搭建对应的内网穿透服务。

OPENWRT系统 5.Docker玩法介绍

· 阅读需 10 分钟

Docker 是一种开源的容器化平台,它通过“容器”来封装应用及其运行环境,使应用能够在不同系统之间快速、稳定地运行。容器轻量、启动快,占用资源少,适合微服务部署与持续集成/交付。Docker 还提供镜像管理、版本控制和环境一致性,让开发、测试、生产环境保持统一,大幅提升部署效率与可移植性。 Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。

硬件环境:OpenWrt 跑在 ARM 高性能 SBC 上(比如 本文使用的Dshanpi-A1),家里还有光猫 + 交换机/AC/AP 等常规设备。
目标:在这块 SBC 上用 Docker 跑 家庭影院 + 下载器 + 网盘 + 广告过滤 + 简单监控,一机多用。

拓扑 & 环境说明

家庭网络

  • 光猫改桥接,把拨号交给 ARM SBC 上的 OpenWrt
  • ARM SBC 既当主路由,又当“轻量 NAS + 家庭影院服务器”
  • 电视盒子、手机、电脑都连在 LAN(有线/无线都行),统一访问 SBC 上的服务

机器配置

  • 设备:ARM 64 位架构 SBC,8G 内存版本
  • 系统:OpenWrt(自己编译/整合固件都可以,关键是要有 Docker)
  • 磁盘
    • 系统盘(eMMC/TF)装 OpenWrt
    • 外接 SSD/HDD/大 U 盘做数据盘,挂到 /mnt/data

Docker 环境 & 目录规划

先把环境和目录规划确认好,后续好维护,这步比较关键。

安装 Docker / Docker Compose

如果你的固件已经打包了 Docker,可以直接跳过安装,推荐安装 luci-app-dockerman,这是 OpenWrt 上专门的 Docker Web 管理界面插件:

opkg install luci-lib-docker dockerd luci-lib-jsonc docker ttyd --force-depends
opkg install luci-app-dockerman
  • dockerd:Docker 守护进程
  • docker:命令行客户端
  • luci-lib-docker / luci-lib-jsonc:Dockerman 的依赖
  • ttyd:用于 Web 终端与容器控制台
  • luci-app-dockerman:Web 管理界面插件

启动并设为开机自启:

/etc/init.d/dockerd start
/etc/init.d/dockerd enable

然后访问 LuCI 后台,菜单里会多出:服务 / Docker服务 / Dockerman

也可以通过命令行的方式,确认环境能用,示例如下:

root@LEDE:~# docker version
Client:
Version: 28.0.4
API version: 1.48
Go version: go1.25.4
Git commit: b8034c0
Built: Sun Sep 7 14:53:18 2025
OS/Arch: linux/arm64
Context: default

Server:
Engine:
Version: 28.0.4
API version: 1.48 (minimum version 1.24)
Go version: go1.25.4
Git commit: 6430e49
Built: Sun Sep 7 14:53:18 2025
OS/Arch: linux/arm64
Experimental: false
containerd:
Version: 1.7.27
GitCommit:
runc:
Version: 1.2.6
GitCommit:
docker-init:
Version: 0.19.0
GitCommit: de40ad0
root@LEDE:~# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
root@LEDE:~#

能看到版本信息 & 空容器列表,就说明 OK。

docker-compose 推荐也装一个,方便后面多服务一起管理(以 ARM64 为例):

wget https://github.com/docker/compose/releases/download/v2.27.0/docker-compose-linux-aarch64 -O /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
docker-compose version

数据盘挂载 & 目录规划

首先把emmc剩余空间新建分区,格式化为ext4,然后界面上挂载为docker数据分区使用,当然使用其他外部存储设备保存也是可以的,比如使用TF卡来做为docker数据分区使用,增加对应的挂载目录配置即可,示例如下:

数据盘以/opt/data为根目录,可以这样规划:

/opt/docker # Docker 根目录(镜像、容器层等)
/opt/data
├─ media # 媒体文件(电影、剧集、音乐)
│ ├─ movies
│ └─ tv
├─ downloads # BT/PT 下载目录
└─ configs # 各容器配置文件
├─ jellyfin
├─ qbittorrent
└─ ...

再把目录建好:

mkdir -p /opt/data/{configs,downloads,media}
mkdir -p /opt/data/configs/{jellyfin,emby,transmission,qbittorrent,aria2,adguard,nextcloud}
mkdir -p /opt/data/media/{movies,tv,anime,music}
mkdir -p /opt/data/downloads/{bt,aria2,tmp}

后面所有容器都尽量挂到 /opt/data 下面,避免写爆系统盘。

这样做的好处:

  • 坏了一个容器就删了重建,数据不受影响
  • 换设备时只要把这块盘接过去,改一下路径就能继续用

配置内核选项支持docker运行

默认配置编译的kernel,docker运行的时候会有警告信息,提示缺少支持对应的功能支持,如下所示:

这些 WARNING 表示你的 内核未开启 cgroup v1/v2 的资源限制功能,导致 Docker 无法对容器进行 CPU、IO、内存 swap 等限制。 需要在我们的系统里面,打开下面对应的配置:

# 打开kernel配置页面
make kernel_menuconfig

按照下面配置,打开CGroup和Namespace支持:

重新编译,然后升级,启动后发现没有对应的报错信息即可。更多配置支持,请查看代码仓库里面的配置。

注意:有的docker版本,需要打开legacy cgroup v1相关的控制支持,此处保持关闭。

加速源配置

  1. 在安装下面的docker镜像的时候,可能会出现默认的仓库下载失败,可以配置内地的源,加速下载;

常见的加速镜像站地址:

{
"registry-mirrors": [
"https://docker.1panel.live",
"https://registry.docker-cn.com",
"http://hub-mirror.c.163.com",
"https://docker.m.daocloud.io"
]
}
  1. 如果发现配置了加速源提示无法访问,可能是安装的openwrt代理插件的问题,修改配置或者禁用代理后重试即可;
  2. 配置好后,执行 docker pull hello-world看是否可以正常拉取镜像,可以则说明网络配置完成。下面是正常工作的概览示例:

常见的一些玩法

Jellyfin 家庭影院(Emby/Plex 同理)

说明:下面用命令行和图文方式进行操作实例,后续章节仅提供命令行示例。

拉取镜像

命令行执行:

# [--platform linux/arm64]是可选参数,可以去掉
docker pull --platform linux/arm64 jellyfin/jellyfin:latest

LUCI界面操作:

拉取成功后,可以在页面镜像列表看到,如下图所示:

启动容器

启动命令示例:

docker run -d \
--name=jellyfin \
--restart=unless-stopped \
-p 8096:8096 \
-v /opt/data/configs/jellyfin:/config \
-v /opt/data/media:/media \
jellyfin/jellyfin:latest

可以直接复制上面的命令,到界面上的解析CLI,点击命令行按钮,然后粘贴,最后点击应用。

增加后,页面可以看到状态为Created,这个时候选中jellyfin容器,然后点击启动:

如果 SBC 支持硬件解码(GPU 驱动也搞好了),可以尝试加上:

--device /dev/dri:/dev/dri

ARM 平台硬解是个坑比较多的进阶话题,能成功算赚到,不能用就当纯软解顶着,1080p 问题不大。

启动参数说明:

Web 配置流程

浏览器访问:http://路由器IP:8096

  1. 创建管理员账号

  1. 添加媒体库:
    • 电影 → /media/movies
    • 电视剧 → /media/tv
    • 动漫 → /media/anime

  1. 语言选简体中文,元数据源可以切中文优先(刮削更顺)

之后你可以:

  • 安卓 TV/电视盒装 Jellyfin 客户端
  • 手机、平板、PC 直接 web/客户端访问
  • 家里的所有终端都在用 ARM SBC 这台“小服务器”作为服务器

使用简介

在上面的初始配置执行完成后,jellyfin就初始化好了,我们通过设置的管理员账户登录进去,可以看到如下界面:

我本地之前通过磁力链下载了Minions的的片源,现在直接点击,就可以在线观看了。

默认影片没有信息,我们可以通过刮削元数据,获取封面等信息,更多玩法请查阅jellyfin的官方文档:

核心玩法二:运行ubuntu

有很多服务,依赖完整的ubuntu环境,而不是openwrt的插件方式,这种时候我们可以在openwrt环境下安装docker ubuntu容器,实现拥有一台类似原生ubuntu的环境,实现各种自定义功能。下面以一个基础的Python实现的web服务器作为示例,展示运行容器版本的ubuntu强大的的自定义能力。

拉取镜像

命令执行:

docker pull ubuntu:24.04

启动容器

docker run -it ubuntu:24.04 bash

命令行启动示例:

docker run -it -d \
--name ubt-web \
--restart=unless-stopped \
-p 8080:8000 \
ubuntu:24.04 \
bash

进入容器,并执行简单HTTP服务器的Python代码,示例如下:

docker exec -it ubt-web bash
apt update
apt install python3 python3-pip -y

cat > /srv/app.py << 'EOF'
from http.server import HTTPServer, SimpleHTTPRequestHandler

PORT = 8000
httpd = HTTPServer(("", PORT), SimpleHTTPRequestHandler)
print(f"Serving on port {PORT}...")
httpd.serve_forever()
EOF

# 上面实现的web server root是当前执行python3的路径
python3 /srv/app.py

web访问测试

这个时候,通过http://路由器IP:8000,访问ubuntu容器里面python写的http服务器,会出现文件列表,如下图所示:

附加玩法:全网去广告

首先,拉取adgardhome镜像:

docker pull adguard/adguardhome:latest

然后启动容器,使用AdGuard Home:全家 DNS 去广告

docker run -d \
--name=adguardhome \
--restart=unless-stopped \
-p 3000:3000 \
-p 53:53/tcp \
-p 53:53/udp \
-v /opt/data/config/adguard:/opt/adguardhome/conf \
-v /opt/data/config/adguard/work:/opt/adguardhome/work \
adguard/adguardhome
  • 初始化地址:http://路由器IP:3000

  • 配置好之后,在 OpenWrt 的 LAN DHCP 里把 DNS 指向adguardhome容器的53端口,从而实现基于DNS的广告过滤功能。

更多配置详情,请查阅AdGuard Home官方文档。

FAQ / 踩坑小结

Q1:外网访问怎么弄?

  • 推荐:ZeroTier/Tailscale/FRP 做内网穿透,尽量别直接裸露端口在公网

Q3:备份怎么搞?

  • 必备:/opt/data/config 整个目录(所有服务的配置)
  • 重要数据:/opt/data/media 和需要保留的下载内容
  • 换机只要把这块盘接过去,重新挂载,容器改一下路径就能接着用

Q4:出问题怎么看?

  • docker logs 容器名 看日志
  • docker exec -it 容器名 /bin/sh 进去容器内部排查
  • 检查挂载目录权限、磁盘空间、内存占用这些基础项

参考链接

DshanPI-A1 Weston多屏配置

· 阅读需 11 分钟

在嵌入式系统开发中,显示配置是用户界面实现的基础环节。Weston作为Wayland合成器的参考实现,广泛应用于嵌入式设备和桌面环境。面对不同的硬件配置和应用场景,需要精确控制显示输出。下面基于DShanPi-A1的buildroot固件版本,提供Weston在单屏独占、双屏同显和双屏异显三种典型场景下的配置方法,提供详细的操作指南。

Weston基础架构与配置原理

在进行配置之前,有比较简单了解一下基本原理。

Weston显示系统架构

Weston采用模块化设计,其核心组件包括:

  • 后端(Backend):负责与底层图形系统交互,如DRM、X11、Wayland等
  • 合成器(Compositor):管理窗口合成、渲染和输出
  • Shell:提供用户界面框架,如桌面、面板等

在嵌入式系统中,通常使用DRM后端drm-backend.so,它直接与Linux内核的Direct Rendering Manager交互,提供了高效的硬件加速支持。

配置文件结构解析

Weston的主要配置文件位于/etc/xdg/weston/weston.ini,采用INI格式组织。关键的配置段包括:

  • [core]:核心配置,定义后端行为和全局参数
  • [output]:显示输出配置,控制每个物理接口的属性
  • [shell]:桌面环境相关设置
  • [libinput]:输入设备配置
  • [device]:特定设备的高级配置

HDMI独占显示配置

在某些嵌入式应用场景中,设备需要强制使用HDMI接口作为唯一显示输出,同时禁用内置屏幕(如DSI接口的LCD)。这种配置常见于:

  • 工业控制台固定连接外接显示器
  • 数字标牌系统使用大屏显示
  • 需要高分辨率输出的专业应用

以下为详细的配置

首先需要配置环境变量

export WESTON_DRM_MIRROR=false
export WESTON_DRM_PREFER_EXTERNAL=0
export WESTON_DRM_SINGLE_HEAD=0
export WESTON_DRM_MASTER_OUTPUT="HDMI-A-1"

再写入:/etc/xdg/weston/weston.ini

[core]
backend=drm-backend.so
require-input=true # 必须连接输入设备才能启动
require-outputs=true # 必须检测到输出设备
idle-time=0 # 禁用屏幕休眠
repaint-window=16 # 重绘窗口,约60Hz刷新率

# 禁用自动检测
use-udev=false # 关闭udev自动检测,手动控制输出

[output]
name=HDMI-A-1 # 指定HDMI-A-1接口
mode=1920x1080@60 # 分辨率1080p,60Hz刷新率
transform=normal # 无旋转变换
scale=1.5 # 150%缩放,适应高DPI

# 禁用DSI接口
[output]
name=DSI-1
mode=off # 关闭该输出

[shell]
panel-scale=2 # 面板元素200%缩放
cursor-size=32 # 鼠标指针大小
locking=false # 禁用屏幕锁定
startup-animation=none # 禁用启动动画

[keyboard]
vt-switching=true # 允许虚拟终端切换

[libinput]
touchscreen_calibrator=true # 启用触摸屏校准
enable-tap=true # 启用点击手势
natural-scroll=true # 自然滚动方向

[device]
name=wch.cn USB2IIC_CTP_CONTROL # 特定触摸设备
rotation=normal # 正常方向

测试

启动

77539c1e9693010348396a61507bc998

运行3D测试

5a4bfe7f0ea355b57326d99d83906fa7

在实际应用中,应关闭DSI的驱动输出

7ddd67907df30eb5a1e9e2512861974e

tips:注意了,这里有一个坑,导致我一开始hdmi屏幕无法启动

看这个weston启动输出日志

root@rk3576-buildroot:/# weston
Date: 2025-12-03 UTC
..........
[03:20:26.528] Output HDMI-A-1 (crtc 72) video modes:
1024x600@59.8, preferred, 50.2 MHz
1920x1080@60.0 16:9, 148.5 MHz
1920x1080@59.9 16:9, 148.4 MHz
1920x1080i@60.0, 74.2 MHz
1920x1080i@60.0 16:9, 74.2 MHz
1920x1080i@59.9 16:9, 74.2 MHz
1920x1080@50.0, current, 148.5 MHz
1920x1080@50.0 16:9, 148.5 MHz
1920x1080i@50.0, 74.2 MHz
1920x1080i@50.0 16:9, 74.2 MHz
1280x1024@75.0, 135.0 MHz
1280x720@60.0 16:9, 74.2 MHz
1280x720@59.9 16:9, 74.2 MHz
1280x720@50.0, 74.2 MHz
1280x720@50.0 16:9, 74.2 MHz
1024x768@75.0, 78.8 MHz
1024x768@70.1, 75.0 MHz
1024x768@60.0, 65.0 MHz
832x624@74.6, 57.3 MHz
800x600@75.0, 49.5 MHz
800x600@72.2, 50.0 MHz
800x600@60.3, 40.0 MHz
800x600@56.2, 36.0 MHz
720x576@50.0, 27.0 MHz
720x576@50.0 4:3, 27.0 MHz
720x576@50.0 16:9, 27.0 MHz
720x480@60.0 4:3, 27.0 MHz
720x480@60.0 16:9, 27.0 MHz
720x480@59.9 4:3, 27.0 MHz
720x480@59.9 16:9, 27.0 MHz
640x480@75.0, 31.5 MHz
640x480@72.8, 31.5 MHz
640x480@60.0 4:3, 25.2 MHz
640x480@59.9, 25.2 MHz
720x400@70.1, 28.3 MHz
.....

注意这个分辨率

1024x600@59.8, preferred, 50.2 MH这是我的屏幕的实际物理分辨率,是默认配置,如果直接配置

# 强制指定输出
[output]
name=HDMI-A-1
#mode=1920x1080@50
mode=1024x600@59.8
transform=normal
scale=1.5

启动时:

xkbcommon: ERROR: couldn't find a Compose file for locale "en_US.UTF-8" (mapped to "en_US.UTF-8")
could not create XKB compose table for locale 'en_US.UTF-8'. Disabiling compose
xkbcommon: ERROR: couldn't find a Compose file for locale "en_US.UTF-8" (mapped to "en_US.UTF-8")
could not create XKB compose table for locale 'en_US.UTF-8'. Disabiling compose
[ 695.260819] dwhdmi-rockchip 27da0000.hdmi: use tmds mode
[ 695.279593] rockchip-vop2 27d00000.vop: [drm:vop2_crtc_atomic_enable] Update mode to 1024x600p60, type: 11(if:HDMI0, flag:0x0) for vp0 dclk: 50250000
[ 695.279636] rockchip-hdptx-phy-hdmi 2b000000.hdmiphy: hdptx_ropll_cmn_config bus_width:7aae4 rate:1485000
[ 695.279810] rockchip-hdptx-phy-hdmi 2b000000.hdmiphy: hdptx phy pll locked!
[ 695.279872] rockchip-hdptx-phy-hdmi 2b000000.hdmiphy: hdptx_ropll_cmn_config bus_width:7aae4 rate:502500
[ 695.280061] rockchip-hdptx-phy-hdmi 2b000000.hdmiphy: hdptx phy pll locked!
[ 695.280069] rockchip-vop2 27d00000.vop: [drm:vop2_crtc_atomic_enable] set dclk_vp0 to 50250000, get 50250000
[ 695.280120] dwhdmi-rockchip 27da0000.hdmi: final tmdsclk = 50250000
[ 695.280189] dwhdmi-rockchip 27da0000.hdmi: don't use dsc mode
[ 695.280198] dwhdmi-rockchip 27da0000.hdmi: dw hdmi qp use tmds mode
[ 695.280206] rockchip-hdptx-phy-hdmi 2b000000.hdmiphy: bus_width:0x7aae4,bit_rate:502500
[ 695.285263] rockchip-hdptx-phy-hdmi 2b000000.hdmiphy: hdptx phy lane can't ready!
[ 695.285271] phy phy-2b000000.hdmiphy.4: phy poweron failed --> -22
[ 695.285278] dwhdmi-rockchip 27da0000.hdmi: dw_hdmi_qp_setup hdmi set operation mode failed
[ 695.285317] dwhdmi-rockchip 27da0000.hdmi: Rate 50250000 missing; compute N dynamically
[ 695.286726] dwhdmi-rockchip 27da0000.hdmi: Rate 50250000 missing; compute N dynamically
[ 695.315462] dwhdmi-rockchip 27da0000.hdmi: use tmds mode

注意:

[ 695.285263] rockchip-hdptx-phy-hdmi 2b000000.hdmiphy: hdptx phy lane can't ready!
[ 695.285271] phy phy-2b000000.hdmiphy.4: phy poweron failed --> -22

屏幕是无法启动的,所以这里有一个经验:

在测试屏幕时,物理分辨率可能会不兼容,需要测试多个分辨率,找到兼容的分辨率

关键配置点

use-udev=false的重要性: 默认情况下,Weston通过udev自动检测所有连接的显示设备。设置为false后,Weston将仅使用配置文件中明确指定的输出,这为实现精确控制提供了基础。

输出优先级控制: 当多个[output]段存在时,Weston按配置文件顺序处理。将需要禁用的输出放在激活的输出之后,并设置mode=off,可以确保正确的显示控制。

缩放配置策略: 嵌入式设备通常需要调整UI元素的物理尺寸。通过scale参数可以独立控制每个输出的缩放比例,这对于连接不同DPI的显示器尤为重要。

dsi独占模式

与HDMI独占相反,某些应用需要仅使用设备内置屏幕,如:

  • 移动设备或便携式仪器
  • 节省功耗的电池供电设备
  • 不需要外接显示的应用场景

/etc/xdg/weston/weston.ini

[core]
backend=drm-backend.so

# Allow running without input devices
require-input=false

# Allow running without output devices
require-outputs=none

# Disable screen idle timeout by default
idle-time=0

# 关键:禁用自动检测所有连接
use-udev=false

# The repaint-window is used to calculate repaint delay(ms) after flipped.
# value <= 0: delay = abs(value)
# value > 0: delay = vblank_duration - value
repaint-window=-1

# Allow blending with lower drm planes
# gbm-format=argb8888

[shell]
# top(default)|bottom|left|right|none, none to disable panel
# panel-position=none

# Scale panel size
panel-scale=2

# Set cursor size
cursor-size=32

# none|minutes(default)|minutes-24h|seconds|seconds-24h
# clock-format=minutes-24h
clock-with-date=false

# Disable screen locking
locking=false

# Disable the desktop starting up animation
startup-animation=none

[libinput]
# Uncomment below to enable touch screen calibrator(weston-touch-calibrator)
# touchscreen_calibrator=true
# calibration_helper=/bin/weston-calibration-helper.sh

[keyboard]
# Comment this to enable vt switching
vt-switching=false

# Configs for auto key repeat
# repeat-rate=40
# repeat-delay=400
[output]
name=DSI-1
mode=480x800
transform=rotate-180
scale=0.2

# 明确禁用 DSI-1
[output]
name=HDMI-A-1
mode=off

测试

开机

9315648b71500098278da76036ecfb39

运行3D测试

d568d0a0960f976ed7dcf92e6ca710ff

ab203fb060903387bbc1d57538240d05

关键点

旋转配置: 嵌入式设备的屏幕安装方向可能不同。transform参数支持多种旋转选项:

  • normal:无旋转
  • rotate-90:顺时针90度
  • rotate-180:180度
  • rotate-270:顺时针270度
  • flipped:水平翻转
  • flipped-rotate-180:组合变换

DPI适配策略: 小尺寸高分辨率屏幕需要适当的UI缩放。通过试验不同scale值,找到物理尺寸合适的UI元素大小。本配置中使用0.2(20%)缩放,确保在480x800分辨率下UI元素可正常操作。

双屏同显

双屏同显(镜像模式)适用于:

  • 演示和教学场景
  • 主控台与观察屏同步显示
  • 故障排除和调试

/etc/xdg/weston/weston.ini

[core]
backend=drm-backend.so
require-input=true
require-outputs=true
idle-time=0
repaint-window=16
mode=mirror
use-udev=true # 双屏需要启用udev

[output]
name=HDMI-A-1
mode=1920x1080
transform=rotate-270
scale=0.25

[output]
name=DSI-1
mode=1920x1080
transform=rotate-270
scale=0.25

[shell]
panel-scale=2
cursor-size=32
locking=false
startup-animation=none

[keyboard]
vt-switching=true

[libinput]
touchscreen_calibrator=true
enable-tap=true
natural-scroll=true

# 关键修正:将触摸设备明确绑定到HDMI输出
[device]
name=wch.cn USB2IIC_CTP_CONTROL
output=HDMI-A-1 # 明确指定到HDMI屏幕
rotation=normal # 根据实际方向调整

测试

开机

05738a923f64d53cb97c9657de1e19a5

运行3D测试

8cf877e4c6383eb2342faad0aeeee927

关键点

分辨率对齐: 镜像模式下,两个输出应使用相同的分辨率,否则Weston会以较低分辨率或缩放显示。本配置中统一使用1920x1080,确保显示内容一致。

触摸输入绑定: 在多屏环境中,触摸输入需要明确绑定到特定屏幕。通过output=HDMI-A-1配置,确保触摸操作仅影响HDMI显示,避免在镜像模式下产生混淆。

性能优化考虑: 镜像模式需要合成器渲染相同内容两次,对系统性能有一定影响。适当调整repaint-window参数可以平衡流畅度和系统负载。

双屏异显

双屏异显(扩展模式),适用于:

  • 多任务工作环境
  • 控制面板与数据显示分离
  • 复杂的专业应用界面

以下为配置

weston.ini配置

[core]
backend=drm-backend.so
require-input=true
require-outputs=true
idle-time=0
repaint-window=16
mode=extend # 关键:改为extend模式
use-udev=true

# HDMI屏幕(右侧)
[output]
name=HDMI-A-1
mode=1920x1080
transform=rotate-270
scale=0.25
x=200 # DSI在左侧,从DSI宽度开始
y=0

# DSI屏幕(左侧)
[output]
name=DSI-1
mode=480x800
transform=rotate-270
scale=0.25
x=0
y=0

[shell]
panel-scale=2
cursor-size=32
locking=false
startup-animation=none

[keyboard]
vt-switching=true

[libinput]
touchscreen_calibrator=true
enable-tap=true
natural-scroll=true

[device]
name=wch.cn USB2IIC_CTP_CONTROL
output=HDMI-A-1 # 触摸绑定到HDMI屏幕
rotation=normal

测试

启动时

6906d29c5eed9701b47a55cc1cd32cc6

需要设置环境变量,关闭镜像模式

#环境变量
export WESTON_DRM_MIRROR=0 # 关闭镜像模式
export WESTON_DRM_PREFER_EXTERNAL=0 # 不优先外部显示
export WESTON_DRM_SINGLE_HEAD=0 # 启用多head支持
pkill weston
weston &

2d0d04ea8fc8f56a40711aaaa0a6f790

启动后,系统识别两个独立显示器,桌面可以跨屏延伸。每个屏幕可以运行不同的应用程序,实现真正的多任务环境。

关键点

屏幕排列控制: Weston默认的屏幕排列可能不符合实际物理布局。可以通过weston.ini中的位置参数或启动后手动调整来优化。

跨屏窗口管理: 在扩展模式下,窗口可以在屏幕间移动。需要确保窗口管理器和应用程序支持多屏幕环境。

性能考量: 扩展模式对图形性能要求更高,特别是当两个屏幕分辨率差异较大时。需要根据硬件能力调整渲染设置。

总结

Weston多屏配置虽然有一定复杂性,但通过深入理解其配置原理和掌握关键参数,可以实现高度定制化的显示解决方案。无论是单屏独占、双屏同显还是双屏异显,都需要综合考虑硬件特性、应用需求和用户体验。

实际配置过程中,建议采取逐步测试的方法:先从基本配置开始,验证单个功能,然后逐步添加复杂特性。

DshanPI-A1buildroot下搭建RKNN环境

· 阅读需 15 分钟

开发环境

PC端: ubuntu22.04-x86-64

板端:buildroot

具体理论部分不再赘述,网上一大把,现在记录整个操作过程,分为两大部分:1.PC端 2.板端

1.PC端

RKNN-Toolkit2环境搭建

#代码库下载
mkdir rknn
cd rknn
wget https://dl.100ask.net/Hardware/MPU/RK3576-DshanPi-A1/utils/rknn-toolkit2.zip
unzip rknn-toolkit2.zip
wget https://dl.100ask.net/Hardware/MPU/RK3576-DshanPi-A1/utils/rknn_model_zoo.zip
unzip rknn_model_zoo.zip
#coda环境搭建
wget -c https://repo.anaconda.com/archive/Anaconda3-2025.06-1-Linux-x86_64.sh
bash Anaconda3-2025.06-1-Linux-x86_64.sh
Please, press ENTER to continue
>>>
Do you accept the license terms? [yes|no]
>>> yes
Anaconda3 will now be installed into this location:
/home/ubuntu/anaconda3
- Press ENTER to confirm the location
- Press CTRL-C to abort the installation
- Or specify a different location below

[/home/ubuntu/anaconda3] >>>
You can undo this by running `conda init --reverse $SHELL`? [yes|no]
[no] >>> yes
Thank you for installing Anaconda3!
#激活环境变量
source ~/.bashrc
#创建RKNN环境
conda create -n rknn-toolkit2 python=3.8
conda activate rknn-toolkit2
#安装RKNN-Toolkit2
cd rknn-toolkit2/rknn-toolkit2/packages/x86_64/
conda install compilers cmake
pip install -r requirements_cp38-2.3.2.txt
pip install rknn_toolkit2-2.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
#验证安装情况
(rknn-toolkit2) ubuntu@ubuntu-2204:~/rknn/rknn-toolkit2/rknn-toolkit2/packages/x86_64$ python3
Python 3.8.20 (default, Oct 3 2024, 15:24:27)
[GCC 11.2.0] :: Anaconda, Inc. on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from rknn.api import RKNN
>>> exit()
(rknn-toolkit2) ubuntu@ubuntu-2204:~/rknn/rknn-toolkit2/rknn-toolkit2/packages/x86_64$

2.连扳推理

首先给出我的环境,我使用的是sdk自带的buildroot,由百问网团队进行了适配,已经开启rknnruntime,并且我没有更新runtime

root@rk3576-buildroot:/# uname -a
Linux rk3576-buildroot 6.1.75 #3 SMP Fri Nov 28 09:41:14 EST 2025 aarch64 GNU/Linux
root@rk3576-buildroot:/# find ./ -name *rknn*
./rockchip-test/npu2/model/RK356X/mobilenet_v1.rknn
./rockchip-test/npu2/model/RK3588/vgg16_max_pool_fp16.rknn
./sys/kernel/debug/clk/hclk_rknn_root
./sys/kernel/debug/clk/clk_rknn_dsu0
./sys/kernel/debug/clk/aclk_rknn0
./sys/kernel/debug/clk/aclk_rknn1
./sys/kernel/debug/clk/aclk_rknn_cbuf
./sys/kernel/debug/clk/hclk_rknn_cbuf
./usr/share/model/RK3562/mobilenet_v1.rknn
./usr/share/model/RK3566_RK3568/mobilenet_v1.rknn
./usr/share/model/RK3588/mobilenet_v1.rknn
./usr/share/model/RK3576/mobilenet_v1.rknn
./usr/lib/librknnrt.so
./usr/bin/start_rknn.sh
./usr/bin/rknn_common_test
./usr/bin/restart_rknn.sh
./usr/bin/rknn_server

**tip:**我的buildroot配置./build.sh bconfig

[*] Rockchip NPU power control for linux │ │
│ │ [ ] Rockchip NPU power control combine for linux │ │
│ │ [ ] Rockchip recovery for linux │ │
│ │ [ ] rkadk │ │
│ │ [ ] rknpu │ │
│ │ [ ] rknpu pcie │ │
│ │ [ ] python-rknn │ │
│ │ [*] rknpu2 │ │
│ │ [*] rknpu2 example │ │
│ │ [ ] rknpu firmware │ │
│ │ [ ] RKPARTYBOX demo │ │
│ │ [*] rockchip script

可以看到,rknpu的配置我没有改动,buildroot已经默认配置好rknpu驱动和rknnruntime

问题1:解决pc端adb权限问题

在pc上进行连扳推理,会报如下错误:

:(rknn-toolkit2) ubuntu@ubuntu-2204:~/rknn/rknn_model_zoo/examples/yolov8/python$ sudo python3 yolov8.py --target rk3576 --model_path ../model/yolov8.rknn --img_show
Traceback (most recent call last):
File "/home/ubuntu/rknn/rknn_model_zoo/examples/yolov8/python/yolov8.py", line 2, in <module>
import cv2
ModuleNotFoundError: No module named 'cv2'
(rknn-toolkit2) ubuntu@ubuntu-2204:~/rknn/rknn_model_zoo/examples/yolov8/python$ python3 yolov8.py --target rk3576 --model_path ../model/yolov8.rknn --img_show
I rknn-toolkit2 version: 2.3.2
--> Init runtime environment
adb: unable to connect for root: insufficient permissions for device: user in plugdev group; are your udev rules wrong?
See [http://developer.android.com/tools/device.html] for more information
I target set by user is: rk3576
E init_runtime: Get board target failed, ret code: 1. error: insufficient permissions for device: user in plugdev group; are your udev rules wrong?
See [http://developer.android.com/tools/device.html] for more information

E init_runtime: Traceback (most recent call last):
File "rknn/api/rknn_log.py", line 344, in rknn.api.rknn_log.error_catch_decorator.error_catch_wrapper
File "rknn/api/rknn_base.py", line 2566, in rknn.api.rknn_base.RKNNBase.init_runtime
File "rknn/api/rknn_runtime.py", line 223, in rknn.api.rknn_runtime.RKNNRuntime.__init__
File "rknn/api/rknn_platform.py", line 607, in rknn.api.rknn_platform.get_board_info
RuntimeError

注意这一句

adb: unable to connect for root: insufficient permissions for device: user in plugdev group; are your udev rules wrong?
See [http://developer.android.com/tools/device.html] for more information

提示adbd权限不足,修改

# 添加udev规则
echo 'SUBSYSTEM=="usb", ATTR{idVendor}=="2207", MODE="0666", GROUP="plugdev"' | sudo tee /etc/udev/rules.d/51-android.rules
# 重新加载规则
sudo udevadm control --reload-rules
sudo udevadm trigger
# 重启ADB
adb kill-server
adb start-server
adb devices
List of devices attached
8074683be1050187 device

问题2:解决板端adbd5037端口未打开问题

现象:

#进行连扳推理
python3 yolov8.py --target rk3576 --model_path ../model/yolov8.rknn --img_show
I rknn-toolkit2 version: 2.3.2
--> Init runtime environment
adbd is already running as root
I target set by user is: rk3576
I Get hardware info: target_platform = rk3576, os = Linux, aarch = aarch64
I Check RK3576 board npu runtime version
W kill server failed while restarting, ret code: 1. warning: killall: rknn_server: no process killed
Please skip it if rknn_server not running on board.
I Starting ntp or adb, target is RK3576
I Start adb...
I Connect to Device success!
I NPUTransfer(3672747): Starting NPU Transfer Client, Transfer version 2.2.2 (12abf2a@2024-09-02T03:22:41)
E RKNNAPI: rknn_init, server connect fail! ret = -9(ERROR_PIPE)!
E init_runtime: The rknn_server on the concected device is abnormal, please start the rknn_server on the device according to:
https://github.com/airockchip/rknn-toolkit2/blob/master/doc/rknn_server_proxy.md
W init_runtime: ===================== WARN(1) =====================
E rknn-toolkit2 version: 2.3.2
E init_runtime: Traceback (most recent call last):
File "rknn/api/rknn_log.py", line 344, in rknn.api.rknn_log.error_catch_decorator.error_catch_wrapper

注意

I NPUTransfer(3672747): Starting NPU Transfer Client, Transfer version 2.2.2 (12abf2a@2024-09-02T03:22:41)
E RKNNAPI: rknn_init, server connect fail! ret = -9(ERROR_PIPE)!
E init_runtime: The rknn_server on the concected device is abnormal, please start the rknn_server on the device according to:
https://github.com/airockchip/rknn-toolkit2/blob/master/doc/rknn_server_proxy.md

参考提示,去 https://github.com/airockchip/rknn-toolkit2/blob/master/doc/rknn_server_proxy.md查找解决方案

在这个文档的

6. 常见问题

问题1

Debian系统上rknn_server服务已经后台启动, 但是连板推理时依旧有如下报错:

D NPUTransfer: ERROR: socket read fd = 4, n = -1: Connection reset by peer
D NPUTransfer: Transfer client closed, fd = 4
E RKNNAPI: rknn_init, server connect fail! ret = -9(ERROR_PIPE)!
E build_graph: The rknn_server on the concected device is abnormal, please start the rknn_server on the device according to:
https://github.com/airockchip/rknn-toolkit2/blob/master/doc/rknn_server_proxy.md

解决方法: 这通常是由于Debian固件上的adbd程序没有监听5037端口导致的,可以在板子上执行以下命令来判断:

netstat -n -t -u -a

如果输出结果中没有5037端口,则执行下列命令下载和更新adbd程序, 并重启板子;否则,跳过下列步骤。

wget -O adbd.zip https://ftzr.zbox.filez.com/v2/delivery/data/7f0ac30dfa474892841fcb2cd29ad924/adbd.zip
unzip adbd.zip
adb push adbd/linux-aarch64/adbd /usr/bin/adbd

进入设备shell命令,增加adbd的可执行权限

adb shell "chmod +x /usr/bin/adbd"
adb reboot

重启设备后,按照启动步骤启动rknn_server服务,再次尝试连板推理。

虽然这个文档用的是Debian固件,我用的是buildroot,现象一直,按照它的解决方案,排查问题:

#这是板端
root@rk3576-buildroot:/# netstat -n -t -u -a
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 0.0.0.0:53 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN
tcp 0 0 :::53 :::* LISTEN
tcp 0 0 :::22 :::* LISTEN
tcp 0 0 :::5555 :::* LISTEN
udp 0 0 0.0.0.0:53 0.0.0.0:*
udp 0 0 0.0.0.0:67 0.0.0.0:*
udp 0 0 0.0.0.0:68 0.0.0.0:*
udp 0 0 127.0.0.1:323 0.0.0.0:*
udp 0 0 :::53 :::*
udp 0 0 ::1:323 :::*
udp 0 0 :::546 :::*

可以看到5037端口确实没有打开

#这是pc端
wget -O adbd.zip https://ftzr.zbox.filez.com/v2/delivery/data/7f0ac30dfa474892841fcb2cd29ad924/adbd.zip
unzip adbd.zip
adb push adbd/linux-aarch64/adbd /usr/bin/adbd
adb shell "chmod +x /usr/bin/adbd"
adb reboot

重启开发板后重新测试

#这是板端
restart_rknn.sh
root@rk3576-buildroot:/# netstat -n -t -u -a
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 127.0.0.1:5037 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:53 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:5555 0.0.0.0:* LISTEN
tcp 0 0 :::22 :::* LISTEN
tcp 0 0 :::53 :::* LISTEN
udp 0 0 0.0.0.0:53 0.0.0.0:*
udp 0 0 0.0.0.0:67 0.0.0.0:*
udp 0 0 0.0.0.0:68 0.0.0.0:*
udp 0 0 127.0.0.1:323 0.0.0.0:*
udp 0 0 :::546 :::*
udp 0 0 :::53 :::*
udp 0 0 ::1:323 :::*

可以看到5037端口已经被监听,现在继续尝试连扳推理

#这是pc端
(rknn-toolkit2) ubuntu@ubuntu-2204:~/rknn/rknn_model_zoo/examples/yolov8/python$ python3 yolov8.py --target rk3576 --model_path ../model/yolov8.rknn --img_show
I rknn-toolkit2 version: 2.3.2
--> Init runtime environment
adb: unable to connect for root: closed
I target set by user is: rk3576
I Get hardware info: target_platform = rk3576, os = Linux, aarch = aarch64
I Check RK3576 board npu runtime version
I Starting ntp or adb, target is RK3576
I Start adb...
I Connect to Device success!
I NPUTransfer(3675220): Starting NPU Transfer Client, Transfer version 2.2.2 (12abf2a@2024-09-02T03:22:41)
I NPUTransfer(3675220): TransferBuffer: min aligned size: 1024
D RKNNAPI: ==============================================
D RKNNAPI: RKNN VERSION:
D RKNNAPI: API: 2.3.2 (1842325 build@2025-03-30T09:55:23)

已经连扳推理成功,显示出人脸识别图像

b70deb69-aaeb-4e8f-a09f-c77091008511

3.板端推理

由于使用的是buildroot,rk平台在rk1808之后,buildroot并未支持python部署方式,自行搭建python推理软件栈颇为耗时,且会遇到很多问题,python demo参考百问网RKNN环境搭建 | 东山Π这里不再赘述,使用cpp接口,还是使用上述的yolo8进行板端推理:

准备模型

cd ~/rknn/rknn_model_zoo/examples/yolov8/model
sh download_model.sh
ls -lah yolov8n.onnx
-rw-rw-r-- 1 ubuntu ubuntu 13M Nov 29 07:36 yolov8n.onnx

模型转换

cd ../python/
(rknn-toolkit2) ubuntu@ubuntu-2204:~/rknn/rknn_model_zoo/examples/yolov8/python$ python convert.py ../model/yolov8n.onnx rk3576 i8 ../model/yolov8n.rknn

python convert.py ../model/yolov5s_relu.onnx rk3576 i8 ../model/yolov5s_relu.rknn

I rknn-toolkit2 version: 2.3.2
--> Config model
done
--> Loading model
I Loading : 100%|██████████████████████████████████████████████| 126/126 [00:00<00:00, 43282.74it/s]
done
--> Building model
I OpFusing 0: 100%|█████████████████████████████████████████████| 100/100 [00:00<00:00, 1590.15it/s]
I OpFusing 1 : 100%|█████████████████████████████████████████████| 100/100 [00:00<00:00, 860.03it/s]
I OpFusing 0 : 100%|█████████████████████████████████████████████| 100/100 [00:00<00:00, 739.11it/s]
I OpFusing 1 : 100%|█████████████████████████████████████████████| 100/100 [00:00<00:00, 652.15it/s]
I OpFusing 2 : 100%|█████████████████████████████████████████████| 100/100 [00:00<00:00, 229.74it/s]
W build: found outlier value, this may affect quantization accuracy
const name abs_mean abs_std outlier value
model.0.conv.weight 2.44 2.47 -17.494
model.22.cv3.2.1.conv.weight 0.09 0.14 -10.215
model.22.cv3.1.1.conv.weight 0.12 0.19 13.361, 13.317
model.22.cv3.0.1.conv.weight 0.18 0.20 -11.216
I GraphPreparing : 100%|████████████████████████████████████████| 161/161 [00:00<00:00, 3291.17it/s]
I Quantizating : 100%|████████████████████████████████████████████| 161/161 [00:04<00:00, 34.87it/s]
W build: The default input dtype of 'images' is changed from 'float32' to 'int8' in rknn model for performance!
Please take care of this change when deploy rknn model with Runtime API!
W build: The default output dtype of '318' is changed from 'float32' to 'int8' in rknn model for performance!
Please take care of this change when deploy rknn model with Runtime API!
W build: The default output dtype of 'onnx::ReduceSum_326' is changed from 'float32' to 'int8' in rknn model for performance!
Please take care of this change when deploy rknn model with Runtime API!
W build: The default output dtype of '331' is changed from 'float32' to 'int8' in rknn model for performance!
Please take care of this change when deploy rknn model with Runtime API!
W build: The default output dtype of '338' is changed from 'float32' to 'int8' in rknn model for performance!
Please take care of this change when deploy rknn model with Runtime API!
W build: The default output dtype of 'onnx::ReduceSum_346' is changed from 'float32' to 'int8' in rknn model for performance!
Please take care of this change when deploy rknn model with Runtime API!
W build: The default output dtype of '350' is changed from 'float32' to 'int8' in rknn model for performance!
Please take care of this change when deploy rknn model with Runtime API!
W build: The default output dtype of '357' is changed from 'float32' to 'int8' in rknn model for performance!
Please take care of this change when deploy rknn model with Runtime API!
W build: The default output dtype of 'onnx::ReduceSum_365' is changed from 'float32' to 'int8' in rknn model for performance!
Please take care of this change when deploy rknn model with Runtime API!
W build: The default output dtype of '369' is changed from 'float32' to 'int8' in rknn model for performance!
Please take care of this change when deploy rknn model with Runtime API!
I rknn building ...
I rknn building done.
done
--> Export rknn model
done
cd ../model
ls -lah yolov8n.rknn
-rw-rw-r-- 1 ubuntu ubuntu 6.2M Nov 29 07:39 yolov8n.rknn

运行RKNN C example

首先需要编译C example,然后将可执行文件 模型文件 资源文件部署到板端

编译

编译要使用rknn_model_zoo目录下的build-linux.sh脚本,需要先配置工具链。修改build-linux.sh

GCC_COMPILER=/home/ubuntu/rk3576/prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu
chmod +x ./build-linux.sh
./build-linux.sh -t rk3576 -a aarch64 -d yolov8
-- Set runtime path of "/home/ubuntu/rknn/rknn_model_zoo/install/rk3576_linux_aarch64/rknn_yolov8_demo/./rknn_yolov8_demo" to "$ORIGIN/../lib"
-- Installing: /home/ubuntu/rknn/rknn_model_zoo/install/rk3576_linux_aarch64/rknn_yolov8_demo/model/bus.jpg
-- Installing: /home/ubuntu/rknn/rknn_model_zoo/install/rk3576_linux_aarch64/rknn_yolov8_demo/model/coco_80_labels_list.txt
-- Installing: /home/ubuntu/rknn/rknn_model_zoo/install/rk3576_linux_aarch64/rknn_yolov8_demo/model/yolov8.rknn
-- Installing: /home/ubuntu/rknn/rknn_model_zoo/install/rk3576_linux_aarch64/rknn_yolov8_demo/model/yolov8n.rknn
-- Installing: /home/ubuntu/rknn/rknn_model_zoo/install/rk3576_linux_aarch64/rknn_yolov8_demo/lib/librknnrt.so
-- Installing: /home/ubuntu/rknn/rknn_model_zoo/install/rk3576_linux_aarch64/rknn_yolov8_demo/lib/librga.so
#查看一下
ubuntu@ubuntu-2204:~/rknn/rknn_model_zoo/install$ tree -L 4
.
└── rk3576_linux_aarch64
└── rknn_yolov8_demo
├── lib
│   ├── librga.so
│   └── librknnrt.so
├── model
│   ├── bus.jpg
│   ├── coco_80_labels_list.txt
│   ├── yolov8n.rknn
│   └── yolov8.rknn
├── rknn_yolov8_demo
└── rknn_yolov8_demo_zero_copy

这个rknn_yolov8_demo就是要部署到板端的文件集合

adb push install/rk3576_linux_aarch64/rknn_yolov8_demo /data/
install/rk3576_linux_aarch64/rknn_yolov8_demo/: 8 files pushed. 3.6 MB/s (23008641 bytes in 6.049s)

在板端运行

root@rk3576-buildroot:/data/rknn_yolov8_demo# ./rknn_yolov8_demo ./model/yolov8.rknn ./model/bus.jpg
load lable ./model/coco_80_labels_list.txt
model input num: 1, output num: 9
input tensors:
index=0, name=images, n_dims=4, dims=[1, 640, 640, 3], n_elems=1228800, size=1228800, fmt=NHWC, type=INT8, qnt_type=AFFINE, zp=-128, scale=0.003922
output tensors:
index=0, name=318, n_dims=4, dims=[1, 64, 80, 80], n_elems=409600, size=409600, fmt=NCHW, type=INT8, qnt_type=AFFINE, zp=-58, scale=0.117659
index=1, name=onnx::ReduceSum_326, n_dims=4, dims=[1, 80, 80, 80], n_elems=512000, size=512000, fmt=NCHW, type=INT8, qnt_type=AFFINE, zp=-128, scale=0.003104
index=2, name=331, n_dims=4, dims=[1, 1, 80, 80], n_elems=6400, size=6400, fmt=NCHW, type=INT8, qnt_type=AFFINE, zp=-128, scale=0.003173
index=3, name=338, n_dims=4, dims=[1, 64, 40, 40], n_elems=102400, size=102400, fmt=NCHW, type=INT8, qnt_type=AFFINE, zp=-45, scale=0.093747
index=4, name=onnx::ReduceSum_346, n_dims=4, dims=[1, 80, 40, 40], n_elems=128000, size=128000, fmt=NCHW, type=INT8, qnt_type=AFFINE, zp=-128, scale=0.003594
index=5, name=350, n_dims=4, dims=[1, 1, 40, 40], n_elems=1600, size=1600, fmt=NCHW, type=INT8, qnt_type=AFFINE, zp=-128, scale=0.003627
index=6, name=357, n_dims=4, dims=[1, 64, 20, 20], n_elems=25600, size=25600, fmt=NCHW, type=INT8, qnt_type=AFFINE, zp=-34, scale=0.083036
index=7, name=onnx::ReduceSum_365, n_dims=4, dims=[1, 80, 20, 20], n_elems=32000, size=32000, fmt=NCHW, type=INT8, qnt_type=AFFINE, zp=-128, scale=0.003874
index=8, name=369, n_dims=4, dims=[1, 1, 20, 20], n_elems=400, size=400, fmt=NCHW, type=INT8, qnt_type=AFFINE, zp=-128, scale=0.003922
model is NHWC input fmt
model input height=640, width=640, channel=3
origin size=640x640 crop size=640x640
input image: 640 x 640, subsampling: 4:2:0, colorspace: YCbCr, orientation: 1
scale=1.000000 dst_box=(0 0 639 639) allow_slight_change=1 _left_offset=0 _top_offset=0 padding_w=0 padding_h=0
rga_api version 1.10.1_[0]
rknn_run
person @ (211 241 282 507) 0.864
person @ (109 235 225 536) 0.856
bus @ (99 136 552 455) 0.856
person @ (476 223 560 521) 0.848
person @ (80 326 116 513) 0.280
write_image path: out.png width=640 height=640 channel=3 data=0x3e2e9200

在pc端查看

adb pull /data/rknn_yolov8_demo/out.png ./

image-20251129221548372

符合预期

至此,rknn环境搭建成功

RK3576 实现sox降噪和rnnoise降噪

· 阅读需 12 分钟

概述

音频降噪技术的发展,经历了从对抗物理噪声到智能识别分离的演变。

模拟时代(20世纪中期)

  • 杜比A型(1965年):开创性的动态降噪技术,采用“压缩-扩张”原理降低磁带本底噪声。
  • 杜比B型(1968年):A型的消费简化版,让卡式磁带走入千家万户。
  • dbx(1971年):更激进的压缩-扩张系统,动态范围更大。
  • 后续发展:Dolby C、SR等更先进的模拟系统相继问世。

数字时代早期(1980-1990年代)

  • 数字信号处理芯片的出现,让实时数字滤波和谱减法成为可能
  • 算法开始从时域转向频域处理
  • 自适应滤波理论成熟并应用于通信领域

数字算法普及期(1990-2010年代)

  • 个人电脑性能提升,专业音频软件(如Audition、iZotope RX)普及了数字降噪工具
  • 心理声学模型的应用提升了降噪音质
  • 主动降噪耳机开始商业化(Bose等厂商推动)

人工智能时代(2010年代至今)

  • 深度学习彻底改变了降噪方式,能够处理复杂的非平稳噪声
  • 技术广泛应用于视频会议、语音助手、音乐流媒体等领域
  • 研究方向从单纯降噪扩展到语音分离、人声提取等精细化任务

主要降噪方法

基于频谱的降噪 核心思路是在频域上区分噪声和信号:

  • 谱减法:从音频中减去噪声频谱
  • 维纳滤波:更优的统计降噪方法
  • 掩蔽效应法:利用人耳特性保留音质

机器学习降噪

  • 使用深度学习模型(RNN、CNN、Transformer等)从数据中学习降噪
  • 能够有效处理复杂场景和非平稳噪声
  • 支持端到端的波形或频谱处理

滤波降噪

  • 自适应滤波:需要参考噪声信号,用于电话和主动降噪耳机
  • 固定滤波:去除特定频率噪声,如50Hz工频干扰

多麦克风技术

  • 通过麦克风阵列形成指向性波束
  • 增强目标方向声音,抑制环境噪声
  • 常见于手机、会议设备和智能音箱

传统处理方法

  • 噪声门:通过阈值静音低电平信号
  • 简单有效,常用于音乐制作和直播场景

下面使用两种方法来进行音频降噪的处理:sox降噪rnnoise降噪

sox降噪

sox的 noisered 是一个经典的、基于噪声采样和谱减法的非AI降噪工具,原理直观,实现相对简单,对稳态噪声(如风扇声、空调声、恒定电流声)效果显著,noisered 效果器是其降噪的核心,核心原理步骤如下:

sox
  1. 分析/训练阶段:核心是建立噪声指纹。SoX需要先“学习”噪声是什么样的。可以提供一段纯噪声片段(例如录音前的环境底噪),或者让它自动检测音频中能量较低的“静音”部分。它会分析这些片段,计算出一个平均噪声频谱,并保存为一个.prof文件。这个文件就是后续降噪的参考基准。
  2. 降噪处理阶段:核心是频谱减法
    • 分帧与变换:将连续的音频信号切成短时重叠的帧,并通过FFT转换到频域。在频域,信号表现为不同频率的能量(幅度)和相位。
    • 关键操作 - 谱减法:这是最核心的一步。算法会比较当前帧的频谱和之前学到的噪声样本频谱。基本思想非常简单:干净信号频谱 ≈ 带噪信号频谱 - 噪声频谱
      • 能量相减:主要操作是在能量/幅度谱上进行减法。当前帧某个频率的能量如果低于或接近噪声样本在该频率的能量,就会被大幅抑制;如果远高于,则会被保留。
      • 相位保留:相位信息对于重建声音波形至关重要。谱减法通常不改变原始信号的相位,直接用降噪后的幅度谱和原始相位谱合成新信号。
      • 抑制因子:SoX的 amount 参数(0.0 到 1.0)控制减法力度。0.5意味着只减去一半的噪声能量,更为保守,能减少失真。
    • 合成与输出:处理后的频域数据通过IFFT变回时域波形,再通过重叠相加方法将短时帧合成连续的音频信号,最终得到降噪后的音频。

看起来很复杂,但操作起来很简单,现在使用sox命令操纵一下就明白了。

sox命令降噪

#noise.wav是背景噪声,在安静状态下录制的一段10s的音频
ls
noise.wav speech.wav
#生成噪声profile(手动截取纯噪声5-10秒)
sox noise.wav -n noiseprof noise.prof
#降噪,0.21是经验值,绝大部分素材不会出现水声
sox speech.wav noisered_speech.wav noisered noise.prof 0.21
#对比播放,可以看出效果明显
aplay noise.wav
Playing WAVE 'noise.wav' : Signed 16 bit Little Endian, Rate 8000 Hz, Stereo
aplay speech.wav
Playing WAVE 'speech.wav' : Signed 16 bit Little Endian, Rate 8000 Hz, Stereo
aplay noisered_speech.wav
Playing WAVE 'noisered_speech.wav' : Signed 16 bit Little Endian, Rate 8000 Hz, Stereo

只是使用sox命令降噪,无法集成到嵌入式系统中,下面采用C语言,使用sox的库和API来进行音频降噪。

使用sox的库和API来进行音频降噪

代码结构如下:

(base) ubuntu@ubuntu-2204:~/baiwen/sox_noise_reduction$ tree -L 2
.
├── CMakeLists.txt
├── deps
│   ├── include
│   └── lib
├── include
│   ├── custom_effects.h
│   └── sox_noise_reduction.h
├── Makefile
└── src
├── custom_effects.c
├── main.c
└── sox_noise_reduction.c

5 directories, 7 files

编译

要编译这个demo,需要设置cmakelist.txt中得工具链

# 指定交叉编译器
set(CMAKE_C_COMPILER /home/ubuntu/rk3576_AI/buildroot/output/rockchip_rk3576/host/bin/aarch64-buildroot-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER /home/ubuntu/rk3576_AI/buildroot/output/rockchip_rk3576/host/bin/aarch64-buildroot-linux-gnu-g++)

# 设置sysroot
set(CMAKE_SYSROOT /home/ubuntu/rk3576_AI/buildroot/output/rockchip_rk3576/staging)
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})

# 添加链接库
target_link_libraries(sox_noise_reduction
sox
)
#ubuntu22.04编译
(base) ubuntu@ubuntu-2204:~/baiwen/sox_noise_reduction$ make
Configuring CMake...
-- The C compiler identification is GNU 11.4.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /home/ubuntu/baiwen/sox_noise_reduction/build
Building with 16 jobs...
make[1]: Entering directory '/home/ubuntu/baiwen/sox_noise_reduction/build'
make[2]: Entering directory '/home/ubuntu/baiwen/sox_noise_reduction/build'
make[3]: Entering directory '/home/ubuntu/baiwen/sox_noise_reduction/build'
make[3]: Leaving directory '/home/ubuntu/baiwen/sox_noise_reduction/build'
make[3]: Entering directory '/home/ubuntu/baiwen/sox_noise_reduction/build'
[ 75%] Building C object CMakeFiles/sox_noise_reduction.dir/src/sox_noise_reduction.c.o
[ 75%] Building C object CMakeFiles/sox_noise_reduction.dir/src/custom_effects.c.o
[ 75%] Building C object CMakeFiles/sox_noise_reduction.dir/src/main.c.o
[100%] Linking C executable sox_noise_reduction
make[3]: Leaving directory '/home/ubuntu/baiwen/sox_noise_reduction/build'
[100%] Built target sox_noise_reduction
make[2]: Leaving directory '/home/ubuntu/baiwen/sox_noise_reduction/build'
make[1]: Leaving directory '/home/ubuntu/baiwen/sox_noise_reduction/build'

#make push会通过adb 将sox_noise_reduction可执行文件推送到buildroot的/develo
(base) ubuntu@ubuntu-2204:~/baiwen/sox_noise_reduction$ make push
Running build/sox_noise_reduction...
build/sox_noise_reduction: 1 file pushed. 10.6 MB/s (23840 bytes in 0.002s)

运行

在rk3576上

root@rk3576-buildroot:/develop# ls
live_life.mp3 noise.wav sox_noise_reduction speech.wav

先在安静环境下录制一段 noise.wav的背景噪声,再对着麦克风说一段话speech.wav

root@rk3576-buildroot:/develop# ./sox_noise_reduction
SoX音频噪声降低处理开始
=======================
噪声文件: noise.wav
输入文件: speech.wav
输出文件: noisered_output.wav
降噪敏感度: 0.21
-----------------------
Processing...
[1/2] Creating noise profile...
Creating noise profile from: noise.wav
Processing noise profile...
Noise profile created: /tmp/noise_profile_NFk1uN.prof
[2/2] Applying noise reduction...
Applying noise reduction to: speech.wav
Processing audio with noise reduction...
Noise reduction applied. Output: noisered_output.wav
Done!
处理成功! 输出文件: noisered_output.wav
#使用aplay播放可以对比
aplay noise.wav
aplay speech.wav
aplay noisered_output.wav

可以听到 noisered_output.wav明显改善,但还有一些微弱的**“音乐噪声”,这是采用sox谱减法降噪固有的副作用,要完全去除,需要再设计一个维纳滤波**效果插件,进一步去除。

sox_noise_reduction的使用可以看-h的帮助信息

root@rk3576-buildroot:/develop# ./sox_noise_reduction -h
SoX音频噪声降低工具
====================

使用方法: ./sox_noise_reduction [选项]

选项:
-n 文件 噪声样本文件 (默认: noise.wav)
-i 文件 输入语音文件 (默认: speech.wav)
-o 文件 输出文件 (默认: noisered_output.wav)
-s 数值 降噪敏感度 0.0-1.0 (默认: 0.21)
-h 显示此帮助信息

参数说明:
敏感度: 0.0表示最强降噪(可能损伤语音),1.0表示最弱降噪
推荐值: 0.21 (经验值,适用于大多数音频素材)

使用示例:
./sox_noise_reduction # 使用所有默认设置
./sox_noise_reduction -n mynoise.wav # 指定噪声文件
./sox_noise_reduction -i myspeech.wav # 指定输入文件
./sox_noise_reduction -s 0.3 # 调整降噪强度
./sox_noise_reduction -o clean.wav # 指定输出文件

rnnoise降噪

RNNoise 是一个优秀的开源音频降噪工具,非常适合入门学习、快速集成和中等性能需求的场景。

  1. 技术原理
    • 结合传统信号处理(谱分析)与深度学习(RNN神经网络)。
    • 输入音频分帧→提取特征(如频带能量、基音)→RNN预测每个频带的增益掩码(VAD也集成在其中)→输出降噪后的频谱→重建波形。
  2. 轻量化设计
    • 模型仅约86KB,适合嵌入式或实时处理。
    • 单核CPU即可实时处理。

优点

  • 开源且易用:代码清晰,提供C语言API,易于集成到各类项目。
  • 低延迟:帧处理延迟约10ms(默认帧长20ms),适合实时通信。
  • 兼容性强:无第三方深度学习框架依赖,纯C实现。
  • 语音保护较好:在抑制稳态噪声(如风扇声)的同时,对语音损伤较小。

局限性

  • 非通用降噪:主要针对语音通信优化,对音乐、突发噪声(如键盘声)效果有限。
  • 参数固定:模型为通用场景训练,难以针对特定噪声定制。
  • 残留“音乐噪声”:某些场景下可能引入类似水波的残留噪声。
  • 不支持高采样率:默认仅支持48kHz/16kHz单声道,音乐降噪需调整。

编译

## 设置交叉编译工具链路径
export TOOLCHAIN_DIR="/home/ubuntu/rk3576_AI/buildroot/output/rockchip_rk3576/host/bin"
export CC="${TOOLCHAIN_DIR}/aarch64-buildroot-linux-gnu-gcc"
export CXX="${TOOLCHAIN_DIR}/aarch64-buildroot-linux-gnu-g++"
export AR="${TOOLCHAIN_DIR}/aarch64-buildroot-linux-gnu-ar"
export LD="${TOOLCHAIN_DIR}/aarch64-buildroot-linux-gnu-ld"
export RANLIB="${TOOLCHAIN_DIR}/aarch64-buildroot-linux-gnu-ranlib"
export STRIP="${TOOLCHAIN_DIR}/aarch64-buildroot-linux-gnu-strip"

# 设置目标架构
export ARCH="aarch64"
export CROSS_COMPILE="aarch64-buildroot-linux-gnu-"

# 设置sysroot(重要!)
export SYSROOT="/home/ubuntu/rk3576_AI/buildroot/output/rockchip_rk3576/staging"
export CFLAGS="--sysroot=${SYSROOT} -O2"
export LDFLAGS="--sysroot=${SYSROOT}"

echo "工具链设置完成"
echo "CC = $CC"

# 克隆项目并编译
git clone https://github.com/xiph/rnnoise.git
cd rnnoise
./autogen.sh
./configure \
--host=aarch64-buildroot-linux-gnu \
--build=x86_64-pc-linux-gnu \
--prefix=$(pwd)/tmp \
--enable-static \
--enable-shared \
CFLAGS="--sysroot=${SYSROOT} -O2" \
LDFLAGS="--sysroot=${SYSROOT}" \
CC="${CC}" \
CXX="${CXX}" \
AR="${AR}" \
LD="${LD}"
#编译
make
#安装到./tmp
make install
(base) ubuntu@ubuntu-2204:~/rnnoise$ tree -L 2 tmp
tmp
├── include
│   └── rnnoise.h
├── lib
│   ├── librnnoise.a
│   ├── librnnoise.la
│   ├── librnnoise.so -> librnnoise.so.0.4.1
│   ├── librnnoise.so.0 -> librnnoise.so.0.4.1
│   ├── librnnoise.so.0.4.1
│   └── pkgconfig
└── share
└── doc
(base) ubuntu@ubuntu-2204:~/rnnoise$ ls examples/
rnnoise_demo rnnoise_demo.c rnnoise_demo.o

./tmp目录下的文件就是需要使用的编译出来的rnnoise的头文件和库,用来做集成,examples下的rnnoise_demo就是参考实现,还有.c文件可以参考

adb push examples/.libs/rnnoise_demo /usr/bin/rnnoise_demo
adb push tmp/lib/librnnoise.so* /usr/lib/

现在可以到开发板进行简单的测试了

测试

# 先检查原始WAV文件信息
soxi speech.wav
# 如果采样率不是48000,先转换到48000
sox speech.wav -r 48000 speech_48k.wav
# 调整输入音量到合适范围(-3dB到-6dB)
# RNNoise要求:48000Hz,单声道,16位有符号整数PCM
sox speech_48k.wav -r 48000 -c 1 -e signed-integer -b 16 -t raw speech.pcm gain -n -3
# 运行RNNoise降噪
rnnoise_demo speech.pcm rnnoise.pcm
#转换回WAV格式
sox -r 48000 -c 1 -e signed-integer -b 16 -t raw rnnoise.pcm rnnoise.wav

使用rnnoise的库和API来进行音频降噪

代码结构

(base) ubuntu@ubuntu-2204:~/baiwen/rnnoise_reduction$ tree -L 3
.
├── CMakeLists.txt
├── deps
│   ├── include
│   │   └── rnnoise.h
│   └── lib
│   └── librnnoise.so
├── include
├── Makefile
└── src
└── main.c

5 directories, 5 files

其中 rnnnoise.h librnnoise.s0来自rnnoise的编译产物,直接复制就行,CMakeLists.txt需要更改工具链路径

# 指定交叉编译器
set(CMAKE_C_COMPILER /home/ubuntu/rk3576_AI/buildroot/output/rockchip_rk3576/host/bin/aarch64-buildroot-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER /home/ubuntu/rk3576_AI/buildroot/output/rockchip_rk3576/host/bin/aarch64-buildroot-linux-gnu-g++)

# 设置sysroot
set(CMAKE_SYSROOT /home/ubuntu/rk3576_AI/buildroot/output/rockchip_rk3576/staging)
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})

编译

make clean
make
#push adb到开发板的develop目录
make push

在开发板测试

root@rk3576-buildroot:/develop# ./rnnoise_reduction speech.wav rnnoise.wav -3
=== RNNoise音频降噪处理 ===
1. 读取WAV文件: speech.wav
采样率: 8000Hz, 声道: 2, 采样点数: 40000 (5.00秒)
2. 应用增益: -3.0dB
3. 重采样到48kHz
重采样后: 240000采样点 (5.00秒)
4. RNNoise降噪处理
处理帧数: 500, 每帧480个样本
第一帧前5个原始样本值: -34 -28 -23 -17 -11
处理后: 240000采样点 (5.00秒)
5. 保存为WAV文件: rnnoise.wav
=== 处理完成 ===
root@rk3576-buildroot:/develop# aplay speech.wav
Playing WAVE 'speech.wav' : Signed 16 bit Little Endian, Rate 8000 Hz, Stereo
root@rk3576-buildroot:/develop# aplay rnnoise.wav
Playing WAVE 'rnnoise.wav' : Signed 16 bit Little Endian, Rate 48000 Hz, Mono

可以听出明显的降噪效果

DshanPI-A1测评一构建buildroot系统,调试IMX415摄像头和问题解决

· 阅读需 7 分钟

板子介绍

DshanPi-A1是深圳百问网(韦东山团队)开发的一款高性能AI嵌入式开发板,基于瑞芯微RK3576芯片设计,专为AI教育、边缘计算和智能设备开发打造。

img

核心参数

参数项规格
主控芯片瑞芯微RK3576,8nm工艺,八核64位4×Cortex-A72(2.2GHz)+4×Cortex-A53(1.8GHz)瑞芯微电子股份有限公司
AI算力内置独立NPU,6TOPS计算能力,支持INT4/INT8/INT16混合运算
内存/存储板载LPDDR4/4X内存支持eMMC、UFS存储扩展SD卡插槽
显示接口HDMIv2.1/eDPv1.3组合接口MIPIDSI(4通道)瑞芯微电子股份有限公司
视频解码支持8K@30fps、4K@120fps高清视频
网络连接支持WiFi6/BLE5.2(需外接模块)千兆网口(部分版本)
USB接口多个USB3.0/2.0(Type-C和A型)瑞芯微电子股份有限公司
其他接口UART、I2C、SPI、GPIO、音频接口、CAN总线(部分版本)
电源支持30WPD快充(Type-C接口)
尺寸紧凑型设计(具体尺寸未公开)

产品特点

1️⃣ 强大的AI处理能力

· 6TOPS独立NPU可流畅运行DeepSeek、Qwen等轻量级大语言模型

· 支持主流AI框架(TensorFlow、PyTorch、MXNet等)

· 适用于图像识别、语音处理、智能监控等AI应用开发

2️⃣ 丰富的多媒体性能

· 支持8K视频解码,可作为高清媒体中心

· HDMIIN功能可将开发板作为电脑副屏使用

· 内置音频编解码器,支持3.5mm音频输出

3️⃣ 完善的开发支持

· 搭载DShanOS系统(百问网自研Linux发行版)

· 配套免费教学课程和文档,降低学习门槛

· 支持Armbian系统,提供官方镜像下载

· 提供完整SDK和示例代码,便于二次开发

4️⃣ 灵活的扩展能力

· 引出全部GPIO接口,方便连接各类传感器和执行器

· 支持多种通信协议,适合IoT应用开发

· 可外接摄像头、显示屏、WiFi/4G模块等扩展功能

应用场景

· AI教育与学习:适合嵌入式AI课程教学和实验

· 边缘计算设备:可部署轻量级AI模型,实现本地智能决策

· 智能家居中控:构建高性能、低功耗的智能家居控制中心

· 工业自动化:适用于智能设备监控和数据采集

· 商业显示:支持8K显示,可用于广告机和信息发布系统

· AI视觉系统:可用于人脸识别、物体检测等场景

配套资源

· 官方文档站点:https://wiki.dshanpi.org/docs/dshanpi-a1/

· 开发社区:韦东山嵌入式开发者社区(100ask.org)

· QQ技术交流群:798273638

· 免费教程:提供从基础到高级的AI开发课程

buildroot SDK安装

1. 官方虚拟机获取

https://pan.baidu.com/s/15M8zuHOwl_SITl6cSk_7Vg?pwd=eaax提取码:eaax

2. 安装打开vmware,运行虚拟机

进去ubuntu系统,账号密码为ubuntu

img

3. 编译SDK

打开虚拟机,执行以下命令,进入SDK根目录:

cd ~/100ask-rk3576_SDK/

img

选择好配置文件:

./build.sh lunch

img

4. 编译

运行下列命令

./build.sh

img

等待编译结束

img

设备树修改打开摄像头节点

进入

/home/ubuntu/100ask-rk3576_SDK/kernel/arch/arm64/boot/dts/rockchip

修改图片中的文件

img

img

改为if1

回到sdk目录使用

./build.sh kernel

img

img

最后再

./build.sh updateimg

img

img

最后上传烧录(记得进入MASKROM)

img

成果展示

img

但是依然有bug但是屏幕摄像头色彩还是显示不对。

问题解决

最开始我是怀疑是摄像头的.xml(.json)没有加载出来,通过调试了好久仍没有结果但是没有结果

经过了多方面的调试最终我找到了解决方案:

第一步:从media-ctl输出发现关键线索

media-ctl -p -d /dev/media0输出中,我注意到了这个关键信息:

entity 63: m00_b_rk628-csi9-0051 (1 pad, 1 link)
type V4L2 subdev subtype Sensor flags 0
device node name /dev/v4l-subdev2
pad 0: Source
[fmt:UYVY8_2X8/64x64@10000/600000 field:none]
-> "rockchip-csi2-dphy0":0 [ENABLED] ←注意这里的[ENABLED]

img

分析点

· 系统中存在一个rk628-csi传感器实体

· 它的链路状态是[ENABLED],意味着它正在占用CSI硬件资源

· 但分辨率只有64x64,这显然不是正常的摄像头输出

第二步:从dmesg日志发现IMX415驱动正常

dmesg | grep -i imx415输出中:

[3.459474] imx415 3-0037: Detected imx415 id 0000e0
[3.515523] imx415 3-0037: Consider updating driver imx415 to match on endpoints
[3.515557] rockchip-csi2-dphy csi2-dphy3: dphy3 matches m01_f_imx415 3-0037: bustype 5

分析点

· IMX415传感器被正确检测到(ID:0000e0)

· 驱动加载成功,甚至建立了与CSI-DPHY的匹配关系

· 但没有出现V4L2子设备创建成功的消息

第三步:从设备树结构推断硬件连接

从设备树片段:

&csi2_dphy3 {
port@0 {
mipi_in_ucam3: endpoint@1 {
remote-endpoint = <&imx415_out0>; ←IMX415连接到这里
data-lanes = <1 2 3 4>;
};
};
port@1 {
csidphy3_out: endpoint@0 {
remote-endpoint = <&mipi3_csi2_input>;
};
};
};

分析点

· IMX415通过CSI-DPHY3连接到系统

· 但实际的media-ctl输出显示的是rockchip-csi2-dphy0被占用

· 这表明可能存在多个CSI接口的资源分配问题

第四步:从V4L2子设备缺失推断注册失败

运行检查命令时:

ls /sys/bus/i2c/devices/3-0037/v4l-subdev*/media_device/
# 输出:No such file or directory

img

分析点

· I2C设备3-0037存在且驱动绑定正常

· 但没有创建对应的V4L2子设备

· 这通常意味着驱动探测成功,但后续的V4L2注册失败

第五步:连接所有线索形成完整画面

把以上线索串联起来:

1. 现象:IMX415驱动加载但无V4L2设备

2. 证据1:rk628-csi占用着CSI链路且状态为[ENABLED]

3. 证据2:IMX415 I2C通信正常但媒体设备缺失

4. 证据3:设备树显示两者可能共享CSI硬件资源

逻辑推理

· 如果IMX415硬件故障→I2C探测应该失败

· 如果IMX415驱动问题→dmesg应该有错误日志

· 如果设备树配置错误→CSI匹配不会成功

· 唯一合理的解释:硬件资源被其他设备占用

第六步:开始实践

img

重新编译设备树烧录开发板完美运行!

img

img

总结

  1. 嵌入式设备中硬件资源冲突是常见隐性问题,尤其多设备共享 CSI、I2C 等链路时,需重点排查设备占用状态。

  2. 开发过程中应充分利用media-ctl、dmesg等工具,结合设备树配置分析,快速定位资源分配问题,避免盲目调试。

  3. 对于驱动加载正常但功能异常的场景,优先排查硬件资源占用、V4L2 子设备注册等关键环节,缩小问题范围。

  4. DshanPi-A1 开发板的 CSI 接口资源分配需通过设备树精准配置,修改后需严格遵循 Buildroot 编译流程重新生成镜像,确保配置生效。

DshanPI-A1第三篇opencv调试与cpu直接推理识别手势

· 阅读需 10 分钟

前面我们已经调试好了摄像头和屏幕,终于可以开始我们的手势识别啦!

这次我会在RK3576 Buildroot系统上实现一个基于OpenCV的实时手势识别系统。系统能够识别五种手势(拳头/一指、二指、三指、四指、五指),并在屏幕上实时显示处理结果。

由于嵌入式系统的特殊性,我们将重点讲解如何在无X11、无OpenGL的Wayland环境下实现图像显示。

手势识别算法

原理

因为我们张开的手指之间会形成凹陷,通过计算凹陷点的角度和深度,可以准确识别手指数量。

1. 肤色检测

使用HSV色彩空间提取肤色区域:

def detect_hand(self, frame):
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
mask = cv2.inRange(hsv, [0, 30, 60], [25, 255, 255]) # 肤色范围

# 形态学处理去噪
kernel = np.ones((7, 7), np.uint8)
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=3)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations=2)

# 查找最大轮廓
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if contours:
c = max(contours, key=cv2.contourArea)
if cv2.contourArea(c) > 3000: # 面积阈值过滤噪声
return c, mask
return None, mask

2. 手指计数(凸包缺陷法)

通过检测手掌轮廓的凹陷点来识别手指:

def recognize(self, contour):
hull = cv2.convexHull(contour, returnPoints=False)
defects = cv2.convexityDefects(contour, hull)

finger_count = 0
for i in range(defects.shape[0]):
s, e, f, d = defects[i, 0]
start = tuple(contour[s][0]) # 凸点1
end = tuple(contour[e][0]) # 凸点2
far = tuple(contour[f][0]) # 凹点(指间)

# 计算角度判断是否为有效指尖
a = np.linalg.norm(np.array(start) - np.array(end))
b = np.linalg.norm(np.array(start) - np.array(far))
c = np.linalg.norm(np.array(end) - np.array(far))
angle = np.arccos((b**2 + c**2 - a**2) / (2 * b * c))
if angle <= np.pi / 2.2 and d > 8000: # 角度和深度阈值
finger_count += 1

gestures = ["Fist/One", "Two", "Three", "Four", "Five"]
return gestures[finger_count]

如何显示OpenCV处理的图像

有了前面的理论知识还是不够的,我们还需要实践才可以啊!如何显示OpenCV处理的图像这是本教程的重点。通常我们用cv2.imshow()显示图像,但在无X11/OpenGL的嵌入式系统上,这个方法不可用。我们需要使用GStreamer+Wayland方案(这个也是我们上一篇文章的方案)。

方案探索过程

方案A: stdin管道传输

最直观的想法是通过stdin管道传输图像数据:

proc = subprocess.Popen(['gst-launch-1.0', 'fdsrc', '!', ...], stdin=subprocess.PIPE)
proc.stdin.write(frame_data)

结果:频繁出现Broken pipe错误,数据传输不稳定,这个我怀疑是管道超时自动关闭了又或者是我的格式不对。所以我直接尝试用fifo来传输原始的数据

方案B: 命名管道(FIFO)传输原始数据

尝试用FIFO传输原始RGB数据:

mkfifo /tmp/video_fifo

img

很遗憾啊,这个太容易动不动就内核奔溃了。到这个时候我面临几个问题:如果我单纯的用命令行看识别结果,我就不知道我的摄像头能不能正常运行;但是如果OpenCV和GStreamer同时读取摄像头是不可以的(摄像头只能被一个进程读取)。后面又觉得GStreamer的显示不需要实时读取摄像头的画面,我只要显示OpenCV处理过的就好了。说干就干,于是我尝试直接在OpenCV里面把处理后的图像通过GStreamer传到屏幕上面,但技术不达标无果,管道不是报错就是关闭。停下来慢慢思考,最后有了方案C(搞到这步已经花费三天了)。

方案C: 多文件序列

改变思路,将处理后的图像保存为JPEG文件序列:

# Python端
cv2.imwrite(f'/dev/shm/gesture_frames/frame_{frame_index:03d}.jpg', processed)
# GStreamer端
gst-launch-1.0 multifilesrc location=frame_%03d.jpg loop=true ! jpegdec ! ...

结果:屏幕终于可以有变动了,开心坏了!但是效果很差,重影严重,而且因为是循环读出文件夹的图片导致一直在循环播放,影响美观和体验度。于是我打算利用FIFO,基于前面的思路升级为现在的方案:

OpenCV处理完一帧 → 立即编码JPEG → 直接写入FIFO → GStreamer立即解码显示

方案D: FIFO+JPEG流(最终方案!)

# Python端: 直接往FIFO写JPEG数据
fifo = open('/tmp/gesture_fifo', 'wb')
_, jpeg = cv2.imencode('.jpg', processed, [cv2.IMWRITE_JPEG_QUALITY, 85])
fifo.write(jpeg.tobytes())
fifo.flush()
# GStreamer端: 用jpegparse自动分割JPEG帧
gst-launch-1.0 filesrc location=/tmp/gesture_fifo ! jpegparse ! jpegdec ! ...

补充:为什么JPEG流可行?

  1. JPEG格式自带开始(0xFFD8)和结束(0xFFD9)标记
  2. GStreamer的jpegparse插件能自动识别边界,分割独立的JPEG帧
  3. 避免了原始数据流的粘包问题

结果:总算是可以流畅地观察到画面了(流程不卡顿,甚至比直接点屏幕上的摄像头图标看摄像头画面都流畅)。演示视频和代码我会放在附件里面。

img img

番外篇—摄像头第二次启动色彩偏绿偏暗

问题描述

我在RK3576 Buildroot系统上使用IMX415摄像头,通过GStreamer+Wayland显示画面。遇到了一个诡异的问题:第一次启动摄像头色彩正常,但第二次启动后画面就变得偏暗偏绿

img

初步分析:对比启动日志

我首先对比了两次启动的kernel日志,发现了关键差异:

第一次启动(色彩正常)

[20.528641] rkisp_hw 27c00000.isp: set isp clk = 594000000Hz
[20.529097] rkcif-mipi-lvds 3: stream[0] start streaming
[20.529317] rockchip-csi2-dphy 3: dphy3, data_rate_mbps 892
[20.529356] imx415 3-0037: s_stream: 1.3864x2192, hdr: 0, bpp: 10

第二次启动(色彩异常)

[79.209321] rkisp_hw 27c00000.isp: set isp clk = 594000000Hz
[79.209967] rkisp rkisp-vir3: first params buf queue
[79.210051] rkisp rkisp-vir3: id: 0 no first iq setting cfg_upd: c000dfecc7fe473b en_upd: 0 en s: 5ffcc7fe473b
[79.210351] rkcif-mipi-lvds 3: stream[0] start streaming

关键发现:第二次启动多了一条警告 no first iq setting。这说明ISP的图像质量参数没有正确加载,导致使用了错误的默认参数,造成色彩偏暗偏绿。

问题解决过程

第一阶段:尝试硬件层面解决

一开始我以为是ISP驱动状态没有正确复位,尝试了几种方法:

  1. 尝试unbind/bind ISP驱动

    echo "27c00000.isp" > /sys/bus/platform/drivers/rkisp_hw/unbind
    echo "27c00000.isp" > /sys/bus/platform/drivers/rkisp_hw/bind

    结果:摄像头直接打不开了,操作太激进导致驱动状态完全错乱。

  2. 尝试使用v4l2-ctl重置、media-ctl reset等方法,都没有解决问题。

第二阶段:深入诊断系统配置

我开始系统性地诊断整个摄像头子系统:

# 查找IQ参数文件
find / -name "*imx415*.xml" -o -name "*imx415*.json" 2>/dev/null
# 结果: 找到了 /etc/iqfiles/imx415_CMK-OT2022-PX1_IR0147-50IRC-8M-F20.json

# 检查3A服务器
ps aux | grep rkaiq_3A_server
# 结果: 服务器正在运行

# 查看设备拓扑
v4l2-ctl --list-devices
# 确认 /dev/video-camera0 -> video11

关键发现

  • IQ参数文件存在
  • 3A服务器(rkaiq_3A_server)正在运行
  • 但为什么IQ参数没有加载?

第三阶段:抓取3A服务器日志

我决定前台运行3A服务器,查看详细输出:

killall rkaiq_3A_server
/usr/bin/rkaiq_3A_server 2>&1 &

启动日志显示:

DBG: get rkisp-isp-subdev devname: /dev/v4l-subdev3
DBG: get rkisp-input-params devname: /dev/video18
DBG: get rkisp-statistics devname: /dev/video17
XCORE: K: cid[1] rk_aiq_uapi2_sysctl_init success. iq: /etc/iqfiles//imx415_CMK-OT2022-PX1_IR0147-50IRC-8M-F20.json
XCORE: K: cid[1] rk_aiq_uapi2_sysctl_prepare success. mode: 0
DBG: /dev/media1: wait stream start event..

重大发现:3A服务器实际上工作正常!IQ文件已经成功加载了!

这时我进行了第二次摄像头启动测试,观察到:

[625.216117] rkisp-vir3: waiting on params stream one event timeout

真相大白:第二次启动时,3A服务器超时无响应!

第四阶段:找到根本原因

通过多次测试和日志分析,我终于理解了问题的本质:

第一次启动流程(正常)

  1. 系统启动时,3A服务器自动启动
  2. 3A服务器加载IQ参数文件到内存
  3. 3A服务器预先准备好IQ参数缓冲区
  4. GStreamer启动摄像头
  5. ISP请求IQ参数
  6. 3A服务器立即响应并推送IQ参数
  7. 色彩正常

第二次启动流程(异常)

  1. 停止第一次的GStreamer进程
  2. 3A服务器还在运行,但进入了某种等待状态
  3. IQ参数缓冲区已经被消费
  4. 立即重启GStreamer
  5. ISP请求IQ参数
  6. 3A服务器来不及响应或状态异常
  7. ISP使用默认参数处理第一帧
  8. 出现no first iq setting警告
  9. 色彩偏暗偏绿

解决方案

问题的根源是:3A服务器在摄像头第一次运行后进入异常状态,无法正确响应第二次启动的IQ参数请求

最终的解决方法很简单:每次启动摄像头前,重启3A服务器

我编写了一个封装脚本:

#!/bin/sh

echo "=== Starting Camera with 3A Server Reset ==="

# 1. 停止所有摄像头进程
pkill -9 gst-launch 2>/dev/null

# 2. 重启3A服务器
killall rkaiq_3A_server 2>/dev/null
sleep 2
rm -f /tmp/.rkaiq_3A*

# 3. 启动3A服务器
/etc/init.d/S40rkaiq_3A start
echo "Waiting for 3A server to initialize..."
sleep 5

# 4. 确认3A服务器运行正常
if ! pgrep rkaiq_3A_server > /dev/null; then
echo "ERROR: 3A server failed to start!"
exit 1
fi

echo "3A server ready, starting camera..."

# 5. 启动摄像头
gst-launch-1.0 v4l2src device=/dev/video11 ! \
video/x-raw,format=NV12,width=640,height=480,framerate=30/1 ! \
waylandsink

echo "Camera stopped"
exit 0

img

验证结果

img

使用新脚本后,连续多次启动摄像头,色彩始终正常,日志中不再出现no first iq setting或超时错误。

经验总结

  1. 对比日志是发现问题的关键:通过对比正常和异常情况的日志,快速定位到no first iq setting这个关键线索

  2. 系统性诊断:不要盲目尝试,先检查各个组件(IQ文件、3A服务器、设备节点)的状态

  3. 前台运行看详细日志:很多后台服务的问题需要前台运行才能看到详细输出

  4. 理解组件间的协作关系:RK平台的摄像头涉及ISP驱动、3A服务器、IQ参数文件三者的协作,任何一个环节出问题都会导致异常

  5. 状态管理很重要:嵌入式系统的服务重启问题往往是状态机管理不当导致的,彻底重置是最可靠的方案。

DshanPI-A1测评第二篇:手势识别编程环境搭建与屏幕调试

· 阅读需 6 分钟

本次测评我将会安装手势识别系统的必要工具和调试屏幕

硬件与环境准备

在开始之前,我们先明确手头的装备和环境:

  • 核心板:Dshanpi-A1,主控为瑞芯微RK3576芯片。

  • 屏幕:一块480x800分辨率的MIPI屏幕。

  • 系统:Buildroot Linux系统。

  • 官方的SDK

安装开发工具

下面是本次的清单:

软件包/配置类别推荐选项与作用
Python环境python3: 核心解释器。 python-pip: 用于安装未包含在Buildroot中的Python包。 python-numpy: 为OpenCV等库提供高效的数值计算支持。 python-setuptools: 一些Python包的基础构建依赖。
计算机视觉与图像处理opencv4: 务必启用 python3 支持。提供核心的计算机视觉库,用于图像处理和手势识别算法。 opencv4 贡献模块: 包含额外的、更先进的算法。
摄像头与显示支持gstreamer1 及相关插件: 构建摄像头图像采集和屏幕显示的管道。 gst1-plugins-base, gst1-plugins-good, gst1-plugins-bad, gst1-plugins-ugly: 提供丰富的编解码器和功能元件。 gst1-python: 允许在Python中创建和操作GStreamer管道。

SDK配置流程

1. 选择芯片类型

./build.sh chip

img

img

2. 进入buildroot配置

cd buildroot
make menuconfig

img

3. 选择Target packages

img

4. 安装Python环境

当找不到安装路径时,可按/键进入搜索:

img

输入python3进行搜索:

img

进入显示的Location路径进行配置:

img

img

5. 保存配置并编译

make

回到SDK主目录运行:

./build.sh rootfs
./build.sh updateimg

最后烧录运行到开发板

开发板调试

检查工具安装情况

python3 --version
pip3 --version
python3 -c "import numpy; print('NumPy version:', numpy.__version__)"
python3 -c "import cv2; print('OpenCV version:', cv2.__version__)"

img

屏幕调试

问题分析

系统已运行Weston合成器,这意味着我们有一个图形界面环境。尝试直接操作FrameBuffer(/dev/fb0)无效,因为Weston已占用显示接口。

通过系统检查发现:

  • /dev/fb0设备存在
  • 屏幕状态为connected
  • 分辨率为480x800

img

解决方案:GStreamer + Wayland

GStreamer基础测试

gst-launch-1.0 videotestsrc pattern=smpte ! video/x-raw,width=480,height=800 ! waylandsink sync=false

img

摄像头直连显示测试

gst-launch-1.0 v4l2src device=/dev/video11 ! video/x-raw,width=640,height=480 ! videoconvert ! waylandsink sync=false

img

注意:请将device参数替换为您的摄像头设备节点

测试脚本

#!/usr/bin/env python3
# fixed_display_test.py

import subprocess
import time
import os

def check_camera_devices():
"""检查可用的摄像头设备"""
print("=== 摄像头设备检查 ===")

try:
# 使用v4l2-ctl检查设备
result = subprocess.run(["v4l2-ctl", "--list-devices"],
capture_output=True, text=True)
if result.returncode == 0:
print("找到的视频设备:")n print(result.stdout)
else:
print("v4l2-ctl命令执行失败")
except Exception as e:
print(f"检查摄像头设备失败: {e}")

# 测试常见的摄像头设备
camera_devices = ["/dev/video11", "/dev/video0", "/dev/video1", "/dev/video2"]
print("\n测试摄像头设备:")

for device in camera_devices:
if os.path.exists(device):
print(f"测试设备: {device}")
try:
# 尝试使用GStreamer测试摄像头
cmd = [
"gst-launch-1.0",
"-v",
"v4l2src", f"device={device}", "!",
"video/x-raw,width=640,height=480,framerate=15/1", "!",
"videoconvert", "!",
"waylandsink", "sync=false"
]

process = subprocess.Popen(cmd)
time.sleep(3) # 显示3秒
process.terminate()
process.wait()
print(f" {device}: 摄像头工作正常")
return device

except Exception as e:
print(f"{device}: 测试失败 - {e}")
else:
print(f"{device}: 设备不存在")

return None

def test_static_patterns():
"""测试静态图案(不会变化)"""
print("\n=== 静态图案测试 ===")

# 设置Wayland环境
os.environ['WAYLAND_DISPLAY'] = 'wayland-0'

# 测试静态图案(不会变化)
static_patterns = [
("smpte100", "SMPTE 100%色彩条"),
("ball", "时钟图案"),
("blink", "闪烁图案"),
("pinwheel", "风车图案"),
("spokes", "辐条图案"),
]

for pattern, description in static_patterns:
print(f"显示: {description}")
try:
cmd = [
"gst-launch-1.0",
"videotestsrc", f"pattern={pattern}", "!",
"video/x-raw,width=480,height=800,framerate=15/1", "!",
"waylandsink", "sync=false"
]

process = subprocess.Popen(cmd)
time.sleep(3)
process.terminate()
process.wait()
print(f"{description} 显示成功")

except Exception as e:
print(f"{description} 显示失败: {e}")

def test_custom_resolution():
"""测试自定义分辨率显示"""
print("\n=== 自定义分辨率测试 ===")

resolutions = [
(480, 800, "竖屏 480x800"),
(800, 480, "横屏 800x480"),
(640, 480, "标准 640x480"),
(400, 800, "竖屏 400x800"),
]

for width, height, desc in resolutions:
print(f"测试分辨率: {desc}")
try:
cmd = [
"gst-launch-1.0",
"videotestsrc", "pattern=smpte100", "!",
f"video/x-raw,width={width},height={height},framerate=15/1", "!",
"videoconvert", "!",
"waylandsink", "sync=false"
]

process = subprocess.Popen(cmd)
time.sleep(2)
process.terminate()
process.wait()
print(f" {desc} 显示成功")

except Exception as e:
print(f" {desc} 显示失败: {e}")

if __name__ == "__main__":
print("=" * 50)

# 1. 检查摄像头
camera_device = check_camera_devices()

# 2. 测试静态图案
test_static_patterns()

# 3. 测试不同分辨率
test_custom_resolution()

print("\n" + "=" * 50)
if camera_device:
print(f"可用的摄像头设备: {camera_device}")
else:
print("未找到可用的摄像头设备")
print("所有测试完成")

img

img

运行结果

image-20251222165424798

image-20251222165428939

演示视频我会放在附件里面

番外篇:FileZilla文件传输

FileZilla连接设置

FileZilla - The free FTP solution

检查SSH服务

ss -tuln | grep 22

img

网络共享设置

img

img

img

选择能上网的网络,点击属性:

img

image-20251222165505322

连接开发板

ifconfig

img

使用FileZilla连接:

  • IP:开发板IP地址
  • 用户名:root
  • 密码:rockchip
  • 端口:22

总结

回顾整个调试过程,以下几个关键点值得特别注意:

  1. 显示路径选择:在运行Weston这类合成器的系统上,优先使用GStreamer + waylandsink的方案来显示图像,而非直接操作FrameBuffer。

  2. 摄像头设备节点:务必使用v4l2-ctl --list-devices命令确认摄像头对应的设备节点,并在代码中正确指定。

  3. 屏幕分辨率:注意系统识别到的屏幕分辨率(可通过cat /sys/class/drm/card0-DSI-1/modes查询)可能与物理分辨率略有不同,在创建显示帧时需以此为准或进行调整。

至此,我们成功地在Dshanpi A1开发板上搭建起了手势识别的编程环境,并解决了MIPI屏幕和摄像头的显示问题。虽然过程曲折,但为后续实际编写手势识别算法奠定了坚实的基础。希望我的这些经验能对大家有所帮助!

DshanPI-A1第五篇NPU实战YOLOv5实时目标检测加速

· 阅读需 17 分钟

前言

在前面的文章中,我们已经实现了基于CPU的MediaPipe手势识别,虽然能跑起来,但15-25 FPS的性能还是有点吃力,而且CPU占用率很高。这次我要榨干RK3576的硬件潜力——使用板载的NPU(神经网络处理单元)来加速深度学习推理。首先外我们需要要来了解一下一些概念

什么是NPU? NPU(Neural Processing Unit)是专门为AI运算设计的硬件加速器。和CPU/GPU不同,NPU针对神经网络的矩阵运算、卷积等操作做了深度优化。RK3576芯片内置了双核NPU,理论算力达到6 TOPS,能大幅提升模型推理速度并降低功耗。

为什么选YOLOv5? 其实我本来是打算继续优化我前面的MediaPipe的TFLite模型转换遇到了依赖地狱(这个坑我踩了好久...),所以这次先用官方提供的YOLOv5模型来验证NPU功能。YOLOv5是目前最流行的实时目标检测算法之一,能同时检测图像中的多个物体及其位置。

一、环境准备

1.1 硬件连接

  • RK3576开发板(已刷Buildroot系统)
  • IMX415摄像头(接在/dev/video11)
  • HDMI显示器
  • 串口连接(用于命令行操作)

image-20251222175427219

1.2 检查NPU硬件

首先登录板子,检查NPU是否正常工作:

# 查看NPU负载(应该显示Core0和Core1)
cat /sys/kernel/debug/rknpu/load

image-20251222175450647

这说明NPU双核都是空闲状态,可以开始干活了!

小知识: RK3576的NPU采用双核架构,可以并行处理两个模型,或者让一个大模型的不同层在两个核心上流水线执行。

1.3 检查Python环境

# 查看Python版本
python3 --version

image-20251222175518307

我的输出是 Python 3.11.8,这个版本很重要,后面安装库的时候要匹配。

二、安装RKNN运行时环境

2.1 什么是RKNN?

RKNN(Rockchip Neural Network)是瑞芯微为自家NPU开发的深度学习推理框架。整个工具链分两部分:

  • rknn-toolkit2(PC端): 用于模型转换,把TensorFlow/PyTorch/ONNX模型转成.rknn格式
  • rknn-toolkit-lite2(板端): 轻量级运行时库,用于在RK芯片上加载和推理.rknn模型

我们这次只用板端推理,所以只装lite版。

2.2 安装安装包

好消息是我们如果不想在github上下载觉得慢或者不稳定,我们可以选择我们百问网提供的下载连接: https://dl.100ask.net/Hardware/MPU/RK3576-DshanPi-A1/utils/rknn-toolkit2.zip 下载完成传到我们的Dshanpi-A1上。

cd /rknn-toolkit2/rknn-toolkit-lite2/packages/
ls -lh

image-20251222175600879

可以看到有多个Python版本的.whl安装包,我们需要的是cp311(Python 3.11)的ARM64版本。

2.3 安装rknn-toolkit-lite2

# 先强制安装主包(不检查依赖,因为依赖后面单独装)
pip3 install --no-deps rknn_toolkit_lite2-2.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

# 再用清华镜像安装缺失的依赖
pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple psutil ruamel.yaml

这样做就不用耗费我们下载一些没必要的包了。

看到 Successfully installed 就成功了!

2.4 验证安装

python3 -c "from rknnlite.api import RKNNLite; print('✅ rknn-toolkit-lite2 安装成功!')"

image-20251222175629367

输出勾勾标志就OK!

三、NPU基准测试

在跑实时检测之前,先用一个简单的图像分类模型测试一下NPU的性能。

3.1 使用ResNet18测试

进入示例目录:

cd /rknn-toolkit2/rknn-toolkit-lite2/examples/resnet18
ls -lh

image-20251222175647550

可以看到有:

  • resnet18_for_rk3576.rknn - 专门为RK3576优化的模型
  • space_shuttle_224.jpg - 测试图片
  • test.py - 推理脚本

运行测试:

python3 test.py

image-20251222175712361

我的结果:

  • 识别结果: Space Shuttle(航天飞机) - 99.96%置信度
  • 推理延迟: 11.21 ms
  • 平均FPS: 89.24

这意味着NPU可以每秒处理89张图片,比我之前CPU跑MediaPipe快3-6倍!

3.2 性能基准测试

为了更准确地测试NPU性能,我写了个循环100次的脚本(可以自行测试):

cd ~
mkdir -p npu_test
cd npu_test

# 复制模型和图片
cp /rknn-toolkit2/rknn-toolkit-lite2/examples/resnet18/resnet18_for_rk3576.rknn ./
cp /rknn-toolkit2/rknn-toolkit-lite2/examples/resnet18/space_shuttle_224.jpg ./

创建测试脚本 benchmark.py:

import cv2
import numpy as np
import time
from rknnlite.api import RKNNLite

rknn = RKNNLite()
rknn.load_rknn('resnet18_for_rk3576.rknn')
rknn.init_runtime(core_mask=RKNNLite.NPU_CORE_0)

img = cv2.imread('space_shuttle_224.jpg')
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img = np.expand_dims(img, 0)

# 预热
for _ in range(10):
rknn.inference(inputs=[img])

# 测试100次
times = []
for i in range(100):
start = time.time()
rknn.inference(inputs=[img])
times.append((time.time() - start) * 1000)

print(f'平均延迟: {np.mean(times):.2f} ms')
print(f'最小延迟: {np.min(times):.2f} ms')
print(f'最大延迟: {np.max(times):.2f} ms')
print(f'平均FPS: {1000/np.mean(times):.2f}')

rknn.release()

运行:

python3 benchmark.py

四、YOLOv5目标检测

现在进入正题——用NPU跑实时目标检测!

4.1 什么是YOLOv5?

YOLO(You Only Look Once)是一种单阶段目标检测算法,能在一次前向传播中同时预测多个物体的位置和类别。相比两阶段的R-CNN系列,YOLO速度更快,非常适合实时场景。

YOLOv5是该系列的第五代,支持检测80种常见物体(人、车、动物、家具等)。

4.2 准备模型和测试图片

cd ~
mkdir -p npu_yolo_test
cd npu_yolo_test

# 复制RK3576专用的YOLOv5模型
cp /rknn-toolkit2/rknpu2/examples/rknn_yolov5_demo/model/RK3576/yolov5s-640-640.rknn ./

# 复制测试图片(一张公交车照片)
cp /rknn-toolkit2/rknpu2/examples/rknn_yolov5_demo/model/bus.jpg ./

ls -lh

image-20251222175741596

4.3 单张图片检测测试

这里完整的后处理代码比较长(包含NMS非极大值抑制等算法),我把它整理成了一个脚本。

创建 yolo_npu_test.py(完整代码见附录A),然后运行:

python3 yolo_npu_test.py

image-20251222175811624

我的结果:

  • 检测到5个目标:
    • 3个人(person): 88.0%, 87.1%, 82.8%
    • 1辆公交车(bus): 70.1%
    • 1个部分遮挡的人: 30.7%
  • NPU推理延迟: 87.94 ms
  • FPS: 11.37

检测结果保存在 result_npu.jpg,可以传到PC上查看:

# 在PC的PowerShell中执行(替换<板子IP>为实际IP)
scp root@<板子IP>:/npu_yolo_test/result_npu.jpg .

【检测结果图片】 image-20251222175836265

为什么YOLOv5比ResNet18慢?

  • ResNet18只做分类,输出1000个类别的概率(简单)
  • YOLOv5要检测多个物体的位置+类别,输出3个不同尺度的特征图(复杂)
  • 但11 FPS对于目标检测来说已经很不错了!

五、实时摄像头检测

单张图片测试成功了,现在来个真正的实战——用IMX415摄像头做实时检测并显示在屏幕上!

5.1 显示方案:FIFO + GStreamer

还是和之前的一样由于Buildroot没有图形界面,OpenCV的imshow()不能用,我们采用命名管道(FIFO) + GStreamer的方案:

  1. Python读摄像头 → NPU推理 → 画框 → 编码成JPEG
  2. 写入FIFO管道
  3. GStreamer从管道读取 → 解码 → 显示到屏幕

这是Linux下常用的进程间通信方式,之前手势识别项目也用的这个。

5.2 创建一键启动脚本

为了方便使用,我把整个流程打包成了一个Shell脚本 yolo_npu_display.sh:

cd /npu_yolo_test

cat > yolo_npu_display.sh << 'EOF'
#!/bin/bash

echo "=========================================="
echo "YOLOv5 NPU Real-time Detection - RK3576"
echo "=========================================="
echo ""

# 重启3A服务器(摄像头自动曝光/白平衡/自动对焦)
echo "Restarting 3A server..."
killall rkaiq_3A_server 2>/dev/null
sleep 2
rm -f /tmp/.rkaiq_3A* 2>/dev/null
/etc/init.d/S40rkaiq_3A start >/dev/null 2>&1
sleep 3

# 创建FIFO管道
FIFO_PATH="/tmp/yolo_fifo"
rm -f $FIFO_PATH
mkfifo $FIFO_PATH

echo "Starting display pipeline..."
gst-launch-1.0 -q filesrc location=$FIFO_PATH ! jpegparse ! jpegdec ! videoconvert ! videoscale ! video/x-raw,width=1280,height=720 ! waylandsink fullscreen=true sync=false &
GST_PID=$!

sleep 2

echo "Starting YOLOv5 NPU detection..."
python3 - <<'PYTHON_CODE' &
import cv2
import numpy as np
import time
from rknnlite.api import RKNNLite
from collections import deque

RKNN_MODEL = '/npu_yolo_test/yolov5s-640-640.rknn'
CAMERA_ID = 11
IMG_SIZE = 640
OBJ_THRESH = 0.25
NMS_THRESH = 0.45
FIFO_PATH = '/tmp/yolo_fifo'

CLASSES = ("person", "bicycle", "car", "motorbike", "aeroplane", "bus", "train", "truck", "boat", "traffic light",
"fire hydrant", "stop sign", "parking meter", "bench", "bird", "cat", "dog", "horse", "sheep", "cow",
"elephant", "bear", "zebra", "giraffe", "backpack", "umbrella", "handbag", "tie", "suitcase", "frisbee",
"skis", "snowboard", "sports ball", "kite", "baseball bat", "baseball glove", "skateboard", "surfboard",
"tennis racket", "bottle", "wine glass", "cup", "fork", "knife", "spoon", "bowl", "banana", "apple",
"sandwich", "orange", "broccoli", "carrot", "hot dog", "pizza", "donut", "cake", "chair", "sofa",
"pottedplant", "bed", "diningtable", "toilet", "tvmonitor", "laptop", "mouse", "remote", "keyboard",
"cell phone", "microwave", "oven", "toaster", "sink", "refrigerator", "book", "clock", "vase",
"scissors", "teddy bear", "hair drier", "toothbrush")

def xywh2xyxy(x):
y = np.copy(x)
y[:, 0] = x[:, 0] - x[:, 2] / 2
y[:, 1] = x[:, 1] - x[:, 3] / 2
y[:, 2] = x[:, 0] + x[:, 2] / 2
y[:, 3] = x[:, 1] + x[:, 3] / 2
return y

def process(input, mask, anchors):
anchors = [anchors[i] for i in mask]
grid_h, grid_w = map(int, input.shape[0:2])
box_confidence = np.expand_dims(input[..., 4], axis=-1)
box_class_probs = input[..., 5:]
box_xy = input[..., :2]*2 - 0.5
col = np.tile(np.arange(0, grid_w), grid_w).reshape(-1, grid_w)
row = np.tile(np.arange(0, grid_h).reshape(-1, 1), grid_h)
col = col.reshape(grid_h, grid_w, 1, 1).repeat(3, axis=-2)
row = row.reshape(grid_h, grid_w, 1, 1).repeat(3, axis=-2)
grid = np.concatenate((col, row), axis=-1)
box_xy += grid
box_xy *= int(IMG_SIZE/grid_h)
box_wh = pow(input[..., 2:4]*2, 2)
box_wh = box_wh * anchors
box = np.concatenate((box_xy, box_wh), axis=-1)
return box, box_confidence, box_class_probs

def filter_boxes(boxes, box_confidences, box_class_probs):
boxes = boxes.reshape(-1, 4)
box_confidences = box_confidences.reshape(-1)
box_class_probs = box_class_probs.reshape(-1, box_class_probs.shape[-1])
_box_pos = np.where(box_confidences >= OBJ_THRESH)
boxes = boxes[_box_pos]
box_confidences = box_confidences[_box_pos]
box_class_probs = box_class_probs[_box_pos]
class_max_score = np.max(box_class_probs, axis=-1)
classes = np.argmax(box_class_probs, axis=-1)
_class_pos = np.where(class_max_score >= OBJ_THRESH)
boxes = boxes[_class_pos]
classes = classes[_class_pos]
scores = (class_max_score * box_confidences)[_class_pos]
return boxes, classes, scores

def nms_boxes(boxes, scores):
x, y = boxes[:, 0], boxes[:, 1]
w, h = boxes[:, 2] - boxes[:, 0], boxes[:, 3] - boxes[:, 1]
areas = w * h
order = scores.argsort()[::-1]
keep = []
while order.size > 0:
i = order[0]
keep.append(i)
xx1 = np.maximum(x[i], x[order[1:]])
yy1 = np.maximum(y[i], y[order[1:]])
xx2 = np.minimum(x[i] + w[i], x[order[1:]] + w[order[1:]])
yy2 = np.minimum(y[i] + h[i], y[order[1:]] + h[order[1:]])
w1 = np.maximum(0.0, xx2 - xx1 + 0.00001)
h1 = np.maximum(0.0, yy2 - yy1 + 0.00001)
inter = w1 * h1
ovr = inter / (areas[i] + areas[order[1:]] - inter)
inds = np.where(ovr <= NMS_THRESH)[0]
order = order[inds + 1]
return np.array(keep)

def yolov5_post_process(input_data):
masks = [[0, 1, 2], [3, 4, 5], [6, 7, 8]]
anchors = [[10, 13], [16, 30], [33, 23], [30, 61], [62, 45],
[59, 119], [116, 90], [156, 198], [373, 326]]
boxes, classes, scores = [], [], []
for input, mask in zip(input_data, masks):
b, c, s = process(input, mask, anchors)
b, c, s = filter_boxes(b, c, s)
boxes.append(b)
classes.append(c)
scores.append(s)
if len(boxes) == 0:
return None, None, None
boxes = np.concatenate(boxes)
boxes = xywh2xyxy(boxes)
classes = np.concatenate(classes)
scores = np.concatenate(scores)
nboxes, nclasses, nscores = [], [], []
for c in set(classes):
inds = np.where(classes == c)
b, c, s = boxes[inds], classes[inds], scores[inds]
keep = nms_boxes(b, s)
nboxes.append(b[keep])
nclasses.append(c[keep])
nscores.append(s[keep])
if not nclasses:
return None, None, None
return np.concatenate(nboxes), np.concatenate(nclasses), np.concatenate(nscores)

class YOLODetector:
def __init__(self):
self.fps_queue = deque(maxlen=30)
self.last_time = time.time()
self.fps = 0.0

def calc_fps(self):
t = time.time()
if t - self.last_time > 0:
self.fps_queue.append(1.0 / (t - self.last_time))
self.fps = sum(self.fps_queue) / len(self.fps_queue)
self.last_time = t

def draw_detections(self, frame, boxes, scores, classes, scale_x, scale_y):
for box, score, cl in zip(boxes, scores, classes):
x1 = int(box[0] * scale_x)
y1 = int(box[1] * scale_y)
x2 = int(box[2] * scale_x)
y2 = int(box[3] * scale_y)
cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2)
label = f'{CLASSES[cl]} {score:.2f}'
cv2.putText(frame, label, (x1, y1 - 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)

def run(self):
print("Initializing NPU...")
rknn_lite = RKNNLite()
rknn_lite.load_rknn(RKNN_MODEL)
rknn_lite.init_runtime(core_mask=RKNNLite.NPU_CORE_0)
print("NPU ready!")

print(f"Opening camera /dev/video{CAMERA_ID}...")
cap = cv2.VideoCapture(CAMERA_ID, cv2.CAP_V4L2)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
cap.set(cv2.CAP_PROP_FPS, 30)

width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
print(f"Camera: {width}x{height}")
print("Detection running...\n")

fifo = open(FIFO_PATH, 'wb')
frame_count = 0
scale_x = width / IMG_SIZE
scale_y = height / IMG_SIZE

try:
while True:
ret, frame = cap.read()
if not ret:
time.sleep(0.1)
continue

frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
img_resized = cv2.resize(frame_rgb, (IMG_SIZE, IMG_SIZE))
img_input = np.expand_dims(img_resized, 0)

inf_start = time.time()
outputs = rknn_lite.inference(inputs=[img_input])
inf_time = (time.time() - inf_start) * 1000

input0 = outputs[0].reshape([3, -1] + list(outputs[0].shape[-2:]))
input1 = outputs[1].reshape([3, -1] + list(outputs[1].shape[-2:]))
input2 = outputs[2].reshape([3, -1] + list(outputs[2].shape[-2:]))
input_data = [
np.transpose(input0, (2, 3, 0, 1)),
np.transpose(input1, (2, 3, 0, 1)),
np.transpose(input2, (2, 3, 0, 1))
]

boxes, classes, scores = yolov5_post_process(input_data)

if boxes is not None:
self.draw_detections(frame, boxes, scores, classes, scale_x, scale_y)
obj_count = len(boxes)
else:
obj_count = 0

self.calc_fps()
cv2.rectangle(frame, (5, 5), (400, 120), (0, 100, 0), -1)
cv2.putText(frame, f'FPS: {self.fps:.1f}', (15, 35),
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2)
cv2.putText(frame, f'NPU: {inf_time:.1f}ms', (15, 70),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
cv2.putText(frame, f'Objects: {obj_count}', (15, 105),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 0), 2)

_, jpeg = cv2.imencode('.jpg', frame, [cv2.IMWRITE_JPEG_QUALITY, 85])
fifo.write(jpeg.tobytes())
fifo.flush()

frame_count += 1
if frame_count % 50 == 0:
print(f"Frame {frame_count}: FPS={self.fps:.1f}, NPU={inf_time:.1f}ms, Objects={obj_count}")

except KeyboardInterrupt:
print("\nStopping...")
finally:
fifo.close()
cap.release()
rknn_lite.release()
print("Released resources")

YOLODetector().run()
PYTHON_CODE

PYTHON_PID=$!

echo ""
echo "=========================================="
echo "System started!"
echo "Screen should show real-time detection"
echo "Press Ctrl+C to exit"
echo "=========================================="
echo ""

trap "echo ''; echo 'Stopping...'; kill $PYTHON_PID $GST_PID 2>/dev/null; rm -f $FIFO_PATH; echo 'Cleaned up'; exit" INT

wait $PYTHON_PID

kill $GST_PID 2>/dev/null
rm -f $FIFO_PATH
echo "Cleaned"
EOF

chmod +x yolo_npu_display.sh

【截图13:脚本创建完成】

5.3 运行实时检测

./yolo_npu_display.sh

image-20251222175903207

会看到:

  1. 重启3A服务器
  2. 创建FIFO管道
  3. 启动GStreamer显示管道
  4. 启动YOLOv5检测

image-20251222175922272

终端会每50帧输出一次性能统计,例如:

Frame 50: FPS=10.2, NPU=52.3ms, Objects=2
Frame 100: FPS=10.5, NPU=48.7ms, Objects=1

image-20251222175948756

  • 绿色检测框标注物体
  • 左上角显示FPS、NPU延迟、检测数量

Ctrl+C 停止程序。

六、性能分析

6.1 实测数据

我的实时检测结果:

指标数值
平均FPS10.0-10.8
NPU推理延迟47-59 ms
总延迟(含采集/绘制/显示)84-101 ms
最多检测目标数13个物体

image-20251222180048556

6.2 和CPU方案对比

方案FPSCPU占用功耗
MediaPipe(CPU)15-2550-65%
YOLOv5(NPU)10-1115-25%

虽然YOLOv5的FPS略低于MediaPipe手势识别,但要注意:

  • YOLOv5是全场景目标检测(80类物体),MediaPipe只做手部检测(任务简单得多)
  • YOLOv5用的是NPU,CPU占用率降低了60%以上
  • NPU功耗远低于CPU全速运行,发热明显减少
  • 如果只用YOLOv5检测人体(person类),可以进一步优化后处理,FPS还能提升

6.3 为什么没达到理论89 FPS?

ResNet18单张图片能跑89 FPS,为什么实时检测只有10 FPS?瓶颈在哪?

经过profiling分析:

  • NPU推理: ~50ms (主要瓶颈)
  • 摄像头采集: ~5ms
  • 后处理(NMS等): ~15ms
  • 绘制框和文字: ~8ms
  • JPEG编码: ~10ms
  • FIFO传输+GStreamer: ~5ms

总结:

  1. YOLOv5模型比ResNet18大得多(7.9MB vs 12MB),计算量也更大
  2. 后处理的NMS算法是纯Python实现,比较慢(可以改用C++或CUDA加速)
  3. JPEG编码也占了不少时间(可以改用H.264硬件编码)

优化方向:

  • 使用YOLOv5-nano(更小的模型)
  • 后处理用Cython加速
  • 启用NPU双核并行
  • 用RK3576的硬件视频编码器

七、遇到的坑和解决方法

7.1 PC端模型转换依赖地狱

问题: 想在PC上用rknn-toolkit2把MediaPipe的TFLite模型转成.rknn格式,结果遇到protobuf版本冲突——TensorFlow要求<3.20,但rknn-toolkit2要求>=4.25,完全不兼容。

尝试的解决方法:

  • 换TensorFlow版本 → 失败
  • 用虚拟环境 → 用户拒绝(我太懒了...)
  • 清华镜像加速 → 依然冲突

最终方案: 放弃PC端转换,直接用官方提供的.rknn模型测试NPU功能。以后有需要再用Docker跑转换工具。

教训: Python依赖管理真是个大坑,尤其是深度学习框架。强烈建议使用Docker或conda环境隔离。

7.2 摄像头打不开

问题: 直接用cv2.VideoCapture(11)打开失败。

原因: 没有重启rkaiq_3A服务器(负责摄像头的自动曝光/白平衡)。

解决: 在脚本开头加上:

killall rkaiq_3A_server 2>/dev/null
sleep 2
rm -f /tmp/.rkaiq_3A* 2>/dev/null
/etc/init.d/S40rkaiq_3A start >/dev/null 2>&1
sleep 3

7.3 GStreamer找不到videoparse

问题: 一开始想用videoparse插件,结果提示没这个插件。

原因: Buildroot精简系统,很多GStreamer插件没装。

解决: 改用JPEG流传输:

  • Python编码成JPEG → FIFO → GStreamer的jpegparse解码
  • 这个插件是默认安装的

7.4 Python脚本中文编码错误

问题: 脚本里有中文注释,运行报错:

SyntaxError: Non-UTF-8 code starting with '\xe5'

解决: 把所有中文注释改成英文,或者在文件开头加:

# -*- coding: utf-8 -*-

八、总结与展望

8.1 本次实战收获

  1. 成功验证了RK3576的NPU硬件加速能力

    • ResNet18: 89 FPS(11ms延迟)
    • YOLOv5: 10 FPS(50ms NPU延迟)
    • CPU占用降低60%,功耗显著下降
  2. 掌握了RKNN工具链的使用

    • rknn-toolkit-lite2的安装和API
    • .rknn模型的加载和推理
    • NPU核心的指定和配置
  3. 建立了完整的实时检测pipeline

    • 摄像头采集 → NPU推理 → 后处理 → 显示
    • FIFO + GStreamer的显示方案
    • 性能监控和FPS计算
  4. 踩过了各种坑

    • 依赖冲突、摄像头初始化、显示管道等
    • 积累了宝贵的debug经验

8.2 感想

RK3576的NPU确实很强大,6 TOPS的算力在边缘设备中算是顶级配置了。虽然用起来有一些坑(主要是依赖管理),但整体体验还是不错的。

最大的感触是:AI落地不容易啊! 从模型训练到部署,要考虑的东西太多了——精度、速度、功耗、成本...每个环节都需要权衡取舍。但看到实时检测画面流畅运行的那一刻,所有的付出都值了!

九、参考资料

  1. RKNN-Toolkit2官方文档
  2. RK3576 NPU技术白皮书
  3. YOLOv5官方仓库
  4. GStreamer Pipeline设计指南
  5. 我的前期文章

附录A:完整的YOLOv5推理脚本

由于篇幅限制,完整的Python代码已经集成在 yolo_npu_display.sh 脚本中。

关键函数说明:

  • xywh2xyxy(): 边界框坐标转换
  • process(): YOLO输出解析
  • filter_boxes(): 置信度过滤
  • nms_boxes(): 非极大值抑制(去除重叠框)
  • yolov5_post_process(): 完整后处理流程

DshanPI-A1第四篇开源手势项目改造

· 阅读需 8 分钟

本次所有的项目改造主要基于兼容性、流畅性、屏幕显示方式和摄像头调用等方面。

显示输出适配

主要的适配问题

1. RK3576运行纯Wayland环境

RK3576运行纯Wayland环境,没有X11和libGL支持,无法使用传统的cv2.imshow()显示图像。

【解决方案】

采用FIFO + GStreamer + Wayland的显示管线:

# Python端代码
fifo = open('/tmp/gesture_fifo', 'wb')
while True:
_, jpeg = cv2.imencode('.jpg', processed_frame,
[cv2.IMWRITE_JPEG_QUALITY, 85])
fifo.write(jpeg.tobytes())
fifo.flush()
# Shell端代码
# GStreamer从管道读取JPEG流并显示
gst-launch-1.0 filesrc location=/tmp/gesture_fifo ! \
jpegparse ! jpegdec ! videoconvert ! waylandsink fullscreen=true

2. IMX415摄像头第二次启动色彩异常

IMX415摄像头第二次启动时出现色彩异常,内核报错"no first iq setting"。

【解决方案】

在每次打开摄像头前重启rkaiq_3A_server:

def restart_3a(self):
os.system("killall rkaiq_3A_server 2>/dev/null")
time.sleep(2)
os.system("rm -f /tmp/.rkaiq_3A* 2>/dev/null")
os.system("/etc/init.d/S40rkaiq_3A start >/dev/null 2>&1")
time.sleep(5)

项目一:贪吃蛇游戏

项目开源地址:Project2/SnakeGame/main.py at main · WLHSDXN/Project2

改造过程

1. 多层级检测器架构

为了适配不同的依赖环境,设计了三层检测器回退机制:

优先级1: cvzone (MediaPipe封装,精度高)
↓ 不可用
优先级2: 原生MediaPipe (21个关键点)
↓ 不可用
优先级3: HSV肤色检测 (轻量级备用)

代码实现:

# 检测器选择逻辑
if USE_CVZONE:
detector = CvzoneHandDetector(detectionCon=0.8, maxHands=1)
elif USE_MEDIAPIPE:
detector = MediapipeHandDetector(maxHands=1,
detectionCon=0.5,
drawLandmarks=False)
else:
detector = SimpleHandDetector() # HSV备用方案

2. MediaPipe集成与封装

实现了MediapipeHandDetector类,返回与cvzone兼容的数据格式:

class MediapipeHandDetector:
def findHands(self, frame, flipType=False):
img_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
results = self.hands.process(img_rgb)

hands = []
if results.multi_hand_landmarks:
for hand_landmarks in results.multi_hand_landmarks:
# 提取21个关键点坐标
lmList = []
for lm in hand_landmarks.landmark:
x_px = int(lm.x * width)
y_px = int(lm.y * height)
lmList.append([x_px, y_px, lm.z])

hands.append({'lmList': lmList})

return hands, frame

【关键点】

食指指尖是lmList[8],直接用作蛇头控制点。

3. HSV肤色检测备用方案

当MediaPipe不可用时,使用简单的肤色检测:

def detect_hand_simple(frame):
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
mask = cv2.inRange(hsv, [0, 30, 60], [255, 255, 255])

# 形态学去噪
kernel = np.ones((7, 7), np.uint8)
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=3)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations=2)

contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
if contours:
c = max(contours, key=cv2.contourArea)
hull = cv2.convexHull(c)
# 提取最高点作为"指尖"
topmost = hull[hull[:, :, 1].argmin()][0]
return topmost

4. MediaPipe性能调优

优化1: 使用轻量级模型
self.hands = mp.solutions.hands.Hands(
model_complexity=0, # 0=lite, 1=full (默认)
max_num_hands=1,
min_detection_confidence=0.5, # 降低阈值换速度
min_tracking_confidence=0.5
)
优化2: 关闭可视化绘制
# 移除耗时的关键点绘制
# mp_drawing.draw_landmarks(frame, landmarks, connections) # 注释掉
drawLandmarks=False # 新增开关
优化3: 减少输入分辨率
# 640x480 已经是最优平衡点
# 若进一步降低到320x240可提升FPS,但会影响检测精度
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
优化4: 优化相机缓冲
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # 减少延迟

【优化效果】

FPS从5-10提升至15-25 FPS,满足游戏交互需求。

5. 文件控制接口

由于FIFO模式下无法直接捕获键盘,采用控制文件方式:

# Shell脚本部分
# 读取按键并写控制文件
stty echo icanon
while read n1 t 0.1 key; do
if [ "$key" = "r" ]; then
touch /tmp/snake_restart
elif [ "$key" = "q" ]; then
touch /tmp/snake_quit
fi
done
# Python部分
# 检测控制文件
if os.path.exists('/tmp/snake_quit'):
print('Quit command detected')
break

if os.path.exists('/tmp/snake_restart'):
os.remove('/tmp/snake_restart')
# 重置游戏状态
self.game.gameOver = False
self.game.points = []
self.game.previousHead = (0, 0)

效果展示

代码和演示视频均在附件当中

img

img

项目二:虚拟画板

开源项目:【基于手部关键点检测,隔空控制鼠标/隔空绘画】https://www.bilibili.com/video/BV1364y1h7PS?vd_source=a16ca768198c38baa684546cf5060811

改造过程

1. 核心逻辑提取

手势识别逻辑:
fingers = detector.fingersUp() # 返回5个值,1表示手指伸直

# 模式1: 选择工具(食指+中指伸直)
if fingers[1] and fingers[2]:
if y1 < 153: # 在顶部工具栏区域
if 0 < x1 < 320: color = [50, 128, 250] # 蓝色
elif 320 < x1 < 640: color = [0, 0, 255] # 红色
elif 640 < x1 < 960: color = [0, 255, 0] # 绿色
elif 960 < x1 < 1280: color = [0, 0, 0] # 橡皮擦

# 模式2: 绘画(仅食指伸直)
elif fingers[1] and not fingers[2]:
cv2.line(imgCanvas, (xp, yp), (x1, y1), color, brushThickness)
画布合成逻辑:
# 1. 将画布转为灰度图并二值化
imgGray = cv2.cvtColor(imgCanvas, cv2.COLOR_BGR2GRAY)
_, imgInv = cv2.threshold(imgGray, 50, 255, cv2.THRESH_BINARY_INV)

# 2. 使用位运算合成
img = cv2.bitwise_and(img, imgInv) # 摄像头画面保留非绘画区域
img = cv2.bitwise_or(img, imgCanvas) # 叠加绘画内容

2. 显示系统重构

复用贪吃蛇游戏的显示方案: FIFO + GStreamer

3. 工具栏内置化

原项目依赖4张PNG图片作为工具栏,在嵌入式系统上不便管理外部资源。

【解决方案】

用OpenCV绘图API生成工具栏

def create_header(self):
"""动态生成工具栏"""
header = np.zeros((100, self.width, 3), np.uint8)
header[:] = (200, 200, 200) # 灰色背景

tools = [
((250, 128, 50), "Blue"), # BGR格式
((0, 0, 255), "Red"),
((0, 255, 0), "Green"),
((0, 0, 0), "Eraser")
]

section_width = self.width // 4
for i, (color, label) in enumerate(tools):
x1 = i * section_width
x2 = (i + 1) * section_width

# 绘制颜色块
cv2.rectangle(header, (x1 + 10, 20), (x2 - 10, 80), color, -1)
cv2.rectangle(header, (x1 + 10, 20), (x2 - 10, 80),
(255, 255, 255), 2) # 白色边框

# 文字标签
cv2.putText(header, label, (x1 + 20, 95),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (50, 50, 50), 1)

return header

这样就可以对外部产生零依赖,更有利于移植和项目开发!

4. 摄像头与3A服务适配

复用贪吃蛇游戏的解决办法

5. 分辨率与性能权衡

【原版配置】

width = 1280, height = 720
画布: imgCanvas = np.zeros((720, 1280, 3), np.uint8)

【RK3576优化】

width = 640, height = 480 # 降低50%分辨率
画布: imgCanvas = np.zeros((480, 640, 3), np.uint8)

【原因】

  1. MediaPipe在640x480下FPS提升约2倍
  2. 画板应用对分辨率要求不如视觉识别高
  3. JPEG编码/传输速度更快

【工具栏适配】

原版: 顶部153像素高,分4个320像素宽的区域 RK3576: 顶部100像素高,分4个160像素宽的区域

section_width = self.width // 4 # 自适应宽度
if y1 < 100: # 工具栏高度
if 0 < x1 < section_width:
self.color = (250, 128, 50) # 蓝色
elif section_width < x1 < section_width * 2:
self.color = (0, 0, 255) # 红色
# ...

6. 交互控制改进

1. 清空画布机制

【原版】

if all(x >= 1 for x in fingers):
imgCanvas = np.zeros((720, 1280, 3), np.uint8)

【问题】

误触发率高,不便于精细控制。

【RK3576改进】

使用按键控制:

# Shell端
while read -n1 -t 0.1 key; do
if [ "$key" = "c" ]; then
touch /tmp/painter_clear
fi
done
# Python端
if os.path.exists('/tmp/painter_clear'):
os.remove('/tmp/painter_clear')
self.imgCanvas = np.zeros((self.height, self.width, 3), np.uint8)
print("Canvas cleared")
2. 退出控制

【原版】

只能通过cv2.waitKey(1)捕获键盘,依赖窗口焦点。

【RK3576】

双重退出机制:

  1. 按键控制: touch /tmp/painter_quit → Python检测并退出

  2. Ctrl+C: Shell脚本trap信号 → kill所有进程 → 清理FIFO

效果展示

代码和演示视频均在附件当中

img

img

技术总结与经验

  1. 跨平台显示适配 PC上的GUI方案不适用嵌入式,必须根据系统特性(Wayland/Framebuffer)选择输出方式。

  2. 资源内置化 嵌入式系统倾向于单文件部署,外部资源应转为代码生成或打包进程序。

  3. 性能分级优化

    • 算法层: 轻量级模型
    • 实现层: 关闭非必要绘制
    • 硬件层: 缓冲区/分辨率调优
  4. 交互方式适配 GUI依赖的键盘/鼠标事件需改为文件控制或GPIO触发。

DshanPI-A1音频录制播放与噪声分析

· 阅读需 14 分钟

音频播放

扬声器设备

先看以下这个扬声器如何使用,先列出音频播放设备

aplay -l
**** List of PLAYBACK Hardware Devices ****
card 0: rockchipes8388 [rockchip-es8388], device 0: dailink-multicodecs ES8323 HiFi-0 [dailink-multicodecs ES8323 HiFi-0]
Subdevices: 1/1
Subdevice #0: subdevice #0
card 1: rockchiphdmiin [rockchip,hdmiin], device 0: 2a640000.sai-dummy_codec dummy_codec-0 [2a640000.sai-dummy_codec dummy_codec-0]
Subdevices: 1/1
Subdevice #0: subdevice #0
card 2: rockchipdp0 [rockchip-dp0], device 0: rockchip-dp0 spdif-hifi-0 [rockchip-dp0 spdif-hifi-0]
Subdevices: 1/1
Subdevice #0: subdevice #0
card 3: rockchiphdmi [rockchip-hdmi], device 0: rockchip-hdmi i2s-hifi-0 [rockchip-hdmi i2s-hifi-0]
Subdevices: 1/1
Subdevice #0: subdevice #0

可以看出,总共检测到 4张音频卡(card 0 ~ card 3)

  • 播放音频到扬声器/耳机:使用 card 0, device 0
  • HDMI音频输出:使用 card 3, device 0
  • DisplayPort音频:使用 card 2, device 0
  • 捕获HDMI输入音频:使用 card 1, device 0

扬声器是card0:subdevice#0,对应alsa设备就是hw:0,0

**** List of PLAYBACK Hardware Devices ****
card 0: rockchipes8388 [rockchip-es8388], device 0: dailink-multicodecs ES8323 HiFi-0 [dailink-multicodecs ES8323 HiFi-0]
Subdevices: 1/1
Subdevice #0: subdevice #0

创建并播放简单测试音

# 创建8kHz采样率的1kHz正弦波WAV文件(5秒)
ffmpeg -f lavfi -i "sine=frequency=1000:duration=5" -c:a pcm_s16le -ar 8000 test_tone.wav
# 播放该文件
aplay test_tone.wav
Playing WAVE 'test_tone.wav' : Signed 16 bit Little Endian, Rate 8000 Hz, Mono

测试的命令都成功了,但是没有听到声音,需要查看这个扬声器的属性,很可能是音频路由问题,首先需要确定排查思路:

音频芯片(ES8388)设计是:

  • 软件控制层Speaker Switch - 控制音频流是否发送到芯片
  • 硬件控制层OUT1/OUT2 Switch - 控制芯片物理引脚输出

音频播放调试

先打开音频可视化工具看一下

alsamixer -c 0

image-20251205094259863

可以看出playback的状态不正常: MM 00,需要进一步确认是哪个配置的问题,使用 PulseAudio控制工具

pactl list short sinks
0 alsa_output.0.HiFi__hw_rockchipes8388__sink module-alsa-card.c s16le 2ch 44100Hz SUSPENDED
1 alsa_output.1.stereo-fallback module-alsa-card.c s16le 2ch 44100Hz SUSPENDED

作一些解释:

  • Sink(接收器):音频输出的终点
  • PulseAudio架构
应用程序 → 音频流 → PulseAudio服务器 → Sink → 硬件
(播放器) (混音器) (输出设备) (声卡)

状态说明

  • RUNNING:正在播放音频
  • IDLE:空闲,准备就绪
  • SUSPENDED:挂起,节能模式
  • UNLINKED:未连接

设备详情

  • sink 0alsa_output.0.HiFi__hw_rockchipes8388__sink
    • 对应ALSA声卡0(ES8388音频芯片)
    • 高保真(HiFi)输出
  • sink 1alsa_output.1.stereo-fallback
    • 备用/回退输出设备
    • 当主设备不可用时使用

两个音频sink都处于SUSPENDED状态

0 ... SUSPENDED
1 ... SUSPENDED

SUSPENDED状态意味着

  • PulseAudio认为没有音频流需要播放
  • 为了节能,自动挂起了音频输出
  • 系统准备好接收音频,但当前没有活跃的音频流

根本原因链条

  1. 系统启动时 → PulseAudio加载音频设备

  2. 没有活跃音频流 → PulseAudio挂起设备(SUSPENDED)

  3. 硬件路由未激活 → OUT1/OUT2开关默认关闭

  4. 开始播放音频时

    • PulseAudio唤醒设备
    • 硬件开关(OUT/OUT2)还是关闭状态
    • 需要手动 amixer -c 0 sset 'OUT1' on

    经过我测试,在打开

    amixer -c 0 sset 'OUT2' on

    之后,扬声器音频就可以正常播放了,这里作为记录,列出我调试过程中使用到的一些有用的命令

    #1. 查看更详细的sink信息
    pactl list sinks
    Sink #0
    ...
    Sink #1
    ...
    # 2. 查看当前音频流
    pactl list sink-inputs
    #3.alsa内容控制
    amixer -c 0 scontents
    ...
    amixer -c 0 get 'Speaker'
    amixer -c 0 get 'Master'
    amixer -c 0 get 'Headphone'

对于这个问题有多个解决方案,下面列出

方案1:阻止自动挂起

# 编辑PulseAudio配置
vi /etc/pulse/default.pa
# 防止自动挂起
load-module module-suspend-on-idle timeout=0 # 0表示永不挂起
# 增加超时时间
load-module module-suspend-on-idle timeout=3600 # 1小时
# 重启PulseAudio
pulseaudio -k
pulseaudio --start

方案2:启动时自动激活硬件

# 创建启动脚本 /etc/pulse/audio-init.sh
#!/bin/bash
# 等待PulseAudio启动
sleep 3
# 激活硬件输出
amixer -c 0 sset 'OUT2' on
amixer -c 0 sset 'Speaker' on
# 设置合适音量
amixer -c 0 sset 'Output 2' 90%

方案3:使用udev规则

# 创建 /etc/udev/rules.d/90-audio.rules
ACTION=="add", SUBSYSTEM=="sound", KERNEL=="card0", \
RUN+="/usr/bin/amixer -c 0 sset 'OUT2' on"
#重启后生效

方案4:最简单的临时测试

# 先激活设备再播放
amixer -c 0 sset 'Speaker' on
amixer -c 0 sset 'OUT2' on
amixer -c 0 sset 'Output 2' 90%
#也可以永久保存设置(并未生效)
alsactl store

总结:问题实际上是:

  • 软件层:PulseAudio正常
  • 驱动层:ALSA正常识别设备
  • 硬件层:OUT2物理开关需要手动激活

tips:音频路由

# 假设有多个音频设备:
# 0 - 内置扬声器
# 1 - USB耳机
# 2 - HDMI输出

# 将Chrome音频发送到耳机
pactl move-sink-input $(pactl list short sink-inputs | grep chrome | awk '{print $1}') 1
# 将音乐播放器发送到HDMI
pactl move-sink-input $(pactl list short sink-inputs | grep spotify | awk '{print $1}') 2

音频录制

使用得硬件是百问网200w usb摄像头+音频mems一体化模块,如下图所示

7de0c186abbafe3783a045089397308b

设备信息获取

首先需要获得整个mems得信息,它是通过usb与rk3576通信得,有几个方法能看出它得信息

v4l2-sysfs-path
Video device: video36
video: video37
sound card: hw:4
pcm capture: hw:4,0
mixer: hw:4
Video device: video37
sound card: hw:4
pcm capture: hw:4,0
mixer: hw:4
.....
alsactl info
......
- card: 4
id: Camera
name: USB 2.0 Camera
longname: lihappe8 Corp. USB 2.0 Camera at usb-xhci-hcd.8.auto-1.2.2, high speed
driver_name: USB-Audio
mixer_name: USB Mixer
components: USB038f:0541
controls_count: 4
pcm:
- stream: CAPTURE
devices:
- device: 0
id: USB Audio
name: USB Audio
subdevices:
- subdevice: 0
name: subdevice #0
.....

可以看出在alsa里这个设备得name是hw:4,0

音频录制测试

#录制一段背景噪声
arecord -D hw:4,0 -f S16_LE -r 8000 -c 2 -d 10 noise
Recording WAVE 'noise.wav' : Signed 16 bit Little Endian, Rate 8000 Hz, Stereo
aplay noise.wav
Playing WAVE 'noise.wav' : Signed 16 bit Little Endian, Rate 8000 Hz, Stereo

可以听到明显得”吱吱“声,这个mems的底噪还是很大的,后续需要对其进行降噪,才可以进行正常的语音通讯。

噪声分析

用sox生成频谱图

sox noise.wav -n spectrogram -o noise_spectrogram.pn

noise_spectrogram

#全局频谱
sox noise.wav -n spectrogram -d 10 -x 1200 -z 80 -o noise_full.png

noise_full

sox noise.wav -r 2000 -c 1 noise_2k.wav
sox noise_2k.wav -n spectrogram -d 10 -x 1200 -z 80 -o noise_low.png

noise_low

整体观察背景噪声属于「近似白噪声 + 低频强峰 + 中频轻微纹理噪声」

1. 0~200Hz 附近有非常明显的低频能量团(尤其 <100Hz)

这一般意味着:

  • 机械/电源相关噪声
  • 风扇、震动、机箱共振
  • 电源纹波(50Hz / 60Hz + 基波)
  • 麦克风指向性/箱体耦合放大超低频噪声

2. 1kHz~4kHz 范围是随机噪声(近似白噪声),能量较低

说明麦克风底噪属于典型:

  • MEMS 麦克风本底噪声
  • ADC 自噪声
  • 放大器输入噪声

这部分是系统底噪

3. 高频(>6kHz)完全没有奇怪峰值

这非常好 → 没有明显:

  • 数字 EMI
  • 时钟泄漏
  • 采样抖动噪声

4. 没有明显「啸叫模式」出现

三张图都没有看到典型啸叫特征:

  • 啸叫通常是固定频率一条亮线持续不变
  • 图中只有起始瞬间的脉冲(可能是开始录音时的点击声)

没有稳定峰随时间持续。

逐图分析


(A) 第一张图:全频谱(到 4kHz)

看起来特点是:

整体偏红/紫,噪声密度高但均匀 50Hz 左右有明显竖线 100Hz、150Hz 也有轻微能量

➜ 这几乎肯定是:

工频噪声(50/60Hz)+ 谐波(100/150Hz)

原因:

  • USB 供电带来的大量 50/60Hz hum
  • 声卡或麦克风的模拟前端隔离不好
  • 地线不干净(如 USB 共地回路)

(B) 第二张图:动态范围压窄的版本(-80dBFS)

这一张另外暴露出:

在 1.8kHz ~ 2.2kHz 有一条非常窄的横线

非常淡,但稳定存在。

这表示:

系统时钟/PLL 干扰泄漏

常见于:

  • I2S/MCLK 泄漏
  • PCB 上麦克风的时钟耦合
  • 数字电源噪声叠加到麦克风模拟部分

这部分不会导致啸叫,但会降低 SNR。


(C) 第三张图:低频到 1000Hz

非常典型:

<150Hz 区域噪声比其他频段高很多

像一个大“灯泡”形状,非常明显。

这说明:

低频振动 + 电源噪声是主要底噪来源

包括:

  • 机箱震动、风扇、台面共振
  • 电源 50/60Hz + 谐波
  • 麦克风本身的 LF roll-off 不够

综合判断:背景噪声构成比例

噪声类型占比特征
低频机械/电源噪声 (<200Hz)50%最大来源,来自电源、机箱、振动
工频泄漏 50/60Hz + 谐波25%图中最强的固定峰
麦克风本底白噪声20%散布在 1k~4kHz 随机噪声
数字时钟泄漏(2kHz 附近)5%很弱但可见的一条细线

PSD噪声模型分析

使用python生成PSD噪声模型

import numpy as np
import matplotlib.pyplot as plt
from scipy.io import wavfile
from scipy.signal import welch, find_peaks, spectrogram, butter, sosfilt
import IPython.display as ipd
import os

rate, data = wavfile.read("/mnt/data/noise.wav")
if data.ndim>1:
data = data.mean(axis=1)
data = data.astype(np.float32)
N = len(data)
duration = N / rate

# Calculate overall RMS and dBFS
# Assuming 16-bit PCM if dtype was int16; determine scale
# infer max possible value from original dtype by reloading header:
import struct
# Determine dtype max
# but we'll normalize by max of int16 if dtype came as int16, else use max(abs(data))
max_possible = 32768.0
rms = np.sqrt(np.mean(data**2))
dbfs_rms = 20*np.log10(rms / max_possible) if rms>0 else -np.inf

# Welch PSD
f, Pxx = welch(data, fs=rate, nperseg=4096, scaling='density')
# find peaks in PSD (in linear)
peaks, props = find_peaks(Pxx, height=np.max(Pxx)*0.15, distance=5)
peak_freqs = f[peaks]
peak_heights = props['peak_heights']

# Find dominant low-frequency peak under 500Hz
low_idx = np.where(f<=500)[0]
low_f = f[low_idx]
low_P = Pxx[low_idx]
lp_peaks, lp_props = find_peaks(low_P, height=np.max(low_P)*0.2)
lp_freqs = low_f[lp_peaks]
lp_heights = lp_props['peak_heights']

# Short-time energy to find transient (e.g., first second pulse)
frame_ms = 20
frame_len = int(rate * frame_ms/1000)
hop = frame_len//2
frames = []
for start in range(0, N-frame_len, hop):
frames.append(np.sum(data[start:start+frame_len]**2))
frames = np.array(frames)
frame_times = (np.arange(len(frames))*hop)/rate

# detect where energy spikes relative to median
median_e = np.median(frames)
spikes = np.where(frames > median_e*8)[0] # 8x median
spike_times = frame_times[spikes]

# Spectrogram
f_s, t_s, Sxx = spectrogram(data, fs=rate, nperseg=2048, noverlap=1024, scaling='density', mode='magnitude')

# Plot PSD with peaks marked
plt.figure(figsize=(10,5))
plt.semilogy(f, Pxx, color='tab:orange')
plt.scatter(peak_freqs, peak_heights, color='k', zorder=5)
for pf, ph in zip(peak_freqs, peak_heights):
plt.text(pf, ph*1.1, f"{pf:.0f} Hz", fontsize=8, ha='center')
plt.xlim(0, rate/2)
plt.xlabel("Frequency (Hz)")
plt.ylabel("PSD")
plt.title("Welch PSD with detected peaks")
plt.grid(alpha=0.3)
plt.tight_layout()
plt.savefig("/mnt/data/psd_peaks.png")

# Plot spectrogram (dB)
Sxx_db = 20*np.log10(Sxx + 1e-12)
plt.figure(figsize=(10,5))
plt.pcolormesh(t_s, f_s, Sxx_db, shading='gouraud')
plt.colorbar(label='dB')
plt.ylim(0, 4000)
plt.xlabel("Time (s)")
plt.ylabel("Frequency (Hz)")
plt.title("Spectrogram (dB)")
plt.tight_layout()
plt.savefig("/mnt/data/spectrogram_db.png")

# Prepare summary
summary = {
"sampling_rate": rate,
"duration_s": duration,
"rms": float(rms),
"dbfs_rms": float(dbfs_rms),
"dominant_peaks_hz": [float(p) for p in peak_freqs[:8]],
"dominant_peaks_vals": [float(p) for p in peak_heights[:8]],
"low_freq_peaks_hz": [float(p) for p in lp_freqs],
"low_freq_peaks_vals": [float(p) for p in lp_heights],
"spike_times_s": [float(s) for s in spike_times[:10]],
"spectrogram_image": "/mnt/data/spectrogram_db.png",
"psd_image": "/mnt/data/psd_peaks.png"
}

import json
with open("/mnt/data/noise_analysis_summary.json","w") as f:
json.dump(summary, f, indent=2)

# Display small tables and figures
from caas_jupyter_tools import display_dataframe_to_user
import pandas as pd

df_peaks = pd.DataFrame({
"freq_hz": peak_freqs,
"psd_val": peak_heights
})
display_dataframe_to_user("Detected PSD Peaks", df_peaks.head(20))

plt.figure(figsize=(10,3))
plt.plot(frame_times, 10*np.log10(frames+1e-12))
plt.xlabel("Time (s)")
plt.ylabel("Frame energy (dB)")
plt.title("Short-time frame energy (20ms frames)")
plt.grid(alpha=0.3)
plt.tight_layout()
plt.savefig("/mnt/data/frame_energy.png")
plt.show()

# Show audio player
ipd.display(ipd.Audio("/mnt/data/noise.wav"))

summary

image-20251205153910716

image-20251205153954232

image-20251205154029359

image-20251205154110367

得出关键量化结果:

  • 采样率:8000 Hz
  • 时长:10.0 s
  • 全段 RMS = 223.11 (样点),换算为 -43.34 dBFS(以 16-bit 满量程 32768 作归一): 说明底噪中等偏高
  • 检测到的显著频率峰(Welch PSD peak,按强度顺序,列出前几项):
    • 74.2 Hz, 85.94 Hz, 103.52 Hz, 113.28 Hz, 146.48 Hz, 167.97 Hz, 以及较小的 396 Hz 等。
  • 低频段(≤500 Hz)主要峰:85.94, 95.70, 103.52, 113.28, 146.48 Hz —— 多个近频段峰,像是工频/开关电源谐波或机械谐振的集合。
  • 瞬态能量峰(短时能量帧,20 ms)检测到在:约 0.36–0.38 s 有几个短脉冲(可能是启动点击/人为触碰/瞬态事件)。

结论:噪声由明显的低频“hum/纹波/振动”组成 + 宽带白噪声(中高频),低频是主要能量所在。

降噪方案

1.在音频链路上加 2 阶高通 (fc = 80 Hz)

  • 直接把大部分机械/供电低频去掉,不损伤语音带宽(语音主要 >100Hz)。
  • 实现:用 DSP 库里的 butter/sos 或自行实现biquad(Direct Form I 或 II)。

2 针对性陷波(notch)

  • 若 HPF 后仍留有几个极窄强峰(如 103Hz),加一两个 Q=20~40 的陷波。Q 越高带宽越窄,越不伤及邻近频段,但系数更接近 1。
  • 考虑实时性需要串联 hp -> notch(强峰1) -> notch(强峰2)

3 本质还是硬件

  • 更换或改善模拟供电(低噪 LDO、更多旁路电容、星形接地)或使用SNR更好的数字 MEMS 麦克风。
  • 在嵌入式板子上,尽量远离开关电源走线与麦克风差分/模拟线。

对实时音质的考虑

  • 如果需要非常低延迟(例如回声取消 / 实时 loopback),得使用 IIR(biquad)单向实时实现(延迟极低),但注意相位会改变,若不允许相位改变(比如做更精确的定位),用 FIR + zero-phase 会有延迟代价。

参考滤波系数

说明:下面给出的系数是标准二阶节(biquad)格式,按 [b0, b1, b2, a0, a1, a2](a0 已正规化为 1 或者给出时需要归一化)排列。实现时通常把 a0 规一化为 1,然后按 Direct Form I/II 实现。

高通:2nd-order Butterworth(fc = 80 Hz, fs = 8000 Hz)

sos_hp 第一节系数(已归一化 a0=1):

b0 = 0.9565432255568767
b1 = -1.9130864511137533
b2 = 0.9565432255568767
a0 = 1.0
a1 = -1.911197067426073
a2 = 0.9149758348014336

若干陷波(notch, Q=30)

(示例三条,取自检测到的峰位)格式同上(b0,b1,b2,a0,a1,a2),a0 已正规化为 1 在下面输出中:

  • notch @ 103.515625 Hz
b0 = 0.998648305437691
b1 = -1.99069933085876
b2 = 0.998648305437691
a0 = 1.0
a1 = -1.99069933085876
a2 = 0.9972966108753819
  • notch @ 146.484375 Hz
b0 = 0.9980904047539934
b1 = -1.9829844796660758
b2 = 0.9980904047539934
a0 = 1.0
a1 = -1.9829844796660758
a2 = 0.9961808095079867
  • notch @ 74.21875 Hz
b0 = 0.9990299707952924
b1 = -1.9946663265604048
b2 = 0.9990299707952924
a0 = 1.0
a1 = -1.9946663265604048
a2 = 0.9980599415905849

这些噪声分析是作传统频减法音频滤波必要的素材,采用标准的rnnoise AI降噪则不需要,但如果自己改进rnnoise,做模型微调则需要参考,以获得更好的降噪效果。

T113x系列异构简介

· 阅读需 17 分钟
世玉轩
100askTeam yuxuan.
韦东山
100askTeam maintainer

T113s4芯片包含四个CPU。两个主核心Arm A7 CPU,运行Tina Linux(全志自研Linux)系统,为芯片主系统;

一个是RISC-V C906辅助CPU,运行RTOS(全志自研RTOS)系统,主要功能是提供通用算力补充、实现实时控制、辅助 Linux 实现快起和低功耗管理等功能。

还有一个是 DSP HIFI4 核心,主要是用来处理音频算法部分,因为这部分编译器需要企业单独申请,所以无法进行开发使用。

T113家族

如下内容摘自 T113x_Datasheet_V1.3.pdf 版本

image-20240909151235761

T113芯片框图

image-20240909151155233

简单描述

image-20240909151332822

异构子系统框架

参阅 T113x_User_Manual_V1.2.pdf

image-20240909152022940

image-20240909151608100

相关内存分布

image-20240909153500017

MSGBOX 与AMP RPMSG

在 Tina Linux 中,提供 AMP 与 RPMsg 对接 C906

  1. Linux remoteproc 管理控制 C907
  2. RPMsg 与 C907 通讯

T113 的异构系统通讯在硬件上使用的是 MSGBOX,在软件层面上使用的是 AMP 与 RPMsg 通讯协议。其中 A7 上基于 Linux 标准的 RPMsg 驱动框架,C906基于 OpenAMP 异构通信框架。

T113 所带有的 A7 主核心与 C906 辅助核心是完全不同的两个核心,为了最大限度的发挥他们的性能,协同完成某一任务,所以在不同的核心上面运行的系统也各不相同。这些不同架构的核心以及他们上面所运行的软件组合在一起,就成了 AMP 系统 (Asymmetric Multiprocessing System, 异构多处理系统)。

由于两个核心存在的目的是协同的处理,因此在异构多处理系统中往往会形成 Master - Remote 结构。主核心启动后启动从核心。当两个核心上的系统都启动完成后,他们之间就通过 IPC(Inter Processor Communication)方式进行通信,而 RPMsg 就是 IPC 中的一种。

在AMP系统中,两个核心通过共享内存的方式进行通信。两个核心通过 AMP 中断来传递讯息。内存的管理由主核负责。

image-20220704155816774

AMP 系统在每个通信方向上都有两个缓冲区,分别是 USEDAVAIL,这个缓冲区可以按照 RPMsg 中消息的格式分成一块一块链接形成一个环。

image-20220704160952936

当主核需要和从核进行通信的时候可以分为四步:

(1)主核先从USED中取得一块内存(Allocate)

(2)将消息按照消息协议填充

(3)将该内存链接到 AVAIL 缓冲区中(Send)

(4)触发中断,通知辅助核有消息处理

img

反之,从核需要和主核通信的时候也类似:

(1)从核先从AVAIL中取得一块内存(Allocate)

(2)将消息按照消息协议填充

(3)将该内存链接到 USED 缓冲区中(Send)

(4)触发中断,通知主核有消息处理。

img

RPMSG 协议

既然 RPMsg 是一种信息交换的协议,与TCP/IP类似,RPMsg 协议也有分层,主要分为三层,分别是传输层、MAC层和物理层。

image-20220704161948895

其中 MAC层 的 VirtIO 是一种I/O 半虚拟化解决方案,是一套通用 I/O 设备虚拟化的程序,是对半虚拟化 Hypervisor 中的一组通用 I/O 设备的抽象。 提供了一套上层应用与各 Hypervisor 虚拟化设备之间的通信框架和编程接口,减少跨平台所带来的兼容性问题,大大提高驱动程序开发效率。

RPMsg 总线上的消息都具有以下结构,包含消息头和数据两个固定的部分,该消息格式的定义位于drivers/rpmsg/virtio_rpmsg_bus.c中,具体定义如下:

struct rpmsg_hdr {
u32 src;
u32 dst;
u32 reserved;
u16 len;
u16 flags;
u8 data[];
} __packed;

异构系统控制

在异构系统中,不止需要消息的传输,还需要相关控制。例如主核对辅助核心的开启,加载固件,关闭等等。这就需要用到 remoteproc 框架。

remoteproc 框架支持对不同平台,不同架构的处理器进行控制,可以监控辅助核心的运行情况。

对于 T113 来说,remoteproc 用于对 C906 进行生命周期管理,一般来说包含有加载固件、 检测远端处理器是否崩溃等功能。它在加载远端处理器的固件时,会根据固件中定义的 resource table 来申请资源,并创建 VirtIO 设备。

remoteproc 框架抽象出硬件差异,允许不同的平台/架构来控制(开机、加载固件、关机)这些远程处理器,此外,还为支持这种通信的远程处理器添加了 rpmsg virtio 设备。这样,特定平台的 remoteproc 驱动程序只需要提供一些低级处理程序,然后所有 rpmsg 驱动程序就可以正常工作。

作用:

  • 从文件系统加载固件
  • 准备远程处理器所需资源
  • 注册一个 rpmsg virtio 设备
  • 提供对要提供对远程处理器的生命周期进行管理

img

所以固件的加载流程大致如下:

1. 加载固件
1. 调用 firmware 接口获取文件系统中的固件
2. 解析固件的 resource_table 段,该段有如下内容
1. 声明需要的内存(Linux 为其分配)
2. 声明使用的 vdev(固定为一个)
3. 声明使用的 vring(固定为两个)
3. 将固件加载到指定地址
2. 注册 rpmsg virtio 设备
1. 提供 vdev->ops(基于 virtio 接口实现的)
2. 与 rpmsg_bus 驱动匹配,完成 rpmsg 初始化
3. 启动小核
1. 调用 rproc->ops->start

Linux内核配置

首先需要配置设备树,预留 C906 核心内存,buffer 内存,vring 内存等。并正确配置 rprocrpbuf,也不要忘记配置 firmware-name,下面的配置是T113默认提供的示例 为测试固件所使用的地址。不同的固件地址可能不同。

reserved-memory {
#address-cells = <2>;
#size-cells = <2>;
ranges;

/* c906 */
c906_ddr: c906_ddr@42300000 {
reg = <0x0 0x42300000 0x0 0x00600000>;
no-map;
};
/*
* The name should be "vdev%dbuffer".
* Its size should be not less than
* RPMSG_BUF_SIZE * (num of buffers in a vring) * 2
* = 512 * (num of buffers in a vring) * 2
*/
rv_vdev0buffer: vdev0buffer@42900000 {
compatible = "shared-dma-pool";
reg = <0x0 0x42900000 0x0 0x40000>;
no-map;
};

/*
* The name should be "vdev%dvring%d".
* The size of each should be not less than
* PAGE_ALIGN(vring_size(num, align))
* = PAGE_ALIGN(16 * num + 6 + 2 * num + (pads for align) + 6 + 8 * num)
*
* (Please refer to the vring layout in include/uapi/linux/virtio_ring.h)
*/
rv_vdev0vring0: vdev0vring0@42940000 {
reg = <0x0 0x42940000 0x0 0x2000>;
no-map;
};
rv_vdev0vring1: vdev0vring1@42942000 {
reg = <0x0 0x42942000 0x0 0x2000>;
no-map;
};

/* dsp0 */
dsp0ddr: dsp0ddr@42000000 {
reg = <0x0 0x42000000 0x0 0x00100000>;
no-map;
};

dsp0_rpbuf_reserved: dsp0_rpbuf@42244000 {
compatible = "shared-dma-pool";
no-map;
reg = <0x0 0x42244000 0x0 0x8000>;
};

/*
* The name should be "vdev%dbuffer".
* Its size should be not less than
* RPMSG_BUF_SIZE * (num of buffers in a vring) * 2
* = 512 * (num of buffers in a vring) * 2
*/
vdev0buffer: vdev0buffer@42200000 {
compatible = "shared-dma-pool";
reg = <0x0 0x42200000 0x0 0x40000>;
no-map;
};

/*
* The name should be "vdev%dvring%d".
* The size of each should be not less than
* PAGE_ALIGN(vring_size(num, align))
* = PAGE_ALIGN(16 * num + 6 + 2 * num + (pads for align) + 6 + 8 * num)
*
* (Please refer to the vring layout in include/uapi/linux/virtio_ring.h)
*/
vdev0vring0: vdev0vring0@42240000 {
reg = <0x0 0x42240000 0x0 0x2000>;
no-map;
};
vdev0vring1: vdev0vring1@42242000 {
reg = <0x0 0x42242000 0x0 0x2000>;
no-map;
};

/*
* dsp ram addr
*/
dsp0dram: dsp0dram@400000 {
reg = <0x0 0x400000 0x0 0x10000>;
no-map;
};
dsp0iram0: dsp0iram0@420000 {
reg = <0x0 0x420000 0x0 0x8000>;
no-map;
};
dsp0iram1: dsp0iram1@440000 {
reg = <0x0 0x440000 0x0 0x8000>;
no-map;
};
};

mailbox_heartbeat: mailbox_heartbeat@0 {
compatible = "mailbox-heartbeat";
rproc-np = <&c906_rproc>;
mboxes = <&msgbox 6>, <&msgbox 7>;
mbox-names = "tx", "rx";
status = "okay";
};

dsp0_rproc: dsp_rproc@0 {
compatible = "allwinner,hifi4-rproc", "simple-bus";
clock-frequency = <600000000>;
clocks = <&ccu CLK_PLL_PERIPH0_2X>, <&ccu CLK_DSP>, <&ccu CLK_BUS_DSP_CFG>, <&r_ccu CLK_R_AHB>;
clock-names = "pll", "mod", "cfg", "ahbs";
resets = <&ccu RST_BUS_DSP>, <&ccu RST_BUS_DSP_CFG>, <&ccu RST_BUS_DSP_DBG>, <&ccu RST_BUS_MSGBOX1>;
reset-names = "mod-rst", "cfg-rst", "dbg-rst", "msg-rst";
reg = <0x0 0x03000008 0x0 0x04>,
<0x0 0x01700000 0x0 0x40>;
reg-names = "sram-for-cpux", "hifi4-cfg";
mboxes = <&msgbox 0>;
mbox-names = "arm-kick";
memory-region = <&dsp0ddr>, <&vdev0buffer>, <&vdev0vring0>, <&vdev0vring1>,
<&dsp0dram>, <&dsp0iram0>, <&dsp0iram1>;
memory-mappings =
/* < DA len PA > */
/* local SRAM via external bus */
< 0x28000 0x20000 0x28000 >,
/* local SRAM via internal bus */
< 0x400000 0x10000 0x400000 >,
< 0x420000 0x8000 0x420000 >,
< 0x440000 0x8000 0x440000 >,
/* DDR front 256MB */
< 0x10000000 0x10000000 0x40000000 >,
/* local SRAM via internal bus */
< 0x20028000 0x10000 0x400000 >,
< 0x20038000 0x8000 0x420000 >,
< 0x20040000 0x8000 0x440000 >,
/* DDR front 256MB */
< 0x30000000 0x10000000 0x40000000 >,
/* DDR front 1GB */
< 0x40000000 0x40000000 0x40000000 >,
/* DDR front 1GB */
< 0x80000000 0x40000000 0x40000000 >,
/* DDR front 1GB */
< 0xC0000000 0x40000000 0x40000000 >;

id = <0>;
status = "okay";
};

rpbuf_controller0: rpbuf_controller@0 {
compatible = "allwinner,rpbuf-controller";
remoteproc = <&dsp0_rproc>;
ctrl_id = <0>; /* index of /dev/rpbuf_ctrl */
//iommus = <&mmu_aw 5 1>;
memory-region = <&dsp0_rpbuf_reserved>;
status = "okay";
};

rpbuf_sample: rpbuf_sample@0 {
compatible = "allwinner,rpbuf-sample";
rpbuf = <&rpbuf_controller0>;
status = "okay";
};

c906_rproc: c906_rproc@0 {
compatible = "allwinner,c906-rproc";
clock-frequency = <800000000>;
clocks = <&ccu CLK_PLL_PERIPH0_800M>, <&ccu CLK_RISCV>, <&ccu CLK_BUS_RISCV_CFG>, <&ccu CLK_RISCV_RST>, <&ccu CLK_BUS_RISCV>;
clock-names = "pll", "mod", "cfg", "riscv-rst", "riscv-gate";
resets = <&ccu RST_BUS_RISCV_CFG>, <&ccu RST_BUS_MSGBOX2>;
reset-names = "cfg-rst", "msg-rst";
memory-region = <&c906_ddr>, <&rv_vdev0buffer>, <&rv_vdev0vring0>, <&rv_vdev0vring1>;
reg = <0x0 0x06010000 0x0 0x1000>;
reg-names = "c906-cfg";
mboxes = <&msgbox 4>;
mbox-names = "arm-kick";
memory-mappings =
/* DA len PA */
/* DDR for c906 */
< 0x40000000 0x10000000 0x40000000 >;
firmware-name = "amp_rv0.bin";
status = "okay";
};

配置内核驱动

接下来需要配置 kernel 选项,配置驱动。

./build.sh menuconfig

并勾选以下驱动:

> Device Drivers > Mailbox Hardware Support
--- Mailbox Hardware Support
< > Platform MHU Mailbox
< > Altera Mailbox
< > Mailbox Test Client
< > Mailbox Heartbeat driver
<*> Allwinner mailbox support
[ ] Allwinner mailbox support txdone irq
> Device Drivers > Remoteproc drivers
[*] Support for Remote Processor subsystem
<*> Allwinner remoteproc support
<*> Allwinner remoteproc hifi4 boot
<*> Allwinner remoteproc c906 boot
> Device Drivers > Rpmsg drivers
< > RPMSG device interface
< > Qualcomm RPM Glink driver
-*- sunxi amp msgbox driver
< > support send dsp standby msg when suspend.
<*> Virtio RPMSG bus driver
< > Allwinner rpmsg notify driver
<*> Allwinnertech RPMSG hearbeat driver
<*> Allwinner RPMsg tty driver
< > Allwinner RPMsg client sample
<*> sunxi rpmsg ctrl driver
< > Allwinner rpmsg openamp test driver

加载小核固件

测试固件下载地址: https://github.com/DongshanPI/T113M4-DevKit_TinaSDK5

烧录启动系统后,可以在 /sys/kernel/debug/remoteproc/ 节点找到 remoteproc1

image-20240909160554949

我们可以使用 cat 命令检查小核目前的状况

cat /sys/kernel/debug/remoteproc/remoteproc1/state

image-20240909160830827

可以看到提示没有 state 这个节点信息,那么应该是没有默认加载 c906 小核固件。此时我们需要把准备好的固件放置到开发板的 lib/firmware 文件夹内。这里我们使用 adb 上传小核固件。

然后我们将 c906 固件置于 firmware 节点内,并启动固件。

echo amp_rv0.bin > /sys/class/remoteproc/remoteproc1/firmware
echo start > /sys/class/remoteproc/remoteproc1/state

此时可以看到 remoteproc remoteproc1: remote processor c906_rproc is now up,同时查看状态也显示了 running

image-20240909161015908

此时也可以用 stop 命令停止小核运行

echo stop > /sys/class/remoteproc/remoteproc1/state

image-20240909161103533

AMP SHELL测试小核

Linux端提供了进入从核控制台的功能也就是AMP SHELL,可以直接在Linux终端中,通过 amp_shell 命令进入 C906 异构核心。

首先确保C906 小核已经正常启动,之后使用如下命令进入小核 shell 终端。

root@TinaLinux:~# amp_shell -d /dev/rpmsg_ctrl-c906_rproc@0

便可以启动小核的固件

image-20240910172921007

RTOS系统介绍

系统架构

RTOS 系统是基于FreeRTOS 内核的软件开发包,包含了系统开发用到的内核源码、驱动、工具、组件与应用程序包。通过Makefile 脚本和Kconfig 配置文件,使得用户可以通过menuconfig 进行个性化裁减,编译出一个可以直接烧写到机器上运行的RTOS 系统软件。

image-20240909152743452

RTOS 系统框图如上图,仅从软件的角度来看,从下至上分为内核层、组件层、应用层三个层次。各层次主要内容如下:

  • Kernel:内核层包括FreeRTOS 核心系统、文件系统、网络系统、BSP 驱动等。
  • Component:组件层包括控制台、多媒体、功耗管理、OTA、音频系统、显示系统、图像采集等。
  • APP:应用层包括各种应用demo。

目录结构

rtos
├── board # 包含各SoC板级配置目录
│ └── t113_s3p_c906 # t113_s3p_c906板级配置目录
│ └── t113_s4_c906 # t113_s4_c906板级配置目录
│ └── t113_s4p_c906 # t113_s4p_c906板级配置目录
│ └── XXX # XXX平台板级配置目录
├── envsetup.sh # SDK环境初始化脚本
├── lichee
│ ├── dsp # DSP FreeRTOS系统
│ ├── rtos # ARM/RISC‑V架构的FreeRTOS系统
│ ├── rtos‑components # FreeRTOS公共组件
│ └── rtos‑hal # BSP驱动
└── tools # 打包相关工具脚本目录

所使用RTOS SDK 目录结构如上所示,主要包括如下几个关键目录:

  • board:板级配置目录,用于存放芯片方案的配置文件,主要包括系统配置文件sys_config.fex等。
  • lichee/dsp:存 放DSP FreeRTOS 系统、组件、应用。
  • lichee/rtos:存放ARM/RISC‑V 架构FreeRTOS 系统、组件、应用。
  • lichee/rtos‑components:公共组件目录,lichee/dsp 与lichee/rtos 都可以使用该组件。
  • lichee/rtos‑hal:BSP 驱动目录,用于存放各种驱动代码。对lichee/dsp 与lichee/rtos 通用。
  • tools:工具目录,用于存放编译打包相关的脚本、工具等。
  • 下面对lichee/rtos、lichee/rtos‑hal 目录进行详细说明。lichee/dsp 目录与lichee/rtos 目录类似,此处不做介绍。

lichee/rtos 目录

lichee/rtos
├── arch # 处理器架构相关
├── build # 编译临时文件输出目录
├── components # 组件
├── drivers # 驱动
├── include # 头文件
├── kernel # FreeRTOS 内核
├── projects # 方案工程
├── scripts
└── tools # 工具链

lichee/rtos 目录主要包括arch(架构相关)、components(组件)、drivers(驱动)、include (头文件)、kernel(内核)、projects(工程)、toos(工具链) 等目录,下面对常用重要目录分别进行介绍。

  • arch 目录 arch 目录主要放置跟SoC 架构相关的内容,每个SoC 单独目录管理,主要包括跟risc‑v 架构相关的ARCH 初始化、中断处理、异常处理、内存映射相关功能的实现。
lichee/rtos/arch/
├── common
└── risc‑v
├── arch.mk
├── c906
├── common
├── e906
├── includes
├── Kconfig
├── Makefile
├── sun55iw3p1
└── sun8iw20p1
  • components 目录 components 目录包含allwinner 和第三方的组件。
lichee/rtos/components/
├── aw
│ ├── blkpart
│ ├── bluetooth
│ ├── csi
│ ├── devfs
│ ├── healthd
│ ├── ......
│ ├── watchpoint
│ └── wireless_video
├── common ‑> ../../rtos‑components
└── thirdparty
├── common
├── console
├── cplusplus
├── elmfat
├── finsh_cli
├── ......
└── vfs
  • drivers 目录 drivers 目录包含所需的外设驱动,主要包括各外设控制器驱动的具体实现(hal 软连接)以及OSAL 层接口(osal)。
lichee/rtos/drivers/
├── drv
│ ├── cpufreq
│ ├── leds
│ ├── uart
│ ├── ......
│ └── wireless
├── hal ‑> ../../rtos‑hal/
└── osal
  • include 目录 include 目录统一管理各模块提供的数据结构定义及函数声明
lichee/rtos/include/
├── arch # 架构相关头文件
├── FreeRTOS_POSIX # POSIX头文件
├── ......
└── vsprintf.h
  • kernel 目录 kernel 目录主要包含FreeRTOS 的kernel 源码,全志实现的系统功能相关代码。
lichee/rtos/kernel/
├── FreeRTOS‑orig
│ └── Source
└── Posix
  • projects 目录 projects 目录下的每一个子目录代表一个project,实现main 入口,选择不同的project 编译出 来的bin 具有不同功能,每个project 有独立的FreeRTOSConfig 配置。例如T113,其对应于 t113_c906 子目录。
lichee/rtos/projects/
├── Kconfig
├── Makefile
└── t113_c906
├── evb
│ ├── freertos.lds.S
│ ├── ......
│ └── src
│ ├── main.c
│ ├── ......
│ └── FreeRTOSConfig.h
└── Makefile
  • tools 目录
    • risc‑v 架构这个目录主要包含一些预编译好的交叉编译工具链,目前risc‑v 基于GCC 8.4.0 的交叉编译器。
xxx@xxx:lichee/rtos/tools$ ./riscv64‑elf‑x86_64‑20201104/bin/riscv64‑unknown‑elf‑gcc ‑‑version
riscv64‑unknown‑elf‑gcc (T‑HEAD RISCV Tools V1.10.2 B20201104) 8.4.0
Copyright (C) 2018 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

lichee/rtos‑hal 目录

lichee/rtos‑hal 目录为BSP 驱动目录,用于存放各种驱动代码。

lichee/rtos/drivers 目录下的rtos‑hal 子目录软链接到该目录,下面对该目录进行介绍。

lichee/rtos‑hal
├── hal # BSP驱动代码
├── include # 驱动相关头文件
└── tools

lichee/rtos‑hal 目录主要包括hal(BSP 驱动代码)、include(驱动相关头文件)等目录,下面分别对其进行介绍。

  • hal 目录 hal 目录主要包含各外设驱动代码以及驱动测试代码,source 子目录为驱动代码,test 子目录为驱动测试代码。
lichee/rtos‑hal/hal
├── Makefile
├── source
│ ├── ccmu
│ ├── gpio
│ ├── ......
│ ├── uart
│ └── watchdog
└── test
├── ccmu
├── gpio
├── ......
├── uart
└── watchdog
  • include 目录 include 目录主要包含驱动相关头文件以及系统相关接口头文件。
lichee/rtos‑hal/include
├── hal
│ ├── aw‑alsa‑lib
│ ├── aw_common.h
│ ├── ......
│ ├── sunxi_hal_usb.h
│ ├── sunxi_hal_watchdog.h
│ └── video
└── osal
├── hal_atomic.h
├── hal_cache.h
├── ......
├── hal_waitqueue.h
└── hal_workqueue.h

RTOS开发说明

RTOS 环境已集成到Tina Linux 开发环境,通过全志代码服务器对外发布。Tina Linux 开发环境下的rtos 子目录即为RTOS 环境。

Tina 集成了RTOS 的编译、打包功能,而且适用于openwrt、buildroot 等文件系统。这里以T113‑S4 作为例子进行阐述。

关联RTOS方案

在BoardConfig.mk 文件配置RTOS 方案名,关键字段为LICHEE_RTOS_PROJECT_NAME:=xxxxx。目前使用t113_s4_c906_evb1_auto的RTOS 方案。

开发者可以通过修改LICHEE_RTOS_PROJECT_NAME 字段,更改构建RTOS 方案。进入Tina 的rtos 目录(目录结构如下),可以查看到支持RTOS 方案。

image-20240910183758461

├── board
├── envsetup.sh ‑> tools/scripts/source_envsetup.sh
├── lichee
├── out
└── tools

执行以下命令 source envsetup.sh # 配置环境变量 lunch_rtos # 选择编译方案 我们看到可以选择RTOS 方案,即是LICHEE_RTOS_PROJECT_NAME 字段可以配置的RTOS 方案。

1. t113_i_c906_evb1_auto
2. t113_s3p_c906_evb1_auto
3. t113_s3p_c906_evb1_auto_fastboot
4. t113_s3p_c906_evb1_auto_fastboot_video
5. t113_s3p_c906_evb1_auto_non_os
6. t113_s3p_c906_example_demo
7. t113_s4_c906_evb1_auto
8. t113_s4_c906_evb1_auto_fastboot_video
9. t113_s4p_c906_evb1_auto

image-20240910184123073

编译RTOS方案

重新打开一个终端,避免环境变量冲突,在Tina SDK 根目录中,先选择好整体平台方案。

buildroot 方案

1、首先使用如下命令选择整体平台方案 ./build.sh config 2、然后RTOS 相关操作命令如下 ./build.sh rtos #单独编译RTOS方案 ./build.sh rtos menuconfig #修改RTOS配置文件 ./build.sh rtos clean #清除RTOS编译中间文件

说明: 执行./build.sh 不会编译RTOS

openwrt 方案

1、首先使用如下命令选择整体平台方案 source build/envsetup.sh #生效环境变量 lunch #选择openwrt方案 2、执行make 会先编译RTOS,再编译Tina

编译快捷命令

在Tina 根目录下,执行了source build/envsetup.sh 后,可以使用RTOS 快捷命令

命令命令有效目录作用
mrtosTina 环境下任意目录单独编译RTOS 方案
mrtos menuconfigTina 环境下任意目录修改RTOS 配置文件
mrtos cleanTina 环境下任意目录清除RTOS 编译中间文件
crtosTina 环境下任意目录进入RTOS 源码目录
crtos‑halTina 环境下任意目录进入RTOS‑HAL 源码目录

然后将Tina 环境重新编译和打包,确保RTOS 编译固件amp_rv0.bin 打包到根文件系统中。

说明 buildroot 需要执行./build.sh && ./build.sh pack 命令 说明 openwrt 需要执行make && pack 命令

打包完成后,生产的固件位于tina5.0/out/t113_s4_linux_evb1_auto_uart0.img,可以使用PhoenixSuit 工具烧录到开发板上。

RTOS 固件打包

编译完成后, 生成的镜像文件会自动拷贝到对应平台方案中, 并且rt_system.elf 重名为 amp_rv0.bin 。

image-20240910184339126

DshanPi-A1介绍

· 阅读需 2 分钟

DshanPi-A1 聚焦 AI 教育场景,以“单板计算机 + 专题项目(付费)”的一体化形态,为从入门到高阶的教学需求提供可持续演进的软硬件生态。 提供中高端性能 SBC 体验,包括 PCIe、USB3.0、双千兆、HDMI、HDMI-IN、DP等丰富的接口。

DshanPI-A1测评一构建buildroot系统,调试IMX415摄像头和问题解决

· 阅读需 7 分钟

板子介绍

DshanPi-A1是深圳百问网(韦东山团队)开发的一款高性能AI嵌入式开发板,基于瑞芯微RK3576芯片设计,专为AI教育、边缘计算和智能设备开发打造。

img

核心参数

参数项规格
主控芯片瑞芯微RK3576,8nm工艺,八核64位4×Cortex-A72(2.2GHz)+4×Cortex-A53(1.8GHz)瑞芯微电子股份有限公司
AI算力内置独立NPU,6TOPS计算能力,支持INT4/INT8/INT16混合运算
内存/存储板载LPDDR4/4X内存支持eMMC、UFS存储扩展SD卡插槽
显示接口HDMIv2.1/eDPv1.3组合接口MIPIDSI(4通道)瑞芯微电子股份有限公司
视频解码支持8K@30fps、4K@120fps高清视频
网络连接支持WiFi6/BLE5.2(需外接模块)千兆网口(部分版本)
USB接口多个USB3.0/2.0(Type-C和A型)瑞芯微电子股份有限公司
其他接口UART、I2C、SPI、GPIO、音频接口、CAN总线(部分版本)
电源支持30WPD快充(Type-C接口)
尺寸紧凑型设计(具体尺寸未公开)

产品特点

1️⃣ 强大的AI处理能力

· 6TOPS独立NPU可流畅运行DeepSeek、Qwen等轻量级大语言模型

· 支持主流AI框架(TensorFlow、PyTorch、MXNet等)

· 适用于图像识别、语音处理、智能监控等AI应用开发

2️⃣ 丰富的多媒体性能

· 支持8K视频解码,可作为高清媒体中心

· HDMIIN功能可将开发板作为电脑副屏使用

· 内置音频编解码器,支持3.5mm音频输出

3️⃣ 完善的开发支持

· 搭载DShanOS系统(百问网自研Linux发行版)

· 配套免费教学课程和文档,降低学习门槛

· 支持Armbian系统,提供官方镜像下载

· 提供完整SDK和示例代码,便于二次开发

4️⃣ 灵活的扩展能力

· 引出全部GPIO接口,方便连接各类传感器和执行器

· 支持多种通信协议,适合IoT应用开发

· 可外接摄像头、显示屏、WiFi/4G模块等扩展功能

应用场景

· AI教育与学习:适合嵌入式AI课程教学和实验

· 边缘计算设备:可部署轻量级AI模型,实现本地智能决策

· 智能家居中控:构建高性能、低功耗的智能家居控制中心

· 工业自动化:适用于智能设备监控和数据采集

· 商业显示:支持8K显示,可用于广告机和信息发布系统

· AI视觉系统:可用于人脸识别、物体检测等场景

配套资源

· 官方文档站点:https://wiki.dshanpi.org/docs/dshanpi-a1/

· 开发社区:韦东山嵌入式开发者社区(100ask.org)

· QQ技术交流群:798273638

· 免费教程:提供从基础到高级的AI开发课程

buildroot SDK安装

1. 官方虚拟机获取

https://pan.baidu.com/s/15M8zuHOwl_SITl6cSk_7Vg?pwd=eaax提取码:eaax

2. 安装打开vmware,运行虚拟机

进去ubuntu系统,账号密码为ubuntu

img

3. 编译SDK

打开虚拟机,执行以下命令,进入SDK根目录:

cd ~/100ask-rk3576_SDK/

img

选择好配置文件:

./build.sh lunch

img

4. 编译

运行下列命令

./build.sh

img

等待编译结束

img

设备树修改打开摄像头节点

进入

/home/ubuntu/100ask-rk3576_SDK/kernel/arch/arm64/boot/dts/rockchip

修改图片中的文件

img

img

改为if1

回到sdk目录使用

./build.sh kernel

img

img

最后再

./build.sh updateimg

img

img

最后上传烧录(记得进入MASKROM)

img

成果展示

img

但是依然有bug但是屏幕摄像头色彩还是显示不对。

问题解决

最开始我是怀疑是摄像头的.xml(.json)没有加载出来,通过调试了好久仍没有结果但是没有结果

经过了多方面的调试最终我找到了解决方案:

第一步:从media-ctl输出发现关键线索

media-ctl -p -d /dev/media0输出中,我注意到了这个关键信息:

entity 63: m00_b_rk628-csi9-0051 (1 pad, 1 link)
type V4L2 subdev subtype Sensor flags 0
device node name /dev/v4l-subdev2
pad 0: Source
[fmt:UYVY8_2X8/64x64@10000/600000 field:none]
-> "rockchip-csi2-dphy0":0 [ENABLED] ←注意这里的[ENABLED]

img

分析点

· 系统中存在一个rk628-csi传感器实体

· 它的链路状态是[ENABLED],意味着它正在占用CSI硬件资源

· 但分辨率只有64x64,这显然不是正常的摄像头输出

第二步:从dmesg日志发现IMX415驱动正常

dmesg | grep -i imx415输出中:

[3.459474] imx415 3-0037: Detected imx415 id 0000e0
[3.515523] imx415 3-0037: Consider updating driver imx415 to match on endpoints
[3.515557] rockchip-csi2-dphy csi2-dphy3: dphy3 matches m01_f_imx415 3-0037: bustype 5

分析点

· IMX415传感器被正确检测到(ID:0000e0)

· 驱动加载成功,甚至建立了与CSI-DPHY的匹配关系

· 但没有出现V4L2子设备创建成功的消息

第三步:从设备树结构推断硬件连接

从设备树片段:

&csi2_dphy3 {
port@0 {
mipi_in_ucam3: endpoint@1 {
remote-endpoint = <&imx415_out0>; ←IMX415连接到这里
data-lanes = <1 2 3 4>;
};
};
port@1 {
csidphy3_out: endpoint@0 {
remote-endpoint = <&mipi3_csi2_input>;
};
};
};

分析点

· IMX415通过CSI-DPHY3连接到系统

· 但实际的media-ctl输出显示的是rockchip-csi2-dphy0被占用

· 这表明可能存在多个CSI接口的资源分配问题

第四步:从V4L2子设备缺失推断注册失败

运行检查命令时:

ls /sys/bus/i2c/devices/3-0037/v4l-subdev*/media_device/
# 输出:No such file or directory

img

分析点

· I2C设备3-0037存在且驱动绑定正常

· 但没有创建对应的V4L2子设备

· 这通常意味着驱动探测成功,但后续的V4L2注册失败

第五步:连接所有线索形成完整画面

把以上线索串联起来:

1. 现象:IMX415驱动加载但无V4L2设备

2. 证据1:rk628-csi占用着CSI链路且状态为[ENABLED]

3. 证据2:IMX415 I2C通信正常但媒体设备缺失

4. 证据3:设备树显示两者可能共享CSI硬件资源

逻辑推理

· 如果IMX415硬件故障→I2C探测应该失败

· 如果IMX415驱动问题→dmesg应该有错误日志

· 如果设备树配置错误→CSI匹配不会成功

· 唯一合理的解释:硬件资源被其他设备占用

第六步:开始实践

img

重新编译设备树烧录开发板完美运行!

img

img

总结

  1. 嵌入式设备中硬件资源冲突是常见隐性问题,尤其多设备共享 CSI、I2C 等链路时,需重点排查设备占用状态。

  2. 开发过程中应充分利用media-ctl、dmesg等工具,结合设备树配置分析,快速定位资源分配问题,避免盲目调试。

  3. 对于驱动加载正常但功能异常的场景,优先排查硬件资源占用、V4L2 子设备注册等关键环节,缩小问题范围。

  4. DshanPi-A1 开发板的 CSI 接口资源分配需通过设备树精准配置,修改后需严格遵循 Buildroot 编译流程重新生成镜像,确保配置生效。

DshanPI-A1第三篇opencv调试与cpu直接推理识别手势

· 阅读需 10 分钟

前面我们已经调试好了摄像头和屏幕,终于可以开始我们的手势识别啦!

这次我会在RK3576 Buildroot系统上实现一个基于OpenCV的实时手势识别系统。系统能够识别五种手势(拳头/一指、二指、三指、四指、五指),并在屏幕上实时显示处理结果。

由于嵌入式系统的特殊性,我们将重点讲解如何在无X11、无OpenGL的Wayland环境下实现图像显示。

手势识别算法

原理

因为我们张开的手指之间会形成凹陷,通过计算凹陷点的角度和深度,可以准确识别手指数量。

1. 肤色检测

使用HSV色彩空间提取肤色区域:

def detect_hand(self, frame):
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
mask = cv2.inRange(hsv, [0, 30, 60], [25, 255, 255]) # 肤色范围

# 形态学处理去噪
kernel = np.ones((7, 7), np.uint8)
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=3)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations=2)

# 查找最大轮廓
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if contours:
c = max(contours, key=cv2.contourArea)
if cv2.contourArea(c) > 3000: # 面积阈值过滤噪声
return c, mask
return None, mask

2. 手指计数(凸包缺陷法)

通过检测手掌轮廓的凹陷点来识别手指:

def recognize(self, contour):
hull = cv2.convexHull(contour, returnPoints=False)
defects = cv2.convexityDefects(contour, hull)

finger_count = 0
for i in range(defects.shape[0]):
s, e, f, d = defects[i, 0]
start = tuple(contour[s][0]) # 凸点1
end = tuple(contour[e][0]) # 凸点2
far = tuple(contour[f][0]) # 凹点(指间)

# 计算角度判断是否为有效指尖
a = np.linalg.norm(np.array(start) - np.array(end))
b = np.linalg.norm(np.array(start) - np.array(far))
c = np.linalg.norm(np.array(end) - np.array(far))
angle = np.arccos((b**2 + c**2 - a**2) / (2 * b * c))
if angle <= np.pi / 2.2 and d > 8000: # 角度和深度阈值
finger_count += 1

gestures = ["Fist/One", "Two", "Three", "Four", "Five"]
return gestures[finger_count]

如何显示OpenCV处理的图像

有了前面的理论知识还是不够的,我们还需要实践才可以啊!如何显示OpenCV处理的图像这是本教程的重点。通常我们用cv2.imshow()显示图像,但在无X11/OpenGL的嵌入式系统上,这个方法不可用。我们需要使用GStreamer+Wayland方案(这个也是我们上一篇文章的方案)。

方案探索过程

方案A: stdin管道传输

最直观的想法是通过stdin管道传输图像数据:

proc = subprocess.Popen(['gst-launch-1.0', 'fdsrc', '!', ...], stdin=subprocess.PIPE)
proc.stdin.write(frame_data)

结果:频繁出现Broken pipe错误,数据传输不稳定,这个我怀疑是管道超时自动关闭了又或者是我的格式不对。所以我直接尝试用fifo来传输原始的数据

方案B: 命名管道(FIFO)传输原始数据

尝试用FIFO传输原始RGB数据:

mkfifo /tmp/video_fifo

img

很遗憾啊,这个太容易动不动就内核奔溃了。到这个时候我面临几个问题:如果我单纯的用命令行看识别结果,我就不知道我的摄像头能不能正常运行;但是如果OpenCV和GStreamer同时读取摄像头是不可以的(摄像头只能被一个进程读取)。后面又觉得GStreamer的显示不需要实时读取摄像头的画面,我只要显示OpenCV处理过的就好了。说干就干,于是我尝试直接在OpenCV里面把处理后的图像通过GStreamer传到屏幕上面,但技术不达标无果,管道不是报错就是关闭。停下来慢慢思考,最后有了方案C(搞到这步已经花费三天了)。

方案C: 多文件序列

改变思路,将处理后的图像保存为JPEG文件序列:

# Python端
cv2.imwrite(f'/dev/shm/gesture_frames/frame_{frame_index:03d}.jpg', processed)
# GStreamer端
gst-launch-1.0 multifilesrc location=frame_%03d.jpg loop=true ! jpegdec ! ...

结果:屏幕终于可以有变动了,开心坏了!但是效果很差,重影严重,而且因为是循环读出文件夹的图片导致一直在循环播放,影响美观和体验度。于是我打算利用FIFO,基于前面的思路升级为现在的方案:

OpenCV处理完一帧 → 立即编码JPEG → 直接写入FIFO → GStreamer立即解码显示

方案D: FIFO+JPEG流(最终方案!)

# Python端: 直接往FIFO写JPEG数据
fifo = open('/tmp/gesture_fifo', 'wb')
_, jpeg = cv2.imencode('.jpg', processed, [cv2.IMWRITE_JPEG_QUALITY, 85])
fifo.write(jpeg.tobytes())
fifo.flush()
# GStreamer端: 用jpegparse自动分割JPEG帧
gst-launch-1.0 filesrc location=/tmp/gesture_fifo ! jpegparse ! jpegdec ! ...

补充:为什么JPEG流可行?

  1. JPEG格式自带开始(0xFFD8)和结束(0xFFD9)标记
  2. GStreamer的jpegparse插件能自动识别边界,分割独立的JPEG帧
  3. 避免了原始数据流的粘包问题

结果:总算是可以流畅地观察到画面了(流程不卡顿,甚至比直接点屏幕上的摄像头图标看摄像头画面都流畅)。演示视频和代码我会放在附件里面。

img img

番外篇—摄像头第二次启动色彩偏绿偏暗

问题描述

我在RK3576 Buildroot系统上使用IMX415摄像头,通过GStreamer+Wayland显示画面。遇到了一个诡异的问题:第一次启动摄像头色彩正常,但第二次启动后画面就变得偏暗偏绿

img

初步分析:对比启动日志

我首先对比了两次启动的kernel日志,发现了关键差异:

第一次启动(色彩正常)

[20.528641] rkisp_hw 27c00000.isp: set isp clk = 594000000Hz
[20.529097] rkcif-mipi-lvds 3: stream[0] start streaming
[20.529317] rockchip-csi2-dphy 3: dphy3, data_rate_mbps 892
[20.529356] imx415 3-0037: s_stream: 1.3864x2192, hdr: 0, bpp: 10

第二次启动(色彩异常)

[79.209321] rkisp_hw 27c00000.isp: set isp clk = 594000000Hz
[79.209967] rkisp rkisp-vir3: first params buf queue
[79.210051] rkisp rkisp-vir3: id: 0 no first iq setting cfg_upd: c000dfecc7fe473b en_upd: 0 en s: 5ffcc7fe473b
[79.210351] rkcif-mipi-lvds 3: stream[0] start streaming

关键发现:第二次启动多了一条警告 no first iq setting。这说明ISP的图像质量参数没有正确加载,导致使用了错误的默认参数,造成色彩偏暗偏绿。

问题解决过程

第一阶段:尝试硬件层面解决

一开始我以为是ISP驱动状态没有正确复位,尝试了几种方法:

  1. 尝试unbind/bind ISP驱动

    echo "27c00000.isp" > /sys/bus/platform/drivers/rkisp_hw/unbind
    echo "27c00000.isp" > /sys/bus/platform/drivers/rkisp_hw/bind

    结果:摄像头直接打不开了,操作太激进导致驱动状态完全错乱。

  2. 尝试使用v4l2-ctl重置、media-ctl reset等方法,都没有解决问题。

第二阶段:深入诊断系统配置

我开始系统性地诊断整个摄像头子系统:

# 查找IQ参数文件
find / -name "*imx415*.xml" -o -name "*imx415*.json" 2>/dev/null
# 结果: 找到了 /etc/iqfiles/imx415_CMK-OT2022-PX1_IR0147-50IRC-8M-F20.json

# 检查3A服务器
ps aux | grep rkaiq_3A_server
# 结果: 服务器正在运行

# 查看设备拓扑
v4l2-ctl --list-devices
# 确认 /dev/video-camera0 -> video11

关键发现

  • IQ参数文件存在
  • 3A服务器(rkaiq_3A_server)正在运行
  • 但为什么IQ参数没有加载?

第三阶段:抓取3A服务器日志

我决定前台运行3A服务器,查看详细输出:

killall rkaiq_3A_server
/usr/bin/rkaiq_3A_server 2>&1 &

启动日志显示:

DBG: get rkisp-isp-subdev devname: /dev/v4l-subdev3
DBG: get rkisp-input-params devname: /dev/video18
DBG: get rkisp-statistics devname: /dev/video17
XCORE: K: cid[1] rk_aiq_uapi2_sysctl_init success. iq: /etc/iqfiles//imx415_CMK-OT2022-PX1_IR0147-50IRC-8M-F20.json
XCORE: K: cid[1] rk_aiq_uapi2_sysctl_prepare success. mode: 0
DBG: /dev/media1: wait stream start event..

重大发现:3A服务器实际上工作正常!IQ文件已经成功加载了!

这时我进行了第二次摄像头启动测试,观察到:

[625.216117] rkisp-vir3: waiting on params stream one event timeout

真相大白:第二次启动时,3A服务器超时无响应!

第四阶段:找到根本原因

通过多次测试和日志分析,我终于理解了问题的本质:

第一次启动流程(正常)

  1. 系统启动时,3A服务器自动启动
  2. 3A服务器加载IQ参数文件到内存
  3. 3A服务器预先准备好IQ参数缓冲区
  4. GStreamer启动摄像头
  5. ISP请求IQ参数
  6. 3A服务器立即响应并推送IQ参数
  7. 色彩正常

第二次启动流程(异常)

  1. 停止第一次的GStreamer进程
  2. 3A服务器还在运行,但进入了某种等待状态
  3. IQ参数缓冲区已经被消费
  4. 立即重启GStreamer
  5. ISP请求IQ参数
  6. 3A服务器来不及响应或状态异常
  7. ISP使用默认参数处理第一帧
  8. 出现no first iq setting警告
  9. 色彩偏暗偏绿

解决方案

问题的根源是:3A服务器在摄像头第一次运行后进入异常状态,无法正确响应第二次启动的IQ参数请求

最终的解决方法很简单:每次启动摄像头前,重启3A服务器

我编写了一个封装脚本:

#!/bin/sh

echo "=== Starting Camera with 3A Server Reset ==="

# 1. 停止所有摄像头进程
pkill -9 gst-launch 2>/dev/null

# 2. 重启3A服务器
killall rkaiq_3A_server 2>/dev/null
sleep 2
rm -f /tmp/.rkaiq_3A*

# 3. 启动3A服务器
/etc/init.d/S40rkaiq_3A start
echo "Waiting for 3A server to initialize..."
sleep 5

# 4. 确认3A服务器运行正常
if ! pgrep rkaiq_3A_server > /dev/null; then
echo "ERROR: 3A server failed to start!"
exit 1
fi

echo "3A server ready, starting camera..."

# 5. 启动摄像头
gst-launch-1.0 v4l2src device=/dev/video11 ! \
video/x-raw,format=NV12,width=640,height=480,framerate=30/1 ! \
waylandsink

echo "Camera stopped"
exit 0

img

验证结果

img

使用新脚本后,连续多次启动摄像头,色彩始终正常,日志中不再出现no first iq setting或超时错误。

经验总结

  1. 对比日志是发现问题的关键:通过对比正常和异常情况的日志,快速定位到no first iq setting这个关键线索

  2. 系统性诊断:不要盲目尝试,先检查各个组件(IQ文件、3A服务器、设备节点)的状态

  3. 前台运行看详细日志:很多后台服务的问题需要前台运行才能看到详细输出

  4. 理解组件间的协作关系:RK平台的摄像头涉及ISP驱动、3A服务器、IQ参数文件三者的协作,任何一个环节出问题都会导致异常

  5. 状态管理很重要:嵌入式系统的服务重启问题往往是状态机管理不当导致的,彻底重置是最可靠的方案。

DshanPI-A1测评第二篇:手势识别编程环境搭建与屏幕调试

· 阅读需 6 分钟

本次测评我将会安装手势识别系统的必要工具和调试屏幕

硬件与环境准备

在开始之前,我们先明确手头的装备和环境:

  • 核心板:Dshanpi-A1,主控为瑞芯微RK3576芯片。

  • 屏幕:一块480x800分辨率的MIPI屏幕。

  • 系统:Buildroot Linux系统。

  • 官方的SDK

安装开发工具

下面是本次的清单:

软件包/配置类别推荐选项与作用
Python环境python3: 核心解释器。 python-pip: 用于安装未包含在Buildroot中的Python包。 python-numpy: 为OpenCV等库提供高效的数值计算支持。 python-setuptools: 一些Python包的基础构建依赖。
计算机视觉与图像处理opencv4: 务必启用 python3 支持。提供核心的计算机视觉库,用于图像处理和手势识别算法。 opencv4 贡献模块: 包含额外的、更先进的算法。
摄像头与显示支持gstreamer1 及相关插件: 构建摄像头图像采集和屏幕显示的管道。 gst1-plugins-base, gst1-plugins-good, gst1-plugins-bad, gst1-plugins-ugly: 提供丰富的编解码器和功能元件。 gst1-python: 允许在Python中创建和操作GStreamer管道。

SDK配置流程

1. 选择芯片类型

./build.sh chip

img

img

2. 进入buildroot配置

cd buildroot
make menuconfig

img

3. 选择Target packages

img

4. 安装Python环境

当找不到安装路径时,可按/键进入搜索:

img

输入python3进行搜索:

img

进入显示的Location路径进行配置:

img

img

5. 保存配置并编译

make

回到SDK主目录运行:

./build.sh rootfs
./build.sh updateimg

最后烧录运行到开发板

开发板调试

检查工具安装情况

python3 --version
pip3 --version
python3 -c "import numpy; print('NumPy version:', numpy.__version__)"
python3 -c "import cv2; print('OpenCV version:', cv2.__version__)"

img

屏幕调试

问题分析

系统已运行Weston合成器,这意味着我们有一个图形界面环境。尝试直接操作FrameBuffer(/dev/fb0)无效,因为Weston已占用显示接口。

通过系统检查发现:

  • /dev/fb0设备存在
  • 屏幕状态为connected
  • 分辨率为480x800

img

解决方案:GStreamer + Wayland

GStreamer基础测试

gst-launch-1.0 videotestsrc pattern=smpte ! video/x-raw,width=480,height=800 ! waylandsink sync=false

img

摄像头直连显示测试

gst-launch-1.0 v4l2src device=/dev/video11 ! video/x-raw,width=640,height=480 ! videoconvert ! waylandsink sync=false

img

注意:请将device参数替换为您的摄像头设备节点

测试脚本

#!/usr/bin/env python3
# fixed_display_test.py

import subprocess
import time
import os

def check_camera_devices():
"""检查可用的摄像头设备"""
print("=== 摄像头设备检查 ===")

try:
# 使用v4l2-ctl检查设备
result = subprocess.run(["v4l2-ctl", "--list-devices"],
capture_output=True, text=True)
if result.returncode == 0:
print("找到的视频设备:")n print(result.stdout)
else:
print("v4l2-ctl命令执行失败")
except Exception as e:
print(f"检查摄像头设备失败: {e}")

# 测试常见的摄像头设备
camera_devices = ["/dev/video11", "/dev/video0", "/dev/video1", "/dev/video2"]
print("\n测试摄像头设备:")

for device in camera_devices:
if os.path.exists(device):
print(f"测试设备: {device}")
try:
# 尝试使用GStreamer测试摄像头
cmd = [
"gst-launch-1.0",
"-v",
"v4l2src", f"device={device}", "!",
"video/x-raw,width=640,height=480,framerate=15/1", "!",
"videoconvert", "!",
"waylandsink", "sync=false"
]

process = subprocess.Popen(cmd)
time.sleep(3) # 显示3秒
process.terminate()
process.wait()
print(f" {device}: 摄像头工作正常")
return device

except Exception as e:
print(f"{device}: 测试失败 - {e}")
else:
print(f"{device}: 设备不存在")

return None

def test_static_patterns():
"""测试静态图案(不会变化)"""
print("\n=== 静态图案测试 ===")

# 设置Wayland环境
os.environ['WAYLAND_DISPLAY'] = 'wayland-0'

# 测试静态图案(不会变化)
static_patterns = [
("smpte100", "SMPTE 100%色彩条"),
("ball", "时钟图案"),
("blink", "闪烁图案"),
("pinwheel", "风车图案"),
("spokes", "辐条图案"),
]

for pattern, description in static_patterns:
print(f"显示: {description}")
try:
cmd = [
"gst-launch-1.0",
"videotestsrc", f"pattern={pattern}", "!",
"video/x-raw,width=480,height=800,framerate=15/1", "!",
"waylandsink", "sync=false"
]

process = subprocess.Popen(cmd)
time.sleep(3)
process.terminate()
process.wait()
print(f"{description} 显示成功")

except Exception as e:
print(f"{description} 显示失败: {e}")

def test_custom_resolution():
"""测试自定义分辨率显示"""
print("\n=== 自定义分辨率测试 ===")

resolutions = [
(480, 800, "竖屏 480x800"),
(800, 480, "横屏 800x480"),
(640, 480, "标准 640x480"),
(400, 800, "竖屏 400x800"),
]

for width, height, desc in resolutions:
print(f"测试分辨率: {desc}")
try:
cmd = [
"gst-launch-1.0",
"videotestsrc", "pattern=smpte100", "!",
f"video/x-raw,width={width},height={height},framerate=15/1", "!",
"videoconvert", "!",
"waylandsink", "sync=false"
]

process = subprocess.Popen(cmd)
time.sleep(2)
process.terminate()
process.wait()
print(f" {desc} 显示成功")

except Exception as e:
print(f" {desc} 显示失败: {e}")

if __name__ == "__main__":
print("=" * 50)

# 1. 检查摄像头
camera_device = check_camera_devices()

# 2. 测试静态图案
test_static_patterns()

# 3. 测试不同分辨率
test_custom_resolution()

print("\n" + "=" * 50)
if camera_device:
print(f"可用的摄像头设备: {camera_device}")
else:
print("未找到可用的摄像头设备")
print("所有测试完成")

img

img

运行结果

image-20251222165424798

image-20251222165428939

演示视频我会放在附件里面

番外篇:FileZilla文件传输

FileZilla连接设置

FileZilla - The free FTP solution

检查SSH服务

ss -tuln | grep 22

img

网络共享设置

img

img

img

选择能上网的网络,点击属性:

img

image-20251222165505322

连接开发板

ifconfig

img

使用FileZilla连接:

  • IP:开发板IP地址
  • 用户名:root
  • 密码:rockchip
  • 端口:22

总结

回顾整个调试过程,以下几个关键点值得特别注意:

  1. 显示路径选择:在运行Weston这类合成器的系统上,优先使用GStreamer + waylandsink的方案来显示图像,而非直接操作FrameBuffer。

  2. 摄像头设备节点:务必使用v4l2-ctl --list-devices命令确认摄像头对应的设备节点,并在代码中正确指定。

  3. 屏幕分辨率:注意系统识别到的屏幕分辨率(可通过cat /sys/class/drm/card0-DSI-1/modes查询)可能与物理分辨率略有不同,在创建显示帧时需以此为准或进行调整。

至此,我们成功地在Dshanpi A1开发板上搭建起了手势识别的编程环境,并解决了MIPI屏幕和摄像头的显示问题。虽然过程曲折,但为后续实际编写手势识别算法奠定了坚实的基础。希望我的这些经验能对大家有所帮助!

DshanPI-A1第五篇NPU实战YOLOv5实时目标检测加速

· 阅读需 17 分钟

前言

在前面的文章中,我们已经实现了基于CPU的MediaPipe手势识别,虽然能跑起来,但15-25 FPS的性能还是有点吃力,而且CPU占用率很高。这次我要榨干RK3576的硬件潜力——使用板载的NPU(神经网络处理单元)来加速深度学习推理。首先外我们需要要来了解一下一些概念

什么是NPU? NPU(Neural Processing Unit)是专门为AI运算设计的硬件加速器。和CPU/GPU不同,NPU针对神经网络的矩阵运算、卷积等操作做了深度优化。RK3576芯片内置了双核NPU,理论算力达到6 TOPS,能大幅提升模型推理速度并降低功耗。

为什么选YOLOv5? 其实我本来是打算继续优化我前面的MediaPipe的TFLite模型转换遇到了依赖地狱(这个坑我踩了好久...),所以这次先用官方提供的YOLOv5模型来验证NPU功能。YOLOv5是目前最流行的实时目标检测算法之一,能同时检测图像中的多个物体及其位置。

一、环境准备

1.1 硬件连接

  • RK3576开发板(已刷Buildroot系统)
  • IMX415摄像头(接在/dev/video11)
  • HDMI显示器
  • 串口连接(用于命令行操作)

image-20251222175427219

1.2 检查NPU硬件

首先登录板子,检查NPU是否正常工作:

# 查看NPU负载(应该显示Core0和Core1)
cat /sys/kernel/debug/rknpu/load

image-20251222175450647

这说明NPU双核都是空闲状态,可以开始干活了!

小知识: RK3576的NPU采用双核架构,可以并行处理两个模型,或者让一个大模型的不同层在两个核心上流水线执行。

1.3 检查Python环境

# 查看Python版本
python3 --version

image-20251222175518307

我的输出是 Python 3.11.8,这个版本很重要,后面安装库的时候要匹配。

二、安装RKNN运行时环境

2.1 什么是RKNN?

RKNN(Rockchip Neural Network)是瑞芯微为自家NPU开发的深度学习推理框架。整个工具链分两部分:

  • rknn-toolkit2(PC端): 用于模型转换,把TensorFlow/PyTorch/ONNX模型转成.rknn格式
  • rknn-toolkit-lite2(板端): 轻量级运行时库,用于在RK芯片上加载和推理.rknn模型

我们这次只用板端推理,所以只装lite版。

2.2 安装安装包

好消息是我们如果不想在github上下载觉得慢或者不稳定,我们可以选择我们百问网提供的下载连接: https://dl.100ask.net/Hardware/MPU/RK3576-DshanPi-A1/utils/rknn-toolkit2.zip 下载完成传到我们的Dshanpi-A1上。

cd /rknn-toolkit2/rknn-toolkit-lite2/packages/
ls -lh

image-20251222175600879

可以看到有多个Python版本的.whl安装包,我们需要的是cp311(Python 3.11)的ARM64版本。

2.3 安装rknn-toolkit-lite2

# 先强制安装主包(不检查依赖,因为依赖后面单独装)
pip3 install --no-deps rknn_toolkit_lite2-2.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

# 再用清华镜像安装缺失的依赖
pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple psutil ruamel.yaml

这样做就不用耗费我们下载一些没必要的包了。

看到 Successfully installed 就成功了!

2.4 验证安装

python3 -c "from rknnlite.api import RKNNLite; print('✅ rknn-toolkit-lite2 安装成功!')"

image-20251222175629367

输出勾勾标志就OK!

三、NPU基准测试

在跑实时检测之前,先用一个简单的图像分类模型测试一下NPU的性能。

3.1 使用ResNet18测试

进入示例目录:

cd /rknn-toolkit2/rknn-toolkit-lite2/examples/resnet18
ls -lh

image-20251222175647550

可以看到有:

  • resnet18_for_rk3576.rknn - 专门为RK3576优化的模型
  • space_shuttle_224.jpg - 测试图片
  • test.py - 推理脚本

运行测试:

python3 test.py

image-20251222175712361

我的结果:

  • 识别结果: Space Shuttle(航天飞机) - 99.96%置信度
  • 推理延迟: 11.21 ms
  • 平均FPS: 89.24

这意味着NPU可以每秒处理89张图片,比我之前CPU跑MediaPipe快3-6倍!

3.2 性能基准测试

为了更准确地测试NPU性能,我写了个循环100次的脚本(可以自行测试):

cd ~
mkdir -p npu_test
cd npu_test

# 复制模型和图片
cp /rknn-toolkit2/rknn-toolkit-lite2/examples/resnet18/resnet18_for_rk3576.rknn ./
cp /rknn-toolkit2/rknn-toolkit-lite2/examples/resnet18/space_shuttle_224.jpg ./

创建测试脚本 benchmark.py:

import cv2
import numpy as np
import time
from rknnlite.api import RKNNLite

rknn = RKNNLite()
rknn.load_rknn('resnet18_for_rk3576.rknn')
rknn.init_runtime(core_mask=RKNNLite.NPU_CORE_0)

img = cv2.imread('space_shuttle_224.jpg')
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img = np.expand_dims(img, 0)

# 预热
for _ in range(10):
rknn.inference(inputs=[img])

# 测试100次
times = []
for i in range(100):
start = time.time()
rknn.inference(inputs=[img])
times.append((time.time() - start) * 1000)

print(f'平均延迟: {np.mean(times):.2f} ms')
print(f'最小延迟: {np.min(times):.2f} ms')
print(f'最大延迟: {np.max(times):.2f} ms')
print(f'平均FPS: {1000/np.mean(times):.2f}')

rknn.release()

运行:

python3 benchmark.py

四、YOLOv5目标检测

现在进入正题——用NPU跑实时目标检测!

4.1 什么是YOLOv5?

YOLO(You Only Look Once)是一种单阶段目标检测算法,能在一次前向传播中同时预测多个物体的位置和类别。相比两阶段的R-CNN系列,YOLO速度更快,非常适合实时场景。

YOLOv5是该系列的第五代,支持检测80种常见物体(人、车、动物、家具等)。

4.2 准备模型和测试图片

cd ~
mkdir -p npu_yolo_test
cd npu_yolo_test

# 复制RK3576专用的YOLOv5模型
cp /rknn-toolkit2/rknpu2/examples/rknn_yolov5_demo/model/RK3576/yolov5s-640-640.rknn ./

# 复制测试图片(一张公交车照片)
cp /rknn-toolkit2/rknpu2/examples/rknn_yolov5_demo/model/bus.jpg ./

ls -lh

image-20251222175741596

4.3 单张图片检测测试

这里完整的后处理代码比较长(包含NMS非极大值抑制等算法),我把它整理成了一个脚本。

创建 yolo_npu_test.py(完整代码见附录A),然后运行:

python3 yolo_npu_test.py

image-20251222175811624

我的结果:

  • 检测到5个目标:
    • 3个人(person): 88.0%, 87.1%, 82.8%
    • 1辆公交车(bus): 70.1%
    • 1个部分遮挡的人: 30.7%
  • NPU推理延迟: 87.94 ms
  • FPS: 11.37

检测结果保存在 result_npu.jpg,可以传到PC上查看:

# 在PC的PowerShell中执行(替换<板子IP>为实际IP)
scp root@<板子IP>:/npu_yolo_test/result_npu.jpg .

【检测结果图片】 image-20251222175836265

为什么YOLOv5比ResNet18慢?

  • ResNet18只做分类,输出1000个类别的概率(简单)
  • YOLOv5要检测多个物体的位置+类别,输出3个不同尺度的特征图(复杂)
  • 但11 FPS对于目标检测来说已经很不错了!

五、实时摄像头检测

单张图片测试成功了,现在来个真正的实战——用IMX415摄像头做实时检测并显示在屏幕上!

5.1 显示方案:FIFO + GStreamer

还是和之前的一样由于Buildroot没有图形界面,OpenCV的imshow()不能用,我们采用命名管道(FIFO) + GStreamer的方案:

  1. Python读摄像头 → NPU推理 → 画框 → 编码成JPEG
  2. 写入FIFO管道
  3. GStreamer从管道读取 → 解码 → 显示到屏幕

这是Linux下常用的进程间通信方式,之前手势识别项目也用的这个。

5.2 创建一键启动脚本

为了方便使用,我把整个流程打包成了一个Shell脚本 yolo_npu_display.sh:

cd /npu_yolo_test

cat > yolo_npu_display.sh << 'EOF'
#!/bin/bash

echo "=========================================="
echo "YOLOv5 NPU Real-time Detection - RK3576"
echo "=========================================="
echo ""

# 重启3A服务器(摄像头自动曝光/白平衡/自动对焦)
echo "Restarting 3A server..."
killall rkaiq_3A_server 2>/dev/null
sleep 2
rm -f /tmp/.rkaiq_3A* 2>/dev/null
/etc/init.d/S40rkaiq_3A start >/dev/null 2>&1
sleep 3

# 创建FIFO管道
FIFO_PATH="/tmp/yolo_fifo"
rm -f $FIFO_PATH
mkfifo $FIFO_PATH

echo "Starting display pipeline..."
gst-launch-1.0 -q filesrc location=$FIFO_PATH ! jpegparse ! jpegdec ! videoconvert ! videoscale ! video/x-raw,width=1280,height=720 ! waylandsink fullscreen=true sync=false &
GST_PID=$!

sleep 2

echo "Starting YOLOv5 NPU detection..."
python3 - <<'PYTHON_CODE' &
import cv2
import numpy as np
import time
from rknnlite.api import RKNNLite
from collections import deque

RKNN_MODEL = '/npu_yolo_test/yolov5s-640-640.rknn'
CAMERA_ID = 11
IMG_SIZE = 640
OBJ_THRESH = 0.25
NMS_THRESH = 0.45
FIFO_PATH = '/tmp/yolo_fifo'

CLASSES = ("person", "bicycle", "car", "motorbike", "aeroplane", "bus", "train", "truck", "boat", "traffic light",
"fire hydrant", "stop sign", "parking meter", "bench", "bird", "cat", "dog", "horse", "sheep", "cow",
"elephant", "bear", "zebra", "giraffe", "backpack", "umbrella", "handbag", "tie", "suitcase", "frisbee",
"skis", "snowboard", "sports ball", "kite", "baseball bat", "baseball glove", "skateboard", "surfboard",
"tennis racket", "bottle", "wine glass", "cup", "fork", "knife", "spoon", "bowl", "banana", "apple",
"sandwich", "orange", "broccoli", "carrot", "hot dog", "pizza", "donut", "cake", "chair", "sofa",
"pottedplant", "bed", "diningtable", "toilet", "tvmonitor", "laptop", "mouse", "remote", "keyboard",
"cell phone", "microwave", "oven", "toaster", "sink", "refrigerator", "book", "clock", "vase",
"scissors", "teddy bear", "hair drier", "toothbrush")

def xywh2xyxy(x):
y = np.copy(x)
y[:, 0] = x[:, 0] - x[:, 2] / 2
y[:, 1] = x[:, 1] - x[:, 3] / 2
y[:, 2] = x[:, 0] + x[:, 2] / 2
y[:, 3] = x[:, 1] + x[:, 3] / 2
return y

def process(input, mask, anchors):
anchors = [anchors[i] for i in mask]
grid_h, grid_w = map(int, input.shape[0:2])
box_confidence = np.expand_dims(input[..., 4], axis=-1)
box_class_probs = input[..., 5:]
box_xy = input[..., :2]*2 - 0.5
col = np.tile(np.arange(0, grid_w), grid_w).reshape(-1, grid_w)
row = np.tile(np.arange(0, grid_h).reshape(-1, 1), grid_h)
col = col.reshape(grid_h, grid_w, 1, 1).repeat(3, axis=-2)
row = row.reshape(grid_h, grid_w, 1, 1).repeat(3, axis=-2)
grid = np.concatenate((col, row), axis=-1)
box_xy += grid
box_xy *= int(IMG_SIZE/grid_h)
box_wh = pow(input[..., 2:4]*2, 2)
box_wh = box_wh * anchors
box = np.concatenate((box_xy, box_wh), axis=-1)
return box, box_confidence, box_class_probs

def filter_boxes(boxes, box_confidences, box_class_probs):
boxes = boxes.reshape(-1, 4)
box_confidences = box_confidences.reshape(-1)
box_class_probs = box_class_probs.reshape(-1, box_class_probs.shape[-1])
_box_pos = np.where(box_confidences >= OBJ_THRESH)
boxes = boxes[_box_pos]
box_confidences = box_confidences[_box_pos]
box_class_probs = box_class_probs[_box_pos]
class_max_score = np.max(box_class_probs, axis=-1)
classes = np.argmax(box_class_probs, axis=-1)
_class_pos = np.where(class_max_score >= OBJ_THRESH)
boxes = boxes[_class_pos]
classes = classes[_class_pos]
scores = (class_max_score * box_confidences)[_class_pos]
return boxes, classes, scores

def nms_boxes(boxes, scores):
x, y = boxes[:, 0], boxes[:, 1]
w, h = boxes[:, 2] - boxes[:, 0], boxes[:, 3] - boxes[:, 1]
areas = w * h
order = scores.argsort()[::-1]
keep = []
while order.size > 0:
i = order[0]
keep.append(i)
xx1 = np.maximum(x[i], x[order[1:]])
yy1 = np.maximum(y[i], y[order[1:]])
xx2 = np.minimum(x[i] + w[i], x[order[1:]] + w[order[1:]])
yy2 = np.minimum(y[i] + h[i], y[order[1:]] + h[order[1:]])
w1 = np.maximum(0.0, xx2 - xx1 + 0.00001)
h1 = np.maximum(0.0, yy2 - yy1 + 0.00001)
inter = w1 * h1
ovr = inter / (areas[i] + areas[order[1:]] - inter)
inds = np.where(ovr <= NMS_THRESH)[0]
order = order[inds + 1]
return np.array(keep)

def yolov5_post_process(input_data):
masks = [[0, 1, 2], [3, 4, 5], [6, 7, 8]]
anchors = [[10, 13], [16, 30], [33, 23], [30, 61], [62, 45],
[59, 119], [116, 90], [156, 198], [373, 326]]
boxes, classes, scores = [], [], []
for input, mask in zip(input_data, masks):
b, c, s = process(input, mask, anchors)
b, c, s = filter_boxes(b, c, s)
boxes.append(b)
classes.append(c)
scores.append(s)
if len(boxes) == 0:
return None, None, None
boxes = np.concatenate(boxes)
boxes = xywh2xyxy(boxes)
classes = np.concatenate(classes)
scores = np.concatenate(scores)
nboxes, nclasses, nscores = [], [], []
for c in set(classes):
inds = np.where(classes == c)
b, c, s = boxes[inds], classes[inds], scores[inds]
keep = nms_boxes(b, s)
nboxes.append(b[keep])
nclasses.append(c[keep])
nscores.append(s[keep])
if not nclasses:
return None, None, None
return np.concatenate(nboxes), np.concatenate(nclasses), np.concatenate(nscores)

class YOLODetector:
def __init__(self):
self.fps_queue = deque(maxlen=30)
self.last_time = time.time()
self.fps = 0.0

def calc_fps(self):
t = time.time()
if t - self.last_time > 0:
self.fps_queue.append(1.0 / (t - self.last_time))
self.fps = sum(self.fps_queue) / len(self.fps_queue)
self.last_time = t

def draw_detections(self, frame, boxes, scores, classes, scale_x, scale_y):
for box, score, cl in zip(boxes, scores, classes):
x1 = int(box[0] * scale_x)
y1 = int(box[1] * scale_y)
x2 = int(box[2] * scale_x)
y2 = int(box[3] * scale_y)
cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2)
label = f'{CLASSES[cl]} {score:.2f}'
cv2.putText(frame, label, (x1, y1 - 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)

def run(self):
print("Initializing NPU...")
rknn_lite = RKNNLite()
rknn_lite.load_rknn(RKNN_MODEL)
rknn_lite.init_runtime(core_mask=RKNNLite.NPU_CORE_0)
print("NPU ready!")

print(f"Opening camera /dev/video{CAMERA_ID}...")
cap = cv2.VideoCapture(CAMERA_ID, cv2.CAP_V4L2)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
cap.set(cv2.CAP_PROP_FPS, 30)

width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
print(f"Camera: {width}x{height}")
print("Detection running...\n")

fifo = open(FIFO_PATH, 'wb')
frame_count = 0
scale_x = width / IMG_SIZE
scale_y = height / IMG_SIZE

try:
while True:
ret, frame = cap.read()
if not ret:
time.sleep(0.1)
continue

frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
img_resized = cv2.resize(frame_rgb, (IMG_SIZE, IMG_SIZE))
img_input = np.expand_dims(img_resized, 0)

inf_start = time.time()
outputs = rknn_lite.inference(inputs=[img_input])
inf_time = (time.time() - inf_start) * 1000

input0 = outputs[0].reshape([3, -1] + list(outputs[0].shape[-2:]))
input1 = outputs[1].reshape([3, -1] + list(outputs[1].shape[-2:]))
input2 = outputs[2].reshape([3, -1] + list(outputs[2].shape[-2:]))
input_data = [
np.transpose(input0, (2, 3, 0, 1)),
np.transpose(input1, (2, 3, 0, 1)),
np.transpose(input2, (2, 3, 0, 1))
]

boxes, classes, scores = yolov5_post_process(input_data)

if boxes is not None:
self.draw_detections(frame, boxes, scores, classes, scale_x, scale_y)
obj_count = len(boxes)
else:
obj_count = 0

self.calc_fps()
cv2.rectangle(frame, (5, 5), (400, 120), (0, 100, 0), -1)
cv2.putText(frame, f'FPS: {self.fps:.1f}', (15, 35),
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2)
cv2.putText(frame, f'NPU: {inf_time:.1f}ms', (15, 70),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
cv2.putText(frame, f'Objects: {obj_count}', (15, 105),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 0), 2)

_, jpeg = cv2.imencode('.jpg', frame, [cv2.IMWRITE_JPEG_QUALITY, 85])
fifo.write(jpeg.tobytes())
fifo.flush()

frame_count += 1
if frame_count % 50 == 0:
print(f"Frame {frame_count}: FPS={self.fps:.1f}, NPU={inf_time:.1f}ms, Objects={obj_count}")

except KeyboardInterrupt:
print("\nStopping...")
finally:
fifo.close()
cap.release()
rknn_lite.release()
print("Released resources")

YOLODetector().run()
PYTHON_CODE

PYTHON_PID=$!

echo ""
echo "=========================================="
echo "System started!"
echo "Screen should show real-time detection"
echo "Press Ctrl+C to exit"
echo "=========================================="
echo ""

trap "echo ''; echo 'Stopping...'; kill $PYTHON_PID $GST_PID 2>/dev/null; rm -f $FIFO_PATH; echo 'Cleaned up'; exit" INT

wait $PYTHON_PID

kill $GST_PID 2>/dev/null
rm -f $FIFO_PATH
echo "Cleaned"
EOF

chmod +x yolo_npu_display.sh

【截图13:脚本创建完成】

5.3 运行实时检测

./yolo_npu_display.sh

image-20251222175903207

会看到:

  1. 重启3A服务器
  2. 创建FIFO管道
  3. 启动GStreamer显示管道
  4. 启动YOLOv5检测

image-20251222175922272

终端会每50帧输出一次性能统计,例如:

Frame 50: FPS=10.2, NPU=52.3ms, Objects=2
Frame 100: FPS=10.5, NPU=48.7ms, Objects=1

image-20251222175948756

  • 绿色检测框标注物体
  • 左上角显示FPS、NPU延迟、检测数量

Ctrl+C 停止程序。

六、性能分析

6.1 实测数据

我的实时检测结果:

指标数值
平均FPS10.0-10.8
NPU推理延迟47-59 ms
总延迟(含采集/绘制/显示)84-101 ms
最多检测目标数13个物体

image-20251222180048556

6.2 和CPU方案对比

方案FPSCPU占用功耗
MediaPipe(CPU)15-2550-65%
YOLOv5(NPU)10-1115-25%

虽然YOLOv5的FPS略低于MediaPipe手势识别,但要注意:

  • YOLOv5是全场景目标检测(80类物体),MediaPipe只做手部检测(任务简单得多)
  • YOLOv5用的是NPU,CPU占用率降低了60%以上
  • NPU功耗远低于CPU全速运行,发热明显减少
  • 如果只用YOLOv5检测人体(person类),可以进一步优化后处理,FPS还能提升

6.3 为什么没达到理论89 FPS?

ResNet18单张图片能跑89 FPS,为什么实时检测只有10 FPS?瓶颈在哪?

经过profiling分析:

  • NPU推理: ~50ms (主要瓶颈)
  • 摄像头采集: ~5ms
  • 后处理(NMS等): ~15ms
  • 绘制框和文字: ~8ms
  • JPEG编码: ~10ms
  • FIFO传输+GStreamer: ~5ms

总结:

  1. YOLOv5模型比ResNet18大得多(7.9MB vs 12MB),计算量也更大
  2. 后处理的NMS算法是纯Python实现,比较慢(可以改用C++或CUDA加速)
  3. JPEG编码也占了不少时间(可以改用H.264硬件编码)

优化方向:

  • 使用YOLOv5-nano(更小的模型)
  • 后处理用Cython加速
  • 启用NPU双核并行
  • 用RK3576的硬件视频编码器

七、遇到的坑和解决方法

7.1 PC端模型转换依赖地狱

问题: 想在PC上用rknn-toolkit2把MediaPipe的TFLite模型转成.rknn格式,结果遇到protobuf版本冲突——TensorFlow要求<3.20,但rknn-toolkit2要求>=4.25,完全不兼容。

尝试的解决方法:

  • 换TensorFlow版本 → 失败
  • 用虚拟环境 → 用户拒绝(我太懒了...)
  • 清华镜像加速 → 依然冲突

最终方案: 放弃PC端转换,直接用官方提供的.rknn模型测试NPU功能。以后有需要再用Docker跑转换工具。

教训: Python依赖管理真是个大坑,尤其是深度学习框架。强烈建议使用Docker或conda环境隔离。

7.2 摄像头打不开

问题: 直接用cv2.VideoCapture(11)打开失败。

原因: 没有重启rkaiq_3A服务器(负责摄像头的自动曝光/白平衡)。

解决: 在脚本开头加上:

killall rkaiq_3A_server 2>/dev/null
sleep 2
rm -f /tmp/.rkaiq_3A* 2>/dev/null
/etc/init.d/S40rkaiq_3A start >/dev/null 2>&1
sleep 3

7.3 GStreamer找不到videoparse

问题: 一开始想用videoparse插件,结果提示没这个插件。

原因: Buildroot精简系统,很多GStreamer插件没装。

解决: 改用JPEG流传输:

  • Python编码成JPEG → FIFO → GStreamer的jpegparse解码
  • 这个插件是默认安装的

7.4 Python脚本中文编码错误

问题: 脚本里有中文注释,运行报错:

SyntaxError: Non-UTF-8 code starting with '\xe5'

解决: 把所有中文注释改成英文,或者在文件开头加:

# -*- coding: utf-8 -*-

八、总结与展望

8.1 本次实战收获

  1. 成功验证了RK3576的NPU硬件加速能力

    • ResNet18: 89 FPS(11ms延迟)
    • YOLOv5: 10 FPS(50ms NPU延迟)
    • CPU占用降低60%,功耗显著下降
  2. 掌握了RKNN工具链的使用

    • rknn-toolkit-lite2的安装和API
    • .rknn模型的加载和推理
    • NPU核心的指定和配置
  3. 建立了完整的实时检测pipeline

    • 摄像头采集 → NPU推理 → 后处理 → 显示
    • FIFO + GStreamer的显示方案
    • 性能监控和FPS计算
  4. 踩过了各种坑

    • 依赖冲突、摄像头初始化、显示管道等
    • 积累了宝贵的debug经验

8.2 感想

RK3576的NPU确实很强大,6 TOPS的算力在边缘设备中算是顶级配置了。虽然用起来有一些坑(主要是依赖管理),但整体体验还是不错的。

最大的感触是:AI落地不容易啊! 从模型训练到部署,要考虑的东西太多了——精度、速度、功耗、成本...每个环节都需要权衡取舍。但看到实时检测画面流畅运行的那一刻,所有的付出都值了!

九、参考资料

  1. RKNN-Toolkit2官方文档
  2. RK3576 NPU技术白皮书
  3. YOLOv5官方仓库
  4. GStreamer Pipeline设计指南
  5. 我的前期文章

附录A:完整的YOLOv5推理脚本

由于篇幅限制,完整的Python代码已经集成在 yolo_npu_display.sh 脚本中。

关键函数说明:

  • xywh2xyxy(): 边界框坐标转换
  • process(): YOLO输出解析
  • filter_boxes(): 置信度过滤
  • nms_boxes(): 非极大值抑制(去除重叠框)
  • yolov5_post_process(): 完整后处理流程

DshanPI-A1第四篇开源手势项目改造

· 阅读需 8 分钟

本次所有的项目改造主要基于兼容性、流畅性、屏幕显示方式和摄像头调用等方面。

显示输出适配

主要的适配问题

1. RK3576运行纯Wayland环境

RK3576运行纯Wayland环境,没有X11和libGL支持,无法使用传统的cv2.imshow()显示图像。

【解决方案】

采用FIFO + GStreamer + Wayland的显示管线:

# Python端代码
fifo = open('/tmp/gesture_fifo', 'wb')
while True:
_, jpeg = cv2.imencode('.jpg', processed_frame,
[cv2.IMWRITE_JPEG_QUALITY, 85])
fifo.write(jpeg.tobytes())
fifo.flush()
# Shell端代码
# GStreamer从管道读取JPEG流并显示
gst-launch-1.0 filesrc location=/tmp/gesture_fifo ! \
jpegparse ! jpegdec ! videoconvert ! waylandsink fullscreen=true

2. IMX415摄像头第二次启动色彩异常

IMX415摄像头第二次启动时出现色彩异常,内核报错"no first iq setting"。

【解决方案】

在每次打开摄像头前重启rkaiq_3A_server:

def restart_3a(self):
os.system("killall rkaiq_3A_server 2>/dev/null")
time.sleep(2)
os.system("rm -f /tmp/.rkaiq_3A* 2>/dev/null")
os.system("/etc/init.d/S40rkaiq_3A start >/dev/null 2>&1")
time.sleep(5)

项目一:贪吃蛇游戏

项目开源地址:Project2/SnakeGame/main.py at main · WLHSDXN/Project2

改造过程

1. 多层级检测器架构

为了适配不同的依赖环境,设计了三层检测器回退机制:

优先级1: cvzone (MediaPipe封装,精度高)
↓ 不可用
优先级2: 原生MediaPipe (21个关键点)
↓ 不可用
优先级3: HSV肤色检测 (轻量级备用)

代码实现:

# 检测器选择逻辑
if USE_CVZONE:
detector = CvzoneHandDetector(detectionCon=0.8, maxHands=1)
elif USE_MEDIAPIPE:
detector = MediapipeHandDetector(maxHands=1,
detectionCon=0.5,
drawLandmarks=False)
else:
detector = SimpleHandDetector() # HSV备用方案

2. MediaPipe集成与封装

实现了MediapipeHandDetector类,返回与cvzone兼容的数据格式:

class MediapipeHandDetector:
def findHands(self, frame, flipType=False):
img_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
results = self.hands.process(img_rgb)

hands = []
if results.multi_hand_landmarks:
for hand_landmarks in results.multi_hand_landmarks:
# 提取21个关键点坐标
lmList = []
for lm in hand_landmarks.landmark:
x_px = int(lm.x * width)
y_px = int(lm.y * height)
lmList.append([x_px, y_px, lm.z])

hands.append({'lmList': lmList})

return hands, frame

【关键点】

食指指尖是lmList[8],直接用作蛇头控制点。

3. HSV肤色检测备用方案

当MediaPipe不可用时,使用简单的肤色检测:

def detect_hand_simple(frame):
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
mask = cv2.inRange(hsv, [0, 30, 60], [255, 255, 255])

# 形态学去噪
kernel = np.ones((7, 7), np.uint8)
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=3)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations=2)

contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
if contours:
c = max(contours, key=cv2.contourArea)
hull = cv2.convexHull(c)
# 提取最高点作为"指尖"
topmost = hull[hull[:, :, 1].argmin()][0]
return topmost

4. MediaPipe性能调优

优化1: 使用轻量级模型
self.hands = mp.solutions.hands.Hands(
model_complexity=0, # 0=lite, 1=full (默认)
max_num_hands=1,
min_detection_confidence=0.5, # 降低阈值换速度
min_tracking_confidence=0.5
)
优化2: 关闭可视化绘制
# 移除耗时的关键点绘制
# mp_drawing.draw_landmarks(frame, landmarks, connections) # 注释掉
drawLandmarks=False # 新增开关
优化3: 减少输入分辨率
# 640x480 已经是最优平衡点
# 若进一步降低到320x240可提升FPS,但会影响检测精度
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
优化4: 优化相机缓冲
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # 减少延迟

【优化效果】

FPS从5-10提升至15-25 FPS,满足游戏交互需求。

5. 文件控制接口

由于FIFO模式下无法直接捕获键盘,采用控制文件方式:

# Shell脚本部分
# 读取按键并写控制文件
stty echo icanon
while read n1 t 0.1 key; do
if [ "$key" = "r" ]; then
touch /tmp/snake_restart
elif [ "$key" = "q" ]; then
touch /tmp/snake_quit
fi
done
# Python部分
# 检测控制文件
if os.path.exists('/tmp/snake_quit'):
print('Quit command detected')
break

if os.path.exists('/tmp/snake_restart'):
os.remove('/tmp/snake_restart')
# 重置游戏状态
self.game.gameOver = False
self.game.points = []
self.game.previousHead = (0, 0)

效果展示

代码和演示视频均在附件当中

img

img

项目二:虚拟画板

开源项目:【基于手部关键点检测,隔空控制鼠标/隔空绘画】https://www.bilibili.com/video/BV1364y1h7PS?vd_source=a16ca768198c38baa684546cf5060811

改造过程

1. 核心逻辑提取

手势识别逻辑:
fingers = detector.fingersUp() # 返回5个值,1表示手指伸直

# 模式1: 选择工具(食指+中指伸直)
if fingers[1] and fingers[2]:
if y1 < 153: # 在顶部工具栏区域
if 0 < x1 < 320: color = [50, 128, 250] # 蓝色
elif 320 < x1 < 640: color = [0, 0, 255] # 红色
elif 640 < x1 < 960: color = [0, 255, 0] # 绿色
elif 960 < x1 < 1280: color = [0, 0, 0] # 橡皮擦

# 模式2: 绘画(仅食指伸直)
elif fingers[1] and not fingers[2]:
cv2.line(imgCanvas, (xp, yp), (x1, y1), color, brushThickness)
画布合成逻辑:
# 1. 将画布转为灰度图并二值化
imgGray = cv2.cvtColor(imgCanvas, cv2.COLOR_BGR2GRAY)
_, imgInv = cv2.threshold(imgGray, 50, 255, cv2.THRESH_BINARY_INV)

# 2. 使用位运算合成
img = cv2.bitwise_and(img, imgInv) # 摄像头画面保留非绘画区域
img = cv2.bitwise_or(img, imgCanvas) # 叠加绘画内容

2. 显示系统重构

复用贪吃蛇游戏的显示方案: FIFO + GStreamer

3. 工具栏内置化

原项目依赖4张PNG图片作为工具栏,在嵌入式系统上不便管理外部资源。

【解决方案】

用OpenCV绘图API生成工具栏

def create_header(self):
"""动态生成工具栏"""
header = np.zeros((100, self.width, 3), np.uint8)
header[:] = (200, 200, 200) # 灰色背景

tools = [
((250, 128, 50), "Blue"), # BGR格式
((0, 0, 255), "Red"),
((0, 255, 0), "Green"),
((0, 0, 0), "Eraser")
]

section_width = self.width // 4
for i, (color, label) in enumerate(tools):
x1 = i * section_width
x2 = (i + 1) * section_width

# 绘制颜色块
cv2.rectangle(header, (x1 + 10, 20), (x2 - 10, 80), color, -1)
cv2.rectangle(header, (x1 + 10, 20), (x2 - 10, 80),
(255, 255, 255), 2) # 白色边框

# 文字标签
cv2.putText(header, label, (x1 + 20, 95),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (50, 50, 50), 1)

return header

这样就可以对外部产生零依赖,更有利于移植和项目开发!

4. 摄像头与3A服务适配

复用贪吃蛇游戏的解决办法

5. 分辨率与性能权衡

【原版配置】

width = 1280, height = 720
画布: imgCanvas = np.zeros((720, 1280, 3), np.uint8)

【RK3576优化】

width = 640, height = 480 # 降低50%分辨率
画布: imgCanvas = np.zeros((480, 640, 3), np.uint8)

【原因】

  1. MediaPipe在640x480下FPS提升约2倍
  2. 画板应用对分辨率要求不如视觉识别高
  3. JPEG编码/传输速度更快

【工具栏适配】

原版: 顶部153像素高,分4个320像素宽的区域 RK3576: 顶部100像素高,分4个160像素宽的区域

section_width = self.width // 4 # 自适应宽度
if y1 < 100: # 工具栏高度
if 0 < x1 < section_width:
self.color = (250, 128, 50) # 蓝色
elif section_width < x1 < section_width * 2:
self.color = (0, 0, 255) # 红色
# ...

6. 交互控制改进

1. 清空画布机制

【原版】

if all(x >= 1 for x in fingers):
imgCanvas = np.zeros((720, 1280, 3), np.uint8)

【问题】

误触发率高,不便于精细控制。

【RK3576改进】

使用按键控制:

# Shell端
while read -n1 -t 0.1 key; do
if [ "$key" = "c" ]; then
touch /tmp/painter_clear
fi
done
# Python端
if os.path.exists('/tmp/painter_clear'):
os.remove('/tmp/painter_clear')
self.imgCanvas = np.zeros((self.height, self.width, 3), np.uint8)
print("Canvas cleared")
2. 退出控制

【原版】

只能通过cv2.waitKey(1)捕获键盘,依赖窗口焦点。

【RK3576】

双重退出机制:

  1. 按键控制: touch /tmp/painter_quit → Python检测并退出

  2. Ctrl+C: Shell脚本trap信号 → kill所有进程 → 清理FIFO

效果展示

代码和演示视频均在附件当中

img

img

技术总结与经验

  1. 跨平台显示适配 PC上的GUI方案不适用嵌入式,必须根据系统特性(Wayland/Framebuffer)选择输出方式。

  2. 资源内置化 嵌入式系统倾向于单文件部署,外部资源应转为代码生成或打包进程序。

  3. 性能分级优化

    • 算法层: 轻量级模型
    • 实现层: 关闭非必要绘制
    • 硬件层: 缓冲区/分辨率调优
  4. 交互方式适配 GUI依赖的键盘/鼠标事件需改为文件控制或GPIO触发。

DshanPI-A1 RK3576 armbian远程桌面

· 阅读需 2 分钟

背景与问题

  • 使用设备:DshanPI-A1,搭载 Armbian 系统,窗口系统为 Wayland,GPU 采用开源驱动。

  • 初始尝试:使用 NoMachine 实现远程桌面,但存在两个问题:

  1. 默认创建虚拟桌面,而非物理桌面;

  2. 对 Wayland 支持不佳,会以兼容模式开启 X11 桌面,导致 OpenGL 无法调用 GPU 加速。

DshanPI-A1 RK3576 gmrender-resurrect B站投屏

· 阅读需 4 分钟

演示效果

https://www.bilibili.com/video/BV1Z646zhEBP

一、环境信息

类别具体配置
板卡DshanPI-A1
主控芯片RK3576
操作系统Armbian
桌面系统GNOME
窗口系统Wayland
GPU 驱动Panfrost

二、实现原理

  1. 核心组件gmrender-resurrect 是一款接收 DLNA 服务内容,并通过 GStreamer 播放的工具,可直接配置为 DLNA 客户端。

  2. 硬件加速基础:已提前实现 GStreamer 硬件加速播放视频,满足高清流解码需求。

  3. B 站投屏适配:B 站 DLNA 投屏时,会发送 FLV 封装的 H264 流(类似直播流),通过 gmrender-resurrect 可直接调用硬件加速播放。

DshanPI-A1 RK3576 gstreamer播放16路视频与硬件加速

· 阅读需 9 分钟

演示视频

https://www.bilibili.com/video/BV1m34VziE2s

一、实验环境

类别具体配置
板卡DshanPI-A1
主控芯片RK3576
操作系统Armbian
桌面系统GNOME
窗口系统Wayland
GPU 驱动Panfrost

核心硬件加速单元说明

RK3576 芯片集成三类关键硬件加速单元,分别负责不同环节的视频处理:

  • VPU(视频处理单元):负责视频解码(如 H.264 硬解),核心元件 mppvideodec 调用此单元;

  • RGA(图像加速单元):负责图像缩放、格式转换(如 NV12→RGBA),可通过 mppvideodec 参数启用;

  • GPU(图形处理单元):负责视频渲染、多画面拼接(如 glvideomixer 拼接画面),由 Panfrost 驱动管理。