I have a 32-bit core timer on a PIC32MZ microcontroller (datasheet) counting at 100 MHz.
The system is running FreeRTOS with multiple tasks (threads) and this code should be able to get called from any task at any time.
It rolls over every 2^32 ticks / 100000000 ticks/sec = 42.9 seconds. I'd like to safely convert it into a 64-bit counter.
There is an ISR which handles counting overflows. It looks something like this:
static volatile uint32_t rollover_count = 0;
void isr_core_timer_overflow()
{
rollover_count++;
}
This concept would also work on 8-bit microcontrollers to convert from 8-bit counters to 16-bit or 32-bit counters.
Here is my proposed solution:
// For 32-bit mcus such as PIC32
uint64_t get_counts()
{
uint32_t count1 = ReadCoreTimer();
// No critical section needed because we are reading a 32-bit value, and
// turning off interrupts doesn't stop the core timer from counting
// anyway.
uint32_t rollover_count_copy = rollover_count;
uint32_t count2 = ReadCoreTimer();
if (count2 < count1)
{
// rollover just occurred between reading count1 and count2, so manually
// account for it; note: rollover_count will be incremented
// by the rollover ISR, so I don't need to update it
rollover_count_copy++;
}
uint64_t total_count = ((uint64_t)rollover_count_copy << 32) | count2;
return total_count;
}
Part of me thinks I do need a critical section, but I can't wrap my head around why. It has something to do with the fact that when the core timer overflows, the overflow ISR would run, and maybe I would need to momentarily stop that.
If I had interrupt guards to protect a critical section, it might look like this:
// For 32-bit mcus such as PIC32
uint64_t get_counts()
{
uint32_t count1 = ReadCoreTimer();
// back up the interrupt state
// - Note: the `__builtin_*` functions are part of the XC32 compiler, and
// are documented in DS50002799C (MPLAB® XC32 C/C++ Compiler User's Guide
// for PIC32M MCUs; XC32 Compiler for PIC32M) p195, p265 (the assembly
// code output for the functions is around here), etc.
unsigned int isr_state = __builtin_get_isr_state();
__builtin_disable_interrupts(); // critical section start
uint32_t rollover_count_copy = rollover_count;
uint32_t count2 = ReadCoreTimer();
// restore the interrupt state
__builtin_set_isr_state(isr_state); // critical section end
if (count2 < count1)
{
// rollover just occurred between reading count1 and count2, so manually
// account for it
rollover_count_copy++;
}
uint64_t total_count = ((uint64_t)rollover_count_copy << 32) | count2;
return total_count;
}
Or maybe move the count1
read into the critical section too to stop the rollover_count
from incrementing in its ISR during that time too:
// For 32-bit mcus such as PIC32
uint64_t get_counts()
{
unsigned int isr_state = __builtin_get_isr_state();
__builtin_disable_interrupts(); // critical section start
uint32_t count1 = ReadCoreTimer();
uint32_t rollover_count_copy = rollover_count;
uint32_t count2 = ReadCoreTimer();
__builtin_set_isr_state(isr_state); // critical section end
if (count2 < count1)
{
rollover_count_copy++;
}
uint64_t total_count = ((uint64_t)rollover_count_copy << 32) | count2;
return total_count;
}
Which is correct? What solution would you propose? Are there any problems with my first approach above?
References
My 8-bit AVR solution that has been working flawlessly for a decade is in the top of this issue here, but I feel like a rewrite following the design above would run faster and be simpler, and now I need to write this for a PIC32 anyway, which I've never done before. The
if (count2 < count1)
check is to try to counter this race condition identified here.The
ReadCoreTimer()
function is provided here inside the document titled PIC32 Peripheral Libraries for MPLAB C32 Compiler - also containsUpdateCoreTimer()
,WriteCoreTimer()
, etc.https://www.microchip.com/en-us/tools-resources/develop/mplab-xc-compilers/xc32 --> scroll down to "MPLAB XC32 Compiler Documentation" and download "MPLAB XC32 C/C++ Compiler User's Guide for PIC32M MCUs".
- Direct link: MPLAB® XC32 C/C++ Compiler User's Guide for PIC32M MCUs--XC32 Compiler for PIC32M--DS50002799C.pdf - contains the documentation for the
__builtin_*
functions. From section 18.8 p195:unsigned int __builtin_get_isr_state(void) void __builtin_set_isr_state(unsigned int) unsigned int __builtin_disable_interrupts(void) void __builtin_enable_interrupts(void)
- Direct link: MPLAB® XC32 C/C++ Compiler User's Guide for PIC32M MCUs--XC32 Compiler for PIC32M--DS50002799C.pdf - contains the documentation for the
Related
- Timer rollover handling - related, but not a duplicate
Notes to self:
Add a
previous_count
variable. Update it to the last returned value after eachget_counts()
call, and update it to0
after each overflow ISR run. Use it to check for overflows.For AVR, enable nested interrupts inside the overflow ISR. Ensure that the interrupt that interrupts that interrupt does not also allow nested interrupts.
Nevermind: the interrupting interrupts could be pin change interrupts and they need to able to read timestamps to measure incoming PWM signals.
_Atomic
, even if your CPU happens to be 32 bit. Because a single C read/write can result in multiple assembler instructions. You can only tell if this is actually "thread safe" by watching the disassembly. If your compiler supports_Atomic
, you could use that. Or otherwise you can write in inline asm. Or otherwise disable the specific interrupt during reads. – Knowitalltasks.c
:/* A critical section is not required because the variables are of type BaseType_t. */
. See that quote and its source in my answer here: Which variable types/sizes are atomic on STM32 microcontrollers?. It applies to the PIC32 mcu I'm using too. I agree that there is no guarantee in C... – Bisulfateatomic_uint_least32_t
) and here (_Atomic
). – Bisulfate