Registers API
Subsystem: dev.regs
(hereafter regs
)
This API provides convenient abstractions for generalized work with device registers.
Interacting with registers is one of the primary tasks a driver developer faces. The API addresses common issues with choosing how to access and interpret register layouts in code.
Typically, developers define constants for register offsets relative to a base address, like:
const SOME_REG = 0x100;
const ANOTHER_REG = 0x1F0;
They then write specific read
and write
functions.
Since this is a common task, many aspects can be generalized and pre-implemented. To ensure uniform and clean code in drivers, this API is presented in the kernel.
Register Group regs.Group
A register group is a structure that abstracts working with a specific group of registers.
With it, you can define the method or mechanism (dev.io.Mechanism
) for accessing registers, as well as the names, locations, and sizes of the registers.
regs.Group
uses comptime
calculations and does not add any runtime overhead.
API
fn regs.Group(
comptime IoMechanism: type,
comptime base: ?comptime_int,
comptime size: ?comptime_int,
comptime regs: []const Register
) type
Returns a register group type.
-
IoMechanism
: type of mechanism used for interacting with registers. -
base
:comptime
base address of the register group.This parameter is optional and can be
null
. It is used only if the base is always static and known at compile time, avoiding runtime address calculations. -
size
: size of the register group.This is also optional and can be
null
. The size can be calculated automatically based on the location and size of all the registers in the group.It is used if the
IoMechanism
implements aninit
function to pass thesize
parameter. -
regs
: a slice of registers contained in the group.Each register in the slice is of type
regs.Register
, see below.
Usage
To use, you need to create an instance of the group structure.
-
For a static group, where the base was specified in
regs.Group
and is known at compile time:const/var my_regs = try MyGroup.init();
The
init
function returns a structure instance, and if theIoMechanism
implements its owninit
function, it is also called to initialize the access mechanism. -
For a dynamic base group, where the base is known at runtime:
const/var my_regs = try MyGroup.initBase(base);
base
: the base address of the group.
The
initBase
function returns a structure instance, and if theIoMechanism
implements its owninit
function, it is also called to initialize the access mechanism. In this case, the base returned by theIoMechanism.init
function is used.
For a dynamic base group, the structure stores only one field of type IoMechanism.Address
. If the base address is known at compile time, the group structure stores no fields and takes up no memory.
When io.Group
is called, an enumeration of register names is generated, which is then used when calling read/write functions.
Interacting with registers
-
Reading:
fn read(member) RegIntType
Reads a register.
member
: the register name from the enumeration, e.g.,.reg_name
.
RegIntType
is the register type defined inregs.Register
.IoMechanism
ensures that data is read at a properly aligned address of the size defined byIoMechanism.DataType
. However, the register itself may be larger or smaller, soRegIntType
represents the specific register type, and the reading process accounts for register offset and size.fn get(T: type, member) T
Reads a register and converts the read value into type
T
.-
member
: the register name from the enumeration, e.g.,.reg_name
. -
T
: the return type.The read numeric value is cast into type
T
using@bitCast
.
This function is useful if you need to read a register and get its representation in a specific type, for example, to work with register bit fields.
-
Writing:
fn write(member, data: RegIntType) void
Writes to a register.
member
: the register name from the enumeration, e.g.,.reg_name
.data
: the value to write.
Similar to reading, writing takes register offset and size into account, allowing writes to misaligned or non-multiple-of-size registers.
fn set(member, data: anytype) void
Writes to a register, casting the data to a numeric value.
-
member
: the register name from the enumeration, e.g.,.reg_name
. -
data
: the value to write.The
data
value is cast into the target register typeRegIntType
using@bitCast
.
This function is useful when you need to write an entire user-defined structure to a register, for example, after initially obtaining the structure using
get
.
Register regs.Register
A structure used to provide register information to io.Group
. It allows you to specify some details.
fn regs.reg(
comptime name: [:0]const u8,
comptime offset: comptime_int,
comptime Type: ?type,
comptime access: Access,
) Register
-
name
: the register name. -
offset
: the offset relative to the base address of the group. -
Type
: the register type, used asRegIntType
.This is optional and can be
null
.The
Type
must be an unsigned integer:u<x>
, wherex
is the bit width. -
access
: access mode for the register, can be:.rw
,.read
,.write
.Used to enhance security, preventing unauthorized read or write operations for the register. A compilation error is generated when an invalid operation is called.
rw
: read and write.read
: read-only.write
: write-only.
Additionally, register information can be automatically generated based on a user-defined structure as a layout. Use the regs.from(...)
function for this.
fn regs.from(comptime Layout: type) []const Register
Generates registers information and returns a slice of []const regs.Register
.
-
Layout
: the name of the user-defined structure.The layout can be a
struct
,packed struct
,extern struct
, orunion
.Each field in this structure is interpreted as a separate register. The register name corresponds to the field name, the register offset is the field's offset within the structure, and the field's type is the register type.
Fields whose names start with an underscore
_
are ignored.To specify access mode
access
, use special types for structure fields:-
regs.ReadOnly(u<x>)
: for read-only registers. -
regs.WriteOnly(u<x>)
: for write-only registers.u<x>
: the type, an unsigned integer wherex
is the bit width.For
packed
orextern
structs, use the postfixP
orE
:regs.ReadOnlyP(u32)
,regs.WriteOnlyE(u32)
, etc.By default, other registers are read-write.
-
If Layout
is a union
, then the Layout
itself is interpreted as a group of Layout
s. In this case, each union
field allows you to define its own set of unique registers with their own names and offsets.
note
However, register names must not be duplicated.
Example:
const Layout = packed union {
regs1: packed struct {
address_reg: u64,
control_reg: u16,
id_reg: u16
},
regs2: packed struct {
_rsrdv: u64, // Ignored
data_reg: u32,
status_reg: u32
}
};
Examples
At first glance, the abstraction may seem overly complicated. However, it is quite simple to use in practice. To better understand, examples are provided below.
Manually Defining Registers
const reg = dev.regs.reg;
const uart_base = 0x3f8;
const UartRegs = dev.regs.Group(
dev.io.IoPortsMechanism(
"uart 8250/16450/16550",
.byte
), // IoMechanism
uart_base, // Base
null, // Size
// Registers slice
&.{
// DLAB == 0
reg("data", 0x0, null, .rw),
reg("intr_enable", 0x1, null, .rw),
// DLAB == 1
reg("div_lo", 0x0, null, .rw),
reg("div_hi", 0x1, null, .rw),
reg("intr_id", 0x2, null, .read),
reg("fifo_ctrl", 0x2, null, .write),
reg("line_ctrl", 0x3, null, .rw),
reg("modem_ctrl", 0x4, null, .rw),
reg("line_status
", 0x5, null, .read),
reg("modem_status", 0x6, null, .read),
reg("scratch", 0x7, null, .rw),
}
);
Automatic Registers Layout Based on a Structure
The code is similar to the manual register example.
const UartRegs = dev.regs.Group(
dev.io.IoPortsMechanism(
"uart 8250/16450/16550",
.byte
), // IoMechanism
uart_base, // Base
null, // Size
// Registers slice
dev.regs.from(UartRegsLayout)
);
// Layout `union`
const UartRegsLayout = packed union {
dlab_0: packed struct {
data: u8,
intr_enable: u8,
fifo_ctrl: dev.regs.WriteOnlyP(u8);
line_ctrl: u8,
modem_ctrl: u8,
line_status: dev.regs.ReadOnlyP(u8),
modem_status: dev.regs.ReadOnlyP(u8),
scratch: u8
},
dlab_1: packed struct {
div_lo: u8,
div_hi: u8
},
intr: packed struct { // Just for `intr_id` register.
_offset: u16,
intr_id: dev.regs.ReadOnlyP(u8);
},
};
Usage
This example shows how a previously defined group of registers can be used.
const regs = UartRegs{};
pub fn init() !void {
// Call `init` at runtime to trigger `dev.io.request`
// which is used by `dev.io.IoPortsMechanism`.
_ = try UartRegs.init();
regs.write(.intr_enable, 0x00); // Disable all interrupts
regs.write(.line_ctrl, 0x80); // Enable DLAB (set baud rate divisor)
regs.write(.div_lo, 0x03); // Set divisor to 3 (low byte) 38400 baud
regs.write(.div_hi, 0x00); // (high byte)
regs.write(.line_ctrl, 0x03); // 8 bits, no parity, one stop bit
regs.write(.fifo_ctrl, 0xC7); // Enable FIFO, clear it, with 14-byte threshold
regs.write(.modem_ctrl, 0x0B); // IRQs enabled, RTS/DSR set
}
pub fn deinit() void {
// Don't forget to release I/O space.
dev.io.release(uart_base, .io_ports);
}