Time accurate multi threading using Interrupts

Hello everyone,

I have a programming issue I would like to handle with multi threading based on interrupts, here is the topic. I a m currently building an Arduino based sound generation engine, which basically receives incomming Midi messages and generates the associated sound on a DAC.

The code today has a simple Main loop in the one Midi data is handled, and an interrupt vector trigged every 50us in the one each sample is processed and brought to the DAC. I use Timer2 to trigger this 50us interrupt and it's working fine up to now.

The main loop is making two actions :

1 - listens to serial in and process input commands. 2 - every 1ms, it updates what I call "low speed parameters" of the synth, like enveloppe and so on.

Because those two tasks will some day interfere, I would like to switch to a different programming scheme but I don't know if it is feasible.

Can I generate a second interrupt with another Timer, and make it happen every 1ms to do that "low speed task" I need to do every 1ms? I don't want it to interfer with the hight speed interrupt that generates the sample : can I prioritise the whole interrupt thing so that when the Timer2 sample interrupt is triggered, no matter if it is running the main loop or the low speed interrupt, it is executed. And on the other hand, can I configure the high speed interrupt so that the lowest priority interrupt can be handled right after it has completed its task when it is triggered?

Thanks in advance for your help.

If this can ease the understanding, this is the configuration of the Timer2 for my current high speed task:

  //  Configuration du Timer 2 pour la fréquence d'échantillonnage
  //  No output (COM2A0 cleared, set = Toggle on Match)
  //  Compteur en FAST PWM, Clear on Match OCR2A, Update at BOTTOM
  //  Clock /8 (CS21 set, si set CS20 en plus /32)
  TCCR2A = _BV(COM2A0) | _BV(WGM21) | _BV(WGM20);
  TCCR2B = _BV(WGM22)  | _BV(CS21);
  TIMSK2 = _BV(TOIE2); // Enable timer overflow interrupt, once every 1/22039Hz = 45.375us.
  OCR2A = SMP_REG;

Moderator: edited for legibility. AWOL

I may have found some data inside the AVR328 datasheet...

The lowest addresses in the program memory space are by default defined as the Reset and Interrupt Vectors. The complete list of vectors is shown in ”Interrupts” on page 58. The list also determines the priority levels of the different interrupts. The lower the address the higher is the priority level. RESET has the highest priority, and next is INT0 – the External Interrupt Request 0. The Interrupt Vectors can be moved to the start of the Boot Flash section by setting the IVSEL bit in the MCU Control Register (MCUCR). Refer to ”Interrupts” on page 58 for more information. The Reset Vector can also be moved to the start of the Boot Flash section by programming the BOOTRST Fuse, see ”Boot Loader Support – Read-While-Write Self-Programming” on page 279.

In the list given in the chapter "Interrupt", for Timer Interrupts, The higher the number, the higher the interrupt priority is.

So I'll give a try with the timer from Timer1 as a low speed task timer. But still this does not clarify whether the lowest priority interrupt vector will be interrupted.

If you want to be able to process other interrupts while in the middle of a current interrupt, you'll have to set the global interrupt flag upon entering your 'low priority' interrupt. This is because the AVR disables interrupts whenever it enters an ISR (and then reenables them upon exiting that ISR). This is to avoid cascading interrupts. For your purposes, you can insert an sei() as the very first statement in your low priority ISR, and the Arduino will respond to your high priority ISR even in the middle of the low priority ISR. Be aware that it will also respond to any other interrupts it may receive during that time as well.

If the processing of your high priority ISR is absolutely time critical, be aware that even inserting a sei() as the very first statement in your low priority ISR, there are still a handful of instructions prior to this sei() call in which interrupts will still be disabled. There is another way around this that isn't surfaced by Arduino. The AVR libc library provides a method of declaring your ISR as a noblock, in which the global interrupt flag is never disabled. See this page: http://www.nongnu.org/avr-libc/user-manual/group__avr__interrupts.html for details (in particular, the section on nested ISRs).

It's normally bad form to nest interrupts and a good way to get into trouble. Apart from that I don't seen any reason to, you have working code now (which we haven't seen) that handles the time-critical DAC stuff and the serial receiving is easily handled by the main loop I would think.

If that's all there is to it I see no reason for a second interrupt, and certainly not a nested one.


Rob

Thank you for your feedback, very interesting. I prefer to share the code before continuing, so that I can be well understood (I removed my LUT to avoid reaching forum char limit)

// Generic Synth Defines
#define BASEACC 910
#define BASEFREQ 440

#define BASEATTACK 1
#define BASEDECAY 1
#define BASESUSTAIN 255
#define BASERELEASE 1

#define IDLE 0
#define ATTACK 1
#define DECAY 2
#define SUSTAIN 4
#define RELEASE 5

#define SYSTEM_TIC 10 //ms
#define TRIGGED 1
#define NOT_TRIGGED 0
#define SMP_REG 90
#define DEFAULTMIDICHANNEL 0

// Midi Message Defines
#define CC_ATTACK 16
#define CC_DECAY 17
#define CC_SUSTAIN 18
#define CC_RELEASE 19

// LUT LENGTH
#define LUT_FORM 256
#define LUT_FREQ 83

// === Système de Synthèse monophonique ===
unsigned int delta_acc = BASEACC;      // delta accumulateur de phase
unsigned int acc = 0; // accumulateur de phase

// Synth State
unsigned int freq = BASEFREQ;
char synth_status = NOT_TRIGGED;
unsigned int sample=0;

//Pin connected to ST_CP of 74HC595
int latchPin = 10;
int clockPin = 13;
int dataPin = 11;
////
unsigned char test_char=0;
// ADSR Function
unsigned int enveloppe = IDLE;
unsigned int ADSR = IDLE;

unsigned int attack = BASEATTACK; // varie entre 0 et 127 x SYSTEM_TIC
unsigned int decay = BASEDECAY; // varie entre 0 et 127 x SYSTEM_TIC
unsigned int sustain = BASESUSTAIN; // varie entre 0 et 127 
unsigned int release = BASERELEASE; // varie entre 0 et 127 x SYSTEM_TIC
unsigned int state = IDLE;

// Timing et Monitoring
unsigned long sync_top = 0;  // System operating TIC, basi
unsigned long time =0;

// Midi Variables
unsigned char mm_com;
unsigned char mm_data1;
unsigned char mm_data2;
unsigned char mm_lastnote=0;
unsigned char mchannel = DEFAULTMIDICHANNEL;



//--- Used to setup SPI based on current pin setup
//    this is called in the setup routine;
void setupSPI(){
  byte clr;
  SPCR |= ( (1<<SPE) | (1<<MSTR) ); // enable SPI as master
  SPCR &= ~( (1<<SPR1) | (1<<SPR0) ); // clear prescaler bits
  clr=SPSR; // clear SPI status reg
  clr=SPDR; // clear SPI data reg
  SPSR |= (1<<SPI2X); // set prescaler bits
  delay(100);
}


//--- The really fast SPI version of shiftOut
byte spi_transfer(byte data)
{
  SPDR = data;              // Start the transmission
  loop_until_bit_is_set(SPSR, SPIF);
  return SPDR;              // return the received byte, we don't need that
}

void setup() {
  // put your setup code here, to run once:
  pinMode(latchPin, OUTPUT);
  pinMode(clockPin, OUTPUT);
  pinMode(dataPin, OUTPUT);
  pinMode(2, OUTPUT);
  digitalWrite(latchPin, HIGH);
  digitalWrite(clockPin, LOW);
  digitalWrite(dataPin, LOW);

  // Setup de la liaison SPI
  byte clr;
  SPCR |= ( (1<<SPE) | (1<<MSTR) ); // enable SPI as master
  SPCR &= ~( (1<<SPR1) | (1<<SPR0) ); // clear prescaler bits
  clr=SPSR; // clear SPI status reg
  clr=SPDR; // clear SPI data reg
  SPSR |= (1<<SPI2X); // set prescaler bits
  delay(100);

  //  Configuration du Timer 2 pour la fréquence d'échantillonnage
  //  No output (COM2A0 cleared, set = Toggle on Match)
  //  Compteur en FAST PWM, Clear on Match OCR2A, Update at BOTTOM
  //  Clock /8 (CS21 set, si set CS20 en plus /32)
  TCCR2A = _BV(COM2A0) | _BV(WGM21) | _BV(WGM20);
  TCCR2B = _BV(WGM22)  | _BV(CS21);
  TIMSK2 = _BV(TOIE2); // Enable timer overflow interrupt, once every 1/22039Hz = 45.375us.
  OCR2A = SMP_REG; 

  Serial.begin(31250);
}

void loop() {
  //  put your main code here, to run repeatedly: 
  //  shiftOut(dataPin, clockPin, MSBFIRST, test_char);  
  //  delta_acc = 1000+analogRead(A0);
  //  Serial.println(delta_acc,DEC);
  sync_top = millis();

  switch (state) {
  case IDLE:
    // Synth is IDLE
    break;
  case RELEASE:
    if (ADSR<=release) {
      enveloppe = 0;
      ADSR = 0;
      state = IDLE;
    }
    else {
      ADSR -= release;
      enveloppe = LUT_exp[ADSR];
    }
    break;
  case SUSTAIN:
    if (synth_status==NOT_TRIGGED){
      state=RELEASE;
      ADSR = sustain;
    }
    break;
  case DECAY:
    ADSR -= decay;
    //Serial.println(ADSR,DEC);
    if (ADSR<=sustain) {
      ADSR=sustain;
      state=SUSTAIN;
    }
    enveloppe = LUT_exp[ADSR];
    //enveloppe = ADSR>>1;
    break;
  case ATTACK:
    ADSR += attack;
    if (ADSR>=255) { 
      ADSR=255;
      state = DECAY;
    }
    enveloppe = LUT_exp[ADSR];
    break;
  }

  while(millis()-sync_top<10) {
    //=============================
    // Midi Data Process
    if (Serial.available()>0){
      mm_com = Serial.read();                     
      if (mm_com>0x7F) {   
        // Midi Command msg
        if ( mchannel == 0) {            
          // Channel Check
          mm_com &= 0xF0;
          while(Serial.available()<2){
            // Waiting for serial buffer to be fed 
            // with the 2 next command bytes
          }
          mm_data1 = Serial.read();
          mm_data2 = Serial.read();
          // Note Off
          if ((mm_lastnote==mm_data1)&&((mm_com==0x80)||((mm_com==0x90)&&(mm_data2==0)))) {
            state = RELEASE;
            synth_status = NOT_TRIGGED;
            //      digitalWrite(13,LOW);
          }
          // Note On
          else if ((mm_com==0x90)&&(mm_data1>=20)) {
            delta_acc = LUT_freq[mm_data1-20];
            state = ATTACK;
            synth_status = TRIGGED;
            mm_lastnote=mm_data1;
            //    digitalWrite(13,HIGH);
          }
          // Control change 
          if (mm_com==0xB0) {
            switch (mm_data1){

              // Attack change
            case CC_ATTACK :
              attack = 127-mm_data2;
              break;

              // Decay change
            case CC_DECAY :
              decay = 127-mm_data2;
              break;

              // Sustain Change
            case CC_SUSTAIN :
              sustain = mm_data2;
              break;

              // Release Change
            case CC_RELEASE :
              release = 127-mm_data2;
              break;
            }
          }
        }    
      }
    }
  }

}

// It
ISR(TIMER2_OVF_vect)
{
  acc += delta_acc; 
  sample = (LUT_exp[acc>>8]*enveloppe)>>8;
  spi_transfer(sample);
  PORTB &= ~(4); 
  PORTB |= 4; 
}

As you can see, the basic loop is performing Midi cycling while waiting for the next 10ms system tick (that I want to lower to 1ms later on).

And I do not think that this way to code is very elegant in the end. But maybe changing the structure of the main is the key.

I think I see where you're going Graynomad: do you think that nesting interrupts will lead to scenarios where the sample will not be processed 'on time' because the lower interrupt had just been triggered a few cycles before, delaying the high speed interrupt in an uncontrolled manner?

That may generate audio glitch indeed. You're right, I may try and clean my main first.

Thanks

That’s the sort of trouble you can get into.

That code looks pretty good to me, I’d probably get rid of the

while(millis()-sync_top<10) {

block as i don’t see any reason to only test for serial chars every N mS.

Bear in mind that the serial reception is already interrupt driven.

EDIT: Just noticed, the two variables (acc and delta_acc) used in the ISR should be declared with “volatile”


Rob

Many thanks for this important remark. I just gave a look at this wikipedia page, and it helped a lot understanding compiler things. Shall this be applied also on the enveloppe variable, and all the variables that can be modified or called inside the IT?

My concern for the main loop is that I want to update low speed variables (like enveloppe, and later on LFO and other stuff) at a rate which is approximately 1ms. Generally speaking, a Midi message (3 Bytes @ 31250bds) shall take approx 1ms to be transmitted, so considering that the system shall be able o process 2 Midi messages during a 1ms cycle shall allow it to not miss any message.

I have another question which is related to memory usage on the one you also might be able to answer.

I'm currently using 4 Look Up Tables filled with 256 Bytes, and intend in the end to have many more (around 10, and possibly more). So I'm thinking about using the Program Memory using the "Progmem" stuff, and the associated functions that read byte next to adresses. But is it as fast as the usage of simple arrays? Do I have an interest of maintaining basic LUT as RAM based arrays for the high speed task, and use only the progmem for the low speed look up tables?

I will in the end use two types of Look Up Tables: some for sound synthesis in itslef, which require to be fast, and some for low speed tasks like response curve, enveloppe curves and LFO purpose.

Regards

Progmem access is slower than RAM access. It is all ultimately RAM access because all progmem values will get copied into at least a temporary RAM location before being used. Can't really say if it will be too slow for your purposes. Only benchmarking test code will answer that question. The only other option for 10+ 256 byte LUTs would be to move up to an AVR with more RAM. The Arduino Mega uses the Atmega 256, which has 8k of RAM. Plenty of RAM for that many LUTs and more. Any sort of external mem will be far slower than even progmem.

So... let's say I don't need them all in RAM indeed. I really want it to be fast and furious (lol) so I need it in RAM.

I'll keep the ones I use in RAM, and store the whole in progmem.

Thanks !

I think you can do both levels of processing with one timer:

Each 50 microsecond interrupt: Do the DAC output. increment a counter. if the counter reaches 20 (1000 microseconds, 1 mS) Set the counter to 0 ENABLE INTERRUPTS Do the lower-priority processing.

By clearing the counter and enabling interrupts you allow the 50 microsecond interrupts to continue. As long as the low-priority processing takes well under 1 millisecond you should have no problems.