FE310G: um microcontrolador open source RISC-V – Sistema de Interrupções

Neste segundo artigo sobre o RISC-V (também publicado no portal Embarcados) eu falo um pouco sobre o sistema de interrupções e de processamento de exceção do RISC-V e sobre a primeira implementação em silício do RISC-V, o FE310G da SiFive. Para mais informações sobre as instruções e registradores do RISC-V, sugiro a leitura da primeira parte deste artigo FE310G: um microcontrolador open source RISC-V – Introdução.

Interrupções

A ISA RISC-V define dois tipos de interrupções principais: globais e locais. Basicamente as interrupções globais são voltadas para ambientes multicore, enquanto que as interrupções locais estão sempre associadas a um core específico. As interrupções locais também sofrem menor overhead já que não passam por arbitramento (necessário quando se há múltiplos cores recebendo interrupções globais).

Interrupções Locais

O core inclui um sistema de interrupções locais responsável por tratar um número limitado (e pequeno) de fontes de interrupção. O CLINT (Coreplex Local Interrupts) é responsável por gerir três fontes locais de interrupção: interrupção por software (SI), interrupção do timer (TI) e interrupção externa (EI). A ISA define ainda outras dezesseis possíveis fontes locais de interrupção que podem ser implementadas ou não (no caso do core E31, elas não estão implementadas). Uma observação muito importante: a interrupção externa (EI) recebe todos os pedidos de interrupção oriundos do PLIC (o controlador de interrupções globais)!

O sistema local de interrupção (CLINT) irá provocar a interrupção e desvio do programa quando uma das fontes locais (desde que previamente habilitada) setar o seu flag de interrupção pendente. Há também um controle global de interrupções (bits MIE/SIE/UIE conforme o modo) disponível no registrador MSTATUS. Este registrador controla também outras funcionalidades como o aninhamento de interrupções, privilégios de acesso à memória, etc. Para maiores informações sobre este registrador consulte o manual de instruções privilegiadas do RISC-V.

Tabela 1 – Registrador MSTATUS

O RISC-V permite trabalhar as interrupções de duas formas: vetor único ou múltiplos vetores de interrupção. No modo de vetor único, o endereço definido no registrador MTVEC (CSR número 0x305) é utilizado como alvo do desvio, ou seja, o MTVEC aponta para o ponto de entrada do código de tratamento de interrupção/exceção. No modo de vetores múltiplos, o MTVEC funciona como apontador do endereço base da tabela de vetores de interrupção, sendo que o índice da tabela é definido pelo código de exceção no registrador MCAUSE (CSR número 0x342).

Tabela 2 – CSRs do modo máquina

Os dois bits menos significativos do MTVEC controlam o modo de operação: quando iguais a “00”, o modo de operação é não-vetorizado (vetor único). Quando os dois bits menos significativos do MTVEC são iguais a “01”, o sistema opera no modo vetorizado e na ocorrência de um evento de interrupção o código será desviado para o endereço especificado por MTVEC (com os dois LSB em zero) + (Exception Code)*4, ou seja, o código de exceção funciona como índice na tabela de vetores apontada pelo endereço base no MTVEC (o endereço base é sempre o conteúdo do MTVEC com os dois LSB em zero e deve apontar sempre para o início de uma página de 128 bytes de memória).

As tabelas 3 e 4 mostram a configuração e valores possíveis para o MCAUSE. Note que o bit 31 do MCAUSE (nos RV32 o valor de XLEN é igual a 32, então XLEN-1 é igual a 31) é o indicador de interrupção, ou seja, ele é setado quando o evento foi uma interrupção ou apagado quando o evento foi uma exceção.

Tabela 3 – Registrador MCAUSE
Tabela 4 – Valores possíveis para o MCAUSE

A tabela 5 apresenta o mapa de memória do CLINT (o termo Hart 0 refere-se a Hardware Thread 0, no sentido de que a ISA é pensada para ambientes multicore e portanto multithreading, desta forma, Hart 0 é a Thread ou código em execução no núcleo físico local).

Tabela 5 – Mapa de memória do CLINT

No E31 a interrupção de timer (MTIP) está associada ao standard timer, que consiste num contador de 64 bits (MTIME) impulsionado pelo clock io_rtcToggle (usualmente 32768Hz) e um comparador de 64 bits (MTIMECMP). Quando a contagem de MTIME é maior ou igual a MTIMECMP, o bit MTIP é setado no registrador MIP. O registrador MSIP é utilizado para gerar interrupções por software e o seu bit 0 é propagado para o bit MSIP no registrador MIP.

O sistema de interrupções conta ainda com registradores para sinalização de interrupção pendente e registradores para habilitação das fontes de interrupção locais. No modo supervisor temos os registradores SIP (interrupção pendente) e SIE (habilitação de interrupção). Os registradores de modo supervisor possuem bits relativos ao modo usuário (os bits iniciados por U) e os relativos ao modo supervisor (os bits iniciados por S). Os bits marcados “WIRI” devem ser ignorados (tanto na escrita quanto na leitura).

Tabela 6 – Registrador SIP (Supervisor Interrupt Pending)
Tabela 7 – Registrador SIE (Supervisor Interrupt Enable)

No modo máquina temos os registradores MIP (sinalização de interrupção pendente) e MIE (habilitação de interrupção). Note que os registradores de modo máquina incluem os bits de modo usuário (iniciados por U), os bits de modo supervisor (iniciados por S) e os bits de modo máquina (iniciados por M).

Tabela 8 – Registrador MIP (Machine Interrupt Pending)
Tabela 9 – Registrador MIE (Machine Interrupt Enable)

Analisando o registrador MIP, podemos identificar as três fontes de interrupção locais:

  • SIP (USIP, SSIP e MSIP): flag de interrupção por software pendente. A habilitação desta fonte de interrupção é feita através do registrador MIE (bits USIE, SSIE e MSIE);
  • TIP (UTIP, STIP e MTIP): flag de interrupção de timer pendente. A habilitação desta fonte de interrupção é feita através do registrador MIE (bits UTIE, STIE e MTIE);
  • EIP (UEIP, SEIP e MEIP): flag de interrupção externa pendente (oriunda do PLIC). A habilitação desta fonte de interrupção é feita através do registrador MIE (bits UEIE, SEIE e MEIE);

Observe que a existência de bits individuais de habilitação e pendência para cada modo de operação (usuário, supervisor e máquina) permite que determinadas interrupções estejam habilitadas apenas em modos específicos. No core E31, apenas as interrupções de modo Máquina devem ser utilizadas!

Interrupções Globais

As interrupções globais do RISC-V são administradas pelo módulo PLIC (Platform-level Interrupt Controller – controlador de interrupções no nível da plataforma). É possível ter até 255 fontes de interrupção globais. No core E31 todas as interrupções originadas dos periféricos (exceto o standard timer) são consideradas globais e por isso geridas pelo PLIC. Como já foi dito anteriormente, a saída do PLIC aciona a interrupção local externa (MEIP/SEIP/UEIP).

Cada fonte de interrupção pode ter a sua prioridade programada (endereços 0x0C000004 a 0x0C000400) entre 8 níveis possíveis (0= nunca interromper, 1 é a menor prioridade e 7 a maior prioridade). Apenas os três bits LSB de cada endereço são utilizados para definição das prioridades individuais, os demais não estão implementados e devem ser mantidos em 0.

Há também um conjunto de 8 registradores (0x0C001000 a 0x0C00101C) utilizados para armazenar os flags de interrupção pendente para as 255 fontes de interrupção possíveis. O endereço 0x0C001000 contém os flags das interrupções 0 (bit 0) até 31 (bit 31), o endereço 0x0C001004 contém os flags das interrupções 32 (bit 0) a 63 (bit 31) e assim por diante.

Nos endereços 0x0C002000 a 0x0C00201C encontramos registradores para a habilitação individual de cada uma das 255 fontes de interrupção, cada registrador controla 32 fontes exatamente como nos registradores dos flags de interrupção.

Tabela 10 – Mapa de memória do PLIC

Existe ainda um registrador de limiar de prioridade (priority threshold) no endereço 0x0C200000 que controla o nível de prioridade atual do PLIC, ignorando interrupções com nível inferior ao especificado.

O mecanismo de interrupção do PLIC é completado por um elemento chave que é o registrador localizado no endereço 0x0C200004 (claim/complete): este registrador sinaliza o ID atual da interrupção em tratamento pelo software (ou zero se não houver interrupção em tratamento).O código da ISR deve fazer o “claim” ou reivindicação da interrupção, sinalizando ao PLIC que irá tratar a interrupção indicada. O processo de reivindicação ocorre simplesmente pela leitura do registrador. O PLIC então irá promover o apagamento do flag de interrupção pendente correspondente ao ID reivindicado.

Após completar o tratamento da interrupção a ISR deve escrever no mesmo registrador (claim/complete) o ID da interrupção que foi tratada, isso completa o processo de interrupção no PLIC e permite criar um mecanismo de tail-chaining (por software) similar ao existente nos NVIC dos Cortex: após completado o tratamento da interrupção, caso o registrador claim/complete não esteja em zero signifca que outra interrupção está pendente e o processo de tratamento deve ser reiniciado.

Falando especificamente do core E31, temos um total de 52 fontes de interrupção globais no PLIC conforme podemos ver pela tabela 11.

Tabela 11 – Fontes de interrupção no PLIC do FE310

Interrupções na Prática

Toda esta conversa sobre registradores, mapas de memória, vetores, etc pode parecer um tanto complicado, então que tal vermos um exemplo prático de como são operadas as interrupções no RISC-V? O trecho de código a seguir poderia ser utilizado para o tratamento da interrupção do standard timer:

void handle_m_time_interrupt(){
  // set next timer compare to half a second from its previous value
  *mtimecmp += RTC_FREQ/2;
  // toggle blue LED state
  GPIO_REG(GPIO_OUTPUT_VAL) ^= 1 << BLUE_LED_OFFSET;
}

E a habilitação das interrupções se daria da seguinte forma:

set_csr(mie, MIP_MTIP);		// habilita interrupção do timer no modo máquina
set_csr(mstatus, MSTATUS_MIE);	// habilita interrupções globais

Note que o código de inicialização (em bsp/env/freedom-e300-hifive1/init.c) fornecido pela SiFive se encarrega de inicializar os vetores de interrupção apropriadamente:

#ifdef USE_M_TIME
extern void handle_m_time_interrupt();
#endif

uintptr_t handle_trap(uintptr_t mcause, uintptr_t epc)
{
  if (0){
#ifdef USE_PLIC
    // External Machine-Level interrupt from PLIC
  } else if ((mcause & MCAUSE_INT) && ((mcause & MCAUSE_CAUSE) == IRQ_M_EXT)) {
    handle_m_ext_interrupt();
#endif
#ifdef USE_M_TIME
    // External Machine-Level interrupt from PLIC
  } else if ((mcause & MCAUSE_INT) && ((mcause & MCAUSE_CAUSE) == IRQ_M_TIMER)){
    handle_m_time_interrupt();
#endif
  }
  else {
    write(1, "trap\n", 5);
    _exit(1 + mcause);
  }
  return epc;
}

void _init()
{
  #ifndef NO_INIT
  use_default_clocks();
  use_pll(0, 0, 1, 31, 1);
  uart_init(115200);
  printf("core freq at %d Hz\n", get_cpu_freq());
  write_csr(mtvec, &trap_entry);
  if (read_csr(misa) & (1 << ('F' - 'A'))) { // if F extension is present
    write_csr(mstatus, MSTATUS_FS); // allow FPU instructions without trapping
    write_csr(fcsr, 0); // initialize rounding mode, undefined at reset
  }
  #endif
}

Maiores detalhes sobre a utilização de interrupções serão vistos nos próximos artigos desta série.

SiFive FE310G

O FE310G é a primeira implementação comercial em silício de um RISC-V e inclui com core de 32 bits seguindo a ISA RV32IMAC, que consiste no conjunto completo de instruções de 32 bits, além de instruções comprimidas (com tamanho reduzido para 16 bits), multiplicação e divisão por hardware e operações atômicas. O core E31 utilizado no chip inclui também um pipeline de 5 estágios, sistema de predição de desvios com tabela de 40 entradas para endereços de destino, 128 entradas de histórico de desvio e pilha de dois níveis para endereços de retorno. Este sistema de predição de desvios evita o esvaziamento do pipeline (e eventualmente do cache) em razão de desvios no fluxo do código. Caso a predição de desvio falhe, a instrução de desvio é executada em três ciclos de clock (ao invés de um). A maioria das instruções é executada em 1 ciclo de clock (desde que esteja dentro do cache), mas instruções de carga (load) como LW (load word) é executada em 2 ciclos enquanto que LB, LBU, LH, LHU (carga de byte ou half-word) são executadas em 3 ciclos, instruções de leitura de CSR executam em 5 ciclos, multiplicações executam em 5 ciclos e divisões entre 2 e 33 ciclos (conforme os operandos).

Figura 1 – Diagrama em blocos do FE310G

As características técnicas do chip são as seguintes:

  • CPU RISC-V RV32IMAC capaz de operar a até 320MHz com cache de instruções (16Kb), memória RAM de 16Kb, memória ROM de boot de 8Kb, memória de configuração OTP de 8Kb e memória de programa serial externa (FLASH QSPI de 128Mb na placa HiFive);
  • Módulo integrado de depuração com interface JTAG;
  • Tensão de alimentação de 1,8V (core e PLL) e 3,3V (E/S);
  • Módulo de proteção de memória (PMP) com 8 regiões (escrita, leitura e execução);
  • Osciladores internos (HFROSC calibrado em 72MHz e LFROSC de 32768Hz), entradas de clock externo (16MHz e 32768Hz) e PLL;
  • 19 pinos de E/S, com interrupção individual para cada pino;
  • Módulo AON (Always On) com 16 registradores de 32 bits, RTC e gerenciamento de energia (no modo sleep este é o único elemento do chip que permanece ativo);
  • 2 UARTs;
  • 3 SPI (uma delas QSPI);
  • Watchdog;
  • 3 timers PWM com 4 saídas cada (um de 8 bits e dois de 16 bits).
Figura 2 – FE310G no encapsulamento QFN de 48 pinos

Como é possível perceber, o conjunto de periféricos é bastante modesto para um microcontrolador de 32 bits lançado em 2016. Não há periféricos para interface analógica (ADC, DAC, comparador, etc.), os timers são relativamente simples e basicamente focados em PWM, estão ausentes interfaces de comunicação mais avançadas (como USB) e até mesmo a quantidade de memória RAM é insuficiente para aplicações um pouco mais complexas.

No entanto, esta carência de periféricos deve ser relevada pois estamos falando do primeiro chip de uma possível geração de microcontroladores de 32 bits com core aberto. Além disso, outros grandes fabricantes de semicondutores (como Qualcomm, Samsung, etc.) que fazem parte da iniciativa RISC-V poderão lançar dispositivos baseados neste core e utilizando suas próprias bibliotecas de periféricos e processos de fabricação (o FE310G é fabricado pela TSMC num processo de 180nm).

Na próxima parte deste artigo vamos conhecer o ambiente Freedom Studio (baseado no Eclipse) e ver alguns pequenos exemplos de código utilizando o FE310G, até lá!

Referências

Nota: as tabelas e imagens utilizadas neste artigo foram retiradas da documentação da SiFive no website indicado nas referências acima.

Leave a Reply