4. RK3568 PCIe EP模式驱动开发
好,咱们今天聊聊EP模式。说实话,很多做驱动的朋友一开始都习惯把RK3568当RC(Root Complex)用,接个Wi-Fi卡、SSD什么的。但真正有意思的玩法,是把RK3568配置成Endpoint,让它作为一个智能外设去跟主机通信。
我在一个工业数据采集项目里就这么干过。RK3568作为EP,挂在x86主机的PCIE总线上,主机通过DMA直接读取采集数据,延迟比网卡低一个数量级。嗯,今天我就把这块的配置要点和踩过的坑都摊开来讲。
4.1 EP模式与RC模式的核心差异
先看一张对比表,帮你快速建立概念。说白了,RC是“总线主人”,EP是“总线仆人”。
| 对比维度 | RC模式(默认) | EP模式(本章目标) |
|---|---|---|
| 总线角色 | 枚举并管理下游设备 | 被上游RC枚举和配置 |
| 配置空间 | 由固件/BIOS初始化 | 由RC写入,驱动需响应 |
| BAR空间 | 由驱动分配(作为主机) | 由RC分配,驱动需映射 |
| 中断生成 | 接收并处理中断 | 主动发送INTx或MSI |
| 典型场景 | 接NVMe、Wi-Fi、GPU | 数据采集卡、AI加速器 |
你想想看,EP模式下的驱动,更像是在写一个“PCIe设备固件”。你要主动告诉RC:“我有多少内存窗口”、“我能产生什么中断”。
4.2 硬件配置:让RK3568进入EP模式
第一步不是写代码,是改设备树。RK3568的PCIe控制器在硬件上同时支持RC和EP,但同一时刻只能选一种。我习惯在dts里明确指定模式。
// rk3568-ep.dtsi 关键片段
&pcie2x1 {
status = "okay";
rockchip,pcie-mode = "ep"; // 关键:声明为EP模式
num-lanes = <1>;
phys = <&phy_pcie>;
phy-names = "pcie-phy";
// EP模式下不需要复位GPIO,由RC控制
// ep-gpios 这里不配
};
rockchip,pcie-mode,结果内核启动时直接去枚举下游设备,总线挂死。如果你发现RC侧根本看不到设备,先检查dts里这个属性有没有配成"ep"。
硬件上还需要注意:EP端的REFCLK必须由RC提供,或者使用独立的时钟源。我在项目中遇到过时钟抖动导致链路训练失败的问题,后来在原理图上加了AC耦合电容才稳定。
4.3 BAR空间配置:你的内存窗口
BAR(Base Address Register)是EP和RC交换数据的核心。RC通过写配置空间来分配BAR的基地址,EP驱动则需要告诉RC:“我需要多大的窗口”。
我个人习惯在驱动里用pci_epc_set_bar()来配置。来看一个实际例子:
// EP驱动中配置BAR0:4MB内存窗口
static int rk3568_ep_bar_init(struct pci_epc *epc)
{
struct pci_epf_bar bar;
int ret;
memset(&bar, 0, sizeof(bar));
bar.phys_addr = 0; // 由RC分配
bar.size = SZ_4M; // 4MB窗口
bar.barno = BAR_0;
bar.flags = PCI_BASE_ADDRESS_MEM_TYPE_32;
ret = pci_epc_set_bar(epc, 0, &bar);
if (ret) {
dev_err(&epc->dev, "Failed to set BAR0\n");
return ret;
}
// 映射BAR到CPU地址空间,用于本地访问
epf->bar_phys[0] = pci_epc_get_bar_phys(epc, 0, BAR_0);
epf->bar_virt[0] = ioremap(epf->bar_phys[0], SZ_4M);
return 0;
}
这里有几个要点:
- BAR大小必须是2的幂,且不能小于4KB。我见过有人配3MB,结果RC直接报错。
- BAR0通常用于控制寄存器,BAR1/BAR2用于数据缓冲区。这是约定俗成的做法。
- 物理地址由RC分配,EP驱动只能通过
pci_epc_get_bar_phys()拿到分配后的地址。
lspci -vvv -s 01:00.0 查看BAR的基地址和大小。如果显示 [disabled],说明EP侧没有正确调用 pci_epc_set_bar()。
4.4 中断生成:INTx与MSI
EP需要主动通知RC。RK3568支持两种中断方式:INTx(传统中断线)和MSI(消息信号中断)。
我个人的建议是:能用MSI就别用INTx。MSI不需要物理中断线,性能更好,而且可以支持多个中断向量。但有些老旧的RC只支持INTx,这时候就得兼容。
4.4.1 MSI中断生成
// 发送MSI中断
static void rk3568_ep_raise_msi(struct pci_epf *epf, u8 interrupt_num)
{
struct pci_epc *epc = epf->epc;
int ret;
// interrupt_num 从1开始,对应MSI的多个向量
ret = pci_epc_raise_irq(epc, 0, PCI_EPC_IRQ_MSI, interrupt_num);
if (ret)
dev_err(&epf->dev, "Failed to raise MSI %d\n", interrupt_num);
}
这里要注意:MSI的向量号从1开始,不是0。我第一次写的时候传了0,结果RC侧死活收不到中断。查了半天spec才发现这个细节。
4.4.2 INTx中断生成
// 发送INTx中断(传统方式)
static void rk3568_ep_raise_intx(struct pci_epf *epf)
{
struct pci_epc *epc = epf->epc;
// INTx只有A/B/C/D四个,通常用INTA
pci_epc_raise_irq(epc, 0, PCI_EPC_IRQ_INTX, 0);
}
pci_epc_raise_irq() 关闭中断。
4.5 数据交互:RC与EP的通信机制
数据交互的核心是:RC通过BAR访问EP的内存,EP通过中断通知RC数据就绪。我画了一张流程图,帮你理清整个交互逻辑。
实际代码中,数据交互通常通过DMA来提升性能。RK3568内部集成了DW DMA控制器,可以配合PCIE EP实现内存到内存的搬运。
// EP侧:准备数据并通知RC
static irqreturn_t rk3568_ep_irq_handler(int irq, void *data)
{
struct rk3568_ep_dev *ep = data;
u32 *buf = ep->bar_virt[1]; // BAR1作为数据缓冲区
// 模拟采集数据
buf[0] = readl(ep->adc_base); // 读取ADC值
buf[1] = readl(ep->gpio_status);
// 通知RC数据就绪
rk3568_ep_raise_msi(ep->epf, 1);
return IRQ_HANDLED;
}
dmaengine_prep_slave_sg() 来配置。我在项目中用DMA搬运1MB数据,延迟从200μs降到了15μs。
4.6 避坑指南与调试技巧
最后,我把这些年积累的调试经验整理成清单,你遇到问题时可以直接对照排查。
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
| RC侧看不到设备 | dts模式配错、链路训练失败 | 检查 rockchip,pcie-mode;用示波器看REFCLK |
| BAR空间读回全F | BAR未正确映射、地址冲突 | RC侧 lspci -vvv 查看BAR状态 |
| MSI中断收不到 | 向量号从1开始、MSI使能未配 | 检查RC侧 /proc/interrupts |
| INTx中断风暴 | 中断未正确清除 | 确认EP侧在ISR中调用清除函数 |
| 数据传输CRC错误 | 时钟不稳定、PCB走线过长 | 降低PCIE速率到Gen1测试 |
嗯,EP模式开发确实比RC模式多了一些弯弯绕绕。但一旦你掌握了BAR配置和中断生成的套路,就会发现RK3568作为EP的潜力非常大——它不再只是一个应用处理器,而是一个可以跟任何x86/ARM主机无缝对接的智能外设。
我个人觉得,调试EP模式最有效的工具就是RC侧的lspci和setpci。配合EP侧的内核打印,大部分问题都能定位。如果遇到链路训练问题,别犹豫,直接上示波器看PCIE差分信号——软件解决不了物理层的问题。