Behavioral Models Reference

Comprehensive guide to writing custom behavioral models in Python and C for Simic simulation.

Introduction

Behavioral models in Simic allow you to implement complex logic components using procedural code rather than gate-level descriptions. Instead of describing a circuit with interconnected gates and flip-flops, you write a function that computes outputs from inputs using arbitrary algorithms.

Behavioral models are declared in SNL netlists using the COMPOSITION=BEHAVIORAL keyword, with the model implementation provided by either a Python class or a C API function. The model is invoked automatically during simulation whenever any of its inputs change.

When to Use Behavioral Models

Behavioral models are ideal for:

CRITICAL WARNING: Do NOT Use for Large Memories

Behavioral models are not suitable for implementing large memory structures (RAMs). Each behavioral model instance allocates full state storage, and Simic's event-driven simulation will invoke your model code on every input change.

For memories, always use Simic's built-in ROM and RAM primitives. These primitives use specialized internal representations optimized for large address spaces:

  • RAM primitives support address spaces up to 2^32 words with sparse initialization
  • Built-in primitives avoid Python/C call overhead on every memory access

Example of what NOT to do: Implementing a 64KB RAM as a behavioral model with a Python list would create 65,536 list entries per instance, generate excessive events, and severely degrade simulation performance.

Timing Monitors as "Logic Analyzers"

A powerful use of behavioral models is implementing timing monitors that act as embedded logic analyzers within your simulation. These models observe signals without driving any outputs, checking for protocol violations, setup/hold timing issues, or bus contention.

Key characteristics of timing monitors:

  • Declared with only inputs, no outputs are necessary)
  • Use the CHECKINFO structure to report warnings and errors
  • Can maintain internal state to track protocol phases or edge timing
  • Violations are reported to the simulation log without stopping simulation

Example applications: SPI bus monitor checking clock polarity and chip-select timing, I2C monitor verifying start/stop conditions, memory controller monitor checking RAS/CAS sequencing.

Decision Guide: Python vs. C

Choose your implementation language based on these factors:

Factor Python Models C Models
Development Speed Fast iteration, no compilation Requires recompilation
Performance Good for most cases Better for critical paths
Debugging Easy with print statements Requires gdb or printf
State Management Natural with class attributes Manual with static variables
Integration Direct access to Python ecosystem Direct access to C libraries

Recommendation: Start with Python unless you have specific performance requirements. Python models are easier to write, debug, and maintain. Only move to C if profiling shows your behavioral model is a simulation bottleneck.

Quick Start

To create a behavioral model, you need two components:

1. SNL Netlist Declaration

Declare the component type with COMPOSITION=BEHAVIORAL:

TYPE=my-component COMPOSITION=BEHAVIORAL BEHAVIORAL=simicpy
INPUTS=clk,reset,data_in[7:0]
OUTPUTS=data_out[7:0],ready
# For Python models:
BEHAVIORAL=simicpy
# For C models:
BEHAVIORAL=simicc

2. Model Implementation

Python model (file: my_component.py):

from simic import LEVEL_FIELD, LEVEL_ZERO, LEVEL_ONE, LEVEL_X

class MyComponent:
    def __init__(self, part_name):
        self.part_name = part_name
        # Initialize state
        self.register = 0

    def evalu8(self, inputs, outputs, ci):
        """Called whenever any input changes.

        Args:
            inputs: List of input values (bytes)
            outputs: List of output values (bytes)

        Returns:
            tuple: (bytearray output values, array of errors or None) 
        """
        clk = inputs[0] & LEVEL_FIELD
        reset = inputs[1] & LEVEL_FIELD
        data_in = inputs[2:10]  # 8-bit bus

        # Implement logic here
        outputs = []
        # ... compute outputs ...

        return (bytes(outputs), None)

C model (file: my_component.c):

#include "simic.h"

void my_component_init(char *part_name) {
    // One-time initialization per instance
}

void my_component(int *inputs, int *outputs, CHECKINFO *check) {
    int clk = inputs[0] & LEVEL_FIELD;
    int reset = inputs[1] & LEVEL_FIELD;

    // Implement logic here
    outputs[0] = LEVEL_ONE;  // Example
}

3. File Naming Convention

Simic locates behavioral model files by converting the TYPE name to a filename:

Python Behavioral Models

Signal Encoding

All signal values in Simic are represented as integers where the low 2 bits encode the logic level:

Constant Value Meaning
Level Encoding (bits 0-1)
LEVEL_FIELD 0x03 Mask to extract level bits
LEVEL_ZERO 0x00 Logic 0
LEVEL_X 0x01 Unknown/uninitialized
LEVEL_ONE 0x03 Logic 1
Strength Encoding (bits 2-7)
STRENGTH_FIELD 0xFC Mask for strength bits
POWER 0x24 Power supply strength
DRIVING 0x48 Active driver strength (default)
RESISTIVE 0x6C Resistive/weak driver
FLOATING 0x90 High impedance strength
Combined Values
LEVEL_Z LEVEL_X | FLOATING (0x91) High-impedance unknown state

Important: Always mask with LEVEL_FIELD before comparing values:

# CORRECT:
if (inputs[0] & LEVEL_FIELD) == LEVEL_ONE:
    # ... handle logic high ...

# WRONG (may fail due to internal flags in upper bits):
if inputs[0] == LEVEL_ONE:
    # ... this comparison may fail unexpectedly ...

The evalu8() Method

The core of every Python behavioral model is the evalu8() method. Simic calls this method automatically whenever any input signal changes.

Method Signature

def evalu8(self, inputs, outputs, ci):
    """
    Evaluate outputs based on current inputs.

    Args:
        inputs:  bytearray, one per input pin in declaration order.
                 Each integer is a Simic strength and value encoded integer.
        outputs: bytearray, one per output pin in declaration order.
                 Each integer is a Simic strength and value encoded integer.

    Returns:
        bytearray, one per output pin in declaration order.
        Return None to indicate no outputs changed (optimization).
    """

Input/Output Ordering

The order of elements in inputs and your returned output list must exactly match the pin order in your SNL declaration:

TYPE=example
INPUTS=clk,reset,enable,data[3:0]
OUTPUTS=result[3:0],flag

# In evalu8():
# inputs[0] = clk
# inputs[1] = reset
# inputs[2] = enable
# inputs[3] = data[3]  (MSB)
# inputs[4] = data[2]
# inputs[5] = data[1]
# inputs[6] = data[0]  (LSB)

# Return value must be:
# [result[3], result[2], result[1], result[0], flag]

State Management

Behavioral models typically need to maintain state between evaluations (register values, FSM state, counters, etc.). Python models handle this naturally with class instance variables.

Initialization

class Counter:
    def __init__(self, part_name):
        """Called once per instance when circuit is compiled.

        Args:
            part_name: The PART= name from the netlist (useful for debug messages)
        """
        self.part_name = part_name

        # Initialize state variables
        self.count = 0
        self.prev_clk = LEVEL_ZERO

        # Debug: verify initialization
        print(f"Initialized counter instance: {part_name}")

    def evalu8(self, inputs, outputs, ci):
        # State variables persist between calls
        clk = inputs[0] & LEVEL_FIELD

        # Detect positive clock edge
        if clk == LEVEL_ONE and self.prev_clk == LEVEL_ZERO:
            self.count = (self.count + 1) & 0xFF  # 8-bit counter

        self.prev_clk = clk
        return [self.count]

State Consistency Rules

Edge Detection Pattern

Most sequential logic requires detecting clock edges. Here's the standard pattern:

class DFlipFlop:
    def __init__(self, part_name):
        self.part_name = part_name
        self.state = LEVEL_ZERO
        self.prev_clk = LEVEL_ZERO

    def evalu8(self, inputs, outputs, ci):
        clk = inputs[0] & LEVEL_FIELD
        d = inputs[1] & LEVEL_FIELD

        # Detect positive edge: clock was 0 or X, now 1
        if clk == LEVEL_ONE and self.prev_clk != LEVEL_ONE:
            self.state = d  # Capture input on rising edge

        # Update previous clock state for next evaluation
        self.prev_clk = clk

        # Output Q follows stored state
        return [self.state]

Negative edge detection:

if clk == LEVEL_ZERO and self.prev_clk != LEVEL_ZERO:
    # Falling edge detected

Complete D Flip-Flop Example

A production-quality D flip-flop with asynchronous reset and set:

from simic import LEVEL_FIELD, LEVEL_ZERO, LEVEL_ONE, LEVEL_X

class DFlipFlop:
    """D flip-flop with async reset/set, positive edge triggered."""

    def __init__(self, part_name):
        self.part_name = part_name
        self.q = LEVEL_ZERO
        self.prev_clk = LEVEL_ZERO

    def evalu8(self, inputs, outputs, ci):
        """
        Inputs: clk, d, reset_n, set_n
        Outputs: q, q_n
        """
        clk = inputs[0] & LEVEL_FIELD
        d = inputs[1] & LEVEL_FIELD
        reset_n = inputs[2] & LEVEL_FIELD
        set_n = inputs[3] & LEVEL_FIELD

        # Asynchronous reset (active low)
        if reset_n == LEVEL_ZERO:
            self.q = LEVEL_ZERO
            self.prev_clk = clk
            q_n = LEVEL_ONE
            return [self.q, q_n]

        # Asynchronous set (active low)
        if set_n == LEVEL_ZERO:
            self.q = LEVEL_ONE
            self.prev_clk = clk
            q_n = LEVEL_ZERO
            return [self.q, q_n]

        # Synchronous operation: rising edge of clock
        if clk == LEVEL_ONE and self.prev_clk != LEVEL_ONE:
            if d == LEVEL_X:
                self.q = LEVEL_X  # Propagate unknown
            else:
                self.q = d

        # Update clock history
        self.prev_clk = clk

        # Compute complementary output
        if self.q == LEVEL_ZERO:
            q_n = LEVEL_ONE
        elif self.q == LEVEL_ONE:
            q_n = LEVEL_ZERO
        else:
            q_n = LEVEL_X

        return [self.q, q_n]

X-Propagation Handling

Proper behavioral models should propagate X (unknown) states correctly. When inputs are unknown, outputs should generally also be unknown unless the specific logic function produces a known result regardless of some inputs.

def evalu8(self, inputs, outputs, ci):
    a = inputs[0] & LEVEL_FIELD
    b = inputs[1] & LEVEL_FIELD

    # AND of a and b
    return (bytes([a & b))

C Behavioral Models

Signal Encoding in C

C models use the same signal encoding as Python models:

#include "simic.h"

// Level encoding (bits 0-1)
#define LEVEL_FIELD 0x03
#define LEVEL_ZERO  0x00
#define LEVEL_X     0x01
#define LEVEL_ONE   0x03

// Strength encoding (bits 2-7)
#define STRENGTH_FIELD 0xFC
#define POWER      0x24
#define DRIVING    0x48
#define RESISTIVE  0x6C
#define FLOATING   0x90

// Combined values
#define LEVEL_Z    (LEVEL_X | FLOATING)  // 0x91

Function Signatures

C behavioral models consist of two functions:

Initialization Function (Optional)

void TYPE_NAME_init(char *part_name) {
    /* Called once when circuit is compiled.
     * Use for one-time setup, allocating static state, etc.
     * The part_name parameter is the instance name from netlist.
     */
    static int initialized = 0;
    if (!initialized) {
        // First-time initialization for this function
        initialized = 1;
    }
}

Evaluation Function (Required)

void TYPE_NAME(int *inputs, int *outputs, CHECKINFO *check) {
    /* Called whenever any input changes.
     *
     * Args:
     *   inputs: Array of input values, one per pin in declaration order
     *   outputs: Array to fill with output values, one per pin
     *   check: Structure for reporting timing violations (may be NULL)
     */
    int clk = inputs[0] & LEVEL_FIELD;
    int d = inputs[1] & LEVEL_FIELD;

    // Compute outputs
    outputs[0] = d;  // Example
}

State Management in C

C models must NOT use static variables to maintain state between calls.

Brief C Model Example: SHIFT4C

#include <string.h>
#include "mimlev.h"
#include "checkinfo.h"

// The shift4 model
// in=write,d0,d1,d2,d3,clk,prev_clk o=d0',d1',d2',d3'
void evalu8(unsigned char *inputs, unsigned char *outputs, CHECKINFO *ci)
{
  unsigned char outbuf[6]; // working register
  unsigned char *out = outbuf;
  if ((inputs[0] & LEVEL_FIELD) == LEVEL_ONE) { // Write is enabled
    for (int i = 0; i++ < 4;) {
      *(out++) = inputs[i] & LEVEL_FIELD;
    }
  } else if ((inputs[0] & LEVEL_FIELD) == LEVEL_ZERO) { // Write is disabled
    if (inputs[6] == LEVEL_ZERO && (inputs[5] & LEVEL_FIELD) == LEVEL_ONE) {
      // Shift in the first input
      *(out++) = inputs[1] & LEVEL_FIELD;
      for (int i = 0; i < 3; i++) {
        *(out++) = outputs[i] & LEVEL_FIELD;
      }
    }
    else for (int i = 0; i < 4; i++) {
      *(out++) = outputs[i] & LEVEL_FIELD;
    }
  }
  // We could do this right, but let's just assume we can't
  else for (int i = 0; i < 4; i++) {
    *(out++) = LEVEL_X;
  }
  // Append the state information (last clock)
  *(out) = inputs[5] & LEVEL_FIELD;
  // Put the results into the returned output
  memcpy(outputs, outbuf, 5);
  if (ci) {
    // Example of sending back violation errors
    ci->free_errors = 1;
    ci->errors[0] = strdup("This is a test 1.");
    ci->errors[1] = strdup("This is a test 2.");
    ci->errors[2] = strdup("This is a test 3.");
  }
}
}

Compiling C Models

C behavioral models must be compiled as shared libraries:

# Linux/Unix:
gcc -shared -fPIC -o shift4c.so shift4c.c -I/path/to/simic/include

# The resulting .so file must be in the current directory or -I path

Complete Example: 74HC195 Shift Register

Component Overview

The 74HC195 is a 4-bit parallel-access shift register with the following features:

Planning the Implementation

1. Define Inputs and Outputs

TYPE=74hc195 COMPOSITION=BEHAVIORAL BEHAVIORAL=simicpy
INPUTS=clk,clr_n,sh_ld_n,ser,a,b,c,d
OUTPUTS=qa,qb,qc,qd,qd_n

# Inputs:
#   clk     - Clock input (positive edge active)
#   clr_n   - Clear (active low, asynchronous)
#   sh_ld_n - Shift/Load control (0=load, 1=shift)
#   ser     - Serial data input
#   a,b,c,d - Parallel data inputs
# Outputs:
#   qa,qb,qc,qd - Parallel outputs
#   qd_n        - Complement of qd

2. Identify State Variables

3. Key Implementation Patterns

Implementation Highlights

from simic import LEVEL_FIELD, LEVEL_ZERO, LEVEL_ONE, LEVEL_X

class HC74195:
    """74HC195 4-bit parallel-access shift register."""

    def __init__(self, part_name):
        self.part_name = part_name
        self.register = 0x00  # 4-bit register
        self.prev_clk = LEVEL_ZERO

    def evalu8(self, inputs, outputs, ci):
        # Parse inputs
        clk = inputs[0] & LEVEL_FIELD
        clr_n = inputs[1] & LEVEL_FIELD
        sh_ld_n = inputs[2] & LEVEL_FIELD
        ser = inputs[3] & LEVEL_FIELD
        a = inputs[4] & LEVEL_FIELD
        b = inputs[5] & LEVEL_FIELD
        c = inputs[6] & LEVEL_FIELD
        d = inputs[7] & LEVEL_FIELD

        # Asynchronous clear (active low)
        if clr_n == LEVEL_ZERO:
            self.register = 0x00
            self.prev_clk = clk
            return [LEVEL_ZERO, LEVEL_ZERO, LEVEL_ZERO, LEVEL_ZERO, LEVEL_ONE]

        # Detect rising clock edge
        if clk == LEVEL_ONE and self.prev_clk != LEVEL_ONE:
            if sh_ld_n == LEVEL_ZERO:
                # Parallel load mode
                self.register = (
                    ((a == LEVEL_ONE) << 3) |
                    ((b == LEVEL_ONE) << 2) |
                    ((c == LEVEL_ONE) << 1) |
                    ((d == LEVEL_ONE) << 0)
                )
            else:
                # Shift mode: shift right, serial input to MSB
                self.register = (self.register >> 1) & 0x07
                if ser == LEVEL_ONE:
                    self.register |= 0x08

        self.prev_clk = clk

        # Generate outputs
        qa = LEVEL_ONE if (self.register & 0x08) else LEVEL_ZERO
        qb = LEVEL_ONE if (self.register & 0x04) else LEVEL_ZERO
        qc = LEVEL_ONE if (self.register & 0x02) else LEVEL_ZERO
        qd = LEVEL_ONE if (self.register & 0x01) else LEVEL_ZERO
        qd_n = LEVEL_ZERO if (self.register & 0x01) else LEVEL_ONE

        return (bytes([qa, qb, qc, qd, qd_n]), None)

Full Source: See the complete, production-quality implementation with X-propagation handling and detailed comments at: /home/gary/workspace/simic/tests/behavioral/python/HC74195.py

Timing Checks

Behavioral models can report timing violations (setup time, hold time, pulse width) using the CHECKINFO structure. This allows your models to act as timing monitors.

Declaring Timing Checks in Netlist

TYPE=dff-with-checks COMPOSITION=BEHAVIORAL BEHAVIORAL=simicpy
INPUTS=clk,d
OUTPUTS=q
TIMING-CHECKS=setup=2,hold=1,pulse=5

CHECKINFO Structure (Python)

When timing checks are declared, evalu8() receives a fourth parameter:

def evalu8(self, inputs, outputs, checkinfo=None):
    """
    Args:
        inputs: Input signal values
        checkinfo: Dictionary with timing parameters, or None if no checks declared
    """
    if checkinfo is not None:
        setup_time = checkinfo['setup']    # In simulation time units
        hold_time = checkinfo['hold']
        pulse_width = checkinfo['pulse']
        current_time = checkinfo['time']   # Current simulation time

        # Check for violations and report
        if violation_detected:
            checkinfo['error'] = f"Setup violation at {current_time}: ..."

Reporting Errors

To report a timing violation, set the 'error' key in checkinfo:

if data_changed_too_close_to_clock:
    checkinfo['error'] = (
        f"Setup time violation in {self.part_name}: "
        f"data changed {actual_time} time units before clock, "
        f"required {setup_time} time units"
    )
    # Violation logged, simulation continues
Note: All timing values in Simic are integers in "time units". The relationship of time units to real time (nanoseconds, picoseconds, etc.) is established when delays and timing constraints are defined in the netlist using ODEL, TPHL, etc. You can use floating-point arithmetic for computations, but all time values passed to/from Simic are integers.

Best Practices & Common Pitfalls

Edge Detection Mistakes

Common Error: Forgetting to save previous clock state
# WRONG: prev_clk never updated
def evalu8(self, inputs, outputs, ci):
    clk = inputs[0] & LEVEL_FIELD
    if clk == LEVEL_ONE and self.prev_clk == LEVEL_ZERO:
        self.count += 1
    # BUG: prev_clk not updated, edge detected every time!
    return (bytes([self.count)]

# RIGHT: Always update prev_clk
def evalu8(self, inputs, ci, outputs, ci):
    clk = inputs[0] & LEVEL_FIELD
    if clk == LEVEL_ONE and self.prev_clk == LEVEL_ZERO:
        self.count += 1
    self.prev_clk = clk  # Critical: update for next call
    return (bytes([self.count]))

Forgetting LEVEL_FIELD Mask

# WRONG: Direct comparison without mask
if inputs[0] == LEVEL_ONE:  # May fail due to internal flags
    ...

# RIGHT: Always mask before comparing
if (inputs[0] & LEVEL_FIELD) == LEVEL_ONE:
    ...

Output Order Mistakes

# Netlist declares: OUTPUTS=q,q_n,overflow
# Must return in exact same order:
return (bytes([q_value, q_n_value, overflow_value]), None)

# WRONG order will connect outputs to wrong pins:
return (bytes([overflow_value, q_value, q_n_value]))  # BUG!

X-Propagation Guidelines

Performance Considerations

API Reference

Python Constants

Constant Value Description
LEVEL_FIELD 0x03 Bit mask to extract logic level from signal value
LEVEL_ZERO 0x00 Logic level 0 (low, false)
LEVEL_X 0x01 Unknown or uninitialized state
LEVEL_ONE 0x03 Logic level 1 (high, true)
STRENGTH_FIELD 0xFC Bit mask to extract strength from signal value
POWER 0x24 Power supply strength
DRIVING 0x48 Active driver strength (default)
RESISTIVE 0x6C Resistive/weak driver strength
FLOATING 0x90 High impedance strength

Python Class Methods

Method Parameters Description
__init__(self, part_name) part_name: string Constructor called once per instance during circuit compilation. Initialize state variables here.
evalu8(self, inputs, outputs, checkinfo=None) inputs: list of bytes
inputs: list of bytes
checkinfo: dict or None
Called whenever any input changes. Return list of output values, empty list for no outputs, or None if outputs unchanged.

C Constants and Macros

Constant Value Description
LEVEL_FIELD 0x03 Bit mask to extract logic level
LEVEL_ZERO 0x00 Logic level 0
LEVEL_X 0x01 Unknown state
LEVEL_ONE 0x03 Logic level 1
STRENGTH_FIELD 0xFC Bit mask to extract strength
POWER 0x24 Power supply strength
DRIVING 0x48 Active driver strength
RESISTIVE 0x6C Resistive driver strength
FLOATING 0x90 High impedance strength

C Function Signatures

Function Description
void TYPE_NAME_init(char *part_name) Optional initialization function called once per instance. Use for setup that needs the part name.
void TYPE_NAME(int *inputs, int *outputs, CHECKINFO *check) Required evaluation function called on input changes.
  • inputs: array of input values
  • outputs: array to fill with output values
  • check: pointer to timing check structure (may be NULL)

Netlist Attributes For Instantiating a Behavioral Model

Attribute Values Description
COMPOSITION BEHAVIORAL Required: declares this type as behavioral
BEHAVIORAL simicpy or simicc Required: specifies Python or C implementation
TIMING-CHECKS begin;setup=X,hold=Y,pulse=Z;end; Optional: timing parameters passed to checkinfo