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:
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.
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.
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 ofgetEncoderCount
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.