mega4809 crystal calibration problem: status report

Go To Last Post
5 posts / 0 new
Author
Message
#1
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0


 

Hi All,

 

ka7ehk started a thread here looking to calibrate the mega4809 crytal using the RTC.CALIB register.  You have a 32768 Hz crystal connected (to the RTC) and want to calibrate using a 1 pulse per second (PPS) reference source, which may be connected only for a few minutes, say.

 

I got nerdsniped and started working it.

I have a concept to do this and currently working on some of the updates.   Below is a diagram illustrating the concept.

 

 

The idea is to configure the RTC to drive TCB0 in FRQ mode (input capture frequency measurement mode) to capture the count once per second, according to the crystal, and to have the 1 PPS reference to drive TCB1 in FRQ mode to capture the cpu_per count once per second, according to the 1 PPS reference.   It's important to have the two cycles in phase due to the error in the internal 5 Mhz (in my case) clock.  Once I average the difference in periods measured by the 5 MHz internal clock, I divide by 5 and get the difference in PPM.   Then write that to RTC.CALIB.

 

Now I'm not there yet.  I have a signal generated on my "bench" but have not gotten there yet.  I have a standalone ATmeag 4809 Curiosity Nano and have a "stub" mode that drives the 1 PPS pin PB4 from TCA0.

 

One problem I'm trying to figure out right now is why my printed updates are not happening every 5 seconds, but at sometimes longer times.   I think the secs update is happing more often than 1/sec.  I realize there is a lot of code here, but if you see anything fishy, I'd appreciate feedback.  The supporting data is below.

 

By the way, I was getting overruns trying to format the printed output in base10.  Now I appreciate why in the olden days octal was so popular.   The data output here is in octal.  Enjoy.

 

PHASE 1
PHASE 2
 0000005 0000017  RTC  0022267 0020014  PPS  0023777 0000306
 0000012 0000033  RTC  0023640 0020053  PPS  0023775 0000611
 0000017 0000045  RTC  0021716 0020103  PPS  0024000 0001107
 0000024 0000067  RTC  0022110 0020127  PPS  0023776 0001402
...
 0102522 0001370  RTC  0022412 0022407  PPS  0023777 0023777
 0102527 0001367  RTC  0023176 0022412  PPS  0023776 0023777
 0102534 0001353  RTC  0022630 0022425  PPS  0023777 0023777

 

And here is my current code.

 

/* minicom -b 57600 -D /dev/ttyACM0 -C data.out */
#ifdef F_CPU
#undef F_CPU
#endif
#define F_CPU 5000000L
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/wdt.h>

/* Wait for user button press to start application code. */
void nano_button_wait() {
  uint8_t st = 1;			/* switch state */
  uint8_t lc;				/* led counter */

  TCB0.CCMP = 333;			/* debounce time */
  TCB0.CTRLA = TCB_CLKSEL_CLKDIV1_gc | TCB_ENABLE_bm;
  PORTF.DIRSET = PIN5_bm;		/* LED output */

  while (st) {
    switch (st) {
    case 1: if ((PORTF.IN & PIN6_bm) == 0) st = 2; break;
    case 2: if ((PORTF.IN & PIN6_bm) != 0) st = 0; break;
    }
    if (lc++ == 0) PORTF.OUTTGL = PIN5_bm; /* toggle LED */
    while ((TCB0.INTFLAGS & 0x01) == 0);   /* wait for bounce */
    TCB0.INTFLAGS = 0x01;		   /* reset flag */
  }

  /* Restore MCU state. */
  PORTF.DIRCLR = PIN5_bm;
  TCB0.CTRLA = 0x00;
  TCB0.CCMP = 0;
  TCB0.INTFLAGS = 0x01;
}

void init_sys_clk() {
 _PROTECTED_WRITE(CLKCTRL.MCLKCTRLA, CLKCTRL_CLKSEL_OSC20M_gc);
 _PROTECTED_WRITE(CLKCTRL.MCLKCTRLB, CLKCTRL_PDIV_4X_gc | CLKCTRL_PEN_bm);
 while ((CLKCTRL.MCLKSTATUS & 0x11) != 0x10); /* wait for stable OSC20M */
}

#define BAUD_FROM_RATE(RATE) ((4UL * (F_CPU))/(RATE))

void init_uart() {
  USART_t *usart = &USART3;

  usart->BAUD = BAUD_FROM_RATE(57600);
  usart->CTRLC = USART_SBMODE_2BIT_gc | USART_CHSIZE_8BIT_gc;
  PORTB.PIN0CTRL = PORT_PULLUPEN_bm;	/* enable pullup on PB0 */
  PORTB.OUTSET = PIN0_bm;		/* needed? */
  PORTB.DIRSET = PIN0_bm;
  usart->CTRLB = USART_TXEN_bm;		/* enable TX */
}

