SPI controlled ADC example

Contents

SPI controlled ADC example#

This example demonstrates connecting a simple SPI block with an ADC modeled in SPICE.

Running the test#

Install the required Python packages:

pip install pytest cocotb cocotbext-spi

Execute the cocotb test:

python test_spi_adc.py
* flash ADC 8-bit

* 1-bit ADC
.subckt ad1bit in ref comp out ref_out vcc
B1 ref_out 0 V = v(ref)/2
B2 comp    0 V = (v(in) > v(ref_out)) ? v(vcc) : 0
B3 out     0 V = (v(in) > v(ref_out)) ? v(in)-v(ref_out) : v(in)
.ends

.subckt adc_ideal_8bit in ref vcc ad7 ad6 ad5 ad4 ad3 ad2 ad1 ad0
Xad7  in  ref  ad7  in7  ref7  vcc ad1bit
Xad6  in7  ref7  ad6  in6  ref6  vcc ad1bit
Xad5  in6  ref6  ad5  in5  ref5  vcc ad1bit
Xad4  in5  ref5  ad4  in4  ref4  vcc ad1bit
Xad3  in4  ref4  ad3  in3  ref3  vcc ad1bit
Xad2  in3  ref3  ad2  in2  ref2  vcc ad1bit
Xad1  in2  ref2  ad1  in1  ref1  vcc ad1bit
Xad0  in1  ref1  ad0  in0  ref0  vcc ad1bit
.ends

Vvin vin 0 0 external
Vcc vcc 0 1
Vrange0 range0 0 0 external
Vrange1 range1 0 0 external
Bref ref 0 V = (v(range1) > 0.5 ? 3.3 : (v(range0) > 0.5 ? 2.0 : 1.0))

Xadc vin ref vcc code[7] code[6] code[5] code[4] code[3] code[2] code[1] code[0] adc_ideal_8bit

.tran 1ns 1

.end
`timescale 1ns/1ps

module adc_core(
    input real vin,
    output [7:0] code,
    input range0,
    input range1
);
    // empty - replaced by SPICE
endmodule

module spi_adc(
    input sclk,
    input mosi,
    output miso,
    input cs,
    input real vin
);
    // internal wires for ADC connection
    wire [7:0] code;
    reg [1:0] range = 0;

    // instantiate analog ADC core
    adc_core adc_inst(
        .vin(vin),
        .code(code),
        .range0(range[0]),
        .range1(range[1])
    );

    // SPI logic
    reg [7:0] mosi_shift = 0;
    reg [7:0] miso_shift = 0;
    reg [3:0] bit_cnt = 0;

    always @(posedge sclk or posedge cs) begin
        if (cs) begin
            bit_cnt <= 0;
        end else begin
            mosi_shift <= {mosi_shift[6:0], mosi};
            bit_cnt <= bit_cnt + 1;
        end
    end

    always @(negedge sclk or posedge cs) begin
        if (cs) begin
            miso_shift <= code;
        end else begin
            miso_shift <= {miso_shift[6:0], 1'b0};
        end
    end

    assign miso = miso_shift[7];

    always @(posedge cs) begin
        range <= mosi_shift[1:0];
    end

endmodule
import os
import pathlib

os.environ.setdefault("COCOTB_RESOLVE_X", "ZEROS")
import cocotb
from cocotb.triggers import Timer
from cocotbext.spi import SpiBus, SpiMaster, SpiConfig
from cocotb.runner import get_runner
import spicebind


@cocotb.test()
async def run_test(dut):
    bus = SpiBus.from_entity(dut)
    master = SpiMaster(bus, SpiConfig(sclk_freq=1e6))

    dut.vin.value = 1.0
    await Timer(1, units="us")

    # range 1V
    await master.write([0x00])
    await master.read()
    await Timer(100, units="ns")
    await master.write([0x00])  # capture new code
    await master.read()
    await master.write([0x00])  # shift out updated code
    code = (await master.read())[0]
    dut._log.info(f"range=1V code={code}")
    dut._log.info(f"range bits {int(dut.range.value)}")
    assert code == 255

    # range 2V
    await master.write([0x01])
    await master.read()
    await Timer(100, units="ns")
    await master.write([0x01])
    await master.read()
    await master.write([0x01])
    code = (await master.read())[0]
    dut._log.info(f"range=2V code={code}")
    dut._log.info(f"range bits {int(dut.range.value)}")
    assert 125 <= code <= 130

    # range 3.3V
    await master.write([0x02])
    await master.read()
    await Timer(100, units="ns")
    await master.write([0x02])
    await master.read()
    await master.write([0x02])
    code = (await master.read())[0]
    dut._log.info(f"range=3.3V code={code}")
    dut._log.info(f"range bits {int(dut.range.value)}")
    assert 75 <= code <= 80


def test_spi_adc():
    sim = os.getenv("SIM", "icarus")
    proj_path = pathlib.Path(__file__).resolve().parent
    sources = [proj_path / "spi_adc.v"]

    runner = get_runner(sim)
    runner.build(
        sources=sources,
        hdl_toplevel="spi_adc",
        always=True,
    )

    runner.test(
        hdl_toplevel="spi_adc",
        test_module="test_spi_adc",
        test_args=["-M", spicebind.get_lib_dir(), "-m", "spicebind_vpi"],
        extra_env={
            "SPICE_NETLIST": str(proj_path / "spi_adc.cir"),
            "HDL_INSTANCE": "spi_adc.adc_inst",
            "COCOTB_RESOLVE_X": "ZEROS",
        },
    )


if __name__ == "__main__":
    test_spi_adc()