4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Arduino環境でESP32のPWMを調べる(3)

Last updated at Posted at 2020-05-11

音質の問題

前回、ESP32のタイマ割り込みを使って波形を正しくPWM変調させて出力させることまでやったものの、あまり音質が良くないよなと思っていました。
ESP32では量子化ビット幅11bitだとPWM周波数は約39kHzが上限です。もちろん半分の18.5kHzというのは可聴周波数域のかなり上のほうにはなってますが、実際に聞こえる音は結構ノイジーです。歪とは違う雑音が聞こえてきます。どうなっているのか、実際に出力している音を FFT で分析してみましょう。

外付けローパスフィルタ

PWMの出力に R1とC1からなる簡単なLPF(low pass filter)をつけています。カットオフ周波数は約15.9kHzです。C2は直流成分をカットするためのキャパシタです。無信号時に 0V にするために付けます。
LPF_fc16kHz.png

11bit, 39kHz PWM

正弦波 440Hz の音を、量子化ビット11bit, PWM周波数39KHz で上のLPF通過後の波形をFFTにかけるとこうでした(赤線は peak)。

sine440_pwm_39kHz_11bit-1.png

-60~-70dB程度のノイズがたくさん... こんなものかなと思うものの、波形がちょっと階段状になっています。もちろん、前回の波形メモリ音源(32個のテーブル)なのである程度は仕方がないです。
これをロジアナのアナログ入力で波形を見ると

sine440_pwm_39kHz_11bit-2.png

あらら... 太い正弦波ですね... 何が起きているのでしょうか??
拡大すると...

sine440_pwm_39kHz_11bit-3.png

PWMのサイクルに応じて細かい up/down がジグザグと入ってます。これが太く見えている元凶ですね。

LPF fc=1.59kHz

高い周波数成分を減衰するために、先の LPF の C1 を 10倍大きく(0.01uF⇒0.1uF) にしました。 カットオフ周波数は 1.59KHzになり、かなり高音が抑制されるはずです。

sine440_pwm_39kHz_11bit-1-fc1600Hz.png

7kHz以上は目立って右肩下がりで高音域を抑制しているのは分かります。

sine440_pwm_39kHz_11bit-2-fc1600Hz.png

正弦波のガタガタもだいぶきれいになりました。

sine440_pwm_39kHz_11bit-3-fc1600Hz.png

PWMのスイッチングノイズもかなり平坦化されているのがわかります。でもこれは LPF で通常の高音も抑制してしまうこととになり、やりたいこととは違うんですよね。

D級アンプなどの仕様を色々調べると、PWMの周波数は数百KHzとか (39KHzより) もっと高いところで使っているのですね。LPFで減衰してもPWMのスイッチングノイズは出てしまうのですが可聴範囲外の高いところでスイッチさせるなら(あまり)聞こえないでしょ、ということですね。
いままので私の試行では、11bit を確保するために PWM周波数を下げていたのでPWMのノイズが聞こえてきてしまっていました。では量子化ビット数を下げてでも PWM周波数を上げたらどうなるか調べてみましょう。

8bit, 312kHz PWM

ESP32では、量子化ビット8bitならPWM周波数は312KHzに上げられます。

sample_8bit_312k_pwm.inno
// PWM tone with timer interrupt
// LED output on GPIO32 (A4 pin)
# define LEDC_PIN        A4
# define LEDC_CHANNEL    0
# define LEDC_FREQ       312000
# define LEDC_TIMERBIT   8

// tone definition
# define TONE_A1  18181818  // A1  (55.000Hz)
# define TONE_A2  9090909   // A2  (110.000Hz)
# define TONE_A3  4545455   // A3  (220.000Hz)
# define TONE_A4  2272727   // A4  (440.000Hz)
# define TONE_A4_ 2267574   // A4 detuned (441.000Hz)
# define TONE_C5  1911129   // C5  (523.251Hz)
# define TONE_Db5 1803866   // C#5 (554.365Hz)
# define TONE_E5  1516865   // E5  (659.255Hz)
# define TONE_Ab5 1203936   // G#5 (830.609Hz)
# define TONE_A5  1136364   // A5  (880.000Hz)
# define TONE_A6  568182    // A6  (1760.000Hz)
# define TONE_A7  284091    // A7  (3520.000Hz)
# define TONE_A8  142045    // A8  (7040.000Hz)

// WAVE TABLE LENGTH
# define WAVE_TBL_LENGTH 32

// TIMER INTERVAL (ns)
# define INT_FREQ 25000

// MAX NOTES
# define MAX_NOTES 1

hw_timer_t *timer0 = NULL;
portMUX_TYPE timerMux0 = portMUX_INITIALIZER_UNLOCKED;

volatile int tonec[MAX_NOTES] ;
volatile uint8_t tbl_index[MAX_NOTES] ; 
volatile int count_tone[MAX_NOTES] ; 

volatile int pwm_tone = 0; 
uint8_t wave[WAVE_TBL_LENGTH] ;

void IRAM_ATTR onTimer(){
  portENTER_CRITICAL_ISR(&timerMux0) ; // exit critical range
  pwm_tone = 0;
  for ( int i=0; i<MAX_NOTES; i++ ) {
    count_tone[i] -= INT_FREQ*WAVE_TBL_LENGTH ;
    while( count_tone[i] <=0 ) {
        count_tone[i] += tonec[i] ;
        tbl_index[i] ++ ;
    }
    tbl_index[i] &= (WAVE_TBL_LENGTH-1) ;
    pwm_tone += (int) wave[tbl_index[i]] ;
  } 
  ledcWrite(LEDC_CHANNEL, pwm_tone) ;
  portEXIT_CRITICAL_ISR(&timerMux0) ; // exit critical range
}

void setup() {
  Serial.begin(115200);
  int i ;
  // intialize wave table (Sine)
    for ( i=0; i<WAVE_TBL_LENGTH; i++ ) {
    wave[i] = (uint8_t) 128+((double) 127*sin((float) 2*3.14159265*i/WAVE_TBL_LENGTH)) ;
  }
  // initialize counter and preset tones
  for (i=0;i<MAX_NOTES;i++) {
    tbl_index[i] = WAVE_TBL_LENGTH -1; 
    count_tone[i] = 0; 
  }
  tonec[0] = TONE_A4 ;

  ledcSetup(LEDC_CHANNEL, LEDC_FREQ, LEDC_TIMERBIT) ;
  ledcAttachPin(LEDC_PIN, LEDC_CHANNEL) ;

  Serial.println("======= start timer ============");
  timer0 = timerBegin(0, INT_FREQ/12.5, true);  // timer 0, 12.5ns*1000 = 12.5us, count up
  timerAttachInterrupt(timer0, &onTimer, true); // edge (not level) triggered 
  timerAlarmWrite(timer0, 1, true); // 12.5us * 1 = 12.5us
  timerAlarmEnable(timer0); // enable
}

void loop() {
  vTaskDelay(portMAX_DELAY); 
}

sine440_pwm_312kHz_8bit-1.png

波形の階段は見えますが、聞いた感じでは11→8bit にした分の音質劣化は分かりませんでした。 波形メモリの粗さが原因ならば、440Hz程度では差が出ないのでしょうね。

sine440_pwm_312kHz_8bit-3.png

PWM周波数を39kHz⇒312kHz に大幅に引き上げたことで、PWMスイッチングノイズも細かく小さくなっているのがわかります。

さらにダメ押しでLPFもカットオフ周波数1.59kHz版にすればさらにノイズは高音とともに減衰します。
sine440_pwm_312kHz_8bit-1-fc1600Hz.png

ここまでくればPWMスイッチングノイズは相当小さくなっています。

sine440_pwm_312kHz_8bit-3-fc1600Hz.png

内蔵DACとおなじビット幅になってしまった。

PWM スイッチングノイズを回避するために PWM周波数を上げると 量子化ビット数は下げなければならない。結局 8bit になってしまいました。......ということは 内蔵DAC でいいのでは?
ということで、内蔵DAC (8bit)で鳴らしてみて比較します。GPIO25が DAC1 の出力ピンに固定されていますので、それに先の LPF をつけて鳴らします。

timer_dac.inno
// DAC tone with timer interrupt
//
// DAC output is GPIO25 
# define DAC_PIN        25

// tone definition
# define TONE_A1  18181818  // A1  (55.000Hz)
# define TONE_A2  9090909   // A2  (110.000Hz)
# define TONE_A3  4545455   // A3  (220.000Hz)
# define TONE_A4  2272727   // A4  (440.000Hz)
# define TONE_A4_ 2267574   // A4 detuned (441.000Hz)
# define TONE_C5  1911129   // C5  (523.251Hz)
# define TONE_Db5 1803866   // C#5 (554.365Hz)
# define TONE_E5  1516865   // E5  (659.255Hz)
# define TONE_Ab5 1203936   // G#5 (830.609Hz)
# define TONE_A5  1136364   // A5  (880.000Hz)
# define TONE_A6  568182    // A6  (1760.000Hz)
# define TONE_A7  284091    // A7  (3520.000Hz)
# define TONE_A8  142045    // A8  (7040.000Hz)

// WAVE TABLE LENGTH
# define WAVE_TBL_LENGTH 32

// TIMER INTERVAL (ns)
# define INT_FREQ 25000

// MAX NOTES
# define MAX_NOTES 1

hw_timer_t *timer0 = NULL;
portMUX_TYPE timerMux0 = portMUX_INITIALIZER_UNLOCKED;

volatile int tonec[MAX_NOTES] ;
volatile uint8_t tbl_index[MAX_NOTES] ; 
volatile int count_tone[MAX_NOTES] ; 

volatile int pwm_tone = 0; 
uint8_t wave[WAVE_TBL_LENGTH] ;

void IRAM_ATTR onTimer(){
  portENTER_CRITICAL_ISR(&timerMux0) ; // exit critical range
  pwm_tone = 0;
  for ( int i=0; i<MAX_NOTES; i++ ) {
    count_tone[i] -= INT_FREQ*WAVE_TBL_LENGTH ;
    while( count_tone[i] <=0 ) {
        count_tone[i] += tonec[i] ;
        tbl_index[i] ++ ;
    }
    tbl_index[i] &= (WAVE_TBL_LENGTH-1) ;
    pwm_tone += (int) wave[tbl_index[i]] ;
  } 
  dacWrite(DAC_PIN, pwm_tone) ;
  portEXIT_CRITICAL_ISR(&timerMux0) ; // exit critical range
}

void setup() {
  Serial.begin(115200);
  int i ;
  // intialize wave table (Sine)
    for ( i=0; i<WAVE_TBL_LENGTH; i++ ) {
    wave[i] = (uint8_t) 128+((double) 127*sin((float) 2*3.14159265*i/WAVE_TBL_LENGTH)) ;
  }
  // initialize counter and preset tones
  for (i=0;i<MAX_NOTES;i++) {
    tbl_index[i] = WAVE_TBL_LENGTH -1; 
    count_tone[i] = 0; 
  }
  tonec[0] = TONE_A4 ;

  Serial.println("======= start timer ============");
  timer0 = timerBegin(0, INT_FREQ/12.5, true);  // timer 0, 12.5ns*1000 = 12.5us, count up
  timerAttachInterrupt(timer0, &onTimer, true); // edge (not level) triggered 
  timerAlarmWrite(timer0, 1, true); // 12.5us * 1 = 12.5us
  timerAlarmEnable(timer0); // enable
}

void loop() {
  vTaskDelay(portMAX_DELAY); 
}

もう内蔵DACでいいのでは...?

FFTで見てみると
sine440_dac_8bit-1.png

階段状にはなっているものの

sine440_dac_8bit-2.png

PWMスイッチングの up/down は見られないことから LPF は 元のR1, C1 のままでよくなりました。

sine440_dac_8bit-3.png

小難しい PWM で頭を悩ますよりはシンプルに DAC でいいではないか、と思いました。

おまけ

このあと、内蔵I2Sは内蔵DACからも音を出せるという事実に気づきました。
どおりで ESP32では PWMを使ったオーディオの記事が少ないわけですね。みなさん I2S⇒内蔵DAC(8bit)で音を出しているのですね。もともと、I2S はDACを外付けしなければらならないと思い込んでいて、外付けしないで簡単に音を出したい、というのがPWM調査の動機だったので...。
次回からは I2S での発音方法を調べます。

関連記事

Arduino環境でESP32のPWMを調べる(1)
Arduino環境でESP32のPWMを調べる(2)

4
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?