前言

在项目的开发过程中,会因为各种功能和需求实现,不断的增加代码量,所需用到的数据变量会越来越多,其中全局变量在嵌入式开发中如同“捷径”,但滥用则会变成“迷宫”。下面我们通过具体代码来剖析问题,并学习如何优化。

一、破坏代码封装:数据被随意修改

问题代码示例:

// 全局变量满天飞
int g_voltage;
int g_temperature;
char g_status;

void Task_Measurement(void) {
    g_voltage = read_adc(); // 读取电压
    g_temperature = read_temp(); // 读取温度
}

void Task_Display(void) {
    // 显示任务可以随意修改这些变量
    printf("Voltage: %d", g_voltage);
    g_temperature = 25; // 危险!其他任务可能依赖这个温度值
}

问题分析:​ 任何函数都能修改g_temperature,导致数据被意外篡改,排查困难。是否想起全巨额搜索一个一个的寻找哪一处修改了这个变量?

优化方法1:模块化封装,提供访问接口

// temperature_module.c
static int s_temperature; // 静态全局变量,仅在本文件内可见

void Temperature_Update(void) {
    s_temperature = read_temp_sensor();
}

int Temperature_Get(void) {
    return s_temperature;
}

// 禁止提供设置函数,或对接进行校验
// void Temperature_Set(int temp) { s_temperature = temp; } // 不提供此类接口
// main.c
void Task_Display(void) {
    // 只能通过接口获取温度,无法直接修改
    int current_temp = Temperature_Get();
    printf("Temp: %d", current_temp);
    // s_temperature = 25; // 编译错误!s_temperature未定义
}

优化效果:​ 数据被保护,只能通过授权接口访问,增强了封装性和安全性。

二、并发安全:多任务数据竞争

问题代码示例:

// 共享的电机控制数据
int g_motor_speed_target = 0;
int g_motor_current_speed = 0;

void Task_SpeedControl(void) { // 高优先级任务
    if (g_motor_current_speed < g_motor_speed_target) {
        // 任务在此处被抢占
        g_motor_current_speed += 1;
        set_motor(g_motor_current_speed);
    }
}

void Task_CmdParser(void) { // 低优先级任务
    if (receive_new_cmd()) {
        g_motor_speed_target = parse_speed_cmd(); // 突然修改目标值
    }
}

问题分析:Task_SpeedControl在读/改/写的过程中被Task_CmdParser打断,可能导致速度控制逻辑错乱。这个问题不是显性BUG,如果没有意识到这一点,排查起来这种隐藏BUG将会非常缓慢且痛苦。

优化方法2:使用互斥锁保护共享资源

#include "FreeRTOS.h"
#include "semphr.h"

// 使用结构体组织相关变量
typedef struct {
    int target;
    int current;
} MotorCtrl_t;

MotorCtrl_t g_motor = {0};
SemaphoreHandle_t xMotorMutex; // 互斥信号量

void Motor_Init(void) {
    xMotorMutex = xSemaphoreCreateMutex();
}

void Task_SpeedControl(void) {
    if (xSemaphoreTake(xMotorMutex, portMAX_DELAY) == pdTRUE) {
        if (g_motor.current < g_motor.target) {
            g_motor.current += 1;
            set_motor(g_motor.current);
        }
        xSemaphoreGive(xMotorMutex);
    }
}

void Task_CmdParser(void) {
    if (receive_new_cmd()) {
        if (xSemaphoreTake(xMotorMutex, portMAX_DELAY) == pdTRUE) {
            g_motor.target = parse_speed_cmd();
            xSemaphoreGive(xMotorMutex);
        }
    }
}

优化效果:​ 确保对电机数据的操作是原子的,避免了任务切换导致的数据不一致。

三、内存浪费:无效的全局变量

问题代码示例:

int g_adc_raw_value[1000]; // 巨大的全局数组
int g_display_brightness;

void Task_DataProcess(void) {
    // 仅在此函数中使用adc数组
    for (int i=0; i<1000; i++) {
        g_adc_raw_value[i] = read_adc();
    }
    process_data(g_adc_raw_value); // 处理数据
    // 此后不再使用该数组
}

问题分析:​ 巨大的数组g_adc_raw_value在整个程序生命周期都占用RAM,而它只在某个函数被短暂使用。这对于内存资源紧张的MCU来说非常的重要,一个合格的嵌入式开发工程师应该具备资源管理这一能力,需要节省不必要的资源,并及时查看.map文件,掌握具体的使用资源。

