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;

  • 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?

  • I now have it more stable, still not quite there. I see from ten to several tens of good captures, then one capture with the channels swapped (or equivalently, misaligned in memory by 2 bytes). I have tried moving a lot of code around, and in particular made sure that nothing can get triggered while I'm setting things up. I also set up most of the SAADC registers only on the first two passes, or if something is changed by a user command (the user can change the gain settings etc.) I've now trimmed down the code to the basics, without a lot of the redundant stops and starts that I tried to no effect. Here's the code I'm currently running - results are close but not quite good enough.

    void StartAdcFor2Ch (PingPongT Which, RangeT Range1, RangeT Range2)
    {
    #define AdcPpiChan 12
    #define AdcTimer NRF_TIMER4

    // Make sure we don't get any premature triggers
    NRF_SAADC -> TASKS_STOP = 1;
    for (int i = 10000000; i; --i) if (NRF_SAADC -> EVENTS_STOPPED) break;
    NRF_PPI -> CHENCLR = 1 << AdcPpiChan;
    AdcTimer -> TASKS_STOP = 1;
    AdcTimer -> TASKS_CLEAR = 1;
    AdcTimer -> EVENTS_COMPARE [0] = 0;

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

    if (AlreadyConfigured < 2)
       {
       // 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 -> ENABLE = 0;
       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;
          }
       NRF_SAADC -> ENABLE = 1;
       
       // Set up 2 channels to capture samples at 100 KSPS
       NRF_SAADC -> ENABLE = 0;
       NRF_SAADC -> CH [1].PSELP = adcCh1P;
       NRF_SAADC -> CH [1].PSELN = adcCh1M;
       NRF_SAADC -> CH [1].CONFIG = (Range1 << 8) | (1 << 20); // nonburst mode, diffierential, 3 uSec, internal 600 mV ref, specified gain, no pullups
       NRF_SAADC -> CH [5].PSELP = adcCh2P;
       NRF_SAADC -> CH [5].PSELN = adcCh2M;
       NRF_SAADC -> CH [5].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;
       NRF_SAADC -> ENABLE = 1;

       // 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);

       ++AlreadyConfigured;
       }

    AdcTimer -> EVENTS_COMPARE [0] = 0;
    NRF_SAADC -> TASKS_START = 1;
    for (int i = 10000000; i; --i) if (NRF_SAADC -> EVENTS_STARTED) break;

    // 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;

    // Ready to run, turn it loose!
    AdcTimer -> TASKS_CLEAR;
    AdcTimer -> TASKS_START = 1;
    }

  • Strange indeed.

    If you try 5us acquisition time instead, do you see the same then?

    Kenneth

Related