DMA+串口空闲中断(开放源码下载)
1)空闲中断+DMA实现高速数据传输
为什么要用DMA+空闲中断?
①在使用串口接受数据时,在使用串口通信外设,如GPS、WiFi、2/3/4G模块等,我们常用的是接收中断,每接收一个字节进入中断,导致这段时间CPU不能做其他事情,降低了效率。STM32的DMA不需要CPU控制,让CPU能够在接收数据的同时去做其他事情。
②接收不定长数据。如果数据定长,我们使用串口接收中断,只需要判断数据的个数就能知道数据是否接收完成。但是当数据不定长,就无法判断了。比如GPS的数据,数据量相对比较大,如果使用串口接收中断,会降低效率,难以判断数据是否接收完成。这时候,使用DMA+空闲传输,让DMA这个工具人去做搬运数据的活,CPU就继续做其他的事情,当进入空闲中断,就知道数据传输完成了,CPU去处理DMA缓存数据就行了。
我会把F1/F4系列的源码贴在最后面,大家需要自行下载。
DMA相关
DMA(Direct Memory Access,直接存储区访问)为实现数据高速在外设寄存器与存储器之间或者存储器与存储器之间传输提供了高效的方法。之所以称之为高效,是因为 DMA 传输实现高速数据移动过程无需任何 CPU 操作控制。从硬件层次上来说,DMA 控制器是独立于 Cortex-M3/M4 内核的,有点类似 GPIO、USART 外设一般,只是 DMA 的功能是可以快速移动内存数据。
所以,DMA就是数据的搬运工,不需要CUP控制,自己就能够高速搬运数据,高效工具人。可以减轻CPU的压力,在DMA搬运数据的同时,CPU可以去做其他的事情,当搬运完毕,我们去处理数据就可以了。
更多关于DMA的知识就不多做阐述,论坛有很多资料参考。
串口空闲中断
串口空闲中断的事件标志是IDLE,进入这个中断事件,表示串口本次的接收任务已经完成了。
理论就讲这么多,上代码开搞(注:F1系列DMA和F4系列不一样,我使用的是F4系列,标准库)
首先是串口配置部分:
只需要把接收中断RXNE修改成IDLE,使能DMA接收就行了。
USART_ITConfig(USART1, USART_IT_IDLE, ENABLE); //使能串口中断接收
USART_DMACmd(USART1,USART_DMAReq_Rx,ENABLE); //使能串口1 DMA接收
然后是DMA配置部分(F1和F4有些区别)
我们先来看看DMA请求映射,USART1_RX是数据流2,通道4。
代码如下
DMA_InitTypeDef DMA_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE); //使能DMA传输
DMA_DeInit(DMA2_Stream2); //数据流2
DMA_InitStructure.DMA_Channel = DMA_Channel_4; //通道4
DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)&USART1->DR; //DMA外设ADC基地址
DMA_InitStructure.DMA_Memory0BaseAddr =(u32)DMA_Rec_Buf;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory; //数据传输方向,从外设读取发送到内存
DMA_InitStructure.DMA_BufferSize = DMA_REC_LEN; //DMA通道的DMA缓存的大小
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址寄存器不变
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //内存地址寄存器递增
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //数据宽度为8位
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //数据宽度为8位
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //工作在正常缓存模式
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; //DMA通道 x拥有中优先级
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; //循环模式
DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh; // 优先级:中
DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable; /*禁用FIFO*/
DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full; // 禁止内存到内存的传输
DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single; /*存储器突发传输 1个节拍*/
DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single; /*外设突发传输 1个节拍*/
DMA_ClearFlag(DMA2_Stream2,DMA_FLAG_TCIF2);
DMA_ITConfig(DMA2_Stream2, DMA_IT_TE, ENABLE);
DMA_Init(DMA2_Stream2, &DMA_InitStructure); //根据DMA_InitStruct中指定的参数初始化DMA的通道
DMA_Cmd(DMA2_Stream2, ENABLE); //正式驱动DMA传输
接下来是串口中断服务函数
void USART1_IRQHandler (void)
{
if(USART_GetITStatus(USART1, USART_IT_IDLE) != RESET) //USART_IT_IDLE空闲中断
{
USART_ReceiveData(USART1);//这句必须要,否则不能够清除中断标志位。
USART_RX_CNT = DMA_REC_LEN - DMA_GetCurrDataCounter(DMA2_Stream2); //总的长度-没有用的长度=本次接收到的数据长度
//***********帧数据处理函数************//
USART_Send((unsigned char*)DMA_Rec_Buf, USART_RX_CNT);//将接收到的数据重新发送给电脑
//*************************************//
USART_ClearITPendingBit(USART1, USART_IT_IDLE); //清除中断标志
DMA_Start(); //恢复DMA指针,等待下一次的接收
USART_Rec_callback();//数据转存
}
}
void DMA_Start(void)//开启一次DMA传输
{
DMA_Cmd(DMA2_Stream2, DISABLE ); //关闭USART1 TX DMA1 所指示的通道
DMA_SetCurrDataCounter(DMA2_Stream2,DMA_REC_LEN);//重新赋值DMA通道的DMA缓存的大小
DMA_Cmd(DMA2_Stream2, ENABLE); //使能USART1 TX DMA1 所指示的通道
}
2)数据转存(便于用户处理)
在串口空闲中断服务函数里,有一个USART_Rec_callback()函数,是用来转存DMA数据的回调函数。我们将串口接收到的数据通过DMA搬运到DMA缓存,如果要处理数据,还需要将DMA缓存的数据读取出来,放到处理数据的缓存Rec里。因为如果这一次的数据还没来得及处理,下一次的数据又来了,会将这一次DMA缓存的数据覆盖。通过转存,可以缓存多条数据,相对保证不丢包。
//首先定义一个结构体
typedef struct node
{
u16 Rec_len;//缓存数据长度
u8 Rec[512];//缓存大小
u8 flag;//转存完成标志
}USART_Rec;
USART_Rec REC;//定义结构体变量
void USART_Rec_callback(void)//转存函数,把DMA缓存的数据取出来
{
if(REC.Rec_len>0)//如果数据还没来得及处理,又接收到了新的数据
{
strcat((char *)REC.Rec,(char *)DMA_Rec_Buf);//将新接收的数据拼接到上一次数据的后面
REC.Rec_len += USART_RX_CNT;//缓存长度就是两次数据的长度
}
else
{
memcpy(REC.Rec,DMA_Rec_Buf,USART_RX_CNT);
REC.Rec_len=USART_RX_CNT;
}
memset(DMA_Rec_Buf,0,USART_RX_CNT);//将DMA缓存清空,准备下一次搬运数据
REC.flag=1;//转存完成标志
}
3)测试
接收数据测试。
转存数据测试(缓存多条数据):实现方法:主函数延时50ms,串口助手周期2ms定时发送数据,让CPU来不及处理太多数据,新的数据又到了。
int main(void)
{
Delay_Init(); //延时函数初始化
Usart_Config(); //串口初始化
while (1)
{
Delay_ms(50);//此处延时50ms,串口助手2ms发送数据,测试缓存功能
if(REC.flag)
{
REC.flag=0;
printf("缓存:\r\n%s",REC.Rec);
printf("长度:%d\r\n\r\n",REC.Rec_len);
memset(REC.Rec,0,REC.Rec_len);
REC.Rec_len=0;
}
}
}
测试现象:一条缓存数据8个字节,三条缓存数据24字节,四条缓存数据32个字节。
说明测试成功。