Tuesday, September 24, 2013

Advanced Arduino Sound Synthesis

I read with interest Jon Thompson's skill builder article Advanced Arduino Sound Synthesis in Make: Magazine.  I have always been interested in electronics, sound, amplification, and music of all kinds, and found myself with some time to explore.  So, I broke out my Atmel chips and o'scope and started experimenting.  I write here about the things that I got hung up on while really trying to understand all of the great information presented in the article.

The first thing I had to deal with was that Jon used the Arduino Nano v3.0 board.  I don't have one of those.  The microcontroller on those boards is the Atmel ATmega328.  I tend to do lots of breadboarding (before Arduino became popular) and have a stock of the older ATmega8 chips.  So, no big deal, they are very similar to the 328 chips.

Since the article is about the Arduino, it is natural that all the source code is based on the Arduino libraries.  I am a command line guy, so I typically use avr-gcc and make tools, along with my trusty home-made USBasp programmer.  I often viewed Arduino as eye-candy that got in the way of the "real programming."  But since Arduino has take over the world, I figured now was the time to see what it was all about.  So I downloaded the Mac version of the IDE, and of course went for the V1.5.4 beta.

The first question I asked myself was "how do I get the Arduino bootloader on a blank ATmega8?"  After some quick research, I realized that you don't have to.  The Arduino IDE has an "Upload Using Programmer" option right off the File menu.  And, the USBasp programmer is supported out of the box (Tools/Programmer).  Nice.

Next problem.  How do you target the build for the ATmega8 on a breadboard?  The closest setting I found was "Arduino NG or older".  So I got listing_2 from the article to generate a series of waveforms, slapped it in the IDE, and viola...errors.

I soon realized that the errors were not due to a targeting problem, but instead because the code was written for the 328.  It referenced registers that the ATmega8 did not have.  But the good news is that the two timers needed for the example (Timer1 and Timer2) worked just fine on the ATmega8.  I just had to correct the registers.  The modified listing_2 follows:

       
# define DEBUG 0

/******** Load AVR timer interrupt macros ********/
#include <avr/interrupt.h>

/******** Sine wave parameters ********/
#define PI2     6.283185 // 2 * PI - saves calculating it later
#define AMP     127      // Multiplication factor for the sine wave
#define OFFSET  128      // Offset shifts wave to just positive values

/******** Lookup table ********/
#define LENGTH  256  // The length of the waveform lookup table
byte wave[LENGTH];   // Storage for the waveform

/******** Waveform parameters ********/
#define SINE     0
#define RAMP     1
#define TRIANGLE 2
#define SQUARE   3
#define RANDOM   4

#if DEBUG
volatile byte timer1_start = 0;
volatile byte timer1_end = 0;
#endif

void setup() {

  /******** Populate the waveform lookup table with a sine wave ********/
  waveform(SINE);                      // Replace sine with the different cases to
                                        // Produce the different waves
#if DEBUG
  // Keep this speed low becuase there are not many cycles left
  Serial.begin(2400);
#endif
  
  /******** Set timer1 for 8-bit fast PWM output ********/
  pinMode(9, OUTPUT);       // Make timer's PWM pin an output
  TCCR1B  = (1 << CS10);    // Set prescaler to 1 - full 8MHz
  TCCR1A |= (1 << COM1A1);  // PWM pin to go low when TCNT1=OCR1A
  TCCR1A |= (1 << WGM10);   // Put timer into 8-bit fast PWM mode
  TCCR1B |= (1 << WGM12); 

  /******** Set up timer 2 to call ISR ********/
  TCCR2 = (1 << CS20);      // Set prescaller to divide by 1
  TIMSK = (1 << OCIE2);     // Set timer to call ISR when TCNT2 = OCR2
  OCR2 = 128;               // sets the frequency of the generated wave
                            // 8Mhz / (OCR2 = 128 * 256)...in this case, 244 Hz
  sei();                    // Enable interrupts to generate waveform!
}

void loop() {  // Nothing to do!
#if DEBUG
  static int diff = 0;
  // This might be negative...ignore it
  diff = timer1_end - timer1_start;
  Serial.println(diff);
  delay(1000);
#endif
}

/******** Called every time TCNT2 = OCR2 ********/
// Question here is...what should be the offset time to set
// TCNT2 given that the timing of my chip is different than the author
// (and maybe even the compiler).
ISR(TIMER2_COMP_vect) {  // Called each time TCNT2 == OCR2
  static byte index=0;    // Points to successive entries in the wavetable
#if DEBUG
  timer1_start = TCNT1L;
#endif
  OCR1AL = wave[index++]; // Update the PWM output
  TCNT2 = 33;  // Timing to compensate for time spent in ISR
#if DEBUG
  timer1_end = TCNT1L;
#endif
}


