Few months ago, I had to create a real-time signal generator using a microcontroller unit (MCU) that would control the trajectory of a multi-kilowatt laser. As the process involved is for large-scale laser welding, it was requested that the system be highly reliable.
I was given an ATMEGA1284P with an external 16MHz quartz. (To see how to set up the external clock, click here)
The signal being generated was between 5-30Hz (which is quite slow), so I tried using time-based Sleep() instructions. This resulted in a terribly incorrect frequency.
The solution was to use interrupts. Interrupts are used everywhere. In software, they can be used to handle errors that pop up during program execution. In electronics, a button press can generate an interrupt, leading to a function execution.
In our case, the interrupt is generated from the MCU itself.
In short, the quartz --which is a crystal oscillator-- 'ticks' (hopefully) at a constant frequency. The MCU is able to count these ticks. At every certain number of ticks (which you assign), the MCU can call a function called an Interrupt Service Routine (ISR).
The benefit of this method is that as long as the quartz is reliable, the MCU will reliably run the ISR at every few instances in time.
The biggest drawback -- and something to watch out for -- is that the ISR must be very concise (a loop of any kind is a big no-no).
Example Problem
Say we want to generate a sine signal to an analog-out pin at a certain frequency.
We can use interrupts to output a certain value of the sine wave at a given time. Unfortunately, depending on the MCU (and true for ATMEGA1284P), calculating sin() within the ISR takes a long time -- long enough to cause delays in the signal. Since sine is a periodic function, it is much better to pre-compute the values with a given temporal resolution and call them from an array during the ISR.
Initialization
First, we need to declare a few variables.
#define RESOLUTION 250
volatile unsigned int counterMaxValue;
const int clkspeed = 16000000;
const int prescaler = 64;
volatile int frequency = 5; // Default freq. is 5Hz
// for pre-computed sine table.
int table_index = 0;
double sin_table[RESOLUTION];
float increment;
const int outportB = 2; // PortB
const int outportC = 1; // PortC
counterMaxValue is the number of 'ticks' of the quartz at which the ISR should be called.
clkspeed is the frequency of our quartz, in Hz.
prescaler is the number at which to divide the number of 'ticks,' and it allows us to use a certain range of frequencies. Read more about prescaler
here.
The Interrupt
Now, we must configure the hardware. We are using Timer 1 of the 1284P, in the
Clear Timer on Compare (CTC) mode.
When I was first learning about CTC, I used
this resource.
void initInterrupt() {
// INTERRUPT SET UP
cli();//stop interrupts
TCCR1A = 0;// set entire TCCR1A register to 0
TCCR1B = 0;// same for TCCR1B
/*
CTC mode activation:
The counter value (TCNT1) is incremented from 0 to the value specified in OCR1A.
Once it hits that value, the interrupt is generated, and the interrupt service routine (ISR) is called.
The below setting for TCCR1B enables this to happen. See the datasheet for more details.
*/
TCCR1B |= (1 << WGM12)|(1 << CS11)|(1 << CS10);
// set compare match register for freq * n increments
counterMaxValue = (unsigned int) (clkspeed/(frequeny*n*prescaler) - 1);
OCR1A = counterMaxValue;//eg:(16*10^6)/(2000*64)-1 (must be <256 for 8 bit counter.); 124 for 2kHz
//initialize counter value to 0
TCNT1 = 0;
// enable timer compare interrupt
TIMSK1 |= (1 << OCIE1A);
//enable interrupts
sei();
}
After this function is run, the interrupts will begin. But first, we must set up a pre-computed sine table; and above all, we must define the ISR.
The setup()
void setup()
{
...
// Pre-compute sine table
increment = 6.283185/(RESOLUTION-1);
for (int index = 0; index < RESOLUTION; index++)
{
sin_table[index] = sin(x);
x = x + increment;
}
initInterrupt();
}
The ISR
I was using Wiring to program the MCU, and there was an error when I tried to use any of the timers. The problem was solved by commenting out the definition set out by wiring.
// Had to comment out line 77-86 of ...\wiring-0100\wiring-0100\cores\AVR8Bit\WHardwareTimer.cpp
// in order to 're-define' TIMER1_COMPA_vect
ISR(TIMER1_COMPA_vect) {
// NO LOOPS / WAITING / DELAYS IN THE ISR!!
// Traverse through the table. This is much faster than computing sine every time! :)
// Equivalent to:
// y = sin(x)*amplitude+1638+offset; // Based at 5V, up to 4V peak to peak; offset +/- 1V. 2047 - 409 = 1638; 5V - 1V to account for offset
// x = x + increment;
if (table_index > RESOLUTION-1) // loop back to beginning
{
table_index = 0;
}
y = sin_table[table_index]*amplitude+1638+offset;
table_index++;
output = word(y);
portWrite(outportB, int (lowByte(output)));
portWrite(outportC, int (highByte(output)));
}
Just a quick note on outputting the signal: I am using a 12-bit digital to analog converter (DAC). Two ports on the MCU are used, each outputting half of the word.
The DAC I used allowed two modes of output: unipolar (0V to 10V), or bipolar (-5V to 5V). I used the latter, which means that when I send
(1111 1111 1111)2 to the DAC, the output value will be
(+Vref)*(2047/2048).
In short, output value of '409' from the MCU corresponds to 1V output from the DAC.
Now everything is ready! An empty loop() suffices to run this on the MCU.
The loop()
void loop(){}
After uploading the program to your 1284P, it should start sending out sine signals in digital form to the DAC, which it can then transform to the analog signal.
Conclusion
Establishing accurate real time controls on a microcontroller requires us to use timer-based interrupts. There are many benefits to this method, including parallelization of tasks for the MCU.
In other words, the MCU can do other things, such as polling for user input in the main loop(), while ISR takes care of the timing-based tasks. This would not be possible using the sleep() function, which simply waits for a certain amount of time until moving on to the next instruction.
Lastly, the most important thing to consider when using timer-based interrupts is to keep the ISR as simple and short as possible!