Как стать автором
Обновить

Комментарии 50

Но, Холмс, зачем?!!!
Какое «дублирование», какие макросы?! У вас и так для каждой секции линкер поддерживает два адреса — адрес в образе и адрес времени исполнения. Задумайтесь, как по-вашему начальные значения попадают (или как должны) в переменные в озу и чем это отличается от вашего случая?
RTFM, же! Есть ощущение, что дети стали забывать румынского диктатора, а программисты контроллеров, как работает линкер и как происходит загрузка и инициализация программы. И это хорошо ;) т.к. конкуренция меньше, но все же!
И не нужно делать макрос отдельно для массивов. Нужен просто макрос, задающий имя секции. И ставить его в коде при определении массивов, структур, переменных и прочего. И выравнивание компилятор сам умеет задавать, в зависимости от типа данных и контроллера — вам там точно это нужно в макросе? И таблицы секций тоже можно и нужно делать через скрипт ликера. Вместе с указателями размерами, флагами и чем хотите — всё необходимое в лимонное есть. И место для crc тоже должно с через смените ликера резеовироваться в вашем случае.
Платите, но ваш велосипед ужасен и вреден.
Задумайтесь, как по-вашему начальные значения попадают (или как должны) в переменные в озу и чем это отличается от вашего случая?

При старте МК попадаю в обработчик по reset. Там копирую из области data (во flash) в область data в ram. Область bss, noinit заполняю 0-ми. Но причем здесь это? Тут ведь речь про данные, которые должны быть в отрыве от прошивки МК. Они должны быть в строго определенных местах, чтобы в случае чего можно было снять dump памяти и посмотреть, с какими последними параметрами стартовало устройство.
У вас и так для каждой секции линкер поддерживает два адреса — адрес в образе и адрес времени исполнения

А нужно 4 по ТЗ. Резервирование. Надежность.
чем это отличается от вашего случая?

Тем, что то, что попадет в RAM зависит от того, целостны ли данные в момент подачи питание или после перезагрузки в резервированных блоках или их стоит заменить начальными из текущей актуальной прошивки.
RTFM, же!

Читал. Нигде нет возможности указать явное дублирование в N копий. Вот пример.
Есть ощущение, что дети стали забывать румынского диктатора

Соглашусь. Даже не понял, о ком вы (не очень дружу с историей. Но это к делу не относится)
а программисты контроллеров, как работает линкер и как происходит загрузка и инициализация программы

Но ведь я тут про дополнение linker-скрипта распинался…
И это хорошо ;) т.к. конкуренция меньше, но все же!

Людей, умеющих адекватно работать с ld и прочими низкоуровневыми штуками и так мало… Не надо уменьшать.
И не нужно делать макрос отдельно для массивов. Нужен просто макрос, задающий имя секции. И ставить его в коде при определении массивов, структур, переменных и прочего.

Полагаю, вы невнимательно читали статью. Поправьте, если я не прав. Мне нужно, чтобы каждая сущность была продублирована в 4 местах. ТЗ такое. При вашем подходе мне придется явно задавать одну и ту же сущность 4 раза, просто указывая в макросе, в какую секцию стоит положить ее.
И выравнивание компилятор сам умеет задавать, в зависимости от типа данных и контроллера — вам там точно это нужно в макросе?

У нас обычно структуры задаются с атрибутом упаковки. Так что внутри них может и не быть адекватного выравнивания. Код к этому готов. А вот сами структуры/массивы и прочие в отдельности обязаны быть выравнены на 4. По-умолчанию компоновщик пытается все объявленные переменные через макрос выше складывать подряд. Без выравнивания. Тут атрибут выравнивания скорее дополнительная гарантия, что этого не произойдет. Уже сталкивался на практике с ситуацией, когда переменные лежали без выравнивания, а обращение к ним шло с использованием взятия ассемблерной команды по кратному 4 адресу. И падение в соответствующий обработчик гарантировано.
И таблицы секций тоже можно и нужно делать через скрипт ликера.

Но ведь я там их и объявил…
Вместе с указателями размерами, флагами и чем хотите — всё необходимое в лимонное есть.

Я получаю из ld скрипта размеры, указатели, чтобы мой собственный модуль произвел инициализацию области RAM валидными данными и далее в процессе работы мог актуальными данными перезаписывать резервируемые блоки, если пользователь захочет изменить настройки при включении.
И место для crc тоже должно с через смените ликера резеовироваться в вашем случае.

Не понял смысла предложения. Поясните пожалуйста, что вас не устроило. Я в mem.ld резервирую место под CRC в областях памяти, реализующих резервирование настроек пользователя.
Платите, но ваш велосипед ужасен и вреден.

Кому платить? Подводя итог, что именно ужасно? Выше аргументировал каждый пункт вашего недовольства.
Да, разобрался. Прошу прощения. Необычность задачи не бросается в глаза.
Посыпаю голову пеплом и в качестве покаяния делюсь страшным секретом, как я эту задачу решаю без всяких макросов.
1) Описываем в заголовке структуру с данными, которые должны дублироваться. И тип указателя на неё. Там же объявлем внешние символы с типом этого указателя и именами, описанными в скрите линкера (см. дальше). Дальше для доступа из других исходных файлов ко всем копиям данных достаточно подключить этот заголовок. Можно даже начальные значения там же прописать, но это зависит от диалекта языка.
2) Добавляем в проект исходный файл, в котором находится определение данной структуры в сегменте ".myPreciousData" (чтобы можно было несколько структур в одном файле располагать, например). Можно совершенно нормально задавать начальные значения, использовать сложне типы и т.п. Очень приятно и читабельно.
3) *** МАГИЯ *** Компилируем этот исходный файл в несколько РАЗНЫХ объектных. Реализация зависит от системы сборки. В Eclipse CDT и некоторых других можно нашаманить несколько «ссылок» на файл с разными именами, что позволяет скомпилировать один исходник под несколькими именами (вообще сказака). В CMAKE, make и прочих руками описываем компиляцию этого файла несколько раз с разными выходными именами (объектных файлов) — тоже отлично. В тяжёлых случаях ставим хук на компиляцию и тупо копируем исходный объектный файл под несколькими именами (тем более, что у меня всегда есть хук на линкер, т.к. есть автоинкремент версии сборки с подстановкой даты сборки) — кривова-то уже.
4) В линкере спокойно раскидываем наши файлы (с указанием имени файла и нашего сегмента ".myPreciousData", чтобы лишнего не попало) по разным секциям создавая символы начала секций. Секции располагаем где хотим. Инициализацию секций в ОЗУ делаем так же, как и для обычных сегментов данных в ОЗУ (т.е если проект правильно построен, то прописываем в таблицы инициализации).
5) В исходный текстам импортируем описанные в скриптах ликера имена с типом указателя на нашу структуру.

Получаем чудо и праздник — гарантированно одни и те же данные в нескольких местах с одним описанием типа и одной инициализаций, автоматически компилирующиеся один раз (что прекрасно с точки зрения обеспечения надёжности и предсказуемости результата) и при этом существующие в нескольких копиях. И даже, в зависимости от компилятора, отладчик всё будет правильно показывать. И в map-файла тоже красота, порядок и полнейша правда — лего контрлироватконтролировать правильность сборки.
3xП — Простота Проверяемость Порядок — МАГИЯ!
Точно так же можно автоматически даже копии кода создавать (с целью иметь гарантированные дубликаты загрузчиков вместе с данными, таблиц всяких и т.п.), но нужно очень хорошо понимать, что именно генерирует компилятор.

И ещё маленький секрет — у меня не просто раздельные таблицы иницализации секций data, bss и т.п., а общая таблица секций в скрипте линкера прописывается, где все таблицы вперемешку, но для каждой записи ещё поле флагов задаётся для каждой. В результате общая процедура инициализации, которая по таблице проходит и в зависимости от флагов делает с ними нужное. Т.е. BSS обнуляет, DATA копирует и т.п. Но ещё появляется возможно это в несколько этапов делать (например, иметь инициализированную секцию data во внешнем ОЗУ, которое инициализируется уже на поздних этапах работы программы — вызываем ту же процедуру начальной инициализации сегментов, но с другим параметров и она инциализиурет только сегменты с определённым флагом). Или автоматически рассчитывать, проверать CRС. Или автоматически находить и восстанавливать повреждения в копиях (в таблицу-то можно даже разное количество полей на каждую секцию запихивать, регулируя флагами — добавлять адрес «эталонного сегмента», например). Всё это при должном документации гораздо проще, проверяемее и понятнее, чем куча шаманских таблиц сегментов и таблиц с шаманской же последовательностью инициализации, размазанной по коду и изменяемой каждый раз, когда нужно карту памяти изменить. Один раз написал, описал и забыл.
На выходе полнейшая радость и удобство с нулевым оверхедом. И никакой магии на уровне исходного кода.
1) Описываем в заголовке структуру с данными, которые должны дублироваться. И тип указателя на неё. Там же объявлем внешние символы с типом этого указателя и именами, описанными в скрите линкера (см. дальше).

Долгое время на работе работал с этой реализацией. У нее есть следующий недостаток: <<Кучу разных данных, никак не связанных между собой по смыслу требуется описывать в одном месте.>>. Сейчас на работе пришли к тому, чтобы было четкое разделение логики и аппаратной части. Например метод «tim_set_pwm_duty» внутри и «set_lcd_brightness» наружу. В этом случае у tim_set_pwm_duty могут быть настраиваемые параметры на уровне железа (например границы ШИМ), а у set_lcd_brightness — параметры на уровне пользователя (например, допустимый диапазон яркости при текущем режиме работы). Сам код прекрасно разносится по разным уровням абстракции и позволяет не менять логику при переходе на другой микроконтроллер. К нему претензий нет. А вот то, что обе эти функции должны знать о существовании общей глобальной структуры несколько огорчает (да, каждая знает только о своей части в этой структуре. Но глобальная структура на весь проект — не есть хорошо). В моем же подходе можно создавать сущности (переменные, структуры) в совершенно разных местах. При этом всегда в map можно посмотреть, где и что лежит, если требуется. Это основное преимущество.
2) Добавляем в проект исходный файл, в котором находится определение данной структуры в сегменте ".myPreciousData" (чтобы можно было несколько структур в одном файле располагать, например). Можно совершенно нормально задавать начальные значения, использовать сложне типы и т.п. Очень приятно и читабельно.

Отписал выше. Нарушается инкапсуляция и падает логика. Хорошо заметно на больших проектах, когда твоя структура под пару сотен элементов в куче по типу: «границы ШИМ каналов таймеров + параметров интерфейса uart + режим работы USB»…
3) *** МАГИЯ *** Компилируем этот исходный файл в несколько РАЗНЫХ объектных. Реализация зависит от системы сборки. В Eclipse CDT и некоторых других можно нашаманить несколько «ссылок» на файл с разными именами, что позволяет скомпилировать один исходник под несколькими именами (вообще сказака). В CMAKE, make и прочих руками описываем компиляцию этого файла несколько раз с разными выходными именами (объектных файлов) — тоже отлично. В тяжёлых случаях ставим хук на компиляцию и тупо копируем исходный объектный файл под несколькими именами (тем более, что у меня всегда есть хук на линкер, т.к. есть автоинкремент версии сборки с подстановкой даты сборки) — кривова-то уже.
4) В линкере спокойно раскидываем наши файлы (с указанием имени файла и нашего сегмента ".myPreciousData", чтобы лишнего не попало) по разным секциям создавая символы начала секций. Секции располагаем где хотим. Инициализацию секций в ОЗУ делаем так же, как и для обычных сегментов данных в ОЗУ (т.е если проект правильно построен, то прописываем в таблицы инициализации).
5) В исходный текстам импортируем описанные в скриптах ликера имена с типом указателя на нашу структуру.

В подходе, который был на работе, делалось проще. Просто в RAM создавалась структура (та самая, глобальная). Просто пустая. Ее начальные данные в flash. В модуль работы с этим делом из компоновщика передавались указатели на области flash. Все. По сути, это недоработанный ваш метод. Вашим можно еще узнать, сколько заполнено и выводить это. Хотя makefile/CMakeLists будет выглядеть действительно страшно. А я категорически против «интересных» решений в этой области. Это делает код сложно воспринимаемым и ведет к вопросу у тех, кто должен поддерживать это, когда вас нет на рабочем месте или вы вовсе его сменили. Но это делает вас не заменяемым на работе… Настолько, что вам звонят даже после увольнения и просят рассказать, как оно работает… Потому что «нужно поддерживать», а никто не знает почему оно вообще работает… Прошу прощения. Немного накипело на эту тему. Приходилось сталкиваться с очень хитрыми системами сборки.
И ещё маленький секрет — у меня не просто раздельные таблицы иницализации секций data, bss и т.п.

Что такое «таблица инициализации data, bss...»? Интересно посмотреть.
На выходе полнейшая радость и удобство с нулевым оверхедом. И никакой магии на уровне исходного кода.

У вас дикий оверхед на уровне сборки. Чтобы в этом разобраться — придется не слабо повозиться, если изначально не знать всех нюансов. В моем же случае есть небольшой наклодняк со стороны кода (хоть и невидимый для программиста), но никаких проблем на уровне сборки.
Кучу разных данных, никак не связанных между собой по смыслу требуется описывать в одном месте

Ну, если мы про отказоустойчивость говорим, то это, как раз, хорошо и правильно. Это позволяет писать легко-верифицируемый код. Откуда уверенность что все критичные данные находятся в защищаемый секциях? А точно правильно работает переключение на резервный набор данных? Это обязательно должно быть собрано в одном месте иначе верифицуемость такого кода под большим вопросом.
При желании это можно по структурам в зависимости от посдсистемы или даже по разным заголовкам (кто мешает структуры определять в разных заголовках, а потом собирать в одну?). Если программа правильно написано, то объединение защищаемых данных в одном месть это хорошо. Другое дело, что народ то бездумо пихает в защищаемые данные то, что не нужно, то наоборот критичные данные оказываются без резерва. Но это уже вопрос общего дебилизма и формализма. В 90% случаев вообще можно обойтись фоновым контролем целостности RO-секции программы и переключением на резервную копию через перезагрузку.
Описывайте параметры своих ШИМов в отдельных структурах и потом объединяйте в одну.
В общем, тут категорически не согласен — цель же условие ТЗ «4 копии каких-то данных иметь» исполнить, а ещё и отказоустойчивую систему сделать.

И у меня, кстати, сложились очень нехорошие подозрения, что вы вообще что-то не то делаете. Вы вообще все константы защищаете по ТЗ? А так понимаю, что да. А как насчёт всяких служебных, которые компилятор для себе генерирует? Там же до 30% RO-данных это служебные константы и всякие адреса переходов, которые компилятор нагенерировал. Их, что, защищать не нужно? А какой тогда смысл защищать «режим работы UART», если у вас в десять раз больше констант, относящихся к критичному коду (в том числе работы с тем же UART) оказываются незащищёнными? У вас в этом случае отказоустойчивость не на два порядка вырастает, как по ТЗ задумывалось, а на 5% — а смысл? В этом случае, как я говорил — делается защита вообще всей области RO-данных и несколько экземпляров этих данных с переключением с случае нарушения целостности рабочей копии. Это можно сделать только тем способом, что я описал. Иначе ТЗ, вроде бы как, выполняется, но отказоустойчивость не только растёт, но иногда и падает. Естественно, что настоящей проверки и сертификации такой код пройти не может (на практике в России и не такой проходит).

Вопрос поддержки кода решается документацией и разделением квалификации исполнителей. Если студенты пишут критичный код, то проекту ничего не поможет. А если критичный код написан профессионалом, проверен, документирован и дальше в него никто не лезет, то можно в какие-то другие части с студентов пускать после прочтения инструкции.
Вопрос же заменяемости… Чтобы писать качественный отказоустойчивый год, нужен специалист соответствующего уровня. Если он есть и решение документировано, то он без проблем разберётся. Абсолютно ничего сложного в моём решении — любой, кто значет, как работает линкер без проблем разберётся. Мы же, в конце-концов, говорим про работу, требующую специалиста высокой квалификации и достаточно редкой специализации.

Я прямо, даже растерялся — мы сейчас точно про разработку авионики говорим, что ушедшего специалиста заменит некем? Ну, в российской, как правило — да. В общем и целом — нет.
Если «звонят после увольнения», то это проблемы звонящего и верный признак, что звонящий занимается не своим делом. Качественный код стоит денег, квалификации и определённой организации работы. А на практике, обычно оказывается, что вместо ушедшего специалиста посадили студента на 35 тысяч, который попутно изучает программирование. Ну, так это проблемы — оно за это деньги получает. Продают-то они этот код не за пять рублей.
Специалистов должно быть несколько, их уровень и опыт должны соответствовать задаче, а документация должна иметься.

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

