Loading...
 

EPOS for RISC-V 64 bits (RV64)

Table of Contents

[Show/Hide]

1. Architectural Considerations

1.1. Terminologias

  • CSRs: Control and Status Registers.
  • HART: hardware threads.
  • WARL: Write Any Values, Reads Legal Values.
  • WLRL: Write/Read Only Legal Values.
  • WPRI: Reserved Writes Preserve Values, Reads Ignore Values.

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

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

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

1.2. Bases e Extensões


A definição de uma ISA única no RISC V é a partir de uma base de instruções sob inteiros que contém instruções compatíveis com as primeiras versões de RISC. Já que, a arquitetura suporta diferentes tamanhos de bits com pouca modificação da ISA, se divide a ISA em quatro bases, que se diferem pelo tamanho do registrador de inteiros e consequentemente do tamanho do espaço de endereçamento destes registradores. As duas mais importantes para o nós são a RV32I, RISC V com a extensão base de inteiros e registradores de inteiros de 32 bits, e a o 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, para adicionarmos por exemplo mais instruções, relativas a multiplicação e divisão de inteiros poderíamos utilizar a extensão M, tendo a ISA RV64IM. Abaixo uma figura com as bases e extensões possíveis para RISC V. Abaixo uma tabela com as bases e extensões.

Screenshot.192

1.3. Níveis de privilégios


O RISC V define três níveis de privilégio, que podem ou não ser implementados nas implementações. Estes níveis são necessários para a proteção dos diferentes componentes da stack de software, por exemplo, uma aplicação executando sob um sistema operacional, a aplicação poderá utilizar alguns CSR’s, mas não todos os que o sistema operacional tem acesso. 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: User que é o nível de privilégio de aplicações executando pelo usuário, Supervisor que é o nível de privilégio que é executado pelo sistema operacional e Machine que é o nível de privilégio com mais privilégios e executa de maneira segura, utilizado no setup do sistema operacional, que depois retorna ao nível Supervisor com MRET. Abaixo a tabela sobre eles e suas identificações.

Screenshot.193


Os níveis de privilégio são definidos por hart, ou seja, uma hart pode estar executando em um privilégio enquanto outra estará executando em outra. Ao sair de níveis de privilégios utilizamos as operações MRET, SRET e URET respectivamente para voltar ao privilégio antes de estar em Machine, Supervisor e User. Registradores que podem ser acessados por cada nível tem prefixado m, s, u para os níveis Machine, Supervisor e User, respectivamente.

1.4. CSR's


Control and status register (CSR) é um registrador que armazena várias informações na CPU. RISC-V define um address space separado de no máximo 4096 CSRs. Tais registradores respeitam o seguinte padrão de nomenclatura:

  • Prefixo “m”, Machine-level ISA
  • Prefixo “s”, Supervisor-level ISA
  • Prefixo “u”, User-level ISA

1.4.1. m/u/sstatus

  • É um CSR que monitora e controla o estado da hart atual. Contém vários bits importantes como:

1.4.1.1. m/u/sie

  • permite interrupções(interrupt-enable) global,
  • tem outro CSR(mie) que permite controlar mais granularmente

1.4.1.2. m/u/spie/pp

  • previous interrupt-enable/previous privilege.
  • Quando acontece uma trap que leva do modo y(M|U|S) para o modo x(M|U|S), xPIE é settado ao valor de xIE, e xIE é settado a 0; xPP é settado a 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 é settado ao valor de xPIE, o modo é mudado para y, xPIE é settado a 1; e * xPP é settado para U(ou M se não suporta User-mode)
  • Pode-se usar o MRET para trocar pela primeira vez a outro modo.


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

1.4.2. mhartid

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

1.4.3. mie

  • controlar interrupt-enable mais granularmente, baseado se é timer, external, software.

1.4.4. mtime

  • mtime é um registrador incrementado conforme decorrer do tempo (i.e., contador). É usado juntamente ao mtimecmp para implementação de timers, conforme explicado na seção CLINT. Em decorrência disso, tem grande importância para o scheduling.

1.4.5. mscratch

  • registrador temporário pra fazer algum cálculo durante troca de contexto

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

1.6. Memory Model

1.6.1. Virtual-Memory Systems (VMS)


Os Virtual-Memory Systems são os sistemas que ditam como será feito a tradução de endereços virtuais para endereços físicos, o registrador satp (Supervisor Address Translation and Protection) é designado para configurar os valores referentes a tradução e proteção. Entre esses valores, está o campo MODE que seleciona o VMS atual.

1.6.1.1. SiFive-E: Sv32


A SiFive-E suporta 2 virtual memory systems: Bare e Sv32. O modo bare desliga a tradução de endereços, no sentido em que o endereço virtual será apenas copiado, esse modo faz com que os benefícios de utilizar memória virtual sejam ignorados. O modo Sv32 é o que mais nos interessa para o desenvolvimento de software de sistema. Algumas características do Sv32 são:

  • Espaço de endereçamento virtual de 32 bits
  • A divisão em páginas é feita com páginas de 4KiB
  • Os 20 bits mais significativos representam o VPN (Virtual Page Number)
  • Os 12 bits menos significativos representam o *page offset*
  • A tradução de VPN para PPN é feita em uma tabela de páginas multi nível de 2 níveis
  • O page offset é copiado

1.6.1.2. Tabela de páginas do Sv32


O Sv32 conta com uma tabela de 2^10 entradas, cada uma de 4 bytes, sendo assim uma tabela de páginas inteira cabe dentro de uma página de 4KiB. O PPN da tabela de páginas raiz é guardado no registrador satp para utilização do sistema.

As figuras abaixo mostram padrões de bits relevantes para a utilização do Sv32. Neste caso estamos assumindo um endereçamento físico de 34 bits. Os endereços são relativamente simples, mas é importante lembrar que o VPN será extendido com zeros caso seja necessário para atingir o tamanho do PPN.

Image

Image


A entrada da tabela de páginas possui os seguintes campos relevantes

  • Bit V: indica a validade da entrada, quando esse bit for 0, todos os outros bits são desconsiderados
  • Bits de permissão (R, W, X): respectivamente indicam permissão de leitura, escrita e execução, caso os 3 bits sejam 0 significa que a entrada não é uma folha, portanto ela possui um ponteiro para a próxima entrada da tradução. Todos os padrões de valores podem ser vistos abaixo

