最近在调一块 Gen6 平台的时候,我又跟 PCIe 的 L0p 状态杠上了。说实话,以前做 Gen4/Gen5 老项目时,这个状态我基本不怎么看,反正链路忙时跑满,idle 时靠 L0s 和 L1 就能交差。但到 PCIe 6.0 这一代,链路速率直接干到 32 GT/s,信号从 NRZ 换成了 PAM4,芯片的功耗一下子就压不住了。板卡稍微跑点低负载流量,散热片温度都能起来,这时候才意识到,链路电源管理里那个一直不被重视的 L0p,反而成了必须正视的节电手段。
这篇内容主要围绕“Gen6 下的 L0p”展开,把 L0p 的原理、进入退出过程、在 Gen6 里的新坑,以及我在实际调试中总结的排查方法串成一份偏实操的笔记。对做 FPGA PCIe 加速卡、服务器网卡、NVMe 控制器或者 PCIe 交换机的工程师来说,应该能少走不少弯路。
1. L0p 到底是干嘛的:一个链路状态机里经常被忽略的状态
1.1 LTSSM 中 L0p 的真正位置
写过 PCIe 驱动或者抓过 LTSSM 状态的朋友都知道,PCIe 的物理层状态机本身就有一堆状态,大家最熟悉的是 L0、L0s、L1、L2/L3 这些。L0 是正常工作状态,L0s 是低功耗待机快速恢复状态,L1 是深度些的低功耗状态,L2 基本是辅助电源都关了的状态。而 L0p,全称是 L0 partial,中文一般叫“部分活动链路状态”。
要注意的是,L0p 并不是像 L1/L2 那种独立的 LTSSM 状态,它本质上属于 L0 的“子模式”。链路在 L0 状态下时,不一定要拿满全部的通道(lane)做传输,可以通过把一部分 lane 的发送器和接收器关掉,让剩下的 lane 继续工作。这时链路的协议状态仍然是 L0,物理介质上却只有部分 lane 在跑,这个状态就是 L0p。
为什么 Gen6 下 L0p 容易被拿出来单独讲,最直接的原因是 PAM4 信号和 32 GT/s 速率把功耗推到了一个不该被忽略的量级。以前 Gen3/Gen4 时代,一条 x16 链路全开的功耗也就那样,大家习惯性把电源管理外包给 L0s 和 L1;到了 Gen6,PHY 的差分对功耗、PAM4 接收端的连续时间线性均衡器、FEC 编解码逻辑,这些全加起来之后,链路即使没有流量,功耗也相当可观。
1.2 L0p、L0s、L1 的区别,别再搞混
很多人一开始会把 L0p 和 L0s 弄混,我觉得主要原因在于名字太像。实际上这两个机制完全不同:
- L0s 是把每个 lane 的发送器静默,接收端进入低功耗接收状态。L0s 里所有 lane 都可以关闭,但链路本身不再传输有效数据,退出时需要一段时间恢复。
- L0p 是保留部分 lane 传输有效数据,其余 lane 关闭。链路里还有数据在跑,只是带宽变小。
- L1 是把整条链路放到比 L0s 更低的功耗,同时要求两端都进入特定的低功耗状态,出口延迟更大。
用仓库物流打比方:L0s 是整个卸货平台都停工,等你电话通知大家再来;L0p 是平台还开着,但只留几个装卸口,其它装卸口暂时拉上帘子;L1 是整个仓库差不多打烊,连灯都关了,有人来货时得先开总闸再开工。
L0p 的好处,是保留了有效数据传输能力,因此低吞吐场景下不会因为退出链路状态而损失过多延迟。坏处是还是有一部分 lane 在工作,功耗不会像 L1 那样降到很低。
1.3 为什么低流量场景会想起用 L0p
假设你现在跑一块 Gen6 x16 的加速卡,多数业务场景下数据吞吐只有 50 GB/s,远远没到链路极限。如果把链路直接切到 L1,一旦来一个大任务,重新回到 L0 的延迟可能会让业务产生明显的卡顿。如果什么都不做,链路所有 lane 都跑着,功耗就一直降不下来。L0p 的定位就在这两者之间:降到 x8 甚至 x4 去应付低流量,等活动数据量上来了再快速回到 x16。
实际工程里,L0p 的典型场景包括:
- 网卡队列空闲但链路不能断,要保证首包时延。
- SSD/NVMe 盘使用率低,主机端不需要那么多通道。
- 多主机拓扑里,某个根端口实际带宽需求有限,不想让整条链路保持满带宽运转。
- FPGA 加速卡在等待的间隙,用来处理器件空闲功耗和散热。
2. Gen6 下 L0p 的特征变化与控制机制
2.1 PAM4 信号带来的功耗和信号质量双重压力
Gen6 相对 Gen5 最直观的变化就是上升到 PAM4 调制。PAM4 在同一个 symbol 里编码 2 bit,理论上同样的通道速率下带宽翻倍,但 PAM4 的噪声裕量比 NRZ 差不少。为了满足误码率,Gen6 引入了前向纠错(FEC),同时 PHY 的均衡和发送端去加重也要比之前复杂得多。
这事儿放到 L0p 里就出现了一个以前不太明显的耦合关系:当你把部分 lane 关闭时,剩下 lane 的功耗并不会等比例下降,因为 PAM4 的 TX 驱动器、RX 的 CTLE/DFE 在活动 lane 上该开着还得开着。而关闭 lane 本身又会造成电源平面上的瞬态电流变化,可能反过来影响活动 lane 的信号眼图。
在调试时,我印象很深的一个问题就是,L0p 从 x8 切到 x4 的瞬间,活动 lane 的误码率会突然升高。后来定位下去,其实不是协议问题,而是关闭 lane 的偏置电流跳变影响到同一 PLL 域里其它 lane 的时钟抖动。所以 Gen6 下做 L0p 电源完整性仿真和实测,不能像以前那样只盯着功能,还得看动态电流特性。
2.2 FEC 与 L0p:通道数变化会牵动编码分布
PCIe 6.0 引入的 FEC 是基于固定长度的码字(一般是固定符号数)块来做的。在整条链路全宽度工作时,FEC 码字会按照所有 lane 的并行数据一起打包,这没什么问题。但在 L0p 状态下,活动 lane 数变了,每个 symbol 分布到 lane 上的逻辑就得跟着调整,否则 FEC 保护的数据块就会错位。
这也是 Gen6 平台在 L0p 实现上比 Gen5 更麻烦的地方。Gen5 之前没有这个强制的前向纠错机制,lane 数的切换更多是 PHY 和链路训练的事;到了 Gen6,FEC 必须感知当前活动 lane 数,并重新映射码字边界。所以很多控制器在 L0p 切换前后会强制先把当前 FEC 码字发完,再做 lane 数调整,否则收发两端对码字的起始位置理解不一致,会直接翻车。
2.3 非对称收发通道数:L0p 特有的实用能力
L0p 有一个容易被忽略但非常实用的能力:允许发送方向保留的 lane 数和接收方向保留的 lane 数不一样。就是说,Tx 可以按 x16 保留,Rx 只按 x4 保留,反过来也一样。这个特性叫非对称 L0p,在 Gen6 下的典型用途就是应对明显不平衡的业务流量。
举一个例子,NVMe SSD 在做大量写入时,主机往设备方向的数据量远大于设备往主机方向。为了省功耗,可以把主机到设备的方向保留 x8,设备到主机的方向只保留 x1 或 x2。虽然设备到主机的带宽低了,但正好匹配当前业务的写突发特征。等读操作变多,再把上行方向加宽。
实际使用时要注意,非对称 L0p 不是所有设备都支持,也不是每个 PCIe 控制器都能灵活配置。硬件设计的时候,需要看清楚端点的能力寄存器里有没有对应的 capability bit。
2.4 Gen6 L0p 下各 lane 数的带宽变化参考
把 Gen6 的带宽变化列成表,方便后面设计时估算:
| 活动 lane 数 | 数据速率(单向) | 典型适用场景 |
|---|---|---|
| x16 | 约 128 GB/s | 全速传输,GPU/AI 加速卡峰值场景 |
| x8 | 约 64 GB/s | 中高负载,突发读写较多的业务 |
| x4 | 约 32 GB/s | 低延迟在线业务,网卡空闲等待 |
| x2 | 约 16 GB/s | 控制面和少量数据面混合部署 |
| x1 | 约 8 GB/s | 纯管理流量、boot 阶段或低负载待机 |
注意,这个表算的是单向有效带宽。PCIe 是双工的,两个方向的流量可以同时跑,所以在规划功耗方案时,两个方向占用 lane 的配置要分别评估,这也是非对称 L0p 的价值所在。
3. L0p 的进入、退出与工程实现要点
3.1 进入 L0p 的条件与链路层协商
L0p 不是随便哪个时刻都能进的。最基本的前提是:
- 当前链路处于 L0 状态,并且链路速率在工作速率而不是降速速率。
- 目标 lane 数必须是 2 的幂(1、2、4、8,受原始链路宽度限制)。
- 两端设备都支持 L0p,并且支持对应的 lane 数组合。
- 当前和目标的 lane 数变化不会超过设备能力位所规定的窗口。
这些条件满足后,进入 L0p 的过程一般由链路层发起。一端设备通过发送特定的链路报文,向对端提出 lane 数变更请求,对端在收到后确认,然后两端在一段时间窗口内完成 lane 数切换。
我自己的实际排查经验是,L0p 频繁进入失败的情况,绝大多数不是卡在协议报文的格式上,而是卡在“两端对能力位的理解不一致”。尤其当你把不同厂商的 CPU 和 FPGA 或交换芯片接在一起,L0p 的 capability 位定义和默认值有时候有差异,导致某一边觉得自己支持 x8,另一边却只支持到 x4。
3.2 从 L0p 退出:不是拨地而起,而是有时序要求
L0p 退出时需要把关闭的 lane 重新打开,这个过程需要时间。关闭的 lane 接收端重新上电、重新做接收检测、再对同步头做锁定,每一步都有具体的时序要求。退出时最怕的是业务已经来了,但 lane 还没恢复,导致缓冲区溢出丢包。
在实际 IP 设计里,通常会把 L0p 退出路径的延迟拆成几个等级:
- 从 x1 恢复到 x2,因为只多一条 lane,恢复速度很快。
- 从 x8 恢复到 x16,涉及驱动器重新上电和均衡收敛,需要更多时间。
- 如果设备刚从 L1 出来又马上进 L0p,后续退出时间会更不可控。
所以 Gen6 下,如果做的是低延迟网络应用,建议把 L0p 的最低保留 lane 数设置得高一点,别为了那点功耗而把所有带宽都减到最小,否则尾延迟非常难压。
3.3 实测中怎么观察 L0p 是否真的生效
判断 L0p 是否生效,不能只看功耗表。我的习惯是先在逻辑分析仪或协议分析仪上抓 LTSSM 状态和链路训练序列。L0p 入口阶段你应该能看到链路仍然处于 L0 状态,但活动 lane 数量发生了变化;在协议分析仪的链路层窗口里,也会看到由报文驱动的 lane 数变更。
如果条件允许,再用功耗分析仪把 PCIe 插槽的 +12V 和 +3.3V 电流分别抓出来。关注两个关键指标:一是进入 L0p 后是否有若干个毫秒级的基本功耗下降;二是退出 L0p 时有没有瞬间大电流尖峰。如果只有下降但退出时电流尖峰很大,就要检查关闭 lane 的上电次序是否合理。
4. 常见问题与排查技巧实录
4.1 进入 L0p 后链路掉训练
这个现象我在 FPGA 和 CPU 组合的平台里遇到过好几次。表现是,设备进入 L0p 之后,过了一段时间对端完全失去同步,链路重新走了训练流程,甚至降速到低速运行。
排查顺序一般是:
- 先确认两端 L0p 能力位是否对齐。用配置空间读取能力位,逐个比对。
- 确认进入 L0p 时间点是否卡在某个 FEC 码字的中间。如果是,基本都是硬件对 FEC 码字边界处理不对。
- 确认是否因为关闭 lane 后,剩余 lane 的发送端没有维持必要的 TS 序列或者对齐标记,导致对端接收状态机误判。
- 最后再检查电源纹波和地弹。对高速系统来说,关闭 lane 带来的瞬态噪声影响信号质量,真不是玄学。
4.2 功耗没有降下来,反而增加了
有的项目开 L0p 之后,整卡功耗不降反增,这个坑最常见的原因是进出 L0p 太频繁。每进出一次,PHY 就要做一次 lane 重新上电和均衡收敛,这期间功耗可能比一直待在 L0 还高。如果业务流量本来就是碎包密集型,L0p 开关次数居高不下,功耗当然降不下来。
解决办法是不要用简单的“链路低利用率”作为进入 L0p 的唯一判据,而是加一个时间窗口,比如链路利用率低于 10% 并且持续超过 50 微秒才允许进入。退出条件也可以带上迟滞,避免在阈值附近来回抖动。
4.3 退出 L0p 后出现误码
退出 L0p 后误码率升高,多半是链路均衡参数没有在 lane 重新启用时再次加载。Gen6 对均衡一致性要求更高,有些实现为了省事,关闭 lane 后直接把该 lane 的均衡寄存器配置清掉了,重新打开时用默认值,这跟对方保持的均衡参数就不一致,短时间会出现 FEC 纠不过来或者误码。
正确的做法是,进入 L0p 时保留剩余 lane 以及已关闭 lane 的均衡参数副本,退出时按原参数恢复。如果 IP 控制器不能自动保留,那要在驱动或固件层做配置备份和恢复。
4.4 L0p 退出延迟影响到业务尾延迟
如果你做的是 RDMA 网卡或者高频交易这类低尾延迟场景,L0p 的退出延迟必须进设计预算。常见调整手段有几个:
- 把 L0p 保留 lane 数从 x1 提到 x4,减少恢复时的 lane 数跨度。
- 在驱动里对预期的大流量事件做提前唤醒,比如收到第一个报文时马上触发全部 lane 恢复。
- 在硬件里做“先恢复 lane 再上报中断”的处理,别让软件等到中断后才启动恢复流程。
4.5 速查表:L0p 排查对照
| 现象 | 可能原因 | 解决倾向 |
|---|---|---|
| 链路掉训练 | 能力位不一致 / FEC 码字边界错位 | 核对双方 capability,检查码字处理逻辑 |
| 误码率高 | 均衡参数未恢复 | 保留并恢复均衡寄存器配置 |
| 功耗不降反增 | 进出 L0p 太频繁 | 增加进入迟滞时间窗口 |
| 尾延迟超标 | 保留 lane 数太少、退出时序太慢 | 提高最低 lane 数,提前唤醒恢复 |
| 电流尖峰大 | 关闭 lane 上电次序不合理 | 优化电源门控和上电时序 |
5. 这几种工具和平台背景,建议提前熟悉
如果你要在实物上验证 L0p,手里最好有几个东西:支持 Gen6 的协议分析仪、带电流探头的功耗分析仪,以及能直接读 LTSSM 状态的调试工具。很多逻辑分析仪对 Gen6 的 32 GT/s 信号有点吃力,实际抓取时尽量选支持 PAM4 的专用协议分析仪,不然看到的数据可能已经是经过重定时或者经过错误修正后的结果,未必反映真实链路行为。
另外,如果项目里有 PCIe switch,也要重点看 switch 对 L0p 的处理方式。switch 的上行口和下行口可能各自维护独立的 L0p 状态,但 switch 本身的数据通路是共享的,一边进入 L0p 另一边未必能省电。更麻烦的是,某些 switch 在内部做 packet arbitration 时,并不会感知链路已经降到 x1,导致大量帧堆积在端口缓冲里,出现意料之外的延迟。
我在一次测试里就碰到过交换机下行口进了 L0p,上行口还全速跑,结果某个队列的占用率高居不下,出口延迟直接飙了几十倍。当时查了半天,最后发现只要把下行口的 L0p 保留 lane 数从 x1 抬到 x4,问题就没了。这种跟拓扑相关的细节,在芯片手册里一般不会写得很直白,实测数据才是真正靠谱的依据。
6. 最后说点个人体会
我在 Gen6 平台调试中最大的感受是,L0p 已经从“可选项”变成了“必须项”。单纯靠 L0s 和 L1 做功耗管理的时代,在 PAM4 面前真的不够用。但是 L0p 也是一个典型的“牵一发动全身”的状态,从 FEC 映射到均衡参数,从电源完整到链路层报文,每一处都可能成为坑。
如果你刚开始做 Gen6 的板卡或者控制器,建议别等到芯片送到手再接 L0p,而是在寄存器规划和 IP 选型阶段就把 L0p 能力位、lane 数组合、退出延迟这些定下来。等真实流量跑了再回头补,代价通常比想象中大得多。最后再提一个小技巧:调 L0p 时,把链路利用率统计和功耗统计同步打点,时间戳对齐到微秒级,很多看似奇怪的功耗问题,一对比曲线就立刻露馅了。