Softdevice confilict with Direct Register Access

Hi

I am using XIAO nRF52840 Sense. I want to sample from SAADC @ 100ksps then send the FFT result via BLE. when I try to combine BLE function, it seems that softdevice (the BLE I want to add) is confilict with Direct Register Access (the code as below). Any good idea the combine them together?

#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);
  }
}

Hi there,

SO with the BLE, the SoftDevice takes control. NO negotiating :grin:
In your code, you set the SAADC interrupt priority to 1:
NVIC_SetPriority(SAADC_IRQn, 1);

  • The Problem: Priority 0, 1, and 4 are strictly reserved for the SoftDevice. Priority 1 is specifically for the SoftDevice’s memory protection. If your application tries to use these, the system will likely crash or trigger a HardFault.
  • The Fix: Use priority 2, 3, 5, 6, or 7 for application interrupts. For high-speed sampling like 100ksps, priority 2 or 3 is recommended to minimize latency from other BLE tasks.

Also To combine 100ksps SAADC with BLE on the XIAO nRF52840, follow these steps:

  • Move to SoC-Safe Functions: Instead of NVIC_EnableIRQ, use the Nordic SoC Library calls like sd_nvic_SetPriority() and sd_nvic_EnableIRQ(). These functions coordinate with the SoftDevice to safely manage interrupts.
  • Avoid Long ISRs: At 100ksps, your SAADC_IRQHandler is called very frequently. Keep it extremely light—only swap pointers and set a flag.
  • Handle FFT in the Main Loop: Do not process the FFT inside the interrupt. Your current structure of checking bufferA_ready in loop() is correct, but ensure your BLE transmissions also happen in the main loop or a low-priority task to avoid blocking the high-speed ADC :backhand_index_pointing_left: :saluting_face:
// Change priority to a SoftDevice-safe level (2 or 3)
// Use the SoftDevice API version if possible
sd_nvic_SetPriority(SAADC_IRQn, 3); 
sd_nvic_EnableIRQ(SAADC_IRQn);

// Inside your loop, check if BLE is ready before sending
if (bufferA_ready) {
    processFFT(bufferA);
    // Send FFT result via BLE characteristic here
    // bleCharacteristic.writeValue(fftData, length);
    bufferA_ready = false;
}

By shifting your interrupt priority away from the reserved levels (0, 1, 4), you should be able to run both your high-speed SAADC sampling and the BLE stack simultaneously.

HTH
GL :slight_smile: PJ :v:

this link may be helpful.
Technical Documentation.

1 Like