Case Study: 74HC195 4-Bit Shift Register

A complete walkthrough of implementing a timing-accurate behavioral model for a standard logic IC.

Overview

This case study demonstrates how to create a production-quality behavioral model by implementing the 74HC195, a 4-bit parallel-access shift register with JK-style serial inputs. This IC is representative of standard logic components that benefit from behavioral modeling.

Why Model This Behaviorally? While a gate-level implementation is possible, a behavioral model captures the datasheet specification directly, including precise timing checks, and provides faster simulation for system-level designs.

Component Overview

74HC195 Features

The 74HC195 is a high-speed CMOS 4-bit shift register with:

Pin Configuration

Pin Name Type Description
MR Input Master Reset (active low) - asynchronous clear
J Input Serial input J (JK flip-flop style)
Input Serial input K complement
D0-D3 Input Parallel data inputs
PE Input Parallel Enable (active low)
CP Input Clock input (rising edge active)
Q0-Q3 Output 4-bit register outputs
Q̄3 Output Complement of Q3

Functional Behavior

The 74HC195 operates in three modes, with priority order:

  1. Asynchronous Reset: When MR=0, all outputs go to 0 (independent of clock)
  2. Parallel Load: When PE=0 on rising clock edge, load D0-D3 → Q0-Q3
  3. Shift Right: When PE=1 on rising clock edge, shift data right with JK control:
    • J=0, K̄=0: Shift in 0 (reset Q0)
    • J=1, K̄=1: Shift in 1 (set Q0)
    • J=1, K̄=0: Toggle Q0
    • J=0, K̄=1: Hold Q0 (no change)
    Then: Q1←Q0, Q2←Q1, Q3←Q2

Timing Specifications (VCC=4.5V)

Parameter Symbol Min Typ Max Units
Clock pulse width (high) tpw_h 20 - - ns
Clock pulse width (low) tpw_l 20 - - ns
Setup time (J, K̄, PE, D) tsu 25 - - ns
Hold time th 3 - - ns
MR pulse width tw_mr 20 - - ns
MR removal time trec_mr 20 - - ns
Propagation delay CP→Q tpd - 22 44 ns
Propagation delay MR→Q tpd_mr - 19 38 ns

Design Approach

Planning the Model Structure

Before writing code, we need to plan how signals map to the behavioral model interface:

Inputs (9 total)

Index  Signal  Description
-----  ------  -----------
  0    MR      Master reset (active low)
  1    J       Serial input J
  2    K_bar   Serial input K complement
  3    D0      Parallel data bit 0
  4    D1      Parallel data bit 1
  5    D2      Parallel data bit 2
  6    D3      Parallel data bit 3
  7    PE      Parallel enable (active low)
  8    CP      Clock input

State Variables (5 total)

The model needs state to remember values between evaluations:

Index  Variable   Purpose
-----  ---------  -------
  9    prev_clk   Previous clock value (for edge detection)
 10    sq0        Saved Q0 value (internal state)
 11    sq1        Saved Q1 value
 12    sq2        Saved Q2 value
 13    sq3        Saved Q3 value
Why Separate State Variables? The sq0-sq3 variables store the primary latch states, which is crucial for proper JK flip-flop operation during hold mode. Using separate state prevents feedback issues when Q outputs are read during the same evaluation.

Outputs (5 total)

Index  Signal  Description
-----  ------  -----------
  0    Q0      Register bit 0 (LSB)
  1    Q1      Register bit 1
  2    Q2      Register bit 2
  3    Q3      Register bit 3 (MSB)
  4    Q3_bar  Complement of Q3

Timing Checks (10 total)

Index  Check            Description
-----  ---------------  -----------
  0    tsu_jk           Setup time for J and K inputs
  1    th_jk            Hold time for J and K inputs
  2    tsu_pe           Setup time for PE input
  3    th_pe            Hold time for PE input
  4    tsu_parallel     Setup time for D0-D3 inputs
  5    th_parallel      Hold time for D0-D3 inputs
  6    tpw_clk_high     Clock high pulse width
  7    tpw_clk_low      Clock low pulse width
  8    mr_width         MR pulse width
  9    mr_removal       MR removal time before clock edge

Implementation Walkthrough

Netlist Declaration

First, declare the component in your SNL netlist:

Type=hc74195
    I=mr,j,k_bar,d0,d1,d2,d3,pe,cp
    O=q0,q1,q2,q3,q3_bar
    behavioral="simicpy"
    state=prev_clk,sq0,sq1,sq2,sq3
    checks=tsu_jk,th_jk,tsu_pe,th_pe,tsu_parallel,th_parallel,tpw_clk_high,tpw_clk_low,mr_width,mr_removal
    ODEL=44,44,44,44,44

Key attributes:

State Initialization

The init_state() method sets initial values for state variables:

def init_state(self):
    """Initialize state variables to unknown (X)."""
    return bytes([LEVEL_X, LEVEL_X, LEVEL_X, LEVEL_X, LEVEL_X])

Core Evaluation Logic

The evalu8() method implements the functional behavior:

1. Extract Inputs and State

# Extract input signal levels (mask to get logic level only)
mr = inputs[0] & LEVEL_FIELD
j = inputs[1] & LEVEL_FIELD
k_bar = inputs[2] & LEVEL_FIELD
d0 = inputs[3] & LEVEL_FIELD
d1 = inputs[4] & LEVEL_FIELD
d2 = inputs[5] & LEVEL_FIELD
d3 = inputs[6] & LEVEL_FIELD
pe = inputs[7] & LEVEL_FIELD
clk = inputs[8] & LEVEL_FIELD

# Extract state variables
prev_clk = inputs[9] & LEVEL_FIELD
sq0 = inputs[10] & LEVEL_FIELD
sq1 = inputs[11] & LEVEL_FIELD
sq2 = inputs[12] & LEVEL_FIELD
sq3 = inputs[13] & LEVEL_FIELD

2. Handle Asynchronous Reset (Highest Priority)

if mr == LEVEL_ZERO:
    # Master reset active - clear all outputs and state
    out[0] = LEVEL_ZERO  # Q0
    out[1] = LEVEL_ZERO  # Q1
    out[2] = LEVEL_ZERO  # Q2
    out[3] = LEVEL_ZERO  # Q3
    out[4] = LEVEL_ONE   # Q3_bar = NOT(0) = 1
    out[5] = clk         # Save current clock
    out[6] = LEVEL_ZERO  # sq0
    out[7] = LEVEL_ZERO  # sq1
    out[8] = LEVEL_ZERO  # sq2
    out[9] = LEVEL_ZERO  # sq3
    return bytes(out)

3. Detect Clock Rising Edge

clock_rising = (prev_clk == LEVEL_ZERO and clk == LEVEL_ONE)

4. Parallel Load Mode

if clock_rising and pe == LEVEL_ZERO:
    # Load parallel data into outputs and state
    out[0] = d0  # Q0
    out[1] = d1  # Q1
    out[2] = d2  # Q2
    out[3] = d3  # Q3
    out[6] = d0  # sq0
    out[7] = d1  # sq1
    out[8] = d2  # sq2
    out[9] = d3  # sq3

5. Shift Mode with JK Logic

The JK flip-flop logic for Q0 implements all four operational modes:

if clock_rising and pe == LEVEL_ONE:
    # JK flip-flop equation: d = (J & ~Q) | (K̄ & (Q | J))
    # This naturally handles:
    #   J=0, K̄=0: d = 0 (reset)
    #   J=1, K̄=1: d = 1 (set)
    #   J=1, K̄=0: d = ~Q (toggle)
    #   J=0, K̄=1: d = Q (hold)
    not_sq0 = notTbl[sq0]
    new_sq0 = (j & not_sq0) | (k_bar & (sq0 | j))

    # Shift right: new value enters Q0, others shift
    out[0] = new_sq0  # Q0 ← JK result
    out[1] = sq0      # Q1 ← Q0
    out[2] = sq1      # Q2 ← Q1
    out[3] = sq2      # Q3 ← Q2
    out[6] = new_sq0  # sq0
    out[7] = sq0      # sq1
    out[8] = sq1      # sq2
    out[9] = sq2      # sq3
Critical Pattern: The JK logic uses sq0 (saved state) for feedback, not out[0] (current output). This prevents circular dependencies during hold operations where Q must read its own previous value.

6. Hold State (No Clock Edge)

else:
    # No edge or falling edge - maintain current values
    out[0] = sq0
    out[1] = sq1
    out[2] = sq2
    out[3] = sq3
    out[6] = sq0  # State stays the same
    out[7] = sq1
    out[8] = sq2
    out[9] = sq3

7. Complement Output and Clock State

# Q3_bar is always complement of Q3
out[4] = notTbl[out[3]]

# Save current clock for next edge detection
out[5] = clk

