--- tags: [source-summary] type: source source: "尚硅谷嵌入式技术之STM32单片机(进阶篇)V1.0.1 — ADC章节 + 配套代码32~35" author: "尚硅谷研究院" date: 2026-07-15 created: 2026-07-15 --- # ADC模数转换 > **用生活理解**:ADC 就像一把精密的电压尺子——输入 0~~3.3V 的模拟电压,输出 0~~4095 的数字(12 位)。就像用一把刻有 4096 个刻度的尺子量高度,输入 1.65V 时输出 2048(中间值)。采样时间就是"尺子读数需要的时间",采样时间越长读数越准。规则组和注入组就像"普通队列"和"VIP 通道"——VIP 可以插队。 --- ## ADC 概述 ADC = Analog-to-Digital Converter(模数转换器),将连续的模拟信号转换为离散的数字信号。 STM32F103ZET6 共有 **3 个 ADC 模块**(ADC1/ADC2/ADC3): | 特性 | 值 | | -------- | ------------------------------------------ | | 分辨率 | **12 位**(输出范围 0~4095) | | 通道数 | 最多 18 个通道(16 个外部引脚 + 2 个内部) | | 转换时间 | 最快 1.17μs(14MHz 时钟下) | | 输入电压 | 0 ~ VREF+(通常 = VDDA = 3.3V) | | ADC 时钟 | 来自 APB2,经预分频器,**最大 14MHz** | ### 内部通道 | 通道 | 连接 | 说明 | | ------ | -------------- | -------------------- | | 通道16 | 内部温度传感器 | 测量芯片内部温度 | | 通道17 | VREFINT | 内部参考电压(1.2V) | --- ## SAR ADC 工作原理 STM32F103 使用**逐次逼近型**(SAR)ADC: ``` 输入电压 VIN = 1.8V, VREF = 3.3V 第1步: VIN > 3.3V/2=1.65V ? → YES → Bit11=1, 余量 = 1.8-1.65 = 0.15V 第2步: VIN > 1.65+1.65/2=2.475V ? → NO → Bit10=0 第3步: VIN > 1.65+0.825/2=2.0625V ? → NO → Bit9=0 第4步: VIN > 1.65+0.4125/2=1.85625V ? → NO → Bit8=0 第5步: VIN > 1.65+0.20625/2=1.753125V ? → YES → Bit7=1 ... 最终: 12 次比较后得到 12 位结果 数字值 = VIN / VREF × 4095 = 1.8 / 3.3 × 4095 ≈ 2234 ``` **特点**: - 优点:转换时间确定(固定周期数),无延迟 - 缺点:对输入噪声敏感(需要适当的采样时间) - 转换时间 = **采样时间 + 12.5 个 ADC 时钟周期** --- ## 关键概念 ### 规则组 vs 注入组 ADC 有两种转换序列: | 组 | 最大通道数 | 特点 | 用途 | | ---------- | ---------- | ---------------------- | -------------------- | | **规则组** | 16 通道 | 常规采集,按顺序转换 | 周期性采集传感器数据 | | **注入组** | 4 通道 | **可打断规则组**的转换 | 紧急事件触发采集 | **类比**:规则组是普通队列(一个个来),注入组是 VIP 通道——来了就插队。 ### 转换模式 | 模式 | 说明 | 适用场景 | | ------------ | ------------------------ | -------------- | | **单次转换** | 每触发一次转换一个通道 | 按键触发采集 | | **连续转换** | 转换完自动开始下一次 | 持续监测电压 | | **扫描模式** | 按序列依次转换所有通道 | 多通道循环采集 | | **间断模式** | 每触发一次转换一部分通道 | 超多通道分批次 | ### 触发方式 | 触发源 | 说明 | | ---------------- | ------------------------ | | **软件触发** | `CR2.SWSTART=1` 启动转换 | | **定时器触发** | TIM1/2/3/4 的 TRGO 事件 | | **外部引脚触发** | EXTI 线上升沿/下降沿 | ### ADC 时钟配置 ``` ADC_CLK = APB2_CLK / ADCPRE[1:0] ADCPRE 分频系数: 00: /2 (36MHz) → 超限! ADC_CLK > 14MHz 01: /4 (18MHz) → 超限! 10: /6 (12MHz) → ✅ 推荐(12MHz < 14MHz) 11: /8 (9MHz) → ✅ 较慢但更准 ``` > **参考**:参考手册 §11(ADC 寄存器描述) > **APB2 时钟 = 72MHz,ADCPRE=Div6 → ADC_CLK=12MHz 是最均衡选择** ### 采样时间 每个通道可独立配置采样时间(SMPR1/SMPR2 寄存器): | SMP[2:0] | 采样周期数 | 总转换时间(12MHz ADC_CLK) | | -------- | ---------- | --------------------------- | | 000 | 1.5 | (1.5+12.5) / 12MHz = 1.17μs | | 001 | 7.5 | (7.5+12.5) / 12MHz = 1.67μs | | 010 | 13.5 | 2.17μs | | 011 | 28.5 | 3.42μs | | 100 | 41.5 | 4.50μs | | 101 | 55.5 | 5.67μs | | 110 | 71.5 | 7.00μs | | **111** | **239.5** | **21.00μs(最准)** | **采样时间选择原则**: - 低阻抗信号源(<10KΩ):短采样时间(1.5~7.5 周期) - 高阻抗信号源(>10KΩ):长采样时间(28.5~239.5 周期) - 追求速度选短时间,追求精度选长时间 --- ## ADC 关键寄存器 | 寄存器 | 地址偏移 | 功能 | | --------- | -------- | ----------------------------------------------------------------------------------------- | | **SR** | 0x00 | 状态寄存器(EOC=转换结束,STRT=开始) | | **CR1** | 0x04 | 控制1(SCAN=扫描,JAUTO=自动注入) | | **CR2** | 0x08 | 控制2(ADON=开启,CONT=连续,SWSTART=软触发,EXTTRIG=外部触发,ALIGN=对齐,DMA=DMA 使能) | | **SMPR1** | 0x0C | 采样时间寄存器1(通道10~17) | | **SMPR2** | 0x10 | 采样时间寄存器2(通道0~9) | | **SQR1** | 0x2C | 规则序列寄存器1(L=序列长度,SQ13~16) | | **SQR2** | 0x30 | 规则序列寄存器2(SQ7~12) | | **SQR3** | 0x34 | 规则序列寄存器3(SQ1~6) | | **DR** | 0x4C | 规则组数据寄存器(12 位结果,右/左对齐) | > **参考**:参考手册 §11 完整寄存器位域定义 --- ## ADC 校准 ADC 上电后需要校准以提高精度: ```c // 校准流程 ADC1->CR2 |= ADC_CR2_ADON; // 开启 ADC // 等待稳定(约 2 个 ADC 时钟) ADC1->CR2 |= ADC_CR2_CAL; // 开始校准 while (ADC1->CR2 & ADC_CR2_CAL) // 等待校准完成(CAL 位硬件自动清 0) { } // 校准完成,ADC 准备就绪 ``` HAL 库版本(CubeMX 自动在 `MX_ADC1_Init()` 后由用户手动调用): ```c HAL_ADCEx_Calibration_Start(&hadc1); ``` > 校准原理:ADC 内部测量一次 VREFINT,根据误差调整内部电容阵列。 --- ## 实验对比总览 本笔记包含 4 个实验项目,覆盖单/双通道 × 寄存器/HAL 的全部组合: | 项目 | 通道数 | 实现方式 | DMA | 关键文件 | | ---------------------- | ---------------------------- | -------- | -------- | ----------------------------------------------------- | | 32_adc_single_register | 单通道(PC0/CH10) | 寄存器 | 无 | `Hardware/ADC/adc.c/.h`, `User/main.c` | | 33_adc_single_hal | 单通道(PC0/CH10) | HAL | 无 | `Core/Src/adc.c`, `Core/Src/main.c` | | 34_adc_double_register | 双通道(PC0/CH10, PC2/CH12) | 寄存器 | DMA1 CH1 | `Hardware/ADC/adc.c/.h`, `User/main.c` | | 35_adc_double_hal | 双通道(PC0/CH10, PC2/CH12) | HAL | DMA1 CH1 | `Core/Src/adc.c`, `Core/Src/dma.c`, `Core/Src/main.c` | --- ## 实验一:单通道 ADC 采集(寄存器版) **项目路径**:`stm32/32_adc_single_register` **需求**:使用 ADC1 通道 10(PC0)采集模拟电压,通过串口打印原始值和电压值。 **配置摘要**: - 单通道、连续转换、非扫描模式 - ADC 时钟 = APB2/6 = 12MHz - 采样时间 = 7.5 周期 **文件:`32_adc_single_register/Hardware/ADC/adc.h`** ```c #ifndef __ADC_H #define __ADC_H #include "stm32f10x.h" void ADC1_Init(void); void ADC1_StartConvert(void); double ADC1_ReadV(void); #endif ``` **文件:`32_adc_single_register/Hardware/ADC/adc.c`** ```c #include "adc.h" void ADC1_Init(void) { // 1. 时钟配置 RCC->APB2ENR |= RCC_APB2ENR_ADC1EN; RCC->CFGR |= RCC_CFGR_ADCPRE_1; RCC->CFGR &= ~RCC_CFGR_ADCPRE_0; // 2. GPIO配置:PC0 - 模拟输入 GPIOC->CRL &= ~(GPIO_CRL_MODE0 | GPIO_CRL_CNF0); // 3. ADC配置 // 3.1 关闭扫描模式 ADC1->CR1 &= ~ADC_CR1_SCAN; // 3.2 连续转换模式 ADC1->CR2 |= ADC_CR2_CONT; // 3.3 右对齐 ADC1->CR2 &= ~ADC_CR2_ALIGN; // 3.4 采样时间:001 - 7.5周期 ADC1->SMPR1 &= ~ADC_SMPR1_SMP10; ADC1->SMPR1 |= ADC_SMPR1_SMP10_0; // 3.5 通道序列 // 3.5.1 L = 0,1个通道 ADC1->SQR1 &= ~ADC_SQR1_L; // 3.5.2 SQ1 = 通道10 ADC1->SQR3 &= ~ADC_SQR3_SQ1; ADC1->SQR3 |= 10 << 0; } void ADC1_StartConvert(void) { // 1. 上电 ADC1->CR2 |= ADC_CR2_ADON; // 2. 校准 ADC1->CR2 |= ADC_CR2_CAL; while (ADC1->CR2 & ADC_CR2_CAL) {} // 3. 启动转换(连续模式) ADC1->CR2 |= ADC_CR2_ADON; // 4. 等待首次转换结束 while ((ADC1->SR & ADC_SR_EOC) == 0) {} } double ADC1_ReadV(void) { return ADC1->DR * 3.3 / 4095; } ``` **文件:`32_adc_single_register/User/main.c`** ```c #include "usart.h" #include "adc.h" #include "delay.h" int main(void) { USART_Init(); ADC1_Init(); printf("Hello, world!\n"); ADC1_StartConvert(); while (1) { printf("V = %.2f\n", ADC1_ReadV()); Delay_ms(1000); } } ``` **说明**:连续转换模式下,只需在 `ADC1_StartConvert()` 中等待首次 EOC,后续硬件自动持续转换,`ADC1_ReadV()` 直接读取 DR 寄存器获得最新结果。 --- ## 实验二:单通道 ADC 采集(HAL 版) **项目路径**:`stm32/33_adc_single_hal` **需求**:与实验一相同,使用 HAL 库实现 ADC1 通道 10(PC0)单通道连续采集。 **文件:`33_adc_single_hal/Core/Src/adc.c`** ```c #include "adc.h" ADC_HandleTypeDef hadc1; void MX_ADC1_Init(void) { ADC_ChannelConfTypeDef sConfig = {0}; hadc1.Instance = ADC1; hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE; hadc1.Init.ContinuousConvMode = ENABLE; hadc1.Init.DiscontinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion = 1; if (HAL_ADC_Init(&hadc1) != HAL_OK) { Error_Handler(); } sConfig.Channel = ADC_CHANNEL_10; sConfig.Rank = ADC_REGULAR_RANK_1; sConfig.SamplingTime = ADC_SAMPLETIME_7CYCLES_5; if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK) { Error_Handler(); } } void HAL_ADC_MspInit(ADC_HandleTypeDef* adcHandle) { GPIO_InitTypeDef GPIO_InitStruct = {0}; if (adcHandle->Instance == ADC1) { __HAL_RCC_ADC1_CLK_ENABLE(); __HAL_RCC_GPIOC_CLK_ENABLE(); GPIO_InitStruct.Pin = GPIO_PIN_0; GPIO_InitStruct.Mode = GPIO_MODE_ANALOG; HAL_GPIO_Init(GPIOC, &GPIO_InitStruct); } } void HAL_ADC_MspDeInit(ADC_HandleTypeDef* adcHandle) { if (adcHandle->Instance == ADC1) { __HAL_RCC_ADC1_CLK_DISABLE(); HAL_GPIO_DeInit(GPIOC, GPIO_PIN_0); } } ``` **文件:`33_adc_single_hal/Core/Src/main.c`** ```c #include "main.h" #include "adc.h" #include "usart.h" #include "gpio.h" int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_ADC1_Init(); MX_USART1_UART_Init(); printf("Hello, world\n"); HAL_ADCEx_Calibration_Start(&hadc1); HAL_ADC_Start(&hadc1); while (1) { double v = HAL_ADC_GetValue(&hadc1) * 3.3 / 4095; printf("V = %.2f\n", v); HAL_Delay(1000); } } void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct = {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; RCC_PeriphCLKInitTypeDef PeriphClkInit = {0}; RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE; RCC_OscInitStruct.HSEState = RCC_HSE_ON; RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1; RCC_OscInitStruct.HSIState = RCC_HSI_ON; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9; HAL_RCC_OscConfig(&RCC_OscInitStruct); RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2; RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2); PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_ADC; PeriphClkInit.AdcClockSelection = RCC_ADCPCLK2_DIV6; HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit); } ``` **说明**: - CubeMX 生成 `adc.c`(`MX_ADC1_Init` + MSP 回调)和 `gpio.c`(`MX_GPIO_Init`,使能 GPIOC 时钟) - 时钟配置在 `SystemClock_Config()` 中用 `RCC_ADCPCLK2_DIV6` 设置 ADC 预分频 - 校准需用户手动调用 `HAL_ADCEx_Calibration_Start`(CubeMX 不自动执行) - 阻塞式读取:`HAL_ADC_Start` → 循环中 `HAL_ADC_GetValue`(连续模式下自动更新 DR) --- ## 实验三:双通道 ADC + DMA 采集(寄存器版) **项目路径**:`stm32/34_adc_double_register` **需求**:使用 ADC1 通道 10(PC0)和通道 12(PC2)采集两路模拟电压,通过 DMA1 通道 1 自动搬运转换结果到内存数组。 **配置摘要**: - 双通道、连续转换、**扫描模式** - DMA1 通道 1:外设→内存,16 位,内存地址递增,循环模式 - 规则序列长度 L=1(2 个通道),SQ1=CH10, SQ2=CH12 **文件:`34_adc_double_register/Hardware/ADC/adc.h`** ```c #ifndef __ADC_H #define __ADC_H #include "stm32f10x.h" void ADC1_Init(void); void ADC1_DMA_Init(void); void ADC1_DMA_StartConvert(uint32_t destAddr, uint8_t len); #endif ``` **文件:`34_adc_double_register/Hardware/ADC/adc.c`** ```c #include "adc.h" void ADC1_Init(void) { // 1. 时钟配置 RCC->APB2ENR |= RCC_APB2ENR_ADC1EN; RCC->CFGR |= RCC_CFGR_ADCPRE_1; RCC->CFGR &= ~RCC_CFGR_ADCPRE_0; RCC->APB2ENR |= RCC_APB2ENR_IOPCEN; // 2. GPIO配置:PC0、PC2 - 模拟输入 GPIOC->CRL &= ~(GPIO_CRL_MODE0 | GPIO_CRL_CNF0); GPIOC->CRL &= ~(GPIO_CRL_MODE2 | GPIO_CRL_CNF2); // 3. ADC配置 // 3.1 开启扫描模式 ADC1->CR1 |= ADC_CR1_SCAN; // 3.2 连续转换模式 ADC1->CR2 |= ADC_CR2_CONT; // 3.3 右对齐 ADC1->CR2 &= ~ADC_CR2_ALIGN; // 3.4 采样时间:001 - 7.5周期(通道10 和 通道12) ADC1->SMPR1 &= ~ADC_SMPR1_SMP10; ADC1->SMPR1 |= ADC_SMPR1_SMP10_0; ADC1->SMPR1 &= ~ADC_SMPR1_SMP12; ADC1->SMPR1 |= ADC_SMPR1_SMP12_0; // 3.5 通道序列 // 3.5.1 L = 1,2个通道 ADC1->SQR1 &= ~ADC_SQR1_L; ADC1->SQR1 |= ADC_SQR1_L_0; // 3.5.2 SQ1 = 通道10, SQ2 = 通道12 ADC1->SQR3 &= ~ADC_SQR3_SQ1; ADC1->SQR3 |= 10 << 0; ADC1->SQR3 &= ~ADC_SQR3_SQ2; ADC1->SQR3 |= 12 << 5; } void ADC1_DMA_Init(void) { // 1. DMA时钟 RCC->AHBENR |= RCC_AHBENR_DMA1EN; // 2. 方向:从外设读取 DMA1_Channel1->CCR &= ~DMA_CCR1_DIR; // 3. 数据宽度:16位 DMA1_Channel1->CCR &= ~DMA_CCR1_PSIZE_1; DMA1_Channel1->CCR |= DMA_CCR1_PSIZE_0; DMA1_Channel1->CCR &= ~DMA_CCR1_MSIZE_1; DMA1_Channel1->CCR |= DMA_CCR1_MSIZE_0; // 4. 地址增量:外设不增、存储器增 DMA1_Channel1->CCR &= ~DMA_CCR1_PINC; DMA1_Channel1->CCR |= DMA_CCR1_MINC; // 5. 循环模式 DMA1_Channel1->CCR |= DMA_CCR1_CIRC; // 6. ADC使能DMA请求 ADC1->CR2 |= ADC_CR2_DMA; } void ADC1_DMA_StartConvert(uint32_t destAddr, uint8_t len) { // 0. DMA参数配置 DMA1_Channel1->CPAR = (uint32_t)&(ADC1->DR); DMA1_Channel1->CMAR = destAddr; DMA1_Channel1->CNDTR = len; DMA1_Channel1->CCR |= DMA_CCR1_EN; // 1. 上电 ADC1->CR2 |= ADC_CR2_ADON; // 2. 校准 ADC1->CR2 |= ADC_CR2_CAL; while (ADC1->CR2 & ADC_CR2_CAL) {} // 3. 启动转换 ADC1->CR2 |= ADC_CR2_ADON; // 4. 等待首次转换结束 while ((ADC1->SR & ADC_SR_EOC) == 0) {} } ``` **文件:`34_adc_double_register/User/main.c`** ```c #include "usart.h" #include "adc.h" #include "delay.h" uint16_t data[2] = {0}; int main(void) { USART_Init(); ADC1_Init(); ADC1_DMA_Init(); printf("Hello, world!\n"); ADC1_DMA_StartConvert((uint32_t)data, 2); while (1) { printf("V_PC0 = %.2f, V_PC2 = %.2f\n", data[0] * 3.3 / 4095, data[1] * 3.3 / 4095); Delay_ms(1000); } } ``` **关键要点**: 1. **扫描模式**(`CR1_SCAN`)必须开启,否则只能转换第一个通道 2. **序列长度 L** = 通道数 - 1,即 `ADC_SQR1_L_0`(=1)表示 2 个通道 3. DMA 的 **外设地址不增**(CPAR 固定指向 `ADC1->DR`)、**内存地址递增**(CMAR 指向数组,自动偏移) 4. **循环模式**(CIRC)使 DMA 持续搬运,与 ADC 连续转换匹配 5. `CNDTR` 初始值为 2,每完成一次 DMA 传输自动减 1,循环模式下到达 0 时自动重载 --- ## 实验四:双通道 ADC + DMA 采集(HAL 版) **项目路径**:`stm32/35_adc_double_hal` **需求**:与实验三相同,使用 HAL 库 + CubeMX 实现双通道 DMA 采集。 **文件:`35_adc_double_hal/Core/Src/adc.c`** ```c #include "adc.h" ADC_HandleTypeDef hadc1; DMA_HandleTypeDef hdma_adc1; void MX_ADC1_Init(void) { ADC_ChannelConfTypeDef sConfig = {0}; hadc1.Instance = ADC1; hadc1.Init.ScanConvMode = ADC_SCAN_ENABLE; hadc1.Init.ContinuousConvMode = ENABLE; hadc1.Init.DiscontinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion = 2; if (HAL_ADC_Init(&hadc1) != HAL_OK) { Error_Handler(); } sConfig.Channel = ADC_CHANNEL_10; sConfig.Rank = ADC_REGULAR_RANK_1; sConfig.SamplingTime = ADC_SAMPLETIME_1CYCLE_5; if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK) { Error_Handler(); } sConfig.Channel = ADC_CHANNEL_12; sConfig.Rank = ADC_REGULAR_RANK_2; if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK) { Error_Handler(); } } void HAL_ADC_MspInit(ADC_HandleTypeDef* adcHandle) { GPIO_InitTypeDef GPIO_InitStruct = {0}; if (adcHandle->Instance == ADC1) { __HAL_RCC_ADC1_CLK_ENABLE(); __HAL_RCC_GPIOC_CLK_ENABLE(); GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_2; GPIO_InitStruct.Mode = GPIO_MODE_ANALOG; HAL_GPIO_Init(GPIOC, &GPIO_InitStruct); // ADC1 DMA Init hdma_adc1.Instance = DMA1_Channel1; hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY; hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE; hdma_adc1.Init.MemInc = DMA_MINC_ENABLE; hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; hdma_adc1.Init.Mode = DMA_CIRCULAR; hdma_adc1.Init.Priority = DMA_PRIORITY_LOW; HAL_DMA_Init(&hdma_adc1); __HAL_LINKDMA(adcHandle, DMA_Handle, hdma_adc1); } } void HAL_ADC_MspDeInit(ADC_HandleTypeDef* adcHandle) { if (adcHandle->Instance == ADC1) { __HAL_RCC_ADC1_CLK_DISABLE(); HAL_GPIO_DeInit(GPIOC, GPIO_PIN_0 | GPIO_PIN_2); HAL_DMA_DeInit(adcHandle->DMA_Handle); } } ``` **文件:`35_adc_double_hal/Core/Src/dma.c`** ```c #include "dma.h" void MX_DMA_Init(void) { __HAL_RCC_DMA1_CLK_ENABLE(); } ``` **文件:`35_adc_double_hal/Core/Src/gpio.c`** ```c #include "gpio.h" void MX_GPIO_Init(void) { __HAL_RCC_GPIOC_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); } ``` **文件:`35_adc_double_hal/Core/Src/main.c`** ```c #include "main.h" #include "adc.h" #include "dma.h" #include "usart.h" #include "gpio.h" int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_DMA_Init(); MX_ADC1_Init(); MX_USART1_UART_Init(); printf("Hello, world\n"); uint16_t data[2] = {0}; HAL_ADCEx_Calibration_Start(&hadc1); HAL_ADC_Start_DMA(&hadc1, (uint32_t *)data, 2); while (1) { printf("V_PC0 = %.2f, V_PC2 = %.2f\n", data[0] * 3.3 / 4095, data[1] * 3.3 / 4095); HAL_Delay(1000); } } ``` **关键要点**: 1. **初始化顺序很重要**:`MX_GPIO_Init()` → `MX_DMA_Init()` → `MX_ADC1_Init()`。DMA 必须在 ADC 之前初始化,因为 `HAL_ADC_Init()` 内部会调用 `HAL_ADC_MspInit()`,而 MSP 中配置了 DMA(`__HAL_LINKDMA`)。 2. `MX_DMA_Init()` 仅使能 DMA1 时钟,实际的 DMA 通道配置在 `HAL_ADC_MspInit()` 中完成。 3. CubeMX 生成代码时,采样时间可能为 `ADC_SAMPLETIME_1CYCLE_5`(默认),实际项目中需根据信号源阻抗调整。 4. DMA 完成回调 `HAL_ADC_ConvCpltCallback` 可通过重载实现每轮转换完成后的处理。 --- ## 核心函数速查表 | 操作 | 寄存器版(单通道) | HAL 版(单通道) | 寄存器版(双通道+DMA) | HAL 版(双通道+DMA) | | -------- | -------------------- | ------------------------------------------------ | -------------------------------- | ----------------------------------------- | | 开启 ADC | `CR2 | = ADON` | `HAL_ADC_Init()` | `CR2 | = ADON` | `HAL_ADC_Init()` | | 校准 | `CR2 | = CAL; while(CR2 & CAL)` | `HAL_ADCEx_Calibration_Start()` | `CR2 | = CAL; while(CR2 & CAL)` | `HAL_ADCEx_Calibration_Start()` | | 配置通道 | `SQR3.SQ1 = ch` | `ADC_ChannelConfTypeDef + HAL_ADC_ConfigChannel` | `SQR3.SQ1/SQ2 = ch` | 同上(多通道重复调用) | | 启动转换 | `CR2 | = ADON` | `HAL_ADC_Start()` | `CR2 | = ADON`(含 DMA 参数) | `HAL_ADC_Start_DMA()` | | 查询完成 | `while(!(SR & EOC))` | `HAL_ADC_PollForConversion()` | `while(!(SR & EOC))`(首次等待) | 回调 `HAL_ADC_ConvCpltCallback` | | 读取结果 | `val = DR` | `HAL_ADC_GetValue()` | DMA 自动搬运到 `data[]` | DMA 自动搬运到 `data[]` | | DMA 配置 | N/A | N/A | 手动配置 DMA1 寄存器 | `MX_DMA_Init()` + MSP 中 `HAL_DMA_Init()` | | 电压换算 | `val × 3.3 / 4095` | 同上 | `data[n] × 3.3 / 4095` | 同上 | | 停止 ADC | `CR2 &= ~ADON` | `HAL_ADC_Stop()` | `CR2 &= ~ADON` + `CCR &= ~EN` | `HAL_ADC_Stop_DMA()` | --- ## 常见问题与避坑 1. **ADC 值始终为 0** → 检查 GPIO 模式(必须是**模拟输入**,不是浮空输入)、ADC 是否校准 2. **ADC 值始终为 4095** → 输入引脚悬空(感应噪声)、输入电压超过 VREF+ 3. **ADC 值跳动很大** → 采样时间太短(增加采样周期数)、电源去耦不良、PCB 布线噪声 4. **多通道值错乱** → SQR 序列配置错误、DMA 缓冲区大小与通道数不匹配、未开启扫描模式 5. **ADC 时钟 >14MHz** → 转换结果不准确,必须确保 ADCPRE 分频后 ADC_CLK ≤ 14MHz 6. **电压换算不准** → VREF+ 实际电压可能是 3.28V 而非 3.30V,需用万用表测量后修正 7. **DMA 传输不停止** → CIRC(循环模式)导致持续传输,停止时需要先清 DMA EN 再清 CIRC 8. **HAL 双通道 DMA 数据错位** → 确认 `MX_DMA_Init()` 在 `MX_ADC1_Init()` **之前**调用,否则 MSP 中的 `__HAL_LINKDMA` 不会生效