TI USBSS批量传输DMA配置实战:从寄存器到描述符的完整指南
1. 项目概述与核心价值
在嵌入式系统开发中,USB通信的效率和稳定性往往是决定产品性能的关键。无论是连接高速存储设备、工业相机,还是作为调试接口,USB批量传输(Bulk Transfer)都是处理大块、非实时数据的首选方式。然而,仅仅实现通信是远远不够的,如何让数据传输不成为CPU的沉重负担,如何确保在高速率下数据不丢失、不卡顿,才是真正考验工程师功力的地方。直接内存访问(DMA)技术正是解决这一痛点的利器,它能让数据在USB控制器和系统内存之间“自动”搬运,将CPU解放出来处理更重要的任务。
德州仪器(TI)的许多高性能处理器,如Sitara AM系列、部分DSP等,都集成了功能强大的USB子系统(USBSS),并配套了其专有的CPPI DMA控制器。这套组合拳为高效USB通信提供了硬件基础,但与之对应的,是其相对复杂的配置流程和众多的寄存器选项。手册上的描述往往分散且偏理论,新手工程师很容易在配置DMA时踩坑,导致传输异常、数据错位甚至系统死锁。
我曾在多个涉及高速数据采集和文件传输的项目中深度使用TI USBSS的批量传输与DMA功能,从最初的磕磕绊绊到后来的游刃有余,积累了不少实战经验。本文将抛开晦涩的理论堆砌,直接切入核心,以TI USBSS为例,手把手带你从原理到寄存器配置,再到DMA描述符的构建,完整走通USB批量传输的DMA实现之路。你会发现,只要理清了数据流和控制流,那些看似复杂的寄存器位和数据结构,其实都有其清晰的逻辑。
2. USB批量传输与DMA核心原理拆解
2.1 为什么是批量传输?
USB协议定义了四种传输类型:控制(Control)、中断(Interrupt)、同步(Isochronous)和批量(Bulk)。它们各有分工:
- 控制传输:用于枚举、配置设备,保证可靠但速率不高。
- 中断传输:用于小数据量、周期性的查询,如USB键盘。
- 同步传输:用于实时性要求高的流媒体数据,如USB摄像头,保证带宽但不保证数据100%正确。
- 批量传输:这正是我们关注的重点。它专为非周期性、大块数据设计,例如U盘读写、文件打印、固件升级。它的特点是利用空闲带宽进行传输。当总线忙时,它可以等待;一旦有空闲,它就尽可能多地占用带宽来传输数据。因此,它不保证延迟,但保证数据的正确性(通过错误重传机制),并且能实现很高的吞吐量。
在主机(Host)模式下,批量传输又分为批量IN(设备到主机)和批量OUT(主机到设备)。IN和OUT的方向是站在主机的角度定义的。
2.2 DMA:性能提升的关键
如果没有DMA,CPU需要亲自处理每一个数据包:当FIFO(先入先出缓冲区)准备好数据(IN)或空出空间(OUT)时,产生中断,CPU响应中断,然后执行内存拷贝指令将数据从FIFO读到内存,或从内存写到FIFO。对于高速批量传输(如USB2.0 High-Speed的峰值约480 Mbps),这种频繁的中断和拷贝会消耗大量CPU周期,导致系统响应迟缓甚至丢包。
DMA的引入彻底改变了这一局面。它的核心思想是“用空间换时间,用硬件换CPU”。
- 空间:我们需要在内存中预先准备好一块区域(描述符+数据缓冲区)来告诉DMA控制器数据从哪里来、到哪里去。
- 硬件:DMA控制器是一个独立的硬件单元,它掌握系统总线的控制权,可以在不打扰CPU的情况下,在内存和外设(这里是USB控制器的FIFO)之间搬运数据。
对于TI USBSS,其DMA架构称为CPPI(Communications Port Programming Interface)。你可以把它想象成一个高效的“物流调度中心”。CPU作为“总经理”,只负责制定运输计划(初始化描述符链表、配置通道),而CPPI DMA作为“自动化物流车队”,负责执行具体的货物(数据包)装卸和运输。USB控制器的端点FIFO就是“仓库码头”。
2.3 TI USBSS CPPI DMA架构精要
理解CPPI DMA的架构,是正确配置它的前提。其核心组件和流程如下:
队列管理器(Queue Manager, QM):这是整个DMA系统的“任务调度中心”。它管理着多种队列,最重要的是发送队列(Tx Queue)和释放队列(Free Queue)。CPU把要发送的数据包描述符放入发送队列;DMA完成接收后,将用过的描述符放回释放队列,等待CPU回收再利用。QM通过硬件加速这些队列操作,效率远高于软件链表。
描述符(Descriptor):这是“物流单据”,描述了数据包的详细信息。CPPI主要使用两种:
- 包描述符(Packet Descriptor):描述一个完整的数据包。包含包长度、包类型(USB)、数据缓冲区指针、下一个描述符指针等信息。它是数据包链表的头节点。
- 缓冲区描述符(Buffer Descriptor):当单个数据包太大,需要分散在多个不连续的内存缓冲区时(即Scatter/Gather),第一个缓冲区描述符就是包描述符,后续的用缓冲区描述符链接起来。对于大多数USB应用,一个包对应一个缓冲区,所以通常只使用包描述符。
数据缓冲区(Data Buffer):这是真正的“货物存放地”,一块连续的物理内存,存放着要发送或接收的原始数据。
CPPI DMA控制器(CDMA)与传输DMA(XDMA):
- CDMA:负责在系统内存(描述符和数据缓冲区)和CPPI FIFO之间搬运数据。它受QM调度。
- XDMA:负责在CPPI FIFO和USB端点FIFO之间搬运数据。它直接响应USB控制器的硬件请求。 数据流可以概括为:内存 <-> (CDMA) <-> CPPI FIFO <-> (XDMA) <-> USB端点FIFO <-> USB总线。
端口(Port)映射:在USBSS中,每个非0端点(EP1-EP15)都映射到一个独立的DMA端口。例如,USB0的EP1对应Port 1,EP2对应Port 2,以此类推。这为每个端点提供了独立的DMA通道。
核心心得:很多初学者配置失败,是因为混淆了“配置USB端点寄存器”和“配置CPPI DMA通道”这两个层次。前者是告诉USB控制器“这个端点怎么工作(速度、类型、包大小)”;后者是告诉DMA控制器“这个端点对应的数据怎么搬运”。两者必须协同配置,缺一不可。
3. 批量传输端点配置详解与实操
在启用DMA之前,我们必须先正确配置USB端点,使其进入批量传输模式。以下配置均以主机(Host)模式为例。
3.1 批量IN端点配置(主机接收数据)
假设我们要从设备的端点1(EP1)批量接收数据。
步骤1:配置端点类型与地址首先,我们需要告诉USB控制器,我们要使用哪个物理端点(例如EP1)来映射到目标设备的哪个逻辑端点(例如EP1),以及传输类型和速度。
// 假设操作的是USB0控制器,EP1对应的寄存器组基址为 USB0_BASE + EP1_OFFSET // 1. 设置目标设备地址和端点号 (RXFUNCADDR) // 通常设备地址在枚举阶段获得,假设为0x02 HWREG(USB0_BASE + EP1_RXFUNCADDR) = 0x02; // 设备地址 // 2. 配置端点类型、速度 (HOST_RXTYPE) // 位[7:6] SPEED: 00 = Full Speed, 01 = Low Speed, 10 = High Speed // 位[5:4] PROT: 10 = Bulk Transfer // 位[3:0] RENDPN: 目标设备的端点号,此处为1 uint32_t rxtype_val = (0x2 << 6) | // High Speed (假设) (0x2 << 4) | // Bulk Transfer (0x1 << 0); // 目标设备端点号 EP1 HWREG(USB0_BASE + EP1_HOST_RXTYPE) = rxtype_val;步���2:设置数据包大小与NAK重试策略
// 3. 设置最大包大小 (RXMAXP) // 这个值必须与设备端点描述符中的wMaxPacketSize一致。对于高速批量端点,通常是512字节。 HWREG(USB0_BASE + EP1_RXMAXP) = 512; // 4. 设置NAK重试超时间隔 (HOST_RXINTERVAL) // 这个值决定了主机在收到设备NAK(未准备好)后,重试发送IN令牌包的时间间隔(单位为微帧,125us)。 // 设为0表示禁用NAK超时,主机会无限重试。通常可以设置一个合理值,如0x10(16个微帧,即2ms)。 HWREG(USB0_BASE + EP1_HOST_RXINTERVAL) = 0x10;步骤3:配置端点控制状态寄存器(HOST_RXCSR)这是最关键的一步,决定了端点的工作模式。
// 先读取当前值,然后修改特定位 uint32_t rxcsr = HWREG(USB0_BASE + EP1_HOST_RXCSR); // 清除AUTOCLEAR位(位8)。在DMA模式下,我们通常手动管理RXPKTRDY。 rxcsr &= ~(1 << 8); // 设置DMAEN位(位13),使能该端点的DMA请求。 rxcsr |= (1 << 13); // 清除DMAMODE位(位11)。注意:对于CPPI DMA,此位应保持为0。DMAMODE是针对另一种较老的DMA模式。 rxcsr &= ~(1 << 11); // 清除DSINYET位(位12),允许正常的PING流控(仅高速传输需要)。 rxcsr &= ~(1 << 12); // 清除AUTOREQ位(位5)。在DMA模式下,IN令牌请求由DMA或专用寄存器控制,而非此位。 rxcsr &= ~(1 << 5); // 初始化数据同步位(Data Toggle)。确保传输从DATA0开始。 // 方法一:直接设置CLRDATATOG位(位7)来清零Toggle位。 rxcsr |= (1 << 7); // 设置CLRDATATOG HWREG(USB0_BASE + EP1_HOST_RXCSR) = rxcsr; // 写入,硬件会自动清除CLRDATATOG位 // 方法二:通过DATATOGWREN和DATATOG位手动翻转(如果需要同步到特定状态)。 // 最后,确保FIFO是空的(初始状态或错误恢复时) rxcsr = HWREG(USB0_BASE + EP1_HOST_RXCSR); // 重新读取 if (rxcsr & 0x0001) { // 检查RXPKTRDY位(位0)是否置位 rxcsr |= (1 << 4); // 设置FLUSHFIFO位(位4) HWREG(USB0_BASE + EP1_HOST_RXCSR) = rxcsr; // 如果使能了双包缓冲,可能需要连续设置两次才能清空 // HWREG(USB0_BASE + EP1_HOST_RXCSR) = rxcsr; } // 将最终配置写回寄存器(除了临时设置的CLRDATATOG和FLUSHFIFO,它们硬件会自动清除) rxcsr = HWREG(USB0_BASE + EP1_HOST_RXCSR); rxcsr &= ~((1 << 7) | (1 << 4)); // 确保临时位已清除 rxcsr |= (1 << 13); // 再次确认DMAEN使能 HWREG(USB0_BASE + EP1_HOST_RXCSR) = rxcsr;步骤4:使能端点中断(可选)即使使用DMA,我们通常也需要使能错误中断,以便处理STALL、超时等情况。
// 使能EP1的接收端点中断 HWREG(USB0_BASE + INTRRXE) |= (1 << 1); // 假设位1对应EP1 // 同时确保核心级中断已使能 HWREG(USB0_BASE + USBINTR) |= (1 << 0); // 使能USB核心中断3.2 批量OUT端点配置(主机发送数据)
配置OUT端点(例如使用EP2发送数据到设备EP2)与IN端点多处相似,但关注的控制寄存器是HOST_TXCSR。
步骤1:配置端点类型与地址
// 设置目标设备地址和端点号 HWREG(USB0_BASE + EP2_TXFUNCADDR) = 0x02; // 设备地址 // 配置HOST_TXTYPE uint32_t txtype_val = (0x2 << 6) | // High Speed (0x2 << 4) | // Bulk Transfer (0x2 << 0); // 目标设备端点号 EP2 HWREG(USB0_BASE + EP2_HOST_TXTYPE) = txtype_val;步骤2:设置数据包大小与NAK重试策略
HWREG(USB0_BASE + EP2_TXMAXP) = 512; // 最大包大小 HWREG(USB0_BASE + EP2_HOST_TXINTERVAL) = 0x10; // NAK超时间隔步骤3:配置端点控制状态寄存器(HOST_TXCSR)
uint32_t txcsr = HWREG(USB0_BASE + EP2_HOST_TXCSR); // 设置MODE位(位13),确保FIFO使能(特别是当FIFO与RX端点共享时) txcsr |= (1 << 13); // 清除FRCDATATOG位(位11),允许正常的数据同步操作。 txcsr &= ~(1 << 11); // **关键区别:AUTOSET位(位15)** // 在CPU模式下,如果设置此位,当向FIFO写入最大包大小的数据时,硬件会自动置位TXPKTRDY。 // 在DMA模式下,我们必须清除此位,因为TXPKTRDY由DMA控制器或软件根据描述符状态来管理。 txcsr &= ~(1 << 15); // 设置DMAEN位(位12),使能DMA请求。 txcsr |= (1 << 12); // **设置DMAMODE位(位10)为1。这是OUT端点DMA模式的关键配置!** txcsr |= (1 << 10); // 初始化数据同步位 txcsr |= (1 << 6); // 设置CLRDATATOG位(位6) HWREG(USB0_BASE + EP2_HOST_TXCSR) = txcsr; // 清空FIFO(如果非空) txcsr = HWREG(USB0_BASE + EP2_HOST_TXCSR); if (txcsr & 0x0002) { // 检查FIFONOTEMPTY位(位1) txcsr |= (1 << 3); // 设置FLUSHFIFO位(位3) HWREG(USB0_BASE + EP2_HOST_TXCSR) = txcsr; } // 写回最终配置 txcsr = HWREG(USB0_BASE + EP2_HOST_TXCSR); txcsr &= ~((1 << 6) | (1 << 3)); txcsr |= (1 << 13) | (1 << 12) | (1 << 10); // 确认MODE, DMAEN, DMAMODE HWREG(USB0_BASE + EP2_HOST_TXCSR) = txcsr;步骤4:使能端点中断
HWREG(USB0_BASE + INTRTXE) |= (1 << 2); // 使能EP2的发送端点中断 HWREG(USB0_BASE + USBINTR) |= (1 << 0);实操要点与避坑指南:
- 顺序很重要:务必先配置
TXMAXP/RXMAXP、TYPE、INTERVAL等基本参数,最后再配置CSR寄存器。错误的顺序可能导致控制器进入不可预测的状态。- 双包缓冲(DPB):在
TXFIFOSZ/RXFIFOSZ寄存器中设置DPB位可以启用双包缓冲。这允许在处理一个数据包时,FIFO可以同时接收或发送另一个包,对于维持高吞吐量、避免FIFO溢出/下溢非常有用。但请注意,在清空FIFO(FLUSHFIFO)时,如果使能了双缓冲,可能需要连续操作两次该位才能确保完全清空。- 数据同步(Data Toggle):USB使用DATA0/DATA1交替来保证数据包顺序。硬件通常会自动管理,但在端点初始化或错误恢复后,必须通过
CLRDATATOG将其重置为DATA0起点,否则会导致设备因同步错误而返回STALL。- AUTOREQ vs. DMA:对于IN端点,在CPU模式下,可以设置
AUTOREQ让硬件在读完一个包后自动请求下一个,实现“连续传输”。但在DMA模式下,必须清除AUTOREQ,因为IN令牌的请求将由CPPI DMA的专用配置(如USBn_AUTOREQ寄存器)或描述符链的机制来控制。混淆这一点是DMA不启动的常见原因。
4. CPPI DMA描述符编程与通道配置
配置好USB端点后,接下来就是搭建DMA的“物流系统”。核心是创建描述符并提交给队列管理器。
4.1 描述符数据结构定义与初始化
根据TI手册,一个最基本的用于USB的包描述符(32字节格式)定义如下:
typedef struct _cppi_packet_desc { uint32_t pd0; // Word 0: 描述符类型、协议区字数、包长度 uint32_t pd1; // Word 1: 源/目的标签 (USB中通常忽略) uint32_t pd2; // Word 2: 包错误、包类型、返回队列等 uint32_t pd3; // Word 3: 缓冲区0长度 uint32_t pd4; // Word 4: 缓冲区0指针 uint32_t pd5; // Word 5: 下一个描述符指针 (NULL表示结束) uint32_t pd6; // Word 6: 原始缓冲区0长度 (Rx时由DMA回写) uint32_t pd7; // Word 7: 原始缓冲区0指针 (Rx时由DMA回写) } cppi_packet_desc_t;我们需要一个函数来初始化这个描述符,用于发送或接收。
// 初始化一个发送包描述符 void init_tx_packet_desc(cppi_packet_desc_t *desc, void *data_buffer, uint32_t buffer_len, uint32_t packet_len, uint16_t return_qnum) { // 确保描述符地址32字节对齐(硬件要求) ASSERT(((uint32_t)desc & 0x1F) == 0); // PD0: 描述符类型(0x10) | 协议区字数(0) | 包长度 desc->pd0 = (0x10 << 27) | (0 << 22) | (packet_len & 0x3FFFFF); // PD1: 源/目的标签,USB应用通常设为0 desc->pd1 = 0; // PD2: 包错误(0) | 包类型(USB=5) | 保留 | 零长度包指示(0) | 协议特定(0) | 返回策略(0) | 片上标志(根据内存位置) | 返回队列 uint32_t on_chip = is_buffer_on_chip_sram(data_buffer) ? 1 : 0; // 需实现此判断函数 desc->pd2 = (5 << 26) | (0 << 16) | (on_chip << 14) | (return_qnum & 0x0FFF); // PD3: 缓冲区0长度 (对于发送,就是我们要发送的数据长度) desc->pd3 = buffer_len & 0x3FFFFF; // PD4: 缓冲区0指针 (数据缓冲区的物理地址) desc->pd4 = (uint32_t)data_buffer; // PD5: 下一个描述符指针。单描述符包,设为NULL。 desc->pd5 = 0; // PD6: 原始缓冲区长度 (发送时与PD3相同) desc->pd6 = buffer_len & 0x3FFFFF; // PD7: 原始缓冲区指针 (发送时与PD4相同) desc->pd7 = (uint32_t)data_buffer; // 重要:确保所有字段在提交给DMA前都已写入内存,可能需要数据同步屏障指令 __DSB(); } // 初始化一个接收包描述符 (准备接收数据) void init_rx_packet_desc(cppi_packet_desc_t *desc, void *data_buffer, uint32_t buffer_len, uint16_t return_qnum) { ASSERT(((uint32_t)desc & 0x1F) == 0); // PD0: 包长度在接收时由DMA填写,我们先设为0。协议区字数也为0。 desc->pd0 = (0x10 << 27) | (0 << 22); // 包长度初始为0 desc->pd1 = 0; uint32_t on_chip = is_buffer_on_chip_sram(data_buffer) ? 1 : 0; desc->pd2 = (5 << 26) | (0 << 16) | (on_chip << 14) | (return_qnum & 0x0FFF); // PD3: 缓冲区长度。告诉DMA这个缓冲区有多大。 desc->pd3 = buffer_len & 0x3FFFFF; desc->pd4 = (uint32_t)data_buffer; desc->pd5 = 0; desc->pd6 = buffer_len & 0x3FFFFF; // 原始长度 desc->pd7 = (uint32_t)data_buffer; // 原始指针 __DSB(); }4.2 队列管理器(QM)初始化与描述符提交
步骤1:初始化QM和创建队列系统上电后,需要初始化队列管理器,并创建我们需要的队列。通常我们需要:
- 一个发送队列(Tx Queue):用于存放待发送数据的包描述符。
- 一个释放队列(Free Queue):用于接收DMA完成传输后返还的描述符,方便循环利用。
// 假设使用QM的队列0作为EP2的发送队列,队列1作为EP2的释放队列 #define QM_BASE 0x... // QM模块基址 #define EP2_TX_QUEUE_NUM 0 #define EP2_FREE_QUEUE_NUM 1 // 1. 配置队列的存储区域(描述符区域) // 这通常涉及设置队列的基地址、大小等,具体依赖TI的底层驱动库或直接操作QM寄存器。 // 例如,使用TI的PDK或sysbios中的QM驱动API: Qm_init(); Qm_queue_create(EP2_TX_QUEUE_NUM, QM_QUEUE_TYPE_STATIC, ...); Qm_queue_create(EP2_FREE_QUEUE_NUM, QM_QUEUE_TYPE_STATIC, ...); // 2. 将初始化好的接收描述符放入释放队列。 // 对于接收,我们需要预先将一些空的描述符(关联了数据缓冲区)放入释放队列, // 这样当USB设备有数据送来时,DMA才能自动获取描述符并将数据填入缓冲区。 cppi_packet_desc_t rx_desc; uint8_t rx_buffer[2048]; init_rx_packet_desc(&rx_desc, rx_buffer, 2048, EP2_FREE_QUEUE_NUM); // 将描述符推入释放队列 (push) Qm_queue_push(EP2_FREE_QUEUE_NUM, (uint32_t)&rx_desc);步骤2:配置CPPI DMA通道每个USB端点对应一个DMA端口。我们需要配置该端口的DMA通道,将其与刚才创建的队列关联起来。
// 假设配置USB0 EP2 (OUT端点) 的DMA发送通道 // EP2 对应 CPPI 端口号可能是 (USB0_PORT_OFFSET + 2),具体需查手册映射 uint32_t cppi_tx_port = USB0_CPPI_TX_PORT_BASE + 2; // 1. 配置发送通道的队列指针 // 告诉DMA控制器,从这个端口的发送队列获取描述符。 HWREG(cppi_tx_port + CPPI_TX_QUEUE_PTR) = Qm_get_queue_mem_addr(EP2_TX_QUEUE_NUM); // 2. 配置释放队列指针 (可选,但推荐) // 告诉DMA控制器,发送完成后,将描述符推送到哪个队列。通常就是我们准备好的释放队列。 HWREG(cppi_tx_port + CPPI_TX_RETURN_QUEUE_PTR) = Qm_get_queue_mem_addr(EP2_FREE_QUEUE_NUM); // 3. 使能DMA通道 HWREG(cppi_tx_port + CPPI_TX_CONTROL) |= CPPI_TX_CH_ENABLE; // 对于接收通道 (例如USB0 EP1 IN端点),配置类似,但指向的是释放队列和接收完成队列。 uint32_t cppi_rx_port = USB0_CPPI_RX_PORT_BASE + 1; // 配置接收通道从哪个释放队列获取空描述符 HWREG(cppi_rx_port + CPPI_RX_FREE_QUEUE_PTR) = Qm_get_queue_mem_addr(EP1_FREE_QUEUE_NUM); // 配置接收完成后,将满载的描述符推送到哪个队列(例如一个专用的接收完成队列) HWREG(cppi_rx_port + CPPI_RX_COMPLETION_QUEUE_PTR) = Qm_get_queue_mem_addr(EP1_RX_DONE_QUEUE_NUM); HWREG(cppi_rx_port + CPPI_RX_CONTROL) |= CPPI_RX_CH_ENABLE;步骤3:启动传输对于发送(OUT):
// 1. 准备要发送的数据 uint8_t tx_data[1024]; fill_data(tx_data, 1024); // 2. 初始化一个发送描述符 cppi_packet_desc_t tx_desc; init_tx_packet_desc(&tx_desc, tx_data, 1024, 1024, EP2_FREE_QUEUE_NUM); // 3. 将描述符推送到发送队列 Qm_queue_push(EP2_TX_QUEUE_NUM, (uint32_t)&tx_desc); // 4. DMA控制器会自动从发送队列取出描述符,开始数据传输。 // 传输完成后,描述符会被自动推送到我们指定的EP2_FREE_QUEUE_NUM队列。对于接收(IN):
// 1. 我们已经预先将空的接收描述符放入了EP1_FREE_QUEUE_NUM(见队列初始化步骤)。 // 2. 需要配置USB控制器的自动请求寄存器,使其在DMA模式下能自动发送IN令牌。 // 对于IN端点,使能自动请求(AUTOREQ)的替代机制: // 设置USB0_AUTOREQ寄存器中对应端点的位域 (例如RX1_AUTOREQ)。 // 二进制01: 收到数据包后自动请求下一个包(适合DMA连续接收)。 HWREG(USB0_BASE + USB_AUTOREQ) |= (0x1 << (1*2)); // 假设位[3:2]控制EP1 // 3. 当设备有数据发送时,DMA会自动从释放队列获取描述符,将数据填入缓冲区, // 然后将描述符推送到接收完成队列(EP1_RX_DONE_QUEUE_NUM)。 // 4. 我们的软件需要定期或中断驱动地从EP1_RX_DONE_QUEUE_NUM队列中弹出(pop)描述符, // 处理数据,然后重新初始化该描述符,再放回释放队列,形成循环。深度解析与避坑指南:
- 内存对齐与一致性:描述符的地址必须32字节对齐(即低5位为0),否则DMA会访问错误或产生不可预知的行为。数据缓冲区也建议进行字节对齐(如32位对齐),以获得最佳性能。此外,在CPU更新描述符内容后、DMA读取前,必须使用数据同步屏障(如
__DSB())或缓存维护操作(如果使用Cache),确保DMA看到的是内存中最新的数据。- 描述符链 vs 单描述符:单个描述符只能描述一个连续的内存缓冲区。如果要发送或接收的数据分散在多个不连续的物理内存块中,就需要使用描述符链(通过PD5的“下一个描述符指针”链接)。这对于零拷贝网络协议栈很有用,但在简单的USB批量传输中,单描述符通常足够。
- 返回队列(Return Queue):描述符PD2中的返回队列号至关重要。它决定了这个描述符在传输完成后被DMA放回哪个队列。发送和接收应使用不同的释放队列,避免混淆。一种好的实践是:为每个��点的发送和接收分别创建独立的释放队列。
- “片上(On-chip)”标志:如果描述符和数据缓冲区位于芯片内部SRAM(访问速度快),将PD2中的
on_chip位置1,可以帮助DMA调度器优化访问。如果位于外部DDR,则置0。- 零长度包(ZLP)处理:对于批量传输,当数据长度恰好是最大包大小的整数倍时,需要在传输结束时发送一个零长度包(Zero Length Packet)来通知对方传输结束。在描述符中,可以通过设置PD2的“零长度包指示”位来实现。很多设备驱动忽略这一点,导致大文件传输到最后阶段挂起。
5. 完整工作流程与系统集成
将端点配置和DMA配置串联起来,一个完整的批量传输DMA工作流程如下:
批量OUT(发送)流程:
- 系统初始化:初始化USB控制器、QM、CPPI DMA模块。
- 端点配置:如3.2节所述,配置
HOST_TXTYPE,TXMAXP,HOST_TXINTERVAL,HOST_TXCSR(使能DMAEN和DMAMODE)。 - DMA通道配置:创建发送队列和释放队列,配置CPPI发送通道的队列指针。
- 准备数据:申请数据缓冲区,填充待发送数据。
- 提交任务:初始化一个发送包描述符,将其推送到发送队列。
- 自动传输:CPPI DMA从发送队列取出描述符,通过XDMA将数据从缓冲区搬移到USB端点FIFO,USB控制器自动处理令牌、数据发送、握手包。发送完成后,DMA将描述符推送到指定的释放队列。
- 回收与循环:应用程序从释放队列取出描述符,可重复使用该描述符和缓冲区准备下一次发送。
批量IN(接收)流程:
- 系统初始化:同上。
- 端点配置:如3.1节所述,配置
HOST_RXTYPE,RXMAXP,HOST_RXINTERVAL,HOST_RXCSR(使能DMAEN,清除DMAMODE和AUTOREQ)。关键:配置USBn_AUTOREQ寄存器使能自动IN请求。 - DMA通道配置:创建释放队列和接收完成队列。配置CPPI接收通道的释放队列指针和完成队列指针。
- 预投递缓冲:初始化多个空接收描述符(关联数据缓冲区),将它们全部推送到释放队列。
- 启动接收:使能USB端点。此后,USB控制器会自动发送IN令牌。当设备返回数据,DMA会自动从释放队列取一个空描述符,将数据填入缓冲区,然后将描述符推送到完成队列。
- 处理数据:应用程序轮询或通过中断(可配置QM在队列非空时产生中断)从完成队列取出描述符。从描述符的PD0字段读取实际接收到的数据长度,处理缓冲区数据。
- 缓冲回收:处理完数据后,重新初始化该描述符(清空PD0中的包长度等),将其再次推回释放队列,等待下一次接收。
6. 调试技巧与常见问题排查
即使按照手册配置,DMA不工作也是常态。以下是我在调试中总结的“三板斧”和常见问题:
调试三板斧:
- 寄存器状态检查:在关键步骤后(如配置完端点、启动DMA前),读取并打印所有相关寄存器的值(
HOST_TXCSR/RXCSR,TXMAXP/RXMAXP,CPPI通道控制寄存器,QM队列状态寄存器),与手册预期值逐位对比。善用调试器的内存查看和寄存器查看窗口。 - 描述符内存查看:在提交描述符到队列前后,用调试器查看描述符所在内存区域的内容,确保每个字段(特别是指针、长度、下一个描述符指针)都正确写入,并且符合对齐要求。
- 队列状态监控:监控发送队列和释放队列的深度。提交描述符后,发送队列深度应减少,表示DMA取走了描述符。传输完成后,释放队列深度应增加。如果队列状态无变化,说明DMA根本没动,问题可能出在通道使能、队列指针配置或USB端点DMA使能上。
常见问题与解决方案:
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| DMA完全不启动,队列无变化 | 1. USB端点DMA未使能。 2. CPPI DMA通道未使能。 3. 队列指针配置错误。 4. 描述符格式错误或未对齐。 | 1. 确认HOST_TXCSR[DMAEN]和HOST_RXCSR[DMAEN]已置1。2. 确认 CPPI_TX/RX_CONTROL通道使能位已置1。3. 核对 CPPI_TX_QUEUE_PTR等寄存器值是否指向正确的QM队列内存地址。4. 检查描述符地址是否32字节对齐,并用调试器查看其内容。 |
| OUT端点能启动但数据发不出去/IN端点收不到数据 | 1. 最大包大小TXMAXP/RXMAXP设置错误,与设备不匹配。2. 设备地址或端点号 TXFUNCADDR/RXFUNCADDR,TENDPN/RENDPN设置错误。3. 数据同步位未初始化,设备回复STALL。 4. (OUT)AUTOSET位在DMA模式下未清零,导致TXPKTRDY逻辑混乱。 5. (IN)未配置 USBn_AUTOREQ寄存器,主机不发送IN令牌。 | 1. 检查设备描述符中的wMaxPacketSize,确保与寄存器设置一致。2. 确认枚举获得的设备地址和端点号正确写入。 3. 在端点初始化时,务必执行一次 CLRDATATOG操作。4. 对于OUT端点,确保 HOST_TXCSR[AUTOSET]=0。5. 对于IN端点,检查 USBn_AUTOREQ寄存器中对应端点位域是否配置为01或11。 |
| 数据传输不完整或卡住 | 1. 描述符中缓冲区长度PD3小于实际数据包长度。2. 未处理零长度包(ZLP)。 3. 释放队列为空,DMA无可用描述符(对于IN接收)。 4. NAK超时或错误中断未处理,端点进入错误状态。 | 1. 确保PD3(缓冲区长度)大于等于PD0中的包长度,且大于等于TXMAXP/RXMAXP。2. 在数据长度是最大包大小整数倍时,主动发送一个ZLP(设置描述符的ZLP标志位)。 3. IN接收前,确保有足够多的空描述符在释放队列中。 4. 使能端点错误中断,在中断服务程序中检查 ERROR,RXSTALL,NAK_TIMEOUT等位,并进行错误恢复(如清FIFO、重置Toggle)。 |
| 数据错乱或覆盖 | 1. 描述符的“下一个描述符指针”(PD5)未正确终止(非NULL),DMA错误地链接了其他内存。 2. 多个DMA通道共用同一个数据缓冲区,产生竞争。 3. Cache一致性未处理,CPU和DMA看到的数据不同。 | 1. 对于单描述符包,务必设置PD5 = 0。2. 确保一个缓冲区在同一时刻只被一个DMA操作使用。 3. 在CPU写入数据后、启动DMA前,调用 CacheWriteBack;在DMA完成后、CPU读取数据前,调用CacheInvalidate。或者使用非缓存(Non-cacheable)的内存区域。 |
| 系统不稳定或死机 | 1. 描述符或缓冲区地址非法(如指向未映射的内存区域)。 2. 队列操作(push/pop)未加锁,在多任务/中断环境下产生竞态。 3. DMA传输过程中修改了正在被使用的描述符内容。 | 1. 确保所有描述符和缓冲区都位于有效的、DMA可访问的物理内存区间。 2. 对队列的push/pop操作使用关中断或信号量进行保护。 3. 严格遵循“描述符提交后即视为DMA所有”的原则,只有在DMA完成(描述符回到释放队列)后才能重新初始化它。 |
一个实用的调试起点:如果DMA完全没反应,可以暂时回退到CPU轮询模式。即不使能DMAEN,用CPU查询RXPKTRDY或TXPKTRDY位,并手动读写FIFO。如果CPU模式能通,说明USB端点基础配置和通信链路是好的,问题就锁定在DMA相关的配置(描述符、队列、通道)上。这是一种非常有效的分步隔离问题法。
最后,耐心和细致的日志是关键。在关键路径上添加日志,记录描述符地址、队列状态、寄存器值,能极大缩短调试时间。TI的USBSS和CPPI DMA功能强大,一旦调通,其带来的性能提升和CPU占用率下降是非常可观的,这份投入绝对值得。