Skip to content

Commit f85b0d0

Browse files
SPI<->AXI interface
1 parent 064dba6 commit f85b0d0

File tree

10 files changed

+1283
-0
lines changed

10 files changed

+1283
-0
lines changed

hdl/ip/vhd/spi/axi_controller/BUCK

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
load("//tools:hdl.bzl", "vhdl_unit", "vunit_sim")
2+
load("//tools:rdl.bzl", "rdl_file")
3+
4+
5+
vhdl_unit(
6+
name = "spi_axi_controller",
7+
srcs = glob(["*.vhd",]),
8+
deps = [
9+
"//hdl/ip/vhd/spi/spi_target_phy:spi_target_phy",
10+
],
11+
visibility = ['PUBLIC']
12+
)
13+
14+
vunit_sim(
15+
name = "spi_axi_tb",
16+
srcs = glob(["sims/**/*.vhd"]),
17+
deps = [
18+
":spi_axi_controller",
19+
"//hdl/ip/vhd/vunit_components:spi_vcs",
20+
"//hdl/ip/vhd/i2c/target:i2c_phy_consolidator",
21+
],
22+
visibility = ['PUBLIC'],
23+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
It supports Mode 0 (0,0) and mode3 (1,1) transfers. It is expected that there are fewer than 256 addressable registers, but we’re picking 16bit addressing for future extensibility without changing the protocol and to support a circular buffer for storing event history.
2+
3+
SPI instructions
4+
[cols=4,options="header"]
5+
|===
6+
|Opcode| Instruction Name | Description| Notes
7+
| 0x0 | Write byte(s) | Write one or more contiguous bytes |
8+
| 0x1| Read byte(s) | Read one or more contiguous bytes|
9+
| 0x2| Bit set | hardware does a bit-wise OR with data and current register state | new_reg = old_reg \|\| data
10+
| 0x3| Bit clr | hardware does a bit-wise clear with data and current register state |new_reg = old_reg && !data
11+
|===
12+
13+
Note that for the bit-set and bit-clear spi instructions, the AXI controller internally does a read-modify-write operation, resulting
14+
in a read of the current register state, a bitwise operation with the data, and a write back of the new register state. These are
15+
not good operations to use if the registers you're modifiying have read-side effects.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
-- This Source Code Form is subject to the terms of the Mozilla Public
2+
-- License, v. 2.0. If a copy of the MPL was not distributed with this
3+
-- file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
--
5+
-- Copyright 2024 Oxide Computer Company
6+
7+
library ieee;
8+
use ieee.std_logic_1164.all;
9+
use ieee.numeric_std.all;
10+
use ieee.numeric_std_unsigned.all;
11+
12+
library vunit_lib;
13+
context vunit_lib.com_context;
14+
context vunit_lib.vunit_context;
15+
context vunit_lib.vc_context;
16+
17+
use vunit_lib.spi_pkg.all;
18+
19+
use work.spi_axi_tb_pkg.all;
20+
use work.spi_axi_pkg.all;
21+
22+
23+
entity spi_axi_tb is
24+
generic (
25+
26+
runner_cfg : string
27+
);
28+
end entity;
29+
30+
architecture tb of spi_axi_tb is
31+
32+
begin
33+
34+
th: entity work.spi_axi_th;
35+
36+
bench: process
37+
alias reset is << signal th.reset : std_logic >>;
38+
alias csn is << signal th.csn : std_logic >>;
39+
variable buf : buffer_t;
40+
variable buf2 : buffer_t;
41+
variable data : std_logic_vector(7 downto 0) := (others => '0');
42+
variable expected_data : std_logic_vector(7 downto 0) := (others => '0');
43+
variable address : std_logic_vector(15 downto 0) := (others => '0');
44+
variable tx_queue : queue_t := new_queue;
45+
begin
46+
-- Always the first thing in the process, set up things for the VUnit test runner
47+
test_runner_setup(runner, runner_cfg);
48+
show_all(wr_logger, display_handler);
49+
-- Reach into the test harness, which generates and de-asserts reset and hold the
50+
-- test cases off until we're out of reset. This runs for every test case
51+
wait until reset = '0';
52+
wait for 500 ns; -- let the resets propagate
53+
54+
while test_suite loop
55+
if run("single-read") then
56+
-- set up the "memory to be read with a known value"
57+
buf := allocate(rmemory, 1 * 64, alignment => 32);
58+
-- Use the simulation interface to set the data we're going to read back
59+
expected_data := X"AA";
60+
write_word(rmemory, 0, expected_data);
61+
-- TB will fault if DUT tries to write to this memory
62+
set_permissions(rmemory, 0, read_only);
63+
-- issue spi read command (data is dummy byte here since this is a)
64+
spi_send_byte(net, spi_opcode_read, address, (others => '0'), csn);
65+
-- Read back word rx'd from DUT and check it matches expected.
66+
-- -- we're going to have 3 dummy bytes here, skip them, keeping the 4th
67+
for i in 0 to 3 loop
68+
pop_stream(net, master_rstream, data);
69+
end loop;
70+
check_equal(data, expected_data, "Read data did not match expected");
71+
elsif run("single-write") then
72+
-- set up the "memory to be read with a known value"
73+
buf := allocate(wmemory, 1 * 64, alignment => 32);
74+
-- Use the simulation interface to set the data we're expected to write
75+
expected_data := X"AA";
76+
set_expected_word(wmemory, 0, expected_data);
77+
-- issue spi write command
78+
spi_send_byte(net, spi_opcode_write, address, expected_data, csn);
79+
-- no reach into the ram and see what is there now
80+
wait for 20 ns;
81+
check_expected_was_written(buf);
82+
elsif run("single-bit-set") then
83+
-- Due to how the axi blocks by vunit work, we need to set up 2 buffers, one for the
84+
-- read side and one for the write-side
85+
-- READ SIDE is buf
86+
-- set up the "memory to be read with a known value"
87+
buf := allocate(rmemory, 1 * 64, alignment => 32);
88+
-- WRITE SIDE is buf2
89+
buf2 := allocate(wmemory, 1 * 64, alignment => 32);
90+
-- Sick X"AA" into the read buffer, this is going to be our starting point for the bit-set
91+
-- Use the simulation interface to set the data we're going to read back
92+
expected_data := X"AA";
93+
write_word(rmemory, 0, expected_data);
94+
-- Our Write address now has X"AA" in it.
95+
data := X"05";
96+
-- This is the expected write into the write-side after the read-modify-write bit-set operation
97+
expected_data := expected_data or data;
98+
set_expected_word(wmemory, 0, expected_data);
99+
100+
-- issue spi write command for bit set with data bits
101+
spi_send_byte(net, spi_opcode_bit_set, address, data, csn);
102+
103+
-- no reach into the ram and see what is there now
104+
wait for 1 us;
105+
check_expected_was_written(buf2);
106+
elsif run("single-bit-clr") then
107+
-- Due to how the axi blocks by vunit work, we need to set up 2 buffers, one for the
108+
-- read side and one for the write-side
109+
-- READ SIDE is buf
110+
-- set up the "memory to be read with a known value"
111+
buf := allocate(rmemory, 1 * 64, alignment => 32);
112+
-- WRITE SIDE is buf2
113+
buf2 := allocate(wmemory, 1 * 64, alignment => 32);
114+
-- Sick X"AA" into the read buffer, this is going to be our starting point for the bit-set
115+
116+
-- Use the simulation interface to set the data we're going to read back
117+
expected_data := X"AA";
118+
write_word(rmemory, 0, expected_data);
119+
-- Our Write address now has X"AA" in it.
120+
data := X"0A";
121+
-- This is the expected write into the write-side after the read-modify-write bit-set operation
122+
expected_data := expected_data and (not data);
123+
set_expected_word(wmemory, 0, expected_data);
124+
125+
-- issue spi write command for bit set with data bits
126+
spi_send_byte(net, spi_opcode_bit_clr, address, data, csn);
127+
-- no reach into the ram and see what is there now
128+
wait for 20 ns;
129+
check_expected_was_written(buf2);
130+
elsif run("multi-read") then
131+
-- set up the "memory to be read with a known value"
132+
buf := allocate(rmemory, 4 * 32, alignment => 32);
133+
-- Use the simulation interface to set the data we're going to read back
134+
-- Load up 3 known bytes into the memory
135+
expected_data := X"AA";
136+
write_word(rmemory, 0, X"ACABAA");
137+
-- TB will fault if DUT tries to write to this memory
138+
set_permissions(rmemory, 0, read_only);
139+
-- issue spi read command for multiple bytes. No early abort
140+
for i in 0 to 2 loop
141+
push_byte(tx_queue, 0); -- push dummy data for the read
142+
end loop;
143+
spi_send_stream(net, spi_opcode_read, address, tx_queue, csn);
144+
-- Read back word rx'd from DUT and check it matches expected.
145+
-- we're going to have 3 dummy bytes here, skip them, keeping the 4th
146+
expected_data := X"AA";
147+
for i in 0 to 4 loop
148+
pop_stream(net, master_rstream, data);
149+
-- only check past the dummy data
150+
if i > 2 then
151+
check_equal(data, expected_data, "Read data did not match expected, iteration " & to_string(i-2));
152+
expected_data := expected_data + 1;
153+
end if;
154+
end loop;
155+
elsif run("multi-write") then
156+
-- set up the "memory to be read with a known value"
157+
buf := allocate(wmemory, 1 * 64, alignment => 32);
158+
-- Use the simulation interface to set the data we're expected to write
159+
set_expected_word(wmemory, 0, X"ACABAA");
160+
-- issue spi write command
161+
expected_data := X"AA";
162+
for i in 0 to 2 loop
163+
push_byte(tx_queue, to_integer(expected_data)); -- push dummy data for the read
164+
expected_data := expected_data + 1;
165+
end loop;
166+
spi_send_stream(net, spi_opcode_write, address, tx_queue, csn);
167+
-- no reach into the ram and see what is there now
168+
wait for 20 ns;
169+
check_expected_was_written(buf);
170+
171+
elsif run("ok-after-invalid-opcode") then
172+
-- set up the "memory to be read with a known value"
173+
buf := allocate(rmemory, 1 * 64, alignment => 32);
174+
-- Use the simulation interface to set the data we're going to read back
175+
expected_data := X"AA";
176+
write_word(rmemory, 0, expected_data);
177+
-- TB will fault if DUT tries to write to this memory
178+
set_permissions(rmemory, 0, read_only);
179+
-- issue invalid opcode.
180+
spi_send_byte(net, "1001", address, (others => '0'), csn);
181+
-- Read back word rx'd from DUT and check it matches expected.
182+
-- we're going to have 3 dummy bytes here, skip them, keeping the 4th
183+
for i in 0 to 3 loop
184+
pop_stream(net, master_rstream, data);
185+
end loop;
186+
wait for 20 ns;-- same thing again with a read this time
187+
-- issue spi read command (data is dummy byte here since this is a)
188+
spi_send_byte(net, spi_opcode_read, address, (others => '0'), csn);
189+
-- Read back word rx'd from DUT and check it matches expected.
190+
-- we're going to have 3 dummy bytes here, skip them, keeping the 4th
191+
for i in 0 to 3 loop
192+
pop_stream(net, master_rstream, data);
193+
end loop;
194+
check_equal(data, expected_data, "Read data did not match expected");
195+
end if;
196+
end loop;
197+
198+
wait for 2 us;
199+
test_runner_cleanup(runner);
200+
wait;
201+
end process;
202+
203+
-- Example total test timeout dog
204+
test_runner_watchdog(runner, 10 ms);
205+
206+
end tb;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
-- This Source Code Form is subject to the terms of the Mozilla Public
2+
-- License, v. 2.0. If a copy of the MPL was not distributed with this
3+
-- file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
--
5+
-- Copyright 2025 Oxide Computer Company
6+
7+
library ieee;
8+
use ieee.std_logic_1164.all;
9+
use ieee.numeric_std.all;
10+
use ieee.numeric_std_unsigned.all;
11+
12+
library vunit_lib;
13+
context vunit_lib.vunit_context;
14+
context vunit_lib.com_context;
15+
context vunit_lib.vc_context;
16+
17+
use vunit_lib.spi_pkg.all;
18+
19+
package spi_axi_tb_pkg is
20+
21+
constant rd_logger : logger_t := get_logger("axi_rd");
22+
constant rmemory : memory_t := new_memory;
23+
constant axi_read_target : axi_slave_t := new_axi_slave(address_fifo_depth => 1,
24+
memory => rmemory,
25+
logger => rd_logger);
26+
27+
constant wr_logger : logger_t := get_logger("axi_wr");
28+
constant wmemory : memory_t := new_memory;
29+
constant axi_write_target : axi_slave_t := new_axi_slave(address_fifo_depth => 1,
30+
memory => wmemory,
31+
logger => wr_logger);
32+
33+
34+
constant master_spi : spi_master_t := new_spi_master;
35+
constant master_wstream : stream_master_t := as_stream(master_spi);
36+
constant master_rstream : stream_slave_t := as_stream(master_spi);
37+
38+
procedure spi_send_byte(
39+
signal net: inout network_t;
40+
constant opcode: in std_logic_vector(3 downto 0);
41+
constant addr: in std_logic_vector(15 downto 0);
42+
constant byte: in std_logic_vector(7 downto 0);
43+
signal csn : out std_logic
44+
);
45+
46+
procedure spi_send_stream(
47+
signal net: inout network_t;
48+
constant opcode: in std_logic_vector(3 downto 0);
49+
constant addr: in std_logic_vector(15 downto 0);
50+
constant payload_queue: in queue_t;
51+
signal csn : out std_logic;
52+
constant cs_abort_after : in natural := 0
53+
);
54+
55+
end package;
56+
57+
package body spi_axi_tb_pkg is
58+
procedure spi_send_byte(
59+
signal net: inout network_t;
60+
constant opcode: in std_logic_vector(3 downto 0);
61+
constant addr: in std_logic_vector(15 downto 0);
62+
constant byte: in std_logic_vector(7 downto 0);
63+
signal csn : out std_logic
64+
) is
65+
alias addr_h : std_logic_vector(7 downto 0) is addr(15 downto 8);
66+
alias addr_l : std_logic_vector(7 downto 0) is addr(7 downto 0);
67+
begin
68+
csn <= '0';
69+
-- read opcode
70+
push_stream(net, master_wstream, resize(opcode, 8));
71+
-- addr h
72+
push_stream(net, master_wstream, addr_h);
73+
-- addr l
74+
push_stream(net, master_wstream, addr_l);
75+
-- data
76+
push_stream(net, master_wstream, byte);
77+
wait_until_idle(net, as_sync(master_spi));
78+
csn <= '1';
79+
end procedure;
80+
81+
procedure spi_send_stream(
82+
signal net: inout network_t;
83+
constant opcode: in std_logic_vector(3 downto 0);
84+
constant addr: in std_logic_vector(15 downto 0);
85+
constant payload_queue: in queue_t;
86+
signal csn : out std_logic;
87+
constant cs_abort_after : in natural := 0
88+
) is
89+
alias addr_h : std_logic_vector(7 downto 0) is addr(15 downto 8);
90+
alias addr_l : std_logic_vector(7 downto 0) is addr(7 downto 0);
91+
variable byte_count : natural := 0;
92+
begin
93+
csn <= '0';
94+
-- read opcode
95+
push_stream(net, master_wstream, resize(opcode, 8));
96+
-- addr h
97+
push_stream(net, master_wstream, addr_h);
98+
-- addr l
99+
push_stream(net, master_wstream, addr_l);
100+
-- data
101+
while not is_empty(payload_queue) loop
102+
-- allow for early abort if requested
103+
if cs_abort_after > 0 and byte_count = cs_abort_after then
104+
flush(payload_queue);
105+
csn <= '1';
106+
exit;
107+
end if;
108+
-- otherwise send the next byte and count it
109+
push_stream(net, master_wstream, to_std_logic_vector(pop_byte(payload_queue), 8));
110+
byte_count := byte_count + 1;
111+
end loop;
112+
wait_until_idle(net, as_sync(master_spi));
113+
csn <= '1';
114+
end procedure;
115+
116+
end package body;

0 commit comments

Comments
 (0)