|
|||||||
STM32 с нуля. Часть 2. Пишем простейшую прошивку
Время создания: 11.09.2023 12:15
Автор: Vladimir Bezhenar
Текстовые метки: linux, stm, stm32, gdb, программирование, отладка, debug, debugger, линкер, линковка, ld, файл, формат
Раздел: Компьютер - Аппаратное обеспечение - Микроконтроллеры ARM
Запись: xintrea/mytetra_syncro/master/base/1694423733xhfsfk9b26/text.html на raw.github.com
|
|||||||
|
|||||||
Вообще говоря, прошивка уже была описана в первой части. Нам нужно создать такой файл, в котором будет записано некое число из четырёх байтов, которое процессор присвоит регистру sp, далее там будет записан, к примеру, адрес 0x08000131 в следующих четырёх байтах, далее будут располагаться 296 нулевых байтов (0x130 - 4 - 4 = 304 - 4 - 4 = 296), а за ними 2 инструкции по 4 байта, которые и будут что-то делать. Итого файл прошивки должен занимать 4 + 4 + 296 + 4 + 4 = 312 байтов. Содержимое этого файла мы запишем в микроконтроллер по адресу 0x08000000, где и располагается флеш-память. Первое, что мы сделаем - напишем, собственно, код на языке ассемблера, который соберём в объектный файл: loop.s: .cpu cortex-m3 .syntax unified .thumb .global reset_exception_handler .section code reset_exception_handler: add r0, r0, 1 bl reset_exception_handler Всё, что начинается с точки - является директивами ассемблера и непосредственно в код не преобразовывается. Первые 3 строчки это некое мумбо-юмбо, которое объяснить кратко вряд ли получится. Самая первая строчка говорит о том, что у нас код для процессора cortex-m3. Это семейство ARM-процессоров, одним из представителей которого и является процессор STM32F103. Почитайте мануал, если интересно, но кроличья нора там глубока. Проще просто запомнить. Директива .global reset_exception_handler говорит о том, что мы хотим экспортировать из этого файла символ с именем reset_exception_handler. Каждый объектный файл обычно экспортирует какие-то символы. Чаще всего символ можно воспринимать, как указатель. Далее идёт директива .section code. Объектный файл это по сути набор секций, в каждой секции лежат данные. В нашем случае объектный файл будет содержать одну секцию с названием code, в которой будут лежать 2 инструкции (8 байтов). А также символ reset_exception_handler, который будет указывать на первую инструкцию (по сути он будет равен нулю). Строка reset_exception_handler: это тоже не код, хоть и не начинается на точку. Это метка (иногда метку называют символом), как угодно. А дальше, наконец-то, идёт код, ради которого всё и затевалось. Две инструкции: инкрементировать значение в регистре r0 и перейти на предыдущую инструкцию, в начало цикла. Для того, чтобы собрать этот код в объектный файл, используется программа as (ассемблер). А точней arm-none-eabi-as: > arm-none-eabi-as -o loop.o loop.s Файл loop.o является объектным файлом в формате ELF. Его можно посмотреть с помощью программ objdump и nm: > arm-none-eabi-nm -g loop.o 00000000 N reset_exception_handler arm-none-eabi-objdump -D loop.o loop.o: file format elf32-littlearm Disassembly of section code: 00000000 <reset_exception_handler>: 0: f100 0001 add.w r0, r0, #1 4: f7ff fffe bl 0 <reset_exception_handler> Disassembly of section .ARM.attributes: 00000000 <.ARM.attributes>: 0: 00002041 andeq r2, r0, r1, asr #32 4: 61656100 cmnvs r5, r0, lsl #2 8: 01006962 tsteq r0, r2, ror #18 c: 00000016 andeq r0, r0, r6, lsl r0 10: 726f4305 rsbvc r4, pc, #335544320 @ 0x14000000 14: 2d786574 cfldr64cs mvdx6, [r8, #-464]! @ 0xfffffe30 18: 0600334d streq r3, [r0], -sp, asr #6 1c: 094d070a stmdbeq sp, {r1, r3, r8, r9, sl}^ 20: Address 0x20 is out of bounds. Как видно из вывода nm, в этом файле экспортируется один символ reset_exception_handler со значением 0. С выводом objdump посложней. В файле находится две секции: code и <.ARM.attributes>. Секция code - это то, что мы объявили. В нём 8 байтов, которые нам любезно дизассемблировали. Секция <.ARM.attributes> содержит служебные сведения, которые в конечной прошивке не появятся, поэтому её можно игнорировать. objdump попытался эти сведения дизассемблировать, но на самом деле это не машинный код, а просто формат такой. objdump -D пытается всё дизассембировать, даже если это не имеет смысла. У нас теперь есть 8 байтов нашего кода, но нужно скомпоновать всё остальное. Конечно можно в каком-нибудь hex-редакторе это сделать вручную, но вообще для этого используется компоновщик (linker, далее линкер). В составе GNU binutils имеется линкер ld, его мы и будем использовать, а точней его версию для ARM arm-none-eabi-ld. Линкер также использует свой особый язык: linker script. По сути задача линкера состоит в следующем: он получает на вход набор объектных файлов (в нашем случае это один файл loop.o). В каждом из этих файлов есть некоторое множество секций и символов. В секциях есть какие-то данные: код, начальные значения переменных и тд. Они называются в терминах линкера входные секции (input sections). На выходе у линкера тоже объектный файл, и в нём тоже некоторый набор секций и символов: выходные секции (output sections). У нас задача простая, поэтому выходная секция будет ровно одна, с интересующими нас данными, которые будут прошиваться в микроконтроллер. Вот такой скрипт для линкера мы будем использовать: Файл linker.ld SECTIONS { main 0x08000000 : { LONG(0x20000000 + 20K); LONG(reset_exception_handler | 1); . = 0x130; loop.o(code) } } Ключевое слово SECTIONS объявляет выходные секции. Далее объявление main - это название нашей выходной секции. Можно называть её как угодно. 0x0800 0000 это адрес, по которому эта секция будет располагаться в памяти. Это необходимо для того, чтобы линкер правильно подсчитал смещения, из loop.o мы экспортируем сивол reset_exception_handler со значением 0, но на самом деле в конечной прошивке у него будет значение 0x0800 0130. Строка LONG(0x20000000 + 20K); запишет по первому адресу в данной секции 4-х байтовое значение, которое процессор присвоит регистру sp. В принципе по выражению очевидно, что мы ему присваиваем значение адреса сразу за окончанием адресного пространства оперативной памяти. Чуть ниже будет попытка объяснить, почему именно такое значение. Строка LONG(reset_exception_handler | 1); запишет по следующему адресу 4-х байтовое значение, которое представляет собой модифицированный адрес кода, который мы хотим выполнять после включения. Символ reset_exception_handler к нам пришёл из loop.o. Как было описано в первой части, этот адрес должен иметь выставленный единичный бит, поэтому мы его и выставляем с помощью операции "побитовый ИЛИ". Следующая строка . = 0x130; - это команда, которая ставит текущую позицию в выходной секции на 0x130 байтов. Вообще . это такое специальное значение, которое равно адресу, куда линкер сейчас будет что-то писать. Изначально оно равно 0, после первых 4 байтов оно равно 4, после следующих 4 байтов оно равно 8, ну а после присваивания оно равно 0x130 и последующие данные будут писаться уже с этим смещением. Почему 0x130 - тоже ниже будет объяснение. Строка loop.o(code) - это выражение, которое берёт входной файл loop.o, берёт в нём секцию code и копирует её содержимое в выходную секцию. Кроме того линкер делает то, ради чего его, собственно, и используют. Он понимает, что reset_exception_handler уже равен не 0, а 0x0800_0130 и в нужном месте запишет правильный адрес. Если у нас есть несколько функций в разных файлах, которые вызывают друг друга, то линкер разберётся, у какой функции какой итоговый адрес и правильно всё скомпонует. Если вы видели в других линкер скриптах выражение вроде *(.text), то это примерно то же: * - это все файлы, .text - это название секции, которую принято использовать для кода. Но в данном примере всё указано максимально явно и для наглядности использовано нестандартное название секции. Компоновщик запускается командой: > arm-none-eabi-ld -T linker.ld -o loop-asm.elf loop.o Если не было допущено никаких ошибок, то у нас получится файл loop-asm.elf. По расширению, наверное, очевидно, что это объектный файл в формате ELF (как и loop.o). Если его просмотреть с помощью nm и objdump, то можно увидеть следующее: > arm-none-eabi-nm loop-asm.elf 08000130 R reset_exception_handler arm-none-eabi-objdump -D loop-asm.elf loop-asm.elf: file format elf32-littlearm Disassembly of section main: 08000000 <reset_exception_handler-0x130>: 8000000: 20005000 andcs r5, r0, r0 8000004: 08000131 stmdaeq r0, {r0, r4, r5, r8} ... 08000130 <reset_exception_handler>: 8000130: f100 0001 add.w r0, r0, #1 8000134: f7ff fffc bl 8000130 <reset_exception_handler> Disassembly of section .ARM.attributes: 00000000 <.ARM.attributes>: 0: 00002041 andeq r2, r0, r1, asr #32 4: 61656100 cmnvs r5, r0, lsl #2 8: 01006962 tsteq r0, r2, ror #18 c: 00000016 andeq r0, r0, r6, lsl r0 10: 726f4305 rsbvc r4, pc, #335544320 @ 0x14000000 14: 2d786574 cfldr64cs mvdx6, [r8, #-464]! @ 0xfffffe30 18: 0600334d streq r3, [r0], -sp, asr #6 1c: 094d070a stmdbeq sp, {r1, r3, r8, r9, sl}^ 20: Address 0x20 is out of bounds. Как видно, этот файл тоже экспортирует символ reset_exception_handler, но теперь уже со значением 0x0800_0130. В этом файле имеются две секции main и <.ARM.attributes>. Последнюю мы так же проигнорируем, а вот в секции main записано то, что мы и хотели получить. Команда objdump -D пытается дизассемблировать первые 8 байтов, и у нее даже что-то получается, но, конечно, это не команды, а адреса. А вот то, что начинается с адреса 0x0800_0130 это уже самый, что ни на есть, машинный код для ARM. Но остаётся одна маленькая проблема. Как в самом начале было написано, файл прошивки должен занимать 312 байтов. А у нас вроде эти байты и есть, но они не пойми где, а весь elf файл занимает 4864 байтов, в общем не совсем то. Чтобы вытащить конечную прошивку, используется команда objcopy: arm-none-eabi-objcopy -O binary -j main loop-asm.elf loop-asm.bin Опция -O binary говорит о том, что мы хотим получить бинарный формат на выходе. Опция -j main говорит, что нас интересует только секция main (этот флаг избыточен, когда у нас только одна не-служебная секция, но пусть будет для ясности). Теперь посмотрим, что получилось: > ls -l loop-asm.bin -rwxr-xr-x. 1 vbezhenar vbezhenar 312 Sep 9 11:30 loop-asm.bin > hexdump -C loop-asm.bin 00000000 00 50 00 20 31 01 00 08 00 00 00 00 00 00 00 00 |.P. 1...........| 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| * 00000130 00 f1 01 00 ff f7 fc ff |........| 00000138 Ну, собственно, 312 ожидаемых байтов и внутри что-то, похожее на правду. В кодировке little-endian число 0x2000_5000 кодируется байтами в обратном порядке: 00 50 00 20, а число 0x0800_0131 кодируется байтами 31 01 00 08. Пора заливать эту прошивку в микроконтроллер: > st-flash --connect-under-reset write loop-asm.bin 0x08000000 st-flash 1.7.0 2023-09-09T11:36:28 WARN common.c: NRST is not connected 2023-09-09T11:36:28 INFO common.c: F1xx Medium-density: 20 KiB SRAM, 64 KiB flash in at least 1 KiB pages. file loop-asm.bin md5 checksum: 20b87b3b138d91c38b47d29d95f773b, stlink checksum: 0x0000058d 2023-09-09T11:36:28 INFO common.c: Attempting to write 312 (0x138) bytes to stm32 address: 134217728 (0x8000000) 2023-09-09T11:36:28 INFO common.c: Flash page at addr: 0x08000000 erased 2023-09-09T11:36:28 INFO common.c: Finished erasing 1 pages of 1024 (0x400) bytes 2023-09-09T11:36:28 INFO common.c: Starting Flash write for VL/F0/F3/F1_XL 2023-09-09T11:36:28 INFO flash_loader.c: Successfully loaded flash loader in sram 2023-09-09T11:36:28 INFO flash_loader.c: Clear DFSR 1/ 1 pages written 2023-09-09T11:36:28 INFO common.c: Starting verification of write complete 2023-09-09T11:36:28 INFO common.c: Flash written and verified! jolly good! Собственно: всё. Прошивка залита в микроконтроллер, он перезагрузился и теперь крутится в вечном цикле, немножко согревая воздух. Теперь к нему можно подключиться через st-util и gdb и проверить, что там происходит, в первой части это и было описано. Теперь пару моментов. Во-первых почему именно такое значение мы пишем в регистр sp. Вообще стек - это такая структура данных, и если вдруг вы не знаете, что это такое, то проглядите википедию, прежде чем продолжать. Эта структура данных настолько важна, что в процессоре есть отдельные регистры и команды для работы с ним, т.н. аппаратный стек. Вопреки интуитивному представлению, аппаратный стек растёт "сверху вниз", или от больших адресов к меньшим. Регистр sp хранит адрес, куда было записано последнее значение. Команда push {r0} сначала уменьшает значение sp на 4, а потом записывает в память по адресу $sp значение $r0. Команда pop {r1} сначала присваивает регистру r1 значение из памяти по адресу $sp, а потом увеличивает значение регистра sp на 4. На саммом деле не обязательно устанавливать sp именно в конце, для стека можно выделить любой удобный участок оперативной памяти, но в простых программах разумно стеку отдать верхнюю часть памяти, с большими адресами, а свои переменные располагать в нижней части памяти, с меньшими адресами. В нашей простейшей программе стек не используется, поэтому регистр sp можно инициализировать любым значеним. Но почти в любой нетривиальной программе стек обязательно будет использоватья. Во-вторых откуда взялось число 0x130, почему бы нам не расположить наш код сразу же со смещением 8. На самом деле это можно сделать и всё будет работать в данном конкретном случае. Но в общем случае так делать не нужно. В начале адресного пространства расположена т.н. таблица векторов (название странное, не ищите в нём смысл). Правильней было бы её назвать таблицей указателей на обработчики исключений. Сразу скажу, что это не исключения из C++, это исключения процессора. Некоторые исключения вызываются прерываниями, некоторые исключения вызываются по другим причинам. К примеру если вы попробуете скормить процессору какую-нибудь дичь, то вызовется исключение под названием usage fault. Когда процессор вызывает исключение, он приостанавливает текущий код (который, кстати, может обрабатывать другое исключение), находит адрес обработчика исключений в таблице векторов, проверяет, что у этого адреса младший бит выставлен в единицу и вызывает функцию с этим адресом. Например по смещению 0x0000 000c расположен адрес обработчика исключения hard fault. Число обработчиков исключений для разных моделей процессоров разное, для STM32F103 эту информацию можно посмотреть в Reference Manual, раздел 10.1.2, таблица 63. Там видно, что адрес последнего обработчика DMA2_Channel4_5 равен 0x0000_012C. Прибавим 4 и получим "свободную" память по адресу 0x0000_0130. В первом разделе мы выяснили, что флеш память доступна с адреса 0x0800_0000, а при загрузке с флеш-памяти она также доступна с адреса 0x0000_0000. Отсюда и взялся этот 0x0800_0130. И напоследок давайте напишем Makefile. Команды выше, конечно, простые и в целом понятные, отрабатывают за тысячные доли секунды, но всё же для организации процесса сборки разумно использовать make. Файл Makefile loop.bin: loop.elf arm-none-eabi-objcopy -O binary -j main loop.elf loop.bin flash: loop.bin st-flash write loop.bin 0x08000000 loop.elf: linker.ld loop.o arm-none-eabi-ld -T linker.ld -o loop.elf loop.o loop.o: loop.s arm-none-eabi-as -o loop.o loop.s clean: rm -f loop.o loop.elf loop.bin Каждое правило имеет вид: target-file: source-file1 source-file2 program argument1 argument2 ... Очень важно, что во второй строчке для отступа использутся символ табуляции, не пробелы. Убедитесь, что ваш редактор настроен правильно. Схема работы make очень простая:
Конечно у GNU make на самом деле функционала несоизмеримо больше, и в сложных программах этот функционал может быть весьма полезен. Если мы запустим make loop.bin в первый раз, то выполняются все нужные команды: > make loop.bin arm-none-eabi-as -o loop.o loop.s arm-none-eabi-ld -T linker.ld -o loop.elf loop.o arm-none-eabi-objcopy -O binary -j main loop.elf loop.bin Если запустим во второй раз, то make ничего не будет делать: > make loop.bin make: 'loop.bin' is up to date. Если изменим дату модификации одного из исходных файлов, то make выполнит часть команд: > touch linker.ld > make loop.bin arm-none-eabi-ld -T linker.ld -o loop.elf loop.o arm-none-eabi-objcopy -O binary -j main loop.elf loop.bin loop.s не изменился, значит loop.o пересобирать нет нужды. Внимательный читатель может увидеть, что файлов clean и flash у нас в проекте нет, а правила есть. Это т.н. phony targets, им никакие файлы не соответствуют, а нужны они просто для удобства. Набрали make clean и почистили директорию. |
|||||||
Так же в этом разделе:
|
|||||||
|
|||||||
|