【工具使用】STM32CubeMX-DMA配置(ADC+DMA 和 UART+DMA)

一、概述

无论是新手还是大佬,基于STM32单片机的开发,使用STM32CubeMX都是可以极大提升开发效率的,并且其界面化的开发,也大大降低了新手对STM32单片机的开发门槛。

本文主要讲述STM32芯片的DMA的配置及其相关知识。

二、软件说明

STM32CubeMX是ST官方出的一款针对ST的MCU/MPU跨平台的图形化工具,支持在Linux、MacOS、Window系统下开发,其对接的底层接口是HAL库,另外习惯于寄存器开发的同学们,也可以使用LL库。STM32CubeMX除了集成MCU/MPU的硬件抽象层,另外还集成了像RTOS,文件系统,USB,网络,显示,嵌入式AI等中间件,这样开发者就能够很轻松的完成MCU/MPU的底层驱动的配置,留出更多精力开发上层功能逻辑,能够更进一步提高了嵌入式开发效率。

演示版本 6.7.0

三、DMA简介

DMA(Direct Memory Access)直接内存访问,其实就是一个数据搬运工,负责将数据从一个地方搬运到另一个地方而不需要内核介入。STM32里的DMA支持从外设到内存,从内存到外设和从内存到内存三种传输方式。

老规矩,先来看下ST芯片手册里DMA的框架图。

从此图可以看出,DMA的数据来源与去向可以源自于各种外设,当然这只限于目前的这款芯片,有些芯片DMA不能访问部分外设,如ST的H750,具体能否访问需要看芯片手册里的总线框架。

我们先把这图拆成四部分:

首先是中间的"Bus matrix"(总线矩阵),是这个图的核心,所有的外设及内核,都是通过总线矩阵进行数据交互的。

左上角就是F072这款芯片的内核,用的是Cortex-M0。内核需要通过总线矩阵才能跟其他外设进行数据交互。

右侧是与本文关系不大的其他外设,包括Flash、SRAM及GPIO等其他外设。

而左下角就是本文的主角------DMA。

前面也说了,DMA就是一个数据搬运工,那什么时候需要这个搬运工呢?一般有两种场景,一是有大量数据需要传输,二是需要内核频繁切换搬运的数据传输。

比如现在要做一个显示屏,用到了一个GUI开源库LVGL,这个库用于显示的接口是操作一段缓存数据,这段缓存叫作显存。如果要操作一个16位480*272分辨率的RGB屏,那这段显存的大小就是480*272*2=255k。如果使用内核来填充这段显存,以48M单片机主频来算,假设一个指令周期填充四个字节的数据,那么需要48000000/(255*1024/4)=735us时间。如果屏更大,那花费的时间就更长了。此时内核用于显示占用了过多的时间,就会导致其他操作无法进行。所以像这种无聊单调的搬运工作,完全没必要内核来进行,只需要有个搬运工,简单地配置后,让它自己按时搬运数据,解放内核的双手,让内核可以去做其他更有意义的事。

再比如现在要实现一个串口的数据收发,ST的串口只提供了一个字节的收发(新的系列提供一段FIFO队列,这里先不考虑这种情况),那如果需要收发100个字节的数据,意味着需要搬运100次数据。相较于上面的255k,可能觉得这100次没什么大不了,但是串口通信是有速率限制的,比如一个9600波特率(1s传输9600个位),传一个字节就需要1ms左右,100个字节就需要100ms。如果使用的是阻塞型发送,即意味着内核需要阻塞100ms的时间。如果使用的中断式发送,那也需要在中断跟主循环中来回切换100次,效率太低。所以像这种重复搬运的,也可以使用DMA来帮忙。

四、功能配置 及 代码实现

这里我们整两个比较常用的实例吧,实例一:使用ADC+DMA。实例二:使用Uart+DMA。

4.1 ADC+DMA

4.1.1 功能配置

这里我们试着一次采三个通道,分别是片内温度、参考电压和备份电源电压。

配置好ADC,ADC的配置可以参考《STM32CubeMX-单ADC模式规则通道配置》。然后在ADC配置的基础上增加DMA的配置。注意,用HAL库的时候,千万不要配置连续转换!!因为HAL库的DMA中断操作时间过长,比ADC转换一次的时间还长,导致程序会一直频繁进DMA中断。


DMA Setting(DMA配置) :DMA的基本功能配置窗口。
DMA Request(DMA请求来源) :这个一般从哪个外设点进来就默认用哪个外设。
Channel(DMA通道ID) :DMA一般有16个通道,当使用了多个DMA通道进行传输时,CubeMX会自动跳过已选择的通道,不用担心会选重。但如果不是通过CubeMX配置的话就要注意去重了。另外,不是所有通道都可以选,芯片手册有写明哪些外设只能用哪些通道,CubeMX会自动给你去除掉不可配置的通道。
Direction(数据传输方向) :DMA本身是可以支持"外设到内存"、"内存到外设"和"内存到内存"这三个数据传输方向的,但因为这里选择了源数据为ADC,所以只能选择"外设到内存"。
Priority(传输优先级) :这里区分了低、中、高、非常高四种优先级。前面说了因为DMA会有多个通道,所以当同时配多个通道时,并且同时有多个传输请求时,这时候就需要区分优先级看哪个先传哪个后传。如果都是一样的优先级,那就按通道的顺序执行。
Add/Delete(添加或删除) :用于添加或删除DMA通道,因为使用的是ADC,只需要一个DMA通道即可解决,所以无法添加第二个DMA通道。
Mode(请求模式) :可以选择单次或循环,如果选择了单次,那DMA会在一轮数据传输后停止传输;如果选择的是循环,则在传输完一轮数据之后自动进行下一轮的传输。
Increment Address(递增地址) :勾选则表示每传一个数据,其对应的物理地址要递增一次。因为这里ADC多个通道采集后的数据都存放在DR这一个寄存器里,所以其物理地址不需要变化,而传输到内存后,如果内存地址不向上加1,则多个通道的数据会被相互覆盖。所以为了达成多个通道的数据自动采集放到不同内存地址,这里需要勾选内存地址递增。
Data Width(数据带宽) :每传输一个数据的位数,可以选择8位、16位和32位。需要注意的是,对应的内存地址需要与数据类型的位数保持对齐,也就是说如果选择了16位数据传输,则用于存放的内存地址必须能被2整除;传输32位的数据则其内存地址需要被4整除。如果地址不对齐会导致数据传输后错位。

上面的DMA功能配置完后,还需要翻到前面ADC的设置页,使能DMA的请求功能。

4.1.2 代码实现

查看手册,找到温度的换算公式如下,其中TS_CAL1和TS_CAL2可以在数据手册中找到对应存放的内存地址,而TS_DATA则是当前的采集值。

VREFINT(参考电压)及VBAT(备份电源电压)的换算公式,其中VREFINT_CAL可以在数据手册中找到对应存放的内存地址,VREFINT_DATA就是采集VREFINT的ADC值。

把VREFINT_DATA换成VBAT_DATA * 2得出来的结果就是VBAT的值。乘2是因为手册里写的,VBAT有可能会大于VDD,为了防止输入的备份电源过高损坏单片机,所以单片机内部做了分压,最终给到ADC采集的值是二分后的电压。

c 复制代码
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
/* ADC采集类型 */
enum emAdcType
{
    ADC_TYPE_temperature = 0,
    ADC_TYPE_vrefint = 1,
    ADC_TYPE_vbat = 2,

    ADC_TYPE_max
};
/* 换算结果缓存 */
float AdcData[ADC_TYPE_max] = {0, 0, 0};
/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */
  /* 采集结果缓存 */
  uint16_t adc_buff[ADC_TYPE_max] = {0, 0, 0};
  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_ADC_Init();
  /* USER CODE BEGIN 2 */

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
	/* 启动采集 */
	HAL_ADC_Start_DMA(&hadc, (uint32_t *)adc_buff, ADC_TYPE_max);
	/* 温度换算 */
	#define TS_CAL1 ((uint16_t *)0x1FFFF7B8)
	#define TS_CAL2 ((uint16_t *)0x1FFFF7C2)
	AdcData[ADC_TYPE_temperature] = (float)(110 - 30) / ((*TS_CAL2) - (*TS_CAL1)) * (adc_buff[ADC_TYPE_temperature] - (*TS_CAL1)) + 30;
	
	/* 参考电压换算 */
	#define VREFINT_CAL ((uint16_t *)0x1FFFF7BA)
	AdcData[ADC_TYPE_vrefint] = (float)3 * (*VREFINT_CAL) / adc_buff[ADC_TYPE_vrefint];
	
	/* 备份电源电压换算 */
	AdcData[ADC_TYPE_vbat] = (float)3 * (*VREFINT_CAL) * 2 / adc_buff[ADC_TYPE_vbat];
		
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

4.1.3 效果演示


注:很奇怪这里的温度按手册的公式算出来一直是50几度,换了几块开发板都这样,后面有时间研究一下。

4.2 Uart+DMA

这里我们来实现一个比较好玩的功能------串口透传,也就是开启两个串口,当一个串口接收到数据时,把数据给到另一个串口,由另一个串口发送出去;反过来同理。有人可能会说了,这样为什么不用板子直接把两个线接起来就行?我只能说这个功能,实际项目中就会用到,比如串口网关或控制器带透传功能等。话不多说,重点来看下实现。(由于F072的DMA不支持交叉通道配置,所以这里咋偷偷切换个C031来实现外设到外设的配置)。

4.2.1 功能配置

这里我们来实现一个接收和发送都用DMA搬运的例子。

同样的先配置好Uart,Uart的配置可以参考《STM32CubeMX-Uart配置 及 数据收发功能实现》

注:这里串口1的发送口不要配在PC14,因为C031的PC14脚跟烧录引脚复用,需要有其他配置才能作为串口发送。

然后在Uart配置的基础上增加DMA的配置。不同于ADC,Uart有收跟发两个传输方向,所以这里DMA可以配置两个通道,一个用于数据接收,一个用于数据发送。透传这里我们打算当接收到一个字节的数据时,使用DMA传输至另一个串口的TDR寄存器发送出去,所以这里只需要配置接收的DMA通道。

因为这里串口的收发寄存器都只有一个,所以Memory地址也不需要递增。另外打开串口中断是为了在发送完一帧数据后,重置一下DMA的配置(因为现在DMA配置的是单次发送,所以每发完一次需要重新配置一次)。

4.2.2 代码实现

c 复制代码
/***************************************main.c*********************************************/
/* USER CODE BEGIN 0 */
void Uart_PassThroughEn1(void)
{
    /* DMA失能 */
    LL_DMA_DisableChannel(DMA1, LL_DMA_CHANNEL_1);

    /* 设置DMA数据源 */
    LL_DMA_SetPeriphAddress(DMA1, LL_DMA_CHANNEL_1, LL_USART_DMA_GetRegAddr(USART1, LL_USART_DMA_REG_DATA_RECEIVE));

    /* 设置DMA目标数据地址为另一路串口的发送寄存器地址 */
    LL_DMA_SetMemoryAddress(DMA1, LL_DMA_CHANNEL_1, LL_USART_DMA_GetRegAddr(USART2, LL_USART_DMA_REG_DATA_TRANSMIT));

    /* 设置DMA数据长度-这里设置的是一次性最大能传输的数量为255 */
    LL_DMA_SetDataLength(DMA1, LL_DMA_CHANNEL_1, 255);

    /* 打开接收的DMA传输使能 */
    LL_USART_EnableDMAReq_RX(USART1);

    /* 开DMA使能前清除标志 */
    LL_DMA_ClearFlag_TC1(DMA1);
    LL_DMA_ClearFlag_HT1(DMA1);

    /* DMA使能 */
    LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_1);

    /* 清除TC标志 */
    LL_USART_ClearFlag_TC(USART1);

    /* 使能TC中断 */
    LL_USART_EnableIT_TC(USART1);
}
void Uart_PassThroughEn2(void)
{
    /* DMA失能 */
    LL_DMA_DisableChannel(DMA1, LL_DMA_CHANNEL_2);

    /* 设置DMA数据源 */
    LL_DMA_SetPeriphAddress(DMA1, LL_DMA_CHANNEL_2, LL_USART_DMA_GetRegAddr(USART2, LL_USART_DMA_REG_DATA_RECEIVE));

    /* 设置DMA目标数据地址为另一路串口的发送寄存器地址 */
    LL_DMA_SetMemoryAddress(DMA1, LL_DMA_CHANNEL_2, LL_USART_DMA_GetRegAddr(USART1, LL_USART_DMA_REG_DATA_TRANSMIT));

    /* 设置DMA数据长度-这里设置的是一次性最大能传输的数量为255 */
    LL_DMA_SetDataLength(DMA1, LL_DMA_CHANNEL_2, 255);

    /* 打开接收的DMA传输使能 */
    LL_USART_EnableDMAReq_RX(USART2);

    /* 开DMA使能前清除标志 */
    LL_DMA_ClearFlag_TC1(DMA1);
    LL_DMA_ClearFlag_HT1(DMA1);

    /* DMA使能 */
    LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_2);

    /* 清除TC标志 */
    LL_USART_ClearFlag_TC(USART2);

    /* 使能TC中断 */
    LL_USART_EnableIT_TC(USART2);
}

/* USER CODE END 0 */

int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  LL_APB2_GRP1_EnableClock(LL_APB2_GRP1_PERIPH_SYSCFG);
  LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_PWR);

  /* SysTick_IRQn interrupt configuration */
  NVIC_SetPriority(SysTick_IRQn, 3);

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_USART1_UART_Init();
  MX_USART2_UART_Init();
  /* USER CODE BEGIN 2 */
  Uart_PassThroughEn1();
  Uart_PassThroughEn2();
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}
/***************************************stm32f0xx_it.c*********************************************/
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
extern void Uart_PassThroughEn1(void);
extern void Uart_PassThroughEn2(void);
/* USER CODE END 0 */

/**
  * @brief This function handles USART1 interrupt.
  */
void USART1_IRQHandler(void)
{
  /* USER CODE BEGIN USART1_IRQn 0 */
	if ( (LL_USART_IsEnabledIT_TC(USART1))
    && (LL_USART_IsActiveFlag_TC(USART1))
    )
  {
    Uart_PassThroughEn1();
    LL_USART_ClearFlag_TC(USART1);
  }
  /* USER CODE END USART1_IRQn 0 */
  /* USER CODE BEGIN USART1_IRQn 1 */

  /* USER CODE END USART1_IRQn 1 */
}

/**
  * @brief This function handles USART2 interrupt.
  */
void USART2_IRQHandler(void)
{
  /* USER CODE BEGIN USART2_IRQn 0 */
	if ( (LL_USART_IsEnabledIT_TC(USART2))
    && (LL_USART_IsActiveFlag_TC(USART2))
    )
  {
    Uart_PassThroughEn2();
    LL_USART_ClearFlag_TC(USART2);
  }
  /* USER CODE END USART2_IRQn 0 */
  /* USER CODE BEGIN USART2_IRQn 1 */

  /* USER CODE END USART2_IRQn 1 */
}

4.2.3 效果演示

五、注意事项

1、使用ADC+DMA时,要留意一下生成代码的初始化顺序,之前出现过生成的代码ADC与DMA的初始化顺序错了,导致ADC配置的通道数被异常修改。

2、DMA传输的内存地址必须与传输的数据类型对齐,如果传输的是16位的数据,则其源和目标内存的地址都必须是2的整数倍;如果传输的是32位的数据,则必须是4的整数倍。如果不对齐DMA会强行访问对齐的地址,结果就是数据错误。

3、使用ADC+DMA时,如果要用HAL库,不能开启连续转换模式。因为HAL库里DMA的中断处理时间长于ADC的采样时间,导致开启传输后,程序几乎一直在DMA中断里出不来,造成程序"假死"的现象。

六、相关链接

【知识分享】异步串行收发器Uart(串口)-通信协议详解
【工具使用】STM32CubeMX-单ADC模式规则通道配置
【工具使用】STM32CubeMX-Uart配置 及 数据收发功能实现

相关推荐
llilian_1620 小时前
总线授时卡 CPCI总线授时卡的工作原理及应用场景介绍 CPCI总线校时卡
运维·单片机·其他·自动化
禾仔仔21 小时前
USB MSC从理论到实践(模拟U盘为例)——从零开始学习USB2.0协议(六)
嵌入式硬件·mcu·计算机外设
The Electronic Cat1 天前
树莓派使用串口启动死机
单片机·嵌入式硬件·树莓派
先知后行。1 天前
常见元器件
单片机·嵌入式硬件
恒锐丰小吕1 天前
屹晶微 EG2302 600V耐压、低压启动、带SD关断功能的高性价比半桥栅极驱动器技术解析
嵌入式硬件·硬件工程
Dillon Dong1 天前
按位或(|=)的核心魔力:用宏定义优雅管理嵌入式故障字
c语言·stm32
Free丶Chan1 天前
dsPIC系列-1:dsPIC33点灯 [I/O、RCC、定时器]
单片机·嵌入式硬件
v先v关v住v获v取1 天前
塔式立体车库5张cad+设计说明书+三维图
科技·单片机·51单片机
恒锐丰小吕1 天前
屹晶微 EG2106D 600V耐压、半桥MOS/IGBT驱动芯片技术解析
嵌入式硬件·硬件工程