Skip to main content

The 8051 “MCU” on the SG2000

Assessment date: 2026-10-13 Boards: Milk-V Duo S (SG2000), Pine64 Oz64 (SG2000) Status: Reasoned from source — not board-tested. Everything below is taken from the SG2000 TRM (Chapter 14 “8051 Subsystem”, Chapter 5 “Power Management”, Chapter 6 “Real Time Clock”), the official Sophgo 8051_Core_Quick_Start_Guide, the Duo S / Oz64 schematics, and the NatTuck/sophgo-sg200x-debian image tree. No custom 8051 firmware has been built or loaded on hardware yet.

This is the third core, and it is not either of the two C906s the other hints target. If you arrived here from fishwaldo-arduino.md, note that the little RISC-V core (cv181x-c906_1) and the 8051 are completely different pieces of silicon.

Summary

The SG2000 is a quad-core chip: 2 x C906, 1 x Arm Cortex A53, 1 x 8051 (TRM §2.1). The 8051 is a small, always-on microcontroller that lives inside the RTC subsystem, on a power rail that stays up when the rest of the SoC is asleep. Its designed job is to run while the main system is off, decide when to wake it, and (optionally) talk to external chips over the RTC-domain peripherals.

For our images, three things matter:

  1. It is not in the boot chain. The FIP carries BL2, OpenSBI, U-Boot and the C906L coprocessor images — no 8051 component. Nothing on the normal boot path needs it.
  2. Our images ship no 8051 firmware and hold the core in reset. The images built by sophgo-sg200x-debian contain no mars_mcu_fw.bin, no 8051_up, no DDR carveout for it, and never release reg_soft_rstn_mcu. The 8051 is inert.
  3. It is optional, and its value is narrow. The always-on power-management machinery (the RTC power state machine, wake sources, rail sequencing) is hardware and is already driven by Linux via the cvitek,rtc driver. The 8051 only adds something in the window where the AP is powered off and you want programmable behaviour during that window.

Where it lives, and the “MCU” naming trap

The 8051 subsystem is inside RTCSYS, in the No-die (always-on) domain (TRM §14.1, §5.1.1.2). RTCSYS has two sub-domains:

  • AO (Always-On) domain — RTC, WDT, interrupt controller, GPIO, SARADC, IRRX.
  • MCU domain (VDDC_SW_MCU) — the 8051 + 8 KB AHB SRAM, 2x 32-bit counters, SPI-NOR control, 2x mailbox, I2C, UART. This domain can be powered off and on independently.

The chip has a low-power mode called “MCU-Only (32k-less) mode” (§5.1.2) in which only the No-die domain runs: “a small MCU system with its own clock, timer, uart, i2c, gpio, adc … can replace the SoC’s external MCU. The MCU in the No-die Domain can wake up the main system and return to Active Mode after receiving and filtering external input.” Idle current in that state is ~200 µA.

Naming trap. The word “MCU” means two different cores in the vendor ecosystem:

Context “MCU” means Evidence
SG2000 TRM the 8051 §5.1.1.2 “An 8051 microcontroller (MCU)”, the “MCU domain”, “MCU-Only mode”
Vendor SDK / Linux the little C906 (cv181x-c906_1) firmware c906-mcu.elf, the FSBL’s MCU_STATUS_* enum, fast_image_mcu

When the TRM says MCU it means the 8051. When the SDK says mcu it almost always means the C906L.

What it can do

Core (TRM §14.2):

Feature Value
ISA standard 8051 (8-bit)
Frequency 25 MHz – 300 MHz
Data access 32-bit data access supported
Code size code banking, max 64 x 64 KB (4 MB)
Boot source reset vector configurable to AHB SRAM / DRAM / SPINOR
Low power WFI (clock gating)
Debug single-step, jump2pc, snapshot of PSW / DPTR / PC

Peripherals it can drive (TRM §5.1.1.2, §14.1, quick-start guide):

  • Its own: 3x 16-bit timers, 1 UART (muxed to PWR_UART_RX/TX).
  • RTC/No-die domain: I2C, SPI-NOR, SD, WDT, GPIO, SARADC, IRRX.
  • When the AP is powered, the whole 0x0–0xFFFF_FFFF AP peripheral space — but only AP interrupts routed through the “aptortc” table can reach it.

Interrupts (TRM Table 14.1):

# Source # Source
0 Vbat_det (power-down) 8 timer_int1
1 mbox_int0 (mailbox) 9 irq_ap2rtc[0]
2 reserved 10 irq_ap2rtc[1]
3 irrx 11 i2c_int
4 gpio_int (PWR GPIO) 12 st_change_int (RTC status)
5 uart_int (PWR UART) 13 hw_thm_shdn (overheat)
6 spinor1_int 14 saradc
7 timer_int0 15 wdt_int

Inter-core comms: 2x Mailbox (ACPU <-> 8051), each with 2 spinlock fields and 4x 32-bit info fields (RTCSYS_MBOX @ 0x0502_4000, ap_mailbox @ 0x0190_0000). External interrupts arrive on int0_n/int1_n; the AP side arrives as irq_ap2rtc[0]/[1].

Power management: mechanism vs. policy

This is the part that is easy to get wrong. “Power management” on this chip is mostly internal — it is about the SoC’s own power state and the board’s power rails, not about peripheral chatter.

  • Mechanism = the RTC block (RTC_CORE @ 0x0502_6000, RTC_CTRL @ 0x0502_5000). A pure-hardware, always-on power state machine plus counters and comparators. Its state is readable as rtc_fsm_st (RTC_CTRL_STATUS0 bits 29:26): ST_ON, ST_OFF, ST_SUSP, ST_PWR_CYC, ST_WARM_RESET.
  • Policy = whoever programs it. On our images that is Linux (see below). The 8051 is an alternative policy engine — a software-defined power manager the vendor can re-flash per product without new silicon.

What the 8051-as-policy-engine would do:

  • Drive the power-state requests in RTC_CTRL0 (0x0502_5008, write-protected; unlock by writing 0xAB18 to RTC_CTRL0_UNLOCKKEY @ 0x0502_5004): req_suspend (bit 7), req_shdn (bit 0), req_pwr_cyc (bit 3), req_warm_rst (bit 4), req_sw_wdg_rst (bit 5) / hw_wdg_rst_en (bit 6), req_sw_thm_shdn (bit 1) / hw_thm_shdn_en (bit 2). Each is gated by the matching RTC_EN_*_REQ in RTC_CORE (e.g. RTC_EN_SUSPEND_REQ @ 0x0502_60E4, RTC_EN_PWR_WAKEUP @ 0x0502_60BC).
  • Select and filter wake sources: RTC_EN_PWR_WAKEUP is a bitmask ([0] = PWR_WAKEUP0, …); the block has per-source debounce (RTC_DB_PWR_ON, RTC_DB_BUTTON1, RTC_DB_PWR_VBAT_DET) and a 7-second long-press reset (RTC_EN_7SEC_RST).
  • Run an RTC alarm / periodic wake (RTC_ALARM_TIME, RTC_ALARM_ENABLE, seconds counter RTC_SEC_CNTR_VALUE that survives power-off).
  • Monitor internally, with no external bus: battery/low-voltage (RTC_PWR_DET_COMP/RTC_PWR_DET_SEL), the No-die SARADC (0x0502_C000, reads PWR_GPIO1/PWR_GPIO2/PWR_VBAT_DET), thermal shutdown (hw_thm_shdn), watchdog, IRRX, PWR GPIO.
  • Set the rail-sequencing timings (RTC_UP_SEQ0..3, RTC_DN_SEQ0..3, RTC_UP_IF_EN, RTC_UP_RSTN) that drive the board’s PWR_SEQ0..3 outputs.
  • Manage the 32 kHz clock (reg_rtc_mode, reg_clk32k_cg_en) and its calibration (RTC_FC_*), and the keep-alive config (reg_rtcsys_rstn_src_sel must be 1 so the RTC subsystem survives power-down; RTC_EN_AUTO_POWER_UP).

