Demystify DAC Empty Event on SAM L21?

Go To Last Post
2 posts / 0 new
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I'm building a project using the DAC on the SAM L21.  I thought I could use the DAC's EMPTY0 event to feed new samples into DATABUF0, but it's not working the way I would expect.


The datasheet is light on how EMPTY0 works, but I've figured out the following through meticulous trial and error:


  1. The first write to DATA0 or DATABUF0 always triggers EMPTY0.  It triggers very quickly; and in the latter case, well before a START0 input event occurs (i.e. before a conversion is started).
  2. If you write to DATABUF0 and don't touch it again until after the next START0, behavior is as expected and the DAC output updates on the START0 event.  Writing to DATABUF0 after the START0 event occurs triggers another immediate EMPTY0.
  3. However, if you attempt to write to DATABUF0 a second time before the next START0 (as would happen if you're loading new data each EMPTY0), you get the following behavior:
    • SYNCBUSY.DATABUF0 stays high until the next START0
    • EMPTY0 doesn't fire until the next START0
    • DAC output (usually) doesn't get updated when the next START0 event fires (may have seen some exceptions to this in async mode but it didn't seem consistent)
    • If a second START0 event fires before DATABUF0 is written again, an event overrun occurs


In all, this seems to make EMPTY0 useless for triggering writes to DATABUF0.  It's firing too soon after the previous value is written.


I had expected the sequence to be more like this:


  • START0 moves value from DATABUF0 to DATA0 (starting a conversion)
  • DATABUF0 is now "empty", firing EMPTY0


In fairness, the datasheet says:

Data Buffer 0 Empty (EMPTY0): The request is set when data is transferred from DATABUF0 or DATA0 to the internal data buffer of DAC0.

I couldn't find any more information or a diagram referencing this "internal data buffer".


Eventually I want to use SleepWalking to feed the DAC directly from low power memory without CPU intervention.  But I'm trying to build this project up one piece at a time, and for now I just want to develop a thorough understanding of the behavior of the peripheral and how it interacts with events.  I'm triggering the events via the a TC4 generator (in some test cases) or via the CPU through a SWEVT (in recent, more methodical test cases).  I do note the datasheet says (under DMA section where the Empty events are discussed):

If the CPU accesses the registers which are source of DMA request set/clear condition, the DMA request can be lost or the DMA transfer can be corrupted, if enabled.

Do you think I would see different behavior than above if it were DMA requests instead of the timer or SWEVT directly triggering things?


Is there anyone out there who's had better luck with EMPTY0 and is willing to share some non-ASF sample code?


From what I can tell, it seems I'll need to trigger loads of new bytes from EOC0 instead of EMPTY0 (which is a bit annoying as it means I don't get as large a head-start before the next sample is due).

Last Edited: Tue. Feb 11, 2020 - 08:20 AM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

No responses on this issue yet, but I've been investigating some more.  Below is sample code and a logic probe trace to show what I've observed.


// PINS:
// PA27: main clock
// PA04: trace for Event 0 (triggered by software, fires DAC START0)
// PA20: trace for Event 1 (triggered by DAC EMPTY0)
// PA02: DAC0 output
// PB09: triggers logic probe capture on program start
// PB10: hardwired to LED on SAM L22 Xplained Pro board
// Event channel 0: Conversion event (triggers DAC START0)
// Event channel 1: DAC.EMPTY0
// GCC compiler optimization set to Debugging (-Og)

#include "sam.h"

// For directly referencing a memory address
#define ADDR8(x)            (*(volatile uint8_t  *)(x))
#define ADDR16(x)           (*(volatile uint16_t *)(x))
#define ADDR32(x)           (*(volatile uint32_t *)(x))

#define REG_PORTA_PMUX1     ADDR8(&REG_PORT_PMUX0 + 1)     // PMUX address for PA02 and PA03
#define REG_PORTA_PMUX13    ADDR8(&REG_PORT_PMUX0 + 13)    // PMUX address for PA26 and PA27
#define REG_PORTA_PINCFG02  ADDR8(&REG_PORT_PINCFG0 + 2)   // PINCFG address for PA02
#define REG_PORTA_PINCFG27  ADDR8(&REG_PORT_PINCFG0 + 27)  // PINCFG address for PA27

#define DAC_MIN             ((uint16_t)0)
#define DAC_MAX             ((uint16_t)0xFFF)

#define IGNORE_BUSY 1

void pulse_led(int count);
void stall(uint32_t loops);
void DAC_Handler(void);

void init(void) {

  // Set direction on output ports

  // Enable clock output for diagnostics
  while (GCLK->SYNCBUSY.bit.GENCTRL0);                    // Wait for sync

  // Wire clock signal from GCLK_IO[0] to PA27 by selecting MUX function H.
  // Note the PMUX0 register is for PA0 and PA1, PMUX1 is for PA2 and PA3, etc.
  // The lower nibble selects the function for the even pin, and upper for the odd pin.
  REG_PORTA_PMUX13 = PORT_PMUX_PMUXO(MUX_PA27H_GCLK_IO0); // Select MUX function H for PA27 (other bits remain 0)
  REG_PORTA_PINCFG27 = PORT_PINCFG_PMUXEN;                // Enable MUX for PA27, disable pullups and input

  // Enable clock GCLK0 for event channel 0; it's used to propogate events
  // No clock is enabled for channel 1, as that one operates in ASYNC mode
  while (!GCLK->PCHCTRL[EVSYS_GCLK_ID_0].bit.CHEN);       // Wait for sync

  // Wire DAC VOUT0 output to PA02 (MUX function B)
  REG_PORTA_PINCFG02 = PORT_PINCFG_PMUXEN;                // Enable MUX for PA02, disable pullups and input

  // Enable clock GCLK0 for DAC
  while (!GCLK->PCHCTRL[DAC_GCLK_ID].bit.CHEN);           // Wait for sync

  // Select PORT and DAC as event channel 0 users
  EVSYS->USER[EVSYS_ID_USER_DAC_START_0].reg = (0 + 1);   // DAC0 Start
  EVSYS->USER[EVSYS_ID_USER_PORT_EV_0].reg = (0 + 1);     // PORT

  // Select PORT as event channel 1 user (to monitor DAC EMPTY0 signal)
  EVSYS->USER[EVSYS_ID_USER_PORT_EV_1].reg = (1 + 1);     // Select user PORT EV1

  // Configure event channel 0.  For this example it's triggered by software.
  EVSYS->CHANNEL[0].reg =
      EVSYS_CHANNEL_ONDEMAND                              // Preferred over "always on"; see errata 14532
    | EVSYS_CHANNEL_EVGEN(0);                             // No generator

  // Configure PORTA to pipe Event 0 signal to PA04, and Event 1 signal to PA20 (for diagnostics purposes)
  // Note "0x31" typo on datasheet pg 475; the pin numbers should be decimal
  PORT->Group[0].EVCTRL.reg =
    // Event signal 0
      PORT_EVCTRL_PORTEI0                                 // Enable event input 0
    | PORT_EVCTRL_EVACT0(0)                               // 0 = follow event, 3 = toggle on event
    | PORT_EVCTRL_PID0(PIN_PA04)                          // Tie event 0 to PA04
    // Event signal 1
    | PORT_EVCTRL_PORTEI1                                 // Enable event input 1
    | PORT_EVCTRL_EVACT1(0)                               // 0 = follow
    | PORT_EVCTRL_PID1(PIN_PA20);                         // Tie to PA20

  // Configure DAC
  DAC->CTRLB.reg = DAC_CTRLB_REFSEL_VDDANA;               // Use analog input voltage as reference
  DAC->EVCTRL.reg =
      DAC_EVCTRL_STARTEI0                                 // Events trigger start of conversion on DAC0
    | DAC_EVCTRL_EMPTYEO0;                                // Output EMPTY event when DAC0 buffer empty
  DAC->DACCTRL[0].reg =
      DAC_DACCTRL_ENABLE                                  // Enable DAC0
    | DAC_DACCTRL_CCTRL_CC1M;                             // Select current level

  // Configure event channel 1
  EVSYS->CHANNEL[1].reg =

int main(void) {


  // 1. Enable DAC and wait for it to be ready.
  // OBSERVATION: Enabling the DAC makes its output LOW, and fires off an EMPTY event a few clock cycles later,
  // around the time the READY bit is set.
  pulse_led(1);                                           // PULSE LED ONCE to indicate we're about to enable the DAC
  DAC->CTRLA.reg = DAC_CTRLA_ENABLE;                      // Enable overall DAC module
  while (DAC->SYNCBUSY.bit.ENABLE);                       // Wait for sync after enabling DAC
  while (!DAC->STATUS.bit.READY0);                        // Await DAC0 startup (note earlier die revs should check...
                                                          // ...INTFLAG.EMPTY instead - see errata 14664 & 14678)
  pulse_led(2);                                           // PULSE LED TWICE to indicate DAC has completed startup
  stall(5);                                               // Wait for good measure, to alleviate any timing concerns

  // 2. Write first value to DATABUF0
  // OBSERVATION: Writing to DATABUF0 immediately triggers another EMPTY event
  pulse_led(1);                                           // PULSE LED ONCE; about to write to DATABUF0
  DAC->DATABUF[0].reg = DAC_MAX;
  while (DAC->SYNCBUSY.bit.DATABUF0);                     // Wait for sync
  stall(5);                                               // Wait for good measure, to alleviate any timing concerns

  // 3. Fire START0 event to begin a conversion
  while (!EVSYS->CHSTATUS.bit.USRRDY0);                   // Ensure users are ready, for good measure
  pulse_led(2);                                           // PULSE LED TWICE; about to fire event
  stall(5);                                               // Wait for good measure, to alleviate any timing concerns

  // 4. Write second value to DATABUF0
  pulse_led(1);                                           // PULSE LED ONCE; about to write to DATABUF0
  DAC->DATABUF[0].reg = DAC_MIN;
  while (!DAC->SYNCBUSY.bit.DATABUF0);                    // Wait for sync
  stall(5);                                               // Wait for good measure, to alleviate any timing concerns

  // 5. This time, write another value to DATABUF0 to show what would happen if writes were triggered by the EMPTY0 event
  pulse_led(1);                                           // PULSE LED ONCE; about to write to DATABUF0
  DAC->DATABUF[0].reg = DAC_MAX;
  while (DAC->SYNCBUSY.bit.DATABUF0);                     // Wait for sync - ***THIS LINE HANGS IF ENABLED***

  // OBSERVATION: Writing to DATABUF0 again before the next START0 event does not produce the EMPTY0 event, and hangs
  // the SYNCBUSY bit high.  If the above while() line is uncommented and an attempt made to fire the next event as
  // seem below, it does not fire and instead causes an event overflow.

  stall(10);                                              // To alleviate timing concerns, and separate this section on probe
  while (!EVSYS->CHSTATUS.bit.USRRDY0);                   // Interestingly, channel reports users as ready...
  //while (EVSYS->CHSTATUS.bit.CHBUSY0);                  // ...but if this line is uncommented it hangs; the channel is busy
  pulse_led(2);                                           // PULSE LED TWICE; about to fire event
  EVSYS->SWEVT.bit.CHANNEL0 = 1;                          // 6. Attempt to fire START0 event
  if (EVSYS->INTFLAG.bit.OVR0) asm("BKPT");               // The overflow isn't immediate; execution doesn't stop here
  if (EVSYS->INTFLAG.bit.OVR0) asm("BKPT");               // ***THIS LINE HALTS*** - event 0 has overflowed


void pulse_led(int count) {
  for (int i = 0; i < 2 * count; i++) {
    PORT->Group[1].OUTTGL.reg = PORT_PB10;

void stall(uint32_t loops) {
  for (volatile int i = 0; i < loops; i++);               // volatile to avoid optimizing out

The fact that EMPTY0 fires as soon as you write to DATABUF0 seems (to me) to make it wholly unsuitable as a trigger for reloading the data buffer.


I also went ahead and whipped up some code to check if the behavior of the peripheral is different when it's connected to the DMA controller (trigger = DAC.EMPTY0, target = DAC.DATABUF0) and the results weren't any better.  A similar DMA example worked fine when I triggered off something else and wrote to DATA0 instead.


All my tests so far are writing all 16 bits of DATABUF0 in one go.  It occurred to me EMPTY0 might be intended for use with DMA transfer beats of one byte each.  e.g. Write to the low byte of the DATABUF0 register first, then an EMPTY0 event requests the high byte.  I mocked up some code along those lines but still wasn't able to get it to work.


My problem in all cases is that (as far as I can tell) EMPTY0 fires before the DAC is actually ready to accept new data.


Hopefully someone can explain if I'm wrong (maybe even post a small, bare-metal example showing how EMPTY0 is intended to be used), or confirm this is a bug.

Last Edited: Wed. Feb 12, 2020 - 11:56 PM