Curso Prático de Assembly Z80 - Aula 5

Curso de ASM

Aula 5: Truques Matemáticos I - Imprimindo Números


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 ( MSX Livros ), 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.

Imprimindo Números em Hexadecimal

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 sequê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.

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

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 valorde A na memória, e sim escrevendo ele no registrador DE (e por consequênciaapagando 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 aoparê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 depoisrecuperando-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 sejammantidos 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ãopreservados.

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 dadireita (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:

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

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çãoseguinte, 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:

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

E... ao executar... Ora pois! O problema continuou! Sim, o problemacontinuou, 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 estevalor é 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:

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

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 ideia é 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 ideia é 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 resultadodeu um "underflow". Um underflow significaria que eu subtraí do registrador umvalor maior do que aquele que ele continha.

Mas como verificaríamos se ocorreu um underflow? Bem, o Z80 tem umregistrador 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 umunderflow... (ou, se estivéssemos tratando em termos de números positivos e negativos, que o número do acumulador se tornou negativo).

A ideia é, 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:

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

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 semafetar 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 ganhosensível de performance (e, em alguns casos, talvez o uso do SUB seja maiseficiente), o uso do CP torna o código mais legível, pois ele diz exatamenteo 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 uma melhora no desempenho:

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

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

Agora vamos adicionar nele a rotina HEXW, que converte um número de 16 bits para uma string. A ideia é 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 ideia 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 é 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:

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

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 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 aberto 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 frequê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.

Daniel Caetano