Skip to content

Commit c4d8222

Browse files
make SDA aribtration logic its own module (#311)
This could be parameterized further to be a more general way to arbitrate a shared bus where the FPGA is a proxy, but for now this is what it is.
1 parent 730635e commit c4d8222

File tree

4 files changed

+280
-1
lines changed

4 files changed

+280
-1
lines changed

hdl/projects/cosmo_seq/spd_proxy/BUCK

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,33 @@
11
load("//tools:hdl.bzl", "vhdl_unit", "vunit_sim")
22

3+
vhdl_unit(
4+
name = "sda_arbiter",
5+
srcs = glob(["sda_arbiter/*.vhd"]),
6+
deps = [],
7+
visibility = ['PUBLIC'],
8+
)
9+
310
vhdl_unit(
411
name = "spd_proxy_top",
512
srcs = glob(["*.vhd"]),
613
deps = [
14+
":sda_arbiter",
715
"//hdl/ip/vhd/common:tristate_if_pkg",
816
"//hdl/ip/vhd/i2c/common:i2c_common_pkg",
917
"//hdl/ip/vhd/i2c/common:i2c_glitch_filter",
1018
"//hdl/ip/vhd/i2c/controller:i2c_ctrl_txn_layer",
1119
],
1220
standard = "2019",
13-
visibility = ['PUBLIC']
21+
visibility = ['PUBLIC'],
22+
)
23+
24+
vunit_sim(
25+
name = "sda_arbiter_tb",
26+
srcs = glob(["sda_arbiter/sims/*.vhd"]),
27+
deps = [
28+
":sda_arbiter",
29+
],
30+
visibility = ['PUBLIC'],
1431
)
1532

1633
vunit_sim(
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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+
-- This block monitors two I2C SDA signals, `a` and `b`, with the intention thatt hey are
8+
-- essentially connected but proxied by the FPGA. It assumes the inactive state for the bus is high,
9+
-- and then monitors for `a` or `b` to pull the line low. It will then grant whichever side pulled
10+
-- the line low the bus until that side releases it, at which point after HYSTERESIS_CYCLES both
11+
-- sides will be resampled again.
12+
13+
library ieee;
14+
use ieee.std_logic_1164.all;
15+
use ieee.numeric_std.all;
16+
17+
entity sda_arbiter is
18+
generic (
19+
HYSTERESIS_CYCLES : natural
20+
);
21+
port (
22+
clk : in std_logic;
23+
reset : in std_logic;
24+
25+
a : in std_logic;
26+
b : in std_logic;
27+
enabled : in std_logic;
28+
a_grant : out std_logic;
29+
b_grant : out std_logic
30+
);
31+
end entity;
32+
33+
architecture rtl of sda_arbiter is
34+
type sda_control_t is (
35+
NONE,
36+
GRANT_A,
37+
GRANT_B
38+
);
39+
signal sda_state : sda_control_t;
40+
signal hysteresis_cntr : natural range 0 to HYSTERESIS_CYCLES;
41+
begin
42+
43+
a_grant <= '1' when sda_state = GRANT_A else '0';
44+
b_grant <= '1' when sda_state = GRANT_B else '0';
45+
46+
arbitration : process(clk, reset)
47+
variable sda_next : sda_control_t;
48+
begin
49+
if reset then
50+
sda_state <= NONE;
51+
hysteresis_cntr <= 0;
52+
elsif rising_edge(clk) then
53+
if enabled then
54+
if hysteresis_cntr = HYSTERESIS_CYCLES then
55+
case sda_state is
56+
when NONE =>
57+
if a = '0' and b = '1' then
58+
sda_next := GRANT_A;
59+
elsif a = '1' and b = '0' then
60+
sda_next := GRANT_B;
61+
end if;
62+
63+
when GRANT_A =>
64+
if a = '1' then
65+
sda_next := NONE;
66+
end if;
67+
68+
when GRANT_B =>
69+
if b = '1' then
70+
sda_next := NONE;
71+
end if;
72+
end case;
73+
74+
-- arbitration changed, enforce hysteresis
75+
if sda_next /= sda_state then
76+
hysteresis_cntr <= 0;
77+
end if;
78+
else
79+
hysteresis_cntr <= hysteresis_cntr + 1;
80+
end if;
81+
else
82+
sda_next := NONE;
83+
hysteresis_cntr <= 0;
84+
end if;
85+
86+
sda_state <= sda_next;
87+
end if;
88+
end process;
89+
90+
end architecture;
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
[*]
2+
[*] GTKWave Analyzer v3.3.104 (w)1999-2020 BSI
3+
[*] Mon Mar 17 12:06:15 2025
4+
[*]
5+
[dumpfile] "/home/aaron/Oxide/git/quartz/vunit_out/test_output/lib.sda_arbiter_tb.hysteresis_fba39672ff868058b296cfd50da9181a1eabe178/nvc/sda_arbiter_tb.fst"
6+
[dumpfile_mtime] "Mon Mar 17 12:06:00 2025"
7+
[dumpfile_size] 767
8+
[savefile] "/home/aaron/Oxide/git/quartz/hdl/projects/cosmo_seq/spd_proxy/sda_arbiter/sims/sda_arbiter_tb.gtkw"
9+
[timestart] 0
10+
[size] 2528 1283
11+
[pos] 1138 -22
12+
*-25.222696 100000000 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1
13+
[treeopen] sda_arbiter_tb.
14+
[sst_width] 221
15+
[signals_width] 313
16+
[sst_expanded] 1
17+
[sst_vpaned_height] 382
18+
@28
19+
sda_arbiter_tb.sda_arbiter_inst.clk
20+
sda_arbiter_tb.sda_arbiter_inst.reset
21+
sda_arbiter_tb.sda_arbiter_inst.enabled
22+
sda_arbiter_tb.sda_arbiter_inst.a
23+
sda_arbiter_tb.sda_arbiter_inst.b
24+
sda_arbiter_tb.sda_arbiter_inst.sda_state
25+
@25
26+
sda_arbiter_tb.sda_arbiter_inst.hysteresis_cntr
27+
@28
28+
sda_arbiter_tb.sda_arbiter_inst.a_grant
29+
sda_arbiter_tb.sda_arbiter_inst.b_grant
30+
[pattern_trace] 1
31+
[pattern_trace] 0
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
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_unsigned.all;
10+
11+
library vunit_lib;
12+
context vunit_lib.com_context;
13+
context vunit_lib.vunit_context;
14+
15+
entity sda_arbiter_tb is
16+
generic (
17+
runner_cfg : string
18+
);
19+
end entity;
20+
21+
architecture tb of sda_arbiter_tb is
22+
constant CLK_PER_TIME : time := 8 ns;
23+
constant HYSTERESIS_CYCLES : integer := 10;
24+
25+
signal clk : std_logic := '0';
26+
signal reset : std_logic := '1';
27+
signal a : std_logic := '1';
28+
signal b : std_logic := '1';
29+
signal enabled : std_logic := '1';
30+
signal a_grant : std_logic;
31+
signal b_grant : std_logic;
32+
begin
33+
34+
clk <= not clk after CLK_PER_TIME / 2;
35+
reset <= '0' after 100 ns;
36+
37+
sda_arbiter_inst: entity work.sda_arbiter
38+
generic map(
39+
HYSTERESIS_CYCLES => HYSTERESIS_CYCLES
40+
)
41+
port map(
42+
clk => clk,
43+
reset => reset,
44+
a => a,
45+
b => b,
46+
enabled => enabled,
47+
a_grant => a_grant,
48+
b_grant => b_grant
49+
);
50+
51+
bench: process
52+
variable cycle_counter : integer := 0;
53+
begin
54+
-- Always the first thing in the process, set up things for the VUnit test runner
55+
test_runner_setup(runner, runner_cfg);
56+
57+
-- wait for reset to clear
58+
wait until reset = '0';
59+
60+
while test_suite loop
61+
if run("grant_a") then
62+
-- assert A bus
63+
b <= '1';
64+
wait for 100 ns;
65+
a <= '0';
66+
67+
check_true(a_grant = '0', "Bus A should not be granted the bus.");
68+
check_true(b_grant = '0', "Bus B should not be granted the bus.");
69+
70+
-- arbitration grant is registered
71+
wait for CLK_PER_TIME + 1 ns;
72+
73+
check_true(a_grant = '1', "Bus A should immediately be granted the bus.");
74+
check_true(b_grant = '0', "Bus B should not be granted the bus.");
75+
elsif run("grant_b") then
76+
-- assert B bus
77+
a <= '1';
78+
wait for 100 ns;
79+
b <= '0';
80+
81+
check_true(a_grant = '0', "Bus A should not be granted the bus.");
82+
check_true(b_grant = '0', "Bus B should not be granted the bus.");
83+
84+
-- arbitration grant is registered
85+
wait for CLK_PER_TIME + 1 ns;
86+
87+
check_true(a_grant = '0', "Bus A should not be granted the bus.");
88+
check_true(b_grant = '1', "Bus B should immediately be granted the bus.");
89+
elsif run("hysteresis") then
90+
-- assert A bus
91+
b <= '1';
92+
wait for 100 ns;
93+
a <= '0';
94+
wait until a_grant = '1';
95+
96+
-- release A bus and assert B bus, expecting the arbitration not to change before
97+
-- HYSTERESIS_CYCLES has passed
98+
a <= '1';
99+
b <= '0';
100+
101+
while cycle_counter <= HYSTERESIS_CYCLES loop
102+
wait for CLK_PER_TIME;
103+
check_true(a_grant = '1', "Bus A should be granted the bus.");
104+
check_true(b_grant = '0', "Bus B should not be granted the bus.");
105+
cycle_counter := cycle_counter + 1;
106+
end loop;
107+
108+
-- after the initial hysteresis period, neither bus should be granted arbitration
109+
cycle_counter := 0;
110+
while cycle_counter <= HYSTERESIS_CYCLES loop
111+
wait for CLK_PER_TIME;
112+
check_true(a_grant = '0', "Bus A should not be granted the bus.");
113+
check_true(b_grant = '0', "Bus B should not be granted the bus.");
114+
cycle_counter := cycle_counter + 1;
115+
end loop;
116+
117+
-- after the second hysteresis period, B should be granted the bus.
118+
wait until rising_edge(clk);
119+
check_true(a_grant = '0', "Bus A should not be granted the bus.");
120+
check_true(b_grant = '1', "Bus B should be granted the bus.");
121+
elsif run("disable") then
122+
-- assert A bus
123+
b <= '1';
124+
wait for 100 ns;
125+
a <= '0';
126+
wait until a_grant = '1';
127+
128+
enabled <= '0';
129+
wait for CLK_PER_TIME + 1 ns;
130+
check_true(a_grant = '0', "Bus A should not be granted the bus.");
131+
check_true(b_grant = '0', "Bus B should not be granted the bus.");
132+
end if;
133+
end loop;
134+
135+
wait for 1 us;
136+
test_runner_cleanup(runner);
137+
wait;
138+
end process;
139+
140+
test_runner_watchdog(runner, 10 us);
141+
end architecture;

0 commit comments

Comments
 (0)