CV Out and Gate are done. Here's the new interface board:
From left to right (er... bottom to top): TRS MIDI output, Gate Out, CV Out.
CV Out uses an MCP4725 I2C 12-bit DAC. I love I2C because I could build an interface with bunches of these without running out of pins on the microcontroller. But for now, just one. It has a range of 0 to (slightly under) 5V, which is an iffy range for Eurorack, but not uncommon.
Here's the full thing:
I'm not a fan of the rat king look, but it'll get worse before it gets better. I'll solder up a cleaner permanent unit once everything is done. I have no idea what to do about a housing at this point.
Here's the Arduino sketch that supports CV/Gate/MIDI out:
Code: Select all
#include <Adafruit_MCP4725.h>
#include <EEPROM.h>
// Data lines (bits 0 - 7)
// Start at bit 7 (pin 5), and end at bit 0 (pin 12)
const int UPORT7 = 5;
// Control lines
const int VCB1 = 2;
const int VCB2 = 3;
// CV Gate
const int GATEOUT = 4;
// CV
Adafruit_MCP4725 dac;
int volt_ref; // Default calibration
int cv_channel; // MIDI channel for MIDI
int curr_status = 0; // Current MIDI status
int curr_channel = 0; // MIDI channel number of current status
int data_count = 0; // Number of MIDI data bytes since last status change
int data1 = 0; // Value of first MIDI data byte
int sysex[8]; // System exclusive message
int sysex_ix; // Sysexm exclusive index
/* This is the starting point for DAC steps-per-volt calibration. It's experimentally-determined
* with my Nano and my meter, but I've noticed that it varies according to power supply. So
* in real life, you'll want to calibrate your interface
*/
const int DEFAULT_VOLT_REF = 876; // Calibration of DAC at 1V
void setup() {
Serial.begin(31250);
//Serial.begin(9600); // Diagnostics
// Data lines - default to input
for (int b = 0; b < 8; b++) pinMode(UPORT7 + b, INPUT);
// Control lines
pinMode(VCB1, OUTPUT); // Set LOW to acknowledge data received
pinMode(VCB2, INPUT); // Reads LOW when data received
// CV Setup
dac.begin(0x60);
int msb = EEPROM.read(0); // This calibration data on the EEPROM is big-endian
int lsb = EEPROM.read(1);
volt_ref = (msb * 256) + lsb;
if (volt_ref > 2048 || volt_ref < 256) volt_ref = DEFAULT_VOLT_REF;
cv_channel = 15; // Default CV binding to channel 16
}
void loop() {
if (!digitalRead(VCB2)) {
int out = 0;
int val = 256;
for (int i = 0; i < 8; i++)
{
val /= 2; // Power of 2, descending
int b = 7 - i; // b is bit number
int pin = i + UPORT7; // Physical pin number
out += digitalRead(pin) * val;
}
Serial.write(out);
digitalWrite(VCB1, LOW); // Acknowledge with transition on CB1
digitalWrite(VCB1, HIGH);
// Handle CV and Gate. Note On on the CV channel will set gate high, and Note Off on the
// CV channel will set gate low. Start of Pitch Bend on the CV channel starts data collection
// of the next two bytes, LSB and MSB, and turns them into a 14-bit value, which is then
// dithered down to a 12-bit value for sending to the DAC.
if (out >= 0x80) {
// This is a MIDI status byte
curr_status = out & 0xf0;
curr_channel = out & 0x0f;
data_count = 0;
if (out == 0xf0) sysex_ix = 0; // Start of exclusive
if (out == 0xf7) { // End of exclusive
// Check for system exclusive signature for this product
if (sysex[1] == 0x7d && sysex[2] == 0x62 && sysex[3] == 0x76) {
// 7d = Non-commercial manufacturer ID
// 62 = "B" Beige Maze
// 76 = "V" VIC-20 Products
if (sysex[4] == 0x6d) { // M binds MIDI channel to CV/Gate
cv_channel = sysex[5] & 0x0f;
}
if (sysex[4] == 0x63) { // C sets 1V calibration value LSB, MSB
int lsb = sysex[5]; // 7-bit values from MIDI
int msb = sysex[6];
volt_ref = (msb * 128) + lsb;
int low = volt_ref % 256; // Convert MIDI data bytes to 8-bit high and
int high = volt_ref / 256; // low bytes
EEPROM.write(0, high); // Calibration data is big-endian
EEPROM.write(1, low);
}
}
}
} else {
if (curr_channel == cv_channel) {
data_count++;
if (data_count == 1) data1 = out;
if (data_count == 2) {
// Handle Gate On (Note on)
if (curr_status == 0x90) { // Note on
digitalWrite(GATEOUT, HIGH);
if (out) { // Quantize CV if velocity > 0
// MIDI Note #36 = C 0V
// #48 = C 1V
// #60 = C 2V
// #72 = C 3V
// #84 = C 4V
if (data1 >= 36 && data1 <= 96) {
int note = data1 % 12; // Get note where C = 0
int oct = (data1 - 36) / 12; // Get octave number
int value = (oct * volt_ref) + ((volt_ref / 12) * note);
dac.setVoltage(value, false);
}
}
}
// Handle Gate off (Note off)
if (curr_status == 0x80) { // Note off
digitalWrite(GATEOUT, LOW);
}
// Handle CV output for 12-bit DAC value
if (curr_status == 0xe0) { // Pitch bend (DAC output)
int value = (out * 128) + data1;
if (value > 4095) value = 4095; // Enforce 12-bit value
dac.setVoltage(value, false);
}
// Handle quantized CV independent of Gate (using CC#3)
if (curr_status == 0xb0) { // Control change
if (data1 == 3) { // Quantize CV if CC#3
if (out >= 36 && out <= 96) {
int note = out % 12; // Get note where C = 0
int oct = (out - 36) / 12; // Get octave number
int value = (oct * volt_ref) + ((volt_ref / 12) * note);
dac.setVoltage(value, false);
}
}
}
}
}
}
// Add to system exclusive buffer if sysex status is on
if (curr_status == 0xf0) sysex[sysex_ix++] = out;
}
}
On the VIC-20 side, CV and Gate just use the MIDI KERNAL. By default, the CV and Gate are bound to MIDI channel 16, but this can be changed with a system exclusive message (see sample code below).
Here's sample code for sending Gate and CV using the MIDI KERNAL, as well as changing the MIDI channel binding.
Note: I'm going to be updating this code on this post as new features are added.
Code: Select all
; Configure for CV/Gate
CVDemo: jsr SETOUT ; Configure port for MIDI Out
lda #CV_CHANNEL ; Set to configured CV channel
jsr SETCH ; ,,
; Set 5V Gate On
GateOn: ldy #0 ; Note On with 0 velocity will set Gate On
jsr NOTEON ; In this case, X has no function
; Set 5V Gate Off
GateOff: jsr NOTEOFF ; And also for Gate Off
; Send DAC Value to CV Out
CVOut: ldx CV ; CV is a 16-bit value with a 12-bit range. It
txa ; needs to be converted to 7-bit MIDI data
asl ; Send bit 7 of low byte over to the MSB
lda CV+1 ; ,,
rol ; ,,
and #%00011111 ; Keep 5 bits (the other 7 will be in the LSB)
tay ; PITCHB expects MSB in Y
jsr PITCHB ; Send CV with MIDI LSB in X and MSB in Y
; Send Quantized Note Value to CV Out and Turn On Gate
Quantize: ldx #NOTE_NUMBER ; Send Note on with note between 36 and 96,
ldy #NONZERO ; and a nonzero velocity to quantize the CV
jsr NOTEON ; output
; Send Quantized Note Value to CV Out, Independent of Gate
QuantOnly: ldx #3 ; Send a control change message on CC#3
ldy #NOTE_NUMBER ; with note number in Y
jsr CONTROLC
; Bind MIDI Channel to CV Output
; Use this to change the binding from the default of 16
BindCVCH: ldy #5 ; Send system exclusive header for channel
-loop: lda CHSYSEX,y ; binding
jsr MIDIOUT ; ,, (Send raw data to MIDI port)
dey ; ,,
bpl loop ; ,,
lda #CV_CHANNEL ; This is the new MIDI channel to bind to CV
jsr MIDIOUT ; ,,
lda #$f7 ; End-of-Exclusive
jsr MIDIOUT ; ,,
; SysEx header in reverse. Start of SysEx ($f0), Non-Commercial ID ($7d),
; Beige Maze ($62), VIC-20 Projects ($76),
; MIDI Channel Set ($6d)
CHSYSEX: .byte $6d,$76,$62,$7d,$f0
The CVOut code isn't that tricky. We must convert a 12-bit DAC value in memory into a pair of MIDI data bytes, which are a pair of 7-bit values. So CVOut takes the lowest 7 bits for the LSB, and the highest 5 bits for the MSB. To do this, it shifts bit 7 of the LSB over to bit 0 of the MSB. You may notice that we're not stripping bit 7 out of X. That's because the MIDI KERNAL does this for us, to preserve the integrity of MIDI status messages.
If you want to send a note, rather than a raw DAC value, see the Quantize and QuantOnly examples. For Quantize, you send a note number between 36 and 96 (in X) and a nonzero velocity (in Y) to NOTEON. This also sets the Gate On. If you send CC#3 with the note number, the CV output will be quantized, but the Gate will remain in its current state.
If you use quantization, you'll want to provide a DAC calibration screen in your software. Example calibration code is a couple posts down.
I have a busy as hell weekend (a holiday in U.S.A.), but I'll post a video demo when I can.