3. RK3568 PCIe RC模式驱动开发
好,咱们进入正题。把RK3568配成Root Complex,说白了就是让这颗芯片当PCIe总线上的“老大”。我刚开始接触这个的时候,总觉得设备树配好就完事了,结果板子死活不枚举设备。后来才发现,时钟、复位、核心控制器初始化,哪一步没到位都不行。
这一节,我带你一步步把RC模式跑起来。咱们从设备树配置开始,一路干到链路训练完成。
3.1 设备树配置——硬件怎么“说”给内核听
设备树是硬件和内核沟通的“翻译官”。RC模式下,你得告诉内核:PCIe控制器挂在哪、用哪个时钟、复位信号怎么控、地址空间怎么映射。
我个人习惯先把pcie0节点打开,再配compatible、reg、interrupts这些基础属性。下面是一个典型配置片段:
&pcie0 {
status = "okay";
num-lanes = <2>;
max-link-speed = <3>; /* Gen3 */
reset-gpios = <&gpio0 RK_PB5 GPIO_ACTIVE_LOW>;
vpcie3v3-supply = <&vcc3v3_pcie>;
vpcie1v8-supply = <&vcc1v8_pcie>;
pinctrl-names = "default";
pinctrl-0 = <&pcie0_clkreqn &pcie0_perstn>;
};
这里有几个坑,我踩过:
- num-lanes:RK3568最多支持4 lane,但实际走线可能只用了2 lane。配多了,链路训练会一直尝试连不存在的lane,浪费时间。
- reset-gpios:PERST#信号必须配对。我曾经把GPIO号搞反,结果设备一直处于复位状态,链路死活起不来。
- vpcie3v3/vpcie1v8:这两个电源域必须稳定。如果供电纹波大,链路训练会频繁失败。
/sys/firmware/devicetree/base/pcie0/下的属性值,确认内核解析正确。这一步能省掉很多调试时间。
3.2 时钟与复位管理——别让“心跳”出问题
PCIe对时钟极其敏感。RC模式下,你需要提供参考时钟(REFCLK)给下游设备。RK3568内部有PLL,可以生成100MHz差分时钟。
时钟配置一般在clk节点里搞定:
&pcie0 {
clocks = <&cru ACLK_PCIE0_MST>,
<&cru ACLK_PCIE0_SLV>,
<&cru ACLK_PCIE0_DBI>,
<&cru PCLK_PCIE0>,
<&cru CLK_PCIE0_AUX>;
clock-names = "aclk_mst", "aclk_slv",
"aclk_dbi", "pclk", "aux";
};
复位管理更关键。RC模式下,复位序列是:
- 先释放控制器内部复位(通过CRU寄存器)。
- 再释放PERST#信号给下游设备。
- 等待100ms以上,让设备准备好。
我记得有一次,PERST#释放后只等了50ms,结果某款SSD死活不响应。后来查手册才发现,它要求至少100ms的复位延迟。嗯,从此我老老实实加了msleep(100)。
LTSSM一直在Polling.Active状态打转。
3.3 核心控制器初始化——让硬件“醒”过来
设备树配好了,时钟复位也搞定了,接下来就是让PCIe控制器真正跑起来。这部分工作在内核的pcie-rockchip.c驱动里完成。
初始化流程大致如下:
| 步骤 | 操作 | 关键寄存器/函数 |
|---|---|---|
| 1 | 使能时钟门控 | clk_bulk_enable() |
| 2 | 释放复位 | reset_control_deassert() |
| 3 | 配置DBI(Device Bus Interface) | rockchip_pcie_dbi_init() |
| 4 | 设置地址空间映射 | rockchip_pcie_cfg_atu() |
| 5 | 启动链路训练 | rockchip_pcie_start_link() |
其中,ATU(Address Translation Unit)配置是重中之重。RC模式下,你需要把CPU的物理地址映射到PCIe总线地址空间。说白了,就是告诉控制器:CPU发往0x30000000的访问,要转换成PCIe总线上的0x80000000。
static int rockchip_pcie_cfg_atu(struct rockchip_pcie *rockchip)
{
/* 配置内存空间映射 */
rockchip_pcie_writel(rockchip, 0x30000000, PCIE_ATU_LOWER_BASE);
rockchip_pcie_writel(rockchip, 0x80000000, PCIE_ATU_LOWER_TARGET);
rockchip_pcie_writel(rockchip, 0x10000000 - 1, PCIE_ATU_LIMIT);
rockchip_pcie_writel(rockchip, PCIE_ATU_TYPE_MEM, PCIE_ATU_CR1);
return 0;
}
ranges属性一致。否则,CPU访问PCIe设备时会触发异常,内核直接oops给你看。
3.4 链路训练——最磨人的阶段
链路训练(Link Training)是PCIe初始化的最后一步,也是最容易出问题的一步。LTSSM(Link Training and Status State Machine)会经历Detect → Polling → Configuration → L0这几个状态。
我习惯在驱动里加调试打印,实时监控LTSSM状态:
static int rockchip_pcie_start_link(struct rockchip_pcie *rockchip)
{
int ret;
u32 val;
/* 触发链路训练 */
ret = rockchip_pcie_writel(rockchip, PCIE_CLIENT_LINK_TRAIN_EN,
PCIE_CLIENT_CONFIG);
if (ret)
return ret;
/* 轮询LTSSM状态,超时5秒 */
ret = readl_poll_timeout(rockchip->apb_base + PCIE_LTSSM_STATUS,
val, (val & LTSSM_STATE_MASK) == L0_STATE,
1000, 5000000);
if (ret) {
dev_err(rockchip->dev, "Link training timeout, LTSSM=0x%x\n", val);
return -ETIMEDOUT;
}
dev_info(rockchip->dev, "Link up at Gen%d, x%d\n",
(val & LTSSM_SPEED_MASK) >> 16,
(val & LTSSM_WIDTH_MASK) >> 8);
return 0;
}
如果链路训练失败,别慌。先看LTSSM停在了哪个状态:
- Detect.Quiet:说明没检测到设备。检查PERST#信号和REFCLK。
- Polling.Active:设备有回应,但训练参数不匹配。检查lane数和速度配置。
- Configuration.LanenumWait:lane协商失败。可能走线有问题,或者设备只支持x1。
我曾经遇到一个奇葩问题:链路训练总是超时,但用示波器量REFCLK和PERST#都正常。最后发现是PCB上PCIe差分对的阻抗不匹配,导致信号质量太差。嗯,硬件问题有时候比软件更难查。
/sys/kernel/debug/pci/下可以查看LTSSM状态和链路信息。配合lspci -vvv,基本能定位90%的问题。
3.5 知识结构图——RC模式初始化全景
下面这张图,把整个RC模式初始化的流程串起来了。你对照着看,思路会更清晰。
3.6 调试与验证——怎么确认RC模式跑起来了
初始化完成后,怎么确认RC模式正常工作?我一般做三步检查:
- 看内核日志:搜索
pcie-rockchip和Link up关键字。如果看到Link up at Gen3, x2,说明链路训练成功。 - 用lspci:执行
lspci -vvv,看能不能枚举到下游设备。如果看不到,检查ATU映射和地址空间。 - 读写测试:用
devmem2或pciutils工具,对PCIe设备的BAR空间进行读写。能正常读写,说明RC模式完全OK。
lspci能看到设备,但读写时系统卡死,多半是ATU映射范围不对。检查dts中的ranges属性,确保CPU地址和PCIe总线地址一一对应。