ADC_DMA模式代码部分

Tutorial: 电子电路 Category: 嵌入式 Published: 2026-10-08 01:26:37 Views: 1 Likes: 0 Comments: 0

151.6.8 ADC中断模式代码部分

/*
 * 整体目的:用 ADC1 转换完成中断反复采集 PA0;中断回调只保存原始码并重新启动
 *           下一次单次转换,主循环负责电压换算和 USART1 输出。
 * 设计思路:ADC1 配置为单通道、非连续转换、软件触发;每次 HAL_ADC_Start_IT() 只
 *           产生一个结果,EOC 事件经共享向量 ADC1_2_IRQHandler() 交给 HAL 分发回调。
 * 硬件依据:PA0 对应 ADC1_IN0,12 位右对齐结果为 0~4095;ADC 时钟由 PCLK2/6
 *           得到 12 MHz,239.5 周期采样加 12.5 周期转换,总硬件时间约为 21 us。
 * 执行效果:回调立即重启会形成高频“转换→中断→重启”循环,而 115200 bit/s 串口
 *           只能显示其中少量结果,屏幕刷新速度不等于 ADC 实际转换频率。
 * 课程校正:原文件名写有 ADC_DMA,但配套 CubeIDE/11ADC_IT 没有 DMA 配置;本节
 *           实际使用 ADC 中断。下面保留课程源码的共享变量和回调写法,并在注释中指出边界。
 * 代码范围:依次摘录 main.c、adc.c 和 stm32f1xx_it.c 的关键代码,不展开 HAL 函数内部。
 */

/* main.c:课程工程的核心用户代码。 */
uint32_t ADC_Num;                           /* 保存最近一次 ADC 原始码;原源码未加 volatile,严格写法应补上。 */

HAL_ADCEx_Calibration_Start(&hadc1);       /* ADC1 初始化后、首次转换前执行自校准;课程代码未检查返回值。 */
HAL_ADC_Start_IT(&hadc1);                  /* 使能 EOC 中断并软件启动第一次单次规则转换。 */

while (1)                                   /* 前台不断读取中断更新的结果并输出,不参与触发当前转换。 */
{                                           /* 一轮显示处理开始。 */
    float ADC_Value =                       /* 根据共享原始码计算本次显示使用的估计电压。 */
        3.3f * (float)ADC_Num / 4096.0f;   /* 假定 VDDA=3.3 V,按 1 LSB=Vref/4096 换算。 */
    printf(                                 /* 通过工程中已重定向的 USART1 打印测量结果。 */
        "PA0 ADC_Value:%1.2fv   ADC Num: %lu\n", /* 保留课程格式;其中小写 v 只是普通字符。 */
        (double)ADC_Value,                  /* float 传入可变参数时会提升为 double。 */
        (unsigned long)ADC_Num);            /* %lu 与 uint32_t 在本工程工具链中的 unsigned long 对应。 */
}                                           /* 返回 while 顶部;阻塞串口发送使显示远慢于 ADC 中断。 */

void HAL_ADC_ConvCpltCallback(              /* 定义 HAL 的规则组转换完成回调,运行于中断上下文。 */
    ADC_HandleTypeDef *hadc)                /* HAL 传入触发事件的 ADC 句柄;课程源码没有使用该参数。 */
{                                           /* 回调函数体开始,应避免在这里执行浮点格式化或阻塞发送。 */
    ADC_Num =                               /* 更新主循环读取的最近一次原始码。 */
        HAL_ADC_GetValue(&hadc1);           /* 课程源码固定读取 ADC1;扩展多 ADC 时应先检查 hadc。 */
    HAL_ADC_Start_IT(&hadc1);               /* 连续模式关闭,必须再次启动才能获得下一个样本。 */
}                                           /* 尽快退出回调,让 CPU 恢复被中断的前台代码。 */

/* adc.c:ADC1 / CH0 / PA0 的单次软件触发配置。 */
hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE; /* 规则组只有一个通道,无需扫描多个 Rank。 */
hadc1.Init.ContinuousConvMode = DISABLE;   /* 每次转换完成后停止,由回调显式启动下一次。 */
hadc1.Init.DiscontinuousConvMode = DISABLE; /* 单 Rank 不需要再切分为间断转换片段。 */
hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; /* HAL_ADC_Start_IT() 通过软件触发转换。 */
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; /* 12 位结果右对齐,直接读取时范围为 0~4095。 */
hadc1.Init.NbrOfConversion = 1;            /* 规则序列只有一次转换,与唯一的 Rank1 对应。 */
sConfig.Channel = ADC_CHANNEL_0;           /* 选择 ADC1_IN0,即芯片的 PA0 模拟输入。 */
sConfig.Rank = ADC_REGULAR_RANK_1;         /* 把 CH0 放到规则组的第一个也是唯一一个位置。 */
sConfig.SamplingTime =                     /* 指定采样保持电容连接输入信号的时间。 */
    ADC_SAMPLETIME_239CYCLES_5;            /* 使用 239.5 个 ADC 周期,适合建立较慢的高阻信号。 */

/* stm32f1xx_it.c:ADC1 与 ADC2 共用的外设中断入口。 */
void ADC1_2_IRQHandler(void)                /* 启动文件中的向量名;“1_2”不表示同时使用两个 ADC。 */
{                                           /* 硬件 EOC 事件使 CPU 从这里进入中断处理。 */
    HAL_ADC_IRQHandler(&hadc1);            /* 将 ADC1 状态交给 HAL,由其进一步调用用户完成回调。 */
}                                           /* 中断入口结束,随后从被打断的位置继续执行。 */

原始文件名和转录文件名写作“ADC_DMA 模式代码部分”,但视频逐字稿、工程名 11ADC_IT、.ioc 和源码均表明本节实际讲的是 ADC 中断模式,工程没有配置 DMA。本精校稿据此更正标题;原文件名保留在精校版文件名中,便于与原笔记对应。

一、新建工程

本节新建的是 ADC 中断采集工程,配套工程为 11ADC_IT,目标芯片为 STM32F103ZET6。工程初始化过程仍遵循 CubeMX/CubeIDE 的常规流程:选择芯片、配置外部高速时钟 HSE、启用所需外设、生成代码,再在 USER CODE 区添加应用逻辑。

工程编号“11”只是课程中的目录名,不是 STM32 的硬件编号;“ADC IT”中的 IT 是 interrupt(中断)的缩写。本节没有 DMA 句柄、DMA 通道、HAL_ADC_Start_DMA() 或 DMA 中断服务函数,不能将其与下一节的 ADC DMA 工程混为一谈。

二、常规操作

原笔记中的“cereal”“dh”“可送操作”等是语音识别错误,视频此处是在说明 CubeMX 的常规创建步骤,核心内容如下:

  1. 在系统调试接口中保留 Serial Wire,以便通过 SWD 下载和调试。该配置使用 PA13(SWDIO)和 PA14(SWCLK),不要把这两个引脚误配给普通 GPIO。
  2. 先在 Pinout 中启用 ADC1 的任意通道,CubeMX 才会显示 ADC1 的参数页;本工程最终选择 ADC1 的 CH0。
  3. 设置 RCC 与时钟树后,再配置 ADC、NVIC 和串口。工程使用 HSE 经 PLL 得到 72 MHz 系统时钟,ADC 时钟选择 RCC_ADCPCLK2_DIV6,即 PCLK2 / 6 = 12 MHz,低于 STM32F103 的 ADC 最高 14 MHz 限制。
  4. 启用 USART1 用于显示数据。PA9 是 USART1_TX,PA10 是 USART1_RX;它们与本工程的 PA0 模拟输入不冲突。

Serial Wire 与 USART 都只是调试/通信接口,不参与 ADC 的模数转换。ADC 的模拟电压实际接在 PA0 上;外部信号地应与开发板模拟地共地,输入工作范围应处于 VSSA 至 VDDA 之间。示例用 3.3f 换算电压,隐含假设 ADC 参考电压 VDDA 为 3.3 V;供电有偏差时,计算结果会按相同比例产生误差。

三、配置ADC

工程实际配置如下,而不是“ADC1 和 ADC2 都开启并持续转换”:

项目实际配置作用
ADC 实例ADC1使用 ADC1 的规则组
通道与引脚CH0,PA0(ADC1_IN0)将 PA0 的模拟电压送入 ADC
扫描模式关闭只有一个 Rank,不需要扫描多个通道
规则组转换数1每次启动只转换 CH0 一次
连续转换关闭本次转换结束后停止,由回调重新启动下一次
触发源软件触发HAL_ADC_Start_IT() 调用后立即启动转换
数据对齐右对齐12 位有效值范围为 0 至 4095
采样时间239.5 个 ADC 时钟周期为输入采样电容提供较长建立时间
ADC 时钟12 MHz72 MHz / 6,满足不高于 14 MHz 的限制

PA0 在本工程中被配置为 GPIO_MODE_ANALOG。模拟模式会关闭数字输入路径,ADC 才能以较少的数字噪声和功耗对引脚电压采样。若信号来自电位器,可将电位器两端接到稳定的参考电压和地、滑动端接 PA0;若来自传感器,先确认其输出幅度、供电和地参考,不能把负电压或高于 VDDA 的电压直接接到 PA0。

采样时间 239.5 周期不是“中断延时”,而是 ADC 输入采样保持阶段的长度。单通道总转换时间还要加固定的 12.5 个 ADC 时钟周期:

tCONV = (239.5 + 12.5) / 12 MHz = 21 us

它决定 ADC 最快可完成一次转换的硬件时间。采样时间设得较长通常更适合源阻抗较高的电位器或传感器输出;若缩短采样时间,输入采样电容可能来不及充到真实电压,从而造成读数误差。串口输出速度、代码执行时间和中断响应还会使实际应用的数据处理频率低于这一理论转换速率。

在 NVIC 中启用的是 ADC1_2_IRQn。这是 STM32F1 为 ADC1 和 ADC2 共享的一个中断向量,不表示本工程同时启用了两个 ADC;生成的服务函数中只调用 HAL_ADC_IRQHandler(&hadc1),所以只处理 ADC1 的事件。

四、代码流程

本工程的中断数据流如下:

main() 初始化 ADC1、GPIO、USART1
        -> ADC 校准
        -> HAL_ADC_Start_IT(&hadc1)
        -> ADC1 对 PA0 完成一次转换并置 EOC
        -> ADC1_2_IRQHandler()
        -> HAL_ADC_IRQHandler(&hadc1)
        -> HAL_ADC_ConvCpltCallback()
        -> 读取 ADC1->DR,保存原始值,再启动下一次单次转换

HAL_ADCEx_Calibration_Start() 必须在开始规则组转换前调用;该函数的前提是 ADC 尚未处于转换中,工程的调用顺序满足这一点。实际代码应检查校准和启动函数的返回值,失败时进入明确的错误处理,而不是继续使用无效数据。

HAL_ADC_Start_IT() 做了三件关键事情:使能 ADC、打开规则组 EOC(转换结束)中断、在软件触发配置下启动转换。由于本工程的 ContinuousConvMode 为 DISABLE,HAL 在收到一次 EOC 后会关闭该 EOC 中断并把 ADC 状态恢复为就绪;所以回调函数中再次调用 HAL_ADC_Start_IT() 是本工程形成“单次转换 -> 中断 -> 再启动”的必要步骤。

这与连续转换模式不同:若把 ContinuousConvMode 改为使能,硬件会自动开始下一次转换,此时不应沿用“每次回调再次启动”的逻辑。连续、高频的每次转换都进入 ADC 中断会增加 CPU 中断负担,连续采样多个数据通常更适合 DMA 缓冲;本节的单次重启方式更适合低频、按事件或按节拍采一笔数据的场景。

五、流程图

5.1 例题1:Mindjet MindManager代码流程讲解

原视频用 Mindjet MindManager 的流程图复盘 ADC 代码。原图在“校准”与“读取结果”之间遗漏了中断链路,应补为:

ADC 初始化 -> ADC 校准 -> 开启 ADC 中断转换
-> ADC 转换完成(EOC)-> NVIC 进入 ADC1_2_IRQHandler
-> HAL_ADC_IRQHandler -> HAL_ADC_ConvCpltCallback
-> 读取结果并决定何时开始下一次转换

HAL_ADC_ConvCpltCallback() 是 HAL 提供的弱定义回调。用户在 main.c 中定义同名函数后,会覆盖 HAL 库的弱实现;不能只在流程图中添加“中断”而不让 ADC1_2_IRQHandler() 调用 HAL_ADC_IRQHandler(),否则回调不会被分发执行。

5.2 例题2:CubelDE代码流程讲解

在 CubeIDE 生成的工程中,ADC 参数位于 adc.c,中断服务函数位于 stm32f1xx_it.c,应用回调和启动代码放在 main.c 的 USER CODE 区。这样重新生成 CubeMX 代码时,用户代码不会被生成器覆盖。

回调应依据传入的句柄读取结果,而不是无条件使用某个全局 ADC:

static volatile uint16_t adc_raw;

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
    if (hadc == &hadc1)
    {
        adc_raw = (uint16_t)HAL_ADC_GetValue(hadc);
        (void)HAL_ADC_Start_IT(hadc);  /* 本工程为单次转换,需开始下一次 */
    }
}

上例与配套工程的功能相同,但补上了两个必要边界:hadc 可能在以后对应别的 ADC,因此先判断来源;adc_raw 同时被中断和主循环访问,必须声明为 volatile,避免优化器把主循环中的读取缓存为旧值。Cortex-M3 对自然对齐的 16 位读写是原子的,但“原子”只保证不会读到半个值,不保证主循环的多次读取来自同一时刻。

5.3 例题3:CubelDE代码流程讲解及错误排查

逐字稿中提到加入 string.h、sprintf 和未定义变量,这是演示过程中的临时编译问题。配套工程实际在 usart.h 中包含的是 <stdio.h>,并定义了以下简化宏:

extern uint8_t u_buf[256];
#define printf(...) \
    HAL_UART_Transmit(&huart1, (uint8_t *)u_buf, \
                      sprintf((char *)u_buf, __VA_ARGS__), 0xffff)

sprintf() 与 printf() 的声明来自 <stdio.h>,不是 <string.h>。u_buf 必须在函数外定义一次,例如工程的 usart.c 中定义 uint8_t u_buf[256];;若在多个 .c 文件重复定义,会发生链接期重复符号错误。若只是其他文件要访问它,应在头文件用 extern 声明。

上述宏仅适合作为教学中的简短输出:sprintf() 没有长度限制,过长格式化字符串会越过 256 字节缓冲区;HAL_UART_Transmit(..., 0xffff) 是阻塞发送,可能长时间占用主循环。更稳妥的方式是使用 snprintf() 限制长度,并检查发送函数返回值。无论使用哪种打印方式,都不要在 ADC 完成回调中格式化和发送串口数据。

六、定义全局变量

配套工程定义 uint32_t ADC_Num;,其宽度能够保存 HAL_ADC_GetValue() 返回的值;但 12 位 ADC 的实际有效范围只需 uint16_t。更重要的是,该变量由中断写、主循环读,应按如下方式声明:

static volatile uint16_t adc_raw;

不要因为“单片机存储空间足够”而随意把缓冲区或全局变量设得很大。全局变量占用静态 RAM,数组越大越容易挤占栈空间;先根据数据范围和实际缓冲需求决定类型和长度。若同时需要保存原始码值和电压,优先保留原始 uint16_t 码值,在使用处再转换为浮点数,便于滤波、平均和排查参考电压误差。

主循环打印时应只读取一次共享值,以使同一行的电压和原始值对应同一份样本:

uint16_t raw = adc_raw;
float voltage = 3.3f * raw / 4096.0f;
printf("PA0: %.2f V, raw: %u\r\n", voltage, (unsigned int)raw);

配套工程的输出字符串写成了 ...%1.2fv...,其中 v 只是普通字符;表示伏特时应写为大写 V。上例的 \r\n 也更符合多数串口终端的换行显示习惯。

七、查看效果

回调函数运行在中断上下文。串口格式化、阻塞发送、浮点格式化和长时间等待都会延长中断处理时间,可能推迟其他中断,因此视频中“中断函数中不应打印”的结论是正确的。本工程应只在回调里读取 DR、保存数据和启动下一次转换;输出、显示、滤波等耗时工作放在主循环或任务上下文完成。

本配置的最长采样时间下,ADC 单次硬件转换约为 21 us。若在每次回调中立刻重启,理论上会非常频繁地产生中断;主循环中的 115200 bit/s 串口输出远慢于 ADC 转换,不能把“屏幕上每秒显示的行数”误认为实际 ADC 转换次数。若只需较低频率数据,应由定时器或主循环按期启动单次转换;若确实需要连续采集一批数据,应使用 DMA 把多个样本写入缓冲区后再处理。

单个 ADC 码值会受到量化误差、参考电压噪声、输入信号噪声和采样建立误差影响。对缓慢变化的信号,可采集固定数量的样本后求平均,例如 10 个样本求和后再除以 10;但平均只能减小随机波动,不能修正错误的参考电压、超过输入范围或采样时间不足等系统性问题。滤波时还要明确采样频率和更新周期,避免为“数值稳定”而掩盖真实的快速变化。