Every synchronous digital design is driven by a clock. Flip-flops sample on rising edges, pipelines advance on falling edges, and the entire notion of "what happens next" is defined relative to clock transitions. In SystemC, sc_clock is the built-in clock generator — a first-class citizen of the simulation kernel that generates posedge and negedge events at precise, repeating intervals.

This article covers how sc_clock works internally, all its constructor parameters, how to connect it to module ports, how to use its events in processes, and the common patterns for multi-clock designs.

1. What sc_clock Actually Is

sc_clock is a specialization of sc_signal<bool>. It inherits the full evaluate-update semantics we covered in Delta Cycles in SystemC — reads return the current value, writes go to the new-value buffer, and the signal fires a value_changed_event after update.

What makes sc_clock special is that it drives itself: the kernel schedules timed notifications at the start of elaboration that toggle the signal at the correct intervals. You don't write to sc_clock manually — it runs autonomously once the simulation starts.

Key fact sc_clock is an sc_signal<bool> with a built-in self-toggling mechanism. All sc_signal methods and events work on it: read(), posedge_event(), negedge_event(), value_changed_event().

2. Constructor Parameters

The full sc_clock constructor signature is:

sc_clock(
    const char*  name,
    sc_time      period,
    double       duty_cycle  = 0.5,
    sc_time      start_time  = SC_ZERO_TIME,
    bool         posedge_first = true
);
Parameter Type Default Meaning
name const char* Unique SystemC object name (used in tracing and hierarchy)
period sc_time Full clock period — time from one rising edge to the next
duty_cycle double 0.5 Fraction of the period that the clock is high (0.0–1.0)
start_time sc_time SC_ZERO_TIME Simulation time at which the clock starts toggling
posedge_first bool true If true, clock starts low and first transition is a rising edge

Common clock declarations

// 10 ns period, 50% duty cycle — the standard system clock
sc_clock clk("clk", sc_time(10, SC_NS));

// 10 ns period, 30% duty cycle — high for 3 ns, low for 7 ns
sc_clock clk("clk", sc_time(10, SC_NS), 0.3);

// 10 ns period, delayed start at 5 ns
sc_clock clk("clk", sc_time(10, SC_NS), 0.5, sc_time(5, SC_NS));

// Starts high, first transition is a falling edge
sc_clock clk("clk", sc_time(10, SC_NS), 0.5, SC_ZERO_TIME, false);

3. The Clock Waveform

With the default parameters (10 ns period, 50% duty cycle, posedge_first = true), the clock waveform looks like this:

clk ___┌─────┐_____┌─────┐_____┌─────┐___
clk 0ns    5ns   10ns  15ns  20ns  25ns  30ns
posedge @ 0 ns, 10 ns, 20 ns ... negedge @ 5 ns, 15 ns, 25 ns ...

With a 30% duty cycle (0.3), the clock is high for 3 ns and low for 7 ns per period:

clk (30%) ___┌───┐_______┌───┐_______┌───┐___
. 0ns 3ns   10ns 13ns  20ns 23ns

4. Posedge and Negedge Events

sc_clock (being an sc_signal<bool>) exposes two edge-specific events in addition to the general value_changed_event():

MethodReturnsFires when
clk.posedge_event() const sc_event& Clock transitions from 0 → 1 (rising edge)
clk.negedge_event() const sc_event& Clock transitions from 1 → 0 (falling edge)
clk.value_changed_event() const sc_event& Any clock edge (both posedge and negedge)
clk.posedge() bool True if a posedge occurred in the immediately previous delta
clk.negedge() bool True if a negedge occurred in the immediately previous delta

Using edge events in SC_THREAD

SC_MODULE(Counter) {
    sc_in<bool> clk;
    sc_in<bool> reset;
    sc_out<int>  count;

    void count_proc() {
        count.write(0);
        while (true) {
            wait(clk.posedge_event());      // wait for rising edge
            if (reset.read())
                count.write(0);
            else
                count.write(count.read() + 1);
        }
    }

    SC_CTOR(Counter) {
        SC_THREAD(count_proc);
    }
};

Using edge events in SC_METHOD

SC_MODULE(FlipFlop) {
    sc_in<bool>  clk, d;
    sc_out<bool> q;

    void ff_proc() {
        if (clk.posedge())    // check which edge triggered us
            q.write(d.read());
    }

    SC_CTOR(FlipFlop) {
        SC_METHOD(ff_proc);
        sensitive << clk.pos();   // pos() = shorthand for posedge_event()
    }
};
pos() and neg() shorthand clk.pos() is equivalent to clk.posedge_event() — just shorter. Same for clk.neg() = clk.negedge_event(). Both work in sensitive << lists and wait() calls.

5. Connecting sc_clock to Module Ports

sc_clock connects to module ports using standard port binding in sc_main. Clock ports in modules are declared as sc_in<bool> — not sc_in<sc_clock>. The clock is just a boolean signal as far as the port is concerned.

// In sc_main:
sc_clock clk("clk", sc_time(10, SC_NS));

Counter counter("counter");
counter.clk(clk);          // bind sc_clock to sc_in<bool> port
counter.reset(reset_sig);
counter.count(count_sig);

You can also connect a clock to multiple modules — one sc_clock instance can drive any number of ports:

sc_clock sys_clk("sys_clk", sc_time(10, SC_NS));

cpu.clk(sys_clk);
ram.clk(sys_clk);
dma.clk(sys_clk);         // all three share the same clock object

6. Multiple Clocks

Real SoCs have multiple clock domains — a fast core clock, a slower bus clock, a low-power peripheral clock. SystemC models this directly by instantiating multiple sc_clock objects:

// 1 GHz core clock — 1 ns period
sc_clock core_clk("core_clk", sc_time(1, SC_NS));

// 200 MHz bus clock — 5 ns period
sc_clock bus_clk("bus_clk", sc_time(5, SC_NS));

// 32 kHz RTC clock — about 31.25 µs period
sc_clock rtc_clk("rtc_clk", sc_time(31250, SC_NS));

cpu.clk(core_clk);
bus.clk(bus_clk);
rtc.clk(rtc_clk);

The SystemC scheduler handles all clocks concurrently — it maintains a single event queue and fires each clock's toggle event at the correct simulation time, regardless of how many clocks exist.

Clock domain crossing When a module sends data from one clock domain to another (e.g., from core_clk to bus_clk), you need CDC synchronization — typically a two-flop synchronizer or a FIFO with separate read/write clocks. This is a hardware design concern that your SystemC model should reflect accurately.

7. sc_clock Inside a Module (Generated Clock)

Sometimes a module generates a divided or gated clock internally. You can declare sc_clock as a member of an SC_MODULE:

SC_MODULE(ClkDivider) {
    sc_in<bool>  clk_in;       // input: 100 MHz
    sc_out<bool> clk_out;      // output: 50 MHz

    sc_signal<bool> divided;   // internal divided clock signal
    bool toggle_state = false;

    void divide_proc() {
        while (true) {
            wait(clk_in.posedge_event());
            toggle_state = !toggle_state;
            clk_out.write(toggle_state);
        }
    }

    SC_CTOR(ClkDivider) {
        SC_THREAD(divide_proc);
    }
};
Note on generated clocks When you drive an output port to act as a divided clock, the downstream modules see a regular sc_signal<bool>. They can still use posedge_event() and negedge_event() — those are methods of sc_signal<bool>, not exclusive to sc_clock.

8. VCD Tracing of sc_clock

sc_clock traces exactly like any other sc_signal<bool>. In the waveform viewer it appears as a regular boolean signal with clean edge transitions — which is exactly what you want for timing analysis.

// In sc_main, before sc_start():
sc_trace_file* tf = sc_create_vcd_trace_file("waveforms");
sc_trace(tf, clk,      "clk");
sc_trace(tf, count,    "count");
sc_trace(tf, reset,    "reset");

sc_start(1000, SC_NS);

sc_close_vcd_trace_file(tf);    // writes waveforms.vcd

Open waveforms.vcd in GTKWave or ModelSim to see clock and signal transitions aligned on the time axis.


9. Complete Example: Clocked Counter

Here is a complete, runnable example combining everything above — a 4-bit counter driven by a 10 ns clock, with synchronous reset:

// counter.h
#include <systemc.h>

SC_MODULE(Counter4) {
    sc_in<bool> clk;
    sc_in<bool> reset;
    sc_out<int> q;

    sc_signal<int> count_r;

    void clk_proc() {
        while (true) {
            wait(clk.posedge_event());
            int nxt = reset.read() ? 0 : (count_r.read() + 1) & 0xF;
            count_r.write(nxt);
            q.write(nxt);
        }
    }

    SC_CTOR(Counter4) {
        SC_THREAD(clk_proc);
    }
};

// sc_main.cpp
int sc_main(int, char**) {
    sc_clock     clk  ("clk",   sc_time(10, SC_NS));
    sc_signal<bool> reset;
    sc_signal<int>  q;

    Counter4 ctr("ctr");
    ctr.clk(clk);
    ctr.reset(reset);
    ctr.q(q);

    reset.write(true);
    sc_start(15, SC_NS);   // hold reset for 1.5 cycles

    reset.write(false);
    sc_start(200, SC_NS);  // run for 20 more clock cycles

    cout << "Final count: " << q.read() << endl;
    return 0;
}

10. Common Pitfalls

PitfallWhat goes wrongFix
Waiting on clk instead of clk.posedge_event() wait(clk) doesn't compile — you can't wait on a signal directly Use wait(clk.posedge_event()) or add to sensitivity list
Declaring clock port as sc_in<sc_clock> Type mismatch — clock is a bool signal Declare as sc_in<bool>
Writing to sc_clock manually Overwrites the self-driving toggle; causes incorrect waveform Never write to an sc_clock — let it drive itself
Forgetting posedge_first default Expecting clock starts high but it starts low Default is posedge_first = true → starts low, first edge is rising at t=0
Reading clk.read() in a combinational process Clock value is just 0 or 1 — not useful for edge detection Use clk.posedge() / clk.negedge() to detect which edge triggered the process

Summary


📬 Get new articles in your inbox

Deep dives on SystemC, C++, and embedded systems — no spam, unsubscribe any time.

No spam, unsubscribe any time. Privacy Policy

Aditya Gaurav

Aditya Gaurav

Embedded systems engineer specializing in SystemC, ARM architecture, and C/C++ internals. Writing deep technical dives for VLSI and embedded engineers.

Part of the SystemC Foundations guide Browse all SystemC guides →