# 10 ADC(HAL) > 目标:单通道 ADC 采集 + 定时器触发注入通道 ## 原理简述 ### 逐次逼近型 (SAR) ADC 工作原理 ```text 模拟输入 → 采样保持 → 比较器 ←── DAC (逐次逼近) │ 逐次逼近寄存器 (SAR) │ ┌───┴───┐ │ 数据寄存器 └───────┘ SAR 转换过程(以 3 位为例): 1. 采样保持: 锁定输入电压 (Vin) 2. 比较 MSB: DAC=100(½Vref) → Vin > 100? → MSB=1(或 0) 3. 比较 next: DAC=110(¾Vref) → Vin > 110? → 确定第二位 4. 比较 LSB: 确定最后一位 5. 转换完成 → 存入数据寄存器 STM32F103: 12 位 → 需要 12 个 SAR 比较周期 ``` **关键参数**: - 分辨率:**12 位** → 数值范围 0~4095 - 电压范围:**0V ~ VREF**(通常 VREF = VDDA = 3.3V) - 转换公式:`V_in = ADC_Value / 4095 × VREF` - 精度:**±1 LSB**(理想条件下 3.3V/4096 ≈ 0.8mV/位) ### 详细工作原理 #### SAR ADC 逐位逼近过程详解 逐次逼近型 ADC 的核心是 **二进制搜索算法**。STM32F103 的 ADC 是 12 位 SAR,需要 12 个比较周期完成一次转换: **第 1 步:采样阶段 (Sampling Phase)** ```text +───┐ Vin ────│ S │ └───┘ │ ┌─┴─┐ │ C │ ← 采样电容 (~8pF) 充电到 Vin └───┘ │ ═══╧═══ GND ``` 模拟开关 S 闭合,采样电容 `C_ADC (≈8pF)` 充电至输入电压 `Vin`。采样时间由 `SMPx[2:0]` 位配置。采样开关断开后,电容上保持的电压即为待转换的模拟量。 **第 2 步:保持与逐次逼近 (Hold & SAR Conversion)** ```text 采样结束后,SAR 开始逐位比较(12 位为例): ① 置 DAC = ½ VREF(二进制 1000 0000 0000 = 0x800) → Vin > ½ VREF ? 是 → MSB = 1, 保留下半区间 否 → MSB = 0, 保留上半区间 ② 根据 MSB 结果设置 DAC: MSB=1 → DAC = ¾ VREF(1100 0000 0000 = 0xC00) MSB=0 → DAC = ¼ VREF(0100 0000 0000 = 0x400) → 比较 → 确定 bit10 ③~⑫ 重复上述过程,逐位逼近,直到 LSB 确定 最终 12 位二进制数即为转换结果 ``` **时序示意图**: ```text 采样阶段 SAR 比较阶段 ├────────┤├──────────────────────┤ ① ② ③ ④ ⑤ ⑥ ⑦ ⑧ ⑨ ⑩ ⑪ ⑫ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ MSB LSB → 结果存入 DR ``` **总转换时间**公式: ```text T_conv = (t_sampling + 12.5) / ADCCLK 其中: t_sampling = SMPx[2:0] 配置的采样周期数 12.5 = 固定 SAR 转换周期数(12 位比较 + 1 个额外周期) ADCCLK = APB2 时钟分频后(≤ 14MHz) ``` #### ADC 时钟树 ADC 时钟来自 APB2 总线,通过可编程预分频器分频: ```text APB2 (72MHz) │ ▼ ┌──────┐ ┌────────────────────┬────────────────┐ │ /2 │───→│ 36MHz (✗ > 14MHz) │ 不可用 │ ├──────┤ ├────────────────────┼────────────────┤ │ /4 │───→│ 18MHz (✗ > 14MHz) │ 不可用 │ ├──────┤ ├────────────────────┼────────────────┤ │ /6 │───→│ 12MHz (✓ ≤ 14MHz) │ 常用配置 │ ├──────┤ ├────────────────────┼────────────────┤ │ /8 │───→│ 9MHz (✓ ≤ 14MHz) │ 可用,较慢 │ └──────┘ └────────────────────┴────────────────┘ ``` 预分频器通过 `ADC_CR2.ADCPRE[1:0]` 位配置。 #### 注入通道优先级与抢占机制 注入通道组 (Injected Group) 的行为类似于**中断**——可以打断正在进行的规则通道转换: ```text 时间轴 → 规则转换: CH0_CH1_CH2_..._CH0_CH1_ │ 注入触发到来 ──────────────┤ ▼ ┌──────────┐ │ 注入 CH0 │ ← 打断并执行注入 ├──────────┤ │ 注入 CH1 │ └──────────┘ │ 规则转换: ..._CH0_CH1_CH2_... ← 恢复规则转换 ``` 优先级规则: ```text 注入通道组 > 规则通道组(注入可打断规则) 注入通道之间按注入通道序号顺序转换(JCH0 > JCH1 > ...) ``` > ⚠️ 高频率注入触发会导致规则通道**饥饿**(永远无法完成转换)。 #### 采样时间与源阻抗的关系 ADC 采样电容的充电速度受信号源输出阻抗影响: ```text R_source R_ADC (~1kΩ) V_source ──┬────/\/\/\/\────┬────/\/\/\/\────┬────┐ │ │ │ │ │ │ │ ┌┴┐ │ │ │ │ C_ADC │ │ │ │ (~8pF) │ │ │ └┬┘ ═══ ═══ ═══ │ GND GND GND ═══ GND ``` 为使采样电容在采样时间内充分充电,需满足: ```text t_sampling ≥ (R_source + R_ADC_max) × C_ADC × ln(2^(n+2)) 代入 STM32F103 参数(n=12): R_ADC_max ≈ 1kΩ C_ADC ≈ 8pF ln(2^14) ≈ 9.7 t_sampling ≥ (R_source + 1kΩ) × 8pF × 9.7 例如 R_source = 10kΩ: t_sampling ≥ (10k + 1k) × 8pF × 9.7 ≈ 11k × 8pF × 9.7 ≈ 0.85µs → 对应 ADCCLK=12MHz(周期≈83ns)→ 需要 0.85µs/83ns ≈ 10.2 → 选 13.5 周期 ``` #### 电压转换公式 ```text V_in = ADC_Value / 4095 × VREF 反推:给定目标电压,求期望 ADC 值: ADC_Value = V_target / VREF × 4095 例: VREF = 3.3V, ADC_Value = 2048 V_in = 2048 / 4095 × 3.3V ≈ 1.65V ``` ![ADC框图](../assets/adc-block.png) ### 规则通道 vs 注入通道 STM32 ADC 有两组通道,可以理解为一个"常规任务"和一个"紧急插队任务": ```text 规则通道组 (Regular Group): ┌─────┐ ┌─────┐ ┌─────┐ │CH0 │──→│CH1 │──→│... │──→ 16 位数据寄存器 (DR) └─────┘ └─────┘ └─────┘ 最多 16 个通道按顺序转换,结果都进同一个 DR (多通道时必须用 DMA,否则后一个覆盖前一个) 注入通道组 (Injected Group): ┌─────┐ │CH0 │──→ 注入数据寄存器 (JDR1~4) └─────┘ 最多 4 个通道,各占独立寄存器 可以打断规则组正在进行的转换 ``` ### 扫描模式 vs 连续模式 ```text 单次/单通道: 触发 → 转换 CH0 → 停止(常用) 连续/单通道: 触发 → 转换 CH0 → 自动再转 CH0 → 一直转 单次/多通道: 触发 → CH0 → CH1 → CH2 → 停止 连续/多通道: 触发 → CH0→CH1→...→CHn→再CH0→...(一直转) ``` ### 数据对齐 ```text 12 位结果存在 16 位寄存器中,可选择对齐方式: 右对齐(常用): DR: [15][14][13][12][11][10][9][8][7][6][5][4][3][2][1][0] ↑ DATA[11:0] ↑ └──────────────┘ 实际值 = DR(直接可用) 左对齐(某些滤波场景用): DR: [15][14][13][12][11][10][9][8][7][6][5][4][3][2][1][0] ↑ DATA[11:0] ↑ └──────────────┘ 实际值 = DR >> 4 ``` ## 采样时间与转换时间 ```text 总转换时间 = 采样时间 + 12.5 个 ADC 时钟周期 ADC 时钟 (ADCCLK) 来自 APB2 分频,必须 ≤ 14MHz: APB2=72MHz → 分频 2→36MHz(✗超限) → 分频 4→18MHz(✗超限) → 分频 6→12MHz(✓) → 分频 8→9MHz(✓) 采样时间可选:1.5 / 7.5 / 13.5 / 28.5 / 41.5 / 55.5 / 71.5 / 239.5 周期 示例 (72MHz, 分频=6, 采样=1.5): ADCCLK = 72/6 = 12MHz 总时间 = (1.5 + 12.5) / 12MHz ≈ 1.17µs 每秒采样 ≈ 856k samples/s 采样时间越短 → 越快,但对信号源内阻敏感 采样时间越长 → 越准,适合高内阻信号源(如分压电阻网络) ``` ## 校准 ADC 每次上电后需要校准,消除内部电容阵列的偏差。CubeMX 生成的代码会自动完成: ```text HAL_ADC_Start(&hadc1) 的内部流程: 1. 检查是否已校准,若未校准则自动校准 2. 校准完成 3. 开始转换 ``` > 如果需要手动校准:`HAL_ADCEx_Calibration_Start(&hadc1)`(部分 HAL 版本) ## 单通道转换(软件触发) ### CubeMX 配置 - **ADC1 → IN0 (PA0)** - 参数: - Clock Prescaler:**PCLK2 divided by 6**(ADCCLK = 12MHz) - Resolution:**12 bits** - Scan Conversion Mode:**Disabled**(单通道) - Continuous Conversion Mode:**Disabled**(单次转换) - Data Alignment:**Right alignment** - Regular Conversion → Channel:**0**, Sampling Time:**1.5 Cycles** ### 关键 API | 函数 | 说明 | |------|------| | `HAL_ADC_Start(&hadc)` | 启动一次转换(含自动校准) | | `HAL_ADC_PollForConversion(&hadc, Timeout)` | 轮询等待转换完成 | | `HAL_ADC_GetValue(&hadc)` | 读取 12 位转换结果 | ### 完整代码 ```c int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_ADC1_Init(); while (1) { HAL_ADC_Start(&hadc1); // 启动转换 if (HAL_ADC_PollForConversion(&hadc1, 100) == HAL_OK) // 等完成 { uint16_t val = HAL_ADC_GetValue(&hadc1); // 读结果 (0~4095) float v = (float)val / 4095.0f * 3.3f; // 换算电压 printf("ADC=%d, V=%.2f\r\n", val, v); // 需重定向 printf } HAL_Delay(500); } } ``` ## 定时器触发转换(注入通道) ### 原理 ```text 定时器 TIM3 (TRGO=Update) │ │ 每过 N ms 触发一次 ↓ ADC 注入通道 ──→ 打断正在进行的常规转换 ──→ 执行注入通道转换 ──→ 恢复常规转换 ``` 优势:精确的采样间隔、CPU 不需要空等轮询。 ### CubeMX 配置 - ADC1 → **IN0 (PA0)** - **Injected Conversion Mode** → **Enable** - External Trigger Conversion Source:**Timer 3 Trigger Out** - External Trigger Conversion Edge:**Rising Edge** - NVIC Settings → **ADC1 and ADC2 global interrupt** → Enable - TIM3 → **Prescaler** 和 **Counter Period** 决定触发间隔 - 例如 PSC=7199, Period=4999 → 72MHz/7200/5000 = 2Hz - TIM3 → **TRGO** → **Update Event** ### 关键 API | 函数 | 说明 | |------|------| | `HAL_ADCEx_InjectedStart(&hadc)` | 启动注入通道(等待触发) | | `HAL_ADCEx_InjectedGetValue(&hadc, InjectedRank)` | 读取注入结果 | | `HAL_ADCEx_InjectedConvCpltCallback(&hadc)` | 注入完成回调 | ```c /* 注入转换完成回调 —— 由 HAL 在 ADC 中断中调用 */ void HAL_ADCEx_InjectedConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc == &hadc1) { uint32_t val = HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_1); // 处理结果(中断中应快速处理,不要用 HAL_Delay) } } ``` ## ADC + DMA(多通道/连续采集) 当需要连续采集多通道时,必须使用 DMA 把结果自动搬走,否则新数据会覆盖旧数据: ```c #define ADC_BUF_LEN 16 uint32_t adc_buf[ADC_BUF_LEN]; // DMA 缓冲区 HAL_ADC_Start_DMA(&hadc1, adc_buf, ADC_BUF_LEN); // DMA 自动把每次转换结果搬入 adc_buf // 填满后调用 HAL_ADC_ConvCpltCallback() void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { // adc_buf 已填满,可以处理 for (int i = 0; i < ADC_BUF_LEN; i++) { printf("CH%d = %d\r\n", i, adc_buf[i]); } } ``` ## 常见坑点 - ⚠️ ADC 输入电压**绝对不能超过 VREF**(通常 VDDA = 3.3V),否则损坏 ADC - ⚠️ ADCCLK **必须 ≤ 14MHz**,否则转换结果不准确(CubeMX 用红色警告) - ⚠️ 多通道扫描但**不使用 DMA** → 后面的结果会覆盖前面的,只能读到最后一个通道 - ⚠️ 注入通道优先级高于常规通道,可打断常规转换。高频率注入会导致常规通道**饥饿** - ⚠️ `HAL_ADC_PollForConversion` 返回 `HAL_TIMEOUT` → 检查 ADC 时钟、校准是否完成 - ⚠️ 连续模式(Continuous Conversion)开启后,ADC 会不停地转,功耗较高