Skip to content

Instantly share code, notes, and snippets.

@GOROman
Created August 17, 2026 06:03
Show Gist options
  • Select an option

  • Save GOROman/c4f17fd6cc945bcbf83b9132127f95eb to your computer and use it in GitHub Desktop.

Select an option

Save GOROman/c4f17fd6cc945bcbf83b9132127f95eb to your computer and use it in GitHub Desktop.
ESP32のLEDC PWMで聴けるオーディオを出す — 手法・落とし穴・サンプルコード(ノイズシェーピング/median-3/レジスタ直叩き)

ESP32 の LEDC PWM で「聴ける」オーディオを出す — 手法と落とし穴とサンプルコード

ESP32-S3 には DAC がない。それでも LEDC(PWM ペリフェラル)+抵抗2本とコンデンサで、 62.5kHz サンプリングのステレオ再生が実用品質でできる。 YM2151 実チップのデジタル音声出力を ESP32 でデコードして鳴らすプロジェクト で実戦投入した手法・工夫・ハマりどころをまとめる。

基本構成

サンプル値(9bit) → LEDC PWM (78.125kHz キャリア) → RC ローパス → DCカット → アンプ
  • キャリア 78.125kHz / 9bit: 80MHz ÷ 2^9 ÷ 2。キャリアが可聴域の遥か上、 かつ後段フィルタで落としやすい
  • サンプルごとにデューティを更新するだけ。デューティ=そのサンプルの電圧
#include <driver/ledc.h>

#define PIN_AUDIO  2
#define PWM_FREQ   78125   // 80MHz / 512 / 2
#define PWM_RES    9       // duty 0-511, 中点 256

void audioBegin() {
  ledcAttachChannel(PIN_AUDIO, PWM_FREQ, PWM_RES, /*ch=*/0);
  ledcWrite(PIN_AUDIO, 256);           // 無音 = 中点
}

// サンプルレートのタイマ/割り込み/ループから呼ぶ
void audioWrite(int16_t pcm) {         // pcm: -32768..32767
  uint32_t duty = ((int32_t)pcm + 32768) >> 7;   // 16bit -> 9bit
  ledcWrite(PIN_AUDIO, duty);
}

ハマりどころ(全部実際に踏んだ)

1. LEDC の分周比制約 — 10bit にできない理由

freq × 2^res がクロック源を超える(分周比 < 1)と div_param=0 で 静かに失敗する。80MHz源で 78.125kHz なら 9bit が上限 (10bit にしたければキャリアを 39kHz に落とす)。

2. LEDC は全タイマーが「クロック源」を共有している

別用途(高周波クロック生成など)で LEDC_USE_APB_CLK を明示すると、 Arduino の ledcAttach(AUTO=XTAL を選びがち)と timer clock conflict になり、片方が黙って壊れる。複数用途で使うなら全タイマーの clk_cfg を揃えること。

3. Arduino の「チャネル→タイマー」自動割り当て

ledcAttachChannel(pin, f, res, ch) のタイマーは ch/2 で決まる。 ch0/1 = timer0、ch6/7 = timer3。別周波数で使いたい2系統を ch6 と ch7 に 置くと同じタイマーを取り合って周波数がどちらかに化ける。 同周波数のステレオ L/R は ch0/ch1(同一タイマー)がむしろ好都合 (周波数変更が1回で両chに効く)。

4. ledcWrite() は遅い — ホットパスはレジスタ直叩き

タイトなループ(今回は 2MHz 信号のポーリング内)から ledcWrite を呼ぶと 数µs 消えて他の処理を取りこぼす。LL API なら ~100ns:

#include <hal/ledc_ll.h>

static inline void dutyFast(ledc_channel_t ch, uint32_t duty) {
  ledc_ll_set_duty_int_part(&LEDC, LEDC_LOW_SPEED_MODE, ch, duty);
  ledc_ll_set_duty_start(&LEDC, LEDC_LOW_SPEED_MODE, ch, true);
  ledc_ll_ls_channel_update(&LEDC, LEDC_LOW_SPEED_MODE, ch);
}

音質を上げる工夫

1次ノイズシェーピング(9bit なのに静か)

16bit ソースを 9bit に丸める誤差を次サンプルへ繰り越すと、量子化ノイズが 高域へ移動して RC フィルタで削れる。クリップ時に誤差が無限に溜まる 「ワインドアップ」対策のクランプが必須:

static int32_t nsErr = 0;

uint32_t shape(int32_t pcm16) {              // pcm16: -32768..32767
  int32_t acc  = (pcm16 + 32768) + nsErr;
  int32_t duty = acc >> 7;                   // 16bit -> 9bit
  if (duty < 0) duty = 0; else if (duty > 511) duty = 511;
  nsErr = acc - (duty << 7);                 // 誤差繰り越し
  if (nsErr > 127) nsErr = 127;              // アンチワインドアップ
  else if (nsErr < -128) nsErr = -128;
  return duty;
}

注意: シェーピングは後段の LPF が前提。 フィルタなし(カップリング コンデンサ直結)だと高域に追いやったノイズがアンプに素通しで、 むしろ「サー」が増えて聴こえる。

median-3 でスパイク(ぶちぶち)除去

入力データが稀に化ける環境(EMI 等)では、1サンプルの飛び値が 「ぶちぶち」ノイズになる。3タップメディアンで単発スパイクだけ消せる (音楽波形への影響は実質なし):

static int32_t h1 = 256, h2 = 256;

int32_t med3(int32_t a, int32_t b, int32_t c) {
  int32_t lo = a < b ? a : b, hi = a < b ? b : a;
  return c < lo ? lo : (c > hi ? hi : c);   // = median(a,b,c)
}

uint32_t despike(int32_t duty) {
  int32_t out = med3(h2, h1, duty);
  h2 = h1; h1 = duty;                        // 履歴は常に更新する
  return out;
}

負数の左シフトは UB

v << 6 は v が負だと C/C++ で未定義動作。乗算で書く(v * 64)。 最適化で同じコードになる。

アナログ側

GPIO ──[1kΩ]──┬──[10nF]── GND      ← 1次LPF fc≒16kHz
              └──[1µF]──→ アンプ入力  ← DCカット必須(PWMはVdd/2中心)
  • fc は「R×C ≒ 10µs」あたり。手持ち部品で 2.4kΩ+4.7nF(fc≒14kHz)でも同等
  • 直列Rはアンプ入力インピーダンスとの分圧になる。10kΩ だと10k入力の アンプで音量半減、1k前後なら実質無視できる
  • DCカットなしで直結しないこと(無音時もデューティ50%の矩形が出ている)

小ネタ

  • PWM で音量調整: 起動音などの矩形波トーンは、デューティを 50% から 数%に絞ると音量が下がる(3%で約-20dB)。ledcWriteTone は 50% 固定なので ledcChangeFrequency + ledcWrite(pin, 小さい値)
  • 周波数のリアルタイム変更: ledc_set_freq() は動作中でも安全に変えられる。 戻り値のエラーチェックを忘れずに(範囲外は失敗して周波数が変わらない)
  • サンプルレートよりキャリアが速い限り、デューティ更新は「次のPWM周期から」 反映される。62.5kHz 更新 × 78kHz キャリアで実用上問題なし

結果

この構成(9bit + ノイズシェーピング + median + RC 1次)で、YM2151 の FM 音源をステレオ再生して「レトロ実機として十分楽しめる」品質が出ている。 さらに上を目指すなら PCM5102 等の I2S DAC へ(配線3本で16bit化できる)。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment