Программирование AVR Как устроен Makefile и что это такое? Wed, November 13 2024  

Поделиться

Нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.


Как устроен Makefile и что это такое? Печать
Добавил(а) microsin   

Makefile - файл, содержащий набор инструкций для программы make. Программа make с помощью этого файла позволяет автоматизировать процесс компиляции программы и выполнять при этом различные действия. При запуске make по умолчанию ищет файл Makefile в текущей папке и обрабатывает его (можно изменить это поведение, чтобы открывался другой файл с набором инструкций, если ввести команду make -f другое_имя_makefile).

Система make родилась в мире UNIX и постепенно переползла и на Windows вместе с портами GNU-компиляторов (gcc). Если открыть пример готового Makefile, то он поначалу может показаться полной абракадаброй, поскольку содержимое файла подчиняется заранее заданному набору правил, которые необходимо предварительно изучить. Для простоты рассмотрим пример работы с проектом AVR и компилятором gcc.

Немного о структуре файла.

- Комментарии, как это принято в UNIX скриптах, начинаются с символа # и продолжаются до конца строки.
- Обычно в файле содержатся метки, идентифицирующие "цели" (targets, см. далее). Метка начинается с начала строки и оканчивается двоеточием :. После двоеточия могут идти так называемые зависимости, dependencies (сразу непонятно, что это такое и для чего надо, но дальше по ходу дела станет яснее). Обычно это имена файлов, либо ссылки на цели.
- Если в качестве dependencies указана последовательность целей, то они будут выполняться друг за другом.
- Если в качестве dependencies указаны имена файлов (обычно объектных), то утилита make может проверить - нужно их компилировать, или нет (мне непонятно, как она проверяет, однако это работает). Например, если компилируется несколько исходных файлов в несколько объектных, то некоторые исходные файлы не требуется каждый раз перекомпилировать заново, если они не изменялись. Для больших проектов это важно, поскольку существенно экономит время сборки программы (в нашем случае - получение двоичной прошивки для AVR).
- Для упрощения содержимого Makefile и для удобства используются переменные. Пример задания переменной (здесь в переменную записана командная строка вызова программатора):

JTAGICEII = c:/Program Files/AtmelAVR Tools/JTAGICEmkII/jtagiceii.exe -d $(DEVICE) -e -mi

После задания переменной на неё можно ссылаться так:

flash: main.hex
    $(JTAGICEII) -pf -if main.hex

- После задания в одной строке цели (цель: [зависимость1] .. [зависимостьN]) в последующих строках могут задаваться так называемые правила (rules). Каждое правило должно ОБЯЗАТЕЛЬНО начинаться с символа табуляции (таким способом make отслеживает правила и другие цели). Правило - это просто обычная команда (вызов компилятора, копирование, удаление и проч.), выполняемая шеллом.
- Переменные $@, $< , $^ называются автоматическими (automatic variables).

$@ заменяется на текущую цель.
$< которая заменяется на первую зависимость из списка.
$^ которая заменяется на список всех зависимостей с их каталогами.

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

1. Компиляция программы (вводимая команда будет выглядеть как make hex).
2. Запись двоичного файла (make flash).
3. Запись бит "перемычек" (make fuse, для микроконтроллеров AVR это обычно 2 байта).
4. Полная запись микроконтроллера (и памяти и перемычек), выполняются действия и 2, и 3.
5. Очистка проекта - удаление всех промежуточных файлов, образующихся при компиляции, обычно объектных (make clean).
6. Стирание микроконтроллера (очистка flash и сброс перемычек в исходное состояние).
7. Бэкап проекта (make backup), будет выполняться цель 5 (clean), а затем архивирование файлов.
8. Вывод подсказки по возможным вариантам работы с проектом (просто make, при этом выводится подсказка по make hex, make flash, make fuse, make clean).

