精华内容
下载资源
问答
  • 在正点原子MINI开发板上测试的,串口DMA间隔2ms发送一次数据,串口空闲接收不定长数据。
  • 6路串口DMA接收,串口dma发送数据,CC++源码.zip.zip
  • 前言: 本系列教程将 对应外设原理,HAL库与STM32CubeMX结合在一起讲解,使您可以更快速的学会各个模块的使用 所用工具: 1、芯片: STM32F407ZET6/ STM32F103ZET6 ...DMA工作原理 STM32CubeMX创建...

    前言:
    本系列教程将 对应外设原理,HAL库与STM32CubeMX结合在一起讲解,使您可以更快速的学会各个模块的使用

    所用工具:

    1、芯片: STM32F407ZET6/ STM32F103ZET6

    2、STM32CubeMx软件

    3、IDE: MDK-Keil软件

    4、STM32F1xx/STM32F4xxHAL库

    知识概括:

    通过本篇博客您将学到:

    DMA工作原理

    STM32CubeMX创建DMA例程

    HAL库定时器DMA函数库

    注意:关于cubemx的DMA配置,在DMA原理介绍中全部都有所讲解,如果有哪里不懂,请仔细阅读原理详解部分。

    DMA的基本介绍

    什么是DMA (DMA的基本定义)

    DMA,全称Direct Memory Access,即直接存储器访问。

    DMA传输将数据从一个地址空间复制到另一个地址空间,提供在外设和存储器之间或者存储器和存储器之间的高速数据传输

    我们知道CPU有转移数据、计算、控制程序转移等很多功能,系统运作的核心就是CPU

    CPU无时不刻的在处理着大量的事务,但有些事情却没有那么重要,比方说数据的复制和存储数据,如果我们把这部分的CPU资源拿出来,让CPU去处理其他的复杂计算事务,是不是能够更好的利用CPU的资源呢?

    因此:转移数据(尤其是转移大量数据)是可以不需要CPU参与。比如希望外设A的数据拷贝到外设B,只要给两种外设提供一条数据通路,直接让数据由A拷贝到B 不经过CPU的处理
    在这里插入图片描述
     DMA就是基于以上设想设计的,它的作用就是解决大量数据转移过度消耗CPU资源的问题。有了DMA使CPU更专注于更加实用的操作–计算、控制等。

    DMA定义:

    DMA用来提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。无须CPU的干预,通过DMA数据可以快速地移动。这就节省了CPU的资源来做其他操作。

    DMA传输方式

    DMA的作用就是实现数据的直接传输,而去掉了传统数据传输需要CPU寄存器参与的环节,主要涉及四种情况的数据传输,但本质上是一样的,都是从内存的某一区域传输到内存的另一区域(外设的数据寄存器本质上就是内存的一个存储单元)。四种情况的数据传输如下:

    • 外设到内存
    • 内存到外设
    • 内存到内存
    • 外设到外设

    DMA传输参数

    我们知道,数据传输,首先需要的是1 数据的源地址 2 数据传输位置的目标地址 ,3 传递数据多少的数据传输量 ,4 进行多少次传输的传输模式 DMA所需要的核心参数,便是这四个

    当用户将参数设置好,主要涉及源地址、目标地址、传输数据量这三个,DMA控制器就会启动数据传输,当剩余传输数据量为0时 达到传输终点,结束DMA传输 ,当然,DMA 还有循环传输模式 当到达传输终点时会重新启动DMA传输。
      
    也就是说只要剩余传输数据量不是0,而且DMA是启动状态,那么就会发生数据传输。  
    在这里插入图片描述

    DMA的主要特征

    每个通道都直接连接专用的硬件DMA请求,每个通道都同样支持软件触发。这些功能通过软件来配置;

    • 在同一个DMA模块上,多个请求间的优先权可以通过软件编程设置(共有四级:很高、高、中等和低),优先权设置相等时由硬件决定(请求0优先于请求1,依此类推);
    • 独立数据源和目标数据区的传输宽度(字节、半字、全字),模拟打包和拆包的过程。源和目标地址必须按数据传输宽度对齐;
    • 支持循环的缓冲器管理;
    • 每个通道都有3个事件标志(DMA半传输、DMA传输完成和DMA传输出错),这3个事件标志逻辑或成为一个单独的中断请求;
    • 存储器和存储器间的传输、外设和存储器、存储器和外设之间的传输;
    • 闪存、SRAM、外设的SRAM、APB1、APB2和AHB外设均可作为访问的源和目标;
    • 可编程的数据传输数目:最大为65535。

    STM32少个DMA资源?

    对于大容量的STM32芯片有2个DMA控制器 两个DMA控制器,DMA1有7个通道,DMA2有5个通道。
    每个通道都可以配置一些外设的地址。

    ①DMA1 controller

    从外设(TIMx[x=1、2、3、4]、ADC1、SPI1、SPI/I2S2、I2Cx[x=1、2]和USARTx[x=1、2、3])产生的7个DMA请求,通过逻辑或输入到DMA1控制器 其中每个通道都对应着具体的外设:
    在这里插入图片描述
    在这里插入图片描述
    ② DMA2 controller

    从外设(TIMx[5、6、7、8]、ADC3、SPI/I2S3、UART4、DAC通道1、2和SDIO)产生的5个请求,经逻辑或输入到DMA2控制器,其中每个通道都对应着具体的外设:
    在这里插入图片描述
    在这里插入图片描述

    这些在下方系统框图中也可以清晰地看到

    DMA工作系统框图

    在这里插入图片描述
    上方的框图,我们可以看到STM32内核,存储器,外设及DMA的连接,这些硬件最终通过各种各样的线连接到总线矩阵中,硬件结构之间的数据转移都经过总线矩阵的协调,使各个外设和谐的使用总线来传输数据。
    我们对他来进行一点一点的分析:

    下面看有与没有DMA的情况下,ADC采集的数据是怎样存放到SRAM中的?

    没有DMA

    1.如果没有DMA,CPU传输数据还要以内核作为中转站,比如要将ADC采集的数据转移到到SRAM中,这个过程是这样的:

    内核通过DCode经过总线矩阵协调,从获取AHB存储的外设ADC采集的数据,

    然后内核再通过DCode经过总线矩阵协调把数据存放到内存SRAM中。

    在这里插入图片描述

    有DMA传输

    有DMA的话,

    1. DMA传输时外设对DMA控制器发出请求。
    2. DMA控制器收到请求,触发DMA工作。
    3. DMA控制器从AHB外设获取ADC采集的数据,存储到DMA通道中
    4. DMA控制器的DMA总线与总线矩阵协调,使用AHB把外设ADC采集的数据经由DMA通道存放到SRAM中,这个数据的传输过程中,完全不需要内核的参与,也就是不需要CPU的参与,

    在这里插入图片描述

    我们把上面的步骤专业一点介绍:

    在发生一个事件后,外设向DMA控制器发送一个请求信号。DMA控制器根据通道的优先权处理请求。当DMA控制器开始访问发出请求的外设时,DMA控制器立即发送给它一个应答信号。当从DMA控制器得到应答信号时,外设立即释放它的请求。一旦外设释放了这个请求,DMA控制器同时撤销应答信号。DMA传输结束,如果有更多的请求时,外设可以启动下一个周期。

    总之,每次DMA传送由3个操作组成:

    • 从外设数据寄存器或者从当前外设/存储器地址寄存器指示的存储器地址取数据,第一次传输时的开始地址是DMA_CPARx或DMA_CMARx寄存器指定的外设基地址或存储器单元;
    • 存数据到外设数据寄存器或者当前外设/存储器地址寄存器指示的存储器地址,第一次传输时的开始地址是DMA_CPARx或DMA_CMARx寄存器指定的外设基地址或存储器单元;
    • 执行一次DMA_CNDTRx寄存器的递减操作,该寄存器包含未完成的操作数目

    DMA传输方式

    方法1:DMA_Mode_Normal正常模式,

    当一次DMA数据传输完后,停止DMA传送 ,也就是只传输一次
      
    方法2:DMA_Mode_Circular循环传输模式

    当传输结束时,硬件自动会将传输数据量寄存器进行重装,进行下一轮的数据传输。 也就是多次传输模式

    仲裁器

    在这里插入图片描述
    仲裁器的作用是确定各个DMA传输的优先级

    仲裁器根据通道请求的优先级来启动外设/存储器的访问。

    优先权管理分2个阶段:

    软件:每个通道的优先权可以在DMA_CCRx寄存器中设置,有4个等级:

    • 最高优先级
    • 高优先级
    • 中等优先级
    • 低优先级;

    硬件:如果2个请求有相同的软件优先级,则较低编号的通道比较高编号的通道有较高的优先权。比如:如果软件优先级相同,通道2优先于通道4。

    注意: 在大容量产品和互联型产品中,DMA1控制器拥有高于DMA2控制器的优先级。

    指针递增模式

    根据 DMA_SxCR 寄存器中 PINC 和 MINC 位的状态,外设和存储器指针在每次传输后可以自动向后递增或保持常量。当设置为增量模式时,下一个要传输的地址将是前一个地址加上增量值

    通过单个寄存器访问外设源或目标数据时,禁止递增模式十分有用。

    如果使能了递增模式,则根据在 DMA_SxCR 寄存器 PSIZE 或 MSIZE 位中编程的数据宽度,下一次传输的地址将是前一次传输的地址递增 1个数据宽度、2个数据宽度或 4个数据宽度。

    存储器到存储器模式

    DMA通道的操作可以在没有外设请求的情况下进行,这种操作就是存储器到存储器模式。

    当设置了DMA_CCRx寄存器中的MEM2MEM位之后,在软件设置了DMA_CCRx寄存器中的EN位启动DMA通道时,DMA传输将马上开始。当DMA_CNDTRx寄存器变为0时,DMA传输结束。存储器到存储器模式不能与循环模式同时使用。

    这里要注意仅 DMA2 的外设接口可以访问存储器,所以仅 DMA2 控制器支持存储器到存储器的传输,DMA1 不支持。

    存储器到存储器模式不能与循环模式同时使用。

    DMA中断

    每个DMA通道都可以在DMA传输过半、传输完成和传输错误时产生中断。为应用的灵活性考虑,通过设置寄存器的不同位来打开这些中断。

    在这里插入图片描述
    使没开启,我们也可以通过查询这些位来获得当前 DMA 传输的状态。这里我们常用的是 TCIFx位,即数据流 x 的 DMA 传输完成与否标志。

    DMA库函数配置过程:

    1、使能DMA时钟:RCC_AHBPeriphClockCmd();

    2、初始化DMA通道:DMA_Init();

    //设置通道;传输地址;传输方向;传输数据的数目;传输数据宽度;传输模式;优先级;是否开启存储器到存储器。

    3、使能外设DMA;

    4、使能DMA通道传输;

    5、查询DMA传输状态。

    关于DMA的介绍我们仅介绍到这里,如果需要更详细的了解DMA原理 DMA寄存器以及库函数 可以参考这篇文章
    《【STM32】 DMA原理,步骤超细详解,一文看懂DMA》




    下面我们将介绍CubeMx 如何创建DMA

    具体流程如下:
    在这里插入图片描述
    我们以USART1 的DMA传输为例

    工程创建

    1设置RCC
    在这里插入图片描述
    设置高速外部时钟HSE 选择外部时钟源

    2设置串口
    在这里插入图片描述

    • 1点击USATR1
    • 2设置MODE为异步通信(Asynchronous)
    • 3基础参数:波特率为115200 Bits/s。传输数据长度为8 Bit。奇偶检验无,停止位1 接收和发送都使能
    • 4GPIO引脚自动设置 USART1_RX/USART_TX
    • 5 NVIC Settings 一栏使能接收中断
      ​​
      在这里插入图片描述
      关于串口部分的讲解可以参考: 【STM32】HAL库 STM32CubeMX教程四—UART串口通信详解

    3 DMA设置

    在这里插入图片描述
    根据DMA通道预览可以知道,我们用的USART1 的TX RX 分别对应DMA1 的通道4和通道5

    • 点击DMASettings 点击 Add 添加通道
    • 选择USART_RX USART_TX 传输速率设置为中速
    • DMA传输模式为正常模式
    • DMA内存地址自增,每次增加一个Byte(字节)

    1DMA基础设置

    右侧点击System Core 点击DMA
    在这里插入图片描述
    DMA RequestDMA传输的对应外设

    注意: 如果你是在DMA设置界面添加DMA 而没有开启对应外设的话 ,默认为MENTOMEN

    Channel DMA传输通道设置
    DMA1 : DMA1 Channel 0~DMA1 Channel 7
    DMA2: DMA2 Channel 1~DMA1 Channel 5

    Dirction : DMA传输方向
    四种传输方向:

    • 外设到内存 Peripheral To Memory
    • 内存到外设 Memory To Peripheral
    • 内存到内存 Memory To Memory
    • 外设到外设 Peripheral To Peripheral

    Priority: 传输速度

    • 最高优先级 Very Hight
    • 高优先级 Hight
    • 中等优先级 Medium
    • 低优先级;Low

    2DMA传输模式
    在这里插入图片描述
    Normal:正常模式
    当一次DMA数据传输完后,停止DMA传送 ,也就是只传输一次

    Circular: 循环模式

    传输完成后又重新开始继续传输,不断循环永不停止

    3DMA指针递增设置
    在这里插入图片描述
    Increment Address:地址指针递增(上方有介绍)。

    左侧Src Memory 表示外设地址寄存器

    功能:设置传输数据的时候外设地址是不变还是递增。如果设置 为递增,那么下一次传输的时候地址加 Data Width个字节,

    右侧Dst Memory 表示内存地址寄存器

    功能:设置传输数据时候内存地址是否递增。如果设置 为递增,那么下一次传输的时候地址加 Data Width个字节,

    这个Src Memory一样,只不过针对的是内存。


    串口发送数据是将数据不断存进固定外设地址串口的发送数据寄存器(USARTx_TDR)。所以外设的地址是不递增。

    而内存储器存储的是要发送的数据,所以地址指针要递增,保证数据依次被发出

    在这里插入图片描述

    串口数据发送寄存器只能存储8bit,每次发送一个字节,所以数据长度选择Byte。在这里插入图片描述

    就是要注意DMA的传输方向别弄错了,到底是PERIPHERIAL到MEMORY还是MEMORY到PERIPHERIAL或者说是Memory到Memory要配置正确。尤其是在用CubeMx配置时,这里有个默认配置是PERIPHERIAL到MEMORY。如果说你的真实意图根本不是从PERIPHERIAL到MEMORY,而你无意中使用了这个默认配置,结果可想而知,DMA传输根本没法正常运行。

    4时钟源设置

    ​​​​在这里插入图片描述
    我的是 外部晶振为8MHz

    • 1选择外部时钟HSE 8MHz
    • 2PLL锁相环倍频9倍
    • 3系统时钟来源选择为PLL
    • 4设置APB1分频器为 /2
    • 5 使能CSS监视时钟

    32的时钟树框图 如果不懂的话请看《【STM32】系统时钟RCC详解(超详细,超全面)》

    5项目文件设置

    在这里插入图片描述

    • 1 设置项目名称
    • 2 设置存储路径
    • 3 选择所用IDE

    在这里插入图片描述
    5创建工程文件

    然后点击GENERATE CODE 创建工程

    配置下载工具
    新建的工程所有配置都是默认的 我们需要自行选择下载模式,勾选上下载后复位运行

    在这里插入图片描述

    测试例程1

    在main.C中添加:

     /* USER CODE BEGIN Init */
    	uint8_t Senbuff[] = "\r\n**** Serial Output Message by DMA ***\r\n   UART DMA Test \r\n   Zxiaoxuan";  //定义数据发送数组
      /* USER CODE END Init */
    

    while循环:

      while (1)
      {
        /* USER CODE END WHILE */
    			HAL_UART_Transmit_DMA(&huart1, (uint8_t *)Senbuff, sizeof(Senbuff));
    	        HAL_Delay(1000);
        /* USER CODE BEGIN 3 */
      }
    

    串口助手测试正常:
    在这里插入图片描述

    注意:如果不开启串口中断,则程序只能发送一次数据,程序不能判断DMA传输是否完成,USART一直处于busy状态。

    HAL库UARTDMA函数库介绍

    1、串口发送/接收函数

    • HAL_UART_Transmit();串口发送数据,使用超时管理机制
    • HAL_UART_Receive();串口接收数据,使用超时管理机制
    • HAL_UART_Transmit_IT();串口中断模式发送
    • HAL_UART_Receive_IT();串口中断模式接收
    • HAL_UART_Transmit_DMA();串口DMA模式发送
    • HAL_UART_Transmit_DMA();串口DMA模式接收
    • HAL_UART_DMAPause() 暂停串口DMA
    • HAL_UART_DMAResume(); 恢复串口DMA
    • HAL_UART_DMAStop(); 结束串口DMA

    因为这部分函数在讲解USART的时候就已经讲解了,所以我们这里不做过多介绍,如果不同的话请看UART的对应博文,很详细。

    串口DMA发送数据

     HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
    

    功能:串口通过DMA发送指定长度的数据。

    参数:

    • UART_HandleTypeDef *huart UATR的别名 如 : UART_HandleTypeDef huart1; 别名就是huart1
    • *pData 需要发送的数据
    • Size 发送的字节数

    举例:

    HAL_UART_Transmit_DMA(&huart1, (uint8_t *)Senbuff, sizeof(Senbuff));  //串口发送Senbuff数组
    

    串口DMA接收数据

    HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
    

    功能:串口通过DMA接受指定长度的数据。

    参数:

    • UART_HandleTypeDef *huart UATR的别名 如 : UART_HandleTypeDef huart1; 别名就是huart1
    • *pData 需要存放接收数据的数组
    • Size 接受的字节数

    举例:

    HAL_UART_Transmit_DMA(&huart1, (uint8_t *)Recbuff, sizeof(Recbuff));  //串口发送Senbuff数组
    

    串口DMA恢复函数

    HAL_UART_DMAResume(&huart1)
    

    作用: 恢复DMA的传输

    返回值: 0 正在恢复 1 完成DMA恢复

    测试例程2

    STM32 IDLE 接收空闲中断

    STM32的IDLE的中断产生条件:在串口无数据接收的情况下,不会产生,当清除IDLE标志位后,必须有接收到第一个数据后,才开始触发,一但接收的数据断流,没有接收到数据,即产生IDLE中断

    本例程功能:

    使用DMA+串口接受空闲中断 实现将接收的数据完整发送到上位机的功能

    例程代码:

    uart.c

    volatile uint8_t rx_len = 0;  //接收一帧数据的长度
    volatile uint8_t recv_end_flag = 0; //一帧数据接收完成标志
    uint8_t rx_buffer[100]={0};  //接收数据缓存数组
    
    void MX_USART1_UART_Init(void)
    {
    
      huart1.Instance = USART1;
      huart1.Init.BaudRate = 115200;
      huart1.Init.WordLength = UART_WORDLENGTH_8B;
      huart1.Init.StopBits = UART_STOPBITS_1;
      huart1.Init.Parity = UART_PARITY_NONE;
      huart1.Init.Mode = UART_MODE_TX_RX;
      huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
      huart1.Init.OverSampling = UART_OVERSAMPLING_16;
      if (HAL_UART_Init(&huart1) != HAL_OK)
      {
        Error_Handler();
      }
    //下方为自己添加的代码
    	__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); //使能IDLE中断
    
    //DMA接收函数,此句一定要加,不加接收不到第一次传进来的实数据,是空的,且此时接收到的数据长度为缓存器的数据长度
    	HAL_UART_Receive_DMA(&huart1,rx_buffer,BUFFER_SIZE);
    
    	
    }
    

    uart.h

    extern UART_HandleTypeDef huart1;
    extern DMA_HandleTypeDef hdma_usart1_rx;
    extern DMA_HandleTypeDef hdma_usart1_tx;
    /* USER CODE BEGIN Private defines */
     
     
    #define BUFFER_SIZE  100  
    extern  volatile uint8_t rx_len ;  //接收一帧数据的长度
    extern volatile uint8_t recv_end_flag; //一帧数据接收完成标志
    extern uint8_t rx_buffer[100];  //接收数据缓存数组
    

    main.c

    /*
    *********************************************************************************************************
    * 函 数 名: DMA_Usart_Send
    * 功能说明: 串口发送功能函数
    * 形  参: buf,len
    * 返 回 值: 无
    *********************************************************************************************************
    */
    void DMA_Usart_Send(uint8_t *buf,uint8_t len)//串口发送封装
    {
     if(HAL_UART_Transmit_DMA(&huart1, buf,len)!= HAL_OK) //判断是否发送正常,如果出现异常则进入异常中断函数
      {
       Error_Handler();
      }
    
    }
    
    
    
    /*
    *********************************************************************************************************
    * 函 数 名: DMA_Usart1_Read
    * 功能说明: 串口接收功能函数
    * 形  参: Data,len
    * 返 回 值: 无
    *********************************************************************************************************
    */
    void DMA_Usart1_Read(uint8_t *Data,uint8_t len)//串口接收封装
    {
    	HAL_UART_Receive_DMA(&huart1,Data,len);//重新打开DMA接收
    }
    

    while循环

     while (1)
      {
        /* USER CODE END WHILE */
    
        /* USER CODE BEGIN 3 */
    		 if(recv_end_flag == 1)  //接收完成标志
    		{
    			
    			
    			DMA_Usart_Send(rx_buffer, rx_len);
    			rx_len = 0;//清除计数
    			recv_end_flag = 0;//清除接收结束标志位
    //			for(uint8_t i=0;i<rx_len;i++)
    //				{
    //					rx_buffer[i]=0;//清接收缓存
    //				}
    				memset(rx_buffer,0,rx_len);
      }
    		HAL_UART_Receive_DMA(&huart1,rx_buffer,BUFFER_SIZE);//重新打开DMA接收
    }
    

    stm32f1xx_it.c中

    #include "usart.h"
    
    void USART1_IRQHandler(void)
    {
    	uint32_t tmp_flag = 0;
    	uint32_t temp;
    	tmp_flag =__HAL_UART_GET_FLAG(&huart1,UART_FLAG_IDLE); //获取IDLE标志位
    	if((tmp_flag != RESET))//idle标志被置位
    	{ 
    		__HAL_UART_CLEAR_IDLEFLAG(&huart1);//清除标志位
    		//temp = huart1.Instance->SR;  //清除状态寄存器SR,读取SR寄存器可以实现清除SR寄存器的功能
    		//temp = huart1.Instance->DR; //读取数据寄存器中的数据
    		//这两句和上面那句等效
    		HAL_UART_DMAStop(&huart1); //
    		temp  =  __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);// 获取DMA中未传输的数据个数   
    		//temp  = hdma_usart1_rx.Instance->NDTR;//读取NDTR寄存器 获取DMA中未传输的数据个数,
    		//这句和上面那句等效
    		rx_len =  BUFFER_SIZE - temp; //总计数减去未传输的数据个数,得到已经接收的数据个数
    		recv_end_flag = 1;	// 接受完成标志位置1	
    	 }
      HAL_UART_IRQHandler(&huart1);
    
    }
    

    测试正常:
    在这里插入图片描述

    完整例程下载:ZXiaoxuanSTM32-DMA-IDLE

    历程详解

    因为本文章已经一万余字了,足足写了好几天,翻手册也翻了好久,所以我们将本例程的详解分到另一篇文章中去,使整体结构更好一点,也方便大家阅读

    正在码字中

    详解包括:

    • 中断原理讲解
    • 例程流程详解
    • 库函数分析详解
    • 对应寄存器介绍
    • 对应函数介绍

    已经更新,请参看:
    STM32 HAL CubeMX 串口IDLE接收空闲中断+DMA
    在这里插入图片描述

    正在码字中…在这里插入图片描述

    展开全文
  • STM32F103CBT6 DMA (串口DMA发送接收)
  • STM32F407+串口接收数据采用DMA方式 六路串口
  • 三路串口DMA收发

    2019-03-17 09:59:43
    三路串口DMA收发
  • STM32F407+串口接收数据采用DMA方式 六路串口
  • STM32之串口DMA接收不定长数据

    万次阅读 多人点赞 2018-09-17 15:49:25
    STM32之串口DMA接收不定长数据 本文为杰杰原创,如需转载请说明出处 引言 在使用stm32或者其他单片机的时候,会经常使用到串口通讯,那么如何有效地接收数据呢?假如这段数据是不定长的有如何高效接收呢? ...

    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来不及处理此次接收到的数据,中断又发来数据的话,这里不能开启,否则数据会被覆盖。有两种方式解决:

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

    喜欢就关注我吧!

    欢迎关注我公众号

    相关代码可以在公众号后台获取。

    展开全文
  • stm32 串口DMA

    2015-03-12 11:05:44
    stm32串口没有FIFO,使用DMA可以实现FIFO,这是网上搜集的关于串口DMA,以及队列的程序,其中大部分测试过,希望对想了解DMA的有所帮助.
  • 本文描述串口DMA收/发机制设计,及STM32串口DMA收/发实现。




    1 前言

      直接存储器访问(Direct Memory Access),简称DMA。DMA是CPU一个用于数据从一个地址空间到另一地址空间“搬运”(拷贝)的组件,数据拷贝过程不需CPU干预,数据拷贝结束则通知CPU处理。因此,大量数据拷贝时,使用DMA可以释放CPU资源。DMA数据拷贝过程,典型的有:

    • 内存—>内存,内存间拷贝
    • 外设—>内存,如uart、spi、i2c等总线接收数据过程
    • 内存—>外设,如uart、spi、i2c等总线发送数据过程

    2 串口有必要使用DMA吗

      串口(uart)是一种低速的串行异步通信,适用于低速通信场景,通常使用的波特率小于或等于115200bps。对于小于或者等于115200bps波特率的,而且数据量不大的通信场景,一般没必要使用DMA,或者说使用DMA并未能充分发挥出DMA的作用。


      对于数量大,或者波特率提高时,必须使用DMA以释放CPU资源,因为高波特率可能带来这样的问题:

    • 对于发送,使用循环发送,可能阻塞线程,需要消耗大量CPU资源“搬运”数据,浪费CPU
    • 对于发送,使用中断发送,不会阻塞线程,但需浪费大量中断资源,CPU频繁响应中断;以115200bps波特率,1s传输11520字节,大约69us需响应一次中断,如波特率再提高,将消耗更多CPU资源
    • 对于接收,如仍采用传统的中断模式接收,同样会因为频繁中断导致消耗大量CPU资源

      因此,高波特率场景下,串口非常有必要使用DMA。


    3 实现方式


    在这里插入图片描述

    整体设计图

    4 STM32串口使用DMA

      关于STM32串口使用DMA,不乏一些开发板例程及网络上一些博主的使用教程。使用步骤、流程、配置基本大同小异,正确性也没什么毛病,但都是一些基本的Demo例子,作为学习过程没问题;实际项目使用缺乏严谨性,数据量大时可能导致数据异常。


    测试平台:

    • STM32F030C8T6
    • UART1/UART2
    • DMA1 Channel2—Channel5
    • ST标准库
    • 主频48MHz(外部12MHz晶振)

    在这里插入图片描述

    5 串口DMA接收

    5.1 基本流程


    在这里插入图片描述

    串口接收流程图

    5.2 相关配置

    关键步骤

    【1】初始化串口

    【2】使能串口DMA接收模式,使能串口空闲中断

    【3】配置DMA参数,使能DMA通道buf半满(传输一半数据)中断、buf溢满(传输数据完成)中断


    为什么需要使用DMA 通道buf半满中断?

      很多串口DMA模式接收的教程、例子,基本是使用了“空间中断”+“DMA传输完成中断”来接收数据。实质上这是存在风险的,当DMA传输数据完成,CPU介入开始拷贝DMA通道buf数据,如果此时串口继续有数据进来,DMA继续搬运数据到buf,就有可能将数据覆盖,因为DMA数据搬运是不受CPU控制的,即使你关闭了CPU中断。


      严谨的做法需要做双buf,CPU和DMA各自一块内存交替访问,即是"乒乓缓存” ,处理流程步骤应该是这样:

    【1】第一步,DMA先将数据搬运到buf1,搬运完成通知CPU来拷贝buf1数据
    【2】第二步,DMA将数据搬运到buf2,与CPU拷贝buf1数据不会冲突
    【3】第三步,buf2数据搬运完成,通知CPU来拷贝buf2数据
    【4】执行完第三步,DMA返回执行第一步,一直循环


    在这里插入图片描述

    双缓存DMA数据搬运过程

      STM32F0系列DMA不支持双缓存(以具体型号为准)机制,但提供了一个buf"半满中断",即是数据搬运到buf大小的一半时,可以产生一个中断信号。基于这个机制,我们可以实现双缓存功能,只需将buf空间开辟大一点即可。


    【1】第一步,DMA将数据搬运完成buf的前一半时,产生“半满中断”,CPU来拷贝buf前半部分数据
    【2】第二步,DMA继续将数据搬运到buf的后半部分,与CPU拷贝buf前半部数据不会冲突
    【3】第三步,buf后半部分数据搬运完成,触发“溢满中断”,CPU来拷贝buf后半部分数据
    【4】执行完第三步,DMA返回执行第一步,一直循环


    在这里插入图片描述

    使用半满中断DMA数据搬运过程

      UART2 DMA模式接收配置代码如下,与其他外设使用DMA的配置基本一致,留意关键配置:

    • 串口接收,DMA通道工作模式设为连续模式
    • 使能DMA通道接收buf半满中断、溢满(传输完成)中断
    • 启动DMA通道前清空相关状态标识,防止首次传输错乱数据
    void bsp_uart2_dmarx_config(uint8_t *mem_addr, uint32_t mem_size)
    {
      	DMA_InitTypeDef DMA_InitStructure;
    	
    	DMA_DeInit(DMA1_Channel5); 
    	DMA_Cmd(DMA1_Channel5, DISABLE);
    	DMA_InitStructure.DMA_PeripheralBaseAddr 	= (uint32_t)&(USART2->RDR);/* UART2接收数据地址 */
    	DMA_InitStructure.DMA_MemoryBaseAddr 		= (uint32_t)mem_addr; /* 接收buf */
    	DMA_InitStructure.DMA_DIR 					= DMA_DIR_PeripheralSRC; 	/* 传输方向:外设->内存 */
    	DMA_InitStructure.DMA_BufferSize 			= mem_size; /* 接收buf大小 */
    	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_Circular; /* 连续模式 */
    	DMA_InitStructure.DMA_Priority 				= DMA_Priority_VeryHigh; 
    	DMA_InitStructure.DMA_M2M 					= DMA_M2M_Disable; 
    	DMA_Init(DMA1_Channel5, &DMA_InitStructure); 
    	DMA_ITConfig(DMA1_Channel5, DMA_IT_TC|DMA_IT_HT|DMA_IT_TE, ENABLE);/* 使能DMA半满、溢满、错误中断 */
    	DMA_ClearFlag(DMA1_IT_TC5);	/* 清除相关状态标识 */
    	DMA_ClearFlag(DMA1_IT_HT5);
    	DMA_Cmd(DMA1_Channel5, ENABLE); 
    }
    

    DMA 错误中断“DMA_IT_TE”,一般用于前期调试使用,用于检查DMA出现错误的次数,发布软件可以不使能该中断。


    5.3 接收处理

      基于上述描述机制,DMA方式接收串口数据,有三种中断场景需要CPU去将buf数据拷贝到fifo中,分别是:

    • DMA通道buf溢满(传输完成)场景
    • DMA通道buf半满场景
    • 串口空闲中断场景

      前两者场景,前面文章已经描述。串口空闲中断指的是,数据传输完成后,串口监测到一段时间内没有数据进来,则触发产生的中断信号。


    5.3 .1 接收数据大小

      数据传输过程是随机的,数据大小也是不定的,存在几类情况:

    • 数据刚好是DMA接收buf的整数倍,这是理想的状态
    • 数据量小于DMA接收buf或者小于接收buf的一半,此时会触发串口空闲中断

       因此,我们需根据“DMA通道buf大小”、“DMA通道buf剩余空间大小”、“上一次接收的总数据大小”来计算当前接收的数据大小。

    /* 获取DMA通道接收buf剩余空间大小 */
    uint16_t DMA_GetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx);
    

    DMA通道buf溢满场景计算

    接收数据大小 = DMA通道buf大小 - 上一次接收的总数据大小
    

    DMA通道buf溢满中断处理函数:

    void uart_dmarx_done_isr(uint8_t uart_id)
    {
      	uint16_t recv_size;
    	
    	recv_size = s_uart_dev[uart_id].dmarx_buf_size - s_uart_dev[uart_id].last_dmarx_size;
    
    	fifo_write(&s_uart_dev[uart_id].rx_fifo, 
    				   (const uint8_t *)&(s_uart_dev[uart_id].dmarx_buf[s_uart_dev[uart_id].last_dmarx_size]), recv_size);
    
    	s_uart_dev[uart_id].last_dmarx_size = 0;
    }
    

    DMA通道buf半满场景计算

    接收数据大小 = DMA通道接收总数据大小 - 上一次接收的总数据大小
    DMA通道接收总数据大小 = DMA通道buf大小 - DMA通道buf剩余空间大小
    

    DMA通道buf半满中断处理函数:

    void uart_dmarx_half_done_isr(uint8_t uart_id)
    {
      	uint16_t recv_total_size;
      	uint16_t recv_size;
    	
    	if(uart_id == 0)
    	{
    	  	recv_total_size = s_uart_dev[uart_id].dmarx_buf_size - bsp_uart1_get_dmarx_buf_remain_size();
    	}
    	else if (uart_id == 1)
    	{
    		recv_total_size = s_uart_dev[uart_id].dmarx_buf_size - bsp_uart2_get_dmarx_buf_remain_size();
    	}
    	recv_size = recv_total_size - s_uart_dev[uart_id].last_dmarx_size;
    	
    	fifo_write(&s_uart_dev[uart_id].rx_fifo, 
    				   (const uint8_t *)&(s_uart_dev[uart_id].dmarx_buf[s_uart_dev[uart_id].last_dmarx_size]), recv_size);
    	s_uart_dev[uart_id].last_dmarx_size = recv_total_size;/* 记录接收总数据大小 */
    }
    

    串口空闲中断场景计算

      串口空闲中断场景的接收数据计算与“DMA通道buf半满场景”计算方式是一样的。


    串口空闲中断处理函数:

    void uart_dmarx_idle_isr(uint8_t uart_id)
    {
      	uint16_t recv_total_size;
      	uint16_t recv_size;
    	
    	if(uart_id == 0)
    	{
    	  	recv_total_size = s_uart_dev[uart_id].dmarx_buf_size - bsp_uart1_get_dmarx_buf_remain_size();
    	}
    	else if (uart_id == 1)
    	{
    		recv_total_size = s_uart_dev[uart_id].dmarx_buf_size - bsp_uart2_get_dmarx_buf_remain_size();
    	}
    	recv_size = recv_total_size - s_uart_dev[uart_id].last_dmarx_size;
    	s_UartTxRxCount[uart_id*2+1] += recv_size;
    	fifo_write(&s_uart_dev[uart_id].rx_fifo, 
    				   (const uint8_t *)&(s_uart_dev[uart_id].dmarx_buf[s_uart_dev[uart_id].last_dmarx_size]), recv_size);
    	s_uart_dev[uart_id].last_dmarx_size = recv_total_size;
    }
    

    注:
    串口空闲中断处理函数,除了将数据拷贝到串口接收fifo中,还可以增加特殊处理,如作为串口数据传输完成标识、不定长度数据处理等等。


    5.3.2 接收数据偏移地址

      将有效数据拷贝到fifo中,除了需知道有效数据大小外,还需知道数据存储于DMA 接收buf的偏移地址。有效数据偏移地址只需记录上一次接收的总大小即,可,在DMA通道buf全满中断处理函数将该值清零,因为下一次数据将从buf的开头存储。


    在DMA通道buf溢满中断处理函数中将数据偏移地址清零:

    void uart_dmarx_done_isr(uint8_t uart_id)
    {
     	/* todo */
    	s_uart_dev[uart_id].last_dmarx_size = 0;
    }
    

    5.4 应用读取串口数据方法

      经过前面的处理步骤,已将串口数据拷贝至接收fifo,应用程序任务只需从fifo获取数据进行处理。前提是,处理效率必须大于DAM接收搬运数据的效率,否则导致数据丢失或者被覆盖处理。


    6 串口DMA发送

    5.1 基本流程


    /20200903163632287.png#pic_center)

    串口发送流程图

    5.2 相关配置

    关键步骤

    【1】初始化串口

    【2】使能串口DMA发送模式

    【3】配置DMA发送通道,这一步无需在初始化设置,有数据需要发送时才配置使能DMA发送通道


      UART2 DMA模式发送配置代码如下,与其他外设使用DMA的配置基本一致,留意关键配置:

    • 串口发送是,DMA通道工作模式设为单次模式(正常模式),每次需要发送数据时重新配置DMA
    • 使能DMA通道传输完成中断,利用该中断信息处理一些必要的任务,如清空发送状态、启动下一次传输
    • 启动DMA通道前清空相关状态标识,防止首次传输错乱数据
    void bsp_uart2_dmatx_config(uint8_t *mem_addr, uint32_t mem_size)
    {
      	DMA_InitTypeDef DMA_InitStructure;
    	
    	DMA_DeInit(DMA1_Channel4);
    	DMA_Cmd(DMA1_Channel4, DISABLE);
    	DMA_InitStructure.DMA_PeripheralBaseAddr 	= (uint32_t)&(USART2->TDR);/* UART2发送数据地址 */
    	DMA_InitStructure.DMA_MemoryBaseAddr 		= (uint32_t)mem_addr; 	/* 发送数据buf */
    	DMA_InitStructure.DMA_DIR 					= DMA_DIR_PeripheralDST; 	/* 传输方向:内存->外设 */
    	DMA_InitStructure.DMA_BufferSize 			= mem_size; 			/* 发送数据buf大小 */
    	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_High;	 
    	DMA_InitStructure.DMA_M2M 					= DMA_M2M_Disable; 
    	DMA_Init(DMA1_Channel4, &DMA_InitStructure);  
    	DMA_ITConfig(DMA1_Channel4, DMA_IT_TC|DMA_IT_TE, ENABLE); /* 使能传输完成中断、错误中断 */
    	DMA_ClearFlag(DMA1_IT_TC4);	/* 清除发送完成标识 */
    	DMA_Cmd(DMA1_Channel4, ENABLE); /* 启动DMA发送 */
    }
    

    5.3 发送处理

      串口待发送数据存于发送fifo中,发送处理函数需要做的的任务就是循环查询发送fifo是否存在数据,如存在则将该数据拷贝到DMA发送buf中,然后启动DMA传输。前提是需要等待上一次DMA传输完毕,即是DMA接收到DMA传输完成中断信号"DMA_IT_TC"

    串口发送处理函数:

    void uart_poll_dma_tx(uint8_t uart_id)
    {
      	uint16_t size = 0;
    	
    	if (0x01 == s_uart_dev[uart_id].status)
        {
            return;
        }
    	size = fifo_read(&s_uart_dev[uart_id].tx_fifo, s_uart_dev[uart_id].dmatx_buf,
    					 s_uart_dev[uart_id].dmatx_buf_size);
    	if (size != 0)
    	{
            s_UartTxRxCount[uart_id*2+0] += size;
    	  	if (uart_id == 0)
    		{
                s_uart_dev[uart_id].status = 0x01;	/* DMA发送状态 */
    		  	bsp_uart1_dmatx_config(s_uart_dev[uart_id].dmatx_buf, size);
    		}
    		else if (uart_id == 1)
    		{
                s_uart_dev[uart_id].status = 0x01;	/* DMA发送状态,必须在使能DMA传输前置位,否则有可能DMA已经传输并进入中断 */
    			bsp_uart2_dmatx_config(s_uart_dev[uart_id].dmatx_buf, size);
    		}
    	}
    }
    
    • 注意发送状态标识,必须先置为“发送状态”,然后启动DMA 传输。如果步骤反过来,在传输数据量少时,DMA传输时间短,“DMA_IT_TC”中断可能比“发送状态标识置位”先执行,导致程序误判DMA一直处理发送状态(发送标识无法被清除)。

    注:
    关于DMA发送数据启动函数,有些博客文章描述只需改变DMA发送buf的大小即可;经过测试发现,该方法在发送数据量较小时可行,数据量大后,导致发送失败,而且不会触发DMA发送完成中断。因此,可靠办法是:每次启动DMA发送,重新配置DMA通道所有参数。该步骤只是配置寄存器过程,实质上不会占用很多CPU执行时间。


    DMA传输完成中断处理函数:

    void uart_dmatx_done_isr(uint8_t uart_id)
    {
     	s_uart_dev[uart_id].status = 0;	/* 清空DMA发送状态标识 */
    }
    

      上述串口发送处理函数可以在几种情况调用:

    • 主线程任务调用,前提是线程不能被其他任务阻塞,否则导致fifo溢出
    void thread(void)
    {
        uart_poll_dma_tx(DEV_UART1);
        uart_poll_dma_tx(DEV_UART2);
    }
    
    • 定时器中断中调用
    void TIMx_IRQHandler(void)
    {
        uart_poll_dma_tx(DEV_UART1);
        uart_poll_dma_tx(DEV_UART2);
    }
    
    • DMA通道传输完成中断中调用
    void DMA1_Channel4_5_IRQHandler(void)
    {
    	if(DMA_GetITStatus(DMA1_IT_TC4))
    	{
    		UartDmaSendDoneIsr(UART_2);
    		DMA_ClearFlag(DMA1_FLAG_TC4);
    		uart_poll_dma_tx(DEV_UART2);
    	}
    }
    

    每次拷贝多少数据量到DMA发送buf:

      关于这个问题,与具体应用场景有关,遵循的原则就是:只要发送fifo的数据量大于等于DMA发送buf的大小,就应该填满DMA发送buf,然后启动DMA传输,这样才能充分发挥会DMA性能。因此,需兼顾每次DMA传输的效率和串口数据流实时性,参考着几类实现:

    • 周期查询发送fifo数据,启动DMA传输,充分利用DMA发送效率,但可能降低串口数据流实时性
    • 实时查询发送fifo数据,加上超时处理,理想的方法
    • 在DMA传输完成中断中处理,保证实时连续数据流

    6 串口设备

    6.1 数据结构

    /* 串口设备数据结构 */
    typedef struct
    {
    	uint8_t status;			/* 发送状态 */
    	_fifo_t tx_fifo;		/* 发送fifo */
    	_fifo_t rx_fifo;		/* 接收fifo */
    	uint8_t *dmarx_buf;		/* dma接收缓存 */
    	uint16_t dmarx_buf_size;/* dma接收缓存大小*/
    	uint8_t *dmatx_buf;		/* dma发送缓存 */
    	uint16_t dmatx_buf_size;/* dma发送缓存大小 */
    	uint16_t last_dmarx_size;/* dma上一次接收数据大小 */
    }uart_device_t;
    

    6.2 对外接口

    /* 串口注册初始化函数 */
    void uart_device_init(uint8_t uart_id)
    {
      	if (uart_id == 1)
    	{
    		/* 配置串口2收发fifo */
    		fifo_register(&s_uart_dev[uart_id].tx_fifo, &s_uart2_tx_buf[0], 
                          sizeof(s_uart2_tx_buf), fifo_lock, fifo_unlock);
    		fifo_register(&s_uart_dev[uart_id].rx_fifo, &s_uart2_rx_buf[0], 
                          sizeof(s_uart2_rx_buf), fifo_lock, fifo_unlock);
    		
    		/* 配置串口2 DMA收发buf */
    		s_uart_dev[uart_id].dmarx_buf = &s_uart2_dmarx_buf[0];
    		s_uart_dev[uart_id].dmarx_buf_size = sizeof(s_uart2_dmarx_buf);
    		s_uart_dev[uart_id].dmatx_buf = &s_uart2_dmatx_buf[0];
    		s_uart_dev[uart_id].dmatx_buf_size = sizeof(s_uart2_dmatx_buf);
    		bsp_uart2_dmarx_config(s_uart_dev[uart_id].dmarx_buf, 
    							   sizeof(s_uart2_dmarx_buf));
    		s_uart_dev[uart_id].status  = 0;
    	}
    }
    
    /* 串口发送函数 */
    uint16_t uart_write(uint8_t uart_id, const uint8_t *buf, uint16_t size)
    {
    	return fifo_write(&s_uart_dev[uart_id].tx_fifo, buf, size);
    }
    
    /* 串口读取函数 */
    uint16_t uart_read(uint8_t uart_id, uint8_t *buf, uint16_t size)
    {
    	return fifo_read(&s_uart_dev[uart_id].rx_fifo, buf, size);
    }
    

    7 相关文章

      依赖的fifo参考该文章:

    【1】通用环形缓冲区模块


    8 完整源码


    代码仓库:https://github.com/Prry/stm32f0-uart-dma


    串口&DMA底层配置:

    #include <stddef.h>
    #include <stdint.h>
    #include <stdbool.h>
    #include "stm32f0xx.h"
    #include "bsp_uart.h"
    
    /**
     * @brief  
     * @param  
     * @retval 
     */
    static void bsp_uart1_gpio_init(void)
    {
        GPIO_InitTypeDef    GPIO_InitStructure;
    #if 0
    	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOB, ENABLE);
    	
        GPIO_PinAFConfig(GPIOB, GPIO_PinSource6, GPIO_AF_0);
        GPIO_PinAFConfig(GPIOB, GPIO_PinSource7, GPIO_AF_0); 
    	
    	GPIO_InitStructure.GPIO_Pin 	= GPIO_Pin_6 | GPIO_Pin_7;
        GPIO_InitStructure.GPIO_Mode 	= GPIO_Mode_AF;
    	GPIO_InitStructure.GPIO_OType 	= GPIO_OType_PP;
        GPIO_InitStructure.GPIO_Speed  	= GPIO_Speed_Level_3;
        GPIO_InitStructure.GPIO_PuPd 	= GPIO_PuPd_UP;
        GPIO_Init(GPIOB, &GPIO_InitStructure);
    #else
    	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOA, ENABLE);
    	
        GPIO_PinAFConfig(GPIOB, GPIO_PinSource9, GPIO_AF_1);
        GPIO_PinAFConfig(GPIOB, GPIO_PinSource10, GPIO_AF_1); 
    	
    	GPIO_InitStructure.GPIO_Pin 	= GPIO_Pin_9 | GPIO_Pin_10;
        GPIO_InitStructure.GPIO_Mode 	= GPIO_Mode_AF;
    	GPIO_InitStructure.GPIO_OType 	= GPIO_OType_PP;
        GPIO_InitStructure.GPIO_Speed  	= GPIO_Speed_Level_3;
        GPIO_InitStructure.GPIO_PuPd 	= GPIO_PuPd_UP;
        GPIO_Init(GPIOA, &GPIO_InitStructure);
    #endif
    }
    
    /**
     * @brief  
     * @param  
     * @retval 
     */
    static void bsp_uart2_gpio_init(void)
    {
    	GPIO_InitTypeDef GPIO_InitStructure;
    	
    	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOB, ENABLE);
    	
    	GPIO_PinAFConfig(GPIOA, GPIO_PinSource2, GPIO_AF_1);
    	GPIO_PinAFConfig(GPIOA, GPIO_PinSource3, GPIO_AF_1);
    	
    	GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_2 | GPIO_Pin_3;
    	GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AF;
    	GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
    	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz;
    	GPIO_InitStructure.GPIO_PuPd  = GPIO_PuPd_UP;
    	GPIO_Init(GPIOA, &GPIO_InitStructure);
    }
    
    /**
     * @brief  
     * @param  
     * @retval 
     */
    void bsp_uart1_init(void)
    {
    	USART_InitTypeDef USART_InitStructure;
    	NVIC_InitTypeDef NVIC_InitStructure;
    	
    	bsp_uart1_gpio_init();
    	
    	/* 使能串口和DMA时钟 */
    	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
    	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
    	
    	USART_InitStructure.USART_BaudRate            = 57600;
    	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(USART1, &USART_InitStructure);
    	
    	USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);	/* 使能空闲中断 */
    	USART_OverrunDetectionConfig(USART1, USART_OVRDetection_Disable);
    	
    	USART_Cmd(USART1, ENABLE);
    	USART_DMACmd(USART1, USART_DMAReq_Rx|USART_DMAReq_Tx, ENABLE); /* 使能DMA收发 */
    
    	/* 串口中断 */
    	NVIC_InitStructure.NVIC_IRQChannel         = USART1_IRQn;
    	NVIC_InitStructure.NVIC_IRQChannelPriority = 2;
    	NVIC_InitStructure.NVIC_IRQChannelCmd      = ENABLE;
    	NVIC_Init(&NVIC_InitStructure);
    
    	/* DMA中断 */
      	NVIC_InitStructure.NVIC_IRQChannel 		   = DMA1_Channel2_3_IRQn;       
      	NVIC_InitStructure.NVIC_IRQChannelPriority = 0; 
    	NVIC_InitStructure.NVIC_IRQChannelCmd      = ENABLE;
      	NVIC_Init(&NVIC_InitStructure);
    }
    
    /**
     * @brief  
     * @param  
     * @retval 
     */
    void bsp_uart2_init(void)
    {
    	USART_InitTypeDef USART_InitStructure;
    	NVIC_InitTypeDef NVIC_InitStructure;
    	
    	bsp_uart2_gpio_init();
    	
    	/* 使能串口和DMA时钟 */
    	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
    	RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);
    
    	USART_InitStructure.USART_BaudRate            = 57600;
    	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(USART2, &USART_InitStructure);
    	
    	USART_ITConfig(USART2, USART_IT_IDLE, ENABLE);	/* 使能空闲中断 */
    	USART_OverrunDetectionConfig(USART2, USART_OVRDetection_Disable);
    	
    	USART_Cmd(USART2, ENABLE);
    	USART_DMACmd(USART2, USART_DMAReq_Rx|USART_DMAReq_Tx, ENABLE); 	/* 使能DMA收发 */
    
    	/* 串口中断 */
    	NVIC_InitStructure.NVIC_IRQChannel         = USART2_IRQn;
    	NVIC_InitStructure.NVIC_IRQChannelPriority = 2;
    	NVIC_InitStructure.NVIC_IRQChannelCmd      = ENABLE;
    	NVIC_Init(&NVIC_InitStructure);
    
    	/* DMA中断 */
    	NVIC_InitStructure.NVIC_IRQChannel         = DMA1_Channel4_5_IRQn;       
      	NVIC_InitStructure.NVIC_IRQChannelPriority = 0; 
    	NVIC_InitStructure.NVIC_IRQChannelCmd      = ENABLE;
      	NVIC_Init(&NVIC_InitStructure);
    }
    
    void bsp_uart1_dmatx_config(uint8_t *mem_addr, uint32_t mem_size)
    {
      	DMA_InitTypeDef DMA_InitStructure;
    	
    	DMA_DeInit(DMA1_Channel2);
    	DMA_Cmd(DMA1_Channel2, DISABLE);
    	DMA_InitStructure.DMA_PeripheralBaseAddr 	= (uint32_t)&(USART1->TDR);
    	DMA_InitStructure.DMA_MemoryBaseAddr 		= (uint32_t)mem_addr; 
    	DMA_InitStructure.DMA_DIR 					= DMA_DIR_PeripheralDST; 	/* 传输方向:内存->外设 */
    	DMA_InitStructure.DMA_BufferSize 			= mem_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_InitStructure.DMA_Mode 					= DMA_Mode_Normal; 
    	DMA_InitStructure.DMA_Priority 				= DMA_Priority_High; 
    	DMA_InitStructure.DMA_M2M 					= DMA_M2M_Disable; 
    	DMA_Init(DMA1_Channel2, &DMA_InitStructure);  
    	DMA_ITConfig(DMA1_Channel2, DMA_IT_TC|DMA_IT_TE, ENABLE); 
    	DMA_ClearFlag(DMA1_IT_TC2);	/* 清除发送完成标识 */
    	DMA_Cmd(DMA1_Channel2, ENABLE); 
    }
    
    void bsp_uart1_dmarx_config(uint8_t *mem_addr, uint32_t mem_size)
    {
      	DMA_InitTypeDef DMA_InitStructure;
    	
    	DMA_DeInit(DMA1_Channel3); 
    	DMA_Cmd(DMA1_Channel3, DISABLE);
    	DMA_InitStructure.DMA_PeripheralBaseAddr 	= (uint32_t)&(USART1->RDR);
    	DMA_InitStructure.DMA_MemoryBaseAddr 		= (uint32_t)mem_addr; 
    	DMA_InitStructure.DMA_DIR 					= DMA_DIR_PeripheralSRC; 	/* 传输方向:外设->内存 */
    	DMA_InitStructure.DMA_BufferSize 			= mem_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_InitStructure.DMA_Mode 					= DMA_Mode_Circular; 
    	DMA_InitStructure.DMA_Priority 				= DMA_Priority_VeryHigh; 
    	DMA_InitStructure.DMA_M2M 					= DMA_M2M_Disable; 
    	DMA_Init(DMA1_Channel3, &DMA_InitStructure); 
    	DMA_ITConfig(DMA1_Channel3, DMA_IT_TC|DMA_IT_HT|DMA_IT_TE, ENABLE);/* 使能DMA半满、全满、错误中断 */
    	DMA_ClearFlag(DMA1_IT_TC3);
    	DMA_ClearFlag(DMA1_IT_HT3);
    	DMA_Cmd(DMA1_Channel3, ENABLE); 
    }
    
    uint16_t bsp_uart1_get_dmarx_buf_remain_size(void)
    {
    	return DMA_GetCurrDataCounter(DMA1_Channel3);	/* 获取DMA接收buf剩余空间 */
    }
    
    void bsp_uart2_dmatx_config(uint8_t *mem_addr, uint32_t mem_size)
    {
      	DMA_InitTypeDef DMA_InitStructure;
    	
    	DMA_DeInit(DMA1_Channel4);
    	DMA_Cmd(DMA1_Channel4, DISABLE);
    	DMA_InitStructure.DMA_PeripheralBaseAddr 	= (uint32_t)&(USART2->TDR);
    	DMA_InitStructure.DMA_MemoryBaseAddr 		= (uint32_t)mem_addr; 
    	DMA_InitStructure.DMA_DIR 					= DMA_DIR_PeripheralDST; 	/* 传输方向:内存->外设 */
    	DMA_InitStructure.DMA_BufferSize 			= mem_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_InitStructure.DMA_Mode 					= DMA_Mode_Normal; 
    	DMA_InitStructure.DMA_Priority 				= DMA_Priority_High; 
    	DMA_InitStructure.DMA_M2M 					= DMA_M2M_Disable; 
    	DMA_Init(DMA1_Channel4, &DMA_InitStructure);  
    	DMA_ITConfig(DMA1_Channel4, DMA_IT_TC|DMA_IT_TE, ENABLE); 
    	DMA_ClearFlag(DMA1_IT_TC4);	/* 清除发送完成标识 */
    	DMA_Cmd(DMA1_Channel4, ENABLE); 
    }
    
    void bsp_uart2_dmarx_config(uint8_t *mem_addr, uint32_t mem_size)
    {
      	DMA_InitTypeDef DMA_InitStructure;
    	
    	DMA_DeInit(DMA1_Channel5); 
    	DMA_Cmd(DMA1_Channel5, DISABLE);
    	DMA_InitStructure.DMA_PeripheralBaseAddr 	= (uint32_t)&(USART2->RDR);
    	DMA_InitStructure.DMA_MemoryBaseAddr 		= (uint32_t)mem_addr; 
    	DMA_InitStructure.DMA_DIR 					= DMA_DIR_PeripheralSRC; 	/* 传输方向:外设->内存 */
    	DMA_InitStructure.DMA_BufferSize 			= mem_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_InitStructure.DMA_Mode 					= DMA_Mode_Circular; 
    	DMA_InitStructure.DMA_Priority 				= DMA_Priority_VeryHigh; 
    	DMA_InitStructure.DMA_M2M 					= DMA_M2M_Disable; 
    	DMA_Init(DMA1_Channel5, &DMA_InitStructure); 
    	DMA_ITConfig(DMA1_Channel5, DMA_IT_TC|DMA_IT_HT|DMA_IT_TE, ENABLE);/* 使能DMA半满、全满、错误中断 */
    	DMA_ClearFlag(DMA1_IT_TC5);
    	DMA_ClearFlag(DMA1_IT_HT5);
    	DMA_Cmd(DMA1_Channel5, ENABLE); 
    }
    
    uint16_t bsp_uart2_get_dmarx_buf_remain_size(void)
    {
    	return DMA_GetCurrDataCounter(DMA1_Channel5);	/* 获取DMA接收buf剩余空间 */
    }
    

    压力测试:

    • 1.5Mbps波特率,串口助手每毫秒发送1k字节数据,stm32f0 DMA接收数据,再通过DMA发送回串口助手,毫无压力。
    • 1.5Mbps波特率,可传输大文件测试,将接收数据保存为文件,与源文件比较。
    • 串口高波特率测试需要USB转TLL工具及串口助手都支持才可行,推荐CP2102、FT232芯片的USB转TTL工具。

    在这里插入图片描述

    1.5Mbps串口回环压力测试

    展开全文
  • STM32串口DMA通信

    2018-06-21 10:11:13
    串口DMA通信可解决接收和发送数据量不确定的情况。尤其是透传方面。
  • FreeRTOS+串口DMA.rar

    2020-04-14 20:59:59
    基于STM32L431RB FreeRTOS+串口DMA接收,串口接收完成释放信号量,从而唤醒接收任务。已经在开发板上测试过,移植到其他STM32芯片也很容易的。
  • 今天主要讨论三个问题:1、什么叫串口DMA 请求;2、串口简要复习;3、串口DMA发送流程。一起来学习吧
  • 串口DMA方式发送数据MINI2440UartDma
  • 6路串口DMA接收.rar

    2019-12-02 13:35:39
    6路串口DMA接收.rar
  • STM32F103串口DMA收发

    2018-07-16 16:17:43
    STM32F103串口DMA收发,参考例程改写,详细注释,试验成功。初学者很好的参考模板。
  • 为了确保我的飞控能够使用乐迪遥控,调试好了串口DMA通讯(SBUS实际是一种串口通讯协议),现在将这个流程写下来,希望能帮助更多的人。(有帮助点个赞谢谢) 笔者在调试时,参考了以下博文,向这些博主及作者表示...

           为了确保我的飞控能够使用乐迪遥控,调试好了串口DMA通讯(SBUS实际是一种串口通讯协议),现在将这个流程写下来,希望能帮助更多的人。(有帮助点个赞谢谢)

    笔者在调试时,参考了以下博文,向这些博主及作者表示感谢!

    (1)https://blog.csdn.net/peach_orange/article/details/52958385    SBUS协议:SBUS解析与合成

    (2)http://www.360doc.com/content/16/0818/08/35267583_584012245.shtml    Futaba SBUS协议解析


    完成整个工作需要3个部分,硬件取反电路、单片机串口DMA配置SBUS协议解析

     

    • 硬件取反电路

    SBUS接收机的信号需要硬件取反(高低电平互换),这点是必须的,软件取反是不行的。

    信号取反电路可以用一个N-mos实现,也可以找一个逻辑门非门。笔者采用的是前一种方案,电路图如下:

    经过反向的接收机sbus信号接到单片机串口的RX端,如下:

    (单片机端)   RX    <————>   SBUS硬件取反信号(SBUS接收机)

     

    • 单片机串口DMA配置

    SBUS信号的格式是特定的,其波特率是固定的10kbps,通过示波器波形可以看到信号每14ms(高速模式为4ms)发送连续的25字节数据

    串口初始化代码:

    void USART1_SBUS_Init(void)
    {
    	NVIC_InitTypeDef NVIC_InitStructure ;//定义中断结构体
     	GPIO_InitTypeDef GPIO_InitStructure;//定义IO初始化结构体
    	USART_InitTypeDef USART_InitStructure;//定义串口结构体
    	DMA_InitTypeDef DMA_InitStructure;//定义DMA结构体
        
            RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);//打开串口对应的外设时钟
    
    	// 0 设置IO口时钟
    	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);
    	GPIO_PinAFConfig(GPIOB,GPIO_PinSource6,GPIO_AF_USART1);
    	GPIO_PinAFConfig(GPIOB,GPIO_PinSource7,GPIO_AF_USART1);
    
            GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;	 //管脚模式:输出口
    	GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;	    //类型:推挽模式
    	GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;	 //上拉下拉设置
    	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;	//IO口速度
    	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;  //管脚指定
    	GPIO_Init(GPIOB, &GPIO_InitStructure);//初始化
    
    
            GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;	//管脚模式:输入口
    	GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;	 //上拉下拉设置
    	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;    //管脚指定
    	GPIO_Init(GPIOB, &GPIO_InitStructure);      //初始化
    	// 1 启动DMA时钟
            RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);//DMA通道配置
    	
    	// 2 DMA通道配置
    	DMA_DeInit(DMA2_Stream5);
    	DMA_InitStructure.DMA_Channel = DMA_Channel_4;
    	DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)(&USART1->DR);//外设地址
    	DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)rec_sbus_data;//内存地址
    	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;//dma传输方向
    	DMA_InitStructure.DMA_BufferSize = SBUS_DATA_LEN;//设置DMA在传输时缓冲区的长度
    	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//设置DMA的外设一个外设
    	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//设置DMA的内存递增模式
    	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;//外设数据字长
    	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;//内存数据字长
    	DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;//设置DMA的传输模式
    	DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh;//设置DMA的优先级别
    	DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;         
    	DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full;
    	DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;//存储器突发单次传输
    	DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;//外设突发单次传输
    	// 3 配置DMA2的通道
    	DMA_Init(DMA2_Stream5, &DMA_InitStructure);
    	// 4 使能通道
    	DMA_Cmd(DMA2_Stream5,ENABLE);
    
        // 5 初始化串口参数
        USART_InitStructure.USART_WordLength = USART_WordLength_9b;
        USART_InitStructure.USART_StopBits = USART_StopBits_2;
        USART_InitStructure.USART_Parity = USART_Parity_Even;
        USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
        USART_InitStructure.USART_Mode = USART_Mode_Rx;
        USART_InitStructure.USART_BaudRate = 100000;
    	
    	// 6 使能串口的DMA接收 
    	USART_DMACmd(USART1,USART_DMAReq_Rx,ENABLE);
    	
    	// 7 初始化串口
        USART_Init(USART1,&USART_InitStructure);
    
    	// 8 配置中断
        NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;               //通道设置为串口中断
        NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;       //中断占先等级
        NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;              //中断响应优先级
        NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;                 //打开中断
        NVIC_Init(&NVIC_InitStructure);
    
    	// 9 中断配置
    	USART_ITConfig(USART1,USART_IT_TC,DISABLE);
    	USART_ITConfig(USART1,USART_IT_RXNE,DISABLE);
    	USART_ITConfig(USART1,USART_IT_TXE,DISABLE);
    	USART_ITConfig(USART1,USART_IT_IDLE,ENABLE);
    
        // 10 启动串口
        USART_Cmd(USART1, ENABLE);
    }

     

     

     

    • SBUS协议解析

    sbus协议一共可以支持最多16个通道,每个通道由11bit表示,也就是十进制下0-2047。数据密集排布在数据帧的22字节数据中,需要通过移位操作获得各通道的数值。

    解析代码具体如下:

    sbus_channel[0]  = ((rec_sbus_data[1]|rec_sbus_data[2]<<8) & 0x07FF);
    sbus_channel[1]  = ((rec_sbus_data[2]>>3 |rec_sbus_data[3]<<5) & 0x07FF);
    sbus_channel[2]  = ((rec_sbus_data[3]>>6 |rec_sbus_data[4]<<2 |rec_sbus_data[5]<<10) & 0x07FF);
    sbus_channel[3]  = ((rec_sbus_data[5]>>1 |rec_sbus_data[6]<<7) & 0x07FF);
    sbus_channel[4]  = ((rec_sbus_data[6]>>4 |rec_sbus_data[7]<<4) & 0x07FF);
    sbus_channel[5]  = ((rec_sbus_data[7]>>7 |rec_sbus_data[8]<<1 |rec_sbus_data[9]<<9) & 0x07FF);
    sbus_channel[6]  = ((rec_sbus_data[9]>>2 |rec_sbus_data[10]<<6) & 0x07FF);
    sbus_channel[7]  = ((rec_sbus_data[10]>>5|rec_sbus_data[11]<<3) & 0x07FF);
    sbus_channel[8]  = ((rec_sbus_data[12]   |rec_sbus_data[13]<<8) & 0x07FF);
    sbus_channel[9]  = ((rec_sbus_data[13]>>3|rec_sbus_data[14]<<5) & 0x07FF);
    sbus_channel[10] = ((rec_sbus_data[14]>>6|rec_sbus_data[15]<<2|rec_sbus_data[16]<<10) & 0x07FF);
    sbus_channel[11] = ((rec_sbus_data[16]>>1|rec_sbus_data[17]<<7) & 0x07FF);
    sbus_channel[12] = ((rec_sbus_data[17]>>4|rec_sbus_data[18]<<4) & 0x07FF);
    sbus_channel[13] = ((rec_sbus_data[18]>>7|rec_sbus_data[19]<<1|rec_sbus_data[20]<<9)& 0x07FF);
    sbus_channel[14] = ((rec_sbus_data[20]>>2|rec_sbus_data[21]<<6) & 0x07FF);
    sbus_channel[15] = ((rec_sbus_data[21]>>5|rec_sbus_data[22]<<3) & 0x07FF);

     

     

     


    笔者对于飞行器设计及其控制兴趣浓厚(目前自行设计的倾转翼飞行器已试飞成功),但因学业原因将逐渐转移至其他方向,当然飞行器作为我的兴趣,我依然会一如既往的关注,研究和更新,谢谢大家的支持,对于感兴趣的萌新或大佬,可以私信交流进步!

    展开全文
  • 6路串口DMA接收源码.zip
  • 串口DMA的配置和使用,实现串口接收不定长数据以及定长数据,实现双缓冲模式,提高MCU效率。句句代码都解释,配合博文看,你会觉得DMA如此的简单,串口DMA实战如此简单! 相关讲解访问博客链接:...
  • f407串口dma 比较实用的程序
  • STM32F429 UART1串口DMA收发程序 实测过,稳定,可以直接用在项目上。UART1通过DMA2接收到数据后,再通过DMA2发送出去。
  • HAL库+CubeMX+Stm32F405实现串口DMA不定长收发 HAL库+CubeMX+Stm32F405实现串口DMA不定长收发,详情请看:
  • 串口DMA方式发送&接收

    万次阅读 多人点赞 2019-01-16 16:38:05
    串口DMA方式收发  笔者使用的是STM32F407VET6,共包含6路串口,页尾处程序已将全部串口DMA收发配置完成,本文仅以串口1为例进行讲解。(查看代码可直接跳至第二节或页尾处下载) 1 STM32F4 DMA 简介  DMA,...
  • STM32f103三个串口DMA设置,包括串口设置、DMA设置、NVIC设置,systick,printf重定向,USART1_Printf等。
  • STM32F103 cube生成的串口DMA不定长数据接收,使用DMA串口IDLE中断配合使用,数据量大建议使用多缓存。
  • STM32F103 串口DMA+空闲中断接收

    热门讨论 2017-11-14 11:54:44
    STM32F103VET6 串口DMA+空闲中断接收,接收到的数据用DMA串口实时发送回去。该版本存在一个bug,推荐下载另一个修复后的版本,或者参照我的博文自己进行修复。 修复版本下载链接:...
  • 在使用串口时,使用dma接收会提高程序的运行效率,所以我写一个串口dma接收程序,使用hal的cubemx配置,实现功能
  • stm32h743串口DMA+空闲中断,可接受不定长数据,代码中对容易出问题的地方做了完善处理。可

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 25,203
精华内容 10,081
关键字:

串口dma