STM32之DAC部分

        本篇开始就进入到了DAC部分啦~一起来学习一下吧。

        我们都知道,单片机读取到的电压信号只有高低电平之分,如果我们想要读取0—3.3V之间的电压怎么办呢?这个时候我们就可以利用ADC模数转化器了,ADC是一个将模拟信号转化为数字信号的外设,STM32的ADC外设是12位的,也就是说,它说读出来的数据是2的十二次方,也就是4096,当从ADC读出来的数据是4095(因为外设结构问题,最大只可读到4095)时,读取到的电压就是最大值。这里ADC输入电压范围为0—3.3V,那么如何将ADC读取到的数据转化为电压值呢?其实就是一个线性关系,也就是读取到的数据数据值/4095*3.3,算出来的数据就是电压值。STM32103XX系列的芯片最多有18个通道,16个外部通道,2个内部通道分别是看门狗通道,内部cpu温度检测通道。当我们想启用看门狗通道时,我们可以设置一个电压阈值,这个模拟看门狗通道可以检测电压值,当电压值不在这个阈值时,看门狗会激励外部做出一些反应,这个我们也是可以设置的。

        因为STM32的ADC采用的是逐次逼近型的ADC,我们就先来了解一下这个ADC是怎么把电压值读出来的吧。

b1dc3bf028e14f53b619a9a10d05b80c.jpg

        以上的框图是ADC0809芯片的内部结构,它也是逐次逼近型的,与STM32的ADC是一样的工作原理。这款ADC芯片是八位的,而STM32的ADC是12位的。

        首先通过左下角的译码器给ADDA、ADDB、ADDC写入数据就可以选择我们需要的通道了,一共有八位通道,选择好之后,通道选择开关就会打开对应的通道,把电压值读取到电压比较器的引脚1,而电压比较器的引脚2是接入到DAC的,这个DAC可以把一个数据转化为电压信号,然后电压比较器会不断对比两者的电压值,这个比较过程我们用二分法来实现,也就是说,这个八位的寄存器满程是256,先对半分为64,然后比较,如果寄存器数据大了就再对半分为32,这样依次二分,就可以最快找到电压值相等时的数据了。这个时候DAC就会把数据写入到锁存缓冲器中。然后标志位EOC置1,完成数据读取过程。我们就可以通过读取缓冲器的值来读取电压的数字值了。这个就是逐次逼近型ADC的原理。

        那么DAC是如何将数据信息转化为电压信号呢?这里就用到了T型网络加权电阻。在这之前,我们先来学习一下运算放大器的工作原理,更有利于我们理解DAC的电路。


4449ce7752224149a90bfdcca1d6fcfa.jpg

         运算放大器的网络符号如上图,看起来好像是一个独立的元器件对吧?但是实际上它是一个集成了许多元器件的芯片,里面集成了差分放大器,电压放大器,功率放大器三级放大电路。因为太常用了,所以做成了一个模板,集成到了一起,变成一个运算放大器。运算放大器有四个运用场景。

  1. 电压比较器:顾名思义,两个输入端输入电压值,这个运算放大器就会比较这两个电压,当V+大于V-时,输出角就会输出一个无穷倍的(V+➖V-),但是由于电源电压限制,输出电压又不能是无穷大,所以最大只能和VCC相等了,反之,当V—大于V+时,输出角就会输出一个无穷倍的(V-➖V+),由于电路限制最小电压是GND,所以输出角电压瞬间被拉低为GND。这就是电压比较器。在这种工作模式下,称之为开环状态。530de25e75594bad9722f9d2d49f0463.jpg
  2. 反向放大器:反向放大器的同相输入脚接GND,反向输入脚接信号源,在这种工作模式下,输出端是与反向输入脚相接的。形成了“闭环”状态,这种工作模式下,基于前者电压比较器的原理,当V-大于V+时,输出为无穷大倍的差值,也就是负电压,这个时候V-电压值被Vout拉低至GND以下,这个时候V+电压值又大于V-了,输出电压值就变成了无穷倍正电压,这个正电压又会拉高Vin的电压,这样子循环往复,直到V+和V-的电压值差不多时,这种负反馈现象才会终止,称之为“虚短”。在这种情况下,运放的输入阻抗非常大,对前级电路几乎没有产生影响,所以可以认为V+与V-是断路的,没有电流经过。称之为“虚短”。那么我们利用“虚断”和“虚短”的特性来分析这个反相放大器。因为“虚短”特性,V+=V-=0V,可以得出经过R1的电流和经过R2的电流应该是相等的,这个时候Vout=0-(Vin/R1)×R2,可以看出-R2/R1就是放大倍数,经过这个反向放大器后,Vout被反相放大了-R2/R1倍。a43d7f74098f43aab30ec88de467e86a.jpg
  3. 同相放大器 :同相放大器的同相输入端接VIN,反相输入端接一个电阻至地,Vout与反向输入端连接,还是一样的利用虚短与虚短的特性,分析知Vout=Vin+(Vin/R1)×R2,整理得Vout=(1+R2/R1)Vin,放大倍数就是1+R2/R1。 5a95e4d3f8804ed5b663b5b488f95f41.jpg
  4. 电压跟随器:这里的电压跟随器实际上就是同相放大器的R2取了无穷大值,相当于断路状态,这个时候Vout=Vin,那么这个电路虽然没有放大的作用,但是可以驱动大功率的设备运行,也就是说,当我们要驱动大功率设备时,可以使用这种电路接法。

    0dc52f1e0cc640779bed2f11685c91c0.jpg

        这就是运算放大器比较常见的用法原理了。那么接下来就可以仔细讲一讲DAC的T型电阻网络电路了。

