首页 / 资讯中心 / 文章详情

UFS 4.0协议与联调:UPIU、UniPro及性能排查

UFS 4.0协议与联调:UPIU、UniPro及性能排查 ★ FEATURED ARTICLE
简介面向移动存储、嵌入式开发与芯片验证人员的UFS 4.0规范原文即JEDEC于2022年8月发布的JESD220F标准文档可用于系统梳理通用闪存从协议接口、数据传输到功耗管理的技术框架。压缩包仅含1个PDF文件大小约5.56MB保留标准正文、版权声明及版本修订信息便于按章节检索关键定义与参数。内容围绕每通道23.2Gbps理论带宽、双通道操作、全双工串行接口、DVFS低功耗机制以及加密认证等安全特性展开并覆盖智能手机、平板电脑和可穿戴设备等应用场景有助于读者把握UFS 4.0相对UFS 3.1的升级重点。已有788人学习下载适合作为存储驱动开发、性能优化和方案选型时查阅与对照的一手资料。1. 23.2Gbps 这个数字骗了不少人UFS 4.0 规范里最容易被拿来做文章的数字是每通道 23.2Gbps。很多人第一反应是“线速率翻了几倍所以体验会快几倍”但真把一份 UFS 4.0 规范文档从头翻到尾就会发现线速率的提升只是结果不是原因。真正让这套协议跟上一代拉开差距的是它把“什么时候提速、什么时候降速、数据怎么插队、命令怎么并发”全部写进了可协商的状态机和数据结构里。换句话说快是链路层和命令层配合出来的不是把时钟调高调出来的。搞懂这一点你再看 UPIU 头里的每一个 bit、看 Unipro 的流控信用、看那些一堆名字很像的复位类型才不会觉得是在背单词。2. 分层架构一条 READ 命令要穿过四层才摸到闪存主机侧发一条读请求最终落到闪存阵列上中间要过四道手。UFS 4.0 规范把这条路径切成应用层UAP、传输协议层UTP、互联层UIC再外加一个横跨各层的设备管理器。分层本身不稀奇稀奇的是每一层之间的耦合被压到了最低层与层之间只传递一种东西就是 UPIU。理解了这个“单一货币”的设计后面所有细节都能挂上去。2.1 UAP 与 UTPSCSI 命令和原生命令怎么挤进同一个通道应用层负责的是“命令长什么样”。它下面有两套命令体系一套是 SCSI 命令集READ(10)、WRITE(16)、UNMAP、SYNCHRONIZE CACHE 这些命令描述块 CDB 定长 16 字节另一套是 UFS 原生命令 UNC比如 NOP、QUERY、TASK MANAGEMENT这些在 SCSI 里没有对应物。传输协议层负责的是“命令怎么送出去”。它把 CDB 或者原生命令的参数塞进一个固定格式的信封配上 LUN、任务标签、方向位和期望传输长度然后交给下面的互联层。这一层还管着命令队列、任务管理、数据传输的分包节奏。规范里把 UTP 的职责描述得很克制但实际实现里最容易出问题的恰恰是这里任务标签的分配与回收、数据段长度和期望长度对不上、RESPONSE 里的 status 和 sense data 组合起来该怎么解释。一个常见的实现思路是把 UTP 层做成一个状态机数组每个任务标签对应一个槽位槽位里记录当前处于“已发命令等响应”“等数据”“数据已收齐等响应”中的哪一态。这套状态机不难写难写的是异常路径设备回 REJECT UPIU、任务被 Task Management 中止、数据长度不匹配触发 CHECK CONDITION每一条都要有明确的回收动作否则标签泄漏之后队列深度会莫名其妙地掉下去。2.2 UIC 层UniPro 和 M-PHY 是两个不同的世界互联层由两部分拼起来上面是 UniPro负责链路层的可靠性下面是 M-PHY负责电气层怎么把 bit 打成波形。规范里把它们分开写是因为两者的失败模式完全不同。UniPro 提供的是带重传的可靠链路它有数据链路层的帧、有流控信用、有重传机制还定义了网络层和传输层的地址映射。UFS 只是 UniPro 的一个“客户协议”所以 UTP 层需要把 LUN 和任务映射到 UniPro 的端口CPort上去。这层抽象带来的好处是链路可以按需重传而不打扰上层代价是多了一层缓冲和一套信用管理调不好的时候表现为带宽上不去但又不报错。M-PHY 则是纯粹的物理层终端匹配、驱动电平、burst 的起止、gear 的切换、自适应均衡。它有一套自己的状态机跟 UniPro 的链路状态不是一回事。联调时最典型的困惑就是“UniPro 说链路起来了但数据就是过不去”这时候要看的是 M-PHY 的 burst 是否真的建立、gear 是否协商到预期档位。2.3 设备管理器与描述符主机怎么“认识”一颗陌生的芯片设备管理器是横切的一层它不参与数据搬运但它决定了主机能不能正确配置这颗芯片。它对外暴露三样东西描述符Descriptor、标志Flag、属性Attribute。描述符是只读的静态信息比如设备能力、几何结构、电源参数标志是布尔开关比如某个功能开没开属性是可读可写的数值比如队列深度、当前功耗模式。主机访问这三样东西走的是 QUERY REQUEST UPIU而不是 SCSI 命令。也就是说在发第一条 READ 之前主机已经通过一串 QUERY 把设备的底细摸了一遍。这套模型跟“寄存器映射”的思路不同寄存器是地址加位域描述符是带索引的结构体读取时要用“描述符类型 索引”两个参数定位返回的是变长结构。好处是扩展性强新增能力只要加一个描述符类型坏处是主机侧必须实现一套完整的解析逻辑解析错了不会报错只会读出一个看似合理但完全不对的值。层搬运的单位典型失败表现UAPCDB / 原生命令参数命令被设备判为非法回 CHECK CONDITIONUTPUPIU任务标签泄漏队列深度逐级下降UniPro帧与信用带宽上不去但没有错误码M-PHYburst 与 gear链路起不来或起在低档位3. UPIU把命令、数据、状态装进同一种信封UPIU 是这套协议里最值得细看的东西。它把“我要读”“这是你要的数据”“我收到了”“出错了”这些语义完全不同的事情全部塞进同一个 32 字节的基本头里靠一个 6 位的类型字段区分。这种设计的好处是设备侧只需要一个解析入口坏处是这个头里每一个字段都要承载多重含义读规范的时候一不留神就会把某个只在特定类型里有效的字段当成通用字段。3.1 32 字节基本头里每一位都有代价基本头的布局是这样的第一个字节的低 6 位是事务类型高 2 位是标志第二个字节里的若干位是方向位和任务属性的高位然后是 1 字节的 LUN、1 字节的任务标签、1 字节的 Initiator ID 加上查询功能位、1 字节的响应码、1 字节的状态码、1 字节的端到端保护标志、1 字节的扩展头长度、1 字节的数据段长度最后是 4 字节的期望传输长度和 4 字节的头部保护字段。/* UFS 4.0 规范中 UPIU 基本头的 32 字节布局按位域还原 */ struct ufs_upiu_header { uint8_t transaction_type : 6; /* bit5..00x01COMMAND0x02RESPONSE0x21DATA IN */ uint8_t flags : 2; /* bit7..6bit6 是方向位bit5 是任务属性高位 */ uint8_t lun : 8; /* 逻辑单元号8 位并非 SCSI 的 64 位寻址 */ uint8_t task_tag : 8; /* 主机分配的票据设备必须原样回填 */ uint8_t iid : 4; /* Initiator ID单主机场景恒为 0 */ uint8_t reserved0 : 2; uint8_t query_function : 2; /* 仅 QUERY REQUEST 有效读描述符/读属性/写描述符/写属性 */ uint8_t response : 8; /* 仅 RESPONSE UPIU 有意义TARGET SUCCESS / TARGET FAILURE */ uint8_t status : 8; /* SCSI 状态GOOD0x00CHECK CONDITION0x02 */ uint8_t e2e : 8; /* 端到端保护信息标志 */ uint8_t total_ehs_len : 8; /* 扩展头长度单位是 32 字节 */ uint8_t data_segment_len : 8; /* 本 UPIU 自带数据段长度单位是 4 字节 */ uint32_t data_transfer_len; /* 本次事务期望传输的总字节数 */ uint32_t header_e2e; /* 头部保护字段低 16 位承载 CRC */ } __attribute__((packed));几个参数必须掰开说。data_segment_len的单位是 4 字节不是 1 字节写驱动的人第一次看很容易直接当字节数用结果算出来的偏移全部错位。data_transfer_len是整笔事务的总长度而data_segment_len只是这一个 UPIU 里附带的那一段两者在多包传输时并不相等。task_tag是主机分配的设备只负责回填所以主机侧必须维护一张从标签到上下文的映射表标签用完不回收队列深度就会假性耗尽。query_function那两位只在 QUERY REQUEST 里有效在 COMMAND UPIU 里它是保留位。这种“同一字段在不同类型下语义不同”的设计在规范里出现了不止一次阅读时必须先把事务类型确定下来再去看字段含义否则会得出互相矛盾的结论。3.2 一次读操作的完整时序COMMAND 到 RESPONSE以一条 READ(10) 为例主机先发一个 COMMAND UPIU带上 LUN、任务标签、CDB 和期望传输长度此时方向位表示“设备要往主机送数据”。设备收到后如果可以直接服务就开始回 DATA IN UPIU每个包带一段数据。全部数据送完设备再回一个 RESPONSE UPIU里面填响应码和状态码任务标签跟上一次一样。写操作的时序长一点。主机发 COMMAND UPIU 之后设备未必立刻要数据它可能先回一个 READY TO TRANSFER UPIU告诉主机“我现在要第 N 个偏移开始的第 M 个字节”。主机收到之后才开始发 DATA OUT UPIU。这个握手存在的意义是让设备掌握节奏闪存写入的时序跟主机内存的读取速度不匹配如果主机一口气灌过来设备侧缓冲会溢出。用 RTT 把控制权交给设备主机就只能按设备要多少给多少。这里的坑是 RTT 里的偏移量语义。规范和 SCSI 的语义不完全一样偏移指的是本次请求内部的偏移不是整个传输的偏移两者混用会导致写进去的数据错位。而且 RTT 可以在一笔事务里出现多次也可以完全不出现取决于设备实现。主机侧如果假设“写操作一定会有 RTT”遇上不发的设备就会直接卡死。3.3 乱序数据与命令队列为什么 UFS 敢让数据“插队”规范里明确写了数据乱序传输的规则。设备在响应一次多块写的时候可以按任意顺序索要数据块用 RTT 逐个指定。这条规则看起来只是给了设备一点灵活性实际上是闪存介质特性的直接映射不同物理页的写入耗时差异很大设备如果能先拿已经准备好的那些块整体吞吐就能上去。命令队列是另一层并发。设备通过属性暴露自己的队列深度主机可以同时挂起多个任务标签。更进一步规范还定义了跨逻辑单元的优先级机制允许主机告诉设备“这个 LUN 的请求优先处理”。这套东西组合起来才让 UFS 在随机小 IO 场景下不至于退化成一条同步的总线。代价是主机侧的实现复杂度直线上升。队列要管理标签要复用优先级要在提交时排序任务管理命令要能在队列中间插进去取消某个任务。一个常见的实现思路是把队列做成两级一级是待提交队列按优先级排序一级是已提交队列按标签索引。提交时从前者取响应回来时按标签回填后者。这套结构写起来不难难的是各种边界取消一个已经发了数据但还没收到 RESPONSE 的任务设备回什么、主机怎么回收缓冲规范给了框架但没给唯一答案。import struct def parse_upiu(buf: bytes) - dict: 解析 32 字节 UPIU 基本头用于在抓包或日志里快速判断事务类型 if len(buf) 32: raise ValueError(基本头不足 32 字节多半是 trace 被截断) b0, b1, lun, tag, iid_q, resp, status, e2e, ehs, dsl struct.unpack(10B, buf[:10]) dlen, struct.unpack(I, buf[10:14]) return { type: b0 0x3F, # 低 6 位才是事务类型高 2 位属于 flags flags: (b0 6) | ((b1 0x01) 2), lun: lun, task_tag: tag, query_func: iid_q 0x03, # 只在 QUERY REQUEST 下有意义 response: resp, status: status, data_segment_len: dsl * 4, # 单位是 4 字节别直接当字节数 expected_data_len: dlen, }这段解析代码的用处是在联调阶段快速判断“设备到底回了什么”。日志里如果出现type0x02且status0x02那就是一条 CHECK CONDITION 的 RESPONSE接下来要去读 sense data如果出现大量task_tag相同但类型交替的包说明主机侧标签复用出了问题。data_segment_len乘 4 这个细节几乎每个第一次写解析脚本的人都会踩一次。4. UniPro 与 M-PHY链路为什么必须反复“起搏”UFS 的物理链路不是一根一直在跑的线。它在两种模式之间来回切低速的 PWM 模式用来做初始化和抗干扰场景高速的 HS 模式用来搬数据两者之间还有一段自适应过程。这套设计的动机很朴素移动设备的功耗预算不允许链路一直全速待命而闪存的访问又是突发的所以链路必须能快速起、快速停且每次起搏都要在几十微秒量级完成。4.1 HS burst 与 gear省电省出来的一套状态机M-PHY 的状态机里有准备阶段、同步阶段、数据传输阶段和收尾阶段。HS burst 开始之前要先发一段准备序列让接收端的均衡器收敛然后是同步序列用来对齐符号边界再进入真正的数据段。gear 决定了这一轮 burst 跑多快规范定义了从低速到高速若干档两端的 gear 必须协商一致。lane 的数量也是可配的单 lane 和双 lane 的带宽差一倍但双 lane 对布线的对称性要求高得多。这套机制的直接后果是带宽不是恒定的而是由“每次 burst 能传多少”和“burst 之间隔多久”共同决定。一条命令如果只有 4KB 数据可能 burst 刚热身完就结束了实际有效带宽远低于理论值。这也解释了为什么小文件随机读的性能测试结果波动很大测的根本不是峰值带宽而是起搏的开销。自适应过程是另一个容易被忽略的点。规范里给了一段可调的均衡参数用于补偿信道损耗。板级设计做得不好自适应就会失败或者收敛到很差的值表现出来是高速下误码率飙升链路反复重传最终降档运行。这时候看链路状态是正常的看带宽却只有预期的一半。4.2 流控与数据 pacing接收端说“慢点”的两种方式UniPro 的流控用的是信用机制。接收端告诉发送端“我还有多少个缓冲可用”发送端每发一帧就扣一个信用收到确认再还回来。这套机制的好处是不会丢帧坏处是信用数量直接限制了在途数据量链路延迟大的时候带宽上不去。规范里还定义了数据 pacing 机制让主机主动控制发送节奏。跟信用流控不同pacing 是主机侧的主动行为用来避免瞬时突发把设备侧缓冲打满。两者配合的逻辑是信用保证不出错pacing 保证不拥塞。实际调试时如果发现“带宽上不去但也没有重传”优先查信用配置如果发现“有重传且延迟抖动大”优先查 pacing 和板级信号质量。4.3 参考时钟规范里那些“不起眼”的硬约束参考时钟是整份规范里最枯燥、也最容易翻车的一节。规范对时钟的频率、抖动、占空比都给了明确指标还专门附了测量方法。原因很简单HS 模式下链路速率是参考时钟的倍数时钟抖动会直接被放大成符号间干扰。很多“链路起不来”的问题最后查出来是时钟源本身抖动超标或者走线太长引入了额外噪声。规范里还提到一种在低速模式下检测参考时钟是否可用的机制用来在初始化阶段判断时钟质量。这个机制的存在说明设计者已经预见到一种情况时钟“有”但“不好”如果直接往上跑高速链路会以难以定位的方式失败。先低速验证再逐档提速是这套协议默认的启动策略。板级设计那一节给出的阻抗和电容建议同样是硬约束而不是参考意见。电源去耦不到位会导致 burst 起始瞬间电压塌陷表现出来是高速下偶发误码而低速完全正常。这类问题在实验室里很难复现一到量产批次就集中爆发属于典型的血泪经验。5. 排查UFS 4.0 联调时最容易翻车的五个地方下面这五条都是“表面上看起来像软件问题根子上在别处”的典型。每条按现象、原因、解决三段写顺序就是排查顺序。5.1 枚举阶段卡在链路启动现象上电之后主机反复尝试建立链路状态机停在启动阶段不动偶尔重试几次能过去但极其不稳定。原因链路启动阶段跑在最低档位此时对时钟质量和终端匹配最敏感。参考时钟抖动超标、终端电阻值不对、差分对走线不对称都会让同步序列对不齐。另一个常见原因是复位时序复位释放得太早设备内部还没准备好主机就开始发启动序列。解决先用示波器量参考时钟的实际抖动对照规范给的指标不要只看频率对不对。再检查终端匹配和差分对阻抗板级设计那一节给了允许的最大阻抗和典型电容值。最后核对复位时序确保复位释放到链路启动之间有足够间隔。这三步做完还起不来再去看设备侧的电源上电斜率是否满足要求。5.2 跑分只有预期的一半现象顺序读写的带宽稳定在理论值的一半左右不报错不重传就是慢。原因最常见的是 gear 没协商到最高档或者只跑在单 lane 上。其次是数据包太小起搏开销占比过高。还有一种隐蔽情况是主机侧的缓冲策略不对一次只发一个命令队列深度实际是 1设备侧的并发能力完全没用上。解决先读设备属性确认当前协商到的速率档位和 lane 数量再跟设备声称支持的能力对比两者不一致说明协商过程有问题回头查信号质量。然后确认传输块大小是否够大小包场景下带宽上不去是正常的。最后检查主机侧的队列配置队列深度属性和实际提交的并发数要匹配得上。5.3 受保护分区写入返回失败现象普通分区读写正常往受保护分区写认证数据时返回失败重试也没用换一台设备又好了。原因受保护分区有一套独立的计数器和认证流程写入请求要带正确的随机数和消息认证码计数器不匹配就直接拒绝。另一个高频原因是分区容量没配置或者配置了但主机侧的写入偏移算错了。规范里对这套流程的请求和响应格式定义得很细字段错一个字节就会被判为非法。解决先把计数器读出来跟主机侧记录的值比对不一致就以设备侧为准重新同步。然后核对请求结构里的每一个字段特别是长度字段的单位。写入之前确认分区已经配置且容量非零。这套流程没有“试出来”的余地只能严格按规范的结构逐字段对。5.4 掉电之后文件系统损坏现象正常关机没事异常掉电之后挂载报错日志里能看到最后一批写入丢了甚至出现半写状态。原因主机的关机流程里没有下发断电通知也没有刷缓存。UFS 设备内部有写缓存还有专门用于加速的缓冲区域这些内容在掉电时如果没有落盘就会丢失。更麻烦的是设备可能已经向主机回了“写完成”但数据还在设备缓冲里。解决关机流程里必须依次下发缓存同步命令和断电通知等设备确认之后再切电源。对于不能保证正常关机的场景要靠文件系统自身的日志机制兜底同时在应用层避免“写完立刻断电”的操作序列。这条属于设计阶段就要定下来的事事后补救代价很大。5.5 长时间写入后速度断崖式下跌现象刚开始写入速度很快跑了几十秒之后突然掉到一个很低的水平过一会儿又恢复反复循环。原因两种可能。一是温度升高触发了性能节流设备主动降速保护介质二是加速缓冲区域写满了后续写入必须直接落到慢速介质上同时后台的垃圾回收开始工作争抢带宽。这两种情况在表现上很像需要看设备上报的事件才能区分。解决先读温度相关的属性和事件通知确认是不是节流触发。如果不是再看缓冲区的占用情况评估工作负载是否超出了缓冲能吸收的量。对于持续写入场景主机侧要做速率整形避免把设备逼到热节流和垃圾回收同时发生的状态。这类问题在短时间的跑分里根本看不出来只有长时间压测才暴露。6. 设计取舍为什么规范把越来越多的决定权交给主机翻完整份规范会发现一个趋势UFS 4.0 定义的机制里有相当一部分只规定了“接口和通知”不规定“策略”。加速缓冲写满了怎么办、温度上来了降不降速、生产状态下写过的区域要不要标记、容量变化了主机怎么感知这些都只给出可观测的信号具体怎么做由主机决定。这套思路的动机不难理解。设备侧的固件要对所有主机通用它不知道对面是手机、平板还是车机也不知道当前跑的是什么业务贸然做策略性决策很可能帮倒忙。把决策权上移设备只负责如实上报状态主机根据自己的业务特点去权衡理论上能得到更优的结果。代价是主机固件变复杂了。每多一个可观测信号主机侧就多一个状态机每多一个可配置项就多一处配置错误的可能。实际项目里最常见的不是“机制不好用”而是“机制压根没被用起来”缓冲开关没打开、节流事件没人监听、断电通知忘了发。规范给了工具用不用是另一回事。验证这类设计是否被正确实现我的做法是构造三类负载纯顺序大块写、随机小块写、长时间混合读写每类跑够时间同时记录设备的属性快照和事件上报。三类负载下的行为如果和规范描述的机制对得上基本可以认为实现是完整的如果某一类跑着跑着就“失联”了问题往往不在数据通路上而在状态管理上。我印象最深的一次是排查一个“偶发掉速”查了三天信号质量最后发现是加速缓冲的开关从来没被打开过设备一直在直写介质。从那以后我每次做 bring-up都强制走一遍“时钟质量、链路档位、命令队列、电源与缓存策略”四项检查每一项都要有实测数据不看完不往下走。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站