普中STM32F103ZET6开发攻略(六)
接续上文:普中STM32F103ZET6开发攻略(五)-CSDN博客
点关注不迷路哟。你的点赞、收藏,一键三连,是我持续更新的动力哟!!!
目录
接续上文:普中STM32F103ZET6开发攻略(五)-CSDN博客
点关注不迷路哟。你的点赞、收藏,一键三连,是我持续更新的动力哟!!!
6. USART串口通信实验
6.1 实验目的
6.2 实验原理
6.3 实验环境
6.4 实验思路
6.5 实验代码
6.5.1 USART源、头
6.5.2 led源、头
6.5.3 delay源、头
6.6 实验思考和拓展
6.6.1 如何修改程序,实现对接收到的字符进行大小写转换后再回显?
6.6.2 如何实现字符串的发送功能,而不是逐字节发送?
6.6.3 波特率对通信质量和效率有何影响?如何选择合适的波特率?
6.6.4 如何结合命令解析,实现通过串口控制LED的开关或闪烁模式?
6.6.5 相比于查询方式,使用中断方式接收串口数据有什么优势?
6.6.6 如何实现多个串口的同时工作?
6.7 注意事项
6. USART串口通信实验
6.1 实验目的
1. 熟悉STM32F10x微控制器的USART通信功能和基本操作
2. 掌握STM32标准库函数对串口通信的配置方法
3. 学会实现串口数据的接收与发送,实现数据回显功能
4. 理解串口中断接收机制和状态管理方法
6.2 实验原理
*1. 串口通信基本原理*
串口通信(UART/USART)是一种常用的串行通信协议,采用异步收发方式,数据帧由起始位、数据位、校验位和停止位组成。STM32F1系列微控制器内置多个USART外设,支持全双工通信,可实现与PC或其他设备的数据交换。
*2. STM32 USART功能简介*
STM32F1的USART外设具有以下主要特性: (1) 全双工异步通信 (2) 可编程数据字长(8位或9位) (3) 可配置停止位(1位或2位等) (4) 支持奇偶校验 (5) 支持硬件流控制 (6) 独立的发送和接收缓冲区 (7) 多种中断源(接收完成、发送完成、空闲检测等)
表1 USART主要寄存器
寄存器 | 名称 | 功能描述 |
---|---|---|
USART_SR | 状态寄存器 | 存储通信状态标志位(如TC, RXNE等) |
USART_DR | 数据寄存器 | 存储接收到的数据或待发送的数据 |
USART_BRR | 波特率寄存器 | 设置通信波特率 |
USART_CR1 | 控制寄存器1 | 配置USART工作模式和中断使能 |
USART_CR2 | 控制寄存器2 | 配置停止位等参数 |
USART_CR3 | 控制寄存器3 | 配置硬件流控制等功能 |
*3. 波特率计算*
波特率是串口通信的速率,表示每秒传输的位数。STM32 USART的波特率计算公式为:
波特率 = fck / (16 × USARTDIV)
其中,fck为USART时钟频率(通常为PCLK),USARTDIV为分频系数,存储在USART_BRR寄存器中。
*4. 中断接收原理*
通过中断方式接收数据,可以提高系统实时性和效率。当USART接收到一个字节数据时,硬件自动置位RXNE(接收缓冲区非空)标志并产生中断。在中断服务程序中,软件读取数据并进行处理,从而实现数据的及时接收。
*5. LED状态指示原理*
LED指示灯常用于反馈系统运行状态。通过控制GPIO输出电平,可以控制LED的点亮和熄灭。定时翻转LED状态可实现闪烁效果,作为系统正常运行的视觉反馈。
*7. 中断接收机制和回显机制*
设置 USART 接收中断,在每次接收到一个字节数据时触发中断服务函数,在中断中将数据存入接收缓冲区。
当检测到接收到完整一行(如遇到换行符 \r
或 \n
),设置接收完成标志,在主函数中读取缓冲区内容并通过 USART 逐个发送,实现回显。
6.3 实验环境
-
开发板:STM32F103ZET6
-
IDE:Keil MDK 5 /Visual Studio
-
调试工具:CMSIS-DAP
6.4 实验思路
硬件电路图:
波特率:
接收器和发送器( Rx 和 Tx)的波特率均设置为相同值。波特率计算公式:
fCK 为 USART 时钟频率,USARTDIV 是一个存放在波特率寄存器 (USART_BRR)的一个无符号定点数。其中 DIV_Mantissa[11:0]位定义 USARTDIV 的整数部分,DIV_Fraction[3:0]位定义 USARTDIV 的小数部分。 串口通信中常用的波特率为 4800、9600、115200 等。
常用串口相关库函数
RCC_APB2PeriphClockCmd:使能或禁用APB2总线上的外设时钟
GPIO_Init:初始化GPIO引脚配置
USART_Init:配置USART参数(波特率、数据位、校验位等)
USART_Cmd:使能或禁用USART功能
USART_ITConfig:配置USART中断
USART_SendData:发送一个字节数据
USART_ReceiveData:接收一个字节数据
USART_GetFlagStatus:获取USART状态标志(如发送完成标志TC)
USART_GetITStatus:获取USART中断状态(如接收中断RXNE)
NVIC_Init:配置中断控制器,设置中断优先级
初始化串口 USART1 配置 GPIOA 的 PA9 为 TX、PA10 为 RX,设置波特率为 115200,启用 USART 接收中断,并配置 NVIC。
配置中断接收逻辑 在 USART1_IRQHandler
中断函数中,读取接收数据并存入缓冲区;遇到换行符或接收满时,设置接收完成标志。
在主循环中轮询标志位 主函数不断检测 rx_flag
,一旦为真,即可通过 USART_SendString
回显接收到的整行数据,然后清空标志与缓冲区。
通过串口调试助手验证 打开串口助手,发送字符或字符串,STM32 端立即回显,验证通信是否正常。
6.5 实验代码
6.5.1 USART源、头
usart1.h
#ifndef __USART_H #define __USART_H #include "stm32f10x.h" extern char rx_buffer[]; extern uint8_t rx_flag; void USART1_Init(uint32_t baudrate); void USART_SendChar(USART_TypeDef* USARTx, char c); void USART_SendString(USART_TypeDef* USARTx, const char* str); #endif
usart1.c
#include "stm32f10x.h" #include "stm32f10x_usart.h" #include <string.h> #include "usart1.h" #define RX_BUFFER_SIZE 64 char rx_buffer[RX_BUFFER_SIZE]; uint8_t rx_index = 0; uint8_t rx_flag = 0; void USART1_Init(uint32_t baudrate) {GPIO_InitTypeDef GPIO_InitStructure;USART_InitTypeDef USART_InitStructure;NVIC_InitTypeDef NVIC_InitStructure; // 开启时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE); // 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); // RX (PA10)GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;GPIO_Init(GPIOA, &GPIO_InitStructure); // 配置 USART1USART_InitStructure.USART_BaudRate = baudrate;USART_InitStructure.USART_WordLength = USART_WordLength_8b;USART_InitStructure.USART_StopBits = USART_StopBits_1;USART_InitStructure.USART_Parity = USART_Parity_No;USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;USART_Init(USART1, &USART_InitStructure); // 中断配置USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);USART_Cmd(USART1, ENABLE); NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;NVIC_Init(&NVIC_InitStructure); } void USART_SendChar(USART_TypeDef* USARTx, char c) {USART_SendData(USARTx, c);while (USART_GetFlagStatus(USARTx, USART_FLAG_TXE) == RESET); } void USART_SendString(USART_TypeDef* USARTx, const char* str) {while (*str) {USART_SendChar(USARTx, *str++);} } void USART1_IRQHandler(void) {if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) {char c = USART_ReceiveData(USART1); // 回车或缓冲区满,标志位设为1if (c == '\r' || c == '\n' || rx_index >= RX_BUFFER_SIZE - 1) {rx_buffer[rx_index] = '\0'; // 结束符rx_flag = 1;rx_index = 0; // 重置接收}else {rx_buffer[rx_index++] = c;}} }
解释:
typedef struct {uint32_t USART_BaudRate; //波特率uint16_t USART_WordLength; //字长uint16_t USART_StopBits; //停止位uint16_t USART_Parity; //校验位uint16_t USART_Mode; //USART 模式uint16_t USART_HardwareFlowControl; //硬件流控制} USART_InitTypeDef; 下面就来简单介绍下每个成员变量的功能: 1.USART_BaudRate:波特率设置。常用的波特率为 4800、9600、115200 等。 标准库 函 数 会 根 据 设 定 值 计 算 得 到 USARTDIV 值 , 并 设 置USART_BRR 寄存器值。 2.USART_WordLength:数据帧字长。可以选择为 8 位或者 9 位,通过 USART_CR1寄存器的 M 位的值决定。如果没有使能奇偶校验控制,一般使用 8 数据位;如果使能了奇偶校验则一般设置为 9 数据位。 3.USART_StopBits:停止位设置。可选 0.5 个、 1 个、 1.5 个和 2 个停止位,它设定 USART_CR2 寄存器的 STOP[1:0]位的值,一般我们选择 1 个停止位。 4.USART_Parity:奇偶校验控制选择。可选 USART_Parity_No( 无 校 验 ) 、 USART_Parity_Even( 偶 校 验 ) 以 及 USART_Parity_Odd( 奇 校 验 ) ,它设 定 USART_CR1 寄存器的 PCE 位和 PS 位的值。 5.USART_Mode:USART 模式选择。可以为 USART_Mode_Rx 和 USART_Mode_Tx, 允许使用逻辑或运算选择两个,它设定 USART_CR1 寄存器的 RE 位和 TE 位。 6.USART_HardwareFlowControl:硬件流控制选择。只有在硬件流控制模式才有效,可以选择无硬件流USART_HardwareFlowControl_None、 RTS 控制USART_HardwareFlowControl_RTS、 CTS 控制 USART_HardwareFlowControl_CTS、 RTS 和 CTS 控制 USART_HardwareFlowControl_RTS_CTS
6.5.2 led源、头
led.h
#ifndef __LED_H #define __LED_H #include "stm32f10x.h" #define LED0_GPIO_PORT GPIOB #define LED0_GPIO_PIN GPIO_Pin_5 //LED0 #define LED1_GPIO_PORT GPIOE #define LED1_GPIO_PIN GPIO_Pin_5 //LED1 void LED_Init(void); void LED0_TOGGLE(void); void LED1_ON(void); void LED1_OFF(void); #endif
led.c
#include "led.h" void LED_Init(void) {GPIO_InitTypeDef GPIO_InitStructure;// 开启 GPIOB 和 GPIOE 的时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOE, ENABLE); //配置PB5(LED0)GPIO_InitStructure.GPIO_Pin = LED0_GPIO_PIN;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(LED0_GPIO_PORT, &GPIO_InitStructure); //配置PE5(LED1)GPIO_InitStructure.GPIO_Pin = LED1_GPIO_PIN;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(LED1_GPIO_PORT, &GPIO_InitStructure); //低电平输出GPIO_ResetBits(LED0_GPIO_PORT, LED0_GPIO_PIN);GPIO_ResetBits(LED1_GPIO_PORT, LED1_GPIO_PIN); } void LED0_TOGGLE(void) {LED0_GPIO_PORT->ODR ^= LED0_GPIO_PIN; } void LED1_ON(void) {GPIO_ResetBits(LED1_GPIO_PORT, LED1_GPIO_PIN); } void LED1_OFF(void) {GPIO_SetBits(LED1_GPIO_PORT, LED1_GPIO_PIN); }
6.5.3 delay源、头
delay.h
#ifndef __DELAY_H #define __DELAY_H #include "stm32f10x.h" void Delay_Init(void); void Delay_ms(u32 nms); #endif
delay.c
#include "delay.h" static u32 fac_ms = 0; void Delay_Init(void) {SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8); // HCLK/8fac_ms = SystemCoreClock / 8000; } void Delay_ms(u32 nms) {u32 temp;SysTick->LOAD = nms * fac_ms;SysTick->VAL = 0x00;SysTick->CTRL = 0x01;do{temp = SysTick->CTRL;} while ((temp & 0x01) && !(temp & (1 << 16)));SysTick->CTRL = 0x00;SysTick->VAL = 0X00; }
6.6 实验思考和拓展
6.6.1 如何修改程序,实现对接收到的字符进行大小写转换后再回显?
思路: 在中断接收完成后,主函数中处理接收缓冲区时,对每个字符进行大小写判断并转换:
for (int i = 0; i < rx_len; i++) {char c = rx_buf[i];if (c >= 'a' && c <= 'z') {c -= 32; // 小写转大写} else if (c >= 'A' && c <= 'Z') {c += 32; // 大写转小写}USART_SendData(USART1, c);while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); }
6.6.2 如何实现字符串的发送功能,而不是逐字节发送?
方法: 定义一个函数,循环发送字符串直到遇到字符串结束符 \0
:
void USART_SendString(USART_TypeDef* USARTx, const char* str) {while (*str) {USART_SendData(USARTx, *str++);while (USART_GetFlagStatus(USARTx, USART_FLAG_TXE) == RESET);} }
6.6.3 波特率对通信质量和效率有何影响?如何选择合适的波特率?
-
波特率高:
-
✅ 提高数据传输效率(速度快);
-
❌ 更容易产生误码,特别是长线或干扰环境下;
-
-
波特率低:
-
✅ 通信稳定可靠;
-
❌ 速度慢,延迟高。
-
选择建议:
-
实验/开发阶段常用
115200
,速度和稳定性兼顾; -
若连接的是传感器或老旧设备,可选
9600
、4800
; -
通信双方必须设置相同波特率;
-
若数据量大且线路短可选更高波特率(如
921600
);
6.6.4 如何结合命令解析,实现通过串口控制LED的开关或闪烁模式?
思路:
-
在串口接收中断中读取一行命令;
-
在主循环中判断指令内容;
-
根据命令控制 LED 状态。
举个例子:
if (strcmp(rx_buf, "LED_ON") == 0) {GPIO_SetBits(GPIOC, GPIO_Pin_13); // 点亮LED } else if (strcmp(rx_buf, "LED_OFF") == 0) {GPIO_ResetBits(GPIOC, GPIO_Pin_13); // 熄灭LED } else if (strcmp(rx_buf, "LED_TOGGLE") == 0) {GPIO_WriteBit(GPIOC, GPIO_Pin_13, (BitAction)!GPIO_ReadOutputDataBit(GPIOC, GPIO_Pin_13)); }
6.6.5 相比于查询方式,使用中断方式接收串口数据有什么优势?
对比项 | 查询方式 | 中断方式 |
---|---|---|
CPU 占用 | 高,占用大量时间轮询 | 低,空闲时可做其他任务 |
响应速度 | 慢,需不断检查标志位 | 快,数据到达立即响应 |
接收效率 | 低,易丢数据 | 高,系统调度高效 |
编程复杂度 | 简单 | 稍高但更强大 |
结论:中断方式更适合实时性强、效率要求高的应用场景。
6.6.6 如何实现多个串口的同时工作?
步骤:
-
开启多个 USART 外设: 例如我们大多数人刚开始接触时都是使用芯片:STM32F103C8T6 ,而它具有 USART1、USART2、USART3;
-
分别初始化每个串口: 配置对应 GPIO、波特率、中断优先级;
-
编写独立中断函数:
USART1_IRQHandler
、USART2_IRQHandler
等; -
分配独立接收缓冲区和标志位: 避免数据混淆;
-
主循环中分别处理每个串口的接收数据。
示例:
void USART1_IRQHandler(void) { /* 处理 USART1 接收 */ } void USART2_IRQHandler(void) { /* 处理 USART2 接收 */ }
6.7 注意事项
-
串口初始化前必须先使能相应的USART时钟和GPIO时钟
-
TX引脚必须配置为复用推挽输出模式,RX引脚配置为浮空输入模式
-
波特率、数据位、停止位等参数必须与串口调试助手设置一致
-
在使用中断方式接收数据时,必须正确配置NVIC优先级
-
接收缓冲区大小必须足够,防止数据溢出
-
发送数据时必须等待上一次发送完成,避免数据丢失
-
使用标准库函数时,需要注意头文件的包含和依赖关系