Loading...
 

EPOS for RISC-V

Table of Contents

[Show/Hide]

1. Compiler

EPOS is itself an operating system. Therefore, its compiler cannot rely on a libc compiled for another OS (such as LINUX). A cross-compiler is needed even if your source and target machines are x86-based PCs. You can use your distro's cross-compilers (version 2.2 onwards), download a precompiled GCC for EPOS from the downloads page (version 2.1 or older). Refer to EPOS documentation for more information on the necessary toolchain to support EPOS. RISC-V toolchain can be built from its repository. However, the following precompiled versions of the toolchain may suit you. In order to use one of them, extract the tarball content to /usr/local/rv32 (for RISC-V 32 bits compiler) or /usr/local/rv64 (for RISC-V 64 bits compiler).

2. Considerações sobre a Arquitetura

2.1. Terminologia

CSRs: Control and Status Registers.
Interrupção se refere a um evento externo que ocorre de forma assíncrona na thread corrente. Quando uma interrupção precisa ser atendida, uma instrução é selecionada para receber a exceção de interrupção.

    • Exemplo: Um timer de interrupção é utilizado para acionar um evento futuro, sendo assim a CPU escreve em seu registrador mtimecmp o valor de mtime + ticks que se referem a um número de clocks de relógio no futuro. Como mtime se incrementa automaticamente independente de qualquer instrução executada pela CPU, em algum ponto mtimecmp se iguala a mtime e dessa forma a CPU entra com o tratador de interrupção.

Exceções se referem a uma condição incomum no sistema ocorrida em tempo de execução em uma instrução.

    • Exemplo: O endereço de um dado que não foi alinhado corretamente em uma instrução load, faz com que a CPU entre com o tratamento de exceção do tipo "endereço desalinhado", que irá aparecer no registrador mcause.

Armadilha ou Trap, se refere a uma transferência de controle síncrona para o tratador de armadilha devido a um condição excepcional causada na thread corrente.

    • Exemplo: Seja uma CPU com três modos de operação: Máquina, Supervisor e Usuário. Cada um deles possui seus próprios registradores de controle e status (CSRs) para tratamento de armadilha e um área de pilha dedicada a eles. Quando em modo usuário, uma troca de contexto é requerida para tratar de um evento em modo supervisor. O software configura o sistema para uma troca de contexto e chama a instrução ECALL que troca o controle para o tratador de exceção de ambiente de usuário.

2.2. Bases e Extensões

A definição de uma ISA única no RISC V parte de uma base de instruções sob inteiros que contém instruções compatíveis com as primeiras versões de RISC. Como a arquitetura suporta diferentes tamanhos de bits com pouca modificação da ISA, divide-se esta em 4 bases, que diferem no tamanho de registradores de inteiros e, consequentemente, no tamanho do espaço de endereçamento destes registradores. As duas mais importantes para nós são:

  • RV32I, RISC V com a extensão base de inteiros e registradores de inteiros de 32 bits; e
  • RV64I, RISC V com a extensão base de inteiros e registradores de inteiros de 64 bits.

Ambas estas ISA’s possuem apenas as operações base das instruções com inteiros. Não obstante a estas, para adicionarmos, por exemplo, mais instruções, relativas a multiplicações e divisões de inteiros poderíamos utilizar a extensão M por meio da RV64IM. Segue abaixo uma figura com as bases e extensões possíveis para RISC V.

Screenshot.192

2.3. Níveis de privilégio

O RISC V define três níveis de privilégio, que podem ou não ser implementados. Estes são necessários para a proteção dos diferentes componentes da stack de software, i.e. uma aplicação em execução sobre um sistema operacional, a qual poderá utilizar CSR’s, mas não todos, pois deve respeitar seu nível de privilégio. Quando um nível de privilégio tenta acessar operações de outro nível de privilégio uma interrupção acontece alertando sobre isso.

Os três níveis de privilégios do RISC V são:

  1. User - nível de privilégio de aplicações executando pelo usuário;
  2. Supervisor - utilizado pelo sistema operacional; e
  3. Machine - com mais privilégios e executa de maneira segura. Utilizado no SETUP do sistema operacional (neste caso, retorna ao nível Supervisor com MRET, mas pode ser configurado por meio do Registrador (m/u/s)status). Abaixo, segue uma tabela sobre eles e suas identificações.

Screenshot.193


Modo de Máquina é o único que deve ser incondicionalmente implementado por todos os sistemas RISC-V e tanto o S quanto o U são opcionais, permitindo as seguintes configurações:

  • M
  • M e U
  • M, S e U

Os níveis de privilégio são definidos por harts. Uma hart pode executar ao mesmo tempo que outra, independente de ambas terem níveis de privilégio diferentes. A fim de transicionar entre níveis de privilégios, utilizam-se as operações MRET, SRET e URET, respectivamente, para voltar ao privilégio antes de estar em Machine, Supervisor e User. Registradores limitados a nível de privilégio são acessados devem ter a inicial do nível de privilégio desejado prefixado em seu nome - m, s e u - para os níveis Machine, Supervisor e User, respectivamente.

2.4. Principais Diferenças entre RV32I e RV64I

Embora tenhamos falado que em RISC-V as ISA’s de base compartilham várias instruções, principalmente com a extensão base de inteiros, há algumas diferenças entre RV32I e RV64I que são importantes para o desenvolvimento de sistemas e iremos falar delas aqui.

A principal diferença é do tamanho dos tipos como podemos ver nas tabelas abaixo. Temos um problema ocasionado, por exemplo, pelo cast de um ponteiro para um inteiro, já que, em RV32I sizeof(int) == sizeof(void *), mas em RV64I sizeof(int) != sizeof(void *). Devemos nos atentar a estas modificações de tamanho de bytes dos tipos para cada modificação.

Screenshot.194


Outra diferença é na leitura de alguns CSR’s. Em RV32I alguns CSR’s por serem definidos com 64 bits, temos que realizar duas leituras de dois registradores diferentes, como, por exemplo, nos registradores de Hardware Performance Monitoring hpmcounter1 e hpmcounter1h, onde o primeiro possui os bits menos significativos dos 64 bits e o segundo os bits mais significativos. Já em RV64I não precisamos de duas leituras, já que temos registradores de 64 bits.

Por fim, algumas instruções são alteradas. Comentamos anteriormente que a maioria das instruções se preservava, mas há algumas alterações entre RV32I e RV64I quanto às instruções. Um dos principais é sobre salvar e carregar palavras da memória. Em RV32I as instruções lw e sw, salvam e carregam, respectivamente, palavras de 32 bits na memória. Elas têm a mesma funcionalidade em RV64I. Para salvar e carregar palavras de 64 bits na memória em RV64I utilizamos as instruções ld e sw, respectivamente.

Nas outras operações mais simples de inteiros as operações são alteradas com base no XLEN da ISA. Portanto, em RV32I add adiciona dois inteiros de 32 bits e o em RV64I add adiciona dois inteiros de 64 bits. Podemos operar sob inteiros de 32 bits em RV64I utilizando o sufixo w na instrução, por exemplo, addw adiciona dois inteiros de 32 bits em RV64I.

3. Registradores de Controle e Status (CSR)

Control and Status Register (CSR) é um tipo de registrador que armazena várias informações na CPU. RISC-V define um "address space" separado de no máximo 4096 CSRs, endereçáveis por meio de 12 bits. Tais registradores respeitam o seguinte padrão de nomenclatura:

  • Prefixo “m” - Machine-level ISA;
  • Prefixo “s” - Supervisor-level ISA; e
  • Prefixo “u” - User-level ISA.

A tabela a seguir apresenta os registradores presentes no S-mode.

Img1

3.1. Instruções CSRs

Para interagir com os registradores CSRs, é necessário utilizar um conjunto de instruções especiais. Estas instruções estão presentes na tabela a seguir.

Inst


A campo rd representa o destino e o rs1 representa de onde vem. A instrução CSRRW, imetiada ou não, apresenta dois comportamentos, quando há rd ele pode apenas escrever no registrador e quando não há ele pode escrever ou ler. As instruções CSRRS e CSRRC apresenentam o mesmo comportamento da CSRRW mas quando há ou não rs1.

3.2. Especificações de campo dos CSRs

A documentação do RISC-V (Volume II) utiliza as seguintes abreviações na especificação do comportamento dos campos dentro dos CSRs

RótuloLeituraEscrita
WPRIProíbidaProíbida (necessário manter campo inalterado)
WLRLValor legal se foi escrito um valor legalApenas valores legais
WARLQualquer valorApenas valores legais

3.2.1. Reserved Writes Preserve Values, Reads Ignore Values (WPRI)

Alguns campos inteiros de escrita/leitura são reservados para uso futuro. Software deve ignorar o valores lidos nestes campos e deve preservar os valores destes campos quando escrever em outros campos do mesmo registrador. Estes campos estão rotulados como WPRI da descrição dos registradores.

Estes campos têm comportamento especificado para apenas um subconjunto legal dentre todas as codificações de bits possíveis, com as demais codificações reservadas. Software não deve escrever nada além de valores legais, também não deve assumir que a leitura vá retornar um valor legal a não ser que a última escrita tenha sido um valor legal. Estes campos estão rotulados como WLRL da descrição dos registradores.

Alguns CSR, assim como em WLRL, tem um subconjunto legal de codificações, porém permite que qualquer valor seja escrito enquanto garante que a leitura seja sempre um valor legal. Estes campos estão rotulados como WARL da descrição dos registradores.

3.3. Machine Status Register (mstatus, sstatus, ustatus)

Este registrador monitora e controla o estado operacional atual da HART.

Supervisor Status Register (sstatus) e User Status Register (ustatus) são o mesmo CSR, porém com os campos de privilégios mais baixos rotulados como WPRI (proíbida a leitura e escrita)

Screenshot 20230416 212238
Fonte: The RISC-V Instruction Set Manual - Volume II: Privileged Architecture

3.3.1. Global Interrupt Enable (m/s/u)IE

Habilita ou desabilita interrupções globais para o respectivo modo de privilégio. Quando o hart está executando no modo de privilégio x, as interrupções globais estão habilitadas quando xIE=1 e desabilitadas quando xIE=0. Interrupções de modos menos privilegiados estão sempre desabilitadas e de modos mais privilegiados estão sempre habilitadas independente dos valores dos respectivos campos. Modos mais privilegiados podem desabilitar interrupções específicas antes de passar o controle para um modo menos privilegiado.

3.3.2. Previous Interrupt Enable (m/s/u)PIE e Previous Privilege (m/s/u)PP

O RISC-V suporta traps aninhadas, para isto é necessário os campos (m/s/u)PIE que guarda o valor do IE anterior e o (m/s/u)PP que guarda o código do privilégio anterior. Quando uma trap é tirada do modo de privilégio y para o modo x, xPIE é setado para o valor de xIE, xIE é setado para 0 e xPP é setado para y.

As instruções MRET, SRET ou URET são usadas para retornar de traps dos modos M, S ou U, respectivamente. Quando se executa essas instruções, assumindo que xPP tem o valor y, xIE é setado ao valor de xPIE, o modo é mudado para y, xPIE é setado a 1 e xPP é setado para U(ou M se não suporta User-mode). Pode-se usar o MRET para trocar pela primeira vez à outro modo.

3.3.3. SUM

O bit de configuração SUM(Supervisor User Memory access) no registrador SSTATUS é responsável por definir o acesso do modo supervisor a área de memórias definidas para o usuário, onde ele poderá escrever ou ler dados, caso este bit não esteja habilitado e o software esteja em modo supervisor, mesmo que esta operação aparenta ter permissão já que o supervisor tem mais direitos que o usuário, ele irá receber um page_fault por a página não pertencer ao seu modo de operação. Ressalta-se, que independentemente do bit estar ou não habilitado a máquina em modo supervisor não é capaz de executar operações na área de usuário.

3.4. Hart ID Register (mhartid)

Screenshot 20230416 222535
Fonte: The RISC-V Instruction Set Manual - Volume II: Privileged Architecture


Contém o ID da hart que está executando o código, importante para multi-core (Garantido alguma hart com ID zero).

3.5. Machine Interrupt-Enable Register (mie, sie, uie) e Machine Interrupt Pending Register (mip, sip, uip)

Screenshot 20230416 223243
Mip
Fonte: The RISC-V Instruction Set Manual - Volume II: Privileged Architecture


O Machine Interrupt-Enable Register (mie, sie, uie) habilita e desabilita fontes de interrupções individuais.

O Machine Interrupt Pending Register (mip, sip, uip) indica o tipo de interrupção pendente.

Campos:

  • (m/s/u)EI(E/P) para interrupções externas.
  • (m/s/u)TI(E/P) para interrupções de timer.
  • (m/s/u)SI(E/P) para interrupções de software.

sie e uie, assim como sip e uip, são os mesmos registradores que os de modo de privilégio de máquina, porém com os campos de privilégios mais baixos rotulados como WPRI (proíbida a leitura e escrita)

Em sistemas com várias CPUs, uma CPU pode escrever no (m/s/u)SIP de qualquer outra. Isso permite uma comunicação eficiente entre processadores.

3.6. Machine Time Register (mtime) e Machine Time Compare Register (mtimecomp)

Screenshot 20230416 223352
Fonte: The RISC-V Instruction Set Manual - Volume II: Privileged Architecture


