Parte 3: Quase carregando o Linux do cartão SD no RocketChip

Parte 3: Quase carregando o Linux do cartão SD no RocketChip Em parte anterior Foi implementado um controlador de memória que funciona mais ou menos, ou melhor dizendo — uma camada sobre o IP Core do Quartus, que atua como um adaptador para TileLink. Hoje, na seção "Portando o RocketChip para uma placa chinesa pouco conhecida com Cyclone", você verá um console em funcionamento. O processo demorou um pouco mais do que o esperado: eu pensei que rapidamente conseguiria executar o Linux e seguir em frente, mas não foi bem assim. Nesta parte, proponho que olhemos para o processo de inicialização do U-Boot, BBL e as tímidas tentativas de inicialização do kernel Linux. Mas a console existe — é a do U-Boot, e bastante avançada, tendo muito do que você esperaria de um console completo.

Na parte de hardware, será acrescentado um cartão SD conectado via interface SPI, assim como um UART. Na parte de software, o BootROM será substituído por xip para sdboot e, na verdade, as seguintes etapas de inicialização serão adicionadas (no cartão SD).

A finalização da parte de hardware

Então, a tarefa é: precisamos mudar para um núcleo "maior" e conectar o UART (do Raspberry) e um adaptador SD (foi utilizada uma placa da Catalex com seis pinos: GND, VCC, MISO, MOSI, SCK, CS).

Em princípio, tudo foi bastante simples. Mas antes de perceber isso, eu fui um pouco jogado de um lado para o outro: após a última vez, decidi que novamente precisava apenas misturar em System algo como HasPeripheryUART (e na implementação correspondente), o mesmo para o cartão SD — e tudo estaria pronto. Depois, decidi verificar como isso estava implementado em um design "sério". Então, o que temos aqui de sério? Arty, aparentemente, não se encaixa — permanece o monstro unleahshed.DevKitConfigs. E então descobri que havia sobreposições por toda parte, que são adicionadas via parâmetros por chaves. Eu suspeito que isso é, provavelmente, muito flexível e configurável, mas eu gostaria de pelo menos fazer algo funcionar para começar... Você não tem algo semelhante, só que mais simples e improvisado? Foi aqui que eu encontrei vera.iofpga.FPGAChip para FPGA Microsemi e imediatamente comecei a tirar citações, tentei fazer minha implementação por analogia, já que aqui está mais ou menos toda a "distribuição da placa do sistema" em um único arquivo.

Parece que, realmente, preciso apenas adicionar em System.scala as linhas

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

A linha no corpo da classe System adiciona informações sobre a frequência com que esta parte do nosso SoC opera no arquivo dts. Pelo que entendo, DTS/DTB é uma espécie de análogo estático da tecnologia plug-and-play para dispositivos embarcados: a árvore de descrição dts é compilada em um arquivo dtb binário e passada pelo carregador de inicialização ao núcleo, para que ele possa configurar corretamente o hardware. O que é interessante é que, sem a linha com tlclock tudo se sintetiza perfeitamente, mas não será possível compilar o BootROM (lembre-se, agora isso já será sdboot) — no processo de compilação, ele analisa o arquivo dts e cria um cabeçalho com a macro TL_CLK, que permitirá que ele configure corretamente os divisores de frequência para as interfaces externas.

Também será necessário ajustar um pouco a "fiação":

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
}

As cadeias de registradores, para ser honesto, foram adicionadas apenas por analogia com alguns outros lugares do código original. Provavelmente, elas devem proteger contra metastabilidade. É possível que em alguns blocos já haja proteção própria, mas, por enquanto, quero apenas rodar pelo menos "em um nível de qualidade". A pergunta mais interessante para mim é por que MISO e MOSI estão em diferentes dq? Ответа я пока так и не нашёл, но, похоже, остальной код рассчитывает именно на такое подключение.

Fisicamente, simplesmente atribuí os pinos do design aos contatos disponíveis na faixa e movi o jumper de seleção de tensão para 3,3V.

Adaptador SD

Vista superior:

Parte 3: Quase carregando o Linux do cartão SD no RocketChip

Vista inferior:

Parte 3: Quase carregando o Linux do cartão SD no RocketChip

Depuração do software: ferramentas

Para começar, vamos falar sobre as ferramentas de depuração disponíveis e suas limitações.

Minicom

Em primeiro lugar, vamos precisar de alguma forma de ler o que o carregador de inicialização e o núcleo estão imprimindo. Para isso, no Linux (neste caso — no que está no Raspberry Pi) precisaremos do programa Minicom. De modo geral, qualquer programa para trabalhar com porta serial servirá.

