ESP32のPWMを使って 16bitの解像度で 思い通りに音が鳴らない
音楽を ESP32で鳴らしたい。
analogWrite()の代わりに ledcWrite()という関数を使って、これで16bit解像度の音を鳴らそうとしたけれども、ピーという高い音(今思えば約1220Hz)しか鳴らない。何が起こっているのか調べた。
Arduino環境で ESP32 のPWMを調べてみる。
使ったESP32 ボートは下の写真のもので、DOITの DEVKIT V1かその互換と思われる。
Arduino IDEは 1.8.12 である。
書き込みについては、コンパイル中は [EN]ボタンを押し、"Connecting..." と出てきたら離して[BOOT]ボタンを書き込みが始まるまで押せばよい。
今回は GPIO32 (IDE上はA4ピン)をPWM 出力として扱うものとする。
量子化ビット16bit, サンプリング周波数 24kHzのつもりが...?
下のようなプログラムをつくって鳴らしてみると、ピーという音が出てくる。
// Audio output on GPIO32 (A4 pin)
#define LEDC_PIN A4
#define LEDC_CHANNEL 0
#define LEDC_FREQ 24000
#define LEDC_TIMERBIT 16
void setup() {
ledcSetup(LEDC_CHANNEL, LEDC_FREQ, LEDC_TIMERBIT) ;
ledcAttachPin(LEDC_PIN, LEDC_CHANNEL) ;
ledcWrite(LEDC_CHANNEL, pow(2,LEDC_TIMERBIT-1));
}
void loop() {
}
ロジックアナライザで撮った画面はこんな (上が GPIO35の出力、一番下はローパスフィルタ後のアナログ信号):
ピー音の正体はこれだ。結局、PWMの1期間が約820usなので約1.2kHz。24kHzなんて最初から無理だったのだ。
ではいくつの周波数までなら有効・妥当なのか?
ESP32のクロックは80MHz。ledcSetupで指定できるビット数は 1bitから指定できる。理屈からすると、
ledcSetup 設定上限周波数 = 80MHz÷2^(ビット数)
表にしてみると
ビット数 | 分解能 | 上限設定周波数 |
---|---|---|
16 | 65536 | 1.2207kHz |
12 | 4096 | 19.53125kHz |
11 | 2048 | 39.0625kHz |
8 | 256 | 312.5kHz |
1 | 2 | 40MHz |
ということで 分解能256でよければ 312.5KHzまで上げられるが、16bitだと1.22KHzまで下がってしまう。
分解能=2 すなわち 0→1→0→1→0→... を繰り返すのは クロック周波数の半分の 40MHz が上限になるので正しそうである。
##11bitにすれば 24kHzは可能なのか?
先の理屈で正しいのか、実際に次のようなコードで試してみた。
// Audio output on GPIO32 (A4 pin)
#define LEDC_PIN A4
#define LEDC_CHANNEL 0
#define LEDC_FREQ 24000
#define LEDC_TIMERBIT 11
void setup() {
ledcSetup(LEDC_CHANNEL, LEDC_FREQ, LEDC_TIMERBIT) ;
ledcAttachPin(LEDC_PIN, LEDC_CHANNEL) ;
ledcWrite(LEDC_CHANNEL, pow(2,LEDC_TIMERBIT-1));
}
void loop() {
}
pow(2,LEDC_TIMERBIT-1) = 1024 すなわち分解能 2048 の 50% の duty でPWM出力されるはずである。
ロジックアナライザの波形はこうだった:
##では 440Hzの音を鳴らそう。
PWM周波数24kHzで 440Hzの音を11bitで鳴らそうと思う。とはいっても ledcWriteTone() を使うので、矩形波となる。
// Audio output on GPIO32 (A4 pin)
#define LEDC_PIN A4
#define LEDC_CHANNEL 0
#define LEDC_FREQ 24000
#define LEDC_TIMERBIT 11
void setup() {
ledcSetup(LEDC_CHANNEL, LEDC_FREQ, LEDC_TIMERBIT) ;
ledcAttachPin(LEDC_PIN, LEDC_CHANNEL) ;
ledcWriteTone(LEDC_CHANNEL, 440);
}
void loop() {
}
ロジアナの波形はこうである。正しく出力していると思う。
一番下のアナログ波形の肩が取れているのはローパスフィルタ通過後だからだ。
うるさいだけだが 短時間で A4(440Hz)→A5(880Hz)→A6(1760Hz)を繰り返すようにした場合はこうである。
// Audio output on GPIO32 (A4 pin)
#define LEDC_PIN A4
#define LEDC_CHANNEL 0
#define LEDC_FREQ 24000
#define LEDC_TIMERBIT 11
void setup() {
ledcSetup(LEDC_CHANNEL, LEDC_FREQ, LEDC_TIMERBIT) ;
ledcAttachPin(LEDC_PIN, LEDC_CHANNEL) ;
}
void loop() {
ledcWriteTone(LEDC_CHANNEL, 440);
delay(20);
ledcWriteTone(LEDC_CHANNEL, 880);
delay(20);
ledcWriteTone(LEDC_CHANNEL, 1760);
delay(20);
}
ファミコンのレーザービームとか警報音のような音が鳴る。
ノコギリ波を出してみる。
今度は saw-tooth 波形を出してみる。
11bit分、すなわち 0から2048まで 8ずつ増やすので、256回のループで1波形分を出力するようにした。
// Audio output on GPIO32 (A4 pin)
#define LEDC_PIN A4
#define LEDC_CHANNEL 0
#define LEDC_FREQ 24000
#define LEDC_TIMERBIT 11
int i = 0;
void setup() {
ledcSetup(LEDC_CHANNEL, LEDC_FREQ, LEDC_TIMERBIT) ;
ledcAttachPin(LEDC_PIN, LEDC_CHANNEL) ;
}
void loop() {
ledcWrite(LEDC_CHANNEL, i);
i = i + 8 ;
if ( i > 2048 ) { i = 0 ; }
// delayMicroseconds(1) ; // ignore this
}
ロジアナ上は約655.45Hzで繰り返しているように見える。アナログ波形はガタガタしているのは、LPFの定数が適切でないのと、ロジアナのアナログ入力は解像度が悪い(8bitかな?) のとが理由だ。
音色は金管楽器ぽいノコギリ波の音に聞こえる。
ノコギリ波 1波形が 1525.6us なので、loop()内の1回まわるのに約5.9593usかかっていることになる。
調子に乗って三角波
// Audio output on GPIO32 (A4 pin)
#define LEDC_PIN A4
#define LEDC_CHANNEL 0
#define LEDC_FREQ 24000
#define LEDC_TIMERBIT 11
int i = 0;
int d = 16;
void setup() {
ledcSetup(LEDC_CHANNEL, LEDC_FREQ, LEDC_TIMERBIT) ;
ledcAttachPin(LEDC_PIN, LEDC_CHANNEL) ;
}
void loop() {
ledcWrite(LEDC_CHANNEL, i);
i = i + d ;
if ( i > 2048 ) {
i = 2048 ;
d = -16 ;
} else if ( i < 0 ) {
i = 0 ;
d = 16 ;
}
}
拡大するとこんな感じ。一番上が GPIO32の出力。一番下がLPF通過後のアナログ波形。PWM動作だ。
正弦波
sin()の計算は単純な加算よりは時間がかかる。
// Audio output on GPIO32 (A4 pin)
#define LEDC_PIN A4
#define LEDC_CHANNEL 0
#define LEDC_FREQ 24000
#define LEDC_TIMERBIT 11
float i = 0;
void setup() {
ledcSetup(LEDC_CHANNEL, LEDC_FREQ, LEDC_TIMERBIT) ;
ledcAttachPin(LEDC_PIN, LEDC_CHANNEL) ;
}
void loop() {
ledcWrite(LEDC_CHANNEL, 1024+1024*sin(i));
i = i +0.08 ;
}
ただし、ずっと音を鳴らしていると、音が下がったり1オクターブ上がったりする。
loop() の中に書くのではなく、ちゃんと一定時間ごとに割り込みをかけて波形を更新する必要があるね。
それにしても、LPF後の波形が汚い。適切な値にしたローパスフィルタを作らねば。