O Machine Time Register (mtime) é um registrador de 64 bits que armazena o valor de um contador de tempo real e com ele é possível inferir intervalos de tempo.

Machine Time Compare Register (mtimecomp) é um registrador de 64 bits utilizado para gerar interrupções de timer. É utilizado em conjunto com o registrador mtime na geração de interrupções de timers. Há um registrador mtimecmp por hart.

3.7. Machine Cause (mcause, scause, ucause)

Mcause
Fonte: The RISC-V Instruction Set Manual - Volume II: Privileged Architecture


O registrador Machine Cause indica qual evento causou a trap, caso seja uma interrupção, o bit Interrupt é setado. O campo Exception Code indica qual o código da da interrupção/exceção. No caso do scause e ucause os códigos são os mesmos, com exceção dos códigos referentes à privilégios mais elevados, estes se tornam reservados.

Tebela
Fonte: The RISC-V Instruction Set Manual - Volume II: Privileged Architecture


Se for gerada mais de uma exceção síncrona, o valor no campo Exception Code será o que tiver a maior prioridade segundo a tabela abaixo.

Priority
Fonte: The RISC-V Instruction Set Manual - Volume II: Privileged Architecture

3.8. Machine Exception Program Counter (mepc, sepc, uepc)

Mepc
Fonte: The RISC-V Instruction Set Manual - Volume II: Privileged Architecture


Quando um trap é encontrado, mepc recebe o endereço virtual da instrução interrompida ou que encontrou a exceção. Caso contrário, mepc nunca é escrito pela implementação, mas pode ser escrito explicitamente pelo software.

3.9. Machine Trap-Vector Base-Address (mtvec, stvec, utvec)

Mtvec
Fonte: The RISC-V Instruction Set Manual - Volume II: Privileged Architecture


O mtvec é utilizado para definir o endereço das traps. Sempre deve ser implementado, mas se poderá ser escrito varia com a implementação. Ele deve ser configurado no início do fluxo de inicialização, para eventuais tratamentos de exceções.

O campo BASE define o endereço base para a trap, deve ser alinhado em 4 bytes.

O campo MODE aceita dois possíveis valores:

  • 0 (Direct): todas as exceções causarão um desvio para o endereço armazenado no campo BASE.
  • 1 (Vetorizado): as interrupções serão desviadas para o endereço BASE + 4*causa, de forma semelhante à um switch case.

3.10. Machine Trap Delegation Registers (medeleg, mideleg, sedeleg, sideleg)

Por padrão todas as traps, em qualquer modo, são tratadas em modo machine. Apesar de ser possível mudar de modo com a instrução MRET, é possível utilizar os CSR's Exception Delegation Registers (medeleg ou sedeleg) e Interruption Delegation Registers (mideleg ou sideleg) para selecionar quais exceções e interrupções serão processadas diretamente em modo um imediatamente menos privilegiado disponível, diminuindo overhead.

Apesar da delegação, uma trap nunca transiciona para um modo menos privilegiado. Ex.: Caso uma exceção de machine seja delegada para o modo Supervisor, se a exceção ocorrer em um software sendo executado em modo machine, a trap será executada em também em modo machine. Ela só será executada em modo supervisor se o software estiver sendo executado em modo de supervisor ou user (mesmo privilégio ou menor).

Os registradores (m/s)edeleg e (m/s)ideleg são configurados a partir de campos em posições iguais aos códigos de exceção de (m/s/u)cause. Ex.: Para delegar a exceção Ilegal Instruction para o modo supervisor, é necessário setar o bit 2 de medeleg, pois o código desta exceção é 2.

Não é feita a delegação automática de interrupções de timer. Para mais informações consulte Problema_da_delega_o_de_timers

3.11. Machine Trap Value (mtval, stval, utval)

Mtval
Fonte: The RISC-V Instruction Set Manual - Volume II: Privileged Architecture


Quando um trap é encontrado, mtval, stval ou utval (de acordo com o modo atual) é definido como zero ou com informações específicas de exceção para auxiliar o software a lidar com o trap. Caso contrário, mtval, stval ou utval nunca é escrito pela implementação, embora possa ser escrito explicitamente pelo software.

A plataforma de hardware especificará quais exceções irão usá-lo. Quando um ponto de interrupção de hardware é acionado ou ocorre uma exceção de busca, carregamento ou armazenamento de instruções desalinhadas, acesso ou falha de página, mtval é gravado com o endereço virtual com falha. Seus bits mais significativos não usados, são colocados em 0.

Opcionalmente, o registrador mtval também pode ser usado para retornar os bits de instrução com falha em uma exceção de instrução ilegal (mepc aponta para a instrução com falha na memória).

3.12. Supervisor Address Translation and Protection (SATP)

Registrador exclusivo de Supervisor mode (não pode ser habilitado por machine mode), responsável pelo sistema de paginação. Com ele, podemos habilitar os três diferentes sistemas de paginação do RiscV: Sv32, Sv39 e Sv48, além também de trocar a tabela de página usada no momento.

Em uma mudança de contexto, o sistema operacional deve gerenciar os espaços de endereçamento e mapeamentos de memória para cada processo. Isso é feito através do registrador SATP. O sistema operacional salva o valor atual do registrador SATP para o processo que está sendo interrompido, e em seguida carrega no SATP o valor salvo correspondente ao processo que está sendo retomado.

O campo ASID do SATP é essencial para o gerenciamento eficiente da Translation Lookaside Buffer (TLB) durante trocas de contexto. Como cada processo possui seu próprio espaço de endereçamento e mapeamento de endereços virtuais para físicos, teoricamente a TLB deveria ser atualizada toda vez que um novo processo começa a ser executado. No entanto, em vez de limpar toda a TLB, é possível usar o campo ASID para permitir que o processador mantenha entradas separadas na TLB para diferentes processos. Isso reduz o overhead nas trocas de contexto.

3.13. Registradores de armazenamento temporário de modo (mscratch e scratch)

Screenshot 20230416 223511
Fonte: The RISC-V Instruction Set Manual - Volume II: Privileged Architecture


Este registrador têm como objetivo fornecer uma área de armazenamento temporário para software durante a execução de operações críticas, como chamadas de interrupções e trocas de contexto. O RISC-V oferece dois registradores mscratch e sscratch, cada um deles sendo utilizado em contextos diferentes. O mscratch é o registrador de proposito geral utilizado no software em execução no modo de machine (M), sscratch por sua vez é utilizando durante a execução em modo supervisor (S).

São muito importantes para o armazenamento temporário de stack pointers entre as trocas de user -> machine -> supervisor -> user ou user -> supervisor -> user, como explicado posteriormente na seção de Troca de Contexto em RISCV64.

3.14. Registrador

4. Interrupções

4.1. CSRs relacionados à interrupções

Mais detalhes em Registradores de Controle e Status (CSR).

4.1.1. Configuração

O registrador de (m/s/u)status contém os campos (m/s/u)IE para habilitar e desabilitar interrupções em cada modo de privilégio.

Para um maior controle, os registradores (m/s/u)ie e (m/s/u)ip são utilizados para habilitar interrupções de origens individuais.

Os endereços para as traps são definidos no CSR (m/s/u)tvec.

Normalmente a trap é executada em modo machine, mas é possível configurar o registrador (m/s)edeleg e (m/s)ideleg para executar automaticamente em outro modo.

4.1.2. Informações

  • (m/s/u)ip é utilizado para identificar a origem da interrupção. É possível escrever no campo (m/s/u)SIP para gerar uma interrupção de software do hart correspondente.

  • (m/s/u)pie e (m/s/u)pp são utilizados para guardar informação do enable e do privilégio da interrupção anterior, permitindo traps aninhadas.

  • (m/s/u)epc guarda endereço virtual da instrução que estava sendo executada.

  • (m/s/u)tval armazena informações adicionais sobre a exceção encontrada.

4.2. Tratamento de Interrupções

Quando uma interrupção chega numa HART um procedimento de troca de execução para tratar a interrupção é tomado. Este processo salva valores importantes em registradores incluindo o program counter e troca a execução para o handler da interrupção, e ao finaliza-lo restaura a execução anterior. A forma com que as informações da interrupção são preparadas será explicitada posteriormente na seção Controladores de Interrupção. O processo detalhado de execução do handler segue:

  • Sempre que ocorrer uma interrupção, o hardware salvará e restaurará automaticamente registros importantes.

Software em execução

Chega a interrupção:

1 mstatus.MPIE	<- mstatus.MIE
2 mstatus.MIE 	<- 0
3 mstatus.MPP 	<- priv
4 mepc        	<- pc
5 pc          	<- mtvec (se mtvec.MODE = Direct) | mtvec.BASE + 4 * exception_code


Handler da interrupção executa

Handler finaliza:

6 priv        	<- mstatus.MPP
7 mstatus.MIE 	<- mstatus.MPIE
8 pc          	<- MEPCte


Software retoma execução

  • 1: Salva o bit que informa se interrupções estão habilitadas.
  • 2: Desabilita interrupções.
  • 3: Salva o nivel de privilégio da programa em execução
  • 4: Salva o program counter do programa em execução.
  • 5: Muda o program counter para o início do código do handler.
  • 6-8: Restaura a execução original.


4.3. Tipos de Interrupção

Apesar de interrupções e exceções serem conceitos distintos ambos são administrados pelos controladores de interrupção. Toda interrupção tem um nível de privilégio. O EPOS não possue interrupções de privilégio nível user, isto é implementado pela extensão N do RISC-V. A manipulação do nível de privilégio de uma interrupção pode ser feita através da delegação. Ainda, toda interrupção tem também origem podendo ser de software, timer, externo ou local. Uma tabela com todas as interrupções e exceções está na seção de registradores.

  • Software: Interrupções geradas através da escrita nos registradores msip.

  • Externo e Local: Interrupções que vem de fora do processador, como IO, sua principal diferença é a forma com que são tratadas

4.4. Controladores de Interrupção

Quando ocorre uma interrupção, antes que chegue numa Hart, ela pode passar por um Controlador de Interrupções. Estes são responsáveis por administrar qual handler deve ser usado para tratar cada tipo de interrupção e se a interrupção deve ou não preemptar a thread corrente. Na arquitetura RISC-V existem três Controladores de Interrupções: o CLINT, implementado no EPOS, o CLIC que não está implementado no EPOS e o PLIC que também não está implementado no EPOS. Os dois últimos não serão explicados tão a fundo quanto o primeiro.

4.5. Handlers

Uma das responsabilidades mais importantes de um Controlador de Interrupção é encaminhar o handler correto para cada interrupção. Toda interrupção (neste caso incluindo exceções como interrupção) é uma anomalia que tem de ser tratada, assim para cada tipo possível de interrupção existe um procedimento para tratá-lo. Quando uma interrupção ocorre o Controlador deve saber encontrar qual procedimento executar e encaminhá-lo para o Hart que a trata. No EPOS todas as execeções tem um handler respectivo, algumas compartilhando o mesmo, mas apenas handlers de interrupção do tipo timer estão implementadas, o restante ou não tem implementação ou ativa um Machine Panic.

4.6. CLINT - Core Local Interrupter

O CLINT só é preparado para lidar com interrupções do tipo software e timer e exceções, e tem um esquema de prioridade fixo. A preempção é permitida apenas de um nível de privilégio maior em um nível de privilégio menor, assim uma interrupção modo Machine pode preemptar uma operação modo Supervisor mas não o contrário, e, uma interrupção modo Machine não pode preemptar operação modo Machine.

Clint
Fonte: SiFive Interrupt CookbookVersion 1.2

4.6.1. Modos de Operação

  • Existem dois modos de operação do CLINT quanto a como administra os handlers, o modo direto e o modo vetorizado. Para configurar os modos, escreva no campo mtvec.MODE, que é o bit0 do registrador de status e controle (mtvec):
    • Para o modo direto, mtvec.MODE = 0.
    • Para o modo vetorizado, mtvec.MODE = 1.

  • O modo direto é o valor padrão de reset. O campo mtvec.BASE guarda o endereço base para interrupções e exceções, e deve ter um valor alinhado a um mínimo de 4 bytes no modo direto, e um mínimo de 64 bytes no modo vetorizado.


Modo Direto

  • O modo direto significa que todas as interrupções têm trap para o mesmo tratador, é responsabilidade do software executar código para descobrir qual interrupção ocorreu.

  • O tratador em software no modo direto, deve primeiro ler mcause.INTERRUPT para determinar se uma interrupção ou exceção ocorreu, para então decidir o que fazer baseado no valor de mcause.CODE que contém o código de interrupção ou exceção respectivo.


Código exemplo:

#define MCAUSE_INT_MASK 0x80000000 // [31]=1 interrupção, 0=exceção
#define MCAUSE_CODE_MASK 0x7FFFFFFF // bits com o codigo da interrupção
void software_handler() {
    unsigned long mcause_value = read_csr(mcause);
    if (mcause_value & MCAUSE_INT_MASK) {
        // Handler de Interrupções
        // Index into 32-bit array containing addresses of functions
        async_handler[(mcause_value & MCAUSE_CODE_MASK)]();
    } else {
        // Handler de Exceções
        sync_handler[(mcause_value & MCAUSE_CODE_MASK)]();
    }
}


