Chapter 07 · ARM 嵌入式 C 开发

ADC 与 DAC

理解逐次逼近型 ADC 原理与精度,掌握多通道 DMA 采集、滤波算法,读取 NTC 温度传感器,输出正弦波。

课程进度70%

关键概念

ADC 工作原理(逐次逼近型 SAR)
Successive Approximation Register ADC,每次转换用二分法逼近:先设 DAC 输出半量程,若输入电压大于它,最高位置 1,否则置 0;然后对下一位重复。12 位 ADC 需要 12 次比较,速度比积分型快(STM32F4 最高 2.4M 次/秒)。
分辨率与精度
STM32F4 ADC 是 12 位,量程 0-4096(0x000-0xFFF)。1 LSB = VREF / 4096。若 VREF=3.3V,则 1 LSB ≈ 0.806mV。分辨率是理想精度,实际精度受 INL(积分非线性)、DNL(微分非线性)、参考电压噪声影响,典型误差约 2-4 LSB。
参考电压(VREF)
ADC 测量的"满量程"基准。STM32F4 的 VREF+ 引脚可接外部精密基准(如 LM4040),也可接 AVDD(片内 3.3V)。VREF 不稳定会直接影响所有通道的测量精度,高精度应用必须使用外部精密基准。
采样时间(Sampling Time)
ADC 对输入电压充电的时间,以 ADC 时钟周期计。源阻抗越高,需要越长采样时间(电容充电更慢)。STM32F4 ADC 时钟最高 36MHz,采样时间选项:3/15/28/56/84/112/144/480 个周期。高阻抗信号源(>1kΩ)应选更长的采样时间。
扫描模式(Scan Mode)
ADC 自动按规则通道序列依次采样多个通道,无需软件干预。结合 DMA,可以让 ADC 连续扫描并自动将结果存入数组,CPU 只需读取数组即可获得所有通道最新值。适合同时采集多个传感器(电压、电流、温度等)。
NTC 热敏电阻
Negative Temperature Coefficient 负温度系数热敏电阻,温度升高则阻值降低。常用 10kΩ NTC,与串联电阻(10kΩ)分压,用 ADC 读分压值,再通过 Steinhart-Hart 方程或查表转换为温度。精度约 ±0.5~1°C,成本低廉。
均值滤波 vs 卡尔曼滤波
均值滤波:取 N 个采样值的算术平均,消除随机噪声,但响应慢(不适合快速变化信号)。卡尔曼滤波:用数学模型预测下一个值,再结合测量值加权融合,适合有规律变化的信号,响应快且噪声抑制好。嵌入式中常用一维卡尔曼(简化版)。
DAC 输出
STM32F4 有 2 路 12 位 DAC(PA4/PA5),将数字值 0-4095 转换为 0~VREF 的模拟电压。配合定时器触发 + DMA,可以输出任意波形(正弦波、锯齿波)。DAC 输出驱动能力较弱(最大几十 μA),驱动负载需加运算放大器缓冲。

ADC 转换原理(SAR)

12位逐次逼近 ADC 工作过程(输入电压 = 2.1V,VREF=3.3V) ═══════════════════════════════════════════════════════════════ 比较次数 寄存器值 DAC输出 比较结果 操作 ───────── ────────── ───────────── ──────── ──── Round 1: 1000_0000_0000 1.65V Vin>DAC bit11=1 Round 2: 1100_0000_0000 2.475V Vin<DAC bit10=0 Round 3: 1010_0000_0000 2.0625V Vin>DAC bit9=1 Round 4: 1011_0000_0000 2.269V Vin<DAC bit8=0 ...(共12轮)... 最终: 1010_0001_1110 ≈2.1V ✓ 原始值 = 0xA1E = 2590 对应电压 = 2590 / 4095 × 3.3V ≈ 2.087V STM32F4 ADC 时钟链: APB2(84MHz) → ADC预分频(/4) → ADC时钟 = 21MHz 采样时间(3周期) + 转换时间(12周期) = 15周期 转换速率 = 21MHz / 15 = 1.4M次/秒(单通道)

多通道 DMA 循环采集

/**
 * @brief ADC1 三通道 DMA 循环采集
 *        CH0(PA0): 外部电压,CH1(PA1): 电位器,CH17: 片内温度传感器
 */
ADC_HandleTypeDef hadc1;
#define ADC_CH_NUM  3
volatile uint16_t adc_values[ADC_CH_NUM];  /* DMA 写入此数组 */

void ADC1_DMA_Init(void) {
  ADC_ChannelConfTypeDef sConfig = {0};

  __HAL_RCC_ADC1_CLK_ENABLE();
  __HAL_RCC_GPIOA_CLK_ENABLE();

  /* 配置 PA0、PA1 为模拟输入 */
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  GPIO_InitStruct.Pin  = GPIO_PIN_0 | GPIO_PIN_1;
  GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;  /* 模拟模式:关闭施密特触发器 */
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

  /* ADC 基本参数 */
  hadc1.Instance                   = ADC1;
  hadc1.Init.ClockPrescaler        = ADC_CLOCK_SYNC_PCLK_DIV4;  /* 84/4=21MHz */
  hadc1.Init.Resolution            = ADC_RESOLUTION_12B;
  hadc1.Init.ScanConvMode          = ENABLE;          /* 扫描多通道 */
  hadc1.Init.ContinuousConvMode    = ENABLE;          /* 持续转换 */
  hadc1.Init.DiscontinuousConvMode = DISABLE;
  hadc1.Init.ExternalTrigConvEdge  = ADC_EXTERNALTRIGCONVEDGE_NONE;
  hadc1.Init.DataAlign             = ADC_DATAALIGN_RIGHT;
  hadc1.Init.NbrOfConversion       = ADC_CH_NUM;      /* 3个通道 */
  hadc1.Init.DMAContinuousRequests = ENABLE;          /* DMA循环模式 */
  hadc1.Init.EOCSelection          = ADC_EOC_SINGLE_CONV;
  HAL_ADC_Init(&hadc1);

  /* 配置三个通道的采样时间和序列顺序 */
  sConfig.SamplingTime = ADC_SAMPLETIME_56CYCLES;

  sConfig.Channel = ADC_CHANNEL_0; sConfig.Rank = 1; HAL_ADC_ConfigChannel(&hadc1, &sConfig);
  sConfig.Channel = ADC_CHANNEL_1; sConfig.Rank = 2; HAL_ADC_ConfigChannel(&hadc1, &sConfig);
  sConfig.Channel = ADC_CHANNEL_TEMPSENSOR;
  sConfig.SamplingTime = ADC_SAMPLETIME_480CYCLES;   /* 温度传感器需长采样 */
  sConfig.Rank = 3; HAL_ADC_ConfigChannel(&hadc1, &sConfig);

  /* 启动 DMA 循环采集 */
  HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_values, ADC_CH_NUM);
}

/* 读取电压值(mV)*/
uint32_t ADC_GetVoltage_mV(uint8_t ch) {
  return ((uint32_t)adc_values[ch] * 3300) / 4095;
}

NTC 温度传感器读取

#include "math.h"

#define NTC_SERIES_R     10000.0f   /* 串联电阻 10kΩ */
#define NTC_R25          10000.0f   /* 25°C 标称阻值 10kΩ */
#define NTC_B            3950.0f    /* B 系数(查手册)*/
#define VREF             3.3f
#define ADC_MAX          4095.0f

/**
 * NTC 分压电路:VCC → R_series → NTC → GND
 * ADC 测量 R_series 和 NTC 之间的电压(即 NTC 两端电压)
 *
 * R_NTC = R_series × V_NTC / (VCC - V_NTC)
 *       = R_series × adc / (ADC_MAX - adc)
 *
 * Steinhart-Hart 简化(B 值方程):
 * 1/T = 1/T25 + (1/B) × ln(R/R25)
 * T = 1 / (1/T25 + ln(R/R25)/B)  (K 转 °C 减 273.15)
 */
float NTC_ReadTemp_C(uint16_t adc_raw) {
  float r_ntc = NTC_SERIES_R * (float)adc_raw / (ADC_MAX - (float)adc_raw);
  float inv_t = (1.0f / (25.0f + 273.15f)) + (1.0f / NTC_B) * logf(r_ntc / NTC_R25);
  return (1.0f / inv_t) - 273.15f;
}

/* ── 均值滤波(消除 ADC 噪声)────────────────────── */
#define AVG_N  16

uint16_t ADC_AverageFilter(uint16_t *samples, uint8_t n) {
  uint32_t sum = 0;
  for (uint8_t i = 0; i < n; i++) sum += samples[i];
  return (uint16_t)(sum / n);
}

/* ── 一维卡尔曼滤波 ─────────────────────────────── */
typedef struct {
  float x;      /* 状态估计值 */
  float p;      /* 估计误差协方差 */
  float q;      /* 过程噪声协方差(系统噪声,越小越信任模型)*/
  float r;      /* 测量噪声协方差(越小越信任测量值)*/
  float k;      /* 卡尔曼增益 */
} Kalman_t;

void Kalman_Init(Kalman_t *kf, float q, float r, float init_val) {
  kf->x = init_val;
  kf->p = 1.0f;
  kf->q = q;
  kf->r = r;
}

float Kalman_Update(Kalman_t *kf, float measurement) {
  kf->p += kf->q;                              /* 预测:增加不确定性 */
  kf->k  = kf->p / (kf->p + kf->r);           /* 计算卡尔曼增益 */
  kf->x += kf->k * (measurement - kf->x);     /* 更新估计值 */
  kf->p *= (1.0f - kf->k);                   /* 更新协方差 */
  return kf->x;
}

DAC 输出正弦波

#include "math.h"

#define SINE_POINTS  100   /* 正弦波一个周期的采样点数 */
uint16_t sine_table[SINE_POINTS];

/* 预计算正弦查找表 */
void Sine_TableInit(void) {
  for (int i = 0; i < SINE_POINTS; i++) {
    float rad = (2.0f * 3.14159f * i) / SINE_POINTS;
    /* 将 -1~1 映射到 0~4095(12位DAC) */
    sine_table[i] = (uint16_t)((sinf(rad) + 1.0f) * 2047.5f);
  }
}

/**
 * DAC1 + DMA + TIM6 触发 → 自动输出正弦波
 * 频率 = TIM6 触发频率 / SINE_POINTS
 * 例:TIM6 触发 = 10kHz → 正弦波 = 100Hz
 */
void DAC_Sine_Init(void) {
  /* PA4 为模拟输出(DAC1 CH1) */
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  __HAL_RCC_GPIOA_CLK_ENABLE();
  GPIO_InitStruct.Pin  = GPIO_PIN_4;
  GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

  /* DAC 启动 DMA(TIM6 TRGO 触发,循环模式)*/
  /* 详细初始化代码由 CubeMX 生成 */
  HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1,
                    (uint32_t*)sine_table, SINE_POINTS,
                    DAC_ALIGN_12B_R);  /* 12位右对齐 */
}
片内温度传感器

STM32F4 的 ADC1 通道 16(CH16)连接片内温度传感器,可测量芯片结温(不是环境温度)。转换公式:Temp = ((V_SENSE - V25) / Avg_Slope) + 25,其中 V25 ≈ 0.76V,Avg_Slope ≈ 2.5mV/°C(参考具体芯片数据手册)。精度约 ±1.5°C,适合监控芯片是否过热。

ADC 精度陷阱

AVDD 和 AGND 必须与数字电源分开走线,否则数字噪声会耦合到 ADC。VREF+ 和 VREF- 引脚附近需要 100nF + 1μF 去耦电容。ADC 输入端加 RC 低通滤波器(如 1kΩ + 100nF)可以滤除高频噪声。多通道同时采集时,通道间的信号切换会产生串扰,高精度应用建议减慢扫描速度或对每通道多次采样取平均。