跳到主要内容

【八】UART1串口通信与独立按键中断

1. 项目概述

本项目演示如何使用 STM32F103C8T6 微控制器的 UART1 串口进行数据通信,并结合独立按键中断功能,实现按键状态通过串口输出的功能。系统支持串口数据收发、printf重定向、按键中断检测,并通过LED指示灯显示按键状态。

技术要点

  • ✅ UART1串口通信(115200波特率)
  • ✅ 串口发送/接收功能
  • ✅ 串口中断接收
  • ✅ printf/scanf重定向
  • ✅ EXTI外部中断(按键检测)
  • ✅ NVIC中断优先级配置
  • ✅ LED状态指示
  • ✅ 多中断协同工作

应用场景

  1. 📡 串口调试与通信
  2. 📡 按键状态远程监控
  3. 📡 人机交互界面
  4. 📡 数据采集与传输
  5. 📡 远程控制系统
  6. 📡 传感器数据上报

2. 硬件平台

主控芯片

  • 型号: STM32F103C8T6
  • 内核: ARM Cortex-M3
  • 主频: 72 MHz
  • Flash: 64 KB
  • SRAM: 20 KB
  • UART: 3个(USART1/2/3)

外设资源

UART1串口

功能引脚说明
UART1_TXPA9发送引脚(复用推挽输出)
UART1_RXPA10接收引脚(浮空输入)
波特率115200标准波特率
数据位8位无奇偶校验
停止位1位标准配置

独立按键(4个)

按键引脚中断线中断通道控制LED串口输出
KEY1PB8EXTI8EXTI9_5LED5"Press KEY1_PIN"
KEY2PB9EXTI9EXTI9_5LED6"Press KEY2_PIN"
KEY3PB10EXTI10EXTI15_10LED7"Press KEY3_PIN"
KEY4PB11EXTI11EXTI15_10LED8"Press KEY4_PIN"

LED指示灯(4个)

LED引脚说明
LED5PB4KEY1控制
LED6PB5KEY2控制
LED7PB6KEY3控制
LED8PB7KEY4控制

硬件连接

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
│ ├─ 串口输出按键信息
│ └─ 等待按键释放

└─ 串口接收中断
├─ 清除中断标志
├─ 读取接收数据
├─ 回显接收数据
└─ 返回主循环

功能特点

  1. UART1串口通信:

    • 115200波特率
    • 8位数据位
    • 无奇偶校验
    • 1位停止位
    • 支持中断接收
    • 支持回显功能
  2. 按键中断检测:

    • 4个独立按键
    • EXTI外部中断
    • 下降沿触发
    • 软件消抖5ms
    • LED状态指示
  3. 串口输出按键状态:

    • KEY1 → "Press KEY1_PIN"
    • KEY2 → "Press KEY2_PIN"
    • KEY3 → "Press KEY3_PIN"
    • KEY4 → "Press KEY4_PIN"
  4. printf/scanf重定向:

    • 支持printf输出到串口
    • 支持scanf从串口输入
    • 方便调试和交互
  5. 多中断协同:

    • 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)
{
; // 主循环空转,等待中断
}
}

主函数要点:

  1. 初始化顺序:

    NVIC_Configuration();      // 先配置中断优先级
    USART1_Configuration(); // 再初始化UART1
    GPIO_Key_Init(); // 初始化按键中断
    LED_Init(); // 初始化LED
  2. 启动信息:

    USART1_Puts("USART1_Configuration\r\n");

    作用:
    - 确认串口工作正常
    - 用于调试和验证
  3. 主循环:

    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); // 使能接收中断
}

初始化要点:

  1. GPIO配置:

    // TX引脚(PA9)
    GPIO_Mode_AF_PP // 复用推挽输出

    特点:
    - 复用功能模式
    - 由UART外设控制
    - 推挽输出驱动能力强

    // RX引脚(PA10)
    GPIO_Mode_IN_FLOATING // 浮空输入

    特点:
    - 高阻态输入
    - 不需要上拉/下拉
    - 由外部信号驱动
  2. 波特率配置:

    USART_BaudRate = 115200

    说明:
    - 标准波特率
    - 每秒115200
    - 适合大多数应用
    - 误差率低
  3. 中断配置:

    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);
}
}

发送函数要点:

  1. 发送单字节:

    while (!(USART1->SR & USART_FLAG_TXE));

    作用:
    - 等待TXE标志位置位
    - TXE=1表示发送缓冲区空
    - 可以写入新数据
  2. 发送字符串:

    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); // 回显数据
}
}

接收中断要点:

  1. 中断标志检查:

    if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)

    作用:
    - 检查RXNE标志位
    - RXNE=1表示接收到数据
  2. 读取数据:

    Uart_RevBuffer = USART1->DR;

    说明:
    - 读取数据寄存器
    - 自动清除RXNE标志位
  3. 回显功能:

    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);
}
}

按键中断特点:

  1. 串口输出按键状态:

    USART1_Puts("Press KEY1_PIN\r\n");

    作用:
    - 通过串口报告按键状态
    - 远程监控按键操作
    - 便于调试和验证
  2. 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配置要点:

  1. 优先级分组:

    NVIC_PriorityGroup_4  // 4位抢占优先级,0位响应优先级

    说明:
    - 抢占优先级:0-1516级)
    - 响应优先级:无
    - 数字越小优先级越高
  2. 中断优先级分配:

    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 硬件连接

连接步骤

  1. 连接串口模块

    STM32          USB转串口模块
    PA9 (TX) → RX
    PA10 (RX) ← TX
    GND → GND

    注意:
    - TX接RX,RX接TX(交叉连接)
    - 共地连接
    - 电平匹配(3.3V)
  2. 连接按键

    KEY1-KEY4 → PB8-PB11(按下接GND)
  3. 连接LED

    LED5-LED8 → PB4-PB7(高电平点亮)

9.2 软件配置

串口调试助手设置

串口参数配置:
┌────────────┬──────────┐
│ 波特率 │ 115200 │
│ 数据位 │ 8 │
│ 停止位 │ 1 │
│ 奇偶校验 │ 无 │
│ 流控 │ 无 │
│ 发送格式 │ ASCII │
│ 接收格式 │ ASCII │
└────────────┴──────────┘

推荐软件:
- 串口调试助手
- SecureCRT
- PuTTY
- Tera Term
- XCOM

9.3 运行效果

预期现象:

  1. 上电启动

    串口输出:
    USART1_Configuration
  2. 按键KEY1

    LED5点亮,其他LED熄灭
    串口输出:
    Press KEY1_PIN
  3. 按键KEY2

    LED6点亮,其他LED熄灭
    串口输出:
    Press KEY2_PIN
  4. 串口回显

    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:串口无输出

可能原因:

  1. ❌ 串口参数配置错误
  2. ❌ TX/RX引脚接反
  3. ❌ 波特率不匹配
  4. ❌ 串口模块未供电

解决方案:

// 检查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);
}