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

Часть 3: Почти что грузим Linux с SD-карты на RocketChip В предыдущей части был реализован более-менее работающий контроллер памяти, а точнее — обёртка над 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-адаптер

Вид сверху:

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

Вид снизу:

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

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

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

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).

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

Казалось бы, а что ещё ожидать — суровый 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.

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

Только нужно грузить не 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 <strnlen+12>: lbu a4,0(a5)
0x800cb06c <strnlen+16>: bnez a4,0x800cb078 <strnlen+28>
0x800cb070 <strnlen+20>: sub a0,a5,a0
0x800cb074 <strnlen+24>: ret
0x800cb078 <strnlen+28>: addi a5,a5,1
0x800cb07c <strnlen+32>: j 0x800cb064 <strnlen+8>
0x800cb080 <strdup>: addi sp,sp,-32
0x800cb084 <strdup+4>: sd s0,16(sp)
0x800cb088 <strdup+8>: sd ra,24(sp)
0x800cb08c <strdup+12>: 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=<optimized out>, precision=<optimized out>, flags=<optimized out>) 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 %08lxn», fmt@entry=0x800d4458 «exception code: %d , %s , epc %08x , ra %08lxn», 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 %08lxn», 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 %08lxn», args=args@entry=0x881cc188) at lib/vsprintf.c:717
#5 0x00000000800ccb50 in printf (fmt=fmt@entry=0x800d4458 «exception code: %d , %s , epc %08x , ra %08lxn») at lib/vsprintf.c:792
#6 0x000000008008a9f0 in _exit_trap (regs=<optimized out>, epc=2148315240, code=<optimized out>) at arch/riscv/lib/interrupts.c:92
#7 handle_trap (mcause=<optimized out>, epc=<optimized out>, regs=<optimized out>) 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»

/* 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 <anatoly.trosinenko@gmail.com>
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 <poweroff>:
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 <softfloat_countLeadingZeros8+0x558>
80001c2e: e406 sd ra,8(sp)
80001c30: f7fff0ef jal ra,80001bae <printm>
80001c34: 8522 mv a0,s0
80001c36: 267000ef jal ra,8000269c <finisher_exit>
80001c3a: 00010797 auipc a5,0x10
80001c3e: 41e78793 addi a5,a5,1054 # 80012058 <htif>
80001c42: 639c ld a5,0(a5)
80001c44: c399 beqz a5,80001c4a <poweroff+0x2a>
80001c46: 72c000ef jal ra,80002372 <htif_poweroff>
80001c4a: 45a1 li a1,8
80001c4c: 4501 li a0,0
80001c4e: dc7ff0ef jal ra,80001a14 <send_ipi_many>
80001c52: 10500073 wfi
80001c56: bff5 j 80001c52 <poweroff+0x32>

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

void poweroff(uint16_t code)
{
printm(«Power offrn»);
finisher_exit(code);
if (htif) {
htif_poweroff();
} else {
send_ipi_many(0, IPI_HALT);
while (1) { asm volatile («wfin»); }
}
}

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

Поиск регистров в 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 — этакая покраска забора по методу Тома Сойера. В общем, сегодня победителей почему-то не нашлось, приз переносится на следующий тираж…

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

Продолжение следует. В нём будет бой с хитрой ошибкой, которая успевает спрятаться, если к ней медленно подкрадываться singlestep-ом.

Текстовый скринкаст загрузки (внешняя ссылка):

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

Источник

Оставьте комментарий