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:
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
externintbanana(void);
2
int(*const__flashpfunc[])(void)={banana,main};
3
4
intcall_pfunc(inti)
5
{
6
returnpfunc[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.
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.
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.
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.
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.
> 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. ;-)
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.
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ß.
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.
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...
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. ;-)
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: