stm32(ADC,DMA)

167 阅读18分钟

1 ADC

1.1 简介

ADC(Analog-Digital Converter)模拟-数字转换器,可以将引脚上连续变化的模拟电压转换为内存中存储的数字变量,建立模拟电路到数字电路的桥梁(STM32主要是数字电路,数字电路只有高低电平,没有几v电压的概念,所以想读取电压值,就需要借助adc模数转换器来实现)

stm32f103c8t6搭载两块1us转换时间的12位逐次逼近型ADC

  • 12位ad值表示范围就是0~(2^12-1)就是量化结果的范围0~4095,位数越高量化结果就越精细,对应分辨率就越高
  • ad转换是需要花一小段时间的,这里1us就是表示从ad转换开始到产生结果需要花1us的时间,对应ad转换的频率就是1MHz(1MHz的周期是1微秒)
  • 逐次逼近是指在内部通过二分法来逼近最终的电压值

1.2 STM32的ADC框图

image.png

总共有18个输入通道包括16个gpio口和2个内部通道(内部温度传感器和内部参考电压)。模拟多路开关可以指定想要的通道,右边是多路开关的输出,进入到模数转换器(执行逐次比较的过程),转换结果会直接放在数据寄存器里。

stm32的多路开关可以同时选中多个通道,而且在转换的时候还分成了两个组(规则组和注入组)

  • 规则组可以一次性最多选中16个通道,只有一个数据寄存器,后转换的数据会将之前转换的数据覆盖,要想实现同时转换的功能,最好配合DMA来将转换后的数据及时转运
  • 注入组最多可以选中4个通道,不用担心数据覆盖的问题了(了解即可)

结构图的左下角为触发转换信号,触发转换信号来源有两种:软件触发和硬件触发。软件触发就是在程序中手动调用一条代码就可以启动转换了。硬件触发信号可以来自于定时器的各个通道、定时器TRGO定时器主模式的输出,外部中断EXTI。(比如可以给tim3定个1ms的时间并且把tim3的更新事件选择为TRGO输出,然后在ADC选择开始触发信号为tim3的TRGO,这样tim3的更新事件就能通过硬件自动触发adc转换了,整个过程不需进中断,节省了中断资源)

ADC的时钟ADCCLK是来自于RCC的APB2时钟,由原理图可得,ADCCLK最大为14MHz,所以ADC预分频器只能选择6分频(得到12MHz)和8分频(得到9MHz)两个值。

模拟看门狗的功能是监测指定的通道。可以设置模拟看门狗的阈值高限(12位)、阈值底限(12位)和指定“看门”的通道。只要通道的电压值超过阈值范围,模拟看门狗就会“乱叫”,申请一个模拟看门狗的中断,之后通向NVIC。

ECC是规则组的完成信号,JEOC是注入组完成信号,这两个信号会在状态寄存器里置一个标志位,读取这个标志位就能知道是不是转换结束了,同时这两个标志位也可以去到NVIC申请中断,如果开启了NVIC对应的通道就会触发中断

1.3 ADC规则组的四种转换模式

  1. 单次转换,非扫描模式
  2. 单次转换,扫描模式
  3. 连续转换,非扫描模式
  4. 连续转换,扫描模式

单次转换,非扫描

列表就是规则组,有16个空位,分别是序列1~16,你可以在列表写入要转换的通道,在非扫描的模式下,这个列表就只有一个序列1的位置有效,这时列表同时选中一组的方式就退化为简单地选中一个的方式了,可以在序列1的位置指定想转换的通道,然后就可以触发转换,adc就会对通道2进行模数转换,过一小段时间后,转换完成,转换结果放在数据寄存器里,同时给EOC标志位置1,整个转换过程就结束了,然后判断EOC标志位,转换完了就可以在数据寄存器里读取结果了,若想再启动一次转换就需要再触发一次

image.png

连续转换,非扫描

在一次转换结束后不会停止,而是立刻开始下一轮的转换,然后一直持续下去,这样就只需要最开始触发一次,之后就可以一直转换了,这个模式的好处是开始转换之后就不需等待一段时间,因为它一直都在转换,所以不需手动开始转换了也不用判断是否结束,想读ad值的时候直接从数据寄存器读取

image.png

单次转换,扫描模式

每次触发之后,就会依次对前7(通道数目)个位置进行AD转换,转换结果都放在数据寄存器里,为了防止数据被覆盖,就需要用DMA及时将数据挪走,7个通道转换完成后产生EOC信号,转换结束,然后再触发下一次,就又开始新一轮的转换

image.png

连续转换,扫描模式

连续转换扫描模式与单次转换扫描模式不同之处就是一次转换完成后,立刻进行下一次的转换

image.png

1.4 ADC单通道测量引脚电压

扭动电位器,测量电压变化

image.png

ADC.c

#include "stm32f10x.h"
/**
  * @brief  ADC初始化函数(软件触发,且这里不使用模拟看门狗和中断)
  * @param  无
  * @retval 无
  */
void adc_Init(void)
{
	// 1. RCC开启时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
	
	// 二分频和四分频都会让ADC超频影响准确性,ADC最高14MHz
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);// ADCCLK = 72MHz / 6 = 12MHz

	// 2. 配置GPIO
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; // 模拟输入
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	// 3. 将指定的GPIO端口接入规则组列表中
	// 通道0,列表的序列号(只有一个放第一个就行),采样周期是55.5个ADCCLK的周期(由于对这个代码对速度和稳定性没有要求,随便选一个)
	ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);// 把通道0填入序列1中

	// 4. 配置ADC
	ADC_InitTypeDef ADC_InitStruct;
	ADC_InitStruct.ADC_Mode = ADC_Mode_Independent;// ADC模式(独立模式或双ADC模式):独立模式						
	ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right;// ADC数据对齐:右对齐(最低位从最右边开始填,左对齐:最高位从最左边开始填)
	ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;// ADC外部触发源选择:不使用外部源触发(这里使用软件触发)
	ADC_InitStruct.ADC_ContinuousConvMode = DISABLE;// ADC连续转换模式:单次转换
	ADC_InitStruct.ADC_ScanConvMode = DISABLE;// ADC扫描模式:非扫描
	ADC_InitStruct.ADC_NbrOfChannel = 1;// 扫描模式下通道的数量
	ADC_Init(ADC1, &ADC_InitStruct);
	
	/*中断和模拟看门狗在此配置	*/
	
	// 5. 开关控制
	ADC_Cmd(ADC1, ENABLE);
	
	// 6. 对ADC进行校准
	ADC_ResetCalibration(ADC1);// 复位校准
	while (ADC_GetResetCalibrationStatus(ADC1) == SET);// 等待复位校准完成
	ADC_StartCalibration(ADC1);// 开始校准
	while (ADC_GetCalibrationStatus(ADC1) == SET);// 等待校准完成
}

/**
  * @brief  ADC结果读取函数(软件触发)
  * @param  无
  * @retval 转换之后的结果
  */
uint16_t adc_GetValue(void)
{
	// 1. 软件触发开启转换
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);
	
	// 2. 等待转换完成(获取标志位状态,等待EOC标志位置1)
	while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);	// 转换未完成则等待
	
	// 3. 读取ADC数据寄存器并返回
	return ADC_GetConversionValue(ADC1);// 读取之后会自动清除EOC标志位
}

main.c

#include "stm32f10x.h"    
#include "OLED.h"
#include "ADC.h"

int main(void)
{
	OLED_Init();
	adc_Init();
	
	uint16_t adcValue;
	float volatge;
	
	OLED_ShowString(1,1,"ADvalue:");
	OLED_ShowString(2,1,"Volatge:0.00V");
	
	while(1){
		adcValue = adc_GetValue();
		volatge = (float)adcValue/4095*3.3; // stm32的ADC是12位的,adcValue最大值4095,开发板提供电压最大3.3V
		OLED_ShowNum(1,9, adcValue,6);
		OLED_ShowNum(2,9, volatge,1); // 电压分两部分显示,这里的OLED_ShowNum不支持显示浮点数
		OLED_ShowNum(2,11, (uint16_t)(volatge*100)%100, 2);
	}
}

image.png

2 DMA

DMA(Direct Memory Access),直接存储器访问(包括运行内存SRAM、程序存储器Flash和寄存器等),DMA可以提供外设和存储器或者存储器和存储器之间的高速数据传输,无须CPU干预,节省了CPU的资源

stm32f103c8t6 DMA资源:DMA1(7通道),每个通道支持软件触发和特定的硬件触发(每个通道硬件触发源不一样)

  • 如果DMA进行的是存储器到存储器的数据转运,比如我们想把Flash里的数据,转运到SRAM里去,那就需要软件触发了。使用软件触发之后,DMA就迅速地把这批数据,以最快的速度,全部转运完成
  • 如果DMA进行的是外设到存储器的数据转运,就不能一股脑地转运,因为外设的数据是有一定时机的,所以这时就需要用硬件触发。比如转运ADC的数据,那就需要等到ADC每个通道AD转换完成后,硬件触发一次DMA,之后DMA再转运,触发一次转运一次,这样数据才是正确的

2.1 STM32的存储器

外设到存储器,存储器到存储器,本质上其实都是存储器之间的数据转运,说成外设到存储器,只不过是STM32他特别指定了可以转运外设的存储器而已

image.png

  • ROM:只读存储器,是一种非易失性、掉电不丢失的存储器。
  • RAM:随机存储器,是一种易失性、掉电丢失的存储器。
  • 程序存储器Flash:主闪存,我们下载程序的位置。运行程序一般也是从主闪存里面开始运行的。如果再软件里看到,某个数据的地址是0800开头的,就可以确定,它是属于主闪存的数据。
  • 系统存储器和选项字节:存储介质也是Flash,不过我们一般说Flash指的是主闪存Flash,而不指这两块区域。选项字节里寸的主要是Flash的读保护、写保护,还有看门狗等等的配置。
  • 运行内存SRAM:我们在程序中定义变量、数组、结构体的地方。定义一个变量,读取它的地址显示出来,它的地址就是0x2000开头的,类比于电脑的话,运行内存就是内存条。
  • 外设寄存器:我们初始化各个外设,最终所读写的东西。存储器也是SRAM。
  • 内核外设寄存器:内核外设就是NVIC和SysTick。内核外设和其它外设不是一个厂家设计的,所以它们的地址也是被分开了。

注意:

如果DMA的目的地址,填了Flash的区域(是ROM只读存储器的一种),那转运时,就会出错。当然Flash也不是绝对的不可写入,可以配置这个Flash接口控制器,对Flash进行写入,这个流程就比较麻烦,要先对Flash按页进行擦除,再写入数据。总之就是CPU或者DMA直接访问Flash的话,是只可以读而不可以写的,然后SRAM是运行内存,可以任意读写,外设寄存器的话,得看参考手册里面的描述。

2.2 DMA框图

image.png

左上角是Cortex-M3内核,里面包含了CPU和内核外设等等。剩下的所有东西,都可以把它看成是存储器。所以总共是CPU和存储器两个东西,Flash是主闪存,SRAM是运行内存。各个外设都可以看成是寄存器,也是一种SRAM存储器。

寄存器是一种特殊的存储器,一方面,CPU可以对寄存器进行读写,就像读写运行内存一样;另一方面,寄存器的每一位背后,都连接了一根导线,这些导线可以用于控制外设电路的状态,比如置引脚的高低电平、导通和断开开关、切换数据选择器,或者多位组合起来,当作计数器、数据寄存器等等。

image.png

为了高效有条理地访问存储器,这里设计了一个总线矩阵,总线矩阵的左端,是主动单元,也就是拥有存储器的访问杈,右边这些,是被动单元,它们的存储器只能被左边的主动单元读写。主动单元这里,内核有DCode和系统总线,可以访问右边的存储器,其中DCode总线是专门访问Flash的,系统总线是访问其他东西的,另外,由于DMA要转运数据,所以DMA也必须要有访问的主动权。那主动单元,除了内核CPU,剩下的就是DMA总线了

image.png

虽然多个通道可以独立转运数据,但是最终DMA总线只有一条,所以所有的通道都只能分时复用这一条DMA总线。如果产生了冲突,那就会由仲裁器,根据通道的优先级来决定谁来使用。另外在总线矩阵这里,也会有个仲裁器,如果DMA和CPU都要访问同一个目标,那么DMA就会暂停CPU的访问,以防止冲突。不过总线仲裁器,仍然会保证CPU得到一半的总线带宽,使CPU也能正常的工作

image.png

AHB从设备,也就是DMA自身的寄存器,因为DMA作为一个外设,它自己也会有相应的配置寄存器,这里连接在了总线右边的AHB总线上,所以DMA,即是总线矩阵的主动单元,可以读写各种存储器,也是AHB总线上的被动单元。CPU通过这一条线路,就可以对DMA进行配置了

image.png

DMA请求,请求就是触发的意思,这条线路右边的触发源,是各个外设,所以这个DMA请求就是DMA的硬件触发源。比如ADC转换完成、串口接收到数据,需要触发DMA转运数据的时候,就会通过这条线路,向DMA发出硬件触发信号,之后DMA就可以执行数据转运的工作了

2.3 DMA工作流程

image.png

DMA的数据转运,可以是从外设到存储器,也可以从存储器到外设,具体是向左还是向右,有一个方向的参数,可以进行控制。另外,还有一种转运方式,就是存储器到存储器,比如Fash到SRAM或者SRAM到SRAM,这两种方式。由于Flash是只读的,所以DMA不可以进行SRAM到Fash,或者Hlash到Hash的转运操作

  • 第一个是起始地址,外设端的起始地址和存储器端的起始地址,这两个参数决定了数据是从哪里来,到哪里去
  • 第二个参数是数据宽度,这个参数的作用是,指定一次转运要按多大的数据宽度来进行
  • 第三个参数,是地址是否自增,指定,是不是要转运一次挪个坑

传输计数器:指定总共需要转运几次的。这个传输计数器是一个自减计数器,比如写一个5,那DMA就只能进行5次数据转运,转运过程中,每转运一次,计数器的数就会减1,当传输计数器减到0之后,DMA就不会再进行数据转运了。另外,它减到0之后,之前自增的地址,也会恢复到起始地址的位置,以方便之后DMA开始新一轮的转运

自动重装器:传输计数器减到0之后,是否要自动恢复到最初的值。它决定了转运的模式,如果不重装,就是正常的单次模式,如果重装,就是循环模式。比如转运一个数组,那一般就是单次模式:如果是ADC扫描模式连续转换那为了配合ADC,DMA也需要使用循环模式

触发:决定DMA需要在什么时机进行转运的。触发源,有硬件触发和软件触发,具体选择哪个,由M2M这个参数决定,M2M就是Memory to Memory(存储器到存储器)

  • 当我们给M2M位1时,DMA就会选择软件触发,软件触发一般适用于存储器到存储器的转运,因为存储器到存储器的转运是软件启动、不需要时机,并且想尽快完成的任务
  • 当M2M位给0,那就是使用硬件触发了。硬件触发源可以选择ADC、串口、定时器等等,使用硬件触发的转运,一般都是与外设有关的转运。这些转运需要一定的时机,比如ADC转换完成、串口收到数据、定时时间到等等,所以需要使用硬件触发,在硬件达到这些时机时,传一个信号过来,来触发DMA进行转运,这就是硬件触发

2.4 ADC多通道扫描模式+DMA

image.png

ADC扫描模式的执行流程,在这里有7个通道,触发一次后,7个通道依次进行AD转换,然后转换结果都放到ADC_DR数据寄存器里面(如果不及时转移走,数据会被覆盖掉)

可以在每个单独的通道转换完成后,进行一个DMA数据转运,并且目的地址进行自增,这样数据就不会被覆盖了。所以在这里DMA的配置就是,外设地址,写入ADC_DR这个寄存器的地址;存储器的地址,可以在SRAM中定义一个数组ADValue,然后把ADValue的地址当做存储器的地址

传输计数器,这里通道有7个,所以计数7次;计数器是否自动重装,这里可以看ADC的配置,ADC如果是单次扫描,那DMA的传输计数器可以不自动重装,转换一轮就停止,如果ADC是连续扫描,那DMA就可以使用自动重装,在ADC启动下一轮转换的时候,DMA也启动下一轮的转运,ADC和DMA同步工作

触发选择,这里ADC_DR的值是在ADC单个通道转换完成后才会有效,所以DMA转运的时机,需要和ADC单个通道转换完成同步,所以DMA的触发要选择ADC的硬件触发(ADC扫描模式,在每个单独的通道转换完成后,没有任何标志位,也不会触发中断,但是会DMA请求)

接线图:

PA0-PA3接了4个传感器,使用ADC连续扫描的方式一直读取这4个GPIO口,通过DMA把数据转移到AD_Value数组中

image.png

代码: ADC.c

#include "stm32f10x.h" // Device header

uint16_t AD_Value[4]; // ADC数据存放的数组

void AD_Init(void)
{
	// 1. 启动时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
	
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);
	
	// 2. 配置GPIO PA0-PA3连接4个传感器
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	// 3. 将指定的GPIO端口接入规则组列表中
	ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
	ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5);
	ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5);
	ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5);
		
	// 4. 配置ADC
	ADC_InitTypeDef ADC_InitStructure;
	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // 不使用外部源触发
	ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; // 连续转换
	ADC_InitStructure.ADC_ScanConvMode = ENABLE; // 扫描模式
	ADC_InitStructure.ADC_NbrOfChannel = 4; // 4个通道
	ADC_Init(ADC1, &ADC_InitStructure);
	
	// 5. 配置DMA
	DMA_InitTypeDef DMA_InitStructure;
	DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; //需DMA转移的源头地址,ADC一次转换后的结果存放在ADC->DR寄存器中
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 不自增
	DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value; // DMA转移的目的地址
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; 
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // 自增
	
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; // 传输方向 外设是源头
	DMA_InitStructure.DMA_BufferSize = 4; // 传输计数器,有4个通道,转运4次
	DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // 循环模式
	DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; // 硬件触发
	DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; // 优先级,这里随便给
	DMA_Init(DMA1_Channel1, &DMA_InitStructure); // 因为DMA每个通道对应不同的外设请求,ADC1对应DMA1的通道1,参考DMA1请求映像
	
	// 6. 使能
	DMA_Cmd(DMA1_Channel1, ENABLE);
	ADC_DMACmd(ADC1, ENABLE); // 开启ADC的DMA触发信号
	ADC_Cmd(ADC1, ENABLE);
	
	// 7. 对ADC进行校准
	ADC_ResetCalibration(ADC1);
	while (ADC_GetResetCalibrationStatus(ADC1) == SET);
	ADC_StartCalibration(ADC1);
	while (ADC_GetCalibrationStatus(ADC1) == SET);
	
	// 8. 软件触发ADC转换
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}

main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "ADC.h"

int main(void)
{
	OLED_Init();
	AD_Init();
	
	OLED_ShowString(1, 1, "AD0:");
	OLED_ShowString(2, 1, "AD1:");
	OLED_ShowString(3, 1, "AD2:");
	OLED_ShowString(4, 1, "AD3:");
	
	while (1)
	{
		OLED_ShowNum(1, 5, AD_Value[0], 4);
		OLED_ShowNum(2, 5, AD_Value[1], 4);
		OLED_ShowNum(3, 5, AD_Value[2], 4);
		OLED_ShowNum(4, 5, AD_Value[3], 4);
		
		Delay_ms(100);
	}
}