SAR ADC¶
You can also find this example as a single file in the GitHub repository.This advanced example demonstrates a SAR ADC (Successive Approximation Register Analog-to-Digital Converter), one of the most popular ADC architectures. This example also shows how to create custom blocks in PathSim by extending the base Block class.
Successive Approximation Register (SAR) ADC Principle¶
A SAR ADC converts analog signals to digital using a binary search algorithm:
Sample the input voltage
Test the MSB (Most Significant Bit) by comparing to DAC output
Keep the bit if comparison succeeds, discard if it fails
Repeat for each bit from MSB to LSB
Output the complete digital word
This requires N comparisons for N bits, making it efficient for medium-speed, medium-resolution applications (10-18 bits, up to several MHz).
Custom SAR Logic Block¶
We’ll implement the SAR control logic as a custom block using PathSim’s event system.
This example shows how to create custom blocks by extending the Block class and using Schedule events for discrete-time logic.
[1]:
import numpy as np
import matplotlib.pyplot as plt
# Apply PathSim docs matplotlib style for consistent, theme-friendly figures
plt.style.use('../pathsim_docs.mplstyle')
from pathsim import Simulation, Connection
from pathsim.blocks import (
Adder, Scope, Source, ButterworthLowpassFilter,
SampleHold, Comparator, DAC
)
from pathsim.solvers import RKBS32
Creating a Custom SAR Logic Block¶
This is one of PathSim’s powerful features - you can create custom blocks with complex behavior. The SAR block:
Uses scheduled events to step through bits
Implements the binary search algorithm
Outputs N parallel digital signals (one per bit)
[2]:
from pathsim.blocks._block import Block
from pathsim.events import Schedule
class SAR(Block):
"""Successive Approximation Register Logic
Implements SAR algorithm for ADC conversion:
- Reads comparator result
- Updates trial bit pattern
- Outputs N-bit digital word
"""
def __init__(self, n_bits=4, T=1, tau=0):
super().__init__()
self.n_bits = n_bits
self.T = T
self.tau = tau
self.register = 0
self.trial_weight = 1 << (self.n_bits - 1) # Start with MSB
self.outputs = {i: 0 for i in range(self.n_bits)}
def _step(t):
"""SAR algorithm step - executes at each clock cycle"""
comparator_result = self.inputs[0]
previous_weight = (self.trial_weight << 1) if self.trial_weight > 0 else 1
# If previous comparison failed, clear that bit
if previous_weight <= (1 << (self.n_bits -1)) and comparator_result == 0:
self.register &= ~previous_weight
# Set current trial bit
self.register |= self.trial_weight
# Update all output bits
for i in range(self.n_bits):
self.outputs[i] = (self.register >> i) & 1
# Move to next bit or restart
if self.trial_weight == 1:
self.trial_weight = 1 << (self.n_bits - 1)
self.register = 0
else:
self.trial_weight >>= 1
# Schedule event for SAR stepping
self.events = [
Schedule(
t_start=self.tau,
t_period=self.T/self.n_bits, # One step per bit
func_act=_step
)
]
def __len__(self):
return 0
System Parameters¶
We’ll use:
8-bit resolution
50 Hz sampling frequency
Modulated sine wave as input signal
[3]:
n = 8 # Number of bits
f_clk = 50 # Sampling frequency
T_clk = 1.0 / f_clk # Sampling period
Block Diagram Setup¶
The system consists of:
src: Modulated sine wave source
sah: Sample & Hold to freeze input during conversion
sub: Subtractor (input - DAC)
cpt: Comparator
sar: Custom SAR logic
dac1: Fast DAC for comparison (updates every bit)
dac2: Output DAC (updates every sample)
lpf: Lowpass filter for reconstruction
[4]:
# Blocks that define the system
src = Source(lambda t: np.sin(2*np.pi*t) * np.cos(5*np.pi*t))
sah = SampleHold(T=T_clk)
sub = Adder("+-")
cpt = Comparator(span=[0, 1])
dac1 = DAC(n_bits=n, T=T_clk/n, tau=T_clk*2e-3) # Fast DAC for comparison
dac2 = DAC(n_bits=n, T=T_clk, tau=T_clk) # Output DAC
lpf = ButterworthLowpassFilter(f_clk/5, n=3) # Reconstruction filter
sar = SAR(n_bits=n, T=T_clk, tau=T_clk*1e-3)
sco = Scope(labels=["src", "sah", "dac1", "dac2", "lpf"])
blocks = [src, cpt, dac1, dac2, lpf, sar, sah, sub, sco]
Connections¶
The connections form the SAR ADC loop. Notice how the 8 digital bits from SAR connect to both DACs in parallel.
[5]:
# Connections between the blocks
connections = [
Connection(src, sah, sco[0]), # Source to S&H and scope
Connection(sah, sub[0], sco[1]), # S&H to subtractor
Connection(dac1, sub[1], sco[2]), # DAC1 feedback to subtractor
Connection(dac2, lpf, sco[3]), # DAC2 to filter
Connection(lpf, sco[4]), # Filtered output
Connection(sub, cpt), # Difference to comparator
Connection(cpt, sar) # Comparator to SAR logic
]
# Connect all N bits from SAR to both DACs
for i in range(n):
connections.append(
Connection(sar[i], dac1[i], dac2[i])
)
Simulation¶
We run the simulation with an adaptive solver that can handle the discrete-time events efficiently.
[6]:
# Simulation with adaptive solver
Sim = Simulation(
blocks,
connections,
Solver=RKBS32
)
# Run simulation for 1 second
Sim.run(1)
10:59:30 - INFO - LOGGING (log: True)
10:59:30 - INFO - BLOCKS (total: 9, dynamic: 1, static: 8, eventful: 5)
10:59:30 - INFO - GRAPH (nodes: 9, edges: 27, alg. depth: 3, loop depth: 0, runtime: 0.145ms)
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In[6], line 9
2 Sim = Simulation(
3 blocks,
4 connections,
5 Solver=RKBS32
6 )
8 # Run simulation for 1 second
----> 9 Sim.run(1)
File ~/checkouts/readthedocs.org/user_builds/pathsim/envs/v0.12.4/lib/python3.13/site-packages/pathsim/simulation.py:1718, in Simulation.run(self, duration, reset, adaptive)
1715 _dt = self.dt
1717 #initial system function evaluation
-> 1718 self._update(self.time)
1719 initial_evals = 1
1721 #catch and resolve initial events
File ~/checkouts/readthedocs.org/user_builds/pathsim/envs/v0.12.4/lib/python3.13/site-packages/pathsim/simulation.py:913, in Simulation._update(self, t)
882 """Distribute information within the system by evaluating the directed acyclic graph
883 (DAG) formed by the algebraic passthroughs of the blocks and resolving algebraic loops
884 through accelerated fixed-point iterations.
(...) 909 evaluation time for system function
910 """
912 #evaluate DAG
--> 913 self._dag(t)
915 #algebraic loops -> solve them
916 if self.graph.has_loops:
File ~/checkouts/readthedocs.org/user_builds/pathsim/envs/v0.12.4/lib/python3.13/site-packages/pathsim/simulation.py:938, in Simulation._dag(self, t)
936 #update connenctions at algebraic depth (data transfer)
937 for connection in connections_dag:
--> 938 if connection: connection.update()
File ~/checkouts/readthedocs.org/user_builds/pathsim/envs/v0.12.4/lib/python3.13/site-packages/pathsim/connection.py:262, in Connection.update(self)
258 """Transfers data from the source block output port
259 to the target block input port.
260 """
261 for trg in self.targets:
--> 262 self.source.to(trg)
File ~/checkouts/readthedocs.org/user_builds/pathsim/envs/v0.12.4/lib/python3.13/site-packages/pathsim/utils/portreference.py:139, in PortReference.to(self, other)
127 """Transfer the data between two `PortReference` instances,
128 in this direction `self` -> `other`. From outputs to inputs.
129
(...) 135 the `PortReference` instance to transfer data to from `self`
136 """
138 # Get cached integer indices (lazy, resolved once, reused forever)
--> 139 src_indices = self._get_output_indices()
140 dst_indices = other._get_input_indices()
142 # Single vectorized transfer
File ~/checkouts/readthedocs.org/user_builds/pathsim/envs/v0.12.4/lib/python3.13/site-packages/pathsim/utils/portreference.py:98, in PortReference._get_output_indices(self)
91 """Get cached output indices, resolving string aliases to integers.
92 Also expands the output array if needed.
93 """
94 if self._output_indices is None:
95
96 # Resolve indices/aliases through mapping
97 self._output_indices = np.array([
---> 98 self.block.outputs._map(p) for p in self.ports
99 ], dtype=np.intp)
101 # Resize register to accomodate indices
102 max_idx = self._output_indices.max()
AttributeError: 'dict' object has no attribute '_map'
Results¶
The plots show:
src: Original analog input
sah: Sampled and held signal
dac1: Fast DAC during conversion (shows binary search)
dac2: Output DAC (quantized signal)
lpf: Reconstructed signal after filtering
Notice how dac1 shows the successive approximation steps within each sample period!
[7]:
Sim.plot()
plt.show()