diff --git a/.pic/Labs/lab_15_coremark/fig_01.png b/.pic/Labs/lab_15_coremark/fig_01.png new file mode 100644 index 00000000..6cd83dc1 Binary files /dev/null and b/.pic/Labs/lab_15_coremark/fig_01.png differ diff --git a/Labs/15. Coremark/Makefile b/Labs/15. Coremark/Makefile new file mode 100644 index 00000000..c49e3850 --- /dev/null +++ b/Labs/15. Coremark/Makefile @@ -0,0 +1,73 @@ +CC_PATH = /c/riscv_cc/bin +CC_PREFIX = riscv-none-elf + +CC = $(CC_PATH)/$(CC_PREFIX)-gcc +OBJDUMP = $(CC_PATH)/$(CC_PREFIX)-objdump +OBJCOPY = $(CC_PATH)/$(CC_PREFIX)-objcopy +SIZE = $(CC_PATH)/$(CC_PREFIX)-size + +ifndef src +src = core_main.o +endif + +OBJS = $(src) startup.o core_list_join.o core_matrix.o core_portme.o core_state.o core_util.o cvt.o ee_printf.o + + +LINK_SCRIPT = linker_script.ld +OUTPUT = coremark +OUTPUT_PROD = $(addprefix $(OUTPUT), .mem _instr.mem _data.mem .elf _disasm.S) +# OUTPUT_PROD :=$(OUTPUT_PROD) $(addprefix tb_$(OUTPUT), .mem _instr.mem _data.mem .elf _disasm.S) + +INC_DIRS = "./" +SRC_DIR = ./src +CC_FLAGS = -march=rv32i_zicsr -mabi=ilp32 -I$(INC_DIRS) +LD_FLAGS = -Wl,--gc-sections -nostartfiles -T $(LINK_SCRIPT) + +.PHONY: all setup clean clean_all size harvard princeton + +all: clean setup harvard + +setup: + cp barebones/*.c barebones/*.h ./ + +harvard: $(OUTPUT).elf $(OUTPUT)_disasm.S size +# $< означает "первая зависимость" + ${OBJCOPY} -O verilog --verilog-data-width=4 -j .data -j .sdata -j .bss $< $(OUTPUT)_data.mem + ${OBJCOPY} -O verilog --verilog-data-width=4 -j .text $< $(OUTPUT)_instr.mem + ${OBJCOPY} -O verilog -j .data -j .sdata -j .bss $< tb_$(OUTPUT)_data.mem + ${OBJCOPY} -O verilog -j .text $< tb_$(OUTPUT)_instr.mem + sed -i '1d' $(OUTPUT)_data.mem + +princeton: $(OUTPUT).elf $(OUTPUT)_disasm.S size + ${OBJCOPY} -O verilog --verilog-data-width=4 --remove-section=.comment $< $(OUTPUT).mem + +$(OUTPUT).elf: $(OBJS) +# $^ Означает "все зависимости". + ${CC} $^ $(LD_FLAGS) $(CC_FLAGS) -o $(OUTPUT).elf + +$(OUTPUT)_disasm.S: $(OUTPUT).elf +# $< означает "первая зависимость", $@ — "цель рецепта". + ${OBJDUMP} -D $< > $@ + + +# Шаблонные рецепты (см. https://web.mit.edu/gnu/doc/html/make_10.html#SEC91) +# Здесь говорится как создать объектные файлы из одноименных исходников +%.o: %.S + ${CC} -c $(CC_FLAGS) $^ -o $@ + +%.o: %.c + ${CC} -c $(CC_FLAGS) $^ -o $@ + +%.o: %.cpp + ${CC} -c $(CC_FLAGS) $^ -o $@ + +size: $(OUTPUT).elf +# $< означает "первая зависимость" + $(SIZE) $< + +clean: + rm -f $(OBJS) + rm -f core_portme.* cvt.c ee_printf.c + +clean_all: clean + rm -f $(OUTPUT_PROD) diff --git a/Labs/15. Coremark/README.md b/Labs/15. Coremark/README.md index 2d2be7fd..89d5f679 100644 --- a/Labs/15. Coremark/README.md +++ b/Labs/15. Coremark/README.md @@ -1,2 +1,345 @@ # Лабораторная работа 15 "Оценка производительности" +## Допуск к лабораторной работе + +Данная лабораторная работа будет полностью опираться на навыки, полученные в ходе выполнения лабораторных работ: + +12. [Периферийные устройства](../12.%20Peripheral%20units/) +13. [Программирование](../13.%20Programming/) + +## Цель + +На текущий момент мы создали процессорную систему, которая способна взаимодействовать с внешним миром посредством периферийных устройств ввода-вывода и программатора. Однако сложно понять, какое место данная система занимает в ряду уже существующих вычислительных систем. + + + +Для оценки производительности мы должны будем модифицировать процессорную систему, собрать и запустить специализированное ПО, отвечающее за измерение производительности (мы будем использовать программу Coremark). + +## Теория + +[Coremark](https://www.eembc.org/coremark/faq.php) (далее кормарк) — это набор синтетических тестов для измерения производительности процессорной системы. В данный набор входят такие тесты, как работа со связными списками, матричные вычисления, обработка конечных автоматов и подсчет контрольной суммы. Результат выражается в одном числе, которое можно использовать для сравнения с результатами других процессорных систем. + +Для подсчета производительности, кормарк опирается на функцию, возвращающую текущее время, поэтому для оценки производительности нам потребуется вспомогательное периферийное устройство: таймер. + +Для вывода результатов тестирования, необходимо описать способ, которым кормарк сможет выводить очередной символ сообщения (для этого мы будем использовать контроллер UART из предыдущих лабораторных работ). + +Кроме того, скомпилированная без оптимизаций программа будет занимать чуть более 32KiB, поэтому нам потребуется изменить размер памяти инструкций + +Таким образом, для того чтобы запустить данную программу, нам необходимо выполнить как аппаратные изменения процессорной системы (добавить таймер и (если отсутствует) контроллер UART), так и программные изменения самого кормарка (для этого в нем предусмотрены специальные платформозависимые файлы, в которых объявлены функции, реализацию которых нам необходимо выполнить). + +## Задание + +1. Реализовать модуль-контроллер "таймер". +2. Подключить этот модуль к системной шине. + 2.1 В случае, если до этого в ЛР12 вашим устройством вывода было не UART TX, вам необходимо подключить к системной шине и готовый модуль [uart_tx_sb_ctrl](../Made-up%20modules/lab_12.uart_tx_sb_ctrl.sv). +3. Добавить реализацию платформозависимых функций программы coremark. +4. Скомпилировать программу. +5. Изменить размер памяти инструкций +6. Запустить моделирование +7. Сравнить результаты измерения производительности с результатами существующих процессорных системам. + +### Таймер + +Разберемся с тем, как будет работать наш таймер. По сути, это просто системный счетчик (не путайте с программным счетчиком), непрерывно считающий такты с момента последнего сброса. Для измерения времени мы будем засекать значение счетчика на момент начала отсчета и значение счетчика в конце отсчета. Зная тактовую частоту и разность между значениями счетчика мы с легкостью сможем вычислить прошедшее время. При этом нужно обеспечить счетчик такой разрядностью, чтобы он точно не смог переполниться. + +Поскольку мы уже назвали данный модуль "таймером", чтобы тот не был слишком простым, давайте добавим ему функциональности: пускай это будет устройство, способное генерировать прерывание через заданное число тактов. Таким образом, процессорная система сможет засекать время без постоянного опроса счетчика. + +Было бы удобно, чтобы мы могли управлять тем, каким образом данный модуль будет генерировать такое прерывание: однократно, заданное число раз или же бесконечно, пока тот не остановят. + +Таким образом, мы сформировали следующее адресное пространство данного контроллера: + +|Адрес|Режим доступа|Допустимые значения| Функциональное назначение | +|-----|-------------|-------------------|---------------------------------------------------------------------------------| +|0x00 | R | [0:2⁶⁴-1] | Значение системного счетчика, доступное только для чтения | +|0x04 | RW | [0:2⁶⁴-1] | Указание задержки, спустя которую таймер будет генерировать прерывание | +|0x08 | RW | [0:2] | Указание режима генерации прерываний (выключен, заданное число раз, бесконечно) | +|0x0c | RW | [0:2³²] | Указание количества повторений генерации прерываний | +|0x24 | W | 1 | Программный сброс | + +Прототип модуля следующий: + +```SystemVerilog +module timer_sb_ctrl( +/* + Часть интерфейса модуля, отвечающая за подключение к системной шине +*/ + input logic clk_i, + input logic rst_i, + input logic req_i, + input logic write_enable_i, + input logic [31:0] addr_i, + input logic [31:0] write_data_i, // не используется, добавлен для + // совместимости с системной шиной + output logic [31:0] read_data_o, + output logic ready_o, +/* + Часть интерфейса модуля, отвечающая за отправку запросов на прерывание + процессорного ядра +*/ + output logic interrupt_request_o +); +``` + +Для работы данного контроллера потребуются следующие сигналы: + +```SystemVerilog +logic [63:0] system_counter; +logic [63:0] delay; +enum logic [1:0] {OFF, NTIMES, FOREVER} mode, next_mode; +logic [31:0] repeat_counter; +logic [63:0] system_counter_at_start; +``` + +- `system_counter` — регистр, ассоциированный с адресом `0x00`, системный счетчик. Задача регистра заключается в ежетактном увеличении на единицу. +- `delay` — регистр, ассоциированный с адресом `0x04`. Число тактов, спустя которое таймер (когда тот будет включен) сгенерирует прерывание. Данный регистр изменяется только сбросом, либо запросом на запись. +- `mode` — регистр, ассоциированный с адресом `0x08`. Режим работы таймера: + - `OFF` — отключен (не генерирует прерывания) + - `NTIMES` — включен до тех пор, пока не сгенерирует N прерываний (Значение N хранится в регистре `repeat_counter` и обновляется после каждого сгенерированного прерывания). После генерации N прерываний, переходит в режим `OFF`. + - `FOREVER` — бесконечная генерация прерываний. Не отключится, пока режим работы прерываний не будет изменен. +- `next_mode` — комбинационный сигнал, который подается на вход записи в регистр `mode` (аналог `next_state` из предыдущей лабораторной работы). +- `repeat_counter` — регистр, ассоциированный с адресом `0x0c`. Количество повторений для режима `NTIMES`. Уменьшается в момент генерации прерывания в этом режиме. +- `system_counter_at_start` — неархитектурный регистр, хранящий значение системного счетчика на момент начала отсчета таймера. Обновляется при генерации прерывания (если это не последнее прерывание в режиме `NTIMES`) и при запросе на запись в регистр `mode` значения не `OFF`. + +Для подключения данного таймера к системной шине, мы воспользуемся первым свободным базовым адресом, оставшимся после ЛР12: `0x08`. Таким образом, для обращения к системному счетчику, процессор будет использовать адрес `0x08000000` для обращения к регистру `delay` `0x08000004` и т.п. + +### Настройка Coremark + +В первую очередь, необходимо скачать исходный код данной программы, размещенный по адресу: [https://github.com/eembc/coremark](https://github.com/eembc/coremark). На случай возможных несовместимых изменений в будущем, все дальнейшие ссылки будут даваться слепок репозитория, который был на момент коммита `d5fad6b`. + +Нам необходимо добавить поддержку нашей процессорной системы. Для этого необходимо + +1. Реализовать функцию, измеряющую время +2. Реализовать функцию, выводящую очередной символ сообщения с результатами +3. Реализовать функцию, выполняющую первичную настройку периферии перед тестом +4. Выполнить мелкую подстройку, такую как количество итераций в тесте и указание аргументов, с которыми будет скомпилирована программа. + +Все файлы, содержимое которых мы будем менять расположены в папке [barebones](https://github.com/eembc/coremark/tree/d5fad6bd094899101a4e5fd53af7298160ced6ab/barebones). + +#### Реализация функции, измеряющей время + +Не мы первые придумали измерять время путем отсчета системных тактов, поэтому вся логика по измерению времени уже реализована в coremark. От нас требуется только реализовать функцию, которая возвращает текущее значение системного счетчика. + +Данной функцией является `barebones_clock`, расположенная в файле [`core_portme.c`](https://github.com/eembc/coremark/blob/d5fad6bd094899101a4e5fd53af7298160ced6ab/barebones/core_portme.c). В данный момент, в реализации функции описан вызов ошибки (поскольку реализации как таковой нет). Мы должны **заменить** реализацию функции следующим кодом: + +```C +barebones_clock() +{ + volatile ee_u32 *ptr = (ee_u32*)0x08000000; + ee_u32 tim = *ptr; + return tim; +} +``` + +После ЛР13 вы уже должны представлять что здесь происходит. Мы создали указатель с абсолютным адресом `0x08000000` — адресом системного счетчика. Разыменование данного указателя вернет текущее значение системного счетчика, что и должно быть результатом вызова этой функции. + +Для того, чтобы корректно преобразовать тики системного счетчика во время, используется функция [`time_in_secs`](https://github.com/eembc/coremark/blob/d5fad6bd094899101a4e5fd53af7298160ced6ab/barebones/core_portme.c#L117), которая уже реализована, но для работы которой нужно определить макрос `CLOCKS_PER_SEC`, характеризующий тактовую частоту, на которой работает процессор. Давайте определим данный макрос сразу над макросом [`EE_TICKS_PER_SEC`](https://github.com/eembc/coremark/blob/d5fad6bd094899101a4e5fd53af7298160ced6ab/barebones/core_portme.c#L62): + +```C +#define CLOCKS_PER_SEC 10000000 +``` + +На этом наша задача по измерению времени завершена. Остальные правки будут не сложнее этих. + +#### Реализация вывода очередного символа сообщения + +Для вывода очередного символа во встраиваемых системах используется (какое совпадение!) функция [`uart_send_char`](https://github.com/eembc/coremark/blob/d5fad6bd094899101a4e5fd53af7298160ced6ab/barebones/ee_printf.c#L663), расположенная в файле [`ee_printf.c`](https://github.com/eembc/coremark/blob/d5fad6bd094899101a4e5fd53af7298160ced6ab/barebones/ee_printf.c). + +В реализации данной функции вам уже предлагают алгоритм, по которому та должна работать, необходимо: + +1. дождаться готовности UART к отправке +2. передать отправляемый символ +3. дождаться готовности UART к отправке (завершения передачи) + +Давайте так и реализуем эту функцию: + +```C +uart_send_char(char c) +{ + volatile ee_u8 *uart_ptr = (ee_u8 *)0x06000000; + while(*(uart_ptr+0x08)); + *uart_ptr = c; + while(*(uart_ptr+0x08)); +} +``` + +`0x06000000` — базовый адрес контроллера UART TX из ЛР12 (и адрес передаваемых этим контроллером данных). +`0x08` — адрес регистра `busy` в адресном пространстве этого контроллера. + +#### Реализация функции первичной настройки + +Это функция [`portable_init`](https://github.com/eembc/coremark/blob/d5fad6bd094899101a4e5fd53af7298160ced6ab/barebones/core_portme.c#L130), расположенная в уже известном ранее файле [`core_portme`.c]. Данная функция выполняет необходимые нам настройки перед началом теста. Для нас главное — настроить нужным образом контроллер UART. +Допустим, мы хотим чтобы данные передавались на скорости `115200`, c одним стоповым битом и контролем бита четности. В этом случае, мы должны добавить в начало функции следующий код: + +```C +portable_init(core_portable *p, int *argc, char *argv[]) +{ + volatile ee_u32 *uart_tx_ptr = (ee_u32 *)0x06000000; + *(uart_tx_ptr + 3) = 115200; + *(uart_tx_ptr + 4) = 1; + *(uart_tx_ptr + 5) = 1; + + //... +} +``` + +#### Дополнительные настройки + +Для тонких настроек используется заголовочный файл [`core_portme.h`](https://github.com/eembc/coremark/blob/d5fad6bd094899101a4e5fd53af7298160ced6ab/barebones/core_portme.h). Нам необходимо: + +1. Объявить в начале файла макрос `ITERATIONS`, влияющий на количество прогонов теста. Нам достаточно выставить значение 1. +2. Обновить значение макроса `COMPILER_FLAGS`, заменив его значение `FLAGS_STR` на`"-march=rv32i_zicsr -mabi=ilp32"`, именно с этими аргументами мы будем собирать программу. Это опциональная настройка, которая позволит вывести флаги компиляции в итоговом сообщении. +3. Добавить подключение заголовочного файла `#include `. + +### Компиляция + +Для компиляции программы, вам потребуются предоставленные файлы [Makefile](Makefile) и [linker_script.ld](linker_script.ld), а так же файл [startup.S](../13.%20Programming/startup.S) из ЛР13. Эти файлы необходимо скопировать с заменой в корень папки с программой. + +`Makefile` написан из рассчета, что кросс-компилятор расположен по пути `C:/riscv_cc/`. В случае, если это не так, измените первую строчку данного файла в соответствии с расположением кросс-компилятора. + +Для запуска компиляции, необходимо выполнить следующую команду, находясь в корне программы coremark: + +```bash +make +``` + +В случае, если на вашем рабочем компьютере не установлена эта утилита, вы скомпилировать программу вручную, выполнив следующую серию команд: + +```bash +cp barebones/*.c barebones/*.h ./ +/c/riscv_cc/bin/riscv-none-elf-gcc -c -march=rv32i_zicsr -mabi=ilp32 -I"./" core_main.c -o core_main.o +/c/riscv_cc/bin/riscv-none-elf-gcc -c -march=rv32i_zicsr -mabi=ilp32 -I"./" startup.S -o startup.o +/c/riscv_cc/bin/riscv-none-elf-gcc -c -march=rv32i_zicsr -mabi=ilp32 -I"./" core_list_join.c -o core_list_join.o +/c/riscv_cc/bin/riscv-none-elf-gcc -c -march=rv32i_zicsr -mabi=ilp32 -I"./" core_matrix.c -o core_matrix.o +/c/riscv_cc/bin/riscv-none-elf-gcc -c -march=rv32i_zicsr -mabi=ilp32 -I"./" core_portme.c -o core_portme.o +/c/riscv_cc/bin/riscv-none-elf-gcc -c -march=rv32i_zicsr -mabi=ilp32 -I"./" core_state.c -o core_state.o +/c/riscv_cc/bin/riscv-none-elf-gcc -c -march=rv32i_zicsr -mabi=ilp32 -I"./" core_util.c -o core_util.o +/c/riscv_cc/bin/riscv-none-elf-gcc -c -march=rv32i_zicsr -mabi=ilp32 -I"./" cvt.c -o cvt.o +/c/riscv_cc/bin/riscv-none-elf-gcc -c -march=rv32i_zicsr -mabi=ilp32 -I"./" ee_printf.c -o ee_printf.o +/c/riscv_cc/bin/riscv-none-elf-gcc core_main.o startup.o core_list_join.o core_matrix.o core_portme.o core_state.o core_util.o cvt.o ee_printf.o -Wl,--gc-sections -nostartfiles -T linker_script.ld -march=rv32i_zicsr -mabi=ilp32 -I"./" -o coremark.elf +/c/riscv_cc/bin/riscv-none-elf-objdump -D coremark.elf > coremark_disasm.S +/c/riscv_cc/bin/riscv-none-elf-objcopy -O verilog --verilog-data-width=4 -j .data -j .sdata -j .bss coremark.elf coremark_data.mem +/c/riscv_cc/bin/riscv-none-elf-objcopy -O verilog --verilog-data-width=4 -j .text coremark.elf coremark_instr.mem +/c/riscv_cc/bin/riscv-none-elf-size coremark.elf +sed -i '1d' coremark_data.mem +``` + +В случае успешной компиляции, вам будет выведено сообщение об итоговом размере секций инструкций и данных: + +```text + text data bss dec hex filename + 34324 2268 100 36692 8f54 coremark.elf +``` + +### Изменение размера памяти инструкций + +Как видите, размер секции инструкций превышает 32KiB на 1556 байт. Поэтому на время оценки моделирования, нам придется увеличить размер памяти инструкций до 64KiB, изменив число слов памяти инструкций до 16384. При этом необходимо изменить диапазон бит адреса, используемых для чтения инструкции из памяти с `[11:2]` на `[15:2]`. + +Обратите внимание, что увеличение размера памяти в 16 раз приведет к значительному увеличению времени синтеза устройства, поэтому данное изменение мы производим исключительно на время поведенческого моделирования. + +### Запуск моделирования + +Программирование 32KiB по UART займет продолжительное время, поэтому вам предлагается проинициализировать память инструкций и данных "по-старинке" через системные функции `$readmemh`. + +Если все было сделано без ошибок, то примерно на `276ms` времени моделирования вам начнется выводиться сообщение вида: + +```text +CoreMark Size : 666 +Total ticks : 2574834 +Total time (secs): <скрыто то получения результатов моделирования> +Iterations/Sec : <скрыто то получения результатов моделирования> +ERROR! Must execute for at least 10 secs for a valid result! +Iterations : 1 +Compiler version : GCC13.2.0 +Compiler flags : -march=rv32i_zicsr -mabi=ilp32 +Memory location : STACK +seedcrc : 0x29f4 +[0]crclist : 0x7704 +[0]crcmatrix : 0x1fd7 +[0]crcstate : 0x8e3a +[0]crcfinal : 0x7704 +Correct operation validated. See README.md for run and reporting rules. +``` + +(вывод сообщения будет завершен приблизительно на `335ms` времени моделирования). + +## Порядок выполнения задания + +1. [Опишите](#таймер) таймер в виде модуля `timer_sb_ctrl`. +2. Подключите данный модуль к системной шине. Сигнал прерывания этого модуля подключать не нужно. + 2.1 В случае, если до этого в ЛР12 вашим устройством вывода было не UART TX, вам необходимо подключить к системной шине и готовый модуль [uart_tx_sb_ctrl](../Made-up%20modules/lab_12.uart_tx_sb_ctrl.sv). +3. Получите исходники программы Coremark. Для этого можно либо склонировать репозиторий, либо скачать его в виде архива со страницы: [https://github.com/eembc/coremark](https://github.com/eembc/coremark). +4. Добавьте реализацию платформозависимых функций программы coremark. Для этого в папке `barebones` необходимо: + 1. в файле `core_portme.c`: + 1. [реализовать](#реализация-функции-измеряющей-время) функцию `barebones_clock`, возвращающую текущее значение системного счетчика; + 2. объявить макрос `CLOCKS_PER_SEC`, характеризующий тактовую частоту процессора; + 3. [реализовать](#реализация-функции-первичной-настройки) функцию `portable_init`, выполняющую первичную инициализацию периферийных устройств до начала теста; + 2. в файле `ee_printf.c` [реализовать](#реализация-вывода-очередного-символа-сообщения) функцию `uart_send_char`, отвечающую за отправку очередного символа сообщения о результате. +5. Добавьте с заменой в корень программы файлы [Makefile](Makefile), [linker_script.ld](linker_script.ld) и [startup.S](../13.%20Programming/startup.S). +6. Скомпилируйте программу вызовом `make`. + 1. Если кросскомпилятор расположен не в директории `C:/riscv_cc`, перед вызовом `make` вам необходимо соответствующим образом отредактировать первую строчку в `Makefile`. + 2. В случае отсутствия на компьютере утилиты `make`, вы можете самостоятельно скомпилировать программу вызовом команд, представленных в разделе ["Компиляция"](#компиляция). +7. Временно измените размер памяти инструкций до 64KiB. + 1. Для этого необходимо изменить размер памяти инструкций с 1024 слов до 16384 слов. + 2. Кроме того, необходимо изменить используемые индексы адреса с `[11:2]` на `[15:2]`. +8. Проинициализируйте память инструкций и память данных файлами "coremark_instr.mem" и "coremark_data.mem", полученными в ходе компиляции программы. +9. Выполните моделирование системы с помощью модуля [tb_coremark](tb_coremark). + 1. Результаты теста будут выведены приблизительно на `335ms` времени моделирования. + +
+ 10. Прочти меня после успешного завершения моделирования + +Итак, вы получили сообщение вида: + +```text +CoreMark Size : 666 +Total ticks : 2574834 +Total time (secs): 0.257483 +Iterations/Sec : 3.883746 +ERROR! Must execute for at least 10 secs for a valid result! +Iterations : 1 +Compiler version : GCC13.2.0 +Compiler flags : -march=rv32i_zicsr -mabi=ilp32 +Memory location : STACK +seedcrc : 0x29f4 +[0]crclist : 0x7704 +[0]crcmatrix : 0x1fd7 +[0]crcstate : 0x8e3a +[0]crcfinal : 0x7704 +Correct operation validated. See README.md for run and reporting rules. +``` + +Не обращайте внимание на строчку "ERROR! Must execute for at least 10 secs for a valid result!". Программа считает, что для корректных результатов, необходимо крутить ее по кругу в течении минимум 10 секунд, однако по большей части это требование необходимо для более достоверного результата у конвейерных систем или системах с запущенной ОС. Наш однотактный процессор будет вести себя одинаково на каждом круге, поэтому нет смысла в дополнительном времени моделирования. + +Нас интересует строка: + +```text +Iterations/Sec : <скрыто то получения результатов моделирования> +``` + +Это и есть так называемый "кормарк" — метрика данной программы. Результат нашего процессора: 3.88 кормарка. + +Обычно, для сравнения между собой нескольких реализаций микроархитектур, более достоверной считается величина "кормарк / МГц", т.е. значение кормарка поделеное на тактовую частоту процессора. Дело в том, что можно реализовать какую-нибудь очень сложную архитектуру, которая будет выдавать очень хороший кормарк, но при этом будет иметь очень низкую частоту. Более того, при сравнении с другими результатами, необходимо учитывать флаги оптимизации, которые использовались при компиляции программы, поскольку они также влияют на результат. + +Мы не будем уходить в дебри темных паттернов маркетинга и вместо этого будет оценивать производительность в лоб: сколько кормарков в секунду смог прогнать наш проц в сравнении с представленными результатами других систем вне зависимости от их оптимизаций. + +Таблица опубликованных результатов находится по адресу: [https://www.eembc.org/coremark/scores.php](https://www.eembc.org/coremark/scores.php). Нам необходимо отсортировать эту таблицу по столбцу `CoreMark`, кликнув по нему. + +Мы получим следующий расклад: + +![../../.pic/Labs/lab_15_coremark/fig_01.png](../../.pic/Labs/lab_15_coremark/fig_01.png) + +На что мы можем обратить внимание? Ну, во-первых мы видим, что ближайший к нам микроконтроллер по кормарку — это `ATmega2560` с результатом `4.25` кормарка. Т.е. наш процессор по производительности схож с микроконтроллерами Arduino. + +Есть ли здесь еще что-нибудь интересное? Посмотрем в верх таблицы, мы можем увидеть производителя Intel с их микропроцессором [Intel 80286](https://ru.wikipedia.org/wiki/Intel_80286). Как написано на вики, данный микропроцессор был в 3-6 раз производительней [Intel 8086](https://ru.wikipedia.org/wiki/Intel_8086), который соперничал по производительности с процессором [Zilog Z80](https://en.wikipedia.org/wiki/Zilog_Z80), который устанавливался в домашний компьютер [TRS-80](https://en.wikipedia.org/wiki/TRS-80). + +А знаете с чем был сопоставим по производительности компьютер TRS-80? С бортовым компьютером [Apollo Guidance Computer](https://en.wikipedia.org/wiki/Apollo_Guidance_Computer), который проводил вычисления и контролировал движение, навигацию, и управлял командным и лунным модулями в ходе полётов по программе Аполлон. + +Иными словами, мы разработали процессор, который приблизительно в 7-14 раз производительнее компьютера, управлявшего полетом космического корабля, который доставившем человека на луну! + +Можно ли как-то улучшить наш результат? Безусловно. Мы можем улучшить его примерно на 5% изменив буквально одну строчку. Дело в том, что для простоты реализации, мы генерировали сигнал `stall` для каждой операции обращения в память. Однако приостанавливать работу процессора было необходимо только для операций чтения из памяти. Если не генерировать сигнал `stall` для операций типа `store`, мы уменьшим время, необходимое на исполнение бенчмарка. + +Добавление умножителей, конвейеризация и множество других потенциальных улучшений увеличат производительность в разы. + +Но это, как говорится, уже другая история. + +
diff --git a/Labs/15. Coremark/linker_script.ld b/Labs/15. Coremark/linker_script.ld new file mode 100644 index 00000000..3e6de779 --- /dev/null +++ b/Labs/15. Coremark/linker_script.ld @@ -0,0 +1,197 @@ +OUTPUT_FORMAT("elf32-littleriscv") /* Указываем порядок следования байт */ + +ENTRY(_start) /* мы сообщаем компоновщику, что первая + исполняемая процессором инструкция + находится у метки "start" + */ + +_text_size = 0x10000; /* Размер памяти инстр.: 16KiB */ +_data_base_addr = _text_size; /* Стартовый адрес секции данных */ +_data_size = 0x04000; /* Размер памяти данных: 16KiB */ + +_data_end = _data_base_addr + _data_size; + +_trap_stack_size = 2560; /* Размер стека обработчика перехватов. + Данный размер позволяет выполнить + до 32 вложенных вызовов при обработке + перехватов. + */ + +_stack_size = 1280; /* Размер программного стека. + Данный размер позволяет выполнить + до 16 вложенных вызовов. + */ + +/* + В данном разделе указывается структура памяти: + Сперва идет регион "rom", являющийся read-only памятью с исполняемым кодом + (об этом говорят атрибуты 'r' и 'x' соответственно). Этот регион начинается + с адреса 0x00000000 и занимает _text_size байт. + Далее идет регион "ram", начинающийся с адреса _data_base_addr и занимающий + _data_size байт. Этот регион является памятью, противоположной региону "ram" + (в том смысле, что это не read-only память с исполняемым кодом). +*/ +MEMORY +{ + rom (x) : ORIGIN = 0x00000000, LENGTH = _text_size + ram (!x) : ORIGIN = _data_base_addr, LENGTH = _data_size +} + + +/* + В данном разделе описывается размещение программы в памяти. + Программа разделяется на различные секции: + - секции исполняемого кода программа; + - секции статических переменных и массивов, значение которых должно быть + "вшито" в программу; + и т.п. +*/ + +SECTIONS +{ + PROVIDE( _start = 0x00000000 ); /* Позиция start в памяти + /* + В скриптах компоновщика есть внутренняя переменная, записываемая как '.' + Эта переменная называется "счетчиком адресов". Она хранит текущий адрес в + памяти. + В начале файла она инициализируется нулем. Добавляя новые секции, эта + переменная будет увеличиваться на размер каждой новой секции. + Если при размещении секций не указывается никакой адрес, они будут размещены + по текущему значению счетчика адресов. + Этой переменной можно присваивать значения, после этого, она будет + увеличиваться с этого значения. + Подробнее: + https://home.cs.colorado.edu/~main/cs1300/doc/gnu/ld_3.html#IDX338 + */ + + /* + Следующая команда сообщает, что начиная с адреса, которому в данных момент + равен счетчик адресов (в данный момент, начиная с нуля) будет находиться + секция .text итогового файла, которая состоит из секций .boot, а также всех + секций, начинающихся на .text во всех переданных компоновщику двоичных + файлах. + Дополнительно мы указываем, что данная секция должна быть размещена в + регионе "rom". + */ + .text : {*(.boot) *(.text*)} >rom + + + /* + Поскольку мы не знаем суммарного размера получившейся секции, мы проверяем + что не вышли за границы памяти инструкций и переносим счетчик адресов за + пределы памяти инструкций в область памяти данных. + Дополнительно мы указываем, что данная секция должна быть размещена в + регионе "ram". + */ + ASSERT(. < _text_size, ".text section exceeds instruction memory size") + . = _data_base_addr; + + /* + Следующая команда сообщает, что начиная с адреса, которому в данных момент + равен счетчик адресов (_data_base_addr) будет находиться секция .data + итогового файла, которая состоит из секций всех секций, начинающихся + на .data во всех переданных компоновщику двоичных файлах. + Дополнительно мы указываем, что данная секция должна быть размещена в + регионе "ram". + */ + .data : {*(.*data*)} >ram + + /* + Общепринято присваивать GP значение равное началу секции данных, смещенное + на 2048 байт вперед. + Благодаря относительной адресации со смещением в 12 бит, можно адресоваться + на начало секции данных, а так же по всему адресному пространству вплоть до + 4096 байт от начала секции данных, что сокращает объем требуемых для + адресации инструкций (практически не используются операции LUI, поскольку GP + уже хранит базовый адрес и нужно только смещение). + Подробнее: + https://groups.google.com/a/groups.riscv.org/g/sw-dev/c/60IdaZj27dY/m/s1eJMlrUAQAJ + */ + _gbl_ptr = _data_base_addr + 0x800; + + + /* + Поскольку мы не знаем суммарный размер всех используемых секций данных, + перед размещением других секций, необходимо выравнять счетчик адресов по + 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 + + Дополнительно мы указываем, что данная секция должна быть размещена в + регионе "ram". + */ + _bss_start = .; + .bss : {*(.bss*)} >ram + _bss_end = .; + + + /*================================= + Секция аллоцированных данных завершена, остаток свободной памяти отводится + под программный стек, стек прерываний и (возможно) кучу. В соглашении о + вызовах архитектуры RISC-V сказано, что стек растет снизу вверх, поэтому + наша цель разместить его в самых последних адресах памяти. + Поскольку стеков у нас два, в самом низу мы разместим стек прерываний, а + над ним программный стек. При этом надо обеспечить защиту программного + стека от наложения на него стека прерываний. + Однако перед этим, мы должны убедиться, что под программный стек останется + хотя бы 1280 байт (ничем не обоснованное число, взятое с потолка). + Такое значение обеспечивает до 16 вложенных вызовов (если сохранять только + необерегаемые регистры). + ================================= + */ + + /* Мы хотим гарантировать, что под стек останется как минимум 1280 байт */ + ASSERT(. < (_data_end - _trap_stack_size - _stack_size), + "Program size is too big") + + /* Перемещаем счетчик адресов над стеком прерываний (чтобы после мы могли + использовать его в вызове ALIGN) */ + . = _data_end - _trap_stack_size; + + /* + Размещаем указатель программного стека так близко к границе стека + прерываний, насколько можно с учетом требования о выравнивании адреса + стека до 16 байт. + Подробнее: + https://riscv.org/wp-content/uploads/2015/01/riscv-calling.pdf + */ + _stack_ptr = ALIGN(16) <= _data_end - _trap_stack_size? + ALIGN(16) : ALIGN(16) - 16; + ASSERT(_stack_ptr <= _data_end - _trap_stack_size, "SP exceed memory size") + + /* Перемещаем счетчик адресов в конец памяти (чтобы после мы могли + использовать его в вызове ALIGN) */ + . = _data_end; + + /* + Обычно память имеет размер, кратный 16, но на случай, если это не так, мы + делаем проверку, после которой мы либо остаемся в самом конце памяти (если + конец кратен 16), либо поднимаемся на 16 байт вверх от края памяти, + округленного до 16 в сторону большего значения + */ + _trap_stack_ptr = ALIGN(16) <= _data_end ? ALIGN(16) : ALIGN(16) - 16; + ASSERT(_trap_stack_ptr <= _data_end, "ISP exceed memory size") +} diff --git a/Labs/15. Coremark/startup.S b/Labs/15. Coremark/startup.S new file mode 100644 index 00000000..74f5509e --- /dev/null +++ b/Labs/15. Coremark/startup.S @@ -0,0 +1,135 @@ + .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 mie, t1 + csrw mscratch, t2 + +# Вызов функции main +_main_call: + li a0, 0 # Передача аргументов argc и argv в main. Формально, argc должен + li a1, 0 # быть больше нуля, а argv должен указывать на массив строк, + # нулевой элемент которого является именем исполняемого файла, + # Но для простоты реализации оба аргумента всего лишь обнулены. + # Это сделано для детерминированного поведения программы в случае, + # если будет пытаться использовать эти аргументы. + call main +# Зацикливание после выхода из функции main +_endless_loop: + j _endless_loop + +# Низкоуровневый обработчик прерывания отвечает за: +# * Сохранение и восстановление контекста; +# * Вызов высокоуровневого обработчика с передачей id источника прерывания в +# качестве аргумента. +# В основе кода лежит обработчик из репозитория urv-core: +# https://github.com/twlostow/urv-core/blob/master/sw/common/irq.S +# Из реализации убраны сохранения нереализованных CS-регистров. Кроме того, +# судя по документу приведенному ниже, обычное ABI подразумевает такое же +# сохранение контекста, что и при программном вызове (EABI подразумевает еще +# меньшее сохранение контекста), поэтому нет нужды сохранять весь регистровый +# файл. +# Документ: +# https://github.com/riscv-non-isa/riscv-eabi-spec/blob/master/EABI.adoc +_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) + 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) + + # Выход из обработчика прерывания + mret diff --git a/Labs/15. Coremark/tb_coremark.sv b/Labs/15. Coremark/tb_coremark.sv new file mode 100644 index 00000000..a88e95d6 --- /dev/null +++ b/Labs/15. Coremark/tb_coremark.sv @@ -0,0 +1,132 @@ +module tb_coremark(); + + logic clk10mhz_i; + logic aresetn_i; + logic rx_i; + logic tx_o; + logic clk_i; + logic rst_i; + + assign aresetn_i = !rst_i; + assign clk10mhz_i = clk_i; + + logic rx_busy, rx_valid, tx_busy, tx_valid; + logic [7:0] rx_data, tx_data; + + always #50ns clk_i = !clk_i; + + byte coremark_msg[103]; + integer coremark_cntr; + + initial begin + $timeformat(-9, 2, " ns", 3); + clk_i = 0; + rst_i <= 0; + @(posedge clk_i); + rst_i <= 1; + repeat(2) @(posedge clk_i); + rst_i <= 0; + + dummy_programming(); + + coremark_cntr = 0; + coremark_msg = {32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32}; + forever begin + @(posedge clk_i); + if(rx_valid) begin + if((rx_data == 10) | (rx_data == 13)) begin + $display("%s", coremark_msg); + coremark_cntr = 0; + coremark_msg = {32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32}; + end + else begin + coremark_msg[coremark_cntr] = rx_data; + coremark_cntr++; + end + end + end + end + + initial #500ms $finish(); + riscv_top_asic DUT(.clk10mhz_i, .aresetn_i, .rx_i, .tx_o); + + uart_rx rx( + .clk_i (clk_i ), + .rst_i (rst_i ), + .rx_i (tx_o ), + .busy_o (rx_busy ), + .baudrate_i (17'd115200 ), + .parity_en_i(1'b1 ), + .stopbit_i (1'b1 ), + .rx_data_o (rx_data ), + .rx_valid_o (rx_valid ) +); + +uart_tx tx( + .clk_i (clk_i ), + .rst_i (rst_i ), + .tx_o (rx_i ), + .busy_o (tx_busy ), + .baudrate_i (17'd115200 ), + .parity_en_i(1'b1 ), + .stopbit_i (1'b1 ), + .tx_data_i (tx_data ), + .tx_valid_i (tx_valid ) +); + +task send_data(input byte mem[$]); + for(int i = mem.size()-1; i >=0; i--) begin + tx_data = mem[i]; + tx_valid = 1'b1; + @(posedge clk_i); + tx_valid = 1'b0; + @(posedge clk_i); + while(tx_busy) @(posedge clk_i); + end +endtask + +task rcv_data(input int size); + byte str[57]; + logic [3:0][7:0] size_val; + for(int i = 0; i < size; i++) begin + @(posedge clk_i); + while(!rx_valid)@(posedge clk_i); + str[i] = rx_data; + size_val[3-i] = rx_data; + end + if(size!=4)$display("%s", str); + else $display("%d", size_val); + wait(tx_o); +endtask + +task program_region(input byte mem[$], input logic [3:0][7:0] start_addr); + byte str [4]; + logic [3:0][7:0] size; + size = mem.size(); + if(start_addr) begin + str = {start_addr[0],start_addr[1],start_addr[2],start_addr[3]}; + send_data(str); + end + rcv_data(40); + str = {size[0],size[1],size[2],size[3]}; + send_data(str); + rcv_data(4); + send_data(mem); + rcv_data(57); + +endtask + +task finish_programming(); + send_data({8'd0, 8'd0, 8'd0, 8'd0}); +endtask + +task dummy_programming(); + byte str [4] = {8'd0, 8'd0, 8'd0, 8'd0}; + rcv_data(40); + send_data(str); + rcv_data(4); + rcv_data(57); + send_data(str); +endtask + +endmodule diff --git a/Labs/Made-up modules/lab_12.uart_tx_sb_ctrl.sv b/Labs/Made-up modules/lab_12.uart_tx_sb_ctrl.sv new file mode 100644 index 00000000..9bd0d0f0 --- /dev/null +++ b/Labs/Made-up modules/lab_12.uart_tx_sb_ctrl.sv @@ -0,0 +1,4 @@ +module uart_tx_sb_ctrl(); + + +endmodule diff --git a/Labs/README.md b/Labs/README.md index 4b706b44..e1c49bea 100644 --- a/Labs/README.md +++ b/Labs/README.md @@ -24,6 +24,7 @@ - [12. Периферийные устройства (PU)](#12-периферийные-устройства-pu) - [13. Программирование на языке высокого уровня](#13-программирование-на-языке-высокого-уровня) - [14. Программатор](#14-программатор) + - [15. Оценка производительности](#15-оценка-производительности) ## Полезное @@ -78,7 +79,7 @@ ![../.pic/Labs/labs.png](../.pic/Labs/labs.png) -Курс *Архитектур процессорных систем* включает в себя цикл из 13 лабораторных работ (10 основных + 3 вспомогательных), в течение которых используя язык описания аппаратуры **SystemVerilog** на основе **FPGA** (ПЛИС, программируемая логическая интегральная схема), с нуля, последовательно, создается система, под управлением процессора с архитектурой **RISC-V**, управляющего периферийными устройствами и программируемого на языке высокого уровня **C++**. +Курс *Архитектур процессорных систем* включает в себя цикл из 15 лабораторных работ (10 основных + 5 вспомогательных), в течение которых используя язык описания аппаратуры **SystemVerilog** на основе **FPGA** (ПЛИС, программируемая логическая интегральная схема), с нуля, последовательно, создается система, под управлением процессора с архитектурой **RISC-V**, управляющего периферийными устройствами и программируемого на языке высокого уровня **C++**. Создаваемая система на ПЛИС состоит из: процессора, памяти, контроллера прерываний и контроллеров периферийных устройств. @@ -181,3 +182,9 @@ В рамках данной лабораторной работы мы немного упростим процесс передачи программы: вместо записи в ПЗУ, программатор будет записывать её сразу в память инструкций, минуя загрузчик. ![../.pic/Labs/lab_14_programming_device/fig_04.drawio.png](../.pic/Labs/lab_14_programming_device.drawio.png) + +## 15. Оценка производительности + +На текущий момент мы создали процессорную систему, которая способна взаимодействовать с внешним миром посредством периферийных устройств ввода-вывода и программатора. Однако сложно понять, какое место данная система занимает в ряду уже существующих вычислительных систем. + +Для оценки производительности мы модифицируем процессорную систему, соберем и запустим специализированное ПО, отвечающее за измерение производительности (мы будем использовать программу Coremark). Получив результаты, сравним наш процессор, с существующими.