UFS命令集:基本命令、查询命令、安全命令

UFS协议里,命令就是主机和设备之间的“对话”。一上来就背命令码?我当初也这么干过,结果项目里连续踩坑。后来我把命令分了三大类:基本命令管数据传输,查询命令管信息获取,安全命令管密钥和认证。分清楚以后,调起问题来思路清晰多了。

这一节,我带你把这三大类捋一遍。重点说清楚:每个类别干什么用、典型命令长什么样、实际项目里容易在哪儿翻车。

1. 基本命令:数据传输的命根子

基本命令覆盖SCSI指令集里最常用的一部分。说白了,就是主机要读写数据块、管理设备状态时发的那些命令。UFS用的命令格式和SCSI差不多,命令描述块(CDB)固定16字节。

自己心里要有个谱:基本命令不能处理描述符这类逻辑实体。描述符的事,得找查询命令。

我把常用基本命令整理了个表,你在测试时直接对照着用:

命令码(OPCODE) 名称 简要说明
0x00 TEST UNIT READY 查设备是否就绪,我常拿它做心跳探测
0x03 REQUEST SENSE 读上次命令的详细错误信息
0x08 READ (6) 读数据块,固定长度寻址
0x28 READ (10) 读数据块,16位逻辑块地址
0x2A WRITE (10) 写数据块,和READ配成一对
0x35 SYNCHRONIZE CACHE (10) 强制写缓冲,掉电保护最依赖它
0x4C LOG SENSE 读设备健康日志,磨损和温度都在里面
0x4D LOG SELECT 写日志参数或清空日志

发一个基本命令,CDB格式长这样:

// READ (10) 命令 - 16字节CDB示例
uint8_t cdb[16] = {
    0x28,           // 操作码:READ (10)
    0x00,           // 控制字节(DPO, FUA等)
    0x00, 0x00, 0x01, 0x00, // LBA: 0x100
    0x00,           // 组号
    0x00, 0x00, 0x00, 0x01, // 传输长度: 1个块
    0x00, 0x00, 0x00        // 填充
};
// 通过UFS传输层(UTP)发送
utp_submit_cmd(cdb, UFS_DIR_READ, block_ofs, block_cnt);

一个小经验:我在做压力测试时,特意构造过超大传输长度(比如0xFFFF个块),目的是触发设备内部缓冲区分配异常。很多早期固件就是在这种边界上崩的。

2. 查询命令:设备信息的“查号台”

查询命令不走普通SCSI命令路径,它走的是UFS专用的Query Request/Response机制。你想想看,设备里那些描述符(Descriptor)、标志(Flag)、属性(Attribute)怎么读?就得靠查询命令。

查询命令的格式和基本命令不一样——它封装在UPIU(UFS Protocol Information Unit)里。我列几个最常用的:

查询功能码 名称 作用
0x01 READ DESCRIPTOR 读设备描述符、配置描述符、单位描述符等
0x02 WRITE DESCRIPTOR 写描述符(通常只有配置描述符可写)
0x03 READ FLAG 读一个标志位的当前值
0x04 SET FLAG 设置标志(比如清空fDeviceInit)
0x05 CLEAR FLAG 清除标志
0x08 READ ATTRIBUTE 读属性值(比如最大电力参数)
0x09 WRITE ATTRIBUTE 写属性(例如修改bPowerDesc或bMaxPower)

我记得有一次项目里,设备上电后一直返回DEVICE_INIT状态。查了半天,结果是用SET FLAG命令清fDeviceInit的时机不对。协议文档写的是“主机必须在启动后立即清这个标志”,但嵌入式平台启动慢,清早了会被设备忽略。改成上电3ms后再送查询命令,就正常了。

注意:查询命令的响应时间不是固定的。压力测试时,如果连续发送大量查询请求(比如每秒1000次READ DESCRIPTOR),设备可能因为内部资源冲突而超时。我建议在查询命令之间至少留5μs的间隙,并且要准备好重试机制。

3. 安全命令:守住数据的最后防线

安全命令处理身份认证、密钥管理、加密数据块传输。UFS 3.0起强推的RPMB(Replay Protected Memory Block)也是靠安全命令操作的。常用的两个跟SCSI一样:

  • SECURITY PROTOCOL IN (A1h)——用于读取安全参数和证书
  • SECURITY PROTOCOL OUT (B5h)——用于写入安全数据(如认证密钥)

发安全命令时,协议类型通过SECURITY PROTOCOL字段指定。比如:

  • 00h – 用于SPC(SCSI Primary Commands)的T10安全协议
  • 01h – 用于ATA安全协议
  • EC-EFh – 厂家自定义(很多UFS设备用它做私有加密)

一个典型的安全命令发送过程:

// SECURITY PROTOCOL OUT 示例 - 16字节CDB
uint8_t cdb[16] = {
    0xB5,             // 操作码
    0x01,             // 安全协议 = T10
    0x00, 0x00,       // 协议特定标识
    0x00, 0x00, 0x00, 0x01, // 传输长度(1块,通常512字节)
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00  // 填充
};
// 数据缓冲区中包含认证握手帧
memcpy(data_buf, auth_challenge, sizeof(auth_challenge));
utp_submit_data_cmd(cdb, UFS_DIR_WRITE, data_buf, blk_size);

一个我自己踩过的坑:安全命令的传输长度不能超过设备在dSecurityOutMaxSize中声明的值。我一开始想当然地按512字节发送,结果设备一直回CHECK CONDITION,查标准才知道不同设备这个值差别很大。预先读一下这个描述符,能省一天排查时间。

三条命令共同的门槛——命令错误恢复

不管哪类命令,发出去以后都要处理超时和错误。我这里有个经过多次测试验证的恢复流程:

  1. 检查UFS传送层状态:先看UTP层有没有报坏帧或NACK,如果链路层不稳,重发也没用。
  2. 发REQUEST SENSE:拿到附加错误数据(Sense Key + ASC/ASCQ)。
  3. 根据错误类型处理:
    • 设备忙(0x04) → 等100μs再试,最多重试3次。
    • 介质错误(0x03) → 这个块已经废了,建议做坏块重映射。
    • 硬件错误(0x01) → 可能是电压温度问题,降低频率再试。
  4. 命令超时:如果超过30ms还没响应,先做链路复位(UFS Link Startup),再发命令。

压力测试时,我会故意构造无效命令码(比如0x7F)或超出LBA范围的地址,验证设备的错误恢复路径是否干净。很多设备在错误路径下会泄漏资源——该释放的传输请求没释放,跑几次就卡死了。

UFS命令集三大分类 UFS 命令集 基本命令

下面的列表是基本命令的子项,我用矩形表示,连接线指向它们

READ / WRITE SYNC CACHE REQUEST SENSE LOG SENSE 查询命令 READ DESCRIPTOR SET/CLEAR FLAG READ/WRITE ATTRIBUTE 安全命令 SECURITY PROTOCOL IN SECURITY PROTOCOL OUT 命令选择原则 数据传输 → 基本命令 │ 设备描述/配置 → 查询命令 │ 密钥/加密 → 安全命令

我习惯在测试的初始化阶段发一次READ DESCRIPTOR,把设备的命令支持掩码和最大命令队列长度全部读出来。后续的测试脚本根据这些参数动态调整压力,比自己硬编码靠谱得多。

最后提醒一句:命令集的测试不能只跑“happy path”。我见过太多设备在混合命令场景下——比如正在写大块数据时突然插入一个安全命令——出现内部状态机紊乱。建议你一定跑一跑命令交织的测试,这才是压力验证的硬骨头。