Modo Vetorizado

  • O modo vetorizado introduz um método para criar uma tabela de vetores que o hardware usa para reduzir a latência do tratamento de interrupções. Quando uma interrupção acontece no modo vetorizado, o registrador pc será atribuído pelo hardware ao endereço do índice da tabela de vetores correspondente ao ID da interrupção. Do índice da tabela de vetores, um jump subsequente irá ocorrer para atender a interrupção e que não precisamos nos preocupar com guardar a program counter no handle pois quando uma interrupção chega numa hart o hardware administra a program counter diretamente.

  • Lembre-se de que a tabela de vetores contém um opcode que é uma instrução de jump para um local específico.

4.7. CLIC - Core Local Interrupt Controller

O CLIC é um superset do CLINT, é uma versão melhorada que é inclusive retrocompativel com o CLINT e oferencendo mais dois modos de handling. Ele é mais flexivel, nele a preempção de interrupções dentro de um mesmo privilégio é permitida. Ao invés de salvar certos dados em registradores ele pode salva-los em espaços reservados na memória. Por fim o CLIC também consegue receber interrupções do tipo externa e local, coisa que o CLINT não consegue fazer.

4.8. PLIC - Plataform Level Interrupt Controller

The PLIC is used to manage all global interrupts and route them to one or many CPUs in the system. It is possible for the PLIC to route a single interrupt source to multiple CPUs. Upon entry to the PLIC handler, a CPU reads the claim register to acquire the interrupt ID. A successful claim will atomically clear the pending bit in the PLIC interrupt pending register, signaling to the system that the interrupt is being serviced. During the PLIC interrupt handling process, the 23 pending flag at the interrupt source should also be cleared, if necessary. It is legal for a CPU to attempt to claim an interrupt even when the pending bit is not set. This may happen, for example, when a global interrupt is routed to multiple CPUs, and one CPU has already claimed the interrupt just prior to the claim attempt on another CPU. Before exiting the PLIC handler with MRET instruction (or SRET/URET), the claim/complete register is written back with the non-zero claim/complete value obtained upon handler entry. Interrupts routed through the PLIC incur an additional three-cycle penalty compared to local interrupts. Cycles in this case are measured at the PLIC frequency, which is typically an integer divided value from the CPU and local interrupt controller frequency.

4.8.1. PLIC - Priorities and Preemption

There are up to 1024 available interrupts routed into the PLIC, which are numbered sequentially 1 through 1024. Each interrupt into the PLIC has a configurable priority, from 1-7, with 7 being the highest priority. A value of 0 means do not interrupt, effectively disabling that interrupt. There is a global threshold register within the PLIC that allows interrupts configured below a certain level to be blocked. For example, if the threshold register contains a value of 5, all PLIC interrupts configured with priorities from 1 through 5 will not be allowed to propagate to the CPU. If global interrupts with the same priority arrive at the same time into the PLIC, priority is given to the lower of the two Interrupt IDs. Global interrupts routed through the PLIC are connected to the CPU in slightly different ways depending on the local interrupt selection. If the PLIC is used with the CLINT, then the external interrupt connection routed from the PLIC is tied directly to the CPU. If the PLIC is used with the CLIC (not our case), then the external interrupt connection is not used, and the interrupt is routed from the PLIC through the CLIC interface. By definition, the PLIC cannot forward a new interrupt to a HART that has claimed an interrupt but has not yet finished the complete step of the interrupt handler. Thus, the PLIC does not support the preemption of global interrupts to an individual HART. However, since PLIC interrupts arrive at the CPU through the external interrupt connection, preemption may occur from other CLIC local interrupts that are configured with a higher priority than the external interrupt. To support preemption, mstatus.mie needs to be re-enabled within the handler since it is disabled by hardware upon entry. Interrupt IDs for global interrupts routed through the PLIC are independent of the interrupt IDs for local interrupts. Thus, the software may need to implement a specific handler that supports a software lookup table for the global interrupts that are managed by the PLIC and arrive at the CPU through the external interrupt connection.

4.8.2. PLIC + CLINT, Machine Mode Interrupts Only

For a multi-CPU system implementing Machine mode only, an example configuration is shown below.
Captura De Tela 2023 09 06 160937

4.8.3. PLIC - Memory-Map

The following EPOS code shows the PLIC base address (0x0c000000) for RISC-V Sifive_U machines:

// Memory-mapped devices
        BIOS_BASE            = 0x00001000,   // BIOS ROM
        TEST_BASE            = 0x00100000,   // SiFive test engine
        RTC_BASE             = 0x00101000,   // Goldfish RTC
        UART0_BASE        = 0x10010000,   // SiFive UART
        CLINT_BASE         = 0x02000000,   // SiFive CLINT
        TIMER_BASE        = 0x02004000,   // CLINT Timer
        PLIC_CPU_BASE   = 0x0c000000,   // SiFive PLIC
        PRCI_BASE           = 0x10000000,   // SiFive-U Power, Reset, Clock, Interrupt
        GPIO_BASE          = 0x10060000,   // SiFive-U GPIO
        OTP_BASE            = 0x10070000,   // SiFive-U OTP
        ETH_BASE            = 0x10090000,   // SiFive-U Ethernet
        FLASH_BASE        = 0x20000000,   // Virt / SiFive-U Flash
        SPI0_BASE           = 0x10040000,   // SiFive-U QSPI 0
        SPI1_BASE           = 0x10041000,   // SiFive-U QSPI 1
        SPI2_BASE           = 0x10050000,   // SiFive-U QSPI 2

And for the offsets from PLIC_CPU_BASE for the registers used by PLIC:

// Registers offsets from PLIC_CPU_BASE
    enum {                                  // Description
        PLIC_PENDING          = 0x001000,     // PLIC Interrupt Pending base offset.
        PLIC_INT_ENABLE      = 0x002000,     // PLIC Interrupt Enable base offset.
        PLIC_THRESHOLD      = 0x200000,     // PLIC Interrupt Priority Threshold Register (threshold)
        PLIC_CLAIM               = 0x200004,     // PLIC Claim/Complete Register (claim)
    };

4.8.4. PLIC Specialization

Since the 1024 number of interruptions is the maximum number in PLIC's RISC-V design, each implementation has its own number and defined external interruptions. Hence, for each RISCV-V machine implementation supported in EPOS, the number of IRQs and the sources' enum must be provided in the machine traits.

This is an example from the SiFive U implementation (code can be found at "machine/riscv/sifive_u/sifive_u_traits.h):

template <> struct Traits<IC>: public Traits<Machine_Common>
{
    static const bool debugged = hysterically_debugged;

    static const unsigned int PLIC_IRQS = 54;           // IRQ0 is used by PLIC to signalize that there is no interrupt being serviced or pending

    struct Interrupt_Source: public _SYS::Interrupt_Source {
        static const unsigned int IRQ_L2_CACHE  = 1;    // 3 contiguous interrupt sources
        static const unsigned int IRQ_UART0     = 4;
        static const unsigned int IRQ_UART1     = 5;
        static const unsigned int IRQ_QSPI2     = 6;
        static const unsigned int IRQ_GPIO0     = 7;    // 16 contiguous interrupt sources
        static const unsigned int IRQ_DMA0      = 23;   // 8 contiguous interrupt sources
        static const unsigned int IRQ_DDR       = 31;
        static const unsigned int IRQ_MSI0      = 32;   // 12 contiguous interrupt sources
        static const unsigned int IRQ_PWM0      = 42;   // 4 contiguous interrupt sources
        static const unsigned int IRQ_PWM1      = 46;   // 4 contiguous interrupt sources
        static const unsigned int IRQ_I2C       = 50;
        static const unsigned int IRQ_QSPI0     = 51;
        static const unsigned int IRQ_QSPI1     = 52;
        static const unsigned int IRQ_ETH0      = 53;
    };
};


The number corresponding to each IRQ is the id provided by the implemented PLIC's documentation. The translation between IRQ and IC INT is done internally by the IC class.

4.8.5. Registering an Interruption

The external interruption IDs and their corresponding source must be in the documentation of the given RISC-V processor.

Therefore, in order to register an external interruption, inside its device driver init:

  • The pointer to its interrupt handler function must be assigned to the correct position of the IC::int_vector;
  • The interruption itself must be enabled via the IC interface;
  • The registered interruption's priority must be set and must be higher than the global threshold so that it is actually dispatched.


The steps aforementioned can be seen below:

// CODE TAKEN FROM NIC's DRIVER at riscv_gem_init.cc

// Install interrupt handler
IC::int_vector(IC::INT_GIGABIT_ETH, &int_handler); // To interact with IC's interface, the IC INTs enum must always be used, instead of the interruptions' id given in the machine's traits. 

// Enable interrupts for device
IC::enable(IC::INT_GIGABIT_ETH); // Enabling an EI automatically sets its priority to 1 (the lowest priority).

4.8.6. Setting the Global Threshold

IC class in EPOS has a threshold() method that sets PLIC's global priority threshold.

  • IC::threshold(0) - sets the global threshold to 0, allowing all enabled interruptions to be dispatched;
  • IC::threshold(7) - sets the global threshold to 7, disallowing all interruptions to be dispatched, even enabled ones;


By default, it is set as 0 in IC's initialization.

4.9. Timers

Os registradores mtime e mtimecmp para a geração de interrupções baseadas em temporização, com um dos usos mais práticos sendo aplicado em alarmes.
O registrador mtime é incrementado de acordo com RTCCLK, tornando-o independente do clock da CPU, permitindo seu uso em aplicações de baixa potência.
Para gerar uma interrupção de timer, precisamos que o registrador (m/s)tie esteja habilitado, enquanto setamos um valor maior que mtime em mtimecmp. Quando mtimecmp for igual ou maior à mtime, é gerada a interrupção.
O EPOS implementa interfaces para o uso de timers dentro do header time.h, onde podemos utilizar delays, alarmes e relógios.

4.9.1. Código de Referência

O código abaixo foi escrito como um app dentro do EPOS, e serve para mostrar a utilização do Delay e Alarm. O propósito é mostrar a interrupção do alarme, que interrompe a execução do código durante o delay e imprime na tela um texto, juntamente com o valor de mcause.

Código exemplo
#include <architecture.h>
#include <utility/ostream.h>
#include <utility/handler.h>
#include <time.h>

using namespace EPOS;
OStream cout;
static const int segundo = 1000000; // 1 segundo em micro segundos

void interrupt()
{
    cout << "Interrompi no meio do delay! " << endl;
    CPU::Reg reg = CPU::mcause();
    cout << "Causa: " << reg << endl;
}
Function_Handler handler = Function_Handler(&interrupt);
Alarm alarme = Alarm(segundo, &handler, 5);

int main()
{
    cout << "Hello world!" << endl;
    Delay(10*segundo);
    cout << "Depois do delay" << endl;
    return 0;
}
Saída esperada
Hello world!
Interrompi no meio do delay!
Causa: 7
Interrompi no meio do delay!
Causa: 7
Interrompi no meio do delay!
Causa: 7
Interrompi no meio do delay!
Causa: 7
Interrompi no meio do delay!
Causa: 7
Depois do delay

4.10. Problema da delegação de timers

Apesar de existir campo no mideleg para delegar interrupções de timer do modo Machine para o modo Supervisor, apenas esta configuração não é o suficiente para utilizar timers em modo Supervisor. O Risc-V não ativa automaticamente a flag STIP dos CSRs mip e sip quando chega uma interrupção de timer, independente da configuração do mideleg.

Para utilizar interrupções de timer em modo Supervisor é necessário programar um handler em modo Machine para receber a interrupção de timer e provocar uma interrupção de timer em modo Supervisor, mas como o campo STIP (Supervisor Timer Interrupt Pending) apenas pode ser modificado em modo machine, é necessário utilizar uma das alternativas descritas abaixo para sinalizar que a interrupção já foi tratada.

4.10.1. Trocar os papeis das flags Supervisor Timer Interrupt Enable (STIE) e STIP (Supervisor Timer Interrupt Pending).

Como é necessário que STIP e STIE tenham valor 1 para gerar uma interrupção de timer em modo supervisor, é possível inverter suas funções, assim a função destes campos se tornam:

  • STIP - Só pode ser alterado em modo Machine. É utilizado para habilitar e desabilitar as interrupções de timer em modo Supervisor.
  • STIE - Pode ser alterado em ambos os modos. É utilizado pelo handler do timer em modo Machine para sinalizar que há uma interrupção de timer pendente. Após a interrupção ser tratada em modo Supervisor, este campo é limpo sem precisar trocar de modo.

Desta forma quando uma interrupção de timer é recebida, os seguintes passos irão ocorrer:

  1. Interrupção de timer ocorre em modo Machine.
  2. Tratador de interrupções de timer (modo Machine) seta o bit STIE.
  3. Interrupção de timer ocorre em modo Supervisor.
  4. Tratador de interrupções de timer (modo Supervisor) limpa o bit STIE.

Apesar deste modo funcionar, pode haver problemas com a compatibilidade desta técnica com códigos que assumem a funcionalidade padrão dos campos trocados.

4.10.2. Utilizar syscall para limpar o campo STIP (Supervisor Timer Interrupt Pending).

Como o campo STIP só pode ser alterado em modo Machine, é necessário configurar uma syscall em modo Machine para limpar este campo. No tratamento do timer em modo Supervisor esta syscall é chamada e o campo STIP é limpo pelo modo Machine.

Desta forma quando uma interrupção de timer é recebida, os seguintes passos irão ocorrer:

  1. Interrupção de timer ocorre em modo Machine.
  2. Tratador de interrupções de timer (modo Machine) seta o bit STIP.
  3. Interrupção de timer ocorre em modo Supervisor.
  4. Tratador de interrupções de timer (modo Supervisor) chama a syscall para limpar STIP.
  5. Exceção de syscall ocorre em modo Machine.
  6. Tratador da syscall (modo Machine) limpa o bit STIP.

Apesar de ser a forma mais "correta" de solucionar o problema, exige que syscalls estejam implementadas no sistema e tem um overhead maior.

5. Troca de Contexto em RISCV64

A troca de contexto pode ocorrer em dois cenários principais:

  • OS Multitasking: o escalonador seleciona um processo ou thread X com maior prioridade em relação ao processo em execução Y, realizando a troca para que X passe a executar, enquanto Y retorna ao estado ready;
  • Interrupt Handling: quando ocorre uma interrupção, há uma troca de modos (user-mode para supervisor no caso de syscall, desde que esteja com mideleg, ou para machine no caso de timer interrupt), logo o hardware alterna automaticamente uma parte do contexto (pelo menos o suficiente para permitir que o manipulador retorne ao código interrompido). O manipulador pode salvar contexto adicional, dependendo dos detalhes dos projetos de hardware e software específicos. Muitas vezes, apenas uma parte mínima do contexto é alterada para minimizar o tempo gasto no tratamento da interrupção. Uma vez que o serviço de interrupção é concluído, o contexto em vigor antes da ocorrência da interrupção é restaurado para que o processo interrompido possa retomar a execução em seu estado adequado.


O ponto mais importante para se tomar cuidado em uma troca de contexto, é o auxílio e manuseio das diferentes pilhas (stacks) que o sistema operacional tem. Temos duas stacks principais, que são utilizadas em todo o tratamento de exceções, uma para machine mode (devido à restrição de timer interrupt no RiscV) e outra para supervisor mode; temos também uma para cada processo de usuário, dado que cada processo está em um segmento diferente e nenhum processo compartilha informações com outros (somente se for pedido para o sistema operacional o fazer).

5.1. Principais CSR da troca de contexto

Os registradores mstatus e sstatus são bastante importantes para o funcionamento das trocas de contexto.

Como as trocas de contexto podem ser acionadas por meio de interrupções, é importante ativa-las com os bits xIE. Um ponto importante sobre tais bits é que como eles estão nos primeiros 5 bits do registradores, eles são facilmente setados e limpos através das instruções csrsi (set immediate) e csrci (clear immediate). Por exemplo, no EPOS podemos habilitar IE no modo machine através do método mint_enable() e desabilitá-las através do método mint_disable().

As trocas de contexto não ocorrem em modo usuário, então também é necessário setar o bit SPP para o retorno para o modo de privilégio correto após o término da troca (para user mode).

Normalmente o registrador sepc é utilizado para voltar para a execução normal do programa quando a interrupção terminar de ser tratada, utilizando o comando sret após definir corretamente o modo de operação para user mode.

A fim de saber qual o stack pointer utilizado no modo anterior à execução de uma interrupção ou troca de processo, é necessário salvar os stack pointers em registradores de controle únicos do modo de operação, ou seja, o mscratch e scratch. Isso será mais abordado no próximo subtópico.

Para trocas de contexto entre processos, é necessário também trocar o espaço de endereçamento virtual, de acordo com as descrições do SATP. No caso do EPOS, como o escalonamento é baseado em Threads, quando a Thread seguinte a ser executada pertence a um processo diferente, é necessário trocar o SATP.

5.2. Diferentes stacks da troca de contexto

Como já dito anteriormente, essa é uma das partes mais importantes para ser administrado em uma troca de contexto, já que uma stack de um diferente modo de operação pode acessar algum dado de outro modo, caso seja feito um gerenciamento inadequado. Além disso, é sempre importante anotar qual era o stack pointer antes de trocar entre as diferentes stacks (o RiscV tem registradores específicos, como explicado no subtópico anterior, o mscratch e scratch), a fim de poder trocar o stack pointer quando retornar para o modo anterior.

Precisamos ter várias stacks, para poder preservar o comportamento do modo anterior à chamada para o próximo modo. Como exemplo, podemos citar as syscalls (muito usadas entre user e supervisor mode). Quando user-mode manda uma system call para supervisor, este ainda está usando a pilha de user, então, neste momento, deve ser salvo em scratch o stack pointer da user stack e, então, fazer a troca para supervisor stack pointer. Como mais interrupções podem ocorrer nesse momento (ainda mais problemáticas como é o caso do Timer Interrupt, que será feita uma chamada para machine trap handler), é necessário salvar o conteúdo de scratch na supervisor stack.

Quando todo o tratamento da interrupção estiver terminado, basta remover da stack do modo atual, todos os registradores salvos do contexto anterior (assim como o scratch contendo o stack pointer do modo anterior) e restaurar para o contexto anterior novamente. Deve-se então trocar novamente o stack pointer para o stack pointer anterior (mv sp, $STACK_ANTERIOR).

Há mais um problema para tratar: o comportamento de colocar/remover informações na stack depende do tipo de situação. Temos basicamente dois tipos de situações para tratar, interrupções e trocas de contexto. Para interrupções é basicamente a explicação anterior. Para trocas de contexto, deve-se colocar como retorno (registrador ra, ou seja, return address) o PC do contexto do novo processo; outras variáveis específicas devem ser passadas também, a fim de iniciar o contexto da forma correta.

5.3. Passo-a-passo de uma troca de contexto

Esse é um exemplo de como pode acontecer uma troca de contexto em um cenário de Machine Timer Interrupt (MTI).

  • Uma MTI é lançada e entra-se em machine mode para fazer o tratamento e, com isso, os registradores ficarão sujos. Para evitar isso, o contexto é salvo assim que ela é recebida na stack pointer de machine mode. No EPOS, o _int_m2s(), função responsável por fazer o tratamento em modo machine como supervisor, primeiro salva o contexto antes de fazer suas operações.
  • Por problemas específicos da arquitetura do RiscV, deve-se realizar os passos conforme a explicação sobre timers; dando reset no mtime e delegando para supervisor com STI; após isso, deve ser restaurado o contexto da stack de machine e dar mret.
  • Na trap de Supervisor, deverá salvar o stack pointer atual, trocar para a stack de supervisor, salvar o stack de user nesta stack e todo o contexto anterior; em seguida transfere o conteúdo da pilha do kernel para a stack do processo. Note que esta etapa não pode ser feita diretamente, porque não é possível obter esta stack sem sujar registradores.
  • Obtém-se o proximo processo a ser escalonado (feito indiretamente pela fila de Threads, no EPOS).
  • Restaura-se o contexto e o SATP do processo escalonado (também indiretamente pela fila de Threads, no EPOS), por fim é feito um jump para o antigo program counter e começa a execução do processo. Se o processo nunca foi escalonado antes, é necessário adicionar uma checagem extra na hora do pop ou só iniciar um contexto vazio ao criar um processo.

5.4. Código de referência

Será mostrado de exemplo um caso de interrupção de timer, onde passará pelo machine trap vector, será delegado para supervisor interruption, que por sua vez passará para supervisor_trap_vector, fará o escalonamento e retornará para user mode. O código será mostrado considerando que todas as interrupções estão sendo delegadas para supervisor mode, ou seja, somente timer interrupt passará por machine trap vector (pela restrição do RiscV já abordada anteriormente).

# like _int_m2s in EPOS
asm_trap_vector:
    # we need to save the previous stack pointer here, as we will change the stacks
    csrw    mscratch, sp

    # take the return address (which will be the next PC (if timer interrupt)) - this is optional dependent on the scheduler implemented
    mv      s0, ra

    # sp at this moment is at user_stack or supervisor_stack, change for machine_stack
    jal     switch_machine_stack
    csrr    s1, satp

    # save registers (context)
    addi    sp, sp, -136
    sd      s0, 0(sp)                 # save PC - 8
    sd      s1, 8(sp)                 # save SATP
    sd      mscratch, 16(sp)    # save old sp to restore later
   ...

   # logic to restore machine timer and deleg interruption to supervisor
   call reset_mtime_cmp

   # restore context and old sp
   addi    sp, sp, 136
   ld      s0, 0(sp)                 # load PC - 8
   ld      s1, 8(sp)                 # load SATP
   ld      mscratch, 16(sp)    # load old sp to restore later
   ...

   # change stack to old stack pointer
   mv sp, mscratch

   # mret will lead to asm_supervisor_trap
   mret

asm_supervisor_trap:
    # we need to save the previous stack pointer here, as we will change the stacks
    csrw    sscratch, sp

    # take the return address (which will be the next PC (if timer interrupt)
    mv      s0, ra
    addi    t1, t1, -8      # adjust return address (future PC to the previous instruction)

    # sp at this moment should be at user_stack, change to supervisor_stack
    jal     switch_kernel_stack
    csrr    s1, satp

    # save context
    addi    sp, sp, -136
    sd      s0, 0(sp)                # save PC - 8
    sd      s1, 8(sp)                # save SATP
    sd      sscratch, 16(sp)    # save old stack pointer
    ...

    # call function to handle general interruptions
    csrr	a0, sepc	         # Machine exception pc
    csrr	a1, stval	         # Machine bad address or instruction
    csrr	a2, scause   	 # Machine trap cause
    li      a3, 1                     # hartid = 1
    csrr	a4, sstatus  	 # Machine status
    call	s_trap               # trap handler (in C code to handle all interruptions and make the change of stacks)

   # s_trap must return the address to return
   csrw sepc, a0

   # restore context and old sp
   addi    sp, sp, 136
   ld      s0, 0(sp)                 # load PC - 8
   ld      s1, 8(sp)                 # load SATP
   ld      sscratch, 16(sp)    # load old sp to restore later
   ...

   # change stack to old stack pointer
   mv sp, sscratch

    sret

6. Modelos de Processo


De modo a entender o conceito de processo no EPOS, faz-se necessário destacar a importância de duas abstrações: Tasks e Threads. Enquanto um processo pode ser entendido como um programa em execução, uma Thread seria a entidade responsável por executar tal atividade e a Task forneceria um ambiente de execução (contexto) para uma ou mais threads encapsuladas por ela.

Fonte: Application-Oriented Operating Systems, Antônio Augusto Medeiros Fröhlich


Um outro modo de pensar, seria considerar as Tasks como as responsáveis por abstrair a porção estática do processo, o que englobaria aspectos como segmentos de código e data, além de um espaço de endereçamento. Já as Threads corresponderíam a parte ativa de um processo, ou seja, a unidade de execução da a aplicação.

6.1. Tasks


Como descrito anteriormente, fornecem contextos de execução isolados e controlados para suas threads, gerenciando recursos, escalonamento e coordenação entre as threads que a compõem. Cada Task é composta por:

  • Espaço de endereçamento;
  • Segmento de dados;
  • Segmento de código;
  • Thread principal;
  • Fila de threads.

6.1.1. Construtor Fork-like de Tasks


De modo a prosseguir com a implementação da funcionalidade de multi-tasking no EPOS, um construtor específico que atende tal necessidade se faz bastante útil. Os passos a seguir indicam o fluxo de criação de uma Task utilizando como base uma outra Task específica já existente:

1. Alocação de recursos: memória para o espaço de endereçamento, segmento de códigos, segmento de dados e um ponteiro de referência para a função da Thread Main da Task devem ser reservados.
2. Estabelecendo um contexto inicial: uma abordagem possível é a criação de cópias dos segmentos de código e dados da Task original para nova Task que está sendo criada. Para isso, copia-se os conteúdos desses segmentos para os novos segmentos reservados e executa-se um detach de forma a desanexar o novo segmento do espaço de endereçamento da Task atual.
3. Mapeamento dos novos segmentos: mapeia-se os segmentos de código e dados da nova Task para seu espaço de endereçamento.
4. Criação da Thread Main: instancia-se uma nova Thread que fará o papel de Thread Main.

6.2. Threads


É uma abstração mais leve e de baixo custo de criação e troca de contexto quando comparado às Tasks. Alguns recursos de uma Tasks, como o segmento de dados, são compartilhados entre as Threads que a compõem. Contudo, apresentam um contexto de execução e um segmento de dados exclusivo.

Um componente importante ao falar de gerenciamento das threads é o escalonador. O escalonador no EPOS pode ser adaptado de acordo com a aplicação e passível de configuração. Isso pode ser feito através dos arquivos de Traits das aplicações, que permite alterar aspectos como a política de escalonamento (FCFS, Round-Robin, Rate-Monotonic etc.) e quantum de tempo destinado as Threads.

6.2.1. Esclarecimento de Aspectos de Implementação das Threads no EPOS


Cria uma Thread no contexto da Task atualmente em execução cuja função a ser executada é entry e os parâmetros associados a ela estão descritos por an.


Cria uma Thread no contexto da Task atualmente em execução cuja função a ser executada é entry e os parâmetros associados a ela estão descritos por an. Além disso, a criação da thread é dada segundo as descrições presentes em conf.

  • State: define os estados que uma Thread pode assumir.
    • Running: indica que ela é a thread que está rodando na CPU.
    • Ready: pronta para ser executada.
    • Suspended: suspensa e não elegível para ser escalonada.
    • Waiting: esperando por recursos.
    • Finishing: em encerramento.
  • Priority: um inteiro que indica as prioridades que uma Thread pode assumir.
  • Criterion: uma forma de representar as prioridades de forma simbólica.
    • HIGH.
    • NORMAL - valor padrão designado a uma Thread.
    • LOW.
    • MAIN - prioridade designada à primeira Thread de uma Task. Pode ser um alias para NORMAL.
    • IDLE - prioridade da Idle Thread.
  • Configuration: constitui um pacote de configuração para threads. O valor stack_size indica o tamanho, em bytes, da pilha da Thread, e pode ser alterada nos traits da aplicação em Traits<Application>::STACK_SIZE.

6.2.2. Registradores Relevantes para Processos


Um processo em andamento, considerando a arquitetura RISC-V64 é representado por um conjunto de informações que incluem seu estado atual, dados e contexto de execução. Essas informações são mantidas estruturas como registradores de CPU e estruturas de memória. Entre alguns dos principais registradores que fazem parte do contexto de gerenciamento de processos estão:

  • PC (Program Counter): contém o endereço da próxima instrução a ser executada pelo processo.
  • Registradores de Controle e Status: contém informações sobre o estado atual do processo, como flags de condição e o modo de operação e privilégio do processador. Entre eles, estão (m/s/u)status, (m/s/u)cause, (m/s/u)vec e outros.
  • Registrador SATP: contém referência para o modo de endereçamento e o espaço de endereçamento do processo em execução.

7. Memória

7.1. Memory map e exemplo


O memory map é usado para descrever onde se encontra cada componente na memória física ou lógica. Alguns componentes são fixos na memória de acordo com a especificação do chip, como mediadores de hardware. No entanto, há algumas liberdades a serem tomadas ao definir os endereços de kernel e aplicativo, entre outros. O memory map exemplar abaixo foi construído de acordo com o memory map do SiFive E, disponibilizado em versões anteriores do software desta disciplina.

WhatsApp Image 2022 11 23 At 14.48.49


Hardware devices, no manual, recebem os mesmos endereços que em Sv32, apesar de terem 64 bits de endereçamento. Portanto, estendeu-se em 0's à esquerda tudo aquilo que se refere a MIO: 0x0000.0000.0000.0000 até 0x0000.0000.2000.0000 na memória física e lógica (mapeamento de um para um). Após 0x0000.0000.2000.0000, poderia ter outros hardware mediators, mas, como ficariam fora do escopo do projeto a ser construído na disciplina, não foram incluídos no modelo sugerido. Este espaço mencionado para futuros mediadores também é mapeado diretamente da memória lógica à física, de um para um.

O sistema operacional é passivo: está mapeado em todos os address spaces, no mesmo endereço. Ou seja, como uma aplicação não sabe da existência das demais durante sua criação, ela presume que tem todo o espaço de endereçamento para si e, portanto, aponta para 0x0000.0000.8000.0000 na memória física. Com a memória apropriadamente ligada, uma aplicação, por fim, recebe de fato seu endereço físico. Abaixo de APP DATA há endereços não canônicos, os quais serão ignorados e não serão traduzidos para a memória física.

Screenshot From 2022 10 24 00 35 29


Na parte inferior do memory map, pode-se observar que componentes referentes ao sistema, desde SYS_CODE a SYS_HEAP, são compreendidos pelos demais endereços que começam em bit 1, como 0xFFFF.FFXX.XXXX.XXXX.

Destinou-se aproximadamente 512 GB para a PHY_MEM (memória lógica), a qual é uma parte da memória que está mapeada para endereços contíguos em toda a RAM. A PHY_MEM é traduzida de acordo com o algoritmo expresso na imagem acima.

7.2. Modelos de memória

7.2.1. Flat Memory


Na ausência de uma MMU, o modelo linear considera a memória como um único bloco contínuo, sendo essa a abordagem padrão. Desta forma, toda a memória do sistema está disponível para a aplicação e não é preciso recorrer a esquemas de segmentação ou de paginação. Contudo, ainda pode-se utilizar de esquemas para controle de acesso assim como realizar a proteção dos endereços sem estender o espaço de endereçamento, ou seja, garantindo a unicidade de cada endereço, bem como mantendo a sequencialidade da memória.

7.2.2. Paginação


Em aplicações multitask, nas quais é de interesse do sistema que os processos não corrompam o segmento de dados por conta de um acesso indevido, é comum o uso de paginação em níveis, de modo em que surgem proporcionalmente novos espaços para o armazenamento de dados da memória principal, estruturados em páginas cujos endereços são mapeados e geridos pela unidade de memória, o que pode vir a servir para proteção (via controle do acesso) e aumento do espaço de endereçamento.

7.2.2.1. Virtual Page Number (VPN)


Um endereço virtual é normalmente dividido em duas partes: os VPNs e o offset de página. Há tantos VPNs em um endereço virtual quanto há níveis de paginação, pois eles servem como um index de onde, em cada tabela de páginas, está localizado a próxima tabela de páginas (ou, se for o nível 0, a página). Esse indexamento ocorre sucessivamente até o endereço virtual ser traduzido por completo (ver seção da MMU, onde este processo é detalhado).

7.2.2.2. Physical Page Number (PPN)


É claro que a memória física é acessada em algum momento. É importante notar que uma tabela de páginas/página é representada por um PPN, o que significa que ao indexar uma tabela de páginas com um VPN, é retornado, entre outras informações do PTE, o endereço físico resultante e, portanto, o PPN resultante. Logo, um endereço físico também contém tantos PPNs quanto níveis de paginação.

7.2.2.3. Page Table Entry (PTE)


O PTE é uma entrada em uma tabela de páginas, que contém algumas informações relevantes à paginação. No RISC-V, além de conter o endereço físico resultante, contém também alguns bits de permissão e controle. Se os bits de R ou X estiverem ativos, por exemplo, é interpretado que esta entrada aponta para uma folha (uma página) e verifica-se se o acesso a esta é permitido (bits R, W, X e U).

7.2.2.4. Translation Lookaside Buffer (TLB)


O TLB é um buffer que armazena as traduções recentes, de modo a reduzir o tempo gasto na tradução de endereços virtuais para físicos. É como um cache e, de fato, as vezes é chamado de address-translation cache.

7.2.3. Segmentação


Consiste de dividir a memória principal em segmentos, de modo a manter referências a tais segmentos junto a um offset, usado para navegar dentro do respectivo segmento. Os segmentos de maneira geral representam divisões que ocorrem de forma natural do código, tais como rotinas ou tabelas de dados, o que simplifica a leitura da perspectiva do programador quando comparado à paginação, por exemplo.

Esta técnica foi desenvolvida com a intenção de separar cada task dos dados que ela manipula. De modo análogo à paginação, são anexadas flags de validação aos endereços, sendo possível implementar memória virtual associando um bit que indique a presença na memória principal.

A segmentação como um paradigma de gerência de memória caiu em desuso, porém pode ser usado em combinação com paginação, de modo que o segmento indica uma tabela de página. A adoção desta técnica traz vantagens no que diz respeito a perda da necessidade de dispor os endereços de forma contínua além de reduzir a fragmentação de memória.

7.3. Conceitos do Kernel

Image

7.3.1. Kernel Space


O espaço do kernel é a região de memória reservada para o núcleo do sistema operacional e seus componentes associados, como código e estruturas de dados. Esse espaço é separado do espaço de usuário, que é a área de memória onde os aplicativos e processos de usuário são executados.

Quando um programa executa uma chamada de sistema ou solicita um serviço do kernel, ele deve fazer uma transição do espaço do usuário para o espaço do kernel. Isso é feito através de um mecanismo denominado de chamada de sistema, que permite ao programa solicitar um serviço ao kernel, invocando uma operação ou função específica. A chamada de sistema muda o contexto de execução do programa do espaço do usuário para o espaço do kernel, onde o serviço solicitado é executado. Depois que o serviço é concluído, o contexto de execução do programa é trocado novamente para o espaço do usuário.

7.3.2. Kernel Stack


Em casos em que ocorre a troca de contexto entre dois processos distintos, simplesmente salvar os registradores do processo anterior não é suficiente, pois, por não compartilharem a memória, eles não têm o mesmo espaço de endereçamento. Para encapsular o contexto privado de um processo, é usada a pilha do kernel.

Quando há uma alta frequência de chamadas de sistema e interrupções, o kernel é projetado para permitir solicitações reentrantes, permitindo que vários processos utilizem seus serviços. Para isso, é necessário que cada processo tenha sua própria referência particular - dentro do espaço de endereçamento - à pilha do kernel, que é mapeada diretamente na memória e, portanto, requer uma região de endereços contíguos.

7.3.3. User/Kernel data handling


Quando o modo de privilégio é modificado para o modo kernel é interessante, para garantir o encapsulamento dos contextos, utilizar apenas stacks do kernel. Para isso, pode-se utilizar o registrador mscratch, que é um registrador de leitura/escrita dedicado ao uso em machine mode, para guardar um ponteiro para a kernel stack.

Screenshot 2023 04 16 At 17 24 03 Riscv Privileged V1.10.pdf


Nesse sentido, durante o início do handler da ecall, pode-se realizar um swap de sp com mscratch e, a partir disso, o kernel utilizará apenas a kernel stack para suas operações. Dessa forma, caso seja necessário sujar registradores, esses podem ser salvos na kernel stack durante a entrada da ecall e restaurados na sua saída, onde também será realizado o swap de sp e mscratch novamente.

Durante uma ecall são passados parâmetros por meio dos registradores a0 a a7, conforme abordado na seção Conveções de chamada. O tipo dos valores dos registradores depende de qual ecall está sendo chamada. Para o Linux, por exemplo, as system calls e os tipos de seus parâmetros podem ser verificadas em: https://www.ime.usp.br/~kon/MAC211/syscalls.html .

Algumas system calls podem exigir mais do que o número de registradores disponível, caso em que os parâmetros podem ser salvos na pilha de usuário e 1 registrador pode ser utilizado para passagem de seu endereço. O procedimento também ocorre para syscalls que necessitem de acesso à memória especificamente, como é o caso de copy_from_user e copy_to_user, juntamente com suas variações, por exemplo. Nesses casos, é necessário que o kernel realize uma checagem dos endereços a serem acessados, de forma que o processo não obtenha acesso a regiões do kernel, à memória de outros processos, já que podem corrompê-las, ou possibilitar a escrita em regiões de memória destinadas somente à leitura.

Nos sistemas operacionais atuais, a checagem consiste em dois passos: verificar se o endereço está dentro do limite para o processo em questão e delegar o restante da checagem às MMUs.

Para a verificação do limite do processo, guarda-se um ponteiro para o maior endereço que o processo pode acessar. No caso de system calls provenientes de threads de usuário, esse limite corresponde ao endereço inicial do kernel. Para system calls provenientes do próprio kernel, esse limite corresponde ao limite físico da memória.

Depois disso, o kernel aloca uma exception table contendo todos os endereços que acessam a memória, juntamente com um endereço de onde resumir caso esses endereços gerem um page fault (fixup code), como pode-se observar a seguir:

Exception Table Entry


Nesse sentido, caso a MMU gere um page fault, o page fault handler verifica se o endereço é proveniente do modo de usuário e está na tabela, caso em que se considera que o endereço acessado se deve a um parâmetro indevido na system call e destina-se a execução ao endereço de fixup, que normalmente consiste em retornar um SIGSEGV. Caso contrário, considera-se que o page fault foi causado por um bug do kernel. Além disso, pode-se configurar acessos em user e supervisor mode na memória por meio de PMP (seção Proteção de Memória Física (PMP)). O processo de checagem do page fault handler pode ser visto abaixo:

Page Fault Handler Flow

Fixup Exception

7.3.4. Código de referência

Start


Inicialmente, o stack pointer é configurado com o endereço da kernel stack, pois se inicia a execução em machine mode e qualquer salvamento deve ser realizado nessa pilha nesse momento.

Depois disso são configurados os registradores mstatus (Registrador (m/u/s)status) para retornar ao user mode, o registrador mepc (CSRs) para que a execução seja retomada do endereço de user após um mret (Instruções de Retorno) e mtvec (CSRs) com o endereço de m_trap para onde será direcionada a execução após uma ecall em user mode. A proteção de memória foi então desativada por completo (Proteção de Memória Física (PMP)), por não ter relevância ao projeto. Então, as interrupções em modo máquina são habilitadas, o endereço da kernel stack é salvo em mscratch e o stack pointer é modificado para apontar para a user stack. Por fim, executa-se um mret.

User Trap


Com isso, a execução continua em user mode do endereço de user, onde é realizada a operação de salvar o valor 1 na user stack e realizar uma system call, passando como parâmetro o endereço na user stack onde o valor foi salvo.

Após a ecall, a execução passa para machine mode. É realizado um swap do stack pointer com o endereço da kernel stack, salvo em mscratch. Com isso, o endereço da user stack fica salvo em mscratch e o kernel utilizará apenas a kernel stack. O registrador t0, que será utilizado, deve ser salvo na kernel stack, pois não há garantia de que se pode salvar na user stack. Diante disso, checa-se o endereço passado ao kernel (por a0) para garantir que o processo de usuário não está tentando acessar a kernel stack. Caso esteja, o m_trap apenas deixa de acessar o endereço. Caso contrário, o m_trap soma 1 ao valor presente na user stack e salva-o novamente no mesmo endereço. Então, o retorno de m_trap é configurado para continuar a execução de user, modifica-se o m_trap para last_m_trap, restaura-se o valor de t0 e realiza-se um swap de sp com mscratch novamente. Alternativamente, caso o objetivo seja retornar um endereço acessível ao usuário, pode-se utilizar a0 para tal.

Por fim, o user continua a execução lendo novamente o valor em sua stack, agora igual a 2.

7.4. Sistemas de Memória Virtual do RISC-V (VMS)


Os VMS são os sistemas que ditam como serão feitas traduções de endereços virtuais para endereços físicos.

7.4.1. SATP (Supervisor Address Translation and Protection)


O SATP é um CSR (Control and Status Register) que controla qual VMS é usado e, portanto, a paginação. É importante notar que em modo M os endereços não são traduzidos por padrão e, apesar de ser possível escrever no SATP nesse modo, a paginação só é ativada em modo S/U. O formato do registrador segue abaixo.

Image

7.4.1.1. MODE


O campo MODE do registrador SATP é usado para indicar o modo de endereçamento de página, que determina como os endereços virtuais serão traduzidos em endereços físicos. Este campo pode ter os seguintes valores:

Image


Caso você tente escrever no campo um valor reservado para o futuro ou então o valor correspondente a um modo que seja válido, a escrita completa feita no SATP é ignorada e nenhum valor será alterado.

Quando se trata de RISC-V 64 bits, o modo mais comum de endereçamento é o Sv39, que usa uma tabela de páginas de três níveis para traduzir endereços virtuais em endereços físicos.

Neste modo, o formato dos endereços é ilustrado abaixo.

Image

7.4.1.2. ASID


O campo ASID (Address Space Identifier) é opcional e pode ser usado para deixar o flush da TLB mais eficiente.

7.4.1.3. PPN


É necessário também definir qual será o PPN da tabela de páginas raiz, no primeiro campo do registrador SATP. Este campo definirá o endereço da tabela de páginas raiz, que será usado na tradução de endereços.

7.4.2. Diferenças


Com exceção do modo Bare, que é usado para memória linear, os outros modos aumentam o espaço de endereçamento em troca de latência. O Sv48 tem quatro níveis de paginação; já o Sv57 e Sv64 estão apenas reservados para uma implementação futura.

Vale citar que a especificação RISC-V comenta que implementações podem fornecer suporte a VMS “menores” sem muito custo adicional para manter compatibilidade com software supervisor que rode apenas com esses sistemas.

7.5. Unidade de Gerenciamento de Memória (MMU)


A MMU é responsável por traduzir endereços virtuais em endereços físicos: os detalhes desse comportamento dependem da arquitetura e do chip específicos. Aqui, será detalhada a MMU do SiFive FU540, no modo Sv39.

7.5.1. Especificidades


Há um cuidado a ser tomado quando se trata do SiFive FU540, pois este chip tem dois cores: um E51 e um U54, onde apenas o U54 (core 1) implementa uma MMU. Tentar escrever no SATP estando no core 0, a fim de habilitar a paginação, resultará em operação ilegal.

O tamanho de página do Sv39 é de 4 KiB por padrão, podendo ter também 2 MiB ou 1 GiB. A fim de manter a invariante de que uma tabela de páginas é exatamente o tamanho de uma página, no Sv39 cada tabela de páginas contém 512 (2^9) entradas cada, conseguindo mapear com três níveis um total de 512 GiB.


7.5.2. Instrução SFENCE.VMA (Fence de Gerenciamento de memória do supervisor)

A instrução sfence.vma é utilizada para sincronizar atualizações de estrutura de dados de gerenciamento de memória, armazenadas em memória, com execuções correntes. A execução de instruções ocasiona leituras e escritas implícitas nas estruturas de dados em questão. Entretanto, essas referências implícitas normalmente não estão ordenadas no que diz respeito às instruções “load” e “store” explícitas. Ao executar uma instrução sfence.vma garante-se que quaisquer “store” prévio, que já está visível à hart atual, sejam ordenados antes de determinadas referências implícitas realizadas pelas instruções subsequentes naquela hart.

Existem, basicamente, dois parâmetros opcionais que servirão para restringir o escopo do esvaziamento da cache:

  • rs1, que indica qual tradução de endereço virtual foi modificado na tabela de páginas.
  • rs2, que fornece o identificador de espaço de endereço do processo cuja tabela de páginas foi modificada. Se o argumento passado for “x0” para ambos os parâmetros, todo o cache de tradução é liberado.


Para o caso em que uma página foi modificada, rs1 pode especificar um endereço virtual dentro dessa página para efetuar uma translation fence dentro dela própria. Além disso, para o caso em que páginas foram modificadas dentro de um ASID, é possível passar seu valor em rs2.

Abaixo, segue estrutura de uma instrução sfence.vma:

Screenshot From 2022 10 16 21 52 12


Vale ressaltar que, se rs1 = x0 e rs2 = x0, o sfence.vma irá realizar um flush completo da TLB, zerando todas as suas entradas, o que é uma recomendação para implementações mais simples.

Implementações mais simplórias podem costumeiramente ignorar o endereço virtual em rs1 e o valor de ASID em rs2 e sempre performar um “flush” global.

7.5.3. Proteção de Memória Física (PMP)

PMP é uma implementação opcional, que o U54 implementa. Por default os modos Supervisor e User não possuem permissão de acesso à memória, qualquer tentativa de acesso irá causar uma trap que deverá ser cuidado pela hart.

Na configuração de 64 bits apenas os registradores pmpcfg pares estão disponíveis para uso (0,2,...14). No U54, os registradores pmpcfg0 a pmpcfg3 estão implementados, mas apenas o pmpcfg0 está definido, enquanto os outros estão settados em 0. Por isso, é possível configurar no máximo 8 regiões de memória.

Cada campo do registrador pmpcfg, possui um registrador pmpaddr correspondente, sendo assim o campo pmp0cfg do registrador pmpcfg0 correponde ao registrador pmpaddr0.

Screenshot From 2022 10 16 22 40 07


Toda vez que ocorre um acesso a memória, é verificado todos os registradores pmp. Se o endereço for maior ou igual a endereço i do pmpaddr i, mas menor que o endereço pmpaaddr i+1, então o registrador de configuração pmpaddr i + 1 decide se esse acesso pode prosseguir; caso contrário isto gera uma exceção de acesso.

Screenshot From 2022 10 16 22 44 59


Cada campo de configuração segue a separação a seguir:

Screenshot From 2022 10 16 22 41 32


Os campos R, W e X são respectivamente para Leitura(R), Escrita(W), Execução(X). Address Matching (A) , e possui 4 configurações, conforme se vê a seguir:

Screenshot From 2022 10 16 22 43 21


Usualmente a configuração NAPOT (naturally aligned power-of-2) é utilizada, que alinha o endereço a 8 bytes. Para alinhar a 4 bytes é necessário utilizar a configuração NA4. E por último a configuração TOR faz com que a verificação seja de forma contrária, sendo que o endereço superior é de um registrador i e o endereço inferior de um registrador i+1. O U54 permite o mapeamento de até 8 regiões de memória, porém caso seja utilizada a configuração TOR, apenas 4 regiões estarão disponíveis.

Locking (L), caso seja setado este bit, a memória estará travada, impedindo leituras, escritas e execuções. Para conseguir acessar a memória é necessário resetar a máquina, ignorando as configurações de R/W/X. A memória poderá ser acessada em modo Machine, mas respeitando o R/W/X.

7.5.4. Tradução de um endereço lógico para físico com 3 níveis

  1. O satp.PPN fornece o endereço base da tabela de páginas de segundo nível e VA2 (L2) fornece o índice de primeiro nível, então o processador lê o PTE localizado no endereço (satp.PPN x 4096 + VA2 x 8);
  2. Este PTE contém o endereço base da tabela de páginas de primeiro nível e VA1 (L1) fornece o índice de segundo nível, para que o processador leia a folha PTE localizada em (PTE.PPN x 4096 + VA1 x 8);
  3. Repete 2 para VA0 (L0);
  4. O campo PPN da folha PTE e o deslocamento de página (12) forma o resultado final: o endereço físico é (LeafPTE.PPN x 4096 + VAOffset).

Screenshot From 2022 10 16 22 14 50

7.5.5. Código de Referência Sv39


O código a seguir demonstra a habilitação da paginação em Sv39. É importante notar que parte essencial desse processo é o mapeamento na memória das tabelas de páginas, que não será ilustrado pois depende de específicos do sistema, como o memory map.

Image


Na primeira linha da função entry, que está sendo executada em modo máquina, é dito que, no próximo mret, haverá uma troca de contexto para o modo supervisor. Isto é necessário pois apesar do SATP habilitar a paginação assim que definido o campo MODE, a paginação só é efetiva no modo S/U, pois é esperado que o modo M não crie exceções (como um page fault). Portanto, no modo M os endereços não são traduzidos por padrão, mesmo com a paginação ativa.

Há outra maneira de resolver este empecilho: mstatus.MPRV = 1. Este campo define que mesmo no modo M, devem ser obedecidas as regras de tradução. No entanto, realizar a troca de contexto é certamente mais correto e será mais util durante o percurso do projeto. No momento, simplesmente circunventamos os obstáculos que acompanham essa troca: desativamos a proteção de memória nas linhas 3-4.

Na linha 7 garantimos que o SATP está zerado, pois depois que sairmos do modo máquina precisaremos dispor a memória, o que causaria problemas se a paginação estivesse ligada.

Nas linhas 8-9, definimos o program counter para a função main e saímos da trap.

Image


Na função main, chamamos 'lay_memory', que cria páginas de 1 GiB mapeando o hardware e a RAM. Pular esta parte ou realizar a disposição das tabelas de página de maneira incorreta muito provavelmente causará um page fault assim que a MMU for habilitada.

Na 'enable_mmu', escrevemos MODE = 8 no SATP, que representa o modo Sv39, e passamos o PPN da tabela de páginas raiz (neste caso, de nível 2). Como este endereço depende do mapeamento da memória, apenas passamos RAM_BASE para ilustrar que, se for passado um endereço físico, é necessário remover o page offset para transformá-lo em PPN.

Após a habilitação, chamamos SFENCE.VMA para garantir que a TLB não contém traduções prévias que, com a modificação do esquema de tradução, estariam incorretas.

As funções usadas:

Image

7.5.6. Código de Referência Sv32


O código abaixo foi produzido por alunos de semestres anteriores e, apesar de não garantir-se a corretude, pode vir a ser útil pois especificam a construção das tabelas de páginas em Sv32:

Ainda em machine mode, o SATP é setado para 0, para assegurar que está no modo Bare.

Screen Shot 2022 10 17 At 02.09.29


Ainda em Machine mode, é alterada as configurações de proteção de memória:

Screen Shot 2022 10 17 At 02.09.49


Para mapear a memória inteira, em nosso linker script é declarado a região de memória da RAM, sendo identificado o inicio e o final, respectivamente, por _memory_start e _memory_end.

Screen Shot 2022 10 19 At 20.52.52


Acessamos essas regiões de memória utilizando o "extern" e pegamos a suas referências:

Screen Shot 2022 10 19 At 20.53.21


Para facilitar o mapeamento, declaramos as flags que de uma Page Table Entry (PTE), e criamos uma macro LEAF que será utilizada pelas folhas:

Screen Shot 2022 10 19 At 20.54.05


Temos 128MB, mas iremos mapear apenas 64MB de RAM, teremos 1 entrada lv2, 32 entradas lv1 (32 * 2MB) e 512 entradas lv0 (512 * 4Kb). Criamos uma máscara com os bits mais significativos 54-63, que como mostrado precisam ser zerados, e os 12 bits menos significativos, que são o offset.

Screen Shot 2022 10 19 At 21.41.29


Começamos o mapeamento pela tabela lv2, posicionando ela no endereço FLAT_PAGE, e avançando o ponteiro em uma página. Criamos a PTE setando o bit VALID e avançamos o endereço de PAGE_ENTRIES, mais uma vez, preparando para a próxima tabela.

Screen Shot 2022 10 19 At 20.56.41


Com o endereço de "page_directory", criamos a tabela lv1, e suas PTE's de forma semelhante.

Screen Shot 2022 10 19 At 21.41.46


Por fim, mapeamos toda a extensão da memória utilizando os bits LEAF (R,W,X,A,D) e depois retornamos o MODE concatenado do endereço da page_directory, sendo posteriormente setado no registrador SATP.

Screen Shot 2022 10 19 At 21.41.58


O retorno da função _entry é utilizado para setar o valor do SATP e é feito um sfence.vma para garantir que a TLB vai estar zerada e o valor lido do SATP estará correto.

Screen Shot 2022 10 17 At 02.21.10

7.6. Cache


Representa o nível da hierarquia de memória entre o processador e a memória principal que se beneficia da localidade espacial e temporal para armazenar dados mais utilizados pelo processador.

A implementação da cache não é definida pela ISA do RISC-V. Portanto, para ativar a cache é necessário verificar no manual da máquina como ela é implementada.

N-associativa

Um cache n-associativa consiste em um número de conjuntos, cada um dos quais consiste em “n” blocos. Cada bloco da memória mapeia para um conjunto único na cache fornecida pelo campo de índice onde um bloco pode ser colocado em qualquer elemento desse conjunto.

7.6.1. Fence


No RISC-V ISA base, a thread do hardware do RISC-V observa suas próprias operações de memória como se elas executassem sequencialmente na ordem do programa. O RISC-V possui um modelo de memória relaxado entre harts, exigindo uma instrução FENCE explícita para garantir a ordenação entre operações de memória de diferentes harts RISCV.

A instrução FENCE é usada para ordenar acessos à memória e E/S do dispositivo conforme visualizado por outros harts RISCV e dispositivos externos ou coprocessadores. Qualquer combinação de entrada do dispositivo (I), saída do dispositivo (O), leituras de memória (R) e gravações de memória (W) podem ser ordenadas em relação a qualquer combinação do mesmo.

Informalmente, nenhum outro hart RISC-V ou dispositivo externo pode observar qualquer operação no conjunto sucessor após um FENCE antes de qualquer operação no conjunto predecessor antes do FENCE. O ambiente de execução definirá quais operações de E/S são possíveis e, em particular, quais instruções de carregamento e armazenamento podem ser tratadas e ordenadas como operações de entrada e saída do dispositivo, respectivamente, em vez de leituras e gravações na memória.

Por exemplo, dispositivos com entrada e saída com memória mapeada normalmente serão acessados com uncached loads e stores que são solicitados usando os bits de I e O em vez dos bits R e W. As extensões do conjunto de instruções também podem descrever o novo coprocessador Instruções de E/S que também serão ordenadas usando os bits de E/S em um FENCE.


7.6.2. Cache L2


Controlador de cache L2

Este controlador é usado para fornecer acesso a cópias rápidas de memória para mestres em um Core Complex. O Controlador de Cache Nível 2 também atua como gerenciador de coerência baseado em diretório.

O controlador é configurado em 4 bancos. Cada banco (banks) contém 512 conjuntos (sets) de 16 ways, e cada way representa um bloco de 64 bytes.

Esta subdivisão em bancos ajuda a facilitar o aumento da largura de banda disponível entre os mestres da CPU e a cache L2, pois cada banco possui sua própria porta interna TL-C de 128 bits. Assim, várias requisições a diferentes bancos podem ocorrer em paralelo. A organização geral do controlador de cache L2 é mostrada na figura a seguir.

2


O controlador de cache L2 permite que suas SRAMs atuem como memória endereçada diretamente no espaço de endereço do Core Complex ou como um cache que é controlado pelo L2 Cache Controller, e que pode conter uma cópia de qualquer endereço que possa ser armazenado em cache.

Os ways da cache podem ser habilitados ou desabilitados escrevendo no registrador WayEnable. Uma vez que um way de cache é habilitado, ele não pode ser desabilitado a menos que o FU540-C000 seja reiniciado. O caminho de cache L2 de maior número é mapeado para o espaço de endereço L2-LIM mais baixo e o menor (way 1) ocupa o intervalo de endereço L2-LIM mais alto. À medida que os ways de cache L2 são habilitados, o tamanho do espaço de endereço L2-LIM diminui.

Nessa implementação, o controlador da cache L2 é configurado em 4 bancos, cada banco contém 512 conjuntos de 16 ways, e cada way contém um bloco de 64 bytes. A figura a seguir mostra esse mapeamento.

3


A cache L2 inicia desabilitada, habilitada escrevendo no registrador wayenable o índice do maior way habilitado. Este registrador é inicializado com 0 e só pode ser incrementado. Sendo assim, para habilitar a cache L2 é necessário somente alterar o registrador wayenable com o número de ways desejado.

Image


L2 Loosely Integrated Memory (L2-LIM)

Quando os ways de cache estão desabilitados, eles são endereçáveis no espaço de endereço L2 Loosely Integrated Memory (L2-LIM). A busca de instruções ou de dados do L2-LIM fornece um comportamento determinístico equivalente a um hit na cache L2, sem possibilidade de fault na cache. Os acessos ao L2-LIM sempre têm prioridade sobre os acessos via cache, que visam o mesmo banco de cache L2.

O Controlador de cache pode controlar a quantidade de memória cache que uma CPU master pode alocar usando o registrador WayMaskX. Observe que os registradores WayMaskX afetam apenas as alocações, mas as leituras ainda podem ocorrer de maneiras mascaradas. Como tal, torna-se possível bloquear formas de cache específicas, mascarando-as em todos os registradores WayMaskX. Nesse cenário, todos os mestres ainda podem ler dados nos ways de cache bloqueados, mas não podem alocar dados.

L2 scratchpad

O SiFive L2 Cache Controller possui uma região de endereço de scratchpad dedicada que permite alocação no cache usando um intervalo de endereços que não é suportado pela memória. Esta região de endereço é indicada como L2 Zero device no mapa de memória.

Mapa de memória

Nas figuras abaixo são apresentados alguns registradores para o controlador de cache L2.

4

7


Registradores

Registrador "config"

Responsável por guardar as configurações da cache L2.

5


Registrador "wayEnable"

Este registrador é inicializado com 0 no reset e o valor somente pode ser aumentado. Valor mínimo de way é 1.

6


Registrador "wayMask"

0-15: podem ser modificados.
63-16: reservados.

8


IDs das masks:

10


Código de configuração para cache

O código assembly a seguir habilita a capacidade máxima disponível para a cache L2, representado pelo valor 0x06091004. Portanto, a capacidade é: quatro bancos, 512 conjuntos, 16 ways e blocos de 64 bytes.

11

8. System Calls

8.1. O que é System Calls

As chamadas de sistema, system calls ou syscalls, são o mecanismo programático pelo qual um programa solicita um serviço do kernel do sistema operacional, os quais são disponibilizados para aplicações de usuários. Geralmente as chamadas de sistema são oferecidas para as aplicações em modo usuário através de uma biblioteca do sistema (system library), como o glibc no Linux, que prepara os parâmetros, invoca a chamada e, no retorno desta, devolve à aplicação os resultados obtidos e permitem a execução de atividades do kernel pelas requisições feitas pelo usuário, então, quando o usuário necessita fazer uma requisição para o kernel como abrir um arquivo, criar um processos, entre outras coisas, é necessário trocar do modo usuário para o modo kernel. A chamada de sistema é dependente da arquitetura, então, cada ISA (Instruct Set Achitecture) têm as suas chamadas de sistema. O RISC V utiliza instruções de sistema (System Instructions) para fazer isso.
Com as chamadas de sistemas é possível, por exemplo, definir acesso a recursos de baixo nível como alocação de memória, periféricos e arquivos. Além disso, são as chamadas de sistemas que permitem a criação e a finalização de processos.

8.2. CSRs relacionados à System Calls

É necessário configurar os seguintes registradores:

(M/S)status

  • (M/S)PP : define o nível de previlégio ao invocar o mret.
  • (M/S)PIE: quando for retornado 'mret', o MIE receberá o MPIE.
  • (M/S)IE: desabilita interrupções.


Medeleg
Delega o atendimento de exceções, de usuário ou supervisor, ao modo supervisor. Cada bit
corresponde a uma exceção que será atendida.
Por padrão todos os traps são tratados como modo máquina.

(M/S)tvec

  • Base: contém o endereço do tratador responsável tratar a exceção.
  • Mode: o nível de privilégio no qual o tratador será executado.


(M/S)epc
Salva o endereço da instrução que será executada após a chamada de M / Sret.

(M/S)cause
É o registrador que armazena o código do evento que causou a trap. Contém um campo de Interrupt
e um campo Exception Code. O campo Interrupt contém 0 quando é uma exceção e 1 quando é uma
interrupção, já o campo Exception Code contém o valor da interrupção ou da exceção.

8.3. Instrução Ecall

Ecall
Fonte: The RISC-V Instruction Set Manual - Volume II: Privileged Architecture


Ecall é a instrução que levanta uma exceção para a mudança de modo privilegiado. Essa exceção depende do modo em que ela está presente, por exemplo, a exceção é diferente no U-mode, S-mode e M-mode, podendo ser environment-call-from-U-mode, environment-call-from-S-mode ou environment-call-from-M-mode.

8.4. Instruções de Retorno

Mreturet
Fonte: The RISC-V Instruction Set Manual - Volume II: Privileged Architecture


URET/SRET/MRET são as instruções que retornam de um modo de privilégio maior para um com privilégio menor. A MRET é sempre disponibilizada e, a SRET e a URET, são disponibilizadas apenas se o S-mode for suportado, caso não tenha, é feita uma exceção de instrução ilegal. A instrução xRET pode ser executada no modo de privilégio x ou maior, quando executada xRET seta o valor de PC no registrador xepc.


8.5. Conveções de chamada


As convenções de chamadas do RISC-V passam os argumentos através de registradores específicos. São utilizados dois tipos de registradores, até oito registradores de inteiros, a0 - a7, e até oito registradores de floats, fa0 - fa7. Mas o uso dos registradores de ponto flutuantes depende do OS, como, por exemplo, o kernel do Linux não os utilizam.

ConvencaoDeChamada
Fonte: The RISC-V Instruction Set Manual - Volume I: Unprivileged ISA


Para passar ao SO qual syscall é necessária ser feita, é utilizado o registrador a7. Cada syscall possui um número específico, como o print é 0, o read é 63, entre outros. Já o resultado é passado para o registrador a0, porém, na hora da chamada, pode também ser utilizado para passar argumentos.

Codigo1

8.6. Exemplo de chamada de Sistema


O código que será apresentado tem como base o código feito para o seminário de System Calls do semestre 2020.2, pelos alunos Arthur Mesquita Pickcius, Arthur Moreira Rodrigues Alves e Maria Eduarda de Melo Hang. Como ele foi desenvolvido para funcionar em RISCV 32 foi necessário alterá-lo para rodar em RISCV 64, para isso alteramos as instruções sw e lw para sd e ld. A estrutura foi dividida em 4 partes, boot, handlers, libs e user. O boot inicializa o sistema, o handlers trata as interrupções ou exceções recebidas, que no nosso caso seriam apenas as exceções geradas pela instrução ecall, o libs tem as chamadas de sistema e o user possui as chamadas de sistema no nível usuário.

Boot

O registrador sapt é zerado desabilitando a MMU, o gp e sp são configurados e a seção BSS é limpa. O registrador mstatus é configurado com o primeiro valor associado ao campo MPP com o valor do modo supervisor, que seria 01. O segundo valor é o MPIE que habilita as interrupções ao retornar do modo de máquina com mret. O terceiro valor é o MIE para desabilitar a interrupção do boot.

Codigo2


Seguido desta parte o registrador mepc é alterado para que a configuração do modo supervisor seja a próxima instrução.

Codigo3


Para fazer o tratamento de exceções e interrupções delegadas ao modo máquina, existe o tratador machine_mode_trap_handler adicionado ao mtvec. Ele apenas limpará o mip e atualizará o mepc para a próxima instrução que causar interrupção ou exceção, sendo mepc + 4.

Codigo4


Após isso, habilitamos apenas interrupções do tipo software interrupt geradas pelo ecall, no registrador mie.

Codigo5


Para fazer a delegação da exceção de chamada de ambiente do modo usuário para o modo supervisor, o registrador medeleg deve ser modificado no bit 8.

Codigo6


O registrador ra é alterado para label 3f, está utiliza a instrução wfi, wait for interrupt, para aguardar por interrupções e a instrução mret é utilizada para sair do modo máquina e ser iniciada a configuração do modo supervisor.

Codigo7


No modo supervisor é necessário alterar alguns registradores para mudar o modo que será executado na main, indicar o tratador e habilitar a interrupção de software.
A alteração do modo de operação da main precisa da alteração no registrador sstatus, definindo o campo SPP para modo usuário, que seria 0, e habilitando as interrupções nos campos SIE e SPIE, com 1.

Codigo8


Também é feita a configuração do registrador sie para 1 habilitando as interrupções de software no modo supervisor.

Codigo9


No fim, o tratador do modo supervisor é definido no registrador stvec e o sepc é alterado para a label init_uart, está é a responsável por cuidar da UART para a system call print executar corretamente e trocar para a main em seguida.

Codigo10



Handlers
Os handlers, ou tratadores, é a parte responsável por tratar as interrupções e exceções para um modo de operação específico. Neste código existe um para o modo máquina e outro para o modo supervisor.
O tratador do modo máquina é relativamente simples, pois ele não é responsável por tratar as chamadas de sistema. É basicamente fazer a limpeza do mip e alterar o mepc para mepc + 4, que seria uma instrução após a que gerou a interrupção ou exceção.

Codigo20


O tratador do modo supervisor é mais complexo, pois ele precisa verificar se a exceção é do tipo user-mode ecall, número 8, avaliar qual system call está sendo feita, chamá-la, limpar o registrador sip e atualizar o sepc para a próxima instrução.
Inicialmente é necessário salvar alguns registradores que serão alterados durante a execução. Não precisamos salvar todos os registradores porque sabemos quais serão alterados.

Codigo11


Depois será verificado se é uma exceção ou interrupção através do campo interrupt do registrador scause. Se for uma exceção, direciona para a label exception_handler que verificará se é uma syscall, se não for apenas é feita a recuperação dos registradores, o sip é limpado e o sepc é alterado para a

Codigo12


No caso de ser uma exceção, é preciso verificar se é a número 8, essa verificação é feita através do campo Exception Code do registrador scause. Se for o caso, o código é direcionado para a label syscall_handler, que tem como função ver qual syscall está sendo solicitada. Se não for o código é direcionada para a label clean.

Codigo13


Sabendo que é uma syscall, é necessário ver qual delas está sendo chamada através do registrador a7. Se a7 for 0 é a syscall_print e se for 1 é syscall_increment. São utilizados dois desvios condicionais para essa verificação e caso não seja nenhuma dessas duas syscalls o código é direcionado para a label clean.

Codigo14


Foram criadas labels para cada syscall e essas labels fazem o armazenamento de alguns registradores que serão utilizados pelas respectivas syscalls, além dos que foram configurados no início do tratador. No fim de ambas as chamadas a label clean é chamada.

Codigo15



Libs
Este código tem duas chamadas de sistema, uma para mostrar o “Hello, World!” no terminal, que é o print, e outra para incrementar um valor passado como argumento e retorná-lo, que é o increment. A convenção do Linux e do RISCV foram seguidas em relação aos registradores a0 e a1. Os argumentos foram considerados como os registradores de a0 a a5, o número da system call como a7 e o retorno como os registradores a0 e a1.
A syscall print itera sobre cada caracter da string e chama put_char para colocar cada caractere na UART, no fim da string ela sai do loop e retorna. A syscall increment apenas incrementa o valor recebido no registrador a0 e retorna.

Codigo16

Codigo17

Codigo18


O user é bem simples, pois ele é responsável por fazer as chamadas de sistema no modo de privilégios de usuário. A main faz a chamada da syscall print, definindo o parâmetro a0 como o endereço para a string guardada em helloWorld, que foi definida na seção .data, o a1 como o tamanho da string, a7 como zero, indicando que é a syscall print, e utilizando a instrução ecall para o tratador executar a chamada de sistema. A syscall increment segue a mesma lógica, em a0 foi definido o valor a ser incrementado, que nesse caso é 0, e em a7 foi colocado 1 para indicar que é a chamada increment. A label after_increment serve para poder ver o resultado em a0 após a chamada increment.

Codigo19

9. Network Interface Card (NIC) for SiFive Unleashed (RISC-V 64 bits SoC)The NIC present in this SoC is the Cadence Gigabit Ethernet MAC (GEM). Its registers are mapped in memory and they have a base address of 0x1009_0000.

9.1. Overview

The documentation is your best friend.

The GEM controller can be divided into two interfaces, one interacting with the hardware and one with the operating system. Our concern here is the interface with the OS, in the blue square, and the hardware interface in red is out of the scope of this documentation. So the intention here is only to configure the DMA controller, program the descriptors and buffers structures, and implement the logic for sending, receiving, and consequently handling interrupts, to achieve all of that we must interact with the physical registers interfaces mapped to memory.

GEM driver components Overview


The documentation provides us an activity diagrams showing the nic driver's lifetime:

NIC initialization


This activity diagram can be easily interpreted and translated to the code in the driver, just like in the riscv_gem_init.cc file:

// Initialize controller
  reg(NWCTRL) = 0;
  reg(NWCTRL) = CLEAR_STATS_REGS;
  reg(NWCFG) = _32_DBUS_WIDTH_SIZE; // 32-bit data bus
  reg(TXSTATUS) = TX_STAT_ALL;
  reg(RXSTATUS) = RX_STAT_ALL;
  reg(IDR) = INT_ALL;
  reg(TXQBASE) = 0;
  reg(RXQBASE) = 0;


The configuration of the controller is done in the same way, by writing to hardware registers as specified in the manual, just like in the riscv_gem.cc file:

db<SiFive_U_NIC>(TRC) << "SiFive_U_NIC::configure()" << endl;

  reg(NWCFG) |= (STRIP_FCS | FULL_DUPLEX);
  reg(NWCFG) |= promiscuous ? PROMISC : 0;

  // Set the MAC address
  for (int i = 0; i < 4; i++)
  {
    unsigned int low = reg(SPADDR1L + i * 8);
    unsigned int high = reg(SPADDR1H + i * 8) & 0xFFFF;

    if (low != 0 || high != 0)
    {
      _configuration.address[0] = low & 0xFF;
      _configuration.address[1] = (low >> 8) & 0xFF;
      _configuration.address[2] = (low >> 16) & 0xFF;
      _configuration.address[3] = (low >> 24) & 0xFF;
      _configuration.address[4] = high & 0xFF;
      _configuration.address[5] = (high >> 8) & 0xFF;
      break;
    };
  };

  reg(SPADDR1L) = (_configuration.address[3] << 24) |
                  (_configuration.address[2] << 16) |
                  (_configuration.address[1] << 8) | _configuration.address[0];
  reg(SPADDR1H) = (_configuration.address[5] << 8) | _configuration.address[4];

  for (int i = 1; i < 4; i++)
  {
    reg(SPADDR1L + i * 8) = 0;
    reg(SPADDR1H + i * 8) = 0;
  };

  // Set up DMA control register
  reg(DMACFG) = dma_cfg_rx_size(sizeof(Frame) + sizeof(Header));

  // Set up PHY of ring descriptors
  reg(TXQBASE) = _tx_ring_phy;
  reg(RXQBASE) = _rx_ring_phy;

  // Enable tx and rx
  reg(NWCTRL) |= TX_EN | RX_EN;

  // Enable interrupts
  // Only those supported from QEMU (real hardware might have more)
  // Except TX_COMPLETE, for better performance
  reg(INT_ENR) |= INTR_RX_COMPLETE | INTR_TX_CORRUPT_AHB_ERR |
                  INTR_TX_USED_READ | INTR_RX_USED_READ;


And the initialization of the buffers and descriptor is done as follows:

// Distribute the DMA_Buffer allocated by init()
  Log_Addr log = _dma_buf->log_address();
  Phy_Addr phy = _dma_buf->phy_address();

  // Rx_Desc Ring
  _rx_cur = 0;
  _rx_ring = log;
  _rx_ring_phy = phy;
  log += RX_BUFS * align64(sizeof(Rx_Desc));
  phy += RX_BUFS * align64(sizeof(Rx_Desc));

  // Tx_Desc Ring
  _tx_cur = 0;
  _tx_ring = log;
  _tx_ring_phy = phy;
  log += TX_BUFS * align64(sizeof(Tx_Desc));
  phy += TX_BUFS * align64(sizeof(Tx_Desc));

  // Rx_Buffer Ring
  for (unsigned int i = 0; i < RX_BUFS; i++)
  {
    _rx_buffer[i] = new (log) Buffer(this, &_rx_ring[i]);
    //_rx_ring[i].update_size(sizeof(Frame));
    _rx_ring[i].addr = phy;    // Keep bits [1-0] from the existing value, and combine with bits [31-2] from buffer addr, manual says do this
    _rx_ring[i].addr &= ~Rx_Desc::OWN; // Owned by NIC
    _rx_ring[i].ctrl = 0;

    log += align64(sizeof(Buffer));
    phy += align64(sizeof(Buffer));
  }
  _rx_ring[RX_BUFS - 1].addr |= Rx_Desc::WRAP; // Mark the last descriptor in the buffer descriptor list with the wrap bit, (bit [1] in word [0]) set.

  // Tx_Buffer Ring
  for (unsigned int i = 0; i < TX_BUFS; i++)
  {
    _tx_buffer[i] = new (log) Buffer(this, &_tx_ring[i]);
    _tx_ring[i].addr = phy;
    //_tx_ring[i].update_size(0); // Clear size
    _tx_ring[i].ctrl |= Tx_Desc::OWN; // Owned by host

    log += align64(sizeof(Buffer));
    phy += align64(sizeof(Buffer));
  }
  _tx_ring[TX_BUFS - 1].ctrl |= Tx_Desc::WRAP; // Mark the last descriptor in the list with the wrap bit. Set bit [30] in word [1] to

9.2. Register Access


As described earlier the I/O and control registers from the SiFive_U SoC are mapped into memory. The register access is simply done by performing a read or write operation to the register's specific memory address. For example, to access the Network Config Register with offset 0x004, you must first add the desired register's offset address to to Ethernet base standing on 0x10090000, this gives us the address of the register. To read or write to that pointer we must do some casting, the size of a register is 4bytes/32bits hence, the cast we must perform is as follows:

Register Access


We must reinterpret the address of the register as a Reg32 pointer, then we can deference it to get its value or write to that register. All of that logic is encapsulated in the reg method in the GEM class, we suggest using that.

9.3. Descriptors


The GEM architecture supports different types of descriptors, the 32/64-bit Addressing Mode and Descriptor Timestamp Capture Mode, but to this documentation, we will stick to the descriptor type we are using 32bits, for more information about the other descriptors please consult the reference manual. The descriptor contains 2 words of 4 bytes each, one used for addressing the buffer it points to, the physical address of that buffer, and a control word, where the bits indicate the status of that buffer. The descriptors are all allocated in a ring-like structure, the following image illustrates that. Note that we must set a MAC register with the address of the ring-like structure.

Descriptors Overview

9.3.1. RX

The most important fields in the RX descriptors are the following.

RX descriptor words

9.3.2. TX


The most important fields in the TX descriptors are the following.

TX descriptor words

9.4. Interrupts

For EPOS working with DIRECT interrupt mode, all interrupt sources call the same function, which then calls the appropriate handler from a handler vector defined in software. In the case of this NIC and SoC, we have only one source: INT_ID = 53, so all interrupts must be handled by a single function. This handler must be bound to the vector during initialization.

The registers involved in setting up and treating the interrupts are:

  • ISR: interrupt status register => reason why an interrupt was raised.
  • TX_STATUS: transmit status register => transmit-specific info.
  • RX_STATUS: receive status register => => receive-specific info
  • IER: interrupt enable register => write-only, bits mapped to one interrupt each. Schedules write to IMR of bits written.
  • IDR: interrupt disable register => write-only, bits mapped to one interrupt each. Schedules write to IMR of bits written.
  • IMR: interrupt mask register => used to know which interrupts should be raised. Read this register to know which ones are enabled or not, as IER and IDR are WO (enabled has '0' in the corresponding bit, disabled '1').


There are a lot of interrupt causes, but QEMU only uses 5:

  • TXCMPL
  • RXCMPL
  • TXUSED
  • RXUSED
  • AMBA_ERR


The TXCMPL interrupt can be disabled, since hardware already updates the USED bit in the TX descriptor, signaling completion. This is faster since handling interrupts is costly.

The register offsets can be found in the Cadence GEM datasheet. The register bits and how to treat each interrupt can be found in the Zynq Ultrascale+ manual. The interrupt handling is mentioned inside the step-by-step guides on each operation (e.g. receiving). Links to both of these documents are provided in the 'Overview' section.

9.5. Debugging


The main debugging methods you should use are the EPOS debugging options (TRC and INF with code prints along the way) and properly reading the Zynq Ultrascale+ manual provided.

If that's not enough to figure out what's actually happening, you have the following options:

  • Enable debugging of GEM in QEMU and recompile: instructions below.
  • Look into QEMU source code to understand how it emulates the hardware: do not try to implement what's there, you need to understand what your code needs to do so that the functions in QEMU respond properly.
  • FreeBSD implementation reference: not recommended, since there's a lot of unnecessary configurations (filtering is difficult and time consuming) and has its own way of handling things (still better than Linux reference).


For the first option, clone the QEMU repository (version 6.x), change this flag in the GEM code and compile a version of QEMU locally using './configure && make' at the root. The default installation folder is './build/'. Then, change the 'makedefs' file in EPOS root and add the path to your compiled QEMU before 'qemu-system-riscv64', so that the SiFive_U emulator uses the binary compiled (as opposed to the one in your /usr/bin). QEMU will then log the GEM function calls and additional information and make them available at the end of the simulation.

Port to VisionFive2

See EPOS port to SBC VisionFive2

Contributions

Topic
Authors
Date
Main Contributions
9
João Victor Volpato and Thiago Bewiahn
06/09/2023
Describe NIC and Its implementation
4.8
João Paulo Bonomo and Rodrigo de Carvalho
06/09/2023
Describe PLIC and Its implementation
7
Grupo G
23/06/2023
Adicionado tópico de System Calls
1.4
Nathan Cezar Cardoso
18/12/2022
Deslocada seção Instruções CSRs
2.1
Nathan Cezar Cardoso
18/12/2022
Recriadas Figuras do PCB e Troca de Contexto de Threads
2.1
Nathan Cezar Cardoso
18/12/2022
Reorganizada e Adicionada seção Threads
3.4
Nathan Cezar Cardoso
18/12/2022
Deslocada seção Endereçamentos no RISC-V 64bits
3.1
Nathan Cezar Cardoso
18/12/2022
Deslocada seção Modelo de memória
5.3
Nathan Cezar Cardoso
18/12/2022
Removida seção Modos de Operação
4.3
Gabriel Simonetti Souza
31/05/2022
Add Atomic Operations intro and AMO text
4.4
Gabriel Simonetti Souza
31/05/2022
Add Locking initial text
4.3
Gabriel Simonetti Souza
31/05/2022
Add text for Load-Reserved/Store-Conditional instructions
4.3
Gabriel Simonetti Souza
31/05/2022
Add text for Sucesso Eventual das Instruções Store-Conditional
2.4
Bryan Lima
01/06/2022
Add text for topic 2.4

Review Log

Ver
Date
Authors
Main Changes
2.0 June 18, 2022 Mateus LucenaRemoved duplicated and refactored text
1.0June 1, 2022Collaborators from INE5424@UFSCInitial version