SAADC with EasyDMA, but some data is missed

Hi ,

I‘m using SAADC with DMA for high frequency sampling.
At a nominal sampling rate of 100 kHz with a buffer size of 1024 samples, we see both missing samples within each acquisition frame and dead time between consecutive frames, resulting in visible gaps and phase jumps in the reconstructed waveform.
I want to know whether this data loss can be avoided by adjusting the code.
attached arduino code and its waveform collected via PC.

#include "nrf.h"
#include <Arduino.h>
#include <arduinoFFT.h>
#include <Adafruit_TinyUSB.h>

#define SAMPLE_RATE_HZ 100000
#define BUF_SIZE       1024

// 双缓冲区
static int16_t bufferA[BUF_SIZE];
static int16_t bufferB[BUF_SIZE];
static volatile bool bufferA_ready = false;
static volatile bool bufferB_ready = false;
static volatile bool activeA = true;

// FFT
double vReal[BUF_SIZE];
double vImag[BUF_SIZE];
ArduinoFFT<double> FFT = ArduinoFFT<double>(vReal, vImag, BUF_SIZE, SAMPLE_RATE_HZ);

// ========== SAADC 中断 ==========
extern "C" void SAADC_IRQHandler(void)
{
  if (NRF_SAADC->EVENTS_END)
  {
    NRF_SAADC->EVENTS_END = 0;

    if (activeA)
    {
      bufferA_ready = true;
      NRF_SAADC->RESULT.PTR = (uint32_t)bufferB;
      activeA = false;
    }
    else
    {
      bufferB_ready = true;
      NRF_SAADC->RESULT.PTR = (uint32_t)bufferA;
      activeA = true;
    }

    // 重新启动下一轮采样
    NRF_SAADC->TASKS_START = 1;
  }

  if (NRF_SAADC->EVENTS_STARTED)
  {
    NRF_SAADC->EVENTS_STARTED = 0;
  }
}

// ========== 定时器 ==========
void setupTimer()
{
  NRF_TIMER1->MODE = TIMER_MODE_MODE_Timer;
  NRF_TIMER1->BITMODE = TIMER_BITMODE_BITMODE_16Bit;
  NRF_TIMER1->PRESCALER = 0;
  uint32_t compare = 16000000 / SAMPLE_RATE_HZ;
  NRF_TIMER1->CC[0] = compare;
  NRF_TIMER1->SHORTS = TIMER_SHORTS_COMPARE0_CLEAR_Msk;
  NRF_TIMER1->TASKS_START = 1;
}

// ========== SAADC ==========
void setupSAADC()
{
  NRF_SAADC->ENABLE = SAADC_ENABLE_ENABLE_Enabled;
  NRF_SAADC->CH[0].PSELP = SAADC_CH_PSELP_PSELP_AnalogInput0;
  NRF_SAADC->CH[0].CONFIG =
      (SAADC_CH_CONFIG_GAIN_Gain1_6 << SAADC_CH_CONFIG_GAIN_Pos) |
      (SAADC_CH_CONFIG_MODE_SE << SAADC_CH_CONFIG_MODE_Pos) |
      (SAADC_CH_CONFIG_REFSEL_Internal << SAADC_CH_CONFIG_REFSEL_Pos) |
      (SAADC_CH_CONFIG_TACQ_5us << SAADC_CH_CONFIG_TACQ_Pos);

  NRF_SAADC->RESOLUTION = SAADC_RESOLUTION_VAL_12bit;
  NRF_SAADC->RESULT.PTR = (uint32_t)bufferA;
  NRF_SAADC->RESULT.MAXCNT = BUF_SIZE;

  NRF_SAADC->INTENSET = SAADC_INTENSET_END_Msk | SAADC_INTENSET_STARTED_Msk;
  NVIC_SetPriority(SAADC_IRQn, 1);
  NVIC_EnableIRQ(SAADC_IRQn);

  // 定时器 → 采样任务
  NRF_PPI->CH[0].EEP = (uint32_t)&NRF_TIMER1->EVENTS_COMPARE[0];
  NRF_PPI->CH[0].TEP = (uint32_t)&NRF_SAADC->TASKS_SAMPLE;
NRF_PPI->CHENSET = (1 << 0);
  // NRF_PPI->CHENSET = PPI_CHENSET_CH0_Msk;

  NRF_SAADC->TASKS_START = 1;
}
// ========================
// 获取特定频率幅值
// ========================
float getMagnitudeAtFrequency(float freq)
{
  float binWidth = (float)SAMPLE_RATE_HZ / BUF_SIZE;
  int index = (int)(freq / binWidth);

  if (index < 0) index = 0;
  if (index >= BUF_SIZE / 2 - 1) index = BUF_SIZE / 2 - 2;

  // 线性插值
  float f1 = index * binWidth;
  float f2 = (index + 1) * binWidth;
  float mag1 = vReal[index];
  float mag2 = vReal[index + 1];
  float mag = mag1 + (mag2 - mag1) * ((freq - f1) / (f2 - f1));
  return mag;
}

// ========== FFT 处理 ==========
void processFFT(int16_t *buffer)
{
    const float V_REF = 0.6;      // 内部参考电压
    const float GAIN  = 1.0 / 6;  // 通道增益配置

    // 1. 转换为真实电压
    uint32_t mode = (NRF_SAADC->CH[0].CONFIG & SAADC_CH_CONFIG_MODE_Msk) >> SAADC_CH_CONFIG_MODE_Pos;
    float res = (float)((mode == 0)? 4096 : 2048);
    for (int i = 0; i < BUF_SIZE; i++) {
        // buffer[i] 范围: -2048 ~ 2047 (signed 12-bit)
        vReal[i] = (buffer[i] / res) * V_REF / GAIN;
        vImag[i] = 0.0;
    }

    // 1b. 输出实时电压值(可选)
    Serial.println("RAW_START");
    for (int i = 0; i < BUF_SIZE; i++) {
        Serial.println(vReal[i], 6); // 保留6位小数
    }
    Serial.println("RAW_END");

    // 2. FFT
    FFT.windowing(vReal, BUF_SIZE, FFT_WIN_TYP_HAMMING, FFT_FORWARD);
    FFT.compute(vReal, vImag, BUF_SIZE, FFT_FORWARD);
    FFT.complexToMagnitude(vReal, vImag, BUF_SIZE);

    // 3. 输出频谱
    Serial.println("FFT_START");
    for (int f = 0; f <= 10000; f += 10) {
        float mag = getMagnitudeAtFrequency((float)f);
        Serial.print(f);
        Serial.print(",");
        Serial.println(mag, 6);
    }
    Serial.println("FFT_END");
}

// ========== 主程序 ==========
void setup()
{
  Serial.begin(921600);
  while (!Serial);

  Serial.println("🔹 初始化 nRF52840 SAADC 双缓冲 DMA + 实时 FFT");
  setupTimer();
  setupSAADC();
  Serial.println("✅ 系统启动");
}

void loop()
{
  if (bufferA_ready)
  {
    bufferA_ready = false;
    processFFT(bufferA);
  }

  if (bufferB_ready)
  {
    bufferB_ready = false;
    processFFT(bufferB);
  }
}

Short answer Yes. However You break one of the Basic coding tennants (Interrupts ISR’s ) need to be short and sweet, set a flag and leave. You restart the sample in the INT. ISR and that blows up the Timing and the Buffer,NO-NO-NO…:grin: :index_pointing_up: So yes: their “dead time between frames” is caused by re-STARTing once per buffer.

The SAADC itself can handle 100 ksps – that’s below the 200 ksps max for nRF52840, even with TACQ=5 µs. The gaps and phase jumps you’re seeing are mostly due to how the SAADC and Serial output are set up, not because 100 kHz is too fast.

  1. SAADC is being re-STARTed every buffer.
    In your SAADC_IRQHandler you call NRF_SAADC->TASKS_START = 1; after EVENTS_END. Once MAXCNT is reached, further SAMPLE triggers are ignored until you START again, so there is inherently dead time between buffers.
    Fix: call TASKS_START once in setupSAADC() and remove it from the IRQ. Just flip RESULT.PTR between bufferA/bufferB on each EVENTS_END.

  2. Serial output is far too slow for 100 ksps.
    Each 1024-sample frame represents ~10 ms of real-time data, but you’re printing tens of kilobytes of ASCII per frame at 921600 bps. That takes ~0.2 s per frame to transmit, so any waveform you reconstruct on the PC will have big gaps by design. The ADC + DMA keep running in the background, but your logging cannot keep up.
    Fix: either lower the sample rate, log only a subset of samples (e.g., one frame every N frames), or store raw data to RAM/SD instead of printing every sample.

With those two changes (no re-START in IRQ + much lighter logging) you should be able to get continuous, gap-free acquisition at 100 ksps and then run your FFT on buffers without the visible phase jumps.

Try this one…

  • Call NRF_SAADC->TASKS_START = 1; once in setupSAADC().
  • Never call START again in the IRQ.
  • Just use EVENTS_END to flip RESULT.PTR between bufferA and bufferB.

Something like:

void setupSAADC() {
  // ... same config ...
  NRF_SAADC->RESULT.PTR    = (uint32_t)bufferA;
  NRF_SAADC->RESULT.MAXCNT = BUF_SIZE;

  NRF_SAADC->INTENSET = SAADC_INTENSET_END_Msk;
  NVIC_SetPriority(SAADC_IRQn, 1);
  NVIC_EnableIRQ(SAADC_IRQn);

  // Timer -> SAMPLE
  NRF_PPI->CH[0].EEP   = (uint32_t)&NRF_TIMER1->EVENTS_COMPARE[0];
  NRF_PPI->CH[0].TEP   = (uint32_t)&NRF_SAADC->TASKS_SAMPLE;
  NRF_PPI->CHENSET     = (1 << 0);

  // Start SAADC once, then let timer+PPI keep it going
  NRF_SAADC->TASKS_START = 1;
}

extern "C" void SAADC_IRQHandler(void)
{
  if (NRF_SAADC->EVENTS_END)
  {
    NRF_SAADC->EVENTS_END = 0;

    if (activeA) {
      bufferA_ready = true;
      NRF_SAADC->RESULT.PTR = (uint32_t)bufferB;
      activeA = false;
    } else {
      bufferB_ready = true;
      NRF_SAADC->RESULT.PTR = (uint32_t)bufferA;
      activeA = true;
    }

    // no TASKS_START here
  }
}

At EVENTS_END, the SAADC considers the current buffer full and ignores further SAMPLE triggers until you START again. So the sequence is:

  1. Timer keeps firing at 100 kHz, PPI keeps poking TASKS_SAMPLE.
  2. Once MAXCNT hits 1024, SAADC stops writing new samples.
  3. In IRQ they set new RESULT.PTR and then call TASKS_START.
  4. Only after the next START + next timer event does sampling resume.

That gap is not huge in absolute time, but at 100 kHz you’re definitely dropping some samples between frames.

HTH
GL :santa_claus: PJ :v: