見出し画像

Seeed Studio XIAO RA4M1 modular synthesizer VCO

Seeed Studio XIAO RA4M1でモジュラーシンセのVCOを試作したので備忘録。

背景

以前、Arduino UNO R4互換ボードを使ったVCOを作成した。

DACを内蔵しており、処理速度もUNO R3と比べて高速なため、VCOとして用いることに適していた。
ただし、Arduino UNO R4は互換ボードを含め、価格が高額なため、DIYで気軽に手を出すことが難しい事情があった。

XIAO RA4M1

そんな中、Seeed studioよりXIAO RA4M1がリリースされた。

Arduino UNO R4と同じRenesas RA4M1を搭載しており、開発言語もArduinoに対応している。Arduino UNO R4の互換機として使用ができそうだ。
さらに、価格も$4.99と非常に安価で小型であるため、モジュラーシンセの開発に理想的なマイコンボードなのだ。

Seeed Studio XIAO RA4M1を使ったモジュール制作に先立ち、シンプルなVCOの作成を企画したのだ。
プロジェクトの内容はArduino UNO R4 VCOをベースにし、XAIO用に一部回路を変更している。

制作物のスペック

電源:USB typeC 5V

Wave selectPOT:4種類のwavetableの選択(SAW/SQU/TRI/SIN)
Pitch tune POT:周波数の調整
V/OCT CV IN:pitch制御用CV入力(0-5V)
OSC OUT:音声出力、3.3Vp-p

技術検証用の試作なので、機能は最低限としている。

V/octの精度はソフトウェアキャリブレーションにより、0.5%以下の精度となっている。VCOとして十分使える精度かと思う。

ハードウェア

シンプルさを優先させたため、保護回路やコンデンサを省略したシンプルなものとなっている。CVとOCTには、XIAOの電圧定格3.3Vを超える電圧が印加されないよう注意。

電源はUSB type-Cから5V給電をしている。

可変抵抗の電源は3.3Vである。
Arduion UNO R4はマイコンのリファレンス電圧が5Vなのに対し、XIAOでは3.3Vと違いがあるためだ。
V/octの入力も33kohmと68kohmの分圧により5V→3.3Vへの変換をしている。

ソフトウェア

割り込み

WASHIYAMA GIKENさんがArduino UNO R4用のタイマー割り込みライブラリを公開しているので使わせていただいた。
任意の時間、任意の周期で割り込み処理ができる。

今回、80kHzで割り込みをかけて、DAC出力値を更新している。
80kHzという数字に深い意味はない。あくまで技術検証の試作品なので、限界の速度まで割り込みを早くした。(なお、160kHzにしたら動作不安定となた)

#include "AGTimerR4.h"
#define FREQ_SAMPLING 80000.0f

copy

DAC出力

UNO R4にはanalogWriteを使用した際に"出力が一瞬0Vに落ちる"という致命的な問題があった。XIAOではその問題は解消しているようだが、analogWriteを使用したDAC出力では正常に動作しなかったため、DAC周りのコードは前回のコードを流用している。

Grumpy-Mike さんのDAC出力まわりのコードをそのまま使わせていただいた。感謝申し上げる。

V/oct

使用するMCUや抵抗によってはV/octに誤差があるかもしれない。
pitch_calbの値を0.8~1.2くらいの範囲で調整することで、V/octの精度を上げることができる。

float pitch_calb = 1.172;

宣伝:オープンソースプロジェクトの支援をお願いします

DIYモジュラーシンセのオープンソースプロジェクトを継続するために、patreonというサービスでパトロンを募集しています。
コーヒー一杯の支援をいただけると嬉しいです。
また、パトロン限定のコンテンツも配信しています。

ソースコード

粗末だが公開する。悪い点があれば指摘を貰えると嬉しい。

#include <AGTimerR4.h>

float table_progress = 0;
float pitch = 0;
float pitch_calb = 1.172;
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();
  }
}

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