UFS 3.1协议实战:从数据通路到Write Booster性能优化
最近帮一个客户调试UFS3.1量产问题卡在写入性能上整整三天。逻辑分析仪抓出来的波形看起来没问题链路训练也过了读写命令都能正常收发可顺序写就是上不去标称值。后来翻协议文档才发现问题出在Write Booster的配置上——那个SLC缓存区域的阈值设得太保守触发回写太频繁性能全耗在搬运上了。这种坑光靠经验猜还真猜不出来必须回到协议本身找答案。这套UFS3.1协议中文学习讲解已经做到第5期了。前几期我们聊过UFS的基本概念、接口演进、器件初始化和链路层的东西本期我想换个视角不按协议文档的章节顺序走而是顺着一条真实的读写数据通路把命令怎么发、数据怎么搬、队列怎么调度、性能怎么挖这些实务问题串起来讲一遍。适合正在做UFS驱动、存储固件或者存储测试的朋友参考也适合想从整体上理解UFS协议是怎么工作的同学。我尽量少贴大段英文规范原文多用流程图式的描述和实际调试经验来说事。1. 先画一张全景图一次读操作到底经过哪些层很多刚接触UFS协议的朋友最容易犯的错就是一头扎进某个协议层的细节里出不来比如盯着M-PHY的电气参数看半天或者死抠UniPro的流控字段却不知道这一层在整个数据通路里扮演什么角色。我这里建议先建立整体感。一条完整的UFS 3.1数据通路从主机端往下数大概是这样的应用层文件系统/块设备层发起的读写请求先经过UFS驱动框架转成标准的SCSI命令这些命令被包进UPIUUFS Protocol Information Unit交给UTP层UTP层通过URTransfer Request描述符把命令和数据地址告诉控制器控制器再通过UniPro协议栈把UPIU打包成M-PHY物理层可以传输的串行信号最后是M-PHY在差分线对上做高速串行传输。往回走的方向也一样设备端的存储介质闪存把读取的数据返回给设备控制器设备控制器把数据封装成数据UPIU再经过UniPro的链路层和传输层回到主机内存。这个分层模型用个不太严谨但好理解的类比UFS协议像一套快递系统。SCSI命令是包裹上的面单写着“我要读哪个逻辑块”UPIU是标准快递箱什么样的内容都按这个箱子封装UniPro是运输网络负责在链路层做路由和可靠投递M-PHY是拉货的重卡决定运输速度上限。为什么要分层因为每一层解决不同的问题。M-PHY只关心信号怎么在物理线上传得又快又稳不关心传的是什么内容UniPro负责把不可靠的物理信号变成可靠的字节流做的是纠错和重传UTP关心的是主机的命令队列怎么组织、数据DMA到哪块内存而UFS应用层UFS Application LayerUAL则负责把SCSI命令映射成UFS设备能执行的操作。有了这种分层任何一个环节升级都只影响相邻层协议演进才可能向前兼容。UFS3.1协议里的关键特性比如HS-G4高速模式、Write Booster、DeepSleep省电、HPB主机性能增强本质上都是落在不同协议层上的优化手段。有的是改物理层速率有的是改命令调度策略有的是改设备端缓存管理理解这一点你就知道排查问题的时候该去哪一层找原因。2. UPIU格式与命令下发主机到底给设备发了什么2.1 一个标准的命令UPIU长什么样命令下发是UFS通信的第一步。主机想读某个LBA逻辑块地址先要构造一个命令UPIU。这个UPIU整个UFS协议里最核心的报文结构没有之一。不管你是做驱动还是做固件早晚要跟它打交道。一个命令UPIU分为几段。最前面是基本的Header字段包括Transaction Type标识这是一个命令UPIUtype0x01还是数据UPIU0x02、响应UPIU0x03等。这一项相当于快递箱上的“货物类型”标签。Flags携带一些特殊标志位比如是否要求设备在部分完成时返回状态。LUNLogical Unit Number目标逻辑单元号。UFS设备可以分成多个LU比如把系统分区、用户数据分区、缓存分区放在不同LUN上。Task Tag任务标签8位用于关联命令和对应的响应。这相当于快递单号主机和设备靠它对得上号。Expected Data Transfer Length期望传输的字节数告诉设备这次操作涉及多少数据。紧随Header之后的是CDBCommand Descriptor Block也就是SCSI命令本身。以最常见的READ(10)命令为例CDB里包含操作码0x28、LBA起始地址、传输块数等信息。UFS设备内部实际上跑着一套SCSI命令处理器发送给UFS设备的命令本质上就是SCSI命令这是很多做嵌入式开发的朋友一开始容易忽略的——UFS不是一个全新的命令体系它在命令层跟经典的SCSI是兼容的。一个命令UPIU里还可能有额外的Segment比如写命令要携带将要写入的数据。但这里有个细节UFS协议里写数据不一定要跟命令UPIU一起走。设备可以通过数据UPIU单独接收写入的数据命令UPIU和后续的数据UPIU用Task Tag关联。对驱动来说使用哪种方式通常由实现决定但绝大多数情况下写命令伴随的数据是通过独立的数据UPIU分段传输的。2.2 响应UPIU与命令完成当设备执行完命令会返回一个响应UPIU。响应UPIU里最重要的字段是Response0x01代表成功非0值代表任务管理和错误信息和Sense Data感知数据即SCSI的状态信息。这里要单独提醒一个坑响应UPIU返回成功并不代表数据一定正确落盘了。在开了Write Booster或设备有写缓存的情况下命令完成可能意味着数据进了SLC缓存而不是真正写入了TLC/QLC主存储区。这在后面第4节我会专门展开讲。响应UPIU还包含一个Status字段用来标识SCSI命令的执行状态Good、Check Condition、Busy等。如果设备返回Check Condition主机就要再发一个REQUEST SENSE命令去获取详细的错误信息。这种“二次确认”的交互逻辑跟SCSI一模一样习惯了SATA/NVMe的朋友可能需要适应一下。我再补充一个实操中经常遇到的现象设备返回Busy状态时驱动不能立刻重发命令必须等设备通过Unit Attention机制通知主机“我准备好了”。很多初学UFS的朋友一看到Busy就直接重试结果加重设备负载反而拖慢恢复。正确做法是读取设备的状态寄存器确认设备退出了忙状态再重发。3. 队列调度与数据传输SQ/CQ和PRD的配合3.1 命令队列的入口Submit Queue和Completion Queue如果只有一个命令在跑UFS的性能优势根本发挥不出来。UFS3.1引入的是多命令并行机制主机可以同时下发最多32个命令UFS 3.1规格里队列深度为32设备端异步处理。这就要用到队列机制。协议里定义了两类关键队列Submit QueueSQ提交队列和Completion QueueCQ完成队列。SQ是主机用来存放待执行命令UR描述符的队列CQ是设备用来存放命令完成信息的队列。两个队列都不是设备内部的概念而是通过主机内存中的环形缓冲区实现的。驱动的工作流程大致是把命令UR写入主机的SQ中SQ的具体地址通过UFS控制器寄存器配置。写Doorbell寄存器门铃寄存器相当于告诉设备“我有新命令了快来看看”。设备收到门铃从SQ中取出命令开始执行。设备执行完把完成信息写入CQ并更新CQ的门铃或者直接产生中断。宿主收到中断后从CQ中读出完成信息释放对应的命令槽位。这里有几个设计细节值得说道。Doorbell寄存器是整个队列机制的核心主机每写一次Doorbell就表示SQ中新增了多少条待处理命令。设备侧通过CQ的Doorbell来判断主机是否已经取走了完成记录。两边各管各的谁都不直接写对方的队列内存这避免了主机和设备同时访问同一块内存的竞争问题。另一个细节是UFS的命令完成不一定每次都触发中断。UFS支持中断聚合Interrupt Aggregation机制也就是多个命令完成之后才产生一次中断减少CPU被打断的次数。这个机制在NVMe协议里叫Interrupt Coalescing思路一样。对高IOPS场景来说中断聚合能明显降低CPU占用但副作用是单命令延迟会变高所以实时性敏感的应用要谨慎开启。3.2 PRD表数据缓冲区怎么描述命令队列解决的是“命令怎么排队”但还没解决“数据怎么搬运”。一次读命令设备要往主机内存的哪个地址塞数据一次写命令设备从哪个地址取数据这个地址信息就靠PRDPhysical Region Descriptor来描述。每个PRD描述一段物理连续的内存区域里面包含起始物理地址和数据长度。一次读或写操作的数据缓冲区经常不是一段连续的物理内存可能散在多处——比如文件系统页缓存里的一堆离散页。驱动需要组织一份PRD表PRD List把所有这些不连续的内存段首尾相接描述出来。设备拿到这个列表之后就能通过DMA直接读写这些内存区域不需要主机先拷贝合并。PRD表在数据UPIU里通过数据段描述符引用。构造PRD表时最容易出的问题有两个地址没按对齐要求处理。UFS设备做DMA传输通常有对齐要求比如32字节对齐。如果PRD里的起始地址不对齐设备可能直接报错或者在某些实现下性能骤降。PRD表跨越了内存页边界但物理地址不连续。宿主驱动需要确保PRD表本身在物理内存中是连续的否则设备访问PRD表时可能读到错误数据。很多UFS控制器的PRD表大小有上限例如最多支持某个数量的PRD条目大块数据传输时还要考虑拆分成多个传输请求。3.3 流控机制别把设备“灌满”了才想起刹车队列深度32数据通路可以并行那是不是主机可以把一大坨数据疯狂往设备里灌当然不是。UFS协议在UTP层有流控机制关键是设备端每个LUN都有收发缓冲区大小的限制。设备通过READY TO TRANSFER UPIURTT来控制主机的数据发送节奏。以写操作为例主机不能发了命令UPIU之后就自动把大数据UPIU连续发给设备必须等设备返回RTT告诉主机“我这边的数据缓冲区准备好了你可以发这么多数据”。这个机制很像网络协议里的滑动窗口。初调UFS驱动的人经常遇到的怪现象是写性能远低于预期命令队列明明深但设备老是发RTT催数据。仔细观察会发现要么是PRD表组织得太碎导致设备每接收一小段就要处理一次要么是主机端响应RTT的速度太慢DMA启动有较大延迟。前者看驱动的缓冲区分配策略后者就要优化DMA描述符的预置逻辑把下一段数据的DMA提前准备好等RTT一到立刻启动搬运。读方向同理。设备返回的数据UPIU不能无限发主机端必须有足够的缓冲区来接收。如果主机的接收缓冲区不足协议里设计了OVERFLOW标志设备发现宿主缓冲区容纳不下时要么报错要么按宿主的实际能力缩小传输长度。这块在使用小的数据缓冲池做读测试时特别容易踩到——明明读命令没问题数据就是不对抓包发现是宿主端缓冲区长度字段搞错了。4. Write Booster和性能优化把3.1版本吃透的关键4.1 SLC缓存不是什么高深魔法UFS3.1在性能上最核心的卖点就是Write Booster。本质很简单TLC或QLC闪存直接写入慢但SLC写入快。Write Booster就是把一部分闪存空间配置成SLC模式的伪缓存区先把数据快速接住再在后台慢慢搬到TLC区。理解Write Booster的运作需要明白几个关键配置项WriteBoosterBufferSizeSLC缓存区的大小以LUN为单位配置。太小区间大流量写入很快就把缓存打满性能断崖太大则浪费了主存储区空间用户显性容量变小。WriteBoosterBufferLifeTimeSLC缓存区的擦写寿命占比。SLC模式擦写寿命比TLC模式高很多但既然这块区域用来频繁写入就需要监控它的损耗程度。WriteBoosterBufferFlushThreshold触发后台回写的阈值。当SLC缓存区的数据占用超过这个百分比设备启动将数据搬移到TLC区。这个阈值设得太小回写频繁性能波动大设得太大缓存容易写满写满之后主机会被强制等待回写完成这也就是用户感知到的“掉速”。AvailableSLCMemory当前SLC缓存区剩余可写空间。这个值是可以实时读取的做性能监控和状态排查很有用。这些参数都通过Mode Select/Mode Sense命令读写具体字段在UFS 3.1规格书的Write Booster章节里有详细定义。驱动在初始化的时候要主动查询设备支持不支持Write Booster支持的话再配置使能。4.2 回写带来的性能锯齿和排查方式开了Write Booster顺序写的性能曲线往往是锯齿状——前段飙高速中段稳定后段突然掉下来过一阵子又恢复。这是SLC缓存满了之后设备边接收边回写的典型表现。真正的性能问题排查不能只看平均速度要看瞬时速度和缓存水位。我常用的办法测试顺序写的时候周期性读AvailableSLCMemory把性能曲线和缓存水位画在同一个时间轴上。如果掉速的时刻刚好是缓存水位冲到阈值附近几乎可以断定是回写策略太激进如果掉速时缓存还很空那问题可能出在设备固件的垃圾回收策略上。还有一个容易忽略的配置当系统有多个LUN时Write Booster是per-LUN配置的。比如数据分区开了Write Booster系统分区没有主机往不同分区写入时走的路径完全不同性能特征也完全不同。性能测试之前先确认目标分区到底有没有启用Write Booster这是最基础但最常踩的坑。另外提醒一点Write Booster的SLC缓存不是永久保存数据的地方。断电时SLC缓存里的数据需要保证一致性设备需要有能力在下次上电时做恢复。UFS3.1协议里有专门的掉电处理机制固件侧要保证回写过程中突然掉电不会丢数据。做硬件测试的朋友可以关注一下掉电测试场景观察设备对突然掉电的处理是否符合预期。4.3 HPB功能让随机读少走弯路再看一眼UFS3.1另一个协议特性HPBHost Performance Booster。它解决的是UFS 3.1时代随机读性能不够的问题。UFS设备里的闪存映射表L2P表太大设备内部SRAM装不下就只能把部分映射表放在闪存里。一次随机读如果映射表不在SRAM设备就要先进闪存读映射条目再做真正的数据读多了一次访问延迟和IOPS都被拖累。HPB的思路是主机内存大把一部分映射表信息缓存在主机侧设备做随机读时可以直接从主机拿映射信息省掉闪存读表的开销。协议通过专用的HPB命令和管理机制实现这一交互。但HPB不是免费的驱动要实现映射条目的缓存管理还得维持缓存与设备实际映射的一致性。设备端映射表更新比如垃圾回收导致物理地址变化时需要主动通知主机使相关缓存失效。任何一致性漏洞都会导致读回错误数据。所以HPB用起来比Write Booster复杂得多目前不少方案的默认策略是保守地不开HPB。如果你在做UFS性能优化我建议先跑满Write Booster的收益理顺队列深度和中断聚合的配置再考虑HPB这种高阶特性。一上来把特性全打开出了问题根本不知道是哪一层在拖后腿。5. 常见问题与排查技巧实录5.1 初始化阶段的链路训练失败现象UFS设备枚举不到或者能识别厂商信息但是读写命令发不出去。排查链路从物理层开始确认。M-PHY在速率协商阶段如果失败表现为链路训练超时。问题通常出在参考时钟频率不对、PCB差分走线阻抗不对、供电电压不稳。再往上就查UniPro层。协议规定设备在链路启动后会进入配置阶段主机需要发送特定的配置序列比如DME_SET指令来协商分频参数、流量控制参数。我见过最多的情况是主机配置的UniPro参数超出设备支持范围比如把TxFcDepth设得太大。解决办法很简单先把参数设为规格书里的默认值一条条往上加看哪项导致失败。5.2 读写命令超时与中断丢失Q命令发出去很久没有完成中断超时了怎么办A首先确认Doorbell寄存器状态。如果Doorbell里还有未清的命令说明命令还没被设备取走大概率是设备端异常需要复位设备。如果Doorbell已经清掉了但CQ没有完成记录那就查中断有没有丢。很多控制器支持中断状态寄存器驱动在超时处理里先读它能直接区分“设备没收命令”和“设备收了命令但中断丢了”两种情况。排查中断丢失时最容易忽略的是CQ的环形缓冲区头尾指针管理。须知完成队列的Head/Tail指针由不同侧维护驱动在处理完CQ条目后必须更新Doorbell把指针推进的位置告诉设备。如果驱动漏更新Doorbell设备可能认为CQ已满不再提交新的完成信息表现就是越收越慢最后完全卡死。5.3 眼看一堆协议热搜词UFS的调试工具怎么选这一阵子总看到大家在聊UART、SPI、CAN、MQTT这些协议其实它们跟UFS不是一回事UART/SPI是芯片间低速互联CAN是车上设备通信的现场总线MQTT是物联网应用层的消息协议。UFS是嵌入式存储的主机-设备接口协议调试工具也完全不同。做UFS协议分析手头至少要有逻辑分析仪或者存储协议分析仪推荐支持M-PHY和UniPro解码的型号。它能抓物理层波形还能把UPIU层的信息解析出来让你直接看到命令和响应交互过程。抓下来的转储文件重点看几个方面有没有异常的RTT节奏、有没有高频率的重传、数据UPIU的长度分布是否合理、响应UPIU里是否频繁出现Sense KeyAborted Command。如果没有协议分析仪可以退而求其次在驱动里加打印。UFS驱动框架一般都提供了跟踪点能记录命令的提交和完成时间戳。把这些时间戳整理出来画出每个命令的生命周期也能定位大部分问题。再不行就靠设备端的寄存器读取来辅助判断比如读设备健康状态、错误历史寄存器。但寄存器信息量有限到了疑难杂症阶段协议分析仪真的是必需品。6. 这套协议还有哪些可以往外延伸的方向写到本期UFS3.1的几个重点核心点基本过了一遍分层的协议栈、命令UPIU的结构、SQ/CQ队列的调度、PRD描述的数据通路、Write Booster和HPB的优化逻辑、以及用协议分析仪做排障的思路。如果顺着这个脉络继续往下走后边还能展开的内容其实不少。UFS4.0已经支持了更大的带宽和LPBLogical Block Provisioning等新特性对比着看更容易理解协议演进的逻辑UFS与NVMe在命令模型上的差异也值得做一篇专题还有UFS在车载存储和AI终端里的应用场景这些已经不只是手机的问题了。我个人在多次调试里的体会是读UFS协议文档不需要逐页背诵而是要“以问题为导向”去查、去对应。先弄明白数据从哪里来、到哪里去、经过什么环节遇到性能瓶颈就知道去查哪一段遇到兼容性问题就知道去查哪个字段。这套思路不管协议未来怎么演进都能用得上。希望这一期对你有点帮助。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →