Forum: Compiler & IDEs RISCV (CH32V003): GNU Assembler übersetzt "CALL" nicht korrekt (oder bin ich nur zu doof?)


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 Thomas T. (knibbel)


Lesenswert?

Hallo zusammen,

ich versuche mir seit ein paar Tagen ein kleines 
HelloWorld-Blink-Programm direkt in RISCV-Assembler für den CH32V003 zu 
bauen und bekam es erst zum Laufen, als ich Verzögerungsschleifen, die 
mit dem Pseudo-Befehl "CALL" aufgerufen wurden, direkt ins Hauptprogramm 
eingebaut hatte und somit Unterprogramme vermieden hatte.

Ich habe inzwischen das Problem auf den folgenden "Zwei 
Zeilen-Quelltext" reduzieren können, welcher bei mir falsch übersetzt 
wird:
1
Start:        CALL Subroutine
2
Subroutine:   RET

Vorweg: Zum Übersetzen benutze ich das Paket 
"xpack-riscv-none-elf-gcc-14.2.0-3", die Version des im Paket 
enthaltenen Assemblers ist "GNU assembler (xPack GNU RISC-V Embedded GCC 
x86_64) 2.43.1".

Wenn man die beiden Zeilen mittels "riscv-none-elf-as.exe myfile.s" 
übersetzt und sich danach das Ergebnis mittels 
"riscv-none-elf-objdump.exe -d a.out" anschaut, dann sieht man 
folgendes:
1
a.out:     file format elf32-littleriscv
2
3
Disassembly of section .text:
4
5
00000000 <Start>:
6
   0:   00000097                auipc   ra,0x0
7
   4:   000080e7                jalr    ra # 0 <Start>
8
9
00000008 <Subroutine>:
10
   8:   00008067                ret

Der CALL-Befehl wird hier in zwei Befehle aufgeteilt (das ist auch noch 
in Ordnung), es fehlt jedoch im zweiten Befehl (jalr) der notwendige 
Offset, damit auch zum Unterprogramm gesprungen wird.

Richtigerweise sollte meiner Ansicht nach diese Zeile so aussehen:
1
   4:   008080e7                jalr    8(ra) # 8 <Subroutine>

Wenn ich in meinem HelloWorld-Blink-Programm die übersetzten 
CALL-Befehle derart patche (also die jalr-Befehle mit dem notwendigen 
Offset versehe), dass sie auf das Unterprogramm mit der 
Verzögerungsschleife zeigen, dann funktioniert auch alles, so wie es 
soll...

Es ist also offensichtlich der CALL-Befehl, der nicht korrekt übersetzt 
wird. Aber kann das sein? Ich habe auch ältere Versionen des Assemblers 
getestet, es ist überall dasselbe Verhalten.

Fehlt vielleicht hier nur eine Option, die ich dem Assembler mitgeben 
muss? Oder habe ich hier wirklich einen Fehler entdeckt? Das kann ich 
mir aber bei so einem grundlegenden Befehl nicht vorstellen...

Habt ihr irgendwelche Ideen?

Gruß
Thomas

: Bearbeitet durch User
von Manuel H. (Firma: Universität Tartu) (xenos1984)


Lesenswert?

Probier mal den Befehl:
1
riscv-none-elf-objdump.exe -d -r a.out

Dann siehst du, dass die erzeugte Objektdatei noch eine Relokation 
enthält, die auf das Unterprogramm verweist. Die richtige Adresse setzt 
dann der Linker ein.

von Cartman E. (cartmaneric)


Lesenswert?

Ausserdem immer an die Effekte der Pipeline denken!
Wer schon mit MIPS zu tun hatte, ist klar im Vorteil.

von Thomas T. (knibbel)


Lesenswert?

Manuel H. schrieb:
> Probier mal den Befehl:
>
>
1
riscv-none-elf-objdump.exe -d -r a.out
>
> Dann siehst du, dass die erzeugte Objektdatei noch eine Relokation
> enthält, die auf das Unterprogramm verweist. Die richtige Adresse setzt
> dann der Linker ein.

DANKE! Das ist ein wertvoller Hinweis! Ich hatte gedacht, ich komme ohne 
einen Linker klar und der Assembler macht schon alles für mich. Da lag 
also mein Denkfehler.

Vielleicht sind folgende Befehle ja eine Hilfe für Gleichgesinnte, die 
ohne eine große IDE auf der Kommandozeile RISCV-Programme für den 
CH32V003 assemblieren wollen:

Assemblieren mittels:
1
riscv-none-elf-as.exe -agl blinken.s

Dann Linken mittels:
1
riscv-none-elf-ld.exe -o a.o -Ttext 0 -e Coldstart a.out

Danach die Binärdatei bauen mittels:
1
riscv-none-elf-objcopy.exe -O binary a.o a.bin

Und Anschauen, was der Assembler gebaut hat, geht mittels:
1
riscv-none-elf-objdump.exe -d -r a.o

Gegebenenfalls müssen natürlich die Dateinamen angepasst werden.

Gruß
Thomas

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

as erzeugt aus der Assembly-Quelle eine Object Datei, was aber kein 
ausführbarer Code ist. Dazu muss mindestens Gelinkt werden, und evtl. 
braucht's noch zusätzlichen Code (Startup Code, CRT, Device 
Initialisierung, Vektor Tabellen, Linker Script, etc).

Am einfachsten bekommt man das, wenn man gcc als Treiber verwendet 
(für.S oder.sx oder mit -x assembler-with-cpp).

Weiterer Vorteil ist, dass man dann auch den C-Präprozessor im asm 
verwenden kann.

von Manuel H. (Firma: Universität Tartu) (xenos1984)


Lesenswert?

Wenn man ohne IDE arbeiten und den Kompiliervorgang etwas vereinfachen 
möchte, kann man auch PlatformIO Core benutzen. Da kann man über die 
Kommandozeile das Projekt erstellen, und statt einzeln die einzelnen 
Programme aufzurufen, tut es ein "pio run".

https://github.com/Community-PIO-CH32V/platform-ch32v

von Harald K. (kirnbichler)


Lesenswert?

Manuel H. schrieb:
> Da kann man über die
> Kommandozeile das Projekt erstellen, und statt einzeln die einzelnen
> Programme aufzurufen, tut es ein "pio run".

Naja, ein simples Makefile würde genügen, da muss man nicht mit so einer 
fetten Keule herumwedeln.

von Christoph M. (mchris)


Lesenswert?

Thomas T. (knibbel)
08.06.2025 13:01

>ich versuche mir seit ein paar Tagen ein kleines
>HelloWorld-Blink-Programm direkt in RISCV-Assembler für den CH32V003 zu
>bauen und bekam es erst zum Laufen, als ich Verzögerungsschleifen, die
>mit dem Pseudo-Befehl "CALL" aufgerufen wurden, direkt ins Hauptprogramm
>eingebaut hatte und somit Unterprogramme vermieden hatte.

Eine sehr interessante Idee, sich bei diesem Prozessor auf die 
Assemblerebene zu begeben.
Kannst du ein einfaches Blink-Programm für den Einstieg posten?

von Thomas T. (knibbel)


Lesenswert?

Christoph M. schrieb:
>
> Eine sehr interessante Idee, sich bei diesem Prozessor auf die
> Assemblerebene zu begeben.
> Kannst du ein einfaches Blink-Programm für den Einstieg posten?

Da der Thread noch nicht zerredet wurde, kann ich das gerne machen. Hier 
kommt also ein sehr einfaches Programm, was bei der Adresse 0 startet 
und auch keinerlei Rücksicht auf irgendwelche Vektortabellen für 
Interrupts nimmt. Normalerweise erfolgt wohl zuallererst bei Adresse 0 
ein Jump-Befehl zum Einsprungspunkt und dann folgen erst mal eine Reihe 
von Vektoren, falls man mit Interrupts arbeitet. Und den Stack-Pointer 
habe ich auch nicht gesetzt, das Speichern der Rücksprungadressen beim 
CALL-Befehl funktioniert hier etwas anders und auch nur für eine Ebene. 
Bei mehreren Unterprogramm-Ebenen muss man sich dann was eigenes 
basteln...
1
# Assemblieren:     riscv-none-elf-as -agl blink.s
2
# Linken:           riscv-none-elf-ld -o blink.o -Ttext 0 -e Coldstart a.out
3
# Anschauen:        riscv-none-elf-objdump -d -r blink.o
4
# Binary bauen:     riscv-none-elf-objcopy -O binary blink.o blink.bin
5
6
                        .file    "blinken.s"
7
                        .section .text
8
                        .balign  4
9
10
                        .global  Coldstart
11
12
R32_RCC_APB2PCENR       =       0x40021018
13
R32_GPIOD_CFGLR         =       0x40011400
14
R32_GPIOD_BSHR          =       0x40011410
15
16
                        .org    0x00000000
17
18
Coldstart:              nop
19
20
Port_Setup_Clock:       li      x10,R32_RCC_APB2PCENR
21
                        lw      x11,0(x10)
22
                        li      x7,(1<<5)
23
                        or      x11,x11,x7
24
                        sw      x11,0(x10)
25
26
Port_Setup_Direction:   li      x10,R32_GPIOD_CFGLR
27
                        lw      x11,0(x10)
28
                        li      x7,0xfff0ffff
29
                        and     x11,x11,x7
30
                        li      x7,0x00030000
31
                        or      x11,x11,x7
32
                        sw      x11,0(x10)
33
34
                        li      x10,R32_GPIOD_BSHR
35
36
Led_PD4_ON:             li      x11,0b00000000000000000000000000010000
37
                        sw      x11,0(x10)
38
39
                        call    Delay
40
41
Led_PD4_OFF:            li      x11,0b00000000000100000000000000000000
42
                        sw      x11,0(x10)
43
44
                        call    Delay
45
46
                        j       Led_PD4_ON
47
48
49
Delay:                  li      x6,200000
50
DelayLoop:              addi    x6,x6,-1
51
                        bne     x6,zero,DelayLoop
52
                        ret

Viel Spaß damit.

Gruß
Thomas

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Auf .org würde ich komplett verzichten, weil es eben nicht die 
absolute Adresse des nächsten Kommandos setzt.

von Christoph M. (mchris)


Lesenswert?

Thomas T. (knibbel)
09.06.2025 14:51
>Da der Thread noch nicht zerredet wurde, kann ich das gerne machen.
Wieso ist doch super. Auf die Art lernt man einen Prozessor in der Tiefe 
kennen (inclusive der Toolchains).

Johann L. (gjlayde)
> weil es eben nicht die
> absolute Adresse des nächsten Kommandos setzt.
Kannst du das mal genauer ausführen?

von Johann L. (gjlayde) Benutzerseite


Lesenswert?


von Thomas T. (knibbel)


Lesenswert?

Johann L. schrieb:
> Auf .org würde ich komplett verzichten, weil es eben nicht die
> absolute Adresse des nächsten Kommandos setzt.

Ich denke, man sollte hier einen Mittelweg finden...

.org setzt nicht immer die absolute Adresse des nächsten Befehls. Wenn 
man allerdings die Einschränkungen berücksichtigt, dann sollte man .org 
nicht komplett ablehnen.

Ein Grund wäre beispielsweise folgender:

Sobald ich für Interrupts eine Vektortabelle am Anfang des Flash 
hinterlege, bin ich gezwungen diese zu umgehen, da der Programmstart 
immer bei 0 beginnt und dort dann ein Jump-Befehl stehen muss. Auch wenn 
man gewöhnlich dem Linker den Programmstart vorgeben kann (oder er 
selbst _start nimmt), funktioniert dies beim Mikrocontroller nicht mehr.

Ich will die Led an PD4 als nächsten Schritt mittels eines 
Timer-Interrupts blinken lassen und bereite den Quelltext so vor, damit 
die Adresse der Interrupt-Routine auch korrekt hinterlegt ist:
1
                        .org 0x00000000
2
Coldstart:              j       Warmstart
3
4
                        .org 0x00000090
5
TIM1TRG_Vector:         .dc.l   Irq_Routine
6
7
                        .org 0x00000100
8
Warmstart:              nop

Ich werde am nächsten Wochenende mal versuchen, die Interrupt-Routine 
auszuformulieren und die Werte für den Timer zu setzen.

Was alternativ allerdings funktionieren könnte, wäre folgendes: Ich baue 
mir eine separate Vektortabelle, in der ich zu Anfang einen Jump-Befehl 
auf das Ende der Vektortabelle setze. Genauer gesagt, direkt hinter dem 
letzten Eintrag der Vektortabelle. Wenn ich diese Datei dann assembliere 
und vor meine eigentliche Objektdatei linke, dann könnte es auch ohne 
.org funktionieren...

Gruß
Thomas

von Cartman E. (cartmaneric)


Lesenswert?

Warum lässt du den Linker nicht einfach seine Arbeit machen?

Die Grösse der Vektortabelle ist bekannt, und gehört neben ihrer
Adresse einfach in eine Section der Linkdefinitionen.
Dazu dann eine Assemblerquelle, die den Inhalt aufdröselt und
genau dieser Section zugeordnet wird.
Die braucht dann eventuell auch eher Aligns statt Org.

Genauso wie der Main- und der Interruptcode in eine Section
gehören. Die richtigen Adressen setzt dann schon der Linker ein.

Die Sections wiederum liegen in Regions der Linkertabelle.

Deine Beispiele werden nicht immer so einfach bleuben, und
willst du ernsthaft in grösseren Projekten immer mit Absolutadressen
hantieren?

Also mach es gleich richtig.

von Christoph M. (mchris)


Lesenswert?

Cartman E. (cartmaneric)
11.06.2025 01:10

>Deine Beispiele werden nicht immer so einfach bleuben,
Ich finde es besser, erst einfach anzufangen und sich nicht auf die 
Komplexität des Link-Prozesses zu verlassen.

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.