max2771 spi liens has been added.

This commit is contained in:
onur
2026-08-06 22:14:52 +03:00
parent 847523078b
commit b4920d31e3
288 changed files with 214687 additions and 18389 deletions
+13
View File
@@ -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
+66
View File
@@ -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