Skip to main content

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

· 7 min read

板子介绍

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 min read

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

这次我会在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 min read

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

硬件与环境准备

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

  • 核心板: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 min read

前言

在前面的文章中,我们已经实现了基于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 min read

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

显示输出适配

主要的适配问题

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 Weston多屏配置

· 11 min read

在嵌入式系统开发中,显示配置是用户界面实现的基础环节。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 min read

开发环境

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 min read

概述

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

模拟时代(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 min read

板子介绍

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 min read

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

这次我会在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. 状态管理很重要:嵌入式系统的服务重启问题往往是状态机管理不当导致的,彻底重置是最可靠的方案。