微控制器编程中 ISR 函数中 volatile 关键字的使用
volatile keyword usage in ISR function in micro-controller programing
作为一般概念,ISR 函数中使用的全局变量 ("value") 应声明为 volatile 以避免编译器优化。但我怀疑是在ISR中调用的子函数"ISR-SUB"中使用了一个全局变量,在ISR中调用的全局变量使用的子函数是否也需要声明为volatile?
unsigned int STATUS; // -----> needs to be declared as volatile ?
void ISR-SUB()
{
STATUS = 1; -->accessed in sub function invoked in ISR which will be optimized or not
}
void ISR ()
{
ISR-SUB();
}
void main()
{
/* interrupt occurred and ISR called */
if (1 == STATUS)
{
code part
}
}
当然有。
volatile
不是 ISR 的特权,它在 C11 standard:
中有特定的定义
An object that has volatile-qualified type may be modified in ways unknown to the implementation or have other unknown side effects.
Therefore any expression referring
to such an object shall be evaluated strictly according to the rules of the abstract machine,
as described in 5.1.2.3.
Furthermore, at every sequence point the value last stored in the
object shall agree with that prescribed by the abstract machine, except as modified by the
unknown factors mentioned previously.
What constitutes an access to an object that
has volatile-qualified type is implementation-defined.
因此,每当您以无法从源中推断出的方式控制流程时(例如发生中断时),编译器就无法知道变量可能同时发生了变化。
您必须使用 volatile
来告诉它该变量随时可能发生变化。
如果这太抽象了,
考虑这个 AVR 微控制器的玩具代码:
unsigned char STATUS;
void ISR_SUB()
{
STATUS = 0x80;
}
void ISR ()
{
ISR_SUB();
}
int main()
{
unsigned char i=1;
while (STATUS & 0x80)
{
STATUS |= i;
}
return 0;
}
这个gets compiled into这个汇编代码
main:
lds r24,STATUS ;r24 = STATUS
sbrs r24,7 ;Skip next inst if bit7 of r27 is set
rjmp .L4 ;Jump to the end
.L6:
ori r24,lo8(1) ;OR r24 with 1
sbrc r24,7 ;Do the test again, break loop if bit7 set
rjmp .L6 ;Jump back to the loop
sts STATUS,r24 ;STATUS = r24
.L4:
ldi r24,lo8(0)
ldi r25,hi8(0)
ret
可以看到,变量STATUS
只被读取一次,更新到寄存器r24
,所以循环永远不会结束!
现在看看what happens when we use volatile
main:
rjmp .L8
.L6:
lds r24,STATUS ;Now status is load in r24 at each iteration ...
ori r24,lo8(1) ;... updated and ...
sts STATUS,r24 ;... stored back
.L8:
lds r24,STATUS
sbrc r24,7
rjmp .L6
ldi r24,lo8(0)
ldi r25,hi8(0)
ret
这次 STATUS
根据要求在每次迭代时读取和更新。
同步注意事项
非常感谢@Olaf 指出本节的必要性。
我在上面没有声明 volatile
是一个 充分 条件,无论 OP 试图实现什么(根本没有足够的上下文来提出任何声明) .
这个答案的解释方式是 volatile
是 必要的 条件(如上面简单的 counter-example 所示)。
如前所述,显示的代码是一个 玩具示例 旨在展示一个可能在没有 volatile
的情况下出现的简单问题。
它不是工作代码,因为实际上我 不会 工作。
在处理并发执行流时,synchronisation 是强制性的,对于 C,这可以使用 stdatomic.h
header 和函数来实现。
由于这是一个 uC 问题,stdatomic.h
可能不存在或可能不需要同步(这种情况很少见)。
只是为了避免任何误解:如果你有 stdatomic.h
然后 使用它 (它最终将编译为空,但它使代码可移植)。
上面的例子包含一个非原子的 RMW 操作(|=
)因此可以取消 ISR 所做的更新。
所以是的,你确实需要(至少)volatile
。
易变:
这个关键字在我们做嵌入式硬件系统的时候一般都会用到,因为声明的变量可能不仅仅被我们的C代码修改,而是可以被我们的嵌入式硬件系统修改
例如,如果我们选择将我们的变量地址映射为我们外部嵌入式硬件系统地址的一部分,那么我们选择作为编译器(例如 GCC)的选项来对代码。
如果我们轮询这个未声明为 volatile 的变量的值来检查它是否变为 1 会发生什么,实际上我们的编译器会看到我们正在检查一个始终具有相同地址的变量的值,所以它会将我们的非易失性变量的初始值复制到一个临时的内部寄存器中,以便于检查,而不是每次都花时间去读取我们的硬件地址的内容。
但是,如果我们的嵌入式硬件系统在这种情况下修改了这个硬件变量地址的内容,我们将永远无法弄清楚,因为编译器正在从临时寄存器而不是变量的硬件地址读取,在这里我们可以弄清楚关键字 volatile 有多重要。
是的。
volatile
的作用是告诉编译器,这个变量的值可能在它不知情的情况下随时被读取或写入。对于普通变量,编译器假定它具有有关如何以及何时更改的完整信息。
考虑将此功能与您的 ISR 搭配使用:
void normal_function()
{
STATUS = 3;
// ... some more code ...
if (STATUS != 3)
// do something
}
如果 STATUS
没有被标记为 volatile
那么允许编译器将 STATUS
的值保存在引用它的两个语句之间的寄存器中,甚至可以假设if 语句永远不会触发。
更常见的模式是
while (STATUS != 1)
// do something
您期望 ISR 将 STATUS
设置为 1 并停止 while
循环。然而,允许编译器读取它一次,将该值保存在寄存器中并且永远不会再次读取它。它甚至可能只测试一次。
补题是
void normal_function()
{
STATUS = 3;
while (!hell_frozen)
// ... lots of code not involving STATUS...
STATUS = STATUS + 1;
}
这里允许编译器直到函数结束才真正写入STATUS的值。只要它跟踪它的值应该是多少,它就可以推迟写入内存位置。如果您的 ISR 正在等待 STATUS
变为 3,那将是一个问题。
作为一般概念,ISR 函数中使用的全局变量 ("value") 应声明为 volatile 以避免编译器优化。但我怀疑是在ISR中调用的子函数"ISR-SUB"中使用了一个全局变量,在ISR中调用的全局变量使用的子函数是否也需要声明为volatile?
unsigned int STATUS; // -----> needs to be declared as volatile ?
void ISR-SUB()
{
STATUS = 1; -->accessed in sub function invoked in ISR which will be optimized or not
}
void ISR ()
{
ISR-SUB();
}
void main()
{
/* interrupt occurred and ISR called */
if (1 == STATUS)
{
code part
}
}
当然有。
volatile
不是 ISR 的特权,它在 C11 standard:
An object that has volatile-qualified type may be modified in ways unknown to the implementation or have other unknown side effects.
Therefore any expression referring to such an object shall be evaluated strictly according to the rules of the abstract machine, as described in 5.1.2.3.Furthermore, at every sequence point the value last stored in the object shall agree with that prescribed by the abstract machine, except as modified by the unknown factors mentioned previously.
What constitutes an access to an object that has volatile-qualified type is implementation-defined.
因此,每当您以无法从源中推断出的方式控制流程时(例如发生中断时),编译器就无法知道变量可能同时发生了变化。
您必须使用 volatile
来告诉它该变量随时可能发生变化。
如果这太抽象了, 考虑这个 AVR 微控制器的玩具代码:
unsigned char STATUS;
void ISR_SUB()
{
STATUS = 0x80;
}
void ISR ()
{
ISR_SUB();
}
int main()
{
unsigned char i=1;
while (STATUS & 0x80)
{
STATUS |= i;
}
return 0;
}
这个gets compiled into这个汇编代码
main:
lds r24,STATUS ;r24 = STATUS
sbrs r24,7 ;Skip next inst if bit7 of r27 is set
rjmp .L4 ;Jump to the end
.L6:
ori r24,lo8(1) ;OR r24 with 1
sbrc r24,7 ;Do the test again, break loop if bit7 set
rjmp .L6 ;Jump back to the loop
sts STATUS,r24 ;STATUS = r24
.L4:
ldi r24,lo8(0)
ldi r25,hi8(0)
ret
可以看到,变量STATUS
只被读取一次,更新到寄存器r24
,所以循环永远不会结束!
现在看看what happens when we use volatile
main:
rjmp .L8
.L6:
lds r24,STATUS ;Now status is load in r24 at each iteration ...
ori r24,lo8(1) ;... updated and ...
sts STATUS,r24 ;... stored back
.L8:
lds r24,STATUS
sbrc r24,7
rjmp .L6
ldi r24,lo8(0)
ldi r25,hi8(0)
ret
这次 STATUS
根据要求在每次迭代时读取和更新。
同步注意事项
非常感谢@Olaf 指出本节的必要性。
我在上面没有声明 volatile
是一个 充分 条件,无论 OP 试图实现什么(根本没有足够的上下文来提出任何声明) .
这个答案的解释方式是 volatile
是 必要的 条件(如上面简单的 counter-example 所示)。
如前所述,显示的代码是一个 玩具示例 旨在展示一个可能在没有 volatile
的情况下出现的简单问题。
它不是工作代码,因为实际上我 不会 工作。
在处理并发执行流时,synchronisation 是强制性的,对于 C,这可以使用 stdatomic.h
header 和函数来实现。
由于这是一个 uC 问题,stdatomic.h
可能不存在或可能不需要同步(这种情况很少见)。
只是为了避免任何误解:如果你有 stdatomic.h
然后 使用它 (它最终将编译为空,但它使代码可移植)。
上面的例子包含一个非原子的 RMW 操作(|=
)因此可以取消 ISR 所做的更新。
所以是的,你确实需要(至少)volatile
。
易变:
这个关键字在我们做嵌入式硬件系统的时候一般都会用到,因为声明的变量可能不仅仅被我们的C代码修改,而是可以被我们的嵌入式硬件系统修改
例如,如果我们选择将我们的变量地址映射为我们外部嵌入式硬件系统地址的一部分,那么我们选择作为编译器(例如 GCC)的选项来对代码。 如果我们轮询这个未声明为 volatile 的变量的值来检查它是否变为 1 会发生什么,实际上我们的编译器会看到我们正在检查一个始终具有相同地址的变量的值,所以它会将我们的非易失性变量的初始值复制到一个临时的内部寄存器中,以便于检查,而不是每次都花时间去读取我们的硬件地址的内容。
但是,如果我们的嵌入式硬件系统在这种情况下修改了这个硬件变量地址的内容,我们将永远无法弄清楚,因为编译器正在从临时寄存器而不是变量的硬件地址读取,在这里我们可以弄清楚关键字 volatile 有多重要。
是的。
volatile
的作用是告诉编译器,这个变量的值可能在它不知情的情况下随时被读取或写入。对于普通变量,编译器假定它具有有关如何以及何时更改的完整信息。
考虑将此功能与您的 ISR 搭配使用:
void normal_function()
{
STATUS = 3;
// ... some more code ...
if (STATUS != 3)
// do something
}
如果 STATUS
没有被标记为 volatile
那么允许编译器将 STATUS
的值保存在引用它的两个语句之间的寄存器中,甚至可以假设if 语句永远不会触发。
更常见的模式是
while (STATUS != 1)
// do something
您期望 ISR 将 STATUS
设置为 1 并停止 while
循环。然而,允许编译器读取它一次,将该值保存在寄存器中并且永远不会再次读取它。它甚至可能只测试一次。
补题是
void normal_function()
{
STATUS = 3;
while (!hell_frozen)
// ... lots of code not involving STATUS...
STATUS = STATUS + 1;
}
这里允许编译器直到函数结束才真正写入STATUS的值。只要它跟踪它的值应该是多少,它就可以推迟写入内存位置。如果您的 ISR 正在等待 STATUS
变为 3,那将是一个问题。