• FPGA

IP Core

Nesse tutorial iremos desenvolver um periférico customizado para o processador NIOS, esse periférico será dedicado ao controle dos LEDs da placa. O periférico terá um banco de registradores interno para seu controle, e interface de "I/O mapeado em memória" para que possamos controlar-lo do NIOS (pelo código em C).

Começando

Para seguir esse tutorial, é necessário:

  • Hardware: DE10-Standard e acessórios
  • Softwares: Quartus 18.01
    • Modelsim Simulator

Entrega no git:

  • Pasta: Lab3_FPGA_IP/

IP Cores

Intelectual Proprety Core (IP Core) são componentes descritos em HDL que podem ser utilizados em múltiplos projetos de Hardware. O Platform Designer (PD) fornece além da interface visual de conexão entre esses IPs um padrão de comunicação entre os componentes, facilitando assim o uso desses IPs.

Além das centenas de projetos espalhados pela internet (github), existe um repositório muito completo de IP cores opensource que concentra grande variedade de projeto:

As empresas também disponibilizando IPs, pagos e gratuitos:

Platform Desginer

O PD é uma ferramenta integradora de IPs, com ela é muito simples inserirmos e criarmos componentes que serão utilizados para formar um sistema mais completo. Como no caso do tutorial passado onde usamos uma série de componentes para criar nosso projeto. Esses componentes são de certa forma IPs (simples como o PIO e complexo como o NIOS).

A integração dos IPs no PD se da devido à padronização da comunicação entre esses componentes, que é dada via o barramento.

Barramentos

A Intel-FPGA define duas categorias de barramento de dados para o PD: Avalon e AXI. O barramento Avalon é a principal maneira de conectar um periférico ao NIOS (processador), já o AXI é o padrão de barramento do ARM, que também é utilizado no platform designer.

Avalon

O barramento Avalon define basicamente dois tipos de módulos: Memory Mapped (MM) e Avalon Streaming Interface (ST), conforme descrição a seguir extraído da documentação:

  • Avalon Streaming Interface (Avalon-ST)an interface that supports the unidirectional flow of data, including multiplexed streams, packets, and DSP data.

  • Avalon Memory Mapped Interface (Avalon-MM)an address-based read/write interface typical of master–slave connections.

  • Avalon Conduit Interfaean interface type that accommodates individual signals or groups of signals that do not fit into any of the other Avalon types. You can connect conduit interfaces inside a Platform Designer system. Or, you can export them to make connections to other modules in the design or to FPGA pins.

  • Avalon Tri-State Conduit Interface - (an interface to support connections to off-chip peripherals. Multiple peripherals can share pins through signal multiplexing, reducing the pin count of the FPGA and the number of traces on the PCB.

  • Avalon Interrupt Interfacean interface that allows components to signal events to other components.

  • Avalon Clock Interface*an interface that drives or receives clocks.
  • Avalon Reset Interfacean interface that provides reset connectivity.

Tip

Iremos trabalhar com o tipo: Avalon-MM como maneira de conectar o periférico ao NIOS.

Projeto

Note

Vamos melhorar o projeto passado, faça uma cópia da pasta do projeto: Lab2_FPGA_NIOS/ e renomeei para: Lab3_FPGA_IP/. Iremos agora trabalhar nessa nova pasta.

Criando um periférico

Vamos criar um novo componente que será capaz de controlar os LEDs com maior autonomia.

Roteiro a ser seguido:

  1. Especificação
  2. Gerar HDL que representa o periférico com interface Avalon
  3. Criar o componente no Platform Designer
    • Associar arquivos ao componente
    • Definições gerais
    • Associar as portas do componente com os sinais do barramento
  4. Usar componente no projeto
  5. Criar driver (.c e .h)
  6. Simular
  7. Implementar/ Testar
  8. Rever especificação (1.)

Primeiramente precisamos definir o papel principal desse periférico e seu fluxo de dados. Com isso será possível definir se o periférico é do tipo: Master ou Slave e se sua interface é do tipo Memory Mapped ou Streaming.

Um periférico pode possuir mais de uma interface, por exemplo: Um periférico que irá processar um áudio em tempo real pode ter até três interfaces: O mesmo irá receber o áudio via a interface streaming e retornar o dado por outra interface de streaming, porém será necessária uma terceira interface para controle desse periférico, muito provavelmente do tipo Memory Mapped.

É possível transmitir pacotes de comando pela interface streaming, mas isso torna o projeto mais complexo.

O nosso simples periférico irá simplesmente receber configurações para acionar o LED, sem nenhum fluxo contínuo ou intenso de dados, sendo a interface mais apropriada a do periférico mapeado em memória. Além disso, nosso periférico exclusivo para controle do LED é um slave do sistema, já que ele deve ser controlado por outra parte do sistema (no nosso caso o uC) para agir conforme necessário.

Avalon Slave Memory Mapped

Para nosso periférico se comunicar com o processador precisamos implementar o padrão de comunicação utilizado pelo NIOS. Podemos optar por implementar o padrão completo ou apenas uma parte de sua especificação. Por exemplo, se nosso periférico não faz uso do waitrequest ou byteenable podemos optar por não implementar esses sinais.

A seguir um exemplo dos sinais de um periférico mapeado em memória que possui como interface com o Avalon-MM-Slave.

entity peripheral_MM is
    port (
        -- Gloabals
        clk                : in  std_logic                     := '0';             
        reset              : in  std_logic                     := '0';             

        -- Avalon Memmory Mapped Slave
        avs_address     : in  std_logic_vector(3 downto 0)  := (others => '0'); 
        avs_read        : in  std_logic                     := '0';             
        avs_readdata    : out std_logic_vector(31 downto 0) := (others => '0'); 
        avs_write       : in  std_logic                     := '0';           
        avs_writedata   : in  std_logic_vector(31 downto 0) := (others => '0')  
    );
end entity peripheral_MM;

Note que a primeira parte do componente define um sinal de clock (clk) e um sinal de reset (reset), lembre que projetos digitais em FPGA devem ser na maioria das vezes síncronos. A segunda parte é a definição dos sinais que irão ser conectados no barramento para acesso de outros periféricos.

Lembrem que estamos criando um componente mapeado em memória, logo o mesmo deve ter comportamento e interface similar ao de uma memória.

  • avs_address: Endereço de acesso ao componente, no caso, 4 bits.
  • avs_read: Indica que é um acesso de leitura
  • avs_readdata: Dado que será retornado ao Master dado um acesso de leitura.
  • avs_write: Indica que é um acesso de escrita
  • avs_writedata: Dado que é transmitido ao componente dado um acesso de escrita.

O tamanho da palavra do avs_readdata e do avs_writadata é definido pelo componente e não é fixo em 32 bits como no exemplo, pode assumir outros valores.

Uma escrita ao periférico é dada da seguinte forma:

  1. Master endereça periférico
  2. Endereço absoluto é traduzido em relativo
    • O endereço que o master escreve no periférico é composto por: addr ➕ offset porém o slave só possui acesso ao offset.
  3. Periférico recebe: avs_address, avs_write = '1' e avs_writedata.

Uma leitura ao periférico é dada da seguinte forma:

  1. Master endereça periférico
  2. Endereço absoluto é traduzido em relativo
  3. Periférico recebe: avs_adddress e avs_read = '1'
  4. Periférico atualiza: avs_readdata

Perguntas

O barramento AVALON define outros sinais, responda a seguir sobre alguns desses sinais:

  • waitrequest
  • Qual o papel do waitrequest?
  • Quem aciona o waitrequest (Slave ou Master)?
  • byteenable
  • Qual o papel do byteenable?
  • Quem aciona o byteenable (Slave ou Master)?

Especificação

Nosso periférico será no começo bem simples, apenas para entendermos todo o processo de desenvolvimento de um periférico e o seu uso. O periférico que iremos desenvolver será um substituto ao periférico PIO fornecido pela Intel-FPGA, utilizado no projeto do pisca LED com o NIOS.

Nosso periférico será mapeado em memória e possuirá um conduit (saída) onde será realizada o acionamento dos LEDs:

O acesso ao nosso periférico será por uma palavra de 32 bits (para mater um padrão com o NIOS) e terá dois registradores REG_CONFIG e REG_DATA:

  • REG_CONIFG: Registrador que controla o periférico, no nosso caso, irá ter somente um bit de: Enable/Disable (bit0 ➡️ Enable/Disable)
  • REG_DATA: Registrador que possui o valor de cada LED (bit0 ➡️ LED0; bit1 ➡️ LED1 ....).

Gerar HDL que representa o periférico com interface Avalon

Partindo da entidade fornecida (peripheral_MM), podemos criar um componente que implementa parcialmente a especificação anterior, nessa implementação não temos os dois registradores (REG_CONFIG e REG_DATA), temos apenas a funcionalidade do REG_DATA. Note que a implementação faz uso de um generic para definir a quantidade de LEDs que esse periférico controla. Esse genérico poderá ser configurado pela interface gráfica do Plataform Designer, tornando um componente customizado.

Info

Crie um arquivo chamado: peripheral_LED.vhd e salve na pasta do projeto : Lab3_FPGA_IP/IP/

Será necessário criar a pasta IP

library IEEE;
use IEEE.std_logic_1164.all;
use IEEE.std_logic_unsigned.all;
use IEEE.numeric_std.all;
use work.all;

entity peripheral_LED is
    generic (
        LEN  : natural := 4
    );
    port (
        -- Gloabals
        clk                : in  std_logic                     := '0';             
        reset              : in  std_logic                     := '0';             

        -- I/Os
        LEDs               : out std_logic_vector(LEN - 1 downto 0) := (others => '0');

        -- Avalion Memmory Mapped Slave
        avs_address     : in  std_logic_vector(3 downto 0)  := (others => '0'); 
        avs_read        : in  std_logic                     := '0';             
        avs_readdata    : out std_logic_vector(31 downto 0) := (others => '0'); 
        avs_write       : in  std_logic                     := '0';             
        avs_writedata   : in  std_logic_vector(31 downto 0) := (others => '0')
    );
end entity peripheral_LED;

architecture rtl of peripheral_LED is
begin

  process(clk)
  begin
    if (reset = '1') then
      LEDs <= (others => '0');
    elsif(rising_edge(clk)) then
        if(avs_address = "0001") then                  -- REG_DATA
            if(avs_write = '1') then
              LEDs <= avs_writedata(LEN - 1 downto 0);
            end if;
        end if;
    end if;
  end process;

end rtl;

Limitações dessa implementação

  • Não possui um registrador de configuração: REG_CONFIG
  • Não é possível ler: REG_DATA via barramento Avalon
  • impede a aplicação de máscaras!

Poderíamos já nessa etapa testar o componente, criando um testbench para excitar o módulo e verificar seu comportamento. Grande parte do desenvolvimento de um projeto de hardware é gasto nos testes, que podem ser tão complexos quanto o próprio módulo. Vamos pular essa etapa aqui, iremos simular em um nível mais alto.

Configurando path

Agora iremos adicionar o nosso periférico no Platform Designer, esse novo componente que será criado será incorporado na ferramenta, para isso:

Precisamos indicar para o PD o local que ele deve buscar para encontrar por códigos fonte que não fazem parte do catálogo padrão, para isso:

  1. Tools ➡️ Options ➡️ IP Search Path
  2. Adicione a pasta IP recém criada.

E agora remova o componente PIO:

  1. Remova o PIO que controlava os LEDs (agora iremos fazer o controle pelo nosso componente)

Criando componente

Só adicionar o arquivo HDL (.vhd ou .v) não é suficiente para o PD reconhecer o componente, precisamos criar um segundo arquivo (*_hw.tcl) que é lido pelo PD, esse arquivo possuirá todas as configurações e descrições do novo componente. Para isso:

  • File ➡️ New Component 🆗

E uma interface gráfica de configuração do componente será exibida. A primeira parte é referente a descrição do próprio componente. De o nome desse componente de : peripheral_LED e preencha sua descrição.

Já na aba Files temos as informações de quais arquivos pertencem ao componente.

Files

Na aba Files adicione o arquivo peripheral_LED.vhd:

  1. Files ➡️ Syntesis Files ➡️ add file ➡️ peripheral_LED.vhd
  2. Clique em ➡️ Analyze Synthesis Files : isso fará com que a ferramenta faça uma breve análise dos arquivos HDL e detecte as interfaces do componente.

Note o atributo do arquivo: Top-level File, isso indica que o peripheral_LED.vhd é o arquivo principal desse componente, se tivéssemos um desenvolvimento hierárquico do componente, nessa etapa adicionaríamos vários arquivos e deveríamos configurar qual deles é o toplevel.

  • Na secção VHDL Simulation Files ➡️ Copy from Synthesis Files 🆗

Note que se não adicionarmos esse arquivo nessa secção, na hora de simular o projeto o componente estaria vazio. Porquê o padrão não é o de automaticamente copiar os arquivos da síntese para a simulação? Pois nem sempre conseguimos simular o que será sintetizado. Pense no caso desse componente ser um controlador de memória, se formos simular não teremos a memória física para o controlador acessar e a simulação não funcionará. Uma solução seria de ter dois componentes, um para simulação (que imita a memória) e outro para síntese.

Signals & Interfaces

Nessa secção iremos configurar as interfaces do nosso componente, e como o PD irá interpretá-las quando formos conectar ao resto do sistema. Note que algumas interfaces já foram detectadas pelo PD, porém temos um erro que será corrigido.

Nas interfaces padrões note que o Component Editor já detectou uma interface:

  • avalon_slave_0
  • clock
  • reset

Isso aconteceu pelos nomes da entidade do peripheral_led.

Vamos primeiramente editar o avalon_slave_0. Clique na interface e note que a ferramenta indica um erro :

Failure

Error: avalon_slave_0_1: Interface must have an associated reset

Vamos associar ter que associar um sinal der reset a interface (parte sequência do IP), para isso :

  • avalon_slave_0 ➡️ Associated Reset ➡️ reset 🆗

Podemos notar ainda pelo diagrama (e pela mensagem de erro) que a ferramenta interpretou de forma errada o nosso sinal LEDs, pertencente a entidade do componente:

-- I/Os
LEDs : out std_logic_vector(LEN - 1 downto 0) := (others => '0');

Note pelo diagrama de blocos que o PD atribui essa saída como sendo parte do barramento Avalon: writerequestvalid_n, o que não é verdade. Para corrigir isso, precisamos de uma nova aba que não é padrão de exibição, no component builder clique em:

  • Component builder ➡️ View ➡️ Signals 🆗

Essa nova aba permite verificarmos (e associarmos) as entradas e saídas da entidade (toplevel) com sinais e tipos de sinais definido pelo PD.

Iremos indicar agora para a ferramenta que o sinal LEDs deve ser interpretado como um conduite, edite os sinais como na figura a seguir :

Finalizando

Verifique os sinais e o diagrama de bloco antes de continuar e clique em Finish. Quando o componente for gerado, ele automaticamente irá aparecer no catálogo de componentes que podem ser inseridos no SoC:

Porém o arquivo de configuração desse componente (.tcl) foi salvo na pasta raiz do projeto do Quartus:

  • Lab3_FPGA_IP/peripheral_LED_hw.tcl

Esse arquivo .tcl descreve todas as configurações realizadas anteriormente no componente. O mais natural é que esse arquivo esteja na mesma localidade (pasta IP) que os códigos HDL. Mova essa arquivo para:

  • Lab3_FPGA_IP/IP/peripheral_LED_hw.tcl

Agora precisamos editar o arquivo .tcl para atualizarmos o local do arquivo peripheral_LED.vhd, procure pela secção files set:

  • Antes
add_fileset_file peripheral_LED.vhd VHDL PATH IP/peripheral_LED.vhd TOP_LEVEL_FILE
...
add_fileset_file peripheral_LED.vhd VHDL PATH IP/peripheral_LED.vhd

E edite para:

add_fileset_file peripheral_LED.vhd VHDL PATH peripheral_LED.vhd TOP_LEVEL_FILE
...
add_fileset_file peripheral_LED.vhd VHDL PATH peripheral_LED.vhd

Utilizando o componente no PD

Agora adicione o componente no projeto e faça as conexões corretas (como se fosse outro componente), exporte o sinal dos LEDs, o resultado final deve ser algo como:

Gere o componente: Clique em Generate HDL ➡️ Generate.

Marque a opção: ✅ Create a Simulation Model

Utilizando o componente no topLevel.vhd

Precisamos agora modificar o componente inserido no topLevel, para isso no PD gere novamente o template de utilização :

  • No Platform Designer: Generate ➡️ Show Instatiation Template ➡️ VHDL

No meu caso o resultado foi:

    component niosHello is
        port (
            buts_export   : in  std_logic_vector(2 downto 0) := (others => 'X'); -- export
            clk_clk       : in  std_logic                    := 'X';             -- clk
            reset_reset_n : in  std_logic                    := 'X';             -- reset_n
            leds_name     : out std_logic_vector(3 downto 0)                     -- name
        );
    end component niosHello;

    u0 : component niosHello
        port map (
            buts_export   => CONNECTED_TO_buts_export,   --  buts.export
            clk_clk       => CONNECTED_TO_clk_clk,       --   clk.clk
            reset_reset_n => CONNECTED_TO_reset_reset_n, -- reset.reset_n
            leds_name     => CONNECTED_TO_leds_name      --  leds.name
        );

Devemos inserir agora esse componente com a nova interface (leds_name) no Lab3_FPGA_IP.vhd.

Você deve fazer essa etapa com cuidado. Esses nomes podem alterar entre versões da ferramenta.

Editando o Lab3_FPGA_IP.vhd:

library IEEE;
use IEEE.std_logic_1164.all;

entity Lab3_FPGA_IP is
    port (
        -- Gloabals
        fpga_clk_50        : in  std_logic;             -- clock.clk

        -- I/Os
        fpga_led_pio       : out std_logic_vector(3 downto 0);
        fpga_button_pio    : in  std_logic_vector(2 downto 0)

    );
end entity Lab3_FPGA_IP;

architecture rtl of Lab3_FPGA_IP is

   component niosHello is
        port (
            buts_export   : in  std_logic_vector(2 downto 0) := (others => 'X'); -- export
            clk_clk       : in  std_logic                    := 'X';             -- clk
            reset_reset_n : in  std_logic                    := 'X';             -- reset_n
            leds_name     : out std_logic_vector(3 downto 0)                     -- name
        );
    end component niosHello;

begin

    u0 : component niosHello port map (
        clk_clk       => fpga_clk_50,     --  clk.clk
        reset_reset_n => '1',             --  reset.reset_n
        leds_name     => fpga_led_pio ,   --  leds.export
        buts_export   => fpga_button_pio  --  buts.export   
    );


end rtl;

Recompile

Salve, compile o projeto e programe a FPGA

Podemos analisar agora o RTL do projeto e mais especificamente o do componente criado:

Verificamos que a ferramenta inferiu um registrador de 4 bits para armazenar o valor dos LEDs, um Mux para indicar se os registradores serão ou não atualizados com um novo valor e um comparador para verificar se o endereço é equivalente a 0x01.

Firmware

Devemos agora escrever um firmware que será executado no NIOS e que acesse e controle nosso periférico. Para isso será necessário criarmos um BSP para o projeto. Abra o NIOS II Software Build ... e refaça a etapa do tutorial anterior com o novo SoC e adicione o código a seguir:

#include <stdio.h>
#include "system.h"
#include <alt_types.h>
#include <io.h> /* Leiutura e escrita no Avalon */

//#define SIM

// LED Peripheral
#define REG_DATA_OFFSET 1

int main(void){
  unsigned int led = 0;
  unsigned int *p_led = (unsigned int *) PERIPHERAL_LED_0_BASE;

#ifndef SIM
  printf("Embarcados++ \n");
#endif

  while(1){
      if (led < 4){
          *(p_led+REG_DATA_OFFSET) = (0x1 << led++);
#ifndef SIM
          usleep(500000); // remover durante a simulação
#endif
      }
      else{
          led = 0;
      }
  };

  return 0;
}

O firmware utiliza o peripheral_LED para controlar os LEDs da placa, note que o acesso dessa vez é feito pelo ponteiro p_led e não mais pela função da Alteara IOWR_32DIRECT (deveria funcionar também).

Executando

Execute o firmware no kit de desenvolvimento e verifique se sua funcionalidade está correta. Lembre que o HW já deve estar programado (via quartus), caso contrário não funcionará.

Simulando

Uma das grandes vantagens de trabalharmos com SoftProcessor é que temos acesso ao seu código fonte (criptografado ou não) e isso possibilita que possamos simular todo o sistema, verificando suas funcionalidades internas, comunicação da CPU com os periféricos, interface do firmware com o resto do sistema. Vamos nessa etapa simular a interface do NIOS com o nosso periférico e verificar se está tudo certo.

Note que no código anterior, o printf foi comentando, assim como o delay de 50000 us, que no lugar foi inserido um de 1us. Isso foi feito para acelerar a simulação e verificarmos mais rapidamente o acesso do NIOS ao periférico, que acontece na linha :

    *(p_led+REG_DATA_OFFSET) = (0x1 << led++);

Nesse momento, o NIOS envia um comando ao barramento no endereço PERIPHERAL_LED_0_BASE + REG_DATA_OFFSET, o comando carrega a mensagem: 0x01 << led, gravando no registrador REG_DATA qual LED deve ser acionado.

Configurando o bsp

Para obtermos um resultado mais rápido é possível ativarmos uma opção no bsp chamada de: enable_sim_opitimize. Quando ativada, o binário compilado só poderá ser usado para simulação, não pode ser embarcado no HW!. Com essa opção temos um ganho significativo no tempo de execução do modelo no modelsim.

Além de configurarmos a otimização durante a simulação, iremos desativar o stdin, stdout, stderr para a simulação ficar ainda mais rápida, caso contrário teremos que esperar por muito tempo até verificarmos o resultado do código. Note que a simulação abrange todo o HW desde o processador até o barramento e periféricos.

Note

Para simularmos 1 ms de execução do HW será necessário muito mais que 1 ms de esforço computacional! O tempo pode chegar a unidades de hora!!

ModelSim

No Eclipse, após ter compilado o projeto:

  • Run ➡️ Run configuration ➡️ Nios II ModelSim

O simulador a ser utilizado é o modelsim da Mentor, o mais completo do mercado e fornecido com algumas customizações pela Intel-FPGA. No modelsim, iremos adicionar os sinais que desejamos visualizar, para isso, siga o que indica a figura a seguir:

Após adicionar todos os sinais que fazem parte do periférico led_peripheral iremos executar 500 us de simulação:

Após a simulação finalizar, note os valore dos sinais avs_write, avs_writedata, avs_LEDs e como eles mudam no tempo em respeito ao que foi feito no código.

Praticando

  1. Faça o periférico ter 6 saídas (6 LEDs)
  2. Faça os LEDs piscarem por HW, adicione um registrador para controlar a frequência.
  3. Como colocar as chaves no periférico para que o NIOS tenha acesso a elas?

Entrega 3

Siga para a terceira entrega: