Arduino UNO R4 modular synthesizer VCO
Arduino UNO R4互換ボードでモジュラーシンセのVCOを試作したので備忘録。
背景
2023年にArduino UNO R4がリリースされた。
他のマイコンボードと比較して、処理速度や価格で劣るため、少し使いにくい感じもする。
一方で、入出力が5Vに対応しているため、モジュラーシンセにおいてはバッファ回路が不要になり、少ない部品でデジタルモジュールを作成できるのは嬉しい。
12bit DACも追加となったため、正確なCV出力が必要なクオンタイザーや、オシレータも追加部品なしで作成ができる。
今回は、Arduino UNO R4でモジュラーシンセのVCOが作成可能なのかを検証するためのプロトタイプを作成した。
制作物のスペック
電源:5V
Wave selectPOT:4種類のwavetableの選択(SAW/SQU/TRI/SIN)
Pitch tune POT:周波数の調整
V/OCT CV IN:pitch制御用CV入力
OSC OUT:音声出力、5Vp-p
技術検証用の試作なので、機能は最低限としている。
V/octの精度はソフトウェアキャリブレーションにより、0.5%以下の精度となっている。VCOとして十分使える精度かと思う。
Arduino UNO R4互換ボード
FLINT ProMicro R4という小型の互換ボードを使用した。秋葉原で購入したもの。
https://flint.works/p/flint-promicro-r4/
日本国外でもArduino UNO R4の互換ボードは販売されている。おそらく、近いうちに中国製の安価な互換品もでてくるだろう。
ただし、ルネサス製MCUの価格が$5くらいするので、UNO R3ほど安くはならないと思っている。
ハードウェア
技術検証用の試作なので、部品は最小限としている。
実際にモジュラーシンセとして使う場合は、出力回路と入力回路に保護回路が必要になる。
スピーカーには直接つながず、途中にミキサーなどを挿入することを推奨する。
ソフトウェア
割り込み
WASHIYAMA GIKENさんがArduino UNO R4用のタイマー割り込みライブラリを公開しているので使わせていただいた。
任意の時間、任意の周期で割り込み処理ができる。
今回、80kHzで割り込みをかけて、DAC出力値を更新している。
80kHzという数字に深い意味はない。あくまで技術検証の試作品なので、限界の速度まで割り込みを早くした。(なお、160kHzにしたら動作不安定となた)
#include "AGTimerR4.h"
#define FREQ_SAMPLING 80000.0f
DAC出力
今回一番苦労したポイント。
Arduinoでは「analogWrite」という関数でDACやPWM出力をすることが一般的だ。
Arduino UNO R4においてもanalogWriteを用いてDAC出力することは出来るのだが、命令をするたびに一瞬だけDAC出力が落ちる問題に直面した。
同様の現象は他のユーザーでも再現しているようだ。AnalogWriteを実行するたびにpinの初期化をしているらしい。
この状態で音声出力すると、不快なノイズが出るため、とてもオシレータとして使うことはできない。
analogWriteを使わずにDAC出力する方法はいくつかあるが、今回はGrumpy-Mike さんのDAC出力まわりのコードをそのまま使わせてもらい、対策することができた。感謝です。
音声出力
過去に作成したRaspberry pi pico VCOをベースにした。
一定周期で割り込みを掛けて、wavetableを一定間隔で進めて、出力するというもの。
アンチエイリアス処理をするのが一般的だが、私は理解できなかった&コーディングの技術力もないので、処理してない。
使用するMCUによってはV/octに誤差があるかもしれない。
pitch_calbの値を0.9~1.1くらいの範囲で調整することで、V/octの精度を上げることができる。
float pitch_calb = 0.97;
宣伝:オープンソースプロジェクトの支援をお願いします
DIYモジュラーシンセのオープンソースプロジェクトを継続するために、patreonというサービスでパトロンを募集しています。
コーヒー一杯の支援をいただけると嬉しいです。
また、パトロン限定のコンテンツも配信しています。
ソースコード
粗末だが公開する。悪い点があれば指摘を貰えると嬉しい。
float table_progress = 0;
float pitch = 0;
float pitch_calb = 0.97;
int selectwave;//select waveform by ADC
long timer = 0;
//wavetable
float voct_table[2048];
const int tableSize = 256; // table size
uint16_t sawtoothTable[tableSize]; // saw
uint16_t squareTable[tableSize]; // squ
uint16_t triangleTable[tableSize]; // tri
uint16_t sineTable[tableSize]; // sine
// 12-Bit D/A Converter.The reference source for the DAC settings is below.
//https://github.com/Grumpy-Mike/Game_of_Life_with_sound
#define DACBASE 0x40050000 // DAC Base - DAC output on A0 (P014 AN09 DAC)
#define DAC12_DADR0 ((volatile unsigned short *)(DACBASE + 0xE000)) // D/A Data Register 0
#define DAC12_DACR ((volatile unsigned char *)(DACBASE + 0xE004)) // D/A Control Register
#define DAC12_DADPR ((volatile unsigned char *)(DACBASE + 0xE005)) // DADR0 Format Select Register
#define DAC12_DAADSCR ((volatile unsigned char *)(DACBASE + 0xE006)) // D/A A/D Synchronous Start Control Register
#define DAC12_DAVREFCR ((volatile unsigned char *)(DACBASE + 0xE007)) // D/A VREF Control Register
#define MSTP_MSTPCRD ((volatile unsigned int *)(MSTP + 0x7008)) // Module Stop Control Register D
#define MSTPD20 20 // DAC12 - 12-Bit D/A Converter Module
#define MSTP 0x40040000 // Module Registers
#define MSTP_MSTPCRB ((volatile unsigned int *)(MSTP + 0x7000)) // Module Stop Control Register B
#define PFS_P014PFS ((volatile unsigned int *)(PORTBASE + P000PFS + (14 * 4))) // A0 / DAC12
#define PORTBASE 0x40040000 /* Port Base */
#define P000PFS 0x0800 // Port 0 Pin Function Select Register
//timer setting
//https://github.com/washiyamagiken/AGTimer_R4_Library
#include "AGTimerR4.h"
#define FREQ_SAMPLING 80000.0f
volatile bool samplingStat = false;
void setup() {
//AGTimerR4.h setting
AGTimer.init(FREQ_SAMPLING, timerCallback);
AGTimer.start();
// make V/oct table
for (int i = 0; i < 2048; ++i) {
voct_table[i] = 1 * pow(2, i / 204.8);
}
// make wavetable
// saw
for (int i = 0; i < tableSize; i++) {
sawtoothTable[i] = map(i, 0, tableSize-1, 0, 4095);
}
// square
for (int i = 0; i < tableSize; i++) {
if (i < tableSize / 2) {
squareTable[i] = 0;
} else {
squareTable[i] = 4095;
}
}
// triangle
for (int i = 0; i < tableSize; i++) {
triangleTable[i] = map(i, 0, tableSize/2, 0, 4095);
if (i > tableSize / 2) {
triangleTable[i] = 4095 - triangleTable[i];
}
}
// sine
for (int i = 0; i < tableSize; i++) {
sineTable[i] = 2047 + 2047 * sin(2 * PI * i / tableSize);
}
timer = micros();
//DAC setting
*MSTP_MSTPCRD &= ~(0x01 << MSTPD20); // Enable DAC12 module
*DAC12_DADPR = 0x00; // DADR0 Format Select Register - Set right-justified format
// *DAC12_DAADSCR = 0x80; // D/A A/D Synchronous Start Control Register - Enable
*DAC12_DAADSCR = 0x00; // D/A A/D Synchronous Start Control Register - Default
// 36.3.2 Notes on Using the Internal Reference Voltage as the Reference Voltage
*DAC12_DAVREFCR = 0x00; // D/A VREF Control Register - Write 0x00 first - see 36.2.5
*DAC12_DADR0 = 0x0000; // D/A Data Register 0
delayMicroseconds(10); // Needed delay - see data sheet
*DAC12_DAVREFCR = 0x01; // D/A VREF Control Register - Select AVCC0/AVSS0 for Vref
*DAC12_DACR = 0x5F; // D/A Control Register -
delayMicroseconds(5); // Needed delay - see data sheet
*DAC12_DADR0 = 2048; // D/A Data Register 0 - value of mid range bias
*PFS_P014PFS = 0x00000000; // Port Mode Control - Make sure all bits cleared
*PFS_P014PFS |= (0x1 << 15); // ... use as an analog pin
}
//DAC output
void timerCallback() {
if (table_progress < 256) {
if(selectwave==0){
*DAC12_DADR0 = sawtoothTable[(int)table_progress];
}
if(selectwave==1){
*DAC12_DADR0 = squareTable[(int)table_progress];
}
if(selectwave==2){
*DAC12_DADR0 = triangleTable[(int)table_progress];
}
if(selectwave==3){
*DAC12_DADR0 = sineTable[(int)table_progress];
}
table_progress=table_progress+0.15*pitch;
}
if (table_progress >= 256) {
table_progress = table_progress-256;
}
}
void loop() {
if (timer + 10 <= micros()) {
pitch = voct_table[(int)(analogRead(A1)*pitch_calb)+analogRead(A3)/4];// pitch set
selectwave = analogRead(A2)/256;//wave select
timer = micros();
}
}
この記事が気に入ったらサポートをしてみませんか?