引言
前面我们学习了ADC基础知识以及常用的寄存器配置内容,接下来开始实战一下,实现ADC独立模式单通道转换,用于采集电压的案例。
所谓的独立模式,就是只有一个ADC外设进行转换的模式。
一、需求描述
基于寄存器操作,采集可变电阻器的电压,并通过串口把电压数据发送到电脑端。
二、硬件电路设计



由硬件电路图可知,使用到的引脚是PC0,其次可使用的ADC通道为ADC123的通道10之一即可,然后引脚另一边是一个可调电阻接VCC,显然当改变电阻阻值时,接入PC0的电压也会改变,也方便观察ADC转换采集到的电压情况。
三、需求分析
根据前面的需求描述和硬件电路,做一下简单的需求分析:
硬件上,通过可变电阻配合 3V3 电源,让 PC0 引脚能输出可变电压,给 ADC 提供不同的采样输入。
软件上,通过寄存器配置ADC外设相关内容,实现PC0对输入的模拟电压进行采样和转换,然后借助串口打印采集的电压数据。
四、软件设计
在开始软件部分之前,建议各位优先学习和掌握ADC基础知识和常用寄存器配置项,这样更顺利一些,相关文章参见如下:
ADC_基础知识-CSDN博客
https://blog.csdn.net/2301_79475128/article/details/156691644?spm=1001.2014.3001.5501ADC_常用寄存器介绍-CSDN博客
https://blog.csdn.net/2301_79475128/article/details/156728203?spm=1001.2014.3001.5502
4.1 思路分析
软件实现上的流程,我们尽可能按照ADC框图所示的流程逐步分析编写方法。明确使用的引脚PC0,一般直接使用的ADC外设即ADC1,然后使用的通道为通道10。
首先,既然使用STM32上的ADC外设,少不了的便是时钟配置,即开启ADC时钟 ,然后考虑到ADC外设的时钟频率不可超过14MHz,因此还需要进行时钟分频。
根据STM32总线架构图可知,ADC123均挂载在APB2总线上,因此时钟频率默认最高为72MHz。

为适配ADC要求时钟频率,一般至少进行6分频,即使用12MHz的时钟。相应配置可在RCC相关寄存器中找到。



接着,模拟电压通过PC0进入通道,那么PC0这个所谓的GPIO引脚就需要进行配置,本身既然动用的GPIO也应该开启GPIO时钟,但是考虑到实际使用的是其模拟输入模式,整个走向并未进入TTL肖特基触发器就直接给到片上外设,因此实际GPIO无需配置时钟。

因此,GPIO上主要需要配置一下适配ADC外设的工作模式,即模拟输入,因此需要对PC0这个引脚配置好工作模式即可。
接着就正式进入ADC外设了,那么初始应该要选择ADC1的基础参数,如工作模式、转换时间、数据存储方式等,工作模式 即扫描和连续转换模式的配置,转换时间 即其中的采样时间需要手动配置,数据存储方式即数据对齐的选择(左对齐还是右对齐)。
考虑到本次案例仅采集一个可变电阻上的电压,仅单通道转换,所以无需开启扫描模式;
由于电阻可变,意味着输入模拟电压可变,因此为保证采集数据的实时性,需要开启连续转换模式,保证电阻变化时及时更新转换的电压数据;
采样时间的配置可自行根据芯片主频设置,一般不快不慢即可,笔者这里将选择7.5周期的采样时间,这里没有固定要求;
通常情况下数据对齐选择常见右对齐即可。
然后输入模拟电压就要进入通道了,因此这里需要配置一下通道相关内容。本次使用的是PC0的ADC1_IN10,即通道10,然后一般使用规则通道组就行,所以本次通道配置主要在于规则通道组序列长度和序列上的配置。
本次仅通道10进行转换,因此规则通道组序列长度为1,即前面介绍寄存器所说的L[3:0]配置为0000即可;
既然只有一个通道,那就默认将通道10排队在序号1呗,也就是设置序列SQ1为10即可,对应的就是SQL3寄存器的低5位给10(二进制01010)了。
简单来看,ADC初始化的部分就这样就可以直接开始准备转换了,但是考虑后未来可能常见多通道转换,涉及到注入和规则同时存在,因此最好再配置一个触发方式,用于启动转换。也就是使用SWSTART配置来启动ADC规则组通道的转换。
因此为了使用该位,这里需要启动规则通道外部触发转换模式 ,然后选择规则通道的外部事件选择为软件启动。

