Clock/Timer Jitter

Started by mah115, January 28, 2019, 03:17:39 AM

mah115

Hi,
   I'm using the TCC0 PPW function to measure the period of a 25Hz pulse train, and I'm getting about 15 uS of measurement jitter for each 40ms period when running the GCLK off the DFLL48M (using a function generator with 10ns of jitter).  I also used TCC0 to generate a 25Hz PWM pulse train, and got similar results measuring the time between the rising edges.  This is considerably worse than the Arduino Uno, which gets about 1uS of period jitter with its servo library.

I doing something wrong, or is the DFLL48M just that jittery? 

Maybe the Moteino Mega with its 16Mhz crystal would be better for my application?

-Mike

Felix

Could you describe your procedure and post some code to replicate this (both M0 and AVR)?
When I have some time I will try to check on several different boards to see if it's repeatable, and the differences between M0 and AVR.

mah115

Here's the code I'm using.  I'm reading the pulse width on D12 and outputting the GCLK1 on D5.  I intended on running everything at 48 MHz, but I divided it down to make it show up better on my scope.

The jitter after 20uS:

//This code reads pulse width/period on D12, and outputs a 8Mhz GCLK1 on D5.

#if defined (MOTEINO_M0) && defined(SERIAL_PORT_USBVIRTUAL)
  #define Serial SERIAL_PORT_USBVIRTUAL // Required for Serial on Zero based boards
#endif


#include "Arduino.h"
//The setup function is called once at startup of the sketch
#include "sam.h"
#include <stdlib.h>

#define BUFFER_SIZE 32
#define SIGNAL 10

static volatile uint32_t period[BUFFER_SIZE]; // Period with PPW capture
static volatile uint32_t period_mean;
static volatile size_t nCap;
#define USE_TCC

void setupInput(void)
{
    PM->APBAMASK.bit.EIC_ = 1;
    REG_GCLK_GENDIV = GCLK_GENDIV_DIV(6) |    // Divide the 48MHz system clock by 6 = 8MHz
                      GCLK_GENDIV_ID(1);      // Set division on Generic Clock Generator (GCLK) 1
    REG_GCLK_GENCTRL = (uint32_t) (GCLK_GENCTRL_GENEN | GCLK_GENCTRL_ID(1) | GCLK_GENCTRL_SRC_DFLL48M|GCLK_GENCTRL_OE);
    GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN | GCLK_CLKCTRL_GEN_GCLK1 | GCLK_CLKCTRL_ID(GCLK_CLKCTRL_ID_EIC);
    while (GCLK->STATUS.bit.SYNCBUSY);

    // Input on PA04
    PORT->Group[0].PMUX[19/2].bit.PMUXO = MUX_PA19A_EIC_EXTINT3;
    PORT->Group[0].PINCFG[19].reg = PORT_PINCFG_PMUXEN|PORT_PINCFG_INEN|PORT_PINCFG_PULLEN;
    EIC->CONFIG[0].bit.SENSE3 = EIC_CONFIG_SENSE3_HIGH_Val; // None of RISE,FALL or BOTH work for this
    EIC->EVCTRL.bit.EXTINTEO3 = 1;


    // The EIC interrupt is only for verifying the input
    //REG_EIC_INTENSET = EIC_INTFLAG_EXTINT4;
    //NVIC_EnableIRQ(EIC_IRQn);
    while (EIC->STATUS.reg & EIC_STATUS_SYNCBUSY);
    EIC->CTRL.bit.ENABLE = 1;
    while (EIC->STATUS.reg & EIC_STATUS_SYNCBUSY);
}


void setupTimer(void)
{
    PM->APBCMASK.bit.TCC0_ = 1;

    REG_GCLK_CLKCTRL = GCLK_CLKCTRL_CLKEN | GCLK_CLKCTRL_GEN_GCLK1 | GCLK_CLKCTRL_ID_TCC0_TCC1;
    while (GCLK->STATUS.bit.SYNCBUSY);

    TCC0->CTRLA.bit.ENABLE = 0;
    while (TCC0->SYNCBUSY.bit.ENABLE);

    TCC0->CTRLA.reg |= TCC_CTRLA_PRESCALER_DIV8 | TCC_CTRLA_CPTEN0 | TCC_CTRLA_CPTEN1; //divide clock by 8 to get 1 uS
    TCC0->PER.reg = 0xFFFFFF;
    while (TCC0->SYNCBUSY.bit.PER);

    TCC0->EVCTRL.reg = TCC_EVCTRL_TCEI1 |  // enable input event
        TCC_EVCTRL_EVACT1_PPW; // event action = PPW
        // | TCC_EVCTRL_MCEI0 | TCC_EVCTRL_MCEI1;

    // Interrupts
    TCC0->INTENSET.bit.MC0 = 1;
    //TCC0->INTENSET.bit.MC1 = 1; // Not needed, can read out both in MC0 interrupt
    NVIC_EnableIRQ(TCC0_IRQn);

    // Enable TCC
    TCC0->CTRLA.bit.ENABLE = 1;
    while (TCC0->SYNCBUSY.bit.ENABLE); // wait for sync

    PM->APBCMASK.bit.EVSYS_ = 1;
    // EVSYS GCLK is needed for interrupts and sync path (so not now)
    // GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN | GCLK_CLKCTRL_GEN_GCLK0 | GCLK_CLKCTRL_ID(GCLK_CLKCTRL_ID_EVSYS_1);
    // while (GCLK->STATUS.bit.SYNCBUSY);

    // TCC_EVCTRL_TCEI1 is used, must be matched with EVSYS_ID_USER_TCC0_EV_1 here
    REG_EVSYS_USER = EVSYS_USER_USER(EVSYS_ID_USER_TCC0_EV_1)|EVSYS_USER_CHANNEL(2); // Channel n-1 selected so 1 here
    while(!EVSYS->CHSTATUS.bit.USRRDY1);
    // EVSYS_CHANNEL_EDGSEL_BOTH_EDGES // not usable with  PATH_ASYNCHRONOUS
    // For this capture it looks like it needs to be PATH_ASYNCHRONOUS
    REG_EVSYS_CHANNEL = EVSYS_CHANNEL_PATH_ASYNCHRONOUS|EVSYS_CHANNEL_EVGEN(EVSYS_ID_GEN_EIC_EXTINT_3)|EVSYS_CHANNEL_CHANNEL(1);

    while(EVSYS->CHSTATUS.bit.CHBUSY1);
    // Below interrupt is for testing the event (not possible with PATH_ASYNCHRONOUS)
    //EVSYS->INTENSET.reg = EVSYS_INTENSET_EVD1;
    //NVIC_EnableIRQ(EVSYS_IRQn);
}



void TCC0_Handler()
{
  uint32_t pulseWidth;  //throwaway variable to clear the interrupt
  //Serial.print(" TCC0: ");
    if (TCC0->INTFLAG.bit.MC0) {
        // The interrupt flag is cleared by reading CC
	period[nCap] = TCC0->CC[0].bit.CC;
        pulseWidth = TCC0->CC[1].bit.CC;

        Serial.print(nCap);
        Serial.print(' ');
        Serial.print(period[nCap]);
        Serial.print(' ');
        Serial.print(pulseWidth);

        nCap++;
        if (nCap == BUFFER_SIZE) {
            uint32_t sum = 0;
            digitalWrite(SIGNAL,HIGH);
            for (uint16_t i=0; i<BUFFER_SIZE; i++){
        	sum += period[i];
            }
            period_mean = sum/BUFFER_SIZE;
            nCap = 0;
            digitalWrite(SIGNAL,LOW);
            Serial.print(period_mean);
            Serial.print("\n\r");
        }
    }
  Serial.print("\n\r");
}


void setup()
{
  SerialUSB.begin(115200);                  // Send data back on the Zero's native port
  while(!SerialUSB);
  pinMode(LED_BUILTIN, OUTPUT);
  pinMode (5, OUTPUT);

  pinMode(SIGNAL, OUTPUT);

  PORT->Group[0].PMUX[15/2].bit.PMUXO = MUX_PA15H_GCLK_IO1; //output GCLK1 on PA15/Moteino D5
  PORT->Group[0].PINCFG[15].bit.PMUXEN = 1;
  PORT->Group[0].PINCFG[15].bit.INEN = 0;


// Add your initialization code here
  setupInput();
  Serial.print("Setup Input\n\r");
  setupTimer();
  Serial.print("Setup Timer\n\r");

}

// The loop function is called in an endless loop
void loop()
{
  digitalWrite(LED_BUILTIN,HIGH);
  while(1);
}



For pulse generation, I'm trying to output 1.5ms every 40ms:

/**  This code outputs 1.5ms pulse every 40ms on Moteino pin4
 * @author Markus Bader
 * @brief this program shows how to use the TCC timer with interrupts on an Arduino Zero board
 * @email [email protected]
 */

#if defined (MOTEINO_M0) && defined(SERIAL_PORT_USBVIRTUAL)
  #define Serial SERIAL_PORT_USBVIRTUAL // Required for Serial on Zero based boards
#endif

int pin_ovf_led = LED_BUILTIN; // debug pin for overflow led
int pin_mc0 = 5;  // debug pin for compare led
int pin_tcc0 = 4;
unsigned int loop_count = 0;
unsigned int irq_ovf_count = 0;
Tcc* TCC = (Tcc*) TCC0; // get timer struct

void pin9_Handler()
{
	Serial.print("pin9");
}

void setup() {
  Serial.begin(115200);
  delay(10);

  pinMode(pin_ovf_led, OUTPUT);   // for debug leds
  digitalWrite(pin_ovf_led, LOW); // for debug leds
  pinMode(pin_mc0, OUTPUT);   // for debug leds
  digitalWrite(pin_mc0, LOW); // for debug leds
  pinMode(pin_tcc0, OUTPUT);
  PORT->Group[0].PINCFG[8].bit.PMUXEN = 1; //pin PA08 (pin 13), or moteino Pin 4
  PORT->Group[0].PMUX[4].bit.PMUXE = PORT_PMUX_PMUXE_E_Val;



  // Enable clock for TC

  //don't use GCLK_0, serial needs it
  REG_GCLK_GENCTRL = (uint32_t) (GCLK_GENCTRL_GENEN | GCLK_GENCTRL_ID(1) | GCLK_GENCTRL_SRC_DFLL48M);
  REG_GCLK_CLKCTRL = (uint16_t) (GCLK_CLKCTRL_CLKEN | GCLK_CLKCTRL_GEN_GCLK1 | GCLK_CLKCTRL_ID_TCC0_TCC1);
  while ( GCLK->STATUS.bit.SYNCBUSY == 1 ); // wait for sync

  // The type cast must fit with the selected timer
  TCC->CTRLA.reg &= ~TCC_CTRLA_ENABLE;   // Disable TC
  while (TCC->SYNCBUSY.bit.ENABLE == 1); // wait for sync
  TCC->CTRLA.reg |= TCC_CTRLA_PRESCALER_DIV1;   // Set perscaler
  TCC->WAVE.reg |= TCC_WAVE_WAVEGEN_NPWM;   // Set wave form configuration
  while (TCC->SYNCBUSY.bit.WAVE == 1); // wait for sync
  TCC->PER.reg = 1920000;              // Set counter Top using the PER register
  while (TCC->SYNCBUSY.bit.PER == 1); // wait for sync
  TCC->CC[0].reg = 72000;
  while (TCC->SYNCBUSY.bit.CC0 == 1); // wait for sync


  // Interrupts
  TCC->INTENSET.reg = 0;                 // disable all interrupts
  TCC->INTENSET.bit.OVF = 1;          // enable overfollow
  TCC->INTENSET.bit.MC0 = 1;          // enable compare match to CC0


/*
  //Set up event system for pin 9 falling edge interrupt to time radio delay
  REG_PM_APBCMASK |= PM_APBCMASK_EVSYS;     // Switch on the event system peripheral
  REG_EIC_EVCTRL |= EIC_EVCTRL_EXTINTEO0;         // Enable event output on external interrupt 3
  attachInterrupt(9, pin9_Handler, FALLING);        // Attach interrupts to digital pin 12 (external interrupt 3)
  REG_EVSYS_USER = EVSYS_USER_CHANNEL(1) |                                // Attach the event user (receiver) to channel 0 (n + 1)
                   EVSYS_USER_USER(EVSYS_ID_USER_TCC0_MC_0);                // Set the event user (receiver) as timer TCC0

  REG_EVSYS_CHANNEL = EVSYS_CHANNEL_EDGSEL_NO_EVT_OUTPUT |                // No event edge detection
                        EVSYS_CHANNEL_PATH_ASYNCHRONOUS |                   // Set event path as asynchronous
                        EVSYS_CHANNEL_EVGEN(EVSYS_ID_GEN_EIC_EXTINT_0) |    // Set event generator (sender) as external interrupt 3
                        EVSYS_CHANNEL_CHANNEL(0);                           // Attach the generator (sender) to channel 0
  REG_TCC0_EVCTRL |= TCC_EVCTRL_MCEI0;              // Enable the TC event input
*/


  // Enable InterruptVector
  NVIC_EnableIRQ(TCC0_IRQn);

  // Enable TC
  TCC->CTRLA.reg |= TCC_CTRLA_ENABLE ;
  while (TCC->SYNCBUSY.bit.ENABLE == 1); // wait for sync

}