The pins this needs are the PWR_* nets — PWR_SEQ1/2/3 to the regulators, PWR_BUTTON1/PWR_ON/PWR_WAKEUP0/1 to the buttons, PWR_VBAT_DET to the battery divider, GPIO_ZQ to the ZQ resistor. Those live on the board’s power-management net, not the user header.

How you program it

Toolchain: SDCC (Small Device C Compiler, 8051 target) — not GCC/LLVM. The vendor toolkit ships an sdcc/ tree with a project called “mars”:

cd sdcc/mars/project/base_project
make clean && make          # -> output/mars_mcu_fw.bin

The prebuilt firmware and the loader live in a separate toolkit download (firmware.zip contains the firmware + 8051_up); they are not in the public SDK git and not in our images.

Memory model. The 8051 has a 16-bit address / 8-bit data bus. Two bridges map it onto the SoC:

  • SFR -> AHB bridge: extended SFRs (reg_51robot_*) let firmware read/write AP registers (the SDK’s robot_read/robot_write).
  • XDATA bridge: maps “external ROM” and “external RAM” onto 32-bit on-chip spaces (AHB SRAM 0x0520_0000, TPU SRAM 0x0C00_0000, DDR 0x8000_0000, SPINOR).

Boot / TCM mapping (quick-start guide, RTC_CTRL):

  • irom first address = 0x0520_0000 + reg_51irom_ioffset[4:0] * 2 KB (reg_51irom_ioffset is in rtcsys_mcu51_ctrl1 @ 0x0502_5024).
  • The core boots from irom only when reg_51_mem_ea_n = 1 (“external ROM does not exist”); otherwise it boots from external ROM.
  • reg_51_rom_addr_def (bit 7 of rtcsys_mcu51_ctrl0 @ 0x0502_5020) must be 1 for SG200x (“mars” definition, 2 KB granularity).
  • reg_51xdata_ioffset0 / reg_51xdata_doffset0 (bits 31:11 of the two ctrl registers) place external ROM / RAM; first address = field x 2 KB.

Bring-up sequence (TRM §14.3.2):

  1. Hold in reset: reg_soft_rstn_mcu = 0 (rtcsys_rst_ctrl @ 0x0502_5018, bit 1; reset default is 0).
  2. Configure reg_51_rom_addr_size (instruction TCM size).
  3. Configure reg_51irom_ioffset (where in AHB SRAM it executes).
  4. Release reset: reg_soft_rstn_mcu = 1.

Powering the MCU domain (§14.3.1):

  • off: soft reset = 0; reg_mcu_iso_en = 1 (rtc_ip_iso_ctrl @ 0x0502_5084, bit 1); reg_mcu_pwr_req = 0 (rtc_ip_pwr_req @ 0x0502_5080, bit 2).
  • on: reg_mcu_pwr_req = 1; poll reg_mcu_pwr_ack = 1 (bit 18); reg_mcu_iso_en = 0; soft reset = 1.

Loading the firmware. Two vendor paths, both run from Linux at runtime:

  • Into AHB SRAM (<= 8 KB). Copy 8051_up, mars_mcu_fw.bin, blink.sh to the board, run 8051_up, then blink.sh (which messages the core over the mailbox to blink an LED).
  • Into DDR (> 8 KB). Add CHIP8051_SIZE/CHIP8051_ADDR to the SDK’s memmap.py, push the C906L FreeRTOS block down, rebuild and reflash; then run 8051_up with an 8051_boot_cfg.ini. SG2000 DDR formula: 0x8000_0000 + 512 MiB - size.

Status on our images

