stm32 高效串口收发
Posted nepqiu
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了stm32 高效串口收发相关的知识,希望对你有一定的参考价值。
文章目录
串口通讯
串口
串行接口简称串口,也称串行通信接口或串行通讯接口(通常指COM接口),是采用串行通信方式的扩展接口。串行接口(SerialInterface)是指数据一位一位地顺序传送,其特点是通信线路简单,只要一对传输线就可以实现双向通信(可以直接利用电话线作为传输线),从而大大降低了成本,特别适用于远距离通信,但传送速度较慢。
-
USART(universal synchronous asynchronous receiver and transmitte): 通用同步异步收发器
- USART是一个串行通信设备,可以灵活地与外部设备进行全双工数据交换。
-
UART(universal asynchronous receiver and transmitter): 通用异步收发器
- 异步串行通信口(UART)就是我们在嵌入式中常说的串口,它还是一种通用的数据通信议。
区别:
USART是指单片机的一个端口模块,可以根据需要配置成同步模式(SPI,I2C),也可以将其配置为异步模式,后者就是UART。所以说UART姑且可以称之为一个与SPI,I2C对等的“协议”,而USART则不是一个协议,而是更应该理解为一个实体。
相比于同步通讯,UART不需要统一的时钟线,接线更加方便。但是,为了正常的对信号进行解码,使用UART通讯的双方必须事先约定好波特率,即单位事件内传输码元的个数。
补充:
在电子通信领域,波特(Baud)即调制速率,指的是有效数据讯号调制载波的速率,即单位时间内载波调制状态变化的次数。它是对符号传输速率的一种度量,1波特即指每秒传输1个符号,而透过不同的调制方式,可以在一个码元符号上负载多个bit位信号。[1]“波特”(Baud)本身已是速率,所以不需要写成 Baud Rate(Rate 是赘字)。单位“波特”本身就已经是代表每秒的调制数,以“波特每秒”(Baud per second)为单位是一种常见的错误,但是在一般中文口语化的沟通上还是常以“波特率”来描述“波特”(Baud)。
USART 中断
USART 中断事件被连接到相同的中断向量:
- 发送期间:发送完成、清除以发送或发送数据寄存器为空中断。
- 接收期间:空闲线路检测、上溢错误、接收数据寄存器不为空、奇偶校验错误、LIN 断路 检测、噪声标志(仅限多缓冲区通信)和帧错误(仅限多缓冲区通信)
串口模式配置
使用 DMA 进行连续通信
USART 能够使用 DMA 进行连续通信。接收缓冲区和发送缓冲区的 DMA 请求是独立的。
使用 DMA 进行发送
使用 DMA 进行发送 将 USART_CR3 寄存器中的 DMAT 位置 1 可以使能 DMA 模式进行发送。当 TXE 位置 1 时,可将数据从 SRAM 区(通过 DMA 配置,参见 DMA 部分)加载到 USART_DR 寄存器。要映射一个 DMA 通道以进行 USART 发送,请按以下步骤操作(x 表示通道编号):
-
在 DMA 控制寄存器中写入 USART_DR 寄存器地址,将其配置为传输的目标地址。每次发生 TXE 事件后,数据都会从存储器移动到此地址。
-
在 DMA 控制寄存器中写入存储器地址,将其配置为传输的源地址。每次发生 TXE 事件后,数据都会从这个存储区域加载到 USART_DR 寄存器中。
-
在 DMA 控制寄存器中配置要传输的总字节数。
-
在 DMA 寄存器中配置通道优先级。
-
根据应用的需求,在完成一半或全部传输后产生 DMA 中断。
-
向 SR 寄存器中的 TC 位写入 0,将其清零。
-
在 DMA 寄存器中激活该通道。
当达到在 DMA 控制器中设置的数据传输量时,DMA 控制器会在 DMA 通道的中断向量上产生一个中断。
在发送模式下,DMA 对所有要发送的数据执行了写操作(DMA_ISR 寄存器中的 TCIF 标志置 1)后,可以对 TC 标志进行监视,以确保 USART 通信已完成。在禁止 USART 或进入停止模式前必须执行此步骤,以避免损坏最后一次发送。软件必须等待直到 TC=1。TC 标志在所有数据发送期间都必须保持清零状态,然后在最后一帧发送结束后由硬件置 1。
使用 DMA 进行接收
将 USART_CR3 寄存器中的 DMAR 位置 1 可以使能 DMA 模式进行接收。接收数据字节时,数据会从 USART_DR 寄存器加载到 SRAM 区域中(通过 DMA 配置,参见 DMA 规范)。要映射一个 DMA 通道以进行 USART 接收,请按以下步骤操作:
-
在 DMA 控制寄存器中写入 USART_DR 寄存器地址,将其配置为传输的源地址。每次发生 RXNE 事件后,数据都会从此地址移动到存储器。
-
在 DMA 控制寄存器中写入存储器地址,将其配置为传输的目标地址。每次发生 RXNE 事件后,数据都会从 USART_DR 寄存器加载到此存储区。
-
在 DMA 控制寄存器中配置要传输的总字节数。
-
在 DMA 控制寄存器中配置通道优先级。
-
根据应用的需求,在完成一半或全部传输后产生中断。
-
在 DMA 控制寄存器中激活该通道。
当达到在 DMA 控制器中设置的数据传输量时,DMA 控制器会在 DMA 通道的中断向量上产生一个中断。在中断子程序中,USART_CR3 寄存器中的 DMAR 位应由软件清零。注意: 如果 DMA 用于接收,则不要使能 RXNEIE 位
多缓冲区通信中的错误标志和中断生成
在多缓冲区通信中,如果事务中发生任何错误,都会在当前字节后放置错误标志。如果中断使 能置 1,则会产生中断。在单字节接收过程中,与 RXNE 一同置位的帧错误、上溢错误和噪声 标志具有单独的错误标志中断使能位(USART_CR3 寄存器中的 EIE 位);如果该位置 1, 则会因其中任何一个错误而在当前字节后产生中断。
编程
接收的方式:
- DMA接收中断接收
- DMA+串口+DMA空闲中断接收
- DMA双缓冲区+串口+DMA空闲中断接收
- DMA+串口+DMA空闲中断+环形队列接收
发送的方式:
- DMA+串口发送
- 单串口发送
- DMA+串口发送+环形队列(双缓冲)
- 动态内存分配的FIFIO
下面主要用 环形队列+DMA+非动态内存分配+IDLE中断
建议先看最下面的参考文章
接收流程
USART1 + DMA + IDLE中断 +无锁队列
主函数:
int main(void)
{
uint8_t buff_read[32];
uint32_t length;
usart1_init();
while (1)
{
length = fifo_read_buff(pfifo_x, buff_read, 32);
if (length)
{
printf("lengtt = %d", length); // 实际接收的数据长度
//对接收的数据进行处理
}
else
{
printf("no data rx");// 没有数据
}
if (pfifo_x->error)
{
printf("fifo error %d", pfifo_x->error);// 接收错误
pfifo_x->error = 0;
}
}
}
中断处理函数:
void USART1_IRQHandler(void) // 接收数据中断
{
__IO uint8_t Len = 0;
//发送完成中断
/*
* DMA中断时,只表示需要传送的所有数据字节全部传送到串口的发送数据寄存器中了。
* 此时串口实际上还有2个字节并未发送完成,数据寄存器和移位寄存器中的2个字节还需要发送,并不能关闭串口发送。
* 同理,如果是485切换方向,必须要等到发送完成,也就是移位寄存器发送完成-TC标志置位。
*
* TXE指的是发送缓冲器DR空,TC指的是SHIFT移位寄存器空。
* DMA完成只是代表把最后一个字节送到DR寄存器里面了,此时SHIFT移位寄存器有1个字节正在开始发送,
* DR寄存器里面有一个字节等待发送,所以就是2个字节未发送完成。
*/
if (USART_GetITStatus(USART1, USART_IT_TC) == SET)
{
USART_ClearITPendingBit(USART1, USART_IT_TC);
USART_ITConfig(USART1, USART_IT_TC, DISABLE);
DMA2_Stream7_working = 0;
}
//总线空闲中断
if (USART_GetITStatus(USART1, USART_IT_IDLE) != RESET) //触发中断标志位
{
Len = USART1->SR; //清除RXNE标志
Len = USART1->DR; //清USART_IT_IDLE标志
Len = DMA_GetCurrDataCounter(DMA2_Stream5); //获取当前剩余数据量大小的函数
if (pfifo_1 != 0)
{
// Len为当前接收索引
pfifo_1->in += ((pfifo_1->last_cnt - Len) & (pfifo_1->size - 1)); //更新in
pfifo_1->last_cnt = Len;
if ((pfifo_1->in - pfifo_1->out) > pfifo_1->size)
{
pfifo_1->out = pfifo_1->in; // 清空缓存,注意赋值顺序,pfifo->in = pfifo->out 是错误的
pfifo_1->error |= FIFO_DMA_ERROR_RX_FULL;
}
}
else
{
pfifo_1->error |= FIFO_DMA_ERROR_RX_POINT_NULL;
}
}
}
初始化(标准库)
#define USART1_RX_LEN 32
#define USART1_TX_LEN 32
uint8_t Usart1_Rx[USART1_RX_LEN] = {0};
uint8_t Usart1_Tx[USART1_TX_LEN] = {0};
uint8_t Usart1_Tx_Buffer[USART1_TX_LEN] = {0};
fifo_rx_def fifo_usart_rx_1;
fifo_rx_def *pfifo_1 = &fifo_usart_rx_1;
fifo_rx_def fifo_usart_tx_1;
uint8_t DMA2_Stream7_working = 0;
void usart1_init(void)
{
/* -------------- Enable Module Clock Source ----------------------------*/
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); //使能GPIOA时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); //使能USART1时钟
GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_USART1); //GPIOA9复用为USART1
GPIO_PinAFConfig(GPIOA, GPIO_PinSource10, GPIO_AF_USART1); //GPIOA10复用为USART1
/* -------------- Configure GPIO ---------------------------------------*/
{
GPIO_InitTypeDef GPIO_InitStruct;
NVIC_InitTypeDef NVIC_InitStructure;
USART_InitTypeDef USART1_InitStruct;
//USART1端口配置
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_10; //GPIOA9与GPIOA10
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF; //复用功能
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; //速度50MHz
GPIO_InitStruct.GPIO_OType = GPIO_OType_PP; //推挽复用输出
GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_UP; //上拉
GPIO_Init(GPIOA, &GPIO_InitStruct); //初始化PA9,PA10
//USART1 初始化设置
USART_DeInit(USART1);
USART1_InitStruct.USART_BaudRate = 115200; //波特率设置
USART1_InitStruct.USART_WordLength = USART_WordLength_8b; //字长为8位数据格式
USART1_InitStruct.USART_StopBits = USART_StopBits_1; //一个停止位
USART1_InitStruct.USART_Parity = USART_Parity_No; //无奇偶校验位
USART1_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //无硬件数据流控制
USART1_InitStruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //收发模式
USART_Init(USART1, &USART1_InitStruct); //初始化串口1
USART_Cmd(USART1, ENABLE); //使能串口1
USART_ClearFlag(USART1, USART_FLAG_TC);
//Usart1 NVIC 配置
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; //串口1中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化VIC寄存器
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //开启相关中断
USART_ITConfig(USART1, USART_IT_TC, DISABLE);
USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);
}
/* -------------- Configure DMA -----------------------------------------*/
/* 发送 */
{
DMA_InitTypeDef DMA_InitStruct;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);
//发送数据
DMA_Cmd(DMA2_Stream7, DISABLE); //关闭DMA
DMA_DeInit(DMA2_Stream7); //重置为缺省值
DMA_InitStruct.DMA_Channel = DMA_Channel_4;
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t) & (USART1->DR); //源地址
DMA_InitStruct.DMA_Memory0BaseAddr = (uint32_t)(Usart1_Tx); //目的地址
DMA_InitStruct.DMA_DIR = DMA_DIR_MemoryToPeripheral; //数据传输方向为外设到内存
DMA_InitStruct.DMA_BufferSize = USART1_TX_LEN; //设置数据的缓冲大小
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址不变
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable; //内存缓冲区地址自加
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //8位字节传输
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //数据宽度为8位
DMA_InitStruct.DMA_Mode = DMA_Mode_Normal; //工作在正常模式
DMA_InitStruct.DMA_Priority = DMA_Priority_VeryHigh; //最高优先级
DMA_InitStruct.DMA_FIFOMode = DMA_FIFOMode_Disable;
DMA_InitStruct.DMA_FIFOThreshold = DMA_FIFOThreshold_1QuarterFull;
DMA_InitStruct.DMA_MemoryBurst = DMA_MemoryBurst_Single;
DMA_InitStruct.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
DMA_Init(DMA2_Stream7, &DMA_InitStruct);
NVIC_InitStructure.NVIC_IRQChannel = DMA2_Stream7_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
DMA_ITConfig(DMA2_Stream7, DMA_IT_TC, ENABLE); //开启发送完成中断
DMA_Cmd(DMA2_Stream7, DISABLE);
USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE); //使能串口的DMA发送
if (fifo_init(&fifo_usart_tx_1, Usart1_Tx_Buffer, USART1_TX_LEN) == -1)
{
// 必须 2 的幂次方
}
}
// 接收数据
{
DMA_InitTypeDef DMA_InitStruct;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);
DMA_Cmd(DMA2_Stream5, DISABLE); //关闭DMA
while (DMA_GetCmdStatus(DMA2_Stream5) != DISABLE)
;
DMA_DeInit(DMA2_Stream5); //重置为缺省值
DMA_InitStruct.DMA_Channel = DMA_Channel_4;
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t) & (USART1->DR); //源地址
DMA_InitStruct.DMA_Memory0BaseAddr = (uint32_t)(Usart1_Rx); //目的地址
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralToMemory; //数据传输方向为外设到内存
DMA_InitStruct.DMA_BufferSize = USART1_RX_LEN; //设置数据的缓冲大小
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址不变
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable; //内存缓冲区地址自加
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //8位字节传输
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //数据宽度为8位
DMA_InitStruct.DMA_Mode = DMA_Mode_Circular; //工作在循环缓存模式
DMA_InitStruct.DMA_Priority = DMA_Priority_VeryHigh; //最高优先级
DMA_InitStruct.DMA_FIFOMode = DMA_FIFOMode_Disable;
DMA_InitStruct.DMA_FIFOThreshold = DMA_FIFOThreshold_1QuarterFull;
DMA_InitStruct.DMA_MemoryBurst = DMA_Mode_Normal; //DMA_MemoryBurst_Single;//
DMA_InitStruct.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
DMA_Init(DMA2_Stream5, &DMA_InitStruct);
DMA_Cmd(DMA2_Stream5, ENABLE);
USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);
if (fifo_init(pfifo_1, Usart1_Rx, USART1_RX_LEN) == -1)
{
// 必须 2 的幂次方
}
}
}
发送流程
在设计串口驱动过程中,要遵循的准则是:
- 尽量的减少程序运行时间
- 尽量的减少程序所占的内存
开启串口发送完成中断
void USART1_IRQHandler(void) // 接收数据中断
{
__IO uint8_t Len = 0;
//发送完成中断
/*
* DMA中断时,只表示需要传送的所有数据字节全部传送到串口的发送数据寄存器中了。
* 此时串口实际上还有2个字节并未发送完成,数据寄存器和移位寄存器中的2个字节还需要发送,并不能关闭串口发送。
* 同理,如果是485切换方向,必须要等到发送完成,也就是移位寄存器发送完成-TC标志置位。
*
* TXE指的是发送缓冲器DR空,TC指的是SHIFT移位寄存器空。
* DMA完成只是代表把最后一个字节送到DR寄存器里面了,此时SHIFT移位寄存器有1个字节正在开始发送,
* DR寄存器里面有一个字节等待发送,所以就是2个字节未发送完成。
*/
if (USART_GetITStatus(USART1, USART_IT_TC) == SET)
{
USART_ClearITPendingBit(USART1, USART_IT_TC);
USART_ITConfig(USART1, USART_IT_TC, DISABLE);
DMA2_Stream7_working = 0;
}
}
开启DMA发送完成中断
//uint8_t DMA2_Stream7_working = 0;
/**
* @brief 发送完成中断
* @param[in] void
* @retval void
*/
/*
* ST官方都有APPNOTE指导的(对于UART没有RS485功能的单片机型号而言):
* 1、启动DMA前,先关闭UART发送完成中断,并清除发送完成中断标志;
* 2、在DMA传输完成中断函数中,开启UART发送完成中断;
* 3、在UART发送完成中断函数中,切换RS485为接收态;
*/
void DMA2_Stream7_IRQHandler(void)
{
if (DMA_GetITStatus(DMA2_Stream7, DMA_IT_TCIF7) != RESET)
{
DMA_ClearFlag(DMA2_Stream7, DMA_IT_TCIF7);
DMA_Cmd(DMA2_Stream7, DISABLE); //不使能传输
//while (DMA_GetCmdStatus(DMA2_Stream7) != DISABLE);
USART_ITConfig(USART1, USART_IT_TC, ENABLE);
//DMA2_Stream7_working = 0;
}
}
DMA发送函数
//uint8_t DMA2_Stream7_working = 0;
/**
* @brief 串口一+DMA 发送
* @param[in] *data: 需要发送的数据
* @param[in] len: 数据的大小
* @retval void
*/
uint32_t usart1_dma_send(uint8_t *data, uint16_t len)
{
uint32_t result = fifo_write_buff(&fifo_usart_tx_1, data, len); //将数据放入循环缓冲区
//result != 0 说明放入数据成功 DMA2_Stream6_working == 0 说明缓冲区里面没有数据
if (DMA2_Stream7_working == 0 && result != 0)
{
len = fifo_read_buff(&fifo_usart_tx_1, Usart1_Tx, USART1_TX_LEN); //从循环缓冲区获取数据
DMA_SetCurrDataCounter(DMA2_Stream7, len); //设定传输长度
DMA_Cmd(DMA2_Stream7, ENABLE); //使能传输
DMA2_Stream7_working = 1;
}
if (result == len)
{
return len;
}
else
{
return result;
}
}
解析
https://www.amobbs.com/thread-4516795-1-1.html
程序1
/*指针是指向ptr,需要发送count个数据*/
void USART1WriteDataToBuffer(*ptr,u8 count)
{
/*判断数据是否发送完毕*/
while(count--)
{
/*发送数据*/
USART1SendByte(*ptr++);
/*等待这个数据发送完毕,然后进入下一个数据的发送过程*/
while(USART_GetFlagStatus(USART1, USART_FLAG_TC);
}
/*数据发送完毕,返回*/
}
这段程序会在实际应用中产生灾难性的后果。首先,但发送数据送到寄存器启动后,CPU就一直在等待这个数据发送完成,这样,直到所有要发送的数据完成,CPU才能做其他事情。相对于CPU内核运行的速度而言,串口外设的运行速度是非常快的,让一个非常快的设备去等待相对很慢的设备,程序的效率是非常底下的。
所以尽量采取中断的方式去发送数据
程序2
/*将数据写入发送缓冲区*/
void USART1WriteDataToBuffer(*ptr,u8 count)
{
while (count != '\\0')
{
USART1SendTCB[Index++] = *ptr++;
Count = count;
}
/***判断溢出等其他代码省略***/
}
/***发送中断的ISR***/
void USART1SendUpdate(void)
{
/***判断发送缓冲区中的数据是否发送完毕***/
/***将发送缓冲区的数据发送出去***/
USART1SendByte( *ptr++ ); /***发送指针加一,待发送的字节数减一等代码***/
}
这样,当调用USART1WriteDataToBuffer
这个函数的时候,我们将要发送的数据写入发送缓冲区,CPU就可以执行其他任务了,待一个数据发送完成以后,中断ISR就会触发,在中断服务程序中将下一个数据写入发送寄存器,启动下一次发送,直到完全发送完毕。
但是在实际工程应用中,经常会遇到这种类似的情况,串口显示屏需要显示1000个点,通过串口发送这1000个点的颜色的RGB年度数值。将这1000个数据写入发送寄存器以后,启动发送。在115200的波特率,一位起始位,一位停止位,在无校验位的情况下,至少需要(10*1000*2)/115200=0.1736
秒,在这段期以内,时钟更新了,需要再发送一串时间更新数据,这个数据大约有100个,这样这串数据需要写入到发送缓冲区的发送字节后面。
同样的道理,在这个时候如果有显示任务更新的话,将会有其他的数据写入到缓冲区。
从图上可以看出,程序2虽然满足了时间上的要求,却没有满空间上的要求,它的数据缓冲区的单向的,这样,当发送缓冲区撑满了以后才能将发送发送缓冲区的数据清空,以便下次的缓冲数据。这样内存较小的嵌入式系统来说是不能容忍的。
因此,也可以将发送缓冲区建立一个环形缓冲区,在这个缓冲区内,通过头指针(HostIndex)和尾指针(HostIndex)来定位空白区和数据区。
- 头指针:指向有数据区的顶部,每次写入数据,都更新头指针,如果到了缓冲区的末端,就自动返回到缓冲区的起始处(StartIndex),直到写入到尾指针为止,这时缓冲区已经被装满,不能再装入数据。
- 尾指针:指向有数据区的尾部,当数据发送完毕后,更新尾指针的位置,如果到了缓冲区的末端(EndIndex),就自动返回到缓冲区的起始处(StartIndex),直到遇到头指针为止,这是证明所有的数据已经发送完毕。
这样就实现了发送缓冲区的动态调整空白区和数据区,刚刚发送完毕的数据,马上就被开辟出来用于存放下一个数据,最大可能的节省了宝贵的发送缓冲区的空间,提高了效率。
fifo_buff
代码
fifo_buff.c
/**
******************************************************************************
* @file fifo_buff.c/h
* @brief 无锁队列
* @note
* @history 2021.07.08
*
@verbatim
==============================================================================
利用 串口+DMA+IDLE中断+无锁队列,提高串口接收效率 (参考《鱼鹰谈单片机公众号》)
1、串口初始化函数一旦执行完成,串口以上是关于stm32 高效串口收发的主要内容,如果未能解决你的问题,请参考以下文章