3 Comentários

Ahmes: Uma CPU em VHDL

Ahmes num FPGA
Este post faz parte da série Ahmes. Leia também os outros posts da série:

Este artigo demonstra a codificação em VHDL de uma CPU de 8 bits bastante simples, a Ahmes. O código foi simulado na ferramenta Quartus e deve servir apenas como referência didática para compreender como uma CPU executa uma sequência de instruções.

Há cerca de nove anos, durante um curso de pós-graduação que frequentei no CEFET-SC, fui apresentado, na disciplina de arquitetura de computadores, às CPUs hipotéticas criadas na UFRGS (Neander, Ahmes, Ramses e Cesar) pelo professor Raul Fernando Weber.

Posteriormente, na disciplina de lógica programável, o nosso professor, Francisco Edson Nogueira de Melo, propôs que os alunos fizessem a implementação de circuitos ou aplicações práticas utilizando lógica programável e VHDL. Apesar de não ser o meu primeiro contato com VHDL (eu já havia participado de um mini-curso do grande Augusto Einsfeldt), eu nunca havia implementado nada em lógica programável.

Foi então que eu percebi a possibilidade de literalmente unir o útil ao agradável e realizar um antigo sonho: utilizando VHDL seria possível implementar uma CPU básica capaz de executar um conjunto de instruções e demonstrar os principais conceitos relacionados à execução de código sequencial!

Então, juntamente com o meu amigo e colega de curso Roberto do Amaral, decidimos implementar em VHDL uma das CPUs hipotéticas da UFRGS que já haviam sido objeto de estudos na disciplina de arquitetura de computadores. Escolhemos a máquina Ahmes por incluir um conjunto de instruções bastante funcional e uma arquitetura muito simples.

A Máquina Ahmes

O modelo de programação da máquina Ahmes é absolutamente enxuto: trata-se de uma arquitetura de 8 bits, com um conjunto de 24 instruções, três registradores e um único modo de endereçamento!

Dada a sua simplicidade, não existe implementação de uma pilha e o uso de sub-rotinas é prejudicado (apesar de ser possível limitadamente através de código auto-modificável), há também a limitação imposta pela capacidade máxima de endereçamento de 256 bytes de memória (num espaço único para memória de programa e de dados, seguindo uma arquitetura Von Neumann tradicional).

Dentre os registradores presentes no Ahmes encontramos: um acumulador de 8 bits (AC), um registrador de status com 5 bits (N-negativo, Z-zero, C-carry, B-borrow e V-overflow) e um contador de programa (PC) com 8 bits.

As 24 instruções reconhecidas pelo Ahmes são as seguintes:

Tabela 1 - Conjunto de instruções do Ahmes

Opcode binário

Mnemônico

Descrição

Comentário

0000 0000

NOP

nenhuma operação

nenhuma operação

0001 0000

STA end

MEM(end) ← AC

armazena o conteúdo do acumulador no endereço de memória especificado

0010 0000

LDA end

AC← MEM(end)

carrega o acumulador com conteúdo da memória

0011 0000

ADD end

AC← MEM(end) + AC

soma o acumulador com conteúdo da memória

0100 0000

OR end

AC← MEM(end) OR AC

operação 'ou' lógico

0101 0000

AND end

AC← MEM(end) AND AC

operação 'e' lógico

0110 0000

NOT

AC← NOT AC

complemento de um do acumulador

0111 0000

SUB end

AC← MEM(end) - AC

subtrai acumulador do conteúdo da memória

1000 0000

JMP end

PC ← end

desvio incondicional para o endereço

1001 0000

JN end

se N=1 então PC ← end

desvio condicional se negativo

1001 0100

JP end

se N=0 então PC ← end

desvio condicional se positivo

1001 1000

JV end

se V=1 então PC ← end

desvio condicional se houve estouro

1001 1100

JNV end

se V=0 então PC ← end

desvio condicional se não houve estouro

1010 0000

JZ end

se Z=1 então PC ← end

desvio condicional se zero

1010 0100

JNZ end

se Z=0 então PC ← end

desvio condicional se diferente de zero

1011 0000

JC end

se C=1 então PC ← end

desvio condicional se foi um

1011 0100

JNC end

se C=0 então PC ← end

desvio condicional se não foi um

1011 1000

JB end

se B=1 então PC ← end

desvio condicional se emprestou um

1011 1100

JNB end

se B=0 então PC ← end

desvio condicional se não emprestou um

1110 0000

SHR

C←AC(0); AC(i-1)←AC(i); AC(7)← 0

deslocamento para a direita

1110 0001

SHL

C←AC(7); AC(i)←AC(i-1); AC(0)←0

deslocamento para a esquerda

1110 0010

ROR

C←AC(0); AC(i-1)←AC(i); AC(7)←C

rotação para a direita

1110 0011

ROL

C←AC(7); AC(i)←AC(i-1); AC(0)←C

rotação para a esquerda

1111 0000

HLT

parada

termina a execução (aguarda um reset)

Em razão das especificações básicas não incluírem os tempos de execução das instruções, tomamos a liberdade de escolher uma implementação que fosse o mais simples possível (compatível com os nossos limitados conhecimentos sobre VHDL e lógica programável).

A figura 1 mostra o esquemático de alto nível do Ahmes. Podemos ver que existem três blocos: a CPU Ahmes propriamente dita, uma ULA (unidade lógica e aritmética) e um bloco de memória. Repare que o bloco de memória está presente apenas para fins de validação de simulação, numa implementação real ele seria substituído por memórias ROM/FLASH e RAM externas.

Diagrama em blocos de alto nível do Ahmes
Figura 1 - Diagrama em blocos de alto nível do Ahmes

Implementação VHDL do Ahmes

A implementação VHDL do Ahmes foi dividida em duas partes. A ULA (Unidade Lógica e Aritmética), responsável pelas operações lógicas e aritméticas da CPU, foi implementada em um bloco e código separados, permitindo que se testasse a mesma independentemente do restante da CPU.

O bloco da ULA é bastante simples: ela possui um barramento de 4 bits para seleção da operação desejada, dois barramentos de 8 bits para os operandos de entrada e um barramento de 8 bits para o resultado. Há também linhas de saída para os flags N, Z, C, B e V e uma linha adicional de entrada de transporte, utilizada nas operações de rotação de bits (ROR e ROL).

O código VHDL da mesma é igualmente simples, já que a ULA consiste basicamente num circuito lógico combinacional com poucas operações implementadas.

O código VHDL da CPU Ahmes é um pouco mais complexo e mais extenso, por isso, vamos explicar a implementação de apenas três instruções: uma de manipulação de dados, uma de aritmética (que faz uso da ULA) e outra de desvio.

A decodificação é baseada em uma máquina de estados que utiliza uma variável interna chamada CPU_STATE cujo estado é controlado/avançado por um evento de subida da linha de clock da CPU.

Note que nos dois primeiros estágios (ou clocks) da decodificação da instrução, a CPU precisa fazer o trabalho “braçal” de buscar o operando na memória e então carregá-lo num registrador interno chamado INSTR, que irá conter o opcode da instrução.

Somente no terceiro pulso de clock é que o processo de decodificação do opcode efetivamente tem início. No fragmento de código acima podemos identificar uma instrução NOP e o processamento relacionado a ela (que é nulo, resumindo-se simplesmente ao incremento do PC para apontar para a próxima instrução). Também podemos ver a parte inicial da decodificação de uma instrução STA. Neste caso, observamos que o barramento de endereços é carregado com PC+1, de forma que o operando da instrução possa ser lido e, em seguida, o estado da máquina avança para o primeiro estágio de decodificação da instrução (DECOD_STA1).

Neste momento é importante ressaltar que este código é resultado de uma primeira e despretensiosa implementação. Apesar de funcional, há uma série de alterações que podem ser feitas para tornar o processo de decodificação mais eficiente, uma delas seria fazer o incremento do PC já no segundo estágio de decodificação.

Prosseguindo com a decodificação da instrução STA, vejamos o restante do código VHDL relacionado à mesma:

No quarto estágio de decodificação o operando de entrada (endereço de memória onde será armazenado o conteúdo do acumulador) é lido e armazenado numa variável temporária (TEMP). Em seguida o mesmo é colocado no barramento de endereços (para selecionar o endereço de memória) e o acumulador é colocado no barramento de dados.

No sexto estágio o valor é escrito na memória e no último estágio (sétimo) a linha de escrita é desativada e a CPU retorna ao estado inicial de busca de instrução.

Vejamos agora a decodificação de uma instrução ADD. Os dois primeiros estágios são os mesmos já vistos antes. A diferenciação acontece no terceiro estágio, quando é lido o operando da instrução:

Os demais estágios prosseguem com a decodificação. Veja que no sexto estágio (DECOD_ADD3) é onde a “mágica” acontece: os operandos são carregados na ULA e a operação é selecionada (ADD). Em seguida a decodificação segue para um último estágio comum a várias instruções que é o DECOD_STORE, quando o resultado proveniente da ULA é armazenado no acumulador.

A última instrução que veremos é a JZ (não é o rapper) que é pula se zero. A decodificação da mesma é bastante simples, com os dois primeiros estágios iguais aos das demais instruções. A decodificação do opcode (terceiro estágio) implementa o seguinte código:

O estágio seguinte (quarto) é comum a todas as instruções de desvio e carrega o PC com o operando da instrução, o que faz o desvio propriamente dito:

O restante das instruções segue a mesma filosofia e acredito que o código VHDL amplamente comentado facilita o entendimento da operação do Ahmes.

Como já dito, a CPU Ahmes foi testada somente dentro do ambiente de simulação da ferramenta Quartus II da Altera. Para facilitar os testes, foi criada uma memória em VHDL que funciona como ROM e RAM ao mesmo tempo. Os endereços 0 a 24 são inicializados com um pequeno programa assembly que testa algumas funções do Ahmes, ao passo que os endereços 128 a 132 armazenam variáveis do programa. O código da mesma é o seguinte:

O código assembly armazenado na memória é o seguinte:

Ainda para fins de simulação, utilizamos um arquivo de formas de onda que inclui um sinal de clock e um pulso de reset:

Arquivo de formas de onda do  Ahmes
Figura 2 - Arquivo de formas de onda ahmes1.vwf

Após a compilação do projeto (ou síntese) o resultado é uma CPU que ocupa 222 células lógicas e 99 registradores, mais 82 células lógicas para a ULA, ou seja, um total de 284 células lógicas e 99 registradores, apenas 6,2% das células lógicas e 2,1% dos registradores disponíveis em um FPGA Altera Cyclone II EP2C5, o modelo utilizado na simulação aqui apresentada.

Note que foi utilizada a ferramenta Quartus II Web Edition versão 9.1sp2, uma vez que a última versão do Quartus (prime 16.0) apresentou alguns problemas de instalação, especialmente da ferramenta de simulação Modelsim (provavelmente algum conflito no meu notebook).

A seguir podemos observar uma parte do arquivo final resultante da simulação da operação do Ahmes. Ele mostra a sequência completa da instrução LDA 130:

Resultado da simulação do Ahmes no Quartus II
Figura 3 - Resultado da simulação do Ahmes no Quartus II

Conclusão

Este artigo pretendeu demonstrar que implementar uma CPU não é nenhuma tarefa absurda e, apesar de não termos implementado fisicamente o Ahmes num FPGA, toda a base para o entendimento de como opera um microprocessador está aqui.

Espero que este artigo possa inspirar outras pessoas a estudar a operação e implementação de CPUs em VHDL, pois, ao menos para mim, entender a operação e criar CPUs é algo absolutamente prazeroso e entusiasmante!

Todos os arquivos do Ahmes estão disponíveis para download na minha conta no GitHub.

Outros artigos da série

Implementação do Ahmes num FPGA Cyclone IV da Altera >>
Licença Creative Commons Esta obra está licenciada com uma Licença Creative Commons Atribuição-CompartilhaIgual 4.0 Internacional.

Receba os melhores conteúdos sobre sistemas eletrônicos embarcados, dicas, tutoriais e promoções.

Hardware » Ahmes: Uma CPU em VHDL
Comentários:
Notificações
Notificar
guest
3 Comentários
recentes
antigos mais votados
Inline Feedbacks
View all comments
Andre Castro
Andre Castro
07/08/2016 01:01

Um excelente artigo

Diego Augusto Silva
Diego Augusto Silva
21/07/2016 07:10

Excelente artigo ! Venho desenvolvendo alguns projetos em FPGA há algum tempo, mas apenas projetos de sistemas digitais simples e alguns projetos com o Nios II. Estava pensando há algum tempo em tentar desenvolver alguma CPU, mas sempre ficava com a ideia na cabeça de que seria algo muito complicado. Seu artigo me mostrou que é uma tarefa muito menos complexa do que eu imaginava, muito obrigado !!!

Caio Alonso
Caio Alonso
Reply to  Diego Augusto Silva
21/07/2016 20:26

Isso aí Diego! Se precisar de alguma ajuda ou quiser discutir algum pontos do seu projeto, entre em contato conosco. Sinta-se a vontade para escrever um artigo e compartilhar sua implementação no site.

Talvez você goste:

Séries



Outros da Série

Menu

WEBINAR
 

Soluções inteligentes para acionamento de MOSFETs/IGBTs com família STDRIVE

Data: 08/10 às 15:00h - Apoio: STMicroelectronics
 
INSCREVA-SE AGORA »



 
close-link