volatile,嵌入式中变心的“人渣”!
NOTE:先总结
你在使用volatile关键字修饰变量时,编译器就会知道这个会被修饰的变量会以一种不可预测的方式发生改变所以编译器在每次访问该变量时,都会直接从内存中读取其最新值。
NEXT:
关键字:volatile(易变的),一个C语言开发嵌入式程序中极其极其重要的关键字!!!
主要作用:告知编译器,被其修饰的变量的值可能会在程序运行过程中,以一种编译器无法预测的方式发生改变,所以编译器在对代码进行优化时,不能对该变量进行常规的优化操作。每次访问该变量时都要直接从内存中读取其真实值,而不是使用寄存器中的缓存值,以确保程序对变量的访问是实时的。
使用场景一:防止编译器优化
使用场景二:硬件寄存器访问
使用场景三:中断服务程序(ISR)中的共享变量
场景一:防止编译器优化
编译器为了提高程序的执行效率,会对代码进行各种各样的优化。
对变量的访问优化很常见,编译器在优化过程中,会假设变量的值在没有被显式修改(在源代码中直接、明确地通过赋值语句或特定操作来改变一个变量的值)的情况下是不会发生变化的。
基于这个假设,编译器会将一些频繁访问的变量缓存到寄存器中 ,这样在后续访问该变量时,就可以直接从寄存器中读取,而不需要再去内存中读取,因为寄存器的访问速度比内存快得多,这样可以提高程序的执行效率。
此外,编译器还会对代码中的一些 “冗余” 访问进行优化,例如若在一段代码中多次读取同一个变量的值,且在这期间该变量没有被修改,那么编译器可能会只读取一次该变量的值,然后在后续使用该变量的地方直接使用之前读取的值,而不再重复读取。
虽然这些优化在大多数情况下能够提升程序性能,但在嵌入式开发等环境中,可能会带来问题。因为在嵌入式系统中,变量的值很可能会因为硬件设备的状态变化、中断服务程序的执行或者多线程(支持多线程的嵌入式系统)的并发操作等原因,在编译器意想不到的情况下被修改。
如果编译器仍然按照常规的优化方式对这些变量进行处理,就会导致程序读取到的变量值不是最新的,从而引发逻辑错误。
假设有一个简单的嵌入式系统,其中有一个硬件设备的状态寄存器,我们通过一个变量来读取这个状态寄存器的值,并且在程序中需要等待这个硬件设备的状态变为某个特定值。

在这段代码中,hardware_status变量代表硬件设备的状态寄存器。wait_for_device_ready函数的作用是等待硬件设备的状态变为 1。在main函数中,我们模拟硬件设备状态被改变为 1 ,然后调用wait_for_device_ready函数。如果hardware_status没有使用volatile修饰,编译器可能会对wait_for_device_ready函数进行优化。编译器可能会认为在while循环中,hardware_status的值没有被显式修改,所以它的值不会发生变化,于是编译器可能会将hardware_status的值缓存到寄存器中,并且在循环中只从寄存器中读取hardware_status的值,而不再去内存中读取。这样一来,即使硬件设备的状态已经被改变为 1,由于编译器使用的是寄存器中的缓存值,while循环也永远不会结束,程序就会陷入死循环。

当使用volatile修饰hardware_status变量后,编译器就会知道这个变量的值可能会在程序运行过程中以一种不可预测的方式发生改变,所以编译器在每次访问hardware_status变量时,都会直接从内存中读取其最新值,而不会使用寄存器中的缓存值。这样,当硬件设备的状态被改变为 1 时,while循环能够及时检测到这个变化,从而正常结束循环,程序也能按照预期继续执行。
场景二:硬件寄存器访问
在嵌入式系统中,硬件设备与处理器之间的通信是通过硬件寄存器来实现的。
为了方便处理器对这些硬件寄存器进行访问,通常会将硬件寄存器映射到内存空间中的固定地址,这种方式被称为内存映射寄存器。
通过内存映射,处理器可以像访问普通内存单元一样访问硬件寄存器,从而实现对硬件设备的控制和状态读取。
例如,在一个简单的嵌入式系统中,可能有一个串口设备,串口设备的发送寄存器和接收寄存器被映射到内存地址 0x4000C000 和 0x4000C004。
当处理器需要向串口发送数据时,只需要将数据写入到地址 0x4000C000 ,串口设备就会自动读取该地址中的数据并发送出去;当处理器需要读取串口接收到的数据时,只需要从地址 0x4000C004 读取数据即可。
这些硬件寄存器的值是由外部硬件设备实时改变的,而不是由程序中的普通赋值操作改变的。
例如,当有新的数据到达串口时,串口硬件会自动将数据存入接收寄存器中,这个过程是不受程序直接控制的。
因此,编译器无法预测这些寄存器的值何时会发生变化,如果不使用volatile修饰这些寄存器对应的变量,编译器可能会对这些变量的访问进行优化,从而导致程序无法正确读取硬件寄存器的最新值,或者无法将数据正确写入硬件寄存器。
下面以对串口发送寄存器进行操作为例,给出使用volatile修饰和不使用volatile修饰的代码示例。
假设串口发送寄存器的地址为 0x4000C000 ,我们通过一个指针来访问这个寄存器。
下面是不使用volatile修饰的代码:

在这段代码中,uart_tx指针指向串口发送寄存器,但由于没有使用volatile修饰,编译器可能会对while (uart_tx != 0)这一行进行优化。编译器可能会认为在uart_tx = data之后,uart_tx的值不会再发生变化(因为在这段代码中没有其他显式修改uart_tx的操作),于是编译器可能会将uart_tx的值缓存到寄存器中,并且在while循环中只从寄存器中读取uart_tx的值,而不再去内存中读取真实的串口发送寄存器的值。这样一来,如果串口硬件在*uart_tx = data之后将数据发送出去并将寄存器值置为 0 ,由于编译器使用的是寄存器中的缓存值,while循环永远不会结束,程序就会陷入死循环。

场景三:中断服务程序(ISR)中的共享变量
在主循环与中断交互的过程中,经常会出现主程序和中断服务程序共享同一个变量的情况。
例如,在一个基于定时器中断的嵌入式系统中,主程序可能需要根据定时器中断的发生次数来执行某些操作,而定时器中断服务程序会在每次中断发生时修改一个表示中断次数的变量。
由于中断的发生是异步的,它可以在主程序执行的任何时刻发生,这就导致了共享变量的值可能会在主程序意想不到的情况下被改变。
如果主程序在访问这个共享变量时,编译器对其进行了优化,例如将变量值缓存到寄存器中,而在中断服务程序修改了变量的值后,主程序仍然使用寄存器中的旧值,就会导致程序逻辑错误。
因此,为了确保主程序能够实时获取到共享变量的最新值,需要使用volatile关键字来修饰这个共享变量,这样编译器就不会对其进行优化,每次访问时都会从内存中读取最新值。

浙公网安备 33010602011771号