前言
本文章讲述的是STM32F103系列基于HAL库开发的串口通信,包含普通串口,串口中断以及串口DMA,感兴趣的可以好好看看。
本次实验用到的模块有:STM32C8T6系统板,0.96寸OLED屏幕以及HC-08蓝牙模块
普通串口接收(HAL_UART_Receive)
简单介绍:`HAL_UART_Receive`是 STM32 HAL 库中用于 UART 数据接收的阻塞函数。它接收指定数量的字节,并将数据存储到指定的缓冲区中。该函数会一直等待,直到数据接收完成或超时,因此会阻塞程序的执行。
首先打开STM32CubeMx,选择对应的芯片型号,首先进入system Core,选择SYS,设置调试方式(DuBug)为Serial Wire。然后返回上一级,进入Connectivity,选择对应的串口USARTx(x表示你所选择的串口)。如果没有特殊情况,默认选择Asynchronous异步通信模式。
基础设置(Basic Parameters)
波特率(Baud Rate):115200 Bits/s(按个人需求来)
字长( Word Length):8 Bits/s(通常为一个字节)
奇偶校验位 (Parity) :None(通常不需要校验位)
停止位(Stop Bit):1 (用于标识数据帧的结束)
高级设置(advanced paramenters)
数据方向(Data Direction):Receive and Transmit(发送和接收)
过采样(Oversampling):16 samples
最后选择GENERATE CODE生成代码即可
核心代码
#include "main.h"
#include "OLED.h"
#include "GPIO_PIN_13.h"
UART_HandleTypeDef huart2;
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_USART2_UART_Init(void);
int main(void)
{
HAL_Init();// 初始化 HAL 库
SystemClock_Config();// 配置系统时钟
MX_GPIO_Init();// 初始化 GPIO
MX_USART2_UART_Init();// 初始化 USART2 UART
OLED_Init();// 初始化 OLED
GPIOC_PIN_13_Init();// 初始化 GPIOC 的第 13 引脚
char DATARCVD[10]; // 定义一个足够大的缓冲区
uint8_t received_byte;// 定义一个变量,用于存储接收到的单个字节
while (1)
{
HAL_UART_Receive(&huart2,&received_byte,1,10); // 通过 UART2 接收一个字节的数据,存储到 received_byte 中
// 参数说明:
// &huart2:指定 UART 句柄
// &received_byte:接收数据存储的变量地址
// 1:接收字节数
// 10:接收超时时间(单位为毫秒)
DATARCVD[0]=received_byte;// 将接收到的字节赋值给 DATARCVD 数组的第一个元素
DATARCVD[1]='\0';// 在 DATARCVD 数组的第二个位置添加字符串结束符,确保 DATARCVD 是一个有效的字符串
if(DATARCVD[0]=='H')// 判断接收到的字符是否为 'H'
{
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_13,GPIO_PIN_SET);// 将 GPIOC 的第 13 引脚设置为高电平(GPIO_PIN_13)
OLED_ShowString(1,1,"LED_OFF"); // 在 OLED 显示屏的第 1 行第 1 列显示 "LED_OFF"
OLED_ShowString(2,1,DATARCVD);// 在 OLED 显示屏的第 2 行第 1 列显示接收到的数据(DATARCVD)
}
else if(DATARCVD[0]=='J') // 判断接收到的字符是否为 'J'
{
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_13,GPIO_PIN_RESET);// 将 GPIOC 的第 13 引脚设置为低电平(GPIO_PIN_13)
OLED_ShowString(1,1,"LED__ON");// 在 OLED 显示屏的第 1 行第 1 列显示 "LED__ON"
OLED_ShowString(2,1,DATARCVD); // 在 OLED 显示屏的第 2 行第 1 列显示接收到的数据(DATARCVD)
}
OLED_ShowString(2,1,DATARCVD);
// 无论是否匹配 'H' 或 'J',都会在 OLED 的第 2 行第 1 列显示接收到的数据
// 这一行代码可能会导致重复显示,因为上面的分支中已经显示过一次 DATARCVD
}
}
功能:
1. 数据接收:通过 UART2 接收一个字节的数据,并存储到 received_byte 中。
2. 数据处理:如果接收到的字符是 'H' ,则点亮 GPIOC 的第 13 引脚(LED 熄灭),并在OLED 上显示 "LED_OFF" 。如果接收到的字符是 'J' ,则熄灭 GPIOC 的第 13 引脚(LED 点亮),并在 OLED 上显示 "LED__ON" 。
3. OLED 显示:在 OLED 的第 2 行显示接收到的1个字节的数据。
优点
1. 逻辑简单:使用阻塞式接收函数,代码逻辑简单直观,易于理解和实现。不需要处理复杂的中断机制,适合初学者或对实时性要求不高的应用。
2. 易于调试:阻塞式接收函数的行为容易预测,调试时可以方便地跟踪数据接收和处理过程。不涉及中断优先级和同步问题,减少了调试的复杂性。
3. 资源占用低:不需要额外的缓冲区管理,减少了内存使用。不需要配置中断优先级,简化了系统资源管理。
缺点
1. 阻塞性: HAL_UART_Receive 是阻塞函数,会阻塞程序的执行,直到接收到数据或超时。如果数据接收间隔较长,可能会导致程序响应变慢,影响其他任务的执行。
2. 效率较低:由于是阻塞式接收,CPU 在等待数据接收时无法执行其他任务,资源利用率低。 每次只接收一个字节,效率较低,不适合高数据量的传输场景。
3. 代码冗余:在 OLED 上重复显示接收到的数据( DATARCVD ),可能会导致显示内容重复。• 代码中存在一些冗余操作,例如在分支中已经显示过 DATARCVD ,最后又重复显示一次。
总结
HAL_UART_Receive是一个阻塞式的 UART 数据接收函数。它会在指定的时间内等待接收指定数量的字节,直到接收完成或超时。这种方式简单易用,适合数据量小且对实时性要求不高的场景,但会阻塞程序执行,降低 CPU 资源利用率。
串口接收中断(HAL_UART_Receive_IT)
简单介绍:`HAL_UART_Receive_IT`是 STM32 HAL 库中用于 UART 数据接收的中断驱动函数。它启动一个非阻塞的接收操作,允许程序在接收数据时继续执行其他任务。当接收完成时,会触发中断并调用回调函数`HAL_UART_RxCpltCallback`,通知用户处理接收到的数据。这种方式提高了程序的效率和实时性,特别适合处理大量数据或对实时性要求较高的场景。
在前面普通串口的基础上,选择NVIC Settings(设置中断),由于这个只是串口的中断调试,所以对中断优先级不做调整,默认即可。
核心代码
#include "main.h"
#include "OLED.h"
#include "GPIO_PIN_13.h"
#define BUFFER_SIZE 10 // 定义接收缓冲区的大小,用于存储接收到的UART数据
UART_HandleTypeDef huart2; // UART2句柄,用于操作串口通信
void SystemClock_Config(void); // 系统时钟配置函数声明
static void MX_GPIO_Init(void); // GPIO初始化函数声明
static void MX_USART2_UART_Init(void); // UART2初始化函数声明
uint8_t DataRcvd[BUFFER_SIZE]; // 定义接收数据缓冲区,用于存储UART接收到的数据
int main(void)
{
HAL_Init(); // 初始化HAL库,设置全局变量、中断优先级分组等
SystemClock_Config(); // 配置系统时钟,设置CPU时钟频率等
MX_GPIO_Init(); // 初始化GPIO,配置GPIO的模式、速度等
MX_USART2_UART_Init(); // 初始化UART2,配置波特率、数据位、停止位等
GPIOC_PIN_13_Init(); // 初始化GPIOC_PIN_13,用于控制LED
OLED_Init(); // 初始化OLED显示屏
HAL_UART_Receive_IT(&huart2, DataRcvd, 2); // 以中断方式接收2个字节数据,存储到DataRcvd缓冲区
while (1) // 主循环
{
// 主循环为空,所有操作在中断回调函数中完成
}
}
// UART接收中断完成回调函数,此函数在stm32f1xx_hal_uart.c文件中的第2622行
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart == &huart2) // 判断是否为huart2的中断
{
char receivedChar[BUFFER_SIZE + 1] = {0}; // 用于存储字符串,+1用于存储字符串结束符'\0'
for (int i = 0; i < BUFFER_SIZE; i++) // 将接收到的数据复制到字符串数组
{
receivedChar[i] = DataRcvd[i];
}
// 显示接收到的字符串
for (int i = 0; i < 200; i++) // 延时,防止显示过快
{
// 空循环,用于延时
}
OLED_ShowString(2, 1, receivedChar); // 在OLED上显示接收到的字符串,位置为第2行第1列
// 根据接收到的第一个字符控制LED和显示信息
if (DataRcvd[0] == 'H') // 如果接收到的字符为'H'
{
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET); // 点亮LED
OLED_ShowString(1, 1, "LED_OFF"); // 在OLED上显示LED状态,位置为第1行第1列
}
else if (DataRcvd[0] == 'J') // 如果接收到的字符为'J'
{
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET); // 熄灭LED
OLED_ShowString(1, 1, "LED__ON"); // 在OLED上显示LED状态,位置为第1行第1列
}
}
HAL_UART_Receive_IT(&huart2, DataRcvd, 2); // 继续以中断方式接收2个字节数据,保持接收功能
}
串口中断的优点
1. 提高效率:
非阻塞:串口中断是非阻塞的,程序可以在等待串口数据接收或发送时继续执行其他任务,而不是在串口操作上浪费时间。
实时性:能够及时响应串口事件,确保数据的及时处理,适合对实时性要求较高的应用。
2. 资源利用合理:
CPU占用低:在没有串口事件发生时,CPU可以执行其他任务,不会被串口操作占用。
缓冲区管理:通过缓冲区管理,可以减少数据丢失的风险,提高数据传输的可靠性。
3. 灵活性高:
动态处理:可以根据接收到的数据动态调整程序的行为,例如根据接收到的命令执行不同的操作。
易于扩展:可以方便地扩展串口功能,例如增加协议解析、数据校验等功能。
串口中断的缺点
1. 复杂性增加:
代码复杂:需要编写中断处理函数,并且需要处理中断的优先级和同步问题,代码复杂度较高。
调试困难:中断相关的问题(如中断冲突、数据丢失等)较难调试,需要使用专业的调试工具和方法。
2. 资源限制:
中断优先级:在多中断系统中,需要合理配置中断优先级,否则可能会导致高优先级中断抢占低优先级中断,影响系统稳定性。
缓冲区大小:缓冲区大小有限,如果数据接收速度过快,可能会导致缓冲区溢出,丢失数据。
3. 对硬件要求高:
硬件支持:需要硬件支持中断功能,且硬件的中断处理能力需要足够强大。
稳定性要求:硬件的稳定性对串口中断的可靠性影响较大,硬件故障可能导致中断处理失败。
总结
HAL_UART_Receive_IT是一个用于启动 UART 接收中断的 HAL 库函数。它允许 UART 在接收到指定数量的数据后触发中断,通过回调函数`HAL_UART_RxCpltCallback`通知用户程序处理数据。这种方式是非阻塞的,可提高程序效率,适合对实时性要求较高的场景,但需要合理管理中断优先级和缓冲区,以避免数据丢失和冲突。
串口DMA(HAL_UARTEx_ReceiveToIdle_DMA)
简单介绍:`HAL_UARTEx_ReceiveToIdle_DMA`是 STM32 HAL 库提供的一个扩展功能,用于在 DMA 模式下接收串口数据,直到检测到空闲字符(即串口空闲一段时间)。它结合了 DMA 的高效数据传输能力和空闲检测机制,能够在接收不定长数据时自动停止接收,非常适合处理如协议帧、用户输入等不定长数据流的场景。
该函数是非阻塞的,启动后程序可以继续执行其他任务,直到接收完成或检测到空闲字符时触发回调函数`HAL_UARTEx_RxEventCallback`,通知用户处理接收到的数据。这种方式不仅提高了数据接收的效率,还减轻了 CPU 的负担,特别适合对实时性和数据处理效率要求较高的应用。
在前面串口中断的基础上选择DMA Settings(DMA设置),然后点击add添加USART_TX和USART_RX,其它设置选择默认即可。
核心代码
#include "main.h"
#include "GPIO_PIN_13.h"
#include "OLED.h"
UART_HandleTypeDef huart2; // UART2句柄,用于串口通信
DMA_HandleTypeDef hdma_usart2_rx; // USART2接收DMA句柄
DMA_HandleTypeDef hdma_usart2_tx; // USART2发送DMA句柄
void SystemClock_Config(void); // 系统时钟配置函数声明
static void MX_GPIO_Init(void); // GPIO初始化函数声明
static void MX_DMA_Init(void); // DMA初始化函数声明
static void MX_USART2_UART_Init(void); // USART2初始化函数声明
uint8_t RxData[256] = {0}; // 定义一个接收缓冲区,大小为256字节,用于存储接收到的数据
int main(void)
{
HAL_Init(); // 初始化HAL库,设置全局变量、中断优先级分组等
SystemClock_Config(); // 配置系统时钟,设置CPU时钟频率等
MX_GPIO_Init(); // 初始化GPIO
MX_DMA_Init(); // 初始化DMA
MX_USART2_UART_Init(); // 初始化USART2 UART
GPIOC_PIN_13_Init(); // 初始化GPIOC的第13引脚,用于控制LED
OLED_Init(); // 初始化OLED显示屏
// 使用扩展函数HAL_UARTEx_ReceiveToIdle_DMA,接收不定长的数据
// 当接收到空闲字符时,触发接收完成事件
HAL_UARTEx_ReceiveToIdle_DMA(&huart2, RxData, sizeof(RxData));
// 关闭传输过半中断(这是HAL库默认开启的,需要我们手动关闭)
// 避免不必要的中断处理
__HAL_DMA_DISABLE_IT(huart2.hdmarx, DMA_IT_HT);
while (1) // 主循环
{
// 主循环为空,所有操作在中断回调函数中完成
}
}
// UART接收事件回调函数,此函数在stm32f1xx_hal_uart.h头文件的784行
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
// 判断是否为USART2的接收事件
if (huart->Instance == USART2)
{
// 将接收到的数据通过DMA发送回去,实现回传功能
HAL_UART_Transmit_DMA(&huart2, RxData, Size);
// 根据接收到的第一个字符控制LED和显示信息
if (RxData[0] == 'H') // 如果接收到的字符为'H'
{
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET); // 点亮LED
OLED_ShowString(1, 1, "LED_OFF"); // 在OLED显示屏的第1行第1列显示"LED_OFF"
}
else if (RxData[0] == 'J') // 如果接收到的字符为'J'
{
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET); // 熄灭LED
OLED_ShowString(1, 1, "LED__ON"); // 在OLED显示屏的第1行第1列显示"LED__ON"
}
// 在OLED显示屏的第2行第1列显示接收到的数据
// 注意:将RxData转换为char指针,因为OLED_ShowString函数可能需要char类型参数
OLED_ShowString(2, 1, (char*)RxData);
// 重新启动接收不定长数据的功能
HAL_UARTEx_ReceiveToIdle_DMA(&huart2, RxData, sizeof(RxData));
// 再次关闭传输过半中断,确保不会被意外触发
__HAL_DMA_DISABLE_IT(huart2.hdmarx, DMA_IT_HT);
}
}
串口DMA的优点
1. 高效的数据传输:DMA可以直接在硬件层面完成数据的传输,无需CPU干预,大大提高了数据传输效率。特别适合处理大量数据,如文件传输或连续数据流。
2. 减轻CPU负担:CPU不需要频繁地处理串口数据的读取和写入,可以专注于执行其他任务,提高系统的整体性能。在多任务系统中,可以显著提高系统的响应速度和稳定性。
3. 实时性增强:DMA可以快速响应串口事件,确保数据的及时处理,适合对实时性要求较高的应用。例如,在工业自动化或传感器数据采集场景中,可以快速处理串口数据。
4. 灵活性高:可以根据需要配置DMA的传输大小、方向等参数,适应不同的应用场景。可以与中断或轮询方式结合使用,进一步优化系统性能。
串口DMA的缺点
1. 复杂性增加:需要配置DMA通道,包括源地址、目标地址、传输方向等,代码复杂度较高。需要处理DMA中断,编写中断处理函数,增加了开发难度。
2. 调试困难: DMA相关的错误(如地址错误、传输错误等)较难调试,需要使用专业的调试工具和方法。需要确保DMA和串口的配置正确,否则可能导致数据丢失或系统不稳定。
3. 资源占用:DMA需要占用硬件资源,如DMA通道和内存。在资源有限的系统中,可能需要合理分配DMA通道,避免资源冲突。
4. 对硬件要求高:需要硬件支持DMA功能,且硬件的DMA控制器需要足够强大。需要确保硬件的稳定性,硬件故障可能导致DMA传输失败。
总结
串口DMA是一种高效的串口通信方式,特别适合处理大量数据或对实时性要求较高的场景。它能够显著减轻CPU的负担,提高系统的整体性能。然而,使用DMA也增加了代码的复杂性和调试难度,需要合理配置和调试。在实际应用中,可以根据需求选择是否使用DMA,或者结合中断和轮询方式优化系统性能。
总结
`HAL_UART_Receive`、`HAL_UART_Receive_IT`和`HAL_UARTEx_ReceiveToIdle_DMA`是 STM32 HAL 库中用于 UART 数据接收的三种不同方式,各有特点和适用场景。
`HAL_UART_Receive`:阻塞式接收函数,程序会停留在接收操作上,直到数据接收完成或超时。适合数据量小、对实时性要求不高的场景,优点是实现简单,缺点是会阻塞程序执行,降低 CPU 资源利用率。
`HAL_UART_Receive_IT`:中断驱动的非阻塞接收函数,启动后程序可以继续执行其他任务,接收完成后通过回调函数通知用户。适合对实时性要求较高的场景,能够提高程序效率,但需要合理管理中断优先级。
`HAL_UARTEx_ReceiveToIdle_DMA`:结合 DMA 和空闲检测的接收方式,适合接收不定长数据流。它利用 DMA 的高效传输能力,减轻 CPU 负担,同时通过空闲检测自动停止接收。这种方式适合处理协议帧等不定长数据,效率高且实时性强,但实现相对复杂,需要配置 DMA 和处理回调函数。
三种方式各有优缺点,开发者可以根据实际需求选择合适的接收方式。