目录

1.  ADC

1.1  简介

1.2  逐次逼近型ADC(ADC0809)

1.3  ADC框图(STM32)

1.4  ADC基本结构

1.4.1  GPIO口配置

1.4.2  转换器配置

1.4.3  采样时钟选择

1.4.4  转换模式选择

1.4.4.1  单次非扫描模式

1.4.4.2  单次扫描模式

1.4.4.3  连续非扫描模式

1.4.4.4  连续扫描模式

1.4.4.5  配置

1.4.5  触发控制

1.4.6  数据对齐

1.4.7  使能

1.4.8  校准

1.4.9  读取

2.  新建工程

3.  代码编写

3.1  代码释义

3.2  printf () 重定向

3.3  ADC相关代码


1.  ADC

1.1  简介

        ADC全称是(Analog-to-Digital Converter)模拟-数字转换器,一般我们把模拟信号(Analog signal) 用A来进行简写,数字信号(digital signal) 用D来表示。主要用于将连续传输的模拟信号转换为数字信号,便于数字系统(如中央处理器CPU、微控制器MCU等)对传输信息进行快速处理和分析。

         模拟信号是指用连续变化的物理量所表达的信息,如温度、湿度、压力、电压、电流等。ADC模块所采集的模拟信号是连续变化的电压或电流信号,其数值在一定范围内连续变化,如下所示:

        在单片机的使用中,我们可以通过ADC的转换将引脚上连续变化的模拟电压转换为内存中存储的数字变量,建立模拟电路到数字电路的桥梁。

         ADC采样的实现方式主要有两种:外接采样芯片控制核心内部的采样模块。以前的一些芯片是外挂ADC芯片,不过现在许多MCU和DSP内部集成了ADC模块。我们使用的STM32内部集成了ADC功能,不过我们还是来了解一下外挂芯片的原理,方便理解。

1.2  逐次逼近型ADC(ADC0809)

        我们先来了解一下ADC0809的工作原理。

        首先是通道选择开关,通过通道选择开关,选择(IN0~IN7)其中的一路,输入到这个点进行转换,对于0809只有8路选择,但是STM32有18个输入通道,可测量16个外部和2个内部信号源(一会下面介绍):

        然后是地址锁存和译码,这里你想要选择哪一路,需要将通道号放到(ADDA、ADDB、ADDC)这三个脚上,然后通过ALE给出锁存信号,对应的通道选择开关就会自动拨好了:

        比较器是一个电压比较器,他可以判断两个输入信号电压的大小关系,其中通道选择开关输入的是待测的电压,另一端是DAC(数模转换器)的电压:

        这里待测电压是未知的,但是DAC所出的电压是我们通过数模转换来的,其编码是已知的,我们通过比较器进行比较,当待测电压大于DAC所示电压,我们就调大DAC的值,如果待测电压小于DAC所示电压,我们就调小DAC的值,直到待测电压等于DAC所示电压,这样DAC的输入数据就是待测电压的数据,我们就可以知道待测电压的编码(类似于二分法)。

        得出待测电压的编码通过三态锁存缓冲器进行输出,8位就有8根线,12位就有12根:

1.3  ADC框图(STM32)

        STM32F1系列其芯片内部有多达18个通道,可测量16个外部和2个内部信号源:

        通道检测数据进入搭配模拟多路开关,模拟多路开关将数据输出的“模拟至数字转换器”(这里的作用类似于上面介绍的逐次逼近ADC),将转换数据传至数据寄存器,取出结果:

        对于这里,我们可以将注入通道或者规则通道比作饭店上菜的菜单,注入通道的通道相当于菜单上的菜,菜单上有菜(有数据),厨房做菜(注入通道寄存器接受数据),菜单上有多少菜做多少,从上往下一道一道做(数据逐次注入);

        规则通道有16个通道(也就是菜单上能写16个),但是规则通道数据寄存器只有一个(可以理解为能够上菜的桌子太小只能放一个菜),所以每当有新菜,就会将之前的菜端走,只能保存最后一个数据(桌子太小只能放最后一个菜,无论你前面吃没吃都给端走了),这里就需要搭配DMA来使用(作用就是当接收到一个数据之后,将这个数据挪到其他地方去,防止被覆盖):

        这里相当于ADC0809的START信号(开始转换信号),对于STM32的ADC触发ADC开始转换的信号又两种:软件触发和硬件触发。

        软件触发就是在程序中手动调用一条代码,就可以启动转换。

        硬件触发如下,由于ADC需要经过一个固定时间转换一次,因此我们需要通过定时器进行控制时间,但是若是实时采集数据,会频繁进入定时器,频繁进入中断,而中断又有优先级,若是多处地方需要频繁进入中断,这将会导致程序卡死,不过对于这种需要频繁进入中断,并且中断之进行简单的操作,一般会有硬件的支持:

1.4  ADC基本结构

1.4.1  GPIO口配置

        上面的有点乱感觉不理解,没关系,我们有简化版本(注意由于对于底层的一些东西STM32CubeMX直接帮我们配好了,因此这里先以标准库为例,下面生成完代码,在解释HAL库部分):

        首先是输入通道的选择:

        引脚的选择: 

通道

ADC1

ADC2

ADC3

通道0

PA0

PA0

PA0

通道1

PA1

PA1

PA1

通道2

PA2

PA2

PA2

通道3

PA3

PA3

PA3

通道4

PA4

PA4

PF6

通道5

PA5

PA5

PF7

通道6

PA6

PA6

PF8

通道7

PA7

PA7

PF9

通道8

PB0

PB0

PF10

通道9

PB1

PB1

通道10

PC0

PC0

PC0

通道11

PC1

PC1

PC1

通道12

PC2

PC2

PC2

通道13

PC3

PC3

PC3

通道14

PC4

PC4

通道15

PC5

PC5

通道16

温度传感器

通道17

内部参考电压

        假如我们选择PA0作为ADC的通道,那么我们先初始化GPIO口:

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	//开启GPIOA的时钟

	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);					//将PA0引脚初始化为模拟输入

1.4.2  转换器配置

        然后是ADC转换器的配置,前面我们也说了ADC有规则组和注入组两种转换器,规则组有16个菜单但是只能上最后一个菜,注入组有只有4个菜单但是能全部上桌,看自己需求配置:

        这里我们用规则组,前面也说了我们使用的是PA0的口,那么这里我们使用ADC1的通道0,采样顺序我们设置为1,采样时间我们选择ADC_SampleTime_55Cycles5:

	/*规则组通道配置*/
	ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);		//规则组序列1的位置,配置为通道0
宏定义 采样时间(周期) 适用场景
ADC_SampleTime_1Cycles5 1.5 高速、低阻抗信号(如直流电压)
ADC_SampleTime_7Cycles5 7.5 中等速度、中等阻抗
ADC_SampleTime_13Cycles5 13.5 一般模拟信号
ADC_SampleTime_28Cycles5 28.5 较高阻抗信号
ADC_SampleTime_41Cycles5 41.5 高阻抗信号(如传感器)
ADC_SampleTime_55Cycles5 55.5 高阻抗、高精度需求
ADC_SampleTime_71Cycles5 71.5 极高阻抗(如热电偶)
ADC_SampleTime_239Cycles5 239.5 超长采样(极低噪声需求)

1.4.3  采样时钟选择

        对于ADC的采样时间的了解,在了解前我们先了解一下,AD转换的步骤,其分为:采样,保持,量化,编码。

        对于采样是将连续的模拟信号在时间域上进行离散化的过程。它通过在特定的时间点上获取信号的值,将模拟波形切分为一系列的离散时间片。这些时间片的大小通常与原波形的特征值相匹配,但由于只在有限的时间点上进行测量,部分信息可能会丢失。这种信息的丢失被称为“抽样失真”。

        保持是采集模拟信号后,需花时间将其转化为数字信号,为了给后续的量化编码过程提供一个稳定值,需用保持电路对取得的模拟信号进行电压保持。此过程可通过并联电容的方式实现。输入的连续模拟信号经过采样与保持后将得到一个时间上离散的模拟信号样本集合。

        而这个采样时间就是我们上面设置的时间,这个时间主要为了防止例如尖峰等波动造成影响,对于STM32中ADC采样的总时间:

T_{CONV}=采样时间+12.5个ADC周期

