返回列表 发新帖我要提问本帖赏金: 50.00元(功能说明)

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

[复制链接]
1393|1
 楼主| woai32lala 发表于 2022-12-12 18:28 | 显示全部楼层 |阅读模式
本帖最后由 woai32lala 于 2022-12-13 20:10 编辑

#申请原创#@21小跑堂

DMA串口空闲中断

第一次使用DMA串口空闲中断是在大学,调试一个九轴陀螺仪HI219,它是一个串口数据输出,一次输出一帧数据,数据长度为41个字节,输出频率为60Hz。
     一开始用的是串口接收中断来处理,每接收到一个字节,产生串口中断,去读取接收到的数据,但这种方式太占CPU,效率很低,一是保护现场;二是执行中断处理程序;三是恢复现场,太麻烦,因此想到用DMA接收,一开始用的DMA只接收固定长度产生DMA中断,但这样就带来一个问题,就是如果上一次发送的是半帧数据,当下一帧发送来前半帧数据时,DMA已经累计了41个数据,就触发DMA中断了,这样就会导致整个数据结构错误,虽然解包函数可以处理,这种方式比第一种方式对于CPU的负担就减轻了很多。
       但还有没有更好的方式来处理呢?查看STM32F407的数据手册,发现有串口空闲中断整个功能,竟然还有这么个好东西,因此我们下面就来说一下串口空闲中断。在STM32的串口控制器中,设置了有串口空闲中断,即如果串口空闲,又使能了串口空闲中断的话,就触发串口空闲中断,然后程序就会跳到串口中断去执行。
f8f90658f6869d3617b328f7ea128dc2
      当以上两点条件均满足后?串口空闲中断就能出发么?当然不是,还需要RXNE位被置位后,串口总线空闲才会触发的。
edf34ded5bd1592e2e8d5f7b3ee617d8
     那还有个问题,那CPU是怎么识别空闲中断的呢?
     在stm32f4xx手册中,在整个数据帧周期内,即包含起始位和停止位,总线检测到全部是1,即判断RX引脚是否高电平,如果高电平时间超过一定时间就认为是空闲状态
dceae7fd248a00431f218825fb4e3569
如上图所示,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会认为这两帧数据为一帧数据。
下面用代码来说明。

  1. /*******************************************************************************
  2. * 函 数 名         : USART1_Init
  3. * 函数功能                   : USART1初始化函数
  4. * 输    入         : bound:波特率
  5. * 输    出         : 无
  6. *******************************************************************************/
  7. void USART1_Init(u32 bound)
  8. {
  9.     //GPIO端口设置
  10.     GPIO_InitTypeDef GPIO_InitStructure;
  11.     USART_InitTypeDef USART_InitStructure;
  12.     NVIC_InitTypeDef NVIC_InitStructure;

  13.     RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE); //使能GPIOA时钟
  14.     RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);//使能USART1外设时钟

  15.     //串口1对应引脚复用映射
  16.     GPIO_PinAFConfig(GPIOA,GPIO_PinSource9,GPIO_AF_USART1); //GPIOA9复用为USART1
  17.     GPIO_PinAFConfig(GPIOA,GPIO_PinSource10,GPIO_AF_USART1); //GPIOA10复用为USART1
  18.     //配置TX端口
  19.     GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
  20.     GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
  21.     GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //推挽复用输出
  22.     GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; //上拉
  23.     GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
  24.     GPIO_Init(GPIOA,&GPIO_InitStructure);
  25.     //配置RX端口
  26.     GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
  27.     GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IN;//先配置为输入,在配置为复用
  28.     GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
  29.     //GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //推挽复用输出
  30.     GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; //配置为浮空输入模式
  31.     GPIO_Init(GPIOA,&GPIO_InitStructure);

  32.     //配置串口的工作参数
  33.     //USART1 初始化设置
  34.     USART_InitStructure.USART_BaudRate = bound;//波特率设置
  35.     USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式
  36.     USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位
  37.     USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验位
  38.     USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制
  39.     USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;        //收发模式
  40.     USART_Init(USART1, &USART_InitStructure); //初始化串口1


  41.     //串口中断优先级设置
  42.     NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;//串口1中断通道
  43.     NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1;//抢占优先级3
  44.     NVIC_InitStructure.NVIC_IRQChannelSubPriority =2;                //子优先级3
  45.     NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;                        //IRQ通道使能
  46.     NVIC_Init(&NVIC_InitStructure);        //根据指定的参数初始化NVIC寄存器
  47.     /*关键的是要开启总线空闲中断,并且开启串口DMA接收.
  48.     注意,不要开启串口接收中断,不然接收数据就会一直产生中断了
  49.     如果USE_USART_DMA_RX定义了USE_USART_DMA_RX=1,则开启串口空闲中断
  50.     否则,只开启普通串口中断
  51.     */
  52.      USART_ClearFlag(USART1, USART_FLAG_RXNE);//清除接收中断
  53.      USART_ClearFlag(USART1, USART_FLAG_IDLE);//清除串口空闲中断

  54. #if USE_USART_DMA_RX

  55.     // 开启 串口空闲IDEL 中断
  56.     USART_ITConfig(DEBUG_USARTx, USART_IT_IDLE, ENABLE);

  57.     //使能串口DMA
  58.     DMAx_Init_RX(DMA2_Stream5,DMA_Channel_4,(u32)&USART1->DR,(u32)Uart_Rx,Unize_Len);//从陀螺仪接收数据
  59.     // 开启串口DMA接收
  60.     USART_DMACmd(DEBUG_USARTx, USART_DMAReq_Rx, ENABLE);
  61. #else
  62.     // 使能普通串口接收中断
  63.     USART_ITConfig(DEBUG_USARTx, USART_IT_RXNE, ENABLE);
  64. #endif

  65. #if USE_USART_DMA_TX
  66.     // 开启串口DMA发送
  67.     USART_DMACmd(DEBUG_USARTx, USART_DMAReq_Tx, ENABLE);
  68.     USARTx_DMA_Tx_Config();
  69. #endif

  70.     // 使能串口
  71.     USART_Cmd(USART1, ENABLE);

  72. }

      该部分代码的关键是要开启总线空闲中断,并且开启串口DMA接收。注意,不要开启串口接收中断和DMA接收完成中断,不然接收数据就会一直产生中断了,只开启串口空闲中断。
2、DMA配置
DMA配置,要先查看串口接收是使用的哪个DMA的哪个通道,对于USART1_RX使用的是DMA2数据流5的通道4。
5da4ca72d3e12ae76c7e9f7462e47c00
然后就是代码配置DMA了。

  1. void DMAx_Init_RX(DMA_Stream_TypeDef *DMA_Streamx,u32 chx,u32 par,u32 mar,u16 ndtr)
  2. {
  3.     DMA_InitTypeDef DMA_InitStructure;

  4.     if((u32)DMA_Streamx>(u32)DMA2)//得到当前stream是属于DMA2还是DMA1
  5.     {
  6.         RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2,ENABLE);//DMA2时钟使能
  7.     }
  8.     else
  9.     {
  10.         RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1,ENABLE);//DMA1时钟使能
  11.     }
  12.     DMA_DeInit(DMA_Streamx);

  13.     while (DMA_GetCmdStatus(DMA_Streamx) != DISABLE)
  14.     {} //等待DMA可配置

  15.     /* 配置 DMA Stream */
  16.     DMA_InitStructure.DMA_Channel = chx;  //通道选择
  17.     DMA_InitStructure.DMA_PeripheralBaseAddr = par;//DMA外设地址
  18.     DMA_InitStructure.DMA_Memory0BaseAddr = mar;//DMA 存储器0地址
  19.     DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;//外设到存储器模式
  20.     DMA_InitStructure.DMA_BufferSize = ndtr;//数据传输量
  21.     DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//外设非增量模式
  22.     DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//存储器增量模式
  23.     DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;//外设数据长度:8位
  24.     DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;//存储器数据长度:8位
  25.     DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;// 使用普通模式  非循环
  26.     DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;//中等优先级
  27.     DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;//禁止FIFO
  28.     DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full;//阈值关闭
  29.     DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;//存储器突发单次传输
  30.     DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;//外设突发单次传输

  31.     DMA_Init(DMA_Streamx, &DMA_InitStructure);//初始化DMA Stream

  32.     /* 清除DMA数据流传输完成标志位 */
  33.     DMA_ClearFlag(DMA2_Stream5,DMA_FLAG_TCIF5);//DMA_FLAG_TCIF5  数据流5传出完成的标志
  34.     /*
  35.     因为这里,不需要用到DMA中断,所以DMA中断就不要使能了。因此DMA中断配置也就不需要了。
  36.     这里,关键的是要设置DMA_DIR为DMA_DIR_PeripheralSRC,表示数据是从外设到内存。
  37.     这里设定的DMA_Mode是普通模式,即数据传输就只能一次。
  38.     */
  39.     DMA_Cmd(DMA2_Stream5, ENABLE);

  40. }

因为这里,不需要用到DMA中断,所以DMA中断就不要使能了,直接屏蔽掉。了。这里,关键的是要设置DMA_DIR为DMA_DIR_PeripheralSRC,表示数据是从外设到内存,因为是从外部往内存传送数据。这里设定的DMA_Mode是普通模式,即数据传输就只能一次。
3、串口中断程序编写
进入串口空闲中断后,要暂时关闭串口接收DMA通道
   1.防止后面又有数据接收到,产生干扰,
    因为此时的数据还未处理。
   2.DMA需要重新配置。
另外还有一点,串口空闲中断触发后,硬件会自动将串口空闲中断标志位给置1,我们是需要将给标志位给置0的,不然又要进中断了,这个在手册中也有说明。
ee8aa11086b002874536133b5fd62299
03a982b634c603a03aaeb320d9f4f41f
关键的一点,就是要读取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发送一帧串口数据
77f0b9a4c7d6d26e283e7ca6daf43201
ab968d270737183260b3d3ed803e7de3
8134a3d6ca30e4c43e711b6fcfa2e76d
串口波特率设置为115200,,1位停止位,8个数据位,None校验,16进制发送,不发送新行。
在主函数中,使用下面代码测试:
  1. /*******************************************************************************
  2. * 函 数 名         : main
  3. * 函数功能                   : 主函数
  4. * 输    入         : 无
  5. * 输    出         : 无
  6. *******************************************************************************/
  7. int main()
  8. {
  9.     SysTick_Init(168);
  10.     NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);  //中断优先级分组 分2组
  11.     LED_Init();
  12.     USART1_Init(115200);
  13.     while(1)
  14.     {
  15.         i++;
  16.         if(i%20 == 0)
  17.         {
  18.             led1=!led1;
  19.         }
  20.         if(receive_flag)
  21.         {
  22.             //add you deal program
  23.             receive_flag=0;
  24.             {
  25.                 DMA_Cmd(DMA2_Stream5, ENABLE);
  26.             }
  27.         }

  28.     }
  29. }

当串口接收数据后,中断程序会使receive_flag为1,然后您可以在主循环中处理数据了。
测试结果:
发送8个16进制数据,测试单片机接收情况
3e8cfcd79884ba85be1a35b970a56219
变量len代表接收到一帧数据的量,我们发送了8个数据,他接收了8个,接收数量正确。
2ae432b504cb4d185d8bcace858dec0e
0b71cf21b827b7db9c16b2a6cd37ea50
接收到的数据和发送数据一致,整个流程正常。
以上就是DMA串口空闲中断的应用,如有错误,请大家指教。


DMA_Test.zip

4.96 MB, 下载次数: 7

打赏榜单

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

评论

DMA让单片机仿佛拥有了多核,属实是一大利器,文章的选题很好,但是作者写的过于简单,合理的发散扩展可更容易获得原创奖哦!  发表于 2022-12-16 09:45
您需要登录后才可以回帖 登录 | 注册

本版积分规则

108

主题

559

帖子

6

粉丝
快速回复 在线客服 返回列表 返回顶部