EC-9.2 Memory-Mapped Input and Output
en
What this is and why it exists
This is the single idea that makes embedded programming different from other programming.
A memory location that changes when nobody wrote to it. A memory write that turns on a motor.
Every board topic in this area writes to a port before anything has explained what a port is. This topic is the explanation those topics assume, and it is also where the first genuinely confusing bugs come from.
The vocabulary
- Memory-mapped input and output — controlling hardware by reading and writing ordinary addresses.
- Peripheral register — an address whose bits control or report the state of a hardware unit.
- Direction register — the register saying which pins of a port are outputs.
- Read-modify-write — reading a register, changing part of it, and writing it back.
- Volatile — a marking that tells the compiler a location can change outside the program.
- Mask — a constant with chosen bits set, used to change some bits and leave others alone.
- Pull-up resistor — a resistor that holds an input at a high level when nothing else drives it.
- Open drain — an output that can pull a line low but cannot drive it high.
The mental model
Talking to hardware is reading and writing addresses. There is no separate instruction for it, no special function that the language provides, and nothing magic. That is what memory-mapped means.
Turning on an output is a write to one address. Reading a switch is a read from another. In C this is done through a pointer to a fixed address. The vendor's header file is a long list of those pointers with useful names attached.
A port usually has three registers and confusing them is the standard first mistake. One says which pins are outputs. One holds what the outputs should be. One reports what the inputs actually are.
Writing to the output register of a pin configured as an input does nothing visible. Reading the output register tells you what you asked for rather than what is there. Reading the input register is the only way to find out what the world is doing.
Now the first real trap. Setting one bit of a register means reading it, changing that bit and writing it back. Three separate operations.
Suppose an interrupt runs in the middle and changes a different bit of the same register. Your write then puts the old value of that bit back. The other change is silently undone, and nothing reports an error. This is read-modify-write, and it is the reason many parts provide separate set and clear registers. Writing a one to a bit position there sets or clears only that bit.
The second trap is the compiler. A compiler may see the same location read twice with no write in between. It is entitled to keep the first value in a register and skip the second read. That is a correct optimisation for ordinary memory.
For a peripheral it is a disaster, because the hardware changed the location and the program never notices. Marking the pointer volatile tells the compiler that the location can change outside the program. Every read must then actually happen, and every write must actually be performed. A loop waiting for a hardware flag without volatile is the classic version of this bug, and it appears only when optimisation is turned on.
Masks are the everyday tool. A constant with a single bit set, combined with the value using a bitwise or, turns that line on and leaves the rest alone. The same constant inverted, combined using a bitwise and, turns it off. Combining with a bitwise exclusive or flips it.
Writing these with a shift makes the intent readable: one shifted left by the bit number. It is worth building the habit, because a hexadecimal constant in the middle of a driver tells the next reader nothing.
The last idea is electrical and it surprises people who arrive from software. An input pin with nothing connected does not read zero. It reads whatever nearby interference decides, and it can change between one read and the next.
A pull-up or pull-down resistor gives it a defined state, and most parts have them built in and switchable from software. A button is normally wired to pull the line low when pressed, with a pull-up holding it high otherwise. That is why so much embedded code treats zero as pressed.
Open-drain outputs go further. They can pull a line low but cannot drive it high at all, and rely on a pull-up for the high level. That sounds like a limitation and is exactly what lets several devices share one line. The two-wire buses in a later topic depend on it.
What you should now be able to explain or do
- Explain what memory-mapped input and output means and why no special instruction is needed.
- Name the three registers of a typical port and say what each one does.
- Describe the read-modify-write problem and two ways of avoiding it.
- Say why a peripheral pointer must be volatile and what happens when it is not.
- Set, clear and flip one bit of a register with a mask, without disturbing its neighbours.
- Predict what an unconnected input reads, and configure a pull-up for a button.
Check yourself
What does volatile actually change?
It forbids the compiler from caching or removing accesses to that location. Every read in the source becomes a real read, which is what a program waiting on a hardware flag depends on.
You set one bit of a port and another bit changes back on its own. What is happening?
A read-modify-write conflict. An interrupt changed the other bit between your read and your write, so your write restored its old value. Separate set and clear registers avoid it.
Why does a button usually read zero when pressed?
Because it is wired to pull the line to ground, with a pull-up holding it high otherwise. That arrangement is more reliable than driving the line high through a switch.
An input pin has nothing connected to it. What does it read?
Whatever interference decides, and it can differ between reads. A pull-up or pull-down resistor is what gives an unconnected input a defined level.
Why can several devices share one open-drain line?
Because none of them can drive it high. Any device may pull it low, and a shared pull-up provides the high level. There is therefore no way for two devices to fight each other.
Go deeper
We haven't checked most of these for screen reader use yet.
Back to Memory-Mapped Input and Output: work through the checklist