ADC中断模式

Tutorial: 电子电路 Category: 嵌入式 Published: 2026-08-29 08:09:36 Views: 0 Likes: 0 Comments: 0

149.6.6 ADC中断模式

/* 配套工程 KEIL/6ADC2_4 的 main.c 核心运行流程。
 * 本节实际完成的是 ADC1 查询采样到 ADC2 查询采样的移植;没有启动 ADC 中断。 */
#include "main.h"
#include "gpio.h"
#include "uart.h"
#include "adc.h"

uint16_t AD_Buf[4];
uint8_t i = 0;
void SystemClock_Config(void);

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    Init_GPIO();
    Init_UART1();
    Init_ADC2();

    while (1)
    {
        for (i = 0; i < 4; i++)
        {
            HAL_ADC_Start(&hadc2);               // 非连续模式下触发下一个规则序列 Rank
            HAL_ADC_PollForConversion(&hadc2, 10);
            AD_Buf[i] = HAL_ADC_GetValue(&hadc2);
            printf("ADC2 Channel:%d: V:%1.2fv Value:%d\\n\\r",
                   i + 1, AD_Buf[i] * 3.3f / 4096.0f, AD_Buf[i]);
        }
        HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
    }
}

本节范围校正:原始标题和两个一级标题都写为“ADC中断模式”,但视频及 KEIL/6ADC2_4 配套工程没有 HAL_ADC_Start_IT()ADCx_2_IRQHandler()HAL_ADC_ConvCpltCallback();实际内容是 ADC 查询模式从 ADC1 移植到 ADC2,并排查工程、串口问题。ADC 中断模式应留待后续包含这些要素的课程,不应把本节代码误认为中断程序。

一、ADC中断模式

1.1 查询模式ADC2使用

  • 视频从原有四通道 ADC1 查询工程复制出新工程,在 Core/Src 建立 adc.c,在 Core/Inc 建立 adc.h,并将 adc.c 加入 Keil 工程的编译组。文件出现在资源管理器中不等于已参与构建,必须同时检查工程的 Source Group。
  • adc.h 使用包含保护,避免同一翻译单元多次展开它:
#ifndef __ADC_H__
#define __ADC_H__

#include "main.h"       // 间接得到 HAL 的 ADC_HandleTypeDef 定义

extern ADC_HandleTypeDef hadc2;
void Init_ADC2(void);

#endif
  • 头文件放的是声明adc.c 放的是定义extern ADC_HandleTypeDef hadc2; 只声明变量由别处定义,不分配存储;实际定义是 ADC_HandleTypeDef hadc2;。函数 void Init_ADC2(void); 是函数声明,函数体必须在某个被编译的 .c 文件中存在。
  • main.c 包含 adc.h 后才能看到 hadc2Init_ADC2() 的声明;adc.c 也应先包含自己的 adc.h,这样声明和定义不一致时编译器能尽早发现。
  • 本工程初始化顺序是 HAL_Init()、系统时钟、GPIO、USART1、ADC2。ADC2 的 GPIO 模拟输入和 ADC2 时钟使能由 HAL_ADC_Init() 调用的 HAL_ADC_MspInit() 完成,不能因为 Init_GPIO() 已经开启了 GPIOA 时钟就省略 ADC 的 MSP 配置。
  • 查询流程为:启动一次规则组转换 HAL_ADC_Start(),等待 EOC HAL_ADC_PollForConversion(),再用 HAL_ADC_GetValue() 读取数据寄存器。HAL_ADC_GetValue() 返回的是 12 位右对齐码值 0~4095,不是电压值。
  • 本工程设置 ScanConvMode=ENABLE、总转换数为 4、DiscontinuousConvMode=ENABLENbrOfDiscConversion=1。因此每次软件启动只转换一个 Rank;循环四次会依次得到 Rank1 到 Rank4。若关闭非连续模式,一次启动会跑完整个四通道序列,不能再按本循环的方式一项一项取值。

1.2 DMA模式ADC2使用

本小节标题来自原始转写,但实际视频没有完成 ADC2 的 DMA 程序,配套工程也没有调用 HAL_ADC_Start_DMA()。对 STM32F103 而言,ADC2 没有独立 DMA 请求;参考手册说明只有 ADC1、ADC3 具备 DMA 功能,ADC2 数据需要双 ADC 模式并借助 ADC1 的 DMA。把“ADC1 改 ADC2”理解为普通 DMA 单通道移植是错误的。本节应以查询模式 ADC2 为准;ADC3 的 DMA 移植见上一节。

1.2.1 ADC1改ADC2需修改的参数
  • ADC_HandleTypeDef 是 HAL 句柄的类型,不能为了“改 ADC2”而把类型名改成 ADC2_HandleTypeDef。本工程保留类型名,定义具体句柄变量 ADC_HandleTypeDef hadc2;
  • ADC1ADC2ADC3 也不是用户自定义的“结构体变量名”。在设备头文件中,它们是指向各自 ADC_TypeDef 寄存器块的宏,底层对应外设基地址。hadc2.Instance = ADC2; 才是在选择 ADC2 的寄存器组。
  • 移植时要成组核对,而不是全局替换字符串:hadc2.Instance = ADC2HAL_ADC_MspInit() 中的 adcHandle->Instance == ADC2__HAL_RCC_ADC2_CLK_ENABLE()、校准和启动函数的 &hadc2 必须一致。只改变量名不会切换硬件外设。
  • 若是 ADC-DMA 工程,HAL 中分别有 HAL_DMA_Init()HAL_ADC_Start_DMA();本配套源码并不存在通用的 ADC_DMA_Init()。而且 ADC2 不能像 ADC1/ADC3 一样独立绑定 DMA 通道,不能照抄该调用。
1.2.2 ADC结构体与寄存器关系
  • 芯片头文件以 ADC_TypeDef 描述一组 ADC 寄存器的布局,ADC1ADC2ADC3 是三组不同寄存器地址的入口。ADC_HandleTypeDef 则是 HAL 在软件中维护的句柄,除 Instance 外还保存状态、锁和配置。
  • 由 HAL 函数接收的是 ADC_HandleTypeDef *,例如 HAL_ADC_Start(&hadc2)。HAL 再通过 hadc2.Instance 访问 ADC2 的寄存器,所以“改为 ADC2”不能只改打印文字或变量名。
  • ADC1 与 ADC2 的通道编号相同不代表可以随意同时使用同一模拟引脚。PA1 的模拟电压可以接到 ADC1_IN1 和 ADC2_IN1 的模拟输入通路,但并行采样会改变输入负载和采样时刻,应按芯片手册与实际阻抗评估。
1.2.3 ADC实例与寄存器赋值
  • hadc2.Instance = ADC2; 指向 ADC2 的寄存器块;随后 HAL_ADC_Init(&hadc2) 会对这组寄存器写入扫描、非连续、触发源、对齐方式和规则序列长度等配置。
  • 规则通道配置中的 sConfig.Channel 选择物理 ADC 通道,sConfig.Rank 决定扫描顺序。两者不能混淆:本工程为 Channel1/2/3/4 分别配置 Rank1/2/3/4。
  • HAL_ADC_MspInit() 是 HAL 初始化钩子。该工程用 if (adcHandle->Instance == ADC2) 把 ADC2 时钟和 PA1~PA4 的模拟模式放在 ADC2 初始化路径中。源码内的 “ADC1 clock enable”“ADC1 GPIO Configuration” 是复制后遗留的注释,实际调用的是 ADC2 时钟宏,注释应更正为 ADC2。
1.2.4 ADC2初始化配置

配套工程的关键配置如下,HAL_ADCEx_Calibration_Start() 放在所有规则通道配置完成后执行:

