ADC模拟-数字转换技术
STM32使用是逐次逼近法(SAR) ,通过不断进行二分比较最终确定电压值的方法 ,STM32F103是12位分辨率的ADC,即最终结果是以12个二进制位存储(0~4095),其中0代表0V,4095代表参考电压的值(一般为3.3V),得到测量后的12位二进制数据后,将其除以4095再乘以参考电压3.3V,便可以知道测得的电压值。
以下面图片为例:
这里高位先行,如果输入的是一个0.9V的电压,那么如果DAC将第一位置1,即2048(1.65V)进行比较发现比0.9V大,所以第一位置0,接着如果第二位置1,即1024(0.825V)进行比较发现比0.9V小,所以第二位置1,接着第三位...以此类推,最终填满12位。

在STM32中,有16个GPIO口 可以进行电压值的采样工作,称其为16个ADC采样通道 ,不过STM32F103C8T6只有前10个通道 ,另外还有两个内部通道,用于采集STM32内部提供的电压值:一个连接在芯片内部的温度传感器上,一个连接在内部参考电压上 。

有两个用于转换的ADC结构:ADC1和ADC2 ,每个ADC中有注入组和规则组 ,注入组我们先忽略,规则组可以理解为"普通组",它就像一个用于注册排队的表格,我们将某个ADC通道"注册"在上面,当我们触发ADC时,ADC就会对此通道进行采样、转换,转换结果放入"规则通道数据寄存器"中等待程序读取 ,我们甚至可以"注册"多个通道,让ADC依次进行转换,这个在下小节来讲:

在我们开始配置CubeMX前,先介绍一下滑动变阻器 ,通过旋钮我们就可以调节这个变阻器的电压,原理图如下:

接下来我们开始配置CubeMX:

这次我们要用的接线口是PA5,点开PA5,可以发现它可以被设置为ADC1或者ADC2的通道5口,接着我们点开ADC1,选择通道5,参数先保持默认

注意因为ADC1、2都是依靠APB2的时钟线,并且频率不宜过快,在F103上不要超过14MHz,因此将ADC专用的分频器改为6分频:

最后代码如下:
c
int16_t Value=0;
float Voltage=0.0;
HAL_ADCEx_Calibration_Start(&hadc1); //校验函数
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
HAL_ADC_Start(&hadc1); //开启ADC转换
HAL_ADC_PollForConversion(&hadc1,HAL_MAX_DELAY); //等待ADC转换结束
Value=HAL_ADC_GetValue(&hadc1); //获取ADC规则组寄存器值
Voltage=(Value/4095.0)*3.3;
OLED_ShowNum(1,1,Voltage,3);
HAL_Delay(500);
}
当前程序是靠HAL_ADC_Start(&hadc1); 触发一次轮询转换,当把数据放在规则组寄存器后,还要再触发一次才能进行下次转换,所以要放在while循环里不断触发,不过STM32还为我们提供了一种连续转换的功能。
持续转换功能:
开启此功能后,我们只需要触发一次ADC,ADC在完成一次转换后,会马上进行下一次的采样转换工作,持续不断更新寄存器里的测量结果,随时读取即可

CubeMX开启如下:

开启后代码如下:
c
int16_t Value=0;
float Voltage=0.0;
HAL_ADCEx_Calibration_Start(&hadc1); //校验函数
HAL_ADC_Start(&hadc1); //开启ADC转换
HAL_ADC_PollForConversion(&hadc1,HAL_MAX_DELAY); //等待ADC转换结束
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
Value=HAL_ADC_GetValue(&hadc1); //获取ADC规则组寄存器值
Voltage=(Value/4095.0)*3.3;
OLED_ShowNum(1,1,Voltage,3);
HAL_Delay(500);
}
可以看到只用把开启转换函数和等待转换函数放while循环前面就好了
相关ADC函数代码:
c
/* ADC启动函数
*/
HAL_ADC_Start(ADC_HandleTypeDef* hadc);
/* 读取ADC值
*/
HAL_ADC_GetValue(ADC_HandleTypeDef* hadc);
/* 等待转换完成的函数
* 一直轮询检查是否转换完成
* 第二个参数是超时时间,一般传入HAL_MAX_DELAY表示无限等待
*/
HAL_ADC_PollForConversion(ADC_HandleTypeDef* hadc, uint32_t Timeout);
/* ADC校准函数
* 建议在每次上电后执行一次校准
*/
HAL_ADCEx_Calibration_Start(ADC_HandleTypeDef* hadc);
连续转换模式:
本小节来同时采集电位器(滑动变阻器)、芯片内部温度、芯片内部参考电压。我们之前说过,ADC的规则组好像一个用来注册排队的表格,当时我们在ADC1的规则组上只注册了通道5,检测电位器的电压值,当我们启动一次ADC,规则组就会对通道5进行一次采样&转换,转换完成后,结果放进规则通道数据寄存器中,并且将ADC状态寄存器的转换结束标志位置1 。当我们调用HAL_ADC_GetValue函数读取寄存器时,转换结束标志位会被自动置0,以待下一次转换,如果我们还开启了连续转换功能,就可以只启动一次ADC,ADC就不断对通道5进行采样转换。
本小节我们会将通道5,内部温度传感器通道,内部参考电压通道都注册到规则组上来 ,并且开启ADC的扫描模式 ,这样每次触发ADC测量时,ADC就会先采样&转换通道5,将结果放在规则通道数据寄存器中,紧接着采样&转换通道4,转换完通道4结果也放入规则通道数据寄存器中,紧接着转换内部温度传感器通道,同样的,最后转换内部参考电压通道;通过ADC的扫描模式,我们就可以在一个ADC上连续测量多个通道的电压值;

