MyTetra Share
Делитесь знаниями!
STM32 с нуля. Часть 1. Подключаем и исследуем плату
Время создания: 08.09.2023 16:09
Автор: Vladimir Bezhenar
Текстовые метки: linux, stm, stm32, gdb, программирование, отладка, debug, debugger
Раздел: Компьютер - Аппаратное обеспечение - Микроконтроллеры ARM
Запись: xintrea/mytetra_syncro/master/base/1694178558npbqj1a7k9/text.html на raw.github.com

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


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

Если интерес будет, буду продолжать.

Итак осваиваем STM32 не как нормальные люди.

Примерный план:

  1. Подключить его к компьютеру и убедиться, что там что-то происходит. Использовать будем st-util и gdb.
  2. Написать простейшую программу на ассемблере, которая в цикле прибавляет регистр, скомпилировать из неё прошивку, залить на плату и пронаблюдать её работу. Использовать будем binutils и st-flash.
  3. Поморгать диодом (на ассемблере же).
  4. Переписать осмысленный код на С (дальше всё на С).
  5. Переписать моргание с использованием таймера, чтобы внести свой вклад в борьбу с глобальным потеплением.
  6. Сказать внешнему миру «Hello world» через UART.
  7. Переписать «Hello world» с помощью CMSIS, уже с пониманием того, что там происходит.

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

Сразу скажу, что в процессе будет использовано достаточно много инструментов вроде make, ld, gdb, as, gcc и тд, по каждому из них можно книги писать (и пишут). Поэтому, конечно, углубляться в них я не буду, а напротив буду использовать в максимально примитивном виде.

Ниже ссылки на мануалы, где можно подробней раскрыть тему. Ожидается, что читатель будет активно ими пользоваться.


Часть 1: подключаем и исследуем плату


Для неё нужен компьютер, собственно плата и программатор ST-Link.

На компьютер нужно установить Arm GNU Toolchain. Его можно скачать тут, возможно в вашем дистрибутиве уже есть готовые пакеты. У вас должна работать команда arm-none-eabi-gdb. Также нужно установить st-link (исходники), он тоже есть во многих репозиториях. Я всё делал на линуксе, но в теории макось и винда тоже должны подойти.

Программатор у меня - фирменный st-link, выломанный из фирменной платы Nucleo. Но больше распространены китайские st-link в виде флешки, они тоже довольно дёшевы.

Программатор нужно подключить к компьютеру и выполнить команду st-info --probe. Он должен отобразить некоторую информацию о программаторе. Если у вас ошибка, попробуйте sudo st-info --probe. Если сработает, значит проблема с правами, копайте udev, ну или работайте от рута.

Плата у меня т.н. blue pill с микроконтроллером STM32F103C8T6. Это дешёвая и распространённая китайская плата. Я не специалист по STM32, но мне показалось, что на базовом уровне они все работают примерно одинаково, поэтому, наверное, информация будет применима и для других плат.

У меня плата после покупки и подключению к компьютеру моргала синим диодом. То бишь в ней уже была какая-то простейшая прошивка для демонстрации работоспособности.

Плату к программатору надо подключить тремя проводами: GND, SWCLK, SWDIO. Я плату и программатор вставлял в два USB-разъёма USB-хаба и у меня всё было хорошо и ничего не сгорело. Также плату можно запитать от программатора, тогда в USB её включать не придётся. Можно ли плату подключить к USB-зарядке, а программатор к компьютеру - я не знаю.

После того, как плата подключена к программатору, st-info --probe должен показать информацию о плате. Если на этом этапе проблемы, нужно их решить.

Вот что выводит у меня:


> st-info --probe

Found 1 stlink programmers

version: V2J29S18

serial: 066CFF494849887767255731

flash: 65536 (pagesize: 1024)

sram: 20480

chipid: 0x0410

descr: F1xx Medium-density


Теперь попробуем подключиться к плате отладчиком. Это самое интересное. Для этого в одной вкладке терминала запускаем st-util --connect-under-reset

Он выведет что-то вроде


2023-09-08T17:26:46 WARN common.c: NRST is not connected

2023-09-08T17:26:46 INFO common.c: F1xx Medium-density: 20 KiB SRAM, 64 KiB flash in at least 1 KiB pages.

2023-09-08T17:26:46 INFO gdb-server.c: Listening at *:4242...


Что это предупреждение означает, я не знаю, если подключить ногу NRST от микроконтроллера к программатору, оно не исчезает. Но вроде всё работает, поэтому будем его игнорировать.

Собственно видно: что st-util запустил TCP-сервер на порту 4242. К этому серверу будет подключаться gdb.

Во второй вкладке запускаем arm-none-eabi-gdb (возможно обычный gdb тоже подойдёт, я не знаю, честно говоря, в чём отличие):


> arm-none-eabi-gdb

GNU gdb (Arm GNU Toolchain 12.3.Rel1 (Build arm-12.35)) 13.2.90.20230627-git

Copyright (C) 2023 Free Software Foundation, Inc.

License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>

This is free software: you are free to change and redistribute it.

There is NO WARRANTY, to the extent permitted by law.

Type "show copying" and "show warranty" for details.

This GDB was configured as "--host=x86_64-pc-linux-gnu --target=arm-none-eabi".

Type "show configuration" for configuration details.

For bug reporting instructions, please see:

<https://bugs.linaro.org/>.

Find the GDB manual and other documentation resources online at:

<http://www.gnu.org/software/gdb/documentation/>.


For help, type "help".

Type "apropos word" to search for commands related to "word".

(gdb)


И дальше подключаемся к вышеупомянутому серверу:


(gdb) target remote 127.0.0.1:4242

Remote debugging using 127.0.0.1:4242

warning: No executable has been specified and target does not support

determining executable automatically. Try using the "file" command.

0x08000130 in ?? ()


Итак, мы подключились отладчиком к плате. В последней строчке у вас, скорей всего, будет другой адрес, не суть.

Теперь пару слов про то, как вообще работает этот микроконтролер. Я, конечно, буду упускать много деталей, попытаюсь передать суть.

Вообще у него всё взаимодействие со всеми устройствами ведётся через оперативную память (и прерывания). Это 32-битный процессор, т.е. он может адресовать 4 ГБ памяти. Конечно самой памяти у него гораздо меньше, на моём устройстве 20 кибибайтов оперативной памяти и 64 кибибайта флеш-памяти. Поэтому адресного пространства хватает и на оперативную память, и на флеш-память и на все остальные периферийные устройства. Если быть более конкретным, то оперативная память начинается с адреса 0x2000 0000, а флеш-память начинается с адреса 0x0800 0000. А также имеется несколько десятков периферийных устройств, каждое из которых имеет свои участки памяти, через которые идёт взаимодействие с этим самым устройством. Всё взаимодействие по сути сводится к чтению и записи данных по адресам памяти.

Итак мы подали питание. У платы есть два пина BOOT0 и BOOT1. Они настраиваются двумя перемычками. Есть три варианта загрузки, нас интересует загрузка из главной флеш-памяти. Для этого нужно выставить BOOT0 в «0» (т.е. соединить с «землёй»), на моей плате этому соответствует положение перемычки ближе к USB-порту. Посмотреть больше информации про это можно в reference manual 3.4. В зависимости от выбранной конфигурации процессор делает доступным выбранное устройство загрузки по адресу 0x0000 0000. Иными словами когда мы грузимся с флеш-памяти, то адрес 0x0800 0000 становится также доступным по адресу 0x0000 0000 (а также все остальные адреса). Далее процессор читает по адресу 0x0000 0000 4 байта и присваивает это значение регистру sp (stack pointer, вершина стека). Далее процессор читает по адресу 0x0000 0004 4 байта и присваивает это значение регистру pc (program counter), иными словами делает переход по указанному адресу. Ну и, собственно, начинает работу. Читает инструкцию, выполняет и тд.

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

Собственно для начала прочитаем по 8 байтов по адресам 0x0000 0000 и 0x0800 0000, убедимся, что они действительно совпадают, а также попробуем понять их смысл.


(gdb) x/2z 0x00000000

0x0: 0x20004ffc 0x08000131

(gdb) x/2z 0x08000000

0x8000000: 0x20004ffc 0x08000131


Команда x читает значения из памяти по указанному адресу. Строка /2z говорит о том, что мы хотим прочитать 2 слова (т.е. 4-байтовых значения) и напечатать их в 16-ричном формате.

Как видно, по обоим адресам действительно лежат одинаковые данные. Иными словами, если верить описанию выше, то после инициализации регистру sp должно было присвоиться значение 0x20004ffc, а регистру pc значение 0x08000131. Сейчас мы этом проверим, а пока попробуем чуть-чуть похулиганить. На моей плате 64 кибибайта флеша, поэтому попробуем прочитать пару слов в самом конце.


(gdb) x/2z 0x08000000 + 64 * 1024 - 4

0x800fffc: 0xffffffff Cannot access memory at address 0x8010000

(gdb)


Собственно как и ожидалось - последнее слово ещё что-то содержит, а после него уже адреса недоступны для чтения. Поэтому командой x можно исследовать что угодно без всяких опасений.

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


(gdb) print/z $sp

$1 = 0x20004ffc

(gdb) print/z $pc

$2 = 0x08000130


Для чтения значений используется команда print. Регистры доступны через синтаксис $sp, $pc, $r0 и тд. Также все регистры можно посмотреть командой info registers. Важно понимать разницу между print $sp и x $sp. Первая команда печатает значение регистра, вторая команда значение регистра интерпретирует как адрес, и печатает значение из памяти.

Собственно видно, что со значением регистра $sp всё ожидаемо - он действительно загрузился из адреса 0x0000 0000, а вот $pc оказался на единицу меньше. Тут можно много рассказывать, но это всё не очень интересно - просто запомните, что в архитектуре arm cortex адреса инструкций всегда чётные, при этом значения указателей иногда нечётные. Собственно адрес 0x0000 0004, с которого начнётся выполнение, должен быть нечётным, при этом младший бит обнулится перед переходом. Также инструкции перехода BX, BLX требуют то же самое: младший бит адреса, на который выполняется переход, должен быть равен единице, но при этом переход делается на адрес с нулевым младшим битом. А команды B и BL не требуют, вот такая особенность.

В итоге процессор перешёл на адрес 0x08000130 и остановился. На вашей плате скорей всего все эти значения будут немного другими, я «заводскую» прошивку стёр, но принцип ровно тот же. На всякий случай напомню, что по адресу 0x08000130 у нас доступна флеш-память, т.е. это код из флеш-памяти.

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


(gdb) x/i $pc

=> 0x8000130: add.w r0, r0, #1


Как видно, для дизассемблирования используется ровно та же команда x (examine). Только вместо /z используется формат /i. Формат z - это шестнадцатеричный формат, а формат i - это интерпретация числа, как инструкции. Для интереса покажу ещё несколько форматов, думаю, всё и так очевидно.


(gdb) x/z $pc

0x8000130: 0x0001f100

(gdb) x/d $pc

0x8000130: 127232

(gdb) x/x $pc

0x8000130: 0x0001f100

(gdb) x/t $pc

0x8000130: 00000000000000011111000100000000


Форматы /z и /x означают одно и то же, но в некоторых случаях /x не добавляет нули слева, а /z всегда добавляет нули.

Иными словами 4 байта 01 00 f1 00 процессором интерпретируются, как команда add.w r0, r0, #1. Эта команда аналогична псевдокоду r0 := r0 + 1.

Важно понимать, что эта команда ещё не выполнена, процессор только готовится её выполнить.

Теперь распечатаем значение регистра r0, далее выполним команду и распечатаем значение регистра ещё раз.


(gdb) p/z $r0

$6 = 0x48b503b6

(gdb) stepi

0x08000134 in ?? ()

(gdb) p/z $r0

$7 = 0x48b503b7


Как видно, в регистре r0 было какое-то непонятное значение и процессор действительно увеличил это значение в регистре r0 на единицу. После команды stepi процессор перешёл на 4 байта вперёд и готовится выполнить следующую команду. Посмотрим, что там за команда.


x/i $pc

=> 0x8000134: b.w 0x8000130


Это команда branch, команда безусловного перехода по указанному адресу. Если её выполнить, то процессор вернётся на предыдущую инструкцию. Ну собственно так и будет туда-сюда прыгать, считая от 0 до 4 миллиардов по кругу, пока не выключим питание.

В псевдокоде эта программа выглядит примерно так:


loop: r0 := r0 + 1;

goto loop;


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

На вашей плате из магазина программа будет другая. Но принципы работы те же.

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


Так же в этом разделе:
 
MyTetra Share v.0.59
Яндекс индекс цитирования