1. 深入理解USB主机控制器:从总线仲裁到数据搬运
搞了这么多年嵌入式,USB这块骨头算是啃得比较透了。今天咱们不聊那些虚的协议栈,就扎扎实实把USB主机控制器(Host Controller)里最核心的几件事掰扯清楚——事务怎么处理、调度器怎么工作、寄存器到底该怎么配。尤其是当你手头用的是TI的MSPM0这类资源受限的MCU时,理解硬件怎么干活,比你调一百遍软件库都管用。
USB本质上是个严格的主从架构,主机是绝对的老大,掌控一切。它要干三件大事:管理总线时间(一毫秒一个帧,分得清清楚楚)、发起所有通信(设备没资格主动说话)、给设备供电和枚举(插上电得先验明正身)。主机控制器就是MCU里专门干这些脏活累活的硬件模块,你把寄存器配好,它就能自动帮你发令牌(Token)、收数据、检错重试,解放CPU。
为什么非得用主机模式?很多场景下,你的设备需要去读取U盘、连接键盘鼠标、或者跟其他USB从设备通信。这时候,你的MCU就得扮演“主机”这个角色。和从设备(Device)模式相比,主机模式的编程思维是反过来的:从设备是“等活干”,主机是“派活干”。你得主动规划每个帧(Frame)里时间槽(Time Slot)怎么分配,怎么处理设备的各种响应(ACK, NAK, STALL),这些逻辑全藏在那一大堆寄存器里。
2. 核心事务处理机制:IN、OUT与状态机
事务(Transaction)是USB通信的原子操作,一次完整的事务包括令牌(Token)、数据(Data)和握手(Handshake)三个阶段。主机控制器硬件帮我们实现了这个状态机,我们要做的就是通过寄存器告诉它“干什么”,以及处理它“干完了”或“出问题了”的通知。
2.1 IN事务:主机向设备要数据
IN事务就是主机向设备索要数据。核心思想是:主机先发一个IN令牌包指明方向和端点,设备如果有数据就回数据包,主机收到后回ACK确认;如果设备没准备好,就回NAK;如果端点出错了(比如没配置),就回STALL。
这个过程在MSPM0的USB控制器里,主要涉及几个关键寄存器位:
- USB.RQPKTCOUNTn:块传输请求包计数。这是批量(Bulk)传输高效化的关键。如果你知道这次IN传输总共要收多少个包(比如从U盘读一个512字节的扇区,每个包最大64字节,共8个包),你就提前把这个值设好(比如8)。控制器每成功完成一次IN事务(收到一个数据包),这个值就自动减1。当它减到0时,AUTORQ位会被硬件自动清零,从而停止自动发起新的IN请求。这避免了软件频繁中断去处理每个包,大大提升了大数据量传输的效率。
- USB.RXMAXPn:接收端点最大负载寄存器。这个必须配,它告诉控制器,你期望从这个端点一次最多收到多少字节的数据。设备返回的数据包不能超过这个值。如果设备返回的数据长度小于
MAXLOAD,这就是一个“短包”(Short Packet),它通常标志着一批数据传输的结束。在未知长度的传输中(比如读取文件直到结束),你需要把USB.RQPKTCOUNTn清零,并依靠检测到短包(数据长度 <MAXLOAD)来终止AUTORQ循环。 - USB.CSRL0及相关端点的USB.RXCSRLn:这里是状态和控制的枢纽。
RXRDY位表示FIFO里有数据待读取;STALLED位表示收到了设备的STALL握手包,意味着端点永久错误,需要软件干预;ERROR位则表示超时或通信错误(如CRC校验失败)。
IN事务的硬件行为逻辑:
- 软件设置目标设备的地址、端点号、传输类型(Bulk/Interrupt)到
USB.TXTYPEn寄存器,并设置AUTORQ和USB.RQPKTCOUNTn(如果需要)。 - 控制器在调度到的时刻,自动发出IN令牌包。
- 设备响应NAK:控制器会根据
USB.NAKLMT0或USB.TXINTERVALn/RXINTERVALn寄存器中配置的NAK重试上限,持续重试。这避免了因设备临时繁忙(NAK)而导致总线被一个端点独占。 - 设备响应STALL:控制器立即停止该端点的后续事务,并置位
STALLED状态位,产生中断。软件必须清除这个错误状态,通常是通过重新配置端点或进行错误恢复流程。 - 设备无响应或数据错误:如果超时(总线无响应)或数据包CRC错误,控制器会自动重试(通常是3次)。如果3次都失败,它会清除
REQPKT位(停止请求),并设置ERROR状态位,通知软件进行错误处理。
实操心得:调试IN事务时,最常卡住的地方就是
STALLED状态没被及时清除。一旦看到STALLED位置1,你的后续请求都会被硬件静默丢弃。正确的做法是在中断服务程序里,先读取USB.RXCSRLn确认错误类型,然后向STALLED位写1来清除它(这是一个W1C——写1清零的位)。不清除它,通信就卡死了。
2.2 OUT事务:主机向设备发数据
OUT事务是主机发送数据到设备。流程是:主机发OUT令牌包,紧接着发数据包,设备收到后回握手包(ACK/NAK/STALL)。
其核心寄存器与IN事务类似,但关注的是发送侧:
- USB.TXCSRLn:发送控制状态寄存器低字节。其中的
TXRDY位是发送流程的油门。你必须把要发送的数据写入端点的FIFO后,再手动将TXRDY置1,控制器才知道“数据已备好,可以发送了”。对于每个数据包都需要重复此操作。 - USB.TXCSRHn:发送控制状态寄存器高字节。这里的
AUTOSET位是个“懒人神器”。如果你配置的USB.TXMAXPn(发送最大负载)是64字节,并且你每次正好都写满64字节到FIFO,那么设置AUTOSET=1后,硬件会在你写完第64个字节的瞬间,自动帮你把TXRDY位置1,省去了一次软件操作。但如果你发送的数据包小于最大负载,AUTOSET不会触发,仍需手动设置TXRDY。
OUT事务的硬件行为逻辑:
- 软件将数据写入目标端点的FIFO,并设置
TXRDY=1(或依赖AUTOSET)。 - 控制器在调度到的时刻,自动发出OUT令牌包和数据包。
- 设备响应NAK:同IN事务,会根据NAK限制进行重试。
- 设备响应STALL:控制器停止该端点事务,在
USB.TXCSRLn中置位STALLED并产生中断。同样需要软件清除。 - 设备无响应或错误:重试3次失败后,控制器会清空该端点的发送FIFO(防止旧数据被重复发送),并设置
ERROR位。
避坑指南:
AUTOSET虽好,但不能滥用。在等时(Isochronous)传输或高带宽中断传输中,数据包大小可能变化,或者需要更精确的发送时机控制,这时手动管理TXRDY更可靠。另外,在调试阶段,建议先关闭AUTOSET,通过手动置位TXRDY来确保每个数据包的发送时机都在你的掌控之中,便于定位问题。
3. 事务调度与带宽管理:帧、微帧与调度器
USB总线时间被精确划分为1毫秒的帧(Frame,全速)或125微秒的微帧(Microframe,高速)。调度器(Scheduler)是主机控制器内部的一个硬件状态机,它负责在每个帧/微帧内,决定何时发起哪个端点的事务。理解调度规则,是优化USB系统实时性和吞吐量的关键。
3.1 调度器的工作原理
MSPM0的USB主机控制器内置了一个基于帧计数器的自动调度器:
- 帧起始(SOF):对于全速设备,每个帧开始时,控制器会自动发送一个SOF(Start Of Frame)包。对于低速设备,则发送一个特殊的“K”状态保持信号,防止其进入挂起(SUSPEND)模式。
- 轮询检查:发送完SOF后,调度器开始循环检查所有已配置的端点。
- 何为“活跃事务”:
- 对于接收端点(IN方向):
REQPKT位被置位。 - 对于发送端点(OUT方向):
TXRDY位被置位,或者FIFONE(FIFO非空)位被置位。
- 对于接收端点(IN方向):
- 调度决策:
- 中断(Interrupt)传输:每个端点都有一个独立的间隔计数器(
USB.TXINTERVAL[n]或USB.RXINTERVAL[n])。调度器只在某个帧的第一次调度循环中,检查该端点的计数器是否减到零。如果是,则发起一次中断事务,然后重置计数器。这就保证了中断传输严格按设定的间隔(1-255帧)发生,满足其“周期性、有界延迟”的要求。 - 批量(Bulk)传输:没有固定的周期。调度器一旦发现某个批量端点有活跃事务,并且当前帧剩余时间足够完成这次事务(包括令牌、数据、握手及包间延迟),就会立即启动它。如果事务需要重试(比如收到NAK),调度器不会死等,而是先跳过去检查其他端点,处理完一圈后,再回来重试这个未完成的事务。这种“公平轮询”机制防止了一个持续NAK的端点饿死其他端点的通信。
- NAK超时:控制器允许为批量端点设置一个NAK超时限制(通过
USB.NAKLMT0或TXINTERVAL/RXINTERVAL寄存器)。如果一个端点持续返回NAK超过这个时间,控制器会暂停该端点的事务并产生超时中断,避免无谓地浪费总线带宽。
- 中断(Interrupt)传输:每个端点都有一个独立的间隔计数器(
3.2 调度策略的配置与实践
- 中断端点配置:关键在于
USB.TXINTERVALn/USB.RXINTERVALn寄存器。这个值不是以毫秒为单位,而是以帧数为单位。例如,一个全速中断端点,如果希望每10ms查询一次,则设置间隔值为10。计算方式很简单:期望的轮询间隔(ms) / 帧长度(1ms)。注意,USB规范对中断端点有最大数据负载限制(全速下最多64字节/帧),配置时需确保数据量在带宽允许范围内。 - 批量端点配置:主要配置
USB.TXMAXPn/USB.RXMAXPn(最大包大小,通常为8、16、32、64字节)以及可选的NAK超时限制。批量传输利用帧内的剩余时间进行,其吞吐量不保证,但可靠性最高(有错误重试)。 - 等时端点配置:在MSPM0中,等时传输也使用
TXINTERVALn/RXINTERVALn来设置周期(通常为1帧)。等时传输没有握手包,不重试,用于音频、视频等容错实时流。需要特别注意FIFO大小和DMA的配合,以防止数据欠载(Underrun)或过载(Overrun)。
带宽计算示例:假设一个全速USB主机,要连接一个键盘(中断端点,每10ms报告一次,数据8字节)和一个串口转换器(批量端点,最大包64字节)。
- 键盘:每帧(1ms)占用带宽 = (令牌包+数据包+握手包)时间 ≈ 0.1ms,10ms间隔下,平均每帧占用0.01ms,带宽占用约1%。
- 批量端点:占用所有剩余时间。但在调度器策略下,它不会阻塞中断端点。因为调度器每帧开始时会先检查并执行“到期”的中断事务,然后再处理批量事务。关键点:调度是硬件自动完成的,但你的配置(端点类型、间隔、最大包大小)决定了硬件如何分配这块固定的1ms“蛋糕”。配置不当会导致中断响应不及时或批量吞吐量低下。
4. 关键寄存器配置详解与驱动编写要点
寄存器是软件与USB主机控制器硬件对话的唯一窗口。配置错了,通信肯定不通。下面我们抛开手册的罗列,从功能模块的角度来梳理。
4.1 时钟与引脚初始化
这是所有操作的前提,时钟不对,USB PHY根本没法工作。
// 假设使用SYSPLL为USB提供60MHz时钟 (USBCLK) // 1. 配置系统时钟SYSCLK,确保 >= 30MHz(USB操作最低要求) // 2. 配置SYSPLL,输出480MHz,再分频得到60MHz USBCLK // 3. 使能USB模块时钟和外设复位释放(操作PWREN, CLKCTL, RSTCTL等系统级寄存器) // 4. 配置USB引脚模式,将DP/DM引脚从GPIO模式切换到USB模式 USB->USBMODE_b.PHYMODE = 1; // PHY模式使能 // 5. 配置为主机模式 USB->USBMODE_b.HOST = 1; USB->USBMODE_b.DEVICEONLY = 0; // 确保设备模式位为04.2 端点配置流程
端点(Endpoint)是USB通信的“管道”。每个端点有独立的寄存器集,通过USB.EPINDEX寄存器来选择当前要配置的端点索引。
配置一个批量输入(Bulk IN)端点(例如EP1 IN)的典型步骤:
// 步骤1:选择要配置的端点索引 USB->EPINDEX = 1; // 配置端点1 // 步骤2:配置端点类型、目标设备地址和端点号 USB->IDXRXTYPE = (0x02 << 4) | // PROTO: 0x02 表示Bulk传输 (0x00 << 6) | // SPEED: 0x00 默认(与控制器同速),或 0x02 全速 (1); // TEP: 目标设备的端点号是1 // 步骤3:配置接收最大数据包大小 USB->IDXRXMAXP = 64; // 假设最大包为64字节 // 步骤4:配置轮询间隔(对于Bulk,此字段用作NAK超时限制,单位是帧数) USB->IDXRXINTERVAL = 10; // NAK超时设为10帧(10ms) // 步骤5:分配FIFO空间(动态FIFO sizing模式) // 先计算FIFO起始地址:EP0固定占用前64字节。假设我们给EP1 IN分配128字节。 uint16_t fifo_start_addr = 64; // 字节偏移地址 USB->IDXRXFIFOSZ_b.SIZE = 4; // SIZE=4 表示128字节 (2^(4+3) = 128) USB->IDXRXFIFOSZ_b.DPB = 0; // 单缓冲 USB->IDXRXFIFOADD = fifo_start_addr / 8; // 寄存器值 = 字节地址 / 8 // 步骤6:使能端点的DMA(如果需要)并设置自动清除等 USB->IDXRXCSRH_b.AUTOCLR = 1; // 收到完整包后自动清除RXRDY USB->IDXRXCSRH_b.DMAEN = 1; // 使能DMA请求 // 在USBDMASEL寄存器中,将某个DMA触发通道(如TRIGARX)映射到这个端点(EP1) USB->USBDMASEL_b.TRIGARX = 1; // 步骤7:对于主机模式,还需要配置目标设备的地址和Hub信息(如果通过Hub连接) USB->RXFUNCADDR[1] = device_address & 0x7F; // 设备地址(7位) if (device_is_through_hub) { USB->RXHUBADDR[1] = (hub_multiple_tt << 7) | (hub_address & 0x7F); USB->RXHUBPORT[1] = hub_port_number & 0x7F; }配置一个中断输出(Interrupt OUT)端点(例如EP2 OUT)的步骤类似,但关注发送寄存器:
USB->EPINDEX = 2; // 配置端点2 USB->IDXTXTYPE = (0x03 << 4) | (0x00 << 6) | (2); // 中断传输,目标端点2 USB->IDXTXMAXP = 8; // 键盘报告通常8字节 USB->IDXTXINTERVAL = 10; // 轮询间隔10帧(10ms) // ... 配置TX FIFO地址 (IDXTXFIFOSZ, IDXTXFIFOADD) USB->IDXTXCSRH_b.AUTOSET = 1; // 自动设置TXRDY(如果总是发最大包) USB->IDXTXCSRH_b.DMAEN = 1; // ... 配置DMA触发映射和Hub信息4.3 传输控制:启动、停止与错误处理
配置好端点后,如何启动一次传输?
启动一次批量IN传输(使用AUTORQ):
USB->EPINDEX = 1; // 选择EP1 USB->IDXRXCSRL_b.STALLREQPKT = 0; // 确保STALL位已清除 USB->RQPKTCOUNT[1] = total_packets; // 设置要接收的总包数 USB->IDXRXCSRH_b.ISOAUTORQ = 1; // 对于批量传输,此位使能自动请求 // 硬件会自动发起IN请求,直到收完total_packets个包或收到短包。启动一次中断OUT传输:
USB->EPINDEX = 2; // 1. 将数据写入端点FIFO (通过USB->FIFO[2]地址) memcpy_to_usb_fifo(USB_FIFO_EP2, data_buffer, data_len); // 2. 如果数据长度等于USB.IDXTXMAXP且AUTOSET=1,TXRDY会自动置1。 // 否则,需要手动置位TXRDY来启动发送。 if (data_len < USB->IDXTXMAXP) { USB->IDXTXCSRL_b.TXRDY = 1; }错误处理:必须在中断服务程序(ISR)或主循环中及时检查状态位。
void USB_IRQ_Handler(void) { uint32_t int_status = USB->RIS; // 读取原始中断状态 if (int_status & USB_RIS_INTRRX) { // 接收端点中断 uint16_t rx_isr = USB->RXIS; if (rx_isr & (1 << 1)) { // EP1中断 USB->EPINDEX = 1; uint8_t csrl = USB->IDXRXCSRL; if (csrl & USB_RXCSRL_STALLED) { // 1. 处理STALL错误 USB->IDXRXCSRL = USB_RXCSRL_STALLED; // W1C,写1清除 // 2. 可能需要重新配置端点或报告错误 } else if (csrl & USB_RXCSRL_ERROR) { // 处理超时等错误 USB->IDXRXCSRL = USB_RXCSRL_ERROR; } else if (csrl & USB_RXCSRL_RXRDY) { // 正常收到数据 uint16_t byte_count = USB->IDXRXCOUNT & 0x1FFF; read_from_usb_fifo(USB_FIFO_EP1, my_buffer, byte_count); // 如果AUTOCLR=0,需要手动清除RXRDY USB->IDXRXCSRL_b.RXRDY = 0; } } } // ... 清除全局中断标志等 }
5. 高级主题:Hub支持、DMA与电源管理
5.1 USB Hub的配置
当你的主机连接的是USB Hub,再接上全速/低速设备时,必须配置Hub地址和端口号寄存器。这是因为高速Hub内部有一个事务转换器(Transaction Translator, TT),负责在高速上行流和全速/低速下行流之间进行协议转换。
- 关键寄存器:
USB.TXHUBADDRn,USB.TXHUBPORTn,USB.RXHUBADDRn,USB.RXHUBPORTn。 - 配置时机:在设备枚举阶段,当你从Hub获取到设备连接的端口和Hub地址后,就需要为这个设备对应的每个端点配置这些信息。
- MULTTRAN位:在
HUBADDR寄存器中,最高位指示该Hub是单TT还是多TT。如果Hub有多个TT(通常一个下游端口对应一个TT),则需要置位此位,以优化调度。
5.2 DMA集成
USB控制器支持4个接收(RX)和4个发送(TX)DMA触发信号,可以映射到任意端点(EP1-EP7)。这能极大减轻CPU负担,尤其是在高速数据流(如音频、大量数据采集)场景。
配置步骤:
- 使能端点的DMA请求:在
USB.TXCSRHn.DMAEN或USB.RXCSRHn.DMAEN位置1。 - 选择DMA触发模式:
DMAMOD位选择模式0(每包中断)或模式1(全部传输完成中断)。 - 映射端点到DMA通道:在
USB.USBDMASEL寄存器中,将TRIGARX(例如)的值设置为目标端点号(如1代表EP1)。 - 配置DMA控制器:设置DMA源/目标地址、传输长度、触发源选择为对应的USB触发事件(如
USB_A_RX)。 - 处理DMA完成中断:在USB中断状态寄存器
USB.RIS中,有DMADONEARX等位指示DMA传输完成。
DMA模式选择建议:
- 模式0(DMAMOD=0):每完成一个数据包(最大
MAXP字节)就产生一次DMA中断。适合需要实时处理每个数据包的场景,但中断频繁。 - 模式1(DMAMOD=1):仅在
USB.RQPKTCOUNTn计数到0(块传输完成)或收到短包时,才产生一次DMA中断。适合大数据量的块传输,能显著减少中断次数。
5.3 电源管理与总线状态控制
通过USB.POWER和USB.DEVCTL寄存器管理主机会话和总线状态。
启动一次主机会话:
USB->DEVCTL_b.SESSION = 1; // 启动会话,开始检测设备连接 // 等待连接中断 (USBIS.CONN置位) while (!(USB->USBIS_b.CONN)); // 读取连接设备速度 if (USB->DEVCTL_b.FSDEV) { /* 全速设备 */ } else if (USB->DEVCTL_b.LSDEV) { /* 低速设备 */ } // 对设备进行复位 USB->POWER_b.RESET = 1; delay_ms(20); // 保持复位至少20ms USB->POWER_b.RESET = 0; // 之后可以开始枚举过程(通过控制传输EP0)挂起(SUSPEND)与恢复(RESUME):
// 进入挂起模式 USB->POWER_b.SUSPEND = 1; // 控制器完成当前事务后停止调度器和帧计数器 // 唤醒总线 USB->POWER_b.RESUME = 1; USB->POWER_b.SUSPEND = 0; delay_ms(20); // 产生至少20ms的恢复信号 USB->POWER_b.RESUME = 0; // 调度器和帧计数器重新启动Babble错误:如果总线在帧结束时仍然活跃,控制器会认为连接的设备发生故障(称为“babble”),产生
USBIS.RESETBABBLE中断并暂停所有事务。这通常需要软件干预,可能包括重置端口或重新枚举设备。
6. 调试技巧与常见问题排查
调试USB主机,逻辑分析仪或专用的USB协议分析仪几乎是必备的。但即使没有这些高级工具,通过仔细检查寄存器状态,也能解决大部分问题。
问题1:设备连接上了,但枚举失败(获取描述符超时)。
- 检查:
USB.DEVCTL中的FSDEV/LSDEV位是否正确识别了设备速度?速度识别错误会导致通信时序全乱。 - 检查:
USB.TYPE0(对于EP0)或端点的TXTYPE/RXTYPE寄存器中的SPEED字段是否与设备实际速度匹配? - 检查:控制传输的
DATA阶段的数据翻转(Data Toggle)是否正确?EP0的DATA阶段,第一个DATA包必须是DATA0。检查USB.CSR0H.DT位,并在SETUP阶段后确保其为0。 - 实操:在发送SETUP包前,手动清除
USB.CSR0H.DT位(通过设置DTWE后再写DT)。
问题2:批量传输中途卡住,不再产生中断。
- 检查:
USB.RXCSRLn或USB.TXCSRLn中的STALLED位是否被置位?这是最常见的原因。STALL需要软件清除。 - 检查:
USB.RQPKTCOUNTn是否已减到0?如果是,AUTORQ位会被清零,传输自动停止。 - 检查:FIFO是否溢出或欠载?检查
RXCSRLn.FULL或TXCSRLn.UNDRN错误位。 - 检查:DMA配置是否正确?如果使能了DMA但DMA没正常工作,数据可能卡在FIFO里。
问题3:中断传输的实时性不达标。
- 检查:
USB.TXINTERVALn/USB.RXINTERVALn设置是否合理?值太小可能超过总线带宽,值太大会导致延迟增加。 - 检查:是否有其他批量传输占用了过多带宽?USB是分时复用,一个帧内所有传输共享时间。可以用逻辑分析仪查看总线活动,确认中断事务是否在每个预定帧内都被调度了。
- 检查:设备端的中断端点间隔描述符(bInterval)是否与主机配置匹配?不匹配可能导致设备不响应或响应错误。
问题4:使用Hub后通信不稳定。
- 检查:
USB.TXHUBADDRn和USB.TXHUBPORTn(或RX对应寄存器)是否已正确配置为Hub的地址和端口号? - 检查:
MULTTRAN位是否正确设置?对于多TT Hub必须置1。 - 注意:Hub本身也需要被枚举和配置。确保你的主机代码完整实现了Hub的枚举流程(读取Hub描述符、设置端口电源等)。
调试心法:始终从EP0开始。EP0(控制端点)是USB通信的基石,所有枚举和配置都通过它。先用EP0成功完成设备枚举(至少完成到获取设备描述符),证明物理层、底层驱动和基本的控制传输逻辑是正确的,然后再去调试其他功能端点。把问题分层隔离,先确保主机控制器初始化、时钟、引脚配置正确,再确保端点配置正确,最后再处理数据传输逻辑。