到这里,ADC1的通道10初始化就完成了,接下来就是要启动转换。
由于默认STM32中的ADC外设是断电状态,也就是寄存器中的ADON位为0,因此首先需要对ADC1上电唤醒;
然后为了保证ADC转换的准确性,一般还需要进行AD校准,并等待校准完成;
接着便是利用规则通道组的软件启动转换,即置位SWSTART;
最后就是等待转换完成,这里涉及到ADC的状态标志,因此相关位在ADC状态寄存器中配置。
最后只需将ADC结果转换成实际电压数据即可,也就是对规则通道数据寄存器的数据利用电压转换公式进行转换即可。关于电压转换公式在前面ADC基础知识已经介绍过,也比较简单,这里就不再赘述了。
最后的最后就是在主程序调用转换 然后利用串口循环打印电压数据进行显示了。
这里在进行整理和一下整个流程:
- 时钟配置:开启 ADC1 时钟,配置 RCC 寄存器将 ADC 时钟 6 分频至 12MHz(满足≤14MHz 要求);
- GPIO 配置:将 PC0 引脚配置为模拟输入模式(无需开启 GPIOC 时钟);
- ADC 基础参数配置:设置 ADC1 工作模式(禁止扫描 / 连续转换)、采样时间7.5周期、数据对齐方式(右对齐);
- 规则通道配置:配置规则通道组序列长度为 1,序列首位设为通道 10;
- 触发方式配置:开启规则通道外部触发转换,选择软件启动(SWSTART)作为触发源;
- ADC 使能与校准:使能 ADC1,启动并等待内部校准完成;
- 启动转换:置位 SWSTART 触发规则通道转换,轮询等待转换完成(EOC 位);
- 数据处理与输出:读取 ADC_DR 寄存器数值,按公式转换为实际电压,串口循环打印。
4.2 程序实现(寄存器方式)
根据前面思路分析,其实当前代码编写就应该很清楚了。
4.2.1 adc.h实现
笔者这里对于ADC1的编写分为三大部分,也就是封装三个函数,分别是ADC1初始化 、启动ADC1转换 、读取转换电压。
cpp
#ifndef __ADC_H
#define __ADC_H
#include "stm32f10x.h"
// 初始化
void ADC1_Init(void);
// 开启转换
void ADC1_StartConvert(void);
// 输出转换后的模拟电压
double ADC1_GetVol(void);
#endif
4.2.2 adc.c实现
其中ADC1初始化就是ADC1上电唤醒前的动作,启动ADC1转换就是转换完成前的动作,电压读取即将数据寄存器的数据变成实际电压值返回。参考代码如下:
cpp
/*
* @Description:
* @version:
* @Author: BreezeJuvenile
* @Date: 2025-03-15 10:22:51
* @LastEditors: BreezeJuvenile
* @LastEditTime: 2025-03-15 11:34:27
*/
#include "adc.h"
// 初始化
void ADC1_Init(void)
{
// 1. 开启时钟
RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;
// 分频:6分频 12MHz
RCC->CFGR |= RCC_CFGR_ADCPRE_1;
RCC->CFGR &= ~RCC_CFGR_ADCPRE_0;
// 2. GPIO工作模式:模拟输入 MODE-00 CNF-00
GPIOC->CRL &= (GPIO_CRL_MODE0 | GPIO_CRL_CNF0);
// 3. ADC配置
// 3.1 工作模式-禁止扫描
ADC1->CR1 &= ~ADC_CR1_SCAN;
// 3.2 转换模式:连续转换(单通道循环)
ADC1->CR2 |= ADC_CR2_CONT;
// 3.3 数据对齐-右对齐
ADC1->CR2 &= ~ADC_CR2_ALIGN;
// 3.4 通道采样时间:7.5周期
ADC1->SMPR1 |= ADC_SMPR1_SMP10_0;
ADC1->SMPR1 &= ~(ADC_SMPR1_SMP10_2 | ADC_SMPR1_SMP10_1);
// 3.5 规则通道组序列配置
// 3.5.1 序列长度:1 L-0000
ADC1->SQR1 &= ~ADC_SQR1_L;
// 3.5.2 序列转换位置:sq1 通道为10
ADC1->SQR3 &= ADC_SQR3_SQ1;
ADC1->SQR3 |= (10 << 0);
// 3.6 选择规则通道转换模式:软件触发-EXTSEL-111
ADC1->CR2 |= ADC_CR2_EXTTRIG;
ADC1->CR2 |= ADC_CR2_EXTSEL;
}
// 开启转换
void ADC1_StartConvert(void)
{
// 1. 上电唤醒
ADC1->CR2 |= ADC_CR2_ADON;
// 2. AD校准
ADC1->CR2 |= ADC_CR2_CAL;
// 轮询判断校准是否完毕,完成则清除
while (ADC1->CR2 & ADC_CR2_CAL)
{}
// 3. 启动转换
ADC1->CR2 |= ADC_CR2_SWSTART;
// 4. 判断是否转换完成 EOC置位
while (ADC1->SR & ADC_SR_EOC == 0)
{}
}
// 输出转换后的模拟电压
double ADC1_GetVol(void)
{
return ADC1->DR * 3.3 / (4096 - 1);
}
4.2.3 主程序实现
最后主程序直接调用相关初始化,然后启动转换,最后间隔1s循环串口发送打印数据即可。
cpp
/*
* @Descripttion:
* @Author: JaRyon
* @version:
* @Date: 2025-03-15 10:20:42
*/
#include "usart.h"
#include "adc.h"
#include "Delay.h"
int main(void)
{
// 初始化
USART_Init();
ADC1_Init();
printf("Hello, World!\n");
// 启动转换
ADC1_StartConvert();
// 死循环保持状态
while (1)
{
// 打印转换得到的可调电阻电压
printf("voltage = %.2f v\n", ADC1_GetVol());
Delay_ms(800);
}
}
4.3 程序实现(HAL库方式)
4.3.1 核心图形化配置
一些比较基础的图形化配置比如晶振调试器以及串口的,这里就不再赘述了,主要展示一下核心配置部分。
首先是关于时钟的配置,因为ADC对时钟频率有要求,因此除了基本的时钟树配置,还需要对连接ADC外设的时钟分频器进行设置,比如与之前寄存器实现时一样设置为6分频。

