Models of Analog Circuits

New 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