
大家好,我是良许
在嵌入式开发中,USB可以说是我们最常打交道的接口之一了。
无论是调试设备、烧录程序,还是开发各种外设,USB都扮演着至关重要的角色。
今天我就来和大家深入聊聊USB总线和协议的那些事儿。
USB技术从1996年诞生至今,已经经历了多个版本的迭代。
最初的USB 1.0速度只有1.5Mbps,到USB 1.1的12Mbps,再到USB 2.0的480Mbps,USB 3.0更是达到了5Gbps。
现在最新的USB 4.0甚至能达到40Gbps的惊人速度。
在我们嵌入式开发中,USB 2.0依然是应用最广泛的版本,因为它在速度、成本和功耗之间取得了很好的平衡。
USB之所以能够如此普及,主要得益于它的几个核心优势。
首先是即插即用(Plug and Play),设备连接后系统会自动识别并加载驱动,这对用户来说非常友好。
其次是热插拔(Hot Swap),不需要关机就能插拔设备,大大提高了使用便利性。
第三是供电能力,USB接口可以为外设提供5V电源,最大电流可达500mA(USB 2.0)或900mA(USB 3.0),这让很多小功率设备无需额外供电。
最后是统一的接口标准,一根线缆可以连接各种不同类型的设备。
USB采用的是主从架构,也就是Host-Device模式。
在一个USB系统中,只能有一个主机(Host),但可以连接多个设备(Device)。
主机负责管理整个总线,包括设备枚举、数据传输调度等。
通过USB Hub(集线器),一个主机最多可以连接127个设备。
这种星型拓扑结构最多支持5层Hub级联,但实际应用中很少会用到这么深的层级。
USB接口经历了多次演进,我们常见的有Type-A、Type-B、Mini USB、Micro USB以及最新的Type-C。
Type-A是最常见的标准USB接口,通常用于主机端。
Type-B接口则多用于打印机等外设。
Mini USB和Micro USB曾经广泛应用于手机和小型设备,现在逐渐被Type-C取代。
Type-C接口最大的特点是正反可插,并且支持更高的功率传输和数据速率。
以USB 2.0的标准A型接口为例,它有4个引脚,从外到内分别是VCC(+5V电源)、D-(数据负)、D+(数据正)、GND(地)。其中D+和D-是一对差分信号线,用于数据传输。
USB采用差分信号的好处是抗干扰能力强,能够实现较长距离的可靠传输。
在实际PCB设计中,我们需要特别注意D+和D-的走线要等长,并且要做差分对处理,阻抗控制在90欧姆左右。
USB 2.0定义了三种速度模式:低速(Low Speed)1.5Mbps、全速(Full Speed)12Mbps和高速(High Speed)480Mbps。
不同速度模式下,电气特性也有所不同。
低速和全速模式使用3.3V的信号电平,而高速模式使用400mV的差分电压。
在设备端,我们可以通过在D+或D-上串联一个1.5K欧姆的上拉电阻来标识设备的速度类型。
全速和高速设备在D+上拉,低速设备在D-上拉。
USB协议采用分层设计,从下到上分为物理层、协议层、功能层和应用层。
物理层负责电气信号的传输,包括编码、解码、位同步等。
协议层处理数据包的组装和解析,包括令牌包、数据包、握手包等。
功能层实现具体的USB功能,比如端点管理、数据缓冲等。
应用层则是具体的设备功能实现,比如USB鼠标、键盘、U盘等。
USB定义了四种传输类型,分别适用于不同的应用场景。
控制传输用于设备配置和状态查询,所有USB设备都必须支持控制传输。
中断传输用于少量、实时性要求高的数据传输,比如鼠标、键盘。批量传输用于大量数据的可靠传输,但不保证实时性,U盘就是典型应用。
同步传输用于音视频等对实时性要求高但可以容忍少量错误的场景。
USB通信的基本单位是包。
一个完整的USB传输由多个包组成,包括令牌包、数据包和握手包。
令牌包由主机发出,用于指示传输的方向和目标端点。
数据包携带实际要传输的数据。
握手包用于确认传输状态,比如ACK表示成功接收,NAK表示设备暂时无法处理,STALL表示端点出错。
每个包都包含同步字段、PID(包标识符)、数据字段和CRC校验。
当一个USB设备插入主机时,主机会通过检测D+或D-上的电平变化来发现新设备。
前面提到的1.5K上拉电阻就是关键,它会将D+或D-拉高,主机检测到这个变化后就知道有新设备连接了。
随后主机会等待至少100ms,让设备的电源稳定下来,这个过程叫做去抖动。
检测到新设备后,主机会发送复位信号,持续至少10ms。
复位后,设备进入默认状态,使用地址0进行通信。
接下来主机会通过控制传输读取设备描述符,了解设备的基本信息,比如厂商ID、产品ID、设备类型等。
然后主机会给设备分配一个唯一的地址(1-127之间),设备收到地址后就不再使用地址0了。
获取设备地址后,主机会继续读取配置描述符、接口描述符和端点描述符,全面了解设备的功能和需求。
根据这些信息,操作系统会加载相应的驱动程序。
最后主机发送SET_CONFIGURATION命令,设备进入配置状态,开始正常工作。
整个枚举过程通常在几秒内完成,这就是我们插入U盘后很快就能使用的原因。
端点是USB设备中数据传输的终点,可以理解为设备内部的一个数据缓冲区。
每个端点都有一个编号(0-15)和方向(IN或OUT)。
IN端点表示数据从设备发送到主机,OUT端点表示数据从主机发送到设备。
端点0比较特殊,它是双向的,专门用于控制传输。
一个USB设备最多可以有32个端点(16个IN + 16个OUT),但实际应用中很少用这么多。
管道(Pipe)是主机和设备端点之间的逻辑连接。
当主机完成设备枚举后,就会根据端点描述符建立相应的管道。
管道分为流管道(Stream Pipe)和消息管道(Message Pipe)。
流管道用于批量、中断和同步传输,数据没有特定的结构。
消息管道用于控制传输,数据有明确的请求-响应结构。
在STM32的HAL库中,配置USB端点的代码大致如下:
// 打开并配置端点
HAL_StatusTypeDef HAL_PCD_EP_Open(PCD_HandleTypeDef *hpcd,
uint8_t ep_addr,
uint16_t ep_mps,
uint8_t ep_type)
{
HAL_StatusTypeDef ret = HAL_OK;
PCD_EPTypeDef *ep;
if ((ep_addr & 0x80U) == 0x80U) {
ep = &hpcd->IN_ep[ep_addr & EP_ADDR_MSK];
ep->is_in = 1U;
} else {
ep = &hpcd->OUT_ep[ep_addr & EP_ADDR_MSK];
ep->is_in = 0U;
}
ep->num = ep_addr & EP_ADDR_MSK;
ep->maxpacket = ep_mps;
ep->type = ep_type;
// 配置硬件寄存器
// ...
return ret;
}
// 端点数据发送
HAL_StatusTypeDef HAL_PCD_EP_Transmit(PCD_HandleTypeDef *hpcd,
uint8_t ep_addr,
uint8_t *pBuf,
uint32_t len)
{
PCD_EPTypeDef *ep;
ep = &hpcd->IN_ep[ep_addr & EP_ADDR_MSK];
ep->xfer_buff = pBuf;
ep->xfer_len = len;
ep->xfer_count = 0U;
// 启动传输
// ...
return HAL_OK;
}设备描述符是USB设备的"身份证",包含了设备的基本信息。
它的长度固定为18字节,包括USB版本号、设备类代码、厂商ID(VID)、产品ID(PID)、设备版本号等。
主机通过读取设备描述符来识别设备类型并加载相应驱动。
在嵌入式开发中,我们需要根据实际设备来定义这个描述符。
// USB设备描述符示例
const uint8_t USBD_DeviceDesc[USB_LEN_DEV_DESC] = {
0x12, // bLength: 描述符长度
USB_DESC_TYPE_DEVICE, // bDescriptorType: 设备描述符类型
0x00, 0x02, // bcdUSB: USB 2.0
0x00, // bDeviceClass: 在接口描述符中定义
0x00, // bDeviceSubClass
0x00, // bDeviceProtocol
USB_MAX_EP0_SIZE, // bMaxPacketSize: 端点0最大包大小
LOBYTE(USBD_VID), // idVendor: 厂商ID低字节
HIBYTE(USBD_VID), // idVendor: 厂商ID高字节
LOBYTE(USBD_PID), // idProduct: 产品ID低字节
HIBYTE(USBD_PID), // idProduct: 产品ID高字节
0x00, 0x02, // bcdDevice: 设备版本号
USBD_IDX_MFC_STR, // iManufacturer: 厂商字符串索引
USBD_IDX_PRODUCT_STR, // iProduct: 产品字符串索引
USBD_IDX_SERIAL_STR, // iSerialNumber: 序列号字符串索引
USBD_MAX_NUM_CONFIGURATION // bNumConfigurations: 配置数量
};配置描述符定义了设备的工作配置,一个设备可以有多个配置,但同一时间只能使用一个。
配置描述符本身只有9字节,但它后面会跟着接口描述符和端点描述符,形成一个描述符集合。
配置描述符中包含了接口数量、配置值、供电方式(自供电或总线供电)、最大功耗等信息。
接口描述符定义了设备的功能接口,一个配置可以包含多个接口。
比如一个USB复合设备可能同时包含HID接口和CDC接口。
接口描述符指定了接口类代码、子类代码和协议代码,这些信息帮助主机识别接口类型。
端点描述符则描述了每个端点的属性,包括端点地址、传输类型、最大包大小和轮询间隔等。
中断传输和同步传输需要指定轮询间隔,表示主机多久查询一次端点。
HID是最常见的USB设备类之一,包括鼠标、键盘、游戏手柄等。
HID类的优势是操作系统都内置了HID驱动,无需安装额外驱动就能使用。
HID设备通过报告来传输数据,报告格式由报告描述符定义。
在嵌入式开发中,我们经常用HID类来实现自定义的数据传输,因为它简单方便。
CDC主要用于串口通信。USB转串口模块就是典型的CDC设备。
CDC类使用两个接口:一个通信接口用于控制,一个数据接口用于数据传输。
通过CDC类,我们可以在PC上虚拟出一个COM口,就像使用传统串口一样方便。
这在调试嵌入式系统时非常有用。
MSC用于U盘、移动硬盘等存储设备。
MSC类基于SCSI协议,支持读写扇区、查询容量等操作。
实现MSC类设备需要提供底层的存储介质访问接口,比如Flash、SD卡等。
在STM32中,我们可以使用内部Flash或外部SPI Flash来实现一个虚拟U盘。
// MSC类读扇区函数示例
int8_t STORAGE_Read(uint8_t lun, uint8_t *buf,
uint32_t blk_addr, uint16_t blk_len)
{
// 计算实际地址
uint32_t addr = blk_addr * STORAGE_BLK_SIZ;
// 从Flash读取数据
for (uint16_t i = 0; i < blk_len; i++) {
// 读取一个扇区
memcpy(buf, (uint8_t*)(FLASH_BASE_ADDR + addr),
STORAGE_BLK_SIZ);
buf += STORAGE_BLK_SIZ;
addr += STORAGE_BLK_SIZ;
}
return 0;
}
// MSC类写扇区函数示例
int8_t STORAGE_Write(uint8_t lun, uint8_t *buf,
uint32_t blk_addr, uint16_t blk_len)
{
uint32_t addr = blk_addr * STORAGE_BLK_SIZ;
// 解锁Flash
HAL_FLASH_Unlock();
for (uint16_t i = 0; i < blk_len; i++) {
// 擦除扇区
FLASH_EraseSector(addr);
// 写入数据
for (uint32_t j = 0; j < STORAGE_BLK_SIZ; j += 4) {
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD,
addr + j,
*(uint32_t*)(buf + j));
}
buf += STORAGE_BLK_SIZ;
addr += STORAGE_BLK_SIZ;
}
// 锁定Flash
HAL_FLASH_Lock();
return 0;
}OTG是USB 2.0引入的一项技术,允许设备在主机和从机之间动态切换。
传统USB只能是主机连接设备,而OTG使得两个设备可以直接连接,并协商谁当主机。
比如手机既可以作为设备连接到电脑,也可以作为主机连接U盘或键盘。
OTG设备通过ID引脚来识别角色,ID引脚接地的一方作为主机。
OTG定义了两个重要协议:HNP(Host Negotiation Protocol,主机协商协议)和SRP(Session Request Protocol,会话请求协议)。
HNP允许两个OTG设备在连接后交换主机角色,比如手机给相机传完照片后,相机可以变成主机来控制手机。
SRP允许从设备请求主机启动会话,这在省电模式下很有用。
在嵌入式系统中,OTG功能非常实用。
比如一个手持设备,既需要连接PC进行数据传输和充电,又需要连接U盘读取文件。
使用OTG技术就能很好地满足这种需求。
STM32的许多型号都支持USB OTG,我们在设计产品时可以充分利用这个特性,提升产品的灵活性和用户体验。
USB硬件调试首先要检查电路连接是否正确,特别是D+和D-的走线。
使用示波器可以观察USB信号的波形,检查是否有过冲、振铃等问题。
如果设备无法被主机识别,可以测量上拉电阻是否正常,电源电压是否稳定。
另外要注意ESD防护,USB接口容易受到静电冲击,建议加装TVS管。
软件调试方面,USB协议分析仪是必不可少的工具。
通过抓取USB通信数据包,我们可以清楚地看到枚举过程、描述符内容、数据传输细节等。
常用的USB协议分析软件有Wireshark、Ellisys、Beagle等。
对于简单的调试,Windows自带的USBView工具也很有用,可以查看设备描述符和当前状态。
在实际开发中,经常遇到的问题包括设备无法枚举、数据传输错误、速度不达标等。
设备无法枚举通常是描述符配置错误或硬件连接问题。
数据传输错误可能是端点配置不对或缓冲区溢出。
速度不达标则需要检查时钟配置和DMA设置。
建议在开发初期就建立完善的日志系统,记录USB事件和错误信息,这对问题定位非常有帮助。
USB技术虽然看起来复杂,但只要掌握了基本原理和协议结构,在实际应用中就能游刃有余。
作为嵌入式工程师,我们不仅要会用USB,更要深入理解它的工作机制。
从硬件接口到协议栈,从设备枚举到数据传输,每个环节都值得我们仔细研究。
希望通过这篇文章,能帮助大家建立起对USB技术的系统认识,在今后的项目开发中少走弯路。
USB技术还在不断发展,Type-C和USB PD等新技术也值得我们持续关注。
只有不断学习,才能在技术的浪潮中保持竞争力。
更多编程学习资源
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。