Forum: Compiler & IDEs erste Schritte mit Assembler


von jack (Gast)


Lesenswert?

Hallo,

ich habe es endkich geschafft mein erstes Assembler Programm laufen zu 
lassen. War bisher nur in C unterwegs.

Folgendes Programm lässt mir 4 LEDs leuchten.
1
#include <avr/io.h>
2
.global main
3
4
main:
5
        ldi     r16,0xAA
6
        out     0x04,r16
7
        out     0x05,r16
8
9
loop:
10
11
        rjmp    loop

Verwende ich aber anstatt 0x04 und 0x05 die Bezeichner DDRB und PORTB, 
so übersetzt mir der Assemblierer das ganze mit 0x24 und 0x25. Laut DB 
müssen aber bei Benutzung von out 0x20 abgezogen werden.

1) Wieso übersetzt mir der mein Programm nicht richtig? Bin nur durch 
Zufall drauf gestosen nachdem ich mir ein Listing eines C Programms 
angeschaut habe...
Benutze avr-gcc als frontend für den GNU Assembler.
Würd natürlich schon gerne die Bezeichner aus den Headern benutzen 
anstatt mit Adressen zu hantieren...

2) Welchen technischen Hintergrund hat es, dass einige I/O Register zwei 
verschiedene Adressen haben? Das war mir bisher auch neu...

Danke und schönen Abend noch!

von Yalu X. (yalu) (Moderator)


Lesenswert?

jack schrieb:
> 2) Welchen technischen Hintergrund hat es, dass einige I/O Register zwei
> verschiedene Adressen haben? Das war mir bisher auch neu...

Die I/O-Register liegen im Datenadressraum der AVRs hinter den
memory-gemappten General-Purpose-Registern (R0-R31). Die GP-Register
belegen die Adressen 0x00-0x1F, die I/O-Register starten somit an
Adresse 0x20.

Die I/O-Befehle IN und OUT haben ein 6-Bit-Argument und können damit 64
Register adressieren. Da es wenig sinnvoll ist, mit IN und OUT auf die
GP-Register zuzugreifen, bezieht sich ein IN/OUT-Argument von n auf die
Adresse 0x20+n. Man kann also mit IN und OUT die I/O-Register an den
Adressen 0x20-0x5F ansprechen.

> 1) Wieso übersetzt mir der mein Programm nicht richtig?

In den Header-Files sind die I/O-Register als Datenadressen definiert.
Diese können direkt mit den Befehlen LDS und STS verwendet werden:
1
  ; korrekt, aber nicht empfohlen (s. u.)
2
  LDS R0, PINB
3
  STS PORTC, R1

Sollen stattdessen die (auf den meisten AVRs kürzeren und schnelleren)
Befehle IN und OUT verwendet werden, muss aus dem o.g. Grund das
Argument um 0x20 verringert werden:
1
  ; korrekt, aber nicht empfohlen (s. u.)
2
  IN  R0, PINB-0x20
3
  OUT PORTC-0x20, R1

I/O-Register an Adressen >= 0x60 liegen außerhalb der Reichweite von IN
und OUT, so dass hier immer LDS und STS verwendet werden muss.

Die Befehle CBI, SBI, SBIC und SBIS haben ein 5-Bit-Argument und
adressieren damit die I/O-Register an den Datenadressen 0x20-0x3f. Wie
bei IN und OUT muss auch hier bei der Verwendung der symbolischen
Registernamen der Wert 0x20 subtrahiert werden. Auf Register an Adressen
>= 0x40 können diese Befehle nicht angwandt werden.

Für die Subtraktion von 0x20 gibt es das Makro _SFR_IO_ADDR. Das
Gegenstück dazu (ohne Subtraktion) heißt _SFR_MEM_ADDR. Guter Stil ist
es, bei allen Zugriffen auf I/O-Register eines dieser beiden Makros zu
verwenden. Für LDS und STS nimmt man _SFR_MEM_ADDR (Adresse bleibt
unverändert), für IN, OUT, CBI, SBI, SBIC und SBIS nimmt man
_SFR_IO_ADDR (von der Adresse wird 0x020 subtrahiert):
1
  ; korrekt und empfohlen
2
3
  ; Datenadresse des Registers beliebig
4
  LDS  R0, _SFR_MEM_ADDR(ADCL)
5
  STS  _SFR_MEM_ADDR(PORTC), R1
6
7
  ; Datenadresse des Registers < 0x60
8
  IN   R0, _SFR_IO_ADDR(PINB)
9
  OUT  _SFR_IO_ADDR(PINC), R1
10
11
  ; Datenadresse des Registers < 0x40
12
  CBI  _SFR_IO_ADDR(PINC), 3
13
  SBI  _SFR_IO_ADDR(PINC), 3
14
  SBIC _SFR_IO_ADDR(PINC), 3
15
  SBIS _SFR_IO_ADDR(PINC), 3

Mehr darüber zu lesen gibt es hier:

  http://www.nongnu.org/avr-libc/user-manual/group__avr__sfr__notes.html

von Tom (Gast)


Lesenswert?

Yalu X. schrieb:
> In den Header-Files sind die I/O-Register als Datenadressen definiert.

Ich programmiere ausschließlich Assembler (aber in AVR-Studio).
In den Include-Dateien von AVR-Studio ist das genauso hinterlegt wie in 
den Datenblättern der MCs angegeben. Also anscheinend anders, als in 
AVR-GCC.
Beispiel aus dem Datenblatt eines AtMega:

...
PINB 0x03 (0x23)
...
ADMUX 0x7C

Die symbolischen Namen in der INC-Datei beziehen sich immer auf die 
nicht geklammerten Werte. Hier: PINB=0x03

Somit ist hier, bezogen auf das obige Beispiel (gottseidank)
1
IN R0,PINB
2
OUT PORTC, R1
vollkommen korrekt. Das wird einen Assembler-Einsteiger vermutlich 
verwirren...

Gruß
Tom

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

jack schrieb:

> #include <avr/io.h>

Die SFR-Adressen sind wie gesagt durchgehend und konsistent als 
RAM-Adressen verfügbar.  Am einfachsten ist stattdessen:

> #define __SFR_OFFSET 0
> #include <avr/io.h>

Damit funktioniert dann auch dein Code, allerdings muss dann auf 
I/O-Register über ihre I/O-Adresse zugegriffen werden, was nur bei 
direkter Adressierung möglich ist.

Wird zB die Adresse eines SFRs genommen um in einer Routine indirekt 
darauf zuzugreifen, geht das nur per LD/ST, und die brauchen eben eine 
RAM-Adresse, keine I/O-Adresse.

Alternativ kann man auch schreiben

> #include <avr/io.h>
> #undef  __SFR_OFFSET
> #define __SFR_OFFSET 0

wenn man das lieber mag um klarzustellen, daß das Makro geändert wird.

Bracht man spärter wieder den Originalwert, dann hilft cpp fu

> #include <avr/io.h>
> #pragma push_macro ("__SFR_OFFSET")
> #undef  __SFR_OFFSET
> #define __SFR_OFFSET 0
>
> ;; PORTB ist I/O-Adresse
> in r0, PORTB
>
> #pragma pop_macro ("__SFR_OFFSET")
>
> ;; PORTB ist wieder RAM-Adresse
> sts PORTB, r0

Ab avr-gcc 4.7 gibt's auch das built-in Makro __AVR_SFR_OFFSET__ mit dem 
man den Offset zurücksetzen kann:

> #include <avr/io.h>
>
> #undef  __SFR_OFFSET
> #define __SFR_OFFSET 0
> #define RAM(X) X+__AVR_SFR_OFFSET__
> #define IO(X)  X-__AVR_SFR_OFFSET__
>
> in  r0, PORTB
> sts RAM(PORTB), r0
>
> #undef  __SFR_OFFSET
> #define __SFR_OFFSET __AVR_SFR_OFFSET__
>
> in r0, IO(PORTB)
> sts PORTB, r0

von jack (Gast)


Lesenswert?

Ich hab mich am Tutorial orientiert, und da wird das Makro ebenfalls 
nicht benutzt.
Ist das jetzt eine Besonderheit des gnu-as? Oder kommt das AVR Studio 
mit anderen Headern? Wie auch immer, wenn mans weis is ja nicht weiter 
tragisch. Bei Verwendung des Makros wird immerhin sofort ersichtlich, 
was man vorhat :-)

Danke euch!

von Karl H. (kbuchegg)


Lesenswert?

jack schrieb:
> Ich hab mich am Tutorial orientiert, und da wird das Makro ebenfalls
> nicht benutzt.

Das ist aber auch auf den Assembler von Atmel ausgelegt

> Ist das jetzt eine Besonderheit des gnu-as?

Besonderheit ist zuviel gesagt. Bei dem wurde halt der andere Weg 
gewählt.

> Oder kommt das AVR Studio
> mit anderen Headern?

Ja. Deren Assembler hat andere Header

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

jack schrieb:

> Ist das jetzt eine Besonderheit des gnu-as?

Nö, der kennt keine SFRs.  Es ist die AVR-Libc, die Makros für SFRs auf 
diese Art durch ihre Header zur Verfügung stellt.  Der Assembler sieht 
nix mehr davon, weil das ganze Makro-Geraffel vom Präprozessor aufgelöst 
und ersetzt wird.

Daß alle Adressen durchgehend einer Konvention folgen ist durchaus 
sinnvoll, finde ich.

Und mit einem 1-Zeiler ist das lösbar, oder in einem 0-Zeiler wenn man 
per Kommandozeile -D__SFR_OFFSET=0 setzt.

von Yalu X. (yalu) (Moderator)


Lesenswert?

Johann L. schrieb:
> Daß alle Adressen durchgehend einer Konvention folgen ist durchaus
> sinnvoll, finde ich.

Das sehe ich auch so, deswegen habe ich oben auch nur diesen Weg vorge-
schlagen.

Die Entwickler der AVR-Libc sehen das ebenfalls so:
1
  For more backwards compatibility, insert the following at the start of
2
  your old assembler source file:
3
4
  #define __SFR_OFFSET 0
5
6
  This automatically subtracts 0x20 from I/O space addresses, but it's a
7
  hack, so it is recommended to change your source: wrap such addresses
8
  in macros defined here, as shown below. After this is done, the
9
  __SFR_OFFSET definition is no longer necessary and can be removed.

Ein Beispiel, wo man mit der Vermischung von Daten- und I/O-Adressen
Schwierigkeiten bekommen kann:

Du hast eine ADC-Routine für den ATmega1280 programmiert. Darin kommt
die folgende Programmzeile vor:
1
  LDS R0, ADCL

Da ADCL (Datenadresse 0x78) außerhalb des von IN anprechbaren Adressbe-
reichs liegt, funktioniert das perfekt sowohl mit der Atmel- als auch
mit der GNU-Toolchain und unabhängig vom Wert von __SFR_OFFSET.

Jetzt möchtest du die Routine auf den ATmega128 portieren. Hier liegt
das Register ADCL an der Datenadresse 0x24, es kann also wahlweise mit
LDS oder IN gelesen werden. Effizienzmäßig wäre IN besser als LDS. Du
möchtest den Assembler-Code aber 1:1 übernehmen und verzichtest dafür
gerne auf den Effizienzgewinn, denn das Programm war ja bereits auf dem
ATmega1280 schnell genug.

Mit der GNU-Toolchain ohne die Umdefinition von __SFR_OFFSET wird der
Code problemlos auch auf dem ATmega128 laufen, da im Header-File ADCL
passend als 0x24 definiert wird.

Verwendest du hingegen hingegen die Atmel-Toolchain oder hast
__SFR_OFFSET auf 0 gesetzt, ist ADCL als 0x04 definiert. Ein LDS auf
diese Adresse liest aber nicht ADCL, sondern das memorygemappte GP-Re-
gister R4. Die Routine wird also in diesem Fall nicht korrekt arbeiten.

Deswegen halte ich es für besser, nur mit Datenadressen zu arbeiten und
speziell für die Befehle IN, OUT, CBI, SBI, SBIC und SBIS die Adresse
mit _SFR_IO_ADDR in die entsprechende I/O-Adresse zu konvertieren.



Das Problem kann übrigens auch dadurch umgangen werden, dass man in den
Assembler-Code eine Fallunterscheidung einbaut, die von den Alternativen
IN und LDS automatisch die geeignetere auswählt:
1
; fuer GNU-Toolchain mit #define __SFR_OFFSET 0
2
#if _SFR_IO_REG_P(ADCL)
3
  IN  R0, ADCL
4
#else
5
  LDS R0, ADCL
6
#endif
1
; fuer Atmel-Toolchain
2
.if  ADCL<0x40
3
  IN  R0, ADCL
4
.else
5
  LDS R0, ADCL
6
.endif

Das hat zusätzlich den Vorteil, dass immer das schnellere IN anstelle
von LDS verwendet wird, wo dies möglich ist.

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Tom schrieb:
> In den Include-Dateien von AVR-Studio ist das genauso hinterlegt wie in
> den Datenblättern der MCs angegeben.

Was das Problem dann nur umkehrt: wenn du aus irgendeinem Grund statt
mit IN oder OUT auf derartige IO-Register zugreifen willst (Yalu hat
ein mögliches Beispiel geliefert, ein anderes wäre, dass eine gerufene
Funktion bspw. beliebig auf alle PORTx/PINx zugreifen können soll,
unabhängig davon, ob sie im mittels IN und OUT ansprechbaren Bereich
liegen oder nicht), dann musst du dort halt manuell 0x20 addieren.

von Tom (Gast)


Lesenswert?

Jörg Wunsch schrieb:
>> In den Include-Dateien von AVR-Studio ist das genauso hinterlegt wie in
>
>> den Datenblättern der MCs angegeben.
>
>
>
> Was das Problem dann nur umkehrt: wenn du aus irgendeinem Grund statt
>
> mit IN oder OUT auf derartige IO-Register zugreifen willst

...stimme Dir voll zu. Es ist halt eine Glaubensfrage, was besser ist. 
Man muss um den Unterschied nur bescheid wissen. Gerade als Anfänger, 
wenn man Beispielcode aus Tutorials nutzt kann man dann schnell in eine 
Falle tappen...

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.