【物联网学习笔记】ADC

前言

本文是本人备赛物联网赛项的学习笔记,主要供本人学习、复习,不是经验分享或教学,若有错误,大佬轻喷。

一、前置知识: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,核心硬件链路如下:

  1. ADC 采集输入引脚:PB4,对应芯片内部ADC_IN3通道
  2. 输入信号:板载滑动电位器输出的模拟电压(范围 0~3.3V)
  3. 参考电压:单片机 VDD,即 3.3V
  4. 调试链路:开发板 USB 口兼顾供电、程序下载、串口打印功能,可直接通过串口助手查看 ADC 采集数值

三、第一阶段:STM32CubeMX 全流程配置

CubeMX 是 ST 官方的图形化配置工具,可自动生成外设初始化代码,避免新手手动配置寄存器出错,以下是分步操作指南。

步骤 1:新建工程,选择目标芯片

  1. 打开 STM32CubeMX,点击首页ACCESS TO MCU SELECTOR进入芯片选择界面
  2. 搜索框输入STM32WLE5CCU6,在右侧列表选中该型号,点击Start Project新建工程

步骤 2:调试接口基础配置(必做,否则程序无法下载)

  1. 左侧System Core下拉菜单,选择SYS
  2. 右侧Debug选项,下拉选择Serial Wire(串行线调试模式,匹配开发板 DAPLink 调试器)
  3. 其余参数保持默认即可

步骤 3:RCC 与时钟树配置(决定外设时钟准确性)

3.1 晶振配置
  1. 左侧System Core下拉菜单,选择RCC
  2. 右侧Low Speed Clock (LSE)选项,下拉选择Crystal/Ceramic Resonator(外部低速晶振,对应板载 32.768kHz 晶振,适配 RTC 功能)
  3. 高速时钟使用芯片内部 MSI RC 时钟,无需额外配置
3.2 时钟树配置
  1. 顶部切换到Clock Configuration时钟树选项卡
  2. 按以下参数配置,确保无红色报错:
    • MSI 时钟范围:RCC_MSIRANGE_11,对应 48MHz
    • 系统时钟源 SYSCLK:选择 MSI,主频 48MHz
    • AHB/APB1/APB2 分频器:全部设置为/1,确保外设时钟均为 48MHz
    • ADC 时钟源:同步 PCLK 时钟,后续配置 2 分频

步骤 4:ADC 外设核心配置(重点)

4.1 引脚功能映射

在芯片引脚预览图中,找到PB4引脚,左键点击,在弹出菜单中选择ADC_IN3,配置完成后引脚会变为绿色高亮状态。

4.2 ADC 全局参数配置
  1. 左侧Analog下拉菜单,选择ADC

  2. 按以下参数配置,和最终生成的初始化代码完全匹配:

    配置项 参数值 新手说明
    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 通道配置
  1. 在 ADC 配置界面下方,找到Regular Channel常规通道配置栏
  2. 配置参数:
    • Channel:ADC_CHANNEL_3(对应 PB4 引脚)
    • Rank:ADC_REGULAR_RANK_1(转换序列第 1 位,单通道仅需配置 1 个序列)
    • Sampling Time:ADC_SAMPLINGTIME_COMMON_1(使用全局配置的 1.5 周期采样时间)

步骤 5:其他必要外设配置

为了实现采集结果串口打印、OLED 显示等功能,同步配置以下外设(和参考工程匹配):

  1. USART2 串口 :Mode 选择Asynchronous异步模式,波特率 115200,8 位数据位、1 位停止位、无校验,引脚 PA2 为 USART2_TX、PA3 为 USART2_RX
  2. I2C1 :用于驱动 OLED 屏幕,Mode 选择I2C,默认参数即可
  3. RTC:实时时钟,开启时钟源,默认参数即可
  4. DMA:开启串口 DMA 接收,默认参数即可

步骤 6:工程管理与代码生成配置(必做)

  1. 顶部切换到Project Manager选项卡
  2. Project 栏目配置
    • Project Name:自定义工程名(严禁使用中文、空格、特殊字符)
    • Project Location:工程保存路径(严禁使用中文路径)
    • Toolchain/IDE:下拉选择MDK-ARM V5(对应 Keil5 开发环境)
  3. 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 串口打印卡死)

  1. 打开 Keil 工程后,点击顶部魔术棒图标Options for Target,打开配置界面
  2. 切换到Target选项卡,勾选Use MicroLIB(Keil 针对嵌入式优化的微型 C 标准库,不勾选会导致 printf 函数卡死)
  3. 点击OK保存配置