void init_wdt() {
  _PROTECTED_WRITE(WDT.CTRLA, WDT_PERIOD_8KCLK_gc); /* 8 sec watchdog */
}

void reset() {
  _PROTECTED_WRITE(RSTCTRL.SWRR, 1);	/* software reset */
}

/* === application =============== */

/* phases of operation:
 * phase0: initial state
 * phase1: align RTC to lead 1 PPS slightly
 * phase2: coarse alignment change CALIB to get within TBD PPM
 * phase3: fine alignment: estimate drift over long period (temp comp?)
 * phase4: adjust clock
 */
volatile  uint8_t phase = 0;

void init_rtc() {
 _PROTECTED_WRITE(CLKCTRL.XOSC32KCTRLA, 0x33);
 while ((CLKCTRL.MCLKSTATUS & 0x41) != 0x40); /* wait for stable XOSC32K */

 while (RTC.STATUS != 0);
 RTC.CLKSEL = RTC_CLKSEL_TOSC32K_gc;	/* external crystal */
 while ((RTC.STATUS & 0x01) != 0);
 RTC.PER = 0x3FFF;			/* wrap once per second */
 RTC.CTRLA = RTC_PRESCALER_DIV2_gc | RTC_RTCEN_bm;
}

void config_evsys() {
  EVSYS.CHANNEL6 = 0x06;		      /* RTC OVF -> chan6 */
  EVSYS_USERTCB1 = EVSYS_CHANNEL_CHANNEL6_gc; /* chan6 -> TCB1 */

  EVSYS_CHANNEL0 = 0x48 + 4;		      /* PB4 => chan0 */
  EVSYS_USERTCB2 = EVSYS_CHANNEL_CHANNEL0_gc; /* chan0 => TCB2 */
}

void init_tcb1() {
  TCB1.EVCTRL = /*TCB_FILTER_bm|TCB_EDGE_bm|*/TCB_CAPTEI_bm; /* enable input */
  TCB1.CTRLB = TCB_CCMPEN_bm | TCB_CNTMODE_FRQ_gc; /* capture count on event */
  TCB1.INTCTRL = TCB_CAPT_bm;			   /* and interrupt */
  TCB1.CTRLA = TCB_CLKSEL_CLKDIV1_gc | TCB_ENABLE_bm;
}

volatile uint16_t rtc_ccnt = 0;
volatile uint8_t rtc_ccnt_ready = 0;

ISR(TCB1_INT_vect) {
  rtc_ccnt = TCB1.CCMP;			/* clears INTR flag */
  rtc_ccnt_ready = 1;
  //PORTF.OUTTGL = PIN5_bm;		/* check: LED pulse 1/s */
}

void init_tcb2() {
  TCB2.EVCTRL = /*TCB_FILTER_bm|TCB_EDGE_bm|*/TCB_CAPTEI_bm; /* enable input */
  TCB2.CTRLB = TCB_CCMPEN_bm | TCB_CNTMODE_FRQ_gc; /* capture count on event */
  TCB2.INTCTRL = TCB_CAPT_bm;			   /* and interrupt */
  TCB2.CTRLA = TCB_CLKSEL_CLKDIV1_gc | TCB_ENABLE_bm;
}

volatile uint16_t pps_ccnt = 0;
volatile uint8_t pps_ccnt_ready = 0;

ISR(TCB2_INT_vect) {
  pps_ccnt = TCB2.CCMP;			/* clears INTR flag */
  pps_ccnt_ready = 1;
}

#if 1
void config_standalone() {
  PORTB.DIRSET = PIN4_bm;		/* PB4 is kludged PPS input */
  PORTB.OUTSET = PIN4_bm;
  PORTB.PIN4CTRL = PORT_PULLUPEN_bm;	/* enable pullup on PB4 */

  /* This is a kludge to generate a standlone test mode. */
#if 0
  TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV1024_gc|0x01;
  TCA0.SINGLE.CTRLB = TCA_SINGLE_WGMODE_NORMAL_gc;
  TCA0.SINGLE.INTCTRL = 0x01;			/* interrupt OVF */
  TCA0.SINGLE.PER = 2436;
#else
  TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV256_gc|0x01;
  TCA0.SINGLE.CTRLB = TCA_SINGLE_WGMODE_NORMAL_gc;
  TCA0.SINGLE.INTCTRL = 0x01;			/* interrupt OVF */
  TCA0.SINGLE.PER = 9745;
#endif
}

