M0AGX / LB9MG

Amateur radio and embedded systems

Making sinewaves with XMEGA DAC

The XMEGA is quite a leap from the "classic" AVRs. Some of the interesting features are the DAC and DMA. When combined, they can be used to generate all kinds of useful signals in the audio range.

This example uses the DAC, DMA, timer and event system to generate 1200 Hz and 2200 Hz sinewaves. I'll show how to make a Bell 202 modem (think: APRS and AX.25) in another post.

How it all works

I use an ATxmega32A4U. Several components have to be brought together to make a signal generator:

  • Array of sinewave samples (or another desired signal)
  • DAC - obviously to output the signal
  • DMA engine - moves the samples to the DAC without any CPU usage
  • timer - decides when (how fast) a new sample should be output
  • event system - connects the timer with the DMA

Why is it so "complicated"? Because otherwise the AVR CPU would have to execute code to load every sample and have little time to do anything else. With this setup the CPU and software is only needed to start the generator. Afterwards the CPU can do other tasks or sleep. Software is only needed to change the frequency ("speed") of the signal or to stop the generator.

Sinewave samples

Let's begin with the sinewave samples to be played. Public interface of the sinewave module is very simple - basically just an array of uint8_t numbers and an init function. This function copies samples from flash to RAM. This is needed because DMA can only move data between RAM and peripherals (it can't access flash). I could skip the PROGMEM attribute (and let the startup code do the copying) but an explicit copy function allows me to adjust the amplitude of the signal.

The interface
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#ifndef SINEWAVE_H_
#define SINEWAVE_H_
#include <stdint.h>

#define SINEWAVE_SAMPLES_N 100

extern uint8_t SINEWAVE[SINEWAVE_SAMPLES_N];

void sinewave_init(uint8_t scale_percent);

#endif
The data
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include "sinewave.h"
#include <avr/pgmspace.h>

uint8_t SINEWAVE[SINEWAVE_SAMPLES_N]; //copy in RAM

const uint8_t SINEWAVE_BASE[SINEWAVE_SAMPLES_N] PROGMEM = {
122, 129, 136, 143, 151, 158, 165, 171, 178, 184, 
190, 196, 202, 207, 212, 216, 220, 224, 227, 230, 
233, 235, 236, 238, 238, 239, 238, 238, 236, 235, 
233, 230, 227, 224, 220, 216, 212, 207, 202, 196, 
190, 184, 178, 171, 165, 158, 151, 143, 136, 129, 
122, 115, 108, 101, 93, 86, 79, 73, 66, 60, 
54, 48, 42, 37, 32, 28, 24, 20, 17, 14, 
11, 9, 8, 6, 6, 5, 6, 6, 8, 9, 
11, 14, 17, 20, 24, 28, 32, 37, 42, 48, 
54, 60, 66, 73, 79, 86, 93, 101, 108, 115};

void sinewave_init(uint8_t scale_percent){
    for (uint8_t i = 0; i < SINEWAVE_SAMPLES_N; i++){
        uint16_t tmp = pgm_read_byte(&SINEWAVE_BASE[i]);
        tmp = tmp * scale_percent;
        tmp = tmp / 100;
        SINEWAVE[i] = tmp;
    }
}

The init function is pretty obvious. Magic numbers were generated using the following python script. I found out that a small offset was needed because the DAC could not reach 0V and clipped the signal.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#!/usr/bin/env python
import math

#the DAC will use 8-bit values
#Bell 202 modem generates 1200 and 2200Hz tones so the highest
#conversion frequency must be 4400.
#To conserve flash only the part of the sine from 1 to -1
#will be stored and played from top to bottom and then backwards

AMPLITUDE_SCALE = 1
AMPLITUDE = (127*AMPLITUDE_SCALE) - 10
OFFSET = AMPLITUDE + 5
N_SAMPLES = 100

full_phase = 2*math.pi

