История изменений
Исправление vbr, (текущая версия) :
Итак мы подключились отладчиком к плате. В последней строчке у вас скорей всего будет другой адрес, не суть.
Теперь пару слов про то, как вообще работает этот микроконтролер. Я, конечно, буду упускать много деталей, попытаюсь передать суть.
Вообще у него всё взаимодействие со всеми устройствами ведётся через оперативную память (и прерывания). Это 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, по крайней мере мне так проще.
На вашей плате из магазина программа будет другая. Но принципы работы те же.
На этом первый пункт закончен. Мы запустили плату, подключились к ней и убедились, что там внутри действительно процессор, а не просто лампочка.
Исходная версия vbr, :
Итак мы подключились отладчиком к плате. В последней строчке у вас скорей всего будет другой адрес, не суть.
Теперь пару слов про то, как вообще работает этот микроконтролер. Я, конечно, буду упускать много деталей, попытаюсь передать суть.
Вообще у него всё взаимодействие со всеми устройствами ведётся через оперативную память (и прерывания). Это 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 требуют то же самое: младший бит адреса, на который выполняется переход, должен быть равен единице, но при этом переход делается на адрес с нулевым младшим битом.
В итоге процессор перешёл на адрес 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, по крайней мере мне так проще.
На вашей плате из магазина программа будет другая. Но принципы работы те же.
На этом первый пункт закончен. Мы запустили плату, подключились к ней и убедились, что там внутри действительно процессор, а не просто лампочка.