跳到主要内容

【七】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);
}
}

程序执行流程

  1. 配置 NVIC 中断优先级
  2. 初始化 USART1 串口
  3. 发送初始化完成信息
  4. 打印系统时钟信息
  5. 循环发送 "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); // 使能接收中断
}

配置步骤详解

  1. 使能时钟

    • GPIOA 时钟:用于 PA9/PA10 引脚
    • USART1 时钟:使能 USART1 外设
  2. 配置 GPIO

    • TX (PA9):复用推挽输出(GPIO_Mode_AF_PP
    • RX (PA10):浮空输入(GPIO_Mode_IN_FLOATING
  3. 配置 USART 参数

    • 波特率:115200 bps
    • 数据位:8 位
    • 停止位:1 位
    • 校验位:无
    • 模式:收发模式(USART_Mode_Rx | USART_Mode_Tx
  4. 使能 USART1:启动串口工作

  5. 使能接收中断:接收到数据时触发中断

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); // 发送数据
}

工作原理

  1. 等待发送数据寄存器为空(TXE 标志位)
  2. 将数据写入数据寄存器(DR
  3. 硬件自动发送数据

3.2 发送字符串

void USART1_Puts(char *str)
{
while(*str)
{
USART_SendData(USART1, *str++);
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}
}

工作原理

  1. 遍历字符串每个字符
  2. 调用 USART_SendData 发送字符
  3. 等待发送完成(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); // 回显接收到的数据
}
}

中断处理流程

  1. 检查接收中断标志(USART_IT_RXNE
  2. 清除接收标志位
  3. 从数据寄存器读取接收到的数据
  4. 回显数据(发送回 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 配置

  1. C/C++ → Define:

    STM32F10X_MD, USE_STDPERIPH_DRIVER
  2. Target → Use MicroLIB:

    • 勾选 "Use MicroLIB"(支持 printf)
  3. Debug → Settings:

    • 选择 ST-Link 或 J-Link

2. 下载程序

参考 8E_OLED_NTC 文档中的下载步骤。


测试步骤

1. 硬件连接

  1. 将 USB 转串口模块连接到 PC
  2. 连接串口线:
    • STM32 PA9(TX) → 串口模块 RX
    • STM32 PA10(RX) → 串口模块 TX
    • STM32 GND → 串口模块 GND
  3. 上电

2. 串口助手配置

  1. 打开串口调试助手(推荐:SSCOM、XCOM)
  2. 配置串口参数:
    • 波特率:115200
    • 数据位:8
    • 停止位:1
    • 校验位:无
  3. 打开串口

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 传输(推荐大量数据)
// 参考后续优化建议