RISC-V: RocketChip в неестественной среде обитания

Конфигурирование RocketChip

Недавно на Хабре публиковалась статья о том, как поэкспериментировать с архитектурой RISC-V без затрат на «железо». А что, если сделать подобное на отладочной плате? Помните мемы про генератор игр: штук 20 галочек в стиле «Графика не хуже Кризиса», «Можно грабить корованы» и кнопка «Сгенерировать». Приблизительно так же устроен генератор SoC-ов RocketChip, только там не окно с галочками, а Scala-код и немного ассемблера и Make-файлов. В этой статье я покажу, как просто портировать этот RocketChip с родного для него Xilinx на Altera/Intel.

DISCLAIMER: автор не несёт ответственности за «спаленную» плату — внимательно смотрите, как настраиваете пины, что физически подключаете и т.д. Ну и технику безопасности тоже соблюдайте. Не следует думать, что раз всё подключается по USB, то для человека точно безопасно: как я понял в своих предыдущих экспериментах, даже если работаешь с USB-платой, всё таки не стоит ногой дотрагиваться до батареи отопления, потому что разность потенциалов… Ах да, я и близко не являюсь профессиональным плисоводом или электронщиком — я просто Scala-программист.

Насколько я понимаю, изначальной платформой для отладки RocketChip были ПЛИС Xilinx. Судя по репозиторию, который мы вскоре клонируем, также его портировали на Microsemi. Про использование Altera где-то что-то слышал, но исходников не видел. Как оказалось, это не большая проблема: с момента получения платы и начала изучения репозитория SiFive Freedom до рабочего «беспамятного» (то есть, имеющего только регистры процессора, BootROM и memory-mapped регистры) 32-битного «микроконтроллера» (хотя это уже какой-то наноконтроллер получается…) прошло 3 выходных дня и 4 будних вечера, и потребовалось бы ещё меньше, если бы до меня сразу дошло определить define SYNTHESIS глобально.

Для начала — список материалов в широком смысле слова. Нам понадобится следующий софт:


  • RocketChip — содержит сам процессор и сборочное окружение (в том числе, описывает несколько подрепозиториев). Умеет создавать RocketChip с Rocket core in-order процессором.
  • SiFive Freedom — обвязка для различных отладочных плат — туда мы и будем добавлять поддержку для своей
  • rocket-tools — инструментарий для сборки кода и отладки для 32- и 64-битных архитектур RISC-V
  • openocd-riscv — порт OpenOCD для работы по JTAG с RISC-V ядрами (по крайней мере, RocketChip)
  • IntelliJ IDEA Community для редактирования Scala-кода, которого в RocketChip большинство
  • также могут пригодиться предсобранный toolchain от SiFive, ссылку на который я увидел в уже упоминавшейся статье

Что понадобится из железа:


  • набор от Zeowaa с Aliexpress: плата с Cyclone 4 EP4CE115 и (клон?) USB Blaster
  • RaspberryPi в роли JTAG-отладчика для софт-ядра

Предполагается, что хостовая машина — относительно мощный компьютер с Ubuntu и установленным Quartus Lite 18.

Вообще-то, есть вариант запуска в облаке Amazon на их FPGA-инстансах от той же SiFive, называющийся FireSim, но это же не так интересно, да и светодиоды видны плохо. К тому же, в этом случае потребуется указывать свой ключ API на управляющем инстансе для запуска других виртуалок, а его нужно очень беречь, а то, по слухам, можно однажды проснуться с долгом в десяток тысяч долларов…

Для начала я банально взял тестовый проект read_write_1G от поставщика платы и попробовал добавить в него требуемые исходники. Почему не создал новый? Потому что я новичок, а в этом проекте уже были сопоставлены имена пинов. Итак, нужно откуда-то взять сами исходники. Для этого нам понадобится уже указанный репозиторий freedom (не путать с freedom-e-sdk). Чтобы получить хоть что-то, соберём по инструкции rocket-tools (буквально запуск двух скриптов и много ожидания), а потом запустим

