LoginSignup
4
0

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-05-06

タイマ割り込みを使ってPWMの音を鳴らそう

前回、最後に loop()の中で ledcWrite() で力任せに音を鳴らしたものの、CPU負荷によって音程が変わっていた。ただのループなので当たり前なのではあるが。今回は負荷が変動しても音程がずれないように、loop() ではなくESP32のタイマ割り込み機能を使って実装してみたいと思う。

環境

使ったESP32 ボートは下の写真のもので、DOITの DEVKIT V1かその互換と思われる。
Arduino IDEは 1.8.12 である。前回同様、GPIO32 (IDE上はA4ピン)をPWM 出力として扱うものとする。
PWMとしては11bitにして、サンプリング周波数はほぼ上限の39kHzとした。
DOIT DEVKIT V1

まずはタイマ割り込み

タイマ割り込みの勉強から。ESP32 のサンプルコードでも十分なのですが、GPIO32 (A4 pin) でタイマ割り込み機能を使ってLEDチカチカをやってみる。
この例では ESP32 の timer0 を使って 1秒ごとにLEDをON/OFFします。

sample1.ino
// LED output on GPIO32 (A4 pin)
#define LEDC_PIN        A4

hw_timer_t *timer0 = NULL;
portMUX_TYPE timerMux0 = portMUX_INITIALIZER_UNLOCKED;
volatile uint8_t ledstat = 0; 

void IRAM_ATTR onTimer(){
  portENTER_CRITICAL_ISR(&timerMux0) ; // enter critical range
  ledstat = 1 - ledstat;
  digitalWrite(LEDC_PIN, ledstat);   // turn the LED on or off
  Serial.print("blinked on core ");
  Serial.println(xPortGetCoreID());
  portEXIT_CRITICAL_ISR(&timerMux0) ; // exit critical range
}

void setup() {
  Serial.begin(115200);
  pinMode(LEDC_PIN, OUTPUT);
  digitalWrite(LEDC_PIN, LOW);
  Serial.println("Start timer ");
  timer0 = timerBegin(0, 80, true);  // timer0, 12.5ns*80 = 1000ns, count-up
  timerAttachInterrupt(timer0, &onTimer, true); // edge-triggered 
  timerAlarmWrite(timer0, 1000000, true); // 1000000*1000ns = 1sec, auto-reload
  timerAlarmEnable(timer0); // enable timer0
}

void loop() {
//  vTaskDelay(portMAX_DELAY); 
  vTaskDelay(3000/portTICK_RATE_MS); // instead of delay(3000);
  Serial.println("done in loop()");
}

ESP32は tick という時間単位 (1tick = 80MHz = 12.5ns)を用いて、プリスケーラ(この例では80)やカウンタを動かして、カウンタが設定値(1,000,000)を超えた場合(12.5ns x 80 x 1,000,000 = 1秒)に割り込みが発生し、timerAttachInterrupt() で設定した onTimer() 関数に飛んで行きます。

onTimer() 関数は IRAM_ATTR という修飾辞が付いていますが、割り込み処理は速度を求められるので、おそらくflashメモリ上に展開されないようにするためと思われます。
portENTER_CRITICAL_ISR()~portEXIT_CRITICAL_ISR() はmutexを制御します。この例では一つの処理しかないので、なくても困らないと思います。
ESP32はデュアルコアプロセッサですが、xPortGetCoreID() 関数はどちらのコアで動いているか知ることができます。Arduino環境では どうも core #1上で動作するのが標準のようです。

loop() の中で vTaskDelay(portMAX_DELAY) とすると、二度と戻ってくることはないようです。数時間待てば戻ってくるのかもしれないですが。このサンプルコードでは、loop() から定期的に onTimer() に飛んでいることを確認するために、3秒ごとに println() させています。delay()関数の代わりに vTaskDelay() を使ってみました。tick に換算するために portTICK_RATE_MS で割ります。もちろん delay() で実装しても大丈夫です。

