ADC中断模式
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后才能看到hadc2和Init_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(),等待 EOCHAL_ADC_PollForConversion(),再用HAL_ADC_GetValue()读取数据寄存器。HAL_ADC_GetValue()返回的是 12 位右对齐码值0~4095,不是电压值。 - 本工程设置
ScanConvMode=ENABLE、总转换数为 4、DiscontinuousConvMode=ENABLE、NbrOfDiscConversion=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;。ADC1、ADC2、ADC3也不是用户自定义的“结构体变量名”。在设备头文件中,它们是指向各自ADC_TypeDef寄存器块的宏,底层对应外设基地址。hadc2.Instance = ADC2;才是在选择 ADC2 的寄存器组。- 移植时要成组核对,而不是全局替换字符串:
hadc2.Instance = ADC2、HAL_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 寄存器的布局,ADC1、ADC2、ADC3是三组不同寄存器地址的入口。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通道与采样时间设置
| 规则 Rank | ADC2 通道 | GPIO 引脚 | 本工程用途 |
|---|---|---|---|
| Rank1 | Channel1 | PA1 | AD_Buf[0] |
| Rank2 | Channel2 | PA2 | AD_Buf[1] |
| Rank3 | Channel3 | PA3 | AD_Buf[2] |
| Rank4 | Channel4 | PA4 | AD_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_HandleTypeDef、HAL_ADC_Init()等 ADC 接口。 - 仅打开
HAL_ADC_MODULE_ENABLED还不够。工程还必须编译stm32f1xx_hal_adc.c和stm32f1xx_hal_adc_ex.c,否则可以通过编译却会在链接阶段报HAL_ADC_Init、HAL_ADCEx_Calibration_Start等未定义符号。 - 本工程的
temp.uvprojx已包含adc.c、stm32f1xx_hal_adc.c和stm32f1xx_hal_adc_ex.c,stm32f1xx_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_Init、HAL_ADC_ConfigChannel、HAL_ADC_Start等 ADC 接口报错,根因是新工程尚未把 ADC HAL 支持完整接入,而非这些 API 在 STM32F1 HAL 中不存在。 - 若
Init_ADC2()或hadc2在main.c处报错,应检查#include "adc.h"、函数声明和extern变量声明;若 HAL API 报错,应继续检查 HAL 模块开关和 HAL 源文件。
1.3.2 编译时函数未定义错误原因分析
- 缺少
HAL_ADC_MODULE_ENABLED或缺少 ADC 相关头文件时,编译器没有 API 声明,会产生“未声明/未知类型”错误。 stm32f1xx_hal_adc.c或stm32f1xx_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.c和stm32f1xx_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_ADC2或hadc2未定义,应检查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 采样速率指示。