ADC Sampling Synchronization using Timer and PPI

I have code set up to configure Timer3 to generate a Compare event at 100 KHz, using the PPI so that triggers an ADC Sample event for the two enabled ADC channels. EasyDMA is configured to capture a run of 4K samples each alternating between 2 ADC channels. Some of the time it works perfectly, but AFAICT at random, it appears to swap the two channels in memory. I can't tell if it's caused by the ADC triggering prematurely so it's one sample out of phase (which would effectively swap the channels in memory), or if there's something else going on.

Please don't tell me to call XYZ function. I don't need yet another layer between me and the hardware, both for more learning curve and more places for errors to creep in. The hardware should work exactly as documented, whether set up by my function or yours.

Here's the raw code that sets up the ADC, Timer, and PPI, and starts the whole ball rolling. The ping-pong buffering is a recent addition to attempt to resolve this issue, but hasn't made any detectable difference. The variable PingPong is toggled by the code that calls this function.

#define Channels 2
typedef int16_t AdcValT;
typedef struct
   {
   AdcValT Ch [Channels];
   }
SnapT;

typedef enum
   {
   ppPing,
   ppPong,
   PingPongs
   }
PingPongT;
PingPongT PingPong;

SnapT Wave [PingPongs] [4096];


// Make sure we don't get any premature triggers
#define AdcPpiChan 7
#define AdcTimer NRF_TIMER3
NRF_PPI -> CHENCLR = 1 << AdcPpiChan;
AdcTimer -> TASKS_STOP = 1;
// Build the parts of WaveInfo that only we know about
WaveInfo.uSecPerPoint = 10;
WaveInfo.uVPerCount [0] = 1000;
WaveInfo.uVPerCount [1] = 1000;
// Set ADC inputs back to reset defaults
NRF_SAADC -> TASKS_STOP = 1;
for (int i = 1000000; i; --i) if (NRF_SAADC -> EVENTS_STOPPED) break;
NRF_SAADC -> ENABLE = 0;
NRF_GPIO -> DIRCLR = 0b10100000000000000000000000010100; // All ADC pins to inputs
for (int i = 0; i < 8; ++i)
   {
   NRF_SAADC -> CH [i].PSELP = adcNoConnect;
   NRF_SAADC -> CH [i].PSELN = adcNoConnect;
   }
// Set up 2 channels to capture samples at 100 KSPS
NRF_SAADC -> CH [0].PSELP = adcCh1P;
NRF_SAADC -> CH [0].PSELN = adcCh1M;
NRF_SAADC -> CH [0].CONFIG = (Range1 << 8) | (1 << 20); // nonburst mode, diffierential, 3 uSec, internal 600 mV ref, specified gain, no pullups
NRF_SAADC -> CH [1].PSELP = adcCh2P;
NRF_SAADC -> CH [1].PSELN = adcCh2M;
NRF_SAADC -> CH [1].CONFIG = (Range2 << 8) | (1 << 20); // nonburst mode, diffierential, 3 uSec, internal 600 mV ref, specified gain, no pullups
NRF_SAADC -> OVERSAMPLE = 0;
NRF_SAADC -> RESOLUTION = 2; // 12 bits
NRF_SAADC -> SAMPLERATE = 0; // Cannot use internal timer for multiple channels 0x1000 + 40; // 100 KSPS per channel
NRF_SAADC -> EVENTS_DONE = 0;
NRF_SAADC -> EVENTS_STARTED = 0;
NRF_SAADC -> EVENTS_END = 0;
NRF_SAADC -> EVENTS_STOPPED = 0;
NRF_SAADC -> EVENTS_RESULTDONE = 0;
// Connect timer output to ADC sampler via PPI
//AdcTimer -> EVENTS_COMPARE [0] = 0;
AdcTimer -> TASKS_CLEAR;
NRF_PPI -> CH [AdcPpiChan].EEP = (uint32_t) &(AdcTimer -> EVENTS_COMPARE [0]);
NRF_PPI -> CH [AdcPpiChan].TEP = (uint32_t) &(NRF_SAADC -> TASKS_SAMPLE);
NRF_PPI -> CHENSET = 1 << AdcPpiChan;
// Set up TIMERn to trigger sampling (multiple channel sampling cannot use internal timer)
AdcTimer -> SHORTS = 1; // auto restart on CC[0]
AdcTimer -> MODE = 0; // Timer
AdcTimer -> BITMODE = 3; // 32 bit timer
AdcTimer -> PRESCALER = 0; // no prescale
AdcTimer -> CC [0] = 16000000 / 100000; //FIXME

NRF_SAADC -> RESULT.PTR = (uint32_t) &(Wave [Which]);
NRF_SAADC -> RESULT.MAXCNT =    sizeof Wave [Which] / sizeof (uint16_t);
NRF_SAADC -> ENABLE = 1;
NRF_SAADC -> TASKS_START = 1;
for (int i = 1000000; i; --i) if (NRF_SAADC -> EVENTS_STARTED) break;
AdcTimer -> TASKS_CLEAR;
AdcTimer -> TASKS_START = 1;

Parents
  • Hello, are you able (for test at least) to reduce the sampling speed (e.g. 10kHz), this to try to narrow down the issue. I am thinking you are updating the PTR at given times and call START task at given time, I would assume the exact time when you do this (asynchronous to the running timer and potential other high priority interrupts) can influence which channel is next to be sampled to the updated PTR. So I guess I would check the RESULT.AMOUNT register if it's possible it sampled an odd amount of values prior to this.

    Kenneth

  • I have already tried reducing the sampling speed, as well as moving around some of the TASKS_START events, along exactly those lines of thought. I will go try saving the RESULT.AMOUNT register at several times throughout the setup. Good idea, that might at least narrow down when the (alleged) extra sample is being taken.

  • Hi,

    Sorry, but not sure I have anything more to suggest here. I guess an option might be to consider the nRF54L-series instead, since we support a bare metal option there:
    https://www.nordicsemi.com/Products/Development-software/nRF-Connect-SDK/Bare-Metal-option-for-nRF54L-Series?lang=en#infotabs

    Kenneth

  • The randomness is a known issue and is sort of described but not fully in the errata. I basically do what you're doing with the nrf52 but everytime you wish to restart a easydma run fully reset the ADC first using the following code and then set everything back up.

    /*
     * Performs a full reset, required due to known NRF52 errata.
     */
    static void reset (void) {
    
      NRF_PPI->CHENCLR = 1 << CFG_PPI_ADC_SAMPLEMETHOD;
    
      volatile uint32_t temp1;
      volatile uint32_t temp2;
      volatile uint32_t temp3;
      temp1 = *(volatile uint32_t *)0x40007640ul;
      temp2 = *(volatile uint32_t *)0x40007644ul;
      temp3 = *(volatile uint32_t *)0x40007648ul;
      *(volatile uint32_t *)0x40007FFCul = 0ul; 
      *(volatile uint32_t *)0x40007FFCul; 
      *(volatile uint32_t *)0x40007FFCul = 1ul;
      *(volatile uint32_t *)0x40007640ul = temp1;
      *(volatile uint32_t *)0x40007644ul = temp2;
      *(volatile uint32_t *)0x40007648ul = temp3;
    }



    And for reference here's the code which sets up my timed run...


      NRF_SAADC->CH[0].PSELP = CFG_ADC_MPPT_VOLTAGE_PSEL;
      NRF_SAADC->CH[1].PSELP = CFG_ADC_MPPT_CURRENT_PSEL;
      NRF_SAADC->CH[0].CONFIG = SAADC_CH_CONFIG_GAIN_Gain1_6 << SAADC_CH_CONFIG_GAIN_Pos | ADC_SAMPLETIME_CFG_MPPTVOLTAGE << SAADC_CH_CONFIG_TACQ_Pos;
      NRF_SAADC->CH[1].CONFIG = mpptCurrentAttenuation << SAADC_CH_CONFIG_GAIN_Pos | ADC_SAMPLETIME_CFG_CURRENT << SAADC_CH_CONFIG_TACQ_Pos;
      NRF_SAADC->RESULT.MAXCNT = ADC_MPPT_SAMPLES * 2;
      NRF_SAADC->RESULT.PTR = (uint32_t) &buffer;
      NRF_SAADC->INTENSET = SAADC_INTENSET_END_Enabled << SAADC_INTENSET_END_Pos;
      NRF_SAADC->RESOLUTION = ADC_RESOLUTION << SAADC_RESOLUTION_VAL_Pos;
      NRF_SAADC->ENABLE = SAADC_ENABLE_ENABLE_Enabled << SAADC_ENABLE_ENABLE_Pos;
      NRF_SAADC->TASKS_START = SAADC_TASKS_START_TASKS_START_Msk;



    Ensure your timer source isn't ticking faster than the ADC required per run. For 2 channels with 20us and 5us TACQ...

    // ADC takes 2us per channel, 4us + 20us + 5us = 29us
    // 32768Hz = 30us
    // 8192Hz = 122us



  • PS) you can't use RESULTSDONE to trigger SAMPLE it won't work. I can't recall why but iirc the event occurs before EASYDMA may have finished so another point where the wrong buffer position can be used and reads 'flip'. Only use a timer. I personally use TIMER_TICK from an RTC.

  • One more thing, oversample will work on one pass of many channels. I believe (iirc) it won't work on many passes. Quite a few things can cause the random 'flip'!

  • Very good, quite a lot of information here. Let me ask questions in the order of the information presented:

    Are those "magic addresses" in your reset() routine documented somewhere? Clearly they're part of the SAADC block, but not documented in the regular user manual, which stops at 0x638.

    In the code block using documented addresses, you appear to be enabling the SAADC interrupt in line 7. What's that for, since you and I both are using EasyDMA to actually capture the values?

    A ways back up this chain you'll see that I explored the possibility that the timer tick was happening before the ADC is ready. In that case I would expect the random flip to happen in the middle of a capture, not just at the start. And in any case I'm running the SAADC precisely per the datasheet, 3µSec settling, 200 KSPS (2 channels at 100KSPS each; I've also tried 8 channels at 25KSPS, with alternating channels sampling the same pins).

    Using RESULTSDONE to trigger SAMPLE (via the PPI, for lack of a SHORTS register on SAADC) seemed to work just fine, and didn't change the random misalignment.

    I already know about OVERSAMPLE vs. multiple channels. Not using it.

    I'll have a go at your reset() routine with the kinky undocumented addresses, and revisit my setup routine referencing your setup routine, and will post here with how it goes. Thank you for the detailed responses.

Reply
  • Very good, quite a lot of information here. Let me ask questions in the order of the information presented:

    Are those "magic addresses" in your reset() routine documented somewhere? Clearly they're part of the SAADC block, but not documented in the regular user manual, which stops at 0x638.

    In the code block using documented addresses, you appear to be enabling the SAADC interrupt in line 7. What's that for, since you and I both are using EasyDMA to actually capture the values?

    A ways back up this chain you'll see that I explored the possibility that the timer tick was happening before the ADC is ready. In that case I would expect the random flip to happen in the middle of a capture, not just at the start. And in any case I'm running the SAADC precisely per the datasheet, 3µSec settling, 200 KSPS (2 channels at 100KSPS each; I've also tried 8 channels at 25KSPS, with alternating channels sampling the same pins).

    Using RESULTSDONE to trigger SAMPLE (via the PPI, for lack of a SHORTS register on SAADC) seemed to work just fine, and didn't change the random misalignment.

    I already know about OVERSAMPLE vs. multiple channels. Not using it.

    I'll have a go at your reset() routine with the kinky undocumented addresses, and revisit my setup routine referencing your setup routine, and will post here with how it goes. Thank you for the detailed responses.

Children
Related