void Init_ADC2(void)
{
    ADC_ChannelConfTypeDef sConfig = {0};

    hadc2.Instance = ADC2;
    hadc2.Init.ScanConvMode = ADC_SCAN_ENABLE;
    hadc2.Init.ContinuousConvMode = DISABLE;
    hadc2.Init.DiscontinuousConvMode = ENABLE;
    hadc2.Init.NbrOfDiscConversion = 1;
    hadc2.Init.ExternalTrigConv = ADC_SOFTWARE_START;
    hadc2.Init.DataAlign = ADC_DATAALIGN_RIGHT;
    hadc2.Init.NbrOfConversion = 4;
    if (HAL_ADC_Init(&hadc2) != HAL_OK) Error_Handler();

    sConfig.SamplingTime = ADC_SAMPLETIME_239CYCLES_5;
    sConfig.Channel = ADC_CHANNEL_1; sConfig.Rank = ADC_REGULAR_RANK_1;
    if (HAL_ADC_ConfigChannel(&hadc2, &sConfig) != HAL_OK) Error_Handler();
    sConfig.Channel = ADC_CHANNEL_2; sConfig.Rank = ADC_REGULAR_RANK_2;
    if (HAL_ADC_ConfigChannel(&hadc2, &sConfig) != HAL_OK) Error_Handler();
    sConfig.Channel = ADC_CHANNEL_3; sConfig.Rank = ADC_REGULAR_RANK_3;
    if (HAL_ADC_ConfigChannel(&hadc2, &sConfig) != HAL_OK) Error_Handler();
    sConfig.Channel = ADC_CHANNEL_4; sConfig.Rank = ADC_REGULAR_RANK_4;
    if (HAL_ADC_ConfigChannel(&hadc2, &sConfig) != HAL_OK) Error_Handler();

    if (HAL_ADCEx_Calibration_Start(&hadc2) != HAL_OK) Error_Handler();
}
  • “所有初始化参数都不用改”的说法不严谨。扫描模式、序列长度、非连续模式、触发源、采样时间要由采集方式决定;只有当 ADC2 与旧 ADC1 实验目标完全相同时,才能复用这些数值。
  • 必须修正 ADC 时钟:工程的 SystemClock_Config() 将 PCLK2 设为 72 MHz,而 temp.ioc 显示 ADC 时钟为 36 MHz,并没有显式配置 ADC 分频。STM32F103 的 ADC 输入时钟不得超过 14 MHz,默认 /2 的 36 MHz 超出规格。应在系统时钟配置中加入:
RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};

PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_ADC;
PeriphClkInit.AdcClockSelection = RCC_ADCPCLK2_DIV6; // 72 / 6 = 12 MHz
if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
{
    Error_Handler();
}

这里的 /6 是硬件规格约束,不是为了解决串口打印而随意放慢。若 PCLK2 不同,应重新计算并保证 ADC 时钟不超过 14 MHz。

  • 校准会让 ADC 进行内部校正,HAL 的 HAL_ADCEx_Calibration_Start() 本身有返回值;源码忽略它会使校准失败后仍继续采样,精校版应检查 HAL_OK
1.2.5 ADC通道与采样时间设置
规则 RankADC2 通道GPIO 引脚本工程用途
Rank1Channel1PA1AD_Buf[0]
Rank2Channel2PA2AD_Buf[1]
Rank3Channel3PA3AD_Buf[2]
Rank4Channel4PA4AD_Buf[3]
  • PA1~PA4 必须配置为 GPIO_MODE_ANALOG。模拟模式会关闭数字输入缓冲,避免数字电路切换影响高阻模拟信号;它不是普通的上拉输入或推挽输出模式。
  • 原文的 “23.95 个时钟周期” 是识别错误。源码为 ADC_SAMPLETIME_239CYCLES_5,即 239.5 个 ADC 时钟周期。对 12 位转换,转换时间近似为 (采样周期 + 12.5) / fADC;在 12 MHz ADC 时钟下约为 (239.5 + 12.5) / 12 MHz = 21 us
  • 采样时间越长,采样电容越有时间通过输入源阻抗充电,更适合电位器、分压电阻等高阻信号;代价是采样速度降低。它不应被当作“串口打印不出来”的通用修复手段,串口异常应先检查复用引脚、波特率、接线和发送缓冲区。
  • 电压计算 V = code * Vref / 4096 使用 1 LSB 为 Vref/4096 的换算。满量程码 4095 对应接近而非数学上恰好等于 3.3 V;精度还受 VDDA、VREF+、量化误差、校准和输入误差影响。
  • HAL_ADC_PollForConversion(&hadc2, 10) 的 10 是毫秒超时,不是 10 个 ADC 周期。对约 21 us 的转换而言余量很大;真实程序必须判断其返回值,超时后直接读取会把旧数据误认为新数据。
1.2.6 ADC配置中的问题排查
  • 视频所称的“小开关”是 stm32f1xx_hal_conf.h 中的 HAL_ADC_MODULE_ENABLED。它决定 stm32f1xx_hal.h 是否包含 ADC HAL 的声明;未定义时,编辑器和编译器会看不到 ADC_HandleTypeDefHAL_ADC_Init() 等 ADC 接口。
  • 仅打开 HAL_ADC_MODULE_ENABLED 还不够。工程还必须编译 stm32f1xx_hal_adc.cstm32f1xx_hal_adc_ex.c,否则可以通过编译却会在链接阶段报 HAL_ADC_InitHAL_ADCEx_Calibration_Start 等未定义符号。
  • 本工程的 temp.uvprojx 已包含 adc.cstm32f1xx_hal_adc.cstm32f1xx_hal_adc_ex.cstm32f1xx_hal_conf.h 也已定义 HAL_ADC_MODULE_ENABLED。这是它能成功构建的必要组合。
  • 还需核对芯片型号是否确有 ADC2、HAL_ADC_MspInit() 是否走入 ADC2 分支、ADC2 时钟是否开启、PA1~PA4 是否为模拟模式,以及 ADC 时钟是否在 14 MHz 以内。只根据 IDE 的红线定位“开关”不够可靠。