Observe que ao iniciar, o nome do dispositivo da porta precisa ser especificado como -D /dev/ttyS0 — após a opção -D. E a informação principal: para sair, use Ctrl-A, X. Na verdade, eu tive um caso em que essa combinação não funcionou — nesse caso, pode-se simplesmente dizer de uma sessão SSH adjacente killall -KILL minicom.

Há mais uma característica. Especificamente no Raspberry Pi, existem duas UARTs, e ambas as portas podem já estar configuradas para alguma função: uma para Bluetooth, enquanto a outra exibe, por padrão, o console do kernel. Felizmente, esse comportamento pode ser reconfigurado. segundo este manual..

Reescrita de memória.

Durante a depuração, para verificar a hipótese, às vezes eu tinha que carregar o carregador (desculpe) na memória RAM diretamente do host. Talvez isso possa ser feito diretamente do GDB, mas eu acabei seguindo o caminho mais simples: copiei o arquivo necessário para o Raspberry e redirecionei pela SSH a porta 4444 (telnet do OpenOCD) e usei o comando load_image.Quando você executa esse comando, parece que tudo congelou, mas na verdade ‘não está parado, só está piscando devagar’: ele está carregando o arquivo, só que está fazendo isso a uma velocidade de alguns kilobytes por segundo.

Particularidades ao definir breakpoints.

Provavelmente, muitos não costumam pensar nisso ao depurar programas comuns, mas os breakpoints nem sempre são configurados via hardware. Às vezes, definir um breakpoint envolve gravar temporariamente uma instrução especial no local desejado diretamente no código de máquina.Por exemplo, esse foi o funcionamento do comando padrão b no GDB. Aqui está o que resulta disso:

  • não é possível colocar um ponto dentro do BootROM, pois o ROM
  • é possível definir um breakpoint no código carregado na RAM a partir do cartão SD, mas é necessário esperar até que ele seja carregado. Caso contrário, não seremos nós a reescrever um pedaço do código, mas o carregador reescreverá nosso breakpoint.

Estou certo de que é possível solicitar explicitamente o uso de breakpoints de hardware, mas eles são limitados de qualquer forma.

Substituição rápida do BootROM.

Na fase inicial da depuração, muitas vezes surge o desejo de corrigir o BootROM e tentar novamente. Mas há um problema: o BootROM é parte do design carregado na FPGA e sua síntese leva alguns minutos (e isso depois da quase instantânea compilação da imagem do BootROM a partir do C e Assembler…). Felizmente, na realidade, tudo muito mais rápido: a sequência de ações é a seguinte:

  • regenerar bootrom.mif (mudei para MIF em vez de HEX, porque com HEX eu sempre tive alguns problemas, e MIF é o formato nativo da Altera)
  • no Quartus, dizer Processing -> Update Memory Initialization File
  • na seção Assembler (na coluna esquerda Tasks), ordenar Start again.

Para tudo isso — alguns dezenas de segundos.

Preparar o cartão SD.

Aqui tudo é relativamente simples, mas você precisará de paciência e cerca de 14GB de espaço em disco:

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

Depois, você precisa inserir um cartão SD limpo, mais precisamente, que não contenha nada útil, e executar

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

… onde sdX — é o dispositivo atribuído ao cartão. ATENÇÃO: os dados no cartão serão deletados, sobrescritos e de fato! É improvável que você deva fazer toda a compilação como sudo, porque então todos os artefatos da compilação pertencerão a root, e a compilação terá que ser feita como sudo constantemente.

No final, você terá um cartão formatado em GPT com quatro partições, uma das quais FAT com uEnv.txt e uma imagem de inicialização no formato FIT (que contém várias sub-imagens, cada uma com seu próprio endereço de inicialização), a outra partição — limpa, e supõe-se que será formatada em Ext4 para Linux. Outras duas partições — misteriosas: uma contém o U-Boot (seu deslocamento, pelo que entendo, está embutido no BootROM), enquanto a outra parece conter suas variáveis de ambiente, mas eu ainda não as estou utilizando.

Nível um, BootROM

A sabedoria popular diz: “Se na programação há danças com um tambor, na eletrônica há também com um extintor de incêndio”. Não se trata apenas de que uma vez quase queimei a placa, pensando que “Bem, GND é o mesmo que um nível baixo” (aparentemente, um resistor não teria feito mal…) O que quero dizer é que se suas mãos não vêm do lugar certo, a eletrônica continua trazendo surpresas: ao soldar um conector na placa, eu não consegui soldar os terminais apropriadamente — em vídeos mostram como a solda se espalha sozinha por toda a conexão, basta encostar o ferro de solda, mas no meu caso, ela "grudava" de forma aleatória. Bem, talvez a solda não fosse adequada para a temperatura do ferro, talvez outra coisa... Enfim, ao ver que já tinha uma dúzia de terminais, desisti e comecei a depurar. E então começou o mistério: conectei RX/TX do UART, carrego o firmware — e aparece

