嵌入式串口通信高阶实战(1/3)——DMA+串口空闲中断+环形队列
🔥 嵌入式串口通信高阶实战(1/3)— DMA+串口空闲中断+环形队列📌 解决传统方案的三大痛点:轮询方式CPU占用率100%问题字节中断导致的系统实时性崩塌DMA直接接收的数据覆盖风险💡 核心技术组合:串口空闲中断精准帧检测DMA双缓冲半/全传输中断机制环形队列实现生产-消费模型
在嵌入式开发领域,串口通信是最基础却又最考验工程师功底的技能之一。以下我将分享我在实际项目中的串口开发经验,如何去构建工业级串口通信框架。
一、传统方案
很多初学者停留在基本的轮询或字节中断等低效处理方式,面对复杂场景(高速率、多任务、实时性)时往往就不太适用。
我们的高效在哪里? 不急,先来看看传统方案怎么做的:
1. 轮询
while(1) {
if(USART_GetFlagStatus(USART1, USART_FLAG_RXNE)) {
uint8_t data = USART_ReceiveData(USART1);
// 处理接收到的字节
}
// 其他任务...
}
核心弊端:
- CPU资源浪费:无数据时CPU也在空转
- 数据丢失风险:轮询间隔若大于串口字节间隔,会导致数据丢失
- 实时性差:响应时间取决于其他任务执行时间
2. 字节中断
void USART1_IRQHandler(void) {
if(USART_GetITStatus(USART1, USART_IT_RXNE)) {
uint8_t data = USART_ReceiveData(USART1);
// 处理接收到的字节
}
}
核心弊端:
- CPU 负载超标:每来一个字节就要进出一次中断,115200波特率下1秒钟岂不是要进出一万多次
- 数据帧解析复杂:需要一个字节一个字节去拼接来解析协议桢
3. DMA直接处理
void DMA1_Channel5_IRQHandler(void) {
if (DMA_GetITStatus(DMA1_IT_TC5)) {
DMA_ClearITPendingBit(DMA1_IT_TC5);
Process_Received_Data(rx_buf, RX_BUF_SIZE); //处理DMA缓冲区数据
DMA_SetCurrDataCounter(DMA1_Channel5, RX_BUF_SIZE);
DMA_Cmd(DMA1_Channel5, ENABLE);
}
}
核心弊端: 如果 数据处理速度 < 数据接收速度,导致数据覆盖丢失
二、本方案设计
1. 方案介绍
DMA(DMA半/全传输中断) + 串口空闲中断 + 环形队列
名词解释:
-
串口空闲中断:仅在接收数据后总线变为空闲时触发。 那么空闲多久再触发呢?空闲时间的定义是达到 1 个字节的传输时间,如果是 1 起始位 + 8 数据位 + 1 停止位,那么空闲时间 t = 10bit / 波特率。 9600bps下≈1ms 115200bps≈87us
-
DMA半传输中断:DMA 传输完成一半数据时触发。例如DMA缓冲区大小为1024,那么在传输到512字节时会触发半传输中断
-
DMA全传输中断:DMA 完成所有数据传输时触发。
Tip: 像DMA半/全传输中断更多的常同于双缓冲机制。 什么是双缓冲机制?也就是将DMA缓冲区设定为二维数组buf[2][N],在DMA开始工作是会先将数据存放到buf[0]中,当触发DMA半传输中断时,即表示buf[0]已存满,系统会转到buf[1]中缓存,这时你就可以及时处理buf[0]中的数据了,当触发DMA全传输中断时,即表示buf[1]已存满,系统会转到buf[0]中缓存,这时你就可以及时处理buf[1]中的数据了。即实现无缝数据流采集和并行处理。
现在讲一下:如何接收一帧完整数据?流程是怎么样的?
分三种情况讲:
- 第一种: 0 < 一帧的数据量大小 < 512, --------处理方式:串口空闲中断
- 第二种:512 < 一帧的数据量大小 < 1024, --------处理方式:DMA半传输中断 + 串口空闲中断
- 第三种:1024 < 一帧的数据量大小 , --------处理方式:DMA半传输中断 + DMA全传输中断 + 串口空闲中断
2. 代码设计
#define DMA_RX_BUF_SIZE 1024
static uint8_t dma_rx_buf[DMA_RX_BUF_SIZE]; //DMA缓冲区
static uint16_t last_dma_rx_size = 0; //记录上一次接收到的数据量大小
static uint8_t queue_buf[DMA_RX_BUF_SIZE]; //队列缓冲区
static ring_queue_t queue_rx = { //环形队列
.wrIdx = 0,
.rdIdx = 0,
.size = DMA_RX_BUF_SIZE,
.buf = queue_buf ,
};
static uint16_t uart_get_dma_rx_buf_remain_size(void)
{
return DMA_GetCurrDataCounter(DMA1_Channel5); //返回DMA缓冲区中未使用的空间大小
}
//DMA中断处理函数
void DMA1_Channel5_IRQHandler(void)
{
if(DMA_GetIntStatus(DMA1_INT_TXC5, DMA1)) //DMA全传输中断
{
dmarx_done_isr(); //DMA全传输中断的处理函数
DMA_ClearFlag(DMA1_FLAG_TC5, DMA1); //清除标志位
}
if(DMA_GetIntStatus(DMA1_INT_HTX5, DMA1)) //DMA半传输中断
{
dmarx_half_done_isr(); //DMA半传输中断的处理函数
DMA_ClearFlag(DMA1_FLAG_HT5, DMA1); //清除标志位
}
}
//串口中断处理函数
void USART1_IRQHandler(void)
{
if(USART_GetIntStatus(USART1, USART_INT_IDLEF) != RESET) //串口空闲中断
{
uart_idle_isr(); //串口空闲中断的处理函数
USART_ClrIntPendingBit(USART1, USART_INT_IDLEF); //清除标志位
}
}
//DMA全传输中断 处理函数
static void dmarx_done_isr(void)
{
uint16_t handle_size;
handle_size = DMA_RX_BUF_SIZE - last_dma_rx_size;
write_block_queue(&queue_rx, (const uint8_t *)&dma_rx_buf[last_dma_rx_size], handle_size);
last_dma_rx_size = 0;
}
// 0_ _ _ _ _ _ _512_ _ _ _ _ _ _1024
//DMA半传输中断 处理函数
static void dmarx_half_done_isr(void)
{
uint16_t recv_total_size; // 总接收大小
uint16_t handle_size; // 本段需要处理的数据量大小
recv_total_size = DMA_RX_BUF_SIZE - uart_get_dma_rx_buf_remain_size();
handle_size = recv_total_size - last_dma_rx_size;
write_block_queue(&queue_rx, (const uint8_t *)&dma_rx_buf[last_dma_rx_size], handle_size);
last_dma_rx_size = recv_total_size;
}
//串口空闲中断 处理函数
static void uart_idle_isr(void)
{
uint16_t recv_total_size; // 总接收大小
uint16_t handle_size; // 本段需要处理的数据量大小
recv_total_size = DMA_RX_BUF_SIZE - uart_get_dma_rx_buf_remain_size();
handle_size = recv_total_size - last_dma_rx_size;
write_block_queue(&queue_rx, (const uint8_t *)&dma_rx_buf[last_dma_rx_size], handle_size);
last_dma_rx_size = recv_total_size;
}
3. 视频讲解
嵌入式串口通信高阶实战(1/3)——DMA+串口空闲中断+环形队列
嵌入式串口通信高阶实战(1/3)——DMA+串口空闲中断+环形队列
三、技术交流
感兴趣同学联系主页wx(Lntt-xbc)进交流群
更多推荐



所有评论(0)