ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

UFS 3.1协议栈完全解读:分层架构与传输机制实战

UFS 3.1协议栈完全解读:分层架构与传输机制实战 很多做存储或者嵌入式底层的朋友第一次接触UFS 3.1协议栈的时候都会被JEDEC那一堆规范文档劝退。几十个章节、几百张表、各种缩写叠在一起UFS、UniPro、M-PHY、UTP、UPIU光是把这些名词对上号就得花不少时间。其实你换个角度看UFS协议栈跟咱们熟悉的TCP/IP网络协议栈走的是同一条路子分层、封装、寻址、传输控制套路都是相通的。这篇是“UFS 3.1协议分析”系列的第五章前几章聊了物理层、链路初始化和设备枚举这一章专门把整个UFS协议栈的结构、分层逻辑、传输机制和实际调试中会踩的坑串一遍适合正在做UFS驱动、固件验证、存储测试的工程师也适合刚入行想建立整体视图的朋友。1. 协议栈整体设计一次搞懂UFS各层在做什么1.1 从TCP/IP说起UFS协议栈其实不陌生我在给团队做内部培训的时候第一次讲UFS协议栈开场用的都是TCP/IP做类比。你现在看的这篇内容从手机到服务器数据经过的是应用层、传输层、网络层、链路层这条路。UFS存储访问其实也一样命令从主机软件出发经过UFS应用层、传输层、互连层最后落到物理介质上。只是这里每一层的名字不太一样数据包的称呼也不同。这种分层设计不是故意搞复杂而是工程上必须这么做。如果没有清晰的层次边界主机控制器要同时管命令语义、数据分包、链路流控、物理信号代码复杂度会爆炸也无法在不同硬件IP之间做组合。比如同一套UFS协议栈软件可以跑在支持HS-G4的硬件上也可以降级跑在HS-G1的老旧平台上同一块主控可以搭配不同厂商的UFS 3.1闪存颗粒靠的就是协议分层把上下层隔离开。这个思路跟网络协议栈把HTTP和以太网解耦是一个道理。1.2 UFS协议栈各层职责速览UFS协议栈从逻辑上可以分成这么几块我按数据流的方向列一下UFS应用层UAPUFS Application Layer负责解释命令语义。主机发过来的SCSI命令、UFS特有的Query请求、任务管理请求在这一层被解析成设备端能执行的操作。设备端的Device Manager和Task Manager就工作在这一层。UFS传输层UTPUFS Transport Protocol负责把应用层的命令和数据处理成标准格式的报文也就是UPIUUFS Protocol Information Unit。它相当于TCP层负责建立传输上下文、管理命令标签、处理数据段的分包与重组。UFS互连层UICUFS Interconnect Layer这一层由MIPI联盟的UniPro和M-PHY组成。UniPro类似IP加链路层的集合负责把UPIU封装成UniPro帧处理流控、重传、链路错误恢复M-PHY则是纯粹的物理层负责高速差分信号的收发。主机控制器层在主机侧还有一个UFS Host ControllerUFSHC它通过UFSHCI寄存器接口跟CPU侧软件交互提供命令队列、中断、数据传输描述符等硬件加速能力。这里有几个容易混淆的点。很多人问UFS协议栈是不是就等于UniPro加上M-PHY严格来说不完全对。UniPro和M-PHY属于UIC层负责“怎么把数据可靠地送过去”但是“送的是什么”“怎么组织命令”“怎么处理设备端的错误”这些是UTP层和应用层的事。调试的时候如果只盯着物理层或者只盯着驱动经常漏掉中间传输层的问题这是我见过最多的排查误区。1.3 各层之间的“翻译”是怎么完成的数据在协议栈里是逐层封装的。主机软件发起一个读命令应用层先构造一个16字节的CDBCommand Descriptor Block这个CDB会被放进Command UPIU的payload区域。然后传输层给这个UPIU加上头部头部里有命令标签、逻辑单元号、预期数据传输方向等信息。接着这个UPIU会被交给UniPro层UniPro再把它当成自己的服务数据单元SDU加上帧头、设备标识、流控信息封装成UniPro帧。最后M-PHY把帧变成差分信号在链路上跑。设备端收到后是反向的逐层解封装。先由M-PHY把差分信号还原成比特流UniPro做帧校验、去重、重组得到UPIU然后传输层解析UPIU头部确定是命令还是数据最后交给应用层执行。这个过程跟TCP/IP里HTTP请求经过TCP段、IP包、以太网帧层层封装是同一个模型。理解了这一点再看UFS规范里的各种图就顺了。2. 传输层核心UTRPs与UPIUUFS的心脏2.1 UTRP怎么描述一次传输要先理解UFS协议栈必须搞明白UTRUTRAPUFS Transport Request这个概念。一次完整的命令传输主机侧需要准备一个UTR描述符这个描述符告诉主机控制器命令放在内存的哪个位置、数据缓冲区的地址和长度是多少、命令标签是多少、数据方向是读还是写。这个结构体在Linux内核的ufshci.h里叫struct utp_transfer_req_desc里面有命令描述符指针、PRDTPhysical Region Descriptor Table指针、响应UPIU缓冲区指针等字段。PRDT是后面数据搬运的关键它描述了一组物理内存区域每个条目包含地址、长度、保留位等字段。设备端DMA或者主机控制器DMA会根据PRDT把数据直接搬到对应内存不需要CPU参与。我建议对协议栈不熟的朋友先别急着看代码先在纸上画一张图命令槽Slot指向UTRDUTRD指向CDB和PRDTCDB描述“做什么”PRDT描述“数据放哪”。这个关系理清楚了后面看中断处理、看调试日志都会轻松很多。2.2 UPIU类型速查UTP层的核心产物是UPIU。UFS 3.1规范里定义了多种UPIU类型我整理了一张常用表调试的时候对照着看会方便很多UPIU类型方向用途关键字段Command UPIU主机到设备携带SCSI/UFS命令CDBLUN、CID、CDBData In UPIU设备到主机读取命令返回的数据CID、Data Segment LengthData Out UPIU主机到设备写入命令携带的数据CID、Data Segment LengthResponse UPIU设备到主机命令执行完成后的状态返回CID、Response、Sense DataReady To Transfer UPIU设备到主机写命令时设备通知主机可以发送数据CID、Expected Data LengthTask Management Request UPIU主机到设备中止任务、查询队列状态等TMC、LUN、CIDTask Management Response UPIU设备到主机任务管理请求的响应TMC、ResponseQuery Request UPIU主机到设备访问描述符、标志、属性设备配置OPCODE、Descriptor IDQuery Response UPIU设备到主机Query请求响应OPCODE、Descriptor DataNOP Out/In UPIU双向链路保活、确认机制CID注意看Data In和Data Out UPIU里的CID字段它是命令标签用来把数据和具体命令关联起来。UFS允许主机同时下发多条命令靠CID区分返回的数据属于哪一条命令这一点跟网络协议里的端口号作用很像。2.3 CID、队列入口与命令生命周期UFS主机控制器一般支持几十个命令槽位具体数量看HCS.DDDevice Descriptors字段和UTRLDBR寄存器中配置的槽位深度。Linux内核默认UFS命令队列深度是32也就是最多同时有32条命令在飞行。命令生命周期大概是这样的驱动选择一个空闲槽位填充UTRD写UTRLDBR寄存器对应的位来提交命令硬件主机控制器看到Doorbell寄存器被置位后就会代表软件去发起UTP传输请求。设备端执行完命令后返回Response UPIU配合一起返回的还有命令完成状态。这里有个容易踩的坑主机软件在提交命令前必须确保UTRD里的所有字段是同步到内存的尤其要注意DMA一致性否则硬件读到的可能是旧数据。我之前调试一个偶发命令超时问题查了三天最后发现是PRDT的地址没有对齐到4字节边界导致DMA搬运数据时出错。这类问题在协议栈层面几乎不会暴露但是实实在在影响可靠性。3. 一条读写命令的完整旅程3.1 主机提交命令从内存到Doorbell咱们跟着一条读命令走一遍完整流程。假设用户态程序发起read()访问某个块设备上的文件层层下来到UFS驱动驱动会构造一个Block Request。第一步分配命令槽位。驱动从命令队列池里拿一个空闲的UTRD填充CDB比如READ(16)命令里面包含起始LBA和传输长度。同时把用于接收数据的DMA缓冲区地址填进PRDT每个PRD定义一个物理连续区域如果缓冲区不连续就填多个PRD条目。第二步刷新缓存并提交。这里要注意大多数架构需要先执行内存屏障确保UTRD和PRDT在内存中可见然后写UTRLDBR寄存器置位对应槽位这一步叫“Ring the Doorbell”。主机控制器看到Doorbell位被置位开始从内存搬UTRD解析CDB和PRDT发起传输。第三步发送Command UPIU。主机控制器通过UniPro把Command UPIU发送到设备侧。设备端的UniPro层完成帧校验和重组后把UPIU传给设备传输层设备传输层检查CID、LUN然后把CDB交给设备应用层比如UFS Device Server执行。3.2 设备执行与数据搬运Data In/Out的流动在读命令场景下设备端收到Command UPIU后闪存控制器开始从NAND介质读取数据。数据准备好以后设备通过Data In UPIU把数据发回主机。这里有个关键点如果数据量比较大数据会被分成多个UPIU分片发送每个分片有独立的序列号主机控制器负责按序重组。重组逻辑在UniPro层和UTP层都有UniPro层负责帧级的有序交付UTP层负责把数据正确放到PRDT对应的内存位置。写命令的逻辑稍微不同。主机先把Command UPIU发过去设备端收到命令后不是立刻接收数据而是先检查自己的缓冲区资源。设备如果准备好了会先发一个Ready To Transfer UPIU简称RTT UPIU给主机主机收到RTT后才会把Data Out UPIU发过去。这个机制很容易被人忽略很多人抓包发现发完命令以后链路半天没动静其实是在等RTT。RTT机制的背后原因是写缓冲区的资源限制。闪存介质不能无限接收数据设备必须确认缓冲区足够才会让主机发数据不然可能出现缓冲区溢出。这一点跟网络协议里的TCP滑动窗口非常像。3.3 实测心得抓一次UFS读写需要看什么如果你手上有逻辑分析仪或者支持UFS的协议分析仪我建议这样操作让设备空跑一次简单的4KB读命令抓下来重点看链路层的帧序列。你大概会看到这样一串东西先是Command UPIU然后可能有一个或者多个Data In UPIU最后是Response UPIU。如果链路协商了HS-G4速率实际抓包的速度很快一帧只有几百纳秒级别分析仪的时间戳分辨率必须足够高。没有硬件分析仪的时候可以用主控厂商提供的寄存器调试接口读取UTRLCNR、UTRLLU这些状态寄存器结合内核的trace_printk拿到命令生命周期的时间戳。我平时排性能问题就是用这套组合拳先确认命令在哪个阶段耗时最长再针对性优化。比如一次随机读如果耗费时间长是卡在Device Manager处理还是卡在DMA搬运从协议层看一目了然。4. 常见问题与排查技巧实录4.1 命令超时与CID异常先说一个高频问题命令提交后长时间没有Response UPIU主机侧报命令超时。这时候不要一上来就怀疑闪存坏了先按这个顺序排查。先确认UTRLDBR的状态看命令槽位是否还处于占用状态再检查UTRLCNR里的错误标志有没有Link Lost或者NACK然后用寄存器dump看设备是不是根本就没收到Command UPIU。如果链路层一直重传同一个帧往往说明设备侧没有正常响应要怀疑设备固件跑飞了。还有一种情况是CID冲突。如果驱动在命令还没有完成时错误地复用了同一个命令标签设备端会认为这是同一条命令导致响应错乱。这个问题在压力测试中比较常见比如高频插拔、热复位场景。内核日志里如果看到类似ufs 0: Error: cid mismatch的报错优先查驱动槽位分配逻辑。4.2 链路层故障UniPro Error与重传UFS链路比想象中脆弱。信号完整性问题、电源纹波、温度变化都可能导致M-PHY上的数据出错。UniPro层的可靠性机制会做错误检测和重传但这并不能完全掩盖问题错误重传率高的时候表现为随机读延迟抖动大、吞吐量下降。我遇到过一个case设备在高温环境下跑压力测试每过一段时间就会出现一次L0到L1的链路降速然后再慢慢升回HS-G4。最开始以为是设备过热保护后来抓了M-PHY信号才发现是接收端眼图裕量不足导致链路错误率升高UniPro触发重传重传累积到阈值后链路自动降速重训练。遇到这种问题协议栈本身没有“错”问题出在硬件设计和PCB布线但你不理解协议栈就很难定位到这里。排查链路问题有两个常用手段一是读取UniPro的PHY Adapter状态寄存器看最近的错误计数和重传统计二是在分析仪上抓PHY层信号做眼图测试。前者适合快速判断链路健康度后者适合深入分析信号质量。4.3 电源模式切换踩坑UFS支持多种电源模式PWM模式、HS模式、休眠模式。如果驱动在处理挂起/恢复时没有按照规范顺序切电源模式很容易导致链路卡死或者设备无响应。我见过最典型的错误是系统进入suspend驱动把链路切到休眠模式resume的时候直接发送HS模式切换命令跳过了PWM-G2。设备固件在状态机上不接受这种跳变直接返回错误导致链路协商失败后面所有命令全部超时。正确做法是需要先回PWM模式做参数重协商再切换到HS模式所有状态跳转都要在设备支持的合法性表里。另外一个细节是切换电源模式之前要确保所有在途命令已经完成否则设备和主机对链路状态的认知会不一致。UFS规范里定义了完整的电源模式变更流程看起来只是几个命令的先后问题但实际工程里最常见的挂死都出在这里。4.4 排查工具与工作方法做UFS协议栈调试我这里列几个实用工具组合协议分析仪能完整抓到UPIU层和UniPro层的交互适合定位协议交互问题。选型时注意是否支持HS-G4速率、能否长时间抓取、有没有自动解码。寄存器级调试Trace32等适合做寄存器读写、内存dump、脚本自动化。排查驱动bug时效率很高可以直接查看UTRD和PRDT内容。内核动态追踪ftrace/perf/kprobesLinux环境下排查驱动问题时离不开可以拿到命令提交到完成的内核函数时间线。特别是看中断上下文的耗时、锁竞争这类问题用这类工具比看printk日志直观得多。厂商诊断工具大部分主控厂商会提供UFS调试工具能直接读设备的内部状态比如逻辑单元状态、写缓冲区水位、坏块管理状态。遇到设备侧问题用厂商工具拿到的信息比规范文档描述的要更贴近实际。日常排查我习惯按“先主机侧、再链路侧、最后设备侧”的顺序来。先用主机控制器的寄存器确认命令是否提交成功再看链路层的重传率、错误计数最后用设备诊断口确认设备是否收到命令、执行状态如何。这条思路能过滤掉大部分干扰因素也不会在排查过程中反复改代码浪费时间。5. 从我这几年的调试经验看UFS协议栈最具技巧性的地方不在某一层本身而在层与层之间的接口如果你只是照着JEDEC规范去理解每一层单独的逻辑看的时候觉得都懂一到实际联调就会发现问题全都发生在边界处。比如UTP层认为数据包已经交给UniPro了但UniPro因为缓冲区不足做了背压导致UPIU迟迟没有发出再比如设备应用层已经在执行命令了但链路层在重传之前的帧主机侧看到的是命令一直在途。类似这种“层与层之间互相等待”的死锁场景在低功耗模式和热插拔场景下尤其频繁。我的建议是阅读协议栈时不要只背诵各层功能而是每看完一层就问自己一个问题这一层接收来自上一层的什么数据交给下一层的时候需要满足什么约束条件把每个接口的握手条件、缓冲能力、错误处理路径都记下来调试的时候收益会很明显。另外一定要在实验室里亲手抓几次真实波形跑几次异常注入测试比如在压力测试中随机做链路复位、电源模式切换、队列满上报这些场景比正常读写路径更容易逼出协议栈的边界问题也能让你对协议的理解更立体。这个系列后续如果再更新我会接着写UFS 3.1的命令队列调度、写缓冲WriteBooster机制以及HPB这类特性对协议栈实现的影响。你在实际调试中遇到过什么奇葩协议栈问题欢迎在评论区一起聊聊排查思路互相补盲区。
返回列表