void loop() {
  // dummy
  delay(1000);
  REG_TCC0_CTRLBSET = TCC_CTRLBSET_CMD_READSYNC;  // Trigger a read synchronization on the COUNT register
  //while (TCC0->SYNCBUSY.bit.CTRLB);               // Wait for the CTRLB register write synchronization
  //while (TCC0->SYNCBUSY.bit.COUNT);               // Wait for the COUNT register read sychronization
  //Serial.print(TC->COUNT.reg,DEC);
  Serial.print(REG_TCC0_CC0,HEX);
  Serial.print("\n\r");
}


void TCC0_Handler()
{
  Tcc* TCC = (Tcc*) TCC0;       // get timer struct
  if (TCC->INTFLAG.bit.OVF == 1) {  // A overflow caused the interrupt
    digitalWrite(pin_ovf_led, irq_ovf_count % 2); // for debug leds
    digitalWrite(pin_mc0, HIGH); // for debug leds
    TCC->INTFLAG.bit.OVF = 1;    // writing a one clears the flag ovf flag
    irq_ovf_count++;                 // for debug leds
  }

  if (TCC->INTFLAG.bit.MC0 == 1) {  // A compare to cc0 caused the interrupt
    digitalWrite(pin_mc0, LOW);  // for debug leds
    TCC->INTFLAG.bit.MC0 = 1;    // writing a one clears the flag ovf flag
  }
}



For the Arduino Uno it's just the servo example that comes with the IDE.  It looks like it was more jitter than originally measured, but still less than the M0:

#include <Servo.h>
Servo myservo;  // create servo object to control a servo
// twelve servo objects can be created on most boards

int pos = 0;    // variable to store the servo position

void setup() {
  myservo.attach(9);  // attaches the servo on pin 9 to the servo object
  myservo.write(90);
}

void loop() {

}



mah115

I ended up using the PLL instead of FLL:

  SYSCTRL->DPLLCTRLB.bit.REFCLK = SYSCTRL_DPLLCTRLB_REFCLK_REF0_Val;
  SYSCTRL->DPLLRATIO.bit.LDR = 1464;
  SYSCTRL->DPLLRATIO.bit.LDRFRAC = 14;
  SYSCTRL->DPLLCTRLA.bit.ENABLE = 1;

  REG_GCLK_GENCTRL = (uint32_t) (GCLK_GENCTRL_GENEN | GCLK_GENCTRL_ID(1) | GCLK_GENCTRL_SRC_FDPLL|GCLK_GENCTRL_OE);

The clock output is still showing jitter, but the measurement is much better and if I average a few samples, it's actually very stable.  I'm guessing maybe the problem with FLL isn't so much the jitter but the frequency stability, it's probably not locking well--maybe it's not tuned well, or it's just sloppy?  I might just have to take the power consumption hit with PLL for now.

Felix

Thanks for the follow up and sharing your code.
I assume then your report is the better solution, can we mark as solved/solution?

Is there no library to help with generating clock?

mah115

Yes, at least I couldn't find a way to make FLL work after playing with it a bit--the registers say the freqency is locked on, but the jitter is still there.  I suspect their FLL is just weak, it'll get you average 48MHz-ish, but the jitter and stability are not good.

Atmel has their ASF library, but I've never tried it. 

perky

Just a question, are you using a PWM mode to drive this clock output directly by hardware, or setting and resetting the clock output inside an ISR? You'll get jitter if in ISRs, and especially if using print statements in them as well.

Mark.

mah115

The clock output is an option on the SAMD21, you can pipe the clock out directly to selected I/O pins.