【八】UART1串口通信与独立按键中断
1. 项目概述
本项目演示如何使用 STM32F103C8T6 微控制器的 UART1 串口进行数据通信,并结合独立按键中断功能,实现按键状态通过串口输出的功能。系统支持串口数据收发、printf重定向、按键中断检测,并通过LED指示灯显示按键状态。
技术要点
- ✅ UART1串口通信(115200波特率)
- ✅ 串口发送/接收功能
- ✅ 串口中断接收
- ✅ printf/scanf重定向
- ✅ EXTI外部中断(按键检测)
- ✅ NVIC中断优先级配置
- ✅ LED状态指示
- ✅ 多中断协同工作
应用场景
- 📡 串口调试与通信
- 📡 按键状态远程监控
- 📡 人机交互界面
- 📡 数据采集与传输
- 📡 远程控制系统
- 📡 传感器数据上报
2. 硬件平台
主控芯片
- 型号: STM32F103C8T6
- 内核: ARM Cortex-M3
- 主频: 72 MHz
- Flash: 64 KB
- SRAM: 20 KB
- UART: 3个(USART1/2/3)
外设资源
UART1串口
| 功能 | 引脚 | 说明 |
|---|---|---|
| UART1_TX | PA9 | 发送引脚(复用推挽输出) |
| UART1_RX | PA10 | 接收引脚(浮空输入) |
| 波特率 | 115200 | 标准波特率 |
| 数据位 | 8位 | 无奇偶校验 |
| 停止位 | 1位 | 标准配置 |
独立按键(4个)
| 按键 | 引脚 | 中断线 | 中断通道 | 控制LED | 串口输出 |
|---|---|---|---|---|---|
| KEY1 | PB8 | EXTI8 | EXTI9_5 | LED5 | "Press KEY1_PIN" |
| KEY2 | PB9 | EXTI9 | EXTI9_5 | LED6 | "Press KEY2_PIN" |
| KEY3 | PB10 | EXTI10 | EXTI15_10 | LED7 | "Press KEY3_PIN" |
| KEY4 | PB11 | EXTI11 | EXTI15_10 | LED8 | "Press KEY4_PIN" |
LED指示灯(4个)
| LED | 引脚 | 说明 |
|---|---|---|
| LED5 | PB4 | KEY1控制 |
| LED6 | PB5 | KEY2控制 |
| LED7 | PB6 | KEY3控制 |
| LED8 | PB7 | KEY4控制 |
硬件连接
STM32F103C8T6 串口模块/USB转串口
PA9 (TX) ----------> RX
PA10 (RX) <---------- TX
GND ----------> GND
STM32F103C8T6 独立按键
PB8 <---------- KEY1 (按下接GND)
PB9 <---------- KEY2 (按下接GND)
PB10 <---------- KEY3 (按下接GND)
PB11 <---------- KEY4 (按下接GND)
STM32F103C8T6 LED指示灯
PB4-PB7 ----------> LED5-LED8 (高电平点亮)
串口通信说明:
UART通信(全双工):
STM32 TX (PA9) ──────→ PC RX (数据发送)
STM32 RX (PA10) ←────── PC TX (数据接收)
电平标准:
- 逻辑1(高电平):3.3V
- 逻辑0(低电平):0V
- 空闲状态:高电平
波特率:115200 bps
- 每秒传输115200位
- 每位时间:8.68μs
3. 项目结构
5B_UART1_独立按键(中断)/
├── Libraries/ # STM32标准外设库
│ ├── CMSIS/ # Cortex微控制器软件接口标准
│ └── STM32F10x_StdPeriph_Driver/ # STM32F10x标准外设驱动
├── MDK/ # Keil工程文件
│ ├── TEST_CODE.uvproj # Keil项目文件
│ ├── TEST_CODE.uvopt # 项目配置选项
│ ├── Output/ # 编译输出目录
│ └── list/ # 列表文件目录
├── Readme/ # 说明文档目录
└── User/ # 用户代码目录
├── main.c # 主程序入口
├── TF_UART1.c # UART1驱动实现
├── TF_UART1.h # UART1驱动头文件
├── TF_KeyBoard.c # 按键中断驱动实现
├── TF_KeyBoard.h # 按键中断驱动头文件
├── TF_LED.c # LED驱动实现
├── TF_LED.h # LED驱动头文件
├── TF_System.c # 系统配置(时钟、NVIC)
├── TF_System.h # 系统配置头文件
├── TF_Delay.c # 延时函数实现
└── TF_Delay.h # 延时函数头文件
4. 功能说明
系统工作流程
系统上电
↓
NVIC中断优先级配置
(USART1=0, EXTI9_5=3, EXTI15_10=4)
↓
UART1初始化
(115200, 8N1, 使能RX中断)
↓
按键中断初始化
(PB8-PB11, 下降沿触发)
↓
LED初始化
(PB4-PB7, 推挽输出)
↓
串口发送启动信息
"USART1_Configuration"
↓
主循环空转
(while(1);)
↓
等待中断事件
├─ 按键中断
│ ├─ 清除中断标志
│ ├─ 延时消抖(5ms)
│ ├─ 确认按键状态
│ ├─ 关闭所有LED
│ ├─ 点亮对应LED
│ ├─ 串口输出按键信息
│ └─ 等待按键释放
│
└─ 串口接收中断
├─ 清除中断标志
├─ 读取接收数据
├─ 回显接收数据
└─ 返回主循环
功能特点
-
UART1串口通信:
- 115200波特率
- 8位数据位
- 无奇偶校验
- 1位停止位
- 支持中断接收
- 支持回显功能
-
按键中断检测:
- 4个独立按键
- EXTI外部中断
- 下降沿触发
- 软件消抖5ms
- LED状态指示
-
串口输出按键状态:
- KEY1 → "Press KEY1_PIN"
- KEY2 → "Press KEY2_PIN"
- KEY3 → "Press KEY3_PIN"
- KEY4 → "Press KEY4_PIN"
-
printf/scanf重定向:
- 支持printf输出到串口
- 支持scanf从串口输入
- 方便调试和交互
-
多中断协同:
- USART1中断(优先级0)
- EXTI9_5中断(优先级3)
- EXTI15_10中断(优先级4)
- 高优先级可打断低优先级
中断优先级配置
中断优先级(数字越小优先级越高):
┌─────────────────────────────────────┐
│ USART1_IRQn → 抢占优先级0 │ ← 最高
│ EXTI9_5_IRQn → 抢占优先级3 │
│ EXTI15_10_IRQn → 抢占优先级4 │ ← 最低
└─────────────────────────────────────┘
中断嵌套场景:
- USART1中断可以打断按键中断
- 按键中断不能打断USART1中断
- EXTI9_5可以打断EXTI15_10
5. UART串口通信原理
UART通信框架
UART异步串行通信:
┌─────────────────────────────────────────┐
│ UART数据帧格式 │
│ │
│ 起始位 数据位(8) 奇偶位 停止位 │
│ 0 D0~D7 无 1 │
│ │ ├──8位─┤ │ │ │
│ 1位 数据 可选 1或2位 │
│ │
│ 示例:发送字符'A' (ASCII: 0x41) │
│ │
│ 空闲 起始 D0 D1 D2 D3 D4 D5 D6 D7 停止 空闲 │
│ 1 0 1 0 0 0 0 0 1 0 1 1 │
│ └───────── 0x41 ─────────┘ │
└─────────────────────────────────────────┘
时序图:
空闲
│
├──────┐
│ │ 起始位(0)
│ └──┐
│ │ D0(1)
│ ├──┐
│ │ │ D1(0)
│ │ └──┐
│ │ │ D2(0)
│ │ └──┐
│ │ │ ...
│ │ └──┐
│ │ │ D7(0)
│ │ ├──┐
│ │ │ │ 停止位(1)
│ │ │ ├──────
│ │ │ │
└─────────┴───────────┴──┘
波特率:115200 bps
- 每位时间 = 1/115200 ≈ 8.68μs
- 发送1字节(10位) ≈ 86.8μs
UART硬件结构
STM32 UART1硬件框图:
┌─────────────────────────────────────────────┐
│ UART1 外设 │
│ │
│ 发送部分: │
│ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ TDR │→ │移位寄存器│→│ TX引脚│→ PA9 │
│ │发送缓冲│ │ (8位) │ │(PA9) │ │
│ └──────┘ └──────┘ └──────┘ │
│ ↑ ↓ │
│ 写入数据 TXE标志位 │
│ │
│ 接收部分: │
│ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ RDR │← │移位寄存器│←│ RX引脚│← PA10 │
│ │接收缓冲│ │ (8位) │ │(PA10)│ │
│ └──────┘ └──────┘ └──────┘ │
│ ↓ ↑ │
│ 读取数据 RXNE标志位 │
│ │
│ 中断控制: │
│ ┌──────┐ │
│ │RXNE中断│→ NVIC → CPU │
│ │TXE中断 │ │
│ └──────┘ │
└─────────────────────────────────────────────┘
UART配置参数
UART1配置:
┌────────────────┬──────────┬────────────┐
│ 参数 │ 值 │ 说明 │
├────────────────┼──────────┼────────────┤
│ 波特率 │ 115200 │ 标准速率 │
│ 数据位 │ 8位 │ 标准配置 │
│ 停止位 │ 1位 │ 标准配置 │
│ 奇偶校验 │ 无 │ 无校验 │
│ 硬件流控 │ 无 │ 不使用 │
│ 工作模式 │ 全双工 │ 收发同时 │
│ TX引脚 │ PA9 │ 复用推挽 │
│ RX引脚 │ PA10 │ 浮空输入 │
│ 中断 │ RXNE使能 │ 接收中断 │
└────────────────┴──────────┴────────────┘
波特率计算:
波特率 = fPCLK2 / (16 × USARTDIV)
115200 = 72MHz / (16 × USARTDIV)
USARTDIV = 72000000 / (16 × 115200) = 39.0625
6. 代码详解
6.1 主程序 (main.c)
完整代码
#include "TF_System.h"
#include "TF_UART1.h"
#include "TF_KeyBoard.h"
#include "TF_LED.h"
int main(void)
{
NVIC_Configuration(); // NVIC中断优先级配置
USART1_Configuration(); // UART1初始化
GPIO_Key_Init(); // 按键中断初始化
LED_Init(); // LED初始化
USART1_Puts("USART1_Configuration\r\n"); // 发送启动信息
while(1)
{
; // 主循环空转,等待中断
}
}
主函数要点:
-
初始化顺序:
NVIC_Configuration(); // 先配置中断优先级
USART1_Configuration(); // 再初始化UART1
GPIO_Key_Init(); // 初始化按键中断
LED_Init(); // 初始化LED -
启动信息:
USART1_Puts("USART1_Configuration\r\n");
作用:
- 确认串口工作正常
- 用于调试和验证 -
主循环:
while(1); // 空转,等待中断
特点:
- 所有功能由中断驱动
- CPU处于空闲状态
- 低功耗
6.2 UART1驱动 (TF_UART1)
TF_UART1.h - 头文件
#ifndef __STM32_UART1_H__
#define __STM32_UART1_H__
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"
#include "stm32f10x_usart.h"
#include <stdio.h>
// 函数声明
extern void USART1_Configuration(void);
extern void USART1_Send_Byte(u16 Data);
extern void USART1_Puts(char *str);
#endif
UART1初始化
void USART1_Configuration(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
//------------------------------------------------------------------------------
// 步骤1:使能端口时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 使能GPIOA时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); // 使能USART1时钟
//------------------------------------------------------------------------------
// 步骤2:配置USART1发送IO(PA9 - TX)
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接收IO(PA10 - RX)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure);
//------------------------------------------------------------------------------
// 步骤3:配置串口参数
USART_InitStructure.USART_BaudRate = 115200; // 波特率115200
USART_InitStructure.USART_WordLength = USART_WordLength_8b; // 8位数据
USART_InitStructure.USART_StopBits = USART_StopBits_1; // 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);
//------------------------------------------------------------------------------
// 步骤4:使能USART1
USART_Cmd(USART1, ENABLE);
//------------------------------------------------------------------------------
// 步骤5:配置串口中断功能
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); // 使能接收中断
}
初始化要点:
-
GPIO配置:
// TX引脚(PA9)
GPIO_Mode_AF_PP // 复用推挽输出
特点:
- 复用功能模式
- 由UART外设控制
- 推挽输出驱动能力强
// RX引脚(PA10)
GPIO_Mode_IN_FLOATING // 浮空输入
特点:
- 高阻态输入
- 不需要上拉/下拉
- 由外部信号驱动 -
波特率配置:
USART_BaudRate = 115200
说明:
- 标准波特率
- 每秒115200位
- 适合大多数应用
- 误差率低 -
中断配置:
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
作用:
- 使能接收中断
- 接收到数据时触发
- RXNE标志位置位
串口发送函数
// 发送单个字节
void USART1_Send_Byte(u16 Data)
{
while (!(USART1->SR & USART_FLAG_TXE)); // 等待发送缓冲区空
USART1->DR = (Data & (uint16_t)0x01FF); // 写入数据寄存器
}
// 发送字符串
void USART1_Puts(char *str)
{
while(*str)
{
USART_SendData(USART1, *str++);
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}
}
发送函数要点:
-
发送单字节:
while (!(USART1->SR & USART_FLAG_TXE));
作用:
- 等待TXE标志位置位
- TXE=1表示发送缓冲区空
- 可以写入新数据 -
发送字符串:
while(*str)
{
USART_SendData(USART1, *str++);
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}
流程:
- 逐字节发送
- 等待每个字节发送完成
- 直到字符串结束符'\0'
串口接收中断
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
USART_ClearFlag(USART1, USART_FLAG_RXNE); // 清除标志位
Uart_RevBuffer = USART1->DR; // 读取接收数据
USART1_Send_Byte(Uart_RevBuffer); // 回显数据
}
}
接收中断要点:
-
中断标志检查:
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
作用:
- 检查RXNE标志位
- RXNE=1表示接收到数据 -
读取数据:
Uart_RevBuffer = USART1->DR;
说明:
- 读取数据寄存器
- 自动清除RXNE标志位 -
回显功能:
USART1_Send_Byte(Uart_RevBuffer);
作用:
- 将接收的数据发送回去
- 用于串口调试
- 验证通信正常
6.3 按键中断驱动 (TF_KeyBoard)
按键初始化
void GPIO_Key_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
EXTI_InitTypeDef EXTI_InitStructure;
// 使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
// 配置GPIO为上拉输入
GPIO_InitStructure.GPIO_Pin = KEY1_PIN | KEY2_PIN | KEY3_PIN | KEY4_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
// 配置EXTI线映射
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource8);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource9);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource10);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource11);
// 配置EXTI参数
EXTI_InitStructure.EXTI_Line = EXTI_Line8 | EXTI_Line9 | EXTI_Line10 | EXTI_Line11;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
}
按键中断服务函数
void EXTI9_5_IRQHandler(void)
{
// 处理KEY1
if(EXTI_GetITStatus(EXTI_Line8) != RESET)
{
EXTI_ClearITPendingBit(EXTI_Line8); // 清除中断标志
Delay_nms(5); // 消抖延时
if(GPIO_ReadInputDataBit(KEY1_GPIO, KEY1_PIN) == 0)
{
LED_Off(10); // 关闭所有LED
LED_On(5); // 点亮LED5
USART1_Puts("Press KEY1_PIN\r\n"); // 串口输出
}
while(GPIO_ReadInputDataBit(KEY1_GPIO, KEY1_PIN) == 0); // 等待释放
}
// 处理KEY2
if(EXTI_GetITStatus(EXTI_Line9) != RESET)
{
EXTI_ClearITPendingBit(EXTI_Line9);
Delay_nms(5);
if(GPIO_ReadInputDataBit(KEY2_GPIO, KEY2_PIN) == 0)
{
LED_Off(10);
LED_On(6);
USART1_Puts("Press KEY2_PIN\r\n");
}
while(GPIO_ReadInputDataBit(KEY2_GPIO, KEY2_PIN) == 0);
}
}
void EXTI15_10_IRQHandler(void)
{
// 处理KEY3
if(EXTI_GetITStatus(EXTI_Line10) != RESET)
{
EXTI_ClearITPendingBit(EXTI_Line10);
Delay_nms(5);
if(GPIO_ReadInputDataBit(KEY3_GPIO, KEY3_PIN) == 0)
{
LED_Off(10);
LED_On(7);
USART1_Puts("Press KEY3_PIN\r\n");
}
while(GPIO_ReadInputDataBit(KEY3_GPIO, KEY3_PIN) == 0);
}
// 处理KEY4
if(EXTI_GetITStatus(EXTI_Line11) != RESET)
{
EXTI_ClearITPendingBit(EXTI_Line11);
Delay_nms(5);
if(GPIO_ReadInputDataBit(KEY4_GPIO, KEY4_PIN) == 0)
{
LED_Off(10);
LED_On(8);
USART1_Puts("Press KEY4_PIN\r\n");
}
while(GPIO_ReadInputDataBit(KEY4_GPIO, KEY4_PIN) == 0);
}
}
按键中断特点:
-
串口输出按键状态:
USART1_Puts("Press KEY1_PIN\r\n");
作用:
- 通过串口报告按键状态
- 远程监控按键操作
- 便于调试和验证 -
LED + 串口双重指示:
LED_On(5); // 本地LED指示
USART1_Puts("Press KEY1_PIN\r\n"); // 远程串口报告
优点:
- 本地可见(LED)
- 远程可知(串口)
- 双重确认
6.4 系统配置 (TF_System)
NVIC中断优先级配置
void NVIC_Configuration(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
// 设置中断向量表位置
NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x0);
// 设置中断优先级分组
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
// 配置USART2中断(代码中写的是USART2,但实际使用USART1)
NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; // 最高优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
// 配置EXTI9_5中断
NVIC_InitStructure.NVIC_IRQChannel = EXTI9_5_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
// 配置EXTI15_10中断
NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 4; // 最低优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
NVIC配置要点:
-
优先级分组:
NVIC_PriorityGroup_4 // 4位抢占优先级,0位响应优先级
说明:
- 抢占优先级:0-15(16级)
- 响应优先级:无
- 数字越小优先级越高 -
中断优先级分配:
USART2_IRQn → 0(最高,可打断按键中断)
EXTI9_5_IRQn → 3(中等)
EXTI15_10_IRQn → 4(最低)
设计理由:
- 串口数据重要,优先级最高
- 避免丢失数据
- 按键可以稍等
7. UART通信协议详解
UART数据帧结构
标准UART数据帧(8N1配置):
┌─────┬────┬────┬────┬────┬────┬────┬────┬────┬─────┬─────┐
│空闲 │起始│ D0 │ D1 │ D2 │ D3 │ D4 │ D5 │ D6 │ D7 │停止 │空闲│
│ 1 │ 0 │ b0 │ b1 │ b2 │ b3 │ b4 │ b5 │ b6 │ b7 │ 1 │ 1 │
└─────┴────┴────┴────┴────┴────┴────┴────┴────┴─────┴─────┘
↑ ↑
下降沿触发 上升沿
开始采样 帧结束
数据位说明:
- D0-D7:8位数据(LSB先发送)
- 起始位:1位(固定为0)
- 停止位:1位(固定为1)
- 无奇偶校验位
示例:发送字符'A' (ASCII: 0x41 = 0b01000001)
空闲→起始→1→0→0→0→0→0→1→0→停止→空闲
1 0 1 0 0 0 0 0 1 0 1 1
波特率与时序
波特率:115200 bps(每秒115200位)
┌────────────────────────────────────────┐
│ 每位时间 = 1/115200 ≈ 8.68 μs │
│ 每帧时间 = 10位 × 8.68μs ≈ 86.8 μs │
│ 每秒传输 = 115200/10 = 11520 字节 │
│ ≈ 11.25 KB/s │
└────────────────────────────────────────┘
常用波特率:
┌──────────┬───────────┬──────────────┐
│ 波特率 │ 每位时间 │ 应用场景 │
├──────────┼───────────┼──────────────┤
│ 9600 │ 104.2μs │ 低速设备 │
│ 115200 │ 8.68μs │ 标准速率 │← 本项目
│ 230400 │ 4.34μs │ 高速通信 │
│ 460800 │ 2.17μs │ 快速传输 │
│ 921600 │ 1.09μs │ 最高速率 │
└──────────┴───────────┴──────────────┘
UART发送流程
发送数据流程:
1. 检查TXE标志位
↓
2. TXE=1?
├─ 否 → 等待
└─ 是 → 继续
↓
3. 写入数据到DR
↓
4. 硬件自动发送
├─ 起始位
├─ 数据位(D0-D7)
└─ 停止位
↓
5. 发送完成
├─ TXE=1(可写入新数据)
└─ TC=1(传输完成)
代码实现:
while(!(USART1->SR & USART_FLAG_TXE)); // 等待TXE=1
USART1->DR = data; // 写入数据
UART接收流程
接收数据流程:
1. 检测起始位(下降沿)
↓
2. 延时半个位时间(采样点居中)
↓
3. 采样数据位D0-D7
↓
4. 校验停止位(应为1)
↓
5. 数据存入RDR
├─ RXNE=1(有数据)
└─ 触发中断(如果使能)
↓
6. 读取数据
├─ data = USART1->DR
└─ RXNE自动清零
代码实现(中断方式):
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1, USART_IT_RXNE))
{
data = USART1->DR; // 读取自动清RXNE
}
}
8. printf重定向实现
fputc函数重定向
// 重定向fputc函数,使printf输出到UART1
int fputc(int ch, FILE *f)
{
while((USART1->SR & 0X40) == 0); // 等待TXE标志位
USART1->DR = (u8)ch; // 发送字符
return ch;
}
工作原理:
printf工作流程:
printf("Hello")
↓
调用fputc('H', stdout)
调用fputc('e', stdout)
调用fputc('l', stdout)
调用fputc('l', stdout)
调用fputc('o', stdout)
↓
每次fputc
├─ 等待TXE=1
├─ 写入USART1->DR
└─ 字符通过串口发送
使用示例:
printf("Key1 Pressed!\r\n");
printf("Value: %d\r\n", value);
fgetc函数重定向
// 重定向fgetc函数,使scanf从UART1输入
int fgetc(FILE *f)
{
while(!(USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == SET))
{
// 等待接收数据
}
return (USART_ReceiveData(USART1));
}
工作原理:
scanf工作流程:
scanf("%d", &num)
↓
循环调用fgetc(stdin)
├─ 等待RXNE=1
├─ 读取USART1->DR
└─ 返回接收到的字符
↓
解析输入,存入变量
使用示例:
scanf("%d", &value);
scanf("%s", str);
使用注意事项
1. 需要包含stdio.h头文件
#include <stdio.h>
2. 需要在Keil中配置
Options → Target → 取消勾选 "Use MicroLIB"
3. 添加宏定义(有些工程需要)
#ifdef __GNUC__
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif
4. 避免在中断中使用printf
// ❌ 错误示例
void EXTI9_5_IRQHandler(void)
{
printf("Key pressed\r\n"); // 可能导致问题
}
// ✅ 正确示例
void EXTI9_5_IRQHandler(void)
{
USART1_Puts("Key pressed\r\n"); // 使用自定义函数
}
9. 使用说明
9.1 硬件连接
连接步骤
-
连接串口模块
STM32 USB转串口模块
PA9 (TX) → RX
PA10 (RX) ← TX
GND → GND
注意:
- TX接RX,RX接TX(交叉连接)
- 共地连接
- 电平匹配(3.3V) -
连接按键
KEY1-KEY4 → PB8-PB11(按下接GND) -
连接LED
LED5-LED8 → PB4-PB7(高电平点亮)
9.2 软件配置
串口调试助手设置
串口参数配置:
┌────────────┬──────────┐
│ 波特率 │ 115200 │
│ 数据位 │ 8 │
│ 停止位 │ 1 │
│ 奇偶校验 │ 无 │
│ 流控 │ 无 │
│ 发送格式 │ ASCII │
│ 接收格式 │ ASCII │
└────────────┴──────────┘
推荐软件:
- 串口调试助手
- SecureCRT
- PuTTY
- Tera Term
- XCOM
9.3 运行效果
预期现象:
-
上电启动
串口输出:
USART1_Configuration -
按键KEY1
LED5点亮,其他LED熄灭
串口输出:
Press KEY1_PIN -
按键KEY2
LED6点亮,其他LED熄灭
串口输出:
Press KEY2_PIN -
串口回显
PC发送:A
STM32回显:A
PC发送:Hello
STM32回显:Hello
9.4 测试方法
测试1:串口通信
步骤:
1. 打开串口调试助手
2. 配置串口参数(115200, 8, N, 1)
3. 连接串口
4. STM32上电复位
5. 观察是否收到"USART1_Configuration"
预期结果:
- 收到启动信息
- 发送字符可回显
测试2:按键功能
步骤:
1. 依次按下KEY1-KEY4
2. 观察LED状态
3. 观察串口输出
预期结果:
- KEY1 → LED5点亮,串口输出"Press KEY1_PIN"
- KEY2 → LED6点亮,串口输出"Press KEY2_PIN"
- KEY3 → LED7点亮,串口输出"Press KEY3_PIN"
- KEY4 → LED8点亮,串口输出"Press KEY4_PIN"
测试3:中断优先级
// 修改代码测试中断嵌套
void EXTI9_5_IRQHandler(void)
{
USART1_Puts("Key Start\r\n");
Delay_nms(3000); // 延时3秒
USART1_Puts("Key End\r\n");
}
测试:
1. 按下KEY1
2. 在3秒延时内通过串口发送数据
3. 观察是否能立即收到回显
预期结果:
- USART中断优先级高,能打断按键中断
- 串口数据立即回显
- 按键中断被打断后继续执行
10. 常见问题
问题1:串口无输出
可能原因:
- ❌ 串口参数配置错误
- ❌ TX/RX引脚接反
- ❌ 波特率不匹配
- ❌ 串口模块未供电
解决方案:
// 检查1:串口参数
USART_BaudRate = 115200 // 确保与PC端一致
// 检查2:GPIO配置
PA9: GPIO_Mode_AF_PP // TX必须是复用推挽
PA10: GPIO_Mode_IN_FLOATING // RX必须是浮空输入
// 检查3:硬件连接
STM32 TX → PC RX // 交叉连接
STM32 RX → PC TX
GND → GND // 共地
// 调试:测试TX引脚
void Test_TX(void)
{
while(1)
{
GPIO_SetBits(GPIOA, GPIO_Pin_9);
Delay_nms(500);
GPIO_ResetBits(GPIOA, GPIO_Pin_9);
Delay_nms(500);
}
// 用示波器观察PA9,应有方波输出
}
问题2:串口乱码
原因: 波特率不匹配
解决方案:
// 确保波特率一致
STM32端:
USART_InitStructure.USART_BaudRate = 115200;
PC端:
串口助手设置:115200
// 常见波特率:
9600, 19200, 38400, 57600, 115200, 230400
// 检查系统时钟
// 波特率精度依赖于系统时钟
RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9); // 8MHz × 9 = 72MHz
问题3:按键串口输出异常
原因: 中断服务函数中使用了阻塞延时
解决方案:
// 方案1:减少中断中的延时
void EXTI9_5_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line8) != RESET)
{
EXTI_ClearITPendingBit(EXTI_Line8);
Delay_nms(5); // 只消抖5ms
if(GPIO_ReadInputDataBit(KEY1_GPIO, KEY1_PIN) == 0)
{
USART1_Puts("Press KEY1_PIN\r\n");
}
// 不要等待按键释放
// while(GPIO_ReadInputDataBit(...) == 0);
}
}
// 方案2:使用标志位,在主循环中处理
volatile uint8_t key_flag = 0;
void EXTI9_5_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line8) != RESET)
{
EXTI_ClearITPendingBit(EXTI_Line8);
key_flag = 1; // 只设置标志位
}
}
int main(void)
{
// 初始化...
while(1)
{
if(key_flag)
{
key_flag = 0;
USART1_Puts("Press KEY1_PIN\r\n");
}
}
}
问题4:printf无法使用
原因: MicroLIB库冲突或未正确重定向
解决方案:
// 步骤1:取消MicroLIB
Keil → Options → Target → 取消勾选 "Use MicroLIB"
// 步骤2:添加fputc函数
int fputc(int ch, FILE *f)
{
while((USART1->SR & 0X40) == 0);
USART1->DR = (u8)ch;
return ch;
}
// 步骤3:包含stdio.h
#include <stdio.h>
// 步骤4:测试
printf("Test printf: %d\r\n", 123);
// 如果还不行,尝试添加:
#pragma import(__use_no_semihosting)
struct __FILE { int handle; };
FILE __stdout;
void _sys_exit(int x) { x = x; }
问题5:NVIC配置中USART2和USART1混淆
原因: 代码中NVIC配置写的是USART2,但实际使用USART1
解决方案:
// 修改NVIC配置
void NVIC_Configuration(void)
{
// ❌ 错误
NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;
// ✅ 正确
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);
}