如何避免系统设置的 EEPROM 结构中的全局变量?

How to avoid globals in EEPROM structs for system settings?

我正在努力从 EEPROM 获取系统设置并试图避免将它们作为全局变量,并且想知道普遍的智慧是什么以及是否有公认的做法和/或优雅的解决方案。

我通过一些错误检查的结构和 main.c 中的 sizeof 运算符将系统设置存储在 EEPROM 中,如下所示:

// EEPROM data structures
typedef struct system_tag
{
    uint8_t     buzzer_volume;
    uint8_t     led_brightness;
    uint8_t     data_field_3;
} system_t;

typedef struct counters_tag
{
    uint16_t    counter_1;
    uint16_t    counter_2;
    uint16_t    counter_3;
} counters_t;

typedef struct eeprom_tag
{
    system_t    system_data;
    uint8_t     system_crc;
    counters_t  counters;
    uint8_t     counters_crc;
} eeprom_t;

// Default values
static system_t system_data =
{
    .buzzer_volume = 50,
    .led_brightness = 50,
    .data_field_3 = 30
};

static counters_t counter =
{
    .counter_1 = 0,
    .counter_2 = 0,
    .counter_3 = 0
};

// Get system settings data from the EEPROM
if (EEPROM_check_ok(EEPROM_BASE_ADDRESS, sizeof(system_t)))
{
        eeprom_read_block(&system_data, (uint16_t *) EEPROM_BASE_ADDRESS, sizeof(system_t));
}

if (EEPROM_check_ok((EEPROM_BASE_ADDRESS + offsetof(eeprom_t, counters)), sizeof(counters_t)))
{
        eeprom_read_block(&counter, (uint16_t *) EEPROM_BASE_ADDRESS, sizeof(counters_t));
}

我现在正在使用系统设置数据来设置不同模块中的其他变量。例如。在另一个文件 buzzer.c 中,我有一个模块静态变量(为了避免全局变量)和访问函数来尝试提供一些封装:

// Current volume setting of the buzzer
static uint8_t volume = 50;

void BUZZER_volume_set(uint8_t new_volume)
{
    volume = new_volume;
}

uint8_t BUZZER_volume_get(void)
{
    return (volume);
}

我觉得问题是我现在有不必要的重复数据,因为当我从系统数据中传递 buzzer_volume 来设置蜂鸣器模块中的静态音量变量时,事情可能会不同步.将系统设置作为全局变量很容易,但我知道这是不受欢迎的。

有没有更优雅的方式来做到这一点而不使用全局变量并且仍然有一些封装?

如有任何建议,我们将不胜感激。

Jack Ganssle 的优秀文章“A Pox on Globals”给出了避免使用全局变量的一般建议(以及为什么需要这样做)。基本阅读。

一个解决方案是简单地在 main.c 中使用访问器函数(或者更好的是一个单独的 nvdata.c,以保护它不被 任何东西 直接访问)。

与其依赖于在访问数据之前调用的单个初始化函数,我建议使用 "initialise on first use" 语义:

const system_t* getSystemData()
{
    static bool initialised = false ;
    if( !initialised )
    {
        eeprom_read_block( &system_data, 
                           (uint16_t*)EEPROM_BASE_ADDRESS, 
                           sizeof(system_t) ) ;
        initialised = true ;
    }

    return &system_data ;
}

void setSystemData( const system_t* new_system_data )
{
    system_data = *new_system_data ;

    eeprom_write_block( &system_data, 
                        (uint16_t*)EEPROM_BASE_ADDRESS, 
                        sizeof(system_t));
}

然后在buzzer.c:

uint8_t BUZZER_volume_get(void)
{
    return getSystemData()->buzzer_volume ;
}

void BUZZER_volume_set( uint8_t new_volume )
{
    system_t new_system_data = *getSystemData() ;
    new_system_data.buzzer_volume = new_volume ;
    setSystemData( &new_system_data ) ;
}

这有一些问题 - 例如,如果您的结构很大,更新单个成员可能会很昂贵。但是,这可以解决,但在您的应用程序中可能不是问题。

另一个问题是在每次更改时写回 EEPROM - 如果您对同一结构进行多次连续更改,这可能会导致 EEPROM 不必要的抖动并使您的程序停滞很长一段时间。在那种情况下,一个简单的方法是有一个单独的提交操作:

void setSystemData( const system_t* new_system_data )
{
    system_data = *new_system_data ;
    system_data_commit_pending = true ;
}

void commitSystemData()
{
    if( system_data_commit_pending )
    {
        eeprom_write_block( &system_data, 
                            (uint16_t*)EEPROM_BASE_ADDRESS, 
                            sizeof(system_t));
    }
}

只有在必要或安全时才提交数据 - 例如在受控关闭或明确选择 UI "save settings" 操作时。

一个更复杂的方法是在更改时设置一个计时器,并在计时器到期时调用提交函数,每个 "set" 都会重新启动计时器,因此提交只会发生在 "quiet"期间。这种方法特别适合多线程的解决方案。