Про таблицу инициализации сегментов. Это выглядит примерно так:
/* Константы флагов регионов. */
 /* Признак обнуляемого региона (BSS). LOAD_ADDR не используется.
  * Взаимно исключающий с другими флагами FREG_TYPE_XXX. */ 
 FREG_TYPE_ZERO = 0;
 /* Регион должен быть скопирован. LOAD_ADDR задёт адрес, куда копируется регион. Взаимно исключающий с другими флагами FREG_TYPE_XXX. */ 
 FREG_TYPE_COPY = 1;
 /* Обрабатывать только первые 32 байта региона. Используется для инициализации динамической кучи. */
 FREG_PROCESS_ONLY32 = 0x8;
 /* Признак того, что регион должен быть обработан на ранних этапах инициализации ппрограммы. */
 FREG_STARTUP = 0x80;
 
 .region_array : ALIGN(4) ONLY_IF_RO
 {
	. = ALIGN(4);
	__region_list_start__ = .;

 	/* RW-DATA IN RAM. STARTUP REGION. */	
	LONG( FREG_TYPE_COPY | FREG_STARTUP )
	LONG( ADDR(.data) );
	LONG( LOADADDR(.data) );
	LONG( SIZEOF(.data) );

 	/* RO-DATA IN RAM. STARTUP REGION. */	
	LONG( FREG_TYPE_COPY | FREG_STARTUP )
	LONG( ADDR(.rodata_ram) );
	LONG( LOADADDR(.rodata_ram) );
	LONG( SIZEOF(.rodata_ram));

 	/* CODE IN RAM. STARTUP REGION. */	
	LONG( FREG_TYPE_COPY | FREG_STARTUP )
	LONG( ADDR(.text_ram) );
	LONG( LOADADDR(.text_ram) );
	LONG( SIZEOF(.text_ram) );

	/* BSS RW-REGION. STARTUP REGION. */
	LONG( FREG_TYPE_ZERO | FREG_STARTUP )
	LONG( ADDR(.bss) );
	LONG( 0 ) /* Not used for zeroed regions. */
	LONG( SIZEOF(.bss) );
	
	/* PRIMARY HEAP - FIRST 32 BYTES MUST BE ZEROED AT STARTUP. */
	LONG( FREG_TYPE_ZERO | FREG_PROCESS_ONLY32 | FREG_STARTUP )
	LONG( __heap_start__ );
	LONG( 0 ) /* Not used for zeroed regions. */
	LONG( __heap_end__ - __heap_start__ );

	/* SECONDARY HEAP - FIRST 32 BYTES MUST BE ZEROED AT STARTUP. */
	LONG( FREG_TYPE_ZERO | FREG_PROCESS_ONLY32 | FREG_STARTUP )
	LONG( __heap2_start__ );
	LONG( 0 ) /* Not used for zeroed regions. */
	LONG( __heap2_end__ - __heap2_start__ );

	__region_list_end__ = .;
} >FLASH


С точки зрения C/C++ это выглядит как массив структур:
typedef struct {
	const uint32_t flags; ///< Flags.
	uint32_t* dst; ///< Destination (runtime start address of the region).
	const uint32_t* src;     ///< Source (load start address of the region).
	const uint32_t size; ///< Size of the region.
} RegionListItem;

Массив в коде доступен под именем "__region_list_start__".
Стартап код до вызова любого другого кода (кроме инициализации стека, WatchDog и тактового генератора), вызывает процедуру (которая тоже находится стартап-сегменте и потому доступна с самого начала), которая проходит по таблице и обрабатывает все регионы с флагом «FREG_STARTUP».
Потом проходим по таблицам инициализации стандартных библиотек и вызываем их код. Вы же, надеюсь, проходите по таблицам "__preinit_array_start__" и "__init_array_start__" — стандартные таблицы для GCC — без этого маcса глюков будет.
Потом стартуем стандартную библиотеку. И уже в main завершаем инициализацию железа и снова вызываем ту же функцию инициализации сегментов. Но теперь уже она инициализирует только сегменты без флага «FREG_STARTUP» — находящиеся во внешней памяти, во всяких аппаратно-защищённых областях памяти т.п. Потом включаем защиту RO-областей памяти и кода в MMU если есть (а какой может быть отказоустойчивый код без этого) и, вот, после этого мы готовы стартовать основной код.
В ту же функцию инициализации сегментов можно затолкать функционал выбора неповреждённой копии сегмента при ошибке CRC. Если есть аппаратная поддержка через ремапинг адресов, то можно и код защищать. И только так можно по-настоящему что-то защитить дублированием. Иначе получается что-то совсем не то, что хотелось бы.
Именно так это делается в «best practices» и серьёзных библиотеках.
Да, и менеджер динамической кучи подключаем свой, т.к. мы не знаем, как работает стандартный и верить в критических применениях не можем.
Ну, если мы про отказоустойчивость говорим, то это, как раз, хорошо и правильно. Это позволяет писать легко-верифицируемый код. Откуда уверенность что все критичные данные находятся в защищаемый секциях? А точно правильно работает переключение на резервный набор данных? Это обязательно должно быть собрано в одном месте иначе верифицуемость такого кода под большим вопросом.

Данный вопрос решается просмотром map файла (чтобы убедиться, что все сохраняемые данные в нужных местах).
При желании это можно по структурам в зависимости от посдсистемы или даже по разным заголовкам (кто мешает структуры определять в разных заголовках, а потом собирать в одну?).

Пробовали и такое. Все равно есть мега-супер-структура. Мы просто придерживаемся соглашения том, что все переменные должны быть спрятаны внутри одного файла (просто C без объектов когда). А наружу торчать только методы. И сейчас единственное, что не вписывалось в это видение — как раз эта глобальная структура. Тут нет смысла спорить. Все решается принятием удовлетворяющей команду разработчиков общей концепции. И все проекты должны делаться согласное ей.
Если программа правильно написано, то объединение защищаемых данных в одном месть это хорошо.

Описал выше.
Другое дело, что народ то бездумо пихает в защищаемые данные то, что не нужно, то наоборот критичные данные оказываются без резерва. Но это уже вопрос общего дебилизма и формализма.

Тут полностью согласен. Мы составляем внутреннее ТЗ, в котором явно прописано, что и где должно лежать.
В 90% случаев вообще можно обойтись фоновым контролем целостности RO-секции программы и переключением на резервную копию через перезагрузку.

Во время работы нет надобности контролировать RO секцию. Она анализируется только при включении устройства. Устройство должно включаться за очень малый промежуток времени (они на летающем аппарате. Хоть и без людей. Если бы там были люди, то такого вопроса бы вообще не было. Поскольку там была бы внешняя flash, аппаратно продублированная и прочие штуки).
Описывайте параметры своих ШИМов в отдельных структурах и потом объединяйте в одну.

Отписал, почему не согласен (принятое соглашение между разработчиками).
В общем, тут категорически не согласен — цель же условие ТЗ «4 копии каких-то данных иметь» исполнить, а ещё и отказоустойчивую систему сделать.

4 секции — это разбиение более высокого требования «Обеспечить отказоустойчивое хранение данных конфигурации устройства.» (+ подпункты про изменяемость при настройке и прочее).
И у меня, кстати, сложились очень нехорошие подозрения, что вы вообще что-то не то делаете. Вы вообще все константы защищаете по ТЗ? А так понимаю, что да.

Ни в коем случае. Как писал выше, только то, что может быть изменено человеком, который будет интегрировать устройство в финальный комплекс.
А как насчёт всяких служебных, которые компилятор для себе генерирует? Там же до 30% RO-данных это служебные константы и всякие адреса переходов, которые компилятор нагенерировал. Их, что, защищать не нужно? А какой тогда смысл защищать «режим работы UART», если у вас в десять раз больше констант, относящихся к критичному коду (в том числе работы с тем же UART) оказываются незащищёнными? У вас в этом случае отказоустойчивость не на два порядка вырастает, как по ТЗ задумывалось, а на 5% — а смысл? В этом случае, как я говорил — делается защита вообще всей области RO-данных и несколько экземпляров этих данных с переключением с случае нарушения целостности рабочей копии. Это можно сделать только тем способом, что я описал. Иначе ТЗ, вроде бы как, выполняется, но отказоустойчивость не только растёт, но иногда и падает. Естественно, что настоящей проверки и сертификации такой код пройти не может (на практике в России и не такой проходит).

Вот тут отмечу то, чего не было в статье. Да, я не храню копию данных того, что генерирует компилятор. И констант, которые не меняются. Но тут есть нюанс. Дело в том, что я 100% знаю, что весь код моей программы в момент включение — без ошибок (аппаратных). Имею ввиду, что прошивка соответствует той, которая была зашита. Об этом заботится загрузчик при включении. Вся прошивка целиком тоже дублирована. Содержит свою контрольную сумму. В статье же речь именно о изменяемых константах, которые может менять пользователь в отрывае от прошивки (ведь эти параметры лежат совсем в других областях flash. Опять же повторюсь. Если бы речь шла о перевозке людей, тогда все эти данные лежали вообще в отдельных накопителях с аппаратным резервированием. Т.к. нельзя позволить микроконтроллеру писать свою же память. Хоть и в другой странице.).
Вопрос поддержки кода решается документацией и разделением квалификации исполнителей. Если студенты пишут критичный код, то проекту ничего не поможет.

