Models of Analog Circuits
Added in version 1.6.0.
This is the example analog_model
showing how to use Python models
for analog circuits together with a digital part.
For an FPGA, these analog circuits would be implemented off-chip,
while for an ASIC, they would usually co-exist with the digital part on the same die.
The Python model consists of an Analog Front-End (AFE) in file afe.py
containing
a Programmable Gain Amplifier (PGA) with a selectable gain of 5.0
and 10.0
,
and a 13-bit Analog-to-Digital Converter (ADC) with a reference voltage of 2.0 V
.
These analog models hand over data via a blocking cocotb.queue.Queue
.
The digital part (in digital.sv
)
monitors the measurement value converted by the ADC
and selects the gain of the PGA based on the received value.
A test test_analog_model.py
exercises these submodules.
When running the example, you will get the following output:
0.00ns INFO ...test_analog_model.0x7ff913700490 decorators.py:313 in _advance Starting test: "test_analog_model"
Description: Exercise an Analog Front-end and its digital controller.
1001.00ns INFO cocotb.digital test_analog_model.py:55 in test_analog_model AFE converted input value 0.1V to 2047
3000.00ns (digital) HDL got meas_val=2047 (0x07ff)
3000.00ns (digital) PGA gain select was 0 --> calculated AFE input value back to 0.099963
3000.00ns (digital) Measurement value is less than 30% of max, switching PGA gain from 5.0 to 10.0
7301.00ns INFO cocotb.digital test_analog_model.py:55 in test_analog_model AFE converted input value 0.1V to 4095
9000.00ns (digital) HDL got meas_val=4095 (0x0fff)
9000.00ns (digital) PGA gain select was 1 --> calculated AFE input value back to 0.099988
13301.00ns INFO cocotb.digital test_analog_model.py:55 in test_analog_model AFE converted input value 0.0V to 0
15000.00ns (digital) HDL got meas_val=0 (0x0000)
15000.00ns (digital) PGA gain select was 1 --> calculated AFE input value back to 0.000000
Saturating measurement value 10238 to [0:8191]!
19301.00ns INFO cocotb.digital test_analog_model.py:55 in test_analog_model AFE converted input value 0.25V to 8191
21000.00ns (digital) HDL got meas_val=8191 (0x1fff)
21000.00ns (digital) PGA gain select was 1 --> calculated AFE input value back to 0.200000
21000.00ns (digital) Measurement value is more than 70% of max, switching PGA gain from 10.0 to 5.0
25301.00ns INFO cocotb.digital test_analog_model.py:55 in test_analog_model AFE converted input value 0.25V to 5119
27000.00ns (digital) HDL got meas_val=5119 (0x13ff)
27000.00ns (digital) PGA gain select was 0 --> calculated AFE input value back to 0.249982
30301.00ns INFO cocotb.regression regression.py:364 in _score_test Test Passed: test_analog_model
You can view the source code of the example by clicking the file names below.
afe.py
# This file is public domain, it can be freely copied without restrictions.
# SPDX-License-Identifier: CC0-1.0
from typing import Optional
import cocotb
from cocotb.queue import Queue
from cocotb.triggers import Timer
"""
This is a Python model of an Analog Front-End (AFE) containing
a Programmable Gain Amplifier (PGA) with a selectable gain of 5.0 and 10.0
and a 13-bit Analog-to-Digital Converter (ADC) with a reference voltage of 2.0 V.
These analog models hand over data via a blocking :class:`cocotb.queue.Queue`.
"""
class PGA:
"""
Model of a Programmable Gain Amplifier.
*gain* is the amplification factor.
"""
def __init__(
self,
gain: float = 5.0,
in_queue: Optional[Queue] = None,
out_queue: Optional[Queue] = None,
) -> None:
self._gain = gain
self.in_queue = in_queue
self.out_queue = out_queue
cocotb.start_soon(self.run())
@property
def gain(self) -> float:
return self._gain
@gain.setter
def gain(self, val: float) -> None:
self._gain = val
async def run(self) -> None:
while True:
in_val_V = await self.in_queue.get()
await Timer(1.0, "ns") # delay
await self.out_queue.put(in_val_V * self._gain)
class ADC:
"""
Model of an Analog-to-Digital Converter.
*ref_val_V* is the reference voltage in V, *n_bits* is the resolution in bits.
"""
def __init__(
self,
ref_val_V: float = 2.0,
n_bits: int = 13,
in_queue: Optional[Queue] = None,
out_queue: Optional[Queue] = None,
) -> None:
self.ref_val_V = ref_val_V
self.min_val = 0
self.max_val = 2**n_bits - 1
self.in_queue = in_queue
self.out_queue = out_queue
cocotb.start_soon(self.run())
async def run(self) -> None:
while True:
in_val_V = await self.in_queue.get() # sample immediately
await Timer(1, "us") # wait for conversion time
out = int((in_val_V / self.ref_val_V) * self.max_val)
if not (self.min_val <= out <= self.max_val):
print(
f"Saturating measurement value {out} to [{self.min_val}:{self.max_val}]!"
)
await self.out_queue.put(min(max(self.min_val, out), self.max_val))
class AFE:
"""
Model of an Analog Front-End.
This model instantiates the sub-models PGA and ADC.
"""
def __init__(
self, in_queue: Optional[Queue] = None, out_queue: Optional[Queue] = None
) -> None:
self.in_queue = in_queue
self.out_queue = out_queue
self.pga_to_adc_queue = Queue()
self.pga = PGA(in_queue=self.in_queue, out_queue=self.pga_to_adc_queue)
self.adc = ADC(in_queue=self.pga_to_adc_queue, out_queue=self.out_queue)
digital.sv
// This file is public domain, it can be freely copied without restrictions.
// SPDX-License-Identifier: CC0-1.0
module digital (
input logic clk,
input logic [13-1:0] meas_val,
input logic meas_val_valid,
output logic pga_high_gain
);
timeunit 1s;
timeprecision 1ns;
real max_val = 2**$bits(meas_val)-1;
real ref_val_V = 2.0;
initial begin
pga_high_gain = 0; // start with low gain
// prints %t scaled in ns (-9), with 2 precision digits,
// and the "ns" string, last number is the minimum field width
$timeformat(-9, 2, "ns", 11);
end
always @(posedge clk) begin
if (meas_val_valid == 1) begin
$display("%t (%M) HDL got meas_val=%0d (0x%x)", $realtime, meas_val, meas_val);
if (pga_high_gain == 0) begin
$display("%t (%M) PGA gain select was %0d --> calculated AFE input value back to %0f",
$realtime, pga_high_gain, meas_val/max_val/ 5.0 * ref_val_V);
end else begin
$display("%t (%M) PGA gain select was %0d --> calculated AFE input value back to %0f",
$realtime, pga_high_gain, meas_val/max_val/10.0 * ref_val_V);
end
// Automatic gain select:
// set new gain for the next measurement
if (meas_val > 0.7 * max_val) begin
if (pga_high_gain == 1) begin
$display("%t (%M) Measurement value is more than 70%% of max, switching PGA gain from 10.0 to 5.0", $realtime);
end
pga_high_gain = 0;
end else if (meas_val < 0.3 * max_val) begin
if (pga_high_gain == 0) begin
$display("%t (%M) Measurement value is less than 30%% of max, switching PGA gain from 5.0 to 10.0", $realtime);
end
pga_high_gain = 1;
end else begin
; // NOP; leave gain unchanged
end
end // if (meas_val_valid == 1)
end
endmodule
test_analog_model.py
# This file is public domain, it can be freely copied without restrictions.
# SPDX-License-Identifier: CC0-1.0
from afe import AFE
import cocotb
from cocotb.clock import Clock
from cocotb.queue import Queue
from cocotb.triggers import Edge, RisingEdge, Timer
"""
This example uses the Python model of an Analog Front-End (AFE)
which contains a Programmable Gain Amplifier (PGA)
and an Analog-to-Digital Converter (ADC).
The digital part (in HDL) monitors the measurement value converted by the ADC
and selects the gain of the PGA based on the received value.
"""
async def gain_select(digital, afe) -> None:
"""Set gain factor of PGA when gain select from the HDL changes."""
while True:
await Edge(digital.pga_high_gain)
if digital.pga_high_gain.value == 0:
afe.pga.gain = 5.0
else:
afe.pga.gain = 10.0
@cocotb.test()
async def test_analog_model(digital) -> None:
"""Exercise an Analog Front-end and its digital controller."""
clock = Clock(digital.clk, 1, units="us") # create a 1us period clock on port clk
cocotb.start_soon(clock.start()) # start the clock
afe_in_queue = Queue()
afe_out_queue = Queue()
afe = AFE(
in_queue=afe_in_queue, out_queue=afe_out_queue
) # instantiate the analog front-end
cocotb.start_soon(gain_select(digital, afe))
for in_V in [0.1, 0.1, 0.0, 0.25, 0.25]:
# set the input voltage
await afe_in_queue.put(in_V)
# get the converted digital value
afe_out = await afe_out_queue.get()
digital._log.info(f"AFE converted input value {in_V}V to {int(afe_out)}")
# hand digital value over as "meas_val" to digital part (HDL)
# "meas_val_valid" pulses for one clock cycle
await RisingEdge(digital.clk)
digital.meas_val.value = afe_out
digital.meas_val_valid.value = 1
await RisingEdge(digital.clk)
digital.meas_val_valid.value = 0
await Timer(3.3, "us")
Makefile
# This file is public domain, it can be freely copied without restrictions.
# SPDX-License-Identifier: CC0-1.0
TOPLEVEL_LANG = verilog
VERILOG_SOURCES = $(shell pwd)/digital.sv
TOPLEVEL = digital
MODULE = test_analog_model
ifneq ($(filter $(SIM),ius xcelium),)
SIM_ARGS += -unbuffered
endif
include $(shell cocotb-config --makefiles)/Makefile.sim