The SAMD21 based Arduino boards have a 10 bit DAC, so naturally I wanted to generate a sine wave! Some years ago when I was teaching microcontrollers I had students build a DAC using a resistor ladder. The microcontroller, a 68HCS12, used a timer interrupt to advance the value to the DAC by indexing a table of sine values. I’ve now done the same thing but with the SAMD21.
I built a table of 256 sine values to represent a full cycle. The table entries are a single byte even though the DAC is 10 bits. There are basically two approaches to generating the output. The simplest is to go directly from table value to the next table value with each timer interrupt. So 256 timer periods is one cycle. To adjust the frequency, the timer interrupt rate must be changed. However a nicer approach is to have the interrupts at a fixed rate and adjust the proportion of a cycle that elapses in each interrupt period.
Key to operation is the uint16_t variable phase which represents the proportion of the sine wave cycle represented by the current time. The most significant byte is used as an index into the 256 byte sine table of the output amplitude at that point in the wave. The least significant byte is the interpolated fraction between the current and next table value.
If the phase value is incremented by 256 each timer interrupt, then a full cycle of the sine wave will be output in 256 interrupt periods. A full cycle is a count of 65,536. If the increment is smaller, it will take longer to reach a full cycle phase values that aren’t exactly multiples of 256 will interpolate between table values. If the increment is larger it will take less time to reach a full cycle and not all the table entries will necessarily be used. The first scope capture has an interrupt rate of 5µs and a sine frequency of 10kHz. There are 20 interrupts per cycle and phase is incremented by 65536/20 or 3276 each interrupt. You can easily see the discrete steps:
We can change the output frequency to 100Hz by changing the incrementing value to 32 without changing the interrupt rate. The output becomes much smoother looking:
It’s still making discrete steps every 5µs:
The interpolation process allows us to make use of all 10 bits of the DAC even though the sine table is 8-bits. The interpolation is smooth even though the table entries are only accurate to 8 bits.
A timer overflow creates an event. The event moves the DATABUF register contents to the DATA register which starts a conversion. The DAC interrupt occurs when the DATABUF register is now “empty” and this interrupt routine calculates the next value.
void DAC_Handler(void) { // invoked when DATABUF is emptied
static uint16_t phase; // Represents a 16 bit fraction of a full cycle
// 8 MSB bits index the sine_value table and 8 LSBs
// interpolate
/* wait for the DATA register write to sync -- hopefully no wait! */
while (DAC->STATUS.reg & DAC_STATUS_SYNCBUSY) { ; }
/* Write a value to its DATABUF register */
DAC->DATABUF.reg = dac_value;
phase += PHASE_INCREMENT;
// Calculate DAC value for next time
// by interpolating in the sine_value table
// scale for 10 bit DAC
dac_value = ((sine_value[phase >> 8] * (255 - (phase & 0xff))) >> 6) +
((sine_value[(phase >> 8) + 1] * (phase & 0xff)) >> 6);
DAC->INTFLAG.reg = DAC_INTFLAG_EMPTY; // Reset flag
}