Часть 3: Почти что грузим Linux с SD-карты на RocketChip

4jfulumeapi32exqv7d6vnseaho.png В предыдущей части был реализован более-менее работающий контроллер памяти, а точнее — обёртка над IP Core из Quartus, являющаяся переходником на TileLink. Сегодня же в рубрике «Портируем RocketChip на малоизвестную китайскую плату с Циклоном» вы увидите работающую консоль. Процесс несколько затянулся: я уже было думал, что сейчас по-быстрому запущу Linux, и пойдём дальше, но не тут то было. В этой части предлагаю посмотреть на процесс запуска U-Boot, BBL, и робкие попытки Linux kernel инициализироваться. Но консоль есть — U-Boot-овская, и довольно-таки продвинутая, имеющая многое из того, что вы ожидаете от полноценной консоли.

В аппаратной части добавится SD-карта, подключённая по интерфейсу SPI, а также UART. В программной части BootROM будет заменён с xip на sdboot и, собственно, добавлены следующие стадии загрузки (на SD-карте).


Допиливание аппаратной части

Итак, задача: нужно перейти на «большое» ядро и подключить UART (от Raspberry) и SD-адаптер (использовалась некая платка от Catalex с шестью пинами: GND, VCC, MISO, MOSI, SCK, CS).

В принципе, всё было довольно просто. Но перед тем, как это осознать, меня немного побросало из стороны в сторону: после предыдущего раза я решил, что снова нужно просто подмешать в System что-то вроде HasPeripheryUART (и в реализацию соответственно), то же для SD-карты — и всё будет готово. Потом я решил посмотреть, а как же оно реализовано в «серьёзном» дизайне. Так, что у нас тут из серьёзного? Arty, видимо, не подходит — остаётся монстр unleahshed.DevKitConfigs. И вдруг обнаружилось, что там повсюду какие-то оверлеи, которые добавляются через параметры по ключам. Я догадываюсь, что это, наверное, очень гибко и конфигурируемо, но мне бы хоть что-то для начала запустить… А у вас нет такого же, только попроще-покостыльнее?… Тут-то я и наткнулся на vera.iofpga.FPGAChip для ПЛИС Microsemi и тут же растащил на цитаты попробовал сделать свою реализацию по аналогии, благо тут более-менее вся «разводка системной платы» в одном файле.

Оказалось, действительно, нужно просто добавить в System.scala строчки

class System(implicit p: Parameters) extends RocketSubsystem
...
  with HasPeripherySPI
  with HasPeripheryUART
...
{
  val tlclock = new FixedClockResource("tlclk", p(DevKitFPGAFrequencyKey))
  ...
}

class SystemModule[+L <: System](_outer: L)
  extends RocketSubsystemModuleImp(_outer)
...
    with HasPeripheryUARTModuleImp
    with HasPeripheryGPIOModuleImp
...

Строчка в теле класса System добавляет информацию о частоте, на которой работает эта часть нашего SoC, в dts-файл. Насколько я понимаю, DTS/DTB — это такой статичный аналог технологии plug-and-play для встраиваемых устройств: дерево dts-описания компилируется в бинарный dtb-файл и передаётся загрузчиком ядру, чтобы оно могло правильно настроить аппаратуру. Что интересно, без строчки с tlclock всё прекрасно синтезируется, но скомпилировать BootROM (напомню, теперь это будет уже sdboot) не получится — в процессе компиляции он парсит dts-файл и создаёт хедер с макросом TL_CLK, благодаря которому он сможет корректно настроить делители частоты для внешних интерфейсов.

Также потребуется немного поправить «разводку»:

Platform.scala:

class PlatformIO(implicit val p: Parameters) extends Bundle {

...

  // UART
  io.uart_tx := sys.uart(0).txd
  sys.uart(0).rxd := RegNext(RegNext(io.uart_rx))

  // SD card
  io.sd_cs := sys.spi(0).cs(0)
  io.sd_sck := sys.spi(0).sck
  io.sd_mosi := sys.spi(0).dq(0).o
  sys.spi(0).dq(0).i := false.B
  sys.spi(0).dq(1).i := RegNext(RegNext(io.sd_miso))
  sys.spi(0).dq(2).i := false.B
  sys.spi(0).dq(3).i := false.B
}

Цепочки регистров, честно говоря, добавлены просто по аналогии с некоторыми другими местами изначального кода. Скорее всего, они должны защищать от метастабильности. Возможно, в некоторых блоках уже есть своя защита, но для начала хочется запустить хотя бы «на качественном уровне». Более интересный для меня вопрос — почему MISO и MOSI висят на разных dq? Ответа я пока так и не нашёл, но, похоже, остальной код рассчитывает именно на такое подключение.

Физически, я просто назначил выводы дизайна на свободные контакты на колодке и переставил джампер выбора напряжения в 3.3V.


SD-адаптер

Вид сверху:


suzfq4lwejcelmwt3rpk5w_min0.jpeg

Вид снизу:


y0__qqavfhkikpsorvmzqhbamlw.jpeg


Отладка программной части: инструменты

Для начала поговорим об имеющихся инструментах отладки и их ограничениях.


Minicom

Во-первых, нам будет нужно как-то читать то, что выводит загрузчик и ядро. Для этого на Linux (в данном случае — на том, что на RaspberryPi) нам потребуется программа Minicom. Вообще говоря, подойдёт любая программа для работы с последовательны портом.

Обратите внимание, что при запуске имя устройства порта нужно указывать как -D /dev/ttyS0 — после опции -D. Ну и главная информация: для выхода используйте Ctrl-A, X. У меня правда был случай, когда эта комбинация не сработала — тогда можно из соседнего сеанса SSH просто сказать killall -KILL minicom.

Есть и ещё одна особенность. Конкретно на RaspberryPi есть два UART, и оба порта могут быть уже для чего-то приспособлены: один для Bluetooth, через другой по умолчанию выводится консоль ядра. К счастью, это поведение можно перенастроить по этому мануалу.


Переписывание памяти