range_start = 0
range_stop = N_SAMPLES

output = ''
output += '#include "sinewave.h"\n'
output += 'const uint8_t SINEWAVE[SINEWAVE_SAMPLES_N] = {'

element_counter = 0

for i in range(range_start, range_stop):
    if element_counter % 10 == 0:
        output += '\n'
    element_counter += 1
    fraction = float(i)/float(N_SAMPLES)
    angle = full_phase*fraction
    value = int(AMPLITUDE*math.sin(angle)) + OFFSET
    output += '%d' % value
    output += ', '

output = output[0:-2] #remove last comma and space
output += '};'
print output

The generator

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#ifndef DDS_H_
#define DDS_H_
#include <stdbool.h>

void dds_init(void);
void dds_start(bool vref_avcc);
void dds_shift(void); //change frequency
void dds_off(void);

#endif

The generator is prepared to output 1200Hz and 2200Hz sinewaves so there is just a shift function to switch between these two frequencies. First element to initialize is the timer that has to count up to a particular top value, reset and start again (CTC mode in the manual). Calculation of the top value is described in the comments.

Second element to initialize is the event system. It allows to transmit interrupt-like events between peripherals so that no CPU action is required. Here, the event channel 0 is set to be triggered by timer overflow. Next the DMA is set up. This may look complex but it boils down to:

  • source address - sinewave table (lines 58-60)
  • target address - DAC output register (lines 55-57)
  • which of the addresses should be incremented (only the sample address), by what amount and if the transfer should be repeated continuously (line 52)
  • how many transfers should take place (line 61)
  • when should a transfer happen - on event channel 0 (line 54)

After initialization, the dds_start function enables the DAC, configures reference voltage (full-scale DAC output can be either Vcc or 1V), loads factory DAC calibration data and starts the timer. Now - every timer overflow an event will be triggered, that will make the DMA update sample value in DAC output register.

While the generator is running the overflow value of the timer can be adjusted on the fly to change output frequency and this is done by the dds_shift function.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
#include <avr/io.h>
#include <avr/cpufunc.h>
#include "dds.h"
#include "sinewave.h"

/* This driver uses a sinewave lookup table (in RAM - because DMA controller
 * can't access flash), timer, event system and DAC.
 * The timer generates an overflow event, this event is sensed by the DMA
 * controller working in single shot mode. DMA controller reads sinewave sample
 * and writes it into the DAC. DMA transfer is configured to wrap adround the
 * source address after a complete transaction.
 * 
 * Even though DMA controller can directly sense timer overflow bit, it can
 * not be used, because that bit will not be cleared and will fire DMA transfers
 * continously. It is solved by using the event system - an event is fired only
 * once when the timer overflows.
 */

#define DDS_DMA_CH DMA.CH1
#define TIMER_DDS TCE0 //used to trigger DMA->DAC transfer

#define FREQ_MARK  1200
#define FREQ_SPACE 2200

/* sinewave.c provides SINEWAVE_SAMPLES_N that must be played
 * fast enough to generate 1200 and 2200Hz signals
 * 
 * Xmega DAC can output up to 250Ksps
 * 
 * DAC sample load rate for 1200Hz is 1/(1200*SINEWAVE_SAMPLES_N) [seconds]
 * DAC sample load rate for 2200Hz is 1/(2200*SINEWAVE_SAMPLES_N) [seconds]
 * 
 * SINEWAVE_SAMPLES_N = 100
 * F_MARK             = 2200 Hz
 * DAC samples per second = 220'000 [Hz] <-- this is also the timer event rate
 */

//formula for those values is (F_CPU / timer_prescaler)/(FREQ * SINEWAVE_SAMPLES_N)
#define TIMER_PER_MARK  267 //exact value is 266,(6)  - relative difference is 0,998751
#define TIMER_PER_SPACE 145 //exact value is 145,(45) - relative difference is 0,996875

void dds_init(void){
    DMA.CTRL  = DMA_ENABLE_bm;
    TIMER_DDS.CTRLB = TC_WGMODE_NORMAL_gc;
    TIMER_DDS.PER = TIMER_PER_MARK;
    TIMER_DDS.CTRLD = TC_EVACT_RESTART_gc; //enable overflow event

    EVSYS.CH0MUX = EVSYS_CHMUX_TCE0_OVF_gc;

    //DMA channel setup
    DDS_DMA_CH.CTRLA = DMA_CH_RESET_bm;
    DDS_DMA_CH.ADDRCTRL = DMA_CH_SRCDIR_INC_gc /*increment*/ | DMA_CH_DESTDIR_FIXED_gc |  DMA_CH_SRCRELOAD_TRANSACTION_gc;

    DDS_DMA_CH.TRIGSRC = DMA_CH_TRIGSRC_EVSYS_CH0_gc;
    DDS_DMA_CH.DESTADDR0 = (((uint16_t)&DACB.CH0DATAH) >> 0) & 0xFF;
    DDS_DMA_CH.DESTADDR1 = (((uint16_t)&DACB.CH0DATAH) >> 8) & 0xFF;
    DDS_DMA_CH.DESTADDR2 = 0x00;
    DDS_DMA_CH.SRCADDR0 = (((uint16_t)SINEWAVE) >> 0) & 0xFF;
    DDS_DMA_CH.SRCADDR1 = (((uint16_t)SINEWAVE) >> 8) & 0xFF;
    DDS_DMA_CH.SRCADDR2 = 0x00; //internal SRAM 
    DDS_DMA_CH.TRFCNT = SINEWAVE_SAMPLES_N; //transfer length

    DACB.CH0DATAL = 0;

    DDS_DMA_CH.CTRLA = DMA_ENABLE_bm | DMA_CH_SINGLE_bm | DMA_CH_BURSTLEN_1BYTE_gc | DMA_CH_REPEAT_bm; //enable DMA transfer

    PORTB.DIRSET = PIN2_bm; //use PB2 as output pin
    PORTB.OUTCLR = PIN2_bm;
}

uint8_t calibration_read_byte(uint8_t index){
    uint8_t result;
    NVM_CMD = NVM_CMD_READ_CALIB_ROW_gc;
    result = pgm_read_byte(index);
    NVM_CMD = NVM_CMD_NO_OPERATION_gc;
    return result ;
}

void dds_start(bool vref_avcc){
    DACB.CTRLA = DAC_CH0EN_bm | DAC_ENABLE_bm;

    if (vref_avcc){
        //left shift all samples, use AVCC (3,3V) as reference
        DACB.CTRLC = DAC_REFSEL_AVCC_gc | DAC_LEFTADJ_bm; 
    } else {
        //left shift all samples, use default 1V reference
        DACB.CTRLC = DAC_LEFTADJ_bm; 
    }

    DACB.CH0GAINCAL   = calibration_read_byte( offsetof(NVM_PROD_SIGNATURES_t, DACB0GAINCAL) );
    DACB.CH0OFFSETCAL = calibration_read_byte( offsetof(NVM_PROD_SIGNATURES_t, DACB0OFFCAL) );

    TIMER_DDS.CTRLA = TC_CLKSEL_DIV1_gc; //start the timer

    TIMER_DDS.PER = TIMER_PER_MARK;

}

void dds_shift(void){
    //This function makes timer max value alternate between
    //mark and space symbol timing. It executes in constant time.
    //Counter value has to be reset every time for consistent timing.
    TIMER_DDS.PER = (TIMER_PER_MARK+TIMER_PER_SPACE) - TIMER_DDS.PER;
    TIMER_DDS.CNT = 0;
}

void dds_off(void){
    TIMER_DDS.CTRLA = TC_CLKSEL_OFF_gc; //stop the timer
    DACB.CTRLA = 0; //disable DAC
}