mirror of
https://github.com/openhwgroup/cva5.git
synced 2025-04-20 03:57:18 -04:00
lsq updates
This commit is contained in:
parent
bb534d617f
commit
8ea982f1ab
12 changed files with 382 additions and 340 deletions
|
@ -96,7 +96,6 @@ module decode_and_issue (
|
|||
logic nop;
|
||||
|
||||
logic issue_valid;
|
||||
logic load_store_operands_ready;
|
||||
logic operands_ready;
|
||||
logic [NUM_UNITS-1:0] unit_operands_ready;
|
||||
logic mult_div_op;
|
||||
|
@ -155,6 +154,7 @@ module decode_and_issue (
|
|||
assign ti.inflight_packet.is_store = is_store;
|
||||
assign ti.issued = instruction_issued & (uses_rd | unit_needed[LS_UNIT_WB_ID]);
|
||||
assign ti.issue_unit_id = unit_needed_for_id_gen_int;
|
||||
assign ti.exception_possible = opcode_trim inside {LOAD_T, STORE_T, AMO_T};
|
||||
////////////////////////////////////////////////////
|
||||
//Unit Determination
|
||||
assign mult_div_op = fb.instruction[25];
|
||||
|
@ -185,20 +185,17 @@ module decode_and_issue (
|
|||
assign issue_valid = fb_valid & ti.id_available & ~gc_issue_hold & ~gc_fetch_flush;
|
||||
|
||||
assign operands_ready = ~rf_issue.rs1_conflict & ~rf_issue.rs2_conflict;
|
||||
assign load_store_operands_ready = operands_ready;//~rf_issue.rs1_conflict & (~rf_issue.rs2_conflict | (rf_issue.rs2_conflict & (opcode_trim == STORE_T) & load_store_forwarding_possible));
|
||||
|
||||
//All units share the same operand ready logic except load-store which has an internal forwarding path
|
||||
always_comb begin
|
||||
unit_operands_ready = {NUM_UNITS{operands_ready}};
|
||||
unit_operands_ready[LS_UNIT_WB_ID] = load_store_operands_ready;
|
||||
unit_operands_ready[LS_UNIT_WB_ID] = ~rf_issue.rs1_conflict;
|
||||
end
|
||||
|
||||
assign issue_ready = unit_needed & unit_ready;
|
||||
assign issue = {NUM_UNITS{issue_valid}} & unit_operands_ready & issue_ready;
|
||||
|
||||
//If not all units can provide constant ready signals:
|
||||
//((|issue_ready) & issue_valid & load_store_operands_ready);
|
||||
assign instruction_issued = (|issue_ready) & issue_valid & load_store_operands_ready;
|
||||
assign instruction_issued = issue_valid & |(unit_operands_ready & issue_ready);
|
||||
assign instruction_issued_no_rd = instruction_issued & ~uses_rd;
|
||||
assign instruction_issued_with_rd = instruction_issued & uses_rd;
|
||||
|
||||
|
@ -273,7 +270,7 @@ module decode_and_issue (
|
|||
assign ls_inputs.fn3 = amo_op ? LS_W_fn3 : fn3;
|
||||
assign ls_inputs.load = is_load;
|
||||
assign ls_inputs.store = is_store;
|
||||
assign ls_inputs.forwarded_store = 0;//rf_issue.rs2_conflict & load_store_forwarding_possible;
|
||||
assign ls_inputs.forwarded_store = rf_issue.rs2_conflict;
|
||||
assign ls_inputs.store_forward_id = rf_issue.rs2_id;
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
|
@ -368,7 +365,7 @@ module decode_and_issue (
|
|||
//Unit EX signals
|
||||
generate
|
||||
for (i = 0; i < NUM_UNITS; i++) begin
|
||||
assign unit_issue[i].possible_issue = unit_needed[i] & unit_ready[i] & unit_operands_ready[i] & fb_valid & ti.id_available & ~gc_issue_hold;//Every condition other than a pipeline flush
|
||||
assign unit_issue[i].possible_issue = unit_needed[i] & unit_operands_ready[i] & fb_valid & ti.id_available;
|
||||
assign unit_issue[i].new_request = issue[i];
|
||||
assign unit_issue[i].instruction_id = ti.issue_id;
|
||||
always_ff @(posedge clk) begin
|
||||
|
@ -403,9 +400,9 @@ module decode_and_issue (
|
|||
////////////////////////////////////////////////////
|
||||
//Trace Interface
|
||||
generate if (ENABLE_TRACE_INTERFACE) begin
|
||||
assign tr_operand_stall = |(unit_needed & unit_ready) & issue_valid & ~load_store_operands_ready;
|
||||
assign tr_unit_stall = ~|(unit_needed & unit_ready) & issue_valid & load_store_operands_ready;
|
||||
assign tr_no_id_stall = |(unit_needed & unit_ready) & (fb_valid & ~ti.id_available & ~gc_issue_hold & ~gc_fetch_flush) & load_store_operands_ready;
|
||||
assign tr_operand_stall = |(unit_needed & unit_ready) & issue_valid & ~|(unit_operands_ready & issue_ready);
|
||||
assign tr_unit_stall = ~|(unit_needed & unit_ready) & issue_valid & |(unit_operands_ready & issue_ready);
|
||||
assign tr_no_id_stall = |(unit_needed & unit_ready) & (fb_valid & ~ti.id_available & ~gc_issue_hold & ~gc_fetch_flush) & |(unit_operands_ready & issue_ready);
|
||||
assign tr_no_instruction_stall = ~fb_valid | gc_fetch_flush;
|
||||
assign tr_other_stall = fb_valid & ~instruction_issued & ~(tr_operand_stall | tr_unit_stall | tr_no_id_stall | tr_no_instruction_stall);
|
||||
assign tr_branch_operand_stall = tr_operand_stall & unit_needed[BRANCH_UNIT_ID];
|
||||
|
|
|
@ -150,9 +150,10 @@ interface tracking_interface;
|
|||
inflight_instruction_packet inflight_packet;
|
||||
logic issued;
|
||||
logic [WB_UNITS_WIDTH-1:0] issue_unit_id;
|
||||
logic exception_possible;
|
||||
|
||||
modport decode (input issue_id, id_available, output inflight_packet, issued, issue_unit_id);
|
||||
modport wb (output issue_id, id_available, input inflight_packet, issued, issue_unit_id);
|
||||
modport decode (input issue_id, id_available, output inflight_packet, issued, issue_unit_id, exception_possible);
|
||||
modport wb (output issue_id, id_available, input inflight_packet, issued, issue_unit_id, exception_possible);
|
||||
endinterface
|
||||
|
||||
interface fifo_interface #(parameter DATA_WIDTH = 42);//#(parameter type data_type = logic[31:0]);
|
||||
|
@ -209,40 +210,24 @@ interface tlb_interface;
|
|||
|
||||
endinterface
|
||||
|
||||
interface store_buffer_request_interface;
|
||||
//Request signals
|
||||
logic [31:0] addr;
|
||||
logic [2:0] fn3;
|
||||
interface load_store_queue_interface;
|
||||
|
||||
logic ready;
|
||||
data_access_shared_inputs_t transaction_in;
|
||||
logic valid;
|
||||
|
||||
logic [31:0] data;
|
||||
logic data_valid;
|
||||
instruction_id_t data_id;
|
||||
|
||||
logic valid;
|
||||
instruction_id_t id;
|
||||
|
||||
logic ready;
|
||||
|
||||
modport store_buffer (input addr, fn3, data, data_valid, data_id, valid, id, output ready);
|
||||
modport ls (output addr, fn3, data, data_valid, data_id, valid, id, input ready);
|
||||
endinterface
|
||||
|
||||
interface store_buffer_output_interface;
|
||||
|
||||
logic [31:0] addr;
|
||||
logic [3:0] be;
|
||||
logic [2:0] fn3;
|
||||
logic [31:0] data;
|
||||
logic valid;
|
||||
instruction_id_t id;
|
||||
data_access_shared_inputs_t transaction_out;
|
||||
logic transaction_ready;
|
||||
logic accepted;
|
||||
|
||||
modport store_buffer (output addr, be, fn3, data, valid, id, input accepted);
|
||||
modport ls (input addr, be, fn3, data, valid, id, output accepted);
|
||||
|
||||
modport queue (input transaction_in, data_valid, data_id, valid, accepted, output ready, transaction_out, transaction_ready);
|
||||
modport ls (output transaction_in, data_valid, data_id, valid, accepted, input ready, transaction_out, transaction_ready);
|
||||
endinterface
|
||||
|
||||
|
||||
interface ls_sub_unit_interface #(parameter BASE_ADDR = 32'h00000000, parameter UPPER_BOUND = 32'hFFFFFFFF, parameter BIT_CHECK = 4);
|
||||
logic data_valid;
|
||||
logic ready;
|
||||
|
|
195
core/load_store_queue.sv
Normal file
195
core/load_store_queue.sv
Normal file
|
@ -0,0 +1,195 @@
|
|||
/*
|
||||
* Copyright © 2020 Eric Matthews, Lesley Shannon
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the 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.
|
||||
*
|
||||
* Initial code developed under the supervision of Dr. Lesley Shannon,
|
||||
* Reconfigurable Computing Lab, Simon Fraser University.
|
||||
*
|
||||
* Author(s):
|
||||
* Eric Matthews <ematthew@sfu.ca>
|
||||
*/
|
||||
|
||||
import taiga_config::*;
|
||||
import riscv_types::*;
|
||||
import taiga_types::*;
|
||||
|
||||
module load_store_queue (
|
||||
input logic clk,
|
||||
input logic rst,
|
||||
|
||||
load_store_queue_interface.queue lsq,
|
||||
|
||||
//Address collision checking
|
||||
input logic [31:0] compare_addr,
|
||||
input logic compare,
|
||||
output logic address_conflict,
|
||||
|
||||
//Writeback data
|
||||
input logic potential_exception,
|
||||
input instruction_id_t writeback_id,
|
||||
input logic [31:0] writeback_data,
|
||||
input logic writeback_valid
|
||||
);
|
||||
|
||||
logic [MAX_INFLIGHT_COUNT-1:0] valid;
|
||||
logic [MAX_INFLIGHT_COUNT-1:0] waiting_for_data;
|
||||
|
||||
instruction_id_t oldest_id;
|
||||
|
||||
localparam TAG_W = 3;
|
||||
localparam TAG_OFFSET = 2;
|
||||
|
||||
logic [TAG_W-1:0] tag_addr [MAX_INFLIGHT_COUNT];
|
||||
|
||||
|
||||
data_access_shared_inputs_t transactions [MAX_INFLIGHT_COUNT];
|
||||
data_access_shared_inputs_t oldest_transaction;
|
||||
instruction_id_t data_ids [MAX_INFLIGHT_COUNT];
|
||||
instruction_id_t data_id;
|
||||
|
||||
logic [31:0] store_data [MAX_INFLIGHT_COUNT];
|
||||
logic [31:0] data_for_alignment;
|
||||
logic [1:0] data_address_alignment;
|
||||
logic writeback_data_match;
|
||||
logic update_store_data;
|
||||
|
||||
logic [MAX_INFLIGHT_COUNT-1:0] issuing_one_hot;
|
||||
logic [MAX_INFLIGHT_COUNT-1:0] new_id_one_hot;
|
||||
logic [MAX_INFLIGHT_COUNT-1:0] is_store;
|
||||
|
||||
logic [MAX_INFLIGHT_COUNT-1:0] need_data_one_hot;
|
||||
|
||||
logic [MAX_INFLIGHT_COUNT-1:0] new_data;
|
||||
|
||||
logic [MAX_INFLIGHT_COUNT-1:0] writeback_store_one_hot;
|
||||
|
||||
fifo_interface #(.DATA_WIDTH($bits(instruction_id_t))) oldest_id_fifo ();
|
||||
////////////////////////////////////////////////////
|
||||
//Implementation
|
||||
////////////////////////////////////////////////////
|
||||
|
||||
taiga_fifo #(.DATA_WIDTH($bits(instruction_id_t)), .FIFO_DEPTH(MAX_INFLIGHT_COUNT)) id_fifo (.fifo(oldest_id_fifo), .*);
|
||||
assign oldest_id_fifo.data_in = lsq.transaction_in.id;
|
||||
assign oldest_id_fifo.push = lsq.valid;
|
||||
assign oldest_id_fifo.supress_push = 0;
|
||||
assign oldest_id_fifo.pop = lsq.accepted;
|
||||
assign oldest_id = oldest_id_fifo.data_out;
|
||||
|
||||
//Request attributes that need only a single read-port
|
||||
always_ff @ (posedge clk) begin
|
||||
if (lsq.valid) begin
|
||||
transactions[lsq.transaction_in.id] <= lsq.transaction_in;
|
||||
data_ids[lsq.transaction_in.id] <= lsq.data_valid ? lsq.transaction_in.id : lsq.data_id;
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
assign writeback_data_match = writeback_valid & waiting_for_data[writeback_id];
|
||||
|
||||
//LUTRAM for the store data
|
||||
assign update_store_data = (lsq.valid & lsq.data_valid) | writeback_data_match;
|
||||
always_ff @ (posedge clk) begin
|
||||
if (update_store_data)
|
||||
store_data[writeback_data_match ? writeback_id : lsq.transaction_in.id] <= writeback_data_match ? writeback_data : lsq.transaction_in.data_in;
|
||||
end
|
||||
|
||||
always_comb begin
|
||||
new_id_one_hot = 0;
|
||||
new_id_one_hot[lsq.transaction_in.id] = lsq.valid;
|
||||
|
||||
need_data_one_hot = 0;
|
||||
need_data_one_hot[lsq.data_id] = lsq.valid & lsq.transaction_in.store & ~lsq.data_valid;
|
||||
|
||||
issuing_one_hot = 0;
|
||||
issuing_one_hot[oldest_id] = lsq.accepted;
|
||||
|
||||
writeback_store_one_hot = 0;
|
||||
writeback_store_one_hot[writeback_id] = writeback_valid;
|
||||
end
|
||||
|
||||
always_ff @ (posedge clk) begin
|
||||
if (rst)
|
||||
valid <= 0;
|
||||
else
|
||||
valid <= (new_id_one_hot | valid) & ~issuing_one_hot;
|
||||
end
|
||||
|
||||
always_ff @ (posedge clk) begin
|
||||
if (rst)
|
||||
is_store <= 0;
|
||||
else
|
||||
is_store <= ((new_id_one_hot & {MAX_INFLIGHT_COUNT{lsq.transaction_in.store}}) | is_store) & ~issuing_one_hot;
|
||||
end
|
||||
|
||||
always_ff @ (posedge clk) begin
|
||||
if (rst)
|
||||
waiting_for_data <= 0;
|
||||
else
|
||||
waiting_for_data <= (need_data_one_hot | waiting_for_data) & ~writeback_store_one_hot;
|
||||
end
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
//Collision checking
|
||||
always_ff @ (posedge clk) begin
|
||||
foreach(tag_addr[i]) begin
|
||||
if (new_id_one_hot[i])
|
||||
tag_addr[i] <= lsq.transaction_in.addr[0+:TAG_W];
|
||||
end
|
||||
end
|
||||
|
||||
always_comb begin
|
||||
address_conflict = 0;
|
||||
for (int i=0; i < MAX_INFLIGHT_COUNT; i++) begin
|
||||
address_conflict |= (tag_addr[i] == compare_addr[0+:TAG_W]) && valid[i] && is_store[i];
|
||||
end
|
||||
end
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
//Output
|
||||
assign data_id = data_ids[oldest_id];
|
||||
assign oldest_transaction = transactions[oldest_id];
|
||||
|
||||
//Can accept an input so long as it is a load or an update from writeback for an exisiting store is not in progress
|
||||
assign lsq.ready = (lsq.transaction_in.load | ~writeback_data_match);
|
||||
|
||||
assign lsq.transaction_ready = valid[oldest_id] & (oldest_transaction.load | ~waiting_for_data[data_id]);
|
||||
|
||||
always_comb begin
|
||||
data_for_alignment = lsq.transaction_ready ? store_data[data_id] : lsq.transaction_in.data_in;
|
||||
|
||||
lsq.transaction_out = lsq.transaction_ready ? oldest_transaction : lsq.transaction_in;
|
||||
lsq.transaction_out.id = lsq.transaction_ready ? oldest_id : lsq.transaction_in.id;
|
||||
|
||||
//Input: ABCD
|
||||
//Assuming aligned requests,
|
||||
//Possible byte selections: (A/C/D, B/D, C/D, D)
|
||||
lsq.transaction_out.data_in[7:0] = data_for_alignment[7:0];
|
||||
lsq.transaction_out.data_in[15:8] = (lsq.transaction_out.addr[1:0] == 2'b01) ? data_for_alignment[7:0] : data_for_alignment[15:8];
|
||||
lsq.transaction_out.data_in[23:16] = (lsq.transaction_out.addr[1:0] == 2'b10) ? data_for_alignment[7:0] : data_for_alignment[23:16];
|
||||
case(lsq.transaction_out.addr[1:0])
|
||||
2'b10 : lsq.transaction_out.data_in[31:24] = data_for_alignment[15:8];
|
||||
2'b11 : lsq.transaction_out.data_in[31:24] = data_for_alignment[7:0];
|
||||
default : lsq.transaction_out.data_in[31:24] = data_for_alignment[31:24];
|
||||
endcase
|
||||
end
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
//End of Implementation
|
||||
////////////////////////////////////////////////////
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
//Assertions
|
||||
|
||||
|
||||
endmodule
|
|
@ -50,6 +50,11 @@ module load_store_unit (
|
|||
|
||||
//Store-Writeback Interface
|
||||
input instruction_id_t oldest_id,
|
||||
output logic load_store_exception_clear,
|
||||
output instruction_id_t load_store_exception_id,
|
||||
input logic potential_exception,
|
||||
|
||||
input instruction_id_t writeback_id,
|
||||
input logic [31:0] writeback_data,
|
||||
input logic writeback_valid,
|
||||
output instruction_id_t store_done_id,
|
||||
|
@ -82,20 +87,19 @@ module load_store_unit (
|
|||
|
||||
logic units_ready;
|
||||
logic unit_switch_stall;
|
||||
logic ready_for_store;
|
||||
logic ready_for_load;
|
||||
logic ready_for_delayed_store;
|
||||
logic ready_for_bypass_store;
|
||||
logic ready_for_issue;
|
||||
logic bypass_possible;
|
||||
logic issue_request;
|
||||
logic load_complete;
|
||||
|
||||
logic use_store_buffer;
|
||||
|
||||
logic [31:0] virtual_address;
|
||||
|
||||
logic [31:0] unit_muxed_load_data;
|
||||
logic [31:0] aligned_load_data;
|
||||
logic [31:0] final_load_data;
|
||||
|
||||
|
||||
logic [31:0] unit_data_array [NUM_SUB_UNITS-1:0];
|
||||
logic [NUM_SUB_UNITS-1:0] unit_ready;
|
||||
logic [NUM_SUB_UNITS-1:0] unit_data_valid;
|
||||
|
@ -115,86 +119,28 @@ module load_store_unit (
|
|||
} load_attributes_t;
|
||||
load_attributes_t load_attributes_in, stage2_attr;
|
||||
|
||||
logic [3:0] be;
|
||||
//FIFOs
|
||||
fifo_interface #(.DATA_WIDTH($bits(load_attributes_t))) load_attributes();
|
||||
|
||||
store_buffer_request_interface sb_request();
|
||||
store_buffer_output_interface sb_output();
|
||||
load_store_queue_interface lsq();
|
||||
|
||||
logic [31:0] compare_addr;
|
||||
logic compare;
|
||||
logic address_conflict;
|
||||
logic store_buffer_bypassable;
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
//Implementation
|
||||
////////////////////////////////////////////////////
|
||||
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
//Unit tracking
|
||||
assign current_unit = sub_unit_address_match;
|
||||
|
||||
initial last_unit = BRAM_ID;
|
||||
always_ff @ (posedge clk) begin
|
||||
if (load_attributes.push)
|
||||
last_unit <= sub_unit_address_match;
|
||||
end
|
||||
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
//Store buffer
|
||||
assign sb_request.addr = tlb.physical_address;
|
||||
assign sb_request.fn3 = ls_inputs.fn3;
|
||||
assign sb_request.data = ls_inputs.rs2;
|
||||
assign sb_request.data_valid = ~ls_inputs.forwarded_store;
|
||||
assign sb_request.data_id = ls_inputs.store_forward_id;
|
||||
assign sb_request.valid = issue.new_request & ls_inputs.store;
|
||||
assign sb_request.id = issue.instruction_id;
|
||||
|
||||
assign sb_output.accepted = use_store_buffer & units_ready;
|
||||
|
||||
store_buffer sb (.*);
|
||||
assign compare_addr = virtual_address;
|
||||
assign compare = ls_inputs.load;
|
||||
|
||||
assign store_done_id = sb_output.id;
|
||||
assign store_complete = sb_output.accepted;
|
||||
////////////////////////////////////////////////////
|
||||
//Primary Control Signals
|
||||
assign units_ready = &unit_ready;
|
||||
assign load_complete = |unit_data_valid;
|
||||
|
||||
assign use_store_buffer = sb_output.valid;
|
||||
|
||||
assign ready_for_load = ls_inputs.load & (~address_conflict) & (~unit_switch_stall) & units_ready & (~sb_output.valid);
|
||||
assign ready_for_store = ls_inputs.store & sb_request.ready;
|
||||
|
||||
assign issue.ready = (ready_for_load | ready_for_store);
|
||||
|
||||
always_ff @ (posedge clk) begin
|
||||
if (rst)
|
||||
unit_switch_stall <= 0;
|
||||
else if (issue_request && (current_unit != last_unit) && load_attributes.valid)
|
||||
unit_switch_stall <= 1;
|
||||
else if (!load_attributes.valid)
|
||||
unit_switch_stall <= 0;
|
||||
end
|
||||
|
||||
//When switching units, ensure no outstanding loads so that there can be no timing collisions with results
|
||||
assign unit_stall = (current_unit != last_unit) && load_attributes.valid;
|
||||
assign issue_request = issue.new_request | sb_output.accepted;
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
//TLB interface
|
||||
assign virtual_address = ls_inputs.rs1 + 32'(signed'(ls_inputs.offset));
|
||||
|
||||
assign tlb.virtual_address = virtual_address;
|
||||
assign tlb.new_request = issue_request;
|
||||
assign tlb.execute = 0;
|
||||
assign tlb.rnw = ls_inputs.load & ~ls_inputs.store;
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
//Alignment Exception
|
||||
assign load_store_exception_clear = issue.new_request;
|
||||
assign load_store_exception_id = issue.instruction_id;
|
||||
|
||||
|
||||
always_comb begin
|
||||
case(ls_inputs.fn3)
|
||||
LS_H_fn3 : unaligned_addr = virtual_address[0];
|
||||
|
@ -216,26 +162,100 @@ module load_store_unit (
|
|||
// assign ls_exception.id = issue.instruction_id;
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
//Unit Inputs
|
||||
assign shared_inputs.addr = use_store_buffer ? sb_output.addr : virtual_address;
|
||||
assign shared_inputs.load = ~use_store_buffer;
|
||||
assign shared_inputs.store = use_store_buffer;
|
||||
assign shared_inputs.be = use_store_buffer ? sb_output.be : 0;
|
||||
assign shared_inputs.fn3 = use_store_buffer ? sb_output.fn3 : ls_inputs.fn3;
|
||||
assign shared_inputs.data_in = sb_output.data;
|
||||
//TLB interface
|
||||
assign virtual_address = ls_inputs.rs1 + 32'(signed'(ls_inputs.offset));
|
||||
|
||||
assign tlb.virtual_address = virtual_address;
|
||||
assign tlb.new_request = issue_request;
|
||||
assign tlb.execute = 0;
|
||||
assign tlb.rnw = ls_inputs.load & ~ls_inputs.store;
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
//Byte enable generation
|
||||
//Only set on store
|
||||
// SW: all bytes
|
||||
// SH: upper or lower half of bytes
|
||||
// SB: specific byte
|
||||
always_comb begin
|
||||
be = 0;
|
||||
case(ls_inputs.fn3[1:0])
|
||||
LS_B_fn3[1:0] : be[virtual_address[1:0]] = 1;
|
||||
LS_H_fn3[1:0] : begin
|
||||
be[virtual_address[1:0]] = 1;
|
||||
be[{virtual_address[1], 1'b1}] = 1;
|
||||
end
|
||||
default : be = '1;
|
||||
endcase
|
||||
be &= {4{~ls_inputs.load}};
|
||||
end
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
//Load Store Queue
|
||||
assign lsq.transaction_in.addr = virtual_address;
|
||||
assign lsq.transaction_in.fn3 = ls_inputs.fn3;
|
||||
assign lsq.transaction_in.be = be;
|
||||
assign lsq.transaction_in.data_in = ls_inputs.rs2;
|
||||
assign lsq.transaction_in.load = ls_inputs.load;
|
||||
assign lsq.transaction_in.store = ls_inputs.store;
|
||||
assign lsq.transaction_in.id = issue.instruction_id;
|
||||
|
||||
assign lsq.data_valid = ~ls_inputs.forwarded_store;
|
||||
assign lsq.data_id = ls_inputs.store_forward_id;
|
||||
assign lsq.valid = issue.new_request & ~bypass_possible;
|
||||
|
||||
load_store_queue lsq_block (.*);
|
||||
assign compare_addr = virtual_address;
|
||||
assign compare = 1;//ls_inputs.load;
|
||||
|
||||
assign lsq.accepted = lsq.transaction_ready & ready_for_issue;
|
||||
|
||||
assign store_done_id = shared_inputs.id;
|
||||
assign store_complete = (lsq.accepted & shared_inputs.store) | (issue.new_request & ls_inputs.store & bypass_possible);
|
||||
|
||||
assign shared_inputs = lsq.transaction_out;
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
//Unit tracking
|
||||
assign current_unit = sub_unit_address_match;
|
||||
|
||||
initial last_unit = BRAM_ID;
|
||||
always_ff @ (posedge clk) begin
|
||||
if (load_attributes.push)
|
||||
last_unit <= sub_unit_address_match;
|
||||
end
|
||||
|
||||
//When switching units, ensure no outstanding loads so that there can be no timing collisions with results
|
||||
assign unit_stall = (current_unit != last_unit) && load_attributes.valid;
|
||||
always_ff @ (posedge clk) begin
|
||||
if (rst)
|
||||
unit_switch_stall <= 0;
|
||||
else if (issue_request && (current_unit != last_unit) && load_attributes.valid)
|
||||
unit_switch_stall <= 1;
|
||||
else if (~load_attributes.valid)
|
||||
unit_switch_stall <= 0;
|
||||
end
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
//Primary Control Signals
|
||||
assign units_ready = &unit_ready;
|
||||
assign load_complete = |unit_data_valid;
|
||||
|
||||
assign ready_for_issue = units_ready & (~unit_switch_stall);
|
||||
assign bypass_possible = ready_for_issue & (~address_conflict) & (~lsq.transaction_ready) & (~ls_inputs.forwarded_store);
|
||||
|
||||
assign issue.ready = lsq.ready;
|
||||
assign issue_request = (issue.new_request & bypass_possible) | lsq.accepted;
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
//Load attributes FIFO
|
||||
one_hot_to_integer #(NUM_SUB_UNITS) hit_way_conv (.*, .one_hot(sub_unit_address_match), .int_out(load_attributes_in.subunit_id));
|
||||
taiga_fifo #(.DATA_WIDTH($bits(load_attributes_t)), .FIFO_DEPTH(ATTRIBUTES_DEPTH))
|
||||
attributes_fifo (.fifo(load_attributes), .*);
|
||||
assign load_attributes_in.fn3 = ls_inputs.fn3;
|
||||
assign load_attributes_in.byte_addr = virtual_address[1:0];
|
||||
assign load_attributes_in.instruction_id = issue.instruction_id;
|
||||
one_hot_to_integer #(NUM_SUB_UNITS) sub_unit_select (.*, .one_hot(sub_unit_address_match), .int_out(load_attributes_in.subunit_id));
|
||||
taiga_fifo #(.DATA_WIDTH($bits(load_attributes_t)), .FIFO_DEPTH(ATTRIBUTES_DEPTH)) attributes_fifo (.fifo(load_attributes), .*);
|
||||
assign load_attributes_in.fn3 = shared_inputs.fn3;
|
||||
assign load_attributes_in.byte_addr = shared_inputs.addr[1:0];
|
||||
assign load_attributes_in.instruction_id = shared_inputs.id;
|
||||
|
||||
assign load_attributes.data_in = load_attributes_in;
|
||||
|
||||
assign load_attributes.push = issue_request & ~use_store_buffer;
|
||||
assign load_attributes.push = issue_request & shared_inputs.load;
|
||||
assign load_attributes.pop = load_complete;
|
||||
assign load_attributes.supress_push = 0;
|
||||
|
||||
|
@ -311,17 +331,33 @@ module load_store_unit (
|
|||
|
||||
////////////////////////////////////////////////////
|
||||
//Output bank
|
||||
assign wb.rd = ls_done ? final_load_data : csr_rd;
|
||||
|
||||
|
||||
logic exception_complete;
|
||||
logic ls_done;
|
||||
always_ff @ (posedge clk) begin
|
||||
exception_complete <= (issue_request & ls_exception_valid & ls_inputs.load);
|
||||
end
|
||||
assign ls_done = load_complete | exception_complete;
|
||||
|
||||
assign exception_complete = (issue_request & ls_exception_valid & ls_inputs.load);
|
||||
assign wb.rd = ls_done ? final_load_data : csr_rd;
|
||||
assign wb.done = csr_done | ls_done;
|
||||
assign wb.id = csr_done ? csr_id : stage2_attr.instruction_id;
|
||||
|
||||
|
||||
// always_ff @ (posedge clk) begin
|
||||
// exception_complete <= (issue_request & ls_exception_valid & ls_inputs.load);
|
||||
|
||||
// wb.rd <= ls_done ? final_load_data : csr_rd;
|
||||
|
||||
|
||||
// if (rst)
|
||||
// wb.done <= 0;
|
||||
// else
|
||||
// wb.done <= csr_done | ls_done;
|
||||
|
||||
// wb.id <= csr_done ? csr_id : stage2_attr.instruction_id;
|
||||
|
||||
// end
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
//End of Implementation
|
||||
////////////////////////////////////////////////////
|
||||
|
@ -330,6 +366,7 @@ module load_store_unit (
|
|||
//Assertions
|
||||
always_ff @ (posedge clk) begin
|
||||
assert ((issue_request & |sub_unit_address_match) || (!issue_request)) else $error("invalid L/S address");
|
||||
assert ((issue_request & ready_for_issue) || (!issue_request)) else $error("L/S internal request issued without subunits ready");
|
||||
end
|
||||
|
||||
endmodule
|
||||
|
|
|
@ -1,201 +0,0 @@
|
|||
/*
|
||||
* Copyright © 2020 Eric Matthews, Lesley Shannon
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the 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.
|
||||
*
|
||||
* Initial code developed under the supervision of Dr. Lesley Shannon,
|
||||
* Reconfigurable Computing Lab, Simon Fraser University.
|
||||
*
|
||||
* Author(s):
|
||||
* Eric Matthews <ematthew@sfu.ca>
|
||||
*/
|
||||
|
||||
import taiga_config::*;
|
||||
import riscv_types::*;
|
||||
import taiga_types::*;
|
||||
|
||||
module store_buffer (
|
||||
input logic clk,
|
||||
input logic rst,
|
||||
|
||||
store_buffer_request_interface.store_buffer sb_request,
|
||||
store_buffer_output_interface.store_buffer sb_output,
|
||||
|
||||
//Load address collision checking
|
||||
input logic [31:0] compare_addr,
|
||||
input logic compare,
|
||||
output logic address_conflict,
|
||||
|
||||
//Writeback data
|
||||
input instruction_id_t oldest_id,
|
||||
input logic [31:0] writeback_data,
|
||||
input logic writeback_valid
|
||||
);
|
||||
|
||||
localparam NUM_ENTRIES = MAX_INFLIGHT_COUNT;
|
||||
localparam NUM_ENTRIES_W = $clog2(NUM_ENTRIES);
|
||||
|
||||
logic [NUM_ENTRIES-1:0] valid;
|
||||
logic [NUM_ENTRIES-1:0] data_valid;
|
||||
|
||||
instruction_id_t required_id_to_store_id_table [NUM_ENTRIES];
|
||||
instruction_id_t store_waiting_id;
|
||||
|
||||
localparam TAG_W = 16;
|
||||
localparam TAG_OFFSET = 2;
|
||||
|
||||
logic [TAG_W-1:0] tag_addr [NUM_ENTRIES];
|
||||
|
||||
typedef struct packed{
|
||||
logic [31:TAG_W] upper_addr;
|
||||
logic [1:0] lower_addr;
|
||||
logic [2:0] fn3;
|
||||
} transaction_attributes_t;
|
||||
|
||||
transaction_attributes_t new_transaction;
|
||||
transaction_attributes_t transaction_attributes [NUM_ENTRIES];
|
||||
logic [31:0] store_data [NUM_ENTRIES];
|
||||
logic [31:0] new_store_data;
|
||||
logic [31:0] new_store_data_aligned;
|
||||
logic [1:0] data_address_alignment;
|
||||
logic writeback_data_match;
|
||||
logic update_store_data;
|
||||
|
||||
logic [NUM_ENTRIES-1:0] store_issuing_one_hot;
|
||||
logic [NUM_ENTRIES-1:0] new_id_one_hot;
|
||||
logic [NUM_ENTRIES-1:0] writeback_store_one_hot;
|
||||
////////////////////////////////////////////////////
|
||||
//Implementation
|
||||
////////////////////////////////////////////////////
|
||||
|
||||
//Request attributes that need only a single read-port
|
||||
assign new_transaction.upper_addr = sb_request.addr[31:TAG_W];
|
||||
assign new_transaction.lower_addr = sb_request.addr[1:0];
|
||||
assign new_transaction.fn3 = sb_request.fn3;
|
||||
|
||||
always_ff @ (posedge clk) begin
|
||||
if (sb_request.valid)
|
||||
transaction_attributes[sb_request.id] <= new_transaction;
|
||||
end
|
||||
|
||||
//Address tags in registers for parallel comparisons
|
||||
always_ff @ (posedge clk) begin
|
||||
foreach(tag_addr[i]) begin
|
||||
if (new_id_one_hot[i])
|
||||
tag_addr[i] <= sb_request.addr[0+:TAG_W];
|
||||
end
|
||||
end
|
||||
|
||||
//ID translation table for looking up the store ID from the ID needed by the store
|
||||
always_ff @ (posedge clk) begin
|
||||
if (sb_request.valid)
|
||||
required_id_to_store_id_table[sb_request.data_id] <= sb_request.id;
|
||||
end
|
||||
assign store_waiting_id = required_id_to_store_id_table[oldest_id];
|
||||
|
||||
|
||||
assign new_store_data = sb_request.valid ? sb_request.data : writeback_data;
|
||||
assign data_address_alignment = sb_request.valid ? sb_request.addr[1:0] : transaction_attributes[store_waiting_id].lower_addr[1:0];
|
||||
//Input: ABCD
|
||||
//Assuming aligned requests,
|
||||
//Possible byte selections: (A/C/D, B/D, C/D, D)
|
||||
always_comb begin
|
||||
new_store_data_aligned[7:0] = new_store_data[7:0];
|
||||
new_store_data_aligned[15:8] = (data_address_alignment[1:0] == 2'b01) ? new_store_data[7:0] : new_store_data[15:8];
|
||||
new_store_data_aligned[23:16] = (data_address_alignment[1:0] == 2'b10) ? new_store_data[7:0] : new_store_data[23:16];
|
||||
case(data_address_alignment[1:0])
|
||||
2'b10 : new_store_data_aligned[31:24] = new_store_data[15:8];
|
||||
2'b11 : new_store_data_aligned[31:24] = new_store_data[7:0];
|
||||
default : new_store_data_aligned[31:24] = new_store_data[31:24];
|
||||
endcase
|
||||
end
|
||||
|
||||
//LUTRAM for the store data
|
||||
assign writeback_data_match = valid[store_waiting_id] & ~data_valid[store_waiting_id];
|
||||
assign update_store_data = (sb_request.valid & sb_request.data_valid) | (writeback_data_match & writeback_valid);
|
||||
always_ff @ (posedge clk) begin
|
||||
if (update_store_data)
|
||||
store_data[sb_request.valid ? sb_request.id : oldest_id] <= new_store_data_aligned;
|
||||
end
|
||||
|
||||
always_comb begin
|
||||
new_id_one_hot = 0;
|
||||
new_id_one_hot[sb_request.id] = sb_request.valid;
|
||||
|
||||
store_issuing_one_hot = 0;
|
||||
store_issuing_one_hot[oldest_id] = sb_output.accepted;
|
||||
|
||||
writeback_store_one_hot = 0;
|
||||
writeback_store_one_hot[store_waiting_id] = writeback_data_match & writeback_valid;
|
||||
end
|
||||
|
||||
always_ff @ (posedge clk) begin
|
||||
if (rst)
|
||||
valid <= 0;
|
||||
else
|
||||
valid <= (new_id_one_hot | valid) & ~store_issuing_one_hot;
|
||||
end
|
||||
|
||||
always_ff @ (posedge clk) begin
|
||||
if (rst)
|
||||
data_valid <= 0;
|
||||
else
|
||||
data_valid <= (({NUM_ENTRIES{sb_request.data_valid}} & new_id_one_hot) | writeback_store_one_hot | data_valid) & ~store_issuing_one_hot;
|
||||
end
|
||||
|
||||
assign sb_request.ready = 1;
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
//Collision checking
|
||||
always_comb begin
|
||||
address_conflict = 0;
|
||||
for (int i=0; i < NUM_ENTRIES; i++) begin
|
||||
address_conflict |= (tag_addr[i] == compare_addr[0+:TAG_W]) && valid[i];
|
||||
end
|
||||
address_conflict &= compare;// & |valid;//&= compare;
|
||||
end
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
//Output
|
||||
assign sb_output.addr = {transaction_attributes[oldest_id].upper_addr, tag_addr[oldest_id]};//jukiiiyuhhhhhhhhhhhhh , transaction_attributes[oldest_id].lower_addr};
|
||||
assign sb_output.fn3 = transaction_attributes[oldest_id].fn3;
|
||||
assign sb_output.data = store_data[oldest_id];
|
||||
assign sb_output.valid = valid[oldest_id] & data_valid[oldest_id];
|
||||
assign sb_output.id = oldest_id;
|
||||
|
||||
//Byte enable generation
|
||||
//Only set on store
|
||||
// SW: all bytes
|
||||
// SH: upper or lower half of bytes
|
||||
// SB: specific byte
|
||||
always_comb begin
|
||||
sb_output.be = 0;
|
||||
case(sb_output.fn3[1:0])
|
||||
LS_B_fn3[1:0] : sb_output.be[sb_output.addr[1:0]] = 1;
|
||||
LS_H_fn3[1:0] : begin
|
||||
sb_output.be[sb_output.addr[1:0]] = 1;
|
||||
sb_output.be[{sb_output.addr[1], 1'b1}] = 1;
|
||||
end
|
||||
default : sb_output.be = '1;
|
||||
endcase
|
||||
end
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
//End of Implementation
|
||||
////////////////////////////////////////////////////
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
//Assertions
|
||||
|
||||
|
||||
endmodule
|
|
@ -119,10 +119,13 @@ module taiga (
|
|||
//LS
|
||||
instruction_id_t store_done_id;
|
||||
logic store_complete;
|
||||
instruction_id_t writeback_id;
|
||||
logic [31:0] writeback_data;
|
||||
logic writeback_valid;
|
||||
|
||||
post_issue_forwarding_interface store_forwarding();
|
||||
logic load_store_exception_clear;
|
||||
instruction_id_t load_store_exception_id;
|
||||
logic potential_exception;
|
||||
|
||||
//Trace Interface Signals
|
||||
logic tr_operand_stall;
|
||||
|
|
|
@ -156,7 +156,7 @@ package taiga_config;
|
|||
////////////////////////////////////////////////////
|
||||
//Branch Predictor Options
|
||||
parameter USE_BRANCH_PREDICTOR = 1;
|
||||
parameter BRANCH_PREDICTOR_WAYS = 2;
|
||||
parameter BRANCH_PREDICTOR_WAYS = 1;
|
||||
parameter BRANCH_TABLE_ENTRIES = 512;
|
||||
parameter RAS_DEPTH = 8;
|
||||
|
||||
|
@ -169,7 +169,7 @@ package taiga_config;
|
|||
|
||||
////////////////////////////////////////////////////
|
||||
//Trace Options
|
||||
parameter ENABLE_TRACE_INTERFACE = 1;
|
||||
parameter ENABLE_TRACE_INTERFACE = 0;
|
||||
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
|
|
|
@ -216,6 +216,7 @@ package taiga_types;
|
|||
logic [3:0] be;
|
||||
logic [2:0] fn3;
|
||||
logic [31:0] data_in;
|
||||
instruction_id_t id;
|
||||
} data_access_shared_inputs_t;
|
||||
|
||||
typedef enum {
|
||||
|
|
|
@ -38,8 +38,15 @@ module write_back(
|
|||
output logic instruction_queue_empty,
|
||||
|
||||
output instruction_id_t oldest_id,
|
||||
|
||||
input logic load_store_exception_clear,
|
||||
input instruction_id_t load_store_exception_id,
|
||||
output logic potential_exception,
|
||||
|
||||
output instruction_id_t writeback_id,
|
||||
output logic [31:0] writeback_data,
|
||||
output logic writeback_valid,
|
||||
|
||||
input instruction_id_t store_done_id,
|
||||
input logic store_complete,
|
||||
|
||||
|
@ -67,6 +74,8 @@ module write_back(
|
|||
inflight_instruction_packet retiring_instruction_packet;
|
||||
|
||||
logic [MAX_INFLIGHT_COUNT-1:0] id_inuse;
|
||||
logic [MAX_INFLIGHT_COUNT-1:0] id_potential_exception;
|
||||
logic [MAX_INFLIGHT_COUNT-1:0] exception_cleared_one_hot;
|
||||
|
||||
logic [MAX_INFLIGHT_COUNT-1:0] id_writeback_pending;
|
||||
logic [MAX_INFLIGHT_COUNT-1:0] id_writeback_pending_r;
|
||||
|
@ -161,6 +170,20 @@ module write_back(
|
|||
end
|
||||
assign retiring_instruction_packet = id_metadata[id_retiring];
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
//Potential Exception Tracking
|
||||
// always_comb begin
|
||||
// exception_cleared_one_hot = 0;
|
||||
// exception_cleared_one_hot[load_store_exception_id] = load_store_exception_clear;
|
||||
// end
|
||||
// always_ff @ (posedge clk) begin
|
||||
// if (rst)
|
||||
// id_potential_exception <= 0;
|
||||
// else
|
||||
// id_potential_exception <= (id_potential_exception | {MAX_INFLIGHT_COUNT{ti.exception_possible}} & id_issued_one_hot) & ~exception_cleared_one_hot;
|
||||
// end
|
||||
// assign potential_exception = |id_potential_exception;
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
//Register File Interface
|
||||
//Track whether the ID has a pending write to the register file
|
||||
|
@ -195,8 +218,10 @@ module write_back(
|
|||
assign rf_wb.rd_nzero = |retiring_instruction_packet.rd_addr;
|
||||
assign rf_wb.rd_data = results_by_id[id_retiring];
|
||||
|
||||
assign writeback_data = results_by_id[id_retiring];
|
||||
assign writeback_valid = instruction_complete;
|
||||
//Store Buffer data
|
||||
assign writeback_id = rf_wb.id;
|
||||
assign writeback_data = rf_wb.rd_data;
|
||||
assign writeback_valid = rf_wb.retiring;
|
||||
|
||||
//Register bypass for issue operands
|
||||
assign rf_wb.rs1_valid = id_writeback_pending_r[rf_wb.rs1_id];//includes the instruction writing to the register file
|
||||
|
@ -223,8 +248,8 @@ module write_back(
|
|||
tr_num_instructions_in_flight = 0;
|
||||
tr_num_of_instructions_pending_writeback = 0;
|
||||
for (int i=0; i<MAX_INFLIGHT_COUNT-1; i++) begin
|
||||
tr_num_instructions_in_flight += id_inuse[i];
|
||||
tr_num_of_instructions_pending_writeback += id_writeback_pending[i];
|
||||
tr_num_instructions_in_flight += ID_W'(id_inuse[i]);
|
||||
tr_num_of_instructions_pending_writeback += ID_W'(id_writeback_pending[i]);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -75,10 +75,10 @@ void TaigaTracer<TB>::update_stats() {
|
|||
|
||||
template <class TB>
|
||||
void TaigaTracer<TB>::print_stats() {
|
||||
std::cout << " Taiga trace stats:\n";
|
||||
std::cout << " Taiga trace stats\n";
|
||||
std::cout << "--------------------------------------------------------------\n";
|
||||
for (int i=0; i < numEvents; i++)
|
||||
std::cout << " " << eventNames[i] << ": " << event_counters[i] << std::endl;
|
||||
std::cout << " " << eventNames[i] << ":" << event_counters[i] << std::endl;
|
||||
|
||||
std::cout << "--------------------------------------------------------------\n\n";
|
||||
}
|
||||
|
|
|
@ -68,7 +68,7 @@ int main(int argc, char **argv) {
|
|||
#endif
|
||||
taigaTracer->reset();
|
||||
cout << "--------------------------------------------------------------\n";
|
||||
cout << " Starting Simulation, logging to: " << argv[1] << "\n";
|
||||
cout << " Starting Simulation, logging to " << argv[1] << "\n";
|
||||
cout << "--------------------------------------------------------------\n";
|
||||
|
||||
// Tick the clock until we are done
|
||||
|
@ -85,7 +85,7 @@ int main(int argc, char **argv) {
|
|||
}
|
||||
|
||||
cout << "--------------------------------------------------------------\n";
|
||||
cout << " Simulation Completed: " << taigaTracer->get_cycle_count() << " cycles.\n";
|
||||
cout << " Simulation Completed. " << taigaTracer->get_cycle_count() << " cycles.\n";
|
||||
taigaTracer->print_stats();
|
||||
|
||||
logFile.close();
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
../core/dtag_banks.sv
|
||||
../core/amo_alu.sv
|
||||
../core/dcache.sv
|
||||
../core/store_buffer.sv
|
||||
../core/load_store_queue.sv
|
||||
../core/load_store_unit.sv
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue