объединение упакованных данных с выравненным доступом к памяти

Я пытаюсь выполнить оптимизацию памяти, которая должна быть теоретически возможной, но я начинаю сомневаться в возможностях arm-elf-gcc. Пожалуйста, покажи мне, что я ошибаюсь.

У меня встроенная система с очень небольшим объемом основной памяти и даже меньшим количеством поддерживаемой батареей nvram. Я храню контрольные данные конфигурации в nvram, поэтому при загрузке я могу проверить контрольную сумму и продолжить предыдущий запуск или запустить новый запуск, если контрольная сумма недействительна. Во время выполнения я обновляю различные данные различных размеров в данных конфигурации (и все в порядке, что это отменяет контрольную сумму до тех пор, пока она не будет повторно пересчитана).

Все это выполняется в физическом адресном пространстве – нормальный sram отображается в одном месте, а nvram сопоставляется с другим. Вот в rub – весь доступ к 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; } 

Вы можете представить себе разные streamи (по одному для выполнения функций Foo, Bar и Baz), при необходимости обновляя свои собственные структуры и синхронизируя в какой-то момент, чтобы объявить время для пересчета контрольной суммы и переспать.

Избегайте битовых полей, которые, как известно, являются проблемой с языком C, ненадежны, не переносятся, могут быть изменены в любое время. И вообще не поможет вам в решении этой проблемы.

Профсоюзы тоже приходят на ум, но я правильно исправил SO на том, что вы не можете использовать союзы для изменения типов в соответствии со стандартами C. Хотя, как я предполагаю, с другим плакатом, я еще не видел случая, когда использование объединения для изменения типов не сработало. Сломанные битовые поля, постоянно, сломанные совместное использование памяти, до сих пор не болели. И союзы не сэкономит вам ни одного бара, поэтому здесь действительно не работает.

Почему вы пытаетесь заставить компилятор выполнить эту работу? Вам нужно будет иметь какой-то скрипт типа компоновщика во время компиляции, который инструктирует компилятор выполнять 32-битные обращения с масками, сдвигами, чтения-изменения-записи, для некоторых адресных пространств, а для других – более естественное слово, полуслово и байтов. Я не слышал о gcc или языке C, у которых такие элементы управления были в синтаксисе, или сценарий компилятора или файл определений. И если он существует, он не используется достаточно широко, чтобы быть надежным, я бы ожидал ошибок компилятора и избегал его. Я просто не вижу, чтобы компилятор делал это, конечно, не в виде структуры.

Для чтения вам может повезти, в значительной степени зависит от аппаратных людей. Где этот интерфейс памяти nvram, внутри чипа, сделанного вашей компанией, какой-то другой компанией, на краю чипа и т. Д.? Ограничение, подобное тому, которое вы описываете частично, может означать, что управляющие сигналы, которые различают размер доступа или байтовые полосы, могут игнорироваться. Таким образом, ldrb может смотреть на nvram как на 32-битное чтение, и arm возьмет правильную байтовую полосу, потому что думает, что это 8-битное чтение. Я бы сделал несколько экспериментов, чтобы убедиться в этом, есть более одной шины памяти памяти, и у каждого есть много разных типов передач. Возможно, поговорите с аппаратными специалистами или сделайте некоторые симуляции 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 бит. Вы записываете несколько дополнительных циклов, выполняющих контрольную сумму, но ваше кодовое пространство и (изменчивое) использование ПЗУ минимальны. Требуется очень маленькая assembly (или нет, если вам это удобно).

Я также рекомендую попробовать другие компиляторы, если вас беспокоит такая оптимизация. Как минимум, попробуйте gcc 3.x, gcc 4.x, llvm и rvct, которые, я думаю, есть версия, которая поставляется с Keil (но не знаю, как она сравнивается с реальным компилятором rvct).

Я не чувствую, насколько небольшой ваш двоичный файл. Если вам нужно упаковать вещи в nvram и не может сделать все 32-разрядные записи, я бы рекомендовал несколько вспомогательных функций ассемблера, один из них – get32 и put32, два варианта get16 и put16 и четыре варианта get8 и put8. Вы будете знать, когда пишете код, где вещи упакованы, поэтому вы можете напрямую или через macros / определять, какой вкус get16 или put8. Эти функции должны иметь только один параметр, поэтому с их помощью требуется нулевое кодовое пространство. Производительность в форме streamа streamа на ветке зависит от вашего аромата ядра. Что я не знаю, так это 50 или 100 инструкций по набору и получить функции, чтобы разорвать бюджет вашего размера кода? Если это так, мне интересно, нужно ли вообще использовать С. В частности, gcc.

И вы, вероятно, хотите использовать большой палец вместо руки, если размер такой критический, thumb2, если он у вас есть.

Я не вижу, как вы могли бы заставить компилятор сделать это за вас, нужно будет быть какой-то конкретной компиляционной прагмой, которая, вероятно, будет редко использоваться и глючит, если она существует.

Какое kernel ​​вы используете? Я недавно работал над чем-то в семье 11-го поколения с акси-автобусом, и arm отлично справляется с чередованием последовательностей ldrs, ldrbs, ldrhs и т. Д. В отдельные 32 или 64-битные чтения (да, несколько отдельных инструкций могут превратиться в один цикл памяти). Вы можете просто избавиться от адаптации вашего кода к функциям ядра, в зависимости от ядра и того, где лежит этот рычаг для интерфейса памяти nvram. Придется делать много симов для этого, хотя, я знаю это только, глядя на автобус не из любой документации.

Самое простое занятие – использовать союз.

 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 бита в массиве в бесполезной структуре), но все же позволяет вам легко и легко получать доступ к реальным членам данных.

Поскольку трудно понять, что компилятор может сделать с битовым полем (а иногда и с объединением), для безопасности я бы создал некоторые функции, которые получают / задают данные определенного размера из произвольных смещений, используя только выровненные чтения / записи.

Что-то вроде следующего (непроверенный – даже не скомпилированный) код:

 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; } 

Теперь вы можете читать / писать somthing как myBar.evenSmaller (предполагая, что myBar был выложен компоновщиком / загрузчиком таким образом, что он находится в адресном пространстве NVRAM) следующим образом:

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

Конечно, функции, которые имеют дело с более крупными типами данных, могут быть более сложными, поскольку они могут охватывать 32-битные границы (если вы упаковываете структуры, чтобы избежать неиспользуемого пространства, занятого заполнением). Если вы не заинтересованы в скорости, они могут использовать вышеперечисленные функции, которые одновременно читают / записывают одиночные байты, чтобы упростить эти функции.

В любом случае, если у вас есть несколько streamов / задач, читающих NVRAM одновременно, вам нужно синхронизировать обращения, чтобы избежать неатомных записей из-за повреждения или возникновения поврежденных чтений.

Возможно, вы можете сделать это, если вы сделаете все поле бит:

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

Однако вы можете перехитрить свой компилятор. Вам нужно будет проверить полученную сборку.