O poder mágico das macros ou como facilitar a vida de um programador de assembler AVR

Muito foi escrito sobre macros no assembler. E na documentação e em vários artigos. Mas, na maioria dos casos, tudo se resume a uma simples lista de diretivas com uma breve descrição de suas funções ou a um conjunto de exemplos díspares de macros prontas.
O objetivo deste artigo é descrever uma abordagem específica da programação em linguagem assembly para gerar o código mais simples e legível usando macros. O artigo não descreverá a sintaxe de comandos e diretivas individuais. Uma descrição detalhada já foi fornecida pelo fabricante . Vamos nos concentrar em como usar essas oportunidades para resolver problemas específicos.


Ao mesmo tempo, o ATMEL tentou e desenvolveu uma linha de microcontroladores de oito bits com uma arquitetura de alta qualidade e um sistema de comando simples, mas ao mesmo tempo muito poderoso. Mas, como você sabe, não há limite para a perfeição, e algumas instruções freqüentemente usadas não são suficientes. Felizmente, o montador de macros, gentil e absolutamente gratuito, fornecido pelo fabricante, pode simplificar significativamente o código através do uso de diretivas. Antes de prosseguir diretamente para as macros, executaremos algumas etapas preliminares


Definição de constantes


.EQU FOSC = 16000000 .EQU CLK8 = 0 

Essas duas definições permitem que você se livre dos "números mágicos" nas macros, onde os valores dos registros são calculados com base na frequência do processador e no estado do fusível do divisor periférico. A primeira definição é a frequência do cristal do processador em hertz, a segunda é o estado do divisor de frequência periférico.


Registrar nomeação


 .DEF TempL = r16 .DEF TempH = r17 .DEF TempQL = r18 .DEF TempQH = r19 .DEF AL = r0 .DEF AH = r1 .DEF AQL = r2 .DEF AQH = r3 

À primeira vista, um pouco redundante registra registros que podem ser usados ​​em macros. Apenas quatro registros para Temp são necessários se estivermos lidando com valores de 32 bits (por exemplo, nas operações de multiplicação de dois números de 16 bits). Se tivermos certeza de que dois registros de armazenamento temporário são suficientes para usarmos em macros, o TempQL e o TempQH não poderão ser determinados. Definições para A são necessárias para macros usando operações de multiplicação. AQ não é mais necessária se não usarmos aritmética de 32 bits com nossas macros.


Macros para implementar comandos simples


Agora que descobrimos o nome dos registradores, começaremos a implementar os comandos que estão faltando e tentaremos simplificar os existentes. O montador do AVR possui um recurso estranho. Para entrada e saída, as primeiras 64 portas usam os comandos de entrada / saída e para os restantes lds / sts . Para não examinar a documentação toda vez em busca do comando necessário para uma porta específica, criaremos um conjunto de comandos universais que substituirão independentemente os valores necessários.


 .MACRO XOUT .IF @0<64 out @0,@1 .ELSE sts @0,@1 .ENDIF .ENDMACRO .MACRO XIN .IF @1<64 in @0,@1 .ELSE lds @0,@1 .ENDIF .ENDMACRO 

Para que a substituição funcione corretamente, a compilação condicional é usada na macro. No caso em que o endereço da porta é menor que 64, a primeira seção condicional é executada, caso contrário, a segunda. Nossas macros repetem completamente a funcionalidade dos comandos padrão para trabalhar com portas de entrada / saída; portanto, para indicar que nossa equipe possui recursos avançados, adicionamos o prefixo de nomeação padrão X.
Um dos comandos mais comuns que não estão disponíveis no assembler, mas são constantemente necessários, é o comando para gravar constantes nos registradores de entrada de saída. A implementação de macro para este comando será semelhante a esta


 .MACRO OUTI ldi TempL,@1 .IF @0<64 out @0, TempL .ELSE sts @0, TempL .ENDIF .ENDMACRO 

Nesse caso, o nome na macro, para não violar a lógica de nomeação de comandos, adicione ao nome padrão o postfix I , usado pelo desenvolvedor para indicar os comandos para trabalhar com constantes. Nesta macro, usamos o registro TempL definido anteriormente para operação .
Em alguns casos, não é necessário um único registro, mas um par inteiro que armazena um valor de 16 bits. Crie uma nova macro para gravar um valor de 16 bits em um par de registros de E / S


 .MACRO OUTIW ldi TempL,HIGH(@1) .IF @0<64 out @0H, TempL .ELSE sts @0H, TempL .ENDIF ldi TempL,LOW(@1) .IF @0<64 out @0L, TempL .ELSE sts @0L, TempL .ENDIF .ENDMACRO 

Nesta macro, usamos as funções LOW e HIGH internas para extrair o byte baixo e alto de um valor de 16 bits. No nome da macro, adicione os postfixes I e W ao comando para indicar que, nesse caso, o comando funciona com um valor de 16 bits (palavra).
Não menos frequentemente em programas, há o carregamento de pares de registradores, por exemplo, para definir ponteiros na memória. Vamos criar uma macro


 .MACRO ldiw ldi @0L, LOW(@1) ldi @0H, HIGH(@1) .ENDMACRO 

Nesta macro, usamos o fato de que a nomeação padrão de registradores e portas no fabricante implica o postfix L para o inferior e o postfix H para a parte superior do valor de byte duplo. Se você seguir esta regra ao nomear suas próprias variáveis, a macro funcionará corretamente, inclusive com elas. A beleza das macros também reside no fato de que elas fornecem uma substituição simples; portanto, no caso em que o segundo operando é um número e no caso em que esse é o nome do rótulo, a macro funcionará corretamente.


Macros para implementar comandos complexos.


Quando se trata de operações mais complexas, as macros geralmente não são usadas, preferindo rotinas. No entanto, nesses casos, as macros podem facilitar a vida e tornar o código mais legível. Nesse caso, a compilação condicional chega ao resgate. Uma abordagem de programação pode ser assim:
Colocamos todas as nossas rotinas em um arquivo separado, que iremos nomear, por exemplo, Library.inc . Cada sub-rotina neste arquivo terá o seguinte formato


 _sub0: .IFDEF __sub0 ; -----    ----- ret .ENDIF 

Nesse caso, a presença da definição __sub0 significa que a sub-rotina deve ser incluída no código resultante. Caso contrário, será ignorado.
Em seguida, em um arquivo separado Macro.inc, definimos macros do formulário


 .MACRO SUB0 .IFNDEF __sub0 .DEF __sub0 .ENDIF ; ---          call _sub0 .ENDMACRO 

Ao usar essa macro, verificamos a definição de __sub0 e, se estiver ausente, executamos a determinação. Como resultado, o uso de uma macro desbloqueia a inclusão do código de sub-rotina no arquivo de saída. No caso de usar rotinas em macros, o código do programa principal assumirá a seguinte forma


 .INCLUDE “Macro.inc” ;----    ---- .INCLUDE “Library.inc” 

Como exemplo, fornecemos uma implementação de uma macro para dividir números inteiros não assinados de 8 bits. Mantemos a lógica do fabricante e colocamos o resultado em AL (r0) e o restante da divisão em AH (r1) . A sub-rotina terá a seguinte aparência


 _div8u: .IFDEF __ div8u ;AH -  ;AL  ;TempL -  ;TempH -  ;TempQL -  clr AL; clr AH; ldi TempQL,9 d8u_1: rol TempL dec TempQL brne d8u_2 ret d8u_2: rol A sub AH, TempH brcc d8u_3 add AH,TempH clc rjmp d8u_1 d8u_3: sec rjmp d8u_1 .ENDIF 

A definição de macro para usar esta rotina será a seguinte


 .MACRO DIV8U .IFNDEF __div8u .DEF __div8u .ENDIF mov TempL, @0 mov TempH, @1 call _div8u .ENDMACRO 

Se desejar, você pode adicionar uma versão para trabalhar com uma constante


 .MACRO DIV8UI .IFNDEF __div8u .DEF __div8u .ENDIF mov TempL, @0 ldi TempH, @1 call _div8u .ENDMACRO 

Como resultado, o uso da operação de divisão no texto do programa é trivial


 DIV8U r10, r11 ; r0 = r10/r11 r1 = r10 % r11 DIV8UI r10, 35 ; r0 = r10/35 r1 = r10 % 35 

Usando compilação condicional, podemos colocar todas as rotinas que podem ser úteis para nós no Library.inc . Nesse caso, apenas aqueles que foram chamados pelo menos uma vez aparecerão no código de saída. Preste atenção à posição da etiqueta de entrada. A saída do rótulo além dos limites da condição é devida ao compilador. Se você colocar o rótulo no corpo do bloco condicional, o compilador poderá gerar um erro. A presença de tags não utilizadas no código não é assustadora, pois a presença de qualquer número de tags não afeta o resultado.


Macros periféricas


Uma das operações em que é difícil fazer sem usar a documentação do fabricante é inicializar dispositivos periféricos. Mesmo com o uso de designações mnemônicas de registradores e bits do código, pode ser difícil entender em que modo um dispositivo está configurado, especialmente porque às vezes o modo é configurado por uma combinação de valores de bits de diferentes registradores. Vamos ver como as macros podem ser usadas com o exemplo USART .
Vamos começar com a macro de inicialização do modo assíncrono.


 .MACRO USART_INIT ; speed, bytes, parity, stop-bits .IF CLK8 == 0 .SET DIVIDER = FOSC/16/@0-1 .ELSE .SET DIVIDER = FOSC/128/@0-1 .ENDIF ; Set baud rate to UBRR0 outi UBRR0H, HIGH(DIVIDER) outi UBRR0L, LOW(DIVIDER) ; Enable receiver and transmitter .SET UCSR0B_ = (1<<RXEN0)|(1<<TXEN0) outi UCSR0B, UCSR0B_ .SET UCSR0C_ = 0 .IF @2 == 'E' .SET UCSR0C_ |= (1<<UPM01) .ENDIF .IF @2 == 'O' .SET UCSR0C_ |= (1<<UPM00) .ENDIF .IF @3== 2 .SET UCSR0C_ |= (1<<USBS0) .ENDIF .IF @1== 6 .SET UCSR0C_ |= (1<<UCSZ00) .ENDIF .IF @1== 7 .SET UCSR0C_ |= (1<<UCSZ01) .ENDIF .IF @1== 8 .SET UCSR0C_ = UCSR0C_ |(1<<UCSZ01)|(1<<UCSZ00) .ENDIF .IF @1== 9 .SET UCSR0C_ |= (1<<UCSZ02)|(1<<UCSZ01)|(1<<UCSZ00) .ENDIF ; Set frame format outi UCSR0C,UCSR0C_ .ENDMACRO 

O uso da macro nos permitiu substituir a inicialização dos registros de instalação do USART por valores que eram incompreensíveis sem a leitura da documentação por uma linha que mesmo aqueles que encontraram este controlador pudessem manipular pela primeira vez. Nessa macro, também ficou finalmente claro por que determinamos as constantes de frequência e divisor. Bem, deve-se notar que, apesar do código impressionante da macro em si, o resultante terá a mesma aparência como se estivéssemos escrevendo a inicialização da maneira usual.
Para finalizar com o USART, aqui estão mais algumas macros pequenas


  .MACRO USART_SEND_ASYNC outi UDR0, @0 .ENDMACRO 

Há apenas uma linha, mas o uso dessa macro permitirá que você veja melhor onde o programa exibe dados no USART . Se supusermos trabalhar no modo síncrono sem usar interrupções, em vez de USART_SEND_ASYNC , é melhor usar a macro abaixo


  .MACRO USART_SEND USART_Transmit: xin TempL, UCSR0A sbrs TempL, UDRE0 rjmp USART_Transmit outi UDR0, @0 .ENDMACRO 

Nesse caso, habilitamos a verificação de ocupação da porta e exibimos dados apenas quando a porta está livre. Obviamente, essa abordagem para trabalhar com dispositivos periféricos funcionará para qualquer dispositivo, e não apenas para o USART .


Comparação de programas sem e usando macros.


Vejamos um pequeno exemplo e compare o código escrito sem usar macros com o código em que são usados. Por exemplo, considere um programa que exibe o clássico "Olá, mundo!" ao terminal via hardware UART .


  RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 USART_Init: out UBRR0H, r17 out UBRR0L, r16 ldi r16, (1<<RXEN0)|(1<<TXEN0) out UCSRnB,r16 ldi r16, (1<<USBS0)|(3<<UCSZ00) out UCSR0C,r16 ldi ZL, LOW(STR<<1) ldi ZH, HIGH(STR<<1) LOOP: lpm r16, Z+ or r16,r16 breq END USART_Transmit: in r17, UCSR0A sbrs r17, UDRE0 rjmp USART_Transmit out UDR0,r16 rjmp LOOP END: rjmp END STR: .DB “Hello world!”,0 

E aqui está o mesmo programa, mas escrito usando macros


 .INCLUDE “macro.inc” .EQU FOSC = 16000000 .EQU CLK8 = 0 RESET: ldiw SP, RAMEND; USART_INIT 19200, 8, "N", 1 ldiw Z, STR<<1 LOOP: lpm TempL, Z+ test TempL breq END USART_SEND TempL rjmp LOOP END: rjmp END STR: .DB “Hello world!”,0 

Neste exemplo, usamos as macros descritas acima, o que nos permitiu simplificar significativamente o código do programa e torná-lo mais compreensível. O código binário nos dois programas será absolutamente idêntico.


Conclusão


O uso de macros pode reduzir significativamente o código do programa do assembler, para torná-lo mais compreensível e legível. A compilação condicional permite criar comandos universais e bibliotecas de procedimentos sem criar código de saída redundante. Como uma desvantagem, pode-se destacar um conjunto bastante modesto pelos padrões de linguagens de alto nível de operações e restrições permitidas ao declarar os dados como "encaminhados". Essa restrição não permite, por exemplo, escrever por meio de macros um comando universal completo para transições jmp / rjmp e inflar significativamente o código da própria macro ao implementar lógica complexa.

Source: https://habr.com/ru/post/pt465261/


All Articles