What This Chapter Teaches

Every skalp design starts with two constructs: an entity that declares the hardware interface, and an impl that defines the behavior. If you have written hardware in SystemVerilog or VHDL, you already know the concept — skalp just separates it more explicitly and removes the boilerplate.

By the end of this chapter you will understand:

  • How entity declares ports (inputs and outputs) without any logic
  • How impl contains the actual behavior for that entity
  • The built-in types: clock, reset, nat[N] (unsigned N-bit), bit[N] (N-bit vector)
  • How signal declares internal state inside an impl
  • How on(clk.rise) defines sequential logic (registered, edge-triggered)
  • How bare assignments like name = expr define combinational logic
  • That skalp has no wire/reg distinction — the compiler determines what is registered and what is combinational from how you use it
  • That forward references work for combinational signals — you can use a signal before you define it

These are the building blocks of every skalp design. The UART project you build across this tutorial starts here with a simple counter.


Standalone Example: 8-Bit Counter

Let us build a counter with an enable input and an overflow output. This is small enough to see every piece of the entity/impl pattern at once.

Create a file called src/counter.sk:

// An 8-bit counter with enable and overflow detection.
//
// When enable is high, the counter increments on every rising
// clock edge. When it wraps from 255 back to 0, the overflow
// output pulses high for one cycle.

entity Counter {
    in clk: clock,
    in rst: reset,
    in enable: bit[1],
    out count: nat[8],
    out overflow: bit[1]
}

impl Counter {
    // Internal state: the register that holds the count value.
    // "signal" declares a value that persists across clock cycles
    // when assigned inside an on() block.
    signal count_reg: nat[8]

    // Sequential logic — this block runs on every rising edge of clk.
    // Everything assigned inside on(clk.rise) becomes a register.
    on(clk.rise) {
        if rst {
            count_reg = 0
        } else if enable {
            count_reg = count_reg + 1
        }
    }

    // Combinational assignments — these are continuous, not clocked.
    // They drive the output ports directly from expressions.
    // No "assign" keyword needed, no wire declaration needed.
    count = count_reg

    // overflow is high when the counter is at max AND enabled,
    // meaning it will wrap on the next clock edge.
    // This is a forward reference to nothing special — combinational
    // signals can reference each other in any order.
    overflow = (count_reg == 255) & enable
}

What Is Happening Here

entity Counter declares the interface. It lists every port with a direction (in or out) and a type. There is no logic here — only the contract that this block of hardware exposes to the outside world. Ports are separated by commas.

impl Counter contains the behavior. Inside it you write signals, sequential blocks, and combinational assignments. The impl must satisfy every output port declared in the entity — if you forget to drive overflow, the compiler tells you.

signal count_reg: nat[8] declares internal state. This is neither a wire nor a register by declaration. It becomes a register because it is assigned inside on(clk.rise). If you assigned it outside the on block, it would be combinational. The compiler makes the distinction based on usage, not declaration.

on(clk.rise) is the sequential block. It is equivalent to always_ff @(posedge clk) in SystemVerilog. Every assignment inside this block creates registered logic — the value updates on the clock edge, not continuously. You can have multiple on blocks in a single impl if needed.

count = count_reg and overflow = ... are combinational assignments. They sit outside any on block, so they are continuous — they update whenever their inputs change, like assign in SystemVerilog. No keyword is needed; a bare name = expr at the impl level is combinational.

Types You Have Seen

TypeMeaning
clockA clock signal. Used with on(clk.rise).
resetA reset signal. Can be used directly in if rst.
nat[N]An unsigned integer that fits in N bits. Range: 0 to 2^N - 1.
bit[N]An N-bit vector. No numeric interpretation — just bits.

nat[8] and bit[8] are both 8 bits wide, but nat[8] carries the semantic meaning “this is an unsigned number” while bit[8] means “this is a bag of bits.” Use nat[N] for counters, addresses, and arithmetic. Use bit[N] for flags, masks, and data that you shift or slice.

Coming from SystemVerilog?

The mapping is straightforward:

SystemVerilogskalpNotes
moduleentity + implskalp separates interface from behavior
always_ff @(posedge clk)on(clk.rise)Same semantics, less punctuation
logic [7:0] countsignal count: nat[8]No wire vs. reg — compiler decides
assign overflow = ...overflow = ...No keyword needed for combinational
[7:0] (8 bits, 0 to 7)nat[8] (8-bit unsigned)Width is the number, not max index
No forward referencesForward references workCombinational signals have no temporal order

The biggest conceptual shift: in SystemVerilog you must decide wire or reg when you declare a signal. In skalp, you declare signal and the compiler infers whether it is registered (assigned in on) or combinational (assigned outside on). This eliminates an entire class of declaration/usage mismatch errors.


