在使用stm32或者其余单片机的时候,会常用到串口通信,那么如何有效地接收数据呢?假如这段数据是不定长的有如何高效接收呢?git
同窗A:数据来了就会进入串口中断,在中断中读取数据就好了!中断就是打断程序正常运行,怎么能保证高效呢?常常把主程序打断,主程序还要不要运行了?github
同窗B:串口能够配置成用DMA的方式接收数据,等接收完毕就能够去读取了!算法
这个同窗是对的,咱们可使用DMA去接收数据,不过DMA须要定长才能产生接收中断,如何接收不定长的数据呢?segmentfault
题外话:其实,上面的问题是颇有必要思考一下的,不断思考,才能进步。
DMA:全称Direct Memory Access,即直接存储器访问数组
DMA 传输将数据从一个地址空间复制到另一个地址空间。CPU只需初始化DMA便可,传输动做自己是由 DMA 控制器来实现和完成。典型的例子就是移动一个外部内存的区块到芯片内部更快的内存区。这样的操做并无让处理器参与处理,CPU能够干其余事情,当DMA传输完成的时候产生一个中断,告诉CPU我已经完成了,而后CPU知道了就能够去处理数据了,这样子提升了CPU的利用率,由于CPU是大脑,主要作数据运算的工做,而不是去搬运数据。DMA 传输对于高效能嵌入式系统算法和网络是很重要的。网络
STM32F1系列的MCU有两个DMA控制器(DMA2只存在于大容量产品中),DMA1有7个通道,DMA2有5个通道,每一个通道专门用来管理来自于一个或者多个外设对存储器的访问请求。还有一个仲裁器来协调各个DMA请求的优先权。函数
而STM32F4/F7/H7系列的MCU有两个DMA控制器总共有16个数据流(每一个DMA控制器8个),每个DMA控制器都用于管理一个或多个外设的存储器访问请求。每一个数据流总共能够有多达8个通道(或称请求)。每一个通道都有一个仲裁器,用于处理 DMA 请求间的优先级。学习
DMA在接收数据的时候,串口接收DMA在初始化的时候就处于开启状态,一直等待数据的到来,在软件上无需作任何事情,只要在初始化配置的时候设置好配置就能够了。等到接收到数据的时候,告诉CPU去处理便可。ui
那么问题来了,怎么知道数据是否接收完成呢?
其实,有不少方法:spa
DMA+串口空闲中断
这两个资源配合,简直就是完美无缺啊,不管接收什么不定长的数据,管你数据有多少,来一个我就收一个,就像广东人吃“山竹”,来一个吃一个~(最近风好大,我好怕)。
可能不少人在学习stm32的时候,都不知道idle是啥东西,先看看stm32串口的状态寄存器:
当咱们检测到触发了串口总线空闲中断的时候,咱们就知道这一波数据传输完成了,而后咱们就能获得这些数据,去进行处理便可。这种方法是最简单的,根本不须要咱们作多的处理,只须要配置好,串口就等着数据的到来,dma也是处于工做状态的,来一个数据就自动搬运一个数据。
串口接收完数据是要处理的,那么处理的步骤是怎么样呢?
注意事项
STM32的IDLE的中断在串口无数据接收的状况下,是不会一直产生的,产生的条件是这样的,当清除IDLE标志位后,必须有接收到第一个数据后,才开始触发,一断接收的数据断流,没有接收到数据,即产生IDLE中断。若是中断发送数据帧的速率很快,MCU来不及处理这次接收到的数据,中断又发来数据的话,这里不能开启,不然数据会被覆盖。有两种方式解决:
实验效果:
当外部给单片机发送数 据的时候,假设这帧数据长度是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更新的文章~中国电信仍是强,台风天气信号一点都不虚,个人移动卡一动不动-_-.
相关代码能够在公众号后台回复获取。
欢迎关注“物联网IoT开发”公众号