RISCV=$(pwd)/../rocket-tools make -f Makefile.e300artydevkit verilog mcs

Цель verilog сгенерирует нам огромный файл на Verilog с исходниками процессора, а mcs скомпилирует BootROM. Не стоит беспокоиться, что mcs завершается ошибкой — просто у нас нет Xilinx Vivado, поэтому скомпилированный BootROM не удаётся конвертировать в нужный для Vivado формат.

Через пункт меню Quartus Project → Add/Remove Files in Project… добавим freedom/builds/e300artydevkit/sifive.freedom.everywhere.e300artydevkit.E300ArtyDevKitConfig.v, выставляем Top-level entity: E300ArtyDevKitFPGAChip на вкладке General и запускаем компиляцию (возможно, список автодополнения top-level entity появится только после первой компиляции). В итоге получаем тонны ошибок, говорящих нам об отсутствии модулей AsyncResetReg, IOBUF и т.д. Если ошибок нет — значит вы забыли поменять Top-level entity. Если порыться в исходниках, то можно прямо найти файл AsyncResetReg.v, а вот IOBUF — это биндинг к IP core от Xilinx. Для начала добавим в список исходников freedom/rocket-chip/src/main/resources/vsrc/AsyncResetReg.v. И plusarg_reader.v тоже добавим.

Запустим компиляцию и получим уже другую ошибку:

Error (10174): Verilog HDL Unsupported Feature error at plusarg_reader.v(18): system function "$value$plusargs" is not supported for synthesis

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


plusarg_reader.v
// See LICENSE.SiFive for license details.

//VCS coverage exclude_file

// No default parameter values are intended, nor does IEEE 1800-2012 require them (clause A.2.4 param_assignment),
// but Incisive demands them. These default values should never be used.
module plusarg_reader #(parameter FORMAT="borked=%d", DEFAULT=0) (
   output [31:0] out
);

`ifdef SYNTHESIS
assign out = DEFAULT;
`else
reg [31:0] myplus;
assign out = myplus;

initial begin
   if (!$value$plusargs(FORMAT, myplus)) myplus = DEFAULT;
end
`endif

endmodule

Как мы видим, этот модуль, вероятно, вычитывает опции симуляции из командной строки, а при синтезе просто выдаёт значение по умолчанию. Проблема в том, что в нашем проекте не определён define с именем SYNTHESIS. Можно было бы прямо перед ifdef внисать на предыдущей строке `define SYNTHESIS, а потом потратить пол-недели на то, чтобы понять, почему ядро не стартует (и ведь синтезируется при этом, зараза…). Не повторяйте моих ошибок, а просто снова откройте свойства проекта, и на вкладке Compiler settings→Verilog HDL Input определите макрос SYNTHESIS, причём хотя бы в 1, не в  (пустую строку).

Вот! Теперь Quartus ругается на отсутствующие биндинги — самое время настроить проект в Idea и начать портирование.

Говорим Идее Import project, указываем путь к репозиторию freedom, указываем тип проекта sbt, ставим галочки use sbt shell for imports, for builds. Тут и сказочке конец, казалось бы, да вот не находит Идея пол-проекта — все исходники почирканы красным. На основе информации отсюда у меня получился такой порядок действий:


  • открываем окно sbt shell
  • вводим clean
  • нажимаем слева зелёную кнопку Restart
  • после перезапуска sbt вводим первой командой ++2.12.4, переключив тем самым все подпроекты на Scala версии 2.12.4, затем командуем compile
  • нажимаем в Идее Refresh all sbt projects
  • PROFIT!, теперь подсветка работает корректно

Пока что я буду пытаться хоть как-то собрать проект, хотя бы в полу ручном режиме. Кстати, кто-нибудь, подскажите, quartus_ipcreate вообще есть в Lite Edition? IP Variations будем пока создавать вручную, и биндинги будут только на Scala в виде BlackBox.

В таком случае, нас интересует следующая иерархия каталогов:

