Biblioteca de gerador de código Assembler para microcontroladores AVR. Parte 2

← Parte 1. Primeiro conhecido


Parte 3. Endereçamento indireto e controle de fluxo →


Biblioteca do gerador de código do assembler para microcontroladores AVR


Parte 2. Introdução


Conforme planejado, nesta parte, consideraremos mais detalhadamente os recursos de programação usando a biblioteca NanoRTOS. Aqueles que começaram a ler este post podem se familiarizar com a descrição geral e os recursos da biblioteca no artigo anterior . Devido ao escopo limitado da publicação planejada, supõe-se que um leitor respeitado esteja pelo menos minimamente familiarizado com a programação C # e também tenha um entendimento da arquitetura e programação em linguagem assembly dos controladores AVR da série Mega.


O estudo de qualquer tecnologia é melhor combinado com a execução de exemplos, por isso sugiro fazer o download da biblioteca em https://drive.google.com/open?id=1FfBBpxlJkWC027ikYpn6NXbOGp7DS-5B e tentar conectá-la ao projeto de aplicativo do console. Se for bem-sucedido, você pode seguir em frente com segurança. Você pode usar qualquer aplicativo C # ou projeto UnitTest como um shell para executar exemplos. Eu pessoalmente gosto mais deste último, porque permite armazenar vários exemplos diferentes em um só lugar e executá-los conforme necessário. De qualquer forma, apenas o texto do exemplo será incluído nas listagens para economizar espaço.


O trabalho com a biblioteca sempre deve começar com um anúncio, como um microcontrolador. Como os parâmetros e o conjunto de dispositivos periféricos dependem do tipo de controlador, a escolha de um controlador específico afeta a formação do código do montador. A linha de declaração do controlador para o qual o programa está gravado é a seguinte


var m = new Mega328(); 

Além disso, podem ser seguidas configurações adicionais do microcontrolador, como parâmetros do relógio ou a atribuição de funções do sistema para saídas. Por exemplo, a permissão para usar a redefinição de hardware elimina o uso da saída como uma porta. Todos os parâmetros do controlador têm valores padrão, e nos exemplos os omitimos, exceto quando é importante, mas em projetos reais, aconselho que você os instale sempre. Por exemplo, uma configuração de relógio pode ser assim


  m.FCLK = 16000000; m.CKDIV8 = false; 

Essa configuração significa que o microcontrolador é sincronizado com um ressonador de quartzo ou uma fonte externa com uma frequência de 16 MHz e o divisor de frequência para dispositivos periféricos está desativado.
A função Texto da classe estática AVRASM é responsável pela saída do trabalho. Essa função sempre será chamada no final do código para gerar o resultado no formato assembler. A instância anteriormente atribuída da classe do controlador, a função recebe como um parâmetro. Portanto, a estrutura mais simples do programa para trabalhar com a biblioteca assume a seguinte forma


 var m = new Mega328(); //     //      //          var t = AVRASM.Text(m); //    t 

Se tentarmos executar o programa, ele terá êxito, mas não gerará nenhum código. Apesar da futilidade do resultado, isso dá motivos para concluir que a biblioteca não gera nenhum código de quebra.


Nós já aprendemos como criar um programa vazio. Agora vamos tentar criar algum código nele. Vamos começar com o mais primitivo. Vamos ver como podemos resolver o problema de incremento de uma variável de oito bits localizada em uma célula RON arbitrária. Do ponto de vista do montador, este é o comando inc [register] . Insira as seguintes linhas no corpo do programa de nossas compras


  var r = m.REG(); r++; 

O objetivo das equipes é bastante óbvio. O primeiro comando associa a variável r a um dos registradores do processador. O segundo comando fala sobre a necessidade de incrementar essa variável. Após a execução, obtemos o primeiro resultado da execução do código.


 RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF R0000 = r20 inc R0000 .DSEG 

Vamos dar uma olhada no que aconteceu como resultado. Os quatro primeiros comandos são a inicialização do ponteiro da pilha. A seguir, é a definição do nome da variável. E, finalmente, nosso inc , para o qual tudo foi iniciado. Nada a mais, exceto a inicialização da pilha, não apareceu. A questão que pode surgir ao analisar esse código é que tipo de R0000 ? Temos uma variável chamada r ? Em um programa C #, um programador pode usar conscientemente e legalmente os mesmos nomes, usando o escopo. Do ponto de vista do montador, todos os rótulos e definições devem ser exclusivos. Para não forçar o programador a monitorar a exclusividade dos nomes, por padrão, os nomes são gerados pelo sistema. No entanto, existe uma situação em que, para fins de depuração, você ainda deseja transferir o nome consciente do programa para o código de saída, para que ele possa ser facilmente encontrado. Não é assustador. Substitua m.REG () por m.REG (”r”) e execute o código novamente. Como resultado, veremos o seguinte


 RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF r = r20 inc r .DSEG 

Então, com a nomeação dos registros resolvidos. Agora vamos ver por que de repente os registros começaram a ser atribuídos a partir de 20, e não a partir de 0? Para responder a essa pergunta, lembramos que a partir de 16, os registros têm uma grande oportunidade de inicializá-los com constantes. E como esse recurso é muito procurado, iniciamos a distribuição pela metade superior para aumentar as oportunidades de otimização. Então, tudo a mesma coisa não está claro - por que c20 e não 16? O motivo é que a conversão de vários comandos no código do assembler é impossível sem o uso de células de armazenamento temporário. Para esses propósitos, alocamos 4 células de 16 a 19. Isso não significa que elas se tornaram completamente inacessíveis ao programador. É apenas que o acesso a eles é organizado de maneira um pouco diferente, para que o programador esteja ciente das possíveis limitações de seu uso e atue conscientemente. Removemos a definição do registrador r do código e substituímos a linha seguinte por
m.TempL ++;
Vejamos o resultado


 RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 inc TempL .DSEG 

Aqui, aparentemente, deve-se notar que o assembler de saída requer a interpretação correta do arquivo de conexão com definições e macros Common.inc do pacote de desenvolvimento. Na verdade, ele contém todas as macros e definições necessárias, incluindo a correspondência de nomes para células de armazenamento temporário. Ou seja, TempL = r16, TempH = r17, TempQL = r18, TempQH = r19. Nesse caso, não usamos nenhum comando que usaria células de armazenamento temporário para funcionar, portanto, nossa decisão de usá-lo na operação TempL é bastante aceitável. E o que devemos fazer se tivermos certeza absoluta de que nenhuma atribuição de constantes para nossa variável não brilha e não queremos gastar células preciosas da metade superior nela? Retorne nossa definição para o código fonte, alterando-o para var r = m.REGL ("r"); e avaliar o resultado do trabalho


 RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF r = r4 inc r .DSEG 

O objetivo é alcançado. Conseguimos explicar para a biblioteca onde, em nossa opinião, a variável deveria estar localizada. Vamos mais longe. Vamos ver o que acontece se várias variáveis ​​forem declaradas ao mesmo tempo. Copiamos nossa definição e ações mais algumas vezes. Para uma mudança, redefiniremos uma nova variável e reduziremos o valor da outra em 1. O resultado deve ser algo assim.


  var m = new Mega328(); var r = m.REGL("r"); r++; var rr = m.REGL("rr"); rr--; var rrr = m.REGL("rrr"); rrr.Clear(); var t = AVRASM.Text(m);  . RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF r = r4 inc r .DEF rr = r5 dec rr .DEF rrr = r6 clr rrr .DSEG 

Ótimo Foi isso que foi solicitado. Agora vamos ver como podemos liberar o registro para outros fins, se não precisarmos mais dele. Aqui, infelizmente, até agora tudo está à mão. As regras C # sobre limites de visibilidade e liberação automática de variáveis ​​ao ir para o exterior para o modo de geração de código ainda não funcionam. Vamos ver como você ainda pode liberar a célula, se necessário. Adicione apenas uma linha ao nosso programa e veja o resultado.


  var m = new Mega328(); var r = m.REGL("r"); r++; var rr = m.REGL("rr"); rr--; r.Dispose(); var rrr = m.REGL("rrr"); rrr.Clear(); var t = AVRASM.Text(m); 

 RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF r = r4 inc r .DEF rr = r5 dec rr .UNDEF r .DEF rrr = r4 clr rrr .DSEG 

É fácil ver que o quarto registro que liberamos ficou disponível novamente para uso. Considerando o fato de que cada nova declaração de variável captura o registro, podemos concluir que, ao compilar o programa, é necessário liberar os registros a tempo, se você não quiser encontrar uma situação em que eles se tornem escassos.


Ao analisar os exemplos, já demonstramos ao longo do caminho como executar operações unicast em registros. Agora vamos ver como estão as coisas com o multicast. A arquitetura do processador permite o máximo de instruções de dois endereços (especialmente as corrosivas, com exceção de duas instruções para as quais o resultado é colocado em registros fixos). Isso deve ser entendido de tal maneira que o primeiro operando na operação após sua execução contenha o resultado. A sintaxe especial [registrador1] [operação] = [registrador2] é fornecida para este tipo de operação. Vamos ver como fica na prática. Vamos tentar declarar e adicionar duas variáveis ​​de registro.


  var m = new Mega328(); var op1 = m.REG(); var op2 = m.REG(); op1 += op2; var t = AVRASM.Text(m); 

Como resultado, veremos


 RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF R0000 = r20 .DEF R0001 = r21 add R0000,R0001 .DSEG 

Conseguiu o que eles esperavam. Você pode experimentar as operações sozinho -, &, | e verifique se o resultado não é pior.
Até agora, todas as opções acima claramente não são suficientes para escrever o programa mais simples. O fato é que ainda não tocamos na inicialização dos próprios registros. A arquitetura do microcontrolador permite inicializar os registradores com uma constante, o valor de outro registrador, o valor da célula de memória RAM em um endereço específico, o valor da célula de memória RAM no ponteiro localizado em um par especial de registros, o valor da célula de entrada / saída em um endereço específico, bem como o valor da célula de memória do programa no ponteiro colocado em um par especial de registros. Mais tarde, trataremos do endereçamento indireto, mas, por enquanto, consideraremos casos mais simples. Escreveremos e executaremos o seguinte programa de teste.


  var m = new Mega328(); var op1 = m.REG(); var op2 = m.REG(); op1.Load(0x10); op2.Load('s'); op1.Load(op2); var t = AVRASM.Text(m); 

Aqui declaramos e inicializamos duas variáveis ​​com o número e o símbolo e, em seguida, copiamos o valor da variável op2 na célula op1. Obviamente, o número deve estar dentro do intervalo de 0 a 255, para que nenhum erro ocorra. O resultado será


 RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF R0000 = r20 .DEF R0001 = r21 ldi R0000,16 ldi R0001,'s' mov R0000,R0001 .DSEG 

Pode-se observar no exemplo que, para todas as operações listadas, uma função é usada e a própria biblioteca gera o conjunto correto de comandos do assembler. Como já foi mencionado muitas vezes, o carregamento direto de dados no registrador com o comando ldi está disponível apenas para a metade superior dos registradores. Vamos tornar nossa biblioteca mais complicada alterando o programa para que ele aloque registros para variáveis ​​da metade inferior.


  var m = new Mega328(); var op1 = m.REGL(); var op2 = m.REGL(); op1.Load(0x10); op2.Load('s'); op1.Load(op2); var t = AVRASM.Text(m); 

Nós temos


 RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF R0000 = r4 .DEF R0001 = r5 ldi TempL,16 mov R0000,TempL ldi TempL,'s' mov R0001,TempL mov R0000,R0001 .DSEG 

A biblioteca lidou com essa tarefa, gastando ao mesmo tempo o número mínimo possível de equipes. E, ao mesmo tempo, vimos por que precisávamos alocar registros de armazenamento temporário. Bem, finalmente, vamos ver como a matemática é implementada para trabalhar com constantes. Sabemos da existência do comando subi assembler para subtrair a constante do registrador, e agora tentaremos descrevê-la em termos da biblioteca.


  var m = new Mega328(); var op1 = m.REG(); op1.Load(0x10); op1 -= 10; var t = AVRASM.Text(m); 

O resultado será


 RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF R0000 = r20 ldi R0000,16 subi R0000,0x0A .DSEG 

Acabou. E como a biblioteca se comportará se não houver um comando assembler que possa executar a operação necessária? Por exemplo, se não queremos subtrair, mas adicionar uma constante. Vamos tentar ver


  var m = new Mega328(); var op1 = m.REG(); op1.Load(0x10); op1 += 10; var t = AVRASM.Text(m); 

 RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF R0000 = r20 ldi R0000,16 subi R0000,0xF6 .DSEG 

A biblioteca saiu subtraindo um valor negativo. Vamos ver como as coisas estão indo com a mudança. Mude o valor do registro por 5 para a direita.


  var m = new Mega328(); var op1 = m.REG(); op1.Load(0x10); op1 >>= 5; var t = AVRASM.Text(m); 

O resultado será


 RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF R0000 = r20 ldi R0000,16 swap R0000 andi R0000,15 lsr R0000 .DSEG 

Nem tudo é óbvio aqui, mas ele executa duas equipes mais rapidamente do que se uma solução frontal de cinco equipes de turno fosse usada.


Portanto, neste artigo, examinamos o uso da biblioteca para trabalhar com aritmética de registro. No próximo artigo, continuaremos a descrever como a biblioteca funciona com ponteiros e considerar métodos para controlar o fluxo de execução de comandos (loops, transições etc.)

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


All Articles