本帖最后由 woai32lala 于 2022-12-13 20:10 编辑
#申请原创#@21小跑堂
DMA串口空闲中断
第一次使用DMA串口空闲中断是在大学,调试一个九轴陀螺仪HI219,它是一个串口数据输出,一次输出一帧数据,数据长度为41个字节,输出频率为60Hz。 一开始用的是串口接收中断来处理,每接收到一个字节,产生串口中断,去读取接收到的数据,但这种方式太占CPU,效率很低,一是保护现场;二是执行中断处理程序;三是恢复现场,太麻烦,因此想到用DMA接收,一开始用的DMA只接收固定长度产生DMA中断,但这样就带来一个问题,就是如果上一次发送的是半帧数据,当下一帧发送来前半帧数据时,DMA已经累计了41个数据,就触发DMA中断了,这样就会导致整个数据结构错误,虽然解包函数可以处理,这种方式比第一种方式对于CPU的负担就减轻了很多。
但还有没有更好的方式来处理呢?查看STM32F407的数据手册,发现有串口空闲中断整个功能,竟然还有这么个好东西,因此我们下面就来说一下串口空闲中断。在STM32的串口控制器中,设置了有串口空闲中断,即如果串口空闲,又使能了串口空闲中断的话,就触发串口空闲中断,然后程序就会跳到串口中断去执行。
当以上两点条件均满足后?串口空闲中断就能出发么?当然不是,还需要RXNE位被置位后,串口总线空闲才会触发的。
那还有个问题,那CPU是怎么识别空闲中断的呢?
在stm32f4xx手册中,在整个数据帧周期内,即包含起始位和停止位,总线检测到全部是1,即判断RX引脚是否高电平,如果高电平时间超过一定时间就认为是空闲状态
如上图所示,1个起始位,8个数据位,一个停止位。如果在10bit的周期内,硬件检测到了10个1,则认为串口空闲。起始位是以低电平0作为判断,停止位是高电平1.
当波特率为115200的时候,每传输一个bit耗时1/115200=8.68us。一个字节帧的周期为:10*8.68=86.8us。也就是说,当总线在>=86.8us的时间内,检测到总线一直为1,则认为当前总线空闲,空闲标志位为1。也就会触发空闲中断。
在采用串口dma接收+空闲中断的时候,如果两帧数据的发送间隔小于86.8us时,则stm32会认为这两帧数据为一帧数据。
下面用代码来说明。
/*******************************************************************************
* 函 数 名 : USART1_Init
* 函数功能 : USART1初始化函数
* 输 入 : bound:波特率
* 输 出 : 无
*******************************************************************************/
void USART1_Init(u32 bound)
{
//GPIO端口设置
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE); //使能GPIOA时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);//使能USART1外设时钟
//串口1对应引脚复用映射
GPIO_PinAFConfig(GPIOA,GPIO_PinSource9,GPIO_AF_USART1); //GPIOA9复用为USART1
GPIO_PinAFConfig(GPIOA,GPIO_PinSource10,GPIO_AF_USART1); //GPIOA10复用为USART1
//配置TX端口
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //推挽复用输出
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; //上拉
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
//配置RX端口
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IN;//先配置为输入,在配置为复用
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
//GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //推挽复用输出
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; //配置为浮空输入模式
GPIO_Init(GPIOA,&GPIO_InitStructure);
//配置串口的工作参数
//USART1 初始化设置
USART_InitStructure.USART_BaudRate = bound;//波特率设置
USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式
USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位
USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验位
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //收发模式
USART_Init(USART1, &USART_InitStructure); //初始化串口1
//串口中断优先级设置
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;//串口1中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1;//抢占优先级3
NVIC_InitStructure.NVIC_IRQChannelSubPriority =2; //子优先级3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化NVIC寄存器
/*关键的是要开启总线空闲中断,并且开启串口DMA接收.
注意,不要开启串口接收中断,不然接收数据就会一直产生中断了
如果USE_USART_DMA_RX定义了USE_USART_DMA_RX=1,则开启串口空闲中断
否则,只开启普通串口中断
*/
USART_ClearFlag(USART1, USART_FLAG_RXNE);//清除接收中断
USART_ClearFlag(USART1, USART_FLAG_IDLE);//清除串口空闲中断
#if USE_USART_DMA_RX
// 开启 串口空闲IDEL 中断
USART_ITConfig(DEBUG_USARTx, USART_IT_IDLE, ENABLE);
//使能串口DMA
DMAx_Init_RX(DMA2_Stream5,DMA_Channel_4,(u32)&USART1->DR,(u32)Uart_Rx,Unize_Len);//从陀螺仪接收数据
// 开启串口DMA接收
USART_DMACmd(DEBUG_USARTx, USART_DMAReq_Rx, ENABLE);
#else
// 使能普通串口接收中断
USART_ITConfig(DEBUG_USARTx, USART_IT_RXNE, ENABLE);
#endif
#if USE_USART_DMA_TX
// 开启串口DMA发送
USART_DMACmd(DEBUG_USARTx, USART_DMAReq_Tx, ENABLE);
USARTx_DMA_Tx_Config();
#endif
// 使能串口
USART_Cmd(USART1, ENABLE);
}
该部分代码的关键是要开启总线空闲中断,并且开启串口DMA接收。注意,不要开启串口接收中断和DMA接收完成中断,不然接收数据就会一直产生中断了,只开启串口空闲中断。
2、DMA配置
DMA配置,要先查看串口接收是使用的哪个DMA的哪个通道,对于USART1_RX使用的是DMA2数据流5的通道4。
然后就是代码配置DMA了。
void DMAx_Init_RX(DMA_Stream_TypeDef *DMA_Streamx,u32 chx,u32 par,u32 mar,u16 ndtr)
{
DMA_InitTypeDef DMA_InitStructure;
if((u32)DMA_Streamx>(u32)DMA2)//得到当前stream是属于DMA2还是DMA1
{
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2,ENABLE);//DMA2时钟使能
}
else
{
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1,ENABLE);//DMA1时钟使能
}
DMA_DeInit(DMA_Streamx);
while (DMA_GetCmdStatus(DMA_Streamx) != DISABLE)
{} //等待DMA可配置
/* 配置 DMA Stream */
DMA_InitStructure.DMA_Channel = chx; //通道选择
DMA_InitStructure.DMA_PeripheralBaseAddr = par;//DMA外设地址
DMA_InitStructure.DMA_Memory0BaseAddr = mar;//DMA 存储器0地址
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;//外设到存储器模式
DMA_InitStructure.DMA_BufferSize = ndtr;//数据传输量
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_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;//禁止FIFO
DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full;//阈值关闭
DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;//存储器突发单次传输
DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;//外设突发单次传输
DMA_Init(DMA_Streamx, &DMA_InitStructure);//初始化DMA Stream
/* 清除DMA数据流传输完成标志位 */
DMA_ClearFlag(DMA2_Stream5,DMA_FLAG_TCIF5);//DMA_FLAG_TCIF5 数据流5传出完成的标志
/*
因为这里,不需要用到DMA中断,所以DMA中断就不要使能了。因此DMA中断配置也就不需要了。
这里,关键的是要设置DMA_DIR为DMA_DIR_PeripheralSRC,表示数据是从外设到内存。
这里设定的DMA_Mode是普通模式,即数据传输就只能一次。
*/
DMA_Cmd(DMA2_Stream5, ENABLE);
}
因为这里,不需要用到DMA中断,所以DMA中断就不要使能了,直接屏蔽掉。了。这里,关键的是要设置DMA_DIR为DMA_DIR_PeripheralSRC,表示数据是从外设到内存,因为是从外部往内存传送数据。这里设定的DMA_Mode是普通模式,即数据传输就只能一次。
3、串口中断程序编写
进入串口空闲中断后,要暂时关闭串口接收DMA通道
1.防止后面又有数据接收到,产生干扰,
因为此时的数据还未处理。
2.DMA需要重新配置。
另外还有一点,串口空闲中断触发后,硬件会自动将串口空闲中断标志位给置1,我们是需要将给标志位给置0的,不然又要进中断了,这个在手册中也有说明。
关键的一点,就是要读取SR,DR,将USART_IT_IDLE标志给清掉,然后DMA设置要注意下。
buff_length = USART_RX_BUFF_SIZE - DMA_GetCurrDataCounter(DMA2_Stream5);
这个公式可以得到这次接收到一帧数据的长度,注意USART_RX_BUFF_SIZE的长度,我们设置的是128,注意这个值要大于你所接收一帧数据的最大长度,否则会出现错误。
DMA_GetCurrDataCounter(DMA2_Stream5)是指你指定了传输数量后剩余的量,比如你设置为128,传输了40个数据,那么该值为128 - 40 =88。
每次传输完成需要重新赋值
DMA2_Stream5-> NDTR = USART_RX_BUFF_SIZE;
硬件接线
我们用的是STM32开发板,用的串口1 的PA9和PA10管脚,通过USB转TTL与电脑连接,通过上位机给Stm32发送一帧串口数据
串口波特率设置为115200,,1位停止位,8个数据位,None校验,16进制发送,不发送新行。
在主函数中,使用下面代码测试:
/*******************************************************************************
* 函 数 名 : main
* 函数功能 : 主函数
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
int main()
{
SysTick_Init(168);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //中断优先级分组 分2组
LED_Init();
USART1_Init(115200);
while(1)
{
i++;
if(i%20 == 0)
{
led1=!led1;
}
if(receive_flag)
{
//add you deal program
receive_flag=0;
{
DMA_Cmd(DMA2_Stream5, ENABLE);
}
}
}
}
当串口接收数据后,中断程序会使receive_flag为1,然后您可以在主循环中处理数据了。
测试结果:
发送8个16进制数据,测试单片机接收情况
变量len代表接收到一帧数据的量,我们发送了8个数据,他接收了8个,接收数量正确。
接收到的数据和发送数据一致,整个流程正常。
以上就是DMA串口空闲中断的应用,如有错误,请大家指教。
|
DMA让单片机仿佛拥有了多核,属实是一大利器,文章的选题很好,但是作者写的过于简单,合理的发散扩展可更容易获得原创奖哦!