What You'll Learn
Testbench Architecture
A well-structured I2C testbench has three layers:
Testbench
|
+-- Test Tasks (test_idle, test_start, test_write_byte, ...)
| |
| +-- DUT (i2c_master_controller)
| +-- Slave Simulator (i2c_slave_responder)
| +-- Assertions & Checks
|
+-- Monitoring Layer (waveform capture)
+-- Reporting (PASS/FAIL summary)
Testbench Layers
| Layer | Responsibility | Example |
|---|---|---|
| Stimulus Generation | Drive input signals (start_cond_req, data_to_send) | Assert start_cond_req, wait for busy=0 |
| Slave Simulation | Model slave device responses (ACK, hold SCL) | Instantiate i2c_slave_responder |
| Observation & Checks | Sample outputs; verify expected behavior | Check scl=1 at idle, sda_falling at START |
| Error Detection | Flag failures; accumulate results | If error != expected, increment fail_count |
Test Task Structure
Each test is a task that follows this pattern:
task test_name;
begin
// 1. Setup: Initialize state, set inputs
reset_all();
@(posedge clk);
// 2. Stimulus: Apply test conditions
start_cond_req = 1'b1;
repeat(100) @(posedge clk);
// 3. Observation: Check outputs
if (scl_in == 1'b1 && sda_in == 1'b0) begin
$display("[PASS] START condition detected: SCL=1, SDA=0");
pass_count++;
end else begin
$display("[FAIL] START condition: SCL=%b, SDA=%b", scl_in, sda_in);
fail_count++;
end
// 4. Cleanup: Deassert signals
start_cond_req = 1'b0;
end
endtask
Stimulus Generation
Stimulus is the input you drive to test the DUT. For I2C masters, key stimuli are:
Master-Controlled Stimuli
start_cond_req- Request START conditionstop_cond_req- Request STOP conditiontx_data- Byte to transmitrx_en- Enable receive modeack_nack_sel- Send ACK (0) or NACK (1)
Slave-Controlled Stimuli
scl_held_low- Slave holds SCL for clock stretchingsda_held_low- Slave drives SDA (collision or slave ACK)slave_addr- Address the slave will respond toslave_data- Data byte slave sends in read transaction
Stimulus Application Patterns
// Pattern 1: Request-wait-complete
start_cond_req = 1'b1;
@(busy == 1'b1); // Wait for DUT to acknowledge
@(busy == 1'b0); // Wait for completion
start_cond_req = 1'b0;
// Pattern 2: Pulse for N cycles
start_cond_req = 1'b1;
repeat(5) @(posedge clk);
start_cond_req = 1'b0;
// Pattern 3: Hold until signal changes
tx_data_req = 1'b1;
wait(tx_done == 1'b1);
tx_data_req = 1'b0;
Best Practice:
Use
@(busy) transitions instead of fixed delays. Designs may have variable latencies, and tests must adapt.
Expected Results & Assertions
Assertions are inline checks that halt simulation on failure:
// Inline assertion
assert (scl_in == 1'b1 && sda_in == 1'b0)
else $error("START condition failed");
// Procedural assertion (collect results)
if (scl_in == 1'b1 && sda_in == 1'b0) begin
$display("[PASS] START detected");
pass_cnt++;
end else begin
$display("[FAIL] Expected START; got SCL=%b, SDA=%b",
scl_in, sda_in);
fail_cnt++;
end
Common Assertion Patterns
| Signal Condition | Assertion | Why It Matters |
|---|---|---|
| Idle state (no tx) | busy=0, error=0, scl=1, sda=1 | Ensures clean initial state |
| START condition | scl=1 & sda=0 at rising edge | Detects spec-compliant transition |
| STOP condition | scl=1 & sda=1 at rising edge | Ensures clean bus release |
| Data bit set-up | SDA stable before SCL rise | Verifies timing margins |
| Collision detected | error=1 when SDA changes unexpectedly | Tests arbitration logic |
| Clock stretching | busy=1 while scl_held_low=1 | Validates slave synchronization |
10 Core Test Cases
Here are the 10 tests included in the reference project:
Test 1: Idle State Verification
Purpose: Verify clean idle state after reset
Steps: Reset DUT, wait 10 cycles
Expected: busy=0, error=0, scl=1, sda=1
Why: Baseline health check
Steps: Reset DUT, wait 10 cycles
Expected: busy=0, error=0, scl=1, sda=1
Why: Baseline health check
Test 2: START Condition
Purpose: Generate valid START condition
Steps: Assert start_cond_req, wait for completion
Expected: scl=1, then sda falls to 0 (START)
Why: Core I2C protocol requirement
Steps: Assert start_cond_req, wait for completion
Expected: scl=1, then sda falls to 0 (START)
Why: Core I2C protocol requirement
Test 3: STOP Condition
Purpose: Generate valid STOP condition
Steps: Send START, then stop_cond_req
Expected: scl=1, then sda rises to 1 (STOP)
Why: Releases bus; all slaves return to idle
Steps: Send START, then stop_cond_req
Expected: scl=1, then sda rises to 1 (STOP)
Why: Releases bus; all slaves return to idle
Test 4: Repeated START
Purpose: START without STOP
Steps: Send START, immediately send START again
Expected: SDA transitions twice while SCL remains high
Why: Multi-phase transactions (write addr, then read data)
Steps: Send START, immediately send START again
Expected: SDA transitions twice while SCL remains high
Why: Multi-phase transactions (write addr, then read data)
Test 5: Write Byte 0xA5 with ACK
Purpose: Transmit data byte; slave acknowledges
Steps: Send START, drive tx_data=8'hA5, simulate slave ACK on bit 9
Expected: All 8 bits transmitted; sda_in pulled LOW on 9th clock (ACK)
Why: Core write transaction
Steps: Send START, drive tx_data=8'hA5, simulate slave ACK on bit 9
Expected: All 8 bits transmitted; sda_in pulled LOW on 9th clock (ACK)
Why: Core write transaction
Test 6: Read Byte (Slave Drives)
Purpose: Master reads 8 bits from slave
Steps: Send START+addr(read), simulate slave driving sda bits
Expected: Master samples all 8 bits correctly
Why: Read transaction; slave is data source
Steps: Send START+addr(read), simulate slave driving sda bits
Expected: Master samples all 8 bits correctly
Why: Read transaction; slave is data source
Test 7: Collision Detection
Purpose: Detect when SDA changes unexpectedly
Steps: Release SDA (sda_out=1) but set sda_held_low (simulate collision)
Expected: error=1, indicating arbitration loss
Why: Collision avoidance for multi-master systems
Steps: Release SDA (sda_out=1) but set sda_held_low (simulate collision)
Expected: error=1, indicating arbitration loss
Why: Collision avoidance for multi-master systems
Test 8: Clock Stretching
Purpose: Slave holds SCL LOW; master waits
Steps: Release SCL, slave holds scl_held_low=1 for 50 cycles
Expected: busy=1 while SCL is held; transitions back after release
Why: Slave synchronization; master must be patient
Steps: Release SCL, slave holds scl_held_low=1 for 50 cycles
Expected: busy=1 while SCL is held; transitions back after release
Why: Slave synchronization; master must be patient
Test 9: Multi-Byte Transaction
Purpose: Send multiple bytes in one transaction
Steps: START, write 0x50, 0xA5, 0x5A, 0xC3, STOP
Expected: All 4 bytes transmitted without errors
Why: Real-world pattern; tests state persistence
Steps: START, write 0x50, 0xA5, 0x5A, 0xC3, STOP
Expected: All 4 bytes transmitted without errors
Why: Real-world pattern; tests state persistence
Test 10: Bus Busy Detection
Purpose: Verify busy flag during active transactions
Steps: Assert start_cond_req, monitor busy
Expected: busy=1 during START, busy=0 after STOP
Why: Prevents master from starting transaction on occupied bus
Steps: Assert start_cond_req, monitor busy
Expected: busy=1 during START, busy=0 after STOP
Why: Prevents master from starting transaction on occupied bus
Debugging Failed Tests
Systematic Debugging Approach
- Read the error message: Does it say "FAIL: START condition" or "FAIL: Collision"?
- Check waveforms: Plot SCL/SDA vs. time. Look for timing violations.
- Verify stimulus: Did you drive the right signals at the right time?
- Check FSM state: Print state transitions: `$display("State: %s", state.name())`
- Review spec: Re-read the I2C timing constraints. Is setup time violated?
Common Failure Patterns
// Pattern 1: START never happens (scl stays 0)
// Cause: FSM stuck in state transition
// Fix: Check edge detection logic; verify scl_sync is toggling
// Pattern 2: Collision detected on every write
// Cause: Open-drain pull-up model is broken
// Fix: Verify assign scl_in = (scl_out==0) ? 0 : 1;
// Pattern 3: Data bits corrupted (0xA5 becomes 0xFF)
// Cause: Slave ACK interfering with bit transmission
// Fix: Ensure ACK happens only after all 8 bits
// Pattern 4: Clock stretching ignored (busy never goes 1)
// Cause: FSM doesn't monitor scl_held_low
// Fix: Add wait condition: wait(scl_stable == 1'b1)
Debugging Golden Rule:
Look at waveforms FIRST. Bus-level signals (SCL, SDA) tell the truth; logic inside the DUT might be opaque. If SCL never rises, the FSM is stuck—dive into register changes.
Key Takeaways
Key Takeaways
- Testbench structure: Stimulus → DUT → Observation → Assert/Report.
- Task-based testing: Each test is a self-contained task with clear pass/fail criteria.
- Stimulus patterns: Use
@(busy)handshakes instead of fixed delays for robustness. - Assertions: Inline assertions halt on failure; procedural checks accumulate pass/fail counts.
- 10 core tests cover idle, START/STOP, data transmission, ACK/NACK, collision, clock stretching.
- Debugging: Always start with waveforms. Bus-level signals reveal the truth.
- Slave simulation: Simple responder beats complex UVM for quick validation.
Series Navigation:
Part 1: Protocol Fundamentals →
Part 2: Master Controller FSM →
Part 3: Slave Responder & Pull-up Logic →
Part 4: Testbench Design & Test Cases (You are here)
Part 5: Interview Prep & Optimization
Part 1: Protocol Fundamentals →
Part 2: Master Controller FSM →
Part 3: Slave Responder & Pull-up Logic →
Part 4: Testbench Design & Test Cases (You are here)
Part 5: Interview Prep & Optimization

Comments
Post a Comment