Для каждого такого действия 1..8 в Makefile прописывается блок команд, этот блок идентифицируется целью (target). Имя цели по сути является меткой, по которой переходит управление при обработке команды, переданной программе make. Для действия 1 это будет запуск компилятора (цель hex), для 2 - вызов программатора (цель flash) и т. д. Рассмотрим для примера ветку обработки цели hex (команда make hex) по шагам - см. рабочий Makefile [2] на примере проекта из библиотеки V-USB.

1. Пользователь вводит команду make hex.
2. Программа make открывает файл Makefile, ищет цель hex и начинает её обработку.
3. Для цели hex указана зависимость main.hex и ни одного правила (строка 131). Программа make ищет цель main.hex и начинает её обработку.
4. Для цели main.hex указана зависимость main.elf (строка 175) и несколько правил. Программа make ищет цель main.elf и начинает её обработку (правила цели main.hex будут отрабатываться после окончания обработки цели main.elf).
5. Для цели main.elf указаны зависимости usbdrv и $(OBJECTS) (строка 172), а также одно правило (вызов компилятора для получения файла main.elf). Программа make ищет цель usbdrv и начинает её обработку.
6. Для цели usbdrv не указано зависимостей (строка 169), только одно правило (копирование папки usbdrv в текущий каталог). Выполняется это правило, цель usbdrv завершена и происходит возврат к обработке цели main.elf.
7. Зависимости, входящие в переменную $(OBJECTS) (строка 172), не являются целями, это просто имена файлов, которые должны быть получены при компиляции. Поэтому сразу начинается выполняться правило, запускающее компилятор (строка 173). В результате те объектные файлы, которые должны быть скомпилированы, появляются в соответствующих каталогах, и появляется выходной файл main.elf (двоичный файл, который может использоваться в качестве входного для эмулятора или симулятора при отладке программы). Цель main.elf завершена, происходит возврат к обработке цели main.hex (строка 175).
8. Начинается обработка правил цели main.hex. Команда rm удаляет старые файлы прошивок flash и eeprom, avr-objcopy генерирует новую прошивку main.hex из файла main.elf, avr-size просто отображает информацию о размере секций в файле main.hex. Обработка цели main.hex закончена, происходит возврат к обработке цели hex.
9. Все зависимости цели hex обработаны, правил у цели hex нет. Работа make на этом завершается.

Есть утилиты-визарды для автоматической генерации файлов Makefile, например входящая в пакет WinAVR утилита MFile (запускается ярлычком C:\WinAVR-20080610\bin\wish84.exe mfile.tcl).

[Проблемы и их решение, FAQ]

Makefile в среде Windows (Makefile работает при помощи пакета MSYS) завершается с ошибкой на команде xcopy, например (выполнение команды make backup):

/usr/bin/sh: -c: line 3: syntax error: unexpected end of file
make: *** [backup] Error 258

Проблема решается добавлением в начало Makefile строки "SHELL=cmd.exe". Вот пример рабочего Makefile, в котором эта ошибка устранена:

RAR = "c:/Program Files/WinRAR/WinRAR.exe"ARCHIVE = myproject.rarSHELL=cmd.exe
 help:
    @echo "This Makefile has no default rule. Use one of the following:"
    @echo "make backup .... backup project" 
 backup:
    $(RAR) a -r -dh -ep1 $(ARCHIVE) ../myproject
    mv $(ARCHIVE) c:/archive/ARMmyproject
    autoname /pattern:YYMMDDhhmmss c:/archive/ARM/myproject/$(ARCHIVE)
    xcopy /M /Y c:/archive/ARMmyproject*.* "\serverWORK"

Сетевые (UNC) пути и пути с пробелами необходимо заключать в двойные кавычки, иначе они будут неправильно переданы интерпретатору cmd.exe. Вот так:

xcopy /M /Y c:/archive/ARMmyproject*.* "serverWORK"

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

→@echo $(OBJECTS)
→@echo "Привет"
→@echo $(BL60X_SDK_PATH)
→rm -r build_out/

info. Для вывода в других местах (до запуска целей) можно использовать info:

$(info $(OBJECTS))
$(info Тут любое сообщение)

warning. Команда warning работает так же, как и info, просто текст сообщения выводится как предупреждение.

error. Команда error позволяет немедленно остановить обработку текущего Makefile с выводом текстового сообщения.

$(error Тут любое сообщение)

Подробный вывод make. Опции -n, -d и V=1 позволят более подробно разобраться, почему при компиляции происходят ошибки.

См. также Q09.

На компьютере бывает установлено несколько систем сборки на основе gcc и make (AVR Studio, Microchip Studio, WinAVR и т. д.). Как определить, какой именно исполняемый файл запускается, когда выполняется команда make?

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

Но есть более простой способ. Можно в текущем каталоге, там где мы используем команду make, создать вот такой командный файл, и запустить его:

@rem Командный Файл для проверки места нахождения make.exe
:loop
make -v
@goto loop

Теперь можно запустить Process Explorer, навести курсор мыши на cmd.exe -> make.exe, и успеете увидеть путь, откуда make.exe запускается.

Системе не удается найти указанный путь. Эта ошибка у меня возникала на makefile-скрипте gcc.mk из библиотеки LUFA, который некорректно работал на Windows. Увидеть причину ошибки позволил запуск make с опцией -d. Ошибка скрипта была в вызове команды mkdir:

   #$(shell mkdir -p $(OBJDIR) 2> /dev/null)
   $(shell mkdir $(OBJDIR) 2> nul)

Первая закомментированная строка содержит 2 ошибки: первая заключается в том, что опция -p ошибочно воспринимается как имя каталога, и команда mkdir создает папку "-p". Вторая ошибка: путь /dev/null на Windows не существует. Во второй строке приведено исправленние ошибки.

Перенаправление в /dev/null присутствует во многих makefile-скриптах библиотеки LUFA, все это надо исправить на nul.

Эта ошибка сигнализирует о проблемах совместимости WinAVR-20100110 с операционной системой Windows 8 и Windows 10. Загрузите и установите патч [9]. Он изменяет файлы sfr_defs.h и msys-1.0.dll.

При сборке появляются ошибки наподобие:

"avr-size" не является внутренней или внешней
командой, исполняемой программой или пакетным файлом.
"grep" не является внутренней или внешней
командой, исполняемой программой или пакетным файлом.

Или:

process_begin: CreateProcess(NULL, avr-gcc --version, ...) failed.

Это означает, что в путях поиска запускаемых утилит (переменная окружения %PATH% операционной системы Windows) отсутствуют соответствующие им записи. Исправить это довольно просто - обычно спасает переустановка WinAVR и перезагрузка (для верности), но можно просто добавить нужные пути в переменную %PATH%. Для этого в переменную Path ужно добавить 2 записи (обычно WinAVR установлен на диск C:):

c:\WinAVR-20100110\bin
c:\WinAVR-20100110\utils\bin

Как редактировать переменные окружения на Windows XP, см. [10]. На более современных Windows 7, Windows 8, Windows 10 процедура аналогичная, нужно только запустить "Дополнительные параметры системы".

Нужно открыть Makefile, найти интересующую цель, и вывести нужную переменную с помощью оператора @echo $(имя_переменной). Например, вот так можно вывести значение переменной SDK_DEMO_PATH:

...
cmake_definition+= -DCONFIG_TLSF=$(CONFIG_TLSF)
 
build:Makefile
   @echo -----------------------------------------------------------------
   @echo $(SDK_DEMO_PATH)
   @echo -----------------------------------------------------------------
   $(CMAKE) -S . -B build -G $(cmake_generator) $(cmake_definition)
   make -C build -j8
   make -C build combine
...

То же самое можно сделать с помощью команды $info, например:

$(info -----------------------------------------------------------------)
$(info $(SDK_DEMO_PATH))
$(info -----------------------------------------------------------------) 

Для этого в командную строку можно добавить V=1 или -n. Пример с использованием V=1:

$ make V=1 CHIP=bl602 BOARD=bl602dk

Пример с -n:

$ make -n CHIP=bl602 BOARD=bl602dk 

Если вы сделали экспорт переменной окружения в консоли и скрипте bash:

export demoPath=/usr/local/demo

... то можете просто извлечь значение этой переменной в makefile (утилита make импортирует все переменные окружения, которые вами были установлены):

DEMOPATH = ${demoPath}

Или так:

DEMOPATH = $(demoPath)

Переменная окружения не будет доступна, пока не сделан её экспорт. Как альтернатива переменную окружения можно передать через командную строку make:

$ make DEMOPATH="${demoPath}" ...

Если вы используете производные от C shell, замените команду export комбинацией setenv demoPath /usr/local/demo.

Это делается командой export. Например, проект нормально компилируется следующей строкой:

$ make CONFIG_CHIP_NAME=BL602 CONFIG_LINK_ROM=1 -j

А хочется, чтобы компиляция происходила более простой командой:

$ make

Для этого в файл Makefile проекта надо добавить строки:

CONFIG_CHIP_NAME=BL602
CONFIG_LINK_ROM=1
export CONFIG_CHIP_NAME CONFIG_LINK_ROM

Всем известно, что для ускорения компиляции можно использовать опцию -j, которая позволяет задействовать дополнительные ядра процессора:

$ make -j

Или (опция -j это аналог опции --jobs):

$ make --jobs

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

1. Написать скрипт, который будет запускать make с нужными опциями.

2. Использовать переменную окружения MAKEFLAGS:

$ MAKEFLAGS="--jobs=32"
$ export MAKEFLAGS

3. Воспользоваться командой alias:

$ alias make='make -j'

Чтобы сделать настройку alias make='make -j' перманентной, эту команду надо добавить в файл .bashrc (этот файл находиится в домашнем каталоге пользователя: ~/.bashrc).

Они управляют поведением make, когда строки команд помечены в начале этими символами:

@ подавляет нормальное 'echo' выполняемой команды. Т. е. предотвращается вывод команды в консоль. Глобально тот же самый эффект будет, если применить для make опцию -s или --keep-silent.

- означает игнорирование статуса на выходе выполненной команды (обычно ненулевое значение на выходе приводит к остановке этой части сборки). Другими словами, дефис в начале строки команды говорит make продолжать работу, даже если команда по какой-то причине потерпела неудачу (вернула ненулевое значение). Вы можете это применить глобально для всех команд через опцию -i (или --ignore-errors).

+ означает 'выполнить эту команду под управлению make -n' (или 'make -t' или 'make -q'), когда команда не выполнилась нормально. См. также спецификацию POSIX для make, а также §9.3 руководства утилиты make (GNU Make manual). Насколько можно судить, плюс в начале строки команды сводит на нет эффект опций -n, -t и -q, которые в основном говорят make, чтобы на самом деле не запускать команды. Таким образом, линия с + в передней части в любом случае будет запущена.

Нотация + является (POSIX-стандартизированным) обобщением де-факто (нестандартизованного) механизма, посредством которого командная строка, содержащая ${MAKE} или $(MAKE), выполняется под make -n (@ обсуждается в §5.2 руководства GNU Make; - описан в §5.5; и §5.7.1 упоминает использование +).

[Как создать простейший makefile]

Предположим, есть три файла: program.c, program.h и header.h. Нужно их скомпилировать компилятором gcc. Как это проще всего осуществить?

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

HEADERS = program.h header.h
 
default: program.exe
 
program.o: program.c $(HEADERS)
    gcc -c program.c -o program.o
 
program.exe: program.o
    gcc program.o -o program.exe
 
clean:
    -rm -f *.o *.exe

Обратите внимание, что вместо пробелов в начале строк должны быть символы табуляции, иначе makefile работать не будет. Это нужно исправить, когда будете копировать этот текст как шаблон нового makefile.

Это простейший шаблон, и недостаток у него в том, что если нужно поддерживать большое количество файлов кода, то для каждого файла нужно добавлять новое правило. Этот недостаток можно устранить так:

HEADERS = program.h headers.h
OBJECTS = program.o
 
default: program.exe
 
%.o: %.c $(HEADERS)
    gcc -c $< -o $@
 
program: $(OBJECTS)
    gcc $(OBJECTS) -o $@
 
clean:
    -rm -f $(OBJECTS) *.exe

Это простейший пример, и для упрощения здесь опущены такие макросы, как $(CC), $(CFLAGS) и другие. Вот пример несложного makefile, который я часто использую для компилирования исходного кода на языке C.

TARGET = prog
LIBS = -lm
CC = gcc
CFLAGS = -g -Wall
 
.PHONY: clean all default
 
default: $(TARGET)
all: default
 
OBJECTS = $(patsubst %.c, %.o, $(wildcard *.c))
HEADERS = $(wildcard *.h)
 
%.o: %.c $(HEADERS)
    $(CC) $(CFLAGS) -c $< -o $@
 
.PRECIOUS: $(TARGET) $(OBJECTS)
 
$(TARGET): $(OBJECTS)
    $(CC) $(OBJECTS) -Wall $(LIBS) -o $@
 
clean:
    -rm -f *.o
    -rm -f $(TARGET)

В этом makefile используются функции wildcard и patsubst утилиты make для автоматического подключения файлов *.c и *.h в текущей директории. Это означает, что когда Вы добавляете новые модули исходного кода в корневой каталог проекта, то Вам уже не надо модифицировать содержимое makefile. Если Вы хотите поменять имя генерируемого исполняемого файла, то Вам нужно просто поменять значение переменной TARGET. Для очистки проекта нужно в корневом каталоге проекта ввести команду make clean, а для компиляции make all или просто make.

В любом случае, ИМХО, не стоит использовать Autoconf/Automake. Конечно, они хорошо работают, но если Вы к ним привыкнете, то не сможете делать самых простых вещей, и Вам будет трудно разобраться в содержимом готовых makefile, которые поставляются вместе с большинством open-source проектов.

addprefix. Функция addprefix позволяет добавить префикс с каждой строке в последовательности строк. Пример:

OBJDIR = Objects
OBJECTS = $(patsubst %.cpp, %.o, $(wildcard *.cpp))
@echo $(OBJECTS)
# выведется trim.o main.o bmpfile.o
OBJ = $(addprefix $(OBJDIR)/, $(OBJECTS))
@echo $(OBJ)
# выведется Objects/trim.o Objects/main.o Objects/bmpfile.o

Утилита gccmakedep. В зависимости от количества заголовков и модулей проекта, и Ваших потребностей в разработке, рассмотрите возможность использования утилиты gccmakedep [3]. Эта программа анализирует текущий каталог, и добавляет в конец makefile зависимости от заголовков для каждого найденного файла *.c/*.cpp. Конечно, если у Вас в проекте только 2 или 3 файла, то такой функционал излишен, но если модулей и заголовков намного больше, то утилита gccmakedep может пригодиться.

[Ссылки]

1. Installing MSYS site:mingw.org.
2. Рабочий Makefile на примере AVR-USB (USB HID-устройство и консольная утилита для работы с ним).
3. gccmakedep site:x.org.
4avr-gcc: как в Makefile задавать и препроцессором проверять тип используемого AVR.
5Переменные окружения Windows.
6Магия makefile на простых примерах.

 

Комментарии  

 
+5 #1 shura 13.04.2017 09:08
> Если в качестве dependencies указаны имена файлов (обычно объектных), то утилита make может проверить - нужно их компилировать, или нет (мне непонятно, как она проверяет, однако это работает).

Проверяется дата создания исходного файла и объектного. Если исходник свежее объектного, то он компилируется.
Цитировать
 

Добавить комментарий


Защитный код
Обновить

Top of Page