Блог

Создание процессора со свободная архитектурой RISC-V. Часть 2.

В предыдущей статье Создание процессора со свободная архитектурой RISC-V. Часть 1. мы рассказали об истории появления, и основных архитектурных решениях микропроцессорной архитектуры со свободным набором комманд RISC-V. Во второй части мы покажем, как можно реализовать FPGA версию микропроцессора RISC-V на языке SystemVerilog. Так-же мы получим в свое распоряжение программы для компиляции ассемблерного кода процессора, и возможность отладки с выводом информации на VGA дисплей и USART консоль. Полученная реализация имеет следующие особенности:

  • CPU*: 5-стадийный конвейер RISC-V, который может выполнять большинство инструкций из набора инструкций RV32I 
  • Шина*: Простой и интуитивно понятный, механизм арбитража, 32-битныая ширина шины адреса и 32-битная ширина шины данных
  • Арбитраж шины*: может быть изменен с помощью определения макросов для облегчения расширения периферийных устройств, DMA, многоядерных реализаций и т. д
  • Интерактивная отладка UART*: поддержка использования Putty на ПК, написанной на C# программы загрузчика через последовательный порт, minicom и другого программного обеспечения для выполнения следующих действий: сброс системы, загрузка программы, просмотр памяти и т. д.
  • Чистая реализация RTL*: Полностью используется SystemVerilog, не используются IP-ядра, легко переносится и эмулируется RAM и ROM, их описание соответствуют описанию на языке Verilog, которые автоматически синтезируются в BLOCK RAM

Структура SOC

pic1

Рисунок выше показывает структуру SoC. Арбитр шины bus_router является центральным элементом SoC, который имеет три ведущих интерфейса и пять подчиненных интерфейсов. Шина, используемая этим SoC, не относится к какому либо стандарту (например, к шине AXI или APB), это простая авторская шина, которая называется naive_bus.

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

Тип перефирииНачальный адресКонечный адрес
ROM Инструкций 0x00000000 0x00007fff
RAM Инструкций 0x00008000 0x00008fff
RAM Данных 0x00010000 0x00010fff
Память VGA 0x00020000 0x00020fff
Память UART 0x00030000 0x00030003

Описание компонентов арбитра памяти.

  • Арбитр с несколькими подчиненными шинами: соответствующий файл naive_bus_router.sv. Адресное пространство каждого подчиненного устройства разделяется, и запросы на чтение и запись шины ведущего устройства направляются на подчиненное устройство. Когда несколько ведущих устройств одновременно получают доступ к подчиненному устройству, конфликтный арбитраж также может выполняться в соответствии с приоритетом ведущего устройства.
  • RV32I Core: соответствующий файл core_top.sv. Включает два основных интерфейса. Один используется для получения инструкций, а другой - для чтения и записи данных.
  • Отладчик UART: соответствующий файл isp_uart.sv. Объедините функцию отладки UART с пользовательским UART. Включает основной интерфейс и дополнительный интерфейс. Основной интерфейс получает команду, отправленную хост-компьютером из UART, на чтение и запись шины. Он может быть использован для онлайн-программирования и онлайн-отладки. Также возможно получать команды от CPU для отправки данных пользователю.
  • Командное ПЗУ: соответствующий файл instr_rom.sv. По умолчанию процессор получает инструкции отсюда. Поток команд фиксируется, когда аппаратный код компилируется и синтезируется, и не может быть изменен во время выполнения. Единственный способ изменить его - отредактировать код в instr_rom.sv, а затем перекомпилировать логику синтеза и программирования ПЛИС. Поэтому instr_rom в основном используется для симуляции. 
  • RAM инструкций: соответствующий файл ram_bus_wrapper.sv. Пользователь использует для этого здесь инструкцию онлайн-программирования isp_uart, затем указывает адрес загрузки здесь, и после сброса SoC ЦПУ начинает выполнять поток инструкций из RAM. 
  • RAM данных: Соответствующий файл ram_bus_wrapper.sv. Храните данные во время выполнения.
  • Память VGA: Соответствующий файл video_ram.sv. На экране отображаются 86 столбцов * 32 строки = 2752 символа. 4096B оперативной памяти разделено на 32 блока, по одному на каждый блок, 128B, и первые 86 байтов для 86 столбцов. На экране отображается символ, соответствующий каждому коду ASCII.

Характеристики процессора

Поддержка: все команды Load, Store, Arithmetic, Logic, Shift, Compare, Jump из набора RV32I.

Не поддерживается: синхронизация, состояние управления, вызов среды и инструкции класса точки останова

Все поддерживаемые инструкции включают в себя: LB, LH, LW, LBU, LHU, SB, SH, SW, ADD, ADDI, SUB, LUI, AUIPC, XOR, XORI, OR, ORI, AND, ANDI, SLL, SLLI, SRL, SRL, SRA, SRAI, SLT, SLTI, SLTU, SLTIU, BEQ, BNE, BLT, BGE, BLTU, BGEU, JAL, JALR

Для набора команд вы можете рассмотреть возможность добавления команд умножения и деления в RV32IM в будущем.

Процессор использует 5-стадийный конвейер. В настоящее время поддерживаются следующие функции конвейера: Forward, Loaduse, Bus wait

Что касается конвейера, характеристики, которые будут добавлены в будущем: Предсказание ветвлений, прерывания

Развертывание SOC для произвольной платы FPGA

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

Создать проекта. После создания проекта, все .sv-файлы из папки ./hardware/RTL/ необходимо добавить в проект.

Изменить верхний уровень проекта. Файл верхнего уровня для SoC - это ./hardware/RTL/soc_top.sv, вам нужно написать файл верхнего уровня для платы, вызвать soc_top и подключить выводы FPGA к soc_top. Ниже приведено описание сигнала для soc_top.

Остается только скомпилировать пример на языке ассемблера, синтезировать и записать полученный дизайн в FPGA

Verilog Code:
  1. module soc_top #(
  2. parameter UART_RX_CLK_DIV = 108, // 50MHz/4/115200Hz=108
  3. parameter UART_TX_CLK_DIV = 434, // 50MHz/1/115200Hz=434
  4. parameter VGA_CLK_DIV = 1
  5. )(
  6. // clock, typically 50MHz, UART_RX_CLK_DIV and UART_TX_CLK_DIV and VGA_CLK_DIV must be modify when clk is not 50MHz
  7. input logic clk,
  8. // debug uart and user uart shared signal
  9. input logic isp_uart_rx,
  10. output logic isp_uart_tx,
  11. // VGA signal
  12. output logic vga_hsync, vga_vsync,
  13. output logic vga_red, vga_green, vga_blue
  14. );

Тестовое программное обеспечение

После того, как оборудование запрограммировано, можно его протестировать, запустив пример Hello World

После синтеза и программирования оборудования, если у вас есть индикатор UART на вашей плате разработки, вы уже можете видеть, что индикатор TX мигает.

Каждое мигание фактически означает отправку строки «Hello», что означает, что CPU выполняет программу по умолчанию в командном ПЗУ. Давайте посмотрим на пример кода на языке ассемблера, отправляющий данные в usart:

ASM Code:
  1. .org 0x0
  2. .global _start
  3. _start:
  4. # Step 1: Let t0 register = 0x00030000, the address of the user_uart peripheral
  5. or t0, zero,zero # t0 Clear
  6. lui t0, 0x00030 # t0 Register high 20bit=0x00020
  7.  
  8. # Step 2: Write hello! to the user_uart peripheral character by character, ie print hello! to uart
  9. print_hello:
  10. ori t1, zero, 0x068 # t1='h'ASCII code
  11. sb t1, (t0) # T1 write t0 address
  12. ori t1, zero, 0x065 # t1='e'ASCII code
  13. sb t1, (t0) # T1 write t0 address
  14. ori t1, zero, 0x06c # t1='l'ASCII code
  15. sb t1, (t0) # T1 write t0 address
  16. ori t1, zero, 0x06c # t1='l'ASCII code
  17. sb t1, (t0) # T1 write t0 address
  18. ori t1, zero, 0x06f # t1='o'ASCII code
  19. sb t1, (t0) # T1 write t0 address
  20. ori t1, zero, 0x00a # t1='\n'ASCII code
  21. sb t1, (t0) # T1 write t0 address
  22.  
  23. # The third step: delay, through the way of large air circulation
  24. lui t2, 0x00c00 # t2 = 0x00c00000
  25. big_loop:
  26. addi t2, t2, -1 # t2 = t2-1
  27. bne t2, zero, big_loop # if t2!=0, jmp to big_loop
  28. jal zero, print_hello # The end of the big loop, jump to print_hello, repeat printing
  29.  

