单片机-STM32部分:11、ADC
飞书文档https://x509p6c8to.feishu.cn/wiki/OclUwlkifiRKR2k6iLbczn5tn8g
STM32的ADC是一种逐次逼近型模拟数字转换器。
是用于将模拟形式的连续信号转换为数字形式的离散信号的一类设备。
逐次逼近型ADC的原理图下:
STM32f103系列有3个ADC,精度为12位,每个ADC最多有16个外部通道。
其中ADC1有两个内部通道,温度传感器和通道ADC1_IN16相连接,内部参照电压VREFINT和ADC1_IN17相连接。
其中ADC1和ADC2都有16个外部通道,ADC3一般有8个外部通道,各通道的A/D转换可以单次、连续、扫描或间断执行,ADC转换的结果可以左对齐或右对齐储存在16位数据寄存器中。ADC的输入时钟不得超过14MHz,其时钟频率由PCLK2分频产生。
通道 | ADC1 | ADC2 | ADC3 | |
通道0 | IN0 | PA0 | PA0 | PA0 |
通道1 | IN1 | PA1 | PA1 | PA1 |
通道2 | IN2 | PA2 | PA2 | PA2 |
通道3 | IN3 | PA3 | PA3 | PA3 |
通道4 | IN4 | PA4 | PA4 | |
通道5 | IN5 | PA5 | PA5 | |
通道6 | IN6 | PA6 | PA6 | |
通道7 | IN7 | PA7 | PA7 | |
通道8 | IN8 | PB0 | PB0 | |
通道9 | IN9 | PB1 | PB1 | |
通道10 | IN10 | PC0 | PC0 | PC0 |
通道11 | IN11 | PC1 | PC1 | PC1 |
通道12 | IN12 | PC2 | PC2 | PC2 |
通道13 | IN13 | PC3 | PC3 | PC3 |
通道14 | IN14 | PC4 | PC4 | |
通道15 | IN15 | PC5 | PC5 | |
内部通道16 | IN16 | 连接内部温度传感器 | ||
内部通道17 | IN17 | 连接内部VRF |
ADC框图说明
电压输入范围
ADC所能测量的电压范围就是VREF- ≤ VIN ≤ VREF+,原理图中把 VSSA 和 VREF-接地,把 VREF+和 VDDA 接 3V3,得到ADC 的输入电压范围为: 0~3.3V。
ADC输入通道
从ADCx_INT0-ADCx_INT15 对应三个ADC的16个外部通道,此外,还有两个内部通道:温度检测或者内部电压检测,选择对应通道之后,便会选择对应GPIO引脚。
规则通道,注入通道
我们看到,在选择了ADC的相关通道引脚之后,在模拟至数字转换器中有两个通道,规则通道,注入通道,
规则通道至多16个,注入通道至多4个,一般使用规则通道。
规则通道就是普通的通道,注入通道就是“插队”通道,是优先级高的通道,如果规则通道遇到注入通道插队,就必须先执行完注入通道的数据转换,再进行规则通道的数据转换,这点类似中断优先级的概念。
假如你在家里的院子内放了5个温度探头,室内放了3个温度探头;你需要时刻监视室外温度即可,但偶尔你想看看室内的温度;因此你可以使用规则通道组循环扫描室外的5个探头并显示AD转换结果,当你想看室内温度时,通过一个按钮启动注入转换组(3个室内探头)并暂时显示室内温度,当你放开这个按钮后,系统又会回到规则通道组继续检测室外温度。 |
ADC时钟
ADC预分频器的ADCCLK是ADC模块的时钟来源。通常,由时钟控制器提供的ADCCLK时钟和PCLK2(APB2时钟)同步。RCC控制器为ADC时钟提供一个专用的可编程预分频器,分频因子由RCC_CFGR的ADCPRE[1:0]配置,可配置2/4/6/8分频,但是为了保证ADC转换结果的准确性,ADC的输入时钟不得超过14MHz
外部触发转换
支持多种外部事件触发,比如定时器捕捉、EXTI线,同时ADC3的触发源与ADC1/2的触发源有所不同,上图已经给出。
DMA
同时ADC还支持DMA触发,规则和注入通道转换结束后会产生DMA请求,用于将转换好的数据传输到内存。
注意,只有ADC1和ADC3可以产生DMA请求
中断
可以配置产生中断,在ADC转换结束时,读取ADC数据
开发板中的ADC电路说明
开发板使用PA0做ADC采集实验,PA0外部接了一个滑动电阻器,R92为保护电阻,当我们滑动电阻器时,PA0上的电压会在0-3.3V变化,而PA0是绑定到ADC的通道0上的,所以我们可以采集ADC1通道0的值,来计算出PA0的电压。
| |
STM32CUBEMX ADC参数说明
下面,我们使用STM32CubeMX配置ADC1的0通道,采集数据。
1、ADCs_Common_Settings ADC通用设置
模式设置
Mode ADC_Mode_Independent 这里设置为独立模式
如果不需要ADC同步或者只是用了一个ADC的时候,应该设成独立模式。
如果需要多个ADC同时使用时,根据采样的需求,可设置双重ADC同步模式,两个ADC采集一个或多个通道,可以提高采样率。
独立模式,常用 |
Data Alignment (数据对齐方式): 右对齐/左对齐
因为ADC得到的数据是12位精度的,但是数据存储在 16 位数据寄存器中,所以ADC的存储结果可以分为左对齐或右对齐方式(12位)
Scan Conversion Mode( 扫描模式 ) : DISABLE
是否开启扫描模式(针对多通道的设置),如果只是用了一个通道的话,DISABLE就可以了(也只能DISABLE),如果使用了多个通道的话,会自动设置为ENABLE。
注意,这里设置多通道后,需要修改Number of Conversion为对应通道数量才生效,否则不能设置为EBABLE |
Continuous Conversion Mode(连续转换模式) : ENABLE
是否开启连续转换模式,设置为ENABLE,即连续转换。如果设置为DISABLE,则是单次转换。
ADC单通道转换: |
Discontinuous Conversion Mode(间断模式) :DISABLE
因为我们只用到了1个ADC,所以这个直接不使能即可,只能在扫描模式为EBABLE时才能设置。
间断模式可以把多个通道的ADC转换分组,间断采集。
比方,我们用到某ADC模块的CH1/CH2/CH3/CH4/CH5五个通道,将它们分成3组,使用定时器触发ADC。第一次触发时,进行CH1/CH2两个通道的AD转换,第二次触发时进行CH3/CH4两个通道的AD转换,第三次触发时,完成CH5通道的AD转换。 第四次触发时进行跟第一次触发一样的转换,这样循环下去。
2、ADC Regular Conversion Mode ADC常规转换模式
Enable Regular Conversions (启用常规转换模式) ENABLE
需要开启常规转换模式后,才可以进行下方配置
Number OF Conversion(转换通道数) 1
用到几个通道就设置为几,如果设置多个通道会自动使能扫描模式
Extenal Trigger Conversion Source (外部触发转换源)
设定ADC的触发方式 |
Rank 转换顺序
多个通道时会有多个Rank,可以设定每个通道的转换顺序,采样时间
补充:采样时间越长,越准确,这里存在一个竞争冒险的关系。
ADC总转换时间如下计算: |
注入通道设置
也就是注入通道的设置,和转换通道没啥太大区别。注入通道一般使用外部中断、定时器中断作为触发源。
Injected Conversion Mode |
WahchDog
Enable Analog WatchDog Mode(使能模拟看门狗中断)
例如,我们想在芯片突然掉电时,保存一些必要数据。这时候我们就可以设置模拟看门狗中断,例如检测通道0的边界电压低于1500或者3500时,触发中断,这时候我们可以在ADC中断中做数据保存操作,存储必要数据。
ADC轮询采样
设置完成后,如果提示时钟设置错误,需要手动修改降低频率
同时,为了打印ADC值,我们可以添加串口打印函数
main.c/* USER CODE BEGIN Includes */#include <stdio.h>/* USER CODE END Includes *//* USER CODE BEGIN 2 */HAL_ADCEx_Calibration_Start(&hadc1); //ADC校准HAL_ADC_Start(&hadc1); //启动ADC转换,设置为连续采样后,只需开启一次/* USER CODE END 2 */while (1){/* USER CODE END WHILE *//* USER CODE BEGIN 3 */uint16_t ADC_Value;HAL_ADC_PollForConversion(&hadc1,50);//查询函数,查询转换完成标志位。每次采样,CUP在这里都要//等待采样完成才能进行下一步,超时时间可设置大于采样时间ADC_Value = HAL_ADC_GetValue(&hadc1); //获取AD值printf("ADC1 Reading : %d \r\n",ADC_Value);//12位ADC,2的12次方,是4096,分辨率为3.3/4096printf("PA1 True Voltage value : %.4f \r\n",ADC_Value*3.3f/4096);HAL_Delay(1000);//HAL_ADC_Stop(&hadc1);}/* USER CODE END 3 *//* USER CODE BEGIN 4 */
int fputc(int ch, FILE *f)
{HAL_UART_Transmit(&huart1 , (uint8_t *)&ch, 1, 0xFFFF);return ch;
}
/* USER CODE END 4 */
实验结果
参考飞书文档
关于ADC校准
HAL_ADCEx_Calibration_Start(&hadc1); //ADC校准 |
ADC转换结束中断
因为使用上方的轮询方式效率太低太慢了,这时候我们可以开启ADC转换结束中断,可以提升采样效率。
void ADC1_2_IRQHandler(void)
{/* USER CODE BEGIN ADC1_2_IRQn 0 *//* USER CODE END ADC1_2_IRQn 0 */HAL_ADC_IRQHandler(&hadc1);/* USER CODE BEGIN ADC1_2_IRQn 1 *//* USER CODE END ADC1_2_IRQn 1 */
}void HAL_ADC_IRQHandler(ADC_HandleTypeDef* hadc){xxxHAL_ADC_ConvCpltCallback(hadc);xxx
}/*** @brief Conversion complete callback in non blocking mode* @param hadc: ADC handle* @retval None*/
__weak void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{/* Prevent unused argument(s) compilation warning */UNUSED(hadc);/* NOTE : This function should not be modified. When the callback is needed,function HAL_ADC_ConvCpltCallback must be implemented in the user file.*/
}
如果使能了ADC转换结束中断,可以这样写:
main.cuint16_t ADC_Value;/* USER CODE BEGIN 2 */
HAL_ADCEx_Calibration_Start(&hadc1);while (1)
{/* USER CODE BEGIN 3 */HAL_ADC_Start_IT(&hadc1); //每秒转换一次,是否可以反正while外,只初始化一次HAL_Delay(1000);
}
/* USER CODE END 3 *//* USER CODE BEGIN 4 */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) //ADC转换完成回调
{HAL_ADC_Stop_IT(&hadc1); //关闭ADCADC_Value=HAL_ADC_GetValue(&hadc1); //获取ADC转换的值printf("ADC1 Reading : %d \r\n",ADC_Value);printf("%.4f V\r\n",(ADC_Value*3.3/4096)); //串口打印电压信息
}
ADC DMA采样
如果采样频率太高,而且多通道同时采样,为了减少对CPU的占用,我们可以使用DMA采样的方式,这样可以大大提高采样效率。
注意DMA采样一般只用在多通道采样的场景,单通道时,直接用ADC中断采样即可。 |
- Mode 模式:独立采集
- Scan Conversion Mode 扫描模式 :ENABLE
- Continuous Conversion Mode 连续转换模式:ENABLE
- 两路ADC,分别是通道0和通道1
采样周期选择:采样周期越短,ADC 转换数据输出周期就越短但数据精度也越低,采样周期越长,ADC 转换数据输出周期就越长同时数据精度越高。 |
- 配置下DMA模式为Circular,既循环更新数据,否则默认的Normal模式触发后只执行 一次,
- 配置自增地址为Memory方式,并选择word或half word,这里选择word,是因为我程序里定义uint32_t 的数组来存储多路ADC数据的,占用四个字节既选择word
main.c/* USER CODE BEGIN 0 */uint32_t ADC1_Value_DMA[2];/* USER CODE BEGIN 2 */HAL_ADCEx_Calibration_Start(&hadc1);HAL_ADC_Start_DMA(&hadc1, ADC1_Value_DMA, 2);while (1){/* USER CODE BEGIN 3 */float Channel_0 = (float)(ADC1_Value_DMA[0]&0xFFF)*3.3/4096;float Channel_1 = (float)(ADC1_Value_DMA[1]&0xFFF)*3.3/4096;printf("ADC1 Channel_6 Count: %d, voltage: %f \r\n",ADC1_Value_DMA[0],Channel_0);printf("ADC1 Channel_7 Count: %d, voltage: %f \r\n",ADC1_Value_DMA[1], Channel_1);}/* USER CODE END 3 */
记得把DMA放到ADC前面初始化哦