ADC_DMA模式代码部分
151.6.8 ADC中断模式代码部分
/* 配套工程:视频配套/3、STM32单片机裸机部分/code/CubeIDE/11ADC_IT */
/* main.c:原工程的核心用户代码 */
uint32_t ADC_Num;
HAL_ADCEx_Calibration_Start(&hadc1);
HAL_ADC_Start_IT(&hadc1);
while (1)
{
float ADC_Value = 3.3f * ADC_Num / 4096.0f;
printf("PA0 ADC_Value:%1.2fv ADC Num: %lu\n", ADC_Value, ADC_Num);
}
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
ADC_Num = HAL_ADC_GetValue(&hadc1);
HAL_ADC_Start_IT(&hadc1);
}
/* adc.c:ADC1 / CH0 / PA0,单次软件触发 */
hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE;
hadc1.Init.ContinuousConvMode = DISABLE;
hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc1.Init.NbrOfConversion = 1;
sConfig.Channel = ADC_CHANNEL_0;
sConfig.Rank = ADC_REGULAR_RANK_1;
sConfig.SamplingTime = ADC_SAMPLETIME_239CYCLES_5;
/* stm32f1xx_it.c */
void ADC1_2_IRQHandler(void)
{
HAL_ADC_IRQHandler(&hadc1);
}
原始文件名和转录文件名写作“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 的常规创建步骤,核心内容如下:
- 在系统调试接口中保留
Serial Wire,以便通过 SWD 下载和调试。该配置使用 PA13(SWDIO)和 PA14(SWCLK),不要把这两个引脚误配给普通 GPIO。 - 先在 Pinout 中启用 ADC1 的任意通道,CubeMX 才会显示 ADC1 的参数页;本工程最终选择 ADC1 的 CH0。
- 设置 RCC 与时钟树后,再配置 ADC、NVIC 和串口。工程使用 HSE 经 PLL 得到 72 MHz 系统时钟,ADC 时钟选择
RCC_ADCPCLK2_DIV6,即PCLK2 / 6 = 12 MHz,低于 STM32F103 的 ADC 最高 14 MHz 限制。 - 启用 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 MHz | 72 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;但平均只能减小随机波动,不能修正错误的参考电压、超过输入范围或采样时间不足等系统性问题。滤波时还要明确采样频率和更新周期,避免为“数值稳定”而掩盖真实的快速变化。