Это попытка оскорбить или сравнение с типовым случаем?) (типа я тоже студент формально… Хоть и не долго осталось до окончания)
А если критичный код написан профессионалом, проверен, документирован и дальше в него никто не лезет, то можно в какие-то другие части с студентов пускать после прочтения инструкции.
Вопрос же заменяемости… Чтобы писать качественный отказоустойчивый год, нужен специалист соответствующего уровня. Если он есть и решение документировано, то он без проблем разберётся. Абсолютно ничего сложного в моём решении — любой, кто значет, как работает линкер без проблем разберётся. Мы же, в конце-концов, говорим про работу, требующую специалиста высокой квалификации и достаточно редкой специализации.

Его трудно найти, легко потерять, и нереально удержать в Красноярске… Тем более за не особо высокую ЗП… Тут уже далеко не технический вопрос. Но и про документацию и прочее. В фирмах, которых мне довелось работать, документация — это последнее, до чего доходят руки. ТЗ иметь на бумаге вот и то редкость. В текущей фирме только начал появляться весь этот задел. Когда сначала ТЗ, потом под-требования, тесты, потом код. И это хорошо. Но фирме больше 7 лет. Что говорить о фирмах, которые «со подрядчики» и т.д.? Довелось работать с людьми, которые разрабатывали многомиллионное оборудование по ТЗ на словах) Тесты — на заказчике. Устройство не серийное. Там устройство не летало. Это оборудование для весьма не маленького предприятия. И… Там двух слойная плата управления без особых защит, никакого резервирования и т.д. При неудачном стечении обстоятельств (вероятность которых в условиях цеха очень высока), оборудование придет в негодность. Ну а надо всем там было срочно, переделывать не надо. работает же. Прототип? И так пойдет… В общем, все зависит тут от конторы. Но я считаю, что чем решение топорнее, тем лучше. Проще будет разгрузиться с себя 100500 разных проектов и технический долг будет хоть капельку меньше.
Я прямо, даже растерялся — мы сейчас точно про разработку авионики говорим, что ушедшего специалиста заменит некем? Ну, в российской, как правило — да. В общем и целом — нет.

Не просто про Россию, а про Красноярск) Днем с огнем не сыщешь человека, который бы писал код хоть как-то вменяемо. При этом был готов если что переквалифицироваться в тех. писателя, пусконаладчика и т.д. Фулл-стек ембеддера короче).
Если «звонят после увольнения», то это проблемы звонящего и верный признак, что звонящий занимается не своим делом.

Это значит, что человек ушел и не оставил ни строчки пояснений, а его код местами выглядит вот так:
p = 1048576.0 - (double)adc_P;
	p = (p - (var2 / 4096.0)) * 6250.0 / var1;
	var1 = ((double)calibrationTabel.dig_P9) * p * p / 2147483648.0;
	var2 = p * ((double)calibrationTabel.dig_P8) / 32768.0;
	p = p + (var1 + var2 + ((double)calibrationTabel.dig_P7)) / 16.0;

Конечно, иногда помогает вдумчивое чтение документации. Но когда человек «для экономии ресурса» умножает 2 константы из документации и использует как одно число прямо в коде — догадаться об этом ОЧЕНЬ трудно. Да и человек порой писал код этот в свободное от командировок по интеграции устройств время. Так сказать «за чашкой чая». И в то время, когда компания только зарождалась — это было нормально и единственным решением чтобы выжить. Но те времена прошли и настало время уже следующему поколению сотрудников разбираться с этим… кодом) И «одна большая структура, которая лежит типа в оперативной памяти, но под нее зарезервировано с запасом 2 блока, в которые данные не пойми как попадают» — один из примеров. В вашем случае вы действительно их туда кладете. Я же столкнулся с реализацией, когда просто 2 страницы резервировались. И не важно, влезет туда структура или нет (конечно, обычно структуры 1-3 кб, а резервировалось 128, но все же).
Качественный код стоит денег, квалификации и определённой организации работы. А на практике, обычно оказывается, что вместо ушедшего специалиста посадили студента на 35 тысяч, который попутно изучает программирование. Ну, так это проблемы — оно за это деньги получает. Продают-то они этот код не за пять рублей.
Специалистов должно быть несколько, их уровень и опыт должны соответствовать задаче, а документация должна иметься.

3 человека хватит на все))) На каждом из которых куча проектов и задач не связанных непосредственно с программированием (например, понять почему упал борт, почему QNX внезапно начинает видеть битые блоки при включении при определенном стечении обстоятельств и прочие штуки).
В общем, тоже не согласен. Решение на самом деле достаточно несложное и если кто-то его не тянет, то возможно ему не стоит заниматься отказоустойчивыми системами, ибо, как я уже сказал, никак иначе по-настоящему отказоустойчивый код вы просто не напишите — так, симуляцию отказоустойчивости, может быть. Короче, если кто-то не обладает необходимыми знаниями для решения задачи, то это его проблемы, а не того, кто обладает.

Про достаточный уровень — соглашусь. Про нехватку кадров и надобность в более простых по реализации решений, но при этом не уступающим по качеству — отписал выше.
Про таблицу инициализации сегментов. Это выглядит примерно так:

Красиво! Реально интересная тема. Не видел раньше, чтобы так пользовались LD скриптом. Обычно +- редактируют LD из демонстрационного проекта и на этом заканчивают.
С точки зрения C/C++ это выглядит как массив структур:

Ну это понятно, да. Интересно. Очень интересно.
Массив в коде доступен под именем "__region_list_start__".
Стартап код до вызова любого другого кода (кроме инициализации стека, WatchDog и тактового генератора), вызывает процедуру (которая тоже находится стартап-сегменте и потому доступна с самого начала), которая проходит по таблице и обрабатывает все регионы с флагом «FREG_STARTUP».

Ну это следует из такой организации.
Потом проходим по таблицам инициализации стандартных библиотек и вызываем их код. Вы же, надеюсь, проходите по таблицам "__preinit_array_start__" и "__init_array_start__" — стандартные таблицы для GCC — без этого маcса глюков будет.

Вот тут уже не подскажу, но вроде это нужно только если вы C++ используете. Там конструкторы глобальных объектов и прочее. Если мне память не изменяет. В чистом C вроде не нужно. Но тут не уверен.
Потом стартуем стандартную библиотеку.

Всегда корю себя за то, что пользуюсь стандартными библиотеками. По-хорошему, в настоящей авиации нельзя. Ни printf ничего такого. Пока используем. Но вообще нельзя.
И уже в main завершаем инициализацию железа и снова вызываем ту же функцию инициализации сегментов. Но теперь уже она инициализирует только сегменты без флага «FREG_STARTUP» — находящиеся во внешней памяти, во всяких аппаратно-защищённых областях памяти т.п. Потом включаем защиту RO-областей памяти и кода в MMU если есть (а какой может быть отказоустойчивый код без этого) и, вот, после этого мы готовы стартовать основной код.

Мне пока так и не удалось кстати настроить MMU + FreeRTOS. Ну это пока никому не требовалось, но вообще сам понимаю, что это необходимо. Просто времени не хватает сделать как положено. Стараюсь эксперементировать с такими штуками дома. А то на работе оправдать то, что ты возился с MMU вместо решения «реальных задач» — достаточно сложно.
В ту же функцию инициализации сегментов можно затолкать функционал выбора неповреждённой копии сегмента при ошибке CRC. Если есть аппаратная поддержка через ремапинг адресов, то можно и код защищать. И только так можно по-настоящему что-то защитить дублированием. Иначе получается что-то совсем не то, что хотелось бы.
Именно так это делается в «best practices» и серьёзных библиотеках.

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

Мы используем стандартный FreeRTOS-овский. Он переопределяет malloc и free. Но вообще в наших устройствах, которые летают нет malloc/free/new/delite. Только статически размеченная ram. В угоду отказоустойчивости.
«Вот тут уже не подскажу, но вроде это нужно только если вы C++ используете. Там конструкторы глобальных объектов и прочее. Если мне память не изменяет. В чистом C вроде не нужно. Но тут не уверен.»

