RISC-V: 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
В принципе, от файла с подобным названием можно было ожидать не синтезируемых конструкций.
// 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 можно писать генераторы в функциональном стиле (здесь показан очень простой случай). Но можно писать и явно каждый провод:
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:
// 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. Можно долго гуглить, как это решить, но я же программист, костыли — наше всё, поэтому просто перепишу
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
Продолжение следует…