STM32F030. make-файл

Сейчас мы займемся темой создания make-файлов. Начнем с создания идеологически неправильного, зато простого и понятного.

Для чего нужен make-файл?

Make-файл — это мощнейший инструмент программиста-разработчика (и не только программиста!), облегчающий многие рутинные работы. Назначение make-файла примерно такое же, как назначение меню, которое обычно находится в верхней части большинства IDE (интегрированной среды разработки).

Если в IDE-шном меню все действия по выбору того или иного процесса (компиляция, сборка, сохранение и т.д.) сводятся к прицельному мышко-тыканию в соответсвующий пункт меню, то те же самые действия в отношении make-файла осуществляются набором команды. Например, команда для сборки проекта:

$ make

Команда для установки программного обеспечения:

$ make install

Команда для очистки проекта от производных файлов и прочего мусора:

$ make clean

Команда для заливки кода в микроконтроллер:

$ make load

Ну и пожалуй хватит примеров. Важно понять, любое действие вы можете прописать в этом файле и он будет выполнять его. И не просто выполнять, но и отслеживать зависимости!

Например, вы только что изменили что-то в одном из множества файлов проекта и даете команду на заливку кода в микроконтроллер. Утилита make (которая, собственно, занимается обработкой вашего make-файла) проверит все зависимости файлов, начиная от файла, который содержит код для заливки, и заканчивая всеми без исключения исходными файлами, и обнаружит, что файл для заливки «устарел». Иначе говоря, дата его создания оказалась более ранней, чем дата одного исходного файла. То есть проект нужно заново откомпилировать, собрать, вытянуть из elf-файла двоичнй код, и только после всего этого можно приступить к заливке.

Но заново компилировать все файлы не имеет смысла. Будут заново откомпилированы и собраны только те файлы, которые не удовлетворяют зависимостям.

Понятно, что Вам не нужно все это отслеживать. Всю эту работу берет на себя ваш make-файл. А поскольку вы его сами создаете и сами поддерживаете в процессе развития проекта, то вы полностью контролируете весь свой проект.

Это очень сильно. Те, кто не имеют достаточного опыта работы с make-файлами, не могут оценить их мощь.

Давайте попробуем создать свой make-файл. Я сразу предупреждаю — наш первый make-файл (и возможно несколько следующих файлов) будут идеологически неверными.

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

Итак, нам нужен make-файл, который автоматизирует наши часто повторяющиеся действия. В голову приходит мысль — поскольку мы в Линуксе, то давайте создадим скрипт.

А что! Давайте!

Вот скрипт, который выполняет сборку для нашего проекта «светодиодоморгания»:

#! /bin/bash

# Идеологически неверный make-файл

# Компиляция (ассемблирование) исходных кодов
# На выходе получаем объектные файлы *.o
arm-none-eabi-as -mcpu=cortex-m0 -o start-up.o start-up.S
arm-none-eabi-as -mcpu=cortex-m0 -o blinky.o blinky.S

# Линковка (сборка объектных файлов в один elf-файл)
# За одно создаем файл карты памяти
arm-none-eabi-ld --gc-sections -Map blinky.map -T linked.ld -o blinky.elf start-up.o blinky.o

# Узнаем чего и сколько мы наплодили
arm-none-eabi-size blinky.elf

# Извлечение HEX-кода для прошивки
# arm-none-eabi-objcopy -j .flash -j .data_at_ram -O ihex blinky.elf blinky.hex

# Еще один вариант извлечения двоичного кода для прошивки
arm-none-eabi-objcopy -Obinary blinky.elf blinky.bin

# Заливаем код в микроконтроллер
stm32flash -o /dev/ttyS0
stm32flash -c -w blinky.bin -g 0x0 /dev/ttyS0

# Далее следуют старые (можно сказать архивные) записи, которые уже не нужны, но
# (но кто знает!), может придеся опять к ним когда-нибудь обратиться

# Заливка кода в МК через программатор ST-Link, который расположен на
# плате STM32_VLDISCOVERY
#st-flash write /dev/sg0 blinky.bin 0x08000000
#st-flash read /dev/sg0 flash.bin 0x08000000 0x100

После создания этого файла, который должен находится в директории проекта, файл нужно сделать исполняемым с помощью команды:

$ chmod +x make.sh

В результате мы можем сообирать наш проект и заливать его в МК одной командой:

$ ./make

Конечно, всё это работает. Но всё это как-то не серьезно и далеко от истины.
Да и управление проектом при таком раскладе оставляет желать лучшего. И в самом деле, а что если нам не нужно выполнять то или иное действие? В этом случае нам придется редактировать файл — закрывать комментарием те команды, которые нам не нужны, и снимать комментарий с нужных. Где гарантия, что при частом изменении файлы мы чего-нибудь не накосячим?

Поэтому давайте перейдем к правильной идеологии создания make-файлов.

Давайте создадим еще один, на этот раз обычный текстовый (то есть не исполняемый скриптовый) файл. Его название должно быть строго — либо makefile, либо Makefile. Только эти два имени утилита make подхватывает по умолчанию. Другие имена тоже допускаются, но утилита их не увидит. Но чтобы «скормить» их утилите, нужно сделать дополнительные движения по клавиатуре, примерно вот так:

$ make -f mymakefile

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

Да, и еще! Пару слов про разницу между makefile и Makefile. Утилита make сначала ищет в текущем директории файл makefile, и если его нет, то пытается найти Makefile. Первый встретившийся из них будет выполнен. Вообще-то есть еще одно имя — GNUmakefile, оно тоже принимает участие в «соревнованиях», но не будем отвлекаться…

Наш новый make-файл выглядеть вот так:

# Вторая версия make-файла.
# Идеологически она более верная, но всё ещё далека от совершенства.

.PHONY: load

# Заливаем код в микроконтроллер
load: blinky.bin
	stm32flash -o /dev/ttyS0
	stm32flash -c -w blinky.bin -g 0x0 /dev/ttyS0

# Извлекаем двоичного код для прошивки
blinky.bin: blinky.elf
	arm-none-eabi-objcopy -Obinary blinky.elf blinky.bin

# Линковка (сборка объектных файлов в один elf-файл)
blinky.elf: start-up.o blinky.o
	arm-none-eabi-ld --gc-sections -Map blinky.map -T linked.ld -o blinky.elf start-up.o blinky.o
	# Узнаем чего и сколько мы наплодили
	arm-none-eabi-size blinky.elf

# Компиляция (ассемблирование) исходных кодов
start-up.o: start-up.S
	arm-none-eabi-as -mcpu=cortex-m0 -o start-up.o start-up.S

blinky.o: blinky.S
	arm-none-eabi-as -mcpu=cortex-m0 -o blinky.o blinky.S

Обратите внимание на отступы — они должны быть сделаны не пробелами, а табуляцией! Это принципиально. Иначе работать не будет. Движок блога не позволяет сделать отступы в виде табуляции, поэтому они здесь представлены в виде пробелов. Учтите этот момент, если будете копировать отсюда содержимое make-файлов.

Теперь попробуем понять, что мы тут имеем. Во-первых, как и почти везде в Линуксе, в make-файле можно делать комментарии. Это очень удобно — пояснять свои действия, оставлять себе записочки на память. (Вообще не представляю себе Виндовое меню вот с такими пользовательскими пометкам. Да, такое там вообще не возможно реализовать!)

Все действия в make-файле подчиняются следующей структуре:

цель: зависимость-1 зависимость-2 зависимость-3 ...
        команда-1
        команда-2
        команда-3
        ...

цель — это ради чего мы выполняем команды (команда-1, команда-2, команда-3 …).

команды — это самые обычные консольные команды.

зависимости — это то, от чего зависит цель.

Зависимостей может не быть совсем, может быть одна зависимость, может быть несколько. Это как раз то, что отслеживает утилита make.

Например

start-up.o: start-up.S
	arm-none-eabi-as -mcpu=cortex-m0 -o start-up.o start-up.S

здесь целью является файл start-up.o. Чтобы достичь этой цели (получить этот файл) нужно выполнить команду arm-none-eabi-as, которая находится в следующей строчке и которая начинается с символа табуляции. В этом примере — это команда ассемблирования исходного текста.

Мы так же видим, что цель зависит от файла start-up.S. Если этот файл измениться, цель потеряет свою актуальность, и ее нужно будет снова достигать.

Еще пример:

blinky.elf: start-up.o blinky.o
	arm-none-eabi-ld --gc-sections -Map blinky.map -T linked.ld -o blinky.elf start-up.o blinky.o
	# Узнаем чего и сколько мы наплодили
	arm-none-eabi-size blinky.elf

Здесь цель — elf-файл blinky.elf. Чтобы его получить, в принципе, достаточно команды ликовки arm-none-eabi-ld. Но мы попутно в этом же месте узнаем про размеры кода и переменных, поэтому здесь используются у нас две команды.

Заметим так же, что цель зависит от двух файлов — start-up.o и blinky.o. И если хотя бы один из них окажется новее, чем цель, то цель будет снова достигнута (путем выполнения всех ее команд).

Более того, утилита make прослеживает цепочки зависимостей до самого последнего файла. Иначе говоря, проверкой зависимости цели blinky.elf от start-up.o и blinky.o дело не ограничивается, ведь сами зависимости зависят от других файлов (от других зависимостей). И вполне может оказаться так, что где-то подновился исходный файл, и хотя здесь все в порядке, но исходник будет заново откомпилирован, и следовательно придется пересобирать и эту цель.

Конечно, сложно удержать в голове все эти зависимости и «внезапные» изменения каких-то файлов. Поэтому и была создана эта утилита.

Еще пример.

load: blinky.bin
	stm32flash -o /dev/ttyS0
	stm32flash -c -w blinky.bin -g 0x0 /dev/ttyS0

Здесь цель — load. Но такого файла у нас в директории нет и никогда не будет. Поэтому эта цель всякий раз будет актуальна. Файла-то нет, значит нужно выполнять команды. Даром, что после их выполнения файл все равно не появится! Поэтому команды будут выполняться каждый раз.

Ну то, что цель зависит от бинарного файла blinky.bin — это вы уже наверняка сами заметили. Бинарный файл тоже где-то в этом make-файле выступает в качестве цели, и тоже имеет свои зависимости. Итак до исходников. Таким образом, мы застрахованы от того, что если что-то у нас в проекте измениться, то всегда бинарник будет соответствовать последним изменениям.

А что же будет, если мы забудем про цель load и случайно создадим в директории файл с таким же именем load ? Хороший вопрос!

Может возникнуть такая ситуация — этот файл будет иметь свежую дату, которая введет в заблуждение утилиту make. Утилита посчитает, что пересоздавать файл load не надо, и ничего делать не будет. Не хорошо, однако!

Вот, чтобы такого не было, в make-файле указываются фиктивные цели. Делается это с помощью команды .PHONY:

.PHONY: load

Теперь утилита make, будет знать, что для достижения цели load не нужно искать в директории одноименный файл.

Я ничего еще не сказал про указание конкретных целей в командной строке при вызове утилиты make. Если мы просто набираем команду:

$ make

то утилита будет выполнять действия по актуализации первой встретившейся ей в make-файле цели.

Make-файл — это описание зависимостей и предпринимаемых по ним действий. Это описание отношений, но не строгая последовательность команд какая обычно бывает в скриптовых файлах или программах. Поэтому, чтобы не произошло непредвиденного, где-нибудь в самом начале make-файла обычно указывают еще одну фиктивную цель all. Для нашего make-файла она будет выглядеть так:

all: blinky.elf

Заметьте, у этой цели нет действий, но есть зависимость от elf-файла. Таким образом, если мы в командной строке введем просто одну команду make, то утилита make попытается удовлетворить зависимость для цели all, то есть попытается собрать файл blinky.elf, если он отсутствует или имеет неактуальную сборку.

При этом цель load оказалась как бы сбоку, то есть к файлу blinky.elf не имеет никакого отношения. И теперь, чтобы залить код в микроконтроллер, мы должны это явно указать:

$ make load

Согласитесь, что набрать два коротких слова — это не чрезмерный труд. Но зато какой мощный инструмент мы получаем в замен!

Давайте в make-файл введем еще одну цель — очистку проекта. Это бывает нужно, когда предстоит сбросить проект на флешку или сохранить его в архиве. Чтобы не таскать за собой ненужные (производные файлы), перед копированием или архивацией от них лучше избавиться. Просто удалим их.

Смотрите как мы это можем сделать:

clean:
       rm *.o *.elf *.hex *.bin *.map *~

Цель clean — фиктивная, и она не имеет зависимостей. Но при написании команды удаления будьте предельно осторожны — не зацепите что-нибудь нужное! У вас в руках настоящий Джедайский меч — Линукс, а не пластмассовый ножичек для пластилина! Помните о силе оружия и личной ответственности!

Поскольку у нас появились еще две фиктивных цели, то нужно подредактировать команду .PHONY:

.PHONY: all clean load

Мы далеко еще не исчерпали возможности утилиты make. Мы только попробовали ее в деле. В следующих статьях я обязательно вернусь к ней и мы научимся еще многим вещам.

А пока остановимся на этом. Хотя…

есть еще одна классная фича — компиляция проекта в нескольких потоках одновременно. Если у вас многоядерный комп, то вы можете существенно ускорить сборку вашего проекта, теоретически в столько раз, сколько у вас в компе ядер.

Например, если у вас 4-ядерный проц, то укажите в команде опцию -j 5 (количество ядер +1)

$ make -j 5

Да, пребудет с вами сила Джа!

Оставьте комментарий