ADC Infuriating Behaviour With Multi-channel Scan Using PPI to RTC

Been on all day at this and ready to smash something! :)

I need to do three 'steps' or 'tasks' which are:

1. read 5 channels
2. read 3 channels, which are the same as above minus two (there's a pin aux)
3. read 2 channels repeatedly filling up one of two banks with additional looping.

For brevity I've removed (3) to just do a single read and then call (1).

Each step works from adc interrupt. In the case of (1) and (2) the SAMPLE is wired to STARTED by PPI. Primarily this is because the RTC tick at 32KHz) is too fast.

Then this PPI is disabled and the RTC TICK EVENT is wired to STARTED instead. This gives 30.5us, and the 2 channels plus overhead are well within it.

Behaviour

The routine(s) loop twice and then end with no further interrupts. I believe the ADC is stuck on (2) because the TICK EVENT isn't being pumped through when the PPI channel is re-enabled.

I think I've tried everything to get this working but maybe another pair of eyes could help! 


extern struct statusProto status;

#define ADC_RESOLUTION SAADC_RESOLUTION_VAL_12bit
#define ADC_SAMPLETIME_CFG_SLOW SAADC_CH_CONFIG_TACQ_40us
#define ADC_SAMPLETIME_CFG_MPPT_VOLTAGE SAADC_CH_CONFIG_TACQ_10us
#define ADC_SAMPLETIME_CFG_MPPT_CURRENT SAADC_CH_CONFIG_TACQ_10us

// ADC takes 2us per channel, 4us + 15us + 5us = 24us

// 32,768Hz = 30.52us
// 667 (fit into ram)*30 = 20ms
#define MPPT_SAMPLES 1

// 200ms total / 20ms
#define MPPT_LOOP 2

enum {
  ADC_CH_USB,
  ADC_CH_FRONT,
  ADC_CH_REAR,
  ADC_CH_STORE,
  ADC_CH_LIGHTSENSE,
  ADC_CH_MPPT_VOLTAGE,
  ADC_CH_MPPT_CURRENT
};

struct mpptReadProto {
  int16_t voltage;
  int16_t current;
};

struct {
  volatile struct {
    int16_t usb;
    int16_t front;
    int16_t rear;
    int16_t storage;
    int16_t lightsense;
  } voltages;
  volatile struct {
    int16_t usb;
    int16_t front;
    int16_t rear;
  } currents;
  struct mpptReadProto banks[2][MPPT_SAMPLES];
} reads;

static void step3n (void);
static void step3 (void);
static void step2 (void);
static void step1 (void);

static void (*step)(void);
static uint8_t loop;
static struct mpptTrackProto track;
static struct mpptReadProto *currentBank;

static void step1 (void) {

  step = &step2;

  NRF_PPI->CHENSET = 1 << CFG_PPI_ADC_SAMPLEONSTART;

  NRF_GPIO->OUTCLR = 1 << CFG_PIN_ADC_MUX;

  NRF_SAADC->CH[ADC_CH_USB].PSELP = CFG_ADC_USB_PSEL;
  NRF_SAADC->CH[ADC_CH_FRONT].PSELP = CFG_ADC_FRONT_PSEL;
  NRF_SAADC->CH[ADC_CH_REAR].PSELP = CFG_ADC_REAR_PSEL;
  NRF_SAADC->CH[ADC_CH_STORE].PSELP = CFG_ADC_STORE_PSEL;
  NRF_SAADC->CH[ADC_CH_LIGHTSENSE].PSELP = CFG_ADC_LIGHTSENSE_PSEL;
  NRF_SAADC->RESULT.PTR = (uint32_t) &reads.voltages;
  NRF_SAADC->RESULT.MAXCNT = sizeof(reads.voltages) / 2;
  NRF_SAADC->TASKS_START = SAADC_TASKS_START_TASKS_START_Trigger << SAADC_TASKS_START_TASKS_START_Pos; 
}

static void step2 (void) {

  step = &step3;

  NRF_GPIO->OUTSET = 1 << CFG_PIN_ADC_MUX;
  // rather than wait for adc task compute we do here and send as event
  // this is only benefical if the storage read is greater than 500ms (adc task)
  if (reads.voltages.storage < STORAGE_VOLTAGE_CRITICAL_ON) {
    POWERMANAGER_EventStorageCritical();
  }

  NRF_SAADC->CH[ADC_CH_STORE].PSELP = SAADC_CH_PSELP_PSELP_NC;
  NRF_SAADC->CH[ADC_CH_LIGHTSENSE].PSELP = SAADC_CH_PSELP_PSELP_NC;
  NRF_SAADC->RESULT.PTR = (uint32_t) &reads.currents;
  NRF_SAADC->RESULT.MAXCNT = sizeof(reads.currents) / 2;
  NRF_SAADC->TASKS_START = SAADC_TASKS_START_TASKS_START_Trigger << SAADC_TASKS_START_TASKS_START_Pos; 
}

static void step3 (void) {

  currentBank = &reads.banks[0][0];
  step = &step3n;

  NRF_PPI->CHENCLR = 1 << CFG_PPI_ADC_SAMPLEONSTART;

  NRF_SAADC->CH[ADC_CH_USB].PSELP = SAADC_CH_PSELP_PSELP_NC;
  NRF_SAADC->CH[ADC_CH_FRONT].PSELP = SAADC_CH_PSELP_PSELP_NC;
  NRF_SAADC->CH[ADC_CH_REAR].PSELP = SAADC_CH_PSELP_PSELP_NC;
  NRF_SAADC->CH[ADC_CH_MPPT_VOLTAGE].PSELP = CFG_ADC_MPPT_VOLTAGE_PSEL;
  NRF_SAADC->CH[ADC_CH_MPPT_CURRENT].PSELP = CFG_ADC_MPPT_CURRENT_PSEL;
  NRF_SAADC->RESULT.PTR = (uint32_t) &reads.banks[0];
  NRF_SAADC->RESULT.MAXCNT = sizeof(reads.banks[0]) / sizeof(reads.banks[0][0]);
  NRF_SAADC->TASKS_START = SAADC_TASKS_START_TASKS_START_Trigger << SAADC_TASKS_START_TASKS_START_Pos;
  NRF_PPI->CHENSET = 1 << CFG_PPI_ADC_SAMPLEBYTIMER;
}


static void step3n (void) {

  // code cut down to remove looping
  NRF_SAADC->CH[ADC_CH_MPPT_VOLTAGE].PSELP = SAADC_CH_PSELP_PSELP_NC;
  NRF_SAADC->CH[ADC_CH_MPPT_CURRENT].PSELP = SAADC_CH_PSELP_PSELP_NC;
  NRF_PPI->CHENCLR = 1 << CFG_PPI_ADC_SAMPLEBYTIMER;
  step1();
}

void SAADC_IRQHandler (void) {

  NRF_SAADC->EVENTS_END = SAADC_EVENTS_END_EVENTS_END_NotGenerated << SAADC_EVENTS_END_EVENTS_END_Pos;
  step();
}

uint32_t ADC_TaskCalibrate (void) {

  NRF_SAADC->ENABLE = SAADC_ENABLE_ENABLE_Enabled << SAADC_ENABLE_ENABLE_Pos;
  NRF_SAADC->TASKS_CALIBRATEOFFSET = SAADC_TASKS_CALIBRATEOFFSET_TASKS_CALIBRATEOFFSET_Trigger << SAADC_TASKS_CALIBRATEOFFSET_TASKS_CALIBRATEOFFSET_Pos;
  return CFG_SCHEDULER_TIMER_MINIMUM;
}

uint32_t ADC_TaskStart (void) {

  NRF_SAADC->EVENTS_END = SAADC_EVENTS_END_EVENTS_END_NotGenerated << SAADC_EVENTS_END_EVENTS_END_Pos;
  NRF_SAADC->INTENSET = SAADC_INTENSET_END_Enabled << SAADC_INTENSET_END_Pos;
  
  // Note RTC Event tick is enabled elsewhere!

  step1();
  return CFG_SCHEDULER_TIMER_MINIMUM;
}


