Curso Fudeba de ASM |
Originalmente eu havia pensado em dar um direcionamento à este curso, mas ao
longo do último ano (já faz mais de um ano desde a última aula) resolvi mudar
algumas coisas.
Percebi que a lógica principal do ASM já foi indicada nas 4 primeiras aulas e
que, se eu ficasse introduzindo comandos para otimizar apenas, grande parte do
interesse no curso seria prejudicado... já que a maioria das pessoas pretende
fazer coisas mais "divertidas" do que simplesmente otimizando um programa que
imprime textos na tela.
Desta forma, preparei um novo direcionamento que vai explicando como usar
características do VDP, PSG, FM, Z80 e PPI para obter coisas interessantes, ao
mesmo tempo em que vou introduzindo outros comandos do próprio Z80, bem como
mais detalhes de sua estrutura e da estrutura do MSX em si.
Quem ficar muito curioso com os registradores e comandos existentes do Z80,
indico o livro do Rossini e Figueredo que pode ser encontrado na página do MSX
Land (
http://www.geocities.com/msxland/msxlivros.html ), que é a minha principal
fonte de referência para as intruções do Z80.
Para esta aula estava previsto falar de duas coisas interessantes, contornando
as limitações matemáticas do Z80: vamos aprender a multiplicar e dividir com o
Z80.
Entretanto, para conseguir trabalhar com números, precisamos primeiramente
aprender a imprimir números, e posteriormente operar com eles... Assim, resolvi
falar primeiramente em como imprimir números, para que numa aula posterior
possamos fazer conta com eles e ver os resultados destas contas.
Sabemos que o Z80 trabalha e faz contas com números em binário. Entretanto,
uma forma mais simples de nós, humanos, trabalharmos com esses números, que têm
correspondência direta com o binário, é usando a notação hexadecimal (base 16).
Além disso, o jeito mais fácil de mostrar respostas numéricas é exatamente em
hexadecimal.
Vamos criar uma função chamada HEX que converte um numero de 8 bits em uma
string terminada por $ do número, e HEXW que faz o mesmo, mas com um numero de
16 bits.
A entrada para estas funcões devem ser:
1) o número
2) o endereço onde a string será colocada
Vamos trabalhar primeiro com a HEX, já que como veremos em seguida, a HEXW
usará a função HEX.
Quando pensamos em converter formatos, a primeira coisa que precisamos ter em
mente é que temos de conhecer muito bem tanto a organização inicial como a
organização de saída, para que não façamos bobagem com os dados. Assim, pensemos
no que de fato é um número de 8 bits...
Um número de 8 bits é um número composto por 8 casas binárias, o que significa
que ele pode variar de 00000000b a 11111111b. Essa progressão se dá da seguinte
forma:
00000000b = 000d = 00h 00000001b = 001d = 01h 00000010b = 002d = 02h 00000011b = 003d = 03h 00000100b = 004d = 04h 00000101b = 005d = 05h ... 11111010b = 250d = FAh 11111011b = 251d = FBh 11111100b = 252d = FCh 11111101b = 253d = FDh 11111110b = 254d = FEh 11111111b = 255d = FFh
Acima é possível ver alguns números em binário e sua representação binária,
decimal e hexadecimal. Deixemos de lado os números decimais (não nos interessam
por enquanto) e trabalhemos apenas com os números binários e hexadecimais.
Como os números hexadecimais tem unidades de 0 a F, podemos dizer que cada
unidade hexadecimal pode ser escrita com apenas 4 bits, segundo a
correspondência abaixo:
0000b = 0h 0001b = 1h 0010b = 2h 0011b = 3h 0100b = 4h 0101b = 5h 0110b = 6h 0111b = 7h 1000b = 8h 1001b = 9h 1010b = Ah 1011b = Bh 1100b = Ch 1101b = Dh 1110b = Eh 1111b = Fh
Podemos reescrever a seqüência original de uma forma mais adequada à visualização do processo de transformação de binário para hexadecimal:
0000:0000b = 00h 0000:0001b = 01h 0000:0010b = 02h 0000:0011b = 03h 0000:0100b = 04h 0000:0101b = 05h ... 1111:1010b = FAh 1111:1011b = FBh 1111:1100b = FCh 1111:1101b = FDh 1111:1110b = FEh 1111:1111b = FFh
Esses "blocos" de 4 bits foi dado o nome de "nibble". Assim, um número de 8
bits é composto de dois nibbles, um nibble alto (o mais da esquerda), e um
nibble baixo (o mais da direita). Assim, dizemos que existe uma correspondência
direta entre o binário e o hexadecimal, pois o nibble alto (esquerdo) pode ser
facilmente traduzido para o dígito hexadecimal correspondente (o da esquerda) e
o mesmo valendo para o nibble baixo (direito) correspondendo ao dígito direito
do hexadecimal.
Assim, a estratégia de conversão de um número de 8 bits em um "texto"
hexadecimal é separar o número de 8 bits em dois nibbles e usar o valor do
nibble para encontrar o dígito hexadecimal que ele representa.
Vamos tentar fazer isso com um código... inicialmente um código que não faz
nada disso, será apenas o programa base que se utilizará de nossa função.
--- Cortar Aqui --- BDOS EQU 5 STROUT EQU 9 START: ; Mostra informacoes do programa LD DE,NOMEDOPRG ; Indica texto do nome do programa CALL MOSTRATXT ; Mostra texto LD DE,AUTOR ; Indica texto do nome do autor CALL MOSTRATXT ; Mostra texto JP 0 ; Volta ao MSX-DOS ;------ ; MOSTRATXT - Funcao que mostra um texto cuja sequencia e' terminada por '$'. ; Entrada: DE - Aponta para sequencia a ser mostrada, terminada por '$' ;------ MOSTRATXT: LD C,STROUT ; Indica funcao de mostrar texto do BDOS CALL BDOS ; Manda o BDOS executar. RET ; Retorna NOMEDOPRG: DB 'Programa 5a - Mostrando Números em Hexadecimal',13,10,'$' AUTOR: DB ' Por Daniel Caetano',13,10,10,'$' END --- Cortar Aqui ---
A primeira coisa é criar uma função deste tipo:
;------ ; HEX - Converte um número na RAM para um texto hex terminado por '$'. ; Entrada: DE - Posição de memória onde o texto será colocado (3 bytes) ; A - Valor a ser "convertido" ;------ HEX: RET
Vamos passo a passo agora. Como primeiro valor do texto deve ser o esquerdo
(pois escrevemos os números da esquerda para a direita), o nibble mais alto deve
ser o primeiro a ser considerado.
Isso pode ser facilmente obtido, usando uma instrução do Z80 chamada SRL
(Shift Right and cLear), que faz exatamente deslocar todos os bits do
registrador e limpar o bit mais significativo. Aplicando quatro vezes seguidas
SRL no registrador A, teremos o efeito de obter diretamente o valor do nibble
superior. Vamos ver como isso funciona... com o número 111d, por exemplo, que
pode ser escrito como 01101111b ou 6Fh. Assim, o nibble alto que queremos isolar
é o valor 0110b, ou seja, 6h
Instrução Registrador A (binário) Registrador A (Hexa) LD A,111d 01101111b 6Fh SRL A 00110111b 37h SRL A 00011011b 1Bh SRL A 00001101b 0Dh SRL A 00000110b 06h
Opa! Então, após quatro chamadas à instrução SRL A, o valor do registrador
A irá nos indicar o valor correto do valor esquerdo do número em hexadecimal!
Queremos colocar isso em DE, certo? Então a nossa função fica assim:
;------ ; HEX - Converte um número na RAM para um texto hex terminado por '$'. ; Entrada: DE - Posição de memória onde o texto será colocado (3 bytes) ; A - Valor a ser "convertido" ;------ HEX: SRL A ; Pega valor do nibble superior SRL A SRL A SRL A LD DE,A RET
Correto? Bem, na verdade não. Primeiramente porque a instrução LD DE,A é
ilegal, e é fácil entender o porquê. O Z80 não sabe como atribuir o valor de um
registrador de 8 bits (A) a um registrador de 16 bits (DE), mesmo sabendo que DE
é a união de dois registradores de 8 bits (D e E).
O jeito "correto" de fazer isso seria:
LD D,0 LD E,A
Entretanto, observe que fazendo isso não estamos colocando o valor de A na
memória, e sim escrevendo ele no registrador DE (e por conseqüência apagando o
valor original, que era exatamente o endereço de memória onde
queríamos registrar a resposta).
Para indicar para o Z80 que queremos colocar o valor de A na posição de
memória que nos foi indicada por DE, devemos mais uma vez recorrer
ao parêntesis. Assim, a forma correta da instrução é:
LD (DE),A
Que é uma forma perfeitamente legal, já que ele coloca um número de 8 bits numa posição de memória (a que tem o nome indicado por DE), que também é de 8 bits. Nossa função então fica assim:
;------ ; HEX - Converte um número na RAM para um texto hex terminado por '$'. ; Entrada: DE - Posição de memória onde o texto será colocado (3 bytes) ; A - Valor a ser "convertido" ;------ HEX: SRL A ; Pega valor do nibble superior SRL A SRL A SRL A LD (DE),A ; Guarda na memória RET
Estaria ótima, SE não fosse um pequeno problema: nós jogamos fora o nibble inferior (direito) de nosso número, e precisamos dele agora! A solução para isso é guardar o valor de entrada, temporariamente, em algum outro lugar... no registrador B, por exemplo, e depois recuperando-o em A. A função fica assim:
;------ ; HEX - Converte um número na RAM para um texto hex terminado por '$'. ; Entrada: DE - Posição de memória onde o texto será colocado (3 bytes) ; A - Valor a ser "convertido" ;------ HEX: LD B,A ; Salva valor original SRL A ; Pega valor do nibble superior SRL A SRL A SRL A LD (DE),A ; Guarda na memória LD A,B ; Recupera valor numérico original RET
E novamente temos o valor no registrador A... mas ele está novamente com o
número original... de onde precisamos tirar o dígito "direito" do nosso número
em hexadecimal. Isso é mais fácil, pois basta mandarmos o Z80 zerar os dígitos
superiores, aplicando uma máscara lógica.
Mas que raios é uma máscara lógica? Uma máscara lógica é uma operação em que
podemos indicar quais são os bits que desejamos que sejam mantidos e todos os
outros serão zerados. A instrução que usamos para aplicar máscaras lógicas é a
instrução AND. A instrução AND opera sobre o Acumulador (registrador A), sendo
que o parâmetro passado para ela indica quais são os bits do registrador A que
serão preservados.
Vejamos isso numericamente:
Operação Registrador A (binário): LD A,01101110b 01101110b AND 11000011b 01000010b
Onde o valor de A (01101110b) é o valor original, e o parâmetro do AND
(11000011b) é a máscara.
Nossa... que coisa bizarra, não entendi o que aconteceu... \^= Vamos então ver
por um outro ângulo:
Bit 76543210 A 01101110 AND 11000011 ---------- 01000010
A instrução AND opera da seguinte forma, bit a bit: se ambos os bits (do número original e o da máscara) de uma dada posição forem 1, o resultado é 1. Em qualquer outro caso, a saída é zero. Trata-se da operação "E" booleana, onde a saída será "verdadeiro" apenas se a primeira *E* a segunda entrada forem verdadeiras:
1 AND 1 = 1 1 AND 0 = 0 0 AND 1 = 0 0 AND 0 = 0
Assim, a operação acima pode ser escrita como:
Bit 7 6 5 4 3 2 1 0 A 0 1 1 0 1 1 1 0 A A A A A A A A N N N N N N N N D D D D D D D D AND 1 1 0 0 0 0 1 1 --- --- --- --- --- --- --- --- 0 1 0 0 0 0 1 0
Assim, fica fácil perceber que se queremos preservar os quatro bits
da direita (nibble inferior), devemos usar uma máscara do tipo 00001111b.
Ou seja, se já possuímos o valor correto em A, basta usar a instrução
AND 00001111b
Para isolar o valor que desejamos no registrador A.
Instrução Registrador A (binário) Registrador A (Hexa) LD A,111d 01101111b 6Fh AND 00001111b 00001111b 0Fh
E pronto! Já temos o segundo byte do nosso texto em hexadecimal! Incorporando isso na função, teremos:
;------ ; HEX - Converte um número na RAM para um texto hex terminado por '$'. ; Entrada: DE - Posição de memória onde o texto será colocado (3 bytes) ; A - Valor a ser "convertido" ;------ HEX: LD B,A ; Salva valor original SRL A ; Pega valor do nibble superior SRL A SRL A SRL A LD (DE),A ; Guarda na memória LD A,B ; Recupera valor numérico original AND 00001111b ; Aplica máscara (isola nibble inferior) RET
Falta agora apenas colocá-lo na memória...
;------ ; HEX - Converte um número na RAM para um texto hex terminado por '$'. ; Entrada: DE - Posição de memória onde o texto será colocado (3 bytes) ; A - Valor a ser "convertido" ;------ HEX: LD B,A ; Salva valor original SRL A ; Pega valor do nibble superior SRL A SRL A SRL A LD (DE),A ; Guarda na memória LD A,B ; Recupera valor numérico original AND 00001111b ; Aplica máscara (isola nibble inferior) LD (DE),A RET
E indicar o fim do texto, colocando o '$' na memória, o que pode ser facilmente feito da seguinte forma:
LD A,'$' LD (DE),A
Que inserido na função dá:
;------ ; HEX - Converte um número na RAM para um texto hex terminado por '$'. ; Entrada: DE - Posição de memória onde o texto será colocado (3 bytes) ; A - Valor a ser "convertido" ;------ HEX: LD B,A ; Salva valor original SRL A ; Pega valor do nibble superior SRL A SRL A SRL A LD (DE),A ; Guarda na memória LD A,B ; Recupera valor numérico original AND 00001111b ; Aplica máscara (isola nibble inferior) LD (DE),A LD A,'$' LD (DE),A RET
E a função está pronta. Podemos inserí-la no nosso programa, para imprimir o número 188d (BCh) na tela, ficando o programa assim:
--- Cortar Aqui --- BDOS EQU 5 STROUT EQU 9 START: ; Mostra informacoes do programa LD DE,NOMEDOPRG ; Indica texto do nome do programa CALL MOSTRATXT ; Mostra texto LD DE,AUTOR ; Indica texto do nome do autor CALL MOSTRATXT ; Mostra texto ; Converte número LD DE,NUMERO ; Indica posição de memória para resposta LD A,188 ; Carrega Registrador A com o valor CALL HEX ; Converte o valor para uma string ; Mostra número CALL MOSTRATXT ; Imprime o número na tela JP 0 ; Volta ao MSX-DOS NOMEDOPRG: DB 'Programa 5a - Mostrando Números em Hexadecimal',13,10,'$' AUTOR: DB ' Por Daniel Caetano',13,10,10,'$' NUMERO: DB 0,0,0 ;------ ; MOSTRATXT - Funcao que mostra um texto cuja sequencia e' terminada por '$'. ; Entrada: DE - Aponta para sequencia a ser mostrada, terminada por '$' ;------ MOSTRATXT: LD C,STROUT ; Indica funcao de mostrar texto do BDOS CALL BDOS ; Manda o BDOS executar. RET ; Retorna ;------ ; HEX - Converte um número na RAM para um texto hex terminado por '$'. ; Entrada: DE - Posição de memória onde o texto será colocado (3 bytes) ; A - Valor a ser "convertido" ;------ HEX: LD B,A ; Salva valor original SRL A ; Pega valor do nibble superior SRL A SRL A SRL A LD (DE),A ; Guarda na memória LD A,B ; Recupera valor numérico original AND 00001111b ; Aplica máscara (isola nibble inferior) LD (DE),A LD A,'$' LD (DE),A RET END --- Cortar Aqui ---
Entretanto, ao executar vemos que... nada foi impresso! Por que isso acontece? Você pode analisar isso com o FuDebug, e vai descobrir que o problema está em nossa função HEX. Suponhamos que DE aponte para a posição de memória 1000h. Então, digamos que as posições 1000h, 1001h e 1002h estão disponíveis para colocarmos o primeiro e o segundo byte do número, MAIS o $ final. Ou seja:
Posição O que deve conter 1000h Valor do Nibble Alto 1001h Valor do Nibble Baixo 1002h '$'
Mas quando observamos o que a nossa função faz, vemos que ela coloca os três valores exatamente na mesma posição de memória! Ou seja, no fim de nossa rotina, as posições de memória deveriam conter:
Posição Valor 1000h Bh 1001h Ch 1002h '$'
Mas não é isso que está acontecendo. O resultado está sendo:
Posição Valor 1000h '$' 1001h 0 1002h 0
E, obviamente, quando mandarmos isso ser impresso, nada aparecerá! O problema está em que esquecemos de atualizar a posição de memória apontada por DE, de forma que a cada valor que escrevemos na RAM, devemos indicar a posição seguinte, para que o valor seguinte não sobreponha o anterior. Isso pode ser feito usando-se a instrução:
INC DE
Onde INC significa INCremente, ou seja, INC DE significa adicione uma unidade ao valor de DE. Como DE aponta um endereço de memória, INC DE pode ser traduzido como "Aponte o próximo endereço de memória". Assim, sempre que escrevermos um dado na posição de memória DE, devemos incrementá-la. O código corrigido da função será:
;------ ; HEX - Converte um número na RAM para um texto hex terminado por '$'. ; Entrada: DE - Posição de memória onde o texto será colocado (3 bytes) ; A - Valor a ser "convertido" ;------ HEX: LD B,A ; Salva valor original SRL A ; Pega valor do nibble superior SRL A SRL A SRL A LD (DE),A ; Guarda na memória INC DE ; Aponta próxima posição de memória LD A,B ; Recupera valor numérico original AND 00001111b ; Aplica máscara (isola nibble inferior) LD (DE),A INC DE ; Aponta próxima posição de memória LD A,'$' LD (DE),A RET
Note que após escrever '$' em DE eu não adicionei um INC DE, visto que não
previa escrever mais nada ali. Quando voltar dessa função, portanto, DE vai
estar apontando para o '$'.
O programa final fica:
--- Cortar Aqui --- BDOS EQU 5 STROUT EQU 9 START: ; Mostra informacoes do programa LD DE,NOMEDOPRG ; Indica texto do nome do programa CALL MOSTRATXT ; Mostra texto LD DE,AUTOR ; Indica texto do nome do autor CALL MOSTRATXT ; Mostra texto ; Converte número LD DE,NUMERO ; Indica posição de memória para resposta LD A,188 ; Carrega Registrador A com o valor CALL HEX ; Converte o valor para uma string ; Mostra número CALL MOSTRATXT ; Imprime o número na tela JP 0 ; Volta ao MSX-DOS NOMEDOPRG: DB 'Programa 5a - Mostrando Números em Hexadecimal',13,10,'$' AUTOR: DB ' Por Daniel Caetano',13,10,10,'$' NUMERO: DB 0,0,0 ;------ ; MOSTRATXT - Funcao que mostra um texto cuja sequencia e' terminada por '$'. ; Entrada: DE - Aponta para sequencia a ser mostrada, terminada por '$' ;------ MOSTRATXT: LD C,STROUT ; Indica funcao de mostrar texto do BDOS CALL BDOS ; Manda o BDOS executar. RET ; Retorna ;------ ; HEX - Converte um número na RAM para um texto hex terminado por '$'. ; Entrada: DE - Posição de memória onde o texto será colocado (3 bytes) ; A - Valor a ser "convertido" ;------ HEX: LD B,A ; Salva valor original SRL A ; Pega valor do nibble superior SRL A SRL A SRL A LD (DE),A ; Guarda na memória INC DE ; Aponta próxima posição de memória LD A,B ; Recupera valor numérico original AND 00001111b ; Aplica máscara (isola nibble inferior) LD (DE),A INC DE ; Aponta próxima posição de memória LD A,'$' LD (DE),A RET END --- Cortar Aqui ---
E... ao executar... Ora pois! O problema continuou! Sim, o
problema continuou, mas por um motivo diferente. Lembra-se que eu disse que, na
saida da função HEX o valor apontado por DE era o '$'? Pois então.
Observe este trecho no programa principal:
; Converte número LD DE,NUMERO ; Indica posição de memória para resposta LD A,188 ; Carrega Registrador A com o valor CALL HEX ; Converte o valor para uma string ; Mostra número CALL MOSTRATXT ; Imprime o número na tela
Como MOSTRATXT usa o valor apontado por DE para começar a imprimir, e
este valor é o '$', que indica fim de impressão... Fica claro que *nada*
será impresso. Isso pode ser corrigindo RECARREGANDO o enderço do número a ser
mostrado, assim:
; Converte número LD DE,NUMERO ; Indica posição de memória para resposta LD A,188 ; Carrega Registrador A com o valor CALL HEX ; Converte o valor para uma string ; Mostra número LD DE,NUMERO ; Indica posição de memória com o número CALL MOSTRATXT ; Imprime o número na tela
E o programa final fica:
--- Cortar Aqui --- BDOS EQU 5 STROUT EQU 9 START: ; Mostra informacoes do programa LD DE,NOMEDOPRG ; Indica texto do nome do programa CALL MOSTRATXT ; Mostra texto LD DE,AUTOR ; Indica texto do nome do autor CALL MOSTRATXT ; Mostra texto ; Converte número LD DE,NUMERO ; Indica posição de memória para resposta LD A,188 ; Carrega Registrador A com o valor CALL HEX ; Converte o valor para uma string ; Mostra número LD DE,NUMERO ; Indica posição de memória com o número CALL MOSTRATXT ; Imprime o número na tela JP 0 ; Volta ao MSX-DOS NOMEDOPRG: DB 'Programa 5a - Mostrando Números em Hexadecimal',13,10,'$' AUTOR: DB ' Por Daniel Caetano',13,10,10,'$' NUMERO: DB 0,0,0 ;------ ; MOSTRATXT - Funcao que mostra um texto cuja sequencia e' terminada por '$'. ; Entrada: DE - Aponta para sequencia a ser mostrada, terminada por '$' ;------ MOSTRATXT: LD C,STROUT ; Indica funcao de mostrar texto do BDOS CALL BDOS ; Manda o BDOS executar. RET ; Retorna ;------ ; HEX - Converte um número na RAM para um texto hex terminado por '$'. ; Entrada: DE - Posição de memória onde o texto será colocado (3 bytes) ; A - Valor a ser "convertido" ;------ HEX: LD B,A ; Salva valor original SRL A ; Pega valor do nibble superior SRL A SRL A SRL A LD (DE),A ; Guarda na memória INC DE ; Aponta próxima posição de memória LD A,B ; Recupera valor numérico original AND 00001111b ; Aplica máscara (isola nibble inferior) LD (DE),A INC DE ; Aponta próxima posição de memória LD A,'$' LD (DE),A RET END --- Cortar Aqui ---
Ao executar esse código... algo inesperado acontece. Lixo na tela e a tela
se limpa. O que tem de errado agora? Qual é o erro?
Bem, o fato é que realmente as posições de memória que mandamos o programa
escrever continham os números em hexadecimal de 0 a 15 tanto do nibble alto
quanto do nibble baixo, mas não podemos nos esquecer que esses números precisam
estar na *notação ASCII*. Ou seja, nao podemos ter um número de 0 a 15, mas sim
um número de 30h (valor ASCII de 0) a 39h (valor ASCII de 9) ou um valor de 41h
(valor ASCII de A) a 46h (valor ASCII de F). Então precisamos criar uma função
que converta um valor de 0 a 15 no valor ASCII correspondente.
Vou chamar essa funcao de DIG2ASCII (converte DIGito para ASCII). A idéia é
somar o valor 30h ao número, caso ele seja inferior a 10d (ou seja, de 0 a 9),
criando o valor 30h a 39h (ASCIIs de 0 a 9). Se o número for superior a 10d, a
idéia é subtrair 10d (Ah) e somar 41h (obtendo o valor 41 a 46h, que são os
valores ASCII de A a F).
A primeira coisa a fazer será uma comparação para verificar se o valor do
registrador A é menor que 10d (0Ah). Uma forma simples de fazer isso seria
subtrair 0Ah do registrador A (usando a instrução SUB) e verificar se o
resultado deu um "underflow". Um underflow significaria que eu subtraí do
registrador um valor maior do que aquele que ele continha.
Mas como verificaríamos se ocorreu um underflow? Bem, o Z80 tem um registrador
chamado F (Flags), em que cada bit representa uma coisa, dependendo da instrução
que se acabou de executar. Um destes bits é chamado C (Carry) e serve para uma
porção de coisas. Uma delas é indicar um underflow... (ou, se estivéssemos
tratando em termos de números positivos e negativos, que o número do acumulador
se tornou negativo).
A idéia é, então, usar o valor do Carry para decidir qual parte do código
executar. Para isso vamos recorrer à instrução JP (JumP) novamente, que já vimos
na primeira aula... Mas agora com uma leve diferença:
JP C,endereço
Que significa: "Se o Carry estiver marcado, pule para o endereço". Ou seja, podemos começar nossa função assim:
;------ ; DIG2ASCII - Converte um valor de 0 a 15 para seu correspondente ASCII ; Entrada: A - Valor a ser convertido ; Saída: A - Valor convertido ;------ DIG2ASCII: SUB 10 ; Subtrai 10 do acumulador JP C,D2ASC2 ; Se ficou negativo, era um número de 0 a 9... ; Vai para rotina lá pra frente ; Esta parte é se o número era maior ou igual a 10 ADD A,'A' ; Soma A com o valor ASCII de 'A' RET D2ASC2: ; Parte que lida com números de 0 a 9 ADD A,'0'+10 ; Soma A com o valor ASCII de '0', MAIS o 10 ; que tiramos no início da função RET
Note que a instrução SUB 10 opera sobre o Acumulador (registrador A). Não
existe SUB A,10 (não me pergunte o porquê, já que para ADD temos, por exemplo,
ADD A,10).
Essa função pode ser inserida no nosso programa, usada para corrigir o valor
no registrador A antes dele ser armazenado na memória:
--- Cortar Aqui --- BDOS EQU 5 STROUT EQU 9 START: ; Mostra informacoes do programa LD DE,NOMEDOPRG ; Indica texto do nome do programa CALL MOSTRATXT ; Mostra texto LD DE,AUTOR ; Indica texto do nome do autor CALL MOSTRATXT ; Mostra texto ; Converte número LD DE,NUMERO ; Indica posição de memória para resposta LD A,188 ; Carrega Registrador A com o valor CALL HEX ; Converte o valor para uma string ; Mostra número LD DE,NUMERO ; Indica posição de memória com o número CALL MOSTRATXT ; Imprime o número na tela JP 0 ; Volta ao MSX-DOS NOMEDOPRG: DB 'Programa 5a - Mostrando Números em Hexadecimal',13,10,'$' AUTOR: DB ' Por Daniel Caetano',13,10,10,'$' NUMERO: DB 0,0,0 ;------ ; MOSTRATXT - Funcao que mostra um texto cuja sequencia e' terminada por '$'. ; Entrada: DE - Aponta para sequencia a ser mostrada, terminada por '$' ;------ MOSTRATXT: LD C,STROUT ; Indica funcao de mostrar texto do BDOS CALL BDOS ; Manda o BDOS executar. RET ; Retorna ;------ ; HEX - Converte um número na RAM para um texto hex terminado por '$'. ; Entrada: DE - Posição de memória onde o texto será colocado (3 bytes) ; A - Valor a ser "convertido" ;------ HEX: LD B,A ; Salva valor original SRL A ; Pega valor do nibble superior SRL A SRL A SRL A CALL DIG2ASCII ; Converte valor de A para ASCII LD (DE),A ; Guarda na memória INC DE ; Aponta próxima posição de memória LD A,B ; Recupera valor numérico original AND 00001111b ; Aplica máscara (isola nibble inferior) CALL DIG2ASCII ; Converte valor de A para ASCII LD (DE),A INC DE ; Aponta próxima posição de memória LD A,'$' LD (DE),A RET ;------ ; DIG2ASCII - Converte um valor de 0 a 15 para seu correspondente ASCII ; Entrada: A - Valor a ser convertido ; Saída: A - Valor convertido ;------ DIG2ASCII: SUB 10 ; Subtrai 10 do acumulador JP C,D2ASC2 ; Se ficou negativo, era um número de 0 a 9... ; Vai para rotina lá pra frente ; Esta parte é se o número era maior ou igual a 10 ADD A,'A' ; Soma A com o valor ASCII de 'A' RET D2ASC2: ; Parte que lida com números de 0 a 9 ADD A,'0'+10 ; Soma A com o valor ASCII de '0', MAIS o 10 ; que tiramos no início da função RET END --- Cortar Aqui ---
E finalmente nosso programa já responde corretamente. Podemos ainda mudar
um pouco a função DIG2ASCII, aproveitando para introduzir o comando CP
(ComPare). O Compare faz EXATAMENTE o que fizemos com o SUB, mas sem afetar o
valor do registrador A. Ou seja, é como se ele fizesse um SUB, preenchesse os
flags (no caso o flag C que nos interessa), mas recuperasse o valor original de
A no fim da operação. Embora isso não traga um ganho sensível de performance (e,
em alguns casos, talvez o uso do SUB seja mais eficiente), o uso do CP torna o
código mais legível, pois ele diz exatamente o que se está fazendo.
O código da função DIG2ASCII usando CP seria:
;------ ; DIG2ASCII - Converte um valor de 0 a 15 para seu correspondente ASCII ; Entrada: A - Valor a ser convertido ; Saída: A - Valor convertido ;------ DIG2ASCII: CP 10 ; Compara A com 10 JP C,D2ASC2 ; Se A era menor que 10, pula pro fim ; Vai para rotina lá pra frente ; Esta parte é se o número era maior ou igual a 10 ADD A,'A'-10 ; Soma A com o valor ASCII de 'A', subtraindo ; 10 (já que não foi subtraído no início). RET D2ASC2: ; Parte que lida com números de 0 a 9 ADD A,'0' ; Soma A com o valor ASCII de '0' RET
Uma forma de "otimizar" um pouco essa função seria torná-la serial. Desta forma, aceleraríamos o processamento para números de 0 a 9, em contrapartida tornaríamos mais lento o processo para números de A a F. Entretanto, eliminaremos o JP do processo, o que usualmente significa um ganho de performance:
;------ ; DIG2ASCII - Converte um valor de 0 a 15 para seu correspondente ASCII ; Entrada: A - Valor a ser convertido ; Saída: A - Valor convertido ;------ DIG2ASCII: ADD A,'0' ; Soma o valor ASCII de '0' indepentende do ; valor original de A CP '9'+1 ; Verifica se o valor em A é menor que o ; valor de '9' + 1 (ou seja, se é menor ou ; igual a '9') RET C ; Se sim, vai embora...! ; Aqui é caso o valor seja maior... ou seja, A a F ADD A,'A'-('0'+10) ; Corrige para um valor de A a F RET
Observe o uso do valor do flag C (Carry) em outra instrução: a instrução
RET. A instrução RET também pode utilizá-lo. Neste caso ela significa:
"RETorne se o flag C estiver marcado".
Finalmente, nosso programa de imprimir um número de 8 bits está pronto.
--- Cortar Aqui --- BDOS EQU 5 STROUT EQU 9 START: ; Mostra informacoes do programa LD DE,NOMEDOPRG ; Indica texto do nome do programa CALL MOSTRATXT ; Mostra texto LD DE,AUTOR ; Indica texto do nome do autor CALL MOSTRATXT ; Mostra texto ; Converte número LD DE,NUMERO ; Indica posição de memória para resposta LD A,188 ; Carrega Registrador A com o valor CALL HEX ; Converte o valor para uma string ; Mostra número LD DE,NUMERO ; Indica posição de memória com o número CALL MOSTRATXT ; Imprime o número na tela JP 0 ; Volta ao MSX-DOS NOMEDOPRG: DB 'Programa 5a - Mostrando Números em Hexadecimal',13,10,'$' AUTOR: DB ' Por Daniel Caetano',13,10,10,'$' NUMERO: DB 0,0,0 ;------ ; MOSTRATXT - Funcao que mostra um texto cuja sequencia e' terminada por '$'. ; Entrada: DE - Aponta para sequencia a ser mostrada, terminada por '$' ;------ MOSTRATXT: LD C,STROUT ; Indica funcao de mostrar texto do BDOS CALL BDOS ; Manda o BDOS executar. RET ; Retorna ;------ ; HEX - Converte um número na RAM para um texto hex terminado por '$'. ; Entrada: DE - Posição de memória onde o texto será colocado (3 bytes) ; A - Valor a ser "convertido" ;------ HEX: LD B,A ; Salva valor original SRL A ; Pega valor do nibble superior SRL A SRL A SRL A CALL DIG2ASCII ; Converte valor de A para ASCII LD (DE),A ; Guarda na memória INC DE ; Aponta próxima posição de memória LD A,B ; Recupera valor numérico original AND 00001111b ; Aplica máscara (isola nibble inferior) CALL DIG2ASCII ; Converte valor de A para ASCII LD (DE),A INC DE ; Aponta próxima posição de memória LD A,'$' LD (DE),A RET ;------ ; DIG2ASCII - Converte um valor de 0 a 15 para seu correspondente ASCII ; Entrada: A - Valor a ser convertido ; Saída: A - Valor convertido ;------ DIG2ASCII: ADD A,'0' ; Soma o valor ASCII de '0' indepentende do ; valor original de A CP '9'+1 ; Verifica se o valor em A é menor que o ; valor de '9' + 1 (ou seja, se é menor ou ; igual a '9') RET C ; Se sim, vai embora...! ; Aqui é caso o valor seja maior... ou seja, A a F ADD A,'A'-('0'+10) ; Corrige para um valor de A a F RET END --- Cortar Aqui ---
Agora vamos adicionar nele a rotina HEXW, que converte um número de 16 bits para uma string. A idéia é a mesma, mas veja... um número de 16 bits pode ser entendido assim:
Binário Hexadecimal 0000:1101:0110:1111 = 0D6F
Ou seja, a idéia de que cada dígito do número hexadecimal é um nible do número binário permanece. Podemos dizer que um número de 16 bits e' composto por dois bytes: o byte alto e o byte baixo. Assim, podemos decompor o número acima em:
Binário Hexadecimal 0000:1101 0110:1111 = 0D 6F
Assim, podemos usar a nossa função HEX já pronta como parte da função HEXW. Podemos criar a base da função HEXW assim:
;------ ; HEXW - Converte um número na RAM para um texto hex terminado por '$'. ; Entrada: DE - Posição de memória onde o texto será colocado (5 bytes) ; HL - Valor a ser "convertido" ;------ HEXW: RET
Sabendo que H é o byte alto do número de 16 bits (tamanho também conhecido como PALAVRA ou WORD... daí o nome HEXW, ou o HEX para Words), ele será o que ficará mais à esquerda do número, então deve ser colocado na string antes. A função fica assim:
;------ ; HEXW - Converte um número na RAM para um texto hex terminado por '$'. ; Entrada: DE - Posição de memória onde o texto será colocado (5 bytes) ; HL - Valor a ser "convertido" ; Modifica: A, DE ;------ HEXW: LD A,H CALL HEX RET
Note que, como o valor de DE já vem pronto, não precisamos mexer nele. Note
também que o fato da função HEX deixar DE apontando sobre o '$' vai nos ajudar
agora... vejamos o porquê:
Suponhamos que DE apontasse para 1000h. Teríamos 1000h, 1001h, 1002h, 1003h e
1004h para colocarmos o nosso número de 16 bits. Ou seja:
Posição Valor 1000h Nibble alto do byte alto 1001h Nibble baixo do byte alto 1002h Nibble alto do byte alto 1003h Nibble baixo do bayte alto 1004h '$'
Ora, após a primeira chamada de HEX, temos:
Posição Valor 1000h Nibble alto do byte alto 1001h Nibble baixo do byte alto 1002h '$' 1003h 0 1004h 0
Com DE apontando para 1002h, que é exatamente onde queremos começar a impressão do byte baixo! Assim, nem precisamos corrigir o valor de DE, basta corrigir o valor de A com o valor do byte baixo e novamente chamar a rotina HEX. Assim, a nossa versão final de HEXW será:
;------ ; HEXW - Converte um número na RAM para um texto hex terminado por '$'. ; Entrada: DE - Posição de memória onde o texto será colocado (5 bytes) ; HL - Valor a ser "convertido" ; Modifica: A, DE ;------ HEXW: LD A,H CALL HEX LD A,L CALL HEX RET
E pronto. Vamos inserí-la em nosso programa, e chamá-la na rotina principal:
--- Cortar Aqui --- BDOS EQU 5 STROUT EQU 9 START: ; Mostra informacoes do programa LD DE,NOMEDOPRG ; Indica texto do nome do programa CALL MOSTRATXT ; Mostra texto LD DE,AUTOR ; Indica texto do nome do autor CALL MOSTRATXT ; Mostra texto ; Mostra número de 8 bits LD DE,NUMERO ; Indica posição de memória para resposta LD A,188 ; Carrega Registrador A com o valor CALL HEX ; Converte o valor para uma string LD DE,NUMERO ; Indica posição de memória com o número CALL MOSTRATXT ; Imprime o número na tela ; Pula uma linha LD DE,PULALINHA ; Indica "texto" CALL MOSTRATXT ; Imprime ; Mostra número de 16 bits LD DE,NUMERO16 ; Indica posição de memória para resposta LD HL,04DC2h ; Carrega Registrador HL com o valor CALL HEXW ; Converte o valor para uma string LD DE,NUMERO16 ; Indica posição de memória com o número CALL MOSTRATXT ; Imprime o número na tela JP 0 ; Volta ao MSX-DOS NOMEDOPRG: DB 'Programa 5a - Mostrando Números em Hexadecimal',13,10,'$' AUTOR: DB ' Por Daniel Caetano',13,10,10,'$' NUMERO: DB 0,0,0 PULALINHA: DB 13,10,'$' NUMERO16: DB 0,0,0,0,0 ;------ ; MOSTRATXT - Funcao que mostra um texto cuja sequencia e' terminada por '$'. ; Entrada: DE - Aponta para sequencia a ser mostrada, terminada por '$' ;------ MOSTRATXT: LD C,STROUT ; Indica funcao de mostrar texto do BDOS CALL BDOS ; Manda o BDOS executar. RET ; Retorna ;------ ; HEX - Converte um número na RAM para um texto hex terminado por '$'. ; Entrada: DE - Posição de memória onde o texto será colocado (3 bytes) ; A - Valor a ser "convertido" ;------ HEX: LD B,A ; Salva valor original SRL A ; Pega valor do nibble superior SRL A SRL A SRL A CALL DIG2ASCII ; Converte valor de A para ASCII LD (DE),A ; Guarda na memória INC DE ; Aponta próxima posição de memória LD A,B ; Recupera valor numérico original AND 00001111b ; Aplica máscara (isola nibble inferior) CALL DIG2ASCII ; Converte valor de A para ASCII LD (DE),A INC DE ; Aponta próxima posição de memória LD A,'$' LD (DE),A RET ;------ ; DIG2ASCII - Converte um valor de 0 a 15 para seu correspondente ASCII ; Entrada: A - Valor a ser convertido ; Saída: A - Valor convertido ;------ DIG2ASCII: ADD A,'0' ; Soma o valor ASCII de '0' indepentende do ; valor original de A CP '9'+1 ; Verifica se o valor em A é menor que o ; valor de '9' + 1 (ou seja, se é menor ou ; igual a '9') RET C ; Se sim, vai embora...! ; Aqui é caso o valor seja maior... ou seja, A a F ADD A,'A'-('0'+10) ; Corrige para um valor de A a F RET ;------ ; HEXW - Converte um número na RAM para um texto hex terminado por '$'. ; Entrada: DE - Posição de memória onde o texto será colocado (5 bytes) ; HL - Valor a ser "convertido" ; Modifica: A, DE ;------ HEXW: LD A,H CALL HEX LD A,L CALL HEX RET END --- Cortar Aqui ---
Bem, chegamos aqui ao fim desta aula... Espero que a despeito do "peso"
dessa aula todos tenham conseguido acompanhar. Não é nada de outro mundo, mas
certamente parece um monstro quando pensamos que em Basic, C ou pascal
conseguimos imprimir esses valores com um único "PRINT". Entretanto, por trás
deste "PRINT" tem tudo isso que acabamos de ver aqui.
Fica como proposta de pensamento analisar o porque seria mais complicado
imprimir os números em decimal - algo que o Z80 ajuda a fazer através de algumas
instruções que, possivelmente, veremos no futuro.
Mais uma vez estou a berto a sugestões... mas não me perguntem quando a
próxima aula sai. Só a título de curiosidade, estou há quase 6 horas preparando
esta "mísera" aula, um tempo que certamente não tenho com
freqüência.
Espero que apreciem, pois esta aqui amplia bastante os horizontes,
introduzindo não só uma rotina importante como a impressão de números, mas
também comandos condicionais por flags, como CALL, RET e JP, além da instrução
CP.
Detalhes sobre instruções que afetam e são afetadas por flags podem ser, mais
uma vez, encontradas no livro do Rossini e Figueredo, sendo que a explicação
detalhada de cada flag, instrução e operação do Z80 foge completamente ao escopo
deste curso.
Reitero que sem programar e brincar, ninguém vai adiante em ASM, e se ninguém
tentar, ninguém tem dúvidas. E se ninguém tem dúvidas, eu fico completamente sem
estímulo em continuar o curso... Afinal, é um tempo enorme consumido que ...
acaba dando impressão que tá sendo jogado fora.
Um abraço e até a próxima!
Daniel Caetano.
PS: Sugestões e comentários são bem-vindos. (^=