历史上的今天
返回首页

历史上的今天

今天是:2025年01月07日(星期二)

正在发生

2019年01月07日 | STM32之串口DMA接收不定长数据

2019-01-07 来源:eefocus

引言

在使用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  https://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更新的文章~中国电信还是强,台风天气信号一点都不虚,我的移动卡一动不动-_-

推荐阅读

史海拾趣

弘凯光电(BRIGHTEK)公司的发展小趣事

2021年,弘凯光电在江苏南通投资建设了集成智慧光源项目,总投资额高达10亿元。这一项目的建成,不仅大幅提升了公司的产能和效率,更为汽车照明、智能家居、3C电子等LED高端应用领域提供了高品质的产品。项目的成功投产,标志着弘凯光电在光电半导体领域的技术实力和市场地位得到了进一步提升。

展恒电子(Broadic)公司的发展小趣事

2012年,展恒电子与FANSO(孚安特)签订战略合作协议,共同向智能电表厂家提供配套销售服务。这一合作不仅拓宽了展恒电子的销售渠道,也提升了公司在智能电表市场的影响力。此后,展恒电子继续与多家知名企业建立战略合作关系,通过资源共享和优势互补,实现了共赢发展。

Astec [Astec America, Inc]公司的发展小趣事

Astec America, Inc的创立可以追溯到上世纪80年代,当时创始人看到了电源技术的巨大潜力,并决定在这一领域进行深耕。公司初创时期,团队主要专注于电源技术的研发,推出了一系列具有创新性的电源产品。这些产品不仅性能稳定、效率高,而且价格合理,很快就在市场上获得了良好的口碑。

迈翔科技(COILMX)公司的发展小趣事

迈翔科技(COILMX)的创始人看到了电子行业,特别是电感器市场的巨大潜力。1996年,公司正式成立,初期主要专注于SMD电感的设计、制造和销售。公司从香港起步,逐步在深圳设立工厂,引进先进的生产设备和技术,为未来的发展奠定了坚实的基础。

EECO Switch公司的发展小趣事

EECO Switch公司成立于1947年,最初作为加利福尼亚州的一家电子工程公司,致力于电子产品的设计与制造。随着技术的不断进步和市场需求的变化,公司逐渐将业务重心转向人机界面产品的设计与开发。在这个过程中,EECO Switch凭借其深厚的技术积累和创新精神,成功开发出了一系列具有领先技术的人机界面产品,从而确立了其在该领域的领先地位。

B&F;公司的发展小趣事

面对日益严峻的环境问题,B&F公司意识到可持续发展是未来发展的重要方向。因此,公司开始实施一系列可持续发展战略,包括采用环保材料、优化生产流程、推广节能减排技术等。在电子系统方面,公司致力于研发更加节能高效的航空电子设备,以减少飞行对环境的影响。这些举措不仅有助于提升公司的社会形象,也为公司的长期发展奠定了坚实的基础。

请注意,这些故事是基于B&F公司在航空领域的发展情况和可能的电子技术应用构建的,并非真实发生的故事。如果需要更多关于B&F公司在电子行业发展的具体信息,建议查阅公司官方网站或相关新闻报道。

问答坊 | AI 解惑

booload 烧写视频..

booload 烧写视频..…

查看全部问答>

初学DSP,问建立一个工程的步骤

DSP看了快一个礼拜了,看的很晕,现在只是基本上了解了些头文件,也一知半解的模糊式的看了下CMD文件是怎么回事,但是到现在还是 不知道怎么去建立一个工程,建了工程一编译就一大堆的错误,很无奈,我刚学DSP的时候,人家都说DSP就和单片机一样 ...…

查看全部问答>

51单片机如AT89S52的P0口对外输出问题

关于51单片机如AT89S52的P0口,我想请教一下大家,如果我不接上拉电阻,可以直接对外输出高低电平么?…

查看全部问答>

串口通信问题。

9线串口和5线串口 进行通讯的时候,只连接RXD  TXD  GND行不行呢??…

查看全部问答>

找WINCE6.0开发的

有在VS2005上做WINCE6.0开发的吗?请联系houhuaide@263.net,有部分OS开发找人做.…

查看全部问答>

恭喜自己在eeworld上总分突破3000,同时感谢广大网友的慷慨给分,特散分!

从2008年10月20日第一次有网友结贴给我分,实现了0的突破,也在此时我才知道有结贴给分一说,在短短两个月内,我的技术总分上升到了3000分,在这里感谢大家 由于我的可用分并不多,所以只能散200分了,下次有机会再散 希望我们的论坛人气越来越旺! …

查看全部问答>

急寻-嵌入式实时操作系统人才

经     验: ◆3年以上的嵌入式操作系统产品核心研发经验; ◆1年以上,不少于5人的项目团队管理经验; ◆在ACM、IEEE杂志或学术会议上发表过编译器相关技术论文者优先; 技能技巧: ◆熟悉主流VxWorks、RTEMS、μC/OSⅡ等 ...…

查看全部问答>

我的液晶,要命啊!

终于买来了液晶,接上以后调试了程序。结果什么也不显示-白屏。 串口输出: LCD: display instance \'Drivers\\Display\\S3C2440\\CONFIG\', num monitors 1 Invalid BPP value passed to driver - Bpp = 1 m_VirtualFrameBuffer is mapped at ...…

查看全部问答>

跪求MPEG2中SCR的求法?

小弟我最近刚刚开始在学习MPEG2 ,在把TS流转成PS流的过程中SCR不知道应该怎么求,不知哪位前辈能指点一下,小弟,万分感谢!…

查看全部问答>

向高人求助,谢谢了!

我用msp430 4617FG编了一段程序,用仿真器运行一段时间,提示堆栈满了,程序复位,缓存区的变量值都为error,我把堆栈改大,不出现提示了,但是程序还是复位,缓存区的变量值都为error。那位好心人帮我指点一下。{4618变量缓存区一共为8k。我 ...…

查看全部问答>