6

我正在尝试执行理论上应该可行但我开始怀疑是否在 arm-elf-gcc 的能力范围内的内存优化。请告诉我我错了。

我有一个嵌入式系统,它的主内存非常少,电池支持的 nvram 更少。我将校验和配置数据存储在 nvram 中,以便在启动时我可以验证校验和并继续之前的运行或在校验和无效时开始新的运行。在运行期间,我更新了此配置数据中各种大小的字段(这可以使校验和失效,直到稍后重新计算)。

所有这些都在物理地址空间中运行——普通 sram 映射到一个位置,而 nvram 映射到另一个位置。这就是问题所在——所有对 nvram 的访问都必须以 32 位字进行;不允许字节或半字访问(尽管在主存储器中显然没问题)。

所以我可以 a) 将所有配置数据的工作副本存储在主内存中,并在我重新计算校验和时将其 memcpy 到 nvram 或 b) 直接在 nvram 中使用它,但以某种方式说服编译器所有结构都是打包和所有访问不仅必须是 32 位对齐的,而且必须是 32位宽的

选项 a) 浪费了宝贵的主内存,我更愿意通过选项 b) 进行运行时权衡来保存它(尽管如果代码大小最终浪费的比我节省的数据大小更多)。

我希望它__attribute__ ((packed, aligned(4)))或其中的一些变体可以在这里有所帮助,但是到目前为止我所做的所有阅读和实验都让我失望了。

这是我正在处理的那种配置数据的玩具示例:

#define __packed __attribute__ ((packed))
struct __packed Foo
{
    uint64_t foo;
    struct FooFoo foofoo;
}

struct __packed Bar
{
    uint32_t something;
    uint16_t somethingSmaller;
    uint8_t evenSmaller;
}

struct __packed PersistentData
{
    struct Foo;
    struct Bar;
    /* ... */
    struct Baz;
    uint_32 checksum;
}

您可以想象不同的线程(每个线程执行函数 Foo、Bar 和 Baz)在适当的时候更新它们自己的结构,并在某个时间同步以声明是时候重新计算校验和并进入睡眠状态。

4

4 回答 4

2

避免位域它们众所周知是 C 语言的一个问题,不可靠,不可移植,随时可能更改实现。无论如何也不会帮助你解决这个问题。

工会也浮现在脑海中,但是我已经在 SO 上进行了足够多的纠正,以至于您不能使用联合来根据 C 标准更改类型。尽管正如我在另一张海报中所假设的那样,我还没有看到使用联合来更改类型不起作用的案例。破碎的位域,不断地,破碎的联合内存共享,到目前为止没有痛苦。工会不会为您节省任何内存,因此在这里实际上不起作用。

你为什么要让编译器完成这项工作?您需要在编译时使用某种链接器类型脚本,指示编译器使用掩码、移位、读取-修改-写入进行 32 位访问,对于某些地址空间,对于其他地址空间,使用更自然的字、半字和字节访问。我没有听说过 gcc 或 C 语言在语法、编译器脚本或某种定义文件中都有这样的控制。如果它确实存在,那么它的使用范围还不够广泛而无法可靠,我预计会出现编译器错误并避免它。我只是没有看到编译器这样做,当然不是以结构的方式。

对于读取,您可能会很幸运,这在很大程度上取决于硬件人员。这个 nvram 内存接口在哪里,在你公司制造的芯片内部,由其他公司制造,在芯片边缘等?像您部分描述的限制可能意味着区分访问大小或字节通道的控制信号可能会被忽略。因此,ldrb 可能会将 nvram 视为 32 位读取,并且 arm 将获取正确的字节通道,因为它认为它是 8 位读取。我会做一些实验来验证这一点,有不止一个臂内存总线,每个都有许多不同类型的传输。如果您可以查看手臂的实际功能,也许可以与硬件人员交谈或进行一些 hdl 模拟。如果你不能走这条捷径,

字大小以外的写入必须是读-修改-写。ldr, bic, shift, or, str. 不管是谁做的,你还是编译器。

自己做吧,我看不出编译器会如何为你做。包括 gcc 在内的编译器在执行您似乎认为告诉它的特定访问时有足够的时间:

*(volatile unsigned int *)(SOME_ALIGNED_ADDRESS)=some_value;

我的语法可能是错误的,因为我几年前就放弃了,但它并不总是产生一个 unsigned int 大小的存储,当编译器不想这样做时,它不会。如果它不能可靠地做到这一点,你怎么能期望它为这个变量或结构创建一种类型的加载和存储,以及为那个变量或结构创建另一种类型?

因此,如果您有需要编译器生成的特定指令,您将失败,您必须使用汇编程序,句号。特别是 ldm、ldrd、ldr、ldrh、ldrb、strd、str、strh、strb 和 stm。

我不知道您有多少 nvram,但在我看来,您的问题的解决方案是将所有内容都设为 nvram 32 位大小。您会花费一些额外的周期来执行校验和,但您的代码空间和(易失性)内存使用量是最低限度的。需要的组装非常少(如果您对此感到满意,则无需组装)。

