
DAC 是什麼?跟「PWM 假類比」差在哪
DAC 的工作用一句話講完:把一個數字,變成一個電壓。你給它 128,它輸出 1.65 V;你給它 255,它輸出 3.3 V。
很多教學會先教你用 PWM 加 RC 濾波來「做類比輸出」,那確實能用,而且幾乎每顆 MCU 都有 PWM。但它的本質是用方波的平均值騙人:漣波大、反應慢、精度取決於電容與電阻的誤差。

真正的 DAC 輸出的是穩定電壓,不需要靠濾波器把方波抹平。這就是為什麼做音頻、做訊號產生器、做可程式電源時,大家還是會去找有 DAC 的 MCU。
關鍵規格:解析度決定你能調多細
DAC 的所有規格裡,最該先看的是解析度,因為它決定「你最小能調動多少伏特」:

公式只有一條: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 會變成必要條件。
一次轉換要經過哪幾步

注意流程圖裡的第二個判斷點:「寫入」與「更新」是兩件事。有些 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:讓硬體自己餵資料
把上面的做法畫成圖就很清楚:

這條鏈路只做一件事:讓「時間到了就換下一個值」變成硬體行為。CPU 的工作只剩下準備表格與修改參數。
這也解釋了下一節的對照表為什麼差距那麼大:

原始規格對照表在這裡:
| 功能 | 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 還有三個結構性限制,直接決定了它適合做什麼、不適合做什麼:

實戰一:可程式電源供應器
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 與選架構就不會再靠感覺了。
文章評論