DAC and DMA in ArduinoIDE

Post here first, or if you can't find a relevant section!
Post Reply
neo_nmik
Posts: 20
Joined: Thu Feb 04, 2021 7:09 pm
Answers: 2

DAC and DMA in ArduinoIDE

Post by neo_nmik »

Hey guys,

The help I got last time has got me up and running!

I'm building a little synth on the Blue Pill/STM32F103 (HID2.2 boot loader) with using Arduino IDE 1.8.13, on MacOS Mojave (10.14.6) and the stm32duino (STM Cores 1.9.0).

It works pretty well, but I'd love to get the DMA working for outputting to my DAC (12-bit MCP4921)! I've had a read around at some older posts, but I can only really find one that refers to a DAC and DMA (linked below), as opposed to quite a few ADC and DMA. I think I understand that I need to maybe disable/alter the way the Arduino API uses the HAL functions, but I'm not sure how (or if I even need to for a DAC).

viewtopic.php?t=442

I've currently got an interrupt that fires off at 44.1kHz. This handles my oscillator calculations, and then sending my 12-bit sample to my DAC over SPI. I've used a couple of low-level commands to reduce the overhead of the SPI sending in the interrupt, and I'm pretty certain I need to doing my oscillator calculations outside of the interrupt, but, it works (even with up to 8 separate oscillators).

My main focus right now is to have the DMA handle the DAC/SPI interaction. I'm not sure this will really give me much more overhead, but it feels like the correct way to do this... I'm not really looking to maintain portability, as my aim is to just develop this as is. (I was super close on the Arduino, but it was just too tight on power).

If someone could talk me through/point me in the right direction, I'd appreciate it!

Cheers,

Nick

Code: Select all

/*
  MCU - STM32F103
  DAC - MCP4921

  Test script for DAC output (12bit and 44.1kHz).

  Plays a 440Hz tone 
  
*/

#include "waveforms.h"
#include <SPI.h>

// -------------------------------- Hardware Definitions -------------------------------- //

#define LED  PC13
#define slaveSelectPin PA4
// Other SPI 'default' SPI1 pins are :- MOSI1 = PA7 / SCLK1 = PA5

// ----------------------------- Software/Audio Definitions ----------------------------- //

#define sampleRate 44100 // 44.1kHz sampling rate

// final sample that goes tote DAC
uint16_t sample;

// the two bytes that go to the DAC over SPI
byte dacHigh;
byte dacLow;

// variables for oscillators
uint16_t phaseAcc = 0;  // large number holds phase
int waveIndex = 0;        // Index for wave lookup (the upper 8 bits of the accumulator)
uint16_t oscil = 8;        // oscillator output

// holds frequency value used for oscillator in phase steps
// this is an integer proportial to Hertz in the following way:
// frequency  = (FrequencyInHertz * 65536) / SampleRate, here sample rate is 44100
uint32_t frequency = 0;

// used for selecting the wavefrom from the wavetable.
int waveform = 0;



// Every 44.1kHz, calculate the oscillation frequency, map to the wavetable and output it via SPI.
void DACinterrupt()
{
  
  phaseAcc = phaseAcc + frequency;  // add in pith, the higher the number, the faster it rolls over, the more cycles per second
  waveIndex = phaseAcc >> 8;   // use top 8 bits as wavetable waveIndex
  oscil = waveTable[(waveform*256)+waveIndex];    // get sample from wave table

  sample = oscil << 8;   // sample format for DAC is 12 bit, left justified

  DACformat(sample);

  SPItransmit(dacHigh, dacLow);


}

void setup()
{

  // ----------------------------------------------------------------------------------- //
  // ------------------------------------ SPI setup ------------------------------------ //
  // ----------------------------------------------------------------------------------- //
  
  // set the slaveSelectPin as an output:
  pinMode(slaveSelectPin, OUTPUT);
  // initialize SPI:
  SPI.begin();

  Serial.begin(9600);
  Serial.println(F("~welcome to the world of tomorrow~"));
  
  // ----------------------------------------------------------------------------------- //
  // ------------------------- DAC sample rate interrupt setup ------------------------- //
  // ----------------------------------------------------------------------------------- //
  
  HardwareTimer *DACtimer = new HardwareTimer(TIM1);
  DACtimer->pause();
  DACtimer->setOverflow(sampleRate, HERTZ_FORMAT); // 44.1kHz
  DACtimer->attachInterrupt(std::bind(DACinterrupt)); // bind argument to callback: When Update_IT_callback is called MyData will be given as argument
  DACtimer->resume();

}


void loop()
{  
  // play a test tone at 440hz 
  testTone(440.0);

}

void testTone (int Freq) {
  float frequencyInHertz = Freq;   // use this to hold a frequency in hertz
                                    // normally we wouldn't use floats, and just use integers,
                                    // but it is convinient here to show freqency calculation
  
  // using formula below, calculate correct oscillator frequency value
  frequency =  (frequencyInHertz * 65536.0) / sampleRate;  //here sampleRate is 44100 (or 44.1kHz)
  // wait 2 seconds
  delay(2000);
}

void DACformat (uint16_t sample) {
  // format/split sample for SPI port (4 config bits, followed by 12 audio bits)
  dacHigh = sample >> 8; 
  dacHigh >>= 4;
  dacHigh |= 0x30;
  dacLow = sample >> 4;
}

void SPItransmit (int a, int b) {
  
  // takes the Slave Select (SS or CS) pin low to select the DAC
  digitalWrite(slaveSelectPin, LOW);
  
  //  send in the address and value via SPI:
  SPI1->DR = (a);
  while (!(SPI1->SR & SPI_SR_TXE)) {}; // Checks and waits if SPI is busy
  
  SPI1->DR = (b);
  while ( SPI1->SR & SPI_SR_BSY ) {}; // Checks and waits till SPI is done
  
  // take the SS pin high to de-select the DAC:
  digitalWrite(slaveSelectPin, HIGH);
}
User avatar
fpiSTM
Posts: 1738
Joined: Wed Dec 11, 2019 7:11 pm
Answers: 91
Location: Le Mans
Contact:

Re: DAC and DMA in ArduinoIDE

Post by fpiSTM »

You should check an example of Cube example conversion: viewtopic.php?f=41&t=110
And try to converts this one to tests: https://github.com/STMicroelectronics/S ... lex_ComDMA
User avatar
Bakisha
Posts: 139
Joined: Fri Dec 20, 2019 6:50 pm
Answers: 5
Contact:

Re: DAC and DMA in ArduinoIDE

Post by Bakisha »

I think i understand what you trying to achieve. I also once tried to calculate samples in main loop and use interrupt for DMA to send samples (in my case driving PWM pin), but failed i to finish that idea.
I can copy-paste some code from another project, that is left on my PC, from testing with SPI DMA (using hardware CS pin):

Code: Select all

#include <SPI.h>

const uint16_t bufferLenght = 40;  // 40 * 8 = 320pixels
uint8_t *buffer_ODD = NULL;
uint8_t *buffer_EVEN = NULL;


void setup() {

  buffer_ODD = (uint8_t*) calloc(bufferLenght, sizeof(uint8_t));
  buffer_EVEN = (uint8_t*) calloc(bufferLenght, sizeof(uint8_t));


  for (int i = 0; i < bufferLenght; i = i + 2) {

    buffer_ODD[i] = uint8_t (0x81);
    buffer_ODD[i + 1] = uint8_t (0x7e);

    buffer_EVEN[i] = uint8_t (0x55);
    buffer_EVEN[i + 1] = uint8_t (0xaa);
  }

  pinMode(PB12, OUTPUT);

  RCC->AHBENR = ( RCC->AHBENR ) | (RCC_AHBENR_DMA1EN); // enable DMA1 clock


  // DMA configuration (DMA1, channel 3).
  // CCR register:
  // - Memory-to-peripheral
  // - Circular mode disabled.
  // - Increment memory ptr, don't increment periph ptr.
  // - 8-bit data size for both source and destination.
  // - High priority .


  //  // Set bits to 0:
  //  DMA1_Channel3->CCR &= ~(                               /* this set all bits to 0. delete unnecessary ones */
  //                          DMA_CCR_MEM2MEM              | /* MEM2MEM: Memory to memory mode; This bit is set and cleared by software. 0: Memory to memory mode disabled. 1: Memory to memory mode enabled.*/
  //                          ( 0x3 << DMA_CCR_PL_Pos )    | /* PL[1:0]: Channel priority level. These bits are set and cleared by software. 00: Low. 01: Medium. 10: High. 11: Very high"  */
  //                          ( 0x3 << DMA_CCR_MSIZE_Pos ) | /* MSIZE[1:0]: Memory size. These bits are set and cleared by software. 00: 8-bits. 01: 16-bits. 10: 32-bits. 11: Reserved */
  //                          ( 0x3 << DMA_CCR_PSIZE_Pos ) | /* PSIZE[1:0]: Peripheral size. These bits are set and cleared by software. 00: 8-bits. 01: 16-bits. 10: 32-bits. 11: Reserved"  */
  //                          DMA_CCR_MINC                 | /* MINC: Memory increment mode. This bit is set and cleared by software. 0: Memory increment mode disabled. 1: Memory increment mode enabled. */
  //                          DMA_CCR_PINC                 | /* PINC: Peripheral increment mode. This bit is set and cleared by software. 0: Peripheral increment mode disabled 1: Peripheral increment mode enabled */
  //                          DMA_CCR_CIRC                 | /* CIRC: Circular mode. This bit is set and cleared by software. 0: Circular mode disabled. 1: Circular mode enabled */
  //                          DMA_CCR_DIR                  | /* DIR: Data transfer direction. This bit is set and cleared by software. 0: Read from peripheral. 1: Read from memory */
  //                          DMA_CCR_TEIE                 | /* TEIE: Transfer error interrupt enable. This bit is set and cleared by software. 0: TE interrupt disabled. 1: TE interrupt enabled */
  //                          DMA_CCR_HTIE                 | /* HTIE: Half transfer interrupt enable. This bit is set and cleared by software. 0: HT interrupt disabled. 1: HT interrupt enabled */
  //                          DMA_CCR_TCIE                 | /* TCIE: Transfer complete interrupt enable. This bit is set and cleared by software. 0: TC interrupt disabled. 1: TC interrupt enabled */
  //                          DMA_CCR_EN                     /* EN: Channel enable. This bit is set and cleared by software. 0: Channel disabled. 1: Channel enabled */
  //
  //                        );
  //
  //
  //  // Set bits to 1:
  //  DMA1_Channel3->CCR |= (                               /* this set all bits to 1. change or delete unnecessary ones */
  //                          DMA_CCR_MEM2MEM              | /* MEM2MEM: Memory to memory mode; This bit is set and cleared by software. 0: Memory to memory mode disabled. 1: Memory to memory mode enabled.*/
  //                          ( 0x3 << DMA_CCR_PL_Pos )    | /* PL[1:0]: Channel priority level. These bits are set and cleared by software. 00: Low. 01: Medium. 10: High. 11: Very high"  */
  //                          ( 0x3 << DMA_CCR_MSIZE_Pos ) | /* MSIZE[1:0]: Memory size. These bits are set and cleared by software. 00: 8-bits. 01: 16-bits. 10: 32-bits. 11: Reserved */
  //                          ( 0x3 << DMA_CCR_PSIZE_Pos ) | /* PSIZE[1:0]: Peripheral size. These bits are set and cleared by software. 00: 8-bits. 01: 16-bits. 10: 32-bits. 11: Reserved"  */
  //                          DMA_CCR_MINC                 | /* MINC: Memory increment mode. This bit is set and cleared by software. 0: Memory increment mode disabled. 1: Memory increment mode enabled. */
  //                          DMA_CCR_PINC                 | /* PINC: Peripheral increment mode. This bit is set and cleared by software. 0: Peripheral increment mode disabled 1: Peripheral increment mode enabled */
  //                          DMA_CCR_CIRC                 | /* CIRC: Circular mode. This bit is set and cleared by software. 0: Circular mode disabled. 1: Circular mode enabled */
  //                          DMA_CCR_DIR                  | /* DIR: Data transfer direction. This bit is set and cleared by software. 0: Read from peripheral. 1: Read from memory */
  //                          DMA_CCR_TEIE                 | /* TEIE: Transfer error interrupt enable. This bit is set and cleared by software. 0: TE interrupt disabled. 1: TE interrupt enabled */
  //                          DMA_CCR_HTIE                 | /* HTIE: Half transfer interrupt enable. This bit is set and cleared by software. 0: HT interrupt disabled. 1: HT interrupt enabled */
  //                          DMA_CCR_TCIE                 | /* TCIE: Transfer complete interrupt enable. This bit is set and cleared by software. 0: TC interrupt disabled. 1: TC interrupt enabled */
  //                          DMA_CCR_EN                     /* EN: Channel enable. This bit is set and cleared by software. 0: Channel disabled. 1: Channel enabled */
  //
  //                        );

  DMA1_Channel3->CCR = uint32_t ( 0x0 ); //reset all bits
  // Set bits to 1:
  DMA1_Channel3->CCR |= (                                /* set only needed bits */
                          ( 0x3 << DMA_CCR_PL_Pos )    | /* PL[1:0]: Channel priority level. These bits are set and cleared by software. 00: Low. 01: Medium. 10: High. 11: Very high"  */
                          DMA_CCR_MINC                 | /* MINC: Memory increment mode. This bit is set and cleared by software. 0: Memory increment mode disabled. 1: Memory increment mode enabled. */
                          DMA_CCR_DIR                    /* DIR: Data transfer direction. This bit is set and cleared by software. 0: Read from peripheral. 1: Read from memory */

                        );
  //  // Set DMA source and destination addresses.
  DMA1_Channel3->CMAR  = (uint32_t)   buffer_ODD;      // set address of source memory buffer
  //
  DMA1_Channel3->CPAR  = (uint32_t)  & (SPI1->DR);     // set destination register
  //
  DMA1_Channel3->CNDTR = uint16_t ( bufferLenght );    // Set DMA data transfer length (16bit unsigned: 0-65535). // Note: Must be set again once all bytes are sended via DMA.

  // DMA1_Channel3->CCR &= ~(DMA_CCR_EN );  // Disable DMA1 Channel 3. // Note: Changing CMAR, CPAR and CNDTR is possible only when DMA_CCR_EN bit is 0
  // DMA1_Channel3->CCR |= ( DMA_CCR_EN );  // Enable  DMA1 Channel 3. // Note: the transfer will start  as soon as DMA_CCR_EN bit is set





  // Setup SPI
  SPI.begin();// hardware CS 
  digitalWrite(PB12, LOW);
  SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));

  digitalWrite(PB12, HIGH);

  SPI1->CR2 = (SPI1->CR2) | (SPI_CR2_TXDMAEN) ;     // enable SPI1 DMA request on TX=1 (Note: at this pont TX is already 1 )

}