Image

  • Bit U: indica se a página pode ser acessado por software de usuário (U-mode), quando U=1 então o acesso é permitido para esse modo de privilégio
  • Bit G: esse bit indica uma página com mapeamento global, o que significa que ela existe em todos os espaços de endereçamento. Se um nodo pai for global, todos os seus nodos filhos também serão
  • Bit A: indica se a página foi acessada desde a última vez que o hardware limpou os bits de acesso.
  • Bit D: indica se a página foi escrita desde a última vez que o hardware limpou os dirty bits.
  • Bits RSW: reservados para uso de software supervisor
  • PPN(0): primeira parte do número físico da página
  • PPN(1): segunda parte do número físico da página


O algoritmo de tradução de endereços virtuais está descrito no item 4.3.2 da especificação do RISC-V lançada em 2021, por ser um algoritmo relativamente longo e sem muita importância nesse contexto, não achamos necessário replicar ele aqui.

1.6.1.3. SiFive-U: Sv39


A SiFive-U suporta 2 virtual memory systems: Bare e Sv39. O modo bare desliga a tradução de endereços, no sentido em que o endereço virtual será apenas copiado, esse modo faz com que os benefícios de utilizar memória virtual sejam ignorados. Já o Sv39 é baseado na implementação do Sv32, portanto iremos citar apenas as principais diferenças:

  • O espaço de endereçamento virtual é de 39 bits
  • A divisão em páginas é feita com páginas de 4KiB
  • Os bits a esquerda do bit 38 devem ser complementados com o conteúdo do bit 38
  • Os 27 bits mais significativos representam o VPN (Virtual Page Number)
  • Os 12 bits menos significativos representam o *page offset*
  • A tradução de VPN para PPN é feita em uma tabela de páginas multi nível de 3 níveis
  • O page offset é copiado


Nas figuras abaixo podemos ver os padrões de bits dos endereços virtuais e físicos, além do formato de entrada na tabela de páginas. Os bits 0 a 9 tem a mesma semântica do Sv32, e os bits 10 a 53 contém o PPN. Os outros bits são reservados para extensões ou implementações futuras.

Image


O algoritmo de tradução de endereços virtuais segue o mesmo modelo do Sv32, inclusive sendo apenas referenciado no manual para o mesmo item 4.3.2.

1.6.1.4. Outras implementações: Sv48, Sv57 e Sv64


Apesar de não serem suportados pela nossa máquina target, existem outros VMSs na especificação atual do RISC-V, eles possuem características análogas aos sistemas já apresentados, apenas escalando para cima o espaço de endereçamento, a quantidade de níveis de tabelas de página.

  • Sv48: aumenta para 4 níveis de tradução na tabela de páginas e expande o espaço de endereçamento virtual.
  • Sv57: aumenta para 5 níveis de tradução na tabela de páginas e expande o espaço de endereçamento virtual.
  • Sv64: na data atual, o release da especificação não detalha este VMS, apenas deixa o slot dele no registrador.


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

1.6.2. Configurando um VMS

1.6.2.1. Definição do campo MODE


Como dito antes, o campo MODE do registrador satp para configurar qual *VMS* será utilizado. É utilizado um padrão de 4 bits, ou seja, existem espaços para implementações futuras de novos sistemas conforme necessário. Os valores definidos para as configurações se encontram na figura abaixo:

Image


Caso você tente escrever no campo um valor reservado para o futuro ou então o valor correspondente a um *VMS* que sua implementação não aceita, a escrita completa feita no satp é ignorada e nenhum valor será alterado.

1.6.2.2. Definição do campo PPN


É necessário também definir qual será o PPN da tabela de páginas raiz, no primeiro campo do registrador satp, a partir dessa posição que o algoritmo de tradução de endereços virtuais será executado.

1.6.3. SiFive-E (RV32) Memory Map

Image

1.6.4. SiFive-U (RV64) Memory Map

Image
Image


1.6.5. Cache Configuration

1.6.5.1. Conceito


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.

1.6.5.1.1. Associações


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.

1.6.5.1.2. Modelo de memória


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.

1.6.5.2. Caches no RISC-V

1


O FU540 inclui um core E51 de 64 bits com as extensões RV64IMAC e quatro cores U54 de 64 bits com as extensões RV64IMAFDC. Tanto no E51 quanto nos U54, possuem pipeline de execução em ordem de emissão única.

1.6.5.2.1. Cache de instruções L1


A cache de instruções consiste em 8-way set-associative de 32 KiB para os cores U54 e 8-way set-associative de 16 KiB para o core E51. A latência de acesso de todos os blocos no sistema de memória de instruções é de um ciclo de clock. A cache não é mantida coerente com o resto do sistema de memória da plataforma. A escrita na memória de instruções deve ser sincronizada com o fluxo de busca de instruções executando uma instrução FENCE.I. Ao tentar executar uma instrução de um endereço não executável resulta em uma exceção síncrona.

1.6.5.2.2. Cache de dados L1


A cache de dados existe somente nos cores U54, sendo de 8-way set-associative de 32 KiB, operando na política de write-back. A latência de acesso é de dois ciclos de clock para words e double words, e três ciclos de clock para quantidades menores. Acessos desalinhados não são suportados em hardware e resultam em uma exceção para suportar a emulação de software. As caches de dados são mantidas coerentes com um gerenciador de coerência de cache baseado em diretório, que reside no cache L2 externo.

1.6.5.2.3. Instruction Tightly Integrated Memory (ITIM)


O ITIM fornece entrega de instruções previsíveis e de alto desempenho. Buscar uma instrução do ITIM é tão rápido quanto um acerto no cache de instruções, sem possibilidade de um falta de cache. O ITIM pode armazenar dados e instruções, embora carregue e armazene de um núcleo para outro. O ITIM não é tão eficiente quanto as DTIMs. As solicitações de memória de um núcleo para o ITIM de qualquer outro núcleo não são tão eficientes quanto a memória solicitações de um núcleo para seu próprio ITIM.

A cache de instruções pode ser parcialmente reconfigurada em ITIM tanto no core E51 quanto nos cores U52, que ocupa um intervalo de endereços fixo no mapa de memória de tamanho de 8 KiB. Exceto para 1 em unidades de linhas de cache (64 bytes, 1 way), cache de instruções deve permanecer como cache de instruções.

1.6.5.2.4. Data Tightly Integrated Memory (DTIM)


A DTIM é apenas aplicável ao core E51 e representa todo o sistema de memória de dados, possuindo capacidade de 8 KiB. A latência de acesso de um núcleo para o seu próprio DTIM é dois ciclos de clock para palavras completas e três ciclos de clock para quantidades menores. Solicitações de um núcleo de memória para DTIM de qualquer outro núcleo não são tão eficientes quanto solicitações de memória de um núcleo para seu próprio DTIM. Acessos desalinhados não são suportados em hardware e resultam em trap para permitir a emulação de software.

1.6.5.2.5. 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. A figura a seguir mostra esse mapeamento.

3

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

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

1.6.5.2.8. Mapa de memória


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

4

7

1.6.5.2.9. Registradores

1.6.5.2.9.1. Registrador config


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

5

1.6.5.2.9.2. Registrador wayEnable


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

6

1.6.5.2.9.3. Registrador wayMask


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

8

IDs das masks:
10

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

1.6.6. MMU Configuration (for kernel mode)

1.7. Process Model

Um processo é um programa em execução que consiste em vários elementos, como por exemplo um código do programa e um conjunto de dados. Para executar um programa, um processo deve ser criado para esse programa.
Um modelo de processo apresenta cinco fases ou estados em que um processo pode se encontrar em um determinado momento.

Captura De Tela 2022 05 31 232546
Fonte: https://allaboutse.com/process-models-in-operating-systems/

Os cinco estados são:

  • Running: significa um processo que está sendo executado no momento.
  • Ready: significa um processo que está preparado para ser executado quando for dada a oportunidade pelo sistema operacional.
  • Waiting: significa que um processo não pode continuar executando até que algum evento ocorra como, por exemplo, a conclusão de uma operação

de entrada-saída.

  • New: significa um novo processo que foi criado, mas ainda não foi admitido pelo SO para sua execução. Um novo processo não é carregado na

memória principal, mas seu bloco de controle de processo (PCB) foi criado.

  • Terminated: Um processo ou trabalho que foi liberado pelo sistema operacional, seja porque foi concluído ou foi abortado por algum problema.

1.7.1. Thread Context

O PCB (Process Control Block) é uma estrutura de dados que armazena toda informação sobre um processo. Cada processo criado possui um PCB correspondente. O PCB é importante no gerenciamento de processos, já que eles são acessados e/ou modificados pela maioria dos utilitários, principalmente aqueles relacionados ao escalonamento (troca de contexto) e no gerenciamento de recursos.

Captura De Tela 2022 05 31 233246
Fonte: https://binaryterms.com/process-control-block-pcb.html

1.7.2. Thread Context Switching

A troca de contexto basicamente é o processo que consiste em armazenar o estado de um processo ou thread corrente, para que possa ser restaurado e retomar a execução posteriormente, permitindo que vários processos compartilhem uma única CPU, sendo um recurso essencial em um sistema operacional multitasking.
A troca de contexto pode ocorrer em três cenários:

  • OS Multitasking: o escalonador seleciona um processo 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, 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.
  • User and kernel mode switching: não necessariamente é uma troca de contexto, mas dependendo do sistema operacional, uma troca de contexto também pode ocorrer neste momento.

1.7.2.1. Como ocorre a troca de contexto?

O estado do processo atualmente em execução deve ser salvo para que possa ser restaurado quando reprogramado para execução.
O estado do processo inclui todos os registradores que o processo pode estar usando, especialmente o contador de programa, além de quaisquer outros dados específicos do sistema operacional que possam ser necessários. O PCB é utilizado para armazenar esses dados.
O PCB pode ser armazenado em uma pilha por processo na memória do kernel ou pode haver alguma estrutura de dados específica definida pelo sistema operacional para essas informações. Um identificador para o PCB é adicionado a uma fila de processos que estão prontos para execução, estado ready.
Como o sistema operacional suspendeu efetivamente a execução de um processo, ele pode alternar o contexto escolhendo um processo da fila de prontos e restaurando seu PCB. Ao fazer isso, o contador de programa da PCB é carregado e, assim, a execução pode continuar no processo escolhido.

Image 3
Fonte: https://zitoc.com/context-switching/

1.7.2.2. Registrador mstatus

Como dito anteriormente, o estado atual do processo em execução deve ser guardado durante a troca de contexto para que seja possível uma posterior recuperação quando necessário. O RISC-V tem um registrador responsável por guardar boa parte dessa informação chamado mstatus. Este é um registrador do tipo CSR onde guardaremos dados de controle do estado no formato booleano, em bits individuais, e outros mais específicos em campos de dois bits.
A troca de contexto pode ser custosa em termos de desempenho por conta das diversas instruções de escrita e leitura exigidas para tal. O mstatus é crucial para que possamos reduzir esse impacto, nos dando informações quanto à necessidade de guardar, ou não, determinado valor de registro.

1.7.2.3. Bits 𝓍IE, 𝓍PIE e 𝓍PP do mstatus

São utilizados para garantir atomicidade em relação ao tratamento de interrupções dentro do modo de privilégio corrente. Os bits MIE, SIE e UIE, são usados para os modos Machine, Supervisor e User, respectivamente. Além disso, interrupções para modos de privilégios mais prioritários sempre estarão habilitados, independente do seu valor 𝓍IE. Da mesma maneira, interrupções para modos menos prioritários estarão globalmente desligadas. Vale lembrar que neste semestre, somente será usado o bit MIE, tendo em vista que somente será usado o modo de privilégio Machine.
Um ponto importante sobre os bits 𝓍IE é 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, podemos habilitar IE no modo machine através do método mint_enable() e desabilitá-las através do método mint_disable()


Image11
Fonte: Código EPOS

Para dar suporte a interrupções encadeadas, cada modo de privilégio possui uma pilha de dois níveis dos bits interrupt-enable (IE). São os bits MPIE, SPIE e UPIE. Além disso, há também os bits MPP e SPP (UPP é implicitamente zero). Dessa maneira, quando ocorre uma interrupção do modo de usuário para modo de máquina, o bit MPIE recebe o valor de MIE, MIE é setado para zero, e MPP é setado para o modo de usuário.
Para retornar de um tratamento de interrupção, as instruções MRET, SRET e URET podem ser utilizadas para retornar do seu respectivo modo. Será usado somente a instrução MRET, neste semestre. Portanto, para se retornar do exemplo de tratamento acima, a instrução MRET iria setar o bit MIE para o valor de MPIE, o modo de privilégio é trocado para User, MPIE é setado para 1 e MPP é setado para U.

1.7.2.4. Context Switch no RISCV


Image3
Fonte: An Introduction to the RISC-V Architecture / Slides

Sempre que ocorrer uma interrupção no hart (núcleo), os seguintes passos são realizados automaticamente pela CPU:

  1. O valor de PC é salvo no CSR mepc (Machine Exception Program Counter);
  2. O modo de privilégio corrente é salvo no campo MPP do CSR mstatus;
  3. O campo MIE do mstatus é salvo no campo MPIE dele mesmo;
  4. PC recebe o valor do mtvec (Machine Trap Vector);
  5. Por fim, é desabilitado as interrupções em modo máquina setando o campo MIE do mstatus para zero.


Após esses passos, a CPU passa o controle para o controlador de interrupção em software.

Image6
Fonte: An Introduction to the RISC-V Architecture / Slides

Essa possível implementação do controlador de interrupções implica no salvamento dos register file, especificamente os registradores x1 e x5 até x31. Além disso, é essencial saber se a exceção foi causada por uma interrupção, isso é possível lendo o último bit mais significativo do CSR mcause. E chamar o tratamento adequado, enviando os bits 62:0 (no caso do RV64) do mcause que identifica a exceção.

Image7
Fonte: An Introduction to the RISC-V Architecture / Slides

Após o devido tratamento da exceção, se retira da pilha os registradores previamente salvos, e finalmente é usada a instrução MRET que desfaz os passos ao se iniciar o tratamento de uma interrupção.
Image4
Fonte: An Introduction to the RISC-V Architecture / Slides

1.7.2.5. Estado das extensões

Um dos principais objetivos do RISC-V é dar suporte a extensões. Tendo isso em vista, alguns campos do mstatus são exclusivos para tratar esses casos. Dois exemplos são os campos FS (Floating-point State) e XS (Extension State).
Ambos são importantes pois geram um ganho de desempenho no processamento da troca de contexto permitindo que leituras e escritas sejam feitas apenas quando necessárias. Além deles, temos também o bit SD (State Dirty) que indica quando um dos dois casos teve mudança de valor e precisará ser guardado ou restaurado. Vemos na tabela a seguir as possíveis combinações de status dos campos em questão e seus respectivos significados.

Image 5
Fonte: The RISC-V Instruction Set Manual / Volume II: Privileged Architecture

  • Off: Uma instrução que tente ler ou escrever o estado correspondente causará uma exceção por instrução ilegal.
  • Initial: O estado correspondente possui valores constantes.
  • Clean: O estado correspondente é possivelmente diferente do inicial, mas não sofreu alterações desde sua última troca de contexto.
  • Dirty: O estado correspondente foi possivelmente alterado após sua última troca de contexto.


O contexto das extensões só precisará ser salvo caso seu status esteja definido como Dirty. Uma vez salvo, esse status agora deve ser alterado para Clean e só volta a ser restaurado se ainda estiver nesse estado (nesse caso ele nunca deveria estar em Dirty). Caso o status seja Initial, todos os registradores devem ser passados para um valor constante pré-definido (zero, por exemplo).
As transições de contexto feitas através dos campos FS e XS devem considerar diversas possibilidades de cenário e adicionam uma maior complexidade à mudança de contexto. Elas não serão abordadas com mais profundidade neste trabalho pois nossas implementações não possuem tais extensões, forçando-as para zero (status = Off) como indicado anteriormente. As ações tomadas de acordo com cada cenário podem ser vistas na tabela abaixo:

Image 6
Fonte: The RISC-V Instruction Set Manual / Volume II: Privileged Architecture

1.7.2.6. Privilégios com o mstatus

Três níveis de privilégio são definidos no RISC-V chamados de modos. Do modo com menos privilégio para o com maior, temos: U-mode (modo de usuário / nível 0), S-mode (modo de supervisor / nível 1) e M-mode (modo de máquina / nível 3). O único modo obrigatório é o de máquina, podendo-se ter as combinações de privilégios vistas na tabela.

Image 7
Fonte: The RISC-V Instruction Set Manual / Volume II: Privileged Architecture

O controle de privilégios é importante pois acessos à memória em um modo podem ser protegidos de outros através da PMP (Physical Memory Protection). Isso previne que softwares que executam em modo de usuário, por exemplo, acessem endereços indesejados. Porém, nas nossas implementações utilizaremos apenas o modo de máquina.
As trocas de contexto devem considerar o modo em que os processos envolvidos estão operando, para garantir que um processo em modo de usuário retorne ao seu contexto anterior sem que haja alteração em suas permissões quanto ao sistema. Alguns campos do registrador mstatus utilizados para tal funcionalidade serão apresentados a seguir.

  • MPRV (Modify Privilege)
    • valor 0: load e store usam a configuração do modo de privilégio atual
    • valor 1: load e store se comportam com privilégio MPP (Machine Previous Privilege)
  • SUM (Supervisor User Memory)
    • valor 0: não é possível acessar memória de U-mode estando em S-mode
    • valor 1: o caso descrito em valor 0 se torna possível
  • MXR (Make Executable Readable)
    • valor 0: lê apenas páginas da memória virtual marcadas como “readable” (R=1)
    • valor 1: lê páginas marcadas como “readable” e “executable” (R=1 ou X=1)


Assim como no tratamento de troca de contexto para extensões, nossa implementação permite uma complexidade menor no controle de privilégios por possuir apenas um modo.

1.7.2.7. Implementação

Para implementação foi utilizado o código do EPOS do RV32 como base. A migração para RV64 é bem simples, é necessário se atentar às instruções LW, que passaram a ser LD (Load Double Word) e os valores de incremento, no qual passaram de 4 bytes para 8 bytes.

Image2
Fonte: Código do EPOS modificado para 64 bits

2. I/O and Peripherals

2.1. Interrupt Controller - CLINT

2.1.1. Registradores

2.1.1.1. Machine Status (mstatus)

  • Acompanha e controla o estado operacional atual do hart.

  • O bit MIE (Machine Interrupt Enable) indica se as interrupções estão habilitadas.
  • Já o bit MPIE (Machine Previous Interrupt Enable) mantém o valor do bit MIE anterior, ou seja, salva o contexto anterior.
    • mstatus.MPIE <- mstatus.MIE
    • mstatus.MIE <- 0

  • Bit MPP recebe Priv (nível de privilégio atual).
    • mstatus.MPP = Priv

  • MRET instruction:
    • mstatus.MIE <- mstatus.MPIE
    • Priv <- m

2.1.1.2. Machine Cause (mcause)

  • Indica qual evento que causou o trap, caso a causa seja uma interrupção o bit Interrupt é setado. Já o campo Code indica qual o código da da interrupção/exceção.

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

  • Se for gerada mais de uma exceção síncrona, a tabela de prioridades é utilizada.

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

2.1.1.3. Machine Exception Program Counter (mepc)

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

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

2.1.1.4. Machine Interrupt Pending (mip)

  • Indica quais interrupções estão pendentes.

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

2.1.1.5. Machine Interrupt Enable (mie)

  • Indica quais interrupções estão habilitadas.

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

2.1.1.6. Machine Trap-Vector Base-Address (mtvec)

  • Contém o endereço base da tabela de vetores de interrupção e a configuração do modo de interrupção. Todas as exceções síncronas usam para tratamento de exceções. 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 consiste no endereço base da tabela de vetores e MODE refere-se ao modo utilizado.

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

2.1.1.7. Machine Exception Delegation (medeleg)

  • Delega exceções ao modo supervisor.

Medeleg

Fonte: SiFive FU540-C000 Manual

2.1.1.8. Machine Interrupt Delegation (mideleg)

  • Delega interrupções ao modo supervisor.

Mideleg
Fonte: SiFive FU540-C000 Manual

2.1.1.9. Machine Trap Value (mtval)

  • Quando um trap é encontrado no modo Machine, mtval é 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 nunca é escrito pela implementação, embora possa ser escrito explicitamente pelo software.

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

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

2.1.1.10. Machine Software Interrupt Pending (msip)

  • Cada CPU possui seu registrador. Em sistemas com várias CPUs, uma CPU pode escrever no msip de qualquer outra. Isso permite uma comunicação eficiente entre processadores.
  • Seu bit menos significativo é refletido no bit mip.MSIP e os demais estão em 0. Todos os registradores msip são zerados no reset.

2.1.1.11. Machine Timer (mtime)

  • Esse registrador é único, contendo o número de ciclos a partir de RTCCLK (CPU real time clock) e deve ser executado em tempo constante, no reset seus campos serão zerados. Interrupções são geradas quando mtime >= mtimecmp, a qual é indicada em mip.MTIP, e sempre vão para o tratador modo Machine (a não ser quando delegadas ao modo Supervisor com o uso do mideleg).

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

2.1.1.12. Machine Timer Compare (mtimecmp)

  • Usado em conjunto com mtime para interrupções de timer. Não é resetado, diferente de mtime. Cada CPU possui seu próprio registrador e pode ser escrito por outras CPUs.

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

2.1.2. Modos de Operação

  • Existem dois modos de operação do CLINT, o modo direto e o modo vetorizado. Para configurar os modos do CLINT, 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.

2.1.2.1. Modo Direto

  • O modo direto significa que todas as interrupções têm armadilha para o mesmo tratador, e não há uma tabela de vetores implementada. É a 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.

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

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

2.1.3. Interrupt Levels

2.1.3.1. Software Interrupts - Interrupt ID #3

  • Interrupções de software são acionadas ao escrever no registrador msip de certa CPU.

2.1.3.2. Timer Interrupts - Interrupt ID #7

  • mtime >= mtimecmp

2.1.3.3. External Interrupts - Interrupt ID #11

  • Interrupções globais geralmente vão para o PLIC primeiro e depois para CPU usando Interrupt ID #11. Em sistemas que não possuam o PLIC, pode ser desativado com mie.MEIE = 0.

2.1.3.4. Local Interrupts - Interrupt ID #16+

  • Conexões locais podem se conectar diretamente a uma fonte de interrupção e não precisam ir para o PLIC. Sua prioridade é fixa com base em Interrupt ID. O número máximo de interrupções locais é dado por XLEN - 16.

2.1.4. Entrada e Saída do Tratador de Interrupções

  • Sempre que ocorrer uma interrupção, o hardware salvará e restaurará automaticamente registros importantes.
Ao Entrar:
mepc            <- pc
mstatus.MPP     <- priv
mstatus.MPIE    <- mstatus.MIE
pc              <- mtvec (se mtvec.MODE = Direct) | mtvec.BASE + 4 * exception_code 
mstatus.MIE     <- 0

Ao Sair:

pc              <- MEPCte
priv            <- mstatus.MPP
mstatus.MIE     <- mstatus.MPIE
  • priv refere-se ao nível de privilégio atual, o qual não é visível quando estamos operando nesse nível.

2.1.5. Código

Repositório: https://github.com/G-Carneiro/clint

  • Start()
    // set M Exception Program Counter to main, for mret.
    // requires gcc -mcmodel=medany
    w_mepc((uint64_t)main);

    // setup trap_entry
    w_mtvec((uint64_t)trap_entry);

    // keep each CPU's hartid in its tp register, for cpuid().
   int id = r_mhartid();
    w_tp(id);
    timer_init();

  • Entrada do Handler
.globl trap_entry
.align 4
trap_entry:
        # make room to save registers.
        addi sp, sp, -256

        # save the registers.
        sd ra, 0(sp)
        sd sp, 8(sp)
        sd gp, 16(sp)
        sd tp, 24(sp)
        sd t0, 32(sp)
       ...
        sd t3, 216(sp)
        sd t4, 224(sp)
        sd t5, 232(sp)
        sd t6, 240(sp)
       ...
call handle_trap
  
        # restore registers.
        ld ra, 0(sp)
        ld sp, 8(sp)
        ld gp, 16(sp)
        ...

  • Handlers
int count = 0;

void handle_interrupt(uint64_t mcause) {
    if ((mcause << 1 >> 1) == 0x7) {
        print_s("Timer Interrupt: ");
        print_i(++count);
        print_s("\n");

        *MTIMECMP = *MTIME + 0xfffff * 5;
        if (count == 10) {
            unsigned long long int mie;
            asm volatile("csrr %0, mie" : "=r"(mie));
            mie &= ~(1 << 7);
            asm volatile("csrw mie, %0" : "=r"(mie));
        }
    } else {
        print_s("Unknown interrupt: ");
        print_i(mcause << 1 >> 1);
        print_s("\n");
        while (1)
            ;
    }
}

void handle_trap() {
    uint64_t mcause, mepc;
    asm volatile("csrr %0, mcause" : "=r"(mcause));
    asm volatile("csrr %0, mepc" : "=r"(mepc));

    if (mcause >> 63) {
        handle_interrupt(mcause);
    } else {
        handle_exception(mcause);
        asm volatile("csrr t0, mepc");
        asm volatile("addi t0, t0, 0x4");
        asm volatile("csrw mepc, t0");
    }
}

2.2. Timers


Em sistemas embarcados, muitas aplicações necessitam de um mecanismo para garantir a sincronia de atividades. O timer pode ser usado tanto para simular um relógio comum, com horas, minutos e segundos, quanto como alarmes e geradores de interrupções para definir comportamentos do sistema.

O RISC-V possui um contador em tempo real armazenado no registrador mtime. A máquina deve definir uma frequência constante como base de tempo do mtime. O mtime tem a precisão de 64 bits para os sistemas RV32, RV64 e RV128.

As plataformas fornecem o registrador de comparação, o mtimecmp, com 64 bits, que realiza uma interrupção quando mtime atingir o valor de mtimecmp. Esta interrupção fica registrada até que o valor de mtimecmp seja reescrito.

Para habilitar interrupções é usado o registrador mie (machine interrupt enable), que possui 16 bits. Nele, é possível utilizar um de seus bits com o objetivo de identificar qual tipo de interrupção será habilitada. Interrupções de timer, por exemplo, ocorrerão somente se estiverem habilitadas a partir do bit mie.MTIE (bit de habilitação de interrupção de timer).

Timer 2

Segue um exemplo de como um timer deve se comportar no RV64:

#include <stdio.h>

int main(int argc, char **argv)
{
    int m;

    printf("Start...\n");

    asm volatile(
        "li a0, 0x0200BFF8     \n"
        "ld t0, 0(a0)          \n" // t0 = mtime
        "li a0, 0x02004000     \n"
        "addi t1, zero, 0x500  \n" // 4s = 0x500 << 15
        "slli t1, t1, 15       \n"
        "add t0, t1, t0        \n" // t0 = mtime + 4s
        "sd t0, 0(a0)          \n" // mtimecmp = t0
        "la a0, trap_routine   \n" 
        "csrw mtvec, a0        \n" // mtvec = endereço de trap_routine
        "li t0, 0x8            \n"
        "csrw mstatus, t0      \n" // mstatus.MIE = 1
        "li t0, 0x80           \n" 
        "csrw mie, t0          \n" // mie.MTIE = 1
        "loop_forever:         \n" 
        "    li a0, 0x0200BFF8 \n" 
        "    ld t0, 0(a0)      \n" // t0 = mtime
        "j loop_forever        \n"
        "trap_routine:         \n"
        "    csrr %[m], mcause \n"

        : [m] "=r" (m)
        :
    );

    printf("mcause: %d\n", m);
}

3. Multicore Control

3.1. Core Activation

3.2. Inter-processor Interrupts (IPIs)

Interrupções entre processadores são um tipo especial de interrupções pela qual um processador pode interromper um processo sendo executado em outro núcleo da mesma máquina.
São muito utilizadas em geral para criar pontos de sincronização de coerência de memórias cache, desligamento de sistema ou escalonamentos.

3.2.1. Registradores Pertinentes

A arquitetura RISC-V prevê um conjunto de registradores para gerenciar e guardar informações acerca das interrupções. São eles:

  • mstatus
  • mcause
  • mepc
  • mtvec

Além destes registradores, explicados anteriormente, existe também o registrador msip. Um registrador (em memória) utilizado para registrar uma interrupção em modo máquina pendente;
MsipRegisters

  • Vale ressaltar que, caso o mesmo processador tente enviar mais de uma interrupção para outro núcleo, antes que esse "avise" que ja lidou com a primeira (ou seja, antes dele limpar o msip), as interrupções enviadas após a primeira serão perdidas.

3.2.2. Ciclo de vida de uma Interrupção

Veja 2.5.2.4. Context Switch no RISCV

3.2.3. Considerações para o caso específico do EPOS:

  • Se os interrupts vetorizados estiverem desabilitados (ou seja, mtvec.MODE = 0 - como é no EPOS) sempre que houver um interrupt, o PC vai ser redirecionado para mtvec.BASE (ou seja, sempre que houver uma interrupção, independente de qual for ela, o PC vai ser redirecionado pro mesmo lugar). Caso mtvec.MODE = 1, ou seja, for vectored, o PC vai ser redirecionado para BASE + 4 * cause, possibilitando um handler específico para cada causa de interrupção (definido em mcause).
  • O core que for gerar uma interrupção deverá setar um bit específico no registrador de interrupções no espaço de memória do CLINT - o MSIP - onde cada registrador cria um interrupt em um hart específico.

3.2.4. Tratamento de uma Interrupção Multi-Core

Em um sistema com várias CPUs, outras CPUs podem setar msip para acionar uma interrupção de software em qualquer outra CPU do sistema. Isso permite uma eficiente comunicação entre processadores.
O tratamento também conta com vetores auxiliares (Vetores de Interrupções) que reduzem a latência do processo como um todo ao armazenar instruções de desvio para o endereço base de cada rotina de tratamento associada a cada tipo diferente de interrupção.

3.2.5. A Instrução WFI

A arquitetura RISC-V conta com uma instrução chamada WFI (Wait For Interrupt) que serve para indicar que uma hart específica do sistema se "candidatou" a tratar eventuais interrupções que venham a ocorrer durante a execução de processos. Esta instrução se mostra útil para diferenciar harts de alta prioridade (que preferencialmente, ou definitivamente, não devem ser interrompidas) daquelas de baixa prioridade (que não causariam prejuízo à execução de uma aplicação caso fossem interrompidas).

3.2.6. Código

Setup Sifive U
Setando o MSI no registrador MIE, no arquivo src/setup/setup_sifive_u.cc.
Ipi
Definição do método para setar o bit msip na posição de memória da CLINT e OFFSET tamanho 4.
Main
Invocando o método IPI na aplicação de teste.
Dispatch
Exemplo de tratamento da interrupção, apenas resetando o msip. Uma forma melhor seria passar um método handler para o _int_vector.

3.3. Atomic Operations

O RISC-V possui operações atômicas que permitem ler, modificar e escrever valores na memória para dar suporte à sincronização entre os núcleos do processador rodando em um mesmo espaço de memória. Essas instruções são do tipo load-reserved/store-conditional e fetch-and-op memory e permitem que o RISC-V dê suporte ao modelo de consistência de liberação (release consistency).

Para arquiteturas 32 bit, as instruções são seguidas de um “W”, de word. Para arquiteturas 64 bit, as instruções são seguidas de um “D”, de double word. Exemplo:

ArquiteturaExemplo de Instrução
32 bitLR.W
64 bitLR.D

3.3.1. Consistência de Liberação

O modelo de consistência de liberação é um modelo relaxado (menos restritivo no contexto de sequencialismo de instruções) com o intuito de promover maior flexibilidade para utilizar paralelismo em nível de instrução, diferente de modelos como o de consistência sequencial. No modelo de consistência de liberação, a ação de entrar e sair de uma seção crítica é classificada como adquirir (acquire) e liberar (release). É esperado que exista código explícito indicando quando são realizadas essas operações.

3.3.2. Ordenamento das Instruções Atômicas

Para dar melhor suporte para a consistência de liberação, cada instrução atômica tem os bits aq e rl, utilizados para especificar restrições adicionais de ordenação. Se ambos os bits estão em zero, nenhuma restrição adicional é imposta na operação. Se somente o bit aq é 1, a operação atômica é tratada como um acesso de aquisição, ou seja, nenhuma operação posterior no mesmo núcleo poderá vir antes dessa. Se somente o bit rl é 1, a operação atômica é tratada como um acesso de liberação, ou seja, essa operação de liberação não poderá vir antes de qualquer operação anterior a ela.

3.3.3. Instruções Load-Reserved/Store-Conditional

Para executar operações atômicas, seja de uma ou duas palavras de memória, são executadas com as instruções load-reserved (LR) e store-conditional (SC).
Chrome NAcDwJV9bs

  • Load-Reserved carrega uma palavra do endereço em rs1, coloca o valor estendido de sinal em rd e registra um conjunto de reserva - um conjunto de bytes que inclui os bytes na palavra endereçada.
  • Store-Conditional grava de forma condicional a palavra em rs2 no endereço em rs1: a SC é bem-sucedida somente se a reserva ainda for válida e o conjunto de reservas contiver os bytes que estão sendo gravados.
    • Se a SC for bem-sucedida, é gravada a palavra em rs2 na memória e gravada zero em rd.
    • Se a SC falhar, não é gravada na memória e grava um valor diferente de zero em rd.

Importante pontuar que, independentemente do sucesso ou falha, a execução de uma instrução Store-Conditional invalida qualquer reserva mantida por este hart.

3.3.4. Sucesso Eventual das Instruções Store-Conditional

A extensão padrão A define os constrained LR/SC loops possuem as seguintes propriedades:

  • O loop compreende apenas uma sequência LR/SC e código para repetir a sequência no caso de falha, e deve conter no máximo 16 instruções colocadas sequencialmente na memória.
  • Uma sequência LR/SC começa com uma instrução LR e termina com uma instrução SC. O código dinâmico executado entre as instruções LR e SC pode conter apenas instruções do conjunto de instruções base “I”, excluindo loads, stores, backward jumps, taken backward branches, instruções JALR, FENCE, FENCE.I e SYSTEM. Se a extensão “C” for suportada, então também são permitidas formas compactadas das instruções “I”.
  • O código para tentar novamente uma sequência LR/SC com falha pode conter saltos e/ou ramificações para trás para repetir a sequência LR/SC, mas de outra forma tem a mesma restrição que o código entre o LR e o SC.
  • Os endereços LR e SC devem estar dentro de uma região de memória com a eventualidade de LR/SC propriedade. O ambiente de execução é responsável por comunicar quais regiões possuem essa propriedade.
  • O SC deve estar no mesmo endereço efetivo e do mesmo tamanho de dados que o LR mais recente executado pelo mesmo hart.

As sequências de LR/SC que não estão contidas nos constrained LR/SC loops são definidas como unconstrained (irrestritas). O que faz com que essas instruções irrestritas apresentem comportamento irregular, funcionando em algumas implementações e outras não.

Caso um hart H entre em um constrained LR/SC loop, o ambiente de execução deve garantir que um dos seguintes eventos eventualmente ocorra:

  • H ou algum outro hart executa um SC bem-sucedido para o conjunto de reserva da instrução LR em constrained LR/SC loop de H.
  • Algum outro hart executa um armazenamento incondicional ou instrução AMO para o conjunto de reserva de a instrução LR em constrained LR/SC loop de H ou algum outro dispositivo no sistema escreve a nesse conjunto de reservas.
  • H executa um desvio ou salto que sai do loop LR/SC restrito.
  • H fica preso.

3.3.5. Atomic Memory Operations

As operações atômicas de memória realizam operações de leitura, alteração e escrita. São formatadas como instruções do tipo R (OP rd, rs, rt).

Chrome DQPbWFSze7


Essas instruções carregam o valor a partir do endereço em rs1, coloca no registrador rd, aplica um operador binário no valor carregado e o valor contigo em rs2, e então guarda o resultado no endereço contido em rs1.

Chrome GVLWqHjR7f


O endereço mantido em rs1 deve ser naturalmente alinhado ao tamanho do operando, ou seja, 8 bytes alinhados para palavras de 64 bits e 4 bytes alinhados para palavras de 32 bits. Se o endereço não estiver alinhado naturalmente, uma exceção do tipo address-misaligned ou access-fault será gerada.

As instruções AMO possuem um facilitador semântico para indicar as preferências de ordenação, indicando qual o bit desejado na chamada da instrução (exemplo: AMOSWAP.W.aq e AMOSWAP.W.rl).

3.4. Locking

A arquitetura do RISC-V fornece a possibilidade de implementação de um spin-lock por meio da operação atômica AMOSWAP e outras operações básicas. Entretanto, um spin-lock neste nível pode custar caro, dado que são desperdiçados ciclos de CPU enquanto o lock está aguardando outra CPU, por isso é indicado a utilização apenas em pequenos trechos críticos do código.

Segue abaixo a implementação de um spin-lock operando sobre o endereço de memória do lock a0.

    li t0, 1 # Inicia o valor para swap.
again:
    lw t1, (a0) # verifica se o lock (em a0) está bloqueado.
    bnez t1, again # retorna se estiver bloqueado.
    amoswap.w.aq t1, t0, (a0) # Tenta adquirir o lock, colocando o valor de t0 em a0; e a0 em t1.
    bnez t1, again # retorna se estiver bloqueado.
    # ...
    # Seção crítica.
    # ...
    amoswap.w.rl x0, x0, (a0) # Libera o lock colocando 0 em a0.


Para a implementação no EPOS, recomenda-se utilizar a mesma classe do utilitário de spin-lock já implementado (spin.h), mas adicionando a classe que implementa a operação atômica a seguir.

class Atomic_Spin
{
public:
    Atomic_Spin(): _locked(false) {}

    void acquire() {
        ASM("       li             t0, 1          \n");
        ASM("again: lw             t1, (%0)       \n" :: "r"(&_locked));
        ASM("       bnez           t1, again      \n");
        ASM("       amoswap.d.aq   t1, t0, (%0)   \n" :: "r"(&_locked));
        ASM("       bnez           t1, again      \n");

        db<Spin>(TRC) << "Spin::acquire[SPIN=" << this << "]()" << endl;
    }

    void release() {
        ASM("amoswap.d.rl x0, x0, (%0) \n" :: "r"(&_locked));

        db<Spin>(TRC) << "Spin::release[SPIN=" << this << "]()}" << endl;
    }

private:
    volatile bool _locked;
};


Ao instanciar a classe, é atribuído um endereço de memória à variável _locked, assim inicializada com 0. Com isso, ao realizar a operação de acquire, é utilizado o endereço da variável _locked na operação de swap.

3.5. MP Timers

3.5.1. Registradores mtime e mtimecmp

Na arquitetura RISCV, o sistema de timers funciona através de 2 registradores, o mtime e o mtimecmp.
Ambos são mapeados em memória e são registradores de 64 bits, independente da versão da arquitetura: rv32, 64 ou 128. Em uma implementação multicore, "mtime" é global e cada núcleo possui um "mtimecmp" dedicado. Os núcleos escrevem um valor nos registradores mtimecmp e, caso esse valor se torne menor ou igual ao mtime, uma interrupção é gerada, caso estejam habilitadas, ou seja, caso o bit MTIE no registrador mie estiver habilitado

Para que seja possível suportar processadores com frequência variável (como medidas de economia de energia), a implementação do timer utiliza tempo de relógio parede, ao invés de tempo de cpu. Os registradores mtimecmp só estão disponíveis para os acessos privileged. Outros níveis de acesso precisam implementar timers virtuais.

Caso ocorra overflow no temporizador ou nos comparadores, eles resetam, e recomeçam a contagem a partir do valor 0.
(Referências: github.com/riscv/riscv-isa-manual/issues/480 e github.com/riscv/riscv-isa-manual/commit/ad315fe5812046abbf77f34580b5351bed54a3a4)

3.5.2. Interrupções

Na arquitetura RV32 só é possível modificar o mtimecmp através de duas escritas. Por isso alterações nesses registradores devem ser feitas de um modo específico para não serem geradas interrupções acidentais.
Assumindo que o novo valor esteja dividido entre os registradores a1 e a0:

  • Primeiro, o mtimecmp é carregado com o valor -1.
  • O registrador a1 é carregado nos bits mais significativos de mtimecmp
  • O registrador a0 é carregado nos bits menos significativos de mtimecmp. Ao final dessa operação o valor de mtimecmp não pode ser menor do que o valor de mtime.


Para poder habilitar as interrupções do timer é necessário habilitar as interrupções de máquina pelo bit 3, que representa o campo Machine Interrupt Enable, do CSR mstatus. E também deve-se habilitar o bit 7 do CSR mie, que representa o campo Machine Timer Interrupt Enable.

Dentro do método trap_handler, é recebido o registrador mcause e com ele é possível verificar se a trap causadora é uma interrupção, através do bit 31 do mesmo. E caso seja, então é possível descobrir a razão da interrupção aplicando uma máscara para pegar os bits de 0 a 9 do registrador mcause. Caso o valor seja 7 então significa que foi uma interrupção do timer.

3.5.3. Implementação


Para agendar as interrupções e ler os registradores foram definidas as seguintes funções no arquivo machine.h, as funções volatile com os nomes dos registradores lidam com a leitura e escrita de mtime e mtimecmp. O endereço de mtimecmp por exemplo é calculado segundo o seu offset com o valor base definido no manual.

volatile unsigned long* mtime() {
    return (unsigned long*) 0x0200BFF8UL;
}

volatile unsigned long* mtimecmp(unsigned int hart_id) {
    return (unsigned long*) (0x02004000UL + (8 * hart_id));
}


No mesmo arquivo machine.h é definido um array para armazenar as quatro funções que podem ser utilizadas como callback nos cores, a função schedule_interrupt recebe isso como parâmetro e armazena. Em seguida é feito o cálculo do delay conforme a frequência da placa utilizada e esse valor é somado ao mtime atual para ser definido um novo valor pro mtimecmp ser interrompido no futuro.

typedef void (*_fn)();
static _fn _timer_interrupt_handler[4];

void schedule_interrupt(unsigned int hart_id, unsigned long milliseconds, void* handler) {
    _timer_interrupt_handler[hart_id] = handler;

    unsigned long delay = milliseconds * MTIME_FREQ_HZ / 1000;
    *mtimecmp(hart_id) = *mtime() + delay;

    enable_machine_timer_interrupt();
}


Como o código utilizado não depende do EPOS a implementação da UART foi refeita de uma maneira mais simples, foi implementada uma função de print simples sem argumentos extra como printf.

Código uart.h:

#pragma once

enum {
    UART_REG_DIV    = 6,
    UART_REG_TXCTRL = 2,
    UART_TXEN       = 1,
    UART_REG_TXFIFO = 0,
};

static volatile int* uart = (int*) 0x10010000;

void uart_init() {
    unsigned int uart_freq = 32000000;
    unsigned int baud_rate = 115200;
    unsigned int divisor = uart_freq / baud_rate - 1;
    uart[UART_REG_DIV] = divisor;
    uart[UART_REG_TXCTRL] = UART_TXEN;
}

int uart_putchar(unsigned char ch) {
    while (uart[UART_REG_TXFIFO] < 0);
    return uart[UART_REG_TXFIFO] = ch & 0xff;
}

void print(char* s) {
    for (char* c = s; *c != '\0'; ++c) {
        uart_putchar(*c);
    }
}

3.5.4. Código de demonstração

#include "uart.h"
#include "machine.h"

void print_hart_id() {
    unsigned int hart_id = mhartid();
    switch (hart_id) {
        case 1: print("1"); break;
        case 2: print("2"); break;
        case 3: print("3"); break;
        case 4: print("4"); break;
    }
}

void pong();

void ping() {
    print("hart ");
    print_hart_id();
    print(": ping\n");
    schedule_interrupt(mhartid(), 1000, pong);
}

void pong() {
    print("hart ");
    print_hart_id();
    print(": pong\n");
    schedule_interrupt(mhartid(), 1000, ping);
}

int main() {
    unsigned int hart_id = mhartid();
    unsigned long initial_delay = 250 * hart_id;
    schedule_interrupt(hart_id, initial_delay, ping);
}


Contributions

Topic
Authors
Date
Main Contributions
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