ISR(TCA0_OVF_vect) {
  PORTB.OUTTGL = PIN4_bm;
  TCA0.SINGLE.INTFLAGS = 0x01;		/* clear intr */
}

#else
void config_standalone() {
}
#endif

/*
 * states:
 * sync: waiting for PPS
 *   on PPS write RTC.CNT = 0x7FF0
 */
int format_data(char *buf, uint16_t a, uint16_t b);

typedef struct {
  uint8_t scnt;			/* sub-integer count */
  uint16_t icnt;		/* integer count */
} sx_time_t;

sx_time_t pps_mean, rtc_mean, del_mean;

void sx_init(sx_time_t *t, uint16_t is) {
  t->icnt = is;
  t->scnt = 0;
}

void sx_update1(uint8_t *px, uint16_t ccnt) {
  /* hard code, not the best way */
  asm("   movw r30,r24\n"
      "	  ldd r19,Z+0\n"
      "	  ldd r20,Z+1\n"
      "	  ldd r21,Z+2\n"
      "	  clr r24\n"
      "	  sub r22,r20\n"
      "	  sbc r23,r21\n"
      "	  brpl 1f\n"
      "	  com r24\n"
      "1: add r22,r19\n"
      "	  adc r23,r20\n"
      "	  adc r24,r21\n"
      "	  std Z+0,r22\n"
      "	  std Z+1,r23\n"
      "	  std Z+2,r24\n"
      : /* no outputs */
      : /* no inputs */
      : "r19", "r20", "r21", "r22", "r23",
	"r24", "r25", "r30", "r31", "cc");
}

void sx_update(sx_time_t *t, uint16_t ccnt) {
  sx_update1(&t->scnt, ccnt);
}

int main(void) {
  USART_t *usart = &USART3;
  int8_t nch = 0;		     /* number of chars to xmit */
  int8_t chx = 0;		     /* index of next char to xmit */
  char buf[64]; 		     /* char buffer for xmit */
  uint16_t secs = 0;
  uint16_t delta;			/* :( */
  int8_t calib;

  nano_button_wait();			/* wait for button-press */

  PORTF.DIRSET = PIN5_bm;		/* set LED/PF5 as output */
  PORTF.OUTSET = PIN5_bm;		/* LED off */

  PORTB.DIRCLR = PIN4_bm;		/* PB4 is PPS input */
  // hack mode:
  //PORTB.PULLUPEN = 

  init_sys_clk();

  init_rtc();
  init_tcb1();
  init_tcb2();
  config_evsys();
  config_standalone();

  init_uart();

  init_wdt();
  sei();

  sx_init(&rtc_mean, 8192);
  sx_init(&pps_mean, 8192);
  sx_init(&pps_mean, 0);

  buf[0] = '\r'; chx = 0; nch = 1;
  while (1) {
    wdt_reset();			/* reset watchdog */

    /* push data out if ready */
    if ((chx < nch) && ((usart->STATUS & USART_DREIF_bm) != 0)) {
      usart->TXDATAL = buf[chx++];
    }

    if (pps_ccnt_ready) {
      pps_ccnt_ready = 0;
      secs += 1;

      PORTF.OUTTGL = PIN5_bm;		/* toggle LED */

      if (chx < nch) reset();		/* deadline for xmit data */

      switch (phase) {

      case 0:
	if (chx == nch) {
	  phase = 1;
	  strcpy(buf, "PHASE 1 \r\n"); nch = strlen(buf); chx = 0;
	}
	break;

      case 1:
	if (chx == nch) {
	  phase = 2;
	  strcpy(buf, "PHASE 2 \r\n"); nch = strlen(buf); chx = 0;
	}
	break;

      case 2:
	sx_update(&rtc_mean, rtc_ccnt);
	sx_update(&pps_mean, pps_ccnt);

	delta = (uint16_t)(0xffff & (pps_ccnt - rtc_ccnt));
	sx_update(&del_mean, delta);

	if (secs < 600) {
	  if ((secs % 5) == 0) {
	    nch = format_data(&buf[0], secs, del_mean.icnt);
	    buf[nch-2] = buf[nch-1] = ' ';
	    chx = 0;
	  }
	  else if ((secs % 5) == 1) {
	    buf[0] = 'R'; buf[1] = 'T'; buf[2] = 'C'; buf[3] = ' '; nch = 4;
	    nch += format_data(&buf[4], rtc_ccnt, rtc_mean.icnt);
	    buf[nch-2] = buf[nch-1] = ' ';
	    chx = 0;
	  }
	  else if ((secs % 5) == 2) {
	    buf[0] = 'P'; buf[1] = 'P'; buf[2] = 'S'; buf[3] = ' '; nch = 4;
	    nch += format_data(&buf[4], pps_ccnt, pps_mean.icnt);
	    chx = 0;
	  }
	} else if ((secs % 5) == 0) {
#if 0
	  calib = ((int16_t) del_mean.icnt) / 5; /* ticks / 5 = PPM */
	  if (calib < 0) {
	    RTC.CALIB = 0x80 | (0x7F)(-calib);
	  } else {
	    RTC.CALIB = (0x7F)(+calib);
	  }
#endif
	  strcpy(buf, "PHASE 3 \r\n"); nch = strlen(buf); chx = 0;
	  phase = 3;
	}

	break;

      case 3:
	sx_update(&rtc_mean, rtc_ccnt);
	sx_update(&pps_mean, pps_ccnt);

	delta = (uint16_t)(0xffff & (pps_ccnt - rtc_ccnt));
	sx_update(&del_mean, delta);

	if ((secs % 5) == 0) {
	  nch = format_data(&buf[0], secs, del_mean.icnt);
	  buf[nch-2] = buf[nch-1] = ' ';
	  chx = 0;
	}
	else if ((secs % 5) == 1) {
	  buf[0] = 'R'; buf[1] = 'T'; buf[2] = 'C'; buf[3] = ' '; nch = 4;
	  nch += format_data(&buf[4], rtc_ccnt, rtc_mean.icnt);
	  buf[nch-2] = buf[nch-1] = ' ';
	  chx = 0;
	}
	else if ((secs % 5) == 2) {
	  buf[0] = 'P'; buf[1] = 'P'; buf[2] = 'S'; buf[3] = ' '; nch = 4;
	  nch += format_data(&buf[4], pps_ccnt, pps_mean.icnt);
	  chx = 0;
	}
	break;

      default:
	break;
      }
    }
  }
}

