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.

  • I have tried a number of things, including moving the sequence around, enuring sections are clearly stopped during programming, etc. It appears that RESULT.AMOUNT is only updated during a runtime (TASKS_START) and the value can only be read after a TASKS_STOP, so I have added routine ReadResAmt() to handle the housekeeping. I find that most of the readings are 0, occasionally the first reading [0] is 1, but that does not coincide with the captures that are misaligned. Before I tried doing a TASKS_STOP and TASKS_START I was reliably seeing 8192 for all reads, which would be correct for the previous complete capture (4096 sets of two channels).

    I find that reducing the sample rate to 10 KSPS cuts down the occurrence of the misaligned data to about 1 in 50 times. Setting it to 25 KSPS gives misaligned data around one in a couple dozen, and 100 KSPS gives misaligned data about 1 in 4 times. This seems to point to some kind of race condition, but I can't find it.

    I also tried moving more of the clock divider into the Timer prescaler, which had no measurable effect.

    My code is below. I'm at my wit's end as to why this doesn't reliably start EasyDMA capture into the designated buffer, without an extra 16-bit value causing a misalignment.

    uint32_t ReadResAmt (void)
    {
    uint32_t Result;
    NRF_SAADC -> ENABLE = 1;
    NRF_SAADC -> TASKS_START = 1;
    for (int i = 1000000; i; --i) if (NRF_SAADC -> EVENTS_STARTED) break;
    NRF_SAADC -> TASKS_STOP = 1;
    for (int i = 1000000; i; --i) if (NRF_SAADC -> EVENTS_STOPPED) break;
    Result = NRF_SAADC -> RESULT.AMOUNT;
    NRF_SAADC -> ENABLE = 0;
    return Result;
    }

    void StartAdcFor2Ch (PingPongT Which, RangeT Range1, RangeT Range2)
    {
    #define AdcPpiChan 11
    #define AdcTimer NRF_TIMER3

    // Make sure we don't get any premature triggers
    NRF_SAADC -> TASKS_STOP = 1;
    NRF_PPI -> CHENCLR = 1 << AdcPpiChan;
    AdcTimer -> TASKS_STOP = 1;
    AdcTimer -> TASKS_CLEAR = 1;
    AdcTimer -> EVENTS_COMPARE [0] = 0;

    // 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;
    WaveInfo.ResAmt [0] = ReadResAmt ();
    NRF_GPIO -> DIRCLR = 0b10100000000000000000000000010100; // All ADC pins to inputs
    //Note that P0.04 must be jumpered to P0.09 via a C28 pad
    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

    // Set up SAADC for 12 bits, 2 channels, rate determined by AdcTimer
    NRF_SAADC -> OVERSAMPLE = 0;
    NRF_SAADC -> RESOLUTION = 2; // 12 bits
    NRF_SAADC -> SAMPLERATE = 0; // Cannot use internal timer for multiple channels
    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;
    WaveInfo .ResAmt [1] = ReadResAmt ();

    // Set up AdcTimer to trigger sampling (multiple channel sampling cannot use internal timer)
    AdcTimer -> SHORTS = 1; // auto restart on CC[0]
    AdcTimer -> TASKS_CLEAR;
    AdcTimer -> MODE = 0; // Timer
    AdcTimer -> BITMODE = 3; // 32 bit timer
    AdcTimer -> PRESCALER = 4; // /16 prescale
    AdcTimer -> CC [0] = 16000000 / 100000 / (1 << AdcTimer -> PRESCALER);
    WaveInfo .ResAmt [2] = ReadResAmt ();

    // Connect timer output to ADC sampler via PPI
    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;
    WaveInfo .ResAmt [3] = ReadResAmt ();

    // Set up EasyDMA, twice to ensure double-buffering isn't a problem
    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;
    NRF_SAADC -> TASKS_STOP = 1;
    for (int i = 1000000; i; --i) if (NRF_SAADC -> EVENTS_STOPPED) break;

    NRF_SAADC -> RESULT.PTR = (uint32_t) &(Wave [Which]);
    NRF_SAADC -> RESULT.MAXCNT =    sizeof Wave [Which] / sizeof (uint16_t);

    // Ready to run, turn it loose!
    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;
    }

Reply
  • I have tried a number of things, including moving the sequence around, enuring sections are clearly stopped during programming, etc. It appears that RESULT.AMOUNT is only updated during a runtime (TASKS_START) and the value can only be read after a TASKS_STOP, so I have added routine ReadResAmt() to handle the housekeeping. I find that most of the readings are 0, occasionally the first reading [0] is 1, but that does not coincide with the captures that are misaligned. Before I tried doing a TASKS_STOP and TASKS_START I was reliably seeing 8192 for all reads, which would be correct for the previous complete capture (4096 sets of two channels).

    I find that reducing the sample rate to 10 KSPS cuts down the occurrence of the misaligned data to about 1 in 50 times. Setting it to 25 KSPS gives misaligned data around one in a couple dozen, and 100 KSPS gives misaligned data about 1 in 4 times. This seems to point to some kind of race condition, but I can't find it.

    I also tried moving more of the clock divider into the Timer prescaler, which had no measurable effect.

    My code is below. I'm at my wit's end as to why this doesn't reliably start EasyDMA capture into the designated buffer, without an extra 16-bit value causing a misalignment.

    uint32_t ReadResAmt (void)
    {
    uint32_t Result;
    NRF_SAADC -> ENABLE = 1;
    NRF_SAADC -> TASKS_START = 1;
    for (int i = 1000000; i; --i) if (NRF_SAADC -> EVENTS_STARTED) break;
    NRF_SAADC -> TASKS_STOP = 1;
    for (int i = 1000000; i; --i) if (NRF_SAADC -> EVENTS_STOPPED) break;
    Result = NRF_SAADC -> RESULT.AMOUNT;
    NRF_SAADC -> ENABLE = 0;
    return Result;
    }

    void StartAdcFor2Ch (PingPongT Which, RangeT Range1, RangeT Range2)
    {
    #define AdcPpiChan 11
    #define AdcTimer NRF_TIMER3

    // Make sure we don't get any premature triggers
    NRF_SAADC -> TASKS_STOP = 1;
    NRF_PPI -> CHENCLR = 1 << AdcPpiChan;
    AdcTimer -> TASKS_STOP = 1;
    AdcTimer -> TASKS_CLEAR = 1;
    AdcTimer -> EVENTS_COMPARE [0] = 0;

    // 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;
    WaveInfo.ResAmt [0] = ReadResAmt ();
    NRF_GPIO -> DIRCLR = 0b10100000000000000000000000010100; // All ADC pins to inputs
    //Note that P0.04 must be jumpered to P0.09 via a C28 pad
    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

    // Set up SAADC for 12 bits, 2 channels, rate determined by AdcTimer
    NRF_SAADC -> OVERSAMPLE = 0;
    NRF_SAADC -> RESOLUTION = 2; // 12 bits
    NRF_SAADC -> SAMPLERATE = 0; // Cannot use internal timer for multiple channels
    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;
    WaveInfo .ResAmt [1] = ReadResAmt ();

    // Set up AdcTimer to trigger sampling (multiple channel sampling cannot use internal timer)
    AdcTimer -> SHORTS = 1; // auto restart on CC[0]
    AdcTimer -> TASKS_CLEAR;
    AdcTimer -> MODE = 0; // Timer
    AdcTimer -> BITMODE = 3; // 32 bit timer
    AdcTimer -> PRESCALER = 4; // /16 prescale
    AdcTimer -> CC [0] = 16000000 / 100000 / (1 << AdcTimer -> PRESCALER);
    WaveInfo .ResAmt [2] = ReadResAmt ();

    // Connect timer output to ADC sampler via PPI
    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;
    WaveInfo .ResAmt [3] = ReadResAmt ();

    // Set up EasyDMA, twice to ensure double-buffering isn't a problem
    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;
    NRF_SAADC -> TASKS_STOP = 1;
    for (int i = 1000000; i; --i) if (NRF_SAADC -> EVENTS_STOPPED) break;

    NRF_SAADC -> RESULT.PTR = (uint32_t) &(Wave [Which]);
    NRF_SAADC -> RESULT.MAXCNT =    sizeof Wave [Which] / sizeof (uint16_t);

    // Ready to run, turn it loose!
    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;
    }

Children
  • Another finding that may or may not point the way: While the SAADC is running, I watch for NRF_SAADC -> EVENTS_END to determine when the buffer is ready for use. If I do NRF_SAADC -> TASKS_STOP = 1; at that time, the buffer is truncated. ??? Doesn't EVENTS_END indicate that the buffer is complete?

  • Hello again,

    I don't remember all the small details at hand, but the peripheral chapter covering SAADC should cover what is needed to know (e.g. including double buffering of buffer and figures showing tasks and events): 
    https://docs.nordicsemi.com/bundle/ps_nrf52840/page/saadc.html

    Kenneth

  • I agree that it *should*. I've re-read that section with close attention to detail, at least a hundred times (I'm not exaggerating!) over the past couple of weeks, finding no help. Is there a simple example code somewhere that shows the correct sequence for setting up the registers?

  • Do you also have a PPI channel between SAADC END->START event to ensure it will start with next buffer when the first is full? Looking at figure 4 it may indicate this is needed.

    Kenneth

  • I'm not sure I understand what you're suggesting. I looked at the text around that figure and I did see that the preferred way is to set MAXCNT first and then PTR. At first that looked like a solution, because I got lucky and the first half-dozen or so captures worked correctly. But then it went back to randomly swapping the two channels.

    I don't see where I would put the PPI link you suggest. My code fully sets up one buffer, waits for that capture to complete, then sets up the other buffer (Wave [0 or 1]) and starts that running while transmitting the previous capture. I have confirmed that the corruption is happening in the ADC capture and not in the transmission, by inserting flag values in the buffer after the capture and before transmitting.

    I have now confirmed that the RESULT.AMOUNT register shows 8192 values (4K samples, two channels) every time, whether the channels are swapped or not.

    I'm nearly certain there's some kind of race condition that I'm setting up, that the documentation doesn't specify, but I can't figure out where it would be. I configure all the ADC, Timer, and PPI registers with all of them stopped, then enable and start the ADC, then the PPi, and finally the timer. Is that not the correct sequence?

Related