diff --git a/.github/filters.yml b/.github/filters.yml index c3548d3e..36beec85 100644 --- a/.github/filters.yml +++ b/.github/filters.yml @@ -30,5 +30,11 @@ cosmo_hp: cosmo_seq: - 'hdl/projects/cosmo_seq/**/*' +# metro_seq depends on cosmo_seq's shared subsystems, so a change there has to +# rebuild metro too. +metro_seq: + - 'hdl/projects/metro_seq/**/*' + - 'hdl/projects/cosmo_seq/**/*' + cosmo_ignition: - 'hdl/projects/cosmo_ignition/**/*' \ No newline at end of file diff --git a/.gitignore b/.gitignore index 840b033d..ae418798 100644 --- a/.gitignore +++ b/.gitignore @@ -51,4 +51,4 @@ vivado*.str **/*.svg.bkp # cobble local env -BUILD.vars \ No newline at end of file +BUILD.vars diff --git a/cos_soak_100.json b/cos_soak_100.json new file mode 100644 index 00000000..6d91eb9b --- /dev/null +++ b/cos_soak_100.json @@ -0,0 +1,53 @@ +{ + "seed": 478, + "steps": [ + { + "divisor": 2, + "freq_mhz": 100.0, + "ok": true, + "udp_timeouts_retried": 9, + "phases": { + "A_const_reads": { + "ok": true, + "values": [ + "0x000001de" + ] + }, + "B_scratch": { + "ok": true, + "patterns": 68, + "mismatches": [] + }, + "C_stress": { + "ok": true, + "write_burst": 1024, + "read_words": 1024, + "mismatches": [] + }, + "D_soak": { + "ok": true, + "ops": 14969449, + "mismatches": [] + }, + "E_throughput": { + "ok": true, + "wire_write_MBps": 1.228, + "wire_read_MBps": 1.003, + "single_read_us": 226.7, + "fmc_read_MBps": 11.41, + "fmc_read_ns_per_word": 351, + "fmc_read_fixed_ns_per_word": 309, + "fmc_write_MBps": 50.17, + "fmc_write_ns_per_word": 80, + "note": "fmc_* numbers are bus line rate (delta-timed block ops); wire_* include per-word network cost and are insensitive to the FMC clock" + } + }, + "btr1": "0x0010ffff", + "clkdiv_verified": true, + "sp_ticks_before": 63172, + "sp_ticks_after": 3664254, + "sp_rebooted": false + } + ], + "restored_baseline_ok": true +} \ No newline at end of file diff --git a/cos_soak_100_26p25.json b/cos_soak_100_26p25.json new file mode 100644 index 00000000..648d5c11 --- /dev/null +++ b/cos_soak_100_26p25.json @@ -0,0 +1,53 @@ +{ + "seed": 478, + "steps": [ + { + "divisor": 2, + "freq_mhz": 100.0, + "ok": true, + "udp_timeouts_retried": 9, + "phases": { + "A_const_reads": { + "ok": true, + "values": [ + "0x000001de" + ] + }, + "B_scratch": { + "ok": true, + "patterns": 68, + "mismatches": [] + }, + "C_stress": { + "ok": true, + "write_burst": 1024, + "read_words": 1024, + "mismatches": [] + }, + "D_soak": { + "ok": true, + "ops": 14923578, + "mismatches": [] + }, + "E_throughput": { + "ok": true, + "wire_write_MBps": 1.224, + "wire_read_MBps": 0.997, + "single_read_us": 226.8, + "fmc_read_MBps": 11.39, + "fmc_read_ns_per_word": 351, + "fmc_read_fixed_ns_per_word": 309, + "fmc_write_MBps": 49.98, + "fmc_write_ns_per_word": 80, + "note": "fmc_* numbers are bus line rate (delta-timed block ops); wire_* include per-word network cost and are insensitive to the FMC clock" + } + }, + "btr1": "0x0010ffff", + "clkdiv_verified": true, + "sp_ticks_before": 48439, + "sp_ticks_after": 3649492, + "sp_rebooted": false + } + ], + "restored_baseline_ok": true +} \ No newline at end of file diff --git a/hdl/ip/vhd/espi/espi_target_top.vhd b/hdl/ip/vhd/espi/espi_target_top.vhd index e5d47c67..16710cf0 100644 --- a/hdl/ip/vhd/espi/espi_target_top.vhd +++ b/hdl/ip/vhd/espi/espi_target_top.vhd @@ -20,6 +20,16 @@ use work.link_layer_pkg.all; use work.axil15x32_pkg.all; entity espi_target_top is + generic ( + -- Whether this instance can ever forward SAFS flash writes and + -- erases. False strips the ability out of the build; true still + -- leaves it gated by the flash_write_enable control bit, which + -- resets to off. Refused writes and erases get an unsuccessful + -- completion. + FLASH_WRITES_ALLOWED : boolean := false; + -- Whether to keep the 4k entry post code buffer, see espi_regs. + POST_CODE_BUFFER_ENABLED : boolean := true + ); port ( clk : in std_logic; reset : in std_logic; @@ -43,6 +53,10 @@ entity espi_target_top is flash_rfifo_rdack : out std_logic; flash_rfifo_rempty: in std_logic; flash_fifo_clear : out std_logic; + -- Write payload FIFO, host to flash. Only ever written on an + -- instance built with FLASH_WRITES_ALLOWED; leave open otherwise. + flash_wfifo_data : out std_logic_vector(7 downto 0); + flash_wfifo_write : out std_logic; -- Interfaces to the UART block to_sp_uart_data : out std_logic_vector(7 downto 0); to_sp_uart_valid: out std_logic; @@ -66,6 +80,8 @@ architecture rtl of espi_target_top is signal flash_np_free : std_logic; signal flash_c_avail : std_logic; signal flash_channel_enable : boolean; + signal flash_write_enable : std_logic; + signal flash_writes_permitted : std_logic; signal dbg_chan : dbg_chan_t; signal spec_regs : spec_regs_t; signal response_done : boolean; @@ -251,6 +267,9 @@ begin chip_sel_active <= not txn_csn; -- system (axi-lite) register block espi_sys_regs_inst: entity work.espi_regs + generic map( + POST_CODE_BUFFER_ENABLED => POST_CODE_BUFFER_ENABLED + ) port map( clk => clk, reset => reset, @@ -258,6 +277,7 @@ begin stuff_fifo => stuff_fifo, stuff_wds => stuff_wds, dbg_chan => dbg_chan, + flash_write_enable => flash_write_enable, spec_regs_view => spec_regs, post_code => post_code, post_code_valid => post_code_valid, @@ -286,6 +306,7 @@ begin data_to_host => txn_resp, data_from_host => txn_cmd, alert_needed => alert_needed, + flash_writes_allowed => flash_writes_permitted, flash_req => flash_req, flash_resp => flash_resp, response_done => response_done, @@ -320,6 +341,11 @@ begin oob_enabled => oob_enabled ); + -- Both halves of the write permission have to agree; the generic keeps + -- the register bit from doing anything on an instance that must never + -- let the host write. + flash_writes_permitted <= '1' when FLASH_WRITES_ALLOWED and flash_write_enable = '1' else '0'; + -- flash access channel logic flash_channel_inst: entity work.flash_channel port map( @@ -335,7 +361,9 @@ begin flash_cfifo_write => flash_cfifo_write, flash_rfifo_data => flash_rfifo_data, flash_rfifo_rdack => flash_rfifo_rdack, - flash_rfifo_rempty => flash_rfifo_rempty + flash_rfifo_rempty => flash_rfifo_rempty, + flash_wfifo_data => flash_wfifo_data, + flash_wfifo_write => flash_wfifo_write ); -- uart channel logic diff --git a/hdl/ip/vhd/espi/flash_channel/flash_channel.vhd b/hdl/ip/vhd/espi/flash_channel/flash_channel.vhd index 8ad67bb6..f667a98f 100644 --- a/hdl/ip/vhd/espi/flash_channel/flash_channel.vhd +++ b/hdl/ip/vhd/espi/flash_channel/flash_channel.vhd @@ -6,6 +6,14 @@ -- channel. It is responsible for queueing up transactions, issuing commands to -- to the spi flash block and providing flash response data to the transaction -- layer. +-- +-- Reads, writes and erases all take a descriptor and a 1kB slot of the DPR. +-- For a read the slot holds the flash data on its way back to the host. For a +-- write it holds the host's payload, captured as the command is still being +-- parsed (before its CRC is known good), and streamed out to the flash block +-- only once the descriptor is enqueued. Writes and erases come back from the +-- flash block as a single status byte, which lands at offset 0 of the slot +-- and only decides whether the completion is reported as successful. library ieee; use ieee.std_logic_1164.all; @@ -13,6 +21,7 @@ use ieee.numeric_std.all; use ieee.numeric_std_unsigned.all; use work.espi_base_types_pkg.all; +use work.espi_protocol_pkg.all; use work.flash_channel_pkg.all; entity flash_channel is @@ -40,6 +49,11 @@ entity flash_channel is flash_rfifo_data : in std_logic_vector(7 downto 0); flash_rfifo_rdack : out std_logic; flash_rfifo_rempty: in std_logic; + -- write payload fifo, host to flash. A whole payload is pushed before + -- the command words for it are, so the flash block never sees a + -- write command it does not already have all the bytes for. + flash_wfifo_data : out std_logic_vector(7 downto 0); + flash_wfifo_write : out std_logic ); end; @@ -51,7 +65,8 @@ architecture rtl of flash_channel is constant max_txn_size : integer := 1024; subtype desc_index_t is natural range 0 to num_descriptors - 1; signal dpr_waddr : std_logic_vector(11 downto 0); - signal dpr_raddr : std_logic_vector(11 downto 0); + signal dpr_wdata : std_logic_vector(7 downto 0); + signal dpr_wren : std_logic; signal readdata : std_logic_vector(7 downto 0); function add_wrap(a : natural; max: natural) return natural is @@ -63,8 +78,19 @@ architecture rtl of flash_channel is end if; end function; + -- Bytes the flash block hands back for a descriptor: the data for a + -- read, one status byte for anything else. + function flash_side_bytes(desc : descriptor_t) return std_logic_vector is + begin + if desc.kind = flash_rd then + return desc.xfr_size_bytes; + else + return To_Std_Logic_Vector(1, desc.xfr_size_bytes'length); + end if; + end function; + - type cmd_state_t is (idle, issue_flash_addr, issue_flash_len, wait_for_data); + type cmd_state_t is (idle, stream_payload, issue_flash_addr, issue_flash_len, wait_for_data); type complete_state_t is (idle, read_dpr); type reg_type is record @@ -76,22 +102,63 @@ architecture rtl of flash_channel is cmd_queue: command_queue_t; dpr_write_en: std_logic; dpr_wdata_buf: std_logic_vector(7 downto 0); + -- host payload capture, registered so it shares the DPR write port + -- with the flash-side path above without ever colliding with it + host_wr_en : std_logic; + host_wdata : std_logic_vector(7 downto 0); + host_waddr : std_logic_vector(11 downto 0); + -- payload stream out to the flash block + wfifo_write : std_logic; + wfifo_wdata : std_logic_vector(7 downto 0); + -- DPR read address, registered so the read port sees a flop rather + -- than a counter, an adder and the reader mux: the LUTRAM read plus + -- the response processor's own muxing already fills most of a + -- 125MHz period. + dpr_raddr : std_logic_vector(11 downto 0); + -- completion header, latched when the get arrives so the descriptor + -- can be retired before the header has gone out on the wire + resp_tag : std_logic_vector(3 downto 0); + resp_length : std_logic_vector(11 downto 0); + resp_cycle_type : std_logic_vector(7 downto 0); tail_desc: desc_index_t; issue_desc: desc_index_t; head_desc: desc_index_t; flash_np_free : std_logic; flash_c_avail: std_logic; end record; - constant reg_reset : reg_type := (idle, idle, 0, 0, 0, (others => descriptor_init), '0', (others => '0'), 0, 0, 0, '0', '0'); + constant reg_reset : reg_type := ( + flash_cmd_state => idle, + compl_state => idle, + flash_side_cntr => 0, + flash_write_addr_offset => 0, + compl_side_cntr => 0, + cmd_queue => (others => descriptor_init), + dpr_write_en => '0', + dpr_wdata_buf => (others => '0'), + host_wr_en => '0', + host_wdata => (others => '0'), + host_waddr => (others => '0'), + wfifo_write => '0', + wfifo_wdata => (others => '0'), + dpr_raddr => (others => '0'), + resp_tag => (others => '0'), + resp_length => (others => '0'), + resp_cycle_type => success_with_data_only, + tail_desc => 0, + issue_desc => 0, + head_desc => 0, + flash_np_free => '0', + flash_c_avail => '0' + ); signal r, rin : reg_type; - signal dpr_wdata: std_logic_vector(7 downto 0); + signal dpr_wdata_dbg: std_logic_vector(7 downto 0); signal dpr_rdata: std_logic_vector(7 downto 0); signal dpr_read_ack: std_logic; signal dpr_wr_delay: std_logic; attribute mark_debug of r : signal is "TRUE"; - attribute mark_debug of dpr_wdata : signal is "TRUE"; + attribute mark_debug of dpr_wdata_dbg : signal is "TRUE"; attribute mark_debug of dpr_rdata : signal is "TRUE"; attribute mark_debug of dpr_read_ack : signal is "TRUE"; attribute mark_debug of dpr_wr_delay : signal is "TRUE"; @@ -102,13 +169,13 @@ begin dbg_regs: process (clk, reset) begin if reset then - dpr_wdata <= (others => '0'); + dpr_wdata_dbg <= (others => '0'); dpr_rdata <= (others => '0'); dpr_read_ack <= '0'; dpr_wr_delay <= '0'; elsif rising_edge(clk) then dpr_rdata <= readdata; - dpr_wdata <= flash_rfifo_data; + dpr_wdata_dbg <= flash_rfifo_data; dpr_read_ack <= response.ready; dpr_wr_delay <= r.dpr_write_en; end if; @@ -120,16 +187,22 @@ begin flash_np_free <= r.flash_np_free; -- flash_c_avail is set when we have pending data to be read back out, but critically this status needs to represent - -- the state *after* any current message, so if we're responding now and this response is the only one available, + -- the status *after* any current message, so if we're responding now and this response is the only one available, -- this needs to be set to 0. flash_c_avail <= r.flash_c_avail when enabled else '0'; + -- The length word carries the request kind in its top nibble; reads + -- encode as zero there so the flash block sees the original two-word + -- command for them. flash_cfifo_data <= r.cmd_queue(r.issue_desc).sp5_addr when r.flash_cmd_state = issue_flash_addr else - resize(r.cmd_queue(r.issue_desc).xfr_size_bytes, flash_cfifo_data'length) when r.flash_cmd_state = issue_flash_len else + to_kind_bits(r.cmd_queue(r.issue_desc).kind) & resize(r.cmd_queue(r.issue_desc).xfr_size_bytes, flash_cfifo_data'length - 4) when r.flash_cmd_state = issue_flash_len else (others => '0'); flash_cfifo_write <= '1' when r.flash_cmd_state = issue_flash_addr or r.flash_cmd_state = issue_flash_len else '0'; + flash_wfifo_data <= r.wfifo_wdata; + flash_wfifo_write <= r.wfifo_write; + -- Let's put a 4kB buffer here as a starting point and see how it goes, this would allow 4 1024Byte max size transactions -- or we could shrink and say 2 2kB etc. We know we're only going read on this interface so we don't have to worry so much about -- various concurrency issues here. This should fit in a single 32kb block ram on the FPGA @@ -142,21 +215,26 @@ begin port map( wclk => clk, waddr => dpr_waddr, - wdata => r.dpr_wdata_buf, - wren => r.dpr_write_en, + wdata => dpr_wdata, + wren => dpr_wren, rclk => clk, - raddr => dpr_raddr, + raddr => r.dpr_raddr, rdata => readdata ); response.data <= readdata; response.valid <= '1' when r.compl_state = read_dpr else '0'; - response.tag <= r.cmd_queue(r.tail_desc).tag; - response.length <= r.cmd_queue(r.tail_desc).xfr_size_bytes; - response.cycle_type <= "00001111"; -- successful completion of with data, only completion for a split txn - - - dpr_waddr <= To_Std_Logic_Vector(r.issue_desc * max_txn_size + r.flash_write_addr_offset, 12); - dpr_raddr <= To_Std_Logic_Vector(r.tail_desc * max_txn_size + r.compl_side_cntr, 12); + response.tag <= r.resp_tag; + response.length <= r.resp_length; + response.cycle_type <= r.resp_cycle_type; + + -- One write port, two writers. The host path is registered off the + -- incoming stream (which cannot be stalled) and the flash path only + -- schedules itself on a cycle where the host is not writing, so the two + -- enables are never set together. + dpr_wren <= r.host_wr_en or r.dpr_write_en; + dpr_waddr <= r.host_waddr when r.host_wr_en = '1' else + To_Std_Logic_Vector(r.issue_desc * max_txn_size + r.flash_write_addr_offset, 12); + dpr_wdata <= r.host_wdata when r.host_wr_en = '1' else r.dpr_wdata_buf; -- We have two state machines running here as both need to be able to update -- the descriptor queues. @@ -178,21 +256,37 @@ begin begin v := r; + ------- + -- Capturing a write payload into the head descriptor's slot + ------ + -- This happens while the command is still arriving, so nothing is + -- known about its CRC yet. If the CRC turns out bad the enqueue + -- below never happens and the next request simply overwrites the + -- slot. The offset is clamped to the slot so an oversized length + -- wraps within it rather than trampling a neighbour. + v.host_wr_en := request.wdata_valid; + v.host_wdata := request.wdata; + v.host_waddr := To_Std_Logic_Vector(r.head_desc * max_txn_size + to_integer(request.wdata_idx(9 downto 0)), 12); + ------- -- Adding new requestes to the processing queue ------ - -- Command processing requests flash command queue, having already filtered - -- out any invalid commands such as writes/erases + -- Command processing requests flash command queue. Writes and erases + -- the command processor was not permitted to accept arrive marked + -- refused and still take a descriptor, so that the host gets an + -- unsuccessful completion rather than silence. -- we have 4 queue (txn) slots and can do 1024 byte per transaction max -- We simply carve up the DPR into 4 slots and then use those one for -- each descriptor, and we use the descriptors in order. if request.flash_np_enqueue_req and r.flash_np_free = '1' then + v.cmd_queue(r.head_desc).kind := request.kind; v.cmd_queue(r.head_desc).sp5_addr := request.sp5_flash_address; v.cmd_queue(r.head_desc).xfr_size_bytes := request.espi_hdr.length; v.cmd_queue(r.head_desc).active := true; v.cmd_queue(r.head_desc).tag := request.espi_hdr.tag; v.cmd_queue(r.head_desc).flash_issued := false; v.cmd_queue(r.head_desc).done := false; + v.cmd_queue(r.head_desc).failed := false; v.head_desc := add_wrap(r.head_desc, desc_index_t'high); end if; @@ -227,12 +321,39 @@ begin -- and store the data back in the DPR and then issue a completion request, and hold until -- the master does a get to get the data v.dpr_write_en := '0'; --only single cycle reads, default to 0 + v.wfifo_write := '0'; case r.flash_cmd_state is when idle => -- have active command that hasn't been issued to flash if flash_issue_needed then + v.flash_side_cntr := 0; + case r.cmd_queue(r.issue_desc).kind is + when flash_refused => + -- Nothing goes to the flash; it is complete + -- (unsuccessfully) as soon as it is looked at. + v.cmd_queue(r.issue_desc).flash_issued := true; + v.cmd_queue(r.issue_desc).done := true; + v.cmd_queue(r.issue_desc).failed := true; + v.issue_desc := add_wrap(r.issue_desc, desc_index_t'high); + when flash_wr => + v.flash_cmd_state := stream_payload; + when others => + v.flash_cmd_state := issue_flash_addr; + end case; + end if; + -- Push the captured payload out ahead of the command words. + -- readdata is combinational off dpr_raddr, so the byte for this + -- cycle's counter is registered on the way out. + when stream_payload => + if r.compl_state /= idle then + null; -- completion owns the read port + elsif r.flash_side_cntr = r.cmd_queue(r.issue_desc).xfr_size_bytes then + v.flash_side_cntr := 0; v.flash_cmd_state := issue_flash_addr; - + else + v.wfifo_write := '1'; + v.wfifo_wdata := readdata; + v.flash_side_cntr := r.flash_side_cntr + 1; end if; -- issue to flash, and wait until we get all the data back -- and have stored it into the DPR. We can't issue more than @@ -248,18 +369,24 @@ begin v.cmd_queue(r.issue_desc).flash_issued := true; when wait_for_data => - if r.flash_side_cntr = r.cmd_queue(r.issue_desc).xfr_size_bytes then + if r.flash_side_cntr = flash_side_bytes(r.cmd_queue(r.issue_desc)) then v.cmd_queue(r.issue_desc).done := true; v.flash_cmd_state := idle; v.flash_side_cntr := 0; v.issue_desc := add_wrap(r.issue_desc, desc_index_t'high); -- "empty" isn't strictly valid if we're acking this cycle since this write could - -- empty it. We only check for empty on a cycle where we're not acking - elsif not flash_rfifo_rempty and (not r.dpr_write_en) then + -- empty it. We only check for empty on a cycle where we're not acking. + -- A host payload byte arriving this cycle takes the write port next cycle. + elsif flash_rfifo_rempty = '0' and r.dpr_write_en = '0' and request.wdata_valid = '0' then v.dpr_write_en := '1'; v.dpr_wdata_buf := flash_rfifo_data; v.flash_write_addr_offset := r.flash_side_cntr; v.flash_side_cntr := r.flash_side_cntr + 1; + -- for a write or erase the one byte back is a status, + -- zero meaning the flash block finished it cleanly + if r.cmd_queue(r.issue_desc).kind /= flash_rd then + v.cmd_queue(r.issue_desc).failed := flash_rfifo_data /= x"00"; + end if; end if; end case; @@ -271,12 +398,24 @@ begin if request.flash_get_req and flash_c_avail = '1' then v.compl_state := read_dpr; v.compl_side_cntr := 0; + v.resp_tag := r.cmd_queue(r.tail_desc).tag; + if r.cmd_queue(r.tail_desc).kind = flash_rd then + v.resp_length := r.cmd_queue(r.tail_desc).xfr_size_bytes; + v.resp_cycle_type := success_with_data_only; + elsif r.cmd_queue(r.tail_desc).failed then + v.resp_length := (others => '0'); + v.resp_cycle_type := unsuccessful_no_data_only; + else + v.resp_length := (others => '0'); + v.resp_cycle_type := success_no_data; + end if; end if; when read_dpr => -- We have a done descriptor, we need to read the data back out - -- to the eSPI master - if r.compl_side_cntr = r.cmd_queue(r.tail_desc).xfr_size_bytes then + -- to the eSPI master. A completion without data retires + -- straight away; the header fields were latched above. + if r.compl_side_cntr = r.resp_length then v.cmd_queue(r.tail_desc).active := false; v.cmd_queue(r.tail_desc).done := false; v.cmd_queue(r.tail_desc).flash_issued := false; @@ -287,6 +426,18 @@ begin end if; end case; + ------ + -- One read port, two readers. Completions win; payload streaming + -- pauses for as long as one is in progress. Computed from the next + -- state so that the registered address always matches the counter + -- the reader is on. + ------ + if v.flash_cmd_state = stream_payload and v.compl_state = idle then + v.dpr_raddr := To_Std_Logic_Vector(v.issue_desc * max_txn_size + v.flash_side_cntr, 12); + else + v.dpr_raddr := To_Std_Logic_Vector(v.tail_desc * max_txn_size + v.compl_side_cntr, 12); + end if; + if not enabled then -- If we're not enabled, reset the state machine v := reg_reset; @@ -304,6 +455,4 @@ begin end process; - - -end rtl; \ No newline at end of file +end rtl; diff --git a/hdl/ip/vhd/espi/flash_channel/flash_channel_pkg.vhd b/hdl/ip/vhd/espi/flash_channel/flash_channel_pkg.vhd index 32de120a..c972e681 100644 --- a/hdl/ip/vhd/espi/flash_channel/flash_channel_pkg.vhd +++ b/hdl/ip/vhd/espi/flash_channel/flash_channel_pkg.vhd @@ -12,7 +12,19 @@ use work.espi_base_types_pkg.all; package flash_channel_pkg is constant num_descriptors : integer := 4; + -- What a queued flash request is. "refused" is a write or erase that + -- arrived while writes were not permitted: it still occupies a + -- descriptor so that the host gets a completion, an unsuccessful one, + -- rather than waiting forever. + type flash_kind_t is (flash_rd, flash_wr, flash_er, flash_refused); + + -- Encoding of the kind in the top nibble of the length word handed to + -- the flash controller. Reads encode as zero so a controller that only + -- understands the original two-word read command sees nothing new. + function to_kind_bits(kind : flash_kind_t) return std_logic_vector; + type descriptor_t is record + kind : flash_kind_t; sp5_addr : std_logic_vector(31 downto 0); xfr_size_bytes : std_logic_vector(11 downto 0); ready_bytes: std_logic_vector(11 downto 0); @@ -20,16 +32,20 @@ package flash_channel_pkg is active: boolean; -- valid waiting for processing or being processed flash_issued: boolean; done: boolean; + -- set once the flash controller reported the write or erase failed + failed: boolean; end record; constant descriptor_init : descriptor_t := ( + kind => flash_rd, sp5_addr => (others => '0'), xfr_size_bytes => (others => '0'), ready_bytes => (others => '0'), tag => (others => '0'), active => false, flash_issued => false, - done => false + done => false, + failed => false ); type command_queue_t is array(0 to num_descriptors - 1) of descriptor_t; @@ -38,11 +54,18 @@ package flash_channel_pkg is type flash_channel_req_t is record espi_hdr : espi_cmd_header; sp5_flash_address : std_logic_vector(31 downto 0); + kind : flash_kind_t; flash_np_enqueue_req : boolean; flash_get_req : boolean; + -- Write payload, streamed as the command is parsed, ahead of the + -- enqueue that follows a good CRC. + wdata : std_logic_vector(7 downto 0); + wdata_valid : std_logic; + wdata_idx : std_logic_vector(11 downto 0); end record; view flash_chan_req_sink of flash_channel_req_t is - espi_hdr, sp5_flash_address, flash_np_enqueue_req, flash_get_req : in; + espi_hdr, sp5_flash_address, kind, flash_np_enqueue_req, flash_get_req, + wdata, wdata_valid, wdata_idx : in; end view; alias flash_chan_req_source is flash_chan_req_sink'converse; @@ -63,4 +86,18 @@ package flash_channel_pkg is end view; alias flash_chan_resp_sink is flash_chan_resp_source'converse; -end package; \ No newline at end of file +end package; + +package body flash_channel_pkg is + + function to_kind_bits(kind : flash_kind_t) return std_logic_vector is + begin + case kind is + when flash_rd => return x"0"; + when flash_wr => return x"1"; + when flash_er => return x"2"; + when flash_refused => return x"0"; + end case; + end function; + +end package body; \ No newline at end of file diff --git a/hdl/ip/vhd/espi/link_layer/link_layer_pkg.vhd b/hdl/ip/vhd/espi/link_layer/link_layer_pkg.vhd index 307b0524..83177847 100644 --- a/hdl/ip/vhd/espi/link_layer/link_layer_pkg.vhd +++ b/hdl/ip/vhd/espi/link_layer/link_layer_pkg.vhd @@ -64,7 +64,6 @@ package body link_layer_pkg is opcode_get_configuration | opcode_reset | opcode_get_flash_c | - opcode_put_flash_np | opcode_get_np | opcode_put_vwire | opcode_get_oob | @@ -84,6 +83,10 @@ package body link_layer_pkg is when opcode_put_np | opcode_get_pc => return true; + when opcode_put_flash_np => + -- reads and erases are address only; a write carries its + -- payload so its size is not known until the length is + return h.cycle_type /= flash_write; when others => return false; end case?; @@ -96,7 +99,8 @@ package body link_layer_pkg is -- '-' (don't care) values. case? h.opcode is when opcode_put_pc | - opcode_put_oob => + opcode_put_oob | + opcode_put_flash_np => return true; when others => return false; @@ -149,10 +153,14 @@ package body link_layer_pkg is return To_StdLogicVector(0, 13); end case; when opcode_put_flash_np => - -- opcode, standard header (3), 4 address bytes, length bytes, crc - -- but we only support opcodes of reads, which don't have length data - -- in the cmd payload - return To_StdLogicVector(1 + 3 + 4 + 1, 13); + -- opcode, standard header (3), 4 address bytes, crc, plus the + -- payload for a write. For an erase the length field is a + -- size code and there is no payload. + if h.cycle_type = flash_write then + return To_StdLogicVector(1 + 3 + 4 + to_integer(h.len) + 1, 13); + else + return To_StdLogicVector(1 + 3 + 4 + 1, 13); + end if; when opcode_put_oob => -- opcode, standard header (3), length bytes, crc return To_StdLogicVector(1 + 3 + to_integer(h.len) + 1, 13); diff --git a/hdl/ip/vhd/espi/sims/espi_tb.vhd b/hdl/ip/vhd/espi/sims/espi_tb.vhd index da7671d2..242a3699 100644 --- a/hdl/ip/vhd/espi/sims/espi_tb.vhd +++ b/hdl/ip/vhd/espi/sims/espi_tb.vhd @@ -19,6 +19,7 @@ use work.espi_spec_regs_pkg.all; use work.espi_regs_pkg; use work.espi_dbg_vc_pkg.all; use work.espi_tb_pkg.all; +use work.espi_protocol_pkg.all; use work.sp5_post_code_pkg.all; entity espi_tb is @@ -52,6 +53,8 @@ begin variable payload_size : integer; variable response : resp_t := (queue => new_queue, num_bytes => 0, response_code => (others => '0'), status => (others => '0'), crc_ok => false); variable pcfree_deasserted : boolean; + variable cycle_type : std_logic_vector(7 downto 0); + variable length_field : std_logic_vector(11 downto 0); begin -- Always the first thing in the process, set up things for the VUnit test runner test_runner_setup(runner, runner_cfg); @@ -186,14 +189,129 @@ begin end if; get_flash_c(net, 16, my_queue, response_code, status, crc_ok); check(crc_ok, "CRC Check failed"); - -- TODO: the data's not coming back right. for j in 0 to 15 loop - report "Flash Byte: " & to_hstring(to_unsigned(pop_byte(my_queue), 8)); + check_equal(To_Std_Logic_Vector(pop_byte(my_queue), 8), fake_flash_pattern(16#03020000# + j), + "Flash byte " & integer'image(j)); end loop; end loop; -- would normally wait for the completion alert now wait for 300 us; + elsif run("flash_write") then + flash_cap_reg.flash_channel_enable := '1'; + set_config(net, CH3_CAPABILITIES_OFFSET, pack(flash_cap_reg), response_code, status, crc_ok); + check(crc_ok, "Set Config CRC Check failed"); + write_bus(net, bus_handle, To_StdLogicVector(espi_regs_pkg.CONTROL_OFFSET, bus_handle.p_address_length), + espi_regs_pkg.CONTROL_FLASH_WRITE_ENABLE_MASK); + wait_until_idle(net, bus_handle); + + -- Erase, so the write lands on ones and reads back exactly + put_flash_erase(net, X"00001000", flash_erase_4k, response_code, status, crc_ok); + check(crc_ok, "put_flash_erase CRC Check failed"); + wait_for_alert(net); + get_flash_completion(net, 0, my_queue, cycle_type, length_field, response_code, status, crc_ok); + check(crc_ok, "erase completion CRC Check failed"); + check_equal(cycle_type, success_no_data, "erase completion cycle type"); + check_equal(length_field, std_logic_vector'(x"000"), "erase completion length"); + + -- 64 bytes is the channel's max payload, written across a + -- page boundary to make sure nothing wraps on the way through + for i in 0 to 63 loop + push_byte(my_queue, to_integer(write_pattern(16#10F0# + i))); + end loop; + put_flash_write(net, X"000010F0", 64, my_queue, response_code, status, crc_ok); + check(crc_ok, "put_flash_write CRC Check failed"); + wait_for_alert(net); + get_flash_completion(net, 0, my_queue, cycle_type, length_field, response_code, status, crc_ok); + check(crc_ok, "write completion CRC Check failed"); + check_equal(cycle_type, success_no_data, "write completion cycle type"); + check_equal(length_field, std_logic_vector'(x"000"), "write completion length"); + + -- Read back the written range plus a byte either side + put_flash_read(net, X"000010EF", 66, response_code, status, crc_ok); + check(crc_ok, "put_flash_read CRC Check failed"); + wait_for_alert(net); + get_flash_completion(net, 66, my_queue, cycle_type, length_field, response_code, status, crc_ok); + check(crc_ok, "read completion CRC Check failed"); + check_equal(cycle_type, success_with_data_only, "read completion cycle type"); + check_equal(To_Std_Logic_Vector(pop_byte(my_queue), 8), std_logic_vector'(x"FF"), "byte before write still erased"); + for i in 0 to 63 loop + check_equal(To_Std_Logic_Vector(pop_byte(my_queue), 8), write_pattern(16#10F0# + i), + "written byte " & integer'image(i)); + end loop; + check_equal(To_Std_Logic_Vector(pop_byte(my_queue), 8), std_logic_vector'(x"FF"), "byte after write still erased"); + elsif run("flash_erase") then + flash_cap_reg.flash_channel_enable := '1'; + set_config(net, CH3_CAPABILITIES_OFFSET, pack(flash_cap_reg), response_code, status, crc_ok); + check(crc_ok, "Set Config CRC Check failed"); + write_bus(net, bus_handle, To_StdLogicVector(espi_regs_pkg.CONTROL_OFFSET, bus_handle.p_address_length), + espi_regs_pkg.CONTROL_FLASH_WRITE_ENABLE_MASK); + wait_until_idle(net, bus_handle); + + put_flash_erase(net, X"00002000", flash_erase_4k, response_code, status, crc_ok); + check(crc_ok, "put_flash_erase CRC Check failed"); + wait_for_alert(net); + get_flash_completion(net, 0, my_queue, cycle_type, length_field, response_code, status, crc_ok); + check(crc_ok, "erase completion CRC Check failed"); + check_equal(cycle_type, success_no_data, "erase completion cycle type"); + + -- last bytes of the sector are erased, first of the next are not + put_flash_read(net, X"00002FF8", 16, response_code, status, crc_ok); + check(crc_ok, "put_flash_read CRC Check failed"); + wait_for_alert(net); + get_flash_c(net, 16, my_queue, response_code, status, crc_ok); + check(crc_ok, "read completion CRC Check failed"); + for i in 0 to 7 loop + check_equal(To_Std_Logic_Vector(pop_byte(my_queue), 8), std_logic_vector'(x"FF"), "erased byte " & integer'image(i)); + end loop; + for i in 8 to 15 loop + check_equal(To_Std_Logic_Vector(pop_byte(my_queue), 8), fake_flash_pattern(16#2FF8# + i), "neighbour byte " & integer'image(i)); + end loop; + + -- an unsupported size code is answered, unsuccessfully + put_flash_erase(net, X"00002000", flash_erase_32k, response_code, status, crc_ok); + check(crc_ok, "put_flash_erase CRC Check failed"); + wait_for_alert(net); + get_flash_completion(net, 0, my_queue, cycle_type, length_field, response_code, status, crc_ok); + check(crc_ok, "erase completion CRC Check failed"); + check_equal(cycle_type, unsuccessful_no_data_only, "unsupported erase completion cycle type"); + check_equal(length_field, std_logic_vector'(x"000"), "unsupported erase completion length"); + elsif run("write_refused_by_register") then + -- Channel up but the write enable bit left at its reset + -- value: writes and erases are answered, unsuccessfully, and + -- the flash is untouched. + flash_cap_reg.flash_channel_enable := '1'; + set_config(net, CH3_CAPABILITIES_OFFSET, pack(flash_cap_reg), response_code, status, crc_ok); + check(crc_ok, "Set Config CRC Check failed"); + + for i in 0 to 15 loop + push_byte(my_queue, 0); + end loop; + put_flash_write(net, X"00004000", 16, my_queue, response_code, status, crc_ok); + check(crc_ok, "put_flash_write CRC Check failed"); + wait_for_alert(net); + get_flash_completion(net, 0, my_queue, cycle_type, length_field, response_code, status, crc_ok); + check(crc_ok, "write completion CRC Check failed"); + check_equal(cycle_type, unsuccessful_no_data_only, "refused write completion cycle type"); + check_equal(length_field, std_logic_vector'(x"000"), "refused write completion length"); + + put_flash_erase(net, X"00004000", flash_erase_4k, response_code, status, crc_ok); + check(crc_ok, "put_flash_erase CRC Check failed"); + wait_for_alert(net); + get_flash_completion(net, 0, my_queue, cycle_type, length_field, response_code, status, crc_ok); + check(crc_ok, "erase completion CRC Check failed"); + check_equal(cycle_type, unsuccessful_no_data_only, "refused erase completion cycle type"); + + -- and a read still works afterwards, returning the untouched contents + put_flash_read(net, X"00004000", 16, response_code, status, crc_ok); + check(crc_ok, "put_flash_read CRC Check failed"); + wait_for_alert(net); + get_flash_completion(net, 16, my_queue, cycle_type, length_field, response_code, status, crc_ok); + check(crc_ok, "read completion CRC Check failed"); + check_equal(cycle_type, success_with_data_only, "read completion cycle type"); + for i in 0 to 15 loop + check_equal(To_Std_Logic_Vector(pop_byte(my_queue), 8), fake_flash_pattern(16#4000# + i), "untouched byte " & integer'image(i)); + end loop; elsif run("oob_no_pec_uart") then enable_debug_mode(net); --Enable OOB Channel diff --git a/hdl/ip/vhd/espi/sims/espi_tb_pkg.vhd b/hdl/ip/vhd/espi/sims/espi_tb_pkg.vhd index cf369dd9..8c6c2773 100644 --- a/hdl/ip/vhd/espi/sims/espi_tb_pkg.vhd +++ b/hdl/ip/vhd/espi/sims/espi_tb_pkg.vhd @@ -71,6 +71,22 @@ package espi_tb_pkg is constant num_bytes: integer; constant bad_crc : boolean := false ) return cmd_t; + -- Payload is consumed from the queue + impure function build_put_flash_write_cmd( + constant address : in std_logic_vector(31 downto 0); + constant num_bytes: integer; + constant payload : queue_t; + constant bad_crc : boolean := false + ) return cmd_t; + impure function build_put_flash_erase_cmd( + constant address : in std_logic_vector(31 downto 0); + constant size_code: std_logic_vector(11 downto 0); + constant bad_crc : boolean := false + ) return cmd_t; + -- Initial contents of the fake flash behind the test harness. Also a + -- convenient source of write payloads that differ from what is there. + function fake_flash_pattern(constant addr : natural) return std_logic_vector; + function write_pattern(constant addr : natural) return std_logic_vector; impure function build_get_flash_c_cmd( constant bad_crc : boolean := false ) return cmd_t; @@ -197,6 +213,72 @@ package body espi_tb_pkg is return cmd; end function; + function fake_flash_pattern(constant addr : natural) return std_logic_vector is + begin + -- odd multiplier so any aligned 256 byte run is a bijection + return To_Std_Logic_Vector((addr * 7 + 3) mod 256, 8); + end function; + + function write_pattern(constant addr : natural) return std_logic_vector is + begin + -- flash programming can only clear bits, so what is written has to + -- be a subset of the erased state (all ones) and not of the initial + -- pattern: tests erase first and then check for exactly this + return To_Std_Logic_Vector((addr * 13 + 5) mod 256, 8); + end function; + + -- Common header for the put_flash_np family: opcode, cycle type, + -- tag/length, length, then the 32 bit address MSB first. + impure function build_put_flash_hdr( + constant cycle_kind : std_logic_vector(7 downto 0); + constant address : in std_logic_vector(31 downto 0); + constant length_field: std_logic_vector(11 downto 0) + ) return cmd_t is + variable cmd : cmd_t := (new_queue, 0); + begin + push_byte(cmd.queue, to_integer(opcode_put_flash_np)); + push_byte(cmd.queue, to_integer(cycle_kind)); + push_byte(cmd.queue, to_integer("0000" & length_field(11 downto 8))); + push_byte(cmd.queue, to_integer(length_field(7 downto 0))); + push_byte(cmd.queue, to_integer(address(31 downto 24))); + push_byte(cmd.queue, to_integer(address(23 downto 16))); + push_byte(cmd.queue, to_integer(address(15 downto 8))); + push_byte(cmd.queue, to_integer(address(7 downto 0))); + cmd.num_bytes := 8; + return cmd; + end function; + + impure function build_put_flash_write_cmd( + constant address : in std_logic_vector(31 downto 0); + constant num_bytes: integer; + constant payload : queue_t; + constant bad_crc : boolean := false + ) return cmd_t is + variable cmd : cmd_t; + begin + cmd := build_put_flash_hdr(flash_write, address, To_Std_Logic_Vector(num_bytes, 12)); + for i in 0 to num_bytes - 1 loop + push_byte(cmd.queue, pop_byte(payload)); + end loop; + cmd.num_bytes := cmd.num_bytes + num_bytes; + push_byte(cmd.queue, to_integer(crc8_atm(cmd.queue, bad_crc))); + cmd.num_bytes := cmd.num_bytes + 1; + return cmd; + end function; + + impure function build_put_flash_erase_cmd( + constant address : in std_logic_vector(31 downto 0); + constant size_code: std_logic_vector(11 downto 0); + constant bad_crc : boolean := false + ) return cmd_t is + variable cmd : cmd_t; + begin + cmd := build_put_flash_hdr(flash_erase, address, size_code); + push_byte(cmd.queue, to_integer(crc8_atm(cmd.queue, bad_crc))); + cmd.num_bytes := cmd.num_bytes + 1; + return cmd; + end function; + impure function build_put_flash_np_cmd( constant address : in std_logic_vector(31 downto 0); constant num_bytes: integer; diff --git a/hdl/ip/vhd/espi/sims/espi_th.vhd b/hdl/ip/vhd/espi/sims/espi_th.vhd index c14ca38f..08b19d56 100644 --- a/hdl/ip/vhd/espi/sims/espi_th.vhd +++ b/hdl/ip/vhd/espi/sims/espi_th.vhd @@ -37,6 +37,8 @@ architecture th of espi_th is signal flash_rfifo_data : std_logic_vector(7 downto 0); signal flash_rfifo_rdack : std_logic; signal flash_rfifo_rempty : std_logic; + signal flash_wfifo_data : std_logic_vector(7 downto 0); + signal flash_wfifo_write : std_logic; signal axi_if : axil15x32_pkg.axil_t; signal uart_data_line : std_logic; signal uart_handshake : std_logic; @@ -92,7 +94,13 @@ begin bresp => axi_if.write_response.resp ); + -- Built with writes allowed so the write path can be exercised; the + -- runtime enable bit still starts off, which is what the refusal tests + -- rely on. dut: entity work.espi_target_top + generic map ( + FLASH_WRITES_ALLOWED => true + ) port map ( clk => clk, reset => reset, @@ -109,6 +117,8 @@ begin flash_rfifo_data => flash_rfifo_data, flash_rfifo_rdack => flash_rfifo_rdack, flash_rfifo_rempty => flash_rfifo_rempty, + flash_wfifo_data => flash_wfifo_data, + flash_wfifo_write => flash_wfifo_write, to_sp_uart_data => to_sp_uart_data, to_sp_uart_valid => to_sp_uart_valid, to_sp_uart_ready => to_sp_uart_ready, @@ -148,6 +158,8 @@ begin reset => reset, espi_cmd_fifo_data => flash_cfifo_data, espi_cmd_fifo_write => flash_cfifo_write, + espi_wfifo_data => flash_wfifo_data, + espi_wfifo_write => flash_wfifo_write, flash_rdata => flash_rfifo_data, flash_rdata_empty => flash_rfifo_rempty, flash_rdata_rdack => flash_rfifo_rdack diff --git a/hdl/ip/vhd/espi/sims/models/espi_controller_vc_pkg.vhd b/hdl/ip/vhd/espi/sims/models/espi_controller_vc_pkg.vhd index d712a69f..3ed7a919 100644 --- a/hdl/ip/vhd/espi/sims/models/espi_controller_vc_pkg.vhd +++ b/hdl/ip/vhd/espi/sims/models/espi_controller_vc_pkg.vhd @@ -71,6 +71,36 @@ package espi_controller_vc_pkg is variable status : inout std_logic_vector(15 downto 0); variable crc_ok : inout boolean ); + -- As get_flash_c but hands back the completion header too, for + -- completions whose cycle type is the interesting part. + procedure get_flash_completion( + signal net : inout network_t; + constant num_bytes : in integer; + variable data_queue: out queue_t; + variable cycle_type: out std_logic_vector(7 downto 0); + variable length: out std_logic_vector(11 downto 0); + variable response_code: inout std_logic_vector(7 downto 0); + variable status : inout std_logic_vector(15 downto 0); + variable crc_ok : inout boolean + ); + -- payload queue is consumed + procedure put_flash_write( + signal net : inout network_t; + constant address : in std_logic_vector(31 downto 0); + constant num_bytes: in integer; + constant payload : in queue_t; + variable response_code: inout std_logic_vector(7 downto 0); + variable status : inout std_logic_vector(15 downto 0); + variable crc_ok : inout boolean + ); + procedure put_flash_erase( + signal net : inout network_t; + constant address : in std_logic_vector(31 downto 0); + constant size_code: in std_logic_vector(11 downto 0); + variable response_code: inout std_logic_vector(7 downto 0); + variable status : inout std_logic_vector(15 downto 0); + variable crc_ok : inout boolean + ); procedure put_iowr_short4( signal net : inout network_t; @@ -272,6 +302,83 @@ package body espi_controller_vc_pkg is end; + procedure get_flash_completion( + signal net : inout network_t; + constant num_bytes : in integer; + variable data_queue: out queue_t; + variable cycle_type: out std_logic_vector(7 downto 0); + variable length: out std_logic_vector(11 downto 0); + variable response_code: inout std_logic_vector(7 downto 0); + variable status : inout std_logic_vector(15 downto 0); + variable crc_ok : inout boolean + ) is + variable cmd : cmd_t := (new_queue, 0); + variable rx_bytes : integer := 3 + num_bytes + 4; -- 3 bytes header, num_bytes, response, 16bit status, 1 crc, + variable msg_target : actor_t := find("espi_vc"); + variable rx_queue : queue_t := new_queue; + variable hdr_byte : std_logic_vector(7 downto 0); + begin + cmd := build_get_flash_c_cmd; + enqueue_tx_data_bytes(net, msg_target, cmd.num_bytes, cmd.queue); + enqueue_transaction(net, msg_target, cmd.num_bytes, rx_bytes); + get_rx_queue(net, msg_target, rx_queue); + crc_ok := check_queue_crc(rx_queue); -- non-destructive to queue + response_code := std_logic_vector(to_unsigned(pop_byte(rx_queue), 8)); + cycle_type := To_Std_Logic_Vector(pop_byte(rx_queue), 8); + hdr_byte := To_Std_Logic_Vector(pop_byte(rx_queue), 8); + length(11 downto 8) := hdr_byte(3 downto 0); + length(7 downto 0) := To_Std_Logic_Vector(pop_byte(rx_queue), 8); + for i in 0 to num_bytes -1 loop + push_byte(data_queue, pop_byte(rx_queue)); + end loop; + status := get_status_from_queue_and_flush(rx_queue); + end; + + procedure put_flash_write( + signal net : inout network_t; + constant address : in std_logic_vector(31 downto 0); + constant num_bytes: in integer; + constant payload : in queue_t; + variable response_code: inout std_logic_vector(7 downto 0); + variable status : inout std_logic_vector(15 downto 0); + variable crc_ok : inout boolean + ) is + variable cmd : cmd_t := (new_queue, 0); + variable rx_bytes : integer := 4; -- response, 16bit status, 1 crc + variable msg_target : actor_t := find("espi_vc"); + variable rx_queue : queue_t := new_queue; + begin + cmd := build_put_flash_write_cmd(address, num_bytes, payload); + enqueue_tx_data_bytes(net, msg_target, cmd.num_bytes, cmd.queue); + enqueue_transaction(net, msg_target, cmd.num_bytes, rx_bytes); + get_rx_queue(net, msg_target, rx_queue); + crc_ok := check_queue_crc(rx_queue); -- non-destructive to queue + response_code := std_logic_vector(to_unsigned(pop_byte(rx_queue), 8)); + status := get_status_from_queue_and_flush(rx_queue); + end; + + procedure put_flash_erase( + signal net : inout network_t; + constant address : in std_logic_vector(31 downto 0); + constant size_code: in std_logic_vector(11 downto 0); + variable response_code: inout std_logic_vector(7 downto 0); + variable status : inout std_logic_vector(15 downto 0); + variable crc_ok : inout boolean + ) is + variable cmd : cmd_t := (new_queue, 0); + variable rx_bytes : integer := 4; -- response, 16bit status, 1 crc + variable msg_target : actor_t := find("espi_vc"); + variable rx_queue : queue_t := new_queue; + begin + cmd := build_put_flash_erase_cmd(address, size_code); + enqueue_tx_data_bytes(net, msg_target, cmd.num_bytes, cmd.queue); + enqueue_transaction(net, msg_target, cmd.num_bytes, rx_bytes); + get_rx_queue(net, msg_target, rx_queue); + crc_ok := check_queue_crc(rx_queue); -- non-destructive to queue + response_code := std_logic_vector(to_unsigned(pop_byte(rx_queue), 8)); + status := get_status_from_queue_and_flush(rx_queue); + end; + procedure put_iowr_short4( signal net : inout network_t; constant address : in std_logic_vector(15 downto 0); diff --git a/hdl/ip/vhd/espi/sims/models/fake_flash_txn_mgr.vhd b/hdl/ip/vhd/espi/sims/models/fake_flash_txn_mgr.vhd index 434a5864..feb5bd78 100644 --- a/hdl/ip/vhd/espi/sims/models/fake_flash_txn_mgr.vhd +++ b/hdl/ip/vhd/espi/sims/models/fake_flash_txn_mgr.vhd @@ -8,13 +8,19 @@ -- The interface is simple: we have a 32bit wide FIFO for commands, and get 2 -- words there: First word is the 32bit flash address from SP5's perspective --- and the second word is the number of bytes to read. --- The read data is pushed back byte-by-byte into an 8bit wide FIFO. Note that +-- and the second word is the byte count, with the request kind in its top +-- nibble (0 read, 1 write, 2 erase) as flash_channel_pkg encodes it. +-- Read data is pushed back byte-by-byte into an 8bit wide FIFO. Note that -- This fifo is not necessarily deep enough to hold a whole transaction read -- (which may be many sets of 256byte blocks), but the espi block should -- generally be able to keep up so we may not have to model that here. --- To simplify the model, we're just going to put a counting pattern back --- into the FIFO as the "read data" which may facilitate debugging. +-- Writes take their payload from the byte stream the flash channel pushes +-- ahead of the command, and writes and erases answer with one status byte, +-- zero for success, the way the real client in spi_nor_top does. +-- +-- Behind it is a 64kB window of NOR-like memory, initialised to +-- fake_flash_pattern so reads can be checked, with clear-only programming +-- and erase-to-ones so writes and erases can be checked back through reads. library ieee; use ieee.std_logic_1164.all; @@ -25,6 +31,8 @@ library vunit_lib; context vunit_lib.vunit_context; context vunit_lib.com_context; +use work.espi_tb_pkg.all; + entity fake_flash_txn_mgr is port ( clk : in std_logic; @@ -32,6 +40,9 @@ entity fake_flash_txn_mgr is -- espi cmd fifo interface espi_cmd_fifo_data : in std_logic_vector(31 downto 0); espi_cmd_fifo_write : in std_logic; + -- write payload, host to flash + espi_wfifo_data : in std_logic_vector(7 downto 0) := (others => '0'); + espi_wfifo_write : in std_logic := '0'; -- Raw flash read_data flash_rdata : out std_logic_vector(7 downto 0); @@ -42,12 +53,18 @@ end entity; architecture model of fake_flash_txn_mgr is + constant window_bytes : natural := 16#10000#; constant cmd_queue : queue_t := new_queue; + constant wqueue : queue_t := new_queue; signal addr : std_logic_vector(31 downto 0); signal cmd_idx : natural range 0 to 1 := 0; signal write_en : std_logic; signal wdata : std_logic_vector(7 downto 0); + -- SAFS erase size codes + constant erase_4k : std_logic_vector(11 downto 0) := x"001"; + constant erase_64k : std_logic_vector(11 downto 0) := x"003"; + begin -- take in two words from the command fifo, (allow queueing more) @@ -67,46 +84,90 @@ begin else -- push both parts of the command into the fifo push(cmd_queue, addr); -- full address - push(cmd_queue, espi_cmd_fifo_data(11 downto 0)); -- txn size + push(cmd_queue, espi_cmd_fifo_data); -- kind and txn size cmd_idx <= 0; end if; end if; end process; - fake_flash_reads: process + capture_payload: process + begin + wait until rising_edge(clk); + if espi_wfifo_write = '1' then + push(wqueue, espi_wfifo_data); + end if; + end process; + + fake_flash: process + type mem_t is array (0 to window_bytes - 1) of std_logic_vector(7 downto 0); + variable mem : mem_t; variable addr : std_logic_vector(31 downto 0); - variable len : std_logic_vector(11 downto 0); - variable top : std_logic_vector(11 downto 0); - variable data : std_logic_vector(11 downto 0); + variable word : std_logic_vector(31 downto 0); + variable len : natural; + variable idx : natural; + variable erase_bytes : natural; + variable status : std_logic_vector(7 downto 0); + + -- Push one byte back with a few cycles of stall after it, so the + -- downstream logic is seen to cope with data that is not + -- clock-over-clock. The delay is somewhat arbitrary but has to be + -- more than the fifo latency for the stall to be visible. + procedure push_byte(constant b : std_logic_vector(7 downto 0)) is + begin + wdata <= b; + write_en <= '1'; + wait until rising_edge(clk); + write_en <= '0'; + for i in 0 to 5 loop + wait until rising_edge(clk); + end loop; + end procedure; begin + for i in mem'range loop + mem(i) := fake_flash_pattern(i); + end loop; write_en <= '0'; loop - exit when not is_empty(cmd_queue); - wait until falling_edge(clk); - end loop; - addr := pop(cmd_queue); - len := pop(cmd_queue); - top := len; - -- push some data into the fifo - while len > 0 loop - data := top - len; - wdata <= resize(data, wdata'length); - write_en <= '1'; - if rising_edge(clk)then - len := len - 1; - wait on clk; - write_en <= '0'; - -- We want this to be "fast" for sim but representative of a real - -- design so we wait a few cycles before pushing the next data into - -- the fifo. The number for delay here is somewhat arbirary, but - -- we want it to be more than 2 or 3 so fifo latency doesn't mask - -- the delay so that we see our downstream logic does the right thing - -- with stalls in the data. - for i in 0 to 5 loop - wait until rising_edge(clk); - end loop; - end if; - wait on clk; + loop + exit when not is_empty(cmd_queue); + wait until falling_edge(clk); + end loop; + addr := pop_std_ulogic_vector(cmd_queue); + word := pop_std_ulogic_vector(cmd_queue); + len := to_integer(word(11 downto 0)); + status := x"00"; + case word(31 downto 28) is + when x"1" => + -- the flash channel pushes the whole payload before the + -- command, so waiting here is only ever a fifo latency + while length(wqueue) < len loop + wait until rising_edge(clk); + end loop; + for i in 0 to len - 1 loop + idx := (to_integer(addr) + i) mod window_bytes; + mem(idx) := mem(idx) and pop_std_ulogic_vector(wqueue); + end loop; + push_byte(status); + when x"2" => + case word(11 downto 0) is + when erase_4k => erase_bytes := 4096; + when erase_64k => erase_bytes := 65536; + when others => erase_bytes := 0; + end case; + if erase_bytes = 0 then + status := x"01"; + else + for i in 0 to erase_bytes - 1 loop + idx := ((to_integer(addr) / erase_bytes) * erase_bytes + i) mod window_bytes; + mem(idx) := x"FF"; + end loop; + end if; + push_byte(status); + when others => + for i in 0 to len - 1 loop + push_byte(mem((to_integer(addr) + i) mod window_bytes)); + end loop; + end case; end loop; end process; diff --git a/hdl/ip/vhd/espi/sys_regs/espi_regs.rdl b/hdl/ip/vhd/espi/sys_regs/espi_regs.rdl index 26fdc291..9eb6f60c 100644 --- a/hdl/ip/vhd/espi/sys_regs/espi_regs.rdl +++ b/hdl/ip/vhd/espi/sys_regs/espi_regs.rdl @@ -32,6 +32,12 @@ addrmap espi_regs { field { desc = "Issue an espi reset via the debug logic. Cleared by hardware after reset."; } espi_reset[5:5] = 0; + field { + desc = "Allow the host to write and erase the flash over the eSPI flash + channel. Only has effect on a build whose FLASH_WRITES_ALLOWED generic is + set; elsewhere writes are refused regardless. A refused write or erase is + answered with an unsuccessful completion."; + } flash_write_enable[4:4] = 0; field { desc = "Set to one to reset the command FIFO. Cleared by hardware after reset."; } cmd_fifo_reset[3:3] = 0; diff --git a/hdl/ip/vhd/espi/sys_regs/espi_regs.vhd b/hdl/ip/vhd/espi/sys_regs/espi_regs.vhd index dfc21a3d..5dfb797a 100644 --- a/hdl/ip/vhd/espi/sys_regs/espi_regs.vhd +++ b/hdl/ip/vhd/espi/sys_regs/espi_regs.vhd @@ -17,6 +17,12 @@ use work.axil15x32_pkg.all; use work.calc_pkg.log2ceil; entity espi_regs is + generic ( + -- An instance that only ever serves the flash channel never sees a + -- post code, so it can leave the 4k entry buffer out; reads of it + -- then return zero. + POST_CODE_BUFFER_ENABLED : boolean := true + ); port ( clk : in std_logic; reset : in std_logic; @@ -25,6 +31,8 @@ entity espi_regs is post_code : in std_logic_vector(31 downto 0); post_code_valid : in std_logic; espi_reset : in std_logic; + -- runtime half of the SAFS write permission, see espi_target_top + flash_write_enable : out std_logic; stuff_fifo : out std_logic; stuff_wds : out std_logic_vector(15 downto 0); -- read-only view of eSPI spec registers @@ -63,6 +71,9 @@ architecture rtl of espi_regs is signal pc_buf_waddr : std_logic_vector(BUFFER_ADDR_WIDTH - 1 downto 0); signal pc_buf_raddr : std_logic_vector(BUFFER_ADDR_WIDTH - 1 downto 0); signal post_code_buffer_rdata : std_logic_vector(31 downto 0); + -- The read in flight is of the buffer, so answer from its output + -- register rather than rdata + signal pc_buf_read : std_logic; begin fifo_status_reg.cmd_used_wds <= dbg_chan.wstatus.usedwds; @@ -73,7 +84,7 @@ begin last_resp_status_reg <= unpack(X"0000" & last_resp_status); live_status_reg <= unpack(X"0000" & live_espi_status); - axi_if.read_data.data <= rdata; + axi_if.read_data.data <= post_code_buffer_rdata when pc_buf_read = '1' else rdata; stuff_wds <= stuff_count.count(15 downto 0); stuff_fifo <= stuff_enable.en; @@ -135,21 +146,38 @@ begin end if; end process; - post_code_buffer: entity work.dual_clock_simple_dpr - generic map( - data_width => 32, - num_words => BUFFER_ENTRIES, - reg_output => false - ) - port map( - wclk => clk, - waddr => pc_buf_waddr, - wdata => post_code, - wren => post_code_valid, - rclk => clk, - raddr => pc_buf_raddr, - rdata => post_code_buffer_rdata - ); + -- The buffer is 128kb, which in distributed RAM was 3.6k LUTRAMs per + -- instance, enough to starve the placer on a part that also carries the + -- DIMM caches. In block RAM the read is registered, so the buffer is + -- answered one cycle after the AXI read is accepted, which is exactly + -- when rvalid rises: the read enable is the accept, so the output holds + -- for as long as the master takes to collect it. + pc_buf: if POST_CODE_BUFFER_ENABLED generate + type pc_mem_t is array (0 to BUFFER_ENTRIES - 1) of std_logic_vector(31 downto 0); + signal pc_mem : pc_mem_t; + attribute ram_style : string; + attribute ram_style of pc_mem : signal is "block"; + begin + pc_mem_write: process(clk) + begin + if rising_edge(clk) then + if post_code_valid then + pc_mem(to_integer(pc_buf_waddr)) <= post_code; + end if; + end if; + end process; + + pc_mem_read: process(clk) + begin + if rising_edge(clk) then + if active_read then + post_code_buffer_rdata <= pc_mem(to_integer(pc_buf_raddr)); + end if; + end if; + end process; + else generate + post_code_buffer_rdata <= (others => '0'); + end generate; -- Axi here are byte_addresses and we need to convert to word addresses for the dpr. pc_buf_raddr <= resize(shift_right(axi_if.read_address.addr - POST_CODE_BUFFER_OFFSET, 2), pc_buf_raddr'length); @@ -161,15 +189,18 @@ begin dbg_chan.rd.rdack <= '1' when axi_if.read_data.ready = '1' and axi_if.read_data.valid = '1' and resp_fifo_ack = '1' else '0'; dbg_chan.espi_reset <= control_reg.espi_reset; + flash_write_enable <= control_reg.flash_write_enable; read_logic: process(clk, reset) begin if reset then rdata <= (others => '0'); resp_fifo_ack <= '0'; + pc_buf_read <= '0'; elsif rising_edge(clk) then resp_fifo_ack <= '0'; if active_read then + pc_buf_read <= '0'; case to_integer(axi_if.read_address.addr) is when FLAGS_OFFSET => rdata <= pack(flags_reg); when CONTROL_OFFSET => rdata <= pack(control_reg); @@ -213,7 +244,7 @@ begin when SPEC_REGS_CH3_CAPABILITIES2_OFFSET => rdata <= espi_spec_regs_pkg.pack(spec_regs_view.ch3_capabilities2); when POST_CODE_BUFFER_MEM_RANGE => - rdata <= post_code_buffer_rdata; + pc_buf_read <= '1'; when others => rdata <= (others => '0'); end case; diff --git a/hdl/ip/vhd/espi/txn_layer/command_processor.vhd b/hdl/ip/vhd/espi/txn_layer/command_processor.vhd index 282e964a..4806c662 100644 --- a/hdl/ip/vhd/espi/txn_layer/command_processor.vhd +++ b/hdl/ip/vhd/espi/txn_layer/command_processor.vhd @@ -33,6 +33,10 @@ entity command_processor is post_code : out std_logic_vector(31 downto 0); post_code_valid : out std_logic; aborted_due_to_bad_crc : out boolean; + -- Whether SAFS writes and erases may be forwarded to the flash. Ones + -- that arrive while this is low are still queued, marked refused, so + -- the host gets an unsuccessful completion for them. + flash_writes_allowed : in std_logic; -- flash channel requests flash_req : view flash_chan_req_source; -- uart channel put interface here @@ -127,14 +131,14 @@ architecture rtl of command_processor is when opcode_put_flash_np => case header.cycle_kind is when flash_write => - -- Note that while we'll rx this payload, we will not - -- act upon it, as we do not allow flash writes over eSPI + -- The payload streams into the flash channel's DPR + -- as it arrives; see flash_req.wdata below. next_state.next_state := parse_addr_header; next_state.cmd_payload_bytes := to_integer(header.length); - when flash_erase => - -- Note that while we'll rx this payload, we will not - -- act upon it, as we do not allow flash writes over eSPI when others => + -- Reads and erases are address only; for an erase + -- the length field is the block size code, not a + -- payload count. null; end case; when opcode_put_pc => @@ -174,8 +178,27 @@ begin -- pass through the flash channel requests here flash_req.espi_hdr <= r.cmd_header; flash_req.sp5_flash_address <= r.ch_addr; - flash_req.flash_np_enqueue_req <= true when r.valid_redge and r.cmd_header.opcode.value = opcode_put_flash_np and r.cmd_header.cycle_kind = flash_read else false; + flash_req.flash_np_enqueue_req <= true when r.valid_redge and r.cmd_header.opcode.value = opcode_put_flash_np and + (r.cmd_header.cycle_kind = flash_read or + r.cmd_header.cycle_kind = flash_write or + r.cmd_header.cycle_kind = flash_erase) else false; flash_req.flash_get_req <= true when r.valid_redge and r.cmd_header.opcode.value = opcode_get_flash_c else false; + -- Permission is sampled at enqueue time only, via kind. Anything that is + -- not a read and is not permitted is queued as refused. + flash_req.kind <= flash_rd when r.cmd_header.cycle_kind = flash_read else + flash_wr when r.cmd_header.cycle_kind = flash_write and flash_writes_allowed = '1' else + flash_er when r.cmd_header.cycle_kind = flash_erase and flash_writes_allowed = '1' else + flash_refused; + -- Write payload bytes go straight through as they are parsed, indexed + -- from the start of the payload so the channel can place them without + -- keeping its own count across a possibly-aborted command. + flash_req.wdata <= data_from_host.data; + flash_req.wdata_valid <= data_from_host.valid when r.cmd_header.opcode.value = opcode_put_flash_np and + r.cmd_header.cycle_kind = flash_write and + r.state = parse_data else '0'; + flash_req.wdata_idx <= To_Std_Logic_Vector(to_integer(r.cmd_header.length) - r.rem_data_bytes, flash_req.wdata_idx'length) + when r.state = parse_data and r.rem_data_bytes <= to_integer(r.cmd_header.length) else + (others => '0'); post_code <= r.io_wr_data; post_code_valid <= '1' when r.cmd_header.opcode.value = opcode_put_iowr_short_4byte and (r.crc_good or (r.crc_bad and (not regs_if.enforce_crcs))) else '0'; diff --git a/hdl/ip/vhd/espi/txn_layer/espi_protocol_pkg.vhd b/hdl/ip/vhd/espi/txn_layer/espi_protocol_pkg.vhd index ad2fa745..44277404 100644 --- a/hdl/ip/vhd/espi/txn_layer/espi_protocol_pkg.vhd +++ b/hdl/ip/vhd/espi/txn_layer/espi_protocol_pkg.vhd @@ -44,9 +44,19 @@ package espi_protocol_pkg is constant flash_read : std_logic_vector(7 downto 0) := "00000000"; constant success_no_data : std_logic_vector(7 downto 0) := "00000110"; - -- We won't actually write/erase, our behavior catching this is TBD constant flash_write : std_logic_vector(7 downto 0) := "00000001"; constant flash_erase : std_logic_vector(7 downto 0) := "00000010"; + -- Completion cycle types. The "with data" family is 0000_1xx1 with the + -- middle bits as first/last flags; "without data" is 0000_0110 for success + -- and 0000_1x00 for failure. A single unsplit response is the "only" form. + -- Values per the eSPI base specification's completion cycle-type table. + constant success_with_data_only : std_logic_vector(7 downto 0) := "00001111"; + constant unsuccessful_no_data_only : std_logic_vector(7 downto 0) := "00001100"; + -- SAFS flash erase: the length field of the request carries the erase + -- block size as a code rather than a byte count. + constant flash_erase_4k : std_logic_vector(11 downto 0) := x"001"; + constant flash_erase_32k : std_logic_vector(11 downto 0) := x"002"; + constant flash_erase_64k : std_logic_vector(11 downto 0) := x"003"; -- Header Indices for general eSPI packages constant cycle_type_idx : integer := 0; diff --git a/hdl/ip/vhd/espi/txn_layer/response_processor.vhd b/hdl/ip/vhd/espi/txn_layer/response_processor.vhd index 8c2ad072..79379a01 100644 --- a/hdl/ip/vhd/espi/txn_layer/response_processor.vhd +++ b/hdl/ip/vhd/espi/txn_layer/response_processor.vhd @@ -222,7 +222,15 @@ begin if data_to_host.ready then v.resp_idx := r.resp_idx + 1; if r.resp_idx = 2 then - v.state := RESPONSE_PAYLOAD; + -- A write or erase completes with no payload at all; + -- counting one down from a zero length would run the + -- payload stage for 4096 bytes. + if r.temp_length = 0 then + v.state := STATUS; + v.status := live_status; + else + v.state := RESPONSE_PAYLOAD; + end if; end if; end if; when RESPONSE_UART_HEADER => diff --git a/hdl/ip/vhd/espi/txn_layer/txn_layer_top.vhd b/hdl/ip/vhd/espi/txn_layer/txn_layer_top.vhd index b7d979dd..91207341 100644 --- a/hdl/ip/vhd/espi/txn_layer/txn_layer_top.vhd +++ b/hdl/ip/vhd/espi/txn_layer/txn_layer_top.vhd @@ -30,6 +30,7 @@ entity txn_layer_top is flash_np_free : in std_logic; flash_c_avail : in std_logic; -- flash channel requests/responses + flash_writes_allowed : in std_logic; flash_req : view flash_chan_req_source; flash_resp : view flash_chan_resp_sink; -- uart channel requests/responses @@ -128,6 +129,7 @@ begin espi_reset => espi_reset, regs_if => regs_if, vwire_if => vwire_if, + flash_writes_allowed => flash_writes_allowed, flash_req => flash_req, host_to_sp_espi => host_to_sp_espi, running_crc => rx_running_crc, diff --git a/hdl/ip/vhd/fmc_if/docs/fmc_if.adoc b/hdl/ip/vhd/fmc_if/docs/fmc_if.adoc index fd1a1f2c..a195778d 100644 --- a/hdl/ip/vhd/fmc_if/docs/fmc_if.adoc +++ b/hdl/ip/vhd/fmc_if/docs/fmc_if.adoc @@ -2,8 +2,8 @@ :toc: left :numbered: :icons: font -:revision: 1.0 -:revdate: 2024-04-24 +:revision: 2.0 +:revdate: 2026-09-10 = FMC Target interface @@ -37,6 +37,83 @@ The AXI state machine monitors the transaction FIFO (and the WDATA FIFO in the case of writes) and wiggles the AXI signals appropriately to issue the read/write transactions. +=== Pacing contract + +The wait line is held asserted by default and released under FSM control. +After the SP samples the wait line released at rising edge N, its current +data beat is valid on the bus at edge N+1, and each further release-sampled +edge advances one beat; on reads the SP samples data on the same edge it +samples the released wait. This cadence was proven on hardware; the FSM +and the simulation model both encode it. + +All FMC inputs pass through dedicated per-pin capture registers (clocked by +fmc_capture_clk, a later-phased MMCM sibling on boards that have one), so +the FSM runs one cycle behind the bus; NWAIT pacing absorbs the delay. +A 32-bit write occupies 6 fmc_clk cycles of bus time (address capture, +dispatch, two arm cycles, two back-to-back beats); the AXI write is posted +and completes behind the SP's back. A 32-bit read stalls through the +CDC/AXI round trip and then streams its two beats on consecutive cycles. +The `extra_beat_setup` generic re-inserts a dead cycle between read beats +for a board that cannot close single-cycle data-out timing (the constraints +must then carry matching setup-2 multicycle exceptions). + +=== Clocking, or how 100 MHz timing closes + +The wait line is sampled by the SP on every rising edge, so it is a +single-cycle output path with no multicycle tricks available: the FPGA has +period - tsu(NWAIT) - trace, about 6 ns at 10 ns, to get from its clock pin +through a flop and back out. A plain IBUF+BUFG spends 3.5-6.5 ns of that on +clock insertion alone, so at 100 MHz the internal FMC clock instead comes +from an MMCM in phase-alignment mode (BUFG in the feedback path), which +nulls the insertion delay and leaves the whole budget for the flop and +output buffer. The output/enable/nwait flops are additionally packed into +the IOBs, which is why data_out_hiz is one registered bit per pin in OBUFT +T polarity. + +Inputs get the opposite treatment. The SP launches on its falling edge, so +data arrives late in the cycle and the valid window at the pin is only a +few nanoseconds wide; worse, STA charges the full corner spread of the MMCM +clock network against a virtual clock with no pessimism removal. Rather +than fight that, the MMCM produces a second output for the capture +registers, phased later than the FSM clock (+135 deg vs +45 deg, see +fmc_pll_ip.tcl), which parks the sampling instant comfortably inside the +data-valid window. The capture registers are the only logic on that clock; +the FSM consumes their outputs on its own clock a cycle later, which is +where the "FSM runs one cycle behind the bus" above comes from. The +45 deg +FSM/output phase is itself pinned by output hold on the NWAIT/data pins +(earlier violates hold), so the two phases are not adjustable +independently of the analysis in cosmo_timing.xdc. + +Both phases are specified in degrees, and the MMCM implements phase as +VCO-referenced taps, so they are true fractions of the period: at 50 MHz +the same bitstream places every edge proportionally later and every margin +only grows. The constraints are therefore written once at 10 ns as a +strict superset of all slower rates. The MMCM's VCO multiplier (M=12) is +chosen so the VCO stays in range from a 50 MHz input through a 100 MHz +one, letting one bitstream lock at any SP CLKDIV setting; a clock-stop +monitor in the fabric holds the MMCM in reset while the SP's clock is +absent and relocks it on return. A board without a clock-capable pin for +the FMC clock (grapefruit) skips the MMCM, ties fmc_capture_clk to +fmc_clk, and is bounded by the plain-BUFG budget to 66.67 MHz. + +=== Robustness + +* NWAIT timeout: a transaction that would stall the SP longer than + `timeout_cycles` (a wedged AXI responder, full FIFOs that never drain, or + a transaction start the FSM never observed -- chip select pinned with no + address latch seen, e.g. a miscaptured NADV) is abandoned: reads return + 0xFFFF poison beats, writes are swallowed, and the sticky `timeout_count` + output increments. Data from a timed-out read that later arrives is + discarded before any live read is served. Without the missed-start case + the SP would hang on its bus with nothing to free it, which from software + looks like a silent death followed by a watchdog reboot. +* Contention tripwire: the tristate control is re-derived from the live NOE/NE + pins every cycle it could drive, and `contention_count` increments if the + enable is ever up while the SP owns the bus (NADV or NWE low). +* A mid-transaction chip-select abort cleans up within a few cycles; a write + whose transaction was already queued is completed with zero filler so the + AXI side never wedges waiting on write data. + == Sim Env The simulation environment has a model of the STM32's FMC peripheral with some @@ -50,18 +127,19 @@ testbench to properly interop with our AXI-Lite controller interface. == Current status -This has been simulated against FMC models for single 32-bit read/writes. No -bursting is supported currently. Only 32bit access with no consecutive access is -implemented. - +Simulated against the FMC model for single 32-bit read/writes at 50, 66.67, +and 100 MHz fmc_clk against a 125 MHz AXI clock, including back-to-back +traffic, FIFO-full backpressure, slow and wedged AXI responders, timeout +recovery, mid-transaction aborts, reset between transactions, and a +randomized soak (`buck2 run //hdl/ip/vhd/fmc_if:fmc_tb`). No bursting beyond +the two beats of a 32-bit access is supported; that is a deliberate scope +decision. The model implements the WAITCFG=1/DATLAT=0 pacing contract, +blocking writes via wait_until_idle, the ES0491 dummy read cycles, and +one-shot abort injection. == Open Actions -* Support multi-word transactions on FMC interface - -* Support byte-enables FMC - AXI for <32bit access - -* Clean up FMC model, especially around writes and figuring out blocking +* Support byte-enables FMC - AXI for <32bit access == Future Investigation/Next steps diff --git a/hdl/ip/vhd/fmc_if/docs/fmc_verification_plan.adoc b/hdl/ip/vhd/fmc_if/docs/fmc_verification_plan.adoc new file mode 100644 index 00000000..69dca327 --- /dev/null +++ b/hdl/ip/vhd/fmc_if/docs/fmc_verification_plan.adoc @@ -0,0 +1,203 @@ +:showtitle: +:toc: left +:numbered: +:icons: font +:revdate: 2026-09-10 + += FMC speed-up verification plan + +Verification for the FMC line-rate work: streaming FSM + input capture stage, +cosmo at 100 MHz behind the fmc_pll MMCM, grapefruit at 66.67 MHz. Each phase +gates the next; a failure stops the progression at a known-good configuration. +Companion pieces: `fmc_if.adoc` (design + pacing contract), +`tools/fmc_sweep/fmc_sweep.py` (the sweep driver), and the hubris revs +(demo-server fixes, then the gated CLKDIV flip). + +Throughout: FMC_CLK divisor N means FMC_CLK = 200 MHz / N, so divisor 4 = +50 MHz (shipping), 3 = 66.67 MHz, 2 = 100 MHz. + +== Phase 0: desk checks (no hardware) + +. Simulation regression: `buck2 run //hdl/ip/vhd/fmc_if:fmc_tb` -- all 19 + tests, which cover single and back-to-back reads/writes, varied addresses, + FIFO backpressure, slow and wedged AXI responders, NWAIT timeout recovery + (including a missed transaction start), mid-transaction aborts, reset + between transactions, and randomized soaks, exercised at 50/66.67/100 MHz + fmc_clk against the 125 MHz AXI clock. + The harness fails any test on bus contention (model and DUT driving in the + same phase). +. Timing closure: build both bitstreams and read the post-route summaries -- + the build does NOT fail on timing, the reports are the authority: ++ +---- +buck2 build //hdl/projects/cosmo_seq:cosmo_seq //hdl/projects/grapefruit:grapefruit +grep -A6 "Design Timing Summary" buck-out/.../cosmo_seq_route_timing.rpt +grep -A6 "Design Timing Summary" buck-out/.../grapefruit_route_timing.rpt +---- ++ +Required: WNS and WHS both positive on both boards, zero failing endpoints, +and the Inter Clock Table must show nonzero endpoint counts for every FMC +class (fmc_virt_clk -> capture clock, capture -> FSM clock, FSM clock -> +fmc_virt_clk). A class with zero endpoints means a constraint silently +stopped matching (renamed cell or clock) and the "pass" is vacuous. +Margins land near +0.1 setup / +0.02 hold on cosmo at 10 ns and somewhat +looser on grapefruit at 15 ns, moving a few tens of picoseconds per rebuild; +cosmo's output hold is the phase-limiting margin and the one to watch. +. Datasheet re-verification (once, on paper): confirm against DS12110 that + the XDC's SP timing numbers (td(CLKL-NExL)=1, td(CLKL-AV)=2.5, + td(CLKL-ADV)=3, tsu(NWAIT-CLKH)=3, th(CLKH-NWAIT)=2) match the synchronous + multiplexed PSRAM tables at the relevant conditions, and that 100 MHz + FMC_CLK is within spec at VOS1. The constraints inherited these values + from the 66.67 MHz era and have not been re-read against rev-current + silicon docs. +. hubris demo-server rev: land before any sweep. Runtime timing control + does not exist without it -- the task previously lacked the FMC controller + register region and faulted on every setter call (reported by hiffy as + success), and the setters themselves clobbered sibling BTR1 fields via + `btr1.write()`. It also adds the BTR1/BCR1 readback ops the sweep uses to + verify each divisor change and the block-transfer ops behind the phase-E + line-rate numbers. + +== Phase 1: grapefruit at 50 MHz (new RTL, old frequency) + +Goal: prove the streamlined FSM + capture stage is a drop-in at the +shipping frequency, before any clock change. The FPGA is NWAIT-paced, so no +behavioral difference is expected. + +. Build the archive with the new bitstream + (`buck2 run //tools/fpga_releaser:cli -- --fpga grapefruit --hubris ` + or the usual release flow) with hubris still at CLKDIV=3: the + demo-server rev included, the CLKDIV flip not included. +. Flash, confirm hubris boots and the FPGA loads (spartan7-loader token + checks pass, grapefruit_seq comes up). +. Baseline functional pass: ++ +---- +./tools/fmc_sweep/fmc_sweep.py --ip --interface \ + --archive --json gf_baseline.json +---- ++ +This runs phases A-E at divisor 4 only: constant-register reads with gaps, +scratch write/readback (info scratchpad at +0x10; fpga_checksum at +0xC is +off-limits -- hubris uses it to decide whether to reprogram the FPGA), +back-to-back write bursts into the scratchpad verified by ordering/last +value, double-pass read sweeps of the post-code buffer (an external capture +mem: readable, but AXI writes to it are dropped, so bulk +write-verification is the simulation suite's job, not the bench's), a +500-op seeded soak, and a throughput measurement. All must pass. +. Record the phase-E numbers as the throughput baseline. +. Leave a longer soak running with `--soak-seconds 3600` (the soak is + otherwise op-counted, and 500 UDP round trips finish in seconds). + Acceptance: zero mismatches, no UDP timeouts. + +== Phase 2: grapefruit sweep to 66.67 MHz + +. `--sweep 4,3` first. Phase ordering inside each step is deliberate: + constant reads come first because the SP never drives the bus during read + data phases, so a timing miss at a new frequency shows up as wrong data, + never as a driver fight. Do not reorder. +. If divisor 3 passes: rerun with `--sweep 4,3,2`. Divisor 2 (100 MHz) is + expected to FAIL on grapefruit -- its clock enters on non-clock-capable + F17 with ~6 ns of detour insertion delay and no MMCM. The point is to + record where the cliff is and confirm the tool's failure handling: + it must stop at the first failing phase, restore divisor 4, and re-verify + phase A (`restored_baseline_ok: true` in the JSON). +. Acceptance for 66.67 MHz: full A-E pass plus a >= 1 hour soak + (`--soak-seconds 3600`) at divisor 3, + and phase-E throughput ~1.33x the baseline batched numbers. + +== Phase 3: cosmo at 50 MHz (MMCM bitstream, old frequency) + +Goal: prove the MMCM clocking infrastructure -- dual-frequency lock, the +clock-stop monitor, reset gating -- before touching the frequency. + +. Flash the new archive (CLKDIV still 3). First FMC access happening at all + proves the MMCM locked at a 50 MHz input (the VCO sits at its 600 MHz + minimum here -- this IS the risky corner of the dual-frequency config, so + a boot-loop or dead FMC points straight at lock failure; see the rollback + note below). +. Run the full fmc_sweep at divisor 4; record baseline JSON. +. Regenerate the ILA against the new netlist (the old fmc_ila.tcl net names + are stale: the clock is now `clk_fmc_fmc_pll`, the enable regs are + `data_out_hiz_int_reg[*]`, and `mark_debug` already tags fmc_state, + axi_state, txn, timeouts, contentions). Probe at minimum: fmc_state, + NWAIT, CS/ADV/WE/OE IBUFs, DA in/out, timeouts, contentions, and the + monitor's mmcm_reset/locked. +. With the ILA, verify on live traffic: + * NWAIT cadence matches the model: one release-sample per beat, two + consecutive released samples per 32-bit access, wait re-asserted between + transactions. + * ES0491 dummy read cycles appear after bursts and provoke no state + change. + * `timeouts == 0` and `contentions == 0` after every soak. Nonzero + contention is a stop-ship: it means the pacing contract disagrees with + real silicon somewhere. +. Robustness loops (the monitor/reset machinery): + * SP warm reset under active traffic, >= 50 iterations scripted. Each + time: FMC clock stops, monitor must hold the MMCM in reset, relock on + clock return, and peek/poke must work immediately after the SP is back. + No FPGA reprogram in this loop -- the bitstream rides through. + * FPGA reprogram (SP-driven, SP idle on FMC), >= 10 iterations, traffic + resumes cleanly each time. + * Full power cycle x5. + * Acceptance: zero hangs (an SP stuck in an FMC access = the timeout + machinery or reset gating failed), counters clean afterward. + +== Phase 4: cosmo sweep to 100 MHz + +. `--sweep 4,3,2` with the lab-image demo-server. All phases must pass at + every step; there is no acceptable-failure divisor on cosmo. +. One-hour soak (`--soak-seconds 3600`) at divisor 2, counters zero. +. MMCM phase margin sweep: rebuild bitstream variants with the phases + moved +/-18.75 deg = 5 MMCM taps = ~0.52 ns (the phase grid is + 45/CLKOUT_DIVIDE = 3.75 deg per tap; edit `fmc_pll_ip.tcl`: FSM/output + phase 26.25/45/63.75, capture phase 116.25/135/153.75 -- move them + together first, then independently if a failure needs isolating). Run + the divisor-2 soak on each variant. Acceptance: the shipped phases pass + with both neighbors also passing, demonstrating >= ~0.5 ns of real + margin beyond STA. If a neighbor fails, the shipped point is marginal: + investigate before un-gating, don't ship on one passing point. +. Re-verify the SP warm-reset loop at divisor 2 (relock now returns to a + 1200 MHz VCO -- the other edge of the range). + +== Phase 5: end-to-end throughput + +. fmc_sweep phase E reports two families of numbers. `fmc_*` values come + from delta-timed server-side block ops (demo-server network ops 17-19: + checksum block reads and a fixed-address fill), where the round trip and + per-packet costs cancel and what remains is bus accesses -- these are the + line-rate measurements and must scale with the divisor (~1.33x going + 4 -> 3, ~2x going 4 -> 2). `wire_*` values come from plain batched + peek/poke: ~9 network bytes per word swamps the ~0.4 us bus + access, so they are expected NOT to move with the divisor -- they are a + network/health reference, not a bus measurement. An `fmc_*` number that + fails to scale with the divisor means the divisor did not actually + change (or the block ops hit an error, which the checksum cross-checks + catch). +. Timed cosmo-hf host-flash operations (the hot path: one status poll + one + data read per u32) before/after -- same image content, hubris timestamps + or a host-side stopwatch around a full read-back. Expected ~1.6-1.8x at + 100 MHz; writes limited by flash program time, so use read-back as the + metric. +. The phase-E `fmc_*` table across divisors goes in the PR description. + +== Phase 6: un-gate the hubris CLKDIV rev + +Per board, only after that board's phases pass: land the hubris rev that +raises the boot-time divider (cosmo CLKDIV=1, grapefruit CLKDIV=2) in the +same archive as its validated bitstream -- the archive carries both, so +image and clock setting move atomically and no mixed-field state exists. + +Rollback at any point is the one-line CLKDIV revert: every bitstream in this +series is NWAIT-paced and runs at 50 MHz unchanged. The single caveat is a +cosmo MMCM that fails to lock at one input frequency (dual-VCO corner): that +is a bitstream-level rollback, which is why Phase 3 proves the 50 MHz lock +corner before anything else depends on it. + +== Open observability item + +`timeout_count` / `contention_count` are ports (left open at the tops) and +mark_debug signals. Wiring them into a spare info/debug register would make +the Phase 3/4 counter checks possible from software instead of the ILA and +would let fmc_sweep gate every step on them; worth doing before the Phase 4 +soaks. diff --git a/hdl/ip/vhd/fmc_if/model/stm32h7_fmc_model.vhd b/hdl/ip/vhd/fmc_if/model/stm32h7_fmc_model.vhd index ad1f166b..62cc83c1 100644 --- a/hdl/ip/vhd/fmc_if/model/stm32h7_fmc_model.vhd +++ b/hdl/ip/vhd/fmc_if/model/stm32h7_fmc_model.vhd @@ -2,11 +2,29 @@ -- License, v. 2.0. If a copy of the MPL was not distributed with this -- file, You can obtain one at https://mozilla.org/MPL/2.0/. ---! FMC controller model based on ST's RM0433 rev8 ---! figures 115 and 116 for simulation of the ---! FPGA's target interface ---! Oxide's internal doc mirror link: ---! https://drive.google.com/file/d/1wPaZAHS3-0HdMkXOC8tvGYgOPOrM0qRQ/view?usp=drive_link +-- FMC controller model based on ST's RM0433 rev8 +-- figures 115 and 116 for simulation of the +-- FPGA's target interface +-- Oxide's internal doc mirror link: +-- https://drive.google.com/file/d/1wPaZAHS3-0HdMkXOC8tvGYgOPOrM0qRQ/view?usp=drive_link +-- +-- Timing semantics modeled here (WAITCFG=1, DATLAT=0, the configuration +-- hubris programs): NWAIT is sampled on rising clock edges once the address +-- phase is over. For writes, each NWAIT-released rising edge advances one +-- data beat onto the bus at the following falling edge, so the beat is +-- captured by the target on the rising edge after the release was sampled. +-- For reads, data is sampled on the same rising edge where NWAIT is seen +-- released. This matches the cadence proven on hardware against the +-- pre-streaming target FSM; if hardware ILA captures ever disagree with +-- this contract, fix it here first. +-- +-- ES0491 (dummy read cycles): after every burst read the controller +-- performs two dummy read accesses with the chip still selected; the model +-- reproduces them so the target's idle-return is exercised under them. +-- +-- Also handled on the bus actor: wait_until_idle (making writes blockable), +-- and the control messages in stm32h7_fmc_model_pkg (inter-transaction gap, +-- one-shot mid-transaction abort). library ieee; use ieee.std_logic_1164.all; @@ -16,6 +34,9 @@ library vunit_lib; context vunit_lib.vunit_context; context vunit_lib.com_context; use vunit_lib.bus_master_pkg.all; +use vunit_lib.sync_pkg.all; + +use work.stm32h7_fmc_model_pkg.all; entity stm32h7_fmc_model is generic ( @@ -26,7 +47,6 @@ entity stm32h7_fmc_model is a : out std_logic_vector(address_length(bus_handle) - 1 downto 16); ad : inout std_logic_vector(data_length(bus_handle) - 1 downto 0); ne : out std_logic_vector(3 downto 0); - -- todo missing byte enables? noe : out std_logic; nwe : out std_logic; nl : out std_logic; @@ -36,18 +56,10 @@ end entity; architecture model of stm32h7_fmc_model is - type txn_type is (read_txn, write_txn); - signal delayed_wait : std_logic := '0'; + type txn_type is (read_txn, write_txn); begin - -- wait_delay : process (clk) - -- begin - -- if rising_edge(clk) then - -- delayed_wait <= not nwait; - -- end if; - -- end process; - bfm: process variable request_msg : msg_t; variable reply_msg : msg_t; @@ -56,6 +68,13 @@ begin variable addr : std_logic_vector(address_length(bus_handle) - 1 downto 0); variable data : std_logic_vector(data_length(bus_handle) - 1 downto 0); variable rem_data_cnt : integer; + variable beats_done : natural; + variable aborted : boolean; + -- extra idle cycles between transactions; 0 = back-to-back + variable gap_cycles : natural := 0; + -- one-shot: abort the next transaction after this many beats + -- (negative = disarmed) + variable abort_beats : integer := -1; procedure bus_idle is begin @@ -96,57 +115,122 @@ begin end; begin bus_idle; - nl <= '1'; - receive(net, BUS_HANDLE.p_actor, request_msg); - msg_type := message_type(request_msg); - -- All bus transactions begin with the FMC_CLK - -- low - wait until falling_edge(clk); - if msg_type = bus_burst_write_msg then - -- Figure 116 - -- activate address, chipsel, write, and latch - transaction_start(WRITE_TXN); - wait until falling_edge(clk); - wait until falling_edge(clk); - -- on next falling edge of clock, apply wdata - while rem_data_cnt > 0 loop - wait on clk; - -- on every rising edge that wait isn't asserted, - -- we've done a transfer, so get the data, dec the counter, - -- apply to bus - if falling_edge(clk) and nwait = '1' then - data := pop_std_ulogic_vector(request_msg); - rem_data_cnt := rem_data_cnt - 1; - ad <= data; + loop + receive(net, BUS_HANDLE.p_actor, request_msg); + msg_type := message_type(request_msg); + if msg_type = set_txn_gap_msg then + gap_cycles := pop_integer(request_msg); + elsif msg_type = abort_next_msg then + abort_beats := pop_integer(request_msg); + elsif msg_type = wait_until_idle_msg then + -- Messages are handled in order, so reaching this one means + -- every previously requested bus cycle has completed; this is + -- what makes fmc_write32 blockable. + handle_wait_until_idle(net, msg_type, request_msg); + elsif msg_type = bus_burst_write_msg then + -- Figure 116: all bus transactions begin with FMC_CLK low + wait until falling_edge(clk); + beats_done := 0; + aborted := false; + transaction_start(WRITE_TXN); + -- NWAIT sampling starts on the first rising edge after the + -- address phase. The abort check sits after the edge wait so + -- an already-applied beat is held through its capture edge + -- before the bus deasserts. + while rem_data_cnt > 0 loop + wait until rising_edge(clk); + if abort_beats >= 0 and beats_done = abort_beats then + aborted := true; + abort_beats := -1; + exit; + end if; + if nwait = '1' then + wait until falling_edge(clk); + data := pop_std_ulogic_vector(request_msg); + rem_data_cnt := rem_data_cnt - 1; + ad <= data; + beats_done := beats_done + 1; + end if; + end loop; + if aborted then + -- drain the un-sent beats so the message queue stays + -- consistent + while rem_data_cnt > 0 loop + data := pop_std_ulogic_vector(request_msg); + rem_data_cnt := rem_data_cnt - 1; + end loop; + bus_idle; + -- The target takes a few cycles to notice the deselect + -- (its view of the bus is one capture-register cycle + -- behind) and clean up; a real SP cannot restart within + -- one cycle of an abort either, so give it room before + -- the next transaction. + for i in 1 to 4 loop + wait until falling_edge(clk); + end loop; + else + -- hold the final beat through its capture edge + wait until falling_edge(clk); + bus_idle; end if; - end loop; - wait until falling_edge(clk); - elsif msg_type = bus_burst_read_msg then - reply_msg := new_msg; - -- Figure 115 - -- activate address, chipsel, and latch - transaction_start(READ_TXN); - push_integer(reply_msg, rem_data_cnt); - -- on next falling edge of clock, data could be on the bus - wait until falling_edge(clk); - wait until rising_edge(clk); - while rem_data_cnt > 0 loop - wait on clk; - if rising_edge(clk) and nwait = '1' then - -- sample data, dec remaining data - push_std_ulogic_vector(reply_msg, ad); - rem_data_cnt := rem_data_cnt - 1; + elsif msg_type = bus_burst_read_msg then + wait until falling_edge(clk); + beats_done := 0; + aborted := false; + reply_msg := new_msg; + -- Figure 115 + transaction_start(READ_TXN); + push_integer(reply_msg, rem_data_cnt); + -- data cannot be valid before the edge after the address + -- phase completes + wait until falling_edge(clk); + while rem_data_cnt > 0 loop + wait until rising_edge(clk); + if abort_beats >= 0 and beats_done = abort_beats then + aborted := true; + abort_beats := -1; + exit; + end if; + if nwait = '1' then + -- sample data on the same edge the released wait is + -- sampled + push_std_ulogic_vector(reply_msg, ad); + rem_data_cnt := rem_data_cnt - 1; + beats_done := beats_done + 1; + end if; + end loop; + if aborted then + -- fill the reply so burst_read_bus completes; the values + -- are meaningless by construction (note: must carry a + -- downto range to match the reader's slice) + data := (others => '0'); + while rem_data_cnt > 0 loop + push_std_ulogic_vector(reply_msg, data); + rem_data_cnt := rem_data_cnt - 1; + end loop; + else + -- ES0491: two dummy read cycles, chip still selected + wait until rising_edge(clk); + wait until rising_edge(clk); end if; + reply(net, request_msg, reply_msg); + wait until falling_edge(clk); + bus_idle; + if aborted then + -- as for writes: let the target finish its abort cleanup + for i in 1 to 4 loop + wait until falling_edge(clk); + end loop; + end if; + else + -- This shouldn't happen but will provide + -- proper error reporting if it does + unexpected_msg_type(msg_type); + end if; + for i in 1 to gap_cycles loop + wait until falling_edge(clk); end loop; - -- data out - -- tbd waits - reply(net, request_msg, reply_msg); - wait on clk; - else - -- This shouldn't happen but will provide - -- proper error reporting if it does - unexpected_msg_type(msg_type); - end if; + end loop; end process; end model; diff --git a/hdl/ip/vhd/fmc_if/model/stm32h7_fmc_model_pkg.vhd b/hdl/ip/vhd/fmc_if/model/stm32h7_fmc_model_pkg.vhd new file mode 100644 index 00000000..8710d6a6 --- /dev/null +++ b/hdl/ip/vhd/fmc_if/model/stm32h7_fmc_model_pkg.vhd @@ -0,0 +1,65 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +-- Control messages for the STM32H7 FMC controller model. These ride the +-- same actor as the VUnit bus-master traffic so they stay ordered with the +-- transactions they configure. + +library ieee; +use ieee.std_logic_1164.all; + +library vunit_lib; + context vunit_lib.vunit_context; + context vunit_lib.com_context; +use vunit_lib.bus_master_pkg.all; + +package stm32h7_fmc_model_pkg is + + constant set_txn_gap_msg : msg_type_t := new_msg_type("fmc_set_txn_gap"); + constant abort_next_msg : msg_type_t := new_msg_type("fmc_abort_next"); + + -- Insert this many extra idle fmc_clk cycles between subsequent bus + -- transactions (0 = back-to-back, the default). + procedure fmc_set_txn_gap ( + signal net : inout network_t; + constant bus_handle : bus_master_t; + constant cycles : natural + ); + + -- Arm a one-shot mid-transaction abort: the next transaction deasserts + -- chip select after `after_beats` data beats have transferred + -- (0 = abort right after the address phase). + procedure fmc_abort_next ( + signal net : inout network_t; + constant bus_handle : bus_master_t; + constant after_beats : natural + ); + +end package; + +package body stm32h7_fmc_model_pkg is + + procedure fmc_set_txn_gap ( + signal net : inout network_t; + constant bus_handle : bus_master_t; + constant cycles : natural + ) is + variable request_msg : msg_t := new_msg(set_txn_gap_msg); + begin + push_integer(request_msg, cycles); + send(net, bus_handle.p_actor, request_msg); + end; + + procedure fmc_abort_next ( + signal net : inout network_t; + constant bus_handle : bus_master_t; + constant after_beats : natural + ) is + variable request_msg : msg_t := new_msg(abort_next_msg); + begin + push_integer(request_msg, after_beats); + send(net, bus_handle.p_actor, request_msg); + end; + +end package body; diff --git a/hdl/ip/vhd/fmc_if/model/stm32h7_fmc_sim_pkg.vhd b/hdl/ip/vhd/fmc_if/model/stm32h7_fmc_sim_pkg.vhd index 1b7a6f63..068566f7 100644 --- a/hdl/ip/vhd/fmc_if/model/stm32h7_fmc_sim_pkg.vhd +++ b/hdl/ip/vhd/fmc_if/model/stm32h7_fmc_sim_pkg.vhd @@ -2,8 +2,7 @@ -- License, v. 2.0. If a copy of the MPL was not distributed with this -- file, You can obtain one at https://mozilla.org/MPL/2.0/. ---! Bus master model based on ST's RM0433 ---! figures 115 and 116 +-- Testbench-facing helpers for driving the STM32H7 FMC controller model. library ieee; use ieee.std_logic_1164.all; @@ -13,6 +12,7 @@ library vunit_lib; context vunit_lib.vunit_context; context vunit_lib.com_context; use vunit_lib.bus_master_pkg.all; +use vunit_lib.sync_pkg.all; package stm32h7_fmc_sim_pkg is @@ -24,12 +24,28 @@ package stm32h7_fmc_sim_pkg is variable data : inout std_logic_vector ); + -- Blocks until the bus cycle has completed on the FMC pins. Note the + -- posted write may still be crossing into the AXI domain when this + -- returns; a subsequent fmc_read32 orders behind it, or wait for the + -- CDC/AXI latency before checking memory directly. procedure fmc_write32 ( signal net : inout network_t; constant address : std_logic_vector; variable data : inout std_logic_vector ); + -- Fire-and-forget variant, for queuing back-to-back traffic. + procedure fmc_write32_nb ( + signal net : inout network_t; + constant address : std_logic_vector; + variable data : inout std_logic_vector + ); + + -- Blocks until every previously queued transaction's bus cycle is done. + procedure fmc_wait_idle ( + signal net : inout network_t + ); + end package; package body stm32h7_fmc_sim_pkg is @@ -41,32 +57,46 @@ package body stm32h7_fmc_sim_pkg is ) is variable queue : queue_t; - constant butst_length : integer := 2; + constant burst_length : integer := 2; begin queue := new_queue; - burst_read_bus(net, SP_BUS_HANDLE, address, BUTST_LENGTH, queue); + burst_read_bus(net, SP_BUS_HANDLE, address, BURST_LENGTH, queue); data(15 downto 0) := pop_std_ulogic_vector(queue); data(31 downto 16) := pop_std_ulogic_vector(queue); end; - procedure fmc_write32 ( + procedure fmc_write32_nb ( signal net : inout network_t; constant address : std_logic_vector; variable data : inout std_logic_vector ) is variable queue : queue_t; - constant butst_length : integer := 2; + constant burst_length : integer := 2; begin queue := new_queue; push_std_ulogic_vector(queue, data(15 downto 0)); push_std_ulogic_vector(queue, data(31 downto 16)); - burst_write_bus(net, SP_BUS_HANDLE, address, BUTST_LENGTH, queue); - -- A bit of a hack until I figure out how to make this blocking - -- See https://github.com/VUnit/vunit/issues/1012 - wait for 150 ns; + burst_write_bus(net, SP_BUS_HANDLE, address, BURST_LENGTH, queue); + end; + + procedure fmc_write32 ( + signal net : inout network_t; + constant address : std_logic_vector; + variable data : inout std_logic_vector + ) is + begin + fmc_write32_nb(net, address, data); + fmc_wait_idle(net); + end; + + procedure fmc_wait_idle ( + signal net : inout network_t + ) is + begin + wait_until_idle(net, SP_BUS_HANDLE.p_actor); end; end package body; diff --git a/hdl/ip/vhd/fmc_if/sims/fmc_tb.vhd b/hdl/ip/vhd/fmc_if/sims/fmc_tb.vhd index 53758baa..2120d410 100644 --- a/hdl/ip/vhd/fmc_if/sims/fmc_tb.vhd +++ b/hdl/ip/vhd/fmc_if/sims/fmc_tb.vhd @@ -12,6 +12,7 @@ library vunit_lib; context vunit_lib.vunit_context; context vunit_lib.vc_context; use work.stm32h7_fmc_sim_pkg.all; +use work.stm32h7_fmc_model_pkg.all; use work.fmc_tb_pkg.all; entity fmc_tb is @@ -30,15 +31,101 @@ begin bench: process -- Note: External names are broken in GHDL llvm backends https://github.com/ghdl/ghdl/issues/2610 -- So this sim only works in other simulators, like nvc - -- reset_a uses the absolute path form (starting with a '.') and - -- reset_b uses the relative path form of external naming for example purposes. alias reset is << signal th.reset : std_logic >>; + alias fmc_half_period is << signal th.fmc_half_period : time >>; + alias timeout_count is << signal th.timeout_count : std_logic_vector(7 downto 0) >>; + alias ne_pins is << signal th.ne : std_logic_vector(3 downto 0) >>; + alias nwait_pin is << signal th.nwait : std_logic >>; + alias contention_count is << signal th.contention_count : std_logic_vector(7 downto 0) >>; variable address : std_logic_vector(25 downto 0) := (others => '0'); variable data : std_logic_vector(31 downto 0) := (others => '0'); variable expected_data : std_logic_vector(31 downto 0) := (others => '0'); variable buf : buffer_t; variable buf2 : buffer_t; + variable rand_state : unsigned(31 downto 0) := x"1234_5678"; + variable addr_nat : natural; + + -- deterministic LCG so the soak needs no external randomization + -- packages and reproduces exactly + impure function rand32 return unsigned is + begin + rand_state := resize(rand_state * 1664525, 32) + 1013904223; + return rand_state; + end; + + -- backing pattern for pre-filled read memory + function rd_pattern ( + addr : natural + ) return std_logic_vector is + begin + return std_logic_vector(resize(to_unsigned(addr, 32) * 1664525, 32) xor x"A5A5_A5A5"); + end; + + function to_addr ( + addr : natural + ) return std_logic_vector is + begin + return std_logic_vector(to_unsigned(addr, 26)); + end; + + procedure basic_write_read_pair is + begin + buf := allocate(wmemory, 4096); + buf2 := allocate(rmemory, 4096); + data := X"DEAD_BEEF"; + set_expected_word(wmemory, 16#40#, data); + fmc_write32(net, to_addr(16#40#), data); + expected_data := X"CAFE_F00D"; + write_word(rmemory, 16#80#, expected_data); + fmc_read32(net, to_addr(16#80#), data); + check_equal(data, expected_data, "Read data did not match expected"); + wait for 2 us; -- let the posted write land before checking memory + check_expected_was_written(buf); + end; + + procedure soak ( + constant num_ops : natural + ) is + constant rd_words : natural := 1024; + constant wr_base : natural := 16#10000#; + variable wr_index : natural := 0; + variable do_read : boolean; + variable r : unsigned(31 downto 0); + begin + buf := allocate(wmemory, 16#20000#); + buf2 := allocate(rmemory, 4 * rd_words); + for i in 0 to rd_words - 1 loop + write_word(rmemory, i * 4, rd_pattern(i * 4)); + end loop; + for i in 1 to num_ops loop + -- occasionally change the inter-transaction gap + r := rand32; + if r(2 downto 0) = "000" then + fmc_set_txn_gap(net, sp_bus_handle, to_integer(r(5 downto 3))); + end if; + r := rand32; + do_read := r(0) = '1'; + if do_read then + addr_nat := to_integer(r(11 downto 2)) * 4; + fmc_read32(net, to_addr(addr_nat), data); + check_equal(data, rd_pattern(addr_nat), + "soak read mismatch at addr " & to_string(addr_nat)); + else + -- distinct write addresses so in-flight writes can never + -- race a later expectation on the same word + addr_nat := wr_base + wr_index * 4; + wr_index := wr_index + 1; + data := std_logic_vector(rand32); + set_expected_word(wmemory, addr_nat, data); + fmc_write32_nb(net, to_addr(addr_nat), data); + end if; + end loop; + fmc_set_txn_gap(net, sp_bus_handle, 0); + fmc_wait_idle(net); + wait for 2 us; + check_expected_was_written(buf); + end; begin -- Always the first thing in the process, set up things for the VUnit test runner test_runner_setup(runner, runner_cfg); @@ -59,11 +146,13 @@ begin set_expected_word(wmemory, to_integer(address), data); -- Do the FMC -> AXI write transaction fmc_write32(net, address, data); + wait for 2 us; -- posted write crosses the CDC behind us check_expected_was_written(buf); -- Do a second transaction data := X"ADEADBAD"; set_expected_word(wmemory, to_integer(address), data); fmc_write32(net, address, data); + wait for 2 us; check_expected_was_written(buf); elsif run("basic_fmc_read_test") then buf := allocate(rmemory, 4 * 2, alignment => 32); @@ -78,11 +167,11 @@ begin -- Now do the FMC transaction, and check that returned data matches fmc_read32(net, address, data); check_equal(data, expected_data, "Read data did not match exptected"); - -- -- Do a second transaction - -- expected_data := X"ADEADBAD"; - -- write_word(rmemory, base_address(buf), expected_data); - -- fmc_read32(net, address, data); - -- check_equal(data, expected_data, "Read data did not match exptected"); + -- Do a second transaction back-to-back + expected_data := X"ADEADBAD"; + write_word(rmemory, base_address(buf), expected_data); + fmc_read32(net, address, data); + check_equal(data, expected_data, "2nd read data did not match exptected"); elsif run("basic_fmc_read_after_write") then data := X"DEADBEEF"; -- Set up the buffer used by the AXI write target @@ -94,25 +183,252 @@ begin set_expected_word(wmemory, to_integer(address), data); -- Do the FMC -> AXI write transaction fmc_write32(net, address, data); - check_expected_was_written(buf); + -- the posted write must land before its expectation is + -- replaced with the second value + wait for 2 us; -- Do a second transaction expected_data := X"ADEADBAD"; set_expected_word(wmemory, to_integer(address), expected_data); fmc_write32(net, address, expected_data); - check_expected_was_written(buf); write_word(rmemory, base_address(buf2), expected_data); set_permissions(rmemory, base_address(buf2), read_only); + -- The read orders behind both writes in the transaction FIFO, + -- so it also acts as the write-completion flush fmc_read32(net, address, data); check_equal(data, expected_data, "Read data did not match exptected"); + check_expected_was_written(buf); + elsif run("varied_address_write_read") then + -- walk a one over the address bits; 512 KB keeps the VUnit + -- memory model inside nvc's heap while still covering the + -- non-muxed a(18:16) upper-address path + buf := allocate(wmemory, 2 ** 19); + buf2 := allocate(rmemory, 2 ** 19); + for bit in 2 to 18 loop + addr_nat := 2 ** bit; + data := rd_pattern(addr_nat); + set_expected_word(wmemory, addr_nat, data); + fmc_write32_nb(net, to_addr(addr_nat), data); + write_word(rmemory, addr_nat, not data); + fmc_read32(net, to_addr(addr_nat), expected_data); + check_equal(expected_data, not data, + "read mismatch at address bit " & to_string(bit)); + end loop; + wait for 2 us; + check_expected_was_written(buf); + elsif run("back_to_back_writes") then + -- more writes than the transaction FIFO holds, with a slowed + -- AXI responder, so the dispatch-stall backpressure engages + buf := allocate(wmemory, 4096); + set_response_latency(net, axi_write_target, 500 ns); + for i in 0 to 31 loop + data := std_logic_vector(rand32); + set_expected_word(wmemory, i * 4, data); + fmc_write32_nb(net, to_addr(i * 4), data); + end loop; + fmc_wait_idle(net); + wait for 25 us; + check_expected_was_written(buf); + check_equal(unsigned(timeout_count), 0, + "backpressure must stall, not time out, at this latency"); + elsif run("back_to_back_reads") then + buf := allocate(rmemory, 4096); + for i in 0 to 31 loop + write_word(rmemory, i * 4, rd_pattern(i * 4)); + end loop; + for i in 0 to 31 loop + fmc_read32(net, to_addr(i * 4), data); + check_equal(data, rd_pattern(i * 4), + "b2b read mismatch at word " & to_string(i)); + end loop; + elsif run("slow_read_responder") then + -- long AXI read latency, but below the wait timeout: the SP + -- just stalls and then gets correct data + buf := allocate(rmemory, 64); + expected_data := X"0BAD_CAFE"; + write_word(rmemory, 16, expected_data); + set_response_latency(net, axi_read_target, 2 us); + fmc_read32(net, to_addr(16), data); + check_equal(data, expected_data, "slow read returned wrong data"); + check_equal(unsigned(timeout_count), 0, "no timeout expected"); + elsif run("read_timeout_recovery") then + -- AXI read latency far beyond the wait timeout: the SP gets + -- poison instead of a hung bus, and the stale response is + -- discarded before the next read + buf := allocate(rmemory, 64); + write_word(rmemory, 0, X"1111_2222"); + write_word(rmemory, 4, X"3333_4444"); + set_response_latency(net, axi_read_target, 50 us); + fmc_read32(net, to_addr(0), data); + check_equal(data, std_logic_vector'(X"FFFF_FFFF"), + "timed-out read must return poison"); + check_equal(unsigned(timeout_count), 1, "expected one timeout"); + -- let the stale response land and drain + wait for 60 us; + set_response_latency(net, axi_read_target, 0 ns); + fmc_read32(net, to_addr(4), data); + check_equal(data, std_logic_vector'(X"3333_4444"), + "post-timeout read must return fresh data"); + check_equal(unsigned(timeout_count), 1, "no further timeouts expected"); + elsif run("write_backpressure_timeout") then + -- wedge the write responder so the FIFOs fill and dispatch + -- stalls past the timeout: the overflowing writes are + -- swallowed rather than hanging the SP + buf := allocate(wmemory, 4096); + set_response_latency(net, axi_write_target, 20 us); + for i in 0 to 15 loop + data := std_logic_vector(rand32); + set_expected_word(wmemory, i * 4, data); + fmc_write32_nb(net, to_addr(i * 4), data); + end loop; + -- these overflow the queue; some will be swallowed by the + -- timeout, so no expectations are set on them + for i in 16 to 19 loop + data := std_logic_vector(rand32); + fmc_write32_nb(net, to_addr(i * 4), data); + end loop; + fmc_wait_idle(net); + check_true(unsigned(timeout_count) > 0, + "expected at least one swallowed write"); + set_response_latency(net, axi_write_target, 0 ns); + wait for 500 us; -- drain the wedged queue + check_expected_was_written(buf); + -- interface must still be alive + data := X"600D_600D"; + set_expected_word(wmemory, 16#100#, data); + fmc_write32(net, to_addr(16#100#), data); + wait for 2 us; + check_expected_was_written(buf); + elsif run("cs_abort_read_recovery") then + buf := allocate(rmemory, 64); + write_word(rmemory, 0, X"AAAA_BBBB"); + write_word(rmemory, 4, X"CCCC_DDDD"); + -- abort right after the address phase: the read is already in + -- flight and its data must be silently discarded + fmc_abort_next(net, sp_bus_handle, 0); + fmc_read32(net, to_addr(0), data); -- returned data is undefined + fmc_read32(net, to_addr(4), data); + check_equal(data, std_logic_vector'(X"CCCC_DDDD"), + "read after phase-0 abort must be clean"); + -- abort after one beat: the DUT finishes the word on its own + fmc_abort_next(net, sp_bus_handle, 1); + fmc_read32(net, to_addr(0), data); -- returned data is undefined + fmc_read32(net, to_addr(4), data); + check_equal(data, std_logic_vector'(X"CCCC_DDDD"), + "read after one-beat abort must be clean"); + check_equal(unsigned(timeout_count), 0, "aborts are not timeouts"); + elsif run("cs_abort_write_recovery") then + buf := allocate(wmemory, 64); + -- aborted writes complete with zero filler on the AXI side by + -- design; no expectations on them + data := X"1234_5678"; + fmc_abort_next(net, sp_bus_handle, 0); + fmc_write32(net, to_addr(0), data); + fmc_abort_next(net, sp_bus_handle, 1); + fmc_write32(net, to_addr(4), data); + -- interface must still work + data := X"8765_4321"; + set_expected_word(wmemory, 8, data); + fmc_write32(net, to_addr(8), data); + wait for 2 us; + check_expected_was_written(buf); + check_equal(unsigned(timeout_count), 0, "aborts are not timeouts"); + elsif run("reset_between_transactions") then + buf := allocate(wmemory, 64); + buf2 := allocate(rmemory, 64); + data := X"BEF0_4E00"; + set_expected_word(wmemory, 0, data); + fmc_write32(net, to_addr(0), data); + wait for 2 us; + check_expected_was_written(buf); + -- yank chip_reset between transactions and confirm the + -- interface comes back + reset <= force '1'; + wait for 300 ns; + reset <= release; + wait for 500 ns; + data := X"5EC0_4D00"; + set_expected_word(wmemory, 4, data); + fmc_write32(net, to_addr(4), data); + write_word(rmemory, 8, X"1357_2468"); + fmc_read32(net, to_addr(8), data); + check_equal(data, std_logic_vector'(X"1357_2468"), + "read after reset must work"); + wait for 2 us; + check_expected_was_written(buf); + elsif run("missed_start_timeout") then + -- Pin chip select with no address latch, emulating the FSM + -- having miscaptured a transaction start: the SP would be + -- stalled on its bus, and only the idle timeout can free it. + ne_pins <= force "1110"; + wait until nwait_pin = '1' for 30 us; + check_equal(nwait_pin, '1', + "wait must release after a missed-start timeout"); + wait for 100 ns; -- let the counter output settle + check_equal(unsigned(timeout_count), 1, "expected one timeout"); + ne_pins <= release; + wait for 2 us; + -- interface must still be alive + buf := allocate(wmemory, 64); + buf2 := allocate(rmemory, 64); + data := X"0DDB_A115"; + set_expected_word(wmemory, 0, data); + fmc_write32(net, to_addr(0), data); + write_word(rmemory, 4, X"BEA7_ED00"); + fmc_read32(net, to_addr(4), data); + check_equal(data, std_logic_vector'(X"BEA7_ED00"), + "read after missed-start recovery must work"); + wait for 2 us; + check_expected_was_written(buf); + check_equal(unsigned(timeout_count), 1, + "no further timeouts expected"); + elsif run("random_soak") then + soak(60); + elsif run("basic_write_read_66mhz") then + fmc_half_period <= force 7.5 ns; + wait for 100 ns; + basic_write_read_pair; + elsif run("basic_write_read_100mhz") then + fmc_half_period <= force 5 ns; + wait for 100 ns; + basic_write_read_pair; + elsif run("back_to_back_100mhz") then + fmc_half_period <= force 5 ns; + wait for 100 ns; + buf := allocate(wmemory, 4096); + buf2 := allocate(rmemory, 4096); + for i in 0 to 31 loop + data := std_logic_vector(rand32); + set_expected_word(wmemory, i * 4, data); + fmc_write32_nb(net, to_addr(i * 4), data); + end loop; + for i in 0 to 15 loop + write_word(rmemory, i * 4, rd_pattern(i * 4)); + fmc_read32(net, to_addr(i * 4), data); + check_equal(data, rd_pattern(i * 4), + "100mhz b2b read mismatch at word " & to_string(i)); + end loop; + fmc_wait_idle(net); + wait for 2 us; + check_expected_was_written(buf); + elsif run("random_soak_66mhz") then + fmc_half_period <= force 7.5 ns; + wait for 100 ns; + soak(60); + elsif run("random_soak_100mhz") then + fmc_half_period <= force 5 ns; + wait for 100 ns; + soak(60); end if; end loop; + -- every test finishes with a quiet bus and zero observed contention + check_equal(unsigned(contention_count), 0, + "DUT contention counter must be zero"); wait for 2 us; test_runner_cleanup(runner); wait; end process; - -- -- Example total test timeout dog - test_runner_watchdog(runner, 1 ms); + test_runner_watchdog(runner, 5 ms); end tb; diff --git a/hdl/ip/vhd/fmc_if/sims/fmc_th.vhd b/hdl/ip/vhd/fmc_if/sims/fmc_th.vhd index 6be6ddd8..c4d92737 100644 --- a/hdl/ip/vhd/fmc_if/sims/fmc_th.vhd +++ b/hdl/ip/vhd/fmc_if/sims/fmc_th.vhd @@ -19,8 +19,14 @@ end entity; architecture th of fmc_th is - signal clk : std_logic := '0'; - signal reset : std_logic := '1'; + -- fmc_clk and aclk are deliberately unrelated so the real CDC gets + -- exercised. fmc_half_period defaults to 50 MHz (the shipped SP CLKDIV) + -- and is forced by the testbench to 7.5 ns / 5 ns for the 66.67 and + -- 100 MHz ratio runs. + signal fmc_half_period : time := 10 ns; + signal fmc_clk : std_logic := '0'; + signal aclk : std_logic := '0'; + signal reset : std_logic := '1'; signal a : std_logic_vector(25 downto 16); signal ad : std_logic_vector(15 downto 0); @@ -30,31 +36,36 @@ architecture th of fmc_th is signal nl : std_logic; signal nwait : std_logic := '1'; - signal rdata : std_logic_vector(31 downto 0); - signal arid : std_logic_vector(3 downto 0) := std_logic_vector(to_unsigned(0, 4)); signal bid : std_logic_vector(3 downto 0); signal awid : std_logic_vector(3 downto 0) := std_logic_vector(to_unsigned(0, 4)); signal rid : std_logic_vector(3 downto 0); - signal data_out_tris : std_logic_vector(15 downto 0); - signal data_out_tris_en : std_logic; + signal data_out_tris : std_logic_vector(15 downto 0); + signal data_out_tris_hiz : std_logic_vector(15 downto 0); + + signal timeout_count : std_logic_vector(7 downto 0); + signal contention_count : std_logic_vector(7 downto 0); signal axi_if : axil_t; begin - -- set up a fastish, clock for the sim - -- env and release reset after a bit of time - clk <= not clk after 4 ns; + aclk <= not aclk after 4 ns; reset <= '0' after 200 ns; + fmc_clk_gen: process + begin + wait for fmc_half_period; + fmc_clk <= not fmc_clk; + end process; + -- sim infrastructure from VUnit axi_read_sim_infra: entity vunit_lib.axi_read_slave generic map ( axi_slave => axi_read_target ) port map ( - aclk => clk, + aclk => aclk, arvalid => axi_if.read_address.valid, arready => axi_if.read_address.ready, @@ -77,7 +88,7 @@ begin axi_slave => axi_write_target ) port map ( - aclk => clk, + aclk => aclk, awvalid => axi_if.write_address.valid, awready => axi_if.write_address.ready, awid => awid, @@ -103,7 +114,7 @@ begin bus_handle => SP_BUS_HANDLE ) port map ( - clk => clk, + clk => fmc_clk, a => a, ad => ad, ne => ne, @@ -113,26 +124,46 @@ begin nwait => nwait ); - ad <= data_out_tris when data_out_tris_en = '1' else (others => 'Z'); + ad <= (others => 'Z') when data_out_tris_hiz(0) = '1' else data_out_tris; + + -- Contention tripwire: the model owns the bus during the address phase + -- (NADV low) and write data beats (NWE low); the DUT driving then means + -- the two sides disagree about the transaction phase. Every test fails + -- fast on this instead of silently resolving the fight. + contention_check: process(fmc_clk) + begin + if rising_edge(fmc_clk) then + assert not (data_out_tris_hiz(0) = '0' and (nl = '0' or nwe = '0')) + report "BUS CONTENTION: DUT driving during SP address/write phase" + severity failure; + end if; + end process; dut: entity work.stm32h7_fmc_target + generic map ( + -- short enough to keep timeout tests quick, long enough that the + -- slow-responder (non-timeout) tests stay under it + timeout_cycles => 512 + ) port map ( -- Interface to the STM32H7's FMC periph - --! Write full flag, sync to write clock domain chip_reset => reset, - fmc_clk => clk, + fmc_clk => fmc_clk, + fmc_capture_clk => fmc_clk, a => a(24 downto 16), addr_data_in => ad, data_out => data_out_tris, - data_out_en => data_out_tris_en, + data_out_hiz => data_out_tris_hiz, ne => ne, - -- todo missing byte enables? noe => noe, nwe => nwe, nl => nl, nwait => nwait, + + timeout_count => timeout_count, + contention_count => contention_count, -- FPGA interface - aclk => clk, + aclk => aclk, aresetn => not reset, axi_if => axi_if diff --git a/hdl/ip/vhd/fmc_if/stm32h7_fmc_target.vhd b/hdl/ip/vhd/fmc_if/stm32h7_fmc_target.vhd index 90a42be0..0fc255f9 100644 --- a/hdl/ip/vhd/fmc_if/stm32h7_fmc_target.vhd +++ b/hdl/ip/vhd/fmc_if/stm32h7_fmc_target.vhd @@ -2,10 +2,28 @@ -- License, v. 2.0. If a copy of the MPL was not distributed with this -- file, You can obtain one at https://mozilla.org/MPL/2.0/. ---! This block provides an FMC target interface from the STM32H7's ---! local bus, crosses clock domains into the FPGA's core logic ---! clock domain, and issues AXI transactions. ---! figures 115 and 116 +-- This block provides an FMC target interface from the STM32H7's +-- local bus (synchronous multiplexed PSRAM mode, RM0433 figures 115/116), +-- crosses clock domains into the FPGA's core logic clock domain, and +-- issues AXI transactions. +-- +-- Pacing contract with the SP, proven on hardware and also encoded in the +-- simulation model: the wait line is held asserted +-- by default and released under FSM control; after the SP samples the wait +-- line released at rising edge N, its current data beat is valid on the +-- bus at edge N+1, and each further release-sampled edge advances one +-- beat. Reads therefore present data one full cycle before the release is +-- sampled, and the SP samples read data on the same edge it samples the +-- released wait. +-- +-- All FMC inputs are captured in dedicated per-pin registers clocked by +-- fmc_capture_clk before the FSM sees them, so the FSM runs one cycle +-- behind the bus. The interface is entirely NWAIT-paced, so that delay is +-- absorbed by the pacing (one extra stall cycle per transaction). On +-- boards with the FMC MMCM the capture clock is a later-phased sibling of +-- fmc_clk, which is what gives the input pins setup margin at 10 ns; +-- boards without an MMCM tie both clock ports to the same clock and get +-- identical cycle behavior. -- ES0491 FMC Errata: -- Dummy read cycles inserted when reading synchronous memories @@ -15,6 +33,8 @@ -- The extra data values read are not used by the FMC and there is no functional failure. -- Workaround -- None +-- (The dummy cycles land while this FSM is back in idle with the wait line +-- asserted and NADV high, so they cannot start a new transaction.) library ieee; use ieee.std_logic_1164.all; @@ -24,27 +44,51 @@ use work.stm32h7_fmc_target_pkg.all; use work.axil26x32_pkg.all; entity stm32h7_fmc_target is + generic ( + -- Bound on how long the SP's bus may be stalled by the wait line, in + -- fmc_clk cycles. An AHB stall is unrecoverable on the SP side, so a + -- wedged AXI responder is converted into poisoned read data (0xFFFF + -- beats) or a swallowed write, plus a bump of timeout_count, instead + -- of a hung SP. + timeout_cycles : positive := 4096; + -- Re-inserts the dead cycle between read beats (the pre-streaming + -- cadence), restoring a 2-cycle data-out window for a board that + -- cannot close single-cycle output timing. The data-out multicycle + -- constraints must match this setting. + extra_beat_setup : boolean := false + ); port ( -- Interface to the STM32H7's FMC periph chip_reset : in std_logic; - --! fmc_clk from STM32's clock generator + -- fmc_clk from STM32's clock generator (deskewed/phase-shifted on + -- boards with the FMC MMCM); clocks the FSM and all outputs fmc_clk : in std_logic; - --! non-multiplexed upper address bits from STM32 + -- clock for the input capture registers only. Tie to fmc_clk when + -- there is no MMCM; with one, the later-phased sibling output. + fmc_capture_clk : in std_logic; + -- non-multiplexed upper address bits from STM32 a : in std_logic_vector(24 downto 16); - --! multiplexed lower address bits/databits to/from STM32 + -- multiplexed lower address bits/databits to/from STM32 addr_data_in : in std_logic_vector(15 downto 0); data_out : out std_logic_vector(15 downto 0); - data_out_en : out std_logic; - --! active-low chip selects + -- Tristate control in OBUFT T polarity: '1' releases the pin, '0' + -- drives it. One bit per pin, all driven identically, registered with + -- no logic between flop and T input -- an active-high enable infers + -- an inverter LUT there, which adds ~1.5 ns and blocks IOB packing. + data_out_hiz : out std_logic_vector(15 downto 0); + -- active-low chip selects ne : in std_logic_vector(3 downto 0); - --! active-low output enable + -- active-low output enable noe : in std_logic; - --! active-low write enable + -- active-low write enable nwe : in std_logic; - --! active-low address latch for address phase + -- active-low address latch for address phase nl : in std_logic; - --! active-low pipelined wait to STM32, asserted 1 cycle before stall + -- active-low wait to STM32 nwait : out std_logic; + -- Saturating diagnostic counters, sticky until chip_reset. + timeout_count : out std_logic_vector(7 downto 0); + contention_count : out std_logic_vector(7 downto 0); -- FPGA interface aclk : in std_logic; aresetn : in std_logic; @@ -55,22 +99,37 @@ end entity; architecture rtl of stm32h7_fmc_target is + -- Driven-'1' bits pass, everything else ('0', and any undriven + -- 'Z'/'X'/'U' from a released bus) becomes '0'. Lets the final write + -- beat be captured unconditionally: qualifying it on chip select would + -- race NE's end-of-burst deassert, which is rising-edge timed and lands + -- within nanoseconds of the capture instant, differently per board. + function drive_or_zero ( + v : std_logic_vector + ) return std_logic_vector is + variable r : std_logic_vector(v'range); + begin + for i in v'range loop + r(i) := '1' when v(i) = '1' else '0'; + end loop; + return r; + end; + attribute mark_debug : string; type fmc_state_type is ( idle, - addr_delay, - addr_delay1, - read_setup, - read_word0_setup_delay, - read_word0, - read_word1_setup_delay, - read_word1, - write_setup, - write_wait_delay, - write_word0, - write_word1, - timeout_cleanup + dispatch, + rd_wait_data, + rd_arm, + rd_beat0, + rd_rearm, + rd_beat1, + wr_arm, + wr_arm2, + wr_beat0, + wr_beat1, + timeout_drain ); type axi_state_type is ( @@ -86,7 +145,6 @@ architecture rtl of stm32h7_fmc_target is attribute mark_debug of axi_state : signal is "TRUE"; signal txn : txn_type; attribute mark_debug of txn : signal is "TRUE"; - signal axi_fifo_rd_path_rdata : std_logic_vector(31 downto 0); signal axi_fifo_rd_path_rd_ack : std_logic; @@ -105,7 +163,50 @@ architecture rtl of stm32h7_fmc_target is attribute mark_debug of axi_addr : signal is "TRUE"; signal axi_fifo_wr_path_wdata : std_logic_vector(31 downto 0); signal axi_fifo_wr_path_write : std_logic; - signal txn_stored : boolean; + signal axi_fifo_wr_path_wfull : std_logic; + + -- One physical flop per pin, so each tristate T can pack into its + -- IOB; synthesis must not merge the identical registers. + signal data_out_hiz_int : std_logic_vector(15 downto 0); + -- IOB rather than keep: both prevent register merging, but keep maps + -- to a dont_touch that also blocks the pad packing it was meant to + -- enable. + attribute IOB : string; + attribute IOB of data_out_hiz_int : signal is "TRUE"; + -- fmc_clk cycles spent in a state that stalls the SP + signal timeout_cntr : natural range 0 to timeout_cycles; + signal timeouts : unsigned(7 downto 0); + attribute mark_debug of timeouts : signal is "TRUE"; + -- Both sides driving the muxed bus at once means the two ends disagree + -- about where in a transaction we are; the count is a tripwire for the + -- testbench and for ILA debug on hardware. + signal contentions : unsigned(7 downto 0); + attribute mark_debug of contentions : signal is "TRUE"; + -- Reads abandoned by timeout still complete on the AXI side eventually; + -- this many arriving read words belong to nobody and must be discarded + -- before data is served to a live read. + signal drop_pending : unsigned(4 downto 0); + + -- Per-pin input capture registers, clocked by fmc_capture_clk. IOB so + -- they land in the pad's ILOGIC flop: the input timing budget is pin -> + -- flop with nothing in between. + -- Deliberately unreset (initial values cover the pre-clock sim window): + -- they track the live bus whenever the clock runs, so by the time the + -- FSM leaves reset they already hold real pin state -- and a reset pin + -- on them would create cross-phase reset recovery paths with only the + -- inter-phase spacing to deassert in. + signal ne_q : std_logic_vector(3 downto 0) := (others => '1'); + signal noe_q : std_logic := '1'; + signal nwe_q : std_logic := '1'; + signal nl_q : std_logic := '1'; + signal a_q : std_logic_vector(24 downto 16) := (others => '0'); + signal ad_q : std_logic_vector(15 downto 0) := (others => '0'); + attribute IOB of ne_q : signal is "TRUE"; + attribute IOB of noe_q : signal is "TRUE"; + attribute IOB of nwe_q : signal is "TRUE"; + attribute IOB of nl_q : signal is "TRUE"; + attribute IOB of a_q : signal is "TRUE"; + attribute IOB of ad_q : signal is "TRUE"; alias awready is axi_if.write_address.ready; alias wready is axi_if.write_data.ready; @@ -123,8 +224,6 @@ architecture rtl of stm32h7_fmc_target is signal araddr : std_logic_vector(25 downto 0); signal rready : std_logic; - signal int_toggle : std_logic; - begin axi_if.write_address.valid <= awvalid; @@ -137,162 +236,281 @@ begin axi_if.read_address.addr <= araddr; axi_if.read_data.ready <= rready; + data_out_hiz <= data_out_hiz_int; + timeout_count <= std_logic_vector(timeouts); + contention_count <= std_logic_vector(contentions); + + -- Input capture stage: the only logic on fmc_capture_clk. + input_capture: process(fmc_capture_clk) + begin + if rising_edge(fmc_capture_clk) then + ne_q <= ne; + noe_q <= noe; + nwe_q <= nwe; + nl_q <= nl; + a_q <= a; + ad_q <= addr_data_in; + end if; + end process; + -- State machine dealing with fmc interface fmc_if_sm: process(fmc_clk, chip_reset) variable chip_selected : boolean; + -- The tristate control is re-derived from the live control pins + -- every cycle it could be driving, so a bus desync (SP deasserting + -- OE/CS under us) releases the bus within one cycle instead of + -- holding a fight until the FSM notices. hiz_v is in T polarity: + -- '1' = release. + variable hiz_v : std_logic; begin if chip_reset then - data_out <= (others => '0'); - data_out_en <= '0'; -- release bus - nwait <= '0'; - txn <= ('0', (others => '0')); + fmc_state <= idle; + data_out <= (others => '0'); + data_out_hiz_int <= (others => '1'); -- release bus + nwait <= '0'; + txn <= ('0', (others => '0')); axi_fifo_wr_path_wdata <= (others => '0'); axi_fifo_rd_path_rd_ack <= '0'; axi_fifo_txn_path_write <= '0'; axi_fifo_wr_path_write <= '0'; - txn_stored <= false; - int_toggle <= '0'; + timeout_cntr <= 0; + timeouts <= (others => '0'); + contentions <= (others => '0'); + drop_pending <= (others => '0'); elsif rising_edge(fmc_clk) then -- some variable naming for more legibility - chip_selected := ne(0) = '0'; + chip_selected := ne_q(0) = '0'; + hiz_v := noe_q or ne_q(0); -- single-cycle flags, unconditionally cleared axi_fifo_rd_path_rd_ack <= '0'; axi_fifo_txn_path_write <= '0'; - axi_fifo_wr_path_write <= '0'; + axi_fifo_wr_path_write <= '0'; + + -- The timeout counter runs in every situation that can stall the + -- SP indefinitely. That includes sitting in idle with chip + -- select asserted: a miscaptured NADV means we never saw the + -- transaction start, the SP is stalled on its bus waiting for a + -- wait release that will never come, and nothing but this + -- counter can free it. (Normal idle-with-CS dwell -- the address + -- cycle itself, ES0491 dummy reads -- lasts a handful of cycles, + -- nowhere near the timeout.) + if fmc_state = dispatch or fmc_state = rd_wait_data + or (fmc_state = idle and chip_selected) then + if timeout_cntr /= timeout_cycles then + timeout_cntr <= timeout_cntr + 1; + end if; + else + timeout_cntr <= 0; + end if; + + -- Contention tripwire: the SP drives the muxed bus during the + -- address phase (NADV low) and during write data beats (NWE low); + -- our enable being up in either is a protocol desync. + if data_out_hiz_int(0) = '0' and (nl_q = '0' or nwe_q = '0') then + if contentions /= x"FF" then + contentions <= contentions + 1; + end if; + end if; + case fmc_state is when idle => - nwait <= '0'; - data_out_en <= '0'; -- release bus - -- Look for a starting transition - -- ( chip sel and address latch) - if chip_selected and nl = '0' then + nwait <= '0'; + data_out_hiz_int <= (others => '1'); -- release bus + if drop_pending /= 0 and axi_fifo_rd_path_rempty = '0' + and axi_fifo_rd_path_rd_ack = '0' then + axi_fifo_rd_path_rd_ack <= '1'; + drop_pending <= drop_pending - 1; + elsif timeout_cntr = timeout_cycles then + -- chip select has been pinned with no transaction + -- start observed: we missed one. Free the SP's bus + -- and swallow whatever beats it clocks out. + if timeouts /= x"FF" then + timeouts <= timeouts + 1; + end if; + data_out <= x"FFFF"; + nwait <= '1'; + fmc_state <= timeout_drain; + elsif chip_selected and nl_q = '0' then -- Bus outputs right-shifted so we shift left here to -- recover byte addrs - txn.addr <= unsigned(a & addr_data_in & "0"); - txn.read_not_write <= nwe; - - fmc_state <= addr_delay; - end if; - when addr_delay => - -- We get here after latching the address - -- We'll delay for an additional cycle - -- so that the next cycle will be checking - fmc_state <= addr_delay1; - when addr_delay1 => - -- We need to immediately stall the bus at this point if we have a full txn fifo - -- other stall conditions will be checked in the read/write setup phase - -- since the conditions differ - if axi_fifo_txn_path_wfull then - nwait <= '0'; - end if; - if txn.read_not_write = '1' then - fmc_state <= read_setup; - -- For reads, we unconditionally wait here since we have to - -- do an axi transaction to even fetch the first data to return - -- which takes more than 1 cycle :) - nwait <= '0'; - else - fmc_state <= write_setup; + txn.addr <= unsigned(a_q & ad_q & "0"); + txn.read_not_write <= nwe_q; + fmc_state <= dispatch; end if; - when read_setup => - -- TODO: need a wait timeout mech here, we're potentially stalling - -- the SP's bus here! - - -- We need to issue this transaction 1x to the txn fifo + when dispatch => + -- Queue the transaction. Stalling here (the wait line is + -- still asserted from idle) is the back-pressure path + -- when the AXI side has fallen behind. if not chip_selected then fmc_state <= idle; - else - -- We're going to be doing a read here we must be waited already - -- We need to immediately stall the bus at this point if we have a full txn fifo - -- not that transactions are processed in order so any writes pending - -- will necessarily happen first. This is important since the writes could - -- have side-effects that affect the reads - if not txn_stored and axi_fifo_txn_path_wfull = '0' then - -- Store the transaction, set the stored flag so we don't - -- do it again while we wait - axi_fifo_txn_path_write <= '1'; - txn_stored <= true; + elsif timeout_cntr = timeout_cycles then + -- Nothing queued yet, so nothing is in flight: free + -- the SP and swallow/poison its beats. + if timeouts /= x"FF" then + timeouts <= timeouts + 1; end if; - -- Wait is held here until we've done the AXI transaction - -- to fetch the data and have the data back in the fifo - if not axi_fifo_rd_path_rempty then - -- Register the data - -- apply the data to the bus - -- take away the wait - data_out <= axi_fifo_rd_path_rdata(15 downto 0); - -- noe should always be active here, but this provides a safety net - -- in case there is a bus-desync of some kind, we don't want to cross-drive with - -- the sp - data_out_en <= '1' and (not noe); - fmc_state <= read_word0_setup_delay; + data_out <= x"FFFF"; + nwait <= '1'; + fmc_state <= timeout_drain; + elsif axi_fifo_txn_path_wfull = '0' and + (txn.read_not_write = '1' or axi_fifo_wr_path_wfull = '0') then + axi_fifo_txn_path_write <= '1'; + if txn.read_not_write then + -- Reads keep the SP waited: the data has to + -- round-trip the AXI side before anything can be + -- returned. + fmc_state <= rd_wait_data; + else + nwait <= '1'; + fmc_state <= wr_arm; end if; end if; - when read_word0_setup_delay => - nwait <= '1'; - fmc_state <= read_word0; - when read_word0 => - nwait <= '0'; - txn_stored <= false; - data_out <= axi_fifo_rd_path_rdata(31 downto 16); - if not chip_selected then + when rd_wait_data => + if axi_fifo_rd_path_rd_ack = '1' then + -- rempty/rdata are one cycle behind a pop; deciding + -- anything off them now would serve the word that was + -- just discarded + null; + elsif drop_pending /= 0 then + -- Stale words from earlier timed-out reads arrive + -- ahead of ours; discard them first. + if axi_fifo_rd_path_rempty = '0' then + axi_fifo_rd_path_rd_ack <= '1'; + drop_pending <= drop_pending - 1; + end if; + elsif not chip_selected then + -- The read was queued on entry to this state, so an + -- SP abort here still leaves it in flight and its + -- data must be discarded when it lands. + if drop_pending /= "11111" then + drop_pending <= drop_pending + 1; + end if; fmc_state <= idle; - data_out_en <= '0'; -- release bus - axi_fifo_rd_path_rd_ack <= '1'; -- clear the read word - else -- not done, move to next word - fmc_state <= read_word1_setup_delay; + elsif timeout_cntr = timeout_cycles then + -- The read is in flight; whenever its data lands it + -- belongs to nobody. + if drop_pending /= "11111" then + drop_pending <= drop_pending + 1; + end if; + if timeouts /= x"FF" then + timeouts <= timeouts + 1; + end if; + data_out <= x"FFFF"; + data_out_hiz_int <= (others => hiz_v); + nwait <= '1'; + fmc_state <= timeout_drain; + elsif axi_fifo_rd_path_rempty = '0' then + -- Present the first beat a full cycle before the wait + -- release can be sampled. + data_out <= axi_fifo_rd_path_rdata(15 downto 0); + data_out_hiz_int <= (others => hiz_v); + fmc_state <= rd_arm; end if; - when read_word1_setup_delay => - nwait <= '1'; - fmc_state <= read_word1; - when read_word1 => - -- TODO: if we want to allow shorter transactions - -- we'd need to do the right thing here, which would - -- be termingating early, and doing the read anyway. - -- we can only do 32bit wide reads on the AXI side - -- so care should be excersized by the user if there - -- are read side-effects on the addresses next to this - -- read since we'd be doing a 32bit axi read in this case - -- and dropping the latter part on the floor since it wasn't - -- requested. - - -- normal case: pop the rdata fifo since we're done with it + when rd_arm => + if not chip_selected then + axi_fifo_rd_path_rd_ack <= '1'; + data_out_hiz_int <= (others => '1'); + fmc_state <= idle; + else + nwait <= '1'; + data_out_hiz_int <= (others => hiz_v); + fmc_state <= rd_beat0; + end if; + when rd_beat0 => + -- SP samples word0 at this edge. + if not chip_selected then + -- Shortened read: the full 32-bit AXI read already + -- happened, so any read side effects have occurred; + -- all we can do is clean up. + axi_fifo_rd_path_rd_ack <= '1'; + data_out_hiz_int <= (others => '1'); + nwait <= '0'; + fmc_state <= idle; + else + data_out <= axi_fifo_rd_path_rdata(31 downto 16); + data_out_hiz_int <= (others => hiz_v); + if extra_beat_setup then + nwait <= '0'; + fmc_state <= rd_rearm; + else + -- wait stays released; word1 is sampled on the + -- very next edge (this word0->word1 transition is + -- the one single-cycle data-out path). + fmc_state <= rd_beat1; + end if; + end if; + when rd_rearm => + -- extra_beat_setup only: one dead cycle re-opens the + -- 2-cycle data-out window of the pre-streaming cadence. + nwait <= '1'; + data_out_hiz_int <= (others => hiz_v); + fmc_state <= rd_beat1; + when rd_beat1 => + -- SP samples word1 at this edge; done with the word. axi_fifo_rd_path_rd_ack <= '1'; - fmc_state <= idle; nwait <= '0'; - data_out_en <= '0'; -- release bus - when write_setup => + data_out_hiz_int <= (others => '1'); -- release bus + fmc_state <= idle; + when wr_arm => + -- The SP samples the released wait at this edge and + -- launches beat0 on the following falling edge. if not chip_selected then - fmc_state <= idle; + -- The transaction is already queued; complete it with + -- zeros rather than leaving the AXI side wedged + -- waiting for write data that will never come. + axi_fifo_wr_path_wdata <= (others => '0'); + axi_fifo_wr_path_write <= '1'; + nwait <= '0'; + fmc_state <= idle; else - if not txn_stored and axi_fifo_txn_path_wfull = '0' then - -- Store the transaction, set the stored flag so we don't - -- do it again while we wait - axi_fifo_txn_path_write <= '1'; - txn_stored <= true; - nwait <= '1'; - -- no waits needed until the fifo fills up - fmc_state <= write_wait_delay; - end if; + fmc_state <= wr_arm2; + end if; + when wr_arm2 => + -- beat0 is on the bus this cycle; it lands in ad_q for + -- wr_beat0 to consume on the next edge + if not chip_selected then + axi_fifo_wr_path_wdata <= (others => '0'); + axi_fifo_wr_path_write <= '1'; + nwait <= '0'; + fmc_state <= idle; + else + fmc_state <= wr_beat0; + end if; + when wr_beat0 => + if not chip_selected then + axi_fifo_wr_path_wdata <= (others => '0'); + axi_fifo_wr_path_write <= '1'; + nwait <= '0'; + fmc_state <= idle; + else + axi_fifo_wr_path_wdata(15 downto 0) <= ad_q; + fmc_state <= wr_beat1; end if; - when write_wait_delay => - fmc_state <= write_word0; - when write_word0 => - axi_fifo_wr_path_wdata(15 downto 0) <= addr_data_in; - txn_stored <= false; - fmc_state <= write_word1; - when write_word1 => - axi_fifo_wr_path_wdata(31 downto 16) <= addr_data_in; + when wr_beat1 => + -- Unconditional: reaching this state means the SP + -- committed the burst, and its data-hold contract covers + -- this capture even as NE deasserts. An aborted burst + -- leaves the bus released, which drive_or_zero turns + -- into zero filler rather than 'Z'/'X' in the AXI data. + axi_fifo_wr_path_wdata(31 downto 16) <= drive_or_zero(ad_q); axi_fifo_wr_path_write <= '1'; - nwait <= '0'; + nwait <= '0'; fmc_state <= idle; - -- We need to immediately stall the bus at this point if we have a full txn fifo - -- other stall conditions will be checked in the write setup phase - -- since the conditions differ - if axi_fifo_txn_path_wfull then + when timeout_drain => + -- Wait is released and poison is on the bus for read + -- phases; let the SP clock through its remaining beats + -- without capturing anything, then return to idle once + -- it deselects. Drive purely off the live OE/CS pins: + -- during write beats NOE is high so this never drives + -- against the SP, and the recorded transaction type may + -- be stale when the drain was entered from idle. + data_out_hiz_int <= (others => hiz_v); + if not chip_selected then + nwait <= '0'; + data_out_hiz_int <= (others => '1'); + fmc_state <= idle; end if; - when timeout_cleanup => - null; end case; end if; end process; @@ -362,7 +580,7 @@ begin reset => chip_reset, write_en => axi_fifo_wr_path_write, wdata => axi_fifo_wr_path_wdata, - wfull => open, + wfull => axi_fifo_wr_path_wfull, wusedwds => open, -- Read interface rclk => aclk, @@ -414,15 +632,16 @@ begin arvalid <= '0'; end if; when axi_write_init => - awvalid <= '1'; + -- AW and W are raised together, and only once the write + -- data has crossed the FIFO: raising AW alone lets a + -- downstream decoder start a write it then has to stall + -- on, and this block's own W data pops on the handshake. if not axi_fifo_wr_path_rempty then + awvalid <= '1'; wvalid <= '1'; + bready <= '1'; axi_state <= axi_write_wait; end if; - if awready and awvalid then - awvalid <= '0'; - end if; - bready <= '1'; when axi_write_wait => if awready and awvalid then awvalid <= '0'; @@ -432,7 +651,7 @@ begin end if; if bvalid then axi_state <= idle; - bready <= '0'; + bready <= '0'; end if; end case; end if; diff --git a/hdl/ip/vhd/hash_engine/hash_engine_regs.rdl b/hdl/ip/vhd/hash_engine/hash_engine_regs.rdl index ac09dbe2..def51f74 100644 --- a/hdl/ip/vhd/hash_engine/hash_engine_regs.rdl +++ b/hdl/ip/vhd/hash_engine/hash_engine_regs.rdl @@ -40,6 +40,9 @@ addrmap hash_engine_regs { enum source_select { LOCAL_REG = 4'h0 {desc = "Bytes come from processor writes to WDATA";}; HOST_QSPI = 4'h1 {desc = "Bytes are fetched from the host QSPI flash starting at FLASH_ADDR";}; + AUX_QSPI = 4'h2 {desc = "Bytes are fetched from the second QSPI flash starting at + FLASH_ADDR. Only designs with two flashes have one; elsewhere a start with + this source is refused as a configuration error.";}; }; field { @@ -174,4 +177,70 @@ addrmap hash_engine_regs { DIGEST6->desc = "Digest bytes 24 to 27"; digest_word DIGEST7; DIGEST7->desc = "Digest bytes 28 to 31"; + + // Hardware-requested measurement. A sequencer can ask the engine to hash + // a flash range without software in the loop, for instance to measure a + // device's boot image before releasing it from reset. The range comes from + // the two registers below, the source is fixed by the design, and the + // result is kept apart from the software-driven DIGEST so a later run + // cannot overwrite it. + reg { + name = "Hardware Request Flash Address"; + desc = "Raw byte address of the first byte hashed on a hardware request. + Not remapped, as FLASH_ADDR is not."; + field { + desc = "Flash byte address"; + } addr[31:0] = 0; + } HW_FLASH_ADDR; + + reg { + name = "Hardware Request Length"; + desc = "Number of bytes hashed on a hardware request. Zero makes the + request fail with HW_STATUS.cfg_err, which is what an unprogrammed range + does; the requester decides what to make of that."; + field { + desc = "Message length in bytes"; + } count[31:0] = 0; + } HW_LENGTH; + + reg { + name = "Hardware Request Status"; + desc = "Outcome of the most recent hardware request. All but busy hold + until the next request begins."; + default sw = r; + default hw = w; + field { + desc = "The request arrived while a software-started hash was running + and was refused; the software run was left alone."; + } engine_busy[4:4] = 0; + field { + desc = "The run was ended early by CONTROL.abort."; + } aborted[3:3] = 0; + field { + desc = "The run was refused as a configuration error, see HW_LENGTH."; + } cfg_err[2:2] = 0; + field { + desc = "The run completed and HW_DIGEST holds its result."; + } done[1:1] = 0; + field { + desc = "A hardware-requested run is in flight."; + } busy[0:0] = 0; + } HW_STATUS; + + digest_word HW_DIGEST0; + HW_DIGEST0->desc = "Hardware request digest bytes 0 to 3"; + digest_word HW_DIGEST1; + HW_DIGEST1->desc = "Hardware request digest bytes 4 to 7"; + digest_word HW_DIGEST2; + HW_DIGEST2->desc = "Hardware request digest bytes 8 to 11"; + digest_word HW_DIGEST3; + HW_DIGEST3->desc = "Hardware request digest bytes 12 to 15"; + digest_word HW_DIGEST4; + HW_DIGEST4->desc = "Hardware request digest bytes 16 to 19"; + digest_word HW_DIGEST5; + HW_DIGEST5->desc = "Hardware request digest bytes 20 to 23"; + digest_word HW_DIGEST6; + HW_DIGEST6->desc = "Hardware request digest bytes 24 to 27"; + digest_word HW_DIGEST7; + HW_DIGEST7->desc = "Hardware request digest bytes 28 to 31"; }; diff --git a/hdl/ip/vhd/hash_engine/hash_engine_regs.vhd b/hdl/ip/vhd/hash_engine/hash_engine_regs.vhd index 8624222d..cd953761 100644 --- a/hdl/ip/vhd/hash_engine/hash_engine_regs.vhd +++ b/hdl/ip/vhd/hash_engine/hash_engine_regs.vhd @@ -38,6 +38,12 @@ entity hash_engine_regs is -- Bit 7 downto 0 is hash byte 0, so DIGESTn is digest(32n+31 downto 32n) digest : in std_logic_vector(255 downto 0); + -- hardware request path, see hash_engine_top + hw_flash_addr : out hw_flash_addr_type; + hw_length : out hw_length_type; + hw_status : in hw_status_type; + hw_digest : in std_logic_vector(255 downto 0); + -- Software data FIFO push port wdata_fifo_wdata : out std_logic_vector(31 downto 0); wdata_fifo_write : out std_logic @@ -89,6 +95,8 @@ begin prepend <= rec_reset; flash_addr <= rec_reset; msg_length <= rec_reset; + hw_flash_addr <= rec_reset; + hw_length <= rec_reset; start_strobe <= '0'; abort_strobe <= '0'; elsif rising_edge(clk) then @@ -104,6 +112,8 @@ begin when PREPEND_OFFSET => prepend <= unpack(axi_if.write_data.data); when FLASH_ADDR_OFFSET => flash_addr <= unpack(axi_if.write_data.data); when LENGTH_OFFSET => msg_length <= unpack(axi_if.write_data.data); + when HW_FLASH_ADDR_OFFSET => hw_flash_addr <= unpack(axi_if.write_data.data); + when HW_LENGTH_OFFSET => hw_length <= unpack(axi_if.write_data.data); when others => null; end case; end if; @@ -135,6 +145,17 @@ begin when DIGEST5_OFFSET => rdata <= digest(191 downto 160); when DIGEST6_OFFSET => rdata <= digest(223 downto 192); when DIGEST7_OFFSET => rdata <= digest(255 downto 224); + when HW_FLASH_ADDR_OFFSET => rdata <= pack(hw_flash_addr); + when HW_LENGTH_OFFSET => rdata <= pack(hw_length); + when HW_STATUS_OFFSET => rdata <= pack(hw_status); + when HW_DIGEST0_OFFSET => rdata <= hw_digest(31 downto 0); + when HW_DIGEST1_OFFSET => rdata <= hw_digest(63 downto 32); + when HW_DIGEST2_OFFSET => rdata <= hw_digest(95 downto 64); + when HW_DIGEST3_OFFSET => rdata <= hw_digest(127 downto 96); + when HW_DIGEST4_OFFSET => rdata <= hw_digest(159 downto 128); + when HW_DIGEST5_OFFSET => rdata <= hw_digest(191 downto 160); + when HW_DIGEST6_OFFSET => rdata <= hw_digest(223 downto 192); + when HW_DIGEST7_OFFSET => rdata <= hw_digest(255 downto 224); when others => rdata <= (others => '0'); end case; end if; diff --git a/hdl/ip/vhd/hash_engine/hash_engine_top.vhd b/hdl/ip/vhd/hash_engine/hash_engine_top.vhd index a6b7ff13..1b80bfde 100644 --- a/hdl/ip/vhd/hash_engine/hash_engine_top.vhd +++ b/hdl/ip/vhd/hash_engine/hash_engine_top.vhd @@ -27,7 +27,26 @@ use work.keccak_pkg.all; -- The integrator should hold the response FIFO in reset only from the global -- reset. This block never asks for it to be flushed: an abandoned read is dealt -- with by consuming the bytes still owed, see hash_feeder. +-- +-- Besides the register interface there is a hardware request: a sequencer can +-- raise hw_req to have a flash range (HW_FLASH_ADDR/HW_LENGTH, on the flash +-- HW_FLASH_SEL names) hashed without software in the loop, and gets hw_ack +-- back once the run is over, with hw_err saying whether it produced a digest. +-- The digest is kept in its own registers so that a later software run does +-- not overwrite it. A request that lands while a software run is in flight is +-- refused rather than restarting the run; software starts that land while a +-- hardware run is in flight are dropped. Four-phase: the requester holds +-- hw_req until it sees hw_ack, and hw_ack drops once hw_req does. entity hash_engine_top is + generic ( + -- How many spi_nor flash clients hang off this engine. CONFIG.source + -- picks between them for a run; AUX_QSPI is a configuration error + -- when there is only one. + NUM_FLASHES : natural range 1 to 2 := 1; + -- Which flash a hardware request reads: 0 the host flash, 1 the aux + -- flash (which needs NUM_FLASHES = 2). + HW_FLASH_SEL : natural range 0 to 1 := 0 + ); port ( clk : in std_logic; reset : in std_logic; @@ -35,14 +54,26 @@ entity hash_engine_top is -- Axilite interface axi_if : view axil_target; - -- Flash read command FIFO: word 0 is a byte address, word 1 a byte count - cmd_fifo_wdata : out std_logic_vector(31 downto 0); - cmd_fifo_write : out std_logic; + -- Hardware request, see above. Leave hw_req unconnected on a design + -- without a requester. + hw_req : in std_logic := '0'; + hw_ack : out std_logic; + hw_err : out std_logic; - -- Flash read response FIFO, showahead so rdack is a read acknowledge - rsp_fifo_rdata : in std_logic_vector(7 downto 0); - rsp_fifo_rdack : out std_logic; - rsp_fifo_rempty : in std_logic + -- The spi_nor side of the engine's own command and response FIFOs, one + -- pair per flash. These match spi_nor_top's hash client port shape: + -- the flash pops commands (word 0 a byte address, word 1 a byte count) + -- and pushes response bytes. Only the flash selected for the run in + -- flight ever sees a non-empty command FIFO, so the others sit idle. + flash_cmd_rdata : out std_logic_vector(31 downto 0); + flash_cmd_rdack : in std_logic_vector(NUM_FLASHES - 1 downto 0); + flash_cmd_rempty : out std_logic_vector(NUM_FLASHES - 1 downto 0); + flash_rsp_wdata : in std_logic_vector(NUM_FLASHES * 8 - 1 downto 0); + flash_rsp_write : in std_logic_vector(NUM_FLASHES - 1 downto 0); + -- Backpressure for clients that honour it. spi_nor_top does not (its + -- raw_flash_txn_mgr paces itself off the SPI link), but a behavioural + -- responder in simulation can push a byte a cycle and needs it. + flash_rsp_wfull : out std_logic_vector(NUM_FLASHES - 1 downto 0) ); end entity; @@ -59,6 +90,36 @@ architecture rtl of hash_engine_top is signal flash_addr : flash_addr_type; signal msg_length : length_type; + -- What the feeder actually sees: the software registers, or the hardware + -- request's configuration while one of those is in flight. + signal feeder_start : std_logic; + signal feeder_cfg : config_type; + signal feeder_prepend : prepend_type; + signal feeder_flash_addr : flash_addr_type; + signal feeder_length : length_type; + + signal hw_flash_addr : hw_flash_addr_type; + signal hw_length : hw_length_type; + signal hw_status : hw_status_type; + signal hw_digest : std_logic_vector(255 downto 0); + + type hw_state_t is (idle, starting, running, acked); + type hw_reg_t is record + state : hw_state_t; + start : std_logic; + active : std_logic; + ack : std_logic; + err : std_logic; + settle : natural range 0 to 3; + status : hw_status_type; + digest : std_logic_vector(255 downto 0); + end record; + constant hw_reg_reset : hw_reg_t := ( + state => idle, start => '0', active => '0', ack => '0', err => '0', + settle => 0, status => rec_reset, digest => (others => '0') + ); + signal hw_r : hw_reg_t; + signal status : status_type; signal progress : progress_type; @@ -77,6 +138,20 @@ architecture rtl of hash_engine_top is signal digest : digest_t; signal digest_valid : std_logic; + -- Feeder side of the flash client FIFOs + signal cmd_fifo_wdata : std_logic_vector(31 downto 0); + signal cmd_fifo_write : std_logic; + signal cmd_fifo_rdack : std_logic; + signal cmd_fifo_rempty : std_logic; + signal rsp_fifo_wdata : std_logic_vector(7 downto 0); + signal rsp_fifo_write : std_logic; + signal rsp_fifo_rdata : std_logic_vector(7 downto 0); + signal rsp_fifo_rdack : std_logic; + signal rsp_fifo_rempty : std_logic; + signal rsp_fifo_wfull : std_logic; + -- Which flash the run in flight is reading, latched by the feeder at start + signal flash_sel : natural range 0 to NUM_FLASHES - 1; + begin hash_engine_regs_inst: entity work.hash_engine_regs @@ -93,10 +168,88 @@ begin status => status, progress => progress, digest => digest, + hw_flash_addr => hw_flash_addr, + hw_length => hw_length, + hw_status => hw_status, + hw_digest => hw_digest, wdata_fifo_wdata => sw_fifo_wdata, wdata_fifo_write => sw_fifo_write ); + -- Hardware request sequencing. The feeder latches its configuration on the + -- cycle it accepts a start, so the mux below only has to hold for as long as + -- the request is active, which it does. + feeder_start <= hw_r.start when hw_r.active = '1' else start_strobe; + feeder_cfg <= (source => AUX_QSPI) when hw_r.active = '1' and HW_FLASH_SEL = 1 else + (source => HOST_QSPI) when hw_r.active = '1' else + cfg; + feeder_prepend <= (count => (others => '0')) when hw_r.active = '1' else prepend; + feeder_flash_addr <= (addr => hw_flash_addr.addr) when hw_r.active = '1' else flash_addr; + feeder_length <= (count => hw_length.count) when hw_r.active = '1' else msg_length; + + hw_ack <= hw_r.ack; + hw_err <= hw_r.err; + hw_status <= hw_r.status; + hw_digest <= hw_r.digest; + + hw_request: process(clk, reset) + begin + if reset then + hw_r <= hw_reg_reset; + elsif rising_edge(clk) then + hw_r.start <= '0'; + case hw_r.state is + when idle => + if hw_req = '1' then + hw_r.status <= rec_reset; + hw_r.err <= '0'; + if status.busy = '1' then + -- A software run owns the engine; do not restart + -- it out from under whoever started it. + hw_r.status.engine_busy <= '1'; + hw_r.err <= '1'; + hw_r.ack <= '1'; + hw_r.state <= acked; + else + hw_r.active <= '1'; + hw_r.start <= '1'; + hw_r.status.busy <= '1'; + hw_r.settle <= 0; + hw_r.state <= starting; + end if; + end if; + when starting => + -- The feeder answers a start two cycles later, with either + -- busy or cfg_err. Neither is ours to look at before then. + if hw_r.settle = 2 then + hw_r.state <= running; + else + hw_r.settle <= hw_r.settle + 1; + end if; + when running => + if status.cfg_err = '1' or status.aborted = '1' or + (status.busy = '0' and status.done = '1') then + hw_r.status.busy <= '0'; + hw_r.status.cfg_err <= status.cfg_err; + hw_r.status.aborted <= status.aborted; + hw_r.status.done <= status.done and not status.aborted; + hw_r.err <= status.cfg_err or status.aborted; + if status.done = '1' and status.aborted = '0' then + hw_r.digest <= digest; + end if; + hw_r.active <= '0'; + hw_r.ack <= '1'; + hw_r.state <= acked; + end if; + when acked => + if hw_req = '0' then + hw_r.ack <= '0'; + hw_r.state <= idle; + end if; + end case; + end if; + end process; + -- Software data path. Written 32 bits at a time by the processor and read a -- byte at a time by the feeder, least significant byte first. sw_fifo_reset <= reset or sw_clear; @@ -123,15 +276,18 @@ begin ); hash_feeder_inst: entity work.hash_feeder + generic map ( + NUM_FLASHES => NUM_FLASHES + ) port map ( clk => clk, reset => reset, - start_strobe => start_strobe, + start_strobe => feeder_start, abort_strobe => abort_strobe, - cfg => cfg, - prepend => prepend, - flash_addr => flash_addr, - msg_length => msg_length, + cfg => feeder_cfg, + prepend => feeder_prepend, + flash_addr => feeder_flash_addr, + msg_length => feeder_length, busy => status.busy, done => status.done, aborted => status.aborted, @@ -144,6 +300,7 @@ begin sw_fifo_rdack => sw_fifo_rdack, sw_fifo_rempty => sw_fifo_rempty, sw_fifo_clear => sw_clear, + flash_sel => flash_sel, cmd_fifo_wdata => cmd_fifo_wdata, cmd_fifo_write => cmd_fifo_write, rsp_fifo_rdata => rsp_fifo_rdata, @@ -151,6 +308,66 @@ begin rsp_fifo_rempty => rsp_fifo_rempty ); + -- Flash client FIFOs. One pair serves every flash: the selected flash is + -- the only one shown a non-empty command FIFO and the only one whose + -- response writes are taken, so the FIFOs never see two clients at once. + -- flash_sel holds still for the whole run, which is what lets this be a + -- plain mux rather than an arbiter. + cmd_fifo: entity work.dcfifo_xpm + generic map ( + fifo_write_depth => 256, + data_width => 32, + showahead_mode => true + ) + port map ( + wclk => clk, + reset => reset, + write_en => cmd_fifo_write, + wdata => cmd_fifo_wdata, + wfull => open, + wusedwds => open, + rclk => clk, + rdata => flash_cmd_rdata, + rdreq => cmd_fifo_rdack, + rempty => cmd_fifo_rempty, + rusedwds => open + ); + + rsp_fifo: entity work.dcfifo_xpm + generic map ( + fifo_write_depth => 256, + data_width => 8, + showahead_mode => true + ) + port map ( + wclk => clk, + reset => reset, + write_en => rsp_fifo_write, + wdata => rsp_fifo_wdata, + wfull => rsp_fifo_wfull, + wusedwds => open, + rclk => clk, + rdata => rsp_fifo_rdata, + rdreq => rsp_fifo_rdack, + rempty => rsp_fifo_rempty, + rusedwds => open + ); + + flash_mux: process(all) + begin + cmd_fifo_rdack <= flash_cmd_rdack(flash_sel); + rsp_fifo_wdata <= flash_rsp_wdata(flash_sel * 8 + 7 downto flash_sel * 8); + rsp_fifo_write <= flash_rsp_write(flash_sel); + flash_rsp_wfull <= (others => rsp_fifo_wfull); + for i in 0 to NUM_FLASHES - 1 loop + if i = flash_sel then + flash_cmd_rempty(i) <= cmd_fifo_rempty; + else + flash_cmd_rempty(i) <= '1'; + end if; + end loop; + end process; + -- Also report full while the FIFO is being flushed at the tail of a run, so a -- processor that polls before writing cannot push bytes into a FIFO that is in -- reset. The flush only happens when a run ends, never as one starts, which is diff --git a/hdl/ip/vhd/hash_engine/hash_feeder.vhd b/hdl/ip/vhd/hash_engine/hash_feeder.vhd index 2407f70a..7e789078 100644 --- a/hdl/ip/vhd/hash_engine/hash_feeder.vhd +++ b/hdl/ip/vhd/hash_engine/hash_feeder.vhd @@ -34,6 +34,9 @@ use work.hash_engine_regs_pkg.all; -- removes the race entirely. It also means data written before the first start is -- kept, so pre-loading works. entity hash_feeder is + generic ( + NUM_FLASHES : natural range 1 to 2 := 1 + ); port ( clk : in std_logic; reset : in std_logic; @@ -77,6 +80,9 @@ entity hash_feeder is -- hangs waiting for them. sw_fifo_clear : out std_logic; + -- Which flash the run in flight reads from, held for the whole run + flash_sel : out natural range 0 to NUM_FLASHES - 1; + -- Flash command FIFO: word 0 is the byte address, word 1 the byte count cmd_fifo_wdata : out std_logic_vector(31 downto 0); cmd_fifo_write : out std_logic; @@ -108,6 +114,7 @@ architecture rtl of hash_feeder is fed : unsigned(31 downto 0); addr : std_logic_vector(31 downto 0); src_qspi : std_logic; + flash_sel : natural range 0 to NUM_FLASHES - 1; clear_cnt : natural range 0 to CLEAR_CYCLES; -- Set when the flush should be followed by a new run rather than idling restart : std_logic; @@ -132,6 +139,7 @@ architecture rtl of hash_feeder is fed => (others => '0'), addr => (others => '0'), src_qspi => '0', + flash_sel => 0, clear_cnt => 0, restart => '0', finished => '0', @@ -185,6 +193,7 @@ begin done <= r.done; aborted <= r.aborted; cfg_err <= r.cfg_err; + flash_sel <= r.flash_sel; bytes_fed <= std_logic_vector(r.fed); sha3_init <= r.init; @@ -205,11 +214,13 @@ begin stop_run := false; -- A start is refused outright if the configuration cannot produce a - -- message: the core has no way to express a zero length one, and a prepend - -- longer than the message is simply nonsense. + -- message: the core has no way to express a zero length one, a prepend + -- longer than the message is simply nonsense, and the second flash can + -- only be asked for on a design that has one. accepted := start_strobe = '1' and unsigned(msg_length.count) /= 0 and - unsigned(prepend.count) <= unsigned(msg_length.count); + unsigned(prepend.count) <= unsigned(msg_length.count) and + not (cfg.source = AUX_QSPI and NUM_FLASHES = 1); if start_strobe = '1' and not accepted then v.cfg_err := '1'; @@ -222,7 +233,8 @@ begin v.prepend_cnt := unsigned(prepend.count); v.flash_req := unsigned(msg_length.count) - unsigned(prepend.count); v.addr := flash_addr.addr; - v.src_qspi := '1' when cfg.source = HOST_QSPI else '0'; + v.src_qspi := '1' when cfg.source = HOST_QSPI or cfg.source = AUX_QSPI else '0'; + v.flash_sel := 1 when cfg.source = AUX_QSPI and NUM_FLASHES > 1 else 0; v.cfg_err := '0'; v.aborted := '0'; v.done := '0'; @@ -245,7 +257,14 @@ begin if stop_run then -- Work out what the flash still owes us so DRAIN can swallow it. - v.drain_left := r.flash_req - r.flash_rx; + -- Nothing is owed unless the read command actually went out: a + -- software fed run, or a flash run abandoned during its prepend, + -- would otherwise wait in DRAIN for bytes that never come. + if r.src_qspi = '1' and r.cmd_sent = '1' then + v.drain_left := r.flash_req - r.flash_rx; + else + v.drain_left := (others => '0'); + end if; v.finished := '0'; v.state := DRAIN; else diff --git a/hdl/ip/vhd/hash_engine/sims/hash_engine_tb.vhd b/hdl/ip/vhd/hash_engine/sims/hash_engine_tb.vhd index cc57506e..ff1b5c32 100644 --- a/hdl/ip/vhd/hash_engine/sims/hash_engine_tb.vhd +++ b/hdl/ip/vhd/hash_engine/sims/hash_engine_tb.vhd @@ -46,10 +46,14 @@ begin bench: process alias reset is << signal th.reset : std_logic >>; + alias hw_req is << signal th.hw_req : std_logic >>; + alias hw_ack is << signal th.hw_ack : std_logic >>; + alias hw_err is << signal th.hw_err : std_logic >>; variable status : std_logic_vector(31 downto 0); variable rdata : std_logic_vector(31 downto 0); variable dig : std_logic_vector(255 downto 0); + variable err : std_logic; variable expected : digest_t; variable msg : queue_t; @@ -175,6 +179,34 @@ begin check_equal(d, std_logic_vector(e), name); end procedure; + -- Raise the hardware request, wait for the acknowledge, drop it and + -- wait for the acknowledge to clear. Returns what the engine said. + procedure hw_request ( + variable err : out std_logic; + variable hw_status : out std_logic_vector(31 downto 0) + ) is + begin + -- register writes are queued; the request must see them landed + wait_until_idle(net, bus_handle); + hw_req <= '1'; + wait until hw_ack = '1'; + err := hw_err; + read_reg(net, HW_STATUS_OFFSET, hw_status); + hw_req <= '0'; + wait until hw_ack = '0'; + end procedure; + + procedure read_hw_digest ( + variable d : out std_logic_vector(255 downto 0) + ) is + variable w : std_logic_vector(31 downto 0); + begin + for i in 0 to 7 loop + read_reg(net, HW_DIGEST0_OFFSET + 4 * i, w); + d(32 * i + 31 downto 32 * i) := w; + end loop; + end procedure; + -- Drive a literal message through the manual path and check it against a -- published digest, rather than against the software sponge. This is the -- one place the engine is measured against the standard instead of @@ -336,6 +368,26 @@ begin -- and the engine still works afterwards run_local(0, 32, "hash after a rejected start"); + elsif run("reject_aux_on_single_flash") then + -- This bench builds the engine with the default NUM_FLASHES of + -- one, so asking for the second flash is a configuration error + -- like any other, and the engine must not go busy on it. + write_reg(net, CONFIG_OFFSET, pack(config_type'(source => AUX_QSPI))); + write_reg(net, PREPEND_OFFSET, To_StdLogicVector(0, 32)); + write_reg(net, LENGTH_OFFSET, To_StdLogicVector(64, 32)); + write_reg(net, FLASH_ADDR_OFFSET, To_StdLogicVector(0, 32)); + write_reg(net, CONTROL_OFFSET, START_CMD); + wait for 2 us; + + read_reg(net, STATUS_OFFSET, status); + check_equal((status and STATUS_CFG_ERR_MASK) /= (status'range => '0'), true, + "AUX_QSPI on a single-flash engine should set cfg_err"); + check_equal((status and STATUS_BUSY_MASK) = (status'range => '0'), true, + "should never go busy"); + + -- and the engine still works afterwards + run_local(0, 32, "hash after a rejected aux start"); + elsif run("reject_prepend_gt_length") then configure(CFG_LOCAL, 100, 50, 0); write_reg(net, CONTROL_OFFSET, START_CMD); @@ -380,6 +432,49 @@ begin run_flash(4, 80, 16#3000#, "second message, flash sourced"); run_local(8, 40, "third message, back to software"); + elsif run("hw_request_hashes_flash_range") then + -- A sequencer's request measures the programmed range and the + -- digest survives a later software run. + expected := sha3_256_digest(expected_msg(0, 700, true, 16#3000#)); + write_reg(net, HW_FLASH_ADDR_OFFSET, To_StdLogicVector(16#3000#, 32)); + write_reg(net, HW_LENGTH_OFFSET, To_StdLogicVector(700, 32)); + hw_request(err, status); + check_equal(err, '0', "hw request reported an error"); + check_equal((status and HW_STATUS_DONE_MASK) /= (status'range => '0'), true, "HW_STATUS.done"); + check_equal((status and HW_STATUS_BUSY_MASK) = (status'range => '0'), true, "HW_STATUS.busy clear"); + read_hw_digest(dig); + check_equal(dig, std_logic_vector(expected), "hw digest of 700 bytes at 0x3000"); + -- software digest is the same run's result too + read_digest(net, dig); + check_equal(dig, std_logic_vector(expected), "sw digest after hw run"); + + run_flash(0, 100, 16#0100#, "software run after the hw run"); + read_hw_digest(dig); + check_equal(dig, std_logic_vector(expected), "hw digest kept after a software run"); + + elsif run("hw_request_zero_length_is_an_error") then + -- An unprogrammed range: refused as a configuration error, + -- reported, and the engine is still usable afterwards. + hw_request(err, status); + check_equal(err, '1', "hw request should report an error"); + check_equal((status and HW_STATUS_CFG_ERR_MASK) /= (status'range => '0'), true, "HW_STATUS.cfg_err"); + check_equal((status and HW_STATUS_DONE_MASK) = (status'range => '0'), true, "HW_STATUS.done clear"); + run_flash(0, 64, 16#0200#, "software run after a refused hw request"); + + elsif run("hw_request_refused_while_software_busy") then + -- A software run that is waiting on data owns the engine; the + -- request is refused and the software run is left alone. + configure(CFG_LOCAL, 0, 8, 0); + write_reg(net, CONTROL_OFFSET, START_CMD); + write_reg(net, HW_LENGTH_OFFSET, To_StdLogicVector(64, 32)); + hw_request(err, status); + check_equal(err, '1', "hw request should be refused"); + check_equal((status and HW_STATUS_ENGINE_BUSY_MASK) /= (status'range => '0'), true, "HW_STATUS.engine_busy"); + read_reg(net, STATUS_OFFSET, status); + check_equal((status and STATUS_BUSY_MASK) /= (status'range => '0'), true, "software run still busy"); + write_reg(net, CONTROL_OFFSET, ABORT_CMD); + wait_not_busy(net, status); + elsif run("wfifo_full_backpressure") then -- Fill the software FIFO with the engine idle, so nothing drains -- it. Starting a hash first would not work: the core consumes a diff --git a/hdl/ip/vhd/hash_engine/sims/hash_engine_th.vhd b/hdl/ip/vhd/hash_engine/sims/hash_engine_th.vhd index e24c20fc..a09ca8f7 100644 --- a/hdl/ip/vhd/hash_engine/sims/hash_engine_th.vhd +++ b/hdl/ip/vhd/hash_engine/sims/hash_engine_th.vhd @@ -14,10 +14,9 @@ library vunit_lib; use work.axil8x32_pkg; use work.hash_engine_sim_pkg.all; --- The command and response FIFOs are real dcfifo_xpm instances here, not --- behavioural stand-ins, because an integrating design owns them exactly like the --- eSPI subsystem does. Neither is reset by the DUT: the engine resynchronises the --- response channel by draining it, not by flushing. +-- The engine owns its command and response FIFOs, so this harness only supplies +-- what sits on their far side: a behavioural flash responder where spi_nor_top +-- would be. hash_spi_nor_tb is the one that goes through the real controller. entity hash_engine_th is end entity; @@ -28,18 +27,18 @@ architecture th of hash_engine_th is signal axi_bus : axil8x32_pkg.axil_t; - signal cmd_fifo_wdata : std_logic_vector(31 downto 0); - signal cmd_fifo_write : std_logic; signal cmd_fifo_rdata : std_logic_vector(31 downto 0); - signal cmd_fifo_rdack : std_logic; - signal cmd_fifo_empty : std_logic; + signal cmd_fifo_rdack : std_logic_vector(0 downto 0); + signal cmd_fifo_empty : std_logic_vector(0 downto 0); signal rsp_fifo_wdata : std_logic_vector(7 downto 0); - signal rsp_fifo_write : std_logic; - signal rsp_fifo_wfull : std_logic; - signal rsp_fifo_rdata : std_logic_vector(7 downto 0); - signal rsp_fifo_rdack : std_logic; - signal rsp_fifo_empty : std_logic; + signal rsp_fifo_write : std_logic_vector(0 downto 0); + signal rsp_fifo_wfull : std_logic_vector(0 downto 0); + + -- Hardware request handshake, driven from the testbench + signal hw_req : std_logic := '0'; + signal hw_ack : std_logic; + signal hw_err : std_logic; begin @@ -74,66 +73,32 @@ begin dut: entity work.hash_engine_top port map ( - clk => clk, - reset => reset, - axi_if => axi_bus, - cmd_fifo_wdata => cmd_fifo_wdata, - cmd_fifo_write => cmd_fifo_write, - rsp_fifo_rdata => rsp_fifo_rdata, - rsp_fifo_rdack => rsp_fifo_rdack, - rsp_fifo_rempty => rsp_fifo_empty - ); - - cmd_fifo: entity work.dcfifo_xpm - generic map ( - fifo_write_depth => 256, - data_width => 32, - showahead_mode => true - ) - port map ( - wclk => clk, - reset => reset, - write_en => cmd_fifo_write, - wdata => cmd_fifo_wdata, - wfull => open, - wusedwds => open, - rclk => clk, - rdata => cmd_fifo_rdata, - rdreq => cmd_fifo_rdack, - rempty => cmd_fifo_empty, - rusedwds => open - ); - - rsp_fifo: entity work.dcfifo_xpm - generic map ( - fifo_write_depth => 256, - data_width => 8, - showahead_mode => true - ) - port map ( - wclk => clk, - reset => reset, - write_en => rsp_fifo_write, - wdata => rsp_fifo_wdata, - wfull => rsp_fifo_wfull, - wusedwds => open, - rclk => clk, - rdata => rsp_fifo_rdata, - rdreq => rsp_fifo_rdack, - rempty => rsp_fifo_empty, - rusedwds => open + clk => clk, + reset => reset, + axi_if => axi_bus, + hw_req => hw_req, + hw_ack => hw_ack, + hw_err => hw_err, + flash_cmd_rdata => cmd_fifo_rdata, + flash_cmd_rdack => cmd_fifo_rdack, + flash_cmd_rempty => cmd_fifo_empty, + flash_rsp_wdata => rsp_fifo_wdata, + flash_rsp_write => rsp_fifo_write, + flash_rsp_wfull => rsp_fifo_wfull ); + -- The engine owns the command and response FIFOs now; the responder sits + -- directly on their far ends, where spi_nor_top would in a real design. fake_flash: entity work.fake_flash_responder port map ( clk => clk, reset => reset, cmd_rdata => cmd_fifo_rdata, - cmd_rdack => cmd_fifo_rdack, - cmd_rempty => cmd_fifo_empty, - rsp_wdata => rsp_fifo_wdata, - rsp_write => rsp_fifo_write, - rsp_wfull => rsp_fifo_wfull + cmd_rdack => cmd_fifo_rdack(0), + cmd_rempty => cmd_fifo_empty(0), + rsp_wdata => rsp_fifo_wdata(7 downto 0), + rsp_write => rsp_fifo_write(0), + rsp_wfull => rsp_fifo_wfull(0) ); end th; diff --git a/hdl/ip/vhd/hash_engine/sims/hash_spi_nor_tb.vhd b/hdl/ip/vhd/hash_engine/sims/hash_spi_nor_tb.vhd index a11d1c85..562ad754 100644 --- a/hdl/ip/vhd/hash_engine/sims/hash_spi_nor_tb.vhd +++ b/hdl/ip/vhd/hash_engine/sims/hash_spi_nor_tb.vhd @@ -38,6 +38,8 @@ architecture tb of hash_spi_nor_tb is pack(control_type'(abort => '0', start => '1')); constant CFG_QSPI : std_logic_vector(31 downto 0) := pack(config_type'(source => HOST_QSPI)); + constant CFG_AUX : std_logic_vector(31 downto 0) := + pack(config_type'(source => AUX_QSPI)); begin @@ -46,7 +48,11 @@ begin bench: process alias reset is << signal th.reset : std_logic >>; - constant flash_actor : actor_t := find("spi_nor_target"); + constant flash_actor : actor_t := find("spi_nor_target0"); + -- The auxiliary flash is never filled, so it reads as erased 0xFF + -- everywhere. That is what tells a hash of it apart from the same + -- range on the host flash. + constant aux_actor : actor_t := find("spi_nor_target1"); variable status : std_logic_vector(31 downto 0); variable rdata : std_logic_vector(31 downto 0); @@ -65,11 +71,12 @@ begin end function; -- The message the engine should end up hashing: the 0xFF run, then the - -- flash from base_addr on. + -- flash from base_addr on. On the aux flash every byte is erased. impure function expected_msg ( prepend : natural; nbytes : natural; - base_addr : natural + base_addr : natural; + aux : boolean := false ) return queue_t is variable q : queue_t := new_queue; begin @@ -78,7 +85,11 @@ begin end loop; for i in 0 to nbytes - 1 loop - push_byte(q, to_integer(unsigned(flash_content(base_addr + i)))); + if aux then + push_byte(q, 16#FF#); + else + push_byte(q, to_integer(unsigned(flash_content(base_addr + i)))); + end if; end loop; return q; @@ -88,15 +99,20 @@ begin prepend : natural; nbytes : natural; base_addr : natural; - name : string + name : string; + aux : boolean := false ) is variable e : digest_t; variable s : std_logic_vector(31 downto 0); variable d : std_logic_vector(255 downto 0); begin - e := sha3_256_digest(expected_msg(prepend, nbytes, base_addr)); + e := sha3_256_digest(expected_msg(prepend, nbytes, base_addr, aux)); - write_reg(net, CONFIG_OFFSET, CFG_QSPI); + if aux then + write_reg(net, CONFIG_OFFSET, CFG_AUX); + else + write_reg(net, CONFIG_OFFSET, CFG_QSPI); + end if; write_reg(net, PREPEND_OFFSET, To_StdLogicVector(prepend, 32)); write_reg(net, LENGTH_OFFSET, To_StdLogicVector(prepend + nbytes, 32)); write_reg(net, FLASH_ADDR_OFFSET, To_StdLogicVector(base_addr, 32)); @@ -172,6 +188,20 @@ begin run_hash(0, 128, 16#1000#, "first fetch"); run_hash(0, 300, 16#3000#, "second fetch, crossing a chunk boundary"); run_hash(0, 64, 16#5000#, "third fetch"); + + elsif run("aux_flash_source") then + -- Same range on the other flash. The aux part is erased, so a + -- fetch that quietly went to the host flash would come back + -- with the pattern and fail the digest. + run_hash(0, 600, 16#1000#, "600 bytes from the aux flash", aux => true); + + elsif run("host_after_aux") then + -- The selection is latched per run; make sure it releases the + -- aux flash and the host flash's channel is still in step after + -- a run that never touched it. + run_hash(0, 300, 16#2000#, "aux first", aux => true); + run_hash(0, 300, 16#2000#, "then host"); + run_hash(0, 700, 16#1234#, "then aux again, unaligned", aux => true); end if; end loop; diff --git a/hdl/ip/vhd/hash_engine/sims/hash_spi_nor_th.vhd b/hdl/ip/vhd/hash_engine/sims/hash_spi_nor_th.vhd index e8827b89..94f8783c 100644 --- a/hdl/ip/vhd/hash_engine/sims/hash_spi_nor_th.vhd +++ b/hdl/ip/vhd/hash_engine/sims/hash_spi_nor_th.vhd @@ -38,35 +38,35 @@ end entity; architecture th of hash_spi_nor_th is + -- Two flashes, indexed the way the engine's flash ports are: 0 is the + -- host's, 1 the auxiliary one CONFIG.source = AUX_QSPI selects. + constant NUM_FLASHES : natural := 2; + signal clk : std_logic := '0'; signal reset : std_logic := '1'; signal axi_bus : axil8x32_pkg.axil_t; - signal spinor_axi : axil8x32_pkg.axil_t; + type spinor_axi_t is array (0 to NUM_FLASHES - 1) of axil8x32_pkg.axil_t; + signal spinor_axi : spinor_axi_t; - signal cmd_fifo_wdata : std_logic_vector(31 downto 0); - signal cmd_fifo_write : std_logic; signal cmd_fifo_rdata : std_logic_vector(31 downto 0); - signal cmd_fifo_rdack : std_logic; - signal cmd_fifo_empty : std_logic; - - signal rsp_fifo_wdata : std_logic_vector(7 downto 0); - signal rsp_fifo_write : std_logic; - signal rsp_fifo_rdata : std_logic_vector(7 downto 0); - signal rsp_fifo_rdack : std_logic; - signal rsp_fifo_empty : std_logic; - - signal cs_n : std_logic; - signal sclk : std_logic; - signal io : std_logic_vector(3 downto 0); - signal io_o : std_logic_vector(3 downto 0); - signal io_oe : std_logic_vector(3 downto 0); - - signal flash_o : std_logic_vector(3 downto 0); - signal flash_oe : std_logic_vector(3 downto 0); - signal io_flash : std_logic_vector(3 downto 0); - signal sclk_flash : std_logic; - signal csn_flash : std_logic; + signal cmd_fifo_rdack : std_logic_vector(NUM_FLASHES - 1 downto 0); + signal cmd_fifo_empty : std_logic_vector(NUM_FLASHES - 1 downto 0); + signal rsp_fifo_wdata : std_logic_vector(NUM_FLASHES * 8 - 1 downto 0); + signal rsp_fifo_write : std_logic_vector(NUM_FLASHES - 1 downto 0); + + type lanes_t is array (0 to NUM_FLASHES - 1) of std_logic_vector(3 downto 0); + signal cs_n : std_logic_vector(NUM_FLASHES - 1 downto 0); + signal sclk : std_logic_vector(NUM_FLASHES - 1 downto 0); + signal io : lanes_t; + signal io_o : lanes_t; + signal io_oe : lanes_t; + + signal flash_o : lanes_t; + signal flash_oe : lanes_t; + signal io_flash : lanes_t; + signal sclk_flash : std_logic_vector(NUM_FLASHES - 1 downto 0); + signal csn_flash : std_logic_vector(NUM_FLASHES - 1 downto 0); begin @@ -99,120 +99,91 @@ begin ); dut: entity work.hash_engine_top - port map ( - clk => clk, - reset => reset, - axi_if => axi_bus, - cmd_fifo_wdata => cmd_fifo_wdata, - cmd_fifo_write => cmd_fifo_write, - rsp_fifo_rdata => rsp_fifo_rdata, - rsp_fifo_rdack => rsp_fifo_rdack, - rsp_fifo_rempty => rsp_fifo_empty - ); - - cmd_fifo: entity work.dcfifo_xpm - generic map ( - fifo_write_depth => 256, - data_width => 32, - showahead_mode => true - ) - port map ( - wclk => clk, - reset => reset, - write_en => cmd_fifo_write, - wdata => cmd_fifo_wdata, - wfull => open, - wusedwds => open, - rclk => clk, - rdata => cmd_fifo_rdata, - rdreq => cmd_fifo_rdack, - rempty => cmd_fifo_empty, - rusedwds => open - ); - - rsp_fifo: entity work.dcfifo_xpm generic map ( - fifo_write_depth => 256, - data_width => 8, - showahead_mode => true + NUM_FLASHES => NUM_FLASHES ) port map ( - wclk => clk, - reset => reset, - write_en => rsp_fifo_write, - wdata => rsp_fifo_wdata, - wfull => open, - wusedwds => open, - rclk => clk, - rdata => rsp_fifo_rdata, - rdreq => rsp_fifo_rdack, - rempty => rsp_fifo_empty, - rusedwds => open + clk => clk, + reset => reset, + axi_if => axi_bus, + flash_cmd_rdata => cmd_fifo_rdata, + flash_cmd_rdack => cmd_fifo_rdack, + flash_cmd_rempty => cmd_fifo_empty, + flash_rsp_wdata => rsp_fifo_wdata, + flash_rsp_write => rsp_fifo_write, + flash_rsp_wfull => open ); - -- The SPI controller's own register interface is not exercised here, so park - -- its initiator side idle. sp5_owns_flash stays at its reset value of zero, - -- which means the hubris register path is nominally selected and the hash - -- client has to win the engine on its own. - spinor_axi.read_address.valid <= '0'; - spinor_axi.read_address.addr <= (others => '0'); - spinor_axi.read_data.ready <= '0'; - spinor_axi.write_address.valid <= '0'; - spinor_axi.write_address.addr <= (others => '0'); - spinor_axi.write_data.valid <= '0'; - spinor_axi.write_data.data <= (others => '0'); - spinor_axi.write_data.strb <= (others => '0'); - spinor_axi.write_response.ready <= '0'; - - spi_nor: entity work.spi_nor_top - port map ( - clk => clk, - reset => reset, - axi_if => spinor_axi, - cs_n => cs_n, - sclk => sclk, - io => io, - io_o => io_o, - io_oe => io_oe, - sp5_owns_flash => open, - espi_cmd_fifo_rdata => (others => '0'), - espi_cmd_fifo_rdack => open, - espi_cmd_fifo_rempty => '1', - espi_data_fifo_wdata => open, - espi_data_fifo_write => open, - hash_cmd_fifo_rdata => cmd_fifo_rdata, - hash_cmd_fifo_rdack => cmd_fifo_rdack, - hash_cmd_fifo_rempty => cmd_fifo_empty, - hash_data_fifo_wdata => rsp_fifo_wdata, - hash_data_fifo_write => rsp_fifo_write - ); - - -- Everything the part sees is delayed by out_delay; everything the DUT - -- captures is delayed again by in_delay coming back. - sclk_flash <= sclk after out_delay; - csn_flash <= cs_n after out_delay; - - flash: entity work.spi_nor_target_vc - generic map ( - actor_name => "spi_nor_target" - ) - port map ( - cs_n => csn_flash, - sclk => sclk_flash, - io => io_flash, - io_o => flash_o, - io_oe => flash_oe - ); - - -- Both ends contribute to the resolved bus at the part, plus a weak pull-up - -- for the board's. If both drive a lane the resolution goes to 'X', which the - -- controller shifts in and the digest check then catches. - bus_gen: for i in io_flash'range generate - io_flash(i) <= io_o(i) after out_delay when io_oe(i) = '1' else 'Z' after out_delay; - io_flash(i) <= flash_o(i) when flash_oe(i) = '1' else 'Z'; - io_flash(i) <= 'H'; + -- One spi_nor_top and one modelled part per flash, each hung off its own + -- pair of the engine's flash ports. The parts are told apart by actor + -- name; which of them a run reads is the whole point of the aux tests. + flashes: for f in 0 to NUM_FLASHES - 1 generate + constant actor : string := "spi_nor_target" & integer'image(f); + begin + -- The SPI controller's own register interface is not exercised here, + -- so park its initiator side idle. sp5_owns_flash stays at its reset + -- value of zero, which means the hubris register path is nominally + -- selected and the hash client has to win the engine on its own. + spinor_axi(f).read_address.valid <= '0'; + spinor_axi(f).read_address.addr <= (others => '0'); + spinor_axi(f).read_data.ready <= '0'; + spinor_axi(f).write_address.valid <= '0'; + spinor_axi(f).write_address.addr <= (others => '0'); + spinor_axi(f).write_data.valid <= '0'; + spinor_axi(f).write_data.data <= (others => '0'); + spinor_axi(f).write_data.strb <= (others => '0'); + spinor_axi(f).write_response.ready <= '0'; + + spi_nor: entity work.spi_nor_top + port map ( + clk => clk, + reset => reset, + axi_if => spinor_axi(f), + cs_n => cs_n(f), + sclk => sclk(f), + io => io(f), + io_o => io_o(f), + io_oe => io_oe(f), + sp5_owns_flash => open, + espi_cmd_fifo_rdata => (others => '0'), + espi_cmd_fifo_rdack => open, + espi_cmd_fifo_rempty => '1', + espi_data_fifo_wdata => open, + espi_data_fifo_write => open, + hash_cmd_fifo_rdata => cmd_fifo_rdata, + hash_cmd_fifo_rdack => cmd_fifo_rdack(f), + hash_cmd_fifo_rempty => cmd_fifo_empty(f), + hash_data_fifo_wdata => rsp_fifo_wdata(f * 8 + 7 downto f * 8), + hash_data_fifo_write => rsp_fifo_write(f) + ); + + -- Everything the part sees is delayed by out_delay; everything the DUT + -- captures is delayed again by in_delay coming back. + sclk_flash(f) <= sclk(f) after out_delay; + csn_flash(f) <= cs_n(f) after out_delay; + + flash: entity work.spi_nor_target_vc + generic map ( + actor_name => actor + ) + port map ( + cs_n => csn_flash(f), + sclk => sclk_flash(f), + io => io_flash(f), + io_o => flash_o(f), + io_oe => flash_oe(f) + ); + + -- Both ends contribute to the resolved bus at the part, plus a weak + -- pull-up for the board's. If both drive a lane the resolution goes to + -- 'X', which the controller shifts in and the digest check then catches. + bus_gen: for i in 0 to 3 generate + io_flash(f)(i) <= io_o(f)(i) after out_delay when io_oe(f)(i) = '1' else 'Z' after out_delay; + io_flash(f)(i) <= flash_o(f)(i) when flash_oe(f)(i) = '1' else 'Z'; + io_flash(f)(i) <= 'H'; + end generate; + + io(f) <= io_flash(f) after in_delay; end generate; - io <= io_flash after in_delay; - end th; diff --git a/hdl/ip/vhd/i2c/io_expanders/PCA9506ish/BUCK b/hdl/ip/vhd/i2c/io_expanders/PCA9506ish/BUCK index 3baf8cb4..21778d07 100644 --- a/hdl/ip/vhd/i2c/io_expanders/PCA9506ish/BUCK +++ b/hdl/ip/vhd/i2c/io_expanders/PCA9506ish/BUCK @@ -1,4 +1,4 @@ -load("//tools:hdl.bzl", "vhdl_unit", "vunit_sim") +load("//tools:hdl.bzl", "vhdl_unit", "vunit_sim", "sim_only_model") load("//tools:rdl.bzl", "rdl_file") rdl_file( @@ -19,11 +19,24 @@ vhdl_unit( visibility = ['PUBLIC'] ) +# The register-access helpers are useful to any testbench that talks to a +# PCA9506 over I2C, not just this one, so they are a target of their own. +sim_only_model( + name = "pca9506_sim_pkg", + srcs = ["sims/i2c_pca9506ish_sim_pkg.vhd"], + deps = [ + ":pca9506_regs_rdl", + "//hdl/ip/vhd/vunit_components:i2c_controller_vc", + ], + visibility = ['PUBLIC'], +) + vunit_sim( name = "i2c_pca9506ish_tb", - srcs = glob(["sims/**/*.vhd"]), + srcs = glob(["sims/**/*.vhd"], exclude = ["sims/i2c_pca9506ish_sim_pkg.vhd"]), deps = [ ":pca9506_top", + ":pca9506_sim_pkg", "//hdl/ip/vhd/vunit_components:i2c_controller_vc", "//hdl/ip/vhd/i2c/target:i2c_phy_consolidator", "//hdl/ip/vhd/axi_blocks:axilite_if_2k19", diff --git a/hdl/ip/vhd/spi_nor_controller/espi_txn/espi_flash_txn_mgr.vhd b/hdl/ip/vhd/spi_nor_controller/espi_txn/espi_flash_txn_mgr.vhd index 8ecc8105..ea6212f5 100644 --- a/hdl/ip/vhd/spi_nor_controller/espi_txn/espi_flash_txn_mgr.vhd +++ b/hdl/ip/vhd/spi_nor_controller/espi_txn/espi_flash_txn_mgr.vhd @@ -2,6 +2,13 @@ -- License, v. 2.0. If a copy of the MPL was not distributed with this -- file, You can obtain one at https://mozilla.org/MPL/2.0/. +-- Flash client for the eSPI flash channel. Commands arrive as two words: the +-- SP5's address, then a length word whose top nibble says what kind of +-- request it is (see flash_channel_pkg in the eSPI IP). Reads come back on +-- the data FIFO as flash bytes. Writes and erases run the whole flash side +-- sequence here (write enable, program or erase, poll until not busy) and +-- come back as a single status byte, zero for success, so the eSPI side can +-- form a completion without knowing anything about the flash part. library ieee; use ieee.std_logic_1164.all; @@ -38,13 +45,15 @@ entity espi_flash_txn_mgr is -- espi command espi_cmd: out spi_nor_cmd_t; spi_hw_busy : in std_logic; + -- High while the bytes the engine shifts out have to come from the + -- eSPI write payload FIFO rather than the hubris TX FIFO. + tx_from_espi : out std_logic; -- espi data fifo interface espi_flash_data_byte : out std_logic_vector(7 downto 0); flash_data_byte_write : out std_logic; -- Raw flash read_data flash_rdata : in std_logic_vector(7 downto 0); - flash_rdata_write : in std_logic; - -- + flash_rdata_write : in std_logic ); end entity; @@ -52,11 +61,33 @@ architecture rtl of espi_flash_txn_mgr is attribute mark_debug : string; constant max_flash_read_size : natural := 255; + constant page_bytes : natural := 256; constant fast_read_dummy_cycles : natural := 8; - type state_t is (idle, read_cmd_addr, read_cmd_len, issue_read, wait_for_data); + -- Request kinds, matching flash_channel_pkg.to_kind_bits + constant kind_read : std_logic_vector(3 downto 0) := x"0"; + constant kind_write : std_logic_vector(3 downto 0) := x"1"; + constant kind_erase : std_logic_vector(3 downto 0) := x"2"; + -- eSPI SAFS erase size codes carried in the length field + constant erase_4k : std_logic_vector(11 downto 0) := x"001"; + constant erase_64k : std_logic_vector(11 downto 0) := x"003"; + -- Status bytes reported back for writes and erases + constant status_ok : std_logic_vector(7 downto 0) := x"00"; + constant status_unsupported : std_logic_vector(7 downto 0) := x"01"; + constant status_timeout : std_logic_vector(7 downto 0) := x"02"; + -- Polls before a program or erase is given up on. Each poll is a short + -- transaction, hundreds of ns, so this is several seconds: comfortably + -- past a worst case 64kB block erase, but not forever if the part is + -- absent. + constant max_polls : natural := 2**24 - 1; + + type state_t is (idle, read_cmd_addr, read_cmd_len, wait_for_data, + wait_idle, issue_cmd, wait_done, report_status); + -- Which flash transaction the current request is up to + type step_t is (step_read, step_wren, step_program, step_erase, step_poll); type reg_t is record state : state_t; + step : step_t; cmd_rdack: std_logic; data_bytes: natural range 0 to 256; dummy_cycles: natural range 0 to 256; @@ -69,9 +100,14 @@ architecture rtl of espi_flash_txn_mgr is apob_end_addr : std_logic_vector(31 downto 0); next_flash_addr: std_logic_vector(31 downto 0); len: std_logic_vector(31 downto 0); + erase_op : std_logic_vector(7 downto 0); + is_erase : boolean; + status : std_logic_vector(7 downto 0); + polls : natural range 0 to max_polls; end record; constant reg_reset : reg_t := ( state => idle, + step => step_read, cmd_rdack => '0', data_bytes => 0, dummy_cycles => 0, @@ -83,40 +119,59 @@ architecture rtl of espi_flash_txn_mgr is cur_flash_addr => (others => '0'), apob_end_addr => (others => '0'), next_flash_addr => (others => '0'), - len => (others => '0') + len => (others => '0'), + erase_op => SECTOR_ERASE_4BYTE_OP, + is_erase => false, + status => status_ok, + polls => 0 ); signal r, rin: reg_t; attribute mark_debug of r : signal is "TRUE"; - + -- Bytes left in the page cur_addr sits in; a page program wraps inside + -- its page on the part, so a chunk never crosses one. + function bytes_to_page_end(addr : std_logic_vector(31 downto 0)) return natural is + begin + return page_bytes - to_integer(addr(7 downto 0)); + end function; begin espi_cmd.addr <=r.cur_flash_addr; espi_cmd.data_bytes <= To_Std_Logic_Vector(r.data_bytes, espi_cmd.data_bytes'length); espi_cmd.dummy_cycles <= To_Std_Logic_Vector(fast_read_dummy_cycles, espi_cmd.dummy_cycles'length); - espi_cmd.instr <= FAST_READ_4BYTE_QUAD_OP; - espi_cmd.go_flag <= '1' when r.state = issue_read and spi_hw_busy = '0' else '0'; + espi_cmd.instr <= FAST_READ_4BYTE_QUAD_OP when r.step = step_read else + WRITE_ENABLE_OP when r.step = step_wren else + QUAD_INPUT_PAGE_PROGRAM_4BYTE_OP when r.step = step_program else + r.erase_op when r.step = step_erase else + READ_STATUS_REG1_OP; + espi_cmd.go_flag <= '1' when r.state = issue_cmd else '0'; + tx_from_espi <= '1' when r.step = step_program and (r.state = issue_cmd or r.state = wait_done) else '0'; -- Turn the flash data we read back around into the data fifo going to the espi, -- but only when we're expecting data going to the espi block and not hubris FIFOs. - espi_flash_data_byte <= flash_rdata; - flash_data_byte_write <= flash_rdata_write when r.state = wait_for_data else '0'; + -- The status byte for a write or erase goes out the same way. + espi_flash_data_byte <= flash_rdata when r.state = wait_for_data else r.status; + flash_data_byte_write <= flash_rdata_write when r.state = wait_for_data else + '1' when r.state = report_status else + '0'; espi_cmd_fifo_rdack <= r.cmd_rdack; -- state machine that will pull 2 words from the command fifo. -- Word1: is the 32bit SP5 address, which we'll adjust to be the flash address when we pop it - -- Word2: is the transaction length in byte-count. + -- Word2: is the transaction length in byte-count, with the request kind in the top nibble. -- We're going to do page reads, so we'll need to do this in 256byte chunks so long as we have room -- in the data fifo. When we get to rem_bytes < 256 we'll do a final read of the remaining bytes. sm: process(all) variable v: reg_t; + variable kind : std_logic_vector(3 downto 0); begin v := r; -- single cycle flag(s) v.cmd_rdack := '0'; + kind := espi_cmd_fifo_rdata(31 downto 28); case r.state is when idle => @@ -160,23 +215,56 @@ begin v.state := read_cmd_len; when read_cmd_len => - -- This comes 1-indexed from the eSPI block, so we need to subtract 1 below - v.rem_bytes := to_integer(espi_cmd_fifo_rdata(11 downto 0)) - 1; - v.state := issue_read; - -- We're either going to issue the max page size, or the 0-indexed remaining bytes - -- which ever is smaller. - v.txn_bytes := minimum(v.rem_bytes, max_flash_read_size); - -- spi is 1-indexed still, so we need to add 1 here - v.data_bytes := v.txn_bytes + 1; - - when issue_read => - if spi_hw_busy = '0' then - v.state := wait_for_data; - -- Adjust info for a potential next read or so we can decide we're done later - v.rem_bytes := r.rem_bytes - r.txn_bytes; - v.next_flash_addr := r.cur_flash_addr + (r.txn_bytes + 1); - end if; - + v.status := status_ok; + v.polls := 0; + v.is_erase := kind = kind_erase; + case kind is + when kind_write => + -- 1-indexed byte count; the payload is already sitting + -- in the write FIFO in full. + v.rem_bytes := to_integer(espi_cmd_fifo_rdata(11 downto 0)); + v.step := step_wren; + v.data_bytes := 0; + if v.rem_bytes = 0 then + v.state := report_status; + else + v.state := wait_idle; + end if; + when kind_erase => + -- Length is the SAFS erase size code. Only the sizes + -- with a 4-byte-address opcode are offered; anything + -- else is reported back as unsupported. + v.step := step_wren; + v.data_bytes := 0; + v.state := wait_idle; + case espi_cmd_fifo_rdata(11 downto 0) is + when erase_4k => + v.erase_op := SECTOR_ERASE_4BYTE_OP; + when erase_64k => + v.erase_op := BLOCK_ERASE_64K_4BYTE_OP; + when others => + v.status := status_unsupported; + v.state := report_status; + end case; + when others => + -- This comes 1-indexed from the eSPI block, so we need to subtract 1 below. + -- The guard is for simulation: the fifo's showahead word is + -- still the address for a delta after the pop, and a zero + -- length would otherwise put -1 in a natural. + if espi_cmd_fifo_rdata(11 downto 0) = 0 then + v.rem_bytes := 0; + else + v.rem_bytes := to_integer(espi_cmd_fifo_rdata(11 downto 0)) - 1; + end if; + v.step := step_read; + v.state := wait_idle; + -- We're either going to issue the max page size, or the 0-indexed remaining bytes + -- which ever is smaller. + v.txn_bytes := minimum(v.rem_bytes, max_flash_read_size); + -- spi is 1-indexed still, so we need to add 1 here + v.data_bytes := v.txn_bytes + 1; + end case; + when wait_for_data => -- count down when we load data into the fifo if flash_rdata_write = '1' and r.txn_bytes > 0 then @@ -188,13 +276,98 @@ begin v.state := idle; -- last data for this part of the transaction else - v.state := issue_read; + v.state := wait_idle; v.cur_flash_addr := r.next_flash_addr; v.txn_bytes := minimum(v.rem_bytes, max_flash_read_size); v.data_bytes := v.txn_bytes + 1; end if; end if; + -- Each flash transaction is one trip around wait_idle -> + -- issue_cmd -> wait_done (or wait_for_data for a read chunk), + -- with r.step saying which one it is. + when wait_idle => + -- The previous transaction has to be completely finished + -- before asking for the next one, otherwise the busy rise + -- waited on below would be its cs_n, not ours. + if spi_hw_busy = '0' then + case r.step is + when step_program => + v.data_bytes := minimum(r.rem_bytes, bytes_to_page_end(r.cur_flash_addr)); + when step_poll => + v.data_bytes := 1; + when step_read => + null; -- already sized for this chunk + when others => + v.data_bytes := 0; + end case; + v.state := issue_cmd; + end if; + when issue_cmd => + -- Hold go until the engine pulls cs_n low. It ignores go during + -- its minimum cs_n high time, and busy is already low then, so + -- leaving on "not busy" would drop the command. + if spi_hw_busy = '1' then + case r.step is + when step_read => + v.state := wait_for_data; + -- Adjust info for a potential next read or so we can decide we're done later + v.rem_bytes := r.rem_bytes - r.txn_bytes; + v.next_flash_addr := r.cur_flash_addr + (r.txn_bytes + 1); + when step_program => + v.state := wait_done; + v.rem_bytes := r.rem_bytes - r.data_bytes; + v.next_flash_addr := r.cur_flash_addr + r.data_bytes; + when others => + v.state := wait_done; + end case; + end if; + when wait_done => + -- The one byte back from a status poll shows up here; the + -- part reports write-in-progress in bit 0. + if flash_rdata_write = '1' then + v.status := flash_rdata; + end if; + if spi_hw_busy = '0' then + case r.step is + when step_read => + null; -- reads finish out of wait_for_data + when step_wren => + if r.is_erase then + v.step := step_erase; + else + v.step := step_program; + end if; + v.state := wait_idle; + when step_program | step_erase => + v.step := step_poll; + v.state := wait_idle; + when step_poll => + if r.status(0) = '1' then + if r.polls = max_polls then + v.status := status_timeout; + v.state := report_status; + else + v.polls := r.polls + 1; + v.state := wait_idle; + end if; + elsif r.rem_bytes > 0 then + -- more pages of this write to go, each one + -- needs its own write enable + v.status := status_ok; + v.cur_flash_addr := r.next_flash_addr; + v.step := step_wren; + v.state := wait_idle; + else + v.status := status_ok; + v.state := report_status; + end if; + end case; + end if; + when report_status => + -- one status byte goes out on the data fifo this cycle + v.state := idle; + end case; -- easier to set up the combo stuff here so that we diff --git a/hdl/ip/vhd/spi_nor_controller/link/spi_clk_gen.vhd b/hdl/ip/vhd/spi_nor_controller/link/spi_clk_gen.vhd index e11fa293..e8b3e1a9 100644 --- a/hdl/ip/vhd/spi_nor_controller/link/spi_clk_gen.vhd +++ b/hdl/ip/vhd/spi_nor_controller/link/spi_clk_gen.vhd @@ -13,6 +13,10 @@ entity spi_clk_gen is reset : in std_logic; divisor : in unsigned(15 downto 0); enable : in boolean; + -- Low parks the pin copy at '0' regardless of what the internal clock + -- is doing, so a design that shares the flash with another master can + -- let go of the bus without a mux between the IOB flop and the pin. + bus_enable : in std_logic := '1'; -- For internal consumers: edge detection, phase counting, debug sclk : out std_logic; -- A second copy of the same flop, for the pin and nothing else. Both @@ -101,7 +105,7 @@ begin nxt_sclk := not sclk_int; end if; sclk_int <= nxt_sclk; -- assign value to output - sclk_pin <= nxt_sclk; -- IOB-resident duplicate, same edge + sclk_pin <= nxt_sclk and bus_enable; -- IOB-resident duplicate, same edge else sclk_int <= '0'; sclk_pin <= '0'; diff --git a/hdl/ip/vhd/spi_nor_controller/link/spi_link.vhd b/hdl/ip/vhd/spi_nor_controller/link/spi_link.vhd index c03edd0a..ebda533c 100644 --- a/hdl/ip/vhd/spi_nor_controller/link/spi_link.vhd +++ b/hdl/ip/vhd/spi_nor_controller/link/spi_link.vhd @@ -33,6 +33,8 @@ entity spi_link is -- Lanes to stop driving early, ahead of a controller-to-flash -- turnaround, so the two ends are never enabled at once release_lanes : in std_logic_vector(3 downto 0); + -- Low parks the pins: sclk low, lanes released. See spi_nor_top. + bus_enable : in std_logic := '1'; rx_byte : out std_logic_vector(7 downto 0); rx_byte_done : out boolean; -- The next byte to shift out. Must be held ready ahead of the reload @@ -170,6 +172,7 @@ begin reset => reset, divisor => divisor, enable => sclk_running, + bus_enable => bus_enable, sclk => sclk_int, sclk_pin => sclk_pin, sclk_fall_now => sclk_fall_now @@ -289,7 +292,11 @@ begin tx_reg <= nxt_tx_reg; io_o <= io_out_bits(nxt_tx_reg, nxt_mode); - io_oe <= oe and not release_lanes; + if bus_enable = '1' then + io_oe <= oe and not release_lanes; + else + io_oe <= (others => '0'); + end if; end if; end process; diff --git a/hdl/ip/vhd/spi_nor_controller/sims/spi_nor_espi_tb.vhd b/hdl/ip/vhd/spi_nor_controller/sims/spi_nor_espi_tb.vhd new file mode 100644 index 00000000..0af6a7fb --- /dev/null +++ b/hdl/ip/vhd/spi_nor_controller/sims/spi_nor_espi_tb.vhd @@ -0,0 +1,192 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; +use ieee.numeric_std_unsigned.all; + +library vunit_lib; + context vunit_lib.com_context; + context vunit_lib.vunit_context; + context vunit_lib.vc_context; +use work.spi_nor_tb_pkg.all; +use work.spi_nor_pkg.all; +use work.spi_nor_regs_pkg.all; +use work.spi_nor_target_vc_pkg.all; + +entity spi_nor_espi_tb is + generic ( + runner_cfg : string + ); +end entity; + +architecture tb of spi_nor_espi_tb is +begin + + th: entity work.spi_nor_espi_th; + + bench: process + alias reset is <>; + alias clk is <>; + alias cmd_wdata is <>; + alias cmd_write is <>; + alias payload_wdata is <>; + alias payload_write is <>; + alias data_rdata is <>; + alias data_rdack is <>; + alias data_rempty is <>; + constant flash : actor_t := find("spi_nor_target"); + + -- Request kinds, as flash_channel_pkg.to_kind_bits encodes them in + -- the top nibble of the length word + constant kind_read : std_logic_vector(3 downto 0) := x"0"; + constant kind_write : std_logic_vector(3 downto 0) := x"1"; + constant kind_erase : std_logic_vector(3 downto 0) := x"2"; + constant erase_4k : natural := 1; + constant erase_32k : natural := 2; + constant erase_64k : natural := 3; + + variable data : std_logic_vector(7 downto 0); + + -- Two-word command, the way the flash channel issues one + procedure put_cmd(constant addr : natural; constant kind : std_logic_vector(3 downto 0); constant len : natural) is + begin + wait until rising_edge(clk); + cmd_wdata <= std_logic_vector(to_unsigned(addr, 32)); + cmd_write <= '1'; + wait until rising_edge(clk); + cmd_wdata <= kind & std_logic_vector(to_unsigned(len, 28)); + wait until rising_edge(clk); + cmd_write <= '0'; + end procedure; + + procedure put_payload(constant addr : natural; constant len : natural) is + begin + wait until rising_edge(clk); + for i in 0 to len - 1 loop + payload_wdata <= pattern_byte(addr + i); + payload_write <= '1'; + wait until rising_edge(clk); + end loop; + payload_write <= '0'; + end procedure; + + procedure get_byte(variable b : out std_logic_vector(7 downto 0)) is + begin + loop + wait until rising_edge(clk); + exit when data_rempty = '0'; + end loop; + b := data_rdata; + data_rdack <= '1'; + wait until rising_edge(clk); + data_rdack <= '0'; + end procedure; + + -- Writes and erases report one status byte, zero for success + procedure check_status(constant expected : std_logic_vector(7 downto 0); constant msg : string) is + variable b : std_logic_vector(7 downto 0); + begin + get_byte(b); + check_equal(b, expected, msg); + end procedure; + + procedure check_flash_range(constant addr : natural; constant len : natural; constant erased : boolean; constant msg : string) is + variable b : std_logic_vector(7 downto 0); + begin + for i in 0 to len - 1 loop + read_flash_byte(net, flash, addr + i, b); + if erased then + check_equal(b, std_logic_vector'(x"FF"), msg & " @" & to_hstring(to_unsigned(addr + i, 32))); + else + check_equal(b, pattern_byte(addr + i), msg & " @" & to_hstring(to_unsigned(addr + i, 32))); + end if; + end loop; + end procedure; + + begin + test_runner_setup(runner, runner_cfg); + wait until reset = '0'; + wait for 500 ns; + -- The eSPI client only runs while the SP5 owns the flash + write_bus(net, bus_handle, To_StdLogicVector(SPICR_OFFSET + 16#100#, bus_handle.p_address_length), + SPICR_SP5_OWNS_FLASH_MASK); + wait_until_idle(net, bus_handle); + + while test_suite loop + if run("espi_read") then + fill_pattern(net, flash); + put_cmd(16#1000#, kind_read, 300); + for i in 0 to 299 loop + get_byte(data); + check_equal(data, pattern_byte(16#1000# + i), "read byte " & integer'image(i)); + end loop; + elsif run("espi_write_then_read_back") then + -- window starts erased + put_payload(16#1000#, 64); + put_cmd(16#1000#, kind_write, 64); + check_status(x"00", "write status"); + check_flash_range(16#0FF0#, 16, true, "before write"); + check_flash_range(16#1000#, 64, false, "written"); + check_flash_range(16#1040#, 16, true, "after write"); + -- and it comes back through the read path too + put_cmd(16#1000#, kind_read, 64); + for i in 0 to 63 loop + get_byte(data); + check_equal(data, pattern_byte(16#1000# + i), "read back byte " & integer'image(i)); + end loop; + elsif run("espi_write_crosses_page") then + -- A page program wraps inside its page on the part, so the + -- client has to split this into two programs itself + put_payload(16#10F0#, 64); + put_cmd(16#10F0#, kind_write, 64); + check_status(x"00", "write status"); + check_flash_range(16#1000#, 16, true, "start of first page untouched"); + check_flash_range(16#10F0#, 64, false, "written across the boundary"); + check_flash_range(16#1130#, 16, true, "after write"); + elsif run("espi_write_max_payload_run") then + -- Back to back writes of the channel's largest payload, as a + -- host streaming an image would issue them + for n in 0 to 7 loop + put_payload(16#2000# + n * 64, 64); + put_cmd(16#2000# + n * 64, kind_write, 64); + check_status(x"00", "write " & integer'image(n) & " status"); + end loop; + check_flash_range(16#2000#, 512, false, "streamed"); + elsif run("espi_erase_4k") then + fill_pattern(net, flash); + put_cmd(16#2000#, kind_erase, erase_4k); + check_status(x"00", "erase status"); + check_flash_range(16#1FF0#, 16, false, "before sector"); + check_flash_range(16#2000#, 16, true, "start of sector"); + check_flash_range(16#2FF0#, 16, true, "end of sector"); + check_flash_range(16#3000#, 16, false, "after sector"); + elsif run("espi_erase_64k") then + fill_pattern(net, flash); + put_cmd(16#0000#, kind_erase, erase_64k); + check_status(x"00", "erase status"); + check_flash_range(16#0000#, 16, true, "start of block"); + check_flash_range(16#FFF0#, 16, true, "end of block"); + elsif run("espi_erase_unsupported_size") then + fill_pattern(net, flash); + put_cmd(16#2000#, kind_erase, erase_32k); + check_status(x"01", "unsupported erase status"); + check_flash_range(16#2000#, 16, false, "untouched"); + -- the client is still alive afterwards + put_cmd(16#2000#, kind_read, 4); + for i in 0 to 3 loop + get_byte(data); + check_equal(data, pattern_byte(16#2000# + i), "read after refusal " & integer'image(i)); + end loop; + end if; + end loop; + + wait for 2 us; + test_runner_cleanup(runner); + wait; + end process; + + test_runner_watchdog(runner, 20 ms); +end tb; diff --git a/hdl/ip/vhd/spi_nor_controller/sims/spi_nor_espi_th.vhd b/hdl/ip/vhd/spi_nor_controller/sims/spi_nor_espi_th.vhd new file mode 100644 index 00000000..6f7a2bb2 --- /dev/null +++ b/hdl/ip/vhd/spi_nor_controller/sims/spi_nor_espi_th.vhd @@ -0,0 +1,218 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +-- Harness for the eSPI client side of spi_nor_top: the three FIFOs the eSPI +-- wrapper would put between the flash channel and this block, with their far +-- ends left for the testbench to drive directly, and the real flash VC on +-- the other side. spi_nor_th covers the hubris register path; this is the +-- path the SP5's SAFS reads, writes and erases take. + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +library vunit_lib; + context vunit_lib.com_context; + context vunit_lib.vunit_context; + context vunit_lib.vc_context; +use work.spi_nor_tb_pkg.all; +use work.axil_common_pkg.all; +use work.axil8x32_pkg; +use work.axil32x32_pkg; +use work.axil26x32_pkg; +use work.axilite_if_2k19_helper_pkg.all; + +entity spi_nor_espi_th is +end entity; + +architecture th of spi_nor_espi_th is + + signal clk : std_logic := '0'; + signal reset : std_logic := '1'; + signal axi_bus : axil26x32_pkg.axil_t; + signal cs_n : std_logic; + signal sclk : std_logic; + signal io : std_logic_vector(3 downto 0); + signal io_o : std_logic_vector(3 downto 0); + signal io_oe : std_logic_vector(3 downto 0); + signal flash_o : std_logic_vector(3 downto 0); + signal flash_oe : std_logic_vector(3 downto 0); + constant config_array : axil_responder_cfg_array_t(0 downto 0) := + ( + 0 => resp_cfg(base_addr => x"00000100", addr_span_bits => 8) + ); + signal responders : axil32x32_pkg.axil_array_t(0 downto 0); + signal responders_8b : axil8x32_pkg.axil_array_t(0 downto 0); + + -- Far ends of the FIFOs, driven and read by the testbench + signal cmd_wdata : std_logic_vector(31 downto 0) := (others => '0'); + signal cmd_write : std_logic := '0'; + signal payload_wdata : std_logic_vector(7 downto 0) := (others => '0'); + signal payload_write : std_logic := '0'; + signal data_rdata : std_logic_vector(7 downto 0); + signal data_rdack : std_logic := '0'; + signal data_rempty : std_logic; + + -- Near ends, into the DUT + signal espi_cmd_fifo_rdata : std_logic_vector(31 downto 0); + signal espi_cmd_fifo_rdack : std_logic; + signal espi_cmd_fifo_rempty : std_logic; + signal espi_data_fifo_wdata : std_logic_vector(7 downto 0); + signal espi_data_fifo_write : std_logic; + signal espi_wfifo_rdata : std_logic_vector(7 downto 0); + signal espi_wfifo_rdack : std_logic; + signal espi_wfifo_rempty : std_logic; + +begin + + clk <= not clk after 4 ns; + reset <= '0' after 200 ns; + + axi_lite_master_inst: entity vunit_lib.axi_lite_master + generic map ( + bus_handle => bus_handle + ) + port map ( + aclk => clk, + arready => axi_bus.read_address.ready, + arvalid => axi_bus.read_address.valid, + araddr => axi_bus.read_address.addr, + rready => axi_bus.read_data.ready, + rvalid => axi_bus.read_data.valid, + rdata => axi_bus.read_data.data, + rresp => axi_bus.read_data.resp, + awready => axi_bus.write_address.ready, + awvalid => axi_bus.write_address.valid, + awaddr => axi_bus.write_address.addr, + wready => axi_bus.write_data.ready, + wvalid => axi_bus.write_data.valid, + wdata => axi_bus.write_data.data, + wstrb => axi_bus.write_data.strb, + bvalid => axi_bus.write_response.valid, + bready => axi_bus.write_response.ready, + bresp => axi_bus.write_response.resp + ); + + axil_interconnect_inst: entity work.axil_interconnect + generic map( + config_array => config_array + ) + port map( + clk => clk, + reset => reset, + initiator => axi_bus, + responders => responders + ); + + resiser: entity work.axil8_resizer port map(fabric => responders(0), responder =>responders_8b(0)); + + cmd_fifo: entity work.dcfifo_xpm + generic map( + fifo_write_depth => 256, + data_width => 32, + showahead_mode => true + ) + port map( + wclk => clk, + reset => reset, + write_en => cmd_write, + wdata => cmd_wdata, + wfull => open, + wusedwds => open, + rclk => clk, + rdata => espi_cmd_fifo_rdata, + rdreq => espi_cmd_fifo_rdack, + rempty => espi_cmd_fifo_rempty, + rusedwds => open + ); + + payload_fifo: entity work.dcfifo_xpm + generic map( + fifo_write_depth => 1024, + data_width => 8, + showahead_mode => true + ) + port map( + wclk => clk, + reset => reset, + write_en => payload_write, + wdata => payload_wdata, + wfull => open, + wusedwds => open, + rclk => clk, + rdata => espi_wfifo_rdata, + rdreq => espi_wfifo_rdack, + rempty => espi_wfifo_rempty, + rusedwds => open + ); + + data_fifo: entity work.dcfifo_xpm + generic map( + fifo_write_depth => 4096, + data_width => 8, + showahead_mode => true + ) + port map( + wclk => clk, + reset => reset, + write_en => espi_data_fifo_write, + wdata => espi_data_fifo_wdata, + wfull => open, + wusedwds => open, + rclk => clk, + rdata => data_rdata, + rdreq => data_rdack, + rempty => data_rempty, + rusedwds => open + ); + + spi_nor_top_inst: entity work.spi_nor_top + port map ( + clk => clk, + reset => reset, + axi_if => responders_8b(0), + cs_n => cs_n, + sclk => sclk, + io => io, + io_o => io_o, + io_oe => io_oe, + sp5_owns_flash => open, + espi_cmd_fifo_rdata => espi_cmd_fifo_rdata, + espi_cmd_fifo_rdack => espi_cmd_fifo_rdack, + espi_cmd_fifo_rempty => espi_cmd_fifo_rempty, + espi_data_fifo_wdata => espi_data_fifo_wdata, + espi_data_fifo_write => espi_data_fifo_write, + espi_wfifo_rdata => espi_wfifo_rdata, + espi_wfifo_rdack => espi_wfifo_rdack, + espi_wfifo_rempty => espi_wfifo_rempty + ); + + flash: entity work.spi_nor_target_vc + generic map ( + actor_name => "spi_nor_target" + ) + port map ( + cs_n => cs_n, + sclk => sclk, + io => io, + io_o => flash_o, + io_oe => flash_oe + ); + + bus_gen: for i in io'range generate + io(i) <= io_o(i) when io_oe(i) = '1' else 'Z'; + io(i) <= flash_o(i) when flash_oe(i) = '1' else 'Z'; + io(i) <= 'H'; + end generate; + + contention_check: process(all) + begin + for i in io'range loop + assert not (io_oe(i) = '1' and flash_oe(i) = '1') + report "Bus contention: controller and flash both driving io(" & to_string(i) & ")" + severity error; + end loop; + end process; + +end th; diff --git a/hdl/ip/vhd/spi_nor_controller/spi_nor_top.vhd b/hdl/ip/vhd/spi_nor_controller/spi_nor_top.vhd index 01e0f616..bf2aa9a6 100644 --- a/hdl/ip/vhd/spi_nor_controller/spi_nor_top.vhd +++ b/hdl/ip/vhd/spi_nor_controller/spi_nor_top.vhd @@ -35,6 +35,14 @@ entity spi_nor_top is io_o : out std_logic_vector(3 downto 0); io_oe : out std_logic_vector(3 downto 0); sp5_owns_flash : out std_logic; + -- Low parks the flash pins (cs_n high, sclk low, lanes released) + -- at the IOB flops themselves, for a design where the flash is + -- reached through a mux shared with another master. The controller + -- keeps running; only the pins are held off, so a grant lost mid + -- transaction stops driving within a clock. Muxing after the flops + -- instead would cost them their IOB placement and the read timing + -- window that depends on it. + bus_enable : in std_logic := '1'; -- eSPI transaction interface. -- FIFO the command, which is simply an 32bit address -- as the first word and the transaction length as the @@ -46,6 +54,13 @@ entity spi_nor_top is -- requested address espi_data_fifo_wdata : out std_logic_vector(7 downto 0); espi_data_fifo_write : out std_logic; + -- Host to flash bytes for an eSPI write command. The eSPI side pushes + -- a whole payload before the command that consumes it, so this is + -- never read while empty. Tie rempty high on a design whose eSPI + -- instance cannot write. + espi_wfifo_rdata : in std_logic_vector(7 downto 0) := (others => '0'); + espi_wfifo_rdack : out std_logic; + espi_wfifo_rempty : in std_logic := '1'; -- Second flash read client, same command/response FIFO shape as the eSPI -- one above. Used by the hashing engine. Addresses here are raw: none of @@ -82,6 +97,10 @@ architecture rtl of spi_nor_top is signal rx_fifo_write8 : std_logic; signal tx_fifo_read8 : std_logic; signal tx_fifo_data8 : std_logic_vector(7 downto 0); + -- The byte stream the engine actually shifts out, and where it comes from + signal tx_byte : std_logic_vector(7 downto 0); + signal tx_byte_ack : std_logic; + signal tx_from_espi : std_logic; signal rx_fifo_wdat8 : std_logic_vector(7 downto 0); signal tx_fifo_data32 : std_logic_vector(31 downto 0); signal read_ack32 : std_logic; @@ -135,6 +154,7 @@ begin in_rx_phases => in_rx_phases, sclk_running => sclk_running, release_lanes => release_lanes, + bus_enable => bus_enable, rx_byte => link_rx_byte, rx_byte_done => rx_byte_done, tx_byte => link_tx_byte, @@ -167,6 +187,7 @@ begin -- link i/f cs_n => cs_n_internal, cs_n_pin => cs_n, + bus_enable => bus_enable, sclk => sclk_internal, rx_byte_done => rx_byte_done, rx_link_byte => link_rx_byte, @@ -178,8 +199,8 @@ begin sclk_running => sclk_running, release_lanes => release_lanes, cur_io_mode => cur_io_mode, - tx_fifo_ack => tx_fifo_read8, - tx_fifo_data => tx_fifo_data8, + tx_fifo_ack => tx_byte_ack, + tx_fifo_data => tx_byte, rx_fifo_data => rx_fifo_wdat8, rx_fifo_write => rx_fifo_write8 ); @@ -226,6 +247,13 @@ begin espi_cmd; sp5_owns_flash <= spicr_reg.sp5_owns_flash; + + -- Outbound bytes normally come from hubris' TX FIFO; during an eSPI page + -- program they come from the eSPI write payload FIFO instead. The engine + -- acks whichever it is reading. + tx_byte <= espi_wfifo_rdata when tx_from_espi = '1' else tx_fifo_data8; + tx_fifo_read8 <= tx_byte_ack when tx_from_espi = '0' else '0'; + espi_wfifo_rdack <= tx_byte_ack when tx_from_espi = '1' else '0'; -- TODO: this would be more simple with a mixed width fifo -- but this was faster than digging around making a new wrapper -- for now @@ -366,6 +394,7 @@ begin reset => reset, espi_cmd => espi_cmd, spi_hw_busy => spisr_reg.busy, + tx_from_espi => tx_from_espi, espi_reads_allowed => spicr_reg.sp5_owns_flash, sp_host_image_flash_addr_offset => signed(sp5_flash_offset.offset), amd_begin_apob_flash_addr => apob_flash_addr.offset, diff --git a/hdl/ip/vhd/spi_nor_controller/spi_txn/spi_txn_mgr.vhd b/hdl/ip/vhd/spi_nor_controller/spi_txn/spi_txn_mgr.vhd index aff99f20..62a4671d 100644 --- a/hdl/ip/vhd/spi_nor_controller/spi_txn/spi_txn_mgr.vhd +++ b/hdl/ip/vhd/spi_nor_controller/spi_txn/spi_txn_mgr.vhd @@ -33,6 +33,8 @@ entity spi_txn_mgr is -- Second copy of the cs_n flop, for the pin only, so it can be packed -- into the IOB. Same reasoning as spi_clk_gen's sclk_pin. cs_n_pin : out std_logic; + -- Low parks the pin copy high (deselected). See spi_nor_top. + bus_enable : in std_logic := '1'; sclk : in std_logic; rx_byte_done : in boolean; rx_link_byte : in std_logic_vector(7 downto 0); @@ -445,7 +447,7 @@ begin tx_pre <= next_tx(r, spi_cmd, tx_fifo_data); -- Duplicate of r.csn, driven from the same next-state value so the -- two flops always agree and change on the same edge. - cs_n_pin <= rin.csn; + cs_n_pin <= rin.csn or not bus_enable; end if; end process; diff --git a/hdl/projects/cosmo_seq/BUCK b/hdl/projects/cosmo_seq/BUCK index c58fbd34..89c5eed7 100644 --- a/hdl/projects/cosmo_seq/BUCK +++ b/hdl/projects/cosmo_seq/BUCK @@ -1,4 +1,4 @@ -load("//tools:hdl.bzl", "vhdl_unit", "black_box") +load("//tools:hdl.bzl", "vhdl_unit", "black_box", "vunit_sim") load("//tools:rdl.bzl", "rdl_file") load("//tools:vivado.bzl", "vivado_bitstream") @@ -35,15 +35,48 @@ vhdl_unit( visibility = ["PUBLIC"], ) +# Shared with metro_seq, which carries its own board_support but the same +# FMC clock arrangement. +vhdl_unit( + name = "fmc_clk_monitor", + srcs = ["board_support/fmc_clk_monitor.vhd"], + visibility = ["PUBLIC"], + standard = "2019", +) + +# Clocks, resets, the info block and the status LED. Nothing in here is +# cosmo-specific -- the other SP5 boards use the same 50MHz input and the same +# 125/200MHz tree -- so metro depends on this target and on the IP scripts +# exported below rather than carrying copies. vhdl_unit( name = "board_support", - srcs = glob(["board_support/*.vhd"], exclude = ["board_support/reset_sync.vhd"]), + srcs = glob(["board_support/*.vhd"], exclude = [ + "board_support/reset_sync.vhd", + "board_support/fmc_clk_monitor.vhd", + ]), deps = [ ":reset_sync", + ":fmc_clk_monitor", ":cosmo_black_boxes", "//hdl/ip/vhd/info:info", ], standard = "2019", + visibility = ["PUBLIC"], +) + +# The clock wizard bakes the module name into the generated netlist and into +# the clock names the timing constraints use, so a board that shares +# board_support has to generate the very same IP: these are the scripts for it. +export_file( + name = "sys_pll_ip_tcl", + src = "xilinx_ip_gen/sys_pll_ip.tcl", + visibility = ["PUBLIC"], +) + +export_file( + name = "fmc_pll_ip_tcl", + src = "xilinx_ip_gen/fmc_pll_ip.tcl", + visibility = ["PUBLIC"], ) @@ -57,6 +90,7 @@ vhdl_unit( "//hdl/ip/vhd/axi_blocks:axil_interconnect", "//hdl/ip/vhd/common:tristate_if_pkg", "//hdl/ip/vhd/fmc_if:stm32h7_fmc_target", + "//hdl/ip/vhd/hash_engine:hash_engine_top", "//hdl/projects/cosmo_seq/sp5_espi_flash_subsystem:sp5_espi_flash_subsystem", "//hdl/projects/cosmo_seq/sp5_uart_subsystem:sp5_uart_subsystem", "//hdl/projects/cosmo_seq/sequencer:sequencer", @@ -76,4 +110,14 @@ vivado_bitstream( constraints=glob(["*.xdc"]), pre_synth_tcl_files=glob(["xilinx_ip_gen/*.tcl"]), #post_synth_tcl_files=glob(["*ila.tcl"]), -) \ No newline at end of file +) + +# Analysis-only: vhdl_unit collects sources but does not run a compiler, so +# without this nothing type-checks cosmo_seq_top until Vivado synthesis. +vunit_sim( + name = "cosmo_seq_top_tb", + srcs = glob(["sims/*.vhd"]), + deps = [":cosmo_seq_top"], + visibility = ["PUBLIC"], + standard = "2019", +) diff --git a/hdl/projects/cosmo_seq/black_box_entities/fmc_pll.vhd b/hdl/projects/cosmo_seq/black_box_entities/fmc_pll.vhd new file mode 100644 index 00000000..032aebe3 --- /dev/null +++ b/hdl/projects/cosmo_seq/black_box_entities/fmc_pll.vhd @@ -0,0 +1,23 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +-- A no synth, no sim, black entity to make analysis happy. +-- Generated by xilinx_ip_gen/fmc_pll_ip.tcl (clk_wiz in phase-alignment +-- mode on the SP's FMC clock). + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; +use ieee.numeric_std_unsigned.all; + +entity fmc_pll is + port ( + clk_fmc_in : in std_logic; + clk_fmc : out std_logic; + clk_fmc_capture : out std_logic; + reset : in std_logic; + locked : out std_logic +); + +end entity; diff --git a/hdl/projects/cosmo_seq/black_box_entities/cosmo_pll.vhd b/hdl/projects/cosmo_seq/black_box_entities/sys_pll.vhd similarity index 96% rename from hdl/projects/cosmo_seq/black_box_entities/cosmo_pll.vhd rename to hdl/projects/cosmo_seq/black_box_entities/sys_pll.vhd index 2aad2efc..2bf47bf0 100644 --- a/hdl/projects/cosmo_seq/black_box_entities/cosmo_pll.vhd +++ b/hdl/projects/cosmo_seq/black_box_entities/sys_pll.vhd @@ -9,7 +9,7 @@ use ieee.std_logic_1164.all; use ieee.numeric_std.all; use ieee.numeric_std_unsigned.all; -entity cosmo_pll is +entity sys_pll is port ( clk_50m : in std_logic; clk_125m : out std_logic; diff --git a/hdl/projects/cosmo_seq/board_support/board_support_top.vhd b/hdl/projects/cosmo_seq/board_support/board_support_top.vhd index 0e13a17f..de38fc32 100644 --- a/hdl/projects/cosmo_seq/board_support/board_support_top.vhd +++ b/hdl/projects/cosmo_seq/board_support/board_support_top.vhd @@ -21,6 +21,11 @@ entity board_support is reset_125m : out std_logic; clk_200m : out std_logic; reset_200m : out std_logic; + -- deskewed/phase-shifted FMC clock from the FMC MMCM; everything in + -- the FMC domain must use this, not the raw pin + fmc_clk_buf : out std_logic; + -- later-phased sibling for the FMC input capture registers only + fmc_capture_clk_buf : out std_logic; reset_fmc : out std_logic; -- misc board signals fpga1_status_led : out std_logic; @@ -38,6 +43,10 @@ architecture rtl of board_support is signal sp_system_reset_syncd : std_logic; signal pll_locked_async : std_logic; signal led_counter : unsigned(27 downto 0); + signal fmc_clk_g : std_logic; + signal fmc_capture_clk_g : std_logic; + signal fmc_mmcm_locked : std_logic; + signal fmc_mmcm_reset : std_logic; begin @@ -66,7 +75,7 @@ begin ); -- Xilinx PLL instantiation - pll: entity work.cosmo_pll + pll: entity work.sys_pll port map ( clk_50m => board_50mhz_clk, clk_125m => clk_125m, @@ -76,15 +85,46 @@ begin ); - -- Reset synchronizer into the clock domains + -- MMCM on the SP's (continuous) FMC clock: BUFG-in-feedback deskew plus + -- a small phase shift, which is what closes the single-cycle FMC pin + -- timing at 10 ns. See xilinx_ip_gen/fmc_pll_ip.tcl for the VCO and + -- phase reasoning. + fmc_pll_inst: entity work.fmc_pll + port map( + clk_fmc_in => sp_fmc_clk, + clk_fmc => fmc_clk_g, + clk_fmc_capture => fmc_capture_clk_g, + reset => fmc_mmcm_reset, + locked => fmc_mmcm_locked + ); + + -- Hold the MMCM in reset while the SP's clock is stopped (SP reset or + -- reconfiguration) and retry the lock if the input frequency changes. + fmc_clk_monitor_inst: entity work.fmc_clk_monitor + port map( + clk => clk_125m, + reset => reset_125m, + fmc_clk_raw => sp_fmc_clk, + mmcm_locked => fmc_mmcm_locked, + mmcm_reset => fmc_mmcm_reset + ); + + fmc_clk_buf <= fmc_clk_g; + fmc_capture_clk_buf <= fmc_capture_clk_g; + + -- Reset synchronizer into the clock domains. The FMC branch is clocked + -- by the MMCM output and additionally gated on MMCM lock: while + -- unlocked there are no FMC-domain clock edges and the async assert is + -- what keeps the FMC target's bus drive released. reset_sync_inst: entity work.reset_sync port map( pll_locked_async => pll_locked_async, + aux_locked_async => fmc_mmcm_locked, clk_125m => clk_125m, reset_125m => reset_125m, clk_200m => clk_200m, reset_200m => reset_200m, - sp_fmc_clk => sp_fmc_clk, + sp_fmc_clk => fmc_clk_g, reset_fmc_clk => reset_fmc ); diff --git a/hdl/projects/cosmo_seq/board_support/fmc_clk_monitor.vhd b/hdl/projects/cosmo_seq/board_support/fmc_clk_monitor.vhd new file mode 100644 index 00000000..5a99b062 --- /dev/null +++ b/hdl/projects/cosmo_seq/board_support/fmc_clk_monitor.vhd @@ -0,0 +1,124 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +-- Supervises the MMCM that sits on the SP's continuous FMC clock. +-- +-- The SP's clock stops whenever the SP resets or reconfigures, and an +-- MMCME2 whose input clock went away is not guaranteed to relock on its +-- own when the clock returns -- AMD's guidance is to assert its reset +-- across a clock interruption. This block watches the raw clock pin as +-- ordinary data in the always-running 125 MHz domain and holds the MMCM +-- in reset while the clock is absent, releasing it (and letting it +-- relock) once the clock is back. While the MMCM is unlocked the FMC +-- domain has no clock and reset_fmc is held asserted (see reset_sync), +-- so the wait line sits asserted and the SP's first post-reset access +-- simply stalls until lock -- nothing needs to sequence against this. +-- +-- The transition count is aliased (a 100 MHz toggle sampled at 125 MHz) +-- so it is a presence detector, not a frequency measurement: an absent +-- clock counts zero transitions, any live SP rate counts hundreds per +-- window. Frequency changes (the CLKDIV flip) drop the MMCM's own LOCKED +-- while the clock keeps toggling; the relock timer covers that case by +-- pulsing reset if LOCKED stays low too long with a clock present. + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +entity fmc_clk_monitor is + generic ( + -- observation window, in clk cycles (256 @ 125 MHz ~= 2 us) + window_cycles : positive := 256; + -- fewer transitions than this in a window means "clock absent" + min_transitions : positive := 16; + -- windows of LOCKED low with a live clock before a relock retry + -- (128 windows ~= 260 us, comfortably past the ~100 us max lock time) + relock_windows : positive := 128; + -- windows of live clock required before releasing mmcm_reset + settle_windows : positive := 4 + ); + port ( + clk : in std_logic; + reset : in std_logic; + -- the FMC clock pin, sampled as data + fmc_clk_raw : in std_logic; + -- LOCKED from the MMCM, async + mmcm_locked : in std_logic; + mmcm_reset : out std_logic + ); +end entity; + +architecture rtl of fmc_clk_monitor is + + signal raw_meta : std_logic_vector(1 downto 0); + signal raw_prev : std_logic; + signal locked_meta : std_logic_vector(1 downto 0); + + signal window_cntr : natural range 0 to window_cycles - 1; + signal transition_cntr : natural range 0 to window_cycles; + signal clk_present : boolean; + + signal settle_cntr : natural range 0 to settle_windows; + signal unlock_cntr : natural range 0 to relock_windows; + +begin + + monitor: process(clk, reset) + begin + if reset then + raw_meta <= (others => '0'); + raw_prev <= '0'; + locked_meta <= (others => '0'); + window_cntr <= 0; + transition_cntr <= 0; + clk_present <= false; + settle_cntr <= 0; + unlock_cntr <= 0; + mmcm_reset <= '1'; + elsif rising_edge(clk) then + raw_meta <= raw_meta(0) & fmc_clk_raw; + raw_prev <= raw_meta(1); + locked_meta <= locked_meta(0) & mmcm_locked; + + if window_cntr = window_cycles - 1 then + window_cntr <= 0; + clk_present <= transition_cntr >= min_transitions; + transition_cntr <= 0; + + if transition_cntr < min_transitions then + -- clock gone: hold the MMCM in reset and start over + mmcm_reset <= '1'; + settle_cntr <= 0; + unlock_cntr <= 0; + elsif settle_cntr /= settle_windows then + -- clock is back; give it a few clean windows before + -- releasing the MMCM + settle_cntr <= settle_cntr + 1; + unlock_cntr <= 0; + elsif mmcm_reset = '1' then + mmcm_reset <= '0'; + elsif locked_meta(1) = '0' then + -- clock alive but no lock (e.g. the input frequency + -- changed): retry after a generous wait + if unlock_cntr = relock_windows then + mmcm_reset <= '1'; + settle_cntr <= 0; + unlock_cntr <= 0; + else + unlock_cntr <= unlock_cntr + 1; + end if; + else + unlock_cntr <= 0; + end if; + else + window_cntr <= window_cntr + 1; + if (raw_meta(1) xor raw_prev) = '1' + and transition_cntr /= window_cycles then + transition_cntr <= transition_cntr + 1; + end if; + end if; + end if; + end process; + +end architecture; diff --git a/hdl/projects/cosmo_seq/board_support/reset_sync.vhd b/hdl/projects/cosmo_seq/board_support/reset_sync.vhd index d1d244da..043f9c29 100644 --- a/hdl/projects/cosmo_seq/board_support/reset_sync.vhd +++ b/hdl/projects/cosmo_seq/board_support/reset_sync.vhd @@ -10,6 +10,13 @@ use ieee.numeric_std.all; entity reset_sync is port ( pll_locked_async : in std_logic; + -- Additional lock qualifier for the FMC domain (the FMC MMCM on + -- cosmo). Defaults to "locked" so boards without one (grapefruit) + -- are unaffected. Asserting reset on lock loss happens + -- asynchronously in the bridge, which matters: when this MMCM + -- unlocks the FMC domain has no clock edges, and the async assert + -- is what still clears the FMC target's bus drive. + aux_locked_async : in std_logic := '1'; clk_125m : in std_logic; reset_125m : out std_logic; @@ -50,7 +57,7 @@ begin ) port map( clk => sp_fmc_clk, - reset_async => pll_locked_async, + reset_async => pll_locked_async and aux_locked_async, reset_sync => reset_fmc_clk ); diff --git a/hdl/projects/cosmo_seq/cosmo_seq_top.vhd b/hdl/projects/cosmo_seq/cosmo_seq_top.vhd index 94b8fbb4..ee2fdb6b 100644 --- a/hdl/projects/cosmo_seq/cosmo_seq_top.vhd +++ b/hdl/projects/cosmo_seq/cosmo_seq_top.vhd @@ -21,6 +21,7 @@ use work.axi_st8_pkg; use work.time_pkg.all; use work.tristate_if_pkg.all; +use work.sp5_power_pkg.all; use work.sequencer_io_pkg.all; use work.sp5_uart_subsystem_pkg.all; @@ -335,6 +336,10 @@ architecture rtl of cosmo_seq_top is signal v1p2_nic_enet_a0hp_pg : std_logic; signal v1p1_nic_enet_a0hp_pg : std_logic; alias fmc_clk : std_logic is fmc_sp_to_fpga1_clk; + -- deskewed/phase-shifted FMC clock from the MMCM in board_support; the + -- FMC domain runs on this, never on the raw pin + signal fmc_clk_buf : std_logic; + signal fmc_capture_clk_buf : std_logic; constant INFO_RESP_IDX : integer := 0; constant SPINOR_RESP_IDX: integer := 1; constant SEQ_RESP_IDX: integer := 2; @@ -364,12 +369,19 @@ architecture rtl of cosmo_seq_top is signal responders_8b : axil8x32_pkg.axil_array_t(config_array'range); signal responders_15b : axil15x32_pkg.axil_array_t(config_array'range); signal fmc_internal_data_out : std_logic_vector(15 downto 0); - signal fmc_data_out_enable: std_logic; + signal fmc_data_out_hiz: std_logic_vector(15 downto 0); signal spinor_io_o : std_logic_vector(3 downto 0); signal spinor_io_oe : std_logic_vector(3 downto 0); signal espi_io_o : std_logic_vector(3 downto 0); signal espi_io_oe : std_logic_vector(3 downto 0); + -- hash engine <-> spi_nor flash client port(s) + constant HASH_NUM_FLASHES : natural := 1; + signal hash_flash_cmd_rdata : std_logic_vector(31 downto 0); + signal hash_flash_cmd_rdack : std_logic_vector(HASH_NUM_FLASHES - 1 downto 0); + signal hash_flash_cmd_rempty : std_logic_vector(HASH_NUM_FLASHES - 1 downto 0); + signal hash_flash_rsp_wdata : std_logic_vector(HASH_NUM_FLASHES * 8 - 1 downto 0); + signal hash_flash_rsp_write : std_logic_vector(HASH_NUM_FLASHES - 1 downto 0); signal ipcc_uart_from_espi_axi_st : axi_st8_pkg.axi_st_t; signal ipcc_uart_to_espi_axi_st : axi_st8_pkg.axi_st_t; @@ -415,7 +427,12 @@ architecture rtl of cosmo_seq_top is alias a0_ok_to_fpga2 : std_logic is fpga1_to_fpga2_io(2); signal uart_dbg_if : uart_dbg_t; signal allow_backplane_pcie_clk : std_logic; - signal nic_dbg_pins : t6_debug_if; + signal nic_dbg_pins : nic_debug_if; + -- No Versal on this board; the shared sequencer's Versal ports are tied + -- off to these and its Versal outputs left open. + signal versal_rails_unused : versal_power_t := versal_power_absent; + signal versal_boot_unused : versal_boot_t := versal_boot_absent; + signal versal_pcie_unused : versal_pcie_t := versal_pcie_absent; signal reg_alert_l_pins : seq_power_alert_pins_t; signal is_rev1 : std_logic; signal dbg_pins_uart_out : std_logic; @@ -470,25 +487,32 @@ begin stm32h7_fmc_target_inst: entity work.stm32h7_fmc_target port map( chip_reset => reset_fmc, - fmc_clk => fmc_clk, + fmc_clk => fmc_clk_buf, + fmc_capture_clk => fmc_capture_clk_buf, a(24 downto 20) => "00000", a(19 downto 16) => fmc_sp_to_fpga1_a(19 downto 16), --a(23 downto 16) => fmc_sp_to_fpga1_a, addr_data_in => fmc_sp_to_fpga1_da, data_out => fmc_internal_data_out, - data_out_en => fmc_data_out_enable, + data_out_hiz => fmc_data_out_hiz, ne(3 downto 1) => "111", ne(0) => fmc_sp_to_fpga1_cs_l, noe => fmc_sp_to_fpga1_oe_l, nwe => fmc_sp_to_fpga1_we_l, nl => fmc_sp_to_fpga1_adv_l, nwait => fmc_sp_to_fpga1_wait_l, + timeout_count => open, + contention_count => open, aclk => clk_125m, aresetn => not reset_125m, axi_if => fmc_axi_if ); -- tristate control for the FMC data bus - fmc_sp_to_fpga1_da <= fmc_internal_data_out when fmc_data_out_enable = '1' else (others => 'Z'); + -- per-bit tristate, hiz already in OBUFT T polarity so each pin's T + -- flop packs into its IOB with no inverter in between + fmc_da_tris: for i in fmc_sp_to_fpga1_da'range generate + fmc_sp_to_fpga1_da(i) <= 'Z' when fmc_data_out_hiz(i) = '1' else fmc_internal_data_out(i); + end generate; -- Axi decode/interconnect axil_interconnect_inst: entity work.axil_interconnect @@ -510,6 +534,8 @@ begin port map( board_50mhz_clk => clk_50mhz_fpga1_1, sp_fmc_clk => fmc_clk, + fmc_clk_buf => fmc_clk_buf, + fmc_capture_clk_buf => fmc_capture_clk_buf, sp_system_reset_l => sp_to_fpga1_system_reset_l, clk_125m => clk_125m, reset_125m => reset_125m, @@ -550,7 +576,29 @@ begin spi_nor_dat => spi_fpga1_to_flash_dat, spi_nor_dat_o => spinor_io_o, spi_nor_dat_oe => spinor_io_oe, - hash_axi_if => responders_8b(HASH_RESP_IDX) + hash_cmd_fifo_rdata => hash_flash_cmd_rdata, + hash_cmd_fifo_rdack => hash_flash_cmd_rdack(0), + hash_cmd_fifo_rempty => hash_flash_cmd_rempty(0), + hash_data_fifo_wdata => hash_flash_rsp_wdata(7 downto 0), + hash_data_fifo_write => hash_flash_rsp_write(0) + ); + + -- SHA3 hashing engine. It reads flash through spi_nor_top's second client + -- port and owns the FIFOs on that path; it sits here rather than inside the + -- eSPI wrapper so one engine can serve more than one flash. + hash_engine_inst: entity work.hash_engine_top + generic map( + NUM_FLASHES => HASH_NUM_FLASHES + ) + port map( + clk => clk_125m, + reset => reset_125m, + axi_if => responders_8b(HASH_RESP_IDX), + flash_cmd_rdata => hash_flash_cmd_rdata, + flash_cmd_rdack => hash_flash_cmd_rdack, + flash_cmd_rempty => hash_flash_cmd_rempty, + flash_rsp_wdata => hash_flash_rsp_wdata, + flash_rsp_write => hash_flash_rsp_write ); --Tristates for spi-nor flash pins and espi spi_nor_espi_tris:process(all) @@ -724,7 +772,8 @@ begin resize_axil(fabric_responders(SEQ_RESP_IDX), responders_8b(SEQ_RESP_IDX)); seq: entity work.sp5_sequencer generic map( - CNTS_P_MS => calc_ms(desired_ms => 1, clk_period_ns => 8) + CNTS_P_MS => calc_ms(desired_ms => 1, clk_period_ns => 8), + NIC_KIND => NIC_T6 ) port map( clk => clk_125m, @@ -742,9 +791,15 @@ begin sp5_seq_pins => sp5_seq_pins, nic_rails_pins => nic_rails, nic_seq_pins => nic_seq_pins, + versal_rails_pins => versal_rails_unused, + versal_boot_pins => versal_boot_unused, + versal_pcie_pins => versal_pcie_unused, + versal_held_in_reset => open, + flash_owned_by_seq => open, + hash_req => open, nic_dbg_pins => nic_dbg_pins, - sp5_t6_perst_l => sp5_t6_perst_l, - sp5_t6_faulted => sp5_t6_faulted, + sp5_nic_perst_l => sp5_t6_perst_l, + sp5_nic_faulted => sp5_t6_faulted, ignition_mux_sel => fpga1_to_sp_mux_ign_mux_sel, ignition_creset => fpga1_to_ign_trgt_fpga_creset, reg_alert_l_pins => reg_alert_l_pins @@ -858,6 +913,7 @@ begin reg_alert_l_pins.v0p96_nic_to_fpga1_alert_l <= v0p96_nic_to_fpga1_alert_l; reg_alert_l_pins.pwr_cont2_to_fpga1_alert_l <= pwr_cont2_to_fpga1_alert_l; reg_alert_l_pins.pwr_cont3_to_fpga1_alert_l <= pwr_cont3_to_fpga1_alert_l; + reg_alert_l_pins.pwr_cont4_to_fpga1_alert_l <= '1'; -- no fourth controller on this board resize_axil(fabric_responders(SPD_PROXY_RESP_IDX), responders_8b(SPD_PROXY_RESP_IDX)); dimm_spd_proxy_top_inst: entity work.dimms_subsystem_top diff --git a/hdl/projects/cosmo_seq/cosmo_timing.xdc b/hdl/projects/cosmo_seq/cosmo_timing.xdc index 8dae48f1..d47fc0e6 100644 --- a/hdl/projects/cosmo_seq/cosmo_timing.xdc +++ b/hdl/projects/cosmo_seq/cosmo_timing.xdc @@ -1,20 +1,27 @@ create_clock -add -name sys_clk_pin -period 20.000 -waveform {0 10.000} [get_ports { clk_50mhz_fpga1_1 }]; -create_clock -add -name fmc_clk_pin -period 15.000 -waveform {0 7.500} [get_ports { fmc_sp_to_fpga1_clk }]; +create_clock -add -name fmc_clk_pin -period 10.000 -waveform {0 5.000} [get_ports { fmc_sp_to_fpga1_clk }]; # # FMC interface constraints # Create a virtual clock, to represent the source clock of the FMC interface -create_clock -name fmc_virt_clk -period 15.000; +create_clock -name fmc_virt_clk -period 10.000; -set_clock_groups -asynchronous -group {fmc_clk_pin fmc_virt_clk} -group {clk_125m_cosmo_pll} -group {clk_200m_cosmo_pll} +# The FMC MMCM's output clock is derived from fmc_clk_pin and must stay in +# the synchronous group with it and the virtual clock. +set_clock_groups -asynchronous -group [get_clocks -include_generated_clocks {fmc_clk_pin fmc_virt_clk}] -group {clk_125m_sys_pll} -group {clk_200m_sys_pll} # ####################### # FMC Interface # ####################### -# SP output a continuous clock here. -# The FMC interface is clocked at 66.67MHz, which is a 15ns period. +# SP outputs a continuous clock here. +# The FMC interface is constrained at 100MHz (10ns period), the CLKDIV=1 +# target rate; the same analysis is a strict superset of 50 and 66.67 MHz +# operation. The internal FMC domain clock comes from an MMCM in +# phase-alignment mode (see xilinx_ip_gen/fmc_pll_ip.tcl), so the flops +# see the pin clock plus the deliberate +45deg phase shift and STA +# accounts for it via the generated clock. # FPGA's input delays have to be low enough that they don't run into the uncertainty region due to any possible skew. # skew_bre is the shortest trace delay vs the clock, and skew_are is the longest trace delay vs the clock. # On cosmo, clock trace is 60.787 rev1, 53.026mm rev2 . @@ -57,17 +64,21 @@ set min_wait_delay 0.3635 # Source sync so external_clk_delay is 0. # Setup time is 1ns, and we include the 1/2 period due to SP shifting the data out on the falling edge. -set sp_output_half_period 7.5 +set sp_output_half_period 5.0 set sp_0_hold 0 set sp_clk_delay 0 -# We have our 1/2 period of 7.5 ns due to SP outputting on falling edges, plus the td in the datasheet +# We have our 1/2 period of 5 ns due to SP outputting on falling edges, plus the td in the datasheet set td_clkl_nehl 1 set nl_output_delay [expr {$sp_output_half_period + $td_clkl_nehl}] set max_nl [expr {$sp_clk_delay + $nl_output_delay + $max_data_delay - $min_clock_delay}] -# latest clock, earliest data. We assume a hold time of 0 for the SP. +# latest clock, earliest data. We assume a hold time of 0 for the SP, but the SP +# holds its outputs until the *next falling edge*, so the earliest change is a +# half period after the capture edge. Without that term Vivado assumes the data +# can change at the capture edge itself, which manufactures a phantom +# input-hold requirement of nearly a half period. # min external: fastest data, slowest clock -set min_nl [expr {$sp_clk_delay + $sp_0_hold + $min_data_delay - $max_clock_delay}] +set min_nl [expr {$sp_clk_delay + $sp_output_half_period + $sp_0_hold + $min_data_delay - $max_clock_delay}] # Apply to all of these pins with similar or better timing relationships. set_input_delay -clock fmc_virt_clk -max $max_nl [get_ports fmc_sp_to_fpga1_cs_l] @@ -85,8 +96,8 @@ set_input_delay -clock fmc_virt_clk -min $min_nl [get_ports fmc_sp_to_fpga1_bl_l set td_clkl_av 2.5 set a_output_delay [expr {$sp_output_half_period + $td_clkl_av}] set max_a [expr {$sp_clk_delay + $a_output_delay + $max_data_delay - $min_clock_delay}] -# Still 0 hold on these pins. -set min_a [expr {$sp_clk_delay + $sp_0_hold + $min_data_delay - $max_clock_delay}] +# Still 0 hold on these pins, held to the next falling edge as above. +set min_a [expr {$sp_clk_delay + $sp_output_half_period + $sp_0_hold + $min_data_delay - $max_clock_delay}] set_input_delay -clock fmc_virt_clk -max $max_a [get_ports fmc_sp_to_fpga1_a[*]] set_input_delay -clock fmc_virt_clk -min $min_a [get_ports fmc_sp_to_fpga1_a[*]] @@ -94,11 +105,23 @@ set_input_delay -clock fmc_virt_clk -min $min_a [get_ports fmc_sp_to_fpga1_a[*]] set td_clkl_adv 3 set ad_output_delay [expr {$sp_output_half_period + $td_clkl_adv}] set max_ad [expr {$sp_clk_delay + $ad_output_delay + $max_data_delay - $min_clock_delay}] -# Still 0 hold on these pins. -set min_ad [expr {$sp_clk_delay + $sp_0_hold + $min_data_delay - $max_clock_delay}] +# Still 0 hold on these pins, held to the next falling edge as above. +set min_ad [expr {$sp_clk_delay + $sp_output_half_period + $sp_0_hold + $min_data_delay - $max_clock_delay}] set_input_delay -clock fmc_virt_clk -max $max_ad [get_ports fmc_sp_to_fpga1_da[*]] set_input_delay -clock fmc_virt_clk -min $min_ad [get_ports fmc_sp_to_fpga1_da[*]] +# The MMCM's +45deg phase shift puts the internal capture edge at 1.25 ns, +# and STA's default edge relationship then times input paths from the +# virtual-clock launch at 0 to that 1.25 ns edge -- a nonsense 1.25 ns +# requirement. The intended capture edge is the *next* shifted edge at +# 11.25 ns, which is exactly what setup-2 selects. The default hold +# relationship (one cycle before the setup edge, back at 1.25 ns) is the +# right check and passes with the half-period of real SP hold, so no -hold +# adjustment. Output paths need nothing: launch at 1.25 ns against the SP's +# capture at 10 ns is already the correct single-cycle relationship. +set_multicycle_path 2 -setup -from [get_clocks fmc_virt_clk] -to [get_clocks *fmc_pll*] +set_multicycle_path 2 -setup -from [get_clocks fmc_clk_pin] -to [get_clocks *fmc_pll*] + #### END Of inputs # ################# @@ -129,12 +152,17 @@ set_output_delay -clock fmc_virt_clk -max $max_da [get_ports fmc_sp_to_fpga1_da[ set_output_delay -clock fmc_virt_clk -min $min_da [get_ports fmc_sp_to_fpga1_da[*]] -# assuming wait_l works, we have multiple cycles to get the data out. This is likely needed due to the tri-state stuff here -# and it has trouble meeting timing without the additional cycles. The fpga design compensates for this with wait_l. -set_multicycle_path -from [get_pins {stm32h7_fmc_target_inst/data_out*/C}] -to [get_ports {fmc_sp_to_fpga1_da[*]}] -setup 2 -set_multicycle_path -from [get_pins {stm32h7_fmc_target_inst/data_out*/C}] -to [get_ports {fmc_sp_to_fpga1_da[*]}] -hold 1 -set_multicycle_path -from [get_pins {stm32h7_fmc_target_inst/data_out_en_reg*/C}] -to [get_ports {fmc_sp_to_fpga1_da[*]}] -setup 2 -set_multicycle_path -from [get_pins {stm32h7_fmc_target_inst/data_out_en_reg*/C}] -to [get_ports {fmc_sp_to_fpga1_da[*]}] -hold 1 +# The streaming FSM presents read beats on consecutive cycles, so the +# word0->word1 transition is a true single-cycle path and no multicycle +# exception applies to the data pins. Single-cycle closes because the output +# and tristate flops pack into the IOBs: data_out_reg and data_out_hiz_int_reg +# are one flop per pin by construction, and nwait, a single-cycle path the SP +# samples every rising edge, gets the same treatment. If a board cannot close +# this way, set the extra_beat_setup generic and add setup-2/hold-1 exceptions +# on the data/tristate paths to match. +set_property IOB TRUE [get_cells -hier -filter {NAME =~ *stm32h7_fmc_target*/data_out_reg[*]}] +set_property IOB TRUE [get_cells -hier -filter {NAME =~ *stm32h7_fmc_target*/data_out_hiz_int_reg[*]}] +set_property IOB TRUE [get_cells -hier -filter {NAME =~ *stm32h7_fmc_target*/nwait_reg}] # End FMC diff --git a/hdl/projects/cosmo_seq/debug_module/debug_header.vhd b/hdl/projects/cosmo_seq/debug_module/debug_header.vhd index c1f00005..232b21b1 100644 --- a/hdl/projects/cosmo_seq/debug_module/debug_header.vhd +++ b/hdl/projects/cosmo_seq/debug_module/debug_header.vhd @@ -8,6 +8,7 @@ use ieee.std_logic_1164.all; use ieee.numeric_std.all; use work.debug_regs_pkg.all; +use work.sp5_power_pkg.all; use work.sequencer_io_pkg.all; entity debug_header is @@ -52,7 +53,7 @@ entity debug_header is mux2_sel_pins : in std_logic_vector(1 downto 0); -- mux2 sel pins debug output to pins mux3_sel_pins : in std_logic_vector(1 downto 0); -- mux3 sel pins debug output to pins -- T6 signals - nic_dbg_pins : view t6_debug_dbg; + nic_dbg_pins : view nic_debug_dbg; -- sp5 toggle pins sp5_debug2_pin : in std_logic; @@ -89,7 +90,7 @@ architecture rtl of debug_header is signal espi_resp_csn_int : std_logic; signal dbg_1v8_ctrl_200 : dbg_1v8_ctrl_type; signal fpga1_spare_reg : std_logic_vector(7 downto 0); - signal nic_dbg_pins_int : t6_debug_if; + signal nic_dbg_pins_int : nic_debug_if; signal sp5_debug2_pin_int : std_logic; signal mux1_sel_int : std_logic_vector(1 downto 0); signal mux2_sel_int : std_logic_vector(1 downto 0); @@ -191,8 +192,8 @@ hdr_dbg_reg_1v8: process(clk_200m, reset_200m) when SP_IPCC_BUS => fpga1_spare_reg(7) <= uart1_fpga1_to_sp_dat_int; fpga1_spare_reg(6) <= uart1_sp_to_fpga1_dat_int; - when T6_SEQUENCER => - -- T6 debug pins + when NIC_SEQUENCER => + -- NIC debug pins, see nic_seq / versal_seq for the taps fpga1_spare_reg(7) <= nic_dbg_pins_int.rails_en; fpga1_spare_reg(6) <= nic_dbg_pins_int.rails_pg; when MUX1_SEL => @@ -246,10 +247,9 @@ hdr_dbg_reg_1v8: process(clk_200m, reset_200m) when SP_IPCC_BUS => fpga1_spare_reg(5) <= uart1_fpga1_to_sp_dat_int; fpga1_spare_reg(4) <= uart1_sp_to_fpga1_dat_int; - when T6_SEQUENCER => - -- T6 debug pins - fpga1_spare_reg(5) <= nic_dbg_pins.cld_rst_l; - fpga1_spare_reg(4) <= nic_dbg_pins.perst_l; + when NIC_SEQUENCER => + fpga1_spare_reg(5) <= nic_dbg_pins.taps(5); + fpga1_spare_reg(4) <= nic_dbg_pins.taps(4); when MUX1_SEL => -- Mux1 select pins fpga1_spare_reg(5) <= mux1_sel_int(1); @@ -301,9 +301,9 @@ hdr_dbg_reg_1v8: process(clk_200m, reset_200m) when SP_IPCC_BUS => fpga1_spare_reg(3) <= uart1_fpga1_to_sp_dat_int; fpga1_spare_reg(2) <= uart1_sp_to_fpga1_dat_int; - when T6_SEQUENCER => - fpga1_spare_reg(3) <= nic_dbg_pins.sp5_mfg_mode_l; - fpga1_spare_reg(2) <= nic_dbg_pins.nic_mfg_mode_l; + when NIC_SEQUENCER => + fpga1_spare_reg(3) <= nic_dbg_pins.taps(3); + fpga1_spare_reg(2) <= nic_dbg_pins.taps(2); when MUX1_SEL => fpga1_spare_reg(3) <= mux1_sel_int(1); fpga1_spare_reg(2) <= mux1_sel_int(0); @@ -353,9 +353,9 @@ hdr_dbg_reg_1v8: process(clk_200m, reset_200m) when SP_IPCC_BUS => fpga1_spare_reg(1) <= uart1_fpga1_to_sp_dat_int; fpga1_spare_reg(0) <= uart1_sp_to_fpga1_dat_int; - when T6_SEQUENCER => - fpga1_spare_reg(1) <= nic_dbg_pins.ext_rst_l; - fpga1_spare_reg(0) <= sp5_debug2_pin_int; -- Unused in this case. + when NIC_SEQUENCER => + fpga1_spare_reg(1) <= nic_dbg_pins.taps(1); + fpga1_spare_reg(0) <= nic_dbg_pins.taps(0); when MUX1_SEL => fpga1_spare_reg(1) <= mux1_sel_int(1); fpga1_spare_reg(0) <= mux1_sel_int(0); diff --git a/hdl/projects/cosmo_seq/debug_module/debug_module_top.vhd b/hdl/projects/cosmo_seq/debug_module/debug_module_top.vhd index d5aeaa1a..3eab51b5 100644 --- a/hdl/projects/cosmo_seq/debug_module/debug_module_top.vhd +++ b/hdl/projects/cosmo_seq/debug_module/debug_module_top.vhd @@ -12,6 +12,7 @@ use work.axil8x32_pkg.all; use work.debug_regs_pkg.all; use work.sp5_uart_subsystem_pkg.all; +use work.sp5_power_pkg.all; use work.sequencer_io_pkg.all; entity debug_module_top is @@ -66,7 +67,7 @@ entity debug_module_top is espi0_sp5_to_fpga1_dat: in std_logic_vector(3 downto 0); espi_resp_csn: in std_logic; --T6 signals - nic_dbg_pins : view t6_debug_dbg; + nic_dbg_pins : view nic_debug_dbg; fpga1_spare_v1p8 : out std_logic_vector(7 downto 0); -- 8 spare pins on the debug header @@ -237,11 +238,11 @@ begin dbg_1v8_ctrl.pins5_4 <= ESPI_BUS; dbg_1v8_ctrl.pins3_2 <= ESPI_BUS; dbg_1v8_ctrl.pins1_0 <= ESPI_BUS; - elsif dbg_convenience.t6_seq_en then - dbg_1v8_ctrl.pins7_6 <= T6_SEQUENCER; - dbg_1v8_ctrl.pins5_4 <= T6_SEQUENCER; - dbg_1v8_ctrl.pins3_2 <= T6_SEQUENCER; - dbg_1v8_ctrl.pins1_0 <= T6_SEQUENCER; + elsif dbg_convenience.nic_seq_en then + dbg_1v8_ctrl.pins7_6 <= NIC_SEQUENCER; + dbg_1v8_ctrl.pins5_4 <= NIC_SEQUENCER; + dbg_1v8_ctrl.pins3_2 <= NIC_SEQUENCER; + dbg_1v8_ctrl.pins1_0 <= NIC_SEQUENCER; end if; when others => null; diff --git a/hdl/projects/cosmo_seq/debug_module/debug_regs.rdl b/hdl/projects/cosmo_seq/debug_module/debug_regs.rdl index 0c0893c7..a73ea1d5 100644 --- a/hdl/projects/cosmo_seq/debug_module/debug_regs.rdl +++ b/hdl/projects/cosmo_seq/debug_module/debug_regs.rdl @@ -126,7 +126,7 @@ addrmap debug_regs { sp_console_bus = 8'h08 {desc = "SP <-> FPGA console UART to pins";}; sp5_console_bus = 8'h09 {desc = "SP5 <-> FPGA console UART to pins";}; sp_ipcc_bus = 8'h0a {desc = "SP <-> FPGA IPCC UART to pins";}; - t6_sequencer = 8'h0b {desc = "T6 sequencer debug output to pins";}; + nic_sequencer = 8'h0b {desc = "NIC sequencer debug output to pins (T6 on cosmo, Versal on metro; see nic_seq/versal_seq for the tap assignment)";}; mux1_sel = 8'h0c {desc = "Mux1 sel pins debug output to pins";}; mux2_sel = 8'h0d {desc = "Mux2 sel pins debug output to pins";}; mux3_sel = 8'h0e {desc = "Mux3 sel pins debug output to pins";}; @@ -166,7 +166,7 @@ addrmap debug_regs { name = "Debug Convenience"; field { desc = "convenience bit for setting up T6 sequencing debug out. This uses 1v8 debug header pins 7..0. Hw clears"; - } t6_seq_en[2:2] = 0; + } nic_seq_en[2:2] = 0; field { desc = "convenience bit for setting up x4 espi debug out. This uses 1v8 debug header pins 7..0. Hw clears"; } espi_dbg_x4_en[1:1] = 0; diff --git a/hdl/projects/cosmo_seq/sequencer/BUCK b/hdl/projects/cosmo_seq/sequencer/BUCK index b8c64721..e4f00764 100644 --- a/hdl/projects/cosmo_seq/sequencer/BUCK +++ b/hdl/projects/cosmo_seq/sequencer/BUCK @@ -1,4 +1,4 @@ -load("//tools:hdl.bzl", "vhdl_unit", "vunit_sim") +load("//tools:hdl.bzl", "vhdl_unit", "vunit_sim", "sim_only_model") load("//tools:rdl.bzl", "rdl_file") rdl_file( @@ -12,10 +12,29 @@ rdl_file( visibility = ["PUBLIC"], ) +# The SP5-side sequencing core: the generic rail records, the SP5 rail groups, +# and the A1/A0 state machine that drives them. None of this is cosmo-specific, +# so sibling SP5 boards depend on this target and supply their own NIC half. +# It still carries sequencer_regs_rdl for the A1/A0 state enums -- there is one +# A1/A0 state machine, so there should be one encoding of its states. +vhdl_unit( + name = "sequencer_core", + srcs = [ + "sp5_power_pkg.vhd", + "a1_a0_seq.vhd", + ], + deps = [ + ":sequencer_regs_rdl", + ], + visibility = ["PUBLIC"], + standard = "2019", +) + vhdl_unit( name = "sequencer", - srcs = glob(["*.vhd"]), + srcs = glob(["*.vhd"], exclude = ["sp5_power_pkg.vhd", "a1_a0_seq.vhd"]), deps = [ + ":sequencer_core", ":sequencer_regs_rdl", "//hdl/ip/vhd/synchronizers:meta_sync", "//hdl/ip/vhd/irq:irq_block", @@ -25,10 +44,32 @@ vhdl_unit( standard = "2019", ) +# The rail models and the SP5 model only speak sp5_power_pkg's records, so they +# are reusable by any SP5 board's sequencer testbench, not just cosmo's. +sim_only_model( + name = "sequencer_core_sims", + srcs = [ + "sims/rail_model_msg_pkg.vhd", + "sims/rail_model.vhd", + "sims/cascade_rail_model.vhd", + "sims/sp5_sim.vhd", + ], + deps = [":sequencer_core"], + visibility = ["PUBLIC"], + standard = "2019", +) + vunit_sim( name = "sp5_seq_tb", - srcs = glob(["sims/**/*.vhd"]), - deps = [":sequencer", + srcs = glob(["sims/**/*.vhd"], exclude = [ + "sims/rail_model_msg_pkg.vhd", + "sims/rail_model.vhd", + "sims/cascade_rail_model.vhd", + "sims/sp5_sim.vhd", + ]), + deps = [ + ":sequencer", + ":sequencer_core_sims", ], visibility = ['PUBLIC'], standard = "2019", diff --git a/hdl/projects/cosmo_seq/sequencer/a1_a0_seq.vhd b/hdl/projects/cosmo_seq/sequencer/a1_a0_seq.vhd index cd646b09..9f593438 100644 --- a/hdl/projects/cosmo_seq/sequencer/a1_a0_seq.vhd +++ b/hdl/projects/cosmo_seq/sequencer/a1_a0_seq.vhd @@ -6,7 +6,7 @@ library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; -use work.sequencer_io_pkg.all; +use work.sp5_power_pkg.all; use work.sequencer_regs_pkg.all; -- This is the A1/A0 sequencer for the SP5 cosmo sled diff --git a/hdl/projects/cosmo_seq/sequencer/nic_seq.vhd b/hdl/projects/cosmo_seq/sequencer/nic_seq.vhd index 1ee181db..c3f634dc 100644 --- a/hdl/projects/cosmo_seq/sequencer/nic_seq.vhd +++ b/hdl/projects/cosmo_seq/sequencer/nic_seq.vhd @@ -6,6 +6,7 @@ library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; +use work.sp5_power_pkg.all; use work.sequencer_io_pkg.all; use work.sequencer_regs_pkg.all; @@ -28,7 +29,7 @@ entity nic_seq is raw_state : out nic_raw_status_type; api_state : out nic_api_status_type; - nic_dbg_pins : view t6_debug_seq_ss; + nic_dbg_pins : view nic_debug_seq_ss; -- From SP5 hotplug sp5_t6_perst_l : in std_logic; -- follows exactly the power_en hotplug signal. perst_l <= power_en; @@ -102,13 +103,15 @@ begin nic_idle <= '1' when nic_r.state = IDLE else '0'; - nic_dbg_pins.cld_rst_l <= final_nic_outs.cld_rst_l; - nic_dbg_pins.ext_rst_l <= nic_seq_pins.ext_rst_l; + -- Debug header taps, on header pins 5..0 in this order nic_dbg_pins.rails_en <= nic_r.nic_power_en; nic_dbg_pins.rails_pg <= '1' when is_power_good(nic_rails) else '0'; - nic_dbg_pins.nic_mfg_mode_l <= final_nic_outs.nic_mfg_mode_l; - nic_dbg_pins.sp5_mfg_mode_l <= nic_seq_pins.sp5_mfg_mode_l; - nic_dbg_pins.perst_l <= final_nic_outs.perst_l; + nic_dbg_pins.taps(5) <= final_nic_outs.cld_rst_l; + nic_dbg_pins.taps(4) <= final_nic_outs.perst_l; + nic_dbg_pins.taps(3) <= nic_seq_pins.sp5_mfg_mode_l; + nic_dbg_pins.taps(2) <= final_nic_outs.nic_mfg_mode_l; + nic_dbg_pins.taps(1) <= nic_seq_pins.ext_rst_l; + nic_dbg_pins.taps(0) <= '0'; -- Gimlet has the following sequence that was empirically determined to work -- We had to double-perst and we know that cld_rst_l needs to be de-asserted 10ms before perst_l @@ -147,6 +150,8 @@ begin api_state.nic_sm <= NIC_RESET; end if; + -- the Versal states; this NIC never has them + when others => null; end case; end if; @@ -249,6 +254,8 @@ begin v.state := IDLE; end if; + -- the Versal states; this NIC never has them + when others => null; end case; -- MAPO fault handling - monitored in all non-IDLE states diff --git a/hdl/projects/cosmo_seq/sequencer/seq_sync.vhd b/hdl/projects/cosmo_seq/sequencer/seq_sync.vhd index 9c860141..4d87f4f3 100644 --- a/hdl/projects/cosmo_seq/sequencer/seq_sync.vhd +++ b/hdl/projects/cosmo_seq/sequencer/seq_sync.vhd @@ -6,9 +6,13 @@ library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; +use work.sp5_power_pkg.all; use work.sequencer_io_pkg.all; use work.sequencer_regs_pkg.all; +-- Synchronises the SP5-side pins, the fans, the DDR hotswaps and the alert +-- pins, which every SP5 board shares. The NIC-side pins are board specific and +-- have their own synchronisers, t6_sync and versal_sync. entity seq_sync is port ( clk : in std_logic; @@ -21,8 +25,6 @@ entity seq_sync is group_b_pins : view group_b_power_at_fpga; group_c_pins : view group_c_power_at_fpga; sp5_seq_pins : view sp5_seq_at_fpga; - nic_rails_pins : view nic_power_at_fpga; - nic_seq_pins: view nic_seq_at_fpga; reg_alert_l_pins : view power_alert_at_fpga; -- internal, synchronized interfaces rail_masks : in rails_type; @@ -33,15 +35,11 @@ entity seq_sync is group_b : view group_b_power_at_reg; group_c : view group_c_power_at_reg; sp5_seq : view sp5_seq_at_sp5; - nic_rails : view nic_power_at_reg; - nic_seq: view nic_seq_at_nic; reg_alert_l : view power_alert_at_reg; ); end entity; architecture rtl of seq_sync is - signal nic_sync_5v_hsc_pg_l : std_logic; - signal nic_sync_12v_hsc_pg_l : std_logic; signal fan_central_pg_raw : std_logic; signal fan_east_pg_raw : std_logic; signal fan_west_pg_raw : std_logic; @@ -231,87 +229,6 @@ begin sycnd_output => sp5_seq.pwrgd_out ); - -- nic rails sync stuff - nic_rails_pins.nic_hsc_12v.enable <= nic_rails.nic_hsc_12v.enable; - v1p5_nic_a0hp: entity work.meta_sync - port map( - async_input => nic_rails_pins.v1p5_nic_a0hp.pg, - clk => clk, - sycnd_output => nic_rails.v1p5_nic_a0hp.pg - ); - v1p2_nic_pcie_a0hp: entity work.meta_sync - port map( - async_input => nic_rails_pins.v1p2_nic_pcie_a0hp.pg, - clk => clk, - sycnd_output => nic_rails.v1p2_nic_pcie_a0hp.pg - ); - v1p2_nic_enet_a0hp: entity work.meta_sync - port map( - async_input => nic_rails_pins.v1p2_nic_enet_a0hp.pg, - clk => clk, - sycnd_output => nic_rails.v1p2_nic_enet_a0hp.pg - ); - v3p3_nic_a0hp: entity work.meta_sync - port map( - async_input => nic_rails_pins.v3p3_nic_a0hp.pg, - clk => clk, - sycnd_output => nic_rails.v3p3_nic_a0hp.pg - ); - v1p1_nic_a0hp: entity work.meta_sync - port map( - async_input => nic_rails_pins.v1p1_nic_a0hp.pg, - clk => clk, - sycnd_output => nic_rails.v1p1_nic_a0hp.pg - ); - v1p4_nic_a0hp: entity work.meta_sync - port map( - async_input => nic_rails_pins.v1p4_nic_a0hp.pg, - clk => clk, - sycnd_output => nic_rails.v1p4_nic_a0hp.pg - ); - v0p96_nic_vdd_a0hp: entity work.meta_sync - port map( - async_input => nic_rails_pins.v0p96_nic_vdd_a0hp.pg, - clk => clk, - sycnd_output => nic_rails.v0p96_nic_vdd_a0hp.pg - ); - nic_hsc_12v: entity work.meta_sync - port map( - async_input => nic_rails_pins.nic_hsc_12v.pg, - clk => clk, - sycnd_output => nic_sync_12v_hsc_pg_l - ); - nic_hsc_5v: entity work.meta_sync - port map( - async_input => nic_rails_pins.nic_hsc_5v.pg, - clk => clk, - sycnd_output => nic_sync_5v_hsc_pg_l - ); - - -- HSC's are actually pg_l signals, so invert them here - nic_rails.nic_hsc_5v.pg <= not nic_sync_5v_hsc_pg_l; - nic_rails.nic_hsc_12v.pg <= not nic_sync_12v_hsc_pg_l; - -- nic sync-related stuff - nic_seq_pins.cld_rst_l <= nic_seq.cld_rst_l; - nic_seq_pins.perst_l <= nic_seq.perst_l; - nic_seq_pins.eeprom_wp_l <= nic_seq.eeprom_wp_l; - nic_seq_pins.eeprom_wp_buffer_oe_l <= nic_seq.eeprom_wp_buffer_oe_l; - nic_seq_pins.flash_wp_l <= nic_seq.flash_wp_l; - nic_seq_pins.nic_mfg_mode_l <= nic_seq.nic_mfg_mode_l; - nic_seq_pins.nic_pcie_clk_buff_oe_l <= nic_seq.nic_pcie_clk_buff_oe_l; - ext_rst_l_sync: entity work.meta_sync - port map( - async_input => nic_seq_pins.ext_rst_l, - clk => clk, - sycnd_output => nic_seq.ext_rst_l - ); - sp5_mfg_mode_l_sync: entity work.meta_sync - port map( - async_input => nic_seq_pins.sp5_mfg_mode_l, - clk => clk, - sycnd_output => nic_seq.sp5_mfg_mode_l - ); - -- Alert sync stuff @@ -419,6 +336,12 @@ begin clk => clk, sycnd_output => reg_alert_l.pwr_cont3_to_fpga1_alert_l ); + pwr_cont4_to_fpga1_alert_l_sync: entity work.meta_sync + port map( + async_input => reg_alert_l_pins.pwr_cont4_to_fpga1_alert_l, + clk => clk, + sycnd_output => reg_alert_l.pwr_cont4_to_fpga1_alert_l + ); end rtl; diff --git a/hdl/projects/cosmo_seq/sequencer/sequencer_io_pkg.vhd b/hdl/projects/cosmo_seq/sequencer/sequencer_io_pkg.vhd index 345c7afe..ea3da65a 100644 --- a/hdl/projects/cosmo_seq/sequencer/sequencer_io_pkg.vhd +++ b/hdl/projects/cosmo_seq/sequencer/sequencer_io_pkg.vhd @@ -6,62 +6,16 @@ library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; -package sequencer_io_pkg is +use work.sp5_power_pkg.all; - -- Generic record type for the common enable/pg feedback. - -- we'll treat this as both active high internally so - -- the outer-most blocks need to invert if required. - type power_rail_t is record - enable : std_logic; - pg : std_logic; - end record; - -- FPGA's view of the world as the controller and recipient of - -- the feedback - view power_rail_at_fpga of power_rail_t is - enable : out; - pg : in; - end view; - alias power_rail_at_reg is power_rail_at_fpga'converse; - -- a cascade power rail only has a PG, the enable came from some - -- other supply - type cascade_power_rail_t is record - pg : std_logic; - end record; - view cascade_power_rail_at_fpga of cascade_power_rail_t is - pg : in; - end view; - alias cascade_power_rail_at_reg is cascade_power_rail_at_fpga'converse; +-- Board-side sequencer I/O types for the SP5 boards. The generic rail records +-- and the SP5 rail groups they build on live in sp5_power_pkg; this package +-- holds the NIC-side records for both NIC flavours (cosmo's T6, metro's +-- Versal) and the alert-pin record, which is the union of both boards' pins. +package sequencer_io_pkg is - -- Sequencing-related SP5 control/feedback pins - type sp5_seq_pins_t is record - thermtrip_l : std_logic; - reset_l : std_logic; - pwr_ok : std_logic; - slp_s3_l : std_logic; - slp_s5_l : std_logic; - rsmrst_l : std_logic; - pwr_btn_l : std_logic; - pwr_good : std_logic; - pwrgd_out : std_logic; - smerr_l : std_logic; - is_cosmo : std_logic; -- uses SP5_TO_FPGA1_DEBUG1 high at power up to indicate cosmo - end record; - -- FPGA's view of the world as the controller and recipient of - -- the feedback - view sp5_seq_at_fpga of sp5_seq_pins_t is - thermtrip_l : in; - reset_l : in; - pwr_ok : in; - slp_s3_l : in; - slp_s5_l : in; - smerr_l : in; - pwrgd_out : in; - rsmrst_l : out; - pwr_btn_l : out; - pwr_good : out; - is_cosmo : out; - end view; - alias sp5_seq_at_sp5 is sp5_seq_at_fpga'converse; + -- Which NIC a board carries. Selects the NIC half of sp5_sequencer. + type nic_kind_t is (NIC_T6, NIC_VERSAL); type seq_power_alert_pins_t is record smbus_fan_central_hsc_to_fpga1_alert_l : std_logic; @@ -81,6 +35,7 @@ package sequencer_io_pkg is v0p96_nic_to_fpga1_alert_l : std_logic; pwr_cont2_to_fpga1_alert_l : std_logic; pwr_cont3_to_fpga1_alert_l : std_logic; + pwr_cont4_to_fpga1_alert_l : std_logic; -- metro only; cosmo ties it high end record; view power_alert_at_fpga of seq_power_alert_pins_t is smbus_fan_central_hsc_to_fpga1_alert_l : in; @@ -100,6 +55,7 @@ package sequencer_io_pkg is v0p96_nic_to_fpga1_alert_l : in; pwr_cont2_to_fpga1_alert_l : in; pwr_cont3_to_fpga1_alert_l : in; + pwr_cont4_to_fpga1_alert_l : in; end view; alias power_alert_at_reg is power_alert_at_fpga'converse; @@ -128,101 +84,21 @@ package sequencer_io_pkg is end view; alias nic_seq_at_nic is nic_seq_at_fpga'converse; - type t6_debug_if is record - cld_rst_l : std_logic; -- T6 cld reset (FPGA output) - ext_rst_l : std_logic; -- T6 external reset (FPGA input) - rails_en : std_logic; -- T6 power rails enable (FPGA output combined) - rails_pg : std_logic; -- T6 power rails power good (FPGA input combined) - nic_mfg_mode_l : std_logic; -- T6 NIC manufacturing mode (FPGA output) - sp5_mfg_mode_l : std_logic; -- T6 SP5 manufacturing mode (FPGA input) - perst_l : std_logic; -- T6 PCIe reset (FPGA output) + -- What the NIC sequencer offers the debug header: the combined rail + -- enable and power good, plus six taps whose meaning is the NIC's to + -- define. They land on header pins 5..0 in order; see nic_seq (T6) and + -- versal_seq for what each board puts there. + type nic_debug_if is record + rails_en : std_logic; -- NIC power rails enable (FPGA output combined) + rails_pg : std_logic; -- NIC power rails power good (FPGA input combined) + taps : std_logic_vector(5 downto 0); end record; - view t6_debug_seq_ss of t6_debug_if is - cld_rst_l : out; - ext_rst_l : out; + view nic_debug_seq_ss of nic_debug_if is rails_en : out; rails_pg : out; - nic_mfg_mode_l : out; - sp5_mfg_mode_l : out; - perst_l : out; - end view; - alias t6_debug_dbg is t6_debug_seq_ss'converse; - - type early_power_t is record - fan_central_hsc_pg : std_logic; - fan_east_hsc_pg : std_logic; - fan_fail : std_logic; - fan_west_hsc_pg : std_logic; - fan_central_hsc_disable : std_logic; - fan_east_hsc_disable : std_logic; - fan_west_hsc_disable : std_logic; - end record; - view early_power_at_fpga of early_power_t is - fan_central_hsc_pg : in; - fan_east_hsc_pg : in; - fan_west_hsc_pg : in; - fan_fail : in; - fan_central_hsc_disable : out; - fan_east_hsc_disable : out; - fan_west_hsc_disable : out; + taps : out; end view; - alias early_power_on_board is early_power_at_fpga'converse; - - -- DDR 12V bulk power rail - type ddr_bulk_power_t is record - abcdef_hsc : power_rail_t; - ghijkl_hsc : power_rail_t; - end record; - function is_power_good(power_group: ddr_bulk_power_t) return boolean; - -- FPGA's view of the world as the controller and recipient of - -- the feedback - view ddr_bulk_power_at_fpga of ddr_bulk_power_t is - abcdef_hsc : view power_rail_at_fpga; - ghijkl_hsc : view power_rail_at_fpga; - end view; - alias ddr_bulk_at_reg is ddr_bulk_power_at_fpga'converse; - - -- SP5 group a rails - --A (G3/S5): VDDBT_RTC_G, VDD_18_S5,VDD_33_S5, VDDIO_AUDIO (shared as 1V8) - type group_a_power_t is record - pwr_v1p5_rtc : power_rail_t; - v3p3_sp5_a1 : power_rail_t; - v1p8_sp5_a1 : power_rail_t; - end record; - function is_power_good(power_group: group_a_power_t) return boolean; - view group_a_power_at_fpga of group_a_power_t is - pwr_v1p5_rtc : view power_rail_at_fpga; - v3p3_sp5_a1 : view power_rail_at_fpga; - v1p8_sp5_a1 : view power_rail_at_fpga; - end view; - alias group_a_power_at_reg is group_a_power_at_fpga'converse; - - -- B (S3): VDD_11_S3 - type group_b_power_t is record - v1p1_sp5 : power_rail_t; - end record; - function is_power_good(power_group: group_b_power_t) return boolean; - view group_b_power_at_fpga of group_b_power_t is - v1p1_sp5 : view power_rail_at_fpga; - end view; - alias group_b_power_at_reg is group_b_power_at_fpga'converse; - - - -- C (S0): VDDIO, VDDCR_SOC,VDDCR_CPU0, VDDCR_CPU1 - type group_c_power_t is record - vddio_sp5_a0 : power_rail_t; - vddcr_cpu1 : power_rail_t; - vddcr_cpu0 : power_rail_t; - vddcr_soc : power_rail_t; - end record; - function is_power_good(power_group: group_c_power_t) return boolean; - view group_c_power_at_fpga of group_c_power_t is - vddio_sp5_a0 : view power_rail_at_fpga; - vddcr_cpu1 : view power_rail_at_fpga; - vddcr_cpu0 : view power_rail_at_fpga; - vddcr_soc : view power_rail_at_fpga; - end view; - alias group_c_power_at_reg is group_c_power_at_fpga'converse; + alias nic_debug_dbg is nic_debug_seq_ss'converse; -- effectively one enable fires all of this due to the -- hardware design. @@ -251,38 +127,136 @@ package sequencer_io_pkg is end view; alias nic_power_at_reg is nic_power_at_fpga'converse; -end package; + -- Metro's Versal rail tree. Everything in here is active high; the + -- synchroniser inverts the schematic's active-low 12V/5V power-good pins + -- on the way in so this layer never has to think about pin polarity. + -- + -- V0P92_NIC_AVCC and V1P2_NIC_AVTT have no FPGA enable -- they cascade off + -- rails we do enable -- so they are readback only. + type versal_power_t is record + hsc_12v : power_rail_t; -- FPGA1_TO_NIC_HSC_EN / V12P0_NIC_A0HP_PG_L + hsc_5v : cascade_power_rail_t; -- cascades off the 12V hotswap + v3p3 : power_rail_t; + v1p8 : power_rail_t; + v1p5 : power_rail_t; + v1p5_avccaux : power_rail_t; + v1p4 : power_rail_t; + v1p1 : power_rail_t; + v0p88 : power_rail_t; + v0p8_vccint : power_rail_t; + v0p92_avcc : cascade_power_rail_t; + v1p2_avtt : cascade_power_rail_t; + end record; + function is_power_good(power_group: versal_power_t) return boolean; + view versal_power_at_fpga of versal_power_t is + hsc_12v : view power_rail_at_fpga; + hsc_5v : view cascade_power_rail_at_fpga; + v3p3 : view power_rail_at_fpga; + v1p8 : view power_rail_at_fpga; + v1p5 : view power_rail_at_fpga; + v1p5_avccaux : view power_rail_at_fpga; + v1p4 : view power_rail_at_fpga; + v1p1 : view power_rail_at_fpga; + v0p88 : view power_rail_at_fpga; + v0p8_vccint : view power_rail_at_fpga; + v0p92_avcc : view cascade_power_rail_at_fpga; + v1p2_avtt : view cascade_power_rail_at_fpga; + end view; + alias versal_power_at_reg is versal_power_at_fpga'converse; + -- What a board without a Versal ties its versal_rails_pins to: nothing + -- enabled, nothing good. Only ever looked at by versal_seq, which such a + -- board does not generate. + constant versal_power_absent : versal_power_t := ( + hsc_12v => (enable => '0', pg => '1'), -- hotswap pg pins are active low + hsc_5v => (pg => '1'), + v3p3 => (enable => '0', pg => '0'), + v1p8 => (enable => '0', pg => '0'), + v1p5 => (enable => '0', pg => '0'), + v1p5_avccaux => (enable => '0', pg => '0'), + v1p4 => (enable => '0', pg => '0'), + v1p1 => (enable => '0', pg => '0'), + v0p88 => (enable => '0', pg => '0'), + v0p8_vccint => (enable => '0', pg => '0'), + v0p92_avcc => (pg => '0'), + v1p2_avtt => (pg => '0') + ); + -- Versal boot straps and status. mode is driven onto the VP1202's + -- MODE[3:0] pins through a buffer we also enable, and por_b is the + -- device's power-on reset. + type versal_boot_t is record + mode : std_logic_vector(3 downto 0); + mode_buffer_en_l : std_logic; + por_b : std_logic; + err_done_buff_en : std_logic; + done : std_logic; + error_out : std_logic; + end record; + view versal_boot_at_fpga of versal_boot_t is + mode : out; + mode_buffer_en_l : out; + por_b : out; + err_done_buff_en : out; + done : in; + error_out : in; + end view; + alias versal_boot_at_versal is versal_boot_at_fpga'converse; + constant versal_boot_absent : versal_boot_t := ( + mode => (others => '0'), mode_buffer_en_l => '1', por_b => '0', + err_done_buff_en => '0', done => '0', error_out => '0' + ); -package body sequencer_io_pkg is + -- One of the Versal's two PCIe channels to the host. + type versal_pcie_chan_t is record + perst_l : std_logic; + prsnt_l : std_logic; + pwren_l : std_logic; + clk_buff_oe_l : std_logic; + end record; + view versal_pcie_chan_at_fpga of versal_pcie_chan_t is + perst_l : out; + prsnt_l : in; + pwren_l : in; + clk_buff_oe_l : out; + end view; + alias versal_pcie_chan_at_nic is versal_pcie_chan_at_fpga'converse; - -- Functions for DDR bulk - function is_power_good(power_group: ddr_bulk_power_t) return boolean is - begin - return (power_group.abcdef_hsc.pg and power_group.ghijkl_hsc.pg) = '1'; - end function; + type versal_pcie_t is record + cha : versal_pcie_chan_t; + chb : versal_pcie_chan_t; + end record; + view versal_pcie_at_fpga of versal_pcie_t is + cha : view versal_pcie_chan_at_fpga; + chb : view versal_pcie_chan_at_fpga; + end view; + alias versal_pcie_at_nic is versal_pcie_at_fpga'converse; + constant versal_pcie_absent : versal_pcie_t := ( + cha => (perst_l => '0', prsnt_l => '1', pwren_l => '1', clk_buff_oe_l => '1'), + chb => (perst_l => '0', prsnt_l => '1', pwren_l => '1', clk_buff_oe_l => '1') + ); - -- Functions for groupA - function is_power_good(power_group: group_a_power_t) return boolean is - begin - return (power_group.pwr_v1p5_rtc.pg and power_group.v3p3_sp5_a1.pg and power_group.v1p8_sp5_a1.pg) = '1'; - end function; - - -- Functions for groupB - function is_power_good(power_group: group_b_power_t) return boolean is - begin - return power_group.v1p1_sp5.pg = '1'; - end function; + -- Likewise what a board without a T6 ties its T6 pins to. + constant nic_power_absent : nic_power_t := ( + v1p5_nic_a0hp => (pg => '0'), + v1p2_nic_pcie_a0hp => (pg => '0'), + v1p2_nic_enet_a0hp => (pg => '0'), + v3p3_nic_a0hp => (pg => '0'), + v1p1_nic_a0hp => (pg => '0'), + v1p4_nic_a0hp => (pg => '0'), + v0p96_nic_vdd_a0hp => (pg => '0'), + nic_hsc_12v => (enable => '0', pg => '1'), -- hotswap pg pins are active low + nic_hsc_5v => (pg => '1') + ); + constant nic_seq_pins_absent : nic_seq_pins_t := ( + cld_rst_l => '0', perst_l => '0', eeprom_wp_l => '0', + eeprom_wp_buffer_oe_l => '1', flash_wp_l => '0', nic_mfg_mode_l => '1', + ext_rst_l => '1', nic_pcie_clk_buff_oe_l => '1', sp5_mfg_mode_l => '1' + ); + +end package; + +package body sequencer_io_pkg is - -- Functions for groupc - function is_power_good(power_group: group_c_power_t) return boolean is - begin - return (power_group.vddio_sp5_a0.pg and - power_group.vddcr_cpu1.pg and - power_group.vddcr_cpu0.pg and - power_group.vddcr_soc.pg - ) = '1'; - end function; function is_power_good(power_group: nic_power_t) return boolean is begin return ( @@ -298,4 +272,22 @@ package body sequencer_io_pkg is ) = '1'; end function; -end package body; \ No newline at end of file + function is_power_good(power_group: versal_power_t) return boolean is + begin + return ( + power_group.hsc_12v.pg and + power_group.hsc_5v.pg and + power_group.v3p3.pg and + power_group.v1p8.pg and + power_group.v1p5.pg and + power_group.v1p5_avccaux.pg and + power_group.v1p4.pg and + power_group.v1p1.pg and + power_group.v0p88.pg and + power_group.v0p8_vccint.pg and + power_group.v0p92_avcc.pg and + power_group.v1p2_avtt.pg + ) = '1'; + end function; + +end package body; diff --git a/hdl/projects/cosmo_seq/sequencer/sequencer_regs.rdl b/hdl/projects/cosmo_seq/sequencer/sequencer_regs.rdl index dbd0768f..3e51676b 100644 --- a/hdl/projects/cosmo_seq/sequencer/sequencer_regs.rdl +++ b/hdl/projects/cosmo_seq/sequencer/sequencer_regs.rdl @@ -1,5 +1,14 @@ -// This is SystemRDL description of the sw-accessible registers in the Cosmo -// Sequencer FPGA block. +// SystemRDL description of the sw-accessible registers in the SP5 sequencer +// block, shared by every SP5 board (cosmo, metro). +// +// The map is the union of what the boards need. The SP5 half is common. The +// NIC half differs by board -- cosmo carries a T6 behind a single reset, metro +// an AMD Versal with a rail per enable, boot straps and two PCIe channels -- +// and both sets of NIC registers exist on both boards: the ones a board has no +// hardware for read as zero and their writes do nothing. Software should key +// off the board (the info block) before trusting a NIC register. Cosmo's +// layout is the original; everything metro added is appended or fills spare +// bits, so cosmo's offsets and bit positions have not moved. addrmap sequencer_regs { name = "Power Sequencing Function"; @@ -87,6 +96,15 @@ This regulator controls VDDCR_CPU1_EN and VDDIO_SP5 rails."; desc = "Live. Set '1' when pwr_cont3_to_fpga1_alert_l is active (low on board). This regulator controls V1P1_SP5_A0, V1P8_SP5_A0 and V3P3_SP5_A0 rails."; } pwr_cont3_to_fpga1_alert[1]; + field { + desc = "Live. Set '1' when pwr_cont4_to_fpga1_alert_l is active (low on board). Metro only."; + } pwr_cont4_to_fpga1_alert[1]; + field { + desc = "Live. Set '1' when the Versal is asserting ERROR_OUT. Metro only."; + } versal_error_out[1]; + field { + desc = "The pre-boot measurement of the NIC image failed (sticky since fpga reset or last clear). Metro only."; + } nic_hash_err[1]; }; // Set up interrupt registers using a common irq_type irq IFR; @@ -111,6 +129,19 @@ This regulator controls V1P1_SP5_A0, V1P8_SP5_A0 and V3P3_SP5_A0 rails."; field { desc = "NIC power OK"; } nicpwrok[1] = 0; + field { + desc = "NIC has finished loading its image (Versal DONE). Metro only."; + } nicdone[1] = 0; + field { + desc = "The hash engine measured the NIC image before the last boot; the + digest is in the hash engine's HW_DIGEST registers. Cleared when the NIC + is next sequenced. Metro only."; + } nic_hash_done[1] = 0; + field { + desc = "The pre-boot measurement failed (see the hash engine's HW_STATUS) and + the NIC was booted regardless. Cleared when the NIC is next sequenced. + Metro only."; + } nic_hash_err[1] = 0; field { desc = "Interrupt Pending Status (set if any enabled IRQ has its flag set)"; } int_pend[31:31] = 0; @@ -236,8 +267,10 @@ This regulator controls V1P1_SP5_A0, V1P8_SP5_A0 and V3P3_SP5_A0 rails."; enum nic_state_machine { IDLE = 8'h00 {desc = "";}; ENABLE_POWER = 8'h01 {desc = "";}; - NIC_RESET = 8'h02 {desc = "";}; + NIC_RESET = 8'h02 {desc = "Rails are up, NIC held in reset (on metro: while the boot mode is strapped)";}; DONE = 8'h03 {desc = "";}; + BOOTING = 8'h04 {desc = "Metro only. POR_B released, waiting for DONE";}; + MEASURING = 8'h05 {desc = "Metro only. Rails are up, holding POR_B while the hash engine measures the boot image";}; }; field { desc = "Nic SM Status"; @@ -260,7 +293,17 @@ This regulator controls V1P1_SP5_A0, V1P8_SP5_A0 and V3P3_SP5_A0 rails."; EARLY_CLD_RST = 8'h03 {desc = "";}; EARLY_PERST = 8'h04 {desc = "";}; EARLY_PERST_ASSERT = 8'h05 {desc = "";}; - DONE = 8'h06 {desc = "";}; + DONE = 8'h06 {desc = "Booted. On metro the boot flash mux is then pointed at the FPGA so the SP5 can reach it over eSPI";}; + HSC_EN = 8'h07 {desc = "Metro (Versal). 12V hotswap enabled, waiting for 12V and 5V";}; + CORE_EN = 8'h08 {desc = "Metro (Versal). VCCINT group enabled, waiting for PG";}; + AUX_EN = 8'h09 {desc = "Metro (Versal). VCCAUX group enabled, waiting for PG";}; + IO_EN = 8'h0a {desc = "Metro (Versal). VCCO group enabled, waiting for PG";}; + RAILS_SETTLE = 8'h0b {desc = "Metro (Versal). All rails good, holding POR_B while they settle";}; + HASH_IMAGE = 8'h0c {desc = "Metro (Versal). Rails up, POR_B held, boot flash mux pointed at the FPGA; waiting for the hash engine to measure the image";}; + HASH_RELEASE = 8'h0d {desc = "Metro (Versal). Measurement over, waiting for the hash engine to drop its acknowledge";}; + MODE_STRAP = 8'h0e {desc = "Metro (Versal). Boot mode driven onto MODE[3:0], buffer enabled";}; + POR_RELEASE = 8'h0f {desc = "Metro (Versal). POR_B released";}; + WAIT_DONE = 8'h10 {desc = "Metro (Versal). Waiting for the Versal to assert DONE";}; }; field { @@ -357,6 +400,37 @@ This regulator controls V1P1_SP5_A0, V1P8_SP5_A0 and V3P3_SP5_A0 rails."; field { desc = "NIC v0p96_nic_vdd_a0hp (discrete pg, enable cascade from nic 5V)"; } v0p96_nic_vdd_a0hp[1]; + // Metro's Versal rail tree. Its hotswaps share the nic_hsc_* bits above. + field { + desc = "Metro only. Versal VCCINT 0.8V"; + } versal_v0p8_vccint[1]; + field { + desc = "Metro only. Versal 0.88V"; + } versal_v0p88[1]; + field { + desc = "Metro only. Versal transceiver AVCC 0.92V (discrete pg, cascade enable)"; + } versal_v0p92_avcc[1]; + field { + desc = "Metro only. Versal 1.1V"; + } versal_v1p1[1]; + field { + desc = "Metro only. Versal transceiver AVTT 1.2V (discrete pg, cascade enable)"; + } versal_v1p2_avtt[1]; + field { + desc = "Metro only. Versal 1.4V"; + } versal_v1p4[1]; + field { + desc = "Metro only. Versal VCCAUX 1.5V"; + } versal_v1p5[1]; + field { + desc = "Metro only. Versal transceiver AVCCAUX 1.5V"; + } versal_v1p5_avccaux[1]; + field { + desc = "Metro only. Versal VCCO 1.8V"; + } versal_v1p8[1]; + field { + desc = "Metro only. Versal VCCO 3.3V"; + } versal_v3p3[1]; }; rails #(.sw_kind(rw)) rail_enables ; @@ -540,5 +614,116 @@ This regulator controls V1P1_SP5_A0, V1P8_SP5_A0 and V3P3_SP5_A0 rails."; } clk_en[0:0] = 0; } pcie_clk_ctrl; + // Everything below is metro's Versal NIC. Cosmo reads zeros here. + + reg { + name = "Versal-related signal readbacks"; + desc = "Metro only. Live status and readbacks of our registers and responses back from the Versal. + For tri-state cases, read '1' as 'Z'. No inversions done here."; + default sw = r; + field { + desc = "POR_B live status, (From FPGA to Versal)"; + } por_b[1]; + field { + desc = "MODE[3:0] live status, (From FPGA to Versal)"; + } mode[4]; + field { + desc = "mode_buffer_en_l live status, (From FPGA to the MODE buffer)"; + } mode_buffer_en_l[1]; + field { + desc = "err_done_buff_en live status, (From FPGA to the DONE/ERROR_OUT buffer)"; + } err_done_buff_en[1]; + field { + desc = "DONE live status, note: only valid once err_done_buff_en is set (From Versal)"; + } done[1]; + field { + desc = "ERROR_OUT live status, note: only valid once err_done_buff_en is set (From Versal)"; + } error_out[1]; + field { + desc = "Channel A perst_l live status, (From FPGA to Versal)"; + } cha_perst_l[1]; + field { + desc = "Channel A prsnt_l live status, (From Versal)"; + } cha_prsnt_l[1]; + field { + desc = "Channel A pwren_l live status, (From Versal)"; + } cha_pwren_l[1]; + field { + desc = "Channel A clk_buff_oe_l live status, (From FPGA to buffer)"; + } cha_clk_buff_oe_l[1]; + field { + desc = "Channel B perst_l live status, (From FPGA to Versal)"; + } chb_perst_l[1]; + field { + desc = "Channel B prsnt_l live status, (From Versal)"; + } chb_prsnt_l[1]; + field { + desc = "Channel B pwren_l live status, (From Versal)"; + } chb_pwren_l[1]; + field { + desc = "Channel B clk_buff_oe_l live status, (From FPGA to buffer)"; + } chb_clk_buff_oe_l[1]; + field { + desc = "Channel A slot sp5_perst_l live status, (From SP5 hotplug to FPGA)"; + } sp5_cha_perst_l[1]; + field { + desc = "Channel B slot sp5_perst_l live status, (From SP5 hotplug to FPGA)"; + } sp5_chb_perst_l[1]; + } versal_readbacks; + + reg { + name = "Versal Overrides"; + desc = "Metro only. Control over the Versal signals for debugging purposes outside the + sequencer logic, muxed in by debug_enables.nic_override. A test MAPO is raised + through nic_overrides.nic_test_mapo as on cosmo."; + field { + desc = "por_b drive, (From FPGA to Versal)"; + } por_b[1] = 0; + field { + desc = "mode_buffer_en_l drive, (From FPGA to the MODE buffer)"; + } mode_buffer_en_l[1] = 1; + field { + desc = "err_done_buff_en drive, (From FPGA to the DONE/ERROR_OUT buffer)"; + } err_done_buff_en[1] = 0; + field { + desc = "Channel A perst_l drive, (From FPGA to Versal)"; + } cha_perst_l[1] = 0; + field { + desc = "Channel A clk_buff_oe_l drive, (From FPGA to buffer)"; + } cha_clk_buff_oe_l[1] = 1; + field { + desc = "Channel B perst_l drive, (From FPGA to Versal)"; + } chb_perst_l[1] = 0; + field { + desc = "Channel B clk_buff_oe_l drive, (From FPGA to buffer)"; + } chb_clk_buff_oe_l[1] = 1; + } versal_overrides; + + reg { + name = "Versal Boot Control"; + desc = "Metro only. Boot-mode straps driven onto the Versal's MODE[3:0] pins while POR_B is held. + The sequencer latches these when it leaves the RAILS_SETTLE state, so changing them + only takes effect on the next Versal power-up."; + field { + desc = "Versal boot mode driven onto MODE[3:0]. 0x0 is JTAG, 0x2 is QSPI32, which + is how the board is wired. JTAG is the escape hatch for bring-up."; + } mode[3:0] = 4'h2; + field { + desc = "Measure the boot image with the hash engine before releasing POR_B. + The range comes from the hash engine's HW_FLASH_ADDR/HW_LENGTH. A failed + measurement is recorded in the status register and the boot goes ahead. + Clear to skip the measurement."; + } hash_image[4:4] = 1; + } versal_boot_ctrl; + + reg { + name = "Board Version"; + desc = "Metro only. Board version straps read directly off the FPGA1_VERSION_ID pins."; + default sw = r; + field { + desc = "FPGA1_VERSION_ID[1:0] strap value"; + } version_id[1:0]; + } board_version; + }; \ No newline at end of file diff --git a/hdl/projects/cosmo_seq/sequencer/sequencer_regs.vhd b/hdl/projects/cosmo_seq/sequencer/sequencer_regs.vhd index 49841d51..7bf9dc7c 100644 --- a/hdl/projects/cosmo_seq/sequencer/sequencer_regs.vhd +++ b/hdl/projects/cosmo_seq/sequencer/sequencer_regs.vhd @@ -10,6 +10,7 @@ use ieee.numeric_std_unsigned.all; use work.axil_common_pkg.all; use work.axil8x32_pkg; +use work.sp5_power_pkg.all; use work.sequencer_io_pkg.all; use work.sequencer_regs_pkg.all; @@ -44,6 +45,20 @@ entity sequencer_regs is -- misc readbacks sp5_readbacks : in sp5_readbacks_type; nic_readbacks : in nic_readbacks_type; + -- power-good summaries for the status register + fans_power_ok : in std_logic; + a0_ok : in std_logic; + nic_power_ok : in std_logic; + -- Metro's Versal NIC. A board without one leaves these at their + -- defaults and the registers read as zero. + nic_done : in std_logic := '0'; + versal_error_out : in std_logic := '0'; + nic_hash_done : in std_logic := '0'; + nic_hash_err : in std_logic := '0'; + versal_readbacks : in versal_readbacks_type; + versal_overrides : out versal_overrides_type; + versal_boot_ctrl : out versal_boot_ctrl_type; + board_version : in board_version_type; -- Ignition mux and reconfig control ignition_mux_sel : out std_logic; ignition_creset : out std_logic; @@ -84,6 +99,7 @@ architecture rtl of sequencer_regs is signal a0_en_last : std_logic; signal rails_pg_max : rails_type; + signal irq_pending : std_logic; signal rdata : std_logic_vector(31 downto 0); signal active_read : std_logic; @@ -98,6 +114,9 @@ architecture rtl of sequencer_regs is -- irq block so it will handle things correctly. constant level_edge_n : irq_type := ( + nic_hash_err => EDGE, + versal_error_out => LEVEL, + pwr_cont4_to_fpga1_alert => LEVEL, pwr_cont3_to_fpga1_alert => LEVEL, pwr_cont2_to_fpga1_alert => LEVEL, pwr_cont1_to_fpga1_alert => LEVEL, @@ -126,12 +145,26 @@ architecture rtl of sequencer_regs is begin + status <= ( + fanpwrok => fans_power_ok, + a0pwrok => a0_ok, + nicpwrok => nic_power_ok, + nicdone => nic_done, + nic_hash_done => nic_hash_done, + nic_hash_err => nic_hash_err, + int_pend => irq_pending + ); + irq_pending <= '1' when (compress(ifr) and compress(ier)) /= (compress(ier)'range => '0') else '0'; + ignition_mux_sel <= ignition_control.mux_to_ignition; ignition_creset <= ignition_control.ignition_creset; allow_backplane_pcie_clk <= pcie_clk_ctrl.clk_en; -- Map a bunch of discrete signals into the irq_raw vector. irq_raw <= ( + nic_hash_err => nic_hash_err, + versal_error_out => versal_error_out, + pwr_cont4_to_fpga1_alert => not reg_alert_l.pwr_cont4_to_fpga1_alert_l, pwr_cont3_to_fpga1_alert => not reg_alert_l.pwr_cont3_to_fpga1_alert_l, pwr_cont2_to_fpga1_alert => not reg_alert_l.pwr_cont2_to_fpga1_alert_l, pwr_cont1_to_fpga1_alert => not reg_alert_l.pwr_cont1_to_fpga1_alert_l, @@ -294,6 +327,8 @@ begin rails_pg_max <= reset_0s; debug_enables <= rec_reset; nic_overrides <= rec_reset; + versal_overrides <= rec_reset; + versal_boot_ctrl <= rec_reset; ignition_control <= rec_reset; pcie_clk_ctrl <= rec_reset; rail_masks <= reset_0s; @@ -303,6 +338,13 @@ begin irq_clear <= reset_0s; -- clear single-cycle flags. igr <= reset_0s; nic_overrides.nic_test_mapo <= '0'; -- Clear test MAPO bit every cycle, so it's a single-cycle pulse when set. + -- Max hold of the live power goods, cleared on a fresh sequence up + -- or by writing the register. + if a0_en_redge then + rails_pg_max <= reset_0s; + else + rails_pg_max <= rails_pg_max or rails_pg_rdbk; + end if; if active_write then case to_integer(axi_if.write_address.addr) is @@ -315,6 +357,8 @@ begin when RAIL_PGS_MAX_HOLD_OFFSET => rails_pg_max <= reset_0s; when DEBUG_ENABLES_OFFSET => debug_enables <= unpack(axi_if.write_data.data); when NIC_OVERRIDES_OFFSET => nic_overrides <= unpack(axi_if.write_data.data); + when VERSAL_OVERRIDES_OFFSET => versal_overrides <= unpack(axi_if.write_data.data); + when VERSAL_BOOT_CTRL_OFFSET => versal_boot_ctrl <= unpack(axi_if.write_data.data); when RAIL_MASKS_OFFSET => rail_masks <= unpack(axi_if.write_data.data); when SP5_SEQ_TEST_MASK_OFFSET => sp5_seq_test_mask <= unpack(axi_if.write_data.data); when IGNITION_CONTROL_OFFSET => ignition_control <= unpack(axi_if.write_data.data); @@ -362,6 +406,10 @@ begin when NIC_OVERRIDES_OFFSET => rdata <= pack(nic_overrides); when IGNITION_CONTROL_OFFSET => rdata <= pack(ignition_control); when PCIE_CLK_CTRL_OFFSET => rdata <= pack(pcie_clk_ctrl); + when VERSAL_READBACKS_OFFSET => rdata <= pack(versal_readbacks); + when VERSAL_OVERRIDES_OFFSET => rdata <= pack(versal_overrides); + when VERSAL_BOOT_CTRL_OFFSET => rdata <= pack(versal_boot_ctrl); + when BOARD_VERSION_OFFSET => rdata <= pack(board_version); when others => rdata <= (others => '0'); end case; end if; diff --git a/hdl/projects/cosmo_seq/sequencer/sims/cascade_rail_model.vhd b/hdl/projects/cosmo_seq/sequencer/sims/cascade_rail_model.vhd index c0cdb038..4046ad6c 100644 --- a/hdl/projects/cosmo_seq/sequencer/sims/cascade_rail_model.vhd +++ b/hdl/projects/cosmo_seq/sequencer/sims/cascade_rail_model.vhd @@ -6,7 +6,7 @@ library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; -use work.sequencer_io_pkg.all; +use work.sp5_power_pkg.all; entity cascade_rail_model is port ( diff --git a/hdl/projects/cosmo_seq/sequencer/sims/nic_model.vhd b/hdl/projects/cosmo_seq/sequencer/sims/nic_model.vhd index 68a091c8..986331b5 100644 --- a/hdl/projects/cosmo_seq/sequencer/sims/nic_model.vhd +++ b/hdl/projects/cosmo_seq/sequencer/sims/nic_model.vhd @@ -13,6 +13,7 @@ library vunit_lib; context vunit_lib.vunit_context; context vunit_lib.com_context; +use work.sp5_power_pkg.all; use work.sequencer_io_pkg.all; use work.nic_model_msg_pkg.all; use work.nic_model_msg_pkg.disable_rail_msg; diff --git a/hdl/projects/cosmo_seq/sequencer/sims/rail_model.vhd b/hdl/projects/cosmo_seq/sequencer/sims/rail_model.vhd index f1e3863a..7add7fe3 100644 --- a/hdl/projects/cosmo_seq/sequencer/sims/rail_model.vhd +++ b/hdl/projects/cosmo_seq/sequencer/sims/rail_model.vhd @@ -10,7 +10,7 @@ library vunit_lib; context vunit_lib.vunit_context; context vunit_lib.com_context; -use work.sequencer_io_pkg.all; +use work.sp5_power_pkg.all; use work.rail_model_msg_pkg.all; entity rail_model is diff --git a/hdl/projects/cosmo_seq/sequencer/sims/sp5_seq_sim_pkg.vhd b/hdl/projects/cosmo_seq/sequencer/sims/sp5_seq_sim_pkg.vhd index 9733554b..255b56b6 100644 --- a/hdl/projects/cosmo_seq/sequencer/sims/sp5_seq_sim_pkg.vhd +++ b/hdl/projects/cosmo_seq/sequencer/sims/sp5_seq_sim_pkg.vhd @@ -53,6 +53,20 @@ package sp5_seq_sim_pkg is constant rail_name : in string ); + -- Bring the board to A0 and the NIC all the way to DONE. + procedure power_up_to_nic_done ( + signal net : inout network_t + ); + + -- Drop one Versal rail (a rail_model of its own, unlike the T6's rails + -- which sit behind nic_model) once the Versal is up and check the NIC + -- MAPO path, including that the flag can be cleared afterwards. + procedure test_versal_rail_mapo_fault_injection ( + signal net : inout network_t; + constant rail_actor : in actor_t; + constant rail_name : in string + ); + end package; package body sp5_seq_sim_pkg is @@ -202,4 +216,67 @@ package body sp5_seq_sim_pkg is info("NIC MAPO fault injection test completed successfully for rail: " & rail_name); end procedure; + procedure power_up_to_nic_done ( + signal net : inout network_t + ) is + variable read_data : std_logic_vector(31 downto 0); + variable seq_state : seq_api_status_a0_sm; + variable nic_state : nic_api_status_nic_sm; + begin + write_bus(net, bus_handle, + To_StdLogicVector(POWER_CTRL_OFFSET, bus_handle.p_address_length), + POWER_CTRL_A0_EN_MASK); + poll_for_seq_state(net, DONE); + read_bus(net, bus_handle, + To_StdLogicVector(SEQ_API_STATUS_OFFSET, bus_handle.p_address_length), + read_data); + seq_state := encode(read_data(7 downto 0)); + check_equal(seq_state = DONE, true, "Expected A0 sequencer to be in DONE state"); + + poll_for_nic_state(net, DONE); + read_bus(net, bus_handle, + To_StdLogicVector(NIC_API_STATUS_OFFSET, bus_handle.p_address_length), + read_data); + nic_state := encode(read_data(7 downto 0)); + check_equal(nic_state = DONE, true, "Expected NIC sequencer to be in DONE state"); + end procedure; + + procedure test_versal_rail_mapo_fault_injection ( + signal net : inout network_t; + constant rail_actor : in actor_t; + constant rail_name : in string + ) is + variable read_data : std_logic_vector(31 downto 0); + variable nic_state : nic_api_status_nic_sm; + begin + power_up_to_nic_done(net); + + info("Injecting Versal fault on rail: " & rail_name); + disable_power_good(net, rail_actor); + wait for 100 us; + + read_bus(net, bus_handle, + To_StdLogicVector(IFR_OFFSET, bus_handle.p_address_length), read_data); + check_equal((read_data and IFR_NICMAPO_MASK) /= x"00000000", true, + "Expected NICMAPO bit to be set in IFR for rail: " & rail_name); + + read_bus(net, bus_handle, + To_StdLogicVector(NIC_API_STATUS_OFFSET, bus_handle.p_address_length), + read_data); + nic_state := encode(read_data(7 downto 0)); + check_equal(nic_state = IDLE, true, + "Expected NIC sequencer to return to IDLE after MAPO on " & rail_name); + + info("Clearing NICMAPO bit in IFR"); + write_bus(net, bus_handle, + To_StdLogicVector(IFR_OFFSET, bus_handle.p_address_length), + IFR_NICMAPO_MASK); + read_bus(net, bus_handle, + To_StdLogicVector(IFR_OFFSET, bus_handle.p_address_length), read_data); + check_equal((read_data and IFR_NICMAPO_MASK) = x"00000000", true, + "Expected NICMAPO bit to clear for rail: " & rail_name); + + enable_power_good(net, rail_actor); + end procedure; + end package body; \ No newline at end of file diff --git a/hdl/projects/cosmo_seq/sequencer/sims/sp5_seq_sim_tb.vhd b/hdl/projects/cosmo_seq/sequencer/sims/sp5_seq_sim_tb.vhd index a28e2661..72d2e496 100644 --- a/hdl/projects/cosmo_seq/sequencer/sims/sp5_seq_sim_tb.vhd +++ b/hdl/projects/cosmo_seq/sequencer/sims/sp5_seq_sim_tb.vhd @@ -14,6 +14,7 @@ library vunit_lib; use work.sp5_seq_sim_pkg.all; use work.sequencer_regs_pkg.all; +use work.sequencer_io_pkg.all; use work.rail_model_msg_pkg; use work.nic_model_msg_pkg.all; @@ -29,11 +30,11 @@ architecture tb of sp5_seq_sim_tb is begin - th: entity work.sp5_seq_sim_th; + th: entity work.sp5_seq_sim_th generic map (NIC_KIND => NIC_T6); bench: process alias reset is << signal th.reset : std_logic >>; - alias sp5_t6_perst_l is << signal th.sp5_t6_perst_l : std_logic >>; + alias sp5_t6_perst_l is << signal th.sp5_nic_perst_l : std_logic >>; variable read_data : std_logic_vector(31 downto 0); variable seq_state : seq_api_status_a0_sm; constant grpa_v3p3_actor : actor_t := find("grpa_v3p3_sp5_a1"); diff --git a/hdl/projects/cosmo_seq/sequencer/sims/sp5_seq_sim_th.vhd b/hdl/projects/cosmo_seq/sequencer/sims/sp5_seq_sim_th.vhd index 0634d42e..dfbb33da 100644 --- a/hdl/projects/cosmo_seq/sequencer/sims/sp5_seq_sim_th.vhd +++ b/hdl/projects/cosmo_seq/sequencer/sims/sp5_seq_sim_th.vhd @@ -12,10 +12,18 @@ library vunit_lib; context vunit_lib.vc_context; use work.axil8x32_pkg; +use work.sp5_power_pkg.all; use work.sequencer_io_pkg.all; use work.sp5_seq_sim_pkg.all; +-- One harness for both NIC flavours: NIC_KIND picks which NIC's models are +-- instantiated, and the testbenches (sp5_seq_sim_tb for the T6, versal_seq_sim_tb +-- for the Versal) each set it. Both NICs' pin records exist as signals so the +-- testbenches can reach them; the absent NIC's are left at their tie-off values. entity sp5_seq_sim_th is + generic ( + NIC_KIND : nic_kind_t := NIC_T6 + ); end entity; architecture th of sp5_seq_sim_th is @@ -48,6 +56,7 @@ architecture th of sp5_seq_sim_th is fan_west_hsc_disable => 'Z' ); signal reg_alert_l_pins : seq_power_alert_pins_t := ( + pwr_cont4_to_fpga1_alert_l => '1', smbus_fan_central_hsc_to_fpga1_alert_l => '1', smbus_fan_east_hsc_to_fpga1_alert_l => '1', smbus_fan_west_hsc_to_fpga1_alert_l => '1', @@ -67,12 +76,31 @@ architecture th of sp5_seq_sim_th is pwr_cont3_to_fpga1_alert_l => '1' ); signal ddr_bulk_pins : ddr_bulk_power_t; - signal nic_rails_pins : nic_power_t; + signal nic_rails_pins : nic_power_t := nic_power_absent; signal a0_ok : std_logic; signal a0_idle : std_logic; - signal sp5_t6_perst_l : std_logic := '1'; + signal sp5_nic_perst_l : std_logic := '1'; + signal sp5_nic_chb_perst_l : std_logic := '1'; signal axi_if : axil8x32_pkg.axil_t; - signal nic_dbg_pins : t6_debug_if; + signal nic_dbg_pins : nic_debug_if; + + -- Versal side + signal versal_rails_pins : versal_power_t := versal_power_absent; + signal versal_boot_pins : versal_boot_t := versal_boot_absent; + signal versal_pcie_pins : versal_pcie_t := ( + cha => (perst_l => 'Z', prsnt_l => '0', pwren_l => '0', clk_buff_oe_l => 'Z'), + chb => (perst_l => 'Z', prsnt_l => '0', pwren_l => '0', clk_buff_oe_l => 'Z') + ); + signal versal_held_in_reset : std_logic; + signal flash_owned_by_seq : std_logic; + -- Hash engine stand-in. The testbench sets how long a measurement takes + -- and whether it fails; the handshake itself is modelled here. + signal hash_req : std_logic; + signal hash_ack : std_logic := '0'; + signal hash_err : std_logic := '0'; + signal hash_model_time : time := 20 us; + signal hash_model_fail : boolean := false; + signal hash_requests : natural := 0; begin @@ -83,7 +111,8 @@ begin -- instantiate the sequencer dut: entity work.sp5_sequencer generic map( - CNTS_P_MS => 100 + CNTS_P_MS => 100, + NIC_KIND => NIC_KIND ) port map( clk => clk, @@ -99,10 +128,20 @@ begin sp5_seq_pins => sp5_seq_pins, nic_rails_pins => nic_rails_pins, nic_seq_pins => nic_seq_pins, + versal_rails_pins => versal_rails_pins, + versal_boot_pins => versal_boot_pins, + versal_pcie_pins => versal_pcie_pins, + versal_held_in_reset => versal_held_in_reset, + flash_owned_by_seq => flash_owned_by_seq, + hash_req => hash_req, + hash_ack => hash_ack, + hash_err => hash_err, + version_id => "01", nic_dbg_pins => nic_dbg_pins, - sp5_t6_perst_l => sp5_t6_perst_l, - irq_l_out => open, - reg_alert_l_pins => reg_alert_l_pins + sp5_nic_perst_l => sp5_nic_perst_l, + sp5_nic_chb_perst_l => sp5_nic_chb_perst_l, + irq_l_out => open, + reg_alert_l_pins => reg_alert_l_pins ); axi_lite_master_inst: entity vunit_lib.axi_lite_master @@ -228,14 +267,76 @@ begin reset => reset, sp5_pins => sp5_seq_pins ); - nic_model_inst: entity work.nic_model - generic map( - actor_name => "nic_model" - ) - port map( - clk => clk, - reset => reset, - nic_rails => nic_rails_pins - ); + t6: if NIC_KIND = NIC_T6 generate + nic_model_inst: entity work.nic_model + generic map( + actor_name => "nic_model" + ) + port map( + clk => clk, + reset => reset, + nic_rails => nic_rails_pins + ); + end generate; + + versal: if NIC_KIND = NIC_VERSAL generate + versal_v3p3: entity work.rail_model + generic map(actor_name => "versal_v3p3") + port map(clk => clk, reset => reset, rail => versal_rails_pins.v3p3); + versal_v1p8: entity work.rail_model + generic map(actor_name => "versal_v1p8") + port map(clk => clk, reset => reset, rail => versal_rails_pins.v1p8); + versal_v1p5: entity work.rail_model + generic map(actor_name => "versal_v1p5") + port map(clk => clk, reset => reset, rail => versal_rails_pins.v1p5); + versal_v1p5_avccaux: entity work.rail_model + generic map(actor_name => "versal_v1p5_avccaux") + port map(clk => clk, reset => reset, rail => versal_rails_pins.v1p5_avccaux); + versal_v1p4: entity work.rail_model + generic map(actor_name => "versal_v1p4") + port map(clk => clk, reset => reset, rail => versal_rails_pins.v1p4); + versal_v1p1: entity work.rail_model + generic map(actor_name => "versal_v1p1") + port map(clk => clk, reset => reset, rail => versal_rails_pins.v1p1); + versal_v0p88: entity work.rail_model + generic map(actor_name => "versal_v0p88") + port map(clk => clk, reset => reset, rail => versal_rails_pins.v0p88); + versal_v0p8_vccint: entity work.rail_model + generic map(actor_name => "versal_v0p8_vccint") + port map(clk => clk, reset => reset, rail => versal_rails_pins.v0p8_vccint); + -- The two transceiver rails have no enable of their own; they cascade + -- off the group that brings them up. + versal_v0p92_avcc: entity work.cascade_rail_model + port map(clk => clk, reset => reset, upstream_pg => versal_rails_pins.v0p88.pg, rail => versal_rails_pins.v0p92_avcc); + versal_v1p2_avtt: entity work.cascade_rail_model + port map(clk => clk, reset => reset, upstream_pg => versal_rails_pins.v1p5.pg, rail => versal_rails_pins.v1p2_avtt); + + -- Four-phase handshake as hash_engine_top does it: acknowledge some + -- time after the request, hold it until the request drops. + hash_model: process + begin + wait until hash_req = '1'; + hash_requests <= hash_requests + 1; + wait for hash_model_time; + if hash_req = '1' then + hash_err <= '1' when hash_model_fail else '0'; + hash_ack <= '1'; + wait until hash_req = '0'; + hash_ack <= '0'; + end if; + end process; + + versal_model_inst: entity work.versal_model + generic map( + actor_name => "versal_model" + ) + port map( + clk => clk, + reset => reset, + hsc_12v => versal_rails_pins.hsc_12v, + hsc_5v => versal_rails_pins.hsc_5v, + boot => versal_boot_pins + ); + end generate; end th; \ No newline at end of file diff --git a/hdl/projects/cosmo_seq/sequencer/sims/sp5_sim.vhd b/hdl/projects/cosmo_seq/sequencer/sims/sp5_sim.vhd index 0d7fe324..83fd87b6 100644 --- a/hdl/projects/cosmo_seq/sequencer/sims/sp5_sim.vhd +++ b/hdl/projects/cosmo_seq/sequencer/sims/sp5_sim.vhd @@ -6,7 +6,7 @@ library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; -use work.sequencer_io_pkg.all; +use work.sp5_power_pkg.all; entity sp5_model is port ( diff --git a/hdl/projects/cosmo_seq/sequencer/sims/versal_model.vhd b/hdl/projects/cosmo_seq/sequencer/sims/versal_model.vhd new file mode 100644 index 00000000..38556b80 --- /dev/null +++ b/hdl/projects/cosmo_seq/sequencer/sims/versal_model.vhd @@ -0,0 +1,100 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +library vunit_lib; + context vunit_lib.vunit_context; + context vunit_lib.com_context; + +use work.sp5_power_pkg.all; +use work.sequencer_io_pkg.all; +use work.versal_model_msg_pkg.all; + +-- Boot-side model of the VP1202. The rails are modelled separately by +-- rail_model instances in the harness; this covers the hotswap pair, whose +-- power-good pins are active low at the FPGA, and the POR_B/DONE handshake. +entity versal_model is + generic ( + actor_name : string := "versal_model"; + -- How long after POR_B releases the device takes to assert DONE. + boot_time : time := 50 us + ); + port ( + clk : in std_logic; + reset : in std_logic; + + hsc_12v : view power_rail_at_reg; + hsc_5v : view cascade_power_rail_at_reg; + + boot : view versal_boot_at_versal + ); +end entity; + +architecture model of versal_model is + + signal boot_allowed : boolean := true; + signal force_error_out : boolean := false; + signal done_int : std_logic := '0'; + signal error_out_int : std_logic := '0'; + +begin + + msg_handler : process + variable self : actor_t; + variable msg_type : msg_type_t; + variable request_msg : msg_t; + begin + self := new_actor(actor_name); + loop + receive(net, self, request_msg); + msg_type := message_type(request_msg); + if msg_type = fail_boot_msg then + info("versal_model: boot will not complete"); + boot_allowed <= false; + elsif msg_type = allow_boot_msg then + info("versal_model: boot allowed"); + boot_allowed <= true; + elsif msg_type = assert_error_out_msg then + info("versal_model: asserting ERROR_OUT"); + force_error_out <= true; + elsif msg_type = clear_error_out_msg then + info("versal_model: clearing ERROR_OUT"); + force_error_out <= false; + else + unexpected_msg_type(msg_type); + end if; + end loop; + wait; + end process; + + -- Hotswap power good is active low at the FPGA pins, and the 5V hotswap + -- cascades off the 12V one. + hsc_12v.pg <= not hsc_12v.enable; + hsc_5v.pg <= not hsc_12v.enable; + + -- DONE comes up some time after POR_B releases, and drops again whenever + -- the device is put back into reset. + boot_sm : process + begin + wait until rising_edge(boot.por_b); + if boot_allowed then + wait for boot_time; + if boot.por_b = '1' then + done_int <= '1'; + end if; + end if; + wait until falling_edge(boot.por_b); + done_int <= '0'; + end process; + + -- The DONE and ERROR_OUT buffers are only enabled by the sequencer once it + -- is looking at them; before that the pins read as their idle level. + boot.done <= done_int when boot.err_done_buff_en = '1' else '0'; + error_out_int <= '1' when force_error_out else '0'; + boot.error_out <= error_out_int when boot.err_done_buff_en = '1' else '0'; + +end model; diff --git a/hdl/projects/cosmo_seq/sequencer/sims/versal_model_msg_pkg.vhd b/hdl/projects/cosmo_seq/sequencer/sims/versal_model_msg_pkg.vhd new file mode 100644 index 00000000..ea75d716 --- /dev/null +++ b/hdl/projects/cosmo_seq/sequencer/sims/versal_model_msg_pkg.vhd @@ -0,0 +1,85 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +library vunit_lib; + context vunit_lib.vunit_context; + context vunit_lib.com_context; + +-- Messages for driving the Versal boot model's failure modes. The rails +-- themselves are separate rail_model instances, so this package only covers +-- what the device does after its rails are up. +package versal_model_msg_pkg is + + constant fail_boot_msg : msg_type_t := new_msg_type("fail_boot"); + constant allow_boot_msg : msg_type_t := new_msg_type("allow_boot"); + constant assert_error_out_msg : msg_type_t := new_msg_type("assert_error_out"); + constant clear_error_out_msg : msg_type_t := new_msg_type("clear_error_out"); + + -- Stop the model from ever asserting DONE, so the sequencer's boot + -- timeout is exercised. + procedure fail_boot ( + signal net : inout network_t; + constant actor : actor_t + ); + + procedure allow_boot ( + signal net : inout network_t; + constant actor : actor_t + ); + + procedure assert_error_out ( + signal net : inout network_t; + constant actor : actor_t + ); + + procedure clear_error_out ( + signal net : inout network_t; + constant actor : actor_t + ); + +end package; + +package body versal_model_msg_pkg is + + procedure fail_boot ( + signal net : inout network_t; + constant actor : actor_t + ) is + variable request_msg : msg_t := new_msg(fail_boot_msg); + begin + send(net, actor, request_msg); + end; + + procedure allow_boot ( + signal net : inout network_t; + constant actor : actor_t + ) is + variable request_msg : msg_t := new_msg(allow_boot_msg); + begin + send(net, actor, request_msg); + end; + + procedure assert_error_out ( + signal net : inout network_t; + constant actor : actor_t + ) is + variable request_msg : msg_t := new_msg(assert_error_out_msg); + begin + send(net, actor, request_msg); + end; + + procedure clear_error_out ( + signal net : inout network_t; + constant actor : actor_t + ) is + variable request_msg : msg_t := new_msg(clear_error_out_msg); + begin + send(net, actor, request_msg); + end; + +end package body; diff --git a/hdl/projects/cosmo_seq/sequencer/sims/versal_seq_sim_tb.vhd b/hdl/projects/cosmo_seq/sequencer/sims/versal_seq_sim_tb.vhd new file mode 100644 index 00000000..9cd62be7 --- /dev/null +++ b/hdl/projects/cosmo_seq/sequencer/sims/versal_seq_sim_tb.vhd @@ -0,0 +1,271 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; +use ieee.numeric_std_unsigned.all; + +library vunit_lib; + context vunit_lib.com_context; + context vunit_lib.vunit_context; + context vunit_lib.vc_context; + +use work.sequencer_regs_pkg.all; +use work.sequencer_io_pkg.all; +use work.sp5_seq_sim_pkg.all; +use work.rail_model_msg_pkg.all; +use work.versal_model_msg_pkg; + +entity versal_seq_sim_tb is + generic ( + runner_cfg : string + ); +end entity; + +architecture tb of versal_seq_sim_tb is +begin + + th: entity work.sp5_seq_sim_th generic map (NIC_KIND => NIC_VERSAL); + + bench: process + alias reset is << signal th.reset : std_logic >>; + alias versal_held_in_reset is << signal th.versal_held_in_reset : std_logic >>; + alias versal_pcie_pins is << signal th.versal_pcie_pins : versal_pcie_t >>; + alias sp5_versal_cha_perst_l is << signal th.sp5_nic_perst_l : std_logic >>; + alias sp5_versal_chb_perst_l is << signal th.sp5_nic_chb_perst_l : std_logic >>; + alias flash_owned_by_seq is << signal th.flash_owned_by_seq : std_logic >>; + alias hash_req is << signal th.hash_req : std_logic >>; + alias hash_model_fail is << signal th.hash_model_fail : boolean >>; + alias hash_requests is << signal th.hash_requests : natural >>; + alias versal_boot_pins is << signal th.versal_boot_pins : versal_boot_t >>; + constant versal_actor : actor_t := find("versal_model"); + variable read_data : std_logic_vector(31 downto 0); + variable versal_state : nic_api_status_nic_sm; + variable rails_pg : rails_type; + variable readbacks : versal_readbacks_type; + variable status : status_type; + variable version : board_version_type; + begin + test_runner_setup(runner, runner_cfg); + wait until reset = '0'; + wait for 500 ns; + + while test_suite loop + if run("normal_power_up") then + power_up_to_nic_done(net); + + -- Every rail should read back good once we are up. + read_bus(net, bus_handle, + To_StdLogicVector(RAIL_PGS_OFFSET, bus_handle.p_address_length), + read_data); + rails_pg := unpack(read_data); + check_equal(rails_pg.versal_v0p8_vccint, '1', + "Expected the Versal VCCINT rail to read power good"); + check_equal(rails_pg.versal_v3p3, '1', + "Expected the Versal 3V3 rail to read power good"); + + -- and the status register should agree. + read_bus(net, bus_handle, + To_StdLogicVector(STATUS_OFFSET, bus_handle.p_address_length), + read_data); + status := unpack(read_data); + check_equal(status.nicpwrok, '1', + "Expected versalpwrok in the status register"); + check_equal(status.nicdone, '1', + "Expected versaldone in the status register"); + + elsif run("boot_mode_is_strapped") then + -- The default boot mode is QSPI32; check it reaches the pins. + power_up_to_nic_done(net); + read_bus(net, bus_handle, + To_StdLogicVector(VERSAL_READBACKS_OFFSET, bus_handle.p_address_length), + read_data); + readbacks := unpack(read_data); + check_equal(readbacks.mode, std_logic_vector'(x"2"), + "Expected MODE[3:0] to be strapped to QSPI32"); + check_equal(readbacks.mode_buffer_en_l, '0', + "Expected the MODE buffer to be enabled"); + + elsif run("pcie_resets_follow_their_slots") then + -- Neither channel leaves reset before the Versal is booted, + -- and afterwards each follows only its own slot's power + -- enable from the SP5 hotplug controller. + check_equal(versal_pcie_pins.cha.perst_l, '0', + "Expected channel A PERST asserted before boot"); + check_equal(versal_pcie_pins.chb.perst_l, '0', + "Expected channel B PERST asserted before boot"); + power_up_to_nic_done(net); + wait for 100 ns; + check_equal(versal_pcie_pins.cha.perst_l, '1', + "Expected channel A PERST released once booted"); + check_equal(versal_pcie_pins.chb.perst_l, '1', + "Expected channel B PERST released once booted"); + + sp5_versal_chb_perst_l <= '0'; + wait for 100 ns; + check_equal(versal_pcie_pins.cha.perst_l, '1', + "Expected channel A PERST unaffected by slot B"); + check_equal(versal_pcie_pins.chb.perst_l, '0', + "Expected channel B PERST to follow slot B"); + read_bus(net, bus_handle, + To_StdLogicVector(VERSAL_READBACKS_OFFSET, bus_handle.p_address_length), + read_data); + readbacks := unpack(read_data); + check_equal(readbacks.sp5_cha_perst_l, '1', "Expected slot A readback high"); + check_equal(readbacks.sp5_chb_perst_l, '0', "Expected slot B readback low"); + check_equal(readbacks.chb_perst_l, '0', "Expected channel B PERST readback low"); + + sp5_versal_chb_perst_l <= '1'; + sp5_versal_cha_perst_l <= '0'; + wait for 100 ns; + check_equal(versal_pcie_pins.cha.perst_l, '0', + "Expected channel A PERST to follow slot A"); + check_equal(versal_pcie_pins.chb.perst_l, '1', + "Expected channel B PERST unaffected by slot A"); + sp5_versal_cha_perst_l <= '1'; + + elsif run("flash_mux_interlock") then + -- The SP may only take the boot flash while POR_B is asserted; + -- after boot the sequencer holds it for the SP5 instead. + check_equal(versal_held_in_reset, '1', + "Expected the Versal to be held in reset before power up"); + check_equal(flash_owned_by_seq, '0', + "Expected no sequencer claim on the flash before power up"); + power_up_to_nic_done(net); + check_equal(versal_held_in_reset, '0', + "Expected the SP's request to be denied once the Versal has booted"); + check_equal(flash_owned_by_seq, '1', + "Expected the sequencer to hold the flash for the SP5 after boot"); + + elsif run("image_is_measured_before_boot") then + -- The sequencer asks the hash engine for a measurement with + -- the rails up, POR_B still held and the flash on the FPGA + -- side, and only releases POR_B once it has an answer. + write_bus(net, bus_handle, + To_StdLogicVector(POWER_CTRL_OFFSET, bus_handle.p_address_length), + POWER_CTRL_A0_EN_MASK); + wait until hash_req = '1' for 50 ms; + check_equal(hash_req, '1', "Expected a measurement request"); + check_equal(versal_boot_pins.por_b, '0', "Expected POR_B held during the measurement"); + check_equal(flash_owned_by_seq, '1', "Expected the flash on the FPGA side during the measurement"); + read_bus(net, bus_handle, + To_StdLogicVector(NIC_API_STATUS_OFFSET, bus_handle.p_address_length), + read_data); + versal_state := encode(read_data(7 downto 0)); + check_equal(versal_state = MEASURING, true, "Expected the MEASURING api state"); + wait until hash_req = '0'; + -- POR_B releases only after the flash has gone back to the Versal + wait until versal_boot_pins.por_b = '1' for 50 ms; + check_equal(versal_boot_pins.por_b, '1', "Expected POR_B released after the measurement"); + check_equal(flash_owned_by_seq, '0', "Expected the flash back with the Versal for boot"); + poll_for_nic_state(net, DONE); + check_equal(flash_owned_by_seq, '1', "Expected the flash back on the FPGA side after boot"); + read_bus(net, bus_handle, + To_StdLogicVector(STATUS_OFFSET, bus_handle.p_address_length), + read_data); + status := unpack(read_data); + check_equal(status.nic_hash_done, '1', "Expected versal_hash_done"); + check_equal(status.nic_hash_err, '0', "Expected no versal_hash_err"); + check_equal(hash_requests, 1, "Expected exactly one measurement"); + + elsif run("failed_measurement_is_recorded_and_boot_continues") then + hash_model_fail <= true; + power_up_to_nic_done(net); + read_bus(net, bus_handle, + To_StdLogicVector(STATUS_OFFSET, bus_handle.p_address_length), + read_data); + status := unpack(read_data); + check_equal(status.nic_hash_done, '0', "Expected no versal_hash_done"); + check_equal(status.nic_hash_err, '1', "Expected versal_hash_err"); + read_bus(net, bus_handle, + To_StdLogicVector(IFR_OFFSET, bus_handle.p_address_length), + read_data); + check_equal((read_data and IFR_NIC_HASH_ERR_MASK) /= (read_data'range => '0'), true, + "Expected the versal_hash_err interrupt flag"); + + elsif run("measurement_can_be_skipped") then + write_bus(net, bus_handle, + To_StdLogicVector(VERSAL_BOOT_CTRL_OFFSET, bus_handle.p_address_length), + VERSAL_BOOT_CTRL_MODE_MASK and x"00000002"); + power_up_to_nic_done(net); + check_equal(hash_requests, 0, "Expected no measurement request"); + read_bus(net, bus_handle, + To_StdLogicVector(STATUS_OFFSET, bus_handle.p_address_length), + read_data); + status := unpack(read_data); + check_equal(status.nic_hash_done, '0', "Expected no versal_hash_done"); + check_equal(status.nic_hash_err, '0', "Expected no versal_hash_err"); + + elsif run("boot_timeout_does_not_drop_power") then + versal_model_msg_pkg.fail_boot(net, versal_actor); + write_bus(net, bus_handle, + To_StdLogicVector(POWER_CTRL_OFFSET, bus_handle.p_address_length), + POWER_CTRL_A0_EN_MASK); + poll_for_seq_state(net, DONE); + poll_for_nic_state(net, BOOTING); + wait for 500 us; + + -- A boot that never completes must not look like a power fault. + read_bus(net, bus_handle, + To_StdLogicVector(NIC_API_STATUS_OFFSET, bus_handle.p_address_length), + read_data); + versal_state := encode(read_data(7 downto 0)); + check_equal(versal_state = BOOTING, true, + "Expected the Versal sequencer to stay in BOOTING"); + read_bus(net, bus_handle, + To_StdLogicVector(IFR_OFFSET, bus_handle.p_address_length), read_data); + check_equal((read_data and IFR_NICMAPO_MASK) = x"00000000", true, + "A failed boot must not raise a Versal MAPO"); + versal_model_msg_pkg.allow_boot(net, versal_actor); + + elsif run("error_out_raises_an_irq") then + power_up_to_nic_done(net); + versal_model_msg_pkg.assert_error_out(net, versal_actor); + wait for 100 us; + read_bus(net, bus_handle, + To_StdLogicVector(IFR_OFFSET, bus_handle.p_address_length), read_data); + check_equal((read_data and IFR_VERSAL_ERROR_OUT_MASK) /= x"00000000", true, + "Expected ERROR_OUT to set its interrupt flag"); + versal_model_msg_pkg.clear_error_out(net, versal_actor); + + elsif run("board_version_readback") then + read_bus(net, bus_handle, + To_StdLogicVector(BOARD_VERSION_OFFSET, bus_handle.p_address_length), + read_data); + version := unpack(read_data); + check_equal(version.version_id, std_logic_vector'("01"), + "Expected the board version straps to read back"); + + elsif run("mapo_fault_v1p1_sp5") then + test_mapo_fault_injection(net, find("grpb_v1p1_sp5"), "v1p1_sp5"); + elsif run("mapo_fault_vddcr_soc") then + test_mapo_fault_injection(net, find("grpc_vddcr_soc"), "vddcr_soc"); + + elsif run("versal_mapo_fault_v0p8_vccint") then + test_versal_rail_mapo_fault_injection(net, find("versal_v0p8_vccint"), "v0p8_vccint"); + elsif run("versal_mapo_fault_v0p88") then + test_versal_rail_mapo_fault_injection(net, find("versal_v0p88"), "v0p88"); + elsif run("versal_mapo_fault_v1p1") then + test_versal_rail_mapo_fault_injection(net, find("versal_v1p1"), "v1p1"); + elsif run("versal_mapo_fault_v1p4") then + test_versal_rail_mapo_fault_injection(net, find("versal_v1p4"), "v1p4"); + elsif run("versal_mapo_fault_v1p5") then + test_versal_rail_mapo_fault_injection(net, find("versal_v1p5"), "v1p5"); + elsif run("versal_mapo_fault_v1p5_avccaux") then + test_versal_rail_mapo_fault_injection(net, find("versal_v1p5_avccaux"), "v1p5_avccaux"); + elsif run("versal_mapo_fault_v1p8") then + test_versal_rail_mapo_fault_injection(net, find("versal_v1p8"), "v1p8"); + elsif run("versal_mapo_fault_v3p3") then + test_versal_rail_mapo_fault_injection(net, find("versal_v3p3"), "v3p3"); + end if; + end loop; + + wait for 2 us; + test_runner_cleanup(runner); + wait; + end process; + + test_runner_watchdog(runner, 20 ms); +end tb; diff --git a/hdl/projects/cosmo_seq/sequencer/sp5_power_pkg.vhd b/hdl/projects/cosmo_seq/sequencer/sp5_power_pkg.vhd new file mode 100644 index 00000000..1b0aafc1 --- /dev/null +++ b/hdl/projects/cosmo_seq/sequencer/sp5_power_pkg.vhd @@ -0,0 +1,178 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +-- The board-agnostic half of the sequencer's I/O types: the generic rail +-- records and the SP5-side rail groups and pins. Every SP5 board carries these +-- unchanged, so they live apart from sequencer_io_pkg's cosmo-specific records +-- to let a sibling design reuse a1_a0_seq without dragging in cosmo's NIC. +package sp5_power_pkg is + + -- Generic record type for the common enable/pg feedback. + -- we'll treat this as both active high internally so + -- the outer-most blocks need to invert if required. + type power_rail_t is record + enable : std_logic; + pg : std_logic; + end record; + -- FPGA's view of the world as the controller and recipient of + -- the feedback + view power_rail_at_fpga of power_rail_t is + enable : out; + pg : in; + end view; + alias power_rail_at_reg is power_rail_at_fpga'converse; + -- a cascade power rail only has a PG, the enable came from some + -- other supply + type cascade_power_rail_t is record + pg : std_logic; + end record; + view cascade_power_rail_at_fpga of cascade_power_rail_t is + pg : in; + end view; + alias cascade_power_rail_at_reg is cascade_power_rail_at_fpga'converse; + + -- Sequencing-related SP5 control/feedback pins + type sp5_seq_pins_t is record + thermtrip_l : std_logic; + reset_l : std_logic; + pwr_ok : std_logic; + slp_s3_l : std_logic; + slp_s5_l : std_logic; + rsmrst_l : std_logic; + pwr_btn_l : std_logic; + pwr_good : std_logic; + pwrgd_out : std_logic; + smerr_l : std_logic; + is_cosmo : std_logic; -- uses SP5_TO_FPGA1_DEBUG1 high at power up to indicate cosmo + end record; + -- FPGA's view of the world as the controller and recipient of + -- the feedback + view sp5_seq_at_fpga of sp5_seq_pins_t is + thermtrip_l : in; + reset_l : in; + pwr_ok : in; + slp_s3_l : in; + slp_s5_l : in; + smerr_l : in; + pwrgd_out : in; + rsmrst_l : out; + pwr_btn_l : out; + pwr_good : out; + is_cosmo : out; + end view; + alias sp5_seq_at_sp5 is sp5_seq_at_fpga'converse; + + type early_power_t is record + fan_central_hsc_pg : std_logic; + fan_east_hsc_pg : std_logic; + fan_fail : std_logic; + fan_west_hsc_pg : std_logic; + fan_central_hsc_disable : std_logic; + fan_east_hsc_disable : std_logic; + fan_west_hsc_disable : std_logic; + end record; + view early_power_at_fpga of early_power_t is + fan_central_hsc_pg : in; + fan_east_hsc_pg : in; + fan_west_hsc_pg : in; + fan_fail : in; + fan_central_hsc_disable : out; + fan_east_hsc_disable : out; + fan_west_hsc_disable : out; + end view; + alias early_power_on_board is early_power_at_fpga'converse; + + -- DDR 12V bulk power rail + type ddr_bulk_power_t is record + abcdef_hsc : power_rail_t; + ghijkl_hsc : power_rail_t; + end record; + function is_power_good(power_group: ddr_bulk_power_t) return boolean; + -- FPGA's view of the world as the controller and recipient of + -- the feedback + view ddr_bulk_power_at_fpga of ddr_bulk_power_t is + abcdef_hsc : view power_rail_at_fpga; + ghijkl_hsc : view power_rail_at_fpga; + end view; + alias ddr_bulk_at_reg is ddr_bulk_power_at_fpga'converse; + + -- SP5 group a rails + --A (G3/S5): VDDBT_RTC_G, VDD_18_S5,VDD_33_S5, VDDIO_AUDIO (shared as 1V8) + type group_a_power_t is record + pwr_v1p5_rtc : power_rail_t; + v3p3_sp5_a1 : power_rail_t; + v1p8_sp5_a1 : power_rail_t; + end record; + function is_power_good(power_group: group_a_power_t) return boolean; + view group_a_power_at_fpga of group_a_power_t is + pwr_v1p5_rtc : view power_rail_at_fpga; + v3p3_sp5_a1 : view power_rail_at_fpga; + v1p8_sp5_a1 : view power_rail_at_fpga; + end view; + alias group_a_power_at_reg is group_a_power_at_fpga'converse; + + -- B (S3): VDD_11_S3 + type group_b_power_t is record + v1p1_sp5 : power_rail_t; + end record; + function is_power_good(power_group: group_b_power_t) return boolean; + view group_b_power_at_fpga of group_b_power_t is + v1p1_sp5 : view power_rail_at_fpga; + end view; + alias group_b_power_at_reg is group_b_power_at_fpga'converse; + + + -- C (S0): VDDIO, VDDCR_SOC,VDDCR_CPU0, VDDCR_CPU1 + type group_c_power_t is record + vddio_sp5_a0 : power_rail_t; + vddcr_cpu1 : power_rail_t; + vddcr_cpu0 : power_rail_t; + vddcr_soc : power_rail_t; + end record; + function is_power_good(power_group: group_c_power_t) return boolean; + view group_c_power_at_fpga of group_c_power_t is + vddio_sp5_a0 : view power_rail_at_fpga; + vddcr_cpu1 : view power_rail_at_fpga; + vddcr_cpu0 : view power_rail_at_fpga; + vddcr_soc : view power_rail_at_fpga; + end view; + alias group_c_power_at_reg is group_c_power_at_fpga'converse; + +end package; + +package body sp5_power_pkg is + + -- Functions for DDR bulk + function is_power_good(power_group: ddr_bulk_power_t) return boolean is + begin + return (power_group.abcdef_hsc.pg and power_group.ghijkl_hsc.pg) = '1'; + end function; + + -- Functions for groupA + function is_power_good(power_group: group_a_power_t) return boolean is + begin + return (power_group.pwr_v1p5_rtc.pg and power_group.v3p3_sp5_a1.pg and power_group.v1p8_sp5_a1.pg) = '1'; + end function; + + -- Functions for groupB + function is_power_good(power_group: group_b_power_t) return boolean is + begin + return power_group.v1p1_sp5.pg = '1'; + end function; + + -- Functions for groupc + function is_power_good(power_group: group_c_power_t) return boolean is + begin + return (power_group.vddio_sp5_a0.pg and + power_group.vddcr_cpu1.pg and + power_group.vddcr_cpu0.pg and + power_group.vddcr_soc.pg + ) = '1'; + end function; + +end package body; diff --git a/hdl/projects/cosmo_seq/sequencer/sp5_sequencer.vhd b/hdl/projects/cosmo_seq/sequencer/sp5_sequencer.vhd index ce540f71..364e0195 100644 --- a/hdl/projects/cosmo_seq/sequencer/sp5_sequencer.vhd +++ b/hdl/projects/cosmo_seq/sequencer/sp5_sequencer.vhd @@ -8,6 +8,7 @@ use ieee.numeric_std.all; use work.axil8x32_pkg; +use work.sp5_power_pkg.all; use work.sequencer_io_pkg.all; use work.sequencer_regs_pkg.all; @@ -17,9 +18,17 @@ use work.sequencer_regs_pkg.all; -- and provides registers for out outputs that are destined for off-chip -- devices. It provides no tri-state logic so tri-stating must be done -- at the chip top if needed/desired. +-- +-- The SP5 half is the same on every board. The NIC half is picked by +-- NIC_KIND: cosmo's T6 or metro's Versal. Both NICs' pin records are ports so +-- that the entity is the same on both boards; a board ties the records for +-- the NIC it does not have to the *_absent constants in sequencer_io_pkg +-- and leaves that NIC's outputs open. The register map is the union of both +-- (see sequencer_regs.rdl), so the registers for the absent NIC read zero. entity sp5_sequencer is generic ( - CNTS_P_MS: integer + CNTS_P_MS: integer; + NIC_KIND : nic_kind_t := NIC_T6 ); port ( clk : in std_logic; @@ -43,22 +52,40 @@ entity sp5_sequencer is group_c_pins : view group_c_power_at_fpga; -- SP5 sequencing I/O sp5_seq_pins : view sp5_seq_at_fpga; - -- nic supplies - nic_rails_pins : view nic_power_at_fpga; - -- nic sequencing I/O - nic_seq_pins: view nic_seq_at_fpga; allow_backplane_pcie_clk : out std_logic; - nic_dbg_pins : view t6_debug_seq_ss; + -- What the NIC sequencer offers the debug header + nic_dbg_pins : view nic_debug_seq_ss; -- regulator alerts reg_alert_l_pins : view power_alert_at_fpga; - sp5_t6_perst_l : in std_logic; - sp5_t6_faulted : out std_logic; + -- From SP5 hotplug: the NIC slot's PERST, which follows the slot power + -- enable. A Versal has two slots; the T6 uses only the first. + sp5_nic_perst_l : in std_logic; + sp5_nic_chb_perst_l : in std_logic := '1'; + sp5_nic_faulted : out std_logic; ignition_mux_sel : out std_logic; ignition_creset : out std_logic; + -- T6 NIC (NIC_KIND = NIC_T6) + nic_rails_pins : view nic_power_at_fpga; + nic_seq_pins: view nic_seq_at_fpga; + -- Versal NIC (NIC_KIND = NIC_VERSAL) + versal_rails_pins : view versal_power_at_fpga; + versal_boot_pins : view versal_boot_at_fpga; + versal_pcie_pins : view versal_pcie_at_fpga; + -- True while POR_B is held low and will stay so, gating the SP's + -- request for the Versal boot-flash mux + versal_held_in_reset : out std_logic; + -- True while the sequencer wants the boot flash on the FPGA side + flash_owned_by_seq : out std_logic; + -- Hash engine hardware request for the pre-boot measurement + hash_req : out std_logic; + hash_ack : in std_logic := '0'; + hash_err : in std_logic := '0'; + -- FPGA1_VERSION_ID board straps, reported straight through + version_id : in std_logic_vector(1 downto 0) := "00" ); end entity; @@ -73,6 +100,9 @@ architecture rtl of sp5_sequencer is signal rail_masks : rails_type; signal fans_power_ok : std_logic; + signal nic_power_ok : std_logic; + -- a0_ok is an output port and this architecture needs to read it + signal a0_ok_int : std_logic; -- We have the following states for the sequencing block -- power ok means we're up and happy -- power idle means we're down and idle @@ -85,25 +115,37 @@ architecture rtl of sp5_sequencer is signal group_b : group_b_power_t; signal group_c : group_c_power_t; signal sp5_seq : sp5_seq_pins_t; - signal nic_rails : nic_power_t; - signal nic_seq : nic_seq_pins_t; signal early_power_ctrl : early_power_ctrl_type; signal early_power_rdbks : early_power_rdbks_type; signal sp5_readbacks : sp5_readbacks_type; - signal nic_readbacks : nic_readbacks_type; - signal nic_overrides : nic_overrides_type; signal debug_enables : debug_enables_type; + signal nic_overrides : nic_overrides_type; signal smerr_assert : std_logic; signal a0_faulted : std_logic; signal nic_faulted : std_logic; signal reg_alert_l : seq_power_alert_pins_t; signal sp5_seq_test_mask : sp5_seq_test_mask_type; - + -- T6 side + signal nic_rails : nic_power_t; + signal nic_seq : nic_seq_pins_t; + signal nic_readbacks : nic_readbacks_type; + -- Versal side + signal versal_rails : versal_power_t; + signal versal_boot : versal_boot_t; + signal versal_pcie : versal_pcie_t; + signal versal_readbacks : versal_readbacks_type; + signal versal_overrides : versal_overrides_type; + signal versal_boot_ctrl : versal_boot_ctrl_type; + signal versal_hash_done : std_logic; + signal versal_hash_failed : std_logic; + signal board_version : board_version_type; begin - sp5_t6_faulted <= nic_faulted; + sp5_nic_faulted <= nic_faulted; + a0_ok <= a0_ok_int; + board_version.version_id <= version_id; sync: entity work.seq_sync port map( @@ -115,8 +157,6 @@ begin group_b_pins => group_b_pins, group_c_pins => group_c_pins, sp5_seq_pins => sp5_seq_pins, - nic_rails_pins => nic_rails_pins, - nic_seq_pins => nic_seq_pins, early_power => early_power, ddr_bulk => ddr_bulk, rail_masks => rail_masks, @@ -125,8 +165,6 @@ begin group_b => group_b, group_c => group_c, sp5_seq => sp5_seq, - nic_rails => nic_rails, - nic_seq => nic_seq, reg_alert_l_pins => reg_alert_l_pins, reg_alert_l => reg_alert_l ); @@ -156,6 +194,17 @@ begin rails_pg_rdbk => rails_pg_rdbk, sp5_readbacks => sp5_readbacks, nic_readbacks => nic_readbacks, + fans_power_ok => fans_power_ok, + a0_ok => a0_ok_int, + nic_power_ok => nic_power_ok, + nic_done => versal_boot.done, + versal_error_out => versal_boot.error_out, + nic_hash_done => versal_hash_done, + nic_hash_err => versal_hash_failed, + versal_readbacks => versal_readbacks, + versal_overrides => versal_overrides, + versal_boot_ctrl => versal_boot_ctrl, + board_version => board_version, ignition_mux_sel => ignition_mux_sel, ignition_creset => ignition_creset, irq_l_out => irq_l_out, @@ -176,17 +225,7 @@ begin early_power_rdbks.fan_hsc_central_pg <= early_power.fan_central_hsc_pg; early_power_rdbks.fan_hsc_east_pg <= early_power.fan_east_hsc_pg; early_power_rdbks.fan_fail <= not early_power.fan_fail; - -- rails - -- NIC rails all cascade enabled from 12V rail - rails_en_rdbk.v0p96_nic_vdd_a0hp <= nic_rails.nic_hsc_12v.enable; - rails_en_rdbk.v1p1_nic_a0hp <= nic_rails.nic_hsc_12v.enable; - rails_en_rdbk.v1p4_nic_a0hp <= nic_rails.nic_hsc_12v.enable; - rails_en_rdbk.v3p3_nic_a0hp <= nic_rails.nic_hsc_12v.enable; - rails_en_rdbk.v1p2_nic_enet_a0hp <= nic_rails.nic_hsc_12v.enable; - rails_en_rdbk.v1p2_nic_pcie_a0hp <= nic_rails.nic_hsc_12v.enable; - rails_en_rdbk.v1p5_nic_a0hp <= nic_rails.nic_hsc_12v.enable; - rails_en_rdbk.nic_hsc_5v <= nic_rails.nic_hsc_12v.enable; - rails_en_rdbk.nic_hsc_12v <= nic_rails.nic_hsc_12v.enable; + -- SP5 rails rails_en_rdbk.vddcr_soc <= group_c.vddcr_soc.enable; rails_en_rdbk.vddcr_cpu0 <= group_c.vddcr_cpu0.enable; rails_en_rdbk.vddcr_cpu1 <= group_c.vddcr_cpu1.enable; @@ -197,16 +236,6 @@ begin rails_en_rdbk.v1p5_rtc <= group_a.pwr_v1p5_rtc.enable; rails_en_rdbk.ghijkl_hsc <= ddr_bulk.ghijkl_hsc.enable; rails_en_rdbk.abcdef_hsc <= ddr_bulk.abcdef_hsc.enable; - -- PG readbacks - rails_pg_rdbk.v0p96_nic_vdd_a0hp <= nic_rails.v0p96_nic_vdd_a0hp.pg; - rails_pg_rdbk.v1p1_nic_a0hp <= nic_rails.v1p1_nic_a0hp.pg; - rails_pg_rdbk.v1p4_nic_a0hp <= nic_rails.v1p4_nic_a0hp.pg; - rails_pg_rdbk.v3p3_nic_a0hp <= nic_rails.v3p3_nic_a0hp.pg; - rails_pg_rdbk.v1p2_nic_enet_a0hp <= nic_rails.v1p2_nic_enet_a0hp.pg; - rails_pg_rdbk.v1p2_nic_pcie_a0hp <= nic_rails.v1p2_nic_pcie_a0hp.pg; - rails_pg_rdbk.v1p5_nic_a0hp <= nic_rails.v1p5_nic_a0hp.pg; - rails_pg_rdbk.nic_hsc_5v <= nic_rails.nic_hsc_5v.pg; - rails_pg_rdbk.nic_hsc_12v <= nic_rails.nic_hsc_12v.pg; rails_pg_rdbk.vddcr_soc <= group_c.vddcr_soc.pg; rails_pg_rdbk.vddcr_cpu0 <= group_c.vddcr_cpu0.pg; rails_pg_rdbk.vddcr_cpu1 <= group_c.vddcr_cpu1.pg; @@ -229,17 +258,6 @@ begin sp5_readbacks.thermtrip_l <= sp5_seq.thermtrip_l; sp5_readbacks.smerr_l <= sp5_seq.smerr_l; sp5_readbacks.pwrgd_out <= sp5_seq.pwrgd_out; - -- NIC sequencing readbacks - nic_readbacks.nic_pcie_clk_buff_oe_l <= nic_seq.nic_pcie_clk_buff_oe_l; - nic_readbacks.flash_wp_l <= nic_seq.flash_wp_l; - nic_readbacks.eeprom_wp_buffer_oe_l <= nic_seq.eeprom_wp_buffer_oe_l; - nic_readbacks.eeprom_wp_l <= nic_seq.eeprom_wp_l; - nic_readbacks.sp5_mfg_mode_l <= nic_seq.sp5_mfg_mode_l; - nic_readbacks.nic_mfg_mode_l <= nic_seq.nic_mfg_mode_l; - nic_readbacks.ext_rst_l <= nic_seq.ext_rst_l; - nic_readbacks.perst_l <= nic_seq.perst_l; - nic_readbacks.cld_rst_l <= nic_seq.cld_rst_l; - nic_readbacks.sp5_perst_l <= sp5_t6_perst_l; fans_power_ok <= early_power_rdbks.fan_hsc_west_pg and early_power_rdbks.fan_hsc_central_pg and @@ -255,7 +273,7 @@ begin downstream_idle => nic_idle, therm_trip => therm_trip, smerr_assert => smerr_assert, - a0_ok => a0_ok, + a0_ok => a0_ok_int, a0_idle => a0_idle, a0_faulted => a0_faulted, sw_enable => power_ctrl.a0_en, @@ -269,25 +287,222 @@ begin sp5_seq_pins => sp5_seq ); - nic_seq_inst: entity work.nic_seq - generic map( - CNTS_P_MS => CNTS_P_MS - ) - port map( - clk => clk, - reset => reset, - nic_idle => nic_idle, - nic_faulted => nic_faulted, - sw_enable => power_ctrl.a0_en, - raw_state => nic_raw_status, - api_state => nic_api_status, - upstream_ok => a0_ok, - nic_overrides_reg => nic_overrides, - debug_enables => debug_enables, - sp5_t6_perst_l => sp5_t6_perst_l, - nic_dbg_pins => nic_dbg_pins, - nic_rails => nic_rails, - nic_seq_pins => nic_seq - ); + ----------------------------------------------------------------------- + -- T6 NIC + ----------------------------------------------------------------------- + t6: if NIC_KIND = NIC_T6 generate + t6_sync_inst: entity work.t6_sync + port map( + clk => clk, + reset => reset, + nic_rails_pins => nic_rails_pins, + nic_seq_pins => nic_seq_pins, + nic_rails => nic_rails, + nic_seq => nic_seq + ); + + nic_seq_inst: entity work.nic_seq + generic map( + CNTS_P_MS => CNTS_P_MS + ) + port map( + clk => clk, + reset => reset, + nic_idle => nic_idle, + nic_faulted => nic_faulted, + sw_enable => power_ctrl.a0_en, + raw_state => nic_raw_status, + api_state => nic_api_status, + upstream_ok => a0_ok_int, + nic_overrides_reg => nic_overrides, + debug_enables => debug_enables, + sp5_t6_perst_l => sp5_nic_perst_l, + nic_dbg_pins => nic_dbg_pins, + nic_rails => nic_rails, + nic_seq_pins => nic_seq + ); + + -- NIC rails all cascade enabled from 12V rail + rails_en_rdbk.v0p96_nic_vdd_a0hp <= nic_rails.nic_hsc_12v.enable; + rails_en_rdbk.v1p1_nic_a0hp <= nic_rails.nic_hsc_12v.enable; + rails_en_rdbk.v1p4_nic_a0hp <= nic_rails.nic_hsc_12v.enable; + rails_en_rdbk.v3p3_nic_a0hp <= nic_rails.nic_hsc_12v.enable; + rails_en_rdbk.v1p2_nic_enet_a0hp <= nic_rails.nic_hsc_12v.enable; + rails_en_rdbk.v1p2_nic_pcie_a0hp <= nic_rails.nic_hsc_12v.enable; + rails_en_rdbk.v1p5_nic_a0hp <= nic_rails.nic_hsc_12v.enable; + rails_en_rdbk.nic_hsc_5v <= nic_rails.nic_hsc_12v.enable; + rails_en_rdbk.nic_hsc_12v <= nic_rails.nic_hsc_12v.enable; + rails_pg_rdbk.v0p96_nic_vdd_a0hp <= nic_rails.v0p96_nic_vdd_a0hp.pg; + rails_pg_rdbk.v1p1_nic_a0hp <= nic_rails.v1p1_nic_a0hp.pg; + rails_pg_rdbk.v1p4_nic_a0hp <= nic_rails.v1p4_nic_a0hp.pg; + rails_pg_rdbk.v3p3_nic_a0hp <= nic_rails.v3p3_nic_a0hp.pg; + rails_pg_rdbk.v1p2_nic_enet_a0hp <= nic_rails.v1p2_nic_enet_a0hp.pg; + rails_pg_rdbk.v1p2_nic_pcie_a0hp <= nic_rails.v1p2_nic_pcie_a0hp.pg; + rails_pg_rdbk.v1p5_nic_a0hp <= nic_rails.v1p5_nic_a0hp.pg; + rails_pg_rdbk.nic_hsc_5v <= nic_rails.nic_hsc_5v.pg; + rails_pg_rdbk.nic_hsc_12v <= nic_rails.nic_hsc_12v.pg; + -- the Versal bits are not this board's + rails_en_rdbk.versal_v0p8_vccint <= '0'; + rails_en_rdbk.versal_v0p88 <= '0'; + rails_en_rdbk.versal_v0p92_avcc <= '0'; + rails_en_rdbk.versal_v1p1 <= '0'; + rails_en_rdbk.versal_v1p2_avtt <= '0'; + rails_en_rdbk.versal_v1p4 <= '0'; + rails_en_rdbk.versal_v1p5 <= '0'; + rails_en_rdbk.versal_v1p5_avccaux <= '0'; + rails_en_rdbk.versal_v1p8 <= '0'; + rails_en_rdbk.versal_v3p3 <= '0'; + rails_pg_rdbk.versal_v0p8_vccint <= '0'; + rails_pg_rdbk.versal_v0p88 <= '0'; + rails_pg_rdbk.versal_v0p92_avcc <= '0'; + rails_pg_rdbk.versal_v1p1 <= '0'; + rails_pg_rdbk.versal_v1p2_avtt <= '0'; + rails_pg_rdbk.versal_v1p4 <= '0'; + rails_pg_rdbk.versal_v1p5 <= '0'; + rails_pg_rdbk.versal_v1p5_avccaux <= '0'; + rails_pg_rdbk.versal_v1p8 <= '0'; + rails_pg_rdbk.versal_v3p3 <= '0'; + + -- NIC sequencing readbacks + nic_readbacks.nic_pcie_clk_buff_oe_l <= nic_seq.nic_pcie_clk_buff_oe_l; + nic_readbacks.flash_wp_l <= nic_seq.flash_wp_l; + nic_readbacks.eeprom_wp_buffer_oe_l <= nic_seq.eeprom_wp_buffer_oe_l; + nic_readbacks.eeprom_wp_l <= nic_seq.eeprom_wp_l; + nic_readbacks.sp5_mfg_mode_l <= nic_seq.sp5_mfg_mode_l; + nic_readbacks.nic_mfg_mode_l <= nic_seq.nic_mfg_mode_l; + nic_readbacks.ext_rst_l <= nic_seq.ext_rst_l; + nic_readbacks.perst_l <= nic_seq.perst_l; + nic_readbacks.cld_rst_l <= nic_seq.cld_rst_l; + nic_readbacks.sp5_perst_l <= sp5_nic_perst_l; + nic_power_ok <= '1' when is_power_good(nic_rails) else '0'; + + -- Nothing Versal on this board + versal_rails <= versal_power_absent; + versal_boot <= versal_boot_absent; + versal_pcie <= versal_pcie_absent; + versal_readbacks <= (mode => (others => '0'), others => '0'); + versal_hash_done <= '0'; + versal_hash_failed <= '0'; + versal_held_in_reset <= '0'; + flash_owned_by_seq <= '0'; + hash_req <= '0'; + end generate; + + ----------------------------------------------------------------------- + -- Versal NIC + ----------------------------------------------------------------------- + versal: if NIC_KIND = NIC_VERSAL generate + versal_sync_inst: entity work.versal_sync + port map( + clk => clk, + reset => reset, + versal_rails_pins => versal_rails_pins, + versal_boot_pins => versal_boot_pins, + versal_pcie_pins => versal_pcie_pins, + rail_masks => rail_masks, + versal_rails => versal_rails, + versal_boot => versal_boot, + versal_pcie => versal_pcie + ); + + versal_seq_inst: entity work.versal_seq + generic map( + CNTS_P_MS => CNTS_P_MS + ) + port map( + clk => clk, + reset => reset, + versal_idle => nic_idle, + versal_faulted => nic_faulted, + sw_enable => power_ctrl.a0_en, + raw_state => nic_raw_status, + api_state => nic_api_status, + upstream_ok => a0_ok_int, + versal_overrides_reg => versal_overrides, + nic_test_mapo => nic_overrides.nic_test_mapo, + boot_ctrl => versal_boot_ctrl, + debug_enables => debug_enables, + sp5_versal_cha_perst_l => sp5_nic_perst_l, + sp5_versal_chb_perst_l => sp5_nic_chb_perst_l, + versal_dbg_pins => nic_dbg_pins, + versal_held_in_reset => versal_held_in_reset, + flash_owned_by_seq => flash_owned_by_seq, + hash_req => hash_req, + hash_ack => hash_ack, + hash_err => hash_err, + hash_done => versal_hash_done, + hash_failed => versal_hash_failed, + versal_rails => versal_rails, + versal_boot => versal_boot, + versal_pcie => versal_pcie + ); + + -- Each Versal rail has its own enable, staged by versal_seq. The two + -- transceiver rails cascade, so they read back their group's enable. + -- The hotswaps share the nic_hsc_* bits with the T6. + rails_en_rdbk.versal_v3p3 <= versal_rails.v3p3.enable; + rails_en_rdbk.versal_v1p8 <= versal_rails.v1p8.enable; + rails_en_rdbk.versal_v1p5_avccaux <= versal_rails.v1p5_avccaux.enable; + rails_en_rdbk.versal_v1p5 <= versal_rails.v1p5.enable; + rails_en_rdbk.versal_v1p4 <= versal_rails.v1p4.enable; + rails_en_rdbk.versal_v1p2_avtt <= versal_rails.v1p5.enable; + rails_en_rdbk.versal_v1p1 <= versal_rails.v1p1.enable; + rails_en_rdbk.versal_v0p92_avcc <= versal_rails.v0p88.enable; + rails_en_rdbk.versal_v0p88 <= versal_rails.v0p88.enable; + rails_en_rdbk.versal_v0p8_vccint <= versal_rails.v0p8_vccint.enable; + rails_en_rdbk.nic_hsc_5v <= versal_rails.hsc_12v.enable; + rails_en_rdbk.nic_hsc_12v <= versal_rails.hsc_12v.enable; + rails_pg_rdbk.versal_v3p3 <= versal_rails.v3p3.pg; + rails_pg_rdbk.versal_v1p8 <= versal_rails.v1p8.pg; + rails_pg_rdbk.versal_v1p5_avccaux <= versal_rails.v1p5_avccaux.pg; + rails_pg_rdbk.versal_v1p5 <= versal_rails.v1p5.pg; + rails_pg_rdbk.versal_v1p4 <= versal_rails.v1p4.pg; + rails_pg_rdbk.versal_v1p2_avtt <= versal_rails.v1p2_avtt.pg; + rails_pg_rdbk.versal_v1p1 <= versal_rails.v1p1.pg; + rails_pg_rdbk.versal_v0p92_avcc <= versal_rails.v0p92_avcc.pg; + rails_pg_rdbk.versal_v0p88 <= versal_rails.v0p88.pg; + rails_pg_rdbk.versal_v0p8_vccint <= versal_rails.v0p8_vccint.pg; + rails_pg_rdbk.nic_hsc_5v <= versal_rails.hsc_5v.pg; + rails_pg_rdbk.nic_hsc_12v <= versal_rails.hsc_12v.pg; + -- the T6 bits are not this board's + rails_en_rdbk.v0p96_nic_vdd_a0hp <= '0'; + rails_en_rdbk.v1p1_nic_a0hp <= '0'; + rails_en_rdbk.v1p4_nic_a0hp <= '0'; + rails_en_rdbk.v3p3_nic_a0hp <= '0'; + rails_en_rdbk.v1p2_nic_enet_a0hp <= '0'; + rails_en_rdbk.v1p2_nic_pcie_a0hp <= '0'; + rails_en_rdbk.v1p5_nic_a0hp <= '0'; + rails_pg_rdbk.v0p96_nic_vdd_a0hp <= '0'; + rails_pg_rdbk.v1p1_nic_a0hp <= '0'; + rails_pg_rdbk.v1p4_nic_a0hp <= '0'; + rails_pg_rdbk.v3p3_nic_a0hp <= '0'; + rails_pg_rdbk.v1p2_nic_enet_a0hp <= '0'; + rails_pg_rdbk.v1p2_nic_pcie_a0hp <= '0'; + rails_pg_rdbk.v1p5_nic_a0hp <= '0'; + + -- Versal sequencing readbacks + versal_readbacks.por_b <= versal_boot.por_b; + versal_readbacks.mode <= versal_boot.mode; + versal_readbacks.mode_buffer_en_l <= versal_boot.mode_buffer_en_l; + versal_readbacks.err_done_buff_en <= versal_boot.err_done_buff_en; + versal_readbacks.done <= versal_boot.done; + versal_readbacks.error_out <= versal_boot.error_out; + versal_readbacks.cha_perst_l <= versal_pcie.cha.perst_l; + versal_readbacks.cha_prsnt_l <= versal_pcie.cha.prsnt_l; + versal_readbacks.cha_pwren_l <= versal_pcie.cha.pwren_l; + versal_readbacks.cha_clk_buff_oe_l <= versal_pcie.cha.clk_buff_oe_l; + versal_readbacks.chb_perst_l <= versal_pcie.chb.perst_l; + versal_readbacks.chb_prsnt_l <= versal_pcie.chb.prsnt_l; + versal_readbacks.chb_pwren_l <= versal_pcie.chb.pwren_l; + versal_readbacks.chb_clk_buff_oe_l <= versal_pcie.chb.clk_buff_oe_l; + versal_readbacks.sp5_cha_perst_l <= sp5_nic_perst_l; + versal_readbacks.sp5_chb_perst_l <= sp5_nic_chb_perst_l; + nic_power_ok <= '1' when is_power_good(versal_rails) else '0'; + + -- Nothing T6 on this board + nic_rails <= nic_power_absent; + nic_seq <= nic_seq_pins_absent; + nic_readbacks <= (others => '0'); + end generate; -end rtl; \ No newline at end of file +end rtl; diff --git a/hdl/projects/cosmo_seq/sequencer/t6_sync.vhd b/hdl/projects/cosmo_seq/sequencer/t6_sync.vhd new file mode 100644 index 00000000..ef7aecfe --- /dev/null +++ b/hdl/projects/cosmo_seq/sequencer/t6_sync.vhd @@ -0,0 +1,117 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +use work.sp5_power_pkg.all; +use work.sequencer_io_pkg.all; +use work.sequencer_regs_pkg.all; + +-- Synchronises cosmo's T6 NIC pins: the rail power goods and the reset, +-- write-protect and manufacturing-mode handshake lines. The SP5-side pins +-- are done in seq_sync. +entity t6_sync is + port ( + clk : in std_logic; + reset : in std_logic; + + -- pins (unsync'd) interface + nic_rails_pins : view nic_power_at_fpga; + nic_seq_pins: view nic_seq_at_fpga; + -- internal, synchronized interfaces + nic_rails : view nic_power_at_reg; + nic_seq: view nic_seq_at_nic + ); +end entity; + +architecture rtl of t6_sync is + signal nic_sync_5v_hsc_pg_l : std_logic; + signal nic_sync_12v_hsc_pg_l : std_logic; +begin + + -- nic rails sync stuff + nic_rails_pins.nic_hsc_12v.enable <= nic_rails.nic_hsc_12v.enable; + v1p5_nic_a0hp: entity work.meta_sync + port map( + async_input => nic_rails_pins.v1p5_nic_a0hp.pg, + clk => clk, + sycnd_output => nic_rails.v1p5_nic_a0hp.pg + ); + v1p2_nic_pcie_a0hp: entity work.meta_sync + port map( + async_input => nic_rails_pins.v1p2_nic_pcie_a0hp.pg, + clk => clk, + sycnd_output => nic_rails.v1p2_nic_pcie_a0hp.pg + ); + v1p2_nic_enet_a0hp: entity work.meta_sync + port map( + async_input => nic_rails_pins.v1p2_nic_enet_a0hp.pg, + clk => clk, + sycnd_output => nic_rails.v1p2_nic_enet_a0hp.pg + ); + v3p3_nic_a0hp: entity work.meta_sync + port map( + async_input => nic_rails_pins.v3p3_nic_a0hp.pg, + clk => clk, + sycnd_output => nic_rails.v3p3_nic_a0hp.pg + ); + v1p1_nic_a0hp: entity work.meta_sync + port map( + async_input => nic_rails_pins.v1p1_nic_a0hp.pg, + clk => clk, + sycnd_output => nic_rails.v1p1_nic_a0hp.pg + ); + v1p4_nic_a0hp: entity work.meta_sync + port map( + async_input => nic_rails_pins.v1p4_nic_a0hp.pg, + clk => clk, + sycnd_output => nic_rails.v1p4_nic_a0hp.pg + ); + v0p96_nic_vdd_a0hp: entity work.meta_sync + port map( + async_input => nic_rails_pins.v0p96_nic_vdd_a0hp.pg, + clk => clk, + sycnd_output => nic_rails.v0p96_nic_vdd_a0hp.pg + ); + nic_hsc_12v: entity work.meta_sync + port map( + async_input => nic_rails_pins.nic_hsc_12v.pg, + clk => clk, + sycnd_output => nic_sync_12v_hsc_pg_l + ); + nic_hsc_5v: entity work.meta_sync + port map( + async_input => nic_rails_pins.nic_hsc_5v.pg, + clk => clk, + sycnd_output => nic_sync_5v_hsc_pg_l + ); + + -- HSC's are actually pg_l signals, so invert them here + nic_rails.nic_hsc_5v.pg <= not nic_sync_5v_hsc_pg_l; + nic_rails.nic_hsc_12v.pg <= not nic_sync_12v_hsc_pg_l; + -- nic sync-related stuff + nic_seq_pins.cld_rst_l <= nic_seq.cld_rst_l; + nic_seq_pins.perst_l <= nic_seq.perst_l; + nic_seq_pins.eeprom_wp_l <= nic_seq.eeprom_wp_l; + nic_seq_pins.eeprom_wp_buffer_oe_l <= nic_seq.eeprom_wp_buffer_oe_l; + nic_seq_pins.flash_wp_l <= nic_seq.flash_wp_l; + nic_seq_pins.nic_mfg_mode_l <= nic_seq.nic_mfg_mode_l; + nic_seq_pins.nic_pcie_clk_buff_oe_l <= nic_seq.nic_pcie_clk_buff_oe_l; + ext_rst_l_sync: entity work.meta_sync + port map( + async_input => nic_seq_pins.ext_rst_l, + clk => clk, + sycnd_output => nic_seq.ext_rst_l + ); + sp5_mfg_mode_l_sync: entity work.meta_sync + port map( + async_input => nic_seq_pins.sp5_mfg_mode_l, + clk => clk, + sycnd_output => nic_seq.sp5_mfg_mode_l + ); + + +end rtl; diff --git a/hdl/projects/cosmo_seq/sequencer/versal_seq.vhd b/hdl/projects/cosmo_seq/sequencer/versal_seq.vhd new file mode 100644 index 00000000..a85f9396 --- /dev/null +++ b/hdl/projects/cosmo_seq/sequencer/versal_seq.vhd @@ -0,0 +1,440 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +use work.sp5_power_pkg.all; +use work.sequencer_io_pkg.all; +use work.sequencer_regs_pkg.all; + +-- A0HP sequencing and boot supervision for the AMD Versal Premium VP1202 that +-- serves as Metro's NIC. This is the metro-specific counterpart to cosmo_seq's +-- nic_seq: same shape and the same register-driven override story, but a +-- Versal wants a staged rail bring-up followed by a strapped boot rather than +-- the T6's cld_rst/perst dance. +-- +-- Rail grouping and timing below follow the Versal power-up requirements in +-- three stages -- core (VCCINT), then auxiliary (VCCAUX), then I/O (VCCO) -- +-- with the transceiver rails riding along in whichever group enables them. +-- The exact inter-group delays are conservative placeholders and MUST be +-- confirmed against the VP1202 datasheet before hardware bring-up; they are +-- gathered into the constants below so that is a one-line change. +entity versal_seq is + generic( + CNTS_P_MS: integer + ); + port( + clk : in std_logic; + reset : in std_logic; + + sw_enable : in std_logic; + upstream_ok : in std_logic; + versal_idle : out std_logic; + versal_faulted : out std_logic; + debug_enables : in debug_enables_type; + versal_overrides_reg : in versal_overrides_type; + -- Self-clearing test MAPO, shared with the T6 flavour's register + nic_test_mapo : in std_logic; + boot_ctrl : in versal_boot_ctrl_type; + + raw_state : out nic_raw_status_type; + api_state : out nic_api_status_type; + + versal_dbg_pins : view nic_debug_seq_ss; + + -- From SP5 hotplug, one per PCIe channel. Each follows its slot's + -- power enable exactly, as the T6's did on cosmo: perst_l <= power_en. + -- The Versal is one device behind two slots, so power itself is not + -- per channel; only the resets are. + sp5_versal_cha_perst_l : in std_logic; + sp5_versal_chb_perst_l : in std_logic; + + -- True while the Versal is held in reset and not about to be let out + -- of it, i.e. while it is safe for the SP to take the Versal's boot + -- flash away from it through the mux. + versal_held_in_reset : out std_logic; + -- True while the sequencer itself wants the boot flash on the FPGA + -- side of the mux: for the pre-boot measurement, and once the Versal + -- has booted so the SP5 can reach the flash over eSPI. + flash_owned_by_seq : out std_logic; + + -- Hash engine hardware request, see hash_engine_top. Held until + -- acknowledged; hash_err is valid with the acknowledge. + hash_req : out std_logic; + hash_ack : in std_logic; + hash_err : in std_logic; + -- Outcome of the measurement for the current or last boot + hash_done : out std_logic; + hash_failed : out std_logic; + + versal_rails : view versal_power_at_fpga; + versal_boot : view versal_boot_at_fpga; + versal_pcie : view versal_pcie_at_fpga + ); +end entity; + +architecture rtl of versal_seq is + constant ONE_MS : integer := 1 * CNTS_P_MS; + constant TEN_MS : integer := 10 * ONE_MS; + constant TWENTY_MS : integer := 20 * ONE_MS; + -- How long the rails must be stable before POR_B is released. + constant RAIL_SETTLE_MS : integer := 20 * ONE_MS; + -- How long MODE[3:0] must be stable before POR_B is released. + constant MODE_SETUP_MS : integer := 1 * ONE_MS; + -- How long to wait for DONE before calling the boot failed. Versal images + -- are large and come off QSPI, so this is generous on purpose. + constant DONE_TIMEOUT_MS : integer := 5000 * ONE_MS; + + -- Rail groups. The Versal wants VCCINT up before VCCAUX before VCCO. + -- v0p92_avcc and v1p2_avtt have no enable of their own; they cascade, so + -- they are checked in the group whose enable brings them up. + function core_group_good(rails : versal_power_t) return boolean is + begin + return (rails.v0p8_vccint.pg and rails.v0p88.pg and + rails.v0p92_avcc.pg) = '1'; + end function; + + function aux_group_good(rails : versal_power_t) return boolean is + begin + return (rails.v1p5.pg and rails.v1p5_avccaux.pg and rails.v1p4.pg and + rails.v1p1.pg and rails.v1p2_avtt.pg) = '1'; + end function; + + function io_group_good(rails : versal_power_t) return boolean is + begin + return (rails.v1p8.pg and rails.v3p3.pg) = '1'; + end function; + + type versal_r_t is record + state : nic_raw_status_hw_sm; + enable_last : std_logic; + enable_pend : std_logic; + cnts : unsigned(31 downto 0); + cha_perst_l_last : std_logic; + chb_perst_l_last : std_logic; + hsc_en : std_logic; + core_en : std_logic; + aux_en : std_logic; + io_en : std_logic; + por_b : std_logic; + mode : std_logic_vector(3 downto 0); + mode_buffer_en_l : std_logic; + err_done_buff_en : std_logic; + clk_buff_oe_l : std_logic; + rails_expected : std_logic; + faulted : std_logic; + boot_failed : std_logic; + hash_req : std_logic; + hash_done : std_logic; + hash_failed : std_logic; + end record; + + constant versal_r_reset : versal_r_t := ( + state => IDLE, + enable_last => '0', + enable_pend => '0', + cnts => (others => '0'), + cha_perst_l_last => '0', + chb_perst_l_last => '0', + hsc_en => '0', + core_en => '0', + aux_en => '0', + io_en => '0', + por_b => '0', + mode => (others => '0'), + mode_buffer_en_l => '1', + err_done_buff_en => '0', + clk_buff_oe_l => '1', + rails_expected => '0', + faulted => '0', + boot_failed => '0', + hash_req => '0', + hash_done => '0', + hash_failed => '0' + ); + signal r, rin : versal_r_t; + + -- PCIe resets follow the SP5's slot power enables once the Versal has + -- booted, exactly as the T6's did on cosmo, one per channel. + signal cha_perst_l : std_logic; + signal chb_perst_l : std_logic; + signal final_outs : versal_overrides_type; + +begin + + raw_state.hw_sm <= r.state; + versal_idle <= '1' when r.state = IDLE else '0'; + versal_faulted <= r.faulted; + -- POR_B low means the Versal is held off its boot flash, so the SP may + -- safely steal the QSPI mux. Not during MODE_STRAP though: that is the + -- last stop before POR_B releases, and a grant given there would still be + -- in force when it does. + versal_held_in_reset <= '1' when r.por_b = '0' and r.state /= MODE_STRAP else '0'; + -- The sequencer's own claims on the flash: measuring it, and after boot, + -- when the Versal has finished with it and the SP5 gets it over eSPI. + flash_owned_by_seq <= '1' when r.state = HASH_IMAGE or r.state = HASH_RELEASE or + r.state = DONE else '0'; + hash_req <= r.hash_req; + hash_done <= r.hash_done; + hash_failed <= r.hash_failed; + + -- Debug header taps, on header pins 5..0 in this order + versal_dbg_pins.rails_en <= r.io_en; + versal_dbg_pins.rails_pg <= '1' when is_power_good(versal_rails) else '0'; + versal_dbg_pins.taps(5) <= final_outs.por_b; + versal_dbg_pins.taps(4) <= final_outs.cha_perst_l; + versal_dbg_pins.taps(3) <= versal_boot.done; + versal_dbg_pins.taps(2) <= versal_boot.error_out; + versal_dbg_pins.taps(1) <= r.mode(0); + versal_dbg_pins.taps(0) <= final_outs.chb_perst_l; + + api_state_proc: process(clk, reset) + begin + if reset then + api_state.nic_sm <= IDLE; + elsif rising_edge(clk) then + case r.state is + when IDLE => + api_state.nic_sm <= IDLE; + when HSC_EN | CORE_EN | AUX_EN | IO_EN => + api_state.nic_sm <= ENABLE_POWER; + when RAILS_SETTLE | MODE_STRAP => + api_state.nic_sm <= NIC_RESET; + when HASH_IMAGE | HASH_RELEASE => + api_state.nic_sm <= MEASURING; + when POR_RELEASE | WAIT_DONE => + api_state.nic_sm <= BOOTING; + when DONE => + api_state.nic_sm <= DONE; + -- the T6 states; this NIC never has them + when others => null; + end case; + end if; + end process; + + versal_sm: process(all) + variable v : versal_r_t; + variable rails_faulted : std_logic; + begin + v := r; + v.cha_perst_l_last := sp5_versal_cha_perst_l; + v.chb_perst_l_last := sp5_versal_chb_perst_l; + + -- Once we expect the rails to be up, any of them dropping is a fault. + rails_faulted := '1' when r.rails_expected = '1' and + (not is_power_good(versal_rails)) else '0'; + + v.enable_last := sw_enable; + if (sw_enable and not r.enable_last) = '1' or + (r.faulted = '1' and r.cha_perst_l_last = '0' and sp5_versal_cha_perst_l = '1') or + (r.faulted = '1' and r.chb_perst_l_last = '0' and sp5_versal_chb_perst_l = '1') then + -- Same two re-enable paths cosmo's nic_seq has: software toggling + -- the enable, or -- after a MAPO, where the SP5 owns slot power -- + -- the SP5 de-asserting PERST for a fresh attempt. Either slot + -- coming back is enough; there is only the one device to bring up. + v.enable_pend := '1'; + v.faulted := '0'; + v.boot_failed := '0'; + end if; + + case r.state is + when IDLE => + v.hsc_en := '0'; + v.core_en := '0'; + v.aux_en := '0'; + v.io_en := '0'; + v.por_b := '0'; + v.mode_buffer_en_l := '1'; + v.err_done_buff_en := '0'; + v.clk_buff_oe_l := '1'; + v.rails_expected := '0'; + v.hash_req := '0'; + v.cnts := (others => '0'); + if r.enable_pend and upstream_ok then + v.state := HSC_EN; + v.enable_pend := '0'; + v.hash_done := '0'; + v.hash_failed := '0'; + end if; + + when HSC_EN => + -- Nothing downstream reports a valid power good until the + -- hotswaps are on, so this stage waits on them alone. + v.hsc_en := '1'; + v.cnts := (others => '0'); + if (versal_rails.hsc_12v.pg and versal_rails.hsc_5v.pg) = '1' then + v.state := CORE_EN; + end if; + + when CORE_EN => + v.core_en := '1'; + v.cnts := (others => '0'); + if core_group_good(versal_rails) then + v.state := AUX_EN; + end if; + + when AUX_EN => + v.aux_en := '1'; + v.cnts := (others => '0'); + if aux_group_good(versal_rails) then + v.state := IO_EN; + end if; + + when IO_EN => + v.io_en := '1'; + v.cnts := (others => '0'); + if io_group_good(versal_rails) then + v.state := RAILS_SETTLE; + -- Every rail is up now, so hold the whole tree to account. + v.rails_expected := '1'; + end if; + + when RAILS_SETTLE => + v.cnts := r.cnts + 1; + if r.cnts = RAIL_SETTLE_MS then + -- Latch the boot mode here so a register write mid-boot + -- cannot move the straps out from under the Versal. + v.mode := boot_ctrl.mode; + v.cnts := (others => '0'); + if boot_ctrl.hash_image = '1' then + v.state := HASH_IMAGE; + else + v.state := MODE_STRAP; + end if; + end if; + + -- Measure the boot image while the Versal is still in POR and the + -- flash is ours. The hash engine owns the timing: a large image + -- takes seconds, and software can abort a run that is going + -- nowhere, which comes back as an error here. Either way the + -- Versal boots; whether a bad measurement matters is for the SP. + when HASH_IMAGE => + v.hash_req := '1'; + if hash_ack = '1' then + v.hash_req := '0'; + v.hash_done := not hash_err; + v.hash_failed := hash_err; + v.state := HASH_RELEASE; + end if; + + when HASH_RELEASE => + -- Let the handshake finish before anything else can start one + if hash_ack = '0' then + v.state := MODE_STRAP; + end if; + + when MODE_STRAP => + v.mode_buffer_en_l := '0'; + v.err_done_buff_en := '1'; + v.cnts := r.cnts + 1; + if r.cnts = MODE_SETUP_MS then + v.state := POR_RELEASE; + v.cnts := (others => '0'); + end if; + + when POR_RELEASE => + v.por_b := '1'; + v.clk_buff_oe_l := '0'; + v.state := WAIT_DONE; + v.cnts := (others => '0'); + + when WAIT_DONE => + v.cnts := r.cnts + 1; + if versal_boot.done = '1' then + v.state := DONE; + v.cnts := (others => '0'); + elsif versal_boot.error_out = '1' or r.cnts = DONE_TIMEOUT_MS then + -- A boot failure is not a power fault: the rails are fine + -- and there is a device to talk to over JTAG, so stay here + -- and let software decide rather than dropping power. + v.boot_failed := '1'; + v.cnts := r.cnts; + end if; + + when DONE => + if sw_enable = '0' then + v.state := IDLE; + end if; + -- the T6 states; this NIC never has them + when others => null; + end case; + + -- MAPO handling, monitored in every non-IDLE state. A measurement + -- in flight is simply abandoned: the request drops in IDLE and the + -- engine's acknowledge, whenever it comes, is ignored there. + if r.state /= IDLE then + if rails_faulted = '1' or upstream_ok = '0' or + nic_test_mapo = '1' then + v.faulted := '1'; + v.state := IDLE; + v.rails_expected := '0'; + end if; + end if; + + rin <= v; + end process; + + -- PCIe resets follow the SP5 hotplug slot power once the Versal is up. + cha_perst_l <= '1' when r.state = DONE and sp5_versal_cha_perst_l = '1' and + debug_enables.force_nic_reset = '0' else '0'; + chb_perst_l <= '1' when r.state = DONE and sp5_versal_chb_perst_l = '1' and + debug_enables.force_nic_reset = '0' else '0'; + + reg_proc: process(clk, reset) + begin + if reset then + r <= versal_r_reset; + elsif rising_edge(clk) then + r <= rin; + end if; + end process; + + -- Register and mux the Versal outputs, letting the debug registers take + -- them over wholesale when asked. + out_reg: process(clk, reset) + begin + if reset then + final_outs <= (others => '0'); + final_outs.mode_buffer_en_l <= '1'; + final_outs.cha_clk_buff_oe_l <= '1'; + final_outs.chb_clk_buff_oe_l <= '1'; + elsif rising_edge(clk) then + if debug_enables.nic_override then + final_outs <= versal_overrides_reg; + else + final_outs.por_b <= r.por_b and not debug_enables.force_nic_reset; + final_outs.mode_buffer_en_l <= r.mode_buffer_en_l; + final_outs.err_done_buff_en <= r.err_done_buff_en; + final_outs.cha_perst_l <= cha_perst_l; + final_outs.chb_perst_l <= chb_perst_l; + final_outs.cha_clk_buff_oe_l <= r.clk_buff_oe_l; + final_outs.chb_clk_buff_oe_l <= r.clk_buff_oe_l; + end if; + end if; + end process; + + versal_boot.por_b <= final_outs.por_b; + versal_boot.mode <= r.mode; + versal_boot.mode_buffer_en_l <= final_outs.mode_buffer_en_l; + versal_boot.err_done_buff_en <= final_outs.err_done_buff_en; + + versal_pcie.cha.perst_l <= final_outs.cha_perst_l; + versal_pcie.cha.clk_buff_oe_l <= final_outs.cha_clk_buff_oe_l; + versal_pcie.chb.perst_l <= final_outs.chb_perst_l; + versal_pcie.chb.clk_buff_oe_l <= final_outs.chb_clk_buff_oe_l; + + -- One enable per rail, staged by the state machine above. + versal_rails.hsc_12v.enable <= r.hsc_en; + versal_rails.v0p8_vccint.enable <= r.core_en; + versal_rails.v0p88.enable <= r.core_en; + versal_rails.v1p5.enable <= r.aux_en; + versal_rails.v1p5_avccaux.enable <= r.aux_en; + versal_rails.v1p4.enable <= r.aux_en; + versal_rails.v1p1.enable <= r.aux_en; + versal_rails.v1p8.enable <= r.io_en; + versal_rails.v3p3.enable <= r.io_en; + +end rtl; diff --git a/hdl/projects/cosmo_seq/sequencer/versal_sync.vhd b/hdl/projects/cosmo_seq/sequencer/versal_sync.vhd new file mode 100644 index 00000000..d935a1d1 --- /dev/null +++ b/hdl/projects/cosmo_seq/sequencer/versal_sync.vhd @@ -0,0 +1,213 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +use work.sp5_power_pkg.all; +use work.sequencer_io_pkg.all; +use work.sequencer_regs_pkg.all; + +-- Synchronises metro's Versal NIC pins: the rail power goods, the boot +-- status pins and the two PCIe channels' slot inputs. The SP5-side pins are +-- done in seq_sync. +entity versal_sync is + port ( + clk : in std_logic; + reset : in std_logic; + + -- pins (unsync'd) interface + versal_rails_pins : view versal_power_at_fpga; + versal_boot_pins : view versal_boot_at_fpga; + versal_pcie_pins : view versal_pcie_at_fpga; + -- internal, synchronized interfaces + rail_masks : in rails_type; + versal_rails : view versal_power_at_reg; + versal_boot : view versal_boot_at_versal; + versal_pcie : view versal_pcie_at_nic + ); +end entity; + +architecture rtl of versal_sync is + signal versal_sync_5v_hsc_pg_l : std_logic; + signal versal_sync_12v_hsc_pg_l : std_logic; + signal versal_v3p3_pg_raw : std_logic; + signal versal_v1p8_pg_raw : std_logic; + signal versal_v1p5_pg_raw : std_logic; + signal versal_v1p5_avccaux_pg_raw : std_logic; + signal versal_v1p4_pg_raw : std_logic; + signal versal_v1p1_pg_raw : std_logic; + signal versal_v0p88_pg_raw : std_logic; + signal versal_v0p8_vccint_pg_raw : std_logic; + signal versal_v0p92_avcc_pg_raw : std_logic; + signal versal_v1p2_avtt_pg_raw : std_logic; +begin + + -- Versal rails sync stuff + versal_rails_pins.v3p3.enable <= versal_rails.v3p3.enable; + versal_rails_pins.v1p8.enable <= versal_rails.v1p8.enable; + versal_rails_pins.v1p5.enable <= versal_rails.v1p5.enable; + versal_rails_pins.v1p5_avccaux.enable <= versal_rails.v1p5_avccaux.enable; + versal_rails_pins.v1p4.enable <= versal_rails.v1p4.enable; + versal_rails_pins.v1p1.enable <= versal_rails.v1p1.enable; + versal_rails_pins.v0p88.enable <= versal_rails.v0p88.enable; + versal_rails_pins.v0p8_vccint.enable <= versal_rails.v0p8_vccint.enable; + versal_rails_pins.hsc_12v.enable <= versal_rails.hsc_12v.enable; + + versal_v3p3_pg: entity work.meta_sync + port map( + async_input => versal_rails_pins.v3p3.pg, + clk => clk, + sycnd_output => versal_v3p3_pg_raw + ); + versal_rails.v3p3.pg <= versal_v3p3_pg_raw when rail_masks.versal_v3p3 = '0' else '0'; + + versal_v1p8_pg: entity work.meta_sync + port map( + async_input => versal_rails_pins.v1p8.pg, + clk => clk, + sycnd_output => versal_v1p8_pg_raw + ); + versal_rails.v1p8.pg <= versal_v1p8_pg_raw when rail_masks.versal_v1p8 = '0' else '0'; + + versal_v1p5_pg: entity work.meta_sync + port map( + async_input => versal_rails_pins.v1p5.pg, + clk => clk, + sycnd_output => versal_v1p5_pg_raw + ); + versal_rails.v1p5.pg <= versal_v1p5_pg_raw when rail_masks.versal_v1p5 = '0' else '0'; + + versal_v1p5_avccaux_pg: entity work.meta_sync + port map( + async_input => versal_rails_pins.v1p5_avccaux.pg, + clk => clk, + sycnd_output => versal_v1p5_avccaux_pg_raw + ); + versal_rails.v1p5_avccaux.pg <= versal_v1p5_avccaux_pg_raw when rail_masks.versal_v1p5_avccaux = '0' else '0'; + + versal_v1p4_pg: entity work.meta_sync + port map( + async_input => versal_rails_pins.v1p4.pg, + clk => clk, + sycnd_output => versal_v1p4_pg_raw + ); + versal_rails.v1p4.pg <= versal_v1p4_pg_raw when rail_masks.versal_v1p4 = '0' else '0'; + + versal_v1p1_pg: entity work.meta_sync + port map( + async_input => versal_rails_pins.v1p1.pg, + clk => clk, + sycnd_output => versal_v1p1_pg_raw + ); + versal_rails.v1p1.pg <= versal_v1p1_pg_raw when rail_masks.versal_v1p1 = '0' else '0'; + + versal_v0p88_pg: entity work.meta_sync + port map( + async_input => versal_rails_pins.v0p88.pg, + clk => clk, + sycnd_output => versal_v0p88_pg_raw + ); + versal_rails.v0p88.pg <= versal_v0p88_pg_raw when rail_masks.versal_v0p88 = '0' else '0'; + + versal_v0p8_vccint_pg: entity work.meta_sync + port map( + async_input => versal_rails_pins.v0p8_vccint.pg, + clk => clk, + sycnd_output => versal_v0p8_vccint_pg_raw + ); + versal_rails.v0p8_vccint.pg <= versal_v0p8_vccint_pg_raw when rail_masks.versal_v0p8_vccint = '0' else '0'; + + versal_v0p92_avcc_pg: entity work.meta_sync + port map( + async_input => versal_rails_pins.v0p92_avcc.pg, + clk => clk, + sycnd_output => versal_v0p92_avcc_pg_raw + ); + versal_rails.v0p92_avcc.pg <= versal_v0p92_avcc_pg_raw when rail_masks.versal_v0p92_avcc = '0' else '0'; + + versal_v1p2_avtt_pg: entity work.meta_sync + port map( + async_input => versal_rails_pins.v1p2_avtt.pg, + clk => clk, + sycnd_output => versal_v1p2_avtt_pg_raw + ); + versal_rails.v1p2_avtt.pg <= versal_v1p2_avtt_pg_raw when rail_masks.versal_v1p2_avtt = '0' else '0'; + + versal_hsc_12v_pg: entity work.meta_sync + port map( + async_input => versal_rails_pins.hsc_12v.pg, + clk => clk, + sycnd_output => versal_sync_12v_hsc_pg_l + ); + + versal_hsc_5v_pg: entity work.meta_sync + port map( + async_input => versal_rails_pins.hsc_5v.pg, + clk => clk, + sycnd_output => versal_sync_5v_hsc_pg_l + ); + + -- The hotswap controllers report power good active low, so invert here and + -- let everything above this line treat power good as active high. + versal_rails.hsc_12v.pg <= (not versal_sync_12v_hsc_pg_l) when rail_masks.nic_hsc_12v = '0' else '0'; + versal_rails.hsc_5v.pg <= (not versal_sync_5v_hsc_pg_l) when rail_masks.nic_hsc_5v = '0' else '0'; + + -- Versal boot straps and status + versal_boot_pins.mode <= versal_boot.mode; + versal_boot_pins.mode_buffer_en_l <= versal_boot.mode_buffer_en_l; + versal_boot_pins.por_b <= versal_boot.por_b; + versal_boot_pins.err_done_buff_en <= versal_boot.err_done_buff_en; + + versal_done_sync: entity work.meta_sync + port map( + async_input => versal_boot_pins.done, + clk => clk, + sycnd_output => versal_boot.done + ); + + versal_error_out_sync: entity work.meta_sync + port map( + async_input => versal_boot_pins.error_out, + clk => clk, + sycnd_output => versal_boot.error_out + ); + + -- Versal PCIe channels + versal_pcie_pins.cha.perst_l <= versal_pcie.cha.perst_l; + versal_pcie_pins.cha.clk_buff_oe_l <= versal_pcie.cha.clk_buff_oe_l; + versal_pcie_pins.chb.perst_l <= versal_pcie.chb.perst_l; + versal_pcie_pins.chb.clk_buff_oe_l <= versal_pcie.chb.clk_buff_oe_l; + + versal_cha_prsnt_l_sync: entity work.meta_sync + port map( + async_input => versal_pcie_pins.cha.prsnt_l, + clk => clk, + sycnd_output => versal_pcie.cha.prsnt_l + ); + + versal_cha_pwren_l_sync: entity work.meta_sync + port map( + async_input => versal_pcie_pins.cha.pwren_l, + clk => clk, + sycnd_output => versal_pcie.cha.pwren_l + ); + + versal_chb_prsnt_l_sync: entity work.meta_sync + port map( + async_input => versal_pcie_pins.chb.prsnt_l, + clk => clk, + sycnd_output => versal_pcie.chb.prsnt_l + ); + + versal_chb_pwren_l_sync: entity work.meta_sync + port map( + async_input => versal_pcie_pins.chb.pwren_l, + clk => clk, + sycnd_output => versal_pcie.chb.pwren_l + ); + + +end rtl; diff --git a/hdl/projects/cosmo_seq/sims/cosmo_seq_top_tb.vhd b/hdl/projects/cosmo_seq/sims/cosmo_seq_top_tb.vhd new file mode 100644 index 00000000..b685b112 --- /dev/null +++ b/hdl/projects/cosmo_seq/sims/cosmo_seq_top_tb.vhd @@ -0,0 +1,34 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +library ieee; +use ieee.std_logic_1164.all; + +library vunit_lib; + context vunit_lib.vunit_context; + +-- Analysis-only smoke test, as metro_seq has: it does not drive the top level, +-- its job is to make a simulator compile the whole design tree, which the +-- synthesis-only build rules do not do on their own. Behavioural coverage +-- lives in the per-subsystem testbenches. +entity cosmo_seq_top_tb is + generic ( + runner_cfg : string + ); +end entity; + +architecture tb of cosmo_seq_top_tb is +begin + bench: process + begin + test_runner_setup(runner, runner_cfg); + while test_suite loop + if run("analyses") then + info("cosmo_seq_top and its dependencies analysed"); + end if; + end loop; + test_runner_cleanup(runner); + wait; + end process; +end tb; diff --git a/hdl/projects/cosmo_seq/sp5_espi_flash_subsystem/BUCK b/hdl/projects/cosmo_seq/sp5_espi_flash_subsystem/BUCK index 0027219d..64f15e8d 100644 --- a/hdl/projects/cosmo_seq/sp5_espi_flash_subsystem/BUCK +++ b/hdl/projects/cosmo_seq/sp5_espi_flash_subsystem/BUCK @@ -11,7 +11,6 @@ vhdl_unit( "//hdl/ip/vhd/axi_blocks:axist_if_2k19_pkg", "//hdl/ip/vhd/axi_blocks:axil_interconnect", "//hdl/ip/vhd/spi_nor_controller:spi_nor_top", - "//hdl/ip/vhd/hash_engine:hash_engine_top", "//hdl/ip/vhd/fifos:dcfifo_xpm", ], standard = "2019", diff --git a/hdl/projects/cosmo_seq/sp5_espi_flash_subsystem/sp5_espi_flash_subsystem.vhd b/hdl/projects/cosmo_seq/sp5_espi_flash_subsystem/sp5_espi_flash_subsystem.vhd index 572059d0..52951b18 100644 --- a/hdl/projects/cosmo_seq/sp5_espi_flash_subsystem/sp5_espi_flash_subsystem.vhd +++ b/hdl/projects/cosmo_seq/sp5_espi_flash_subsystem/sp5_espi_flash_subsystem.vhd @@ -12,6 +12,18 @@ use work.axi_st8_pkg.all; entity sp5_espi_flash_subsystem is + generic ( + -- Passed to espi_target_top. Off for the SP5 boot flash, where the + -- host must never be able to modify what it boots from. + FLASH_WRITES_ALLOWED : boolean := false; + -- Passed to espi_target_top. A flash-only instance has no use for + -- the post code buffer. + POST_CODE_BUFFER_ENABLED : boolean := true; + -- spi_nor_top's rate and sample point. The defaults are the SP5 boot + -- flash's; a flash on a slower bank or a longer path wants its own. + SPI_NOR_SCLK_DIVISOR : natural := 0; + SPI_NOR_RX_SAMPLE_TAPS : natural range 0 to 4 := 2 + ); port( clk_125m : in std_logic; reset_125m : in std_logic; @@ -35,12 +47,19 @@ entity sp5_espi_flash_subsystem is spi_nor_dat : in std_logic_vector(3 downto 0); spi_nor_dat_o : out std_logic_vector(3 downto 0); spi_nor_dat_oe : out std_logic_vector(3 downto 0); + -- Parks the flash pins when low, see spi_nor_top. Only a design that + -- shares the flash through a mux needs to drive it. + spi_nor_bus_enable : in std_logic := '1'; - -- SHA3 hashing engine. It lives here rather than at the top level because - -- it reads the flash through spi_nor_top's second client port, so it needs - -- the same command/response FIFO pattern the eSPI flash channel uses. - hash_axi_if : view axil8x32_pkg.axil_target; - + -- spi_nor_top's second flash client port, for the hashing engine. The + -- engine sits at the project top rather than in here so that one engine + -- can be shared between several of these wrappers; these are the + -- engine's own FIFO endpoints, in spi_nor_top's port shape. + hash_cmd_fifo_rdata : in std_logic_vector(31 downto 0); + hash_cmd_fifo_rdack : out std_logic; + hash_cmd_fifo_rempty : in std_logic; + hash_data_fifo_wdata : out std_logic_vector(7 downto 0); + hash_data_fifo_write : out std_logic ); end entity; @@ -57,23 +76,13 @@ architecture rtl of sp5_espi_flash_subsystem is signal flash_rfifo_rempty : std_logic; signal flash_fifo_clear : std_logic; signal fifo_reset : std_logic; + signal flash_wfifo_data : std_logic_vector(7 downto 0); + signal flash_wfifo_write : std_logic; + signal espi_wfifo_rdata : std_logic_vector(7 downto 0); + signal espi_wfifo_rdack : std_logic; + signal espi_wfifo_rempty : std_logic; signal rst_cnts : integer range 0 to 5 := 5; - -- Hashing engine <-> spi_nor_top, the same shape as the eSPI pair above. - -- Deliberately not tied to fifo_reset: that is flushed on every eSPI reset, - -- which happens at the start of every boot and has nothing to do with a hash - -- the SP may have in flight. The engine resynchronises its own channel by - -- draining it, so a global reset is the only thing that needs to clear these. - signal hash_cmd_fifo_wdata : std_logic_vector(31 downto 0); - signal hash_cmd_fifo_write : std_logic; - signal hash_cmd_fifo_rdata : std_logic_vector(31 downto 0); - signal hash_cmd_fifo_rdack : std_logic; - signal hash_cmd_fifo_rempty : std_logic; - signal hash_data_fifo_wdata : std_logic_vector(7 downto 0); - signal hash_data_fifo_write : std_logic; - signal hash_rsp_fifo_rdata : std_logic_vector(7 downto 0); - signal hash_rsp_fifo_rdack : std_logic; - signal hash_rsp_fifo_rempty : std_logic; begin @@ -139,64 +148,45 @@ begin rusedwds => open ); - -- Hashing engine -> SPI NOR FIFO - hash_spinor_cmd_fifo: entity work.dcfifo_xpm - generic map( - fifo_write_depth => 256, - data_width => 32, - showahead_mode => true - ) - port map( - wclk => clk_125m, - reset => reset_125m, - write_en => hash_cmd_fifo_write, - wdata => hash_cmd_fifo_wdata, - wfull => open, - wusedwds => open, - rclk => clk_125m, - rdata => hash_cmd_fifo_rdata, - rdreq => hash_cmd_fifo_rdack, - rempty => hash_cmd_fifo_rempty, - rusedwds => open - ); - -- SPI NOR -> hashing engine FIFO - hash_spinor_data_fifo: entity work.dcfifo_xpm - generic map( - fifo_write_depth => 256, - data_width => 8, - showahead_mode => true - ) - port map( - wclk => clk_125m, - reset => reset_125m, - write_en => hash_data_fifo_write, - wdata => hash_data_fifo_wdata, - wfull => open, - wusedwds => open, - rclk => clk_125m, - rdata => hash_rsp_fifo_rdata, - rdreq => hash_rsp_fifo_rdack, - rempty => hash_rsp_fifo_rempty, - rusedwds => open - ); - - hash_engine_inst: entity work.hash_engine_top - port map( - clk => clk_125m, - reset => reset_125m, - axi_if => hash_axi_if, - cmd_fifo_wdata => hash_cmd_fifo_wdata, - cmd_fifo_write => hash_cmd_fifo_write, - rsp_fifo_rdata => hash_rsp_fifo_rdata, - rsp_fifo_rdack => hash_rsp_fifo_rdack, - rsp_fifo_rempty => hash_rsp_fifo_rempty - ); - -- eSPI block -- Only the link layer runs at 200MHz, the remaining -- logic runs at 125MHz so all the interfaces are synchronous -- to 125MHz + -- Host to flash write payloads. One eSPI write's payload at most sits in + -- here at a time: the eSPI side issues one flash command at a time and + -- the payload is consumed before it reports the command done. Sized for + -- the flash channel's 1kB per-descriptor slot. Absent on a read-only + -- instance, where it would only ever be empty. + wfifo: if FLASH_WRITES_ALLOWED generate + espi_spinor_wdata_fifo: entity work.dcfifo_xpm + generic map( + fifo_write_depth => 1024, + data_width => 8, + showahead_mode => true + ) + port map( + wclk => clk_125m, + reset => fifo_reset, + write_en => flash_wfifo_write, + wdata => flash_wfifo_data, + wfull => open, + wusedwds => open, + rclk => clk_125m, + rdata => espi_wfifo_rdata, + rdreq => espi_wfifo_rdack, + rempty => espi_wfifo_rempty, + rusedwds => open + ); + else generate + espi_wfifo_rdata <= (others => '0'); + espi_wfifo_rempty <= '1'; + end generate; + espi_target_top_inst: entity work.espi_target_top + generic map( + FLASH_WRITES_ALLOWED => FLASH_WRITES_ALLOWED, + POST_CODE_BUFFER_ENABLED => POST_CODE_BUFFER_ENABLED + ) port map( clk_200m => clk_200m, reset_200m => reset_200m, @@ -215,6 +205,8 @@ begin flash_rfifo_data => flash_rfifo_data, flash_rfifo_rdack => flash_rfifo_rdack, flash_rfifo_rempty => flash_rfifo_rempty, + flash_wfifo_data => flash_wfifo_data, + flash_wfifo_write => flash_wfifo_write, to_sp_uart_data => ipcc_uart_from_espi.data, to_sp_uart_valid => ipcc_uart_from_espi.valid, to_sp_uart_ready => ipcc_uart_from_espi.ready, @@ -231,14 +223,14 @@ begin -- round trip out to the flash and back has to land within half an -- sclk period of rx_sample_taps, and above this rate that window -- closes. Faster would need per-lane IDELAY read training. - sclk_divisor => 0, + sclk_divisor => SPI_NOR_SCLK_DIVISOR, -- Sample 8ns after the sclk rising edge. Taps are in half-clk (4ns) -- steps. With the flash IO flops packed into the IOBs the round trip -- out and back is bounded to roughly 3.7..11.6ns, which puts the -- usable sample window at 3.6..11.7ns; 8ns sits about 4ns clear of -- either end. cosmo_timing.xdc carries the arithmetic. Sweep this on -- hardware if reads come back corrupted. - rx_sample_taps => 2, + rx_sample_taps => SPI_NOR_RX_SAMPLE_TAPS, cs_setup_cnts => 4, cs_high_cnts => 7 ) @@ -252,11 +244,15 @@ begin io_o => spi_nor_dat_o, io_oe => spi_nor_dat_oe, sp5_owns_flash => open, + bus_enable => spi_nor_bus_enable, espi_cmd_fifo_rdata => espi_cmd_fifo_rdata, espi_cmd_fifo_rdack => espi_cmd_fifo_rdack, espi_cmd_fifo_rempty => espi_cmd_fifo_rempty, espi_data_fifo_wdata => espi_data_fifo_wdata, espi_data_fifo_write => espi_data_fifo_write, + espi_wfifo_rdata => espi_wfifo_rdata, + espi_wfifo_rdack => espi_wfifo_rdack, + espi_wfifo_rempty => espi_wfifo_rempty, hash_cmd_fifo_rdata => hash_cmd_fifo_rdata, hash_cmd_fifo_rdack => hash_cmd_fifo_rdack, hash_cmd_fifo_rempty => hash_cmd_fifo_rempty, diff --git a/hdl/projects/cosmo_seq/sp5_hotplug_subsystem/BUCK b/hdl/projects/cosmo_seq/sp5_hotplug_subsystem/BUCK index 140245e8..711b514c 100644 --- a/hdl/projects/cosmo_seq/sp5_hotplug_subsystem/BUCK +++ b/hdl/projects/cosmo_seq/sp5_hotplug_subsystem/BUCK @@ -16,10 +16,23 @@ vhdl_unit( ], deps = [ ":perst_oneshot", + "//hdl/ip/vhd/common:time_pkg", "//hdl/ip/vhd/synchronizers:meta_sync", "//hdl/ip/vhd/axi_blocks:axist_if_2k19_pkg", "//hdl/ip/vhd/i2c/io_expanders/PCA9506ish:pca9506_top", ], standard = "2019", visibility = ["PUBLIC"], -) \ No newline at end of file +) +vunit_sim( + name = "sp5_hotplug_tb", + srcs = glob(["sims/*.vhd"]), + deps = [ + ":sp5_hotplug_subsystem", + "//hdl/ip/vhd/i2c/io_expanders/PCA9506ish:pca9506_sim_pkg", + "//hdl/ip/vhd/vunit_components:i2c_controller_vc", + "//hdl/ip/vhd/axi_blocks:axilite_if_2k19", + ], + visibility = ["PUBLIC"], + standard = "2019", +) diff --git a/hdl/projects/cosmo_seq/sp5_hotplug_subsystem/sims/sp5_hotplug_tb.vhd b/hdl/projects/cosmo_seq/sp5_hotplug_subsystem/sims/sp5_hotplug_tb.vhd new file mode 100644 index 00000000..937393fb --- /dev/null +++ b/hdl/projects/cosmo_seq/sp5_hotplug_subsystem/sims/sp5_hotplug_tb.vhd @@ -0,0 +1,149 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; +use ieee.numeric_std_unsigned.all; + +library vunit_lib; + context vunit_lib.com_context; + context vunit_lib.vunit_context; + context vunit_lib.vc_context; + +use work.pca9506_regs_pkg.all; +use work.i2c_pca9506ish_sim_pkg.all; + +-- Slot-level behaviour of the two NIC hotplug slots, as the SP5 sees them +-- through the emulated PCA9506: bank 2 (the T6 slot cosmo has always had) and +-- bank 4 (the second slot metro adds). The expander itself is covered by +-- i2c_pca9506ish_tb; this is about the wiring from its bits to the slot pins. +entity sp5_hotplug_tb is + generic ( + runner_cfg : string + ); +end entity; + +architecture tb of sp5_hotplug_tb is + constant hp_addr : std_logic_vector(6 downto 0) := b"0100_010"; + -- AMD Mode A: bit 4 of each bank is PWR_EN_L, an output from the SP5. + constant ALL_INPUTS : std_logic_vector(7 downto 0) := x"FF"; + constant PWR_EN_AS_OUTPUT : std_logic_vector(7 downto 0) := x"EF"; + constant PWR_EN_ASSERTED : std_logic_vector(7 downto 0) := x"00"; + constant PWR_EN_DEASSERTED : std_logic_vector(7 downto 0) := x"10"; +begin + + th: entity work.sp5_hotplug_th; + + bench: process + alias reset is << signal th.reset : std_logic >>; + alias t6_power_en is << signal th.t6_power_en : std_logic >>; + alias t6_perst_l is << signal th.t6_perst_l : std_logic >>; + alias t6_faulted is << signal th.t6_faulted : std_logic >>; + alias t6_prsnt_l is << signal th.t6_prsnt_l : std_logic >>; + alias nic2_power_en is << signal th.nic2_power_en : std_logic >>; + alias nic2_perst_l is << signal th.nic2_perst_l : std_logic >>; + alias nic2_faulted is << signal th.nic2_faulted : std_logic >>; + alias nic2_prsnt_l is << signal th.nic2_prsnt_l : std_logic >>; + variable ack_status : boolean; + constant rx_queue : queue_t := new_queue; + constant ack_queue : queue_t := new_queue; + variable ip : std_logic_vector(7 downto 0); + + -- Drive a bank's PWR_EN_L through the expander: configure bit 4 as an + -- output, then write it. + procedure set_pwr_en(bank : natural; asserted : boolean) is + begin + single_write_pca9506_reg(net, hp_addr, I2C_IOC0_OFFSET + bank, PWR_EN_AS_OUTPUT, ack_status); + if asserted then + single_write_pca9506_reg(net, hp_addr, I2C_OP0_OFFSET + bank, PWR_EN_ASSERTED, ack_status); + else + single_write_pca9506_reg(net, hp_addr, I2C_OP0_OFFSET + bank, PWR_EN_DEASSERTED, ack_status); + end if; + wait for 200 ns; + end procedure; + + procedure read_ip(bank : natural; variable value : out std_logic_vector(7 downto 0)) is + begin + read_pca9506_reg(net, hp_addr, I2C_IP0_OFFSET + bank, 1, rx_queue, ack_queue); + flush(ack_queue); + value := to_std_logic_vector(pop_byte(rx_queue), 8); + end procedure; + begin + test_runner_setup(runner, runner_cfg); + wait until reset = '0'; + wait for 500 ns; + + while test_suite loop + if run("nic_slot_power_enable_follows_expander") then + check_equal(t6_power_en, '0', "NIC slot must be off out of reset"); + set_pwr_en(2, true); + check_equal(t6_power_en, '1', "PWR_EN_L low should enable the NIC slot"); + check_equal(t6_perst_l, '1', "PERST follows the power enable on this slot"); + set_pwr_en(2, false); + check_equal(t6_power_en, '0', "PWR_EN_L high should disable the NIC slot"); + check_equal(t6_perst_l, '0', "PERST follows the power enable on this slot"); + + elsif run("nic2_slot_power_enable_follows_expander") then + check_equal(nic2_power_en, '0', "second NIC slot must be off out of reset"); + set_pwr_en(4, true); + check_equal(nic2_power_en, '1', "PWR_EN_L low should enable the second NIC slot"); + check_equal(nic2_perst_l, '1', "PERST follows the power enable on this slot"); + set_pwr_en(4, false); + check_equal(nic2_power_en, '0', "PWR_EN_L high should disable the second NIC slot"); + check_equal(nic2_perst_l, '0', "PERST follows the power enable on this slot"); + + elsif run("nic_slots_are_independent") then + set_pwr_en(2, true); + check_equal(nic2_power_en, '0', "enabling bank 2 must not enable bank 4"); + set_pwr_en(4, true); + check_equal(t6_power_en, '1', "bank 2 stays enabled when bank 4 is enabled"); + set_pwr_en(2, false); + check_equal(nic2_power_en, '1', "disabling bank 2 must not disable bank 4"); + + elsif run("power_enable_ignored_until_bank_is_configured") then + -- The guard used to look at bank 0's direction bit, so + -- configuring M.2 A as an output would have let a still-input + -- bank 2 turn the NIC on. Configure everything *but* bank 2 and + -- bank 4 as outputs and write their OP bits low: neither NIC + -- slot may move. + for bank in 0 to 3 loop + if bank /= 2 then + single_write_pca9506_reg(net, hp_addr, I2C_IOC0_OFFSET + bank, PWR_EN_AS_OUTPUT, ack_status); + end if; + end loop; + single_write_pca9506_reg(net, hp_addr, I2C_OP0_OFFSET + 2, PWR_EN_ASSERTED, ack_status); + single_write_pca9506_reg(net, hp_addr, I2C_OP0_OFFSET + 4, PWR_EN_ASSERTED, ack_status); + wait for 200 ns; + check_equal(t6_power_en, '0', "bank 2 still an input: NIC slot must stay off"); + check_equal(nic2_power_en, '0', "bank 4 still an input: second NIC slot must stay off"); + + elsif run("nic_slots_report_presence_and_fault") then + -- Both present, no fault: PRSNT_L low, PWRFLT_L high. + read_ip(2, ip); + check_equal(ip(0), '0', "bank 2 PRSNT_L should follow the presence input"); + check_equal(ip(1), '1', "bank 2 PWRFLT_L should be deasserted with no fault"); + read_ip(4, ip); + check_equal(ip(0), '0', "bank 4 PRSNT_L should follow the presence input"); + check_equal(ip(1), '1', "bank 4 PWRFLT_L should be deasserted with no fault"); + + t6_prsnt_l <= '1'; + nic2_faulted <= '1'; + wait for 200 ns; + read_ip(2, ip); + check_equal(ip(0), '1', "bank 2 PRSNT_L should deassert when the NIC is absent"); + read_ip(4, ip); + check_equal(ip(1), '0', "bank 4 PWRFLT_L should assert on a fault"); + t6_prsnt_l <= '0'; + nic2_faulted <= '0'; + end if; + end loop; + + wait for 2 us; + test_runner_cleanup(runner); + wait; + end process; + + test_runner_watchdog(runner, 10 ms); +end tb; diff --git a/hdl/projects/cosmo_seq/sp5_hotplug_subsystem/sims/sp5_hotplug_th.vhd b/hdl/projects/cosmo_seq/sp5_hotplug_subsystem/sims/sp5_hotplug_th.vhd new file mode 100644 index 00000000..c95a0c6b --- /dev/null +++ b/hdl/projects/cosmo_seq/sp5_hotplug_subsystem/sims/sp5_hotplug_th.vhd @@ -0,0 +1,143 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +library vunit_lib; + context vunit_lib.vunit_context; + context vunit_lib.com_context; + context vunit_lib.vc_context; + +use work.axil8x32_pkg; +use work.i2c_ctrl_vc_pkg.all; +use work.i2c_pca9506ish_sim_pkg.all; + +-- One hotplug subsystem with the second NIC slot enabled, driven over I2C the +-- way the SP5 drives it. The slot pins are exposed as signals for the tb to +-- read and poke via external names. +entity sp5_hotplug_th is +end entity; + +architecture th of sp5_hotplug_th is + + signal clk : std_logic := '0'; + signal reset : std_logic := '1'; + + signal i2c_bus_scl : std_logic; + signal i2c_bus_sda : std_logic; + signal scl_o, scl_oe, sda_o, sda_oe : std_logic; + signal axi_if : axil8x32_pkg.axil_t; + + signal int_n : std_logic; + signal a0_ok : std_logic := '1'; + + -- NIC slot, bank 2 + signal t6_power_en : std_logic; + signal t6_perst_l : std_logic; + signal t6_faulted : std_logic := '0'; + signal t6_prsnt_l : std_logic := '0'; + -- Second NIC slot, bank 4 + signal nic2_power_en : std_logic; + signal nic2_perst_l : std_logic; + signal nic2_faulted : std_logic := '0'; + signal nic2_prsnt_l : std_logic := '0'; + + -- The M.2 and backplane slots are not under test; park their inputs. + signal m2a_hsc_en, m2a_perst_l, pcie_clk_buff_m2a_oe_l : std_logic; + signal m2b_hsc_en, m2b_perst_l, pcie_clk_buff_m2b_oe_l : std_logic; + signal pcie_aux_rsw_perst_l, pcie_clk_buff_rsw_oe_l : std_logic; + +begin + + clk <= not clk after 4 ns; + reset <= '0' after 200 ns; + + axi_lite_master_inst: entity vunit_lib.axi_lite_master + generic map ( + bus_handle => bus_handle + ) + port map ( + aclk => clk, + arready => axi_if.read_address.ready, + arvalid => axi_if.read_address.valid, + araddr => axi_if.read_address.addr, + rready => axi_if.read_data.ready, + rvalid => axi_if.read_data.valid, + rdata => axi_if.read_data.data, + rresp => axi_if.read_data.resp, + awready => axi_if.write_address.ready, + awvalid => axi_if.write_address.valid, + awaddr => axi_if.write_address.addr, + wready => axi_if.write_data.ready, + wvalid => axi_if.write_data.valid, + wdata => axi_if.write_data.data, + wstrb => axi_if.write_data.strb, + bvalid => axi_if.write_response.valid, + bready => axi_if.write_response.ready, + bresp => axi_if.write_response.resp + ); + + i2c_controller_vc_inst: entity work.i2c_controller_vc + generic map( + i2c_ctrl_vc => i2c_ctrl_vc + ) + port map( + scl => i2c_bus_scl, + sda => i2c_bus_sda + ); + + -- Open-drain resolution for the one target on the bus + i2c_bus_scl <= scl_o when scl_oe = '1' else 'H'; + i2c_bus_sda <= sda_o when sda_oe = '1' else 'H'; + + dut: entity work.sp5_hotplug_subsystem + generic map( + -- Keep the PERST oneshots short so the M.2 slots settle in sim time + PERST_US_ONESHOT => 10, + NS_PER_CLK => 8, + NIC2_SLOT_ENABLED => true + ) + port map( + clk => clk, + reset => reset, + sp5_i2c_sda => i2c_bus_sda, + sp5_i2c_sda_o => sda_o, + sp5_i2c_sda_oe => sda_oe, + sp5_i2c_scl => i2c_bus_scl, + sp5_i2c_scl_o => scl_o, + sp5_i2c_scl_oe => scl_oe, + int_n => int_n, + a0_ok => a0_ok, + axi_if => axi_if, + allow_backplane_pcie_clk => '0', + m2a_pedet => '0', + m2a_prsnt_l => '1', + m2a_hsc_en => m2a_hsc_en, + m2a_perst_l => m2a_perst_l, + pcie_clk_buff_m2a_oe_l => pcie_clk_buff_m2a_oe_l, + m2a_pwr_fault_l => '1', + m2b_pedet => '0', + m2b_prsnt_l => '1', + m2b_hsc_en => m2b_hsc_en, + m2b_perst_l => m2b_perst_l, + pcie_clk_buff_m2b_oe_l => pcie_clk_buff_m2b_oe_l, + m2b_pwr_fault_l => '1', + t6_power_en => t6_power_en, + t6_perst_l => t6_perst_l, + t6_faulted => t6_faulted, + t6_prsnt_l => t6_prsnt_l, + nic2_power_en => nic2_power_en, + nic2_perst_l => nic2_perst_l, + nic2_faulted => nic2_faulted, + nic2_prsnt_l => nic2_prsnt_l, + pcie_aux_rsw_perst_l => pcie_aux_rsw_perst_l, + pcie_aux_rsw_prsnt_buff_l => '1', + pcie_aux_rsw_pwrflt_buff_l => '1', + pcie_clk_buff_rsw_oe_l => pcie_clk_buff_rsw_oe_l, + rsw_sp5_pcie_attached_buff_l => '1' + ); + +end th; diff --git a/hdl/projects/cosmo_seq/sp5_hotplug_subsystem/sp5_hotplug_subsystem.vhd b/hdl/projects/cosmo_seq/sp5_hotplug_subsystem/sp5_hotplug_subsystem.vhd index 8c4c4a92..8454baf0 100644 --- a/hdl/projects/cosmo_seq/sp5_hotplug_subsystem/sp5_hotplug_subsystem.vhd +++ b/hdl/projects/cosmo_seq/sp5_hotplug_subsystem/sp5_hotplug_subsystem.vhd @@ -15,7 +15,12 @@ use work.time_pkg.all; -- for calc_ms and calc_us entity sp5_hotplug_subsystem is generic( PERST_US_ONESHOT : integer := 100000; -- 100ms for Tpvperl, this is used in the oneshot - NS_PER_CLK : integer := 8 + NS_PER_CLK : integer := 8; + -- Metro's NIC presents a second PCIe endpoint to the SP5 that needs a + -- hotplug slot of its own. It goes on PCA9506 bank 4, which has always + -- been wired into the expander and referenced by nothing. Cosmo leaves + -- this off; the bank then stays exactly as inert as it is today. + NIC2_SLOT_ENABLED : boolean := false ); port( clk : in std_logic; @@ -50,10 +55,19 @@ entity sp5_hotplug_subsystem is pcie_clk_buff_m2b_oe_l : out std_logic; m2b_pwr_fault_l : in std_logic; - -- T6 things + -- NIC slot on bank 2. The T6 on cosmo, the Versal's channel A on metro. + -- Presence defaults to asserted, which is the tie cosmo has always had. t6_power_en : out std_logic; t6_perst_l : out std_logic; t6_faulted : in std_logic; + t6_prsnt_l : in std_logic := '0'; + + -- Second NIC slot on bank 4, only when NIC2_SLOT_ENABLED. Same shape as + -- the T6 slot: PERST is a copy of the power enable, no oneshot. + nic2_power_en : out std_logic; + nic2_perst_l : out std_logic; + nic2_faulted : in std_logic := '0'; + nic2_prsnt_l : in std_logic := '0'; -- Sidecar things pcie_aux_rsw_perst_l : out std_logic; @@ -175,14 +189,31 @@ begin perst_l => m2b_perst_l ); - -- T6 - t6_power_en <= not io_o(2)(4) when io_oe(0)(4) else '0'; - io(2)(3) <= '1'; -- PEDET for T6 + -- NIC slot, bank 2 (the T6 on cosmo). The power enable only counts once the + -- SP5 has configured this bank's bit 4 as an output; until then the pull + -- default is "off". + t6_power_en <= not io_o(2)(4) when io_oe(2)(4) else '0'; + io(2)(3) <= '1'; -- emils: nothing to report for a soldered-down NIC io(2)(1) <= not t6_faulted; io(2)(2) <= '1'; -- attnsw_l - io(2)(0) <= '0'; -- PRSNT_L for T6 + io(2)(0) <= t6_prsnt_l; t6_perst_l <= t6_power_en; + -- Second NIC slot, bank 4. Only metro populates this; with the generic off + -- the bank reads back its zero initialiser and the outputs stay parked, so + -- an SP5 that pokes bank 4 on cosmo sees what it always has. + nic2_slot: if NIC2_SLOT_ENABLED generate + nic2_power_en <= not io_o(4)(4) when io_oe(4)(4) else '0'; + io(4)(3) <= '1'; -- emils + io(4)(1) <= not nic2_faulted; + io(4)(2) <= '1'; -- attnsw_l + io(4)(0) <= nic2_prsnt_l; + nic2_perst_l <= nic2_power_en; + else generate + nic2_power_en <= '0'; + nic2_perst_l <= '0'; + end generate; + -- Backplane connected switch pcie_aux_power_en <= not io_o(3)(4) when io_oe(3)(4) else '0'; pcie_perst_oneshot: entity work.perst_oneshot diff --git a/hdl/projects/cosmo_seq/xilinx_ip_gen/fmc_pll_ip.tcl b/hdl/projects/cosmo_seq/xilinx_ip_gen/fmc_pll_ip.tcl new file mode 100644 index 00000000..c1a81044 --- /dev/null +++ b/hdl/projects/cosmo_seq/xilinx_ip_gen/fmc_pll_ip.tcl @@ -0,0 +1,58 @@ +# MMCM on the SP's FMC clock, in phase-alignment (deskew) mode: the feedback +# path through a BUFG zeroes out the clock insertion delay so the FMC-domain +# flops launch and capture aligned to the clock at the pin (plus the phase +# shift below). This is what closes the single-cycle NWAIT and data-out paths +# at 10 ns; a plain IBUF+BUFG eats 3.5-6.5 ns of insertion delay across PVT +# and cannot. +# +# VCO choice (M=12, D=1, O=12): VCO = 12 x f_in, so 600 MHz at a 50 MHz input +# and 1200 MHz at 100 MHz -- exactly the -1 speed grade MMCM limits per +# DS189, and the only M that spans both. One bitstream therefore locks at +# either SP CLKDIV setting, which is what lets this bitstream ship before the +# hubris CLKDIV flip. If a Vivado DRC ever rejects the edge-of-range VCO, +# drop the dual-frequency property and coordinate the rollout as a single +# archive bump instead (the hubris archive carries the bitstream, so the pair +# still moves atomically). +# +# Two output phases: +# - clk_fmc (+45 deg = +1.25 ns at 10 ns) clocks the FSM and the output +# flops. Its phase is hold-limited on the NWAIT/data pins (going earlier +# breaks output hold), so it cannot be pushed later for input margin. +# - clk_fmc_capture (+135 deg = +3.75 ns) clocks only the dedicated input +# capture registers. The late phase is what gives the input paths setup +# margin against STA's uncredited clock-network corner spread; input hold +# still has a half period of real SP hold behind it. The FSM consumes the +# captured values a cycle later, which the NWAIT pacing absorbs. +# Sweep either phase in the lab under the fmc_sweep soak to confirm real +# margin exceeds STA margin; the grid is 45/CLKOUT_DIVIDE = 3.75 deg per +# MMCM tap, so +/-5 taps = +/-18.75 deg = ~0.52 ns. Phases are fractions of +# the period, so every hold-side margin only grows at 50 MHz operation. +# +# OVERRIDE_MMCM is required: without it clk_wiz silently recalculates M +# (it picked M=11, whose 550 MHz VCO at a 50 MHz input cannot lock, killing +# the dual-frequency property). +create_ip -name clk_wiz -vendor xilinx.com -library ip -version 6.0 -module_name fmc_pll +set_property -dict [list \ + CONFIG.Component_Name {fmc_pll} \ + CONFIG.PRIM_IN_FREQ {100.000} \ + CONFIG.PRIMARY_PORT {clk_fmc_in} \ + CONFIG.CLK_OUT1_PORT {clk_fmc} \ + CONFIG.CLKOUT1_REQUESTED_OUT_FREQ {100.000} \ + CONFIG.CLKOUT1_REQUESTED_PHASE {45.000} \ + CONFIG.CLKOUT2_USED {true} \ + CONFIG.CLK_OUT2_PORT {clk_fmc_capture} \ + CONFIG.CLKOUT2_REQUESTED_OUT_FREQ {100.000} \ + CONFIG.CLKOUT2_REQUESTED_PHASE {135.000} \ + CONFIG.USE_PHASE_ALIGNMENT {true} \ + CONFIG.JITTER_SEL {Min_O_Jitter} \ + CONFIG.OVERRIDE_MMCM {true} \ + CONFIG.MMCM_DIVCLK_DIVIDE {1} \ + CONFIG.MMCM_CLKFBOUT_MULT_F {12.000} \ + CONFIG.MMCM_CLKIN1_PERIOD {10.000} \ + CONFIG.MMCM_CLKOUT0_DIVIDE_F {12.000} \ + CONFIG.MMCM_CLKOUT0_PHASE {45.000} \ + CONFIG.MMCM_CLKOUT1_DIVIDE {12} \ + CONFIG.MMCM_CLKOUT1_PHASE {135.000} \ + CONFIG.NUM_OUT_CLKS {2} \ +] [get_ips fmc_pll] +synth_ip [get_ips fmc_pll] diff --git a/hdl/projects/cosmo_seq/xilinx_ip_gen/cosmo_pll_ip.tcl b/hdl/projects/cosmo_seq/xilinx_ip_gen/sys_pll_ip.tcl similarity index 87% rename from hdl/projects/cosmo_seq/xilinx_ip_gen/cosmo_pll_ip.tcl rename to hdl/projects/cosmo_seq/xilinx_ip_gen/sys_pll_ip.tcl index 900aa60f..a6434825 100644 --- a/hdl/projects/cosmo_seq/xilinx_ip_gen/cosmo_pll_ip.tcl +++ b/hdl/projects/cosmo_seq/xilinx_ip_gen/sys_pll_ip.tcl @@ -1,5 +1,5 @@ # Generate the PLL (copied from tcl console using the IP generator) -create_ip -name clk_wiz -vendor xilinx.com -library ip -version 6.0 -module_name cosmo_pll +create_ip -name clk_wiz -vendor xilinx.com -library ip -version 6.0 -module_name sys_pll set_property -dict [list \ CONFIG.CLKIN1_JITTER_PS {200.0} \ CONFIG.CLKOUT1_JITTER {154.207} \ @@ -11,7 +11,7 @@ set_property -dict [list \ CONFIG.CLKOUT2_USED {true} \ CONFIG.CLK_OUT1_PORT {clk_125m} \ CONFIG.CLK_OUT2_PORT {clk_200m} \ - CONFIG.Component_Name {cosmo_pll} \ + CONFIG.Component_Name {sys_pll} \ CONFIG.MMCM_CLKFBOUT_MULT_F {20.000} \ CONFIG.MMCM_CLKIN1_PERIOD {20.000} \ CONFIG.MMCM_CLKIN2_PERIOD {10.0} \ @@ -20,5 +20,5 @@ set_property -dict [list \ CONFIG.NUM_OUT_CLKS {2} \ CONFIG.PRIMARY_PORT {clk_50m} \ CONFIG.PRIM_IN_FREQ {50} \ -] [get_ips cosmo_pll] -synth_ip [get_ips cosmo_pll] \ No newline at end of file +] [get_ips sys_pll] +synth_ip [get_ips sys_pll] \ No newline at end of file diff --git a/hdl/projects/grapefruit/BUCK b/hdl/projects/grapefruit/BUCK index ff6f595b..a5872eb6 100644 --- a/hdl/projects/grapefruit/BUCK +++ b/hdl/projects/grapefruit/BUCK @@ -1,4 +1,4 @@ -load("//tools:hdl.bzl", "vhdl_unit", "black_box") +load("//tools:hdl.bzl", "vhdl_unit", "black_box", "vunit_sim") load("//tools:rdl.bzl", "rdl_file") load("//tools:vivado.bzl", "vivado_bitstream") @@ -102,4 +102,14 @@ vivado_bitstream( python_library( name = 'drivers', srcs = glob(["integration/drivers/*.py"]), -) \ No newline at end of file +) + +# Analysis-only: vhdl_unit collects sources but does not run a compiler, so +# without this nothing type-checks grapefruit_top until Vivado synthesis. +vunit_sim( + name = "grapefruit_top_tb", + srcs = glob(["sims/*.vhd"]), + deps = [":grapefruit_top"], + visibility = ["PUBLIC"], + standard = "2019", +) diff --git a/hdl/projects/grapefruit/grapefruit_timing.xdc b/hdl/projects/grapefruit/grapefruit_timing.xdc index 395863df..66ba0401 100644 --- a/hdl/projects/grapefruit/grapefruit_timing.xdc +++ b/hdl/projects/grapefruit/grapefruit_timing.xdc @@ -64,10 +64,14 @@ set_output_delay -clock fmc_virt_clk -min 1.414 [get_ports fmc_sp_to_fpga_wait_l set_output_delay -clock fmc_virt_clk -max 3.851 [get_ports fmc_sp_to_fpga_da[*]] set_output_delay -clock fmc_virt_clk -min 0.830 [get_ports fmc_sp_to_fpga_da[*]] -set_multicycle_path -from [get_pins {stm32h7_fmc_target_inst/data_out*/C}] -to [get_ports {fmc_sp_to_fpga_da[*]}] -setup 2 -set_multicycle_path -from [get_pins {stm32h7_fmc_target_inst/data_out*/C}] -to [get_ports {fmc_sp_to_fpga_da[*]}] -hold 1 -set_multicycle_path -from [get_pins {stm32h7_fmc_target_inst/data_out_en_reg*/C}] -to [get_ports {fmc_sp_to_fpga_da[*]}] -setup 2 -set_multicycle_path -from [get_pins {stm32h7_fmc_target_inst/data_out_en_reg*/C}] -to [get_ports {fmc_sp_to_fpga_da[*]}] -hold 1 +# The streaming FSM presents read beats on consecutive cycles, so the +# word0->word1 transition is a true single-cycle path with no multicycle +# exception; IOB packing of the per-pin output, tristate, and nwait flops is +# what closes single-cycle at 15 ns. See cosmo_timing.xdc for the derivation; +# extra_beat_setup on the target is the escape hatch if this fails to close. +set_property IOB TRUE [get_cells -hier -filter {NAME =~ *stm32h7_fmc_target*/data_out_reg[*]}] +set_property IOB TRUE [get_cells -hier -filter {NAME =~ *stm32h7_fmc_target*/data_out_hiz_int_reg[*]}] +set_property IOB TRUE [get_cells -hier -filter {NAME =~ *stm32h7_fmc_target*/nwait_reg}] # ####################### # SPI NOR flash interface (Winbond W25Q01JV) diff --git a/hdl/projects/grapefruit/grapefruit_top.vhd b/hdl/projects/grapefruit/grapefruit_top.vhd index fd6dd60d..b6801f18 100644 --- a/hdl/projects/grapefruit/grapefruit_top.vhd +++ b/hdl/projects/grapefruit/grapefruit_top.vhd @@ -197,7 +197,7 @@ architecture rtl of grapefruit_top is signal reset_200m : std_logic; signal reset_fmc: std_logic; signal fmc_internal_data_out : std_logic_vector(15 downto 0); - signal fmc_data_out_enable: std_logic; + signal fmc_data_out_hiz: std_logic_vector(15 downto 0); signal fmc_axi_if : axil26x32_pkg.axil_t; @@ -214,19 +214,13 @@ architecture rtl of grapefruit_top is signal fabric_responders : axil32x32_pkg.axil_array_t(config_array'range); signal responders_8b : axil8x32_pkg.axil_array_t(config_array'range); signal responders_15b : axil15x32_pkg.axil_array_t(config_array'range); - -- Hashing engine <-> spi_nor_top, the same shape as the eSPI pair below. - -- Reset from reset_125m only: the engine resynchronises its own response - -- channel by draining it, so nothing else should be clearing these. - signal hash_cmd_fifo_wdata : std_logic_vector(31 downto 0); - signal hash_cmd_fifo_write : std_logic; + -- Hashing engine <-> spi_nor_top's hash client port. The engine owns the + -- FIFOs on this path; these are their spi_nor-side ends, one flash. signal hash_cmd_fifo_rdata : std_logic_vector(31 downto 0); - signal hash_cmd_fifo_rdack : std_logic; - signal hash_cmd_fifo_rempty : std_logic; + signal hash_cmd_fifo_rdack : std_logic_vector(0 downto 0); + signal hash_cmd_fifo_rempty : std_logic_vector(0 downto 0); signal hash_data_fifo_wdata : std_logic_vector(7 downto 0); - signal hash_data_fifo_write : std_logic; - signal hash_rsp_fifo_rdata : std_logic_vector(7 downto 0); - signal hash_rsp_fifo_rdack : std_logic; - signal hash_rsp_fifo_rempty : std_logic; + signal hash_data_fifo_write : std_logic_vector(0 downto 0); signal espi_cmd_fifo_rdata : std_logic_vector(31 downto 0); signal espi_cmd_fifo_rdack : std_logic; signal espi_cmd_fifo_rempty : std_logic; @@ -307,17 +301,22 @@ begin port map( chip_reset => reset_fmc, fmc_clk => fmc_sp_to_fpga_clk, + -- no MMCM here (F17 is not clock-capable); the capture stage still + -- exists, it just runs on the same clock + fmc_capture_clk => fmc_sp_to_fpga_clk, a(24 downto 20) => "00000", a(19 downto 16) => fmc_sp_to_fpga_a, addr_data_in => fmc_sp_to_fpga_da, data_out => fmc_internal_data_out, - data_out_en => fmc_data_out_enable, + data_out_hiz => fmc_data_out_hiz, ne(3 downto 1) => "111", ne(0) => fmc_sp_to_fpga_cs1_l, noe => fmc_sp_to_fpga_oe_l, nwe => fmc_sp_to_fpga_we_l, nl => fmc_sp_to_fpga_adv_l, nwait => fmc_sp_to_fpga_wait_l, + timeout_count => open, + contention_count => open, aclk => clk_125m, aresetn => not reset_125m, axi_if => fmc_axi_if @@ -337,7 +336,11 @@ begin ); -- tristate control for the FMC data bus - fmc_sp_to_fpga_da <= fmc_internal_data_out when fmc_data_out_enable = '1' else (others => 'Z'); + -- per-bit tristate, hiz already in OBUFT T polarity so each pin's T + -- flop packs into its IOB with no inverter in between + fmc_da_tris: for i in fmc_sp_to_fpga_da'range generate + fmc_sp_to_fpga_da(i) <= 'Z' when fmc_data_out_hiz(i) = '1' else fmc_internal_data_out(i); + end generate; resize_axil(fabric_responders(0), responders_8b(0)); info_regs: entity work.info generic map( @@ -374,65 +377,24 @@ begin espi_data_fifo_wdata => espi_data_fifo_wdata, espi_data_fifo_write => espi_data_fifo_write, hash_cmd_fifo_rdata => hash_cmd_fifo_rdata, - hash_cmd_fifo_rdack => hash_cmd_fifo_rdack, - hash_cmd_fifo_rempty => hash_cmd_fifo_rempty, + hash_cmd_fifo_rdack => hash_cmd_fifo_rdack(0), + hash_cmd_fifo_rempty => hash_cmd_fifo_rempty(0), hash_data_fifo_wdata => hash_data_fifo_wdata, - hash_data_fifo_write => hash_data_fifo_write + hash_data_fifo_write => hash_data_fifo_write(0) ); - -- Hashing engine -> SPI NOR - hash_spinor_cmd_fifo: entity work.dcfifo_xpm - generic map( - fifo_write_depth => 256, - data_width => 32, - showahead_mode => true - ) - port map( - wclk => clk_125m, - reset => reset_125m, - write_en => hash_cmd_fifo_write, - wdata => hash_cmd_fifo_wdata, - wfull => open, - wusedwds => open, - rclk => clk_125m, - rdata => hash_cmd_fifo_rdata, - rdreq => hash_cmd_fifo_rdack, - rempty => hash_cmd_fifo_rempty, - rusedwds => open - ); - -- SPI NOR -> hashing engine - hash_spinor_data_fifo: entity work.dcfifo_xpm - generic map( - fifo_write_depth => 256, - data_width => 8, - showahead_mode => true - ) - port map( - wclk => clk_125m, - reset => reset_125m, - write_en => hash_data_fifo_write, - wdata => hash_data_fifo_wdata, - wfull => open, - wusedwds => open, - rclk => clk_125m, - rdata => hash_rsp_fifo_rdata, - rdreq => hash_rsp_fifo_rdack, - rempty => hash_rsp_fifo_rempty, - rusedwds => open - ); - resize_axil(fabric_responders(4), responders_8b(4)); hash_engine_inst: entity work.hash_engine_top port map( clk => clk_125m, reset => reset_125m, axi_if => responders_8b(4), - cmd_fifo_wdata => hash_cmd_fifo_wdata, - cmd_fifo_write => hash_cmd_fifo_write, - rsp_fifo_rdata => hash_rsp_fifo_rdata, - rsp_fifo_rdack => hash_rsp_fifo_rdack, - rsp_fifo_rempty => hash_rsp_fifo_rempty + flash_cmd_rdata => hash_cmd_fifo_rdata, + flash_cmd_rdack => hash_cmd_fifo_rdack, + flash_cmd_rempty => hash_cmd_fifo_rempty, + flash_rsp_wdata => hash_data_fifo_wdata, + flash_rsp_write => hash_data_fifo_write ); -- eSPI block -> SPI NOR diff --git a/hdl/projects/grapefruit/sims/grapefruit_top_tb.vhd b/hdl/projects/grapefruit/sims/grapefruit_top_tb.vhd new file mode 100644 index 00000000..78014b9d --- /dev/null +++ b/hdl/projects/grapefruit/sims/grapefruit_top_tb.vhd @@ -0,0 +1,33 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +library ieee; +use ieee.std_logic_1164.all; + +library vunit_lib; + context vunit_lib.vunit_context; + +-- Analysis-only smoke test: it does not drive the top level, its job is to +-- make a simulator compile the whole design tree, which the synthesis-only +-- build rules do not do on their own. +entity grapefruit_top_tb is + generic ( + runner_cfg : string + ); +end entity; + +architecture tb of grapefruit_top_tb is +begin + bench: process + begin + test_runner_setup(runner, runner_cfg); + while test_suite loop + if run("analyses") then + info("grapefruit_top and its dependencies analysed"); + end if; + end loop; + test_runner_cleanup(runner); + wait; + end process; +end tb; diff --git a/hdl/projects/metro_seq/BUCK b/hdl/projects/metro_seq/BUCK new file mode 100644 index 00000000..b93e1d2c --- /dev/null +++ b/hdl/projects/metro_seq/BUCK @@ -0,0 +1,74 @@ +load("//tools:hdl.bzl", "vhdl_unit", "vunit_sim") +load("//tools:rdl.bzl", "rdl_file") +load("//tools:vivado.bzl", "vivado_bitstream") + +rdl_file( + name = "metro_seq_top_rdl", + src = "metro_seq_top.rdl", + deps = [ + "//hdl/projects/cosmo_seq/debug_module:debug_regs_rdl", + "//hdl/projects/cosmo_seq/sequencer:sequencer_regs_rdl", + "//hdl/projects/metro_seq/versal_subsystem:versal_flash_regs_rdl", + "//hdl/projects/cosmo_seq/dimms_subsystem:dimm_regs_rdl", + "//hdl/projects/cosmo_seq/sp_i2c_subsystem:sp_i2c_regs_rdl", + "//hdl/ip/vhd/info:info_regs_rdl", + "//hdl/ip/vhd/spi_nor_controller:spi_nor_regs_rdl", + "//hdl/ip/vhd/hash_engine:hash_engine_regs_rdl", + "//hdl/ip/vhd/espi:espi_regs_rdl", + "//hdl/ip/vhd/i2c/io_expanders/PCA9506ish:pca9506_regs_rdl", + ], + outputs = [ + "metro_seq_top.adoc", + "metro_seq_top.html", + "metro_seq_top.json", + ], +) + +vhdl_unit( + name = "metro_seq_top", + srcs = ["metro_seq_top.vhd"], + deps = [ + ":metro_seq_top_rdl", + # Clocks, resets, info block and LED are the same on both boards, PLL + # IP included; see the export_file targets next to it. + "//hdl/projects/cosmo_seq:board_support", + "//hdl/ip/vhd/axi_blocks:axilite_common_pkg", + "//hdl/ip/vhd/axi_blocks:axil_interconnect", + "//hdl/ip/vhd/common:tristate_if_pkg", + "//hdl/ip/vhd/fmc_if:stm32h7_fmc_target", + "//hdl/ip/vhd/hash_engine:hash_engine_top", + # The sequencer is shared: one entity, NIC_KIND => NIC_VERSAL here. + "//hdl/projects/cosmo_seq/sequencer:sequencer", + "//hdl/projects/metro_seq/versal_subsystem:versal_flash_subsystem", + "//hdl/projects/cosmo_seq/debug_module:debug_module_top", + # Shared verbatim with cosmo_seq -- identical hardware on both boards. + "//hdl/projects/cosmo_seq/sp5_espi_flash_subsystem:sp5_espi_flash_subsystem", + "//hdl/projects/cosmo_seq/sp5_uart_subsystem:sp5_uart_subsystem", + "//hdl/projects/cosmo_seq/sp_i2c_subsystem:sp_i2c_subsystem", + "//hdl/projects/cosmo_seq/sp5_hotplug_subsystem:sp5_hotplug_subsystem", + "//hdl/projects/cosmo_seq/dimms_subsystem:dimms_subsystem_top", + ], + standard = "2019", +) + +vivado_bitstream( + name = "metro_seq", + top_entity_name = "metro_seq_top", + top = ":metro_seq_top", + part = "xc7s100fgga484-1", + constraints = glob(["*.xdc"]), + pre_synth_tcl_files = [ + "//hdl/projects/cosmo_seq:sys_pll_ip_tcl", + "//hdl/projects/cosmo_seq:fmc_pll_ip_tcl", + ], +) + +# Analysis-only: vhdl_unit collects sources but does not run a compiler, so +# without this nothing type-checks metro_seq_top until Vivado synthesis. +vunit_sim( + name = "metro_seq_top_tb", + srcs = glob(["sims/*.vhd"]), + deps = [":metro_seq_top"], + visibility = ["PUBLIC"], + standard = "2019", +) diff --git a/hdl/projects/metro_seq/metro_seq_pins.xdc b/hdl/projects/metro_seq/metro_seq_pins.xdc new file mode 100644 index 00000000..3e628929 --- /dev/null +++ b/hdl/projects/metro_seq/metro_seq_pins.xdc @@ -0,0 +1,341 @@ +# Pin assignments for the Metro sequencer FPGA (U27, XC7S100-1FGGA484I). +# +# Derived from schematic 913-0000028 rev 1 sheets 57-59 (dated 6/17/2026) and +# reviewed against the rendered drawing. IOSTANDARD follows the bank supply: +# banks 13/14/15/16/36 are on V3P3_SYS_A2, banks 34/35 on V1P8_SYS_A2. +# +# Dedicated configuration and JTAG pins (CCLK_0, INIT_B_0, PROGRAM_B_0, DONE_0, +# TCK/TDI/TDO/TMS) are wired on the schematic but are not fabric I/O and so have +# no port here. + +set_property -dict { PACKAGE_PIN M8 IOSTANDARD LVCMOS18 } [get_ports { clk_50mhz_fpga1_2 }]; +set_property -dict { PACKAGE_PIN P15 IOSTANDARD LVCMOS33 } [get_ports { clk_50mhz_fpga1_1 }]; +set_property -dict { PACKAGE_PIN T16 IOSTANDARD LVCMOS33 } [get_ports { clk_buff_m2_rsw_to_fpga1_los_l }]; +set_property -dict { PACKAGE_PIN R20 IOSTANDARD LVCMOS33 } [get_ports { clk_buff_nic_to_fpga1_los_l }]; +set_property -dict { PACKAGE_PIN AA13 IOSTANDARD LVCMOS33 } [get_ports { dimm_a_pg }]; +set_property -dict { PACKAGE_PIN AB13 IOSTANDARD LVCMOS33 } [get_ports { dimm_b_pg }]; +set_property -dict { PACKAGE_PIN W11 IOSTANDARD LVCMOS33 } [get_ports { dimm_c_pg }]; +set_property -dict { PACKAGE_PIN Y11 IOSTANDARD LVCMOS33 } [get_ports { dimm_d_pg }]; +set_property -dict { PACKAGE_PIN AB11 IOSTANDARD LVCMOS33 } [get_ports { dimm_e_pg }]; +set_property -dict { PACKAGE_PIN AB12 IOSTANDARD LVCMOS33 } [get_ports { dimm_f_pg }]; +set_property -dict { PACKAGE_PIN AA17 IOSTANDARD LVCMOS33 } [get_ports { dimm_g_pg }]; +set_property -dict { PACKAGE_PIN AB18 IOSTANDARD LVCMOS33 } [get_ports { dimm_h_pg }]; +set_property -dict { PACKAGE_PIN AA18 IOSTANDARD LVCMOS33 } [get_ports { dimm_i_pg }]; +set_property -dict { PACKAGE_PIN Y18 IOSTANDARD LVCMOS33 } [get_ports { dimm_j_pg }]; +set_property -dict { PACKAGE_PIN V16 IOSTANDARD LVCMOS33 } [get_ports { dimm_k_pg }]; +set_property -dict { PACKAGE_PIN U16 IOSTANDARD LVCMOS33 } [get_ports { dimm_l_pg }]; +# eSPI0 DAT1 and ALERT are swapped relative to the schematic names, the same +# way cosmo_seq swaps them. Both boards' schematics name W3 ALERT_L and Y3 +# DAT1, and both carry the same note: the ALERT ball doubles as DAT1 when the +# SP5 boots over SAFS eSPI, which is what this FPGA serves. cosmo_seq_pins.xdc +# resolves it the same way and has been through bring-up; do not "correct" +# these back to the schematic names without re-reading that note. +set_property -dict { PACKAGE_PIN W3 IOSTANDARD LVCMOS18 SLEW FAST } [get_ports { espi0_sp5_to_fpga1_dat[1] }]; +set_property -dict { PACKAGE_PIN U3 IOSTANDARD LVCMOS18 } [get_ports { espi0_sp5_to_fpga1_clk }]; +set_property -dict { PACKAGE_PIN T3 IOSTANDARD LVCMOS18 } [get_ports { espi0_sp5_to_fpga1_cs_l }]; +set_property -dict { PACKAGE_PIN U4 IOSTANDARD LVCMOS18 SLEW FAST } [get_ports { espi0_sp5_to_fpga1_dat[0] }]; +set_property -dict { PACKAGE_PIN Y3 IOSTANDARD LVCMOS18 } [get_ports { espi0_fpga1_to_sp5_alert_l }]; +set_property -dict { PACKAGE_PIN U1 IOSTANDARD LVCMOS18 SLEW FAST } [get_ports { espi0_sp5_to_fpga1_dat[2] }]; +set_property -dict { PACKAGE_PIN U2 IOSTANDARD LVCMOS18 SLEW FAST } [get_ports { espi0_sp5_to_fpga1_dat[3] }]; +set_property -dict { PACKAGE_PIN N3 IOSTANDARD LVCMOS18 } [get_ports { espi1_fpga1_to_sp5_alert_l }]; +set_property -dict { PACKAGE_PIN M1 IOSTANDARD LVCMOS18 } [get_ports { espi1_sp5_to_fpga1_clk }]; +set_property -dict { PACKAGE_PIN N4 IOSTANDARD LVCMOS18 } [get_ports { espi1_sp5_to_fpga1_clk_2 }]; +set_property -dict { PACKAGE_PIN M2 IOSTANDARD LVCMOS18 } [get_ports { espi1_sp5_to_fpga1_cs_l }]; +set_property -dict { PACKAGE_PIN M3 IOSTANDARD LVCMOS18 SLEW FAST } [get_ports { espi1_sp5_to_fpga1_dat[0] }]; +set_property -dict { PACKAGE_PIN M4 IOSTANDARD LVCMOS18 SLEW FAST } [get_ports { espi1_sp5_to_fpga1_dat[1] }]; +set_property -dict { PACKAGE_PIN L1 IOSTANDARD LVCMOS18 SLEW FAST } [get_ports { espi1_sp5_to_fpga1_dat[2] }]; +set_property -dict { PACKAGE_PIN K1 IOSTANDARD LVCMOS18 SLEW FAST } [get_ports { espi1_sp5_to_fpga1_dat[3] }]; +set_property -dict { PACKAGE_PIN Y1 IOSTANDARD LVCMOS18 } [get_ports { espi_sp5_to_fpga1_reset_l }]; +set_property -dict { PACKAGE_PIN D4 IOSTANDARD LVCMOS33 } [get_ports { fan_central_hsc_to_fpga1_pg }]; +set_property -dict { PACKAGE_PIN F1 IOSTANDARD LVCMOS33 } [get_ports { fan_east_hsc_to_fpga1_pg }]; +set_property -dict { PACKAGE_PIN V20 IOSTANDARD LVCMOS33 } [get_ports { fan_to_fpga1_fan_fail }]; +set_property -dict { PACKAGE_PIN E4 IOSTANDARD LVCMOS33 } [get_ports { fan_west_hsc_to_fpga1_pg }]; +set_property -dict { PACKAGE_PIN H20 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_a[16] }]; +set_property -dict { PACKAGE_PIN H21 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_a[17] }]; +set_property -dict { PACKAGE_PIN J22 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_a[18] }]; +set_property -dict { PACKAGE_PIN K22 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_a[19] }]; +set_property -dict { PACKAGE_PIN M16 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_a[20] }]; +set_property -dict { PACKAGE_PIN L20 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_a[21] }]; +set_property -dict { PACKAGE_PIN L21 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_a[22] }]; +set_property -dict { PACKAGE_PIN L22 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_a[23] }]; +set_property -dict { PACKAGE_PIN J17 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_adv_l }]; +set_property -dict { PACKAGE_PIN H17 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_bl_l[0] }]; +set_property -dict { PACKAGE_PIN G17 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_bl_l[1] }]; +set_property -dict { PACKAGE_PIN F19 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_clk }]; +set_property -dict { PACKAGE_PIN H16 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_cs_l }]; +set_property -dict { PACKAGE_PIN L18 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_da[0] }]; +set_property -dict { PACKAGE_PIN L19 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_da[1] }]; +set_property -dict { PACKAGE_PIN M15 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_da[2] }]; +set_property -dict { PACKAGE_PIN L15 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_da[3] }]; +set_property -dict { PACKAGE_PIN G20 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_da[4] }]; +set_property -dict { PACKAGE_PIN H19 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_da[5] }]; +set_property -dict { PACKAGE_PIN M17 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_da[6] }]; +set_property -dict { PACKAGE_PIN M18 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_da[7] }]; +set_property -dict { PACKAGE_PIN L16 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_da[8] }]; +set_property -dict { PACKAGE_PIN K17 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_da[9] }]; +set_property -dict { PACKAGE_PIN G21 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_da[10] }]; +set_property -dict { PACKAGE_PIN G22 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_da[11] }]; +set_property -dict { PACKAGE_PIN J19 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_da[12] }]; +set_property -dict { PACKAGE_PIN J20 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_da[13] }]; +set_property -dict { PACKAGE_PIN K18 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_da[14] }]; +set_property -dict { PACKAGE_PIN K19 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_da[15] }]; +set_property -dict { PACKAGE_PIN H18 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_oe_l }]; +set_property -dict { PACKAGE_PIN K16 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_wait_l }]; +set_property -dict { PACKAGE_PIN G16 IOSTANDARD LVCMOS33 } [get_ports { fmc_sp_to_fpga1_we_l }]; +set_property -dict { PACKAGE_PIN AA3 IOSTANDARD LVCMOS18 } [get_ports { fpga1_debug_uart_buf_oe_en_l }]; +set_property -dict { PACKAGE_PIN Y8 IOSTANDARD LVCMOS18 } [get_ports { fpga1_espi0_cs_l_buff_oe_en_l }]; +set_property -dict { PACKAGE_PIN W2 IOSTANDARD LVCMOS18 } [get_ports { fpga1_spare_v1p8[0] }]; +set_property -dict { PACKAGE_PIN U5 IOSTANDARD LVCMOS18 } [get_ports { fpga1_spare_v1p8[1] }]; +set_property -dict { PACKAGE_PIN W1 IOSTANDARD LVCMOS18 } [get_ports { fpga1_spare_v1p8[2] }]; +set_property -dict { PACKAGE_PIN V7 IOSTANDARD LVCMOS18 } [get_ports { fpga1_spare_v1p8[3] }]; +set_property -dict { PACKAGE_PIN V1 IOSTANDARD LVCMOS18 } [get_ports { fpga1_spare_v1p8[4] }]; +set_property -dict { PACKAGE_PIN V6 IOSTANDARD LVCMOS18 } [get_ports { fpga1_spare_v1p8[5] }]; +set_property -dict { PACKAGE_PIN U8 IOSTANDARD LVCMOS18 } [get_ports { fpga1_spare_v1p8[6] }]; +set_property -dict { PACKAGE_PIN T8 IOSTANDARD LVCMOS18 } [get_ports { fpga1_spare_v1p8[7] }]; +set_property -dict { PACKAGE_PIN C22 IOSTANDARD LVCMOS33 } [get_ports { fpga1_spare_v3p3_0 }]; +set_property -dict { PACKAGE_PIN K15 IOSTANDARD LVCMOS33 } [get_ports { fpga1_spare_v3p3_1 }]; +set_property -dict { PACKAGE_PIN D21 IOSTANDARD LVCMOS33 } [get_ports { fpga1_spare_v3p3_2 }]; +set_property -dict { PACKAGE_PIN J15 IOSTANDARD LVCMOS33 } [get_ports { fpga1_spare_v3p3_3 }]; +set_property -dict { PACKAGE_PIN C20 IOSTANDARD LVCMOS33 } [get_ports { fpga1_spare_v3p3_4 }]; +set_property -dict { PACKAGE_PIN B21 IOSTANDARD LVCMOS33 } [get_ports { fpga1_spare_v3p3_5 }]; +set_property -dict { PACKAGE_PIN D20 IOSTANDARD LVCMOS33 } [get_ports { fpga1_spare_v3p3_6 }]; +set_property -dict { PACKAGE_PIN B22 IOSTANDARD LVCMOS33 } [get_ports { fpga1_spare_v3p3_7 }]; +set_property -dict { PACKAGE_PIN U19 IOSTANDARD LVCMOS33 } [get_ports { fpga1_status_led }]; +set_property -dict { PACKAGE_PIN AA11 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_bp_buff_output_en_l }]; +set_property -dict { PACKAGE_PIN G2 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_fan_central_hsc_disable }]; +set_property -dict { PACKAGE_PIN G1 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_fan_east_hsc_disable }]; +set_property -dict { PACKAGE_PIN E1 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_fan_west_hsc_disable }]; +set_property -dict { PACKAGE_PIN B6 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_fpga2_io[0] }]; +set_property -dict { PACKAGE_PIN A3 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_fpga2_io[1] }]; +set_property -dict { PACKAGE_PIN A4 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_fpga2_io[2] }]; +set_property -dict { PACKAGE_PIN A5 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_fpga2_io[3] }]; +set_property -dict { PACKAGE_PIN B3 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_fpga2_io[4] }]; +set_property -dict { PACKAGE_PIN A2 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_fpga2_io[5] }]; +set_property -dict { PACKAGE_PIN E19 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_i2c_mux1_sel[0] }]; +set_property -dict { PACKAGE_PIN A20 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_i2c_mux1_sel[1] }]; +set_property -dict { PACKAGE_PIN G15 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_i2c_mux2_sel[0] }]; +set_property -dict { PACKAGE_PIN D19 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_i2c_mux2_sel[1] }]; +set_property -dict { PACKAGE_PIN F4 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_i2c_mux3_sel[0] }]; +set_property -dict { PACKAGE_PIN D1 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_i2c_mux3_sel[1] }]; +set_property -dict { PACKAGE_PIN C3 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_ign_trgt_fpga_creset }]; +set_property -dict { PACKAGE_PIN C4 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_jtag_mux_sel }]; +set_property -dict { PACKAGE_PIN AB21 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_m2_apml_xltr_en }]; +set_property -dict { PACKAGE_PIN W18 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_m2a_hsc_en }]; +set_property -dict { PACKAGE_PIN N19 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_m2a_perst_l }]; +set_property -dict { PACKAGE_PIN Y19 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_m2b_hsc_en }]; +set_property -dict { PACKAGE_PIN P21 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_m2b_perst_l }]; +set_property -dict { PACKAGE_PIN T22 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_nic_hsc_en }]; +set_property -dict { PACKAGE_PIN AB20 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_pcie_clk_buff_m2a_oe_l }]; +set_property -dict { PACKAGE_PIN AB19 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_pcie_clk_buff_m2b_oe_l }]; +set_property -dict { PACKAGE_PIN V18 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_pcie_clk_buff_nic_cha_oe_l }]; +set_property -dict { PACKAGE_PIN Y22 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_pcie_clk_buff_nic_chb_oe_l }]; +set_property -dict { PACKAGE_PIN V19 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_pcie_clk_buff_rsw_oe_l }]; +set_property -dict { PACKAGE_PIN Y12 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_sp5_apml_xltr_en }]; +set_property -dict { PACKAGE_PIN T2 IOSTANDARD LVCMOS18 } [get_ports { fpga1_to_sp5_espi_kbrst_l }]; +set_property -dict { PACKAGE_PIN AB10 IOSTANDARD LVCMOS18 } [get_ports { fpga1_to_sp5_pwr_btn_l }]; +set_property -dict { PACKAGE_PIN L7 IOSTANDARD LVCMOS18 } [get_ports { fpga1_to_sp5_pwrgd }]; +set_property -dict { PACKAGE_PIN AA8 IOSTANDARD LVCMOS18 } [get_ports { fpga1_to_sp5_romtype0 }]; +set_property -dict { PACKAGE_PIN AB7 IOSTANDARD LVCMOS18 } [get_ports { fpga1_to_sp5_rsmrst_l }]; +set_property -dict { PACKAGE_PIN AA7 IOSTANDARD LVCMOS18 } [get_ports { fpga1_to_sp5_sys_reset_l }]; +set_property -dict { PACKAGE_PIN E11 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_sp_int_l }]; +set_property -dict { PACKAGE_PIN D11 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_sp_irq_l[1] }]; +set_property -dict { PACKAGE_PIN F13 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_sp_irq_l[2] }]; +set_property -dict { PACKAGE_PIN F14 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_sp_irq_l[3] }]; +set_property -dict { PACKAGE_PIN F12 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_sp_irq_l[4] }]; +set_property -dict { PACKAGE_PIN A19 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_sp_irq_l[5] }]; +set_property -dict { PACKAGE_PIN E17 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_sp_irq_l[6] }]; +set_property -dict { PACKAGE_PIN F10 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_sp_misc_a }]; +set_property -dict { PACKAGE_PIN G11 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_sp_misc_b }]; +set_property -dict { PACKAGE_PIN F11 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_sp_misc_c }]; +set_property -dict { PACKAGE_PIN G10 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_sp_misc_d }]; +set_property -dict { PACKAGE_PIN J21 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_sp_mux_ign_mux_sel }]; +set_property -dict { PACKAGE_PIN C2 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_v12_ddr5_abcdef_hsc_en }]; +set_property -dict { PACKAGE_PIN C1 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_v12_ddr5_ghijkl_hsc_en }]; +set_property -dict { PACKAGE_PIN R5 IOSTANDARD LVCMOS18 } [get_ports { fpga1_to_vercel_flash_qspi_mux_en_l }]; +set_property -dict { PACKAGE_PIN R4 IOSTANDARD LVCMOS18 } [get_ports { fpga1_to_vercel_flash_qspi_mux_sel }]; +set_property -dict { PACKAGE_PIN F18 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_versal_erro_done_buff_en }]; +set_property -dict { PACKAGE_PIN D2 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_versal_mode[0] }]; +set_property -dict { PACKAGE_PIN E3 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_versal_mode[1] }]; +set_property -dict { PACKAGE_PIN F6 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_versal_mode[2] }]; +set_property -dict { PACKAGE_PIN B7 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_versal_mode[3] }]; +set_property -dict { PACKAGE_PIN J16 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_versal_mode_buffer_en_l }]; +set_property -dict { PACKAGE_PIN E12 IOSTANDARD LVCMOS33 } [get_ports { fpga1_to_versal_por_b }]; +set_property -dict { PACKAGE_PIN AB2 IOSTANDARD LVCMOS18 } [get_ports { fpga1_uart0_buff_oe_en_l }]; +set_property -dict { PACKAGE_PIN AB3 IOSTANDARD LVCMOS18 } [get_ports { fpga1_uart1_buff_oe_en_l }]; +set_property -dict { PACKAGE_PIN D5 IOSTANDARD LVCMOS33 } [get_ports { fpga1_version_id[0] }]; +set_property -dict { PACKAGE_PIN G8 IOSTANDARD LVCMOS33 } [get_ports { fpga1_version_id[1] }]; +set_property -dict { PACKAGE_PIN Y4 IOSTANDARD LVCMOS18 } [get_ports { sp5_to_fpga1_genint_l }]; +set_property -dict { PACKAGE_PIN H2 IOSTANDARD LVCMOS18 } [get_ports { hdt_conn_to_mux_testen }]; +set_property -dict { PACKAGE_PIN J1 IOSTANDARD LVCMOS18 } [get_ports { hdt_fpga1_to_mux_dat }]; +set_property -dict { PACKAGE_PIN J3 IOSTANDARD LVCMOS18 } [get_ports { hdt_fpga1_to_mux_dbreq_l }]; +set_property -dict { PACKAGE_PIN K3 IOSTANDARD LVCMOS18 } [get_ports { hdt_fpga1_to_mux_en_l }]; +set_property -dict { PACKAGE_PIN H4 IOSTANDARD LVCMOS18 } [get_ports { hdt_fpga1_to_mux_sel }]; +set_property -dict { PACKAGE_PIN J2 IOSTANDARD LVCMOS18 } [get_ports { hdt_fpga1_to_mux_tck }]; +set_property -dict { PACKAGE_PIN K4 IOSTANDARD LVCMOS18 } [get_ports { hdt_fpga1_to_mux_tms }]; +set_property -dict { PACKAGE_PIN K2 IOSTANDARD LVCMOS18 } [get_ports { hdt_fpga1_to_mux_trst_l }]; +set_property -dict { PACKAGE_PIN H5 IOSTANDARD LVCMOS18 } [get_ports { hdt_fpga1_to_mux_xtrig5_l }]; +set_property -dict { PACKAGE_PIN H7 IOSTANDARD LVCMOS18 } [get_ports { hdt_fpga1_to_mux_xtrig6_l }]; +set_property -dict { PACKAGE_PIN H6 IOSTANDARD LVCMOS18 } [get_ports { hdt_fpga1_to_mux_xtrig7_l }]; +set_property -dict { PACKAGE_PIN H3 IOSTANDARD LVCMOS18 } [get_ports { hdt_mux_to_fpga1_dat }]; +set_property -dict { PACKAGE_PIN AA15 IOSTANDARD LVCMOS33 } [get_ports { i2c_sp5_sec_v3p3_scl }]; +set_property -dict { PACKAGE_PIN AA16 IOSTANDARD LVCMOS33 } [get_ports { i2c_sp5_sec_v3p3_sda }]; +set_property -dict { PACKAGE_PIN N6 IOSTANDARD LVCMOS18 } [get_ports { i2c_sp5_to_fpgax_hp_scl }]; +set_property -dict { PACKAGE_PIN P7 IOSTANDARD LVCMOS18 } [get_ports { i2c_sp5_to_fpgax_hp_sda }]; +set_property -dict { PACKAGE_PIN M21 IOSTANDARD LVCMOS33 } [get_ports { i2c_sp_to_fpga1_scl }]; +set_property -dict { PACKAGE_PIN N21 IOSTANDARD LVCMOS33 } [get_ports { i2c_sp_to_fpga1_sda }]; +set_property -dict { PACKAGE_PIN P1 IOSTANDARD LVCMOS18 } [get_ports { i2c_sp_to_nic_sysmon_alert_l }]; +set_property -dict { PACKAGE_PIN AB4 IOSTANDARD LVCMOS18 } [get_ports { i3c_fpga1_to_dimm_abcdef_scl }]; +set_property -dict { PACKAGE_PIN AB5 IOSTANDARD LVCMOS18 } [get_ports { i3c_fpga1_to_dimm_abcdef_sda }]; +set_property -dict { PACKAGE_PIN Y5 IOSTANDARD LVCMOS18 } [get_ports { i3c_fpga1_to_dimm_ghijkl_scl }]; +set_property -dict { PACKAGE_PIN Y6 IOSTANDARD LVCMOS18 } [get_ports { i3c_fpga1_to_dimm_ghijkl_sda }]; +set_property -dict { PACKAGE_PIN C19 IOSTANDARD LVCMOS33 } [get_ports { i3c_fpga1_to_dimm_oe_l }]; +set_property -dict { PACKAGE_PIN AA1 IOSTANDARD LVCMOS18 } [get_ports { i3c_sp5_to_fpga1_abcdef_scl }]; +set_property -dict { PACKAGE_PIN AA2 IOSTANDARD LVCMOS18 } [get_ports { i3c_sp5_to_fpga1_abcdef_sda }]; +set_property -dict { PACKAGE_PIN AB6 IOSTANDARD LVCMOS18 } [get_ports { i3c_sp5_to_fpga1_ghijkl_scl }]; +set_property -dict { PACKAGE_PIN AA6 IOSTANDARD LVCMOS18 } [get_ports { i3c_sp5_to_fpga1_ghijkl_sda }]; +set_property -dict { PACKAGE_PIN C18 IOSTANDARD LVCMOS33 } [get_ports { i3c_sp5_to_fpga1_oe_l }]; +set_property -dict { PACKAGE_PIN E5 IOSTANDARD LVCMOS33 } [get_ports { m2a_hsc_to_fpga1_fault_l }]; +set_property -dict { PACKAGE_PIN V22 IOSTANDARD LVCMOS33 } [get_ports { m2a_to_fpga1_pedet }]; +set_property -dict { PACKAGE_PIN U22 IOSTANDARD LVCMOS33 } [get_ports { m2a_to_fpga1_prsnt_l }]; +set_property -dict { PACKAGE_PIN G7 IOSTANDARD LVCMOS33 } [get_ports { m2b_hsc_to_fpga1_fault_l }]; +set_property -dict { PACKAGE_PIN Y21 IOSTANDARD LVCMOS33 } [get_ports { m2b_to_fpga1_pedet }]; +set_property -dict { PACKAGE_PIN W21 IOSTANDARD LVCMOS33 } [get_ports { m2b_to_fpga1_prsnt_l }]; +set_property -dict { PACKAGE_PIN E18 IOSTANDARD LVCMOS33 } [get_ports { main_hsc_to_fpga1_alert_l }]; +set_property -dict { PACKAGE_PIN N17 IOSTANDARD LVCMOS33 } [get_ports { pcie_aux_fpga1_to_rsw_perst_l }]; +set_property -dict { PACKAGE_PIN N20 IOSTANDARD LVCMOS33 } [get_ports { pcie_aux_rsw_to_fpga1_prsnt_buff_l }]; +set_property -dict { PACKAGE_PIN P22 IOSTANDARD LVCMOS33 } [get_ports { pcie_aux_rsw_to_fpga1_pwrflt_buff_l }]; +set_property -dict { PACKAGE_PIN P8 IOSTANDARD LVCMOS18 } [get_ports { pcie_fpga1_to_nic_cha_perst_l }]; +set_property -dict { PACKAGE_PIN N7 IOSTANDARD LVCMOS18 } [get_ports { pcie_fpga1_to_nic_chb_perst_l }]; +set_property -dict { PACKAGE_PIN N8 IOSTANDARD LVCMOS18 } [get_ports { pcie_nic_to_fpga1_cha_prsnt_l }]; +set_property -dict { PACKAGE_PIN N5 IOSTANDARD LVCMOS18 } [get_ports { pcie_nic_to_fpga1_cha_pwren_l }]; +set_property -dict { PACKAGE_PIN R7 IOSTANDARD LVCMOS18 } [get_ports { pcie_nic_to_fpga1_chb_prsnt_l }]; +set_property -dict { PACKAGE_PIN R6 IOSTANDARD LVCMOS18 } [get_ports { pcie_nic_to_fpga1_chb_pwren_l }]; +set_property -dict { PACKAGE_PIN V13 IOSTANDARD LVCMOS33 } [get_ports { pwr_cont1_to_fpga1_alert_l }]; +set_property -dict { PACKAGE_PIN T12 IOSTANDARD LVCMOS33 } [get_ports { pwr_cont1_to_fpga1_vddcr_cpu0_pg }]; +set_property -dict { PACKAGE_PIN V14 IOSTANDARD LVCMOS33 } [get_ports { pwr_cont1_to_fpga1_vddcr_cpu1_pg }]; +set_property -dict { PACKAGE_PIN T13 IOSTANDARD LVCMOS33 } [get_ports { pwr_cont1_to_fpga1_vddcr_soc_pg }]; +set_property -dict { PACKAGE_PIN V15 IOSTANDARD LVCMOS33 } [get_ports { pwr_cont1_to_fpga1_vddio_sp5_pg }]; +set_property -dict { PACKAGE_PIN W15 IOSTANDARD LVCMOS33 } [get_ports { pwr_cont2_to_fpga1_alert_l }]; +set_property -dict { PACKAGE_PIN W16 IOSTANDARD LVCMOS33 } [get_ports { pwr_cont3_to_fpga1_alert_l }]; +set_property -dict { PACKAGE_PIN W14 IOSTANDARD LVCMOS33 } [get_ports { pwr_cont3_to_fpga1_cfp }]; +set_property -dict { PACKAGE_PIN Y14 IOSTANDARD LVCMOS33 } [get_ports { pwr_cont3_to_fpga1_vrhot_n }]; +set_property -dict { PACKAGE_PIN U17 IOSTANDARD LVCMOS33 } [get_ports { pwr_cont4_to_fpga1_alert_l }]; +set_property -dict { PACKAGE_PIN P16 IOSTANDARD LVCMOS33 } [get_ports { pwr_cont4_to_fpga1_cfp }]; +set_property -dict { PACKAGE_PIN P17 IOSTANDARD LVCMOS33 } [get_ports { pwr_cont4_to_fpga1_vrhot_n }]; +set_property -dict { PACKAGE_PIN W12 IOSTANDARD LVCMOS33 } [get_ports { pwr_fpga1_to_v1p5_sp5_rtc_a2_en }]; +set_property -dict { PACKAGE_PIN U12 IOSTANDARD LVCMOS33 } [get_ports { pwr_v1p5_sp5_rtc_a2_to_fpga1_pg }]; +set_property -dict { PACKAGE_PIN P6 IOSTANDARD LVCMOS18 } [get_ports { qspi_fpga1_to_vercel_flash_mux_cs_l }]; +set_property -dict { PACKAGE_PIN K6 IOSTANDARD LVCMOS18 } [get_ports { qspi_fpga1_to_vercel_flash_mux_d[0] }]; +set_property -dict { PACKAGE_PIN J6 IOSTANDARD LVCMOS18 } [get_ports { qspi_fpga1_to_vercel_flash_mux_d[1] }]; +set_property -dict { PACKAGE_PIN J8 IOSTANDARD LVCMOS18 } [get_ports { qspi_fpga1_to_vercel_flash_mux_d[2] }]; +set_property -dict { PACKAGE_PIN K8 IOSTANDARD LVCMOS18 } [get_ports { qspi_fpga1_to_vercel_flash_mux_d[3] }]; +set_property -dict { PACKAGE_PIN M5 IOSTANDARD LVCMOS18 } [get_ports { qspi_fpga1_to_vercel_flash_mux_sck }]; +set_property -dict { PACKAGE_PIN P20 IOSTANDARD LVCMOS33 } [get_ports { rsw_to_sp5_pcie_attached_buff_l }]; +set_property -dict { PACKAGE_PIN C16 IOSTANDARD LVCMOS33 } [get_ports { seq_rev_id[0] }]; +set_property -dict { PACKAGE_PIN B15 IOSTANDARD LVCMOS33 } [get_ports { seq_rev_id[1] }]; +set_property -dict { PACKAGE_PIN B16 IOSTANDARD LVCMOS33 } [get_ports { seq_rev_id[2] }]; +set_property -dict { PACKAGE_PIN C5 IOSTANDARD LVCMOS33 } [get_ports { smbus_fan_central_hsc_to_fpga1_alert_l }]; +set_property -dict { PACKAGE_PIN D6 IOSTANDARD LVCMOS33 } [get_ports { smbus_fan_east_hsc_to_fpga1_alert_l }]; +set_property -dict { PACKAGE_PIN C6 IOSTANDARD LVCMOS33 } [get_ports { smbus_fan_west_hsc_to_fpga1_alert_l }]; +set_property -dict { PACKAGE_PIN AA22 IOSTANDARD LVCMOS33 } [get_ports { smbus_ibc_to_fpga1_alert_l }]; +set_property -dict { PACKAGE_PIN D7 IOSTANDARD LVCMOS33 } [get_ports { smbus_m2_hsc_to_fpga1_alert_l }]; +set_property -dict { PACKAGE_PIN D8 IOSTANDARD LVCMOS33 } [get_ports { smbus_nic_hsc_to_fpga1_alert_l }]; +set_property -dict { PACKAGE_PIN A10 IOSTANDARD LVCMOS33 } [get_ports { smbus_therm_ne_to_fpga1_alert_l }]; +set_property -dict { PACKAGE_PIN C9 IOSTANDARD LVCMOS33 } [get_ports { smbus_therm_nw_to_fpga1_alert_l }]; +set_property -dict { PACKAGE_PIN B9 IOSTANDARD LVCMOS33 } [get_ports { smbus_therm_sc_to_fpga1_alert_l }]; +set_property -dict { PACKAGE_PIN A9 IOSTANDARD LVCMOS33 } [get_ports { smbus_therm_se_to_fpga1_alert_l }]; +set_property -dict { PACKAGE_PIN E8 IOSTANDARD LVCMOS33 } [get_ports { smbus_therm_sw_to_fpga1_alert_l }]; +set_property -dict { PACKAGE_PIN F5 IOSTANDARD LVCMOS33 } [get_ports { smbus_v12_ddr5_abcdef_hsc_to_fpga1_alert }]; +set_property -dict { PACKAGE_PIN F8 IOSTANDARD LVCMOS33 } [get_ports { smbus_v12_ddr5_ghijkl_hsc_to_fpga1_alert }]; +set_property -dict { PACKAGE_PIN K5 IOSTANDARD LVCMOS18 } [get_ports { sp5_to_fpga1_alert_l }]; +set_property -dict { PACKAGE_PIN AA9 IOSTANDARD LVCMOS18 } [get_ports { sp5_to_fpga1_debug1 }]; +set_property -dict { PACKAGE_PIN AB9 IOSTANDARD LVCMOS18 } [get_ports { sp5_to_fpga1_debug2 }]; +set_property -dict { PACKAGE_PIN L6 IOSTANDARD LVCMOS18 } [get_ports { sp5_to_fpga1_pwrgd_out }]; +set_property -dict { PACKAGE_PIN J7 IOSTANDARD LVCMOS18 } [get_ports { sp5_to_fpga1_pwrok_unbuf }]; +set_property -dict { PACKAGE_PIN L4 IOSTANDARD LVCMOS18 } [get_ports { fpga1_to_sp5_reset_l }]; +set_property -dict { PACKAGE_PIN AA10 IOSTANDARD LVCMOS18 } [get_ports { sp5_to_fpga1_slp_s3_l }]; +set_property -dict { PACKAGE_PIN Y7 IOSTANDARD LVCMOS18 } [get_ports { sp5_to_fpga1_slp_s5_l }]; +set_property -dict { PACKAGE_PIN L8 IOSTANDARD LVCMOS18 } [get_ports { sp5_to_fpga1_smerr_l }]; +set_property -dict { PACKAGE_PIN W7 IOSTANDARD LVCMOS18 } [get_ports { sp5_to_fpga1_spd_host_ctrl_l }]; +set_property -dict { PACKAGE_PIN L5 IOSTANDARD LVCMOS18 } [get_ports { sp5_to_fpga1_thermtrip_l }]; +set_property -dict { PACKAGE_PIN T1 IOSTANDARD LVCMOS18 } [get_ports { sp5_to_nic_mfg_mode_l }]; +set_property -dict { PACKAGE_PIN G13 IOSTANDARD LVCMOS33 } [get_ports { sp_to_fpga1_mux_reset_l }]; +set_property -dict { PACKAGE_PIN C15 IOSTANDARD LVCMOS33 } [get_ports { sp_to_fpga1_system_reset_l }]; +set_property -dict { PACKAGE_PIN N1 IOSTANDARD LVCMOS18 } [get_ports { spi0_sp5_to_fpga1_cs_l }]; +set_property -dict { PACKAGE_PIN P3 IOSTANDARD LVCMOS18 } [get_ports { spi1_sp5_to_fpga1_cs_l }]; +set_property -dict { PACKAGE_PIN P2 IOSTANDARD LVCMOS18 } [get_ports { spi2_sp5_to_fpga1_cs_l }]; +set_property -dict { PACKAGE_PIN E15 IOSTANDARD LVCMOS33 } [get_ports { spi_fpga1_to_flash_clk }]; +set_property -dict { PACKAGE_PIN C17 IOSTANDARD LVCMOS33 } [get_ports { spi_fpga1_to_flash_cs_l }]; +set_property -dict { PACKAGE_PIN E16 IOSTANDARD LVCMOS33 } [get_ports { spi_fpga1_to_flash_dat[0] }]; +set_property -dict { PACKAGE_PIN B14 IOSTANDARD LVCMOS33 } [get_ports { spi_fpga1_to_flash_dat[1] }]; +set_property -dict { PACKAGE_PIN A14 IOSTANDARD LVCMOS33 } [get_ports { spi_fpga1_to_flash_dat[2] }]; +set_property -dict { PACKAGE_PIN A16 IOSTANDARD LVCMOS33 } [get_ports { spi_fpga1_to_flash_dat[3] }]; +set_property -dict { PACKAGE_PIN T7 IOSTANDARD LVCMOS18 } [get_ports { uart0_fpga1_to_sp5_dat_buff }]; +set_property -dict { PACKAGE_PIN W5 IOSTANDARD LVCMOS18 } [get_ports { uart0_fpga1_to_sp5_rts_l_buff }]; +set_property -dict { PACKAGE_PIN F15 IOSTANDARD LVCMOS33 } [get_ports { uart0_fpga1_to_sp_dat }]; +set_property -dict { PACKAGE_PIN C13 IOSTANDARD LVCMOS33 } [get_ports { uart0_fpga1_to_sp_rts_l }]; +set_property -dict { PACKAGE_PIN T6 IOSTANDARD LVCMOS18 } [get_ports { uart0_sp5_to_fpga1_dat }]; +set_property -dict { PACKAGE_PIN R2 IOSTANDARD LVCMOS18 } [get_ports { uart0_sp5_to_fpga1_int }]; +set_property -dict { PACKAGE_PIN W4 IOSTANDARD LVCMOS18 } [get_ports { uart0_sp5_to_fpga1_rts_l }]; +set_property -dict { PACKAGE_PIN G14 IOSTANDARD LVCMOS33 } [get_ports { uart0_sp_to_fpga1_dat }]; +set_property -dict { PACKAGE_PIN D13 IOSTANDARD LVCMOS33 } [get_ports { uart0_sp_to_fpga1_rts_l }]; +set_property -dict { PACKAGE_PIN V5 IOSTANDARD LVCMOS18 } [get_ports { uart1_fpga1_to_sp5_dat_buff }]; +set_property -dict { PACKAGE_PIN A12 IOSTANDARD LVCMOS33 } [get_ports { uart1_fpga1_to_sp_dat }]; +set_property -dict { PACKAGE_PIN C10 IOSTANDARD LVCMOS33 } [get_ports { uart1_fpga1_to_sp_rts_l }]; +set_property -dict { PACKAGE_PIN V4 IOSTANDARD LVCMOS18 } [get_ports { uart1_sp5_to_fpga1_dat }]; +set_property -dict { PACKAGE_PIN B13 IOSTANDARD LVCMOS33 } [get_ports { uart1_sp_to_fpga1_dat }]; +set_property -dict { PACKAGE_PIN A13 IOSTANDARD LVCMOS33 } [get_ports { uart1_sp_to_fpga1_rts_l }]; +set_property -dict { PACKAGE_PIN F17 IOSTANDARD LVCMOS33 } [get_ports { uart8_fpga1_to_sp_dat }]; +set_property -dict { PACKAGE_PIN A21 IOSTANDARD LVCMOS33 } [get_ports { uart8_sp_to_fpga1_dat }]; +set_property -dict { PACKAGE_PIN D22 IOSTANDARD LVCMOS33 } [get_ports { uart_debug_to_fpga1_dat }]; +set_property -dict { PACKAGE_PIN F22 IOSTANDARD LVCMOS33 } [get_ports { uart_debug_to_fpga1_rts_l }]; +set_property -dict { PACKAGE_PIN E22 IOSTANDARD LVCMOS33 } [get_ports { uart_fpga1_to_debug_dat }]; +set_property -dict { PACKAGE_PIN F21 IOSTANDARD LVCMOS33 } [get_ports { uart_fpga1_to_debug_rts_l }]; +set_property -dict { PACKAGE_PIN D15 IOSTANDARD LVCMOS33 } [get_ports { uart_local_fpga1_to_sp_dat }]; +set_property -dict { PACKAGE_PIN C12 IOSTANDARD LVCMOS33 } [get_ports { uart_local_fpga1_to_sp_rts_l }]; +set_property -dict { PACKAGE_PIN D14 IOSTANDARD LVCMOS33 } [get_ports { uart_local_sp_to_fpga1_dat }]; +set_property -dict { PACKAGE_PIN D12 IOSTANDARD LVCMOS33 } [get_ports { uart_local_sp_to_fpga1_rts_l }]; +set_property -dict { PACKAGE_PIN N18 IOSTANDARD LVCMOS33 } [get_ports { v0p88_nic_a0hp_en }]; +set_property -dict { PACKAGE_PIN T17 IOSTANDARD LVCMOS33 } [get_ports { v0p88_nic_a0hp_pg }]; +set_property -dict { PACKAGE_PIN R19 IOSTANDARD LVCMOS33 } [get_ports { v0p8_nic_vccint_a0hp_en }]; +set_property -dict { PACKAGE_PIN R18 IOSTANDARD LVCMOS33 } [get_ports { v0p8_nic_vccint_a0hp_pg }]; +set_property -dict { PACKAGE_PIN F20 IOSTANDARD LVCMOS33 } [get_ports { v0p92_nic_avcc_a0hp_pg }]; +set_property -dict { PACKAGE_PIN G4 IOSTANDARD LVCMOS33 } [get_ports { v12_ddr5_abcdef_a0_pg }]; +set_property -dict { PACKAGE_PIN G3 IOSTANDARD LVCMOS33 } [get_ports { v12_ddr5_ghijkl_a0_pg }]; +set_property -dict { PACKAGE_PIN T19 IOSTANDARD LVCMOS33 } [get_ports { v12p0_nic_a0hp_pg }]; +set_property -dict { PACKAGE_PIN G6 IOSTANDARD LVCMOS33 } [get_ports { v12p0_nic_a0hp_to_fpga1_fault_l }]; +set_property -dict { PACKAGE_PIN W8 IOSTANDARD LVCMOS18 } [get_ports { v1p1_i3c_a2_pg }]; +set_property -dict { PACKAGE_PIN R16 IOSTANDARD LVCMOS33 } [get_ports { v1p1_nic_a0hp_en }]; +set_property -dict { PACKAGE_PIN AB16 IOSTANDARD LVCMOS33 } [get_ports { v1p1_sp5_en }]; +set_property -dict { PACKAGE_PIN T15 IOSTANDARD LVCMOS33 } [get_ports { v1p1_sp5_pg }]; +set_property -dict { PACKAGE_PIN AA21 IOSTANDARD LVCMOS33 } [get_ports { v1p2_fpga2_a2_pg }]; +set_property -dict { PACKAGE_PIN A18 IOSTANDARD LVCMOS33 } [get_ports { v1p2_nic_avtt_a0hp_pg }]; +set_property -dict { PACKAGE_PIN U20 IOSTANDARD LVCMOS33 } [get_ports { v1p4_nic_a0hp_en }]; +set_property -dict { PACKAGE_PIN N22 IOSTANDARD LVCMOS33 } [get_ports { v1p5_nic_a0hp_en }]; +set_property -dict { PACKAGE_PIN U18 IOSTANDARD LVCMOS33 } [get_ports { v1p5_nic_a0hp_pg }]; +set_property -dict { PACKAGE_PIN B19 IOSTANDARD LVCMOS33 } [get_ports { v1p5_nic_avccaux_a0hp_en }]; +set_property -dict { PACKAGE_PIN N15 IOSTANDARD LVCMOS33 } [get_ports { v1p5_nic_avccaux_a0hp_pg }]; +set_property -dict { PACKAGE_PIN D18 IOSTANDARD LVCMOS33 } [get_ports { v1p8_nic_a0hp_en }]; +set_property -dict { PACKAGE_PIN Y13 IOSTANDARD LVCMOS33 } [get_ports { v1p8_nic_a0hp_pg }]; +set_property -dict { PACKAGE_PIN AB17 IOSTANDARD LVCMOS33 } [get_ports { v1p8_sp5_en }]; +set_property -dict { PACKAGE_PIN T14 IOSTANDARD LVCMOS33 } [get_ports { v1p8_sp5_pg }]; +set_property -dict { PACKAGE_PIN Y20 IOSTANDARD LVCMOS33 } [get_ports { v2p5_fpga2_a2_pg }]; +set_property -dict { PACKAGE_PIN T21 IOSTANDARD LVCMOS33 } [get_ports { v2p5_mgmt_a2_pg }]; +set_property -dict { PACKAGE_PIN AA20 IOSTANDARD LVCMOS33 } [get_ports { v3p3_fpga2_a2_pg }]; +set_property -dict { PACKAGE_PIN V21 IOSTANDARD LVCMOS33 } [get_ports { v3p3_m2a_a0hp_pg_l }]; +set_property -dict { PACKAGE_PIN W22 IOSTANDARD LVCMOS33 } [get_ports { v3p3_m2b_a0hp_pg_l }]; +set_property -dict { PACKAGE_PIN A17 IOSTANDARD LVCMOS33 } [get_ports { v3p3_nic_a0hp_en }]; +set_property -dict { PACKAGE_PIN R17 IOSTANDARD LVCMOS33 } [get_ports { v3p3_nic_a0hp_pg }]; +set_property -dict { PACKAGE_PIN W17 IOSTANDARD LVCMOS33 } [get_ports { v3p3_sp5_en }]; +set_property -dict { PACKAGE_PIN U15 IOSTANDARD LVCMOS33 } [get_ports { v3p3_sp5_pg }]; +set_property -dict { PACKAGE_PIN T20 IOSTANDARD LVCMOS33 } [get_ports { v5p0_nic_a0hp_pg }]; +set_property -dict { PACKAGE_PIN F7 IOSTANDARD LVCMOS33 } [get_ports { v5p0_nic_a0hp_to_fpga1_fault_l }]; +set_property -dict { PACKAGE_PIN Y17 IOSTANDARD LVCMOS33 } [get_ports { vddcr_cpu0_en }]; +set_property -dict { PACKAGE_PIN AB14 IOSTANDARD LVCMOS33 } [get_ports { vddcr_cpu1_en }]; +set_property -dict { PACKAGE_PIN AA14 IOSTANDARD LVCMOS33 } [get_ports { vddcr_soc_en }]; +set_property -dict { PACKAGE_PIN V12 IOSTANDARD LVCMOS33 } [get_ports { vddio_sp5_en }]; +set_property -dict { PACKAGE_PIN A11 IOSTANDARD LVCMOS33 } [get_ports { versal_to_fpga1_done }]; +set_property -dict { PACKAGE_PIN C11 IOSTANDARD LVCMOS33 } [get_ports { versal_to_fpga1_error_out }]; +set_property -dict { PACKAGE_PIN C8 IOSTANDARD LVCMOS33 } [get_ports { vr_v1p8_sys_to_fpga1_alert_l }]; +set_property -dict { PACKAGE_PIN A6 IOSTANDARD LVCMOS33 } [get_ports { vr_v3p3_sys_to_fpga1_alert_l }]; +set_property -dict { PACKAGE_PIN B8 IOSTANDARD LVCMOS33 } [get_ports { vr_v5p0_sys_to_fpga1_alert_l }]; diff --git a/hdl/projects/metro_seq/metro_seq_top.rdl b/hdl/projects/metro_seq/metro_seq_top.rdl new file mode 100644 index 00000000..db085b55 --- /dev/null +++ b/hdl/projects/metro_seq/metro_seq_top.rdl @@ -0,0 +1,29 @@ +addrmap pca9506_axi_regs { + name = "pca9506 registers"; + desc = "Registers accessible on the i2c bus for controlling the emulated PCA9545 i2c mux"; + + default regwidth = 32; + default sw = rw; + default hw = r; + + pca9506_raw_regs #(.SIZE(32)) i2c; + +}; +// Offsets here must match the config_array constant in metro_seq_top.vhd; the +// two are kept in step by hand, as they are in cosmo_seq. +addrmap metro_seq_top { + default regwidth = 32; + + info_regs info @ 0x0; + spi_nor_regs spi_nor @ 0x0100; + sequencer_regs sequencer @ 0x0200; + sp_i2c_regs sp_i2c @ 0x0300; + pca9506_axi_regs fpga1_hotplug @ 0x0400; + dimm_regs dimms @ 0x0500; + debug_regs debug_ctrl @ 0x0600; + hash_engine_regs hash @ 0x0700; + spi_nor_regs versal_flash @ 0x0800; + versal_flash_regs versal_flash_ctrl @ 0x0900; + espi_regs espi @ 0x8000; + espi_regs espi1 @ 0x10000; +}; diff --git a/hdl/projects/metro_seq/metro_seq_top.vhd b/hdl/projects/metro_seq/metro_seq_top.vhd new file mode 100644 index 00000000..308b3a19 --- /dev/null +++ b/hdl/projects/metro_seq/metro_seq_top.vhd @@ -0,0 +1,1126 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +-- Metro Sequencer FPGA targeting the Spartan-7 (U27, XC7S100-1FGGA484I). +-- +-- Structurally this is cosmo_seq's top with the T6 NIC replaced by an AMD +-- Versal Premium VP1202: the SP-facing FMC bus, eSPI/SPI-NOR service, DIMM SPD +-- proxy, hotplug emulation, UART routing and I2C muxing are all the shared +-- blocks, and the Versal sequencing and boot-flash mux are Metro's own. + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; +use ieee.numeric_std_unsigned.all; + +use work.axil_common_pkg.all; +use work.axil26x32_pkg; +use work.axil8x32_pkg; +use work.axil32x32_pkg; +use work.axil15x32_pkg; +use work.axi_st8_pkg; +use work.axilite_if_2k19_helper_pkg.all; +use work.i2c_common_pkg.all; +use work.time_pkg.all; +use work.tristate_if_pkg.all; + +use work.sp5_power_pkg.all; +use work.sequencer_io_pkg.all; +use work.sp5_uart_subsystem_pkg.all; + +entity metro_seq_top is + port ( + -- Board clocks and resets + clk_50mhz_fpga1_2 : in std_logic; + clk_50mhz_fpga1_1 : in std_logic; + clk_buff_m2_rsw_to_fpga1_los_l : in std_logic; + clk_buff_nic_to_fpga1_los_l : in std_logic; + sp_to_fpga1_system_reset_l : in std_logic; + -- FMC interface to the SP + fmc_sp_to_fpga1_a : in std_logic_vector(23 downto 16); + fmc_sp_to_fpga1_adv_l : in std_logic; + fmc_sp_to_fpga1_bl_l : in std_logic_vector(1 downto 0); + fmc_sp_to_fpga1_clk : in std_logic; + fmc_sp_to_fpga1_cs_l : in std_logic; + fmc_sp_to_fpga1_da : inout std_logic_vector(15 downto 0); + fmc_sp_to_fpga1_oe_l : in std_logic; + fmc_sp_to_fpga1_wait_l : out std_logic; + fmc_sp_to_fpga1_we_l : in std_logic; + -- eSPI interfaces to the SP5 + espi0_fpga1_to_sp5_alert_l : in std_logic; + espi0_sp5_to_fpga1_clk : in std_logic; + espi0_sp5_to_fpga1_cs_l : in std_logic; + espi0_sp5_to_fpga1_dat : inout std_logic_vector(3 downto 0); + espi1_fpga1_to_sp5_alert_l : in std_logic; + espi1_sp5_to_fpga1_clk : in std_logic; + espi1_sp5_to_fpga1_clk_2 : in std_logic; + espi1_sp5_to_fpga1_cs_l : in std_logic; + espi1_sp5_to_fpga1_dat : inout std_logic_vector(3 downto 0); + espi_sp5_to_fpga1_reset_l : in std_logic; + -- SP5 boot flash (SPI-NOR) + spi0_sp5_to_fpga1_cs_l : in std_logic; + spi1_sp5_to_fpga1_cs_l : in std_logic; + spi2_sp5_to_fpga1_cs_l : in std_logic; + spi_fpga1_to_flash_clk : out std_logic; + spi_fpga1_to_flash_cs_l : out std_logic; + spi_fpga1_to_flash_dat : inout std_logic_vector(3 downto 0); + -- Versal (VP1202) boot straps, status and boot-flash mux + fpga1_to_vercel_flash_qspi_mux_en_l : out std_logic; + fpga1_to_vercel_flash_qspi_mux_sel : out std_logic; + fpga1_to_versal_erro_done_buff_en : out std_logic; + fpga1_to_versal_mode : out std_logic_vector(3 downto 0); + fpga1_to_versal_mode_buffer_en_l : out std_logic; + fpga1_to_versal_por_b : out std_logic; + qspi_fpga1_to_vercel_flash_mux_cs_l : out std_logic; + qspi_fpga1_to_vercel_flash_mux_d : inout std_logic_vector(3 downto 0); + qspi_fpga1_to_vercel_flash_mux_sck : out std_logic; + versal_to_fpga1_done : in std_logic; + versal_to_fpga1_error_out : in std_logic; + -- Versal power rails + fpga1_to_nic_hsc_en : out std_logic; + sp5_to_nic_mfg_mode_l : in std_logic; + v0p88_nic_a0hp_en : out std_logic; + v0p88_nic_a0hp_pg : in std_logic; + v0p8_nic_vccint_a0hp_en : out std_logic; + v0p8_nic_vccint_a0hp_pg : in std_logic; + v0p92_nic_avcc_a0hp_pg : in std_logic; + v12p0_nic_a0hp_pg : in std_logic; + v12p0_nic_a0hp_to_fpga1_fault_l : in std_logic; + v1p1_nic_a0hp_en : out std_logic; + v1p2_nic_avtt_a0hp_pg : in std_logic; + v1p4_nic_a0hp_en : out std_logic; + v1p5_nic_a0hp_en : out std_logic; + v1p5_nic_a0hp_pg : in std_logic; + v1p5_nic_avccaux_a0hp_en : out std_logic; + v1p5_nic_avccaux_a0hp_pg : in std_logic; + v1p8_nic_a0hp_en : out std_logic; + v1p8_nic_a0hp_pg : in std_logic; + v3p3_nic_a0hp_en : out std_logic; + v3p3_nic_a0hp_pg : in std_logic; + v5p0_nic_a0hp_pg : in std_logic; + v5p0_nic_a0hp_to_fpga1_fault_l : in std_logic; + -- Versal PCIe channels + fpga1_to_pcie_clk_buff_nic_cha_oe_l : out std_logic; + fpga1_to_pcie_clk_buff_nic_chb_oe_l : out std_logic; + pcie_fpga1_to_nic_cha_perst_l : out std_logic; + pcie_fpga1_to_nic_chb_perst_l : out std_logic; + pcie_nic_to_fpga1_cha_prsnt_l : in std_logic; + pcie_nic_to_fpga1_cha_pwren_l : in std_logic; + pcie_nic_to_fpga1_chb_prsnt_l : in std_logic; + pcie_nic_to_fpga1_chb_pwren_l : in std_logic; + -- SP5 power rails and sequencing + fpga1_to_sp5_apml_xltr_en : out std_logic; + fpga1_to_sp5_espi_kbrst_l : in std_logic; + fpga1_to_sp5_pwr_btn_l : out std_logic; + fpga1_to_sp5_pwrgd : out std_logic; + fpga1_to_sp5_romtype0 : in std_logic; + fpga1_to_sp5_rsmrst_l : out std_logic; + fpga1_to_sp5_sys_reset_l : out std_logic; + pwr_cont1_to_fpga1_alert_l : in std_logic; + pwr_cont1_to_fpga1_vddcr_cpu0_pg : in std_logic; + pwr_cont1_to_fpga1_vddcr_cpu1_pg : in std_logic; + pwr_cont1_to_fpga1_vddcr_soc_pg : in std_logic; + pwr_cont1_to_fpga1_vddio_sp5_pg : in std_logic; + pwr_cont2_to_fpga1_alert_l : in std_logic; + pwr_cont3_to_fpga1_alert_l : in std_logic; + pwr_cont3_to_fpga1_cfp : in std_logic; + pwr_cont3_to_fpga1_vrhot_n : in std_logic; + pwr_cont4_to_fpga1_alert_l : in std_logic; + pwr_cont4_to_fpga1_cfp : in std_logic; + pwr_cont4_to_fpga1_vrhot_n : in std_logic; + pwr_fpga1_to_v1p5_sp5_rtc_a2_en : out std_logic; + pwr_v1p5_sp5_rtc_a2_to_fpga1_pg : in std_logic; + sp5_to_fpga1_alert_l : in std_logic; + sp5_to_fpga1_debug1 : out std_logic; + sp5_to_fpga1_debug2 : in std_logic; + sp5_to_fpga1_pwrgd_out : in std_logic; + sp5_to_fpga1_pwrok_unbuf : in std_logic; + fpga1_to_sp5_reset_l : in std_logic; + sp5_to_fpga1_slp_s3_l : in std_logic; + sp5_to_fpga1_slp_s5_l : in std_logic; + sp5_to_fpga1_smerr_l : in std_logic; + sp5_to_fpga1_spd_host_ctrl_l : in std_logic; + sp5_to_fpga1_thermtrip_l : in std_logic; + v1p1_i3c_a2_pg : in std_logic; + v1p1_sp5_en : out std_logic; + v1p1_sp5_pg : in std_logic; + v1p2_fpga2_a2_pg : in std_logic; + v1p8_sp5_en : out std_logic; + v1p8_sp5_pg : in std_logic; + v2p5_fpga2_a2_pg : in std_logic; + v2p5_mgmt_a2_pg : in std_logic; + v3p3_fpga2_a2_pg : in std_logic; + v3p3_sp5_en : out std_logic; + v3p3_sp5_pg : in std_logic; + vddcr_cpu0_en : out std_logic; + vddcr_cpu1_en : out std_logic; + vddcr_soc_en : out std_logic; + vddio_sp5_en : out std_logic; + -- DDR bulk power and DIMM power good + dimm_a_pg : in std_logic; + dimm_b_pg : in std_logic; + dimm_c_pg : in std_logic; + dimm_d_pg : in std_logic; + dimm_e_pg : in std_logic; + dimm_f_pg : in std_logic; + dimm_g_pg : in std_logic; + dimm_h_pg : in std_logic; + dimm_i_pg : in std_logic; + dimm_j_pg : in std_logic; + dimm_k_pg : in std_logic; + dimm_l_pg : in std_logic; + fpga1_to_v12_ddr5_abcdef_hsc_en : out std_logic; + fpga1_to_v12_ddr5_ghijkl_hsc_en : out std_logic; + v12_ddr5_abcdef_a0_pg : in std_logic; + v12_ddr5_ghijkl_a0_pg : in std_logic; + -- DIMM SPD I3C + i3c_fpga1_to_dimm_abcdef_scl : inout std_logic; + i3c_fpga1_to_dimm_abcdef_sda : inout std_logic; + i3c_fpga1_to_dimm_ghijkl_scl : inout std_logic; + i3c_fpga1_to_dimm_ghijkl_sda : inout std_logic; + i3c_fpga1_to_dimm_oe_l : out std_logic; + i3c_sp5_to_fpga1_abcdef_scl : inout std_logic; + i3c_sp5_to_fpga1_abcdef_sda : inout std_logic; + i3c_sp5_to_fpga1_ghijkl_scl : inout std_logic; + i3c_sp5_to_fpga1_ghijkl_sda : inout std_logic; + i3c_sp5_to_fpga1_oe_l : out std_logic; + -- M.2 hotplug + fpga1_to_m2_apml_xltr_en : out std_logic; + fpga1_to_m2a_hsc_en : out std_logic; + fpga1_to_m2a_perst_l : out std_logic; + fpga1_to_m2b_hsc_en : out std_logic; + fpga1_to_m2b_perst_l : out std_logic; + fpga1_to_pcie_clk_buff_m2a_oe_l : out std_logic; + fpga1_to_pcie_clk_buff_m2b_oe_l : out std_logic; + m2a_hsc_to_fpga1_fault_l : in std_logic; + m2a_to_fpga1_pedet : in std_logic; + m2a_to_fpga1_prsnt_l : in std_logic; + m2b_hsc_to_fpga1_fault_l : in std_logic; + m2b_to_fpga1_pedet : in std_logic; + m2b_to_fpga1_prsnt_l : in std_logic; + v3p3_m2a_a0hp_pg_l : in std_logic; + v3p3_m2b_a0hp_pg_l : in std_logic; + -- Backplane / rear switch + fpga1_to_bp_buff_output_en_l : out std_logic; + fpga1_to_pcie_clk_buff_rsw_oe_l : out std_logic; + pcie_aux_fpga1_to_rsw_perst_l : out std_logic; + pcie_aux_rsw_to_fpga1_prsnt_buff_l : in std_logic; + pcie_aux_rsw_to_fpga1_pwrflt_buff_l : in std_logic; + rsw_to_sp5_pcie_attached_buff_l : in std_logic; + -- Fans and thermal + fan_central_hsc_to_fpga1_pg : in std_logic; + fan_east_hsc_to_fpga1_pg : in std_logic; + fan_to_fpga1_fan_fail : in std_logic; + fan_west_hsc_to_fpga1_pg : in std_logic; + fpga1_to_fan_central_hsc_disable : out std_logic; + fpga1_to_fan_east_hsc_disable : out std_logic; + fpga1_to_fan_west_hsc_disable : out std_logic; + smbus_fan_central_hsc_to_fpga1_alert_l : in std_logic; + smbus_fan_east_hsc_to_fpga1_alert_l : in std_logic; + smbus_fan_west_hsc_to_fpga1_alert_l : in std_logic; + smbus_therm_ne_to_fpga1_alert_l : in std_logic; + smbus_therm_nw_to_fpga1_alert_l : in std_logic; + smbus_therm_sc_to_fpga1_alert_l : in std_logic; + smbus_therm_se_to_fpga1_alert_l : in std_logic; + smbus_therm_sw_to_fpga1_alert_l : in std_logic; + -- Regulator and hotswap alerts + i2c_sp_to_nic_sysmon_alert_l : in std_logic; + main_hsc_to_fpga1_alert_l : in std_logic; + smbus_ibc_to_fpga1_alert_l : in std_logic; + smbus_m2_hsc_to_fpga1_alert_l : in std_logic; + smbus_nic_hsc_to_fpga1_alert_l : in std_logic; + smbus_v12_ddr5_abcdef_hsc_to_fpga1_alert : in std_logic; + smbus_v12_ddr5_ghijkl_hsc_to_fpga1_alert : in std_logic; + vr_v1p8_sys_to_fpga1_alert_l : in std_logic; + vr_v3p3_sys_to_fpga1_alert_l : in std_logic; + vr_v5p0_sys_to_fpga1_alert_l : in std_logic; + -- I2C to the SP and SP5 + fpga1_to_i2c_mux1_sel : out std_logic_vector(1 downto 0); + fpga1_to_i2c_mux2_sel : out std_logic_vector(1 downto 0); + fpga1_to_i2c_mux3_sel : out std_logic_vector(1 downto 0); + i2c_sp5_sec_v3p3_scl : inout std_logic; + i2c_sp5_sec_v3p3_sda : inout std_logic; + i2c_sp5_to_fpgax_hp_scl : inout std_logic; + i2c_sp5_to_fpgax_hp_sda : inout std_logic; + i2c_sp_to_fpga1_scl : inout std_logic; + i2c_sp_to_fpga1_sda : inout std_logic; + sp_to_fpga1_mux_reset_l : in std_logic; + -- UARTs + uart0_fpga1_to_sp5_dat_buff : out std_logic; + uart0_fpga1_to_sp5_rts_l_buff : out std_logic; + uart0_fpga1_to_sp_dat : out std_logic; + uart0_fpga1_to_sp_rts_l : out std_logic; + uart0_sp5_to_fpga1_dat : in std_logic; + uart0_sp5_to_fpga1_int : in std_logic; + uart0_sp5_to_fpga1_rts_l : in std_logic; + uart0_sp_to_fpga1_dat : in std_logic; + uart0_sp_to_fpga1_rts_l : in std_logic; + uart1_fpga1_to_sp5_dat_buff : out std_logic; + uart1_fpga1_to_sp_dat : out std_logic; + uart1_fpga1_to_sp_rts_l : out std_logic; + uart1_sp5_to_fpga1_dat : in std_logic; + uart1_sp_to_fpga1_dat : in std_logic; + uart1_sp_to_fpga1_rts_l : in std_logic; + uart8_fpga1_to_sp_dat : out std_logic; + uart8_sp_to_fpga1_dat : in std_logic; + uart_debug_to_fpga1_dat : in std_logic; + uart_debug_to_fpga1_rts_l : in std_logic; + uart_fpga1_to_debug_dat : out std_logic; + uart_fpga1_to_debug_rts_l : out std_logic; + uart_local_fpga1_to_sp_dat : in std_logic; + uart_local_fpga1_to_sp_rts_l : in std_logic; + uart_local_sp_to_fpga1_dat : in std_logic; + uart_local_sp_to_fpga1_rts_l : in std_logic; + -- HDT debug mux + hdt_conn_to_mux_testen : in std_logic; + hdt_fpga1_to_mux_dat : in std_logic; + hdt_fpga1_to_mux_dbreq_l : in std_logic; + hdt_fpga1_to_mux_en_l : in std_logic; + hdt_fpga1_to_mux_sel : in std_logic; + hdt_fpga1_to_mux_tck : in std_logic; + hdt_fpga1_to_mux_tms : in std_logic; + hdt_fpga1_to_mux_trst_l : in std_logic; + hdt_fpga1_to_mux_xtrig5_l : in std_logic; + hdt_fpga1_to_mux_xtrig6_l : in std_logic; + hdt_fpga1_to_mux_xtrig7_l : in std_logic; + hdt_mux_to_fpga1_dat : in std_logic; + -- Ignition, straps, spares and misc + fpga1_debug_uart_buf_oe_en_l : out std_logic; + fpga1_espi0_cs_l_buff_oe_en_l : out std_logic; + fpga1_spare_v1p8 : out std_logic_vector(7 downto 0); + fpga1_spare_v3p3_0 : out std_logic; + fpga1_spare_v3p3_1 : out std_logic; + fpga1_spare_v3p3_2 : out std_logic; + fpga1_spare_v3p3_3 : out std_logic; + fpga1_spare_v3p3_4 : in std_logic; + fpga1_spare_v3p3_5 : in std_logic; + fpga1_spare_v3p3_6 : out std_logic; + fpga1_spare_v3p3_7 : out std_logic; + fpga1_status_led : out std_logic; + fpga1_to_fpga2_io : out std_logic_vector(5 downto 0); + fpga1_to_ign_trgt_fpga_creset : out std_logic; + fpga1_to_jtag_mux_sel : out std_logic; + fpga1_to_sp_int_l : in std_logic; + fpga1_to_sp_irq_l : out std_logic_vector(6 downto 1); + fpga1_to_sp_misc_a : in std_logic; + fpga1_to_sp_misc_b : in std_logic; + fpga1_to_sp_misc_c : in std_logic; + fpga1_to_sp_misc_d : in std_logic; + fpga1_to_sp_mux_ign_mux_sel : out std_logic; + fpga1_uart0_buff_oe_en_l : out std_logic; + fpga1_uart1_buff_oe_en_l : out std_logic; + fpga1_version_id : in std_logic_vector(1 downto 0); + sp5_to_fpga1_genint_l : out std_logic; + seq_rev_id : in std_logic_vector(2 downto 0); + ); +end entity; + +architecture rtl of metro_seq_top is + + signal clk_125m : std_logic; + signal reset_125m : std_logic; + signal clk_200m : std_logic; + signal reset_200m : std_logic; + signal reset_fmc : std_logic; + alias fmc_clk : std_logic is fmc_sp_to_fpga1_clk; + -- deskewed/phase-shifted FMC clock from the MMCM in board_support; the + -- FMC domain runs on this, never on the raw pin + signal fmc_clk_buf : std_logic; + signal fmc_capture_clk_buf : std_logic; + constant INFO_RESP_IDX : integer := 0; + constant SPINOR_RESP_IDX: integer := 1; + constant SEQ_RESP_IDX: integer := 2; + constant SP_I2C_RESP_IDX: integer := 3; + constant SP5_HP_RESP_IDX : integer := 4; + constant SPD_PROXY_RESP_IDX : integer := 5; + constant DBG_CTRL_RESP_IDX : integer := 6; + constant ESPI_RESP_IDX: integer := 7; + constant HASH_RESP_IDX : integer := 8; + constant VERSAL_FLASH_RESP_IDX : integer := 9; + constant VERSAL_FLASH_CTRL_RESP_IDX : integer := 10; + constant ESPI1_RESP_IDX : integer := 11; + + constant config_array : axil_responder_cfg_array_t := + (INFO_RESP_IDX => resp_cfg(base_addr => x"00000000", addr_span_bits => 8), + SPINOR_RESP_IDX => resp_cfg(base_addr => x"00000100", addr_span_bits => 8), + SEQ_RESP_IDX => resp_cfg(base_addr => x"00000200", addr_span_bits => 8), + SP_I2C_RESP_IDX => resp_cfg(base_addr => x"00000300", addr_span_bits => 8), + SP5_HP_RESP_IDX => resp_cfg(base_addr => x"00000400", addr_span_bits => 8), + SPD_PROXY_RESP_IDX => resp_cfg(base_addr => x"00000500", addr_span_bits => 8), + DBG_CTRL_RESP_IDX => resp_cfg(base_addr => x"00000600", addr_span_bits => 8), + -- eSPI is the largest register file and the most distant block, and it + -- owns the worst 125MHz path in the design, so give the fabric a cycle + -- in each direction to get there and back. + ESPI_RESP_IDX => resp_cfg(base_addr => x"00008000", addr_span_bits => 15, pipe_stages => 1), + HASH_RESP_IDX => resp_cfg(base_addr => x"00000700", addr_span_bits => 8), + VERSAL_FLASH_RESP_IDX => resp_cfg(base_addr => x"00000800", addr_span_bits => 8), + VERSAL_FLASH_CTRL_RESP_IDX => resp_cfg(base_addr => x"00000900", addr_span_bits => 8), + -- Second eSPI target, same size and same reasoning as the first. + ESPI1_RESP_IDX => resp_cfg(base_addr => x"00010000", addr_span_bits => 15, pipe_stages => 1) + ); + signal fmc_axi_if : axil26x32_pkg.axil_t; + signal fabric_responders : axil32x32_pkg.axil_array_t(config_array'range); + signal responders_8b : axil8x32_pkg.axil_array_t(config_array'range); + signal responders_15b : axil15x32_pkg.axil_array_t(config_array'range); + signal fmc_internal_data_out : std_logic_vector(15 downto 0); + signal fmc_data_out_hiz: std_logic_vector(15 downto 0); + + signal spinor_io_o : std_logic_vector(3 downto 0); + signal spinor_io_oe : std_logic_vector(3 downto 0); + signal espi_io_o : std_logic_vector(3 downto 0); + signal espi_io_oe : std_logic_vector(3 downto 0); + signal espi1_io_o : std_logic_vector(3 downto 0); + signal espi1_io_oe : std_logic_vector(3 downto 0); + -- The eSPI1 wrapper's spi_nor drives the Versal flash pins directly and + -- parks them unless the mux control block has the flash. + signal versal_flash_bus_enable : std_logic; + -- eSPI1 has no IPCC UART behind it; its peripheral channel is tied off. + signal espi1_uart_from_axi_st : axi_st8_pkg.axi_st_t; + signal espi1_uart_to_axi_st : axi_st8_pkg.axi_st_t; + -- hash engine <-> spi_nor flash client port(s) + -- One hash engine serves both flashes: the SP5 boot flash behind eSPI0 + -- and the Versal boot flash behind eSPI1. + constant HASH_NUM_FLASHES : natural := 2; + signal hash_flash_cmd_rdata : std_logic_vector(31 downto 0); + signal hash_flash_cmd_rdack : std_logic_vector(HASH_NUM_FLASHES - 1 downto 0); + signal hash_flash_cmd_rempty : std_logic_vector(HASH_NUM_FLASHES - 1 downto 0); + signal hash_flash_rsp_wdata : std_logic_vector(HASH_NUM_FLASHES * 8 - 1 downto 0); + signal hash_flash_rsp_write : std_logic_vector(HASH_NUM_FLASHES - 1 downto 0); + + signal ipcc_uart_from_espi_axi_st : axi_st8_pkg.axi_st_t; + signal ipcc_uart_to_espi_axi_st : axi_st8_pkg.axi_st_t; + signal a0_ok : std_logic; + signal a0_idle : std_logic; + signal ddr_bulk : ddr_bulk_power_t; + signal sp5_group_a : group_a_power_t; + signal sp5_group_b : group_b_power_t; + signal sp5_group_c : group_c_power_t; + signal versal_rails : versal_power_t; + signal versal_boot : versal_boot_t; + signal versal_pcie : versal_pcie_t; + signal versal_held_in_reset : std_logic; + signal flash_owned_by_seq : std_logic; + signal versal_hash_req : std_logic; + signal versal_hash_ack : std_logic; + signal versal_hash_err : std_logic; + signal sp5_seq_pins : sp5_seq_pins_t; + signal early_power : early_power_t; + signal sp_scl_o : std_logic; + signal sp_scl_oe : std_logic; + signal sp_sda_o : std_logic; + signal sp_sda_oe : std_logic; + signal sp5_scl_o : std_logic; + signal sp5_scl_oe : std_logic; + signal sp5_sda_o : std_logic; + signal sp5_sda_oe : std_logic; + + signal sp5_versal_power_en : std_logic; + signal sp5_versal_cha_perst_l : std_logic; + signal sp5_versal_chb_perst_l : std_logic; + signal sp5_versal_faulted : std_logic; + signal vercel_flash_io_o : std_logic_vector(3 downto 0); + signal vercel_flash_io_oe : std_logic_vector(3 downto 0); + signal espi_resp_csn : std_logic; + signal hp_int_n : std_logic; + + signal fpga1_to_pcie_clk_buff_rsw_oe_l_int : std_logic; + + signal sp5_abcdef_scl_if : tristate; + signal sp5_abcdef_sda_if : tristate; + signal sp5_ghijkl_scl_if : tristate; + signal sp5_ghijkl_sda_if : tristate; + signal dimm_abcdef_scl_if : tristate; + signal dimm_abcdef_sda_if : tristate; + signal dimm_ghijkl_scl_if : tristate; + signal dimm_ghijkl_sda_if : tristate; + + signal amd_hp_irq_n_final : std_logic; + -- Metro's FPGA2 sends its hotplug interrupts straight to the SP + -- (FPGA2_TO_SP_INT[1..3]), so unlike cosmo there is no FPGA2 IRQ arriving + -- here to fold in. + alias a0_ok_to_fpga2 : std_logic is fpga1_to_fpga2_io(2); + signal uart_dbg_if : uart_dbg_t; + signal allow_backplane_pcie_clk : std_logic; + signal versal_dbg_pins : nic_debug_if; + signal reg_alert_l_pins : seq_power_alert_pins_t; + -- No T6 on this board; the shared sequencer's T6 ports are tied off to + -- these and its T6 outputs left open. + signal nic_rails_unused : nic_power_t := nic_power_absent; + signal nic_seq_pins_unused : nic_seq_pins_t := nic_seq_pins_absent; + signal dbg_pins_uart_out : std_logic; + signal dbg_pins_uart_out_rts_l : std_logic; + signal dbg_pins_uart_in : std_logic; + signal dbg_pins_uart_in_rts_l : std_logic; + signal uart_headder_fall_back_to_debug_pins : std_logic; + signal sp_mux_reset_l_syncd : std_logic; + +begin + + meta_sync_inst_mux_reset_l: entity work.meta_sync + port map( + async_input => sp_to_fpga1_mux_reset_l, + clk => clk_125m, + sycnd_output => sp_mux_reset_l_syncd + ); + + -- SP5 SEC I2C: pins are wired but nothing drives this bus yet. + i2c_sp5_sec_v3p3_scl <= 'Z'; + i2c_sp5_sec_v3p3_sda <= 'Z'; + -- misc things tied: + fpga1_to_fpga2_io(5 downto 3) <= (others => 'Z'); + fpga1_to_fpga2_io(1 downto 0) <= (others => 'Z'); + fpga1_to_sp5_sys_reset_l <= 'Z'; -- We don't use this in product, external PU. + fpga1_to_sp_irq_l(6 downto 2) <= (others => '1'); + -- The JTAG mux stays pointed at the external header; the FPGA only takes it + -- when someone deliberately drives this from a debug session. + fpga1_to_jtag_mux_sel <= '0'; + -- Metro adds a buffer enable for the dedicated debug UART header alongside + -- the two SP-facing ones. + fpga1_debug_uart_buf_oe_en_l <= '0'; + -- The low half of the 3V3 spare header. Nothing drives these, and they go + -- to a header someone may well jumper, so hold them off rather than leaving + -- output ports undriven for the tools to resolve however they like. + fpga1_spare_v3p3_0 <= 'Z'; + fpga1_spare_v3p3_1 <= 'Z'; + fpga1_spare_v3p3_2 <= 'Z'; + fpga1_spare_v3p3_3 <= 'Z'; + fpga1_to_bp_buff_output_en_l <= '0'; -- This buffer has to be enabled to see any BP PCIe signals + -- Enable various buffers when we're in A0: + fpga1_espi0_cs_l_buff_oe_en_l <= '0' when sp5_seq_pins.pwr_good else 'Z'; + fpga1_to_sp5_apml_xltr_en <= sp5_seq_pins.pwr_good; + fpga1_to_m2_apml_xltr_en <= sp5_seq_pins.pwr_good; + fpga1_uart0_buff_oe_en_l <= '0' when a0_ok else '1'; + fpga1_uart1_buff_oe_en_l <= '0' when a0_ok else '1'; -- not used but why not enable anyway? + uart1_fpga1_to_sp5_dat_buff <= '1'; -- Make this idle generally, buffer protects from cross-drive + + i3c_sp5_to_fpga1_oe_l <= '0' when sp5_seq_pins.pwr_good else '1'; + -- Metro does not carry the rev1 cosmo buffer bug, so the DIMM side can be + -- enabled regardless of SP5 power state, which is what we actually want. + i3c_fpga1_to_dimm_oe_l <= '0'; + + --------------------------------------------- + -- FMC to AXI Interface from the SP + --------------------------------------------- + stm32h7_fmc_target_inst: entity work.stm32h7_fmc_target + port map( + chip_reset => reset_fmc, + fmc_clk => fmc_clk_buf, + fmc_capture_clk => fmc_capture_clk_buf, + a(24 downto 20) => "00000", + a(19 downto 16) => fmc_sp_to_fpga1_a(19 downto 16), + --a(23 downto 16) => fmc_sp_to_fpga1_a, + addr_data_in => fmc_sp_to_fpga1_da, + data_out => fmc_internal_data_out, + data_out_hiz => fmc_data_out_hiz, + ne(3 downto 1) => "111", + ne(0) => fmc_sp_to_fpga1_cs_l, + noe => fmc_sp_to_fpga1_oe_l, + nwe => fmc_sp_to_fpga1_we_l, + nl => fmc_sp_to_fpga1_adv_l, + nwait => fmc_sp_to_fpga1_wait_l, + timeout_count => open, + contention_count => open, + aclk => clk_125m, + aresetn => not reset_125m, + axi_if => fmc_axi_if + ); + -- tristate control for the FMC data bus + -- per-bit tristate, hiz already in OBUFT T polarity so each pin's T + -- flop packs into its IOB with no inverter in between + fmc_da_tris: for i in fmc_sp_to_fpga1_da'range generate + fmc_sp_to_fpga1_da(i) <= 'Z' when fmc_data_out_hiz(i) = '1' else fmc_internal_data_out(i); + end generate; + + -- Axi decode/interconnect + axil_interconnect_inst: entity work.axil_interconnect + generic map( + config_array => config_array + ) + port map( + clk => clk_125m, + reset => reset_125m, + initiator => fmc_axi_if, + responders => fabric_responders + ); + + -- Block that generates our clocks, resets and + -- deals with core board-level functionality + -- includes the common "info" block on the axi bus + resize_axil(fabric_responders(INFO_RESP_IDX), responders_8b(INFO_RESP_IDX)); + board_support_inst: entity work.board_support + port map( + board_50mhz_clk => clk_50mhz_fpga1_1, + sp_fmc_clk => fmc_clk, + fmc_clk_buf => fmc_clk_buf, + fmc_capture_clk_buf => fmc_capture_clk_buf, + sp_system_reset_l => sp_to_fpga1_system_reset_l, + clk_125m => clk_125m, + reset_125m => reset_125m, + clk_200m => clk_200m, + reset_200m => reset_200m, + reset_fmc => reset_fmc, + fpga1_status_led => fpga1_status_led, + hubris_compat_ver => seq_rev_id, + info_axi_if => responders_8b(INFO_RESP_IDX) + ); + + -- espi and flash interface block + -- espi and spi-nor blocks manage their own synchronization. + -- only a tiny portion of the espi design runs at 200MHz + -- all the system interfaces run at 125MHz for common clocking + resize_axil(fabric_responders(ESPI_RESP_IDX), responders_15b(ESPI_RESP_IDX)); + resize_axil(fabric_responders(SPINOR_RESP_IDX), responders_8b(SPINOR_RESP_IDX)); + resize_axil(fabric_responders(HASH_RESP_IDX), responders_8b(HASH_RESP_IDX)); + espi_spinor_ss: entity work.sp5_espi_flash_subsystem + port map( + clk_125m => clk_125m, + reset_125m => reset_125m, + clk_200m => clk_200m, + reset_200m => reset_200m, + espi_axi_if => responders_15b(ESPI_RESP_IDX), + espi_csn => espi0_sp5_to_fpga1_cs_l, + espi_clk => espi0_sp5_to_fpga1_clk, + espi_dat => espi0_sp5_to_fpga1_dat, + espi_dat_o => espi_io_o, + espi_dat_oe => espi_io_oe, + response_csn => espi_resp_csn, -- debugging with saleae if you have access + ipcc_uart_from_espi => ipcc_uart_from_espi_axi_st, + ipcc_uart_to_espi => ipcc_uart_to_espi_axi_st, + spinor_axi_if => responders_8b(SPINOR_RESP_IDX), + spi_nor_csn => spi_fpga1_to_flash_cs_l, + spi_nor_clk => spi_fpga1_to_flash_clk, + spi_nor_dat => spi_fpga1_to_flash_dat, + spi_nor_dat_o => spinor_io_o, + spi_nor_dat_oe => spinor_io_oe, + hash_cmd_fifo_rdata => hash_flash_cmd_rdata, + hash_cmd_fifo_rdack => hash_flash_cmd_rdack(0), + hash_cmd_fifo_rempty => hash_flash_cmd_rempty(0), + hash_data_fifo_wdata => hash_flash_rsp_wdata(7 downto 0), + hash_data_fifo_write => hash_flash_rsp_write(0) + ); + + -- SHA3 hashing engine. It reads flash through spi_nor_top's second client + -- port and owns the FIFOs on that path; it sits here rather than inside the + -- eSPI wrapper so one engine can serve more than one flash. + hash_engine_inst: entity work.hash_engine_top + generic map( + NUM_FLASHES => HASH_NUM_FLASHES, + -- The sequencer's pre-boot measurement is of the Versal image, on + -- the flash behind eSPI1. + HW_FLASH_SEL => 1 + ) + port map( + clk => clk_125m, + reset => reset_125m, + axi_if => responders_8b(HASH_RESP_IDX), + hw_req => versal_hash_req, + hw_ack => versal_hash_ack, + hw_err => versal_hash_err, + flash_cmd_rdata => hash_flash_cmd_rdata, + flash_cmd_rdack => hash_flash_cmd_rdack, + flash_cmd_rempty => hash_flash_cmd_rempty, + flash_rsp_wdata => hash_flash_rsp_wdata, + flash_rsp_write => hash_flash_rsp_write + ); + -- Second eSPI target on the SP5's other eSPI port, fronting the Versal's + -- boot flash. This is the one place the host may write flash over SAFS, + -- which is how a Versal image gets loaded; the enable bit in its control + -- register still has to be set by the SP. There is no IPCC UART behind + -- this port, and no post codes either: the SP5 only writes those to + -- eSPI0, so the buffer for them is left out. + -- Its spi_nor runs at half the SP5 boot flash's rate, 125MHz + -- / (2 * 2) = 31.25MHz: nothing here is on a boot path, the SP only + -- reaches this flash while the Versal is held in reset, so there is no + -- reason to run at the ceiling. The slower rate widens the read sample + -- window from roughly 3.6..11.7ns to -4.4..19.7ns, which leaves + -- rx_sample_taps = 2 (8ns) far from either edge instead of about 4ns + -- clear of both. metro_timing.xdc carries the arithmetic. + resize_axil(fabric_responders(ESPI1_RESP_IDX), responders_15b(ESPI1_RESP_IDX)); + resize_axil(fabric_responders(VERSAL_FLASH_RESP_IDX), responders_8b(VERSAL_FLASH_RESP_IDX)); + espi1_uart_from_axi_st.ready <= '1'; + espi1_uart_to_axi_st.valid <= '0'; + espi1_uart_to_axi_st.data <= (others => '0'); + espi1_versal_flash_ss: entity work.sp5_espi_flash_subsystem + generic map( + FLASH_WRITES_ALLOWED => true, + POST_CODE_BUFFER_ENABLED => false, + SPI_NOR_SCLK_DIVISOR => 1, + SPI_NOR_RX_SAMPLE_TAPS => 2 + ) + port map( + clk_125m => clk_125m, + reset_125m => reset_125m, + clk_200m => clk_200m, + reset_200m => reset_200m, + espi_axi_if => responders_15b(ESPI1_RESP_IDX), + espi_csn => espi1_sp5_to_fpga1_cs_l, + espi_clk => espi1_sp5_to_fpga1_clk, + espi_dat => espi1_sp5_to_fpga1_dat, + espi_dat_o => espi1_io_o, + espi_dat_oe => espi1_io_oe, + response_csn => open, + ipcc_uart_from_espi => espi1_uart_from_axi_st, + ipcc_uart_to_espi => espi1_uart_to_axi_st, + spinor_axi_if => responders_8b(VERSAL_FLASH_RESP_IDX), + spi_nor_csn => qspi_fpga1_to_vercel_flash_mux_cs_l, + spi_nor_clk => qspi_fpga1_to_vercel_flash_mux_sck, + spi_nor_dat => qspi_fpga1_to_vercel_flash_mux_d, + spi_nor_dat_o => vercel_flash_io_o, + spi_nor_dat_oe => vercel_flash_io_oe, + spi_nor_bus_enable => versal_flash_bus_enable, + hash_cmd_fifo_rdata => hash_flash_cmd_rdata, + hash_cmd_fifo_rdack => hash_flash_cmd_rdack(1), + hash_cmd_fifo_rempty => hash_flash_cmd_rempty(1), + hash_data_fifo_wdata => hash_flash_rsp_wdata(15 downto 8), + hash_data_fifo_write => hash_flash_rsp_write(1) + ); + + --Tristates for spi-nor flash pins and espi + spi_nor_espi_tris:process(all) + begin + for i in spi_fpga1_to_flash_dat'range loop + spi_fpga1_to_flash_dat(i) <= spinor_io_o(i) when spinor_io_oe(i) = '1' else 'Z'; + espi0_sp5_to_fpga1_dat(i) <= espi_io_o(i) when espi_io_oe(i) = '1' else 'Z'; + espi1_sp5_to_fpga1_dat(i) <= espi1_io_o(i) when espi1_io_oe(i) = '1' else 'Z'; + end loop; + end process; + + -- UART subsystem + -- stuff externally synchronized inside the UART block(s) + sp5_uart_ss: entity work.sp5_uart_subsystem + port map( + clk => clk_125m, + reset => reset_125m, + dbg_if => uart_dbg_if, + in_a0 => a0_ok, + -- UART pins + -- IPCC SP side + ipcc_from_sp => uart1_sp_to_fpga1_dat, + ipcc_to_sp => uart1_fpga1_to_sp_dat, + ipcc_from_sp_rts_l => uart1_sp_to_fpga1_rts_l, + ipcc_to_sp_rts_l => uart1_fpga1_to_sp_rts_l, + -- UART0 SP-side + console_from_sp => uart0_sp_to_fpga1_dat, + console_to_sp_dat => uart0_fpga1_to_sp_dat, + console_to_sp_rts_l => uart0_fpga1_to_sp_rts_l, + console_from_sp_rts_l => uart0_sp_to_fpga1_rts_l, + -- UART0 SP5-side + host_from_fpga => uart0_fpga1_to_sp5_dat_buff, + host_to_fpga => uart0_sp5_to_fpga1_dat, + host_from_fpga_rts_l => uart0_fpga1_to_sp5_rts_l_buff, + host_to_fpga_rts_l => uart0_sp5_to_fpga1_rts_l, + uart_from_fpga => open, + uart_to_fpga => '1', + uart_from_fpga_rts_l => open, + uart_to_fpga_rts_l => '0', + -- IPCC "UART" from espi + ipcc_from_espi => ipcc_uart_from_espi_axi_st, + ipcc_to_espi => ipcc_uart_to_espi_axi_st, + -- + dbg_pins_uart_out => dbg_pins_uart_out, + dbg_pins_uart_out_rts_l => dbg_pins_uart_out_rts_l, + dbg_pins_uart_in => dbg_pins_uart_in, + dbg_pins_uart_in_rts_l => dbg_pins_uart_in_rts_l + ); + + -- Metro has a dedicated debug UART header, so there is no fallback onto the + -- spare pins and no rev-conditional NIC power-good remap the way cosmo has. + uart_fpga1_to_debug_dat <= dbg_pins_uart_out; + dbg_pins_uart_in <= uart_debug_to_fpga1_dat; + uart_fpga1_to_debug_rts_l <= dbg_pins_uart_in_rts_l; + dbg_pins_uart_out_rts_l <= uart_debug_to_fpga1_rts_l; + fpga1_spare_v3p3_6 <= 'Z'; + fpga1_spare_v3p3_7 <= 'Z'; + + -- UART8 is a Metro addition: a fifth SP-facing UART, data only, with no + -- flow control and no peer defined on the schematic. Park it until we know + -- what it is meant to carry, rather than guessing at a mapping. + uart8_fpga1_to_sp_dat <= '1'; + + -- SP I2C muxes + -- i2c is the only input, sycn'd inside the mux block(s) + resize_axil(fabric_responders(SP_I2C_RESP_IDX), responders_8b(SP_I2C_RESP_IDX)); + sp_i2c_subsystem_inst: entity work.sp_i2c_subsystem + port map( + clk => clk_125m, + reset => reset_125m, + in_a0 => a0_ok, + sp_mux_reset_l => sp_mux_reset_l_syncd, + axi_if => responders_8b(SP_I2C_RESP_IDX), + sp_scl => i2c_sp_to_fpga1_scl, + sp_scl_o => sp_scl_o, + sp_scl_oe => sp_scl_oe, + sp_sda => i2c_sp_to_fpga1_sda, + sp_sda_o => sp_sda_o, + sp_sda_oe => sp_sda_oe, + i2c_mux1_sel => fpga1_to_i2c_mux1_sel, + i2c_mux2_sel => fpga1_to_i2c_mux2_sel, + i2c_mux3_sel => fpga1_to_i2c_mux3_sel, + -- cosmo drives an M.2 translator enable off mux1; metro's translator + -- enable is tied to SP5 power good above, so leave this open. + i2c_mux1_en => open + ); + --Tristates for spi-nor flash pins and espi + i2c_sp_to_fpga1_scl <= sp_scl_o when sp_scl_oe = '1' else 'Z'; + i2c_sp_to_fpga1_sda <= sp_sda_o when sp_sda_oe = '1' else 'Z'; + + -- SP5 I2c hotplug expanders + -- Inputs synchronized inside the block + resize_axil(fabric_responders(SP5_HP_RESP_IDX), responders_8b(SP5_HP_RESP_IDX)); + sp5_hotplug_subsystem_inst: entity work.sp5_hotplug_subsystem + generic map( + NIC2_SLOT_ENABLED => true + ) + port map( + clk => clk_125m, + reset => reset_125m, + axi_if => responders_8b(SP5_HP_RESP_IDX), + allow_backplane_pcie_clk => allow_backplane_pcie_clk, + sp5_i2c_sda => i2c_sp5_to_fpgax_hp_sda, + sp5_i2c_sda_o => sp5_sda_o, + sp5_i2c_sda_oe => sp5_sda_oe, + sp5_i2c_scl => i2c_sp5_to_fpgax_hp_scl, + sp5_i2c_scl_o => sp5_scl_o, + sp5_i2c_scl_oe => sp5_scl_oe, + int_n => hp_int_n, + a0_ok => a0_ok, + m2a_pedet => m2a_to_fpga1_pedet, + m2a_prsnt_l => m2a_to_fpga1_prsnt_l, + m2a_hsc_en => fpga1_to_m2a_hsc_en, + m2a_perst_l => fpga1_to_m2a_perst_l, + m2a_pwr_fault_l => m2a_hsc_to_fpga1_fault_l, + pcie_clk_buff_m2a_oe_l => fpga1_to_pcie_clk_buff_m2a_oe_l, + m2b_pedet => m2b_to_fpga1_pedet, + m2b_prsnt_l => m2b_to_fpga1_prsnt_l, + m2b_hsc_en => fpga1_to_m2b_hsc_en, + m2b_perst_l => fpga1_to_m2b_perst_l, + m2b_pwr_fault_l => m2b_hsc_to_fpga1_fault_l, + pcie_clk_buff_m2b_oe_l => fpga1_to_pcie_clk_buff_m2b_oe_l, + -- The Versal takes the slot the T6 has on cosmo for its channel A, + -- and the otherwise unused fifth expander bank for channel B. Both + -- are the one device, so both slots report the same fault; each has + -- its own presence and its own PERST. + t6_power_en => sp5_versal_power_en, + t6_perst_l => sp5_versal_cha_perst_l, + t6_faulted => sp5_versal_faulted, + t6_prsnt_l => pcie_nic_to_fpga1_cha_prsnt_l, + nic2_power_en => open, + nic2_perst_l => sp5_versal_chb_perst_l, + nic2_faulted => sp5_versal_faulted, + nic2_prsnt_l => pcie_nic_to_fpga1_chb_prsnt_l, + pcie_aux_rsw_perst_l => pcie_aux_fpga1_to_rsw_perst_l, + pcie_aux_rsw_prsnt_buff_l => pcie_aux_rsw_to_fpga1_prsnt_buff_l, + pcie_aux_rsw_pwrflt_buff_l=> pcie_aux_rsw_to_fpga1_pwrflt_buff_l, + pcie_clk_buff_rsw_oe_l => fpga1_to_pcie_clk_buff_rsw_oe_l_int, + rsw_sp5_pcie_attached_buff_l =>rsw_to_sp5_pcie_attached_buff_l + ); + + a0_ok_to_fpga2 <= a0_ok; -- A0 OK signal to fpga2, used for power sequencing + + amd_hp_irq_n_final <= hp_int_n; + sp5_to_fpga1_genint_l <= '0' when amd_hp_irq_n_final = '0' else 'Z'; + + fpga1_to_pcie_clk_buff_rsw_oe_l <= '0' when fpga1_to_pcie_clk_buff_rsw_oe_l_int = '0' else 'Z'; + + --Tristates for spi-nor flash pins and espi + i2c_sp5_to_fpgax_hp_scl <= sp5_scl_o when sp5_scl_oe = '1' else 'Z'; + i2c_sp5_to_fpgax_hp_sda <= sp5_sda_o when sp5_sda_oe = '1' else 'Z'; + + --Block that deals with sequencing the SP5 and nic etc + -- inputs synchronized inside the block + resize_axil(fabric_responders(SEQ_RESP_IDX), responders_8b(SEQ_RESP_IDX)); + seq: entity work.sp5_sequencer + generic map( + CNTS_P_MS => calc_ms(desired_ms => 1, clk_period_ns => 8), + NIC_KIND => NIC_VERSAL + ) + port map( + clk => clk_125m, + reset => reset_125m, + axi_if => responders_8b(SEQ_RESP_IDX), + a0_ok => a0_ok, + a0_idle => a0_idle, + irq_l_out => fpga1_to_sp_irq_l(1), + allow_backplane_pcie_clk => allow_backplane_pcie_clk, + early_power_pins => early_power, + ddr_bulk_pins => ddr_bulk, + group_a_pins => sp5_group_a, + group_b_pins => sp5_group_b, + group_c_pins => sp5_group_c, + sp5_seq_pins => sp5_seq_pins, + versal_rails_pins => versal_rails, + versal_boot_pins => versal_boot, + versal_pcie_pins => versal_pcie, + nic_dbg_pins => versal_dbg_pins, + nic_rails_pins => nic_rails_unused, + nic_seq_pins => nic_seq_pins_unused, + versal_held_in_reset => versal_held_in_reset, + flash_owned_by_seq => flash_owned_by_seq, + hash_req => versal_hash_req, + hash_ack => versal_hash_ack, + hash_err => versal_hash_err, + version_id => fpga1_version_id, + sp5_nic_perst_l => sp5_versal_cha_perst_l, + sp5_nic_chb_perst_l => sp5_versal_chb_perst_l, + sp5_nic_faulted => sp5_versal_faulted, + ignition_mux_sel => fpga1_to_sp_mux_ign_mux_sel, + ignition_creset => fpga1_to_ign_trgt_fpga_creset, + reg_alert_l_pins => reg_alert_l_pins + ); + + -- early power related pins + early_power.fan_central_hsc_pg <= fan_central_hsc_to_fpga1_pg; + early_power.fan_east_hsc_pg <= fan_east_hsc_to_fpga1_pg; + early_power.fan_west_hsc_pg <= fan_west_hsc_to_fpga1_pg; + early_power.fan_fail <= fan_to_fpga1_fan_fail; + fpga1_to_fan_central_hsc_disable <= early_power.fan_central_hsc_disable; + fpga1_to_fan_east_hsc_disable <= early_power.fan_east_hsc_disable; + fpga1_to_fan_west_hsc_disable <= early_power.fan_west_hsc_disable; + + -- Bulk DDR power control and HSC readback + ddr_bulk.abcdef_hsc.pg <= v12_ddr5_abcdef_a0_pg; + fpga1_to_v12_ddr5_abcdef_hsc_en <= ddr_bulk.abcdef_hsc.enable; + ddr_bulk.ghijkl_hsc.pg <= v12_ddr5_ghijkl_a0_pg; + fpga1_to_v12_ddr5_ghijkl_hsc_en <= ddr_bulk.ghijkl_hsc.enable; + -- SP5 rails + -- group A enables and PGs + pwr_fpga1_to_v1p5_sp5_rtc_a2_en <= sp5_group_a.pwr_v1p5_rtc.enable; + sp5_group_a.pwr_v1p5_rtc.pg <= pwr_v1p5_sp5_rtc_a2_to_fpga1_pg; + v3p3_sp5_en <= sp5_group_a.v3p3_sp5_a1.enable; + sp5_group_a.v3p3_sp5_a1.pg <= v3p3_sp5_pg; + v1p8_sp5_en <= sp5_group_a.v1p8_sp5_a1.enable; + sp5_group_a.v1p8_sp5_a1.pg <= v1p8_sp5_pg; + -- group B enable and pg + v1p1_sp5_en <= sp5_group_b.v1p1_sp5.enable; + sp5_group_b.v1p1_sp5.pg <= v1p1_sp5_pg; + -- group C enables and pgs + vddio_sp5_en <= sp5_group_c.vddio_sp5_a0.enable; + sp5_group_c.vddio_sp5_a0.pg <= pwr_cont1_to_fpga1_vddio_sp5_pg; + vddcr_cpu1_en <= sp5_group_c.vddcr_cpu1.enable; + sp5_group_c.vddcr_cpu1.pg <= pwr_cont1_to_fpga1_vddcr_cpu1_pg; + vddcr_cpu0_en <= sp5_group_c.vddcr_cpu0.enable; + sp5_group_c.vddcr_cpu0.pg <= pwr_cont1_to_fpga1_vddcr_cpu0_pg; + vddcr_soc_en <= sp5_group_c.vddcr_soc.enable; + sp5_group_c.vddcr_soc.pg <= pwr_cont1_to_fpga1_vddcr_soc_pg; + -- Versal rails. Every rail here has its own enable, unlike cosmo's T6 + -- where a single hotswap enable cascaded the lot. + -- + -- The two hotswap power goods are active low at the pin, despite the port + -- names, which follow cosmo_seq's. Pass them through raw: versal_sync is + -- the single place that inverts them, exactly as seq_sync does on cosmo. + -- Inverting here as well would double up and hand the sequencer the + -- opposite of the truth. + fpga1_to_nic_hsc_en <= versal_rails.hsc_12v.enable; + versal_rails.hsc_12v.pg <= v12p0_nic_a0hp_pg; + versal_rails.hsc_5v.pg <= v5p0_nic_a0hp_pg; + v3p3_nic_a0hp_en <= versal_rails.v3p3.enable; + versal_rails.v3p3.pg <= v3p3_nic_a0hp_pg; + v1p8_nic_a0hp_en <= versal_rails.v1p8.enable; + versal_rails.v1p8.pg <= v1p8_nic_a0hp_pg; + v1p5_nic_a0hp_en <= versal_rails.v1p5.enable; + versal_rails.v1p5.pg <= v1p5_nic_a0hp_pg; + v1p5_nic_avccaux_a0hp_en <= versal_rails.v1p5_avccaux.enable; + versal_rails.v1p5_avccaux.pg <= v1p5_nic_avccaux_a0hp_pg; + v1p4_nic_a0hp_en <= versal_rails.v1p4.enable; + -- There is no power-good pin for the 1V4 rail on this board, so mirror its + -- enable the way cosmo does for its rev1 1V4. + versal_rails.v1p4.pg <= versal_rails.v1p4.enable; + v1p1_nic_a0hp_en <= versal_rails.v1p1.enable; + -- Likewise no discrete 1V1 power good; it comes up with the aux group. + versal_rails.v1p1.pg <= versal_rails.v1p1.enable; + v0p88_nic_a0hp_en <= versal_rails.v0p88.enable; + versal_rails.v0p88.pg <= v0p88_nic_a0hp_pg; + v0p8_nic_vccint_a0hp_en <= versal_rails.v0p8_vccint.enable; + versal_rails.v0p8_vccint.pg <= v0p8_nic_vccint_a0hp_pg; + -- Transceiver rails: readback only, they cascade off the groups above. + versal_rails.v0p92_avcc.pg <= v0p92_nic_avcc_a0hp_pg; + versal_rails.v1p2_avtt.pg <= v1p2_nic_avtt_a0hp_pg; + + -- SP5 sequence-related pins + sp5_seq_pins.thermtrip_l <= sp5_to_fpga1_thermtrip_l; + sp5_seq_pins.smerr_l <= sp5_to_fpga1_smerr_l; + sp5_seq_pins.reset_l <= fpga1_to_sp5_reset_l; + sp5_seq_pins.pwr_ok <= sp5_to_fpga1_pwrok_unbuf; + fpga1_to_sp5_pwr_btn_l <= '0' when sp5_seq_pins.pwr_btn_l = '0' else 'Z'; + sp5_seq_pins.slp_s3_l <= sp5_to_fpga1_slp_s3_l; + sp5_seq_pins.slp_s5_l <= sp5_to_fpga1_slp_s5_l; + fpga1_to_sp5_rsmrst_l <= sp5_seq_pins.rsmrst_l; + -- Board-type strap the SP5 samples at power up. a1_a0_seq drives its + -- is_cosmo output high here because it is cosmo's block; Metro is not + -- cosmo, so hold the pin low and leave that output unread until Metro's own + -- board-identity convention is settled with the SP5 firmware. + sp5_to_fpga1_debug1 <= '0'; + fpga1_to_sp5_pwrgd <= sp5_seq_pins.pwr_good; + + -- Versal boot straps and status + fpga1_to_versal_por_b <= versal_boot.por_b; + fpga1_to_versal_mode <= versal_boot.mode; + fpga1_to_versal_mode_buffer_en_l <= versal_boot.mode_buffer_en_l; + fpga1_to_versal_erro_done_buff_en <= versal_boot.err_done_buff_en; + versal_boot.done <= versal_to_fpga1_done; + versal_boot.error_out <= versal_to_fpga1_error_out; + + -- Versal PCIe, two channels. The clock buffer output enables are open + -- drain on this board, same as the M.2 and backplane ones. + pcie_fpga1_to_nic_cha_perst_l <= versal_pcie.cha.perst_l; + versal_pcie.cha.prsnt_l <= pcie_nic_to_fpga1_cha_prsnt_l; + versal_pcie.cha.pwren_l <= pcie_nic_to_fpga1_cha_pwren_l; + fpga1_to_pcie_clk_buff_nic_cha_oe_l <= '0' when versal_pcie.cha.clk_buff_oe_l = '0' else 'Z'; + pcie_fpga1_to_nic_chb_perst_l <= versal_pcie.chb.perst_l; + versal_pcie.chb.prsnt_l <= pcie_nic_to_fpga1_chb_prsnt_l; + versal_pcie.chb.pwren_l <= pcie_nic_to_fpga1_chb_pwren_l; + fpga1_to_pcie_clk_buff_nic_chb_oe_l <= '0' when versal_pcie.chb.clk_buff_oe_l = '0' else 'Z'; + + -- Versal boot flash mux on sheet 137. The controller behind it is the + -- eSPI1 wrapper's spi_nor above; this block only gates its pins on the + -- mux being granted, which needs the Versal held in reset. + resize_axil(fabric_responders(VERSAL_FLASH_CTRL_RESP_IDX), responders_8b(VERSAL_FLASH_CTRL_RESP_IDX)); + versal_flash_ss: entity work.versal_flash_subsystem + port map( + clk => clk_125m, + reset => reset_125m, + ctrl_axi_if => responders_8b(VERSAL_FLASH_CTRL_RESP_IDX), + versal_held_in_reset => versal_held_in_reset, + flash_owned_by_seq => flash_owned_by_seq, + flash_qspi_mux_sel => fpga1_to_vercel_flash_qspi_mux_sel, + flash_qspi_mux_en_l => fpga1_to_vercel_flash_qspi_mux_en_l, + flash_bus_enable => versal_flash_bus_enable + ); + vercel_flash_tris: process(all) + begin + for i in qspi_fpga1_to_vercel_flash_mux_d'range loop + qspi_fpga1_to_vercel_flash_mux_d(i) <= + vercel_flash_io_o(i) when vercel_flash_io_oe(i) = '1' else 'Z'; + end loop; + end process; + + -- SP5 <-> FPGA busses (filtered in proxy block) + i3c_sp5_to_fpga1_abcdef_scl <= sp5_abcdef_scl_if.o when sp5_abcdef_scl_if.oe else 'Z'; + sp5_abcdef_scl_if.i <= i3c_sp5_to_fpga1_abcdef_scl; + i3c_sp5_to_fpga1_abcdef_sda <= sp5_abcdef_sda_if.o when sp5_abcdef_sda_if.oe else 'Z'; + sp5_abcdef_sda_if.i <= i3c_sp5_to_fpga1_abcdef_sda; + i3c_sp5_to_fpga1_ghijkl_scl <= sp5_ghijkl_scl_if.o when sp5_ghijkl_scl_if.oe else 'Z'; + sp5_ghijkl_scl_if.i <= i3c_sp5_to_fpga1_ghijkl_scl; + i3c_sp5_to_fpga1_ghijkl_sda <= sp5_ghijkl_sda_if.o when sp5_ghijkl_sda_if.oe else 'Z'; + sp5_ghijkl_sda_if.i <= i3c_sp5_to_fpga1_ghijkl_sda; + -- FPGA <-> DIMMs busses (filtered in proxy block) + i3c_fpga1_to_dimm_abcdef_scl <= dimm_abcdef_scl_if.o when dimm_abcdef_scl_if.oe else 'Z'; + dimm_abcdef_scl_if.i <= i3c_fpga1_to_dimm_abcdef_scl; + i3c_fpga1_to_dimm_abcdef_sda <= dimm_abcdef_sda_if.o when dimm_abcdef_sda_if.oe else 'Z'; + dimm_abcdef_sda_if.i <= i3c_fpga1_to_dimm_abcdef_sda; + i3c_fpga1_to_dimm_ghijkl_scl <= dimm_ghijkl_scl_if.o when dimm_ghijkl_scl_if.oe else 'Z'; + dimm_ghijkl_scl_if.i <= i3c_fpga1_to_dimm_ghijkl_scl; + i3c_fpga1_to_dimm_ghijkl_sda <= dimm_ghijkl_sda_if.o when dimm_ghijkl_sda_if.oe else 'Z'; + dimm_ghijkl_sda_if.i <= i3c_fpga1_to_dimm_ghijkl_sda; + + + reg_alert_l_pins.smbus_fan_central_hsc_to_fpga1_alert_l <= smbus_fan_central_hsc_to_fpga1_alert_l; + reg_alert_l_pins.smbus_fan_east_hsc_to_fpga1_alert_l <= smbus_fan_east_hsc_to_fpga1_alert_l; + reg_alert_l_pins.smbus_fan_west_hsc_to_fpga1_alert_l <= smbus_fan_west_hsc_to_fpga1_alert_l; + reg_alert_l_pins.smbus_ibc_to_fpga1_alert_l <= smbus_ibc_to_fpga1_alert_l; + reg_alert_l_pins.smbus_m2_hsc_to_fpga1_alert_l <= smbus_m2_hsc_to_fpga1_alert_l; + reg_alert_l_pins.smbus_nic_hsc_to_fpga1_alert_l <= smbus_nic_hsc_to_fpga1_alert_l; + reg_alert_l_pins.smbus_v12_ddr5_abcdef_hsc_to_fpga1_alert <= smbus_v12_ddr5_abcdef_hsc_to_fpga1_alert; + reg_alert_l_pins.smbus_v12_ddr5_ghijkl_hsc_to_fpga1_alert <= smbus_v12_ddr5_ghijkl_hsc_to_fpga1_alert; + -- cosmo-only alert pins, absent here + reg_alert_l_pins.smbus_v12_mcio_a0hp_hsc_to_fpga1_alert_l <= '1'; + reg_alert_l_pins.v0p96_nic_to_fpga1_alert_l <= '1'; + reg_alert_l_pins.main_hsc_to_fpga1_alert_l <= main_hsc_to_fpga1_alert_l; + reg_alert_l_pins.vr_v1p8_sys_to_fpga1_alert_l <= vr_v1p8_sys_to_fpga1_alert_l; + reg_alert_l_pins.vr_v3p3_sys_to_fpga1_alert_l <= vr_v3p3_sys_to_fpga1_alert_l; + reg_alert_l_pins.vr_v5p0_sys_to_fpga1_alert_l <= vr_v5p0_sys_to_fpga1_alert_l; + reg_alert_l_pins.pwr_cont1_to_fpga1_alert_l <= pwr_cont1_to_fpga1_alert_l; + reg_alert_l_pins.pwr_cont2_to_fpga1_alert_l <= pwr_cont2_to_fpga1_alert_l; + reg_alert_l_pins.pwr_cont3_to_fpga1_alert_l <= pwr_cont3_to_fpga1_alert_l; + reg_alert_l_pins.pwr_cont4_to_fpga1_alert_l <= pwr_cont4_to_fpga1_alert_l; + + resize_axil(fabric_responders(SPD_PROXY_RESP_IDX), responders_8b(SPD_PROXY_RESP_IDX)); + dimm_spd_proxy_top_inst: entity work.dimms_subsystem_top + generic map( + CLK_PER_NS => 8, + I2C_MODE => FAST_PLUS + ) + port map( + clk => clk_125m, + reset => reset_125m, + axi_if => responders_8b(SPD_PROXY_RESP_IDX), + in_a0 => a0_ok, + dimm_a_pcamp => dimm_a_pg, + dimm_b_pcamp => dimm_b_pg, + dimm_c_pcamp => dimm_c_pg, + dimm_d_pcamp => dimm_d_pg, + dimm_e_pcamp => dimm_e_pg, + dimm_f_pcamp => dimm_f_pg, + dimm_g_pcamp => dimm_g_pg, + dimm_h_pcamp => dimm_h_pg, + dimm_i_pcamp => dimm_i_pg, + dimm_j_pcamp => dimm_j_pg, + dimm_k_pcamp => dimm_k_pg, + dimm_l_pcamp => dimm_l_pg, + cpu_scl_if0 => sp5_abcdef_scl_if, + cpu_sda_if0 => sp5_abcdef_sda_if, + cpu_scl_if1 => sp5_ghijkl_scl_if, + cpu_sda_if1 => sp5_ghijkl_sda_if, + dimm_scl_if0 => dimm_abcdef_scl_if, + dimm_sda_if0 => dimm_abcdef_sda_if, + dimm_scl_if1 => dimm_ghijkl_scl_if, + dimm_sda_if1 => dimm_ghijkl_sda_if + ); + + resize_axil(fabric_responders(DBG_CTRL_RESP_IDX), responders_8b(DBG_CTRL_RESP_IDX)); + debug_module_top_inst: entity work.debug_module_top + port map( + clk_200m => clk_200m, + reset_200m => reset_200m, + clk => clk_125m, + reset => reset_125m, + axi_if => responders_8b(DBG_CTRL_RESP_IDX), + in_a0 => a0_ok, + -- Metro has no FPGA2 hotplug IRQ arriving here, so tie the tap idle. + fpga2_hp_irq_n => '1', + hp_int_n => hp_int_n, + sp5_debug2_pin => sp5_to_fpga1_debug2, + uart_headder_fall_back_to_debug_pins => uart_headder_fall_back_to_debug_pins, + uart_dbg_if => uart_dbg_if, + -- hotplug + i2c_sp5_to_fpgax_hp_sda => i2c_sp5_to_fpgax_hp_sda, + i2c_sp5_to_fpgax_hp_scl => i2c_sp5_to_fpgax_hp_scl, + -- sp + i2c_sp_to_fpga1_scl => i2c_sp_to_fpga1_scl, + i2c_sp_to_fpga1_sda => i2c_sp_to_fpga1_sda, + -- sp5 i2c + i2c_sp5_sec_to_fpga1_scl => i2c_sp5_sec_v3p3_scl, + i2c_sp5_sec_to_fpga1_sda => i2c_sp5_sec_v3p3_sda, + -- dimms + i3c_sp5_to_fpga1_abcdef_scl => i3c_sp5_to_fpga1_abcdef_scl, + i3c_sp5_to_fpga1_abcdef_sda => i3c_sp5_to_fpga1_abcdef_sda, + i3c_sp5_to_fpga1_ghijkl_scl => i3c_sp5_to_fpga1_ghijkl_scl, + i3c_sp5_to_fpga1_ghijkl_sda => i3c_sp5_to_fpga1_ghijkl_sda, + i3c_fpga1_to_dimm_abcdef_scl => i3c_fpga1_to_dimm_abcdef_scl, + i3c_fpga1_to_dimm_abcdef_sda => i3c_fpga1_to_dimm_abcdef_sda, + i3c_fpga1_to_dimm_ghijkl_scl => i3c_fpga1_to_dimm_ghijkl_scl, + i3c_fpga1_to_dimm_ghijkl_sda => i3c_fpga1_to_dimm_ghijkl_sda, + -- UARTs + uart1_sp_to_fpga1_dat => uart1_sp_to_fpga1_dat, + uart1_fpga1_to_sp_dat => uart1_fpga1_to_sp_dat, + uart0_sp_to_fpga1_dat => uart0_sp_to_fpga1_dat, + uart0_fpga1_to_sp_dat => uart0_fpga1_to_sp_dat, + uart0_fpga1_to_sp5_dat => uart0_fpga1_to_sp5_dat_buff, + uart0_sp5_to_fpga1_dat => uart0_sp5_to_fpga1_dat, + -- ESPI signals + espi0_sp5_to_fpga_clk => espi0_sp5_to_fpga1_clk, + espi0_sp5_to_fpga_cs_l => espi0_sp5_to_fpga1_cs_l, + espi0_sp5_to_fpga1_dat => espi0_sp5_to_fpga1_dat, + espi_resp_csn => espi_resp_csn, + nic_dbg_pins => versal_dbg_pins, + -- MUX + mux1_sel => fpga1_to_i2c_mux1_sel, + mux2_sel => fpga1_to_i2c_mux2_sel, + mux3_sel => fpga1_to_i2c_mux3_sel, + + fpga1_spare_v1p8 => fpga1_spare_v1p8 + ); + + +end rtl; diff --git a/hdl/projects/metro_seq/metro_timing.xdc b/hdl/projects/metro_seq/metro_timing.xdc new file mode 100644 index 00000000..154161e7 --- /dev/null +++ b/hdl/projects/metro_seq/metro_timing.xdc @@ -0,0 +1,354 @@ +# Timing constraints for the Metro sequencer FPGA. +# +# Ported from cosmo_seq's cosmo_timing.xdc. The two boards run the same blocks +# at the same rates, so the structure and the reasoning in the comments carry +# over; what is board specific is the port names, the PLL-derived clock names, +# and the FMC trace-delay numbers flagged below. + +create_clock -add -name sys_clk_pin -period 20.000 -waveform {0 10.000} [get_ports { clk_50mhz_fpga1_1 }]; +create_clock -add -name fmc_clk_pin -period 10.000 -waveform {0 5.000} [get_ports { fmc_sp_to_fpga1_clk }]; + +# +# FMC interface constraints +# Create a virtual clock, to represent the source clock of the FMC interface +create_clock -name fmc_virt_clk -period 10.000; + +# The FMC MMCM's output clock is derived from fmc_clk_pin and must stay in +# the synchronous group with it and the virtual clock. +set_clock_groups -asynchronous -group [get_clocks -include_generated_clocks {fmc_clk_pin fmc_virt_clk}] -group {clk_125m_sys_pll} -group {clk_200m_sys_pll} + + +# ####################### +# FMC Interface +# ####################### + +# SP outputs a continuous clock here. +# The FMC interface is constrained at 100MHz (10ns period), the CLKDIV=1 +# target rate; the same analysis is a strict superset of 50 and 66.67 MHz +# operation. The internal FMC domain clock comes from an MMCM in +# phase-alignment mode (see xilinx_ip_gen/fmc_pll_ip.tcl), so the flops +# see the pin clock plus the deliberate +45deg phase shift and STA +# accounts for it via the generated clock. +# FPGA's input delays have to be low enough that they don't run into the +# uncertainty region due to any possible skew. skew_bre is the shortest trace +# delay vs the clock, and skew_are is the longest trace delay vs the clock. +# +# !!! The six delays below are cosmo's, carried over unchanged. Metro's FMC +# !!! routing has not been measured yet, so these are a placeholder that keeps +# !!! the constraint structure meaningful, not a description of this board. +# !!! Re-derive them from Metro's trace lengths (clock, longest and shortest +# !!! data, and the wait line) before trusting a timing report for hardware. +# !!! The conversion is length_mm * 6.8ns/m. + +set max_clock_delay 0.4134 +set min_clock_delay 0.3602 +set max_data_delay 0.4186 +set min_data_delay 0.3573 +set max_wait_delay 0.4134 +set min_wait_delay 0.3635 + +# ################# +# Input constraints. +# Effectively longest data delay, fastest clock arrival at FPGA. +# input_max = clk_ext_delay_max + extTco_max + board_delay_max - fpga_clk_delay_min +# Effectively shortest data delay, slowest clock arrival at FPGA. +# input_min = clk_ext_delay_min + extTco_min + board_delay_min - fpga_clk_delay_max + +# For the inputs data valid before rising edge can be calculated based on the SP's datasheet timings and trace delays. +# td(CLKL-NExL) clock to out is max 1ns +# td(CLKH_NExH) is min +# td(CLKL-AV) 2.5ns +# td(CLKH-AIV) 8ns? +# td(CLKL-NOEL) 1.5ns +# td(CLKH-NOEH) 7.5ns +# td(CLKL-ADV) 3 ns +# td(CLKL-ADIV) 0 ns +# tsu(ADV-CLKH) 3 ns +# th(CLKH-ADV) 0 +# tsu(NWAIT-CLKH) 3 ns (worst read timing) +# th(CLKH-NWAIT) 2 ns (worst write timing) + +# Source sync so external_clk_delay is 0. +# Setup time is 1ns, and we include the 1/2 period due to SP shifting the data out on the falling edge. +set sp_output_half_period 5.0 +set sp_0_hold 0 +set sp_clk_delay 0 + +# We have our 1/2 period of 5 ns due to SP outputting on falling edges, plus the td in the datasheet +set td_clkl_nehl 1 +set nl_output_delay [expr {$sp_output_half_period + $td_clkl_nehl}] +set max_nl [expr {$sp_clk_delay + $nl_output_delay + $max_data_delay - $min_clock_delay}] +# latest clock, earliest data. We assume a hold time of 0 for the SP, but the SP +# holds its outputs until the *next falling edge*, so the earliest change is a +# half period after the capture edge. Without that term Vivado assumes the data +# can change at the capture edge itself, which manufactures a phantom +# input-hold requirement of nearly a half period. +# min external: fastest data, slowest clock +set min_nl [expr {$sp_clk_delay + $sp_output_half_period + $sp_0_hold + $min_data_delay - $max_clock_delay}] + +# Apply to all of these pins with similar or better timing relationships. +set_input_delay -clock fmc_virt_clk -max $max_nl [get_ports fmc_sp_to_fpga1_cs_l] +set_input_delay -clock fmc_virt_clk -min $min_nl [get_ports fmc_sp_to_fpga1_cs_l] +set_input_delay -clock fmc_virt_clk -max $max_nl [get_ports fmc_sp_to_fpga1_we_l] +set_input_delay -clock fmc_virt_clk -min $min_nl [get_ports fmc_sp_to_fpga1_we_l] +set_input_delay -clock fmc_virt_clk -max $max_nl [get_ports fmc_sp_to_fpga1_oe_l] +set_input_delay -clock fmc_virt_clk -min $min_nl [get_ports fmc_sp_to_fpga1_oe_l] +set_input_delay -clock fmc_virt_clk -max $max_nl [get_ports fmc_sp_to_fpga1_adv_l] +set_input_delay -clock fmc_virt_clk -min $min_nl [get_ports fmc_sp_to_fpga1_adv_l] +set_input_delay -clock fmc_virt_clk -max $max_nl [get_ports fmc_sp_to_fpga1_bl_l] +set_input_delay -clock fmc_virt_clk -min $min_nl [get_ports fmc_sp_to_fpga1_bl_l] + +# Address has diff relationship 2.5ns (max tco) +set td_clkl_av 2.5 +set a_output_delay [expr {$sp_output_half_period + $td_clkl_av}] +set max_a [expr {$sp_clk_delay + $a_output_delay + $max_data_delay - $min_clock_delay}] +# Still 0 hold on these pins, held to the next falling edge as above. +set min_a [expr {$sp_clk_delay + $sp_output_half_period + $sp_0_hold + $min_data_delay - $max_clock_delay}] +set_input_delay -clock fmc_virt_clk -max $max_a [get_ports fmc_sp_to_fpga1_a[*]] +set_input_delay -clock fmc_virt_clk -min $min_a [get_ports fmc_sp_to_fpga1_a[*]] + +# Data in has diff relationship 3ns (max tco) +set td_clkl_adv 3 +set ad_output_delay [expr {$sp_output_half_period + $td_clkl_adv}] +set max_ad [expr {$sp_clk_delay + $ad_output_delay + $max_data_delay - $min_clock_delay}] +# Still 0 hold on these pins, held to the next falling edge as above. +set min_ad [expr {$sp_clk_delay + $sp_output_half_period + $sp_0_hold + $min_data_delay - $max_clock_delay}] +set_input_delay -clock fmc_virt_clk -max $max_ad [get_ports fmc_sp_to_fpga1_da[*]] +set_input_delay -clock fmc_virt_clk -min $min_ad [get_ports fmc_sp_to_fpga1_da[*]] + +# The MMCM's +45deg phase shift puts the internal capture edge at 1.25 ns, +# and STA's default edge relationship then times input paths from the +# virtual-clock launch at 0 to that 1.25 ns edge -- a nonsense 1.25 ns +# requirement. The intended capture edge is the *next* shifted edge at +# 11.25 ns, which is exactly what setup-2 selects. The default hold +# relationship (one cycle before the setup edge, back at 1.25 ns) is the +# right check and passes with the half-period of real SP hold, so no -hold +# adjustment. Output paths need nothing: launch at 1.25 ns against the SP's +# capture at 10 ns is already the correct single-cycle relationship. +set_multicycle_path 2 -setup -from [get_clocks fmc_virt_clk] -to [get_clocks *fmc_pll*] +set_multicycle_path 2 -setup -from [get_clocks fmc_clk_pin] -to [get_clocks *fmc_pll*] + +#### END Of inputs + +# ################# +# Output constraints. +# Effectively need to meet setup time with longest FPGA data delay and fastest clock arrival at other device. +# output_max = fpga_clk_delay_max + board_delay_max + extTsu - ext_clk_delay_min +# Effectively need to meet hold time with shortest FPGA data delay and slowest clock arrival at other device. +# input_min = fpga_clk_delay_min + board_delay_min - extTh - ext_clk_delay_max + +# Ext setup time is 3ns +set tsu_nwait_clkh 3 +# SP rising edge samples so 0 clock delay at external device. +set max_wait [expr {$tsu_nwait_clkh + $max_wait_delay + $max_clock_delay - $sp_clk_delay}] +# Ext hold time is 2ns, still 0 clock delay at external device. +set th_clkh_nwait 2 +set min_wait [expr {$min_clock_delay + $min_wait_delay - $th_clkh_nwait - $sp_clk_delay}] +set_output_delay -clock fmc_virt_clk -max $max_wait [get_ports fmc_sp_to_fpga1_wait_l] +set_output_delay -clock fmc_virt_clk -min $min_wait [get_ports fmc_sp_to_fpga1_wait_l] + +# Ext setup time is 3ns +set tsu_adv_clkh 3 +# Ext hold time is 0ns +set th_clkh_adv 0 +# Still 0 clk delay at external device +set max_da [expr {$tsu_adv_clkh + $max_data_delay + $max_clock_delay - $sp_clk_delay}] +set min_da [expr {$min_data_delay - $th_clkh_adv + $min_clock_delay - $sp_clk_delay}] +set_output_delay -clock fmc_virt_clk -max $max_da [get_ports fmc_sp_to_fpga1_da[*]] +set_output_delay -clock fmc_virt_clk -min $min_da [get_ports fmc_sp_to_fpga1_da[*]] + + +# The streaming FSM presents read beats on consecutive cycles, so the +# word0->word1 transition is a true single-cycle path and no multicycle +# exception applies to the data pins. Single-cycle closes because the output +# and tristate flops pack into the IOBs: data_out_reg and data_out_hiz_int_reg +# are one flop per pin by construction, and nwait, a single-cycle path the SP +# samples every rising edge, gets the same treatment. If a board cannot close +# this way, set the extra_beat_setup generic and add setup-2/hold-1 exceptions +# on the data/tristate paths to match. +set_property IOB TRUE [get_cells -hier -filter {NAME =~ *stm32h7_fmc_target*/data_out_reg[*]}] +set_property IOB TRUE [get_cells -hier -filter {NAME =~ *stm32h7_fmc_target*/data_out_hiz_int_reg[*]}] +set_property IOB TRUE [get_cells -hier -filter {NAME =~ *stm32h7_fmc_target*/nwait_reg}] + +# End FMC + +set_false_path -from [get_ports {*}] -to [get_ports {fpga1_spare_v3p3*}] +set_false_path -from [get_ports {*}] -to [get_ports {fpga1_spare_v1p8[*]}] + +# ####################### +# eSPI Interface +# ####################### +# TODO: This is likely not correct but I need to re-write the link-layer logic again +# and then re-constrain +# 20MHz espi constraints, 50ns clock periods. +# ESPI interface has 2.2ns of trace delay +# AMD says 7ns of data setup +# AMD says 0.3ns of data hold +# AMD Data output valid time min 1 max 3 +# in delay max = tco_ext to max delay ext to fpga +# in delay min = minTco_ext to min delay ext to fpga +# out delay max = ext setup + max delay fpga to external +# out delay min = ext hold + min delay fpga to external + +# when sending to the SP5, it's going to take 2.2ns of trace time, and it needs to be there +# Clock took 2.2 ns to get to us, it's going to take 2.2ns of trace time to get back to the SP5 +# and SP5 wants 7 ns of setup time. We also eat ~4ns by syncing the espi clock. + +# outputs +# max = 7ns (SP5's needed setup time) + clock delay to FPGA (2.2ns) + return delay (2.2ns) +# min = .3ns (SP5's needed hold time) + clock delay to FPGA (2.2ns) + return delay (2.2ns) + + +# Data +# max= 7.5ns (1/2 period) + 3ns (maxreal tco) +# min= 7.5ns (1/2 period) + 1ns (min real tco) + +# This is a stop-gap to provide some kind of output timing constraints per the eSPI base spec +set_max_delay -to [get_ports espi0_sp5_to_fpga1_dat[*]] 6 +set_min_delay -to [get_ports espi0_sp5_to_fpga1_dat[*]] 0 +# eSPI1 is a second instance of the same target on the SP5's other eSPI +# port, serving the Versal's boot flash; same link, same budget. +set_max_delay -to [get_ports espi1_sp5_to_fpga1_dat[*]] 6 +set_min_delay -to [get_ports espi1_sp5_to_fpga1_dat[*]] 0 + +# ####################### +# SPI NOR flash interface (Winbond W25Q01JV) +# ####################### +# sclk is toggled by fabric logic off clk_125m at clk/2, so 62.5MHz, a 16ns +# period with an 8ns half period. Nothing inside the FPGA is clocked by it, so +# there is deliberately no create_generated_clock here: what actually has to be +# bounded is the clock-to-data skew leaving the FPGA and the pin-to-flop delay +# coming back, and both are directly constrainable. +# +# Trace delays are short and local; using the same 6.8ns/m as the FMC block +# above. +# On cosmo flash trace min is 31.982mm (Dat0), and max is 33.436 (Dat3) +# so min = 0.031982m * 6.8ns/m = 0.217 +# max = 0.033436 * 6.8ns/m = 0.227 +set flash_trace_max 0.227 +set flash_trace_min 0.217 + +# Pull the launch flops into the IOBs. Every one of these is a dedicated +# duplicate whose only load is its pin (see spi_clk_gen's sclk_pin and +# spi_txn_mgr's cs_n_pin), which is what makes packing legal. It matters a lot: +# left in the fabric the placer put them wherever it liked and measured 12 to 13 +# ns of routing to the pin, which both blew the clock-to-data skew budget and +# pushed the read round trip past every available sample point. In the IOB the +# delay is small, deterministic, and the same for all four. +set_property IOB TRUE [get_cells -hier -filter {NAME =~ *spi_nor_top_inst/link/clk_gen/sclk_pin_reg}] +set_property IOB TRUE [get_cells -hier -filter {NAME =~ *spi_nor_top_inst/spi_txn_mgr_inst/cs_n_pin_reg}] +set_property IOB TRUE [get_cells -hier -filter {NAME =~ *spi_nor_top_inst/link/io_o_reg[*]}] +set_property IOB TRUE [get_cells -hier -filter {NAME =~ *spi_nor_top_inst/link/io_oe_reg[*]}] +set_property IOB TRUE [get_cells -hier -filter {NAME =~ *spi_nor_top_inst/link/io_cap_*_reg[*]}] + +# ################# +# Outputs: sclk, and dat[] during the instruction, address and write phases. +# cs_n is handled separately below. +# +# The part samples mosi on the sclk rising edge, and the FPGA launches both mosi +# and the sclk falling edge from the same clk edge. So the flash sees a full half +# period of setup, less whatever skew the IOBs and routing add between the clock +# pin and the data pins: +# +# skew_budget = half_period - tDVCH = 8.0 - 2.0 = 6.0 ns +# +# Constraining all of these pins into one delay window makes the worst-case skew +# the difference between the two bounds, which is the quantity that matters here: +# the same clock insertion delay applies to every one of these launch flops, so it +# cancels out of the skew and only the window width has to fit the budget. +# +# Note the window looks wide (4.5ns against a 6ns budget) for four pins that are +# all IOB-packed and launched off the same clk edge. That is because max and min +# delay checks compare the slow corner of one path against the fast corner of +# another, so most of the width is process/voltage/temperature spread rather than +# pin-to-pin skew, which is well under a nanosecond here. The bound is a tripwire +# against a pin losing its IOB or picking up extra logic, not a skew estimate. +# +# Hold is not a concern for the part: mosi is held until the following falling +# edge, 8ns after the sampling edge, against a tCHDX of 3ns. +# +# These numbers assume the IOB packing above. Packed, the flop-to-pin delay is +# about 3.3ns and essentially all of it is logic -- 0.001ns of routing -- so a +# tight window is both meetable and meaningful. Left in the fabric the same paths +# measured 12 to 13ns of routing and varied by several ns between builds. +# Metro has two spi_nor_top instances: the SP5 boot flash, on the same 3V3 pins +# cosmo uses, and the Versal's boot flash behind the mux on sheet 137. The IOB +# properties above cover both already, because their -hier filters match on the +# instance name rather than the path to it. The delay windows do not, because +# the two flashes sit in banks at different voltages. +set_max_delay 5.0 -to [get_ports {spi_fpga1_to_flash_clk \ + spi_fpga1_to_flash_dat[*]}] +set_min_delay 0.5 -to [get_ports {spi_fpga1_to_flash_clk \ + spi_fpga1_to_flash_dat[*]}] + +# The Versal flash pins are in bank 35 at 1.8V, where the OBUFT is both slower +# and wider-spread than the 3V3 one the window above was measured against: the +# same IOB-packed path measures 2.9ns min to 7.0ns max here, against roughly +# 1.5 to 3.3ns on the 3V3 pins. Both ends of the window move with it, so the +# floor of 0.5ns is simply unreachable in this bank and says nothing about +# whether the path is healthy. +# +# What the window is for is unchanged: bounding the spread between sclk and the +# data pins so the flash sees them together. That spread is small for the same +# reason it is on the 3V3 pins -- every one of these is a dedicated IOB flop in +# one bank, launched off the same edge, so almost all of the width below is +# common-mode process/voltage/temperature spread rather than pin-to-pin skew. +# The bound is a tripwire against a pin losing its IOB or picking up fabric +# logic, and the max side plus the IOB properties are what actually catch that. +set_max_delay 5.0 -to [get_ports {qspi_fpga1_to_vercel_flash_mux_sck \ + qspi_fpga1_to_vercel_flash_mux_d[*]}] +set_min_delay 0.0 -to [get_ports {qspi_fpga1_to_vercel_flash_mux_sck \ + qspi_fpga1_to_vercel_flash_mux_d[*]}] + +# Two things are deliberately outside that window, because pulling them into it +# would make the placer work hard on paths that have an order of magnitude more +# real slack than the data pins do: +# +# cs_n only has to be low before the first sclk edge and stay low after +# the last. spi_txn_mgr spends cs_setup_cnts = 4 clk cycles, 32ns, +# on each, against tSLCH/tCHSH of 5ns. +# the tristate carries no data and only has to have settled before the part +# enable starts driving, which release_lanes gives it a full sclk cycle +# to do. +set_max_delay 16.0 -to [get_ports {spi_fpga1_to_flash_cs_l \ + qspi_fpga1_to_vercel_flash_mux_cs_l}] +set_max_delay 16.0 -from [get_cells -hier -filter {NAME =~ *spi_nor_top_inst/link/io_oe_reg[*]}] \ + -to [get_ports {spi_fpga1_to_flash_dat[*] \ + qspi_fpga1_to_vercel_flash_mux_d[*]}] + +# ################# +# Inputs: dat[] during read phases. +# +# The controller samples read data at a fixed point S after the sclk rising edge, +# set by the rx_sample_taps generic on spi_nor_top. S has to satisfy +# +# round_trip_valid - half_period <= S <= half_period + round_trip_hold +# +# where round_trip_valid is built from the part's tCLQV and round_trip_hold from +# its tCLQX. Note the upper limit comes from tCLQX, not tCLQV: sampling too late +# catches the next bit rather than the current one. +# +# With the IOB packing and the output bounds above: +# flop to sclk pin 3.30 max 1.50 min +# sclk trace 0.40 0.10 +# flash tCLQV / tCLQX 6.00 1.50 +# data trace back 0.40 0.10 +# pin to capture flop 1.50 0.50 +# ----- ----- +# round_trip_valid max 11.60 round_trip_hold min 3.70 +# +# so at an 8ns half period S has to land in 3.6 .. 11.7ns. rx_sample_taps = 2 puts +# it at 8ns, about 4ns clear of either limit. spi_nor_fast_tb and +# spi_nor_fast_quick_io_tb model these delays and check both corners. +# +# If reads are marginal on hardware, sweep rx_sample_taps before assuming anything +# else is wrong; taps are 4ns apart so 1 and 3 bracket the shipped value. +# +# -datapath_only because this is a pin to flop propagation bound, not a +# synchronous transfer: without it Vivado charges the MMCM's clock insertion +# delay against the budget and the check becomes meaningless. +# +# The dedicated capture flops in spi_link are the only loads on these pins, so +# these paths are exactly the pin-to-flop delay. +set_max_delay 3.0 -datapath_only -from [get_ports {spi_fpga1_to_flash_dat[*] \ + qspi_fpga1_to_vercel_flash_mux_d[*]}] \ No newline at end of file diff --git a/hdl/projects/metro_seq/sims/metro_seq_top_tb.vhd b/hdl/projects/metro_seq/sims/metro_seq_top_tb.vhd new file mode 100644 index 00000000..8d6007fe --- /dev/null +++ b/hdl/projects/metro_seq/sims/metro_seq_top_tb.vhd @@ -0,0 +1,41 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +library ieee; +use ieee.std_logic_1164.all; + +library vunit_lib; + context vunit_lib.vunit_context; + +-- Analysis-only smoke test. It does not drive the top level; its job is to make +-- a simulator compile the whole design tree, which the synthesis-only build +-- rules do not do on their own. Behavioural coverage lives in the per-subsystem +-- testbenches. +-- +-- Known gap: nothing exercises metro_seq_top itself, so the pin-to-record glue +-- in its architecture is unverified. That glue has already been wrong once -- +-- the hotswap power goods were inverted both here and in the sequencer synchroniser, which +-- the sequencer testbench cannot see because it instantiates the sequencer +-- directly. Driving 264 ports from a testbench is the obvious next step if that +-- layer keeps biting. +entity metro_seq_top_tb is + generic ( + runner_cfg : string + ); +end entity; + +architecture tb of metro_seq_top_tb is +begin + bench: process + begin + test_runner_setup(runner, runner_cfg); + while test_suite loop + if run("analyses") then + info("metro_seq_top and its dependencies analysed"); + end if; + end loop; + test_runner_cleanup(runner); + wait; + end process; +end tb; diff --git a/hdl/projects/metro_seq/versal_subsystem/BUCK b/hdl/projects/metro_seq/versal_subsystem/BUCK new file mode 100644 index 00000000..b76d8607 --- /dev/null +++ b/hdl/projects/metro_seq/versal_subsystem/BUCK @@ -0,0 +1,24 @@ +load("//tools:hdl.bzl", "vhdl_unit") +load("//tools:rdl.bzl", "rdl_file") + +rdl_file( + name = "versal_flash_regs_rdl", + src = "versal_flash_regs.rdl", + outputs = [ + "versal_flash_regs_pkg.vhd", + "versal_flash_regs.html", + "versal_flash_regs.json", + ], + visibility = ["PUBLIC"], +) + +vhdl_unit( + name = "versal_flash_subsystem", + srcs = ["versal_flash_subsystem.vhd"], + deps = [ + ":versal_flash_regs_rdl", + "//hdl/ip/vhd/axi_blocks:axilite_if_2k19", + ], + visibility = ["PUBLIC"], + standard = "2019", +) diff --git a/hdl/projects/metro_seq/versal_subsystem/versal_flash_regs.rdl b/hdl/projects/metro_seq/versal_subsystem/versal_flash_regs.rdl new file mode 100644 index 00000000..467efb45 --- /dev/null +++ b/hdl/projects/metro_seq/versal_subsystem/versal_flash_regs.rdl @@ -0,0 +1,50 @@ +// SystemRDL description of the sw-accessible registers owning the Versal's +// boot-flash mux on the Metro sequencer FPGA. + +addrmap versal_flash_regs { + name = "Versal boot flash mux control"; + desc = ""; + + default regwidth = 32; + default sw = rw; + default hw = r; + + reg { + name = "Flash Mux Control"; + desc = "Requests the Versal's QSPI boot flash for the FPGA. The request + is only honoured while the sequencer is holding the Versal in reset, so + that the FPGA and the Versal can never drive the flash at once. The + sequencer also takes the flash on its own, without a request, while it + measures the image before boot and once the Versal has booted; see + mux_status.seq_owned."; + field { + desc = "Set to 1 to ask for the Versal's boot flash."; + } request[1] = 0; + } mux_ctrl; + + reg { + name = "Flash Mux Status"; + default sw = r; + field { + desc = "Set when the FPGA actually owns the flash: the sequencer + claimed it, or the request was made and the sequencer is holding + the Versal in reset."; + } granted[1] = 0; + field { + desc = "Set while the sequencer itself is claiming the flash for + the FPGA, during the pre-boot measurement and after the Versal has + booted."; + } seq_owned[1] = 0; + field { + desc = "Live readback of the mux select pin (From FPGA to the mux)"; + } mux_sel[1] = 0; + field { + desc = "Live readback of the mux enable pin, active low (From FPGA to the mux)"; + } mux_en_l[1] = 1; + field { + desc = "Live readback of whether the sequencer is holding the Versal + in reset, i.e. whether a request would be granted."; + } versal_held_in_reset[1] = 0; + } mux_status; + +}; diff --git a/hdl/projects/metro_seq/versal_subsystem/versal_flash_subsystem.vhd b/hdl/projects/metro_seq/versal_subsystem/versal_flash_subsystem.vhd new file mode 100644 index 00000000..326c57de --- /dev/null +++ b/hdl/projects/metro_seq/versal_subsystem/versal_flash_subsystem.vhd @@ -0,0 +1,131 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; +use ieee.numeric_std_unsigned.all; + +use work.axil8x32_pkg; +use work.versal_flash_regs_pkg.all; + +-- Control of the mux on schematic sheet 137 that hands the Versal's QSPI boot +-- flash to the FPGA, so a Versal image can be read back or written without a +-- separate programmer. The controller that drives the flash is the spi_nor in +-- the eSPI1 wrapper at the top level, which is how the SP5 reaches it over +-- SAFS as well as the SP over registers; this block only decides whether it +-- may drive the part. +-- +-- The mux is switched to the FPGA when the sequencer asks for it, to measure +-- the image before boot and to hand the flash to the SP5 once the Versal has +-- booted, and on the SP's request while the sequencer is holding the Versal in +-- POR. Either way the Versal is never driving the flash at the same time. The +-- SP's request is a level, not a pulse: if the Versal is released while the SP +-- still holds the request, ownership drops on its own. +entity versal_flash_subsystem is + port( + clk : in std_logic; + reset : in std_logic; + + -- Control and status for the mux itself + ctrl_axi_if : view axil8x32_pkg.axil_target; + + -- From the sequencer: high while POR_B is asserted and will stay so + versal_held_in_reset : in std_logic; + -- From the sequencer: it wants the flash on the FPGA side itself + flash_owned_by_seq : in std_logic; + + -- Mux control pins + flash_qspi_mux_sel : out std_logic; + flash_qspi_mux_en_l : out std_logic; + + -- To the flash controller: high while it may drive the flash. Goes + -- to spi_nor_top's bus_enable, which parks the pins at the IOB flops + -- when low; muxing the pins here instead would pull those flops out + -- of the IOBs. + flash_bus_enable : out std_logic + ); +end entity; + +architecture rtl of versal_flash_subsystem is + + signal active_read : std_logic; + signal active_write : std_logic; + signal rdata : std_logic_vector(31 downto 0); + signal mux_ctrl : mux_ctrl_type; + signal mux_status : mux_status_type; + signal granted : std_logic; + +begin + + -- The interlock. The SP requesting is not enough; the sequencer has to be + -- holding the Versal off the flash as well. The sequencer's own claim + -- needs no request. + granted <= flash_owned_by_seq or (mux_ctrl.request and versal_held_in_reset); + flash_qspi_mux_sel <= granted; + flash_qspi_mux_en_l <= not granted; + + -- The controller parks its pins whenever we do not own the flash, so + -- losing the grant mid-transaction stops driving within a clock. + flash_bus_enable <= granted; + + mux_status <= ( + granted => granted, + mux_sel => granted, + mux_en_l => not granted, + versal_held_in_reset => versal_held_in_reset, + seq_owned => flash_owned_by_seq + ); + + axil_target_txn_inst: entity work.axil_target_txn + port map( + clk => clk, + reset => reset, + arvalid => ctrl_axi_if.read_address.valid, + arready => ctrl_axi_if.read_address.ready, + awvalid => ctrl_axi_if.write_address.valid, + awready => ctrl_axi_if.write_address.ready, + wvalid => ctrl_axi_if.write_data.valid, + wready => ctrl_axi_if.write_data.ready, + bvalid => ctrl_axi_if.write_response.valid, + bready => ctrl_axi_if.write_response.ready, + bresp => ctrl_axi_if.write_response.resp, + rvalid => ctrl_axi_if.read_data.valid, + rready => ctrl_axi_if.read_data.ready, + rresp => ctrl_axi_if.read_data.resp, + active_read => active_read, + active_write => active_write + ); + ctrl_axi_if.read_data.data <= rdata; + + write_logic: process(clk, reset) + begin + if reset then + mux_ctrl <= rec_reset; + elsif rising_edge(clk) then + if active_write then + case to_integer(ctrl_axi_if.write_address.addr) is + when MUX_CTRL_OFFSET => mux_ctrl <= unpack(ctrl_axi_if.write_data.data); + when others => null; + end case; + end if; + end if; + end process; + + read_logic: process(clk, reset) + begin + if reset then + rdata <= (others => '0'); + elsif rising_edge(clk) then + if active_read then + case to_integer(ctrl_axi_if.read_address.addr) is + when MUX_CTRL_OFFSET => rdata <= pack(mux_ctrl); + when MUX_STATUS_OFFSET => rdata <= pack(mux_status); + when others => rdata <= (others => '0'); + end case; + end if; + end if; + end process; + +end rtl; diff --git a/tools/fmc_sweep/fmc_sweep.py b/tools/fmc_sweep/fmc_sweep.py new file mode 100755 index 00000000..0143939d --- /dev/null +++ b/tools/fmc_sweep/fmc_sweep.py @@ -0,0 +1,567 @@ +#!/usr/bin/env python3 +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +"""FMC interface frequency sweep and verification driver. + +Drives the FPGA's FMC target through hubris' fmc-demo-server: bulk traffic +uses the UDP peek/poke protocol (tools/speeker/udp_if.py), and the FMC_CLK +frequency is changed between phases through humility hiffy calls to the +FmcDemo timing Idol operations, and each change is verified by reading +BTR1 back. + +The phases are ordered so that a timing miss at a new frequency shows up as +bad data before it can show up as bus contention: reads of a known-constant +register first (the SP never drives the bus during read data phases, so a +miscapture is data-corruption-only), then scratch write/readback, then +batched back-to-back stress, then a seeded random soak, then a throughput +measurement. + +Usage +----- + # sanity run at the current frequency + ./tools/fmc_sweep/fmc_sweep.py --ip fe80::0c1d:beff:fe3f:0001 --interface eno1 + + # sweep 50 -> 66.67 -> 100 MHz (CLKDIV divisor values, i.e. kernel/N) + ./tools/fmc_sweep/fmc_sweep.py --ip ... --interface eno1 \ + --sweep 4,3,2 --archive /path/to/build-grapefruit.zip + + # frequency control done by hand (or by re-flashing), just verify + ./tools/fmc_sweep/fmc_sweep.py --ip ... --interface eno1 --no-timing-control + +Notes +----- +* --sweep takes *divisor* values (FMC_CLK = 200 MHz / N), so 4 = 50 MHz, + 3 = 66.67 MHz, 2 = 100 MHz. The Idol op takes the same divisor and + subtracts one for the register field itself. +* Neither board exposes a bulk R/W RAM over FMC, so bulk traffic is split: + back-to-back *reads* sweep a read-only region (default: the eSPI post-code + buffer at FPGA offset 0x8100, an external capture mem -- AXI writes to it + are silently dropped, which is exactly why it cannot be used for write + verification), and back-to-back *writes* hammer a single R/W scribble + register (default: the info block scratchpad at offset 0x10). Write + verification is last-value-plus-ordering here; per-word write verification + is the simulation suite's job. Do NOT point --scratch-addr at + fpga_checksum (offset 0xC): hubris uses it to decide whether the FPGA + needs reprogramming. +* On any phase failure the sweep restores the baseline divisor and + re-verifies phase A so the board is left usable. +""" + +import argparse +import json +import os +import random +import re +import subprocess +import sys +import time + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) +from speeker.udp_if import Request, UDPMem # noqa: E402 + +FPGA_WINDOW = 0xC0000000 +KERNEL_CLK_MHZ = 200.0 +# ops per packet: each peek_adv4 is 1 byte, each poke_adv4 is 5 bytes, plus +# the 2-byte header and 6-byte address op; 128 words stays well inside the +# 1500-byte cap in both directions (a read reply is 4 bytes per word). +BATCH_WORDS = 128 + + +class TimingCtl: + """FMC_CLK control through humility hiffy FmcDemo calls.""" + + def __init__(self, humility, archive, dry_run=False, verbose=False): + self.cmd_base = [humility] + if archive: + self.cmd_base += ["-a", archive] + self.dry_run = dry_run + self.verbose = verbose + + def _run(self, call, args): + cmd = list(self.cmd_base) + ["hiffy", "-c", call] + for key, value in args: + cmd += ["-a", "%s=%s" % (key, value)] + if self.verbose or self.dry_run: + print(" $ " + " ".join(cmd)) + if self.dry_run: + return + out = subprocess.run(cmd, capture_output=True, text=True, timeout=30) + # humility exits 0 even when the served op returns Err, including + # "" from a task that faulted and restarted, so the + # output text has to be checked too. + if out.returncode != 0 or "Err(" in out.stdout: + raise SystemExit( + "humility hiffy failed: %s\n%s%s" + % (" ".join(cmd), out.stdout, out.stderr) + ) + + def set_divisor(self, divisor): + self._run("FmcDemo.set_clock_divider", [("n", str(divisor))]) + + def get_btr1(self): + """FMC_BTR1 readback via the get_btr1 Idol op, or None on older + firmware / dry runs. CLKDIV is bits [23:20].""" + if self.dry_run: + return None + cmd = list(self.cmd_base) + ["hiffy", "-c", "FmcDemo.get_btr1"] + try: + out = subprocess.run(cmd, capture_output=True, text=True, timeout=30) + except (subprocess.TimeoutExpired, FileNotFoundError): + return None + if out.returncode != 0 or "Err(" in out.stdout: + return None + # parse only the success-reply form "FmcDemo.get_btr1() => "; + # anything looser can match numbers inside an Err message + m = re.search(r"=>\s*(0x[0-9a-fA-F]+|\d+)\s*$", out.stdout.strip()) + return int(m.group(1), 0) if m else None + + def verify_divisor(self, divisor): + """Returns (btr1, ok_or_None): ok compares the CLKDIV field against + what set_divisor should have programmed (divisor - 1); None when the + firmware has no readback op.""" + btr1 = self.get_btr1() + if btr1 is None: + return None, None + clkdiv = (btr1 >> 20) & 0xF + return btr1, clkdiv == divisor - 1 + + def system_time(self): + """Kernel tick count from `humility tasks`, or None if it could not + be read. Ticks reset on an SP reboot, which is how a hang-then- + watchdog event is told apart from a dropped packet: the tick clock + going backwards across a step means the SP restarted under us.""" + if self.dry_run: + return None + cmd = list(self.cmd_base) + ["tasks"] + try: + out = subprocess.run(cmd, capture_output=True, text=True, timeout=30) + except (subprocess.TimeoutExpired, FileNotFoundError): + return None + if out.returncode != 0: + return None + m = re.search(r"system time = (\d+)", out.stdout) + return int(m.group(1)) if m else None + + +def freq_mhz(divisor): + return KERNEL_CLK_MHZ / divisor + + +def batched_read(mem, base, count): + """Read count 32-bit words starting at base using peek_adv4 batches.""" + words = [] + for chunk_at in range(0, count, BATCH_WORDS): + n = min(BATCH_WORDS, count - chunk_at) + req = Request() + req.set_address(base + 4 * chunk_at) + req.add_read32_advances(n) + resp = mem.execute_prebuilt_request(req) + words += [r.payload for r in resp.expected_responses] + return words + + +def phase_a_const_reads(mem, args, golden): + """Repeated single reads of a known-constant register, with gaps.""" + seen = set() + for i in range(args.const_iters): + seen.add(mem.read32(args.const_addr)) + time.sleep(0.001) + ok = len(seen) == 1 and (golden is None or seen == {golden}) + return ok, {"values": sorted("0x%08x" % v for v in seen)} + + +def phase_b_scratch(mem, args): + """Single write/readback of walking and random patterns.""" + addr = args.scratch_addr + patterns = ( + [1 << b for b in range(32)] + + [0xFFFFFFFF ^ (1 << b) for b in range(32)] + + [0x00000000, 0xFFFFFFFF, 0xA5A5A5A5, 0x5A5A5A5A] + ) + bad = [] + for pat in patterns: + mem.write32(addr, pat) + got = mem.read32(addr) + if got != pat: + bad.append({"wrote": "0x%08x" % pat, "read": "0x%08x" % got}) + return not bad, {"patterns": len(patterns), "mismatches": bad[:8]} + + +def scratch_write_burst(mem, addr, values): + """Back-to-back non-advancing writes to one register per packet batch.""" + for chunk_at in range(0, len(values), BATCH_WORDS): + chunk = values[chunk_at : chunk_at + BATCH_WORDS] + req = Request() + req.set_address(addr) + req.add_write32s(chunk) + mem.execute_prebuilt_request(req) + + +def phase_c_stress(mem, args, rng): + """Back-to-back stress: a write burst into the scratch register (posted + writes queue back-to-back; the last value read back proves ordering and + that nothing was dropped or hung), then two read sweeps of the read-only + stress region compared against each other (content is arbitrary but must + be stable).""" + values = [rng.getrandbits(32) for _ in range(args.stress_words)] + scratch_write_burst(mem, args.scratch_addr, values) + got = mem.read32(args.scratch_addr) + bad = [] + if got != values[-1]: + bad.append({"scratch_last": "0x%08x" % values[-1], + "read": "0x%08x" % got}) + pass1 = batched_read(mem, args.stress_base, args.stress_words) + pass2 = batched_read(mem, args.stress_base, args.stress_words) + for i, (x, y) in enumerate(zip(pass1, pass2)): + if x != y and len(bad) < 8: + bad.append({"index": i, "pass1": "0x%08x" % x, "pass2": "0x%08x" % y}) + return not bad, {"write_burst": len(values), "read_words": args.stress_words, + "mismatches": bad} + + +def phase_d_soak(mem, args, rng): + """Seeded random mix of scratch writes/readbacks, constant reads, and + read-region bursts, scoreboarding the scratch register and the golden + constant. Runs for --soak-ops operations or --soak-seconds, whichever + lasts longer (each op is one UDP round trip, so op counts alone finish + in seconds).""" + snapshot = batched_read(mem, args.stress_base, args.stress_words) + golden = mem.read32(args.const_addr) + last_scratch = None + bad = [] + ops = 0 + deadline = time.monotonic() + args.soak_seconds + while ops < args.soak_ops or time.monotonic() < deadline: + ops += 1 + op = rng.randrange(4) + if op == 0: + last_scratch = rng.getrandbits(32) + mem.write32(args.scratch_addr, last_scratch) + elif op == 1 and last_scratch is not None: + got = mem.read32(args.scratch_addr) + if got != last_scratch and len(bad) < 8: + bad.append({"scratch_expect": "0x%08x" % last_scratch, + "read": "0x%08x" % got}) + elif op == 2: + got = mem.read32(args.const_addr) + if got != golden and len(bad) < 8: + bad.append({"const_expect": "0x%08x" % golden, + "read": "0x%08x" % got}) + else: + index = rng.randrange(args.stress_words - 16) + got = batched_read(mem, args.stress_base + 4 * index, 16) + if got != snapshot[index : index + 16] and len(bad) < 8: + bad.append({"region_index": index}) + if bad and len(bad) >= 8: + break # no point soaking further on a badly broken link + return not bad, {"ops": ops, "mismatches": bad} + + +def _timed_block_op(mem, build_request, small, large, repeats=5): + """Median duration delta between a `large`-count and a `small`-count + server-side block op: the round trip, stack, and per-packet costs cancel, + leaving (large - small) bus accesses. Returns (seconds_per_word, checksum + of the last large op).""" + def run(count): + req = build_request(count) + t0 = time.monotonic() + resp = mem.execute_prebuilt_request(req) + dt = time.monotonic() - t0 + payload = resp.expected_responses[0].payload if resp.expected_responses else None + return dt, payload + deltas = [] + checksum = None + for _ in range(repeats): + t_small, _ = run(small) + t_large, checksum = run(large) + deltas.append(t_large - t_small) + deltas.sort() + return deltas[len(deltas) // 2] / (large - small), checksum + + +def phase_e_throughput(mem, args, rng): + """FMC line rate via the server-side block ops (delta-timed so network, + stack, and per-packet costs cancel), with the wire-level batched numbers + kept for reference. Falls back to wire-level only against firmware + without ops 17-19.""" + results = {} + ok = True + + # wire-level reference numbers (dominated by per-word network bytes) + values = [rng.getrandbits(32) for _ in range(args.stress_words)] + t0 = time.monotonic() + scratch_write_burst(mem, args.scratch_addr, values) + t_write = time.monotonic() - t0 + ok = mem.read32(args.scratch_addr) == values[-1] + t0 = time.monotonic() + snapshot = batched_read(mem, args.stress_base, args.stress_words) + t_read = time.monotonic() - t0 + nbytes = 4 * args.stress_words + results["wire_write_MBps"] = round(nbytes / t_write / 1e6, 3) + results["wire_read_MBps"] = round(nbytes / t_read / 1e6, 3) + + n = 200 + t0 = time.monotonic() + for _ in range(n): + mem.read32(args.const_addr) + results["single_read_us"] = round((time.monotonic() - t0) / n * 1e6, 1) + + # bus-level numbers via ops 17-19 + try: + golden = mem.read32(args.const_addr) + retried_before = mem.timeouts_retried + + def rd_req(count): + req = Request() + req.set_address(args.stress_base) + req.add_peek_block_checksum(count, advance=True) + return req + + spw, checksum = _timed_block_op(mem, rd_req, 64, args.stress_words) + results["fmc_read_MBps"] = round(4 / spw / 1e6, 2) + results["fmc_read_ns_per_word"] = round(spw * 1e9) + expect = sum(snapshot) & 0xFFFFFFFF + if checksum != expect: + ok = False + results["read_checksum_mismatch"] = { + "got": "0x%08x" % checksum, "expect": "0x%08x" % expect} + + def rdf_req(count): + req = Request() + req.set_address(args.const_addr) + req.add_peek_block_checksum(count, advance=False) + return req + + spw, checksum = _timed_block_op(mem, rdf_req, 64, 8192) + results["fmc_read_fixed_ns_per_word"] = round(spw * 1e9) + if checksum != (golden * 8192) & 0xFFFFFFFF: + ok = False + results["fixed_checksum_mismatch"] = "0x%08x" % checksum + + fill_value = rng.getrandbits(32) + + def wr_req(count): + req = Request() + req.set_address(args.scratch_addr) + req.add_poke_block_fill(count, fill_value) + return req + + spw, _ = _timed_block_op(mem, wr_req, 64, 8192) + results["fmc_write_MBps"] = round(4 / spw / 1e6, 2) + results["fmc_write_ns_per_word"] = round(spw * 1e9) + if mem.read32(args.scratch_addr) != fill_value: + ok = False + results["fill_readback_mismatch"] = True + if mem.timeouts_retried != retried_before: + # a resend during a timed op makes that sample garbage + results["retried_during_timing"] = True + except Exception as exc: + results["block_ops"] = "unsupported or failed: %r" % exc + + results["note"] = ("fmc_* numbers are bus line rate (delta-timed block " + "ops); wire_* include per-word network cost and are " + "insensitive to the FMC clock") + return ok, results + + +PHASES = [ + ("A_const_reads", phase_a_const_reads), + ("B_scratch", phase_b_scratch), + ("C_stress", phase_c_stress), + ("D_soak", phase_d_soak), + ("E_throughput", phase_e_throughput), +] + + +def run_phases(mem, args, rng, golden): + results = {} + all_ok = True + for name, fn in PHASES: + if name == "A_const_reads": + ok, detail = fn(mem, args, golden) + elif name == "B_scratch": + ok, detail = fn(mem, args) + else: + ok, detail = fn(mem, args, rng) + results[name] = {"ok": ok, **detail} + status = "ok" if ok else "FAIL" + print(" %-16s %s %s" % (name, status, json.dumps(detail)[:120])) + if not ok: + all_ok = False + break # keep the electrical exposure of later phases off a bad link + return all_ok, results + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--ip", required=True, help="SP link-local IPv6 address") + ap.add_argument("--interface", required=True, help="host network interface") + ap.add_argument("--port", type=int, default=11114) + ap.add_argument("--sweep", default=None, + help="comma-separated FMC_CLK divisors to sweep, e.g. 4,3,2") + ap.add_argument("--baseline-divisor", type=int, default=4, + help="known-good divisor to calibrate at and restore to") + ap.add_argument("--no-timing-control", action="store_true", + help="never call humility; frequency is managed externally") + ap.add_argument("--humility", default="humility") + ap.add_argument("--archive", default=None, help="hubris archive for humility") + ap.add_argument("--const-addr", type=lambda x: int(x, 0), + default=FPGA_WINDOW, + help="address of a read-only constant register") + ap.add_argument("--const-iters", type=int, default=50) + ap.add_argument("--scratch-addr", type=lambda x: int(x, 0), + default=FPGA_WINDOW + 0x10, + help="address of a harmless 32-bit r/w scratch register " + "(default: info block scratchpad; never point this " + "at fpga_checksum)") + ap.add_argument("--stress-base", type=lambda x: int(x, 0), + default=FPGA_WINDOW + 0x8100, + help="base of a readable region for bulk read traffic " + "(default: eSPI post-code buffer; reads only, its " + "content just has to be stable during the run)") + ap.add_argument("--stress-words", type=int, default=1024) + ap.add_argument("--soak-ops", type=int, default=500, + help="minimum soak operations (each is one UDP round trip)") + ap.add_argument("--soak-seconds", type=int, default=0, + help="minimum soak duration; the soak runs until BOTH " + "this and --soak-ops are satisfied. Applies to every " + "divisor in a sweep, so budget accordingly.") + ap.add_argument("--udp-retries", type=int, default=3, + help="resend attempts after a UDP timeout before a step " + "fails (0 = old fail-on-first-loss behavior)") + ap.add_argument("--seed", type=int, default=0x1DE) + ap.add_argument("--json", default=None, help="write results here") + ap.add_argument("--dry-run", action="store_true", + help="print humility commands without running them") + args = ap.parse_args() + + # retries make a dropped frame on a flaky link (USB NICs especially) a + # logged statistic instead of a dead hour-long soak; three consecutive + # losses still fails the step, which is the "target actually gone" case + mem = UDPMem(args.ip, args.interface, target_port=args.port, + retries=args.udp_retries) + # The archive enables observation (BTR1 readback, SP uptime/reboot + # detection) even when frequency control is off; --no-timing-control + # only means "never change the divisor" -- the mode for soaking a board + # at its kernel-configured boot frequency. + ctl = TimingCtl(args.humility, args.archive, + dry_run=args.dry_run) if args.archive else None + control = ctl is not None and not args.no_timing_control + + report = {"seed": args.seed, "steps": []} + + # Calibrate the constant register at the known-good divisor. + if control: + ctl.set_divisor(args.baseline_divisor) + time.sleep(0.1) + golden = mem.read32(args.const_addr) + print("golden const read @%s: 0x%08x" % (hex(args.const_addr), golden)) + + if args.sweep: + if not control: + raise SystemExit("--sweep requires timing control " + "(an --archive, without --no-timing-control)") + divisors = [int(d) for d in args.sweep.split(",")] + elif control: + divisors = [args.baseline_divisor] + else: + # frequency untouched: label the step with the divisor the SP + # actually booted with, when readable + btr1 = ctl.get_btr1() if ctl else None + divisors = [((btr1 >> 20) & 0xF) + 1 if btr1 is not None + else args.baseline_divisor] + if btr1 is not None: + print("boot BTR1 = 0x%08x -> divisor %d (%.2f MHz)" + % (btr1, divisors[0], freq_mhz(divisors[0]))) + overall_ok = True + for divisor in divisors: + print("== divisor %d (FMC_CLK %.2f MHz) ==" % (divisor, freq_mhz(divisor))) + if control: + ctl.set_divisor(divisor) + time.sleep(0.1) + rng = random.Random(args.seed) + btr1, div_ok = ctl.verify_divisor(divisor) if ctl else (None, None) + if btr1 is not None: + print(" BTR1 = 0x%08x (CLKDIV field %d, %s)" % + (btr1, (btr1 >> 20) & 0xF, + "matches" if div_ok else "DOES NOT MATCH requested divisor")) + if div_ok is False and not control: + # informational only: we did not request this divisor + div_ok = None + if div_ok is False: + # the programmed divider never landed: nothing this step would + # measure is at the requested frequency, so fail before phases + report["steps"].append({ + "divisor": divisor, + "freq_mhz": freq_mhz(divisor), + "ok": False, + "btr1": "0x%08x" % btr1, + "clkdiv_verified": False, + }) + overall_ok = False + print(" divisor %d FAILED (CLKDIV readback mismatch); " + "restoring baseline" % divisor) + break + ticks_before = ctl.system_time() if ctl else None + try: + ok, results = run_phases(mem, args, rng, golden) + except Exception as exc: # timeouts etc. count as a hard step failure + ok, results = False, {"exception": repr(exc)} + print(" step raised: %r" % exc) + step = { + "divisor": divisor, + "freq_mhz": freq_mhz(divisor), + "ok": ok, + "udp_timeouts_retried": mem.timeouts_retried, + "phases": results, + } + if btr1 is not None: + step["btr1"] = "0x%08x" % btr1 + step["clkdiv_verified"] = div_ok + if ctl: + ticks_after = ctl.system_time() + step["sp_ticks_before"] = ticks_before + step["sp_ticks_after"] = ticks_after + if ticks_before is not None and ticks_after is not None: + # ticks are milliseconds since boot and only ever increase + # while the SP stays up + step["sp_rebooted"] = ticks_after < ticks_before + if step["sp_rebooted"]: + ok = False + step["ok"] = False + print(" SP REBOOTED during this step (ticks %d -> %d):" + " it hung and was reset, this was not packet loss" + % (ticks_before, ticks_after)) + elif ticks_after is None: + step["sp_unresponsive"] = True + print(" could not read SP system time after the step " + "(SP hung or humility unavailable)") + report["steps"].append(step) + if not ok: + overall_ok = False + print(" divisor %d FAILED; restoring baseline" % divisor) + break + + if control: + # also covers the reboot case: a restarted SP booted back at its + # kernel-configured divisor, and this re-asserts the sweep baseline + ctl.set_divisor(args.baseline_divisor) + time.sleep(0.1) + ok, _ = phase_a_const_reads(mem, args, golden) + report["restored_baseline_ok"] = ok + print("baseline restore check: %s" % ("ok" if ok else "FAIL")) + overall_ok = overall_ok and ok + + if args.json: + with open(args.json, "w") as f: + json.dump(report, f, indent=2) + print("results written to", args.json) + + sys.exit(0 if overall_ok else 1) + + +if __name__ == "__main__": + main() diff --git a/tools/speeker/udp_if.py b/tools/speeker/udp_if.py index ad1158fe..4390bb6b 100644 --- a/tools/speeker/udp_if.py +++ b/tools/speeker/udp_if.py @@ -37,6 +37,11 @@ class Request: poke_adv2_arg = 14 # write 2 byte to addr, increment internal addr by 2 poke_adv4_arg = 15 # write 4 byte to addr, increment internal addr by 3 poke_adv8_arg = 16 # write 8 byte to addr, increment internal addr by 4 + # server-side block ops: N bus accesses per op with a constant-size + # response, so request duration is dominated by the FMC accesses + peek_block_checksum_arg = 17 # u16 count; advancing; returns u32 sum + peek_block_checksum_fixed_arg = 18 # u16 count; same address; returns u32 sum + poke_block_fill_arg = 19 # u16 count + u32 value; same address def __init__(self): # Build a bytearray to represent the packet we're going to send @@ -99,6 +104,24 @@ def add_write32s(self, values: list) -> None: else: raise Exception(f"Invalid type {type(value)} for value") + def add_peek_block_checksum(self, count, advance=True) -> None: + """Server-side read of `count` 32-bit words returning one u32 + wrapping-sum checksum; `advance` selects walking memory vs + re-reading one address.""" + op = self.peek_block_checksum_arg if advance else self.peek_block_checksum_fixed_arg + self.bytes += op.to_bytes(1, byteorder='little') + self.bytes += count.to_bytes(2, byteorder='little') + self.response.add_expected_peek(ResponsePeek(self.cur_addr, 4)) + if advance: + self.cur_addr += 4 * count + + def add_poke_block_fill(self, count, value) -> None: + """Server-side write of `value` to the current address `count` + times (non-advancing).""" + self.bytes += self.poke_block_fill_arg.to_bytes(1, byteorder='little') + self.bytes += count.to_bytes(2, byteorder='little') + self.bytes += value.to_bytes(4, byteorder='little') + def add_write32_advances(self, values: list) -> None: for value in values: self.bytes += self.poke_adv4_arg.to_bytes(1, byteorder='little') @@ -157,9 +180,18 @@ class UDPMem: Note that the SP is IPv6 only and runs on a link-local address so specification of the pc's output interface is required. """ - def __init__(self, target_ip, ifname, target_port=11114, timeout=2): + def __init__(self, target_ip, ifname, target_port=11114, timeout=2, retries=0): self.debug = False self.timeout = timeout + # Additional attempts after a receive timeout. Requests are + # idempotent (peeks re-read, pokes re-write the same value), so a + # resend after a dropped frame is safe; the socket is drained before + # each send so a late-arriving response can never be matched to a + # newer request. + self.retries = retries + # count of timed-out attempts that were subsequently retried; lets a + # caller report link flakiness instead of dying on it + self.timeouts_retried = 0 # Basic UDP IPv6 socket setup self.sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) # Build the target address using getaddrinfo and the interface name @@ -199,18 +231,37 @@ def _get_resp_from_request(self, request: Request) -> Response: request.response.process_bytes(resp_bytes) return request.response - def _send_get_reply_handshake(self, request: Request) -> bytes: - # Send the request out the wire - if self.debug: - print(f"Sending request: {request.hex()}") - self.sock.sendto(bytes(request), self.target_addr) - # try rx up to mtu size for timeout time and return - # response or exception on timeout - self.sock.settimeout(self.timeout) + def _drain(self) -> None: + """Discard any stale datagrams (late responses from a timed-out + attempt) so request/response pairing stays in lockstep.""" + self.sock.setblocking(False) try: - resp = self.sock.recv(1500) - except socket.timeout: - raise Exception("Timeout- no response back from target") - if self.debug: - print(f"Got response: {resp.hex()}") - return resp \ No newline at end of file + while True: + self.sock.recv(1500) + except (BlockingIOError, OSError): + pass + finally: + self.sock.setblocking(True) + + def _send_get_reply_handshake(self, request: Request) -> bytes: + for attempt in range(1 + self.retries): + self._drain() + if self.debug: + print(f"Sending request: {request.hex()}") + self.sock.sendto(bytes(request), self.target_addr) + # try rx up to mtu size for timeout time and return + # response or exception on timeout + self.sock.settimeout(self.timeout) + try: + resp = self.sock.recv(1500) + except socket.timeout: + if attempt < self.retries: + self.timeouts_retried += 1 + continue + raise Exception( + "Timeout- no response back from target" + + (f" after {1 + self.retries} attempts" if self.retries else "") + ) + if self.debug: + print(f"Got response: {resp.hex()}") + return resp \ No newline at end of file