A arquitetura de um processador, como a usada na placa raspberry pi4b, suporta um conjunto de instruções, chamado: armv8-a. Desse conjunto de instruções, existe um subconjunto chamado: legv8, que é mais otimizado para processamento de 64 bits. Usaremos esse subconjunto por ser um avanço na arquitetura dos processadores ARM.
Na figura a seguir podemos ver os blocos do processador usado na placa Raspberry Pi4B
Podemos destacar a capacidade de executar instruções para 32 e 64 bits. Nesta seção veremos o subconjunto LEGv8, que é mais otimizado para 64 bits. Observe a figura a seguir:
A arquitetura LEGv8 na placa Raspberry Pi4B é baseada em um subconjunto de instruções do conjunto de instruções: ARMv8-A. A figura a seguir mostra o simulador de instruções para este subconjunto (LEGv8):
No simulador podemos observar: o editor de instruções, o conjunto de registradores (X0, X27), os registradores de uso especial (SP, FP, LR, XZR), onde podemos ver os resultados das instruções e o caminho que os dados seguem (datapath). A figura a seguir mostra o diagrama da arquitetura LEGv8 da placa Raspberry Pi4B:
Vamos observar como esta arquitetura possui: Memória de Instrução, Contador de Programa, Registradores, Unidade Lógica Aritmética (ULA), Multiplexadores, Memória RAM, Lógica de Controle, Dados, Endereços, Instruções, etc. A figura a seguir mostra as partes mais importantes da arquitetura LEGv8:
Como visto no capítulo anterior, o processador Raspberry Pi4B possui um contador de programa (PC) para acessar a instrução a ser executada. Este contador é incrementado em 4 para cada busca ou ciclo de clock. Observe a seguinte imagem:
Assim, o contador de programa possui o endereço da "instrução" que está sendo executada. A memória de instrução só pode ser acessada pelo contador de programa. Isso garante o bom funcionamento das instruções de acesso e consulta. Embora existam alguns modos de endereçamento de memória, o contador de programa deve conter o endereço da instrução. Observe a imagem a seguir, onde está localizado o endereço da memória de instruções:
Escreva este bloco de código e vejamos o que acontece no caminho de dados:
Lembre-se que é necessário montar e executar as instruções, pressionando os botões mostrados na imagem a seguir:
Ao executar as instruções, o caminho dos dados, a partir do endereço "memória de instruções", segue o somador mostrado na imagem a seguir:
Isso se deve ao fato de cada instrução ter largura de 32 bits e por isso é necessário um incremento de 4 no somador, para encontrar o endereço da próxima instrução a ser executada pelo processador. Então, da memória de instruções, lemos as instruções a serem executadas. Assim, encontramos o conceito: “instrução”, conforme a imagem a seguir:
As instruções vão para a entrada de um "decodificador de instruções". Um decodificador, em eletrônica digital, é um circuito com várias entradas digitais e várias saídas digitais. De acordo com os valores colocados na entrada do decodificador, algumas saídas serão acionadas. Observe a seguinte imagem:
Como as instruções têm 32 bits de largura, é possível colocar determinados valores na instrução para selecionar determinadas partes do processador. Observe a imagem a seguir, por exemplo: bits [4-0] selecionam o registrador de destino (rd), bits [9-5] selecionam o registrador fonte 1 (rs1), bits [20-16] selecionam o registrador fonte 2 (rs2), os bits [31-21] contém o “OpCode” da instrução. O OpCode é a parte que identifica a instrução, sendo único para cada instrução.
A figura a seguir resume o trabalho do decodificador de instruções. Porém, deve-se levar em conta que, dependendo do tipo de instrução, os bits da instrução terão informações referentes a essa instrução. Por exemplo: para uma instrução de movimentação de registradores, a instrução indicará: o registrador fonte 1, o registrador fonte 2 e o registrador destino. Para uma instrução de salto, a instrução conterá o endereço para o qual a próxima instrução deve procurar.
Aqui, entra o conceito: “Registros”. A arquitetura LEGv8 possui 32 registradores. A figura a seguir mostra como os registradores são selecionados em uma instrução:
A figura a seguir mostra os registradores do processador:
Até agora, com um processador como o mostrado nas figuras anteriores, podemos carregar dados em registradores e mover dados entre registradores. É hora de adicionar lógica para fazer saltos incondicionais. Na figura a seguir, podemos ver como uma instrução (OpCode) pode ser utilizada para realizar saltos na memória de instruções:
Quando uma instrução de salto é executada, normalmente, é informado o número de instruções que precisam ser saltadas. É importante observar que os saltos podem ser para frente ou para trás. Quando os saltos são para frente, o número de instruções para pular é positivo. Quando o salto é para trás, o número de instruções para pular é negativo. Em ambos os casos é necessário multiplicar por 4. Isso pode ser feito movendo o número de instruções para pular para a esquerda. É necessário utilizar um multiplexador, para selecionar o endereço onde a próxima instrução será executada. Observe a figura a seguir:
Escreva essas instruções no editor e vejamos a instrução de salto incondicional:
Observe que quando as três primeiras instruções são executadas, o adicionador no topo do caminho de dados é usado, conforme mostrado na imagem a seguir:
Mas quando a instrução de salto incondicional é executada, o outro somador é usado para encontrar o endereço de memória da instrução. Observe isso na figura a seguir:
A instrução que o simulador está executando pode ser vista na parte inferior do diagrama do caminho de dados (datapath), conforme a figura a seguir:
Se uma “Unidade Lógica Aritmética” (ALU) for adicionada à saída de dados dos registradores, instruções desse tipo poderão ser executadas. Observe na figura a seguir, como foi colocada uma ALU, na saída dos registradores:
Quando a ALU executa alguma operação, é possível saber se o resultado foi: “Zero”. Com base nessas informações é possível realizar um salto condicional.
Escreva no editor de instruções, um programa como o seguinte:
O programa carrega o registrador X1 com o valor: 10, o registrador X2 com o valor: 10, então é executada a instrução de subtração (SUB), entre os registradores X2 e X1, deixando o resultado no registrador X2. Em seguida, é executada a instrução: "CBZ", que está verificando se o registrador X2 está com valor zero. Se isso for verdade, pule para a tag: “rep_sub”. Podemos ver que na primeira vez que a instrução de salto condicional é passada, a verificação é verdadeira, mas na segunda vez não é. Para executar o programa, pressione o botão: “Montar” e em seguida o botão: “Executar Instrução”. Observe no "datapath", como ao executar a instrução: "CBZ", pela primeira vez, a porta lógica AND tem suas 2 entradas em nível lógico alto:
A Unidade Lógica Aritmética, além de executar instruções deste tipo, também fornece informações sobre os resultados das operações. Ele faz isso através de alguns sinalizadores (flags), que possuem os seguintes nomes: N, Z, C, V.
O sinalizador: "N": indica se uma operação foi negativa.
A flag: “Z”: indica se uma operação deu Zero.
A bandeira: “C”: indica se uma operação carrega um “1” (Carry).
O flag: "V": indica se uma operação teve estouro (Overflop).
Observe na imagem a seguir, que podemos fazer saltos condicionais, verificando o valor desses flags:
A figura a seguir mostra os sinalizadores ALU no caminho de dados:
A tabela a seguir mostra os tipos de condições que podem ser verificadas nos flags da ALU:
Com o que vimos até agora, fica fácil entender como uma linguagem de alto nível como Python ou C++ pode interpretar código e convertê-lo em linguagem assembly. Vamos criar um programa Python simples:
O seguinte programa cria uma variável chamada: “k” e atribui a ela o valor: 10. Em seguida, cria uma variável: “j” e atribui o valor: 10. Em seguida, faz uma subtração (k-j), e o resultado é armazenado na variável: “r”. Em seguida, verifique se o resultado é igual a zero. Se for verdade, carregue o valor: 4, na variável: “r”. O programa Python ficaria assim:
Um interpretador usando o subconjunto de instruções LEGv8 pode gerar o seguinte programa assembly:
Assim, o interpretador Python, eu atribuo a:
X22, a variável: “k”
X23, a variável: "j"
X9, a variável: “r”
A instrução: "CBNZ", verifica se o valor na variável: "r", não é zero. Se for zero, então carregue o registrador: X9, com o valor: 4
Conclusão
A arquitetura de um processador é fácil de entender, à medida que você pratica com suas instruções. Para o caso do Raspberry Pi4B, usamos o subconjunto de instruções LEGv8. O simulador LEGv8 permite escrever instruções de montagem e ver seus resultados em registradores. Além disso, nos permite ver quais caminhos as instruções seguiram e os dados que elas manipulam. Nesta seção, consideramos os componentes mais importantes do processador. Muito pode ser explorado, mais a “arquitetura de um processador”, pode ser entendido e ter uma visão melhor da forma como foi construído.