1. 项目概述:STM32F103标准库USART串口通信中断实验
在嵌入式系统开发中,串口通信是最基础也最常用的外设接口之一。STM32F103系列MCU作为经典的Cortex-M3内核微控制器,其USART模块提供了灵活可靠的串行通信能力。本次实验将基于标准外设库(Standard Peripheral Library),实现USART的中断接收功能模拟。
这个实验特别适合刚接触STM32硬件编程的开发者,通过中断方式处理串口数据相比轮询方式能显著提高系统效率。当我们需要同时处理多个任务时,中断机制可以避免CPU长时间阻塞在数据等待状态。实际应用中,这种技术被广泛用于工业控制、智能家居、物联网设备等需要实时响应串口数据的场景。
2. 硬件与开发环境准备
2.1 硬件配置要求
进行本实验需要以下硬件设备:
- STM32F103C8T6最小系统板(蓝色药丸板)或兼容开发板
- USB转TTL串口模块(如CH340G、CP2102等)
- 杜邦线若干
- 可选:LED和电阻用于状态指示
硬件连接示意图:
code复制STM32F103 USB转TTL模块
PA9(TX) ---- RX
PA10(RX) ---- TX
GND ---- GND
注意:切勿将STM32的TX直接连接到串口模块的TX,这会导致通信失败。务必交叉连接TX和RX引脚。
2.2 软件工具链
开发环境需要以下软件组件:
- Keil MDK-ARM 5.x(或其他支持STM32的IDE如IAR、STM32CubeIDE)
- STM32F10x标准外设库(可从ST官网下载)
- 串口调试工具(如Putty、SecureCRT或串口助手)
标准外设库目录结构说明:
code复制Libraries
├── CMSIS // 内核相关文件
├── STM32F10x_StdPeriph_Driver // 外设驱动
│ ├── inc // 头文件
│ └── src // 源文件
└── Project
└── Template // 工程模板
3. USART基础配置详解
3.1 时钟与GPIO初始化
在STM32中,使用任何外设前都必须先使能其时钟。USART1挂载在APB2总线上,而GPIOA的时钟也由APB2控制。初始化代码如下:
c复制void USART1_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
// 使能USART1和GPIOA时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
// 配置USART1 TX(PA9)为复用推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置USART1 RX(PA10)为浮空输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// USART参数配置
USART_InitStructure.USART_BaudRate = 115200;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(USART1, &USART_InitStructure);
// 使能USART1
USART_Cmd(USART1, ENABLE);
}
3.2 波特率计算原理
STM32的USART波特率计算公式为:
code复制波特率 = fCK / (16 * USARTDIV)
其中fCK是USART模块的输入时钟频率(APB2时钟,通常为72MHz),USARTDIV是一个无符号定点数,存储在USART_BRR寄存器中。
例如115200波特率的计算:
code复制USARTDIV = 72000000 / (16 * 115200) = 39.0625
BRR寄存器值 = 整数部分(39) << 4 | 小数部分(0.0625*16=1)
= 0x0271
4. 中断接收实现
4.1 NVIC中断控制器配置
要使USART接收中断正常工作,必须配置嵌套向量中断控制器(NVIC):
c复制void NVIC_Configuration(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
// 配置USART1中断优先级
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);
// 使能USART1接收中断
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
}
4.2 中断服务函数实现
当USART接收到数据时,会触发RXNE(接收寄存器非空)中断,进入以下服务函数:
c复制// 接收缓冲区定义
#define BUF_SIZE 128
uint8_t RxBuffer[BUF_SIZE];
uint16_t RxCounter = 0;
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
// 读取接收到的数据
RxBuffer[RxCounter++] = USART_ReceiveData(USART1);
// 防止缓冲区溢出
if(RxCounter >= BUF_SIZE) {
RxCounter = 0;
}
// 可选:回显接收到的字符
USART_SendData(USART1, RxBuffer[RxCounter-1]);
while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);
}
// 错误中断处理
if(USART_GetITStatus(USART1, USART_IT_ORE) != RESET)
{
// 必须读取SR和DR寄存器来清除ORE标志
(void)USART1->SR;
(void)USART1->DR;
}
}
5. 数据发送功能实现
5.1 单字节发送函数
虽然中断实验主要关注接收,但通常也需要发送功能用于调试:
c复制void USART1_SendByte(uint8_t data)
{
USART_SendData(USART1, data);
// 等待发送完成
while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);
}
5.2 字符串发送函数
基于单字节发送,可以实现字符串发送功能:
c复制void USART1_SendString(char *str)
{
while(*str != '\0')
{
USART1_SendByte(*str++);
}
}
6. 常见问题与调试技巧
6.1 通信不稳定或乱码
可能原因及解决方案:
- 波特率不匹配:检查双方设备波特率设置是否一致,确认时钟配置正确
- 硬件连接错误:确认TX-RX交叉连接,共地良好
- 信号干扰:缩短连接线长度,增加适当的滤波电容
6.2 中断不触发
排查步骤:
- 确认NVIC和USART中断已正确使能
- 检查中断优先级配置是否合理
- 在调试器中查看USART_SR寄存器状态
- 确保USART时钟和GPIO时钟已使能
6.3 缓冲区溢出处理
在实际应用中,应考虑以下策略:
- 使用环形缓冲区代替线性数组
- 实现流控机制(硬件或软件)
- 设置接收超时中断(IDLE中断)
c复制// 环形缓冲区实现示例
typedef struct {
uint8_t buffer[BUF_SIZE];
uint16_t head;
uint16_t tail;
} RingBuffer;
RingBuffer rxBuf = {0};
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
uint16_t next = (rxBuf.head + 1) % BUF_SIZE;
if(next != rxBuf.tail) { // 缓冲区未满
rxBuf.buffer[rxBuf.head] = USART_ReceiveData(USART1);
rxBuf.head = next;
}
}
}
7. 实验进阶与扩展
7.1 DMA传输结合
对于高速数据通信,可以使用DMA减轻CPU负担:
c复制void USART1_DMA_Config(void)
{
DMA_InitTypeDef DMA_InitStructure;
// 使能DMA时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
// 配置DMA通道4(USART1_TX)
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 = BUF_SIZE;
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);
// 使能USART1 DMA发送请求
USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);
}
7.2 多串口管理
当系统需要多个串口时,可采用以下设计模式:
c复制typedef struct {
USART_TypeDef* USARTx;
uint8_t rxBuffer[BUF_SIZE];
uint16_t rxIndex;
void (*callback)(uint8_t data);
} UART_Handle;
UART_Handle uart1, uart2;
void UART_Init(UART_Handle *huart, USART_TypeDef* USARTx, void (*cb)(uint8_t))
{
huart->USARTx = USARTx;
huart->rxIndex = 0;
huart->callback = cb;
}
// 统一的中断处理
void USARTx_IRQHandler(UART_Handle *huart)
{
if(USART_GetITStatus(huart->USARTx, USART_IT_RXNE) != RESET)
{
uint8_t data = USART_ReceiveData(huart->USARTx);
if(huart->callback != NULL) {
huart->callback(data);
}
}
}
7.3 协议解析框架
在实际项目中,通常需要实现通信协议解析:
c复制typedef enum {
STATE_IDLE,
STATE_HEADER,
STATE_LENGTH,
STATE_DATA,
STATE_CHECKSUM
} ParserState;
typedef struct {
ParserState state;
uint8_t buffer[256];
uint8_t index;
uint8_t length;
void (*packetHandler)(uint8_t *data, uint8_t len);
} ProtocolParser;
void ParseByte(ProtocolParser *parser, uint8_t data)
{
switch(parser->state) {
case STATE_IDLE:
if(data == 0xAA) { // 帧头
parser->state = STATE_HEADER;
parser->index = 0;
}
break;
case STATE_HEADER:
if(data == 0x55) { // 第二个帧头
parser->state = STATE_LENGTH;
} else {
parser->state = STATE_IDLE;
}
break;
case STATE_LENGTH:
parser->length = data;
parser->state = STATE_DATA;
break;
case STATE_DATA:
parser->buffer[parser->index++] = data;
if(parser->index >= parser->length) {
parser->state = STATE_CHECKSUM;
}
break;
case STATE_CHECKSUM:
// 校验处理...
if(parser->packetHandler) {
parser->packetHandler(parser->buffer, parser->length);
}
parser->state = STATE_IDLE;
break;
}
}
8. 性能优化与最佳实践
8.1 中断处理优化原则
- 保持ISR尽可能简短
- 避免在ISR中调用复杂函数或进行浮点运算
- 使用标志位将数据处理移至主循环
- 合理设置中断优先级
8.2 电源管理考虑
在低功耗应用中:
- 在不使用USART时关闭其时钟
- 利用USART唤醒功能从低功耗模式恢复
- 适当降低USART时钟频率
c复制// 进入低功耗模式前
USART_Cmd(USART1, DISABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, DISABLE);
// 唤醒后重新初始化
USART_Cmd(USART1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
8.3 错误处理机制
完善的错误处理应包括:
- 帧错误检测
- 噪声错误处理
- 溢出错误恢复
- 超时机制
c复制void USART1_IRQHandler(void)
{
// 检查各种错误标志
uint32_t errorFlags = USART1->SR & (USART_FLAG_ORE | USART_FLAG_NE | USART_FLAG_FE | USART_FLAG_PE);
if(errorFlags) {
// 记录错误日志
logError(errorFlags);
// 清除错误标志
(void)USART1->SR;
(void)USART1->DR;
}
// 正常数据处理...
}
在实际项目开发中,我发现合理使用DMA+中断的组合方式能显著提高系统效率。对于115200及以下波特率,纯中断方式通常足够;但当波特率达到1Mbps或更高时,DMA几乎是必须的选择。另外,在实现协议解析时,状态机设计比简单的字符匹配更加可靠和灵活。