inline void ADC_Init (void) {

  NRF_GPIO->PIN_CNF[CFG_PIN_ADC_STORE] = pinDisconnectInputBuffer;
  NRF_GPIO->PIN_CNF[CFG_PIN_ADC_USB] = pinDisconnectInputBuffer;
  NRF_GPIO->PIN_CNF[CFG_PIN_ADC_FRONT] = pinDisconnectInputBuffer;
  NRF_GPIO->PIN_CNF[CFG_PIN_ADC_REAR] = pinDisconnectInputBuffer;
  NRF_GPIO->PIN_CNF[CFG_PIN_ADC_MPPT_VOLTAGE] = pinDisconnectInputBuffer;
  NRF_GPIO->PIN_CNF[CFG_PIN_ADC_MPPT_CURRENT] = pinDisconnectInputBuffer;
  NRF_GPIO->PIN_CNF[CFG_PIN_ADC_LIGHTSENSE] = pinDisconnectInputBuffer;
  NRF_GPIO->PIN_CNF[CFG_PIN_ADC_MUX] = pinDisconnectInputBuffer | (GPIO_PIN_CNF_DIR_Output << GPIO_PIN_CNF_DIR_Pos);
  NRF_GPIO->OUTSET = 1 << CFG_PIN_ADC_MUX;

  NRF_SAADC->RESOLUTION = SAADC_RESOLUTION_VAL_12bit << SAADC_RESOLUTION_VAL_Pos;
  NRF_SAADC->CH[ADC_CH_USB].CONFIG = SAADC_CH_CONFIG_REFSEL_Internal << SAADC_CH_CONFIG_REFSEL_Pos | SAADC_CH_CONFIG_GAIN_Gain1 << SAADC_CH_CONFIG_GAIN_Pos | ADC_SAMPLETIME_CFG_SLOW << SAADC_CH_CONFIG_TACQ_Pos;
  NRF_SAADC->CH[ADC_CH_FRONT].CONFIG = SAADC_CH_CONFIG_REFSEL_Internal << SAADC_CH_CONFIG_REFSEL_Pos | SAADC_CH_CONFIG_GAIN_Gain1 << SAADC_CH_CONFIG_GAIN_Pos | ADC_SAMPLETIME_CFG_SLOW << SAADC_CH_CONFIG_TACQ_Pos;
  NRF_SAADC->CH[ADC_CH_REAR].CONFIG = SAADC_CH_CONFIG_REFSEL_Internal << SAADC_CH_CONFIG_REFSEL_Pos | SAADC_CH_CONFIG_GAIN_Gain1 << SAADC_CH_CONFIG_GAIN_Pos | ADC_SAMPLETIME_CFG_SLOW << SAADC_CH_CONFIG_TACQ_Pos;
  NRF_SAADC->CH[ADC_CH_STORE].CONFIG = SAADC_CH_CONFIG_REFSEL_Internal << SAADC_CH_CONFIG_REFSEL_Pos | SAADC_CH_CONFIG_GAIN_Gain1_2 << SAADC_CH_CONFIG_GAIN_Pos | ADC_SAMPLETIME_CFG_SLOW << SAADC_CH_CONFIG_TACQ_Pos;
  NRF_SAADC->CH[ADC_CH_LIGHTSENSE].CONFIG = SAADC_CH_CONFIG_REFSEL_Internal << SAADC_CH_CONFIG_REFSEL_Pos | SAADC_CH_CONFIG_GAIN_Gain2 << SAADC_CH_CONFIG_GAIN_Pos | ADC_SAMPLETIME_CFG_SLOW << SAADC_CH_CONFIG_TACQ_Pos;
  NRF_SAADC->CH[ADC_CH_MPPT_VOLTAGE].CONFIG = SAADC_CH_CONFIG_REFSEL_Internal << SAADC_CH_CONFIG_REFSEL_Pos | SAADC_CH_CONFIG_GAIN_Gain1_6 << SAADC_CH_CONFIG_GAIN_Pos | ADC_SAMPLETIME_CFG_MPPT_VOLTAGE << SAADC_CH_CONFIG_TACQ_Pos;
  NRF_SAADC->CH[ADC_CH_MPPT_CURRENT].CONFIG = SAADC_CH_CONFIG_REFSEL_Internal << SAADC_CH_CONFIG_REFSEL_Pos | SAADC_CH_CONFIG_GAIN_Gain1_6 << SAADC_CH_CONFIG_GAIN_Pos | ADC_SAMPLETIME_CFG_MPPT_CURRENT << SAADC_CH_CONFIG_TACQ_Pos;
  
  NRF_PPI->CH[CFG_PPI_ADC_SAMPLEONSTART].EEP = (uint32_t) &NRF_SAADC->EVENTS_STARTED;
  NRF_PPI->CH[CFG_PPI_ADC_SAMPLEONSTART].TEP = (uint32_t) &NRF_SAADC->TASKS_SAMPLE;
  NRF_PPI->CH[CFG_PPI_ADC_SAMPLEBYTIMER].EEP = (uint32_t) &NRF_RTC1->EVENTS_TICK;
  NRF_PPI->CH[CFG_PPI_ADC_SAMPLEBYTIMER].TEP = (uint32_t) &NRF_SAADC->TASKS_SAMPLE;
  
  NVIC_SetPriority(SAADC_IRQn, CFG_INTERRUPT_PRIORITY_ADC);
  NVIC_EnableIRQ(SAADC_IRQn);
}

void ADC_Shutdown (void) {

  NRF_SAADC->ENABLE = SAADC_ENABLE_ENABLE_Disabled << SAADC_ENABLE_ENABLE_Pos;
}