При отладке, для проверки гипотезы мне иногда приходилось загрузить загрузчик (извините) в оперативную память непосредственно с хоста. Может, это можно сделать прямо из GDB, но я в итоге пошёл по простому пути: скопировал на Raspberry необходимый файл, пробросил через SSH также порт 4444 (telnet от OpenOCD) и воспользовался командой load_image. Когда вы её выполняете, кажется что всё зависло, но на самом деле «оно не спит, оно просто медленно моргает»: оно грузит файл, просто делает это со скорость пару килобайт в секунду.


Особенности установки breakpoint-ов

Вероятно, многим об этом не приходилось задумываться при отладке обычных программ, но точки останова не всегда ставятся аппаратно. Иногда постановка breakpoint-а заключается во временном записывании специальной инструкции в нужное место прямо в машинный код. Например, так у меня действовала стандартная команда b в GDB. Вот, что из этого следует:


  • нельзя поставить точку внутри BootROM, потому что ROM
  • поставить точку останова на код, загруженный в оперативку с SD-карты, можно, но нужно дождаться, когда он будет загружен. В противном случае не мы перепишем кусочек кода, а загрузчик перепишет наш breakpoint

Уверен, можно явно попросить использовать аппаратные точки останова, но их в любом случае ограниченное число.


Быстрая подмена BootROM

На начальном этапе отладки нередко возникает желание поправить BootROM и попробовать ещё разок. Но есть проблема: BootROM является частью дизайна, загружаемого в ПЛИС, а его синтез — дело нескольких минут (и это-то после почти мгновенной компиляции самого образа BootROM из C и Assembler…). К счастью, на самом деле всё намного быстрее: последовательность действий такая:


  • перегенерировать bootrom.mif (я перешёл на MIF вместо HEX, потому что с HEX у меня вечно были какие-то проблемы, а MIF — родной Альтеровский формат)
  • в Quartus сказать Processing -> Update Memory Initialization File
  • на пункте Assembler (в левой колонке Tasks) скомандовать Start again

На всё про всё — пара десятков секунд.


Подготовка SD-карты

Тут всё относительно просто, но нужно запастись терпением и около 14Gb места на диске:

git clone https://github.com/sifive/freedom-u-sdk
git submodule update --recursive --init
make

После чего нужно вставить чистую, а точнее, не содержащую ничего нужного, SD-карту, и выполнить

sudo make DISK=/dev/sdX format-boot-loader

… где sdX — устройство, назначенное карте. ВНИМАНИЕ: данные на карте будут удалены, перезаписаны и вообще! Вряд ли стоит делать всю сборку из-под sudo, потому что тогда все артефакты сборки будут принадлежать root, и сборку придётся делать из-под sudo постоянно.

В итоге получается карточка, размеченная в GPT с четырьмя разделами, на одном из которых FAT с uEnv.txt и загружаемым образом в формате FIT (он содержит несколько подобразов, каждый со своим адресом загрузки), другой раздел — чистый, его предполагается отформатировать в Ext4 для Линукса. Ещё два раздела — загадочные: на одном живёт U-Boot (его смещение, насколько я понимаю, зашито в BootROM), на другом, похоже, живут его переменные окружения, но я их пока не использую.


Уровень первый, BootROM

Народная мудрость гласит: «Если в программировании бывают пляски с бубном, то в электронике — ещё и с огнетушителем». Речь даже не о том, что один раз я чуть не спалил плату, решив, что «Ну GND — это же тот же низкий уровень» (видимо, резистор всё-таки не помешал бы…) Речь скорее о том, что если руки растут не оттуда, то электроника не перестаёт приносить сюрпризы: припаивая разъём на плату, я так и не сумел нормально пропаять контакты — на видео показывают, как припой прямо сам растекается по всему соединению, только паяльник приложи, у меня же он «нашлёпывался» как попало. Ну, может, припой не подходил для температуры паяльника, может, ещё что… В общем, увидев, что десяток контактов у меня уже есть, я плюнул, и начал отлаживать. И тут началось загадочное: подключил RX/TX от UART-а, загружаю прошивку — оно пишет

INIT
CMD0
ERROR

Ну, всё логично — модуль SD-карты я не подключил. Исправляем ситуацию, грузим прошивку… И тишина… Чего я только не передумал, а ларчик-то просто открывался: один из выводов модуля нужно было подключить на VCC. В моём случае модуль поддерживал 5V для питания, поэтому я, недолго думая, воткнул провод, тянувшийся от модуля, на противоположную сторону платы. В итоге криво пропаянный разъём перекосился, и просто потерялся контакт UART. facepalm.jpg В общем, «дурная голова ногам покоя не даёт», а кривые руки — голове…

В итоге я увидил в Minicom долгожданное

INIT
CMD0
CMD8
ACMD41
CMD58
CMD16
CMD18
LOADING /

Более того, оно шевелится крутится индикатор загрузки. Прямо вспоминаются школьные годы и неспешная загрузка MinuetOS с дискеты. Разве что дисковод не скрежещет.

Проблема в том, что после сообщения BOOT не происходит ничего. Значит, самое время подключиться через OpenOCD на Raspberry, к нему GDB на хосте, и посмотреть, что же это такое.

Во-первых, подключение с помощью GDB тут же показало, что $pc (program counter, адрес текущей инструкции) улетает в 0x0 — вероятно, это происходит после множественной ошибки. Поэтому, сразу после выдачи сообщения BOOT добавим бесконечный цикл. Это его ненадолго задержит…

diff --git a/bootrom/sdboot/sd.c b/bootrom/sdboot/sd.c
index c6b5ede..bca1b7f 100644
--- a/bootrom/sdboot/sd.c
+++ b/bootrom/sdboot/sd.c
@@ -224,6 +224,8 @@ int main(void)

        kputs("BOOT");

+    while(*(volatile char *)0x10000){}
+
        __asm__ __volatile__ ("fence.i" : : : "memory");
        return 0;
 }

Такой хитрый код используется «для надёжности»: я где-то слышал, что, вроде бы, бесконечный цикл — это Undefined Behavior, а тут компилятор вряд ли догадается (Напоминаю, что по 0x10000 находится BootROM).

