Many components need a small control and status interface. Software may need to start an operation, provide parameters, read a result, inspect status, or access a memory window. Livt integrates HxS so these operations can be described on the component itself instead of maintained as a separate register-map design.
HxS follows a default-first approach. A basic register needs only @Register. Add addresses, bit positions, names, blocks, or advanced objects only when the public interface requires them.
A First Register Interface
Import the HxS annotations and select a bus protocol on the component:
using Livt.HxS
using Livt.Bus.Axi4Lite
@Interface(BusType="AXI4Lite")
component DeviceControl
{
public enabled: logic
new(bus: flip IAxi4Lite32Master)
{
this.enabled = 0b0
}
@Register
public fn Enable()
{
this.enabled = 0b1
}
}
Writing the function's register invokes Enable(). The constructor makes the bus boundary explicit: the component receives the target side of an AXI4-Lite connection through the IAxi4Lite32Master contract.
This small example relies on the normal defaults:
- the interface uses 32-bit data;
- a default block is created;
- the register ID is derived from the function name;
- the register is placed at the next aligned offset;
- a function without parameters or a return value acts as a command trigger.
This is the preferred starting point. Do not specify metadata merely to repeat the defaults.
Use an Existing HxS File
An existing HxS register contract can be attached to a Livt component with the File parameter:
using Livt.HxS
using Livt.Bus.Axi4Lite
@Interface(File="src/hxs/DeviceControl.hxs", Id="DeviceControlIfc")
component DeviceControl
{
new(bus: flip IAxi4Lite32Master)
{
}
@Register(Id="ControlRegister")
@Data(Id="EnableData")
public fn Enable()
{
// Component behavior
}
}
File is a project-relative path to a .hxs file. The interface, register, data, and other explicit IDs in Livt must match the corresponding objects in that file. This makes an established register contract reusable while the Livt component provides its behavior and tests.
Keep referenced HxS files with the package sources, for example under src/hxs. The normal build, test, package, and vendor workflows then include the contract. AXI4-Lite remains the default bus type; set BusType explicitly when the referenced contract uses Avalon or Wishbone.
Add Livt.Bus
Projects that connect or test the bus directly depend on Livt.Bus:
[dependencies]
Livt.Bus = "0.1.0"
The package provides the bus contracts and AXI4-Lite test components used in this chapter. The HxS annotations are available from Livt.HxS.
Functions as Registers
A function's parameters describe values supplied by a write. Its return value describes data returned by a read.
Command Trigger
A parameterless function without a return value is a command:
@Register
public fn ResetCounters()
{
this.accepted = 0 as uint
this.rejected = 0 as uint
}
A write invokes the function. The written value is not needed unless the function has parameters.
Write Parameters
Function parameters receive register-write data:
@Register
public fn SetThreshold(value: uint)
{
this.threshold = value
}
When a register contains several parameters, their bit positions can be made explicit with @Data:
@Register(Id="Configure")
public fn Configure(
@Data(Id="Mode", Position=0) mode: byte,
@Data(Id="Limit", Position=8) limit: byte
)
{
this.mode = mode
this.limit = limit
}
Use explicit positions for a stable public register layout. For local projects and early prototypes, Livt can infer positions when they are omitted.
Read Results
A return-valued function exposes its result to register reads:
@Register
public fn GetPacketCount() uint
{
return this.packetCount
}
Command and Result
Parameters and a return value can share one command/result register:
@Register(Id="Transform")
@Data(Id="Result", Position=0)
public fn Transform(@Data(Id="Value", Position=0) value: uint) uint
{
return (value * (2 as uint)) + (1 as uint)
}
Writing 5 invokes Transform(5). Reading the register returns 11. The most recent completed result remains readable until another invocation completes.
The write and read layouts are separate, so a parameter and a return value may both begin at bit zero.
Fields as Registers
Fields can be bound directly when a separate function would add no useful meaning:
@Register(Id="Mode")
mode: byte
@Register(Id="Status")
public status: out logic[8]
A directionless field remains stored component state. Register writes update the field, and reads observe its current value. Directed fields follow their declared access direction.
Add @Data when the field needs a specific name, description, position, or access behavior:
@Register(Id="Control")
@Data(Id="Enable", Name="Enable output", Position=0)
enabled: logic
Prefer functions for actions with meaningful behavior. Prefer fields for simple configuration and status values.
Names, Values, and Reset Metadata
Metadata makes a register interface easier to understand and consume. An enum binding can define the allowed named values and its reset value:
@Register(Id="Mode")
@Enum(Id="ModeData", Name="Operating mode")
@Value(Id="Off", Name="Off", Value=0)
@Value(Id="Idle", Name="Idle", Value=1)
@Value(Id="Run", Name="Run", Value=2)
@Reset(Id="ModeReset", Name="Idle reset", Value=1)
mode: byte
Use Name and Description for information intended for people and tools. Use stable Id values for identifiers that should remain unchanged when source names are refactored.
Address Maps and Blocks
Without an explicit block, HxS creates one default block and places registers in source order at aligned offsets. This is convenient while an interface is young.
For a published interface, make the layout explicit:
@Interface(BusType="AXI4Lite", AddressBusWidth=16)
@Block(Id="Control", BaseAddress=256, Alignment=16, Size=32)
@Block(Id="Status", BaseAddress=512, Alignment=8)
component ManagedDevice
{
@Register(Id="Command", Block="Control", Offset=8)
command: uint
@Register(Id="Flags", Block="Status", Address=516)
flags: uint
@Register(Id="Version", Block="Status")
version: uint
// ...
}
Offset is relative to the selected block. Address is absolute. Registers without either value continue to use the next available aligned location in their block.
The layout checks IDs, block references, alignment, overlap, block bounds, and the configured address width. Invalid maps are reported before a vendor project is created.
Reserved Bits
Use @Reserved to keep part of a register unavailable:
@Reserved(Id="Gap", Register="Command", Position=8, Width=8)
component CommandDevice
{
@Register(Id="Command")
@Data(Position=0)
command: byte
// ...
}
This register exposes the low byte while bits 8 through 15 remain reserved. Reserved regions are useful when preserving compatibility or leaving room for future fields.
Selected Views
A select value can choose between register or block views:
@Interface(BusType="AXI4Lite")
@Block(Id="Control", Selects=["Bank"])
component BankedControl
{
@Select(Id="Bank")
bank: logic
@Register(Id="Command", Block="Control", Selects=["Bank"])
command: byte
// ...
}
@Select creates the named selector. Selects associates that selector with a block, register, or delegated window. The default selector behavior is read/write; specify Behaviour only when the interface needs a different contract.
Delegated Address Windows
A component can reserve part of its address map for another bus target. The delegated window is exposed as a public bus endpoint:
@Interface(BusType="AXI4Lite", AddressBusWidth=8)
@Block(Id="Control", Size=64)
component DeviceWithMemory
{
@Register(Id="Command", Block="Control", Offset=0)
command: byte
@Delegate(Id="Memory", Block="Control", Offset=32, Size=32)
public memory: IAxi4Lite32Master
new(bus: flip IAxi4Lite32Master)
{
this.command = 0x00
}
}
Addresses 0x20 through 0x3F are forwarded through memory. This keeps the component's control registers and a downstream memory or peripheral under one software-visible address space.
The delegated endpoint type must match the component's selected bus protocol.
Supported Bus Contracts
The annotated component does not need protocol-specific application logic. The bus type and constructor contract define its external connection:
| Bus type | Livt contract |
|---|---|
| AXI4-Lite | Livt.Bus.Axi4Lite.IAxi4Lite32Master |
| Avalon | Livt.Bus.Avalon.IAvalon32Master |
| Wishbone | Livt.Bus.Wishbone.IWishbone32Master |
AXI4-Lite currently has the complete Livt transaction test master. Avalon and Wishbone provide explicit signal contracts and can be connected to matching test or integration components.
Testing with Livt.Bus
Memory-mapped interfaces should be tested through transactions, not only by calling the bound functions directly. This verifies the public register contract that software and other bus masters will use.
using Livt.Bus.Axi4Lite.Testing
@Test
component DeviceControlTest
{
master: Axi4LiteTestMaster
dut: DeviceControl
new()
{
this.master = new Axi4LiteTestMaster()
this.dut = new DeviceControl(this.master)
}
@Test
fn EnablesDeviceThroughRegister()
{
this.master.SetTimeoutTicks(200)
this.master.Write32(0x0, 1 as uint)
assert this.dut.enabled == 0b1
}
}
For readable values, use Read32 or Verify32:
this.master.Write32(0x0, 5 as uint)
this.master.Verify32(0x0, 11 as uint)
A thorough register-interface test should cover:
- reset values and the first read;
- the first and last mapped address;
- command invocation and repeated commands;
- parameter packing and byte enables;
- function results and repeated reads;
- unmapped addresses;
- reserved bits;
- selectors and delegated windows when present.
Reusable Component Boundaries
Keep the bus connection visible in the constructor. A component that declares new(bus: flip IAxi4Lite32Master) clearly communicates that it owns an AXI4-Lite target interface. Tests can connect a Livt master directly, and parent components can pass the same contract without hidden setup.
The bound fields and functions remain normal Livt APIs. Internal component code can call them without going through the bus, while system-level tests exercise the memory-mapped contract.
Vendor Projects
HxS-annotated components use the normal vendor workflow:
livt test
livt vendor vivado-ip
Run the transaction tests before creating the vendor project. Keep explicit IDs, addresses, and bit positions stable once software or another component depends on the interface.
See Vendor Integration for project and IP packaging workflows.
Annotation Reference
Annotation parameter names are case-sensitive. Omit optional parameters when the default behavior is sufficient.
| Annotation | Applies to | Common parameters |
|---|---|---|
@Interface |
Component | File, Id, Name, Description, BusDescription, BusType, AddressBusWidth, DataBusWidth |
@Block |
Component | Id, Name, Description, BaseAddress, Alignment, Size, Selects |
@Register |
Field or function | Id, Block, Name, Description, Offset, Address, Async, Selects |
@Data |
Field, function, or parameter | Id, Name, Description, Position, Behaviour |
@Enum |
Field, function, or parameter | Id, Name, Description, Position |
@Value |
Enum-bound element | Id, Name, Description, Value |
@Reset |
Data or enum binding | Id, Name, Description, Value |
@Reserved |
Component | Id, Register, Name, Description, Position, Width, Behaviour |
@Select |
Field | Id, Name, Description, Behaviour |
@Delegate |
Public bus endpoint field | Id, Block, Name, Description, Offset, Address, Size, Selects |
Use the detailed examples in this chapter before reaching for every parameter. The HxS Objects reference describes the corresponding object vocabulary in more depth.
Design Guidance
- Start with
@Registerand defaults. - Use
Filewhen an existing HxS contract is already the source of the register map. - Add
@Datawhen packing or metadata must be explicit. - Use stable IDs and explicit offsets for published interfaces.
- Keep command functions small and give them intent-revealing names.
- Use fields for simple stored configuration and status.
- Use a delegated window for a genuine downstream address space.
- Test the interface through
Livt.Bustransactions. - Treat the register map as a versioned public API.
Summary
HxS lets a Livt component describe its memory-mapped contract where its fields and functions are defined. Simple interfaces remain concise, while blocks, metadata, reserved ranges, selectors, and delegated windows support larger systems. An explicit Livt.Bus constructor makes the boundary reusable and gives tests the same transaction-level view used by the rest of the system.