In short: A good JK flip-flop testbench generates a free-running clock, drives J and K away from the active clock edge, predicts the expected output with an independent reference model, and compares it automatically on every cycle, so a single PASS or FAIL line replaces eyeballing waveforms.
The JK flip-flop is a favourite interview vehicle because it is small enough to finish on a whiteboard yet rich enough to expose every classic testbench mistake: races at the clock edge, X values at time zero, and comparisons that silently pass. If you want the device theory first, read our JK flip flop guide. Here we focus on building a verification environment around it, following the general flow in writing a Verilog testbench.
The device under test and its behaviour
A JK flip-flop samples J and K on the active clock edge. The characteristic equation is Q(next) = J·Q' + K'·Q, which gives four behaviours: hold when J=K=0, reset when J=0 and K=1, set when J=1 and K=0, and toggle when both are 1. The toggle case is what removes the forbidden state of the SR flip-flop, and it is also why tying J and K together yields a T flip-flop.
| J | K | Q(next) | Action |
|---|---|---|---|
| 0 | 0 | Q | Hold |
| 0 | 1 | 0 | Reset |
| 1 | 0 | 1 | Set |
| 1 | 1 | Q' | Toggle |
We write the DUT with a case statement and an asynchronous active-low reset, because real flops almost always have one and it gives us an extra scenario to test:
module jk_ff (input clk, rst_n, j, k, output reg q);
always @(posedge clk or negedge rst_n)
if (!rst_n) q <= 1'b0;
else case ({j,k})
2'b00: q <= q;
2'b01: q <= 1'b0;
2'b10: q <= 1'b1;
2'b11: q <= ~q;
endcase
endmodule
Anatomy of a self-contained testbench
Four pieces are enough: a clock generator, a stimulus driver, a reference model and a checker. Because the testbench has no ports, everything lives inside one top module, and a simulator such as Synopsys VCS, Cadence Xcelium or Siemens Questa elaborates it directly as the root.
Key fact: The reference model must be written differently from the DUT. If you copy the case statement into the testbench, any misreading of the spec appears in both places and the comparison passes while the design is wrong.
Clock generation and race-free stimulus
The simplest clock is always #5 clk = ~clk; with a timescale 1ns/1ps, giving a 10 ns period (100 MHz). Initialise clk to 0 so the first edge at 5 ns is a clean rising edge instead of an X-to-1 transition that some simulators count as a posedge.
The classic bug is driving J and K at the same instant the flop samples them. With blocking assignments in the testbench, the DUT may see the old or the new value depending on event ordering, and results change between VCS and Questa. We avoid this by launching new stimulus a small delay after the active edge, here #1, so the inputs are stable for 9 ns before the next edge. That mimics a real launching flop with clock-to-Q delay and keeps setup and hold margins obvious.
Directed tests first, then random
Start with a short directed sequence that hits every row of the truth table in a known order, because a failure there is trivial to debug. Then add constrained-random cycles to cover input transitions you did not think of. The timing diagram below shows the directed sequence we use.
For the random phase, two inputs and one state bit give just eight state-and-input combinations. With 200 random cycles the chance of missing any one of them is about (7/8)^200, roughly 3 in 10^12, so pure randomness is enough here. Real designs with wide buses need functional coverage; the move from this style to class-based randomisation is covered in Verilog vs SystemVerilog.
The checker and the full testbench
Compare on the falling clock edge: Q has settled for 4 ns, and the stimulus will not move for another 6 ns. Use !==, not !=. The case-inequality operator treats X and Z as real values, so an uninitialised Q mismatches loudly instead of evaluating to X and letting the if silently skip.
`timescale 1ns/1ps
module tb_jk_ff;
reg clk = 0, rst_n = 0, j = 0, k = 0;
wire q;
reg q_exp;
integer errors = 0, checks = 0, i;
jk_ff dut (.clk(clk), .rst_n(rst_n), .j(j), .k(k), .q(q));
always #5 clk = ~clk; // 100 MHz
// reference model: characteristic equation, not a case table
always @(posedge clk or negedge rst_n)
if (!rst_n) q_exp <= 1'b0;
else q_exp <= (j & ~q_exp) | (~k & q_exp);
always @(negedge clk)
if (rst_n) begin
checks = checks + 1;
if (q !== q_exp) begin
errors = errors + 1;
$display("%0t FAIL j=%b k=%b q=%b exp=%b", $time, j, k, q, q_exp);
end
end
task drive(input jj, input kk);
begin @(posedge clk); #1 {j,k} = {jj,kk}; end
endtask
initial begin
repeat (2) @(posedge clk);
#1 rst_n = 1;
drive(1,0); drive(0,1); drive(1,1); drive(1,1); drive(0,0);
for (i = 0; i < 200; i = i + 1) drive($urandom, $urandom);
@(negedge clk); #1;
if (errors == 0) $display("PASS: %0d checks", checks);
else $display("FAIL: %0d of %0d checks", errors, checks);
$finish;
end
endmodule
Key fact: A testbench that never fails proves nothing. Mutate the DUT on purpose, for example change 2'b11 to hold, and confirm the run prints FAIL. This is a cheap form of mutation testing used in real sign-off flows.
Seeing toggle mode in motion
With J=K=1 every active edge inverts Q, so Q runs at half the clock frequency. That is the basis of ripple and synchronous JK counters, and it is a quick sanity scenario for your testbench: Q must swing exactly once per clock period. The animation below moves a cursor across five cycles while the LED follows Q.
Running it and what interviewers probe
- Commands:
vcs -full64 jk_ff.v tb_jk_ff.v -R,xrun jk_ff.v tb_jk_ff.v, orvlog jk_ff.v tb_jk_ff.v && vsim -c tb_jk_ff -do "run -all". Add$dumpfileand$dumpvarsonly when debugging; waves slow regressions. - Async reset: pulse
rst_nlow mid-cycle with Q=1 and verify Q clears immediately, not at the next edge. - Exit status: regressions grep for the PASS or FAIL string, so keep the message format stable.
- Next step: when stimulus, monitor and scoreboard become separate classes you are moving toward the UVM testbench architecture; the module-level checker above is its seed.
If you want mentored practice on this kind of flow, ChipXpert's VLSI training programmes run it on industry tools, and our full syllabus is listed under CourseTron courses.
Ready to go deeper? Join the CourseTron Verification course and build self-checking testbenches, assertions and UVM environments step by step. Browse the courses.
Frequently asked questions
Why drive inputs with #1 after the clock edge?
It removes the race between the testbench and the DUT. Inputs change after the sampling edge and stay stable for most of the period, similar to a real upstream flop.
Why compare with !== instead of !=?
The != operator returns X when either side contains X, and an if statement treats X as false, so a broken design can slip through. !== compares X and Z literally and flags the mismatch.
Is a reference model not just duplicating the DUT?
Only if you copy it. Writing the model from a different representation, such as the characteristic equation against the DUT's case table, gives independent evidence that both match the specification.
Do I need SystemVerilog or UVM for a flip-flop?
No. A plain Verilog self-checking module is enough for a single flop. SystemVerilog adds assertions, coverage and classes that pay off as designs grow.