Отладчик есть, исходников нет

Казалось бы, а что ещё ожидать — суровый embedded, какие уж тут исходники. Но ведь в той статье автор отлаживал сишный код… Крекс-фекс-пекс:

(gdb) file builds/zeowaa-e115/sdboot.elf
A program is being debugged already.
Are you sure you want to change the file? (y or n) y
Reading symbols from builds/zeowaa-e115/sdboot.elf...done.

Есть исходники!

Только нужно грузить не MIF-файл и не bin, а оригинальную версию в формате ELF.

Теперь можно с энной попытки угадать адрес, где выполнение продолжится (это ещё одна причина, почему компилятор не должен был догадаться, что цикл — бесконечный). Команда

set variable $pc=0xADDR

позволяет поменять значение регистра на ходу (в данном случае — адрес текущей инструкции). С её же помощью можно менять значения, записанные в память (и memory-mapped регистры).

В конечном итоге я пришёл к выводу (не уверен, что правильному), что у нас «образ sd-карты не той системы», и переходить нужно не на самое начало загруженных данных, а на 0x89800 байтов дальше:

diff --git a/bootrom/sdboot/head.S b/bootrom/sdboot/head.S
index 14fa740..2a6c944 100644
--- a/bootrom/sdboot/head.S
+++ b/bootrom/sdboot/head.S
@@ -13,7 +13,7 @@ _prog_start:
   smp_resume(s1, s2)
   csrr a0, mhartid
   la a1, dtb
-  li s1, PAYLOAD_DEST
+  li s1, (PAYLOAD_DEST + 0x89800)
   jr s1

   .section .rodata

Возможно, на этом также сказалось то, что не имея под рукой ненужной карты на 4Gb, я взял на 2Gb и методом тыка заменил в Makefile DEMO_END=11718750 на DEMO_END=3078900 (не ищите смысл в конкретном значении — его нет, просто теперь образ помещается на карточку).


Уровень второй, U-Boot

Теперь мы всё ещё «падаем», но оказываемся уже по адресу 0x0000000080089a84. Тут я вынужден признаться: на самом деле, изложение идёт не «со всеми остановками», а частично пишется уже «опосля», поэтому здесь я уже успел подложить правильный dtb-файл от нашего SoC, поправить в настройках HiFive_U-Boot переменную CONFIG_SYS_TEXT_BASE=0x80089800 (вместо 0x08000000), чтобы адрес загрузки совпадал с фактическим. Загружаем теперь уже карту следующего уровня другой образ:

(gdb) file ../freedom-u-sdk/work/HiFive_U-Boot/u-boot
(gdb) tui en

И видим:

   │304     /*                                               │
   │305      * trap entry                                    │
   │306      */                                              │
   │307     trap_entry:                                      │
   │308         addi sp, sp, -32*REGBYTES                    │
  >│309         SREG x1, 1*REGBYTES(sp)                      │
   │310         SREG x2, 2*REGBYTES(sp)                      │
   │311         SREG x3, 3*REGBYTES(sp)                      │

Причём мы прыгаем между строчками 308 и 309. И неудивительно, учитывая, что в $sp лежит значение 0xfffffffe31cdc0a0. Увы, оно ещё и постоянно «убегает» из-за строчки 307. Поэтому попробуем поставить точку останова на trap_entry, а потом снова перейти на 0x80089800 (точку входа U-Boot), и будем надеяться, что оно не требует правильного выставления регистров перед переходом… Похоже, работает:

(gdb) b trap_entry
Breakpoint 1 at 0x80089a80: file /hdd/trosinenko/fpga/freedom-u-sdk/HiFive_U-Boot/arch/riscv/cpu/HiFive/start.S, line 308.
(gdb) set variable $pc=0x80089800
(gdb) c
Continuing.

Breakpoint 1, trap_entry () at /hdd/trosinenko/fpga/freedom-u-sdk/HiFive_U-Boot/arch/riscv/cpu/HiFive/start.S:308
(gdb) p/x $sp
$4 = 0x81cf950

Так себе указатель стека, прямо скажем: указывает вообще мимо оперативки (если, конечно, у нас ещё нет трансляции адресов, но будем надеяться на простой вариант).

Попробуем заменить указатель на 0x881cf950. В итоге приходим к тому, что handle_trap вызывается и вызывается, при этом уходим в _exit_trap с аргументом epc=2148315240 (в десятичном виде):

(gdb) x/10i 2148315240
   0x800cb068 :     lbu     a4,0(a5)
   0x800cb06c :     bnez    a4,0x800cb078 
   0x800cb070 :     sub     a0,a5,a0
   0x800cb074 :     ret
   0x800cb078 :     addi    a5,a5,1
   0x800cb07c :     j       0x800cb064 
   0x800cb080 : addi    sp,sp,-32
   0x800cb084 :       sd      s0,16(sp)
   0x800cb088 :       sd      ra,24(sp)
   0x800cb08c :      li      s0,0

Ставим breakpoint на strnlen, продолжаем и видим:

(gdb) bt
#0  strnlen (s=s@entry=0x10060000 "", count=18446744073709551615) at lib/string.c:283
#1  0x00000000800cc14c in string (buf=buf@entry=0x881cbd4c "", end=end@entry=0x881cc15c "", s=0x10060000 "", field_width=, precision=, flags=) at lib/vsprintf.c:265
#2  0x00000000800cc63c in vsnprintf_internal (buf=buf@entry=0x881cbd38 "exception code: 5 , ", size=size@entry=1060, fmt=0x800d446e "s , epc %08x , ra %08lx\n", fmt@entry=0x800d4458 "exception code: %d , %s , epc %08x , ra %08lx\n", args=0x881cc1a0,
    args@entry=0x881cc188) at lib/vsprintf.c:619
#3  0x00000000800cca54 in vsnprintf (buf=buf@entry=0x881cbd38 "exception code: 5 , ", size=size@entry=1060, fmt=fmt@entry=0x800d4458 "exception code: %d , %s , epc %08x , ra %08lx\n", args=args@entry=0x881cc188) at lib/vsprintf.c:710
#4  0x00000000800cca68 in vscnprintf (buf=buf@entry=0x881cbd38 "exception code: 5 , ", size=size@entry=1060, fmt=fmt@entry=0x800d4458 "exception code: %d , %s , epc %08x , ra %08lx\n", args=args@entry=0x881cc188) at lib/vsprintf.c:717
#5  0x00000000800ccb50 in printf (fmt=fmt@entry=0x800d4458 "exception code: %d , %s , epc %08x , ra %08lx\n") at lib/vsprintf.c:792
#6  0x000000008008a9f0 in _exit_trap (regs=, epc=2148315240, code=) at arch/riscv/lib/interrupts.c:92
#7  handle_trap (mcause=, epc=, regs=) at arch/riscv/lib/interrupts.c:55
#8  0x0000000080089b10 in trap_entry () at /hdd/trosinenko/fpga/freedom-u-sdk/HiFive_U-Boot/arch/riscv/cpu/HiFive/start.S:343
Backtrace stopped: frame did not save the PC

Похоже, _exit_trap хочет выдать отладочную информацию про произошедшее исключение, но у него не получается. Так, что-то у нас исходники опять не отображаются. set directories ../freedom-u-sdk/HiFive_U-Boot/ О! Теперь отображаются!

Что же, запустим ещё раз, и увидим по стек-трейсу причину исходной проблемы, вызвавшей первую ошибку (mcause == 5). Если я правильно понял, что написано здесь на стр. 37, то это исключение означает Load access fault. Причина, по-видимому, в том, что вот здесь

arch/riscv/cpu/HiFive/start.S:

call_board_init_f:
    li  t0, -16
    li  t1, CONFIG_SYS_INIT_SP_ADDR
    and sp, t1, t0  /* force 16 byte alignment */

#ifdef CONFIG_DEBUG_UART
    jal debug_uart_init
#endif

call_board_init_f_0:
    mv  a0, sp
    jal board_init_f_alloc_reserve
    mv  sp, a0
    jal board_init_f_init_reserve

    mv  a0, zero    /* a0 <-- boot_flags = 0 */
    la t5, board_init_f
    jr t5       /* jump to board_init_f() */

$sp имеет то самое некорректное значение, и внутри board_init_f_init_reserve возникает ошибка. Похоже, вот и виновник: переменная с недвусмысленным названием CONFIG_SYS_INIT_SP_ADDR. Она определена в файле HiFive_U-Boot/include/configs/HiFive-U540.h. В какой-то момент я даже подумал, а может, ну его, допиливать загрузчик под процессор — может, легче чуть поправить процессор? Но потом я увидел, что это больше похоже на артефакт от не до конца за-#if 0-енных настроек под другую конфигурацию памяти, и можно попробовать сделать так:

diff --git a/include/configs/HiFive-U540.h b/include/configs/HiFive-U540.h
index ca89383..245542c 100644
--- a/include/configs/HiFive-U540.h
+++ b/include/configs/HiFive-U540.h
@@ -65,12 +65,9 @@
 #define CONFIG_SYS_SDRAM_BASE  PHYS_SDRAM_0
 #endif
 #if 1
-/*#define CONFIG_NR_DRAM_BANKS 1*/
+#define CONFIG_NR_DRAM_BANKS   1
 #define PHYS_SDRAM_0   0x80000000              /* SDRAM Bank #1 */
-#define PHYS_SDRAM_1   \
-       (PHYS_SDRAM_0 + PHYS_SDRAM_0_SIZE)      /* SDRAM Bank #2 */
-#define PHYS_SDRAM_0_SIZE      0x80000000      /* 2 GB */
-#define PHYS_SDRAM_1_SIZE      0x10000000      /* 256 MB */
+#define PHYS_SDRAM_0_SIZE      0x40000000      /* 1 GB */
 #define CONFIG_SYS_SDRAM_BASE  PHYS_SDRAM_0
 #endif
 /*
@@ -81,7 +78,7 @@
 #define CONSOLE_ARG                            "console=ttyS0,115200\0"

 /* Init Stack Pointer */
-#define CONFIG_SYS_INIT_SP_ADDR                (0x08000000 + 0x001D0000 - \
+#define CONFIG_SYS_INIT_SP_ADDR                (0x80000000 + 0x001D0000 - \
                                        GENERATED_GBL_DATA_SIZE)

 #define CONFIG_SYS_LOAD_ADDR           0xa0000000      /* partway up SDRAM */

В какой-то момент количество костылей технологического крепежа достигло критической отметки. Немного помучавшись, я пришёл к необходимости сделать корректный порт на свою плату. Для этого нужно скопировать и поправить под нашу конфигурацию некоторое количество файлов.


Ну, приблизительно, вот столечко
trosinenko@trosinenko-pc:/hdd/trosinenko/fpga/freedom-u-sdk/HiFive_U-Boot$ git show --name-status
commit 39cd67d59c16ac87b46b51ac1fb58f16f1eb1048 (HEAD -> zeowaa-1gb)
Author: Anatoly Trosinenko 
Date:   Tue Jul 2 17:13:16 2019 +0300

    Initial support for Zeowaa A-E115FB board

M       arch/riscv/Kconfig
A       arch/riscv/cpu/zeowaa-1gb/Makefile
A       arch/riscv/cpu/zeowaa-1gb/cpu.c
A       arch/riscv/cpu/zeowaa-1gb/start.S
A       arch/riscv/cpu/zeowaa-1gb/timer.c
A       arch/riscv/cpu/zeowaa-1gb/u-boot.lds
M       arch/riscv/dts/Makefile
A       arch/riscv/dts/zeowaa-1gb.dts
A       board/Zeowaa/zeowaa-1gb/Kconfig
A       board/Zeowaa/zeowaa-1gb/MAINTAINERS
A       board/Zeowaa/zeowaa-1gb/Makefile
A       board/Zeowaa/zeowaa-1gb/Zeowaa-A-E115FB.c
A       configs/zeowaa-1gb_defconfig
A       include/configs/zeowaa-1gb.h

