見出し画像

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();
    }
}

この記事が気に入ったらサポートをしてみませんか?