After yesterday’s article about ADC, it seems fitting to describe the other side of the coin: the digital-to-analog converter (DAC) and how to generate an analog waveform with it.

DAC is easy

This is one of the simplest hardware peripherals there are, as far as the software side is concerned. I’m again using a Nucleo-L053, since not all L0-series µCs have a DAC.

Here is the basic driver, as now added to the JeeH code repository:

struct DAC {
    constexpr static uint32_t base    = 0x40007400;
    constexpr static uint32_t cr      = base + 0x00;
    constexpr static uint32_t dhr12r1 = base + 0x08;

    static void init () {
        MMIO32(Periph::rcc+0x38) |= (1<<29);  // enable DAC
        MMIO32(cr) = (1<<0);  // EN1
    }

    static void write (uint32_t val) {
        MMIO32(dhr12r1) = val;
    }
};

And here’s an example of how to generate a sawtooth signal with it:

#include <jee.h>

DAC dac;
PinA<4> aout;

int main() {
    aout.mode(Pinmode::in_analog); // disable digital circuitry
    dac.init();

    uint32_t i = 0;
    while (true)
        dac.write(i++ % 4096);
}

The result is a very nice sawtooth wave:

But as you’ll notice, it’s not 100% rail-to-rail - the signal range is slightly less than the full 0.0V .. 3.3V. Furthermore, there are some slight non-linearities at both ends.

To avoid signal distortion, it’s best to avoid these extreme excursions, i.e. DAC values just above 0 and just below 4095. I decided to avoid the first and last 128 points.

Making waves

Next, let’s generate a sine wave. For this, I wrote a Python script to generate the values:

# use as: python gensine.py | fold -s -w 80 >src/sine.h

import math

N = 2500    # number of entries in wave table
R = 1920    # range of values, +/- around the offset
O = 2048    # offset, set to mid-range of the DAC

L = []
for i in range(N):
    v = math.sin(i*2*math.pi/N)
    w = O + int(R*v+0.5)
    L.append(str(w))

print(", ".join(L))

When executed with the command shown above, we get a src/sine.h file with this data:

2048, 2053, 2058, 2062, 2067, 2072, 2077, 2082, 2087, 2091, 2096, 2101, 2106,
2111, 2116, 2120, 2125, 2130, 2135, 2140, 2144, 2149, 2154, 2159, 2164, 2169,
2173, 2178, 2183, 2188, 2193, 2197, 2202, 2207, 2212, 2217, 2221, 2226, 2231, 
[...]
1866, 1871, 1876, 1880, 1885, 1890, 1895, 1900, 1904, 1909, 1914, 1919, 1924,
1928, 1933, 1938, 1943, 1948, 1953, 1957, 1962, 1967, 1972, 1977, 1981, 1986,
1991, 1996, 2001, 2006, 2010, 2015, 2020, 2025, 2030, 2035, 2039, 2044

Note that the calculated sine wave points cover one full cycle (i.e. 2π), are centred around 2048, i.e. the middle of the DAC range, and have excursions of ± 1920, nicely avoiding the 128-point non-linear ranges near the DAC’s two extremes.

These sine wave constants can be added to the C++ code as foillows:

const uint16_t sine [] = {
#include "sine.h"
};

And the main loop in the demo can now be changed to:

    uint32_t i = 0;
    while (true)
        dac.write(sine[i++ % 2500]);

The resulting signal is a very clean sine wave:

That’s great, but it’s obviously not 50 Hz. It’s whatever timing this loop happens to have.

Generating 50 Hz

To actually generate a 50 Hz signal with this 2500-point sine table, we need to send those 2500 points to the DAC in exactly 20 ms. That’s one value every 8 µs.

While this could be done with a timer and interrupts, it would 1) not be very precise when other interrupts interfere, and 2) require lots of interrupts and keep the µC very busy.

There’s a better way, with the Direct Memory Access (DMA) controller built into all STM32 µCs. This device can be told to transfer values from one memory range to another, and it can be driven by a hardware timer to maintain a fixed transfer rate. Also, the DMA can be set to “circular” mode, so that it will continue sending that range of memory words to the DAC over and over again. Note that there’s zero CPU involvement in this, once set up!

It takes some serious sleuthing in the RM0367 reference manual to get all the pieces working together properly. But the end result is beautifully simple to use:

#include <jee.h>

// sine table: has 2500 entries with a swing of +/- 1920 around 2048
// when sampled at 125 kHz, this will produce a pure 50 Hz sine wave
const uint16_t sine [] = {
#include "sine.h"
};

DAC dac;

int main() {
    enableClkAt32mhz();

    PinA<4>::mode(Pinmode::in_analog);
    dac.init();

    dac.dmaWave(sine, 2500, 256); // 32 MHz / 256 = 125 ksps = 50 Hz

    while (true) {}
}

And indeed, the resulting signal is a very pure 50 Hz sine wave. Here is an FFT of the signal, as my scope so helpfully calculates:

(please ignore the moiré pattern at the top, its an artifact of the scope’s screen capture)

The strongest harmonic is at 150 Hz, and it’s ≈ 59 dBm below the 50 Hz wave. Excellent!

How it’s done

For the curious (I hope everyone’s reading this), here is the code that does the hard work:

    static void dmaWave (const uint16_t* ptr, uint16_t num, uint16_t div) {
        MMIO32(cr) |= (1<<12) | (1<<2);  // DMAEN1, TEN1

        { // DMA setup
            const uint32_t dma    = 0x40020000;
            const uint32_t ccr2   = dma + 0x1C;
            const uint32_t cndtr2 = dma + 0x20;
            const uint32_t cpar2  = dma + 0x24;
            const uint32_t cmar2  = dma + 0x28;
            const uint32_t cselr  = dma + 0xA8;

            MMIO32(Periph::rcc+0x30) |= (1<<0);  // DMAEN
            MMIO32(cselr) |= (9<<4);  // DAC on DMA chan 2
            MMIO32(cndtr2) = num;
            MMIO32(cpar2) = dhr12r1;
            MMIO32(cmar2) = (uint32_t) ptr;

            // msize 16b, psize 16b, minc, circ, m2p
            MMIO32(ccr2) = (1<<10) | (1<<8) | (1<<7) | (1<<5) | (1<<4);
            MMIO32(ccr2) |= (1<<0);  // EN
        }

        { // TIM6 setup
            const uint32_t tim6 = 0x40001000;
            const uint32_t cr1  = tim6 + 0x00;
            const uint32_t cr2  = tim6 + 0x04;
            const uint32_t dier = tim6 + 0x0C;
            const uint32_t arr  = tim6 + 0x2C;

            MMIO32(Periph::rcc+0x38) |= (1<<4);  // TIM6EN
            MMIO32(arr) = div-1;
            MMIO32(dier) = (1<<8); // UDE
            MMIO32(cr2) = (2<<4); // MMS update
            MMIO32(cr1) |= (1<<0); // CEN
        }
    }

The DAC needs to be told to accept DMA events, the DMA device needs to be set up with all the right incantations to drive the DAC, and timer #6 needs to be set up to count down at the proper rate and generate events which drive the DAC’s DMA channel.

And that’s it. A clean 50 Hz signal comes out of pin PA4, while the CPU is free to do whatever it wants (including nothing, as in the above demo). No sweat!

References