PSEL and PENABLE
You will learn
How the APB two-phase handshake works, and what the compiler emits for the bridge.
APB moves a transfer in two phases: setup, where PSEL names the servant and the address settles, and access, where PENABLE rises and the transfer completes. The spec's ApbTransfer enum carries the kinds, and the bench of the twelfth lesson drives write and read through them. The recording lowers the APB bridge spec to synthesizable Verilog: the port list, the wiring, the decode.
Try it
In the recording, find the bridge's port list; then in the spec frame find the ApbTransfer kinds and say which phase PENABLE names.

The APB bridge spec lowered to synthesizable Verilog: port list, PSEL and PENABLE wiring, the address decode.
specs/fpga/apb_bridge.t27
// SPDX-License-Identifier: Apache-2.0
// t27/specs/fpga/apb_bridge.t27
// APB (Advanced Peripheral Bus) Bridge Specification for Trinity T27 FPGA HIR
// Register-mapped peripheral bridge for low-bandwidth peripherals
// Uses flat arrays + count fields (parser-compatible)
// phi^2 + 1/phi^2 = 3 | TRINITY
module ApbBridge {
// === APB bus width constants ===
pub const APB_ADDR_WIDTH : u32 = 32;
pub const APB_DATA_WIDTH : u32 = 32;
pub const APB_STRB_WIDTH : u32 = 4;
// === APB transfer kind ===
pub const ApbTransfer = enum(i8) {
idle = 0,
setup = 1,
access = 2,
}
// === APB bridge configuration ===
pub struct ApbConfig {
name : &str,
addr_width : u32,
data_width : u32,
num_peripherals : u32,
base_addr : u32,
addr_mask : u32,
has_pslverr : bool,
has_pprot : bool,
}
// === Peripheral address range ===
pub struct PeripheralMap {
name : &str,
base_addr : u32,
size : u32,
index : u32,
}
// === APB read/write request (for simulation) ===
pub struct ApbRequest {
addr : u32,
wdata : u32,
write : bool,
strb : u32,
valid : bool,
}
// === APB response (for simulation) ===
pub struct ApbResponse {
rdata : u32,
ready : bool,
slverr : bool,
}
// === Constructor helpers ===
fn apb_bridge(name: &str, addr_width: u32, data_width: u32, num_peripherals: u32) -> ApbConfig {
return ApbConfig{
.name = name,
.addr_width = addr_width,
.data_width = data_width,
.num_peripherals = num_peripherals,
.base_addr = 0,
.addr_mask = 0,
.has_pslverr = false,
.has_pprot = false,
};
}
fn apb_bridge_with_error(name: &str, addr_width: u32, data_width: u32, num_peripherals: u32) -> ApbConfig {
return ApbConfig{
.name = name,
.addr_width = addr_width,
.data_width = data_width,
.num_peripherals = num_peripherals,
.base_addr = 0,
.addr_mask = 0,
.has_pslverr = true,
.has_pprot = true,
};
}
fn peripheral_map(name: &str, base_addr: u32, size: u32, index: u32) -> PeripheralMap {
return PeripheralMap{
.name = name,
.base_addr = base_addr,
.size = size,
.index = index,
};
}
fn apb_read_request(addr: u32) -> ApbRequest {
return ApbRequest{
.addr = addr,
.wdata = 0,
.write = false,
.strb = 15,
.valid = true,
};
}
fn apb_write_request(addr: u32, data: u32, strb: u32) -> ApbRequest {
return ApbRequest{
.addr = addr,
.wdata = data,
.write = true,
.strb = strb,
.valid = true,
};
}
fn apb_ok_response(data: u32) -> ApbResponse {
return ApbResponse{
.rdata = data,
.ready = true,
.slverr = false,
};
}
fn apb_error_response() -> ApbResponse {
return ApbResponse{
.rdata = 0,
.ready = true,
.slverr = true,
};
}
// === Query functions ===
fn strb_width(cfg: ApbConfig) -> u32 {
return cfg.data_width / 8;
}
fn addr_bits_for_peripherals(cfg: ApbConfig) -> u32 {
var n : u32 = cfg.num_peripherals;
if (n <= 1) {
return 0;
}
var bits : u32 = 0;
while (n > 1) {
bits = bits + 1;
n = n / 2;
}
return bits;
}
fn peripheral_addr_offset(cfg: ApbConfig, periph_index: u32) -> u32 {
var offset_bits : u32 = addr_bits_for_peripherals(cfg);
return periph_index << offset_bits;
}
fn is_read(req: ApbRequest) -> bool {
return req.valid and req.write == false;
}
fn is_write(req: ApbRequest) -> bool {
return req.valid and req.write;
}
fn apb_port_count(cfg: ApbConfig) -> u32 {
var count : u32 = 0;
count = count + 1;
count = count + 1;
count = count + cfg.addr_width;
count = count + cfg.data_width;
count = count + cfg.data_width / 8;
count = count + cfg.data_width;
count = count + 1;
if (cfg.has_pslverr) {
count = count + 1;
}
if (cfg.has_pprot) {
count = count + 3;
}
return count;
}
fn select_peripheral(cfg: ApbConfig, addr: u32, maps: [16]PeripheralMap, map_count: u32) -> u32 {
var i : u32 = 0;
while (i < map_count) {
var base : u32 = maps[i].base_addr;
var size : u32 = maps[i].size;
if (addr >= base and addr < base + size) {
return i;
}
i = i + 1;
}
return 65535;
}
// === Validation ===
fn validate_apb(cfg: ApbConfig) -> u32 {
var errors : u32 = 0;
if (cfg.name == "") {
errors = errors + 1;
}
if (cfg.addr_width == 0) {
errors = errors + 1;
}
if (cfg.data_width == 0) {
errors = errors + 1;
}
if (cfg.data_width % 8 != 0) {
errors = errors + 1;
}
if (cfg.num_peripherals == 0) {
errors = errors + 1;
}
return errors;
}
fn validate_peripheral_map(m: PeripheralMap) -> u32 {
var errors : u32 = 0;
if (m.name == "") {
errors = errors + 1;
}
if (m.size == 0) {
errors = errors + 1;
}
return errors;
}
// === Tests ===
test apb_bridge_creation
given cfg = apb_bridge("apb0", 32, 32, 4)
then cfg.name == "apb0"
and cfg.addr_width == 32
and cfg.data_width == 32
and cfg.num_peripherals == 4
and cfg.has_pslverr == false
test apb_bridge_with_error
given cfg = apb_bridge_with_error("apb1", 32, 32, 8)
then cfg.has_pslverr == true
and cfg.has_pprot == true
test strb_width_32bit
given cfg = apb_bridge("apb0", 32, 32, 4)
then strb_width(cfg) == 4
test strb_width_16bit
given cfg = apb_bridge("apb0", 16, 16, 4)
then strb_width(cfg) == 2
test addr_bits_for_1_peripheral
given cfg = apb_bridge("apb0", 32, 32, 1)
then addr_bits_for_peripherals(cfg) == 0
test addr_bits_for_4_peripherals
given cfg = apb_bridge("apb0", 32, 32, 4)
then addr_bits_for_peripherals(cfg) == 2
test addr_bits_for_8_peripherals
given cfg = apb_bridge("apb0", 32, 32, 8)
then addr_bits_for_peripherals(cfg) == 3
test read_request
given req = apb_read_request(256)
then is_read(req) == true
and is_write(req) == false
and req.addr == 256
test write_request
given req = apb_write_request(256, 42, 15)
then is_read(req) == false
and is_write(req) == true
and req.wdata == 42
test ok_response
given resp = apb_ok_response(99)
then resp.rdata == 99
and resp.ready == true
and resp.slverr == false
test error_response
given resp = apb_error_response()
then resp.slverr == true
test validate_ok
given cfg = apb_bridge("apb0", 32, 32, 4)
then validate_apb(cfg) == 0
test validate_empty_name
given cfg = apb_bridge("", 32, 32, 4)
then validate_apb(cfg) > 0
test validate_zero_addr
given cfg = apb_bridge("apb0", 0, 32, 4)
then validate_apb(cfg) > 0
test validate_zero_peripherals
given cfg = apb_bridge("apb0", 32, 32, 0)
then validate_apb(cfg) > 0
test validate_peripheral_map_ok
given m = peripheral_map("uart0", 4096, 256, 0)
then validate_peripheral_map(m) == 0
test validate_peripheral_map_no_name
given m = peripheral_map("", 4096, 256, 0)
then validate_peripheral_map(m) > 0
test apb_port_count_basic
given cfg = apb_bridge("apb0", 32, 32, 4)
then apb_port_count(cfg) > 0
// === Invariants ===
invariant addr_width_positive
given cfg = apb_bridge("inv", 32, 32, 4)
assert cfg.addr_width > 0
invariant data_width_byte_aligned
given cfg = apb_bridge("inv", 32, 32, 4)
assert cfg.data_width % 8 == 0
invariant strb_matches_data
given cfg = apb_bridge("inv", 32, 32, 4)
assert strb_width(cfg) == cfg.data_width / 8
invariant num_peripherals_positive
given cfg = apb_bridge("inv", 32, 32, 4)
assert cfg.num_peripherals > 0
invariant validate_non_negative
given cfg = apb_bridge("inv", 32, 32, 4)
assert validate_apb(cfg) >= 0
// === Benchmarks ===
bench validate_latency
measure: nanoseconds to validate_apb(apb_bridge("b", 32, 32, 4))
target: < 100ns
}
// phi^2 + 1/phi^2 = 3 | TRINITY
All lessons
Module 1 · What a bus is
Why a bus exists at all: a conversation on wires, framed and addressed, and who is allowed to talk.
Module 2 · UART
The two-wire bus with no clock: the frame, the divisor that sets the rate, and the status a driver polls.
Module 3 · SPI
The clocked conversation: four modes, a prescaler ladder, and a chip select per servant.
Module 4 · APB
The register bus: PSEL and PENABLE, strobes and wait states, and how many address bits a peripheral count costs.
Module 5 · AXI4
The five channels: address, data and response in both directions, lite or full, bursts and IDs.
Module 6 · Memory
What sits on the far side of every bus: memory maps, port kinds, and latency that a wait state must cover.
Module 7 · Bridges
Why designs grow more than one bus, and the packet bridge that moves work between them.
Module 8 · Ethernet
Frames, the frame check sequence, RGMII timing, and the pre-registered steps of a real bring-up.
Module 9 · The bench
The discipline that guards real hardware: who holds the IO, taking and giving the lock, and what runs next.