这篇是UFS3.1协议学习系列的第六、七篇。前面我们聊过UFS的基本架构、设备枚举和命令集初始化,今天把协议栈里最劝退的两块内容拆开揉碎讲清楚:第6章UTP(UFS Transport Protocol)是命令和数据怎么封装成UPIU并在主机与设备之间传递;第7章UIC(UFS Interconnect)是这些UPIU到底怎么通过MIPI M-PHY和UniPro在物理链路上跑起来。如果你和我一样,读JEDEC JESD220E规范时在第6章和第7章反复卡住,这篇就是为正被位域、Gear、CPort折磨的人写的。读完之后,你再去看厂商的trace、抓协议分析仪波形,脑子里会立刻浮现该看哪个字段、该查哪一层。
1. 先打通整个UFS协议栈:别一上来就啃UPIU
1.1 UFS三层协议架构
UFS的软件架构和网络协议栈很像,从上到下可以分为三层:应用层(UFS Command Set,UCS)、传输层(UTP)、互连层(UIC)。应用层负责解释和执行命令,比如READ/WRITE、INQUIRY、START STOP UNIT等,这些本质上都是SCSI命令的裁剪版;传输层负责把命令、数据、状态打包成标准格式的UPIU(UFS Protocol Information Unit);互连层负责把UPIU变成位流,通过物理信号发送出去,并保证对端能正确接收。
把这三层的关系类比成快递发货:应用层决定“要寄什么东西”(命令内容),传输层把东西装进标准纸箱并贴好面单(UPIU),互连层是货车和道路,负责把箱子运到目的地。你在驱动代码里看到的ufshcd_send_command实际上是站在传输层的视角,而寄存器、PHY配置则属于互连层。很多调试问题之所以难定位,就是因为问题发生在某一层,症状却出现在另一层。
1.2 UTP和UIC各自管什么
UTP的核心产物是UPIU,它类似网络协议里的TCP段。所有主机发给设备的请求,以及设备返回主机的响应,都以UPIU为单位。UPIU有固定32字节的头部,后面可以跟可选头(EHS)和数据段。UTP不关心底层是M-PHY还是别的物理层,它只负责“内容对不对、格式对不对”。
UIC则完全不同,它关心的是“怎么传”。UIC由MIPI联盟定义的M-PHY物理层和UniPro协议栈组成。M-PHY定义电气特性、时钟、差分信号、Gear速率和PWM/HS模式;UniPro负责链路启动、数据传输、流控、重传,相当于把“TCP/IP”能力搬到了芯片互联场景。UIC之上还有个DME(Device Management Entity),它用来配置M-PHY和UniPro的参数,比如当前链路速率、激活的Lane数、是否开启CRC校验。
1.3 学习顺序建议
初学者最常犯的错是直接啃UPIU结构或者直接看M-PHY速率表,结果越看越懵。我的建议是先建立“命令—UPIU—物理链路”的流水线认知,再逐个细节突破。具体顺序:先搞懂UCS层的几个核心命令(READ、WRITE、QUERY,其中QUERY用于访问描述符和属性),再理解UTP层描述的完整命令交互流程,最后才去看UIC层的链路启动和Gear协商。这样当你看到协议分析仪抓到的UPIU时,才能明白它属于哪一步、下一步该出现什么。
2. UTP传输层:命令是怎么变成UPIU的
2.1 UPIU头部字段逐位看
一个标准UPIU头部固定32字节。头部的核心字段包括:
| 字段 | 作用 | 调试时怎么看 |
|---|---|---|
| Transaction Type | 指明UPIU类型,比如命令、数据、响应、任务管理 | 抓包第一眼先看它 |
| Flags | 方向、EHS标志、是否带数据段等 | 确认数据方向 |
| LUN | 逻辑单元号,0~31 | 确认命令发给哪个LUN |
| Task Tag | 命令标识,用于并发和多命令匹配 | 超高并发时检查是否会重复 |
| Initiator ID | 发起端标识,主机侧通常为0 | 多主机场景少,一般忽略 |
| Command Type | 区分是SCSI命令还是UFS原生命令 | 判断走的是哪套命令集 |
| Expected Data Transfer Length | 主机期望的总数据长度 | 和DATA段长度对比可查超时 |
| Data Segment Length | 当前UPIU携带的数据字节数 | 配合EDTL定位丢包 |
| Data Unit Number | 数据起始编号,用于校验连续性和重传 | 重排序时特别有用 |
很多新手看到这些字段觉得复杂,其实只需要抓住三个:Transaction Type决定“这条UPIU是什么角色”;Task Tag决定“它属于哪个命令”;EDTL(Expected Data Transfer Length)决定“这次传输总共该有多少数据”。只要这三个对得上,大概率命令流程不会乱。
2.2 常见的UPIU类型:不止COMMAND和RESPONSE
UFS协议里常用UPIU类型有下面这些,建议记成一张速查表:
| UPIU类型 | 方向 | 用途 |
|---|---|---|
| NOP_OUT / NOP_IN | 主机→设备 / 设备→主机 | 链路保活,类似ping |
| COMMAND | 主机→设备 | 携带SCSI或UFS命令的CDB |
| RESPONSE | 设备→主机 | 返回SCSI状态、Sense信息、任务管理结果 |
| DATA_OUT | 主机→设备 | 写数据 |
| DATA_IN | 设备→主机 | 读数据 |
| TASK MANAGEMENT REQUEST | 主机→设备 | 中止任务、逻辑单元复位等 |
| TASK MANAGEMENT RESPONSE | 设备→主机 | 返回任务管理结果 |
| QUERY REQUEST / RESPONSE | 主机→设备 / 设备→主机 | 访问描述符、属性、标志,以及DME命令 |
| REJECT | 设备→主机 | 通知主机UPIU非法或不被支持 |
这里最容易忽略的是QUERY UPIU。UFS没有专门的“寄存器”命令,设备描述符、设备属性、标志位全都靠QUERY请求来读写。驱动初始化时你会看到一串QUERY REQUEST,读设备描述符、几何描述符、能力描述符,这些都是在UTP层完成的。
2.3 用一次READ命令串一遍UTP流程
咱们拿一次最简单的16KB读操作来看完整UTP交互,假设LUN=0,LBA=0x1000。主机侧在软件里构造一个READ(10)命令,CDB里填好LBA和传输长度,然后封装成COMMAND UPIU发送。这个UPIU的Transaction Type是0x01(COMMAND),Task Tag被主机分配为比如0x0A,EDTL填16384。设备收到后,因为这是读操作,会拆成4个DATA_IN UPIU(每个带4KB数据)返回给主机,每个DATA_IN UPIU的Task Tag都是0x0A,Data Segment Length都是4096。数据全部发完后,设备再回一个RESPONSE UPIU,里面携带SCSI状态GOOD。
实际上设备不一定会严格按“数据完后响应”的顺序,也可能先回RESPONSE再回DATA IN,这取决于设备的实现。所以主机驱动不能傻等响应,而应该根据Task Tag分别匹配数据和状态。这也是为什么Task Tag必须严格唯一,如果两个命令共用一个Tag,重排序场景下数据归属就会错乱。
2.4 UTP字段的几个坑
踩过UTP的坑后,我列几个最常见的问题。第一,EDTL填错:很多驱动用blk_rq_bytes直接填,但没考虑分片,导致设备发现EDTL和实际数据长度不一致,直接报错或挂起。建议用块层的传输字节数而不是请求总长度。第二,Data Segment Length的字节序:UPIU头部里的长度字段都是大端序,如果你用cpu_to_be16习惯性转成小端,设备解析出来的长度会非常离谱。第三,LUN范围:UFS规范中LUN只有4位,有效值0到7,但部分厂商设备支持扩展LUN(在额外字段里用EHS表示),如果你只在标准字段填LUN,高位会丢。第四,NOP_OUT是调通UTP最快的办法:驱动初始化互相扯皮时,先发一个NOP_OUT,如果能收到NOP_IN,说明UTP基本通路是通的,再往下查命令层。
3. UIC互连层:M-PHY和UniPro如何把比特送到对方
3.1 DME:设备管理实体
UIC并不是单纯的一根线,它内部有一套完整的管理体系。DME就是这套体系里管配置的模块。你可以把DME理解成设备内部的一个“控制面板”,里面挂满了属性,比如PA_ActiveTxDataLanes、PA_ActiveRxDataLanes、PA_TxGear、PA_RxGear等。主机要改链路配置,不能直接写寄存器,而是通过UTP层发一个QUERY REQUEST,在UPIU的数据段里携带DME_GET或DME_SET的命令,让设备内部的DME去执行。
DME的属性和UniPro/M-PHY强相关。比如你想知道当前链路跑在HS-Gear3还是HS-Gear4,就发一个DME_GET去读PA_RxGear。想切换链路的Gear,就先DME_SET写新值,再把PHY重新适配。调试时看链路协商失败,第一件事就是确认DME操作有没有正常返回。DME属性读出来的值往往很直接:0表示PWM-G1,1表示PWM-G2,3表示HS-G1,5表示HS-G3,6表示HS-G4。知道了这个映射,你就不用靠猜去看链路速率了。
3.2 M-PHY物理层:PWM和HS模式的选择
M-PHY是MIPI联盟定义的物理层标准,它有两种工作模式。PWM模式用低速、低功耗的方波信号,适合设备空闲或待机;HS模式用高速差分信号,适合大数据吞吐。每个模式下面又有多个Gear级别,相当于档位。PWM-G1到PWM-G7速度逐步提升,HS-G1到HS-G4才是真正跑满UFS3.1性能的档位。UFS3.0引入了HS-Gear3,UFS3.1在消费电子场景常见的是HS-Gear3和HS-Gear4。理论上一条lane在HS-Gear4大约11.6Gbps,UFS最多支持两条发送lane和两条接收lane,所以双lane合计最高可以到23.2Gbps左右。
为什么搞这么多档位?因为链路并不是永远全速跑。手机待机时需要省电,主机会通过DME把M-PHY降到PWM模式,甚至进入休眠;一旦有读写请求,再通过唤醒流程升到HS模式。这个切换过程如果时序没处理好,常见现象就是设备第一次读特别慢,或者链路起来后频繁CRC Error。我调试过的项目里,至少有一半的“性能差”问题出在Gear切换策略上,而不是主控或Flash本身。
3.3 UniPro协议栈:链路启动和流量控制
UniPro位于M-PHY之上,可以看成一个小型网络协议栈,包含物理适配层、数据链路层、网络层和传输层。物理适配层把UPIU映射成UniPro的帧;数据链路层负责成帧、CRC校验、重传和流控;网络层处理不同CPort之间的路由;传输层保证端到端可靠传递。
链路启动是UniPro里最壮观也最容易出错的过程。当UFS设备上电或从休眠唤醒时,主机会发起Link Startup。这期间双方先以最低速PWM模式交换一轮训练序列,然后逐步协商到双方都支持的更高Gear和Lane数。这个过程就像两个人打电话,先互相“喂喂”确认通话质量,再切到高清语音。协议里涉及START、END、TRAFFIC等多种序列,以及时序参数PA_TActivateTime等。如果链路启动失败,大概率不是硬件完全坏,而是其中一方没有在超时时间内回复。这时优先去查DME属性里的PA_Error和链路状态机,比直接换主板更有用。
UniPro的流控机制非常关键。它采用基于Credit的流控,接收方会告诉发送方自己还有多少缓冲区可用。如果发送方缓冲区被写满,它就不会再发数据,直到对端释放Credit。很多吞吐量异常的案例,不是因为物理层不稳定,而是Credit配置太小,导致链路永远跑不满。修改UniPro缓冲区大小是通过DME属性配置的,具体属性名通常是DataLane0 Credit或Local Rx Buffer这一类。遇到速率忽高忽低,别急着换Gear,先把Credit加大试试。
3.4 为什么UFS选M-PHY而不是C-PHY
同为MIPI家族的C-PHY和D-PHY大家可能更眼熟,因为手机摄像头接口用的就是D-PHY和C-PHY。但UFS没有选它们,主要原因有三个。第一,M-PHY天然是为存储类双向高带宽设计的,它提供独立的发送lane和接收lane,读和写可以同时进行,而D-PHY/C-PHY更多是单向视频流场景。第二,M-PHY支持从极低功耗PWM到高速HS的宽范围,适配存储设备长时间待机和突发高吞吐的两种极端需求。第三,M-PHY的差分信令在抗干扰和功耗控制上更适合板级连接。C-PHY在三线制编码、无独立时钟上有优势,但代价是复杂度和功耗,在存储领域并不合算。
你如果去看手机主板,UFS芯片和SoC之间的走线通常就是一对用于发送、一对用于接收的差分线,外加时钟、复位和电源。走线短、阻抗匹配好,才能让M-PHY的HS-Gear4稳定跑起来。PCB设计时,差分对间距、参考地完整性、过孔数量都会直接影响眼图质量。这也是为什么明明主控和设备都支持UFS3.1,但跑到高速时忽然读写速率掉一半——大概率是信号质量问题,不是代码问题。
4. UFS3.1新增特性与UTP/UIC的关联
4.1 HS-Gear4速率支持
UFS3.1规范一个明显变化是正式支持HS-Gear4,把单lane理论速率拉到11.6Gbps量级。这个速率带来的影响不只是数字好看,它对M-PHY的物理层设计和UniPro的重传机制都提出了更高要求。速率越高,信号的建立和保持时间窗口越窄,对时钟抖动和PCB损耗更敏感。对开发者而言,UFS3.1设备在初始化时,要通过QUERY/UPIU读取设备能力描述符,看设备支持的最高Gear是多少,再去DME里设置主机和目标一致的Gear。强行让仅支持HS-G3的设备跑HS-G4,通常会导致Link Startup失败或大量CRC Error。这种不匹配在兼容性测试里很常见。
4.2 WriteBooster对UTP的影响
UFS3.1引入了WriteBooster,本质上是在设备内部划出一块SLC缓存,先以高速写进SLC,后台再把数据搬到TLC。这给UTP层带来的变化是:主机可以发出WriteBooster Buffer相关的QUERY命令,比如查询当前Buffer状态、手动触发Flush。如果你在调UFS3.1写性能,看到写速先快后慢是正常的,SLC缓存满了就会回到TLC原生速度。但由于WriteBooster的数据段往往很大,一旦UTP层的EDTL和数据段长度字段配合不好,很容易出现命令超时或数据段不完整。解决思路是在驱动里把大块写请求分片,而不是让设备处理超大UPIU。设备端通常对单次UPIU数据段长度有限制,查一下量产spec的MaxDataSegmentSize字段就明白了。
4.3 其他扩展:性能降级通知与休眠优化
UFS3.1还新增了一些和UTP/UIC相关的辅助机制,比如温度过高时的性能降级通知。设备可以通过异步事件或QUERY响应告诉主机“我现在很热,我会自动降速”,主机的调频调度器需要识别这种情况,不要误判是主控问题。另外UFS3.1对深睡状态做了更细的功耗优化,要求链路在唤醒时能更快完成Link Startup。这里有个实操细节:深睡唤醒慢,往往是因为UniPro的唤醒序列时序参数没有按UFS3.1规范配成最短值。设备厂商会在量产固件里把这些参数调好,但如果你在自研主控上调试,记得在DME属性里确认PA_TActivateTime和PA_Hibern8Time不是默认的保守值。
5. 实操过程与排查技巧:从初始化到压力测试
5.1 读取UFS设备的能力信息
不管做什么优化,第一步永远是确认设备真实能力。在Linux内核里,你可以通过cat /sys/block/sda/device/ufs_device_descriptor这类节点看到设备描述符的内容,包括厂商ID、型号、最大Lane数、最大Gear等。如果想看更底层的能力,用ufs-utils发QUERY命令去读几何描述符和属性。重点关注这几个值:
| 字段 | 含义 | 建议值 |
|---|---|---|
| bDeviceMaxGear | 设备支持的最高Gear | 3或4,取决于UFS3.1 |
| bDeviceMaxLanes | 最大Lane数 | 多数为2 |
| bDeviceMaxWriteBoosterCap | 写加速缓冲大小 | 几GB到十几GB |
| qTotalRawDeviceCapacity | 设备总容量 | 与你买的容量一致 |
| bNumberOfLUNs | LUN数量 | 通常1或2 |
我自己遇到过设备明明标称UFS3.1,但bDeviceMaxGear只支持到3的情况。这说明芯片用的是UFS3.1协议但物理层只做到Gear3,性能上限就锁在那了。驱动初始化时不要默认按Gear4去协商,先从描述符读出来再配参数,能省掉后面一连串链路问题。
5.2 常见问题与排查方法
把多年调试经验浓缩成一张速查表,遇到问题先对号入座。
| 现象 | 大概率原因 | 排查动作 |
|---|---|---|
| Link Startup失败 | Gear/Lane协商不一致 | 读DME属性确认双方Gear;降级到HS-G1试 |
| 链路频繁CRC Error | 信号质量差、唤醒时序不对 | 用示波器看眼图;调整PHY参数 |
| 命令超时,Task Tag一直pending | UPIU的EDTL或数据段长度错误 | 抓UTP trace,比对EDTL和DataSegLen |
| 吞吐量上不去 | Credit配置太小或Gear被降 | 增加UniPro Credit;读取当前PA_RxGear |
| 写入先快后慢 | WriteBooster SLC耗尽 | 查WriteBooster状态,不是异常 |
| 休眠唤醒后命令超时 | 深睡唤醒时序没配短 | 调整PA_Hibern8Time等属性 |
特别提醒一句:遇到“莫名其妙的性能差”,先排除链路降级。很多平台会把UFS自动降级到PWM模式,然后再也不升回HS模式。你可以持续读PA_RxGear,如果发现一直是PWM-G1,说明升级逻辑坏了。这种问题跟协议本身关系不大,但表现上很像协议错误。
5.3 抓取和分析UTP流量的经验
调UFS协议最有力的工具是协议分析仪,比如Keysight的UFS Protocol Analyzer,或者部分逻辑分析仪配合UFS解码插件。没有硬件工具时,软件trace也能看到UTP层交互,内核里开启/sys/kernel/debug/ufs下的tracepoint,可以记录UPIU发送和接收事件。抓下来的分析要点有三个:第一,看Transaction Type是否序列正常,比如COMMAND后面有没有RESPONSE,RESPONSE有没有超时;第二,看Task Tag是否在同一时间点出现重复,重复意味着驱动或设备挂了;第三,比较EDTL和所有DATA段总和,如果总和小于EDTL,一定有数据丢了。
我第一次抓UFS trace时困在一个问题上:RESPONSE UPIU明明已经收到了,但应用层还在等。最后发现RESPONSE里的Sense Key和Additional Sense Code在QUERY Request的扩展字段里,而驱动只解析了标准SCSI状态,把扩展Sense数据当噪声丢掉了。从那以后我就养成了习惯,凡是抓包分析,一定要先确认UPIU的可选EHS部分有没有内容。EHS通常用于承载超过标准头容量的信息,比如扩展LUN、厂商特定数据,忽略它等于丢掉一部分关键情报。
结尾
写了这么多,总归一句话:UFS3.1协议里UTP和UIC是相辅相成的,命令能不能跑通看UTP,跑得稳不稳快不快看UIC。我最初读协议时觉得第6章和第7章是两座孤岛,后来在调试一个深睡唤醒失败问题时才意识到,唤醒的命令传输依赖UTP格式正确,而能否及时唤醒依赖UIC的Link Startup时序。那个问题最后就是靠同时看UTP的NOP_OUT/IN和DME属性才定位到的。所以别嫌协议文档枯燥,每一章都有它存在的意义。这套协议栈你越往后调,越会发现那些当初咬牙啃下来的位域和Gear参数,最终都会变成你手里最顺手的调试工具。