Вот, не хотелось бы обижать, но вот про это я и говорю ;) Тут вопрос, есть ли в фирме кто-то, кто это точно знает, применительно к используемым компилятором. Если есть, то это его зона ответственности. Если нет, то мы имеем то, что имеем — люди учатся на живом коде и где-то что-то летает, что содержит ошибки и непредсказуемое поведение.
И про malloc/free тоже. Эти функции используются стандартной библиотекой даже если вы вы их не пользуете и даже если вы стандартную библиотеку не используете. Например, если вы используете в плавающую точку в любом виде на GCC, то вы уже используете динамическую кучу. А если вы ещё и FreeRTOS используете, то нужно убедиться, что структура "_reent" переключается и работает правильно.
MMU не просто нужен. Если есть, то он обязателен в абсолютно любом проекте, т.к. позволяет найти ошибки, которые иначе вообще никак не найдёшь. Настраивается он за 15 минут, если мы говорим про базовый MMU и разделение регионов памяти (запрет исполнения из ОЗУ, запрет записи в RO-области ОЗУ). Полноценная виртуальная память, которая тоже MMU, это уже сложнее.
Вот, не хотелось бы обижать, но вот про это я и говорю ;) Тут вопрос, есть ли в фирме кто-то, кто это точно знает, применительно к используемым компилятором. Если есть, то это его зона ответственности. Если нет, то мы имеем то, что имеем — люди учатся на живом коде и где-то что-то летает, что содержит ошибки и непредсказуемое поведение.

А есть иные варианты, кроме как учеба на живом коде? О многих вещах даже не задумываешься до того, как не столкнешься. А на чтение документации обычно времени практически нет. Ибо в моем случае я не только пишу код, но еще и тесты, требования, и т.д. Хотя хотелось бы как вы, конечно. Просто сидеть и копать в свою область. Это полезно, я считаю.
И про malloc/free тоже. Эти функции используются стандартной библиотекой даже если вы вы их не пользуете и даже если вы стандартную библиотеку не используете. Например, если вы используете в плавающую точку в любом виде на GCC, то вы уже используете динамическую кучу.

Хочу обоснований. Я просто осматривал _sbrk. Она не дергалась вроде ничем при работы с плавающей кучей (есть аппаратный блок работы с плавающей точкой). Куда копать?
А если вы ещё и FreeRTOS используете, то нужно убедиться, что структура "_reent" переключается и работает правильно.

Вот честно. Даже не знал о существовании такой штуки. Об этом статей не видел. Вот и не знал. Смотрю, вы много знаете нюансов. Был бы рад, если бы хоть писали об этом. С радостью бы почитал (без сарказма).
MMU не просто нужен. Если есть, то он обязателен в абсолютно любом проекте, т.к. позволяет найти ошибки, которые иначе вообще никак не найдёшь. Настраивается он за 15 минут, если мы говорим про базовый MMU и разделение регионов памяти (запрет исполнения из ОЗУ, запрет записи в RO-области ОЗУ). Полноценная виртуальная память, которая тоже MMU, это уже сложнее.

С просони не так написал. Не MMU, а MPU. MMU нет на тех процессорах, с которыми приходится работать. А вот настроить MPU для той же защиты при переключения контекста FreeRTOS — хотелось бы. А то без дела лежит… Руки не доходят.
А есть иные варианты, кроме как учеба на живом коде? О многих вещах даже не задумываешься до того, как не столкнешься. А на чтение документации обычно времени практически нет. Ибо в моем случае я не только пишу код, но еще и тесты, требования, и т.д. Хотя хотелось бы как вы, конечно.

Вот, проблема в том, что так делать можно, но не нужно. Качество как гарантировать? Опять же, писать тесты, и требования это хорошо полезно, но опыт в этом есть или тоже по ходу нарабатывается? Т.е контора под видом коммерческого продукта впаривает непонятно что.
Это не к вам претензия, это к российским реалиям, в которых я двадцать лет отработал. Плохо то, что в результате многие российские разработчики даже не понимают, как нужно правильно разработку вести и буквально поколениями воспроизводят неправильный опыт.
С MMU/MPU в мире ARM это вопрос терминологии, кстати. MPU однозначно нужен. Полноценный же MMU с преобразованием адресов нужен в 99% случаев лишь для выполнения функций MPU. Он ставит очень жёсткие требования к структуре программы. Для большинства задач это просто не нужно. Или начинаются костыли, когда зачем-о включают MMU, а потом начинают придумывать, как бы его обойти, т.к. он работе программы мешает. У меня за всю историю было только два проекта на контроллерах, где нужно было преобразование адресов. В одном пришлось сделать полноценную подсистему виртуальной памяти, т.к. был процессор с полноценными многоуровневыми кэшами и для получения необходимой производительности пришлось MMU на полную катушку врубать (т.к. есть регионы память, где вообще кэша не должно быть, есть, где writethrough, есть где witeback и т.п., а управление этим всем потребовало включения страничного преобразования). А во втором был большой сложный многопоточный проект с кучей независимых процессов, который достиг того предела сложности, когда включение MMU позволяет быстрее находить ошибки, но и там можно было обойтись переключением MPU в планировщике задач. Во всех остальных случаях полноценный MMU был не нужен и даже вреден.
Вот, проблема в том, что так делать можно, но не нужно.

Приходится… Ибо не знаю, где можно было бы этому профессионально обучится. Я тут говорил, что почти уже не студент. Но, честно, толку от учебы не было вообще (в плане технического развития). Мы на предмете «микроконтроллеры» 2 семестра смотрели на видео с ютуба по программированию ПЛК на языке графическом LD (нет, не язык скриптов линкера… Погуглите. Эта интересный на вид конструктор. Но никак не программирование).
Качество как гарантировать?

Тестами и внимательным отсмотром кода, когда есть время.
Опять же, писать тесты, и требования это хорошо полезно, но опыт в этом есть или тоже по ходу нарабатывается?

Конечно самостоятельно. Смотреть как делают другие и учиться на примере этого. Набивать свои шишки и продолжать искать нужный путь. Читать литературу, что есть. Не видел у нас мест. где бы этому реально учили.
Т.е контора под видом коммерческого продукта впаривает непонятно что.

В защиту некоторых контор в которых работал скажу, что они хотя бы обеспечивают поддержку и идут на встречу заказчиком. Когда что-то деделывается по месту. Приходилось работать и с теми, что просто клали на все и говорили что-то типа «устройство готово. Доработки — другое устройство. Заказывайте заново».
Это не к вам претензия, это к российским реалиям, в которых я двадцать лет отработал. Плохо то, что в результате многие российские разработчики даже не понимают, как нужно правильно разработку вести и буквально поколениями воспроизводят неправильный опыт.

Ну так… Да)… А что еще остается? Как правило, чем дольше контора живет, тем лучше у нее все с продуманностью. Обычно когда контора закрывается, ее опыт неудач и наоборот успехов уходит вместе с ней. Ну и в опыте сотрудников остается. Которые на новом месте уже пытаются не совершать определенных ошибок, с которыми столкнулись на прошлом месте работы.
С MMU/MPU в мире ARM это вопрос терминологии, кстати. MPU однозначно нужен. Полноценный же MMU с преобразованием адресов нужен в 99% случаев лишь для выполнения функций MPU. Он ставит очень жёсткие требования к структуре программы. Для большинства задач это просто не нужно. Или начинаются костыли, когда зачем-о включают MMU, а потом начинают придумывать, как бы его обойти, т.к. он работе программы мешает. У меня за всю историю было только два проекта на контроллерах, где нужно было преобразование адресов. В одном пришлось сделать полноценную подсистему виртуальной памяти, т.к. был процессор с полноценными многоуровневыми кэшами и для получения необходимой производительности пришлось MMU на полную катушку врубать (т.к. есть регионы память, где вообще кэша не должно быть, есть, где writethrough, есть где witeback и т.п., а управление этим всем потребовало включения страничного преобразования). А во втором был большой сложный многопоточный проект с кучей независимых процессов, который достиг того предела сложности, когда включение MMU позволяет быстрее находить ошибки, но и там можно было обойтись переключением MPU в планировщике задач. Во всех остальных случаях полноценный MMU был не нужен и даже вреден.

