Пирамида логики

Наступает момент , когда начинаешь задумываться как наиболее удобным образом разбить программу на несколько файлов для того , чтобы потом брать какую-то часть файлов (обычно это пары *.h + *.c) и легко и не принужденно использовать в другом проекте.

Например надо выбрать функционал по работе с памятью AT45 (это про контроллеры) и включить в другой проект.

Что мы на самом деле имеем ввиду : надо взять определенный функционал из одного проекта и добавить в другой.

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

И вот тут фишка вся в том , что если программа грамотно по логике разбита на функциональные части , то и связи будут минимальными, точнее они будут реализовываться через минимальное количество объектов.

И вот тут мы приходим к выгоде использования СТРУКТУР. Структуры - это по сути указатели на неограниченное количество объектов, которые можно реализовать через один объект , разделяемый в разных файлах (через extern).

Достаточно в программе сделать #include какого-то заголовочного H файла из другого проекта , где описана некая структура данных и тогда эти данные будут нормально поняты компилятором и доступны в вашем новом проекте.

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

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

Чтобы что-то реально начало происходить надо чтобы в коде, который начинается грубо говоря с функции main, вы начали где-то вызывать функции вашего функционала или начали работать с вашими объектами.

Структуры

Теперь хочется показать логический пример из 2 частей программы (2 пары *.h и *.c ) файлов, два функционала.

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

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

фотка 1

Самое трудное что понять? Правильно - какой кирпич сделать самым нижним, так как если не выделить правильно его функционал , то потом придется переделывать всю пирамиду.

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

--------------- h -------------------- 
typedef const struct DONOR2{
	uint32_t page;
	uint32_t offset;
	uint32_t size;
}DONOR2;

typedef const struct DONOR1{
	DONOR2wifi_ssid;
	DONOR2wifi_pw;
	DONOR2http_host;
	DONOR2http_port;
}MEM_SETTINGS;

--------------- c -------------------- 
сначала глобально сразу инициализируем структуру
const DONOR1 don1 = {
		{ 123, 0 , 456} , // здесь комментарии что это 
		{ 234, 789, 123} , // здесь комментарии что это 
		{ 456, 456, 234}, // 
		{ 567, 678, 012}  // 
		 };
// const это для того , чтобы данные прОписались во FLASH память 
// (адреса 0х8ххххх), тогда экономится SRAM.

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

И вот тут ВАЖНО понимать , что изменять мы будем именно с нашими данные в нашем файле С.
К чужим мы будем обращаться только если они находятся на ниже стоящем уровне пирамиды.

Второй файл (по очереди написания) - это файл юзера, то есть он находится на вышестоящей ступеньке пирамиды.

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

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



-------------------------- H -----------------------------
typedef struct USER1{
 uint8_t ii; 
 DONOR1 don1; // явно
 }USER1;

typedef struct USER2{
 const uint8_t yy;
 const DONOR1 *pDon1; // как ссылка 
 }USER2;
-------------------------- С -----------------------------
extern const DONOR1 don1;
const USER1 user1  = {1 , don1}; // и это уже не прокатывает
// initializer element is not constant 
// = элемент инициализатора don1 не является постоянным.
const USER2 user2  = {1 , NULL };  // ПРОКАТИТ
// а далее в коде можно будет :
// присвоить user2. pDon1 = &don1; !!!!

Тут уже инициализация user1 только через функцию и const у нее придется поэтому убрать.
Для user2 далее в любой функции можно присвоить pDon1 любое значение.

В итоге yy и user2.pDon1 попадут во FLASH [0х8ххххх] (обратите внимание внутри структуры USER2 все элементы объявлены const) , а user2 в SRAM .

Выводы : вариант с USER1 у нас не прокатит, а вариант с USER2 приемлем вполне , так как во SRAM будет использована только одна ячейка памяти (это адрес самой USER2). Экономия SRAM будет достигнута.

Что далее плохо с указателями - их можно путать. Думаешь , что присваиваешь указатель на один тип объекта , а он принадлежит другому на самом деле со всеми вытекающими последствиями.

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

Для чего мы все это делали - для того , чтобы взять теперь функционал первого файла легко и перенести во другой проект. Там в другом проекте объявляем extern const DONOR1 don1; и пользуемся на здоровье его функционалом. Сам перенесенный файл при этом править вообще не надо.