AVR-based Octaver with modulation and envelope follower

Hi!

I’m working on an AVR-based octaver project. It’s not a standard octaver that can do fuzz octave up and down (I would just have gone for classic CMOS circuits). This one has modulation (using an internally generated LFO), and also follows the level of the input signal. It’s similar to the “Into the Unknown” pedal by Parasit Studio, but using a practical, cheap, small-footprint, all-in-one MCU, instead of a combination of CD4046 + CD4015 + LFO and VCA circuitry.

I’m using the ATtiny84 as it has an Input Capture Pin. The idea is to count clock cycles between input captures, calculate the frequency of the input signal, and then use that calculated frequency value to generate an octave up/down signal, which can be modulated and have its amplitude adjusted. Another option would be to count the number of rising edges for a given interval, but for low frequencies that would be too inaccurate I believe.

I have got the whole thing to work, but when I start to get into the high frequency range (above 1kHz), the signal becomes very noisy. I have tried out a few things, but I haven’t been able to find out the reason for that noise. I'm not sure if calculating the frequency is the best approach, but I can't think of another suitable one. For my particular purpose, the input and output of the octaver will always be square waves, so sampling the input and using DSP seems a bit overkill. Do you have any idea what might be the reason for that noise in the high-pitched tones? And do you know of any other projects out there similar to what I'm trying to achieve? It seems like all octaver/pitch-shifter/harmonizer projects are either CMOS-based or involve complicated signal processing…

In case it is useful, I briefly describe the timers and interrupts setup:

Timer0 runs at 20/8=2.5Mhz (using a 20Mhz crystal) and has a 44kh OVF interrupt to output the values of the audio output signal, as well as the values of the LFO (the latter are actually calculated at 44/4=11kHz, to save CPU time).

Timer1 runs at 20Mhz and has a 78kHz OVF interrupt for keeping track of overflows in between each input capture. The capture interrupt basically reads the counter and calculates the frequency. Timer1 is also running the PWM for outputting the samples at 78kHz.

At the moment, I do the octave up / down selection directly in the code and the LFO only outputs a square wave, but these are features that are very easy to implement and I want to get the sound right before moving on. BTW, this is my second ATtiny84 project, I hope that the code is not too terrible...

/* 

ATtiny84 pin layout:

                              ___ ___
                        VCC -|   U   |- GND
                XTAL1 (PB0) -|       |- (PA0) ADC0 <- Rate LFO
                XTAL2 (PB1) -|       |- (PA1) ADC1 <- Depth LFO
                RESET (PB3) -|       |- (PA2) 
                      (PB2) -|       |- (PA3)
 Signal input ->  ICP (PA7) -|       |- (PA4) ADC4 <- Enveloppe
       Output <- OC1A (PA6) -|_______|- (PA5) 
                   
*/

#include <avr/io.h> // for port commands.

//-------------------------------- I/O PORT PINS & ADC INPUTS ----------------------------------//

// PWM output:
#define OUTPUT_PIN PA6            // OC1A PWM output - Pin 7 of the IC.

// Analog input pins: 
#define INPUTCAPT_PIN PA7         // ICP - Pin 6 of the IC.
#define ENVELOPE_PIN PA4          // ADC4 - Pin 8 of the IC.
#define rateLFO_PIN PA0           // ADCO - Pin 13 of the IC.
#define depthLFO_PIN PA1          // ADC1 - Pin 12 of the IC.

// Analog inputs:
const uint8_t ENVELOPE_READ = 4;   // ADC4 - Pin 8 of the IC.
const uint8_t rateLFO_READ = 0;    // ADCO - Pin 13 of the IC.
const uint8_t depthLFO_READ = 1;   // ADC1 - Pin 12 of the IC.


//----------------------- VARIABLE DEFINITION AND INITIALISATION -------------------------------//

volatile uint16_t ovfCount0, ovfCount1; // Input signals no lower than 20Hz. For 20Hz Timer/Counter1 overflows (20Mhz/256)/20=3906.25 times.
volatile uint16_t ovfCount; // ovfCount is the difference between ovfCount1 and ovfCount0.

volatile uint8_t captCount0, captCount1; //  Limited by Timer/Counter1 resolution, which is 8bit (OCR1A = 255).
volatile int16_t captCount; // captCount = captCount1 - captCount0, can be negative and reach +/-255.

volatile uint32_t count;     // For 20Hz, count = 1,000,000.
volatile uint32_t freq;      // Highest value, for 20kHz, is 29,000.

volatile uint8_t amplitude;  // Only read 8-bit.
volatile uint8_t rateLFO;    // Only read 8-bit.
volatile uint8_t depthLFO;   // Only read 8-bit.
volatile uint8_t outputLFO;  // 8-bit PWM.


//------------------ I/O CONFIGURATION, TIMER/COUNTERS & INTERRUPTS SETUP ---------------------//

void setup() {
  
  // I/O configuration on the Data Direction Register (DDRB):
 
  DDRA = 1<<OUTPUT_PIN;         // Configure PWM output pin as output.
  DDRA &= ~(1<<ENVELOPE_PIN);   // Configure analog and digital input pins as inputs.
  DDRA &= ~(1<<INPUTCAPT_PIN);
  DDRA &= ~(1<<rateLFO_PIN);
  DDRA &= ~(1<<depthLFO_PIN);

  // Set up Timer/Counter0 & interrupts:        
  
  // COM0A1:0 = 0 and COM0B1:0 = 0 for normal port operation, both OC0A and OC0B disconnected.
  // WGM02:0 = 2 for Clear Timer on Compare (CTC) match mode.
  // CS02:0 = 2 for prescaler of 8 (timer running at 2.5MHz).
  TCCR0A = 2<<WGM00;                                       
  TCCR0B = 2<<CS00;                          
  
  // Timer/Counter0 interrupts:
  // OCIE0A = 1 to enable Output Compare Match A interrupts.
  // OCR0A = 55 to generate interrupts at ~44kHz (2.5MHz/55+1 = 44.6kHz).
  TIMSK0 = 1<<OCIE0A;
  OCR0A = 55;
 
  // Set up Timer/Counter1 & interrupts:  
  
  // COM1A1:0 = 2 to clear OC1A on Compare Match (non-inverting mode).
  // COM1B1:0 = 0 for normal port operation, OC1B disconnected.
  // WGM13:10 = 5 for 8-bit Fast PWM.
  // CS12:0 = 1 for no prescaling, 20Mhz clock.
  TCCR1A = 2<<COM1A0; 
  TCCR1A |= 1<<WGM10;
  TCCR1B = 1<<WGM12;   
  TCCR1B |= 1<<CS10;

  // Input capture set-up:
  // ICNC1 = 1 to activate noise cancelling.
  // ICES1 = 1 to trigger capture on raising edge.
  TCCR1B |= 1<<ICNC1;
  TCCR1B |= 1<<ICES1;                         

  // Timer/Counter1 interrupts:
  // ICIE1 = 1 to enable Input Capture Interrupts.
  // TOIE1 = to enable Overflow Interrupts.
  TIMSK1 = 1<<ICIE1;
  TIMSK1 |= 1<<TOIE1; 
  

  // Set up ADC:

  ADCSRA = 1<<ADEN | 7<<ADPS0;  // enable ADC, 156.25kHz ADC clock (128 prescaler).
  ADCSRB = 1<<ADLAR; // Right adjusted (8-bit readings).
  DIDR0 = 1<<ADC0D | 1<<ADC1D | 1<<ADC4D; // Disable digital input buffer to save power.
}


//--------------------------------- INTERRUPT SERVICE ROUTINES ---------------------------------//


// Timer/Counter0 Compare Match Interrupt - runs at (20Mhz/8)/(OCR0A+1) = 44.6kHz

ISR(TIM0_COMPA_vect) {

  // ### LFO ###
  
  // We only want to compute LFO samples at a rate of ~10kHz (which is more than enough)
  static uint8_t timerLFO;
  static uint16_t accLFO;            
  uint8_t indexLFO = (accLFO >> 8);
  static uint8_t outputLFO;
  
  if ((timerLFO & 3) == 3) {
        
    // Square wave:
    if (indexLFO < 128) {
        outputLFO = ((uint16_t) (256) * depthLFO) >> 8;
     } else {
        outputLFO = 0;
     }

    accLFO += rateLFO;
  }
  timerLFO += 1;

  // ### OUTPUT  ###

  static uint16_t acc;            
  uint8_t index = (acc >> 8);
  uint8_t output;
  uint32_t temp;

  if (count == 0) {
    output = 0;  
  } else {
    // Square wave:
    if (index < 128) {
        output = ((uint16_t) (256) * amplitude) >> 8;
     } else {
        output = 0;
     }
  }
  OCR1A = output; 

  //Increment the accumulator:
  if (depthLFO < 10) { 
    // If LFO depth is near 0, just add the frequency.
    acc += freq;  
  } else {
    // If LFO != 0, add the LFO output 
    acc += (freq + (((uint32_t) (outputLFO) * freq) >> 8));
  }
}


// Timer/Counter1 Overflow Interrupt - runs at 20Mhz/256 = 78kHz.

ISR(TIM1_OVF_vect) {
// Update ovfCount1:
  if (amplitude > 50) {
    ovfCount1++; 
  } else { 
    // If the level of the input signal is not high enough, basically restart all variables.
    ovfCount0 = ovfCount1 = ovfCount = 0; 
    captCount0 = captCount1 = captCount = 0;
    count = 0;
  }
}

// Timer/Counter1 Input Capture Interrupt:

ISR(TIM1_CAPT_vect) {

  uint8_t temp = ICR1; //  clock is cleared when it gets to 255;
  
  // We first compute the difference between input captures:
  captCount1 = temp;
  captCount = captCount1 - captCount0;
  captCount0 = captCount1;

  // We compute the difference between the number of overflows:
  ovfCount = ovfCount1 - ovfCount0 + 65535 * (ovfCount1 < ovfCount0);
  ovfCount0 = ovfCount1;

  // The total clock cycles that went through a whole period of the input signal is 
  // the difference between input captures (either positive or negative) and the number of 
  // OVF interrupts multiplied by the number of clock cycles between OVF interrupts (256)
  count = captCount + (uint32_t (ovfCount) * 256); 

  // Compute the frequency:
  freq =  (uint32_t) 29360128 / count; // number calculated so that freq added to acc in Timer0 gives the input frequency.
  freq = freq * 2; // Octave up.
  //freq = freq / 2; // Octave down.
  freq &= 0xFFFFFFFC; // remove last bits to have more stable output.
}


//-------------------------------------- SAMPLING INPUTS --------------------------------------//

void loop() {
  GetEnvelope(ENVELOPE_READ);
  GetRateLFO (rateLFO_READ);
  GetDepthLFO (depthLFO_READ);
}

//---------------------- WAVEFORM, FREQUENCY AND AMPLITUDE READ FUNCTIONS ----------------------//

void GetEnvelope(uint8_t pin) {
  amplitude = ReadAnalog(pin);
}
void GetRateLFO(uint8_t pin) {
  rateLFO = ReadAnalog(pin);
}
void GetDepthLFO(uint8_t pin) {
  depthLFO = ReadAnalog(pin);
}

// 8-bit ADC analog read
uint8_t ReadAnalog(uint8_t ADC_VALUE) {
  ADMUX = ADC_VALUE; // Select ADC, Vcc as voltage reference
  ADCSRA |= 1<<ADSC; // Start conversion
  while(ADCSRA & 1<<ADSC);  // Wait until complete
  return ADCH;
}

One thing that jumps out at me is the large amount of very slow math (e.g. 32 bit integer division and multiplication) being performed in the Timer1 input capture ISR. No other CPU activity or interrupts can be processed while that is running.

There are several references to the timing of these remarkably slow operations, this is one example: Speed of math operations (particularly division) on Arduino

I imagined that division would be slower than multiplication, but I didn't expect it to be that slow! I'm definitely going to look into methods to optimize the division.

Another option would be to use the period of the input signal (instead of the frequency) to adjust the interrupt rate of Timer0. However, Timer0 counter is only 8 bits, so that would not give me enough resolution... Maybe I should look for another chip?

Timer1 in input capture mode can measure very long periods if you count overflows in a 16 bit variable. Clocked at 20 MHz, the maximum period is over 200 seconds. Code for that can be found here: Gammon Forum : Electronics : Microprocessors : Timers and counters

I have used the technique to very accurately calibrate the CPU clock, by counting CPU cycles between 1 PPS pulses output by a GPS module.

Sorry, I'm not sure I'm getting this right, but I think this is actually what I'm doing. Timer1 is 8-bit, which means the ICP reads values up to 255 (not enough for low frequencies) and stores them in an 8-bit variable (captCount), but then overflows are counted in a 16-bit variable (ovfCount). Since I am only interested in the audio range, the maximum period I will have to measure is 50ms (20Hz), but even for that period ovfCount needs to be 16-bit ((20Mhz/256)/20Hz = 3906 > 8-bit). Again, sorry if I misunderstood your comment.

We don't seem to be communicating well. Timer1 on the ATtiny84 is a 16 bit timer/counter.

I was responding to this comment, and suggested to use Timer1 to measure the period. I did not look very closely at the code details, so if you are measuring the period, ignore my suggestion.

to use the period of the input signal (instead of the frequency)

Oh, okay, I get it now. Sorry for the misunderstanding.

When I said Timer1 was 8-bit I was referring to the fact that, in my code, Timer1 is in 8-bit Fast PWM mode, so in my case Timer1 is effectively 8-bit, if I'm not mistaken.

As for measuring the period, I am indeed measuring the clock cycles between input captures. I then perform a division to get the frequency (very resource consuming, as you pointed out), which in turn is used to generate the output in Timer0. By "using the period of the input signal (instead of the frequency)" I meant directly using the period to control the output, without the need to convert it to frequency first. That could be achieved by adjusting the interrupt rate of Timer0 in relation to the measured period.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.