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
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.
>> 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:
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.
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
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.
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?
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...
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?
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
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.
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.
Christoph M. schrieb:> Gibt es irgendwo eine Befehlsübersicht? Am besten wäre, wenn man auch> die Kodierung sehen könnte.
Keine vollständige Befehlsübersicht, aber in dem rvalp.pdf, auf den
nachfolgender Link zeigt, hat man auf den letzten Seiten auch ein paar
Tabellen zur Kodierung der Befehle:
https://github.com/johnwinans/rvalp
Die Compiler Toolchain zu installieren war für mich relativ umständlich.
Einfacher ist es, die Arduino IDE mit der passenden Toolchain zu
installieren und dann einfach den vorhandenen Assembler mit einem Script
zu nutzen.
Den Pfad zur Toolchain kann man einfach aus der Arduino-IDE heraus
kopieren, wenn man einmal ein leeres Programm compiliert hat.
Das folgende Script assembliert und flashed den Mikrocontroller in einem
Rutsch:
1
#!/bin/bash
2
3
# CH32V003 on Ubuntu
4
# To assemble programms for the CH32V003 microcontroller, you can use
5
# the assembler toolchain provide by this Arduino Framework:
Paar Anmerkungen:
OpenOCD versteht ELF. Der Zwischenschritt über iHex / Binary ist also
überflüssig: https://openocd.org/doc/html/Flash-Commands.html
Teilweise ist Binary auch kontraproduktiv, etwa wenn etwas Code ab 0x0
liegt und etwas Daten ab 0x1000000. Das ELF file ist dann klein um
überschaubar, aber das Binary ist riesig weil es keine Section o.ä.
darstellen kann.
Assemble und Link lassen sich mit einem Kommando ausführen, indem man
riscv-none-embed-gcc als Treiber verwendet:
In reinen Asm Programmen will man i.d.R keinen C Startup Code, daher das
-nostartfiles (Mit den RISCV Tools kenne ich mich nicht aus, und ich
weiß nicht ob da überhaupt Startup Code anbei ist).
Um Argumente an den Linker zu übergeben wird normalerweise -Wl
verwendet, also zum Beispiel -Wl,-e,Coldstart. Hier ist das nicht nötig
weil gcc weiß, dass -e für den Linker ist. Dito für -Ttext. Ein
Map-File wird man etwa mit -Wl,-Map,blink.map vom Linker erstellen
lassen.
Ein Vorteil bei der Verwendung von gcc ist, dass man neben .macro von
GAS noch einen zweiten Makro-Prozessor nutzen kann, nämlich den C
Präprozessor (#include, #define, #if, #ifdef, ...). Dazu muss man
allerdings .S oder .sx als Suffix verwenden, oder man kann auch mit -x
die "Sprache" explizit angeben:
Liegt die Applikation in mehreren Modulen vor, dann kann man einfach
alle Module in einem Rutsch übersetzen, anstatt für jedes Modul händisch
einen GAS-Aufruf zu bemühen:
Um die Ausgaben / Ergebnisse des C Präprozessors zu behalten dient
-save-temps. Die Zwischenergebnisse werden dann als blink.s, sound.s
und grafik.s gespeichert.
Wenn man Assembler mit C kombinieren will, dann gibt man einfach die
C-Quellen beim Aufruf mit an. Zu beachten ist, dass -x die Sprache für
alle folgenden Inputs setzt. Mit -x none erreicht man, dass die
Sprache wieder vom Suffix abgeleitet wird:
Johann L. (gjlayde) Benutzerseite
> Paar Anmerkungen
Danke für die vielen wertvollen Hinweise.
Für mich ist die Erstellung der Makefiles immer etwas mühsam, deshalb
mach ich der Einfachheit halber Bash-Scripts.
Aber: Dank Perplexity sind Makefiles nun auch kein unlösbares Problem
mehr.
Im Anhang das Makefile. Ich musste es ein wenig "tweaken" weil unter
anderem "make clean" frecherweise gleich mein Sourcefile gelöscht hat.
Jetzt gehen die Kommandos ziemlich gut und der Pfad zur Toolchain im
Arduino-Ordner ist schön separiert, sodass es jeder selbst leicht an
seinen Pfad anpassen könnte.
Kommandos u.a. sind:
make
make clean
make flash
Weiß eigentlich jemand, mit welcher Taktfrequenz die MCU im obigen
Blinkbeispiel läuft? Ich habe das Assemblerfile auf einen Quarz-Losen
CH32V003 geflasht und es blinkt recht schnell.
Eigentlich wäre es besser, als Referenz den Systemtimer statt der
Delay-Loop zu verwenden.
Ich habe mal versucht, ein 1Hz Blinken mit dem obigen Assemblerprogramm
zu erzeugen
Beitrag "Re: RISCV (CH32V003): GNU Assembler übersetzt "CALL" nicht korrekt (oder bin ich nur zu doof?)"
Dazu muss man x6 mit 1e6 laden:
Christoph M. schrieb:> Weiß eigentlich jemand, mit welcher Taktfrequenz die MCU im obigen> Blinkbeispiel läuft? Ich habe das Assemblerfile auf einen Quarz-Losen> CH32V003 geflasht und es blinkt recht schnell.> Eigentlich wäre es besser, als Referenz den Systemtimer statt der> Delay-Loop zu verwenden.
Die Taktfrequenz sollte gemäß Datenblatt bei ca. 24 MHz liegen. Sie
kommt vom "24 MHz HSI RC" und wird - ohne weiter geteilt zu werden - zu
FCLK geleitet.
Ja, es wäre wesentlich besser einen Timer für die Erzeugung der
Blink-Impulse zu verwenden. Ich habe dazu mal ein wenig mit Interrupts
gespielt und das Umschalten des LED-Ausgangs in eine Interrupt-Routine
verlegt. Das Hauptprogramm macht jetzt nur noch ein paar
Initialisierungen und verweilt dann in einer Endlosschleife.
Danke für das Code-Beispiel :-)
Thomas T. (knibbel)
29.06.2025 15:09
>Die Taktfrequenz sollte gemäß Datenblatt bei ca. 24 MHz liegen. Sie>kommt vom "24 MHz HSI RC" und wird - ohne weiter geteilt zu werden - zu>FCLK geleitet.
Die Delay-Loop mit dem Zählerwert 1 Million ergibt die Verzögerung von
500ms, die für das 1Hz Blinken gebraucht werden. Bei 24MHz ( 24
Millionen Zyklen/Sekunde) wären das 12 Taktzyklen für die beiden Befehle
addi und bne.
Dann wäre diese RISC-V Implementierung definitv schnarchlangsam.
Ich versuche mehr über den Prozessor herauszufinden.
Die Firma QingKeV scheint ein wenig von den normalen RISC-V Konventionen
abzuweichen.
Laut Prozessor-Manal sollte das Ding einen Hardwaremultiplizierer haben:
1
QingKeV2_Processor_Manual
2
3
RISC-V Kern:
4
RV32ECZmmul
5
6
RV32: 32-bit architecture
7
E: 16 registers
8
C: 16-bit compressed instructions
9
XW: 16-bit compression instruction for self-extending byte and half-word operations
Was mich vor allem interessiert, ist der Systick-Timer. Den verwendet
man normalerweise in Echtzeitsystemen für alles Mögliche als Zeitbasis.
Man kann ihn z.B. auch sehr gut für die 'delay' Funktion verwenden.
Hier ist die Initialisierung im Arduino-Core, was für die
Assemblerimplementierung hilfreich sein könnte:
https://github.com/openwch/arduino_core_ch32/blob/main/cores/arduino/ch32/hw_config.c
Um im Reference-Manual fündig zu werden, muss man nach R32_STK suchen.
Christoph M. schrieb:> Die Firma QingKeV scheint ein wenig von den normalen RISC-V Konventionen> abzuweichen.
'QingKeV' ist keine Firma, sondern eine Reihe von Mikrocontroller-Serien
bzw. -Kernen. Das Unternehmen nennt sich 'Nanjing Qinheng
Microelectronics'.
Manuel H. (Firma: Universität Tartu) (xenos1984)
08.06.2025 18:06
>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
Wie kriege ich Platform-IO dazu, nur mein Assembler-File zu verwenden?
Das Arduino-Framework kann ich selektieren und mit dem ini-File
1
[env:ch32v003f4p6_evt_r0]
2
platform = ch32v
3
board = ch32v003f4p6_evt_r0
4
framework = arduino
5
upload_protocol = wch-link
und einen rohen, auf einen TSSOP-Adapter aufgelöteten CH32V003F4P6 und
sonst keine Bauteile außer der LED mit Vorwiderstand blinkt es auch
schön:
1
#include <Arduino.h>
2
3
#define LED PD4 // TSSOP 20 Board
4
5
void setup() {
6
pinMode(LED, OUTPUT);
7
}
8
9
void loop() {
10
digitalWrite(LED, HIGH);
11
delay(1000);
12
digitalWrite(LED, LOW);
13
delay(1000);
14
}
Ich will aber nicht das dicke Framework, sonder das rohe blink.s