Raspberry Pi Pico DCO


I got the Raspberry Pi Pico DCO working! It uses PIO to generate accurate frequencies on GPIO pins and is controlled by MIDI USB. The “analog” oscillator part is based on the Juno 106 and produces a 10Vpp ramp and square signal.

Here are some examples on how it sounds:

It is currently a single voice. I am going to add DIN MIDI input and polyphony later on. The Pi Pico should be able to control up to 8 voices.

Source code and schematics can be found here: GitHub - polykit/pico-dco

13 Likes

Hell yes dude! This is a gorgeous build, really looking forward to a stripboard version of this!

Nice one! How is the RESET0 used / what is it supposed to achieve while shorting the 1nF capacitor via the transistor? I did not find it being used in the code.

The reset pin/GPIO2 is passed to the assembler routine which is turning it high/low and shorting the capacitor via the transistor. This is controlling the frequency of the ramp signal.
Reset pin is defined here: pico-dco/pico-dco.c at 4e8106dd417928f03c9ebdd10268a4b6094ccb00 · polykit/pico-dco · GitHub
Assembler routine which is controlling the PIO state machine is here: pico-dco/pico-dco.pio at master · polykit/pico-dco · GitHub
Frequency change of the reset pin is set from outside here: pico-dco/pico-dco.c at 4e8106dd417928f03c9ebdd10268a4b6094ccb00 · polykit/pico-dco · GitHub

1 Like

I now have a working PCB with 6 voices driven by the Pi Pico. I also added serial DIN MIDI input which is more reliable than USB MIDI. Code now supports all six voices. This is how it sounds:

Currently it is not really polyphonic, I don’t have 6 VCAs/ADSRs. I need to work on that.

7 Likes

Wow, that’s really impressive! Most DCO projects seem to go quiet after one voice is complete.

Does it have gate out per voice?

Curious why midi usb is not reliable? Do you mean the connector? Or is there some comms issue?

1 Like

Yes, the upper 16 pin IDC connector on the picture is for gate outputs. It only has 3.3V but seems okay at least for my ADSRs. The other 16 pin connectors are for ramp and pulse signal.

The Pi Picos SDK is using an outdated and forked version of TinyUSB. This seems to be the cause for loosing notes especially when they are played fast or at the same time. I hope this will be fixed in a future release. There is an open issue regarding this on GitHub: bump TinyUSB to latest upstream release · Issue #161 · raspberrypi/pico-sdk · GitHub

That’s pretty damned slick!

Thanks for the heads up on the tinyUSB thing, I was likely heading straight towards that trainwreck!

1 Like

Most likely this is only a problem for USB Midi input to the Pico Pi and not output.

Yoooo I love this.

I’m new to the pico and I’m having a bit of an issue. I’m trying to modify the code a little to turn it into mono but with 2 detunable oscillators. I figured out the mono portion pretty easy lmao but I’m having a hard time making it output on 2 different pins at once.

Think you could tell me which part I should be looking at?

2 Likes

Take a look at the note_on function: pico-dco/pico-dco.c at a13c620fb4ff3d5d7cfbcce0f9e300e5d5b28979 · polykit/pico-dco · GitHub

A loop around this or repeating it should be all that is needed. You need to add some parameter for detuning the voices individually but maybe you can also reuse pitch_bend_task function for this.

Another option would be to mess with the assembler code directly but I think this is much more complicated.

I also wanted to add some functionality for stacking multiple voices. Maybe this gives me some motivation doing so.

Btw. nice to hear that someone is actually using the project!

1 Like

Awesome! thanks Jan! I’m going to try something out and let you know how it goes! I am also going to add the “metallic sync” they had on some Rolands where one oscillator was fed in the integrator’s reset. I’ll let you know how that goes!

2 Likes

Hey Jan, so I managed make it output on 2 different pins and I am not struggling to make output at different frequencies. Here’s my code:

#include <stdio.h>
#include <math.h>
#include "pico/stdlib.h"
#include "hardware/gpio.h"
#include "hardware/pio.h"
#include "hardware/clocks.h"
#include "pico-dco.pio.h"
#include "hardware/pwm.h"
#include "bsp/board.h"
#include "tusb.h"
#include "hardware/uart.h"

#define NUM_VOICES 2
#define MIDI_CHANNEL 1

const float BASE_NOTE = 440.0f;
const uint8_t RESET_PINS[NUM_VOICES] = {13, 8, 12, 9, 11, 10};
const uint8_t RANGE_PINS[NUM_VOICES] = {16, 19, 15, 18, 14, 17};
const uint8_t GATE_PINS[NUM_VOICES] = {2, 3, 4, 5, 6, 7};
const uint8_t VOICE_TO_PIO[NUM_VOICES] = {0, 0, 0, 0, 1, 1};
const uint8_t VOICE_TO_SM[NUM_VOICES] = {0, 1, 2, 3, 0, 1};
const uint16_t DIV_COUNTER = 1250;
uint8_t RANGE_PWM_SLICES[NUM_VOICES];
uint8_t NOTES[128];
uint32_t VOICES[NUM_VOICES];
uint8_t VOICE_NOTES[NUM_VOICES];
uint8_t NEXT_VOICE = 0;
uint32_t LED_BLINK_START = 0;
PIO pio[2] = {pio0, pio1};
uint8_t midi_serial_status = 0;
uint16_t midi_pitch_bend = 0x2000, last_midi_pitch_bend = 0x2000;

void init_sm(PIO pio, uint sm, uint offset, uint pin);
void set_frequency(PIO pio, uint sm, float freq);
float get_freq_from_midi_note(uint8_t note);
void led_blinking_task();
uint8_t get_free_voice();
void usb_midi_task();
void serial_midi_task();
void note_on_osc1(uint8_t note, uint8_t velocity);
void note_on_osc2(uint8_t note, uint8_t velocity);
void note_off(uint8_t note);
void pitch_bend_task();

int main() {
    board_init();
    tusb_init();

    // use more accurate PWM mode for buck-boost converter
    gpio_init(23);
    gpio_set_dir(23, GPIO_OUT);
    gpio_put(23, 1);

    // init serial midi
    uart_init(uart0, 31250);
    uart_set_fifo_enabled(uart0, true);
    gpio_set_function(0, GPIO_FUNC_UART);
    gpio_set_function(1, GPIO_FUNC_UART);

    // pwm init
    for (int i=0; i<NUM_VOICES; i++) {
        gpio_set_function(RANGE_PINS[i], GPIO_FUNC_PWM);
        RANGE_PWM_SLICES[i] = pwm_gpio_to_slice_num(RANGE_PINS[i]);
        pwm_set_wrap(RANGE_PWM_SLICES[i], DIV_COUNTER);
        pwm_set_enabled(RANGE_PWM_SLICES[i], true);
    }

    // pio init
    uint offset[2];
    offset[0] = pio_add_program(pio[0], &frequency_program);
    offset[1] = pio_add_program(pio[1], &frequency_program);
    for (int i=0; i<NUM_VOICES; i++) {
        init_sm(pio[VOICE_TO_PIO[i]], VOICE_TO_SM[i], offset[VOICE_TO_PIO[i]], RESET_PINS[i]);
    }

    // gate gpio init
    for (int i=0; i<NUM_VOICES; i++) {
        gpio_init(GATE_PINS[i]);
        gpio_set_dir(GATE_PINS[i], GPIO_OUT);
    }

    // init voices
    for (int i=0; i<NUM_VOICES; i++) {
        VOICES[i] = 0;
    }

    while (1) {
        tud_task();
        usb_midi_task();
        serial_midi_task();
        pitch_bend_task();
        led_blinking_task();
    }
}

void init_sm(PIO pio, uint sm, uint offset, uint pin) {
    init_sm_pin(pio, sm, offset, pin);
    pio_sm_set_enabled(pio, sm, true);
}

void set_frequency(PIO pio, uint sm, float freq) {
    uint32_t clk_div = clock_get_hz(clk_sys) / 2 / freq;
    if (freq == 0) clk_div = 0;
    pio_sm_put(pio, sm, clk_div);
    pio_sm_exec(pio, sm, pio_encode_pull(false, false));
    pio_sm_exec(pio, sm, pio_encode_out(pio_y, 32));
}

float get_freq_from_midi_note(uint8_t note) {
    return pow(2, (note-69)/12.0f) * BASE_NOTE;
}

void usb_midi_task() {
    if (tud_midi_available() < 4) return;

    uint8_t buff[4];

    LED_BLINK_START = board_millis();
    board_led_write(true);

    if (tud_midi_packet_read(buff)) {
        if (buff[1] == (0x90 | (MIDI_CHANNEL-1))) {
            if (buff[3] > 0) {
                note_on_osc1(buff[2], buff[3]);
                note_on_osc2(buff[2], buff[3]);
            } else {
                note_off(buff[2]);
            }
        }

        if (buff[1] == (0x80 | (MIDI_CHANNEL-1))) {
            note_off(buff[2]);
        }

        if (buff[1] == (0xE0 | (MIDI_CHANNEL-1))) {
            midi_pitch_bend = buff[2] | (buff[3]<<7);
        }
    }
}

void serial_midi_task() {
    if (!uart_is_readable(uart0)) return;

    uint8_t lsb = 0, msb = 0;
    uint8_t data = uart_getc(uart0);

    LED_BLINK_START = board_millis();
    board_led_write(true);

    // cc status
    if (data >= 0xF0 && data <= 0xF7) {
        midi_serial_status = 0;
        return;
    }

    // realtime message
    if (data >= 0xF8 && data <= 0xFF) {
        return;
    }

    if (data >= 0x80 && data <= 0xEF) {
        midi_serial_status = data;
    }

    if (midi_serial_status >= 0x80 && midi_serial_status <= 0x90 ||
        midi_serial_status >= 0xE0 && midi_serial_status <= 0xEF) {
        lsb = uart_getc(uart0);
        msb = uart_getc(uart0);
    }

    if (midi_serial_status == (0x90 | (MIDI_CHANNEL-1))) {
        if (msb > 0) {
            note_on_osc1(lsb, msb);
            note_on_osc1(lsb, msb);
        } else {
            note_off(lsb);
        }
    }

    if (midi_serial_status == (0x80 | (MIDI_CHANNEL-1))) {
        note_off(lsb);
    }

    if (midi_serial_status == (0xE0 | (MIDI_CHANNEL-1))) {
        midi_pitch_bend = lsb | (msb<<7);
    }
}

void note_on_osc1(uint8_t note, uint8_t velocity) {
    if (NOTES[note] > 0) return; // note already playing
    uint8_t voice_num = 0;
    NOTES[note] = voice_num;
    VOICES[voice_num] = board_millis();
    VOICE_NOTES[voice_num] = note;
    float freq = get_freq_from_midi_note(note);
    set_frequency(pio[VOICE_TO_PIO[0]], VOICE_TO_SM[0], freq);
    // amplitude adjustment
    pwm_set_chan_level(RANGE_PWM_SLICES[0], pwm_gpio_to_channel(RANGE_PINS[0]), (int)(DIV_COUNTER*(freq*0.00025f-1/(100*freq))));
    // gate on
    gpio_put(GATE_PINS[0], 1);
    last_midi_pitch_bend = 0;
}

void note_on_osc2(uint8_t note, uint8_t velocity) {
    if (NOTES[note] > 0) return; // note already playing
    uint8_t voice_num = 1;
    NOTES[note] = voice_num;
    VOICES[voice_num] = board_millis();
    VOICE_NOTES[voice_num] = note;
    float freq = get_freq_from_midi_note(note);
    set_frequency(pio[VOICE_TO_PIO[1]], VOICE_TO_SM[1], freq);
    // amplitude adjustment
    pwm_set_chan_level(RANGE_PWM_SLICES[0], pwm_gpio_to_channel(RANGE_PINS[1]), (int)(DIV_COUNTER*(freq*0.00025f-1/(100*freq))));
    // gate on
    gpio_put(GATE_PINS[0], 1);
    last_midi_pitch_bend = 0;
}

void note_off(uint8_t note) {
    // gate off
    gpio_put(GATE_PINS[NOTES[note]], 0);
    VOICE_NOTES[NOTES[note]] = 0;
    VOICES[NOTES[note]] = 0;
    NOTES[note] = 0;
}

uint8_t get_free_voice() {
    uint32_t oldest_time = board_millis();
    uint8_t oldest_voice = 0;

    for (int i=0; i<NUM_VOICES; i++) {
        uint8_t n = (NEXT_VOICE+i)%NUM_VOICES;

        if (VOICES[n] == 0) {
            NEXT_VOICE = (n+1)%NUM_VOICES;
            return n;
        }

        if (VOICES[i]<oldest_time) {
            oldest_time = VOICES[i];
            oldest_voice = i;
        }
    }

    return oldest_voice;
}

void pitch_bend_task() {
    if (midi_pitch_bend != last_midi_pitch_bend) {
        last_midi_pitch_bend = midi_pitch_bend;
        for (int i=0; i<NUM_VOICES; i++) {
            if (VOICE_NOTES[i] > 0) {
                float freq = get_freq_from_midi_note(VOICE_NOTES[i]);

                if (midi_pitch_bend != 0x2000) {
                    freq = freq-(freq*((0x2000-midi_pitch_bend)/67000.0f));
                }

                set_frequency(pio[VOICE_TO_PIO[i]], VOICE_TO_SM[i], freq);
            }
        }
    }
}

void led_blinking_task() {
    if (board_millis() - LED_BLINK_START < 50) return;
    board_led_write(false);
}

For example: I was trying to double the frequency of one oscillator to make it play an octave higher like so:

void note_on_osc2(uint8_t note, uint8_t velocity) {
    if (NOTES[note] > 0) return; // note already playing
    uint8_t voice_num = 1;
    NOTES[note] = voice_num;
    VOICES[voice_num] = board_millis();
    VOICE_NOTES[voice_num] = note;
    float freq = get_freq_from_midi_note(note);
    set_frequency(pio[VOICE_TO_PIO[1]], VOICE_TO_SM[1], freq * 2);
    // amplitude adjustment
    pwm_set_chan_level(RANGE_PWM_SLICES[0], pwm_gpio_to_channel(RANGE_PINS[1]), (int)(DIV_COUNTER*(freq*0.00025f-1/(100*freq))));
    // gate on
    gpio_put(GATE_PINS[0], 1);
    last_midi_pitch_bend = 0;
}

But it had no effect. By the look of things the parameter is not coming from a Globalor static variable so why am I not able to change it?

Édit:

I have found how to make this happen with a bit of tinkering! I created a new function that calls set_frequency() inside of main() in the while loop! I am going to set-up 2x 3 way switches and will use the pins state to choose the frequency multiplier (0.5, 1, 2) to switch the octave in software. Then a good old tuning pots for each oscillators and we’ll have a mono dual DCO :smile:. I’ll fork the code when I am done and post it on GitHub this way people can get either the mono or poly version!

4 Likes

@humphrey_m808 fyi I added optional ADC input for stacking and detuning voices. By uncommenting these two lines pico-dco/pico-dco.c at c7a35ddd0ec99a7517f3a91fd87460ccf858af9b · polykit/pico-dco · GitHub and adding potentiometers to adc1 (gpio 27) and adc2 (gpio28) you can change both parameters.

4 Likes

You are the real MVP

Just wondering, why are you using the PIO to generate the frequency? Is there some advantage over setting a hardware PWM pin to the same frequency? Did you just want to muck about with it and see if it could be done?

1 Like

Haha, yes, for sure I wanted to try it out. PIO is super accurate in timing, I never tried PWM though. I also tried an Arduino Nano and had a hard time to get the cycles correct.

2 Likes