1. 项目概述与核心价值
搞嵌入式USB设备开发的朋友,对“中断”和“DMA”这两个词一定不陌生。它们就像是设备控制器的“神经系统”和“高速公路”。中断负责及时响应主机的各种指令和状态变化,比如设备被挂起、恢复,或者某个端点有数据到达;而DMA则是在大数据量传输时,绕开CPU,让数据在内存和USB控制器之间直接“飞奔”,解放CPU算力。今天,我们就以TI经典的USB设备控制器(参考文档SPRU761A)为蓝本,把这两个核心机制——尤其是挂起/恢复中断和DMA操作——掰开揉碎了讲清楚。这不仅仅是读数据手册,更是理解如何让一个USB设备既省电又高效的关键。无论你是在设计一个低功耗的HID设备,还是一个需要高速传输数据的数据采集卡,吃透这部分内容,都能让你在调试时心里更有底,设计时方案更优化。
2. USB设备控制器中断体系深度解析
USB设备控制器通过一套精细的中断系统,让微处理器(MPU)能够及时知晓并处理总线上发生的各类事件。这套体系大致分为三类:通用USB中断、端点特定中断(非同步)和SOF(帧起始)中断。理解它们的触发条件和处理流程,是编写稳定驱动的基础。
2.1 挂起与恢复中断:低功耗的守门人
挂起(Suspend)和恢复(Resume)是USB电源管理的关键。当总线(D+和D-线)保持空闲状态超过3ms时,主机认为可以挂起设备以节省功耗;设备控制器会在检测到5ms空闲后(为识别挂起和准备恢复留出余量),触发挂起中断。
中断处理的核心逻辑在于判断当前是进入挂起还是退出挂起。这通过读取设备状态寄存器(DEVSTAT)的SUS位来实现。如果SUS=1,表示设备已进入挂起模式;反之,则表示设备正在从挂起中恢复。
关键操作与考量:
- 低功耗模式进入:一旦确认进入挂起,MPU应通知应用程序,并可能将自身切入低功耗状态(如睡眠模式)。此时,若控制器配置允许(
SYSCON1.SOFF_DIS=0),48MHz的主时钟会被自动关闭,这是功耗下降的大头。 - 远程唤醒:设备并非只能被动等待主机唤醒。如果设备支持远程唤醒(通过设置
DEVSTAT.R_WK_OK = 1),它可以在需要时主动唤醒总线。操作流程是:MPU先恢复时钟(如果已停止),然后设置系统控制寄存器2的远程唤醒位(SYSCON2.RMT_WKP),控制器便会驱动总线产生一个“恢复”信号(K状态)。 - 中断清理:处理完状态判断和应用层通知后,必须通过向中断源寄存器(
IRQ_SRC)的DS_CHG位写1来清除此次中断,否则中断会持续触发。
注意:一个常见的陷阱是,USB复位(Reset)也会使设备退出挂起状态。但此时硬件会依次产生三个中断:恢复中断、复位中断、复位结束中断。因此,你的挂起/恢复中断处理程序不需要特别处理复位退出的情况,只需专注于
SUS状态判断即可,复位流程由专门的复位中断服务程序处理。试图在一个中断里处理所有情况,反而会让逻辑变得复杂且容易出错。
2.2 非同步端点中断:数据搬运的指挥官
非同步(Non-ISO)端点,主要指控制(Control)、批量(Bulk)和中断(Interrupt)传输端点(除了端点0的控制传输部分)。它们的数据传输成功与否,依赖于精确的握手包(ACK, NAK, STALL)。对应的中断服务程序(ISR)是USB数据传输的“前线指挥官”。
中断路由与解析:这类中断通常映射到MPU的一个特定中断线(如IRQ_30)。ISR的第一步是“破译”中断源:
- 读取
IRQ_SRC寄存器:检查是接收中断(EPn_RX)被置位,还是发送中断(EPn_TX)被置位,或者两者同时发生。硬件保证同一时间,每个方向(IN或OUT)只有一个端点能产生中断。 - 定位具体端点:通过读取端点状态寄存器(
EPn_STAT),获取产生中断的端点号(ENDP_NB)。 - 配置端点号寄存器:这是非常关键且容易出错的一步。在处理该端点的数据之前,必须正确设置
EP_NUM寄存器,告诉控制器接下来要操作哪个端点、哪个方向。例如,要处理端点2的OUT(接收)中断,需要设置:EP_NUM.EP_NUM = 2,EP_NUM.EP_DIR = 0(OUT方向),EP_NUM.EP_SEL = 1。
OUT端点接收中断处理:以非控制OUT端点为例,中断产生通常意味着一次OUT事务完成。ISR需要检查状态标志寄存器(STAT_FLG):
ACK收到:表示主机发送的数据包已被设备成功接收。此时,ISR需要从RX FIFO中读取数据(流程见下文),然后通过设置CTRL.SET_FIFO_EN=1来“解锁”FIFO,准备接收下一个数据包。STALL:表示端点处于停滞状态。需要判断停滞是由主机发起还是本地(LH)发起。如果是本地发起的停滞且条件已解除,可以通过设置CTRL.CIR_HALT来清除停滞条件。NAK:如果使能了NAK中断,则表示设备暂时无法接收数据(比如缓冲区满)。通常,在非DMA模式下,NAK中断可能被禁用,由硬件自动回复NAK。
IN端点发送中断处理:当设备有数据要发送给主机时,处理流程类似但方向相反。
ACK收到:表示主机已成功接收上一个数据包。ISR可以将已发送的数据从应用层的发送缓冲区中“退休”(移除),如果还有后续数据,则继续写入TX FIFO,并设置CTRL.SET_FIFO_EN=1启动发送。STALL:处理方式同OUT端点。- 数据搬运:将数据从应用缓冲区写入TX FIFO时,需注意端点配置的包大小。如果应用数据长度小于包大小,需要发送一个短包(Short Packet),这通常是通过写入实际数据长度并设置相应标志来实现的。短包是通知主机传输结束的重要信号,尤其在批量传输中。
2.3 SOF中断与同步传输:实时性的保障
SOF(Start Of Frame)中断每1毫秒发生一次,为同步(Isochronous)传输提供了精确的时间基准。同步传输常用于音频、视频等对延迟敏感、允许一定错误率的场景。
SOF ISR的核心任务就是为所有已配置的同步端点服务一轮。流程是循环式的:
- 遍历所有已配置的同步OUT端点,读取其RX FIFO中的数据。
- 遍历所有已配置的同步IN端点,向其TX FIFO中写入下一帧要发送的数据。
- 处理同步传输特有的错误状态,如
DATA_FLUSH(数据被刷新,通常因错误发生)或ISO_ERROR(同步错误)。 - 清除SOF中断标志(
IRQ_SRC.SOF)。
实操心得:同步传输的时序要求非常严格。MPU必须在下一个SOF到来之前,完成对所有同步端点的读写操作。这意味着你的SOF ISR执行时间必须足够短。在设计时,要避免在SOF ISR中进行复杂的计算或内存拷贝。通常的做法是,在SOF ISR中只进行必要的数据搬运和状态检查,将数据处理(如音频编解码)放在后台任务中。此外,对于IN端点,如果上一帧的数据因故未能发送(
MISS_IN标志),需要决定是重发旧数据还是发送新数据,这需要根据应用场景制定策略。
3. DMA操作:解放CPU的数据高速公路
当数据量变大时,如果每个USB数据包都通过CPU来搬运,将会产生海量的中断,消耗大量CPU资源。DMA(直接内存访问)正是为了解决这个问题而生。USB设备控制器通常集成DMA引擎,能够自动管理数据在内部FIFO和系统内存之间的传输。
3.1 DMA通道架构与配置要点
以参考设计为例,控制器提供了6个独立的DMA通道:3个用于接收(OUT),3个用于发送(IN)。这是一个非常重要的资源划分。
配置黄金法则:
- 通道与端点绑定:通过
RXDMA_CFG.RXDMAn_EP或TXDMA_CFG.TXDMAn_EP寄存器,将一个物理DMA通道分配给一个特定的端点号。写入0则解绑。特别注意:控制端点0(EP0)通常不支持DMA操作,因为其传输量小且协议复杂,更适合用中断处理。 - 配置时机:DMA通道的配置(如分配端点、设置传输大小)必须在设备地址设置阶段(Address State)完成,也就是在
DS_CHG(设备状态改变)中断期间进行。在其他时间配置可能导致不可预知的行为。 - MPU访问禁忌:一旦启动了某个端点的DMA传输,MPU就绝对不能再通过
EP_NUM、CTRL、STAT_FLG等寄存器直接访问这个端点,直到DMA传输结束。这包括不能设置Halt(停滞)特性。所有的控制都应通过DMA相关的寄存器来完成。
3.2 接收(OUT)DMA传输详解
接收DMA用于将主机发送到设备的数据,自动搬运到系统内存。根据端点类型,行为有所不同。
非同步OUT端点DMA:
- 请求触发:当数据被硬件放入端点FIFO,且达到可触发DMA搬运的条件时,控制器会向系统DMA控制器发出请求。
- 两个关键中断:
- 传输结束中断(
RXn_EOT):标志着一次DMA传输会话的结束。触发条件有两个:1) 收到一个短包(包括长度为0的空包);2) 传输的事务数达到了预设值(RXDMAn.RXn_TC)且RXDMAn.RXn_STOP位被置位。处理完EOT中断后,必须重新设置CTRL.SET_FIFO_EN来使能该端点,才能准备下一次DMA传输。 - 事务计数中断(
RXn_CNT):用作“水印”(Watermark)通知。当传输的事务数达到RXDMAn.RXn_TC预设值时触发,即使传输尚未结束。这常用于监控文件传输进度,例如在接收一个未知大小的文件时,可以每接收N个数据包就通知一次应用层,用于更新进度条或进行初步处理。
- 传输结束中断(
- 双缓冲机制:很多控制器的DMA支持双缓冲(Double Buffering)。当DMA正在从“前台缓冲区”(Buffer A)向内存搬运数据时,USB核心可以同时向“后台缓冲区”(Buffer B)写入新收到的数据。两个缓冲区交替使用,极大地提高了吞吐率,避免了数据丢失。在配置和使用时,需要仔细阅读手册中关于缓冲区切换和指针管理的部分。
同步OUT端点DMA: 行为相对简单。只要主机在每一个USB帧(1ms)里发送了同步数据包,并且没有错误,DMA请求就会在每个帧开始时产生一次。同步DMA没有专用的传输结束或计数中断,传输的启停完全由主机的数据流决定。应用层需要通过其他方式(如数据量统计)来判断传输是否完成。
3.3 发送(IN)DMA传输详解
发送DMA用于将系统内存中的数据,自动搬运到USB控制器的TX FIFO,进而发送给主机。
非同步IN端点DMA: 这是最复杂的一种情况,因为它需要处理任意大小的数据块。核心参数是传输大小计数器(TXDMAn.TXN_TSC)和结束位(TXDMAn.TXN_EOT)。
EOT=1:TXN_TSC表示要传输的字节总数。EOT=0:TXN_TSC表示要传输的缓冲区个数,每个缓冲区的大小等于端点的物理缓冲区大小(如64字节)。
由于单个DMA传输会话有长度限制(例如受限于内部计数器位数),传输一个大文件需要拆分成多次DMA会话(多次“Pass”)。文档中给出了一个精妙的算法和示例:
大文件传输拆分算法(以端点缓冲区大小=32字节为例):
- 设文件总字节数
FTZ = 100603。 - 计算
EOTB = FTZ & (32-1) = 27。这是最后一段零头字节数。 - 计算
temp = FTZ >> 5(除以32) = 3143。 - 计算
XSWL = temp >> 10(除以1024) = 3。这表示有3个完整的“1024缓冲区”块。 - 计算
FBT = temp & 0x3FF= 71。这是最后一个“非完整1024块”中的缓冲区数量。
传输将分5个DMA会话完成:
- 会话1:
EOT=0, TSC=0(0代表1024),XSWL=3-> 传输 1024 * 32 = 32768 字节。 - 会话2:
EOT=0, TSC=0,XSWL=2-> 传输 32768 字节。 - 会话3:
EOT=0, TSC=0,XSWL=1-> 传输 32768 字节。 - 会话4:
EOT=0, TSC=71,XSWL=0-> 传输 71 * 32 = 2272 字节。 - 会话5:
EOT=1, TSC=27,XSWL=0-> 传输 27 字节。
每个DMA会话结束后,都会产生一个TXn_DONE中断。在中断处理程序中,需要根据XSWL和FBT的值,决定并启动下一个会话的传输参数,直到整个文件发送完毕。
同步IN端点DMA: 行为与同步OUT DMA类似,但方向相反。在每个SOF到来时,如果DMA通道已配置且就绪,控制器会发出DMA请求,要求MPU的DMA控制器将下一帧的数据写入。TXN_TSC在这里直接表示要写入的字节数,且不能超过端点缓冲区大小。同样,没有专用的传输完成中断。
3.4 DMA通道的禁用与注意事项
不当的DMA通道禁用操作会导致数据丢失或硬件挂起。推荐的做法是等待传输完成中断(RXn_EOT或TXn_DONE)后,再将配置寄存器中的端点号写0来禁用通道。
如果必须在传输过程中强制禁用,需要理解其行为:
- 接收时禁用:如果禁用时DMA请求正活跃,则禁用操作会延迟到当前请求的数据全部被DMA读取完毕后生效。如果使用了双缓冲,则需要两个缓冲区的数据都被读走。
- 发送时禁用:如果禁用时DMA请求正活跃,则禁用操作会延迟到当前请求的数据被填入FIFO并成功发送(产生IN事务)后生效。即使此时
TSC已减到0,也不会产生TX_DONE中断。
在强制禁用后,软件必须轮询TX/RXDMA_CFG.TX/RXDMAn_EP寄存器,直到其值变为0,确认通道已真正释放,才能对该端点进行其他操作。
4. 实战配置流程与避坑指南
理解了原理,我们来看如何将这些知识落地到代码中。以下是一个非同步批量OUT端点启用DMA接收的典型配置流程。
4.1 非同步OUT端点DMA接收配置流程
- 端点初始化:在设备枚举阶段,配置该端点的类型(Bulk OUT)、地址、最大包大小等。确保端点的双缓冲(如果使用)等特性已按需配置。
- 等待地址设置中断:在设备收到主机分配的地址,触发
DS_CHG中断后,开始DMA配置。 - 配置系统DMA控制器:这不是USB控制器的工作,但至关重要。你需要配置系统(MPU侧)的DMA控制器,设置源地址为USB控制器的
DATA_DMA寄存器地址,目标地址为你的应用接收缓冲区,传输宽度,并启用该DMA通道。 - 绑定USB DMA通道:将USB控制器的某个RX DMA通道(例如通道0)分配给目标端点。
RXDMA_CFG.RXDMA0_EP = endpoint_number。 - 设置DMA传输参数:
- 如果希望传输在收到短包时自动结束,只需使能
RXn_EOT中断即可。 - 如果希望限制最大传输量(例如文件传输),则设置
RXDMAn.RXn_TC = N-1(N为期望的事务数),并置位RXDMAn.RXn_STOP = 1。同时使能RXn_EOT和RXn_CNT中断。
- 如果希望传输在收到短包时自动结束,只需使能
- 使能中断:在USB控制器的DMA中断使能寄存器(
DMA_IRQ_EN)中,使能对应通道的RXn_EOT_IE和/或RXn_CNT_IE。 - 启动传输:这是关键一步!先通过
EP_NUM寄存器选中目标端点(设置EP_NUM.EP_NUM,EP_NUM.EP_DIR=0,EP_NUM.EP_SEL=1),然后设置CTRL.SET_FIFO_EN = 1。这个操作会“激活”该端点的FIFO,使其准备好接收数据并触发DMA。
4.2 常见问题与排查技巧
DMA传输不启动:
- 检查端点配置:确认端点已正确配置为Bulk/Interrupt OUT,且已使能。
- 检查
SET_FIFO_EN:这是最容易被遗忘的一步。必须在绑定DMA通道后,手动设置一次CTRL.SET_FIFO_EN=1来启动DMA就绪状态。 - 检查系统DMA:确认系统DMA控制器的配置正确,特别是外设请求信号是否与USB控制器的DMA请求线(如
DMA_REQ_26)正确映射。
数据错位或丢失:
- 双缓冲切换问题:在DMA进行中,应用层不能直接访问USB控制器端点的FIFO。数据必须通过DMA搬运到指定内存区域。确保你的应用是从DMA目标缓冲区读取数据,而不是试图从USB寄存器读取。
- 数据对齐:注意USB控制器和系统内存的数据宽度(8位/16位/32位)。在配置系统DMA时,传输宽度必须匹配。如果USB是16位访问,而你的缓冲区是字节数组,可能需要处理字节序和访问对齐问题。
EOT中断不产生:- 主机未发送短包:对于批量传输,主机通过发送一个长度小于最大包大小的包(短包)来表示传输结束。确保主机端协议正确。
RXn_STOP机制:如果你依赖于RXn_STOP来触发EOT,请检查RXDMAn.RXn_TC值是否设置正确,以及事务计数是否已达到。可以通过调试器读取相关状态寄存器来验证。
性能瓶颈:
- DMA缓冲区大小:对于高速批量传输,增大端点的MPS(最大包大小)和合理使用双缓冲能显著提升吞吐量。
- 系统DMA优先级:确保USB DMA请求在系统DMA控制器中有足够高的优先级,避免因其他DMA传输导致数据溢出。
- 中断处理延迟:
EOT或DONE中断处理函数应尽可能快,仅进行必要的状态清除和通知,繁重的数据处理(如校验、存储)应交给后台任务。长时间的ISR会阻塞下一次传输的启动。