Подробности можно посмотреть в репозитории.

Как оказалось, на этой SiFive-овской плате регистры некоторых устройств имеют другие адреса. А ещё оказалось, что U-Boot конфигурируется уже знакомым по ядру Linux механизмом Kconfig — например, можно скомандовать make menuconfig, и перед вами появится удобный текстовый интерфейс с показом описаний параметров по ? и т.д. В общем, слепив из описаний двух плат описание третьей, выкинув оттуда всякие пафосные перенастройки PLL (видимо, это как-то связано с управлением с хостового компьютера по PCIe, но это не точно), я получил некоторую прошивку, которая при правильной погоде на Марсе выдавала мне по UART сообщение о том, из какого хеша коммита она собрана, и о том, сколько у меня DRAM (но эту информацию я сам же в хедере и прописал).

Жаль только, что после этого плата обычно переставала отвечать по процессорному JTAG, а загрузка с SD-карты — дело, увы, в моей конфигурации не быстрое. С другой стороны, иногда BootROM выдавал сообщение, что ERROR, не удалось загрузиться, и тут же выскакивал U-Boot. Тут-то до меня и дошло: видимо, после перезагрузки bitstream в ПЛИС память не перетирается, не успевает «растренироваться» и т.д. Короче, можно просто при появлении сообщения LOADING / подключаться отладчиком и командовать set variable $pc=0x80089800, минуя тем самым эту долгую загрузку (конечно, в предположении, что оно в прошлый раз сломалось достаточно рано, и не успело поверх оригинального кода что-то загрузить).

Кстати, а это вообще нормально, что процессор напрочь виснет, и к нему не может подключиться JTAG-отладчик с сообщениями

Error: unable to halt hart 0
Error:   dmcontrol=0x80000001
Error:   dmstatus =0x00030c82

Так, постойте! Я это уже видел! Что-то подобное происходит при дедлоке TileLink, а автору контроллера памяти я как-то не доверяю — сам же писал… Внезапно, после первой же удачной пересборки процессора после редактирования контроллера я увидел:

INIT
CMD0
CMD8
ACMD41
CMD58
CMD16
CMD18
LOADING
BOOT

U-Boot 2018.09-g39cd67d-dirty (Jul 03 2019 - 13:50:33 +0300)

DRAM:  1 GiB
MMC:
BEFORE LOAD ENVBEFORE FDTCONTROLADDRBEFORE LOADADDRIn:    serial
Out:   serial
Err:   serial
Hit any key to stop autoboot:  3

На эту странную строчку перед In: serial не обращайте внимания — это я пытался на виснущем процессоре понять, корректно ли оно работает с environment. Что значит, «Уже десять минут так висит»? Оно хотя бы сумело релоцироваться и перейти к загрузочному меню! Небольшое отступление: хоть U-Boot и грузится в числе первых 2^24 байт с SD-карты, запустившись, он копирует себя куда подальше по адресу, то ли записанному в конфигурационном хедере, то ли просто в старшие адреса оперативной памяти, производит релокацию ELF-символов, и передаёт туда управление. Так вот: похоже, этот уровень прошли и бонусом получили процессор, не виснущий намертво после этого.

Итак, почему не работает таймер? Похоже, часы в принципе почему-то не идут…

(gdb) x/x 0x0200bff8
0x200bff8:      0x00000000

А что, если стрелки вручную покрутить?

(gdb) set variable *0x0200bff8=310000000
(gdb) c

Тогда:

Hit any key to stop autoboot:  0
MMC_SPI: 0 at 0:1 hz 20000000 mode 0

Вывод: часы не идут. Вероятно, из-за этого же и не работает ввод с клавиатуры:

HiFive_U-Boot/cmd/bootmenu.c:

static void bootmenu_loop(struct bootmenu_data *menu,
        enum bootmenu_key *key, int *esc)
{
    int c;

    while (!tstc()) {
        WATCHDOG_RESET();
        mdelay(10);
    }

    c = getc();