INIT
CMD0
ERROR

Bem, tudo faz sentido — eu não conectei o módulo do cartão SD. Corrigindo a situação, carrego o firmware... E silêncio... Pensei em tantas coisas, mas a caixa simples se abria: um dos pinos do módulo precisava ser conectado ao VCC. No meu caso, o módulo suportava 5V para alimentação, então sem pensar muito, conectei o fio que vinha do módulo ao lado oposto da placa. No final, o conector mal soldado se entortou, e o contato UART simplesmente se perdeu. facepalm.jpg Em geral, uma "cabeça burra não dá descanso aos pés", e mãos desajeitadas a uma cabeça...

No final, vi no Minicom o tão esperado

INIT
CMD0
CMD8
ACMD41
CMD58
CMD16
CMD18
CARREGANDO /

Além disso, o indicador de carregamento está se movendo. Lembro-me dos meus tempos de escola e do carga lenta do MinuetOS a partir de um disquete. A única diferença é que a unidade não está rangendo.

O problema é que, após a mensagem BOOT, nada acontece. Portanto, é hora de conectar via OpenOCD ao Raspberry, GDB ao host e ver o que está acontecendo.

Primeiro, a conexão via GDB imediatamente mostrou que $pc (program counter, o endereço da instrução atual) está indo para 0x0 — provavelmente isso acontece após múltiplos erros. Portanto, logo após a mensagem BOOT vamos adicionar um loop infinito. Isso irá atrasá-lo por um tempo...

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;
 }

Esse código astuto é usado "para segurança": ouvi em algum lugar que, aparentemente, um loop infinito é um Comportamento Indefinido, e aqui o compilador provavelmente não vai perceber (Lembro que 0x10000 é onde se encontra o BootROM).

Parte 3: Quase carregando o Linux do cartão SD no RocketChip

Pode parecer, e o que mais esperar — um embedded rígido, que código fonte temos aqui. Mas na aquele artigo o autor depurou o código C... Crux-fex-pex:

(gdb) file builds/zeowaa-e115/sdboot.elf
Um programa já está sendo depurado.
Tem certeza de que deseja alterar o arquivo? (s ou n) s
Lendo símbolos de builds/zeowaa-e115/sdboot.elf...feito.

Parte 3: Quase carregando o Linux do cartão SD no RocketChip

Só é necessário carregar não o arquivo MIF nem o bin, mas a versão original no formato ELF.

Agora podemos, em uma certa tentativa, adivinhar o endereço onde a execução continuará (esta é mais uma razão pela qual o compilador não deveria ter percebido que o loop é infinito). O comando

set variable $pc=0xADDR

permite mudar o valor do registrador em tempo real (neste caso — o endereço da instrução atual). Com isso, também é possível alterar valores armazenados na memória (e registradores mapeados na memória).

No final, cheguei à conclusão (não tenho certeza se correta) de que temos "a imagem da sd-card não do sistema correto", e que devemos ir não para o início dos dados carregados, mas para 0x89800 bytes à frente:

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

Talvez isso também tenha sido impactado pelo fato de que, não tendo um cartão desnecessário de 4 Gb à mão, peguei um de 2 Gb e, por tentativa e erro, substituí no Makefile DEMO_END=11718750 para DEMO_END=3078900 (não procure sentido no valor específico — ele não existe, só agora a imagem cabe no cartão).

Nível dois, U-Boot

Agora estamos ainda «caindo», mas já chegamos ao endereço 0x0000000080089a84. Aqui eu tenho que admitir: na verdade, a exposição não está indo «com todas as paradas», mas está sendo escrita parcialmente já «depois», então aqui eu consegui já colocar o arquivo dtb correto do nosso SoC, corrigir as configurações HiFive_U-Boot a variável CONFIG_SYS_TEXT_BASE=0x80089800 (em vez de 0x08000000), para que o endereço de carregamento corresponda ao real. Estamos agora carregando um cartão do próximo nível, outra imagem:

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

E vemos:

   │304     
 *                                               │
   │305      * entrada de armadilha                                    │
   │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)                      │

E estamos pulando entre as linhas 308 e 309. E não é de se surpreender, considerando que em $sp está o valor 0xfffffffe31cdc0a0. Infelizmente, ele também está constantemente «fugindo» por causa da linha 307. Então vamos tentar colocar um ponto de parada em trap_entry, e depois ir novamente para 0x80089800 (o ponto de entrada do U-Boot), e vamos torcer para que não exija a configuração correta dos registradores antes da transição... Parece que está funcionando:

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

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

É um ponteiro de pilha ruim, para ser sincero: aponta totalmente para fora da memória RAM (se, é claro, ainda não temos tradução de endereços, mas vamos esperar pela opção simples).

Vamos tentar substituir o ponteiro por 0x881cf950. No fim, chegamos ao ponto em que handle_trap é chamado repetidamente, enquanto vamos para _exit_trap com o argumento epc=2148315240 (em formato decimal):

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

Defina um breakpoint em strnlen, continue e veja:

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

Parece que _exit_trap quer fornecer informações de depuração sobre a exceção ocorrida, mas não consegue. Então, os nossos códigos fontes não estão sendo exibidos novamente. defina diretórios ../freedom-u-sdk/HiFive_U-Boot/ Oh! Agora eles aparecem!

Vamos executar novamente e veremos pelo stack trace a causa do problema original que causou o primeiro erro (mcause == 5). Se eu entendi corretamente o que está escrito aqui na linha 37, isso significa que a exceção é Load access fault. A razão parece estar aqui

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

call_board_init_f:
    li  t0, -16
    li  t1, CONFIG_SYS_INIT_SP_ADDR
    and sp, t1, t0  /* força alinhamento de 16 bytes */

#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       /* salta para board_init_f() */

$sp tem aquele valor incorreto, e dentro board_init_f_init_reserve ocorre um erro. Parece que aqui está o culpado: a variável com um nome inequívoco CONFIG_SYS_INIT_SP_ADDR. Ela está definida no arquivo HiFive_U-Boot/include/configs/HiFive-U540.hEm algum momento, cheguei a pensar se não seria mais fácil ajustar o processador em vez de aprimorar o carregador para este processador. Mas depois percebi que parecia mais um artefato de configurações que não estavam totalmente ajustadas para outra configuração de memória, e poderia tentar fazer assim:#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              /* Banco SDRAM #1 */
-#define PHYS_SDRAM_1   
-       (PHYS_SDRAM_0 + PHYS_SDRAM_0_SIZE)      /* Banco SDRAM #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 "

 /* Inicializa o Ponteiro da Pilha */
-#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      /* parte elevada da SDRAM */

Em algum momento, a quantidade de gambiarras de fixação tecnológica alcançou um ponto crítico. Depois de alguns desafios, percebi a necessidade de fazer uma porta correta para a minha placa. Para isso, é preciso copiar e ajustar alguns arquivos para a nossa configuração.

Bem, aproximadamente, aqui está um pouco

trosinenko@trosinenko-pc:/hdd/trosinenko/fpga/freedom-u-sdk/HiFive_U-Boot$ git show --name-status
commit 39cd67d59c16ac87b46b51ac1fb58f16f1eb1048 (HEAD -> zeowaa-1gb)
Author: Anatoly Trosinenko 
Date:   Tue Jul 2 17:13:16 2019 +0300

    Suporte inicial para a placa Zeowaa A-E115FB

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

Os detalhes podem ser vistos em repositório.

Como se viu, nesta placa da SiFive, os registradores de alguns dispositivos têm endereços diferentes. Também foi descoberto que o U-Boot é configurado por um mecanismo de Kconfig, já conhecido do núcleo do Linux — por exemplo, é possível ordenar fazer menuconfig, e uma interface de texto conveniente aparecerá com descrições dos parâmetros ? E etc. No geral, ao fundir as descrições de duas placas em uma terceira, descartando qualquer configuração pomposa de PLL (aparentemente isso está de alguma forma relacionado ao controle do computador host via PCIe, mas não é certo), obtive um firmware que, em condições favoráveis em Marte, me fornecia uma mensagem via UART sobre qual hash de commit foi usado e quanto DRAM eu tinha (mas essa informação eu mesmo a escrevi no cabeçalho).

É uma pena que, após isso, a placa geralmente parasse de responder pelo JTAG do processador, e a inicialização a partir do cartão SD — infelizmente, em minha configuração, não é rápida. Por outro lado, às vezes o BootROM exibia a mensagem que ERRO, não conseguiu iniciar, e em seguida o U-Boot aparecia. Foi aí que percebi: aparentemente, após a reinicialização, o bitstream na FPGA não é sobrescrito, não tem tempo de 'desconfigurar-se' etc. Em resumo, basta conectar o depurador ao ver a mensagem CARREGANDO / conectar-se ao depurador e emitir set variable $pc=0x80089800, assim contornando essa longa carga (é claro, supondo que na última vez tenha travado cedo o suficiente e não tenha conseguido carregar algo sobre o código original).

A propósito, isso é normal? O processador travar completamente e o depurador JTAG não conseguir conectar-se, recebendo mensagens

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

Espere um minuto! Eu já vi isso! Algo semelhante acontece durante um deadlock no TileLink, e eu não confio muito no autor do controlador de memória — ele mesmo o escreveu... De repente, após a primeira recompilação bem-sucedida do processador depois de editar o controlador, vi:

INIT
CMD0
CMD8
ACMD41
CMD58
CMD16
CMD18
CARREGANDO
BOOT

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

DRAM:  1 GiB
MMC:
ANTES DE CARREGAR ENVANTES DE FDTCONTROLADDRANTES DE LOADADDRIn:    serial
Out:   serial
Err:   serial
Pressione qualquer tecla para parar o autoboot:  3

Sobre esta linha estranha antes de In: serial Não se preocupe — eu estava tentando entender com um processador congelado se estava funcionando corretamente com o ambiente. O que significa "Já faz dez minutos que está assim"? Ele conseguiu pelo menos se realocar e ir para o menu de boot! Um pequeno desvio: embora o U-Boot carregue nos primeiros 2^24 bytes do cartão SD, ao iniciar, ele se copia para longe, para o endereço que pode estar escrito no cabeçalho de configuração ou apenas nos endereços altos da memória RAM, realiza a realocação dos símbolos ELF e passa o controle para lá. Então: parece que esse nível foi alcançado e como bônus recebemos um processador que não congela completamente depois disso.

Então, por que o timer não está funcionando? Parece que o relógio de alguma forma não está funcionando...

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

E se eu girar os ponteiros manualmente?

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

Então:

Pressione qualquer tecla para parar o autoboot:  0
MMC_SPI: 0 at 0:1 hz 20000000 mode 0

Conclusão: o relógio não está funcionando. Provavelmente, por causa disso, a entrada do teclado também não funciona:

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:
        /* Primeiro caractere da sequência de escape ANSI 'e' */
        if (c == 'e') {
            *esc = 1;
            *key = KEY_NONE;
        }
        break;
    case 1:
        /* Segundo caractere do ANSI '[' */
        if (c == '[') {
...

O problema foi que eu exagerei um pouco: eu adicionei à configuração do processador a chave:

  case DTSTimebase => BigInt(0)

… considerando que no comentário dizia "se você não souber — deixe 0". E na verdade WithNBigCores colocava exatamente em 1MHz (como aliás estava indicado na configuração do U-Boot). Mas eu fui, droga, cuidadoso e metódico: aqui não sei, aqui 25MHz! No final, nada funciona. Removi minhas "melhorias" e...

Pressione qualquer tecla para parar o autoboot:  0
MMC_SPI: 0 at 0:1 hz 20000000 mode 0
## Tipo de tabela de partição desconhecido 0
libfdt fdt_path_offset() retornou FDT_ERR_NOTFOUND
** Nenhuma tabela de partição - mmc 0 **
## Info: tamanho dos dados de entrada = 34 = 0x22
Executando uEnv.txt boot2...
## Erro: "boot2" não definido
HiFive-Unleashed #

É possível até mesmo inserir comandos! Por exemplo, depois de um pouco de mexendo, finalmente dá para perceber que deve-se inserir mmc_spi 1 10000000 0; mmc part, reduzindo a frequência do SPI de 20MHz para 10MHz. Por quê? Bem, no configuração estava escrita a frequência máxima de 20MHz, e ela ainda está lá. Mas, pelo que entendi, as interfaces, pelo menos aqui, funcionam assim: o código divide a frequência do bloco de hardware (no meu caso — sempre 25MHz) pela frequência alvo e configura o valor resultante como divisor no respectivo registro de controle. O problema é que se para o UART de 115200Hz for aproximadamente o que precisa, então se dividir diretamente 25000000 por 20000000 dá 1, ou seja, funcionará em 25MHz. Pode ser que isso esteja tudo bem, mas se existem restrições, significa que é necessário para alguém (mas pode não ser bem assim)… Enfim, é mais fácil ajustar e seguir em frente — longe e, infelizmente, por muito tempo. 25MHz — isso não é um Core i9.

Saída do console

HiFive-Unleashed # env edit mmcsetup
edit: mmc_spi 1 10000000 0; mmc part
HiFive-Unleashed # boot
MMC_SPI: 1 em 0:1 hz 10000000 modo 0

Mapa de Partição para o dispositivo MMC 0  --   Tipo de Partição: EFI

Part    Início LBA       Fim LBA         Nome
        Atributos
        Tipo GUID
        Partition GUID
  1     0x00000800      0x0000ffde      "Vfat Boot"
        attrs:  0x0000000000000000
        tipo:   ebd0a0a2-b9e5-4433-87c0-68b6b72699c7
        tipo:   dados
        guid:   76bd71fd-1694-4ff3-8197-bfa81699c2fb
  2     0x00040800      0x002efaf4      "root"
        attrs:  0x0000000000000000
        tipo:   0fc63daf-8483-4772-8e79-3d69d8477de4
        tipo:   linux
        guid:   9f3adcc5-440c-4772-b7b7-283124f38bf3
  3     0x0000044c      0x000007e4      "uboot"
        attrs:  0x0000000000000000
        tipo:   5b193300-fc78-40cd-8002-e86c45580b47
        guid:   bb349257-0694-4e0f-9932-c801b4d76fa3
  4     0x00000400      0x0000044b      "uboot-env"
        attrs:  0x0000000000000000
        tipo:   a09354ac-cd63-11e8-9aff-70b3d592f0fa
        guid:   4db442d0-2109-435f-b858-be69629e7dbf
libfdt fdt_path_offset() retornou FDT_ERR_NOTFOUND
2376 bytes lidos em 0 ms
Executando uEnv.txt boot2...
15332118 bytes lidos em 0 ms
## Carregando o kernel de FIT Image em 90000000 ...
   Usando a configuração 'config-1'
   Tentando o subimagem 'bbl' do kernel
     Descrição:  BBL/SBI/riscv-pk
     Tipo:         Imagem do Kernel
     Compressão:  não compactado
     Início dos Dados:   0x900000d4
     Tamanho dos Dados:    74266 Bytes = 72.5 KiB
     Arquitetura: RISC-V
     SO:           Linux
     Endereço de Carga: 0x80000000
     Ponto de Entrada:  0x80000000
     Algoritmo de Hash:    sha256
     Valor do Hash:   28972571467c4ad0cf08a81d9cf92b9dffc5a7cb2e0cd12fdbb3216cf1f19cbd
   Verificando a Integridade do Hash ... sha256+ OK
## Carregando fdt de FIT Image em 90000000 ...
   Usando a configuração 'config-1'
   Tentando o subimagem 'fdt' fdt
     Descrição:  indisponível
     Tipo:         Flat Device Tree
     Compressão:  não compactado
     Início dos Dados:   0x90e9d31c
     Tamanho dos Dados:    6911 Bytes = 6.7 KiB
     Arquitetura: RISC-V
     Endereço de Carga: 0x81f00000
     Algoritmo de Hash:    sha256
     Valor do Hash:   10b0244a5a9205357772ea1c4e135a4f882409262176d8c7191238cff65bb3a8
   Verificando a Integridade do Hash ... sha256+ OK
   Carregando fdt de 0x90e9d31c para 0x81f00000
   Inicializando usando o blob fdt em 0x81f00000
## Carregando carregáveis de FIT Image em 90000000 ...
   Tentando o subimagem 'kernel' carregáveis
     Descrição:  kernel do Linux
     Tipo:         Imagem do Kernel
     Compressão:  não compactado
     Início dos Dados:   0x900123e8
     Tamanho dos Dados:    10781356 Bytes = 10.3 MiB
     Arquitetura: RISC-V
     SO:           Linux
     Endereço de Carga: 0x80200000
     Ponto de Entrada:  indisponível
     Algoritmo de Hash:    sha256
     Valor do Hash:   72a9847164f4efb2ac9bae736f86efe7e3772ab1f01ae275e427e2a5389c84f0
   Verificando a Integridade do Hash ... sha256+ OK
   Carregando carregáveis de 0x900123e8 para 0x80200000
## Carregando carregáveis de FIT Image em 90000000 ...
   Tentando o subimagem 'ramdisk' carregáveis
     Descrição:  buildroot initramfs
     Tipo:         Imagem do RAMDisk
     Compressão:  gzip comprimido
     Início dos Dados:   0x90a5a780
     Tamanho dos Dados:    4467411 Bytes = 4.3 MiB
     Arquitetura: RISC-V
     SO:           Linux
     Endereço de Carga: 0x82000000
     Ponto de Entrada:  indisponível
     Algoritmo de Hash:    sha256
     Valor do Hash:   883dfd33ca047e3ac10d5667ffdef7b8005cac58b95055c2c2beda44bec49bd0
   Verificando a Integridade do Hash ... sha256+ OK
   Carregando carregáveis de 0x90a5a780 para 0x82000000

Ok, nós avançamos para um novo nível, mas ainda está travando. E às vezes ainda apresenta exceções. Pode-se ver mcause, esperando o código no endereço indicado. $pc e depois si encontrar-se em trap_entry. O processador U-Boot só consegue exibir para mcause = 0..4, então prepare-se para entrar em um loop com uma carga incorreta. Aqui, eu examinei a configuração e comecei a pensar no que eu havia mudado, e lembrei: lá estava conf/rvboot-fit.txt A empresa Finservice atua desde 2012 e é um dos principais corretores financeiros independentes no setor de crédito POS no mercado da Rússia. Atualmente, a Finservice faz parte de um grande conglomerado multifuncional, conta com mais de 200 funcionários e está realizando uma expanção ativa em todas as regiões da Rússia.

fitfile=image.fit
# abaixo, deve corresponder ao que está no FIT (ugha)

Então, vamos alinhar todos os arquivos, substituindo a linha de comando do núcleo aproximadamente assim, já que há suspeitas de que SIF0 — isso é uma saída para algum lugar via PCIe:

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

E ainda vamos mudar o algoritmo de hash de SHA-256 para MD5: não preciso de segurança criptográfica (especialmente durante a depuração), considera-se que é incrivelmente lento, e para capturar erros de integridade durante a carga, o MD5 dá conta do recado. E no final? Passamos para o nível anterior significativamente mais rápido (graças ao hash mais simples), e o próximo nível foi desbloqueado:

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

Só que o relógio não está funcionando...

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

Oops, parece que corrigir o relógio foi apenas um placebo, embora eu tenha achado que ajudou na época. Não, realmente precisamos consertar isso, mas vamos primeiro girar os ponteiros manualmente e ver o que acontece:

0x00000000bff6dbb0 in ?? ()
(gdb) set variable *0x0200bff8=1000000
(gdb) c
Continuando.
^C
Programa recebeu sinal SIGINT, Interromper.
0x00000000bff6dbb0 in ?? ()
(gdb) set variable *0x0200bff8=2000000
(gdb) c
Continuando.
^C
Programa recebeu sinal SIGINT, Interromper.
0x00000000bff6dbb0 in ?? ()
(gdb) set variable *0x0200bff8=3000000
(gdb) c
Continuando.

Entretanto...

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

Não, vou automatizar o relógio — senão, pode ser que ele decida calibrar o temporizador!

E o endereço da instrução atual, por sua vez, aponta para algum lugar em

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

dentro do Berkeley Boot Loader carregado. Pessoalmente, a menção disso me incomoda htif — interface do host, utilizada para o bootstrap tethered do núcleo (ou seja, em cooperação com ARM do host), eu imaginava que era standalone. No entanto, ao encontrar essa função nos códigos-fonte, percebe-se que não é tão ruim assim:

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"); }
  }
}

Desafio: inicie o relógio

A busca pelos registradores no CLINT nos leva a

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

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

Que se conecta ao RTC, ou no enigmático MockAON, sobre o qual inicialmente pensei: "O que é isso? Incompreensível? Vamos desconectar!" Como ainda não entendo que mágica de clock está acontecendo lá, decidirei apenas reaplicar essa lógica em System.scala:

  val rtcDivider = RegInit(0.asUInt(16.W)) // para garantir suporte até 16GHz, sou otimista :)
  val mhzInt = p(DevKitFPGAFrequencyKey).toInt
  // Suponha que a frequência seja igual a um número inteiro em megahertz
  rtcDivider := Mux(rtcDivider === (mhzInt - 1).U, 0.U, rtcDivider + 1.U)
  outer.clintOpt.foreach { clint =>
    clint.module.io.rtcTick := rtcDivider === 0.U
  }

Caminhando em direção ao núcleo do Linux

Aqui a narrativa já se estendeu e se tornou um pouco monótona, então descreverei superficialmente:

BBL previa a presença do FDT no endereço 0xF0000000, e eu já tinha corrigido isso! Vamos procurar mais… Encontrei em HiFive_U-Boot/arch/riscv/lib/boot.c, substituí por 0x81F00000, conforme indicado na configuração de inicialização do U-Boot.

Depois, o BBL reclamou que não havia memória. Meu caminho levou à função mem_prop, que em riscv-pk/machine/fdt.c: daí soube que era necessário marcar o nó fdt ram como device_type = "memory" — depois, talvez seja necessário corrigir o gerador de processador, mas por enquanto vou apenas digitar manualmente — de qualquer forma, eu transferi esse arquivo manualmente.

Agora recebi uma mensagem (apresentada em formato formatado, com quebras de linha):

Este é o dummy_payload do bbl. Para inicializar um kernel real, reconfigure o bbl com a flag --with-payload=CAMINHO, e então reconstrua o bbl. Alternativamente, o bbl pode ser usado em modo apenas firmware, adicionando nós de árvore de dispositivo para um payload externo e usando as opções -bios e -kernel do QEMU.

Parece que as opções estão sendo especificadas corretamente. riscv,kernel-start e riscv,kernel-end no DTB, mas os zeros estão sendo analisados. Depuração query_chosen mostrou que o BBL está tentando analisar um endereço de 32 bits, mas está recebendo um par <0x0 0xADDR>, e o primeiro valor parece ser os bits menos significativos. Adicionei na seção chosen

chosen {
      #address-cells = ;
      #size-cells = ;
      ...
}

e corrigi a geração de valores: não adicionar 0x0 como primeiro elemento.

Estes 100500 passos simples permitirão ver fácil e rapidamente como o pinguim cai:

Texto oculto

   Verificando a Integridade do Hash ... md5+ OK
   Carregando arquivos de 0x90a5a758 a 0x82000000
libfdt fdt_check_header(): FDT_ERR_BADMAGIC
escolhido {
        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() retornou FDT_ERR_NOTFOUND
escolhido {
        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";
};
   Carregando Imagem do Kernel ... OK
Iniciando o kernel em
3
2
1
0
## Iniciando a aplicação em 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: Ignorando o intervalo de memória 0x80000000 - 0x80200000
[    0.000000] versão do Linux 4.19.0-sifive-1+ (trosinenko@trosinenko-pc) (gcc versão 8.3.0 (Buildroot 2019.02-07449-g4eddd28f99)) #1 SMP Qua Jul 3 21:29:21 MSK 2019
[    0.000000] bootconsole [early0] habilitado
[    0.000000] Ramdisk inicial em: 0x(____ptrval____) (16777216 bytes)
[    0.000000] Intervalos de zonas:
[    0.000000]   DMA32    [mem 0x0000000080200000-0x00000000bfffffff]
[    0.000000]   Normal   [mem 0x00000000c0000000-0x00000bffffffffff]
[    0.000000] Início da zona móvel para cada nó
[    0.000000] Intervalos de nós de memória inicial
[    0.000000]   nó   0: [mem 0x0000000080200000-0x00000000bfffffff]
[    0.000000] Configuração do nó de memória inicial 0 [mem 0x0000000080200000-0x00000000bfffffff]
[    0.000000] Em nós 0 totalpages: 261632
[    0.000000]   zona DMA32: 3577 páginas usadas para memmap
[    0.000000]   zona DMA32: 0 páginas reservadas
[    0.000000]   zona DMA32: 261632 páginas, LIFO batch:63
[    0.000000] TLB de IO de software: mapeado [mem 0xbb1fc000-0xbf1fc000] (64MB)

(o emblema é exibido pelo BBL, e o que está com marcas de tempo é o kernel).

Felizmente, não sei como em todos os lugares, mas no RocketChip, ao conectar um depurador via JTAG, é possível capturar traps diretamente - o depurador para exatamente nesse ponto.

O programa recebeu sinal SIGTRAP, Trap de traço/ponto de interrupção.
0xffffffe0000024ca em ?? ()
(gdb) bt
#0  0xffffffe0000024ca em ?? ()
Backtrace interrompido: quadro anterior idêntico a este quadro (pilha corrompida?)
(gdb) arquivo work/linux/vmlinux
Um programa já está sendo depurado.
Você tem certeza de que deseja mudar o arquivo? (y ou n) y
Lendo símbolos de work/linux/vmlinux... feito.
(gdb) bt
#0  0xffffffe0000024ca em setup_smp () em /hdd/trosinenko/fpga/freedom-u-sdk/linux/arch/riscv/kernel/smpboot.c:75
#1  0x0000000000000000 em ?? ()
Backtrace interrompido: quadro não salvou o 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); // < VOCÊ ESTÁ AQUI
}

Como dizia uma velha anedota, CPU não encontrada, emulando software. Ou talvez não esteja rodando. Perdido em um único núcleo de processador.

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

Um bom comentário em linux/arch/riscv/kernel/setup.c — uma pintura de cerca no método de Tom Sawyer. Enfim, hoje, inexplicavelmente, não houve vencedores, o prêmio será transferido para o próximo sorteio...

Com isso, proponho encerrar este artigo que já se alongou bastante.

Continua. Nele haverá uma luta contra um erro astuto que se esconde se nós nos aproximarmos lentamente com single-step.

Captura de texto da inicialização (link externo):
Parte 3: Quase carregando o Linux do cartão SD no RocketChip

Fonte: habr.com

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster