Так вы логики в .h файлах не видите потому не понимаете процесс компиляции и сборки проекта.
В Си вам можно обойтись и без .h файлов, но не наоборот. Так вот то то вы делаете - это такой извращенный способ сформирвать один большой .с файл.
Препроцессор же вместо #include "header.h" просто вставляет текст из файла header.h. Ни больше ни меньше. Вы щас получаете большой .c файл, который отправляется на копиляцию.
По-пробую как-то сжато передать суть...
Для упрощения пока не рассматриваем данные и скажем, что по сути каждый .c файл это тупо набор функций - кусков исполняемого кода который можно вызвать по имени.
Для того
чтобы вызвать этот кусок кода надо знать имя, типы возвращаемого функцией значения и типы аргументов функции(плюс их кол-во).В общем случае мы имеем несколько .с файлов, которые вызывают функции друг друга.
Но
компиляция этих файлов происходит независимо!Допустим, у вас в проекте три .с файла. 1.с 2.с 3.с Это будет означать, что надо ТРИ раза вызвать компилятор и на выходе мы получим три объектных модуля 1.obj 2.obj 3.obj
Эти объектные модули - полуфабрикаты, в которых указано имя функции и исполняемый код ей соответствующий. Данные мы пока не рассматриваем, как я уже и говорил.
Вы же помните да, что
чтобы вызвать функцию надо знать её имя, тип результата и аргументов? Это необходимо чтобы сгенерировать код вызова этой фнкции.
Так вот
это и есть тот самый прототип функции. Мы в файле 1.с хотим использовать фукнцию void func(int a); которая реализована в файле 2.с и магия в том, что нам для этого нужно знать только прототип функции.
Помните же да, что компилятор компилирует один .с файл за один раз. Может быть файл 1.с поступил на компиляцию раньше и машинного кода для 2.с еще вовсе нет! Это не проблема, нам достаточно прототипа функции. Дальше линкер разберется.. Но об этом позднее.
Так вот мы могли бы в файле 1.с ПЕРЕД вызовом func(1); объявить её прототип. Написать void func(int a);
Тогда не возникнет никаких проблем и компилятор будет знать что func(1) соответствует прототипу и сможет с генерировать машинный код для вызова. Как видите, никаких #include и .h файлов вообще не нужно!
Однако это очень неудобно, потому что функция func() может потребоваться нам и в файле 1.с и в файле 10.с и еще в многих местах! Копипаситить прототипы теперь везде? Было бы глупо.
Выход простой: Функция func() реализована в файле 2.с, поэтому
очень логично сделать файл 2.h и поместить туда прототипы всех функций реализованных в файле 2.с.Теперь любой .с файл, который хочет использовать функции реализованные в 2.с просто делает #include 2.h и таким образом в начале этого .с файла препроцессор подставляет все прототипы всех функций из файла 2.с и мы можем свободно их вызывать из любых других .с файлов. Прототипы теперь описаны один раз и хранятся централизовано. Этот файл еще называют интерфейсом модуля. В этом-же .h файле могут хранится описания типов данных, но этого мы пока не касаемся для упрощения понимания.
Также стоит отметить, что нет такого понятия как "основной .с файл проекта", которое вы тут используете.
Есть просто требование, что
где-то должна обязательная быть функция main(), которая будет вызвана из стартапа, после того как будет подготовлено Си окружение(инициализирована память, указатель стека и т.д.)
Короче после того как компилятор был вызван 100500 раз, ОТДЕЛЬНО для каждого .с файла и на выходе получены .obj файлы
https://ru.wikipedia.org/wiki/%D0%9E%D0%B1%...%83%D0%BB%D1%8C Начинается компоновка(линкинг) вызывается линкер, которому скармливаются все эти объектные модули(и не только, но это щас не важно).
Линкер размещает каждый .obj файл по определенному адресу и таким образом знает какая функция из какого .obj расположена по какому адресу в памяти.
Я сейчас намеренно всё очень упрощаю и не касаюсь темы данных, секци и т.д...
Далее линкер проходится по всем вызовам функций и заменяет вызов по имени на конкретный адрес! Ведь он теперь знает адреса всех функций из всех .obj файлов.
Например 1.obj вызывается функция func(). Линкер находит ее реализацию в модуле 2.obj и в то место подставляет уже АДРЕС. Либо выдаст ошибку если нигде ни в одном из .obj которые ему передали он эту функкцию не нашел. Либо если в двух разных .obj есть две функции с одинаковым именем.
Так вот вы разделите свой проект исходя из вышеописанной логики и обязательно добавьте все свои .c файлы в проект.
IDE AVRStudio для вас сделает всю магию - т.е. будет вызывать компилятор отдельно для каждого .c файла, потом все полученные результаты передаст линкеру и на выходе у вас получится один исполняемый файл. Это всё произойдет автоматически.
Во всем этом разделении есть очень много логики и смысла, я лишь ОЧЕНЬ упрощенно показал вам процесс.
Без дальнейшего изучения тут не обойтись. Есть static функции которые из других .c файлов вызвать нельзя. Есть inline функции которые вместо вызова сразу встраиваются в точку вызова... Есть много тонкостей разных которые требуют отдельного освещения и имеют свои тонкости.
С Новым Годом всех, успехов в разработках и изучении Си )))