Хабрахабр

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

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

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

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

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

Но перед тем, как это осознать, меня немного побросало из стороны в сторону: после предыдущего раза я решил, что снова нужно просто подмешать в System что-то вроде HasPeripheryUART (и в реализацию соответственно), то же для SD-карты — и всё будет готово. В принципе, всё было довольно просто. Так, что у нас тут из серьёзного? Потом я решил посмотреть, а как же оно реализовано в «серьёзном» дизайне. DevKitConfigs. Arty, видимо, не подходит — остаётся монстр unleahshed. Я догадываюсь, что это, наверное, очень гибко и конфигурируемо, но мне бы хоть что-то для начала запустить… А у вас нет такого же, только попроще-покостыльнее?.. Тут-то я и наткнулся на 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
...

Насколько я понимаю, DTS/DTB — это такой статичный аналог технологии plug-and-play для встраиваемых устройств: дерево dts-описания компилируется в бинарный dtb-файл и передаётся загрузчиком ядру, чтобы оно могло правильно настроить аппаратуру. Строчка в теле класса System добавляет информацию о частоте, на которой работает эта часть нашего SoC, в dts-файл. Что интересно, без строчки с 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? Возможно, в некоторых блоках уже есть своя защита, но для начала хочется запустить хотя бы «на качественном уровне». Ответа я пока так и не нашёл, но, похоже, остальной код рассчитывает именно на такое подключение.

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

SD-адаптер

Вид сверху:

Вид снизу:

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

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

Minicom

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

Ну и главная информация: для выхода используйте Ctrl-A, X. Обратите внимание, что при запуске имя устройства порта нужно указывать как -D /dev/ttyS0 — после опции -D. У меня правда был случай, когда эта комбинация не сработала — тогда можно из соседнего сеанса 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 из C и Assembler...). На начальном этапе отладки нередко возникает желание поправить BootROM и попробовать ещё разок. К счастью, на самом деле всё намного быстрее: последовательность действий такая:

  • перегенерировать 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

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

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

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

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

INIT
CMD0
ERROR

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

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

INIT
CMD0
CMD8
ACMD41
CMD58
CMD16
CMD18
LOADING /

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

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

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

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

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

(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) │

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

(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

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

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

(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 %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=<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.

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

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() */

Похоже, вот и виновник: переменная с недвусмысленным названием CONFIG_SYS_INIT_SP_ADDR. $sp имеет то самое некорректное значение, и внутри board_init_f_init_reserve возникает ошибка. В какой-то момент я даже подумал, а может, ну его, допиливать загрузчик под процессор — может, легче чуть поправить процессор? Она определена в файле 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 <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

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

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

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

Кстати, а это вообще нормально, что процессор напрочь виснет, и к нему не может подключиться 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)

И ведь WithNBigCores как раз проставляло его в 1MHz (как, кстати, и было указано в конфиге U-Boot). … ориентируясь на то, что в комментарии было сказано «если не знаете — оставьте 0». В итоге ничего не работает. Но я же, блин, аккуратный и дотошный: там я не знаю, тут 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, она же там и сейчас написана. Почему? Проблема в том, что если для 115200Hz UART-а будет приблизительно то, что нужно, то если нацело поделить 25000000 на 20000000 получится 1, т.е. Но, насколько я понял, интерфейсы, по крайней мере здесь, работают так: код делит частоту аппаратного блока (у меня — везде 25MHz) на целевую, и выставляет получившееся значение в качестве делителя в соответствующий управляющий регистр. Может, это и нормально, но если ограничения выставляют, значит, это кому-нибудь нужно (но это не точно)… В общем, легче проставить и пойти дальше — далеко и, увы, надолго. работать оно будет на 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

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

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>

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

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

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

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

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

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

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.

Отладка query_chosen показала, что BBL пытается парсить 32-битный адрес, а ему попадается пара <0x0 0xADDR>, и первое значение, похоже, младшие разряды. Вроде, и указываются как нужно опции riscv,kernel-start и riscv,kernel-end в DTB, но парсятся нули. Дописал в секцию 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); // < ВЫ НАХОДИТЕСЬ ЗДЕСЬ
}

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

/* 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-ом. Продолжение следует.

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

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»