前言:大家好,本人大学在读生,由于本科电赛学习期间,受到不少github与CSDN的帮助,又发现有些文章重理论轻实践,对于刚接触单片机一窍不通的小白来说许多地方很难理解,最终无法配置成功实现相应功能,挫伤学习积极性(比如说我)。因此我本着回馈的初衷,想介绍STM32F4系列单片机如何具体配置一些重要外设的功能(主要是STM32F407与STM32F429)。
(本人电赛期间使用的STM32F429核心板与自制底板)
本篇是关于基于cubemx的多重adc采样DMA传输配置方式,工程是STM32F429IGT6,理论部分可能会讲的少一些,主要还是手把手教大家配置的过程。第一次写文,如有问题还请多多包涵~
一、原理讲解
如果要我简单介绍adc的话,它就像一个长长的阶梯,阶梯顶端是标定电压,而每一格所对应的电压值,则由标定电压与adc的位数共同确定。比如3.3V的标定电压,位数是10位,那么台阶总数就是2的10次方,1024,而每一格就是3.3/1024 V,约为3mV,这也是adc的最小识别精度。
当外界电压加到adc对应IO口上时,首先会将该电压与标定电压进行比较,得到其对应的阶梯序号,并被单片机所读取。比如外加电压是1.65V,标定电压为3.3V,位数10位,那么对应阶梯序号就是1.65V/3.3V *1024 = 512,512就是单片机读到的值。如果要将它转换成实际电压并输出,则需要在软件上进行数值转换,即512/1024 *3.3V = 1.65V。
众所周知,STM32F429系列单个adc最高可以实现2.4MHz采样率(亲测完全可行,理论上可以更高,但不推荐去配),根据奈奎斯特采样定律可以采1.2MHz的信号。但实际用过的人就明白,想要得到相对漂亮的信号(比如正弦波),一般采样率要比信号频率高7-8倍左右,否则会存在较大失真。
然而,多重adc采样可以实现三个adc交替采一个通道,即adc1采完后adc2采,adc2采完adc3采,adc3采完又回到adc1,以此循环。如果只有单个adc,在一次采样后,必须等待转换完成才能进行下一次采样!而多重adc则可以在adc的转换时间里,调用其他adc去采样,这样大大提高了采样率,并且理论上可以实现7.2MHz的采样率!以下是示意图。
(有关三重adc模式的介绍)
话不多说,教学开始!
二、Cubmx配置
1.工程建立
在搜索框中找到STM32F429IGT6型号,选中后点击“start project”
2.基本环境配置
首先RCC配置为外部时钟晶振,SYS中debug选择“SW”即线网模式,这些都是基操了。
然后在时钟配置这里修改频率,这里需要注意的地方比较多,友好起见我一一列出:
1.首先input frequency栏里,这个表示的是外部晶振频率,STM32F429是25MHz,而STM32F407是8MHz,以前在这里吃过很多亏,希望大家一定要根据自己的型号确定晶振频率!!!
2.选择HSE,并且在"System Clock Mux"处选择PLLCLK(锁相环模式),否则sysclk(系统时钟)频率直接就等于HSE的频率了。
3.将APB1时钟频率改成72MHz,因为这里我用的是TIM3定时器触发adc采样,而TIM3是挂在APB1总线上的,因此这里的频率对我们后面修改采样率而言至关重要!(当然对定时器触发adc非常了解的朋友可以不配成72MHz,只要最后能得到想要的采样率即可)。
3.TIM3定时器触发多重adc的配置方式
重头戏来啦~
(1)定时器的配置
首先选中TIM3,将时钟源设为"Internal Clock",然后预分频设为1,周期设为14,根据计算公式即可得到定时器触发adc的频率为:72MHz/[(1+1)*(14+1)] = 2.4MHz,而定时器触发一次会带动三个adc交替采样一次,从而实现3*2.4MHz = 7.2MHz的采样率!
此外,我们需要使能自动重装载,并且将触发事件改为更新事件(图中红框都有标出)。
定时器配置完成!
(2)三重adc的配置
此处是最为核心,也是最容易遗漏东西的点,敲黑板!!
首先,开启所有的adc(总共就三个),并为它们选择相同的通道,我这里选择的就是通道0
adc1的配置:
将Mode调成"Triple interleaved mode only", 即三重adc交替模式,然后开启DMA连续传输请求,并将触发转换源改成"TIM3 Trigger Out event",其他设为默认或是依照我的参数配即可。当然了,在开启DMA请求前,你需要在"DMA Settings"中使能它。
adc1的DMA使能方式如下:
选中"DMA Settings",单击下方"Add",选择"ADC1"。
还没完!首先要将DMA的数据宽度,即"Data Width"修改为Word格式,并确定是"Memory"。至于Mode,我个人习惯用Normal模式,所以没试过配成Circular,感兴趣的朋友可以试试。
如果选择配成"Normal"模式,则需要在每次DMA传输完一组数据后重新开启DMA,重启代码我写在了最后的main.c里。
这些完成后,就可以回到参数设置里,将“DMA连续请求”那里使能啦~
adc2和adc3的配置:
如果你的adc1已经配好,那么这两个adc只需要开启DMA请求即可(注意,不需要额外配置DMA了,用adc1的就行)!但是我的Cubemx可能是版本原因存在bug,无法直接在Cubemx中开启!
(不骗你,确实无法开启)
那么,我们就只能在Keil中自行配置开启了。
4.Keil软件编程
在adc.c文件中,找到adc2的初始化,将DMA连续请求设置为"ENABLE",对adc3也进行同样的配置。
最后,附上我的main.c给大家,内含dma重开启的方法以及如何读取dma搬运的数据,存到你设定的数组里:
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file : main.c
* @brief : Main program body
******************************************************************************
* @attention
*
* Copyright (c) 2023 STMicroelectronics.
* All rights reserved.
*
* This software is licensed under terms that can be found in the LICENSE file
* in the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
******************************************************************************
*/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "adc.h"
#include "dma.h"
#include "tim.h"
#include "usart.h"
#include "gpio.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "stdio.h"
#define data_len 1024 //数组长度
uint32_t dmabuffer[data_len];
float adc_123[data_len*2];
int conv_done=0;//DMA传输完成标志
/* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
/* USER CODE END PTD */
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
/* USER CODE END PM */
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
int fputc(int ch, FILE *f)//printf重定义
{
HAL_UART_Transmit(&huart1, (uint8_t *)(&ch), 1, 0xFFF);
return 1;
}
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
conv_done=1;
int i=0,j=0;
for(i=0,j=0;j<data_len && i<data_len*2;)
{
adc_123[i]=(dmabuffer[j] & 0x0000FFFF)*3.3/4096 ;//取低16位
i++;
adc_123[i]=((dmabuffer[j] & 0xFFFF0000)>>16)*3.3/4096 ;//取高16位
i++;
j++;
}//取出DMA传输的数据
}
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */
/* USER CODE END PFP */
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
void resetdma_adc(void) //dma重开启函数
{
conv_done=0;
HAL_TIM_Base_Stop(&htim3);
HAL_ADC_Stop(&hadc1);
HAL_ADC_Stop(&hadc2);
HAL_ADC_Stop(&hadc3);
// HAL_ADC_Stop_DMA(&hadc1);
// HAL_ADC_Start_DMA(&hadc1,(uint32_t *)dmabuffer,data_len);
HAL_ADC_Start(&hadc3);
HAL_ADC_Start(&hadc2);
HAL_ADCEx_MultiModeStart_DMA(&hadc1,(uint32_t *)dmabuffer,data_len);
HAL_TIM_Base_Start(&htim3);
while(!conv_done);//开启——等待完成
//DMA传输时间=858* 1/72us 约等于12us
}
/* USER CODE END 0 */
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* 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_ADC1_Init();
MX_TIM3_Init();
MX_USART1_UART_Init();
MX_ADC2_Init();
MX_ADC3_Init();
/* USER CODE BEGIN 2 */
HAL_ADC_Start(&hadc3);
HAL_ADC_Start(&hadc2);
HAL_ADCEx_MultiModeStart_DMA(&hadc1,(uint32_t *)dmabuffer,data_len);
//多重adc开启,并让dma把数据搬运到dmabuffer数组中
HAL_TIM_Base_Start(&htim3);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
while(!conv_done) ; //等待dma传输完成
for(int i=0;i<data_len*2;i++)
{
printf("%.3f\r\n",adc_123[i]); //打印采样得到的值
}
conv_done=0;
resetdma_adc();
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
实测可以实现7.2MHz的采样率,当时是用800kHz的正弦波,一个周期能采到9个点~
写在最后
最后补充一些细节部分吧:
首先,并不是任意的采样率都可以通过这个多重adc实现!因为我们假定定时器触发频率是f1,而由数据手册可知,adc1采样后经过固定时间 t(假定这个t对应频率是f2,f2=1/t)后触发adc2采样,再经过时间 t 触发adc3采样,直到下一次定时器触发,才重新开始这个过程。那么你很容易发现,其实一旦f1远小于f2,就容易变成短时间采集三个点的情况(那其实跟一次只采一个点也没多大区别)!只有当f1和f2相等时,才能够实现每个采样点的间隔是相同的,才是一个好的采样方式!
其次,之所以DMA传输数据设置为"Word",是因为DMA一次可以传输32位的数据,而adc转换完成后是16位的数据。所以dma传输一个32位的数据,其实是两个adc的转换结果拼接而成,至于谁是高16位谁是低16位,与转换次序有关,想了解更多的可以看看下面这张图。可以综合这个知识看看我上面是如何编写代码读取dma传输数据的~
最后的最后:当时自己做这个工程的时候也是花费了很多精力,有不少体会,所以码字可能有点多。第一次写文,无关乎热度与关注,只希望能对来到CSDN的大家有些许帮助,笔者就十分满足了。