单片机的内存空间(RAM和ROM)本来就很吃紧,大多数8位或16位的单片机芯片,RAM容量可能就几十KB,全局变量在定义的时候就会一直占用内存空间,直到程序停止运行才会释放出来。如果全局变量应用得太多,内存的利用率就会大大降低,严重的时候还会出现栈溢出或者内存泄漏的问题。而且,全局变量分散存储会破坏内存空间的连续性,让CPU读取内存更加耗费时间,影响系统的实时响应速度,如果在汽车电子、医疗设备这些对实时性要求极高的场景里面,还有可能引发严重的安全事故。

优化方法3:使用局部变量或动态内存

void Task_DataProcess(void) {
    // 改为局部变量,函数结束时栈空间回收
    int adc_raw_value[1000];

    for (int i=0; i<1000; i++) {
        adc_raw_value[i] = read_adc();
    }
    process_data(adc_raw_value);
} // 数组内存被释放

// 或者使用动态内存(需谨慎管理)
void Task_DataProcess_Dynamic(void) {
    int *adc_buffer = pvPortMalloc(1000 * sizeof(int)); // FreeRTOS申请内存
    
    if (adc_buffer != NULL) {
        for (int i=0; i<1000; i++) {
            adc_buffer[i] = read_adc();
        }
        process_data(adc_buffer);
        vPortFree(adc_buffer); // 及时释放
    }
}

优化效果:​ 仅在需要时占用内存,极大提高了RAM利用率。

四、测试困难:模块无法独立测试

问题代码示例: 过参数传递依赖,而非全局变量

int g_system_mode; // 依赖全局状态

int Critical_Operation(int input) {
    if (g_system_mode == NORMAL_MODE) {
        return input * 2;
    } else {
        return input;
    }
}

// 测试该函数非常麻烦,需要先设置g_system_mode
void test_Critical_Operation(void) {
    g_system_mode = NORMAL_MODE;
    if (Critical_Operation(2) != 4) { /* 测试失败 */ }
    
    g_system_mode = SAFE_MODE;
    if (Critical_Operation(2) != 2) { /* 测试失败 */ }
}

增加测试难度,可靠性打折

嵌入式软件在完成开发之后,通常要经过单元测试、集成测试、系统测试、可靠性测试,等多个环节,而在软件里面大量使用全局变量,会让单元测试很难独立开展。模块单元测试的核心就是,把每个模块单独隔离开来,然后通过各种测试用例验证单个函数或者模块是否可靠,但如果模块之间使用了全局变量,就会让模块依赖外部数据,根本没有办法搭建纯净的开发环境。比如,要测试一个依赖全局变量“g_config_param”的函数,需要先手动初始化这个变量,如果这个全局变量被其他测试用例修改了,测试结果就会变得不准确。更加麻烦的是,全局变量引发的问题通常都很隐蔽,在进行单任务测试的时候可能根本复现不了,只有在多任务同时运行和特定时序下才会暴露出来,排查和修复起来特别棘手,从而影响软件的整体可靠性

// 将依赖项作为参数传入
int Critical_Operation(int input, SystemMode_t mode) {
    if (mode == NORMAL_MODE) {
        return input * 2;
    } else {
        return input;
    }
}

// 单元测试变得简单可靠
void test_Critical_Operation(void) {
    // 无需设置全局状态,函数是纯函数
    assert(Critical_Operation(2, NORMAL_MODE) == 4);
    assert(Critical_Operation(2, SAFE_MODE) == 2);
}

优化效果:​ 函数变为无状态的“纯函数”,测试简单可靠,不依赖外部环境。


全局变量使用准则总结

场景

不良实践

优化方案

优点

模块内部数据

全局变量g_xxx

静态变量+访问接口

数据隐藏,接口清晰

多任务共享

裸全局变量

互斥锁/队列保护

避免竞争,数据安全

大型临时数据

全局数组

局部变量/动态内存

节省内存,提高利用率

配置参数

分散的全局变量

参数结构体传递

减少耦合,易于测试

函数依赖

依赖全局状态

参数化依赖项

纯函数,易于测试

核心原则:

  1. 最小作用域原则:变量应定义在尽可能小的作用域内(局部 > 静态 > 全局)。

  2. 单一职责原则:每个变量/模块只负责一个明确的功能。

  3. 显式优于隐式:通过函数参数传递依赖,而非隐式的全局状态。

通过以上方法和准则,可以显著提高嵌入式代码的可读性、可维护性和可靠性,让代码结构更清晰,bug更易排查。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