A simple system level modeling library
A modern, type-safe system-level hardware modeling library for Cxy, inspired by SystemC but designed to leverage Cxy's native coroutines, compile-time plugins, and strong type system.
Version: 0.1.0 License: MIT Author: Carter Mbotho
The rtl library enables hardware designers to model digital systems at a behavioral level using event-driven simulation. It provides the core primitives for describing concurrent hardware components, their communication through signals, and timing relationshipsall with compile-time safety guarantees and clean, declarative syntax.
Signal[T] types prevent connection mismatchesBit type supports '0', '1', 'X' (unknown), and 'Z' (high-impedance) statesInPort, OutPort, and InOutPort types enforce correct signal flow at compile timeAdd rtl to your Cxy project:
cxy package add rtl
The package includes a required plugin that enables the @rtl::component and related attributes.
Here's a simple counter component that increments on each clock edge:
import { Clock, ns, Signal, Bit, simContext } from "@rtl"
import plugin "rtl" as rtl
@rtl::component
class Counter {
@input clk: Clock
@input reset: Bit
@output count: u32
_value: u32 = 0
@method(clk: "posedge")
func increment() {
if *reset == Bit('1') {
_value = 0
} else {
_value += 1
}
count << _value
}
}
func main() {
var ctx = simContext()
var clk = Clock("clk", 10.ns) // 10ns period clock
var reset = Signal[Bit](Bit('1'))
var count = Signal[u32](0)
clk.start()
var counter = Counter(&clk, reset, count)
// Reset phase
ctx.run(20.ns)
// Release reset and count
reset << Bit('0')
ctx.run(100.ns)
println("Final count: ", count.read())
}
The Time type represents simulation time with natural syntax using literal suffixes:
import { ps, ns, us, ms, sec } from "@rtl"
var delay = 10.ns // 10 nanoseconds
var timeout = 5.us // 5 microseconds
var period = 100.ps // 100 picoseconds
// Time arithmetic
var total = delay + timeout
var scaled = delay * 2
Signals are the primary communication mechanism between hardware components:
import { Signal } from "@rtl"
var dataSignal = Signal[u32](0) // 32-bit signal initialized to 0
dataSignal << 42 // Write value to signal
var value = dataSignal.read() // Read current value
Ports provide typed, directional interfaces to components:
InPort[T] - Read-only input (can't be written inside component)OutPort[T] - Write-only output (can't be read inside component)InOutPort[T] - Bidirectional port (can read and write)The Bit type models digital logic values including unknown and high-impedance states:
import { Bit, Bit0, Bit1, BitX, BitZ } from "@rtl"
var low = Bit('0') // Logic low
var high = Bit('1') // Logic high
var unknown = Bit('X') // Unknown/uninitialized
var highz = Bit('Z') // High-impedance (tri-state)
// Bitwise operations handle all four states correctly
var result = low & high // Bit('0')
var or_result = low | high // Bit('1')
var xor_result = low ^ high // Bit('1')
var not_result = ~high // Bit('0')
BitVector[N] represents fixed-width vectors of N bits with full four-state logic:
import { BitVector } from "@rtl"
// Create from integer
var addr = BitVector[8](0x3F)
// Parse from strings (binary/hex, with X/Z states)
var data = BitVector[8]("0b1010_0101")
var mask = BitVector[16]("0xABCD")
var bus = BitVector[4]("0b10XZ")
// Type-safe conversion
var value = addr as u64
var signed = addr as i64
// Bitwise and arithmetic
var result = addr & mask
var sum = addr + mask
// Indexing and slicing
var bit0 = addr.[0]
addr.[7] = Bit('1')
var lowNibble = addr.slice[0, 4]()
// String conversion
var binary = addr.toBinary()
var hex = addr.toHex()
BitSignal[N] provides signals with automatic slice change propagation:
import { BitSignal, BitVector } from "@rtl"
var dataReg = BitSignal[16](0xABCD)
// Zero-copy slices
var lowByte = dataReg.slice[0, 8]()
var highByte = dataReg.slice[8, 16]()
// Parent write notifies affected slices
dataReg << BitVector[16](0x1234)
// Slice write updates parent
lowByte << BitVector[8](0xFF) // dataReg becomes 0x12FF
// Nested slices work
var nibble = lowByte.slice[0, 4]()
nibble << BitVector[4](0xA)
// Smart change detection - only affected signals fire events
highByte << BitVector[8](0xAB) // lowByte unchanged, no event
The Clock class automatically generates periodic signals:
import { Clock, ns } from "@rtl"
var clk = Clock("sys_clk", 10.ns) // 10ns period (100MHz)
clk.start() // Begin generating clock edges
Use the @rtl::component attribute to define hardware modules. The plugin transforms annotated fields and methods:
@rtl::component
class MyComponent {
@input clk: Clock // Becomes InPort[Clock]
@input data: u32 // Becomes InPort[u32]
@output result: u32 // Becomes OutPort[u32]
_state: u32 = 0 // Private internal state
@method(clk: "posedge") // Triggered on rising clock edge
func process() {
_state += *data
result << _state
}
}
Supported trigger specifications:
"posedge" - Rising edge"negedge" - Falling edge"anyedge" - Either edge@method(rst, en) waits for ANY event (OR)@method(_and, rst, en) waits for ALL events (AND)@method(rst, clk: "posedge") combines events and edgesUse Event for custom synchronization between processes:
import { Event, wait } from "@rtl"
var dataReady = Event()
async {
// Producer process
wait(&dataReady)
println("Data is ready!")
}
// Signal the event
dataReady.notify()
EventGroup allows waiting on multiple events with ANY or ALL semantics:
import { EventGroup, Event } from "@rtl"
var evt1 = Event()
var evt2 = Event()
var evt3 = Event()
// ANY semantics (OR) - resume when any event fires
var anyGroup = EventGroup(evt1, evt2, evt3)
async {
anyGroup.waitAny()
println("At least one event fired!")
}
// ALL semantics (AND/barrier) - resume when all events fire
var allGroup = EventGroup(evt1, evt2)
async {
allGroup.waitAll()
println("Both events have fired!")
}
// Use in @method triggers
@rtl::component
class MultiEventComponent {
@input rst: bool
@input en: bool
// ANY semantics - fires when rst OR en changes
@method(rst, en)
func onAnyChange() { }
// ALL semantics - fires when BOTH rst AND en change
@method(_and, rst, en)
func onAllChange() { }
}
The SimContext manages simulation time and event scheduling:
import { simContext, ns } from "@rtl"
var ctx = simContext()
// Run for specified duration
ctx.run(100.ns)
// Check current time
var t = ctx.currentTime()
Export signals for viewing in waveform tools (GTKWave, etc.):
import { Vcd } from "@rtl"
import plugin "rtl" as rtl
var clk = Clock("clk", 10.ns)
var data = Signal[u32](0)
// Create trace file
var trace = rtl::vcd("simulation.vcd", clk)
// Add signals to trace
rtl::trace(trace, data)
// Run simulation (trace is automatically recorded)
simContext().run(1000.ns)
The library is structured in three layers:
src/*.cxy) - Core types and simulation kernel- Time, Bit, BitVector, BitSignal, Signal, Clock - Event queue and delta cycle manager - SimContext (simulation scheduler) - Slice propagation and change detection
src/plugin/*.c) - Compile-time code generation - Transforms @rtl::component classes
- Generates port bindings and process wrappers
- Validates component structure
- Declarative attributes (@input, @output, @method)
- Natural Cxy syntax
- Compile-time safety
Run the test suite:
cxy package test
Tests cover:
Familiar to SystemC Users - Adopts proven concepts (modules, ports, signals, delta cycles) from the IEEE 1666 standard.
Leverage Cxy's Strengths - Native coroutines replace macros, plugins eliminate boilerplate, type system ensures safety.
Correct by Construction - Compile-time validation catches errors before runtime:
Efficient Simulation - Binary heap event queue, lightweight coroutines, minimal overhead in critical paths.
| Feature | SystemC | rtl (Cxy) |
|---|---|---|
| Process model | Macros (SC_METHOD, SC_THREAD) |
Native coroutines with @method |
| Module definition | SC_MODULE macro |
@rtl::component attribute |
| Port types | Templates (sc_in<T>) |
Generic types (InPort[T]) |
| Sensitivity lists | Manual registration | Auto-generated from @method trigger |
| Signal updates | sc_signal<T> |
Signal[T] with << operator |
| Time literals | sc_time(10, SC_NS) |
10.ns (natural syntax) |
| Code generation | Preprocessor macros | Compile-time AST transformation |
This is an early-stage project. Contributions welcome!
@rtl::component transformationMIT License - see LICENSE file for details.
For questions, issues, or contributions, please visit the project repository.