普冉单片机PY32F002BF的ADC内部参考电压VREFINT检测详解
第一次使用普冉的单片机,因为太便宜了,20脚的32位单片机PY32F002B只需要几毛钱。
刚好项目需要用到低成本方案。
这篇主要是记录使用ADC内部参考电压遇到的问题。
因为用到了锂电池电压是浮动的3V-4.2V,再加上是低成本方案,没有稳压3.3V芯片,所以不适合使用ADC外部参考电压,也不适合增加一个运放跟随器,所以精度完全靠mcu本身adc的精度。
PY32F002B标的ADC是12位的,实际上可能达不到12位。
遇到的问题:
1.adc模式输入上下拉问题。
2.输入阻抗问题。
3.采样时间设置
4.使用内部1.5V的参考电压后数据偏大。
功能要求,低功耗,adc检测精度在10mV左右。
问题1,adc的输入模式设置不对,导致adc值误差。
py_gpio_config(GPIOB, GPIO_PIN_1, GPIO_MODE_ANALOG, GPIO_SPEED_FREQ_HIGH, GPIO_NOPULL);
这是输入模式的配置,主要是上下拉模式的配置,上下拉配置成上拉或者下拉时,会出现adc偏大或者偏小,所以需要配置不上下拉模式。GPIO_NOPULL。问题解决。
问题2.输入阻抗问题。电路采样串联电阻分压方式。原来为了低功耗设置,用了两个2M:1M的电阻,导致的后果就是输入阻抗过大,adc直接读取到电源的电压值,无法分压。
最后修改成20k:10k分压电阻。
为了低功耗,多加一个IO 口控制接地。这样只需要检测的时候才打开,就可以减低功耗了。然后输入主控是≈6.6k.F002B的数据手册没写输入阻抗是多少,但是最好别超过100K。问题2解决。
问题3,采样时间设置。因为是采样的电池的电压,为了防止尖刺,增加了一个RC滤波。R=100R,C=0.1uf。R*C=10us,充电时间是10us,再加上MCU本身的adc有一个5PF的电容,加上电路寄生电容和噪声,索引采样时间大概=5倍*10us=50us。采样时间设置50us。
我这边用到了低功耗,所以减低了AHP,APB总线上的频率。
//配置RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1 时钟源为内部高速HSI
void py_SystemClockConfig_HSI(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/* 振荡器配置 */
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE | RCC_OSCILLATORTYPE_HSI | RCC_OSCILLATORTYPE_LSI | RCC_OSCILLATORTYPE_LSE; /* 选择振荡器 HSE, HSI, LSI, LSE */
RCC_OscInitStruct.HSIState = RCC_HSI_ON; /* 打开 HSI */
RCC_OscInitStruct.HSIDiv = RCC_HSI_DIV1; /* HSI 1 分频 */
RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_24MHz; /* 配置 HSI 时钟 24MHz */
RCC_OscInitStruct.HSEState = RCC_HSE_BYPASS_DISABLE; /* 关闭 HSE bypass */
RCC_OscInitStruct.LSIState = RCC_LSI_OFF; /* 打开 LSI */
/*RCC_OscInitStruct.LSICalibrationValue = RCC_LSICALIBRATION_32768Hz;*/
RCC_OscInitStruct.LSEState = RCC_LSE_OFF; /* 关闭 LSE */
/*RCC_OscInitStruct.LSEDriver = RCC_LSEDRIVE_MEDIUM;*/
HAL_RCC_OscConfig(&RCC_OscInitStruct);
/* 配置时钟源 */
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1; /* 选择配置时钟: HCLK, SYSCLK, PCLK1 */
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_HSISYS; /* 设置HSI为系统时钟 */
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV4; /* AHB 时钟一分频*/
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4; /* APB 时钟一分频 */
HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_0);
}
主频是24M,AHP四分频=6M,APB四分频=1.5M.所有挂载在APB上的外设最高不超过1.5M.而ADC1就是挂载到APB总线上
//初始化IO口
py_adc_gpioconfig();
//adc配置
__HAL_RCC_ADC_CLK_ENABLE(); /* 使能 ADC 时钟 */
hadc.Instance = ADC1;
hadc.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV1; /* 设置adc时钟*/
hadc.Init.Resolution = ADC_RESOLUTION_12B; /* 12位 转换模式 */
hadc.Init.DataAlign = ADC_DATAALIGN_RIGHT; /* 数据右对齐 */
hadc.Init.ScanConvMode = ADC_SCAN_DIRECTION_FORWARD; /* 顺序扫描*/
hadc.Init.EOCSelection = ADC_EOC_SINGLE_CONV; /* 单次转换 */
hadc.Init.LowPowerAutoWait = ENABLE; /* 自动延迟转换功能禁用 */
hadc.Init.ContinuousConvMode = DISABLE; /* ENABLE:连续模式, DISABLE:单次模式 */
hadc.Init.DiscontinuousConvMode = DISABLE; /* 禁用不连续模式 */
hadc.Init.ExternalTrigConv = ADC_SOFTWARE_START; /* 软件触发 */
hadc.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE; /* 软件触发时,该参数无效 */
hadc.Init.Overrun = ADC_OVR_DATA_OVERWRITTEN; /* 在溢出的情况下,DR寄存器被最后一次转换结果覆盖 */
hadc.Init.SamplingTimeCommon = ADC_SAMPLETIME_239CYCLES_5; /* 通道采样时间为41.5 ADC时钟周期 */
if (HAL_ADC_Init(&hadc) != HAL_OK) /* ADC initialization */
{
printf("HAL_ADC_Init error1 \r\n");
return 0;
}
ADC的时钟设置1分频,精度12位,采样时间设置239.5个adc周期。
转换时间=(12.5+239.5)*(1/1.5M)=168US.转换一次adc需要168us,大于50us可以用。
问题4,最麻烦的,就是adc内部参考电压存在误差。
HAL_ADC_ConfigVrefBuf(&hadc,ADC_VREFBUF_1P5V);
设置为使用内部1.5V参考电压。
刚开始是直接使用的电压转换公式是 实际电压mV=(adc*1500)/4095;
然后发现计算的的电压比万用表量到的电压大十几mv,1000mV时大了十几mv,再加是这个是电池2:1分压后的,电池电压还需要*3,那电池电压就偏大了30-40mV,这有点夸张了。本来以为是adc采样精度或者是采样电路噪声过大问题,然后通过测量,使用了软件滤波后,电压跳动只有1-3mv的误差,而且还是比万用表测得的电压大十几个毫伏。然后用一样的代码,放到一个最小系统板上测试,反向误差小了一点。那么应该就是板子环境影响的误差。
终于在数据手册上找到了ADC的内部参考电压不是1500MV固定不变,是有误差的。
那么上面的计算公式存在有问题。
HAL_ADC_Start(&hadc); /* Start ADC Conversion */
HAL_ADC_PollForConversion(&hadc, 100000); /* Polling for ADC Conversion */
adc_value = HAL_ADC_GetValue(&hadc); /* Get ADC Value */
VrefBuf_Value = (4095 * 1200) / adc_value; /* Calculate the data */
adc_vref_1500_calibrate = VrefBuf_Value;
/* print VrefBuf value */
printf("VrefBuf:%dmV \r\n",VrefBuf_Value);
return VrefBuf_Value;
通过检测内部通道的参考电压通道VREFINT的电压值,我这块板计算得到1491和1500小了9MV,怪不得大那么多。修改公式 实际电压mV=(adc*1491)/4095;
得到的电压比原来的小了很多,但是电压值还是存在偏大。
而且这个偏大是程线性的,就是电压越大的时候,adc误差越大。查了一些资料,说的是使用内部参考电压时,会存在一个权重的问题,那么就需要软件上做补偿。或许也可能是温漂的问题。通过软件补偿后电压目前控制在5mV以内,还没做环境测试,不清楚误差是否会增大,不过这个误差目前够项目使用了。
这种模式只适合低成本,低功耗,对adc精度和实时性要求不高的时候使用。主要后面还有没有问题,还需要继续开发测试。
作者:芯芯点灯