然后是ADC的参数配置部分,这里主要在Analog中,选择ADC1,然后选择使用的通道10,即IN10。然后配置其中的参数,如下图所示。

为方便看懂,我已经中文标出含义,考虑到是HAL库,所以在配置方面考虑更加全面,因此会出现前面没有介绍的相关配置项,这里直接disable即可。
比如间断转换模式,这个的意思是同时需要处理多通道时,转换几个通道后直接停止转换,过会再进行一部分转换这样的意思,因为这样不是完全连续的转换,因此叫做间断转换模式。
其余内容就按照寄存器方式配置统一即可,配置ADC1的通道10后,PC0端口其实直接自动配置好了,如下图所示。

然后中断和DMA本案例没有使用,因此不用配置。
4.3.2 代码完善
使用HAL库好在初始化全部自动生成了,因此ADC的初始化、启动转换等等都已经直接调用即可,而且主函数已经自动生成了ADC初始化的调用。
因此这里我们只需要继续调用一下相关函数即可。需要注意的是,在HAL库中并未直接在启动转换前对AD进行校准,而是将AD校准当做ADC的扩展部分了,因此在启动转换前需要手动调用校准函数进行校准,然后启动转换。(若不进行校准,会出现转换数据无法正确显示在实际电压范围)
cpp
/* USER CODE BEGIN 2 */
printf("Hello, World!\n");
// 开启校准
HAL_ADCEx_Calibration_Start(&hadc1);
// 启动转换
HAL_ADC_Start(&hadc1);
/* USER CODE END 2 */
然后在循环中获取ADC转换结果,计算出实际电压值串口打印即可。
cpp
// 打印输出可调电阻电压值
printf("voltage = %.2f V\n", HAL_ADC_GetValue(&hadc1) * 3.3 / (4096 - 1));
HAL_Delay(1000);
至此,HAL库实现就完成了。
4.4 效果测试
4.4.1 寄存器实现测试

4.4.2 HAL库实现测试

五、小结
本文介绍了STM32 ADC单通道电压采集的实现方法。通过配置PC0引脚为模拟输入模式,设置ADC1时钟分频至12MHz,并配置工作模式为连续转换、7.5周期采样时间、右对齐数据格式。采用寄存器方式实现ADC初始化、启动转换和电压读取功能,并通过串口循环输出采集的电压值。同时给出了HAL库的实现方案,强调校准步骤的重要性。
以上便是本次文章的所有内容,欢迎各位朋友在评论区讨论,本人也是一名初学小白,愿大家共同努力,一起进步吧!
鉴于笔者能力有限,难免出现一些纰漏和不足,望大家在评论区批评指正,谢谢!


