如何正确计数定时器溢出,将32位高精度定时器转为64位
PIC32MZ核心定时器64位计数器实现方案分析
问题背景
我在PIC32MZ微控制器上使用一个以100MHz计数的32位核心定时器,系统运行FreeRTOS多任务,该定时器每42.9秒发生一次溢出。我通过中断服务程序(ISR)维护rollover_count变量来计数溢出次数,希望将其安全转换为64位计数器。现针对以下三种get_counts()函数实现方案,咨询:
- 哪种方案正确?
- 应采用何种解决方案?
- 第一种方案存在哪些问题?
现有溢出ISR代码
static volatile uint32_t rollover_count = 0; void isr_core_timer_overflow() { rollover_count++; }
方案一
// 适用于PIC32这类32位微控制器 uint64_t get_counts() { uint32_t count1 = ReadCoreTimer(); // 无需临界区,因为读取的是32位值,而且关中断也不会阻止核心定时器计数 uint32_t rollover_count_copy = rollover_count; uint32_t count2 = ReadCoreTimer(); if (count2 < count1) { // 在读取count1和count2之间刚好发生了溢出,所以手动补计一次;注意:rollover_count会被溢出ISR递增,无需手动更新 rollover_count_copy++; } uint64_t total_count = ((uint64_t)rollover_count_copy << 32) | count2; return total_count; }
方案二(含临界区保护)
// 适用于PIC32这类32位微控制器 uint64_t get_counts() { uint32_t count1 = ReadCoreTimer(); // 保存中断状态 // 注:`__builtin_*`函数属于XC32编译器,文档可参考DS50002799C(《MPLAB® XC32 C/C++编译器用户指南(PIC32M系列)》)第195、265页附近(函数对应的汇编输出内容在此处)等 unsigned int isr_state = __builtin_get_isr_state(); __builtin_disable_interrupts(); // 临界区开始 uint32_t rollover_count_copy = rollover_count; uint32_t count2 = ReadCoreTimer(); // 恢复中断状态 __builtin_set_isr_state(isr_state); // 临界区结束 if (count2 < count1) { // 在读取count1和count2之间刚好发生了溢出,所以手动补计一次 rollover_count_copy++; } uint64_t total_count = ((uint64_t)rollover_count_copy << 32) | count2; return total_count; }
方案三(将count1读取移入临界区)
// 适用于PIC32这类32位微控制器 uint64_t get_counts() { unsigned int isr_state = __builtin_get_isr_state(); __builtin_disable_interrupts(); // 临界区开始 uint32_t count1 = ReadCoreTimer(); uint32_t rollover_count_copy = rollover_count; uint32_t count2 = ReadCoreTimer(); __builtin_set_isr_state(isr_state); // 临界区结束 if (count2 < count1) { rollover_count_copy++; } uint64_t total_count = ((uint64_t)rollover_count_copy << 32) | count2; return total_count; }
方案分析与结论
方案一的问题
- 存在计数错误的风险:假设在读取
count1之后、读取rollover_count_copy之前发生定时器溢出,ISR会将rollover_count加1。此时count1是溢出前的大值,rollover_count_copy是更新后的新值,后续读取count2得到溢出后的小值,代码会判断count2 < count1为真,进而将rollover_count_copy再加1,最终导致64位计数器多计一次溢出,数据完全错误。 - 核心原因是
rollover_count的读取和定时器计数的读取没有同步,无法保证两者状态的一致性。
- 存在计数错误的风险:假设在读取
正确方案:方案二
- 方案二通过临界区保护
rollover_count和count2的读取操作,确保这两个操作之间不会触发ISR,避免了rollover_count与当前定时器计数的状态不一致。 - 临界区外读取的
count1用于判断“临界区启动前是否发生溢出”:如果count2 < count1,说明在count1读取后、临界区开始前发生了一次溢出,手动给rollover_count_copy加1即可补上这次计数,逻辑完全正确。 - 同时临界区仅包含必要的读取操作,耗时极短,对系统实时性影响可以忽略。
- 方案二通过临界区保护
方案三的问题
- 临界区内读取
count1和count2,虽然两次读定时器耗时很短,但完全没必要延长临界区时长。 - 更关键的是:临界区内关中断后,即使定时器发生溢出,ISR也无法触发更新
rollover_count。如果此时count2 < count1(两次读取之间刚好溢出),代码会手动给rollover_count_copy加1,而后续ISR恢复后会再给rollover_count加1,导致重复计数,最终64位计数器值错误。
- 临界区内读取
内容的提问来源于stack exchange,提问作者Gabriel Staples
相关产品推荐
相关产品推荐