fdf3cd9bdc854153bba0a0b2f7328022.jpg

        不难看出,这个电路用到的电阻阻值只有2R和R,从最右边开始看,2R与2R并联,得到R,R再与I1支路上的R串联,就是2R,依次循环往复,可以得到整个电路的等效电阻就是R,那么总电流I=Vref/R,又因为I=2I7=4I6=8I5=16I4=32I3=64I2=128I0,这些都是2的位权,通过写入数据,开关会对应拨到左边或者右边以此来控制电流是否接入。那么当我们写入数据后,具体电流I'=(写入数据值/256)×I,那么Vo=0-I'×Rfb。这就是T型电阻网络DA转换器的电路原理了。

        那么接下来言归正传,继续回到STM32的ADC理论框图吧。

5f677e7ee06d460f9bca5ef5e5465514.jpg

        首先是通道选择,这里之前说过了,DAC一共有18个通道 ,分别是16个GPIO口以及两个内部通道(cpu温度采集通道和看门狗监测电压通道),通过开关选择器可以选择哪一个通道接入,随后就是选择接入到注入组通道还是规则组通道了。注入组最多有4个通道,并且这4个通道的数据可以被同步写入到寄存器中,不会有数据覆盖问题。而规则组最多有16个通道,但是如果有多个通道被选择时,只有最后一个通道的数据会被写入到寄存器中,因为前面几个通道的数据都被覆盖了,所以我们要搭配后续讲解到的dma来使用这个规则组多通道。dma可以把ADC读取到的数据立马搬运出来,避免了数据覆盖问题。

b3a6ee82b9db4a4c93f49bcccd0e37e8.jpg

        接下来就是触发源选择了,那么触发ADC采集数据的触发源分为两大类,软件触发和硬件触发。软件触发只需要我们在程序中写一条执行代码即可,而硬件触发源可以是来自于定时器的通道,,这里有一个之前提到的TRGO通道,也就是说,我们可以把定时器3定时为1ms,然后把定时器3的更新事件映射到主模式的TRGO引脚,然后这里就可以选择TIM3的TRGO为触发源,每1ms就出发一次数据读取。如果不使用这个TRGO模式,那么就只能利用定时中断,每隔1ms就进入到中断中执行一次读取数据的操作,这样过于频繁的进入中断会影响主程序的运行,而这种操作又只是简单的读取数据,所以我们可以利用主模式的TRGO来完成这个过程。

        当然了,这里也可以选择外部中断来触发数据读取。

d42684b0b52345df8cac936c44f811a8.jpg

        接下来看一下ADC的时钟选择,因为手册中写到ADC的工作频率最大只能是14MHZ,因此,通过内部时钟源分频后的驱动ADC的工作频率就不能超过14MHZ,所以72/6=12MHZ,这里我们就选择6分频就行了。

        下面是规则组和注入组的数据寄存器,当寄存器中写入值时,就会立一个标志位(规则组和注入组都能立EOC标志位,注入组立JEOC标志位,模拟看门狗立AWD标志位),使能后面的NVIC中断,如果需要中断的话,可以配置这部分的参数。这个模拟看门狗之前也介绍过了,在这里设置一个电压阈值,当超过这个电压阈值时,模拟看门狗就会立一个标志位,申请中断。

        那么接下来就来了解一下ADC的4种转化模式吧。

  1. 单次转化,非扫描模式:这种模式下的DAC,读取完一次数据后就会停下来,并且只有第一个通道有效。如果想要连续转化,就要在程序中循环这个过程,也就是触发转化,读取标志位判断是否转化完成,取出数据。
  2. 连续转化,非扫描模式:这种模式下的DAC每读取完一次数据后不会停下来,而是进行下一次的数据读取,所以这种模式下我们只需要执行一次软件触发转化,不需要判断标志位,直接读取数据即可。同样的,非扫描模式下的DAC也是只有第一个通道是有效的。
  3. 单次转化,扫描模式:这种模式下的ADC与第一种模式一样,读取完一次数据后会立标志位,然后需要不断的执行软件触发的操作,但是这种模式下多个通道都是有效的,可以转运多个通道的数据。
  4. 连续转化,扫描模式:这种模式是以上模式的结合,也就是触发源触发一次,判断标志位置1后,就会不断的读取多个通道的数据,然后DAM就会立即转运这些数据,防止数据被覆盖。

        以上就是四种转运模式了。

c15fdd7d83ff43fd906e1db09f607fdc.jpgADC通道对应的引脚关系图

        接下来说一下配置参数时需要用到的知识吧。

ce579222277f46cb91605493ab6eacdf.jpg

         我们读取到的数据可以选择左对齐和右对齐,那么一般情况下我们都是选择右对齐的,因为要读取电压数据值,用右对齐数据不会被扩大。用左对齐的话数据会被扩大16倍,这种对齐模式一般用于读取大概电压值范围的,用左对齐取出高八位的数据,这样就把把后四位的精度给省略了,到一个大概的电压值用于简单的判断。

        接下来是采集数据的时间,ADC采集数据时间=采样保持时间+量化编码时间。

        量化编码时间是固定的为12.5个ADC周期,这里也很好理解,因为ADC是12位的,每得到一位数据要花费一个周期的时间,多出来的0.5个ADC周期可能是用于其他一些细节部分。

        这里的采样保持时间我们是可以配置的,一般来说,采样的时间越长,越能避免一些毛刺信号的干扰,采集到的数据就越准确。

        最后还有一个ADC校准,这个是开发者为了我们读取到的数据更加精准而设计出来的,流程就是开启复位校准,等待复位校准完成,开启校准,等待校准完成,这也是固定的四步。


        接下来就是代码部分,这里我们实现在VCC和GND之间接一个可调电阻,通过调节这个可调电阻来实现调节电压。并通过ADC来采集这个通道的数据。这里我们就把这个可调电阻接到通道8吧,也就是GPIOB的Pin0口。

        还是老样子,把框图里面的部分都配置好,就可以打通这条电路了。

3a2c496fb7f34453826a71cedded2c75.jpg

  1. 第一步:开启GPIO和DAC的时钟。
  2. 第二步:配置好GPIOB的相关参数,这里引脚编号是Pin0,GPIO口模式为模拟输入(这种模式是专门为ADC模块设计的,这种模式下GPIO口的电压会被断开以避免影响外部电压的采集)。
  3. 第三步:配置好ADC的分频,固定选择为6分频。这里不能忘记,忘记的话ADC就没有驱动时钟了。
  4. 第四步:选择通道,我们用的是ADC1的第八个通道,通道数为1,采样时间为55个ADC周期。
  5. 第五步:初始化ADC结构体,需要配置的参数有ADC的工作模式(我们先使用独立工作模式,后续还有双ADC工作模式),数据的对齐方式(选择右对齐),触发源选择(我们选择软件触发),扫描模式还是非扫描模式(我们只用到了1个通道,选择非扫描模式),单次模式还是连续模式(我们先选择单次模式),通道数目是多少(这里通道数目是1)。
  6. 第六步:开启ADC电源。
  7. 第七步:校准电压。(固定的四条代码)
  8. 第八步:写一个函数封装读取ADC采样值。首先软件触发,然后判断标志位是否置1(为1时读取数据完成),最后就是读取数据了。
void AD_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
	
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);
	
	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(GPIOB, &GPIO_InitStructure);
	
	ADC_RegularChannelConfig(ADC1,ADC_Channel_8,1,ADC_SampleTime_55Cycles5);
	
	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=DISABLE;
	ADC_InitStructure.ADC_ScanConvMode=DISABLE;
	ADC_InitStructure.ADC_NbrOfChannel=1;
	ADC_Init(ADC1, &ADC_InitStructure);
	
	ADC_Cmd(ADC1,ENABLE);
	
	ADC_ResetCalibration(ADC1);
	while(ADC_GetResetCalibrationStatus(ADC1)==SET);
	ADC_StartCalibration(ADC1);
   while(ADC_GetCalibrationStatus(ADC1)==SET);
	
}
uint16_t AD_GetValue()
{
	ADC_SoftwareStartConvCmd(ADC1,ENABLE);
	while(ADC_GetFlagStatus(ADC1,ADC_FLAG_EOC)==RESET);
	return ADC_GetConversionValue(ADC1);
}

        在主函数中运行即可。

OLED_Init();
	AD_Init();
	
	OLED_ShowString(1,1,"ADValue:");
	OLED_ShowString(2,1,"V:0.00V");
	while(1)
	{
		ADValue=AD_GetValue();
		OLED_ShowNum(1,9,ADValue,4);
		V=(float)ADValue/4095*3.3;
		OLED_ShowNum(2,3,V,1);
		OLED_ShowNum(2,5,(uint16_t)(V*100)%100,2);
		Delay_ms(100);
	}

        假设我们这里要使用连续模式呢?连续模式就是只用触发一次,DAC就会不停的搬运数据。那么用代码实现的话就是把软件触发放在初始化代码里面,调用一次即可,然后在循环里面不断获取数据寄存器的值即可,也是很方便的。

void AD_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
	
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);
	
	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(GPIOB, &GPIO_InitStructure);
	
	ADC_RegularChannelConfig(ADC1,ADC_Channel_8,1,ADC_SampleTime_55Cycles5);
	
	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=DISABLE;
	ADC_InitStructure.ADC_NbrOfChannel=1;
	ADC_Init(ADC1, &ADC_InitStructure);
	
	ADC_Cmd(ADC1,ENABLE);
	
	ADC_ResetCalibration(ADC1);
	while(ADC_GetResetCalibrationStatus(ADC1)==SET);
	ADC_StartCalibration(ADC1);
   while(ADC_GetCalibrationStatus(ADC1)==SET);
	ADC_SoftwareStartConvCmd(ADC1,ENABLE);
}
uint16_t AD_GetValue()
{
	return ADC_GetConversionValue(ADC1);
}

        这里我再变换一下,如果我想获取多个通道的数据如何实现呢?这里就可以用扫描模式了,但是因为还没有讲到DMA数据搬运,我们就先不用扫描模式了,就用单次模式,非扫描模式。我们只需要把配置通道参数的函数拿出来放到我们封装的获取电压数据的函数里面就可以了,形参给一个通道值,这样在循环里面一直调用这个获取电压数据值的函数就可以了,因为在获取电压数据值的函数里面我们已经指定了特定的通道并且也使用了软件触发,满足了ADC工作的条件。我们只需要多次调用这个函数,就可以显示多个通道的数据了。

 

void AD_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
	
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);
	
	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(GPIOB, &GPIO_InitStructure);
	
	
	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=DISABLE;
	ADC_InitStructure.ADC_ScanConvMode=DISABLE;
	ADC_InitStructure.ADC_NbrOfChannel=1;
	ADC_Init(ADC1, &ADC_InitStructure);
	
	ADC_Cmd(ADC1,ENABLE);
	
	ADC_ResetCalibration(ADC1);
	while(ADC_GetResetCalibrationStatus(ADC1)==SET);
	ADC_StartCalibration(ADC1);
   while(ADC_GetCalibrationStatus(ADC1)==SET);
	
}
uint16_t AD_GetValue(uint8_t ADC_Channel)
{
		ADC_RegularChannelConfig(ADC1,ADC_Channel,1,ADC_SampleTime_55Cycles5);
	ADC_SoftwareStartConvCmd(ADC1,ENABLE);
	while(ADC_GetFlagStatus(ADC1,ADC_FLAG_EOC)==RESET);
	return ADC_GetConversionValue(ADC1);
}

uint16_t AD0,AD1,AD2,AD3;

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)
	{
		AD0=AD_GetValue(ADC_Channel_0);
		AD1=AD_GetValue(ADC_Channel_1);
		AD2=AD_GetValue(ADC_Channel_2);
		AD3=AD_GetValue(ADC_Channel_3);
		
		OLED_ShowNum(1,5,AD0,4);
		OLED_ShowNum(2,5,AD1,4);
		OLED_ShowNum(3,5,AD2,4);
		OLED_ShowNum(4,5,AD3,4);
		
		Delay_ms(100);
	}
	
	
}

图均源自网络

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值