c语言volatile关键字(详细)总结附示例讲解

一、简介

volatile属于C语言的关键字。开发者告诉编译器该变量是易变的,无非就是希望编译器去注意该变量的状态,时刻注意该变量是易变的,让编译器不再去优化被volatile修饰的变量的操作,每次读取该变量的值都重新从内存中读取,但是volatile并不能做内存屏障的功能,想使用内存屏障请使用平台相关的屏障指令,比如GCC提供了一个内联asm volatile (“” : : : “memory”);的编译器屏障。
一般说来,volatile用在如下的几个地方:

1、中断服务程序中修改的供其它程序检测的变量需要加 volatile;
2、多任务环境下各任务间共享的标志应该加 volatile;
3、存储器映射的硬件寄存器通常也要加 volatile 说明,因为每次对它的读写都可能由不同意义;

二、示例代码解析

2.1 修饰变量

一个简单的打印变量程序:

#include <stdio.h>
 
void main()
{
    int i = 10;
    int a = i;
 
    printf("i = %d", a);
}

程序输出如下:

i = 10

下面采用volatile关键字修饰:

#include <stdio.h>
 
void main()
{
    int i = 10;
    int a = i;
    printf("i = %d", a);
    
//下面汇编语句的作用就是改变内存中 i 的值
//但是又不让编译器知道
    __asm {
        mov dword ptr [ebp-4], 20h
    }
    int b = i;
    printf("i = %d", b);
}

volatile 指出 i 是随时可能发生变化的,每次使用它的时候必须从 i的地址中读取,因而编译器生成的汇编代码会重新从i的地址读取数据放在 b 中。而优化做法是,由于编译器发现两次从 i读数据的代码之间的代码没有对 i 进行过操作,它会自动把上次读的数据放在 b 中。而不是重新从 i 里面读。这样以来,如果 i是一个寄存器变量或者表示一个端口数据就容易出错,所以说 volatile 可以保证对特殊地址的稳定访问。
然后,在 Debug 版本模式运行程序,输出结果如下:

i = 10
i = 32

然后,在 Release 版本模式运行程序,输出结果如下:

i = 10
i = 10

输出的结果明显表明,Release 模式下,编译器对代码进行了优化,第二次没有输出正确的 i 值。下面,我们把 i 的声明加上 volatile 关键字,看看有什么变化:

#include <stdio.h>
 
void main()
{
    volatile int i = 10;
    int a = i;
 
    printf("i = %d", a);
    __asm {
        mov dword ptr [ebp-4], 20h
    }
 
    int b = i;
    printf("i = %d", b);
}

分别在 Debug 和 Release 版本运行程序,输出都是:

i = 10
i = 32

这说明这个 volatile 关键字发挥了它的作用。其实不只是内嵌汇编操纵栈"这种方式属于编译无法识别的变量改变,另外更多的可能是多线程并发访问共享变量时,一个线程改变了变量的值,怎样让改变后的值对其它线程 visible。

2.2 修饰硬件寄存器地址

1、寄存器地址的定义:

#define UART_BASE_ADRS (0x10000000)     /* 串口的基地址 */
#define UART_RHR *(volatile unsigned char *)(UART_BASE_ADRS + 0)  /* 数据接受寄存器 */
#define UART_THR *(volatile unsigned char *)(UART_BASE_ADRS + 0)  /* 数据发送寄存器 */

2、寄存器读写操作:

UART_THR = ch; /* 发送数据 */
ch = UART_RHR; /* 接收数据 */

也可采用定义带参数宏实现

#define WRITE_REG(addr, ch) *(volatile unsigned char *)(addr) = ch
#define READ_REG(addr, ch) ch = *(volatile unsigned char *)(addr)

3、对寄存器相应位的操作方法:

定义寄存器

#define UART_LCR *(volatile unsigned char *)(UART_BASE_ADRS + 3)  /* 线控制寄存器 */

采用标准C的强制转换和指针的概念来实现访问MCU的寄存器,例如:

#define DDRB (*(volatile unsigned char *)0x25)

分析如下:
(unsigned char *)0x25中的0x25只是个值,前面加(unsigned char *)表示把这个值强制类型转换为unsigned char型的指针。再在前面加”*”,对这个0x25这个地址存放的值进行操作,然后#define相当于将0x25这个地址存放的值即*p指定为DDRB变量,这样当读/写以0x25为地址的寄存器存放的值时,直接操作DDRB即可,如下:

DDRB = 0xff;

相当于:

unsigned char *p, i;
p = 0x25;
i = *p;        //把地址为0x25单元中的数据读出送入i变量
*p = 0xff;     //向地址为0x25的单元中写入0xff

三、其他相关链接

1、C语言常用函数详细总结

2、C语言中指针、数组作为作为函数参数使用总结

3、C语言常见数据类型字节数和打印格式总结

4、C语言、Makefile和shell中添加打印调试信息总结

猜你喜欢

转载自blog.csdn.net/Luckiers/article/details/127401628