在嵌入式开发中,串口通信是最基础也最常用的外设功能之一。STM32的标准外设库(Standard Peripheral Library)虽然已经被HAL库取代,但在许多老项目中依然广泛使用。这套库提供了对硬件寄存器的封装,让开发者能够以更直观的方式操作外设。
我最近在调试一个基于STM32F103的工业控制器项目,需要实现设备与上位机的稳定通信。经过多次实践,总结出一套可靠的串口初始化与通信方案。下面将完整分享从硬件配置到软件实现的全部细节,包括容易踩坑的注意事项。
串口通信的第一步是正确配置硬件。STM32的USART外设需要APB总线时钟和对应GPIO端口的时钟支持。以USART1为例,它挂在APB2总线上,使用PA9(TX)和PA10(RX)引脚:
c复制RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
这里有个关键点:不同USART挂在不同的APB总线上。USART1在APB2,而USART2/3在APB1。如果错误地开启时钟,会导致无法通信。
GPIO模式配置需要特别注意:
实际项目中遇到过因GPIO模式配置错误导致通信失败的情况。特别是某些开发板可能默认配置了上拉/下拉电阻,如果与外部设备不匹配,会导致信号电平异常。
USART_InitTypeDef结构体包含了串口通信的核心参数:
c复制USART_InitStruct.USART_BaudRate = 115200; // 常用波特率
USART_InitStruct.USART_WordLength = USART_WordLength_8b; // 8位数据
USART_InitStruct.USART_StopBits = USART_StopBits_1; // 1位停止位
USART_InitStruct.USART_Parity = USART_Parity_No; // 无校验
USART_InitStruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; // 全双工
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; // 无流控
波特率计算是串口配置的关键。STM32使用分数波特率发生器,实际波特率计算公式为:
code复制Tx/Rx波特率 = fCK / (16 * USARTDIV)
其中fCK是USART时钟频率,USARTDIV是一个存储在USART_BRR寄存器中的无符号定点数。标准库的USART_Init()函数会自动计算并设置这个值。
STM32使用嵌套向量中断控制器(NVIC)管理中断优先级。建议在项目初期统一设置优先级分组:
c复制NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 2位抢占优先级,2位响应优先级
对于USART接收中断的配置:
c复制NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 2; // 抢占优先级
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1; // 响应优先级
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStruct);
在资源紧张的项目中,需要合理分配中断优先级。串口通信通常不需要最高优先级,但要确保不会因长时间阻塞而丢失数据。
基本的中断服务函数框架如下:
c复制void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
uint8_t data = USART_ReceiveData(USART1);
// 处理接收数据
}
}
关键点:
最基本的字节发送函数:
c复制void Serial_SendByte(uint8_t Byte)
{
USART_SendData(USART1, Byte);
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}
这里等待的是TXE(发送数据寄存器空)标志,而不是TC(发送完成)标志。两者的区别在于:
基于字节发送函数,可以构建更高级的发送功能:
c复制// 发送数组
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
for(uint16_t i=0; i<Length; i++)
{
Serial_SendByte(Array[i]);
}
}
// 发送字符串
void Serial_SendString(char *String)
{
while(*String != '\0')
{
Serial_SendByte(*String++);
}
}
// 带换行的字符串发送
void Serial_SendStringLine(char *String)
{
Serial_SendString(String);
Serial_SendString("\r\n"); // Windows换行格式
}
在实际项目中,建议为字符串发送函数添加长度限制,防止意外越界。我曾经遇到过因字符串未正确终止导致的内存越界问题。
简单的回显测试可以直接在中断中发送接收到的数据,但实际项目需要更健壮的方案:
c复制#define RX_BUF_SIZE 256
typedef struct {
uint8_t buffer[RX_BUF_SIZE];
uint16_t head;
uint16_t tail;
} RingBuffer;
RingBuffer rxBuf = {0};
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1, USART_IT_RXNE))
{
rxBuf.buffer[rxBuf.head++] = USART_ReceiveData(USART1);
rxBuf.head %= RX_BUF_SIZE;
}
}
uint8_t Serial_ReadByte(void)
{
if(rxBuf.head != rxBuf.tail)
{
uint8_t data = rxBuf.buffer[rxBuf.tail++];
rxBuf.tail %= RX_BUF_SIZE;
return data;
}
return 0;
}
这种环形缓冲区设计可以有效处理突发数据,避免丢失。
STM32的波特率发生器在某些频率下会产生较大误差。以72MHz系统时钟为例:
| 目标波特率 | 实际波特率 | 误差率 |
|---|---|---|
| 9600 | 9599.67 | 0.003% |
| 115200 | 115108.70 | 0.079% |
| 230400 | 229885.71 | 0.223% |
| 460800 | 458333.33 | 0.536% |
当误差超过3%时,通信可能不稳定。可以通过调整系统时钟或使用高级波特率计算方法改善。
对于高速或大数据量传输,建议使用DMA:
c复制// 发送DMA配置示例
DMA_InitTypeDef DMA_InitStructure;
DMA_DeInit(DMA1_Channel4);
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 = bufferSize;
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_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);
DMA_Cmd(DMA1_Channel4, ENABLE);
DMA传输可以大幅降低CPU负载,特别是在需要同时处理多个任务的系统中。
检查步骤:
可能原因:
典型检查点:
在调试串口通信时,我习惯先实现回显测试(发送什么就返回什么),这是验证基本功能是否正常的最快方法。当遇到复杂问题时,可以逐步简化代码,定位问题根源。