Running Project: Your First Build

The counter above is the first piece of the UART project. Every chapter adds files to the same uart-tutorial project you created during installation. Let us verify that it compiles and simulates.

Your project structure should look like this:

uart-tutorial/
  skalp.toml
  src/
    counter.sk

The skalp.toml was created by skalp new and contains:

[package]
name = "uart-tutorial"
version = "0.1.0"

[build]
top = "Counter"

Set the top field to Counter so the toolchain knows which entity is the design root.

Building

Run the build command from the project root:

skalp build

If everything is correct, you will see output like:

   Compiling uart-tutorial v0.1.0
   Analyzing Counter
       Built Counter -> build/counter.sv

The compiler parses counter.sk, type-checks it, lowers it through HIR and MIR, and generates synthesizable SystemVerilog in the build/ directory. You can inspect build/counter.sv to see what the compiler produced — it will be a straightforward module with always_ff and assign statements.

Viewing Waveforms

To capture waveforms for debugging, add an export_waveform call at the end of a test:

tb.export_waveform("build/counter.skw.gz").unwrap();

This writes a .skw.gz file (skalp’s native compressed waveform format) that you can open in the skalp VS Code extension. You will see count ramping up, overflow pulsing at 255, and the wrap-around behavior.

Common Errors

If you see error: output port 'overflow' is never driven, you forgot the combinational assignment for the overflow port. Every output declared in the entity must be assigned somewhere in the impl.

If you see error: signal 'count_reg' used before declaration, check that the signal line appears before the on(clk.rise) block. Signal declarations must precede their first use in sequential blocks. (Combinational signals can appear in any order, but signal declarations cannot.)


Testing Your Design

Every hardware design needs a testbench — code that drives inputs and checks outputs. In Chapter 10, we build a complete test suite for the final UART. But you can start testing right now with Rust’s built-in test framework.

Here is the testbench for the counter from tests/counter_test.rs:

use skalp_testing::Testbench;

#[tokio::test]
async fn test_counter_counts() {
    let mut tb = Testbench::with_top_module("src/counter.sk", "Counter")
        .await.unwrap();
    tb.reset(2).await;

    // Counter should start at 0 after reset
    tb.expect("count", 0u32).await;

    // Enable counting
    tb.set("enable", 1u8);

    // Count up from 1 to 10
    for i in 1..=10u32 {
        tb.clock(1).await;
        tb.expect("count", i).await;
    }
}

#[tokio::test]
async fn test_counter_overflow() {
    let mut tb = Testbench::with_top_module("src/counter.sk", "Counter")
        .await.unwrap();
    tb.reset(2).await;
    tb.set("enable", 1u8);

    // Run to just before overflow (8-bit counter wraps at 256)
    tb.clock(255).await;
    tb.expect("count", 255u32).await;

    // One more cycle — should wrap to 0 and assert overflow
    tb.clock(1).await;
    tb.expect("count", 0u32).await;
    tb.expect("overflow", 1u32).await;
}

The pattern is simple: create a testbench with Testbench::with_top_module(), reset the design, drive inputs with tb.set(), advance time with tb.clock(n).await, and check outputs with tb.expect().await. Run it with:

cargo test

Exercise: Add a test_counter_disable test that enables counting to 5, disables it for 10 cycles, then verifies the count is still 5.


Quick Reference

ConceptSyntaxExample
Entity declarationentity Name { ... }entity Counter { in clk: clock, out count: nat[8] }
Implementationimpl Name { ... }impl Counter { ... }
Input portin name: typein enable: bit[1]
Output portout name: typeout overflow: bit[1]
Internal statesignal name: typesignal count_reg: nat[8]
Sequential logicon(clk.rise) { ... }Assignments inside become registers
Combinational logicname = exproverflow = (count_reg == 255) & enable
Clock typeclockin clk: clock
Reset typeresetin rst: reset
Unsigned integernat[N]nat[8] = 8-bit unsigned (0..255)
Bit vectorbit[N]bit[1] = single bit flag
Comment//// this is a comment

Next: State Machines

The counter is a single-state design — it does one thing on every clock edge. Real hardware needs to do different things depending on where it is in a sequence. In Chapter 2, you will build state machines using if-else chains inside on(clk.rise), starting with a traffic light controller and then building the UART transmitter — the first real piece of the UART peripheral.

You will learn how to:

  • Encode FSM states as integer values in a signal
  • Use baud rate counters to time serial bit transmission
  • Shift data out one bit at a time with a shift register
  • Structure state transitions cleanly with nested if-else
  • Use forward references for combinational tick signals

Continue to Chapter 2: State Machines – UART Transmitter.