打印
[其他ST产品]

STM32 ADC测量电池电压(使用内部参考电压)

[复制链接]
146|7
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
  在使用ADC时,通常的用法是Vref+接电源VDD3.3V,然后计算时直接用3.3V做参考电压,但是这种方法忽略了一些情况如供电电压有可能随外部一些其他用电器工作使用的大电流而导致电压不稳定,还有可能MCU供电LDO转换的精度个别偏差较大。这时候依然用3.3V的定值做参考电压计算显然得出的值就会出现与实际电压偏差较大的问题。

【解决方案】
一般 100 脚以上的 STM32 MCU都有 VREF 引脚。对于 100 脚以下的芯片,STM32 没有把 VREF 引脚引出来,而是直接在内部连接到了 VDDA 引脚。这样就导致了 ADC 的供电电源和参考电源实际是一个。通常项目中我们VDDA也是连接到了VDD。
如果有 VREF 引脚,可以在VREF上接一个稳定且精度高的电压作为参考电压。

还有一种方法是启用内部参考电压。
根据STM32f10xCDE数据手册中的数据,这个参照电压的典型值是1.20V,最小值是1.16V,最大值是1.24V(-40~85度)。这个电压基本不随外部供电电压的变化而变化。




使用特权

评论回复
沙发
9dome猫|  楼主 | 2024-2-27 16:04 | 只看该作者
很明显,如果以这个为参考电压,我们也得测量其值,因为它对于不同的芯片是一个范围,并不是确定值。 STM32 可以通过配置将 VREFINT 接入到 ADC 内部的通道17,然后我们就可以像测试普通的通道一样测量 VREFINT 到底是多少。注意MCU 不同 具体连接的 ADC 通道也是不同的。




具体方法是在测量某个通道的电压值之前,先读出参照电压的ADC测量数值(即从ADC1_IN17读出的值),记为ADrefint;再读出要测量通道的ADC转换数值,记为ADchx;则要测量的电压为:

Vchx = Vrefint * (ADchx/ADrefint)

其中Vrefint为参照电压=1.20V(STM32F10x)。

推导过程:

Vchx = VREFINT* (((ADchx*(VREF/4096))/(ADre*(VREF/4096)))

注:VREFINT=1.2V(这里取1.20V实际上会有误差),VREF为参考电压值=3.3V
此公式可以理解为:(ADchx*(VREF/4096)是正常计算的含误差电压值,VREFINT/(ADre*(VREF/4096)是修正参数,ADre*(VREF/4096)得到实际的参考电压值比较接近VREFINT,VREFINT是校准电压值,VREFINT/(ADre*(VREF/4096)是约等于1的一个修正值。 – 个人理解


使用特权

评论回复
板凳
9dome猫|  楼主 | 2024-2-27 16:08 | 只看该作者
公式简化后:

Vchx = VREFINT*(ADchx/ADre)

该式计算得到的值是该通道的实际电压值。
注意:上面的方法针对F1芯片只给了参考电压的范围,没有提供出厂校准值的情况,下面将介绍提供了校准值的情况。

使用特权

评论回复
地板
9dome猫|  楼主 | 2024-2-27 16:20 | 只看该作者
VREFINT_CAL是芯片出厂时厂家测量出来的参考电压值固化在flash中,U16两个字节,可以作为基准电压。使用时读出即可。该值不是所有系列芯片都有,F103貌似都没有,下图是L475的(注意不同芯片该值保存的地址不一致): 上图表示,厂家在30度左右环境温度下,VDDA和参考电压等于3.0V的状态下,通过ADC通道读出的参考电压值,保存在0x1FFF75AA开始地址的2个字节中。如何读取示例:VREFINT_CAL = *(u16*)0x1FFF75AA,笔者手里的L475读出来是0x067F,换算3.0*(0x067F/4095)=1.218315,接近1.20了。

使用特权

评论回复
5
9dome猫|  楼主 | 2024-2-27 16:21 | 只看该作者
我们重点关注这个公式:


VREFINT_CAL:内部参考电压校准值,直接地址读取。比如该款芯片地址:0x1FFF75AA,那么我们可以这么做:
VREFINT_CAL = *(__IO uint16_t *)(0x1FFF75AA);

FULL_SCALE:根据我们设置的ADC分辨率而定,12位ADC分辨率值:2^12 - 1 = 4096 - 1

VREFINT_DATA:从ADC_17通道读出实际内部参考电压值

ADCx_DATA:需要测试的电压通道读值
注意:公式中的3.0V有时可能是3.3V,取决于厂家给的校准值是在3.0V条件下测试还是3.3V或是其他。

使用特权

评论回复
6
9dome猫|  楼主 | 2024-2-27 16:22 | 只看该作者
推导过程:
第一个公式VDDA = 3.0V x VREFINT_CAL / VREFINT_DATA 这个公式是怎么来的呢?
ST厂商 通过配置将 VREFINT 连接到 ADC 后,则有:VREFINT = 3.0V * (VREFINT_CAL / 4095); VREFINT_CAL 就是校准条件下的 ADC 采样值(校准条件就是指VDDA=Vref+=3.0V,环境温度30度),采到的VREFINT_CAL值写入到flash。
我们自己通过配置将 VREFINT 连接到 ADC:VREFINT = VDDA * (VREFINT_DATA / 4095);
因此,VDDA * (VREFINT_DATA / 4095) = 3.0 * (VREFINT_CAL / 4095);
VDDA = 3.0V x VREFINT_CAL / VREFINT_DATA

VDDA是这个公式中的重点,我们常规的算法直接用3.3V作为VDDA计算才导致了误差,因为VDDA有误差不是刚好3.3V。

第二个公式Vchannalx=VDDA*(ADCx_DATA/FULL_SCALE)
这个公式很好理解,就是我们常用的计算电压方式,ADCx_DATA是读出的采样值,如:3.3*(1650/4095)

上面两式联立就能得出最后的公式。

前面讲过的 Vchx = Vrefint * (ADchx/ADrefint),Vrefint=1.20V

使用特权

评论回复
7
9dome猫|  楼主 | 2024-2-27 16:23 | 只看该作者
实际上就是Vchannelx = (3.0xVrefint/FULL_SCALE)*(ADCx_DATA/VREFINT_DATA),其中(3.0xVrefint/FULL_SCALE)=1.20V

#define ADC1_DR_ADDR      ((uint32_t)0x4001244C)
float battaryValue;
u16 ADC_VALUE[2];
void ADC1_AdcInit(void)
{
//ADC_InitTypeDef ADC_InitStuctrue;  
       
                        ADC_InitTypeDef ADC_InitStructure;

                        GPIO_InitTypeDef GPIO_InitStructure;  

                        DMA_InitTypeDef DMA_InitStructure;       

                        RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_ADC1,ENABLE); //使能AHB预分频器到端口A的开关

                        RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);


                        RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
                        // RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);//引脚复用 进行重映射时要开启AFIO 时钟
                        //声明结构变量
                        GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;             //定义PA0,PA1脚为AD输入
                        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;                      //IO口为模拟输入模式
                        //GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;            //AD口设置为浮空输入
                        GPIO_Init(GPIOA, &GPIO_InitStructure);   


                        RCC_ADCCLKConfig(RCC_PCLK2_Div4);

                        /* DMA1 channel1 configuration ----------------------------------------------*/
                        DMA_DeInit(DMA1_Channel1);
                        DMA_InitStructure.DMA_PeripheralBaseAddr = ADC1_DR_ADDR;
                        DMA_InitStructure.DMA_MemoryBaseAddr = (u32)&ADC_VALUE;
                        DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
                        DMA_InitStructure.DMA_BufferSize = 2;
                        DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
                        DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
                        DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
                        DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
                        DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
                        DMA_InitStructure.DMA_Priority = DMA_Priority_High;
                        DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
                        DMA_Init(DMA1_Channel1, &DMA_InitStructure);

                        //DMA_ITConfig(DMA1_Channel1,DMA_IT_TC,ENABLE);
                        /* Enable DMA1 channel1 */
                        DMA_Cmd(DMA1_Channel1, ENABLE);

                        // ADC内置温度传感器禁止
                        ADC_TempSensorVrefintCmd(DISABLE);

                        /* ADC1 configuration ------------------------------------------------------*/
                        ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
                        ADC_InitStructure.ADC_ScanConvMode = ENABLE;
                        ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;//DISABLE;
                        ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
                        ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
                        ADC_InitStructure.ADC_NbrOfChannel = 2;
                        ADC_Init(ADC1, &ADC_InitStructure);

                        ADC_RegularChannelConfig(ADC1,  ADC_Channel_4,      1, ADC_SampleTime_55Cycles5);
                        ADC_RegularChannelConfig(ADC1,  ADC_Channel_17,     2, ADC_SampleTime_55Cycles5);

                        ADC_DMACmd(ADC1, ENABLE);

                        /* Enable ADC1 external trigger */
                        ADC_ExternalTrigConvCmd(ADC1, DISABLE);

                        ADC_Cmd(ADC1, ENABLE);
                        /* 重置ADC校准寄存器 */
                        ADC_ResetCalibration(ADC1);
                        /* 检测ADC校准寄存器状态 */
                        while(ADC_GetResetCalibrationStatus(ADC1));
                        /* 开始ADC校准程序 */
                        ADC_StartCalibration(ADC1);
                        /* 检测ADC校准状态 */
                        while(ADC_GetCalibrationStatus(ADC1));

                        ADC_SoftwareStartConvCmd(ADC1, ENABLE);



                        ADC_TempSensorVrefintCmd(ENABLE);//打开内部参照电压       

  
}


void readBattaryValue(void)
{
         
    battaryValue=(float)1.20*((float)ADC_VALUE[0]/ADC_VALUE[1]);//参考电压1.2 V  
                                       
}

使用特权

评论回复
8
9dome猫|  楼主 | 2024-2-27 16:23 | 只看该作者
附带记录VDD、VDDA,VBAT,VREF的区别
MCU 的参考手册都会有一章节单独介绍 MCU 的电源管理,针对不同的 MCU(封装不同等)其外部电源如何连接也是有要求的,我们在 MCU 上一般都会发现如下引脚(注意不同 MCU 是有区别的):

VDD / VSS: VDD is the external power supply for the I/Os, the internal regulator and the system analog such as reset, power management and internal clocks. It is provided externally through VDD pins.

VDDA / VSSA: VDDA 是A/D转换器,D/A 转换器,参考电压缓冲器,运算放大器和比较器的外部模拟电源。 VDDA 电压电平与 VDD 电压无关。 不使用这些外设时,最好将 VDDA 连接到 VDD。

VBAT: 当不存在 VDD 时,VBAT 是 RTC,外部时钟 32kHz 振荡器和备用寄存器(通过电源开关)的电源。 对于没有专用引脚的小型封装,VBAT内部连接到了 VDD

VREF+ / VREF-: VREF+ 是 ADC 和 DAC 的输入参考电压。 使能后,它也是内部参考电压缓冲器的输出。当不使用 ADC 和 DAC 时,VREF+ 可以接地。VRE- 必须始终等于 VSSA。
VREF- 和 VREF+ 引脚并非在所有封装中都可用。 如果封装上未提供它们,则它们在 MCU 内部分别与 VSSA 和 VDDA 相连。

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

99

主题

1292

帖子

1

粉丝