# Лабораторная работа №14 "Высокоуровневое программирование"

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

## Цель

В соответствии с индивидуальным заданием написать программу на языке программирования высокого уровня C, скомпилировать в машинные коды и запустить на ранее разработанном процессоре RISC-V.

## Ход работы

1. Изучить теорию:
   1. [Соглашение о вызовах](#Соглашение-о-вызовах)
   2. [Скрипт для компоновки](#Скрипт-для-компоновки-linker_scriptld)
   3. [Файл первичных команд](#Файл-первичных-команд-при-загрузке-startups)
2. [Подготовить набор инструментов для кросс-компиляции](#Практика)
3. Изучить порядок компиляции и команды, её осуществляющую:
   1. [Компиляция объектных файлов](#Компиляция-объектных-файлов)
   2. [Компоновка объектных файлов в исполняемый](#Компоновка-объектных-файлов-в-исполняемый)
   3. [Экспорт секций для инициализации памяти](#Экспорт-секций-для-инициализации-памяти)
   4. [Дизассемблирование](#Дизассемблирование)
4. [Написать и скомпилировать собственную программу](#Задание)
5. Проверить исполнение программы вашим процессором в ПЛИС

## Теория

В рамках данной лабораторной работы вы напишите полноценную программу, которая будет запущена на вашем процессоре. В процессе компиляции, вам потребуются файлы [linker_script.ld](linker_script.ld) и [startup.S](startup.S), лежащие в этой папке.

> — Но зачем мне эти файлы? Мы ведь уже делали задания по программированию на предыдущих лабораторных работах и нам не были нужны никакие дополнительные файлы.

Дело в том, что ранее вы писали небольшие программки на ассемблере. Однако, язык ассемблера архитектуры RISC-V, так же, как и любой другой RISC архитектуры, недружелюбен к программисту, поскольку изначально создавался с прицелом на то, что будут созданы компиляторы и программы будут писаться на более удобных для человека языках высокого уровня. Ранее вы писали простенькие программы, которые можно было реализовать на ассемблере, теперь же вам будет предложено написать полноценную программу на языке Си.

> —  Но разве в процессе компиляции исходного кода на языке Си мы не получаем программу, написанную на языке ассемблера? Получится ведь тот же код, что мы могли написать и сами.

Штука в том, что ассемблерный код, который писали ранее вы отличается от ассемблерного кода, генерируемого компилятором. Код, написанный вами, обладал, скажем так... более тонким микро-контролем хода программы. Когда вы писали программу, вы знали какой у вас размер памяти, где в памяти расположены инструкции, а где данные (ну, при написании программ вы почти не пользовались памятью данных, а когда пользовались — просто использовали случайные адреса и всё получалось). Вы пользовались всеми регистрами регистрового файла по своему усмотрению, без ограничений. Однако, представьте на секунду, что вы пишете проект на ассемблере вместе с коллегой: вы пишите одни функции, а он другие. Как в таком случае вы будете пользоваться регистрами регистрового файла? Ведь если вы будете пользоваться одними и теми же регистрами, вызов одной функции может испортить данные в другой. Поделите его напополам и будете пользоваться каждый своей половиной? Но что будет, если к проекту присоединится ещё один коллега — придётся делить регистровый файл уже на три части? Так от него уже ничего не останется. Для разрешения таких ситуаций было разработано [соглашение о вызовах](#Соглашение-о-вызовах) (calling convention).

Таким образом, генерируя ассемблерный код, компилятор не может так же, как это делали вы, использовать все ресурсы без каких-либо ограничений — он должен следовать ограничениям, накладываемым на него соглашением о вызовах, а также ограничениям, связанным с тем, что он ничего не знает о памяти устройства, в котором будет исполняться программа — а потому он не может работать с памятью абы как. Работая с памятью, компилятор следует некоторым правилам, благодаря которым после компиляции компоновщик сможет собрать программу под ваше устройство с помощью специального скрипта.

### Соглашение о вызовах

Соглашение о вызовах [устанавливает](https://github.com/riscv-non-isa/riscv-elf-psabi-doc/releases/download/v1.0/riscv-abi.pdf) порядок вызова функций: где размещаются аргументы при вызове функций, где находятся указатель на стек, адрес возврата и т.п.

Кроме того, соглашение делит регистры регистрового файла на две группы: оберегаемые и необерегаемые регистры.

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

Простая аналогия — в маленькой квартире двое делят один рабочий стол по времени. Каждый использует стол по полной, но после себя он должен оставить половину стола соседа (оберегаемые регистры) в том же виде, в котором её получил, а со своей (необерегаемые регистры) делает что хочет. Кстати, вещи соседа, чтоб не потерять, убирают на стопку (**stack**) рядом (в основную память).

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

В _таблице 1_ приведено разделение регистров на оберегаемые (в правом столбце записано `Callee`, т.е. за их сохранение отвечает вызванная функция) и необерегаемые (`Caller` — за сохранение отвечает вызывающая функция). Кроме того, есть три регистра, для которых правый столбец не имеет значения: нулевой регистр (поскольку его невозможно изменить) и указатели на поток и глобальную область памяти. По соглашению о вызовах, эти регистры нельзя использовать для вычислений функций, они изменяются только по заранее оговорённым ситуациям.

В столбце `ABI name` записывается синоним имени регистра, связанный с его функциональным назначением (см. описание регистра). Часто ассемблеры одинаково воспринимают обе формы написания имени регистров.

|Register|ABI Name|           Description           | Saver |
|--------|--------|---------------------------------|-------|
|   x0   |  zero  |Hard-wired zero                  |   —   |
|   x1   |  ra    |Return address                   |Caller |
|   x2   |  sp    |Stack pointer                    |Callee |
|   x3   |  gp    |Global pointer                   |   —   |
|   x4   |  tp    |Thread pointer                   |   —   |
|   x5   |  t0    |Temporary/alternate link register|Caller |
|  x6–7  |  t1–2  |Temporaries                      |Caller |
|   x8   |  s0/fp |Saved register/frame pointer     |Callee |
|   x9   |  s1    |Saved register                   |Callee |
| x10–11 |  a0–1  |Function arguments/return values |Caller |
| x12–17 |  a2–7  |Function arguments               |Caller |
| x18–27 |  s2–11 |Saved registers                  |Callee |
| x28–31 |  t3–6  |Temporaries                      |Caller |

_Таблица 1. Ассемблерные мнемоники для целочисленных регистров RISC-V и их назначение в соглашении о вызовах[1, стр. 6]._

Несмотря на то, что указатель на стек помечен как Callee-saved регистр, это не означает, что вызываемая функция может записать в него что заблагорассудится, предварительно сохранив его значение на стек. Ведь как вы вернёте значение указателя на стек со стека, если в регистре указателя на стек лежит что-то не то?

Запись `Callee` означает, что к моменту возврата из вызываемой функции, значение Callee-saved регистров должно быть ровно таким же, каким было в момент вызова функций. Для s0-s11 регистров это осуществляется путём сохранения их значений на стек. При этом, перед каждым сохранением на стек, изменяется значение указателя на стек таким образом, чтобы он указывал на сохраняемое значение (обычно он декрементируется). Затем, перед возвратом из функции все сохранённые на стек значения восстанавливаются, попутно изменяя значение указателя на стек противоположным образом (инкрементируют его). Таким образом, несмотря на, то что значение указателя на стек менялось в процессе работы вызываемой функции, к моменту выхода из неё, его значение в итоге останется тем же.

### Скрипт для компоновки (linker_script.ld)

Скрипт для компоновки описывает то, как в вашей памяти будут храниться данные. Вы уже могли слышать о том, что исполняемый файл содержит секции `.text` и `.data` — инструкций и данных соответственно. Компоновщик (**linker**) ничего не знает о том, какая у вас структура памяти: принстонская у вас архитектура или гарвардская, по каким адресам у вас должны храниться инструкции, а по каким данные, какой в памяти используется порядок следования байт (**endianess**). У вас может быть несколько типов памятей под особые секции — и обо всем этом компоновщику можно сообщить в скрипте для компоновки.

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

Для удобства этого описания существует вспомогательная переменная: счётчик адресов. Этот счётчик показывает в какое место в памяти будет размещена очередная секция (если при размещении секции в явном виде не будет указано иного). На момент начала исполнения скрипта этот счётчик равен нулю. Размещая очередную секцию, счётчик увеличивается на размер размещаемой секции. Допустим, у нас есть два файла `startup.o` и `main.o`, в каждом из которых есть секции `.text` и `.data`. Мы хотим разместить их в памяти следующим образом: сперва разместить секции `.text` обоих файлов, а затем секции `.data`.

В итоге начиная с нулевого адреса будет размещена секция `.text` файла `startup.o`. Она будет размещена именно там, поскольку счётчик адресов в начале скрипта равен нулю, а очередная секция размещается по адресу, куда указывает счётчик адресов. После этого, счётчик будет увеличен на размер этой секции и секция `.text` файла `main.o` будет размещена сразу же за секцией `.text` файла `startup.o`. После этого счётчик адресов будет увеличен на размер этой секции. То же самое произойдёт и при размещении оставшихся секций.

Кроме того, вы в любой момент можете изменить значение счетчика адресов. Например, если адресное пространство памяти поделено на две части: под инструкции отводится 512 байт, а под данные 1024 байта. Таким образом, выделенный диапазон адресов для инструкций: `[0:511]`, а для данных: `[512:1535]`. Предположим при этом, что общий объем секций `.text` составляет 416 байт. В этом случае, вы можете сперва разместить секции `.text` так же, как было описано в предыдущем примере, а затем, выставив значение на счетчике адресов равное `512`, описать размещение секций данных. Тогда, между секциями появится разрыв в 96 байт, а данные окажутся в выделенном для них диапазоне адресов.

В нашей процессорной системе гарвардская архитектура. Это значит, что память инструкций и данных у нас независимы друг от друга. Это физически разные устройства, с разными шинами и разным адресным пространством. Однако обе эти памяти имеют общие значения младших адресов: самый младший имеет адрес ноль, следующий адрес 1 и т.д. Таким образом, происходит наложение адресных пространств памяти инструкций и памяти данных. Компоновщику трудно работать в таких условиях: "как я могу разместить по этому адресу секцию данных, когда здесь уже размещена секция инструкций".

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

Вместо этого, будет использован другой подход, механизм виртуальных адресов (**Virtual Memory Address**, **VMA**) и адресов загрузки (**Load Memory Address**, **LMA**).

- VMA — это адрес, по которому секция будет доступна во время выполнения программы. По этому адресу процессор будет обращаться к секции.
- LMA — это адрес, по которому секция будет загружена в память при старте программы.

Обычно LMA совпадает с VMA. Однако в некоторых случаях они могут быть и различны (например, изначально секция данных записывается в ROM, а перед выполнением программы, копируется из ROM в RAM). В этом случае, LMA — это адрес секции в ROM, а VMA — адрес секции в RAM.

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

Помимо прочего, в скрипте компоновщика необходимо прописать, каков [порядок следования байт](https://en.wikipedia.org/wiki/Endianness), где будет находиться стек, и какое будет значение у указателя на глобальную область памяти.

Всё это с подробными комментариями описано в файле `linker_script.ld`.

```ld
OUTPUT_FORMAT("elf32-littleriscv")      /* Указываем порядок следования байт */

ENTRY(_start)                           /* мы сообщаем компоновщику, что первая
                                           исполняемая процессором инструкция
                                           находится у метки "_start"
                                        */

/*
  В данном разделе указывается структура памяти:
  Сперва идёт регион "instr_mem", являющийся памятью с исполняемым кодом
  (об этом говорит аттрибут 'x'). Этот регион начинается
  с адреса 0x00000000 и занимает 1024 байта.
  Далее идет регион "data_mem", начинающийся с адреса 0x00000000 и занимающий
  2048 байт. Этот регион является памятью, противоположной региону "instr_mem"
  (в том смысле, что это не память с исполняемым кодом).
*/
MEMORY
{
  instr_mem (x) : ORIGIN = 0x00000000, LENGTH = 1K
  data_mem (!x) : ORIGIN = 0x00000000, LENGTH = 2K
}

_trap_stack_size = 640;                /* Размер стека обработчика перехватов.
                                          Данный размер позволяет выполнить
                                          до 8 вложенных вызовов при обработке
                                          перехватов.
                                        */

_stack_size = 640;                     /* Размер программного стека.
                                          Данный размер позволяет выполнить
                                          от 8 вложенных вызовов.
                                       */

/*
  В данном разделе описывается размещение программы в памяти.
  Программа разделяется на различные секции:
  - секции исполняемого кода программа;
  - секции статических переменных и массивов, значение которых должно быть
    "вшито" в программу;
  и т.п.
*/

SECTIONS
{

  /*
    В скриптах компоновщика есть внутренняя переменная, записываемая как '.'
    Эта переменная называется "счётчиком адресов". Она хранит текущий адрес в
    памяти.
    В начале файла она инициализируется нулём. Добавляя новые секции, эта
    переменная будет увеличиваться на размер каждой новой секции.
    Если при размещении секций не указывается никакой адрес, они будут размещены
    по текущему значению счётчика адресов.
    Этой переменной можно присваивать значения, после этого, она будет
    увеличиваться с этого значения.
    Подробнее:
      https://home.cs.colorado.edu/~main/cs1300/doc/gnu/ld_3.html#IDX338
  */

  /*
    Следующая команда сообщает, что начиная с адреса, которому в данных момент
    равен счётчик адресов (в данный момент, начиная с нуля) будет находиться
    секция .text итогового файла, которая состоит из секций .boot, а также всех
    секций, начинающихся на .text во всех переданных компоновщику двоичных
    файлах.
    Дополнительно мы указываем, что данная секция должна быть размещена в
    регионе "instr_mem".
  */
  .text : {
    PROVIDE(_start = .);
    *(.boot)
    *(.text*)
  } > instr_mem


  /*
  Секция данных размещается аналогично секции инструкций за исключением
  адреса загрузки в памяти (Load Memory Address, LMA). Поскольку память
  инструкций и данных физически разделены, у них есть пересекающееся адресное
  пространство, которое мы бы хотели использовать (поэтому в разделе MEMORY мы
  указали что стартовые адреса обоих памятей равны нулю). Однако компоновщику
  это не нравится, ведь как он будет размещать две разные секции в одно и то же
  место. Поэтому мы ему сообщаем, с помощью оператора "AT", что загружать секцию
  данных нужно на самом деле не по нулевому адресу, а по какому-то другому,
  заведомо большему чем размер памяти инструкций, но процессор будет
  использовать адреса, начинающиеся с нуля. Такой вариант компоновщика
  устраивает и он собирает исполняемый файл без ошибок. Наша же задача,
  загрузить итоговую секцию данных по нулевым адресам памяти данных.
  */
  .data : AT (0x00800000) {
    /*
    Общепринято присваивать GP значение равное началу секции данных, смещённое
    на 2048 байт вперёд.
    Благодаря относительной адресации со смещением в 12 бит, можно адресоваться
    на начало секции данных, а также по всему адресному пространству вплоть до
    4096 байт от начала секции данных, что сокращает объем требуемых для
    адресации инструкций (практически не используются операции LUI, поскольку GP
    уже хранит базовый адрес и нужно только смещение).
    Подробнее:
      https://groups.google.com/a/groups.riscv.org/g/sw-dev/c/60IdaZj27dY/m/s1eJMlrUAQAJ
    */
    _gbl_ptr = . + 2048;
    *(.*data*)
    *(.sdata*)
  } > data_mem


  /*
    Поскольку мы не знаем суммарный размер всех используемых секций данных,
    перед размещением других секций, необходимо выровнять счётчик адресов по
    4х-байтной границе.
  */
  . = ALIGN(4);


  /*
    BSS (block started by symbol, неофициально его расшифровывают как
    better save space) — это сегмент, в котором размещаются неинициализированные
    статические переменные. В стандарте Си сказано, что такие переменные
    инициализируются нулём (или NULL для указателей). Когда вы создаёте
    статический массив — он должен быть размещён в исполняемом файле.
    Без bss-секции, этот массив должен был бы занимать такой же объем
    исполняемого файла, какого объёма он сам. Массив на 1000 байт занял бы
    1000 байт в секции .data.
    Благодаря секции bss, начальные значения массива не задаются, вместо этого
    здесь только записываются названия переменных и их адреса.
    Однако на этапе загрузки исполняемого файла теперь необходимо принудительно
    занулить участок памяти, занимаемый bss-секцией, поскольку статические
    переменные должны быть проинициализированы нулём.
    Таким образом, bss-секция значительным образом сокращает объем исполняемого
    файла (в случае использования неинициализированных статических массивов)
    ценой увеличения времени загрузки этого файла.
    Для того чтобы занулить bss-секцию, в скрипте заводятся две переменные,
    указывающие на начало и конец bss-секции посредством счётчика адресов.
    Подробнее:
      https://en.wikipedia.org/wiki/.bss

    Дополнительно мы указываем, что данная секция должна быть размещена в
    регионе "data_mem".
  */
  _bss_start = .;
  .bss : {
    *(.bss*)
    *(.sbss*)
  } > data_mem
  _bss_end = .;


  /*=================================
      Секция аллоцированных данных завершена, остаток свободной памяти отводится
      под программный стек, стек прерываний и (возможно) кучу. В соглашении о
      вызовах архитектуры RISC-V сказано, что стек растёт снизу вверх, поэтому
      наша цель разместить его в самых последних адресах памяти.
      Поскольку стеков у нас два, в самом низу мы разместим стек прерываний, а
      над ним программный стек. При этом надо обеспечить защиту программного
      стека от наложения на него стека прерываний.
      Однако перед этим, мы должны убедиться, что под оба стека хватит места.
    =================================
  */

  /* Мы хотим гарантировать, что под стек останется место */
  ASSERT(. < (LENGTH(data_mem) - _trap_stack_size - _stack_size),
            "Program size is too big")

  /*  Перемещаем счётчик адресов над стеком прерываний (чтобы после мы могли
      использовать его в вызове ALIGN) */
  . = LENGTH(data_mem) - _trap_stack_size;

  /*
      Размещаем указатель программного стека так близко к границе стека
      прерываний, насколько можно с учетом требования о выравнивании адреса
      стека до 16 байт.
      Подробнее:
        https://riscv.org/wp-content/uploads/2015/01/riscv-calling.pdf
  */
  _stack_ptr = ALIGN(16) <= LENGTH(data_mem) - _trap_stack_size?
                ALIGN(16) : ALIGN(16) - 16;
  ASSERT(_stack_ptr <= LENGTH(data_mem) - _trap_stack_size,
            "SP exceed memory size")

  /*  Перемещаем счётчик адресов в конец памяти (чтобы после мы могли
      использовать его в вызове ALIGN) */
  . = LENGTH(data_mem);

  /*
      Обычно память имеет размер, кратный 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. Пример скрипта компоновщика с комментариями._

> [!IMPORTANT]
> Обратите внимание на указанные размеры памяти инструкций и данных. Они отличаются от размеров, которые использовались ранее в пакете `memory_pkg`. Дело в том, что пока система и исполняемые ей программы были простыми, в большом объеме памяти не было нужды и меньший размер значительно сокращал время синтеза системы. Однако в данный момент, чтобы обеспечить программе достаточно места под инструкции, а также программный стек и стек прерываний, необходимо увеличить объемы памяти инструкций и памяти данных. Для этого необходимо обновить значения параметров `INSTR_MEM_SIZE_BYTES` и `DATA_MEM_SIZE_BYTES` на 32'd1024 и 32'd2048 соответственно. В зависимости от сложности вашего проекта, в будущем вам может снова потребоваться изменять размер памяти в вашей системе. Помните, все изменения в memory_pkg должны отражаться и в скрипте компоновщика для вашей системы.

### Файл первичных команд при загрузке (startup.S)

В стартап-файле хранятся инструкции, которые обязательно необходимо выполнить перед началом исполнения любой программы. Это инициализация регистров указателей на стек и глобальную область данных, контрольных регистров системы прерывания, инициализация .bss-секции и т.п.

По завершению инициализации, стартап-файл выполняет процедуру передачи управления точке входа в запускаемую программу.

```asm
  .section    .boot

 .global _start
_start:
  la    gp, _gbl_ptr     # Инициализация глобального указателя
  la    sp, _stack_ptr   # Инициализация указателя на стек

# Инициализация (зануление) сегмента bss
  la    t0, _bss_start
  la    t1, _bss_end
_bss_init_loop:
  blt   t1, t0, _irq_config
  sw    zero, 0(t0)
  addi  t0, t0, 4
  j     _bss_init_loop

# Настройка вектора (mtvec) и маски (mie) прерываний, а также указателя на стек
# прерываний (mscratch).
_irq_config:
  la    t0, _int_handler
  li    t1, -1 # -1 (все биты равны 1) означает, что разрешены все прерывания
  la    t2, _trap_stack_ptr
  csrw  mtvec, t0
  csrw  mscratch, t2
  csrw  mie, t1

# Вызов функции main
_main_call:
  li    a0, 0 # Передача аргументов argc и argv в main. Формально, argc должен
  li    a1, 0 # быть больше нуля, а argv должен указывать на массив строк,
              # нулевой элемент которого является именем исполняемого файла,
              # Но для простоты реализации оба аргумента всего лишь обнулены.
              # Это сделано для детерминированного поведения программы в случае,
              # если программист будет пытаться использовать эти аргументы.

  # Вызов main.
  # Для того чтобы программа скомпоновалась, где-то должна быть описана
  # функция именно с таким именем.
  call  main
# Зацикливание после выхода из функции main
_endless_loop:
  j     _endless_loop

# Низкоуровневый обработчик прерывания отвечает за:
#   * Сохранение и восстановление контекста;
#   * Вызов высокоуровневого обработчика с передачей id источника прерывания в
#     качестве аргумента.
# В основе кода лежит обработчик из репозитория urv-core:
# https://github.com/twlostow/urv-core/blob/master/sw/common/irq.S
# Из реализации убраны сохранения нереализованных CS-регистров. Кроме того,
# в реализации сохраняются только необерегаемые регистры регистрового файла.
# Это сделано по причине того, что при вызове высокоуровневого обработчика
# прерываний, тот будет обязан сохранить оберегаемые регистры в соответствии
# с соглашением о вызовах.
_int_handler:
  # Данная операция меняет местами регистры sp и mscratch.
  # В итоге указатель на стек прерываний оказывается в регистре sp, а вершина
  # программного стека оказывается в регистре mscratch.
  csrrw sp, mscratch,sp

  # Далее мы поднимаемся по стеку прерываний и сохраняем все регистры.
  addi  sp, sp, -80  # Указатель на стек должен быть выровнен до 16 байт, поэтому
                     # поднимаемся вверх не на 76, а на 80.
  sw    ra, 4(sp)
  # Мы хотим убедиться, что очередное прерывание не наложит стек прерываний на
  # программный стек, поэтому записываем в освободившийся регистр низ
  # программного стека, и проверяем что приподнятый указатель на верхушку
  # стека прерываний не залез в программный стек.
  # В случае, если это произошло (произошло переполнение стека прерываний),
  # мы хотим остановить работу процессора, чтобы не потерять данные, которые
  # могут помочь нам в отладке этой ситуации.
  la    ra, _stack_ptr
  blt   sp, ra, _endless_loop

  sw    t0, 12(sp) # Мы перепрыгнули через смещение 8, поскольку там должен
                   # лежать регистр sp, который ранее сохранили в mscratch.
                   # Мы запишем его на стек чуть позже.
  sw    t1, 16(sp)
  sw    t2, 20(sp)
  sw    a0, 24(sp)
  sw    a1, 28(sp)
  sw    a2, 32(sp)
  sw    a3, 36(sp)
  sw    a4, 40(sp)
  sw    a5, 44(sp)
  sw    a6, 48(sp)
  sw    a7, 52(sp)
  sw    t3, 56(sp)
  sw    t4, 60(sp)
  sw    t5, 64(sp)
  sw    t6, 68(sp)

  # Кроме того, мы сохраняем состояние регистров прерываний на случай, если
  # произойдет ещё одно прерывание.
  csrr  t0, mscratch
  csrr  t1, mepc
  csrr  a0, mcause
  sw    t0, 8(sp)
  sw    t1, 72(sp)
  sw    a0, 76(sp)

  # Вызов высокоуровневого обработчика прерываний.
  # Для того чтобы программа скомпоновалась, где-то должна быть описана
  # функция именно с таким именем.
  call  int_handler

  # Восстановление контекста. В первую очередь мы хотим восстановить CS-регистры,
  # на случай, если происходило вложенное прерывание. Для этого, мы должны
  # вернуть исходное значение указателя стека прерываний. Однако его нынешнее
  # значение нам ещё необходимо для восстановления контекста, поэтому мы
  # сохраним его в регистр a0, и будем восстанавливаться из него.
  mv    a0,sp

  lw    t1, 72(a0)
  lw    t2, 76(a0)
  addi  sp, sp, 80
  csrw  mscratch, sp
  csrw  mepc, t1
  csrw  mcause, t2
  lw    ra, 4(a0)
  lw    sp, 8(a0)
  lw    t0, 12(a0)
  lw    t1, 16(a0)
  lw    t2, 20(a0)
  lw    a1, 28(a0)   # Мы пропустили a0, потому что сейчас он используется в
                     # качестве указателя на верхушку стека и не может быть
                     # восстановлен.
  lw    a2, 32(a0)
  lw    a3, 36(a0)
  lw    a4, 40(a0)
  lw    a5, 44(a0)
  lw    a6, 48(a0)
  lw    a7, 52(a0)
  lw    t3, 56(a0)
  lw    t4, 60(a0)
  lw    t5, 64(a0)
  lw    t6, 68(a0)
  lw    a0, 24(a0)

  # Выход из обработчика прерывания
  mret


```

_Листинг 2. Пример содержимого файла первичных команд с поясняющими комментариями._

> [!IMPORTANT]
> Обратите внимание на строки `call  main` и `call  int_handler`. Компоновка объектного файла, полученного после компиляции `startup.S` будет успешной только в том случае, если в других компонуемых файлах будут функции именно с такими именами.

## Практика

Для того чтобы запустить моделирование исполнения программы на вашем процессоре, сперва эту программу необходимо скомпилировать и преобразовать в текстовый файл, которым САПР сможет проинициализировать память процессора. Для компиляции программы, вам потребуется особый компилятор, который называется "кросскомпилятор". Он позволяет компилировать исходный код под архитектуру компьютера, отличную от компьютера, на котором ведется компиляция. В нашем случае, вы будете собирать код под архитектуру `RISC-V` на компьютере с архитектурой `x86_64`.

Компилятор, который подойдет для данной задачи должен быть установлен в учебной аудитории. Но если что, вы можете скачать его [отсюда](https://github.com/xpack-dev-tools/riscv-none-elf-gcc-xpack/releases/download/v13.2.0-1/xpack-riscv-none-elf-gcc-13.2.0-1-win32-x64.zip) (обратите внимание, что размер архива составляет ~550 МБ, попытка скачивания этого архива из учебной аудитории может потратить вашу месячную квоту интернет-трафика).

### Компиляция объектных файлов

В первую очередь необходимо скомпилировать файлы с исходным кодом в объектные. Это можно сделать следующей командой:

```text
<исполняемый файл компилятора> -с <флаги компиляции> <входной файл с исходным кодом> -o <выходной объектный файл>
```

Вам потребуются следующие флаги компиляции:

- `-march=rv32i_zicsr` — указание разрядности и набора расширений в архитектуре, под которую идет компиляция (у нас процессор rv32i, расширенный набором инструкций для взаимодействия с регистрами контроля и статуса Zicsr)
- `-mabi=ilp32`  — указание двоичного интерфейса приложений. Здесь сказано, что типы `int`, `long` и `pointer` являются 32-разрядными.

Есть очень [хорошее видео](https://youtu.be/29iNHEhHmd0?t=141), описывающее состав тулчейнов, именование исполняемых файлов компиляторов, как формируются ключи архитектуры и двоичного интерфейса приложений.

С учетом названия исполняемого файла скачанного вами компилятора (при условии, что папку из архива вы переименовали в `riscv_cc` и скопировали в корень диска `C:`, а команду запускаете из оболочки `git bash`), командой для компиляции файла [`startup.S`](startup.S) может быть:

```bash
/c/riscv_cc/bin/riscv-none-elf-gcc -c -march=rv32i_zicsr -mabi=ilp32 startup.S -o startup.o
```

### Компоновка объектных файлов в исполняемый

Далее необходимо выполнить компоновку объектных файлов. Это можно сделать командой следующего формата:

```text
<исполняемый файл компилятора> <флаги компоновки> <входные объектные файлы> -o <выходной объектный файл>
```

Исполняемый файл компилятора тот же самый, флаги компоновки будут следующие:

- `-march=rv32i_zicsr -mabi=ilp32` — те же самые флаги, что были при компиляции (нам все ещё нужно указывать архитектуру, иначе компоновщик может скомпоновать объектные файлы со стандартными библиотеками от другой архитектуры)
- `-Wl,--gc-sections` — указать компоновщику удалять неиспользуемые секции (сокращает объем итогового файла). Обратите внимание, что **после запятой не должно быть пробела** — это важно!
- `-nostartfiles` — указать компоновщику не использовать стартап-файлы стандартных библиотек (сокращает объем файла и устраняет ошибки компиляции из-за конфликтов с используемым стартап-файлом).
- `-T linker_script.ld` — передать компоновщику скрипт компоновки

Пример команды компоновки:

```bash
/c/riscv_cc/bin/riscv-none-elf-gcc -march=rv32i_zicsr -mabi=ilp32 -Wl,--gc-sections -nostartfiles -T linker_script.ld startup.o main.o -o result.elf
```

### Экспорт секций для инициализации памяти

В результате компоновки вы получите исполняемый файл формата `elf` ([Executable and Linkable Format](https://ru.wikipedia.org/wiki/Executable_and_Linkable_Format)). Это двоичный файл, однако это не просто набор двоичных инструкций и данных, которые будут загружены в память процессора. Данный файл содержит заголовки и специальную информацию, которая поможет загрузчику разместить этот файл в памяти компьютера. Поскольку роль загрузчика будете выполнять вы и САПР, на котором будет вестись моделирование, эта информация вам не понадобятся, поэтому вам потребуется экспортировать из данного файла только двоичные инструкции и данные, отбросив всю остальную информацию. Полученный файл уже можно будет использовать в функции `$readmemh`.

Для экспорта используйте команду:

```bash
/c/riscv_cc/bin/riscv-none-elf-objcopy -O verilog result.elf init.mem
```

ключ `-O verilog` говорит о том, что файл надо сохранить в формате, который сможет воспринять команда `$readmemh`.

Поскольку память инструкций и данных у вас разделены, можно экспортировать отдельные секции в разные файлы:

```bash
/c/riscv_cc/bin/riscv-none-elf-objcopy -O verilog -j .text result.elf init_instr.mem
/c/riscv_cc/bin/riscv-none-elf-objcopy -O verilog -j .data -j .bss -j .sdata result.elf init_data.mem
```

Обратите внимание на содержимое полученного файла:

```text
@00000000
97 11 00 00 93 81 01 AD 13 01 00 76 93 02 00 2D
13 03 00 2D 63 88 62 00 23 A0 02 00 93 82 42 00
6F F0 5F FF 93 02 40 04 13 03 F0 FF 73 90 52 30
73 10 43 30 13 05 00 00 93 05 00 00 EF 00 C0 1F
6F 00 00 00 73 11 01 34 13 01 01 FB 23 22 11 00
...
```

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

Для того чтобы итоговый файл подходил для памяти с 32-разрядными ячейками, команду экспорта необходимо дополнить опцией `--verilog-data-width=4`, которая указывает размер ячейки инициализируемой памяти в байтах. Файл примет следующий вид:

```text
@00000000
00001197 AD018193 76000113 2D000293
2D000313 00628863 0002A023 00428293
FF5FF06F 04400293 FFF00313 30529073
30431073 00000513 00000593 1FC000EF
0000006F 34011173 FB010113 00112223
...
```

Обратите внимание что байты не просто склеились в четверки, изменился так же и порядок следования байт. Это важно, т.к. в память данные должны лечь именно в таком (обновленном) порядке байт (см. первую строчку скрипта компоновщика). Когда-то `objcopy` содержал [баг](https://sourceware.org/bugzilla/show_bug.cgi?id=25202), из-за которого порядок следования байт не менялся. В каких-то версиях тулчейна (отличных от представленного в данной лабораторной работе) вы всё ещё можете столкнуться с подобным поведением.

Вернемся к первой строке: `@00000000`. Как уже говорилось, число, начинающееся с символа `@` говорит САПР, что с этого момента инициализация идет начиная с ячейки памяти, номер которой совпадает с этим числом. Когда вы будете экспортировать секции данных, первой строкой будет: `@20000000`. Так произойдет, поскольку в скрипте компоновщика сказано инициализировать память данных с `0x80000000` адреса (значение которого было поделено на 4, чтобы получить номер 32-битной ячейки памяти; когда-то в objcopy был ещё один [баг](https://sourceware.org/bugzilla/show_bug.cgi?id=27214), в результате которого деление на 4 не производилось). Это было сделано, чтобы не произошло наложения адресов памяти инструкций и памяти данных (см параграф [скрипт для компоновки](#Скрипт-для-компоновки-linker_scriptld)). **Чтобы система работала корректно, эту строчку необходимо удалить.**

### Дизассемблирование

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

Пример дизасемблированного файла:

```asm
Disassembly of section .text:

00000000 <_start>:
   0: 00001197           auipc gp,0x1
   4: adc18193           addi gp,gp,-1316 # adc <_gbl_ptr>
   8: 76000113           li sp,1888
   c: 2dc00293           li t0,732
  10: 2dc00313           li t1,732

00000014 <_bss_init_loop>:
  14: 00628863           beq t0,t1,24 <_irq_config>
  18: 0002a023           sw zero,0(t0)
  1c: 00428293           addi t0,t0,4
...

00000164 <bubble_sort>:
 164: fd010113           addi sp,sp,-48
 168: 02112623           sw ra,44(sp)
 16c: 02812423           sw s0,40(sp)
 170: 03010413           addi s0,sp,48
 174: fca42e23           sw a0,-36(s0)
 178: fcb42c23           sw a1,-40(s0)
 17c: fe042623           sw zero,-20(s0)
 180: 09c0006f           j 21c <bubble_sort+0xb8>
...

00000244 <main>:
 244: ff010113           addi sp,sp,-16
 248: 00112623           sw ra,12(sp)
 24c: 00812423           sw s0,8(sp)
 250: 01010413           addi s0,sp,16
 254: 00a00593           li a1,10
 258: 2b400513           li a0,692
 25c: f09ff0ef           jal ra,164 <bubble_sort>
 260: 2b400793           li a5,692
...

Disassembly of section .data:

000002b4 <array_to_sort>:
 2b4: 00000003           lb zero,0(zero) # 0 <_start>
 2b8: 0005                 c.nop 1
 2ba: 0000                 unimp
 2bc: 0010                 0x10
 2be: 0000                 unimp
...
```

_Листинг 3. Пример дизасемблированного файла._

Числа в самом левом столбце, увеличивающиеся на 4 — это адреса в памяти. Отлаживая программу на временной диаграмме, вы можете ориентироваться на эти числа, как на значения PC.

Следующая за адресом строка, записанная в шестнадцатеричном виде — это та инструкция (или данные), которая размещена по этому адресу. С помощью этого столбца вы можете проверить, что считанная инструкция на временной диаграмме (сигнал `instr`) корректна.

В правом столбце находится ассемблерный (человекочитаемый) аналог инструкции из предыдущего столбца. Например, инструкция `00001197` — это операция `auipc gp,0x1`, где `gp` — это синоним (ABI name) регистра `x3` (см. параграф [Соглашение о вызовах](#Соглашение-о-вызовах)).

Обратите внимание на последнюю часть листинга: дизасм секции `.data`. В этой секции адреса могут увеличиваться на любое число, шестнадцатеричные данные могут быть любого размера, а на ассемблерные инструкции в правом столбце и вовсе не надо обращать внимание.

Дело в том, что дизасемблер пытается декодировать вообще все двоичные данные, которые видит: не делая различий инструкции это или нет. В итоге, если у него получается как-то декодировать байты из секции данных (которые могут быть абсолютно любыми) — он это сделает. Причем получившиеся инструкции могут быть из совершенно не поддерживаемых текущим файлом расширений: сжатыми (по два байта вместо четырех), инструкциями операций над числами с плавающей точкой, атомарными и т.п.

Это не значит, что секция данных в дизасме бесполезна — в приведенном выше листинге вы можете понять, что первыми элементами массива `array_to_sort` являются числа `3`, `5`, `10`, а также то, по каким адресам они лежат (`0x2b4`, `0x2b8`, `0x2bc`, если непонятно почему первое число записано в одну 4-байтовую строку, а два других разделены на две двубайтовые — попробуйте перечитать предыдущий абзац). Просто разбирая дизасемблерный файл, обращайте внимание на то, какую именно секцию вы сейчас читаете.

Для того чтобы произвести дизасемблирование, необходимо выполнить следующую команду:

```text
[исполняемый файл дизасемблера] -D (либо -d) [входной исполняемый файл] > [выходной файл на языке ассемблер]
```

Для нашего примера, командной будет

```bash
/c/riscv_cc/bin/riscv-none-elf-objdump -D result.elf > disasmed_result.S
```

Опция `-D` говорит, что дизасемблировать необходимо вообще все секции. Опция `-d` позволяет дизасемблировать только исполняемые секции (секции с инструкциями). Таким образом, выполнив дизасемблирование с опцией `-d` мы избавимся от проблем с непонятными инструкциями, в которые декодировались данные из секции `.data`, однако в этом случае, мы не сможем проверить адреса и значения, которые хранятся в этих секциях.

---

## Задание

Написать программу для вашего [индивидуального задания](../04.%20Primitive%20programmable%20device/Индивидуальное%20задание#Индивидуальные-задания) к 4-ой лабораторной работе на языке C или C++ (в зависимости от выбранного языка необходимо использовать соответствующий компилятор: gcc для C, g++ для C++).

Для того чтобы ваша программа собралась, необходимо описать две функции: `main` и `int_handler`. Аргументы и возвращаемые значения могут быть любыми, но использоваться они не смогут. Функция `main` будет вызвана в начале работы программы (после исполнения .boot-секции startup-файла), функция `int_handler` будет вызываться автоматически каждый раз, когда ваш контроллер устройства ввода будет генерировать запрос прерывания (если процессор закончил обрабатывать предыдущий запрос).

Таким образом, минимальный алгоритм работы заключается в том, чтобы считать по прерыванию данные от устройства ввода (в индивидуальном задании обозначалось как sw_i), выполнить обработку из вашего варианта, и записать результат в устройство вывода. При этом необходимо помнить о следующем:

- При вводе данных с клавиатуры, отправляется скан-код клавиши, а не значение нажатой цифры (и не ascii-код нажатой буквы). Более того, при отпускании клавиши, генерируется скан-код `F0`, за которым следует повторная отправка скан-кода этой клавиши.
- Работая с uart через программу Putty, вы отправляете ascii-код вводимого символа.

Для этих двух устройств ввода, вам необходимо продумать протокол, по которому вы будете вводить числа в вашу программу. В простейшем случае можно обрабатывать данные "как есть". Т.е. в случае клавиатуры, нажатие на клавишу `1` в верхнем горизонтальном ряду на клавиатуры со скан-кодом 0x16 интерпретировать как число `0x16`. А в случае отправки по uart символа `1` с ascii-кодом `0x31` интерпретировать его как `0x31`. Однако вывод в Putty осуществляется в виде символов принятого ascii-кода, поэтому высок риск получить непечатный символ.

Функция main может быть как пустой, содержать один лишь оператор return или бесконечный цикл — ход работы в любом случае не сломается, т.к. в стартап-файле прописан бесконечный цикл после выполнения main. Тем не менее, вы можете разместить здесь и какую-то логику, получающую данные от обработчика прерываний через глобальные переменные.

Доступ к регистрам контроллеров периферии осуществляется через обращение в память. В простейшем случае такой доступ осуществляется через [разыменование указателей](https://ru.wikipedia.org/wiki/Указатель_(тип_данных)#Действия_над_указателями), проинициализированных адресами регистров из [карты памяти](../13.%20Peripheral%20units#Задание) 13-ой лабораторной работы.

При написании программы помните, что в C++ сильно ограничена арифметика указателей, поэтому при присваивании указателю целочисленного значения адреса, необходимо использовать оператор `reinterpret_cast`.

Для того чтобы уменьшить ваше взаимодействие с чёрной магией указателей, вам представлен файл [platform.h](platform.h), в котором объявлены указатели на структуры, отвечающие за отображение полей на физические адреса периферийных устройств. Вам нужно лишь воспользоваться указателем на ваше периферийное устройство.

Если вашим устройством вывода является VGA-контроллер, то вам необходимо использовать экземпляр структуры, а не указатель на нее. Внутри данной структуры представлены указатели на байты: `char_map`, `color_map`, `tiff_map`. Как вы знаете, указатель может использоваться в качестве имени массива, а значит вы можете обращаться к нужному вам байту в соответствующей области памяти VGA-контроллера как к элементу массива. Например, для того, чтобы записать символ в шестое знакоместо второй строки, вам необходимо будет обратиться к `char_map[2*80+6]` (2*80 — индекс начала второй строки).

Пример взаимодействия с периферийным устройством через структуру **ВЫМЫШЛЕННОГО** периферийного устройства. Данная программа является лишь примером, иллюстрирующим взаимодействие с периферией через представленные указатели на структуры. Вам необходимо разобраться в том, как осуществляется работа с вымышленным устройством, а затем написать собственную программу, работающую по логике вашего индивидуального задания, которая взаимодействует с вашим реальным устройством.

```C++
/*
 Не надо копировать и использовать в качестве основы вашей программы этот код.
 Он для этого не подходит. В вашей процессорной системе нет никаких коллайдеров
 DEADLY_SERIOUS-событий и аварийных выключателей.
 Просто разберитесь в операторах `->`, ".", использовании указателей в качестве
 имени массива и напишите собственную программу.
*/
#include "platform.h"

/*
  В заголовочном файле "platform.h" объявлены collider_ptr — указатель на
  структуру SUPER_COLLIDER_HANDLE и collider_obj — экземпляр аналогичной
  структуры.
  Доступ к полям этой структуры через указатель можно осуществлять посредством
  оператора "->". Доступ к полям через экземпляр осуществляется с помощью
  оператора ".".
  Среди прочих полей, структура содержит указатель collider_mem, который
  указывает на некоторую память этого периферийного устройства. Данный указатель
  можно использовать в качестве имени массива.
*/

int main(int argc, char** argv)
{
  while(1){                             // В бесконечном цикле
    while (!(collider_ptr->ready));     // Постоянно опрашиваем регистр ready,
                                        // пока тот не станет равен 1.

                                        // После чего запускаем коллайдер,
    collider_ptr->start = 1;            // записав 1 в контрольный регистр start
    collider_obj.mem[0] = 300;          // Пример взаимодействия с памятью,
                                        // Используя объявленный в структуре
                                        // указатель в качестве имени массива.
  }
}

#define DEADLY_SERIOUS_EVENT 0xDEADDAD1

// extern "C" нужно использовать только в С++. Благодаря этому, в объектном
// файле функция будет называться именно int_handler, как и ожидает компоновщик
// при объединении кода с startup.S
// Без extern "C", при компиляции C++ кода имя функции в объектном файле будет
// немного другим (что-то типа _Z11int_handlerv), из-за чего возникнут проблемы
// в процессе компоновки.
extern "C" void int_handler()
{
  // Если от коллайдера приходит прерывание, сразу же проверяем регистр статуса
  // и если его код равен DEADLY_SERIOUS_EVENT, экстренно останавливаем
  // коллайдер
  if(DEADLY_SERIOUS_EVENT == collider_ptr->status)
  {
    collider_ptr->emergency_switch = 1;
  }
}
```

_Листинг 4. Пример кода на C++, взаимодействующего с выдуманным периферийным устройством через указатели на структуру и массив, объявленные в platform.h._

При написании программы на языках высокого уровня может возникнуть соблазн использовать все блага высокоуровневого программирования: использовать `scanf`/`cin` для консольного ввода, `printf`/`cout` для консольного вывода, а также использовать динамические массивы, или контейнеры библиотеки STL. Вам следует избегать подобных позывов, поскольку "из коробки" весь этот функционал вам не будет доступен по следующим причинам:

1. **Консольный ввод-вывод**. Для того чтобы `printf` мог вывести сообщение в ваше устройство вывода, необходимо "рассказать" ему как это сделать. Для этого необходимо переопределить функцию `write`, используемую функцией `printf`.
2. **Динамическая память**. Использование динамических массивов и контейнеров, неявно использующих динамическую память ограничено сложной механикой контроля за этой памятью. В системах общего назначения за это отвечает операционная система, однако в нашей встраиваемой системе её не будет. Для использования динамической памяти, вам потребуется написать **аллокатор** — специальную функцию, или класс, обеспечивающий выделение и освобождение памяти.
3. **Размер стандартных библиотек**. Даже если вы реализуете весь требуемый функционал, любая из перечисленных выше функций стандартных библиотек C/C++ потянет за собой включение огромного числа кода, который в итоге попросту не уместится в вашу память инструкций.

Однако, если вы всё ещё горите желанием писать по-настоящему высокоуровневый код, вы можете воспользоваться сторонними библиотеками, написанными специально для встраиваемых систем. К примеру, для `printf` можно использовать следующий репозиторий: https://github.com/mpaland/printf. Для использования этой библиотеки вам потребуется реализовать одну лишь функцию со следующим прототипом:

```C
void _putchar(char character);
```

В этой функции вам необходимо рассказать, как вывести один ASCII-символ в ваше устройство вывода.

Динамических массивов вам всё же стоит избегать, заменяя их статическими массивами заведомо большего размера. Однако, не всегда можно решить проблему одним лишь статическим массивом. К примеру, вы можете захотеть использовать словарь (ассоциативный массив). В этом случае вы можете использовать следующий репозиторий: https://github.com/mpaland/avl_array. Он не будет использовать динамическую память, в основе данного контейнера будет также лежать статический массив, размер которого вам необходимо будет задать вручную.

---

### Порядок выполнения задания

1. Внимательно изучите параграфы теории и практики.
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` может быть пустой — по её завершению в стартап-файле предусмотрен бесконечный цикл, из которого процессор сможет выходить только по прерыванию.
   3. В функции `int_handler` вам необходимо считать поступившие от устройства ввода входные данные.
   4. Вам необходимо самостоятельно решить, как вы хотите построить ход работы вашей программы: будет ли ваше индивидуальное задание вычисляться всего лишь 1 раз в функции `main`, данные в которую поступят от функции `int_handler` через глобальные переменные, или же оно будет постоянно пересчитываться при каждом вызове `int_handler`.
   5. Доступ к регистрам контроля и статуса необходимо осуществлять посредством указателей на структуры, объявленные в файле [platform.h](./platform.h). В случае VGA-контроллера, доступ к областям памяти осуществляется через экземпляр структуры (а не указатель на нее), содержащий имена массивов `char_map`, `color_map` и `tiff_map`.
5. [Скомпилируйте](#Практика) программу и [стартап-файл](startup.S) в объектные файлы.
6. Скомпонуйте объектные файлы в исполняемый файл, передав компоновщику соответствующий [скрипт](linker_script.ld).
7. Экспортируйте из объектного файла секции `.text` и `.data` в текстовые файлы `init_instr.mem`, `init_data.mem`. Если вы не создавали инициализированных статических массивов или глобальных переменных, то файл `init_data.mem` может быть оказаться пустым.
   1. Если файл `init_data.mem` не пустой, необходимо проинициализировать память в модуле `data_mem` c помощью системной функции `$readmemh` как это было сделано для памяти инструкций.
   2. Перед этим из файла `init_data.mem` необходимо удалить первую строку (вида `@20000000`), указывающую начальный адрес инициализации.
8. Добавьте получившиеся текстовые файлы в проект Vivado.
9. Запустите моделирование исполнения программы вашим процессором с помощью тестбенча из ЛР№13.
   1. В `peripheral_pkg` находятся вспомогательные вызовы, позволяющие сымитировать ввод с клавиатуры или uart (для переключателей никаких вспомогательных вызовов не требуется). Пример имитации ввода вы можете посмотреть в тестбенче. Обновите код тестбенча таким образом, чтобы в вашу систему были поданы необходимые для работы вашей программы данные.
   2. Для отладки во время моделирования будет удобно использовать дизасемблерный файл, ориентируясь на сигналы адреса и данных шины инструкций.
10. Проверьте корректное исполнение программы процессором в ПЛИС.

## Список источников:

1. [RISC-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 Groups — "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).


