1
\$\begingroup\$

I am using the PDEC on a SAMD51 to read a quadrature encoder from a closed loop stepper motor which has 4000 encoder increments per rotation. The PDEC counter is only 16 bits wide, which would overflow in just over 16 rotations. This isn't large enough for my application.

So I've used the SAM EVSYS event system to extend the counter using TCC0. This gives me up to another 24 bits, but I'm only using 16 bits of TCC0 for a total of 32 bits representing the encoder position. Note that this has to be a TCC, not a TC 32-bit pair, because only TCC supports DIR events for counting up or down.

static void setupEncoder() {
  GCLK->PCHCTRL[PDEC_GCLK_ID].reg = GCLK_PCHCTRL_GEN_GCLK1 | GCLK_PCHCTRL_CHEN;
  GCLK->PCHCTRL[TCC0_GCLK_ID].reg = GCLK_PCHCTRL_GEN_GCLK1 | GCLK_PCHCTRL_CHEN;
  while (GCLK->SYNCBUSY.bit.GENCTRL1);
  MCLK->APBBMASK.reg |= MCLK_APBBMASK_TCC0;
  MCLK->APBCMASK.reg |= MCLK_APBCMASK_PDEC;

  PDEC->CTRLA.bit.ENABLE = 0;
  PDEC->EVCTRL.reg = PDEC_EVCTRL_OVFEO | PDEC_EVCTRL_DIREO;
  PDEC->CTRLA.reg = PDEC_CTRLA_MODE_QDEC | PDEC_CTRLA_PINEN0 | PDEC_CTRLA_PINEN1 | PDEC_CTRLA_ANGULAR(7) | PDEC_CTRLA_ENABLE;
  PDEC->CTRLBSET.reg = PDEC_CTRLBSET_CMD_START;
  while (PDEC->SYNCBUSY.reg & PDEC_SYNCBUSY_MASK);

  // TCC0 is used to count the upper 16 bits of the encoder.
  TCC0->EVCTRL.reg = TCC_EVCTRL_TCEI0 | TCC_EVCTRL_TCEI1 | TCC_EVCTRL_EVACT0_COUNTEV | TCC_EVCTRL_EVACT1_DIR;
  TCC0->PER.bit.PER = (1 << 16) - 1;
  TCC0->CTRLA.reg = TCC_CTRLA_ENABLE;
  while (TCC0->SYNCBUSY.reg & TCC_SYNCBUSY_MASK);

  // Event channel 1 connects PDEC DIR events to TCC0 EV1 (DIR).
  EVSYS->USER[EVSYS_ID_USER_TCC0_EV_1].bit.CHANNEL = 2;
  EVSYS->Channel[1].CHANNEL.reg = EVSYS_CHANNEL_EVGEN(EVSYS_ID_GEN_PDEC_DIR) | EVSYS_CHANNEL_PATH_ASYNCHRONOUS;

  // Event channel 2 connects PDEC OVF events to TCC0 EV0 (COUNTEV).
  EVSYS->USER[EVSYS_ID_USER_TCC0_EV_0].bit.CHANNEL = 3;
  EVSYS->Channel[2].CHANNEL.reg = EVSYS_CHANNEL_EVGEN(EVSYS_ID_GEN_PDEC_OVF) | EVSYS_CHANNEL_PATH_ASYNCHRONOUS;
}

static int32_t getEncoderCount() {
  // WARNING: reading these two separate registers may be inconsistent!
  TCC0->CTRLBSET.reg = TCC_CTRLBSET_CMD_READSYNC;
  PDEC->CTRLBSET.reg = PDEC_CTRLBSET_CMD_READSYNC;
  while (TCC0->SYNCBUSY.bit.CTRLB);
  while (TCC0->CTRLBSET.bit.CMD);
  while (PDEC->SYNCBUSY.bit.CTRLB);
  while (PDEC->CTRLBSET.bit.CMD);
  return (TCC0->COUNT.bit.COUNT << 16) | PDEC->COUNT.bit.COUNT;
}

This is all working great, but I smell a race condition in getEncoderCount. Since TCC0->COUNT and PDEC->COUNT are separate registers, and those registers are modified by the hardware peripherals (outside of code), it's not possible to perform a consistent read across both registers. Therefore it's possible that the encoder increments/decrements between reading the two variables, and if this results in an overflow of PDEC->COUNT then the resulting value would be off by 2^16.

How can this inconsistent read of two hardware registers be avoided?

I can think of the following options:

  1. Temporary disable the PDEC while performing the read. This solves the problem by ensuring that the PDEC counter does not change while performing inconsistent reads, but it may result in missed encoder steps. This is not acceptable in my application.

  2. Configure a PDEC counter period of 4, then only use the remaining precision of the TCC0 counter to represent the encoder position. This solves the problem by only requiring a single register read, but discards the lower 2 bits of precision from the encoder, and only provides 24 bits of position data via the TCC counter. This may be acceptable in my application.

  3. In getEncoderCount, re-read the two variables, compute the difference from the previous read, and repeat if the difference is greater than expected. This solves the problem by repeating the read as long as the race condition is detected (which could theoretically go on forever if the PDEC counter is oscillating across the overflow boundary), at the cost of getEncoderCount being a bit slower. This is acceptable in my application.

So in my case, I have at least one or two options. But I'm mostly curious if the world of embedded engineering has a common technique to deal with inconsistent reads of hardware registers, where things like mutexes and barriers aren't applicable.

\$\endgroup\$
6
  • \$\begingroup\$ Why exactly do you have to catch and process every single encoder change? \$\endgroup\$
    – Lundin
    Commented Dec 5, 2023 at 10:05
  • \$\begingroup\$ As for the multiple reads: do you clock the two peripherals from the same GCLK? Do they have the same time base? If so, do you really need to read both, if they are synchronized from the same clock? \$\endgroup\$
    – Lundin
    Commented Dec 5, 2023 at 10:09
  • \$\begingroup\$ "I'm mostly curious if the world of embedded engineering has a common technique to deal with inconsistent reads of hardware registers" Normally this is designed by handling it in hardware, having separate timer channels of the same timer peripheral, which are synchronized through some common "trigger" interrupt. And when that trigger hits, the timer channel values ought to freeze until manually reset. Not all timer peripherals have such features though. \$\endgroup\$
    – Lundin
    Commented Dec 5, 2023 at 10:16
  • \$\begingroup\$ @Lundin Re: every single encoder change - It's an incremental encoder, so if I miss steps then I lose track of position. Given the precision of the encoder, I'm not really worried about missing just a few steps, but I am worried about the slow accumulation of missed steps over time. \$\endgroup\$ Commented Dec 5, 2023 at 16:24
  • \$\begingroup\$ @Lundin Re: do the peripherals use the same GCLK? Yes, they do both share GCLK1. Though I'm not clear on how that helps. I need to read the counter registers from both peripherals because one of them holds the upper 16 bits and the other holds the lower 16 bits. \$\endgroup\$ Commented Dec 5, 2023 at 16:28

2 Answers 2

0
\$\begingroup\$

Is there no index available so you don't have to use the overflow?

Also, the PDEC in QDEC 'secure' mode has automatic missed step compensation - although I've never tried it, I am planning on using it in a far less extreme situation: 53.6.2.6.5 of the D5x/E5x datasheet.

\$\endgroup\$
2
  • \$\begingroup\$ As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center. \$\endgroup\$
    – Community Bot
    Commented Dec 12, 2023 at 21:19
  • \$\begingroup\$ No index is available. \$\endgroup\$ Commented Jan 30 at 22:07
0
\$\begingroup\$

I finally realized that I was overthinking the problem and trying to depend too much on hardware. By letting hardware do most of the work, software can make up for the shortcomings - specifically the too-small 16-bit hardware counter can be supplemented by software.

As long as software can reprocess the 16 bit hardware counter before it changes by 2^15, software can accumulate the counter into as many bits as desired. Using the secondary TCC0 timer counter is not necessary.

int16_t pdec_prev = 0;
int32_t pdec_accumulated = 0;

int32_t getEncoderCount() {
  int16_t pdec_count, pdec_diff;

  // Read the 16 bit hardware register
  PDEC->CTRLBSET.reg = PDEC_CTRLBSET_CMD_READSYNC;
  while (PDEC->SYNCBUSY.bit.CTRLB);
  while (PDEC->CTRLBSET.bit.CMD);
  pdec_count = PDEC->COUNT.bit.COUNT;

  // Compute the difference from the previous pdec counter
  // then add the difference to the accumulator.
  pdec_diff = pdec_count - pdec_prev;
  pdec_prev = pdec_count;
  pdec_accumulated += pdec_diff;

  return pdec_accumulated;
}

void loop() {  // or some other function that is run periodically
  // this discards the result, but we just need to call it frequently
  getEncoderCount();
}

Ironically, I ended up switching my project from the SAMD51 to the STM32G431 for other reasons, and the STM32G431 supports encoder mode on most of its timer peripherals (it doesn't have a separate PDEC peripheral), and one of those timers is 32 bits. For various reasons, I still opted to use one of the 16 bit timers and use the combined hardware/software approach.

\$\endgroup\$

Not the answer you're looking for? Browse other questions tagged or ask your own question.