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:
- Complex Standard ICs: Implementing chips like shift registers (74HC195), counters, UARTs, or ALUs where the gate-level description would be unwieldy
- Processor Models: Modeling CPUs, microcontrollers, or instruction decoders where functional behavior matters more than gate-level accuracy
- Bus Protocol Controllers: Components that implement state machines or protocols (I2C, SPI, etc.)
- Timing Monitors: "Logic analyzer" components that watch signals and report violations without affecting circuit operation
- Custom Arithmetic Units: Specialized math operations (multiply, divide, floating-point) that would be impractical to describe at the gate level
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.
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:
TYPE=my-component→my_component.py(hyphens become underscores) - C:
TYPE=my-component→my_component.cand functionmy_component() - Files searched in: current directory, then directories specified with
-Iflag
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
- Always update state at the end of evalu8(): Don't modify state partway through and then return early, as this can cause inconsistent behavior
- Initialize all state in __init__(): Don't rely on Python's default None values, explicitly initialize to appropriate logic levels
- Be careful with mutable state: If you store lists or dictionaries, remember they're shared across simulation time
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:
- 4 parallel data inputs (D0-D3) with parallel load capability
- Serial data input for shift operations
- Shift/Load control pin
- Clock input (positive edge triggered)
- Asynchronous clear (active low)
- 4 parallel outputs (Q0-Q3) plus complementary outputs
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
register- 4-bit value holding current stateprev_clk- Previous clock state for edge detection
3. Key Implementation Patterns
- Asynchronous clear: Check clr_n first, override all other logic
- Clock edge detection: Compare current clk with prev_clk
- Shift vs. Load: On rising edge, check sh_ld_n to decide operation
- Output generation: Extract individual bits from register state
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
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
# 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
- Propagate X conservatively: If you can't determine the output from known inputs, return LEVEL_X
- AND/NAND gates: 0 AND X = 0, but 1 AND X = X
- OR/NOR gates: 1 OR X = 1, but 0 OR X = X
- XOR gates: Always propagate X (X XOR anything = X)
- Registers: If clock or data is X at trigger point, output becomes X
Performance Considerations
- Minimize work in evalu8(): This function is called on every input transition
- Return None for no change: If outputs haven't changed, return None to avoid unnecessary event scheduling
- Use integer arithmetic: Avoid floating point or complex data structures in hot paths
- Cache computed values: If computing the same value multiple times per call, compute once and store in local variable
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 bytesinputs: list of bytescheckinfo: 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.
|
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 |