Curso de Assembly
Conhecendo o Processador Z80
Você está em: MarMSX >> Cursos >> Assembly Z80
O principal componente de um computador é a CPU. No MSX, a CPU é o processador Z80-A, da Zilog, com um clock de 3,58 MHz. O Z80-A é um processador de 8 bits, ou seja, é capaz de processar 8 bits de cada vez. O endereçamento de memória, por sua vez, é feito com 16 bits, resultando em 65536 posições de memória diferentes [1].
A imagem a seguir, apresenta a arquitetura do processador Z80 do MSX.
Repare no lado direito do esquema do processador acima, que há três setas grandes: a de dados (DATA BUS), de endereço (ADR BUS) e de controle (CON BUS). Elas representam as vias de comunicação entre o processador, memória principal e periféricos.
  Registradores
Os registradores são as memórias internas do processador Z80, que são extremamente rápidas e armazenam pequena quantidade de informações. Elas são utilizadas pelo processador para auxiliar na execução de programas e cálculos. Eles podem ser de 8 ou 16 bits.
A memória principal é uma memória localizada fora do processador, conectada através de um barramento de dados de 8 bits e outro de endereçamento de 16 bits. Ela possui no MSX o tamanho de 64 Kbytes (65536 bytes), variando de 0 a 65535 (0 a FFFF, em hexadecimal). O objetivo da memoria principal é armazenar os programas e os dados que serão processados pelo Z80. Por exemplo, no endereço de memória &H3CD2 do esquema abaixo, encontramos o valor &H2C.
O registrador possui velocidade de escrita/leitura maior que a memória principal. Dessa forma, os registradores deverão ser os preferidos para a realização de cálculos, sempre que possível. Para uma troca de valores de registrador para outro registrador são necessários 4 ciclos de clock, enquanto que de um registrador para a memória são necessários de 7 a 13 ciclos. Quanto menos ciclos de clock necessitar uma instrução, mais rápida será sua execução.
Os registradores do Z80:
A - O acumulador é o registrador responsável pelos resultados aritméticos, lógicos e comunicação externa através de INs e OUTs.
B - Registrador comum. Utilizado também como contador de laços de repetição, como na instrução DJNZ.
C - Registrador comum. Pode ser usado em par com o registrador B, se tornando um registrador de 16 bits.
D - Registrador comum.
E - Registrador comum. Pode ser utilizado em par com o registrador D, se tornando um registrador de 16 bits.
HL - Registrador de 16 bits.
IX - Registrador de índice. Permite a utilização de deslocamentos fixos para uma variável, como o valor "1" na expressão "A + 1". Ex: IX+1.
IY - Registrador de índice. Permite a utilização de deslocamentos fixos para uma variável, como o valor "1" na expressão "A + 1". Ex: IY+2.
F - Flag. São bits que indicam certas propriedades resultantes após uma operação aritmética.
PC - Esse registrador é um ponteiro para a memória principal que contém o local da próxima instrução a ser executada.
SP - Esse registrador é um ponteiro para a memória principal que contém o endereço do topo da pilha.
Registradores alternativos: A', B', C', D', E', F', H' e L'.
São cópias dos registradores A, B, C, D, E, F, H e L que ficam em segundo plano e não podem ser usados. Entretanto, utilizando as instruções EX e EXX, trocamos o conteúdo de pares dos registradores AF, BC, DE e HL pelos seus correspondentes AF', BC', DE' e HL'.
Através dessa troca nós:
- Temos acesso ao conteúdo dos registradores linha, que são passados para os registradores comuns.
- Armazenamos o valor dos registradores comuns, que são passados para os registradores linha.
Trocas possíveis:
EX AF,AF' faz AF ⇄ AF'
┌ BC ⇄ BC'
EXX faz ─┤ DE ⇄ DE'
└ HL ⇄ HL'
Exemplo:
Instante 0:
A 00
A' 00
Instante 1: LD A,&H20 ; Faz A = &H20
A 20
A' 00
Instante 2: EX AF,AF'
A 00
A' 20
Instante 3: LD A,&H45 ; Faz A = &H45
A 45
A' 20
Instante 4: EX AF,AF'
A 20
A' 45
O objetivo desses registradores é aumentar a memória interna do processador e também armazenar temporariamente dados de um processamento, para posterior uso.
A troca entre esses registradores é feita aos pares. Dessa forma, sempre que desejarmos utilizar esse recurso para um registrador, como por exemplo o registrador A, a troca de conteúdo EX/EXX afetará também o seu par correspondente, que nesse caso é o registrador F.
No caso da instrução EXX, ela afeta um grupo maior: BC, DE e HL. Assim, quando é feita a troca de conteúdo com EXX, troca-se o conteúdo desses 6 registradores com seus correspondentes linha.
Flags
Os flags indicam algumas propriedades resultantes de operações aritméticas. Esses resultados são representados pelos 8 bits do registrador F.
A configuração de bits desse registrador, bem como a descrição de cada propriedade é mostrada a seguir.
Bit | 7 6 5 4 3 2 1 0
------+--------------------------------- ← Registrador F
Flag | S Z - H - P N C
S - Sign - O resultado da operação é negativo (1) ou positivo (0).
Z - Zero - O resultado da operação é zero (1) ou não (0).
H - Half-Carry - Se 1, houve estouro no bit 3 (ou ficou devendo).
P - Parity/Overflow - Indica se número de bits 1 é par ou ímpar.
N - Subtraction - A última operação foi subtração (1) ou adição (0).
C - Carry - Se 1, houve estouro no bit 7 (ou ficou devendo).
Exemplos de operações que afetam o flag:
Primeiro caso:
00000000 Teste em Assembly:
- 00000001
---------- 10 ORG &HC000
11111111 20 LD A,0
30 LD B,1
7 6 5 4 3 2 1 0 40 SUB B
S Z - H - P N C 50 RET
1 0 1 1 1 0 1 1
Segundo caso:
00001111 Teste em Assembly:
+ 00000001
---------- 10 ORG &HC000
00010000 20 LD A,&HF
30 LD B,1
7 6 5 4 3 2 1 0 40 ADD A,B
S Z - H - P N C 50 RET
0 0 0 1 0 0 0 0
Terceiro caso:
11111111 Teste em Assembly:
+ 00000001
---------- 10 ORG &HC000
00000000 20 LD A,&HFF
30 LD B,1
7 6 5 4 3 2 1 0 40 ADD A,B
S Z - H - P N C 50 RET
0 1 0 1 0 0 0 1
Program Counter - PC e Stack Pointer - SP
Para a execução de uma instrução, o processador precisa saber em que lugar da memória principal está localizada a próxima instrução. O registrador PC contém esse endereço.
Cada instrução do Z80 possui um comprimento em bytes que varia de 1 a 4. A instrução de tamanho "n" executada consome todos bytes pertencentes à ela. Dessa forma, toda vez que uma instrução de comprimento de n bytes é executada, o registrador PC avança n bytes, em busca da próxima instrução.
Por exemplo, se uma dada instrução do Z80 possuir 2 bytes e o registrador PC apontar para o endereço de memória &HC000, após a execução desta instrução, o registrador PC irá apontar para a posição de memória &HC002.
Instrução: LD A,4
Código de máquina equivalente: &H3E &H04
End Código de máquina
C000 3E
C001 04
C002 XX ← Código da próxima instrução
Uma pilha de dados na memória principal é semelhante a uma pilha de livros sobre uma mesa. No caso dessa, um primeiro livro é colocado diretamente sobre a mesa, sendo esse o topo da pilha. O segundo é posto acima do primeiro livro e esse passa a ser o topo da pilha. O terceiro é posto sobre o segundo e esse passa a ser o topo da pilha. Quando retiramos um livro da pilha, retiramos o livro do topo, que no caso é o terceiro livro. Assim, o segundo livro passa a ser o topo da pilha. Por aí vai, até retirarmos o último livro da pilha, que no caso é o primeiro que foi colocado na mesa, tornando-se assim uma pilha vazia.
A seqüência de eventos de empilhamento e desempilhamento, narrada acima, é ilustrada a seguir.
L3
L2 L2 L2
L1 L1 L1 L1 L1
Mesa ---- ---- ---- ---- ---- ---- ----
Legenda:
L1: primeiro livro colocado
L2: segundo livro colocado
L3: terceiro livro colocado
: Topo da pilha
A convenção de listagem de memória em papel ou em tela é feita de forma crescente, de cima para baixo. No caso do Z80, que é capaz de endereçar 65536 posições, tem-se:
Endereço
--------
0000 |
0001 |
0002 |
0003 |
... |
FFFE |
FFFF V
O empilhamento é feito no sentido decrescente da memória. A partir da convenção apresentada, temos que o empilhamento na memória é feito no sentido de baixo para cima. Entretanto, quando o valor for "estourado", isto é, uma posição de memória menor que &H0000, retorna-se ao valor &HFFFF.
De forma a auxiliar o programador a armazenar dados na memória, o registrador SP contém um ponteiro para a posição de memória que indica o topo de uma pilha.
SP=&HF000
...
EFFD ^
EFFE | Sentido da pilha
EFFF |
F000 <--------- topo
F001
...
A instrução PUSH reg, empilha dados contidos no registrador reg na próxima posição de memória acima do topo. Já a instrução POP reg, recupera o dado contido no topo da pilha para o registrador reg.
Obs: reg é necessariamente um registrador de 16 bits (AF, BC, DE, HL).
Exemplo prático: suponha que temos a seqüência de bytes 01 02 03 04 05 06. Se o SP estiver apontando para a posição &H1000, ao empilharmos byte a byte, os dados ficariam assim:
End. Dado
-----------
0FFA 06
0FFB 05
0FFC 04
0FFD 03
0FFE 02
0FFF 01
1000 xx
Obs: xx significa "lixo" da memória.
Ao final da operação, o registrador SP irá apontar para a posição de memória &HFFA, que é o valor do topo da pilha. Assim, quando houver desempilhamento, o valor 06 é retornado.
Exemplo com programa Assembly:
BC = 0102 (B=01 e C=02)
SP = 1000
PUSH BC faz: (SP-2)=C, (SP-1)=B e SP=SP-2
0FFE 02
0FFF 01
1000 xx
Código em linguagem de máquina e Mnemônicos
As instruções passadas ao processador são códigos numéricos, no qual é extremamente difiícil para um programador entendê-los ou memorizá-los. Por exemplo, o seguinte trecho é um código em linguagem de máquina (valores em hexadecimal):
3E FF 06 01 80 C9
De modo a facilitar a vida do programador de Assembly, foram criadas certas palavras que representassem os códigos em linguagem de máquina, tornando-se mais compreensíveis para o Homem. A essas palavras que representam os códigos de máquina, chamamos de Mnemônicos.
O programa anterior, ao ser reescrito utilizando os Mnemônicos, torna-se agora legível para nós:
LD A,&HFF
LD B,1
ADD A,B
RET
Observe que fica muito mais fácil de entender o que o programa faz:
- Carrega (LD = load) o valor &HFF no registrador A.
- Carrega o valor 1 no registrador B.
- Soma (ADD) o conteúdo do registrador B com o registrador A.
- Por fim, devolve o controle à rotina que chamou o programa (RET = return).
É necessário converter o código em Mnemônicos para o Assembly, pois o processador só entende os códigos numéricos. O utilitário que realiza essa conversão é chamado de Assembler.
Assembler / Disassembler
Assembler (ou Assemblador) é um programa que converte os mnemônicos para código de máquina. Por exemplo, o programa fonte (Mnemônicos) a seguir será convertido para linguagem de máquina no Macro-Assemblador RSCII, que será visto em detalhes no próximo capítulo.
10 ORG &HC000
20 LD A,B
Obs: o comando ORG define o endereço inicial do programa.
Foi digitado EN e teclado "Enter", para assemblar o código:
EN ↵
Número de Opción: 3 ↵
Modo (H/D): H ↵
Resultado:
End Cód.Maq. Linha Mnemônico
-------------------------------------------
C000 10 ORG &HC000
C000 78 20 LD A,B
Assim, o código de máquina para a instrução LD A,B é 78 (hexadecimal).
O sentido inverso do Assembler, ou seja, converter código de máquina para mnemônicos, é chamado de Disassembler.
No RSCII, o comando DE seguido do endereço inicial (ou endereço inicial mais endereço final) desassembla o código.
DE &HC000 ↵
Modo (H/D): H ↵
DEFB: ↵
C000 78 LD A,B
Ao adicionar a opção "G" no final do comando DE, o código mnemônico é salvo em disco. Para isso, o programa pedirá um nome.
DE &HC000,G ↵
Modo (H/D): H ↵
Nombre: "prog1.asm" ↵
O arquivo será salvo no formato de listagem do RSCII e poderá ser carregado através do comando CT.
O Disassembler para uma faixa de endereços é feito da seguinte maneira: suponha que nosso programa em Assembly esteja locado da posição de memória &HC000 até &HC00A. O comando DE para salvar nosso código será:
DE &HC000-&HC00A ↵
ou
DE &HC000-&HC00A,G ↵
Obs: a listagem numérica de um programa com mnemônicos similar ao Basic, é convenção do Macro-Assemblador RSCII.
Fluxo de execução dos programas
Conforme dito anteriormente, o registrador PC contém o endereço da próxima instrução a ser executada. Uma instrução é executada por vez, onde cada instrução pode ter de um a quatro bytes.
O fluxo de execução é sempre progressivo na memória, executando instruções em seqüência, salvo quando encontrados desvios.
Quando uma instrução é executada, o registrador PC avança N bytes, de acordo com o tamanho de cada instrução.
A seguir, será apresentado um programa onde o fluxo de leitura de instruções é ilustrado.
End Cód.Maq. Linha Mnemônico
-------------------------------------------
C000 10 ORG &HC000
C000 78 20 LD A,B
C001 3E04 30 LD A,4
C003 DD340A 40 INC (IX+10)
C006 DDCB0A1E 50 RR (IX+10)
O fluxo de execução, instrução a instrução, é o seguinte:
Ordem Antes Instrução Executada Depois
1 PC=C000 LD A,B PC=C001
2 PC=C001 LD A,4 PC=C003
3 PC=C003 INC (IX+10) PC=C006
4 PC=C006 RR (IX+10) PC=C00A
O fluxo segue sempre adiante, salvo quando encontrar desvios. Os desvios serão abordados no capítulo 6.
Para simular esse programa no RSCII, use o comando SI (simular).
SI &HC000 ↵
Modo (H/D): H ↵
Principais comandos:
Teclar E para executar cada instrução.
Teclar M, caso deseje alterar um dos registradores:
A, B, C, D, E, F, H, L, IX ou IY.
Tecle "enter" para validar as modificações nos registradores, em cada linha
(a primeira linha são os registradores normais, a segunda os alternativos).
A linguagem Assembly é bem mais complexa do que as linguagens de alto nível. Portanto, para começar a entendê-la bem, é necessário:
- Se ambientar bem com o simulador RSC II.
- Começar pelos programas de exemplo fornecidos nesse curso.
- Executar o passo a passo e observar o comportamento dos registradores, flags e memória.
- Testar pequenas mudanças, sejam elas pela alteração do valor dos registradores ou memória, bem como pela alteração do código fonte.
- Procurar por entender as instruções do Z80, através de pequenos programas de teste.
No capítulo 6 serão apresentadas as principais instruções do Z80, divididas por tipo de operação.
Conforme dito anteriormente, pequenos programas podem ser feitos para testar e comparar os efeitos das instruções do Z80.
Referências:
[1] - Aprofundando-se no MSX; Piazzi, Maldonado, Oliveira; Ed. Aleph, 1987.