一、核心流程图(双版本适配)
1. 纯文本流程图(无渲染依赖,直接可读)
┌─────────────┐ ┌──────────────────────────────┐ ┌──────────────────────────────────────┐ │ 系统上电 │───>│ 引入核心头文件 │───>│ 定义全局变量(原始值/电压值) │ └─────────────┘ └──────────────────────────────┘ └──────────────────────────────────────┘ │ ▼ ┌──────────────────────────────┐ ┌──────────────────────────────┐ ┌──────────────────────────────┐ │ GPIO初始化 │ │ 开启GPIOA时钟 │ │ 配置PA0为模拟输入 │ └──────────────────────────────┘ └──────────────────────────────┘ └──────────────────────────────┘ │ ▼ ┌──────────────────────────────┐ ┌──────────────────────────────┐ ┌──────────────────────────────┐ │ ADC初始化 │ │ 开启ADC1时钟 │ │ ADC时钟分频(72MHz→12MHz) │ └──────────────────────────────┘ └──────────────────────────────┘ └──────────────────────────────┘ │ ▼ ┌──────────────────────────────┐ ┌──────────────────────────────┐ ┌──────────────────────────────┐ │ 配置ADC参数(单次/软件触发) │ │ 使能ADC1 │ │ ADC校准(复位→启动→等待) │ └──────────────────────────────┘ └──────────────────────────────┘ └──────────────────────────────┘ │ ▼ ┌──────────────────────────────┐ ┌──────────────────────────────┐ ┌──────────────────────────────┐ │ 进入main函数 │ │ 调用GPIO+ADC初始化函数 │ │ 进入while(1)死循环 │ └──────────────────────────────┘ └──────────────────────────────┘ └──────────────────────────────┘ │ ▼ ┌──────────────────────────────┐ ┌──────────────────────────────┐ ┌──────────────────────────────┐ │ 读取ADC原始值 │ │ 原始值换算电压(值/4095×3.3)│ │ 简易延时 │ └──────────────────────────────┘ └──────────────────────────────┘ └──────────────────────────────┘ │ ▼ ┌──────────────────────────────┐ │ 回到while(1)继续循环采集 │ └──────────────────────────────┘二、工程分步解析(逐行注释版)
工程背景
实现PA0引脚(ADC1_CH0)的模拟电压采集,输出实际电压值(参考电压3.3V,12位ADC,数值范围0~4095)。
核心目标
让小白看懂「每一步为什么做、每一行代码什么意思」,流程拆分为8个核心步骤,代码行级注释。
步骤1:引入必要头文件(工程基础)
#include "stm32f10x.h" // STM32F103标准库核心头文件,包含所有寄存器/函数定义 #include <stdio.h> // 可选:如果需要用printf打印电压值,需引入注释解读:
stm32f10x.h是STM32标准库的"基础字典",没有它就无法调用库函数/操作寄存器stdio.h是C语言标准输入输出头文件,后续打印电压会用到
步骤2:定义全局变量(方便后续调用)
u16 ADC_RawValue; // 存储ADC原始采集值(12位范围:0~4095) float ADC_Voltage; // 存储换算后的实际电压值(单位:V)注释解读:
u16是STM32标准库定义的"无符号16位整数"(等价于unsigned short),刚好存12位ADC值float是浮点型,用于存储带小数的电压值(如1.85V)
步骤3:GPIO初始化(配置PA0为模拟输入)
// 函数功能:配置ADC通道对应的GPIO口(PA0) void ADC_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct; // 定义GPIO初始化结构体(标准库固定写法) // 1. 使能GPIOA的时钟(STM32所有外设必须先开时钟才能用) RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 2. 配置PA0为"模拟输入模式" GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0; // 指定配置PA0引脚 GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AIN; // 模拟输入模式(关键:禁止数字功能,避免干扰) GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;// 模拟输入模式下该参数无效,随便填即可 // 3. 将配置写入硬件寄存器(标准库核心操作:把结构体参数"刷"到硬件) GPIO_Init(GPIOA, &GPIO_InitStruct); }小白必懂:
STM32的GPIO默认是"关闭时钟"的,必须先通过
RCC_APB2PeriphClockCmd开启时钟,否则配置无效模拟输入模式(
GPIO_Mode_AIN)是ADC专用模式,此模式下引脚不响应任何数字信号,只采集模拟电压
步骤4:ADC核心参数初始化
// 函数功能:配置ADC1的工作模式、通道、采样时间等核心参数 void ADC_Config(void) { ADC_InitTypeDef ADC_InitStruct; // 定义ADC初始化结构体(标准库固定写法) // 1. 使能ADC1的时钟(ADC属于APB2总线外设) RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); // 2. 配置ADC时钟分频(ADC最大时钟不能超过14MHz,STM32F103主频72MHz) RCC_ADCCLKConfig(RCC_PCLK2_Div6); // 72MHz / 6 = 12MHz(符合要求) // 3. 配置ADC核心参数(逐行解释) ADC_InitStruct.ADC_Mode = ADC_Mode_Independent; // 独立模式(单ADC工作,最常用) ADC_InitStruct.ADC_ScanConvMode = DISABLE; // 关闭扫描模式(单通道采集无需扫描) ADC_InitStruct.ADC_ContinuousConvMode = DISABLE; // 单次转换模式(采集1次就停止) ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // 软件触发(手动启动转换) ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right; // 数据右对齐(方便计算,小白必选) ADC_InitStruct.ADC_NbrOfChannel = 1; // 采集通道数:1个(只采PA0) // 4. 将ADC参数写入硬件寄存器 ADC_Init(ADC1, &ADC_InitStruct); // 5. 使能ADC1(相当于给ADC"开机") ADC_Cmd(ADC1, ENABLE); // 6. ADC校准(提升采集精度,小白必做) ADC_ResetCalibration(ADC1); // 复位校准寄存器 while(ADC_GetResetCalibrationStatus(ADC1));// 等待复位完成(循环检测标志位) ADC_StartCalibration(ADC1); // 启动ADC校准 while(ADC_GetCalibrationStatus(ADC1)); // 等待校准完成(校准期间不能操作ADC) }小白必懂:
扫描模式(
ADC_ScanConvMode):多通道采集时开启,单通道必须关连续转换模式(
ADC_ContinuousConvMode):开启后ADC会自动重复采集,新手先学单次模式校准:ADC出厂后有微小零点误差,校准能消除误差,是提升精度的关键步骤,必须等校准完成再用ADC
步骤5:编写ADC数据读取函数(查询方式)
// 函数功能:读取指定ADC通道的原始值(查询方式:等转换完成再读) // 参数ch:要采集的ADC通道(如ADC_Channel_0对应PA0) u16 ADC_Read_Value(u8 ch) { // 1. 配置要采集的通道和采样时间 // 参数1:ADC1;参数2:通道号;参数3:通道优先级(单通道随便填1);参数4:采样时间 ADC_RegularChannelConfig(ADC1, ch, 1, ADC_SampleTime_55Cycles5); // 2. 软件触发ADC转换(手动"开始采集") ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 3. 等待转换完成(循环检测"转换完成标志位") while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); // EOC=1表示转换完成 // 4. 读取ADC原始值并返回(ADC_GetConversionValue读取数据寄存器) return ADC_GetConversionValue(ADC1); }小白必懂:
采样时间(
ADC_SampleTime_55Cycles5):采样时间越长,精度越高,55.5个时钟周期是新手常用值查询方式:最容易理解的读取方式,缺点是CPU要等转换完成(适合低速采集)
ADC_FLAG_EOC 是ADC的"转换完成标志位",硬件自动置1,读数据后自动清零
步骤6:编写电压换算函数(把原始值转实际电压)
// 函数功能:将ADC原始值换算为实际电压(参考电压3.3V,12位ADC) void ADC_Calc_Voltage(void) { ADC_RawValue = ADC_Read_Value(ADC_Channel_0); // 读取通道0的原始值 // 电压公式:实际电压 = (原始值 / ADC满量程) × 参考电压 // 12位ADC满量程是4095(2^12 - 1),参考电压3.3V ADC_Voltage = (float)ADC_RawValue / 4095 * 3.3; }小白必懂:
(float) 是强制类型转换,因为ADC_RawValue是整数,不转换会导致除法结果为0(如2048/4095=0,转换后是0.5)参考电压:STM32F103默认用VREF+引脚的电压(板载一般接3.3V),如果你的板子接5V,就把3.3改成5
步骤7:主函数(工程入口,串联所有步骤)
int main(void) { // 1. 初始化GPIO(PA0) ADC_GPIO_Init(); // 2. 初始化ADC1 ADC_Config(); // 死循环(单片机主程序必须用循环,否则会跑飞) while(1) { // 3. 计算当前电压 ADC_Calc_Voltage(); // 4. 可选:打印电压值(需提前配置串口,新手可先注释) // printf("ADC原始值:%d,实际电压:%.2fV\r\n", ADC_RawValue, ADC_Voltage); // 5. 延时(避免采集过快,新手可加简易延时) for(int i=0; i<1000000; i++); } }小白必懂:
main函数是程序入口,所有代码从这里开始执行死循环
while(1):单片机上电后会一直执行循环内的代码,直到断电简易延时:新手暂时不用学定时器,用for循环延时即可,数值越大延时越长
三、工程汇总(完整可运行代码)
/* 文件: adc_voltage_measure.c * 功能: STM32 ADC电压采集完整示例 * 硬件: STM32F103C8T6 (BluePill) * 引脚: PA0 (ADC1_IN0) 接可调电压源 (0-3.3V) * 输出: 采集模拟电压并计算实际电压值 */ #include "stm32f10x.h" #include <stdio.h> // 步骤2:定义全局变量 u16 ADC_RawValue; // 存储ADC原始采集值 float ADC_Voltage; // 存储换算后的实际电压值 // 步骤3:GPIO初始化(PA0模拟输入) void ADC_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct; // 1. 使能GPIOA的时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 2. 配置PA0为模拟输入模式 GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AIN; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; // 3. 将配置写入硬件寄存器 GPIO_Init(GPIOA, &GPIO_InitStruct); } // 步骤4:ADC核心参数初始化 void ADC_Config(void) { ADC_InitTypeDef ADC_InitStruct; // 1. 使能ADC1的时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); // 2. 配置ADC时钟分频 RCC_ADCCLKConfig(RCC_PCLK2_Div6); // 3. 配置ADC核心参数 ADC_InitStruct.ADC_Mode = ADC_Mode_Independent; ADC_InitStruct.ADC_ScanConvMode = DISABLE; ADC_InitStruct.ADC_ContinuousConvMode = DISABLE; ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStruct.ADC_NbrOfChannel = 1; // 4. 将ADC参数写入硬件寄存器 ADC_Init(ADC1, &ADC_InitStruct); // 5. 使能ADC1 ADC_Cmd(ADC1, ENABLE); // 6. ADC校准 ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); } // 步骤5:读取ADC原始值 u16 ADC_Read_Value(u8 ch) { // 1. 配置要采集的通道和采样时间 ADC_RegularChannelConfig(ADC1, ch, 1, ADC_SampleTime_55Cycles5); // 2. 软件触发ADC转换 ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 3. 等待转换完成 while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); // 4. 读取ADC原始值并返回 return ADC_GetConversionValue(ADC1); } // 步骤6:电压换算 void ADC_Calc_Voltage(void) { // 1. 读取通道0的原始值 ADC_RawValue = ADC_Read_Value(ADC_Channel_0); // 2. 换算为实际电压 ADC_Voltage = (float)ADC_RawValue / 4095 * 3.3; } // 步骤7:主函数 int main(void) { // 1. 初始化GPIO ADC_GPIO_Init(); // 2. 初始化ADC ADC_Config(); // 3. 主循环 while(1) { // 4. 计算电压 ADC_Calc_Voltage(); // 5. 延时(控制采集频率) for(int i = 0; i < 1000000; i++); } }四、小白额外提示
1. 编译运行环境
开发环境:Keil MDK 5.x
芯片选择:新建工程时选择"STM32F103C8T6"(最常用的入门芯片)
库文件:添加STM32F10x标准库
代码位置:将上面代码粘贴到
main.c中
2. 硬件验证步骤
1. 连接硬件 PA0引脚 ──→ 可调电源正极 (0-3.3V) GND引脚 ──→ 可调电源负极 2. 烧录程序 用ST-Link或USB转串口下载程序 3. 调试查看 - 方法1:用Keil调试模式查看ADC_RawValue和ADC_Voltage变量 - 方法2:配置串口后用printf打印 4. 测试验证 调节电压源,观察采集值是否正确变化 0V → ADC值约0 1.65V → ADC值约2048 3.3V → ADC值约40953. 串口打印配置(进阶)
如果想启用printf打印,需要添加以下代码:
#include "stm32f10x_usart.h" // 串口初始化函数 void USART1_Init(void) { // 串口配置代码(略) } // 重定向printf到串口 int fputc(int ch, FILE *f) { USART_SendData(USART1, (uint8_t)ch); while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET); return ch; }4. 常见问题排查表
现象 | 可能原因 | 解决方法 |
|---|---|---|
采集值一直为0 | 1. PA0没接电压 | 1. 检查接线 |
采集值偏差大 | 1. 未执行ADC校准 | 1. 确保执行校准流程 |
数值跳变严重 | 1. 电源噪声 | 1. 加滤波电容 |
编译报错 | 1. 头文件路径错误 | 1. 检查include路径 |
5. 进阶功能扩展
// 1. 多通道采集(扫描模式) ADC_InitStruct.ADC_ScanConvMode = ENABLE; ADC_InitStruct.ADC_NbrOfChannel = 3; // 采集3个通道 // 2. 连续转换模式 ADC_InitStruct.ADC_ContinuousConvMode = ENABLE; // 3. 硬件触发(定时器触发) ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T1_CC1; // 4. 添加软件滤波 uint16_t ADC_Filter_Avg(uint8_t ch, uint8_t times) { uint32_t sum = 0; for(uint8_t i = 0; i < times; i++) { sum += ADC_Read_Value(ch); } return sum / times; }6. 学习路径建议
新手入门(1-2天) → 中级应用(3-5天) → 高级应用(1-2周) ↓ ↓ ↓ 1. 单通道采集 1. 多通道扫描 1. DMA高速采集 2. 查询方式 2. 中断方式 2. 双重/三重ADC 3. 电压换算 3. 定时器触发 3. 过采样提高精度 4. 基本调试 4. 软件滤波 4. 与DAC闭环控制记住:ADC是连接模拟世界和数字世界的桥梁。从简单的电压采集开始,逐步掌握多通道、中断、DMA等高级功能,你就能应对各种传感器数据采集需求!