Në u realizua një kontrollues më shumë-më pak funksional, konkretisht – një mbulesë mbi IP Core nga Quartus, e cila shërben si një kalim në TileLink. Sot në rubrikën "Portimi i RocketChip në një kartelë të panjohur kineze me Cyclone" do të shihni një konsolë funksionale. Procesi u zgjat pak: mendova se do ta nisa shpejt Linux-in dhe do të vazhdoja, por nuk ishte ashtu. Në këtë pjesë propozoj të shikoni procesin e ngarkimit të U-Boot, BBL dhe përpjekjet e ngadaltë të kernelit Linux për tu inicializuar. Por konsola është aty – ajo e U-Boot-it, dhe mjaft e avancuar, me shumë nga ato që prisni nga një konsolë e plotë.
Në pjesën harduerike do të shtohet një kartë SD, e lidhur përmes një interfesi SPI, si dhe UART. Në pjesën softuerike, BootROM do të zëvendësohet me xip në sdboot dhe, konkretisht, do të shtohen fazat e ngarkimit të mëposhtme (në kartelën SD).
Përmirësimi i pjesës harduerike
Pra, detyra: duhet të kalojmë në një bërthamë "të madhe" dhe të lidhim UART (nga Raspberry) dhe adaptorin SD (mënyra e përdorur ishte një kartë nga Catalex me gjashtë pinë: GND, VCC, MISO, MOSI, SCK, CS).
Në thelb, gjithçka ishte mjaft e thjeshtë. Por para se ta kuptoja, u ndjeva paksa i tunduar: pas herës së kaluar mendova se duhet vetëm të përzihem në Sistemi diçka si HasPeripheryUART (dhe në implementimin përkatës), e njëjta gjë për kartelën SD — dhe gjithçka do të ishte gati. Më pas vendosa të shikoj se si është realizuar në një dizajn "serioz". Pra, çfarë kemi këtu nga serioziteti? Arty, duke dukur, nuk i përshtatet — mbetet monstruoz unleahshed.DevKitConfigs. Dhe papritmas zbulova se atje kishte disa overlay-e, të cilat shtohen përmes parametrave me çelësa. Mësoj se ndoshta kjo është shumë fleksibël dhe konfigurueshme, por do më duhet diçka për të filluar… A e keni një të ngjashme, vetëm më të thjeshtë dhe më të paqëndrueshme?.. Aty unë zbulova vera.iofpga.FPGAChip për FPGA-të Microsemi dhe menjëherë fillova të përdorja citata për të provuar të bëja implementimin tim në bazë të saj, për fat të mirë këtu kishte më shumë-më pak tërë "shtrirjen e pllakës sistemike" në një skedar.
Doli se, me të vërtetë, duhet thjesht të shtojmë në System.scala rreshtat
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
...Rreshti në trupin e klasës Sistemi shton informacion rreth frekuencës me të cilën funksionon kjo pjesë e SoC tonë, në skedarin dts. Sa kam kuptuar, DTS/DTB është një equivalent statik i teknologjisë plug-and-play për pajisjet e integruara: struktura e përshkrimit dts kompilohen në një skedar binar dtb dhe dërgohen ngarkuesit në kernel për të mundësuar konfigurimin e duhur të harduerit. E çuditshme, pa rreshtin me tlclock gjithçka sintetizohet mirë, por nuk mund të kompiloj BootROM (të cilin ju kujtoj se tani do të jetë sdboot) — gjatë procesit të kompilimit ai analizon skedarin dts dhe krijon një header me makro TL_CLK, falë të cilit ai do të mund të konfiguronte saktësisht ndarësit e frekuencës për interfesat e jashtme.
Gjithashtu do të kërkohet të bëjmë disa ndryshime në "shtrirje":
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))
// Kartela SD
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
}Rregjistrat e zhveshur, për të qenë të sinqertë, janë shtuar thjesht sipas analogjisë me disa vende të tjera të kodit origjinal. Më shumë gjasa, ata duhet të mbrojnë nga . Ndoshta, në disa blloqe tashmë ka mbrojtjen e saj, por për fillim dua të nisim të paktën "në një nivel të cilësisë". Një pyetje më interesante për mua është – pse MISO dhe MOSI janë lidhur në diferente dq? Ответа я пока так и не нашёл, но, похоже, остальной код рассчитывает именно на такое подключение.
Fizikisht, unë thjesht e vendosa projektin në kontakte të lira në shënjën dhe e ndryshova jumper-in e zgjedhjes së tensionit në 3.3V.
Adaptori SD
Pamja nga lart:

