const 和 volatile 是两个极其重要的关键字。它们不仅是语法规则,更是与硬件交互、保证程序正确运行(尤其是在涉及编译器优化时)的关键。
volatile关键字
volatile 告诉编译器,这个变量的值可能在程序的控制流之外被意外改变。这迫使编译器每次使用该变量时都直接从内存地址读取,而不是使用保存在寄存器中的副本(即禁止优化)。在ARM开发中,如果发现程序在开启编译器优化(如 -O2)后工作异常,而在关闭优化(-O0)时工作正常,通常就是因为遗漏了必要的 volatile 关键字。
在嵌入式系统中,如果不使用 volatile,以下三种情况会导致严重且难以调试的 Bug:
①访问内存映射的外设寄存器
这是最常见的用途。硬件寄存器的值会随着硬件状态的变化而改变,或者写入寄存器代表着硬件动作。
②在中断服务程序(ISR)中修改的全局变量
如果在主循环中使用一个由中断设置的标志变量,必须加 volatile。
③在多线程/多任务系统中(RTOS)
与中断类似,如果一个变量在两个任务之间共享,并且编译器无法推断它会被另一个任务修改,则需要使用 volatile(尽管在多核系统中通常需要配合内存屏障,但 volatile 是基础)。

防止编译器过度优化
有这样一种情况,如果希望使用非系统提供的延时函数,自实现一个让CPU干等的延时
cpp
void my_delay(){
int i, j;
for(i = 0; i < 20000; i++)
for(j = 0; j < 20000; j++);
}
int main(int argc, char** argv){
printf("off\n");
my_delay();
printf("on\n");
my_delay();
return 0;
}
程序的本意是希望让off执行一段时间,然后再on执行一段时间,但是由于自实现的延迟函数,在编译器看来,是无作为的,于是编译器会为了提高程序的效率,把它优化掉,让off执行紧接着on执行,然后程序结束。
编译器优化的其中两点:1.把常用的变量存放在寄存器中 2.去除没用的代码优化循环结构
这种情况下可以使用,volatile关键字来防止编译器过度优化程序。
cpp
void my_delay(){
volatile int i, j;
for(i = 0; i < 20000; i++)
for(j = 0; j < 20000; j++);
}
int main(int argc, char** argv){
printf("off\n");
my_delay();
printf("on\n");
my_delay();
return 0;
}
(这样一来可以防止变量被编译器优化,使得延时起作用)
如果不加 volatile:编译器可能会认为 while 循环在反复检查同一个地址,而该地址的值没有被程序修改。为了效率,编译器可能优化成:把*status_reg的值读入寄存器,然后不断检查寄存器的值。这样,即使硬件修改了内存地址的值,循环也可能永远无法退出,因为它检查的是旧值的副本。
如果加了 volatile:编译器每次循环都会生成一条读取内存的指令,直接去物理地址 0x40021000 取值。
cpp
// 假设这是一个状态寄存器,地址为 0x40021000
// 如果不加 volatile
uint32_t *status_reg = (uint32_t *)0x40021000;
while (*status_reg == 0) {
// 等待硬件将状态位从 0 变为 1
}
/*=============================================================*/
// 定义一个指向 volatile 无符号整型的指针
#define STATUS_REG ((volatile uint32_t *)0x40021000)
while (*STATUS_REG == 0) {
// 等待...
}
中断服务程序
如果不加 volatile:编译器可能看到主循环中没有代码修改 data_ready_flag,于是优化成:将 data_ready_flag 的值缓存在寄存器中,主循环只检查这个寄存器。中断修改了内存中的 data_ready_flag,但寄存器中的值没变,导致主循环永远无法触发 if 条件。
cpp
volatile uint8_t data_ready_flag = 0;
// 中断服务程序
void USART_IRQHandler(void) {
// ... 接收数据 ...
data_ready_flag = 1; // 告诉主循环数据已准备好
}
// 主循环
int main(void) {
while(1) {
if (data_ready_flag) {
// 处理数据
data_ready_flag = 0;
}
}
}
多线程
多线程的环境中,编译器同样会做一些优化,下面的程序,主线程负责创建子线程和回收子线程,而子线程负责修改主线程死循环的条件。
cpp
int flag = 1;
void * my_thread(void* argp){
sleep(1);
flag = 0;
}
int main(int argc, char** argv){
pthread_t tid;
pthread_create(&tid, NULL, my_thread, NULL);
while(flag);
pthread_join(tid, NULL);
printf("end\n");
return 0;
}
初衷是希望,1秒后打印end并结束,但是被编译器优化了,直接变成死循环,程序无法结束,子线程把变量修改了,主线程却察觉不到,因为编译器优化后,程序为了提高运行效率,可能从缓存或者寄存器中读取变量数据,读到的还是没有修改的数据。
cpp
volatile int flag = 1;
void * my_thread(void* argp){
sleep(1);
flag = 0;
}
int main(int argc, char** argv){
pthread_t tid;
pthread_create(&tid, NULL, my_thread, NULL);
while(flag);
pthread_join(tid, NULL);
printf("end\n");
return 0;
}
此时,程序如初衷一样,使用volatile让程序每次都从内存中直接读取该变量,即可解决这个问题。一旦数据被其他线程修改,可以立刻读取新值处理。
const关键字
const 用于定义一个变量不应该被改变 ,或者说是只读的。在ARM开发中,它的主要用途如下:
①将数据存储在 ROM/Flash 中(节省 RAM)
这是MCU开发中最实际的用途。RAM(内存)通常非常有限,而 Flash(程序存储空间)相对较大。如果在函数内部定义一个大的字符串或常量数组,如 char str[] = "Hello",它通常会被复制到 RAM 中。如果加上 const,如 const char str[] = "Hello",编译器会将这个数组直接存放在 Flash(代码区)。程序运行时直接从 Flash 读取,不会占用宝贵的 RAM 空间。
②保护被指针访问的硬件寄存器
在驱动开发中,可能会传递指向硬件寄存器的指针。如果不希望函数内部修改这些寄存器的值,可以使用 const 修饰。
③定义不可变的系统参数
定义采样率、波特率、版本号等运行时不应该被修改的全局变量。
④优化函数接口
在传递大型结构体给函数时,为了提高效率,通常使用指针传递(避免复制整个结构体)。但如果不想让函数修改结构体内容,使用 const 指针是最佳实践。
cpp
// 假设有一个函数,只读取外设的状态,不应该修改寄存器
void print_status(const uint32_t *status_reg) {
// *status_reg = 10; // 编译错误!因为参数是 const 的
uint32_t value = *status_reg; // 只能读取
printf("Status: %lu\n", value);
}
// 定义在 Flash 中的常量
const float ADC_REFERENCE_VOLTAGE = 3.3f;
const uint32_t SYSTEM_CLOCK_HZ = 72000000UL;
typedef struct {
int x;
int y;
} Point;
void draw_point(const Point *p) {
// p->x = 10; // 编译错误,防止意外修改
// 执行绘制操作,使用 p->x 和 p->y
}
**联合使用:**const volatile 这是一个常见的组合,尤其在标识硬件状态寄存器时。
含义: 程序员不能(不应该)去修改它(const),但它的值会由硬件自动改变(volatile)。
例子: 读取一个系统的运行时间计数器(Tick Counter),该计数器由硬件定时器递增,软件只能读不能写。
cpp
// 假设硬件滴答计数器位于地址 0xE0001004
// 它是只读的(const),但会自动变化(volatile)
const volatile uint32_t *system_tick = (const volatile uint32_t *)0xE0001004;
uint32_t current_tick = *system_tick; // OK:读取操作
// *system_tick = 100; // 编译错误:这里不能赋值,因为它是 const
**const:**主要关乎存储位置(RAM vs Flash)和代码安全(防止意外修改)。它告诉程序员和编译器:"这是个只读变量"。
**volatile:**主要关乎编译优化。它告诉编译器:"不要对这个变量做任何假设,每次都要老老实实从内存地址读取/写入"。