1. 项目背景与核心挑战:当RK3399遇上EC20
大家好,我是老张,在嵌入式这行摸爬滚打十几年了,从功能机时代玩基带芯片到现在折腾各种智能硬件,踩过的坑比走过的路还多。今天想和大家聊聊一个非常具体、但又让很多开发者头疼的问题:在RK3399这块性能强悍的平台上,为Android 6.0系统,把EC20 4G模块通过PCIe接口给跑起来,并且打通从驱动到RIL(无线接口层)的整个链路。
你可能要问,RK3399不是有原生USB接口吗,为啥要走PCIe?这其实是个很实际的工程选择。RK3399的PCIe接口带宽高、延迟低,对于需要稳定高速数据吞吐的4G模块来说,能提供比传统USB转接更可靠的物理连接。尤其是当你做的是工业平板、车载中控、智能零售终端这类对网络稳定性要求极高的设备时,PCIe的优势就体现出来了。EC20模块本身支持PCIe形态,直接对接能省去一个USB转换芯片,既节约成本又减少了一个潜在的故障点。
但这条路走起来并不轻松。Android 6.0虽然经典稳定,但其内核版本(比如常用的4.4)对较新的PCIe网卡类设备支持并不完善,特别是涉及到4G模块这种“上网卡”+“调制解调器”二合一的复杂设备。你需要面对的是一连串的挑战:内核需要正确的驱动来识别这个“新网卡”;系统需要配置好PPP拨号来建立数据连接;最后,Android特有的RIL框架必须正确对接,让上层APP(比如拨号、流量监控)能感知和控制4G网络。这就像给一个新房接水电,不仅要管道通(驱动),还要水龙头能出水(PPP),最后还得确保屋里的电器(APP)知道怎么用水(RIL)。我刚开始做的时候,也是被各种日志和错误码折腾得够呛,不过趟平了之后发现,只要思路清晰,一步步来,完全能搞定。
2. 硬件识别与驱动移植:让系统“看见”你的模块
万事开头难,第一步就是让Linux内核认识插在PCIe槽上的这位“新朋友”。EC20模块通过PCIe暴露给主机的,本质上是一个USB复合设备。所以,驱动移植的核心是USB网络驱动,而不是直接的PCIe驱动。PCIe在这里只是物理通道,内核的USB子系统会通过它来枚举设备。
2.1 确认硬件身份:Vendor ID与Product ID
动手改代码前,务必先确认你手里的EC20具体型号。EC20有多个变种,常见的是EC20-C和EC20-CE,它们的USB识别码不同。接上模块后,立刻用adb shell连上设备,或者通过串口查看内核日志:
dmesg | grep -i "usb.*new"或者更精确地:
cat /proc/kmsg | grep -E "idVendor|idProduct"你期待看到类似这样的输出:
[ 123.456789] usb 1-1.1: New USB device found, idVendor=2c7c, idProduct=0125这里,idVendor=2c7c,idProduct=0125就对应EC20-CE。如果是05c6和9215,那就是EC20-C。这个信息是后续所有驱动配置的基石,千万不能搞错。我见过有兄弟折腾一星期,最后发现是模块型号和驱动代码对不上,白白浪费了时间。
2.2 内核驱动配置:三选一的策略
EC20作为高通方案的模块,在内核层面通常有三种驱动模式可选,它们决定了系统以何种方式与模块的“上网”功能交互:
- USB Serial (CDC ACM):这是最基础的模式。驱动会把模块虚拟成几个串口设备(
/dev/ttyUSB0~ttyUSB4或ttyACM0~ttyACM4)。每个串口有固定分工,比如ttyUSB2用于发AT命令,ttyUSB3用于PPP拨号。这种模式通用性强,但性能和管理功能较弱。 - GobiNet:这是高通提供的传统内核态驱动。移植后,它会创建一个网络接口(如
eth1)和一个QMI控制通道设备(/dev/qcqmi0)。数据直接走网络接口,效率高;控制命令(如网络注册、信号查询)走QMI通道。但需要手动将供应商提供的GobiNet内核源码集成到你的内核树中并编译。 - QMI WWAN (cdc-wdm):这是目前更推荐、也更现代的方式。它利用Linux内核自带的
cdc-wdm和qmi_wwan驱动。你的工作主要是确保内核配置中启用了这些驱动,并在驱动中添加你模块的USB识别码。成功后,系统会创建wwan0这样的网络接口和/dev/cdc-wdm0控制节点。Android 6.0默认内核通常已包含此驱动,因此工作量最小。
我的选择与建议:对于RK3399 + Android 6.0这个组合,我强烈建议使用QMI WWAN方案。原因很简单:省事、稳定、社区支持好。你不需要维护额外内核代码,只需修改配置文件。具体操作是进入内核配置菜单:
make ARCH=arm64 menuconfig确保以下选项被启用(标为*):
Device Drivers ---> Network device support ---> USB Network Adapters ---> <*> Multi-purpose USB Networking Framework <*> CDC Ethernet support (smart devices such as cable modems) <*> QMI WWAN driver for Qualcomm MSM based devices USB support ---> <*> USB Modem (CDC ACM) support然后,找到drivers/net/usb/qmi_wwan.c文件,在qmi_wwan_pre_reset函数附近的设备列表里,添加你的EC20的USB PID/VID。例如,对于EC20-CE,你需要找到类似{QMI_GOBI_DEVICE(0x2c7c, 0x0125)}的宏,如果没有,就仿照格式添加一行。添加后重新编译内核并烧录。
2.3 驱动验证:检查设备节点
烧录新内核后,重启设备,再次查看日志,确认模块被正确识别。然后进入系统,检查设备节点是否生成:
ls -l /dev/ | grep -E "ttyUSB|cdc-wdm|wwan"如果QMI WWAN驱动生效,你应该能看到/dev/cdc-wdm0和/sys/class/net/wwan0。看到这些,恭喜你,驱动层的大门已经敲开了。如果没看到,别慌,回头仔细核对内核日志里关于qmi_wwan的加载和探测信息,最常见的问题就是VID/PID没添加对,或者内核配置选项没真正编译进去。
3. 网络连接建立:PPP拨号与APN配置
驱动让系统认识了模块,接下来就要让模块“上网”。对于4G模块,建立数据连接通常需要经过PPP(Point-to-Point Protocol)拨号。这个过程就像是给你的手机卡“激活”数据业务。
3.1 PPP拨号配置
在Android系统中,PPP拨号功能一般由rild(RIL守护进程)来管理,但底层依赖pppd(PPP守护进程)和chat(拨号脚本工具)这两个程序。你需要确保系统中有这些工具。通常,Android源码的external/ppp/目录下就有。在RK3399的SDK里,可能需要检查是否已编译。
关键的配置在于拨号脚本。这个脚本告诉pppd使用哪个串口、以什么AT命令序列去激活模块的数据承载。对于EC20,使用QMI WWAN模式时,通常不再使用传统的ttyUSB3进行PPP拨号,而是由QMI协议通过cdc-wdm0设备来管理数据连接的建立和释放,这更高效。但为了兼容性和理解流程,我们了解一下传统方式。
传统PPP脚本(如/etc/ppp/peers/quectel-chat)可能包含:
ABORT "BUSY" ABORT "ERROR" ABORT "NO ANSWER" TIMEOUT 30 "" "AT" OK "AT+CGDCONT=1,\"IP\",\"你的APN\"" OK "ATD*99#" CONNECT ""这个脚本的意思是:发送AT命令,设置APN为“你的APN”,然后拨打*99#这个数据呼叫号码。但在QMI模式下,APN信息通常是通过RIL层下发的。
3.2 APN信息配置
APN(接入点名称)是移动网络用来识别你数据业务类型的“钥匙”。国内三大运营商的常用APN如下:
| 运营商 | 4G APN名称 | 说明 |
|---|---|---|
| 中国移动 | cmnet | 最常用的移动互联网接入点 |
| 中国联通 | 3gnet | 联通4G/3G通用接入点 |
| 中国电信 | ctnet | 电信4G互联网接入点 |
这些信息需要正确配置。在Android系统中,APN信息通常以数据库形式存储。对于嵌入式设备,我们可以在系统编译时预置。找到你项目中的APN配置文件,路径可能类似device/rockchip/rk3399/overlay/frameworks/base/core/res/res/xml/apns_config.xml。你需要在其中添加正确的APN条目。例如,添加中国移动的:
<apn carrier="China Mobile Internet" mcc="460" mnc="00" apn="cmnet" type="default,supl" protocol="IPV4V6" roaming_protocol="IPV4V6"/>mcc(国家码)和mnc(运营商网络码)需要根据你的SIM卡来设置。编译后,这个APN列表就会被集成到系统中。当模块注册到网络后,RIL会根据SIM卡信息自动选择匹配的APN进行数据连接。
4. RIL层集成:连接Android框架与硬件
驱动和网络通了,但Android上层应用还无法使用4G网络。这就需要RIL出场了。RIL是Android架构中承上启下的关键层,它把Java框架的电话服务请求(如“打开移动数据”、“获取信号强度”)翻译成底层模块能懂的AT命令或QMI消息,反之亦然。
4.1 获取与集成Vendor RIL
Android的RIL分为两部分:通用框架(hardware/ril/rild)和厂商实现(vendor ril)。高通或模块厂商会提供针对特定模块的vendor ril库源码。对于EC20,你需要从移远通信获取名为Quectel_Android_RIL_xxx的源码包。这里有个大坑:不同Android版本对应的RIL源码包不同!为Android 6.0准备的包,名字可能类似Quectel_Android_RIL_SR01A41V17。用错了版本,编译会出一堆语法错误,根本过不了。
拿到正确源码后,将其替换到Android源码树的hardware/ril/reference-ril/目录下。注意备份原来的文件。然后,需要修改编译配置,确保编译系统能把它编进去。通常需要检查hardware/ril/reference-ril/Android.mk文件,确保其中的LOCAL_MODULE名字(例如libreference-ril)是唯一的,不会和系统其他库冲突。
4.2 配置RIL守护进程(rild)
替换库文件后,要告诉系统使用我们这个新的库。这需要通过修改初始化脚本来实现。传统做法是修改init.rc文件,在其中定义ril-daemon服务。你需要在你的设备特定目录下找到init.rc,例如device/rockchip/rk3399/init.rc,添加或修改如下服务定义:
service ril-daemon /system/bin/rild -l /system/lib64/libreference-ril.so -- -d /dev/ttyUSB2 class main socket rild stream 660 root radio socket rild-debug stream 666 radio system user root group radio cache inet misc audio sdcard_rw log这里有几个关键点:
-l /system/lib64/libreference-ril.so:指定我们编译出来的vendor ril库的路径。注意:RK3399是64位平台,库通常在/system/lib64/下。如果是32位系统,则在/system/lib/下。这里错了,RIL完全不起作用。-d /dev/ttyUSB2:指定用于AT命令通信的串口设备节点。在QMI WWAN模式下,这个参数可能不是必须的,或者需要改为-d /dev/cdc-wdm0,具体取决于你的vendor ril实现。务必参考移远提供的文档。user和group:确保rild进程有足够的权限访问串口和网络设备。上面配置的radio组通常拥有这些权限。
4.3 解决编译冲突与路径覆盖
编译时你可能会遇到一个经典错误:
build/core/base_rules.mk:157: *** hardware/ril/reference-ril: MODULE.TARGET.EXECUTABLES.chat already defined by external/ppp/chat。这是因为移远的RIL源码包里自带了一个chat工具,和Android源码external/ppp/下的chat重名了。最简单的解决办法是,在编译前,删除移远源码包里的chat目录,或者修改其Android.mk中的模块名。我通常直接rm -rf hardware/ril/reference-ril/chat,一劳永逸。
另一个RK平台特有的坑是路径覆盖。即便你在init.rc里配好了库路径,RK的构建系统可能会在device.mk或system.prop里再次覆盖。你需要检查以下文件:
device/rockchip/rk3399/device.mkdevice/rockchip/rk3399/system.propdevice/rockchip/common/device.mk
搜索rild.libpath这个属性。例如,你可能会发现有一行:
rild.libpath=/system/lib64/libril-rk29-dataonly.so这行代码会用RK自带的RIL库覆盖你的设置。你需要果断地注释掉或删除这一行,让系统使用我们指定的库。
5. 调试与问题排查:从日志中寻找答案
集成工作完成后,最紧张的时刻就是上电测试。不出意外的话,这时候应该会出点“意外”。别担心,通过系统日志我们能定位大部分问题。
5.1 抓取专用日志
Android日志分多个缓冲区,RIL相关的日志主要在radio缓冲区。
adb logcat -b radio -v time这个命令会实时输出所有RIL和调制解调器交互的日志,是调试的“第一现场”。另外,内核日志也至关重要:
adb shell dmesg | tail -100或者实时监控:
adb shell cat /proc/kmsg5.2 关键检查点与常见问题
当4G网络没有如预期般出现时,按照以下清单逐项排查,能帮你快速定位:
RIL进程活着吗?
adb shell getprop init.svc.ril-daemon输出必须是
running。如果是stopped或restarting,说明服务启动失败,去查logcat和dmesg看崩溃原因。系统加载了谁的RIL库?
adb shell getprop gsm.version.ril-impl这是最重要的属性之一。如果输出是
Quectel_Android_RIL_SR...,恭喜,你的库被加载了。如果输出是RIL_RK_DATA_V3.6...,说明被RK的库覆盖了,回去检查rild.libpath的覆盖问题。如果输出为空,可能是库路径错误,或者库文件本身与平台(32/64位)不兼容。SELinux在捣乱吗?Android 6.0的SELinux可能已经处于强制模式(Enforcing),这会阻止
rild进程访问设备节点。adb shell getenforce如果返回
Enforcing,可以先将其设为宽容模式测试:adb shell setenforce 0如果网络随后恢复正常,说明你需要为
rild和你的设备节点(如/dev/cdc-wdm0,/dev/ttyUSB*)添加正确的SELinux策略。这涉及到修改*.te策略文件,是另一个深水区,但为了产品化,必须解决。模块注册上网络了吗?在
radio日志中搜索REGISTERED、ATTACHED等关键词。也可以使用AT命令手动查询(如果AT串口可用):adb shell echo -e "AT+CREG?\r\n" > /dev/ttyUSB2 cat /dev/ttyUSB2查看返回码,确认网络注册状态。
数据连接建立了吗?查看
radio日志中是否有SETUP_DATA_CALL的成功响应。或者使用ifconfig命令查看wwan0接口是否获得了IP地址。adb shell ifconfig wwan0
我印象最深的一次调试,是ril-daemon一直重启。查遍日志发现是权限问题,/dev/ttyUSB2的设备节点虽然存在,但rild进程所属的radio用户组没有读写权限。最后是在ueventd.rc文件中添加规则才解决的:
/dev/ttyUSB0 0660 radio radio /dev/ttyUSB1 0660 radio radio /dev/ttyUSB2 0660 radio radio /dev/ttyUSB3 0660 radio radio这些规则确保了设备节点在创建时就被赋予正确的权限。嵌入式开发就是这样,很多时候问题不在核心逻辑,而在这些细微的配置和权限上。把这些问题都解决了,看着设备右上角出现4G信号图标,并且能够真正上网冲浪时,那种成就感,就是驱动我们这行不断钻研下去的动力。希望我的这些经验,能帮你少走些弯路。