Интересно. Я просто всегда был уверен, что MMU реально нужен только чтобы QNX/Linux запустить.
Вообще, считаю, что достаточно мало информации по таким узким местам, как использование кучи разными стандартными функциями, используемые ресурсы стандартными библиотеками, различные варианты проведений и прочее. Вообще, честно, до ваших примеров был уверен, что все заканчивают «свое развитие» на том, что понимают, как работает стандартный ld скрипт, добавляют в него внешнюю память и все. Теперь вижу, что нет. Был бы рад, если бы порекомендовали литературу на эту тему. Всегда рад изучить вопрос подробнее.
Гхм… Что значит «мало информации»? А вы её искали? Литературу нужно копать в сторону «портирование 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? Вы должны как минимум знать, как она устроена и как её портировать собрать. Даже если не собираете и не портируете. Иначе гарантированно будет получать редкие и удивительные глюки в программах.
Под всем текстом: ничего себе! Про перекомпиляцию не знал. Когда я только пришел, мне строго настрого запретили лезть в newlib, ld и прочие штуки. Оно и понятно… Тогда был по сути без опыта работы (сам уже не плохо тогда, ИМХО, кодил, но под keil и знал тонкости работы с периферией МК, но не знал тонкостей работы сборки. Т.к. keil все это скрывал). Потом реальная работа, gcc и т.д. Ну и как мне сказали, что там все настроено и трогать не надо. А потом я узнал, что это просто скопированная папка с гита))) После ваших слов, думаю, что стоит максимально подробно остановиться над этим вопросом и изучить как положено. Кстати. Про интересные глюки… У нас они решались выбором newlib-nano (стандартного под платформу с помощью флагов компоновщика) и вызовом функций типа setvbuf (вроде так называлось), где устанавливалось для всех потоков 1 буфер и подобные костыли. Достаточно костыльно.
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 тоже очень плохо для контроллеров походит — медленный и слабопредсказуемый. Нужно свой легковесный ставить.
Кстати, если вы только изменяемые пользвателем данные резервируете и только при старте проверяете, то ваш велосипед совсем плох. Только отдельные сектора памяти, описание всех данных в общей структуре и выбор копии при старте. И никакого шаманства не нужно в 90% случаев — старые добрые указатели, либо копия структуры в ОЗУ. Всё остальное — очень вредные велосипеды и порочный путь, который усложняет структуру программы и её верификацию.
Кстати, если вы только изменяемые пользвателем данные резервируете и только при старте проверяете, то ваш велосипед совсем плох.

Чем плох? Мы при старте убеждаемся, что все хорошо или делаем, чтобы все было хорошо и далее работаем точно зная, что данные целостны. Хотя, конечно. есть вероятность, что может что-то пойти не так из-за аппаратки. Но это уже не очень понятно как обходить. Конечно, идут проверки на != -1 (либо все 1. Если хоть 1 бит выбился, то не валиден флаг и т.д.). Но таких штук лично я почти не применяю. Не было случая, когда реально пригодилось.
Только отдельные сектора памяти, описание всех данных в общей структуре и выбор копии при старте. И никакого шаманства не нужно в 90% случаев — старые добрые указатели, либо копия структуры в ОЗУ.

Так вроде я и писал об этом. Ну только если копия повредилась, ее еще по другому экземпляру восстанавливают.
Ну, тут мы в совсем тонкие материи углубляемся. 99% случаев нужно делать общую структуру с указателями ((или копировать данные при старте ОЗУ, но тоже в общую структуру). Тут нужно уже целые лекции читать. Как это подсистемам и по модулям разнести это уже прикладной момент — тоже способы есть.
Для снижения времени обращения к переменным настроек устройства во время работы требуется держать актуальные значения в RAM (обычно объем таких данных варьируется от десятка переменных до 3 килобайт в зависимости от устройства).

Скажите, пожалуйста, а вот этот пункт был обоснован какими-то реальными замерами?

Да. Был опыт работы над устройством измерения частот с 8 каналов. Каждый импульс приходило прерывание. Из него я узнавал на каком канале произошел импульс и его длительность (ведется статистика отклонений (наибольшее, наименьшее) и прочие штуки по тех заданию). И вот. Параметры, которые следует фиксировать задаются пользователем. Для того, чтобы удовлетворить требованиям пришлось разместить методы обработчики в RAM, как и саму таблицу векторов прерываний. И каждый раз дергать данные из FLASH, выполняя обработчик из RAM оказалось очень накладно. Конкретных цифр привести не могу. Со временем пришло понимание, что именно настройки даже лучше размещать не в RAM, а в CCRAM. Чтобы разгрузить шину (код выполняется из RAM, а данные берутся параллельно из CCRAM). Про CCRAM справедливо только для F405 (с которым работал). Возможно и для других мк/серий тоже.

Интересно, спасибо.

CCMRAM отвязана от DMA, т.е. отсутствуют коллизии при доступе CPU и DMA к RAM. За счет этого теоретически возможно повышение быстродействия.
Да. И это тоже. Вообще, по опыту, CCMRAM идеальное место для хранения стеков/внутренних структур объектов FreeRTOS (если используется в проекте). Позволяет решить вопрос с тем, когда переполнение стека задачи затрагивает данные, с которыми работает прошивки в основной RAM.
В смысле, чтобы сделать чистый дамп данных перед уходом в failsafe? Или есть какой то способ восстановится после переполнения стека?
У меня зачастую работа с устройствами, в которых «нужно выжить любой ценой и не уронить аппарат». Так что там как правило задается с очень сильным запасом все. Тут скорее имею ввиду, что не получится ситуации, когда перешедший за пределы выделенного стек задачи не перетрет данные, с которыми работает программа. То есть это дает возможность избежать ситуации, когда код продолжает работать, но уже с битыми данными. И начинаются приключения типа «в конфиге же было задание шим от 0.1 до 0.7 держать (скважность), почему вдруг резко начал выдавать 0.9? (то есть значение момент обновилось в таймере, а там уже было битое значение из-за того, что стек соседней задачи был поблизости и перетер данные, с которыми работает другой поток.
Ну в таком режиме наверное проще резервировать область на переполнение и проверять ее циклично. Хотя, конечно, если есть возможность перестраховаться, то лучше ей воспользоваться. А что за устройства, авионика? В промышленной функциональной безопасности принцип скорее «не уверен — урони аппарат надежно прежде чем сам упадет»
Да. Авионика. Конечно же, статика, защиты от зависания, стеки в несколько раз больше, чем нужно и прочие штуки по стандартам. Но если есть аппаратная возможность, почему бы еще и ей не воспользоваться? А уронить самому, понятное дело… Начиная с тестов алгоритмов в симуляции (как-нибудь напишу про это), заканчивая тестами на длительное выполнение.
FLASH почти всегда имеет ненулевый задержки, а статическая память — нулевые. Кроме того, контроллер непрерывно загружает код из FLASH и в результате конкурирует с загружаемыми данным (т.к например у ARM раздельные шины данных и кода). А там ещё и всякие prefetch и кэши есть, которые могут FLASH на несколько тактов загружать. В результате перенос данных в ОЗУ на некоторых алгоритмах может поднять быстродействие на треть, если не больше. Правда, народ так же часто забывать, что с ОЗУ всякие DMA могут работать параллельно, а они 50% шины забирают (на типовых ARM) в результате «быстрое» ОЗУ может оказаться сильно медленнее «медленного» FLASH.
А ещё бывает, что у контроллера несколько областей ОЗУ на разных шинах. Тогда сам Бог велел стек, часто используемые данные и критичный к быстродействию код обработчиков прерываний туда переложить, чтобы получить гарантированное предсказуемое время работы программы (DMA крутятся из другого ОЗУ на других шинах, данные не влияют предвобрку кода — наш код крутится с постоянной максимальной скоростью, чтоб ни просиходило в системе). Критично если нужно, например, иметь Ethernet (который грузит шину мама не горюй) или FS/HS USB (тже не слабая штуковина) и при этом хочется очень быстро и гарантированно что-то обрабатывать.

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

У меня было устройство на ST32F4xx, в котором нагруженный Ethernet и два дуплексных E1 канала связевых. Да, ещё и внешнее ОЗУ с кэшами данных с огромными по врекам встроенной памяти задержками. И вот там уже загрузка CPU начала доходить до 90%, что плохо. А нужно было на фоне по прерываниям отрабатывать приоритетную связь с FPGA. Всё начинало плавать в плане времени обработки прерывания. Оказалось что Ethernet и четыре DMA канала мегабитных USART сильно грузят шину. Вынос критичного кода и связанных с ним данных в отдельное ОЗУ на отдельной шине (там несколько диапазонов SRAM) полностью снял проблему — приоритетные прерывания стали отрабатывать за постоянное время.
В другом проекте нужно было хитрым быстрым ШИМом рулить. Использовалась плавающая точка с кучей вычислений прямо в прерываниях. Вынос RO-данных в ОЗУ сократил время обработки прерывания примерно на 15%.
Но было и обратное. На ST32F2xx работал фоновый активный DMA. И если константы в ОЗУ, то время обработки тяжёлых прерываний (а там многозадачность была и планировщик тоже можно считать «тяжёлым» прерыванием) начинало плавать. А если во FLASH, то всё становилось хорошо, т.к. FLASH не был загружен DMA.
А на устройстве на ST32L1xx, у которого среднее потребление было 11мкА константы во FLASH приводили к небольшому росту потребления, т.к. FLASH оказался чуть более прожорливым, чем ОЗУ. Сущая мелочь, буквально микроампер, но для того применения разница видна было.
Короче, от конкретного проекта и требуемых целей зависит на сколько оно надо. Нужно ещё помнить, что FLASH во многий процессорах ECC защищён (STM серий 4xx, L1xx, возможно 2xx — не помню уже — и множество других), а ОЗУ нет. И потому FLASH заметно надёжнее бывает.
Я всегда измеряю времена работы всех критичных секций кода осциллографом. Часто видно разницу. Если всё правильно сделано, то разница никогда не должна быть принципиально, но т.к. уже есть привычка всё делать надёжным, то правильное размещение кода и данных часто делает работу программы более предсказуемой и даёт больший запас по производительности.

Спасибо за развернутый ответ! Судя по всему, мне просто везло с задачами :)

Вадим — вы очень странно работаете с настройками, то-есть со статическими данными.
Вадим — вы очень странно работаете с настройками, то-есть со статическими данными.

Поясните. Как «не странно»?
Все статические данные помеченные как настройки — помещаются в отдельную секцию.
У секции есть начало-конец в «заводском» флеше. Сам флеш оканчивается раньше физического размера. И при загрузке определяем программным способом, какой из трёх вариантов грузить.
И только после этого можно работать.
Неважно где используются настройки, как выглядят, и сколько весят (главное чтоб влезло) — линкер сам соберёт их всех в один компактный блок данных.
«заводском» флеше

Это что? Имеется ввиду flash внутри микроконтроллера или одноразовая память внутри МК?
И при загрузке определяем программным способом, какой из трёх вариантов грузить.
И только после этого можно работать.

Ну оно так и сделано. А при обнаружении проблем еще и можно восстановить поврежденный экземпляр по живому.
Неважно где используются настройки, как выглядят, и сколько весят (главное чтоб влезло) — линкер сам соберёт их всех в один компактный блок данных.

Ну так тут так и сделано. Линкер сам собирает все сущности (переменные, структуры, массивы...), помеченные как настройки — в одну секцию. И создаются еще их копии в других блоках.
Сорри, но по мне ТЗ не полно, и это вызывет вопросы.

Для примера: толком не описана работа с копиями и тп. Я понимаю, что используется некий common sense, но…
Зачем столько копий? Зачем усложнять задачу залезая на системный уровень(LD script) если задача решает на прикладном уровне?
Зачем программисту каждый раз думать какую копию использовать? Можно реализовать всю логику в классе/функциях. И в дальнейшем это переиспользовать в других проектах.
Не очень понятен подход по работе с настройками со стороны самого приложения?
И сейчас уже 2020 год, можно же использовать С++? но даже на С все можно решить проще.

Самый простой и может быть самый наглядный способ, это объеденить все пользовательские данные в одной структуре/классе, которая доступна всем модулям, и написать класс который отвечает за загрузку этих данных в память и сохранение этих данных: LoadSettings/SaveSettings. И реализовать работу с копиями данных внутри этого класса.

У себя (для ардуинки) я сделал более сложную вещь, сделал маленькую базу данных которая позволяет хранить любое количество сеттингов (в зависимости от размера доступного флеша) по ключу. А данные могут быть простые типы с фиксированным размером как int, float, структ, так и сложные типа строк и массивов. Также это позволяет упростить разработку нескольких приложений на одном девайсе: используя разные ключи можно хранить сеттинги разных приложений не переустанавливая их каждый раз когда переключаешься на другое приложение. Структура хранения одна для всех приложений.
Также этот класс позволит скрыть реализацию для конкретной платформы: для ардуинки это флеш, для 8266 это уже может быть файловая система.
Сорри, но по мне ТЗ не полно, и это вызывет вопросы.

Обычно можно сказать спасибо за то, что оно вообще есть)
Для примера: толком не описана работа с копиями и тп. Я понимаю, что используется некий common sense, но…
Зачем столько копий? Зачем усложнять задачу залезая на системный уровень(LD script) если задача решает на прикладном уровне?

