STM32 之串口 DMA 接收不定长数据
引言
在使用 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 请求的优先权。
而 STM32F4/F7/H7 系列 的 MCU 有两个 DMA 控制器总共有 16 个数据流(每个 DMA 控制器 8 个),每一个 DMA 控制器都用于管理一个或多个外设的存储器访问请求。每个数据流总共可以有多达 8 个通道(或称请求)。每个通道都有一个仲裁器,用于处理 DMA 请求间的优先级。
DMA 接收数据
DMA 在接收数据的时候,串口接收 DMA 在初始化的时候就处于开启状态,一直等待数据的到来,在软件上无需做任何事情,只要在初始化配置的时候设置好配置就可以了。等到接收到数据的时候,告诉 CPU 去处理即可。
判断数据接收完成
那么问题来了,怎么知道数据是否接收完成呢?
其实,有很多方法:
- 对于定长的数据,只需要判断一下数据的接收个数,就知道是否接收完成,这个很简单,暂不讨论。
- 对于不定长的数据,其实也有好几种方法,麻烦的我肯定不会介绍,有兴趣做复杂工作的同学可以在网上看看别人怎么做,下面这种方法是最简单的,充分利用了 stm32 的串口资源,效率也是非常之高。
DMA+ 串口空闲中断
这两个资源配合,简直就是天衣无缝啊,无论接收什么不定长的数据,管你数据有多少,来一个我就收一个,就像广东人吃“山竹”,来一个吃一个~(最近风好大,我好怕)。
可能很多人在学习 stm32 的时候,都不知道 idle 是啥东西,先看看 stm32 串口的状态寄存器:
当我们检测到触发了串口总线空闲中断的时候,我们就知道这一波数据传输完成了,然后我们就能得到这些数据,去进行处理即可。这种方法是最简单的,根本不需要我们做多的处理,只需要配置好,串口就等着数据的到来,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 更新的文章~ 中国电信还是强,台风天气信号一点都不虚,我的移动卡一动不动 -_-.
喜欢就关注我吧!
相关代码可以在公众号后台回复获取。
欢迎关注“物联网 IoT 开发”公众号