Начальные условия
Есть устройство на базе микроконтроллера (для примера будет взят stm32f405rgt6). При включении оно настраивает свою периферию на основе предпочтений пользователя или настроек по-умолчанию. Пользователь может менять настройки во время работы устройства (как правило, только во время интеграции в комплекс) через один из возможных интерфейсов (CLI меню или утилита установки параметров работы, работающая через бинарный протокол). После установки параметров пользователь сохраняет настройки специальной командой (так же через один из возможных интерфейсов).
Задача
- Для снижения времени обращения к переменным настроек устройства во время работы требуется держать актуальные значения в RAM (обычно объем таких данных варьируется от десятка переменных до 3 килобайт в зависимости от устройства).
- Необходимо хранить настройки пользователя в двух экземплярах во flash микроконтроллера.
- Каждый экземпляр настроек пользователя должен оканчиваться CRC32, лежащей сразу после полезных данных
- Под каждый экземпляр настроек пользователя используется отдельная страница во flash памяти (даже если полезных данных 2 кб, а страницы делятся по 128 кб, то вся страница отдается под один блок)
- В коде программного обеспечения микроконтроллера должны храниться настройки по-умолчанию, которыми должны будут стать блоки настроек пользователя, если в обоих будут поврежденные данные
Попытки решения
В GCC (на момент написания статьи) отсутствует флаг для получения копии секции. Дополнить LD скрипт <<магическими строками>> так же не удастся. Можно было бы поработать с objcopy, но этот подход очень неявный и ведет к трудно уловимым ошибкам.
Решение
Решение заключается в создании в коде пользователя неявных копий нужной сущности (переменной, структуры, массива и т.д.) с последующим расположением их в памяти.
Создание макроса для скрытого создания копий структур
Создадим макрос, с помощью которого будем резервировать место под структуру в RAM, 2 экземплярах flash-памяти на отдельных страницах и в коде пользователя (под начальные значения).
#define USER_CFG_DATA_STRUCT(TYPE,NAME,...) __attribute__((aligned(4), section (".user_cfg_data_ram_page"))) TYPE NAME = __VA_ARGS__; __attribute__((aligned(4), section (".user_cfg_data_flash_default_page"))) TYPE flash_default_page_##NAME = __VA_ARGS__; __attribute__((aligned(4), section (".user_cfg_data_flash_page_1"))) TYPE flash_page_1_##NAME = __VA_ARGS__; __attribute__((aligned(4), section (".user_cfg_data_flash_page_2"))) TYPE flash_page_2_##NAME = __VA_ARGS__;
Использовав макрос будет создано 4 экземпляра структуры с разными именами. В коде проекта (за исключением системы работы с страницами памяти настроек пользователя) все модули должны использовать структуру без приставки (заданное в макросе имя), которая будет находиться в RAM (ее можно будет изменять модулям, которые отвечают за взаимодействие с пользователем. Остальные должны только читать из нее).
Пример использования макроса в коде пользователя:
typedef struct _test_st {
uint32_t a1;
uint32_t a2;
} test_st_t;
USER_CFG_DATA_STRUCT(test_st_t, name_st, {
.a1 = 1,
.a2 = 2
})
После экземпляр структуры name_st доступен из кода проекта.
Создание макросов для скрытого создания копий других сущностей
Для создания переменной потребуется просто сделать подстановку макроса создания структур.
Для массивов же — добавить количество элементов.
#define USER_CFG_DATA_VAR USER_CFG_DATA_STRCUT
#define USER_CFG_DATA_ARRAY(TYPE,NAME,SIZE,...) __attribute__((aligned(4), section (".user_cfg_data_ram_page"))) TYPE NAME[SIZE] = __VA_ARGS__; __attribute__((aligned(4), section (".user_cfg_data_flash_default_page"))) TYPE flash_default_page_##NAME[SIZE] = __VA_ARGS__; __attribute__((aligned(4), section (".user_cfg_data_flash_page_1"))) TYPE flash_page_1_##NAME[SIZE] = __VA_ARGS__; __attribute__((aligned(4), section (".user_cfg_data_flash_page_2"))) TYPE flash_page_2_##NAME[SIZE] = __VA_ARGS__;
Принцип использования аналогичен использованию структуры.
Доработка LD скрипта
При создании копий сущностей было указано, в каких секциях они лежат. Теперь следует создать эти секции в LD скрипте. Для F4 дополненный LD скрпит от ST будет выглядеть так:
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K - 256K /* 256 КБ для user_cfg */
USER_CFG_PAGE_1 (rx) : ORIGIN = 0x080C0000, LENGTH = 128K - 4 /* 4 Байта CRC32 */
USER_CFG_PAGE_2 (rx) : ORIGIN = 0x080E0000, LENGTH = 128K - 4 /* 4 Байта CRC32 */
CCM (xrw) : ORIGIN = 0x10000000, LENGTH = 64K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
}
/* Записывается размер с учетом поля под CRC32.
А компоновщик работает без него. Чтобы если структура не залезет - предупредить. */
user_cfg_data_flash_page_1_size = LENGTH(user_cfg_PAGE_1) + 4;
user_cfg_data_flash_page_2_size = LENGTH(user_cfg_PAGE_2) + 4;
user_cfg_data_flash_page_size = user_cfg_data_flash_page_1_size;
/*
* The '__stack' definition is required by crt0, do not remove it.
*/
__stack = ORIGIN(RAM) + LENGTH(RAM);
_estack = __stack; /* STM specific definition */
/*
* Default stack sizes.
* These are used by the startup in order to allocate stacks
* for the different modes.
*/
__Main_Stack_Size = 1024 ;
PROVIDE ( _Main_Stack_Size = __Main_Stack_Size ) ;
__Main_Stack_Limit = __stack - __Main_Stack_Size ;
/*"PROVIDE" allows to easily override these values from an object file or the command line. */
PROVIDE ( _Main_Stack_Limit = __Main_Stack_Limit ) ;
/*
* There will be a link error if there is not this amount of
* RAM free at the end.
*/
_Minimum_Stack_Size = 512 ;
/*
* Default heap definitions.
* The heap start immediately after the last statically allocated
* .sbss/.noinit section, and extends up to the main stack limit.
*/
PROVIDE ( _Heap_Begin = _end_noinit ) ;
PROVIDE ( _Heap_Limit = __stack - __Main_Stack_Size ) ;
SECTIONS
{
.isr_vector :
{
KEEP(*(.isr_vector)) /* Interrupt vectors */
KEEP(*(.cfmconfig)) /* Freescale configuration words */
*(.after_vectors .after_vectors.*) /* Startup code and ISR */
. = ALIGN(4);
} >FLASH
.inits :
{
. = ALIGN(4);
/*
* These are the old initialisation sections, intended to contain
* naked code, with the prologue/epilogue added by crti.o/crtn.o
* when linking with startup files. The standalone startup code
* currently does not run these, better use the init arrays below.
*/
KEEP(*(.init))
KEEP(*(.fini))
. = ALIGN(4);
/*
* The preinit code, i.e. an array of pointers to initialisation
* functions to be performed before constructors.
*/
PROVIDE_HIDDEN (__preinit_array_start = .);
/*
* Used to run the SystemInit() before anything else.
*/
KEEP(*(.preinit_array_sysinit .preinit_array_sysinit.*))
/*
* Used for other platform inits.
*/
KEEP(*(.preinit_array_platform .preinit_array_platform.*))
/*
* The application inits. If you need to enforce some order in
* execution, create new sections, as before.
*/
KEEP(*(.preinit_array .preinit_array.*))
PROVIDE_HIDDEN (__preinit_array_end = .);
. = ALIGN(4);
/*
* The init code, i.e. an array of pointers to static constructors.
*/
PROVIDE_HIDDEN (__init_array_start = .);
KEEP(*(SORT(.init_array.*)))
KEEP(*(.init_array))
PROVIDE_HIDDEN (__init_array_end = .);
. = ALIGN(4);
/*
* The fini code, i.e. an array of pointers to static destructors.
*/
PROVIDE_HIDDEN (__fini_array_start = .);
KEEP(*(SORT(.fini_array.*)))
KEEP(*(.fini_array))
PROVIDE_HIDDEN (__fini_array_end = .);
. = ALIGN(4);
} >FLASH
/*
* For some STRx devices, the beginning of the startup code
* is stored in the .flashtext section, which goes to FLASH.
*/
.flashtext :
{
. = ALIGN(4);
*(.flashtext .flashtext.*) /* Startup code */
. = ALIGN(4);
} >FLASH
/*
* The program code is stored in the .text section,
* which goes to FLASH.
*/
.text :
{
. = ALIGN(4);
*(.text .text.*) /* all remaining code */
*(.rodata .rodata.*) /* read-only data (constants) */
*(vtable) /* C++ virtual tables */
KEEP(*(.eh_frame*))
/*
* Stub sections generated by the linker, to glue together
* ARM and Thumb code. .glue_7 is used for ARM code calling
* Thumb code, and .glue_7t is used for Thumb code calling
* ARM code. Apparently always generated by the linker, for some
* architectures, so better leave them here.
*/
*(.glue_7)
*(.glue_7t)
} >FLASH
.user_cfg_data_flash_default_page :
{
. = ALIGN(4);
user_cfg_data_flash_default_page_start = .;
KEEP(*(.user_cfg_data_flash_default_page .user_cfg_data_flash_default_page.*))
. = ALIGN(4);
user_cfg_data_flash_default_page_stop = .;
} >FLASH
/* ARM magic sections */
.ARM.extab :
{
*(.ARM.extab* .gnu.linkonce.armextab.*)
} > FLASH
__exidx_start = .;
.ARM.exidx :
{
*(.ARM.exidx* .gnu.linkonce.armexidx.*)
} > FLASH
__exidx_end = .;
. = ALIGN(4);
_etext = .;
__etext = .;
/*
* This address is used by the startup code to
* initialise the .data section.
*/
_sidata = _etext;
/* MEMORY_ARRAY */
/*
.ROarraySection :
{
*(.ROarraySection .ROarraySection.*)
} >MEMORY_ARRAY
*/
/*
* The initialised data section.
* The program executes knowing that the data is in the RAM
* but the loader puts the initial values in the FLASH (inidata).
* It is one task of the startup to copy the initial values from
* FLASH to RAM.
*/
.data : AT ( _sidata )
{
. = ALIGN(4);
/* This is used by the startup code to initialise the .data section */
_sdata = . ; /* STM specific definition */
__data_start__ = . ;
*(.data_begin .data_begin.*)
*(.data .data.*)
*(.data_end .data_end.*)
*(.ramfunc*)
. = ALIGN(4);
/* This is used by the startup code to initialise the .data section */
_edata = . ; /* STM specific definition */
__data_end__ = . ;
} >RAM
/*
* The uninitialised data section. NOLOAD is used to avoid
* the "section `.bss' type changed to PROGBITS" warning
*/
.bss (NOLOAD) :
{
. = ALIGN(4);
__bss_start__ = .; /* standard newlib definition */
_sbss = .; /* STM specific definition */
*(.bss_begin .bss_begin.*)
*(.bss .bss.*)
*(COMMON)
*(.bss_end .bss_end.*)
. = ALIGN(4);
__bss_end__ = .; /* standard newlib definition */
_ebss = . ; /* STM specific definition */
} >RAM
.user_cfg_data_ram_page :
{
. = ALIGN(4);
user_cfg_data_ram_page_start = .;
KEEP(*(.user_cfg_data_ram_page .user_cfg_data_ram_page.*))
. = ALIGN(4);
user_cfg_data_ram_page_stop = .;
} > CCM
user_cfg_data_ram_page_size = user_cfg_data_ram_page_stop - user_cfg_data_ram_page_start;
.user_cfg_data_page_1 :
{
. = ALIGN(4);
user_cfg_data_flash_page_1_start = .;
KEEP(*(.user_cfg_data_flash_page_1 .user_cfg_data_flash_page_1.*))
. = ALIGN(4);
user_cfg_data_flash_page_1_stop = .;
} > user_cfg_PAGE_1
.user_cfg_data_page_2 :
{
. = ALIGN(4);
user_cfg_data_flash_page_2_start = .;
KEEP(*(.user_cfg_data_flash_page_2 .user_cfg_data_flash_page_2.*))
. = ALIGN(4);
user_cfg_data_flash_page_2_stop = .;
} > user_cfg_PAGE_2
.noinit (NOLOAD) :
{
. = ALIGN(4);
_noinit = .;
*(.noinit .noinit.*)
. = ALIGN(4) ;
_end_noinit = .;
} > RAM
/* Mandatory to be word aligned, _sbrk assumes this */
PROVIDE ( end = _end_noinit ); /* was _ebss */
PROVIDE ( _end = _end_noinit );
PROVIDE ( __end = _end_noinit );
PROVIDE ( __end__ = _end_noinit );
/*
* Used for validation only, do not allocate anything here!
*
* This is just to check that there is enough RAM left for the Main
* stack. It should generate an error if it's full.
*/
._check_stack :
{
. = ALIGN(4);
. = . + _Minimum_Stack_Size ;
. = ALIGN(4);
} >RAM
/* After that there are only debugging sections. */
/* This can remove the debugging information from the standard libraries */
/*
DISCARD :
{
libc.a ( * )
libm.a ( * )
libgcc.a ( * )
}
*/
/* Stabs debugging sections. */
.stab 0 : { *(.stab) }
.stabstr 0 : { *(.stabstr) }
.stab.excl 0 : { *(.stab.excl) }
.stab.exclstr 0 : { *(.stab.exclstr) }
.stab.index 0 : { *(.stab.index) }
.stab.indexstr 0 : { *(.stab.indexstr) }
.comment 0 : { *(.comment) }
/*
* DWARF debug sections.
* Symbols in the DWARF debugging sections are relative to the beginning
* of the section so we begin them at 0.
*/
/* DWARF 1 */
.debug 0 : { *(.debug) }
.line 0 : { *(.line) }
/* GNU DWARF 1 extensions */
.debug_srcinfo 0 : { *(.debug_srcinfo) }
.debug_sfnames 0 : { *(.debug_sfnames) }
/* DWARF 1.1 and DWARF 2 */
.debug_aranges 0 : { *(.debug_aranges) }
.debug_pubnames 0 : { *(.debug_pubnames) }
/* DWARF 2 */
.debug_info 0 : { *(.debug_info .gnu.linkonce.wi.*) }
.debug_abbrev 0 : { *(.debug_abbrev) }
.debug_line 0 : { *(.debug_line) }
.debug_frame 0 : { *(.debug_frame) }
.debug_str 0 : { *(.debug_str) }
.debug_loc 0 : { *(.debug_loc) }
.debug_macinfo 0 : { *(.debug_macinfo) }
/* SGI/MIPS DWARF 2 extensions */
.debug_weaknames 0 : { *(.debug_weaknames) }
.debug_funcnames 0 : { *(.debug_funcnames) }
.debug_typenames 0 : { *(.debug_typenames) }
.debug_varnames 0 : { *(.debug_varnames) }
}
В этом примере структура конфигурации лежит не в RAM области, а в CCMRAM, для повышения быстродействия. Так же замечу, что блоки данных настроек пользователя — это последние 2 страницы flash, каждая из которых 128 кб.
Инициализация и контроль копий
Забота о том, что будет лежать в RAM лежит на программисте. До первого использования данных из этой области потребуется инициализировать область. Во flash же при загрузке программы в микроконтроллер будут находиться презаписанные начальными значениями, указанными в коде программы данные. Это сделает невалидными копии блоков настроек пользователя в отдельных страницах, если они оканчиваются CRC32 (посколькуо никакого рассчета CRC32 не производится. Это может делать ваш код. Так же до первого использования параметров конфигурации).
Для написания модуля работы с блоками конфигурации потребуется воспользоваться переменными из LD скрипта. Потребуются следующие:
extern uint32_t user_cfg_data_flash_default_page_start;
extern uint32_t user_cfg_data_flash_page_1_start;
extern uint32_t user_cfg_data_flash_page_2_start;
extern uint32_t user_cfg_data_ram_page_start;
extern uint32_t user_cfg_data_flash_page_size;
extern uint32_t user_cfg_data_ram_page_size;
Так же не следует забывать, что user_cfg_data_flash_page_size и user_cfg_data_ram_page_size — можно присваивать как обычные значения. А вот user_cfg_data_flash_page_1_start и прочие переменные, хранящие адрес требуется указывать через &.
Sap_ru
Но, Холмс, зачем?!!!
Какое «дублирование», какие макросы?! У вас и так для каждой секции линкер поддерживает два адреса — адрес в образе и адрес времени исполнения. Задумайтесь, как по-вашему начальные значения попадают (или как должны) в переменные в озу и чем это отличается от вашего случая?
RTFM, же! Есть ощущение, что дети стали забывать румынского диктатора, а программисты контроллеров, как работает линкер и как происходит загрузка и инициализация программы. И это хорошо ;) т.к. конкуренция меньше, но все же!
И не нужно делать макрос отдельно для массивов. Нужен просто макрос, задающий имя секции. И ставить его в коде при определении массивов, структур, переменных и прочего. И выравнивание компилятор сам умеет задавать, в зависимости от типа данных и контроллера — вам там точно это нужно в макросе? И таблицы секций тоже можно и нужно делать через скрипт ликера. Вместе с указателями размерами, флагами и чем хотите — всё необходимое в лимонное есть. И место для crc тоже должно с через смените ликера резеовироваться в вашем случае.
Платите, но ваш велосипед ужасен и вреден.
Vadimatorikda Автор
При старте МК попадаю в обработчик по reset. Там копирую из области data (во flash) в область data в ram. Область bss, noinit заполняю 0-ми. Но причем здесь это? Тут ведь речь про данные, которые должны быть в отрыве от прошивки МК. Они должны быть в строго определенных местах, чтобы в случае чего можно было снять dump памяти и посмотреть, с какими последними параметрами стартовало устройство.
А нужно 4 по ТЗ. Резервирование. Надежность.
Тем, что то, что попадет в RAM зависит от того, целостны ли данные в момент подачи питание или после перезагрузки в резервированных блоках или их стоит заменить начальными из текущей актуальной прошивки.
Читал. Нигде нет возможности указать явное дублирование в N копий. Вот пример.
Соглашусь. Даже не понял, о ком вы (не очень дружу с историей. Но это к делу не относится)
Но ведь я тут про дополнение linker-скрипта распинался…
Людей, умеющих адекватно работать с ld и прочими низкоуровневыми штуками и так мало… Не надо уменьшать.
Полагаю, вы невнимательно читали статью. Поправьте, если я не прав. Мне нужно, чтобы каждая сущность была продублирована в 4 местах. ТЗ такое. При вашем подходе мне придется явно задавать одну и ту же сущность 4 раза, просто указывая в макросе, в какую секцию стоит положить ее.
У нас обычно структуры задаются с атрибутом упаковки. Так что внутри них может и не быть адекватного выравнивания. Код к этому готов. А вот сами структуры/массивы и прочие в отдельности обязаны быть выравнены на 4. По-умолчанию компоновщик пытается все объявленные переменные через макрос выше складывать подряд. Без выравнивания. Тут атрибут выравнивания скорее дополнительная гарантия, что этого не произойдет. Уже сталкивался на практике с ситуацией, когда переменные лежали без выравнивания, а обращение к ним шло с использованием взятия ассемблерной команды по кратному 4 адресу. И падение в соответствующий обработчик гарантировано.
Но ведь я там их и объявил…
Я получаю из ld скрипта размеры, указатели, чтобы мой собственный модуль произвел инициализацию области RAM валидными данными и далее в процессе работы мог актуальными данными перезаписывать резервируемые блоки, если пользователь захочет изменить настройки при включении.
Не понял смысла предложения. Поясните пожалуйста, что вас не устроило. Я в mem.ld резервирую место под CRC в областях памяти, реализующих резервирование настроек пользователя.
Кому платить? Подводя итог, что именно ужасно? Выше аргументировал каждый пункт вашего недовольства.
Sap_ru
Да, разобрался. Прошу прощения. Необычность задачи не бросается в глаза.
Посыпаю голову пеплом и в качестве покаяния делюсь страшным секретом, как я эту задачу решаю без всяких макросов.
1) Описываем в заголовке структуру с данными, которые должны дублироваться. И тип указателя на неё. Там же объявлем внешние символы с типом этого указателя и именами, описанными в скрите линкера (см. дальше). Дальше для доступа из других исходных файлов ко всем копиям данных достаточно подключить этот заголовок. Можно даже начальные значения там же прописать, но это зависит от диалекта языка.
2) Добавляем в проект исходный файл, в котором находится определение данной структуры в сегменте ".myPreciousData" (чтобы можно было несколько структур в одном файле располагать, например). Можно совершенно нормально задавать начальные значения, использовать сложне типы и т.п. Очень приятно и читабельно.
3) *** МАГИЯ *** Компилируем этот исходный файл в несколько РАЗНЫХ объектных. Реализация зависит от системы сборки. В Eclipse CDT и некоторых других можно нашаманить несколько «ссылок» на файл с разными именами, что позволяет скомпилировать один исходник под несколькими именами (вообще сказака). В CMAKE, make и прочих руками описываем компиляцию этого файла несколько раз с разными выходными именами (объектных файлов) — тоже отлично. В тяжёлых случаях ставим хук на компиляцию и тупо копируем исходный объектный файл под несколькими именами (тем более, что у меня всегда есть хук на линкер, т.к. есть автоинкремент версии сборки с подстановкой даты сборки) — кривова-то уже.
4) В линкере спокойно раскидываем наши файлы (с указанием имени файла и нашего сегмента ".myPreciousData", чтобы лишнего не попало) по разным секциям создавая символы начала секций. Секции располагаем где хотим. Инициализацию секций в ОЗУ делаем так же, как и для обычных сегментов данных в ОЗУ (т.е если проект правильно построен, то прописываем в таблицы инициализации).
5) В исходный текстам импортируем описанные в скриптах ликера имена с типом указателя на нашу структуру.
Получаем чудо и праздник — гарантированно одни и те же данные в нескольких местах с одним описанием типа и одной инициализаций, автоматически компилирующиеся один раз (что прекрасно с точки зрения обеспечения надёжности и предсказуемости результата) и при этом существующие в нескольких копиях. И даже, в зависимости от компилятора, отладчик всё будет правильно показывать. И в map-файла тоже красота, порядок и полнейша правда — лего контрлироватконтролировать правильность сборки.
3xП — Простота Проверяемость Порядок — МАГИЯ!
Точно так же можно автоматически даже копии кода создавать (с целью иметь гарантированные дубликаты загрузчиков вместе с данными, таблиц всяких и т.п.), но нужно очень хорошо понимать, что именно генерирует компилятор.
И ещё маленький секрет — у меня не просто раздельные таблицы иницализации секций data, bss и т.п., а общая таблица секций в скрипте линкера прописывается, где все таблицы вперемешку, но для каждой записи ещё поле флагов задаётся для каждой. В результате общая процедура инициализации, которая по таблице проходит и в зависимости от флагов делает с ними нужное. Т.е. BSS обнуляет, DATA копирует и т.п. Но ещё появляется возможно это в несколько этапов делать (например, иметь инициализированную секцию data во внешнем ОЗУ, которое инициализируется уже на поздних этапах работы программы — вызываем ту же процедуру начальной инициализации сегментов, но с другим параметров и она инциализиурет только сегменты с определённым флагом). Или автоматически рассчитывать, проверать CRС. Или автоматически находить и восстанавливать повреждения в копиях (в таблицу-то можно даже разное количество полей на каждую секцию запихивать, регулируя флагами — добавлять адрес «эталонного сегмента», например). Всё это при должном документации гораздо проще, проверяемее и понятнее, чем куча шаманских таблиц сегментов и таблиц с шаманской же последовательностью инициализации, размазанной по коду и изменяемой каждый раз, когда нужно карту памяти изменить. Один раз написал, описал и забыл.
На выходе полнейшая радость и удобство с нулевым оверхедом. И никакой магии на уровне исходного кода.
Vadimatorikda Автор
Долгое время на работе работал с этой реализацией. У нее есть следующий недостаток: <<Кучу разных данных, никак не связанных между собой по смыслу требуется описывать в одном месте.>>. Сейчас на работе пришли к тому, чтобы было четкое разделение логики и аппаратной части. Например метод «tim_set_pwm_duty» внутри и «set_lcd_brightness» наружу. В этом случае у tim_set_pwm_duty могут быть настраиваемые параметры на уровне железа (например границы ШИМ), а у set_lcd_brightness — параметры на уровне пользователя (например, допустимый диапазон яркости при текущем режиме работы). Сам код прекрасно разносится по разным уровням абстракции и позволяет не менять логику при переходе на другой микроконтроллер. К нему претензий нет. А вот то, что обе эти функции должны знать о существовании общей глобальной структуры несколько огорчает (да, каждая знает только о своей части в этой структуре. Но глобальная структура на весь проект — не есть хорошо). В моем же подходе можно создавать сущности (переменные, структуры) в совершенно разных местах. При этом всегда в map можно посмотреть, где и что лежит, если требуется. Это основное преимущество.
Отписал выше. Нарушается инкапсуляция и падает логика. Хорошо заметно на больших проектах, когда твоя структура под пару сотен элементов в куче по типу: «границы ШИМ каналов таймеров + параметров интерфейса uart + режим работы USB»…
В подходе, который был на работе, делалось проще. Просто в RAM создавалась структура (та самая, глобальная). Просто пустая. Ее начальные данные в flash. В модуль работы с этим делом из компоновщика передавались указатели на области flash. Все. По сути, это недоработанный ваш метод. Вашим можно еще узнать, сколько заполнено и выводить это. Хотя makefile/CMakeLists будет выглядеть действительно страшно. А я категорически против «интересных» решений в этой области. Это делает код сложно воспринимаемым и ведет к вопросу у тех, кто должен поддерживать это, когда вас нет на рабочем месте или вы вовсе его сменили. Но это делает вас не заменяемым на работе… Настолько, что вам звонят даже после увольнения и просят рассказать, как оно работает… Потому что «нужно поддерживать», а никто не знает почему оно вообще работает… Прошу прощения. Немного накипело на эту тему. Приходилось сталкиваться с очень хитрыми системами сборки.
Что такое «таблица инициализации data, bss...»? Интересно посмотреть.
У вас дикий оверхед на уровне сборки. Чтобы в этом разобраться — придется не слабо повозиться, если изначально не знать всех нюансов. В моем же случае есть небольшой наклодняк со стороны кода (хоть и невидимый для программиста), но никаких проблем на уровне сборки.
Sap_ru
Ну, если мы про отказоустойчивость говорим, то это, как раз, хорошо и правильно. Это позволяет писать легко-верифицируемый код. Откуда уверенность что все критичные данные находятся в защищаемый секциях? А точно правильно работает переключение на резервный набор данных? Это обязательно должно быть собрано в одном месте иначе верифицуемость такого кода под большим вопросом.
При желании это можно по структурам в зависимости от посдсистемы или даже по разным заголовкам (кто мешает структуры определять в разных заголовках, а потом собирать в одну?). Если программа правильно написано, то объединение защищаемых данных в одном месть это хорошо. Другое дело, что народ то бездумо пихает в защищаемые данные то, что не нужно, то наоборот критичные данные оказываются без резерва. Но это уже вопрос общего дебилизма и формализма. В 90% случаев вообще можно обойтись фоновым контролем целостности RO-секции программы и переключением на резервную копию через перезагрузку.
Описывайте параметры своих ШИМов в отдельных структурах и потом объединяйте в одну.
В общем, тут категорически не согласен — цель же условие ТЗ «4 копии каких-то данных иметь» исполнить, а ещё и отказоустойчивую систему сделать.
И у меня, кстати, сложились очень нехорошие подозрения, что вы вообще что-то не то делаете. Вы вообще все константы защищаете по ТЗ? А так понимаю, что да. А как насчёт всяких служебных, которые компилятор для себе генерирует? Там же до 30% RO-данных это служебные константы и всякие адреса переходов, которые компилятор нагенерировал. Их, что, защищать не нужно? А какой тогда смысл защищать «режим работы UART», если у вас в десять раз больше констант, относящихся к критичному коду (в том числе работы с тем же UART) оказываются незащищёнными? У вас в этом случае отказоустойчивость не на два порядка вырастает, как по ТЗ задумывалось, а на 5% — а смысл? В этом случае, как я говорил — делается защита вообще всей области RO-данных и несколько экземпляров этих данных с переключением с случае нарушения целостности рабочей копии. Это можно сделать только тем способом, что я описал. Иначе ТЗ, вроде бы как, выполняется, но отказоустойчивость не только растёт, но иногда и падает. Естественно, что настоящей проверки и сертификации такой код пройти не может (на практике в России и не такой проходит).
Вопрос поддержки кода решается документацией и разделением квалификации исполнителей. Если студенты пишут критичный код, то проекту ничего не поможет. А если критичный код написан профессионалом, проверен, документирован и дальше в него никто не лезет, то можно в какие-то другие части с студентов пускать после прочтения инструкции.
Вопрос же заменяемости… Чтобы писать качественный отказоустойчивый год, нужен специалист соответствующего уровня. Если он есть и решение документировано, то он без проблем разберётся. Абсолютно ничего сложного в моём решении — любой, кто значет, как работает линкер без проблем разберётся. Мы же, в конце-концов, говорим про работу, требующую специалиста высокой квалификации и достаточно редкой специализации.
Я прямо, даже растерялся — мы сейчас точно про разработку авионики говорим, что ушедшего специалиста заменит некем? Ну, в российской, как правило — да. В общем и целом — нет.
Если «звонят после увольнения», то это проблемы звонящего и верный признак, что звонящий занимается не своим делом. Качественный код стоит денег, квалификации и определённой организации работы. А на практике, обычно оказывается, что вместо ушедшего специалиста посадили студента на 35 тысяч, который попутно изучает программирование. Ну, так это проблемы — оно за это деньги получает. Продают-то они этот код не за пять рублей.
Специалистов должно быть несколько, их уровень и опыт должны соответствовать задаче, а документация должна иметься.
В общем, тоже не согласен. Решение на самом деле достаточно несложное и если кто-то его не тянет, то возможно ему не стоит заниматься отказоустойчивыми системами, ибо, как я уже сказал, никак иначе по-настоящему отказоустойчивый код вы просто не напишите — так, симуляцию отказоустойчивости, может быть. Короче, если кто-то не обладает необходимыми знаниями для решения задачи, то это его проблемы, а не того, кто обладает.
Про таблицу инициализации сегментов. Это выглядит примерно так:
С точки зрения C/C++ это выглядит как массив структур:
Массив в коде доступен под именем "__region_list_start__".
Стартап код до вызова любого другого кода (кроме инициализации стека, WatchDog и тактового генератора), вызывает процедуру (которая тоже находится стартап-сегменте и потому доступна с самого начала), которая проходит по таблице и обрабатывает все регионы с флагом «FREG_STARTUP».
Потом проходим по таблицам инициализации стандартных библиотек и вызываем их код. Вы же, надеюсь, проходите по таблицам "__preinit_array_start__" и "__init_array_start__" — стандартные таблицы для GCC — без этого маcса глюков будет.
Потом стартуем стандартную библиотеку. И уже в main завершаем инициализацию железа и снова вызываем ту же функцию инициализации сегментов. Но теперь уже она инициализирует только сегменты без флага «FREG_STARTUP» — находящиеся во внешней памяти, во всяких аппаратно-защищённых областях памяти т.п. Потом включаем защиту RO-областей памяти и кода в MMU если есть (а какой может быть отказоустойчивый код без этого) и, вот, после этого мы готовы стартовать основной код.
В ту же функцию инициализации сегментов можно затолкать функционал выбора неповреждённой копии сегмента при ошибке CRC. Если есть аппаратная поддержка через ремапинг адресов, то можно и код защищать. И только так можно по-настоящему что-то защитить дублированием. Иначе получается что-то совсем не то, что хотелось бы.
Именно так это делается в «best practices» и серьёзных библиотеках.
Да, и менеджер динамической кучи подключаем свой, т.к. мы не знаем, как работает стандартный и верить в критических применениях не можем.
Vadimatorikda Автор
Данный вопрос решается просмотром map файла (чтобы убедиться, что все сохраняемые данные в нужных местах).
Пробовали и такое. Все равно есть мега-супер-структура. Мы просто придерживаемся соглашения том, что все переменные должны быть спрятаны внутри одного файла (просто C без объектов когда). А наружу торчать только методы. И сейчас единственное, что не вписывалось в это видение — как раз эта глобальная структура. Тут нет смысла спорить. Все решается принятием удовлетворяющей команду разработчиков общей концепции. И все проекты должны делаться согласное ей.
Описал выше.
Тут полностью согласен. Мы составляем внутреннее ТЗ, в котором явно прописано, что и где должно лежать.
Во время работы нет надобности контролировать RO секцию. Она анализируется только при включении устройства. Устройство должно включаться за очень малый промежуток времени (они на летающем аппарате. Хоть и без людей. Если бы там были люди, то такого вопроса бы вообще не было. Поскольку там была бы внешняя flash, аппаратно продублированная и прочие штуки).
Отписал, почему не согласен (принятое соглашение между разработчиками).
4 секции — это разбиение более высокого требования «Обеспечить отказоустойчивое хранение данных конфигурации устройства.» (+ подпункты про изменяемость при настройке и прочее).
Ни в коем случае. Как писал выше, только то, что может быть изменено человеком, который будет интегрировать устройство в финальный комплекс.
Вот тут отмечу то, чего не было в статье. Да, я не храню копию данных того, что генерирует компилятор. И констант, которые не меняются. Но тут есть нюанс. Дело в том, что я 100% знаю, что весь код моей программы в момент включение — без ошибок (аппаратных). Имею ввиду, что прошивка соответствует той, которая была зашита. Об этом заботится загрузчик при включении. Вся прошивка целиком тоже дублирована. Содержит свою контрольную сумму. В статье же речь именно о изменяемых константах, которые может менять пользователь в отрывае от прошивки (ведь эти параметры лежат совсем в других областях flash. Опять же повторюсь. Если бы речь шла о перевозке людей, тогда все эти данные лежали вообще в отдельных накопителях с аппаратным резервированием. Т.к. нельзя позволить микроконтроллеру писать свою же память. Хоть и в другой странице.).
Это попытка оскорбить или сравнение с типовым случаем?) (типа я тоже студент формально… Хоть и не долго осталось до окончания)
Его трудно найти, легко потерять, и нереально удержать в Красноярске… Тем более за не особо высокую ЗП… Тут уже далеко не технический вопрос. Но и про документацию и прочее. В фирмах, которых мне довелось работать, документация — это последнее, до чего доходят руки. ТЗ иметь на бумаге вот и то редкость. В текущей фирме только начал появляться весь этот задел. Когда сначала ТЗ, потом под-требования, тесты, потом код. И это хорошо. Но фирме больше 7 лет. Что говорить о фирмах, которые «со подрядчики» и т.д.? Довелось работать с людьми, которые разрабатывали многомиллионное оборудование по ТЗ на словах) Тесты — на заказчике. Устройство не серийное. Там устройство не летало. Это оборудование для весьма не маленького предприятия. И… Там двух слойная плата управления без особых защит, никакого резервирования и т.д. При неудачном стечении обстоятельств (вероятность которых в условиях цеха очень высока), оборудование придет в негодность. Ну а надо всем там было срочно, переделывать не надо. работает же. Прототип? И так пойдет… В общем, все зависит тут от конторы. Но я считаю, что чем решение топорнее, тем лучше. Проще будет разгрузиться с себя 100500 разных проектов и технический долг будет хоть капельку меньше.
Не просто про Россию, а про Красноярск) Днем с огнем не сыщешь человека, который бы писал код хоть как-то вменяемо. При этом был готов если что переквалифицироваться в тех. писателя, пусконаладчика и т.д. Фулл-стек ембеддера короче).
Это значит, что человек ушел и не оставил ни строчки пояснений, а его код местами выглядит вот так:
Конечно, иногда помогает вдумчивое чтение документации. Но когда человек «для экономии ресурса» умножает 2 константы из документации и использует как одно число прямо в коде — догадаться об этом ОЧЕНЬ трудно. Да и человек порой писал код этот в свободное от командировок по интеграции устройств время. Так сказать «за чашкой чая». И в то время, когда компания только зарождалась — это было нормально и единственным решением чтобы выжить. Но те времена прошли и настало время уже следующему поколению сотрудников разбираться с этим… кодом) И «одна большая структура, которая лежит типа в оперативной памяти, но под нее зарезервировано с запасом 2 блока, в которые данные не пойми как попадают» — один из примеров. В вашем случае вы действительно их туда кладете. Я же столкнулся с реализацией, когда просто 2 страницы резервировались. И не важно, влезет туда структура или нет (конечно, обычно структуры 1-3 кб, а резервировалось 128, но все же).
3 человека хватит на все))) На каждом из которых куча проектов и задач не связанных непосредственно с программированием (например, понять почему упал борт, почему QNX внезапно начинает видеть битые блоки при включении при определенном стечении обстоятельств и прочие штуки).
Про достаточный уровень — соглашусь. Про нехватку кадров и надобность в более простых по реализации решений, но при этом не уступающим по качеству — отписал выше.
Красиво! Реально интересная тема. Не видел раньше, чтобы так пользовались LD скриптом. Обычно +- редактируют LD из демонстрационного проекта и на этом заканчивают.
Ну это понятно, да. Интересно. Очень интересно.
Ну это следует из такой организации.
Вот тут уже не подскажу, но вроде это нужно только если вы C++ используете. Там конструкторы глобальных объектов и прочее. Если мне память не изменяет. В чистом C вроде не нужно. Но тут не уверен.
Всегда корю себя за то, что пользуюсь стандартными библиотеками. По-хорошему, в настоящей авиации нельзя. Ни printf ничего такого. Пока используем. Но вообще нельзя.
Мне пока так и не удалось кстати настроить MMU + FreeRTOS. Ну это пока никому не требовалось, но вообще сам понимаю, что это необходимо. Просто времени не хватает сделать как положено. Стараюсь эксперементировать с такими штуками дома. А то на работе оправдать то, что ты возился с MMU вместо решения «реальных задач» — достаточно сложно.
Да, при вашем подходе хорошее решение. У меня этим занимается загрузчик. Как и восстановлением, если что-то пошло не так с кодом программы целевого устройства. А сама программа (целевого устройства) считает. что она не повреждена из коробки.
Мы используем стандартный FreeRTOS-овский. Он переопределяет malloc и free. Но вообще в наших устройствах, которые летают нет malloc/free/new/delite. Только статически размеченная ram. В угоду отказоустойчивости.
Sap_ru
Вот, не хотелось бы обижать, но вот про это я и говорю ;) Тут вопрос, есть ли в фирме кто-то, кто это точно знает, применительно к используемым компилятором. Если есть, то это его зона ответственности. Если нет, то мы имеем то, что имеем — люди учатся на живом коде и где-то что-то летает, что содержит ошибки и непредсказуемое поведение.
И про malloc/free тоже. Эти функции используются стандартной библиотекой даже если вы вы их не пользуете и даже если вы стандартную библиотеку не используете. Например, если вы используете в плавающую точку в любом виде на GCC, то вы уже используете динамическую кучу. А если вы ещё и FreeRTOS используете, то нужно убедиться, что структура "_reent" переключается и работет правильно.
Vadimatorikda Автор
А есть иные варианты, кроме как учеба на живом коде? О многих вещах даже не задумываешься до того, как не столкнешься. А на чтение документации обычно времени практически нет. Ибо в моем случае я не только пишу код, но еще и тесты, требования, и т.д. Хотя хотелось бы как вы, конечно. Просто сидеть и копать в свою область. Это полезно, я считаю.
Хочу обоснований. Я просто осматривал _sbrk. Она не дергалась вроде ничем при работы с плавающей кучей (есть аппаратный блок работы с плавающей точкой). Куда копать?
Вот честно. Даже не знал о существовании такой штуки. Об этом статей не видел. Вот и не знал. Смотрю, вы много знаете нюансов. Был бы рад, если бы хоть писали об этом. С радостью бы почитал (без сарказма).
С просони не так написал. Не MMU, а MPU. MMU нет на тех процессорах, с которыми приходится работать. А вот настроить MPU для той же защиты при переключения контекста FreeRTOS — хотелось бы. А то без дела лежит… Руки не доходят.
Sap_ru
Вот, проблема в том, что так делать можно, но не нужно. Качество как гарантировать? Опять же, писать тесты, и требования это хорошо полезно, но опыт в этом есть или тоже по ходу нарабатывается? Т.е контора под видом коммерческого продукта впаривает непонятно что.
Это не к вам претензия, это к российским реалиям, в которых я двадцать лет отработал. Плохо то, что в результате многие российские разработчики даже не понимают, как нужно правильно разработку вести и буквально поколениями воспроизводят неправильный опыт.
С MMU/MPU в мире ARM это вопрос терминологии, кстати. MPU однозначно нужен. Полноценный же MMU с преобразованием адресов нужен в 99% случаев лишь для выполнения функций MPU. Он ставит очень жёсткие требования к структуре программы. Для большинства задач это просто не нужно. Или начинаются костыли, когда зачем-о включают MMU, а потом начинают придумывать, как бы его обойти, т.к. он работе программы мешает. У меня за всю историю было только два проекта на контроллерах, где нужно было преобразование адресов. В одном пришлось сделать полноценную подсистему виртуальной памяти, т.к. был процессор с полноценными многоуровневыми кэшами и для получения необходимой производительности пришлось MMU на полную катушку врубать (т.к. есть регионы память, где вообще кэша не должно быть, есть, где writethrough, есть где witeback и т.п., а управление этим всем потребовало включения страничного преобразования). А во втором был большой сложный многопоточный проект с кучей независимых процессов, который достиг того предела сложности, когда включение MMU позволяет быстрее находить ошибки, но и там можно было обойтись переключением MPU в планировщике задач. Во всех остальных случаях полноценный MMU был не нужен и даже вреден.
Vadimatorikda Автор
Приходится… Ибо не знаю, где можно было бы этому профессионально обучится. Я тут говорил, что почти уже не студент. Но, честно, толку от учебы не было вообще (в плане технического развития). Мы на предмете «микроконтроллеры» 2 семестра смотрели на видео с ютуба по программированию ПЛК на языке графическом LD (нет, не язык скриптов линкера… Погуглите. Эта интересный на вид конструктор. Но никак не программирование).
Тестами и внимательным отсмотром кода, когда есть время.
Конечно самостоятельно. Смотреть как делают другие и учиться на примере этого. Набивать свои шишки и продолжать искать нужный путь. Читать литературу, что есть. Не видел у нас мест. где бы этому реально учили.
В защиту некоторых контор в которых работал скажу, что они хотя бы обеспечивают поддержку и идут на встречу заказчиком. Когда что-то деделывается по месту. Приходилось работать и с теми, что просто клали на все и говорили что-то типа «устройство готово. Доработки — другое устройство. Заказывайте заново».
Ну так… Да)… А что еще остается? Как правило, чем дольше контора живет, тем лучше у нее все с продуманностью. Обычно когда контора закрывается, ее опыт неудач и наоборот успехов уходит вместе с ней. Ну и в опыте сотрудников остается. Которые на новом месте уже пытаются не совершать определенных ошибок, с которыми столкнулись на прошлом месте работы.
Интересно. Я просто всегда был уверен, что MMU реально нужен только чтобы QNX/Linux запустить.
Vadimatorikda Автор
Вообще, считаю, что достаточно мало информации по таким узким местам, как использование кучи разными стандартными функциями, используемые ресурсы стандартными библиотеками, различные варианты проведений и прочее. Вообще, честно, до ваших примеров был уверен, что все заканчивают «свое развитие» на том, что понимают, как работает стандартный ld скрипт, добавляют в него внешнюю память и все. Теперь вижу, что нет. Был бы рад, если бы порекомендовали литературу на эту тему. Всегда рад изучить вопрос подробнее.
Sap_ru
Гхм… Что значит «мало информации»? А вы её искали? Литературу нужно копать в сторону «портирование newlib». Есть масса мануалов по портированию и работе newlib (вы же наверняка её используете). Там расписано, что, как и где отрабатывает и что нужно настраивать. раз вы не используете стандартных скриптов линкера и стндартного кода инициализации, то вы уже портируете newlib на новую систему и обязательно должны знать во что лезете.
Например есть уже упомянутая "_reent", которую ОБЯЗАТЕЛЬНО нужно переключать при переключении процессов. Особенно если используется плавающая точка, т.к. это использует стандартная библиотеке в функция округления, контроля ошибок FPU и т.п. Что динамическая память используется стандартной библиотекой на ранних этапах инициализации (до запуска main) — выделяют всякие служебные области. Что таблицы "__init_" ОБЯЗАТЕЛЬНО нужно обрабатывать, т.к. через них работает инициализация стандартной библиотеки. Более того, через них работают некоторый функции языка. Как иначе значение переменной может быть инициализировано результатом выполнения функции? «static int a = claculateInitalValue()» — как оно может сработать без списков инициализации. А в старов стандартном C есть специальная стандартная pragma, которая аналогичное делает. И это всё используется в стандартной библиотеке.
Т менджер динамической памяти стандартный — он как работает? Какие сегменты использует, какие к ним требования, какие расходы на выделение памяти? Вы ему начало кучи обнуляете? Если нет, то он вам с некоторой вероятностью однажды память попортит. Это тоже относится к портированию newlib.
А ещё есть newlib-nano, и если вы используете строковые функции или многозадачность, то нужно думать использовать её или полноценную версию. Т.к. nano не поддерживает многозадачность без костылей — сюрприз, а полноценная требует динамической кучи памяти и функций синхронизации.
Более того саму newlib, уж если мы про ответственные применения говорим, тоже нужно бы самим компилировать под себя, благо что не сложно. А иначе ваша программа основывается на непонятно каком коде, непонятно кем написанном, который может измениться с изменением минорной версии компилятора. Или, например, простая перекомпиляция функции 64-битного деления позволяет сэкономить 2 килобайта памяти и работает в 4 раза быстрее. А пересобранная newlib выходит в четыре раза меньше и два-четыре раза быстрее (это математика с 64-битными числами, деления и т.п., даже без явного вызова библиотек, не говоря уже о плавающей точке, т.к. она тоже через неявный вызов библиотеки компилятором реализована). А всё потому, что стандартная библиотека скомпилирована под неких абстрактный процессор и использует только базовые функции и команды (например, она, не использует операции деления, т.к. их нет Cortex-M0, а библиотека универсальная). А когда вы её под конкретный процессор собираете, то работают все оптимизации.
Вы используете newlib? Вы должны как минимум знать, как она устроена и как её портировать собрать. Даже если не собираете и не портируете. Иначе гарантированно будет получать редкие и удивительные глюки в программах.
Vadimatorikda Автор
Под всем текстом: ничего себе! Про перекомпиляцию не знал. Когда я только пришел, мне строго настрого запретили лезть в newlib, ld и прочие штуки. Оно и понятно… Тогда был по сути без опыта работы (сам уже не плохо тогда, ИМХО, кодил, но под keil и знал тонкости работы с периферией МК, но не знал тонкостей работы сборки. Т.к. keil все это скрывал). Потом реальная работа, gcc и т.д. Ну и как мне сказали, что там все настроено и трогать не надо. А потом я узнал, что это просто скопированная папка с гита))) После ваших слов, думаю, что стоит максимально подробно остановиться над этим вопросом и изучить как положено. Кстати. Про интересные глюки… У нас они решались выбором newlib-nano (стандартного под платформу с помощью флагов компоновщика) и вызовом функций типа setvbuf (вроде так называлось), где устанавливалось для всех потоков 1 буфер и подобные костыли. Достаточно костыльно.
Sap_ru
newlib-nano, как я уже говорил, хуже работает с многозадачностью. Но для небольших проектов нормально подходит. Там минимум три нереетерабельных буфера — строковых функций, файловых и математики. И правильный способ переключения это всего счастья — через "_reent", которая и задумывалась для этих целей. И если в прерываниях используется 64-битная математика, плавающая точка или printf/scanf, то тоже нужно явно переключать при входе и восстанавливать при выходе. Файловые операции это вообще отдельная песня, т.к. они в newlib-nano не реетерабильны и требуют отдельных приседаний.
А глюки вы скорее всего и не видели даже. Ой, иногда целочисленное деление или плавающая точка дают неправильный результат при разрешённых прерываниях. Это, наверное, newlib кривая, запретим прерывания на время выполния вот этого кода и дело с концом! Правда это эффекты там глубже, но кому оно интересно.
Стандартный newlib собран только под основные архитектуры. Но на самом деле их в мире ARM больше — где-то есть аппаратное деление, где-то нет. Где-то нужно выравнивание адресов, где-то нет. Порядок байт. Операции изменения порядка байт. Операции подсчёта количества нулевых бит (круто ускоряют математику). В плавающей точке вообще зоопарк. Поэтому стандартый newlib собран под самые урезанные версии архитектур самым стандартным образом, чтобы везде работало. Хотя, код у него весьма гибкий и универсальный и может подстраиваться практически под что угодно. В результате в стандартом виде оно в разы больше и в разы медленнее, чем могло бы быть на каждом конкретном процессоре. А ещё он собирается и тестируется авторами на "-O2" и "-O3", но в стандартные сборки идёт "-O1" (хорошо если "-O2"). Можно, например, только математику выборочно пересобрать. Я так иногда делаю, если проблема есть, но лень весть newlib тащить (самый популярный случай — пресловутое 64-битное деление/умножение, которое повально нужно если мы хотим хорошую точность но не хотим плавающей точки). Там всё на weak-ссылках, поэтому добавляем в исходники из того же newlib функции, которые критичны для нашего проекта, они подменяют стандартные и получаем офигенный прирост скорости.
А ещё есть специальная лёгкая версия sprintf, которая очень полезна, но не хочется тащить 20..60..100 килобайт медленного кода из newlib при первом же использовании «sprintf». Тогда очень полезно знать, как правильно подменять стандартную своей легковесной. Очень популярная задача. 99% чудо-программистов ардуинщиков не знают и либо велосипеды колхозят, либо таких монстров на выходе выдают, что простейшая программа сотни килобайт FLASH требует и сотен мегагерц процессора.
Менеджер памяти в newlib тоже очень плохо для контроллеров походит — медленный и слабопредсказуемый. Нужно свой легковесный ставить.
Sap_ru
Кстати, если вы только изменяемые пользвателем данные резервируете и только при старте проверяете, то ваш велосипед совсем плох. Только отдельные сектора памяти, описание всех данных в общей структуре и выбор копии при старте. И никакого шаманства не нужно в 90% случаев — старые добрые указатели, либо копия структуры в ОЗУ. Всё остальное — очень вредные велосипеды и порочный путь, который усложняет структуру программы и её верификацию.
Vadimatorikda Автор
Чем плох? Мы при старте убеждаемся, что все хорошо или делаем, чтобы все было хорошо и далее работаем точно зная, что данные целостны. Хотя, конечно. есть вероятность, что может что-то пойти не так из-за аппаратки. Но это уже не очень понятно как обходить. Конечно, идут проверки на != -1 (либо все 1. Если хоть 1 бит выбился, то не валиден флаг и т.д.). Но таких штук лично я почти не применяю. Не было случая, когда реально пригодилось.
Так вроде я и писал об этом. Ну только если копия повредилась, ее еще по другому экземпляру восстанавливают.
Sap_ru
Ну, тут мы в совсем тонкие материи углубляемся. 99% случаев нужно делать общую структуру с указателями ((или копировать данные при старте ОЗУ, но тоже в общую структуру). Тут нужно уже целые лекции читать. Как это подсистемам и по модулям разнести это уже прикладной момент — тоже способы есть.