Pamja nga poshtë:

Debugging i pjesës softuerike: mjetet
Për të filluar, le të flasim për mjetet e debugimit në dispozicion dhe kufizimet e tyre.
Minicom
Së pari, do të na nevojitet një mënyrë për të lexuar atë që shkruan ngarkuesi dhe kernel. Për këtë, në Linux (në këtë rast – në atë që është në RaspberryPi) do na nevojitet programi Minicom. Në përgjithësi, çdo program për të punuar me port serial do të bëjë.
Vini re që kur e nisim, emri i pajisjes së portit duhet të jepet si -D /dev/ttyS0 — pas opsionit -D. Dhe informacioni kryesor: për të dalë përdorni Ctrl-A, X. Në fakt, kam pasur një rast kur kjo kombinim nuk funksionoi – atëherë mund të themi nga një sesion tjetër SSH thjesht killall -KILL minicom.
Ka është edhe një veçori tjetër. Konkrëtisht në RaspberryPi ka dy UART, dhe të dy portet mund të jenë tashmë të përshtatura për ndonjë gjë: një për Bluetooth, ndërsa tjetri në mënyrë default jep konsolën e bërthamës. Për fat, ky qëndrim mund të rregullohet. .
Riprogramimi i memories.
Gjatë debugging-ut, për të verifikuar hipotezat, ndonjëherë më është dashur të ngarkoj ngarkuesin. (më falni) në memorien operative direkt nga hosti. Ndoshta, kjo mund të bëhet drejtpërdrejt nga GDB, por në fund e mora rrugën e thjeshtë: kopjova skedarin e nevojshëm në Raspberry, e kalova përmes SSH gjithashtu portin 4444 (telnet nga OpenOCD) dhe përdora komandën load_image.Kur e ekzekutoni, duket sikur gjithçka ka ngecur, por në të vërtetë «nuk fle, thjesht pulson ngadalë»: po ngarkohet skedari, thjesht po e bën me një shpejtësi disa kilobajt në sekondë.
Veçoritë e vendosjes së breakpoint-eve.
Ndoshta, shumë njerëz nuk kanë menduar ndonjëherë për këtë kur debug-ojnë programet e zakonshme, por pikat ndaluese nuk vendosen gjithmonë në mënyrë harduerike. Ndonjëherë vendosja e një breakpoint-i përfshin regjistrimin e përkohshëm të një instrukcioni të veçantë në vendin e nevojshëm direct në kodin e makinës.Për shembull, kështu vepronte komanda standarde b në GDB. Ja se çfarë ndodh:
- nuk mund të vendosni një pikë brenda BootROM, sepse ROM
- mund të vendosni një pikë ndalimi mbi kodin e ngarkuar në memorien operative nga SD karta, por duhet pritur që ai të ngarkohet. Ndryshe, nuk do të nevojitet të riprogramojmë një copë kod, por ngarkuesi do të riprogramojë breakpoint-in tonë.
Jam i sigurt se mund të kërkoni qartë për të përdorur piketat ndaluese harduerike, por ato janë në çdo rast të numëruara.
Ndërrimi i shpejtë i BootROM.
Në fazën fillestare të debugging-ut shpesh shfaqet dëshira për të rregulluar BootROM dhe të provojnë përsëri. Por ka një problem: BootROM është një pjesë e dizajnit, e ngarkuar në FPGA, dhe sintetizimi i saj zgjat disa minuta (dhe kjo pas një kompilimi pothuajse të menjëhershëm të imazhit të BootROM nga C dhe Assembler…). Për fat, në të vërtetë gjithçka është shumë më shpejt: sekuenca e veprimeve është e tillë:
- për të ripjellur bootrom.mif (unë kalova në MIF në vend të HEX, sepse me HEX kam pasur gjithmonë disa probleme, ndërsa MIF është formati i natyrshëm i Altera)
- në Quartus thoni
Processing -> Update Memory Initialization File - në pikën Assembler (në kolonën e majtë të Detyrave) urdhëroni Filloni përsëri.
Për të gjithë këtë - disa dhjetëra sekonda.
Përgatitja e kartës SD.
Këtu gjithçka është relativisht e thjeshtë, por duhet të keni durim dhe rreth 14Gb hapësirë në disk:
git clone https://github.com/sifive/freedom-u-sdk
git submodule update --recursive --init
make.Pas kësaj, duhet të fusni një kartë SD të pastër, për të qenë saktë, që nuk përmban asgjë të nevojshme, dhe të ekzekutoni
sudo make DISK=/dev/sdX format-boot-loader.… ku sdX - pajisja e caktuar për kartën. KUJDES: të dhënat në kartë do të fshihen, të ri-përshkruhen dhe gjithashtu! Nuk është shumë e mirë të bëni të gjithë ndërtimin nga sudo, sepse atëherë të gjitha artefaktet e ndërtimit do të jenë në pronësi të root, dhe ndërtimi do të duhet të bëhet nga sudo përhershëm.
Përfundimisht, del një kartë e shënuar në GPT me katër ndarje, një nga të cilat është FAT me uEnv.txt dhe imazhin e ngarkueshëm në formatin FIT (ai përmban disa nën-imazhe, secili me adresën e ngarkimit të tij), ndarja tjetër - e pastër, e cila pritet të formatohet në Ext4 për Linux. Dy ndarjet e tjera - misterioze:në një qëndron U-Boot (ç offset-i, sa kuptoj, është i koduar në BootROM), në tjetrën, duket se qëndrojnë variablat e tij të mjedisit, por unë ende nuk i përdor.
Niveli i parë, BootROM.
Një urtësi popullore thotë: «Nëse në programim ka vallëzime me tambur, në elektronika është edhe me të sjellë zjarr». Është fjala për atë se një herë thuajse ia vura flakën bordit, duke menduar se «No GND - është niveli më i ulët» (dukshëm, një rezistor në të vërtetë do të ndihmonte…) Fjala është më tepër se nëse duar nuk rriten nga drejtimi i duhur, elektronika vazhdon të ofrojë befasi: duke ngjitur një konektor në bord, nuk arrita kurrë të pin lidhjet normalisht - në video tregojnë se si spina vetë përhapet në të gjithë lidhjen, vetëm vendosni hekurin, ndërsa unë e kam pasur, «se bashkuar» si ka rënë. Ndoshta, mund të mos kishte përshtatur temperatura për hekurin; ndoshta diçka tjetër… Në përgjithësi, duke ri-shikuar se kam pasur një dhjetëzinë kontaktet, u dorëzova dhe fillova debug-imin. Dhe këtu erdhi misteri:lidha RX/TX nga UART-i, ngarko firmware-n - ai shkruan
INIT
CMD0
ERROR.Pra, çdo gjë është logjike - moduli SD nuk e kam lidhur. E rregullojmë situatën, ngarko firmware-n… Dhe heshtje… Çfarë s’kam menduar, ndërsa kutia hapeshin thjesht: një nga pinet e modulit duhej të lidhej me VCC. Në rastin tim, moduli mbështeste 5V për energjimin, kështu që, pa menduar shumë, e lidha kabllin e tërhequr nga moduli në anën e kundërt të bordit. Në fund, konektori i ngjitur keq u deformua dhe просто потерялся контакт 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 në DEMO_END=3078900 (не ищите смысл в конкретном значении — его нет, просто теперь образ помещается на карточку).
Уровень второй, U-Boot
Теперь мы всё ещё «падаем», но оказываемся уже по адресу 0x0000000080089a84. Тут я вынужден признаться: на самом деле, изложение идёт не «со всеми остановками», а частично пишется уже «опосля», поэтому здесь я уже успел подложить правильный dtb-файл от нашего SoC, поправить в настройках HiFive_U-Boot ndryshoren 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 me argumentin 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) cAtëherë:
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 0x82000000Mirë, ne kaluam në një nivel të ri, por akoma e ka problem. Dhe ndonjëherë nxjerr edhe përjashtime. Mund ta shohësh mcause-n, duke kapur kodin në adresën e dhënë. $pc dhe pas si të jesh në trap_entry. Vetë procesori nga U-Boot di të shfaqë vetëm për mcause = 0..4, prandaj përgatituni të bini në cikël për ngarkimin e gabuar. Këtu hyra në konfigurim, fillova të shikoja çfarë kam ndryshuar dhe më kujtohej: aty në conf/rvboot-fit.txt shkruar:
fitfile=image.fit
# më poshtë duhet të përputhet me ato që janë në FIT (ugha)Çfarë do të bëjmë, do t'i sjellim të gjitha skedarët në përputhje, do ta zëvendësojmë komandën e bërthamës në këtë mënyrë, pasi kemi dyshime se SIF0 — kjo është dalja diku përmes PCIe:
-bootargs=console=ttySIF0,921600 debug
+bootargs=console=ttyS0,125200 debugDhe për një ditë të tërë do të ndryshojmë algoritmin e heqjes së hash-it nga SHA-256 në MD5: nuk më nevojitet që të jem kriptografikisht i qëndrueshëm (më shumë kur jemi në debug), dhe kap te njëjtat gabime të integritetit dhe MD5 — është mjaft. Pra, çfarë përfundimi kemi? Ne e kaluam nivelin e mëparshëm dukshëm më shpejt (për shkak të heqjes më të thjeshtë të hash-it) dhe hapëm të ardhshëm:
...
Verifying Hash Integrity ... md5+ OK
Loading loadables from 0x90a5a758 to 0x82000000
libfdt fdt_check_header(): FDT_ERR_BADMAGIC
chosen {
linux,initrd-end = ;
linux,initrd-start = ;
riscv,kernel-end = ;
riscv,kernel-start = ;
bootargs = "debug console=tty0 console=ttyS0,125200 root=/dev/mmcblk0p2 rootwait";
};
libfdt fdt_path_offset() returned FDT_ERR_NOTFOUND
chosen {
linux,initrd-end = ;
linux,initrd-start = ;
riscv,kernel-end = ;
riscv,kernel-start = ;
bootargs = "debug console=tty0 console=ttyS0,125200 root=/dev/mmcblk0p2 rootwait";
};
Loading Kernel Image ... OK
Booting kernel in
3Por ora, orët nuk po lëvizin...
(gdb) x/x 0x0200bff8
0x200bff8: 0x00000000Ups, duket se rregullimi i orës ishte një placebo, megjithëse më dukej se ndihmoi. Jo, sigurisht që duhet ta rregulloj, por le të fillojmë duke rrotulluar duar manualisht dhe të shohim se çfarë ndodh:
0x00000000bff6dbb0 in ?? ()
(gdb) set variable *0x0200bff8=1000000
(gdb) c
Duke vazhduar.
^C
Programi mori sinjalin SIGINT, Prek.
0x00000000bff6dbb0 in ?? ()
(gdb) set variable *0x0200bff8=2000000
(gdb) c
Duke vazhduar.
^C
Programi mori sinjalin SIGINT, Prek.
0x00000000bff6dbb0 in ?? ()
(gdb) set variable *0x0200bff8=3000000
(gdb) c
Duke vazhduar.Ndërkohë...
Loading Kernel Image ... OK
Booting kernel in
3
2
1
0
## Starting application at 0x80000000 ...Jo, do të shkoj të automatizoj orën — ndoshta, mund të mendojë të kalibrojë ndonjëherë timer-in!
Ndërkohë, adresa e instruksionit aktual tregohet diku në
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 80001c52në brenda Berkeley Boot Loader që është bootuar. Personal, më shqetëson përmendja htif — interfaci i hostit, që përdoret për nisjen e lidhur të bërthamës (dmth në bashkëpunim me ARM-in host), unë e parashikoja standalone. Megjithatë, nëse e gjej këtë funksion në burimet, dallohet se gjërat nuk janë aq keq:
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"); }
}
}Misioni: nis orën
Kërkimi i regjistrave në CLINT na çon te
val io = IO(new Bundle {
val rtcTick = Bool(INPUT)
})
val time = RegInit(UInt(0, width = timeWidth))
when (io.rtcTick) { time := time + UInt(1) }I cili lidhet me RTC, ose me MockAON-in misterioz, për të cilin unë fillimisht mendoja: "Çfarë është kjo këtu? E paqartë? E çaktivizojmë!" Sepse edhe tani nuk e kuptoj se çfarë magjie taktesh ndodh atje, kështu që thjesht do ta riimplementoj këtë logjikë në System.scala:
val rtcDivider = RegInit(0.asUInt(16.W)) // për çdo rast do ta mbështes deri në 16GHz, unë jam optimist :)
val mhzInt = p(DevKitFPGAFrequencyKey).toInt
// Supozoni se frekuenca është një numër i plotë megahercash
rtcDivider := Mux(rtcDivider === (mhzInt - 1).U, 0.U, rtcDivider + 1.U)
outer.clintOpt.foreach { clint =>
clint.module.io.rtcTick := rtcDivider === 0.U
}Duke u përpjekur për të hyrë në bërthamën Linux
Këtu tregimi tashmë është shtyrë dhe është bërë pak monotone, prandaj do ta përshkruaj sipërfaqësisht:
BBL supozonte praninë e FDT në adresën 0xF0000000, dhe unë tashmë e kam korrigjuar! Çfarë të bëjmë… Le ta kërkojmë përsëri… Gjetur në HiFive_U-Boot/arch/riscv/lib/boot.c, e zëvendësova me 0x81F00000, e cila është e shënuar në konfigurimin e-ngarkesës U-Boot.
Pastaj BBL u ankua se nuk kishte memorie. Rruga ime të çonte te funksioni mem_prop, që në riscv-pk/machine/fdt.c: nga aty mësova se duhet të shënoj node-n fdt ram si device_type = "memory" — pastaj, ndoshta, do të duhet të rregulloj gjeneruesin e procesorit, por për momentin thjesht do ta shkruaj me dorë — gjithsesi, këtë skedar e kam transferuar manualisht.
Tani kam marrë mesazhin (i dhënë në formë të formatizuar, me kthime karte):
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.Duket se, dhe janë shënuar ashtu siç duhet opsionet riscv,kernel-start dhe riscv,kernel-end në DTB, por janë analizuar zeros. Debugimi query_chosen tregoi se BBL po përpiqej të analizohej një adresë 32-bit, dhe i duhej një <0x0 0xADDR>, dhe vlera e parë duket se është reduktimi më i vogël. E shkrova në seksionin chosen
chosen {
#address-cells = ;
#size-cells = ;
...
}dhe rregullova gjenerimin e vlerave: për të mos e shkruar si elementin e parë. 0x0 Këto 100500 hapa të thjeshtë do të lejojnë lehtësisht dhe thjesht të shohim se si bie pinguini:
Эти 100500 простых шагов позволят легко и просто посмотреть, как падает пингвин:
Teksti i fshehur
Verifikimi i Integritetit të Hash-it ... md5+ OK
Ngarkimi i komponentëve nga 0x90a5a758 në 0x82000000
libfdt fdt_check_header(): FDT_ERR_BADMAGIC
chosen {
linux,initrd-end = ;
linux,initrd-start = ;
riscv,kernel-end = ;
riscv,kernel-start = ;
#address-cells = ;
#size-cells = ;
bootargs = "debug console=tty0 console=ttyS0,125200 root=/dev/mmcblk0p2 rootwait";
stdout-path = "uart0:38400n8";
};
libfdt fdt_path_offset() ktheu FDT_ERR_NOTFOUND
chosen {
linux,initrd-end = ;
linux,initrd-start = ;
riscv,kernel-end = ;
riscv,kernel-start = ;
#address-cells = ;
#size-cells = ;
bootargs = "debug console=tty0 console=ttyS0,125200 root=/dev/mmcblk0p2 rootwait";
stdout-path = "uart0:38400n8";
};
Ngarkimi i Imazhit të Kernel-it ... OK
Bootimi i kernel-it në
3
2
1
0
## Duke nisur aplikacionin në 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: Duke injoroni zonën e memories 0x80000000 - 0x80200000
[ 0.000000] Versioni Linux 4.19.0-sifive-1+ (trosinenko@trosinenko-pc) (gcc version 8.3.0 (Buildroot 2019.02-07449-g4eddd28f99)) #1 SMP Mër 3 Korrik 21:29:21 MSK 2019
[ 0.000000] bootconsole [early0] aktivizuar
[ 0.000000] Ramdisk fillestar në: 0x(____ptrval____) (16777216 bytes)
[ 0.000000] Zonat e memories:
[ 0.000000] DMA32 [mem 0x0000000080200000-0x00000000bfffffff]
[ 0.000000] Normal [mem 0x00000000c0000000-0x00000bffffffffff]
[ 0.000000] Zona e lëvizshme fillon për çdo nod
[ 0.000000] Rrugët e hershme të memories për nodin
[ 0.000000] nodi 0: [mem 0x0000000080200000-0x00000000bfffffff]
[ 0.000000] Konfigurimi i memories për nodin 0 [mem 0x0000000080200000-0x00000000bfffffff]
[ 0.000000] Në nodin 0 totalpages: 261632
[ 0.000000] zona DMA32: 3577 faqe të përdorura për memmap
[ 0.000000] zona DMA32: 0 faqe të rezervuara
[ 0.000000] zona DMA32: 261632 faqe, LIFO batch:63
[ 0.000000] softueri IO TLB: mapuar [mem 0xbb1fc000-0xbf1fc000] (64MB)(emblema shfaqet BBL, ndërsa ajo që ka me etiketat e kohës — është bërthama).
Fatmirësisht, nuk e di si është gjithandej, por në RocketChip, duke u lidhur me debugguer-it përmes JTAG, mund të kapësh trap-at nga kutia — debugguer do të ndalet saktësisht në këtë pikë.
Programi mori sinjal SIGTRAP, dhe rregullimi/kapja e gabimeve.
0xffffffe0000024ca në ?? ()
(gdb) bt
#0 0xffffffe0000024ca në ?? ()
Backtrace ndaluar: çadra e mëparshme identike me këtë çadër (staku i korruptuar?)
(gdb) file work/linux/vmlinux
Një program po debuggueshët tashmë.
A jeni të sigurt që dëshironi të ndryshoni skedarin? (y apo n) y
Leximi i simboleve nga work/linux/vmlinux...përfunduar.
(gdb) bt
#0 0xffffffe0000024ca në setup_smp () në /hdd/trosinenko/fpga/freedom-u-sdk/linux/arch/riscv/kernel/smpboot.c:75
#1 0x0000000000000000 në ?? ()
Backtrace ndaluar: çadra nuk ruajti PC-në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); // < JENI KËTU
}Siç thuhet në një anekdotë të vjetër, CPU nuk u gjet, po ekzekutohet emulimi softuerik. Ose ndoshta jo ekzekutohet. Humbur në bërthamën e vetme të procesorit.
/* The lucky hart to first increment this variable will boot the other cores */
atomic_t hart_lottery;
unsigned long boot_cpu_hartid;Një koment i mirë në linux/arch/riscv/kernel/setup.c — një lloj pikture e gardhit sipas metodës së Tom Sawyer. Përndryshe, sot nuk kishte fitues, çmimi do të kalojë në raundin e ardhshëm…
Në këtë pikë propozoj të përfundoj artikullin e sapo zgjedhur.
Vazhdimi vijon. Atje do të ketë një përballje me një gabim të zgjuar, që arrin të fshihet, nëse i qasesh ngadalë me singlestep-in.
Teksti i skrinimit të ngarkesës (Lidhje e jashtme):
Burimi: habr.com
