cancel
Showing results for 
Search instead for 
Did you mean: 

STM32F4: Continuous DMA won't iterate, stuck on first SPI command

fufu930
Associate II

I'm working on a DMA routine for my SPI DAC, and I've loaded up a buffer with the commands to make a sine wave. But when I check it with the oscilloscope, it's just stuck at 2.05V, which is actually the very first command in my sequence.

I've spent hours on it, trying everything from physically wiring LDAC low to fiddling with different write commands, and still no luck.

I'm really hoping this is something super simple I'm missing, because I'm still pretty new to embedded and C.

// AD5668_DMA.cpp
#include "AD5668_DMA.h"
#include "stm32f4xx.h"

// AD5668 “write input & update N” command base
static constexpr uint8_t CMD_WU = 0x30;

AD5668_DMA::AD5668_DMA(uint8_t csPin, uint8_t dacChan, bool circular)
  : _csPin(csPin), _dacChan(dacChan & 0x0F),
    _samples(0), _bytes(0), _buf(nullptr), _circ(circular)
{
  pinMode(_csPin, OUTPUT);
  digitalWrite(_csPin, HIGH);
}

void AD5668_DMA::begin(const uint16_t* wavetable, uint16_t size) {
  _samples = size;
  _bytes   = size * 4;            // 4 bytes per sample
  delete[] _buf;
  _buf = new uint8_t[_bytes];
  packFrames(wavetable);

  // 1) Enable DMA2 clock
  RCC->AHB1ENR |= RCC_AHB1ENR_DMA2EN;

  // 2) Disable stream for config
  DMA2_Stream3->CR &= ~DMA_SxCR_EN;
  while (DMA2_Stream3->CR & DMA_SxCR_EN);

  // 3) Point to SPI1->DR & our buffer
  DMA2_Stream3->PAR  = (uint32_t)&SPI1->DR;
  DMA2_Stream3->M0AR = (uint32_t)_buf;
  DMA2_Stream3->NDTR = _bytes;

  // 4) Build CR for 8‑bit, mem→periph, incr, high‑prio, optional circ
  uint32_t cr = 
      (3 << DMA_SxCR_CHSEL_Pos) |  // Channel 3
      DMA_SxCR_DIR_0       |       // Mem→Periph
      DMA_SxCR_MINC        |       // Mem inc
      /* MSIZE=0, PSIZE=0 ⇒ 8‑bit */ 
      DMA_SxCR_PL_1;               // High prio

  if (_circ) cr |= DMA_SxCR_CIRC;

  DMA2_Stream3->CR = cr;

  // 5) Enable TX‑DMA on SPI1
  SPI1->CR2 |= SPI_CR2_TXDMAEN;
}

void AD5668_DMA::start() {
  if (!_buf) return;
  digitalWrite(_csPin, LOW);
  delayMicroseconds(1);
  DMA2_Stream3->CR |= DMA_SxCR_EN;
}

void AD5668_DMA::stop() {
  DMA2_Stream3->CR &= ~DMA_SxCR_EN;
  while (DMA2_Stream3->CR & DMA_SxCR_EN);
  digitalWrite(_csPin, HIGH);
}

void AD5668_DMA::packFrames(const uint16_t* wavetable) {
  for (uint16_t i = 0; i < _samples; i++) {
    uint16_t v = wavetable[i];
    uint8_t b1 = CMD_WU | _dacChan;
    uint8_t b2 = (_dacChan << 4) | (v >> 12);
    uint8_t b3 = (v >> 4) & 0xFF;
    uint8_t b4 = ((v << 4) & 0xF0) | 0x0F;

    uint32_t idx = i * 4;
    _buf[idx + 0] = b1;
    _buf[idx + 1] = b2;
    _buf[idx + 2] = b3;
    _buf[idx + 3] = b4;
  }
}

 

// Command definition list
#define WRITE_INPUT_REGISTER 0
#define UPDATE_OUTPUT_REGISTER 1
#define WRITE_INPUT_REGISTER_UPDATE_ALL 2
#define WRITE_INPUT_REGISTER_UPDATE_N 3
#define POWER_DOWN_UP_DAC 4
#define LOAD_CLEAR_CODE_REGISTER 5
#define LOAD_LDAC_REGISTER 6
#define RESET_POWER_ON 7
#define SETUP_INTERNAL_REF 8

 

// main.ino
#include <Arduino.h>
#include <SPI.h>
#include "AD5668_DMA.h"
#include "AD5668.h"

#define CS_PIN        PB0
#define CLR_PIN       PB1
#define LDAC_PIN      PB10
#define WAVETABLE_SIZE 256

// circular=true for continuous sine
AD5668_DMA dmaDac(CS_PIN, /*chan=*/0, /*circ=*/true);
AD5668     dac(CS_PIN, CLR_PIN, LDAC_PIN);

uint16_t waveform[WAVETABLE_SIZE];

void generateSine() {
  for (int i = 0; i < WAVETABLE_SIZE; i++) {
    float a = 2*PI * i / WAVETABLE_SIZE;
    waveform[i] = (uint16_t)((sin(a) + 1.0) * 32767.5);
  }
}

void setup() {
  // 1) SPI (8‑bit default)
  SPI.begin();
  SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE1));

  // 2) AD5668 init
  dac.init();
  dac.enableInternalRef();
  dac.powerDAC_Normal(B11111111);

  // 3) Build LUT & DMA
  generateSine();
  dmaDac.begin(waveform, WAVETABLE_SIZE);

  // 4) Start continuous stream
  dmaDac.start();
}

void loop() {
  // nothing — hardware is running
}
4 REPLIES 4
TDK
Super User

The STM32F4 SPI can't drive the CS pin high and low between bytes. Even if it could, you're using 8-bit words and the chip needs 16-bit words. Use a logic analyzer to see what's happening on the lines.

If you feel a post has answered your question, please click "Accept as Solution".

Ouch, I'm not even flipping CS (SYNC), just only once before the first message. My SPI transfers are 8-bit long and the whole message is 32-bit, so that part seems alright. I can find libraries for that chip and they do in fact set CS HIGH after the 4 x 8-bit SPI transfers.
As per datasheet:5668-sync.png
So now I must find a way to control CS based on DMA transfer count with handlers and move everything to a 48 kHz timer to send one command per tick.

Edit: And also reduce the buffer size to the message size (32 bits) and on each TC interrupt set CS high and set the next consecutive M0AR address?

Yep, you understand what needs to happen.

Set CS high when the SPI is done, not when TC is raised. TC only indicates the DMA has transferred all data. You can take a look at HAL_SPI_TransmitReceive_DMA fow how this is done correctly.

 

Newer chip families handle this better. On F4, options are fewer. Synchronizing a PWM signal to act as CS can work if done correctly.

If you feel a post has answered your question, please click "Accept as Solution".

I get it, thanks TDK, but what I don't yet understand is how do I tell I'm on the 4th or the 8th or the 12th and so on DMA transfer in order to trigger CS.

That's why I was thinking to reduce DMA buffer size to just 4 transfers (one 32-bit command) and utilize TC to trigger CS. (And also change M0AR dynamically at TC.)

- - -

Update: Isn't HAL_SPI_Transmit_DMA just an abstraction of what I already work with? Isn't HAL_SPI_TxCpltCallback essentially TC, so I still won't have a callback that tells me when DMA had sent N amount of transfers that divide by 4 in order for me to trigger the CS pin. 

Even though I plan to switch to H7, I will still face the same issues, because H7's SPI NSS pulse mode only goes for 16-bit frames and my DAC messages are 32-bit.

Related: https://stackoverflow.com/questions/71469486/stm32f4-timer-triggered-dma-spi-nss-problem
Seems like the suggestion there is one timer with two channels for the CS (PWM) and the DMA transfers. Which won't be possible, because I can't lock DMA to 48 kHz timer on F4 afaik. It will just burst out the buffer with SPI's frequency. So I'm forced to do single sample (message) sized buffers.