2025-07-05 6:49 AM
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
}
2025-07-05 7:07 AM
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.
2025-07-05 8:56 AM - edited 2025-07-05 9:44 AM
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:
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?
2025-07-05 10:20 AM
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.
2025-07-05 11:32 AM - edited 2025-07-05 6:00 PM
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.