Clocks, Power, DMA, and the Engines on the SG2000
2026-10-14
The CPU is a small part of the die. This deck is about the machine around it.
Framing
Mechanisms
Context
The SG2000 is 2 x C906 + 1 x A53 + 1 x 8051 — plus a pile of engines:
| Engine | Job | In our images |
|---|---|---|
| TPU | 0.5 TOPS INT8 neural net | yes — lab later |
| VPU | H.264/H.265 encode+decode, JPEG | yes (firmware-vcodec) |
| ISP / VI | camera pipeline | yes — later deck |
| VPSS / IVE / DWA / RGN | scale, crop, dewarp, OSD | yes (cvitek-osdrv) |
| SDMA | 8-channel data mover | yes |
| USB / Eth / I2S | I/O | yes |
| RTC + MCU domain | the always-on island | yes (idle) |
Three of the four CPUs are ours. The engines are mostly dataflow hardware — you point them at buffers, and DMA feeds them.
On a modern SoC the CPU is a few percent of the transistors. Everything else is:
The architectural questions this raises:
1. How is it clocked?
One 25 MHz crystal in; eight PLLs, a mux tree, and dozens of dividers out. Every block runs at a frequency someone chose.
2. How is it powered?
Two power domains, a three-level reset hierarchy, and a piece of silicon that stays awake when everything else is off.
3. How does data move without the CPU?
Programmed I/O and interrupts both cost the CPU a word at a time. The DMA engine doesn’t.
Two engines get their own labs, so today they are one-liners:
Everything else in the deck we will look at for real:
The chip has exactly two clock inputs:
| Input | Frequency | Feeds |
|---|---|---|
XTAL_XIN |
25 MHz crystal | every PLL, and clk_xtal_* |
RTC_XIN |
32.768 kHz crystal | the always-on RTC |
Everything else is derived. Eight PLLs multiply the 25 MHz up to hundreds of MHz (TRM Table 8.1):
| PLL | Preset | Type | PLL | Preset | Type |
|---|---|---|---|---|---|
FPLL |
1500 MHz | int | APLL |
1050 MHz | int/frac |
MPLL |
1000 MHz | int/frac | CAM0PLL |
1050 MHz | int/frac |
TPLL |
1400 MHz | int/frac | CAM1PLL |
1025 MHz | int/frac |
MIPIMPLL |
900 MHz | int | DISPPLL |
1200 MHz | int/frac |
25 MHz XTAL ─┬─────────────────────────────────────────────► clk_xtal_*
│
├─► FPLL 1500 ─┬─ /2 ─► clk_a53 750 MHz
│ ├─ /3 ─► clk_cpu_axi0 500 MHz
│ ├─ /3 ─► clk_tpu 500 MHz
│ ├─ /5 ─► clk_cpu_gic 300 MHz
│ └─ /5 ─► clk_axi4 300 MHz
│ └─ /3 ─► clk_axi6 100 MHz
├─► TPLL/APLL/MIPIMPLL/CAMxPLL/DISPPLL ─► (media, audio, cam, display)
└─► … dozens of leaf clocks, each with a divider
32.768 kHz RTC_XIN ───────────────────────────────────────► RTCRead it right-to-left: every block names a parent and a divide. The leaf names are in the DT (clocks = <&clk CV181X_CLK_…>).
An integer PLL is VCO = REF x Div_sel / Pre_div_sel, FOUT = VCO / Post_div_sel (TRM Table 8.2):
| Parameter | Range | Constraint |
|---|---|---|
PLL_REF |
25–2500 MHz | input |
PLL_VCO |
800–2500 MHz | the VCO must land in band |
Pre_div_sel / Div_sel / Post_div_sel |
1–127 / 6–127 / 1–127 | multipliers |
Ictrl, Mode |
0–7, 0–3 | loop filter; must satisfy 0.2 < 1.84(1+Mode)(1+Ictrl)/2/Div_sel ≤ 0.35 |
Tuning an integer PLL (TRM §8.4.1):
*_pll_csr.*_pll_pwd.You cannot retune a PLL under a running consumer. The rule “turn off the clock before you touch the PLL” is the same discipline as the reset rule later.
Every leaf clock has three knobs (TRM §8.5):
| Knob | Register(s) | What it does |
|---|---|---|
| Gate | clk_en_0 … clk_en_4 |
turn the leaf clock on/off (one bit per clock) |
| Divide | div_clk_<name> |
set the divisor; some also pick a clk_src group (DIV_IN0/DIV_IN1) |
| Bypass | clk_byp_0 / clk_byp_1 |
route the leaf straight to XTAL |
A few clocks (the CPUs) have two divider registers — div_clk_c906_0_0 and div_clk_c906_0_1 — because they can select between two source groups.
A peripheral’s clock is part of its interface, not an implementation detail:
mmap of the DesignWare controller saw IC_STATUS = 0 until the kernel ungated it.“The hardware is memory-mapped” is necessary, not sufficient. The clock is part of the interface.
The kernel models the whole tree with the common clock framework:
compatible = "cvitek,cv181x-clk") registers every clock.clocks = <&clk CV181X_CLK_SPI>.clk_prepare_enable() / clk_disable_unprepare() gate; clk_set_rate() retunes.CONFIG_DEBUG_FS=y is set in our images, so clk_summary should exist — but verify on the board: the duos/oz64 defconfigs do not obviously enable CONFIG_CPU_FREQ or a common-clock driver by name.
Expected (per TRM Table 8.4 defaults):
| Clock | Parent / div | Rate |
|---|---|---|
clk_a53 |
fpll / 2 |
750 MHz |
clk_cpu_axi0 |
fpll / 3 |
500 MHz |
clk_tpu |
fpll / 3 |
500 MHz |
clk_axi4 |
fpll / 5 |
300 MHz |
clk_axi6 |
fpll / 15 |
100 MHz |
clk_spi |
fpll / 8 |
187.5 MHz |
clk_pwm_src |
fpll / 10 |
150 MHz |
The clock tree is a programmable structure, not a constant.
This is the first half of “how fast is the chip?” The second half is how much power it burns to go that fast — Part II.
The chip has two power domains (TRM §5.1.1):
| Domain | Supply | Contents |
|---|---|---|
| Active | the ordinary SoC rail | CPUs, fabric, most peripherals, DDR |
No-die (RTCSYS) |
VDDIO_RTC |
RTC, WDT, ictl, GPIO, SARADC, IRRX, and the MCU domain |
RTCSYS splits again into:
VDDC_SW_MCU) — the 8051 + 8 KB SRAM, timers, SPI-NOR, mailbox, I²C, UART.The MCU domain can be powered off independently. The AO domain never goes away as long as VDDIO_RTC is present.
The RTC block (RTC_CORE @ 0x0502_6000) is a hardware power state machine plus counters and comparators. It runs off 32.768 kHz and stays alive.
Its state is readable: rtc_fsm_st = RTC_CTRL_STATUS0[29:26].
The RTC is not a clock chip. It is the root power controller — the thing that decides when the rest of the chip is allowed to exist.
You don’t command the RTC; you request, and each request is gated by an enable. RTC_CTRL0 @ 0x0502_5008 (unlock via RTC_CTRL0_UNLOCKKEY @ 0x0502_5004 = 0xAB18):
| Bit | Request | Gated by |
|---|---|---|
| 7 | req_suspend |
RTC_EN_SUSPEND_REQ |
| 0 | req_shdn (power off, incl. DDR) |
RTC_EN_SHDN_REQ |
| 3 | req_pwr_cyc |
RTC_EN_PWR_CYC_REQ |
| 4 | req_warm_rst |
RTC_EN_WARM_RST_REQ |
| 5/6 | req_sw_wdg_rst / hw_wdg_rst_en |
RTC_EN_WDG_RST_REQ |
| 1/2 | req_sw_thm_shdn / hw_thm_shdn_en |
RTC_EN_THM_SHDN |
The RTC_EN_* registers live in RTC_CORE (e.g. RTC_EN_PWR_WAKEUP @ 0x0502_60BC). The enables are the safety interlock: a request does nothing until its enable is set.
The full suspend flow (TRM §6.4.6):
RTC_PG_REG = 0 — park the DDR I/O so DRAM survives power-off.RTC_EN_PWR_WAKEUP (a bitmask).req_suspend = 1.RTC_PG_REG = 1 to release the DDR I/O, then re-init DRAM.Wake sources include PWR_WAKEUP0/1, PWR_ON, PWR_BUTTON1, PWR_VBAT_DET, and the RTC alarm (RTC_ALARM_TIME / RTC_ALARM_ENABLE).
On our images this whole path is driven by Linux (
cvitek,rtc:set_alarm,alarm_irq_enable,suspend/resume,device_init_wakeup). No firmware needed.
The RTC also drives PWR_SEQ0..3 with programmable timing (RTC_UP_SEQ0..3, RTC_DN_SEQ0..3, RTC_UP_IF_EN, RTC_UP_RSTN, RTC_UP_MAX).
On the Duo S those nets go to the regulators:
| Net | Duo S use |
|---|---|
PWR_SEQ1 |
0.9 V / 1.8 V MIPI rails |
PWR_SEQ2 |
MAIN +3.3 V |
PWR_SEQ3 |
1.35 V |
So the SoC sequences the board’s rails as part of its own power-up/down — and the timing lives in RTC registers, not in the board’s firmware.
Three reset levels (TRM §7.2):
| Level | Scope | Trigger |
|---|---|---|
| L1 | system power-on reset | POR, overheat, watchdog |
| L2 | system hard reset | watchdog, external RSTN pin (6.56 ms debounce) |
| L3 | soft resets (CRG) | software |
L3 (RSTGEN @ 0x0300_3000) is what software uses:
| Register | Offset | Effect |
|---|---|---|
SOFT_RSTN_0..3 |
0x000..0x00c |
per-module soft reset (active low) |
SOFT_CPUAC_RSTN |
— | reset the processor subsystem |
SOFT_CPU_RSTN |
— | reset the processor |
Soft reset is active-low and sticky: write 0 to reset, then write 1 to release. And the rule from the DMA chapter: make sure the module is idle first.
Inside the No-die domain sits a fourth CPU: an 8051 microcontroller (TRM ch. 14).
| Feature | Value |
|---|---|
| ISA | standard 8051 (8-bit) |
| Clock | 25–300 MHz |
| Code | 8 KB AHB SRAM, or a DDR carveout; banking to 4 MB |
| Peripherals | RTC GPIO/I²C/UART/SARADC/IRRX/SPI-NOR, timers, WDT, 2× mailbox |
| Idle | ~200 µA in “MCU-Only” mode |
It exists to be the always-on supervisor: run while the AP is off, watch wake conditions, and bring the system back.
Our images hold the 8051 in reset and ship no firmware for it:
mars_mcu_fw.bin, no 8051_up, no SDCC project — not in the SDK, not in the FIP.memmap.py.PWR_* power-management nets, not header pins.The SG2000 has four CPUs. We use three. The fourth is real, always-on, and unreachable in practice — a job (software-defined power policy) that Linux already does here.
Power is a hardware mechanism plus a policy layer.
Next: the third question — who moves the data?
Three ways to get data from A to B, in increasing sophistication:
| Mechanism | Who touches each word | CPU cost |
|---|---|---|
| Programmed I/O | the CPU, in a loop | 1 load + 1 store per word |
| Interrupt-driven | the CPU, one word per IRQ | worse — plus trap overhead |
| DMA | an engine | ~0 — the CPU sets up a descriptor |
The CPU is the most general and the most expensive way to move bytes. DMA is the cheapest, because the CPU stops being in the loop.
For a memcpy this is fine. For a 1080p frame (≈ 3 MB) it is a disaster: the CPU spends milliseconds doing nothing but copying.
An interrupt tells you when a device has a word ready — it does not remove the CPU from the copy.
Interrupts are for notification, not for transfer. Using them to move bulk data is a classic performance mistake.
The CPU writes a descriptor (source, destination, length) and enables the channel. The DMAC does the rest — and can be fed by a peripheral’s request line, so the transfer happens at the device’s pace.
The CPU’s cost drops from per word to per transfer.
TRM ch. 11: a Synopsys DesignWare DMAC (snps,dmac-bm, base 0x0433_0000).
| Feature | Value |
|---|---|
| Channels | 8, independent, with priorities |
| Directions | RAM→RAM, RAM→device, device→RAM, device→device |
| Burst | configurable burst length; block + single transfers |
| Linked list | hardware scatter-gather (LLP) |
| Security | per-channel awprot/arprot (AXI) |
It reaches memory and these peripherals: UART0–4, I²C0–4, SPI0–3, SPI_NAND, I2S0–3.
Each of the 8 channels is wired to one peripheral request by the system controller (sdma_dma_ch_remap0/1). TRM Table 9.1 lists 42 request lines:
| # | Line | # | Line |
|---|---|---|---|
| 0/1 | i2s0 rx/tx |
24/25 | i2c0 rx/tx |
| 8/9 | uart0 rx/tx |
26/27 | i2c1 rx/tx |
| 16/17 | spi0 rx/tx |
38 | spi_nand |
| 34/35 | tdm0 rx/tx |
39 | spi_nor |
Configuration: write the remap register, then write 1 to update_dma_remap_0_3 / _4_7 to latch it.
Multiple channels may not map to the same request line. The 8 channels are a pool you assign to whichever devices need them.
CHx_CTL @ +0x118 is the interesting one:
| Field | Bits | Meaning |
|---|---|---|
SRC_TR_WIDTH / DST_TR_WIDTH |
10:8 / 13:11 | transfer width (0 = 8b, 1 = 16b, 2 = 32b, 3 = 64b) |
SRC_MSIZE / DST_MSIZE |
17:14 / 21:18 | burst length |
TT_FC |
34:32 | transfer type / flow control |
ShadowReg_Or_LLI_Valid, LLI_Last |
— | linked-list control |
Sizes follow from those (TRM §11.3.3):
Basic (TRM §11.4.3) — one contiguous block:
Linked-list (TRM §11.4.4) — scatter-gather across many blocks:
A linked list turns “copy these 100 scattered blocks” into one channel enable. The CPU touches the descriptor list, not the data.
The lab: copy a buffer N ways and measure throughput and CPU time.
# if the DMAC is registered with dmaengine:
ls /sys/class/dma # dma0chan0 … dma0chan7 ?
# the kernel's built-in benchmark, if CONFIG_DMATEST is set:
modprobe dmatest
echo 4096 > /sys/module/dmatest/parameters/transfer_size
echo 200 > /sys/module/dmatest/parameters/iterations
echo 1 > /sys/module/dmatest/parameters/run
dmesg | tail # MB/sCompare with a userspace memcpy loop: DMA should win on CPU% even when it loses on raw throughput for small blocks.
Verify first: our duos/oz64 defconfigs set CONFIG_DMADEVICES=y but do not obviously enable a DMAC driver or CONFIG_DMATEST. If /sys/class/dma is empty, the lab needs a small out-of-tree driver — or we scope it to reading the descriptor registers.
DMA moves bytes behind the CPU’s cache. That is the whole point — and the whole problem:
The kernel handles this with the DMA API (dma_map_* / dma_sync_*), which does the cache maintenance for you. Bare-metal code has to do it by hand.
The little C906 is not cache-coherent with the Linux core (see physical-memory.md). Shared buffers between the two need explicit cache ops — or an uncached mapping.
Once you have a DMAC, peripherals lean on it:
| Peripheral | Uses DMA for |
|---|---|
| SD/eMMC (SDHCI) | block transfers via SDMA_SADDR / ADMA2 descriptor tables |
| I²S / audio | continuous sample streaming |
| SPI / UART | FIFO fill/drain without an IRQ per byte |
| Video | frames into/out of the encoders |
The boot deck already showed SDHCI’s DMA path (SDMA_SADDR is a physical address — the controller DMAs straight into SRAM/DRAM). Same idea, different engine.
“Who moves the data” is an architectural choice with a measurable cost.
Next: where the buffers actually are — the software memory map.
We saw the physical map in the boot deck. Now the question is who owns what:
| Region | Base | Owner |
|---|---|---|
| BootROM | 0x0440_0000 |
mask ROM |
| CLKGEN / PLL | 0x0300_2000 |
clock driver |
| RSTGEN | 0x0300_3000 |
reset driver |
| SDMA | 0x0433_0000 |
dmaengine |
| DDR | 0x8000_0000 |
Linux — minus the carveouts |
Every peripheral is memory-mapped, but “memory-mapped” ≠ “yours”. The kernel maps only what the DT describes.
memmap.py + the DTS reserved-memory node reserve DRAM before Linux sees it:
| Carveout | Purpose |
|---|---|
FREERTOS (top 2 MB) |
the C906L image window |
monitor (0x8000_0000) |
OpenSBI |
ION (~74 MB) |
multimedia heap |
bootlogo / framebuffer |
the boot logo |
rproc / vdev0* |
remoteproc + virtio rings (arduino builds) |
This is why the same board reports different “RAM” depending on SECOND_CPU=camera vs arduino — the personality changes the map.
The engines (VPU, ISP, TPU) need big, physically-contiguous, DMA-able buffers. That is what ION is:
cvitek,cvitek-ion exposes a carveout heap to userspace.The engines don’t allocate; they borrow. A camera frame goes sensor → ISP → VPSS → encoder with zero copies — the same physical buffer, handed along.
Two things every driver must get right:
| Question | Mechanism |
|---|---|
| Is this line in cache or in DRAM? | cache maintenance (dma_sync_*) |
| Is this access even allowed? | MMU page tables + PMP/firewall |
physical-memory.md).Three views of the same DRAM: virtual (Linux), physical (the little core), and DMA (the engines). Keeping them straight is most of driver writing.
A general-purpose CPU can compute anything — expensively. An engine trades generality for efficiency:
| CPU | Engine | |
|---|---|---|
| Flexibility | total | one job |
| Energy per op | high | very low |
| Area | shared | dedicated |
| Parallelism | a few cores | thousands of MACs / pixels |
This is domain-specific architecture: when one computation dominates the workload (a video codec, a convolution), build silicon that does only that.
| Engine | Compatible (DT) | Does |
|---|---|---|
| TPU | cvitek,tpu |
0.5 TOPS INT8 inference — lab later |
| VPU | cvitek,asic-vcodec |
H.264/H.265 encode+decode |
| JPU | cvitek,asic-jpeg |
JPEG encode/decode |
| ISP / VI | cvitek,vi, cvitek,cif |
camera front end — later deck |
| VPSS | cvitek,vpss |
scale / crop / format |
| IVE | cvitek,ive |
image ops (OpenCV-ish) |
| DWA | cvitek,dwa |
dewarp (lens correction) |
| RGN | cvitek,rgn |
region / OSD overlay |
The engines are not called one at a time — they are chained:
The CPU is the conductor, not the orchestra. This is what a modern SoC looks like: a small control core and a lot of fixed-function dataflow.
From Linux, engines are reached through the vendor’s osdrv / middleware stack:
cvitek-osdrv-<board>) expose /dev/cvi-* nodes.cvitek-middleware) and the tpusdk / onnxruntime packages.SECOND_CPU=camera personality (the ISP/VENC stack is the little core’s other life).One core, two lives: the C906L is either the vendor’s camera/ISP engine (
SECOND_CPU=camera) or yours (SECOND_CPU=arduino) — never both.
The TPU lab — run a real model:
The camera deck — the ISP pipeline:
SECOND_CPU=camera firmwarecat /sys/kernel/debug/clk/clk_summary; identify CPU / DDR / TPU / SPI clocks and their parents.cat /sys/class/thermal/thermal_zone*/temp./sys/class/dma + dmatest, or write a descriptor by hand via /dev/mem and watch CHx_SSTAT/CHx_DSTAT.See Reference Notes for the condensed version, and:
boot-slides/ — privilege modes, memory map, SDHCI DMA.i2c-slides/ — the clock-gating surprise, in full.hints/physical-memory.md — M-mode, physical access, coherency.hints/8051-mcu.md — the fourth core, in detail.CS 4250 · Beyond the CPU — SG2000