Curso Fudeba 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 ( 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.
 

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 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. (^=