草庐IT

《嵌入式蓝桥杯》STM32单片机+USART2+DMA+IDLE空闲中断来接收数据

嵌入式历练者 2023-04-18 原文

作者博客主页
作者 : Eterlove
一笔一画,记录我的学习生活!站在巨人的肩上Standing on Shoulders of Giants!
该文章为原创,转载请注明出处和作者

声明:这段时间较忙,相关知识点分析讲解后面抽时间补上。

    谈谈我为什么想写这篇文章?---->嵌入式软件面试的那点事,重点难点一网打尽

嵌入式软件面试的那点事,重点难点一网打尽
你是怎么接收、发送串口数据的?
        这个问题其实比较宽泛,一般经验少的会说使用查询方式,但是查询方式效率是非常低下的,所以如果你只能回答这个,100分的题你只能得个30分。如果你说用中断方式,那么请问你具体是如何处理的?如果你回答说一个字节接收完之后再接收下一个字节,那么可以得个50分。
         紧接着又问你,你是怎么接收一帧数据的(这个其实不应该由面试官问,而是由你自己补充全面),如果你说采用帧头、帧尾判断的方式接收的,那么这道题还是给你50分,但是你说用空闲中断,那么70分以上,如果你说用DMA+空闲中断的方式接收的,那么90分以上(这是我认为最好的方式了,可能会有其他更好的方式也说不定)。
        那么现在说说空闲中断,为什么你说了空闲中断之后,一下子从不及格到及格了?
        空闲中断,顾名思义,就是串口空闲后产生的中断。我们都知道,数据一般是按照数据帧来发送的,即一个数据帧一个数据帧的发送,如果两帧发送之间能间隔一段时间,那么在接收端就可以产生空闲中断(关于这个空闲中断,以后可能会专门写一篇笔记介绍),有了空闲中断有什么好处?

        可以接收不定长数据(这是最明显的好处) 不需要复杂的帧格式(比如帧头、帧尾可以不要) 一个数据帧接收错误,不会影响到下一帧数据的接收 有了空闲中断,可谓好处多多(有的单片机没有空闲中断,那就没办法,当然也可以舍弃一个定时器资源来获得空闲中断的效果),所以当初了解到这个之后,就一直使用这种方式接收了。
        但是空闲中断虽好,如果你每接收一个字节都要CPU干预,还是效率太低,那么这时候就得配合DMA了。
怎么配合?比如说你一个数据帧的最大长度是10个字节,设置串口接收缓存区为20个字节,那么你可以设置DMA传输长度为20,这样DMA每从串口传输一个字节,传输长度就会自减,当产生空闲中断时,只要你知道开始设置的传输长度和剩余的传输长度,那么就可以得到你已经接收的数据长度,之后你再重新设置新的接收长度即可进行下一次数据帧的接收。
        如此一来,接收一个数据帧只要CPU干预一次就够了,就是在接收完数据帧的时候由空闲中断通知CPU进行后续处理即可(注意不是DMA中断),极大的减少了CPU工作时间。

有的时候,数据量很大,CPU来不及处理,那么你可以通过以下方式解决:

  1. 增加消息队列(非常好的解决方式)
  2. 增加两帧之间的发送时间(对于实时性要求很高的可能不合适) 前面两种方式叠加

         读了颇有心得体会,亲自动手调试实践才有了自己一些浅谈技巧,故写下《STM32单片机+USART2+DMA+IDLE空闲中断来接受数据》这篇博客,拙作一篇,敬请斧正,本文章期望起到一个抛砖引玉的作用。

一.参考资料

1.STM32F10x_StdPeriph_Lib_V3.5.0\Project\STM32F10x_StdPeriph_Examples\USART\DMA_Interrupt
2.STM32中文参考手册_V10

二.实现源码

/*  PA2--->TX2      PA3--->RX2        
    初始化注意时钟USART2是挂在APB1-->RCC_APB1Periph_USART2!!!
		USART2_TX  ---->DMA1 通道7
		USART2_RX  ---->DMA1 通道6  <参见STM32F10xxx参考手册P148>
*/

#include "stm32f10x.h"
#include "USART_IDLE_DMA.h"  
#include "stdio.h"

