M0AGX / LB9MG

Amateur radio and embedded systems

Triggering DMA from timers (or GPIO) on the SAM E70

The SAM E70 is still a powerful MCU despite being released around 2015. It has a 300 MHz Cortex-M7, 150 MHz bus system and high-speed USB. High-speed USB (not the same thing as USB 2.0) is pretty rare in Cortex-M devices. It is usually the domain of much more powerful Cortex-A chips. When developing a data acquisition system on the SAM E70 I was surprised that there is no obvious way to trigger a DMA transfer from a timer!

Triggering DMA from a timer is a must if you have to do repetitive tasks with minimal jitter. For example triggering an external sensor or reading from an ADC. Of course you could do that in a timer interrupt handler. However, that would put load on the CPU which is undesirable when done at hundreds of kHz. Even with the interrupt handler running at the highest priority (and nesting in other handlers) there would be some jitter depending on the bus state, cache state, interrupt tail chaining etc.

Modern MCUs like the EFM32 (or even XMEGA) have a facility to connect various peripheral events to each other. Timers and DMA are an obvious combination. However, the SAM E70 lacks such features. The only DMA trigger signal I could find in the datasheet that had anything to do with timers is timer capture. Timer capture is only available on some GPIOs and needs an external signal so how can you use it to generate periodic DMA transfers? By outputting PWM on one pin and looping it back to the capture pin!

Overview

SAM E70 timers have multiple channels so one timer is enough. One timer channel is configured to output a PWM signal at the desired frequency. Second timer channel is configured in input capture mode. Pins PA0 and PA15 are connected on the PCB. The DMA reacts to the capture event. Easy. 🙂

overview

In case the transfer should be started by an independent external signal (like an IRQ from a sensor) only the timer capture channel is needed.

Let's dive into the details...

Timer configuration

Timers in the SAM E70 are mostly the same. All channels are similar and behave pretty independently. Any combination of timers and channels should be okay. Of course the appropriate TIOxx timer signals must be available on outside pins.

The first step is to enable clocks for the the timer channels:

1
2
_pmc_enable_periph_clock(ID_TC0_CHANNEL0);
_pmc_enable_periph_clock(ID_TC0_CHANNEL1);

Pin muxing

Timer TIOxx pins have to be connected to the pins. In my case they were available on PA0 and PA15 but any other option could also work. If you are using libraries from Atmel START the code looks like:

1
2
3
4
gpio_set_pin_direction(PIN_PA15, GPIO_DIRECTION_OUT);
gpio_set_pin_direction(PIN_PA0, GPIO_DIRECTION_IN);
gpio_set_pin_function(PIN_PA15, MUX_PA15B_TC0_TIOA1);
gpio_set_pin_function(PIN_PA0, MUX_PA0B_TC0_TIOA0);

PWM channel

This is the easiest part. Five lines of code and there should be a square wave on the output pin. I picked MCK (150 MHz bus clock) divided by 32 as the timer clock. Any other source can also be used. tick_rate_Hz is of course the PWM frequency in Hz.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
TC0->TcChannel[1].TC_CCR = TC_CCR_CLKDIS; // Stop timer channel to have a clean state

// Output is TIOA1 (pin PA15)
TC0->TcChannel[1].TC_CMR =
    TC_CMR_TCCLKS_TIMER_CLOCK3 |   // Select MCK/32 as the timer clock
    TC_CMR_WAVE |                  // Mode is waveform
    TC_CMR_WAVEFORM_WAVSEL_UP_RC | // Count up to TC_RC.
    TC_CMR_WAVEFORM_ACPC_SET |     // Set the pin on compare to TC_RC
    TC_CMR_WAVEFORM_ACPA_CLEAR;    // Clear the pin on compare to TC_RA

uint32_t timebase_register = CONF_MCK_FREQUENCY / 32 / tick_rate_Hz;
uint32_t duty_register = timebase_register / 2; // 50% duty cycle

TC0->TcChannel[1].TC_RC = timebase_register;
TC0->TcChannel[1].TC_RA = duty_register; // Has to be written after WAVE in CMR is set

TC0->TcChannel[1].TC_CCR = TC_CCR_CLKEN | TC_CCR_SWTRG; // Start the channel

Capture channel

Capture channel configuration is also easy. Timer channel clock can be anything but if it is slower than the PWM channel there will be a slight delay in the input capture. There should be no extra jitter as both channels operate from the same synchronous MCK clock. If you use totally different clock sources for the two channels there will be some jitter due to synchronization.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
TC0->TcChannel[0].TC_CCR = TC_CCR_CLKDIS; // Stop timer channel to have a clean state

TC0->TcChannel[0].TC_CMR =
    TC_CMR_TCCLKS_TIMER_CLOCK2 |  // Select MCK/8 as the timer clock
    TC_CMR_CAPTURE_LDRA_FALLING | // Capture on every falling edge of TIOA0 (pin PA0)
    TC_CMR_CAPTURE_CPCTRG;        // RC compare trigger in capture mode

// This is important. Somehow the effective DMA signal is like
// RC trigger AND TIOA trigger so let's arm the timer on every clock cycle.
TC0->TcChannel[0].TC_RC = 1;

TC0->TcChannel[0].TC_CCR = TC_CCR_CLKEN | TC_CCR_SWTRG;  // Start the channel

DMA configuration

SAM E70 has a single DMA controller called XDMAC with multiple channels. Every channel can do a different operation but because there is only one controller all operations will be done sequentially anyway. There are various schemes to switch between active channels like round robin or fixed priorities. Every channel can also generate an interrupt when the operation is done.

DMA descriptors

DMA controllers come in all shapes and sizes but in every case the configuration must contain at least the same basic elements:

  • source address
  • destination address
  • size of burst (byte/word/half word)
  • number of transfers
  • trigger signal and flow control by a peripheral

Controllers in smaller chips only use registers to configure the transfers. The ones in larger chips use descriptors to specify the operations. In the SAM E70 descriptors are fetched from regular RAM. From the C point of view they can be treated just like regular structs with a special format that is understood by the DMA hardware. There are several kinds of descriptors supported by this DMA controller. I found type 2 the most useful.

I could not find any predefined descriptor structs in Atmel's headers so I made my own type 2 descriptor struct that looks like this:

1
2
3
4
5
6
7
typedef struct {
    uint32_t NDA; // Next descriptor address
    uint32_t UBC; // Transfer length + next descriptor enables
    uint32_t SA;  // Source address
    uint32_t DA;  // Destination address
    uint32_t CFG; // Value that gets copied to XDMAC_CCx
} dma_view2_desc_t;

A killer feature is the ability to link descriptors together to execute a sequence of independent transfers. Descriptor can hold the address of the next descriptor to start (the NDA field).

It is important to discern between:

  • DMA channel
  • DMA descriptor
  • DMA trigger signal

Example

Transfer a chunk of 16 bytes from RAM to the UART at 100 Hz.

There are two independent operations here so there must be at least two descriptors.

  • Operation 1 is the (abstract) timer running at 100 Hz.
  • Operation 2 is the transfer of 16 bytes from RAM to the UART but only after the timer delivers an event.

Because these two operations must happen in sequence they have to use the same channel. The first operation waits for the timer trigger signal. The second operation waits for UART TX data register empty trigger signal. There are two operations that use different source/destination addresses and different trigger signals so there have to be two linked descriptors. If the operation is supposed to run continuously the last descriptor must link back to the first descriptor.

overview

Timer capture descriptor

I discovered that the timer in capture mode can generate continuous DMA trigger signals only when the captured value is actually read from the timer. It is not as simple as just using the XDMAC_CC_PERID_TC0 signal for any DMA channel. The DMA needs to do a dummy read from the timer. This means that there is a need for an extra descriptor that will only do a read from the timer to any location in RAM.

This is the descriptor that reads timer capture value. Again, I was not able to find any #defines for UBC register so I had to write raw bits based on the datasheet.

 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
static uint32_t capture_register_sinkhole = 0; // This variable will hold the (useless) timer capture value

static dma_view2_desc_t dma_timer_capture_desc; // forward declaration

static dma_view2_desc_t dma_timer_capture_desc = {
    .NDA = (uint32_t)&dma_timer_capture_desc, // Next linked descriptor (this very descriptor)
    .UBC = XDMAC_CUBC_UBLEN(1) // Length in words (1)
           (2 << 27) |         // NVIEW = 2, next descriptor type is view 2
           (0 << 26) |         // NDEN  = 0, do not update destination from next descriptor
           (0 << 25) |         // NSEN  = 0, do not update source from next descriptor
           (1 << 24),          // NDE   = 1, next descriptor is enabled
    .SA = (uint32_t) &(TC0->TcChannel[0].TC_RAB), // Source: capture register to be read
    .DA = (uint32_t) &capture_register_sinkhole,            // Destination: dummy variable
    .CFG =
        XDMAC_CC_PERID_TC0     | // Trigger signal: timer 0 capture
        XDMAC_CC_DAM_FIXED_AM  | // Fixed destination
        XDMAC_CC_SAM_FIXED_AM  | // Fixed source
        XDMAC_CC_DIF_AHB_IF1   | // Use AHB port 1 for destination
        XDMAC_CC_SIF_AHB_IF1   | // Use AHB port 1 for source
        XDMAC_CC_DWIDTH_WORD   | // Data size: 32-bit word
        XDMAC_CC_CSIZE_CHK_1   | // Chunk size: 1 data size
        XDMAC_CC_DSYNC_PER2MEM | // Direction: peripheral to memory
        XDMAC_CC_MBSIZE_SINGLE | // Burst: 1 data
        XDMAC_CC_TYPE_PER_TRAN,  // Transfer synchronized with peripheral
};

Even though the descriptor does not change it can not have the const attribute because it must be located in RAM for fast access. Accessing it from flash would be much slower.

Another important thing in the SAM E70 DMA is the AHB port number. There are two bus ports and they have to be carefully selected depending on source/destination.

This is the exact table from the datasheet. You can see that some combinations can work with any port.

SAM E70 bus matrix

Starting the transfer

I declared a convenient constant that is used whenever the first descriptor is configured in the controller. In a heavily loaded system (or when power consumption is critical) it may be beneficial to skip some operations. For example: skip reloading the source address when reading data from a peripheral to two independent buffers in RAM. In my case I want to reload all settings when changing descriptors.

1
2
3
4
5
static const uint32_t NEXT_DESC_SETTINGS =
    XDMAC_CNDC_NDVIEW_NDV2              | // next descriptor type is view 2
    XDMAC_CNDC_NDDUP_DST_PARAMS_UPDATED | // update destination using next descriptor
    XDMAC_CNDC_NDSUP_SRC_PARAMS_UPDATED | // update source using next descriptor
    XDMAC_CNDC_NDE;                       // enable next descriptor

Starting is as simple as telling the controller where the descriptor is, its characteristics, and setting the channel enable bit.

1
2
3
4
5
6
// Load the DMA descriptor to channel 0
XDMAC->XdmacChid[0/*DMA channel number*/].XDMAC_CNDA = (uint32_t)&dma_timer_capture_desc;
XDMAC->XdmacChid[0/*DMA channel number*/].XDMAC_CNDC = NEXT_DESC_SETTINGS;

// Enable the channel, linked descriptor will be loaded immediately
XDMAC->XDMAC_GE = (1 << 0/*DMA channel number*/);

Adding a second descriptor

To get anything useful done you can create your own descriptor (for example doing a UART transfer or another operation) and link it to dma_timer_capture_desc. For continuous operation the NDA field in dma_timer_capture_desc should point to the second descriptor and the NDA field of the second descriptor should point back to dma_timer_capture_desc. 🙂

Diagrams were made with Ditaa.