-
Notifications
You must be signed in to change notification settings - Fork 62
Blank project building
Авторы: Казиахмедов Эдгар, Молодцов Владислав
При изучении любого микроконтроллера после нескольких дней, потраченных на чтение документации, рассмотрения внутреннего устройства и подготовки окружения, наступает момент написания первой программы. На этом этапе у большинства людей мотивация к изучению существенно ослабевает, а происходит это в силу ряда причин:
- отсутствие готового кода, который легко запустить (так как называемый пустой проект);
- отсутствие надлежащего объяснения кода в пустом проекте;
- привязка к каким-то конкретным 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,
# В нашем случае 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 доступна в разделе документы на репозитории.