The puzzle
0x2000_1000 and 0x4001_E004 are both just 32-bit numbers, and the processor issues exactly the same kind of store to each. Yet the first simply remembers the value, and on an RP2040 the second sets the pull-up enable bit of pin GPIO0’s pad, that one bit of a pad-control register, without disturbing its neighbours. Nothing in the instruction says which is which. Something between the core and the silicon looks at the number and decides. What is that something, and what does it do when two parts of the chip want the same memory in the same clock cycle?
STEP 1
What one transfer carries
A bus is a set of shared wires plus a protocol for using them. In one transfer a master drives three groups of signals:
- Address: which location, 32 bits wide on a 32-bit microcontroller.
- Control: read or write, the size of the access (byte, halfword, word), and sometimes whether it is an instruction fetch or data, privileged or not.
- Data: from master to slave for a write, from slave to master for a read.
The slave has two more things to say. It can hold the transfer with a wait signal until it is ready, which is how a slow memory stretches an access over several clock cycles (lesson 6). And it can answer with an error, which the core turns into a fault. On Arm microcontrollers the on-chip buses follow Arm’s AMBA protocols, usually a fast AHB for the core, memories and DMA, and a simpler, slower APB for most peripherals, joined by a bridge. The RP2040’s SDK register headers label each block with its bus: UART, DMA configuration and IO_BANK0 registers are on APB; PIO is on the faster AHB-lite.
STEP 2
Decoding an address
Every slave owns a range of addresses. The interconnect compares the upper address bits with those ranges and selects exactly one slave; the lower bits, the offset, select a location inside it. On every Cortex-M processor the architecture divides the 4 GiB space into eight 512 MiB slices and gives each a default meaning, which tells you what kind of memory to expect and which default memory attributes (executable or not, device or normal memory) the core applies. The slice is simply
that is, the top three bits of the address. Code lives in region 0, SRAM in region 1, peripherals in region 2, external RAM and devices in regions 3 to 6, and the core’s own private peripherals and vendor system space in region 7 (the Cortex-M0+ Devices Generic User Guide, §2.2). The architecture fixes these defaults and the location of the private peripheral bus; what the vendor actually places in each region, and how it is decoded, is the chip’s own address map. Vendors do bend the defaults: the RP2040’s SIO block is on-chip, yet sits at 0xD000_0000, in a slice labelled External device.
↑ This step uses the figure at the top of the page.
Choose 0x4001_E004 in the figure. The top bits 010 say Peripheral. The RP2040 map puts the pad-control block PADS_BANK0 at 0x4001_C000, so the offset is 0x2004. Bits 13:12 of the offset are 10, which on the RP2040 selects the atomic SET alias, and the remaining offset 0x004 is GPIO0’s pad-control register. Writing 0x08 there sets only bit 3, the pull-up enable (PADS_BANK0_GPIO0_PUE_BITS in the SDK header), without a read-modify-write (unit 2 lesson 6). One number, three levels of meaning, all decided by fixed wiring.
The same header is also a warning about reset states. The pad register resets to 0x56, which has bit 2, the pull-down enable, already set; with both bits set the RP2040 enables a weak “bus keeper” rather than a pull-up (the SDK’s gpio.h documents this). A real pull-up needs a second atomic write, 0x04 to the CLEAR alias at 0x4001_F004, or the SDK’s gpio_pull_up(), which sets one bit and clears the other.
STEP 3
One address space, or two
Cortex-M and RISC-V microcontrollers use a single address space for instructions and data: a pointer can point at flash, SRAM or a register, and const data in flash is read with an ordinary load. This is the von Neumann arrangement. Many cores still fetch instructions and data over separate bus interfaces for speed, but software sees one map.
The classic 8-bit AVR is Harvard: program memory and data memory are separate spaces, each starting at address 0, reached by different instructions. A const table on an AVR is copied into scarce SRAM at startup unless it is marked PROGMEM, and then it must be read with pgm_read_byte(), because an ordinary pointer dereference would read data address 0x0100, not program address 0x0100 (avr-libc pgmspace.h). The C language hides the difference until a pointer crosses it.
STEP 4
The bus matrix and arbitration
A single shared bus serves one transfer at a time. A bus matrix (or crossbar) connects every master to every slave through separate paths, so the core can fetch from flash while DMA writes SRAM in the same cycle. The limit is per slave: one memory port serves one access per cycle. When two masters address the same slave at once, an arbiter grants one and makes the other wait.
A bus matrix lets different masters reach different slaves in the same cycle. When two masters want the same slave, an arbiter lets one through and the other waits. Here each master makes six accesses and each slave serves one access per cycle; the arbiter alternates between the two. Real matrices add wait states, burst transfers and programmable priorities.
With both masters on the same SRAM bank the twelve accesses take twelve cycles; on different banks, six. That is the reason chips split RAM into banks. The RP2040’s crossbar has separate downstream ports for the ROM, the flash window, each of six SRAM banks, the fast peripherals and the APB bridge, and its BUSCTRL block can count “contested” accesses on each port and give each master (either core, the DMA read side, the DMA write side) a high or low arbitration priority. Its main 256 KiB of SRAM also appears twice in the address map: once striped at 0x2000_0000, where consecutive words fall in different banks so that masters walking through memory rarely collide, and once as four separate 64 KiB banks from 0x2100_0000, so that a program can dedicate one bank to a DMA buffer. The dedication only works if the linker script also keeps that bank out of the striped window’s allocations, because the striped window spans all four banks; the SDK’s default layout already puts core 0’s stack in SCRATCH_Y, the separate bank 5.
STEP 5
When the answer is “no”
A slave, or the interconnect itself, can reject an access: an address with nothing behind it, a write to a read-only region, an access size the slave does not support, a peripheral whose clock is off. Which of these actually produce an error is chip-specific: the RP2040’s peripheral bridge, for example, does not fault a narrow write but replicates it across the whole 32-bit word, which is its own surprise. The rejection comes back as a bus error, and the core raises a fault exception: HardFault on Armv6-M cores such as the Cortex-M0+, or the more specific BusFault on Armv7-M cores when it is enabled. Unit 6 lesson 4 reads those faults. The practical rule: the address map is a contract, and only the ranges it lists are safe to touch.
STEP 6
Worked example: decode three addresses by hand
0x4003_4018. Region : Peripheral. RP2040 UART0 is at 0x4003_4000, so the offset is 0x18; bits 13:12 are 00, a normal access. The SDK header uart.h names offset 0x18 UARTFR, the flag register that says whether the transmit FIFO has room.
0xE000_ED0C. Region 7: the system region. The Cortex-M private peripheral bus starts at 0xE000_0000, the System Control Space at 0xE000_E000, and the System Control Block at SCS + 0xD00 = 0xE000_ED00 (CMSIS core_cm0plus.h). Offset 0x00C in the SCB is AIRCR, the register firmware writes to request a system reset (lesson 6). This part of the map is the same on every Cortex-M0+, whoever made the chip.
0x2100_0000. Region 1: SRAM. On the RP2040 this is SRAM0_BASE, the first byte of bank 0 seen without striping. The same physical cell also appears somewhere in the striped window at 0x2000_0000; which address you use decides how your data is spread across banks, not what the bytes are.
MYTHS AND FACTS
Common misconceptions
The CPU knows which addresses are peripherals
The core issues the same transfer for any address. The interconnect decodes it; the core only learns the answer, or an error.
A bus is one set of wires everyone shares
A modern MCU has a matrix of paths plus bridges to slower buses. Masters conflict only when they want the same slave.
Two addresses mean two registers
Aliases map several addresses to one register or memory cell, often with different behaviour.
A const pointer can read flash on any MCU
Only in a single address space. Harvard parts such as AVR need special attributes and access functions.
A bad address just returns garbage
It often returns a bus error and a fault, which is far easier to debug than garbage. Either way, it is not a value to use.
Peripheral accesses are as fast as SRAM
Peripherals behind a bridge on a slower bus can take several cycles per access.
Check yourself
Answer in your head, then open the card.
Which Cortex-M region does 0x6000_1234 fall in, and which does 0x1FFF_FFFF?
0x6000_1234 ÷ 0x2000_0000 = 3 (top bits 011): External RAM. 0x1FFF_FFFF has top bits 000: the last byte of the Code region.
On the RP2040, what does writing 0x08 to 0x4001_F004 do, if PADS_BANK0 is at 0x4001_C000?
Offset 0x3004: bits 13:12 are 11, the atomic CLEAR alias, and the register is offset 0x004, GPIO0’s pad control. The 1 in bit 3 clears the pull-up enable; every other bit of the register is left alone, including the pull-down enable, which is set at reset.
Why does an AVR need pgm_read_byte() to read a PROGMEM table when a Cortex-M does not?
The AVR is Harvard: flash is a separate address space reached by different instructions, so an ordinary pointer dereference reads data memory. A Cortex-M has one address space, so an ordinary load at the flash address reads the table.
A DMA stream and the core’s stack both use the same SRAM bank and the application is slower than expected. What does the bus matrix suggest as a fix?
Put them in different banks, so they use different slave ports and no longer contend. On the RP2040 that means placing the DMA buffer in one of the unstriped banks, and keeping that bank out of the linker’s striped allocations, or raising the relative priority of whichever master must not stall.
Sources (5)
- Arm, Cortex-M0+ Devices Generic User Guide (DUI0662), §2.2 “Memory model” — the Cortex-M0+ memory map (the same regions as other Cortex-M cores), with default memory types and attributes per region: Code 0x00000000–0x1FFFFFFF, SRAM 0x20000000–0x3FFFFFFF, Peripheral 0x40000000–0x5FFFFFFF, External RAM 0x60000000–0x9FFFFFFF, External device 0xA0000000–0xDFFFFFFF, Private peripheral bus 0xE0000000–0xE00FFFFF, vendor-specific device memory above
- Raspberry Pi Ltd, pico-sdk 1.5.1, src/rp2040/hardware_regs/include/hardware/regs/addressmap.h, uart.h and pads_bank0.h — block base addresses; the REG_ALIAS_XOR/SET/CLR_BITS definitions (0x1, 0x2, 0x3 shifted left by 12); SRAM_STRIPED_BASE 0x20000000 and the unstriped SRAM0–3 views from 0x21000000; XIP_NOCACHE and XIP_NOALLOC aliases; uart.h gives UART_UARTFR_OFFSET 0x18 (with the TXFF “transmit FIFO full” bit); pads_bank0.h gives PADS_BANK0_GPIO0_OFFSET 0x04 with PUE at bit 3 (0x08), PDE at bit 2 and a reset value of 0x56 (pull-down enabled); hardware_gpio/gpio.h notes that “setting both pulls enables a ‘bus keep’ function”; each header states the block’s bus type (apb or ahbl)
- Raspberry Pi Ltd, pico-sdk 1.5.1, src/rp2040/hardware_regs/include/hardware/regs/busctrl.h — BUS_PRIORITY sets a priority bit for each master (PROC0, PROC1, DMA_R, DMA_W) “for bus arbitration”; the performance-counter event list names the crossbar’s downstream ports (rom, xip_main, sram0–sram5, fastperi, apb) and counts contested accesses on each
- Arm, CMSIS 6, CMSIS/Core/Include/core_cm0plus.h — SCS_BASE 0xE000E000 and SCB_BASE = SCS_BASE + 0x0D00; in SCB_Type, AIRCR is at offset 0x00C
- avr-libc, include/avr/pgmspace.h — on AVR, program memory is a separate address space: PROGMEM places data in flash and pgm_read_byte() reads it with a dedicated instruction