Programação Assembly para Microcontroladores
Parece ser um pouco de contra-senso escrever sobre programação assembly, quando a maioria dos desenvolvedores de sistemas embutidos está interessada em usar linguagem C em seus projetos. Mas na verdade, quem trabalha com micro-controladores deve ter bom domínio de assembly e de como tirar proveito de suas características para aperfeiçoar as regiões críticas de código.
Parece ser um pouco de contra-senso escrever sobre programação
assembly, quando a maioria dos desenvolvedores de sistemas
embutidos está interessada em usar linguagem C em seus projetos. Mas na
verdade, quem trabalha com micro-controladores deve ter bom domínio de
assembly e de como tirar proveito de suas características para
aperfeiçoar as regiões críticas de código.
Um programa de computador pode ser representado de diversas formas.
Linguagens de alto nível, como C, C++ e Java, dão uma maior capacidade
de abstração, recursos computacionais e independência do
hardware ao programador. Programas escritos em assembly
tendem a ser menores e mais rápidos do que os escritos em linguagens de
alto nível, entretanto são dependentes de plataforma e difíceis de
depurar e dar manutenção.
O processador só entende a linguagem de máquina específica de sua arquitetura, que é representada pelos seus códigos de operação (opcodes) no formato binário. Seria extremamente tedioso, e até mesmo desumano, escrever programas utilizando os opcodes, por isso usamos uma representação simbólica da linguagem de máquina: o assembly. Um programa especial chamado assemblador trata os símbolos e os transformam em código binário, tratando ainda da alocação da memória de programa e de dados.
Neste artigo, iremos estudar a estrutura da linguagem assembly de uma das famílias de micro-controladores mais usadas no mundo a PICmicro MIDRANGE da Microchip, também conhecidos como PIC16CXXX.
Arquitetura PIC16
Como dito antes, o assembly é a representação simbólica da linguagem de máquina. Então, antes de aprendermos a programar em assembly, devemos entender como funciona o processador em questão. Questões como tamanho da memória e sua organização, modos de endereçamento, registradores especiais e o próprio conjunto de instruções precisam ser bem estudadas.Os “PIC16” são micro-controladores de arquitetura RISC ( Reduced Instruction Set Computer). Esta arquitetura permite que o micro-controlador tenha um conjunto de instruções mais simples e de menor número (apenas 35), dois formatos de instrução apenas (uma para instruções que manipulam bits e outras para bytes), execução de uma instrução por ciclo de máquina, pipeline de execução e busca de instruções, e amplo número de registradores de uso geral.
Essa família possui uma arquitetura baseada em acumulador (o registrador W), todas as instruções lógicas e aritméticas são referenciadas à ele. Os registradores são divididos em duas categorias: especiais (SFR) e de propósitos geral (GPR). A maior parte dos SFRs são usados para configurar e usar os diversos periféricos disponíveis. O registrador de STATUS sinaliza condições especiais ocorridas durante as operações. Ele vai ser muito útil quando iniciarmos a programação!
A memória dos PIC16 está organizada de modo que o barramento de dados é separado do barramento de instruções (arquitetura harward modificada). Dessa forma, podem-se realizar acessos simultâneos as duas regiões de memória. Entretanto, as duas memórias estão organizadas em regiões de endereçamento especial, os conhecidos bancos de memória (paginação). Todo mundo que já trabalhou com essa família tem alguma coisa para falar desse “recurso”. Muitos erros de programação consistem na utilização errada ou de enganos em relação aos bancos de memória.
Os dispositivos da MIDRANGE possuem uma pilha de oito níveis, isolada da região endereçável de memória. A pilha serve para guardar temporariamente o endereço de retorno das rotinas. Quando ocorre um overflow na pilha o primeiro endereço é sobre-escrito, como em um buffer circular.
Dois são os modos de endereçamentos disponíveis: direto e indireto. O endereçamento indireto é provido pelos registradores INDF e FSR. O segundo serve como um ponteiro para uma posição na RAM, enquanto o primeiro é um registro virtual que acessa o endereço apontado pelo FSR. Pode-se considerar um terceiro modo de endereçamento, quando se faz uso do registrador PCLATH e de algumas “operações especiais” para se ter um endereçamento indexado.
Para mais informações sobre a arquitetura dessa família de micro-controladores dê uma olhada em [3].
Na tabela abaixo, segue o conjunto de instruções e seus respectivos opcodes.
| Instrução | Descrição |
| ADDWF f,d | Adiciona W com o registrador ‘f’. O resultado é salvo em W caso ‘d’ == 0 e em ‘f’ se ‘d’==1. |
| ANDWF f, d | Operação de E bitwise entre W e f. O resultado é salvo em W caso ‘d’ == 0 e em ‘f’ se ‘d’==1. |
| CLRF f | Faz 'f' = 0; |
| CLRW | Faz W = 0; |
| COMF f,d | Complementa o registrador 'f'. O resultado é salvo em W caso ‘d’ == 0 e em ‘f’ se ‘d’==1. |
| DECF f,d | Decrementa o registrador 'f'. O resultado é salvo em W caso ‘d’ == 0 e em ‘f’ se ‘d’==1. |
| DECFSZ f,d | Decrementa o registrador 'f' e salta a próxima instrução se o resultado for igual a 0. O resultado é salvo em W caso ‘d’ == 0 e em ‘f’ se ‘d’==1. |
| INCF f,d | Incrementa o registrador 'f'. O resultado é salvo em W caso ‘d’ == 0 e em ‘f’ se ‘d’==1. |
| INCFSZ f,d | Incrementa o registrador 'f' e salta a próxima instrução se o resultado for igual a 0. O resultado é salvo em W caso ‘d’ == 0 e em ‘f’ se ‘d’==1. |
| IORWF f,d | Operação OU bitwise entre 'W' e 'f'. O resultado é salvo em W caso ‘d’ == 0 e em ‘f’ se ‘d’==1. |
| MOVF f,d | Move o valor do registrador 'f'. O resultado é salvo em W caso ‘d’ == 0 e em f se ‘d’==1. |
| MOVWF f | Move o conteúdo do registrador W para 'f'. |
| NOP | Sem operação (No Operation) |
| RLF f,d | Rotação à esquerda com carry. O resultado é salvo em W caso ‘d’ == 0 e em ‘f’ se ‘d’==1. |
| RRF f,d | Rotação à direita com carry. O resultado é salvo em W caso ‘d’ == 0 e em ‘f’ se ‘d’==1. |
| SUBWF f,d | Subtrai W com f. O resultado é salvo em W caso ‘d’ == 0 e em ‘f’ se ‘d’==1. |
| SWAPF f,d | Troca os nibbles no registrador f. O resultado é salvo em W caso ‘d’ == 0 e em ‘f’ se ‘d’==1. |
| XORWF f,d | Operação bitwise XOR (OU Exclusivo) entre W e f. O resultado é salvo em W caso ‘d’ == 0 e em ‘f’ se ‘d’==1. |
| BCF f, b | Faz o bit 'b' do registrador f igual a 0 (bit clear). |
| BSF f, b | Faz o bit 'b' do registrador 'f' igual a 1 (bit set). |
| BTFSC f,b | Se o bit 'b' no registrador 'f' for igual à 0 a próxima instrução será saltada. |
| BTFSS f,b | Se o bit 'b' no registrador 'f' for igual à 1 a próxima instrução será saltada. |
| ADDLW k | Soma entre o literal 'k' e W. |
| ANDLW k | E lógico entre o literal 'k' e W. |
| CALL k | Chama uma subrotina. |
| CLRWDT | Limpa o Watchdog Timer. |
| GOTO k | Vai à um endereço. |
| IORLW k | Operação OU entre um literal e W. |
| MOVLW k | Move o literal 'k' para W. |
| RETFIE | Retorna de uma interrupção. |
| RETLW k | Retorna com o literal 'k' em W. |
| RETURN | Retorna de uma subrotina. |
| SLEEP | Vai ao modo de 'standby'. |
| SUBLW k | Subtrai W com um literal 'k'. |
| XORLW | Operação XOR (OU Exclusivo) entre W e um literal 'k'. |
Programando em Assembly MPASM
Agora que já conhecemos um pouco da arquitetura e do conjunto de instruções dos PIC16, iremos começar a escrever alguns programas simples em MPASM, o assembly da Microchip. Para escrevermos programas em assembly, precisamos de um programa que os traduza para a linguagem de máquina.A Microchip disponibiliza um programa gratuito que faz o papel de ambiente gráfico de desenvolvimento e vem com ferramentas de assemblador e linker, o MPLAB. Faça download do aplicativo no site da Microchip (www.microchip.com), e instale-o em seu computador.
Iremos analisar três trechos de código que nos darão uma visão geral da programação assembly para micro-controladores.
O primeiro trecho de código realiza um loop de oito iterações onde duas variáveis são somadas continuamente e o resultado é salvo no registrador W, fazemos uso de uma terceira variável como contador.
1 - clrf counter ; zera o valor do contador 2 - 3 - loop: 4 - movf somador_1, 0 ;move o valor de somador_1 para W 5 - addwf somador_2, 0 ;soma W com somador_2 e guarda o valor em W 6 - incf counter, 1 ;incrementa o registrador counter 7 - btfss counter,#3 ;testa se o 3 bit de counter está setado 8 - goto loop 9 - fim_loop: ;fim do loopA linha 1 prepara a variável “counter” para servir como contador, zerando o seu valor. O loop inicia na linha 3, o valor de “somador_1” é atribuído ao registrador W. Em seguida, o valor de W é somado ao valor de “somador_2” e o resultado é armazenado no próprio W. Estas duas linhas são o corpo do loop, agora vamos ao controle do número de iterações.
As linhas restantes controlam quantas vezes o corpo do loop será executado. A linha 6 incrementa o valor do contador, indicando quantas iterações houve. Utilizamos a instrução “btfss counter, #3” que simplesmente testa se o bit 3 da variável “counter” está setado e caso esteja pula a próxima instrução; para verificar o fim do loop.
Vocês podem estar se perguntando por que fizemos uso dessa instrução, o objetivo deste trecho de código é repetir oito vezes o somatório e então sair. Sabemos que oito em binário é representado por “00001000”, ou seja, o bit 3 setado.
É claro que se o número de iterações não fosse uma potência de dois o trecho de código acima estaria inutilizado. Muitas vezes, precisamos fazer reduções de código para adequarmos o tamanho do código ao espaço de memória de programa disponível. E nos vemos obrigados a fazer uso deste tipo de “aperfeiçoamento” de código, dificilmente um compilador C faria algo deste gênero. “É uma questão de ver a simetria do problema!”, como diz meu antigo professor de Geometria Euclidiana.
No próximo trecho de código, iremos realizar uma comparação entre duas variáveis com o objetivo de saber qual é a maior.
1 – movwf var_1 ; move o valor de var_1 para W 2 – subwf var_2, 0 ;subtrai o valor de var_2 de W 3 – btfss STATUS, C ;verifica o estado do bit CARRY 4 – goto v1_maior ;vai para valor menor 5 – v2_maior: ; var_2 > var_1 6 – return ;sai da rotina 7 – v1_maior: ;trata o caso de var_2 < var_1
Esse trecho é bem simples antes de uma operação de “subwf” o bit C do registrador STATUS é setado, após a execução da instrução, caso o registrado seja menor do que W o bit C é limpo. O registrador STATUS contém diversos flags que nos auxiliam a tomar decisões após instruções aritméticas.
O último trecho analisado irá copiar dez bytes de uma posição de memória RAM para outra região.
1 - movlw 0x45 ; 2 - movwf pointer_0 ;inicializa pointer_0 com o endereço do buffer destino 3 - movlw 0x55 4 - movwf pointer_1 ;inicializa pointer_1 com o endereço do buffer fonte 5 - movwf FSR, 1 ;aponta o FSR para o endereço fonte 6 – copy_ram: 7 - movf INDF, 0 ;põe em W o valor da posição do buffer fonte 8 - movwf aux ;guarda o valor na variável auxiliar 9 - movf pointer_0 ; 10- movwf FSR ;aponta para o buffer destino 11- movf aux,0 ; 12- movwf INDF ; põe o valor de aux no buffer destino 13- incf pointer_0, 1 ;incrementa os ponteiros 14- incf pointer_1, 1 15- incf counter ;incrementa o contador 16- movlw d’10’ 17- subwf counter, 0 ;verifica se passaram-se todas a iterações 18- btfss STATUS, C 19- goto copy_ram_aux ;counter < 10 20- fim_copy_ram: ;todos os bytes copiados!!! 21- return 22- 23- copy_ram_aux: ; aponta FSR para a próxima posição 24- movf pointer_1, 0 ; do buffer fonte 24- movwf FSR 25- goto copy_ram ;volta para realizar outra cópia
Este trecho é bem mais complexo do que os primeiros, fazemos uso do modo de endereçamento indireto para copiar um buffer de dez bytes entre duas regiões de memória RAM.
A primeira parte atribui a dois ponteiros os valores iniciais dos buffers de destino e fonte. Então o registrador FSR é iniciado com a posição inicial do buffer fonte. O valor da posição apontado por FSR é copiado para a variável auxiliar “aux” via registrador INDF.
FSR é apontado para o buffer destino nas linhas 9 e 10. E o valor de “aux” é copiado para INDF que agora aponta para o buffer destino. As linhas seguintes incrementam o valor dos ponteiros dos buffers.
A partir da linha 15 segue o código de controle que verifica quantos bytes foram copiados e caso necessário configura o FSR para apontar novamente para o buffer fonte. Este tipo de rotina é interessante quando se trabalha com recepção de caracteres por uma interface serial e precisa-se copiar o buffer de recepção para ser tratado por outra rotina.
Dicas Úteis
O conjunto de instruções PIC16 é bem reduzido, o que pode representar mais um desafio para o desenvolvedor de sistemas embutidos. Conforme o programa vai crescendo em tamanho, cresce também a dificuldade em mantê-lo e entendê-lo. Por isso, é importante aplicar uma metodologia bem estruturada na análise e codificação do sistema. Diagramas de fluxo e de estado dão maior entendimento e são mais fáceis de trabalhar do que especificações escritas.Comentários bem elaborados também ajudam na programação e deixam importantes informações para os próximos programadores que venham a trabalhar com o sistema.
Caso o seu problema seja espaço de programa, vale a pena queimar neurônios pensando na melhor forma de escrever dada rotina. Muitas vezes, o próprio problema dá pistas de soluções que consomem menos instruções. Um bom conhecimento e prática no assembly também ajuda muito!!!
Referências
1 - ABD-EL-BARR, Mostafa; EL-REWINI, Hesham. Fundamentals of Computer Organization and Architecture. WILEY, 1 Ed. 2005.2 - SHIVA, Sajjan. Computer Design and Architecture. DEKKER, 3 Ed. 2000.
3 - MICROCHIP. PICmicro MIDRANGE MCU Family Reference Manual. Microchip Technology, 1997.
Otávio Alcântara
Otávio Alcântara é Tecnólogo em Telemática pelo CEFET-CE e
especializado em desenvolvimento de software em tempo real para
sistemas embutidos .

