max2771 spi liens has been added.
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
QUESTA ?=
|
||||
VLOG := $(if $(QUESTA),$(QUESTA)/bin/)vlog
|
||||
VSIM := $(if $(QUESTA),$(QUESTA)/bin/)vsim
|
||||
WORK := work
|
||||
|
||||
.PHONY: test clean
|
||||
|
||||
test: clean
|
||||
$(VLOG) -sv -work $(WORK) rtl/max2771_spi_engine.sv tb/max2771_spi_engine_tb.sv
|
||||
$(VSIM) -c -work $(WORK) max2771_spi_engine_tb -do "run -all; quit -f"
|
||||
|
||||
clean:
|
||||
rm -rf $(WORK) transcript vsim.wlf
|
||||
@@ -0,0 +1,66 @@
|
||||
# MAX2771 Three-Wire SPI IP
|
||||
|
||||
This directory contains the dedicated MAX2771 serial engine. It is independent
|
||||
of the generic `pmod_spi` controller and implements the MAX2771 transaction
|
||||
format directly.
|
||||
|
||||
Primary references:
|
||||
|
||||
- [MAX2771 data sheet](https://www.analog.com/media/en/technical-documentation/data-sheets/max2771.pdf), Serial Interface and Table 15
|
||||
- [MAX2771 EV kit data sheet and schematic](https://www.analog.com/media/en/technical-documentation/data-sheets/MAX2771EVKIT.pdf)
|
||||
|
||||
## Protocol
|
||||
|
||||
Every command is one 48-SCLK frame while `CSN` is low:
|
||||
|
||||
```text
|
||||
clocks 0..11 A[11:0], MSB first; A[11:4] are zero
|
||||
clock 12 R/W: 1 read, 0 write
|
||||
clocks 13..15 three turnaround clocks
|
||||
clocks 16..47 D[31:0], MSB first
|
||||
```
|
||||
|
||||
`SCLK` idles low. Both sides sample on rising edges and change data on falling
|
||||
edges. `SDATA` is one bidirectional line, not separate MOSI and MISO signals.
|
||||
For reads, the host releases `SDATA` on the falling edge after the R/W bit and
|
||||
keeps it as an input through the three turnaround clocks and 32 data clocks.
|
||||
|
||||
The divider uses ceiling division so SCLK never exceeds the 4 MHz data-sheet
|
||||
limit:
|
||||
|
||||
```text
|
||||
board fabric clock half-period SCLK period
|
||||
Genesys 2 50 MHz 7 cycles 3.571 MHz 280 ns
|
||||
VCU118 100 MHz 13 cycles 3.846 MHz 260 ns
|
||||
```
|
||||
|
||||
Both satisfy the 250 ns minimum period. The engine also enforces at least one
|
||||
half-period for CS setup and hold and at least 100 ns between transactions.
|
||||
|
||||
## MMIO
|
||||
|
||||
The TileLink wrapper is at `0x10016000` on both FPGA targets.
|
||||
|
||||
| Offset | Name | Description |
|
||||
| --- | --- | --- |
|
||||
| `0x00` | COMMAND | bits 3:0 address, bit 8 read; write starts a frame |
|
||||
| `0x04` | WRITE_DATA | 32-bit register write value |
|
||||
| `0x08` | READ_DATA | last 32-bit register read value |
|
||||
| `0x0c` | STATUS | bit 0 ready, bit 1 busy |
|
||||
| `0x10` | INPUT_CLOCK_HZ | configured board fabric clock |
|
||||
| `0x14` | SCLK_HZ | actual divided serial clock |
|
||||
| `0x18` | HALF_PERIOD | fabric cycles per SCLK half-period |
|
||||
| `0x1c` | IDENT | `0x4d415831` (`MAX1`) |
|
||||
|
||||
## Simulation
|
||||
|
||||
The self-checking testbench models the MAX2771 side of the resolved SDATA wire
|
||||
and covers writes, reads, turnaround high impedance, contention, frame order,
|
||||
frame length, CS timing, and both board clock parameters.
|
||||
|
||||
```bash
|
||||
make -C hardware/ip/max2771_spi test
|
||||
```
|
||||
|
||||
This target uses `vlog` and `vsim`. Set `QUESTA=/path/to/questa` when they are
|
||||
not on `PATH`.
|
||||
@@ -0,0 +1,104 @@
|
||||
`timescale 1ns/1ps
|
||||
|
||||
module max2771_spi_engine #(
|
||||
parameter integer CLOCK_HZ = 50_000_000,
|
||||
parameter integer MAX_SCLK_HZ = 4_000_000
|
||||
) (
|
||||
input wire clk,
|
||||
input wire rst_n,
|
||||
input wire start,
|
||||
input wire read_not_write,
|
||||
input wire [3:0] address,
|
||||
input wire [31:0] write_data,
|
||||
output reg [31:0] read_data,
|
||||
output reg busy,
|
||||
output wire ready,
|
||||
output reg sclk,
|
||||
output reg csn,
|
||||
input wire sdata_i,
|
||||
output reg sdata_o,
|
||||
output reg sdata_oe
|
||||
);
|
||||
// Ceiling division keeps SCLK at or below the MAX2771 4 MHz limit.
|
||||
localparam integer HALF_PERIOD_CYCLES =
|
||||
(CLOCK_HZ + (2 * MAX_SCLK_HZ) - 1) / (2 * MAX_SCLK_HZ);
|
||||
localparam integer CS_INACTIVE_CYCLES =
|
||||
(CLOCK_HZ + 10_000_000 - 1) / 10_000_000;
|
||||
|
||||
reg [47:0] tx_shift;
|
||||
reg [31:0] rx_shift;
|
||||
reg [5:0] bit_index;
|
||||
integer half_count;
|
||||
integer inactive_count;
|
||||
reg transaction_is_read;
|
||||
|
||||
assign ready = !busy && (inactive_count == 0);
|
||||
|
||||
initial begin
|
||||
if (CLOCK_HZ <= 0)
|
||||
$error("CLOCK_HZ must be positive");
|
||||
if (MAX_SCLK_HZ <= 0)
|
||||
$error("MAX_SCLK_HZ must be positive");
|
||||
if (HALF_PERIOD_CYCLES < 1)
|
||||
$error("HALF_PERIOD_CYCLES must be at least one");
|
||||
end
|
||||
|
||||
always @(posedge clk) begin
|
||||
if (!rst_n) begin
|
||||
read_data <= 32'd0;
|
||||
busy <= 1'b0;
|
||||
sclk <= 1'b0;
|
||||
csn <= 1'b1;
|
||||
sdata_o <= 1'b0;
|
||||
sdata_oe <= 1'b0;
|
||||
tx_shift <= 48'd0;
|
||||
rx_shift <= 32'd0;
|
||||
bit_index <= 6'd0;
|
||||
half_count <= 0;
|
||||
inactive_count <= 0;
|
||||
transaction_is_read <= 1'b0;
|
||||
end else if (busy) begin
|
||||
if (half_count != 0) begin
|
||||
half_count <= half_count - 1;
|
||||
end else begin
|
||||
half_count <= HALF_PERIOD_CYCLES - 1;
|
||||
if (!sclk) begin
|
||||
// MAX2771 samples input and the host samples read data on rising SCLK.
|
||||
sclk <= 1'b1;
|
||||
if (transaction_is_read && (bit_index >= 6'd16))
|
||||
rx_shift <= {rx_shift[30:0], sdata_i};
|
||||
end else begin
|
||||
// Both sides change or release SDATA on falling SCLK.
|
||||
sclk <= 1'b0;
|
||||
if (bit_index == 6'd47) begin
|
||||
read_data <= rx_shift;
|
||||
busy <= 1'b0;
|
||||
csn <= 1'b1;
|
||||
sdata_oe <= 1'b0;
|
||||
inactive_count <= CS_INACTIVE_CYCLES;
|
||||
end else begin
|
||||
bit_index <= bit_index + 1'b1;
|
||||
tx_shift <= {tx_shift[46:0], 1'b0};
|
||||
sdata_o <= tx_shift[46];
|
||||
if (transaction_is_read && (bit_index == 6'd12))
|
||||
sdata_oe <= 1'b0;
|
||||
end
|
||||
end
|
||||
end
|
||||
end else if (inactive_count != 0) begin
|
||||
inactive_count <= inactive_count - 1;
|
||||
end else if (start) begin
|
||||
// Frame: A[11:0], R/W, three turnaround bits, D[31:0]. A[11:4] are zero.
|
||||
tx_shift <= {8'd0, address, read_not_write, 3'd0, write_data};
|
||||
rx_shift <= 32'd0;
|
||||
bit_index <= 6'd0;
|
||||
transaction_is_read <= read_not_write;
|
||||
busy <= 1'b1;
|
||||
csn <= 1'b0;
|
||||
sclk <= 1'b0;
|
||||
sdata_o <= 1'b0;
|
||||
sdata_oe <= 1'b1;
|
||||
half_count <= HALF_PERIOD_CYCLES - 1;
|
||||
end
|
||||
end
|
||||
endmodule
|
||||
@@ -0,0 +1,251 @@
|
||||
`timescale 1ns/1ps
|
||||
|
||||
module max2771_spi_engine_case #(
|
||||
parameter integer CLOCK_HZ = 50_000_000,
|
||||
parameter integer CLOCK_PERIOD_NS = 20,
|
||||
parameter integer EXPECTED_HALF_CYCLES = 7
|
||||
) (
|
||||
output reg done,
|
||||
output reg failed
|
||||
);
|
||||
localparam [31:0] READ_VALUE = 32'hA596_3CC3;
|
||||
localparam [31:0] WRITE_VALUE = 32'hBEA4_1603;
|
||||
localparam realtime EXPECTED_HALF_NS = CLOCK_PERIOD_NS * EXPECTED_HALF_CYCLES;
|
||||
|
||||
reg clk = 1'b0;
|
||||
reg rst_n = 1'b0;
|
||||
reg start = 1'b0;
|
||||
reg read_not_write = 1'b0;
|
||||
reg [3:0] address = 4'd0;
|
||||
reg [31:0] write_data = 32'd0;
|
||||
wire [31:0] read_data;
|
||||
wire busy;
|
||||
wire ready;
|
||||
wire sclk;
|
||||
wire csn;
|
||||
wire sdata_i;
|
||||
wire sdata_o;
|
||||
wire sdata_oe;
|
||||
tri sdata;
|
||||
reg slave_oe = 1'b0;
|
||||
reg slave_data = 1'b0;
|
||||
|
||||
realtime previous_sclk_edge;
|
||||
realtime csn_assert_time;
|
||||
realtime last_rising_time;
|
||||
realtime previous_csn_release;
|
||||
integer edge_count;
|
||||
|
||||
assign sdata = sdata_oe ? sdata_o : 1'bz;
|
||||
assign sdata = slave_oe ? slave_data : 1'bz;
|
||||
assign sdata_i = sdata;
|
||||
|
||||
max2771_spi_engine #(
|
||||
.CLOCK_HZ(CLOCK_HZ),
|
||||
.MAX_SCLK_HZ(4_000_000)
|
||||
) dut (
|
||||
.clk(clk),
|
||||
.rst_n(rst_n),
|
||||
.start(start),
|
||||
.read_not_write(read_not_write),
|
||||
.address(address),
|
||||
.write_data(write_data),
|
||||
.read_data(read_data),
|
||||
.busy(busy),
|
||||
.ready(ready),
|
||||
.sclk(sclk),
|
||||
.csn(csn),
|
||||
.sdata_i(sdata_i),
|
||||
.sdata_o(sdata_o),
|
||||
.sdata_oe(sdata_oe)
|
||||
);
|
||||
|
||||
always #(CLOCK_PERIOD_NS / 2.0) clk = ~clk;
|
||||
|
||||
task automatic fail(input string message);
|
||||
begin
|
||||
$error("CLOCK_HZ=%0d: %s", CLOCK_HZ, message);
|
||||
failed = 1'b1;
|
||||
end
|
||||
endtask
|
||||
|
||||
task automatic check(input bit condition, input string message);
|
||||
begin
|
||||
if (!condition)
|
||||
fail(message);
|
||||
end
|
||||
endtask
|
||||
|
||||
task automatic launch(
|
||||
input bit is_read,
|
||||
input [3:0] register_address,
|
||||
input [31:0] value
|
||||
);
|
||||
begin
|
||||
wait (ready);
|
||||
@(negedge clk);
|
||||
read_not_write = is_read;
|
||||
address = register_address;
|
||||
write_data = value;
|
||||
start = 1'b1;
|
||||
@(negedge clk);
|
||||
start = 1'b0;
|
||||
wait (!csn);
|
||||
end
|
||||
endtask
|
||||
|
||||
task automatic check_edge_timing(input realtime now_time);
|
||||
realtime delta;
|
||||
begin
|
||||
if (edge_count != 0) begin
|
||||
delta = now_time - previous_sclk_edge;
|
||||
if (delta != EXPECTED_HALF_NS)
|
||||
fail($sformatf("SCLK half-period %0.3f ns, expected %0.3f ns",
|
||||
delta, EXPECTED_HALF_NS));
|
||||
end
|
||||
previous_sclk_edge = now_time;
|
||||
edge_count = edge_count + 1;
|
||||
end
|
||||
endtask
|
||||
|
||||
task automatic run_write;
|
||||
reg [47:0] expected_frame;
|
||||
integer bit_number;
|
||||
begin
|
||||
expected_frame = {8'd0, 4'h0, 1'b0, 3'd0, WRITE_VALUE};
|
||||
edge_count = 0;
|
||||
launch(1'b0, 4'h0, WRITE_VALUE);
|
||||
csn_assert_time = $realtime;
|
||||
if (previous_csn_release != 0.0)
|
||||
check((csn_assert_time - previous_csn_release) >= 100.0,
|
||||
"CSN inactive interval is shorter than 100 ns");
|
||||
|
||||
for (bit_number = 0; bit_number < 48; bit_number = bit_number + 1) begin
|
||||
@(posedge sclk);
|
||||
check_edge_timing($realtime);
|
||||
if (bit_number == 0)
|
||||
check(($realtime - csn_assert_time) >= 10.0,
|
||||
"CSN setup is shorter than 10 ns");
|
||||
check(sdata_oe === 1'b1, "host released SDATA during write frame");
|
||||
check(sdata === expected_frame[47-bit_number],
|
||||
$sformatf("write frame mismatch at bit %0d", bit_number));
|
||||
check(sdata !== 1'bx, "SDATA contention detected during write");
|
||||
last_rising_time = $realtime;
|
||||
@(negedge sclk);
|
||||
check_edge_timing($realtime);
|
||||
end
|
||||
|
||||
#1;
|
||||
check(csn === 1'b1, "CSN did not deassert after 48 clocks");
|
||||
check(sdata_oe === 1'b0, "host did not release SDATA after write");
|
||||
check(($realtime - 1.0 - last_rising_time) >= 10.0,
|
||||
"CSN hold is shorter than 10 ns");
|
||||
previous_csn_release = $realtime - 1.0;
|
||||
end
|
||||
endtask
|
||||
|
||||
task automatic run_read;
|
||||
reg [47:0] expected_header;
|
||||
integer bit_number;
|
||||
begin
|
||||
expected_header = {8'd0, 4'h9, 1'b1, 3'd0, 32'd0};
|
||||
edge_count = 0;
|
||||
launch(1'b1, 4'h9, 32'd0);
|
||||
csn_assert_time = $realtime;
|
||||
check((csn_assert_time - previous_csn_release) >= 100.0,
|
||||
"CSN inactive interval is shorter than 100 ns");
|
||||
|
||||
for (bit_number = 0; bit_number < 48; bit_number = bit_number + 1) begin
|
||||
@(posedge sclk);
|
||||
check_edge_timing($realtime);
|
||||
if (bit_number <= 12) begin
|
||||
check(sdata_oe === 1'b1,
|
||||
$sformatf("host released SDATA in read header at bit %0d", bit_number));
|
||||
check(sdata === expected_header[47-bit_number],
|
||||
$sformatf("read header mismatch at bit %0d", bit_number));
|
||||
end else if (bit_number <= 15) begin
|
||||
check(sdata_oe === 1'b0, "host drove SDATA during turnaround");
|
||||
check(slave_oe === 1'b0, "slave drove SDATA during turnaround");
|
||||
check(sdata === 1'bz, "SDATA was not high impedance during turnaround");
|
||||
end else begin
|
||||
check(sdata_oe === 1'b0, "host drove SDATA during read data");
|
||||
check(slave_oe === 1'b1, "slave did not drive read data");
|
||||
check(sdata === READ_VALUE[47-bit_number],
|
||||
$sformatf("read data mismatch at bit %0d", bit_number));
|
||||
check(sdata !== 1'bx, "SDATA contention detected during read");
|
||||
end
|
||||
last_rising_time = $realtime;
|
||||
|
||||
@(negedge sclk);
|
||||
check_edge_timing($realtime);
|
||||
if ((bit_number >= 15) && (bit_number < 47)) begin
|
||||
slave_oe = 1'b1;
|
||||
slave_data = READ_VALUE[46-bit_number];
|
||||
end
|
||||
end
|
||||
|
||||
slave_oe = 1'b0;
|
||||
#1;
|
||||
check(csn === 1'b1, "CSN did not deassert after read frame");
|
||||
check(read_data === READ_VALUE,
|
||||
$sformatf("read result 0x%08x, expected 0x%08x", read_data, READ_VALUE));
|
||||
check(sdata_oe === 1'b0, "host did not keep SDATA as input after read");
|
||||
previous_csn_release = $realtime - 1.0;
|
||||
end
|
||||
endtask
|
||||
|
||||
initial begin
|
||||
done = 1'b0;
|
||||
failed = 1'b0;
|
||||
previous_csn_release = 0.0;
|
||||
repeat (5) @(posedge clk);
|
||||
rst_n = 1'b1;
|
||||
repeat (2) @(posedge clk);
|
||||
check(ready === 1'b1, "engine was not ready after reset");
|
||||
check(csn === 1'b1, "CSN was not idle high after reset");
|
||||
check(sclk === 1'b0, "SCLK was not idle low after reset");
|
||||
check(sdata_oe === 1'b0, "SDATA was not an input after reset");
|
||||
|
||||
run_write();
|
||||
run_read();
|
||||
done = 1'b1;
|
||||
end
|
||||
endmodule
|
||||
|
||||
module max2771_spi_engine_tb;
|
||||
wire done_50mhz;
|
||||
wire failed_50mhz;
|
||||
wire done_100mhz;
|
||||
wire failed_100mhz;
|
||||
|
||||
max2771_spi_engine_case #(
|
||||
.CLOCK_HZ(50_000_000),
|
||||
.CLOCK_PERIOD_NS(20),
|
||||
.EXPECTED_HALF_CYCLES(7)
|
||||
) case_50mhz (
|
||||
.done(done_50mhz),
|
||||
.failed(failed_50mhz)
|
||||
);
|
||||
|
||||
max2771_spi_engine_case #(
|
||||
.CLOCK_HZ(100_000_000),
|
||||
.CLOCK_PERIOD_NS(10),
|
||||
.EXPECTED_HALF_CYCLES(13)
|
||||
) case_100mhz (
|
||||
.done(done_100mhz),
|
||||
.failed(failed_100mhz)
|
||||
);
|
||||
|
||||
initial begin
|
||||
wait (done_50mhz && done_100mhz);
|
||||
if (failed_50mhz || failed_100mhz)
|
||||
$fatal(1, "MAX2771 SPI engine test FAILED");
|
||||
$display("MAX2771 SPI engine test PASSED: 50 MHz -> 3.571 MHz, 100 MHz -> 3.846 MHz");
|
||||
$finish;
|
||||
end
|
||||
|
||||
initial begin
|
||||
#100_000;
|
||||
$fatal(1, "MAX2771 SPI engine test timed out");
|
||||
end
|
||||
endmodule
|
||||
Reference in New Issue
Block a user