Наступает момент , когда начинаешь задумываться как наиболее удобным образом разбить программу на несколько файлов для того , чтобы потом брать какую-то часть файлов (обычно это пары *.h + *.c) и легко и не принужденно использовать в другом проекте.
Например надо выбрать функционал по работе с памятью AT45 (это про контроллеры) и включить в другой проект.
Что мы на самом деле имеем ввиду : надо взять определенный функционал из одного проекта и добавить в другой.
И вот тут желательно , чтобы старые связи этого функционала со старым проектом никак не мешали в новом (точнее желательно чтобы их совсем не было). Имеется ввиду какие-то явные ссылки на объекты из других файлов должны быть минимальны.
И вот тут фишка вся в том , что если программа грамотно по логике разбита на функциональные части , то и связи будут минимальными, точнее они будут реализовываться через минимальное количество объектов.
И вот тут мы приходим к выгоде использования СТРУКТУР. Структуры - это по сути указатели на неограниченное количество объектов, которые можно реализовать через один объект , разделяемый в разных файлах (через extern).
Достаточно в программе сделать #include какого-то заголовочного H файла из другого проекта , где описана некая структура данных и тогда эти данные будут нормально поняты компилятором и доступны в вашем новом проекте.
Но так устроен язык программирования, что чтобы реально использовать эти данные их надо сначала инициализировать, а потом что-то с ними делать и эта задача решается в файле с аналогичным названием только с расширением С.
В чем тут идея : можно включать H файлы сколь угодно много , все скомпилируется нормально , но ничего реально происходить не будет, никакого кода генерироваться не будет.
Чтобы что-то реально начало происходить надо чтобы в коде, который начинается грубо говоря с функции main, вы начали где-то вызывать функции вашего функционала или начали работать с вашими объектами.
Структуры
Теперь хочется показать логический пример из 2 частей программы (2 пары *.h и *.c ) файлов, два функционала.
Сначала определим кто для кого поставляет информацию по объектам , кто будет донором ,а кто будет юзером. То есть всегда надо понять для кого мы предоставляем функционал (смотрим сначала со стороны донора). Если логика реализована правильно , то всегда выстраивается пирамида из кирпичей (это какой-то функционал), где снизу понятно доноры , выше юзеры и связь между ними через минимально количество объектов.
Причем в идеале каждый кирпич контактирует только с соседом и не пытается прыгнуть через ряды к другому кирпичу. Обратите внимание , что управление (использование) идет всегда сверху , а ресурсы предоставляются всегда снизу.
Самое трудное что понять? Правильно - какой кирпич сделать самым нижним, так как если не выделить правильно его функционал , то потом придется переделывать всю пирамиду.
И еще мы пытаемся так все продумать, чтобы кирпич из этой пирамиды с минимальными изменениями можно было легко вставить в другую пирамиду.
--------------- 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; и пользуемся на здоровье его функционалом. Сам перенесенный файл при этом править вообще не надо.