其中采样时间就是我们设置的时间

12.5个ADC周期:是因为STM32是12位逐次逼近型,所以有12个周期,而另外0.5个采样走起是为了例如程序运行时间等给做的一个缓冲。

举个例子:

当ADCCLK=14MHz,采样时间为1.5个ADC周期

T_{CONV}=1.5+12.5=14个ADC周期=1us

这个也是为什么STM32的ADC采样频率最大到1us转换时间

        而对于ADCCLK的设置,最大到14MHz,我们可以通过:

	/*设置ADC时钟*/
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);						//选择时钟6分频,ADCCLK = 72MHz / 6 = 12MHz

        对于其中的参数配置:

#define RCC_PCLK2_Div2                   ((uint32_t)0x00000000)
#define RCC_PCLK2_Div4                   ((uint32_t)0x00004000)
#define RCC_PCLK2_Div6                   ((uint32_t)0x00008000)
#define RCC_PCLK2_Div8                   ((uint32_t)0x0000C000)
#define IS_RCC_ADCCLK(ADCCLK) (((ADCCLK) == RCC_PCLK2_Div2) || ((ADCCLK) == RCC_PCLK2_Div4) || \
                               ((ADCCLK) == RCC_PCLK2_Div6) || ((ADCCLK) == RCC_PCLK2_Div8))

        这里有人会说如果我们将参数设为:RCC_PCLK2_Div2或者RCC_PCLK2_Div4不是超过14Mhz了吗,为什么最大只能到14MHz,那是因为这样设置就会超频了,速度虽然更快了,但是稳定性方面就不敢保证了。

        量化是数字信号在时间和幅值上都是离散的,量化是将采样电压转化为离散电平的近似过程。常用的量化方法有只舍不入和四舍五入。量化过程中会产生量化误差,它是一种无法消除的原理性误差。ADC的位数越高,离散电平之间的差值越小,量化误差也会越小。以参考电压3.3V的12位ADC采样模块为例,输入模拟电压与量化后产生的数值之间的关系如下:

        编码为方便数字信号数据的传输与存储,需要将量化得到的十进制数字信号转换成二进制编码。常用的编码方式有二进制编码、格雷编码、调制编码和二进制补码编码等。

1.4.4  转换模式选择

        STM32有四种选择模式,分别是:单次非扫描模式、单次扫描模式、连续非扫描模式以及连续扫描模式。

1.4.4.1  单次非扫描模式

        只有第一个序列1的位置有效,在其中选择转换的通道,例如选择通道2,这样ADC就会对通道2进行模数转换,过一会转换完成,将其放到数据寄存器里,将EOC标志位置1,表示转换完成:

1.4.4.2  单次扫描模式

        其与非扫描模式不同的是,非扫描模式只使用了序列1,而扫描模式将下面序列使用了起来,在结构体初始化时,需要初始化通道数目:

1.4.4.3  连续非扫描模式

        这个和单次转换非扫描模式有些类似,但是不同的是,这个在转换结束后不需要停止,可以直接进行下一次转换,不需要多次触发:

1.4.4.4  连续扫描模式

        这个就是在单次转换扫描模式基础上,使其连续触发:

1.4.4.5  配置

        首先是上面四种模式的配置,我们可以通过开启和关闭一下两个函数进行调整:

	ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;		//连续转换,失能,每转换一次规则组序列后停止
	ADC_InitStructure.ADC_ScanConvMode = DISABLE;			//扫描模式,失能,只转换规则组的序列1这一个位置

        假如我们采用单次非扫描,那么我们就需要将通道数设为1:

	ADC_InitStructure.ADC_NbrOfChannel = 1;					//通道数,为1,仅在扫描模式下,才需要指定大于1的数,在非扫描模式下,只能是1

        由于这里我们只使用了PA0,只是一个通道,因此选择单通道模式,对于这一块的配置:

ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;		//模式,选择独立模式,即单独使用ADC1

        其他的模式如下:

/* ADC(模数转换器)工作模式配置宏定义 */

#define ADC_Mode_Independent                       ((uint32_t)0x00000000)// 独立模式:每个ADC独立工作,不与其他ADC同步
#define ADC_Mode_RegInjecSimult                    ((uint32_t)0x00010000)// 规则组和注入组同步模式:规则通道和注入通道同时转换
#define ADC_Mode_RegSimult_AlterTrig               ((uint32_t)0x00020000)// 规则组同步 + 交替触发模式:多个ADC同步采样规则通道,并使用交替触发
#define ADC_Mode_InjecSimult_FastInterl            ((uint32_t)0x00030000)// 注入组同步 + 快速交替模式:注入通道同步采样,并结合快速交替采样
#define ADC_Mode_InjecSimult_SlowInterl            ((uint32_t)0x00040000)// 注入组同步 + 慢速交替模式:注入通道同步采样,并结合慢速交替采样
#define ADC_Mode_InjecSimult                       ((uint32_t)0x00050000)// 注入组同步模式:多个ADC的注入通道同步采样
#define ADC_Mode_RegSimult                         ((uint32_t)0x00060000)// 规则组同步模式:多个ADC的规则通道同步采样
#define ADC_Mode_FastInterl                        ((uint32_t)0x00070000)// 快速交替模式:ADC之间快速交替采样(适用于高频率采样)
#define ADC_Mode_SlowInterl                        ((uint32_t)0x00080000)// 慢速交替模式:ADC之间慢速交替采样(适用于低频率采样)
#define ADC_Mode_AlterTrig                         ((uint32_t)0x00090000)// 交替触发模式:多个ADC通过交替触发信号进行采样

/* 检查ADC模式是否有效的宏 */
// 判断给定的MODE参数是否是上述合法的ADC模式之一
#define IS_ADC_MODE(MODE) (((MODE) == ADC_Mode_Independent) || \
                           ((MODE) == ADC_Mode_RegInjecSimult) || \
                           ((MODE) == ADC_Mode_RegSimult_AlterTrig) || \
                           ((MODE) == ADC_Mode_InjecSimult_FastInterl) || \
                           ((MODE) == ADC_Mode_InjecSimult_SlowInterl) || \
                           ((MODE) == ADC_Mode_InjecSimult) || \
                           ((MODE) == ADC_Mode_RegSimult) || \
                           ((MODE) == ADC_Mode_FastInterl) || \
                           ((MODE) == ADC_Mode_SlowInterl) || \
                           ((MODE) == ADC_Mode_AlterTrig))

1.4.5  触发控制

        对于STM32的触发,前面我们也说了有软件和硬件两种:

ADC1和ADC2用于规则通道的外部触发:

        配置如下: 

	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;	//外部触发,使用软件触发,不需要外部触发

        是STM32F1系列的一些触发源:

/* ADC(模数转换器)外部触发源配置宏定义 */

// ============= 适用于ADC1和ADC2的触发源 =============
// TIM1(定时器1)的CC1(捕获/比较通道1)事件触发
#define ADC_ExternalTrigConv_T1_CC1                ((uint32_t)0x00000000)
// TIM1的CC2(捕获/比较通道2)事件触发
#define ADC_ExternalTrigConv_T1_CC2                ((uint32_t)0x00020000)
// TIM2的CC2(捕获/比较通道2)事件触发
#define ADC_ExternalTrigConv_T2_CC2                ((uint32_t)0x00060000)
// TIM3的TRGO(触发输出)事件触发
#define ADC_ExternalTrigConv_T3_TRGO               ((uint32_t)0x00080000)
// TIM4的CC4(捕获/比较通道4)事件触发
#define ADC_ExternalTrigConv_T4_CC4                ((uint32_t)0x000A0000)
// 外部中断线11或TIM8的TRGO事件触发
#define ADC_ExternalTrigConv_Ext_IT11_TIM8_TRGO    ((uint32_t)0x000C0000)

// ============= 适用于ADC1/2/3的通用触发源 =============
// TIM1的CC3(捕获/比较通道3)事件触发
#define ADC_ExternalTrigConv_T1_CC3                ((uint32_t)0x00040000)
// 不使用外部触发(软件触发模式)
#define ADC_ExternalTrigConv_None                  ((uint32_t)0x000E0000)

// ============= 仅适用于ADC3的专用触发源 =============
// TIM3的CC1(捕获/比较通道1)事件触发
#define ADC_ExternalTrigConv_T3_CC1                ((uint32_t)0x00000000)
// TIM2的CC3(捕获/比较通道3)事件触发
#define ADC_ExternalTrigConv_T2_CC3                ((uint32_t)0x00020000)
// TIM8的CC1(捕获/比较通道1)事件触发
#define ADC_ExternalTrigConv_T8_CC1                ((uint32_t)0x00060000)
// TIM8的TRGO(触发输出)事件触发
#define ADC_ExternalTrigConv_T8_TRGO               ((uint32_t)0x00080000)
// TIM5的CC1(捕获/比较通道1)事件触发
#define ADC_ExternalTrigConv_T5_CC1                ((uint32_t)0x000A0000)
// TIM5的CC3(捕获/比较通道3)事件触发
#define ADC_ExternalTrigConv_T5_CC3                ((uint32_t)0x000C0000)

/* 检查外部触发源是否有效的宏 */
// 判断给定的REGTRIG参数是否是上述合法的ADC触发源之一
#define IS_ADC_EXT_TRIG(REGTRIG) (((REGTRIG) == ADC_ExternalTrigConv_T1_CC1) || \
                                  ((REGTRIG) == ADC_ExternalTrigConv_T1_CC2) || \
                                  ((REGTRIG) == ADC_ExternalTrigConv_T1_CC3) || \
                                  ((REGTRIG) == ADC_ExternalTrigConv_T2_CC2) || \
                                  ((REGTRIG) == ADC_ExternalTrigConv_T3_TRGO) || \
                                  ((REGTRIG) == ADC_ExternalTrigConv_T4_CC4) || \
                                  ((REGTRIG) == ADC_ExternalTrigConv_Ext_IT11_TIM8_TRGO) || \
                                  ((REGTRIG) == ADC_ExternalTrigConv_None) || \
                                  ((REGTRIG) == ADC_ExternalTrigConv_T3_CC1) || \
                                  ((REGTRIG) == ADC_ExternalTrigConv_T2_CC3) || \
                                  ((REGTRIG) == ADC_ExternalTrigConv_T8_CC1) || \
                                  ((REGTRIG) == ADC_ExternalTrigConv_T8_TRGO) || \
                                  ((REGTRIG) == ADC_ExternalTrigConv_T5_CC1) || \
                                  ((REGTRIG) == ADC_ExternalTrigConv_T5_CC3))

1.4.6  数据对齐

        我们知道STM32的ADC采集数据是12位的,但是我们的数据要存到数据寄存器,但是数据寄存器是16位的,这就需要我们将数据进行对齐了,有两种方法:左对齐和右对齐。

数据右对齐:

数据左对齐:

        一个是高位补零,一个是低位补零,一般情况下我们都是使用数据右对齐,这样读取出来的数据就是原值,而如果要是使用左对齐,因为左移了四位,对于二进制而言,就需要将最终数据除以16(2的4次方),才能得到原值。

这里可能会有问,既然右对齐还要多操作一步,那么其存在的意义是干什么呢?对于右对齐适用于精度比较高的数据,左对齐适用于不需要我们进行高精度的东西,我们可直接去高8位数据,舍弃后面的四位,这样不需要这个高的精度。

        配置:

	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;	//数据对齐,选择右对齐

        这个参数就比较少了,一个左,一个右,看需求:

#define ADC_DataAlign_Right                        ((uint32_t)0x00000000)
#define ADC_DataAlign_Left                         ((uint32_t)0x00000800)
#define IS_ADC_DATA_ALIGN(ALIGN) (((ALIGN) == ADC_DataAlign_Right) || \
                                  ((ALIGN) == ADC_DataAlign_Left))

1.4.7  使能

        这没啥,就ADC使能,用哪个使能哪个:

	/*ADC使能*/
	ADC_Cmd(ADC1, ENABLE);									//使能ADC1,ADC开始运行

1.4.8  校准

        ADC有一个内置自校准模式。校准可大幅减小因内部电容器组的变化而造成的准精度误差。校准期间,在每个电容器上都会计算出一个误差修正码(数字值),这个码用于消除在随后的转换中每个电容器上产生的误差。

        启动校准前, ADC必须处于关电状态超过至少两个ADC时钟周期。

        建议在每次上电后执行一次校准。

/* ADC校准流程 */
// 1. 复位ADC1的校准寄存器(启动校准复位)
ADC_ResetCalibration(ADC1);		// 固定流程,内部有专用校准电路会自动执行校准操作

// 2. 等待校准复位完成(硬件会自动清除标志位)
while (ADC_GetResetCalibrationStatus(ADC1) == SET); // 循环检测直到复位完成

// 3. 启动ADC1的校准过程
ADC_StartCalibration(ADC1);		// 开始实际校准过程

// 4. 等待校准完成
while (ADC_GetCalibrationStatus(ADC1) == SET);	// 循环检测直到校准完成

1.4.9  读取

        上面已经完成了ADC的初始化操作,那么我们如何进行数据的读取呢?STM32给我们封装了三个函数,首先:

ADC_SoftwareStartConvCmd(ADC1, ENABLE);

        其作用是在开始前调用软件触发ADC1开始一次转换(无需外部触发信号),调用后,ADC硬件会自动开始采样和转换。


ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)

        ADC转换完成标志位,其中 ADC_FLAG_EOC 转换结束标志位,硬件自动置位。若其返回值为 RESET 表示标志位未就绪(STM32库中通常定义为0),在裸机开发中我们可以通过while()循环去阻塞式等待,来完成检测ADC是否完成采集,如:

while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);

        但是如果我们要将其运用到FreeRTOS当中,这种方法是不行的,首先循环会持续占用CPU,直到ADC转换完成,这样的阻塞会阻止其他任务运行,破坏实时性,而我们使用FreeRTOS的初衷就是实时操作,这样肯定是不行的。

        那么我们要使用什么方法呢?我们可以通过中断来实现,通过任务通知来传递数据,具体实现方法下面实例进行讲解。


ADC_GetConversionValue(ADC1);

        从ADC数据寄存器中读取12位转换结果(STM32的ADC分辨率为12位时,范围0~4095),读取后,EOC标志位会自动清除。

2.  新建工程

        还是一样,重复的配置,这里不再一步一步演示步骤,详细可以参考:

STM32学习·HAL库速通篇(三)·如何在STM32CubeMX新建工程_stm32cubemx 配置cmsis库-CSDN博客

        只进行一些上面没有的配置,首先是引脚配置,根据需求配置自己需要配置的引脚,这里我选PA6:

        然后是该引脚的一些配置,相关功能可以查看上方介绍,这里因为我们就配置一个引脚,就默认配置了:

        然后看向时钟配置部分,会发现当我们选择ADC引脚后,这里会出现报错:

        这里主要是因为这里最大只能设置到14MHz:

        这里我们将其改为6分频:

        然后来到GPIO配置,改个名字:

        同时为了方便数据打印,我们更直观的查看数据,这里我们将串口也初始化一下:

        将中断勾选上:

        然后配置文件名称等信息生成代码:

3.  代码编写

3.1  代码释义

        我们首先来看一下ADC配置有关的代码:

/**
  * @brief  ADC1外设初始化函数(由STM32CubeMX自动生成,适配前文的ADC配置界面参数)
  * @note   配置ADC1为独立模式、软件触发、单通道(通道6)、单次转换、右对齐
  * @param  无
  * @retval 无
  */
void MX_ADC1_Init(void)
{
  /* USER CODE BEGIN ADC1_Init 0 */
  // 用户自定义初始化前置代码区(CubeMX不会覆盖此区域)
  /* USER CODE END ADC1_Init 0 */

  // 定义ADC通道配置结构体,用于配置规则通道的参数(通道号、排序、采样时间等)
  ADC_ChannelConfTypeDef sConfig = {0};

  /* USER CODE BEGIN ADC1_Init 1 */
  // 用户自定义初始化中间代码区(CubeMX不会覆盖此区域)
  /* USER CODE END ADC1_Init 1 */

  /** Common config - ADC通用参数配置 **/
  // 指定当前初始化的ADC外设为ADC1(绑定句柄到硬件外设)
  hadc1.Instance = ADC1;
  // 禁用扫描模式:仅转换指定的单个规则通道(若启用则按Rank顺序扫描多个通道)
  hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE;
  // 禁用连续转换模式:单次转换完成后停止,需重新触发才会再次转换
  hadc1.Init.ContinuousConvMode = DISABLE;
  // 禁用间断转换模式:不拆分转换序列(仅多通道扫描时生效)
  hadc1.Init.DiscontinuousConvMode = DISABLE;
  // 设置外部触发源为软件触发:通过HAL_ADC_Start()等函数手动启动转换
  hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
  // 转换数据右对齐:12位ADC数据的最低位对应寄存器的0位(高4位补0)
  hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
  // 规则转换序列的通道数量:仅1个通道(与前文配置的Number Of Conversion=1对应)
  hadc1.Init.NbrOfConversion = 1;
  
  // 初始化ADC1外设,应用上述通用配置
  if (HAL_ADC_Init(&hadc1) != HAL_OK)
  {
    // 初始化失败时调用错误处理函数(通常为死循环或硬件报错)
    Error_Handler();
  }

  /** Configure Regular Channel - 配置规则转换通道 **/
  // 指定规则通道为ADC_CHANNEL_6(对应芯片的PA6引脚,需确认硬件引脚映射)
  sConfig.Channel = ADC_CHANNEL_6;
  // 设置该通道在规则序列中的排序为第1位(仅1个通道时必须设为1)
  sConfig.Rank = ADC_REGULAR_RANK_1;
  // 设置采样时间为1.5个ADC时钟周期:采样时间越短,转换速度越快,但精度略低
  // 可选值:ADC_SAMPLETIME_1CYCLE_5/7CYCLE_5/13CYCLE_5/28CYCLE_5/41CYCLE_5/55CYCLE_5/71CYCLE_5/239CYCLE_5
  sConfig.SamplingTime = ADC_SAMPLETIME_1CYCLE_5;
  
  // 应用通道配置到ADC1的规则序列中
  if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
  {
    Error_Handler();
  }

  /* USER CODE BEGIN ADC1_Init 2 */
  // 用户自定义初始化后置代码区(CubeMX不会覆盖此区域,可添加校准、使能等操作)
  /* USER CODE END ADC1_Init 2 */
}

        其一些参数:

/** 
  * @brief  ADC及规则组初始化的结构体定义
  * @note   该结构体的参数分为两个作用域:
  *          - 整个ADC的全局作用域(影响规则组和注入组):数据对齐方式(DataAlign)、扫描转换模式(ScanConvMode)。
  *          - 规则组专属作用域:连续转换模式(ContinuousConvMode)、转换通道数(NbrOfConversion)、间断转换模式(DiscontinuousConvMode)、
  *            间断转换次数(NbrOfDiscConversion)、外部触发转换边沿(ExternalTrigConvEdge)、外部触发转换源(ExternalTrigConv)。
  * @note   通过HAL_ADC_Init()函数配置这些参数时,需满足ADC的状态要求:
  *         ADC必须处于“禁用状态”,或“启用但规则组无正在进行的转换”状态。
  */
