本帖最后由 DKENNY 于 2024-12-18 15:37 编辑
#技术资源# #申请原创# @21小跑堂
前言
volatile 是一种编程语言的修饰符,用于告诉编译器某个变量的值可能会在程序的运行过程中被外部因素改变,从而阻止编译器对该变量的优化。通过使用 volatile,我们可以确保每次访问该变量时,都是从内存中读取最新的值,而不是使用可能被缓存的旧值。
在本文中,我们将深入探讨 volatile 关键字在嵌入式开发中的重要性,分析其使用场景和最佳实践,并通过具体实例来说明如何有效地应用这一关键字。
1. volatile 概述
volatile 是 C 语言中的一个关键字,主要用于告知编译器某个变量的值可能会被外部因素改变,例如硬件或多线程环境中的其他线程。使用 volatile 关键字可以防止编译器对该变量进行优化,从而确保每次访问时都能读取最新的值。
2. volatile 应用场景
在程序中,volatile变量常用于以下几种情况:
l 硬件寄存器:如并行设备的状态寄存器,可能会被外部硬件随时修改。
l 中断服务程序:在中断处理函数中访问的全局变量,这些变量在中断发生时可能会被改变。
l 多线程环境:在多个线程之间共享的变量,以确保它们能够获取到最新的值。
一般变量通常存储在内存中,但也可能存放在处理器的寄存器中。在执行过程中,只要寄存器的内容未被改变,就可以直接通过寄存器访问变量,而无需访问内存。
以下是一个定义 volatile 变量的示例:
void test()
{
volatile char temp;
}
当 temp 被声明为 volatile 类型时,编译器将不会对其进行优化,每次访问 temp 时都会重新从内存读取其值。
在编译器优化过程中,像 temp 这样的栈上变量通常不太可能被外界更改。一般来说,更容易被修改的变量是那些指针所指向的内容。通过使用 volatile,可以确保程序在访问这些变量时,总是能读取到最新的状态。
3. volatile 官方解释
文档入口:http://tigcc.ticalc.org/doc/keywords.html#volatile
原文如下:
volatile
Indicates that a variable can be changed by a background routine.
Keyword volatile is an extreme opposite of const. It indicates that a variable may be changed in a way which is absolutely unpredictable by analysing the normal program flow (for example, a variable which may be changed by an interrupt handler). This keyword uses the following syntax:
volatile data-definition;
Every reference to the variable will reload the contents from memory rather than take advantage of situations where a copy can be in a register.
翻译如下:
volatile
表示一个变量可能会被后台例程更改。
关键字 volatile 是 const 的完全对立面。它表明一个变量可能以一种无法通过分析正常程序流程预测的方式被改变(例如,可能被中断处理程序修改的变量)。该关键字的语法如下:
volatile data-definition;
对该变量的每次引用都会直接从内存中读取内容,而不是利用可能已经在寄存器中的副本。
这里的变量可以被理解为一块具有地址的内存区域。以 GPIO(通用输入输出)为例,其数据寄存器拥有一个特定的地址,通常大小为 32 位。我们可以对这个寄存器进行读写操作。
如果将 GPIO 设置为输入模式,修改 GPIO 数据寄存器的变量实际上就是对应的 GPIO 引脚。由于外部信号的变化,我们无法通过分析程序的逻辑确定 GPIO 数据寄存器中的值。因此,在每次读取时都有可能获得不同的数据。这意味着我们需要的数据可能是不一致的。
使用 volatile 关键字后,会强制编译器在每次引用 GPIO 寄存器对应的变量时,从寄存器中重新读取其值,而不是使用可能在寄存器中缓存的副本。这样可以确保我们获取到的是最新的、准确的数据,从而避免因不一致性带来的潜在错误。
在涉及可能被外部因素(如硬件或中断)改变的变量时,使用 volatile 是非常重要的,它能够确保程序在每次访问这些变量时都能读取到最新的状态。
4. volatile应用示例
4.1 实例1
在 C 语言中,编译器通常会进行优化以提高代码效率。例如:
int tmp, a1, a2;
tmp = (unsigned int*)0x4004;
a1 = *tmp;
a2 = *tmp;
在某些编译器中,这段代码可能会被优化为:
int tmp, a1, a2;
tmp = (unsigned int*)0x4004;
a1 = *tmp;
a2 = a1;
这种优化在一般情况下是有效的,但在特定情况下可能导致错误。例如,第一次读取操作 (a1 = *tmp) 后,*tmp 的内容可能已经被外部因素(如硬件)更新。在这种情况下,第二次读取 (a2 = *tmp) 可能得到与第一次不同的值。然而经过优化后的代码可能只会读出相同的值,这会导致程序的不正确行为。
为了解决这个问题,可以使用 volatile 关键字来确保每次访问变量时都从内存中读取最新的值。修改后的代码如下:
volatile unsigned int *tmp;
int a1, a2;
tmp = (volatile unsigned int*)0x4004;
a1 = *tmp;
a2 = *tmp;
继续,我们看看下面具体的这段实例代码,左边为c源码,右边是其对应的反汇编代码。
可以看到两个汇编文件相差不大,接下来我们调整优化等级。
在未使用 volatile 修饰的情况下,编译器可能会对代码进行优化。例如,考虑以下代码片段:
在优化过程中,编译器可能会判断这两个赋值是冗余的,从而直接将其优化掉。这意味着最终生成的汇编代码中,可能根本不会执行这两条语句。
如果将代码改为:
a = 1; // 控制GPIO拉高
a = 0; // 控制GPIO拉低
在没有 volatile 修饰的情况下,编译器可能也会优化掉这两行代码。这就会导致原本希望通过先将 GPIO 设置为高电平再设置为低电平以实现某种时序的意图被完全忽视。最终的结果是 GPIO 状态可能并没有按照预期改变,造成硬件调试时的困惑和错误。
通过使用 volatile 关键字,我们可以告诉编译器该变量的值可能随时会被外部因素改变,编译器不应对其进行优化。这样,每次对 GPIO 控制语句的引用都会确保从硬件寄存器中读取最新的值,避免了因为优化而导致的逻辑错误。
4.2 实例2
比如我们再看一个例子。
这里我们可以看到a.c被优化后,第一次取*a的值,是从地址0x08002000取出来的(对应汇编语句:ldr r0, [r3]),第二次就是直接使用了r0的值。这里的r0就是*a的缓存。
使用 volatile 修饰的变量会强制程序在每次访问该变量时,从内存中读取其值,而不是使用缓存中的副本。
总结
在嵌入式系统中,volatile 通常用于处理可能被外部或并行程序修改的数据。这些变量的值可能会在不经过当前程序的情况下被改变,因此使用 volatile 可以确保程序能够正确地读取到最新的数据。
5. 实际开发时,什么时候需要加volatile关键字修饰?
在实际开发中,以下几种情况通常需要使用 volatile 关键字修饰变量。
1、中断服务程序修改的全局变量
当全局变量可能在中断服务程序中被修改时,应该使用 volatile 修饰。例如:
int temp1, temp2;
int main(void)
{
temp1 = 1; // 赋值
temp2 = temp1; // 可能在这里被中断打断
// 假设在这里发生中断
}
void XXX_IQRHandler(void)
{
temp1 = 2; // 在中断中修改temp1
}
假设在 temp2 = temp1; 时发生中断,并且中断中将 temp1 修改为 2。此时,temp2 可能依然是 1,而实际 temp1 已经是 2,导致程序出现不可预知的错误。因此,在这种情况下,应该使用 volatile 修饰 temp1。
2、带实时操作系统(RTOS)的情况
在 RTOS 中,多个任务可能会相互打断。在任务间共享变量时,使用 volatile 是必要的。例如:
int temp1, temp2;
void task1(void)
{
temp1 = 1; // 赋值
temp2 = temp1; // 可能在这里被打断
}
void task2(void)
{
temp1 = 2; // 修改temp1
}
假设 task2 的优先级高于 task1,当 task1 执行到 temp2 = temp1; 时被 task2 打断,则 temp2 可能仍然是 1,而 temp1 已经被修改为 2。这同样会导致程序错误,因此应该使用 volatile 修饰 temp1。
3、读取单片机的寄存器值
在读取单片机特定寄存器时,寄存器的值可能会因为硬件操作而随时改变。此时,必须使用 volatile 来保证每次读取的都是最新的值。例如:
unsigned int temp1, temp2;
int task1(void)
{
temp1 = USART1->DR; // 读取寄存器
temp2 = temp1; // 可能在这里寄存器值已变化
}
USART1->DR 是单片机的串口数据寄存器,若在读取后其值发生变化,则 temp1 和 temp2 可能不相等,导致程序逻辑错误。因此,USART1->DR 应使用 volatile 修饰,以确保每次读取时访问的是寄存器的最新值。
6. 总结
总的来说,volatile 关键字的主要作用是告诉编译器,在处理特定变量时不要进行过度优化。它确保在每次访问这些变量时,编译器会从内存中读取最新的数据,而不是依赖于可能缓存的值。这样做的目的是为了保证程序在以下情况下能够正确运行:
l 外部因素影响:变量的值可能会被外部硬件或中断服务程序等因素改变。
l 并发操作:在多任务或多线程环境中,任务之间可能会共享变量,导致变量的值在不同的上下文中发生变化。
l 寄存器读取:在涉及直接读取硬件寄存器时,寄存器的值可能随时改变,因此需要确保每次读取都是最新的。
7. 附加问题及思考
Q. 前面说,volatile与const对立,那么这两者能够同时使用吗?如果我要用volatile和const同时修饰一个变量,那么修饰的这个变量到底是可变的还是不可变的?
分析:
l const :表示这个变量的值在程序中是不可变的,也就是说,程序的其他部分不能直接修改这个变量的值。
l volatile :表示这个变量的值可能会被外部因素(例如中断服务程序或硬件)不定期地改变。编译器在每次访问这个变量时,都会直接从内存中读取,而不是使用寄存器中的缓存值。
然而,volatile 和 const 虽然是两个不同的修饰符,它们在某种程度上确实有对立的特性,但它们确实可以用于修饰同一个变量。
如果要同时使用volatile和const修饰一个变量,可以按照以下方式声明:
变量修饰可变性讨论:
关于这个问题,我在keil进行了测试,如下是相关的关键代码,代码全局使用volatile和const修饰了同一个变量,并在main函数中对这个值进行了读取。
在debug界面时,我把currentValue(也就是*value)变量添加到了watch窗口。
在某区域打断点后,可以看到,ADC每采集一次数据时,value值是变化的,而这时,value值的获取是直接从硬件寄存器读取的。
我又做了一个测试,即直接在程序中修改这个值。
这时,会发现编译不通过,这个报错意味着编译器期望某个表达式是一个可修改的左值(lvalue),但实际提供的是一个右值(rvalue)或不可修改的表达式。当然,主要原因还是由于const限制。
结论:
在这种情况下,变量的可变性我们可得出以下结论。
l 在程序中:由于const的限制,程序的其他部分不能直接修改这个变量。因此,从程序的逻辑角度来看,这个变量是不可变的。
l 在外部因素中:由于volatile的特性,外部因素(如中断处理程序)可以改变这个变量的值。因此,从硬件或中断的角度来看,这个变量是可变的。
综上所述,当我们声明一个const volatile变量时,它在程序逻辑中是不可变的,但该变量的值可能会被外部因素改变。因此,使用const volatile变量通常用于表示 某些硬件状态或传感器值,这些值在程序中不应被修改,但可能会受到外部事件的影响。
|
C语言中volatile关键字的用法解读,通过对关键字的解释和使用案例实例,详解volatile的有效应用。