1
votes

Assemblage AVR - numéro de bit à masquer

Dans mon programme ATtiny84a AVR Assembly, je me retrouve avec un nombre de bits compris entre 0 et 7, dans un registre, disons r16. Maintenant, je dois créer un masque avec ce nombre de bits défini. Pour compliquer les choses, le timing de l'opération doit être le même, quel que soit le bit défini.

Par exemple, si r16 = 5, le masque résultant sera 0x20 (bit 5 défini).

Jusqu'à présent, j'ai décalé un peu en position par LSL et en utilisant r16 (le nombre de bits) comme compteur de boucle, puis pour garder le timing exact quel que soit le nombre de bits, faites une boucle factice de NOP 8-r16 fois.

L'instruction d'assemblage SBR définit le (s) bit (s) dans un registre à partir d'un masque afin qu'il ne puisse pas être utilisé. L'instruction d'assemblage SBI définit un bit dans un registre d'E / S à partir du numéro de bit, mais c'est une constante, pas un registre (j'aurais pu utiliser un registre d'E / S comme registre temporaire). < / p>

Le masque est ensuite utilisé pour effacer un bit dans un emplacement mémoire, donc s'il y a une autre solution pour le faire à partir d'un nombre de bits dans un registre, alors ça va aussi.

J'ai une autre solution à essayer (basée sur le décalage avec carry) mais j'espérais que quelqu'un ait une solution plus élégante que les boucles et les décalages.


4 commentaires

Que diriez-vous d'une table de consultation avec 8 entrées?


L'approche @Michael sub / ror ci-dessous échoue également correctement sur l'index d'entrée hors limites, alors que la recherche ne serait probablement pas sans vérifications supplémentaires ou plus de flash.


@bigjosh 19 mots, pas octets


@ReAl Si vrai. Commentaire supprimé. Merci.


5 Réponses :


6
votes

Je pense que votre intuition avec les changements et les portages est une solution élégante. En gros, vous décrémentez le registre d'index, définissez le report lorsque le décrément est égal à zéro, puis décalez le report dans le registre de sortie.

Vous pouvez utiliser subtract pour faire la décrémentation, qui sera automatiquement définir le bit de retenue lorsque l'index atteint 0.

Vous pouvez utiliser une rotation vers la droite au lieu du décalage car cela vous permet de déplacer les bits dans la bonne direction pour correspondre au décement.

Ensuite, vous pouvez devenir vraiment délicat et utiliser un bit sentinelle dans la sortie comme compteur de boucle psuedu pour se terminer après 8 itérations de boucle.

Donc, quelque chose comme ...

; Assume r16 is the index 0-7 of the bit to set in the output byte
; Assume r17 is the output byte
; r17 output will be 0 if r16 input is out of bounds
; r16 is clobbered in the process (ends up as r16-8)

ldi r17, 0b10000000 ; Sort of a psuedo-counter. When we see this 
                    ; marker bit fall off the right end
                    ; then we know we did 8 bits of rotations

loop:
subi r16,1  ; decrement index by 1, carry will be set if 0
ror r17     ; rotate output right, carry into the high bit
brcc loop   ; continue until we see our marker bit come output

Je compte 4 mots (8 octets) de stockage et 24 cycles cette opération sur tous les AVR, donc je pense gagnant sur la taille, étonnamment (même pour moi!) battant le champ fort de les entrées basées sur une table de recherche.

Comprend également une gestion judicieuse des conditions hors liaisons et aucun autre registre n'a changé en plus de l'entrée et de la sortie. Les rotations répétitives aideront également à empêcher l'accumulation de dépôts de carbone dans les portes de changement de vitesse ALU.

Un grand merci à @ReAI et @PeterCordes qui ont rendu ce code possible! :)


11 commentaires

Cela vaudrait-il la peine de considérer un ijmp calculé dans une séquence d'instructions juste ror ou rol ? Si vous commencez avec un ensemble de report, le premier se décale dans un 1 , et le reste le décale simplement. Le pire des cas est probablement similaire, et c'est probablement ce qui compte pour la plupart des applications.


