发新帖本帖赏金 50.00元(功能说明)我要提问
返回列表
打印
[STM32F4]

这里学透DMA串口空闲接收中断

[复制链接]
1205|1
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
本帖最后由 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_Test.zip

4.96 MB

使用特权

评论回复

打赏榜单

21小跑堂 打赏了 50.00 元 2022-12-16
理由:恭喜通过原创审核!期待您更多的原创作品~

评论
21小跑堂 2022-12-16 09:45 回复TA
DMA让单片机仿佛拥有了多核,属实是一大利器,文章的选题很好,但是作者写的过于简单,合理的发散扩展可更容易获得原创奖哦! 
发新帖 本帖赏金 50.00元(功能说明)我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

106

主题

531

帖子

5

粉丝