Relatório de Microprocessadores 2007/2008 Engenharia Física Tecnológica PROGRAMAÇÃO DE UM MICROPROCESSADOR EM C E ASSEMBLY PARA CONTROLO DE UM LED Laboratório I Trabalho realizado por: André Cunha, nº53757 João Pereira, nº 55315 Grupo 3; 5ªfeira 13:00-16:00h Lisboa, 22 de Setembro de 2007
Introdução e Objectivos O objectivo inicial deste trabalho laboratorial consiste em controlar um led de duas formas distintas. A primeira consiste em acender e apagar o led conforme a posição de um interruptor de pressão, a segunda forma é em tudo idêntica, mas quando premido o interruptor, o led deverá estar intermitente. Deverão ainda, ser feitas algumas considerações acerca dos vários métodos alternativos de implementação. Implementação e Procedimento A implementação deverá ser atingida, nos dois casos (led aceso e led intermitente), de duas formas distintas: primeiro programando o microprocessador usado directamente em código máquina (Assembly) e depois utilizando a linguagem de programação C. 1ª sessão de laboratório Material utilizado: MPLAB IDE SDK PIC184550 Começando então pela primeira implementação do primeiro objectivo, controlar o led através de um botão de pressão utilizando código máquina para programar directamente o microprocessador, começou-se por esboçar num fluxograma o algoritmo inerente ao trabalho. Figura 1 Fluxograma que traduz as rotinas executadas em código máquina para o 1º objectivo
Explicando brevemente, assim que entramos na rotina principal, começamos por verificar se o interruptor (switch) está premido e caso o esteja saltamos a instrução seguinte (que nos envia para o início) e vamos para a instrução que acende o led, caso contrário, ficamos presos na verificação do switch até que este seja pressionado e o led permanece apagado (notar que se assume que por default, que o led se encontra apagado). De seguida entramos na primeira subrotina, verificamos agora se o switch não está premido e caso não o esteja, saltamos a instrução seguinte (que nos envia para o início da subrotina) para a instrução que apaga o led, caso contrário, ficamos presos na verificação do switch até que este deixe de ser pressionado e o led permanece aceso. Dada a pequena dimensão e fraca complexidade do programa, torna-se conveniente introduzi-lo (micro1_asm.asm): ;define-se o registo PORTA com o endereço correspondente disponível na documentação ;PORTA corresponde ao interruptor usado para controlar o led porta equ 0xf80 ;faz-se o mesmo para o registo PORTB ;PORTB corresponde ao led propriamente dito portb equ 0xf81 ;define-se o bit que vai corresponder ao interruptor que se pretende usar ;vamos usar o bit 4 que corresponde ao interruptor RA4 switch equ 4 ;define-se o bit que vai corresponder ao led que se pretende usar ;vamos usar o bit 0 que corresponde ao led RB0 led equ 0 ;definição do registo equivalente a TRISB como expresso na documentação trisb equ 0xf93 ;uma vez que por definição, podemos sempre ler os registos usados, basta-nos ;impôr o comportamento de output relativamente ao bit "led" do registo PORTB ;que controla o comportamento do led ;TRISB vai permitir que usemos PORTB como um output, condição necessária ;para acender o led bcf trisb, led ;definição da rotina principal do programa num infinite loop ;verifica se o switch está on (a 0) ;caso esteja on, salta a instrução seguinte, (saindo do loop) e vai para ;a instrução que acende o led ;caso contrário, permanece em loop infinito até que alguém carregue main btfsc porta,switch ;instrução que fecha o ciclo e aponta para o início da rotina principal goto main ;põe o PORTB a 1 e como tal, acende o led bsf portb,led
;verifica se o switch está off (a 1) ;caso esteja off, salta a instrução seguinte (saindo do loop) e vai para ;a instrução que apaga o led ;caso contrário, permanece em loop infinito até que deixem de carregar no ;botão lp1 btfss porta,switch ;instrução que fecha o ciclo e aponta para o início da primeira subrotina goto lp1 ;põe o PORTB a 0 e como tal, apaga o led bcf portb,led ;instrução que fecha o ciclo e aponta para o início da rotina principal goto main ;fim do programa end E concluíu-se aqui a primeira fase do primeiro objectivo. 2ª sessão de laboratório MPLAB IDE e compilador de C para este IDE SDK PIC184550 Prosseguindo agora para a segunda fase do primeiro objectivo, controlar o led através de um interruptor de pressão utilizando um microprocessador mas utilizando uma linguagem de programação, o C. Seguindo a mesma trajectória que na sessão anterior, construiu-se um pequeno fluxograma que traduz o comportamento que se deseja implementar, agora em C. Figura 2 Fluxograma que traduz as rotinas executadas em C para o 1º objectivo
É óbvia, ainda antes de examinar o fluxograma, a maior simplicidade aparente do algoritmo em relação ao Assembly, simplicidade essa que advém de estarmos a utilizar no C, operações complexas que na realidade correspondem a um conjunto de várias operações elementares como veremos mais à frente. A explicação do algoritmo é brevíssima, entramos na rotina principal definida num loop infinito e verificamos se o switch está on ou off. Caso esteja on, o led acende, caso esteja off, o led apaga. Mais uma vez, torna-se conveniente introduzir o código onde é explícita a maior simplicidade para o programador em relação ao código máquina (ficheiro micro1_c.c): //inclusão da biblioteca necessária para controlo do microprocessador PIC184550 #include <p18f4550.h> //declaração da rotina principal void main() //permite o output a partir de PORTB, //que é o porto que corresponde ao led TRISBbits.TRISB0 = 0; //criamos um infinite loop que em cada iterada //vai verificar o estado do interruptor while(1) //verifica que se o PORTA, correspondente ao interruptor, //está activo (ou seja, a 0, notar lógica invertida por causa da natureza //do circuito) if(!portabits.ra4) //caso esteja activo, o PORTB, correspondente ao led, //vai a 1 e o led acende PORTBbits.RB0 = 1; //caso contrário e o PORTA, correspondente ao interruptor, //esteja a 1, ou seja, desactivado else //o PORTB, correspondente ao led, vai a 0 e o led apaga PORTBbits.RB0 = 0; Explicitando a diferença neste caso particular, entre o C e o Assembly, note-se que enquanto que o C permite verificar o estado (on ou off) do interruptor, e em função disso, decidir de imediato se acende ou apaga o led, no Assembly tivemos que utilizar comandos específicos para verificação do estado on e verificação do estado off, exigindo naturalmente mais código. Passemos então ao segundo objectivo do trabalho laboratorial, em tudo idêntico ao primeiro excepto que agora o led deverá ser intermitente. Uma vez que possuímos as bases adquiridas na execução do primeiro objectivo, podemos realizar o segundo recorrendo-lhes. No caso do C, basta-nos acrescentar uma linha de código (ver linha highlighted) em que mandamos o led apagar logo a seguir de o mandarmos acender (micro1i_c.c):
//inclusão da biblioteca necessária para controlo do microprocessador PIC184550 #include <p18f4550.h> //declaração da rotina principal void main() //permite o output a partir de PORTB, //que é o porto que corresponde ao led TRISBbits.TRISB0 = 0; //criamos um infinite loop que em cada iterada //vai verificar o estado do interruptor while(1) //verifica que se o PORTA, correspondente ao interruptor, //está activo (ou seja, a 0, notar lógica invertida por causa da natureza //do circuito) if(!portabits.ra4) //caso esteja activo, o PORTB, correspondente ao led, //vai a 1 e o led acende PORTBbits.RB0 = 1; //e de seguida, vai imediatamente a 0 e o led apaga PORTBbits.RB0 = 0; //caso contrário e o PORTA, correspondente ao interruptor, //esteja a 1, ou seja, desactivado else //o PORTB, correspondente ao led, vai a 0 e o led apaga PORTBbits.RB0 = 0; Relativamente ao Assembly, a intermitência permite uma simplificação do programa realizado para cumprir o primeiro objectivo. Esboçando um fluxograma: Figura 3 Fluxograma que traduz as rotinas executadas em código máquina para o 2º objectivo
Como já se referiu, a maior extensão do algoritmo no caso particular do primeiro objectivo, deve-se à restrição de apenas se poderem analisar os estados especificamente ou para 0 ou para 1, tendo por isso, que o fazer para ambos os estados quando queremos saber se um dado parâmetro (led ou switch) está activo ou inactivo. Assim sendo, se soubermos a todo o momento em que estado está um dado parâmetro, não precisamos de o verificar. Desta forma, com a intermitência podemos assegurar o led a 0 no princípio (por default está off) e no fim de cada ciclo realizado (micro1i_asm.asm): ;define-se o registo PORTA com o endereço correspondente disponível na documentação ;PORTA corresponde ao interruptor usado para controlar o led porta equ 0xf80 ;faz-se o mesmo para o registo PORTB ;PORTB corresponde ao led propriamente dito portb equ 0xf81 ;define-se o bit que vai corresponder ao interruptor que se pretende usar ;vamos usar o bit 4 que corresponde ao interruptor RA4 switch equ 4 ;define-se o bit que vai corresponder ao led que se pretende usar ;vamos usar o bit 0 que corresponde ao led RB0 led equ 0 ;definição do registo equivalente a TRISB trisb equ 0xf93 ;uma vez que por definição, podemos sempre ler os registos usados, basta-nos ;impôr o comportamento de output relativamente ao bit "led" do registo PORTB ;que controla o comportamento do led ;TRISB vai permitir que usemos PORTB como um output, condição necessária ;para acender o led bcf trisb, led ;definição da rotina principal do programa num infinite loop responsável ;pela intermitência do led ;verifica se o switch está off (a 1) ;caso esteja off, salta a instrução seguinte (instrução essa que acende o ;led)e salta para a instrução que apaga o led ;caso contrário, acende o led antes de o apagar na instrução seguinte main btfss porta,switch ;põe o PORTB a 1 e como tal, acende o led bsf portb,led ;põe o PORTB a 0 e como tal, apaga o led bcf portb,led ;regressa ao início da rotina garantindo assim, o loop goto main ;fim do programa end
De forma a poder fazer algumas considerações sobre o Assembly e uma linguagem que permite comunicar artificialmente com a máquina, neste caso, o C, ligou-se um dos terminais do led ao osciloscópio, para poder assim medir o tempo de execução de cada ciclo de intermitência em ambos os casos (C e Assembly). Obtiveram-se as seguintes imagens: Figura 4 Ciclo de intermitência para o caso do Assembly ( T = 5µs) Figura 5 Ciclo de intermitência para o caso do C ( T = 8µs)
Tendo em conta esta discrepância entre a duração do ciclo em C e no Assembly, torna-se pertinente perceber o porquê do tempo maior do programa em C. Sabendo a duração de cada instrução executada no ciclo, podemos fazer dismantle do código de C para perceber que operações foram de facto realizadas em código máquina. No caso do Assembly, não há mistérios, realizamos 3 operações de 1 ciclo de relógio mais uma operação goto que ocupa 2 ciclos de relógio, obtendo assim 5 ciclos de relógio que equivale a 5µs. Depois de se fazer o dismantle do programa em C e se procurar pelas operações que faziam sentido no contexto do programa entre todo o código gerado pelo IDE utilizado, encontrou-se a seguinte sequência de código máquina correspondente ao ciclo gerado: ;verifica se o interruptor está on (a 0) ;se estiver salta para a instrução que acende o led, caso contrário passa para a ;instrução seguinte e permanece em loop infinito 00E8 BTFSC 0xf80, 0x4, ACCESS 2 ciclos de relógio ;o mesmo que um goto, regressa à verificação do switch 00EA BRA 0xf2 ;acende o led 00EC BSF 0xf81, 0, ACCESS 1 ciclo de relógio ;apaga o led 00EE BCF 0xf81, 0, ACCESS 1 ciclo de relógio ;salta a instrução seguinte 00F0 BRA 0xf4 2 ciclos de relógio ;apaga o led 00F2 BCF 0xf81, 0, ACCESS ;regressa ao início 00F4 BRA 0xe8 2 ciclos de relógio Depois de identificadas (em highlight) as operações realizadas no loop quando o led está intermitente, basta contar os ciclos de relógio. O botão está premido, por isso a primeira operação vai ocupar 2 ciclos de relógio, as 2 operações de branch (realizam sesivelmente o mesmo que goto) ocupam 2 ciclos de relógio cada uma e as 2 operações restantes ocupam 1 ciclo de relógio cada. No total vamos ter 8 ciclos de relógio, ou seja, 8µs. Comentários e Conclusões Tendo em conta as notas explícitadas ao longo do capítulo anterior, não há muito mais a acrescentar, ainda assim podemos dizer o seguinte: Apesar da diferente forma com que se apresentam aos olhos do operador a nível estrutural, percebemos de imediato que existe uma equivalência entre o Assembly e o C. Curiosamente ou não, é claramente visível que o C, apesar de ser uma linguagem de chamada de alto nível, é ainda muito próxima do código máquina executado
microprocessador. Esta evidência pode ser cosubstanciada precisamente ao analisarmos alguns aspectos na programação da intermitência do led. Apesar de usarmos C e Assembly, ao compararmos o tempo de duração do ciclo em cada um dos casos utlizando o osciloscópio, obtemos apenas uma diferença de 3µs explícitada pelo próprio dismantle do código em C se revela bastante eficiente (embora exista aqui uma clara dependência do compilador usado). Apesar destas conclusões, temos sempre optimizações e refinamentos que podem ser obtidos através do Assembly e não se conseguem fazer em C. Ainda assim, tendo em conta o aumento da complexidade que se tem no Assembly em relação ao C para implementar algoritmos relativamente simples, esses refinamentos parecem ser muitas vezes pouco rentáveis quando comparados com o esforço e tempo dispendidos. A título adicional, é ainda importante referir que o algoritmo em Assembly poderia ter sido optimizado através do comando btg que faz a operação de bit toggle numa só operação em vez de duas e que a utilização do comando branche é possivelmente mais eficiente que o go to. De notar que a construção física das portas lógicas usadas no microprocessador em questão, impede a utilização do PORTB como se usou na implementação escolhida. Ter-se-ia que utilizar o LATB. Em PORTB, o estado anterior é invariante e como tal isso impossibilita que o toggle seja realizado de todo. Já em LATB tem-se o estado anterior correcto necessário para fazer o toggle.