深入CC253x UART:从寄存器配置到DMA驱动的嵌入式通信实战
1. 串口通信:嵌入式系统的数据动脉
在嵌入式开发的世界里,设备间的“对话”是项目成败的关键。无论是让传感器上报温湿度数据,还是让主控芯片向无线模块发送AT指令,亦或是两块电路板之间交换控制信息,都需要一个可靠、简单且高效的通信渠道。串口通信,特别是其最常见的异步形式UART,就扮演了这个“数据动脉”的角色。它不像SPI或I2C那样需要额外的时钟线来同步,仅凭一根发送线(TX)、一根接收线(RX)和共地,就能完成全双工的数据交换,这种简洁性使其成为嵌入式系统中最基础、最广泛使用的通信接口。
但“能用”和“好用”之间,往往隔着对细节的深刻理解。很多新手在配置串口时,可能只是从例程里拷贝一段初始化代码,设置一个波特率,看到调试助手能收到“Hello World”就觉得大功告成。然而,当项目进入实际应用,面对复杂的电磁环境、长距离传输或高数据量吞吐时,各种问题就会接踵而至:数据丢包、乱码、系统响应迟缓甚至死机。这些问题的根源,常常不在于串口协议本身,而在于开发者是否真正吃透了其背后的寄存器配置、中断服务以及更高级的DMA传输机制。
以德州仪器的CC253x系列芯片为例,其USART(通用同步异步收发器)模块是理解这一系列技术的绝佳样板。它不仅仅是一个UART,还支持同步的SPI模式,其寄存器设计精巧,中断和DMA触发机制完备。本文将带你深入这个模块的腹地,从最根本的波特率计算寄存器UxBAUD和UxGCR讲起,一步步拆解如何通过配置它们来获得精准的通信速率;然后深入到控制与状态寄存器UxCSR、UxUCR,看如何设置数据帧格式、启用硬件流控;接着,我们将探讨如何利用URX和UTX中断来构建响应及时的异步事件驱动架构;最后,解锁DMA触发器的威力,实现数据搬运的“自动驾驶”,彻底解放CPU。我会结合手册中的关键表格和寄存器描述,补充大量实际工程中总结出的配置逻辑、避坑指南和调试技巧,让你不仅知道要设置哪个位,更明白为什么这么设置,以及设置不当会引发什么后果。无论你是正在调试第一块开发板的学生,还是需要优化现有产品通信稳定性的工程师,这些从寄存器层面出发的硬核知识,都将是你构建健壮嵌入式系统的坚实基石。
2. 核心原理与寄存器配置:从时钟到字节流
串口通信的本质,是在一条线上用高低电平的变化来表示0和1,并且通信双方需要约定一个相同的速度来采样这些电平。这个速度就是波特率。配置串口的首要任务,就是让芯片内部的硬件电路以我们期望的波特率工作。这完全依赖于对几个核心寄存器的精确配置。
2.1 波特率生成:BAUD_M与BAUD_E的数学游戏
在CC253x中,波特率由系统时钟(CLK_PER,通常是32MHz)通过一个分频器产生。分频系数由一个公式决定,涉及两个寄存器:UxBAUD.BAUD_M(尾数)和UxGCR.BAUD_E(指数)。手册给出的公式相对抽象,但我们可以用一个更直观的方式来理解。
其波特率计算公式为:波特率 = (CLK_PER) / (256 * (256 + BAUD_M) * 2^(BAUD_E - 1))
其中,CLK_PER是系统时钟频率,BAUD_M是8位无符号整数(0-255),BAUD_E是5位无符号整数(0-31)。这个公式看起来复杂,但TI已经为我们计算好了常用组合。例如,在32MHz系统时钟下,要得到115200bps这个非常经典的波特率,查表可知需要设置BAUD_M = 216,BAUD_E = 11,理论误差仅为0.03%,这在绝大多数应用中完全可以忽略不计。
注意:这里的误差指的是实际生成的波特率与目标波特率之间的偏差。误差过大会导致采样点偏移,积累到一定程度就会造成数据帧错误。通常要求误差在2%以内,而TI提供的这些预计算值都远优于这个标准。
那么,在代码中如何配置呢?假设我们使用USART0,操作如下:
// 设置波特率为115200 (32MHz系统时钟下) U0BAUD = 216; // 设置尾数BAUD_M U0GCR |= 11; // 设置指数BAUD_E,注意是低5位,需保留U0GCR其他位这里有一个关键细节:U0GCR寄存器的高3位(BIT7, BIT6, BIT5)分别用于SPI模式的时钟极性(CPOL)、时钟相位(CPHA)和传输位序(ORDER)。在配置UART模式时,这些位通常应为0(默认值)。因此,在设置BAUD_E时,不能简单粗暴地直接赋值U0GCR = 11,这会清空其他配置位。更安全的做法是使用位操作:
U0GCR = (U0GCR & 0xE0) | 11; // 清零低5位后,写入BAUD_E值实操心得:在项目初期,建议将波特率设置为9600或115200这类最常用、最稳定的值。当通信距离较远或线缆质量一般时,较低的波特率(如9600)有更强的抗干扰能力。只有在需要高速传输(如固件升级)且硬件环境良好时,才考虑使用230400或更高的波特率。
2.2 控制与状态寄存器:塑造通信行为
配置好心跳(波特率)后,我们需要定义通信的“语言规则”,即数据帧格式。这主要通过UxCSR和UxUCR两个寄存器完成。
UxCSR (控制与状态寄存器):这个寄存器是双刃剑,既能控制模式,也能反映状态。
MODE位:必须设置为1,选择UART模式。RE位(接收使能):这是一个极易踩坑的地方!手册明确警告:“Do not enable receive before UART is fully configured.” 必须在波特率、数据位、停止位、校验位等所有参数配置完成后,最后才将RE置1。否则,在配置过程中可能因为引脚电平波动或配置顺序问题,误触发接收,导致状态位混乱或收到垃圾数据。FE(帧错误)和ERR(校验错误):这两个是只写0(R/W0)的状态位。当硬件检测到停止位电平错误或校验失败时,相应位会被硬件置1。它们会在软件读取UxCSR寄存器后被自动清零。这个特性非常有用,意味着你可以在中断服务程序中通过读取UxCSR来清除错误标志,同时获取其值进行判断。RX_BYTE和TX_BYTE:这两个状态位指示数据缓冲区的状态。RX_BYTE=1表示接收缓冲区(UxDBUF)有数据可读;TX_BYTE=1表示发送缓冲区为空,可以写入新的待发送数据。RX_BYTE在读取UxDBUF后自动清零,TX_BYTE在写入UxDBUF后自动清零。
UxUCR (UART控制寄存器):这里定义了数据帧的具体格式。
START和STOP:定义起始位和停止位的电平。必须确保起始位和停止位的电平相反,这是UART协议的基础。通常采用“低电平起始位,高电平停止位”(START=0, STOP=1)。SPB:停止位数量。0代表1个停止位,1代表2个停止位。绝大多数现代设备使用1个停止位。使用2个停止位可以增加帧间隔,在某些古老的或速度特别慢的系统中用于提高可靠性,但会降低有效数据吞吐率。PARITY和BIT9、D9:这三个位共同控制奇偶校验。PARITY=1使能校验功能。BIT9=1则启用第9位传输。如果同时使能了校验(PARITY=1),则第9位的内容由D9决定,用于生成奇校验(D9=0)或偶校验(D9=1)。如果仅需要9位数据通信而不需要硬件校验(例如在多机通信中,第9位作为地址/数据标识位),则应设置PARITY=0,BIT9=1,此时第9位数据由软件写入UxDBUF时的最高位(bit8)决定(需要结合UxGCR.ORDER位序设置)。FLOW:硬件流控使能。如果使能,则需要使用额外的RTS(请求发送)和CTS(清除发送)引脚。在数据收发速度不匹配(如MCU处理慢,而上位机发送快)时,硬件流控能有效防止数据溢出丢失,是高速可靠通信的保障。FLUSH:刷新位。这是一个非常关键但需谨慎使用的功能。写1可以中止当前操作,清空数据缓冲区。但手册特别指出:如果在TX或RX某一位的传输过程中设置FLUSH,刷新动作会等到该位传输完毕后才执行(缓冲区立即清空,但控制位时序的定时器会继续完成当前位的计时)。因此,使用FLUSH时,要么与USART中断对齐(在中断服务程序中操作),要么在操作后等待一个当前波特率下的位时长,再更新数据或配置。滥用FLUSH可能导致数据帧不完整或通信时序错乱。
一个典型的UART初始化代码框架如下:
void UART0_Init(uint32_t baudRate) { // 1. 配置IO引脚功能为外设模式 (例如P0_2为RX, P0_3为TX) P0SEL |= 0x0C; // 假设P0.2和P0.3为UART功能 // 2. 首先关闭接收,避免配置过程中误触发 U0CSR &= ~0x40; // 清除RE位 // 3. 设置UART模式 U0CSR |= 0x80; // 设置MODE=1 // 4. 根据目标波特率查表或计算,设置U0BAUD和U0GCR U0BAUD = 216; U0GCR = (U0GCR & 0xE0) | 11; // 5. 配置数据帧格式:8N1 (8数据位,无校验,1停止位) U0UCR = 0x02; // STOP=1 (高电平停止位),其他位默认0(低起始位,1停止位,无校验,8位) // 6. 最后,使能接收器 U0CSR |= 0x40; // 设置RE=1 }3. 中断驱动架构:从轮询到事件响应
在简单的示例中,我们常用轮询(Polling)方式检查RX_BYTE或TX_BYTE位来收发数据。这种方式代码简单,但效率低下,CPU时间被大量浪费在“询问”状态上。在复杂的多任务系统中,我们需要让CPU去处理更重要的计算或响应其他外设,而串口数据到来或发送完成这类事件,应该以一种“通知”的方式告知CPU。这就是中断(Interrupt)机制。
3.1 中断使能与标志位
CC253x的每个USART有两个独立的中断源:
- RX完成中断 (URXx):当接收到一个完整字节,并将其从移位寄存器转移到接收数据缓冲区
UxDBUF后,此中断被触发。对应的状态标志是RX_BYTE被置1。 - TX中断 (UTXx):当发送数据缓冲区
UxDBUF中的数据被加载到发送移位寄存器,即缓冲区变空,可以写入下一个字节时,此中断被触发。对应的状态标志是TX_BYTE被置1。
这些中断的使能位和标志位分布在不同的寄存器中,需要仔细配置:
- 中断使能寄存器:
IEN0.URX0IE/IEN0.URX1IE:使能USART0/1的RX中断。IEN2.UTX0IE/IEN2.UTX1IE:使能USART0/1的TX中断。
- 中断标志寄存器:
TCON.URX0IF/TCON.URX1IF:USART0/1的RX中断标志。硬件置1,需要软件清零。IRCON2.UTX0IF/IRCON2.UTX1IF:USART0/1的TX中断标志。硬件置1,需要软件清零。
这里存在一个关键差异:UxCSR中的RX_BYTE和TX_BYTE是状态位,它们由硬件自动置位和清零(读UxDBUF清RX_BYTE,写UxDBUF清TX_BYTE)。而TCON和IRCON2中的URXxIF和UTXxIF是中断标志位,它们在中断事件发生时被硬件置1,但必须由软件在中断服务程序(ISR)中手动写0来清除,否则中断将持续触发。
3.2 中断服务程序编写要点
一个健壮的中断服务程序,不仅要处理数据,还要妥善管理标志位。以下是RX中断服务程序的一个示例框架:
#pragma vector = URX0_VECTOR __interrupt void UART0_RX_ISR(void) { uint8_t status; uint8_t received_data; // 1. 读取状态寄存器,此举会清除FE和ERR标志 status = U0CSR; // 2. 检查是否有帧错误或校验错误 if (status & 0x10) { // 检查FE位 // 处理帧错误,例如丢弃该字节或记录错误计数 // 注意:读取U0CSR后FE已自动清零 } if (status & 0x08) { // 检查ERR位 // 处理校验错误 } // 3. 确保是接收完成中断(而非其他中断误入) if (URX0IF) { // 4. 读取接收到的数据(读取操作会使U0CSR.RX_BYTE自动清零) received_data = U0DBUF; // 5. 处理数据,例如放入环形缓冲区 ring_buffer_write(&rx_buf, received_data); // 6. 必须手动清除中断标志位! URX0IF = 0; } }注意事项:
- 先读状态,后读数据:在读取
U0DBUF之前先读取U0CSR,可以将错误标志捕获下来,否则读数据后错误标志可能因后续操作而丢失。 - 区分状态位和中断标志位:
U0CSR.RX_BYTE是状态,读U0DBUF后硬件清零;URX0IF是中断标志,必须软件清零。 - 中断服务要快进快出:避免在ISR中进行复杂运算或长时间操作(如软件延时、等待其他慢速外设)。通常的做法是将数据快速存入一个环形缓冲区(FIFO),并设置一个任务标志,然后在主循环中处理缓冲区数据。
- 使能全局中断:在初始化完所有外设中断后,别忘记使用
EA = 1;语句使能CPU的全局中断。
对于TX中断,其典型用途是实现“非阻塞发送”。你可以准备一个发送缓冲区,在TX中断服务程序中,从缓冲区取出下一个字节写入UxDBUF,直到缓冲区为空后关闭TX中断,避免无数据可发时中断空转。
volatile uint8_t tx_buffer[TX_BUF_SIZE]; volatile uint16_t tx_write_idx = 0, tx_read_idx = 0; volatile uint8_t tx_busy = 0; void UART0_SendByte(uint8_t data) { // 将数据放入发送缓冲区 tx_buffer[tx_write_idx++] = data; tx_write_idx %= TX_BUF_SIZE; // 如果发送器空闲,则启动第一次发送并开启TX中断 if (!tx_busy) { tx_busy = 1; U0DBUF = tx_buffer[tx_read_idx++]; // 发送第一个字节 tx_read_idx %= TX_BUF_SIZE; UTX0IE = 1; // 使能TX中断,以便发送后续字节 } } #pragma vector = UTX0_VECTOR __interrupt void UART0_TX_ISR(void) { UTX0IF = 0; // 清除中断标志 if (tx_read_idx != tx_write_idx) { // 缓冲区还有数据 U0DBUF = tx_buffer[tx_read_idx++]; tx_read_idx %= TX_BUF_SIZE; } else { // 缓冲区已空,关闭TX中断,标记发送器空闲 UTX0IE = 0; tx_busy = 0; } }4. DMA触发传输:解放CPU的终极利器
中断机制虽然比轮询高效,但每个字节的收发仍需要CPU介入中断服务程序进行压栈、跳转、读写数据、出栈等操作。当波特率很高(如921600bps)或需要连续传输大量数据(如固件升级、图像传输)时,频繁的中断仍会消耗可观的CPU资源,并可能因中断响应延迟导致缓冲区溢出。此时,直接存储器访问(DMA)就成了提升系统性能的关键。
4.1 DMA触发机制原理
DMA控制器就像一个“智能搬运工”,它可以在外设和内存之间直接搬运数据,完全不需要CPU参与。CC253x的USART模块提供了完美的DMA支持,它有两个DMA触发器:
- RX DMA触发器:在“接收完成”事件(即一个字节被完整接收并存入
UxDBUF)时激活。 - TX DMA触发器:在“发送完成”事件(即
UxDBUF中的数据被移入发送移位寄存器,缓冲区空)时激活。
你可以配置一个DMA通道,将其源地址或目的地址指向UxDBUF寄存器。例如:
- DMA用于接收:将DMA通道的源地址设置为
UxDBUF,目的地址设置为内存中的某个数组。当RX DMA触发器激活时,DMA自动将UxDBUF中的新数据搬运到指定内存,搬运次数和长度由你预先配置。 - DMA用于发送:将DMA通道的源地址设置为内存中的数组,目的地址设置为
UxDBUF。当TX DMA触发器激活(即发送缓冲区空)时,DMA自动将内存中的下一个数据搬运到UxDBUF进行发送。
4.2 DMA配置流程与核心代码
配置DMA通常涉及以下几个步骤(以CC2530的DMA通道0用于UART0接收为例):
- 配置DMA描述符:DMA通道的行为由一个描述符结构体控制,需要配置源/目的地址、传输长度、触发模式等。
- 配置USART:正常初始化UART,但通常不需要开启USART的RX/TX中断,因为数据搬运由DMA接管。
- 配置DMA通道:将描述符地址告诉DMA控制器,并启用该通道。
- 启动传输:对于发送,可能需要手动写入第一个字节来启动链条;对于接收,使能DMA和USART接收后自动开始。
以下是关键代码片段:
#include <ioCC2530.h> #define DMA_RX_BUF_SIZE 128 uint8_t dma_rx_buffer[DMA_RX_BUF_SIZE]; // DMA描述符(位于XDATA空间,通常用__xdata定义) __xdata DMA_DESC dmaChannel0Desc; void DMA_UART0_RX_Init(void) { // 1. 初始化UART0(波特率、帧格式等),但先不开启RX中断 UART0_Init(115200); // U0CSR的RE位已在UART0_Init中最后使能 // 2. 配置DMA描述符 dmaChannel0Desc.SRCADDRH = (uint16)(&U0DBUF) >> 8; // 源地址高字节:U0DBUF寄存器地址 dmaChannel0Desc.SRCADDRL = (uint16)(&U0DBUF); dmaChannel0Desc.DESTADDRH = (uint16)dma_rx_buffer >> 8; // 目的地址高字节:内存缓冲区 dmaChannel0Desc.DESTADDRL = (uint16)dma_rx_buffer; dmaChannel0Desc.VLEN = 0x01; // 使用LEN作为传输长度 dmaChannel0Desc.LENH = 0; // 传输长度高字节 dmaChannel0Desc.LENL = DMA_RX_BUF_SIZE; // 传输长度低字节:128字节 dmaChannel0Desc.WORDSIZE = 0; // 字节传输(0),而非字传输(1) dmaChannel0Desc.TMODE = 0x02; // 触发模式:重复、单次触发 dmaChannel0Desc.TRIG = 25; // 触发源:25对应URX0 DMA触发 (请查具体芯片手册) dmaChannel0Desc.SRCINC = 0; // 源地址不递增(外设寄存器地址固定) dmaChannel0Desc.DESTINC = 1; // 目的地址递增(存入连续内存) dmaChannel0Desc.IRQMASK = 0; // 传输完成不产生中断(也可设为1,在传输完指定长度后中断) dmaChannel0Desc.M8 = 0; // 使用全部8位 dmaChannel0Desc.PRIORITY = 0; // 通道优先级 // 3. 配置DMA通道0 DMA0CFGH = (uint16)(&dmaChannel0Desc) >> 8; DMA0CFGL = (uint16)(&dmaChannel0Desc); DMAIRQ = 0x00; // 清除所有DMA中断标志(可选) // 使能DMA通道0的ARM位,等待触发 DMAARM |= 0x01; }当UART0每接收到一个字节,RX DMA触发器就会激活一次,DMA通道0自动将U0DBUF中的数据搬运到dma_rx_buffer数组中,并且目的地址自动递增。当搬运完128个字节后(如果配置了IRQMASK=1),会产生DMA传输完成中断,你可以在中断中处理这批数据,然后重新配置描述符并ARM通道,开始下一轮接收。
DMA使用的核心优势与陷阱:
- 优势:CPU占用率极低。在DMA搬运数据期间,CPU可以休眠或执行其他任务,非常适合低功耗应用。大数据量传输效率极高。
- 陷阱一:缓冲区管理:DMA是“无脑”搬运,你需要确保目的缓冲区足够大,不会溢出。通常结合双缓冲区(Ping-Pong Buffer)或环形缓冲区技术,在DMA填满一个缓冲区时,切换至另一个缓冲区,同时CPU处理已满的那个。
- 陷阱二:触发与同步:确保DMA触发源配置正确(
TRIG值)。对于UART接收,DMA的触发频率等于数据到达频率。如果波特率很高,DMA通道的优先级和总线带宽需要考量。 - 陷阱三:数据一致性:当CPU和DMA可能同时访问同一块内存区域时(例如CPU读取DMA正在写入的缓冲区),需要考虑数据一致性问题。简单的做法是使用标志位或禁止CPU在DMA传输期间访问该缓冲区。
5. 实战配置与调试技巧实录
理解了寄存器、中断和DMA的原理后,我们来看几个综合性的实战场景和调试中必然会遇到的“坑”。
5.1 场景一:高可靠性的工业传感器数据采集
需求:通过UART以9600bps周期读取多个工业传感器数据,环境存在电气噪声,要求通信绝对可靠,并允许偶尔的错误重传。
方案设计:
- 波特率与帧格式:选择9600bps,1个停止位,启用奇偶校验(
PARITY=1, BIT9=1)。奇偶校验可以在硬件层面过滤掉单比特错误。数据帧格式设为8位数据+1位校验位。 - 错误处理:使能RX中断,并在中断服务程序中首先检查
UxCSR中的FE和ERR位。一旦发现错误,立即丢弃该字节,并通过软件协议(如增加校验和、超时重发)请求重传整个数据包。 - 流量控制:由于传感器响应可能较慢,建议启用硬件流控(
FLOW=1),连接RTS/CTS线。确保MCU在未准备好接收时,能通过CTS信号通知传感器暂停发送,防止接收缓冲区溢出。 - 中断优先级:将UART RX中断设置为较高优先级,确保数据到来时能被及时响应,避免因处理其他低优先级任务而丢失字节。
配置要点:
U0UCR = 0x0A; // 0b00001010: FLOW=0(假设不接硬件流控),BIT9=1(9位传输),PARITY=1(校验使能),D9=0(奇校验) // 注意:使能了PARITY和BIT9,实际数据位是9位,其中第9位是硬件生成的校验位。 // 在读取U0DBUF时,我们得到的是8位数据,校验结果由硬件自动检查并反映在ERR位。5.2 场景二:高速无线模块AT指令通信
需求:与4G/Wi-Fi模块通信,波特率115200bps,需要频繁发送AT指令并接收长文本响应(如HTTP数据)。
方案设计:
- DMA接收:这是最理想的方案。模块返回的数据流可能很长且连续,使用DMA接收可以将CPU彻底解放出来。配置一个足够大的环形缓冲区(例如1KB),DMA以循环模式工作,自动将数据填入缓冲区。CPU只需定期检查缓冲区中有多少新数据即可处理。
- 中断发送:AT指令的发送是离散的、由应用层触发的。采用“非阻塞发送+TX中断+发送缓冲区”的模式。应用层将AT指令字符串放入发送缓冲区,启动发送后立即返回,由TX中断服务程序在后台逐个字节发送完毕。
- 超时机制:必须实现软件超时。对于每个AT指令的发送,启动一个定时器。如果在预期时间内未收到完整响应(如”OK\r\n”或”ERROR\r\n”),则判定为超时,进行错误处理或重试。
避坑指南:
- DMA缓冲区溢出:这是高速接收中最常见的问题。如果CPU处理速度跟不上数据到达速度,DMA缓冲区会被写满并覆盖旧数据。解决方案:1) 增大缓冲区;2) 提高CPU处理优先级或优化处理算法;3) 如果协议允许,使用硬件流控让对端暂停发送。
- AT指令解析:避免在中断服务程序或DMA完成中断中解析复杂的AT响应。只做最简单的数据搬运和标志设置,将解析工作放在主循环中。
5.3 调试技巧与常见问题排查
收不到任何数据(RX静默)
- 检查电平:首先用示波器或逻辑分析仪测量TX、RX引脚。确认发送方确实发出了符合UART格式的电平信号(起始位低电平,数据位,停止位高电平)。
- 检查波特率:这是最常见的原因。用示波器测量一个字节的时长,计算实际波特率,与配置值对比。务必确认系统时钟
CLK_PER是否是你认为的值(32MHz?16MHz?)。 - 检查引脚复用:确认IO引脚是否已正确配置为外设功能(
PxSEL寄存器),而不是普通的GPIO。 - 检查接收使能:确认
UxCSR.RE位是否已置1,并且是在所有其他配置完成后才置1的。 - 检查缓冲区:读取
UxDBUF前,先确认UxCSR.RX_BYTE是否为1。
收到乱码
- 波特率不匹配:即使误差在2%以内,长报文也可能出错。这是最可能的原因,请精确核对双方波特率设置。
- 数据帧格式不一致:检查双方的数据位长度(8位/9位)、停止位数量(1位/2位)、奇偶校验设置是否完全相同。特别是当一方启用校验而另一方未启用时,必然乱码。
- 电气干扰:长距离通信时,考虑增加线路驱动(如MAX3232电平转换芯片本身有驱动能力),或使用差分信号(如RS485)。
- 地线问题:确保通信双方共地,这是信号参考的基础。
发送数据丢失或卡住
- 阻塞式发送未等待:如果在轮询发送中,未检查
TX_BYTE状态就连续写入UxDBUF,会导致数据被覆盖丢失。必须等待TX_BYTE==1后再写入下一个字节。 - 中断冲突:如果使用了TX中断,但在中断服务程序中未正确清除
UTXxIF标志,会导致中断只触发一次。或者,全局中断被意外关闭(EA=0)。 - 硬件流控未处理:如果使能了硬件流控(
FLOW=1),但CTS引脚被对端拉低(表示对方未准备好),那么发送将会被硬件挂起。需要检查CTS引脚状态。
- 阻塞式发送未等待:如果在轮询发送中,未检查
使用DMA时数据不完整
- DMA触发源错误:确认DMA描述符中的
TRIG值是否正确对应了URXx或UTXx的DMA触发编号。 - DMA未ARM:配置好描述符和通道后,必须设置
DMAARM寄存器的对应位来武装通道,使其进入等待触发状态。 - 缓冲区指针未更新:在单次触发模式(
TMODE非重复模式)下,DMA传输完指定长度后会自动停止。需要在传输完成中断中重新配置源/目的地址和长度,并再次ARM通道。 - 内存区域:确保DMA操作的目的地址所在的内存区域(如XDATA)是可访问的,并且没有被其他代码意外修改。
- DMA触发源错误:确认DMA描述符中的
我个人在多年的嵌入式开发中,调试串口问题最有效的工具始终是逻辑分析仪。它能清晰地展示出每一帧数据的波形、起始位、数据位、停止位的宽度,让你直观地判断波特率是否准确、数据内容是否正确、帧格式是否匹配。在问题定位上,它比printf大法要直接和可靠得多。当你觉得配置“绝对正确”却仍有问题时,不妨用逻辑分析仪抓一下波形,真相往往就藏在那些微妙的时序细节里。
