mirror of
https://github.com/MPSU/APS.git
synced 2025-09-15 17:20:10 +00:00
WIP: APS cumulative update (#98)
* WIP: APS cumulative update * Update How FPGA works.md * Перенос раздела "Последовательностная логика" в отдельный док * Исправление картинки * Исправление оформления индексов * Переработка раздела Vivado Basics * Добавление картинки в руководство по созданию проекта * Исправление ссылок в анализе rtl * Обновление изображения в sequential logic * Исправление ссылок в bug hunting * Исправление ссылок * Рефактор руководства по прошивке ПЛИС * Mass update * Update fig_10 * Restore fig_02
This commit is contained in:
committed by
GitHub
parent
78bb01ef95
commit
a28002e681
@@ -1,4 +1,4 @@
|
||||
# Лабораторная работа 14 "Высокоуровневое программирование"
|
||||
# Лабораторная работа №14 "Высокоуровневое программирование"
|
||||
|
||||
Благодаря абстрагированию можно создавать действительно сложные системы — из вентилей можно собрать модули, из модулей микроархитектуру и так далее. В этом контексте архитектура выступает как фундамент, на котором строится программный стек абстракций. На основе архитектур строятся ассемблеры, на основе которых "строятся" языки высокого уровня, на основе которых создаются фреймворки и метафреймворки, что обеспечивает более высокий уровень и удобство при разработке новых программ. Давайте немного глубже погрузимся в этот стек.
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
> — Но разве в процессе компиляции исходного кода на языке Си мы не получаем программу, написанную на языке ассемблера? Получится ведь тот же код, что мы могли написать и сами.
|
||||
|
||||
Штука в том, что ассемблерный код, который писали ранее вы отличается от ассемблерного кода, генерируемого компилятором. Код, написанный вами, обладал, скажем так... более тонким микро-контролем хода программы. Когда вы писали программу, вы знали какой у вас размер памяти, где в памяти расположены инструкции, а где данные (ну, при написании программ вы почти не пользовались памятью данных, а когда пользовались — просто лупили по случайным адресам и все получалось). Вы пользовались всеми регистрами регистрового файла по своему усмотрению, без ограничений. Однако, представьте на секунду, что вы пишете проект на ассемблере вместе с коллегой: вы пишите одни функции, а он другие. Как в таком случае вы будете пользоваться регистрами регистрового файла? Ведь если вы будете пользоваться одними и теми же регистрами, вызов одной функции может испортить данные в другой. Поделите его напополам и будете пользоваться каждый своей половиной? Но что будет, если к проекту присоединится еще один коллега — придется делить регистровый файл уже на три части? Так от него уже ничего не останется. Для разрешения таких ситуаций было разработано [соглашение о вызовах](#соглашение-о-вызовах) (calling convention).
|
||||
Штука в том, что ассемблерный код, который писали ранее вы отличается от ассемблерного кода, генерируемого компилятором. Код, написанный вами, обладал, скажем так... более тонким микро-контролем хода программы. Когда вы писали программу, вы знали какой у вас размер памяти, где в памяти расположены инструкции, а где данные (ну, при написании программ вы почти не пользовались памятью данных, а когда пользовались — просто лупили по случайным адресам и все получалось). Вы пользовались всеми регистрами регистрового файла по своему усмотрению, без ограничений. Однако, представьте на секунду, что вы пишете проект на ассемблере вместе с коллегой: вы пишите одни функции, а он другие. Как в таком случае вы будете пользоваться регистрами регистрового файла? Ведь если вы будете пользоваться одними и теми же регистрами, вызов одной функции может испортить данные в другой. Поделите его напополам и будете пользоваться каждый своей половиной? Но что будет, если к проекту присоединится ещё один коллега — придётся делить регистровый файл уже на три части? Так от него уже ничего не останется. Для разрешения таких ситуаций было разработано [соглашение о вызовах](#соглашение-о-вызовах) (calling convention).
|
||||
|
||||
Таким образом, генерируя ассемблерный код, компилятор не может так же, как это делали вы, использовать все ресурсы без каких-либо ограничений — он должен следовать ограничениям, накладываемым на него соглашением о вызовах, а также ограничениям, связанным с тем, что он ничего не знает о памяти устройства, в котором будет исполняться программа — а потому он не может работать с памятью абы как. Работая с памятью, компилятор следует некоторым правилам, благодаря которым после компиляции компоновщик сможет собрать программу под ваше устройство с помощью специального скрипта.
|
||||
|
||||
@@ -43,11 +43,11 @@
|
||||
|
||||
При работе с оберегаемыми регистрами, функция должна гарантировать, что перед возвратом в этих регистрах останется тоже самое значение, что было при вызове функции. То есть, если функция собирается записать что-то в оберегаемый регистр, она должна сохранить перед этим его значение на стек, а затем, перед возвратом, вернуть это значение со стека обратно в этот же регистр.
|
||||
|
||||
Простая аналогия — в маленькой квартире двое делят один рабочий стол по времени. Каждый использует стол по полной, но после себя он должен оставить половину стола соседа (оберегаемые регистры) в том же виде, в котором ее получил, а со своей (необерегаемые регистры) делает что хочет. Кстати, вещи соседа, чтоб не потерять, убирают на стопку (**stack**) рядом (в основную память).
|
||||
Простая аналогия — в маленькой квартире двое делят один рабочий стол по времени. Каждый использует стол по полной, но после себя он должен оставить половину стола соседа (оберегаемые регистры) в том же виде, в котором её получил, а со своей (необерегаемые регистры) делает что хочет. Кстати, вещи соседа, чтоб не потерять, убирают на стопку (**stack**) рядом (в основную память).
|
||||
|
||||
С необерегаемыми регистрами функция может работать как ей угодно — не существует никаких гарантий, которые вызванная функция должна исполнить. При этом, если функция вызывает другую функцию, она точно так же не получает никаких гарантий, что вызванная функция оставит значения необерегаемых регистров без изменений, поэтому если там хранятся значения, которые потребуются по окончанию выполнения вызываемой функции, эти значения необходимо сохранить на стек.
|
||||
|
||||
В таблице ниже приведено разделение регистров на оберегаемые (в правом столбце записано `Callee`, т.е. за их сохранение отвечает вызванная функция) и необерегаемые (`Caller` — за сохранение отвечает вызывающая функция). Кроме того, есть три регистра, для которых правый столбец не имеет значения: нулевой регистр (поскольку его невозможно изменить) и указатели на поток и глобальную область памяти. По соглашению о вызовах, эти регистры нельзя использовать для вычислений функций, они изменяются только по заранее оговоренным ситуациям.
|
||||
В _таблице 1_ приведено разделение регистров на оберегаемые (в правом столбце записано `Callee`, т.е. за их сохранение отвечает вызванная функция) и необерегаемые (`Caller` — за сохранение отвечает вызывающая функция). Кроме того, есть три регистра, для которых правый столбец не имеет значения: нулевой регистр (поскольку его невозможно изменить) и указатели на поток и глобальную область памяти. По соглашению о вызовах, эти регистры нельзя использовать для вычислений функций, они изменяются только по заранее оговорённым ситуациям.
|
||||
|
||||
В столбце `ABI name` записывается синоним имени регистра, связанный с его функциональным назначением (см. описание регистра). Часто ассемблеры одинаково воспринимают обе формы написания имени регистров.
|
||||
|
||||
@@ -69,9 +69,9 @@
|
||||
|
||||
_Таблица 1. Ассемблерные мнемоники для целочисленных регистров RISC-V и их назначение в соглашении о вызовах[1, стр. 6]._
|
||||
|
||||
Несмотря на то, что указатель на стек помечен как Callee-saved регистр, это не означает, что вызываемая функция может записать в него что заблагорассудится, предварительно сохранив его значение на стек. Ведь как вы вернете значение указателя на стек со стека, если в регистре указателя на стек лежит что-то не то?
|
||||
Несмотря на то, что указатель на стек помечен как Callee-saved регистр, это не означает, что вызываемая функция может записать в него что заблагорассудится, предварительно сохранив его значение на стек. Ведь как вы вернёте значение указателя на стек со стека, если в регистре указателя на стек лежит что-то не то?
|
||||
|
||||
Запись `Callee` означает, что к моменту возврата из вызываемой функции, значение Callee-saved регистров должно быть ровно таким же, каким было в момент вызова функций. Для s0-s11 регистров это осуществляется путем сохранения их значений на стек. При этом, перед каждым сохранением на стек, изменяется значение указателя на стек таким образом, чтобы он указывал на сохраняемое значение (обычно он декрементируется). Затем, перед возвратом из функции все сохраненные на стек значения восстанавливаются, попутно изменяя значение указателя на стек противоположным образом (инкрементируют его). Таким образом, несмотря на то, что значение указателя на стек менялось в процессе работы вызываемой функции, к моменту выхода из нее, его значение в итоге останется тем же.
|
||||
Запись `Callee` означает, что к моменту возврата из вызываемой функции, значение Callee-saved регистров должно быть ровно таким же, каким было в момент вызова функций. Для s0-s11 регистров это осуществляется путём сохранения их значений на стек. При этом, перед каждым сохранением на стек, изменяется значение указателя на стек таким образом, чтобы он указывал на сохраняемое значение (обычно он декрементируется). Затем, перед возвратом из функции все сохранённые на стек значения восстанавливаются, попутно изменяя значение указателя на стек противоположным образом (инкрементируют его). Таким образом, несмотря на то что значение указателя на стек менялось в процессе работы вызываемой функции, к моменту выхода из неё, его значение в итоге останется тем же.
|
||||
|
||||
### Скрипт для компоновки (linker_script.ld)
|
||||
|
||||
@@ -79,15 +79,15 @@ _Таблица 1. Ассемблерные мнемоники для целоч
|
||||
|
||||
В самом простом виде скрипт компоновки состоит из одного раздела: раздела секций, в котором вы и описываете какие части программы куда и в каком порядке необходимо разместить.
|
||||
|
||||
Для удобства этого описания существует вспомогательная переменная: счетчик адресов. Этот счетчик показывает в какое место в памяти будет размещена очередная секция (если при размещении секции в явном виде не будет указано иного). На момент начала исполнения скрипта этот счетчик равен нулю. Размещая очередную секцию, счетчик увеличивается на размер размещаемой секции. Допустим, у нас есть два файла `startup.o` и `main.o`, в каждом из которых есть секции `.text` и `.data`. Мы хотим разместить их в памяти следующим образом: сперва разместить секции `.text` обоих файлов, а затем секции `.data`.
|
||||
Для удобства этого описания существует вспомогательная переменная: счётчик адресов. Этот счётчик показывает в какое место в памяти будет размещена очередная секция (если при размещении секции в явном виде не будет указано иного). На момент начала исполнения скрипта этот счётчик равен нулю. Размещая очередную секцию, счётчик увеличивается на размер размещаемой секции. Допустим, у нас есть два файла `startup.o` и `main.o`, в каждом из которых есть секции `.text` и `.data`. Мы хотим разместить их в памяти следующим образом: сперва разместить секции `.text` обоих файлов, а затем секции `.data`.
|
||||
|
||||
В итоге начиная с нулевого адреса будет размещена секция `.text` файла `startup.o`. Она будет размещена именно там, поскольку счетчик адресов в начале скрипта равен нулю, а очередная секция размещается по адресу, куда указывает счетчик адресов. После этого, счетчик будет увеличен на размер этой секции и секция `.text` файла `main.o` будет размещена сразу же за секцией `.text` файла `startup.o`. После этого счетчик адресов будет увеличен на размер этой секции. То же самое произойдет и при размещении оставшихся секций.
|
||||
В итоге начиная с нулевого адреса будет размещена секция `.text` файла `startup.o`. Она будет размещена именно там, поскольку счётчик адресов в начале скрипта равен нулю, а очередная секция размещается по адресу, куда указывает счётчик адресов. После этого, счётчик будет увеличен на размер этой секции и секция `.text` файла `main.o` будет размещена сразу же за секцией `.text` файла `startup.o`. После этого счётчик адресов будет увеличен на размер этой секции. То же самое произойдёт и при размещении оставшихся секций.
|
||||
|
||||
Кроме того, вы в любой момент можете изменить значение счетчика адресов. Например, если адресное пространство памяти поделено на две части: под инструкции отводится 512 байт, а под данные 1024 байта. Таким образом, выделенный диапазон адресов для инструкций: `[0:511]`, а для данных: `[512:1535]`. Предположим при этом, что общий объем секций `.text` составляет 416 байт. В этом случае, вы можете сперва разместить секции `.text` так же, как было описано в предыдущем примере, а затем, выставив значение на счетчике адресов равное `512`, описать размещение секций данных. Тогда, между секциями появится разрыв в 96 байт, а данные окажутся в выделенном для них диапазоне адресов.
|
||||
|
||||
В нашей процессорной системе гарвардская архитектура. Это значит, что память инструкций и данных у нас независимы друг от друга. Это физически разные устройства, с разными шинами и разным адресным пространством. Однако обе эти памяти имеют общие значения младших адресов: самый младший имеет адрес ноль, следующий адрес 1 и т.д. Таким образом, происходит наложение адресных пространств памяти инструкций и памяти данных. Компоновщику трудно работать в таких условиях: "как я записать что по этому адресу будет размещаться секция данных, когда здесь уже размещена секция инструкций".
|
||||
|
||||
Есть два механизма для решения этого вопроса. Первый: компоновать секции инструкций и данных по отдельности. В этом случае будет два отдельных скрипта компоновщика. Однако, компоновка секций инструкций зависит от компоновки секций данных (в частности, от того по каким адресам будут размещены стек и .bss-секция, а также указатель на глобальную область данных), поскольку в часть инструкций необходимо прописать конкретные адреса. В этом случае, придется делать промежуточные операции в виде экспорта глобальных символов в отдельный объектный файл, который будет использован при компоновке секции инструкций, что кажется некоторым переусложнением.
|
||||
Есть два механизма для решения этого вопроса. Первый: компоновать секции инструкций и данных по отдельности. В этом случае будет два отдельных скрипта компоновщика. Однако, компоновка секций инструкций зависит от компоновки секций данных (в частности, от того по каким адресам будут размещены стек и .bss-секция, а также указатель на глобальную область данных), поскольку в часть инструкций необходимо прописать конкретные адреса. В этом случае, придётся делать промежуточные операции в виде экспорта глобальных символов в отдельный объектный файл, который будет использован при компоновке секции инструкций, что кажется некоторым переусложнением.
|
||||
|
||||
Вместо этого, будет использован другой подход, механизм виртуальных адресов (**Virtual Memory Address**, **VMA**) и адресов загрузки (**Load Memory Address**, **LMA**).
|
||||
|
||||
@@ -96,7 +96,7 @@ _Таблица 1. Ассемблерные мнемоники для целоч
|
||||
|
||||
Обычно LMA совпадает с VMA. Однако в некоторых случаях они могут быть и различны (например, изначально секция данных записывается в ROM, а перед выполнением программы, копируется из ROM в RAM). В этом случае, LMA — это адрес секции в ROM, а VMA — адрес секции в RAM.
|
||||
|
||||
Таким образом, мы можем сделать общие VMA (процессор, обращаясь к секциям инструкций и данных будет использовать пересекающееся адресное пространство), а конфликт размещения секций компоновщиком разрешить задав какой-нибудь заведомо большой VMA для секции данных. В последствии, мы просто проигнорируем этот адрес, проинициализировав память данных начиная с нуля.
|
||||
Таким образом, мы можем сделать общие VMA (процессор, обращаясь к секциям инструкций и данных будет использовать пересекающееся адресное пространство), а конфликт размещения секций компоновщиком разрешить, задав какой-нибудь заведомо большой VMA для секции данных. В последствии, мы просто проигнорируем этот адрес, проинициализировав память данных начиная с нуля.
|
||||
|
||||
Помимо прочего, в скрипте компоновщика необходимо прописать, каков [порядок следования байт](https://en.wikipedia.org/wiki/Endianness), где будет находиться стек, и какое будет значение у указателя на глобальную область памяти.
|
||||
|
||||
@@ -112,7 +112,7 @@ ENTRY(_start) /* мы сообщаем компоно
|
||||
|
||||
/*
|
||||
В данном разделе указывается структура памяти:
|
||||
Сперва идет регион "instr_mem", являющийся памятью с исполняемым кодом
|
||||
Сперва идёт регион "instr_mem", являющийся памятью с исполняемым кодом
|
||||
(об этом говорит аттрибут 'x'). Этот регион начинается
|
||||
с адреса 0x00000000 и занимает 1024 байта.
|
||||
Далее идет регион "data_mem", начинающийся с адреса 0x00000000 и занимающий
|
||||
@@ -150,12 +150,12 @@ SECTIONS
|
||||
|
||||
/*
|
||||
В скриптах компоновщика есть внутренняя переменная, записываемая как '.'
|
||||
Эта переменная называется "счетчиком адресов". Она хранит текущий адрес в
|
||||
Эта переменная называется "счётчиком адресов". Она хранит текущий адрес в
|
||||
памяти.
|
||||
В начале файла она инициализируется нулем. Добавляя новые секции, эта
|
||||
В начале файла она инициализируется нулём. Добавляя новые секции, эта
|
||||
переменная будет увеличиваться на размер каждой новой секции.
|
||||
Если при размещении секций не указывается никакой адрес, они будут размещены
|
||||
по текущему значению счетчика адресов.
|
||||
по текущему значению счётчика адресов.
|
||||
Этой переменной можно присваивать значения, после этого, она будет
|
||||
увеличиваться с этого значения.
|
||||
Подробнее:
|
||||
@@ -164,7 +164,7 @@ SECTIONS
|
||||
|
||||
/*
|
||||
Следующая команда сообщает, что начиная с адреса, которому в данных момент
|
||||
равен счетчик адресов (в данный момент, начиная с нуля) будет находиться
|
||||
равен счётчик адресов (в данный момент, начиная с нуля) будет находиться
|
||||
секция .text итогового файла, которая состоит из секций .boot, а также всех
|
||||
секций, начинающихся на .text во всех переданных компоновщику двоичных
|
||||
файлах.
|
||||
@@ -194,8 +194,8 @@ SECTIONS
|
||||
*/
|
||||
.data : AT (0x00800000) {
|
||||
/*
|
||||
Общепринято присваивать GP значение равное началу секции данных, смещенное
|
||||
на 2048 байт вперед.
|
||||
Общепринято присваивать GP значение равное началу секции данных, смещённое
|
||||
на 2048 байт вперёд.
|
||||
Благодаря относительной адресации со смещением в 12 бит, можно адресоваться
|
||||
на начало секции данных, а также по всему адресному пространству вплоть до
|
||||
4096 байт от начала секции данных, что сокращает объем требуемых для
|
||||
@@ -212,7 +212,7 @@ SECTIONS
|
||||
|
||||
/*
|
||||
Поскольку мы не знаем суммарный размер всех используемых секций данных,
|
||||
перед размещением других секций, необходимо выровнять счетчик адресов по
|
||||
перед размещением других секций, необходимо выровнять счётчик адресов по
|
||||
4х-байтной границе.
|
||||
*/
|
||||
. = ALIGN(4);
|
||||
@@ -222,21 +222,21 @@ SECTIONS
|
||||
BSS (block started by symbol, неофициально его расшифровывают как
|
||||
better save space) — это сегмент, в котором размещаются неинициализированные
|
||||
статические переменные. В стандарте Си сказано, что такие переменные
|
||||
инициализируются нулем (или NULL для указателей). Когда вы создаете
|
||||
статический массив — он должен быть размещен в исполняемом файле.
|
||||
инициализируются нулём (или NULL для указателей). Когда вы создаёте
|
||||
статический массив — он должен быть размещён в исполняемом файле.
|
||||
Без bss-секции, этот массив должен был бы занимать такой же объем
|
||||
исполняемого файла, какого объема он сам. Массив на 1000 байт занял бы
|
||||
исполняемого файла, какого объёма он сам. Массив на 1000 байт занял бы
|
||||
1000 байт в секции .data.
|
||||
Благодаря секции bss, начальные значения массива не задаются, вместо этого
|
||||
здесь только записываются названия переменных и их адреса.
|
||||
Однако на этапе загрузки исполняемого файла теперь необходимо принудительно
|
||||
занулить участок памяти, занимаемый bss-секцией, поскольку статические
|
||||
переменные должны быть проинициализированы нулем.
|
||||
переменные должны быть проинициализированы нулём.
|
||||
Таким образом, bss-секция значительным образом сокращает объем исполняемого
|
||||
файла (в случае использования неинициализированных статических массивов)
|
||||
ценой увеличения времени загрузки этого файла.
|
||||
Для того, чтобы занулить bss-секцию, в скрипте заводятся две переменные,
|
||||
указывающие на начало и конец bss-секции посредством счетчика адресов.
|
||||
указывающие на начало и конец bss-секции посредством счётчика адресов.
|
||||
Подробнее:
|
||||
https://en.wikipedia.org/wiki/.bss
|
||||
|
||||
@@ -254,7 +254,7 @@ SECTIONS
|
||||
/*=================================
|
||||
Секция аллоцированных данных завершена, остаток свободной памяти отводится
|
||||
под программный стек, стек прерываний и (возможно) кучу. В соглашении о
|
||||
вызовах архитектуры RISC-V сказано, что стек растет снизу вверх, поэтому
|
||||
вызовах архитектуры RISC-V сказано, что стек растёт снизу вверх, поэтому
|
||||
наша цель разместить его в самых последних адресах памяти.
|
||||
Поскольку стеков у нас два, в самом низу мы разместим стек прерываний, а
|
||||
над ним программный стек. При этом надо обеспечить защиту программного
|
||||
@@ -267,7 +267,7 @@ SECTIONS
|
||||
ASSERT(. < (LENGTH(data_mem) - _trap_stack_size - _stack_size),
|
||||
"Program size is too big")
|
||||
|
||||
/* Перемещаем счетчик адресов над стеком прерываний (чтобы после мы могли
|
||||
/* Перемещаем счётчик адресов над стеком прерываний (чтобы после мы могли
|
||||
использовать его в вызове ALIGN) */
|
||||
. = LENGTH(data_mem) - _trap_stack_size;
|
||||
|
||||
@@ -283,24 +283,25 @@ SECTIONS
|
||||
ASSERT(_stack_ptr <= LENGTH(data_mem) - _trap_stack_size,
|
||||
"SP exceed memory size")
|
||||
|
||||
/* Перемещаем счетчик адресов в конец памяти (чтобы после мы могли
|
||||
/* Перемещаем счётчик адресов в конец памяти (чтобы после мы могли
|
||||
использовать его в вызове ALIGN) */
|
||||
. = LENGTH(data_mem);
|
||||
|
||||
/*
|
||||
Обычно память имеет размер, кратный 16, но на случай, если это не так, мы
|
||||
делаем проверку, после которой мы либо остаемся в самом конце памяти (если
|
||||
делаем проверку, после которой мы либо остаёмся в самом конце памяти (если
|
||||
конец кратен 16), либо поднимаемся на 16 байт вверх от края памяти,
|
||||
округленного до 16 в сторону большего значения
|
||||
округлённого до 16 в сторону большего значения
|
||||
*/
|
||||
_trap_stack_ptr = ALIGN(16) <= LENGTH(data_mem) ? ALIGN(16) : ALIGN(16) - 16;
|
||||
ASSERT(_trap_stack_ptr <= LENGTH(data_mem), "ISP exceed memory size")
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
_Листинг 1. Пример скрипта компоновщика с комментариями._
|
||||
|
||||
Обратите внимание на указанные размеры памяти инструкций и данных. Они отличаются от размеров, которые использовались ранее в пакете `memory_pkg`. Дело в том, что пока система и исполняемые ей программы были простыми, в большом объеме памяти не было нужды и меньший размер значительно сокращал время синтеза системы. Однако в данный момент, чтобы обеспечить программе достаточно места под инструкции, а так же программный стек и стек прерываний, необходимо увеличить объемы памяти инструкций и памяти данных. Для этого необходимо обновить значения параметров `INSTR_MEM_SIZE_BYTES` и `DATA_MEM_SIZE_BYTES` на 32'd1024 и 32'd2048 соответственно. В зависимости от сложности вашего проекта, в будущем вам может снова потребоваться изменять размер памяти в вашей системе. Помните, все изменения в memory_pkg должны отражаться и в скрипте компоновщика для вашей системы.
|
||||
Обратите внимание на указанные размеры памяти инструкций и данных. Они отличаются от размеров, которые использовались ранее в пакете `memory_pkg`. Дело в том, что пока система и исполняемые ей программы были простыми, в большом объеме памяти не было нужды и меньший размер значительно сокращал время синтеза системы. Однако в данный момент, чтобы обеспечить программе достаточно места под инструкции, а также программный стек и стек прерываний, необходимо увеличить объемы памяти инструкций и памяти данных. Для этого необходимо обновить значения параметров `INSTR_MEM_SIZE_BYTES` и `DATA_MEM_SIZE_BYTES` на 32'd1024 и 32'd2048 соответственно. В зависимости от сложности вашего проекта, в будущем вам может снова потребоваться изменять размер памяти в вашей системе. Помните, все изменения в memory_pkg должны отражаться и в скрипте компоновщика для вашей системы.
|
||||
|
||||
### Файл первичных команд при загрузке (startup.S)
|
||||
|
||||
@@ -360,7 +361,7 @@ _endless_loop:
|
||||
# https://github.com/twlostow/urv-core/blob/master/sw/common/irq.S
|
||||
# Из реализации убраны сохранения нереализованных CS-регистров. Кроме того,
|
||||
# судя по документу приведенному ниже, обычное ABI подразумевает такое же
|
||||
# сохранение контекста, что и при программном вызове (EABI подразумевает еще
|
||||
# сохранение контекста, что и при программном вызове (EABI подразумевает ещё
|
||||
# меньшее сохранение контекста), поэтому нет нужды сохранять весь регистровый
|
||||
# файл.
|
||||
# Документ:
|
||||
@@ -404,7 +405,7 @@ _int_handler:
|
||||
sw t6,68(sp)
|
||||
|
||||
# Кроме того, мы сохраняем состояние регистров прерываний на случай, если
|
||||
# произойдет еще одно прерывание.
|
||||
# произойдет ещё одно прерывание.
|
||||
csrr t0,mscratch
|
||||
csrr t1,mepc
|
||||
csrr a0,mcause
|
||||
@@ -420,7 +421,7 @@ _int_handler:
|
||||
# Восстановление контекста. В первую очередь мы хотим восстановить CS-регистры,
|
||||
# на случай, если происходило вложенное прерывание. Для этого, мы должны
|
||||
# вернуть исходное значение указателя стека прерываний. Однако его нынешнее
|
||||
# значение нам еще необходимо для восстановления контекста, поэтому мы
|
||||
# значение нам ещё необходимо для восстановления контекста, поэтому мы
|
||||
# сохраним его в регистр a0, и будем восстанавливаться из него.
|
||||
mv a0,sp
|
||||
|
||||
@@ -494,7 +495,7 @@ _Листинг 2. Пример содержимого файла первичн
|
||||
|
||||
Исполняемый файл компилятора тот же самый, флаги компоновки будут следующие:
|
||||
|
||||
- `-march=rv32i_zicsr -mabi=ilp32` — те же самые флаги, что были при компиляции (нам все еще нужно указывать архитектуру, иначе компоновщик может скомпоновать объектные файлы со стандартными библиотеками от другой архитектуры)
|
||||
- `-march=rv32i_zicsr -mabi=ilp32` — те же самые флаги, что были при компиляции (нам все ещё нужно указывать архитектуру, иначе компоновщик может скомпоновать объектные файлы со стандартными библиотеками от другой архитектуры)
|
||||
- `-Wl,--gc-sections` — указать компоновщику удалять неиспользуемые секции (сокращает объем итогового файла)
|
||||
- `-nostartfiles` — указать компоновщику не использовать стартап-файлы стандартных библиотек (сокращает объем файла и устраняет ошибки компиляции из-за конфликтов с используемым стартап-файлом).
|
||||
- `-T linker_script.ld` — передать компоновщику скрипт компоновки
|
||||
@@ -550,13 +551,13 @@ FF5FF06F 04400293 FFF00313 30529073
|
||||
...
|
||||
```
|
||||
|
||||
Обратите внимание что байты не просто склеились в четверки, изменился так же и порядок следования байт. Это важно, т.к. в память данные должны лечь именно в таком (обновленном) порядке байт (см. первую строчку скрипта компоновщика). Когда-то `objcopy` содержал [баг](https://sourceware.org/bugzilla/show_bug.cgi?id=25202), из-за которого порядок следования байт не менялся. В каких-то версиях тулчейна (отличных от представленного в данной лабораторной работе) вы все еще можете столкнуться с подобным поведением.
|
||||
Обратите внимание что байты не просто склеились в четверки, изменился так же и порядок следования байт. Это важно, т.к. в память данные должны лечь именно в таком (обновленном) порядке байт (см. первую строчку скрипта компоновщика). Когда-то `objcopy` содержал [баг](https://sourceware.org/bugzilla/show_bug.cgi?id=25202), из-за которого порядок следования байт не менялся. В каких-то версиях тулчейна (отличных от представленного в данной лабораторной работе) вы все ещё можете столкнуться с подобным поведением.
|
||||
|
||||
Вернемся к первой строке: `@00000000`. Как уже говорилось, число, начинающееся с символа `@` говорит САПР, что с этого момента инициализация идет начиная с ячейки памяти, номер которой совпадает с этим числом. Когда вы будете экспортировать секции данных, первой строкой будет: `@20000000`. Так произойдет, поскольку в скрипте компоновщика сказано, указано инициализировать память данных с `0x80000000` адреса (значение которого было поделено на 4, чтобы получить номер 32-битной ячейки памяти). Это было сделано, чтобы не произошло наложения адресов памяти инструкций и памяти данных (см раздел [скрипт для компоновки](#скрипт-для-компоновки-linker_scriptld)). **Чтобы система работала корректно, эту строчку необходимо удалить.**
|
||||
|
||||
### Дизассемблирование
|
||||
|
||||
В процессе отладки лабораторной работы потребуется много раз смотреть на программный счетчик и текущую инструкцию. Довольно тяжело декодировать инструкцию самостоятельно, чтобы понять, что сейчас выполняется. Для облегчения задачи можно дизасемблировать скомпилированный файл. Полученный файл на языке ассемблера будет хранить адреса инструкций, а также их двоичное и ассемблерное представление.
|
||||
В процессе отладки лабораторной работы потребуется много раз смотреть на программный счётчик и текущую инструкцию. Довольно тяжело декодировать инструкцию самостоятельно, чтобы понять, что сейчас выполняется. Для облегчения задачи можно дизасемблировать скомпилированный файл. Полученный файл на языке ассемблера будет хранить адреса инструкций, а также их двоичное и ассемблерное представление.
|
||||
|
||||
Пример дизасемблированного файла:
|
||||
|
||||
@@ -730,8 +731,8 @@ _Листинг 4. Пример кода на C++, взаимодействую
|
||||
2. Разберите принцип взаимодействия с контрольными и статусными регистрами периферийного устройства на примере _Листинга 4_.
|
||||
3. Обновите значения параметров `INSTR_MEM_SIZE_BYTES` и `DATA_MEM_SIZE_BYTES` в пакете `memory_pkg` на 32'd1024 и 32'd2048 соответственно. Поскольку пакеты не являются модулями, вы не увидите их во вкладке `Hierarchy` окна исходников, вместо этого вы сможете найти их во вкладках `Libraries` и `Compile order`.
|
||||
4. Напишите программу для своего индивидуального задания и набора периферийных устройств на языке C или C++. В случае написания кода на C++ помните о необходимости добавления `extern "C"` перед определением функции `int_handler`.
|
||||
1. В описываемой программе обязательно должны присутствовать функции `main` и `int_handler`, т.к. в стартап-файле описаны вызовы этих функций. При необходимости, вы можете описать необходимые вам вспомогательные функции — ограничений на то что должно быть ровне две этих функции нет.
|
||||
2. Функция `main` может быть пустой — по ее завершению в стартап-файле предусмотрен бесконечный цикл, из которого процессор сможет выходить только по прерыванию.
|
||||
1. В описываемой программе обязательно должны присутствовать функции `main` и `int_handler`, т.к. в стартап-файле описаны вызовы этих функций. При необходимости, вы можете описать необходимые вам вспомогательные функции — ограничений на то, что должно быть ровне две этих функции нет.
|
||||
2. Функция `main` может быть пустой — по её завершению в стартап-файле предусмотрен бесконечный цикл, из которого процессор сможет выходить только по прерыванию.
|
||||
3. В функции `int_handler` вам необходимо считать поступившие от устройства ввода входные данные.
|
||||
4. Вам необходимо самостоятельно решить, как вы хотите построить ход работы вашей программы: будет ли ваше индивидуальное задание вычисляться всего лишь 1 раз в функции `main`, данные в которую поступят от функции `int_handler` через глобальные переменные, или же оно будет постоянно пересчитываться при каждом вызове `int_handler`.
|
||||
5. Доступ к регистрам контроля и статуса необходимо осуществлять посредством указателей на структуры, объявленные в файле [platform.h](./platform.h). В случае VGA-контроллера, доступ к областям памяти осуществляется через экземпляр структуры (а не указатель на нее), содержащий имена массивов `char_map`, `color_map` и `tiff_map`.
|
||||
@@ -746,4 +747,12 @@ _Листинг 4. Пример кода на C++, взаимодействую
|
||||
2. Для отладки во время моделирования будет удобно использовать дизасемблерный файл, ориентируясь на сигналы адреса и данных шины инструкций.
|
||||
10. Проверьте корректное исполнение программы процессором в ПЛИС.
|
||||
|
||||
---
|
||||
## Список источников:
|
||||
|
||||
1. [ISC-V ABIs Specification, Document Version 1.0', Editors Kito Cheng and Jessica
|
||||
Clarke, RISC-V International, November 2022](https://github.com/riscv-non-isa/riscv-elf-psabi-doc/releases/download/v1.0/riscv-abi.pdf);
|
||||
2. [Using LD, the GNU linker — Linker Scripts](https://home.cs.colorado.edu/~main/cs1300/doc/gnu/ld_3.html#IDX338);
|
||||
3. [Google Gropus — "gcc gp (global pointer) register"](https://groups.google.com/a/groups.riscv.org/g/sw-dev/c/60IdaZj27dY/m/s1eJMlrUAQAJ?pli=1);
|
||||
4. [Wikipedia — .bss](https://en.wikipedia.org/wiki/.bss).
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user