在ARM开发中 volatile与const关键字的关键用途

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:**主要关乎编译优化。它告诉编译器:"不要对这个变量做任何假设,每次都要老老实实从内存地址读取/写入"。

相关推荐
檀越剑指大厂10 小时前
【Elasticsearch系列廿】Logstash 学习
大数据·学习·elasticsearch
java干货11 小时前
如何让 iPhone 用上 Type-C 充电器?适配器模式详解
c语言·iphone·适配器模式
woodykissme12 小时前
渐开线圆柱齿轮几何计算全解析(一):从理论到实践的完整指南
学习·齿轮·齿轮加工
代码改善世界12 小时前
C语言项目实战:学生成绩管理系统(支持登录注册、随机考试、分数区间统计)
c语言·网络·课程设计
Asher阿舍技术站13 小时前
【AI基础学习系列】四、Prompt基础知识
人工智能·学习·prompt
CappuccinoRose14 小时前
CSS 语法学习文档(十三)
前端·css·学习·postcss·模块化·预处理器
im_AMBER14 小时前
Leetcode 121 翻转二叉树 | 二叉树中的最大路径和
数据结构·学习·算法·leetcode
『往事』&白驹过隙;15 小时前
浅谈PC开发中的设计模式搬迁到ARM开发
linux·c语言·arm开发·设计模式·iot
じ☆冷颜〃15 小时前
随机微分层论:统一代数、拓扑与分析框架下的SPDE论述
笔记·python·学习·线性代数·拓扑学