绿联 AX300(AIC8800DC)在 OpenWrt Linux 6.6 / 鲁班猫 2 上的驱动移植——软路由搭建指南

关键词:绿联 AX300、UGREEN CM760、AIC8800DC、AIC8800DC OpenWrt 驱动、Linux 6.6、OpenWrt 24.10、鲁班猫 2、RK3568、USB Wi-Fi、Wi-Fi 6、cfg80211、mac80211、usbmode

笔者有一个闲置的usb网卡,因此想到和鲁班猫2 rk3568开发板组合,刷入OpenWrt系统制作一个软路由。
本文记录将绿联 AX300 USB 无线网卡移植到鲁班猫 2 OpenWrt 24.10(Linux 6.6)上的完整过程。最终成果不是一个可跨设备直接安装的“通用 ARM64 IPK”,而是一套可以放进目标 OpenWrt 源码树、随目标内核重新编译的驱动包、兼容补丁和启动恢复机制。

本文使用的适配源码已经公开:

1. 实验环境和最终验证结果

本次实际验证环境如下:

项目 环境
开发板 EmbedFire LubanCat 2,RK3568,ARM64
OpenWrt LubanCatWrt / OpenWrt 24.10,提交 5f736f2
Linux(构建机和目标机保持一致,我的构建机用了Windows的wsl2 ubuntu24.04) 6.6.79
工具链 GCC 13.3.0、musl 1.2.5
无线网卡 绿联 CM760 / AX300,AIC8800DC
USB ID 初始 a69c:5722,切换后 a69c:88de
驱动模块 aic_load_fw.koaic8800_fdrv.ko
最终驱动包 kmod-aic8800dc_6.6.79.2025.03.03-r7_aarch64_generic.ipk

实际完成了固件加载、phy0/wlan0 注册、2.4 GHz AP、HE20(802.11ax)、WPA2 关联和持续流量验证。

需要先明确:内核模块和 OpenWrt 的 kernel ABI、内核配置以及 cfg80211/mac80211 backport 紧密绑定。即使另一台设备也是 ARM64,也应在它自己的 SDK 或完整源码树内重新编译本包,不能把上述 IPK 当作通用包强制安装。

2. 识别网卡:为什么插上后只看到一个“光驱”

在运行中的 OpenWrt 路由器 SSH/串口终端中输入:

lsusb

上述命令的终端输出最初类似如下(这是观察结果,不要输入):

Bus 006 Device 002: ID a69c:5722 aicsemi Aic MSC

MSC 表示 Mass Storage Class。该网卡上电后先以虚拟光驱/存储设备出现,这是因为 AX300 产品中还包括放置 Windows 驱动的存储区域。模式切换成功后,再次在终端执行 lsusb,应从终端输出中看到(不要输入这一行):

Bus 005 Device 003: ID a69c:88de AICSemi AIC8800DC

因此只移植内核驱动还不够,系统必须同时具备 USB 模式切换工具和对应规则。

这里需要区分两个名称:

  • OpenWrt 软件包名是 usb-modeswitch
  • 该软件包安装的实际可执行程序是 /sbin/usbmode

也就是说,在 OpenWrt 中通常执行 opkg install usb-modeswitch,而不是寻找一个名为 usbmode 的 opkg 软件包。OpenWrt 使用自己的 usbmode 程序和 /etc/usb-mode.json 数据库,并不是桌面 Linux 发行版常见的 usb_modeswitch 命令及其配置目录。

2.1 方法一:编译固件时内置(本文推荐)

在 OpenWrt 源码根目录的 Linux/WSL 终端执行 make menuconfig,然后在菜单中依次进入并选中(以下是菜单路径,不是终端命令):

Utilities  --->
    <*> usb-modeswitch

如果使用可复现的 seed 配置,则编辑项目文件 configs/bypass-router.seed,在软件包配置区域加入以下一行;若不用 seed,也可在 OpenWrt 源码根目录的 .config 中加入,但下一次 make defconfig 可能重排该文件:

CONFIG_PACKAGE_usb-modeswitch=y

然后在 OpenWrt 源码树(不是运行中路由器的根文件系统)创建以下文件;它位于 usbmode 包的 data 目录,文件名就是初始 VID-PID:

package/utils/usbmode/data/a69c-5722

新建文件 package/utils/usbmode/data/a69c-5722,从文件开头写入以下全部内容(该文件只有这些内容):

# UGREEN CM760 AX300 (AICSemi AIC8800DC) virtual CD-ROM mode
TargetVendor=0xa69c
TargetProduct=0x88de
StandardEject=1
WaitBefore=1

这个文本文件是构建输入。编译 usb-modeswitch 时,OpenWrt 的 convert-modeswitch.pl 会把 package/utils/usbmode/data/ 中的规则合并并转换为固件里的下列运行时文件路径(这是路径说明,不要输入):

/etc/usb-mode.json

因此不能简单地在运行中的路由器上创建 /package/utils/usbmode/data/a69c-5722;运行时根本不会读取这个源码目录。

保存规则后,在 Linux/WSL 构建机终端中进入 OpenWrt 源码根目录并输入:

cd openwrt
make defconfig
make package/utils/usbmode/clean
make package/utils/usbmode/compile V=sc -j1

上述单包编译成功后,如需生成整机固件,继续在同一个 OpenWrt 源码根目录终端输入:

make V=sc -j"$(nproc)"

将固件写入路由器并启动后,在 OpenWrt SSH/串口终端输入以下检查命令;每条命令都应列出对应文件:

ls -l /sbin/usbmode
ls -l /etc/usb-mode.json
ls -l /etc/init.d/usbmode

2.2 方法二:在已经运行的 OpenWrt 中安装

如果正在运行的 OpenWrt 24.10 能联网,在它的 SSH/串口终端输入:

opkg update
opkg install usb-modeswitch usbutils

如果目标系统明确使用 apk 而不是 opkg,才在该 OpenWrt 终端改用:

apk update
apk add usb-modeswitch usbutils

但是,只安装官方 usb-modeswitch 包不一定包含 a69c:5722 这条新增规则。本文的稳定做法是把规则放进源码树,重新编译包含该规则的 usb-modeswitch IPK 或整个固件,再安装生成的自定义 IPK。

不建议直接手工编辑 /etc/usb-mode.json 作为长期方案:它是构建过程中自动生成的文件,重新安装或升级软件包后可能被覆盖。手工 JSON 更适合临时诊断,格式和命令可参考 OpenWrt 官方 USB mode switch 文档。

2.3 usbmode 如何自动工作

安装完成后,下列内容是路由器根文件系统中应出现的文件路径(用于理解和检查,不是一次性粘贴执行的命令):

/sbin/usbmode
/etc/usb-mode.json
/etc/init.d/usbmode
/etc/hotplug.d/usb/20-usb_mode

/etc/init.d/usbmode 文件的 start_service() 函数会让 procd 启动下列命令;这是启动机制说明,通常不需要读者手工输入:

/sbin/usbmode -s

USB 热插拔时,hotplug 脚本会再次启动 usbmode 服务。因此规则已经进入 /etc/usb-mode.json 后,正常使用方式就是插入网卡,不需要每次手工运行切换命令。

在运行中的 OpenWrt SSH/串口终端按顺序输入以下命令。以 # 开头的行是说明性注释,可以随命令一起粘贴,也可以跳过:

# 确认软件已安装
opkg list-installed | grep usb-modeswitch

# 查看 usbmode 识别到的 USB 设备
/sbin/usbmode -l

# 确认数据库中包含初始 VID:PID
grep -i 'a69c:5722' /etc/usb-mode.json

# 插入网卡后观察初始和目标 ID
lsusb
logread -f

# 必要时手工触发一次系统扫描
/sbin/usbmode -s
# 或通过服务触发
/etc/init.d/usbmode restart

# 切换成功后应看到
lsusb | grep -Ei 'a69c:88de|AIC8800DC'

如果网卡始终停留在 a69c:5722,在 OpenWrt SSH/串口终端依次输入:

test -x /sbin/usbmode && echo usbmode-ok
test -f /etc/usb-mode.json && echo database-ok
grep -i 'a69c:5722' /etc/usb-mode.json
logread | grep -Ei 'usbmode|a69c|usb'
dmesg | grep -Ei 'a69c|usb-storage|CD-ROM|AIC'

若数据库中没有该 ID,说明自定义 data 文件没有进入本次 usb-modeswitch 构建产物,应清理并重新编译该包,而不是继续排查 AIC 内核模块。

2.4 参考资料

用于构建本文固件的项目文件 configs/bypass-router.seed 应在软件包配置区域包含以下两行;如果直接维护 OpenWrt .config,则检查其中有相同配置:

CONFIG_PACKAGE_usbutils=y
CONFIG_PACKAGE_usb-modeswitch=y

这里容易混淆两个概念:此处的切换是 USB 设备内部工作模式的切换,usbmode 负责把设备从 5722 切换到 88de;网络默认路由、旁路由网关和流量转发则属于 netifd、UCI 与防火墙的工作,不能靠 USB 驱动完成。

3. 获取 OpenWrt 和 AIC8800DC 源码

3.1 获取鲁班猫 2 的 OpenWrt 24.10 源码

在 Linux/WSL 构建机终端中,先切换到准备存放源码的、区分大小写的 Linux 文件系统目录,然后输入:

git clone --branch v24.10 --single-branch \
  https://github.com/LubanCat/LubanCatWrt.git openwrt
cd openwrt

完成克隆后,当前终端已经位于 openwrt 源码根目录;输入以下命令切换到本文验证过的提交:

git checkout 5f736f2

OpenWrt 必须在区分大小写的 Linux 文件系统中编译。WSL2 用户不要直接在 /mnt/c/mnt/d 下构建,应将工程复制到 WSL 的 ext4 文件系统,例如 ~/openwrt

如果 WSL 自动把 Windows PATH 拼进 Linux PATH,包含空格和括号的路径可能进入 U-Boot 等递归 Makefile。构建前在当前 Linux/WSL 终端输入以下命令;它只修改当前 shell 的 PATH

export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

3.2 获取已经整理好的 OpenWrt 驱动包

回到同时包含(或准备包含)openwrt/ 目录的上一级 Linux/WSL 工作目录,在终端输入以下命令。命令会克隆配套仓库,并把驱动包与 usbmode 规则复制到 OpenWrt 源码树:

git clone https://github.com/kasonhaimen/openwrt-aic8800dc.git
cp -a openwrt-aic8800dc/package/kernel/aic8800dc \
  openwrt/package/kernel/
cp openwrt-aic8800dc/package/utils/usbmode/data/a69c-5722 \
  openwrt/package/utils/usbmode/data/

配套仓库文件 package/kernel/aic8800dc/Makefile 的开头、PKG_NAME/PKG_VERSION/PKG_RELEASE 之后是源码定义。读者应在该位置检查或写入以下变量,而不是在终端输入:

PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/For-ACGN/AIC8800DC.git
PKG_SOURCE_VERSION:=b4e4a49137f08eab403770d323274fd95514c830
PKG_MIRROR_HASH:=ad91b1715892d7e1b1c104ab9e26dbbc6802ed392aa93cbfb2b1ec8d68028c9d

固定 commit 和 mirror hash 很重要:它保证其他人获取的是同一份驱动与固件,而不是上游后来发生变化的 main 分支。

4. OpenWrt 内核包的核心写法

编辑 package/kernel/aic8800dc/Makefile:在 include $(INCLUDE_DIR)/package.mk 之后的包定义区域加入以下完整 KernelPackage/aic8800dc 段;配套仓库已经包含,无需重复添加:

define KernelPackage/aic8800dc
  SUBMENU:=Wireless Drivers
  TITLE:=AIC8800DC/FC USB Wi-Fi driver
  DEPENDS:=@USB_SUPPORT +@DRIVER_11AX_SUPPORT +kmod-cfg80211 +kmod-mac80211 +kmod-usb-core
  FILES:= \
	$(PKG_BUILD_DIR)/drivers/aic8800/aic_load_fw/aic_load_fw.ko \
	$(PKG_BUILD_DIR)/drivers/aic8800/aic8800_fdrv/aic8800_fdrv.ko
  AUTOLOAD:=$(call AutoLoad,50,aic_load_fw aic8800_fdrv)
endef

OpenWrt 的无线栈使用 backports,不能仅引用内核源码原生头文件。编辑 package/kernel/aic8800dc/Makefile,在 KernelPackage/aic8800dc/description 结束之后、Build/Compile 之前加入以下 NOSTDINC_FLAGS 段:

NOSTDINC_FLAGS := \
	$(KERNEL_NOSTDINC_FLAGS) \
	-I$(PKG_BUILD_DIR)/drivers/aic8800 \
	-I$(STAGING_DIR)/usr/include/mac80211-backport \
	-I$(STAGING_DIR)/usr/include/mac80211-backport/uapi \
	-I$(STAGING_DIR)/usr/include/mac80211 \
	-I$(STAGING_DIR)/usr/include/mac80211/uapi \
	-include backport/backport.h

继续编辑同一个 package/kernel/aic8800dc/Makefile,紧接 NOSTDINC_FLAGS 段之后定义/替换 Build/Compile,内容如下:

define Build/Compile
	+$(KERNEL_MAKE) $(PKG_JOBS) \
		$(KERNEL_MAKE_FLAGS) \
		M="$(PKG_BUILD_DIR)/drivers/aic8800" \
		KBUILD_EXTRA_SYMBOLS="$(LINUX_DIR)/../symvers/mac80211.symvers" \
		NOSTDINC_FLAGS="$(NOSTDINC_FLAGS)" \
		CONFIG_PLATFORM_UBUNTU=n \
		modules
endef

这解决的是“在 OpenWrt 的无线 backport 环境里编译”,不是简单地“让 gcc 找到一个 cfg80211.h”。如果混用了内核原生头文件和 OpenWrt backport 头文件,可能编译通过一部分,最后却出现结构体、函数原型或模块符号版本冲突。

5. Linux 6.6 / OpenWrt backport 的主要报错与原因

5.1 为什么代码判断了 Linux 版本,仍然会报 API 不匹配

最关键的认识是:OpenWrt 24.10 虽然运行 Linux 6.6,但其 cfg80211/mac80211 API 来自更新内核的 backport。驱动中只用 LINUX_VERSION_CODE 判断函数签名并不可靠。

在 Linux/WSL 构建机终端执行 make package/kernel/aic8800dc/compile V=sc -j1 时,可能从终端构建日志中看到以下类型的错误(这些是示例输出,不要输入):

error: too few arguments to function 'cfg80211_ch_switch_notify'
error: too many arguments to function 'cfg80211_ch_switch_started_notify'
error: initialization of ... from incompatible pointer type
error: 'struct cfg80211_ops' ... incompatible pointer type for 'change_beacon'
error: too few arguments to function 'cfg80211_cac_event'

原因不是 ARM64 或 RK3568 本身,而是驱动假设的 cfg80211 API 与 OpenWrt 实际编译头文件不一致。

5.2 方案一:不修改源码版本,直接使用配套补丁编译(推荐)

适用人群:使用本文指定的 AIC8800DC 上游提交、OpenWrt 24.10/LubanCat2基线,只想复现实验并生成驱动或固件的读者。

如果你的硬件环境与本文一致或接近(本文使用鲁班猫2 RK3568开发板),请优先使用配套仓库中的补丁。 后文展示每一处 C 代码修改,是为了说明报错原因和方便排查,并不意味着读者必须逐行编辑上游源码。逐行手改容易漏掉关联修改,也不利于升级、清理和重复构建。

OpenWrt 会在准备驱动源码时,按照文件名顺序自动应用包目录 patches/ 下的补丁。本文配套补丁位于:

openwrt-aic8800dc/package/kernel/aic8800dc/patches/

上面是配套仓库中的目录路径,用于确认文件位置,不是终端命令。当前共有 001012 十二个补丁;Linux 6.6 和 OpenWrt cfg80211 backport 适配主要集中在 006010011012

如果已经按照第 3.2 节复制了整个 aic8800dc 包,那么补丁也已经一起复制,无需再执行其他 patch 命令。可以在 Linux/WSL 构建机终端进入 OpenWrt 源码根目录,然后输入以下命令确认:

ls package/kernel/aic8800dc/patches/

终端应列出 001-...patch012-...patchOpenWrt 在准备软件包源码时会按照文件名顺序自动应用补丁。确认后,在同一个 OpenWrt 源码根目录终端输入以下命令,清除旧的解包结果并重新编译:

make package/kernel/aic8800dc/clean
make package/kernel/aic8800dc/compile V=sc -j1

执行 clean 很重要:如果驱动源码此前已经在 build_dir 中解包,直接再次编译可能继续使用旧源码,让人误以为新补丁没有生效。重新编译时,OpenWrt 会重新下载/解包固定版本的上游源码,并自动按顺序应用 patches/ 目录中的文件。

如果构建日志出现类似下面的内容,说明某个补丁没有成功应用;这是构建终端中的错误输出示例,不要输入:

Patch failed! Please fix ...
Hunk #1 FAILED at ...

这通常表示目标驱动版本或 OpenWrt API 已经与本文基线不同。此时不要使用 --force 跳过失败补丁,而应检查:

  1. PKG_SOURCE_VERSION 是否仍为本文固定的上游提交;
  2. 是否误用了其他版本的 AIC8800DC 源码;
  3. 目标 OpenWrt 的 cfg80211/mac80211 backport 是否已经更新;
  4. 失败补丁修改的函数在目标源码中是否更名或改变参数。

对于本方案,到这里即可跳转到第 7 章的完整构建流程。后面的第 5.3 节属于源码移植原理和手工修改参考,使用本文固定版本及配套补丁时可以跳过。

5.3 方案二:更换源码或 OpenWrt 版本时的代码修改参考

适用人群:准备使用不同 AIC8800DC 驱动提交、不同 OpenWrt 分支或不同 cfg80211/mac80211 backport,需要重新制作或调整补丁的开发者。

本节解释现有补丁具体修改了什么。这里展示的代码不是终端命令;使用第 5.2 节固定源码和现成补丁的读者无需再次手工修改。

如果需要在另一份已经解压的上游驱动源码中测试现有补丁,应先进入那份上游源码的根目录,再在 Linux/WSL 终端输入下面的命令;路径请按实际 OpenWrt 工程位置调整:

patch -p1 --dry-run < /path/to/openwrt/package/kernel/aic8800dc/patches/010-build-against-openwrt-cfg80211-backport.patch
patch -p1 < /path/to/openwrt/package/kernel/aic8800dc/patches/010-build-against-openwrt-cfg80211-backport.patch

第一条只检查补丁能否应用,不修改文件;只有 --dry-run 没有报错时才执行第二条。正常使用第 5.2 节的 OpenWrt 包构建方式时不需要执行这两条命令。

5.3.1 适配 cfg80211 信道切换、Beacon 和雷达接口

这里需要修改两个“信道切换通知”函数的参数,使它们与当前 OpenWrt 的 cfg80211 接口一致:

  • cfg80211_ch_switch_notify():通知系统“信道切换已经完成”;
  • cfg80211_ch_switch_started_notify():通知系统“信道切换已经开始”。

编辑驱动源码文件 drivers/aic8800/aic8800_fdrv/rwnx_main.c,分别找到上述两个函数的调用位置,改成下面的形式。本文提供的 OpenWrt 驱动包已经通过 patches/010-build-against-openwrt-cfg80211-backport.patch 自动完成这项修改,因此使用配套驱动包的读者只需理解代码,不需要在终端输入或再次手工修改:

cfg80211_ch_switch_notify(vif->ndev, &csa->chandef, 0);

cfg80211_ch_switch_started_notify(dev, &csa->chandef, 0,
                                  params->count, params->block_tx);

同一上游文件 drivers/aic8800/aic8800_fdrv/rwnx_main.crwnx_cfg80211_change_beacon() 定义处需要使用 backport 提供的 cfg80211_ap_update。下列代码由补丁 010-build-against-openwrt-cfg80211-backport.patch 替换原函数签名和构建 beacon 的调用:

static int rwnx_cfg80211_change_beacon(struct wiphy *wiphy,
                                       struct net_device *dev,
                                       struct cfg80211_ap_update *info)
{
    /* ... */
    buf = rwnx_build_bcn(bcn, &info->beacon);
    /* ... */
}

drivers/aic8800/aic8800_fdrv/rwnx_main.c 中找到 rwnx_cfg80211_set_monitor_channel() 函数定义,在参数列表中增加 net_device 参数;配套包通过补丁 010-build-against-openwrt-cfg80211-backport.patch 完成:

static int rwnx_cfg80211_set_monitor_channel(struct wiphy *wiphy,
                                             struct net_device *dev,
                                             struct cfg80211_chan_def *chandef)

drivers/aic8800/aic8800_fdrv/rwnx_main.c 中找到 rwnx_cfg80211_start_radar_detection() 的函数定义,在参数列表末尾增加 link_id;该修改位于补丁 010-build-against-openwrt-cfg80211-backport.patch

int rwnx_cfg80211_start_radar_detection(struct wiphy *wiphy,
                                        struct net_device *dev,
                                        struct cfg80211_chan_def *chandef,
                                        u32 cac_time_ms,
                                        int link_id)

在上游文件 drivers/aic8800/aic8800_fdrv/rwnx_radar.crwnx_radar_cac_work()rwnx_radar_cancel_cac() 附近,给 cfg80211_cac_event() 调用补充最后一个 link ID 参数;配套包由 patches/011-add-cac-link-id-for-cfg80211-backport.patch 修改:

cfg80211_cac_event(ndev, &ctxt->chan_def,
                   NL80211_RADAR_CAC_FINISHED, GFP_KERNEL, 0);
5.3.2 网络设备注册必须经过 cfg80211

在未修改的上游文件 drivers/aic8800/aic8800_fdrv/rwnx_main.c 中,接口创建和删除函数附近原本能看到以下调用(这是原代码摘录,不要在终端输入):

register_netdevice(ndev);
unregister_netdevice(dev);

在同一个 rwnx_main.c 的相同位置,应把上述两处分别替换为下列调用;配套包通过 patches/012-use-cfg80211-netdevice-registration.patch 自动完成:

cfg80211_register_netdevice(ndev);
cfg80211_unregister_netdevice(dev);

这使 netdev 的注册/注销与 cfg80211 的锁和对象生命周期一致。本项目 r3 真机运行时,在鲁班猫 2 串口终端和 dmesg 中曾看到以下 ARM64 Oops;原始记录保存在项目文件 artifacts/r3首次开机.txt 中(这是日志输出,不要输入):

Internal error: Oops: 0000000096000006 [#1] SMP
pc : rwnx_cfg80211_probe_client+0xf00/0x2988 [aic8800_fdrv]
lr : cfg80211_scan+0x180/0x1b8 [cfg80211]

仅凭一次栈回溯不能把所有责任归结为同一行代码,但它明确说明“模块能加载”不等于驱动已经正确适配 cfg80211 生命周期。后续版本加入 backport API 修正和 cfg80211 netdevice 注册后,继续进行扫描、AP 和持续流量测试。

5.3.3 内核 6.x 的写法与 GCC 13 警告

GCC 13 和内核构建选项会把部分警告当成错误。对应修正包括:

在上游文件 drivers/aic8800/aic8800_fdrv/rwnx_tx.crwnx_select_txq() switch 中,用 fallthrough; 明确标记 AP_VLAN 分支穿透;配套包由 patches/004-mark-ap-vlan-fallthrough.patch 修改:

case NL80211_IFTYPE_AP_VLAN:
    rwnx_vif = rwnx_vif->ap_vlan.master;
    fallthrough;
case NL80211_IFTYPE_AP:

drivers/aic8800/aic8800_fdrv/rwnx_main.crwnx_interface_add() 设置接口 MAC 的位置,用内核接口替换对 ndev->dev_addr 的直接 memcpy;该代码来自 patches/006-update-main-for-linux-6.x.patch

eth_hw_addr_set(ndev, params->macaddr);

drivers/aic8800/aic8800_fdrv/rwnx_msg_rx.crwnx_rx_sm_disconnect_ind() 局部变量声明区,将 macaddr 声明改为以下类型;该修改位于 patches/002-use-const-netdev-address.patch

const u8 *macaddr;

drivers/aic8800/aic8800_fdrv/rwnx_rx.crwnx_rx_add_rtap_hdr() 中,替换向 radiotap HE 区域写入数据的原 memcpy;该修改位于 patches/003-annotate-variable-radiotap-buffer-copy.patch

unsafe_memcpy(pos, &he, sizeof(he),
              "radiotap header is a variable-length skb buffer");

drivers/aic8800/aic8800_fdrv/rwnx_debugfs.crwnx_dbgfs_rc_fixed_rate_idx_write() 中,替换解析父目录 MAC 地址的原 sscanf 和无效的 mac == NULL 判断;该修改位于 patches/008-validate-debugfs-mac-parse.patch

if (sscanf(name, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx",
           &mac[0], &mac[1], &mac[2],
           &mac[3], &mac[4], &mac[5]) != ETH_ALEN)
    return 0;

此外,在 drivers/aic8800/aic8800_fdrv/rwnx_main.crwnx_csa_finish() 末尾,原代码在完成路径调用了 wiphy_lock(),但上下文实际需要释放锁。该行由 patches/010-build-against-openwrt-cfg80211-backport.patch 改为:

wiphy_unlock(rwnx_hw->wiphy);

此类修改应结合目标头文件和调用上下文审查,不能机械地按内核版本号替换。

6. 驱动加载了,为什么仍然找不到固件

第一次驱动探测时,在鲁班猫 2 串口/SSH 终端执行 dmesglogread 能看到以下关键输出;原始记录位于项目文件 artifacts/网卡检查.txt(这是日志,不要输入):

rwnx_load_firmware: firmware path = /vendor/etc/firmware/aic8800DC/fmacfw_patch_8800dc_u02.bin
rwnx_load_firmware: fmacfw_patch_8800dc_u02.bin file failed to open
usb_err:<aicwf_usb_probe,...>: failed with errno -1
aic8800_fdrv: probe ... failed with error -1

而安装 IPK 或刷入固件后,固件应出现在路由器根文件系统的下列目录(这是路径说明,不要输入):

/lib/firmware/aic8800DC/

调试阶段为了验证“仅仅是路径错误”,曾在运行中的鲁班猫 2 OpenWrt 终端输入以下临时命令。它只是诊断手段,正式固件应修改源码,不应长期依赖该软链接:

mkdir -p /vendor/etc
ln -s /lib/firmware /vendor/etc/firmware

软链接后固件能够继续加载,证明问题是硬编码路径,而不是固件文件缺失或 USB 传输失败。正式修改位于上游文件 drivers/aic8800/aic_load_fw/aicbluetooth.c 靠近全局默认固件路径定义处;配套包由 patches/009-use-openwrt-firmware-path.patch 替换为:

#if defined(CONFIG_PLATFORM_UBUNTU)
static const char *aic_default_fw_path = "/lib/firmware/";
#else
static const char *aic_default_fw_path = "/lib/firmware";
#endif

编辑 package/kernel/aic8800dc/Makefile,在文件末尾、$(eval $(call KernelPackage,aic8800dc)) 之前的 KernelPackage/aic8800dc/install 定义中加入固件复制逻辑:

define KernelPackage/aic8800dc/install
	$(INSTALL_DIR) $(1)/lib/firmware/aic8800DC
	$(CP) $(PKG_BUILD_DIR)/fw/aic8800DC/* \
		$(1)/lib/firmware/aic8800DC/
endef

刷入新固件或安装新 IPK、重启并插入网卡后,在鲁班猫 2 的 OpenWrt SSH/串口终端输入以下命令验证:

find /lib/firmware/aic8800DC -type f
dmesg | grep -Ei 'aic|8800|firmware|cfg80211|wlan'
lsmod | grep -Ei 'aic|cfg80211|mac80211'
iw phy
ip link show

7. 编译配置与构建步骤

编辑项目文件 configs/bypass-router.seed,在目标设备和软件包配置区域检查或加入以下配置;如果不使用项目 seed,则在 OpenWrt 源码根目录 .config 中确保 make defconfig 后能看到相同内容:

CONFIG_TARGET_rockchip=y
CONFIG_TARGET_rockchip_armv8=y
CONFIG_TARGET_DEVICE_rockchip_armv8_DEVICE_embedfire_lubancat-2=y
CONFIG_PACKAGE_usbutils=y
CONFIG_PACKAGE_usb-modeswitch=y
CONFIG_PACKAGE_kmod-aic8800dc=y
CONFIG_PACKAGE_wpad-openssl=y
CONFIG_DRIVER_11AX_SUPPORT=y

不要同时保留多个 wpad/hostapd 完整实现。Wi-Fi 6 AP 测试使用 wpad-openssl,并启用 CONFIG_DRIVER_11AX_SUPPORT,否则 netifd 生成的 HE 参数可能被 hostapd 拒绝。

在 Linux/WSL 构建机终端进入 OpenWrt 源码根目录,然后按顺序输入以下命令;其中以 # 开头的行是终端注释:

cd openwrt
./scripts/feeds update -a
./scripts/feeds install -a

make menuconfig
make defconfig
make download -j"$(nproc)"

# 先单独验证驱动包,便于看到完整错误
make package/kernel/aic8800dc/clean
make package/kernel/aic8800dc/compile V=sc -j1

# 驱动成功后再构建固件
make V=sc -j"$(nproc)"

使用 -j1 V=sc 重现驱动编译问题非常重要。并行构建时,屏幕最后一行往往只是上层 Makefile 的 Error 2,真正的 C 编译错误可能在此前数百行。

构建完成后,下列是相对于 OpenWrt 源码根目录的产物路径模式(用于查找文件,不是终端输出,也不是需要逐行输入的命令):

bin/targets/rockchip/armv8/
bin/targets/rockchip/armv8/packages/kmod-aic8800dc_*.ipk

8. 鲁班猫 2 真机调试过程概述

本项目不是一次编译成功,而是按以下顺序逐步排除问题:

  1. 初始固件:系统能启动,但网卡保持 a69c:5722,确认需要 usbmode 自动弹出规则。
  2. USB mode 测试:网卡变为 a69c:88de,驱动开始 probe,但固件从 /vendor/etc/firmware 加载失败。
  3. 固件路径测试:通过软链接验证后,把默认路径正式改为 /lib/firmware
  4. r3 / backport 适配:模块和固件可以加载,phy0 出现;扫描期间发生 rwnx_cfg80211_probe_client ARM64 Oops,继续修正 cfg80211 API 和 netdev 注册。
  5. r4 / AP 调试:驱动注册成功,但 hostapd 报配置错误或 Failed to set up interface。先用 HT20、固定 2.4 GHz/信道 6 排除 HE 配置影响。
  6. r5 / 11ax:加入 wpad-opensslCONFIG_DRIVER_11AX_SUPPORT,再开启 HE20,验证 WPA2 和 Wi-Fi 6 AP。
  7. r6 / 开机重试:解决 usbmode、固件上传、PHY 注册晚于 netifd 首轮无线初始化的问题。
  8. r7 / 动态 path:解决 USB 总线号跨启动变化后,UCI 中静态 radio0.path 指向旧 sysfs 路径的问题。

AP 最小诊断配置可以从保守值开始。在运行中的鲁班猫 2 OpenWrt SSH/串口终端输入以下 UCI 命令;这些命令会修改 /etc/config/wireless 中的 radio0default_radio0 段:

uci set wireless.radio0.country='CN'
uci set wireless.radio0.band='2g'
uci set wireless.radio0.channel='6'
uci set wireless.radio0.htmode='HT20'
uci set wireless.radio0.disabled='0'
uci set wireless.default_radio0.disabled='0'
uci commit wireless
wifi reload

确认普通 HT20 AP 正常后,再改成目标 HE 模式。这样可以区分“驱动/AP 基础功能故障”和“hostapd 的 11ax 配置/能力协商故障”。

建议每轮都在鲁班猫 2 OpenWrt SSH/串口终端输入以下只读诊断命令,并把终端输出保存到日志文件用于版本间对比:

lsusb
lsmod | grep -Ei 'aic|cfg80211|mac80211'
iw phy
iw dev
ubus call network.wireless status
uci show wireless
dmesg | grep -Ei 'aic|8800|usb|firmware|cfg80211|wlan|oops'
logread | grep -Ei 'netifd|hostapd|AP-ENABLED|failed|error'

9. 为什么需要开机无线恢复服务

手工执行 wifi up radio0 能成功,并不代表每次冷启动都会成功。下面是启动时序示意图,用于阅读理解,不是终端命令或实际日志:

a69c:5722 虚拟光驱
  -> usbmode 弹出
  -> USB 重新枚举为 a69c:88de
  -> aic_load_fw 上传固件
  -> aic8800_fdrv 注册 phy0
  -> netifd/hostapd 配置 radio0

netifd 可能在 phy0 出现前已经完成首轮无线设置并放弃。另一个问题是 USB 重新枚举后的总线拓扑可能从例如 5-1 变成其他路径,/etc/config/wireless 中保存的 radio0.path 因此失效;再次运行 wifi config 还可能生成额外的禁用 radio 配置。

r7 服务安装一个 START=99 的晚启动脚本。新建包内文件 package/kernel/aic8800dc/files/aic8800dc-wifi-retry.init,从文件开头写入以下全部内容;安装 IPK 后它会成为路由器上的 /etc/init.d/aic8800dc-wifi-retry

#!/bin/sh /etc/rc.common

START=99

start() {
    /usr/sbin/aic8800dc-wifi-retry &
}

恢复逻辑位于包内文件 package/kernel/aic8800dc/files/aic8800dc-wifi-retry.sh。在 sync_radio_path() 函数开头、局部变量声明之后加入以下代码,用于读取真实 sysfs 路径;安装后对应 /usr/sbin/aic8800dc-wifi-retry

phy_path="$(readlink -f /sys/class/ieee80211/phy0 2>/dev/null)"
phy_path="${phy_path#/sys/devices/}"
phy_path="${phy_path%/ieee80211/phy0}"

继续编辑同一个 aic8800dc-wifi-retry.sh,在 sync_radio_path() 中完成 phy_path 非空检查后加入以下条件块,同步 radio0.path

if [ "$(uci -q get wireless.radio0.path)" != "$phy_path" ]; then
    logger -t aic8800dc "updating radio0 path to $phy_path"
    uci set wireless.radio0.path="$phy_path"
    uci commit wireless
    ubus call network reload >/dev/null 2>&1
    sleep 3
fi

本实验板只有一个 WLAN PHY,因此脚本还会清理绑定旧 USB 枚举路径的 radio。以下两个循环位于同一文件 aic8800dc-wifi-retry.shsync_radio_path() 中,放在设置 wireless.radio0.path 之后、uci commit wireless 之前:

for iface in $(uci -q show wireless |
        sed -n 's/^wireless\.\([^.=]*\)=wifi-iface$/\1/p'); do
    iface_device="$(uci -q get wireless."$iface".device)"
    [ "$iface_device" = "radio0" ] || uci -q delete wireless."$iface"
done

for section in $(uci -q show wireless |
        sed -n 's/^wireless\.\([^.=]*\)=wifi-device$/\1/p'); do
    [ "$section" = "radio0" ] || uci -q delete wireless."$section"
done

同一文件 aic8800dc-wifi-retry.sh 的后半部分,在 sync_radio_path() 函数结束后定义 radio_up()(配套仓库实际把这个小函数放在文件开头也可以),并在脚本主流程末尾加入以下轮询重试逻辑。以下是文件代码,不是在终端逐行输入:

radio_up() {
    ubus call network.wireless status 2>/dev/null |
        jsonfilter -e '@.radio0.up' 2>/dev/null
}

attempt=0
retries=0
while [ "$attempt" -lt 12 ]; do
    sleep 5
    attempt=$((attempt + 1))

    [ "$(radio_up)" = "true" ] && exit 0
    [ -e /sys/class/ieee80211/phy0 ] || continue

    retries=$((retries + 1))
    wifi up radio0
    sleep 8
    [ "$(radio_up)" = "true" ] && exit 0
    [ "$retries" -ge 3 ] && break
done

注意:清理非 radio0 配置的逻辑只适合本实验的单 PHY 板。如果目标设备同时有板载 Wi-Fi 或多个 USB 无线网卡,必须根据 VID/PID、驱动名或 sysfs 路径精确识别 AIC 设备,不能直接删除其他 radio。

10. 旁路由网络和“自动路由”配置

kmod-tunkmod-nft-tproxy 是通用的 OpenWrt 内核网络功能包,与 AIC8800DC 无线驱动本身没有直接依赖关系:

  • kmod-tun 提供 Linux TUN/TAP 虚拟网络接口支持;
  • kmod-nft-tproxy 提供 nftables 的透明代理数据包处理能力。

如果后续网络方案需要虚拟三层接口或透明转发能力,可以在项目文件 configs/bypass-router.seed 的软件包配置区域保留以下两行;如果只验证 AIC8800DC 无线驱动和普通 AP,这两个包不是必需项:

CONFIG_PACKAGE_kmod-tun=y
CONFIG_PACKAGE_kmod-nft-tproxy=y

上面是构建配置文件内容,不是运行中路由器终端需要输入的命令。两个 kmod 都必须随目标 OpenWrt 固件一起编译,或从与当前固件的内核版本、内核配置和 kernel ABI 完全匹配的软件源安装;不能仅因为设备同为 RK3568 或 ARM64 就混用其他固件的 kmod IPK。

无线驱动只提供链路,旁路由仍需明确配置管理地址、上级网关、DNS、DHCP 和 WAN。新建项目文件 scripts/configure-bypass.sh,从文件开头写入以下脚本内容;这是文件源码,不要直接逐行粘贴进终端:

#!/bin/sh
set -eu

LAN_IP="$1"
GATEWAY="$2"
NETMASK="${3:-255.255.255.0}"

uci batch <<EOF
set network.lan.proto='static'
set network.lan.ipaddr='$LAN_IP'
set network.lan.netmask='$NETMASK'
set network.lan.gateway='$GATEWAY'
del_list network.lan.dns='$GATEWAY'
add_list network.lan.dns='$GATEWAY'
set network.wan.disabled='1'
set network.wan6.disabled='1'
set dhcp.lan.ignore='1'
EOF

uci commit network
uci commit dhcp
/etc/init.d/dnsmasq restart
/etc/init.d/network restart

scripts/configure-bypass.sh 复制到鲁班猫 2 后,在它的 OpenWrt SSH/串口终端进入脚本所在目录,并输入以下命令。执行会立即修改网络配置并重启网络,SSH 地址随后变为 192.168.1.2

sh configure-bypass.sh 192.168.1.2 192.168.1.1 255.255.255.0

脚本禁用 OpenWrt 自己的 LAN DHCP,防止与主路由形成两个 DHCP 服务;同时将主路由设为默认网关和 DNS。若只希望个别终端经过鲁班猫 2 转发,应在主路由中为这些终端单独设置网关/DNS,或在终端手工配置,而不是让整个局域网无条件改道。

11. 将恢复脚本打进 IPK

编辑 package/kernel/aic8800dc/Makefile,找到文件末尾的 KernelPackage/aic8800dc/install 定义,用以下完整定义替换原安装段;它必须位于最终 $(eval $(call KernelPackage,aic8800dc)) 之前:

define KernelPackage/aic8800dc/install
	$(INSTALL_DIR) $(1)/lib/firmware/aic8800DC
	$(CP) $(PKG_BUILD_DIR)/fw/aic8800DC/* $(1)/lib/firmware/aic8800DC/

	$(INSTALL_DIR) $(1)/etc/init.d
	$(INSTALL_BIN) ./files/aic8800dc-wifi-retry.init \
		$(1)/etc/init.d/aic8800dc-wifi-retry

	$(INSTALL_DIR) $(1)/usr/sbin
	$(INSTALL_BIN) ./files/aic8800dc-wifi-retry.sh \
		$(1)/usr/sbin/aic8800dc-wifi-retry
endef

把生成的 IPK 安装到鲁班猫 2 后,在 OpenWrt SSH/串口终端输入以下命令,建立开机启动链接、立即启动服务并读取日志:

/etc/init.d/aic8800dc-wifi-retry enable
/etc/init.d/aic8800dc-wifi-retry start
logread | grep aic8800dc

如果脚本随固件预装,还应确认 OpenWrt 打包阶段是否自动生成了 init 链接;若只安装 IPK,则显式执行 enable 最稳妥。

12. 完整验收清单

每次升级驱动、内核或 OpenWrt backport 后,至少完成以下测试:

  1. 冷启动时 a69c:5722 能自动切换为 a69c:88de
  2. aic_load_fwaic8800_fdrv 自动加载,无 Unknown symbol/invalid module format。
  3. 固件从 /lib/firmware/aic8800DC 成功读取。
  4. iw phy 出现 phy0iw dev 出现接口。
  5. 扫描不会再触发 Oops。
  6. HT20 AP 能启动,日志出现 AP-ENABLED
  7. WPA2 客户端能够反复关联、断开和重连。
  8. 开启 HE20 后客户端能够协商 802.11ax。
  9. 持续传输和经鲁班猫 2 路由转发的流量保持稳定。
  10. 至少进行多次断电冷启动,确认 USB 总线号变化时 radio0.path 会自动更新。
  11. 拔插网卡后验证模块、netifd 和配置不会进入不可恢复状态。

完成一次冷启动后,在鲁班猫 2 OpenWrt SSH/串口终端输入以下只读检查命令;终端输出应分别反映 USB ID、无线 UCI、实际 sysfs path、netifd 状态、无线接口和关键日志:

lsusb
cat /etc/config/wireless
readlink -f /sys/class/ieee80211/phy0
ubus call network.wireless status
iw dev
logread | grep -Ei 'aic8800dc|hostapd|AP-ENABLED|failed'
dmesg | grep -Ei 'aic|8800|firmware|oops|unknown symbol'

13. 移植到其他 ARM64 OpenWrt 设备时要改什么

源码包可以继续移植,但应考虑下列情况:

  • 在目标 OpenWrt SDK/源码树内重新编译,绝不强装鲁班猫 2 的 IPK。
  • 根据目标树的 cfg80211.hnet/cfg80211.hstruct cfg80211_ops 核对回调原型。
  • 核对 mac80211.symvers 和 backport include 路径。
  • 确认目标内核的 USB Host、电源和 cfg80211/mac80211 配置。
  • 多 PHY 设备必须改写 r7 的 radio 清理逻辑。
  • Linux 6.1、5.15、6.12 或 OpenWrt 25.x 不能假设直接适用现有补丁。

判断兼容性的单位不是“ARM64”,甚至也不只是“Linux 6.6”,而是目标 OpenWrt 的完整内核 ABI、无线 backport API 和配置组合。

结语

这次移植最容易误判的地方有三个:第一,网卡附带了一个 U盘,首次识别到的是U盘,驱动根本没有机会 probe;第二,驱动能够编译和加载,并不代表它已经适配 OpenWrt 的 cfg80211 backport;第三,手工 wifi up 成功不代表冷启动可靠,usbmode、固件加载、PHY 注册和 netifd 之间仍存在时序竞争。

本文将USB 模式切换、驱动源码补丁、固件路径、OpenWrt backport 集成、hostapd 11ax 配置以及延迟启动恢复服务作为一个整体处理,最终得到可重复构建、可多次冷启动并可持续运行的无线软路由。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