@PeterCordes Idée intéressante! Malheureusement, je pense que cette approche serait liée à la table de recherche d'espace, mais elle perdrait toujours du temps car elle nécessite toutes les mêmes étapes pour configurer et restaurer le registre Z, et l'IJMP remplace le LPM. Y a-t-il un moyen de sauter dans une table sur AVR sans le reg Z?


Je ne connais pas très bien AVR, juste ce que je vois en feuilletant un tableau d'instructions comme this . Bon point que si vous êtes défini pour un ijmp , vous pourriez aussi bien charger des données à partir d'une table à la place. Cependant, vous n'avez pas nécessairement besoin de sauvegarder / restaurer Z (ou X ou Y); écrasez-le simplement et laissez le code suivant le définir sur quelque chose s'il le veut.


Les deux premières commandes doivent être clr r17 $ inc r17


@ReAI Correction du clr à r17 , merci! Je pense que inc devrait être r16 car cela est destiné à incrémenter l'entrée afin que nous puissions la décrémenter pour définir le drapeau de retenue. Ça a du sens?


Pouvez-vous utiliser un subi normal pour le premier au lieu de clc / sbci r16,1 ? Ou peut-être cmp r16, 1 pour définir Carry si l'entrée est 0 sans la modifier?


C'est une solution très créative pour mettre un peu, j'aime ça!


Ah oui, @PeterCordes c'est bien mieux! J'avais supposé que subi ne mettait pas à jour le bit de report mais les fiches techniques disent le contraire. Cela me permet également de changer tous les autres sbci et ainsi de me débarrasser du clr r17 en haut puisque maintenant peu importe ce qui est tourné dans carry par le ror s. Merci!


Sur la plupart des ISA qui ont des instructions add / adc séparées, une opération BigInt ressemble à add / adc / adc / ... ou sub / sbb / sbb / ... , avec add / sub écrivant les drapeaux normalement mais ne lisant pas les drapeaux comme entrée. C'est aussi ainsi que sub / cmp définit des indicateurs pour les branches conditionnelles à tester; par exemple. brlo teste simplement si l'indicateur de retenue est défini. J'espère que cela vous aidera à comprendre pourquoi il est conçu de cette façon.


@PeterCordes Entièrement compris - juste au moment où j'ai vu que l'instruction s'appelait "soustraire sans porter", j'ai lu bêtement que cela signifie que je n'utilisais pas du tout le bit de report! Vérifiez toujours les fiches techniques! Quoi qu'il en soit, cet échange a finalement conduit à un bien meilleur algorithme, alors merci encore! :)


Bonne idée pour une boucle à taille optimisée, joli changement pour remplacer la version déroulée qui s'est avérée ne pas avoir d'avantages par rapport à l'intelligent sbrc / swap de ReAl. Mais oui, je pensais que "soustraire sans porter" ressemblait à une drôle de description. J'avais joué avec AVR GCC sur godbolt.org pour voir comment GCC faisait des choses comme l'incrémentation d'un int ou pointeur, donc je n'ai pas été induit en erreur. Je suppose que travailler avec des entiers plus larges est si courant sur une machine 8 bits que c'est l'exception, pas la règle? par exemple. les rotations sont des rotations transversales (contrairement à x86 où rcr rot-through-carry, ror ne l'est pas.)



2
votes

9 mots, 9 cycles

ldi r17, 1

; 4
sbrc    r16, 2  ; if n >= 4
swap    r17     ; 00000001 -> 00010000, effectively shift left by 4

; 2
sbrc    r16, 1
lsl     r17
sbrc    r16, 1
lsl     r17

; 1
sbrc    r16, 0
lsl     r17


1 commentaires

C'est adorable! Je n'ai jamais vu SWAP utilisé de manière productive auparavant!



2
votes

Puisque votre sortie n'a que 8 variantes, vous pouvez utiliser une table de recherche. Cela fera exactement le les mêmes opérations quelle que soit l'entrée ayant donc exactement le même temps d'exécution.

  ldi r30, low(shl_lookup_table * 2) // Load the table address into register Z
  ldi r31, high(shl_lookup_table * 2)

  clr r1 // Make zero

  add r30, r16 // Add our r16 to the address
  adc r31, r1  // Add zero with carry to the upper half of Z

  lpm r17, Z // Load a byte from program memory into r17

  ret // assuming we are in a routine, i.e. call/rcall was performed

...

shl_lookup_table:
  .db 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80


2 commentaires

Ne pouvez-vous pas aligner votre table de recherche pour éviter clr / adc ? (Ou vraiment, assurez-vous simplement qu'il ne franchit pas une limite de 256 octets, mais l'alignement par 8 est un moyen facile de le faire.) Mieux encore, si vous alignez par 256, vous pouvez supprimer le ldi r30 . Ou si vous faites en sorte que la valeur soit dans r30 en premier lieu, vous pouvez ajouter r30, low (shl_lookup_table * 2) , je pense.


Publié une réponse avec cette idée: 5 cycles, 7 mots de stockage total, y compris le tableau.



1
votes

Une table de recherche alignée sur 8 octets simplifie l'indexation devrait être bonne pour les puces AVR qui prennent en charge lpm - Charger à partir de la mémoire programme. (Optimisé à partir de la réponse de @ AterLux). Aligner le tableau sur 8 signifie que les 8 entrées ont le même octet de poids fort de leur adresse. Et pas de wrapping des 3 bits bas donc nous pouvons utiliser ori au lieu d'avoir à nier l'adresse pour subi . ( adiw ne fonctionne que pour 0..63, donc il se peut que vous ne puissiez pas représenter une adresse.)

Je montre le meilleur des cas où vous pouvez facilement générer l'entrée dans r30 (faible moitié de Z) en premier lieu, sinon vous avez besoin d'un mov . De plus, cela devient trop court pour valoir la peine d'appeler une fonction, donc je ne montre pas un ret , juste un fragment de code.

Suppose que l'entrée est valide (en 0..7 ); considérez @ ReAl's si vous devez ignorer les bits élevés, ou simplement andi r30, 0x7

Si vous pouvez facilement recharger Z après cela, ou si vous n'avez pas besoin de le conserver de toute façon, ceci c'est génial. Si l'écrasement de Z est nul, vous pouvez envisager de construire la table en RAM lors du démarrage initial (avec une boucle) afin de pouvoir utiliser X ou Y pour le pointeur avec une charge de données au lieu de lpm . Ou si votre AVR ne prend pas en charge lpm .

## gas / clang syntax
### Input:    r30 = 0..7 bit position
### Clobbers: r31.  (addr of a 256-byte chunk of program memory where you might have other tables)
### Result:   r17 = 1 << r30

  ldi   r31, hi8(shl_lookup_table)    // Same high byte for all table elements.  Could be hoisted out of a loop
  ori   r30, lo8(shl_lookup_table)    // Z = table | bitpos  = &table[bitpos] because alignment

  lpm   r17, Z

.section .rodata
.p2align 3        // 8-byte alignment so low 3 bits of addresses match the input.
           // ideally place it where it will be aligned by 256, and drop the ORI
           // but .p2align 8 could waste up to 255 bytes of space!  Use carefully
shl_lookup_table:
  .byte 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80
  • size = code 3 mots + 8 octets (4 mots) data = 7 mots . (Plus jusqu'à 7 octets de remplissage pour l'alignement si vous ne faites pas attention à la disposition de la mémoire du programme)
  • cycles = 1 (ldi) + 1 (ori) + 3 (lpm) = 5 cycles

Dans une boucle, si vous avez besoin d'autres données dans le même bloc de 256B de mémoire programme, le ldi r31, hi8 ne peut être soulevé / fait qu'une seule fois.

Si vous pouvez aligner le tableau par 256, cela économise un mot de code et un cycle de temps. Si vous sortez également le ldi de la boucle, cela ne laisse que le lpm à 3 cycles .

(Non testé , Je n'ai pas de chaîne d'outils AVR autre que clang -target avr . Je pense que GAS / clang ne veut que des références de symboles normales et gère le symbole * 2 en interne. assemble correctement avec clang -c -target avr -mmcu = atmega128 shl.s , mais le démontage du .o plante llvm-objdump -d 10.0.0.) p>


0 commentaires

1
votes

Merci à tous pour vos réponses créatives, mais j'ai choisi la table de recherche sous forme de macro. Je trouve que c'est la solution la plus flexible car je peux facilement avoir différentes tables de recherche à des fins diverses à des cycles fixes de 7.

; @0 mask table
; @1 bit register
; @2 result register
.MACRO GetMask
    ldi     ZL,low(@0)
    ldi     ZH,high(@0)
    add     ZL,@1
    adc     ZH,ZERO
    lpm     @2,Z
.ENDM

bitmask_lookup:
    .DB 0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80
inverse_lookup:
    .DB ~0x01,~0x02,~0x04,~0x08,~0x10,~0x20,~0x40,~0x80
lrl2_lookup:
    .DB 0x04,0x08,0x10,0x20,0x40,0x80,0x01,0x02

ldi r16,2
GetMask bitmask_lookup, r16, r1 ; gives r1 = 0b00000100
GetMask inverse_lookup, r16, r2 ; gives r2 = 0b11111011
GetMask lrl2_lookup,    r16, r3 ; gives r3 = 0b00010000 (left rotate by 2)

L'espace n'est pas tellement un problème, mais la vitesse l'est. Cependant, je pense que c'est un bon compromis et je ne suis pas obligé d'aligner les données sur quatre mots. 7 contre 5 cycles est le prix à payer.

J'ai déjà un registre "ZERO" réservé dans tout le programme, donc cela ne me coûte rien de plus pour faire l'ajout de 16 bits.


5 commentaires

Aligner votre table enregistrerait l'instruction adc , comme indiqué dans ma réponse. Vous pouvez également économiser de l'espace pour la version lrl2 en la faisant chevaucher la table normale. (c'est-à-dire ajoutez simplement 2 octets supplémentaires). Si vous utilisez un mode d'adressage de lpm r1, Z + 2 , vous pouvez toujours simplement faire ajouter ZL, @ 1 pour la partie basse, avec la gestion du calcul d'adresse possible l'octet de poids fort de Z. Notez que l'alignement de données requis n'est pas un mot, c'est un quadruple mot (8 octets) pour ma réponse, pas seulement un "alignement de mot de données".


Oui bon point, j'ai édité un alignement de mot à quadruple. Les autres tables n'étaient que des exemples d'utilisations différentes, mais si j'utilise la rotation, les tables qui se chevauchent économiseraient effectivement quelques octets.


Terminologie: "invert ed _lookup" serait un nom plus clair. La recherche "inverse" donne l'impression que vous mappez 1 << n à n , le mappage inverse de n à 1 << n . Si vous prenez la peine d'avoir toute une table supplémentaire pour la version binaire NOTed du résultat (au lieu d'utiliser un com r1 après le chargement), il me semble étrange que vous ne soyez pas prêt à aller extra mile et éviter adc en alignant les données; cela prendrait strictement moins d'espace qu'une table supplémentaire entière, et vous donnerait le résultat inversé dans le même temps que maintenant, le résultat normal en moins. (Ou peut-être que ce sont censés être des alternatives, pas toutes utilisées)


De plus, si vous utilisez votre ldi / add au lieu de ori , vous pouvez supprimer le adc si la table ne couvre tout simplement pas une limite de 256 octets. IDK en termes pratiques à quel point il est facile de faire une sorte de assert au moment de la construction pour vérifier cela.


Pourriez-vous éviter d'avoir besoin d'un registre ZERO ici en utilisant sub / sbci depuis fin du tableau, avec les tables dans l'ordre inverse? Il n'y a pas de adci mais il y a sbci ZH, 0 . Je suppose qu'un zéro reg est utile pour d'autres cas, mais peut-être que d'autres futurs lecteurs pourraient en bénéficier. (Bien que, comme je l'ai dit, aligner les tableaux semble être une méthode encore meilleure.)