Wishbone · Module 1
Memory-Mapped IO
Memory-mapped I/O does not turn a peripheral into memory. It gives the peripheral's registers addresses in the processor's address space, so an ordinary load or store selects them. The address then does two jobs — name the target, name the register inside it — and the map that assigns them is a contract between software and RTL.
Chapter 1.1 ended one step short of a working system. It established that a peripheral exposes locations software reads and writes, that an interconnect routes an access to whichever block owns the addressed location, and that a peripheral's own registers are numbered from zero because the block has no idea where it sits in a larger system.
Something has to close that gap. The core issues an address; the peripheral understands an offset; nothing so far says how one becomes the other.
So: by what mechanism does an ordinary load or store — an instruction built to read and write memory — end up reaching a UART instead?
1. What the Decision Actually Is
An address space is a namespace, not a description. A 32-bit core can name 2³² byte locations. Nothing in the architecture says what is behind any of them — that a given address holds RAM, holds a peripheral register, or holds nothing at all is a property of the system, not of the core.
Memory-mapped I/O is the decision to spend part of that namespace on hardware.
Stated that way, its main consequence is immediate and slightly anticlimactic: the core needs no new capability. The load/store unit, the addressing modes, the pointer arithmetic, the linker, the debugger's memory view — all of it already works on addresses, and a peripheral register that has an address inherits the lot for free.
That is the whole argument for the approach, and it is a strong one. It is also the source of the single most durable misunderstanding in embedded work, because reusing the addressing mechanism gets read as behaving like memory. Section 7 is about the gap between those, and Section 9 is about what the system has to do to keep the gap from becoming a bug.
One claim this chapter will not make: that this is the only way. A processor can instead put peripherals in a second, separate address space reached by its own instructions, and some do. Section 8 resolves the thread Chapter 1.1 opened there.
2. One Address Space, Allocated
Take the system from Chapter 1.1 — a core, some memory, a GPIO block, a UART and a timer — and give every block a home.
Expanding the peripheral band:
| Region | Range | Size | Owner |
|---|---|---|---|
| Boot ROM | 0x0000_0000 – 0x0000_FFFF | 64 KiB | instruction fetch at reset |
| SRAM | 0x2000_0000 – 0x2000_FFFF | 64 KiB | code, data, stack |
| GPIO | 0x4000_0000 – 0x4000_0FFF | 4 KiB | GPIO block |
| UART | 0x4000_1000 – 0x4000_1FFF | 4 KiB | UART block |
| Timer | 0x4000_2000 – 0x4000_2FFF | 4 KiB | timer block |
| — | everything else | — | nothing |
Three observations, and each one has teeth later.
The peripherals are tiny and the regions are not. The UART has four registers — sixteen bytes of actual state — and owns four kilobytes. That is not waste, because address space is the one resource in this system that is genuinely abundant: a 32-bit space is four billion addresses and this design uses about 140 thousand of them. Section 6 explains why generous regions are bought deliberately.
Almost everything is unmapped. The interesting question is not what the mapped regions do but what happens to an access that lands in the enormous space between them. Section 6 again — an address map is not complete until it says.
Nothing here came from Wishbone, from the core, or from the UART. The three 4 KiB windows, their base addresses, the decision to group peripherals at 0x4000_0000: all of it was chosen by whoever assembled this SoC and then written down. The next section is about what that writing-down obliges everyone to.
3. The Address Does Two Jobs
Here is the decomposition that the rest of the chapter rests on.
Written as arithmetic, it is the least surprising formula in the track:
absolute address = peripheral base address + register offset
0x4000_1004 = 0x4000_1000 + 0x04What matters is not the sum. It is that the two operands belong to different owners, and that neither owner knows the other's value.
The base address belongs to the system. It was chosen when the SoC was assembled, it appears in the memory map document, it is what the interconnect compares an incoming address against, and it changes if the system is rearranged. The UART does not know it. Chapter 1.1's peripheral module took a four-bit local address and was explicitly ignorant of where it sat — this is why that was the right design rather than a simplification.
The offset belongs to the peripheral. It was chosen by whoever designed the UART, it appears in the UART's datasheet, it is what the block's own decode examines, and it is the same in every system the block is ever dropped into.
That separation is what makes a peripheral portable at all. Move the UART to 0x5000_0000 and every offset is unchanged; put a second UART at 0x4000_3000 and one design serves both.
4. What Happens When the Instruction Executes
Trace one access, conceptually. The core executes a load from 0x4000_1004.
- The core issues an addressed read. It has no idea a UART exists. As far as the load/store unit is concerned this is the same operation it performs against SRAM.
- The address reaches the interconnect on the core's memory interface, along with the direction and the access width.
- The interconnect compares the address against the system map.
0x4000_1004falls inside0x4000_1000–0x4000_1FFF, so the UART is the target. Exactly one region should match; Section 6 is about what it costs when zero or two do. - The request is presented to the UART, carrying the offset —
0x004— rather than the absolute address. The block sees the number it was designed around. - The UART decodes the offset locally and selects its status register, exactly as Chapter 1.1's module selected between its three locations.
- The UART returns the value and signals that the access is complete, and the interconnect routes both back.
- The core's load completes and writes the result into a register. From the instruction's point of view nothing unusual happened.
Step 6 is doing more work than its one sentence suggests, and this chapter is deliberately not opening it. How a request is presented, when address and data are valid, how completion is signalled, what happens when the target needs longer, and what a failure looks like — none of that is settled by having an address map. Chapter 1.1 derived the list of things that must cross the boundary; this chapter has only named the locations. Chapter 1.3 is where the missing agreement becomes the subject.
5. What This Looks Like in Software
An address is an integer; a C pointer is an integer the compiler will dereference. That is the entire mechanism.
/* Read the UART status register at 0x4000_1004. */
uint32_t status = *(volatile uint32_t *)0x40001004u;Read outward from the constant. 0x40001004u is the address. The cast makes it a pointer to a 32-bit unsigned object. * dereferences it, which the compiler turns into a 32-bit load. Nothing in that line is special to hardware — it is the same code shape you would write to read a 32-bit value out of RAM, which is precisely the point of the whole approach.
volatile is the one part that is not ordinary, and it is the part most often misdescribed.
In real code the constant does not appear at the point of use. The address map becomes a header:
#define UART_BASE 0x40001000u
#define UART_DATA 0x00u /* write: byte to transmit; read: byte received */
#define UART_STATUS 0x04u /* read-only: ready / busy flags */
#define UART_CTRL 0x08u /* enable, interrupt enable */
#define UART_BAUD 0x0Cu /* divider */
#define UART_REG(off) (*(volatile uint32_t *)(UART_BASE + (off)))Which makes the header the software half of a two-part contract. The other half is the RTL: the interconnect's decode, which must place the block at 0x4000_1000, and the block's own offset decode, which must agree that 0x04 is the status register. Nothing checks that the two halves match. They are separate files, usually written by separate people, and frequently in separate languages.
That is not a hypothetical fragility. It is the origin of a specific and very recognisable class of bug, and Section 6 is about why the map is the artefact that has to prevent it.
6. The Map Is a Contract, and Contracts Have Clauses
Every requirement below is derived from the same two-job model rather than being a separate rule to memorise. In each case the question is: what must be true of the map for the address to answer its two questions unambiguously?
Exactly one region may match an address. If two regions overlap, an address inside the overlap has two owners, and what happens then is undefined by the map and decided by an accident of the decoder — one block answers, or both do and the returned data is whichever won electrically. The symptom is characteristic: a peripheral that mostly works, and a handful of registers that read back nonsense, because only part of one region overlapped.
Every address must have a defined outcome, including the unmapped ones. The map above leaves the overwhelming majority of the space owned by nothing. An access there is a real event that a real system must do something specific about — return an error the core turns into a fault, return zeros silently, or hang waiting for a completion that never comes. The third is the one that hurts, and it is what you get by default if nobody decided. A map that lists its regions and stops has only described the easy case.
Offsets are frozen the moment software depends on them. A base address can move: it appears in one place in the header and one place in the interconnect. An offset cannot, because it is baked into every driver, every test, every ROM image already in the field. Renumbering a peripheral's registers is a breaking change of the same weight as changing a function signature in a published library.
Reserved space is how the map stays extensible. The UART's 4 KiB for sixteen bytes of registers is the answer to a question asked in advance: what happens when revision two adds a FIFO depth register? If the region is snug, the new register has to go somewhere else and the peripheral is split across two ranges forever. If it is generous, the new register takes the next free offset and nothing else in the map moves. Address space is cheap; a fragmented map is not.
Alignment and access width are part of the contract, not incidental. Most peripheral registers are defined as naturally-aligned words. A byte write to the middle of one, or an unaligned word access straddling two, is frequently not supported — and not supported covers a range of behaviours, from a bus error, to the access being applied to the whole register, to it being silently dropped. Which of those a given block does is implementation-specific and belongs in its documentation. The general rule worth carrying is that a register's defined access width is part of its definition, and that "it worked when I used a byte pointer" is evidence about one implementation, not about the interface.
Peripheral regions usually need different treatment from RAM. This is the largest clause and it gets Section 9.
7. Same Addressing, Different Semantics
Chapter 1.1 established that a peripheral register is an interface to hardware behaviour rather than a place that stores a value. That is not being re-argued here. What is new is the interaction between that fact and this chapter's decision.
Memory-mapped I/O deliberately makes hardware registers reachable by exactly the same instructions as RAM. It does not, and cannot, make them behave like RAM. The mechanism is shared; the semantics are not.
Everything the addressing machinery gives away for free assumes memory semantics — that a read has no effect, that a value read twice is the same value, that a write can be delayed or merged with a neighbouring one, that a location can be read speculatively because reading is harmless. Applied to a register that clears a flag when read, or that starts a transmission when written, every one of those assumptions is wrong.
The registers in the map above already span most of the ways that happens:
| Location | What it is | Why memory semantics fail |
|---|---|---|
UART_STATUS | read-only, hardware-updated | Changes with no write; two reads may differ |
UART_DATA | write: transmit; read: receive | One address, two different physical registers |
GPIO_IN | read-only, samples pins | The value is the outside world, not stored state |
TIMER_CTRL | write starts counting | The write is an action with a lasting effect |
The insight worth carrying out of this section is narrow and load-bearing: sharing an addressing mechanism is not sharing a contract. Section 9 is what a system has to do about that, and the misconceptions in Section 11 are almost all cases of forgetting it.
8. The Alternative: a Separate I/O Space
Chapter 1.1 asserted that memory-mapped I/O is a choice rather than a law. Here is the counterexample.
x86 has a second address space — 16 bits wide, 64 KiB of I/O ports, entirely disjoint from the physical memory space — reached by its own instructions, IN and OUT, rather than by loads and stores. An ordinary MOV cannot touch it, and the port number 0x3F8 has no relationship to the memory address 0x3F8. The distinction is carried out of the processor, so the platform can tell the two kinds of access apart.
The trade is clean in both directions.
| Memory-mapped | Port-mapped | |
|---|---|---|
| Instructions needed | the existing load/store | dedicated I/O instructions |
| Addressing modes, pointers, structs | all available | not available |
| C without extensions | works | needs intrinsics or assembly |
| Address space consumed by hardware | yes | none |
| Peripheral and memory accesses distinguishable | only by address | intrinsically |
| Accidental caching or speculation of a register | possible, must be prevented | not possible — the space is not memory |
Neither column is the better engineering. Port-mapped I/O gets a hard separation for free: a peripheral access cannot be confused with a memory access by any part of the system, which removes a whole class of problem that Section 9 exists to manage. It pays for that with instructions that only one part of the system can use, no pointer arithmetic, no ability to write a driver in portable C, and a small fixed space.
Memory-mapped I/O made the opposite trade, and it is the trade essentially every modern SoC architecture makes, including every core Wishbone is used with. Recognising it as a trade is what this section is for; the reason the industry settled on one side is that generality and toolchain reuse mattered more than the separation, once address space stopped being scarce.
Note one thing precisely: IN and OUT are still addressed reads and writes in the sense Chapter 1.1 used. What differs is which namespace the address belongs to and which instructions may name it — not the shape of the access.
9. Why the Region Usually Needs Different Rules
A processor that treats the whole address space uniformly will do things to a peripheral region that are correct for memory and wrong for hardware. The system therefore has to mark the region as different, and this is where "just give it an address" stops being free.
The behaviours that have to be controlled — stated as categories, because the mechanism and the exact guarantees differ by architecture:
Caching. A cache exists to avoid going to memory. A cached read of a status register returns whatever the line held when it was filled; a cached write sits in the cache and reaches the UART whenever the line is evicted, if ever. Both are correct cache behaviour and neither is usable. Peripheral regions are therefore normally configured as non-cacheable.
Speculation and prefetch. A core that may issue reads it has not yet committed to — down a branch it is guessing at, or as a prefetch past the end of an array — can perform a read that architecturally never happens. Harmless on memory. On a read-to-clear register or a receive FIFO it consumes an event that no instruction ever asked for, and the bug is invisible in the source.
Reordering. Where the architecture permits accesses to complete out of program order, a sequence like write the data register, then write the control register that transmits it can reach the peripheral in the other order. The driver source reads correctly and the hardware sees nonsense.
Merging and repeating. A system that may combine two adjacent writes into one wider access, split one into several, or issue a read twice, will produce results no register expecting exactly one access of a defined width can survive.
How it is controlled is architecture-specific, and this chapter deliberately does not teach a memory model. As examples with their scope stated: Arm architectures classify regions by memory type, and the Device types exist precisely to forbid the behaviours above to varying degrees, configured through the MMU or MPU. RISC-V expresses comparable properties as physical memory attributes for an address range, with the FENCE instruction ordering accesses where ordering is required. Other architectures use uncached address windows, or dedicated instructions. The mechanisms are genuinely different and the guarantees are not interchangeable — a driver's ordering requirements must be checked against the architecture actually being used, and against that architecture's manual rather than against a habit carried from another one.
10. Worked Example — Sending One Byte
Put the whole chapter through a single operation: software wants to transmit the character A on the UART.
What is given. From the system's memory map: the UART's base is 0x4000_1000. From the UART's own datasheet: DATA is at offset 0x00, STATUS at offset 0x04, and bit 0 of STATUS reads 1 when the transmitter is free.
Compute the addresses. Two additions, each combining one fact from each source:
UART_STATUS = 0x4000_1000 + 0x04 = 0x4000_1004
UART_DATA = 0x4000_1000 + 0x00 = 0x4000_1000The driver. Chapter 1.1 showed this sequence as abstract reads and writes; here it has real addresses.
/* Wait until the transmitter is free, then hand it the byte. */
while ((UART_REG(UART_STATUS) & 1u) == 0u) {
/* The volatile in UART_REG is what keeps this loop re-reading
the register rather than spinning on a value cached in a
register by the compiler. */
}
UART_REG(UART_DATA) = 'A';What the store does, address first. The write becomes a 32-bit store to 0x4000_1000:
- The core issues it as an ordinary addressed write. It does not know what is there.
- The interconnect matches
0x4000_1000against the map: inside0x4000_1000–0x4000_1FFF, so the UART. - The UART receives offset
0x000and decodes it asDATA. - The write lands in the transmit register, and that is the action — the transmitter begins shifting the byte out at the configured bit rate. Chapter 1.1's point that a peripheral write changes state the hardware then acts on is exactly what is happening.
- The access completes. The core's next instruction runs while the byte is still on the wire.
Three things this example is deliberately showing.
The two facts needed to compute the address came from two different documents, owned by two different parties, and the code combined them at compile time. That is the two-job split in its everyday form.
The polling loop reads a register whose value changes with no write anywhere in the program. Without volatile the compiler is entitled to read it once and loop forever on the copy — the canonical first-week embedded bug.
The final store is a plain C assignment whose effect is a serial waveform leaving the chip. Nothing in the syntax says so. That is memory-mapped I/O working exactly as designed, and also exactly why the next section exists.
11. Common Mistakes
"An MMIO register is memory at a funny address."
The wrong model: the addressing is the same, so the semantics are the same.
What it costs: the whole family at once — read-back verification that fails on a write-one-to-clear register, a polling loop optimised into a single read, a debugger's register view that consumes a received byte, a cached write that reaches the device minutes later or never.
The corrected model: memory-mapped I/O reuses the addressing mechanism and shares nothing else. A read may have effects, a write may not store, a value may change unprompted, and the access may need to be exactly one access of exactly the defined width.
"volatile makes the access safe."
The wrong model: one keyword covers every way an access can go wrong.
What it costs: a driver that is correct on a simple in-order core with no cache and fails when moved to a core that reorders or caches — the hardest kind of failure to diagnose, because the source did not change and looks right.
The corrected model: volatile constrains the compiler and nothing else. Processor reordering needs barriers; caching, speculation and write merging need the region's memory attributes. Three layers, three separate fixes, as Section 9's callout sets out.
"The peripheral knows its own address."
The wrong model: a UART at 0x4000_1000 has 0x4000_1000 somewhere inside it.
What it costs: a block written to compare against absolute addresses, which then cannot be instantiated twice or moved, defeating the reuse the whole arrangement exists for. It also produces drivers that hard-code absolute addresses instead of base-plus-offset, which then cannot address a second instance.
The corrected model: the peripheral sees an offset and nothing more. The base lives in the interconnect's decode and in the software's header. This is the separation that lets one design serve every instance.
"Overlapping regions will be caught somewhere."
The wrong model: an address map is data, so a mistake in it will be flagged like a syntax error.
What it costs: two blocks answering one address, a peripheral that works except for the registers inside the overlap, and a search that goes into the peripheral's RTL where there is nothing wrong.
The corrected model: nothing validates a map unless the project builds something that does. The map is a document plus a C header plus an RTL decode, in three files, with no cross-check. Overlap is a design error that surfaces as data corruption.
"An unmapped access will just return an error."
The wrong model: the system has a sensible default for addresses nobody owns.
What it costs: a hang. If no target claims the access and nothing was built to answer on their behalf, the initiator waits for a completion that will never arrive, and the whole system stops on what began as a stray pointer.
The corrected model: unmapped behaviour is something the integrator must implement — usually a default target that terminates the access with an error. It is a positive design decision, and a map that does not state it has left its largest region undefined.
"Wishbone defines where peripherals go."
The wrong model: the bus specification is the system specification.
What it costs: looking in the wrong document, and an expectation that conformance implies compatibility. Two conformant Wishbone systems can have entirely unrelated maps, and a driver is never portable between them on the strength of the bus alone.
The corrected model: Wishbone specifies an interface. The address map, the region sizes, the decode and the unmapped behaviour are the integrator's, every time. That is what makes the peripheral portable; it is also what makes the system something you must read the SoC's own documentation to understand.
12. Interview Reasoning
Memory-mapped I/O assigns a peripheral's software-visible registers addresses within the processor's address space. A load or store names an address; the interconnect routes the access to whichever block owns that address; if the address belongs to a peripheral region, the peripheral answers it.
What a strong answer adds: the core needs no new capability at all. The value of the approach is entirely in reuse — the existing addressing modes, pointer arithmetic, compiler, linker and debugger all work on a peripheral register the moment it has an address.
And the qualification that shows the model is real: it reuses the addressing mechanism without importing memory semantics. The register may be read-only, may change with no write, may act on being written and may have side effects on being read. Sharing an addressing mechanism is not sharing a contract.
13. Understanding Check
Use this map throughout. GPIO base 0x4000_0000, UART base 0x4000_1000, timer base 0x4000_2000, each region 4 KiB; SRAM 0x2000_0000 – 0x2000_FFFF. Offsets: GPIO 0x00 DIR, 0x04 OUT, 0x08 IN (read-only). UART 0x00 DATA, 0x04 STATUS (read-only), 0x08 CTRL. Timer 0x00 RELOAD, 0x04 CTRL, 0x08 STATUS.
14. What's Next
The core and the peripheral now agree on where. An address identifies a target and a location inside it, the map that assigns them is written down, and an ordinary load or store is enough to reach hardware.
They still do not agree on how. Nothing so far settles:
- how a request is represented — which signals carry the address, the data and the direction, and what they are called;
- when the address and the data may be considered valid, and by whom;
- how the target says done, and how the initiator waits when it does not say so immediately;
- what a failure looks like, and how it is distinguished from a slow success;
- and what any of this obliges two blocks written by people who never met.
Chapter 1.1 derived the list of information that has to cross the boundary. This chapter gave the locations names. Neither has said anything about the form of the exchange — and two engineers can agree completely on an address map and still build blocks that cannot talk to each other.
What has to be agreed, beyond the address, for two independently designed blocks to interoperate on the first attempt — and why should that agreement be a published standard rather than a decision each project makes for itself?
Chapter 1.3 — Need for Standardized Interconnects takes that up, and it is where Wishbone stops being context and becomes the subject. The full path is on the Wishbone curriculum index.
Continue learning
Related tutorials
- Related topic
CPU to Peripheral Communication
A CPU reaches hardware outside itself by reading and writing addressed locations, and a peripheral is hardware it cannot execute. Everything a driver does has to be expressed as a read or a write of a location the peripheral answers for — and once more than a couple of peripherals exist, wiring each one to the core separately stops scaling. That is the problem an on-chip bus is the answer to.
- Related topic
Address Decoding
A bus address answers two questions, not one. Measured across a three-target SoC, including the boundary where one window ends and the next begins.
- Related topic
OpenCores Origins
Wishbone was written by Wade Peterson at Silicore Corporation and placed in the public domain; OpenCores published revision B3 in 2002 and took over its stewardship the same year. The engineering question behind that history is why a community of independent IP authors needed an interconnection convention at all — and what the public-domain decision was protecting against.
- Related topic
Why Wishbone Was Created
Six chapters of engineering pressure produce a specific set of requirements: a fixed interface, a signalled completion, a synchronous reference, an interconnect the integrator still owns, and a licence a volunteer project can adopt without a legal review. Wishbone is what those requirements look like written down — including the things it deliberately refuses to decide.
Standards & specifications
- Governing standard
- Wishbone SoC Interconnection Architecture (OpenCores)(opens OpenCores in a new tab)
Defines the Wishbone signal set, the bus cycles built from it and the interface rules a portable IP core must follow. It deliberately leaves interconnect topology, address map and arbitration policy to the integrator, so those are system decisions rather than requirements of the specification.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the Wishbone curriculum.