Тут как раз идет упрощение, а не усложнение. Конечный программист просто указывает, где хочет, чтобы его данные были. Просто в RAM/FLASH, или же в той области, которая может изменяться с помощью модуля взаимодействия с пользователем с функциями резервирования.
Зачем программисту каждый раз думать какую копию использовать? Можно реализовать всю логику в классе/функциях. И в дальнейшем это переиспользовать в других проектах.
Не очень понятен подход по работе с настройками со стороны самого приложения?

А ему не нужно думать об этом. Он просто ставит макрос, если хочет, чтобы модуль заботился о том, чтобы эти данные резервировались.
И сейчас уже 2020 год, можно же использовать С++? но даже на С все можно решить проще.

Ну есть порт этого метода и на C++. Просто объект, который при инициализации получает данные из переменных LD и далее работает с ними.
Самый простой и может быть самый наглядный способ, это объеденить все пользовательские данные в одной структуре/классе, которая доступна всем модулям, и написать класс который отвечает за загрузку этих данных в память и сохранение этих данных: LoadSettings/SaveSettings. И реализовать работу с копиями данных внутри этого класса.

Собственно, именно это и было сделано раньше. От меня лишь было дополнения, чтобы не пришлось пихать все в одну структуру. А чтобы можно было в любом месте объявить сущность (переменную/структуру...) и она бы обрабатывалась как раз объектом класса, отвечающего за старт и резервирование.
У себя (для ардуинки) я сделал более сложную вещь, сделал маленькую базу данных которая позволяет хранить любое количество сеттингов (в зависимости от размера доступного флеша) по ключу. А данные могут быть простые типы с фиксированным размером как int, float, структ, так и сложные типа строк и массивов. Также это позволяет упростить разработку нескольких приложений на одном девайсе: используя разные ключи можно хранить сеттинги разных приложений не переустанавливая их каждый раз когда переключаешься на другое приложение. Структура хранения одна для всех приложений.
Также этот класс позволит скрыть реализацию для конкретной платформы: для ардуинки это флеш, для 8266 это уже может быть файловая система.

Правильно понимаю, что у вас лежит во flash по сути std::map? И вы прошивали arduino разными прошивками и просто получали по ключу из памяти (полагаю, внешней), свои данные? А данные общие для всех 100500 прошивкок? У меня просто не было такой задачи. Просто нужен был способ красиво сказать. что сущность должна резервироваться. Все.
Тут как раз идет упрощение, а не усложнение. Конечный программист просто указывает, где хочет, чтобы его данные были. Просто в RAM/FLASH, или же в той области, которая может изменяться с помощью модуля взаимодействия с пользователем с функциями резервирования.

По мне так это усложнение, вместо одной точки входа для работы с сеттингами, я получаю кучу размазанных по глобальному неймспейсу переменных с кучей суфиксов. Сразу встает вопрос: когда, каким образом и к каким переменным я могу обращаться в конкретный момент. А если прийдет новый человек на поддержку/разработку? Плюс просто человеческая ошибка, использовал переменную с другим префиксом. В 99% случаев эта проблема может лежать очень долго, и оставшийся 1% будет вылетать у клиента при старнных обстаятельствах. Плюс каждая установка значения делает дуплицированный код для установки нескольких переменных вместо одной, а как на счет ресурсов под програмный код? Плюс если FLASH требует явного программирования, те очистить страницу и записать страницу заного, как здесь быть?
Также если понадобилось резервирование, то это говорит, что есть реальные потери данных, поэтому запись данных всегда в одни и теже страницы памяти может привести к тому, что все копии будут невалидными.


Правильно понимаю, что у вас лежит во flash по сути std::map? И вы прошивали arduino разными прошивками и просто получали по ключу из памяти (полагаю, внешней), свои данные? А данные общие для всех 100500 прошивкок?

да, типа мапы. Но это больше нужно для удобства разработки нежели для финального продукта.

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

Вы используете только то имя, которое написали в макросе. Остальные использовать по внутреннему соглашению нельзя. О них заботится модуль резервирования.
Сразу встает вопрос: когда, каким образом и к каким переменным я могу обращаться в конкретный момент.

К любой, в любой момент времени. До выполнения вашего кода модуль позаботится о том, чтобы там находились валидные данные. Во время сохранения в память позаботится о том, чтобы данные не менялись.
А если прийдет новый человек на поддержку/разработку? Плюс просто человеческая ошибка, использовал переменную с другим префиксом. В 99% случаев эта проблема может лежать очень долго, и оставшийся 1% будет вылетать у клиента при старнных обстаятельствах.

Описал выше. Вы используете только то имя, что указали в макросе.
. Плюс каждая установка значения делает дуплицированный код для установки нескольких переменных вместо одной, а как на счет ресурсов под програмный код? Плюс если FLASH требует явного программирования, те очистить страницу и записать страницу заного, как здесь быть?

Возможно, я неточно описал задачу. Через интерфейсы можно менять только данные в RAM или отдать команду модулю о том, что настало время сделать текущие данные — актуальными при запуске устройства. Если пользователь понаставил данных при конфигурации и понял, что «раньше было лучше», то он просто перезагружает устройство по питанию или отдает команду на восстановление настроек (в этом случа в RAM попадают значения, которые там были при включении и вызываются методы-обработчики разных объектов, которые работают с этими данными, чтобы сделать какие-то действия после обновления в них значений). И все становится как было.
Также если понадобилось резервирование, то это говорит, что есть реальные потери данных, поэтому запись данных всегда в одни и теже страницы памяти может привести к тому, что все копии будут невалидными.

Тут скорее защита от случая, когда началась перезапись настроек и пропало питание. В таком случае, часть настроек запишется, а часть нет. Контрольная сумма не совпадет и будет считаться, что данные не валидны. Тут произойдет восстановление старой конфигурации из соседнего блока.
Тут скорее защита от случая, когда началась перезапись настроек и пропало питание. В таком случае, часть настроек запишется, а часть нет. Контрольная сумма не совпадет и будет считаться, что данные не валидны. Тут произойдет восстановление старой конфигурации из соседнего блока.

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

Вы предлагаете вести журнал конфигураций. Под это нет памяти. Файловой системы тоже нет. Она в большинстве моих приложений избыточна. Но ваш вариант тоже не плох) Если есть ресурс на него.

Нет, никакого журналирования :) Я видимо не очень ясно выразил свою мысль. Есть некая безопасная логика работы с блоком (или несколькими последовательными блоками) флеша. Которая практически во всех случаях возвращает валидные данные успешно записанне последний раз. И соотвественно отвечает за правильную запись. А поверх я просто запилил простенькую файлову систему, поскольку страницы флеша могут быть достаточно большими (я работал с 4К до 64К). А данные которые нужно было менять обычно группировались по 100-400 байт. Но это уже несколько офтопик:)


Возращаясь к началу разговора: непонятно какой скоп проблем мы пытаемся решить. Дуплицирование данных говорит о том, что мы пытаемся не потерять данные если одна из страниц будет повреждена во время хранения, иначе используемое решение через чур разорительное. Если боремся с повреждениями страниц, тогда сразу большой вопрос: перезапись одних и тех же страниц ведет их к более высокой деградации, те писать надо в разные страницы. Плюс обычно сбой происходит в соседних страницах, те страницы надо выбирать используюя физическое представение памяти. и еще может быть куча ньюансов.

Про ваш способ — услышал. Но для текущих задач он избыточен. Но в других случаях я может быть даже бы и воспользовался. Есть ссылка не github?

Про запись. Вообще, правильно было бы иметь внешнюю флешку. Вернее, 2 модуля. На 2-х разных интерфейсах. В виде 2-х разных микросхем. Но тут были 2 блока. Изменяются они крайне редко. Предполагается, что близко к «никогда». На практике раз 5-10 все таки перетерают при интеграции устройства. Ну и далее просто задача иметь валидные даннные для старта. Вот и все.
Есть ссылка не github?

Сорри нет, я на проекте работал до 2004 года. гитхаба еще не существовало :) но там ничего сложного не было, страница(ы) имела заголовок: размер, порядковый номер, CRC32. для чтения выбиралась страница с валидным CRC32 и максимальным порядковым номером, при записи порядковый номер увеличивался и писалось в соседнюю(ии) страницу(и), обычно циклически. Если происходил сбой при записи, то всегда оставалась предыдущая валидная версия.


На практике раз 5-10 все таки перетерают при интеграции устройства.

Я вот про кейс с интеграцией даже не подумал :( но сразу вопрос: а как вы узнаете реальную структуру/лайаут данных для программирования флеша при интеграции? может измениться код, линкер может положить эти данные в другом порядке (сегменты он соритрует по алфавиту насколько мне память не изменяет, а вот сами данные он кладет в сегмент по мере резолвинга их, и совершенно нормальная ситуация когда данные могут поменяться местами)? А что при апдейте происходит (новые сеттинги добавились, какие-то старые удалилсь), CRC-то будет валидным?

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

С этим уже были проблемы, да. Когда новая прошивка думала, что хранятся валидные значения. Решили созданием утилиты, которая вычитывает старые параметры предварительно запросив у устройства текущую структуру, а потом шьет после перепрошивки в совпадающие по названиям поля обратно после перепрошивки. Во время прошивки новой версии все конфиги сбрасываются (страницы по просту чистятся и при первом старте CRC не совпадает и все затирается начальными значениями из новой прошивки). Вообще вся эта система рассчитана на то, что сама прошивка не меняется. Порой нет права сменить ее, даже если найдены ошибки. Пример… Есть устройство, которое уже 4 года как имеет неактуальный софт. С кучей известных багов. Но обновить ничего нельзя. Потому что уполномоченные люди уже тогда давно приняли устройство с той прошивкой. И ответственность за обновление никто брать не хочет.
Непонятно зачем такой велосипед с линкером.
MyStruct struct_in_ram;
const volatile MyStruct struct_in_flash_ex1;
const volatile MyStruct struct_in_flash_ex2;
const MyStruct struct_in_flash_default_settings;

При желании можно прагмами задать секции и выравнивание
Плюс пара функций для записи/восстановления структур из флеш в рам. Можно все еще в класс обернуть.
При желании можно прагмами задать секции и выравнивание

Первое, чему меня учили, когда я только начинал свой путь инженера — не использовать никаких pragma, кроме как pragma once. Причина — непереносимость кода, в случае надобности.
Плюс пара функций для записи/восстановления структур из флеш в рам. Можно все еще в класс обернуть.

  1. Чтобы задать секции, надо их определить в линкер скрипте. Что я и сделал)
  2. При вашем подходе опять же. Одна структура. Мы обсуждали этот подход выше. Его плюсы и минусы. Можете почитать, если интересно.
Первое, чему меня учили, когда я только начинал свой путь инженера — не использовать никаких pragma, кроме как pragma once. Причина — непереносимость кода, в случае надобности.

Ну так __attribute__ та же самая прагма и есть, нестандартное расширение языка. В ISO не входит. В одних компиляторах секция задается прагмой, в других — атрибутами, в третьих можно и так и так.

А уж шаманство с линкером еще более неперносимо, чем атрибуты и прагмы.
В ISO не входит.

Так-то да. Вообще, можно делать по примеру HAL-а и создать .h с переопределениями под каждый компилятор.
А уж шаманство с линкером еще более неперносимо, чем атрибуты и прагмы.

Уже не помню, какие были нюансы с kail, но вроде да. Но вам в любом случае нужно в линкере секции описать. Без этого никак.
Хотелось бы поинтересоваться у автора статьи, насколько давно вы занимаетесь встроенными системами?
А то от вашей оживленной дискуссии с другим товарищем в комментариях аж голова кругом идёт)
Задокументированный стаж — 5-й год сейчас (официального трудоустройства). Официально устроился до первой сессии на первом курсе. С МК начал работать плотно в 9 классе. То есть, еще 3 года. Но это сложно начать профессиональным занятием. Максимальный прогресс на то время — «умный дом» (тогда было очень модно в моем окружении). Там по сути atmega8 + ассемблерного кода на 2 кб. Работа от часового кварца. Опрос датчиков (пара температурных датчиков), поддержка меню на 7-ми сегментном индикаторе (управление 4-мя нажимными кнопками в режиме матричной клавиатуры) и простенькая логика управления релюшками включения/выключения отопления (электронные клапаны на батареи). Сейчас же, самое сложное из того, что делал именно на работе — это устройство ввода-вывода для беспилотников (генераторы шим, измерения температуры, захват сигналов и прочие бытовые штуки). Тут отказоустойчивость и все дела. И устройство имитации окружающей среды в интеграции с матлабом по UDP. Там F4 принимает по ETH данные из матлаба и имитирует для конкретного комплекса показания температур, углов датчиков положения и прочие штуки (подмена информации с реальных чипов). Как у товарища Sap_ru опыта у меня не было такого. Хотя, теперь есть к чему стремиться.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории