Pfad: Home => AVR-Überblick => Programmiertechniken => Adressieren =>> Portregister    (This page in English: Flag EN) Logo

Addressierung von Speicherzellen in AVRs

  1. Adressierung von SRAM-Speicher
  2. Adressierung von Portregistern,
    1. Adressen von Portregistern,
    2. Adressieren von klassischen Portregistern,
    3. Adressieren von erweiterten Portregistern,
    4. Addressieren von Portregistern mit Zeigern (zirkuläres LED-Licht),
  3. Addressierung von EEPROM-Speicherzellen, und
  4. Addressierung von Flash-Speicherzellen.

2 Adressen von Portregistern in AVRs

Addressraum in AVRs Mit Portregistern wird der Unterschied zwischen physikalischen Adressen und Zeigeradressen besonders relevant. Beide Adressierungsarten kommen vor, und der Anfänger kann dadurch verwirrt werden.

Zwei Arten von Portregistern sind zu unterscheiden:
  1. klassische Portregister, die mit den Instruktionen OUT und IN beschrieben und gelesen werden können, sowie
  2. erweiterte Portregister, die nur mit Zeigeroperationen (STS/LDS, ST/LD, STD/LDT) beschrieben und gelesen werden können.

2.2 Adressieren von klassischen Portregistern

Diese Zugriffsart verwendet die Instruktionen OUT Ausgabeport,Register zum Schreiben und IN Register,Einbgabeport zum Lesen. Beide Instruktionen verwenden die physische Adresse des Aus- und Eingabeports.

Der Zugriff mit OUT und IN ist auf die ersten 64 Bytes der physischen Portadressen begrenzt. Erweiterte Portadressen, wie sie neuere und größere AVRs haben, sind damit nicht zugänglich (siehe weiter unten in Kapitel 2.3).

Wenn in einem 8 Bit umfassenden Portregister nur eines der acht Bits geändert werden soll, kann man dafür die Instruktionen SBI Port,Bit (zum Setzen) und CBI Port,Bit verwenden. Diese beiden Instruktionen ersetzen die folgende Befehlsfolge:

  in R16,port ; Lese den Port
  ; Setze das Bit
  sbr R16,1<<Bit ; Setze das Bit, oder:
  ori R16,1<<Bit ;   alternatives Setzen des Bits
  ; Loesche das Bit
  cbr R16,1<<bit ; Loesche (Clear) das Bit, oder:
  andi R16,256-1<<bit ; alternatives Loeschen des Bits
  out port,R16 ; Schreibe den Port

Während diese etwas längere Version bei allen 64 Portregistern funktioniert, ist das kürzere SBI oder CBI nur bei der unteren Hälfte der Portregister zulässig.

SBI und CBI benötigen zwei Taktzyklen, während die allgemeingültige Methode drei Taktzyklen und ein Register benötigt.

Das Torkeln eines Bits in einem Portregister kann mit der Exklusiv-ODER-Instruktion EOR ausgeführt werden, die im ersten Register alle diejenigen Bits umkehrt, die im zweiten Register Eins sind.

  in R16,port ; Lese den Port
  ldi R17,1<<bit ; Das Portbit das torkeln soll, oder
  ldi R17,(1<<bit1)|(1<<bit2) ; zwei Bits auf einmal torkeln
  eor R16,R17 ; Exklusiv-ODER
  out port,R16 ; Schreibe den getorkelten port

Wenn der Port, der getorkelt werden soll, ein I/O-Port ist, dann gibt es bei den modereren AVR-Typen die Möglichkeit, durch Schreiben in den Eingabeport PINx die Portpins in PORTx torkeln zu lassen:

  ldi R16,(1<<1)|(1<<3) ; Torkele Pin 1 und 3
  out PINA,R16 ; in PORTA

Aber der Zugriff auf die untersten 64 Bytes der Portregister ist mit der Zeigeradresse möglich, man muss nur 0x0020 zur physischen Adresse hinzu addieren:

  ldi R16,0xAA ; AA in Register R16
  sts PORTA+0x20,R16 ; und in PORTA's Zeigeradresse schreiben

2.3 Adressieren von erweiterten Portregistern

Wenn ein IN- oder OUT-Zugriff auf ein existierendes Portregister mit der Fehlermeldung endet, die Adresse liege außerhalb des zulässigen Adressbereichs, dann handelt es sich bei diesem Portregister um eines im erweiterten Portbereich oberhalb von 0x003F. Solche erweiterten Portregister-Bereiche gibt es bei vielen neueren AVRs, da die 64 Byte nicht mehr ausgereicht haben, um die interne umfangreiche Hardware zu steuern.

In diesem Fall muss die Zeigeradresse verwendet werden, um mit STS/LDS oder ST/LD auf diese Portregister zuzugreifen. In der def.inc solcher AVR-Typen sind solche Ports direkt mit der Zeigeradresse adressiert und man muss keine 0x0020 hinzuaddieren. Man kann also einfach statt OUT STS Port,Register und statt IN LDS Register,Port verwenden. Beide Instruktionen benötigen allerdings zwei Taktzyklen.

Natürlich sind auch die Instruktionen SBI/CBI und die Verzweigungs- Instruktionen SBIS/SBIC für den erweiterten Portregister-Bereich nicht zulässig und müssen durch andere Instruktionen ersetzt werden.

2.4 Adressieren mit Zeigern, Beispiel: zirkuläres LED-Licht

Geeignete AVR-Typen mit 32 I/O-Portpins Um die Adressierung mit Zeigern aufzuzeigen, hier das Beispiel eines mit 32 LEDs bestückten Zirkularbeleuchtung, wie sie in Flugzeugen zum Einsatz kommt, um den geneigten Passagieren im Notfall den Weg zum nächsten Notausgang anzuzeigen. Wenn der gesamte Zyklus eine Sekunde dauern soll, müssen die 32 LEDs mit einer Frequenz von 32 Hz umgeschaltet werden, so dass jede LED für 21,25 ms lang an ist.

Das erfordert einen AVR mit vier kompletten 8-Bit-I/O-Ports. Dieses Bild zeigt alle AVR-Typen, die dafür in Frage kommen. Wir können jeden aufgelisteten Typ verwenden, z. B. einen ATmega324PA.

Schaltbild des 32-Bit-Zirkularlichts Dies zeigt die nötige Hardware. Sieht ziemlich einfach aus, viele Widerstände und LEDs. Wenn immer gleichzeitig nur eine einzige LED an sein soll und alle anderen Aus, können wir alle Kathoden mit einem einzigen Widerstand verbinden und brauchen nur einen einzigen Widerstand. Wenn immer nur eine LED in einem 8-Bit-I/O-Port an sein soll, reduziert sich die Anzahl Widerstände auf vier.

Der erste Schritt in der Software ist, alle I/O-Richtungsregister als Ausgang zu schalten. Das kann ohne und mit Zeigerzugriff erfolgen. Die Version ohne wäre:

  ldi R16,0xFF ; Alle Bits als Ausgang 
  out DDRA,R16
  out DDRB,R16
  out DDRC,R16
  out DDRD,R16
  clr R16 ; Alle oberen 24 Bits loeschen
  out PORTD,R16
  out PORTC,R16
  out PORTB,R16
  ldi R16,0x01 ; Bit 0 setzen
  out PORTA,R16

Die Portregister der vier I/O-Ports Dies hier sind die Portregister, die die vier I/O-Ports in einem ATmega324PA steuern. PINx, DDRx und PORTx liegen alle schön regelmäßig in einer Reihe. Mit einer Zeiger-Basis-Adresse von 0x0020 in Y oder Z sind die Portregister PINx mit einem Versatz von 0, 3, 6 und 9, die DDRx mit einem Versatz von 1, 4, 7 und 10 und die PORTx mit 2, 5, 8 und 11 für die Instruktion STD Y+Versatz,R16 zugänglich. Wer STD und die Bedeutung von "Versatz" noch nicht kennt, liest sich dies hier mal kurz durch.

Die Version mit dem Zeiger würde daher folgendermaßen aussehen:

  ldi R16,0xFF ; Alle Bits auf Ausgang
  ldi YH,High(PINA+0x20) ; Zeige Y auf PINA's Zeigeradresse
  ldi YL,Low(PINA+0x20)
  std Y+1,R16 ; Setze die Richtungsregister der vier I/O-Ports
  std Y+4,R16
  std Y+7,R16
  std Y+10,R16
  clr R16 ; Loesche die obersten 24 Bits
  std Y+5,R16 ; Zugriff auf die PORT-Portregister
  std Y+8,R16
  std Y+11,R16
  ldi R16,0x01 ; Setze das niedrigste Bit
  std Y+2,R16

Gegenüber der direkten Adressierung oben ist dies jetzt weniger effizient, weil jeder STS-Zugriff zwei Taktzyklen braucht. Zum Init würden wir daher die klassische OUT-Methode oben verwenden.

Aber nun ist es an der Zeit, die Interrupt-Service-Routine für den Compare-Match des Timers zu schreiben, der für die Geschwindigkeit des Lichtumlaufs zuständig ist. Die klassische Methode ginge folgendermaßen:

Tc0CmpIsr: ; 7 Taktzyklen fuer Interrupt und rjmp
  in R16,PORTA ; +1 = 8
  lsl R16 ; +1 = 9
  out PORTA,R16 ; +1 = 10
  in R16,PORTB ; +1 = 11
  rol R16 ; +1 = 12
  out PORTB,R16 ; +1 = 13
  in R16,PORTC ; +1 = 14
  rol R16 ; +1 = 15
  out PORTC,R16 ; +1 = 16
  in R16,PORTD ; +1 = 17
  rol R16 ; +1 = 18
  out PORTD,R16 ; +1 = 19
  brcc Tc0CmpIsrReti ; +1/2 = 20/21
  in R16,PORTA ; +1 = 21
  rol R16 ; +1 = 22
  out PORTA,R16 ; +1 = 23
Tc0CmpIsrReti: ; 21/23 Taktzyklen
  reti ; +4 = 25/27 Zyklen
  ; Gesamt-Zyklen: 31 * 25 + 27 = 802 Zyklen

Die angegebenen Taktzyklen gelten für jeden Interrupt. 31 Mal wird kein Neustart nötig, ein Mal ist Neustart angesagt. Zusammen mit 32-maligem Aufwachen und 64 Zyklen zum erneuten Schlafenlegen ergeben sich etwa 866 Taktzyklen pro Sekunde. Der Sleep-Anteil läge bei 99,13%.

Aber: drei der vier OUT-Instruktionen sind überflüssig, weil sich die aktive LED gar nicht in diesem I/O-Port befindet. Daher wäre es schon ausreichend, nur den jeweils aktiven I/O-Port zu beschreiben.

In den seltenen Fällen, in denen die LED den I/O-Port wechselt (einer von acht Fällen), muss dann sowohl der neue als auch der vorherige I/O-Port beschrieben werden. Das ergibt einen etwas anderen Algrithmus, dieses Mal sind Zeiger nötig und sinnvoll. Zuerst das Initiieren des Zeigers:

  ldi XH,High(PORTA+0x20) ; Zeigeradresse in X, +1 = 1
  ldi XL,Low(PORTA+0x20) ; dto. LSB, +1 = 2
  ldi rShift,0x01 ; Starte Schieberegister, +1 = 3
  st X,rShift ; Schreibe das in den PORTA, +2 = 5

Da diese fünf Instruktionen nur ein einziges Mal beim Anfahren ausgeführt werden, zählen sie nicht beim Ermitteln der Taktzyklen. Die Interrupt-Service-Routine ginge dann so:

OC0AIsr: ; 7 Zykles fuer Int und rjmp
  lsl rShift ; +1 = 8
  st X,rShift ; +2 = 9
  brcc Tc0CmpIsrReti ; +1/2 = 10/11
  ; Next I/O port
  adiw XL,3 ; Zeige auf nächsten Kanal, +2 = 12
  cpi XL,Low(PORTD+3+0x20) ; +1 = 13
  brne Tc0CmpIsr1 ; +1/2 = 14/15
  ldi XH,High(PORTA+0x20) ; +1 = 16
  ldi XL,Low(PORTA+0x20) ; +1 = 17
Tc0CmpIsr1: ; 15/17 Zyklen
  ; Neustart von Beginn an
  ldi rShift,0x01 ; Set bit 0, +1 = 16/18
  st X,rShift ; +2 = 18/20
Tc0CmpIsrReti: ; 11/18/20
  reti ; +4 = 15/22/24
  ; Gesamtzyklen: = 28*15 + 3*22 + 1*24 = 510

Die Anzahl der nötigen Zyklen ist etwa halb so groß wie bei der klassischen Methode ohne Zeiger. Das ist ein klarer Indikator dafür, dass die Zeigermethode doppelt so effizient ist, weil sie keine Zeitverschwendung betreibt und nur Schritte macht, die auch einen Sinn ergeben. Das steigert den Schlafanteil auf 99,43% und reduziert auch etwas den Strom aus der Notstromversorgung des Flugzeugs.

Wenn wir nun die Schaltung etwas ändern und die LED-Kathoden an die I/O-Pins anschließen und dafür die Widerstände an den Anoden an Plus, dann sind im Quellcode ein paar wenige Änderungen nötig. Aus CLR rShift würde SER rShift, aus LDI rShift,0x01 würde LDI rShift,0xFE und aus brcc Tc0CmpIsrReti würde brcs Tc0CmpIsrReti. Diese Änderungen betreffen sowohl die Initiierung als auch die ISR. Etwas trickreicher ist der Ersatz von lsl rShift: das würde in die beiden Instruktionen, sec und rol rShift umgestellt werden. Damit wäre die Software für pull-down-LEDs anstelle von push-up umgestellt.

Die Assembler-Software dafür kann hier heruntergeladen werden. Mit der Software kann die Zyklusdauer zwischen 50 Millisekunden und zwei Sekunden verstellt werden als auch die Polarität der LEDs gewählt werden (die Konstanten im Kopf des Quellcodes machen das).

Was, wenn wir mehr als eine von den 32 LEDs anmachen wollen? Wenn wir davon vier gleichzeitig anmachen wollen und eine davon in jedem 8-Bit-I/O-Port, ist die Software dafür ziemlich einfach und nicht mit Zeigern: einfach rShift in jedem Port mit OUT ausgeben.

Wenn Du zwei LEDs auf einmal anmachen willst und diese jeweils in größter Distanz zueinander, also L1 mit L16, L2 mit L17, etc., dann ist es etwas trickreicher, denn der Neustart mit PORTA erfolgt in beiden I/O-Ports jeweils abwechselnd nach 16 Schiebeoperationen.

Wenn jeweils acht der 32 LEDs an sein sollen, sollte man vielleicht über eine Tabelle im Flash nachdenken, in der kann man flexibel alles verewigen und es wie hier gezeigt auslesen.

Wenn noch mehr LEDs gleichzeitig an sein sollen, bekommst Du es vielleicht mit einer Strombeschränkung zu tun: der ATmega324PA kann nur 200 mA über seinen GND- oder VCC-Pin liefern. Daher muss z. B. bei 16 LEDs gleichzeitig der LED-Strom auf 12,5 mA und bei 32 LEDs gleichzeitig auf 6,25 mA begrenzt werden, damit der Prozessor nicht ausflippt.

Schlussfolgerung:

Wenn Dein Programm die immer gleichen Operationen mit verschiedenen I/O-Ports ausführen muss, lohnt es sich über eine Programmierung mittels Zeigern nachzudenken und unnütze Schreiboperationen durch ein intelligentere Programmierung zu ersetzen.

Zum Seitenanfang

©2021 by http://avr-asm-tutorial.net