【七】5A_UART1 串口通信
项目概述
本项目是基于 STM32F103C8T6 微控制器的 UART1 串口通信示例程序。该项目演示了如何配置和使用 STM32 的 USART1 外设进行串口数据收发,实现与 PC 端串口调试助手的双向通信,支持数据发送、接收和回显功能。
硬件资源配置
1. USART1 引脚定义
串口引脚配置
- PA9:TX(发送端,复用推挽输出)
- PA10:RX(接收端,浮空输入)
- GND:接地(与 USB 转串口模块共地)
硬件连接
STM32F103C8T6 USB转串口模块(CH340/CP2102)
┌─────────┐ ┌─────────┐
│ PA9 │ ────→ │ RXD │ (交叉连接)
│ (TX) │ │ │
│ │ │ │
│ PA10 │ ←──── │ TXD │ (交叉连接)
│ (RX) │ │ │
│ │ │ │
│ GND │ ────→ │ GND │ (共地)
└─────────┘ └─────────┘
注意:串口通信需要交叉连接(TX->RX, RX->TX)
2. 串口参数配置
| 参数 | 配置值 |
|---|---|
| 波特率 | 115200 bps |
| 数据位 | 8 位 |
| 停止位 | 1 位 |
| 校验位 | 无 |
| 流控制 | 无 |
| 工作模式 | 全双工(同时收发) |
USART 通信原理
1. USART 基础知识
USART(Universal Synchronous/Asynchronous Receiver/Transmitter)通用同步/异步收发器。
关键特性:
- 全双工通信:可同时发送和接收数据
- 异步通信:不需要时钟信号
- 可编程波特率:支持多种通信速率
- 中断驱动:支持发送完成、接收完成中断
2. 串口通信时序
串口帧格式:
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│起始│D0 │D1 │D2 │D3 │D4 │D5 │D6 │D7 │停止│
│ 0 │ │ │ 数据位(8位) │ │ 1 │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
1bit 8bit (LSB先发送) 1bit
时间 = 1/波特率
115200 bps → 每位时间 = 1/115200 ≈ 8.68μs
传输1字节(10位)≈ 86.8μs
3. 波特率计算
// STM32 USART 波特率计算公式
波特率 = fPCLK / (16 × USARTDIV)
其中:
- fPCLK:APB2 时钟频率(USART1 在 APB2 上)
- USARTDIV:波特率分频因子(自动计算)
示例:
fPCLK = 72MHz
波特率 = 115200
USARTDIV = 72,000,000 / (16 × 115200) = 39.0625
寄存器值自动配置,无需手动计算
项目文件结构
5A_UART1/
├── Libraries/ # STM32标准外设库
│ ├── CMSIS/ # ARM Cortex微控制器软件接口标准
│ └── STM32F10x_StdPeriph_Driver/
│ ├── src/stm32f10x_usart.c # USART驱动(重要)
│ ├── src/stm32f10x_gpio.c
│ └── inc/ # 驱动头文件
├── MDK/ # Keil MDK项目文件
│ ├── TEST_CODE.uvproj # 项目文件
│ └── Output/ # 编译输出文件
├── User/ # 用户应用代码
│ ├── main.c # 主程序
│ ├── stm32f10x_conf.h # 外设库配置
│ ├── stm32f10x_it.c # 中断服务函数
│ ├── system_stm32f10x.c
│ └── TF_App/ # 应用模块
│ ├── TF_Delay.c/h # 延时函数
│ ├── TF_System_Cfg.c/h # 系统配置(NVIC)
│ └── TF_UART1.c/h # UART1驱动(核心)
└── Readme/ # 说明文档
核心代码解析
1. 主程序流程(main.c)
int main(void)
{
RCC_ClocksTypeDef RCC_Clocks;
NVIC_Configuration(); // 配置NVIC中断优先级
USART1_Configuration(); // USART1串口初始化
USART1_Puts("USART1_Configuration\r\n");
printf("RCC_GetSYSCLKSource:0X%X\r\n", RCC_GetSYSCLKSource());
RCC_GetClocksFreq(&RCC_Clocks);
printf("RCC_GetClocksFreq:0X%d\r\n", RCC_Clocks.SYSCLK_Frequency);
while(1)
{
USART1_Puts("Hello UART1\r\n");
Delay_nms(200);
}
}
程序执行流程:
- 配置 NVIC 中断优先级
- 初始化 USART1 串口
- 发送初始化完成信息
- 打印系统时钟信息
- 循环发送 "Hello UART1" 字符串(每 200ms 一次)
2. USART1 初始化配置(TF_UART1.c)
2.1 完整初始化函数
void USART1_Configuration(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
// 步骤1:使能端口时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 使能串口1相关IO口时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); // 使能串口1时钟
// 步骤2:配置 USART1发送IO口
// 配置发送引脚--Tx (PA9)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 配置开漏输出模式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 配置输出频率为50MHz
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置接收引脚--Rx (PA10)
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); // 使能接收中断
}
配置步骤详解:
-
使能时钟:
- GPIOA 时钟:用于 PA9/PA10 引脚
- USART1 时钟:使能 USART1 外设
-
配置 GPIO:
- TX (PA9):复用推挽输出(
GPIO_Mode_AF_PP) - RX (PA10):浮空输入(
GPIO_Mode_IN_FLOATING)
- TX (PA9):复用推挽输出(
-
配置 USART 参数:
- 波特率:115200 bps
- 数据位:8 位
- 停止位:1 位
- 校验位:无
- 模式:收发模式(
USART_Mode_Rx | USART_Mode_Tx)
-
使能 USART1:启动串口工作
-
使能接收中断:接收到数据时触发中断
2.2 GPIO 模式说明
| GPIO 模式 | 说明 | 用途 |
|---|---|---|
GPIO_Mode_AF_PP | 复用推挽输出 | USART TX 引脚 |
GPIO_Mode_AF_OD | 复用开漏输出 | I2C 引脚 |
GPIO_Mode_IN_FLOATING | 浮空输入 | USART RX 引脚 |
GPIO_Mode_IPU | 上拉输入 | 按键输入 |
GPIO_Mode_IPD | 下拉输入 | 特殊输入 |
3. 数据发送函数
3.1 发送单个字节
void USART1_Send_Byte(u16 Data)
{
while (!(USART1->SR & USART_FLAG_TXE)); // 等待发送数据寄存器空
USART1->DR = (Data & (uint16_t)0x01FF); // 发送数据
}
工作原理:
- 等待发送数据寄存器为空(
TXE标志位) - 将数据写入数据寄存器(
DR) - 硬件自动发送数据
3.2 发送字符串
void USART1_Puts(char *str)
{
while(*str)
{
USART_SendData(USART1, *str++);
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}
}
工作原理:
- 遍历字符串每个字符
- 调用
USART_SendData发送字符 - 等待发送完成(
TXE标志位置位)
4. 数据接收中断
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); // 回显接收到的数据
}
}
中断处理流程:
- 检查接收中断标志(
USART_IT_RXNE) - 清除接收标志位
- 从数据寄存器读取接收到的数据
- 回显数据(发送回 PC 端)
5. printf 函数支持
为了支持标准 C 库的 printf 函数,需要重定向 fputc 函数:
// 重定向 fputc 函数
// Printf 函数最终会调用 fputc 输出字符
int fputc(int ch, FILE *f)
{
while((USART1->SR & 0X40) == 0); // 循环发送,直到发送完毕
USART1->DR = (u8) ch;
return ch;
}
// 重定向 fgetc 函数
// scanf 输入函数最终会调用 fgetc
int fgetc(FILE *f)
{
while(!(USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == SET))
{
}
return (USART_ReceiveData(USART1));
}
使用示例:
// 使用 printf 发送数据
printf("Temperature: %.2f C\r\n", temperature);
printf("ADC Value: %d\r\n", adc_value);
// 使用 scanf 接收数据
int value;
scanf("%d", &value);
6. NVIC 中断优先级配置
void NVIC_Configuration(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
// 配置中断优先级分组
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
// 配置 USART1 中断
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // 抢占优先级1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; // 子优先级1
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
中断优先级分组(NVIC_PriorityGroup_2):
- 抢占优先级:2 位(0-3)
- 子优先级:2 位(0-3)
- 抢占优先级高的可以打断低的
- 子优先级仅决定同时到达时的处理顺序
程序执行效果
串口调试助手显示
打开串口调试助手(波特率 115200),将看到以下输出:
USART1_Configuration
RCC_GetSYSCLKSource:0X08
RCC_GetClocksFreq:0X4493E00
Hello UART1
Hello UART1
Hello UART1
...(每200ms输出一次)
输出说明:
USART1_Configuration:串口初始化完成RCC_GetSYSCLKSource:0X08:系统时钟源(0x08 = PLL)RCC_GetClocksFreq:0X4493E00:系统时钟频率(0x4493E00 = 72,000,000 Hz = 72MHz)Hello UART1:循环发送的测试字符串
回显功能测试
在串口调试助手中输入任意字符,例如输入 "ABC",将立即收到回显:
发送:ABC
接收:ABC
这是因为 USART1_IRQHandler 中实现了回显功能。
编译与下载
1. 编译配置
必须包含的库文件
Libraries/STM32F10x_StdPeriph_Driver/src/stm32f10x_usart.c ← 必须
Libraries/STM32F10x_StdPeriph_Driver/src/stm32f10x_gpio.c
Libraries/STM32F10x_StdPeriph_Driver/src/stm32f10x_rcc.c
Keil MDK 配置
-
C/C++ → Define:
STM32F10X_MD, USE_STDPERIPH_DRIVER -
Target → Use MicroLIB:
- 勾选 "Use MicroLIB"(支持 printf)
-
Debug → Settings:
- 选择 ST-Link 或 J-Link
2. 下载程序
参考 8E_OLED_NTC 文档中的下载步骤。
测试步骤
1. 硬件连接
- 将 USB 转串口模块连接到 PC
- 连接串口线:
- STM32 PA9(TX) → 串口模块 RX
- STM32 PA10(RX) → 串口模块 TX
- STM32 GND → 串口模块 GND
- 上电
2. 串口助手配置
- 打开串口调试助手(推荐:SSCOM、XCOM)
- 配置串口参数:
- 波特率:115200
- 数据位:8
- 停止位:1
- 校验位:无
- 打开串口
3. 功能测试
测试1:数据发送
- 观察串口助手是否收到 "Hello UART1"(每 200ms 一次)
测试2:数据接收与回显
- 在串口助手发送区输入任意字符
- 观察是否立即收到回显
测试3:printf 功能
- 修改代码,添加 printf 语句
- 观察格式化输出是否正常
常见问题与解决方案
1. 串口无输出
可能原因:
- 串口线连接错误(TX/RX 未交叉)
- 波特率不匹配
- 串口助手未打开或选错端口
- USART1 时钟未使能
解决方法:
// 1. 检查串口线连接(必须交叉)
STM32 TX(PA9) → 串口模块 RX
STM32 RX(PA10) → 串口模块 TX
// 2. 确认波特率一致
USART_InitStructure.USART_BaudRate = 115200; // 代码中
串口助手设置:115200 // 软件中
// 3. 检查时钟使能
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
// 4. 确认 USART 已使能
USART_Cmd(USART1, ENABLE);
2. 输出乱码
可能原因:
- 波特率不匹配
- 系统时钟配置错误
- 数据位/停止位配置错误
解决方法:
// 1. 确认波特率配置
USART_InitStructure.USART_BaudRate = 115200;
// 2. 检查系统时钟(应为 72MHz)
RCC_GetClocksFreq(&RCC_Clocks);
printf("SYSCLK: %d Hz\r\n", RCC_Clocks.SYSCLK_Frequency);
// 3. 确认通信参数
USART_InitStructure.USART_WordLength = USART_WordLength_8b; // 8位
USART_InitStructure.USART_StopBits = USART_StopBits_1; // 1停止位
USART_InitStructure.USART_Parity = USART_Parity_No; // 无校验
3. 接收中断不触发
可能原因:
- NVIC 未配置
- 接收中断未使能
- 中断服务函数名称错误
解决方法:
// 1. 配置 NVIC
NVIC_Configuration();
// 2. 使能接收中断
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
// 3. 确认中断服务函数名称(不能改)
void USART1_IRQHandler(void) // 固定名称
{
// ...
}
// 4. 在 stm32f10x_it.c 中声明
// 或将中断函数放在 TF_UART1.c 中
4. printf 不工作
可能原因:
- 未勾选 MicroLIB
- fputc 未重定向
- 未包含 stdio.h
解决方法:
// 1. Keil 工程配置
// Target → Use MicroLIB(勾选)
// 2. 包含头文件
#include <stdio.h>
// 3. 重定向 fputc
int fputc(int ch, FILE *f)
{
while((USART1->SR & 0X40) == 0);
USART1->DR = (u8) ch;
return ch;
}
// 4. 测试
printf("Test printf\r\n");
5. 发送大量数据丢失
可能原因:
- 发送太快,缓冲区溢出
- 未等待发送完成
解决方法:
// 方法1:每次发送后等待
void USART1_Send_Byte(u16 Data)
{
while(!(USART1->SR & USART_FLAG_TXE)); // 等待发送寄存器空
USART1->DR = Data;
while(!(USART1->SR & USART_FLAG_TC)); // 等待发送完成
}
// 方法2:使用 DMA 传输(推荐大量数据)
// 参考后续优化建议