0x6A Logbook

0x6A Logbook
Shi6a的筆記本
  1. 首頁
  2. 程式開發
  3. 正文

DAC 數位類比轉換器完整教學:從原理到 ESP32/STM32 波形產生實作

2026 年 6 月 9 日 621點熱度 0人點贊 0條評論

DAC 數位類比轉換器完整教學封面

DAC 是什麼?跟「PWM 假類比」差在哪

DAC 的工作用一句話講完:把一個數字,變成一個電壓。你給它 128,它輸出 1.65 V;你給它 255,它輸出 3.3 V。

很多教學會先教你用 PWM 加 RC 濾波來「做類比輸出」,那確實能用,而且幾乎每顆 MCU 都有 PWM。但它的本質是用方波的平均值騙人:漣波大、反應慢、精度取決於電容與電阻的誤差。

DAC 在訊號鏈中的位置

真正的 DAC 輸出的是穩定電壓,不需要靠濾波器把方波抹平。這就是為什麼做音頻、做訊號產生器、做可程式電源時,大家還是會去找有 DAC 的 MCU。

關鍵規格:解析度決定你能調多細

DAC 的所有規格裡,最該先看的是解析度,因為它決定「你最小能調動多少伏特」:

解析度與 LSB 的關係

公式只有一條:LSB = Vref ÷ 2ⁿ。8-bit 在 3.3 V 下每階 12.9 mV,12-bit 是 0.8 mV。

但這裡有個很容易被誤導的地方:位元數不等於有效精度。雜訊、參考電壓的穩定度、PCB 佈局與接地,都會把低幾位直接吃掉。一顆標榜 12-bit 的 DAC 在雜亂的板子上,實際可能只有 9 到 10 個有效位元。

其他幾個要看的規格:

規格 說明 典型值
解析度 (Resolution) 數位輸入的位元數,決定電壓階數 8-bit (256階), 12-bit (4096階)
LSB (Least Significant Bit) 最小可變化的電壓步進 = Vref / 2ⁿ 3.3V/256=12.9mV (8-bit)
3.3V/4096=0.8mV (12-bit)
建立時間 (Settling Time) 數值更新到輸出穩定的時間 ~10 μs(一般)
更新率 (Update Rate) 每秒可更新多少次輸出 ~1 Msps(STM32 DAC)
輸出範圍 DAC 可輸出的最低/最高電壓 0V ~ Vref (通常=VDD)
DNL/INL 非線性誤差(差分/積分) ±1 LSB 以內

其中 Settling Time(建立時間)最常被忽略:它決定你「多快能改一次輸出」。一般約 10 µs,看起來很快,但換算成更新率就只有 100 kHz 級別 —— 這就是為什麼做訊號產生器時,DMA 會變成必要條件。

一次轉換要經過哪幾步

DAC 轉換時序流程

注意流程圖裡的第二個判斷點:「寫入」與「更新」是兩件事。有些 MCU 允許你先把資料寫進緩衝,等觸發事件(例如 Timer)到了才一起轉換 —— 這正是波形產生器的基礎:讓 Timer 規律觸發,輸出就成了等間隔的取樣點。

另外有些機型有 LDAC 腳位,可以讓多個 DAC 通道同時更新輸出。做雙通道訊號(例如 I/Q 或雙電源)時,這個腳位決定了兩個通道會不會有先後差。

四種常見波形怎麼產生

四種常見波形與產生方式

原理其實都很樸素:把要輸出的電壓事先算好、排成一張表,時間到了就依序丟給 DAC。正弦波用查表法(LUT)預先算 256 點的 sin 值,鋸齒波與三角波甚至不用查表,直接讓計數器上下跑就是波形。

ESP32 實作:先從最基本的輸出開始

ESP32 的 DAC 藏在 GPIO 25 與 26 上(DAC1/DAC2),寫一行就出電壓:

// ESP32 DAC - 基本電壓輸出
#define DAC_PIN 25  // DAC1

void setup() {
    Serial.begin(115200);
    // DAC 初始化(Arduino 中不需額外設定)
}

void loop() {
    // 輸出 1.65V(中點電壓)
    // dacWrite(value): value=0→0V, value=255→3.3V
    dacWrite(DAC_PIN, 128);  // 128/255 * 3.3V ≈ 1.65V
    delay(2000);

    // 輸出 0V
    dacWrite(DAC_PIN, 0);
    delay(2000);

    // 輸出 3.3V
    dacWrite(DAC_PIN, 255);
    delay(2000);
}

