目录
[一、EC20-CE 模块简介](#一、EC20-CE 模块简介)
[二、Mini PCIe 接口深度解析](#二、Mini PCIe 接口深度解析)
[1. 什么是 Mini PCIe](#1. 什么是 Mini PCIe)
[2. Mini PCIe 的信号定义](#2. Mini PCIe 的信号定义)
[3. Mini PCIe 的三种工作模式](#3. Mini PCIe 的三种工作模式)
[4. 为什么 EC20-CE 走 USB 通道](#4. 为什么 EC20-CE 走 USB 通道)
[1. 电源管理设计](#1. 电源管理设计)
[2. Mini PCIe 的 USB 数据线连接](#2. Mini PCIe 的 USB 数据线连接)
[3. 4G 模块节点](#3. 4G 模块节点)
[4. USB Host 节点使能](#4. USB Host 节点使能)
[1. USB 设备识别](#1. USB 设备识别)
[2. 串口驱动配置](#2. 串口驱动配置)
[3. 替换 QMI_WWAN 驱动](#3. 替换 QMI_WWAN 驱动)
[4. 零包处理](#4. 零包处理)
[5 防止 option 驱动抢占网络接口](#5 防止 option 驱动抢占网络接口)
[五、Android RIL 服务配置](#五、Android RIL 服务配置)
[2. FCM 兼容性矩阵配置](#2. FCM 兼容性矩阵配置)
[3. RIL 守护进程配置](#3. RIL 守护进程配置)
[4. SELinux 配置](#4. SELinux 配置)
[1. 检查设备节点](#1. 检查设备节点)
[2. 检查 RIL 服务状态](#2. 检查 RIL 服务状态)
[3. Radio Log 分析](#3. Radio Log 分析)
前言
最近在 RK3568 Android 13 平台上适配移远 EC20-CE 模块时,遇到了不少细节问题:移远一键脚本能用但不全、FCM 版本不匹配导致 RIL 服务无响应、libril.so 文件冲突编译报错...... 花了不少时间摸索,总算把所有问题都打通了。
本文从硬件原理 、设备树配置 、内核驱动适配 到 Android RIL 服务配置,完整复盘这次移植过程,希望能帮到正在做类似工作的朋友。
一、EC20-CE 模块简介
EC20-CE 是移远通信专为 M2M 和 IoT 应用设计的 LTE Cat 4 模块:
-
网络制式:支持 LTE FDD/TDD、WCDMA、TD-SCDMA、CDMA、GSM
-
速率:下行 150Mbps,上行 50Mbps
-
接口:LCC 封装,支持 USB 2.0、UART、GPIO、(U)SIM
-
特色功能:多星座 GNSS(可选)、DFOTA 远程升级
本项目中,模块通过 Mini PCIe 接口 与 RK3568 主板连接,采用 USB 通信方式,不涉及 GPS 功能。
二、Mini PCIe 接口深度解析
1. 什么是 Mini PCIe
Mini PCIe(Mini Peripheral Component Interconnect Express)是 PCI-SIG 组织为笔记本等紧凑型设备制定的小型化扩展接口标准。它主要用于连接 Wi-Fi、蓝牙、4G/5G 蜂窝模块、SSD 等内部扩展卡。
物理特性:
-
52-Pin 金手指插槽
-
尺寸约为 30mm × 50.95mm(全高)或 30mm × 26.8mm(半高)
-
供电电压主要为 3.3V,部分引脚提供 1.5V
2. Mini PCIe 的信号定义
标准的 Mini PCIe 接口包含以下关键信号组:
| 信号组 | 引脚 | 说明 |
|---|---|---|
| PCIe 数据通道 | 引脚 23/25(PETp/n)、31/33(PERp/n) | 一对发送、一对接收差分信号,提供 2.5Gbps 带宽 |
| USB 数据通道 | 引脚 36(USB_D+)、38(USB_D-) | 全速/高速 USB 2.0 接口 |
| 电源 | 引脚 2(3.3V)、24(3.3V)、52(3.3V)、引脚 50(1.5V) | 主供电和辅助供电 |
| 控制信号 | 引脚 20(W_DISABLE#)、引脚 22(PERST#)、引脚 51(WAKE#) | 无线禁用、复位、唤醒 |
3. Mini PCIe 的三种工作模式
在实际的 RK3568 开发板设计中,Mini PCIe 插槽可能存在以下三种模式:
模式一:标准 PCIe 通道
-
由 RK3568 内置的 Synopsys DesignWare PCIe 控制器驱动
-
提供 PCIe Gen2 x1 lane(约 500MB/s 带宽)
-
适用于需高速数据传输的 Wi-Fi 6/5G 模块
模式二:USB 通道(本文场景)
-
物理上是 Mini PCIe 形状,但实际走的是 USB 总线
-
使用引脚 36(USB_D+)和 38(USB_D-)传输数据
-
RK3568 的 USB 2.0 Host 控制器直接驱动
-
适用于 EC20-CE 等仅支持 USB 通信的 4G 模块
模式三:共享 mSATA 功能
-
Mini PCIe 与 mSATA 接口物理定义基本一致(均基于 52-Pin)
-
部分主板支持将接口配置为 mSATA 硬盘接口
-
不能同时作为 Mini PCIe 和 mSATA 使用
4. 为什么 EC20-CE 走 USB 通道
EC20-CE 模块的 USB 接口是其主通信接口,承担以下功能:
-
AT 指令通道 :通过 USB 虚拟出的
ttyUSB*串口设备发送 AT 指令 -
数据通路 :通过 USB 协议栈实现 QMI(Qualcomm MSM Interface)通信,最终映射为
wwan0网络接口 -
固件升级:通过 USB 进行 DFOTA 升级
对于 RK3568 平台,使用 Mini PCIe 的 USB 通道有以下优势:
-
简化硬件设计:无需配置复杂的 PCIe 控制器和时钟
-
驱动成熟:Linux 内核自带 USB 串口和 QMI 驱动
-
兼容性好:移远官方主要基于 USB 接口提供驱动支持
三、硬件原理与设备树配置
1. 电源管理设计
EC20 供电电压范围为 3.3V~4.3V,典型值 3.8V。这里使用 DCDC 将 12V 转为 3.85V 供电,需注意:
-
模块瞬时发射功耗较高,电源需提供 2A 以上电流裕量
-
电源使能时序需早于 USB 枚举

在设备树中定义电源节点:
cpp
vcc3v3_miniPcie: vcc3v3-miniPcie-regulator {
compatible = "regulator-fixed";
regulator-name = "vcc3v3_miniPcie";
regulator-always-on;
regulator-boot-on;
regulator-min-microvolt = <3300000>;
regulator-max-microvolt = <3300000>;
enable-active-high;
gpio = <&gpio0 RK_PC6 GPIO_ACTIVE_HIGH>;
startup-delay-us = <5000>;
vin-supply = <&dc_12v>;
pinctrl-names = "default";
pinctrl-0 = <&pwr_en_4g>;
};
&pinctrl {
net-4g{
pwr_en_4g: pwr_en_4g {
rockchip,pins = <0 RK_PC6 RK_FUNC_GPIO &pcfg_pull_none>;
};
};
};
2. Mini PCIe 的 USB 数据线连接
在原理图中,Mini PCIe 插座的 引脚 36 和 引脚 38 分别连接到 RK3568 USB 2.0 Host 控制器的 DM/DP 信号线上。这种设计使得模块被系统识别为一个 USB 设备。
3. 4G 模块节点
rk-modem 节点用于上层获取复位等 GPIO 信息:
cpp
rk_modem: rk-modem {
compatible="4g-modem-platdata";
//4G,power-gpio = <&gpio0 RK_PB0 GPIO_ACTIVE_HIGH>;
4G,reset-gpio = <&gpio3 RK_PC1 GPIO_ACTIVE_HIGH>;
status = "okay";
};
4. USB Host 节点使能
确保 EC20 连接的 USB 端口处于 Host 模式:
XML
&usb_host0_ehci {
status = "okay";
};
&usb_host0_ohci {
status = "okay";
};
四、内核驱动适配
1. USB 设备识别
由于 Mini PCIe 走的是 USB 通道,模块上电后会被系统识别为 USB 设备。通过 lsusb 可看到 EC20 的 VID:PID(移远 USB VID 为 0x2C7C)。
2. 串口驱动配置
RK3568 内核(4.19/5.10)已自带移远 Option 驱动,无需额外添加 VID/PID。确保以下内核选项开启:
cpp
CONFIG_USB_SERIAL=y
CONFIG_USB_SERIAL_OPTION=y
CONFIG_USB_ACM=y
上电后,系统会枚举出 ttyUSB0~ttyUSB3 等串口节点。
还需要修改BoardConfig.mk,确保编译系统里打开4G,如下值为true。
XML
device/rockchip/common/BoardConfig.mk
BOARD_HAS_RK_4G_MODEM ?= true
BOARD_HAVE_DONGLE ?= true
3. 替换 QMI_WWAN 驱动
这是最关键的一步。内核自带的 qmi_wwan.c 无法满足 EC20 需求,必须替换为移远提供的 qmi_wwan_q.c:
-
将
qmi_wwan_q.c放入kernel/drivers/net/usb/ -
修改该目录下
Makefile:
XML
kernel-5.10/drivers/net/usb/Makefile
//需要把qmi_wwan_q放到qmi_wwan前面
obj-$(CONFIG_USB_NET_QMI_WWAN) += qmi_wwan_q.o
obj-$(CONFIG_USB_NET_QMI_WWAN) += qmi_wwan.o
此驱动会将 USB 数据包转换为网络数据包,生成 wwan0 网络节点,这是 4G 数据通路的关键
4. 零包处理
在 usb_wwan.c 中添加移远 PID 判断,启用 URB_ZERO_PACKET 标志:
cpp
kernel-5.10/drivers/usb/serial/usb_wwan.c
static struct urb *usb_wwan_setup_urb(struct usb_serial_port *port,
int endpoint,
int dir, void *ctx, char *buf, int len,
void (*callback) (struct urb *))
{
struct usb_serial *serial = port->serial;
struct usb_wwan_intf_private *intfdata = usb_get_serial_data(serial);
struct urb *urb;
urb = usb_alloc_urb(0, GFP_KERNEL); /* No ISO */
if (!urb)
return NULL;
usb_fill_bulk_urb(urb, serial->dev,
usb_sndbulkpipe(serial->dev, endpoint) | dir,
buf, len, callback, ctx);
if (intfdata->use_zlp && dir == USB_DIR_OUT)
urb->transfer_flags |= URB_ZERO_PACKET;
//====================新增=====================
if (dir == USB_DIR_OUT) {
struct usb_device_descriptor *desc = &serial->dev->descriptor;
if (desc->idVendor == cpu_to_le16(0x2C7C))
urb->transfer_flags |= URB_ZERO_PACKET;
}
//=====================
return urb;
}
5 防止 option 驱动抢占网络接口
EC20-CE 模块是一个复合 USB 设备,内部包含多个功能接口:串口接口(/dev/ttyUSB*)用于 AT 指令通信,网络接口(wwan0)用于数据传输,此外还有诊断、音频等其他接口。
Linux 内核的 option 驱动可能会"过于积极"地绑定所有看起来像串口的 USB 接口。如果不加干预,option 驱动可能会错误地绑定到本该由 qmi_wwan_q 管理的网络接口 ,导致 wwan0 无法正常创建,RIL 层无法通过 QMI 协议进行数据通信。
因此,需要在 option.c 的 option_probe() 函数中添加过滤逻辑,让 option 驱动主动放弃对非串口接口的绑定:
cpp
kernel-5.10/drivers/usb/serial/option.c
static struct usb_serial_driver option_1port_device = {
.driver = {
.owner = THIS_MODULE,
.name = "option1",
},
.description = "GSM modem (1-port)",
.id_table = option_ids,
.num_ports = 1,
.probe = option_probe,
.open = usb_wwan_open,
.close = usb_wwan_close,
.dtr_rts = usb_wwan_dtr_rts,
.write = usb_wwan_write,
.write_room = usb_wwan_write_room,
.chars_in_buffer = usb_wwan_chars_in_buffer,
.tiocmget = usb_wwan_tiocmget,
.tiocmset = usb_wwan_tiocmset,
.get_serial = usb_wwan_get_serial_info,
.set_serial = usb_wwan_set_serial_info,
.attach = option_attach,
.release = option_release,
.port_probe = usb_wwan_port_probe,
.port_remove = usb_wwan_port_remove,
.read_int_callback = option_instat_callback,
#ifdef CONFIG_PM
.suspend = usb_wwan_suspend,
//------------新增--------------
.reset_resume = usb_wwan_resume,
//------------------------------
.resume = usb_wwan_resume,
#endif
};
static int option_probe(struct usb_serial *serial,
const struct usb_device_id *id)
{
struct usb_interface_descriptor *iface_desc =
&serial->interface->cur_altsetting->desc;
unsigned long device_flags = id->driver_info;
/* Never bind to the CD-Rom emulation interface */
if (iface_desc->bInterfaceClass == USB_CLASS_MASS_STORAGE)
return -ENODEV;
/*
* Don't bind reserved interfaces (like network ones) which often have
* the same class/subclass/protocol as the serial interfaces. Look at
* the Windows driver .INF files for reserved interface numbers.
*/
if (iface_is_reserved(device_flags, iface_desc->bInterfaceNumber))
return -ENODEV;
/*
* Allow matching on bNumEndpoints for devices whose interface numbers
* can change (e.g. Quectel EP06).
*/
if (device_flags & NUMEP2 && iface_desc->bNumEndpoints != 2)
return -ENODEV;
//=========================新增===========================
if (serial->dev->descriptor.idVendor == cpu_to_le16(0x2C7C)) {
__u16 idProduct = le16_to_cpu(serial->dev->descriptor.idProduct);
struct usb_interface_descriptor *intf = &serial->interface->cur_altsetting->desc;
if (intf->bInterfaceClass != 0xFF || intf->bInterfaceSubClass == 0x42) {
return -ENODEV;
}
if ((idProduct&0xF000) == 0x0000) {
if (intf->bInterfaceNumber == 4 && intf->bNumEndpoints == 3
&& intf->bInterfaceSubClass == 0xFF && intf->bInterfaceProtocol == 0xFF)
return -ENODEV;
}
}
//=======================================================
/* Store the device flags so we can use them during attach. */
usb_set_serial_data(serial, (void *)device_flags);
return 0;
}
五、Android RIL 服务配置
1.移远一键移植脚本配置
移远提供了一键移植脚本配置,具体使用是:将quectel目录放到vendor下,将quectel.sh放在源码顶层目录运行。
经过分析它会删除原生 RIL 配置,从 .mk、.rc、.xml 中清理旧 Radio/GNSS 服务;修改系统源码,调整 hardware/ril/rild/rild.c、system/core/init/devices.cpp以适配移远模组;添加编译兼容性标志,绕过构建系统的严格检查;包含移远 RIL/GNSS 编译配置,将移远库集成到 Android 编译系统中;启用移动数据功能,修改 overlay 配置,使系统支持蜂窝网络;
执行quectel.sh脚本后,它能够将RIL.mk编译配置文件被包含到你的编译文件去,也就是device/rockchip/rk356x/rk3568_t/rk3568_t.mk。
这里我把在移远官网下载的驱动包和库,以及移远的一键移植资源都已打包绑定本篇文章,文章头部链接可下载。
2. FCM 兼容性矩阵配置
移远一键脚本默认只配置了低版本 IRadio。Android 13 上层若感知不到高版本 Radio 接口,RIL 服务无法正常通信。一般在编译阶段就会报错,因此需要修改FCM7的兼容规则,兼容低版本,在 compatibility_matrix.7.xml 中添加 IRadio 1.5/1.6 支持:
XML
hardware/interfaces/compatibility_matrices/compatibility_matrix.7.xml
<hal format="hidl" optional="true">
<name>android.hardware.radio</name>
<version>1.6</version>
<version>1.5</version>
<version>1.4</version>
<interface>
<name>IRadio</name>
<instance>slot1</instance>
<instance>slot2</instance>
<instance>slot3</instance>
</interface>
</hal>
3. RIL 守护进程配置
需要修改 rild.rc,指定加载 libreference-ril.so,删除之前的/vendor/bin/hw/rild:
bash
hardware/ril/rild/rild.rc
service ril-daemon /vendor/bin/hw/rild -l /vendor/lib64/libquectel-ril.so
class main
user radio
group radio cache inet misc audio sdcard_rw log
capabilities BLOCK_SUSPEND NET_ADMIN NET_RAW
4. SELinux 配置
配置RIL的SELinux规则
bash
device/rockchip/common/rootdir/ueventd.rockchip.rc
#quectel port
/dev/ttyUSB* 0660 radio radio
/dev/ttyACM* 0660 radio radio
/dev/cdc-wdm* 0660 radio radio
/dev/qcqmi* 0660 radio radio
/dev/cdc-acm* 0660 radio radio
#PCIe
/dev/mhi_DUN 0660 radio radio
/dev/mhi_DIAG 0660 radio radio
/dev/mhi_BHI 0660 radio radio
/dev/mhi_LOOPBACK 0660 radio radio
/dev/mhi_QMI0 0660 radio radio
bash
device/rockchip/common/sepolicy/private/file_contexts
#quectel port
/dev/ttyUSB[0-9] u:object_r:radio_device:s0
/dev/ttyACM[0-9] u:object_r:radio_device:s0
/dev/cdc-wdm[0-9] u:object_r:radio_device:s0
/dev/qcqmi[0-9] u:object_r:radio_device:s0
/vendor/bin/hw/rild u:object_r:rild_exec:s0
/dev/socket/rildOemHook u:object_r:rild_socket:s0
#pcie
/dev/mhi_DUN u:object_r:radio_device:s0
/dev/mhi_DIAG u:object_r:radio_device:s0
/dev/mhi_BHI u:object_r:radio_device:s0
/dev/mhi_LOOPBACK u:object_r:radio_device:s0
/dev/mhi_QMI0 u:object_r:radio_device:s0
bash
system/sepolicy/vendor/rild.te
allow rild self:packet_socket { create bind write read };
5.设置→网络和互联网里添加"移动网络"选项
XML
device/rockchip/rk356x/overlay/frameworks/base/core/res/res/values/config.xml
<string-array translatable="false" name="networkAttributes">
<item>"wifi,1,1,2,-1,true"</item>
<!-- -------------新增------------------->
<item>"mobile,0,0,0,-1,true"</item>
<item>"mobile_mms,2,0,2,60000,false"</item>
<item>"mobile_supl,3,0,2,60000,true"</item>
<item>"mobile_dun,4,0,2,60000,true"</item>
<item>"mobile_hipri,5,0,3,60000,true"</item>
<item>"mobile_fota,10,0,2,60000,true"</item>
<item>"mobile_ims,11,0,2,60000,true"</item>
<item>"mobile_cbs,12,0,2,60000,true"</item>
<!-- ----------------------------------->
<item>"bluetooth,7,7,0,-1,true"</item>
<item>"ethernet,9,9,9,-1,true"</item>
</string-array>
六、调试与验证
1. 检查设备节点
XML
ls -l /dev/ttyUSB*
应显示 ttyUSB0~ttyUSB3 等串口节点。
bash
ifconfig -a
应显示 wwan0 网络接口。
2. 检查 RIL 服务状态
bash
getprop init.svc.ril-daemon
应返回 running。
3. Radio Log 分析
bash
logcat -b radio
总结
以上为在 RK3568 Android 13 上移植 EC20-CE 模块的完整过程,希望这篇记录能帮助你在 RK3568 平台上的 4G 开发少走弯路。