如何在 C 中对变化的变量(例如计时器)进行快照?

How to take a snapshot of a changing variable (such as a timer) in C?

我目前正在为 Teensy 微控制器编程,想为游戏设置暂停功能。我已经能够从 ISR 计数器溢出创建一个计时器,但是我一直无法弄清楚如何暂停这个计数器。我试过:

ISR(TIMER1_OVF_vect) {
    if (paused == 0){   
        overflow_counter ++;
    }
}

这似乎对计数器没有影响,无论我在函数中放入什么指令,计数器都会继续运行。我尝试了各种 if 语句,但它们都被忽略了——函数只是出于某种原因增加了计数器(即使我输入了 overflow_counter --)!

所以我尝试设置另一个变量,在按下暂停按钮时拍摄快照,然后当游戏未暂停时,它会拍摄另一个快照并计算差异。这将从总时间中扣除。

double snapshot_time = 0;
double get_current_time(void){
    double time = ( overflow_counter * 65536.0 + TCNT1 ) * PRESCALE  / FREQ;
    if (paused == 0 && switch_state[5] == 1){
        snapshot_time = time;
    }
    return time;
}

我尝试将 snapshot_time 设置为全局变量并使其等于 time 认为这可能会动态捕获一个静态变量,但不幸的是它没有。谁能提供一种方法来做到这一点?

你的问题隐藏了很多方面。

1. 首先要将计数器变量标记为volatile。编译器正在对变量应用不同的优化,因此它可能会将一个变量加载到寄存器中并继续使用寄存器,假设它只是存储变量内容的地方。如果一个变量是用关键字volatile声明的,那么编译器就知道它随时可能偶尔改变,因此编译器会在每次访问时重新加载and/or重写该变量。所以,它可以这样声明

volatile uint16_t overflow_counter;

paused 变量也是如此。

2. 你应该记住,如果中断没有被禁用,那么定时器中断可以发生在任何两个处理器指令之间。由于处理器是 8 位的,它使用 8 位宽的总线访问内存。也就是说,要读取 16 位数据,需要 2 条指令。假设我们将计数器值复制到局部变量中:

uint16_t counter_snapshot = overflow_counter;

局部变量会分配两个寄存器,进行两次内存读取操作。让我们想象中断发生在第一个之后,但在第二个之前。因此,在输出时,您将从它的先前值复制一半的数字,而后半部分是新的。 IE。值将被损坏。如果变量是 8 位并通过一条指令复制,则不会发生这种情况。但是如果比较宽,或者是read-modified-written,那就要注意了:

 uint8_t old_sreg = SREG; // SREG i/o register contains processor state flags, including "interrupt flag", which allows interrupt
 cli(); // clear the "interrupt flag", preventing interrupts from happening
 uint16_t counter_snapshot = overflow_counter; // safely taking the snapshot
 SREG = old_sreg; // restoring the "interrupt flag" to it's previous state. Other flags also restored but we do not care about them.

3. 如上所述,中断随时可能发生。这意味着如果您尝试同时读取 overflow_counter 和 TCNT1,则中断可能发生在两者之间,因此结果将与预期不同。特别是如果这两个值的读取被浮点乘法这样的长操作分开。因此,解决方法可能如下:

 uint8_t old_sreg = SREG; // saving state
 cli(); // locking interrupts
 // latching values we are interested in
 uint16_t latch_overflow_counter = overflow_counter;
 uint16_t latch_tcnt1 = TCNT1; 
 uint8_t latch_tifr1 = TIFR1;
 SREG = old_sreg; // restoring interrupts
 /* We are disabling interrupts, but it do not stop the timer from counting,
 therefore TCNT1 value continue changing, and timer could overflow in any time
 within that block above. But which moment exactly? Before we read TCNT1 or just after? 
 Let's assume if TCNT1 have high values then we got it's value just before the timer overflow;
 otherwise, overflow happened before that */
 if ((latch_tifr1 & (1 << TOV1)) && // we got the overflow flag set
     (latch_tcnt < 32768) { // we are within the low values
     latch_overflow_counter++; // increasing the latched value
 }

double time = ( latch_overflow_counter * 65536.0 + latch_tcnt1 ) * PRESCALE  / FREQ; // now using latched values to calculate...

顺便说一句,如果避免在不必要的地方使用浮点数,吞吐量可以大大提高。