The images built by NatTuck/sophgo-sg200x-debian contain no 8051 firmware and leave the core held in reset. Evidence:

  1. The FIP has no 8051 component. The FSBL is cloned from sophgo-fsbl (licheervnano-cvisdk, commit 1e73867) and built with BLCP_2ND_PATH=.../blank.bin and LOADER_2ND_PATH=.../u-boot.bin (configs/chip/sg200x/chip.mk). Its fip.mk/fiptool.py pack the FIP from exactly: BL2, MONITOR (OpenSBI), LOADER_2ND (U-Boot), BLCP (fast_image_mcu or test/empty.bin), BLCP_2ND. There is no MCU entry.
  2. The coprocessor slot is deliberately blanked: @printf '\163\000\120\020\157\360\337\377' > .../fsbl/blank.bin (an 8-byte stub) is used as BLCP_2ND_PATH.
  3. No DDR carveout. configs/duos/memmap.py and configs/oz64/memmap.py reserve DRAM for FreeRTOS, OpenSBI, ION, boot logo, U-Boot and the kernel — there is no CHIP8051_SIZE/CHIP8051_ADDR, which is exactly what the vendor guide says you must add to run the 8051 from DDR.
  4. No firmware in the rootfs/packages. The only firmware shipped is the AIC8800 WiFi firmware, the vcodec firmware (coda980.bin, monet.bin) and two camera ISP params. No mars_mcu_fw.bin, no 8051_up, no SDCC project.
  5. The only “8051” strings are descriptive — notes/oz64.md (SoC blurb) and a commented-out transfer_config_s.mcu_status = MCU_STATUS_NONOS_DONE in an FSBL patch (and that is the C906L, not the 8051).

Consequence: the 8051 never comes out of reset, and there is no Linux driver for it. Power management on these images is Linux-driven: the kernel carries a cvitek,rtc driver (configs/chip/sg200x/patches/linux/0007-…) with read_alarm/set_alarm/alarm_irq_enable, suspend/resume, shutdown, and device_init_wakeup(&pdev->dev, 1); and a SARADC driver (0008-…) that maps the RTC-domain ADC (0x0502_C000) as well as the top-domain one. So the ACPU already drives RTC-domain hardware directly — the 8051 is not needed for any of it.

If we wanted to add custom 8051 firmware

Could we? Yes — and the runtime route needs no image rebuild. The 8051’s instruction memory is plain on-chip SRAM the ACPU can write, so you can load firmware from Linux exactly the way the vendor’s 8051_up tool does:

  1. Hold in reset (reg_soft_rstn_mcu = 0).
  2. Write the firmware bytes into 0x0520_0000 (the AHB SRAM) via /dev/mem.
  3. Configure rtcsys_mcu51_ctrl0/1: reg_51_rom_addr_def = 1, reg_51_mem_ea_n = 1 (boot from irom), reg_51irom_ioffset = 0 (or the offset you wrote to), reg_51_rom_addr_size as needed.
  4. Release reset (reg_soft_rstn_mcu = 1).

A reasoned sketch of a userspace loader (untested):

import mmap, os, struct

SRAM     = 0x05200000          # RTCSYS AHB SRAM (irom)
RTC_CTRL = 0x05025000

fw = open("mars_mcu_fw.bin", "rb").read()
assert len(fw) <= 8192, "SRAM method: firmware must be <= 8 KB"

f    = os.open("/dev/mem", os.O_RDWR | os.O_SYNC)
sram = mmap.mmap(f, 0x2000, mmap.MAP_SHARED, mmap.PROT_READ | mmap.PROT_WRITE, offset=SRAM)
ctrl = mmap.mmap(f, 0x1000, mmap.MAP_SHARED, mmap.PROT_READ | mmap.PROT_WRITE, offset=RTC_CTRL)

def rd(off):
    ctrl.seek(off); return struct.unpack("<I", ctrl.read(4))[0]
def wr(off, v):
    ctrl.seek(off); ctrl.write(struct.pack("<I", v))

# 1. hold in reset
wr(0x18, rd(0x18) & ~(1 << 1))                 # rtcsys_rst_ctrl: reg_soft_rstn_mcu = 0
# 2. copy firmware into irom
sram.seek(0); sram.write(fw)
# 3. configure: mars define, boot from irom, TCM size, irom offset 0
c0 = rd(0x20)
c0 = (c0 & ~0x7F) | 0xC | (1 << 7) | (1 << 5)  # size=0xC, rom_addr_def=1, mem_ea_n=1
wr(0x20, c0)
wr(0x24, (rd(0x24) & ~0x1F) | 0x0)             # reg_51irom_ioffset = 0
# 4. release reset
wr(0x18, rd(0x18) | (1 << 1))

The exact control-field values should be matched against the vendor’s 8051_up/mars defaults before trusting this; treat the numbers above as reasoned, not verified.

To make it persistent, add a systemd unit that runs the loader at boot — the same pattern the repo already uses for duo-pinmux / oled-setup.

Other routes:

  • > 8 KB firmware (run from DDR): add CHIP8051_SIZE/CHIP8051_ADDR to configs/<board>/memmap.py and rebuild (vendor “method II”).
  • Before Linux (in the FIP): not available — the FIP format has no MCU entry and the FSBL never touches the 8051. You would have to patch the FSBL.

Caveats:

  • RTC_CTRL is shared with the Linux cvitek,rtc driver (it maps 0x0502_5000). Coordinate the offsets you touch (the driver uses the alarm / wake-source registers; the 8051 config lives at +0x018/+0x020/+0x024).
  • The core is currently parked; releasing reset while the rest of the system assumes it is parked is worth testing carefully.
  • The PWR_* pads are board power-management nets, so there is no easy way to attach an external sensor.

What it would let us do that we can’t do now

Start from the current state: Linux owns everything, and when the AP is off the only thing running is the fixed hardware RTC state machine — it can wake on an RTC alarm, a button, or VBAT detect, and that is all. No code executes during sleep.

The 8051 adds a programmable agent that runs while the AP is off:

  • Custom wake policy, not just “alarm at time T” — e.g. wake every minute, read the internal SARADC, and power the AP up only if a voltage crosses a threshold; or distinguish short / long / double press of a wake input.
  • Autonomous use of the always-on peripherals — RTC SARADC, IRRX, RTC GPIO, RTC I2C/UART — during sleep, which Linux can only use while awake.
  • Drive the RTC power state machine itself as a stateful supervisor (suspend / power-cycle / wake).
  • Faster, deterministic reaction to wake events (its own clock, no Linux wake latency).
  • A mailbox co-processor for the ACPU.

Honest limits:

  • The 8051’s usable pins are the PWR_* power-management nets, not header pins (the RTC I2C is on PWR_GPIO1/PWR_GPIO2; the RTC UART on PWR_GPIO0/PWR_WAKEUP0). You cannot readily hang an external sensor on it.
  • It is an 8-bit 8051 with ~8 KB of code (or a DDR carveout).
  • It overlaps Linux, which already has RTC-alarm wake and a SARADC driver. The 8051’s value is only in the window where the AP is off.

So the realistic win is a programmable always-on supervisor: autonomous low-power monitoring + custom wake logic + power-state control, running with the AP powered down. For an always-on Linux SBC — which is what our images are — that is a narrow benefit. For a course, the bigger payoff is pedagogical: a third ISA (8051 via SDCC) on the same die, in the always-on domain, loadable at runtime from Linux — a clean parallel to the C906L/remoteproc lab.

Reference: RTC-domain addresses

Block Base
RTCSYS_Timer 0x0502_0000
RTCSYS_GPIO 0x0502_1000
RTCSYS_UART 0x0502_2000
RTCSYS_INTR 0x0502_3000
RTCSYS_MBOX 0x0502_4000
RTCSYS_CTRL 0x0502_5000
RTCSYS_CORE (RTC) 0x0502_6000
RTCSYS_IO 0x0502_7000
RTCSYS_WDT 0x0502_D000
RTCSYS_I2C 0x0502_B000
RTCSYS_SARADC 0x0502_C000
RTCSYS_IRRX 0x0502_E000
RTCSYS_SRAM (8051 irom) 0x0520_0000 (8 KB)
ap_mailbox 0x0190_0000

