Programming the 8051: Step3, 8051-only Blinky

Today, the blinky, but on 8051.

We could look at the datasheet and determine what we are supposed to do, but milkv has thankfully been very fast to answer when asked and provided great examples for us (which enabled me to rewrite this tutorial in a simpler way), as well as drivers.
We will first segway into a tool you might come to use later on and will be useful here: strace.

So we will retrieve milkv’s example as well as its source and use strace on them:

chmod +x 8051_up
strace ./8051_up

xecve(“./8051_up”, [“./8051_up”], 0x3ffff18bd0 /* 18 vars */) = 0
set_tid_address(0x3fdbf13ed8) = 402
brk(NULL) = 0x14000
brk(0x16000) = 0x16000
mmap(0x14000, 4096, PROT_NONE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x14000
openat(AT_FDCWD, “/mnt/system/lib/libwiringx.so”, O_RDONLY|O_LARGEFILE|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, “/mnt/system/usr/lib/libwiringx.so”, O_RDONLY|O_LARGEFILE|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, “/etc/ld-musl-riscv64.path”, O_RDONLY|O_LARGEFILE|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, “/lib/libwiringx.so”, O_RDONLY|O_LARGEFILE|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, “/usr/local/lib/libwiringx.so”, O_RDONLY|O_LARGEFILE|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, “/usr/lib/libwiringx.so”, O_RDONLY|O_LARGEFILE|O_CLOEXEC) = 3
fcntl(3, F_SETFD, FD_CLOEXEC) = 0
fstat(3, {st_mode=S_IFREG|0644, st_size=269136, …}) = 0
read(3, “\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0\363\0\1\0\0\0@\300\0\0\0\0\0\0”…, 960) = 960
mmap(NULL, 274432, PROT_READ|PROT_EXEC, MAP_PRIVATE, 3, 0) = 0x3fdbe40000
mmap(0x3fdbe61000, 139264, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED, 3, 0x20000) = 0x3fdbe61000
close(3) = 0
mprotect(0x3fdbe61000, 4096, PROT_READ) = 0
mprotect(0x12000, 4096, PROT_READ) = 0
openat(AT_FDCWD, “8051_boot_cfg.ini”, O_RDONLY|O_LARGEFILE) = 3
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x3fdbe3f000
read(3, “0x84080000”, 1024) = 10
read(3, “”, 1024) = 0
ioctl(1, TIOCGWINSZ, {ws_row=68, ws_col=252, ws_xpixel=2019, ws_ypixel=1032}) = 0
writev(1, [{iov_base=“8051 will boot on address 840800”…, iov_len=34}, {iov_base=“\n”, iov_len=1}], 28051 will boot on address 84080000
) = 35
faccessat(AT_FDCWD, “/lib/firmware/mars_mcu_fw.bin”, F_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, “mars_mcu_fw.bin”, O_RDONLY|O_LARGEFILE) = 4
lseek(4, 0, SEEK_END) = 7857
lseek(4, 0, SEEK_CUR) = 7857
lseek(4, 0, SEEK_SET) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x3fdbe3d000
readv(4, [{iov_base=“\2\0\t\2\1J\2\21\340u\201&\22\32O\345\202\3\2\0\6y\f\351D\0\33z\1\220”…, iov_len=7856}, {iov_base=“\341”, iov_len=1024}], 2) = 7857
close(4) = 0
writev(1, [{iov_base=“size = 7857”, iov_len=11}, {iov_base=“\n”, iov_len=1}], 2size = 7857
) = 12
openat(AT_FDCWD, “/dev/mem”, O_RDWR|O_SYNC|O_LARGEFILE) = 4
mmap(NULL, 28, PROT_READ|PROT_WRITE, MAP_SHARED, 4, 0x5025000) = 0x3fdbe3c000
munmap(0x3fdbe3c000, 28) = 0
close(4) = 0
openat(AT_FDCWD, “/dev/mem”, O_RDWR|O_SYNC|O_LARGEFILE) = 4
mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED, 4, 0x84080000) = 0x3fdbe3c000
munmap(0x3fdbe3c000, 4) = 0
close(4)
lots, we are copying the firmware to DDR RAM at 0x84080000
openat(AT_FDCWD, “/dev/mem”, O_RDWR|O_SYNC|O_LARGEFILE) = 4
mmap(NULL, 3764, PROT_READ|PROT_WRITE, MAP_SHARED, 4, 0x84081000) = 0x3fbf79c000
munmap(0x3fbf79c000, 3764) = 0
close(4) = 0
munmap(0x3fbf79d000, 8192) = 0
openat(AT_FDCWD, “/dev/mem”, O_RDWR|O_SYNC|O_LARGEFILE) = 4
mmap(NULL, 588, PROT_READ|PROT_WRITE, MAP_SHARED, 4, 0x3000000) = 0x3fbf79e000
munmap(0x3fbf79e000, 588) = 0
close(4) = 0
openat(AT_FDCWD, “/dev/mem”, O_RDWR|O_SYNC|O_LARGEFILE) = 4
mmap(NULL, 36, PROT_READ|PROT_WRITE, MAP_SHARED, 4, 0x5025000) = 0x3fbf79e000
munmap(0x3fbf79e000, 36) = 0
close(4) = 0
openat(AT_FDCWD, “/dev/mem”, O_RDWR|O_SYNC|O_LARGEFILE) = 4
mmap(NULL, 28, PROT_READ|PROT_WRITE, MAP_SHARED, 4, 0x5025000) = 0x3fbf79e000
munmap(0x3fbf79e000, 28) = 0
close(4) = 0
writev(1, [{iov_base=“done”, iov_len=4}, {iov_base=“\n”, iov_len=1}], 2done
) = 5
exit_group(0) = ?
+++ exited with 0 +++

What is happening?
Strace is a utility that logs the linux syscalls the program is using as well as their arguments, we can see that as expected, milkv’s utility writes to /dev/mem
We can see it loads dynamic libraries as well as other stuffs, but what interests us is the moment where it writes to /dev/mem

openat(AT_FDCWD, "/dev/mem", O_RDWR|O_SYNC|O_LARGEFILE) = 4 mmap(NULL, 28, PROT_READ|PROT_WRITE, MAP_SHARED, 4, 0x5025000) = 0x3fdbe3c000 munmap(0x3fdbe3c000, 28) = 0 close(4) = 0

We open file /dev/mem, getting file descriptor with number 4
with memory map (we want to access it via addresses and not as a file) at address 0x3fdbe3c000
Between mmap and munmap, writing and reading operations happen, they are not loggued as they do not go through syscalls, but direct memory access.
munmap removes the memory mapping
and close, closes the file and makes the file descriptor invalid and available for further use.
0x5025000 corresponds to RTCSYS_CTRL, which is where the control registers for the 8051 are located.
I wanted to make sure i was doing the right things, so i acquired what is written in RTCSYS_CTRL with ghidra before the firmware is copied.

devmem_writel(0x5025018,0xffffffff8107fffd);

LSBs are rtcsys_rst_ctrl
It sets all reset bits to 1, but the MCU one, which corresponds to the instructions of disabling the MCU, but also bit 24 and 31, we do not know what those do.
MSBs writes into a hole. Are the registers 64-bits aligned in a 32-bits address space?

mmap(NULL, 588, PROT_READ|PROT_WRITE, MAP_SHARED, 4, 0x3000000) = 0x3fbf79e000 munmap(0x3fbf79e000, 588) = 0 close(4) = 0 devmem_writel(0x3000248,1); 'top_misc', undocumented in datasheet. google -> [PATCH v3 1/2] dt-bindings: clock: sophgo: Add top misc controller of CV18XX/SG200X series SoC

Sadly this is not one of the registers documented within the code submitted to the linux kernel, but given the example code we got (blinky) and since it’s the only one i cannot identify, this might be the security lock for memory access we can check at 0x0502502c (rtcsys_status)

[root@milkv-duo]~/firmware_8051# devmem 0x3000248 32 0 [root@milkv-duo]~/firmware_8051# devmem 0x0502502c 32 0x00000000 [root@milkv-duo]~/firmware_8051# devmem 0x3000248 32 1 [root@milkv-duo]~/firmware_8051# devmem 0x0502502c 32 0x00000001

It is ! This address (0x3000248) enables us to switch the 8051 access to the rest of the SoC instead of being locked within the RTC domain (0x05000000+16MB)
This is necessary to write a blinky, as the led gpio controller is out of that domain.

mmap(NULL, 36, PROT_READ|PROT_WRITE, MAP_SHARED, 4, 0x5025000) = 0x3fbf79e000 devmem_writel(0x5025020,0xffffffff83f80084);

RTCSYS_CTRL, rtcsys_mcu51_ctrl0.
MSBs write into a hole
LSBs set mars define memory scheme, 4 bits IROM address size (mars scheme, 8k - 1), irom address in SoC space is 0x83f80000, otherwise, defaults.

mmap(NULL, 28, PROT_READ|PROT_WRITE, MAP_SHARED, 4, 0x5025000) = 0x3fbf79e000 devmem_writel(0x5025018,0xffffffff8107ffff); MSBs write into a hole. LSBs unresets the MCU like we did before copying the data.

So this is how the example work.
It also appears I’m a idiot and the source code for 8051_up was available, you can check the not quite same values there (more on that later).

Now we will also be doing it, but slightly differently:

We will use the GPIOC/GPIO2 peripheral to blink the led without remote control.

The way to access the SoC memory from the 8051 is using the robot, which uses extended/SFR 8051 registers.
MilkV provides code to use it, I have commented it:

void write_robot(uint32_t addr, uint32_t value) __reentrant
{
    // EA stands for interrupt enable. it is a register at address 0xaf
	char ea;
    ea = EA;
    // disable interrupts
	EA = 0;

	// AS_BYTES #define AS_BYTES(x) ((uint8_t *)&(x))
	// cast x's address to pointer to a 8 bit size value, x is a 32bits value here, so the pointer is technically a table of size 4.
    // 'adr'x is the range from 0xF4 to 0xF7, addresses where it will be written by robot, in order, as 4, 8 bits registers, making a 32 bits address
    r51_adr0 = AS_BYTES(addr)[0];
    r51_adr1 = AS_BYTES(addr)[1];
    r51_adr2 = AS_BYTES(addr)[2];
    r51_adr3 = AS_BYTES(addr)[3];

    // 'wd'x is the range from 0xFB to 0xF8, value that will be written by robot at previously defined adrx, in order, as 4, 8 bits registers, making a 32 bits value
    r51_wd0 = AS_BYTES(value)[0];
    r51_wd1 = AS_BYTES(value)[1];
    r51_wd2 = AS_BYTES(value)[2];
    r51_wd3 = AS_BYTES(value)[3];

    // 1 bit at 0xF3, bit 0, tells the robot we are writing.
    // 2 bits at 0xF3, bits 1 and 2, sets the lengths in 32 bits elements, here we write 1
    r51_we = 1 | (2 << 1);
    // tell robot to do it
    r51_fire = 1;

    // wait for robot to say it did it
    while(r51_fire == 1)
        ;
    // restore interrupt enabled status from when the function was called.
    EA = ea;
}

Note there is a version for big endian, it might be relevant if you use the keil compiler instead of SDCC as RISCV is little endian and 8051 ‘can be’ either, but SDCC uses little-endian and this is
what milkv and this tutorial use.

for future reference, all mmio_write_32, mmio_read_32, mmio_write_64, etc, are aliased write_robot and read_robot

#define mmio_write_32 write_robot
#define mmio_read_32 read_robot
#define write_robot_irq write_robot
#define read_robot_irq read_robot
#define readl read_robot
#define writel write_robot

Reading is the same in broad lines, but we dont set values and set we’s bit 0 to 0, resulting in the data being written to 'rd’x range 0xE4 to 0xE7.

MilkV has provided us with a ready to use folder, but which as of 24/03/2024 doesnt work in AHB SRAM mode, because it setups things wrong (notice the lack of disabling external rom in the previously
looked at code? this will not work on CV18xx according to milkv, but then it also doesn’t set the xdata values to the sram, resulting in a not working setup), it is also slightly different from the
source code published (which should work with SRAM), so you should rebuild it if you want to use it.

# in milkv's duo-8051 git clone cd duo-8051/sdcc/mars/ cp -r project ./blinky cd blinky/base_project

main.c in src:

#include "cvi1822.h"
#include "chip_cv1822.h"
#include "cvi_gpio.h"
// delay function using cycles, proper low-power way would use timers.
void delay_us(uint8_t time)
{
	uint8_t i=0;
	while(time--) {
		i=10;
		while(i--) ;
	}
}
void delay_ms(uint8_t time)
{
	while(time--) {
		delay_us(1000);
	}
}
void main(void)
{
	// for debug purpose, if RTC_INFO0 (0x0502601c) is not this value, problem is happen.
	mmio_write_32(RTC_INFO0, 0x8051);
	while(1) {
		delay_ms(500);
		 // we saw this with devmem in the second tutorial, this is exactly the same operations, but through the robot
		set_gpio_direction(GPIO2_BASE,24,GPIO_DIRECTION_OUT);
		set_gpio_level(GPIO2_BASE,24,GPIO_LEVEL_HIGH);
		delay_ms(500);
		set_gpio_direction(GPIO2_BASE,24,GPIO_DIRECTION_OUT);
		set_gpio_level(GPIO2_BASE,24,GPIO_LEVEL_LOW);
	}
}

We have SDCC but no make on the duo following the first tutorial, we can use the magic of py-make because we have a full python.

copy files somewhere.
no-cache-dir bc 64M ram, if it still doesnt work (‘Killed’), enable swap or retrieve a riscv64 make

python3 -m pip install py-make-0.1.2.tar.gz --no-cache-dir

Otherwise, build make like in tutorial 2, I did it because I got tired of waiting for the python package to install (if you’re on a Duo 256 or Duo S you wont have to wait much)

First we modify the makefile to not use milkv’s X86_64 binaries, surprisingly this ‘worked’ on my duo with unsavory results:

CC  := $(BUILD_DIR)/../../../../tools/sdcc/bin/sdcc
AS  := $(BUILD_DIR)/../../../../tools/sdcc/bin/sdas8051
LD  := $(BUILD_DIR)/../../../../tools/sdcc/bin/sdld
SDOBJCOPY := $(BUILD_DIR)/../../../../tools/sdcc/bin/sdobjcopy

to:

CC  := sdcc
AS  := sdas8051
LD  := sdld
SDOBJCOPY := sdobjcopy

if they are in your PATH.

We build:

make

This will error. We need to make changes:

CFLAGS = -I./include $(IINC_DIR) -mmcs51 --model-$(MODEL) $(EXTRA_CFLAG) --out-fmt-ihx $(DEBUG)

to

CFLAGS = -I/usr/local/share/sdcc/include/ -I./include $(IINC_DIR) -mmcs51 --model-$(MODEL) $(EXTRA_CFLAG) --out-fmt-ihx $(DEBUG)

The file giving __sdcc_external_startup undefined warning also serves no purpose, it’s a copy of SDCC’s with 3 additional commented lines and a missing character and breaks everything (or that’s a upstream issue)

rm src/crtstart.asm

in src/src.mk

asm_srcs +=
$(SRC_PATH)/crtstart.asm

to

asm_srcs += \ /usr/local/share/sdcc/lib/src/mcs51/crtstart.asm

or change __sdcc_external_startup to ___sdcc_external_startup in crtstart.asm

Finally, there is another syntax error in dw_ictl.h

void dw_ictl_intr_handler(void) __interrupt 0;

to

void dw_ictl_intr_handler(void) __interrupt(0);

Finally, it should work:

devmem 0x05025020 32 This returned 0x0520000C for me, which means it defaulted to ahb SRAM. We need to set the LSBs to 84 so it is in mars mode. devmem 0x05025020 32 0x05200084

Then we follow the ‘short’ procedure for restarting the 8051 (the datasheet indicates additional steps):

Reset:

devmem 0x05025018 32

0x001FFFFD

devmem 0x05025018 32 0x001FFFFC

Then, we write our program using this custom script (that only writes the program to memory) i made:

python to_devmem.py 0x05200000 out/mars_mcu_fw.bin

Disable access security limits so we can write to the GPIO device:

devmem 0x0502502c 32

0x0

devmem 0x3000248 32 0x1


devmem 0x0502502c 32

0x1

And disable 8051 software reset status:

devmem 0x05025018 32 0x001FFFFF

It should work.
We can check the INFO0 address:

devmem 0x0502601c 32

→ 0x00008051

Files: https://vyndragon.github.io/data/Blinky.tar.gz
Additional, more up to date, sg2000 datasheet: https://vyndragon.github.io/data/sg2000.pdf

3 Likes