4. UniPro 链路层协议:帧结构、流量控制、错误恢复

今天跟你聊聊 UniPro 链路层。说白了,它就是 UFS 设备之间通信的“交通规则”。你要是写过底层驱动,肯定跟它打过交道——帧怎么拼、流控怎么调、遇到错怎么恢复,这些弄不清楚,链路根本跑不起来。

我刚开始调试 UniPro 时,总觉得帧结构无非就是包头加数据,结果第一次板子接上,物理层说 Link Up 了,可数据就是发不出去。后来才发现,是流控方式没配对,接收端直接把帧给丢了。所以这一节,我把自己踩过的坑也带进去,帮你少走弯路。

4.1 UniPro 帧结构——拆开看看里面到底长什么样

UniPro 帧从逻辑上分两部分:帧头(Header)和数据域(Payload),尾部还有可选的 CRC。我习惯把帧结构拆成三块来记:

  • 帧头最少 4 字节,包含目标端口号、序列号、请求/响应标志、AP(Auto-Pause)流控信息等。
  • 数据域长度可变,最大 4 KB(具体受 PA_Unit 配置限制)。
  • CRC 可选(启用 QoS 时强制带 CRC)。

关键字段速记表(我项目里最常出问题的是 FSN 回绕和 FC0 设置)

字段位宽说明常见踩坑点
SeqNum(帧序列号)8 bit发送端递增,接收端用于去重与乱序重排回绕后收发两端计数不一致
AP(Auto-Pause)2 bit指示接收缓冲区水位:00=正常,01=接近满,10=已满接收端未及时响应 PAUSE 导致溢出丢帧
PortID8 bit目标逻辑端口号多端口场景下误配为广播地址
CRC16/32 bit选项帧校验,使能 L4S 时必须包含CRC 计算时漏掉帧头字段

记得有一次,我在一个多端口的控制器上调试,发现某个方向的帧总是 CRC error。查了半天,原来是 PortID 配成了 0xFF(广播),而接收端硬件不处理广播帧的 CRC,文档里那行小字我没注意到……嗯,从那以后我每次配 PortID 都会拿掩码算一遍。

4.2 流量控制——怎么让发送端不把接收端撑爆

UniPro 的流量控说白了就两种手段:PAUSE 帧和AP 回调。我个人更习惯把 AP 看成实时背压,PAUSE 帧是一次性的“命令”。

  • PAUSE 帧:发送端收到 PAUSE 后,必须停止发送普通数据帧,等待 PAUSE 解除。PAUSE 帧本身不带 CRC,所以接收端收到后必须立即生效。
  • AP(Auto-Pause):在帧头里直接捎带接收缓冲区水位信息。发送端根据 AP 值动态调整发送速率。这是一种“细粒度”流控,不需要额外帧。

避坑指南:我曾经在一个项目里只用 PAUSE 不用 AP,结果当多对一汇聚流量时,PAUSE 帧处理延迟导致缓冲区反复溢出。后来改成 AP + 对端硬件限速才稳定。我的建议是——双活场景下一定把 AP 打开,PAUSE 当作最后防线。

流量控制的实现依赖两个参数:PA_Unit(每帧最大长度)和 Freeze 计数器。你想想看,如果 PA_Unit 设得太大,一次传输占线太久,其他端口就要等。反过来设太小,帧头开销比例就上来了。我一般先跑一次流量测试,看吞吐量曲线拐点,再往回缩 10% 当作保险值。

4.3 错误恢复——链路出错了怎么办?

UniPro 链路层的错误恢复分两级:帧级重传和链路级复位。帧级重传由发送端根据 NAK 或超时触发,链路级复位在多次重传失败后由链路管理实体发起。

我画了一张流程图,帮你把整个处理路径串起来:

UniPro 链路层帧处理与错误恢复流程 ① 上层数据 ② 帧封装 加帧头 + CRC ③ 物理层发送 M-PHY 发送 ④ 接收校验 CRC / 序列号 ⑤ 错误恢复 NAK / 链路复位 校验通过 → 交付上层 重传请求 (NAK) 关键时序 - 发送后等待 ACK/NAK - 超时重传,最多 3 次 - 3 次失败 → 链路复位 - 复位后重新协商参数 重传路径:通过 NAK 帧 通知发送端重新发送

错误恢复的核心是序列号 + ACK/NAK 机制。每个帧都有一个递增的序列号,接收端校验 CRC 通过后发 ACK,CRC 失败或超时就发 NAK。发送端收到 NAK 或者超时未收到 ACK,就重传。这里有个细节:UniPro 支持选择性重传,不需要把所有帧重发,只重传丢失或损坏的那一帧。

重要提示:我见过不少驱动在实现重传时,直接把重发帧的序列号设为原始值,但接收端已经用去重表记住了这个序列号,导致后续帧被当成重复帧丢弃。正确的做法是保持序列号不变,但需要重置去重窗口的时间戳。建议对照 UniPro 协议 “Duplicate Detection” 章节的伪代码实现。

如果连续重传 3 次都失败,UniPro 链路管理实体就会启动链路复位。链路复位会清空所有缓冲区,重新进行 PA 协商。我个人习惯在复位前把错误日志拉出来——尤其是 Frame Error Count 和 CRC Error Count 寄存器,能帮你快速定位是物理层问题还是协议层问题。

// 我常用的 UniPro 状态检查代码片段(基于 UFS 驱动层)
unsigned int read_link_error_count(struct ufs_hba *hba) {
    unsigned int crc_err, frame_err;
    // 读取 UniPro 寄存器(具体偏移需参考 SoC 手册)
    crc_err = ufshcd_readl(hba, REG_UIC_ERROR_CODE) & 0xFFFF;
    frame_err = ufshcd_readl(hba, REG_UIC_ERROR_CODE + 4) & 0xFFFF;
    return crc_err + frame_err;
}

// 调用时机:每次链路复位前
if (read_link_error_count(hba) > THRESHOLD_ERROR) {
    // 记录日志,触发物理层重训练
    dev_err(hba->dev, "UniPro link errors too high, resetting PHY...\n");
    ufshcd_uic_cmd(hba, UIC_CMD_DME_RESET, 0, 0);
}

你可能会问:链路复位会不会导致上层 I/O 超时?会的,所以一般驱动会先暂停 SCSI 命令队列,等链路恢复后再重新派发。这里有个小窍门:复位后不要马上发数据,先发一个 NOP 帧确认链路稳定,我以前跳过这一步,结果复位完第一帧就丢,又触发第二次复位……死循环了。

好了,关于 UniPro 链路层的帧结构、流量控制、错误恢复,核心就是这些。你在实际调试时,可以把重点放在帧头的 AP 字段和 CRC 校验上——这两项出问题的概率最高,也最容易通过寄存器判断。