A GPIO controller is the simplest peripheral you will ever wire to APB, and its most important register hides the single cleverest trick in the AMBA peripheral catalogue: the address is the bit mask. This chapter grounds GPIO-over-APB in a real, shipped, billion-instance design — the Arm PrimeCell PL061 — and uses it to teach how an APB slave exposes pins to firmware. The centrepiece is GPIODATA, a data register aliased across 0x000–0x3FC so that the address bits PADDR[9:2] act as a per-bit mask: a write only affects the data bits whose mask address bit is 1, and a read returns only those bits (the rest read as 0). The single idea to carry: this address-as-bitmask trick turns "set bit 3 without touching the others" — normally a read-modify-write that races the hardware — into one atomic, race-free APB write, and once you see it in GPIODATA you understand why the PL061 register map is shaped the way it is. Around that centrepiece sit two more paths every GPIO needs: the direction register GPIODIR, and the interrupt chain IS/IBE/IEV → RIS → IE → MIS → IC.
1. Problem statement
The problem is exposing a bank of bidirectional package pins to firmware over APB such that software can configure each pin's direction, drive or sample its level, and take an interrupt on a pin event — and crucially, do all of that on individual pins without disturbing the others, atomically, from a single bus master.
A GPIO port is eight pins (in the PL061) that can each be an input or an output, and that can each raise an interrupt. Firmware needs four distinct services from the slave, and each forces a design decision the APB protocol alone does not answer:
- Per-pin direction. Each pin is independently an input or output. That is a plain configuration register (GPIODIR) — one bit per pin, read-write — and is the easy part.
- Per-pin data, written and read without a read-modify-write race. This is the hard part. "Set pin 3 high, leave the others alone" naively means read the data register, OR in bit 3, write it back. But between the read and the write, the hardware (or another context) may change another pin — and the write-back clobbers it. On a peripheral whose bits change asynchronously, RMW is a race. The PL061 solves it in the register map itself.
- Per-pin interrupt configuration and a clean clear. Each pin can interrupt on a level or an edge, on one edge or both, on high/rising or low/falling — and the raw event must be maskable and individually clearable. That is a small state-register pipeline (IS/IBE/IEV → RIS → IE → MIS → IC), and the W1C clear has its own correctness rules.
- All from one APB master, mechanically. The slave must service every one of these as ordinary APB reads and writes, with the storage cleanly separated from the bus glue, so the whole thing is a register bank the decoder and read mux service.
So the engineering problem is not "make eight flops addressable" — it is to expose them typed, per-pin, and atomically writable, and the PL061's answer to the atomicity requirement — the address-masked GPIODATA — is the non-trivial insight of this chapter.
2. Why previous knowledge is insufficient
This module has built the complete machinery of an APB slave — the register bank, the address decoder, the write logic, the read-data mux, the typed configuration and status registers — but every one of those chapters assumed one register per offset. The PL061's GPIODATA breaks that assumption, and that is exactly the gap this chapter fills.
- Register-bank design taught storage-versus-bus-glue separation and the register kinds (RW, RO, W1C, WO). It is the foundation here — GPIODIR is a plain RW config register, GPIORIS is an RO status register, GPIOIC is W1C — but it never contemplated a single register decoded across 1024 bytes of address space where the low address bits are not selecting a register but masking its bits. The bank model is necessary; it is not the GPIODATA model.
- The address decoder taught
PADDR→ which-register. GPIODATA inverts that: across0x000–0x3FC,PADDRdoes not choose a register — it always chooses GPIODATA — and instead carries the per-bit mask inPADDR[9:2]. The decoder you built selects storage; here the address selects which bits of one register the access touches. Same pins, opposite meaning. - The write logic and PSTRB strobes taught byte-granular writes —
PSTRBmasks bytes. GPIODATA needs bit-granular, race-free writes, and it gets them not fromPSTRBbut from the address. Understanding byte strobes does not give you bit-masked atomic writes; the address mask is a different mechanism entirely.
The gap is therefore precise: prior chapters give you a slave that maps one address to one register and writes it whole (or byte-masked). The PL061 GPIODATA needs one register aliased across many addresses, where the address supplies a per-bit read/write mask — a mechanism with no analogue in anything built so far. Teaching that mechanism, and why it removes the RMW race, is this chapter.
3. Mental model
The model: GPIODATA is not one register at one address — it is one 8-bit register viewed through 256 different stencils, and the address you write picks the stencil. A stencil is a cut-out mask: lay it over the eight data bits and only the holes are exposed. Write through a stencil and only the bits under the holes change; read through it and only the bits under the holes come back (the covered bits read 0). The address bits PADDR[9:2] are the stencil — bit i of the mask is PADDR[2+i]. So "set pins 3 and 1" is not read-modify-write; it is one write to the address whose bits 3 and 1 are set, carrying the desired values, and the hardware applies it to only those pins in a single atomic APB access.
Three refinements make this precise and interview-ready:
- The mask lives in the address, the value lives in
PWDATA. For a write, the slave updates data bitionly ifPADDR[2+i]is 1, and then sets it toPWDATA[i]. Bits whose mask address bit is 0 are physically not written — their flop's write-enable is deasserted — so there is no read, no merge, no window for a race. This is the whole trick: the enable per bit comes from the address, atomically, in the one cycle of the APB ACCESS phase. - It removes the RMW race because the unmasked bits are never touched. Classic "set one bit" is
data = read(); data |= (1<<3); write(data)— three steps, and any change to bit 5 between the read and the write is lost. The address mask collapses this to a single write whose hardware effect is exactlyif (mask[i]) data[i] <= pwdata[i]— bit 5 hasmask[5]=0, so its flop is simply not clocked-in this access. No read, nothing to clobber, no race. One master doing many such writes never corrupts itself, and the hardware-driven input bits are never disturbed by a software write to other pins. - Everything else is an ordinary typed register. Once GPIODATA is understood, the rest of the PL061 is a textbook register bank: GPIODIR (RW direction), GPIOAFSEL (RW alternate-function select), the interrupt-config trio GPIOIS / GPIOIBE / GPIOIEV (RW), GPIORIS (RO raw status), GPIOIE (RW mask/enable), GPIOMIS (RO masked status = RIS & IE), and GPIOIC (W1C clear). The interrupt path is a straight pipeline: detect per IS/IBE/IEV → latch into RIS → gate by IE → present as MIS → clear via IC.
4. Real SoC implementation
The PL061 register map is small and every register is a typed flop array the bank services — except GPIODATA, which is aliased across a kilobyte of address space so the address can carry the mask. The map (all offsets relative to the GPIO block base; reset values per the PrimeCell PL061 TRM):
| Offset | Name | Access | Reset | Purpose |
|---|---|---|---|---|
0x000–0x3FC | GPIODATA | RW | 0x00 | Pin data. PADDR[9:2] is the per-bit mask: a write updates only data bits whose mask bit is 1; a read returns only the masked bits (others 0). The address-masked atomic data register. |
0x400 | GPIODIR | RW | 0x00 | Direction. Bit i = 1 → pin i is an output; 0 → input. |
0x404 | GPIOIS | RW | 0x00 | Interrupt sense. Bit i = 1 → level-sensitive; 0 → edge-sensitive. |
0x408 | GPIOIBE | RW | 0x00 | Both-edges. When edge-sensitive, bit i = 1 → interrupt on both edges (GPIOIEV ignored for that pin). |
0x40C | GPIOIEV | RW | 0x00 | Interrupt event. Bit i = 1 → high-level / rising edge; 0 → low-level / falling edge. |
0x410 | GPIOIE | RW | 0x00 | Interrupt mask/enable. Bit i = 1 → the raw interrupt for pin i is allowed to propagate to MIS / the NVIC. |
0x414 | GPIORIS | RO | 0x00 | Raw interrupt status. Bit i = 1 → pin i has a pending detected event, before masking. |
0x418 | GPIOMIS | RO | 0x00 | Masked interrupt status = GPIORIS & GPIOIE. This is what the interrupt controller actually sees. |
0x41C | GPIOIC | W1C | 0x00 | Interrupt clear. Writing 1 to bit i clears that pin's edge-detected raw interrupt; level interrupts persist while the level holds. |
0x420 | GPIOAFSEL | RW | 0x00 | Alternate-function select. Bit i = 1 → pin i is controlled by a peripheral, not the GPIO data path. |
The only non-mechanical register is GPIODATA. Here is the APB decode for the address-masked write and the masked read, with real AMBA signal names — note the per-bit write-enable comes from PADDR, not from the data:
// PL061 GPIODATA — address-masked data register.
// Aliased across 0x000..0x3FC: PADDR[9:2] is a PER-BIT mask, NOT a register select.
// gpio_data[i] is the software-visible level of pin i.
// The mask bit for data bit i is PADDR[2+i] (i in 0..7).
logic [7:0] gpio_data; // the single 8-bit data register
// ---- MASKED WRITE: atomic per-bit update, no read-modify-write ----
// A write updates data bit i ONLY if its mask address bit PADDR[2+i] is 1,
// and then sets it to PWDATA[i]. Bits with mask=0 are NOT clocked -> untouched.
always_ff @(posedge pclk or negedge presetn) begin
if (!presetn) begin
gpio_data <= 8'h00; // reset default
end else if (psel && penable && pwrite && pready &&
(paddr[11:10] == 2'b00)) begin // in the GPIODATA aliased window
for (int i = 0; i < 8; i++) begin
if (paddr[2 + i]) // mask bit for pin i is set...
gpio_data[i] <= pwdata[i]; // ...so commit ONLY this bit, atomically
// else: write-enable for bit i is OFF -> flop holds -> no RMW, no race
end
end
end
// ---- MASKED READ: return only the masked bits; the rest read as 0 ----
// PRDATA[i] = gpio_data[i] if PADDR[2+i]==1, else 0. Combinational into the read mux.
always_comb begin
prdata_gpiodata = 32'h0;
for (int i = 0; i < 8; i++)
prdata_gpiodata[i] = paddr[2 + i] ? gpio_data[i] : 1'b0; // address masks the read too
endTwo facts make this the right shape. First, the atomicity is structural, not a software convention. The per-bit write-enable paddr[2+i] is what removes the read-modify-write: a flop whose enable is 0 is simply not updated this cycle, so a write to "pins 3 and 1" physically cannot disturb pin 5 — there is no read of pin 5, nothing to merge, nothing to clobber. Contrast the forbidden alternative — prdata = read(); val = prdata | (1<<3); write(val) — which has a window where the hardware can change pin 5 between the read and the write-back; the address mask deletes that window. Second, the same address mask governs the read, so a read of 0x0FF & ~(mask)-style alias returns only the bits you asked for — firmware can test "is pin 3 set?" with a single read of the address whose only mask bit is 3, getting data[3] in bit position 3 and zeros elsewhere, no shift-and-mask in software. The direction and interrupt registers, by contrast, are ordinary RW/RO/W1C flops the write logic and read mux service mechanically — GPIODIR gates the pad output-enable, GPIORIS is driven by the detect block, and GPIOIC is the W1C clear into the raw-interrupt latch.
5. Engineering tradeoffs
The deliverable is the map plus the why of each register's kind — and above all, why GPIODATA pays a kilobyte of address space to buy atomicity. Memorise the shape: each register's kind dictates its read value, its write effect, and its hardware-update behaviour.
| Register | Kind | Why this kind / the tradeoff it buys |
|---|---|---|
| GPIODATA | RW, address-masked, aliased 0x000–0x3FC | Spends 1 KB of address space so the address can carry a per-bit mask — buying atomic, race-free single-pin set/clear with no RMW. The cost (address space) is trivial; the benefit (no RMW race from one master, no disturbance of hardware-driven input bits) is large. The signature PL061 design choice. |
| GPIODIR | RW | Plain config: one bit per pin selects pad output-enable. No atomicity hazard — direction rarely changes under contention — so a normal RW register is correct; no mask needed. |
| GPIOIS / GPIOIBE / GPIOIEV | RW | Interrupt-detect configuration, written once at setup. RW is sufficient; these gate the detect logic combinationally. |
| GPIOIE | RW | The enable mask. RW so firmware can mask/unmask pins; gating happens as MIS = RIS & IE, keeping enable orthogonal to raw detection. |
| GPIORIS | RO | Hardware-owned raw status. Software must not be able to set a raw interrupt, so writes are ignored — RO is the safety property. |
| GPIOMIS | RO | Pure function of other state (RIS & IE); there is nothing to store, so it is RO/derived — a read-only view, never written. |
| GPIOIC | W1C | Write-1-to-clear so a clear of pin 3 cannot accidentally clear pin 5; W1C also makes the clear idempotent and per-bit. Edge interrupts clear; level interrupts persist while the level holds — encoded in the kind. |
| GPIOAFSEL | RW | Mux select between GPIO and a peripheral function on shared pads. RW, set at pinmux configuration time. |
The throughline: GPIODATA trades address space for atomicity, and every other register's kind encodes a correctness property — RO so software cannot forge a raw interrupt, W1C so a clear is per-bit and idempotent, RW where contention is not a hazard. The one deliberate non-obvious cost is GPIODATA's address footprint: 256 aliases of an 8-bit register. You pay it because the alternative — a single GPIODATA address plus software RMW — reintroduces exactly the race the part exists to avoid. (A modern alternative is a separate SET register and CLR register, as on many Cortex-M GPIOs; the PL061 instead folds set-and-clear into one masked-write address, which also lets a single write set some pins and clear others atomically.)
6. Common RTL mistakes
7. Debugging scenario
The signature GPIO field bug is an interrupt that will not clear — firmware writes GPIOIC, returns from the ISR, and is immediately re-entered, looping forever. It is the most common PL061 integration failure, and it splits into two distinct root causes that look identical from software.
- Observed symptom: the GPIO interrupt fires continuously. The ISR reads GPIOMIS, identifies pin 3, does its work, writes
1to GPIOIC bit 3, and returns — and the CPU re-enters the same ISR on the next instruction. GPIOMIS still shows pin 3 set. To firmware it looks as if the W1C clear "did nothing." - Waveform / register clue: capture GPIORIS, GPIOIE, GPIOMIS and GPIOIS around the IC write. In the level-sense case (GPIOIS bit 3 = 1), GPIORIS bit 3 does drop on the IC write — for one cycle — and then re-asserts on the very next cycle because the pin is still at the active level GPIOIEV selected; the level source is still present, so the raw status re-latches. In the enable case, GPIORIS bit 3 was never the problem — GPIOMIS is
RIS & IE, and IE bit 3 is set, so masking is correct — but the actual fault is that the wrong pin's IC bit was written (an off-by-one in the bit position), so pin 3's raw event was never cleared at all. - Root cause: for the level-sense variant, GPIOIC genuinely cannot clear a level interrupt whose source still holds — that is correct hardware behaviour, not a bug in the slave; the bug is in firmware's expectation. For the wrong-bit variant, the IC write targeted the wrong bit because firmware computed the mask as
(1 << irq_num)against a mis-numbered pin, or wrote to a stalependingvalue. Both present as "the interrupt won't clear," but one is a level that must have its source removed and the other is a clear aimed at the wrong bit. - Correct handling: distinguish them by GPIOIS. If the pin is level-sensitive, the ISR must remove or de-assert the source (or reconfigure to edge sense) before the clear has any lasting effect — clearing IC is futile while the level holds. If edge-sensitive, the W1C clear works, so verify the bit position: read GPIOMIS to get the masked pending set and write exactly those bits back to GPIOIC:
Azvya Education Pvt. Ltd.VLSI MentorSnippet
// ISR clear, done right: clear exactly the masked-pending edge interrupts. logic [7:0] pending = gpio_mis; // RIS & IE — what actually fired gpio_ic_write = pending; // W1C: one write clears every fired edge bit // For any level-sensitive pin still asserting, IC will re-latch next cycle // until the source level is removed — that is correct, not a slave bug. - Debug habit: when a GPIO interrupt "won't clear," your first read is GPIOIS for the offending pin. Level-sensitive and still-asserting? The clear is working; remove the source. Edge-sensitive? Then the clear is real, so check the bit position of the IC write against GPIOMIS — an off-by-one in
1 << pinis the usual culprit. Never assume the W1C is broken before you have ruled out a held level and a mis-aimed bit.
8. Verification perspective
GPIO verification splits into two distinct obligations: prove the address-mask atomicity of GPIODATA (a safety property — an unmasked bit must never change), and prove the interrupt pipeline (RIS latches, IE gates, IC clears edges, levels persist). Both reduce to a few named assertions bound to the slave.
- Atomicity is a per-bit "unmasked bits never change" property. The core SVA proves that a GPIODATA write only ever updates the masked bits — every bit whose mask address bit is 0 must be unchanged across the access. This is the property the whole register exists to guarantee:
Azvya Education Pvt. Ltd.VLSI MentorSnippet
// Masked write changes ONLY the masked bits: any bit with PADDR[2+i]==0 is stable. genvar gi; generate for (gi = 0; gi < 8; gi++) begin : g_mask assert property (@(posedge pclk) disable iff (!presetn) (psel && penable && pwrite && pready && (paddr[11:10]==2'b00) && !paddr[2+gi]) |=> $stable(gpio_data[gi]) // unmasked bit i must NOT change on a masked write ); end endgenerate // And the masked bits DO take the written value (liveness of the intended update): assert property (@(posedge pclk) disable iff (!presetn) (psel && penable && pwrite && pready && (paddr[11:10]==2'b00) && paddr[2]) |=> (gpio_data[0] == $past(pwdata[0])) // mask bit 0 set -> bit 0 updated ); - The interrupt clear is a W1C-clears-the-masked-bit property. Prove that writing 1 to a GPIOIC bit clears that pin's raw edge interrupt — and, paired, that a level interrupt whose source still holds re-asserts (so the test does not falsely expect a level to stay clear):
Azvya Education Pvt. Ltd.VLSI MentorSnippet
// GPIOIC W1C clears an EDGE raw interrupt: write 1 to IC bit i, RIS[i] drops. assert property (@(posedge pclk) disable iff (!presetn) (psel && penable && pwrite && (paddr[11:0]==12'h41C) && pwdata[3] && !gpio_is[3]) |=> !gpio_ris[3] // edge-sensitive pin 3: cleared by the W1C ); // MIS is exactly RIS & IE at all times (the derived-register contract). assert property (@(posedge pclk) disable iff (!presetn) gpio_mis == (gpio_ris & gpio_ie) ); - Coverage must reach the corner masks, not just one. A general "GPIODATA was written" bin is silent about which mask. Cover the boundary masks explicitly —
0x00(write nothing),0xFF(write all eight), and a single-bit mask (the atomic-single-pin case) — plus the cross of "mask bit set × the other bits stable," because the atomicity property is only meaningfully exercised when some bits are masked off. And cover the level-versus-edge clear distinction: an IC write with GPIOIS set (level, must persist) and clear (edge, must drop), so the persist-while-level-holds path is reached.
The point: GPIODATA needs a per-bit safety assertion that the unmasked bits are stable (the atomicity guarantee), the interrupt path needs a W1C-clears-edge / level-persists pair plus the MIS == RIS & IE derived-register check, and coverage must hit the boundary masks and the level/edge clear split — because a single "it was written" bin proves none of the things that actually matter.
9. Interview discussion
"How does the PL061 GPIO let you set one pin without a read-modify-write?" is a favourite SoC-integration question because the address-masked GPIODATA is a genuinely elegant mechanism, and a strong answer shows you understand why it removes a race, not just that the register exists.
Lead with the mechanism: GPIODATA is aliased across 0x000–0x3FC, and PADDR[9:2] is a per-bit mask — bit i of the mask is PADDR[2+i]. A write updates data bit i only if its mask bit is 1, then sets it to PWDATA[i]; bits with mask 0 are physically not clocked. Then land the why: this collapses the classic three-step read / OR-in-the-bit / write-back — which has a window where the hardware can change another pin between the read and the write-back, losing that change — into a single atomic APB write whose hardware effect is exactly if (mask[i]) data[i] <= pwdata[i]. No read, nothing to merge, no race; one write can even set some pins and clear others atomically. The depth flourishes are three: the read is masked too (a read through the mask returns only the selected bits, zeros elsewhere — so "is pin 3 set?" is one read), it is the address not PSTRB that carries the bit mask (PSTRB masks bytes; this masks bits), and it costs 1 KB of address space to buy the atomicity — a trade the part makes deliberately. Closing with the interrupt-clear nuance — "and the other classic GPIO gotcha is that GPIOIC's W1C clears edge interrupts but a level interrupt re-asserts while its source holds, so 'the interrupt won't clear' is usually a held level, not a broken clear" — signals you have actually integrated and debugged a GPIO, not just read the TRM.
10. Practice
- Compute the mask address. GPIO base is
0x4002_0000. Give the address firmware writes to set only pin 5 (and thePWDATAvalue), and the address to write all eight pins at once. - Show the atomicity. With
gpio_data = 8'b0010_0000(pin 5 set by hardware as an input), firmware writes0xFFto the mask-bit-3 address. State the resultinggpio_dataand explain why pin 5 is untouched in terms of the per-bit write-enable. - Write the safety SVA. Write the assertion that a masked GPIODATA write leaves every unmasked data bit
$stable, and state which bug it catches that a "data was written" cover bin does not. - Diagnose the stuck interrupt. Firmware writes GPIOIC for pin 2 and the interrupt immediately re-fires. Give the two distinct root causes and the single register you read first to tell them apart.
- Read vs strobe. Explain why the GPIODATA bit mask is carried in
PADDR[9:2]and not inPSTRB, and whatPSTRBwould and would not let you do here.
11. Q&A
12. Key takeaways
- GPIODATA is the signature PL061 feature: one 8-bit register aliased across
0x000–0x3FC, wherePADDR[9:2]is a per-bit mask (biti↔PADDR[2+i]). A write updates only the masked bits; a read returns only the masked bits, zeros elsewhere. - The address mask removes the read-modify-write race because unmasked bits are physically never written — there is no read, no merge, no window in which a hardware-driven input bit can be clobbered. "Set pins 3 and 1" is one atomic APB write, and one write can set some pins and clear others at once.
- It is the address that carries the bit mask, not
PSTRB(which masks bytes). The cost is 1 KB of address space — 256 aliases of an 8-bit register — a deliberate trade for atomicity. - Every other register's kind encodes a correctness property: GPIODIR (RW direction), GPIOIS/GPIOIBE/GPIOIEV (RW detect config), GPIOIE (RW enable), GPIORIS (RO — software cannot forge a raw interrupt), GPIOMIS (RO, derived = RIS & IE), GPIOIC (W1C — per-bit, idempotent clear), GPIOAFSEL (RW pinmux).
- "The interrupt won't clear" is usually a held level, not a broken W1C. GPIOIC clears edge interrupts; a level interrupt re-asserts while its source holds — read GPIOIS first to tell the two apart, then either remove the source or fix the IC bit position.
- Verification reduces to named properties: a per-bit "unmasked bits are
$stable" safety assertion for GPIODATA atomicity, a W1C-clears-edge / level-persists pair plusMIS == RIS & IEfor the interrupt path, and coverage of the boundary masks (0x00,0xFF, single-bit) and the level/edge clear split. Cited against the Arm PrimeCell GPIO (PL061) TRM and AMBA APB (IHI 0024C) §2.1.