Утилита для компиляции исходного кода:

pic2

Для проверки работоспособности процессорного ядра написано несколько тестовых программ:

Имя файлаОписание
io-test/uart_print.S UART циклически печатает "hello", что является программой в ПЗУ инструкций.
io-test/vga_hello.S Показывает "hello" на VGA экране
calculation-test/Fibonacci.S Рекурсивный метод для вычисления восьмого числа из ряда чисел Фибоначчи
calculation-test/Number2Ascii.S Преобразует числа в строки ASCII, аналогично itoa или sprintf% d в C
calculation-test/QuickSort.S Инициализирует часть данных в оперативной памяти и сортирует их алгоритмом быстрой сортировки
basic-test/big_endian_little_endian.S Проверка, является ли система с прямым или обратным порядком слов (наша с прямым порядком слов)
basic-test/load_store.S Тест чтение и запись в память

Теперь попробуйте заставить SoC запустить программу, которая исполняет алгоритм быстрой сортировки. Для этого надо повторить следующие шаги:

Откройте программу USTCRVSoC-tool.exe

Открыть файл: нажмите кнопку «Open» и перейдите в каталог ./software/asm-code/calculation-test/, чтобы открыть файл программы QuickSort.S.

Сборка: Нажмите на кнопку «Compilation», и вы увидите строку шестнадцатеричных чисел в поле ниже. Это машинный код, полученный после сборки программы.

Программирование: Убедитесь, что FPGA подключена к компьютеру и в него загружено аппаратное обеспечение SoC, затем выберите правильный COM-порт, нажмите на кнопку «Write», если в строке состояния ниже показано «write done», значит ЦПУ уже запустил машинный код.

Просмотр памяти: в этот момент нажмите на кнопку «DUMP memory» справа, чтобы увидеть упорядоченную последовательность. Программа QuickSort сортирует неупорядоченные массивы от -9 до + 9, и каждое число повторяется дважды. Память DUMP по умолчанию не может быть отображена полностью, вы можете установить длину 100, поэтому число байтов в DUMP составляет 0x100 байт, вы можете увидеть полный результат сортировки.

Кроме того, инструмент USTCRVSoC также может просматривать данные порта USART в режиме USER. Пожалуйста, откройте ./software/asm-code/io-test/uart_print.S, скомпилируйте и запишите, вы должны увидеть непрерывную печать строки "hello" в окне просмотра последовательного порта справа.

Теперь вы можете попробовать запустить эти тесты сборки или написать собственную сборку для тестирования. Можно веселиться!

Утилита отладчика для последовательного порта:

pic3

Отладчик UART имеет два режима:

Режим пользователя USER: в этом режиме вы можете получать пользовательские данные печати, отправленные процессором через isp_uart. После того, как FPGA запрограммирован, процессор находится в этом режиме по умолчанию. Посылку "hello" можно увидеть только в этом режиме. Отправив \n в uart, вы можете выйти из режима USER и войти в режим DEBUG.

Режим отладки DEBUG: в этом режиме любые данные, напечатанные ЦП, будут подавлены, и UART больше не будет активно отправлять данные, и порт переходит в командный режим. Команда debug, отправленная пользователем, и полученный ответ заканчиваются символом \n, отправляя «o» или сброс системы может вернуться в режим USER.

Давайте попробуем функцию отладки UART. Введите «o» и нажмите Enter. Вы увидите, что другая сторона отправляет 8-значное шестнадцатеричное число. Этот номер является данными, считанными по адресу 0x00000000 шины SoC, которая является первой инструкцией в ПЗУ инструкций, как показано ниже.

Команды отладчика:

КомандаПримерОтветРезультат
Прочитать адрес 00020000 abcd1234 По адрес 0x00020000 считаны данные 0xabcd1234
Запись адрес 00020004 1276acd0 wr done Записаны данные 0x1276acd0 по адресу 0x00020004
Переключиться в режим USER o user Переключение обратно в режим USER
Сброс r00008000 rst done Выполнение сброса и запуска ЦП с адреса 0x00008000, возврат в режим USER.
Недопустимая команда ^^$aslfdi invalid Отправленная команда не определена

Ну и по традиции видео работы, и исходники:

Проект: https://github.com/Visual-e/USTC-RVSoC