    switch (*esc) {
    case 0:
        /* First char of ANSI escape sequence '\e' */
        if (c == '\e') {
            *esc = 1;
            *key = KEY_NONE;
        }
        break;
    case 1:
        /* Second char of ANSI '[' */
        if (c == '[') {
...

Проблема оказалась в том, что я малость перемудрил: я добавил в конфиг процессора ключ:

  case DTSTimebase => BigInt(0)

… ориентируясь на то, что в комментарии было сказано «если не знаете — оставьте 0». И ведь WithNBigCores как раз проставляло его в 1MHz (как, кстати, и было указано в конфиге U-Boot). Но я же, блин, аккуратный и дотошный: там я не знаю, тут 25MHz! В итоге ничего не работает. Убрал свои «улучшения» и…

Hit any key to stop autoboot:  0
MMC_SPI: 0 at 0:1 hz 20000000 mode 0
## Unknown partition table type 0
libfdt fdt_path_offset() returned FDT_ERR_NOTFOUND
** No partition table - mmc 0 **
## Info: input data size = 34 = 0x22
Running uEnv.txt boot2...
## Error: "boot2" not defined
HiFive-Unleashed #

Можно даже вводить команды! Например, немного поковырявшись, можно, наконец, догадаться ввести mmc_spi 1 10000000 0; mmc part, уменьшив частоту SPI с 20MHz до 10MHz. Почему? Ну, в конфиге была написана максимальная частота 20MHz, она же там и сейчас написана. Но, насколько я понял, интерфейсы, по крайней мере здесь, работают так: код делит частоту аппаратного блока (у меня — везде 25MHz) на целевую, и выставляет получившееся значение в качестве делителя в соответствующий управляющий регистр. Проблема в том, что если для 115200Hz UART-а будет приблизительно то, что нужно, то если нацело поделить 25000000 на 20000000 получится 1, т.е. работать оно будет на 25MHz. Может, это и нормально, но если ограничения выставляют, значит, это кому-нибудь нужно (но это не точно)… В общем, легче проставить и пойти дальше — далеко и, увы, надолго. 25MHz — это вам не Core i9.


Вывод консоли
HiFive-Unleashed # env edit mmcsetup
edit: mmc_spi 1 10000000 0; mmc part
HiFive-Unleashed # boot
MMC_SPI: 1 at 0:1 hz 10000000 mode 0

Partition Map for MMC device 0  --   Partition Type: EFI

Part    Start LBA       End LBA         Name
        Attributes
        Type GUID
        Partition GUID
  1     0x00000800      0x0000ffde      "Vfat Boot"
        attrs:  0x0000000000000000
        type:   ebd0a0a2-b9e5-4433-87c0-68b6b72699c7
        type:   data
        guid:   76bd71fd-1694-4ff3-8197-bfa81699c2fb
  2     0x00040800      0x002efaf4      "root"
        attrs:  0x0000000000000000
        type:   0fc63daf-8483-4772-8e79-3d69d8477de4
        type:   linux
        guid:   9f3adcc5-440c-4772-b7b7-283124f38bf3
  3     0x0000044c      0x000007e4      "uboot"
        attrs:  0x0000000000000000
        type:   5b193300-fc78-40cd-8002-e86c45580b47
        guid:   bb349257-0694-4e0f-9932-c801b4d76fa3
  4     0x00000400      0x0000044b      "uboot-env"
        attrs:  0x0000000000000000
        type:   a09354ac-cd63-11e8-9aff-70b3d592f0fa
        guid:   4db442d0-2109-435f-b858-be69629e7dbf
libfdt fdt_path_offset() returned FDT_ERR_NOTFOUND
2376 bytes read in 0 ms
Running uEnv.txt boot2...
15332118 bytes read in 0 ms
## Loading kernel from FIT Image at 90000000 ...
   Using 'config-1' configuration
   Trying 'bbl' kernel subimage
     Description:  BBL/SBI/riscv-pk
     Type:         Kernel Image
     Compression:  uncompressed
     Data Start:   0x900000d4
     Data Size:    74266 Bytes = 72.5 KiB
     Architecture: RISC-V
     OS:           Linux
     Load Address: 0x80000000
     Entry Point:  0x80000000
     Hash algo:    sha256
     Hash value:   28972571467c4ad0cf08a81d9cf92b9dffc5a7cb2e0cd12fdbb3216cf1f19cbd
   Verifying Hash Integrity ... sha256+ OK
## Loading fdt from FIT Image at 90000000 ...
   Using 'config-1' configuration
   Trying 'fdt' fdt subimage
     Description:  unavailable
     Type:         Flat Device Tree
     Compression:  uncompressed
     Data Start:   0x90e9d31c
     Data Size:    6911 Bytes = 6.7 KiB
     Architecture: RISC-V
     Load Address: 0x81f00000
     Hash algo:    sha256
     Hash value:   10b0244a5a9205357772ea1c4e135a4f882409262176d8c7191238cff65bb3a8
   Verifying Hash Integrity ... sha256+ OK
   Loading fdt from 0x90e9d31c to 0x81f00000
   Booting using the fdt blob at 0x81f00000
## Loading loadables from FIT Image at 90000000 ...
   Trying 'kernel' loadables subimage
     Description:  Linux kernel
     Type:         Kernel Image
     Compression:  uncompressed
     Data Start:   0x900123e8
     Data Size:    10781356 Bytes = 10.3 MiB
     Architecture: RISC-V
     OS:           Linux
     Load Address: 0x80200000
     Entry Point:  unavailable
     Hash algo:    sha256
     Hash value:   72a9847164f4efb2ac9bae736f86efe7e3772ab1f01ae275e427e2a5389c84f0
   Verifying Hash Integrity ... sha256+ OK
   Loading loadables from 0x900123e8 to 0x80200000
## Loading loadables from FIT Image at 90000000 ...
   Trying 'ramdisk' loadables subimage
     Description:  buildroot initramfs
     Type:         RAMDisk Image
     Compression:  gzip compressed
     Data Start:   0x90a5a780
     Data Size:    4467411 Bytes = 4.3 MiB
     Architecture: RISC-V
     OS:           Linux
     Load Address: 0x82000000
     Entry Point:  unavailable
     Hash algo:    sha256
     Hash value:   883dfd33ca047e3ac10d5667ffdef7b8005cac58b95055c2c2beda44bec49bd0
   Verifying Hash Integrity ... sha256+ OK
   Loading loadables from 0x90a5a780 to 0x82000000

Окей, мы прошли на новый уровень, но оно всё ещё зависает. А иногда ещё и сыплет эксепшенами. Увидеть mcause можно, подкараулив код по указанному адресу $pc и после si оказаться на trap_entry. Сам обработчик из U-Boot умеет выводить только для mcause = 0…4, поэтому готовьтесь зациклиться на некорректной загрузке. Тут я полез в конфиг, стал смотреть, что же я менял, и вспомнил: там же в conf/rvboot-fit.txt написано:

fitfile=image.fit
# below much match what's in FIT (ugha)

Что же, приведём все файлы в соответствие, заменим командную строку ядра приблизительно так, поскольку есть подозрения, что SIF0 — это вывод куда-то по PCIe:

-bootargs=console=ttySIF0,921600 debug
+bootargs=console=ttyS0,125200 debug

И до кучи поменяем алгоритм хеширования с SHA-256 на MD5: криптостойкости мне не нужно (тем более, при отладке), считается оно жутко долго, а для отлова ошибок целостности при загрузке и MD5 — за глаза. Что же в итоге? Проходить предыдущий уровень мы стали заметно быстрее (за счёт более простого хеширования), и открылся следующий:

...
   Verifying Hash Integrity ... md5+ OK
   Loading loadables from 0x90a5a758 to 0x82000000
libfdt fdt_check_header(): FDT_ERR_BADMAGIC
chosen {
        linux,initrd-end = <0x00000000 0x83000000>;
        linux,initrd-start = <0x00000000 0x82000000>;
        riscv,kernel-end = <0x00000000 0x80a00000>;
        riscv,kernel-start = <0x00000000 0x80200000>;
        bootargs = "debug console=tty0 console=ttyS0,125200 root=/dev/mmcblk0p2 rootwait";
};
libfdt fdt_path_offset() returned FDT_ERR_NOTFOUND
chosen {
        linux,initrd-end = <0x00000000 0x83000000>;
        linux,initrd-start = <0x00000000 0x82000000>;
        riscv,kernel-end = <0x00000000 0x80a00000>;
        riscv,kernel-start = <0x00000000 0x80200000>;
        bootargs = "debug console=tty0 console=ttyS0,125200 root=/dev/mmcblk0p2 rootwait";
};
   Loading Kernel Image ... OK
Booting kernel in
3

Вот только часы не тикают…

(gdb) x/x 0x0200bff8
0x200bff8:      0x00000000

Упс, похоже, исправление хода часов оказалось плацебо, хотя мне тогда и показалось, что помогло. Нет, починить, конечно надо, но давайте для начала покрутим стрелки вручную и посмотрим, что получится:

0x00000000bff6dbb0 in ?? ()
(gdb) set variable *0x0200bff8=1000000
(gdb) c
Continuing.
^C
Program received signal SIGINT, Interrupt.
0x00000000bff6dbb0 in ?? ()
(gdb) set variable *0x0200bff8=2000000
(gdb) c
Continuing.
^C
Program received signal SIGINT, Interrupt.
0x00000000bff6dbb0 in ?? ()
(gdb) set variable *0x0200bff8=3000000
(gdb) c
Continuing.

Тем временем…

   Loading Kernel Image ... OK
Booting kernel in
3
2
1
0
## Starting application at 0x80000000 ...

Нет уж, пойду автоматизировать ход часов —, а то, может, он там таймер калибровать вздумает!

А адрес текущей инструкции тем временем указывает куда-то в

0000000080001c20 :
    80001c20:   1141                    addi    sp,sp,-16
    80001c22:   e022                    sd      s0,0(sp)
    80001c24:   842a                    mv      s0,a0
    80001c26:   00005517                auipc   a0,0x5
    80001c2a:   0ca50513                addi    a0,a0,202 # 80006cf0 
    80001c2e:   e406                    sd      ra,8(sp)
    80001c30:   f7fff0ef                jal     ra,80001bae 
    80001c34:   8522                    mv      a0,s0
    80001c36:   267000ef                jal     ra,8000269c 
    80001c3a:   00010797                auipc   a5,0x10
    80001c3e:   41e78793                addi    a5,a5,1054 # 80012058 
    80001c42:   639c                    ld      a5,0(a5)
    80001c44:   c399                    beqz    a5,80001c4a 
    80001c46:   72c000ef                jal     ra,80002372 
    80001c4a:   45a1                    li      a1,8
    80001c4c:   4501                    li      a0,0
    80001c4e:   dc7ff0ef                jal     ra,80001a14 
    80001c52:   10500073                wfi
    80001c56:   bff5                    j       80001c52 

внутри загрузившегося Berkeley Boot Loader. Лично меня в этом смущает упоминание htif — host interface, используемого для tethered-запуска ядра (то есть в кооперации с хостовым ARM), я-то предполагал standalone. Впрочем, если найти эту функцию в исходниках, то видно, что не всё так плохо:

void poweroff(uint16_t code)
{
  printm("Power off\r\n");
  finisher_exit(code);
  if (htif) {
    htif_poweroff();
  } else {
    send_ipi_many(0, IPI_HALT);
    while (1) { asm volatile ("wfi\n"); }
  }
}


Квест: запусти часы

Поиск регистров в CLINT выводит нас к

    val io = IO(new Bundle {
      val rtcTick = Bool(INPUT)
    })

    val time = RegInit(UInt(0, width = timeWidth))
    when (io.rtcTick) { time := time + UInt(1) }

Который подключается в RTC, либо в загадочном MockAON, про который я изначально рассудил: «Так, что это у нас тут? Непонятно? Отключаем!» Поскольку мне до сих пор непонятно, что это за тактовая магия там творится, поэтому просто перереализую эту логику в System.scala:

  val rtcDivider = RegInit(0.asUInt(16.W)) // на всякий случай поддержу до 16ГГц, я оптимист :)
  val mhzInt = p(DevKitFPGAFrequencyKey).toInt
  // Преположим, частота равна целому числу мегагерц
  rtcDivider := Mux(rtcDivider === (mhzInt - 1).U, 0.U, rtcDivider + 1.U)
  outer.clintOpt.foreach { clint =>
    clint.module.io.rtcTick := rtcDivider === 0.U
  }


Пробираясь к Linux kernel

Тут повествование уже и без того затянулось и стало малость однообразным, поэтому опишу по верхам:

BBL предполагал наличие FDT по адресу 0xF0000000, а я ведь уже исправлял! Ну что же, поищем ещё… Нашёл в HiFive_U-Boot/arch/riscv/lib/boot.c, заменил на 0x81F00000, указанное в конфигурации загрузки U-Boot.

Потом BBL жаловался, что нет памяти. Мой путь лежал в функцию mem_prop, что в riscv-pk/machine/fdt.c: оттуда я узнал, что нужно пометить узел fdt ram как device_type = "memory" — потом, возможно, нужно будет генератор процессора поправить, но пока просто впишу руками — всё равно я этот файл вручную переносил.

Теперь я получил сообщение (приведено в отформатированном виде, с возвратами каретки):

This is bbl's dummy_payload.  To boot a real kernel, reconfigure bbl
with the flag --with-payload=PATH, then rebuild bbl. Alternatively,
bbl can be used in firmware-only mode by adding device-tree nodes
for an external payload and use QEMU's -bios and -kernel options.

Вроде, и указываются как нужно опции riscv,kernel-start и riscv,kernel-end в DTB, но парсятся нули. Отладка query_chosen показала, что BBL пытается парсить 32-битный адрес, а ему попадается пара <0x0 0xADDR>, и первое значение, похоже, младшие разряды. Дописал в секцию chosen

chosen {
      #address-cells = <1>;
      #size-cells = <0>;
      ...
}

и поправил генерацию значений: не дописывать 0x0 первым элементом.

Эти 100500 простых шагов позволят легко и просто посмотреть, как падает пингвин:


Скрытый текст
   Verifying Hash Integrity ... md5+ OK
   Loading loadables from 0x90a5a758 to 0x82000000
libfdt fdt_check_header(): FDT_ERR_BADMAGIC
chosen {
        linux,initrd-end = <0x83000000>;
        linux,initrd-start = <0x82000000>;
        riscv,kernel-end = <0x80a00000>;
        riscv,kernel-start = <0x80200000>;
        #address-cells = <0x00000001>;
        #size-cells = <0x00000000>;
        bootargs = "debug console=tty0 console=ttyS0,125200 root=/dev/mmcblk0p2 rootwait";
        stdout-path = "uart0:38400n8";
};
libfdt fdt_path_offset() returned FDT_ERR_NOTFOUND
chosen {
        linux,initrd-end = <0x83000000>;
        linux,initrd-start = <0x82000000>;
        riscv,kernel-end = <0x80a00000>;
        riscv,kernel-start = <0x80200000>;
        #address-cells = <0x00000001>;
        #size-cells = <0x00000000>;
        bootargs = "debug console=tty0 console=ttyS0,125200 root=/dev/mmcblk0p2 rootwait";
        stdout-path = "uart0:38400n8";
};
   Loading Kernel Image ... OK
Booting kernel in
3
2
1
0
## Starting application at 0x80000000 ...
bbl loader

                SIFIVE, INC.

         5555555555555555555555555
        5555                   5555
       5555                     5555
      5555                       5555
     5555       5555555555555555555555
    5555       555555555555555555555555
   5555                             5555
  5555                               5555
 5555                                 5555
5555555555555555555555555555          55555
 55555           555555555           55555
   55555           55555           55555
     55555           5           55555
       55555                   55555
         55555               55555
           55555           55555
             55555       55555
               55555   55555
                 555555555
                   55555
                     5

           SiFive RISC-V Core IP
[    0.000000] OF: fdt: Ignoring memory range 0x80000000 - 0x80200000
[    0.000000] Linux version 4.19.0-sifive-1+ (trosinenko@trosinenko-pc) (gcc version 8.3.0 (Buildroot 2019.02-07449-g4eddd28f99)) #1 SMP Wed Jul 3 21:29:21 MSK 2019
[    0.000000] bootconsole [early0] enabled
[    0.000000] Initial ramdisk at: 0x(____ptrval____) (16777216 bytes)
[    0.000000] Zone ranges:
[    0.000000]   DMA32    [mem 0x0000000080200000-0x00000000bfffffff]
[    0.000000]   Normal   [mem 0x00000000c0000000-0x00000bffffffffff]
[    0.000000] Movable zone start for each node
[    0.000000] Early memory node ranges
[    0.000000]   node   0: [mem 0x0000000080200000-0x00000000bfffffff]
[    0.000000] Initmem setup node 0 [mem 0x0000000080200000-0x00000000bfffffff]
[    0.000000] On node 0 totalpages: 261632
[    0.000000]   DMA32 zone: 3577 pages used for memmap
[    0.000000]   DMA32 zone: 0 pages reserved
[    0.000000]   DMA32 zone: 261632 pages, LIFO batch:63
[    0.000000] software IO TLB: mapped [mem 0xbb1fc000-0xbf1fc000] (64MB)

(эмблему выводит BBL, а то что с метками времени — ядро).

К счастью, не знаю, как везде, но на RocketChip при подключении отладчика по JTAG можно ловить trap-ы из коробки — отладчик остановится ровно в этой точке.

Program received signal SIGTRAP, Trace/breakpoint trap.
0xffffffe0000024ca in ?? ()
(gdb) bt
#0  0xffffffe0000024ca in ?? ()
Backtrace stopped: previous frame identical to this frame (corrupt stack?)
(gdb) file work/linux/vmlinux
A program is being debugged already.
Are you sure you want to change the file? (y or n) y
Reading symbols from work/linux/vmlinux...done.
(gdb) bt
#0  0xffffffe0000024ca in setup_smp () at /hdd/trosinenko/fpga/freedom-u-sdk/linux/arch/riscv/kernel/smpboot.c:75
#1  0x0000000000000000 in ?? ()
Backtrace stopped: frame did not save the PC

freedom-u-sdk/linux/arch/riscv/kernel/smpboot.c:

void __init setup_smp(void)
{
    struct device_node *dn = NULL;
    int hart;
    bool found_boot_cpu = false;
    int cpuid = 1;

    while ((dn = of_find_node_by_type(dn, "cpu"))) {
        hart = riscv_of_processor_hartid(dn);
        if (hart < 0)
            continue;

        if (hart == cpuid_to_hartid_map(0)) {
            BUG_ON(found_boot_cpu);
            found_boot_cpu = 1;
            continue;
        }

        cpuid_to_hartid_map(cpuid) = hart;
        set_cpu_possible(cpuid, true);
        set_cpu_present(cpuid, true);
        cpuid++;
    }

    BUG_ON(!found_boot_cpu); // < ВЫ НАХОДИТЕСЬ ЗДЕСЬ
}

Как говорилось в старом анекдоте, CPU not found, running software emulation. Ну или не running. Заблудились в единственном ядре процессора.

/* The lucky hart to first increment this variable will boot the other cores */
atomic_t hart_lottery;
unsigned long boot_cpu_hartid;

Хороший комментарий в linux/arch/riscv/kernel/setup.c — этакая покраска забора по методу Тома Сойера. В общем, сегодня победителей почему-то не нашлось, приз переносится на следующий тираж…

На этом предлагаю закончить и без того затянувшуюся статью.

Продолжение следует. В нём будет бой с хитрой ошибкой, кот

© Habrahabr.ru