diff --git a/src/main/scala/chisel/lib/spi/README.md b/src/main/scala/chisel/lib/spi/README.md new file mode 100644 index 0000000..155a1eb --- /dev/null +++ b/src/main/scala/chisel/lib/spi/README.md @@ -0,0 +1,27 @@ +# Serial Peripheral Interface (SPI) + +Minimalistic version of SPI interface. + +Features: +- Modes 00, 01, 10 and 11 +- Word length in parameters + +Status: Tested in mode 00, 01, 10, 11 with cocotb + +# Test + +prerequisites: +- cocotb +- cocotbext-spi + +* Generate verilog sources +```sbt "runMain chisel.lib.spi.MasterOneCS"``` + +* Run testbench +``` +cd src/test/scala/chisel/lib/spi +make +``` + +* Display wave +```gtkwave dump.vcd``` diff --git a/src/main/scala/chisel/lib/spi/Spi.scala b/src/main/scala/chisel/lib/spi/Spi.scala new file mode 100644 index 0000000..887932d --- /dev/null +++ b/src/main/scala/chisel/lib/spi/Spi.scala @@ -0,0 +1,130 @@ +/* + * + * Serial Peripheral Interface (SPI) interface + * + * Author: Kevin Joly (kevin.joly@armadeus.com) + * + */ +package chisel.lib.spi + +import chisel3._ +import chisel3.experimental.Analog +import chisel3.experimental.ChiselEnum +import chisel3.stage.ChiselStage +import chisel3.util._ + +class Master(frequency: Int, clkfreq: Int, bsize: Int) extends Module { + val io = IO(new Bundle { + val cpol = Input(Bool()) + val cpha = Input(Bool()) + val msbfirst = Input(Bool()) + val mosi = Output(Bool()) + val miso = Input(Bool()) + val sclk = Output(Bool()) + val din = Decoupled(UInt(bsize.W)) + val dout = Flipped(Decoupled(UInt(bsize.W))) + val busy = Output(Bool()) + }) + + object State extends ChiselEnum { + val sIdle, sHalfCycle, sLoad, sShift = Value + } + + val state = RegInit(State.sIdle) + + val clockPrescaler = (frequency + clkfreq / 2) / clkfreq / 2 - 1 + val CLKPRE = clockPrescaler.asUInt(clockPrescaler.W) + + val bits = RegInit(0.U((bsize - 1).W)) + val regout = RegInit(0.U(bsize.W)) + val regin = RegInit(0.U(bsize.W)) + val cnt = RegInit(0.U(clockPrescaler.W)) + + val cpolReg = RegInit(false.B) + val cphaReg = RegInit(false.B) + val msbfirstReg = RegInit(false.B) + + switch(state) { + is(State.sIdle) { + + cpolReg := io.cpol + cphaReg := io.cpha + msbfirstReg := io.msbfirst + + when(io.dout.valid) { + regout := io.dout.bits + bits := bsize.asUInt - 1.U + cnt := CLKPRE + when(cphaReg) { + state := State.sHalfCycle + }.otherwise { + state := State.sLoad + } + } + regin := 0.U + } + is(State.sHalfCycle) { + when(cnt > 0.U) { + cnt := cnt - 1.U + }.otherwise { + cnt := CLKPRE + state := State.sLoad + } + } + is(State.sLoad) { + when(cnt > 0.U) { + cnt := cnt - 1.U + }.elsewhen(bits > 0.U || io.din.ready) { + cnt := CLKPRE + state := State.sShift + when(msbfirstReg) { + regin := Cat(regin(bsize - 2, 0), io.miso) + }.otherwise { + regin := Cat(io.miso, regin(bsize - 1, 1)) + } + } + } + is(State.sShift) { + when(cnt > 0.U) { + cnt := cnt - 1.U + }.otherwise { + when(bits > 0.U) { + cnt := CLKPRE + bits := bits - 1.U + state := State.sLoad + }.otherwise { + state := State.sIdle + } + } + } + } + + io.dout.ready := state === State.sIdle + + io.din.bits := regin + val lastBitShifted = Wire(Bool()) + lastBitShifted := (state === State.sShift) && (bits === 0.U) + io.din.valid := lastBitShifted && !RegNext(lastBitShifted) + + when(state === State.sLoad) { + io.sclk := cpolReg ^ cphaReg + }.elsewhen(state === State.sShift) { + io.sclk := !(cpolReg ^ cphaReg) + }.otherwise { + io.sclk := cpolReg + } + + when(state === State.sIdle) { + io.mosi := false.B + }.elsewhen(msbfirstReg) { + io.mosi := regout(bits) + }.otherwise { + io.mosi := regout(bsize.U - 1.U - bits) + } + + io.busy := state =/= State.sIdle +} + +object Master extends App { + (new ChiselStage).emitSystemVerilog(new Master(100000000, 10000000, 8), Array("--target-dir", "generated")) +} diff --git a/src/test/scala/chisel/lib/spi/SpiTester.scala b/src/test/scala/chisel/lib/spi/SpiTester.scala new file mode 100644 index 0000000..f34226d --- /dev/null +++ b/src/test/scala/chisel/lib/spi/SpiTester.scala @@ -0,0 +1,176 @@ +package chisel.lib.spi + +import chisel3._ +import chiseltest._ +import org.scalatest.flatspec.AnyFlatSpec + +import scala.util.Random + +class SpiMasterTest extends AnyFlatSpec with ChiselScalatestTester { + + val frequency = 100000000 + val clkfreq = 10000000 + + def transferOneWord(bsize: Int, mode: Int, msbFirst: Boolean, dut: => Master) { + test(dut) { dut => + val clkStepPerHalfSclk = (frequency + clkfreq / 2) / clkfreq / 2 - 1 + + val rnd = new Random() + val outputVal = rnd.nextLong() & (Math.pow(2, bsize).toInt - 1) + val inputVal = rnd.nextLong() & (Math.pow(2, bsize).toInt - 1) + + var sclkExp = if ((mode == 0) || (mode == 1)) false else true + + dut.clock.step() + + mode match { + case 0 => { + dut.io.cpol.poke(false.B) + dut.io.cpha.poke(false.B) + } + case 1 => { + dut.io.cpol.poke(false.B) + dut.io.cpha.poke(true.B) + } + case 2 => { + dut.io.cpol.poke(true.B) + dut.io.cpha.poke(false.B) + } + case 3 => { + dut.io.cpol.poke(true.B) + dut.io.cpha.poke(true.B) + } + } + + dut.io.msbfirst.poke(msbFirst.B) + + dut.clock.step() + + // Idle state + dut.io.sclk.expect(sclkExp) + dut.io.mosi.expect(false.B) + dut.io.busy.expect(false.B) + + // Send data + dut.io.din.ready.poke(true.B) + dut.io.dout.bits.poke(outputVal.asUInt(bsize.W)) + + dut.io.dout.valid.poke(true.B) + + dut.clock.step() + + dut.io.dout.valid.poke(false.B) + + if ((mode == 1) || (mode == 3)) { + // Wait half sclk step + for (s <- 0 to clkStepPerHalfSclk) { + dut.io.busy.expect(true.B) + dut.io.sclk.expect(sclkExp) + + dut.clock.step() + } + + sclkExp = !sclkExp + } + + val firstBit = if (msbFirst) bsize - 1 else 0 + val lastBit = if (msbFirst) 0 else bsize - 1 + val step = if (msbFirst) -1 else 1 + + for (bit <- firstBit to lastBit by step) { + + if ((inputVal & (0x1 << bit)) != 0) { + dut.io.miso.poke(true.B) + } else { + dut.io.miso.poke(false.B) + } + + for (s <- 0 to clkStepPerHalfSclk) { + dut.io.busy.expect(true.B) + dut.io.sclk.expect(sclkExp) + if ((outputVal & (0x1 << bit)) != 0) { + dut.io.mosi.expect(true.B) + } else { + dut.io.mosi.expect(false.B) + } + + dut.clock.step() + } + + sclkExp = !sclkExp + + if (bit == lastBit) { + // End of word + dut.io.din.valid.expect(true.B) + dut.io.din.bits.expect(inputVal.asUInt) + } else { + dut.io.din.valid.expect(false.B) + } + + for (s <- 0 to clkStepPerHalfSclk) { + dut.io.busy.expect(true.B) + dut.io.sclk.expect(sclkExp) + if ((outputVal & (0x1 << bit)) != 0) { + dut.io.mosi.expect(true.B) + } else { + dut.io.mosi.expect(false.B) + } + + dut.clock.step() + } + + sclkExp = !sclkExp + } + + dut.io.busy.expect(false.B) + } + } + + it should "work in mode 0 lsb first" in { + for (bsize <- Seq(4, 8, 16, 32)) { + transferOneWord(bsize, 0, false, new Master(frequency, clkfreq, bsize)) + } + } + + it should "work in mode 1 lsb first" in { + for (bsize <- Seq(4, 8, 16, 32)) { + transferOneWord(bsize, 1, false, new Master(frequency, clkfreq, bsize)) + } + } + + it should "work in mode 2 lsb first" in { + for (bsize <- Seq(4, 8, 16, 32)) { + transferOneWord(bsize, 2, false, new Master(frequency, clkfreq, bsize)) + } + } + + it should "work in mode 3 lsb first" in { + for (bsize <- Seq(4, 8, 16, 32)) { + transferOneWord(bsize, 3, false, new Master(frequency, clkfreq, bsize)) + } + } + + it should "work in mode 0 msb first" in { + for (bsize <- Seq(4, 8, 16, 32)) { + transferOneWord(bsize, 0, true, new Master(frequency, clkfreq, bsize)) + } + } + + it should "work in mode 1 msb first" in { + for (bsize <- Seq(4, 8, 16, 32)) { + transferOneWord(bsize, 1, true, new Master(frequency, clkfreq, bsize)) + } + } + + it should "work in mode 2 msb first" in { + for (bsize <- Seq(4, 8, 16, 32)) { + transferOneWord(bsize, 2, true, new Master(frequency, clkfreq, bsize)) + } + } + + it should "work in mode 3 msb first" in { + for (bsize <- Seq(4, 8, 16, 32)) { + transferOneWord(bsize, 3, true, new Master(frequency, clkfreq, bsize)) + } + } +}