Curso Fudeba de ASM |
Na aula passada vimos de maneira detalhada a forma de imprimir números em
hexadecinal. Juntamente com isso, vimos alguns conceitos importantes sobre
valores binários, deslocamentos de bits e coisas deste tipo.
Como foi possível ver, existiu uma conversão razoavelmente complexa entre a
forma que o computador trabalha com os números para que pudéssemos ver o
resultado na tela. Isso indica uma coisa: o computador lida com os números de
uma forma bem diferente da nossa, e é melhor fugir dos números decimais na hora
de implementar funções matemáticas no computador.
Fundamentalmente, na maioria das vezes iremos implementar usando exatamente a
mesma idéia que usamos nas contas decimais que fazemos à mão, mas sempre tomando
o cuidado de adaptá-las para que funcionem em base binária... Evitando assim
muita perda de tempo de processamento.
Nesta aula vamos criar uma função interessante: MULUU. Esta função deve
multiplicar um numero de 8 bits por outro de 8 bits, gerando um número de 16
bits.
Deixaremos a divisão para uma aula posterior, caso contrário essa aula seria
literalmente interminável.
Como era de se esperar, o nosso programa base desta aula será baseado no programa final da aula 5. O esqueleto está reproduzido abaixo:
--- 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 NOMEDOPRG: DB 'Programa 6a - Multiplicando Numeros.',13,10,'$' AUTOR: DB ' Por Daniel Caetano',13,10,10,'$' PULALINHA: DB 13,10,'$' ;------ ; 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 ---
A primeira coisa que podemos definir é que a nossa nova função terá a seguinte cara:
;------ ; MULUU - Multiplica um número de 8 bits por outro de 8 bits (sem sinal) ; Entrada: L - Multiplicando ; A - Multiplicador ; Saída: A - Produto ;------ MULUU: RET
Bem, pensemos um pouco. Como fazer um processador que só faz soma e subtração realizar uma multiplicação? A forma mais simples, baseada naquela que aprendemos quando crianças na escola, é fazer um monte de somas... Afinal, é correto afirmar que:
5*1 = 1 + 1 + 1 + 1 + 1 = 5 7*3 = 7 + 7 + 7 = 21
Opa! Isso parece uma tarefa para aquela instrução bizarra da aula 4... o DJNZ! Para quem já se esqueceu, o DJNZ é uma instrução que significa "Decrement B and Jump if Not Zero". Se fizermos uma construção do tipo:
LD B,n LOOP: ; Operações DJNZ LOOP
As operações serão realizadas "n" vezes! Ora... se 7*3 = somar 7 por 3 vezes... Hum! Interessante. Implementando essa idéia, temos que colocar o valor de A em B, e ir somando L várias vezes... O que resulta em:
;------ ; MULUU - Multiplica um número de 8 bits por outro de 8 bits (sem sinal) ; Entrada: L - Multiplicando ; A - Multiplicador ; Saída: A - Produto ;------ MULUU: LD B,A MUUL: ADD A,L DJNZ MUUL RET
E pronto! No nosso programa teríamos:
--- 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 LD L,010h ; Multiplicando LD A,005h ; Multiplicador CALL MULUU ; Chama multiplicacao (resultado em A) LD DE,NUMERO ; Indica posição do texto CALL HEX ; Converte para ASCII LD DE,NUMERO ; Aponta número CALL MOSTRATXT ; Imprime JP 0 ; Volta ao MSX-DOS NOMEDOPRG: DB 'Programa 6a - Multiplicando Numeros.',13,10,'$' AUTOR: DB ' Por Daniel Caetano',13,10,10,'$' NUMERO: DB 0,0,0 PULALINHA: DB 13,10,'$' ;------ ; MULUU - Multiplica um número de 8 bits por outro de 8 bits (sem sinal) ; Entrada: L - Multiplicando ; A - Multiplicador ; Saída: A - Produto ;------ MULUU: LD B,A MUUL: ADD A,L DJNZ MUUL RET ;------ ; 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 ---
Entretanto, ao rodar esse programa... Algo estranho aconteceu. O número
impresso nada tem a ver com a resposta esperada. O programa nos retornou o valor
55h, quando bem sabemos que 5h * 10h = 50h! O que está errado?!
Analisemos com calma aquilo que a função está fazendo: Colocamos o número de
repetições em B e somamos B vezes L em A... opa! Estamos somando o valor em A,
antes de ter zerado o valor de A... então, na realidade, fizemos 5h * 10h + 5h =
55h! Isso pode ser corrigido então, usando a seguinte modificação:
;------ ; MULUU - Multiplica um número de 8 bits por outro de 8 bits (sem sinal) ; Entrada: L - Multiplicando ; A - Multiplicador ; Saída: A - Produto ;------ MULUU: LD B,A LD A,0 MUUL: ADD A,L DJNZ MUUL RET
E pronto! A vai conter o resultado da multiplicação na saída da função, e o programa imprime corretamente o valor 50h. Entretanto, basta substituir:
LD L,010h ; Multiplicando LD A,005h ; Multiplicador
Por algo como:
LD L,082h ; Multiplicando LD A,005h ; Multiplicador
Que a nossa alegria vai para o vinagre. O programa passa a responder 8A,
quando deveria responder 28A. O que está errado? Certamente a nossa concepção do
problema. Observe que o valor máximo possível de se guardar em um registrador de
8 bits é FFh. 28Ah é um número bem superior a FFh.
A solução então é para isso usar um registrador de mais bits. Quantos? Bem, se
estamos multiplicando dois números de 8 bits... temos, por uma regra matemática
simples, que:
(2^8 * 2^8) = 2^(8+8) = 2^16
Ou seja, basta um número de 16 bits que conseguiremos, com sucesso, armazenar qualquer resultado de uma multiplicação entre dois números de 8 bits. Uma outra forma de ver isso seria pensar assim: qual é o maior número de 8 bits possível? FFh. Quanto vale FFh * FFh ? FE01. Assim, um número capaz de armazenar FE01 será suficiente para guardar resultado de uma multiplicação de 8 bits por 8 bits. Assim, iremos usar o registrador HL, o que implica que precisamos inicializar, antes de mais nada, o valor de H:
------ ; MULUU - Multiplica um número de 8 bits por outro de 8 bits (sem sinal) ; Entrada: L - Multiplicando ; A - Multiplicador ; Saída: HL - Produto ;------ MULUU: LD H,0 ; Zera H... LD B,A ; Indica número de vezes LD A,0 ; Zera acumulador MUUL: ADD A,L ; Soma B vezes o valor de L DJNZ MUUL LD L,A ; Copia pro destino RET
Ao inserir essa função no programa principal, algumas mudanças precisam ser realizadas no corpo do código. O programa completo 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 LD L,082h ; Multiplicando LD A,005h ; Multiplicador CALL MULUU ; Chama multiplicacao ; HL já vem com o valor da reposta LD DE,NUMERO ; Indica posição do texto CALL HEXW ; Converte para ASCII LD DE,NUMERO ; Aponta número CALL MOSTRATXT ; Imprime JP 0 ; Volta ao MSX-DOS NOMEDOPRG: DB 'Programa 6a - Multiplicando Numeros.',13,10,'$' AUTOR: DB ' Por Daniel Caetano',13,10,10,'$' NUMERO: DB 0,0,0,0,0 PULALINHA: DB 13,10,'$' ;------ ; MULUU - Multiplica um número de 8 bits por outro de 8 bits (sem sinal) ; Entrada: L - Multiplicando ; A - Multiplicador ; Saída: HL - Produto ;------ MULUU: LD H,0 ; Zera H... LD B,A ; Indica número de vezes LD A,0 ; Zera acumulador MUUL: ADD A,L ; Soma B vezes o valor de L DJNZ MUUL LD L,A ; Copia pro destino RET ;------ ; 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 ---
Entretanto... mais uma vez, ao rodar o programa, a decepção: O programa responde 008A, quando deveria responder 028A. O que ainda está errado? Analisemos, mais uma vez, com calma.
;------ ; MULUU - Multiplica um número de 8 bits por outro de 8 bits (sem sinal) ; Entrada: L - Multiplicando ; A - Multiplicador ; Saída: HL - Produto ;------ MULUU: LD H,0 ; Zera H... LD B,A ; Indica número de vezes LD A,0 ; Zera acumulador MUUL: ADD A,L ; Soma B vezes o valor de L DJNZ MUUL LD L,A ; Copia pro destino RET
Se pensarmos bem, estamos ainda fazendo a conta só com 8 bits, pois estamos usando o registrador A para realizar o cálculo. E, com efeito, devemos realizar com o registrador HL! Assim, a funcao deve ser modificada da seguinte forma:
;------ ; MULUU - Multiplica um número de 8 bits por outro de 8 bits (sem sinal) ; Entrada: L - Multiplicando ; A - Multiplicador ; Saída: HL - Produto ;------ MULUU: LD H,0 ; Zera H... HL agora contem o número a ser multiplicado LD D,H ; Zera DE LD E,H EX DE,HL ; Troca o conteudo de DE com HL (HL fica 0000 e DE fica o multiplicando) LD B,A ; Indica número de vezes MUUL: ADD HL,DE ; Soma B vezes o valor de DE em HL DJNZ MUUL RET
Isso corrige o problema dos 8 bits para 16 bits. Entretanto, se percebermos
bem... Caso o multiplicador seja ZERO teremos uma reposta errada: a
multiplicação ocorrerá, mas será uma multiplicação por 256!
Ora, e por que isso acontece? Por uma peculiaridade do DJNZ. O DJNZ decrementa
o valor de B antes de testá-lo. Desta forma, se enviarmos um valor ZERO
como multiplicador, este valor será colocado em B e, ao ser decrementado, se
tornará FFh. Desta forma, a multiplicação não será finalizada e o resultado será
incorreto.
Para corrigir isso, basta que verifiquemos, no início, se o valor do
multiplicador é zero e, se for, iremos embora. Para isso, utilizaremos um CP 0 e
um RET Z (RETurn if Zero). Note que colocamos isso em um local que HL tem um
valor zero, assim o valor da solução estará correto (0*n = 0):
;------ ; MULUU - Multiplica um número de 8 bits por outro de 8 bits (sem sinal) ; Entrada: L - Multiplicando ; A - Multiplicador ; Saída: HL - Produto ;------ MULUU: LD H,0 ; Zera H... HL agora contem o número a ser multiplicado LD D,H ; Zera DE LD E,H EX DE,HL ; Troca o conteudo de DE com HL (HL fica 0000 e DE fica o multiplicando) CP 0 ; Verifica se A é zero RET Z ; Vai embora se for LD B,A ; Indica número de vezes MUUL: ADD HL,DE ; Soma B vezes o valor de DE em HL DJNZ MUUL RET
Com essas correções, nossa função de multiplicar está pronta! O programa final completo tem este aspecto:
--- 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 LD L,082h ; Multiplicando LD A,005h ; Multiplicador CALL MULUU ; Chama multiplicacao ; HL já vem com o valor da reposta LD DE,NUMERO ; Indica posição do texto CALL HEXW ; Converte para ASCII LD DE,NUMERO ; Aponta número CALL MOSTRATXT ; Imprime JP 0 ; Volta ao MSX-DOS NOMEDOPRG: DB 'Programa 6a - Multiplicando Numeros.',13,10,'$' AUTOR: DB ' Por Daniel Caetano',13,10,10,'$' NUMERO: DB 0,0,0,0,0 PULALINHA: DB 13,10,'$' ;------ ; MULUU - Multiplica um número de 8 bits por outro de 8 bits (sem sinal) ; Entrada: L - Multiplicando ; A - Multiplicador ; Saída: HL - Produto ;------ MULUU: LD H,0 ; Zera H... HL agora contem o número a ser multiplicado LD D,H ; Zera DE LD E,H EX DE,HL ; Troca o conteudo de DE com HL (HL fica 0000 e DE fica o multiplicando) CP 0 ; Verifica se A é zero RET Z ; Vai embora se for LD B,A ; Indica número de vezes MUUL: ADD HL,DE ; Soma B vezes o valor de DE em HL DJNZ MUUL RET ;------ ; 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 ---
Entretanto, vocês devem ter percebido que essa função é um tanto quanto lenta. Se precisarmos realizar muitas dessas operações por segundo, podemos ter um problema realmente sério. Em especial se substituirmos isso:
LD L,082h ; Multiplicando LD A,005h ; Multiplicador
Por uma operação deste tipo:
LD L,082h ; Multiplicando LD A,0FFh ; Multiplicador
Uma forma de acelerar neste caso, seria escolher o menor valor como sendo o multiplicador... Entretanto essa solução não resolve todos os problemas. Por exemplo, se a operação a ser realizada for:
LD L,0FFh ; Multiplicando LD A,0FFh ; Multiplicador
Não há muita alternativa... não é? Sim, é claro que há. (^=
Vamos voltar um pouco à teoria e pensar: Como fazemos contas de multiplicar?
Obviamente ninguem fica contando nos dedos o número de vezes... Nem usa
palitinho. E, de certa forma, foi isso que ensinamos o computador fazer.
Nós não fazemos isso manualmente porque é lento e desajeitado. Não há muita
razão para imaginar que seria diferente para o computador. Pensemos então: como
fazmos conta de multiplicação no papel?
A idéia da multiplicação no papel é a seguinte:
A idéia da multiplicação no papel é a seguinte: 134 x 7 é reproduzida na seguinte operação: 7*4 = 28. Fica 8 e vai dois. 7*3 = 21 mais dois = 23. Fica 3, vai 2. 7*1 = 7 mais 2 = 9. Fica 9.
Com isso construímos a primeira operação: multiplicamos o multiplicando pelo primeiro dígito do multiplicador. Costumamos desenhar isso da seguinte forma:
134 x7 --- 938
Ora, podemos escrever também essa operação de forma inversa:
7 x134 ---- 938
Embora seja anti-natural, ela facilita um pouco a explicação do que está por vir. Como realizaríamos essa conta, agora?
4*7 = 28.
Reescreveríamos isso assim:
7 x134 ---- 28 + 3*7 = 21.
Compondo a segunda etapa:
7 x134 ---- 28 21+ ++ 1*7 = 7.
Sendo finalmente:
7 x134 ---- 28 21+ 7++ ---- 938
Mas esses "+" não parecem ajudar muito na explicação matemática, certo? Uma outra forma de realizar este mesmo cálculo, mais elucidativa matematicamente, seria:
7 x134 ---- 938 4*7 = 28. ... 7 x134 ---- 28 30*7 = 210. ... 7 x134 ---- 28 210 100*7 = 700.
Sendo finalmente:
7 x134 ---- 28 + 210 + 700 ---- 938
Pode-se dizer então que fizemos foi o seguinte:
134 * 7 = 1*4*7 + 10*3*7 + 100*1*7 = (10^0)*4*7 + (10^1)*3*7 + (10^2)*1*7
O que fizemos aqui foi transformar uma multiplicação de um número
complicado em uma sequências de multiplicações por números entre 0 e 9 (números
da base decimal, em cada uma das etapas), multiplicações por potências de 10, ou
seja, da base (entre cada etapa) e finalmente em um número de somas que é igual
ao número de dígitos do multiplicador (no caso, 3).
O que aconteceria se transformássemos essa lógica para uma multiplicação de
lógica binária? Poderíamos escrever, por exemplo, que:
4 * 3 00000100b * 00000011b
Seria como? Vamos tentar aplicar a mesma lógica: Multiplicar o primeiro dígito do nultiplicador pelo multiplicando, multiplicado pela base elevada a zero; o segundo dígito do multiplicador pelo multiplicando, multiplicado pela base elevada a um; o terceiro dígido to multiplicador pelo multiplicando, multiplicado pela base elevada a dois... e assim por diante:
00000100b * 00000011b 1o. Digito do Multiplicador: 0 * 00000011b * 2^0 = 0 2o. Digito do Multiplicador: 0 * 00000011b * 2^1 = 0 3o. Digito do Multiplicador: 1 * 00000011b * 2^2 = 00000011b * 4 = 00001100b
... (todos os outros dígitos do multiplicador são zero)
Somando todos estes resultados:
00000000b + 00000000b + 00001100b = 00001100b = 12
E 12 é exatamente o valor do cálculo 4*3!
Qual é a vantagem de se fazer isso? Se observarmos bem, podemos reescrever
assim:
1o. Digito do Multiplicador: 0 * 00000011b * 2^0 = 0 * 00000011b * 1 = 0 * 00000011b 2o. Digito do Multiplicador: 0 * 00000011b * 2^1 = 0 * 00000011b * 2 = 0 * 00000110b 3o. Digito do Multiplicador: 1 * 00000011b * 2^2 = 1 * 00000011b * 4 = 1 * 00001100b
...
Observem os valores mais da direita... Eles significam que podemos transformar
uma multiplicacao em uma sequência de somas de valores... e esses valores, vão
sendo deslocados uma casa à esquerda em cada iteração!
Parece estranho, mas... é exatamente ESSA a função daquele "+", "++", "+++" ...
que colocamos numa operação como essa:
7 x134 ---- 28 21+ 7++ ---- 938
Podemos dizer que "escrever esse +" é uma boa forma de realizar potências
da base utilizada sem ter de pensar muito a respeito!
Assim, um bom algoritmo de multiplicação pode se utilizar dessa "manha" para
reduzir seu loop. Uma multiplicacao de dois números de 8 bits se reduzirá de 255
somas (no pior caso) para 8 somas e 8 deslocamentos à esquerda!
Um bom algoritmo para realizar isso, em binário, seria:
0- Zera-se resultado. 1- Primeira casa do multiplicador é um? Não? Pula pro passo 2. Sim? Soma ao resultado o multiplicando 2- Rotaciona-se o multiplicando à esquerda Segunda casa do multiplicador é um? Não? Pula pro passo 3. Sim? Soma ao resultado o multiplicando. 3- Rotaciona-se o multiplicando à esquerda Terceira casa do multiplicador é um? Não? Pula pro passo 4. Sim? Soma ao resultado o multiplicando.
... e assim por diante até serem verificadas as 8 casas do multiplicador.
Nesta primeira implementação usaremos algumas instruções de rotação novas:
RRCA, SLA e RL. O que essas instruções fazem?
RRCA - Rotate Right (with Copy) Accumulator. Em língua de gente, a RRCA
rotaciona o acumulador pra direita, jogando o bit 7 pro bit 6, o bit 6 pro bit 5
e assim por diante... até o bit 0, que é jogado no bit 7. Além disso, uma cópia
do bit 0 é sempre jogada no carry. Desta forma, depois de aplicada 8 vezes esta
instrução faz o Acumulador (registrador A) voltar a ter exatamente o mesmo valor
original. Usaremos essa instrução para poder testar bit por bit do acumulador,
usando-se do flag Carry para isso (o qual podemos usar facilmente em instruções
JP (JumP) e JR (Jump Relative).
SLA - Shift Left And clear. Apesar do nome bizarro o que esta rotina faz é
simples. Ela desloca para a esquerda os bits de um registrador qualquer, jogando
bit 7 no flag Carry, o bit 6 no bit 7, o bit 5 no bit 6... até o bit 0 no bit 1.
E no lugar no bit 0 um bit de valor "0" é inserido. Trocando em miúdos,
multiplica um registrador por 2 e indica no carry se "foi um". Usaremos essa
para multiplicar o byte menos significativo do multiplicando.
RL - Rotate Left. Essa aqui é a mais simples de rodas. Simplesmente rotaciona
um registrador pra esquerda, jogando o conteúdo do flag Carry no bit 0, o bit 0
no bit 1, o bit 2 no bit 2... até o bit 7, que vai parar no carry. Como temos um
possível "vai um" no byte menos significativo, essa instrução vai fazer no byte
mais significativo uma multiplicação por 2 caso o valor do Carry for zero (não
houve "vai um") ou então fará uma multiplicação por 2 e em seguida somará 1,
caso o valor do Carry seja zero.
Com essas instruções, podemos implementar a multiplicação da seguinte forma:
;------ ; MULUU - Multiplica um número de 8 bits por outro de 8 bits (sem sinal) ; Entrada: L - Multiplicando ; A - Multiplicador ; Saída: HL - Produto ;------ MULUU: LD H,0 ; Zera H... HL agora contem o número a ser multiplicado LD D,H ; Zera DE LD E,H EX DE,HL ; Troca o conteudo de DE com HL (HL fica 0000 e DE fica o multiplicando) LD B,8 ; Repetir 8 vezes, uma vez pra cada bit MUUL: ; Verifica o bit mais baixo de A RRCA ; Joga bit mais baixo de A no flag Carry JR NC,MUUL2 ; Se era zero, não soma nada ao HL. ; Aqui se bit valia 1, então vai somar... ADD HL,DE MUUL2: ; Faz o Shift pra esquerda em DE (multiplica DE por 2) SLA E ; Shifta E pra direita, jogando bit mais significativo no Carry RL D ; Rotaciona D, colocando o bit do carry no bit 0 DJNZ MUUL ; Repete 8 vezes RET
Apesar da rotina ser bem maior, ela seguramente, no caso médio, é bem mais
rápida que a de somas simples, que fazia todas as somas. Essa rotina sempre vai
fazer 24 shifts e de 0 a 8 somas. A rotina de somas simples podia, em alguns
casos, realizar apenas algumas poucas somas. Mas para qualquer número maior que
32, por exemplo, esta rotina será mais eficiente.
Esta forma de raciocínio é extensível e permite realizar operações com bem
mais de 16 e 8 bits (quantos for desejado). Obviamente nestes casos é necessário
um pouco mais de controle sobre alguns flags, mas a idéia é basicamente a mesma.
Vale ressaltar que para alguns casos especiais, pode não ser interessante usar
essa rotina. Por exemplo, se voce sempre vai multiplicar por números entre 1 e
15, por exemplo, pode considerar uma rotina que conte só até o quarto bit,
mudando a
linha:
LD B,8 ; Repetir 8 vezes, uma vez pra cada bit
para:
LD B,4 ; Repetir 4 vezes, uma vez pra cada bit
Se a maioria das vezes a multiplicação é por números pequenos, mas às vezes o número é grande, talvez seja interessante trocar o DJNZ por algo um pouco diferente:
;------ ; MULUU - Multiplica um número de 8 bits por outro de 8 bits (sem sinal) ; Entrada: L - Multiplicando ; A - Multiplicador ; Saída: HL - Produto ;------ MULUU: LD H,0 ; Zera H... HL agora contem o número a ser multiplicado LD D,H ; Zera DE LD E,H EX DE,HL ; Troca o conteudo de DE com HL (HL fica 0000 e DE fica o multiplicando) MUUL: ; Verifica o bit mais baixo de A SRL A ; Joga bit mais baixo no flag Carry JR NC,MUUL2 ; Se era zero, não soma nada ao HL. ; Aqui se bit valia 1, então vai somar... ADD HL,DE MUUL2: ; Faz o Shift pra esquerda em DE (multiplica DE por 2) SLA E ; Shifta E pra direita, jogando bit mais significativo no Carry RL D ; Rotaciona D, colocando o bit do carry no bit 0 CP 0 ; Verifica se A virou zero JR NZ,MUUL ; Se A ainda não é zero, continua o loop RET
Note as diferenças importantes: primeiramente trocamos o RRCA pelo SRL. Por
que isso? Bem, a idéia desta segunda implementação é ir "limpando" o
multiplicador à medida que testamos seus bits, de forma que quando ele for zero
a rotina será finalizada, mesmo que nem todos os 8 bits tenham sido testados um
a um (se o multiplicador virou zero, significa que todos os bits restantes são
zero, e portanto não há mais nada a somar).
Ora, como eu disse, o RRCA deixa o Acumulador intocado (e ainda por cima faz o
favor de "sujá-lo" com o valor desconhecido do Carry durante as 8 rotações).
Obviamente esta instrução não serve para o nosso propósito. Precisamos de uma
instrução que vá jogando os bits do registrador no Carry, deslocando-os para a
direita, e inserindo um zero no bit 7. Pois é exatamente isso que a instrução
SRL faz:
SRL - Shift Right and cLear the most significant bit. Ou seja, desloca todos
os bits pra direita (bit 0 vai pro Carry, bit 1 vai pro bit 0... e assim por
diante... e o bit 7 ganha um bit de valor "0" nele). Assim, na pior hipótese,
após 8 instruções destas seguidas... o registrador sempre valerá zero.
Ora, mas então ... porquê eu não propus essa forma originalmente? Bem, vamos
analisar qual é o "overhead" adicionado com as instruções extras, por etapa da
operação:
Saiu TStates Entrou TStates RRCA 4+1 = 5 SRL 8+2 = 10 DJNZ 13+1 = 14 CP 0 + JR NZ 4+1 + 12+1 = 18
Ora, acrescentamos um overhead de (10-5)+(18-14) = 9 TStates em cada rodada do Loop. É preciso que esses 9 TStates por loop sejam compensados pela redução de rodadas. De fato, no melhor caso do loop "original" (quando o bit sendo testado é zero), o tempo de uma rodada do loop é SRL + JR NC (verdadeiro) + SLA + RL + DJNZ. O tempo disso é 10 + 13 + 10 + 10 + 18 = 61 T States. Como na versão modificada temos apenas 9 TStates a mais por loop, vejamos a tabela:
Bit mais alto TStates TStates Variação No multiplicador economizados gastos a mais 5 61*2 = 122 9*6 = 54 -68 6 61*1 = 61 9*7 = 63 2 7 61*0 = 0 9*8 = 72 72
Por essa conta simples fica fácil de ver que, quando tivermos um número de
até 6 bits, (multiplicadores de 0 a 63) não vale a pena usar a versão "original"
da rotina, é melhor usar a versão extendida (o ganho é tanto maior quanto menor
for o multiplicador, pois há uma economia de tempo cada vez maior). Para números
entre 64 e 127 o desempenho é praticamente o mesmo, sendo ligeiramente mais
rápido na versão original.
Para números maiores (de 128 a 255), a versão original é claramente mais
rápida, e portanto a versão "mexida" perde o sentido. Assim, é importante fazer
uma análise de qual é o problema que se está resolvendo e usar uma ou outra
rotina. Veja que embora a maior parte dos números (64 a 255) seja resolvido mais
rapidamente pela versão "original", os ganhos são brutais para multiplicadores
entre 0 e 63 na versão extendida.
Na dúvida de qual o escopo de soluções, talvez seja mais interessante ficar
com a versão original.
O programa desta aula, com a versão "original" (que sempre executa os 8 passos
do loop) da função tem a seguinte cara:
--- 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 LD L,082h ; Multiplicando LD A,005h ; Multiplicador CALL MULUU ; Chama multiplicacao ; HL já vem com o valor da reposta LD DE,NUMERO ; Indica posição do texto CALL HEXW ; Converte para ASCII LD DE,NUMERO ; Aponta número CALL MOSTRATXT ; Imprime JP 0 ; Volta ao MSX-DOS NOMEDOPRG: DB 'Programa 6a - Multiplicando Numeros.',13,10,'$' AUTOR: DB ' Por Daniel Caetano',13,10,10,'$' NUMERO: DB 0,0,0,0,0 PULALINHA: DB 13,10,'$' ;------ ; MULUU - Multiplica um número de 8 bits por outro de 8 bits (sem sinal) ; Entrada: L - Multiplicando ; A - Multiplicador ; Saída: HL - Produto ;------ MULUU: LD H,0 ; Zera H... HL agora contem o número a ser multiplicado LD D,H ; Zera DE LD E,H EX DE,HL ; Troca o conteudo de DE com HL (HL fica 0000 e DE fica o multiplicando) LD B,8 ; Repetir 8 vezes, uma vez pra cada bit MUUL: ; Verifica o bit mais baixo de A RRCA ; Joga bit mais baixo de A no flag Carry JR NC,MUUL2 ; Se era zero, não soma nada ao HL. ; Aqui se bit valia 1, então vai somar... ADD HL,DE MUUL2: ; Faz o Shift pra esquerda em DE (multiplica DE por 2) SLA E ; Shifta E pra direita, jogando bit mais significativo no Carry RL D ; Rotaciona D, colocando o bit do carry no bit 0 DJNZ MUUL ; Repete 8 vezes RET ;------ ; 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 ---
Entretanto, essas são sempre as melhores soluções? De forma alguma. Essas
são ótimas soluções para cálculos mais "genéricos", por assim dizer... quando de
antemão você não sabe exatamente o tipo de multiplicação que vai realizar.
Por exemplo: tem um jeito muito mais rápido e eficiente de realizar
multiplicações quando se tratam de potências de dois, que é simplesmente fazer o
número de shifts adequados, sem loops, sem toda essa parafernalha. Aliás, uma
boa idéia, quando a velocidade é algo realmente fundamental, é abrir esses
loops, ou seja: tirar fora o DJNZ ou o JR NZ... e simplesmente copiar 8 vezes a
rotina. Com isso o ganho é razoável, já que cada um desses DJNZs ou CP 0 + JR NZ
levam respectivamente 14 T States e 18 T States para serem executados. (^=
Uma outra aproximação é usar uma tabela pré-fixada, mas essa solução só é
viável quando a multiplicação é sempre por um número fixo. Cria-se uma tabela
com as respostas na seguinte forma:
MULTTBL: DW RESULTADO0 ; N*0 DW RESULTADO1 ; N*1 DW RESULTADO2 ; N*2 (...) DW RESULTADO255 ; N*255
E para o cálculo da multiplicação a nossa função fica:
;------ ; MULNU - Multiplica N por um número de 8 bits ; Entrada: A - Multiplicador ; Saída: HL - Produto ;------ MULNU: LD L,A ; Coloca multiplicador em HL LD H,0 ADD HL,HL ; Multiplica por 2 (offset da tabela) LD DE,MULTTBL ; Coloca endereço base da tabela em DE ADD HL,DE ; Calcula posição da memória que contém a solução, em HL LD A,(HL) ; Pega solução em HL INC HL LD H,(HL) LD L,A RET MULTTBL: DW RESULTADO0 ; N*0 DW RESULTADO1 ; N*1 DW RESULTADO2 ; N*2 (...) DW RESULTADO255 ; N*255
Essa é uma solução de alto desempenho, mas gasta uma grande quantidade de
memória. Essa é, aliás, uma das grandes dicotomias do ASM. Muitas vezes para
acelerar você é obrigado a gastar mais espaço, e muitas vezes para economizar
espaço você deixa as coisas mais lentas.
Note, entretanto, que isso não é uma regra sem exceção. Códigos mal planejados
tornam-se maiores e mais lentos do que seria necessário. Nem sempre um código
enorme é sinônimo de rápido e nem sempre um código pequeno é sinônimo de lento.
Como sempre... há casos e casos.
Na próxima aula trataremos então as divisões no Z80. Até lá!
Um abraço e até a próxima!
Daniel Caetano.
PS: Sugestões e comentários são bem-vindos. (^=