What a System-on-Chip Is

Open a desktop PC from 1998 and you find a small city: a CPU chip, rows of memory chips, a sound card, a modem card, a disk controller, a graphics board — dozens of packages shouting at each other across centimetres of copper trace. Open a modern phone and you find… one chip. The CPU is in there. The memory controller is in there. The USB port's brains, the timers, the display engine, the AI accelerator — all in there, on one die. You have spent five modules building and verifying a CPU; this module zooms out to the thing that actually ships: the System-on-Chip, or SoC. A CPU is an engine. An SoC is the whole car.

Why integration won

The chips-on-a-board approach lost for three compounding reasons. Cost: every separate package needs its own slice of silicon overhead, its own pins, its own board area and assembly step — one die amortises all of that. Power: driving a signal across a board trace costs hundreds of times the energy of an on-chip wire, because board traces are enormous capacitors by chip standards; a phone that had to shout across a board would be a hand-warmer. Latency: on-chip neighbours talk in fractions of a nanosecond, while a trip through package pins and back is an eternity the clock cannot hide. The buses you met at the systems level still exist — they just moved inside, got faster, and multiplied.

So who lives on the die? A typical SoC's cast list:

Different vendors pick different casts, but every SoC answers the same organising question: when the CPU executes a load or a store, who answers?

The memory map: the org chart of the chip

The answer is the memory map. Every block that software can talk to claims a range of addresses, and the whole 32-bit (or larger) address space is carved up like territory on a map:

Range (start)SizeOwnerWhat lives there
0x0000_000064 KiBBoot ROMthe first instructions ever fetched
0x2000_0000128 KiBSRAMfast on-chip working memory
0x4000_00004 KiBUARTserial-port registers
0x4001_00004 KiBTimercounter and compare registers
0x4002_00004 KiBGPIOpin input/output registers
0x8000_00001 GiBDRAMmain memory, via the controller

Here is the move that makes the whole scheme sing: memory-mapped I/O. The UART's transmit register is not reached by some special "talk to a device" instruction — it simply is address 0x4000_0000. A store to that address drops a byte into the UART; a load from the timer's range reads the current count. The same lw/sw instructions your CPU already speaks — the exact datapath you built — control every device on the chip. Software's view of hardware is just… addresses.

Turning an address into an owner is address decode: combinational logic (you could write it in an afternoon of always_comb) that inspects the high bits and routes the transaction. High bits pick the block; low bits are the offset — which register or word inside it. Address 0x4001_0008 decodes as "the timer block, offset 8".

Watch it assemble

Step through the diagram and watch a system condense around one spine. Every box that joins brings its own address range — its entry in the org chart:

Notice the shape: a few masters on top (blocks that start transactions — the CPU, the DMA engine) and many slaves below (blocks that answer). The spine between them is the interconnect — it gets two whole lessons of its own next.

Decode it yourself

Address decode is so central it deserves running code. Here is the map above as data, and a decoder that answers the only question the fabric ever asks — including what happens when an address lands in nobody's territory:

// The memory map: each block claims [base, base + size). interface Region { name: string; base: number; size: number } const map: Region[] = [ { name: "ROM", base: 0x00000000, size: 0x10000 }, { name: "SRAM", base: 0x20000000, size: 0x20000 }, { name: "UART", base: 0x40000000, size: 0x1000 }, { name: "TIMER", base: 0x40010000, size: 0x1000 }, { name: "GPIO", base: 0x40020000, size: 0x1000 }, { name: "DRAM", base: 0x80000000, size: 0x40000000 }, ]; const hex = (n: number) => "0x" + n.toString(16).padStart(8, "0"); function decode(addr: number): string { for (const r of map) { if (addr >= r.base && addr < r.base + r.size) { return r.name.padEnd(5) + " offset " + hex(addr - r.base); } } return "HOLE -> error response (bus fault)"; } const accesses = [0x00000100, 0x20000004, 0x40000000, 0x40010008, 0x50000000, 0x80001234]; console.log("address | owner and offset"); console.log("------------+--------------------------------"); for (const a of accesses) console.log(hex(a) + " | " + decode(a));

The store to 0x4000_0000 decodes to "UART, offset 0" — that byte is on its way out of the serial port. And 0x5000_0000 hits a hole: no block claims it, so a well-built fabric returns an error response and the CPU takes a fault. (A badly built one hangs forever waiting for an answer that never comes — a bug every SoC bring-up team has met at 2 a.m.)

The IP-reuse economy and the platform view

Nobody designs all of an SoC. A phone-chip team typically licenses the CPU cluster (say, Arm cores), buys or reuses the standard peripherals and interconnect — collectively called IP blocks ("intellectual property") — and spends its own designers on the blocks that differentiate the product: the camera pipeline, the neural engine, the modem. Integration is the craft of making a hundred blocks from a dozen sources behave as one chip. That works only because the blocks agree on standard interfaces — which is exactly what the next lesson's AXI protocol is for.

Integration also owns the concerns no single block can: system-level decisions. Where does the CPU fetch its very first instruction? (The boot ROM, at a fixed, agreed address.) Which device interrupt is wired to which CPU input? (The interrupt map.) In what order do a hundred blocks come out of reset, and who feeds each one its clock? (The clock and reset trees.) None of these belong to the UART or the CPU — they belong to the system, and getting them wrong bricks a chip made of individually perfect blocks.

In 1983, Motorola's DynaTAC "brick" phone carried its radio, logic and audio on roughly a dozen boards' worth of components — and cost about $4,000. By the late 1990s a phone needed a handful of chips: a baseband processor, separate memory, a power-management chip, radio chips. Today a budget smartphone's main SoC folds the CPU cluster, GPU, modem logic, image processors and dozens of peripherals into a single package smaller than your thumbnail — billions of transistors that cost less than the brick's battery did. The driver is the economics in this lesson run for forty years straight: every generation, moving one more chip's job onto the main die made the system cheaper, cooler and faster — so every generation, one more chip vanished.

The classic integration bug: two blocks are configured with overlapping address ranges — say a new accelerator is dropped in at 0x4001_0000, where the timer already lives. Now a store to that range has two possible owners. Depending on how the decode was built, either both blocks respond — two slaves driving one response channel, giving bus contention and corrupted data — or the decoder's priority logic silently prefers one, and the other block becomes unreachable in a way no simulation of either block alone will ever catch. The lesson: the memory map is a chip-wide contract with single ownership. Real teams keep it in one machine-readable file, generate the decoder and the software headers from it, and gate every merge on an overlap check. One map, one owner per address, no exceptions.