1. 运行时动态修改USB MSC设备查询数据的完整方案
在嵌入式USB设备开发中,Mass Storage Class(MSC)设备的查询字符串(Inquiry Data)是主机识别设备的重要标识。传统做法是在编译时静态定义这些字符串,但实际产品往往需要根据运行环境动态调整设备信息。本文将详细介绍如何在Keil MDK环境下实现USB MSC设备查询字符串的运行时动态配置。
注意:本方案适用于需要同一固件适配不同产品标识的场景,如OEM贴牌生产或多型号共线生产的情况。
1.1 查询字符串的结构解析
USB MSC设备的查询字符串由三部分组成,总长度固定为28字节:
- 厂商信息(Vendor Information):8字节(必须补足空格)
- 产品标识(Product Identification):16字节(必须补足空格)
- 产品版本(Product Revision Level):4字节(必须补足空格)
在Keil MDK的默认实现中,这些信息硬编码在USBD_Config_MSC_#.h配置文件中。例如:
#define USBD_MSC0_LUN1_INQUIRY_DATA "Keil " \ "Disk LUN 1 " \ "1.0 "这种静态定义方式无法满足以下实际需求:
- 同一固件需要显示不同厂商信息
- 产品序列号需要动态生成
- 版本信息需要根据运行环境自动更新
2. 动态配置方案实现步骤
2.1 配置文件修改
首先需要修改USBD_Config_MSC_0.h配置文件,将静态定义改为动态指针:
#include <stdint.h> extern uint8_t msc0_runtime_inquiry_string[]; #define USBD_MSC0_LUN1_INQUIRY_DATA msc0_runtime_inquiry_string这一修改使得查询字符串不再使用编译期常量,而是指向运行时内存中的变量。
2.2 字符串变量定义
在应用程序源文件中定义默认字符串和运行时字符串:
uint8_t msc0_runtime_inquiry_string[28] = "Vendor " /* 8字节厂商信息 */ \ "Product " /* 16字节产品标识 */ \ "Rev " /* 4字节版本信息 */ \ ; uint8_t msc0_runtime_inquiry_string_new[28] = "Vendor B" /* 8字节厂商信息 */ \ "Product 2" /* 16字节产品标识 */ \ "Rev2" /* 4字节版本信息 */ \ ;重要:数组大小必须严格为28字节,每个字段必须用空格补足规定长度。
2.3 运行时字符串更新
在需要更新查询字符串的位置调用内存拷贝:
#include "rl_usb.h" #include <string.h> extern uint8_t msc0_runtime_inquiry_string[]; extern uint8_t msc0_runtime_inquiry_string_new[]; void update_inquiry_string(void) { memcpy(msc0_runtime_inquiry_string, msc0_runtime_inquiry_string_new, 28); USBD_Initialize(0); /* USB设备重新初始化 */ USBD_Connect(0); /* USB设备重新连接 */ }更新操作可以在USB初始化前后的任意时刻进行:
- 初始化前更新:主机首次枚举时将看到新字符串
- 初始化后更新:需要重新初始化USB控制器才能使更改生效
3. 高级应用与注意事项
3.1 非易失性存储集成
对于需要持久化的场景,应将字符串保存在非易失性存储器中:
void load_inquiry_string(void) { if(nvm_read(INQUIRY_STRING_ADDR, msc0_runtime_inquiry_string, 28) != 28) { // 读取失败,使用默认值 memcpy(msc0_runtime_inquiry_string, default_inquiry_string, 28); } } void save_inquiry_string(void) { nvm_write(INQUIRY_STRING_ADDR, msc0_runtime_inquiry_string_new, 28); }警告:避免使用RAM磁盘存储这些信息,因为电源循环会导致数据丢失。
3.2 动态内容生成
可以利用设备唯一ID生成个性化字符串:
void generate_serialized_inquiry(void) { uint32_t uid[3]; get_device_unique_id(uid); // 获取芯片唯一ID snprintf((char*)msc0_runtime_inquiry_string_new+8, 16, "SN:%08X%08X", uid[0], uid[1]); memcpy(msc0_runtime_inquiry_string_new, "MyVendor", 8); memcpy(msc0_runtime_inquiry_string_new+24, "1.0", 4); }3.3 主机枚举时机处理
Windows和Linux主机对USB设备重新枚举的处理方式不同:
- Windows:需要完全断开连接后重新枚举才能识别新字符串
- Linux:部分内核版本支持热重新枚举
可靠的重枚举流程应包含:
- 延迟处理(确保主机完成当前操作)
- 物理断开模拟(如有硬件支持)
- 软件重新初始化
void safe_reconnect(void) { USBD_Disconnect(0); delay_ms(500); // 确保主机检测到断开 USBD_Initialize(0); USBD_Connect(0); }4. 常见问题与调试技巧
4.1 字符串未更新问题排查
如果主机仍显示旧字符串,按以下步骤排查:
- 确认memcpy确实执行且数据正确
- 检查数组越界问题
- 验证USB重新初始化流程
- 使用USB分析仪抓取实际传输数据
4.2 主机缓存问题
某些操作系统会缓存设备信息,解决方案:
- 更换USB端口强制重新枚举
- 修改设备VID/PID组合
- 在设备管理器中卸载设备驱动
4.3 性能优化建议
频繁更新字符串时应注意:
- 避免在中断上下文中执行内存拷贝
- 对非易失性存储操作进行磨损均衡
- 添加字符串校验和防止数据损坏
bool validate_inquiry_string(void) { // 检查各字段长度和内容 if(msc0_runtime_inquiry_string[7] != ' ') return false; if(msc0_runtime_inquiry_string[23] != ' ') return false; return true; }在实际项目中,我们还需要考虑字符串的国际化和本地化支持。对于需要显示多语言产品信息的场景,可以扩展为根据系统语言环境自动选择适当的字符串版本。
通过这套方案,我们成功实现了:
- 生产线上统一固件自动适配不同客户标识
- 根据硬件配置显示不同产品型号
- 固件版本信息动态更新
- 设备序列号自动生成
这种动态配置方法不仅适用于USB MSC设备,其原理也可以推广到其他需要运行时配置信息的USB设备类实现中。