fpga-shells (подрепозиторий)
   |
   +-src/main/scala
       |
       +- ip/intel        <-- сюда положим биндинги к IP Variations
       |   |
       |   +- Intel.scala
       |
       +- shell/intel     <-- сюда положим описание выводов нашей платы
           |
           +- ZeowaaShell.scala

src/main/scala
   |
   +- everywhere.e300artydevkit <-- здесь лежат "образцы" исходников, которые будем адаптировать
   |
   +- zeowaa/e115         <-- здесь будет жить конкретная реализация "разводки" SoC
       |
       +- Config
       +- FPGAChip
       +- Platform
       +- System

Также нужно добавить Makefile по аналогии с Makefile.e300artydevkit, примерно такой:

Makefile.zeowaa-e115:

# See LICENSE for license details.
base_dir := $(patsubst %/,%,$(dir $(abspath $(lastword $(MAKEFILE_LIST)))))
BUILD_DIR := $(base_dir)/builds/zeowaa-e115
FPGA_DIR := $(base_dir)/fpga-shells/intel
MODEL := FPGAChip
PROJECT := sifive.freedom.zeowaa.e115
export CONFIG_PROJECT := sifive.freedom.zeowaa.e115
export CONFIG := ZeowaaConfig
export BOARD := zeowaa
export BOOTROM_DIR := $(base_dir)/bootrom/xip

rocketchip_dir := $(base_dir)/rocket-chip
sifiveblocks_dir := $(base_dir)/sifive-blocks

include common.mk

Для начала, реализуем этот IOBUF — едва ли это будет сложно. Судя по Scala-коду, это модуль, управляющий физической «ножкой» (шариком?) микросхемы: её можно включить на вход, можно на выход, а можно и вообще отключить. В правой части окна Quartus введём в IP Catalog «IOBUF», и тут же получим компонент с именем ALTIOBUF. Зададим какое-нибудь имя для файла вариации, выберем «As a bidirectional buffer». После этого у нас в проекте появится модуль с именем iobuf:

// ...
module obuf (
    datain,
    oe,
    dataout);

    input   [0:0]  datain;
    input   [0:0]  oe;
    output  [0:0]  dataout;

    wire [0:0] sub_wire0;
    wire [0:0] dataout = sub_wire0[0:0];

    obuf_iobuf_out_d5t  obuf_iobuf_out_d5t_component (
                .datain (datain),
                .oe (oe),
                .dataout (sub_wire0));

endmodule
// ...

Напишем для него blackbox-модуль:

package ip.intel

import chisel3._
import chisel3.core.{Analog, BlackBox}
import freechips.rocketchip.jtag.Tristate

class IOBUF extends BlackBox {
  val io = IO(new Bundle {
    val datain = Input(Bool())
    val dataout = Output(Bool())
    val oe = Input(Bool())

    val dataio = Analog(1.W)
  })

  override def desiredName: String = "iobuf"
}

object IOBUF {
  def apply(a: Analog, t: Tristate): IOBUF = {
    val res = Module(new IOBUF)
    res.io.datain := t.data
    res.io.oe := t.driven

    a <> res.io.dataio

    res
  }
}

Типом Analog опишем верилоговский inout, а метод desiredName позволяет поменять имя класса модуля. Это особенно важно, поскольку мы генерируем биндинг, а не реализацию.

Также нам понадобится BootROM — для этого создадим вариацию ROM: 1-PORT (2048×32-bit words, регистровый только address, создать порт rden). Блок создаём с именем rom, поскольку потом нам придётся написать адаптер на тот интерфейс, который ожидает класс ROMGenerator: rden вместо me и отсутствующий у нас oe (он всё равно привязан к 1):

BootROM.v:

module BootROM(
  input wire [10:0] address,
  input wire clock,
  input wire me,
  input wire oe,
  output wire [31:0] q
);

rom r(
  .address(address),
  .clock(clock),
  .rden(me),
  .q(q)
);

endmodule

