Forum: Compiler & IDEs [AVR] Low-Byte einer Label-Adresse in Tabelle


Announcement: there is an English version of this forum on EmbDev.net. Posts you create there will be displayed on Mikrocontroller.net and EmbDev.net.
von S. R. (svenska)


Bewertung
0 lesenswert
nicht lesenswert
Hi,

ich portiere gerade ein Assemblermodul von avra nach avr-as.

Darin befindet sich eine Tabelle, die jeweils die Low-Bytes von drei 
Labels enthält. Daraus baue ich zur Laufzeit einen Pointer zusammen, den 
ich dann anspringe. Die Tabelle fülle ich mit einem Makro:
1
; avra
2
.macro op
3
  .db low(@2), low(@1), low(@0), 0
4
.endm
5
.org (PC+255) & 0xFF00
6
op_tab:
7
  op op1_a, op2_a, op3_a
8
  op op1_b, op2_a, op3_b
9
  ; ...

Wenn ich das Makro direkt für avr-as übernehme, dann steht in der 
Tabelle das Low-Byte der Byte-Adresse(!) des Labels und der Sprung geht 
ins Nirvana.

Ich brauche also das Low-Byte der Word-Adresse, und dafür gibt es laut 
Dokumentation pm() oder pm_lo8(). Damit bekomme ich folgendes Makro:
1
; avr-as
2
.macro op op1:req, op2:req, op3:req
3
  .byte lo8(pm(\op3)), lo8(pm(\op2)), lo8(pm(\op1)), 0
4
  ; mit pm_lo8(\opX) statt lo8(pm(\opX)) tritt der gleiche Fehler auf!
5
.endm
6
.balign 512
7
op_tab:
8
  op op1_a, op2_a, op3_a
9
  op op1_b, op2_a, op3_b
10
  ; ...

Dieses Konstrukt (bzw. pm() im Makro) mag der Assembler aber nicht und 
reagiert mit Fehlermeldungen:
1
$ make
2
avr-gcc -mmcu=atmega8515 -DF_CPU=16000000 -std=gnu11 -Wall -Wextra -Os -Iinclude/ -c -o modul.o modul.S
3
modul.S: Assembler messages:
4
modul.S:786: Error: `)' required
5
modul.S:786: Error: junk at end of line, first unrecognized character is `('
6
modul.S:787: Error: `)' required
7
modul.S:787: Error: junk at end of line, first unrecognized character is `('
8
[...]

Wie bekomme ich das Low-Byte der Label-Wortadresse in die Tabelle?

Schöne Grüße

von S. Landolt (Gast)


Bewertung
0 lesenswert
nicht lesenswert
Vielleicht ist das jetzt zu einfach gedacht (andererseits denkt man um 
01:17 manchmal zu kompliziert), ich versuche es mal:
Byte-Adresse/2 ?

von Johann L. (gjlayde) Benutzerseite


Bewertung
1 lesenswert
nicht lesenswert
S. R. schrieb:
> Wenn ich das Makro direkt für avr-as übernehme, dann steht in der
> Tabelle das Low-Byte der Byte-Adresse(!) des Labels und der Sprung geht
> ins Nirvana.
>
> Ich brauche also das Low-Byte der Word-Adresse, und dafür gibt es laut
> Dokumentation pm() oder pm_lo8() [...] Wie bekomme ich das Low-Byte der 
Label-Wortadresse in die Tabelle?

M.W. garnicht. lo8(pm()) und pm_lo8() werden momentan nur für Code 
unterstützt, z.B. als Reloc für LDI.  Für Daten müssten die Tools 
erweitert werden ähnlich wie für https://sourceware.org/PR13503

Relocs wurden nur soweit implementiert, wie es für von avr-gcc erzeugten 
Code erforderlich ist.  Anfragen / Anforderungen wie von dir gab es zwar 
hin und wieder, dies konnte aber immer dadurch gelöst werden, die 
Word-Adresse zu speichern anstatt 2 einzelner Bytes derselben. 
Codebeispiel von avr-gcc:
1
extern int banana (void);
2
int (* const __flash pfunc[])(void) = { banana, main };
3
4
int call_pfunc (int i)
5
{
6
    return pfunc[i] ();
7
}
1
.text
2
call_pfunc:
3
  lsl r24
4
  rol r25
5
  movw r30,r24
6
  subi r30,lo8(-(pfunc))
7
  sbci r31,hi8(-(pfunc))
8
  lpm r0,Z+
9
  lpm r31,Z
10
  mov r30,r0
11
  ijmp
12
.section  .progmem.data.pfunc,"a",@progbits
13
pfunc:
14
  .word  gs(banana)
15
  .word  gs(main)

Je nach Kontext vereifacht sich das um ein paar Instruktionen, z.B. wenn 
der Index bzw. Zeiger pfunc[] in einer Schleife durchlaufen wird.

Wenn die Tabelle nicht zu groß ist, kann auch erwogen werden, diese im 
RAM zu halten weil dies schneller und einfacher zugegriffen werden kann.

von S. R. (svenska)


Bewertung
0 lesenswert
nicht lesenswert
Johann L. schrieb:
> M.W. garnicht. lo8(pm()) und pm_lo8() werden momentan nur für Code
> unterstützt, z.B. als Reloc für LDI.

Das ist ... verdammt doof.
Wie gut stehen die Chancen, dass das irgendwann implementiert wird?
Um avr-gcc/avr-binutils steht es ja allgemein nicht so gut, oder?

Johann L. schrieb:
> Anfragen / Anforderungen wie von dir gab es zwar
> hin und wieder, dies konnte aber immer dadurch gelöst werden, die
> Word-Adresse zu speichern anstatt 2 einzelner Bytes derselben.

Ich speichere ja absichtlich nur das untere Byte des Sprungziels, weil 
die Tabelle so nur 1024 Bytes braucht und ein Eintrag in nur 10 Takten 
gelesen werden kann.

Die Kernschleife (drei Operationen aus Tabelle laden und verketten) 
braucht bisher nur 32 Takte (plus Operationen), da tut jeder zusätzliche 
Befehl weh. Also entweder die Tabelle extern erzeugen, oder jeden 
Pointer vor dem Anspringen rechtsshiften.

Danke jedenfalls für deine Hilfe.

von Johann L. (gjlayde) Benutzerseite


Bewertung
1 lesenswert
nicht lesenswert
S. R. schrieb:
> Johann L. schrieb:
>> Anfragen / Anforderungen wie von dir gab es zwar
>> hin und wieder, dies konnte aber immer dadurch gelöst werden, die
>> Word-Adresse zu speichern anstatt 2 einzelner Bytes derselben.
>
> Ich speichere ja absichtlich nur das untere Byte des Sprungziels, weil
> die Tabelle so nur 1024 Bytes braucht und ein Eintrag in nur 10 Takten
> gelesen werden kann.

D.h. hi8 ist konstant, d.h. die Zielcode passt in 512B?  Wieviel Luft 
ist da?

> Die Kernschleife (drei Operationen aus Tabelle laden und verketten)
> braucht bisher nur 32 Takte (plus Operationen), da tut jeder zusätzliche
> Befehl weh. Also entweder die Tabelle extern erzeugen, oder jeden
> Pointer vor dem Anspringen rechtsshiften.

Hängt davon ab was du machen willst und vieviel Luft bzgl. Speicher da 
ist.  Vielleicht gibt's nen anderen Ansatz, aber ohne das Problem 
genauer zu kennen kann man da nix zu sagen.

> Wie gut stehen die Chancen, dass das irgendwann implementiert wird?

Es ist nicht unüblich, dass jemand was implementiert, weil er es braucht 
(hat Bosch mal so gemacht).

> Um avr-gcc/avr-binutils steht es ja allgemein nicht so gut, oder?

Kommt drauf an was du damit meinst.  Das Target ist einigermaßen 
gepflegt, aber es war noch nie so, dass sich Entwickler um avr-Tools 
gerissen hätten.  Auch mit v8 wird es ein paar kleine Änderungen geben: 
http://gcc.gnu.org/gcc-8/changes.html#avr

Üblicherweise kommen ein Großteil bis 100% der Beiträge von 
Hardwareherstellern der entsprechenden Architektur, und das ist bei AVR 
nur ansatzweise erkennbar.

von S. R. (svenska)


Bewertung
0 lesenswert
nicht lesenswert
Johann L. schrieb:
> Hängt davon ab was du machen willst und vieviel Luft bzgl. Speicher da
> ist.  Vielleicht gibt's nen anderen Ansatz, aber ohne das Problem
> genauer zu kennen kann man da nix zu sagen.

Der Zielcode passt größtenteils in 512 Byte, aber ein paar komplexe 
Operationen liegen außerhalb und werden per rjmp-Trampolin angesprungen. 
Mit der jetzigen Aufteilung sind da noch 6 Byte Luft.

Das eigentliche Modul ist der i8080-Emulator aus dem avrcpm-Projekt, 
aber für einen m8515 mit externem SRAM, und für avr-as, damit ich C für 
den Rest des Systems nutzen kann. Der Emulator selbst lebt komplett in 
den AVR-Registern.

Das Problem habe ich jetzt so gelöst, dass in im Makefile ein 
Perl-Script aufrufe, welches die Ausgabe von "avr-nm i8080.o" parst und 
daraus die eigentliche Tabelle nach i8080_table.S passend generiert. Das 
funktioniert auch, ist nur ziemlich hässlich. Vielleicht siehst du ja 
eine bessere Lösung.

Die eigentliche Kernschleife besteht nur aus drei verketteten 
Operationen und braucht (wenn ich mich nicht verzählt habe) nur 32 Takte 
pro Opcode. Ich sehe keine Möglichkeit, das noch weiter zu optimieren 
oder auch nur annähernd so effizient in C zu lösen.
1
next:
2
  sbrc state, state_hlt
3
  ret ; HLT endet hier
4
5
  ld  opl, X+ ; X zeigt auf nächstes opcode-byte
6
  ldi oph, 4  ; 4 byte/tabelleneintrag
7
8
  ldi zl, lo8(instr_tab)
9
  ldi zh, hi8(instr_tab)
10
  mul opl, oph
11
  add zl, r0
12
  adc zh, r1  ; Z zeigt auf tabelleneintrag
13
14
  lpm rop3, Z+  ; op3
15
  lpm rop2, Z+  ; op2
16
  lpm zl, Z     ; op1
17
  ldi zh, hi8(pm(op1_begin))
18
  ijmp        ; springe zu op1
19
20
op1:
21
  ; whatever
22
  mov zl, rop2
23
  ldi zh, hi8(pm(op2_begin))
24
  ijmp        ; springe zu op2
25
26
op2:
27
  ; whatever
28
  mov zl, rop3
29
  ldi zh, hi8(pm(op3_begin))
30
  ijmp        ; springe zu op3
31
32
op3:
33
  ; whatever
34
  rjmp next   ; und wieder zum anfang

von Johann L. (gjlayde) Benutzerseite


Bewertung
1 lesenswert
nicht lesenswert
S. R. schrieb:
> Das Problem habe ich jetzt so gelöst, dass in im Makefile ein
> Perl-Script aufrufe, welches die Ausgabe von "avr-nm i8080.o" parst [...] 
Vielleicht siehst du ja eine bessere Lösung.

Nö, ich seh da momentan nix außer Autogenerieren der Tabelle mit 
externen Tools.

> Das eigentliche Modul ist der i8080-Emulator aus dem avrcpm-Projekt,

Den Code versteh ich leider nicht.

>   ld  opl, X+ ; X zeigt auf nächstes opcode-byte

D.h. alle Opcodes sind 1 Byte, und wo folgen die Argumente?

>   ldi oph, 4  ; 4 byte/tabelleneintrag
>
>   ldi zl, lo8(instr_tab)
>   ldi zh, hi8(instr_tab)
>   mul opl, oph

Ok, instr_tab ist eine Lookup-Tab in ein Array, das Info zu op1 enthält 
(4 Bytes pro Element).

>   add zl, r0
>   adc zh, r1  ; Z zeigt auf tabelleneintrag
>
>   lpm rop3, Z+  ; op3
>   lpm rop2, Z+  ; op2

Was sind op2/3 ?

>   lpm zl, Z     ; op1
>   ldi zh, hi8(pm(op1_begin))
>   ijmp        ; springe zu op1

Ok, indirekter Sprung zu Code, der *X emuliert.

> op1:
>   ; whatever
>   mov zl, rop2
>   ldi zh, hi8(pm(op2_begin))
>   ijmp        ; springe zu op2

Warum indirekt nud nicht einfach "JMP op2" ?

> op2:
>   ; whatever
>   mov zl, rop3
>   ldi zh, hi8(pm(op3_begin))
>   ijmp        ; springe zu op3

Dito. Mir ist nicht klar welchem Zweck diese "Stückelung" des Codes 
dient.

> Der Zielcode passt größtenteils in 512 Byte, aber ein paar komplexe
> Operationen liegen außerhalb und werden per rjmp-Trampolin angesprungen.

Meine Idee war folgende: Wenn der Code für alle Instruktionen aligned 
ist, dann kann man sich 1 Lookup sparen: Wenn du etwa 32KiB an Code zur 
Verfügung hast, dann sind das 32KiB/256 = 128B = 64 AVR-Instruktionen 
pro 8080 Code.  Blöd wird dann nur, dass alle Sprunglabels an exaktem 
Offset stehen müssen und keine "Überläufe" ins nächste Label passieren 
dürfen.

Je nach ISA-Struktur eignet sich vielleicht auch eine andere Größe.

Und es stellt sich auch die Frage, wie viel Zeit der Emulator überhaupt 
im Kopf der Hauptschleife rumhängt. Wenn das nur 2% sind, dann bringt es 
nicht viel bzw. kostet nicht viel, ein paar Ticks weniger oder mehr zu 
haben.

von S. R. (svenska)


Bewertung
0 lesenswert
nicht lesenswert
> Den Code versteh ich leider nicht.

Jeder i8080-Opcode besteht aus drei Operationen (FETCH = op1, EXEC = 
op2, STORE = op3), die miteinander verkettet werden. FETCH lädt den 
Operanden in ein temporäres Registerpaar (opl:oph), EXEC bearbeitet den 
Operanden, und STORE schreibt ihn wieder zurück. Für komplexere Opcodes 
gibt es Abweichungen.

Ein "INC B" besteht aus { fetch_b, exec_in, store_b }.
Ein "MOV B, A" besteht aus { fetch_a, exec_nop, store_b }.
Ein "PUSH HL" besteht aus { fetch_hl, exec_push, store_nop }.
Ein "JMP xxxx" besteht aus { fetch_dir16, exec_nop, store_pc }.

Der Code springt die Ziele in der Tabelle nacheinander an.

>>   ld  opl, X+ ; X zeigt auf nächstes opcode-byte
> D.h. alle Opcodes sind 1 Byte, und wo folgen die Argumente?

Die werden, wenn nötig, in der FETCH-Phase geladen.

>>   lpm rop3, Z+  ; op3
>>   lpm rop2, Z+  ; op2
>
> Was sind op2/3 ?

Das sind die Low-Bytes der folgenden Sprungziele.

>> op2:
>>   ; whatever
>>   mov zl, rop3
>>   ldi zh, hi8(pm(op3_begin))
>>   ijmp        ; springe zu op3
>
> Dito. Mir ist nicht klar welchem Zweck diese "Stückelung" des Codes
> dient.

Ich brauche nicht für jeden der 256 Opcodes eine eigene Funktion 
schreiben, sondern komme mit (20+41+21) relativ kurzen Stücken aus.

Der gesamte Emulator passt dadurch in 3 KB (von 8 KB insgesamt) Flash.

>> Der Zielcode passt größtenteils in 512 Byte, aber ein paar komplexe
>> Operationen liegen außerhalb und werden per rjmp-Trampolin angesprungen.
>
> Meine Idee war folgende: Wenn der Code für alle Instruktionen aligned
> ist, dann kann man sich 1 Lookup sparen: Wenn du etwa 32KiB an Code zur
> Verfügung hast, dann sind das 32KiB/256 = 128B = 64 AVR-Instruktionen
> pro 8080 Code.

Das funktioniert, wenn man genug Codespeicher zur Verfügung hat und es 
nicht zu viele Instruktionen gibt (beim Z80 wird das schon schwieriger).

> Und es stellt sich auch die Frage, wie viel Zeit der Emulator überhaupt
> im Kopf der Hauptschleife rumhängt.

Die Hauptschleife liegt bei ca. 32 Takten, dazu kommen FETCH (0..6 
Takte), STORE (0..9 Takte) und EXEC (0..15 Takte, mit einer Ausnahme). 
Also im Normalfall deutlich über 50% der Zeit.

Ausgenommen sind natürlich die I/O-Befehle, die von C-Code verarbeitet 
werden sollen. Die dürfen aber extrem langsam sein, das war die reale 
Hardware damals ja auch. ;-)

von Johann L. (gjlayde) Benutzerseite


Bewertung
1 lesenswert
nicht lesenswert
S. R. schrieb:
> Der gesamte Emulator passt dadurch in 3 KB (von 8 KB insgesamt) Flash. [...] Die 
Hauptschleife liegt bei ca. 32 Takten, dazu kommen FETCH (0..6
> Takte), STORE (0..9 Takte) und EXEC (0..15 Takte, mit einer Ausnahme).
> Also im Normalfall deutlich über 50% der Zeit.

Das lässt natürlich nicht viel Spielraum.

Die Alternativen sind hässliches Script oder Binutils anfassen. 
Letzteres hab ich erst 2x gemacht und war nie kompliziert.  Deine 
Änderung wäre fast genau wie für den o.g. PR13503, nur dass eben nicht 
ein Reloc hi8 für Bits 8..15 zu machen wäre sondern ein pm_hi8 für Bits 
9..16 etc.

: Bearbeitet durch User
von S. R. (svenska)


Bewertung
0 lesenswert
nicht lesenswert
Johann L. schrieb:
> Die Alternativen sind hässliches Script oder Binutils anfassen.

Ich hab mich erstmal für hässliches Script entschieden. Und im Simulator 
funktioniert es auch, auf realer Hardware nicht. Debugging macht in C 
definitiv mehr Spaß.

von Johann L. (gjlayde) Benutzerseite


Bewertung
1 lesenswert
nicht lesenswert
S. R. schrieb:
> Und im Simulator funktioniert es auch, auf realer Hardware nicht.

Vielleicht Problem mit dem C-ABI?  Z.B: clobberst du oben R1, beim 
Aufruf einer C-Funktion muss in R1 aber 0 stehen. Und:

> Der Emulator selbst lebt komplett in den AVR-Registern.

Auch im C-Teil qua globaler Register? Da gibt's einige Fallstricke.

von S. R. (svenska)


Bewertung
0 lesenswert
nicht lesenswert
Johann L. schrieb:
>> Der Emulator selbst lebt komplett in den AVR-Registern.
> Auch im C-Teil qua globaler Register? Da gibt's einige Fallstricke.

Nein, vor dem Aufruf einer C-Funktion sichere ich den Emulatorzustand 
(clr r1, push r18-r27, push r30-r31) und hole die Sicherung danach auch 
wieder vom Stack runter. ISRs sollten eigentlich unproblematisch sein.

Eigentlich brauche ich den C-Teil nur für ISRs, die IN/OUT-Befehle und 
printf_P() fürs Instruction-Tracing. Für letzteres lege ich alle 
Argumente auf den Stack (je 2 Byte) und hole den Rückgabewert auch dort 
ab (2 Byte). Das scheint auch zu funktionieren...

von S. R. (svenska)


Bewertung
0 lesenswert
nicht lesenswert
S. R. schrieb:
> Für letzteres lege ich alle Argumente auf den Stack (je 2 Byte)
> und hole den Rückgabewert auch dort ab (2 Byte).

Falls jemand in die gleiche Falle tappt... eine variadische Funktion 
konsumiert ihre Argumente nicht. Weder die avr-libc noch AppNote 
AT1886 weisen darauf hin.

Von Assembler aus ruft man printf_P() mit folgender Schrittfolge auf:
(1) r1 leeren (clr r1).
(2) Alle Parameter (von rechts nach links) auf den Stack pushen,
    8 Bit-Werte werden auf 16 Bit erweitert.
(3) Die Adresse des Formatstrings (erst high, dann low) pushen.
(4) Funktion aufrufen (rcall printf_P)
(5) Die Adresse des Formatstrings
    und alle Parameter wieder vom Stack nehmen.

Nach dem Aufruf sind r0, r18-r27 und r30-r31 zerstört.

Der Emulator läuft jetzt. ;-)

von Johann L. (gjlayde) Benutzerseite


Bewertung
0 lesenswert
nicht lesenswert
S. R. schrieb:
> Nach dem Aufruf sind r0, r18-r27 und r30-r31 zerstört.

Dito für SREG.T, das zu behandeln ist wie tmp_reg (R0).

> (5) Die Adresse des Formatstrings
>     und alle Parameter wieder vom Stack nehmen.

Ja, für avr-gcc. Wie das gehandhabt wird bestimmt die C-Implementation. 
Falls hier Unklarheit herrscht, kann man auch eine entsprechende 
Funktion aufrufen und die Compilerausgabe begutachten:
1
extern void variadic_func (const char*, ...);
2
3
void use_func (int x, unsigned char c)
4
{
5
    variadic_func ("Hallo", x, c);
6
}
1
;; avr-gcc -c f.c -Os -mmcu=atmega168 -fdata-sections -save-temps
2
.section  .rodata.str1.1,"aMS",@progbits,1
3
.LC0:
4
  .string  "Hallo"
5
.text
6
use_func:
7
  push __zero_reg__
8
  push r22
9
  push r25
10
  push r24
11
  ldi r24,lo8(.LC0)
12
  ldi r25,hi8(.LC0)
13
  push r25
14
  push r24
15
  call variadic_func
16
  pop __tmp_reg__
17
  pop __tmp_reg__
18
  pop __tmp_reg__
19
  pop __tmp_reg__
20
  pop __tmp_reg__
21
  pop __tmp_reg__
22
  ret
23
.global __do_copy_data

: Bearbeitet durch User

Antwort schreiben

Die Angabe einer E-Mail-Adresse ist freiwillig. Wenn Sie automatisch per E-Mail über Antworten auf Ihren Beitrag informiert werden möchten, melden Sie sich bitte an.

Wichtige Regeln - erst lesen, dann posten!

  • Groß- und Kleinschreibung verwenden
  • Längeren Sourcecode nicht im Text einfügen, sondern als Dateianhang

Formatierung (mehr Informationen...)

  • [c]C-Code[/c]
  • [code]Code in anderen Sprachen, ASCII-Zeichnungen[/code]
  • [math]Formel in LaTeX-Syntax[/math]
  • [[Titel]] - Link zu Artikel
  • Verweis auf anderen Beitrag einfügen: Rechtsklick auf Beitragstitel,
    "Adresse kopieren", und in den Text einfügen




Bild automatisch verkleinern, falls nötig
Bitte das JPG-Format nur für Fotos und Scans verwenden!
Zeichnungen und Screenshots im PNG- oder
GIF-Format hochladen. Siehe Bildformate.
Hinweis: der ursprüngliche Beitrag ist mehr als 6 Monate alt.
Bitte hier nur auf die ursprüngliche Frage antworten,
für neue Fragen einen neuen Beitrag erstellen.

Mit dem Abschicken bestätigst du, die Nutzungsbedingungen anzuerkennen.