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()