1.2.7 ADC模块使能配置
  • 包含链为:adc.c -> adc.h -> main.h -> stm32f1xx_hal.h -> stm32f1xx_hal_conf.h -> stm32f1xx_hal_adc.h。所以在 adc.h 包含 main.h 后,ADC 句柄类型可见;若换用不同工程模板,应确认这条包含链仍成立。
  • HAL_ADC_MODULE_ENABLED预处理开关,决定头文件和库源码里的 ADC 代码是否参与编译;__HAL_RCC_ADC2_CLK_ENABLE()运行时外设时钟开关,决定 ADC2 寄存器是否有时钟。两者处于不同层次,缺任一个都不能正常工作。
  • 建议每次移植按“预处理宏、源文件已加入工程、头文件可见、MSP 时钟/GPIO、时钟树、编译输出”顺序检查,而不是仅在某处“打开 ADC”。

1.3 编译时函数未定义错误及其解决

1.3.1 编译时函数未定义错误展示
  • “函数未定义”要先区分阶段:编译阶段常见提示是未声明、未知类型或隐式声明;链接阶段常见提示是 undefined symbol。两者成因和解决方法不同,不能一律加 extern
  • 视频中 HAL_ADC_InitHAL_ADC_ConfigChannelHAL_ADC_Start 等 ADC 接口报错,根因是新工程尚未把 ADC HAL 支持完整接入,而非这些 API 在 STM32F1 HAL 中不存在。
  • Init_ADC2()hadc2main.c 处报错,应检查 #include "adc.h"、函数声明和 extern 变量声明;若 HAL API 报错,应继续检查 HAL 模块开关和 HAL 源文件。
1.3.2 编译时函数未定义错误原因分析
  • 缺少 HAL_ADC_MODULE_ENABLED 或缺少 ADC 相关头文件时,编译器没有 API 声明,会产生“未声明/未知类型”错误。
  • stm32f1xx_hal_adc.cstm32f1xx_hal_adc_ex.c 未加入工程时,声明仍可能存在,编译会通过,但链接器找不到函数实现。这是“头文件存在却仍未定义”的典型原因。
  • 用户自写的 adc.c 没有加入工程、函数名与头文件不一致,或 hadc2 定义为 static 却想在 main.c 使用,都会造成自己的模块接口不可用。
  • HAL 函数的名称不是把 ADC1 字样替换为 ADC2 得出的。比如 HAL_ADC_Start() 的参数是句柄地址,ADC1/ADC2 的差异由 hadcX.Instance 决定;应查看函数原型,而不是改库函数名称。
1.3.3 编译时函数未定义错误的解决: 添加库文件
  • 在 Keil 中将 Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_hal_adc.cstm32f1xx_hal_adc_ex.c 加入同一 Target 的编译组,并检查 Include Paths 能找到 Drivers/STM32F1xx_HAL_Driver/Inc 和 CMSIS 头文件目录。
  • stm32f1xx_hal_conf.h 中启用:
#define HAL_ADC_MODULE_ENABLED

它使 HAL 总头文件包含 ADC 的声明,也使 ADC 库源中的条件编译分支有效。

  • 重新构建后应同时查看首个错误和最终的链接摘要。若错误从“未声明”转为“未定义符号”,说明包含问题已解决、但实现文件仍未参与链接;若两者都消失,才说明软件构建链完整。
  • 不要直接改 stm32f1xx_hal_adc.c 的寄存器参数来“迁移 ADC1 到 ADC2”。应用层只需创建并初始化不同句柄;HAL 库本身按句柄的 Instance 操作相应 ADC 寄存器。
1.3.4 编译时函数未定义错误的解决: 添加函数声明
  • 本模块需要对外暴露的接口是 void Init_ADC2(void);extern ADC_HandleTypeDef hadc2;,应统一放在 adc.h,由 main.c 包含。不要在多个 .c 文件手写重复声明,后续修改容易不一致。
  • 函数声明通常不必写 extern,普通函数声明默认具有外部链接;变量才需要用 extern 声明“定义在别的源文件”。extern 只解决编译器看不到声明的问题,不能生成函数实现。
  • 若链接器仍提示 Init_ADC2hadc2 未定义,应检查 adc.c 是否在工程中、是否被当前 Target 编译、名称拼写是否一致。烧录后观察串口值只能验证运行结果,不能替代编译和链接错误的定位。

二、ADC中断模式

2.1 问题解答

  • 视频中最初使用 USART2,而实验把 PA1、PA2、PA3、PA4 配为 ADC2 的模拟输入。STM32F103 的 USART2 默认引脚是 PA2(TX)、PA3(RX),恰好与 ADC2 Channel2、Channel3 冲突:同一引脚不能同时作为模拟采样和串口复用功能。
  • 配套工程最终改用 USART1:PA9 为 TX、PA10 为 RX,和 PA1~PA4 的 ADC 输入不冲突。这里的 “P2/P3/P9/P10” 应规范写为 PA2/PA3/PA9/PA10,端口字母不能省略。
  • 硬件上还要确认 USB 转串口模块接到正确的引脚,并交叉连接:模块 RX 接 MCU PA9(TX),模块 TX 接 MCU PA10(RX),同时共地。若开发板串口接口只接 USART2,即使代码改为 USART1 也不会有输出,必须检查原理图或跳线。
  • 迁移串口不是只改 Instance = USART1:时钟宏、MSP 分支判断、GPIO 配置、printf 重定向使用的句柄都必须一致。配套工程的 HAL_UART_MspInit() 已正确使用 __HAL_RCC_USART1_CLK_ENABLE()、PA9 复用推挽输出和 PA10 输入。

2.2 UART头文件

配套工程的 uart.h 用宏将 printf 重定向到 USART1:

extern uint8_t u_buf[256];
extern UART_HandleTypeDef huart1;

#define printf(...) \
    HAL_UART_Transmit(&huart1, (uint8_t *)u_buf, \
                      sprintf((char *)u_buf, __VA_ARGS__), 0xffff)
  • 这里应统一使用 huart1,而不是只把“重定向编号”改成 1 却仍向 huart2 发送。工程的 uart.c 已定义 UART_HandleTypeDef huart1;,并在 Init_UART1() 中设为 115200、8 数据位、1 停止位、无校验。
  • 源码字符串使用 \n\r,常见串口终端换行顺序是 \r\n;两者在许多终端都能显示,但建议统一使用 \r\n。这不影响 ADC 采样本身。
  • sprintf 不检查 256 字节 u_buf 的边界,且该宏不具备可重入性;中断中或多任务环境不要调用它。入门实验可用,正式代码应改为 snprintf 并检查长度,或使用 UART DMA/环形缓冲。
  • HAL_UART_Transmit(..., 0xffff) 是阻塞发送。串口断开或发送量增大时,它会拖慢主循环;所以 LED 翻转频率和 ADC 采样频率都不应由这段打印代码推断。

2.3 数据结果展示

  • 视频展示的四路读数约为 1.6 V、3 V、0 V、3.3 V,和接入各通道的电压状态相符,说明 ADC2 的通道映射、扫描顺序和串口输出路径基本正确。单次现象相符不等于量程精度已经校准,仍要考虑参考电压和输入源阻抗。
  • 更可靠的验证方法是对每个通道分别施加已知且不超过 VDDA 的电压,记录码值并按 V = code * Vref / 4096 计算,检查通道顺序是否正确、读数是否稳定、零点和满量程附近是否异常。模拟输入不可超过芯片模拟电源允许范围。
  • 源码在 HAL_ADC_Start()HAL_ADC_PollForConversion() 和校准调用后没有检查状态,演示可运行但诊断性不足。至少应在轮询超时后跳过本次读取并输出错误信息,避免把上一次的寄存器值打印成当前通道值。
  • 程序下载后未自动运行时,视频建议在 Keil 的 Options for Target -> Debug -> Settings -> Flash Download 勾选 Reset and Run。若仍无输出,应先手动复位,再依次检查 BOOT0/BOOT1 启动状态、复位电路、调试器下载设置、供电和串口连接;“勾选后仍无效”不是 ADC2 移植失败的证据。
  • HAL_GPIO_TogglePin() 没有延时,LED 会因主循环和阻塞串口发送而呈现不稳定的闪烁或近似常亮。它只说明循环可能在执行,不能作为精确时基或 ADC 采样速率指示。