sc_clock in SystemC — Period, Duty Cycle, and Clock-Driven Design
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.
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:
With a 30% duty cycle (0.3), the clock is high for 3 ns and low for 7 ns per period:
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():
| Method | Returns | Fires 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()
}
};
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.
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);
}
};
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
| Pitfall | What goes wrong | Fix |
|---|---|---|
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
sc_clockis a self-drivensc_signal<bool>— it toggles automatically at the configured period.- Constructor parameters: period (required), duty_cycle (default 0.5), start_time (default 0), posedge_first (default true).
- Use
clk.posedge_event()/clk.pos()to wake on rising edges;clk.negedge_event()/clk.neg()for falling edges. - Connect
sc_clocktosc_in<bool>ports — one clock can drive multiple module ports. - Multiple
sc_clockobjects model different clock domains; the scheduler handles all of them in one event queue. - VCD tracing with
sc_trace()captures clock and signal waveforms for post-simulation analysis. - For the simulation entry point where clocks are instantiated and connected, see sc_main Deep Dive.
Part of the SystemC Foundations guide Browse all SystemC guides →