void waveform(byte w) {
 switch(w) {

   case SINE: 
    for (int i=0; i<LENGTH; i++) 
      {float v = OFFSET+(AMP*sin((PI2/LENGTH)*i));
      wave[i]=int(v);
    }
    break;

  case RAMP:
    for (int i=0; i<LENGTH; i++) {
      wave[i]=i;
    }
    break;

  case TRIANGLE:
    for (int i=0; i<LENGTH; i++) {
      if (i<(LENGTH/2)) {
        wave[i]=i*2;
      } else {
        wave[i]=(LENGTH-1)-(i*2);
      }
    }
    break;
    
  case SQUARE:
    for (int i=0; i<(LENGTH/2); i++) {
      wave[i]=255;
    }
    break;

  case RANDOM:
    randomSeed(2);
    for (int i=0; i<LENGTH; i++) {
      wave[i]=random(256);
    }
      break;
  }
}

After I got the coding errors fixed, the sketch compiled and uploaded easily.  You can see my rig here (the board on the left is like the Nano...that I designed years ago, but it plugs directly into the power rails on the breadboard):



There are a couple of enhancements I made along the way.  Notice in the original interrupt service routine (ISR), Jon makes reference to two lines that are designed to account for time spent in the ISR:

       
/******** Called every time TCNT2 = OCR2A ********/
ISR(TIMER2_COMPA_vect) {  // Called each time TCNT2 == OCR2A
  static byte index=0;    // Points to successive entries in the wavetable
  OCR1AL = wave[index++]; // Update the PWM output
  asm("NOP;NOP");         // Fine tuning
  TCNT2 = 6;              // Timing to compensate for time spent in ISR
}

Notice the NOP and TCNT2 = 6 lines.  I had no idea what I should change these values to.  For one thing, I changed the prescale of Timer2 to 1 instead of 8, so that I could get higher frequencies out of the test (this makes the formula 8MHz / (OCR2 * 256)).  Also, my chip is running at 8MHz vs. 16MHz (for the 328).

I always say, if it offends thee, take it out.  So when I set OCR2 = 128 and took the timing lines out, what I got was the following (126 Hz):


This is clearly not right, and expected, as OCR2 is supposed to set the overflow value for the next value in the waveform table.  But that assumes an instantaneous change.  While the ISR routine does not have much in it, it still takes some clock ticks to execute, but how many?  I am expecting to see 244 Hz (8,000,000 / (128 * 256)).

Through trial and error, I got to a setting of TCNT2 = 33:


But I continued to ask myself...how do I make this more scientific?  I might have expected Jon's value of 6 scaled to account for my slower processor (8 MHz / 16 MHz) and scaled up by the prescale factor difference (8 for his vs 1 for mine).  This would yield 24 plus two ticks for the NOPs, or 26.  33 is off by more than 10%, so how can I figure this out (and how did he...it is not in the article)?

One way is to look at the assembler from the compiler and try to discern the number of clock ticks.  This is beyond what I was willing to do.  But after looking at AVR136 Application Note and studying the code, I realized that I could just time the routine by capturing the timer counter before and after the code executed.  So, if you study my code, you will note a DEGUB symbol that causes that timer difference to be written to the UART0 serial console.  So what was the result?  17.

Hmph!  Where is the other missing time?  I occurred to me that the interrupt service routine must have some overhead associated with it, and it may well be compiler specific.  More Googling...

What I found was this AVR Interrupt Response Time posting by an Atmel engineer.  The answer is 8 to 11 cycles plus any extra time preserving registers (which is a compiler specific optimization).  This jives with 33 - 17 = 16 answer I was looking for.

Here is a final side note.  When I put in the console debugging code, I noticed that I could not get the baud rate specified in the sketch to match the setting I needed to put in to my terminal emulator to see expected results.  I had to specify a baud value 1/2 of that in the sketch.  Having run into this in past projects, I recognized that this must be a clock rate problem in the f_cpu compile directive.  So the Arduino board settings are now coming back to haunt me.  The Arduino NG board runs at 16MHz.  Because I was really into using the Arduino IDE, I figured out how (with much difficulty) to create board files for a breadboarded ATmega8 at different clock rates.  You can see (and use if you want) the result of that work at my github project chuckb/atmega.

No comments:

Post a Comment