int format_data(char *buf, uint16_t a, uint16_t b) {
  uint8_t n = 0;
  uint16_t q,r;

  buf[n++] = ' ';
  buf[n++] = '0';
  q = a; n += 6;
  for (uint8_t i = 0; i < 6; i++) {
    r = q%8; q = q/8; buf[n-i-1] = r + '0';
  }
  buf[n++] = ' ';
  buf[n++] = '0';
  q = b; n += 6;
  for (uint8_t i = 0; i < 6; i++) {
    r = q%8; q = q/8; buf[n-i-1] = r + '0';
  }
  buf[n++] = '\r'; buf[n++] = '\n'; buf[n] = '\0';
  return n;
}

/* --- last line --- */

 

EDIT: uploaded (hopefully) readable diagram

 

Last Edited: Sat. Aug 22, 2020 - 01:54 AM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Does it need to be 5MHz during calibration?
Is it not allowed to switch the CPU clock to 32768Hz only during calibration?

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

kabasan wrote:
Is it not allowed to switch the CPU clock to 32768Hz only during calibration?

 

From the original thread, it seemed ka7ehk wasn't very keen on running the main clock at low frequencies.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I still don't understand why all this code, and and don't time it from the 16(/20) MHz internal osc.

 

init:

start free running timer at 16MHz

wait for GPS 1 pps  (high going edge)

wait for 5 us (at this point we don't know which is the fastest but the the GPS has to be the source)

start RTC for 1pps.

 

measure difference in speed:

wait for GPS 1 pps  (high going edge)

read timer

wait for RTC 1 pps  (high going edge)

read timer

calc the diff in time 

 

(repeat) 

If we run 20 MHz and use while there will be about 1/5 us jitter, and what we really need here is a 1us calc.

 

It don't really matter how precise the 20MHz is because it's only about 1/500000 of the time where they don't  use the same.

 

The GPS 1pps needs to be precise but that will be the case for all solutions.

 

I guess if there is no jitter on GPS 1pps I would write to the calibrate register  for every test until the delay between the two 1pps's is constant.     

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

For this approach I don't want to run at 32768 Hz because the resolution is poor.  Using an approach comparing the crystal directly to the 1 PPS would require a closed-loop asynchronous comparison I think.

 

Yes, 20 MHz would provide higher resolution, but lower range on the timers.   I have not traded all the possibilities.   Note that the TCBs will wrap multiple times over one second.  I need to make sure the difference in counts between the 1 PPS source and the crystal running over one second is within [-32768,32768] (the range of TCB.CCMP) given the possible differences between XOSC30K and 1PPS in OSC20M ticks over one second.

 

The precision of the OSC20M is not important as long as the two signals (1 PPS and XOSC37K) are sampled in phase (over the same time interval).

Now that I look back at the posted code, it is missing the hook (during Phase 1) to bring the two signals into phase.  I had an update of RTC.CNT to do that in the TCB2 INT, but must have removed it (during debugging).