你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

STM32之串口DMA接收不定长数据

[复制链接]
xiaojie0513 发布时间:2018-9-18 21:07
本帖最后由 xiaojie0513 于 2018-9-18 21:07 编辑

引言
在使用stm32或者其他单片机的时候,会经常使用到串口通讯,那么如何有效地接收数据呢?假如这段数据是不定长的有如何高效接收呢?
  
同学A:数据来了就会进入串口中断,在中断中读取数据就行了!
  
中断就是打断程序正常运行,怎么能保证高效呢?经常把主程序打断,主程序还要不要运行了?
  
同学B:串口可以配置成用DMA的方式接收数据,等接收完毕就可以去读取了!
  
这个同学是对的,我们可以使用DMA去接收数据,不过DMA需要定长才能产生接收中断,如何接收不定长的数据呢?
DMA简介
  
题外话:其实,上面的问题是很有必要思考一下的,不断思考,才能进步。
什么是DMA
DMA:全称Direct Memory Access,即直接存储器访问
DMA 传输将数据从一个地址空间复制到另外一个地址空间。CPU只需初始化DMA即可,传输动作本身是由 DMA 控制器来实现和完成。典型的例子就是移动一个外部内存的区块到芯片内部更快的内存区。这样的操作并没有让处理器参与处理,CPU可以干其他事情,当DMA传输完成的时候产生一个中断,告诉CPU我已经完成了,然后CPU知道了就可以去处理数据了,这样子提高了CPU的利用率,因为CPU是大脑,主要做数据运算的工作,而不是去搬运数据。DMA 传输对于高效能嵌入式系统算法和网络是很重要的。
在STM32的DMA资源
STM32F1系列的MCU有两个DMA控制器(DMA2只存在于大容量产品中),DMA1有7个通道,DMA2有5个通道,每个通道专门用来管理来自于一个或者多个外设对存储器的访问请求。还有一个仲裁器来协调各个DMA请求的优先权。
STM32F1
STM32F1
而STM32F4/F7/H7系列的MCU有两个DMA控制器总共有16个数据流(每个DMA控制器8个),每一个DMA控制器都用于管理一个或多个外设的存储器访问请求。每个数据流总共可以有多达8个通道(或称请求)。每个通道都有一个仲裁器,用于处理 DMA 请求间的优先级。
STM32F4
STM32F4

DMA接收数据

DMA在接收数据的时候,串口接收DMA在初始化的时候就处于开启状态,一直等待数据的到来,在软件上无需做任何事情,只要在初始化配置的时候设置好配置就可以了。等到接收到数据的时候,告诉CPU去处理即可。

判断数据接收完成
  
那么问题来了,怎么知道数据是否接收完成呢?
其实,有很多方法:
  • 对于定长的数据,只需要判断一下数据的接收个数,就知道是否接收完成,这个很简单,暂不讨论。
  • 对于不定长的数据,其实也有好几种方法,麻烦的我肯定不会介绍,有兴趣做复杂工作的同学可以在网上看看别人怎么做,下面这种方法是最简单的,充分利用了stm32的串口资源,效率也是非常之高。

DMA+串口空闲中断
这两个资源配合,简直就是天衣无缝啊,无论接收什么不定长的数据,管你数据有多少,来一个我就收一个,就像广东人吃“山竹”,来一个吃一个~(最近风好大,我好怕)。
可能很多人在学习stm32的时候,都不知道idle是啥东西,先看看stm32串口的状态寄存器:
idle
idle说明
当我们检测到触发了串口总线空闲中断的时候,我们就知道这一波数据传输完成了,然后我们就能得到这些数据,去进行处理即可。这种方法是最简单的,根本不需要我们做多的处理,只需要配置好,串口就等着数据的到来,dma也是处于工作状态的,来一个数据就自动搬运一个数据。
接收完数据时处理
串口接收完数据是要处理的,那么处理的步骤是怎么样呢?
  • 暂时关闭串口接收DMA通道,有两个原因:1.防止后面又有数据接收到,产生干扰,因为此时的数据还未处理。2.DMA需要重新配置。
  • 清DMA标志位。
  • 从DMA寄存器中获取接收到的数据字节数(可有可无)。
  • 重新设置DMA下次要接收的数据字节数,注意,数据传输数量范围为0至65535。这个寄存器只能在通道不工作(DMA_CCRx的EN=0)时写入。通道开启后该寄存器变为只读,指示剩余的待传输字节数目。寄存器内容在每次DMA传输后递减。数据传输结束后,寄存器的内容或者变为0;或者当该通道配置为自动重加载模式时,寄存器的内容将被自动重新加载为之前配置时的数值。当寄存器的内容为0时,无论通道是否开启,都不会发生任何数据传输。
  • 给出信号量,发送接收到新数据标志,供前台程序查询。
  • 开启DMA通道,等待下一次的数据接收,注意,对DMA的相关寄存器配置写入,如重置DMA接收数据长度,必须要在关闭DMA的条件进行,否则操作无效。

注意事项
STM32的IDLE的中断在串口无数据接收的情况下,是不会一直产生的,产生的条件是这样的,当清除IDLE标志位后,必须有接收到第一个数据后,才开始触发,一断接收的数据断流,没有接收到数据,即产生IDLE中断。如果中断发送数据帧的速率很快,MCU来不及处理此次接收到的数据,中断又发来数据的话,这里不能开启,否则数据会被覆盖。有两种方式解决:
  • 在重新开启接收DMA通道之前,将Rx_Buf缓冲区里面的数据复制到另外一个数组中,然后再开启DMA,然后马上处理复制出来的数据。
  • 建立双缓冲,重新配置DMA_MemoryBaseAddr的缓冲区地址,那么下次接收到的数据就会保存到新的缓冲区中,不至于被覆盖。


程序实现
实验效果:
当外部给单片机发送数 据的时候,假设这帧数据长度是1000个字节,那么在单片机接收到一个字节的时候并不会产生串口中断,只是DMA在背后默默地把数据搬运到你指定的缓冲区里面。当整帧数据发送完毕之后串口才会产生一次中断,此时可以利用DMA_GetCurrDataCounter()函数计算出本次的数据接受长度,从而进行数据处理。
串口的配置
很简单,基本与使用串口的时候一致,只不过一般我们是打开接收缓冲区非空中断,而现在是打开空闲中断——USART_ITConfig(DEBUG_USARTx, USART_IT_IDLE, ENABLE);。
/**
  * @brief  USART GPIO 配置,工作参数配置
  * @param  无
  * @retval 无
  */

void USART_Config(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    USART_InitTypeDef USART_InitStructure;

    // 打开串口GPIO的时钟
    DEBUG_USART_GPIO_APBxClkCmd(DEBUG_USART_GPIO_CLK, ENABLE);

    // 打开串口外设的时钟
    DEBUG_USART_APBxClkCmd(DEBUG_USART_CLK, ENABLE);

    // 将USART Tx的GPIO配置为推挽复用模式
    GPIO_InitStructure.GPIO_Pin = DEBUG_USART_TX_GPIO_PIN;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(DEBUG_USART_TX_GPIO_PORT, &GPIO_InitStructure);

  // 将USART Rx的GPIO配置为浮空输入模式
    GPIO_InitStructure.GPIO_Pin = DEBUG_USART_RX_GPIO_PIN;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(DEBUG_USART_RX_GPIO_PORT, &GPIO_InitStructure);

    // 配置串口的工作参数
    // 配置波特率
    USART_InitStructure.USART_BaudRate = DEBUG_USART_BAUDRATE;
    // 配置 针数据字长
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;
    // 配置停止位
    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(DEBUG_USARTx, &USART_InitStructure);
    // 串口中断优先级配置
    NVIC_Configuration();

#if USE_USART_DMA_RX
    // 开启 串口空闲IDEL 中断
    USART_ITConfig(DEBUG_USARTx, USART_IT_IDLE, ENABLE);  
  // 开启串口DMA接收
    USART_DMACmd(DEBUG_USARTx, USART_DMAReq_Rx, ENABLE);
    /* 使能串口DMA */
    USARTx_DMA_Rx_Config();
#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(DEBUG_USARTx, ENABLE);        
}
串口DMA配置
把DMA配置完成,就可以直接打开DMA了,让它处于工作状态,当有数据的时候就能直接搬运了。
#if USE_USART_DMA_RX

static void USARTx_DMA_Rx_Config(void)
{
    DMA_InitTypeDef DMA_InitStructure;

    // 开启DMA时钟
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
    // 设置DMA源地址:串口数据寄存器地址*/
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)USART_DR_ADDRESS;
    // 内存地址(要传输的变量的指针)
    DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)Usart_Rx_Buf;
    // 方向:从内存到外设   
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
    // 传输大小
    DMA_InitStructure.DMA_BufferSize = USART_RX_BUFF_SIZE;
    // 外设地址不增      
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    // 内存地址自增
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
    // 外设数据单位   
    DMA_InitStructure.DMA_PeripheralDataSize =
    DMA_PeripheralDataSize_Byte;
    // 内存数据单位
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;  
    // DMA模式,一次或者循环模式
    //DMA_InitStructure.DMA_Mode = DMA_Mode_Normal ;
    DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
    // 优先级:中   
    DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh;
    // 禁止内存到内存的传输
    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
    // 配置DMA通道         
    DMA_Init(USART_RX_DMA_CHANNEL, &DMA_InitStructure);     
    // 清除DMA所有标志
    DMA_ClearFlag(DMA1_FLAG_TC5);
    DMA_ITConfig(USART_RX_DMA_CHANNEL, DMA_IT_TE, ENABLE);
    // 使能DMA
    DMA_Cmd (USART_RX_DMA_CHANNEL,ENABLE);
}
#endif
接收完数据处理
因为接收完数据之后,会产生一个idle中断,也就是空闲中断,那么我们就可以在中断服务函数中知道已经接收完了,就可以处理数据了,但是中断服务函数的上下文环境是中断,所以,尽量是快进快出,一般在中断中将一些标志置位,供前台查询。在中断中先判断我们的产生在中断的类型是不是idle中断,如果是则进行下一步,否则就无需理会。
/**
  ******************************************************************
  * @brief   串口中断服务函数
  * @author  jiejie
  * @version V1.0
  * @date    2018-xx-xx
  ******************************************************************
  */

void DEBUG_USART_IRQHandler(void)
{
#if USE_USART_DMA_RX
    /* 使用串口DMA */
    if(USART_GetITStatus(DEBUG_USARTx,USART_IT_IDLE)!=RESET)
    {      
        /* 接收数据 */
        Receive_DataPack();
        // 清除空闲中断标志位
        USART_ReceiveData( DEBUG_USARTx );
    }   
#else
  /* 接收中断 */
    if(USART_GetITStatus(DEBUG_USARTx,USART_IT_RXNE)!=RESET)
    {      
    Receive_DataPack();
    }
#endif
}
Receive_DataPack()
这个才是真正的接收数据处理函数,为什么我要将这个函数单独封装起来呢?因为这个函数其实是很重要的,因为我的代码兼容普通串口接收与空闲中断,不一样的接收类型其处理也不一样,所以直接封装起来更好,在源码中通过宏定义实现选择接收的方式!更考虑了兼容操作系统的,可能我会在系统中使用dma+空闲中断,所以,供前台查询的信号量就有可能不一样,可能需要修改,我就把它封装起来了。不过无所谓,都是一样的。
/************************************************************
  * @brief   Uart_DMA_Rx_Data
  * @param   NULL
  * @return  NULL
  * @author  jiejie
  * @github  http://github.com/jiejieTop
  * @date    2018-xx-xx
  * @version v1.0
  * @note    使用串口 DMA 接收时调用的函数
  ***********************************************************/

#if USE_USART_DMA_RX
void Receive_DataPack(void)
{
    /* 接收的数据长度 */
    uint32_t buff_length;

    /* 关闭DMA ,防止干扰 */
    DMA_Cmd(USART_RX_DMA_CHANNEL, DISABLE);  /* 暂时关闭dma,数据尚未处理 */

    /* 清DMA标志位 */
    DMA_ClearFlag( DMA1_FLAG_TC5 );  

    /* 获取接收到的数据长度 单位为字节*/
    buff_length = USART_RX_BUFF_SIZE - DMA_GetCurrDataCounter(USART_RX_DMA_CHANNEL);

    /* 获取数据长度 */
    Usart_Rx_Sta = buff_length;

    PRINT_DEBUG("buff_length = %d\n ",buff_length);

    /* 重新赋值计数值,必须大于等于最大可能接收到的数据帧数目 */
    USART_RX_DMA_CHANNEL->CNDTR = USART_RX_BUFF_SIZE;   

    /* 此处应该在处理完数据再打开,如在 DataPack_Process() 打开*/
    DMA_Cmd(USART_RX_DMA_CHANNEL, ENABLE);      

    /* (OS)给出信号 ,发送接收到新数据标志,供前台程序查询 */

    /* 标记接收完成,在 DataPack_Handle 处理*/
    Usart_Rx_Sta |= 0xC000;

    /*
    DMA 开启,等待数据。注意,如果中断发送数据帧的速率很快,MCU来不及处理此次接收到的数据,
    中断又发来数据的话,这里不能开启,否则数据会被覆盖。有2种方式解决:

    1. 在重新开启接收DMA通道之前,将Rx_Buf缓冲区里面的数据复制到另外一个数组中,
    然后再开启DMA,然后马上处理复制出来的数据。

    2. 建立双缓冲,重新配置DMA_MemoryBaseAddr的缓冲区地址,那么下次接收到的数据就会
    保存到新的缓冲区中,不至于被覆盖。
    */

}
f1使用dma是非常简单的,我在f4用dma的时候也遇到一些问题,最后看手册解决了,打算下一篇文章就写一下调试过程,没有什么是debug不能解决的,如果有,那就两次。今天台风天气,连着舍友的WiFi更新的文章~中国电信还是强,台风天气信号一点都不虚,我的移动卡一动不动-_-.

不设置回复可见,大家一起学习~感谢
本文为杰杰原创如需转载,请说明出处:


收藏 3 评论19 发布时间:2018-9-18 21:07

举报

19个回答
wdshuang09 回答时间:2018-9-18 23:11:56
xiaojie0513 发表于 2018-9-18 22:49

STM32进阶之串口环形缓冲区实现
https://www.stmcu.org.cn/module/forum/forum.php?mod=viewthread&tid=61 ...

这篇先前看过,好像是正点原子的例子改的,解析接收数据没有启到环形队列特点;比如当你接收到一帧数据时,(USART_RX_STA|=0x8000;        //接收完成了);假设CPU处理外设多,没有及时处理这一帧数据,此时串口又来了一帧数据,就会丢数据了,没有用到环形队列缓存功能,

void USART1_IRQHandler(void)                        //串口1中断服务程序
        {
        u8 Res;
#if SYSTEM_SUPPORT_OS                 //如果SYSTEM_SUPPORT_OS为真,则需要支持OS.
        OSIntEnter();   
#endif
        if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)  //接收中断(接收到的数据必须是0x0d 0x0a结尾)
                {
                Res =USART_ReceiveData(USART1);        //读取接收到的数据
               
                if((USART_RX_STA&0x8000)==0)//接收未完成
                        {
                        if(USART_RX_STA&0x4000)//接收到了0x0d
                                {
                                if(Res!=0x0a)USART_RX_STA=0;//接收错误,重新开始
                                else USART_RX_STA|=0x8000;        //接收完成了
                                }
                        else //还没收到0X0D
                                {       
                                if(Res==0x0d)USART_RX_STA|=0x4000;
                                else
                                        {
                                        USART_RX_BUF[USART_RX_STA&0X3FFF]=Res ;
                                        USART_RX_STA++;
                                        if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;//接收数据错误,重新开始接收          
                                        }                 
                                }
                        }                    
     }
#if SYSTEM_SUPPORT_OS         //如果SYSTEM_SUPPORT_OS为真,则需要支持OS.
        OSIntExit();                                                                                           
#endif
}                  
wdshuang09 回答时间:2018-9-19 08:57:50
xiaojie0513 发表于 2018-9-19 01:47
你接收完如果暂时没法处理可以将数据放到缓冲区去,然后有空再去处理,,,你这仅仅是接收而已,你在哪放 ...

这个逻辑好像不对,
1、接收数据就是直接放在环形队列中,待CPU有空闲时根据环形队列队头与队尾位置,逐步取出接收到的数据进行处理;
2、如果你每接收到一个数据都去中断判断是否接收一帧数据,会影响CPU的效率,接收中断只接收数据,不做任何数据分析,来一个存一个,减少在中断里处理的时间;待CPU空闲时去环形队列中取数据、解析数据
3、已经定义了一个环形队列,还去定义一个缓冲区好像有点多余,如果CPU没有时间处理那就不去环形队列取数据即可了,空闲时去取不就可以了。
wdshuang09 回答时间:2018-9-19 13:48:31
xiaojie0513 发表于 2018-9-19 12:25
f1的dma是没有FIFO的吧,dma接收根本无需CPU去处理的好吗,先理解一下吧,,,,,dma接收的数据是放到指 ...

这里说的是你发的“STM32进阶之串口环形缓冲区实现”贴子,对于贴子内容是采用环形串口收接,没有说这篇贴子DMA处理;当然有些单片机没有DMA处理,采用环形接收的数据,看到“STM32进阶之串口环形缓冲区实现”这篇贴子,说了一下个人的理解,
对于这种环形队列加中断的方式接收数据和楼主探讨如何更高效接收与解析数据的方法。
1.jpg
wdshuang09 回答时间:2018-9-18 22:17:10
有些单片机是没有DMA,可以用环形串口接收数据,不知楼主有没有环形串口结合中断方式接收定长与不定长的数据例子,
xiaojie0513 回答时间:2018-9-18 22:49:39
wdshuang09 发表于 2018-9-18 22:17
有些单片机是没有DMA,可以用环形串口接收数据,不知楼主有没有环形串口结合中断方式接收定长与不定长的数 ...


STM32进阶之串口环形缓冲区实现
https://www.stmcu.org.cn/module/ ... amp;fromuid=3250941
(出处: 意法半导体STM32/STM8技术社区)

xiaojie0513 回答时间:2018-9-19 01:46:40
wdshuang09 发表于 2018-9-18 23:11
这篇先前看过,好像是正点原子的例子改的,解析接收数据没有启到环形队列特点;比如当你接收到一帧数据时 ...

你这是接收,跟缓冲区有啥关系啊?
xiaojie0513 回答时间:2018-9-19 01:47:31
xiaojie0513 发表于 2018-9-19 01:46
你这是接收,跟缓冲区有啥关系啊?

你接收完如果暂时没法处理可以将数据放到缓冲区去,然后有空再去处理,,,你这仅仅是接收而已,你在哪放入缓冲区呢
loveu99 回答时间:2018-9-19 09:33:55
wdshuang09 发表于 2018-9-18 23:11
这篇先前看过,好像是正点原子的例子改的,解析接收数据没有启到环形队列特点;比如当你接收到一帧数据时 ...

我看到我们一同事在用这种代码格式,我想他也写不出这样的格式呀,原来是出自正点原子
xiaojie0513 回答时间:2018-9-19 12:25:08
wdshuang09 发表于 2018-9-19 08:57
这个逻辑好像不对,
1、接收数据就是直接放在环形队列中,待CPU有空闲时根据环形队列队头与队尾位置,逐 ...

f1的dma是没有FIFO的吧,dma接收根本无需CPU去处理的好吗,先理解一下吧,,,,,dma接收的数据是放到指定的缓冲区的,并没有使用环形队列,,,,使用环形队列需要CPU的参与,,这是f1的,你可以在接收完重置dma存放数据的位置,也能起到缓存的作用

支持FIFO的dma无需这样子做。
xiaojie0513 回答时间:2018-9-19 12:26:06
loveu99 发表于 2018-9-19 09:33
我看到我们一同事在用这种代码格式,我想他也写不出这样的格式呀,原来是出自正点原子 ...

每来一个字节的数据就进入中断一次,效率太低,,,
ssssss 回答时间:2018-9-19 14:39:46
本帖最后由 wwwheihei 于 2018-9-19 15:53 编辑
wdshuang09 发表于 2018-9-19 13:48
这里说的是你发的“STM32进阶之串口环形缓冲区实现”贴子,对于贴子内容是采用环形串口收接 ...

dma接收就可以了,够高效了,
wdshuang09 回答时间:2018-9-19 14:57:49
wwwheihei 发表于 2018-9-19 14:39
dma接收就可以了,够高效了,再往高就是装x了

不要断章取义,也不要出口成脏,你说的DMA效率高效,不一定每款单片机都有DMA,同时也不防碍讨论环形队列接收数据高效的方法。
ssssss 回答时间:2018-9-19 15:15:20
wdshuang09 发表于 2018-9-19 14:57
不要断章取义,也不要出口成脏,你说的DMA效率高效,不一定每款单片机都有DMA,同时也不防碍讨论环形队列 ...

没有出口成章啊,在一般应用中数据流没那么快,所以说高效不高效还得看项目需求。如果每一帧的间隔时间比较长,或者数据交互比较不频繁,我觉得用不用都可以
xiaojie0513 回答时间:2018-9-19 20:16:13
wdshuang09 发表于 2018-9-19 13:48
这里说的是你发的“STM32进阶之串口环形缓冲区实现”贴子,对于贴子内容是采用环形串口收接 ...

其实,反正我喜欢dma+空闲中断,,,我一般都用32的芯片,如果非要说环形缓冲区的话,,,,Linux的kfifo很经典,我也移植到32了,,到时候可以发出来看看,支持os
12下一页

所属标签

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
STM32N6 AI生态系统
STM32MCU,MPU高性能GUI
ST ACEPACK电源模块
意法半导体生物传感器
STM32Cube扩展软件包
关注我们
st-img 微信公众号
st-img 手机版