uint32_t   RxBuffer[20];  
/* Private define ------------------------------------------------------------*/
#define RxBufferSize   (countof(RxBuffer) - 1)
/* Private macro -------------------------------------------------------------*/
#define countof(a)   (sizeof(a) / sizeof(*(a)))

void RCC_Configuration()
{
	  /* DMA clock enable */
  RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

  /* Enable GPIO clock */
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);
  RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);
	
}

void NVIC_Configuration(void)
{
   NVIC_InitTypeDef NVIC_InitStructure;

  /* Enable the USART2 Interrupt */
  NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  NVIC_Init(&NVIC_InitStructure);
}

	void GPIO_Configuration(void)
{
  GPIO_InitTypeDef GPIO_InitStructure;

  /* Configure USART2 Rx as input floating */
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
  GPIO_Init(GPIOA, &GPIO_InitStructure);
   
  /* Configure USART2 Tx as alternate function push-pull */
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
  GPIO_Init(GPIOA, &GPIO_InitStructure);
}
	

void DMA_Configuration(void)
{
  DMA_InitTypeDef DMA_InitStructure;

  /* USART2_Rx_DMA_Channel 6  Config */
  DMA_DeInit(DMA1_Channel6);  
  DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART2->DR;   //搬运数据的开始地
  DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)RxBuffer;          //搬运数据的目的地
  DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;  //指定外围设备是源设备还是目标设备,这里指USART2
  DMA_InitStructure.DMA_BufferSize = RxBufferSize;    //以字节单位指定指定通道的缓冲区大小。
	
  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_InitStructure.DMA_Mode = DMA_Mode_Normal;
  DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh;
  DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
	
  DMA_Init(DMA1_Channel6, &DMA_InitStructure);
}


/* USART2 configuration -------------------------------------------*/
  /*    - BaudRate = 115200 baud  
        - Word Length = 8 Bits
        - One Stop Bit
        - No parity
        - Hardware flow control disabled (RTS and CTS signals)
        - Receive and transmit enabled
  */
void USART2_IDLE_DMA_Init(void)
{
	USART_InitTypeDef USART_InitStructure;
	/* System Clocks Configuration */
  RCC_Configuration();
       
  /* NVIC configuration */
  NVIC_Configuration();

  /* Configure the GPIO ports */
  GPIO_Configuration();
	
  DMA_Configuration();
	
	

  USART_InitStructure.USART_BaudRate = 115200;
  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;
  
  /* Configure USART2 */
  USART_Init(USART2, &USART_InitStructure);

  /* Enable USART2 DMA RX request */
  USART_DMACmd(USART2, USART_DMAReq_Rx, ENABLE);

  /* Enable the USART2 Receive Interrupt */
//  USART_ITConfig(USART2, USART_IT_RXNE, ENABLE);

	/* Enable the USART2 IDLE Interrupt */
  USART_ITConfig(USART2, USART_IT_IDLE, ENABLE);
  
  /* Enable USART2 */
  USART_Cmd(USART2, ENABLE);

  /* Enable USART2 DMA RX Channel */
  DMA_Cmd(DMA1_Channel6, ENABLE);

}

//printf重定向
#ifdef __GNUC__
  /* With GCC/RAISONANCE, small printf (option LD Linker->Libraries->Small printf
     set to 'Yes') calls __io_putchar() */
  #define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
  #define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif /* __GNUC__ */
	
PUTCHAR_PROTOTYPE
{
  /* Place your implementation of fputc here */
  /* e.g. write a character to the USART */
  USART_SendData(USART2  , (uint8_t) ch);

  /* Loop until the end of transmission */
  while (USART_GetFlagStatus(USART2  , USART_FLAG_TC) == RESET)
  {}

  return ch;
}

/*当检测到总线空闲时,该位被硬件置位。如果USART_CR1中的IDLEIE为’1’,则产生中断。由
软件序列清除该位(先读USART_SR,然后读USART_DR)。*

void USART2_IRQHandler(void)
{
	   uint8_t USART2_ReceSize;
	  if(USART_GetITStatus(USART2, USART_IT_IDLE) != RESET)
		{
			   USART2->SR;
			   USART2->DR;
			 /*计算出接受这一帧数据的长度*/
			 USART2_ReceSize = RxBufferSize-DMA_GetCurrDataCounter(DMA1_Channel6); 
			 
			/*以下是数据接受以后,对数据处理进行一个简单的模拟,在真实项目中,中断服务函数的
			代码应该短小精悍!所以我们一般这样处理:在中断服务函数(前台)中,设置一个接受完成标志位
			Rece_Flag , 在Main主函数(我们称为后台)while()大循环中判断标志位进行处理数据*/
			printf ("The Data length is:%d\r\n",USART2_ReceSize);
			printf ("Eterlove! Sending data completed\r\n");
		    printf ("The data:%s\r\n",RxBuffer);
		    memset(RxBuffer,0,sizeof(RxBuffer));
		    /*************************************/
		    DMA_Cmd(DMA1_Channel6, DISABLE );  	//关闭USART2 RX DMA1所指示的通道    
            DMA_SetCurrDataCounter(DMA1_Channel6,RxBufferSize); //现在我们得知这帧数据的真实长度,重新设置DMA通道的缓冲区大小
            DMA_Cmd(DMA1_Channel6, ENABLE);  	//打开USART2 RX DMA1所指示的通道 
		}
  
}

三. 基本讲解

(1)对宏定义不懂请看这------>(sizeof(a) / sizeof(*(a)))解析

/* Private define ------------------------------------------------------------*/
#define RxBufferSize   (countof(RxBuffer) - 1)
/* Private macro -------------------------------------------------------------*/
#define countof(a)   (sizeof(a) / sizeof(*(a)))

(2)接受数据思路是: 数据到来时,DMA直接从USART2的DR处运输数据到我定义的数组RxBuffer[ ]内,中间无需CPU的参与。 只需要CPU开始时告诉DMA从哪里搬数据(源地址),搬到哪里去(目标地址)即可,数据传输结束后,DMA只需要告诉CPU一声 ”数据我搬完了“

  DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART2->DR;   //搬运数据的开始地
  DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)RxBuffer;          //搬运数据的目的地
  DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;  //指定外围设备是源设备还是目标设备,这里指USART2
  DMA_InitStructure.DMA_BufferSize = RxBufferSize;    //以字节单位指定指定通道的缓冲区大小。

(3)讲解以下如何算出收到的数据长度(字节数) ,首先最开始我并不知道数据长度有多少,所以我定义一个比较长的数组RxBuffer[20] ;通过我定义的宏运算《参见讲解(1)》来算出RxBufferSize。所以我设置DMA通道缓冲区为RxBufferSize。

 DMA_InitStructure.DMA_BufferSize = RxBufferSize;    //以字节单位指定指定通道的缓冲区大小。

那我们调用这个函数干什么呢?DMA_GetCurrDataCounter(),请看函数原型:

uint16_t DMA_GetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx)
{
  /* Check the parameters */
  assert_param(IS_DMA_ALL_PERIPH(DMAy_Channelx));
  /* Return the number of remaining data units for DMAy Channelx */
  return ((uint16_t)(DMAy_Channelx->CNDTR));
}

这个函数去读CNDTR寄存器的值,参见STM32参考手册,这个寄存器中的值是DMA通道设置的传输字节数,注意:每接受数据一字节,此值将被递减一字节

例如设置RxBufferSize为20 , 此时接受到5个字节数的数据 ,CNDTR寄存器的值递减5个字节(RxBufferSize减去5个字节),此时我们调用DMA_GetCurrDataCounter()函数去读CNDTR寄存器的值为20-5=15.所以我们调用DMA_GetCurrDataCounter()函数返回的是DMA通道缓冲区的剩余字节长度。

接收的字符串长度=设置的接收长度 - 剩余DMA缓存大小

所以计算出接受这一帧数据的长度如下图所示。

/*计算出接受这一帧数据的长度*/
 USART2_ReceSize = RxBufferSize-DMA_GetCurrDataCounter(DMA1_Channel6); 
			 
 DMA_SetCurrDataCounter(DMA1_Channel6,RxBufferSize); 
   //现在我们得知这帧数据的真实长度,重新设置DMA通道的缓冲区大小

(4)如何清除IDLE空闲中断的标志位 : 先读USART_SR,然后读USART_DR ,为什么?,参考STM32手册P541 , 当检测到总线空闲时,该位被硬件置位。如果USART_CR1中的IDLEIE为’1’,则产生中断。由软件序列清除该位(先读USART_SR,然后读USART_DR)。

(5)中断服务函数的处理
    以下是数据接受以后,对数据处理进行一个简单的模拟。

	printf ("The Data length is:%d\r\n",USART2_ReceSize);
	printf ("Eterlove! Sending data completed\r\n");
    printf ("The data:%s\r\n",RxBuffer);
	memset(RxBuffer,0,sizeof(RxBuffer));

    在真实项目中,中断服务函数的代码应该短小精悍!所以我们一般这样处理:在中断服务函数(前台)中,设置一个接受完成标志位Rece_Flag , 在Main主函数(我们称为后台while()大循环中判断标志位进行处理数据。
改进如下:

 _Bool Rece_Flag = 0;    //接受完成标志

int main(void)
{
	SysTick_Config(SystemCoreClock/1000);
	Delay_Ms(200);
	STM3210B_LCD_Init();
	LCD_Clear(Black);
	LCD_SetBackColor(Black);  
	LCD_SetTextColor(White);
	Led_Init();    //LED初始化
	Led_Control(LED_ALL , OFF); //LED全关
    Key_Init();  //按键初始化
  
	USART2_IDLE_DMA_Init();
	while(1)
	{
		
		if(Rece_Flag==1)
		{
		    Rece_Flag = 0;
			  printf ("The Data length is:%d\r\n",USART2_ReceSize);
			  printf ("Eterlove! Sending data completed\r\n");
		      printf ("The data:%s\r\n",RxBuffer);
		      memset(RxBuffer,0,sizeof(RxBuffer));
		}		
	}
} 

//中断服务函数处理
void USART2_IRQHandler(void)
{
	   
	  if(USART_GetITStatus(USART2, USART_IT_IDLE) != RESET)
		{
			  USART2->SR;
			  USART2->DR;
			  USART2_ReceSize =RxBufferSize-DMA_GetCurrDataCounter(DMA1_Channel6); //计算出接受这一帧数据的长度
			  
			  Rece_Flag = 1;
			  
		    DMA_Cmd(DMA1_Channel6, DISABLE );  											//关闭USART2 RX DMA1所指示的通道    
        DMA_SetCurrDataCounter(DMA1_Channel6,RxBufferSize);			//现在我们得知这帧数据的真实长度,重新设置DMA通道的缓冲区大小
        DMA_Cmd(DMA1_Channel6, ENABLE);  												//打开USART2 RX DMA1所指示的通道 
		}
  
}

四.实验现象

                                          

有关《嵌入式蓝桥杯》STM32单片机+USART2+DMA+IDLE空闲中断来接收数据的更多相关文章

  1. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  2. ruby - Ruby 有 `Pair` 数据类型吗? - 2

    有时我需要处理键/值数据。我不喜欢使用数组,因为它们在大小上没有限制(很容易不小心添加超过2个项目,而且您最终需要稍后验证大小)。此外,0和1的索引变成了魔数(MagicNumber),并且在传达含义方面做得很差(“当我说0时,我的意思是head...”)。散列也不合适,因为可能会不小心添加额外的条目。我写了下面的类来解决这个问题:classPairattr_accessor:head,:taildefinitialize(h,t)@head,@tail=h,tendend它工作得很好并且解决了问题,但我很想知道:Ruby标准库是否已经带有这样一个类? 最佳

  3. ruby - 我如何添加二进制数据来遏制 POST - 2

    我正在尝试使用Curbgem执行以下POST以解析云curl-XPOST\-H"X-Parse-Application-Id:PARSE_APP_ID"\-H"X-Parse-REST-API-Key:PARSE_API_KEY"\-H"Content-Type:image/jpeg"\--data-binary'@myPicture.jpg'\https://api.parse.com/1/files/pic.jpg用这个:curl=Curl::Easy.new("https://api.parse.com/1/files/lion.jpg")curl.multipart_form_

  4. 世界前沿3D开发引擎HOOPS全面讲解——集3D数据读取、3D图形渲染、3D数据发布于一体的全新3D应用开发工具 - 2

    无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD

  5. FOHEART H1数据手套驱动Optitrack光学动捕双手运动(Unity3D) - 2

    本教程将在Unity3D中混合Optitrack与数据手套的数据流,在人体运动的基础上,添加双手手指部分的运动。双手手背的角度仍由Optitrack提供,数据手套提供双手手指的角度。 01  客户端软件分别安装MotiveBody与MotionVenus并校准人体与数据手套。MotiveBodyMotionVenus数据手套使用、校准流程参照:https://gitee.com/foheart_1/foheart-h1-data-summary.git02  数据转发打开MotiveBody软件的Streaming,开始向Unity3D广播数据;MotionVenus中设置->选项选择Unit

  6. 使用canal同步MySQL数据到ES - 2

    文章目录一、概述简介原理模块二、配置Mysql使用版本环境要求1.操作系统2.mysql要求三、配置canal-server离线下载在线下载上传解压修改配置单机配置集群配置分库分表配置1.修改全局配置2.实例配置垂直分库水平分库3.修改group-instance.xml4.启动监听四、配置canal-adapter1修改启动配置2配置映射文件3启动ES数据同步查询所有订阅同步数据同步开关启动4.验证五、配置canal-admin一、概述简介canal是Alibaba旗下的一款开源项目,Java开发。基于数据库增量日志解析,提供增量数据订阅&消费。Git地址:https://github.co

  7. ruby-on-rails - 创建 ruby​​ 数据库时惰性符号绑定(bind)失败 - 2

    我正在尝试在Rails上安装ruby​​,到目前为止一切都已安装,但是当我尝试使用rakedb:create创建数据库时,我收到一个奇怪的错误:dyld:lazysymbolbindingfailed:Symbolnotfound:_mysql_get_client_infoReferencedfrom:/Library/Ruby/Gems/1.8/gems/mysql2-0.3.11/lib/mysql2/mysql2.bundleExpectedin:flatnamespacedyld:Symbolnotfound:_mysql_get_client_infoReferencedf

  8. STM32读取串口传感器数据(颗粒物传感器,主动上传) - 2

    文章目录1.开发板选择*用到的资源2.串口通信(个人理解)3.代码分析(注释比较详细)1.主函数2.串口1配置3.串口2配置以及中断函数4.注意问题5.源码链接1.开发板选择我用的是STM32F103RCT6的板子,不过代码大概在F103系列的板子上都可以运行,我试过在野火103的霸道板上也可以,主要看一下串口对应的引脚一不一样就行了,不一样的就更改一下。*用到的资源keil5软件这里用到了两个串口资源,采集数据一个,串口通信一个,板子对应引脚如下:串口1,TX:PA9,RX:PA10串口2,TX:PA2,RX:PA32.串口通信(个人理解)我就从串口采集传感器数据这个过程说一下我自己的理解,

  9. C51单片机——实现用独立按键控制LED亮灭(调用函数篇) - 2

    说在前面这部分我本来是合为一篇来写的,因为目的是一样的,都是通过独立按键来控制LED闪灭本质上是起到开关的作用,即调用函数和中断函数。但是写一篇太累了,我还是决定分为两篇写,这篇是调用函数篇。在本篇中你主要看到这些东西!!!1.调用函数的方法(主要讲语法和格式)2.独立按键如何控制LED亮灭3.程序中的一些细节(软件消抖等)1.调用函数的方法思路还是比较清晰地,就是通过按下按键来控制LED闪灭,即每按下一次,LED取反一次。重要的是,把按键与LED联系在一起。我打算用K1来作为开关,看了一下开发板原理图,K1连接的是单片机的P31口,当按下K1时,P31是与GND相连的,也就是说,当我按下去时

  10. SPI接收数据异常问题总结 - 2

    SPI接收数据左移一位问题目录SPI接收数据左移一位问题一、问题描述二、问题分析三、探究原理四、经验总结最近在工作在学习调试SPI的过程中遇到一个问题——接收数据整体向左移了一位(1bit)。SPI数据收发是数据交换,因此接收数据时从第二个字节开始才是有效数据,也就是数据整体向右移一个字节(1byte)。请教前辈之后也没有得到解决,通过在网上查阅前人经验终于解决问题,所以写一个避坑经验总结。实际背景:MCU与一款芯片使用spi通信,MCU作为主机,芯片作为从机。这款芯片采用的是它规定的六线SPI,多了两根线:RDY和INT,这样从机就可以主动请求主机给主机发送数据了。一、问题描述根据从机芯片手

随机推荐