Сразу же обнаруживается ещё одна проблема: hex-файлы, генерируемые сборщиком, почему-то оказываются несовместимыми с Quartus-ом. После лёгкого гуглинга на тему Intel HEX файлов (Intel он был задолго до покупки этим самым Интелом Альтеры, насколько я понимаю), приходим к такой команде, конвертирующей бинарные файлы в HEX:

srec_cat -Output builds/zeowaa-e115/xip.hex -Intel builds/zeowaa-e115/xip.bin -Binary -Output_Block_Size 128

Поэтому наш Makefile чуточку преобразится:


Скрытый текст
base_dir := $(patsubst %/,%,$(dir $(abspath $(lastword $(MAKEFILE_LIST)))))
BUILD_DIR := $(base_dir)/builds/zeowaa-e115
FPGA_DIR := $(base_dir)/fpga-shells/intel
MODEL := FPGAChip
PROJECT := sifive.freedom.zeowaa.e115
export CONFIG_PROJECT := sifive.freedom.zeowaa.e115
export CONFIG := ZeowaaConfig
export BOARD := zeowaa
export BOOTROM_DIR := $(base_dir)/bootrom/xip

rocketchip_dir := $(base_dir)/rocket-chip
sifiveblocks_dir := $(base_dir)/sifive-blocks

all: verilog
    $(MAKE) -C $(BOOTROM_DIR) clean romgen || true
    srec_cat -Output $(BUILD_DIR)/xip.hex -Intel $(BUILD_DIR)/xip.bin -Binary -Output_Block_Size 128

include common.mk

Итак, проект в целом синтезируется, теперь самое интересное — отладка. В дом первой пускают кошку, а в микроконтроллер, пожалуй, JTAG. Давайте создадим почти минимальную систему для отладки: BootROM, чтобы грузиться, GPIO, чтобы светодиодами мигать, и JTAG, чтобы понять почему ничего не грузится и не мигает. По аналогии с E300ArtyDevKit создадим пакет и в нём четыре файла. Во-первых,

Config.scala:

class DefaultZeowaaConfig extends Config (
  new WithNBreakpoints(2)        ++
    new WithNExtTopInterrupts(0)   ++
    new WithJtagDTM                ++
    new TinyConfig
)

class Peripherals extends Config((site, here, up) => {
  case PeripheryGPIOKey => List(
    GPIOParams(address = BigInt(0x64002000L), width = 6)
  )
  case PeripheryMaskROMKey => List(
    MaskROMParams(address = 0x10000, name = "BootROM"))
})

class ZeowaaConfig extends Config(
  new Peripherals    ++
    new DefaultZeowaaConfig().alter((site, here, up) => {
      case JtagDTMKey => new JtagDTMConfig (
        idcodeVersion = 2,
        idcodePartNum = 0xe31,
        idcodeManufId = 0x489,
        debugIdleCycles = 5)
    })
)

Описание по большей части скопировано и урезано из E300: мы задаём из чего состоит наше ядро и где оно будет валяться в адресном пространстве. Прошу заметить, что хотя у нас и нет оперативной памяти (так по умолчанию задано в TinyConfig), но адресное пространство есть, причём 32-битное!

Также есть файл, несущий в себе некоторое количество boilerplate.
System.scala:

class System(implicit p: Parameters) extends RocketSubsystem
  with HasPeripheryMaskROMSlave
  with HasPeripheryDebug
  with HasPeripheryGPIO
{
  override lazy val module = new SystemModule(this)
}

class SystemModule[+L <: System](_outer: L)
  extends RocketSubsystemModuleImp(_outer)
    with HasPeripheryDebugModuleImp
    with HasPeripheryGPIOModuleImp
{
  // Reset vector is set to the location of the mask rom
  val maskROMParams = p(PeripheryMaskROMKey)
  global_reset_vector := maskROMParams(0).address.U
}

Собственно, разводка нашей «системной платы» находится в трёх (пока что) несложных файлах:
Platform.scala:

class PlatformIO(implicit val p: Parameters) extends Bundle {
  val jtag = Flipped(new JTAGIO(hasTRSTn = false))
  val jtag_reset = Input(Bool())
  val gpio = new GPIOPins(() => new BasePin(), p(PeripheryGPIOKey)(0))
}

