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 >>
Este post faz da série Ahmes. Leia também os outros posts da série:
NEWSLETTER

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

Obrigado! Sua inscrição foi um sucesso.

Ops, algo deu errado. Por favor tente novamente.

Licença Creative Commons Esta obra está licenciada com uma Licença Creative Commons Atribuição-CompartilhaIgual 4.0 Internacional.

Fábio Pereira
Técnico em eletrônica, advogado, pós-graduado em projetos eletrônicos, autor de 9 livros na área de programação de microcontroladores (1 em inglês), entusiasta de eletrônica e computação. Desenvolvimento em diversas plataformas de 8, 16 e 32 bits, desde microcontroladores até desktop e celulares, utilizando C, C#, Java, Pascal, PHP dentre outras. Curte rock'n roll e cerveja. Atualmente residindo em Kitchener, ON, Canada, é também mantenedor do Embedded Systems Blog em http://embeddedsystems.io

3
Deixe um comentário

avatar
 
2 Comment threads
1 Thread replies
0 Followers
 
Most reacted comment
Hottest comment thread
3 Comment authors
Andre CastroCaio AlonsoDiego Augusto Silva Recent comment authors
  Notificações  
recentes antigos mais votados
Notificar
Andre Castro
Visitante
Andre Castro

Um excelente artigo

Diego Augusto Silva
Visitante
Diego Augusto Silva

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
Visitante
Caio Alonso

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.