Este artigo demonstra a codificação em VHDL de uma CPU de 8 bits bastante simples. 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 atrás, 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 a execução de código sequencial!

Então, juntamente com o meu amigo e colega de curso Roberto 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:

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)

Tabela 1 – Conjunto de instruções do Ahmes

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.

ahmes1

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

Implementação VHDL

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 a 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:

ahmes_sim_vwf

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:

ahmes_sim_lda

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 material 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.

Ahmes – um processador simples de 8 bits em VHDL
Classificado como:                

Deixe uma resposta