1. 项目概述与核心价值
在嵌入式系统开发中,串口通信是连接传感器、执行器、调试终端和其他微控制器的基石。然而,当数据流量增大或系统需要处理多任务时,传统的轮询或中断驱动方式会迅速成为性能瓶颈,大量消耗宝贵的CPU周期。这时,直接内存访问技术就成了解放CPU、提升系统整体效率的关键。它允许数据在外设(如UART)和内存之间“自动”搬运,CPU只需发起和结束传输,中间过程无需干预。
但仅仅启用DMA还不够。想象一下,如果UART每收到一个字节就向DMA控制器请求一次传输,那么DMA请求的频繁中断和微小数据块的搬移开销,反而会抵消DMA带来的优势。这就是FIFO和DMA触发阈值机制发挥作用的地方。它们共同构成了一个高效的“缓冲-批量”处理流水线:FIFO作为数据的蓄水池,积累到一定水位(阈值)后,才触发DMA进行一次批量搬运。这种设计极大地减少了DMA请求的次数和总线占用,让数据传输变得平滑且高效。
本文将以德州仪器AM261x系列微控制器的UART模块为蓝本,深入剖析其FIFO DMA模式的运作机制。我们不止步于手册的寄存器描述,而是结合我多年的嵌入式通信开发经验,拆解四种DMA模式(0-3)的应用场景、详细解读阈值配置的逻辑陷阱、并手把手演示从模式配置到安全关闭的完整流程。无论你是正在优化现有串口通信性能,还是为新的高带宽应用选型设计,理解这些底层机制都将帮助你构建出更稳定、更高效的嵌入式通信系统。
2. DMA模式深度解析:从模式0到模式3
AM261x的UART模块提供了四种DMA操作模式(Mode 0, 1, 2, 3),其核心区别在于DMA请求信号的分配和使用逻辑。理解这些模式是进行正确配置的第一步。
2.1 模式0与模式1:现代双通道DMA的基石
模式0和模式1是当前最常用、功能最完整的模式,它们为TX(发送)和RX(接收)分别提供了独立的DMA请求信号。
- DMA模式0:这是最基础的独立双通道模式。当
SCR[0]=0且FCR[3]=0时启用。在此模式下,TX和RX的DMA请求通过独立的信号线(通常是USARTi_DMA_TX和USARTi_DMA_RX)发出,可以分别映射到DMA控制器的不同通道,实现全双工、完全独立的DMA传输。发送和接收的阈值、传输量均可独立配置,互不干扰。 - DMA模式1:在模式0的基础上,增加了一层由
FCR[3](DMA_MODE)位控制的使能开关。当SCR[0]=0且FCR[3]=1时启用。其物理连接与模式0相同,但FCR[3]位提供了一个软件快速开关。你可以通过简单地置位或清零FCR[3]来全局启用或禁用UART的DMA功能,而无需改动SCR寄存器中更复杂的模式配置。这在需要动态切换传输方式(例如从DMA模式切换回中断模式进行调试)时非常方便。
实操心得:在新项目设计中,我强烈建议优先使用模式1。它既保留了模式0的全部功能,又通过
FCR[3]位提供了额外的灵活性。你可以将SCR寄存器的配置作为初始化的一部分固化,后续仅通过操作FCR[3]来管理DMA的启停,代码逻辑会更清晰。
2.2 模式2与模式3:理解“传统模式”的遗产
模式2和模式3被手册标记为“传统模式”,它们只使用一个DMA请求信号(USARTi_DMA0)来服务TX或RX中的一个方向。
- DMA模式2:仅RX使用DMA。在此模式下,
USARTi_DMA0信号专用于接收方向的DMA请求。发送方向则必须使用其他方式(如中断或轮询)来处理。这种模式适用于那些数据接收流量大、但发送数据量小或实时性要求不高的场景,例如数据采集器,它大部分时间在接收传感器数据,偶尔才发送配置指令或状态报告。 - DMA模式3:仅TX使用DMA。与模式2相反,
USARTi_DMA0信号专用于发送。接收方向需使用非DMA方式。这适用于需要持续、高速发送数据(如日志流、音频流),但接收多为短命令的应用。
模式选择的关键逻辑:模式的选择并非在FCR或SCR中直接写入一个模式编号,而是通过SCR[0](DMA_MODE_CTL)和FCR[3](DMA_MODE)两个位的组合,以及当SCR[0]=1时由SCR[2:1](DMA_MODE_2)位域共同决定的。这看起来有些复杂,我将其整理成一个决策表:
| 期望的DMA模式 | SCR[0](DMA_MODE_CTL) | FCR[3](DMA_MODE) | SCR[2:1](DMA_MODE_2) | 说明 |
|---|---|---|---|---|
| 禁用DMA | 1 | 忽略 | 00 | FCR[3]被忽略,完全由SCR寄存器控制。 |
| 模式0 | 0 | 0 | 忽略 | 经典双通道独立模式。 |
| 模式1 | 0 | 1 | 忽略 | 带使能开关的双通道模式。 |
| 模式1(替代) | 1 | 忽略 | 01 | 通过SCR寄存器配置的模式1,FCR[3]被忽略。 |
| 模式2(仅RX) | 1 | 忽略 | 10 | 传统模式,仅接收使用DMA。 |
| 模式3(仅TX) | 1 | 忽略 | 11 | 传统模式,仅发送使用DMA。 |
注意事项:这里存在一个关键的“覆盖”规则。当
SCR[0]设置为1时,SCR[2:1]的配置将完全覆盖FCR[3]的设置。这意味着,即使你在FCR[3]中使能了DMA,只要SCR[0]=1且SCR[2:1]=00,DMA功能依然是被禁用的。在调试DMA不生效的问题时,务必同时检查SCR和FCR这两个寄存器,确认它们处于一致的配置状态。
2.3 FIFO禁用时的回退机制
手册中有一句非常关键但容易被忽略的话:“If the FIFOs are disabled (FCR[0]=0), DMA operations occur in single character transfers.”
这意味着,FIFO的使能是高效DMA传输的前提。如果FIFO被禁用(FCR[0]=0),无论你配置了哪种DMA模式,DMA请求都会退化为“每收到/发送一个字符就请求一次”的模式。这相当于把DMA用成了“自动化的单字节中断”,总线仲裁和上下文切换的开销会非常大,完全失去了使用DMA和FIFO的意义。因此,在启用任何DMA功能前,必须确保FCR[0](FIFO使能位)已被置为1。
3. 阈值配置的艺术:平衡性能与实时性
DMA传输的效率和实时性,很大程度上由TX/RX FIFO的触发阈值决定。阈值设得太高,可能导致FIFO已满但DMA请求还未触发,造成数据溢出;设得太低,又会频繁产生DMA请求,增加总线负载。AM261x提供了灵活的阈值配置方法,尤其是针对TX DMA。
3.1 RX FIFO阈值:相对简单的触发
对于接收方向,逻辑相对直接。DMA请求在RX FIFO中的数据量达到UART_TLR寄存器中定义的触发水平时产生。当DMA控制器读取了相应数量的字节后,请求取消。例如,如果RX FIFO深度为64字节,阈值设为16,那么每收到16个字节,UART就会产生一个DMA请求,DMA控制器一次搬运16字节。这能有效减少中断次数。
3.2 TX FIFO阈值:两种计算策略与“空间”概念
发送方向的阈值配置更为复杂,因为它引入了“空间”的概念。TX DMA请求的触发条件不是“FIFO中有多少数据”,而是“FIFO中有多少空闲空间”。
AM261x提供了两种阈值编程方式,由UART_MDR3[2](SET_DMA_TX_THRESHOLD)位控制:
方式一:直接阈值法(SET_DMA_TX_THRESHOLD = 1)阈值直接等于UART_TX_DMA_THRESHOLD寄存器的值。当TX FIFO中的空闲空间(即“空间”)大于或等于此阈值时,DMA请求被置起。DMA控制器开始向FIFO写入数据,直到写入了UART_TLR寄存器中定义的“触发空间数”个字节后,请求才取消。如果写入的字节数不足,DMA请求将保持有效。
方式二:计算阈值法(SET_DMA_TX_THRESHOLD = 0)阈值 = TX FIFO总大小 -UART_TLR寄存器中定义的“触发空间数”。这是更常用也更直观的方式。DMA请求在TX FIFO中的数据量低于这个计算出的阈值时被置起。例如,TX FIFO大小为64字节,UART_TLR设置的触发空间数为8,那么阈值就是64-8=56字节。当FIFO中数据少于56字节(即空闲空间大于8字节)时,触发DMA请求,DMA控制器写入8字节数据后请求取消。
手册中的图示(Figure 13-73, 13-74)清晰地展示了这个过程。关键在于,UART_TLR寄存器中为TX定义的值,代表的是“希望DMA一次填充多少字节到FIFO中”。这个值必须与DMA控制器的传输量(Burst Size或Transfer Size)精确匹配,否则会导致DMA请求无法正确取消或提前取消,引发传输错误。
核心避坑指南:这里有一个极其重要的保护机制。手册指出:“If the selected threshold level + trigger level exceeds max buffer size, then the original TX DMA threshold method is used to prevent TX overrun, regardless of the UART_MDR3[1] NONDEFAULT_FREQ value.” 意思是,如果你配置的“阈值” + “触发空间数” > FIFO总大小,系统为了防止TX溢出(Overrun),会自动忽略你的配置,回退到使用“FIFO大小”作为阈值的默认方法。例如,FIFO为64字节,你设置阈值为60,触发空间数为8,60+8=68>64,此时配置失效,系统会使用阈值=64。这实际上意味着DMA请求只在FIFO完全空时才触发,失去了阈值调优的意义。在配置时,务必确保“阈值+触发空间数 <= FIFO总大小”。
3.3 阈值配置实战建议
- 确定传输特性:首先评估你的数据流。是稳定流(如音频)还是突发包(如Modbus帧)?对延迟敏感吗?
- 计算合理值:对于稳定流,可以设置较大的触发空间数(如FIFO深度的一半),以减少DMA请求频率。对于突发小包,设置较小的触发空间数(如4或8)可以降低单次传输延迟。
- 匹配DMA配置:
UART_TLR中设置的TX触发空间数,必须与DMA控制器配置的单次传输大小(Transfer Size)完全一致。这是很多驱动BUG的根源。 - 启用FIFO:再次强调,配置前确保
FCR[0]=1。 - 选择配置方式:对于大多数应用,使用“计算阈值法”(
SET_DMA_TX_THRESHOLD=0)更直观。直接操作UART_TX_DMA_THRESHOLD寄存器的方式,通常在需要非常规阈值时才使用。
4. 完整配置流程与核心代码实现
理解了原理和模式后,我们来看如何将其转化为代码。以下流程基于模式1(双通道DMA)进行配置,这是最通用的场景。
4.1 初始化步骤分解
假设我们要配置UART2,使用DMA模式1,TX/RX FIFO深度均为64,RX阈值设为16,TX触发空间数设为8(即当TX FIFO数据少于56字节时,请求DMA写入8字节)。
步骤1:关闭模块并进入配置模式在对UART的时钟、模式等关键参数进行修改前,必须先将模块置于禁用状态,并进入配置模式以访问所有寄存器。
// 1. 禁用UART功能模块 HWREG(UART2_BASE + UART_MDR1) = 0x7; // MODE_SELECT = 0x7 (Disable) // 2. 进入配置模式A (访问DLL/DLH等寄存器必需) HWREG(UART2_BASE + UART_LCR) |= 0x80; // LCR[7] = 1, 进入配置模式 // 注意:此时LCR[7:0]不能等于0xBF,否则会进入配置模式B步骤2:配置波特率与帧格式
// 假设系统时钟48MHz, 目标波特率115200, 使用16倍速模式 uint16_t divisor = 48000000 / (16 * 115200); // 计算分频值 = 26 HWREG(UART2_BASE + UART_DLL) = divisor & 0xFF; // 写入分频值低8位 HWREG(UART2_BASE + UART_DLH) = (divisor >> 8) & 0xFF; // 写入分频值高8位 // 配置帧格式:8位数据,无校验,1位停止位 HWREG(UART2_BASE + UART_LCR) = 0x03; // LCR[7]自动清零,退出配置模式A,进入操作模式,并设置8N1步骤3:使能FIFO并设置触发阈值
// 使能FIFO,并设置TX/RX触发阈值 // FCR[0]=1: 使能FIFO // FCR[1:2]: 保留位 // FCR[3]: DMA模式使能位,稍后设置 // FCR[7:4]: 保留位 // TLR[3:0]: RX FIFO触发等级 (设为16字节) // TLR[7:4]: TX FIFO触发空间数 (设为8字节) // 注意:TLR寄存器在某些模式下可能与SPR共享地址,需确保在正确的子模式下访问。 // 通常需要先设置EFR[4]和MCR[6]来进入TCR_TLR子模式。 // 进入TCR_TLR子模式以访问TLR寄存器 HWREG(UART2_BASE + UART_EFR) |= (1 << 4); // EFR[4] = 1 HWREG(UART2_BASE + UART_MCR) |= (1 << 6); // MCR[6] = 1 // 现在可以安全写入TLR uint8_t tlr_value = (8 << 4) | 16; // TX触发空间=8, RX触发等级=16 HWREG(UART2_BASE + UART_TLR) = tlr_value; // 使能FIFO (FCR[0]=1) HWREG(UART2_BASE + UART_FCR) = 0x01;步骤4:配置DMA模式我们选择通过SCR寄存器配置DMA模式1(SCR[0]=1, SCR[2:1]=01),这样FCR[3]位将被忽略,配置更清晰。
// 配置SCR寄存器,启用DMA模式1 // 先读取当前值,避免修改其他位 uint32_t scr_val = HWREG(UART2_BASE + UART_SCR); scr_val &= ~0x07; // 清除[2:0]位 scr_val |= (1 << 0); // SCR[0] = 1, DMA_MODE_CTL使能 scr_val |= (1 << 1); // SCR[2:1] = 01b, 选择DMA模式1 // SCR[2]保持为0 HWREG(UART2_BASE + UART_SCR) = scr_val; // 由于SCR[0]=1,此时FCR[3]位被忽略,但为了代码清晰,可以将其清零 uint32_t fcr_val = HWREG(UART2_BASE + UART_FCR); fcr_val &= ~(1 << 3); // FCR[3] = 0 HWREG(UART2_BASE + UART_FCR) = fcr_val;步骤5:配置TX DMA阈值方式我们选择“计算阈值法”,即SET_DMA_TX_THRESHOLD = 0。
// 配置MDR3寄存器 uint32_t mdr3_val = HWREG(UART2_BASE + UART_MDR3); mdr3_val &= ~(1 << 2); // 清除MDR3[2], 即SET_DMA_TX_THRESHOLD = 0 HWREG(UART2_BASE + UART_MDR3) = mdr3_val; // 此时TX DMA阈值 = FIFO_SIZE - TLR[7:4] = 64 - 8 = 56步骤6:重新使能UART功能模块
// 重新使能UART为16x模式 HWREG(UART2_BASE + UART_MDR1) = 0x0; // MODE_SELECT = 0x0 (UART 16x mode)至此,UART模块本身的DMA相关配置已完成。接下来需要在系统级的DMA控制器(如EDMA)中配置对应的通道,将外设请求与内存缓冲区关联起来,这部分代码依赖于具体的DMA控制器驱动,但核心是正确映射USARTi_DMA_TX和USARTi_DMA_RX请求信号。
4.2 安全关闭TX DMA的官方序列详解
手册13.1.4.4.6.4.1节详细描述了一个“关闭TX DMA”的序列。这个序列非常重要,尤其是在传输中途需要动态禁用DMA,或进入低功耗模式前。其核心目标是避免在FIFO中尚有未传输数据时关闭DMA导致的数据丢失或损坏。这里以我们配置的模式1(通过SCR设置)为例,解析其步骤:
准备工作:确保所有待发送数据已从TX FIFO发出(可通过查询
UART_LSR的TEMT位确认),并且所有RX FIFO中的数据已被读取(如果不再需要RX DMA)。对于UART模式,可以考虑使用RTS/CTS硬件流控来阻止对方继续发送数据。执行关闭序列: a.清除FIFO与计数器:将
UART_FCR[2:1](注意,手册中标注为DMA_MODE位域,此处可能为笔误或特定含义,结合上下文,应是指FCR中用于清除FIFO的位)设置为11b。这步会清空TX和RX FIFO,并重置其内部计数器。fcr_val = HWREG(UART2_BASE + UART_FCR); fcr_val |= (0x03 << 1); // 假设[2:1]位是FIFO清除位 HWREG(UART2_BASE + UART_FCR) = fcr_val; // 该位会自动清零b.切换至仅RX DMA模式:将
SCR[2:1]修改为10b,即DMA模式2(仅RX DMA)。这一步实质上是解除了TX方向与DMA控制器的关联。scr_val = HWREG(UART2_BASE + UART_SCR); scr_val &= ~(0x03 << 1); // 清除[2:1] scr_val |= (0x02 << 1); // 设置为10b, 模式2 (仅RX) HWREG(UART2_BASE + UART_SCR) = scr_val;c.再次清除FIFO与DMA请求:重复步骤a,再次清除FIFO,并确保任何挂起的DMA请求被复位。 d.完全禁用DMA或保持RX DMA:根据需求,将
SCR[2:1]设置为00b(完全禁用DMA),或者保持10b(如果RX DMA仍需运行)。
致命细节警告:手册在步骤a和b之间特别加了一条Note:“It is strongly suggested to do steps ‘a’ and ‘b’ in two separate write in order to avoid malfunction of the device.”必须将清除FIFO和修改DMA模式这两步操作分成两次独立的寄存器写操作,中间至少间隔几个NOP指令或确保写操作完成。如果合并成一次赋值,可能会导致芯片内部状态机紊乱,引发不可预知的行为。这是来自芯片设计者的重要警告,务必遵守。
5. 传输机制图解与实战问题排查
5.1 DMA传输时序全景
手册中的图13-77和13-78清晰地展示了DMA传输和接收的流程。对于发送,流程是:DMA将数据从内存搬至TX FIFO -> FIFO达到触发阈值后停止请求 -> UART内核将数据从FIFO移位发出 -> FIFO数据量低于阈值,再次触发DMA请求。这是一个“填充-发送-再填充”的循环。
对于接收,流程是:UART将数据存入RX FIFO -> FIFO达到触发阈值 -> 触发DMA请求 -> DMA将数据从FIFO搬至内存 -> 搬移完成后请求取消。这是一个“积累-搬运-再积累”的循环。
关键点在于“DMA元素同步”:每次DMA请求,传输的是“一个元素”,在UART上下文中通常就是1个字节。但DMA控制器可以配置为一次请求传输一个“突发”(Burst)的数据块,其大小必须与UART中设置的触发空间数(TX)或触发等级(RX)匹配。
5.2 常见问题与排查技巧实录
在实际项目中,UART DMA配置出错的表现可能千奇百怪。以下是我总结的常见问题速查表:
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| DMA传输完全无法启动 | 1. DMA模式配置错误。 2. FIFO未使能。 3. DMA控制器通道未正确映射或使能。 | 1. 检查SCR[0]、SCR[2:1]、FCR[3],确保模式配置一致且非零(非禁用状态)。2. 确认 FCR[0]为1。3. 使用示波器或逻辑分析仪检查 USARTi_DMA_TX/RX信号线是否有脉冲。检查DMA控制器的请求源(Source)是否选择正确,通道是否使能。 |
| 数据发送不完整,最后几个字节丢失 | 1. TX DMA关闭序列不正确,导致FIFO中残留数据未发出。 2. DMA传输大小与UART触发空间数不匹配。 | 1. 严格按照手册的“关闭TX DMA序列”操作,并在关闭后查询LSR寄存器的TEMT位,确认发送移位寄存器已空。2. 核对DMA配置的传输大小(Transfer Size)是否等于 UART_TLR中设置的TX触发空间数。 |
| 数据接收出现乱码或断帧 | 1. RX FIFO溢出。 2. DMA搬运速度慢于UART接收速度。 3. 波特率误差过大。 | 1. 检查UART_LSR的OE(溢出错误)位。如果置位,说明数据丢失。尝试增大RX FIFO阈值,或提高DMA优先级/带宽。2. 优化DMA传输,使用更高的总线时钟,或减少内存访问冲突。 3. 校准时钟源,使用手册提供的公式精确计算分频值。 |
| DMA请求过于频繁,CPU性能下降 | TX/RX FIFO触发阈值设置过小。 | 适当增大UART_TLR中的触发值。但需权衡:增大阈值会减少请求次数,但会增加单次传输延迟。对于实时性要求高的场景,需找到平衡点。 |
| 仅能传输一次,无法循环触发 | DMA控制器配置为单次(One-shot)模式,而非自动重装(Ping-Pong或Continuous)模式。 | 检查DMA通道的配置,确保在每次传输完成后,传输参数(如源/目标地址、数据量)能自动重载,以准备下一次触发。 |
| 在IrDA模式下,传输结束判断过早 | 手册Note指出,在IrDA模式下,UART_THR空(TX FIFO空)并不代表传输真正结束,CRC和停止位仍在发送。 | 在IrDA模式中,不要仅凭TX FIFO空来判断DMA传输结束。需要等待特定的IrDA传输完成中断,或增加一个固定的延时(几毫秒)。 |
调试心得:
- 善用寄存器回读:在初始化序列的每一步之后,都回读并打印关键寄存器(如
SCR,FCR,MDR3,TLR)的值,确保写入成功且位域符合预期。硬件寄存器访问可能因总线问题失败。 - 先中断,后DMA:在复杂驱动开发初期,不要一开始就启用DMA。先使用中断模式让UART收发数据正常工作,确保波特率、帧格式等基础配置无误。然后再在此基础上叠加DMA配置,这样能将问题域隔离。
- 逻辑分析仪是关键:遇到棘手的时序问题,没有什么比逻辑分析仪更直观。同时抓取UART的TX/RX引脚、DMA请求信号线、以及相关的GPIO(用于标记软件事件),可以清晰地看到数据流、DMA请求触发时机、以及软件响应的延迟,很多问题一目了然。
6. 不同工作模式下的寄存器映射差异
AM261x的UART模块是一个多功能模块,除了标准的UART模式,还支持IrDA、CIR等模式。手册13.1.4.4.7节详细列出了不同模式下可访问的寄存器差异。这对于编写健壮的驱动非常重要。
核心要点:
- 访问模式:通过
UART_LCR[7]位切换操作模式(LCR[7]=0)和配置模式(LCR[7]=1)。在配置模式下,才能访问DLL/DLH等波特率设置寄存器。 - 子模式:在配置模式下,又通过
UART_EFR[4]和UART_MCR[6]的组合,分为MSR_SPR、TCR_TLR、XOFF等子模式,用于访问不同的寄存器集合(如TLR和SPR共享地址)。 - 功能模式:
UART_MDR1[2:0]选择UART、IrDA或CIR等功能。不同功能下,同一寄存器地址的位域含义可能不同(例如UART_IER_UART在不同模式下中断使能位不同),且可用的寄存器集合也不同(详见手册Table 13-86, 13-87, 13-88)。
驱动设计建议:在驱动层抽象出统一的初始化、发送、接收接口。在接口内部,根据UART_MDR1中设置的模式,调用不同的底层配置函数。特别是访问TLR、EFR等寄存器前,务必检查当前所处的访问模式和子模式,必要时进行临时切换,操作完成后再恢复。可以编写类似UART_enterConfigModeA()和UART_exitConfigMode()这样的辅助函数来封装这些细节,避免寄存器误写。
最后,关于波特率计算,手册提供了48MHz时钟下的常用表格。但在实际项目中,时钟源可能不同。务必使用公式除数 = 操作频率 / (波特率倍数 × 目标波特率)进行精确计算,并评估误差(通常要求<2%)。高波特率(如921600以上)对时钟精度要求更高,误差过大会直接导致通信失败。