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:

Thanks PJ.

I have tested no NRF_SAADC->TASKS_START = 1; in SAADC_IRQHandler, but it dont work after from first loop. and no data update to bufferA or bufferB.

Hi there,

Ok , so progress nonetheless, I see this :santa_claus: What’s going on

In your current setup:

  • Timer → PPI → NRF_SAADC->TASKS_SAMPLE
  • NRF_SAADC->TASKS_START is ONLY being called inside SAADC_IRQHandler on EVENTS_END.

So the sequence per buffer is:

  1. You set RESULT.PTR and MAXCNT.
  2. You do NRF_SAADC->TASKS_START = 1;
  3. Timer via PPI keeps triggering TASKS_SAMPLE at 100 kHz until MAXCNT samples are taken.
  4. When buffer is full: EVENTS_END fires, IRQ runs, and you:
  • Flip buffers
  • Set new RESULT.PTR
  • Call TASKS_START again → this re-arms the SAADC for the next block

If you remove NRF_SAADC->TASKS_START = 1; from the IRQ and don’t add anything else, then:

  • SAADC runs once (fills the first buffer)
  • Hits EVENTS_END
  • Swaps the buffer pointer…
  • …but never actually starts the next conversion sequence, so PPI SAMPLE events are ignored and both buffers stay frozen.

“no data update to bufferA or bufferB.”

…is exactly what you’d expect. You took away the only restart mechanism and didn’t replace it with a continuous-mode setup. :face_with_open_eyes_and_hand_over_mouth:

Right now your SAADC only runs again because you explicitly call
NRF_SAADC->TASKS_START = 1; in the SAADC_IRQHandler when EVENTS_END fires.
If you remove that line and don’t add an alternative mechanism, the SAADC will:

  • Fill the first buffer
  • Assert EVENTS_END once
  • Then never restart and never accept new SAMPLE triggers from the timer/PPI.

That’s why your buffers stop updating after the first frame.

To reduce gaps / dead time between blocks, you need to change the structure, not just remove TASKS_START. Some options:

  1. Use the START→SAMPLE shortcut
    Enable:
NRF_SAADC->SHORTS = SAADC_SHORTS_STARTED_SAMPLE_Msk;

Then in the IRQ on EVENTS_END:

NRF_SAADC->EVENTS_END = 0;
// swap RESULT.PTR between bufferA / bufferB
NRF_SAADC->RESULT.PTR = (uint32_t)nextBuffer;
NRF_SAADC->RESULT.MAXCNT = BUF_SIZE;
NRF_SAADC->TASKS_START = 1;   // SAADC will auto-SAMPLE because of the SHORT

  • That at least makes the restart tighter and avoids you manually poking SAMPLE.

  • Lower sample rate or buffer size
    At 100 kHz and 1024 samples you’re asking the SAADC + CPU + IRQ + Serial to run very hard. Even with double-buffering, some dead time between frames is expected at that rate if you’re also doing FFT + printing.

  • Move heavy work out of the ISR and don’t block
    Your processFFT() plus printing thousands of lines at 921600 baud will absolutely cause gaps. Let the ISR do the minimum (swap buffers, restart SAADC) and handle FFT/Serial in loop() only when a buffer_ready flag is set.

  • Just deleting TASKS_START doesn’t magically make it “continuous”; it just stops after the first buffer. :grin:

  • To truly avoid visible gaps in the waveform, you’ll likely need to either:

  • Drop the sample rate (e.g., 50 kHz or 20 kHz), or

  • Reduce BUF_SIZE, or

  • Stop printing full buffers every frame at high baud while sampling. :backhand_index_pointing_left: :face_savoring_food:

HTH
Season’s Greetings :christmas_tree:
GL :santa_claus: PJ :v:

I try to use continuous sampling mode in SAADC, as the datasheet it should be triggered by PPI, but seems not work only in first frame.
when I add NRF_SAADC->TASKS_START = 1; in the IRQ, it will continue run, still some data loss.
if NRF_SAADC->TASKS_START = 1; deleted, just first frame printed.

// ========== SAADC Interrupt ==========
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;  // Keep SAADC active (ensures continuous DMA)
  }
}

// ========== Timer Configuration ==========
void setupTimer()
{
  NRF_TIMER1->MODE = TIMER_MODE_MODE_Timer;
  NRF_TIMER1->BITMODE = TIMER_BITMODE_BITMODE_16Bit;
  NRF_TIMER1->PRESCALER = 0;  // 16 MHz base clock
  NRF_TIMER1->CC[0] = 16000000 / SAMPLE_RATE_HZ;
  NRF_TIMER1->SHORTS = TIMER_SHORTS_COMPARE0_CLEAR_Msk; // Auto-clear
  NRF_TIMER1->TASKS_START = 1;
}

// ========== SAADC Configuration ==========
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;

  // Continuous sampling mode — internal timer trigger
  uint32_t cc = 16000000 / SAMPLE_RATE_HZ;
  NRF_SAADC->SAMPLERATE =
      (SAADC_SAMPLERATE_MODE_Timers << SAADC_SAMPLERATE_MODE_Pos) |
      (cc << SAADC_SAMPLERATE_CC_Pos);

  // Enable END interrupt
  NRF_SAADC->INTENSET = SAADC_INTENSET_END_Msk;
  NVIC_SetPriority(SAADC_IRQn, 1);
  NVIC_EnableIRQ(SAADC_IRQn);

  // PPI: TIMER1 COMPARE[0] → SAADC 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 continuous sampling
  NRF_SAADC->TASKS_START = 1;
}

At 100 kHz, your SAADC setup drops samples and creates gaps. This happens because your buffer swap and restart happen inside the interrupt. That blocks continuous DMA. Serial printing also slows everything down. You cannot print 1024 raw samples at 921600 baud in real time. To fix this, enable true ping‑pong DMA without restarting SAADC in the ISR. Only set a flag in the interrupt. Process buffers later in loop() or defer to another task. Use a faster data path instead of Serial for raw samples. For example, external ADC designs with USB streaming, like this custom DAQ project (https://www.pcbway.com/project/shareproject/ADS1256_RP2040_Custom_DAQ_7a442a9e.html), show how to handle continuous high‑rate sampling. Alternatively, reduce buffer size and minimize processing while sampling. In essence, avoid blocking code during high‑rate acquisition and let DMA run independently to prevent phase jumps and missing samples.