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

← Parte 2. Introdução
Parte 4. Programação de dispositivos periféricos e manipulação de interrupções →


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


Parte 3. Endereçamento indireto e controle de fluxo


Na parte anterior, detalhamos bastante o trabalho com variáveis ​​de registro de 8 bits. Se você perdeu o post anterior, aconselho a lê-lo. Nele, você pode encontrar um link para a biblioteca para experimentar os exemplos no artigo. Para quem baixou a biblioteca anteriormente, recomendo baixar a versão mais recente, pois a biblioteca é atualizada constantemente e alguns exemplos podem não funcionar na versão antiga da biblioteca.


Infelizmente, as profundidades de bits das variáveis ​​de registro consideradas anteriormente claramente não são suficientes para serem usadas como ponteiros de memória. Portanto, antes de prosseguir diretamente para a discussão de ponteiros, consideramos outra classe de descrição de dados. A maioria dos comandos na arquitetura AVR Mega é projetada para funcionar apenas com operandos de registro, ou seja, ambos os operandos e o resultado têm tamanho de 8 bits. No entanto, existem várias operações em que dois registros RON localizados consecutivamente são considerados como um único registro de 16 bits. Existem poucas operações desse tipo, e elas estão focadas principalmente no trabalho com ponteiros.


Do ponto de vista da sintaxe da biblioteca, trabalhar com um par de registradores é quase o mesmo que trabalhar com uma variável de registrador. Considere um pequeno exemplo em que tentamos trabalhar com um par de registradores. Para economizar espaço aqui e abaixo, forneceremos o resultado da execução somente onde for necessário explicar certos recursos da geração de código.


var m = new Mega328(); var dr1 = m.DREG(); var dr2 = m.DREG(); dr1.Load(0xAA55); dr2.Load(0x55AA); dr1++; dr1--; dr1 += 0x100; dr1 += dr2; dr2 *= dr1; dr2 /= dr1; var t = AVRASM.Text(m); 

Neste exemplo, declaramos duas variáveis ​​de 2 bytes localizadas em pares de registradores usando o comando DREG (). Com os seguintes comandos, atribuímos a eles o valor inicial e executamos uma série de operações aritméticas. Como você pode ver no exemplo, a sintaxe para trabalhar com um par de registradores é basicamente a mesma que trabalhar com um registrador regular. Um par de registradores também pode ser considerado como uma variável composta por dois registradores independentes. O registro é acessado como um conjunto de dois registros de 8 bits através da propriedade High para acessar os 8 bits superiores como um registro de 8 bits, e a propriedade Low para acessar os 8 bits inferiores. O código ficará assim


 var m = new Mega328(); var dr1 = m.DREG(); dr1.Load(0xAA55); dr1.Low--; dr1.High += dr1.Low; var t = AVRASM.Text(m); 

Como você pode ver no exemplo, podemos trabalhar com High e Low como variáveis ​​de registro independentes, incluindo a execução de várias operações aritméticas e lógicas entre elas.


Agora que descobrimos variáveis ​​de tamanho duplo, podemos começar a descrever como trabalhar com variáveis ​​na memória. A biblioteca permite que você trabalhe com variáveis ​​de 8, 16 bits e matrizes de bytes de comprimento arbitrário. Considere um exemplo de alocação de espaço para variáveis ​​na RAM.


 var m = new Mega328(); var bt = m.BYTE(); //8-    var wd = m.WORD(); //16-    var arr = m.ARRAY(16); //  16  var t = AVRASM.Text(m); 

Vamos ver o que aconteceu.


 RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DSEG L0002: .BYTE 16 L0001: .BYTE 2 L0000: .BYTE 1 

Na seção de definição de dados, temos uma alocação de memória. Observe que a ordem de alocação é diferente da declaração de variáveis. Isso não é coincidência. A alocação de memória para variáveis ​​ocorre após a classificação em ordem decrescente, de acordo com os seguintes critérios (em ordem decrescente de importância) O múltiplo máximo de divisor de grau 2 → O tamanho da memória alocada. Isso significa que, se quisermos alocar 4 matrizes de tamanho de 64, 48,40 e 16 bytes, a ordem de alocação, independentemente da ordem da declaração, terá a seguinte aparência:


Comprimento 64 - Múltiplos divisores máximos de grau 2 = 64
Comprimento 48 - Múltiplos divisores máximos de grau 2 = 16
Comprimento 16 - Múltiplo máximo de divisor de grau 2 = 16
Comprimento 40 - Múltiplos divisores máximos de grau 2 = 8
Isso é feito para simplificar o controle dos limites da matriz.
e reduza o tamanho do código nas operações com ponteiros. Não podemos executar diretamente nenhuma operação com variáveis ​​na memória; portanto, tudo o que está disponível para nós é a leitura / gravação para registrar variáveis. A maneira mais simples de trabalhar com variáveis ​​na memória é o endereçamento direto.


 var m = new Mega328(); var bt = m.BYTE(); // 8-    var rr = m.REG(); //    rr.Load(0xAA); //  rr   0xAA rr.Mstore(bt); //     rr.Clear(); //  rr.Mload(bt); //    var t = AVRASM.Text(m); 

Neste exemplo, declaramos uma variável na memória e uma variável de registro. Depois disso, atribuímos à variável o valor 0x55 e escrevemos na variável na memória. Depois apagado e restaurado de volta.


Para trabalhar com elementos de matriz, usamos a seguinte sintaxe


 var rr = m.REG(); var arr = m.ARRAY(10); rr.MLoad(arr[5]); 

A numeração dos elementos na matriz começa com 0. Portanto, no exemplo acima, o valor 6 do elemento da matriz é gravado na célula rr.


Agora você pode ir para o endereçamento indireto. A biblioteca possui seu próprio tipo de dados para um ponteiro para o espaço de memória RAM - MEMPtr . Vamos ver como podemos usá-lo. Modificamos nosso exemplo anterior para que o trabalho com a variável na memória seja realizado através do ponteiro.


 var m = new Mega328(); var bt1 = m.BYTE(); var bt2 = m.BYTE(); var rr = m.REG(); var ptr = m.MEMPTR(); //  ptr ptr.Load(bt1); //ptr   bt1 rr.Load(0xAA); // rr - 0xAA ptr.MStore(rr); //  bt1 0xAA rr.Load(0x55); // rr - 0x55 ptr.Load(bt2); //ptr   bt2 ptr.MStore(rr); //  bt2 0x55 ptr.Load(bt1); //ptr   bt1 ptr.MLoad(rr); //  rr  0xAA var t = AVRASM.Text(m); 

Pode ser visto no texto que declaramos primeiro o ponteiro ptr e, em seguida, executamos operações de gravação e leitura com ele. Além da capacidade de alterar o endereço de leitura / gravação no comando durante a execução, o uso do ponteiro simplifica o trabalho com matrizes, combinando a operação de leitura / gravação com o incremento / decremento do ponteiro. Vejamos um programa que pode preencher uma matriz com um valor específico.


 var m = new Mega328(); var bt1 = m.ARRAY(4); //   4  var rr = m.REG(); var ptr = m.MEMPTR(); ptr.Load(bt1.Label); //ptr   bt1 rr.Load(0xAA); // rr - 0xAA ptr.MStoreInc(rr); //  bt1 0xAA ptr.MStoreInc(rr); //  bt1+1 0xAA ptr.MStoreInc(rr); //  bt1+2 0xAA ptr.MStoreInc(rr); //  bt1+3 0xAA rr.Clear(); rr.MLoad(bt1[2]); //  rr 3-   var t = AVRASM.Text(m); 

Neste exemplo, aproveitamos a capacidade de incrementar um ponteiro ao gravar na memória.
Em seguida, passamos à capacidade da biblioteca de controlar o fluxo de comandos. Se for mais fácil, como programar saltos e loops condicionais e incondicionais usando a biblioteca. A maneira mais fácil de gerenciar isso é usar comandos de navegação de etiqueta. Os rótulos de um programa são declarados de duas maneiras diferentes. A primeira é que, com a equipe AVRASM.Label , criamos um rótulo para uso futuro, mas não o inserimos no código do programa. Este método é usado para criar saltos para frente, ou seja, nos casos em que o comando de salto deve preceder o rótulo. Para definir o rótulo no local necessário do código do assembler, você deve executar o comando AVRASM.newLabel ([variável do rótulo criado anteriormente]) . Para voltar, você pode usar uma sintaxe mais simples, definindo um rótulo e atribuindo seu valor a uma variável com um comando AVRASM.newLabel () sem parâmetros.


O tipo mais simples de transição é uma transição incondicional. Para chamá-lo, usamos o comando GO ([jump_mark]] . Vamos ver como fica com um exemplo.


 var m = new Mega328(); var r = m.REG(); //  var lbl1 = AVRASM.Label;//        m.GO(lbl1); r++; //    r++; AVRASM.NewLabel(lbl1);//  //  var lbl2 = AVRASM.NewLabel();//    r--; //    r--; m.GO(lbl2); var t = AVRASM.Text(m); 

Transições condicionais têm mais controle sobre o fluxo de execução. Seu comportamento depende do estado dos sinalizadores de operação e isso possibilita o controle do fluxo de operações, dependendo do resultado de sua execução. A biblioteca usa a função SE para descrever um bloco de comandos que devem ser executados somente sob determinadas condições. Vejamos um exemplo.


 var m = new Mega328(); var rr1 = m.REG(); var rr2 = m.REG(); rr1.Load(0x22); rr2.Load(0x33); m.IF(rr1 == rr2, () => { AVRASM.Comment(" - ,  "); }); var t = AVRASM.Text(m); 

Como a sintaxe do comando IF não é muito familiar, considere-a em mais detalhes. O primeiro argumento aqui é a condição de transição. A seguir está o método no qual o bloco de código é colocado, que deve ser executado se a condição for atendida. Uma variante da função é a capacidade de descrever uma ramificação alternativa, ou seja, um bloco de código que deve ser executado se a condição não for atendida. Além disso, você pode prestar atenção à função AVRASM.Comment () , com a qual podemos adicionar comentários ao assembler de saída.


 var m = new Mega328(); var rr1 = m.REG(); var rr2 = m.REG(); rr1.Load(0x22); rr2.Load(0x33); m.IF(rr1 == rr2, () => { AVRASM.Comment(" - ,  "); },()=> { AVRASM.Comment(" - ,   "); }); AVRASM.Comment(" "); var t = AVRASM.Text(m); 

O resultado neste caso terá a seguinte aparência


 RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF R0000 = r20 .DEF R0001 = r21 ldi R0000,34 ldi R0001,51 cp R0000,R0001 brne L0002 ;---  - ,   --- xjmp L0004 L0002: ;---  - ,    --- L0004: ;---   --- .DSEG 

Os exemplos anteriores mostram uma opção de ramificação condicional na qual um comando de comparação é usado para determinar as condições de ramificação. Em alguns casos, isso não é necessário, pois as condições de transição devem ser determinadas pelo estado dos sinalizadores após a última operação executada. A sintaxe a seguir é fornecida para esses casos.


 var m = new Mega328(); var rr1 = m.REG(); rr1.Load(0x22); rr1--; m.IFEMPTY(() =>AVRASM.Comment(",    0")); var t = AVRASM.Text(m); 

Neste exemplo, a função IFEMPTY verifica o status do sinalizador Z após um incremento e executa o código do bloco condicional quando atinge 0.
A mais flexível em termos de uso pode ser considerada a função LOOP . Destina-se a uma descrição conveniente dos ciclos do programa. Considere a assinatura dela


 LOOP(Register iter, Action<Register, string> Condition, Action<Register, string> body) 

O parâmetro iter atribui uma variável de registro que pode ser usada como um iterador em um loop. O segundo parâmetro contém um bloco de código que descreve as condições para sair do loop. O iterador atribuído e o rótulo inicial do loop a serem retornados são passados ​​para este bloco de código. O último parâmetro é usado para descrever o bloco de código do corpo principal do loop. O exemplo mais simples de uso da função LOOP é um loop de stub, ou seja, um loop infinito para pular para a mesma linha. A sintaxe nesse caso será a seguinte


 m.LOOP(m.TempL, (r, l) => m.GO(l), (r,l) => { }); 

O resultado da compilação é dado abaixo.


 L0002: xjmp L0002 

Vamos voltar ao nosso exemplo de preenchimento de uma matriz com um determinado valor e alterá-lo para que o preenchimento seja realizado em um loop


 var m = new Mega328(); var rr1 = m.REG(); var rr2 = m.REG(); var arr = m.ARRAY(16); var ptr = m.MEMPTR(); ptr.Load(arr[0]); //     rr2.Load(16); //    rr1.Load(0xAA); //   m.LOOP(rr2, (r, l) => //rr2     . { r--; //   m.IFNOTEMPTY(l); // ,   }, (r,l) => ptr.MStoreInc(rr1)); //   var t = AVRASM.Text(m); 

O código de saída, neste caso, terá a seguinte aparência


 RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF R0000 = r20 .DEF R0001 = r21 ldi YL, LOW(L0002+0) ldi YH, HIGH(L0002+0) ldi R0001,16 ldi R0000,170 L0003: st Y+,R0000 dec R0001 brne L0003 L0004: .DSEG L0002: .BYTE 16 

Outra maneira de organizar transições é através de transições endereçadas indiretamente. O analógico mais próximo em idiomas de alto nível para eles é um ponteiro para uma função. O ponteiro nesse caso não apontará para o espaço da RAM, mas para o código do programa. Como o AVR possui uma arquitetura de Harvard e usa seu próprio conjunto específico de instruções para acessar a memória do programa, o ROMPtr, em vez do MEMPtr descrito acima, é usado como ponteiro. O caso de uso para transições endereçadas indiretamente pode ser ilustrado pelo exemplo a seguir.


 var m = new Mega328(); var block1 = AVRASM.Label; var block2 = AVRASM.Label; var block3 = AVRASM.Label; var ptr = m.ROMPTR(); ptr.Load(block1); //     var loop = AVRASM.NewLabel(); AVRASM.Comment("   "); m.GOIndirect(ptr); //   ,     AVRASM.NewLabel(block1); AVRASM.Comment("  1"); ptr.Load(block2); m.GO(loop); AVRASM.NewLabel(block2); AVRASM.Comment("  2"); ptr.Load(block3); m.GO(loop); AVRASM.NewLabel(block3); AVRASM.Comment("  3"); ptr.Load(block1); m.GO(loop); var t = AVRASM.Text(m); 

Neste exemplo, temos 3 blocos de comandos. Após a conclusão de cada bloco, o controle é transferido de volta ao comando de filial endereçado indiretamente. Como no final do bloco de comando, definimos o vetor de transição para um novo bloco a cada vez, a execução será semelhante ao Bloco1 → Bloco2 → Bloco3 → Bloco1 ... e assim por diante em um círculo. Esse comando, junto com os comandos de ramificação condicional, permite meios simples e convenientes da linguagem para descrever algoritmos tão complexos como uma máquina de estado.


Uma versão mais sofisticada de uma ramificação endereçada indiretamente é o comando SWITCH . Ele não usa um ponteiro para um rótulo de transição para a transição, mas um ponteiro para uma variável na memória na qual o endereço do rótulo de transição está armazenado.


 var m = new Mega328(); var block1 = AVRASM.Label; var block2 = AVRASM.Label; var block3 = AVRASM.Label; var arr = m.ARRAY(6); var ptr = m.MEMPTR(); //    m.Temp.Load(block1); m.Temp.Store(arr[0]); m.Temp.Load(block2); m.Temp.Store(arr[2]); m.Temp.Load(block3); m.Temp.Store(arr[4]); ptr.Load(arr[0]); //     var loop = AVRASM.NewLabel(); m.SWITCH(ptr); //   ,     AVRASM.NewLabel(block1); AVRASM.Comment("  1"); ptr.Load(arr[2]); //       m.GO(loop); AVRASM.NewLabel(block2); AVRASM.Comment("  2"); m.Temp.Load(block3); ptr.MStore(m.Temp); //       m.GO(loop); AVRASM.NewLabel(block3); AVRASM.Comment("  3"); ptr.Load(arr[0]); //       m.GO(loop); 

Neste exemplo, a sequência de transição será a seguinte: Bloco1 → Bloco2 → Bloco3 → Bloco1 → Bloco3 → Bloco1 → Bloco3 → Bloco1 ... Conseguimos implementar um algoritmo no qual os comandos do Bloco2 são executados apenas no primeiro ciclo.


Na próxima parte da postagem, consideraremos trabalhar com dispositivos periféricos, implementar interrupções, rotinas e muito mais.

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


All Articles