步骤 2:新建用户自定义文件 app.c 与 app.h

2.1 新建 app.h 头文件(函数声明与宏定义)
  1. 在 Keil 左侧工程树,右键点击Application/User/Core文件夹,选择Add New Item to Group
  2. 选择Header File (.h),文件名填写app.h,点击Add
  3. 在 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 源文件(功能函数实现)
  1. 同样右键点击Application/User/Core文件夹,选择Add New Item to Group
  2. 选择C File (.c),文件名填写app.c,点击Add
  3. 在 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. 串口助手配置与现象验证

  1. 打开串口调试助手(推荐 SSCOM、野火串口助手)
  2. 端口选择:设备管理器中开发板对应的 COM 口
  3. 串口参数配置(必须和工程一致):
    • 波特率:115200
    • 数据位:8
    • 停止位:1
    • 校验位:无
    • 流控:无
  4. 点击打开串口,即可看到串口持续打印 ADC 采集值
  5. 实验现象 :转动开发板上的滑动电位器,串口打印的 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. 读取转换结果寄存器的值,返回给主函数                               
}
  1. 启动转换HAL_ADC_Start()函数会使能 ADC 外设,按照 CubeMX 配置的参数,启动单次转换。
  2. 等待转换完成HAL_ADC_PollForConversion()函数会检测转换结束标志位(EOC),转换完成后返回 HAL_OK,超时时间 200ms 避免硬件故障导致程序卡死。
  3. 读取结果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);

七、新手高频踩坑点与解决方案

  1. 串口 printf 无输出 / 乱码

    • 解决方案:检查是否勾选Use MicroLIB;串口助手波特率、数据位等参数和工程是否一致;PA2/PA3 引脚是否正确配置为 USART2_TX/RX。
  2. ADC 数值固定不变,不随电位器变化

    • 解决方案:检查 PB4 引脚是否配置为模拟输入模式;硬件接线是否正确,电位器是否正常供电;ADC 是否成功启动,转换函数是否被调用。
  3. 程序卡死在 ADC 等待转换函数中

    • 解决方案:检查 ADC 时钟配置是否正确;CubeMX 中 ADC 参数是否配置错误,特别是触发模式和转换模式;超时时间是否设置过短。
  4. 重新生成 CubeMX 代码后,自己写的代码丢失

    • 解决方案:所有用户代码必须写在/* USER CODE BEGIN X *//* USER CODE END X */注释块之间;自定义功能建议单独放在 app.c/app.h 文件中,不要修改 CubeMX 自动生成的外设初始化代码。
  5. ADC 数值跳动严重,不稳定

    • 解决方案:延长 ADC 采样时间,CubeMX 中将采样时间改为 39.5 周期或更长;在硬件上增加 0.1uF 滤波电容;软件上多次采集取平均值。
相关推荐
lkbhua莱克瓦242 小时前
考研数学零基础学习Day1
学习
Alonse_沃虎电子2 小时前
VOOHU沃虎网络变压器接线核心技术规范与风险防控指南
网络·物联网·产品·方案·电子元器件·网络变压器
foundbug9992 小时前
基于STM32的步进电机加减速程序设计(梯形加减速算法)
stm32·单片机·算法
solicitous2 小时前
遇到一个口头机遇的答辩准备3(ai告诉的要点)
学习·生活
CheerWWW2 小时前
C++学习笔记——this关键字、对象生命周期(栈作用域)、智能指针、复制与拷贝构造函数
c++·笔记·学习
温天仁3 小时前
西门子PLC编程实践教程:工控工程案例学习
开发语言·学习·自动化·php
busideyang3 小时前
嵌入式代码编写规范1.0
单片机·嵌入式
mftang3 小时前
Cortex-M 中断跳转: MCU内部实现原理和流程
单片机·嵌入式硬件·armv8-m
charlie1145141913 小时前
嵌入式C++教程实战之Linux下的单片机编程:从零搭建 STM32 开发工具链(5):调试进阶篇 —— 从 printf 到完整 GDB 调试环境
linux·c++·单片机·学习·嵌入式·c