Parents
  • And into the evening it continues.

    I've managed to determine the loop is good, but when softdevice is enabled the reads tail off and stop.

    <info> app: Step2 11
    <info> app: Step2 10
    <info> app: Step2 15
    <info> app: Step2 16
    <info> app: Step2 17
    <info> app: Step2 -4
    <ends>

    I've now replaced the timer and PPI's with raw

      NRF_SAADC->EVENTS_STARTED = 0;
      NRF_SAADC->TASKS_START = SAADC_TASKS_START_TASKS_START_Trigger << SAADC_TASKS_START_TASKS_START_Pos;
      while (!NRF_SAADC->EVENTS_STARTED) {}
      NRF_SAADC->TASKS_SAMPLE = 1;


    Eventually I've found that the ADC dies if it's interrupt priority 4 or above. 3 works, above the softdevice non-critical timing ops. I can't tell why this is because I also have a few interrupts at higher levels and they are being serviced.

  • Right, so I've determined that it will mostly work with an RTC prescale of 9, but mostly the ADC will eventually fail and start giving crap out. At a prescale of 8 it will mostly not work and at 7 it's crap out all the time.

    A prescale of 9 equals 3631Hz, which is 274us. 

    Something is interfering with the operation and it worsens as load increases (i.e run softdevice).


    #define ADC_RESOLUTION SAADC_RESOLUTION_VAL_12bit
    #define ADC_SAMPLETIME_CFG_SLOW SAADC_CH_CONFIG_TACQ_20us
    #define ADC_SAMPLETIME_CFG_MPPT_VOLTAGE SAADC_CH_CONFIG_TACQ_20us
    #define ADC_SAMPLETIME_CFG_MPPT_CURRENT SAADC_CH_CONFIG_TACQ_10us
    
    // ADC takes 2us per channel, 4us + 40us + 40us = 84us
    
    // 3641Hz = 274us
    // 667 (fit into ram)*274 = 182ms
    #define MPPT_SAMPLES 350
    
    // 200ms total / 81ms
    #define MPPT_LOOP 3
    
    enum {
      ADC_CH_USB,
      ADC_CH_FRONT,
      ADC_CH_REAR,
      ADC_CH_STORE,
      ADC_CH_LIGHTSENSE,
      ADC_CH_MPPT_VOLTAGE,
      ADC_CH_MPPT_CURRENT
    };
    
    struct mpptReadProto {
      int16_t voltage;
      int16_t current;
    };
    
    volatile static struct {
      int16_t usb;
      int16_t front;
      int16_t rear;
      int16_t storage;
      int16_t lightsense;
    } voltages;
    volatile static struct {
      int16_t usb;
      int16_t front;
      int16_t rear;
    } currents;
    static struct mpptReadProto banks[2][MPPT_SAMPLES];
    
    static void step4 (void);
    static void step3 (void);
    static void step2 (void);
    static void step1 (void);
    
    static void (*step)(void);
    static uint8_t loop;
    static struct mpptTrackProto track;
    static uint8_t bank;
    
    static void step1 (void) {
    
      step = &step2;
    
      NRF_GPIO->OUTCLR = 1 << CFG_PIN_ADC_MUX;
      NRF_PPI->CHENSET = 1 << CFG_PPI_ADC_SAMPLEONSTART;
    
      NRF_SAADC->CH[ADC_CH_USB].PSELP = CFG_ADC_USB_PSEL;
      NRF_SAADC->CH[ADC_CH_FRONT].PSELP = CFG_ADC_FRONT_PSEL;
      NRF_SAADC->CH[ADC_CH_REAR].PSELP = CFG_ADC_REAR_PSEL;
      NRF_SAADC->CH[ADC_CH_STORE].PSELP = CFG_ADC_STORE_PSEL;
      NRF_SAADC->CH[ADC_CH_LIGHTSENSE].PSELP = CFG_ADC_LIGHTSENSE_PSEL;
    
      NRF_SAADC->RESULT.PTR = (uint32_t) &voltages;
      NRF_SAADC->RESULT.MAXCNT = sizeof(voltages) / 2;
    
      NRF_SAADC->TASKS_START = SAADC_TASKS_START_TASKS_START_Trigger << SAADC_TASKS_START_TASKS_START_Pos;
    }
    
    static void step2 (void) {
    
      step = &step3;
    
      NRF_GPIO->OUTSET = 1 << CFG_PIN_ADC_MUX;
    
      NRF_SAADC->RESULT.PTR = (uint32_t) &currents;
      NRF_SAADC->RESULT.MAXCNT = sizeof(currents) / 2;
      NRF_SAADC->TASKS_START = SAADC_TASKS_START_TASKS_START_Trigger << SAADC_TASKS_START_TASKS_START_Pos;
    }
    
    static void step3 (void) {
    
      step = &step4;
      NRF_PPI->CHENCLR = 1 << CFG_PPI_ADC_SAMPLEONSTART;
    
      NRF_SAADC->CH[ADC_CH_USB].PSELP = SAADC_CH_PSELP_PSELP_NC;
      NRF_SAADC->CH[ADC_CH_FRONT].PSELP = SAADC_CH_PSELP_PSELP_NC;
      NRF_SAADC->CH[ADC_CH_REAR].PSELP = SAADC_CH_PSELP_PSELP_NC;
      NRF_SAADC->CH[ADC_CH_STORE].PSELP = SAADC_CH_PSELP_PSELP_NC;
      NRF_SAADC->CH[ADC_CH_LIGHTSENSE].PSELP = SAADC_CH_PSELP_PSELP_NC;
      NRF_SAADC->CH[ADC_CH_MPPT_VOLTAGE].PSELP = CFG_ADC_MPPT_VOLTAGE_PSEL;
      NRF_SAADC->CH[ADC_CH_MPPT_CURRENT].PSELP = CFG_ADC_MPPT_CURRENT_PSEL;
    
      NRF_SAADC->RESULT.PTR = (uint32_t) &banks;
      NRF_SAADC->RESULT.MAXCNT = MPPT_SAMPLES * 2;
    
      NRF_SAADC->TASKS_START = SAADC_TASKS_START_TASKS_START_Trigger << SAADC_TASKS_START_TASKS_START_Pos;
      NRF_PPI->CHENSET = 1 << CFG_PPI_ADC_SAMPLEBYTIMER;
    }
    
    static void trackAssess (uint8_t bankId) {
    
      for(struct mpptReadProto *pCur = &banks[bankId][0]; pCur < &banks[bankId][0] + sizeof(banks[0]); pCur++) {
        if (pCur->voltage > track.voltage && pCur->current > track.current) {
          track.voltage = pCur->voltage;
          track.current = pCur->current;
        }
      }
    }
    
    static void step4 (void) {
    
      if (loop == MPPT_LOOP-1) {
        loop = 0;
        bank = 0;
    
        NRF_SAADC->CH[ADC_CH_MPPT_VOLTAGE].PSELP = SAADC_CH_PSELP_PSELP_NC;
        NRF_SAADC->CH[ADC_CH_MPPT_CURRENT].PSELP = SAADC_CH_PSELP_PSELP_NC;
        NRF_PPI->CHENCLR = 1 << CFG_PPI_ADC_SAMPLEBYTIMER;
        
        trackAssess(bank);
        MPPT_Track(&track);
        track.voltage = 0;
        track.current = 0;
        step1();
        return;
      }
    
      NRF_SAADC->RESULT.PTR = bank? (uint32_t) &banks[0] : (uint32_t) &banks[1];
      NRF_SAADC->TASKS_START = SAADC_TASKS_START_TASKS_START_Trigger << SAADC_TASKS_START_TASKS_START_Pos;
      trackAssess(bank);
      bank = bank? 0:1;
      loop++;
    }
    
    void SAADC_IRQHandler (void) {
    
      NRF_SAADC->EVENTS_END = SAADC_EVENTS_END_EVENTS_END_NotGenerated << SAADC_EVENTS_END_EVENTS_END_Pos;
      step();
    }
    
    uint32_t ADC_TaskCalibrate (void) {
    
      NRF_SAADC->ENABLE = SAADC_ENABLE_ENABLE_Enabled << SAADC_ENABLE_ENABLE_Pos;
      NRF_SAADC->TASKS_CALIBRATEOFFSET = SAADC_TASKS_CALIBRATEOFFSET_TASKS_CALIBRATEOFFSET_Trigger << SAADC_TASKS_CALIBRATEOFFSET_TASKS_CALIBRATEOFFSET_Pos;
      return CFG_SCHEDULER_TIMER_MINIMUM;
    }
    
    uint32_t ADC_TaskStart (void) {
    
      NRF_SAADC->EVENTS_END = SAADC_EVENTS_END_EVENTS_END_NotGenerated << SAADC_EVENTS_END_EVENTS_END_Pos;
      NRF_SAADC->INTENSET = SAADC_INTENSET_END_Enabled << SAADC_INTENSET_END_Pos;
    
      step1();
      return CFG_SCHEDULER_TIMER_MINIMUM;
    }
    
    inline void ADC_Init (void) {
    
      NRF_GPIO->PIN_CNF[CFG_PIN_ADC_STORE] = pinDisconnectInputBuffer;
      NRF_GPIO->PIN_CNF[CFG_PIN_ADC_USB] = pinDisconnectInputBuffer;
      NRF_GPIO->PIN_CNF[CFG_PIN_ADC_FRONT] = pinDisconnectInputBuffer;
      NRF_GPIO->PIN_CNF[CFG_PIN_ADC_REAR] = pinDisconnectInputBuffer;
      NRF_GPIO->PIN_CNF[CFG_PIN_ADC_MPPT_VOLTAGE] = pinDisconnectInputBuffer;
      NRF_GPIO->PIN_CNF[CFG_PIN_ADC_MPPT_CURRENT] = pinDisconnectInputBuffer;
      NRF_GPIO->PIN_CNF[CFG_PIN_ADC_LIGHTSENSE] = pinDisconnectInputBuffer;
      NRF_GPIO->PIN_CNF[CFG_PIN_ADC_MUX] = pinDisconnectInputBuffer | (GPIO_PIN_CNF_DIR_Output << GPIO_PIN_CNF_DIR_Pos);
      NRF_GPIO->OUTSET = 1 << CFG_PIN_ADC_MUX;
    
      NRF_SAADC->RESOLUTION = SAADC_RESOLUTION_VAL_12bit << SAADC_RESOLUTION_VAL_Pos;
      NRF_SAADC->CH[ADC_CH_USB].CONFIG = SAADC_CH_CONFIG_REFSEL_Internal << SAADC_CH_CONFIG_REFSEL_Pos | SAADC_CH_CONFIG_GAIN_Gain1 << SAADC_CH_CONFIG_GAIN_Pos | ADC_SAMPLETIME_CFG_SLOW << SAADC_CH_CONFIG_TACQ_Pos;
      NRF_SAADC->CH[ADC_CH_FRONT].CONFIG = SAADC_CH_CONFIG_REFSEL_Internal << SAADC_CH_CONFIG_REFSEL_Pos | SAADC_CH_CONFIG_GAIN_Gain1 << SAADC_CH_CONFIG_GAIN_Pos | ADC_SAMPLETIME_CFG_SLOW << SAADC_CH_CONFIG_TACQ_Pos;
      NRF_SAADC->CH[ADC_CH_REAR].CONFIG = SAADC_CH_CONFIG_REFSEL_Internal << SAADC_CH_CONFIG_REFSEL_Pos | SAADC_CH_CONFIG_GAIN_Gain1 << SAADC_CH_CONFIG_GAIN_Pos | ADC_SAMPLETIME_CFG_SLOW << SAADC_CH_CONFIG_TACQ_Pos;
      NRF_SAADC->CH[ADC_CH_STORE].CONFIG = SAADC_CH_CONFIG_REFSEL_Internal << SAADC_CH_CONFIG_REFSEL_Pos | SAADC_CH_CONFIG_GAIN_Gain1_2 << SAADC_CH_CONFIG_GAIN_Pos | ADC_SAMPLETIME_CFG_SLOW << SAADC_CH_CONFIG_TACQ_Pos;
      NRF_SAADC->CH[ADC_CH_LIGHTSENSE].CONFIG = SAADC_CH_CONFIG_REFSEL_Internal << SAADC_CH_CONFIG_REFSEL_Pos | SAADC_CH_CONFIG_GAIN_Gain2 << SAADC_CH_CONFIG_GAIN_Pos | ADC_SAMPLETIME_CFG_SLOW << SAADC_CH_CONFIG_TACQ_Pos;
      NRF_SAADC->CH[ADC_CH_MPPT_VOLTAGE].CONFIG = SAADC_CH_CONFIG_REFSEL_Internal << SAADC_CH_CONFIG_REFSEL_Pos | SAADC_CH_CONFIG_GAIN_Gain1_6 << SAADC_CH_CONFIG_GAIN_Pos | ADC_SAMPLETIME_CFG_MPPT_VOLTAGE << SAADC_CH_CONFIG_TACQ_Pos;
      NRF_SAADC->CH[ADC_CH_MPPT_CURRENT].CONFIG = SAADC_CH_CONFIG_REFSEL_Internal << SAADC_CH_CONFIG_REFSEL_Pos | SAADC_CH_CONFIG_GAIN_Gain1_6 << SAADC_CH_CONFIG_GAIN_Pos | ADC_SAMPLETIME_CFG_MPPT_CURRENT << SAADC_CH_CONFIG_TACQ_Pos;
      
      NRF_PPI->CH[CFG_PPI_ADC_SAMPLEONSTART].EEP = (uint32_t) &NRF_SAADC->EVENTS_STARTED;
      NRF_PPI->CH[CFG_PPI_ADC_SAMPLEONSTART].TEP = (uint32_t) &NRF_SAADC->TASKS_SAMPLE;
      NRF_PPI->CH[CFG_PPI_ADC_SAMPLEBYTIMER].EEP = (uint32_t) &NRF_RTC1->EVENTS_TICK;
      NRF_PPI->CH[CFG_PPI_ADC_SAMPLEBYTIMER].TEP = (uint32_t) &NRF_SAADC->TASKS_SAMPLE;
      
      NVIC_SetPriority(SAADC_IRQn, CFG_INTERRUPT_PRIORITY_ADC);
      NVIC_EnableIRQ(SAADC_IRQn);
    }
    
    void ADC_Shutdown (void) {
    
      NRF_SAADC->ENABLE = SAADC_ENABLE_ENABLE_Disabled << SAADC_ENABLE_ENABLE_Pos;
    }



    Here's an example of when it switches to crap out. High reads are valid.

    <info> app: Store 625
    <info> app: Store 633
    <info> app: Store 626
    <info> app: Store 628
    <info> app: Store 627
    <info> app: Store 632
    <info> app: Store 632
    <info> app: Store 5
    <info> app: Store 7
    <info> app: Store 11
    <info> app: Store 12


  • Really useful discussion :-)  Adding to this note a couple of additional ways to improve the SAADC. Instead of calibration another way is to read the temperature and apply a correction based on that, simple to characterise to get a good curve fit and the sort of thing done frequently on (say) the 32kHz crystal for better timekeeping.

    Avoiding data shift can be improved by reducing the underlying cause (this affects SPIM as well). This example is for the nRF52810.

    "EasyDMA is an AHB bus master similar to CPU and is connected to the AHB multilayer interconnect for direct access to Data RAM. AHB multilayer enables parallel access paths between multiple masters and slaves in a system. Access is resolved using priorities.

    Each bus master is connected to the slave devices using an interconnection matrix. The bus masters are assigned priorities. Priorities are used to resolve access when two (or more) bus masters request access to the same slave device. The following applies:
    • If two (or more) bus masters request access to the same slave device, the master with the highest priority is granted the access first.
    • Bus masters with lower priority are stalled until the higher priority master has completed its transaction.
    • If the higher priority master pauses at any point during its transaction, the lower priority master in queue is temporarily granted access to the slave device until the higher priority master resumes its activity.
    • Bus masters that have the same priority are mutually exclusive, thus cannot be used concurrently."

    nRF52810 Priorities
    Bus master name Description
    CPU
    SPIM0/SPIS0 Same priority and mutually exclusive
    RADIO
    CCM/ECB/AAR Same priority and mutually exclusive
    SAADC
    UARTE0
    TWIM0/TWIS0 Same priority and mutually exclusive
    PDM
    PWM

    CPU, RADIO and SPIM0 are likely to cause a stall on the SAADC DMA which can lead to a data shift; with a fixed streaming rate over DMA this is a problem. Reduce this likelihood by dedicating an AHB Bus Master-Slave to a single RAM segment reserved for the SAADC and low-usage cpu data.

    READERBUFFER_SIZE 5
    WRITERBUFFER_SIZE 6
    uint8_t readerBuffer[READERBUFFER_SIZE] __at__ 0x20000000; <== choose dedicated RAM segment
    uint8_t writerBuffer[WRITERBUFFER_SIZE] __at__ 0x20000005;
    // Configuring the READER channel
    MYPERIPHERAL->READER.MAXCNT = READERBUFFER_SIZE;
    MYPERIPHERAL->READER.PTR = &readerBuffer;
    // Configure the WRITER channel
    MYPERIPHERAL->WRITER.MAXCNT = WRITEERBUFFER_SIZE;
    MYPERIPHERAL->WRITER.PTR = &writerBuffer;

    Reduce likelihood further by scheduling SAADC reads between RADIO and CPU and SPIM0 events (more tricky if streaming lots of ADC measurements).

  • So my implementation was based on another post on this forum by a nordic engineer saying the calibration values are retained until power reset, which doesn't appear to be the case according to Karl (probably because it wasn't factoring in the errata code).

    This means I have to do calibration on every step, of which there are 4 in a continuous loop running at around 5Hz. I'm unsure if it's worth the overhead.

    Regarding the buffer shift, the buffer always moves forwards not backwards, which I believe indicates a false signal (double or noise) not a missing one. The shift is also retained across all adc reconfiguration which indicates there's state held somewhere.

  • Hello again,

    A very useful discussion indeed! :) 

    snoopy20 said:
    So my implementation was based on another post on this forum by a nordic engineer saying the calibration values are retained until power reset, which doesn't appear to be the case according to Karl (probably because it wasn't factoring in the errata code).

    I dug deeper into this, and I stand corrected: You will fortunately not have to perform a new calibration each time the Errata 212 workaround is ran.

    Part of the workaround from the errata is to power cycle the peripheral - which means resetting all the internal registers, thus warranting a new calibration - but closer examination on my part has revealed that the previous calibration is actually carried over in the rest of the errata workaround.
    The easiest thing here is of course to abide by the Errata text and set up all registers again, but as it turns out for the specific offset calibration results this value will be carried over. Apologies for the confusion here.

    hmolesworth said:
    Avoiding data shift can be improved by reducing the underlying cause (this affects SPIM as well). This example is for the nRF52810.
    hmolesworth said:
    Reduce likelihood further

    Could you clarify on what you mean when you say that you can 'reduce likelihood' here?
    I agree with your analysis of the AHB priorities and the potential impact of the timing, but you should not see a buffer shift if you implement the workarounds (for each errata as they apply to your project) + the PPI channel described by my colleague Jørgen in the post you referenced earlier, and so you should not need to take actions to 'further reduce likelihood' - perhaps I am misunderstanding something here.

    snoopy20 said:
    The errata says that a TASK_STOP should be used, this is not true as one of my posts above shows, however the delay it introduces may change the timing upon which easydma fails and perhaps gave the impression of such.

    Which errata are you referring to here?
    Since we are discussing multiple ones it would be great if you could reference their number is possible.

    snoopy20 said:
    Another errata says that 5us channels can't be used with timer+PPI. This is untrue, I'm doing it and have never seen an issue.

    Are you here referring to the Errata 212? If so, it only comes into effect when you are also switching from multiple channels to single channel input.
    If not, please specify which errata this is in relation to.

    snoopy20 said:
    And in the datasheet it says burst can't be combined with scan mode. This is not true, I do it just fine. But it may be the case that it can only be one sweep.

    Could it be that you are here referring to that 'scan + oversampling' should not be combined?  It is also true that you may in fact do this if you also enable BURST on all channels.
    As per the Product specification you can use scan + burst so long as burst is enabled on all channels.

    Best regards,
    Karl

  • Cool, I'll revert the code to doing calibration once per loop, thanks. I also read the temperature of the die so will likely add some skipping code at the same time.

    Other matters

    Datasheet - "Scan mode and oversampling cannot be combined. "
    Outcome - Not true, it works. It may not work for repeated scans (I haven't tested) but for a single loop of multiple channels it works fine.

    The 5us, I was looking at #150 within v1 by mistake. I'm unlikely to have these chips in my inventory, likely v2 where it appears it was resolved.

  • My thinking is a stalled DMA write has a write transaction which will complete but before doing so the DMA ptr hardware register is triggered to increment in the peripheral hardware (no DMA memory AHB or process APB interaction completion required for this increment, it's just a hardware "counter" inside the peripheral) and writes the value at this updated memory address. This write completion triggers another hardware counter increment, so now there has been a shift. There is no information on how this hardware pointer register (counter) works, though the likelihood is that it uses the 16MHz clock and a stalled AHB bus write may address the physical memory just before or just after the 16MHz clock edge triggers. Speculation, of course. Worst case two bytes may be written, one at the old required address and another (unwanted) value at the next address which in turns triggers a hardware counter address update; that's even more speculative. The real issue is incorrect synchronisation between the 3 busses: cpu to peripheral via AHB2APB, peripheral DMA AHB to RAM which is shared by the higher priority cpu, and the peripheral innards which is where the DMA address memory pointer is incremented. Both cpu code and cpu data transactions can lead to a stall, so this stall is of an indeterminate period. I understand the Cortex-M7 has fixed this Cortex-M4 issue.

Reply
  • My thinking is a stalled DMA write has a write transaction which will complete but before doing so the DMA ptr hardware register is triggered to increment in the peripheral hardware (no DMA memory AHB or process APB interaction completion required for this increment, it's just a hardware "counter" inside the peripheral) and writes the value at this updated memory address. This write completion triggers another hardware counter increment, so now there has been a shift. There is no information on how this hardware pointer register (counter) works, though the likelihood is that it uses the 16MHz clock and a stalled AHB bus write may address the physical memory just before or just after the 16MHz clock edge triggers. Speculation, of course. Worst case two bytes may be written, one at the old required address and another (unwanted) value at the next address which in turns triggers a hardware counter address update; that's even more speculative. The real issue is incorrect synchronisation between the 3 busses: cpu to peripheral via AHB2APB, peripheral DMA AHB to RAM which is shared by the higher priority cpu, and the peripheral innards which is where the DMA address memory pointer is incremented. Both cpu code and cpu data transactions can lead to a stall, so this stall is of an indeterminate period. I understand the Cortex-M7 has fixed this Cortex-M4 issue.

Children
  • Hello,

    snoopy20 said:
    Datasheet - "Scan mode and oversampling cannot be combined. "
    Outcome - Not true, it works. It may not work for repeated scans (I haven't tested) but for a single loop of multiple channels it works fine.

    Are you here also using BURST?
    If not, then the samples will be averaged over the full scan (across channels), which will produce samples, but they will be averaged over different inputs.
    When BURST is used it will collect all the samples in one channel, average, and then move on to the next, which is why this combination works as expected.

    snoopy20 said:
    The 5us, I was looking at #150 within v1 by mistake. I'm unlikely to have these chips in my inventory, likely v2 where it appears it was resolved.

    Thank you for clarifying! :) 

     
    I concur that this sounds like it could be a likely scenario, but I unfortunately do not have enough prior knowledge of the easyDMA/AHB architecture to provide much insight here unfortunately.

    Best regards,
    Karl

Related