前言
本文是本人备赛物联网赛项的学习笔记,主要供本人学习、复习,不是经验分享或教学,若有错误,大佬轻喷。
一、前置知识:ADC 核心基础原理
1.1 什么是 ADC
ADC 即模拟数字转换器(Analog-to-Digital Converter) ,是 STM32 单片机内置的核心外设,作用是把连续变化的模拟量 (比如电位器电压、传感器输出信号)转换成单片机能识别的数字量,是嵌入式系统实现物理信号采集的核心桥梁。
1.2 核心基础概念
- 模拟量:在连续范围内可取任意值的物理量,比如电位器输出的 0~3.3V 电压,数值是连续无间隔的。
- 数字量 :只能取有限离散值的量,比如 STM32 的 12 位 ADC,输出值范围是
0~4095,只有 4096 个离散数值。 - 分辨率:本文使用 12 位分辨率,代表 ADC 能把参考电压(3.3V)分成 2¹²=4096 个最小刻度,每个刻度对应电压约为 3.3V/4095≈0.8mV。
- 转换模式 :本文使用单次转换模式,即 ADC 每次收到触发指令,只执行一次转换,完成后自动停止,适合低速、低功耗的采集场景,也是新手入门最易理解的模式。
- 对齐方式 :本文使用右对齐,转换结果直接存在数据寄存器低 12 位,无需额外移位处理,新手使用更方便。
二、硬件说明:开发板与 ADC 采集链路
本文基于CT127C 蓝桥杯物联网开发板,主控为 STM32WLE5CCU6,核心硬件链路如下:
- ADC 采集输入引脚:
PB4,对应芯片内部ADC_IN3通道 - 输入信号:板载滑动电位器输出的模拟电压(范围 0~3.3V)
- 参考电压:单片机 VDD,即 3.3V
- 调试链路:开发板 USB 口兼顾供电、程序下载、串口打印功能,可直接通过串口助手查看 ADC 采集数值

三、第一阶段:STM32CubeMX 全流程配置
CubeMX 是 ST 官方的图形化配置工具,可自动生成外设初始化代码,避免新手手动配置寄存器出错,以下是分步操作指南。
步骤 1:新建工程,选择目标芯片
- 打开 STM32CubeMX,点击首页
ACCESS TO MCU SELECTOR进入芯片选择界面 - 搜索框输入
STM32WLE5CCU6,在右侧列表选中该型号,点击Start Project新建工程
步骤 2:调试接口基础配置(必做,否则程序无法下载)
- 左侧
System Core下拉菜单,选择SYS - 右侧
Debug选项,下拉选择Serial Wire(串行线调试模式,匹配开发板 DAPLink 调试器) - 其余参数保持默认即可
步骤 3:RCC 与时钟树配置(决定外设时钟准确性)
3.1 晶振配置
- 左侧
System Core下拉菜单,选择RCC - 右侧
Low Speed Clock (LSE)选项,下拉选择Crystal/Ceramic Resonator(外部低速晶振,对应板载 32.768kHz 晶振,适配 RTC 功能) - 高速时钟使用芯片内部 MSI RC 时钟,无需额外配置
3.2 时钟树配置
- 顶部切换到
Clock Configuration时钟树选项卡 - 按以下参数配置,确保无红色报错:
- MSI 时钟范围:
RCC_MSIRANGE_11,对应 48MHz - 系统时钟源 SYSCLK:选择 MSI,主频 48MHz
- AHB/APB1/APB2 分频器:全部设置为
/1,确保外设时钟均为 48MHz - ADC 时钟源:同步 PCLK 时钟,后续配置 2 分频
- MSI 时钟范围:
步骤 4:ADC 外设核心配置(重点)
4.1 引脚功能映射
在芯片引脚预览图中,找到PB4引脚,左键点击,在弹出菜单中选择ADC_IN3,配置完成后引脚会变为绿色高亮状态。

4.2 ADC 全局参数配置
-
左侧
Analog下拉菜单,选择ADC -
按以下参数配置,和最终生成的初始化代码完全匹配:
配置项 参数值 新手说明 Clock Prescaler ADC_CLOCK_SYNC_PCLK_DIV2 ADC 时钟 2 分频,最终 24MHz,符合芯片时钟要求 Resolution ADC_RESOLUTION_12B 12 位分辨率,对应 0~4095 采集范围 Data Align ADC_DATAALIGN_RIGHT 数据右对齐,无需额外移位处理 Scan Conv Mode ADC_SCAN_DISABLE 关闭扫描模式,单通道采集无需扫描 Continuous Conv Mode DISABLE 关闭连续转换,使用单次转换模式 EOC Selection ADC_EOC_SINGLE_CONV 单次转换结束标志位 External Trig Conv ADC_SOFTWARE_START 软件触发转换,代码手动控制采集 SamplingTime Common1 ADC_SAMPLETIME_1CYCLE_5 采样时间 1.5 个时钟周期,入门场景足够使用 Oversampling Mode DISABLE 关闭过采样,简化入门配置

4.3 ADC 通道配置
- 在 ADC 配置界面下方,找到
Regular Channel常规通道配置栏 - 配置参数:
- Channel:
ADC_CHANNEL_3(对应 PB4 引脚) - Rank:
ADC_REGULAR_RANK_1(转换序列第 1 位,单通道仅需配置 1 个序列) - Sampling Time:
ADC_SAMPLINGTIME_COMMON_1(使用全局配置的 1.5 周期采样时间)
- Channel:

步骤 5:其他必要外设配置
为了实现采集结果串口打印、OLED 显示等功能,同步配置以下外设(和参考工程匹配):
- USART2 串口 :Mode 选择
Asynchronous异步模式,波特率 115200,8 位数据位、1 位停止位、无校验,引脚 PA2 为 USART2_TX、PA3 为 USART2_RX - I2C1 :用于驱动 OLED 屏幕,Mode 选择
I2C,默认参数即可 - RTC:实时时钟,开启时钟源,默认参数即可
- DMA:开启串口 DMA 接收,默认参数即可
步骤 6:工程管理与代码生成配置(必做)
- 顶部切换到
Project Manager选项卡 - Project 栏目配置 :
- Project Name:自定义工程名(严禁使用中文、空格、特殊字符)
- Project Location:工程保存路径(严禁使用中文路径)
- Toolchain/IDE:下拉选择
MDK-ARM V5(对应 Keil5 开发环境)
- Code Generator 栏目配置 :
- 核心勾选:
Generate peripheral initialization as a pair of '.c/.h' files per peripheral(每个外设生成独立的.c/.h 文件,方便代码管理) - 保持勾选:
Keep User Code when re-generating(重新生成代码时保留用户编写的代码) - 其余参数保持默认
- 核心勾选:
步骤 7:生成工程代码
点击界面右上角GENERATE CODE按钮,等待代码生成完成,在弹出的弹窗中点击Open Project,直接打开 Keil MDK 工程。
四、第二阶段:Keil MDK 工程配置与代码编写
本阶段将实现 ADC 采集的功能代码,所有用户自定义代码均放在新建的app.c/app.h文件中,避免重新生成 CubeMX 代码时被覆盖,同时将教程中的zsdz前缀全部替换为app,方便个人复用。
步骤 1:Keil 工程基础配置(必做,解决 printf 串口打印卡死)
- 打开 Keil 工程后,点击顶部魔术棒图标
Options for Target,打开配置界面 - 切换到
Target选项卡,勾选Use MicroLIB(Keil 针对嵌入式优化的微型 C 标准库,不勾选会导致 printf 函数卡死) - 点击
OK保存配置
步骤 2:新建用户自定义文件 app.c 与 app.h
2.1 新建 app.h 头文件(函数声明与宏定义)
- 在 Keil 左侧工程树,右键点击
Application/User/Core文件夹,选择Add New Item to Group - 选择
Header File (.h),文件名填写app.h,点击Add - 在 app.h 中写入以下代码,包含所有功能函数的声明:
cpp
#ifndef __APP_H
#define __APP_H
// 包含必要的HAL库头文件与外设头文件
#include "main.h"
#include "string.h"
#include "stdio.h"
#include "gpio.h"
#include "oled.h"
/************************* 宏定义 *************************/
#define APP_ON 1
#define APP_OFF 2
#define APP_TOGGLE 3
// OLED显示行定义
#define APP_OLED_LINE1 0
#define APP_OLED_LINE2 2
/************************* 结构体定义 *************************/
// RTC时间结构体
typedef struct
{
unsigned char Year;
unsigned char Month;
unsigned char WeekDay;
unsigned char Date;
unsigned char Hours;
unsigned char Minutes;
unsigned char Seconds;
}RTC_APP;
/************************* 全局变量声明 *************************/
extern RTC_APP rtc_value;
/************************* 函数声明 *************************/
// LED控制函数
void app_write_AL(unsigned short int ALx,unsigned char state);
// 读取LED状态
unsigned char app_read_AL(unsigned short int ALx);
// 读取按键状态
unsigned char app_read_ASW(GPIO_TypeDef *ASWx);
// 串口发送字符串
void app_tx_UART(const unsigned char *Data);
// 串口空闲中断接收处理
void app_UART_IDLE_rx(void);
// 串口接收循环处理
void app_UART_rx_loop(void);
// 串口接收数据解析
void app_UART_rx_deal(void);
// 读取RTC时间
void app_read_RTC(void);
// OLED底层写入函数
void OLED_Write(unsigned char Type, unsigned char Data);
// OLED字符串显示函数
void app_write_OLED(unsigned char Lin, unsigned char *Data);
// ADC采集读取函数(核心)
unsigned short int app_read_ADC(void);
// 外设初始化函数
void app_init(void);
#endif
2.2 新建 app.c 源文件(功能函数实现)
- 同样右键点击
Application/User/Core文件夹,选择Add New Item to Group - 选择
C File (.c),文件名填写app.c,点击Add - 在 app.c 中写入以下代码,完整实现所有功能,重点关注核心的 ADC 采集函数:
cpp
#include "app.h"
/************************* 全局变量定义 *************************/
#define UART_RX_BUFFER_LEN 100 // 串口接收缓存区最大长度
RTC_APP rtc_value;
unsigned char g_u8uart_rx_len; // 接收到的一帧数据长度
unsigned char g_u8uart_rx_end_flag; // 串口一帧数据接收完成标志位
unsigned char g_u8uart_rx_buffer[UART_RX_BUFFER_LEN]; // 串口接收缓存数组
/************************* 功能函数实现 *************************/
/**
* @brief LED状态控制函数
* @param ALx: LED引脚编号
* @param state: LED状态 APP_ON/APP_OFF/APP_TOGGLE
* @retval 无
*/
void app_write_AL(unsigned short int ALx,unsigned char state)
{
switch (state)
{
case APP_ON:
HAL_GPIO_WritePin(GPIOB, ALx, GPIO_PIN_RESET);
break;
case APP_OFF:
HAL_GPIO_WritePin(GPIOB, ALx, GPIO_PIN_SET);
break;
case APP_TOGGLE:
HAL_GPIO_TogglePin(GPIOB, ALx);
break;
default:
break;
}
}
/**
* @brief 读取LED当前状态
* @param ALx: LED引脚编号
* @retval LED状态 APP_ON/APP_OFF
*/
unsigned char app_read_AL(unsigned short int ALx)
{
unsigned char state=HAL_GPIO_ReadPin(GPIOB, ALx);
if(state == GPIO_PIN_SET)
return APP_OFF;
else
return APP_ON;
}
/**
* @brief 按键状态读取函数(带消抖)
* @param ASWx: 按键对应的GPIO端口
* @retval 按键状态 APP_ON/APP_OFF
*/
unsigned char app_read_ASW(GPIO_TypeDef *ASWx)
{
unsigned char state = APP_OFF;
if(HAL_GPIO_ReadPin(ASWx,GPIO_PIN_8) == GPIO_PIN_RESET)
{
HAL_Delay(20); // 延时消抖
if(HAL_GPIO_ReadPin(ASWx,GPIO_PIN_8) == GPIO_PIN_RESET)
{
state = APP_ON;
while(HAL_GPIO_ReadPin(ASWx,GPIO_PIN_8) == GPIO_PIN_RESET); // 等待按键松开
}
}
return state;
}
/**
* @brief 串口发送字符串函数
* @param Data: 要发送的字符串首地址
* @retval 无
*/
void app_tx_UART(const unsigned char *Data)
{
extern UART_HandleTypeDef huart2;
HAL_UART_Transmit(&huart2,Data,strlen((const char *)Data),0xFFFF);
}
/**
* @brief printf函数重定向,实现printf串口输出
* @note 重写C库fputc底层函数,将printf输出指向USART2
*/
int fputc(int ch,FILE *f)
{
extern UART_HandleTypeDef huart2;
HAL_UART_Transmit(&huart2,(unsigned char*)&ch,1,0xFFFF);
return ch;
}
/**
* @brief 串口空闲中断处理函数,判断一帧数据接收完成
* @note 需在串口中断服务函数中调用
*/
void app_UART_IDLE_rx(void)
{
extern UART_HandleTypeDef huart2;
extern DMA_HandleTypeDef hdma_usart2_rx;
if(__HAL_UART_GET_FLAG(&huart2,UART_FLAG_IDLE)==SET) // 检测到空闲标志位
{
__HAL_UART_CLEAR_IDLEFLAG(&huart2); // 清除空闲标志位
HAL_UART_DMAStop(&huart2); // 停止DMA传输
// 计算接收到的数据长度:总缓存长度 - 剩余未传输长度
g_u8uart_rx_len=UART_RX_BUFFER_LEN -__HAL_DMA_GET_COUNTER(&hdma_usart2_rx);
g_u8uart_rx_end_flag=1; // 置位接收完成标志
}
}
/**
* @brief 串口接收循环处理函数,需在main主循环中调用
* @retval 无
*/
void app_UART_rx_loop(void)
{
extern UART_HandleTypeDef huart2;
if(g_u8uart_rx_end_flag) // 判断是否接收完成一帧数据
{
app_UART_rx_deal(); // 处理接收到的数据
// 清空标志与长度,重新开启DMA接收
g_u8uart_rx_len=0;
g_u8uart_rx_end_flag=0;
HAL_UART_Receive_DMA(&huart2,g_u8uart_rx_buffer,UART_RX_BUFFER_LEN);
}
}
/**
* @brief 串口接收数据解析函数,本例实现串口回环功能
* @retval 无
*/
void app_UART_rx_deal(void)
{
extern UART_HandleTypeDef huart2;
HAL_UART_Transmit(&huart2,g_u8uart_rx_buffer,g_u8uart_rx_len,0xFFFF);
}
/**
* @brief 读取RTC实时时钟时间
* @retval 无
*/
void app_read_RTC(void)
{
extern RTC_HandleTypeDef hrtc;
RTC_DateTypeDef data_value;
RTC_TimeTypeDef time_value;
// 注意:HAL库必须先读时间,再读日期,否则会出错
HAL_RTC_GetTime(&hrtc,&time_value,RTC_FORMAT_BIN);
HAL_RTC_GetDate(&hrtc,&data_value,RTC_FORMAT_BIN);
// 赋值到全局结构体
rtc_value.Year = data_value.Year;
rtc_value.Month = data_value.Month;
rtc_value.WeekDay = data_value.WeekDay;
rtc_value.Date = data_value.Date;
rtc_value.Hours = time_value.Hours;
rtc_value.Minutes = time_value.Minutes;
rtc_value.Seconds = time_value.Seconds;
}
/**
* @brief OLED底层写入函数
* @param Type: 命令/数据标志 0x00=命令 0x40=数据
* @param Data: 要写入的字节
* @retval 无
*/
void OLED_Write(unsigned char Type, unsigned char Data)
{
extern I2C_HandleTypeDef hi2c1;
unsigned char pData[2];
pData[0] = Type;
pData[1] = Data;
HAL_I2C_Master_Transmit(&hi2c1, 0x78, pData, 2, 10);
}
/**
* @brief OLED显示字符串函数
* @param Lin: 显示行号
* @param Data: 要显示的字符串
* @retval 无
*/
void app_write_OLED(unsigned char Lin, unsigned char *Data)
{
OLED_ShowString(0,Lin,Data,16);
}
/**
* @brief ADC采集读取函数(核心功能)
* @retval 12位ADC原始采集值(范围0~4095)
*/
unsigned short int app_read_ADC(void)
{
extern ADC_HandleTypeDef hadc;
HAL_ADC_Start(&hadc); // 软件启动ADC转换
// 等待转换完成,超时时间200ms,防止程序卡死
while(HAL_ADC_PollForConversion(&hadc, 200) != HAL_OK);
return HAL_ADC_GetValue(&hadc); // 读取转换结果并返回
}
/**
* @brief 用户外设初始化函数,需在main函数中调用
* @retval 无
*/
void app_init(void)
{
extern UART_HandleTypeDef huart2;
__HAL_UART_ENABLE_IT(&huart2, UART_IT_IDLE); // 使能串口空闲中断
HAL_UART_Receive_DMA(&huart2,g_u8uart_rx_buffer,UART_RX_BUFFER_LEN); // 开启串口DMA接收
OLED_Init(); // 初始化OLED屏幕
}
步骤 3:main.c 函数修改
打开工程中Application/User/Core文件夹下的main.c文件,按以下步骤修改,所有用户代码必须写在/* USER CODE BEGIN X */和/* USER CODE END X */之间,否则重新生成 CubeMX 代码时会被覆盖。
3.1 包含自定义头文件
找到/* USER CODE BEGIN Includes */代码块,添加 app.h 头文件:
cpp
/* USER CODE BEGIN Includes */
#include "app.h"
#include "oled.h"
/* USER CODE END Includes */
3.2 主函数初始化与主循环编写
找到 main 函数,修改为以下内容,重点是初始化 app 模块,主循环中读取 ADC 并串口打印:
cpp
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init();
MX_USART2_UART_Init();
MX_RTC_Init();
MX_I2C1_Init();
MX_ADC_Init();
/* USER CODE BEGIN 2 */
app_init(); // 用户外设初始化
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
// 读取ADC原始值并通过串口打印
printf("ADC采集原始值:%d\r\n",app_read_ADC());
// 延时100ms,控制采集频率
HAL_Delay(100);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
五、第三阶段:编译下载与实验现象验证
1. 代码编译
点击 Keil 界面顶部的Build按钮(编译图标),等待编译完成,控制台提示0 Error(s), 0 Warning(s)即为编译成功。
2. 程序下载
用 USB 线连接开发板与电脑,点击 Keil 界面顶部的Download按钮(下载图标),等待程序下载完成,开发板会自动运行程序。
3. 串口助手配置与现象验证
- 打开串口调试助手(推荐 SSCOM、野火串口助手)
- 端口选择:设备管理器中开发板对应的 COM 口
- 串口参数配置(必须和工程一致):
- 波特率:115200
- 数据位:8
- 停止位:1
- 校验位:无
- 流控:无
- 点击
打开串口,即可看到串口持续打印 ADC 采集值 - 实验现象 :转动开发板上的滑动电位器,串口打印的 ADC 值会在
0~4095之间同步变化,电位器旋到最小时数值接近 0,旋到最大时数值接近 4095。
六、核心代码原理解析
核心 ADC 采集函数执行流程
cpp
unsigned short int app_read_ADC(void)
{
extern ADC_HandleTypeDef hadc;
HAL_ADC_Start(&hadc); // 1. 软件触发,启动ADC单次转换
// 2. 阻塞等待转换完成,超时时间200ms,防止程序卡死
while(HAL_ADC_PollForConversion(&hadc, 200) != HAL_OK);
return HAL_ADC_GetValue(&hadc); // 3. 读取转换结果寄存器的值,返回给主函数
}
- 启动转换 :
HAL_ADC_Start()函数会使能 ADC 外设,按照 CubeMX 配置的参数,启动单次转换。 - 等待转换完成 :
HAL_ADC_PollForConversion()函数会检测转换结束标志位(EOC),转换完成后返回 HAL_OK,超时时间 200ms 避免硬件故障导致程序卡死。 - 读取结果 :
HAL_ADC_GetValue()函数会读取 ADC 数据寄存器中的转换结果,12 位右对齐,直接返回 0~4095 的原始值。
电压换算拓展
如果需要将原始 ADC 值换算成实际电压,可使用以下公式:
cpp
// 3.3V为参考电压,4095为12位ADC的最大值
float adc_voltage = (float)app_read_ADC() * 3.3f / 4095.0f;
printf("ADC采集电压:%.2f V\r\n", adc_voltage);
七、新手高频踩坑点与解决方案
-
串口 printf 无输出 / 乱码
- 解决方案:检查是否勾选
Use MicroLIB;串口助手波特率、数据位等参数和工程是否一致;PA2/PA3 引脚是否正确配置为 USART2_TX/RX。
- 解决方案:检查是否勾选
-
ADC 数值固定不变,不随电位器变化
- 解决方案:检查 PB4 引脚是否配置为模拟输入模式;硬件接线是否正确,电位器是否正常供电;ADC 是否成功启动,转换函数是否被调用。
-
程序卡死在 ADC 等待转换函数中
- 解决方案:检查 ADC 时钟配置是否正确;CubeMX 中 ADC 参数是否配置错误,特别是触发模式和转换模式;超时时间是否设置过短。
-
重新生成 CubeMX 代码后,自己写的代码丢失
- 解决方案:所有用户代码必须写在
/* USER CODE BEGIN X */和/* USER CODE END X */注释块之间;自定义功能建议单独放在 app.c/app.h 文件中,不要修改 CubeMX 自动生成的外设初始化代码。
- 解决方案:所有用户代码必须写在
-
ADC 数值跳动严重,不稳定
- 解决方案:延长 ADC 采样时间,CubeMX 中将采样时间改为 39.5 周期或更长;在硬件上增加 0.1uF 滤波电容;软件上多次采集取平均值。