阮祈翰
Run Mado on rv32emu by using SDL system call and RV32IM ISA (with B extension)
$ wget -q -O - https://raw.githubusercontent.com/xpack/assets/master/scripts/install-nvm-node-npm-xpm.sh | ${SHELL}
$ export NVM_DIR="$HOME/.nvm"
$ source "$NVM_DIR/nvm.sh"
$ npm install --location=global xpm@latest
$ which xpm
/home/eric/.nvm/versions/node/v22.13.0/bin/xpm
$ xpm --version
0.20.5
$ xpm install @xpack-dev-tools/riscv-none-elf-gcc@latest --global --verbose
$ export PATH=$HOME/.local/xPacks/@xpack-dev-tools/riscv-none-elf-gcc/14.2.0-2.1/.content/bin:$PATH
$ riscv-none-elf-gcc --version
riscv-none-elf-gcc (xPack GNU RISC-V Embedded GCC x86_64) 14.2.0
Copyright (C) 2024 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
//hello.c
#include <stdio.h>
int main(){
printf("hello world!!!\n");
return 0;
}
riscv-none-elf-gcc -march=rv32gc -mabi=ilp32 -o hello hello.c
$ build/rv32emu hello
hello world!!!
inferior exit code 0
#include <stdio.h>
int main(){
int syscall_num = 0xbeef;
int width = 1000;
int height = 600;
static int buffer[1000 * 600];
int i = 0;
for(;i < 1000 * 300;i++){
buffer[i] = 0xFF0000;
}
for(;i < 1000 * 600;i++){
buffer[i] = 0xFBFF00;
}
register int* a0 asm("a0") = &buffer[0];
register int a1 asm("a1") = width;
register int a2 asm("a2") = height;
while(1) {
register int a7 asm("a7") = syscall_num;
asm volatile ("scall");
}
return 0;
}
$ sudo apt install libjpeg-dev libpng-dev
$ sudo apt install libpixman-1-dev
$ sudo apt install libsdl2-dev
$ make config
$ make
$ ./demo-sdl
make
.$ make
DEP .libtwin.a/src/work.c.d
DEP .libtwin.a/src/window.c.d
DEP .libtwin.a/src/widget.c.d
...
CC .demo-sdl/apps/main.c.o
LINK .demo-sdl/demo-sdl
CP demo-sdl
The files in app
and src
will implement Dependency Generation (DEP)
and Compile (CC)
then write into .libapps.a/apps
and .libtwin.a/src
respectively.
AR
stands for "Archive tool". It collects all the .o
file to .a
file which become a static library. In this case, we collect files in .libapps.a/apps
to .libapps.a/libapps.a
and files in .libtwin.a/src
to .libtwin.a/libtwin.a
RANLIB
stands for "Random Library Index Generator." It creates an index table for .a
static libraries. This index table contains the symbol information of all .o
files within the library, allowing for quick location of required functions or variables during the linking process.
implement DEP
and CC
on app/main.c
and LINK
the following files : .demo-sdl/apps/main.c.o
libapps.a
libtwin.a
in order to create executable file .demo-sdl/demo-sdl
.
$ file demo-sdl
demo-sdl: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=f554f2df4c8710331ec9b5cb8bfb511e763d58cd, for GNU/Linux 3.2.0, not stripped
This tell us demo-sdl
is 64-bit executable file and is built in x86-64
ISA. The final goal is to turn it to 32-bit which built in riscv-none-elf-gcc
and run on rv32emu
.
Since riscv-none-elf-gcc
is not able to access third-party packages like libsdl2-dev
or libjpeg-dev
. I have to write a new graphic backend rv32emu.c
which call the system calls in rv32emu to implement window display.
mado/mk/toolchain.mk
, changed the compiler to prebuild xPack GNU RISC-V Embedded GCC.CC_IS_CLANG :=
CC_IS_GCC :=
# FIXME: Cross-compilation using Clang is not supported.
ifdef CROSS_COMPILE
CC := $(CROSS_COMPILE)gcc
endif
+ CC = riscv-none-elf-gcc
+ CFLAGS += -std=gnu99
+ CFLAGS += -march=rv32im
+ CFLAGS += -mabi=ilp32
override CC := $(shell which $(CC))
ifndef CC
$(error "Valid C compiler not found.")
endif
rv32emu.c
#ifndef MADO
#define MADO
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
struct mado {
const char *title;
const int width, height;
uint32_t *buf;
int keys[256]; /* keys are mostly ASCII, but arrows are 17..20 */
int mod; /* mod is 4 bits mask, ctrl=1, shift=2, alt=4, meta=8 */
int x, y;
int mouse;
void *event_queue;
uint32_t event_count;
size_t event_queue_start;
};
#ifndef MADO_API
#define MADO_API extern
#endif
MADO_API int mado_open(struct mado *m);
MADO_API int mado_loop(struct mado *m);
MADO_API void mado_close(struct mado *m);
MADO_API void mado_sleep(int64_t ms);
MADO_API int64_t mado_time(void);
#define mado_pixel(m, x, y) ((m)->buf[((y) * (m)->width) + (x)])
#ifndef MADO_HEADER
#define RV_QUEUE_CAPACITY 128
enum {
RV_KEYCODE_RETURN = 0x0000000D,
RV_KEYCODE_UP = 0x40000052,
RV_KEYCODE_DOWN = 0x40000051,
RV_KEYCODE_RIGHT = 0x4000004F,
RV_KEYCODE_LEFT = 0x40000050,
RV_KEYCODE_LCTRL = 0x400000E0,
RV_KEYCODE_RCTRL = 0x400000E4,
RV_KEYCODE_LSHIFT = 0x400000E1,
RV_KEYCODE_RSHIFT = 0x400000E5,
RV_KEYCODE_LALT = 0x400000E2,
RV_KEYCODE_RALT = 0x400000E6,
RV_KEYCODE_LMETA = 0x400000E3,
RV_KEYCODE_RMETA = 0x400000E7,
};
//KEY_EVENT
typedef struct {
uint32_t keycode;
uint8_t state;
uint16_t mod;
} rv_key_rv_event_t;
//MOUSE_MOTION_EVENT
typedef struct {
int32_t x, y, xrel, yrel;
} rv_mouse_motion_t;
enum {
RV_MOUSE_BUTTON_LEFT = 1,
};
//MOUSE_BUTTON_EVENT
typedef struct {
uint8_t button;
uint8_t state;
} rv_mouse_button_t;
enum {
RV_KEY_EVENT = 0,
RV_MOUSE_MOTION_EVENT = 1,
RV_MOUSE_BUTTON_EVENT = 2,
RV_QUIT_EVENT = 3,
};
typedef struct {
uint32_t type;
union {
rv_key_rv_event_t key_event;
union {
rv_mouse_motion_t motion;
rv_mouse_button_t button;
} mouse;
};
} rv_event_t;
enum {
RV_RELATIVE_MODE_SUBMISSION = 0,
RV_WINDOW_TITLE_SUBMISSION = 1,
};
typedef struct {
uint8_t enabled;
} rv_mouse_rv_submission_t;
typedef struct {
uint32_t title;
uint32_t size;
} rv_title_rv_submission_t;
typedef struct {
uint32_t type;
union {
rv_mouse_rv_submission_t mouse;
rv_title_rv_submission_t title;
};
} rv_submission_t;
MADO_API int mado_open(struct mado *m)
{
m->event_queue = malloc((sizeof(rv_event_t) + sizeof(rv_submission_t)) *
RV_QUEUE_CAPACITY);
m->event_count = 0;
register int a0 __asm("a0") = (int) m->event_queue;
register int a1 __asm("a1") = RV_QUEUE_CAPACITY;
register int a2 __asm("a2") = (int) &m->event_count;
register int a7 __asm("a7") = 0xc0de;
__asm volatile("scall" : : "r"(a0), "r"(a1), "r"(a2), "r"(a7) : "memory");
m->event_queue_start = 0;
rv_submission_t submission = {
.type = RV_WINDOW_TITLE_SUBMISSION,
.title.title = (uint32_t) m->title,
.title.size = strlen(m->title),
};
rv_submission_t *submission_queue =
(rv_submission_t *) ((rv_event_t *) m->event_queue + RV_QUEUE_CAPACITY);
submission_queue[0] = submission;
a0 = 1;
a7 = 0xfeed;
__asm volatile("scall" : : "r"(a0), "r"(a7) : "memory");
return 0;
}
MADO_API void mado_close(struct mado *m)
{
free(m->event_queue);
}
MADO_API int mado_loop(struct mado *m)
{
for (; m->event_count > 0; m->event_count--) {
rv_event_t event =
((rv_event_t *) m->event_queue)[m->event_queue_start];
switch (event.type) {
case RV_KEY_EVENT: {
uint32_t keycode = event.key_event.keycode;
uint8_t state = event.key_event.state;
if (keycode == RV_KEYCODE_RETURN)
m->keys[10] = state;
else if (keycode < 128)
m->keys[keycode] = state;
if (keycode == RV_KEYCODE_UP)
m->keys[17] = state;
if (keycode == RV_KEYCODE_DOWN)
m->keys[18] = state;
if (keycode == RV_KEYCODE_RIGHT)
m->keys[19] = state;
if (keycode == RV_KEYCODE_LEFT)
m->keys[20] = state;
if (keycode == RV_KEYCODE_LCTRL || keycode == RV_KEYCODE_RCTRL)
m->mod = state ? m->mod | 1 : m->mod & 0b1110;
if (keycode == RV_KEYCODE_LSHIFT || keycode == RV_KEYCODE_RSHIFT)
m->mod = state ? m->mod | 2 : m->mod & 0b1101;
if (keycode == RV_KEYCODE_LALT || keycode == RV_KEYCODE_RALT)
m->mod = state ? m->mod | 4 : m->mod & 0b1011;
if (keycode == RV_KEYCODE_LMETA || keycode == RV_KEYCODE_RMETA)
m->mod = state ? m->mod | 8 : m->mod & 0b0111;
break;
}
case RV_MOUSE_MOTION_EVENT:
m->x = event.mouse.motion.x;
m->y = event.mouse.motion.y;
break;
case RV_MOUSE_BUTTON_EVENT:
if (event.mouse.button.button == RV_MOUSE_BUTTON_LEFT)
m->mouse = event.mouse.button.state;
break;
case RV_QUIT_EVENT:
return -1;
}
m->event_queue_start =
(m->event_queue_start + 1) & (RV_QUEUE_CAPACITY - 1);
}
register int a0 __asm("a0") = (uintptr_t) m->buf;
register int a1 __asm("a1") = m->width;
register int a2 __asm("a2") = m->height;
register int a7 __asm("a7") = 0xbeef;
__asm volatile("scall" : : "r"(a0), "r"(a1), "r"(a2), "r"(a7) : "memory");
return 0;
}
#undef RV_QUEUE_CAPACITY
MADO_API int64_t mado_time(void)
{
return (int64_t) clock() / (CLOCKS_PER_SEC / 1000.0f);
}
MADO_API void mado_sleep(int64_t ms)
{
int64_t start = mado_time();
while (mado_time() - start < ms)
;
}
#ifdef __cplusplus
class Mado
{
struct mado m;
int64_t now;
public:
Mado(const int w, const int h, const char *title)
: m{.title = title, .width = w, .height = h}
{
this->m.buf = new uint32_t[w * h];
this->now = mado_time();
mado_open(&this->m);
}
~Mado()
{
mado_close(&this->m);
delete[] this->m.buf;
}
bool loop(const int fps)
{
int64_t t = mado_time();
if (t - this->now < 1000 / fps) {
mado_sleep(t - now);
}
this->now = t;
return mado_loop(&this->m) == 0;
}
inline uint32_t &px(const int x, const int y)
{
return mado_pixel(&this->m, x, y);
}
bool key(int c) { return c >= 0 && c < 128 ? this->m.keys[c] : false; }
int x() { return this->m.x; }
int y() { return this->m.y; }
int mouse() { return this->m.mouse; }
int mod() { return this->m.mod; }
};
#endif /* __cplusplus */
#endif /* !MADO_HEADER */
#endif /* MADO_H */
+#include "../backend/rv32emu.c"
int main(void)
{
+ enum { W = 640, H = 480 };
+ uint32_t buf[W * H];
+ struct mado m = {.title = "mado", .width = W, .height = H, .buf = buf};
+ mado_open(&m);
+ uint32_t t = 0;
+ while(mado_loop(&m) == 0){
+ t++;
+ for (int i = 0; i < 320; i++) {
+ for (int j = 0; j < 240; j++)
+ mado_pixel(&m, i, j) = i ^ j ^ t; /* Munching squares */
+ }
+ mado_sleep(100);
+ }
+ mado_close(&m);
+ return 0;
- tx = twin_create(WIDTH, HEIGHT);
- if (!tx)
- return 1;
//scroll to bottom
- twin_dispatch(tx);
mado/configs/Kconfig
choice
prompt "Backend Selection"
default BACKEND_SDL
+ config BACKEND_rv32emu
+ bool "rv32emu output support"
config BACKEND_FBDEV
bool "Linux framebuffer support"
select CURSOR
config BACKEND_SDL
bool "SDL video output support"
config BACKEND_VNC
bool "VNC server output support"
endchoice
Makefile
# Graphical backends
BACKEND := none
+ifeq ($(CONFIG_BACKEND_rv32emu), y)
+BACKEND = rv32emu
+libtwin.a_files-y += backend/rv32emu.c
+libtwin.a_cflags-y += -I$(HOME)/.local/xPacks/@xpack-dev-tools/riscv-none-elf-gcc/14.2.0-2.1/.content/riscv-none-elf/include
+endif
ifeq ($(CONFIG_BACKEND_SDL), y)
BACKEND = sdl
libtwin.a_files-y += backend/sdl.c
libtwin.a_cflags-y += $(shell sdl2-config --cflags)
TARGET_LIBS += $(shell sdl2-config --libs)
endif
mado/src/draw.c
-#if defined(__APPLE__)
#include <machine/endian.h>
-#else
-#include <endian.h>
-#endif
Third-party packages like libjpeg
and libpng
are prepared for x86-64 projects.
So I have to compile those packages to riscv version using the cross-compiler xPack GNU RISC-V Embedded GCC
$ PACKAGE_PREFIX=$HOME/.local/xPacks/@xpack-dev-tools/riscv-none-elf-gcc/14.2.0-2.1/.content/riscv-none-elf
# build libjpeg and libpng
$ sudo tar xzf jpegsrc.v9f $PACKAGE_PREFIX
$ cd jpeg-9f
$ ./configure –host=riscv-none-elf-gcc –prefix=$PACKAGE_PREFIX
$ make
$ sudo make install
$ sudo tar xzf libpng-1.5.12 $PACKAGE_PREFIX
$ cd libpng-1.5.12
$ ./configure –host=riscv-none-elf-gcc –prefix=$PACKAGE_PREFIX
$ make
$ sudo make install
demo-rv32emu
Compare to original demo file demo-sdl
, I took away the jpeg, png and gif file from the makefile, just barely implement system call for display window.
$ build/rv32emu demo-rv32emu
I implement cross-compile on mado from x86-64
to riscv-none-elf-gcc
and run on rv32emu, but I can only show an empty window without those pictures like the demo-sdl
.
Although I didn't finish my goal, I spend a lot time to do the research and actually learn knowledge about cross-compile and how to write Makefile. So it's still a great experience I think.