Skip to content

Blank project building

Edgar K edited this page Feb 27, 2019 · 41 revisions

Авторы: Казиахмедов Эдгар, Молодцов Владислав

Зачем это нужно?

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

  • отсутствие готового кода, который легко запустить (так как называемый пустой проект);
  • отсутствие надлежащего объяснения кода в пустом проекте;
  • привязка к каким-то конкретным IDE;
  • крайности в использовании библиотек (либо что-то очень тяжелое и высокоуровневое, либо низкоуровневое и нечитаемое).

В данном курсе вероятность возникновения вышеупомянутых проблем сведена к минимуму. Конечно на второй лекции, посвященной сборке пустого проекта, было практически невозможно рассмотреть в подробностях все детали этого процесса, поэтому многие объяснения были попросту опущены. Чтобы избежать непонимания, в статье еще раз будет подробно рассмотрена структура проекта и его сборка с момента скачивания до отладки.

Скачиваем, собираем и заливаем!

Итак, вся работа с кодом и инструментарием предполагается в Linux или MacOS. Цель курса заключается в том, чтобы обеспечить максимальное понимание всего процесса: от написания main до отлаживания (или дебаггинга), а не ограничиться копированием готового кода. Мы отказываемся от использования любых IDE, которые за нас решают, как собирать конечный бинарный файл. Мы будем контролировать все параметры: куда класть полученный бинарный файл, где располагать переменные и как много, какой программой прошивать, какой программой отлаживать, какие выставлять опции для компиляции и так далее. Конечно, это не значит, что IDE не умеют этого делать и поэтому их нельзя использовать (и вообще это абсолютное зло), это лишь значит, что мы должны разобраться как все устроено, и в данном окружении это представляется наиболее возможным.

Итак, после того как установлены все необходимые программы, скачиваем репозиторий в любую удобную папку:

git clone https://github.com/edosedgar/stm32f0_ARM/

После скачивания перейдем в папку labs/01_blank, в которой находится первая лабораторная работа - пустой проект, который ничего не делает. Именно на нём мы будем проверять правильность установки всего инструментария.

cd stm32f0_ARM/labs/01_blank

В папке содержатся следующие файлы:

├── core
├── lib
├── plib
├── main.c
└── Makefile

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

Давайте теперь подробнее рассмотрим структуру папки core:

├── cmsis_gcc.h
├── core_cm0.h
├── startup_stm32f051x8.s
├── stm32f051r8tx_mmap.ld
├── stm32f051x8.h
├── stm32f0xx.h
├── system_stm32f0xx.c
└── system_stm32f0xx.h

У каждого файла есть свое назначение:

cmsis_gcc.h
Здесь содержатся функции для доступа к регистрам Cortex-M0 ядра, функции для выполнения инструкций, вызов которых невозможен напрямую средствами языка С (такие, например, как уход в режим сна, выполнение пользовательского прерывания).

core_cm0.h
В этом файле определяются адреса и структуры регистров ядра, специальных объявлений типов, методы для работы с контроллером прерываний, функции инициализации системного таймера.

startup_stm32f051x8.s
Один из самых важных файлов, его задача - подготовить микроконтроллер к запуску функции main (процессор начинает исполнять инструкции с адреса 0x00000000, а не по адресу где находится main), т.е. реализовать обработчик прерывания Reset, почистить память, перенести глобальные переменные и неконстантные массивы из ПЗУ в ОЗУ, настроить стек, объявить вектор обработчиков прерываний.

stm32f051r8tx_mmap.ld
Данный файл необходим для хранения структуры памяти микроконтроллера. После компиляции линковщик принимает объектные файлы, а также данный файл, чтобы знать, по какому адресу размещать программу, а по какому должен быть стек или переменные (если размер программы будет слишком большой, то именно линковщик скажет вам об этом, а не компилятор).

stm32f051x8.h
В этом файле предоставится набор объявлений (то есть мнемоник адресов памяти) для обращения ко всей периферии в микроконтроллере, а также номера всех прерываний.

stm32f0xx.h
Тут объявляются некоторые флаги, регулирующие параметры Low Layer библиотеки. Другими словами вся библиотека написана под семейство stm32f0, внутри которой есть некоторые минорные различия между определенными подсериями, и для их учета в данном файле хранятся параметры для каждой модели (к примеру, одна и та же периферия может быть недоступна в разных моделях).

system_stm32f0xx.c, system_stm32f0xx.h
Здесь предоставляется функция для сброса состояния системы тактирования.

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

В папке lib хранятся все вспомогательные файлы для main. В данном случае это файл, реализующий стандартные обработчики для системных прерываний, чтобы при возможном вызове одного из них микроконтроллер не перешел по неопределенному адресу.

Последний файл это так называемый файл сборки проекта (или Makefile). Здесь остановимся подробнее и представим, что мы исполняем Makefile вручную, и посмотрим пошагово, что же в нём происходит.

Первым шагом инициализируются все переменные, объявленные со строчки 4 до 68. Далее мы попадаем на следующую строчку:

.PHONY: dirs all clean flash erase

Это всего ли информация о том, что названия, перечисленные после .PHONE является целями сборки, а не именами файлов, которые make должен собрать. Напомним, что Makefile занимается сборкой целей (targets), где каждая цель объявляется слева и заканчивается двоеточием, после которого перечисляются цели, необходимые для сборки на данном этапе:

target: target1 target2 ...
        некоторые операции над target1, target2.. для получения target

Некоторые цели могут быть непосредственно названиями файлов, которые необходимо собрать, но некоторые могут и не быть. Вернемся к нашему Makefile и рассмотрим следующую строчку:

# В нашем случае Makefile уже знает, что PROJECT=build/blank,
# конструкция $() лишь извлекает значение переменной
all: dirs $(PROJECT).bin $(PROJECT).asm

Здесь объявляется первая цель all, для сборки которой необходима сборка целей dirs, $(PROJECT).bin и $(PROJECT).asm. Что ж в таком случае переходим на dirs:

# В нашем случае OUTPATH=build
dirs: ${OUTPATH}

Видно, что для сборки dirs нужно сначала собрать ${OUTPATH}, сказано-сделано:

${OUTPATH}:
	mkdir -p ${OUTPATH}

Отлично, мы собрали первую цель из изначального листа! Теперь появилась папка build в директории проекта. Следующая цель $(PROJECT).bin:

%.bin: %.elf
	$(OBJCOPY) -O binary $< $@

Здесь вместо $(PROJECT).bin написано %.bin, это все лишь значит, что для сборки любого файла с расширением bin нужно использовать данную конструкцию. Но чтобы собрать бинарный файл с нашей программой, необходимо сначала получить файл elf. Разница между ними заключается в том, что первый это лишь набор байтов, который отправится в память, а второй содержит название секций и их адреса. Переходим на сборку elf:

%.elf:
        # Здесь специальная переменная $@ указывает на названием цели, 
        # то есть в нашем случае $(PROJECT).elf
	$(LD) $(OBJS) $(LDFLAGS) -o $@
	$(SIZE) -A $@

Учитывая, что все переменные (LD, OBJS и т.д.) определены, становится понятно, что здесь происходит вызов линковщика, принимающего на вход объектные файлы и флаги (основные характеристики нашего микроконтроллера, такие как линкер-скрипт, модель ядра, формат хранения данных в памяти, наличие математического сопроцессора и т.д.). В результате его работы создается необходимый файл с расширением elf. Далее вызывается программа для распечатки расхода памяти (оперативной, FLASH и пр.). Теперь уже с собранным файлом elf возвращаемся обратно и получаем свой bin. Аналогичным образом собирается файл asm.

Остается последний вопрос, а откуда берутся объектные файлы?

Они собираются в Makefile автоматически по запросу (они являются такими же целями, только ассоциированными с файлами, так как не были перечисленны в PHONY). Таким образом, если нужен файл main.o, Makefile автоматически соберет его из main.c c флагами:

INC_CORE = -Icore
INC_LIB = -Ilib
INC_PERIPH = -Iplib
INCLUDES += $(INC_CORE)
INCLUDES += $(INC_LIB)
INCLUDES += $(INC_PERIPH)

DEFINES = -DSTM32 -DSTM32F0 -DSTM32F051x8 -DHEAP_SIZE=$(HEAP_SIZE)
MCUFLAGS = -mcpu=cortex-m0 -mlittle-endian -mfloat-abi=soft -mthumb \
           -mno-unaligned-access
DEBUG_OPTIMIZE_FLAGS = -O0 -ggdb -gdwarf-2
CFLAGS = -Wall -Wextra --pedantic
CFLAGS_EXTRA = -nostartfiles -nodefaultlibs -nostdlib \
               -fdata-sections -ffunction-sections

CFLAGS += $(DEFINES) $(MCUFLAGS) $(DEBUG_OPTIMIZE_FLAGS) $(CFLAGS_EXTRA) 

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

Теперь давайте соберем проект:

make

В результате мы получим файл blank.elf и blank.bin в папке build, после чего вызовется утилита arm-none-eabi-size, благодаря которой мы получим информацию о размере секций (размер секций может варьироваться в зависимости от версии установленного инструментария):

build/blank.elf  :
section              size        addr
.isr_vector           192   134217728
.text                 852   134217920
.rodata                 8   134218772
.init_array             8   134218780
.fini_array             4   134218788
.data                1072   536870912
.bss                   28   536871984
._user_heap_stack    1540   536872012
.ARM.attributes        40           0
.comment               43           0
.debug_info          8608           0
.debug_abbrev        2418           0
.debug_loc           1474           0
.debug_aranges        320           0
.debug_ranges         152           0
.debug_line          1986           0
.debug_str           2529           0
.debug_frame          520           0
Total               21794

Теперь пришло время загрузить полученную программу в память микроконтроллера и убедиться, что программа для прошивки отрабатывает корректно:

make flash

В результате выполнится:

flash: $(PROJECT).bin
	st-flash write $(PROJECT).bin 0x08000000

что означает запись файла blank.bin в микроконтроллер по адресу 0x08000000, который соответствует начальному адресу во FLASH.

Если программа успешно была загружена в микроконтроллер, то будет выдано следующее сообщения:

INFO flash_loader.c: Successfully loaded flash loader in sram
  2/2 pages written
INFO common.c: Starting verification of write complete
INFO common.c: Flash written and verified! jolly good!

Однако вовсе не обязательно подключать микроконтроллер, чтобы проверить правильность установки инструментария: при отсутствии подключенного микроконтроллера будет выдано следующее сообщение:

WARN usb.c: Couldn't find any ST-Link/V2 devices

свидетельствующее о том, что всё настроено корректно, а единственная возникшая проблема - отсутствие устройства, которое нужно прошить.

Запускаем отладчик

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

Тем не менее для ознакомления открываем еще одно окно терминала в той же папке и запускаем отладочный сервер на базе st-link:

make gdb-server-st

А при установленном отладочном сервере openocd:

make gdb-server-ocd

Теперь в зависимости от выбранного сервера в первом терминале выполняем:

make gdb-st-util / make gdb-openocd

После успешного установления соединения с отладочным модулем в ядре на экране должно быть следующее:

...
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from build/blank.elf...done.
Remote debugging using localhost:4242
0x080002d4 in Reset_Handler ()
Loading section .isr_vector, size 0xc0 lma 0x8000000
Loading section .text, size 0x354 lma 0x80000c0
Loading section .rodata, size 0x8 lma 0x8000414
Loading section .init_array, size 0x8 lma 0x800041c
Loading section .fini_array, size 0x4 lma 0x8000424
Loading section .data, size 0x430 lma 0x8000428
Start address 0x80002d4, load size 2136
Transfer rate: 2 KB/sec, 356 bytes/write.
(gdb) 

Нажимаем Ctrl-X и Ctrl-A друг за другом и переключаемся в красивый внутренний графический интерфейс. Теперь создает новую точку прерывания (или брейкпоинт):

(gdb) b main

тем самым отладка будет остановлена при вызове функции main. Чтобы добраться до нее, запустим ядро:

(gdb) c

Чтобы перейти на следующую строчку кода, достаточно ввести:

(gdb) n

Дополнительная информация по командам GDB доступна в разделе документы на репозитории.