将打包数据与对齐的内存访问相结合

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

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

所有这些都在物理地址空间中运行 – 正常的sram映射在一个位置,nvram映射到另一个位置。 这是擦除 – 所有对nvram的访问必须以32位字进行; 不允许字节或半字访问(虽然它在主存中显然很好)。

因此,我可以a)将所有配置数据的工作副本存储在主存储器中,并在重新计算校验和时将其存储到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函数)根据需要更新自己的结构,并在某个时刻同步以声明重新计算校验和并进入hibernate状态的时间。

避免使用位域,众所周知它们是C语言的问题,不可靠,不可移植,随时可能会在实现中发生变化。 无论如何,不​​会帮助你解决这个问题。

工会也会浮现在脑海中,但我已经在SO上做了足够多的纠正,你不能根据C标准使用工会改变类型。 虽然正如我在另一张海报中所假设的那样,我还没有看到使用联合改变类型的情况并没有奏效。 破碎的bitfields,不断,破碎的联盟记忆共享,到目前为止没有痛苦。 工会不会救你任何公羊因此在这里真的没有用。

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

对于读取,您可能会很幸运,在很大程度上取决于硬件人员。 这个nvram存储器接口在哪里,在贵公司制造的芯片内部,由其他公司制造,在芯片的边缘等等? 类似于您部分描述的限制可能意味着可以忽略区分访问大小或字节通道的控制信号。 因此,ldrb可能会将nvram视为32位读取,并且arm将获取正确的字节通道,因为它认为它是8位读取。 我会做一些实验来validation这一点,有多个臂内存总线,每个都有许多不同类型的传输。 也许可以与硬件人员交谈或者进行一些hdl模拟,如果你有可以看到arm正在做什么。 如果您不能使用此快捷方式,则无论您如何让编译器执行此操作,读取都将成为具有可能的掩码和移位的ldr。

除了字大小以外的写入必须是读 – 修改 – 写。 ldr,bic,shift,或str。 无论是谁,你或编译器。

只是自己动手,我看不出编译器会如何为你做这件事。 包括gcc在内的编译器有足够的时间执行您认为正在告诉它的特定访问:

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

我的语法可能是错误的,因为我多年前就提出过这个问题,但它并不总是产生一个无符号的int大小的存储,当编译器不想这样做时,它就不会。 如果它不能可靠地做到这一点你怎么能期望它为这个变量或结构创建一种加载和存储的味道,以及为该变量或结构创建另一种味道?

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

我不知道你有多少nvram,但在我看来,你的问题的解决方案是使nvram中的所有内容都是32位。 您刻录了一些执行校验和的额外周期,但您的代码空间和(易失性)ram使用率最低。 需要非常少的assembly(如果您对此感到满意,则无需assembly)。

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

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

你可能想用拇指代替arm,如果尺寸是关键的话,拇指2如果你有它。

我不知道如何让编译器为你做这件事,需要一些编译器特定的pragma事情,如果它存在的话很可能很少使用和bug。

你用的是什么核心? 我最近一直在使用axi总线处理11系列中的某些东西,并且arm可以很好地将ldrs,ldrbs,ldrhs等序列转换为单独的32位或64位读取(是的,一些单独的指令可能会变成单个记忆周期)。 您可能只是将代码定制到核心的function,具体取决于核心以及此臂到nvram内存接口的位置。 虽然我不得不做很多sims,我只是通过查看总线而不是从任何arm文档中知道这一点。

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

 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位),但仍然允许您轻松地,类型安全地访问真实数据成员。

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

类似下面的内容(未经测试 – 甚至未编译)代码:

 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.evenSmaller (假设myBar已被链接器/加载器布局,使得它在NVRAM地址空间中),如下所示:

 uint8_t evenSmaller = nvram_get_u8( &myBar.evenSmaller); nvram_set_u8( &myBar.evenSmaller, 0x5a); 

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

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

如果你把所有东西都变成了一个点,你可以做到这一点:

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

但是,您的编译器可能会超出您的要求。 您必须检查生成的程序集。