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,查标准才知道不同设备这个值差别很大。预先读一下这个描述符,能省一天排查时间。
三条命令共同的门槛——命令错误恢复
不管哪类命令,发出去以后都要处理超时和错误。我这里有个经过多次测试验证的恢复流程:
- 检查UFS传送层状态:先看UTP层有没有报坏帧或NACK,如果链路层不稳,重发也没用。
- 发REQUEST SENSE:拿到附加错误数据(Sense Key + ASC/ASCQ)。
- 根据错误类型处理:
- 设备忙(0x04) → 等100μs再试,最多重试3次。
- 介质错误(0x03) → 这个块已经废了,建议做坏块重映射。
- 硬件错误(0x01) → 可能是电压温度问题,降低频率再试。
- 命令超时:如果超过30ms还没响应,先做链路复位(UFS Link Startup),再发命令。
压力测试时,我会故意构造无效命令码(比如0x7F)或超出LBA范围的地址,验证设备的错误恢复路径是否干净。很多设备在错误路径下会泄漏资源——该释放的传输请求没释放,跑几次就卡死了。
我习惯在测试的初始化阶段发一次READ DESCRIPTOR,把设备的命令支持掩码和最大命令队列长度全部读出来。后续的测试脚本根据这些参数动态调整压力,比自己硬编码靠谱得多。
最后提醒一句:命令集的测试不能只跑“happy path”。我见过太多设备在混合命令场景下——比如正在写大块数据时突然插入一个安全命令——出现内部状态机紊乱。建议你一定跑一跑命令交织的测试,这才是压力验证的硬骨头。