Este relatório documenta, em detalhes, o desenvolvimento de um sistema completo de processamento matricial baseado em FPGA, envolvendo três camadas principais:
- Um coprocessador programado em Verilog, implementado em um kit DE1-SoC, capaz de executar operações matemáticas sobre matrizes 5×5 com inteiros de 8 bits.
- Uma biblioteca em C/Assembly, que funciona como driver e interface de controle entre o processador ARM (HPS) e a FPGA.
- Uma aplicação interativa em C99, baseada em terminal, que permite ao usuário carregar arquivos, selecionar operações e visualizar os resultados.
O projeto prioriza baixo nível, eficiência e robustez, promovendo a integração direta entre software e hardware por meio de registradores mapeados em memória. Ao longo deste documento, cada componente é abordado separadamente, com explicações técnicas e exemplos práticos de uso, desde a FSM do hardware até a lógica de parsing da DSL do usuário.
gcc -std=c99 -Iapp -Ilib app/main.c lib/laplace.s -o laplaceObservações: Você deve incluir as pastas app/ e lib/ para compilá-lo.
Na aba Releases é possível ver dois arquivos, um arquivo zip, que contém o projeto Quartus pré-compilado e o binário da aplicação em C compilado para Linux ARMv7.
- Cláudio Daniel Figueredo Peruna
- Paulo Gabriel da Rocha Costa Silva
- Paulo Henrique Barreto Dantas
- Visão Geral da Arquitetura
- Interface Host ↔ FPGA
- Máquina de Estados Finita (FSM)
- Handshake e Protocolo de Comunicação
- Feedback Visual por LEDs
- Driver em C + Assembly
- Aplicação de Usuário (C99 CLI)
- Testes e Estratégias de Depuração
- Tabela-Resumo de Sinais
O atual projeto implementa um ecossistema embarcado completo voltado para o processamento de operações matriciais por meio de um coprocessador dedicado implementado em FPGA. Para alcançar esse objetivo, a solução foi dividida em três grandes blocos interdependentes, cada qual com uma responsabilidade bem definida:
O coração do projeto é o coprocessador, uma unidade lógica programada em Verilog e sintetizada em uma FPGA do kit DE1-SoC. Este coprocessador é capaz de realizar, de maneira totalmente paralela, operações matriciais sobre matrizes 5×5 de inteiros com sinal de 8 bits.
Entre suas características mais relevantes, destacam-se:
- Modularidade via FSM (Finite State Machine): toda a operação é controlada por uma máquina de estados clara e bem definida, que responde a comandos específicos vindos do host.
- Interface binária controlada: a comunicação com o host se dá via dois registradores mapeados em memória,
pio_out[31:0](entrada) epio_in[31:0](saída), controlados pelo barramento leve HPS–FPGA.
Essa separação entre entrada e saída garante que o coprocessador possa operar de forma previsível e síncrona, obedecendo a comandos e reconhecendo confirmações do host via pulsos bem definidos.
Para permitir que um programa em C consiga se comunicar com o coprocessador, foi desenvolvida uma biblioteca híbrida contendo:
- Um cabeçalho em C (
laplace.h) com constantes, tipos e protótipos de funções. - Corpo da função implementados em Assembly ARMv7, especialmente os que manipulam diretamente os registradores de controle da FPGA (por exemplo, escritas bit-a-bit nos registradores
cmdestat).
Essa biblioteca atua como uma ponte transparente entre o alto nível (aplicação C) e o baixo nível (registradores físicos da FPGA), abstraindo detalhes complexos como:
- Aritmética de ponteiros para mapear memória física via
/dev/mem - Envio de instruções codificadas em 32 bits
- Sincronização por delay microcontrolado e pulsos de "avança estágio"
Ela foi pensada para ser mínima, segura e previsível. Com poucos arquivos, consegue encapsular toda a lógica de comunicação, oferecendo ao programador funções fáceis de usar como mpu_store(), mpu_load() e next_stage().
O terceiro bloco do projeto é a interface de linha de comando, escrita em C99, que permite ao usuário interagir com o coprocessador de maneira intuitiva. Essa aplicação é composta por vários arquivos modulares:
main.c— loop principal, inicialização da conexão e chamada das funções da biblioteca e pedidos de interação com o usuário.parser.h— leitura e validação de arquivos.lpque representam matrizes ou escalaresui.h— interação com o terminal.types.h— tipos auxiliares.
Destaque especial vai para o uso de uma DSL (Domain Specific Language) chamada Laplace, que facilita a entrada de dados pelo usuário. A linguagem permite:
- Representação textual simples de matrizes e escalares
- Comentários e sintaxe tolerante a erros
- Detecção automática de tamanho da matriz
- Validação detalhada com mensagens amigáveis
A escolha pelo padrão C99 foi deliberada. O projeto se beneficia de recursos como:
- Tipos de inteiros de tamanho fixo (
int8_t,uint32_t, etc.) - Declarações dentro de blocos
- Melhor suporte à modularização
- Legibilidade e segurança de tipo
- Regras de declaração de variáveis mais legíveis e intuitivas.
Isso tudo contribui para que o código da aplicação seja mais robusto, moderno e fácil de manter, especialmente em comparação com o antigo padrão C89.
A comunicação entre os três blocos ocorre em camadas bem delimitadas:
Usuário
⇅ (input via terminal)
Aplicação C99
⇅ (via structs e funções)
Driver (C + Assembly)
⇅ (via registradores mapeados)
FPGA (Verilog)
Essa separação favorece testes modulares (cada parte pode ser validada independentemente), bem como portabilidade (o software poderia se adaptar a outro hardware mantendo a mesma interface de driver).
A interface entre o host (um processador ARM embarcado no HPS do DE1-SoC) e o coprocessador (implementado na FPGA) é feita por meio de dois registradores de 32 bits mapeados em memória, acessados via o Lightweight HPS–FPGA Bridge:
pio_out[31:0]: registrador de entrada – envia comandos e dados do host para o coprocessador.pio_in[31:0]: registrador de saída – recebe os dados de volta do coprocessador para o host.
Esses registradores são acessados diretamente através de ponteiros volatile em C e Assembly, garantindo total controle e baixa latência.
O registrador pio_out é onde o host escreve instruções para o coprocessador. Ele é dividido em campos com significados específicos:
| Bits | Campo | Função |
|---|---|---|
| 31 | start_pulse |
Pulso de início (1 ciclo). Dispara carregamento ou execução, dependendo do estágio atual. |
| 30 | matrix_size_reg |
Define o tamanho lógico da matriz: 0 = 3×3, 1 = 2×2. Usado somente na operação determinante. |
| 29–27 | op_code |
Código da operação a ser executada. Decodificado internamente pela FSM para selecionar a lógica apropriada. |
| 26 | ack_pulse |
Pulso de confirmação de leitura: o host informa que já processou o dado recebido. |
| 25–9 | (reservado) | Não utilizados. Sempre escritos como zero para evitar efeitos indesejados. |
| 8–1 | bit_pos |
Posição de 0 a 199 que indica qual bit está sendo gravado em Matrix_A ou Matrix_B. |
| 0 | bit_val |
Valor binário (0 ou 1) que será gravado na posição indicada por bit_pos. |
O registrador pio_out permite o carregamento completo das duas matrizes A e B, bit a bit, além de configurar a operação desejada e sinalizar o avanço entre estágios com pulsos de controle. A lógica no Verilog faz detecção de borda para start_pulse e ack_pulse, simplificando o protocolo: basta escrever um "1" por um ciclo para disparar a ação correspondente.
Já o pio_in é utilizado pelo coprocessador para informar o progresso da operação e entregar os resultados, estruturado da seguinte forma:
| Bits | Campo | Função |
|---|---|---|
| 31 | flag_reg |
Flag de novo dado: quando 1, indica que há um byte pronto para leitura. |
| 30–26 | byte_idx |
Índice do byte atual dentro do vetor de saída Result[199:0]. |
| 25–8 | Zeros (18'd0) |
Preenchimento fixo para completar os 32 bits. |
| 7–0 | data_out_reg |
O byte efetivo de resultado (parte da matriz calculada). |
A comunicação é orientada a bytes sequenciais: a matriz final de 25 bytes é transmitida do coprocessador para o host um byte por vez. Para cada byte, o FPGA sinaliza que há dado disponível (flag_reg = 1). O host responde com ack_pulse, fazendo o byte seguinte ser colocado no barramento. A contagem byte_idx ajuda o host a controlar o progresso da leitura.
(Host) Envia bits de A → `bit_val`, `bit_pos`, `start_pulse`
(Host) Envia bits de B → idem
(Host) Define `op_code`, `matrix_size_reg`, envia novo `start_pulse`
(FPGA) Executa operação
(FPGA) Coloca byte de resultado em `data_out_reg`, aciona `flag_reg`
(Host) Lê byte, envia `ack_pulse`
↻ Repete até `byte_idx == 24`
- Eficiência binária: O protocolo usa poucos bits com significados claros, evitando overhead de comunicação.
- Controle total: O host tem domínio total sobre o ritmo da operação, inclusive podendo interromper e reiniciar.
- Simplicidade no hardware: O uso de borda e pulsos binários simplifica a FSM do Verilog e evita bugs de latch ou sinal mantido.
-
A interface é projetada somente para matrizes 5×5 (200 bits). Outros tamanhos exigiriam mudanças tanto no FSM quanto na lógica de envio e leitura.
-
Os bits
25:9reservados podem futuramente acomodar:- Flags de saturação, overflow ou underflow
- Configurações de ponto flutuante
- Operações com mais de duas matrizes ou com máscara condicional
A FSM (Finite State Machine) é uma estrutura de controle sequencial implementada em Verilog que orquestra o comportamento do coprocessador. Ela age como um “diretor de cena”, garantindo que cada módulo execute sua função no momento certo e com os sinais apropriados.
A FSM responde a comandos recebidos via pio_out e avança por cinco estados principais, garantindo sincronização e segurança na execução da operação matricial.
| Código | Nome do Estado | Função Principal | Ação de Saída / Comportamento | Condição de Transição |
|---|---|---|---|---|
000 |
IDLE | Estado inicial. Espera o comando para começar. | Lê op_code e matrix_size_reg, zera variáveis de controle. |
Pulso em start_pulse → LOAD_A |
001 |
LOAD_A | Carrega 200 bits da matriz A, bit a bit. | Armazena cada bit em Matrix_A com base em bit_pos e bit_val. |
Quando load_cnt == 199 → LOAD_B |
010 |
LOAD_B | Carrega 200 bits da matriz B. | Idêntico a A, mas escreve em Matrix_B. |
Quando load_cnt == 199 → EXEC_OP |
011 |
EXEC_OP | Executa a operação com base no op_code. |
Ativa módulo MpuOperations; conta 7 ciclos para garantir latência. |
Quando cycle_counter > 6 → READ_RES |
100 |
READ_RES | Exporta resultado da operação, byte a byte. | Gera data_out_reg, levanta flag_reg, aguarda ack_pulse. |
Se byte_idx == 24 → IDLE |
A FSM segue a seguinte cronologia linear com laços internos bem definidos:
IDLE ──start_pulse──▶ LOAD_A (200 ciclos)
└── (bit-a-bit com pio_out)
↓
LOAD_B (200 ciclos)
└── (bit-a-bit com pio_out)
↓
EXEC_OP (~7 ciclos)
└── (executa operação combinacional/sequencial)
↓
READ_RES (25 ciclos)
└── (handshake byte-a-byte com pio_in ↔ ack_pulse)
↓
IDLE ─── (espera novo start_pulse para nova operação)
A FSM é protegida contra comandos fora de ordem. Por exemplo:
- Se o host enviar um novo
start_pulseduranteREAD_RES, a FSM retorna para IDLE, abortando a operação — isso evita conflitos ou leituras truncadas. - Cada
ack_pulsesó é aceito se vier depois deflag_regser levantado, mantendo a integridade da troca de dados.
Três LEDs físicos da placa FPGA são utilizados como indicadores visuais de estado, úteis para debug e demonstrações:
| LED | Estado Ativado | Significado |
|---|---|---|
| 0 | LOAD_A |
"Carregando Matriz A" |
| 1 | LOAD_B |
"Carregando Matriz B" |
| 2 | READ_RES |
"Enviando Resultado para o Host" |
Estes LEDs acendem automaticamente quando a FSM entra no estado correspondente, ajudando a monitorar o fluxo em tempo real sem ferramentas de depuração sofisticadas.
-
Contadores internos: usados para controlar o número de bits recebidos e o número de ciclos de execução.
load_cnt: conta até 199 durante LOAD_A e LOAD_B.cycle_counter: conta até 6 durante EXEC_OP.byte_idx: percorre de 0 a 24 durante READ_RES.
-
Modularidade: cada estado tem lógica separada, permitindo fácil extensão ou modificação (ex: para adicionar novos estágios).
Durante o estágio final da execução – o estado READ_RES da FSM – o coprocessador envia os dados da matriz-resultado de volta ao host um byte por vez. Esse processo é cuidadosamente sincronizado através de um protocolo de handshake, que garante:
- Que o FPGA não sobrescreva dados antes que o host os leia.
- Que o host saiba exatamente quando um novo byte está pronto.
- Que os dois lados avancem em total sincronia, evitando perda de dados.
Durante o READ_RES, os seguintes sinais e registradores são utilizados:
| Sinal | Direção | Papel durante o Handshake |
|---|---|---|
Result[199:0] |
Interno | Vetor com os 200 bits da matriz de resultado. |
byte_idx |
Interno | Índice que controla qual byte está sendo enviado (0 a 24). |
data_out_reg |
Saída | Armazena o byte atual que será lido pelo host. |
flag_reg |
Saída | Quando 1, indica que data_out_reg está pronto para leitura. |
ack_pulse |
Entrada | Pulso do host confirmando que o byte foi lido. |
Ao entrar em READ_RES, a FSM fatiará o vetor de resultado em 25 blocos de 8 bits. Para cada byte:
data_out_reg <= Result[byte_idx*8 +: 8];
flag_reg <= 1; // Sinaliza ao host: "byte disponível"→ O host verifica o bit 31 de pio_in (flag_reg == 1) e sabe que há dado novo.
O host lê o valor de pio_in[7:0] (conteúdo de data_out_reg), armazena em sua RAM e então envia:
pio_out = (1 << 26); // ack_pulse = 1→ Esse pulso de confirmação é capturado pelo hardware por detecção de borda.
Após detectar o pulso, a FSM limpa o flag e avança para o próximo byte (ou retorna a IDLE):
flag_reg <= 0;
if (byte_idx == 24)
state <= IDLE; // todos os bytes enviados
else
byte_idx <= byte_idx + 1;Esse ciclo se repete 25 vezes, exatamente um por byte, até que todos os dados da matriz sejam entregues ao host.
Clock →
FPGA: flag_reg = 1 ─────────────┐
Host: lê dado ─────────┐ │
Host: ack_pulse = 1 │ │
FPGA: flag_reg ← 0 │ │
FPGA: byte_idx++ └──┘
Cada par flag_reg↑ → ack_pulse↑ representa uma iteração do handshake. Nenhum dado é sobrescrito sem confirmação.
A FSM está preparada para proteger a integridade dos dados:
- Start precoce? Se o host acionar
start_pulsefora de hora, a FSM reseta paraIDLE, abandonando a leitura e reiniciando a operação – evitando vazamentos ou dados fora de ordem. - Flag ignorado? Sem
ack_pulse, o FPGA não avança. Isso significa que se o host travar, a operação pausa, não falha silenciosamente. - Reenvio? O protocolo atual não prevê retransmissão, mas a estrutura permite fácil inclusão de CRC ou verificação de integridade.
Durante a operação do sistema, o coprocessador implementado na FPGA acende LEDs físicos para indicar em qual estado da FSM ele se encontra. Esses LEDs estão conectados diretamente aos pinos da placa DE1-SoC e são acionados com base no valor atual do estado, permitindo que qualquer pessoa — mesmo sem terminal ou console — acompanhe o fluxo da execução visualmente.
O uso de LEDs no projeto serve a três propósitos:
- Depuração prática: em caso de falhas ou travamentos, o LED aceso indica em qual fase o sistema parou.
- Confirmação de funcionamento: durante apresentações, ver os LEDs piscando conforme o esperado reforça que o sistema está operando corretamente.
- Diagnóstico de desempenho: variações no tempo de acendimento podem sugerir gargalos ou uso indevido do protocolo.
Cada LED é associado a um estado específico da FSM, sendo aceso apenas durante aquele estado. O comportamento é o seguinte:
| LED Físico | Estado FSM | Significado Visual |
|---|---|---|
| LED 0 | LOAD_A |
A matriz A está sendo carregada bit a bit |
| LED 1 | LOAD_B |
A matriz B está sendo carregada |
| LED 2 | READ_RES |
O sistema está transmitindo os resultados |
Durante os estados IDLE e EXEC_OP, nenhum LED é aceso — o que ajuda a detectar quando o sistema está parado ou em execução interna silenciosa.
Suponha que o usuário carregue duas matrizes e execute uma soma. O comportamento esperado dos LEDs seria:
- LED 0 acende → o sistema está no estado
LOAD_A, recebendo bits da Matriz A. - LED 1 acende → o sistema está no estado
LOAD_B, agora recebendo a Matriz B. - Nenhum LED (breve) → o estado
EXEC_OPé silencioso (apenas contadores internos rodam). - LED 2 acende → o sistema entra em
READ_RES, e inicia a transmissão dos bytes da matriz-resultado.
Ao final do ciclo (após 25 handshakes), todos os LEDs apagam ao retornar para IDLE.
A lógica de controle dos LEDs é extremamente simples e eficiente. Exemplo típico em Verilog:
assign LED[0] = (state == LOAD_A);
assign LED[1] = (state == LOAD_B);
assign LED[2] = (state == READ_RES);Essa associação direta evita lógica adicional ou registros dedicados — basta comparar o estado atual com os nomes simbólicos da FSM.
O driver foi concebido como uma biblioteca leve e modular, escrita principalmente em C99, com suporte a algumas rotinas de mais baixo nível em Assembly ARMv7. Sua função central é servir como ponte direta entre o programa do usuário e o coprocessador, encapsulando o acesso aos registradores pio_out e pio_in da FPGA.
- Abstrair o acesso à memória mapeada (
/dev/mem) - Construir comandos de 32 bits que o hardware entenda
- Sincronizar os estágios do processamento usando pulsos
- Enviar/receber dados bit a bit ou byte a byte
- Evitar que o programador lide diretamente com aritmética de ponteiros e volátil
A biblioteca é composta por:
laplace.h– cabeçalho principal com tipos, macros e protótiposlaplace.c– implementação em C das funções utilitárias- (opcional)
laplace_asm.s– trechos de rotinas críticas em Assembly
#define LW_BRIDGE_BASE 0xFF200000u
#define LW_BRIDGE_SPAN 0x00005000uEsses valores mapeiam a região de memória do Lightweight HPS–FPGA Bridge e são usados para que o programa C possa acessar diretamente os registradores da FPGA como se fossem posições de memória RAM.
#define DIM 5
#define N_BYTES (DIM * DIM) // 25 bytes
#define N_BITS (N_BYTES * 8) // 200 bitsEssas definições asseguram que a matriz usada esteja sempre no formato fixo 5×5, como exigido pela lógica do hardware.
| Struct | Função | Detalhes |
|---|---|---|
PIO |
Representa os registradores da FPGA (cmd, stat) |
Ponteiros volatile uint32_t*, usados para escrever e ler bits diretamente da FPGA |
Connection |
Controla a memória mapeada do bridge | Contém o file descriptor de /dev/mem e um ponteiro base para o mapeamento |
Instruction |
Guarda o comando a ser enviado, já estruturado em 32 bits | Campos opcode, matrix_size, basic_cmd com o valor pronto para pio_out |
O uso de
volatileemPIOé essencial para impedir que o compilador otimize acessos que podem mudar fora do controle da CPU.
| Função | Objetivo |
|---|---|
delay_1us |
Aguarda o número desejado de microssegundos (sincronismo fino) |
new_connection |
Abre /dev/mem, mapeia o bridge, retorna um Connection |
close_connection |
Fecha o arquivo e desfaz o mapeamento |
build_base_cmd |
Monta o comando base (32 bits) com opcode, matrix_size, bit_val, bit_pos |
next_stage |
Envia um start_pulse de 1 ciclo para avançar o estado na FSM |
store |
Envia uma matriz inteira, bit a bit, para o hardware (25×8 = 200 ciclos por chamada) |
load |
Lê os 25 bytes da matriz-resultado da FPGA e armazena no destino |
Observação: as funções acima tiveram o prefixo mpu_ removido por questões de legibilidade. Porém quando usá-las em C ou Assembly, o mesmo é necessário.
- Crie a conexão usando
new_connection, ouwith_connectiondo modulomain.c. - Mapeie os PIOs de acordo com o endereço base e o offset:
-
PinIO pins = { .cmd = bridge.connection.base + PIO_CMD_OFFSET, .stat = bridge.connection.base + PIO_STAT_OFFSET };
-
- Armazena a matriz A via
store - Armazena a matriz B via
store - Vai para próximo estágio via
next_stage - Carrega matrix resutado via
load - Fecha conexão via
close_connectionao final do programa.
O driver cuida de todos os detalhes da comunicação: carregamento das matrizes bit a bit, criação de pulsos, controle de estado e leitura sequencial do resultado.
A aplicação foi construída como uma ferramenta de terminal, permitindo que o usuário selecione operações, carregue arquivos e visualize resultados diretamente. Apesar de simples em aparência, ela carrega robustez interna por meio de modularização, validações sintáticas, uso de tipos seguros e integração limpa com o driver.
A aplicação é dividida em múltiplos arquivos-fonte, organizados por função:
| Arquivo | Função |
|---|---|
main.c |
Entrada principal do programa. Gera o loop de interação e executa comandos. |
parser.h |
Lê e interpreta os arquivos de entrada da mini-DSL (Laplace). |
ui.h |
Lida com interação com o terminal. Mensagens, menus e feedbacks. |
types.h |
Define os tipos auxiliares, enums de operações e seus nomes legíveis. |
Cada módulo é independente, facilitando manutenção, extensão e testes unitários.
O sistema utiliza uma linguagem de domínio específico (DSL) extremamente simples e didática para representar as entradas:
- Escritas em arquivos
.lp.a.lpeb.lprepresentam, respectivamente, as matrizes A e B.scalar.lprepresenta a entrada do escalar.
- Cada linha com colchetes:
[1 0 -3 8 7] - Linhas incompletas são preenchidas com zeros
- Até 5 linhas ⇒ matriz 5×5
- Comentários iniciados com
#
- Linha única:
scalar: -7 - Também permite comentários
-
O parser detecta erros de:
- Sintaxe inválida (faltando colchetes, valor fora do intervalo
int8_t, etc.) - Tamanho excessivo ou insuficiente
- Sintaxe inválida (faltando colchetes, valor fora do intervalo
-
Mensagens de erro indicam:
- Arquivo
- Linha
- Tipo de erro
- Exemplo de uso correto
No arquivo ui.h, as operações possíveis são descritas por um enum:
enum Operations {
Add = 0,
Sub = 1,
ScalarMult = 2,
Opposite = 3,
Transpose = 4,
Determinant = 5,
MatrixMult = 6,
Quit = 7,
InvalidOperation = 8
};Esses valores têm um mapeamento direto para strings (op_repr[]), facilitando:
- Exibição no menu
- Geração do
op_codeenviado para o coprocessador - Depuração e logs
const cstring op_repr[9] = {
[Add] = "Add",
[Sub] = "Sub",
[ScalarMult] = "Scalar multiplication",
[Opposite] = "Opposite",
[Transpose] = "Transpose",
[Determinant] = "Determinant",
[MatrixMult] = "Matrix multiplication",
[Quit] = "Quit",
[InvalidOperation] = "Invalid operation"
};Inspiradas na estrutura with do Python, essas macros facilitam o uso de arquivos e conexões sem esquecer de fechá-los:
#define with_open(file, mode, var)
#define with_connect(conn)Elas ajudam a evitar:
- Vazamento de recursos
- Deadlocks em arquivos
- Bugs silenciosos com conexões abertas
Para isso, é necessário utilizar de um for loop, paraa iniciar, verificar e fechar o acesso. Vale ressaltar que esse recurso só está disponível no C99.
1. Inicializa conexão com a FPGA
2. Mostra menu com operações disponíveis
3. Usuário escolhe operação (ex: soma, multiplicação, determinante)
4. Carrega arquivos `.lp` conforme necessário (A, B, escalar)
5. Constrói e envia a instrução para a FPGA
6. Envia matriz/matrizes bit a bit
7. Chama `next_stage()` para iniciar operação
8. Lê matriz-resultado byte a byte
9. Imprime resultado no terminal
10. Pergunta se o usuário deseja repetir ou sair
Durante esse fluxo, cada erro potencial é tratado com mensagens explicativas. Por exemplo:
[line: 2] Invalid syntax.
Em projetos embarcados, testar vai muito além de rodar uma função e esperar um valor: envolve sincronizar componentes físicos, verificar comportamento em tempo real e garantir integridade nos fluxos de dados. Neste projeto, diversas estratégias foram adotadas para validar cada camada — hardware, driver e aplicação.
A função mpu_init_default_matrix() preenche automaticamente uma matriz com dados determinísticos (ex: crescente de 1 a 25). Ela foi essencial durante a fase inicial, pois permitia:
- Testar toda a cadeia de envio sem depender de parser
- Verificar se o FPGA estava armazenando os dados corretamente
- Comparar o resultado com o esperado de forma visual
Exemplo da matriz gerada:
[ 1 2 3 4 5 ]
[ 6 7 8 9 10 ]
[ 11 12 13 14 15 ]
[ 16 17 18 19 20 ]
[ 21 22 23 24 25 ]Cada operação disponível na CLI foi testada com:
- Arquivos
.lpde entrada gerados manualmente - Cálculo esperado feito por programas externos (ex: Python/NumPy)
- Comparação exata byte a byte do resultado retornado pela FPGA
Testes como:
- Soma de matrizes conhecidas
- Produto por escalar
- Determinantes de matrizes 2×2 e 3×3
- Matrizes com valores máximos/mínimos possíveis de
int8_t
Foram todos validados.
Durante o estado READ_RES, os LEDs, flag_reg e byte_idx foram acompanhados em tempo real para garantir que:
flag_regsó era levantado após escrever um byte válido- O host só enviava
ack_pulsequando o byte era lido - O índice avançava corretamente até 24
Esse acompanhamento foi feito tanto visualmente (LEDs) quanto por logs no terminal.
Delays mínimos (delay_us()) foram ajustados para garantir que os pulsos start_pulse e ack_pulse fossem detectados corretamente — valores abaixo do ideal geravam falhas que podiam ser diagnosticadas e corrigidas facilmente.
O parser da DSL foi testado com dezenas de casos:
- Linhas incompletas
- Tipos fora do intervalo
- Erros de sintaxe
- Matrizes maiores que 5×5
Cada erro resultava em uma mensagem clara e amigável, sempre com:
- Número da linha
- Descrição do erro
- Exemplo de como corrigir
No nível da FPGA, foram usadas as seguintes estratégias:
- LEDs como marcadores de estado: ajudaram a identificar rapidamente travamentos e loops inesperados
Para validar a resiliência do sistema:
- Foi enviado um novo
start_pulseduranteREAD_RES→ FSM retornou paraIDLEcorretamente - Foi omitido o
ack_pulse→ sistema congelou no byte atual, como esperado - Foram testadas combinações inválidas de
op_code→ o coprocessador executou comportamento padrão ou zerado, sem travar
Para facilitar a compreensão geral e oferecer uma referência rápida, abaixo está a tabela consolidada de todos os sinais utilizados na comunicação entre o host (via driver em C) e o coprocessador (na FPGA).
| Campo | Bits | Direção | Largura | Função Principal |
|---|---|---|---|---|
start_pulse |
31 | IN | 1 bit | Inicia carregamento ou execução. É um pulso de 1 ciclo. |
matrix_size_reg |
30 | IN | 1 bit | Define o tamanho lógico da matriz (0 = 3×3, 1 = 2×2). Relevante para determinante. |
op_code |
29–27 | IN | 3 bits | Código da operação a ser executada (soma, multiplicação, determinante, etc.). |
ack_pulse |
26 | IN | 1 bit | Sinal do host dizendo “já li o byte, pode mandar o próximo”. |
| (reservado) | 25–9 | IN | 17 bits | Mantidos a zero. Reservado para expansões futuras. |
bit_pos |
8–1 | IN | 8 bits | Posição do bit (0–199) a ser escrito nas matrizes A ou B. |
bit_val |
0 | IN | 1 bit | Valor do bit (0 ou 1) a ser gravado na posição bit_pos. |
| Campo | Bits | Direção | Largura | Função Principal |
|---|---|---|---|---|
flag_reg |
31 | OUT | 1 bit | Quando 1, indica que data_out_reg está pronto para leitura. |
byte_idx |
30–26 | OUT | 5 bits | Índice do byte atual no vetor de resultado (0–24). |
| (constante) | 25–8 | OUT | 18 bits | Zeros fixos. Preenchimento para completar 32 bits. |
data_out_reg |
7–0 | OUT | 8 bits | Byte atual da matriz-resultado, transmitido ao host após a operação. |
| Nome | Tipo | Local | Função |
|---|---|---|---|
load_cnt |
reg [7:0] | FSM | Conta os bits recebidos durante LOAD_A e LOAD_B (até 199). |
cycle_counter |
reg [3:0] | FSM | Conta ciclos para aguardar conclusão da operação no estado EXEC_OP. |
byte_idx |
reg [4:0] | FSM | Índice atual de leitura na exportação de Result[199:0]. |
Result |
reg [199:0] | interno | Vetor que armazena a matriz final da operação. |
state |
reg [2:0] | FSM | Representa o estado atual da máquina de estados (IDLE, LOAD_A, etc.). |
LED[2:0] |
output wire | físico | LEDs que indicam os estados visíveis (LOAD_A, LOAD_B, READ_RES). |
| Registrador | Nome | Tipo | Papel Central |
|---|---|---|---|
pio_out |
Entrada | Host → FPGA | Controla o fluxo, escreve dados, envia pulsos de sincronismo. |
pio_in |
Saída | FPGA → Host | Entrega os resultados em blocos de 8 bits com sinalização por flag_reg. |
-
O uso de 32 bits em ambos os registradores permitiu embutir múltiplas informações compactadas: comandos, endereços, flags e dados.
-
Os bits reservados (25–9) foram deliberadamente mantidos como zero para:
- Prevenir glitches na lógica combinacional
- Preparar o sistema para futuras extensões, como flags de overflow, modos de ponto flutuante ou validações adicionais
-
A tabela ajuda também no uso como documentação técnica, sendo útil para qualquer novo desenvolvedor que precise compreender ou estender o sistema.
Este projeto demonstra como é possível construir um co-processador de matriz usando o DE1-SoC (FPGA Cyclone V), interligando software e hardware de forma direta, eficiente e educacionalmente rica. Ao integrar FPGA, C99, e Assembly, conseguimos controlar com precisão cada etapa — do envio de bits à exibição dos resultados — consolidando conhecimentos fundamentais em Sistemas Digitais, organização de computadores e engenharia de software embarcado.
Mais do que executar operações matemáticas, esta experiência mostrou o valor de arquiteturas bem projetadas, testes rigorosos e abstrações seguras — elementos essenciais para qualquer sistema crítico em tempo real. O código permanece modular e extensível, pronto para novas operações, modos de precisão ou até integração com interfaces gráficas.
