嵌入式USB控制器开发实战:从架构解析到MSPM0配置避坑指南
1. USB控制器在嵌入式系统中的核心价值与定位
在嵌入式系统开发领域,USB接口早已从PC外设的专属,演变为各类智能设备、工业模块、消费电子不可或缺的“标准配置”。作为一名长期与各类MCU打交道的工程师,我深刻体会到,一个稳定、高效的USB通信功能,往往是产品从“能用”到“好用”的关键跨越。它不仅仅是简单的数据线,更是设备与外界进行高速、可靠数据交换的生命线。
USB协议的魅力在于其高度的标准化和灵活性。它通过严格的主从架构(Host-Device)和差分信号传输(D+, D-),在嘈杂的电气环境中保证了数据的完整性。对于嵌入式开发者而言,集成在微控制器内部的USB控制器模块,将复杂的协议处理、时序管理、错误校验等任务从软件中剥离,由硬件高效完成,极大地减轻了CPU负担,并提升了系统的实时性与可靠性。
以德州仪器(TI)的MSPM0 G系列微控制器为例,其内置的USB 2.0全速控制器就是一个非常典型的工业级解决方案。它支持设备(Device)和嵌入式主机(Host)两种模式,集成了物理层收发器(PHY),提供了多达16个可配置的端点(Endpoint)和2KB的专用FIFO内存。这意味着,开发者可以用同一颗芯片,轻松实现一个USB鼠标(设备模式),或者一个读取U盘的数据采集器(主机模式)。本文将深入拆解这类USB控制器的工作原理,并聚焦于实际开发中最关键的配置细节与避坑指南,让你不仅能看懂数据手册,更能玩转它。
2. USB控制器整体架构与工作模式解析
要驾驭USB控制器,首先得从宏观上理解它的“身体结构”和“两种人格”。这就像了解一辆车,你得知道它的发动机、变速箱在哪,以及它既能自动挡也能手动挡。
2.1 模块功能框图与信号定义
参考MSPM0的USB模块框图,我们可以将其核心划分为几个协同工作的部分:
- USB PHY(物理层接口):这是与外部世界直接对话的“嘴巴和耳朵”。它负责将控制器内部的数字信号,转换成USB线缆上标准的差分模拟信号(D+, D-),并完成信号的同步、串并转换、CRC校验等底层工作。集成PHY是一大优势,省去了外置PHY芯片,简化了PCB布局和BOM成本。
- 协议引擎(Packet Encode/Decode, Scheduler):这是控制器的“大脑”。它根据USB 2.0协议规范,自动生成或解析令牌包(Token)、数据包(Data)、握手包(Handshake),并调度事务(Transaction)的执行。在设备模式下,它被动响应主机的调度;在主机模式下,它主动发起事务。
- 端点(Endpoint)与FIFO控制器:这是数据吞吐的“仓库和调度中心”。端点本质上是USB通信的逻辑管道,每个端点都有唯一的地址和方向(IN或OUT)。控制器提供的16个端点中,EP0固定用于控制传输,其余14个(7 IN + 7 OUT)可由固件灵活配置。每个端点都关联着一块FIFO(先进先出)存储器,用于临时存放待发送或已接收的数据包。2KB的专用RAM可以被动态划分给各个端点的FIFO。
- CPU接口与中断系统:这是与MCU核心的“联络官”。它提供一组寄存器,让CPU可以配置控制器、查询状态、读写FIFO数据。同时,它产生丰富的中断事件(如传输完成、总线复位、挂起唤醒等),通知CPU及时处理。
关键信号提示:USB控制器需要连接D+、D-和VBUS三根信号线。特别注意,D+和D-引脚内部有特殊的USB缓冲器,并非普通的GPIO,其引脚位置是固定的,不可随意映射。上电复位后,这些引脚默认是GPIO功能,必须在软件中将其重新配置为USB功能引脚(通常通过设置某个模式寄存器位,如
USBMODE.PHYMODE)。
2.2 设备模式(Device Mode)深度剖析
当你的嵌入式设备作为“从机”(如U盘、键盘、传感器)连接电脑或其他主机时,就工作在设备模式。
核心工作流程:
- 连接与上电:设备通过VBUS检测到连接后,通过内部上拉电阻(通常接在D+,表示全速设备)告知主机它的存在。
- 枚举(Enumeration):主机发起一系列标准控制传输(通过EP0),获取设备的描述符(设备描述符、配置描述符、接口描述符、端点描述符)。这个过程就是“打招呼”和“自我介绍”,主机据此为设备加载合适的驱动程序并分配一个唯一的设备地址(1-127)。
- 数据传输:枚举完成后,设备根据主机的指令,通过配置好的批量(Bulk)、中断(Interrupt)或同步(Isochronous)端点进行实际的数据传输。
在设备模式下的关键行为:
- 事务调度:设备完全被动,无法主动发起通信。它只能等待主机发来的令牌包(IN或OUT),然后做出响应(发送数据、接收数据或返回握手信号)。
- 控制端点EP0:这是所有USB设备都必须有的“管理通道”,专门用于枚举、配置和传输控制命令。其FIFO通常固定为64字节,且IN和OUT方向共享这块内存。
- NAK响应:如果主机请求数据(IN令牌)时,设备FIFO为空(数据未就绪),或者主机发送数据(OUT令牌)时,设备FIFO已满(无法接收),设备会回复NAK(Not Acknowledge)握手包。这告诉主机“我还没准备好,请稍后再试”。主机协议本身包含了重试机制。
2.3 主机模式(Host Mode)深度剖析
当你的嵌入式设备需要充当“主机”去管理其他USB设备(如连接U盘、鼠标)时,就工作在主机模式。这在很多脱离PC的嵌入式场景中非常有用,如车载系统读取U盘、工控机连接扫码枪。
核心工作流程:
- 检测与供电:主机通过持续监测D+/D-线状态来检测设备连接。一旦检测到,主机通过VBUS线为设备提供电源(通常为5V)。
- 复位与枚举:主机向总线发送一个持续的复位信号(SE0状态),使设备进入默认状态(地址0)。随后,主机作为主动方,发起对设备的枚举过程,步骤与设备模式视角相反。
- 调度与传输:主机负责管理整个总线的带宽,按照一定的调度算法(如对于全速/低速中断和同步传输,以1ms帧为单位)向各个设备发起事务。
在主机模式下的关键行为:
- 主动发起:主机控制器需要软件设置相关寄存器(如设置
REQPKT位)来主动发起一个IN或OUT事务请求。 - 事务翻译:如果系统通过USB 2.0集线器(Hub)连接了一个低速(Low-Speed)设备,主机控制器硬件会自动进行“事务翻译”,将主机发出的全速事务转换成低速事务,这对开发者是透明的。
- 错误处理:主机需要处理设备可能返回的各种错误,如STALL(端点 halted)、NAK等,并决定重试或上报错误。
模式选择心得:选择设备模式还是主机模式,在芯片选型时就要确定,因为两者对软件栈(USB Device Stack / USB Host Stack)的要求完全不同。有些高级的USB控制器支持OTG(On-The-Go),可以在两种模式间动态切换,但这需要更复杂的协议(HNP, SRP)和软件支持。对于MSPM0这类侧重应用的MCU,固定模式的设计反而更简单可靠。
3. 核心机制:端点、FIFO与双包缓冲配置实战
理解了宏观模式,我们深入到最影响性能和稳定性的微观层面:端点配置和FIFO管理。这是USB驱动开发的核心,配置不当会导致数据丢失、性能低下甚至通信失败。
3.1 端点(Endpoint)配置详解
端点是USB通信的基石,你可以把它理解为设备上的一个“数据信箱”,每个信箱有唯一的编号和收件/发件方向。
- 控制端点(EP0):双向端点(既有IN也有OUT方向),但共享同一个端点号0。它用于传输标准的USB请求,如获取描述符、设置地址、设置配置等。其传输是可靠的,拥有最高的总线优先级。切记:所有USB设备都必须实现一个控制端点,且其最大包长(MPS)固定为8、16、32或64字节(对于全速设备通常是64字节)。
- 中断端点(Interrupt Endpoint):用于传输少量、但需要保证延迟时间的数据。如USB键盘、鼠标。主机会以固定的时间间隔(如1ms到255ms)来轮询(Poll)这个端点。即使设备没有新数据,也会进行一次事务(可能返回NAK)。
- 批量端点(Bulk Endpoint):用于传输大量、对实时性要求不高但必须准确的数据。如U盘、打印机。它不占用固定带宽,只在总线空闲时传输,因此速度可能波动,但可靠性最高(有错误重传)。
- 同步端点(Isochronous Endpoint):用于传输实时性要求高的流数据,如USB音频、视频。它占用固定的带宽,保证每帧(1ms)都能传输一次,但不保证数据一定正确(没有握手包,错误不重传)。
配置要点: 在MSPM0中,除了EP0,其余端点(EP1-EP7)的IN和OUT部分可以独立配置成不同的类型。例如,你可以将EP1-IN配置为批量传输用于上传数据,而将EP1-OUT配置为中断传输用于接收控制命令。配置主要通过设置端点控制状态寄存器(如TXCSRHn.TYPE,RXCSRHn.TYPE)中的类型字段来完成。
3.2 FIFO内存规划与双包缓冲(Double-Packet Buffering)
FIFO是端点的数据缓冲区。2KB的RAM如何分配给多个端点,直接决定了系统的吞吐能力和响应速度。
1. FIFO大小计算与分配每个端点的FIFO大小必须至少能容纳一个最大数据包。对于全速设备,批量/中断端点的最大包长是64字节,同步端点最大可达1023字节。
- 单包缓冲(Single-Packet):FIFO大小 = 最大包长(Max Packet Size, MPS)。这是最节省内存的方式,但效率较低。因为CPU必须在当前包被USB引擎完全发送/接收完成后,才能准备下一个包的数据,中间存在等待时间。
- 双包缓冲(推荐):FIFO大小 ≥ 2 × MPS。这是提升性能的关键。它允许CPU在USB引擎处理一个包的同时,准备下一个包的数据,实现了“流水线”操作,几乎可以占满USB总线的理论带宽。
分配策略: 通常通过TXFIFOADD和RXFIFOADD寄存器来设置每个端点FIFO的起始地址和大小。你需要像管理堆内存一样,手动规划这片2KB的空间。一个典型的分配例子如下:
| 端点 | 方向 | 类型 | 最大包长 | 分配大小 | 起始地址 (示例) | 缓冲策略 |
|---|---|---|---|---|---|---|
| EP0 | IN/OUT | 控制 | 64字节 | 64字节 | 0x0000 | 共享,单包 |
| EP1 | IN | 批量 | 64字节 | 128字节 | 0x0040 | 双包 |
| EP1 | OUT | 批量 | 64字节 | 128字节 | 0x00C0 | 双包 |
| EP2 | IN | 中断 | 8字节 | 16字节 | 0x0140 | 双包 |
| EP3 | IN | 同步 | 256字节 | 512字节 | 0x0150 | 双包 |
注意:地址必须对齐,且不能重叠。计算下一个起始地址时,要严格累加上一个端点分配的大小。
2. 双包缓冲机制运作流程这是理解高效USB编程的关键。以设备模式的IN传输(设备发送数据给主机)为例:
- 使能:首先,需要清除
TXDPKTBUFDIS寄存器中对应端点的禁用位。 - 填充包1:CPU将第一个数据包写入端点的FIFO。当写入的数据量达到MPS(或手动设置)时,硬件自动(若
AUTOSET=1)或软件手动将TXCSRLn.TXRDY位置1,表示“包1已就绪,可以发送”。 - 立即填充包2:一旦
TXRDY置位,USB引擎开始发送包1,同时硬件会清除TXRDY并产生中断(如果使能)。此时,CPU可以立即向FIFO中填充第二个数据包,而无需等待包1发送完成。填充完成后,再次设置TXRDY。 - 状态查询:通过查询
TXCSRLn.FIFONE位,可以知道FIFO中是否还有未发送完的包。如果FIFONE=1,表示还有一个包在排队,CPU只能再填充一个包;如果FIFONE=0,表示FIFO全空,CPU可以连续填充两个包。
OUT传输(设备接收主机数据)同理,只是方向相反,核心是RXRDY和FIFONE位的操作,以及AUTOCL(自动清除)位的使用。
双包缓冲配置心得:对于任何需要连续、高速传输数据的端点(如批量传输的U盘、摄像头),务必启用双包缓冲。这能将USB带宽利用率从不足50%提升到90%以上。对于传输间隔很长或数据量很小的端点(如每秒报告一次状态的HID设备),单包缓冲足以应对。
4. 关键配置流程与寄存器操作指南
理论最终要落到代码上。下面以MSPM0 USB设备模式初始化为例,拆解关键步骤和寄存器操作。
4.1 初始化与基础配置流程
时钟与电源使能:
// 使能USB外设时钟 CLK->PERIPHCLKEN |= CLK_PERIPHCLKEN_USB_MASK; // 等待时钟稳定... // 使能USB模块电源(如果存在独立电源控制位) USB->POWER |= USB_POWER_SOFTCONN_MASK; // 先保持软断开状态这是第一步,没有时钟,USB控制器就是一块“砖头”。
引脚复用配置:
// 将指定GPIO引脚功能切换到USB_D+和USB_D- IOMUX->ALTERNATE[USB_DP_PIN] = PIN_ALT_FUNC_USB; IOMUX->ALTERNATE[USB_DM_PIN] = PIN_ALT_FUNC_USB; // 根据数据手册,可能还需要禁用这些引脚的上拉/下拉电阻避坑提示:务必查阅芯片数据手册的“引脚复用表”,确认USB_DM/DP对应的具体GPIO引脚编号。配置错误会导致USB无法识别。
PHY与模式选择:
// 选择内部PHY,并设置为设备模式 USB->USBMODE = USB_USBMODE_PHYMODE_INTERNAL | USB_USBMODE_CM_DEVICE; // 如果需要,配置PHY的某些参数,如驱动强度VBUS检测配置(自供电设备必需): 对于自供电设备,USB规范要求必须在VBUS掉电后10秒内断开内部上拉电阻。MSPM0提供了几种方法,推荐使用比较器(COMP)方案,因其在低功耗模式下仍可工作。
// 假设使用COMP1,通过电阻分压监测VBUS // 1. 配置COMP1正端输入为外部引脚(连接分压后的VBUS) // 2. 配置COMP1负端输入为内部DAC,设置DAC输出为约1.0V(对应VBUS 4.0V) // 3. 使能COMP1,并配置其输出在上升沿和下降沿产生中断 COMP->CONTROL1 = COMP_CONFIG_FOR_VBUS_DETECT; NVIC_EnableIRQ(COMP1_IRQn); // 在COMP中断服务程序中 void COMP1_IRQHandler(void) { if (COMP->STATUS & COMP_STATUS_OUT1_MASK) { // VBUS电压高于阈值(USB插入) USB->POWER |= USB_POWER_SOFTCONN_MASK; // 软连接 } else { // VBUS电压低于阈值(USB拔出) USB->POWER &= ~USB_POWER_SOFTCONN_MASK; // 软断开 // 可选:在10秒定时器后,彻底关闭上拉电阻相关电路 } COMP->STATUS = ...; // 清除中断标志 }重要警告:绝对不要将5V的VBUS信号直接连接到MCU的任何GPIO引脚,MSPM0的IO口通常不是5V耐受的,这会永久损坏芯片!必须使用电阻分压或电平转换电路。
端点FIFO地址配置:
// 假设规划如上一节的表格 USB->TXFIFOADD = 0x0040; // EP1-IN FIFO 起始地址 USB->TXFIFOSZ = (EP1_TX_SIZE / 16) - 1; // 设置EP1-IN FIFO大小,寄存器值= (Size/16)-1 USB->RXFIFOADD = 0x00C0; // EP1-OUT FIFO 起始地址 USB->RXFIFOSZ = (EP1_RX_SIZE / 16) - 1; // 设置EP1-OUT FIFO大小 // ... 配置其他端点 // 注意:EP0的FIFO通常是固定的,无需配置起始地址,只需配置大小(如果可配)端点属性配置:
// 选择要配置的端点索引 USB->EPINDEX = 1; // 配置EP1 // 配置EP1-IN 为批量传输,最大包长64字节,使能双包缓冲 USB->TXCSRH1 = USB_TXCSRH1_TYPE_BULK; // 设置类型 USB->TXMAXP1 = 64; // 设置最大包长 USB->TXDPKTBUFDIS &= ~(1 << 1); // 清除EP1的双包缓冲禁用位(使能双缓冲) // 配置EP1-OUT USB->RXCSRH1 = USB_RXCSRH1_TYPE_BULK | USB_RXCSRH1_AUTOCL; // 批量传输,使能自动清除RXRDY USB->RXMAXP1 = 64; USB->RXDPKTBUFDIS &= ~(1 << 1);中断使能与软连接:
// 使能所需的中断:传输完成、总线复位、挂起/恢复等 USB->IE = USB_IE_RESET_MASK | USB_IE_SUSPEND_MASK | USB_IE_TX1_MASK | USB_IE_RX1_MASK; NVIC_EnableIRQ(USB_IRQn); // 最后,执行软连接,让设备出现在总线上 USB->POWER |= USB_POWER_SOFTCONN_MASK;
4.2 数据传输的软件流程
初始化完成后,设备进入枚举阶段,主机通过EP0完成枚举。之后,应用数据传输开始。
IN传输(设备发送)示例:
void send_data_ep1_in(uint8_t *data, uint16_t len) { uint16_t packet_size; uint16_t sent = 0; while (sent < len) { // 等待FIFO有空间(通过检查FIFONE位或等待TX中断) while ((USB->TXCSRL1 & USB_TXCSRL1_FIFONE_MASK) != 0) { // 可能进入低功耗,等待中断唤醒 } // 计算本次要写入的包大小(不超过最大包长) packet_size = (len - sent) > 64 ? 64 : (len - sent); // 将数据写入FIFO(实际是写入以端点FIFO起始地址为基址的存储区) write_to_fifo(USB_FIFO_EP1_IN, &data[sent], packet_size); sent += packet_size; // 如果写满了一个最大包,且AUTOSET已使能,硬件会自动置位TXRDY。 // 否则,或者最后一个包不足最大包,需要手动置位TXRDY。 if (packet_size == 64) { // 依赖AUTOSET,或在此手动设置 TXRDY // USB->TXCSRL1 |= USB_TXCSRL1_TXRDY_MASK; } else { // 短包,必须手动置位TXRDY以触发发送 USB->TXCSRL1 |= USB_TXCSRL1_TXRDY_MASK; } } } // 在USB中断服务程序中处理TX完成 void USB_IRQHandler(void) { if (USB->TXIS & (1 << 1)) { // EP1-IN 传输完成中断 // 清除中断标志 USB->TXIS = (1 << 1); // 检查状态,确认发送成功(TXCSRL1.ERROR位等) // 如果使用双缓冲,可以在此准备下一包数据 // 通知主循环或任务,发送完成/可继续发送 } // ... 处理其他中断 }OUT传输(设备接收)示例:
// 通常在中断中处理OUT数据接收 void USB_IRQHandler(void) { if (USB->RXIS & (1 << 1)) { // EP1-OUT 接收完成中断 uint16_t rx_count; // 读取接收到的字节数(具体寄存器名需查手册) rx_count = USB->RXCOUNT1; // 从FIFO读取数据 read_from_fifo(USB_FIFO_EP1_OUT, rx_buffer, rx_count); // 清除RXRDY位,告知主机已处理完,准备接收下一包 // 如果使能了AUTOCL且收到的是最大包,硬件已自动清除 USB->RXCSRL1 &= ~USB_RXCSRL1_RXRDY_MASK; // 清除中断标志 USB->RXIS = (1 << 1); // 处理接收到的数据... } }5. 高级话题与疑难问题排查实录
即使配置正确,在实际开发中仍会遇到各种“诡异”的问题。下面分享一些常见坑点和排查思路。
5.1 枚举失败问题排查
这是新手最常遇到的问题。设备连接后,电脑提示“无法识别的USB设备”或没有任何反应。
排查清单:
- 硬件连接:
- 测量VBUS:用万用表确认USB口提供了稳定的5V电压。
- 检查D+/D-:使用示波器或逻辑分析仪查看信号。连接瞬间,主机应发出复位信号(SE0状态持续至少10ms),随后D+(全速)或D-(低速)应被上拉到3.3V。如果没有上拉,检查内部上拉电阻是否使能(
USBPOWER.SOFTCONN位是否置位?)。 - 阻抗匹配:USB差分线对(90欧姆差分阻抗)的PCB布线是否符合要求?过长的走线、糟糕的参考平面都会导致信号完整性差。
- 软件配置:
- 描述符是否正确?这是枚举的“剧本”。使用USB协议分析仪(如Beagle USB, Ellisys)是终极利器,可以抓取总线上的每一个数据包,看到主机发送了什么请求,设备回复了什么。没有分析仪时,可以逐字节核对设备描述符、配置描述符等,确保长度、类型、字段值都符合USB规范。
- 端点0最大包长:在设备描述符中声明的
bMaxPacketSize0必须与硬件EP0 FIFO的大小匹配,且必须是8, 16, 32, 64之一。不匹配会导致第一个数据阶段就出错。 - 地址设置时机:在
SET_ADDRESS请求的状态阶段完成后(即主机发来IN令牌,设备返回0长度包,主机回复ACK之后),才能修改USB.FADDR寄存器。过早修改会导致主机后续的请求发往旧地址而无人响应。这是手册中明确警告的经典错误。 - 中断是否及时响应?枚举过程是主机驱动的,设备必须在极短的时间内(微秒级)响应请求。如果CPU忙于其他高优先级任务或中断被长时间关闭,会导致响应超时,枚举失败。确保USB中断优先级足够高,且中断服务程序执行时间尽可能短。
5.2 数据传输不稳定或速度慢
设备能识别,但传输文件经常出错或速度远低于理论值(全速12 Mbps,约1.2 MB/s)。
排查与优化:
- 双包缓冲是否启用:这是影响吞吐量的首要因素。确认
TXDPKTBUFDIS和RXDPKTBUFDIS寄存器中对应端点的位已被清除。 - FIFO大小是否足够:对于批量传输,FIFO大小至少应为2倍最大包长。如果传输大文件,可以考虑分配更大的FIFO(如4倍甚至8倍包长),让CPU有更充裕的时间准备数据,减少NAK的概率。
- CPU处理速度:USB全速每毫秒一帧,批量传输虽然异步,但数据到达频率依然很高。如果CPU从FIFO读取数据或向FIFO写入数据的速度跟不上,就会导致缓冲区满/空,触发NAK,主机不得不重试,降低有效带宽。优化数据处理算法,使用DMA是根本解决方案。
- 使用DMA:MSPM0的USB控制器支持DMA触发。对于大数据量的端点,强烈建议配置DMA。将USB FIFO与内存之间的数据搬运工作交给DMA,可以解放CPU,并确保数据及时处理。配置时注意DMA传输宽度与FIFO访问对齐,以及DMA完成中断的处理。
- 总线干扰:如果设备是自供电,且与主机共地不良,可能导致地电位差,引起数据错误。确保USB屏蔽层良好接地,电源干净。
5.3 低功耗与挂起(Suspend)模式
USB设备在总线空闲3ms后必须进入挂起模式,电流消耗需低于500uA(对于总线供电设备)。
实现要点:
- 检测挂起:使能
USBIE.SUSPEND中断。当总线空闲超时,硬件会自动产生挂起中断。在此中断中,软件应:- 保存必要的上下文。
- 将MCU自身切换到低功耗模式(如LPM3)。
- 注意,USB模块的某些时钟可能需要在进入低功耗前配置。
- 唤醒:唤醒源可以是USB总线上的
RESUME信号(主机发起),也可以是其他的外部中断(如按键)。如果是USB唤醒,硬件会产生RESUME中断。在唤醒中断服务程序中:- 将MCU从低功耗模式唤醒。
- 恢复USB模块和应用的正常工作状态。
- 如果需要远程唤醒主机(即设备主动唤醒总线),需要在挂起前设置好远程唤醒能力,并在唤醒时先设置
USBPOWER.RESUME位驱动恢复信号至少10ms,然后清除该位。
- VBUS掉电处理(自供电设备):如前所述,必须用比较器监控VBUS。在VBUS掉电中断中,除了软断开,还应考虑在10秒定时器后,将MCU切换到更深度的低功耗模式或完全关机。
5.4 设备模式下的零长度包(ZLP)处理
零长度包在USB控制传输的状态阶段和批量传输的末尾有特殊作用。
- 控制传输:在状态阶段,主机发送一个IN令牌,设备必须返回一个零长度包(ZLP)作为ACK。这是很多枚举请求(如
SET_ADDRESS,SET_CONFIGURATION)完成的标志。硬件通常会自动处理EP0状态阶段的ZLP,但需要正确设置DATAEND位。 - 批量传输:当需要传输的数据总量正好是最大包长的整数倍时,必须在最后一个满尺寸数据包之后,再发送一个ZLP,以通知主机数据发送完毕。这是很多开发者容易遗漏的地方。例如,你要发送64字节数据,最大包长是64字节。你不能只发一个64字节的包就结束,必须再发一个ZLP。否则,主机可能会一直等待更多数据。
// 批量IN传输发送ZLP示例 if ((total_length % max_packet_size) == 0) { // 数据长度是包长的整数倍,需要发送一个ZLP // 对于短包(不足最大包长),硬件/协议会自动标识结束,无需ZLP USB->TXCSRLn |= USB_TXCSRL1_TXRDY_MASK; // 在FIFO为空时置位TXRDY,将发送一个ZLP }
调试USB是一个系统工程,从硬件信号、电源、到软件协议栈、驱动逻辑,环环相扣。我的经验是,分而治之:先用最简单的代码实现枚举,确保描述符和EP0通信正常;再逐个端点测试数据传输;最后集成业务逻辑并优化性能。拥有一套可靠的底层USB驱动框架后,在此基础上开发各种USB设备应用,就会变得事半功倍。