class Platform(implicit p: Parameters) extends Module {
  val sys = Module(LazyModule(new System).module)
  override val io = IO(new PlatformIO)

  val sjtag = sys.debug.systemjtag.get
  sjtag.reset := io.jtag_reset
  sjtag.mfr_id := p(JtagDTMKey).idcodeManufId.U(11.W)
  sjtag.jtag <> io.jtag
  io.gpio <> sys.gpio.head
}

FPGAChip.scala:

class FPGAChip(override implicit val p: Parameters) extends ZeowaaShell {
  withClockAndReset(cpu_clock, cpu_rst) {
    val dut = Module(new Platform)

    dut.io.jtag.TCK := jtag_tck
    dut.io.jtag.TDI := jtag_tdi
    IOBUF(jtag_tdo, dut.io.jtag.TDO)
    dut.io.jtag.TMS := jtag_tms
    dut.io.jtag_reset := jtag_rst

    Seq(led_0, led_1, led_2, led_3) zip dut.io.gpio.pins foreach {
      case (led, pin) =>
        led := Mux(pin.o.oe, pin.o.oval, false.B)
    }

    dut.io.gpio.pins.foreach(_.i.ival := false.B)
    dut.io.gpio.pins(4).i.ival := key1
    dut.io.gpio.pins(5).i.ival := key2
  }
}

Как видите, на Chisel можно писать генераторы в функциональном стиле (здесь показан очень простой случай). Но можно писать и явно каждый провод:


ZeowaaShell.scala
object ZeowaaShell {
  class MemIf extends Bundle {
    val mem_addr = IO(Analog(14.W))
    val mem_ba = IO(Analog(3.W))
    val mem_cas_n = IO(Analog(1.W))
    val mem_cke = IO(Analog(2.W))
    val mem_clk = IO(Analog(2.W))
    val mem_clk_n = IO(Analog(2.W))
    val mem_cs_n = IO(Analog(2.W))
    val mem_dm = IO(Analog(8.W))
    val mem_dq = IO(Analog(64.W))
    val mem_dqs = IO(Analog(8.W))
    val mem_odt = IO(Analog(2.W))
    val mem_ras_n = IO(Analog(1.W))
    val mem_we_n = IO(Analog(1.W))
  }
}

class ZeowaaShell(implicit val p: Parameters) extends RawModule {
  val clk25 = IO(Input(Clock()))
  val clk27 = IO(Input(Clock()))
  val clk48 = IO(Input(Clock()))

  val key1 = IO(Input(Bool()))
  val key2 = IO(Input(Bool()))
  val key3 = IO(Input(Bool()))

  val led_0 = IO(Output(Bool()))
  val led_1 = IO(Output(Bool()))
  val led_2 = IO(Output(Bool()))
  val led_3 = IO(Output(Bool()))

  val jtag_tdi = IO(Input(Bool()))
  val jtag_tdo = IO(Analog(1.W))
  val jtag_tck = IO(Input(Clock()))
  val jtag_tms = IO(Input(Bool()))

  val uart_rx = IO(Input(Bool()))
  val uart_tx = IO(Analog(1.W))

  // Internal wiring

  val cpu_clock = Wire(Clock())
  val cpu_rst = Wire(Bool())
  val jtag_rst = Wire(Bool())

  withClockAndReset(cpu_clock, false.B) {
    val counter = Reg(UInt(64.W))
    counter := counter + 1.U
    cpu_rst := (counter > 1000.U) && (counter < 2000.U)
    jtag_rst := (counter > 3000.U) && (counter < 4000.U)
  }

  cpu_clock <> clk25
}

Почему это разбито на три файла? Ну, во-первых, так оно было в прототипе:) Логика отделения Shell от FPGAChip, видимо, была в том, что Shell — это описание интерфейса к внешнему миру: какие у нас есть выводу на конкретной плате (и как они будут отображены на выводы микросхемы!), а FPGAChip диктуется тем, что мы в конкретный SoC хотим напихать. Ну, а Platform отделена вполне логично: обратите внимание: ZeowaaShell (а значит, и Platform) — это RawModule, в частности, у них нет неявного clock и reset — это естественно для «разводки платы», но неудобно для работы (и, вероятно, чревато хитрыми ошибками с расплодившимися частотными доменами). Ну, а Platform — это уже обычный чизелевский модуль, в котором можно спокойно описывать регистры и т.д.

Пара слов о том, как настроить JTAG. Поскольку у меня уже была RaspberryPi 3 Model B+, то очевидным решением было как-то попытаться использовать её GPIO. К счастью, всё уже реализовано до нас: в свежем OpenOCD есть описание интерфейса interface/sysfsgpio-raspberrypi.cfg, с помощью которого можно указать отладчику подключаться через колодку (TCK = 11, TMS = 25, TDI = 10, TDO = 9, а GND оставим в качестве упражнения) — распиновка здесь.

Далее, взяв за основу Freedom.cfg из репозитория riscv-tests, я получил следующее:

adapter_khz     10000
source [find interface/sysfsgpio-raspberrypi.cfg]
set _CHIPNAME riscv

jtag newtap $_CHIPNAME cpu -irlen 5 -expected-id 0x20e31913

set _TARGETNAME $_CHIPNAME.cpu
target create $_TARGETNAME riscv -chain-position $_TARGETNAME -rtos riscv

init
halt
echo "Ready for Remote Connections"

Для работы потребуется порт riscv-openocd, собранный под ARM, поэтому халява не прошла вместо предсобранного варианта от SiFive придётся клонировать репозиторий и собрать:

./configure --enable-remote-bitbang --enable-sysfsgpio

Если кто знает, как запустить remote bitbang, то собирать кастомный порт под ARM может и не понадобиться…

В итоге запускаем от рута на Малинке

root@ubuntu:~/riscv-openocd# ./src/openocd -s tcl -f ../riscv.tcl
Open On-Chip Debugger 0.10.0+dev-00614-g998fed1fe-dirty (2019-06-03-10:27)
Licensed under GNU GPL v2
For bug reports, read
        http://openocd.org/doc/doxygen/bugs.html
adapter speed: 10000 kHz
SysfsGPIO nums: tck = 11, tms = 25, tdi = 10, tdo = 9
SysfsGPIO nums: swclk = 11, swdio = 25
Info : auto-selecting first available session transport "jtag". To override use 'transport select '.
Info : SysfsGPIO JTAG/SWD bitbang driver
Info : JTAG and SWD modes enabled
Warn : gpio 11 is already exported
Warn : gpio 25 is already exported
Info : This adapter doesn't support configurable speed
Info : JTAG tap: riscv.cpu tap/device found: 0x20e31913 (mfg: 0x489 (SiFive, Inc.), part: 0x0e31, ver: 0x2)
Info : datacount=1 progbufsize=16
Info : Disabling abstract command reads from CSRs.
Info : Examined RISC-V core; found 1 harts
Info :  hart 0: XLEN=32, misa=0x40001105
Info : Listening on port 3333 for gdb connections
Ready for Remote Connections
Info : Listening on port 6666 for tcl connections
Info : Listening on port 4444 for telnet connections

Предварительно зайдя туда по SSH с пробросом порта 3333, подставив нужный IP:

ssh -L 3333:127.0.0.1:3333 ubuntu@192.168.1.104

Теперь на хосте можно запустить GDB под архитектуру riscv32:

$ ../../rocket-tools/bin/riscv32-unknown-elf-gdb -q
(gdb) target remote :3333
Remote debugging using :3333
warning: No executable has been specified and target does not support
determining executable automatically.  Try using the "file" command.
0x00000000 in ?? ()

Пропустим некоторое количество отладки вслепую, неконетящегося JTAG по причине того, что макрос SYNTHESIS активно используется в основном сгенерированном файле, и перемотаем ситуацию до состояния «JTAG подцепляется, но лампочки не мигают».