typedef struct
{
  uint32_t DataAlign;                        /*!< 指定ADC数据的对齐方式:
                                                  右对齐(默认配置):数据最高位(MSB)存储在寄存器第11位,最低位(LSB)存储在寄存器第0位;
                                                  左对齐:
                                                    - 规则组:数据最高位(MSB)存储在寄存器第15位,最低位(LSB)存储在寄存器第4位;
                                                    - 注入组(因偏移量应用后可能出现负值,最高位保留符号位):数据最高位(MSB)存储在寄存器第14位,最低位(LSB)存储在寄存器第3位。
                                                  该参数取值需为 @ref ADC_Data_align 宏定义的值 */

  uint32_t ScanConvMode;                     /*!< 配置规则组和注入组的序列器模式。
                                                  该参数可与“间断转换模式(DiscontinuousConvMode)”配合使用,将主转换序列拆分为多个连续的子序列。
                                                  禁用时:转换以单次模式执行(仅转换一个通道,即序列排名1的通道),
                                                          “转换通道数(NbrOfConversion)”和“注入组转换通道数(InjectedNbrOfConversion)”参数失效(等效于设为1);
                                                  启用时:转换以序列模式执行(转换多个通道,通道数量由“NbrOfConversion/InjectedNbrOfConversion”和各通道排名定义),
                                                          扫描方向为向上(从排名1到排名n依次转换)。
                                                  该参数取值需为 @ref ADC_Scan_mode 宏定义的值。
                                                  注意:
                                                    1. 规则组若启用扫描模式,建议通过轮询方式(HAL_ADC_Start + 间断模式 + 间断转换次数=1)
                                                       或DMA方式(HAL_ADC_Start_DMA)执行转换,不建议使用中断方式(HAL_ADC_Start_IT):
                                                       扫描模式下,中断仅在序列最后一个转换完成时触发,此前所有转换结果会被最后一个结果覆盖;
                                                    2. 注入组使用扫描模式无此限制:每个排名的通道有独立的结果寄存器,数据不会被覆盖。 */

  FunctionalState ContinuousConvMode;        /*!< 指定规则组在触发信号(软件启动或外部触发)到来后,
                                                  转换执行模式为单次转换(仅转换1次)或连续转换。
                                                  该参数可设为ENABLE(启用)或DISABLE(禁用)。 */

  uint32_t NbrOfConversion;                  /*!< 指定规则组序列器中待转换的通道排名数量。
                                                  若要使用规则组序列器转换多个通道,必须先启用“扫描转换模式(ScanConvMode)”。
                                                  该参数取值范围为 最小值=1 至 最大值=16。 */

  FunctionalState DiscontinuousConvMode;     /*!< 指定规则组的转换序列执行方式:完整序列执行/间断序列执行(主序列拆分为多个连续子序列)。
                                                  仅当序列器启用(ScanConvMode=ENABLE)时,间断模式才生效;若序列器禁用,该参数失效。
                                                  仅当连续模式禁用(ContinuousConvMode=DISABLE)时,间断模式才可启用;若连续模式启用,该参数配置失效。
                                                  该参数可设为ENABLE(启用)或DISABLE(禁用)。 */

  uint32_t NbrOfDiscConversion;              /*!< 指定规则组主转换序列(由NbrOfConversion定义)拆分为多少个间断转换子序列执行。
                                                  若“间断转换模式(DiscontinuousConvMode)”禁用,该参数失效。
                                                  该参数取值范围为 最小值=1 至 最大值=8。 */

  uint32_t ExternalTrigConv;                 /*!< 选择用于触发规则组转换启动的外部事件。
                                                  若设为ADC_SOFTWARE_START,则禁用外部触发(由软件启动转换);
                                                  若设为外部触发源,则在事件上升沿触发转换。
                                                  该参数取值需为 @ref ADC_External_trigger_source_Regular 宏定义的值 */
}ADC_InitTypeDef;

3.2  printf () 重定向

        C 语言中的 printf () 默认输出到显示器,在嵌入式中没有显示器,需要重定向到串口通过重写 fputc () 函数,把 printf () 输出的字符通过串口发出去:

#include <stdio.h>
 
int fputc(int ch, FILE *f)
{
    HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
    return ch;
}

        直接将代码复制进去即可:

        然后找到图示位置勾选上,否则 printf () 可能无法正常使用:

3.3  ADC相关代码

        首先启动ADC校准功能:

	HAL_ADCEx_Calibration_Start(&hadc1);//启动ADC校准

        然后开启ADC:

		HAL_ADC_Start(&hadc1);//启动一次ADC转换

        计算ADC采集的值:

		if(HAL_ADC_PollForConversion(&hadc1,50) == HAL_OK)
		{
			adc_val = HAL_ADC_GetValue(&hadc1);

			//计算电压值
			voltage = (float)adc_val / 4095 * 3.3f;

			printf("ADC原始值:%d,ADC电压值:%.3f V\r\n",adc_val,voltage);
		}

        打印看一下,由于我将ADC采集的引脚接在了3.3V电源上,因此此时显示的是3.3V:

        我将引脚拔掉,此时处于浮空状态,可以看到引脚电平在不稳定的跳变:

完整工程:

基于STM32实现ADC采集电压(HAL库版本)资源-CSDN下载

HAL库学习笔记_时光の尘的博客-CSDN博客

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