Read the article...
Let's create a Software Emulator for Sophgo SG2000 SoC and Milk-V Duo S SBC! It runs every day for our Daily Automated Testing of Apache NuttX RTOS for SG2000.
We begin with the TinyEMU RISC-V Emulator for Ox64 BL808 SBC. And we tweak it for SG2000.
We update the System Memory Map for SG2000...
Update RAM Base Addr, CLINT Addr, PLIC Addr
#define RAM_BASE_ADDR 0x80200000ul
#define CLINT_BASE_ADDR 0x74000000ul // TODO: Unused
#define PLIC_BASE_ADDR 0x70000000ul
Then build and run TinyEMU...
## Build TinyEMU for macOS
## For Linux: See https://github.com/lupyuen/nuttx-sg2000/blob/main/.github/workflows/sg2000-test.yml#L29-L45
cd $HOME/sg2000-emulator/
make clean
make \
CFLAGS="-I$(brew --prefix)/opt/openssl/include -I$(brew --prefix)/opt/sdl2/include" \
LDFLAGS="-L$(brew --prefix)/opt/openssl/lib -L$(brew --prefix)/opt/sdl2/lib" \
CONFIG_MACOS=y
## Build NuttX for SG2000
## https://lupyuen.github.io/articles/sg2000#appendix-build-nuttx-for-sg2000
cd $HOME/nuttx
tools/configure.sh milkv_duos:nsh
make
## Omitted: Create the `Image` file for SG2000 NuttX
## Boot TinyEMU with NuttX for SG2000
cd $HOME/nuttx
wget https://raw.githubusercontent.com/lupyuen/nuttx-sg2000/main/nuttx.cfg
$HOME/sg2000/sg2000-emulator/temu nuttx.cfg
This is how we fix the rest of the emulator...
When we boot TinyEMU with SG2000 NuttX, it crashes...
$ sg2000-emulator/temu nuttx.cfg
TinyEMU Emulator for Sophgo SG2000 SoC
virtio_console_init
Patched DCACHE.IALL (Invalidate all Page Table Entries in the D-Cache) at 0x80200a28
Patched SYNC.S (Ensure that all Cache Operations are completed) at 0x80200a2c
Found ECALL (Start System Timer) at 0x8020b2c6
Patched RDTIME (Read System Time) at 0x8020b2cc
elf_len=0
virtio_console_resize_event
raise_exception2: cause=1, tval=0xffffffff80200000, pc=0xffffffff80200000
pc =ffffffff80200000 ra =0000000000000000 sp =0000000000000000 gp =0000000000000000
tp =0000000000000000 t0 =ffffffff80200000 t1 =0000000000000000 t2 =0000000000000000
s0 =0000000000000000 s1 =0000000000000000 a0 =0000000000000000 a1 =0000000000001040
a2 =0000000000000000 a3 =0000000000000000 a4 =0000000000000000 a5 =0000000000040800
a6 =0000000000000000 a7 =0000000000000000 s2 =0000000000000000 s3 =0000000000000000
s4 =0000000000000000 s5 =0000000000000000 s6 =0000000000000000 s7 =0000000000000000
s8 =0000000000000000 s9 =0000000000000000 s10=0000000000000000 s11=0000000000000000
t3 =0000000000000000 t4 =0000000000000000 t5 =0000000000000000 t6 =0000000000000000
priv=S mstatus=0000000a00040080 cycles=19
mideleg=0000000000000222 mie=0000000000000000 mip=00000000000000a0
tinyemu: Illegal instruction, quitting: pc=0x0, instruction=0x0
0xffffffff80200000 looks sus, it seems related to RAM Base Address 0x80200000. We check the TinyEMU Boot Code: riscv_machine.c
static void copy_bios(...) {
...
// TinyEMU Boot Code
q = (uint32_t *)(ram_ptr + 0x1000);
// Load RAM Base Address into Register T0:
// auipc t0, RAM_BASE_ADDR
q[0] = 0x297 + RAM_BASE_ADDR - 0x1000;
Maybe auipc has a problem? We run the RISC-V Online Assembler
We try to assemble the TinyEMU Boot Code (for loading the RAM Base Address 0x80200000)...
auipc t0, 0x80200000
But it produces this error...
file.s: Assembler messages:
file.s:3: Error: lui expression not in range 0..1048575
file.s:3: Error: value of 0000080200000000 too large for field of 4 bytes at 0000000000000000
0x80200000 is too big to assemble into the auipc address!
So we load 0x80200000 into Register t0 in another way: li
li t0, 0x80200000
RISC-V Online Assembler produces...
0: 4010029b addiw t0,zero,1025
4: 01529293 slli t0,t0,0x15
We copy this into our TinyEMU Boot Code...
Change auipc t0, 0x80200000 to li t0, 0x80200000
// `li t0, 0x80200000` gets assembled to:
// 4010029b addiw t0,zero,1025
// 01529293 slli t0,t0,0x15
q[pc++] = 0x4010029b; // addiw t0,zero,1025
q[pc++] = 0x01529293; // slli t0,t0,0x15
// Previously: q[pc++] = 0x297 + RAM_BASE_ADDR - 0x1000; /* auipc t0, jump_addr */
// Which fails because RAM_BASE_ADDR is too big for auipc
And it hangs (instead of crashing). Some progress!
$ sg2000-emulator/temu nuttx.cfg
TinyEMU Emulator for Sophgo SG2000 SoC
virtio_console_init
Patched DCACHE.IALL (Invalidate all Page Table Entries in the D-Cache) at 0x80200a28
Patched SYNC.S (Ensure that all Cache Operations are completed) at 0x80200a2c
Found ECALL (Start System Timer) at 0x8020b2c6
Patched RDTIME (Read System Time) at 0x8020b2cc
elf_len=0
virtio_console_resize_event
We fix the UART Output Registers: Update UART Output Register and UART Status Register
Now we see NSH Shell yay!
$ $HOME/sg2000/sg2000-emulator/temu nuttx.cfg
...
TinyEMU Emulator for Sophgo SG2000 SoC
virtio_console_init
Patched DCACHE.IALL (Invalidate all Page Table Entries in the D-Cache) at 0x80200a28
Patched SYNC.S (Ensure that all Cache Operations are completed) at 0x80200a2c
Found ECALL (Start System Timer) at 0x8020b2c6
Patched RDTIME (Read System Time) at 0x8020b2cc
elf_len=0
virtio_console_resize_event
ABC
NuttShell (NSH) NuttX-12.5.1
nsh>
When we press a key, NuttX triggers an expected interrupt...
$ $HOME/sg2000/sg2000-emulator/temu nuttx.cfg
...
NuttShell (NSH) NuttX-12.5.1
nsh> irq_unexpected_isr: ERROR irq: 45
_assert: Current Version: NuttX 12.5.1 218ccd843a Jun 18 2024 22:14:46 risc-v
_assert: Assertion failed panic: at file: irq/irq_unexpectedisr.c:54 task: Idle_Task process: Kernel 0x8020110c
up_dump_register: EPC: 000000008021432a
Now we fix the UART Input and UART Interrupt. Based on the NuttX Config...
CONFIG_16550_UART0_IRQ=69
Since NuttX IRQ Offset is 25, so Actual RISC-V IRQ is 69 - 25 = 44 (as confirmed by the SG2000 Reference Manual)...
#define VIRTIO_IRQ 44 // UART0 IRQ
But when we press a key: TinyEMU crashes with a Segmentation Fault...
$ $HOME/sg2000/sg2000-emulator/temu nuttx.cfg
...
NuttShell (NSH) NuttX-12.5.1
nsh> [1] 94499 segmentation fault $HOME/sg2000/sg2000-emulator/temu nuttx.cfg
We debug with lldb
(because gdb
is not available for macOS Arm64)...
$ lldb $HOME/sg2000/sg2000-emulator/temu nuttx.cfg
(lldb) target create "/Users/luppy/sg2000/sg2000-emulator/temu"
Current executable set to '/Users/luppy/sg2000/sg2000-emulator/temu' (arm64).
(lldb) settings set -- target.run-args "nuttx.cfg"
(lldb) r
Process 90245 launched: '/Users/luppy/sg2000/sg2000-emulator/temu' (arm64)
...
NuttShell (NSH) NuttX-12.5.1
Process 90245 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x0)
frame #0: 0x0000000000000000
error: memory read failed for 0x0
Target 0: (temu) stopped.
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x0)
* frame #0: 0x0000000000000000
frame #1: 0x000000010000b978 temu`virt_machine_run(m=0x000000013461b2a0) at temu.c:598:17 [opt]
frame #2: 0x000000010000be6c temu`main(argc=<unavailable>, argv=<unavailable>) at temu.c:845:9 [opt]
frame #3: 0x0000000197a4e0e0 dyld`start + 2360
(lldb)
Which is at...
void virt_machine_run(VirtMachine *m) {
...
virtio_console_write_data(m->console_dev, buf, ret);
Why did TinyEMU crash? This doesn't make sense, m
is non-null!
Maybe it's already optimised? Let's disable GCC Optimisation...
Now it makes more sense!
$ lldb $HOME/sg2000/sg2000-emulator/temu nuttx.cfg
...
NuttShell (NSH) NuttX-12.5.1
Process 1595 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x0)
frame #0: 0x0000000000000000
error: memory read failed for 0x0
Target 0: (temu) stopped.
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x0)
* frame #0: 0x0000000000000000
frame #1: 0x0000000100002604 temu`set_irq(irq=0x000000013a607b08, level=1) at iomem.h:145:5
frame #2: 0x00000001000025a4 temu`virtio_console_write_data(s=0x000000013d604080, buf="a", buf_len=1) at virtio.c:1346:5
frame #3: 0x000000010000fd2c temu`virt_machine_run(m=0x000000013a607680) at temu.c:598:17
frame #4: 0x00000001000105cc temu`main(argc=2, argv=0x000000016fdff080) at temu.c:845:9
frame #5: 0x0000000197a4e0e0 dyld`start + 2360
Which crashes here...
static inline void set_irq(IRQSignal *irq, int level) {
irq->set_irq(irq->opaque, irq->irq_num, level);
}
Which means irq->set_irq
is null! (Rightfully it should be set to plic_set_irq
)
Why is irq->set_irq
set to null?
We verify with an Assertion Check: irq->set_irq is null!
NuttShell (NSH) NuttX-12.5.1
nsh> Assertion failed: (irq->set_irq != NULL), function set_irq, file iomem.h, line 145.
Process 15262 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = hit program assert
frame #4: 0x000000010000249c temu`set_irq(irq=0x000000014271a478, level=1) at iomem.h:145:5
142
143 static inline void set_irq(IRQSignal *irq, int level)
144 {
-> 145 assert(irq->set_irq != NULL); //// TODO
146 irq->set_irq(irq->opaque, irq->irq_num, level);
147 }
148
Target 0: (temu) stopped.
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = hit program assert
frame #0: 0x0000000197d9ea60 libsystem_kernel.dylib`__pthread_kill + 8
frame #1: 0x0000000197dd6c20 libsystem_pthread.dylib`pthread_kill + 288
frame #2: 0x0000000197ce3a30 libsystem_c.dylib`abort + 180
frame #3: 0x0000000197ce2d20 libsystem_c.dylib`__assert_rtn + 284
* frame #4: 0x000000010000249c temu`set_irq(irq=0x000000014271a478, level=1) at iomem.h:145:5
frame #5: 0x0000000100002418 temu`virtio_console_write_data(s=0x0000000142719980, buf="a\xc0\xedE", buf_len=1) at virtio.c:1346:5
frame #6: 0x000000010000fc30 temu`virt_machine_run(m=0x0000000142719ff0) at temu.c:598:17
frame #7: 0x00000001000104d0 temu`main(argc=2, argv=0x000000016fdff080) at temu.c:845:9
frame #8: 0x0000000197a4e0e0 dyld`start + 2360
We inspect the variables in LLDB...
frame #4: 0x000000010000249c temu`set_irq(irq=0x000000014271a478, level=1) at iomem.h:145:5
142
143 static inline void set_irq(IRQSignal *irq, int level)
144 {
-> 145 assert(irq->set_irq != NULL); //// TODO
146 irq->set_irq(irq->opaque, irq->irq_num, level);
147 }
148
(lldb) frame variable
(IRQSignal *) irq = 0x000000014271a478
(int) level = 1
(lldb) p *irq
(IRQSignal) {
set_irq = 0x0000000000000000
opaque = 0x0000000000000000
irq_num = 0
}
irq
is all empty! Where does irq
come from? We step up the Call Stack...
(lldb) up
frame #5: 0x0000000100002418 temu`virtio_console_write_data(s=0x0000000142719980, buf="a\xc0\xedE", buf_len=1) at virtio.c:1346:5
1343 _info("[%c]\n", buf[0]); ////
1344 set_input(buf[0]);
1345 s->int_status |= 1;
-> 1346 set_irq(s->irq, 1);
1347
1348 #ifdef NOTUSED
1349 int queue_idx = 0;
(lldb) frame variable
(VIRTIODevice *) s = 0x0000000142719980
(const uint8_t *) buf = 0x000000016fdfeae8 "a\xc0\xedE"
(int) buf_len = 1
(lldb) p *s
(VIRTIODevice) {
mem_map = 0x000000014380fc00
mem_range = 0x000000014380fe60
pci_dev = NULL
irq = 0x000000014271a478
get_ram_ptr = 0x00000001000065dc (temu`virtio_mmio_get_ram_ptr at virtio.c:193)
debug = 0
int_status = 1
status = 0
device_features_sel = 0
queue_sel = 0
queue = {
[0] = {
ready = 0
num = 16
last_avail_idx = 0
desc_addr = 0
avail_addr = 0
used_addr = 0
manual_recv = YES
}
[1] = {
ready = 0
num = 16
last_avail_idx = 0
desc_addr = 0
avail_addr = 0
used_addr = 0
manual_recv = NO
}
[2] = {
ready = 0
num = 16
last_avail_idx = 0
desc_addr = 0
avail_addr = 0
used_addr = 0
manual_recv = NO
}
[3] = {
ready = 0
num = 16
last_avail_idx = 0
desc_addr = 0
avail_addr = 0
used_addr = 0
manual_recv = NO
}
[4] = {
ready = 0
num = 16
last_avail_idx = 0
desc_addr = 0
avail_addr = 0
used_addr = 0
manual_recv = NO
}
[5] = {
ready = 0
num = 16
last_avail_idx = 0
desc_addr = 0
avail_addr = 0
used_addr = 0
manual_recv = NO
}
[6] = {
ready = 0
num = 16
last_avail_idx = 0
desc_addr = 0
avail_addr = 0
used_addr = 0
manual_recv = NO
}
[7] = {
ready = 0
num = 16
last_avail_idx = 0
desc_addr = 0
avail_addr = 0
used_addr = 0
manual_recv = NO
}
}
device_id = 3
vendor_id = 65535
device_features = 1
device_recv = 0x00000001000025cc (temu`virtio_console_recv_request at virtio.c:1278)
config_write = 0x0000000000000000
config_space_size = 4
config_space = "P\0\U00000019"
}
This says that s
is a virtio_console
.
Why is s->irq
empty? We step up the Call Stack...
(lldb) up
frame #6: 0x000000010000fc30 temu`virt_machine_run(m=0x0000000142719ff0) at temu.c:598:17
595 len = min_int(len, sizeof(buf));
596 ret = m->console->read_data(m->console->opaque, buf, len);
597 if (ret > 0) {
-> 598 virtio_console_write_data(m->console_dev, buf, ret);
599 }
600 }
601 #endif
(lldb) p *m
(VirtMachine) {
vmc = 0x0000000100080620
net = NULL
console_dev = 0x0000000142719980
console = 0x0000000142719510
fb_dev = NULL
}
s->irq
comes from m->console_dev
.
Why is console_dev
not properly inited?
From earlier: UART0 is at RISC-V IRQ 44. But we discover that TinyEMU supports only 32 IRQs!
static VirtMachine *riscv_machine_init(const VirtMachineParams *p) {
for(i = 1; i < 32; i++) {
irq_init(&s->plic_irq[i], plic_set_irq, s, i);
}
So we increase the IRQs from 32 to 256: Increase the IRQs from 32 to 256
(256 IRQs is too many, as we shall soon see)
Now we see something different when we press a key!
NuttShell (NSH) NuttX-12.5.1
nsh> irq_unexpected_isr: ERROR irq: 37
_assert: Current Version: NuttX 12.5.1 218ccd843a Jun 18 2024 22:14:46 risc-v
_assert: Assertion failed panic: at file: irq/irq_unexpectedisr.c:54 task: Idle_Task process: Kernel 0x8020110c
up_dump_register: EPC: 000000008021432a
What is NuttX IRQ 37? (RISC-V IRQ 12) Shouldn't it be RISC-V IRQ 44 for UART Input?
Seems the RISC-V IRQs wrap around at 32? So RISC-V IRQ 44 becomes IRQ 12?
NuttShell (NSH) NuttX-12.5.1
nsh> plic_set_irq: irq_num=44, state=1
plic_pending_irq=0x800, plic_served_irq=0x0, mask=0x800
plic_update_mip: set_mip, pending=0x800, served=0x0
plic_read: offset=0x201004
plic_update_mip: reset_mip, pending=0x800, served=0x800
plic_read: pending irq=0xc
plic_pending_irq=0x800, plic_served_irq=0x800, mask=0x800
irq_unexpected_isr: ERROR irq: 37
So we fix the IRQ Size: Increase the Pending IRQ Size and Served IRQ Size from 32-bit to 64-bit
Now we see the correct Pending IRQ!
NuttShell (NSH) NuttX-12.5.1
nsh> plic_set_irq: irq_num=44, state=1
plic_update_mip: set_mip, pending=0x80000000000, served=0x0
plic_read: offset=0x201004
plic_update_mip: reset_mip, pending=0x80000000000, served=0x80000000000
plic_read: pending irq=0x2c
plic_write: offset=0x201004, val=0x2c
plic_update_mip: set_mip, pending=0x80000000000, served=0x0
plic_read: offset=0x201004
plic_update_mip: reset_mip, pending=0x80000000000, served=0x80000000000
plic_read: pending irq=0x2c
plic_write: offset=0x201004, val=0x2c
plic_update_mip: set_mip, pending=0x80000000000, served=0x0
plic_read: offset=0x201004
plic_update_mip: reset_mip, pending=0x80000000000, served=0x80000000000
plic_read: pending irq=0x2c
plic_write: offset=0x201004, val=0x2c
Why does Pending IRQ loop forever? Maybe because we haven't cleared the UART Interrupt?
To find out why, we trace the reads and writes to UART Registers: Log the invalid memory accesses
NuttShell (NSH) NuttX-12.5.1
target_write_slow: invalid physical address 0x0000000004140004
User ECALL: pc=0xc0001998
target_write_slow: invalid physical address 0x0000000004140004
target_write_slow: invalid physical address 0x0000000004140004
nsh> target_write_slow: invalid physical address 0x0000000004140004
User ECALL: pc=0xc0001998
target_write_slow: invalid physical address 0x0000000004140004
target_write_slow: invalid physical address 0x0000000004140004
target_write_slow: invalid physical address 0x0000000004140004
User ECALL: pc=0xc000aa0e
target_write_slow: invalid physical address 0x0000000004140004
target_write_slow: invalid physical address 0x0000000004140004
plic_set_irq: irq_num=44, state=1
plic_update_mip: set_mip, pending=0x80000000000, served=0x0
plic_read: offset=0x201004
plic_update_mip: reset_mip, pending=0x80000000000, served=0x80000000000
plic_read: pending irq=0x2c
target_read_slow: invalid physical address 0x0000000004140008
target_read_slow: invalid physical address 0x0000000004140008
target_read_slow: invalid physical address 0x0000000004140018
target_read_slow: invalid physical address 0x0000000004140008
target_read_slow: invalid physical address 0x0000000004140018
target_read_slow: invalid physical address 0x0000000004140008
target_read_slow: invalid physical address 0x0000000004140018
What are UART Registers 0x4140008 and 0x4140018? Why are they read when we press a key?
We look up the UART Registers...
// UART Registers from https://github.com/apache/nuttx/blob/master/include/nuttx/serial/uart_16550.h
#define UART0_BASE_ADDR 0x04140000
#define CONFIG_16550_REGINCR 4
#define UART_IIR_INCR 2 /* Interrupt ID Register */
#define UART_FCR_INCR 2 /* FIFO Control Register */
#define UART_MSR_INCR 6 /* Modem Status Register */
// So 0x4140008 is one of these...
#define UART_IIR_OFFSET (CONFIG_16550_REGINCR*UART_IIR_INCR)
#define UART_FCR_OFFSET (CONFIG_16550_REGINCR*UART_FCR_INCR)
// And 0x4140018 is...
#define UART_MSR_OFFSET (CONFIG_16550_REGINCR*UART_MSR_INCR)
We check the NuttX 16550 UART Driver for UART_IIR_OFFSET, UART_FCR_OFFSET and UART_MSR_OFFSET. (See the next section)
What is UART Register 0x4140004? Why is it read when we print to UART?
We look up the UART Register...
// UART Registers from https://github.com/apache/nuttx/blob/master/include/nuttx/serial/uart_16550.h
#define UART0_BASE_ADDR 0x04140000
#define CONFIG_16550_REGINCR 4
#define UART_DLM_INCR 1 /* (DLAB =1) Divisor Latch MSB */
#define UART_IER_INCR 1 /* (DLAB =0) Interrupt Enable Register */
// So 0x4140004 is one of these...
#define UART_DLM_OFFSET (CONFIG_16550_REGINCR*UART_DLM_INCR)
#define UART_DLM_OFFSET (CONFIG_16550_REGINCR*UART_IER_INCR)
TODO: Check the NuttX 16550 UART Driver for UART_DLM_OFFSET and UART_DLM_OFFSET
How does the NuttX 16550 UART Driver use UART_IIR_OFFSET, UART_FCR_OFFSET and UART_MSR_OFFSET?
The NuttX 16550 UART Driver reads UART_IIR_OFFSET and UART_MSR_OFFSET to handle UART Interrupts: uart_16550.c
static int u16550_interrupt(int irq, FAR void *context, FAR void *arg)
{
FAR struct uart_dev_s *dev = (struct uart_dev_s *)arg;
FAR struct u16550_s *priv;
uint32_t status;
int passes;
DEBUGASSERT(dev != NULL && dev->priv != NULL);
priv = (FAR struct u16550_s *)dev->priv;
/* Loop until there are no characters to be transferred or,
* until we have been looping for a long time.
*/
for (passes = 0; passes < 256; passes++)
{
/* Get the current UART status and check for loop
* termination conditions
*/
status = u16550_serialin(priv, UART_IIR_OFFSET);
/* The UART_IIR_INTSTATUS bit should be zero if there are pending
* interrupts
*/
if ((status & UART_IIR_INTSTATUS) != 0)
{
/* Break out of the loop when there is no longer a
* pending interrupt
*/
break;
}
/* Handle the interrupt by its interrupt ID field */
switch (status & UART_IIR_INTID_MASK)
{
/* Handle incoming, receive bytes (with or without timeout) */
case UART_IIR_INTID_RDA:
case UART_IIR_INTID_CTI:
{
uart_recvchars(dev);
break;
}
/* Handle outgoing, transmit bytes */
case UART_IIR_INTID_THRE:
{
uart_xmitchars(dev);
break;
}
/* Just clear modem status interrupts (UART1 only) */
case UART_IIR_INTID_MSI:
{
/* Read the modem status register (MSR) to clear */
status = u16550_serialin(priv, UART_MSR_OFFSET);
sinfo("MSR: %02"PRIx32"\n", status);
break;
}
/* Just clear any line status interrupts */
case UART_IIR_INTID_RLS:
{
/* Read the line status register (LSR) to clear */
status = u16550_serialin(priv, UART_LSR_OFFSET);
sinfo("LSR: %02"PRIx32"\n", status);
break;
}
/* There should be no other values */
default:
{
serr("ERROR: Unexpected IIR: %02"PRIx32"\n", status);
break;
}
}
}
return OK;
}
Thus we need to emulate the UART Interrupt. To Receive Data...
-
UART Register UART_IIR: Should return UART_IIR_INTID_RDA (data available)
Then it should return UART_IIR_INTSTATUS (no more data)
-
UART Register UART_LSR: Should return UART_LSR_DR (data available)
Then it should return 0 (no more data)
-
UART Register UART_RBR: Should return the input data
We emulate the UART Interrupt like this: Emulate UART Interrupt: UART_LSR, UART_IIR, UART_RBR
Now we see...
plic_set_irq: irq_num=44, state=1
plic_update_mip: set_mip, pending=0x80000000000, served=0x0
plic_read: offset=0x201004
plic_update_mip: reset_mip, pending=0x80000000000, served=0x80000000000
plic_read: pending irq=0x2c
read UART_IIR_OFFSET
read UART_RBR_OFFSET
read UART_IIR_OFFSET
plic_write: offset=0x201004, val=0x2c
plic_update_mip: set_mip, pending=0x80000000000, served=0x0
plic_read: offset=0x201004
plic_update_mip: reset_mip, pending=0x80000000000, served=0x80000000000
plic_read: pending irq=0x2c
read UART_IIR_OFFSET
plic_write: offset=0x201004, val=0x2c
plic_update_mip: set_mip, pending=0x80000000000, served=0x0
plic_read: offset=0x201004
plic_update_mip: reset_mip, pending=0x80000000000, served=0x80000000000
plic_read: pending irq=0x2c
read UART_IIR_OFFSET
Why is pending IRQ looping forever?
That's because we forgot to clear the UART Interrupt duh!
Finally it works OK yay! (We disabled logging)
$ sg2000-emulator/temu nuttx.cfg
TinyEMU Emulator for Sophgo SG2000 SoC
virtio_console_init
Patched DCACHE.IALL (Invalidate all Page Table Entries in the D-Cache) at 0x80200a28
Patched SYNC.S (Ensure that all Cache Operations are completed) at 0x80200a2c
Found ECALL (Start System Timer) at 0x8020b2c6
Patched RDTIME (Read System Time) at 0x8020b2cc
elf_len=0
virtio_console_resize_event
ABC
NuttShell (NSH) NuttX-12.5.1
nsh> uname -a
NuttX 12.5.1 50fadb93f2 Jun 18 2024 09:20:31 risc-v milkv_duos
nsh>
SG2000 Emulator seems slower than Ox64 BL808 Emulator?
Yeah probably because SG2000 runs on MTIMER_FREQ of 25000000.
When we execute sleep 10
, it completes in 25 seconds. We might need to adjust the TinyEMU System Timer.
(CPU-bound operations like getprime
won't have this timing delay)
Creating the SG2000 Emulator... Doesn't look so hard?
Yeah I'm begging all RISC-V SoC Makers: Please provide a Software Emulator for your RISC-V SoC! ๐
Just follow the steps in this article to create your RISC-V Emulator. Some SoC Peripherals might be missing, but a Barebones Emulator is still super helpful for porting, booting and testing any Operating System. ๐ ๐ ๐
This is a modified version of Fabrice Bellard's TinyEMU.
- 32/64/128-bit RISC-V emulation.
- VirtIO console, network, block device, input and 9P filesystem.
- Framebuffer emulation through SDL.
- Remote HTTP block device and filesystem.
- Small code, easy to modify, no external dependencies.
Changes from Fabrice Bellard's 2019-02-10 release:
- macOS and iOS support.
- Support for loading ELF images.
- Support for loading initrd images or compressed initramfs archives.
- Framebuffer support through SDL 2 instead of 1.2.
Use the VM images available from Fabrice Bellard's jslinux (no need to download them):
$ temu https://bellard.org/jslinux/buildroot-riscv64.cfg
Welcome to JS/Linux (riscv64)
Use 'vflogin username' to connect to your account.
You can create a new account at https://vfsync.org/signup .
Use 'export_file filename' to export a file to your computer.
Imported files are written to the home directory.
[root@localhost ~]# uname -a
Linux localhost 4.15.0-00049-ga3b1e7a-dirty #11 Thu Nov 8 20:30:26 CET 2018 riscv64 GNU/Linux
[root@localhost ~]#
Use C-a x
to exit the emulator.
You can also use TinyEMU with local configuration and disks. You can find more information in Fabrice Bellard's documentation for TinyEMU.
The easiest way to install TinyEMU is through Homebrew. There is a formula for TinyEMU in my Homebrew tap.
If you're compiling from source, you'll need:
Make sure to disable CONFIG_INT128
for 32-bit hosts.
TinyEMU was created by Fabrice Bellard. This port is maintained by Fernando Tarlรก Cardoso Lemos.
Unless otherwise specified in individual files, TinyEMU is available under the MIT license.
The SLIRP library has its own license (two-clause BSD license).