ЛР14. Уточнение методички

This commit is contained in:
Andrei Solodovnikov
2024-05-22 15:10:39 +03:00
parent af51ef612f
commit bf865179ea
3 changed files with 157 additions and 116 deletions

View File

@@ -332,6 +332,10 @@ _main_call:
# Но для простоты реализации оба аргумента всего лишь обнулены.
# Это сделано для детерминированного поведения программы в случае,
# если программист будет пытаться использовать эти аргументы.
# Вызов main.
# Для того чтобы программа скомпоновалась, где-то должна быть описана
# функция именно с таким именем.
call main
# Зацикливание после выхода из функции main
_endless_loop:
@@ -370,34 +374,36 @@ _int_handler:
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)
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)
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-регистры,
@@ -405,38 +411,41 @@ _int_handler:
# вернуть исходное значение указателя стека прерываний. Однако его нынешнее
# значение нам еще необходимо для восстановления контекста, поэтому мы
# сохраним его в регистр a0, и будем восстанавливаться из него.
mv a0, sp
mv a0,sp
lw t1, 72(a0)
addi sp, sp, 80
csrw mscratch, sp
csrw mepc, t1
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 t1,72(a0)
addi sp,sp,80
csrw mscratch,sp
csrw mepc,t1
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, 40(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,40(a0)
# Выход из обработчика прерывания
mret
```
_Листинг 2. Пример содержимого файла первичных команд с поясняющими комментариями._
Обратите внимание на строки `call main` и `call int_handler`. Компоновка объектного файла, полученного после компиляции `startup.S` будет успешной только в том случае, если в других компонуемых файлах будут функции именно с такими именами.
## Практика
Для того, чтобы запустить моделирование исполнения программы на вашем процессоре, сперва эту программу необходимо скомпилировать и преобразовать в текстовый файл, которым САПР сможет проинициализировать память процессора. Для компиляции программы, вам потребуется особый компилятор, который называется "кросскомпилятор". Он позволяет компилировать исходный код под архитектуру компьютера, отличную от компьютера, на котором ведется компиляция. В нашем случае, вы будете собирать код под архитектуру `RISC-V` на компьютере с архитектурой `x86_64`.
@@ -453,8 +462,8 @@ _Листинг 2. Пример содержимого файла первичн
Вам потребуются следующие флаги компиляции:
* `-march=rv32i_zicsr` — указание разрядности и набора расширений в архитектуре, под которую идет компиляция (у нас процессор rv32i с расширением инструкциями для взаимодействия с регистрами контроля и статуса Zicsr)
* `-mabi=ilp32` — указание двоичного интерфейса приложений. Здесь сказано, что типы `int`, `long` и `pointer` являются 32-разрядными.
- `-march=rv32i_zicsr` — указание разрядности и набора расширений в архитектуре, под которую идет компиляция (у нас процессор rv32i с расширением инструкциями для взаимодействия с регистрами контроля и статуса Zicsr)
- `-mabi=ilp32` — указание двоичного интерфейса приложений. Здесь сказано, что типы `int`, `long` и `pointer` являются 32-разрядными.
Есть очень [хорошее видео](https://youtu.be/29iNHEhHmd0?t=141), описывающее состав тулчейнов, именование исполняемых файлов компиляторов, как формируются ключи архитектуры и двоичного интерфейса приложений.
@@ -474,10 +483,10 @@ _Листинг 2. Пример содержимого файла первичн
Исполняемый файл компилятора тот же самый, флаги компоновки будут следующие:
* `-march=rv32i_zicsr -mabi=ilp32` — те же самые флаги, что были при компиляции (нам все еще нужно указывать архитектуру, иначе компоновщик может скомпоновать объектные файлы со стандартными библиотеками от другой архитектуры)
* `-Wl,--gc-sections` — указать компоновщику удалять неиспользуемые секции (сокращает объем итогового файла)
* `-nostartfiles` — указать компоновщику не использовать стартап-файлы стандартных библиотек (сокращает объем файла и устраняет ошибки компиляции из-за конфликтов с используемым стартап-файлом).
* `-T linker_script.ld` — передать компоновщику скрипт компоновки
- `-march=rv32i_zicsr -mabi=ilp32` — те же самые флаги, что были при компиляции (нам все еще нужно указывать архитектуру, иначе компоновщик может скомпоновать объектные файлы со стандартными библиотеками от другой архитектуры)
- `-Wl,--gc-sections` — указать компоновщику удалять неиспользуемые секции (сокращает объем итогового файла)
- `-nostartfiles` — указать компоновщику не использовать стартап-файлы стандартных библиотек (сокращает объем файла и устраняет ошибки компиляции из-за конфликтов с используемым стартап-файлом).
- `-T linker_script.ld` — передать компоновщику скрипт компоновки
Пример команды компоновки:
@@ -589,6 +598,8 @@ Disassembly of section .data:
...
```
_Листинг 3. Пример дизасемблированного файла._
Числа в самом левом столбце, увеличивающиеся на 4 — это адреса в памяти. Отлаживая программу на временной диаграмме, вы можете ориентироваться на эти числа, как на значения PC.
Следующая за адресом строка, записанная в шестнадцатеричном виде — это та инструкция (или данные), которая размещена по этому адресу. С помощью этого столбца вы можете проверить, что считанная инструкция на временной диаграмме (сигнал `instr`) корректна.
@@ -621,7 +632,16 @@ Disassembly of section .data:
Вам необходимо написать программу для вашего [индивидуального задания](../04.%20Primitive%20programmable%20device/Индивидуальное%20задание#индивидуальные-задания) к 4-ой лабораторной работе на языке C или C++ (в зависимости от выбранного языка необходимо использовать соответствующий компилятор: gcc для C, g++ для C++).
При этом, вам необходимо получить входные данные от вашего устройства ввода и вывести результат на устройство вывода. Продумайте, как именно будет работать ваша программа, (бесконечно пересчитывать значения, получая новые данные от устройства ввода, или считать один раз, ожидая данные в бесконечном цикле — вариантов реализации очень много).
Для того чтобы ваша программа собралась, необходимо описать две функции: `main` и `int_handler`. Аргументы и возвращаемые значения могут быть любыми, но использоваться они не смогут. Функция `main` будет вызвана в начале работы программы (после исполнения подготовительной части startup-файла), функция `int_handler` будет вызываться автоматически каждый раз, когда ваш контроллер устройства ввода будет генерировать запрос прерывания (если процессор закончил обрабатывать предыдущий запрос).
Таким образом, минимальный алгоритм работы заключается в том, чтобы считать по прерыванию данные от устройства ввода (в индивидуальном задании обозначалось как sw_i), выполнить обработку из вашего варианта, и записать результат в устройство вывода. При этом необходимо помнить о следующем:
- При вводе данных с клавиатуры, отправляется скан-код клавиши, а не значение нажатой цифры (и не ascii-код нажатой буквы). Более того, при отпускании клавиши, генерируется скан-код `FO`, за которым следует повторная отправка скан-кода этой клавиши.
- Работая с 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-ой лабораторной работы.
@@ -629,9 +649,16 @@ Disassembly of section .data:
Для того, чтобы уменьшить ваше взаимодействие с черной магией указателей, вам представлен файл [platform.h](platform.h), в котором объявлены указатели структуры, отвечающие за отображение полей на физические адреса периферийных устройств. Вам нужно лишь воспользоваться указателем на ваше периферийное устройство.
Пример взаимодействия с периферийным устройством через вымышленную структуру:
Пример взаимодействия с периферийным устройством через структуру **ВЫМЫШЛЕННОГО** периферийного устройства. Данная программа является лишь примером, иллюстрирующим взаимодействие с периферией через представленные указатели на структуры. Вам необходимо разобраться в том, как осуществляется работа с вымышленным устройством, а затем написать собственную программу, работающую по логике вашего индивидуального задания, которая взаимодействует с вашим реальным устройством.
```C++
/*
Не надо копировать и использовать в качестве основы вашей программы этот файл.
Он для этого не подходит. В вашей процессорной системе нет никаких коллайдеров
DEADLY_SERIOUS-событий и аварийных выключателей.
Просто разберитесь в операторе `->` и использовании указателей в качестве имени
массива и напишите собственную программу.
*/
#include "platform.h"
/*
@@ -675,20 +702,24 @@ extern "C" void int_handler()
}
```
_Листинг 4. Пример кода на C++, взаимодействующего с выдуманным периферийным устройством через указатели на структуру и массив, объявленные в platform.h._
Если одним из ваших периферийных устройств был VGA-контроллер, то вы можете использовать не указатель на структуру, а объявленные в том же файле указатели на байты: `char_map`, `color_map`, `tiff_map`. Как вы знаете, указатель может использоваться в качестве имени массива, а значит вы можете обращаться к нужному вам байту в соответствующей области памяти VGA-контроллера как к элементу массива. Например, для того, чтобы записать символ в шестое знакоместо второй строки, вам необходимо будет обратиться к `char_map[2*80+6]` (2*80 — индекс начала второй строки).
---
### Порядок выполнения задания
1. Написать программу для своего индивидуального задания на языке C или C++.
2. [Скомпилировать](#практика) программу и [стартап-файл](startup.S) в объектные файлы.
3. Скомпоновать объектные файлы исполняемый файл, передав компоновщику соответствующий [скрипт](linker_script.ld).
4. Экспортировать из объектного файла секции `.text` и `.data` в текстовые файлы `init_instr.mem`, `init_data.mem`. Если вы не создавали инициализированных статических массивов или глобальных переменных, то файл `init_data.mem` может быть оказаться пустым.
1. Внимательно изучить разделы теории и практики.
2. Разобрать принцип взаимодействия с контрольными и статусными регистрами периферийного устройства на примере _Листинга 4_.
3. Написать программу для своего индивидуального задания и набора периферийных устройств на языке C или C++. В случае написания кода на C++ помните о необходимости добавления `extern "C"` перед определением функции `int_handler`.
4. [Скомпилировать](#практика) программу и [стартап-файл](startup.S) в объектные файлы.
5. Скомпоновать объектные файлы исполняемый файл, передав компоновщику соответствующий [скрипт](linker_script.ld).
6. Экспортировать из объектного файла секции `.text` и `.data` в текстовые файлы `init_instr.mem`, `init_data.mem`. Если вы не создавали инициализированных статических массивов или глобальных переменных, то файл `init_data.mem` может быть оказаться пустым.
1. Если файл `init_data.mem` не пустой, необходимо проинициализировать память в модуле `ext_mem` c помощью системной функции `$readmemh` как это было сделано для памяти инструкций.
2. Перед этим из файла `init_data.mem` необходимо удалить первую строку (вида `@00001000`), указывающую начальный адрес инициализации.
5. Добавить получившиеся текстовые файлы в проект Vivado.
6. Запустить моделирование исполнения программы вашим процессором. Для отладки во время моделирования будет удобно использовать дизасемблерный файл, ориентируясь на сигналы адреса и данных шины инструкций.
7. Проверить корректное исполнение программы процессором в ПЛИС.
7. Добавить получившиеся текстовые файлы в проект Vivado.
8. Запустить моделирование исполнения программы вашим процессором. Для отладки во время моделирования будет удобно использовать дизасемблерный файл, ориентируясь на сигналы адреса и данных шины инструкций.
9. Проверить корректное исполнение программы процессором в ПЛИС.
---