3. ASPM 相关寄存器:Link Control 寄存器详解
好了,咱们直接切入正题。上一章我讲了 ASPM 是个什么东西,它为什么省电,又为什么容易惹麻烦。这一章,我们得把手伸进 PCIe 设备的“大脑”里,看看那个控制 ASPM 的关键寄存器——Link Control Register。
这个寄存器,属于 PCIe 能力结构(PCIe Capability Structure)。说白了,每一个 PCIe 设备,从 CPU 里的 Root Port 到你的 NVMe 硬盘,都有一个这样的寄存器。ASPM 的开关,就藏在它里面。
核心结论:Link Control 寄存器中的第 1 位和第 2 位,分别控制 L0s 和 L1 这两个 ASPM 状态。驱动层面想控制 ASPM,本质上就是读写这两个位。
3.1 寄存器的“门牌号”
先别急着翻代码。你得知道这个寄存器在哪。PCIe 设备的配置空间里,有一段区域叫“PCIe Capability”。这个 Capability 结构的偏移地址不是固定的,需要通过设备的 Capability Pointer 来找。
我习惯用一个简单的比喻:Capability Pointer 就像一个寻宝图上的起点。每个 PCIe 设备在配置空间的 0x34 字节处,都存着一个指针,指向第一个 Capability。然后每个 Capability 结构开头都有一个 ID 和下一个指针。你顺着链表找,直到 ID 等于 0x10(PCI Express Capability ID),就找到了。
我的小经验:在 Linux 内核里,你根本不用手动去遍历这个链表。直接用 pci_find_capability(dev, PCI_CAP_ID_EXP) 这个函数,它会返回 PCIe Capability 结构的起始偏移。省心吧?
找到 PCIe Capability 后,Link Control 寄存器就在这个结构的 偏移 0x10 处。它是一个 16 位的寄存器。
3.2 关键位域逐位分析
我们只盯着 ASPM 相关的位看。其他位,比如“中断使能”、“链路训练”之类的,本章先放一放。下表是 Link Control 寄存器中与 ASPM 强相关的位域。
| 位 (Bit) | 名称 | 寄存器属性 | 功能描述 |
|---|---|---|---|
| 1:0 | ASPM Control | RW | ASPM 使能控制。详见下表。 |
| 4 | Enable L0s (部分设备) | RW | 在一些旧规范或特定实现中,L0s 的使能位独立于此。 |
| 5 | Enable L1 (部分设备) | RW | 类似 Bit 4,L1 的独立使能位。 |
| 15 | L1 PM Substates (L1.1/L1.2) 使能 | RW | 控制更深的 L1 子状态,跟 ASPM 有关联但不是一回事。 |
重点来了。ASPM Control 这位域是两个比特(Bit1 和 Bit0),它们的编码如下:
| Bit1 | Bit0 | ASPM 状态 |
|---|---|---|
| 0 | 0 | 禁用 ASPM(默认状态) |
| 0 | 1 | 使能 L0s(仅入口延迟节省) |
| 1 | 0 | 使能 L1(更节能,但延迟大) |
| 1 | 1 | 使能 L0s 和 L1(兼顾低延迟和省电) |
避坑指南:我曾经在一个项目中,发现读出来的值明明是 0x03,按说 L0s 和 L1 都使能了,但设备就是不进 L1。折腾了半天才发现,上游端口(Upstream Port)的 Link Control 寄存器里,L1 使能位根本没设。记住,ASPM 是链路两端协商的,两端都得匹配。
3.3 为什么设计两个独立使能位?
你可能会问:为什么设计 L0s 和 L1 两个独立使能位?直接用 ASLM Control 那两位不就行了?
嗯,这个问题我当年也困惑过。其实,这是为了兼容不同的硬件实现和功耗策略。有些设备可能只实现了 L0s,没实现 L1。更常见的是,芯片组或 BIOS 可能强制要求某些端口“只能 L0s 不能 L1”,或者反过来。
举个例子:我做过一个显卡驱动项目。显卡挂在 PCIe 插槽上。如果 ASPM 进入 L1,显卡的链路时钟会完全停止,重新唤醒需要几十微秒。对于实时帧率敏感的显示驱动,这可能导致画面撕裂。所以,驱动里我们只开了 L0s,把 L1 死死摁住。这样,短时空闲能省点电,一旦需要马上干活,恢复得极快。
- L0s:发完一个 TLP,物理层可以快速进入低功耗,但链路逻辑(流量控制、重播缓冲)必须保持。恢复时间大约几百纳秒到几微秒。
- L1:整个链路进入深度睡眠,时钟停止,甚至电压都可以降。恢复时间长达几微秒到几十微秒。
- 全开:如果业务流量是“稀疏大包”,L0s 效果明显;如果是长时间空闲,L1 更好。
3.4 还有几个“狡猾”的位
除了 ASPM Control,Link Control 寄存器里还有几个位对 ASPM 影响很大。我把它们列出来,你心里有个数:
- Enable L0s (Bit 4) 和 Enable L1 (Bit 5):这两个位存在于旧一些的 PCIe 规范版本中。有时候 BIOS 会通过它们来控制基本的 ASPM 功能,而 ASPM Control 位域可能被用来控制更细粒度的策略。注意,它们不是必须要实现的。
- Link Autonomous Bandwidth Status 和 Link Bandwidth Management Status:虽然不直接控制 ASPM,但当 ASPM 导致链路宽度或速度发生改变时,这些位会被置位。调试 ASPM 问题时,检查这些位会给你线索。
- Retry Buffer 相关位:L0s 状态下,发送缓冲区不能丢。有些控制位会影响重播缓冲区在 L0s 期间的行为。不过,这部分寄存器属性通常是只读或硬件自动管理,驱动一般不要动它。
我的调试习惯:先用 setpci -s <BDF> CAP_EXP+0x10.W 直接读 Link Control 寄存器,看看当前 ASPM 到底被设成了什么。如果发现是“禁用”,再往上查 BIOS 或内核参数。这一步能省掉你很多猜测时间。
3.5 寄存器读写的几种方式
在 Linux 驱动里,你肯定不会去手算偏移了。内核已经封装好了 API。我给个简单的代码片段,展示怎么改 ASPM。注意,这只是一个演示,不涉及更复杂的协议协商。
#include <linux/pci.h>
#include <linux/pcieportdrv.h>
/* 获取 PCIe 能力结构偏移 */
int pcie_cap = pci_find_capability(pdev, PCI_CAP_ID_EXP);
if (!pcie_cap) {
dev_err(&pdev->dev, "PCIe cap not found\n");
return -ENODEV;
}
/* 读 Link Control 寄存器 */
u16 link_ctrl;
pci_read_config_word(pdev, pcie_cap + PCI_EXP_LNKCTL, &link_ctrl);
/* 禁用 ASPM:清掉 ASPM Control 位域 的位 */
link_ctrl &= ~PCI_EXP_LNKCTL_ASPM_L0S;
link_ctrl &= ~PCI_EXP_LNKCTL_ASPM_L1;
/* 写回寄存器 */
pci_write_config_word(pdev, pcie_cap + PCI_EXP_LNKCTL, link_ctrl);
/*
* 注意:仅仅写本端设备可能不够。
* 对于 Bridge 设备,你还需要操作它的对端(Secondary Bus)上的设备。
* 内核函数 pci_disable_link_state() 做了这些脏活累活。
*/
这段代码虽然简单,但它包含了核心逻辑:读出、修改、写入。我自己写驱动时,一般不会直接调用 pci_read_config_word,而是用内核已经封装好的 pcie_capability_read_word 系列函数。它们内部做了端点检测,更安全。
3.6 用一张图理清脉络
下面这个 SVG 图,帮你把本章的脉络串起来。寄存器怎么找、位怎么配、链路两端怎么协商,一目了然。
再次提醒:图中最后一步“通知链路对端”,实际是由硬件通过 LTSSM 状态机自动协商的。驱动只管写本端寄存器。但如果对端没设好,硬件协商会失败,ASPM 仍然进不去。所以我建议,调试时务必将链路两端都检查一遍。
好,Link Control 寄存器的 ASPM 控制位就说到这。你理解了这几位,驱动层面控制 ASPM 就已经掌握了 80% 的核心。接下来,你可以考虑如何动态调整策略,或者如何处理 BIOS 已经配好的情况。这些,我后面章节再聊。