MiniMO Compatible 4-Pot ADSR

May 21, 2020 at 12:18 pm (computers, maker, music) (, , , )

I’ve been enjoying having a play with the MiniMO synth, now I can programme it easily, but although I seriously like the idea of a piece of common hardware with software-defined synth personalities, I just found the ADSR with a single potentiometer just too limiting and not responsive enough for trying things out.

So I’ve sacrificed the common hardware and created a MiniMO compatible (i.e. based on the same circuit and code) 4-potentiometer ADSR instead.  To continue to use an ATtiny85 though requires a few compromises:

  • There is no button or LED.
  • There is no CV input to control the ADSR parameters (although this could be added).
  • The pots are always “live” – changing them will instantly change the parameter they represent.
  • And I needed to reclaim the ATtiny85 RST pin (pin 1) as an I/O pin.

If you are familiar with the ATtiny85 you’ll know that there is a fuse setting that will disable its response to the RST signal on pin 1 allowing you to use it as an additional I/O pin.

But you will also therefore probably know that this means you can no longer program it using the normal methods!  Once that fuse is set the only way to get back into your device is using a High Voltage Serial Programmer (HVSP).  There are a number of designs around the Internet for these and I’ve build some of them myself so can confirm they work.  The one I tend to use is a variation on this one with both 8-pin and 14-pin sockets, driven by an ATtiny2313 and some tweaked code to act as a “fuse resitter”.  I’ll perhaps post about that at some point.

You can’t set the RSTDISBL fuse from within the Arduino environment, so the general procedure is as follows:

  • Set your parameters as required for your device.
  • In “preferences” enable verbose messages on upload.
  • “Burn bootloader” to set the existing fuses – this will give you the command used to set the fuses.
  • In “preferences” disable verbose messages on upload again (if you want).
  • Upload your sketch to the ATtiny85.


  • Copy the command to set the fuses and update the hfuse value to set the RSTDISBL flag (i.e. put it to zero) and run it in a command window.

Running through the above, my “burn bootloader” command looks like this:

[PATH TO AVRDUDE]/avrdude -C[PATH TO AVRDUDE CONF]/avrdude.conf -v      -pattiny85 -cusbtiny -e -Uefuse:w:0xFF:m -Uhfuse:w:0b11010111:m         -Ulfuse:w:0xE2:m -Uflash:w:[PATH TO BOOTLOADER]empty_all.hex:i

I am using a USBtiny programmer. We only want the hfuse “write” command (and really don’t want the -e option which erases the sketch in memory). You can use a ATtiny85 fuse calculator to see what the value should be – for example, see this one.

First I read back the just set hfuse value.

[PATH TO AVRDUDE]/avrdude -C[PATH TO AVRDUDE CONF]/avrdude.conf -v      -pattiny85 -cusbtiny -Uhfuse:r:out.txt

Then write the new value as calculated by the fuse calculator – RSTDISBL is the top bit of the hfuse, so for me, it read back 0xD7, so I write out 0x57 – running this command WILL “BRICK” YOUR DEVICE if you don’t have a HVSP.

[PATH TO AVRDUDE]/avrdude -C[PATH TO AVRDUDE CONF]/avrdude.conf -v      -pattiny85 -cusbtiny -Uhfuse:w:0x57:m

So with all that out of the way, this is the circuit and code I used to get a MiniMO compatible 4-potentiometer ADSR.MinoMo-ADSR2-2_schem

As I said, I’ve sacrificed some of the hardware, but the pot input stage is the same (just repeated four times), and the output and gate stages are the same as the original MinoMO design, but the output is on a different pin to free up all four ADCs. It has moved from using OC1B as the PWM output to using OC1A instead.

The code is a lot simpler as it isn’t having to handle the button and shared parameter setting via the single pot.  Instead the potentiometer values are read “live” in my version.

//*      miniMO ADSR     *
//*   2016 by enveloop   *
CC BY 4.0 Licensed under a Creative Commons Attribution 4.0 International license: // --- Kevin --- Updated to build on recent Arduino IDE with ATTiny Core from: ADSR Re-written to use four individual potentiometers and reduced inputs. Note: this sacrifices the LED output, button for input, control inputs on the pots and relies on repurposing RST as an I/O pin (see below). It also switches the output from digital I/O 4 (pin 3) to digital I/O 1 (pin 6) so that ADC4 can be used. This means switching the PWM output from OC1B on compare to OC1A. I/O 1 and 2: Outputs - control voltage (usually for amplitude) 3: Not connected - but could optionally double with the potentiometers as per the original design 4: Input - gate (note ON/OFF) Mapped to ATtiny85 I/O: --------- ADC0 - 5/A0 - PB5 - | 1 8 | - VCC ADC3 - 3/A3 - PB3 - | 2 7 | - PB2 - 2/A1 - ADC1 ADC2 - 4/A2 - PB4 - | 3 6 | - PB1 - 1 - OC1A - PWM (Output) GND - | 4 5 | - PB0 - 0 - dig I/P (Gate) --------- Map potentiometers to ADSR: ADC0 (5) - (A)ttack ADC1 (2) - (D)ecay ADC3 (3) - (S)ustain ADC2 (4) - (R)elease NOTE: To use ADC0 need to programme the fuses to repurpose RST as an I/O pin which means it is no longer possible to programme the ATtiny85 except with a high voltage serial programmer (HVSP). --- Kevin --- */ #include <avr/io.h> #include <util/delay.h> // Define Arduino pin numbers - NB: Digital and Analogue have different numbers! #define ADSR_A 5 // PB5 = ADC0 - digital I/O 5 (pin 1) is the repurposed RST pin #define ADSR_ALG_A A0 #define ADSR_D 2 // PB2 = ADC1 #define ADSR_ALG_D A1 #define ADSR_S 3 // PB3 = ADC3 #define ADSR_ALG_S A3 #define ADSR_R 4 // PB4 = ADC2 #define ADSR_ALG_R A2 #define ADSR_G 0 // Gate input #define ADSR_O 1 // OC1A output int ADSR_adc[] = { ADSR_ALG_A, ADSR_ALG_D, ADSR_ALG_S, ADSR_ALG_R }; volatile unsigned int globalTicks; //output int envelopeValue; //envelope stage control; bool readyToAttack = true; bool readyToRelease = false; const int attackLevel = 255; int currentStep = 0; int ADSR[] = { 0, //attackLength 1, //decayLength 255, //sustainLevel 0 //releaseLength }; void setup() { //disable USI to save power as we are not using it PRR = 1<<PRUSI; ADMUX = 0; //reset multiplexer settings pinMode(ADSR_G, INPUT); //digital input (gate) pinMode(ADSR_O, OUTPUT); //output (PWM on OC1A) pinMode(ADSR_A, INPUT); //analog- ADC0 - (A)ttack pinMode(ADSR_D, INPUT); //analog- ADC1 - (D)ecay pinMode(ADSR_S, INPUT); //analog- ADC2 - (S)ustain pinMode(ADSR_R, INPUT); //analog- ADC3 - (R)elease //set clock source for PWM -datasheet p94 PLLCSR |= (1 << PLLE); // Enable PLL (64 MHz) while (!(PLLCSR & (1 << PLOCK))); // Ensure PLL lock PLLCSR |= (1 << PCKE); // Enable PLL as clock source for timer 1 cli(); // Interrupts OFF (disable interrupts globally) //PWM Generation -timer 1 TCCR1 = (1 << PWM1A) | // PWM, output on PB1, compare with OCR1A (see interrupt below), reset on match with OCR1C (1 << COM1A1) | (1 << CS10); // no prescale OCR1C = 0xff; // 255 //Timer Interrupt Generation -timer 0 TCCR0A = (1 << WGM01); // Clear Timer on Compare (CTC) with OCR0A TCCR0B = (1 << CS01); // prescaled by 8 OCR0A = 0x64; // 0x64 = 100 //10000hz - 10000 ticks per second TIMSK = (1 << OCIE0A); // Enable Interrupt on compare with OCR0A sei(); // Interrupts ON (enable interrupts globally) } //Timer0 interrupt ISR(TIMER0_COMPA_vect) { //10000 ticks per second globalTicks++; } void loop() { setADSR(); triggerADSR(); } void setADSR(){ // Read one each scan if (currentStep == 2) setLevel(currentStep); // S is a level not a length else setLength (currentStep); // ADR are all lengths currentStep++; currentStep &= 0x03; } void setLength(int step) { int lengthRead = analogRead(ADSR_adc[step]) >> 6 ; //values between 0-15 ADSR[step] = lengthRead; } void setLevel(int step) { int levelRead = analogRead(ADSR_adc[step]) >> 2 ; //values between 0-255 ADSR[step] = levelRead; } int checkGATE() { return (PINB & (1<<ADSR_G)); } void triggerADSR() { // Check the Gate input if (checkGATE()) { if (readyToAttack){ readyToAttack = false; readyToRelease = true; readADS(); } } else{ if (readyToRelease){ readyToAttack = true; readyToRelease = false; readR(); } } } void readADS() { int attackLength = ADSR[0]; int decayLength = ADSR[1]; int sustainLevel = ADSR[2]; //ATTACK if (attackLength == 0) OCR1A = attackLevel; else { globalTicks = 0; for (envelopeValue = 0; envelopeValue <= 255; ){ OCR1A = envelopeValue; if (globalTicks >= attackLength) { envelopeValue++; globalTicks = 0; } } } //DECAY globalTicks = 0; if ((decayLength == 0) || (sustainLevel == attackLevel)) OCR1A = sustainLevel; else{ if (sustainLevel < attackLevel){ for (envelopeValue = attackLevel; envelopeValue >= sustainLevel; ){ OCR1A = envelopeValue; if (globalTicks >= decayLength) { envelopeValue--; globalTicks = 0; } } } } //SUSTAIN --nothing to do, we keep the last value until the "note off" trigger } void readR() { int sustainLevel = ADSR[2]; int releaseLength = ADSR[3]; //RELEASE if (releaseLength == 0) OCR1A = 0; else { OCR1A = sustainLevel; globalTicks = 0; for (envelopeValue = sustainLevel; envelopeValue >= 0;){ if (checkGATE()){ //if during R stage there's a trigger, silence and exit OCR1A = 0; return; } OCR1A = envelopeValue; if (globalTicks >= releaseLength) { envelopeValue--; globalTicks = 0; } } } }

I made one up and am pretty pleased with the results!

2020-05-21 13.10.52



Leave a Reply

Please log in using one of these methods to post your comment: Logo

You are commenting using your account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: