Você está na página 1de 23

UNIVERSIDADE FEDERAL DO AMAZONAS - UFAM

FACULDADE DE TECNOLOGIA - FT
DEPARTAMENTO DE ELETRÔNICA E COMPUTAÇÃO - DTEC

RELATÓRIO DE LABORATÓRIO

Carlos Gabriel
Fábio Arthur Soares
Juliano Salvattore
Nathália Colares

MANAUS
2021
Carlos Gabriel
Fábio Arthur Soares
Juliano Salvattore
Nathália Colares

RELATÓRIO DE LABORATÓRIO
PROJETO 2: PROCESSADOR PROGRAMÁVEL EM VHDL

Relatório apresentado ao curso de graduação em


Engenharia Elétrica da FACULDADE DE
TECNOLOGIA – UNIVERSIDADE FEDERAL DO
AMAZONAS (UFAM) para obtenção de nota parcial da
disciplina de Laboratório de Eletrônica Digital.

Professor: Francisco Januário.

MANAUS
2021
SUMÁRIO

2
1 Introdução 4

2 Desenvolvimento 5

3 Resultados 15

4 Conclusão 23

Referências Bibliográficas

1 Introdução

3
Como pode-se estudar durante a disciplina, o VHDL, que é uma linguagem de
descrição de hardware, é bastante utilizado tanto para síntese, simulação, especificação, e
propriedade intelectual. O objetivo deste segundo projeto, é descrever um processador
programável de 4 instruções na linguagem VHDL, e para tal, precisamos elucidar alguns
conceitos.
Um processador programável , que também é chamado de processador de propósito
geral, é um circuito digital que é responsável por armazenar uma memória. Alguns mais
conhecidos são:Atmel, Intel, ARM, Ryzen/AMD etc. Um processador contém dois blocos
principais: a unidade de controle (Control Unit) e o Caminho de dados (Datapath), como
pode-se observar na imagem abaixo:

Figura 1 - Organização do processador programável

Fonte: Embarcados(2015)

O bloco de controle é uma máquina de estados finitos ou FSM, que como sabemos,
possui entradas e saídas; estados; ações executadas por esses estados e transições decorrentes
da mudança desses estados. Como o nome sugere, o bloco de controle comanda: o caminho de
dados; memórias e outros dispositivos que possuem entrada e saída, as chamadas I/O’s. O
bloco de Caminho de dados, realiza prioritariamente o armazenamento dos dados, operações,
podendo ser: aritmética, multiplicação, rotação de bits e etc, e envolve tipos de dados que não
sejam booleanos.
2 Desenvolvimento

Para desenvolver o código com as instruções desejadas no código VHDL (de acordo
com a tabela abaixo), precisamos declarar e definir cada instrução, onde as mesmas serão
4
armazenadas na Memória de Instruções. A unidade de Controle, lê as instruções definidas e as
executa. Nesse mesmo bloco, temos:

➢ IR, o Registrador de Instruções - que vai guardar a próxima instrução que deve ser
executada. Por exemplo, a unidade de controle está executando a instrução: D[1] = 3 ,
o IR, então sabe que a próxima instrução a ser executada é a D[2] = 4.
➢ PC, o Contador de programa - um contador que guarda a localização da próxima
instrução que será executada.

Memória de Dados

Para a construção da memória de dados, RAM (Memória de acesso aleatório) em


inglês, de memória, enquanto que o registrador possui 16 bits. Como podemos ver abaixo:

Trecho 1 - Código da memória de dados em VHDL

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity data_mem is
port(clk: in std_logic;
D_addr: in std_logic_vector(7 downto 0);
D_rd, D_wr: in std_logic;
W_data: in std_logic_Vector(15 downto 0);
R_data: out std_logic_Vector(15 downto 0)
);
end data_mem;
architecture bhv_data_mem of data_mem is
signal i: integer;
type data_mem is array (0 to 255) of std_logic_vector (15
downto 0);
signal RAM: data_mem;
begin
process(D_addr, D_rd, D_wr, W_data)
begin
if (rising_edge(clk)) then
if (D_wr='1') then
RAM(to_integer(unsigned(D_addr))) <= W_data;
end if;
end if;
end process;
R_data <= RAM(to_integer(unsigned(D_addr))) when (D_rd='1')
else x"0000";
end bhv_data_mem;

Em entity definimos:

➢ W_data: Recebe a informação, ou seja, é a entrada para os dados;


➢ D_addr: Endereço de memória que neste caso possui 8 bits;
5
➢ D_rd: É o sinal que indicará que o valor que será lido quando D_rd = “1”;
➢ D_wr: É o sinal que indicará que o valor que será escrito quando D_wr = “1”;
➢ R_data: Faz a leitura da informação, ou seja, é a saída para os dados de 16
bits;

Como temos uma entrada vinda da memória de dados de 8 bits, que pode ser
observado em (7 downto 0) e uma saída de 16 bits (15 downto 0), significa que pode-se
guardar até 256 palavras. Já na parte de architecture convertemos o sinal de D_addr para
inteiro, além de pegar o valor de W_data e adicioná-lo na RAM, que é o data_mem, quando
tivermos uma subida no pulso de clock (clk) e o sinal de D_wr for igual a 1.

ULA

library ieee;
use ieee.std_logic_1164.all;
--use ieee.std_logic_signed.all;
use ieee.std_logic_unsigned.all;
entity alu is
port(a,b : in std_logic_vector(15 downto 0);
alu_s0 : in std_logic_vector(2 downto 0);
alu_result: out std_logic_vector(15 downto
0);
zero: out std_logic -- Flag que identificar
resultado zero
);
end alu;
architecture bhv_alu of alu is
signal result: std_logic_vector(15 downto 0);
begin
process(a, b, alu_s0)
begin
case alu_s0 is
when "001" => result <= a + b; -- operacao Soma
when "010" => result <= a - b; -- operacao Subtracao
when "011" => result <= a and b; -- operacao and
when "100" => result <= a or b; -- operacao OR
when "101" => result <= not (a); -- operacao NOT
when "110" => result <= not(a) + '1'; -- Operacao
Complemento2 = CMP1 + '1'
when others => result <= x"0000";
end case;
end process;
zero <= '1' when result = x"0000" else '0';
alu_result <= result;
end bhv_alu;

O código consiste em uma ULA de 16 bits, primeiramente foram importadas as


bibliotecas necessárias para a montagem. Então após isso são estabelecidas as entradas e
6
saídas do circuito, nele são definidas todas as entradas “a” e “b”, a função “alu_s0 “, que será
utilizada para definir se o circuito irá trabalhar em parte aritmética ou lógica e, por fim, a sua
saída “alu_result”. Após isso é definido todas as funções que a ULA vai executar.

Banco de Registradores

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity register_file is
port(clk,rst: in std_logic;
RF_W_wr: in std_logic; -- sinal de Ra escrita
RF_W_addr: in std_logic_vector(3 downto 0); -- endereco
de Ra
W_data: in std_logic_vector(15 downto 0); -- endereco de
entrada de dados do reg
RF_Rp_addr: in std_logic_vector(3 downto 0); -- endereco
de Rb
RF_Rp_rd: in std_logic; -- sinal de Rb leitura
Rp_data: out std_logic_vector(15 downto 0); -- endereco
de saida de dados do reg
RF_Rq_addr: in std_logic_vector(3 downto 0); -- endereco
de Rc
RF_Rq_rd: in std_logic; -- sinal de Rb leitura
Rq_data: out std_logic_vector(15 downto 0) -- endereco Rc
);
end register_file;
architecture bhv_register_file of register_file is
type reg_type is array (0 to 16) of std_logic_vector (15 downto
0);
signal reg_array: reg_type;
begin
process(clk,rst)
begin
if(rst='1') then
for i in 0 to 15 loop
reg_array(i) <= x"0000";
end loop;
elsif(rising_edge(clk)) then
if(RF_W_wr = '1') then
reg_array(to_integer(unsigned(RF_W_addr))) <=
W_data;
end if;
end if;
end process;
Rp_data <= reg_array(to_integer(unsigned(RF_Rp_addr))) when
RF_Rp_rd = '1' else x"0000";
Rq_data <= reg_array(to_integer(unsigned(RF_Rq_addr))) when
RF_Rq_rd = '1' else x"0000";
end bhv_register_file;

7
O código do Banco de Registradores estabelece os sinais de entrada “W_data” que será a
informação a ser armazenada, “RF_W_addr” o endereço onde ela será armazenada e
“RF_W_wr” que indicará que a informação será escrita. Assim como também insere os sinais
de saída “RF_Rq_rd” que indicará que a informação será lida, “RF_Rq_addr” que determina
o endereço de onde ela será lida e “Rq_data” que será a saída da informação.

Unidade de Controle

Como vimos sobre, nessa parte, que é a Unidade de Controle (Control Unit) é a
responsável por validar a leitura na Memória de Instruções e então executar as instruções. As
atividades são realizadas em três fases:

➢ Busca: Que carrega a próxima instrução no IR; O PC, que irá apresentar a
próxima instrução que deverá ser realizada; E o IR, que guarda a instrução que
deverá ser executada.
➢ Decodificação: Irá determinar em qual operação está sendo especificada na
instrução guardada.
➢ Execução: Esta irá alimentar os componentes que deverão ser utilizados da
ULA para a devida operação;

Trecho - Código da unidade de controle


library ieee;
use ieee.std_logic_1164.all;
entity control_unit is
port(reset: in std_logic;
ir: in std_logic_vector(15 downto 0);
D_rd: out std_logic;
D_wr: out std_logic;
RF_s: out std_logic_vector(1 downto 0);
RF_w_addr: out std_logic_vector(3 downto 0);
RF_w_wr: out std_logic;
RF_rp_addr: out std_logic_vector(3 downto 0);
RF_rp_rd: out std_logic;
RF_rq_addr: out std_logic_vector(3 downto 0);
RF_rq_rd: out std_logic;
alu_sl0: out std_logic_vector(2 downto 0)
);
end control_unit;

architecture bhv_control_unit of control_unit is


begin
process(reset)
variable opcode: std_logic_vector(3 downto 0);
begin

8
opcode := ir(15 downto 12);
if (reset = '1') then
D_rd <= '0';
D_wr <= '0';
RF_s <= "00";
RF_w_wr <= '0';
RF_rp_rd <= '0';
RF_rq_rd <= '0';
alu_sl0 <= "000";
else
case opcode is
when "0000" => -- MOV Ra, d
D_rd <= '1';
D_wr <= '0';
RF_s <= "00";
RF_w_addr <= ir(11 downto 8);
RF_w_wr <= '1';
RF_rp_rd <= '0';
RF_rq_rd <= '0';
alu_sl0 <= "000";
when "0001" => -- MOV d, Rb
D_rd <= '0';
D_wr <= '1';
RF_s <= "01";
RF_w_wr <= '0';
RF_rp_addr <= ir(11 downto 8);
RF_rp_rd <= '1';
RF_rq_rd <= '0';
alu_sl0 <= "000";
when "0010" => -- ADD Ra, Rb, Rc
D_rd <= '0';
D_wr <= '0';
RF_s <= "10";
RF_w_addr <= ir(11 downto 8);
RF_w_wr <= '1';
RF_rp_addr <= ir(7 downto 4);
RF_rp_rd <= '1';
RF_rq_addr <= ir(3 downto 0);
RF_rq_rd <= '1';
alu_sl0 <= "001";
when "0011" => -- MOV Ra, #c
D_rd <= '0';
D_wr <= '0';
RF_s <= "11";
RF_w_addr <= ir(11 downto 8);
RF_w_wr <= '1';
RF_rp_rd <= '0';
RF_rq_rd <= '0';
alu_sl0 <= "000";
when "0100" => -- SUB RA, Rb, RC
D_rd <= '0';
D_wr <= '0';
RF_s <= "10";
RF_w_addr <= ir(11 downto 8);
RF_w_wr <= '1';
RF_rp_addr <= ir(7 downto 4);
RF_rp_rd <= '1';
RF_rq_addr <= ir(3 downto 0);
RF_rq_rd <= '1';
alu_sl0 <= "010";
when "0101" => -- AND Ra, Rb, RC
9
D_rd <= '0';
D_wr <= '0';
RF_s <= "10";
RF_w_addr <= ir(11 downto 8);
RF_w_wr <= '1';
RF_rp_addr <= ir(7 downto 4);
RF_rp_rd <= '1';
RF_rq_addr <= ir(3 downto 0);
RF_rq_rd <= '1';
alu_sl0 <= "011";
when "0110" => -- OR Ra, Rb, RC
D_rd <= '0';
D_wr <= '0';
RF_s <= "10";
RF_w_addr <= ir(11 downto 8);
RF_w_wr <= '1';
RF_rp_addr <= ir(7 downto 4);
RF_rp_rd <= '1';
RF_rq_addr <= ir(3 downto 0);
RF_rq_rd <= '1';
alu_sl0 <= "100";
when "0111" => -- NOT Ra
D_rd <= '0';
D_wr <= '0';
RF_s <= "10";
RF_w_addr <= ir(11 downto 8);
RF_w_wr <= '1';
RF_rp_addr <= ir(7 downto 4);
RF_rp_rd <= '1';
RF_rq_addr <= "0000";
RF_rq_rd <= '0';
alu_sl0 <= "101";
when "1000" => -- CMP Ra
D_rd <= '0';
D_wr <= '0';
RF_s <= "10";
RF_w_addr <= ir(11 downto 8);
RF_w_wr <= '1';
RF_rp_addr <= ir(7 downto 4);
RF_rp_rd <= '1';
RF_rq_addr <= "0000";
RF_rq_rd <= '0';
alu_sl0 <= "110";
when others =>
D_rd <= '0';
D_wr <= '0';
RF_s <= "XX";
RF_w_wr <= '0';
RF_rp_rd <= '0';
RF_rq_rd <= '0';
alu_sl0 <= "000";
end case;
end if;
end process;
end bhv_control_unit;

Como podemos ver no código acima, a unidade de controle possui apenas uma
entrada, que neste caso é o IR, o qual possui 16 bits como mencionado anteriormente, e mais
10 saídas que variam entre 2 e 4 bits. Algumas das saídas presentes são:
10
➢ RF_w_addr: Mostra o endereço do multiplexador;
➢ RF_w_wr: Mostra a informação que será armazenada no registrador;
➢ RF_s: Irá selecionar o multiplexador correspondente;
➢ alu_s10: Irá mostrar qual operação será realizada pela ULA.

De acordo com o valor retornado pela variável opcode, que pega os primeiros 4 bits, ele é o
que vai especificar qual instrução deve ser feita. A primeira condicional (if-else) no código
verifica se o processador foi resetado, fazendo assim o opcode retornar o valor 1, porém
quando retorna o valor 0, é que entramos na segunda condicional, que irá verificar pra qual
instrução aquele valor deve ser realizado.

Memória de Instruções

library ieee;

use ieee.std_logic_1164.ALL;

use ieee.numeric_std.all;

entity instruction_mem is

port(pc: in std_logic_vector(3 downto 0);

instruction: out std_logic_vector(15 downto 0)

);

end instruction_mem;

architecture bhv_instruction_mem of instruction_mem is

type ROM_type is array(0 to 15) of std_logic_vector(15 downto 0);

constant rom_data: ROM_type:=("0011000000000010", -- MOV R0,#2

"0001000000000000", -- MOV 0,R0

"0011000100000011", -- MOV R1,#3

"0001000100000001", -- MOV 1,R1

"0000001000000000", -- MOV R2,0

"0000001100000001", -- MOV R3,1

"0010010000100011", -- ADD R4,R2,R3

"0001010000001001", -- MOV 9,R4

"0000000000000000",

"0000000000000000",

"0000000000000000",

11
"0000000000000000",

"0000000000000000",

"0000000000000000",

"0000000000000000",

"0000000000000000");

begin

instruction <= rom_data(to_integer(unsigned(pc))) when pc < "1000"


else x"0000";

end bhv_instruction_mem;

A Memória de Instruções possui dois sinais de controle : um sinal de entrada de 4


bits (PC), e uma saída de 16 bits (Instruction). Na parte da arquitetura é definido a constante
“rom_data” onde é atribuído todas as 16 instruções que são codificadas através de códigos de
máquina. Por fim, é atribuído ao código que somente os códigos de máquina válidos sejam
lidos pelo programa.

Processador

Aqui na parte do processador, o qual é o responsável por acrescentar o Contador de


Programa e executar as instruções, descrevemos todos os sinais. Além de fazer o mapeamento
para a unidade de controle e para a memória de instruções.

library ieee;
use ieee.std_logic_1164.all;
use ieee.std_logic_signed.all;
entity processador is
port(clk: in std_logic;
reset: in std_logic;
pc_out: out std_logic_vector(3 downto 0);
alu_res_out: out std_logic_vector(15 downto 0)
);
end processador;
architecture bhv_processador of processador is
signal pc_next: std_logic_vector(3 downto 0);
signal ir: std_logic_vector(15 downto 0);
signal data_rd: std_logic;
signal data_wr: std_logic;
signal reg_sel: std_logic_vector(1 downto 0);
signal reg_w_addr: std_logic_vector(3 downto 0);
signal reg_w_wr: std_logic;
signal reg_w_data: std_logic_vector(15 downto 0);
signal reg_rp_addr: std_logic_vector(3 downto 0);
signal reg_rp_rd: std_logic;
signal reg_rp_data: std_logic_vector(15 downto 0);
signal reg_rq_addr: std_logic_vector(3 downto 0);
signal reg_rq_rd: std_logic;
signal reg_rq_data: std_logic_vector(15 downto 0);
signal alu_sel: std_logic_vector(2 downto 0);
12
signal alu_out: std_logic_vector(15 downto 0);
signal zero_flag: std_logic;
signal read_data: std_logic_vector(15 downto 0);
begin
process(clk,reset)
begin
if (reset='1') then
pc_next <= "0000";
elsif rising_edge(clk) then
pc_next <= pc_next + '1';
-- Saidas
pc_out <= pc_next;
alu_res_out <= alu_out;
end if;
end process;
-- Instrucao (IR) da Memoria de Instrucoes
Instruction_Memory: entity work.instruction_mem port map(
pc=> pc_next,
instruction => ir
);
-- Sinais de Controle (Unidade de Controle)
Control: entity work.control_unit port map(
reset => reset,
ir => ir,
D_rd => data_rd,
D_wr => data_wr,
RF_s => reg_sel,
RF_w_addr => reg_w_addr,
RF_w_wr => reg_w_wr,
RF_rp_addr => reg_rp_addr,
RF_rp_rd => reg_rp_rd,
RF_rq_addr => reg_rq_addr,
RF_rq_rd => reg_rq_rd,
alu_sl0 => alu_sel
);
-- Multiplexador
process(reg_sel)
begin
if reg_sel = "00" then
reg_w_data <= read_data;
end if;
if reg_sel = "01" then
reg_w_data <= reg_rp_data;
end if;
if reg_sel = "10" then
reg_w_data <= alu_out;
end if;
if reg_sel = "11" then
reg_w_data <= "00000000" & ir(7 downto 0); --
constante
end if;
end process;
-- Register File
Reg_file: entity work.register_file port map(
clk => clk,
rst => reset,
RF_W_wr => reg_w_wr,
RF_W_addr => reg_w_addr,
W_data => reg_w_data,
RF_Rp_addr => reg_rp_addr,
RF_Rp_rd => reg_rp_rd,
13
Rp_data => reg_rp_data,
RF_Rq_addr => reg_rq_addr,
RF_Rq_rd => reg_rq_rd,
Rq_data => reg_rq_data
);
-- ULA
Alu_Process: entity work.alu port map (
a => reg_rp_data,
b => reg_rq_data,
alu_s0 => alu_sel,
alu_result => alu_out,
zero => zero_flag
);
-- Memoria de Dados
Data_Memory: entity work.data_mem port map(
clk => clk,
D_addr => ir(7 downto 0),
D_rd => data_rd,
D_wr => data_wr,
W_data => reg_w_data,
R_data => read_data
);
end bhv_processador;

Temos duas entradas clock e reset, e duas saídas pc_out, que exibe qual a última
posição do contador de programa. e a variável alu_res_out para apresentar o resultado de uma
operação. Caso o processador seja resetado ou seja, reset =’1’, então o contador do nosso
programa, declarado como pc_next é zerado, do contrário é lido o sinal de borda de subida do
clock, que incrementa +1 na variável pc_next.

3 Resultados

Realizadas todas as etapas descritas para o desenvolvimento do nosso processador


programável , agora iremos analisar e discutir os resultados obtidos durante a simulação da
mesmo, através de códigos em VHDL e Quartus no campo “Simulation Waveform”.

TESTE 1

14
TESTE 2

TESTE 3

15
TESTE 4

TESTE 5

16
TESTE 6

Resultado do Teste 01

Como é possível notar, após 13 ciclos de clock foi realizada a operação soma com
1001.

17
Resultado do Teste 01

Aqui para o segundo resultado do Teste 01, temos que após 14 ciclos de clock a
operação realizada foi a subtração do número 1010 para 0110.

Resultado do Teste 02

Já neste caso, temos a saída "0010", referente a operação lógica AND, que ocorre
após 10 ciclos com o número “0110”.

Resultado do Teste 02

Neste resultado, a saída foi "1111111111111101" após 11 ciclos, ou seja, a operação


lógica NOT foi executada no número 0111.
18
Resultado do Teste 02

No terceiro resultado do Teste 02, a saída foi "1111111111111101" após 12 ciclos, se


tratando da operação OR.

Resultado do Teste 03

Após 14 ciclos de clock, o resultado foi "0000000000000001", referente a operação


AND.

Resultado do Teste 03

19
Já neste caso, o número “0000000000000011” saiu como resultado após 13 ciclos, se tratando
da operação OR aplicada no número “1001”.

Resultado do Teste 04

Para o teste 4, que a cada 14 ciclos realiza a instrução de ADD que é a R3, R2 e R3,
retornou o valor “0000000000101010”.

Resultado do Teste 05

Para o teste 5, na instrução para a operação SUB, temos que R3, R2 e R3, retornaram
o valor “0000000000100011”.

20
Resultado do Teste 06

Para o primeiro resultado do teste 06, o número ‘0000000010010001” foi obtido após
12 ciclos de clock, o que indica o uso da operação lógica AND

Resultado do Teste 6

Para o teste 6, na parte da operação do complemento de 2, descrita como CMP, que


fará a operação entre R0 e R2, retornou o valor “1111111111011001”

Resultado do Teste 06

Ainda no teste 6, agora para a operação da instrução NOT, de R4 e R2, tal operação
retornou o valor “1111111111011000”.

21
4 Conclusão

Através do projeto realizado em um programa de dispositivo lógico programável


(Quartus II), obtivemos maior conhecimento sobre processadores programáveis. Em resumo,
os resultados que obtivemos no projeto atende às finalidades para as quais foi projetado, pois
os testes realizados nas tabelas e gráficos de simulação corresponderam dentro dos resultados
esperados de cada uma das instruções. Ao fim deste projeto, ficou nítido o quão importante e
imprescindível é o conhecimento sobre o funcionamento dos processadores programáveis,
pois são bastante utilizados para a execução das funcionalidades requisitadas por um sistema
em questão.

Referências Bibliográficas

22
FERRARI, Rafael. Processadores de Propósito Geral. Unicamp. Campinas, 2017. Disponível
em: <https://www.dca.fee.unicamp.br/~rferrari/EA075_2s2017/Cap.%203%20-
%20Processadores_Proposito_Geral.pdf > .Acesso em: 26 de nov. de 2021.

PEREIRA, Rodrigo. Processadores programáveis – Como projetar um processador em


verilog. Embarcados. 2015. Disponível em: https://www.embarcados.com.br/processadores -
programaveis-parte-2/. Acesso em: 26 de nov. de 2021.

23

Você também pode gostar