STM32学习记录(七):ADC
模拟/数字转换器(Analog-to-digital converter:ADC)将模拟量转为数字量。STM32F103C8T6中的有2个12bit转换时间为1us的A/D转换器,内置了一个温度传感器,可以通过ADC读取。
ADC的系统框图
ADC读取温度传感器
STM32内部有一个温度传感器,只有使用ADC1时,内部温度传感器才是可用的。
使用单片机STM32F103C8T6的ADC1读取内部温度传感器的步骤:
- 配置ADC1的时钟、初始化结构体等
- 配置规则组或注入组的采样通道和采样时间,采样时间应在17.1us
- 使能温度传感器和内部参考电压通道
- 开启ADC1、重置ADC校准器、开始转换
- 读取数据寄存器的16位或者32位(仅在ADC1的双倍模式下)的值
- 将读取的值使用公式转换为温度:\((V_{25}-V_{SENCE})/AVG\_Slope+25\),其中\(V_{25}\)是25摄氏度时的\(V_{SENCE}\)值,查阅数据手册第5章《Electrical characteristics》,可得典型值\(V_{25}\) =1.43V、\(AVG\_Slope\) = 4.3mv/℃
STM32F103C8T6 ADC的模拟输入通道与GPIO的对应关系如下,共有10个可用的模拟输入通道。ADC1和ADC2的模拟输入通道是共用的。
ADC1/ADC2 | GPIO |
---|---|
ADC12_IN0 | PA0 |
ADC12_IN1 | PA1 |
ADC12_IN2 | PA2 |
ADC12_IN3 | PA3 |
ADC12_IN4 | PA4 |
ADC12_IN5 | PA5 |
ADC12_IN6 | PA6 |
ADC12_IN7 | PA7 |
ADC12_IN8 | PB0 |
ADC12_IN9 | PB1 |
ADC1的通道16与内部温度传感器相连,ADC1的通道17与内部参考电压\(V_{REFER}\)相连。
ADC的时钟设置
PCLK2默认等于SYSCLK,也就是72MHz,而ADC最大频率为14MHz。查阅数据手册中的表格 "Table 47. RAIN max for fADC = 14 MHz"可知,在ADC时钟为14MHz、采样时间17.1us对应的采样时间(时钟周期为单位)为239.5个时钟周期。ADC要得到14MHz的时钟,HCLK的时钟应设置为56MHz,APB2预分频器再进行二分频可得14MHz的ADC时钟频率。HAL库编程时,使用STM32CubeMX很容易配置时钟。在STM32CubeMX的时钟配置中更加直观。
那么使用标准库时如何将系统时钟SYSCLK修改为56MHz?参照另一篇博客:STM32修改系统时钟频率
主要程序
ADC_InitTypeDef结构体的配置需查阅固件库函数手册和参考手册。
初始化ADC
因为温度传感器是芯片内部的传感器,温度传感器已经与ADC的通道16连接,内部参考电压\(V_{REFINT}\)已经与ADC的通道17连接。注意ADC必须要开启校准,否则得到的结果是不正确的。
ADC工作模式:ADC工作模式分独立模式(independent mode)和双ADC模式(dual mode)。 见参考手册11.9 Dual ADC mode
- 独立模式:每个ADC单独工作
- 双ADC模式:设备有2个或更多ADC的时候才能使用。双ADC模式包括以下模式,以下模式还可以进行组合
- 同步注入模式
- 同步规则模式
- 快速交错模式
- Slow interleaved mode
- 交替触发模式
持续转换模式:开启后,持续进行数据转换
扫描模式:此模式用于扫描一组模拟通道。对于多通道,需要开启扫描模式;单通道无需开启。
数据右对齐:转换之后数据的右对齐。对于规则组而言,数据右对齐就是12bit数据从低位到高位的顺序存放在16位的内存或者寄存器中,前高4位补0。见参考手册Figure 27. Right alignment of data。其余各个取值参照参考手册。
外部触发:转换可以由外部事件触发(例如定时器捕获、EXTI线)。事件(event)和中断(interrupt)是不同的,外部中断线配置为事件模式时,中断请求寄存器是不会被置位的,仅仅是通过脉冲发生器产生一个脉冲。
void ADC_Configuration(void)
{/* 开启GPIO、ADC1时钟 */RCC_ADCCLKConfig(RCC_PCLK2_Div2); //PCLK2进行2分频:28 / 2 = 14MHzRCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);/* 配置ADC */ADC_InitTypeDef ADC_InitStructure;ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //独立工作模式ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; //开启持续转换模式ADC_InitStructure.ADC_ScanConvMode = DISABLE; //关闭多通道扫描模式,只有1个通道ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //数据对齐方式:右对齐ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //无外部触发,由软件触发ADC_InitStructure.ADC_NbrOfChannel = 1; //1个ADC通道ADC_Init(ADC1, &ADC_InitStructure);/* Configures ADC1 Channel16 as: first converted channel with an 239 cycles sample time */ ADC_RegularChannelConfig(ADC1, ADC_Channel_16, 1, ADC_SampleTime_239Cycles5); //采样时间239.5/14MHz = 17.1us/* 使能温度传感器和内部参考电压通道 */ ADC_TempSensorVrefintCmd(ENABLE); /* 开启ADC */ADC_Cmd(ADC1, ENABLE);/* 重置ADC校准寄存器 */ADC_ResetCalibration(ADC1);/* 检查复位ADC校准是否结束 */while(ADC_GetResetCalibrationStatus(ADC1));/* 开启ADC校准 */ADC_StartCalibration(ADC1);/* 检查校准是否结束 */while(ADC_GetCalibrationStatus(ADC1));/* 开始ADC软件转换 */ADC_SoftwareStartConvCmd(ADC1,ENABLE);
}
读取数据
可以选择读取多次的数据,然后求平均值,这里演示没有多次读取并求平均值。
int main()
{COM_Init();ADC_Configuration();while(1){showTemperature();delay_ms(1000);}
}void showTemperature(void)
{/*Returns the ADC1 Master data value of the last converted channel*/ uint16_t DataValue = 0; DataValue = ADC_GetConversionValue(ADC1);// ADC精度:(VDDA/4096),VDDA是ADC供电电压,使用的是3.3v// VSENSE = DataValue*(3.3/4096);// Temperature (in °C) = {(V25 - VSENSE ) / Avg_Slope} + 25.// v25典型值1.43V Avg_Slope 4.3mv/°c VSENSE=DataValue//精度Vdda/4096double temperature;double VSENSE = DataValue*(3.3/4096);temperature = (1.43 - VSENSE) / (0.0043) + 25;printf("VSENSE: %.2f, temperature: %.2f",VSENSE, temperature); //向串口发送消息
}
演示结果
以下是没有加ADC校准得到的错误结果。正确的演示结果参照后面的演示结果。
ADC使用DMA多通道读取
什么是DMA?引用参考手册第13章:
Direct memory access (DMA) is used in order to provide high-speed data transfer between peripherals and memory as well as memory to memory. Data can be quickly moved by DMA without any CPU actions. This keeps CPU resources free for other operations.
DMA用于外设和内存之间的高速数据传输,数据通过DMA能够没有在CPU的参与下进行快速传输,DMA减轻了CPU的压力。STM32F103C8T6有一个7通道的DMA控制器,支持的外设包括ADC、SPI、I2C、USART。
ADC1在独立工作模式下的数据寄存器只有16位,对于ADC的多通道采样有两种方式:
- 不使用DMA。ADC应当被配置为:单次转换、关闭扫描模式、采样通道为1。通过软件设置采样通道来读取不同通道的数据
- 使用DMA。ADC将多个通道按照设置的采样顺序,将数据经过DMA的传输,传输到一个用户设置的缓冲区,只需读取这个缓冲区的值就可以获得每个通道的采样数据
这里介绍使用DMA的方式,ADC1的通道0、通道7、通道16这3个通道完成,连线:
- 寻迹传感器(反射式光电传感器)AO引脚接PA0
- 光敏电阻传感器接AO口接PA7
- STM32内部温度传感器与通道16已经相连无需连接GPIO。
在这里传感器不做介绍。
主要程序
初始化以及配置DMA1
__IO uint16_t ADCConvertedValue[3]; //缓冲区,存放DMA将读取到的数据void DMA_Configuration(void)
{/* 开启DMA时钟 */RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);/* 反初始化DMA1通道1 */DMA_DeInit(DMA1_Channel1);/* 配置DMA初始化结构体 */DMA_InitTypeDef DMA_InitStructure;DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&(ADC1->DR); //配置外设基地址,这里是ADC1数据寄存器的地址DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; //外设数据大小,16位DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)ADCConvertedValue; //配置内存基地址,这里是存数据的内存地址DMA_InitStructure.DMA_BufferSize = 3; //缓存区中数据的个数DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //外设作为数据传输的来源 DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //关闭DMA的内存到内存模式DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; //内存数据大小,16位DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Disable; //内存地址递增,缓冲区要存放3个数据DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; //循环缓存模式,即多次读取数据到缓冲区DMA_InitStructure.DMA_Priority = DMA_Priority_High; //优先级高DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址不递增DMA_Init(DMA1_Channel1, &DMA_InitStructure); //初始化DMADMA_Cmd(DMA1_Channel1, ENABLE); //使能DMA
}
配置ADC
void ADC_Configuration(void)
{/* 开启GPIO、ADC1时钟 */RCC_ADCCLKConfig(RCC_PCLK2_Div2); //PCLK2进行2分频:28 / 2 = 14MHz = 0.083us RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);/* 配置ADC */ADC_InitTypeDef ADC_InitStructure;ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //独立工作模式ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; //开启持续转换模式ADC_InitStructure.ADC_ScanConvMode = ENABLE; //关闭多通道扫描模式,只有1个通道ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //数据对齐方式:右对齐ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //无外部触发,由软件触发ADC_InitStructure.ADC_NbrOfChannel = 3; //1个ADC通道ADC_Init(ADC1, &ADC_InitStructure);/* ADC通道按照0、7、16的顺序进行采样,采样时间为239.5个时钟周期 */ ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5); //采样时间0.083us * 239.5 = 17.1usADC_RegularChannelConfig(ADC1, ADC_Channel_7, 2, ADC_SampleTime_239Cycles5); //采样时间0.083us * 239.5 = 17.1usADC_RegularChannelConfig(ADC1, ADC_Channel_16, 3, ADC_SampleTime_239Cycles5); //采样时间0.083us * 239.5 = 17.1us/* 打开ADC内部温度传感器通道、参考电压通道 */ ADC_TempSensorVrefintCmd(ENABLE); /* 开始ADC1的DMA模式 */ADC_DMACmd(ADC1, ENABLE);/* ADC校准是固定流程,且是必须要的 *//* 开启ADC1 */ADC_Cmd(ADC1, ENABLE);/* 重置ADC校准寄存器 */ADC_ResetCalibration(ADC1);/* 检查复位ADC校准是否结束 */while(ADC_GetResetCalibrationStatus(ADC1));/* 开启ADC校准 */ADC_StartCalibration(ADC1);/* 检查校准是否结束 */while(ADC_GetCalibrationStatus(ADC1));/* ADC1开始转换 */ADC_SoftwareStartConvCmd(ADC1,ENABLE);
}
读取数据
将ADC1采样的数据读取到自行设置的缓冲区中,DMA会直接将ADC1的数据读取到地址为ADCConvertedValue的内存单元,不再使用ADC_GetConversionValue(ADC1)
读取ADC1的数据,DMA的存在减少了CPU的负担,CPU不再处理ADC1的读取操作,而是直接读从DMA读取出来的数据
int main(void)
{COM_Init(); //串口初始化GPIO_Configuration()DMA_Configuration(); ADC_Configuration();while (1){showData();delay_ms(3000); //每3s读取一次采样数据}
}void showData(void)
{double temperature;double VSENSE = (double)ADCConvertedValue[2]*(3.3/4096);temperature = (1.43 - VSENSE) / (0.0043) + 25;/* 分别打印出3个采样到的数据,下标为0:寻迹传感器的数据,1:光敏电阻传感器的数据 */printf("ch1: %d, ch2: %d, ch3: %.2f", ADCConvertedValue[0], ADCConvertedValue[1], temperature);
}void GPIO_Configuration()
{RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);/* 配置GPIO */GPIO_InitTypeDef GPIO_InitStructure;/* 配置通道7、通道0对应的GPIO口 */GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7 | GPIO_Pin_0;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure);
}
演示结果
手指按住STM32芯片,温度会明显上升。ch1是通道0采样的数据,ch2是通道7采样的数据,ch3是通道16采样的数据
双ADC模式:规则同步模式
举个例子说明双ADC模式下的规则同步模式(Regular simultaneous mode)。双ADC模式下每次转换的通道不能相同,比如ADC1使用通道1时,ADC2不要同时使用通道1进行转换。
参考手册11.9.2 Regular simultaneous mode写道:
At the end of conversion event on ADC1 or ADC2:
- A 32-bit DMA transfer request is generated (if DMA bit is set) which transfers to SRAM the ADC1_DR 32-bit register containing the ADC2 converted data in the upper halfword and the ADC1 converted data in the lower halfword.
- An EOC interrupt is generated (if enabled on one of the two ADC interfaces) when ADC1/ADC2 regular channels are all converted.
大致意思是说,当ADC转换完成时,一个32位的DMA传输请求就会生成,DMA将32位的数据传输值SRAM中。双ADC模式下,32位的数据寄存器ADC1_DR高16位存放的是ADC2转换后的数据,低16位存放的是ADC1转换后的数据
主要程序
大致流程图如下:
开启时钟、配置GPIO
void RCC_Configuration(void)
{/* 对PCLK2进行二分频 */RCC_ADCCLKConfig(RCC_PCLK2_Div2); //PCLK2进行2分频:28 / 2 = 14MHz = 0.083us /* 开启GPIOB、ADC1时钟 */RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC2, ENABLE);/* 开启DMA时钟 */RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
}void GPIO_Configuration(void)
{/* 配置GPIO */ GPIO_InitTypeDef GPIOInitStructure;GPIOInitStructure.GPIO_Mode = GPIO_Mode_AIN; //模拟输入GPIOInitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_7; //PA0:通道0,PA7:通道7GPIOInitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIOInitStructure);
}
初始化以及配置DMA1
__IO uint32_t ADCConvertedValue[2]; //缓存区,DMA将读取到的数据存放到其中void DMA_Configuration(void)
{/* 反初始化DMA1通道1 */DMA_DeInit(DMA1_Channel1);/* 配置DMA初始化结构体 */DMA_InitTypeDef DMA_InitStructure;DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&(ADC1->DR); //配置外设基地址,这里是ADC1数据寄存器的地址DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word; //外设数据大小,32位, 双ADC模式DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)ADCConvertedValue; //配置内存基地址,这里是数据存放的内存地址DMA_InitStructure.DMA_BufferSize = 2; //缓存区中数据的个数DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //外设作为数据传输的来源 DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //关闭DMA的内存到内存模式DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word; //内存数据大小,32位DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //内存地址需要递增,缓存区中数据个数>1DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; //循环缓存模式,即多次读取数据到缓冲区DMA_InitStructure.DMA_Priority = DMA_Priority_High; //优先级高DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址不递增DMA_Init(DMA1_Channel1, &DMA_InitStructure); //初始化DMADMA_Cmd(DMA1_Channel1, ENABLE); //使能DMA
}
配置ADC
void ADC_Configuration(void)
{/* 开启GPIO、ADC1时钟 */RCC_ADCCLKConfig(RCC_PCLK2_Div2); //PCLK2进行2分频:28 / 2 = 14MHz = 0.083us RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_ADC2, ENABLE);/* 配置ADC1 */ADC_InitTypeDef ADC_InitStructure;ADC_InitStructure.ADC_Mode = ADC_Mode_RegSimult; //常规同步模式Regular simultaneous modeADC_InitStructure.ADC_ContinuousConvMode = ENABLE; //开启持续转换模式ADC_InitStructure.ADC_ScanConvMode = ENABLE; //开启扫描转换模式ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //数据对齐方式:右对齐ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //无外部触发ADC_InitStructure.ADC_NbrOfChannel = 2; //2个ADC通道ADC_Init(ADC1, &ADC_InitStructure);/* 配置ADC1规则组通道 */ ADC_RegularChannelConfig(ADC1, ADC_Channel_16, 1, ADC_SampleTime_239Cycles5); //采样时间0.083us * 239.5 = 17.1usADC_RegularChannelConfig(ADC1, ADC_Channel_0, 2, ADC_SampleTime_239Cycles5); //采样时间0.083us * 239.5 = 17.1us/* 打开温度传感器通道和内部参考电压通道 */ ADC_TempSensorVrefintCmd(ENABLE); /* 开始ADC1的DMA模式 */ADC_DMACmd(ADC1, ENABLE);/* 配置ADC2 */ADC_InitStructure.ADC_Mode = ADC_Mode_RegSimult; //常规同步模式Regular simultaneous modeADC_InitStructure.ADC_ContinuousConvMode = ENABLE; //开启持续转换模式ADC_InitStructure.ADC_ScanConvMode = ENABLE; //开启扫描转换模式ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //数据对齐方式:右对齐ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //无外部触发ADC_InitStructure.ADC_NbrOfChannel = 1; //1个ADC通道ADC_Init(ADC2, &ADC_InitStructure);ADC_RegularChannelConfig(ADC2, ADC_Channel_7, 1, ADC_SampleTime_239Cycles5); //采样时间0.083us * 239.5 = 17.1us/* 开启ADC1 */ADC_Cmd(ADC1, ENABLE);/* 重置ADC校准寄存器 */ADC_ResetCalibration(ADC1);/* 检查复位ADC校准是否结束 */while(ADC_GetResetCalibrationStatus(ADC1));/* 开启ADC校准 */ADC_StartCalibration(ADC1);/* 检查校准是否结束 */while(ADC_GetCalibrationStatus(ADC1));/* 开启ADC2 */ADC_Cmd(ADC2, ENABLE);/* 重置ADC校准寄存器 */ADC_ResetCalibration(ADC2);/* 检查复位ADC校准是否结束 */while(ADC_GetResetCalibrationStatus(ADC2));/* 开启ADC校准 */ADC_StartCalibration(ADC2);/* 检查校准是否结束 */while(ADC_GetCalibrationStatus(ADC2));/* ADC1、ADC2开始转换 */ADC_SoftwareStartConvCmd(ADC1, ENABLE);ADC_SoftwareStartConvCmd(ADC2, ENABLE);/* 测试DMA1是否传输完成 */while(!DMA_GetFlagStatus(DMA1_FLAG_TC1));/* 清除DMA1传输完成标志位 */DMA_ClearFlag(DMA1_FLAG_TC1);
}
读取数据
在\(f_{ADC}=14MHz\)下,ADC的转换时间为1us,见参考手册中11章的内容:11.6 Channel-by-channel programmable sample time。因此无需判断标志位,直接读取结果即可。
__IO uint32_t ADCConvertedValue[2]; //缓存区,DMA将读取到的数据存放到其中int main()
{COM_Init(); //串口初始化,用于输出采样结果RCC_Configuration(); //时钟配置GPIO_Configuration(); //GPIO配置DMA_Configuration(); //DMA配置 ADC_Configuration(); //ADC配置while (1){parseData();delay_ms(1000); //delay 1s}
}void parseData(void)
{double temperature;uint16_t adc1_data, adc2_data, adc1_data2;/* 低16位是ADC1的数据,高16位是ADC2的数据 */adc1_data = ADCConvertedValue[0] & 0xffff;adc2_data = (ADCConvertedValue[0] >> 16) ;adc1_data2 = ADCConvertedValue[1] & 0xffff;double VSENSE = (double)adc1_data*(3.3/4096);temperature = (1.43 - VSENSE) / (0.0043) + 25;printf("t: %.2f, d1: %d, d2: %d", temperature, adc2_data, adc1_data2);
}
演示结果
参考资料
《STM32F103xx固件函数库用户手册》
《STM32F10xxx Reference manual》
https://doc.embedfire.com/mcu/stm32/f103zhinanzhe/std/zh/latest/book/ADC.html