规则通道数据寄存器只有一个,那么后面的数据就会把前面的数据覆盖掉,最后从规则通道数据寄存器读取的值就是最后测量出来的数据,而且HAL_ADC_GetValue函数也不能使用了,因为它也不知道什么时候完成规则组全部数据的转换,那怎么办呢?这时候就可以用到我们的DMA了

如图,我们先定义一个数组,然后我们可以告诉DMA,我们有一个大小为4的数组,要将规则组中的数据依次搬运到此数组中;这样,每次转换完成,转换结束标志位被置1时,转换结束事件就会通知DMA进行搬运;如果我们给ADC开了连续转换模式,还可以给DMA设置循环模式;这样的话ADC连续不断地依次对四个通道进行转换,DMA也同时依次循环搬运转换数据到数组中,形成配合
下面我们开始CubeMX的配置:
来到ADC1将内部温度传感器和内部参考电压 勾上

将规则组通道(Nunmber Of Conversion)设置为3,然后可以看到扫描模式(Scan Conversion Mode)自动开启

接下来将通道设置为内部温度传感器通道和内部参考电压通道

内部参考电压的存在,是因为我们不能保证参考电压一定稳定在3.3V,所以STM32在内部提供了一个一直输出1.2V的内部参考电压,然后我们测得内部参考电压通道的ADC值,与1.2V进行计算,就能得出真正的参考电压:

根据芯片手册,内部参考电压的采样时间典型值是5.1us,按照我们设置的ADC时钟周期为12MHz,5.1us就是61.2个周期,因此采样周期(Sampling Time)我都设置成了71.5个周期。为了使用DMA搬运,来到DMA选项卡:
点开DMA Setting,然后点击添加(Add)然后选择ADC1,方向是外设到内存,因为规则通道数据寄存器只有一个,所以地址不用自增,数组则要,转运数据因为ADC转运的是12位数据,所以半个字(2个字节大小)就好,所以我们保持默认就好。

代码如下:
c
uint16_t Value[3];
//ADC转换完成回调函数
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
if(hadc==&hadc1)
{
OLED_ShowNum(1,1,Value[0],4);
OLED_ShowNum(2,1,Value[1],4);
OLED_ShowNum(3,1,Value[2],4);
}
}
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
//开启ADC转换,sizeof(Value)/sizeof(uint16_t)是取数组个数
//第一个参数ADC句柄指针,第二个参数搬运的地址,第三个参数搬运次数
HAL_ADC_Start_DMA(&hadc1,(uint32_t*)Value,sizeof(Value)/sizeof(uint16_t));
}
除了在循环里触发转换,然后等待中断回调,我们也可以像之前一样,让其持续转换,转换完一遍后,马上进行下一轮转换
连续转换模式:
回到CubeMX,将连续转换模式 开启:

然后来到DMA选项卡,将DMA模式改为Circular循环模式:

这样再地址自增时到了最后一个,就会回到开口,和ADC的连续转换模式配合起来,因为一直在转换,所以我们直接取数据就行了,不需要回调函数了,上面的代码也可以将OLED函数放到while循环中,这样也不用回调函数了。
c
HAL_ADC_Start_DMA(&hadc1,(uint32_t*)Value,sizeof(Value)/sizeof(uint16_t));
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
OLED_ShowNum(1,1,Value[0],4);
OLED_ShowNum(2,1,Value[1],4);
OLED_ShowNum(3,1,Value[2],4);
}
将HAL_ADC_Start_DMA()放到while循环前面启动一次就欧克了,可以看到效果是一样的