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;

  • Almost the same, certainly the same random (perhaps < 10% of the time) swapping of channels. A very clear effect is that with 5uSec acquisition time + 2uSec conversion time, x 2 channels = 14uSec, it's not ready at the 10uSec point when the timer tries to trigger the next sample, so it waits for the next tick and gets a 50 KSPS sampling rate instead of the advertised 100 KSPS.

    I also tried going out to 10uSec acquisition time, and got the expected 33.3KSPS sampling rate, no visible change in the rate of channel swapping.

    Then I went back to the original 3uSec acquisition time, but changed the timer to trigger at 90KSPS. Yep, the sampling slows down - and the channel swapping stays at the same rate.

    Getting desperate here...hmmm, maybe it's some noisy node in this particular processor. Swapped another processor (different board) and the result was.....exactly the same.

    Clearly my code is doing something that causes a race condition, doesn't quite meet setup or hold times, something like that. But what??

  • My best suggestion is that the timer is like triggering continuous timer compare events asynchronous to the state and configuration of the SAADC at any given time, so likely the CPU is updating buffer pointer, triggering start task or something related at a "bad" time compared to the timer triggering. Do you see the same if you simply disable the timer compare ppi channel to the saadc sample task when you do this "job"?

    Kenneth

  • That sounds reasonable, so much so that I explored it some time ago (and again just now). By changing the sample rate to 50 KSPS I guarantee that the ADC is ready when the timer calls for the next sample. Same result. And anyway, with your explanation, the mismatch would usually happen in the middle of a buffer. I never see that - it's always a full buffer off by one int16, if it's off at all (one in a few tens of buffers).

    I don't see any SHORTS register for the SAADC, so I'll next try using the PPI to connect the EVENTS_RESULTDONE to TASKS_SAMPLE. In theory that will run the ADC at the fastest possible rate, which should be 100 KSPS with two channels enabled. Will be interesting to compare and see how accurate that is, and also whether it solves the problem.

    I'm not hopeful, though. Right now, after the first two passes, most of the ADC setup is skipped on each future pass. Only the EasyDMA registers are set up on each pass after the 2nd, along with the timer and PPI.

  • Update: Using RESULTSDONE to trigger SAMPLE, it samples ever so slightly faster than 100 KSPS. And the problem behavior doesn't change. At random, about one buffer in a few tens of buffers is misaligned by one int16.

    I tried another thought: Slow the timer by a factor of 4, and set up all 8 channels so they alternate between the two real sets of pins. That is, Ch 0, 2, 4, and 6 all see the same set of pins (the original Ch 0), and Ch 1, 3, 5, and 7 sample the original Ch 1. It appears I understand the SAADC because this gives exactly the same result - except that now the misaligned buffers appear to happen more frequently, maybe one in half a dozen. Any ideas as to why that would be, and what it points to as the underlying cause?

    Can you point me to a simple example of just the code that sets up the SAADC to capture a buffer of N results (4096 in my case) on two channels that alternate, at the max 200 KSPS (100 KSPS per channel) rate? I don't need a whole application, just want to see the approved sequence to set things up.

  • Standing by for any guidance you can offer......

Related