1. 项目概述
在嵌入式开发领域,设备间的数据交换是构建复杂系统的基石。无论是调试信息输出、传感器数据采集,还是多个微控制器之间的协同工作,都需要一种可靠、高效且成本可控的通信方式。串行通信,特别是基于通用异步收发器(UART)的串口通信,因其协议简单、硬件资源需求少、实现成本低而成为最经典、应用最广泛的解决方案之一。它不像SPI或I2C那样需要时钟线同步,仅凭一根发送线(TX)、一根接收线(RX)和共地(GND)即可完成全双工数据交换,这种“异步”特性赋予了其极大的灵活性和布线简便性。
然而,看似简单的UART背后,其硬件实现和软件配置却蕴含着诸多工程细节。一个配置不当的串口,轻则导致数据乱码、通信中断,重则可能因缓冲区溢出而丢失关键数据,在工业控制或数据采集场景中这是不可接受的。因此,深入理解UART的工作原理,特别是其内部寄存器的功能与配置方法,是每一位嵌入式工程师的必修课。本文将以德州仪器(TI)经典的DM35x系列数字媒体片上系统(DMSoC)中的UART外设为蓝本,进行一次“庖丁解牛”式的深度剖析。我们将不仅停留在“如何配置波特率”的层面,更会深入到其基于TL16C550标准的内部架构,探讨16字节FIFO缓冲机制如何优雅地减轻CPU中断负担,分析UART2独有的自动流控(Autoflow Control)功能如何从根本上避免数据溢出错误,并逐一解读每个关键寄存器的比特位所代表的精确含义。无论你是正在调试一块新的开发板,还是希望优化现有系统的串口通信效率,相信这篇结合了原理与实战的详解都能为你提供清晰的路径和可靠的参考。
2. UART核心架构与工作原理解析
要熟练驾驭UART,绝不能仅仅满足于调用库函数。理解其内部时钟树、数据通路和控制逻辑,是进行精准配置和高效调试的前提。DM35x的UART外设是一个高度集成化的数字模块,其设计遵循了工业标准TL16C550,并在其基础上进行了功能增强。
2.1 时钟生成:一切时序的源头
UART通信的“心跳”来自于其输入时钟。在DM35x中,UART模块的时钟源由系统时钟发生器提供。这个输入时钟(UART input clock)的频率是配置波特率的基石。UART内部包含一个可编程的波特率发生器,其核心是一个16位的分频器(Divisor Latch),由两个8位寄存器DLL(Divisor Latch LSB)和DLH(Divisor Latch MSB)组成。
这里有一个关键概念:UART内部的工作时钟(BCLK)并不是直接等于我们设定的波特率(Baud Rate)。BCLK的频率是波特率的16倍。也就是说,在传输或接收每一位数据(一个Bit)的周期内,UART内部会进行16次采样或计时。这样设计的主要目的是为了提高抗干扰能力和定位起始位的准确性。接收端会在起始位开始后的第8个BCLK周期对数据线进行采样,这正好位于起始位时间的中间点,可以有效避开信号边沿的抖动区域,确保采样稳定。
因此,计算分频器值的公式是:分频器值 = UART输入时钟频率 / (期望波特率 * 16)
例如,假设UART输入时钟为24 MHz,我们需要配置成9600波特率,计算过程如下:分频器值 = 24,000,000 / (9600 * 16) = 24,000,000 / 153,600 = 156.25
分频器值必须为整数,所以取整后为156。此时实际的波特率为:实际波特率 = 24,000,000 / (156 * 16) ≈ 9615.385 bps与目标值存在约0.16%的误差。在异步通信中,只要收发双方的波特率误差在一定范围内(通常要求小于2-3%),通信即可正常进行。上表2中列举了24MHz时钟下常用波特率对应的理论分频值与实际误差,可以看到大部分常用波特率的误差都非常小。
注意:在编写初始化代码时,必须确保在修改DLL和DLH之前,通过设置线路控制寄存器(LCR)的DLAB位为1,来使能对分频器寄存器的访问。配置完成后,再将DLAB位清零,以访问其他数据/状态寄存器。这是一个经典的配置“坑点”,忘记操作DLAB位会导致写入的波特率配置无效。
2.2 数据格式与协议帧
UART以“帧”为单位传输数据。一帧数据包含以下部分(按传输顺序):
- 起始位(Start Bit):一个逻辑低电平(0),标志着数据帧的开始。它用于同步收发双方的时钟。
- 数据位(Data Bits):紧接着起始位之后,可以是5、6、7或8位。这由LCR寄存器的WLS[1:0]位决定。通常使用8位数据位(一个字节)。
- 校验位(Parity Bit,可选):用于简单的错误检测。可以是奇校验、偶校验或无校验。由LCR寄存器的PEN和EPS位控制。
- 停止位(Stop Bit(s)):一个或多个逻辑高电平(1),标志着数据帧的结束。可以是1、1.5或2位。由LCR寄存器的STB位控制。
图4清晰地展示了不同数据位长度下的帧格式。需要注意的是,在接收端,无论发送端设置了多少个停止位,UART都只检测第一个停止位。如果检测到停止位为低电平(应为高电平),则会产生帧错误(Framing Error)标志。
2.3 发送与接收的硬件流水线
发送流程:
- CPU或DMA将待发送的数据写入发送保持寄存器(THR)。在FIFO模式下,THR是一个16字节的先进先出缓冲区。
- 当发送移位寄存器(TSR)为空时,控制逻辑自动将THR(或FIFO中的下一个字节)加载到TSR中。
- 波特率发生器产生的BCLK驱动TSR,将并行数据逐位移出到TX引脚,同时自动添加起始位、校验位(如果使能)和停止位,形成完整的串行数据流。
接收流程:
- RX引脚上的串行数据流在BCLK的同步下,被采样并移入接收移位寄存器(RSR)。
- 当一帧数据接收完成(即收到停止位),且接收缓冲寄存器(RBR)或接收FIFO有空闲位置时,RSR中的数据(以及对应的3位错误状态:奇偶错、帧错误、线路中断)被并行转移到RBR/FIFO中。
- CPU或DMA可以从RBR/FIFO中读取数据。在非FIFO模式下,每收到一个字节就会产生一次中断;在FIFO模式下,则可以根据FIFO填充水平来触发中断,大大减少CPU干预频率。
这种“保持寄存器/缓冲寄存器 + 移位寄存器”的双缓冲乃至多缓冲(FIFO)架构,是UART能够实现连续、可靠通信的关键。它允许软件在相对宽松的时间窗口内处理数据,而不会因为偶尔的延迟导致数据丢失。
3. 核心功能特性深度剖析:FIFO与自动流控
DM35x的UART并非简单的TL16C450兼容模式,其核心价值在于提供了TL16C550标准的FIFO模式和UART2独有的自动流控功能。这两项特性是提升系统性能和可靠性的“利器”。
3.1 16字节深度FIFO:中断负载的“减压阀”
在早期的UART(如TL16C450)或许多微控制器的基本UART模式中,每收发一个字节就会产生一次中断。在高速通信(如115200波特率及以上)时,频繁的中断会严重消耗CPU资源,导致系统响应变慢,甚至可能因为中断服务程序(ISR)处理不及时而发生数据溢出。
DM35x的UART内置了独立的16字节发送FIFO和接收FIFO。启用FIFO模式后(通过设置FCR寄存器的FIFOEN位),情况发生了根本改变:
- 对于发送:软件可以一次性向发送FIFO写入最多16个字节的数据。UART硬件会按顺序自动发送,仅在FIFO完全变空时产生一次“发送保持寄存器空”中断,通知软件可以填充下一批数据。这相当于将最多16次中断合并为1次。
- 对于接收:接收FIFO提供了可编程的触发水平(Trigger Level),通过FCR寄存器的RT[1:0]位设置为1、4、8或14字节。当FIFO中积累的数据达到或超过这个水平时,才产生一次“接收数据就绪”中断。软件的中断服务程序可以一次性读取多个字节,极大提高了处理效率。
FIFO中断模式与查询模式:
- 中断模式:使能FCR的FIFOEN位,同时使能IER中相应的接收或发送中断。这是最常用的高效方式,让CPU可以异步处理通信任务。
- 查询模式:使能FCR的FIFOEN位,但禁用IER中的所有中断。软件通过定期读取线路状态寄存器(LSR)的DR(数据就绪)位、THRE(发送保持寄存器空)位等来判断状态。这种方式适用于简单的、实时性要求不高的场景,或者在没有中断资源的系统中。
接收超时中断(Receiver Time-out Interrupt): 这是一个非常实用的特性,用于处理“零星数据”。即使接收到的数据量很少,没有达到FIFO触发水平,如果满足以下条件,也会产生中断:
- FIFO中至少有一个字符。
- 自最后一个字符被接收后,已经过去了超过4个字符时间(Character Time)。
- 自上次CPU读取FIFO后,也已经过去了超过4个字符时间。 这确保了即使是一个单独的、后续没有跟数据的字节,也能被及时处理,而不会一直滞留在FIFO中等待达到触发水平。
3.2 自动流控(Autoflow Control):硬件级的流量保障
数据溢出(Overrun Error)是串口通信中常见的错误,发生在接收端尚未读取旧数据,而新数据已经到达并覆盖RBR时。在软件流控(如XON/XOFF)不可靠或无法使用的场合,硬件流控是终极解决方案。DM35x的UART2模块支持通过RTS(Request to Send)和CTS(Clear to Send)信号实现自动流控。
工作原理: 如图5所示,当两个支持自动流控的UART(如两个DM35x的UART2)背对背连接时,可以将一方的RTS输出连接到另一方的CTS输入,反之亦然。
- 接收方(RTS控制):接收方UART通过监测自身接收FIFO的填充水平来控制RTS引脚输出。
- 当FIFO中数据量达到预设的触发水平(1, 4, 8, 14字节)时,RTS信号被置为无效(高电平),向发送方宣告:“我的缓冲区快满了,请暂停发送”。
- 当FIFO中的数据被读取,存量下降到触发水平以下时(对于触发水平14,是低于14字节;对于1、4、8,则是FIFO被读空时),RTS信号被重新置为有效(低电平),邀请发送方继续发送。
- 发送方(CTS响应):发送方UART在发送每一个字节的之前,会检查CTS输入引脚的状态。
- 如果CTS为有效(低电平),则正常发送该字节。
- 如果CTS为无效(高电平),则发送方会完成当前字节的发送,然后暂停,直到CTS再次变低。如图7所示,CTS必须在当前字节的停止位中点之前变为无效,才能成功阻止下一个字节的发送。
工程价值: 自动流控完全由硬件逻辑实现,无需CPU干预。它从根本上杜绝了因接收端处理不及时导致的溢出错误,特别适用于高速、大数据量的稳定传输场景,如文件传输、固件升级等。需要注意的是,要使能此功能,除了正确连接硬件引脚,还需要设置MCR寄存器的AFE位为1。
实操心得:在调试带硬件流控的通信时,如果发现通信卡死,一个常见的排查步骤是使用示波器或逻辑分析仪同时抓取TX、RX、RTS、CTS四根线的波形。观察RTS/CTS的握手时序是否符合预期,往往能快速定位是软件配置问题还是硬件连接问题。另外,有些RS-232电平转换芯片对RTS/CTS的支持不完整,选择转换芯片时也需要注意。
4. 寄存器配置详解与编程指南
理解了原理,最终要落实到寄存器的配置上。DM35x的UART寄存器映射是标准化的,掌握每个寄存器的关键位,就等于掌握了控制UART的开关。下面我们抛开手册式的罗列,从功能驱动的角度,分组讲解最关键的几个寄存器及其配置流程。
4.1 通信参数配置组:LCR, DLL, DLH
这一组寄存器决定了通信的基本格式和速度。
线路控制寄存器(LCR - Line Control Register):这是配置的“总开关”。
- DLAB (Bit 7):分频器锁存访问位。这是第一个要操作的位。置1时,访问地址偏移0x00和0x01将指向分频器锁存器DLL和DLH;清零时,则指向接收缓冲寄存器RBR和发送保持寄存器THR。标准操作顺序是:先置位DLAB,配置DLL/DLH;再清零DLAB,进行数据收发和其他配置。
- BC (Bit 6):中断控制位。置1时,强制TX输出为低电平(Space),即发送一个“Break”信号。用于通知对方通信线路中断。
- STB (Bit 2):停止位选择。0=1个停止位;1=2个停止位(数据位为5位时,为1.5个停止位)。
- EPS, PEN (Bit 4, Bit 3):校验位控制。
- PEN=0:无校验。
- PEN=1且EPS=0:奇校验。
- PEN=1且EPS=1:偶校验。
- WLS[1:0] (Bit 1:0):字长选择。00=5位;01=6位;10=7位;11=8位。
分频器锁存器(DLL & DLH):波特率的“调音台”。
- 这两个8位寄存器共同组成一个16位除数N。
N = (DLH << 8) | DLL。 - 波特率计算公式:
波特率 = UART输入时钟频率 / (16 * N)。 - 编程示例(假设时钟24MHz,目标波特率115200):
// 首先,设置LCR的DLAB=1,以允许配置波特率 UART_REG(LCR) |= (1 << 7); // 假设UART_REG是映射到寄存器地址的宏 // 计算除数N = 24,000,000 / (115200 * 16) = 13.02 -> 取整13 uint16_t divisor = 13; UART_REG(DLL) = divisor & 0xFF; // 写入低8位 UART_REG(DLH) = (divisor >> 8) & 0xFF; // 写入高8位 // 然后,清除DLAB,配置其他参数并准备收发数据 UART_REG(LCR) = 0x03; // DLAB=0, 8位数据,无校验,1位停止位 (0b0000 0011)
4.2 中断与FIFO控制组:IER, IIR, FCR
这一组寄存器管理着UART如何与CPU互动,是性能调优的关键。
中断使能寄存器(IER - Interrupt Enable Register):
- ERBFI (Bit 0):接收数据可用中断使能。置1后,当RBR中有数据(非FIFO模式)或接收FIFO达到触发水平(FIFO模式)时产生中断。
- ETBEI (Bit 1):发送保持寄存器空中断使能。置1后,当THR空(非FIFO模式)或发送FIFO空(FIFO模式)时产生中断。
- ELSI (Bit 2):接收线路状态中断使能。置1后,当发生溢出错误、奇偶错误、帧错误或检测到Break信号时产生中断。此中断优先级最高。
- EDSSI (Bit 3):Modem状态中断使能(仅UART2有效)。置1后,当CTS、RTS等Modem状态线发生变化时产生中断。
中断识别寄存器(IIR - Interrupt Identification Register)(只读): 当发生中断时,CPU读取此寄存器以判断中断源。其低3位(Bit2-0)编码指示了最高优先级的中断类型。这是一个“查看即清除”的寄存器,对某些中断类型的读取操作会自动清除该中断在内部的挂起状态。表11详细列出了编码与中断类型的对应关系及清除方式。例如,编码0110表示字符接收超时中断,清除方式是读取接收缓冲寄存器(RBR)。
FIFO控制寄存器(FCR - FIFO Control Register)(只写):
- FIFOEN (Bit 0):FIFO使能位。必须置1以启用发送和接收FIFO。这是发挥UART高性能的基础。
- RCVRCLR, XMITCLR (Bit 1, Bit 2):接收FIFO复位、发送FIFO复位。写1可分别清除对应的FIFO。在初始化或需要清空缓冲区时使用。
- RT[1:0] (Bit 7:6):接收FIFO触发水平选择。00=1字节;01=4字节;10=8字节;11=14字节。根据你的系统处理能力和数据包大小来设定。如果每次中断处理的数据量较大,可以设高一点以减少中断频率。
4.3 状态与数据寄存器组:LSR, RBR, THR
这一组寄存器用于获取状态和交换数据。
线路状态寄存器(LSR - Line Status Register)(只读):这是诊断通信问题的“仪表盘”。
- DR (Bit 0):数据就绪。为1表示RBR或接收FIFO中有数据可读。在查询模式下,轮询此位来判断是否有新数据。
- OE, PE, FE, BI (Bit 1-4):分别是溢出错误、奇偶错误、帧错误和中断检测标志。任何一位为1都表示接收过程中发生了问题。读取LSR会清除OE、PE、FE、BI标志。良好的程序应该在每次读取数据后检查这些错误位。
- THRE (Bit 5):发送保持寄存器空。为1表示THR或发送FIFO已空,可以写入新的发送数据。在查询发送模式下,轮询此位来判断是否可以发送下一个字节。
- TEMT (Bit 6):发送器空。为1表示THR和TSR都为空,即所有数据都已发送完毕。在发送完最后一包数据后,可以查询此位以确保所有数据均已物理发出,再关闭串口或进入低功耗模式。
接收缓冲寄存器(RBR)与发送保持寄存器(THR): 这两个寄存器在DLAB=0时,共享同一个地址。对该地址的读操作访问的是RBR(获取接收到的数据),写操作访问的是THR(写入要发送的数据)。在FIFO模式下,读写操作是针对16字节的FIFO缓冲区进行的。
4.4 初始化流程与示例代码
一个稳健的UART初始化流程应遵循以下步骤:
- 关闭UART:通过PWREMU_MGMT寄存器禁用UART时钟,确保配置过程稳定。
- 配置引脚复用:将对应的GPIO引脚功能设置为UART的TX、RX(以及可选的RTS、CTS)。
- 软件复位:通过PWREMU_MGMT寄存器的URRST位对UART进行软件复位。
- 使能UART时钟:重新使能PWREMU_MGMT寄存器中的时钟使能位。
- 配置波特率:设置LCR的DLAB=1,然后写入DLL和DLH。
- 配置通信格式:设置LCR(DLAB清零),选择数据位、停止位、校验位。
- 配置FIFO与中断:写入FCR以启用并可能复位FIFO,设置触发水平。配置IER以启用所需的中断。
- (仅UART2)配置自动流控:设置MCR寄存器的AFE位为1以启用自动流控。
- 使能UART收发器:设置PWREMU_MGMT寄存器的UTXEN和URXEN位。
下面是一个简化的C语言初始化代码框架:
void UART_Init(uint32_t baseAddr, uint32_t baudRate, uint8_t dataBits, uint8_t parity, uint8_t stopBits) { volatile uint32_t *uart = (volatile uint32_t *)baseAddr; uint32_t divisor; // 1. 禁用UART (假设通过系统控制模块) // SYSCTL->RCGCUART &= ~(1U); // 示例,具体寄存器名需查手册 // 2. 等待UART模块进入空闲(可选但推荐) // while(uart[FR] & UART_FR_BUSY); // 检查忙标志 // 3. 配置LCR的DLAB=1,准备设置波特率 uart[LCR] |= (1 << 7); // DLAB = 1 // 4. 计算并设置波特率除数 (假设输入时钟为24MHz) divisor = 24000000 / (baudRate * 16); uart[DLL] = divisor & 0xFF; uart[DLH] = (divisor >> 8) & 0xFF; // 5. 配置线路参数,同时清除DLAB uart[LCR] = 0; uart[LCR] |= (dataBits & 0x03); // 设置数据位 if(parity != PARITY_NONE) { uart[LCR] |= (1 << 3); // PEN = 1 uart[LCR] |= ((parity & 0x01) << 4); // EPS (0=奇, 1=偶) } uart[LCR] |= ((stopBits & 0x01) << 2); // STB // 6. 启用并复位FIFO,设置触发水平为8字节 uart[FCR] = (1 << 0) | (1 << 1) | (1 << 2) | (0x2 << 6); // FIFOEN=1, RCVRCLR=1, XMITCLR=1, RT=8字节(10b) // 7. 使能接收数据可用中断和接收线路状态中断 uart[IER] = (1 << 0) | (1 << 2); // ERBFI=1, ELSI=1 // 8. (如果是UART2)使能自动流控 if(baseAddr == UART2_BASE) { uart[MCR] |= (1 << 5); // AFE = 1 } // 9. 使能UART收发器 (通过PWREMU_MGMT,地址偏移可能不同) // uart[PWREMU_MGMT] = (1 << 13) | (1 << 14); // UTXEN=1, URXEN=1 }5. 实战应用:在DM35x上构建可靠的串口通信驱动
理解了所有寄存器之后,我们需要将它们组合起来,构建一个在真实项目中稳定运行的驱动。这里我们设计一个基于中断和环形缓冲区(Ring Buffer)的驱动模型,它比单纯使用硬件FIFO更灵活,能处理更大的数据突发。
5.1 驱动架构设计
我们的驱动将包含以下组件:
- 硬件抽象层(HAL):直接操作UART寄存器的底层函数,如
UART_Init(),UART_SendByte(),UART_GetStatus()等。 - 环形缓冲区(Ring Buffer):在软件层面为发送和接收分别创建一个环形缓冲区。发送缓冲区用于缓存应用程序要发送的数据,接收缓冲区用于存储从硬件FIFO中读出的数据,等待应用程序处理。
- 中断服务程序(ISR):在中断中,快速读取IIR判断中断源,然后进行相应处理:
- 接收中断:从硬件RBR/FIFO中读取所有可用数据,存入软件接收环形缓冲区。
- 发送中断:从软件发送环形缓冲区中取出数据,写入硬件THR/FIFO,直到FIFO满或软件缓冲区空。
- 线路状态中断:读取LSR,记录错误标志,进行错误处理或通知上层。
- 应用层接口:提供
UART_Send()和UART_Receive()等非阻塞式API。UART_Send()将数据拷贝到发送环形缓冲区并尝试启动发送(触发第一次发送中断);UART_Receive()则从接收环形缓冲区中拷贝数据。
这种“硬件FIFO + 软件环形缓冲区”的双重缓冲架构,既利用了硬件FIFO减少中断次数的优势,又通过软件缓冲区提供了更大的弹性,能够平滑处理数据流的波动。
5.2 关键代码实现与解析
环形缓冲区实现:
typedef struct { uint8_t *buffer; uint16_t size; uint16_t head; // 写指针 uint16_t tail; // 读指针 } ring_buffer_t; bool RingBuffer_Write(ring_buffer_t *rb, uint8_t data) { uint16_t next_head = (rb->head + 1) % rb->size; if(next_head == rb->tail) return false; // 缓冲区满 rb->buffer[rb->head] = data; rb->head = next_head; return true; } bool RingBuffer_Read(ring_buffer_t *rb, uint8_t *data) { if(rb->head == rb->tail) return false; // 缓冲区空 *data = rb->buffer[rb->tail]; rb->tail = (rb->tail + 1) % rb->size; return true; }中断服务程序核心逻辑:
void UART_IRQHandler(void) { volatile uint32_t *uart = UART2_BASE; uint8_t iir_value = uart[IIR]; // 检查是否是UART中断(Bit0=0表示有中断 pending) if((iir_value & 0x01) == 0) { uint8_t int_id = (iir_value >> 1) & 0x07; switch(int_id) { case 0x04: // 接收数据可用(FIFO触发水平达到) case 0x0C: // 字符超时 // 循环读取,直到硬件FIFO为空 while(uart[LSR] & 0x01) { // DR位为1 uint8_t data = uart[RBR]; // 读取数据,同时清除RBR uint8_t lsr = uart[LSR]; // 读取LSR以获取错误标志 if(lsr & 0x1E) { // 检查OE, PE, FE, BI错误 // 处理错误,记录日志等 g_uart_error_flags |= (lsr & 0x1E); } else { // 将有效数据存入软件接收环形缓冲区 RingBuffer_Write(&g_rx_ringbuf, data); } } // 可以在这里设置信号量或标志,通知应用层任务有数据到达 break; case 0x02: // 发送保持寄存器空(THRE) // 从软件发送环形缓冲区取数据,填入硬件FIFO while((uart[LSR] & 0x20) && RingBuffer_Read(&g_tx_ringbuf, &data)) { // THRE为1且软件缓冲区有数据 uart[THR] = data; } // 如果软件发送缓冲区空了,可以禁用THRE中断以避免无意义中断 if(RingBuffer_IsEmpty(&g_tx_ringbuf)) { uart[IER] &= ~(1 << 1); // 禁用ETBEI中断 } break; case 0x06: // 接收线路状态错误(OE, PE, FE, BI) // 读取LSR会自动清除错误标志,但我们已经在上面的接收流程中处理了 // 这里可以额外进行一些错误统计或系统报警 break; // ... 其他中断类型处理 } } }应用层发送函数:
int32_t UART_Send(const uint8_t *data, uint16_t length) { // 1. 将数据写入软件发送环形缓冲区 for(uint16_t i = 0; i < length; i++) { if(!RingBuffer_Write(&g_tx_ringbuf, data[i])) { return -1; // 缓冲区满,返回错误 } } // 2. 使能发送空中断,以启动发送过程 volatile uint32_t *uart = UART2_BASE; uint8_t ier = uart[IER]; if(!(ier & (1 << 1))) { // 如果THRE中断未使能 uart[IER] = ier | (1 << 1); // 使能THRE中断 // 手动触发一次发送:如果THR空,写入第一个字节 if(uart[LSR] & 0x20) { uint8_t first_byte; if(RingBuffer_Read(&g_tx_ringbuf, &first_byte)) { uart[THR] = first_byte; } } } return length; }5.3 性能优化与注意事项
- 中断优先级设置:UART中断,尤其是接收中断,应该被赋予较高的优先级,以防止因中断延迟导致数据溢出。但也要注意,其优先级不应高于系统关键中断(如看门狗)。
- DMA配合使用:对于极高波特率(如921600以上)或大数据量传输,可以考虑使用DMA。DM35x的UART支持发送和接收DMA事件。可以配置DMA控制器在接收FIFO达到触发水平时,自动将数据搬运到指定的内存区域;或在发送时,从内存自动填充发送FIFO。这能彻底解放CPU。
- 电源管理:在低功耗应用中,注意PWREMU_MGMT寄存器的配置。当串口不使用时,可以关闭其时钟以节省功耗。重新启用时,需要重新初始化波特率等参数。
- 错误处理与恢复:健壮的驱动必须处理线路错误。除了在ISR中记录错误,还应有上层机制来查询和清除错误状态。对于持续的帧错误或奇偶错误,可能是波特率不匹配或线路干扰,驱动可以尝试重新初始化或通知网络层。
6. 调试技巧与常见问题排查实录
即使理解了所有原理和配置,在实际调试中依然会遇到各种问题。下面是我在多年项目中总结的一些典型问题及其排查思路,它们比手册上的理论更有价值。
6.1 通信完全无反应(收不到也发不出)
- 检查清单:
- 物理连接:TX和RX是否交叉连接?电平转换芯片(如MAX3232)是否正常工作?用万用表测量TX引脚在发送时是否有电平变化。
- 时钟与电源:UART模块的输入时钟是否使能?芯片和电平转换器的供电是否正常?
- 引脚复用:这是最容易被忽略的一点!DM35x的引脚功能是复用的。确认你的UART TX/RX引脚是否已经通过PinMux寄存器正确配置为UART功能,而不是GPIO或其他外设功能。
- 基本配置顺序:是否遗漏了关键步骤?特别是DLAB位的操作顺序(先置1配波特率,再清零)和FIFO的使能(FCR的Bit0必须为1)。一个简单的验证方法是:在初始化后,尝试向THR写一个字节,然后用示波器或逻辑分析仪观察TX引脚是否有波形输出。如果没有,问题大概率出在1-3步。
6.2 能发送但不能接收,或反之
- 单向通信故障:
- 仅不能接收:检查RX引脚连接、外部设备发送是否正常。在软件端,确认IER的ERBFI位是否已使能接收中断,或者查询模式下是否在轮询LSR的DR位。重点检查FCR的RCVRCLR位是否在初始化时被置1后又清零?该位是只写的,读回总是0,但写1会清空接收FIFO。如果初始化后不小心又写了一次FCR(例如在使能FIFO时),可能会意外清空刚收到的数据。
- 仅不能发送:检查TX引脚连接。在软件端,确认是否成功写入了THR。在中断模式下,检查THRE中断是否使能。在查询模式下,发送前必须等待LSR的THRE位或TEMT位为1。一个常见错误是:在FIFO模式下,向THR写入数据后,如果没有使能THRE中断,且FIFO未满,数据会停留在FIFO里但不一定会被立即发送出去。需要确保有机制(中断或查询)来持续喂数据,或者最后一次写入后等待TEMT位变1。
6.3 数据错乱(乱码)
- 波特率不匹配:这是乱码的首要原因。计算分频器时是否使用了正确的UART输入时钟频率?这个频率可能不是主频,而是经过分频后的外设时钟。务必查阅芯片数据手册的时钟章节。用示波器测量实际发送的位宽,计算出发送方的实际波特率,与接收方配置进行对比。
- 数据格式不一致:双方的数据位、停止位、校验位设置必须完全相同。例如,一方8N1(8数据位,无校验,1停止位),另一方7E1(7数据位,偶校验,1停止位),必然导致乱码和帧错误。检查LCR寄存器的WLS、STB、PEN、EPS位配置。
- 信号完整性问题:在长距离或高速通信时,导线阻抗、反射、噪声会导致信号畸变。表现为偶尔的帧错误或奇偶错误。解决方案包括:降低波特率、使用屏蔽线、在两端增加串联匹配电阻(如22Ω-100Ω)、检查地线连接是否良好。
6.4 通信一段时间后卡死
- 缓冲区溢出:在非FIFO模式或FIFO触发水平设置过低,且中断服务程序处理太慢时,接收端可能因来不及读取数据而发生溢出(OE)。OE标志一旦置位,必须通过读取LSR来清除,否则可能影响后续状态判断。确保IER的ELSI中断使能,并在ISR中处理OE错误。
- 自动流控失效:如果使用了UART2的自动流控但依然卡死,检查:
- MCR的AFE位是否已设置为1。
- RTS和CTS引脚是否已正确交叉连接(本机RTS接对端CTS,本机CTS接对端RTS)。
- 用逻辑分析仪查看RTS/CTS信号是否在正常翻转。可能是对端设备不支持硬件流控。
- 中断丢失或嵌套问题:如果中断服务程序执行时间过长,且未及时清除中断标志,可能导致后续中断丢失。确保ISR尽可能短小高效。对于复杂的处理,应在ISR中快速将数据移入环形缓冲区,然后通过任务信号量等方式唤醒一个低优先级的处理任务。避免在中断中进行耗时操作(如打印日志到另一个串口)。
6.5 使用调试工具
工欲善其事,必先利其器。串口调试离不开几样关键工具:
- 逻辑分析仪:这是终极武器。可以同时捕获TX、RX、RTS、CTS多条线上的数字波形,直观地看到每一位数据、每一个帧的起止、以及流控信号的交互时序。对于分析复杂的通信问题(如时序问题、流控问题)不可或缺。
- 示波器:用于观察信号质量,测量波特率,查看是否有过冲、振铃或噪声。
- 串口调试助手:在PC端使用,用于验证发送的数据和接收显示的数据。选择一个可以显示十六进制和ASCII码,并能发送任意十六进制数据的调试助手。
- 终端软件(如SecureCRT, Minicom):用于与运行了命令行系统的嵌入式设备交互。
调试时,养成“由简入繁”的习惯:先确保最简单的8N1格式、无流控、低波特率(如9600)能够通信,然后再逐步增加复杂度(启用FIFO、提高波特率、启用流控、启用校验等)。每做一步更改,都进行验证。将你的初始化配置参数(波特率、LCR、FCR、IER的值)打印出来或通过调试器查看,与预期值进行比对,往往能快速发现配置错误。