AVR Си - выносим функции и переменные в модули, компиляция нескольких файлов в avr-gcc, Makefile
В этой небольшой статье покажу на простейшем примере как вынести функции и переменные программы, написанной на языке программирования AVR Си, в отдельные файлы, собрать их в подключаемый модуль. Вы узнаете как выполнить компиляцию нескольких файлов проекта в avr-gcc, напишем простой и удобный Makefile для автоматизации процесса сборки и прошивки программы в AVR микроконтроллер.
Содержание
- Зачем разбивать на модули программу для AVR C
- Выносим функции и константы в отдельный модуль, простой пример
- Компиляция и прошивка программы состоящей из нескольких Си-файлов в МК
- Настройка Geany для компиляции нескольких C-файлов
- Makefile для проектов на AVR C
- Примеры работы с Makefile
- Настройка Geany для работы с Makefile
- В звершение
Зачем разбивать на модули программу для AVR C
Если вы только начали знакомиться с программированием AVR микроконтроллеров (МК) на языке Си, то ваши простейшие программы, скорее всего, состоят из одного файла с исходным кодом, например: main.c.
В этом файле у вас содержатся директивы для подключения внешних заголовочных файлов *.h, определения значений для констант, переменных, реализации функций и код главной функции main().
Код такой программы размещен в одном файле, он не большой по размеру, легко читается, в него легко вносить изменения и дополнения. Как правило, такая простая программа выполняет простейшие функции, служит для обучения и экспериментов.
При разработке реального проекта, в котором задействовано множество портов МК, различные электронные датчики, устройства обмена и отображения информации, программный код получится достаточно объемный, его целесообразно разбить на логические части, распределить по отдельным модулям и файлам.
Плюсы в разбивке проекта на отдельные файлы и модули здесь очевидны: код легче читать и отлаживать, возможна коллективная работа над программой и ее частями, проект можно компилировать по кусочкам.
Выносим функции и константы в отдельный модуль, простой пример
В статье Простая программа для AVR микроконтроллера на языке Си был приведен пример очень простой программы, ее код содержится всего в одном файле с расширением ".c".
Немного оптимизированный и более наглядный код программы приведен в комментарии #13 к этой же статье. Сейчас мы переделаем этот код таким образом, что константы и функции будут вынесены в отдельный модуль.
Теперь программа будет состоять из трех файлов:
- main.c - основной файл программы;
- library.c - библиотека, содержащая реализацию функций, которые будут использоваться в основной программе;
- library.h - заголовочный файл библиотеки, содержит список функций, служебные константы.
Файлы "library.c" и "library.h" - это и есть наша библиотека, которая будет подключена к основной программе с помощью директивы "#include".
Содержание файла "main.c":
/* Светодиодная мигалка на микроконтроллере ATmega8.
* https://ph0en1x.net
*/
#include <avr/io.h>
// Включаем заголовочный файл библиотеки.
#ifndef LIBRARY
#define LIBRARY
#include "library.h"
#endif
// Основная функция программы.
void main(void) {
port_setup(PD0, PD1);
while (1) {
led_blink(PD0);
led_blink(PD0);
delay_2();
led_blink(PD1);
led_blink(PD1);
}
}
Строкой "#include <avr/io.h>" подключается заголовочный файл "io.h" из установленной в системе библиотеки avr-libc, в нем содержится набор IO (Input Output) макроопределений для различных типов микроконтроллеров, определяемых опцией "-mmcu=X" при компиляции программы.
Строка "#ifndef LIBRARY" (ifndef следует понимать как "If Not Defined") - содержит директиву условной компиляции, которая проверяет объявлена ли константа с именем "LIBRARY" и в случае если это не так - выполняет код внутри блока до директивы "#endif". Внутри этого блока кода мы объявляем константу "LIBRARY" и подключаем заголовочный файл библиотеки.
Данная директива в этом файле приведена для примера, здесь можно было бы обойтись и без нее (у нас библиотека включается в проект только один раз). Подобное подключение библиотеки целесообразно в случае если необходимо использовать ее в нескольких разных модулях проекта.
Эта конструкция кода позволит исключить повторное включение файла "library.h" в проект и тем самым избежать ошибки в процессе компиляции, вызванной повторным включением одного и того же кода библиотеки.
Дальше идет реализация основной функции "main()", которая будет запущена при старте программы. В ней мы используем функции из созданной нами библиотеки, а также константы "PD0" и "PD1" полученные из включения файла "io.h".
Думаю вы заметили здесь две немножко отличающиеся формы использования директивы "#include":
#include <avr/io.h>
#include "library.h"
В первом случае путь к подключаемому файлу взят в скобки, а во втором - в двойные кавычки. Разница здесь лишь в том, где препроцессор первым делом будет выполнять поиск файла для включения в код.
Если путь к файлу взят в скобки <library.h>, то поиск файла для включения в код будет выполнен сперва в глобальных папках, которые прописаны в среде разработке или указаны компилятору.
А если путь указан в двойных кавычках "library.h", то поиск файла будет осуществляться сперва в локальных, по отношению к проекту, папках.
В приведенном выше примере сперва выполняется поиск и подключение файла "io.h", он должен находиться в папке "avr" относительно глобальной директории в которой будет выполняться поиск.
Давайте найдем реально размещение файла "io.h", это можно сделать командой:
locate io.h | grep avr
Получим примерно следующий список:
/usr/lib/avr/include/avr/io.h
/usr/lib/avr/include/stdio.h
/usr/share/doc/avr-libc/avr-libc-user-manual/group__avr__io.html
/usr/share/doc/avr-libc/avr-libc-user-manual/group__avr__stdio.html
/usr/share/man/man3/io.h.3avr.gz
/usr/share/man/man3/stdio.h.3avr.gz
"/usr/lib/avr/include/" - это и есть глобальная директория, в которой компилятор avr-gcc ищет заголовочные файлы для включения. Можете изучить ее и убедиться в том, что там собрано достаточно много различных библиотек.
Вернемся к нашей программе. Дальше в коде ми подключаем файл "library.h", который нами же был написан и размещен в директории проекта вместе с основным файлом программы "main.c".
Если бы в директории проекта била создана дочерная папка 'modules' и файл "library.h" был размещен в ней, то включение этого файла в код нужно было бы выполнять вот так:
#include "modules/library.h"
Позже, можете попробовать изменить "library.h" на <library.h> - и при попытке скомпилировать ее увидите что из этого получится.
При использовании компилятора AVR-GCC нужно запомнить следующее:
- #include <путь> - компилятор ищет файл в установленных для него путях, они могут быть вшиты по умолчания или же заданы с помощью опции "-I" в командной строке;
- #include "путь" - компилятор ищет файл сначала в папке относительно проекта, а потом в глобальных путях (там же где и #include <путь>).
Содержание файла "library.h":
#include <avr/io.h>
// Константа, задающая частоту МК, используется в функциях _delay_ms .
#define F_CPU 1000000UL
#include <util/delay.h>
// Значения временных задержек для мигания светодиодов.
#define DELAY_MS_1 100
#define DELAY_MS_2 700
// Прототипы функций (возвращаемое значение, название, параметры).
static void delay_1(void);
void delay_2(void);
void led_blink(unsigned int pin_num);
void port_setup(int port_1, int port_2);
Это заголовочный файл, который содержит описание библиотеки, ее функций, констант.
Подключаем "io.h", поскольку в функциях библиотеки будет идти работа с портом и связанными с ним константами для текущего МК.
Директива "#include <util/delay.h>" подключает модуль из которого мы будем использовать всего лишь одну функцию "_delay_ms".
Но почему же она размещена после объявления константы "F_CPU"? - вы можете попробовать разместить директиву включения "delay.h" сразу после директивы с включением "io.h" и посмотреть на результат, компиляция завершится с ошибкой:
warning: #warning "F_CPU not defined for <util/delay.h>"
Дело в том что функция, объявленная в "delay.h" требует установленного значения константы "F_CPU", которое говорит сколько тактов делаем микропроцессор за одну секунду (частота), например 1000000 - означает частоту 1МГц. На основе значения этой константы функция "_delay_ms" делает расчет количества тактов для реализации необходимой временной задержки.
Константы "DELAY_MS_1" и "DELAY_MS_2" содержат числовые значения задержки по времени в миллисекундах, которые используются во внутренних функциях.
В конце файла перечислены все прототипы функций библиотеки с указанием возвращаемого значения, имени и аргументов.
Эти прототипы можно сравнить с выставочными товарами (заголовки функций), выложенными на фасаде магазина (наш проект), которые дают покупателю (компилятор) понимание какие товары (реализации функций) доступны для покупки и как они выглядят (указание имен и аргументов функций).
Зачем же нужны эти прототипы функций? - Дело в том, что в языке Си код выполняется в том порядке, в котором мы его задаем. Если ми написали реализацию двух функций А и Б, причем реализация Б идет после А, то при попытке вызвать функцию А в которой внутри вызывается функция Б компилятором будет выведена ошибка с сообщением о том, что функция Б не найдена.
И это верно, потому что мы пытаемся вызвать функцию, реализация которой следует в коде позже - после вызываемой нами функции А.
Ну а теперь представьте себе что у вас 10 функций и причем в каждой из них перекрестно вызывается другая соседняя, в каком порядке в коде размещать реализации функций чтобы все заработало? - в некоторых случаях такой комбинации может и не существовать, придется сливать некоторые функции в одну, дублировать код...
Здесь нам и приходят на помощь объявления прототипов функций, они говорят компилятору что данные функции готовы к использованию в любом из последующих участков кода.
Содержание файла "library.c":
#include "library.h"
void port_setup(int port_1, int port_2) {
DDRD |= (1 << port_1);
DDRD |= (1 << port_2);
}
void led_blink(unsigned int pin_num) {
PORTD |= (1 << pin_num);
delay_1();
PORTD &= ~(1 << pin_num);
delay_1();
}
static void delay_1(void) {
_delay_ms(DELAY_MS_1);
}
void delay_2(void) {
extern int delay_ms_leds;
_delay_ms(DELAY_MS_2);
}
Этот файл содержит реализацию функций нашей библиотеки. В начале подключается файл "library.h", в нем мы раньше определили необходимые константы и описали реализуемые в библиотеке функции.
Дальше идет определение и реализация четырех функций:
- port_setup - настройка каналов порта;
- led_blink - мигание светодиодом на пине, связанном с каналом порта (включение и выключение с задержкой);
- delay_1 - задержка по времени, используемая в функции led_blink;
- delay_2 - задержка по времени, используемая в основной программе.
Здесь важно обратить внимание на объявление функции "static void delay_1(void)", а именно на ключевое слово static - оно дает компилятору понять что функция должна быть доступна к вызову только внутри библиотеки. Если попробовать ее вызвать из файла "main.c" то получим ошибку:
warning: ‘delay_1’ used but never defined
Остальные функции, которые описаны и содержатся в этом модуле будут доступны к использованию основной программе и другим модулям внутри ее.
Компиляция и прошивка программы состоящей из нескольких Си-файлов в МК
Итак, вместо одного файла с исходным кодом на Си у нас теперь три файла. Сейчас я покажу как их скомпилировать и прошить в AVR микроконтроллер, здесь все не намного сложнее чем в ситуации с одним файлом.
Для компиляции исходного кода в объектный файл в одной из прошлых статей была использована команда вида:
avr-gcc -mmcu=atmega8 -Os leds_blinking.c -o leds_blinking.o
Это пример для программы, исходный код которой содержится всего лишь в одном файле "leds_blinking.c". Для компиляции здесь указан к использованию микроконтроллер ATMega8.
В случае с тремя файлами "main.c", "library.c", "library.h" команда компиляции будет выглядеть следующим образом:
avr-gcc -mmcu=atmega8 -Os main.c library.c -o main.o
Все что изменилось в логической структуре команды - мы добавили имя файла "library.c" к списку аргументов сразу же после указания "main.c". Таким образом, код этих файлов будет скомпилирован в один общий объектный файл, функции из библиотеки "library.c" станут доступными к использованию в программе из файла "main.c".
Генерируем файл прошивки для МК в формате формате Intel Hex:
avr-objcopy -j .text -j .data -O ihex main.o main.hex
Записываем прошивку в микроконтроллер ATMega8 используя программатор USBAsp:
avrdude -c usbasp -p m8 -P usb -U flash:w:main.hex
Настройка Geany для компиляции нескольких C-файлов
Для успешной компиляции написанной нами программы из трех файлов, в среде программирования Geany нужно сделать некоторые исправления.
Выбираем пункт меню "Build" - "Set Build Commands" и прописываем следующие команды сборки и прошивки, как указано ниже.
C commands:
- 1. Compile - "avr-gcc -mmcu=atmega8 -Os main.c library.c -o main.o"
- 2. Build - "avr-objcopy -j .text -j .data -O ihex main.o main.hex"
Execute commands:
- 1. Execute - "avrdude -c usbasp -p m8 -P usb -U flash:w:main.hex"
Рис. 1. Настройка команд компиляции и прошивки программы для AVR микроконтроллера в среде Geany.
Как видите все достаточно просто и удобно. Но это только если не учитывать того факта, что при работе над несколькими различными программами в среде программирования Geany придется постоянно прописывать индивидуальные настройки для каждой из них.
Чтобы убрать это неудобство как раз самое время познакомиться с таким инструментом как Makefile.
Makefile для проектов на AVR C
GNU Makefile - это файл, содержащий набор инструкций для программы 'make', который в сочетании с ней позволяет выполнять преобразования файлов из одного формата в другой (компиляция, извлечение какой-то информации), запускать пользовательские программы с параметрами в процессе обработки файлов.
В нашем случае GNU Makefile и программа 'make' помогут нам автоматизировать цепочку "компиляция-сборка-прошивка". Также это даст нам в руки дополнительные удобные инструменты, для вызова которых нужно всего лишь передать в качестве аргумента для команды 'make' один из параметров.
Ниже приведен пример не сложного Makefile, в котором я собрал различные полезные инструменты для сборки и работы с проектом на основе AVR-GCC и AVRDUDE.
# Название: Makefile
# Автор: ph0en1x
# Копирайт: https://ph0en1x.net
# Лицензия: MIT
# Название проекта.
# Имя основного С-файла без расширения, пример для 'project1.c': 'project1'.
PROJECT = main
# Тип чипа для AVR GCC и частота ядра.
# https://gcc.gnu.org/onlinedocs/gcc/AVR-Options.html
GCC_MCU = atmega8
CLOCK_HZ = 1000000
# Опции для AVRDUDE.
# https://ph0en1x.net/77-avrdude-full-howto-samples-options-gui-linux.html
AVRDUDE_MCU = m8
AVRDUDE_PROGRAMMER = usbasp
AVRDUDE_PROGRAMMER_PORT = usb
# Fuses
FUSE_L = 0xe1
FUSE_H = 0xd9
FUSE_E = 0xff
# Список дополнительных C-файлов для компиляции (указывать через пробел).
C_FILES = library.c
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
CFLAGS = -g -Os -Wall -mcall-prologues -std=c99 -mmcu=$(GCC_MCU) -DF_CPU=$(CLOCK_HZ)UL
FUSES = -U lfuse:w:$(FUSE_L):m -U hfuse:w:$(FUSE_H):m -U efuse:w:$(FUSE_E):m
FLASH = -U flash:w:$(PROJECT).hex
AVR_GCC = `which avr-gcc`
AVR_OBJCOPY = `which avr-objcopy`
AVR_SIZE = `which avr-size`
AVR_OBJDUMP = `which avr-objdump`
AVRDUDE = `which avrdude`
REMOVE = `which rm`
NANO = `which nano`
TAR = `which tar`
DATETIME = `date +"%d-%m-%Y"`
AVRDUDE_CMD = $(AVRDUDE) -p $(AVRDUDE_MCU) -c $(AVRDUDE_PROGRAMMER) -P $(AVRDUDE_PROGRAMMER_PORT) -v
%.elf: %.c
$(AVR_GCC) $(CFLAGS) $< $(C_FILES) -o $@
%.hex: %.elf
$(AVR_OBJCOPY) -R .eeprom -O ihex $< $@
all: clean elf hex
program: $(PROJECT).hex
$(AVRDUDE_CMD) $(FLASH)
fuses:
$(AVRDUDE_CMD) $(FUSES)
elf: $(PROJECT).elf
hex: $(PROJECT).hex
size: $(PROJECT).elf
$(AVR_SIZE) $(PROJECT).elf
disasm: $(PROJECT).elf
$(AVR_OBJDUMP) -d $(PROJECT).elf
clean:
$(REMOVE) -f *.hex *.elf *.o
edit:
$(NANO) $(PROJECT).c
tar:
$(TAR) -zcf $(PROJECT)_$(DATETIME).tgz ./*
Описывать синтаксис и принципы построения Makefile я здесь не буду, поскольку это отдельная и достаточно обширная тема. Если у вас есть опыт программирования и работы в консоли GNU/Linux, то разобраться что и как здесь работает не составит особого труда.
Подробно о построении своего Makefile для сборки программы, а также о работе с программой 'make' вы можете почитать в официальном мануале от Free Software Foundation - GNU Make Manual.
В верхней части файла находятся различные опции, которые можно изменить по своему усмотрению под свой тип МК, программатор и проект.
Все настройки в файле сейчас адаптированы для сборки и прошивки программы, которая была рассмотрена выше. В списке дополнительных файлов для компиляции указан файл библиотеки "library.c". Прошиваться будет микроконтроллер ATMega8 с использованием программатора 'usbasp' через порт 'usb'.
Опция 'CLOCK_HZ' указывает компилятору частоту ядра МК в Герцах, это число может быть переопределено внутри программы с помощью директивы вида "#define F_CPU 1000000UL" (суффикс UL - означает Unsigned Long, тип значения).
Значения байтов Fuses приведены те, которые установлены по умолчанию в микроконтроллере ATMega8. Обязательно измените их под свой проект и будьте осторожны с установкой фьюзов!
Файл Makefile должен располагаться в той же папке, где находится основной файл исходного кода на языке Си со всеми библиотеками и ресурсами.
Примеры работы с Makefile
Сейчас я расскажу как использовать Makefile и почему это удобно. Все приведенные ниже примеры базируются на приведенной выше программе из трех файлов.
Перед использованием 'make' нужно перейти в директорию где располагается проект с файлом Makefile. Пусть это будет папка '/home/master/avr-test', выполним в консоли следующую команду:
cd /home/master/avr-test
Теперь приведу примеры команд и описание того что они делают и для чего служат.
Компилируем программу, получаем на выходе объектный файл (*.c -> *.elf):
make elf
Извлекаем нужные области данных из объектного файла и строим файл с прошивкой (*.elf -> *.hex):
make hex
Очистка промежуточных и результирующих файлов в проекте (*.hex *.elf *.o):
make clean
Цепочка операций по умолчанию (очистка + компиляция + генерация hex):
make
Данная команда по результату аналогична следующей:
make clean elf hex
Смотрим размеры секций с данными в объектном файле (*.elf):
make size
Примерный вывод команды:
text data bss dec hex filename
216 0 0 216 d8 main.elf
Дизассемблируем объектный файл (*.elf):
make disasm
Пример вывода:
main.elf: file format elf32-avr
Disassembly of section .text:
00000000 <__vectors>:
0: 12 c0 rjmp .+36 ; 0x26 <__ctors_end>
.........<много кода>...........
000000ae <main>:
ae: 61 e0 ldi r22, 0x01 ; 1
b0: 70 e0 ldi r23, 0x00 ; 0
b2: 80 e0 ldi r24, 0x00 ; 0
b4: 90 e0 ldi r25, 0x00 ; 0
b6: c0 df rcall .-128 ; 0x38 <port_setup>
b8: 80 e0 ldi r24, 0x00 ; 0
ba: 90 e0 ldi r25, 0x00 ; 0
bc: d1 df rcall .-94 ; 0x60 <led_blink>
be: 80 e0 ldi r24, 0x00 ; 0
c0: 90 e0 ldi r25, 0x00 ; 0
c2: ce df rcall .-100 ; 0x60 <led_blink>
c4: ea df rcall .-44 ; 0x9a <delay_2>
c6: 81 e0 ldi r24, 0x01 ; 1
c8: 90 e0 ldi r25, 0x00 ; 0
ca: ca df rcall .-108 ; 0x60 <led_blink>
cc: 81 e0 ldi r24, 0x01 ; 1
ce: 90 e0 ldi r25, 0x00 ; 0
d0: c7 df rcall .-114 ; 0x60 <led_blink>
d2: f2 cf rjmp .-28 ; 0xb8 <main+0xa>
000000d4 <_exit>:
d4: f8 94 cli
000000d6 <__stop_program>:
d6: ff cf rjmp .-2 ; 0xd6 <__stop_program>
Прошиваем HEX-файл в память микроконтроллера:
make program
Если программа не была скомпилирована и не был HEX-файл не был создан, то это будет сделано автоматически перед запуском процесса прошивки.
Если же файлы *.elf и *.hex уже есть в папке проекта то будет запущена команда прошивки в МК уже существующего файла *.hex.
Открываем основной файл проекта с исходным кодом в консольном редакторе 'nano':
make edit
Для выхода из редактора nano нужно нажать комбинацию клавиш CTRL + X.
Чтобы выполнить компиляцию и прошивку программы, исходный код которой был изменен нужно выполнить очистку старых файлов (*.elf и *.hex), а потом уже наново выполнить сборку и прошивку. Эти операции можно выполнить всего лишь одной командой:
make clean program
Запакуем все файлы проекта в архив (со сжатием GZip) для резервирования или пересылки:
make tar
Имя файла будет содержать название проекта (из переменной PROJECT в Makefile), а также строчку с текущей датой, например: main_24-09-2018.tgz.
Прошивка фьюзов в МК (будьте предельно осторожны с этой командой):
make fuses
Значения Fuse-байтов для прошивки прописаны в шапке Makefile в переменных:
- FUSE_L - младший fuse-байт (Lower);
- FUSE_H - старший fuse-байт (Higher);
- FUSE_E - дополнительный fuse-байт.
Как видите, легко запоминающийся набор команд творит магию - выполняет всю рутинную работу по компиляции, сборке и прошивке программы в МК, предоставляет дополнительные полезные инструменты.
Настройка Geany для работы с Makefile
Используя подобный Makefile для своих проектов можно очень просто перенастроить среду программирования Geany для работы с различными проектами без постоянной корректировки строчек с командами сборки и прошивки.
Вся магия будет выполняться в Makefile, а в Geany мы будем вызывать лишь уже стандартизированные команды, которые указаны в предыдущем разделе.
Переходим по пунктам меню "Build" - "Set Build Commands" и изменяем строчки с настройками на указанные ниже.
C commands:
- 1. Compile - "make" (также можно использовать команду "make clean elf hex")
- 2. Build - "make clean hex" (результат будет тот же, что и в предыдущей команде)
Independent commands:
- 1. Get Size - "make size"
- 2. Disasm - "make disasm"
- 3. Pack project - "make tar"
- 4. Write FUSES - "make fuses"
Execute commands:
- 1. Execute - "make clean program"
Вот как это выглядит в окне настроек Build в Geany:
Рис. 2. Настройки Geany для программирования AVR микроконтроллеров с использованием Makefile.
Теперь для работы можно использовать следующие кнопки и пункты меню:
Рис. 3. Кнопки и пункты меню в Geany для компиляции, сборки и обработки программы для AVR МК.
- 1 - компиляция программы (получение файлов *.elf и *.hex);
- 2 - аналогично пункту 1;
- 3 - прошивка микроконтроллера;
- 4 - меню с набором дополнительных команд: Get Size, Disasm, Pack project, Write FUSES.
Для пересборки и прошивки программы в МК достаточно нажать на кнопку 3. Просто и удобно!
В завершение
Архив с примером программы где используется разделение исходного кода и Makefile - Скачать (2,3КБ Zip).
После небольшой подготовки и настройки своего Makefile можно приступать к реализации уже более сложного проекта, который состоит из нескольких файлов с исходным кодом.
Удачи вам в ваших проектах!
Спасибо вам за проделанную работу
Пожалуйста! И вам спасибо за отзыв
Благодарю за труд, планируете написать статью про прерывания?
Да, такая статья запланирована, как соберу всю необходимую информацию по ней и продумаю ее структуру то приступлю к написанию.
Спасибо добрый человек
Смайлики из ICQ, как круто
..."константа с именем "LIBRARY" и в случае если это так " --и в случае если это НЕ так
Шикарный мануал, СПАСИБО, ДОБРЫЙ ЧЕЛОВЕК.
Благодарю за отзывы, рад что кому-то пригодилось!
P.S. Исправил описание про "#ifndef LIBRARY" и добавил небольшое примечание.
Попытался вынести настроечные константы для портов - но что-то не получилось.
Надо тщательнее прочитать!!!
Функции вроде норм выносятся, а вот что до main -- ??? Буду тренироваться! ;-) (шоб мозг не сох)
Пока идут мелкие проекты, тренируюсь. Потом бы на солидной меге "сваять" домашнюю автоматику, а то малина Пи3 -- как-то шибко круто для мелких задач ;-)
СпасиБо!
Ох ништяаааак. Спасибо! Стало гораздо понятнее, немного перенастроил Makefile под свои реалии. Всё супер! Спасибо!