では音を鳴らそう

簡単なタイマ割り込みはできるようになったので、実際に音を鳴らしてみます。サンプリング周波数かそれより速い周期で割り込みをかけてPWMに波形の値を設定することで、原理的には音は鳴るはずです。
今回は波形メモリ音源を目指します。いわゆるSCC音源というやつと同じですな。テーブル (wave[]) に1波形の値を保存しておき(この例では WAVE_TBL_LENGTH = 32サンプルを1波形として)、発音周波数x32の周期でテーブルから読み出して ledcWrite() に書く、というのが基本コンセプトです。ただし、サンプリング周波数と発音周波数は非同期関係(というか無関係)にあるので、そのつじつま合わせがちょっと面倒です。ESP32はDSPを内蔵しているので除算もそれなりの時間で処理できるのかもしれませんが、時間の都合で調べ切れていないので、今回は整数演算でやっつけるようにしています。発音周波数の周期長からタイマ割り込みの度にその分を削って、負数になったら波形テーブルをひとつ進め、発音周波数の周期長も1周期分足す、というグダグダな処理になっています。
PWMのサンプリング周波数(LEDC_FREQ)は 39.0KHz としたので解像度は 11bitが上限になります。タイマ割り込み(INT_FREQ)は12.5us (80kHz) にしていますが、25.0us (40kHz > 39kHz)でも大丈夫のはずです。  

前回のコードと合体させてみました。GPIO32 (A4) からPWM出力します。 オクターブ4のラ(A4=440Hz) の音を鳴らします。単音の評価なので波形は派手な音色のノコギリ波にしました。三角波と正弦波もコメントとして残してあります。
onTimer() の中で pwm_tone を x8 しているのは、音量を稼ぐため (単音 = 8bit ⇒ 8倍すると 11bit で最大)です。それ以上の意味はありません。

sample2.ino
// LED output on GPIO32 (A4 pin)
#define LEDC_PIN        A4
#define LEDC_CHANNEL    0
#define LEDC_FREQ       39000
#define LEDC_TIMERBIT   11

// WAVE TABLE LENGTH
#define WAVE_TBL_LENGTH 32

// TIMER_INTERVAL (ns)
#define INT_FREQ 12500
// #define INT_FREQ 250  // WDT error

hw_timer_t *timer0 = NULL;
portMUX_TYPE timerMux0 = portMUX_INITIALIZER_UNLOCKED;

//int tone0 = 18181818 ; // A1 (55.000Hz)
//int tone0 = 9090909 ; // A2 (110.000Hz)
//int tone0 = 4545455 ; // A3 (220.000Hz)
int tone0 = 2272727 ; // A4 (440.000Hz)
//int tone0 = 1911129 ; // C5 (523.251Hz)
//int tone0 = 1516865 ; // E5 (659.255Hz)
//int tone0 = 1136364 ; // A5 (880.000Hz)
//int tone0 = 568182 ; // A6 (1760.000Hz)
//int tone0 = 284091 ; // A7 (3520.000Hz)
//int tone0 = 142045 ; // A8 (7040.000Hz)

volatile uint8_t tbl_index0 = WAVE_TBL_LENGTH -1; 
volatile int count_tone0 = 0; 

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

void IRAM_ATTR onTimer(){
  portENTER_CRITICAL_ISR(&timerMux0) ; // exit critical range
  count_tone0 -= INT_FREQ*WAVE_TBL_LENGTH ;
  while( count_tone0 <=0 ) {
      count_tone0 += tone0 ;
      tbl_index0 ++ ;
  }
  tbl_index0 &= (WAVE_TBL_LENGTH-1) ;
  pwm_tone = ((int) wave[tbl_index0])*8 ;
  ledcWrite(LEDC_CHANNEL, pwm_tone) ;
  portEXIT_CRITICAL_ISR(&timerMux0) ; // exit critical range
}

void setup() {
  Serial.begin(115200);
  // intialize wave table (Saw-tooth)
  for ( int i=0; i<WAVE_TBL_LENGTH; i++ ) {
    wave[i] = (uint8_t) i*(256/WAVE_TBL_LENGTH) ;
  }  
  /*
  // intialize wave table (Triangle)
  for ( int i=0; i<WAVE_TBL_LENGTH/2; i++ ) {
    wave[i]                   = (uint8_t) i*(512/WAVE_TBL_LENGTH) ;
    wave[WAVE_TBL_LENGTH/2+i] = (uint8_t) 255-wave[i] ;
  }
  // intialize wave table (Sine)
    for ( int i=0; i<WAVE_TBL_LENGTH; i++ ) {
    wave[i] = (uint8_t) 128+((double) 127*sin((float) 2*3.14159265*i/WAVE_TBL_LENGTH)) ;
  }
  for ( int i=0; i<WAVE_TBL_LENGTH; i++ ) {
    Serial.println(wave[i]) ;
  }
  */

  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); 
  timerAlarmWrite(timer0, 1, true); // 12.5us * 1 = 12.5us
  timerAlarmEnable(timer0); // enable
}

void loop() {
  vTaskDelay(portMAX_DELAY); 
}

ロジアナの波形は下図です。loop()の中はなにもしてませんが、タイマ割り込みによって440Hzで発生できていることがわかります。
saw440.png
周波数を上げていくと音が濁ってきます。波形メモリ音源の宿命ですが、周波数が上がってくると1サンプル出力する間にテーブルが2以上移動するようになります。この時すでに元の波形ではないので音色も変わってしまいます。今回のコードでは発音周波数が2kHz(オクターブ6)くらいまでなら、耐えられるかなという感じでした。

タイマ周期を短くしてみた

タイマ割り込み周期を短くするとどうなるのでしょうか? 上の sample2.ino で、INT_FREQを250 (250ns = 4MHz) にすると、 持続音ではなく、断続音が鳴ります。シリアルモニタには次のメッセージが出てきていることがわかりました。

serial-output
Guru Meditation Error: Core  1 panic'ed (Interrupt wdt timeout on CPU1)
Core 1 register dump:
PC      : 0x40089ffa  PS      : 0x00060034  A0      : 0x8008805e  A1      : 0x3ffbe710  
A2      : 0x3ffc1190  A3      : 0x00000000  A4      : 0x00000001  A5      : 0x00000001  
A6      : 0x00060023  A7      : 0x00000000  A8      : 0x00000001  A9      : 0x3ffb8058  
A10     : 0x00000003  A11     : 0x3ffb8058  A12     : 0x00000001  A13     : 0x00000001  
A14     : 0x00060023  A15     : 0x00000000  SAR     : 0x00000013  EXCCAUSE: 0x00000006  
EXCVADDR: 0x00000000  LBEG    : 0x400014fd  LEND    : 0x4000150d  LCOUNT  : 0xffffffff  
Core 1 was running in ISR context:
EPC1    : 0x400d26e2  EPC2    : 0x00000000  EPC3    : 0x00000000  EPC4    : 0x40089ffa

Backtrace: 0x40089ffa:0x3ffbe710 0x4008805b:0x3ffbe730 0x400d10ab:0x3ffbe770 0x40080ea8:0x3ffbe790 0x40080f9d:0x3ffbe7b0 0x400846cd:0x3ffbe7d0 0x400d26df:0x3ffb1fb0 0x400882f5:0x3ffb1fd0

Core 0 register dump:
PC      : 0x400e979a  PS      : 0x00060134  A0      : 0x800d45aa  A1      : 0x3ffbbff0  
A2      : 0x00000000  A3      : 0x00000001  A4      : 0x00000000  A5      : 0x00000001  
A6      : 0x00060320  A7      : 0x00000000  A8      : 0x800d4172  A9      : 0x3ffbbfc0  
A10     : 0x00000000  A11     : 0x40084e58  A12     : 0x00060320  A13     : 0x3ffbb9c0  
A14     : 0x00000003  A15     : 0x00060c23  SAR     : 0x00000000  EXCCAUSE: 0x00000006  
EXCVADDR: 0x00000000  LBEG    : 0x00000000  LEND    : 0x00000000  LCOUNT  : 0x00000000  

Backtrace: 0x400e979a:0x3ffbbff0 0x400d45a7:0x3ffbc010 0x400897e6:0x3ffbc030 0x400882f5:0x3ffbc050

Rebooting...
ets Jun  8 2016 00:22:57

WDT (watch dog timer) がタイムアウトしたからリセットしたべ、って言ってます。
割り込み周期を短くしたので、割り込み処理中にさらに割り込みがかかって詰まってしまった、ということだと思います。
タイマ割り込み周期 (INT_FREQ) は割り込み処理の重さによっても調整が必要ということですね。場合によってはデュアルコアのもう一方に処理させるなどの対応が必要になるのでしょう。

和音に挑戦

今度は和音を出してみます。サンプルとして4重和音を鳴らしてみました。コードでいうとAM7(ラ・ド#・ミ・ソ#) です。エレクトリックピアノっぽく正弦波の波形としました。
同時発生音数は MAX_NOTES で設定、配列 tonec[n] に発音する音の周期長を入れればよいです。
wave[] の値が 8bitで、PWMの分解能が 11bit なので、9音以上重なると桁あふれが起きます。pwm_toneを右シフトするなど、桁あふれ処理を追加してください。

sample3.ino
// LED output on GPIO32 (A4 pin)
#define LEDC_PIN        A4
#define LEDC_CHANNEL    0
#define LEDC_FREQ       39000
#define LEDC_TIMERBIT   11

// 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_A4_ 2257336   // A4 detuned (443.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 4 

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]] ;
  } 
  // pwm_tone *= 2 ; // boost
  ledcWrite(LEDC_CHANNEL, pwm_tone) ;
  portEXIT_CRITICAL_ISR(&timerMux0) ; // exit critical range
}

void setup() {
  Serial.begin(115200);
  int i ;
  /*  
  // intialize wave table (Saw-tooth)
  for ( i=0; i<WAVE_TBL_LENGTH; i++ ) {
    wave[i] = (uint8_t) i*(256/WAVE_TBL_LENGTH) ;
  }  
  // intialize wave table (Triangle)
  for ( i=0; i<WAVE_TBL_LENGTH/2; i++ ) {
    wave[i]                   = (uint8_t) i*(512/WAVE_TBL_LENGTH) ;
    wave[WAVE_TBL_LENGTH/2+i] = (uint8_t) 255-wave[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)) ;
  }
  // print wave value to debug
  for ( i=0; i<WAVE_TBL_LENGTH; i++ ) {
    Serial.println(wave[i]) ;
  }
  // initialize counter and preset tones
  for (i=0;i<MAX_NOTES;i++) {
    tbl_index[i] = WAVE_TBL_LENGTH -1; 
    count_tone[i] = 0; 
  }
  // AM7 chord
  tonec[0] = TONE_A4 ;
  tonec[1] = TONE_Db5 ;
  tonec[2] = TONE_E5 ;
  tonec[3] = TONE_Ab5 ;

//  Detuned A4
//  tonec[0] = TONE_A4 ;
//  tonec[1] = 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);
  timerAttachInterrupt(timer0, &onTimer, true);
  timerAlarmWrite(timer0, 1, true);
  timerAlarmEnable(timer0);
}

void loop() {
  vTaskDelay(portMAX_DELAY); 
}

複雑な波形になっていますが、足し合わされているのがわかりますね。
AM7.png

おまけ

ディチューン (少しずらした周波数で音を重ねることでうねり響かせる) もやってみたのですが、なぜかそれほどきれいにうなりませんでした。なぜかは分かりませんでした。
プログラムに間違いがあったためでした。今のコードは修正済みです。

4
0
0

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