Journey to Engineering

Search IconIcon to open search

How to Use the DMA on Teensy 3.6 DAC to Generate Waveforms

Last updated Nov 24, 2020

# Introduction

In some applications, timing and precision are essential. Unfortunately, in those cases, the fact that your code must be processed by the CPU is not desirable, especially if your MCU is doing lots of other things.

Luckily you can decrease your dependence from the CPU by using Direct Memory Access (DMA).

In this guide, you will learn how to generate a precise and periodic sawtooth waveform attaching the DMA to the DAC of the Teensy 3.6.


# Prerequisites


# Step 1 - Setup DAC0

First of all, you need to set up the DAC, and to do that you need to change some registers.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
uint8_t dac_setup(uint8_t channel) {
  // Set clock gates for DACs
    SIM_SCGC2 |= SIM_SCGC2_DAC0 | SIM_SCGC2_DAC1;

  // Enable selected channel
    if (channel == 0)
        DAC0_C0 = DAC_C0_DACEN | DAC_C0_DACRFS;
    else if (channel == 1)
        DAC1_C0 = DAC_C0_DACEN | DAC_C0_DACRFS;
    else
        return 1;

    return 0;
}

This function will do the following:

  1. setup the System Integration Module (SIM) to activate the DAC clocks;
  2. activate one of the DAC, based on the provided channel.

If the channel is not either 0 or 1, the function will return 1 since only two DACs are onboard the Teensy 3.6.

To understand what exactly you are doing with this code you can do two things:

For example, you can see that SIM_SCGC2_DAC0 is defined as 0x00001000 in the kinetis.h file, somewhere in the Teensy libraries. In practice, the first instruction of the code is telling the Teensy to set to 1 the 12th bit (count starting from 0) of the SIM_SCGC2 register.

Then again, if you search the register in the documentation and look at the definition of the 12th bit you will find the following:

DAC0 Clock Gate Control

This bit controls the clock gate to the DAC0 module.

  • 0 Clock disabled
  • 1 Clock enabled

If you are at the start of your embedded development journey, exploring the code that you are copy/pasting is a tremendous learning tool.


# Step 2 - Load the LUT

Now, the DMA if fast because in general it is activated minimizing the CPU usage. To achieve this, you can create a Look Up Table (LUT) at the start of the program and read values from there. This will avoid the usage of a variable, that needs to be constantly increased and passed to the analogWriteDAC1 function.

1
2
3
4
5
6
7
8
#define LUT_SIZE 8190
uint16_t lut_ramp[LUT_SIZE];

void lut_setup() {
    for (uint32_t i = 0; i < LUT_SIZE; i++) {
        lut_ramp[i] = (i <= LUT_SIZE / 2) ? i : 8190 - i;
    }
}

The previous function will generate a global array 8190 position long. Since the DAC of the Teensy is 12-bit in resolution, only values in the range [0, 4095] are valid. Also, you want the ramp to both increase and decrease, so you must fill the first half of the array with [0, 4094] and the second half with [4095, 1].

The 0 and the 4095 are filled only a single time since you don’t want them to appear twice when the ramp reach the ends.


# Step 3 - Setup DMA

Now it’s time to set up the actual DMA, and it can be done using registers. Nonetheless, since the Teensy framework provides a handy library to deal with it (called DMAChannel), I will use that instead.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <DMAChannel.h>

DMAChannel dma1(true);

void dma_setup() {
    dma1.disable();
    dma1.sourceBuffer(lut_ramp, LUT_SIZE * sizeof(uint16_t));
    dma1.transferSize(2);
    dma1.destination(*(volatile uint16_t *)&(DAC0_DAT0L));
    dma1.triggerAtHardwareEvent(DMAMUX_SOURCE_PDB);

    dma1.enable();
}

The name of the functions used are mostly self-explanatory, but here are some more details of the general working principle.

  1. First of all create a DMAChannel object, which represents the actual DMA you want to use.
  2. Disable the DMA.
  3. Define which is the array that is storing all the values that you want to write to the DAC.
  4. Define how many bytes each value has. Since we are using uint16_t, the corresponding number of bytes is 2.
  5. DMA can be used with almost all the modules of an MCU, and until now we have just defined a DMA channel. The destination function is telling to the Teensy which module should be attached to this DMA channel.
  6. In general, a DMA is triggered by “something”, which can be a hardware or software event. Since you want to generate a wavefunction, the DAC must be updated at a constant time interval. In our case is provided by the PDB (more in the next section).

If you want to learn more about the DMA (e.g. the module structure, the precise definition of some words like channel, source etc.), you can read chapters 23 and 24 of the development manual. You MUST also explore what the various functions of the framework are doing.

By doing so, you can actually dig deeper than what you can learn by just reading the manual. In fact, those two chapters are fundamental but somewhat less detailed than the others.


# Step 4 - Enable the PDB

Finally is time to set up the last required building block: the Programmable Delay Block (PDB). You can consider it as a precise timer that will trigger an event when it reaches a certain value.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#define RAMP_FREQ 2 // Hz
#define PDB_CONFIG                                                   \
    (PDB_SC_TRGSEL(15) | PDB_SC_PDBEN | PDB_SC_PDBIE | PDB_SC_CONT | \
     PDB_SC_DMAEN)

void trigger_clock_setup() {
    SIM_SCGC6 |= SIM_SCGC6_PDB;  // Enable PDB clock

    uint32_t mod = F_BUS / (RAMP_FREQ * LUT_SIZE);
    PDB0_MOD = (uint16_t)(mod - 1);      // Counter between PDB activations
    PDB0_IDLY = 0x0;                     // PDB interrupts delay
    PDB0_SC = PDB_CONFIG | PDB_SC_LDOK;  // Load configuration
    PDB0_SC = PDB_CONFIG | PDB_SC_SWTRIG;
    PDB0_CH0C1 = 0x0101;  // Enable pre-trigger
}

I will not explain in detail the effect of every register. I have placed some guide comments and you can read more about it in chapter 44 of the development manual. Nonetheless, I can tell you that the RAMP_FREQ value is the final frequency of the generated ramp waveform (and you can play with it).


# Step 5 - Make everything Work

The main.cpp file will look like that:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void setup() {
  // DAC
  dac_setup(0);

  // RAMP
  lut_setup();
  dma_setup();
  trigger_clock_setup();
}

void loop() {}

Congratulations, you just transformed your Teensy into a waveform generator!


# Conclusion

In this article, you configured a DMA channel of your Teensy to update the DAC0 output and generate a sawtooth waveform. I hope that you got a feel of how to read a development manual, a very useful thing if you want to dig deeper into embedded developments.

If you modify a little bit this program you can create a waveform generator. For example, you can implement the following:

Have fun!


# Other Resources

In the following you can find some resources that I found useful while researching this topic.

  1. Attaching an interrupt to two DMAs - PJRC Forum
  2. Using 2 DMA’s on two DAC on Teensy 3.6 - this was the most helpful in writing this program
  3. Some advice on how to start using DMA by PaulStoffregen