1. 串口通信基础与STM32 USART模块
大家好,我是老李,在嵌入式行业摸爬滚打十多年了。今天咱们来聊聊STM32的串口通信,特别是USART模块的轮询和中断两种工作模式。说实话,刚开始接触STM32的时候,我也被这些概念搞得头晕,但实际用多了就会发现,这东西真的非常实用。
串口通信可以说是嵌入式开发中最基础、最常用的通信方式之一。简单来说,它就是通过一根数据线逐个比特地传输数据。STM32芯片内部集成了USART(Universal Synchronous/Asynchronous Receiver/Transmitter)模块,也就是通用同步异步收发器。
USART和UART有什么区别呢?其实很简单:USART既支持同步通信也支持异步通信,而UART只支持异步通信。同步通信需要额外的时钟线来同步数据传输,异步通信则不需要。在大多数应用中,我们用的都是异步模式。
我记得刚开始做项目的时候,最常用的就是USART和电脑通信进行调试。通过串口打印调试信息,比用LED灯调试方便多了。而且很多传感器模块,比如GPS、蓝牙、WiFi模块,都是通过串口与主控芯片通信的。
2. USART轮询模式深度解析
2.1 轮询模式的工作原理
轮询模式是最基础的串口通信方式,它的工作原理很简单:CPU不断地查询串口状态寄存器的标志位,检查是否有数据需要发送或接收。
举个例子,这就好比你在等一个重要的快递包裹,但你不知道快递什么时候到,所以每隔几分钟就跑到门口看看快递来了没有。这种方式虽然简单直接,但效率很低,因为大部分时间你都在跑去看门口,做不了其他事情。
在STM32中,我们主要关注两个标志位:TXE(发送数据寄存器空)和TC(发送完成)。TXE=1表示发送数据寄存器为空,可以写入新的数据;TC=1表示所有数据都已经发送完成。
2.2 轮询发送的具体实现
让我们来看一个具体的例子,如何用轮询方式发送一个字节的数据:
void USART_SendByte(USART_TypeDef* USARTx, uint8_t data)
{
// 等待发送数据寄存器为空
while(USART_GetFlagStatus(USARTx, USART_FLAG_TXE) == RESET);
// 写入要发送的数据
USART_SendData(USARTx, data);
// 等待发送完成
while(USART_GetFlagStatus(USARTx, USART_FLAG_TC) == RESET);
}
这段代码虽然简单,但有几个需要注意的地方。首先,我们等待TXE标志位为1,这表示发送数据寄存器已经准备好接收新的数据。然后我们写入数据,最后等待TC标志位为1,确保数据已经完全发送出去了。
在实际项目中,我们经常需要发送字符串而不是单个字节。这时候可以这样实现:
void USART_SendString(USART_TypeDef* USARTx, char *str)
{
while(*str != '\0')
{
USART_SendByte(USARTx, *str);
str++;
}
// 等待最后一个字节发送完成
while(USART_GetFlagStatus(USARTx, USART_FLAG_TC) == RESET);
}
2.3 轮询接收的实现方法
接收数据也是类似的原理,我们需要不断检查RXNE(接收数据寄存器非空)标志位:
uint8_t USART_ReceiveByte(USART_TypeDef* USARTx)
{
// 等待接收到数据
while(USART_GetFlagStatus(USARTx, USART_FLAG_RXNE) == RESET);
// 读取接收到的数据
return USART_ReceiveData(USARTx);
}
轮询模式的最大优点是简单易懂,代码容易编写和调试。但它有个致命的缺点:CPU利用率极低。因为CPU大部分时间都在循环检查标志位,不能执行其他任务。在实际项目中,除非是非常简单的应用,否则一般不推荐使用纯轮询模式。
3. USART中断模式全面掌握
3.1 中断模式的工作原理
中断模式是更高效的串口通信方式。在这种模式下,CPU不需要 constantly轮询标志位,而是可以去执行其他任务。当有数据需要发送或接收时,USART模块会向CPU发出中断请求,CPU暂停当前工作来处理串口数据。
继续用快递的比喻,这次你不需要 constantly跑到门口看快递来了没有,而是安装了一个门铃。当快递到来时,门铃会响,你再去开门取快递。这样在等待快递的过程中,你可以正常做其他事情。
STM32的USART支持多种中断源,包括发送完成中断、接收数据寄存器非空中断、校验错误中断等。最常用的是接收数据寄存器非空中断(USART_IT_RXNE),当接收到数据时触发中断。
3.2 中断接收的配置与实现
配置USART中断需要以下几个步骤:
首先,在初始化USART时开启接收中断:
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
然后配置NVIC(嵌套向量中断控制器):
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
最后编写中断服务函数:
void USART1_IRQHandler(void)
{
uint8_t receivedData;
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
// 读取接收到的数据
receivedData = USART_ReceiveData(USART1);
// 处理数据...
// 清除中断标志位
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
3.3 中断发送的高级应用
中断模式也可以用于数据发送,虽然不如接收那么常用。中断发送的主要优点是可以在后台发送大量数据,而不需要CPU constantly等待。
实现中断发送需要以下步骤:
首先,开启发送中断:
USART_ITConfig(USART1, USART_IT_TXE, ENABLE);
然后在中断服务函数中处理发送:
void USART1_IRQHandler(void)
{
static uint8_t *pData = NULL;
static uint16_t dataLength = 0;
if(USART_GetITStatus(USART1, USART_IT_TXE) != RESET)
{
if(dataLength > 0)
{
USART_SendData(USART1, *pData);
pData++;
dataLength--;
}
else
{
// 所有数据已发送完成,关闭发送中断
USART_ITConfig(USART1, USART_IT_TXE, DISABLE);
}
USART_ClearITPendingBit(USART1, USART_IT_TXE);
}
// 接收中断处理...
}
使用时,只需要设置好要发送的数据指针和长度,然后开启发送中断即可:
void USART_SendDataByIT(uint8_t *data, uint16_t length)
{
// 设置全局变量
pData = data;
dataLength = length;
// 开启发送中断
USART_ITConfig(USART1, USART_IT_TXE, ENABLE);
}
4. 轮询与中断模式的对比分析
4.1 性能与资源消耗对比
轮询和中断模式各有优缺点,适用于不同的应用场景。我们来做个详细的对比:
响应速度方面:轮询模式的响应时间取决于CPU检查标志位的频率。如果检查间隔长,可能会错过数据;如果检查间隔短,CPU资源浪费严重。中断模式的响应时间是确定的,一旦有数据到达,CPU会立即响应。
CPU利用率方面:轮询模式CPU利用率极高,几乎100%的时间都在检查标志位。中断模式CPU利用率低,大部分时间可以执行其他任务,只有数据到达时才处理串口通信。
实现复杂度方面:轮询模式简单易懂,代码容易编写和调试。中断模式相对复杂,需要配置中断控制器,编写中断服务函数,处理数据缓冲区等。
资源消耗方面:轮询模式不需要额外的内存资源。中断模式通常需要数据缓冲区,会消耗一定的内存空间。
在实际项目中,我一般这样选择:如果只是简单的调试信息输出,或者对实时性要求不高的应用,可以用轮询模式。如果是主控芯片需要处理多个任务,或者有高实时性要求的应用,一定要用中断模式。
4.2 适用场景与选择建议
根据我的经验,以下几种情况适合使用轮询模式:
- 简单的调试信息输出
- 数据传输量小,间隔时间长
- 系统资源极度紧张,没有足够内存用于缓冲区
- 对实时性要求不高的应用
以下几种情况则必须使用中断模式:
- 需要同时处理多个外设或任务
- 数据传输量大,或者需要高实时性
- 需要接收不定长数据
- 系统功耗敏感,需要尽可能降低CPU利用率
我记得有一个智能家居的项目,STM32需要同时处理温湿度传感器、人体红外传感器、无线通信和串口调试。如果使用轮询模式,CPU根本忙不过来,最后采用了中断模式处理串口通信,系统才稳定运行。
5. 实际应用中的问题与解决方案
5.1 数据溢出与缓冲区管理
在实际项目中,最容易出现的问题就是数据溢出。当数据到达的速度快于处理速度时,就会发生数据丢失。
解决这个问题的关键是使用环形缓冲区(FIFO)。当数据到达时,先存入缓冲区;主程序再从缓冲区读取数据进行处理。这样即使短时间内有大量数据到达,也不会丢失。
下面是一个简单的环形缓冲区实现:
#define BUFFER_SIZE 256
typedef struct {
uint8_t buffer[BUFFER_SIZE];
uint16_t head;
uint16_t tail;
} RingBuffer;
RingBuffer rxBuffer = {0};
void Buffer_Init(void)
{
rxBuffer.head = 0;
rxBuffer.tail = 0;
}
uint8_t Buffer_Write(uint8_t data)
{
uint16_t next = (rxBuffer.head + 1) % BUFFER_SIZE;
if(next == rxBuffer.tail)
return 0; // 缓冲区已满
rxBuffer.buffer[rxBuffer.head] = data;
rxBuffer.head = next;
return 1;
}
uint8_t Buffer_Read(uint8_t *data)
{
if(rxBuffer.head == rxBuffer.tail)
return 0; // 缓冲区为空
*data = rxBuffer.buffer[rxBuffer.tail];
rxBuffer.tail = (rxBuffer.tail + 1) % BUFFER_SIZE;
return 1;
}
在中断服务函数中,我们将接收到的数据写入缓冲区:
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
uint8_t data = USART_ReceiveData(USART1);
Buffer_Write(data);
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
在主循环中,我们从缓冲区读取并处理数据:
while(1)
{
uint8_t data;
if(Buffer_Read(&data))
{
// 处理数据...
}
// 执行其他任务...
}
5.2 错误处理与稳定性提升
串口通信中常见的错误包括校验错误、噪声错误、溢出错误等。良好的错误处理机制是保证通信稳定性的关键。
STM32的USART提供了多种错误标志位:
- PE:校验错误
- FE:帧错误
- NE:噪声错误
- ORE:溢出错误
我们应该在中断服务函数中检查这些错误标志位,并采取相应的处理措施:
void USART1_IRQHandler(void)
{
// 处理接收中断
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
// 正常处理数据...
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
// 处理错误中断
if(USART_GetITStatus(USART1, USART_IT_PE) != RESET)
{
// 校验错误处理
USART_ClearITPendingBit(USART1, USART_IT_PE);
}
if(USART_GetITStatus(USART1, USART_IT_FE) != RESET)
{
// 帧错误处理
USART_ClearITPendingBit(USART1, USART_IT_FE);
}
if(USART_GetITStatus(USART1, USART_IT_ORE) != RESET)
{
// 溢出错误处理
USART_ClearITPendingBit(USART1, USART_IT_ORE);
}
}
除了硬件错误处理,我们还应该实现软件层面的容错机制,比如数据校验、超时重传等。特别是在工业控制等对可靠性要求高的场合,这些机制尤为重要。
6. 高级应用与性能优化技巧
6.1 DMA与USART的配合使用
对于高速数据传输,即使是中断模式也可能无法满足要求。这时候就需要使用DMA(直接存储器访问)来解放CPU。
DMA可以在不需要CPU干预的情况下,直接在内存和外设之间传输数据。对于USART来说,我们可以配置DMA来自动发送和接收数据。
配置DMA发送的步骤:
void USART_DMA_Config(void)
{
DMA_InitTypeDef DMA_InitStructure;
// 开启DMA时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
// 配置DMA通道
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)txBuffer;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
DMA_InitStructure.DMA_BufferSize = 0;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel4, &DMA_InitStructure);
// 使能USART的DMA发送
USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);
}
使用DMA发送数据:
void USART_SendDataByDMA(uint8_t *data, uint16_t length)
{
// 等待上一次DMA传输完成
while(DMA_GetFlagStatus(DMA1_FLAG_TC4) == RESET);
// 清除DMA标志位
DMA_ClearFlag(DMA1_FLAG_TC4);
// 配置DMA传输参数
DMA_SetCurrDataCounter(DMA1_Channel4, length);
DMA_SetMemoryBaseAddr(DMA1_Channel4, (uint32_t)data);
// 开启DMA传输
DMA_Cmd(DMA1_Channel4, ENABLE);
}
DMA接收的配置类似,只是方向改为从外设到内存。使用DMA可以极大地提高数据传输效率,特别是在需要传输大量数据的场合。
6.2 低功耗设计中的USART应用
在电池供电的设备中,功耗是需要重点考虑的因素。STM32的低功耗模式与USART的配合使用很有讲究。
STM32有多种低功耗模式,如睡眠模式、停止模式、待机模式等。在不同的低功耗模式下,USART的行为也不同。
在睡眠模式下,CPU停止工作,但外设包括USART仍然正常工作。这意味着USART可以继续接收数据,并在接收到数据时唤醒CPU。
配置USART在接收数据时唤醒CPU:
// 进入睡眠模式前配置
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
NVIC_EnableIRQ(USART1_IRQn);
// 进入睡眠模式
__WFI(); // 等待中断
在停止模式下,所有时钟都停止,USART也不能工作。如果需要在这种模式下保持串口通信,需要特殊处理。
一种常见的做法是使用LPUART(低功耗串口),这是专门为低功耗应用设计的串口模块,可以在停止模式下工作。如果没有LPUART,就需要定期唤醒系统检查是否有数据。
在实际项目中,我需要做一个无线传感器节点,电池供电要求工作一年以上。最后采用了STM32L系列的低功耗芯片,精心设计了USART的工作时序,最终实现了超低功耗的串口通信。
7. 调试技巧与常见问题排查
7.1 常用的调试方法
串口调试中最常用的工具就是串口调试助手。通过串口调试助手,我们可以监视串口数据,发送测试命令,验证通信协议。
除了通用的串口调试助手,我还经常使用逻辑分析仪。逻辑分析仪可以捕获串口波形,帮助我们分析时序问题、电平问题等硬件层面的问题。
在代码层面,我习惯使用printf重定向到串口输出调试信息:
int fputc(int ch, FILE *f)
{
USART_SendData(DEBUG_USART, (uint8_t)ch);
while(USART_GetFlagStatus(DEBUG_USART, USART_FLAG_TXE) == RESET);
return ch;
}
这样就可以直接使用printf输出调试信息了:
printf("系统启动完成,当前温度:%d℃\r\n", temperature);
7.2 常见问题与解决方案
在实际项目中,我遇到过很多串口通信的问题,这里分享几个常见的:
数据乱码问题:最常见的原因是波特率不匹配。确保发送端和接收端的波特率、数据位、停止位、校验位等参数完全一致。有时候虽然设置的值相同,但由于时钟源误差,实际波特率可能有偏差。
数据丢失问题:如果是偶尔丢失一两个字节,可能是处理速度跟不上接收速度。增加缓冲区大小或者提高处理速度。如果是大量数据丢失,可能是硬件问题,如电平转换芯片驱动能力不足。
通信距离短问题:串口通信距离一般不超过15米。如果需要长距离通信,可以使用RS-485转换芯片,通信距离可达千米以上。
抗干扰能力差问题:在工业环境中,干扰较大,容易导致通信错误。可以增加校验机制,使用屏蔽线,或者在软件层面增加错误重传机制。
我记得最折腾的一次是做一个工业控制器,串口通信总是偶尔出错。最后发现是电源纹波太大,影响了串口电平的稳定性。更换了电源滤波电容后问题就解决了。
串口通信虽然简单,但细节很多。实际项目中,需要根据具体需求选择合适的模式,设计合理的通信协议,处理好各种异常情况。希望我的这些经验能对大家有所帮助。

270

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