RTC_CTRL @ 0x0502_5000 registers used here:

Offset Name Notes
+0x004 RTC_CTRL0_UNLOCKKEY write 0xAB18 to unlock RTC_CTRL0
+0x008 RTC_CTRL0 req_suspend(7) req_shdn(0) req_pwr_cyc(3) req_warm_rst(4) req_sw_wdg_rst(5) hw_wdg_rst_en(6) req_sw_thm_shdn(1) hw_thm_shdn_en(2) reg_rtc_mode(10) reg_clk32k_cg_en(11)
+0x00c RTC_CTRL_STATUS0 rtc_fsm_st[29:26] rtc_alarm_o(21) rtc_pwr_button1_o(2) rtc_pwr_on_o(4) rtc_pwr_wakeup0_o(5)
+0x018 rtcsys_rst_ctrl bit 1 reg_soft_rstn_mcu (reset 0)
+0x020 rtcsys_mcu51_ctrl0 reg_51_rom_addr_size[4:0] reg_51_mem_ea_n(5) reg_51_xdata_mode(6) reg_51_rom_addr_def(7) reg_51xdata_ioffset0[31:11]
+0x024 rtcsys_mcu51_ctrl1 reg_51irom_ioffset[4:0] reg_51_pf_mode[9:6] reg_51xdata_doffset0[31:11]
+0x05c rtcsys_mcu51_dbg 8051 debug
+0x07c rtcsys_mcu51_ictrl1 reg_51_int1_src_mask
+0x080 rtc_ip_pwr_req reg_mcu_pwr_req(2) reg_mcu_pwr_ack(18)
+0x084 rtc_ip_iso_ctrl reg_mcu_iso_en(1)
+0x098 rtcsys_sram_ctrl reg_mcu_sram_slp/sd
+0x09c rtcsys_io_ctrl reg_i2c_mux_opt0(0): PWR_IIC on PWR_GPIO6/8

Sources

  • pdfs/sg2000_trm_en.pdf — Chapter 14 “8051 Subsystem” (§14.1–14.5), §5.1 (power domains / MCU-Only mode), §6.6.3.6/.8/.9/.28/.29 (RTC control registers), §21.1 (I2C controllers), §21.5 (GPIO), Chapter 10 (pinmux).
  • Sophgo sophgo-doc: SG200X/8051/8051_Core_Quick_Start_Guide_en.md, SG200X/TRM/contents/en/mcu/*, and the d8051_db.pdf datasheet.
  • Schematics: duo_s_SCH_v1.1.pdf (Milk-V), PINE64_Oz64_Schematic-20240517.pdf (Pine64).
  • NatTuck/sophgo-sg200x-debian — configs/chip/sg200x/chip.mk, configs/{duos,oz64}/memmap.py, configs/chip/sg200x/patches/{fsbl,linux}/.

Open questions

  • Does a stock (vendor) Duo S / Oz64 image run any 8051 firmware? The public SDK and the FIP do not carry one, but the FSBL references MCU_FLAG 0x8051 and a warm-boot handoff through RTC SRAM, so the vendor suspend/resume flow may involve it. Not confirmed either way.
  • Can the 8051, in MCU-Only mode, request the RTC state machine to power the AP up directly (via RTC_CTRL0), or does it rely on a wake line / mailbox? The TRM says it “can wake up the main system”; the mechanism is not spelled out.
  • Exact rtcsys_mcu51_ctrl0/1 values the vendor 8051_up uses for the SG2000 (to validate the loader sketch above).
  • Whether releasing the 8051 reset interacts with the Linux cvitek,rtc driver’s register writes.