void loop() {
  // put your main code here, to run repeatedly:
  delay(100);
  digitalWrite(PB12, LOW);





  DMA1_Channel3->CCR &= ~( DMA_CCR_EN );  // Must be disabled to be able to change buffer size and pointer to source

 // DMA1_Channel3->CMAR  = (uint32_t)   buffer_ODD;      // set address of source memory buffer

  DMA1_Channel3->CNDTR = uint16_t( bufferLenght ); // Must set lenght of a buffer

  DMA1_Channel3->CCR |= ( DMA_CCR_EN ); // Start DMA transfer

//  for (int i = 0; i < bufferLenght; i = i + 2) {  // timing check
//
//
//    buffer_EVEN[i] = uint8_t (0x55);
//    buffer_EVEN[i + 1] = uint8_t (0xaa);
//  }




  digitalWrite(PB12, HIGH);
//
//  delay(100);
//  digitalWrite(PB12, LOW);
//
//
//
//
//
//  DMA1_Channel3->CCR &= ~( DMA_CCR_EN );  // Must be disabled to be able to change buffer size and pointer to source
//
//  DMA1_Channel3->CMAR  = (uint32_t)   buffer_EVEN;      // set address of source memory buffer
//
//  DMA1_Channel3->CNDTR = bufferLenght; // Must set lenght of a buffer
//
//  DMA1_Channel3->CCR |= ( DMA_CCR_EN ); // Start DMA transfer
//
//  for (int i = 0; i < bufferLenght; i = i + 2) { // timing check
//
//    buffer_ODD[i] = uint8_t (0x81);
//    buffer_ODD[i + 1] = uint8_t (0x7e);
//  }
//
//
//  digitalWrite(PB12, HIGH);



}
I'll attach a excel file that i make to help me understand SPI DMA with CMSIS.

I don't know how many samples you are trying to pre-calculate, just make sure that you can calculate samples faster then sending it at 44.1KHz rate.
Attachments
DMA1_Channel3.zip
(12.59 KiB) Downloaded 213 times
stevestrong
Posts: 502
Joined: Fri Dec 27, 2019 4:53 pm
Answers: 8
Location: Munich, Germany
Contact:

Re: DAC and DMA in ArduinoIDE

Post by stevestrong »

This is my project for DAC and DMA for STM32F4 generic, but this uses my libmaple core.
Attachments
STM32F4_DAC_DMA.zip
(2.03 KiB) Downloaded 316 times
Post Reply

Return to “General discussion”