Skip to main content

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音频录制播放与噪声分析

· 14 min read

音频播放

扬声器设备

先看以下这个扬声器如何使用,先列出音频播放设备

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,做模型微调则需要参考,以获得更好的降噪效果。

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

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

序言

本文档旨在为开发者和爱好者提供一份清晰、简洁的 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 min read

在下载百问网官方适配后的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 min read

方案介绍与选择

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

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 RK3576 armbian远程桌面

· 2 min read

背景与问题

  • 使用设备:DshanPI-A1,搭载 Armbian 系统,窗口系统为 Wayland,GPU 采用开源驱动。

  • 初始尝试:使用 NoMachine 实现远程桌面,但存在两个问题:

  1. 默认创建虚拟桌面,而非物理桌面;

  2. 对 Wayland 支持不佳,会以兼容模式开启 X11 桌面,导致 OpenGL 无法调用 GPU 加速。