Sítio do Piropo

B. Piropo

< Coluna em Fórum PCs >
Volte
13/02/2006

< Computadores XXIV: Instruções >


Já conhecemos bastante sobre o funcionamento de uma UCP. Já vimos como é feita a leitura e escrita na memória principal . Já aprendemos quais são seus principais componentes e já vimos até mesmo uma UCP elementar em pleno funcionamento. Sabemos que um programa é um conjunto encadeado de instruções com um propósito determinado. E que instruções nada mais são que números, expressos em binário que, ao serem “decodificados”, fazem com que a UCP execute determinadas tarefas. Já vimos até mesmo no que consiste a “decodificação” de uma instrução. O que nos falta para efetivamente entender o funcionamento de uma UCP?
Falta saber como interpretar instruções, conhecer seu “formato”, entender o que fazem e como fazem. Ou seja: faltam noções sobre o meio de programar um computador de tal modo que as instruções (e seus operandos) sejam fornecidas ao computador sob a forma de números binários, a única “linguagem” que a máquina “entende” e que, por isso mesmo, é chamada de “linguagem de máquina”.
Como sabemos cada microprocessador tem seu conjunto de instruções (“instruction set”), um repertório das ações que é capaz de executar. Na verdade, o primeiro passo no projeto de um microprocessador é justamente determinar o que se espera dele, que tarefas poderá executar. E isso é feito definindo seu conjunto de instruções (sempre tendo em mente que quanto mais complexo for este conjunto, ou seja, quanto mais “coisas” esperarmos que nosso microprocessador faça, mais complexa será sua implementação, portanto terá maior número de transistores em sua estrutura de portas lógicas interligadas).
Então vamos continuar seguindo a norma que tem nos orientado até agora: conceber uma unidade muito simples, porém perfeitamente funcional, e destrinchar seu funcionamento para que possamos extrapolar esse conhecimento e entender como funcionam as unidades mais complexas.
Tipos de instruções
Microprocessadores podem ter instruções extremamente poderosas. O nosso, certamente, não será assim. Seu conjunto de instruções será muito próximo do mínimo indispensável. E qual é o mínimo indispensável?
Bem, todo microprocessador deve ter instruções que permitam receber do mundo exterior os dados a serem processados e enviar ao mundo exterior os resultados do processamento. São as instruções de entrada e saída.
Além destas, considerando que todas as instruções que formam o programa e muitos dos dados utilizados no processamento são lidos e escritos na memória principal (MP), é preciso que o conjunto de instruções contenha algumas destinadas a movimentar dados entre a MP e seus registradores internos. São as instruções de movimentação de dados.
O processamento de dados é feito principalmente através da execução de operações matemáticas ou lógicas. Então todo microprocessador deve dispor de algumas instruções capazes de executar tais operações. São as instruções matemáticas ou lógicas.
Finalmente, considerando que durante a execução de um programa pode haver desvios de fluxo (alteração da ordem em que as instruções são executadas), são necessárias instruções capazes de efetuar esses desvios. São as instruções de controle de fluxo.
Evidentemente pode haver muitas outras. Mas estes quatro grupos são os fundamentais. Então o conjunto de instruções de nosso microprocessador será formado apenas por eles.
Formato das instruções
Isto posto, precisamos decidir qual o “formato” de nossas instruções, ou seja, o que representam os bits que formam os números binários que constituem uma instrução (pois como sabemos toda instrução é representada por um número binário).
Há basicamente dois tipos de instruções: as que necessitam e as que não necessitam de operandos. E, entre estas últimas, as que necessitam de um ou mais operandos. Vamos esclarecer isso melhor.
Tomemos como exemplo uma instrução obrigatória em todo conjunto de instruções: a usada para indicar que o programa acabou e que o controle deve ser devolvido ao sistema operacional. Ou seja: aquela usada no final de todo programa. Tudo o que ela faz é informar ao microprocessador que o programa terminou. Ela se completa por si mesma, não precisa de mais nenhum parâmetro. É, portanto, uma instrução sem operandos. A instrução inteira consiste apenas no seu número, ou seja, no código em binário que a representa, o “código da instrução”.
Vejamos agora uma instrução de desvio de fluxo. Ela muda o fluxo do programa e, como vimos na coluna “Ciclo de busca e execução – Final” faz isso de forma muito simples: alterando o conteúdo do Ponteiro de Instruções (PI), o registrador que contém o endereço da próxima instrução a ser executada, alteração que consiste na substituição do seu conteúdo pelo endereço da posição de memória que contém a nova instrução. De onde se conclui que uma instrução de desvio de fluxo necessita de um único parâmetro: o endereço de MP para onde o fluxo será desviado. Uma instrução deste tipo consistirá então do código da instrução seguido deste único operando. Trata-se portanto de uma instrução que só requer um operando (que, no caso particular da instrução de desvio, é um endereço mas que, dependendo da finalidade da instrução, pode ser qualquer outro parâmetro).
Há, por outro lado, instruções que necessitam de mais de um operando. Por exemplo: a escrita de um valor em uma posição da memória principal geralmente necessita de dois, já que além do código da instrução deve conter o valor a ser escrito e o endereço da posição da MP onde a escrita deve ser feita. Instruções deste tipo consistem do código da instrução seguido de dois operandos (no exemplo, um valor e um endereço mas, novamente, dependendo da finalidade da instrução, podem ser quaisquer outros parâmetros).
Há ainda instruções que necessitam de três parâmetros. Por exemplo: uma instrução razoavelmente complexa como aquela que copia todo um conjunto de bytes de um trecho da MP para outro. Essa instrução, além do código da instrução, necessita de três parâmetros: o endereço da primeira posição de memória do trecho cujo conteúdo deverá ser copiado, o endereço da primeira posição de memória do trecho para onde os valores serão copiados e o número de posições de memória cujos conteúdos serão copiados para o outro trecho. Uma instrução deste tipo consistirá do código da instrução seguido de três parâmetros (no exemplo, dois endereços e um valor mas que, repito, dependendo da finalidade da instrução, podem ser quaisquer outros parâmetros).
Considerando o exposto, é fácil concluir que muitas UCPs aceitam instruções de “tamanho” variável, dependendo do número de bits usados para codificar a instrução e do número e tamanho (número de bits) dos parâmetros que cada instrução utiliza.
Formato das instruções simplificadas
No nosso caso, como buscamos a simplicidade, iremos estabelecer que o código da instrução de nossa UCP será de tamanho fixo e que ela usarà no máximo um único operando (ou seja: usará um ou nenhum operando). Vejamos como implementar essa simplificação.
As instruções de entrada e saída de nossa UCP serão apenas duas: uma que lê o conteúdo do dispositivo de entrada padrão (geralmente o teclado) e o copia no registrador denominado Acumulador (ACC) e outra que lê o conteúdo do ACC e o escreve no dispositivo de saída padrão (geralmente vídeo ou impressora). Como só há uma entrada padrão, uma saída padrão e um Acumulador, elas não necessitam de qualquer parâmetro: o que entra, sempre entra pela entrada padrão e vai para o Acumulador, o que sai sempre sai do acumulador e vai para a saída padrão. Portanto a instrução toda se resume em informar que algo entra ou que algo sai, dispensando a necessidade de acrescentar de onde e para onde.
As instruções de controle podem interromper (parar) o programa em seu final ou desviar seu fluxo alterando o endereço da próxima instrução a ser executada. A primeira não precisa de operando: simplesmente interrompe a execução e devolve o controle para o sistema operacional (é, portanto, uma instrução sem operandos, formada apenas pelo código da instrução). Porém o mesmo não ocorre com as demais, que desviam o fluxo do programa fazendo-o “saltar” para uma determinada instrução: todas elas precisam de um operando, o endereço da posição de memória que contém a próxima instrução a ser executada, representando assim o destino do “salto” que o programa fará. A execução da instrução consistirá apenas em escrever este endereço no PI, o que fará o programa saltar para lá imediatamente. Então toda instrução de desvio de fluxo consistirá do código da instrução seguido apenas de um parâmetro: o endereço a ser escrito no PI.
As instruções de movimentação de dados são basicamente as que fazem leitura e escrita na memória principal. Que, como vimos acima, costumam necessitar de mais de um parâmetro. Mas ns nossa UCP introduziremos uma simplificação fazendo com que no caso da escrita o valor a ser escrito seja sempre aquele contido no ACC e no caso da leitura o valor lido seja sempre encaminhado ao ACC. Portanto a instrução de leitura simplesmente copia o conteúdo de uma posição da MP no ACC e a de escrita copia em uma posição de MP o conteúdo do ACC. Logo, ambas usarão apenas um operando, o endereço da posição da MP onde o dado será lido ou escrito. Além dessas, no nosso conjunto de instruções, haverá apenas mais uma instrução de movimentação de dados: a que escreve um valor (expresso sob a forma de um número binário) no ACC. De todas as instruções com parâmetro que nossa UCP usará, esta será a única cujo operando não será um endereço de MP mas sim o próprio valor a ser escrito no ACC. E, para facilitar as coisas, determinaremos que o valor a ser escrito no ACC não poderá ser maior que o maior endereço de memória principal (essa é uma limitação arbitrária que estamos fazendo apenas à guisa de simplificação). Portanto também as instruções de movimentação de dados consistirão do código da instrução seguido de um único parâmetro que será um endereço da MP ou um valor de mesmo “tamanho” (número de bits) que um endereço da MP.
Restam as instruções matemáticas e lógicas, que a princípio deveriam oferecer maior dificuldade para nossa simplificação já que a maioria das operações exigem pelo menos dois operandos (parcelas na soma, minuendo e subtraendo na subtração, fatores no produto, dividendo e divisor na divisão e assim por diante). Então, a instrução que manda executar, por exemplo, a soma, deveria em princípio conter dois operandos (as duas parcelas a serem somadas). Mas isso faz as coisas ficarem muito complicadas. Então vamos simplificar: nossa UCP só será capaz de efetuar operações deste tipo quando um dos operandos estiver no ACC e o outro em uma posição qualquer da MP. E, ainda por amor à simplificação, o resultado será sempre escrito no ACC. Por exemplo: a instrução que efetua a soma usará sempre como parcelas o conteúdo do ACC e o de uma posição de memória (da qual deverá ser fornecido o endereço) e a soma será escrita sempre no ACC, sobrescrevendo uma das parcelas. Da mesma forma: a divisão será feita sempre dividindo o dividendo armazenado no ACC pelo divisor armazenado em uma posição de MP (da qual deverá ser fornecido o endereço) e o quociente será depositado no ACC sobrescrevendo o dividendo. E multiplicações e subtrações serão feitas de forma análoga. Isso torna nossa programação um pouco mais difícil obrigando sempre a transcrever um dos operandos no ACC antes de comandar a operação mas em contrapartida  faz com que só seja necessário fornecer um único parâmetro, o endereço da posição da MP que contém o outro operando, já que o primeiro estará sempre no ACC, onde também será escrito o resultado. Com essa simplificação, todas as nossas instruções matemáticas serão constituídas do código da instrução seguido apenas de um operando, que será sempre um endereço da posição da MP.
Pronto: agora já sabemos que nossas instruções ou não terão operandos ou terão apenas um operando. E que este operando será sempre um endereço da MP ou um valor expresso no mesmo número de bits que o maior endereço da MP.
Tamanho das instruções simplificadas
Agora que conhecemos seu formato, já estamos habilitados a determinar o tamanho de nossas instruções. Como nenhuma delas usará mais de um parâmetro, todas podem ser decompostas apenas em duas partes, a primeira contendo a instrução propriamente dita (o número a ser decodificado ou o “código da instrução”) e a segunda contendo o único operando (ou sendo preenchida com zeros no caso das instruções que prescindem de operando).
De que tamanho será cada parte?
Bem, o tamanho da primeira parte depende da quantidade de instruções que a UCP pode executar, ou seja, do tamanho de seu conjunto de instruções. Vamos estabelecer que nossa UCP, simples como é, não precisará executar mais que dezesseis diferentes instruções (e, apesar disso, como veremos adiante, será perfeitamente funcional e poderá executar programas de razoável nível de dificuldade). Como os números de zero a quinze em binário podem ser expressos em apenas quatro bits, a parte que contém a instrução propriamente dita (ou o “código da instrução”) terá quatro bits.
E a outra?
Bem, a segunda parte é destinada a conter o operando. E todas as nossas instruções simplificadas têm como operando um endereço de MP ou um valor que tenha o mesmo número de bits que um endereço de MP. Portanto o que vai determinar o tamanho desta parte é o tamanho da memória, ou seja, o maior endereço da MP. Vamos fixar o tamanho máximo de nossa memória principal em 256 posições de memória. Então, o maior endereço de memória será 255. Um número que, expresso em binário, ocupa oito bits. Portanto a segunda parte de nossa instrução terá oito bits.
Logo, a instrução inteira terá doze bits. Os quatro de ordem mais elevada (ou seja, os quatro da esquerda) representarão a instrução propriamente dita (serão o “código da instrução”), enquanto os oito bits de ordem menos elevada (os oito bits da direita) representarão o operando. O formato de nossa instrução será então: “iiiixxxxxxxx”, onde “iiii” são os quatro bits que representam a instrução e “xxxxxxxx” os oito bits que representam o operando (que, não esqueça, caso a instrução não tenha operando serão preenchidos com zeros, conferindo-lhes o formato “iiii00000000”).
Isto facilita bastante o projeto da unidade de controle de nossa UCP que, ao receber uma instrução do RI (veja a coluna “Busca e execução – Final”) precisará apenas isolar os quatro bits de ordem mais elevada para decodificá-los, usando os demais como operando da instrução.
Características da UCP simples
Agora, que conhecemos o formato de nossa instrução, já podemos concluir o projeto de nosso microprocessador. Que terá o aspecto da Figura 1.

FIGURA 1: Diagrama da UCP simplificada


Toda UCP tem que ter pelo menos cinco registradores: o Registrador de Instruções (RI, que como sabemos da coluna acima citada contém a instrução que está sendo executada em um determinado momento), o Ponteiro de Instruções (PI, que como vimos na mesma coluna contém o endereço da posição da MP onde está armazenada a próxima instrução a ser executada) e o Acumulador (ACC), o registrador de uso geral, além dos imprescindíveis Registrador de Dados da Memória (RDM) e Registrador de Endereços da Memória (REM; veja a coluna Leitura e Escrita na MP). A nossa UCP, simples como é, terá apenas estes cinco registradores obrigatórios: ACC, RI, PI, RDM e REM. Terá ainda uma unidade de controle com um decodificador de quatro entradas (todos os códigos de instrução terão quatro bits) e uma ULA capaz de executar as quatro operações fundamentais: soma, subtração, multiplicação e divisão.
Nossas instruções terão sempre doze bits. Portanto o RI, Registrador de Instruções, destinado a conter instruções, será também de 12 bits para que a instrução “caiba” nele. Assim como o RDM, já que os valores que representam instruções terão de passar por ele quando forem lidos da MP. E como, para serem executadas, as instruções são obrigatoriamente lidas na MP, as posições da memória principal de nosso sistema deverão igualmente ter doze bits. E levando em conta que as operações de leitura e escrita na MP são feitas com a participação do ACC, também este registrador deverá ter 12 bits, pois nele deve caber qualquer valor lido de uma posição da MP. Então as posições da MP, o RI, o RDM e o ACC terão “largura” de doze bits (e, incidentalmente, por razões óbvias o barramento de dados de nosso sistema deverá ter doze linhas).
Restam o PI, o Ponteiro de instruções, e o REM, Registrador de Endereços da Memória. Ambos são destinados a conter endereços. Seu tamanho deverá então comportar o maior endereço a ser escrito, o endereço da posição de memória mais “alta’. Ou seja: aquele que determina a capacidade da memória. No nosso caso, para fins de simplificação, determinamos que a memória principal de nosso sistema comporta um máximo de 256 posições. O endereço da posição mais “alta” será então 255 em decimal (o da mais baixa, como sabemos, será “zero”). Um número que quando expresso em binário ocupa oito bits. Portanto, tanto o PI quando o RDM, registradores destinados a conter endereços, terão apenas oito bits (o que faz com que o barramento de endereços de nosso sistema tenha apenas oito linhas).
Agora já temos uma boa idéia de como será nossa CPU. Terá os seguintes registradores: RI, RDM, ACC (todos de doze bits), PI e REM (estes últimos de oito bits). Será capaz de acessar uma MP de 256 posições cada uma com doze bits de “largura” e poderá executar dezesseis diferentes instruções, cujo formato será de 12 bits, os quatro de ordem mais elevada correspondendo à instrução propriamente dita e os oito de ordem menos elevada correspondendo ao único operando que cada instrução pode suportar. E seu decodificador terá apenas quatro entradas (porque todos os códigos de instrução terão quatro bits). Uma UCP simples, mas bastante eficaz, como veremos.
Só falta agora escolher uma a uma as instruções que comporão seu conjunto de instruções.
Nossa tarefa para a próxima coluna.

B. Piropo