DAC programming on Arduino Giga using DMA

Hi all. I have successfully run the "advanced DAC" examples on my recently purchased Arduino Giga R1 Wifi. That works pretty well, but I am quite disappointed that one cannot setup a waveform in a memory location from wich the DMA would feed the data into the DAC automatically, without the involvement of the processor. In the example sketches the MCU would have to call dac1.write(buf) forever and ever and ever...There surely must be a way to make use of the numerous internal DMA channels, I have seen some related HAL setup functions in the Giga libraries. Any advice would be highly appreciated.

1 Like

I have not found anything that answers this yet. I guess we will have to wait a little bit longer. It must be on arduino’s parent company plans. It is one of the main attributes of these MCUs.

Sorry, I have not done anything with DAC on the GIGA. And as such have not tried it using DMA

I have experimented with DMA transfers to SPI, that I did as part of my ILI9341 driver for GIGA...

I did a writeup about it in the thread:

Not sure if it will help or not, but I suspect that going to the DAC would be similar.

Hi all,

thanks for this. I have managed to do this using the STM32CubeIDE environment some time ago and I will post some code once I have time. I am not using the Arduino Giga anymore but I have the chip embedded instead. I think that using an Arduino platform would only make sense if its programming was easier, and if all functionality was available. That would be great because I find programming STM32 products really a nightmare.

I totally agree with you. If it is not easy to program, there is no point of programming STMs with the arduino IDE. Maybe an arduino is better suited for your needs.

I have an update after I have been in touch with one of the github contriubtors for the advanced analog library, iabdalkader (see issue Option for statically allocated DMA buffer(s) for waveform generation in circular mode · Issue #74 · arduino-libraries/Arduino_AdvancedAnalog · GitHub).

He suggested a solution that could be implemented using the existing API. The user needs to first fill at least three buffers, but only once. If these buffers are to be used in a circular fashion then there is no need to update the buffers thereafter. I am not completely satisified with that solution as polling is still needed, but it is an improvement as copying the same data again and again is no longer necessary.

Having said that, the number of samples per period could be greatly extended to reduce the number of times that dac1.avaliable() would need to be called. Further more, I guess that this would not be a problem if the DAC loop would run in its separate thread, but I do not have much experience with RTOS.

The example below is a slight modification from his square wave solution to outputting a sine wave instead, and this was successfully tested on the Giga.

// This example outputs an 16KHz sine wave on A12/DAC0.
#include <Arduino_AdvancedAnalog.h>

uint16_t lut[] = {
    0x0800,0x08c8,0x098f,0x0a52,0x0b0f,0x0bc5,0x0c71,0x0d12,0x0da7,0x0e2e,0x0ea6,0x0f0d,0x0f63,0x0fa7,0x0fd8,0x0ff5,
    0x0fff,0x0ff5,0x0fd8,0x0fa7,0x0f63,0x0f0d,0x0ea6,0x0e2e,0x0da7,0x0d12,0x0c71,0x0bc5,0x0b0f,0x0a52,0x098f,0x08c8,
    0x0800,0x0737,0x0670,0x05ad,0x04f0,0x043a,0x038e,0x02ed,0x0258,0x01d1,0x0159,0x00f2,0x009c,0x0058,0x0027,0x000a,
    0x0000,0x000a,0x0027,0x0058,0x009c,0x00f2,0x0159,0x01d1,0x0258,0x02ed,0x038e,0x043a,0x04f0,0x05ad,0x0670,0x0737
};

static size_t lut_size = sizeof(lut) / sizeof(lut[0]);

AdvancedDAC dac1(A12);

void setup() {
    Serial.begin(9600);
    while (!Serial) {
    }

    if (!dac1.begin(AN_RESOLUTION_12, 16000 * lut_size , lut_size, 3)) {
        Serial.println("Failed to start DAC1 !");
        while (1);
    }

    for (int b=0; b<3; b++) {
        // Get a free buffer for writing.
        SampleBuffer buf = dac1.dequeue();

        // Write data to buffers.

        for (size_t i=0; i<buf.size(); i++) {
            buf.data()[i] =  lut[i];
        }
        //for (int i=0; i<buf.size(); i++) {
         //   buf.data()[i] =  (i % 2 == 0) ? 0: 0xfff;
        //}
        
        dac1.write(buf);
    }
}

void loop() {
    if (dac1.available()) {
        dac1.write(dac1.dequeue());
    }
}
1 Like

Might be a bit of the topic here but I'll try anyway since I'm not getting any responses on the other thread I created.

All the examples I can find relies on polling the dac.available() function all the time to detect when a buffer is ready for new data. I'm trying to setup a DMA transfer complete interrupt so I can do other stuff in my code and only fill another dac buffer when a buffer is finished transfering. Can this be done with the AdvancedAnalog library?

What I want to do is the following:

  1. Set up the dac to output at 44.1kHz, no problem here.
  2. Fill two 64 sample dac buffers with values respresenting the first 128 samples of a sinewave.
  3. Send the first buffer to the dac
  4. When buffer 1 finishes transfering DMA should start sending the second buffer and trigger an interrupt that fills the first buffer with the next values for the sinewave.
  5. When buffer 2 finishes transfer the newly filled buffer 1 should start transfering again and again trigger the interrupt to now start filling buffer 2 with new values again.

Is this the right approach?

There seem to be half transfer complete and full transfer complete interrupt in the HAL file but I'm not sure how to utilize them my code. Can anyone give some guidence?

#include <AdvancedDAC.h>

#define SAMPLE_RATE 44100
#define BUFFER_SIZE 64
#define FREQUENCY 1000 // Frequency of the sine wave in Hz

AdvancedDAC dac0(A12);

// Sine wave generation state variables
static float phase = 0.0;
const float phase_increment = 2 * PI * FREQUENCY / SAMPLE_RATE;

// Interrupt callback functions
void HAL_DAC_ConvCpltCallbackCh1(DAC_HandleTypeDef *hdac);

// Declare the dac_descr_all as extern if it's available externally
extern dac_descr_t dac_descr_all[];

void setup() {
    Serial.begin(9600);

    while (!Serial) {
        // Wait for serial connection
    }

    if (!dac0.begin(AN_RESOLUTION_12, SAMPLE_RATE, BUFFER_SIZE, 64)) {
        Serial.println("Failed to start DAC0!");
        while (1);
    }

    // Enable the Transfer Complete interrupt
    __HAL_DMA_ENABLE_IT(&dac_descr_all[0].dma, DMA_IT_TC);
}

void dac_fill_buffer(AdvancedDAC &dac_out) {
    // Get a free buffer for writing
    SampleBuffer buf = dac_out.dequeue();
    
    // Calculate the sine wave values
    for (size_t i = 0; i < buf.size(); i++) {
        float value = 2047 + 2047 * sin(phase); // 12-bit DAC range: 0 to 4095
        buf.data()[i] = static_cast<uint16_t>(value);
        phase += phase_increment;

        // Wrap phase to avoid overflow
        if (phase >= 2 * PI) {
            phase -= 2 * PI;
        }
    }

    // Write the buffer to DAC
    dac_out.write(buf);
}

// DMA Transfer Complete Callback
extern "C" void HAL_DAC_ConvCpltCallbackCh1(DAC_HandleTypeDef *hdac) {
    dac_fill_buffer(dac0);
}


void loop() {
    // No need to poll here anymore
}

I don't get that to compile because some problem (I think) related to the scope of dac_descr_all[0].dma:

// Enable the Transfer Complete interrupt
__HAL_DMA_ENABLE_IT(&dac_descr_all[0].dma, DMA_IT_TC);

sorry for not getting back to you earlier. Note that if you only want to generate a single signal then you can simply do this by using the code below. Note that there is now a "true" parameter at the DAC.begin(...,true), which means that in this "loop mode" the DAC starts automatically after all the buffers are filled, and continuously cycle through over all buffers. So there is no deed for waiting for the DAC any longer. I have requested this feature and it was kindly implemented by the creators of the advanced analog library, see the GIT repository

I am just a user, so please ask them if you need more details on usage etc.

#include <Arduino_AdvancedAnalog.h>

AdvancedDAC dac1(A12);

void setup() {
    Serial.begin(9600);

    while (!Serial) {

    }

    // Start DAC in loop mode.
    if (!dac1.begin(AN_RESOLUTION_12, 16000, 32, 16, true)) {
        Serial.println("Failed to start DAC1 !");
        while (1);
    }

    // In loop mode, the DAC will start automatically after all
    // buffers are filled, and continuously cycle through over all buffers.
    uint16_t sample = 0;
    while (dac1.available()) {
        // Get a free buffer for writing.
        SampleBuffer buf = dac1.dequeue();

        // Write data to buffer.
        for (int i=0; i<buf.size(); i++) {
            buf.data()[i] = sample;
        }       
        
        // Write the buffer to DAC.
        dac1.write(buf);
        sample += 256;
    }
}


void loop() {
  // In loop mode, no other DAC functions need to be called.
}

Thanks!

But I need to update the buffers continuously so that’s why I need an interrupt everytime the a buffer finish transfers, so that my process audio function gets called and fill and send a new buffer.

The functionality seems to be there in the HAL and library but not sure how to use it to call my own function.

I am not sure if the higher level Arduino library is suitable for this, maybe you have to implement this yourself such as in

https://youtu.be/zA3PVZzaUiU?si=ITCp1gogr_S4imFx

I guess that one way to get this onto the Arduino Giga is to

a) open a STM32H747 project in cubeIDE
b) setup the ADC/DAC accordingly and generate the required code and behaviour as desired. I found it important to define both half- and full interrupts, even if only one of them is used. Both ISRs need to be defined, even if empty.
c) try the dac/adc setup code on the GIGA using the ArduinoIDE or VScode/PlatformIO. I believe that the Arduino needs to access all the lower level HAL functions, so they should be hopefully included.

But please ask someone who has more experience in bringing lower level HAL code onto the Arduino. I have decided to stick to cubeIDE with custom made PCBs as I am using the TochGFX framework for a larger display. But that is an entirely different story.

Alternatively you can either modify the Arduino libraries yourself or request specific features from the developers to which I have sent you a link.

1 Like