0x6A Logbook

0x6A Logbook
Shi6a的筆記本
  1. 首頁
  2. 未分類
  3. 正文

ADC 類比數位轉換器完整教學:從原理到 STM32/ESP32 實作

2026 年 5 月 29 日 4點熱度 0人點贊 0條評論

前言

ADC 類比數位轉換器(Analog-to-Digital Converter)是嵌入式系統中感測器資料的入口。無論是讀取電位器、溫度感測器(LM35、DS18B20)、麥克風音訊還是電池電壓,最終都需要 ADC 將連續的類比訊號轉換為數位值供 MCU 處理。STM32 內建多組 12-bit SAR ADC,支援三通道同步採樣;ESP32 則有兩組 12-bit SAR ADC,最多可連接 18 個類比輸入。本文將從 ADC 的基本原理講到實際程式碼,涵蓋 STM32 與 ESP32 的不同實作方式。

一、ADC 基本原理

1.1 什麼是 ADC?

ADC 將連續的類比電壓轉換為離散的數位數值。轉換過程包含三個步驟:

  1. 取樣(Sampling):以固定間隔擷取類比訊號的瞬時值
  2. 量化(Quantization):將取樣值近似到最接近的離散電平
  3. 編碼(Encoding):將量化電平轉為二進位編碼

1.2 核心參數

參數 說明 公式
解析度 (Resolution) 數位輸出的位元數 N bits → 2^N 個電平
參考電壓 (Vref) ADC 的最大輸入電壓 通常 = VDD 或內部參考
LSB (最小有效位) 1 個數位步進對應的電壓 Vref / 2^N
取樣率 (Sampling Rate) 每秒可完成的轉換次數 SPS (Samples Per Second)

ADC Resolution

圖 4:ADC 解析度 vs 電壓解析度 (Vref=3.3V) — 12-bit ADC 的 1 LSB = 0.806mV,16-bit 可達 50.3μV。

1.3 常見 ADC 類型

ADC Types

圖 3:常見 ADC 類型比較 — SAR 是 MCU 內建的主流架構,Sigma-Delta 用於高精度儀表,Flash 用於極高速場景。

STM32 和 ESP32 的內建 ADC 都是 SAR(逐次逼近暫存器,Successive Approximation Register) 架構,12-bit 解析度,支援多通道多工。

二、SAR ADC 時序

SAR ADC Timing

圖 1:SAR ADC 轉換時序 (12-bit) — Sample → Hold → 逐位逼近 (D11~D0) → 輸出結果。每個位元需要一個 SCLK 週期。

SAR ADC 的工作原理類似天平:

  1. Sample 階段:開關閉合,取樣電容追蹤輸入電壓
  2. Hold 階段:開關斷開,電容保持取樣電壓
  3. 逐次逼近:從 MSB 開始,比較器逐位決定每一位元是 0 還是 1
  4. 轉換完成:結果鎖存到資料暫存器,觸發 IRQ 或 DMA

轉換時間計算:

T_conv = (取樣週期 + 轉換位元數) × ADC_CLK 週期
T_conv = (n_cycles + 12) × 1/ADC_CLK

// STM32F4 @ 21 MHz ADC Clock, 3 cycle sampling:
T_conv = (3 + 12) / 21 MHz = 0.714 μs
Sampling Rate ≈ 1.4 MSPS

三、STM32 ADC 實作 (HAL Library)

STM32F4 的 ADC 支援多種模式:單次/連續/掃描、暫存器/DMA/Triple 交錯。

3.1 單通道單次轉換

#include "stm32f4xx_hal.h"

ADC_HandleTypeDef hadc1;

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

    hadc1.Instance = ADC1;
    hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4;  // 84/4=21 MHz
    hadc1.Init.Resolution = ADC_RESOLUTION_12B;
    hadc1.Init.ScanConvMode = DISABLE;        // 單通道
    hadc1.Init.ContinuousConvMode = DISABLE;   // 單次
    hadc1.Init.DiscontinuousConvMode = DISABLE;
    hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;  // 軟體觸發
    hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
    hadc1.Init.NbrOfConversion = 1;
    hadc1.Init.EOCSelection = ADC_EOC_SEQ_CONV;
    HAL_ADC_Init(&hadc1);

    sConfig.Channel = ADC_CHANNEL_0;      // PA0
    sConfig.Rank = 1;
    sConfig.SamplingTime = ADC_SAMPLETIME_3CYCLES;
    HAL_ADC_ConfigChannel(&hadc1, &sConfig);
}

uint32_t read_adc(void)
{
    HAL_ADC_Start(&hadc1);                       // 啟動轉換
    HAL_ADC_PollForConversion(&hadc1, 100);      // 等待完成
    return HAL_ADC_GetValue(&hadc1);             // 讀取結果
}

// 轉換為電壓
float adc_to_voltage(uint32_t raw)
{
    return (float)raw * 3.3f / 4095.0f;  // 12-bit, Vref=3.3V
}

3.2 連續模式 + DMA (多通道)

Multi-channel Scan with DMA

圖 2:ADC 多通道掃描 + DMA 傳輸 — Timer 觸發轉換,DMA 自動將結果搬運到記憶體陣列。

#define ADC_CHANNELS  3
uint32_t adc_values[ADC_CHANNELS];

void MX_ADC1_DMA_Init(void)
{
    // ... 同上設定,修改以下參數
    hadc1.Init.ScanConvMode = ENABLE;          // 掃描模式
    hadc1.Init.ContinuousConvMode = ENABLE;    // 連續轉換
    hadc1.Init.NbrOfConversion = ADC_CHANNELS; // 3 通道
    HAL_ADC_Init(&hadc1);

    // 通道 0: PA0 (電位器)
    sConfig.Channel = ADC_CHANNEL_0;
    sConfig.Rank = 1;
    sConfig.SamplingTime = ADC_SAMPLETIME_15CYCLES;
    HAL_ADC_ConfigChannel(&hadc1, &sConfig);

    // 通道 1: PA1 (LM35 溫度)
    sConfig.Channel = ADC_CHANNEL_1;
    sConfig.Rank = 2;
    HAL_ADC_ConfigChannel(&hadc1, &sConfig);

    // 通道 2: PA2 (電池電壓)
    sConfig.Channel = ADC_CHANNEL_2;
    sConfig.Rank = 3;
    HAL_ADC_ConfigChannel(&hadc1, &sConfig);
}

void start_dma_adc(void)
{
    HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_values, ADC_CHANNELS);
}

// DMA half-complete / complete callback
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
    if (hadc->Instance == ADC1)
    {
        // adc_values[0] = PA0, adc_values[1] = PA1, adc_values[2] = PA2
        float v0 = adc_values[0] * 3.3f / 4095.0f;
        float temp = adc_values[1] * 3.3f / 4095.0f * 100.0f;  // LM35: 10mV/°C
        float batt = adc_values[2] * 3.3f / 4095.0f * 2.0f;     // 分壓 x2

        printf("Pot: %.2fV  Temp: %.1f°C  Batt: %.2fV\n", v0, temp, batt);
    }
}

3.3 雙重 ADC 交錯模式 (Dual Interleaved)

使用兩個 ADC 同時轉換同一通道,可將取樣率翻倍:

// ADC1 = Master, ADC2 = Slave (同步模式)
// 需要 HAL_ADCEx_MultiModeConfigChannel() 設定
// ADC1 轉換偶數次,ADC2 轉換奇數次
// 有效取樣率從 ~1.4 MSPS → ~2.8 MSPS

四、ESP32 ADC 實作

ESP32 有兩組 SAR ADC,總共支援 18 個通道(ADC1: 8ch, ADC2: 10ch)。注意 ADC2 與 Wi-Fi 共用,Wi-Fi 啟用時建議只用 ADC1。

4.1 基本 ADC 讀取 (Arduino)

#define POT_PIN   34   // ADC1_CH6
#define LIGHT_PIN 35   // ADC1_CH7
#define BAT_PIN   36   // ADC1_CH0 (SENSOR_VP)

void setup()
{
    Serial.begin(115200);
    analogReadResolution(12);   // 12-bit (0~4095)
    analogSetAttenuation(ADC_11db);  // 0~3.3V 全量程
}

void loop()
{
    int pot   = analogRead(POT_PIN);
    int light = analogRead(LIGHT_PIN);
    int bat   = analogRead(BAT_PIN);

    float v_pot   = pot   * 3.3f / 4095.0f;
    float v_light = light * 3.3f / 4095.0f;
    float v_bat   = bat   * 3.3f / 4095.0f * 2.0f;  // Voltage divider

    Serial.printf("Pot: %.2fV  Light: %.2fV  Batt: %.2fV\n",
                  v_pot, v_light, v_bat);
    delay(100);
}

4.2 ESP-IDF ADC 單次讀取

#include "driver/adc.h"
#include "esp_adc_cal.h"

#define DEFAULT_VREF 1100  // mV (typ. calibration)
static esp_adc_cal_characteristics_t adc_chars;

void adc_init(void)
{
    adc1_config_width(ADC_WIDTH_BIT_12);
    adc1_config_channel_atten(ADC1_CHANNEL_6, ADC_ATTEN_DB_11);  // GPIO34
    esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11,
                             ADC_WIDTH_BIT_12, DEFAULT_VREF, &adc_chars);
}

uint32_t read_adc_mv(void)
{
    uint32_t raw = adc1_get_raw(ADC1_CHANNEL_6);
    uint32_t mv  = esp_adc_cal_raw_to_voltage(raw, &adc_chars);
    return mv;  // mV 單位
}

4.3 ESP32 連續 ADC + DMA (ESP-IDF)

ESP32 的連續 ADC 驅動(Continuous ADC)使用硬體 FIFO + DMA,支援多通道自動掃描:

#include "esp_adc/adc_continuous.h"

adc_continuous_handle_t adc_handle;

void adc_continuous_init(void)
{
    adc_continuous_handle_cfg_t handle_cfg = {
        .max_store_buf_size = 1024,
        .conv_frame_size    = 256,
    };
    adc_continuous_new_handle(&handle_cfg, &adc_handle);

    adc_continuous_config_t dig_cfg = {
        .sample_freq_hz = 20000,         // 20 kHz 取樣率
        .conv_mode      = ADC_CONV_SINGLE_UNIT_1,
        .format         = ADC_DIGI_OUTPUT_FORMAT_TYPE2,
    };

    adc_digi_pattern_config_t patterns[2] = {
        {.atten = ADC_ATTEN_DB_11, .channel = 6,  .unit = ADC_UNIT_1, .bit_width = ADC_BIT_WIDTH_12},
        {.atten = ADC_ATTEN_DB_11, .channel = 7,  .unit = ADC_UNIT_1, .bit_width = ADC_BIT_WIDTH_12},
    };
    dig_cfg.adc_pattern = patterns;
    dig_cfg.pattern_num = 2;

    adc_continuous_config(adc_handle, &dig_cfg);
    adc_continuous_start(adc_handle);
}

void adc_read_task(void *pv)
{
    uint8_t buf[256];
    uint32_t ret_num = 0;

    while (1)
    {
        adc_continuous_read(adc_handle, buf, 256, &ret_num, pdMS_TO_TICKS(100));
        if (ret_num > 0)
        {
            for (int i = 0; i < ret_num; i += 4) // TYPE2: 4 bytes per sample { uint32_t raw = *(uint32_t *)&buf[i]; uint8_t ch = (raw >> 16) & 0xF;
                uint16_t val = raw & 0xFFF;
                printf("CH%d: %d (%.2fV)\n", ch, val, val * 3.3f / 4095.0f);
            }
        }
    }
}

五、實戰專案:資料記錄儀 (Data Logger)

整合 ADC + RTC + SPI Flash 實現長時間感測器記錄:

// 架構
// Sensor → ADC1_CH0 (PA0) ─┐
// Sensor → ADC1_CH1 (PA1) ─┤ DMA → adc_values[2] → Log to SD/Flash
// Sensor → ADC1_CH2 (PA2) ─┘

typedef struct {
    uint32_t timestamp;   // RTC timestamp
    uint16_t ch0;         // 12-bit raw
    uint16_t ch1;
    uint16_t ch2;
    uint16_t reserved;    // 4-byte alignment
} LogEntry;

#define LOG_SIZE 512
LogEntry log_buf[LOG_SIZE];
uint16_t log_index = 0;

void log_adc_data(void)
{
    log_buf[log_index].timestamp = get_rtc_sec();
    log_buf[log_index].ch0 = adc_values[0];
    log_buf[log_index].ch1 = adc_values[1];
    log_buf[log_index].ch2 = adc_values[2];
    log_index++;

    if (log_index >= LOG_SIZE)
    {
        flash_write_sector((uint8_t *)log_buf, LOG_SIZE * sizeof(LogEntry));
        log_index = 0;
    }
}

六、常見問題與除錯

6.1 讀值不穩定 / 跳動

  1. 硬體濾波:在 ADC 輸入腳加 100nF 電容對地
  2. 軟體濾波:取多次讀值後平均
  3. 取樣時間不足:增加 SamplingTime(特別是訊號源阻抗高時)
  4. 參考電壓雜訊:Vref+ 腳加濾波電容,不建議與數位電源共用
// 移動平均濾波
#define FILTER_TAP 16
uint32_t filter_buf[FILTER_TAP];
uint8_t filter_idx = 0;

uint32_t adc_filtered(void)
{
    filter_buf[filter_idx++] = read_adc();
    if (filter_idx >= FILTER_TAP) filter_idx = 0;

    uint32_t sum = 0;
    for (int i = 0; i < FILTER_TAP; i++) sum += filter_buf[i];
    return sum / FILTER_TAP;
}

6.2 轉換速度太慢

  • 提高 ADC Clock(STM32F4 最高 36 MHz,但建議 21 MHz)
  • 降低取樣週期(從 480 cycles → 3 cycles)
  • 使用 DMA + 連續模式(不佔 CPU)
  • 雙重 ADC 交錯模式(取樣率翻倍)

6.3 ESP32 ADC 非線性誤差

  • ESP32 ADC 在中間範圍 (0.2V~2.8V) 線性度較好,接近 Vref 或 GND 時誤差增大
  • 使用 esp_adc_cal 庫進行廠商校正(每個晶片不同)
  • 避免使用 ADC2 通道(與 Wi-Fi 衝突)
// ESP32 ADC 校正(更加精確)
uint32_t adc_calibrated_read(adc1_channel_t channel)
{
    uint32_t raw = adc1_get_raw(channel);
    uint32_t voltage = esp_adc_cal_raw_to_voltage(raw, &adc_chars);
    return voltage;  // 已校準到 mV
}

七、總結

ADC 是嵌入式系統中類比與數位世界的橋樑。無論是 STM32 還是 ESP32,掌握以下要點就能穩定獲取感測器資料:

  1. 解析度決定精度:12-bit = 0.8mV/LSB,16-bit = 50μV/LSB(Vref=3.3V)
  2. 取樣時間:訊號源阻抗越高,需要的取樣時間越長
  3. 硬體平均 vs 軟體濾波:硬體 oversampling 不耗 CPU,軟體濾波靈活
  4. DMA 是王道:連續多通道採集一定要用 DMA,CPU 可以專心做控制
  5. 參考電壓:ADC 的精度最終取決於 Vref 的穩定度
標籤: RS485 工業通訊
最後更新:2026 年 5 月 29 日

shi6a

這個人很懶,什麼都沒留下

點贊
< 上一篇

文章評論

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回覆

COPYRIGHT © 2026 0x6A Logbook. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang