无论是新手还是大佬,基于STM32单片机的开发,使用STM32CubeMX都是可以极大提升开发效率的,并且其界面化的开发,也大大降低了新手对STM32单片机的开发门槛。
本文主要讲述STM32芯片的ADC的配置及其相关知识。
STM32CubeMX是ST官方出的一款针对ST的MCU/MPU跨平台的图形化工具,支持在Linux、MacOS、Window系统下开发,其对接的底层接口是HAL库,另外习惯于寄存器开发的同学们,也可以使用LL库。STM32CubeMX除了集成MCU/MPU的硬件抽象层,另外还集成了像RTOS,文件系统,USB,网络,显示,嵌入式AI等中间件,这样开发者就能够很轻松的完成MCU/MPU的底层驱动的配置,留出更多精力开发上层功能逻辑,能够更进一步提高了嵌入式开发效率。
演示版本 6.7.0
ADC是普通攻击持续输出核心的简称,是……不好意思,拿错剧本了。ADC(Analog-to-Digital Converter的缩写),指模/数转换器或者模数转换器。是指将连续变化的模拟信号转换为离散的数字信号的器件。作为单片机最常用的一种外设之一,我们先来看下普通ADC的工作原理。
然后再来看下ST里ADC这个外设有什么特别之处(不同单片机ADC的配置界面及框图可能会有所不同,本文以STM32F072RB为例,简述下最基本的配置)。可以看到这里除了ADC核心转换部分,还有外围一堆设置项。接下来我们就按这个框图来讲解一下ST里的ADC功能。(其中核心转换部分就跟上面讲的工作原理差不多,这部分跟配置使用相关性不大,就不再细讲,主要看配置部分)
以此图为例,其ADC采样通道数有19个,包括0到11和15到18这16个普通外部ADC采样通道(ADC_IN),一路内部温度检测通道(VSENSE),一路参考电压采样通道(VREFINT),一路备份电源采样通道(VBAT)。因为ADC转换器同一时间只能转换一个通道的数据,所以输入部分的配置主要是选择需要转换的通道,由CH_SEL决定。
这里可能有人就有疑问了,这个单片机只有一个ADC转换器,如果我要同时采两个或者两个以上的数据时,是不是就不能用这个单片机了?
是的,但这只限于真正意义上的同时采样,也就是要求完全同一时刻采样的时候,这个才无法满足。而生活中大部分应用情况,只需要分时采样即可,就像我们用的电脑,电脑看似同时跑了很多个应用,但实际操作系统也是分时处理这些应用的,只是因为切换速度很快,人一般感知不到,所以看起来就像是同时在运行。回到ADC这里也一样,只要我们快速切换通道采样,那看起来也跟同时采一样。
快速切换通道采样,我们一般称之为扫描(Scan),所以这里有这么一个配置项,扫描方向(SCANDIR),就是采样顺序是从前到后还是从后到前。一般对时序要求不高的场景,比如采几个温度值用来显示用的,可以不用关注这个顺序,但对时序采样有要示的场景就需要关注这个顺序了。
触发源,就是选择使用什么机制来启动ADC采集,ST支持两大类的触发机制,一种是常用的,由软件触发,设置ADSTART和ADSTP用来启动和停止ADC转换。另一种则是硬件触发,可配置触发源为定时器或外部中断。
虽然图中看着功能比较多,但无非就两大类,一是硬件用的,给到模拟看门狗作为触发源使用;二是软件用的,可通过中断知会用户获取使用,也可以通过DMA进行自动传输。
根据以上原理我们知道,采集ADC值是需要有个电压值与采集值作对比的,那么这个用于参考比较的电压值从哪来呢?答案就在这里,单片机的VREF+接口。所以这里可以得出一个电压的计算公式,其中VREF+可以默认按实际接入的参考电压直接代入:
V采集值 = (ADC原始值 / 2ADC位数) * VREF+
这时候可能有人会有疑问,为什么不直接用VCC单片机电源就行,而要浪费一个IO口来作为参考源输入呢?因为单片机虽然说一般是3.3V供电,但实际单片机可支持的电压是有一定范围的,所以一般电源设计用于单片机供电的电压并不会很精确,如果直接用这个作为参考源,会导致ADC计算的误差偏大。
比如下图A点时刻,实际VREF+是3.33V,而公式使用的是固定3.3V进行计算,这样就会导致计算的结果偏小。而B点时刻则是相反,计算结果偏大。
那另外有人会说了,前面不是说了参考源电压也可以采集么,那我用采集的参考源电压代入公式来计算,不就可以消除这个偏差了么?确实可以,但回到前面讲的ST芯片中ADC的采样原理,虽说可以多通道采集,但实际是多通道分时采集的数据,也就是说采集的参考源电压值与采集的通道电压值并不是同一时刻的值,如果参考源电压波动较为频繁,则还是存在检测偏差。
以下图为例,如果先采集的是参考源电压,那先采集到A点的值,然后经过一个采样周期后,再采集输入电压值,即D点的值,将这两个值代入公式中,计算出来的结果是偏大的。反之如果先采的是输入电压再采参考电压,则是采了B、C点的值,代入公式结果则是偏小。
综上所述,这里推荐使用LDO输出的稳压源作为参考电压源。
因为芯片做工的问题,ADC外设模块多多少少存在一定的差异常性,所以ST芯片里的ADC就提供了一个用于消除工艺或其他因素导致的芯片间检测差异的功能——校准。通过启动校准功能,ADC外设自身可以计算出一个校准值,用于消除零点偏差。ST官方是建议每次使能ADC时都需要校准一次。
ADC的通道循环采集的方式有两种,一种是配置成规则通道,一种是配置成注入通道。第一次听到这两个名词可能会比较陌生,这里举个例子可能就比较清晰。
比如现在配置1、2、3、4通道为规则通道,那么当启动采集后,ADC会按照1、2、3、4的顺序依次采集,如果配置了循环采集,则在采集完第4通道后自动启动下一个周期的采集。
如果把1、2、3、4通道配置为注入通道,此时则需要一个触发启动源,触发源触发一个采集事件时,立即进行一次采集。
就上面两个例子看下来,感觉像什么?是不是跟单片机的代码结构一样,规则通道就是主循环里一直跑的代码,而注入通道则是中断里执行的代码,中断可以随时打断主循环里的内容。不过虽说是立即打断规则通道的采集,但实际也是要等规则通道当前一次通道的值采样完成后才会进行注入通道的采样,就像主循环被中断打断,其实也是要等主循环里的某一条语句执行完成才可以切换。
这一章节标星是因为本文不涉及规则通道及注入通道的配置,只是简述下这两者的概念。
先选择端口,如果是内部ADC,如温度、参考电压、备份电压,则直接在左边Analog选项卡中选中ADC,在菜单栏中勾选需要检测的电压源。
如果是外部端口,建议先从端口视图中找到对应需要检测的端口,选择ADC通道及GPIO的Analog复用功能。
这一次我们只选择内部温度检测进行后面的操作。
时钟分频(Clock Prescaler): 主时钟给到ADC后,可根据不同需要设置分频系数,分频越低,能耗越低,当然对应的转换时间也会更长。
分辨率(Resolution): ADC位数不同,其采样精度就不一样,以3.3V参考源为例,6位的ADC,其转换电压的颗粒度是3.3/26,而12位的ADC,转换电压的颗粒度则是3.3/212。所以分辨率越高,其采样的精度也就越高,当然代价就是转换时间更长,能耗更高。
数据对齐方式(Data Alignment): 可以选择采样后的数据左对齐存放或右对齐存放。以12位ADC为例,比如采样出0x0123的值,当设置为右对齐时,ADC的DR寄存器中存放的值就是0x0123,设置成左对齐时,则是0x1230(原始数据左移(16 - ADC位数))。这里可能有人会有疑问,用右对齐就可以直接得到原始值,那左对齐有什么用?一开始我也觉得左对齐没用,但后面看到ST官方电机库里的换算实现才知道这个的好处。
回想下上面的电压换算公式,右对齐时,需要使用下面公式,这里需要知道分辨率的设置。
V = Data / 2分辨率 * VREF+
而数据左对齐时,只需要使用公式:
V = Data / 0xFFFF * VREF+
扫描方向(Scan Conversion Mode): 可选择从0到18向后扫描,也可以选择从18到0向前扫描。这个单片机只能支持这两个顺序扫描,F1之后的单片机,是支持自己设置扫描顺序的。
连续转换模式(Continuous Conversion Mode)和非连续转换模式(Discontinuous Conversion Mode): 设置成连续转换模式时,在一个组的ADC转换结束后,可以继续启动转换,不需要触发源介入。禁止时,则是在一个组转换结束后即停止转换,需要等到下个触发启动才开始转换。这两个我一直想吐槽,为什么非得分成两个配置项,外设也是有两个单独的寄存器进行设置。但这两个寄存器实则是互斥的关系,即开启了连续转换模式,就不能开启非连续转换模式;开启了非连续转换模式,就不能开启连续转换模式。
DMA传输模式(DMA Continuous Requests): 开启此功能可召唤DMA当搬运工帮你搬运数据,具体还需要配置DMA相关的参数,这部分本文就不作介绍,在单独的ADC+DMA篇章中细说。
结束方式选择(End Of Conversion Selection):
数据覆盖方式(Overrun Behaviour): 当ADC的DR寄存器里已经存有上次转换完的数据,并且未读取时,又有一次ADC通道转换完成时,可通过此选项,选择是保持之前的数据,还是用新的数据覆盖之前的数据。
低电压自动等待(Low Power Auto Wait): 低电压时不进行转换,等待电压恢复。
低电压自动断电(Low Power Auto Power Off): 低电压时断ADC外设的电,电压恢复后需要手动给ADC上电。
转换时间(Sampling Time): 前面原理讲到的,ADC转换需要一定的时间,而这个时间跟外部电路也有一定的关系,所以这个时间是根据外部电路可以做灵活调整,一般来讲时间设置越长,检测的准确性也就越高。如果对时序要求不高的场合,设置成最大的周期数即可。
外部触发源(External Trigger Conversion Source): 一般默认是软件触发,即需要在代码里调用启动转换的接口。另外可以选择是各种定时器事件触发。
外部触发方式(External Trigger Conversion Edge): 当选择了定时器事件触发后,这个选项可以选择是由定时器的上升沿、下降沿或边沿触发采样。
模拟看门狗使能(Enable Analog Watchdog Mode): 相当于一个比较器,当检测到的电压超过设定阀值时,可以触发一个中断。
下限阀值(Low Threshold): 设置触发中断的下限阀值。
查看手册说明,采集到内部温度通道的ADC值后,可以使用以下的公式换算出当前温度值。
其中TS_CAL1和TS_CAL2分别是30℃和110℃温度下对应的ADC采集值,但这个值哪来呢?别担心,这个是ST芯片出厂就给你内置好的一个值,在数据手册里有说明,只要获取这个地址的值即可。ST库里也提供有相关的宏。
然后工程按如下配置,不考虑功耗的情况,我们只需要让其一直采样即可,不需要开启中断,只要用到数据的时候再去取就行。
配置完成后,生成工程代码,在工程里添加如下代码:
#define TEMP110_CAL_ADDR ((uint16_t*) ((uint32_t) 0x1FFFF7C2))
#define TEMP30_CAL_ADDR ((uint16_t*) ((uint32_t) 0x1FFFF7B8))
float Temp = 0;
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_ADC_Init();
/* USER CODE BEGIN 2 */
/* HAL库实现 */
HAL_ADC_Start(&hadc);
#if 0
/* LL库实现 */
LL_ADC_Enable(ADC1);
LL_ADC_REG_StartConversion(ADC1);
#endif
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* HAL库实现 */
int32_t adc_data = HAL_ADC_GetValue(&hadc);
Temp = (110 - 30) * ((float)adc_data - *TEMP30_CAL_ADDR)
/ (*TEMP110_CAL_ADDR - *TEMP30_CAL_ADDR)
+ 30;
/* LL库实现 */
#if 0
int32_t adc_data = LL_ADC_REG_ReadConversionData12(ADC1);
Temp = (110 - 30) * ((float)adc_data - *TEMP30_CAL_ADDR)
/ (*TEMP110_CAL_ADDR - *TEMP30_CAL_ADDR)
+ 30;
#endif
}
}
编译烧录进入调试。
很奇怪在空调房里都检出50℃出来,不知道是不是板子吃灰太久有点问题。换个最新拿到的C0板子试了下,公式不大一样。
同样的按照公式编写代码,烧录并调试。
#define TEMP30_CAL_ADDR ((uint16_t*) ((uint32_t) 0x1FFF7568))
float Temp = 0;
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_ADC1_Init();
/* USER CODE BEGIN 2 */
HAL_ADC_Start(&hadc1);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
int32_t adc_data = HAL_ADC_GetValue(&hadc1);
/* 因为这里参考电压是3.3V,所以不用公式里乘Vdd除3的操作 */
Temp = (((float)adc_data) - *TEMP30_CAL_ADDR) / (2.53 * 4096 / 3300) + 30;
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
1、使用ADC+DMA的时候,留意下自动生成的代码中ADC和DMA的初始化顺序,之前旧版本生成的代码是有Bug的,因为ADC和DMA的初始化的调用顺序不对导致多个通道采集ADC值时会有异常。
2、F0没办法配置多通道的扫描顺序,只能按从0到18或从18到0的顺序进行采集。