2018-09-13

# 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请求的优先权。

![STM32F1](https://note.youdao.com/yws/api/personal/file/1A5309B80FAB41709CC8A229D588403B?method=download&shareKey=7e8848c6228cc98c6df45ec4ac934052)

![STM32F1](https://note.youdao.com/yws/api/personal/file/E54C58EFB905499CAFA4D678A5DB0FB7?method=download&shareKey=f7973380c4d883e932fc9d1026f96471)

**而STM32F4/F7/H7系列**的MCU有两个DMA控制器总共有16个数据流(每个DMA控制器8个),每一个DMA控制器都用于管理一个或多个外设的存储器访问请求。每个数据流总共可以有多达8个通道(或称请求)。每个通道都有一个仲裁器,用于处理 DMA 请求间的优先级。

![STM32F4](https://note.youdao.com/yws/api/personal/file/4A54FECA588047CDB48D3C699D8612EA?method=download&shareKey=2b1058e7f1a578b1f8e56a52c0894f66)

![STM32F4](https://note.youdao.com/yws/api/personal/file/B0059ABB61B64806AC6056A03AD56992?method=download&shareKey=b3502264cc2d29f2275646f5fcb81dda )

### DMA接收数据

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

### 判断数据接收完成

> 那么问题来了,怎么知道数据是否接收完成呢?

其实,有很多方法:

- 对于定长的数据,只需要判断一下数据的接收个数,就知道是否接收完成,这个很简单,暂不讨论。

- 对于不定长的数据,其实也有好几种方法,麻烦的我肯定不会介绍,有兴趣做复杂工作的同学可以在网上看看别人怎么做,下面这种方法是最简单的,充分利用了stm32的串口资源,效率也是非常之高。

**DMA+串口空闲中断**

这两个资源配合,简直就是天衣无缝啊,无论接收什么不定长的数据,管你数据有多少,来一个我就收一个,就像广东人吃“山竹”,来一个吃一个~(最近风好大,我好怕)。

可能很多人在学习stm32的时候,都不知道idle是啥东西,先看看stm32串口的状态寄存器:

![idle](https://note.youdao.com/yws/api/personal/file/WEBf3c67833dc9c0ce2be4ac912925d1bcb?method=download&shareKey=3a524caa59faf8819048d98121e76b5c)

![idle说明](https://note.youdao.com/yws/api/personal/file/WEBb144880886147b85f8602a8f7f117941?method=download&shareKey=04179de3da48c34ee0c3ca78920d7ccf)

当我们检测到触发了串口总线空闲中断的时候,我们就知道这一波数据传输完成了,然后我们就能得到这些数据,去进行处理即可。这种方法是最简单的,根本不需要我们做多的处理,只需要配置好,串口就等着数据的到来,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来不及处理此次接收到的数据,中断又发来数据的话,这里不能开启,否则数据会被覆盖。有两种方式解决:

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

2. 建立双缓冲,重新配置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更新的文章~中国电信还是强,台风天气信号一点都不虚,我的移动卡一动不动-_-\.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容