diff --git a/Bender.yml b/Bender.yml index 6109e62..850a45b 100644 --- a/Bender.yml +++ b/Bender.yml @@ -21,6 +21,7 @@ sources: - src/apb_intf.sv # Level 2 - src/apb_regs.sv + - src/apb_cdc.sv - target: simulation files: @@ -29,9 +30,9 @@ sources: - target: test files: - test/tb_apb_regs.sv + - test/tb_apb_cdc.sv - target: synth_test files: # Level 0 - test/synth_bench.sv - diff --git a/src/apb_cdc.sv b/src/apb_cdc.sv new file mode 100644 index 0000000..2db3db0 --- /dev/null +++ b/src/apb_cdc.sv @@ -0,0 +1,285 @@ +// Copyright 2021 ETH Zurich and University of Bologna. +// Copyright and related rights are licensed under the Solderpad Hardware +// License, Version 0.51 (the "License"); you may not use this file except in +// compliance with the License. You may obtain a copy of the License at +// http://solderpad.org/licenses/SHL-0.51. Unless required by applicable law +// or agreed to in writing, software, hardware and materials distributed under +// this License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, either express or implied. See the License for the +// specific language governing permissions and limitations under the License. + +// APB Clock Domain Crossing +// Author: Manuel Eggimann +// Description: A clock domain crossing module on an APB interface. The module uses gray-counting +// CDC FIFOS in both directions to synchronize source side with destination side. +// Parameters: +// - `LogDepth`: The FIFO crossing the clock domain has `2^LogDepth` entries +// - `req_t`: APB4 request struct. See macro definition in `include/typedef.svh` +// - `resp_t`: APB4 response struct. See macro definition in `include/typedef.svh` +// +// Ports: +// +// - `src_pclk_i: Source side clock input signal (1-bit). +// - `src_preset_ni: Source side asynchronous active low reset signal (1-bit). +// - `src_req_i: Source side APB4 request struct, bundles all APB4 signals from the master (req_t). +// - `src_resp_o: Source side APB4 response struct, bundles all APB4 signals to the master (resp_t). +// - `dst_pclk_i: Destination side clock input signal (1-bit). +// - `dst_preset_ni: Destination side asynchronous active low reset signal (1-bit). +// - `dst_req_o: Destination side APB4 request struct, bundles all APB4 signals to the slave (req_t). +// - `dst_resp_i: Destination side APB4 response struct, bundles all APB4 signals from the slave (resp_t). +// +// This file also features the module `apb_cdc_intf`. The difference is that instead of the +// request and response structs it uses a `APB` interfaces. The parameters have the same +// function, however are defined in `ALL_CAPS`. + +(* no_ungroup *) +(* no_boundary_optimization *) +module apb_cdc #( + parameter LogDepth = 1, + parameter type req_t = logic, + parameter type resp_t = logic, + parameter type addr_t = logic, + parameter type data_t = logic, + parameter type strb_t = logic +) ( + // synchronous slave port - clocked by `src_pclk_i` + input logic src_pclk_i, + input logic src_preset_ni, + input req_t src_req_i, + output resp_t src_resp_o, + // synchronous master port - clocked by `dst_pclk_i` + input logic dst_pclk_i, + input logic dst_preset_ni, + output req_t dst_req_o, + input resp_t dst_resp_i +); + + typedef struct packed { + addr_t paddr; + apb_pkg::prot_t pprot; + logic pwrite; + data_t pwdata; + strb_t pstrb; + } apb_async_req_data_t; + + typedef struct packed { + data_t prdata; + logic pslverr; + } apb_async_resp_data_t; + + typedef enum logic {Src_Idle, Src_Busy} src_fsm_state_e; + typedef enum logic[1:0] {Dst_Idle, Dst_Access, Dst_Busy} dst_fsm_state_e; + src_fsm_state_e src_state_d, src_state_q; + dst_fsm_state_e dst_state_d, dst_state_q; + logic src_req_valid, src_req_ready, src_resp_valid, src_resp_ready; + logic dst_req_valid, dst_req_ready, dst_resp_valid, dst_resp_ready; + + apb_async_req_data_t src_req_data, dst_req_data; + + apb_async_resp_data_t dst_resp_data_d, dst_resp_data_q, src_resp_data; + + assign src_req_data.paddr = src_req_i.paddr; + assign src_req_data.pprot = src_req_i.pprot; + assign src_req_data.pwrite = src_req_i.pwrite; + assign src_req_data.pwdata = src_req_i.pwdata; + assign src_req_data.pstrb = src_req_i.pstrb; + + assign dst_req_o.paddr = dst_req_data.paddr; + assign dst_req_o.pprot = dst_req_data.pprot; + assign dst_req_o.pwrite = dst_req_data.pwrite; + assign dst_req_o.pwdata = dst_req_data.pwdata; + assign dst_req_o.pstrb = dst_req_data.pstrb; + + assign src_resp_o.prdata = src_resp_data.prdata; + assign src_resp_o.pslverr = src_resp_data.pslverr; + + ////////////////////////// + // SRC DOMAIN HANDSHAKE // + ////////////////////////// + + // In the source domain we translate simultaneous assertion of psel and penable into a transaction + // on the CDC. The FSM then transitions into a busy state where it waits for a response comming + // back on the other CDC FIFO. Once this response appears the pready signal is asserted to finish + // the APB transaction. + + always_comb begin + src_state_d = src_state_q; + src_req_valid = 1'b0; + src_resp_ready = 1'b0; + src_resp_o.pready = 1'b0; + case (src_state_q) + Src_Idle: begin + if (src_req_i.psel & src_req_i.penable) begin + src_req_valid = 1'b1; + if (src_req_ready) src_state_d = Src_Busy; + end + end + Src_Busy: begin + src_resp_ready = 1'b1; + if (src_resp_valid) begin + src_resp_o.pready = 1'b1; + src_state_d = Src_Idle; + end + end + default:; + endcase + end + + always_ff @(posedge src_pclk_i, negedge src_preset_ni) begin + if (!src_preset_ni) + src_state_q <= Src_Idle; + else + src_state_q <= src_state_d; + end + + + ////////////////////////// + // DST DOMAIN HANDSHAKE // + ////////////////////////// + + // In the destination domain we need to perform a proper APB handshake with setup and access + // phase. Once the destination slave asserts the pready signal we store the response data and + // transition into a busy state. In the busy state we send the response data back to the src + // domain and wait for the transaction to complete. + + always_comb begin + dst_state_d = dst_state_q; + dst_req_ready = 1'b0; + dst_resp_valid = 1'b0; + dst_req_o.psel = 1'b0; + dst_req_o.penable = 1'b0; + dst_resp_data_d = dst_resp_data_q; + case (dst_state_q) + Dst_Idle: begin + if (dst_req_valid) begin + dst_req_o.psel = 1'b1; + dst_state_d = Dst_Access; + end + end + + Dst_Access: begin + dst_req_o.psel = 1'b1; + dst_req_o.penable = 1'b1; + if (dst_resp_i.pready) begin + dst_req_ready = 1'b1; + dst_resp_data_d.prdata = dst_resp_i.prdata; + dst_resp_data_d.pslverr = dst_resp_i.pslverr; + dst_state_d = Dst_Busy; + end + end + + Dst_Busy: begin + dst_resp_valid = 1'b1; + if (dst_resp_ready) begin + dst_state_d = Dst_Idle; + end + end + + default: begin + dst_state_d = Dst_Idle; + end + endcase + end + + always_ff @(posedge dst_pclk_i, negedge dst_preset_ni) begin + if (!dst_preset_ni) begin + dst_state_q <= Dst_Idle; + dst_resp_data_q <= '0; + end else begin + dst_state_q <= dst_state_d; + dst_resp_data_q <= dst_resp_data_d; + end + end + + + /////////////// + // CDC FIFOS // + /////////////// + + cdc_fifo_gray #( + .T ( apb_async_req_data_t ), + .LOG_DEPTH ( LogDepth ) + ) i_cdc_fifo_gray_req ( + .src_clk_i ( src_pclk_i ), + .src_rst_ni ( src_preset_ni ), + .src_data_i ( src_req_data ), + .src_valid_i ( src_req_valid ), + .src_ready_o ( src_req_ready ), + + .dst_clk_i ( dst_pclk_i ), + .dst_rst_ni ( dst_preset_ni ), + .dst_data_o ( dst_req_data ), + .dst_valid_o ( dst_req_valid ), + .dst_ready_i ( dst_req_ready ) + ); + + cdc_fifo_gray #( + .T ( apb_async_resp_data_t ), + .LOG_DEPTH ( LogDepth ) + ) i_cdc_fifo_gray_resp ( + .src_clk_i ( dst_pclk_i ), + .src_rst_ni ( dst_preset_ni ), + .src_data_i ( dst_resp_data_q ), + .src_valid_i ( dst_resp_valid ), + .src_ready_o ( dst_resp_ready ), + + .dst_clk_i ( src_pclk_i ), + .dst_rst_ni ( src_preset_ni ), + .dst_data_o ( src_resp_data ), + .dst_valid_o ( src_resp_valid ), + .dst_ready_i ( src_resp_ready ) + ); + +endmodule // apb_cdc + + +`include "apb/typedef.svh" +`include "apb/assign.svh" + +module apb_cdc_intf #( + parameter int unsigned APB_ADDR_WIDTH = 0, + parameter int unsigned APB_DATA_WIDTH = 0, + /// Depth of the FIFO crossing the clock domain, given as 2**LOG_DEPTH. + parameter int unsigned LOG_DEPTH = 1 +)( + input logic src_pclk_i, + input logic src_preset_ni, + APB.Slave src, + input logic dst_pclk_i, + input logic dst_preset_ni, + APB.Master dst +); + + typedef logic [APB_ADDR_WIDTH-1:0] addr_t; + typedef logic [APB_DATA_WIDTH-1:0] data_t; + typedef logic [APB_DATA_WIDTH/8-1:0] strb_t; + + `APB_TYPEDEF_REQ_T(apb_req_t, addr_t, data_t, strb_t) + `APB_TYPEDEF_RESP_T(apb_resp_t, data_t) + + apb_req_t src_req, dst_req; + apb_resp_t dst_resp, src_resp; + + `APB_ASSIGN_TO_REQ(src_req, src) + `APB_ASSIGN_FROM_REQ(dst, dst_req) + `APB_ASSIGN_FROM_RESP(src, src_resp) + `APB_ASSIGN_TO_RESP(dst_resp, dst) + + apb_cdc #( + .LogDepth ( LOG_DEPTH ), + .req_t ( apb_req_t ), + .resp_t ( apb_resp_t ), + .addr_t ( addr_t ), + .data_t ( data_t ), + .strb_t ( strb_t ) + ) i_apb_cdc ( + .src_pclk_i, + .src_preset_ni, + .src_req_i ( src_req ), + .src_resp_o ( src_resp ), + .dst_pclk_i, + .dst_preset_ni, + .dst_req_o ( dst_req ), + .dst_resp_i ( dst_resp ) + ); +endmodule diff --git a/test/tb_apb_cdc.sv b/test/tb_apb_cdc.sv new file mode 100644 index 0000000..d96944f --- /dev/null +++ b/test/tb_apb_cdc.sv @@ -0,0 +1,223 @@ +// Copyright 2018 ETH Zurich and University of Bologna. +// Copyright and related rights are licensed under the Solderpad Hardware +// License, Version 0.51 (the "License"); you may not use this file except in +// compliance with the License. You may obtain a copy of the License at +// http://solderpad.org/licenses/SHL-0.51. Unless required by applicable law +// or agreed to in writing, software, hardware and materials distributed under +// this License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, either express or implied. See the License for the +// specific language governing permissions and limitations under the License. + +// Author: Manuel Eggimann + +// Description: Testbench for `apb_cdc`. +// Step 1: Read over a range of addresses, where the registers are inbetween. +// Step 2: Random reads and writes to the different registers. +// Assertions provide checking for correct functionality. + +`include "apb/assign.svh" + +module tb_apb_cdc #( + parameter int unsigned ApbAddrWidth = 32'd32, + parameter int unsigned ApbDataWidth = 32'd27, + localparam int unsigned ApbStrbWidth = cf_math_pkg::ceil_div(ApbDataWidth, 8), + parameter int unsigned CDCLogDepth = 3, + + // TB Parameters + parameter time TCLK_UPSTREAM = 10ns, + parameter time TA_UPSTREAM = TCLK_UPSTREAM * 1/4, + parameter time TT_UPSTREAM = TCLK_UPSTREAM * 3/4, + parameter time TCLK_DOWNSTREAM = 3ns, + parameter time TA_DOWNSTREAM = TCLK_DOWNSTREAM * 1/4, + parameter time TT_DOWNSTREAM = TCLK_DOWNSTREAM * 3/4, + parameter int unsigned REQ_MIN_WAIT_CYCLES = 0, + parameter int unsigned REQ_MAX_WAIT_CYCLES = 10, + parameter int unsigned RESP_MIN_WAIT_CYCLES = 0, + parameter int unsigned RESP_MAX_WAIT_CYCLES = REQ_MAX_WAIT_CYCLES/2, + parameter int unsigned N_TXNS = 1000 +); + + logic clk; + logic rst_n; + logic done; + + + // Clocks and Resets + + logic upstream_clk, + downstream_clk, + upstream_rst_n, + downstream_rst_n; + + clk_rst_gen #( + .ClkPeriod (TCLK_UPSTREAM), + .RstClkCycles (5) + ) i_clk_rst_gen_upstream ( + .clk_o (upstream_clk), + .rst_no (upstream_rst_n) + ); + + clk_rst_gen #( + .ClkPeriod (TCLK_DOWNSTREAM), + .RstClkCycles (5) + ) i_clk_rst_gen_downstream ( + .clk_o (downstream_clk), + .rst_no (downstream_rst_n) + ); + + + APB_DV #( + .ADDR_WIDTH ( ApbAddrWidth ), + .DATA_WIDTH ( ApbDataWidth ) + ) upstream_dv(upstream_clk); + APB #( + .ADDR_WIDTH ( ApbAddrWidth ), + .DATA_WIDTH ( ApbDataWidth ) + ) upstream(); + `APB_ASSIGN(upstream, upstream_dv) + + APB_DV #( + .ADDR_WIDTH ( ApbAddrWidth ), + .DATA_WIDTH ( ApbDataWidth ) + ) downstream_dv(downstream_clk); + APB #( + .ADDR_WIDTH ( ApbAddrWidth ), + .DATA_WIDTH ( ApbDataWidth ) + ) downstream(); + `APB_ASSIGN(downstream_dv, downstream) + + + typedef logic [ApbAddrWidth-1:0] apb_addr_t; + typedef logic [ApbDataWidth-1:0] apb_data_t; + typedef logic [ApbStrbWidth-1:0] apb_strb_t; + typedef apb_test::apb_request #(.ADDR_WIDTH(ApbAddrWidth), .DATA_WIDTH(ApbDataWidth)) apb_request_t; + typedef apb_test::apb_response #(.DATA_WIDTH(ApbDataWidth)) apb_response_t; + + apb_cdc_intf #( + .APB_ADDR_WIDTH ( ApbAddrWidth ), + .APB_DATA_WIDTH ( ApbDataWidth ), + .LOG_DEPTH ( CDCLogDepth ) + ) i_dut ( + .src_pclk_i ( upstream_clk ), + .src_preset_ni ( upstream_rst_n ), + .src ( upstream ), + .dst_pclk_i ( downstream_clk ), + .dst_preset_ni ( downstream_rst_n ), + .dst ( downstream ) + ); + + apb_test::apb_rand_master #( + .ADDR_WIDTH ( ApbAddrWidth ), + .DATA_WIDTH ( ApbDataWidth ), + .TA ( TA_UPSTREAM ), + .TT ( TT_UPSTREAM ) + ) apb_master = new(upstream_dv); + + + logic mst_done = 1'b0; + initial begin + wait(upstream_rst_n); + apb_master.run(N_TXNS); + mst_done = 1'b1; + end + + apb_test::apb_rand_slave #( + .ADDR_WIDTH ( ApbAddrWidth ), + .DATA_WIDTH ( ApbDataWidth ), + .TA ( TA_DOWNSTREAM ), + .TT ( TT_DOWNSTREAM ) + ) apb_slave = new(downstream_dv); + + initial begin + wait (downstream_rst_n); + apb_slave.run(); + end + + int unsigned upstream_errors = 0; + + // Monitor Upstream Respones + initial begin + automatic apb_response_t received_response = new; + automatic apb_response_t expected_response = new; + automatic bit pop_success; + forever begin + apb_master.response_queue.get(received_response); + $info("Received new response"); + pop_success = apb_slave.response_queue.try_get(expected_response); + assert(pop_success) else begin + $error("Upstream: Received spurious read response. prdata: %h, pslverr: %b", received_response.prdata, received_response.pslverr); + upstream_errors++; + continue; + end + //Compare upstream received response with downstream sent response + assert(received_response.prdata == expected_response.prdata) else begin + $error("Upstream: Received response's prdata: %h does not match the expected prdata: %h", received_response.prdata, expected_response.prdata); + upstream_errors++; + end + assert(received_response.pslverr == expected_response.pslverr) else begin + $error("Upstream: Received response's pslverr: %b does not match the expected pslverr: %b", received_response.pslverr, expected_response.pslverr); + upstream_errors++; + end + end + end // initial begin + + int unsigned downstream_errors = 0; + + // Monitor Downstream Requests + initial begin + automatic apb_request_t received_request = new; + automatic apb_request_t expected_request = new; + automatic bit pop_success; + forever begin + apb_slave.request_queue.get(received_request); + $info("Received new request"); + pop_success = apb_master.request_queue.try_get(expected_request); // Since the requests are recorded in + // the master driver strictly before they arrive in the slave, an + // empy request_queue in the master means, that we received an + // spurious request. + assert(pop_success) else begin + $error("Downstream: Received spurious request. Addr: %h, Data: %h, write_en: %b", received_request.paddr, received_request.pdata, received_request.pwrite); + downstream_errors++; + continue; + end + //Compare downstream received request with upstream sent request + assert(received_request.paddr == expected_request.paddr) else begin + $error("Downstream: Received request's paddr: %h does not match the paddr: %h", received_request.paddr, expected_request.paddr); + downstream_errors++; + end + assert(received_request.pdata == expected_request.pdata) else begin + $error("Downstream: Received request's pdata: %h does not match the expected pdata: %h", received_request.pdata, expected_request.pdata); + downstream_errors++; + end + assert(received_request.pwrite == expected_request.pwrite) else begin + $error("Downstream: Received request's pwrite: %b does not match the expected pwrite: %b", received_request.pwrite, expected_request.pwrite); + downstream_errors++; + end + end + end // initial begin + + // Terminate simulation after all transactions have left master and the last response should have + // arrived back + + initial begin + wait(mst_done); + // 10 Additional cycles for the request/response to pass through the + // CDC FIFO should be enough. + #((REQ_MAX_WAIT_CYCLES+10)*TCLK_UPSTREAM+(RESP_MAX_WAIT_CYCLES+10)*TCLK_DOWNSTREAM); + + // Now check if all queues are empty. If all requests/responses passed through the CDC the + // master and slave queue should all be empty now. + assert(apb_master.request_queue.num() == 0) else begin + $error("Lost %0d requests.", apb_master.request_queue.num()); + downstream_errors += apb_master.request_queue.num(); + end + assert(apb_slave.response_queue.num() == 0) else begin + $error("Lost %0d responses.", apb_slave.response_queue.num()); + upstream_errors += apb_slave.response_queue.num(); + end + $info("TB CDC finished after simulation of %0d random transactions.", N_TXNS); + $info("Got %0d upstream errors and %0d downstream errors.", upstream_errors, downstream_errors); + $finish(); + end + +endmodule