Как мы уже видели в Makefile, код для bootrom берётся из файла bootrom/xip/xip.S. Сейчас он выглядит так:

// See LICENSE for license details.
// Execute in place
// Jump directly to XIP_TARGET_ADDR

  .section .text.init
  .option norvc
  .globl _start
_start:
  csrr a0, mhartid
  la a1, dtb
  li t0, XIP_TARGET_ADDR
  jr t0

  .section .rodata
dtb:
  .incbin DEVICE_TREE

Понимаю, что там должен лежать device tree (содержимое dtb-файла), чтобы его прочитала ОС, но какая уж тут ОС без оперативки. Поэтому смело временно заменим его на мигалку лампочками:


Скрытый текст
  .section .text.init
  .option norvc
  .globl _start
_start:
  li a5, 0x64002000
  li a1, 0x0F
  li a2, 0x01
  li a3, 0x30

  li a6, 0x10
  li a7, 0x20

  sw zero, 0x38(a5) // iof_en
  sw a1,   0x08(a5) // output_en
  sw a2,   0x00(a5) // value
  sw a1,   0x14(a5) // drive

  sw a3,   0x04(a5) // input_en

// a0 <- timer
// a1 <- 0x0F
// a2 <- [state]
// a3 <- 0x30
// a4 <- [buttons]
// a5 <- [gpio addr]
// a6 <- 0x10
// a7 <- 0x20

loop:
  li a4, 0x1000
  add a0, a0, a4
  bgtu a0, zero, loop

  lw a4,   0x00(a5) // value
  beq a4, a6, plus
  beq a4, a7, minus
  j store
plus:
  srai a2, a2, 1
  beq a2, zero, pzero
  j store
pzero:
  li a2, 0x08
  j store
minus:
  slli a2, a2, 1
  beq a2, a6, mzero
  j store
mzero:
  li a2, 0x01
store:
  sw a2,  0x0c(a5) // value
  j loop

Этот код развивался итеративно, поэтому прошу меня извинить за странную нумерацию регистров. К тому же, ассемблер RISC-V я, честно говоря, изучал методом «скомпилируем кусочек кода в объектный файл, дизассемблируем, посмотрим». Когда я несколько лет назад читал книжку по электронике, там говорилось о программировании ATTiny на ассемблере. «Вот же скукотища и рутина, наверное» — думал я, но сейчас, видимо, проявился эффект одного шведского магазина: у шкафчика процессора, самостоятельно собранного из запчастей, даже ассемблер кажется родным и интересным. В результате выполнения этого кода зажжённый светодиод должен «бегать» влево или вправо в зависимости от того, какая кнопка нажата.

Запускаем… И ничего: все лампочки горят, на кнопки не реагируют. Подключимся по JTAG: program counter = 0×00000000 — как-то всё грустно. Но по адресу 0x64002000 у нас доступны регистры GPIO:


GPIOCtrlRegs.scala
// See LICENSE for license details.
package sifive.blocks.devices.gpio

object GPIOCtrlRegs {
  val value       = 0x00
  val input_en    = 0x04
  val output_en   = 0x08
  val port        = 0x0c
  val pullup_en   = 0x10
  val drive       = 0x14
  val rise_ie     = 0x18
  val rise_ip     = 0x1c
  val fall_ie     = 0x20
  val fall_ip     = 0x24
  val high_ie     = 0x28
  val high_ip     = 0x2c
  val low_ie      = 0x30
  val low_ip      = 0x34
  val iof_en      = 0x38
  val iof_sel     = 0x3c
  val out_xor     = 0x40
}

Попробуем пошевелить их вручную:

(gdb) set variable *0x64002038=0
(gdb) set variable *0x64002008=0xF
(gdb) set variable *0x64002000=0x1
(gdb) set variable *0x64002014=0xF
(gdb) set variable *0x6400200c=0x1

Таак… Один из светодиодов погас… А если не 0x1, а 0x5… Правильно, теперь светодиоды горят через один. Также стало понятно, что их нужно инвертировать, а в регистр 0×00 писать не нужно — оттуда нужно читать.

