从硬件寄存器视角:TXE、TC、RXNE如何驱动串口数据流
在嵌入式系统开发中,串口通信是最基础却又最关键的通信方式之一。对于从事底层开发的工程师来说,仅仅知道如何配置串口参数是远远不够的——真正理解数据在硬件层面的流动机制,才能从根本上解决实际开发中遇到的数据丢失、时序错乱等问题。本文将从硬件寄存器的微观视角,深入剖析STM32芯片内部TDR、RDR与移位寄存器的协同工作机制,揭示TXE、TC、RXNE三个标志位在串口数据流中的核心作用。
1. 串口数据寄存器的硬件架构与数据流
STM32的串口模块采用了一种精巧的双缓冲结构设计,这种设计在保证数据可靠传输的同时,最大限度地提高了数据传输效率。从硬件层面来看,串口数据流涉及三个核心组件:发送数据寄存器(TDR)、接收数据寄存器(RDR)和移位寄存器。
TDR(发送数据寄存器) 是CPU向串口模块写入待发送数据的地方。当我们调用USART_SendData()函数时,实际上就是将数据写入到这个寄存器中。但这里有一个关键细节:数据写入TDR后并不会立即发送到引脚上,而是需要等待发送移位寄存器空闲时,硬件自动将TDR中的数据转移到移位寄存器中。
移位寄存器 是实际进行并串转换的硬件单元。它从TDR接收完整的字节数据,然后按照设定的波特率逐位将数据发送到TX引脚。这个转换过程是硬件自动完成的,不需要CPU干预。
RDR(接收数据寄存器) 则负责存储从RX引脚接收到的完整数据。当移位寄存器从引脚上接收完一个完整的字节后,硬件会自动将数据转移到RDR中,此时CPU就可以读取这些数据了。
这种双缓冲结构的关键优势在于:当移位寄存器正在发送或接收数据时,CPU可以同时准备下一个要发送的数据或处理已经接收到的数据,从而实现近乎并行的数据处理流程。
硬件设计提示:双缓冲结构虽然提高了效率,但也引入了时序复杂性。开发者必须清楚每个操作发生在哪个阶段,否则容易出现数据覆盖或丢失的问题。
2. TXE标志位的硬件机理与实战应用
TXE(Transmit Data Register Empty)标志位是串口发送过程中最重要的状态指示器。从硬件信号的角度来看,TXE标志被置1的时刻精确对应着TDR寄存器中的数据已经被成功转移到移位寄存器的时刻。
硬件层面的TXE置位条件:
- 上电复位后,TDR寄存器初始状态为空,TXE自动置1
- 当数据从TDR转移到移位寄存器后,TXE被硬件自动置1
- 向TDR写入数据后,TXE被硬件自动清零
在实际开发中,TXE标志的使用直接关系到数据发送的可靠性。常见的错误做法是连续多次调用USART_SendData()而不检查TXE状态:
// 错误示例:可能导致数据丢失
USART_SendData(USART1, 'A');
USART_SendData(USART1, 'B');
USART_SendData(USART1, 'C');
这种写法的风险在于:当第一个字符'A'刚刚被写入TDR但还未转移到移位寄存器时,TXE仍然为0(表示TDR不空),此时写入'B'会覆盖TDR中尚未处理的数据'A',导致数据丢失。
正确的轮询发送方式:
// 等待TDR为空后再写入下一个字符
USART_SendData(USART1, 'H');
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); // 等待TXE置位
USART_SendData(USART1, 'K');
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
USART_SendData(USART1, 'G');
在中断模式下,TXE标志的作用更加关键。使能TXE中断后,每当TDR为空时就会触发中断,我们可以在中断服务程序中写入下一个要发送的数据:
// TXE中断发送示例
volatile uint8_t *pDataByte;
volatile uint32_t dataLength;
void USART_StartSend(USART_TypeDef* USARTx, uint8_t *data, uint32_t len)
{
pDataByte = data;
dataLength = len;
// 先手动发送第一个字节
USART_SendData(USARTx, *pDataByte++);
dataLength--;
// 然后使能TXE中断,后续字节在中断中发送
USART_ITConfig(USARTx, USART_IT_TXE, ENABLE);
}
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1, USART_IT_TXE) != RESET)
{
if(dataLength > 0)
{
USART_SendData(USART1, *pDataByte++);
dataLength--;
}
else
{
// 所有数据已发送,关闭TXE中断
USART_ITConfig(USART1, USART_IT_TXE, DISABLE);
}
}
}
3. TC标志位的时序特性与精确控制
TC(Transmission Complete)标志位反映了更加全局的发送状态。与TXE只关注TDR寄存器不同,TC标志同时考虑了TDR和移位寄存器两者的状态。
TC置位的硬件条件:
- 移位寄存器中没有正在发送的数据(已发送完毕)
- TDR寄存器为空(没有待发送的数据)
- 只有当这两个条件同时满足时,TC标志才会被硬件置1
这种特性使得TC标志特别适合用于检测整个发送过程的完成,特别是在发送多个字节数据时。例如,当我们发送一个字符串时,最后一个字符写入TDR后,TXE会很快置位(因为数据已转移到移位寄存器),但此时数据可能还在移位寄存器中尚未完全发送出去。如果此时进行关闭串口等操作,就会导致最后的数据丢失。
使用TC标志确保完整发送:
void USART_SendString(USART_TypeDef* USARTx, const char *str)
{
while(*str != '\0')
{
USART_SendData(USARTx, *str++);
while(USART_GetFlagStatus(USARTx, USART_FLAG_TXE) == RESET); // 等待TXE
}
while(USART_GetFlagStatus(USARTx, USART_FLAG_TC) == RESET); // 等待TC
// 此时所有数据已完全发送完毕
}
在中断模式下使用TC标志需要特别注意:TC中断在每个字节发送完成后都会触发,而不仅仅是在整个传输完成时。这意味着我们需要在代码中区分是中间字节发送完成还是整个传输完成。
TC中断的精细控制:
volatile uint8_t tcComplete = 0;
void USART_TC_Send(USART_TypeDef* USARTx, uint8_t *data, uint32_t len)
{
// 先清除TC标志
USART_ClearFlag(USARTx, USART_FLAG_TC);
// 发送第一个字节
USART_SendData(USARTx, *data++);
len--;
// 使能TC中断
USART_ITConfig(USARTx, USART_IT_TC, ENABLE);
}
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1, USART_IT_TC) != RESET)
{
if(len > 0)
{
USART_SendData(USART1, *data++);
len--;
}
else
{
// 所有数据发送完成
USART_ITConfig(USART1, USART_IT_TC, DISABLE);
tcComplete = 1;
}
USART_ClearITPendingBit(USART1, USART_IT_TC);
}
}
4. RXNE标志位的数据接收机制
RXNE(Receive Data Register Not Empty)标志位是数据接收过程中的核心状态指示。从硬件信号流来看,当移位寄存器从RX引脚完整接收一个字节后,硬件会自动将数据转移到RDR寄存器,并同时将RXNE标志置1。
RXNE置位的硬件时机:
- 移位寄存器完成一个字节的接收
- 数据从移位寄存器转移到RDR寄存器
- RDR寄存器中存在尚未被CPU读取的数据
在轮询模式下,我们需要不断检查RXNE状态来判断是否有新数据到达:
uint8_t USART_ReceiveByte(USART_TypeDef* USARTx)
{
while(USART_GetFlagStatus(USARTx, USART_FLAG_RXNE) == RESET)
{
// 等待数据到达
}
return (uint8_t)USART_ReceiveData(USARTx);
}
在中断模式下,RXNE标志的使用更加高效。使能RXNE中断后,每当有新数据到达就会自动触发中断,CPU无需轮询检查:
volatile uint8_t rxData;
volatile uint8_t rxFlag = 0;
void USART_EnableRXInterrupt(USART_TypeDef* USARTx)
{
USART_ITConfig(USARTx, USART_IT_RXNE, ENABLE);
}
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
rxData = USART_ReceiveData(USART1);
rxFlag = 1; // 设置数据到达标志
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
5. 标志位的协同工作与实战优化
在实际项目中,TXE、TC和RXNE三个标志位往往需要协同工作,才能实现高效可靠的串口通信。理解它们之间的时序关系至关重要。
标志位时序关系表:
| 操作阶段 | TXE状态 | TC状态 | RXNE状态 | 说明 |
|---|---|---|---|---|
| 上电复位 | 1 | 0 | 0 | TDR为空,无发送无接收 |
| 写入TDR | 0 | 0 | 0 | 数据已写入但未开始发送 |
| 数据转移到移位寄存器 | 1 | 0 | 0 | 可以写入下一个数据 |
| 移位寄存器发送开始 | 1 | 0 | 0 | 数据正在发送中 |
| 字节发送完成 | 1 | 1 | 0 | 当前字节完全发送完毕 |
| 数据接收完成 | 1 | 1 | 1 | 同时有发送完成和接收数据 |
基于这种时序特性,我们可以设计出更加高效的串口通信方案。例如,在需要同时收发数据的全双工通信中:
// 全双工串口通信示例
volatile uint8_t txBuffer[256];
volatile uint8_t rxBuffer[256];
volatile uint32_t txHead = 0, txTail = 0;
volatile uint32_t rxHead = 0, rxTail = 0;
void USART_InitFullDuplex(USART_TypeDef* USARTx)
{
// 使能TXE和RXNE中断
USART_ITConfig(USARTx, USART_IT_TXE, ENABLE);
USART_ITConfig(USARTx, USART_IT_RXNE, ENABLE);
}
void USART1_IRQHandler(void)
{
// 处理发送中断
if(USART_GetITStatus(USART1, USART_IT_TXE) != RESET)
{
if(txHead != txTail)
{
USART_SendData(USART1, txBuffer[txTail]);
txTail = (txTail + 1) % 256;
}
else
{
// 发送缓冲区空,关闭TXE中断
USART_ITConfig(USART1, USART_IT_TXE, DISABLE);
}
}
// 处理接收中断
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
uint8_t data = USART_ReceiveData(USART1);
rxBuffer[rxHead] = data;
rxHead = (rxHead + 1) % 256;
// 处理数据接收后的逻辑...
}
}
这种设计实现了真正的全双工通信:发送和接收完全独立进行,互不干扰。TXE中断负责在发送缓冲区有数据时及时发送,RXNE中断负责及时处理接收到的数据。
在实际项目中,我经常遇到开发者因为不理解这些标志位的精确时序而导致的bug。最常见的是在发送大量数据时,只检查TXE就认为数据已发送完成,结果最后几个字节丢失。另一个常见问题是在中断处理中没有及时清除标志位,导致中断重复触发或者丢失。
对于高性能应用,还可以进一步优化:使用DMA与这些标志位配合。例如,让DMA负责大数据块的传输,而用TC中断来通知传输完成,这样既能减轻CPU负担,又能保证传输的可靠性。
理解TXE、TC、RXNE这些标志位背后的硬件机理,就像是掌握了串口通信的"内功心法"。无论面对什么样的应用场景,都能设计出既高效又可靠的通信方案。在实际开发中,我建议每次都要仔细考虑每个标志位的状态变化时机,特别是在临界条件下的行为,这样才能写出真正健壮的串口通信代码。
281



被折叠的 条评论
为什么被折叠?