要產生正弦波就得自己算表,再用固定間隔更新:

// ESP32 DAC - 正弦波產生器(查表法)
#define DAC_PIN 25

// 256 點正弦波查找表(8-bit 值)
const uint8_t sine_table[256] = {
    #include "sine_table.h"  // 或直接內建陣列
};

// 手動產生正弦表
void generate_sine_table() {
    Serial.println("const uint8_t sine_table[256] = {");
    for (int i = 0; i < 256; i++) {
        float v = sin(2.0 * PI * i / 256.0);
        int dac_val = (int)((v + 1.0) * 127.5);  // 映射到 0~255
        if (i % 16 == 0) Serial.println();
        Serial.printf("%3d,", dac_val);
    }
    Serial.println("}");
}

void setup() {
    Serial.begin(115200);
    // generate_sine_table(); // 第一次執行後複製結果
}

void loop() {
    static int idx = 0;

    // 直接輸出正弦值
    dacWrite(DAC_PIN, sine_table[idx]);
    idx = (idx + 1) % 256;

    delayMicroseconds(40); // ~100 Hz 正弦波 (256*40μs≈10ms)
}

想連續輸出、不想讓 CPU 一直忙,可以用 ESP-IDF 的 DAC 驅動搭配 DMA:

// ESP-IDF - DAC + DMA 連續波形輸出
#include "driver/dac.h"
#include "driver/dac_common.h"

#define DAC_CHANNEL DAC_CHANNEL_1  // GPIO25

// 8-bit 正弦波資料 (256 點)
uint8_t waveform[256];

void app_main() {
    // 初始化正弦波資料
    for (int i = 0; i < 256; i++) {
        float v = sin(2.0 * M_PI * i / 256.0);
        waveform[i] = (uint8_t)((v + 1.0) * 127.5);
    }

    // 啟用 DAC
    dac_output_enable(DAC_CHANNEL);

    // 連續輸出(DMA 硬體驅動,CPU 幾乎不用管)
    while (1) {
        for (int i = 0; i < 256; i++) {
            dac_output_voltage(DAC_CHANNEL, waveform[i]);
            esp_rom_delay_us(40);
        }
    }
}

STM32 實作:HAL 與 TIM 觸發

STM32 的 DAC 通常放在 PA4/PA5,用 HAL 也很簡單:

// STM32 HAL - DAC 基本輸出(STM32F4)
// PA4 = DAC_OUT1, PA5 = DAC_OUT2

void MX_DAC_Init(void)
{
    DAC_ChannelConfTypeDef sConfig = {0};

    hdac.Instance = DAC;

    // 初始化 DAC
    HAL_DAC_Init(&hdac);

    // 設定 Channel 1
    sConfig.DAC_Trigger      = DAC_TRIGGER_NONE;     // 軟體觸發
    sConfig.DAC_OutputBuffer = DAC_OUTPUTBUFFER_ENABLE;
    HAL_DAC_ConfigChannel(&hdac, &sConfig, DAC_CHANNEL_1);
}

// 輸出指定電壓
void dac_set_voltage(uint16_t mv)
{
    // 計算 DAC 碼值: Data = (mv * 4095) / 3300
    // 3.3V Vref, 12-bit
    uint32_t dac_val = (uint32_t)mv * 4095 / 3300;
    if (dac_val > 4095) dac_val = 4095;

    // 設定 DAC 輸出值
    HAL_DAC_SetValue(&hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, dac_val);
    HAL_DAC_Start(&hdac, DAC_CHANNEL_1);
}

// 使用範例
int main()
{
    HAL_Init();
    SystemClock_Config();
    MX_DAC_Init();

    HAL_DAC_Start(&hdac, DAC_CHANNEL_1);

    while (1)
    {
        // 輸出 1.65V
        HAL_DAC_SetValue(&hdac, DAC_CHANNEL_1,
                         DAC_ALIGN_12B_R, 2048);
        HAL_Delay(1000);

        // 輸出 0.5V
        HAL_DAC_SetValue(&hdac, DAC_CHANNEL_1,
                         DAC_ALIGN_12B_R, 621);
        HAL_Delay(1000);

        // 輸出 3.0V
        HAL_DAC_SetValue(&hdac, DAC_CHANNEL_1,
                         DAC_ALIGN_12B_R, 3723);
        HAL_Delay(1000);
    }
}

真正好用的是 TIM + DMA 自動連續輸出:Timer 每次溢位就觸發一次轉換,DMA 負責把表格裡的下一個值搬到 DAC,CPU 完全不用插手。

// STM32 - DAC + TIM + DMA 自動連續輸出(無 CPU 干預)
// 這是最有效率的方式

#define WAVE_POINTS 256
uint16_t sine_wave_12bit[WAVE_POINTS];

// 產生 12-bit 正弦波資料
void generate_sine_12bit()
{
    for (int i = 0; i < WAVE_POINTS; i++) {
        float v = sin(2.0 * M_PI * i / WAVE_POINTS);
        sine_wave_12bit[i] = (uint16_t)((v + 1.0) * 2047.5);
    }
}

// 設定 TIM6 觸發 DAC(自動更新)
void MX_DAC_DMA_Init()
{
    // ... DAC 初始化 ...

    // 設定 DAC 觸發源為 TIM6
    sConfig.DAC_Trigger = DAC_TRIGGER_T6_TRGO;
    HAL_DAC_ConfigChannel(&hdac, &sConfig, DAC_CHANNEL_1);

    // 啟動 DAC + DMA(循環模式)
    HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1,
                      (uint32_t*)sine_wave_12bit,
                      WAVE_POINTS, DAC_ALIGN_12B_R);

    // 啟動 TIM6(觸發頻率決定波形頻率)
    // 若 TIM6 頻率 = 25600 Hz, 則正弦頻率 = 100 Hz
    HAL_TIM_Base_Start(&htim6);
}

// 在 HAL_DAC_ConvCpltCallbackCh1 中可監控 DMA 完成
void HAL_DAC_ConvCpltCallbackCh1(DAC_HandleTypeDef* hdac)
{
    // DMA 完成一個循環(可選)
}

DAC + DMA:讓硬體自己餵資料

把上面的做法畫成圖就很清楚:

DAC 加 DMA 的資料流

這條鏈路只做一件事:讓「時間到了就換下一個值」變成硬體行為。CPU 的工作只剩下準備表格與修改參數。

這也解釋了下一節的對照表為什麼差距那麼大:

ESP32 與 STM32 的更新率差距

原始規格對照表在這裡:

功能 ESP32 STM32 (F4/H7)
解析度 8-bit 12-bit
通道數 2 個 2 個(獨立/雙通道模式)
DMA 支援 有限(需連續 CPU 寫入) 完整(TIM+DMA 自動輸出)
內建 Buffer 無 有(Output Buffer)
雙通道同步 各自獨立 支援同時更新(LDAC)
波形產生 軟體計算 硬體三角波產生器(部分型號)
更新率 ~200 kHz (軟體) ~1 Msps (DMA)

差距的來源不是誰的 DAC 比較「準」,而是資料搬運的方式。ESP32 的 DAC 基本上要 CPU 逐點寫入,所以更新率被壓在約 200 kHz;STM32 有完整的 TIM + DMA 鏈路,能穩定跑到 1 Msps。差了 5 倍,但兩者都足以應付一般應用。

更實際的影響是「CPU 要不要陪著燒」。ESP32 用軟體連續寫 DAC,CPU 幾乎被吃滿;STM32 交給 DMA 之後,CPU 可以去處理其他事情。這一點在同時要做通訊或顯示的專案裡差很多。

而 ESP32 還有三個結構性限制,直接決定了它適合做什麼、不適合做什麼:

ESP32 DAC 的三個限制與繞道

實戰一:可程式電源供應器

DAC 最實用的應用之一是「可調電源」:DAC 決定輸出電壓,ADC 讀回來確認實際值,形成一個閉環。

// ESP32 - 可程式電源(DAC 控制 + ADC 回讀)
#include <driver/dac.h>
#include <driver/adc.h>

#define DAC_OUT 25     // 控制輸出電壓
#define ADC_FB  34     // 回讀實際電壓

void setup() {
    Serial.begin(115200);
    dac_output_enable(DAC_CHANNEL_1);
    adc1_config_width(ADC_WIDTH_BIT_12);
    adc1_config_channel_atten(ADC1_CHANNEL_6, ADC_ATTEN_DB_11);
}

void set_voltage(float mv) {
    // 換算 DAC 值(假設 OP Amp 電路增益 = 2)
    uint8_t dac_val = (uint8_t)(mv * 255 / 6600);
    dac_output_voltage(DAC_CHANNEL_1, dac_val);
}

float read_output() {
    int raw = adc1_get_raw(ADC1_CHANNEL_6);
    return raw * 3.3 * 2 / 4095.0 * 1000;  // mV
}

void loop() {
    // 設定 2.5V 輸出
    set_voltage(2500);
    delay(100);
    float actual = read_output();
    Serial.printf("設定: 2500mV, 實際: %.0fmV\n", actual);

    delay(2000);
}

這個模式在實驗室很好用 —— 想測試某個電路在某個電壓下的反應,不用手轉可變電阻,直接打一行指令。

實戰二:簡單音頻播放器

音頻是 DAC 最經典的用途:把 WAV 的取樣值依序丟給 DAC,後面接一級放大驅動喇叭。

// ESP32 - 簡易 WAV 播放器(DAC + SD + I2S 或 DAC)
// 實際使用建議用 I2S + MAX98357,此處示範 DAC 直出

#define DAC_PIN 25

// 8-bit PCM 資料直接餵給 DAC
void play_pcm(const uint8_t* data, uint32_t len, uint32_t sample_rate)
{
    uint32_t delay_us = 1000000 / sample_rate;

    for (uint32_t i = 0; i < len; i++) {
        dacWrite(DAC_PIN, data[i]);
        delayMicroseconds(delay_us);
    }
}

// WAV 檔案格式
typedef struct __attribute__((packed)) {
    char     riff[4];         // "RIFF"
    uint32_t fileSize;
    char     wave[4];         // "WAVE"
    char     fmt[4];          // "fmt "
    uint32_t fmtSize;         // 16
    uint16_t audioFormat;     // 1 = PCM
    uint16_t numChannels;     // 1 = Mono
    uint32_t sampleRate;      // 8000, 22050, 44100
    uint32_t byteRate;
    uint16_t blockAlign;
    uint16_t bitsPerSample;   // 8 or 16
    char     data[4];         // "data"
    uint32_t dataSize;
} wav_header_t;

// 從 SD 讀取 WAV 並播放(簡化版)
void play_wav(const char* filename) {
    // 1. 開啟 WAV 檔案
    // 2. 解析 Header
    // 3. 取得 PCM 資料位址和長度
    // 4. 用 play_pcm() 播放
    // 5. 關閉檔案
    Serial.println("播放 WAV (需實作 SD 讀取)");
}

但要說實話:ESP32 用 DAC 直出播音樂只是「能響」。8-bit 解析度配上 12.9 mV 的階距,底噪會很明顯。真要聽音樂,正解是 I2S + 外接 DAC(例如 MAX98357),把數位訊號直接送到專門的音訊晶片。

應用場景與選型

最後回到最實務的問題:什麼場合該用哪顆?

應用 所需解析度 更新率 適合 MCU
LED 亮度控制 8-bit 即可 ~100 Hz ESP32
音頻輸出 8~16-bit 8~48 kHz ESP32/I2S 或 STM32
可程式電源 10~12-bit ~10 kHz STM32
訊號產生器 12-bit ~1 MHz STM32 + DMA
馬達控制 8~10-bit ~20 kHz 兩者皆可

這張表其實可以簡化成一句話:看你要「解析度」還是要「速度」。LED 亮度與馬達控制只需要 8-bit、幾十 kHz,ESP32 就很夠;音頻要的是 8 到 16-bit 搭配 8 到 48 kHz 的穩定取樣;而訊號產生器要 12-bit 與 1 MHz,那就是 STM32 加 DMA 的領域。

總結

DAC 這個主題看起來很硬,但其實可以收斂成四件事:

  • 解析度決定最小步進:LSB = Vref ÷ 2ⁿ,先算這個再談其他
  • Settling Time 決定更新率上限:約 10 µs 的建立時間意味著 100 kHz 級別的極限
  • DMA 決定 CPU 要不要陪燒:有 TIM + DMA 才是真正的波形產生器
  • 位元數不等於有效精度:雜訊與佈局會吃掉低幾位

把這四點想清楚,選 MCU 與選架構就不會再靠感覺了。

標籤: 教學
最後更新:2026 年 9 月 30 日

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