(gdb) x/x 0x64002000
0x64002000:     0x00000030
// нажали одну кнопку
(gdb) x/x 0x64002000
0x64002000:     0x00000020
// нажали другую
(gdb) x/x 0x64002000
0x64002000:     0x00000010
// нажали обе
(gdb) x/x 0x64002000
0x64002000:     0x00000000

Отлично, memory-mapped регистры обновляются без запуска процессора, можно не нажимать cont + Ctrl-C каждый раз — мелочь, а приятно.

Но почему мы не крутимся в цикле, а стоим на $pc=0x0000000?

(gdb) x/10i 0x10000
   0x10000:     addi    s1,sp,12
   0x10002:     fsd     ft0,-242(ra)
   0x10006:     srli    a4,a4,0x21
   0x10008:     addi    s0,sp,32
   0x1000a:     slli    t1,t1,0x21
   0x1000c:     lb      zero,-1744(a2)
   0x10010:     nop
   0x10012:     addi    a0,sp,416
   0x10014:     c.slli  zero,0x0
   0x10016:     0x9308

ЭТО ЧТО ЗА ПОКЕМОН??? Я таких инструкций не писал, мне подкинули! Присмотримся поближе:

(gdb) x/10x 0x10000
0x10000:        0xb7270064      0x9305f000      0x13061000      0x93060003
0x10010:        0x13080001      0x93080002      0x23ac0702      0x23a4b700
0x10020:        0x23a0c700      0x23aab700

С другой стороны, что у нас должно там лежать?

$ ../../rocket-tools/bin/riscv32-unknown-elf-objdump -d builds/zeowaa-e115/xip.elf

builds/zeowaa-e115/xip.elf:     формат файла elf32-littleriscv

Дизассемблирование раздела .text:

00010054 <_start>:
   10054:       640027b7                lui     a5,0x64002
   10058:       00f00593                li      a1,15
   1005c:       00100613                li      a2,1
   10060:       03000693                li      a3,48
   10064:       01000813                li      a6,16
   10068:       02000893                li      a7,32
   1006c:       0207ac23                sw      zero,56(a5) # 64002038 <__global_pointer$+0x63ff0770>
   10070:       00b7a423                sw      a1,8(a5)
   10074:       00c7a023                sw      a2,0(a5)
   10078:       00b7aa23                sw      a1,20(a5)
   1007c:       00d7a223                sw      a3,4(a5)
...

Как видим, Quartus честно положил те же слова, что были в файле инициализации, но поменял им endianness. Можно долго гуглить, как это решить, но я же программист, костыли — наше всё, поэтому просто перепишу


BootROM.v
module BootROM(
  input wire [10:0] address,
  input wire clock,
  input wire me,
  input wire oe,
  output wire [31:0] q
);

wire [31:0] q_r;

rom r(
  .address(address),
  .clock(clock),
  .rden(me),
  .q(q_r)
);

assign q[31:24] = q_r[7:0];
assign q[23:16] = q_r[15:8];
assign q[15:8] = q_r[23:16];
assign q[7:0] = q_r[31:24];

endmodule

Так, собираем, запускаем, не светится. Подключаемся по JTAG: $pc честно указывает куда-то внутри цикла, маска горящих лампочек = 4, ах да, нужно выставить output_en: set variable *0x64002008=0xF, нажимаем c (continue) — всё работает! Ой, а мы же его уже выставили. Почему-то, как я ни перебирал порядок записи в регистры, не работает… Поэтому для начала подопрём всё это простеньким костылём, перенеся выставление output_en перед выставлением фактического значения.

Итого на данный момент использование ресурсов:

Total logic elements   17,370 / 114,480 ( 15 % )
Total registers        8357
Total pins             16 / 281 ( 6 % )
Total virtual pins     0
Total memory bits      264,000 / 3,981,312 ( 7 % )
Embedded Multiplier    9-bit elements 4 / 532 ( < 1 % )

Исходники на Chisel

Продолжение следует…

© Habrahabr.ru