Timing Checks Implementation

When timing checks are enabled (ci is not None), validate timing constraints:

Setup Time Check Example

if clock_rising and check_time[0] > 0 and j != LEVEL_X and k_bar != LEVEL_X:
    latest_change = max(last_time[PIN_J], last_time[PIN_K_BAR])
    if current_time - latest_change < check_time[0]:
        errors[0] = "J/K setup time violation before clock rising edge"

Pulse Width Check Example

if clock_falling and check_time[6] > 0:
    pulse_width = current_time - last_time[PIN_CP]
    if pulse_width < check_time[6]:
        errors[6] = "Clock high pulse width too short"

Testing Strategy

Test Scenarios

A comprehensive test suite should cover:

  1. Reset Test
    • Apply MR=0, verify all outputs go to 0
    • Verify reset is asynchronous (works without clock)
    • Check Q3_bar=1 when Q3=0
  2. Parallel Load Test
    • Set PE=0, load patterns: 0000, 1111, 1010, 0101
    • Verify all Q outputs match D inputs after clock edge
    • Test multiple consecutive loads
  3. Shift Operations Test
    • Set PE=1, test J=1,K̄=1 (shift in 1s)
    • Test J=0,K̄=0 (shift in 0s)
    • Test J=0,K̄=1 (hold mode - no change)
    • Test J=1,K̄=0 (toggle mode)
  4. Mixed Operations Test
    • Load pattern, then shift it out
    • Shift pattern, then load new value
    • Reset during shift operation
  5. Timing Violation Test
    • Setup time violations (data changes too close to clock)
    • Hold time violations (data changes too soon after clock)
    • Pulse width violations (clock too narrow)

Sample Test Commands

>>: define file=hc74195_test
>>: get type=hc74195_test

# Test 1: Reset
>>: set mr=0
>>: simulate 50
>>: print q0,q1,q2,q3,q3_bar
# Expected: 0,0,0,0,1

# Test 2: Parallel load 1010
>>: set mr=1 pe=0 d0=1 d1=0 d2=1 d3=0
>>: clock cp period=100
>>: simulate 100
>>: print q0,q1,q2,q3
# Expected: 1,0,1,0

# Test 3: Shift in 1s
>>: set pe=1 j=1 k_bar=1
>>: clock cp period=100
>>: simulate 400
>>: print q0,q1,q2,q3
# Expected: 1,1,1,1

Validation Checklist

Lessons Learned

Key Implementation Patterns

1. Priority-Based Control Flow

Organizing the evaluation as a priority cascade (reset → parallel load → shift → hold) mirrors the datasheet description and makes the code easier to verify against specifications.

2. Separate State Variables

Using sq0-sq3 instead of just tracking outputs is essential for feedback logic. The JK flip-flop needs to read its own previous value, which would create a circular dependency if we used the output being computed.

3. Edge Detection Pattern

Saving the previous clock value and comparing it to the current value is the standard pattern for edge-triggered behavior. This works correctly even when the clock has unknown (X) values.

4. X-Propagation Discipline

Always handle unknown inputs explicitly. For the reset input, unknown (X) values must be treated conservatively - any flip-flop that's 0 stays 0, others become X.

Common Mistakes to Avoid

Mistake #1: Forgetting LEVEL_FIELD Mask

Always use inputs[i] & LEVEL_FIELD when extracting logic levels. The input bytes contain both level (bits 0-1) and strength (bits 2-7) information. Comparing raw bytes will give incorrect results.

Mistake #2: Wrong Output Order

The output array must exactly match the netlist O= declaration order. For the 74HC195: Q0, Q1, Q2, Q3, Q3_bar, then state variables.

Mistake #3: Forgetting to Save State

The output array must contain both outputs AND state variables. A common bug is forgetting to append state variables, which causes state to be lost between evaluations.

Mistake #4: Using Output Instead of State for Feedback

During JK hold mode, Q0 must read its own previous value. Using out[0] (current output being computed) instead of sq0 (saved state) creates incorrect behavior because out[0] hasn't been computed yet.

Performance Considerations

For this model, Python performance is acceptable. However, if you're simulating large arrays of shift registers (e.g., 1000 instances), consider:

Complete Source Code

The full implementation with all timing checks and detailed comments is available at:

/home/gary/workspace/simic/tests/behavioral/python/HC74195.py

Supporting files:

The complete implementation is ~330 lines including comprehensive timing checks and documentation. Study this as a template for creating your own standard IC behavioral models.