4. L0s深入:进入条件、退出延迟、LTSSM中的对应状态
说实话,刚开始接触ASPM时,我一直以为L0s就是个“省电模式”而已——让链路歇着就完了。但实际调过几个项目后才发现,L0s的脾气远比你想象的要大。你配置对了,它能帮你省下可观的电量;配置错了,链路上能给你整出一堆莫名其妙的超时和错误。
我个人习惯把L0s看作一种“浅休眠”状态。不像L1那样能睡到天荒地老,L0s更像是在等红绿灯时的那几秒闭眼。它能快速恢复,代价就是省电幅度有限。这一节,咱们就把它里里外外捋一遍。
4.1 L0s到底是个什么状态?
L0s是LTSSM(链路训练状态机)中的一个子状态,属于Active State Power Management的一环。它的特点是:
- 逐方向独立进入:每个方向(发送/接收)可以单独进入L0s,另一方向可能还在L0。
- 不保留TLP:一旦进入L0s,发送缓冲区的内容会被清空,不保存事务包。
- 退出极快:理论退出延迟为数ns到数百ns,远快于L1(几μs)。
你想想看,为什么PCIe要设计这么个“半吊子”低功耗状态?原因很简单:很多时候链路只是偶尔空闲几μs,如果能在这几μs里省电还保证快速响应,那整体功耗就能掉下来。L0s就是干这个的。
| 状态 | 功耗节省程度 | 退出延迟 | 双向独立 | 保留TLP |
|---|---|---|---|---|
| L0 | 无 | — | — | 是 |
| L0s | 中 | ns级 | 是 | 否 |
| L1 | 高 | μs级 | 否(双向同步) | 否 |
4.2 L0s进入条件
进入L0s的条件并不复杂,但在项目里调错的地方恰恰是这些“看起来简单”的东西。
- 链路必须处于L0状态——这不用多说,得先醒着才能睡。
- 发送端TX的Physical Layer空闲超过一定时间(由软件配置):
- 具体时间由L0s Entry Timer决定,单位为1个UI(Ultra Interval)。
- 典型的默认值是7个UI,但你可以通过寄存器调节。
- 我个人建议:对于延迟敏感的链路(比如NVMe盘),把这个timer设大一点,避免频繁进出L0s。
- 接收端RX没有数据正在接收——如果对面正在发东西,你还想睡?门都没有。
- 链路两侧都必须使能L0s:
- 通过Device Control Register (DCR)中的Enable L0s位控制。
- 如果对端不使能,那本端强行进入L0s也没用,链路会直接报错。
- 没有Pending的数据包或重放缓冲区中的包——缓冲区里有货,你就别想进L0s。
- Gen1: 1UI ≈ 1ns
- Gen2: 1UI ≈ 0.5ns
- Gen3: 1UI ≈ 0.25ns
- Gen4: 1UI ≈ 0.125ns
4.3 L0s退出过程与延迟
L0s的退出可比进入要敏感得多。为什么?因为退出延迟直接决定了系统响应时间。你要是退出太慢,应用层那边可能已经等到超时了。
退出触发条件:TX一旦有数据要发送,必须立即退出L0s。这个“立即”是硬件层面的动作——不需要软件干预,完全由Physical Layer自己裁决。
退出延迟的组成:
- Electrical Idle Exit Time(电气空闲退出时间):
- 从L0s退出命令发出到链路重新电气激活的时间。
- PCIe Base Spec规定:L0s退出延迟 ≤ 4个UI + 几十ns。
- 实际上,Gen3/Gen4的电气空闲检测更复杂,有时会多出额外的训练序列。
- Bit Lock + Block Alignment Time(位锁定与块对齐时间):
- 退出L0s后,接收端需要重新建立位同步和块对齐。
- Gen1/Gen2: 约需4个符号(Symbol)时间。
- Gen3/Gen4: 需要额外的8b/10b或128b/130b块同步序列。
- Fast Training Sequence (FTS) 开销:
- L0s退出时需要发送FTS(最多4个FTS,由链路宽度决定)。
- 每增加1条Lane,FTS需要额外重复一次。
| Gen | 典型退出延迟 | 主要影响因素 |
|---|---|---|
| Gen1 | 100~200ns | 电气空闲检测 + FTS |
| Gen2 | 70~150ns | 电气空闲检测 + FTS |
| Gen3 | 200~400ns | 电气空闲检测 + 块对齐 + EQ |
| Gen4 | 300~600ns | 电气空闲检测 + 块对齐 + EQ |
4.4 LTSSM中的对应状态
LTSSM是PCIe协议的“交通警察”,它控制着链路所有状态的转换。L0s不是孤立的,它跟Recovery、L0、L1之间都有明确的跳转路径。
我画了一张图,把相关的状态流转关系标出来了,你一看就懂:
结合这张图,咱们再捋一下LTSSM的行为:
- L0 → L0s Entry:当TX空闲时间大于设定的Entry Timer,硬件自动发起进入L0s的请求。
- L0s Entry → L0s:成功进入低功耗等待状态。此时发送端停止驱动,链路呈现Electrical Idle。
- L0s → L0s Exit:一旦TX有新数据要发出,立即退出。这个过程不可被软件打断,完全由硬件完成。
- L0s Exit → L0:退出后如果链路状态正常,直接回到L0继续收发数据。
- L0s Exit → Recovery:如果退出过程中电气空闲检测失败、或者对端未准备好,LTSSM会跳转到Recovery进行链路恢复。
| 状态名称 | 性质 | 典型停留时间 | 主要动作 |
|---|---|---|---|
| L0 | 活动状态 | 不定 | 正常发送接收TLP、DLLP |
| L0s Entry | 过渡状态 | 几个UI | 发送Electrical Idle Ordered Set (EIOS) |
| L0s | 低功耗状态 | 几μs到几ms | 电气空闲,不驱动差分对 |
| L0s Exit | 过渡状态 | 几十ns到几百ns | 发送FTS、进行位同步和块对齐 |
| Recovery | 错误恢复状态 | 几μs | 重新训练链路、重新均衡 |
4.5 一些实战中需要注意的点
关于L0s Entry Timer的选择
我在多个项目中试过不同的值,最终得到一条经验:
- 对于延迟敏感的应用(如NVMe、GPU),建议设到16~32 UI,减少无谓的进出。
- 对于功耗敏感但带宽要求不高的应用(如某些IoT Card),可以用7~8 UI。
- 千万不要全部用芯片默认值。默认值通常是厂商提供的保守值,不一定适合你的设计。
关于逐方向独立带来的问题
嗯,这里要特别注意:L0s是逐方向独立的。也就是说,读方向可能进入了L0s,写方向还在L0。这会导致一种情况——你收到一个读请求,但对方发方向的TX还在L0s,你需要等它醒来才能响应。
我记得有一个NVMe RAID卡的调试场景:两个NVMe盘共用一条PCIe链路,一个盘在做大量读操作,另一个盘空闲进入了L0s。结果空闲盘的读方向上突然收到请求,需要先退出L0s再响应,引入了额外的访问延迟。
L0s与L1的联动
有些芯片组会把L0s作为进入L1的“前置条件”,也就是必须先进入L0s才能进入L1。这样做的好处是确保链路在进入更深休眠前已经处于稳定状态。但这也会让L1的进入时间变长。
我曾经在一个Gen3的存储服务器主板上遇到过L0s和L1冲突的问题:L0s Entry Timer太短,导致链路频繁进出L0s,结果G1根本进不去,功耗下不来。后来我把Entry Timer调整到24UI,同时把L1的进入条件改为“L0s持续100μs以上”,问题就解决了。
4.6 小结
L0s是整个ASPM体系中最容易被低估的一环。它看似简单——让链路歇几微秒再起来——但实际上,Entry Timer的配置、退出延迟的掌控、以及LTSSM中状态机的流转,都会直接影响系统的实际表现。
我个人建议你在设计初期就把L0s的行为纳入链路仿真环境中测试,而不是等到硬件投片后再去调。否则,像上面提到的NVMe性能下降、L1进入失败这些坑,你大概率也会踩一遍。