プログラムと電子工作・楽曲演奏(4)割込み処理でDAC出力
楽曲演奏(3)では、楽曲データを平場(loop() 関数内)でデジタル・アナログ変換(DAC)して出力しました。この方法では演奏速度が CPUクロック周波数によって変化します。
今回の楽曲演奏(4)では、DACの周期をタイマ割込みで発生し、割込み処理で DAC出力します。この方法では演奏速度が CPUクロック周波数などの処理系に依存しません。その他は、楽曲演奏(3)と同様です。
割込み処理とは、人の仕事を例に取ると、書類を作成中に電話がかかってきた時、書類作成を一時中断して電話の対応後、また書類作成に戻るというような状況です。書類の作成が平場(loop() 関数内)で、電話応対が割込み処理です。
人の仕事でもそうですが、割込み処理が長すぎると本来の仕事ができなくなります。M5StickC Plus でも割込み処理時間が数10マイクロ秒を超えるとリブートします。割込みでできる処理は短時間で終わる処理だけです。
曲目はヴィヴァルディの「四季」より「春」の冒頭部分 8秒間です。
目標
外部接続した SpeakerHAT で音を鳴らします。
ヴィヴァルディの「四季」より「春」の冒頭部分を演奏します。
部品・機材
使用する部品は次のとおりです。SpeakerHAT を使用します。
電子部品
M5StickC Plus 1台
SpeakerHAT 1台[例:スイッチサイエンス]
開発用機材
PC(Windows10 または 11)、開発環境 Arduino-IDE 導入ずみ
USB-A・USB-C ケーブル
開発手順
M5StickC Plus に SpeakerHAT を接続する。
PC と M5StickC Plus を USBケーブルで接続する。
Arduino-IDE でスケッチ dacwrite2.ino を開く。
同じフォルダに vivaldi_1ch_16000hz_uint8.h を保存する。
検証・コンパイルする。
M5StickC Plus に書き込む。
上ボタン(押しボタンA)を押して、演奏を開始する。
曲が流れることを確認する。
スケッチ
dacwrite2.ino
#include <M5StickCPlus.h>
#include "vivaldi_1ch_16000hz_uint8.h"
#define SAMPLING_FREQ 16000
#define VOLUME 11 /*音量、0~11 最大*/
#define SPEAKER_PIN GPIO_NUM_26 /*SpeakerHAT 26ピン*/
// 割込み設定
#define ID_TIMER_0 0
#define COUNTUP true
#define EDGE_TRIGGER true
#define AUTORELOAD true
hw_timer_t* timer = nullptr;
// 演奏パラメータ
volatile const uint8_t* music_data_ptr; /*楽曲データポインタ*/
volatile uint32_t music_data_remained; /*楽曲データの残りバイト数*/
volatile uint8_t music_vol; /*音量、最大 1、最小 11*/
volatile bool stop_music = false; /*演奏停止要求フラグ、true 停止する*/
volatile bool is_playing = false; /*演奏中フラグ、true 演奏中*/
bool mute = false; /*ミュート*/
void IRAM_ATTR onTimer(void);
//------------------------------------------------------------------------------
// setup()
void setup() {
// 電源ON時に 1回だけ実行する処理をここに書く。
M5.begin(); /*M5を初期化する*/
M5.Axp.ScreenBreath(20); /*画面の輝度を少し下げる*/
M5.Lcd.setTextSize(3); /*文字サイズはちょっと小さめ*/
M5.Lcd.setRotation(3); /*上スイッチが左になる向き*/
M5.Lcd.println("dacwrite2");
/*setCpuFrequencyMhz(80);*/
/*240, 160, 80, 40, 20MHz と変えても演奏速度は変化しない。*/
/*40MHz以下ではウォッチドッグタイマがタイムアウトして、システムがリセットする。*/
Serial.begin(115200); /*デバッグ用のシリアル通信を初期化する*/
M5.Lcd.println("BtnA to start");
pinMode(SPEAKER_PIN, OUTPUT);
}
//------------------------------------------------------------------------------
// loop()
void loop() {
// 自動的に繰り返し実行する処理をここに書く。
M5.update();
if (M5.BtnA.wasPressed()) {
// 演奏を開始/中止する。
if (!is_playing) {
// --- 演奏中ではない。
// ヴィヴァルディ「春」を演奏する。
// M5StickC Plus の場合、SPEAKER_PINが GPIO_NUM_2では動作しない。
// GPIO_NUM_2には DA変換機能がない。
M5.Lcd.println("spring");
// ■ playMusic()関数はブロックされない。
Serial.println("*** playMusic()");
playMusic(wav, sizeof(wav) / sizeof(wav[0]), SAMPLING_FREQ, VOLUME);
is_playing = true;
}
else {
// --- 演奏中である。
// 演奏停止要求
stop_music = true;
}
}
if (M5.BtnB.wasPressed()) {
// 音量を変える。
if (!mute) {
setVolume(VOLUME / 2);
mute = true;
}
else {
setVolume(VOLUME);
mute = false;
}
}
delay(1);
}
//------------------------------------------------------------------------------
// dacwrite.ino スケッチの playMusic()以下を割込み処理に変更する。
//------------------------------------------------------------------------------
// setVolume
// 音量を設定する。
// in: uint8_t volume 音量、最大 11、最小 0
void setVolume(uint8_t volume)
{
music_vol = (volume >= 11) ? 1 : 11 - volume;
}
//------------------------------------------------------------------------------
// playMusic
// 楽曲データを演奏開始する。
// in: const uint8_t* music_data 8ビット符号なし整数の音の振幅データ
// const uint32_t length music_dataのバイト数
// const uint32_t sample_rate 楽曲データのサンプリング周波数
// const uint8_t volume 音量、最大 11、最小 0
// ・ wavファイルの場合は、dataチャンクを 8ビット符号なし整数に変換する。
// ・ M5StickC Plus の場合、SPEAKER_PIN は GPIO_NUM_25、GPIO_NUM_26 の 2ピンでのみ動作する。
// ・ 実験的に dacWrite()の実行時間が 20マイクロ秒かかることが判明したので、
// delay_interval は理論値から 20を減じる。なぜこれほど処理時間がかかるのか不明。
void playMusic(const uint8_t* music_data, const uint32_t length, const uint32_t sample_rate, const uint8_t volume)
{
uint32_t c1us = getApbFrequency() / 1000000; /*1usec: count up/down on clock count*/
uint32_t delay_interval = (uint32_t)1000000 / sample_rate;
music_data_ptr = music_data;
music_data_remained = length;
music_vol = (volume >= 11) ? 1 : 11 - volume;
// タイマを開始する。
timer = timerBegin(ID_TIMER_0, c1us, COUNTUP);
timerAttachInterrupt(timer, &onTimer, EDGE_TRIGGER);
timerAlarmWrite(timer, delay_interval, AUTORELOAD); /*interrupt on each music data byte*/
timerAlarmEnable(timer); /*to stop call timerAlarmDisable()*/
}
//------------------------------------------------------------------------------
// 割込み処理
// o 割込み処理は、楽曲のサンプリング周波数に応じた周期で起動される。
// o 正確な周期で、dacWrite()関数を実行するので、正しい速度の演奏になる。
void IRAM_ATTR onTimer() /*IRAM_ATTR:割込み処理関数は IRAM上に配置する。*/
{
static uint8_t last_data = 0;
if (timer != nullptr) {
if (!stop_music && music_data_remained > 0) {
// 曲を演奏する。
last_data = *music_data_ptr / music_vol;
dacWrite(SPEAKER_PIN, last_data);
music_data_ptr++;
music_data_remained--;
}
else {
// フェードアウトする。
if (last_data > 0) {
dacWrite(SPEAKER_PIN, last_data);
last_data--;
}
else {
// タイマを停止する。
timerAlarmDisable(timer);
timerDetachInterrupt(timer);
timerEnd(timer);
timer = nullptr;
dacWrite(SPEAKER_PIN, 0); /*音量を 0にする。*/
is_playing = false;
stop_music = false;
}
}
}
/*割込み処理は極短い処理のみ。*/
}
#include "vivaldi_1ch_16000hz_uint8.h" が、楽曲データです。const uint8_t wav[] PROGMEM = { 0x80, 0x80, 0x80, …}; という符号なし 8ビット整数の配列です。このデータは .wavファイルから python で生成しています(別の記事で紹介します)。
#define SAMPLING_FREQ 16000 は楽曲データのサンプリング周波数と一致させてください。
割込み設定
ID_TIMER_0:M5StickC、M5StickC Plus ではタイマ割込みは 4チャンネル使用できます。IDは 0~3です。
COUNTUP:タイマは、カウンタを増やす方向で使用します。
EDGE_TRIGGER:カウンタが設定値に達した瞬間に割込みを発生します。
AUTORELOAD:割込み発生後はカウンタを 0に戻して再度カウント開始します。割込みは周期的に発生します。
演奏パラメータ
割込み発生ごとに 1音データずつ DAC出力します。グローバル変数に、楽曲データポインタと楽曲の残りバイト数を記録します。
演奏停止要求フラグ、演奏中フラグは、割込み処理と平場(loop() 関数内)との間の情報のやり取り用変数です。明示的に「volatile」を付けてメモリ上に領域を確保します。(volatileの正確な意味は C言語の指導書を参照してください。)
playMusic()関数
playMusic()はその名に反して、楽曲データを演奏しません。割込み処理の準備を行うだけです。
割込み処理関数に渡すグローバル変数を初期化し、タイマを開始します。
onTimer()関数
楽曲を演奏します。周期的に起動されますので、曲のどこを演奏しているかを常に記憶しながら動作します。
平場(loop()関数内)の処理とは、グローバル変数を通じて情報をやり取りします。演奏を停止するときは、平場(loop()関数内)で stop_music変数を true にします。割込み処理で stop_music が true を検出したら直ちにフェードアウトを開始し、タイマを停止します。割込みが発生しなくなりますので、演奏は停止します。
演奏中かどうかを平場(loop()関数内)で知るには、is_playing変数をチェックします。
if文の thenブロックは、波形データを順番に出力する処理です。1音ずつのデータを dacWrite()で電圧 0~3.3Vに変換し、SPEAKER_PINへ出力します。データポインタを一つ進めます。
if文の elseブロックは、フェードアウト処理です。最後に出力した音量から徐々に小さくして最後に音量を 0にします。
dacWrite()の処理時間は処理系に依存しますが、割込み処理関数は一定の周期で起動するので、演奏速度は正確です。setup()内の setCpuFrequencyMhz(80); で CPUクロック周波数を変えても演奏速度は変わりません。
結果
上ボタン(押しボタンA)を押すと、曲が流れます。大きな音は出ないです。
このスケッチでは、演奏中に並行して他の処理ができます。演奏中に、上ボタン(押しボタンA)を押すと演奏が停止します。演奏中に、横ボタン(押しボタンB)を押すと音量を上げ下げできます。
練習問題
CPUクロックを 240, 160, 80, 40, 20MHzと変えてみて、演奏速度がどうなるか試してください。
様々な WAVEファイルを演奏してみてください。
参考
SpeakerHAT の回路図は、スイッチサイエンスの Webサイトに掲載されています。
参考にした関数 setVolume()、playMusic() は、次のファイルで定義されています。
C:\Users\<ユーザ>\Documents\Arduino\libraries\M5StickCPlus\src\utility\Speaker.h
C:\Users\<ユーザ>\Documents\Arduino\libraries\M5StickCPlus\src\utility\Speaker.cppサンプルスケッチ
Arduino-IDE メニュー→ファイル→スケッチ例→ESP32→Timer→RepeatTimerタイマ関数は次のファイルに定義されています。
C:\Users\<ユーザ>\AppData\Local\Arduino15\packages\m5stack\hardware\esp32\2.1.0\cores\esp32/esp32-hal-timer.h
C:\Users\<ユーザ>\AppData\Local\Arduino15\packages\m5stack\hardware\esp32\2.1.0\cores\esp32/esp32-hal-timer.c
コードを解釈して整理すると、下記のようになると思います。
/**
*
* o タイマ関数一覧:
* hw_timer_t* timerBegin(uint8_t num, uint16_t divider, bool countUp)
* // タイマを初期化する。
* // return: hw_timer* タイマ構造体
* // in: uint8_t num タイマ番号、0~3(M5StickC Plusはタイマ 4個)
* // uint16_t divider 分周比、1マイクロ秒を上げ/下げするカウント数
* // bool countUp true 上げ、false 下げ
* void timerAttachInterrupt(hw_timer_t* timer, void (*fn)(void), bool edge)
* // 割込み処理関数を紐付ける。
* // in: hw_timer_t* timer タイマ構造体
* // void (*fn)(void) 割り込みがトリガされたときに呼び出される関数
* // bool edge true 上げ/下げの変化でトリガする、false 状態でトリガする
* void timerAlarmWrite(hw_timer_t* timer, uint64_t alarm_value, bool autoreload)
* // トリガ発生条件を設定する。
* // in: hw_timer_t* timer タイマ構造体
* // uint64_t alarm_value トリガ発生周期(マイクロ秒)
* // bool autoreload true 自動的に繰り返す、false 1回限り
* void timerAlarmEnable(hw_timer_t* timer)
* // トリガ発生を許可する。
* // in: hw_timer_t* timer タイマ構造体
* void timerAlarmDisable(hw_timer_t* timer)
* // トリガ発生を禁止する。
* // in: hw_timer_t* timer タイマ構造体
* void timerDetachInterrupt(hw_timer_t* timer)
* // トリガ発生条件を解除する。
* // in: hw_timer_t* timer タイマ構造体
* void timerEnd(hw_timer_t* timer)
* // タイマを終了する。
* // in: hw_timer_t* timer タイマ構造体
*
*/
ライセンス
このページのソースコードは、複製・改変・配布が自由です。営利目的にも使用してかまいませんが、何ら責任を負いません。