简介:面向嵌入式 Linux 平台开发者的 MT7663 USB 转 Wi-Fi 驱动源码包,已经在海思3531硬件平台上完成交叉编译验证,可直接生成大小约 3MB 的内核驱动模块,适合正在移植无线网卡驱动或者调试 Wi-Fi 相关功能的开发人员直接使用。该源码包内部的核心文件是已经配置完成的 Makefile 文件,里面明确指定了 aarch64-linux-gnu- 交叉编译工具链,并将工具链前缀、内核源码所在目录、驱动源码所在目录集中整合,方便使用者根据不同的目标平台自行调整路径和架构参数,从而快速集成到现有嵌入式系统项目中。整个压缩包共包含 453 个文件,体积约为 7.62MB,文件类型以 C 语言源码与头文件、命令脚本、编译中间文件、固件和配置文件为主,同时还有 makefile、说明文档、内核符号表和可装载模块等,基本覆盖了从源码修改、交叉编译生成到固件配置验证的完整过程。目前已有 424 人学习下载,额外附带的补丁固件及无线配置设置文件,对排查驱动模块加载失败、交叉编译环境异常和无线功能配置错误等问题具有直接参考价值。 上个月帮客户调一块基于ARM Cortex-A7的工业网关,硬件上要增加一路USB WiFi做无线管理通道。客户丢给我一个网盘链接,说这是mt7663 USB转WiFi的源码包,makefile已经配好了,直接交叉编译就行。我解压一看,所谓"配好了",离"能编过"还差着好几段弯路。
MT7663这颗料在嵌入式圈子里出现频率很高。联发科的老一代WiFi 5方案,支持USB和SDIO两种接口形态,2.4G/5G双频,蓝牙5.0共存,很多工控板、机顶盒、边缘网关都拿它做无线接入。但官方驱动源码散落各处,GitHub上fork版本一大堆,每个版本的内核适配、编译配置、固件依赖都不一样。这篇把mt7663 USB驱动交叉编译这条路上的关键点串一遍,包括makefile里到底改了什么、为什么这样改、编译不过时怎么定位,给后面要踩这坑的人一个完整参考。
1. USB接口的mt7663到底解决什么问题
1.1 为什么不用板载SDIO而是USB转WiFi
先聊一下这芯片在嵌入式产品里的定位。MT7663本身是支持SDIO和USB两种接口的WiFi/BT combo芯片。板载SDIO的好处是走总线带宽高、延迟低,但代价是主控SoC必须预留SDIO控制器引脚,PCB布局要走差分线,天线匹配也要一并设计,整个硬件改版周期拖得很长。
USB转WiFi的思路完全不一样。主板只要引出一路USB Host接口,插一个USB WiFi模块,驱动程序加载之后就能工作。对于那些存量设备、已经定型的主板、或者只打算小批量出货验证功能的项目来说,这是性价比极高的方案。模块坏了直接换,不用返修主板。很多工业平板和边缘计算盒子就是这么干的,一个USB WiFi dongle搞定无线接入,剩下几个USB口还能挂4G模组、U盘或者调试串口。
1.2 源码包里通常有哪些东西
从网上拿到的mt7663驱动源码包,解压之后一般长这样:
mt7663_usb/ ├── Makefile ├── Makefile.6.x ├── common/ │ ├── mt76_chip.c │ ├── mt76_phy.c │ └── ... ├── include/ │ ├── mt7663_reg.h │ └── ... ├── os/ │ ├── linux/ │ │ ├── mt76_usb.c │ │ └── ... │ └── ... ├── firmware/ │ ├── mt7663_patch_e2_hdr.bin │ ├── mt7663_rom_patch.bin │ └── ... └── patch/ ├── 0001-mt7663-support.patch └── ...注意firmware目录,mt7663不是一颗纯软件MAC的芯片,而是一个半开源的FullMAC方案。所谓FullMAC,是指大部分MAC层管理功能在芯片固件里完成,主机侧驱动只负责把配置命令和网络数据送进去。但固件二进制是闭源的,官方只提供编译好的.bin文件,不提供固件源码。这就是mt7663驱动编译和部署绕不开的第一道坎——驱动和固件必须配套,版本对不上,模块能装上,但WiFi接口根本起不来。
2. 交叉编译环境搭建:三个关键决策
2.1 工具链:用哪个交叉编译器,差别很大
热搜词里出现"linaro交叉编译工具链最新版本下载",说明不少人卡在工具链选型上。mt7663这类老芯片的驱动源码,对工具链版本其实很敏感。我这次用的是ARM Cortex-A7平台,glibc版本是2.28,内核版本4.19,选的工具链是gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf。为什么不用最新的gcc 12/13?
因为驱动源码里有些老的函数调用写法,在高版本gcc下会直接报warning甚至error。比如在结构体初始化时用了隐式类型转换,老gcc睁一只眼闭一只眼,新gcc按C11标准严格校验直接不给过。交叉编译工具链不是越新越好,老内核最好配合老工具链,这个经验在嵌入式Linux里几乎通用。
2.2 工具链安装和PATH配置
我习惯把工具链解压到/opt/toolchains/下,然后软链成固定路径:
sudo mkdir -p /opt/toolchains sudo tar xf gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz -C /opt/toolchains/ sudo ln -sf /opt/toolchains/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf /opt/toolchains/arm-linux-gnueabihf export PATH=/opt/toolchains/arm-linux-gnueabihf/bin:$PATH arm-linux-gnueabihf-gcc --version能用软链固定路径是很有用的习惯,因为多个项目会共用同一套工具链,路径被写死在makefile里之后,一旦工具链升级或换目录,改一个入口就行了,不用到处改。
2.3 内核源码路径:整个编译体系的锚点
mt7663驱动不是一个完全独立的应用,它编译时要引用内核源码树里的头文件、Kbuild规则和模块符号。所以必须在开发机上准备一份和板子上内核版本完全一致的内核源码,并先完成基础配置,生成Module.symvers和include/generated/autoconf.h。
我第一次偷懒,直接拿一份没配置过的内核源码树来编,结果报了一堆linux/version.h: No such file or directory。这类错不是真的没装内核头文件,而是内核源码没执行过make ARCH=arm modules_prepare。正确姿势是:
export ARCH=arm export CROSS_COMPILE=arm-linux-gnueabihf- # 如果是原厂提供的内核,一般自带defconfig make <board_defconfig> make modules_preparemodules_prepare这个目标会生成编译模块所需的一系列中间文件,不做这一步,后面编译必挂。这个教训写下来给所有第一次碰内核模块编译的人:拿到内核源码,先别急着进驱动目录编驱动,先把内核源码的状态整理好。
3. makefile里"已配置好"到底配置了什么
3.1 Kbuild结构下的模块挂载方式
MT7663驱动的makefile,核心机制是Linux内核的Kbuild模块编译系统。也就是说,驱动源码本身不是一个独立的工程,它必须插入到内核源码树的构建流程里。常用做法是在驱动目录里执行:
make -C /path/to/kernel M=$(pwd) modules这里的-C是切换到内核源码目录,M=是指定外部模块源码位置。在这个框架下,makefile写什么就非常关键。我摘一段典型内容:
# 指定交叉编译工具链前缀 CROSS_COMPILE ?= arm-linux-gnueabihf- # 指定目标架构 ARCH ?= arm # 指向板子对应的内核源码根目录 KDIR ?= /home/user/kernel/4.19-imx6ull obj-m := mt7663_usb.o mt7663_usb-objs := \ common/mt76_chip.o \ common/mt76_phy.o \ os/linux/mt76_usb.o EXTRA_CFLAGS += -DMT7663_USB -DCONFIG_MT76_LED all: $(MAKE) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(KDIR) M=$(PWD) modules clean: $(MAKE) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(KDIR) M=$(PWD) clean不要小看这个结构。obj-m := mt7663_usb.o是Kbuild识别"我要编哪些模块"的唯一入口。mt7663_usb-objs列出这个模块由哪些分散在不同子目录下的.o组成,这在驱动代码分散在common、os等多个目录时是标准做法。
3.2 最容易踩的路径硬编码问题
网上所谓的"已配置好",绝大多数指的是CROSS_COMPILE和KDIR这两个变量已经写好了。但坑也在这里,因为别人板子的内核路径和你不一样,直接make大概率报错:
Makefile: XXX: *** KDIR 路径不存在。Stop.解决办法是在makefile里把两个变量拆开,允许命令行覆盖:
KDIR ?= /home/user/kernel/4.19-imx6ull加一个?=号,表示如果没有在命令行指定就用默认值。这样你拿到手之后不需要改文件,直接执行:
make KDIR=/实际的内核源码路径建议每个动手编译的人拿到源码先做这一步,而不是闷头直接make,否则第一行输出就会让你抓狂。
3.3 针对不同内核版本的兼容层
mt7663驱动的源码包往往同时提供Makefile和Makefile.6.x,或者用ifeq ($(shell uname -r), ...)做版本判断。这是因为Linux内核在5.x之后,cfg80211、mac80211的API变动比较大。比如老版本里注册无线接口用的是cfg80211_ops的某些回调,新内核里字段名变了、参数类型变了,直接编会报struct成员冲突。
遇到这种兼容多版本的设计,我的做法是先在makefile里加一行版本打印:
$(info "编译目标内核版本 from KDIR: $(shell grep "VERSION =" $(KDIR)/Makefile | head -1)")然后先搞清楚板子内核到底是哪个大版本,再决定用哪个makefile入口。用错文件的话,编译报错根本没有头绪。
4. 实测中的完整排查链路:从报错到加载成功
4.1 第一次编译:linux/version.h失踪案
我在一个干净环境里第一次执行make,第一屏就蹦出来:
fatal error: linux/version.h: No such file or directory排查思路要清晰。这个头的来源实际上是内核源码树的include/generated/uapi/linux/version.h,它是在内核配置阶段生成的。解决办法是回到内核源码目录,执行make modules_prepare。这一步会生成version.h、autoconf.h等关键中间文件。
这里我多解释一句,很多人以为装了kernel-devel包或交叉工具链就够了,忽略modules_prepare这一步,直接编外部模块就会卡在这里。内核源码树不是一个纯静态目录,它需要先被"激活"才能用于模块编译。
4.2 第二次编译:结构体成员不兼容
version.h解决之后,紧接着来了一堆类型错误,报错集中在sk_buff、net_device相关操作。这是因为驱动源码里某些API是按旧内核写的,但板子内核已经是4.19,接口早改了。
具体报错长这样:
error: ‘struct sk_buff’ has no member named ‘next’这个场景很典型。解决方式有两个方向,一是打官方release里的patch,patch目录里的0001-mt7663-support.patch就是干这个的;二是手动改源码,把老API换成新API。对于4.19内核,我实际处理中改了两处:一处是无线数据包的发送函数签名,一处是cfg80211_ops里start_ap和stop_ap的结构体填充。
官方patch一般是用git apply打的:
cd 内核源码目录 git apply /path/to/mt7663/patch/0001-mt7663-support.patch如果patch报错,可以先git apply --check看冲突,再用patch -p1 < xxx.patch强制打。真到手动改源码这一步,一定要备份原文件,逐项对内核文档,不要凭记忆改。
4.3 编译过了,但模块加载报unknown symbol
编译终于通过,用adb push或NFS挂载把mt7663_usb.ko拷到板子上,insmod的时候却报:
mt7663_usb: Unknown symbol cfg80211_xxx (err 0)这个问题的根源不是一个符号,而是依赖关系没理清。内核模块不是独立的,它依赖内核导出的其他符号。MT7663驱动依赖cfg80211和mac80211这两个内核模块,而这两个模块在板子内核config里可能压根没编,或者编成了模块但没有事先加载。
排查方法:
grep "cfg80211" /lib/modules/$(uname -r)/modules.dep正确加载顺序是:
modprobe mac80211 modprobe cfg80211 insmod mt7663_usb.ko很多人在这一步反复卡死,以为是驱动编译错了,其实是内核config里CONFIG_CFG80211、CONFIG_MAC80211没开成模块。需要在板子内核的defconfig里确认:
CONFIG_CFG80211=m CONFIG_MAC80211=m重新编译内核并烧录后,再按顺序加载模块,链路就通了。
4.4 固定IP和无线网卡名称的不确定性
还有一个很容易被忽略的问题:驱动加载成功后,无线网卡接口名不一定是wlan0。有些官方驱动会把它注册成ra0,有些是wlan0。这取决于驱动里struct wireless_dev的注册名和内核的命名规则。
在init脚本里不要写死网卡名,建议用udev规则固定:
SUBSYSTEM=="net", ACTION=="add", DRIVERS=="mt7663_usb", NAME="wlan0"或者写脚本动态获取:
WLAN_IFACE=$(ls /sys/class/net | grep -E 'wlan|ra' | head -n1) ifconfig $WLAN_IFACE up我当时就是没注意这茬,把所有脚本都按wlan0写了,结果设备上显示的是ra0,排查了大半天。这类接口名不一致的问题,在嵌入式Linux里非常常见。
5. 部署验证:从firmware到USB枚举的全链路确认
5.1 固件文件放哪、怎么放
前面说过,mt7663是FullMAC方案,固件必须被正确加载,无线接口才会工作。固件文件默认查找路径是/lib/firmware/mediatek/,代码里写死的话不能乱改,建议直接按默认路径放:
mkdir -p /lib/firmware/mediatek cp mt7663_patch_e2_hdr.bin /lib/firmware/mediatek/ cp mt7663_rom_patch.bin /lib/firmware/mediatek/ cp mt7663_wifi_soc.bin /lib/firmware/mediatek/固件放好之后再加载驱动。判断固件是否被正确读取,看dmesg输出:
mt7663_usb 1-1:1.0: Firmware Version: 6_0_1_01 mt7663_usb 1-1:1.0: Firmware download success如果出现Firmware download failed或者patch file not found,几乎可以断定是固件路径不对或者驱动和固件版本不匹配。注意,有的源码包里固件文件是分散放的,需要按源码目录里的install脚本去拷贝,不要只拷一个bin文件。
5.2 用dmesg和usb抓包确认硬件枚举
驱动加载前,先插上USB WiFi模块,查看usb设备是否识别到:
lsusb正常会看到类似:
Bus 001 Device 003: ID 0e8d:7663 MediaTek Inc. MT7663如果没有,先确认USB Host口能不能枚举其他设备,排除硬件供电问题。MT7663模块的供电电流比普通键鼠大,很多USB口的供电不足也会导致枚举失败。我实际测过,部分模块在弱供电下能枚举但加载驱动时会掉线,最后加了一路独立的5V供电才稳定。
如果枚举到了但驱动报错,可以用USB抓包工具看USB控制传输的返回包。用Linux主机做USB抓包不需要额外硬件,加载usbmon模块即可:
modprobe usbmon cat /sys/kernel/debug/usb/usbmon/1u抓包能看到URB提交和撤销的时机,重点观察加载驱动后固件下载阶段是否有URB -ESHUTDOWN之类的错误。这个信号基本指向USB通信链路不稳定,多半是供电或USB信号质量问题,而不是驱动问题。
5.3 无线接口能否扫描到AP
驱动加载、固件下载都成功之后,最后一步验证无线功能:
ip link set wlan0 up iw dev wlan0 scan | head -30如果可以scan出周围的AP,说明mt7663 USB转WiFi的收发链路已经通了。再走一步用wpa_supplicant接入测试网络,如果260Mbps的连接速率能打满,整个编译和部署流程就算是彻底闭环了。
6. 后续扩展方向
驱动调通之后,这个USB WiFi口就可以纳入整个嵌入式系统做业务了。一是接Qt应用做无线配置界面,配合Qt 5.12.10交叉编译环境,在触摸屏上做SSID扫描和密码输入,这块我已经在另一个项目里跑通了,后续有机会单独写一篇;二是配合RK3576这类性能更强的ARM平台,USB WiFi可以作为热点模式使用,让设备反向广播一个AP,手机直接连上来做本地调试。不过这需要驱动支持AP模式,mt7663固件是支持的,但需要把NL80211的接口模式配置改一下,涉及的东西又不一样了。
我个人的习惯是,每次拿到一颗新芯片的驱动源码,先把编译环境、内核源码树、固件依赖这三个基础问题解决,再碰代码逻辑。mt7663这套流程走完,后面编其他MTK芯片的驱动基本都是同一套打法:KDIR指对、config开对、firmware放对,剩下的就是API level的排雷。如果只是编译驱动这一个目标,这篇文章应该能帮你省下至少两天的折腾时间。
本文还有配套的精品资源,点击获取