如果您担心那么多优化,我还建议您尝试其他编译器。至少尝试 gcc 3.x、gcc 4.x、llvm 和 rvct,我认为 Keil 有一个版本(但不知道它与真正的 rvct 编译器相比如何)。

我不知道你的二进制文件有多小。如果您必须将内容打包到 nvram 中并且不能使其全部成为 32 位条目,我会推荐几个汇编程序辅助函数,一种 get32 和 put32,两种风格 get16 和 put16,以及四种风格 get8 和 put8。当您编写代码时,您将知道打包的内容,因此您可以直接编码或通过宏/定义 get16 或 put8 的风格。这些函数应该只有一个参数,因此使用它们的代码空间成本为零,性能以分支上的管道刷新的形式出现,具体取决于您的核心风格。我不知道的是,这 50 条或 100 条 put 和 get 函数指令是否会破坏您的代码大小预算?如果是这样,我想知道您是否应该使用 C。特别是 gcc。

如果尺寸很关键,你可能想用拇指代替手臂,如果你有拇指2。

我不明白你将如何让编译器为你做这件事,需要一些编译器特定的 pragma 东西,如果它存在,它可能很少使用并且有问题。

你用的是什么内核?我最近一直在使用带有 axi 总线的 arm 11 系列中的一些东西,arm 在将 ldrs、ldrbs、ldrhs 等序列转换为单独的 32 或 64 位读取方面做得非常好(是的,一些单独的指令可能会变成单个内存周期)。您可能只是根据内核的功能定制您的代码,这取决于内核以及此臂到 nvram 内存接口所在的位置。不过,我必须为此做很多模拟,我只能通过查看总线而不是从任何 arm 文档中知道这一点。

于 2010-11-02T06:33:50.363 回答
1

由于很难知道编译器可能对位域(有时甚至是联合)做什么,为了安全起见,我将创建一些函数,这些函数仅使用对齐的读/写从任意偏移量获取/设置特定大小的数据。

类似于以下(未经测试 - 甚至未编译)代码:

uint8_t nvram_get_u8( uint8_t const* p)
{
    uint32_t const* p32 = ((uintptr_t) p) & (~0x03);    // get a 32-bit aligned pointer
    int bit_offset = (((uintptr_t) p) & 0x03) * 8;      // get the offset of the byte 
                                                        //      we're interested in

    uint8_t val = ((*p32) >> bit_offset) & 0xff;

    return val;
}


void nvram_set_u8( uint8_t* p, uint8_t val)
{
    uint32_t* p32 = ((uintptr_t) p) & (~0x03);  // get a 32-bit aligned pointer
    int offset = (((uintptr_t) p) & 0x03) * 8;  // get the offset of the byte 
                                                //      we're interested in

    uint32_t tmp = *p32;

    tmp &= ~(((uint32_t) 0xff) << bit_offset);  // clear the byte we're writing
    tmp |= val << bit_offset;                   // and 'or' in the new data

    *p32 = tmp;

    return;
}

现在您可以像这样读/写类似的东西myBar.evenSmaller(假设myBar链接器/加载器已将其布置在 NVRAM 地址空间中),如下所示:

uint8_t evenSmaller = nvram_get_u8( &myBar.evenSmaller);

nvram_set_u8( &myBar.evenSmaller, 0x5a);

当然,处理较大数据类型的函数可能更复杂,因为它们可能跨越 32 位边界(如果您要打包结构以避免填充占用未使用的空间)。如果您对速度不感兴趣,他们可以在上述函数的基础上构建一次读取/写入单个字节的函数,以帮助保持这些函数的简单性。

在任何情况下,如果您有多个线程/任务同时读取写入 NVRAM,则需要同步访问以避免非原子写入损坏或导致读取损坏。

于 2010-11-02T19:54:14.107 回答
1

最简单的做法是使用联合。

typedef union something {
    struct useful {
        uint8_t one;
        uint8_t two;
    };
    struct useless {
        uint32_t size_force[1];
    };
} something;
void something_memcpy(something* main_memory, something* memory_in_nvram) {
    for(int i = 0; i < sizeof(main_memory->useless.size_force); i++) {
        memory_in_nvram->useless.size_force[i] = main_memory->useless.size_force[i];
    }
}

这只是一个例子——你可能会编写一些算术来在编译时完成以自动确定大小。根据无用成员从 NVRam 读取和写入,但始终根据“真正的”有用成员在主内存中访问它。这应该强制编译器一次读取和写入 32 位(无用结构中的数组中的每个 32 位),但仍然允许您轻松且类型安全地访问真实数据成员。

于 2010-11-01T23:59:40.550 回答
0

如果将所有内容都设为位域,则可能可以做到:

uint32_t something;
uint32_t somethingSmaller:16;
uint32_t evenSmaller:8;
uint32_t pad:8;  // not strictly necessary but will help with your sanity

但是,您的编译器可能比您聪明。您必须检查生成的程序集。

于 2010-11-01T23:59:05.560 回答