Connecting hierarchical modules: struct vs interface in SystemVerilog - system-verilog

In SystemVerilog hierarchical modules can be connected by simple data types, complex data types (structs, unions, etc), or interfaces. The feature that I am interested in is aggregating all signals between two modules in one place which simplifies maintenance of the code.
For example in the following one change s_point's definition without changing the declarations of m1, m2, and top:
typedef struct {
logic [7:0] x;
logic [7:0] y;
} s_point; // named structure
module m1 (output s_point outPoint);
//
endmodule
module m2 (input s_point inPoint);
//
endmodule
module top ();
s_point point;
m1 m1_inst (point);
m2 m2_inst (point);
endmodule
Alternatively, this could have been done using interfaces.
However, I believe using structures are easier and they are supported by more CAD tools, and they don't need to be instantiated as is the case for interfaces (although one still has to declare them in the top module).
My question is if only aggregating signals is required (i.e. no interest in interface's task, functions, modports, generic interface, internal always blocks etc) for a synthesizable design, which one is preferred?

interface is preferred.
A struct okay to use only when all the signals within the struct all follow the same port direction; input, output, or inout wire. It becomes challenging to use structs when driving directions become mixed. Mixed direction is a allow using the ref keyword, however the ref keyword is not supported by many synthesis tools, yet. inout cannot be used because logic is considered a variable, IEEE Std 1800-2012 ยง 6.5 Nets and variables. However, inout wire can be used to cast the struct as a net. The components of a stuct can not be assigned within an always-block and needs an assign statement instead; just like a regular wire.
An interface should be grouping signals where the port direction is not consistent. Tri-states need to be defined as wire and the variable types do not require explicit port direction when used in conjunction with always_{ff|comb|latch}. It should also be used if the signals are part of a protocol. This way assertions can be added and it can be connected to a classes for an UVM test-bench or other SVTB environment.
Use a sturct when only passing a explicit direction data type. Use an interface for passing shared signals.
Example Senario:
Imagine there is a collection of signals x,y,&z, where module m_x drives x and reads y&z, module m_b drive y and reads x&z, and module m_z drives z and reads x&y. Each signals have only one driver and always_ff can be used to guarantee this.
If we try adding a bidirectional tri-state bus, to the mix then the struct cannot be used. A wire resolves conflicting drivers while logic/reg clobber and keep the scheduler running.
Sample code:
Using struct using ref (no tri-state allowed):
typedef struct {logic [7:0] x, y, z, bus; } s_point;
module m_x_st (ref s_point point, input clk);
always_ff #(posedge clk)
point.x <= func(point.y, point.z);
//assign point.bus = (point.y!=point.z) ? 'z : point.x; // NO tir-state
endmodule
module m_y_st (ref s_point point, input clk);
always_ff #(posedge clk)
point.y <= func(point.x, point.z);
//assign point.bus = (point.x!=point.z) ? 'z : point.y; // NO tir-state
endmodule
module m_z_st (ref s_point point, input clk);
always_ff #(posedge clk)
point.z <= func(point.x, point.y);
//assign point.bus = (point.x!=point.y) ? 'z : point.z; // NO tir-state
endmodule
module top_with_st (input clk);
s_point point;
m_x_st mx_inst (point,clk);
m_y_st my_inst (point,clk);
m_z_st mz_inst (point,clk);
endmodule
Using struct using inout wire (nets must be driven with assign, loses single driver guarantee):
typedef struct {logic [7:0] x, y, z, bus; } s_point;
module m_x_wst (inout wire s_point point, input clk);
logic [$size(point.x)-1:0] tmp;
assign point.x = tmp;
always_ff #(posedge clk)
tmp <= func(point.y, point.z);
assign point.bus = (point.y!=point.z) ? 'z : point.x; // tir-state
endmodule
module m_y_wst (inout wire s_point point, input clk);
logic [$size(point.y)-1:0] tmp;
assign point.y = tmp;
always_ff #(posedge clk)
tmp <= func(point.x, point.z);
assign point.bus = (point.x!=point.z) ? 'z : point.y; // tir-state
endmodule
module m_z_wst (inout wire s_point point, input clk);
logic [$size(point.z)-1:0] tmp;
assign point.z = tmp;
always_ff #(posedge clk)
tmp <= func(point.x, point.y);
assign point.bus = (point.x!=point.y) ? 'z : point.z; // tri-state
endmodule
module top_with_wst (input clk);
wire s_point point; // must have the 'wire' keyword
m_x_wst mx_inst (point,clk);
m_y_wst my_inst (point,clk);
m_z_wst mz_inst (point,clk);
endmodule
Using interface (with tri-state):
interface if_point;
logic [7:0] x, y, z;
wire [7:0] bus; // tri-state must be wire
endinterface
module m_x_if (if_point point, input clk);
always_ff #(posedge clk)
point.x <= func(point.y, point.z);
assign point.bus = (point.y!=point.z) ? 'z : point.x;
endmodule
module m_y_if (if_point point, input clk);
always_ff #(posedge clk)
point.y <= func(point.x, point.z);
assign point.bus = (point.x!=point.z) ? 'z : point.y;
endmodule
module m_z_if (if_point point, input clk);
always_ff #(posedge clk)
point.z <= func(point.x, point.y);
assign point.bus = (point.x!=point.y) ? 'z : point.z;
endmodule
module top_with_if (input clk);
if_point point();
m_x_if mx_inst (point,clk);
m_y_if my_inst (point,clk);
m_z_if mz_inst (point,clk);
endmodule
Running code: http://www.edaplayground.com/s/6/1150

Related

How to make an empty datatype or conditional field in SystemVerilog

I'd like to write code in which some inputs don't exist. I'd like to create a structure which contains those params (among other things). How can I do it? I tried to use generate, define an empty struct (typedef struct {} empty_t) and 0-size array (logic foo[0]) but all my attempts failed as syntax errors.
EDIT: I'm trying to do something like (simplified):
module foo(clk, data_in, opt_data_in);
parameter USE_OPT_IN = 1;
input logic clk;
input logic data_in;
input logic opt_data_in;
typedef struct packed {
logic data_in;
// Since it's stored in on-chip RAM I'd like it
// to be as small as possible
if (USE_OPT_IN != 0)
logic opt_data_in;
} ram_entry_t;
ram_entry_t my_ram[4096];
always_ff #(posedge clk) begin
ram_entry_t new_entry;
new_entry.data_in = data_in;
if (USE_OPT_IN != 0)
new_entry.opt_data_in = opt_data_in;
my_ram[$random() % 4096] = new_entry;
end
endmodule
This of course does not compile.
EDIT2: Above is simplified example. There is over 7 optional fields and coding 128 structs seems unreasonable - compared with just using unstructured bit field.
There is no such thing as an optional port or empty datatype. The code inside the module that references the port has to make sense for any situation.
There are a few things you can do
You can assign a default value to an input port that will be used if left unconnected.
You can parameterize a port type to use different structures with different number of fields. Again, the code inside the module must work for any datatype. This usually means casting the input to a known type inside the module.
You can also use an variable sized array if all the input types are the same.
I guess the only suggestion is to use a TYPE as a parameter to the module where the latter could be a struct with or without the field. You will also need a genarate block unless you can work with interfaces or classes. Here is a skeleton way of doing it. It would probably require yet another OPT param though for generate block. Sorry, i have not try to compile it.
module foo#(parameter type TYPE = int, parameter bit OPT = 1)
(clk, data_in, opt_data_in);
input logic clk;
input logic data_in;
input logic out_data_in;
TYPE my_ram[4096];
if (OPT) begin
always_ff #(posedge clk) begin
TYPE new_entry;
new_entry.data_in <= data_in;
new_entry.opt_data_in <= opt_data_in;
my_ram[$random() % 4096] <= new_entry;
end
else begin
always_ff #(posedge clk) begin
TYPE new_entry;
new_entry.data_in <= data_in;
my_ram[$random() % 4096] <= new_entry;
end
end
endmodule
now you can instantiate you module with different classes:
typedef struct packed {
logic data_in;
logic opt_data_in;
} ram_entry_full_t;
typedef struct packed {
logic data_in;
} ram_entry_short_t;
foo#(ram_entry_full_t, 1) foo_full(..);
foo#(ram_entry_short_t, 0) foo_short(..);
I'll build on #Serge answer. If you want to make your RAM entry types external to the module and use them as parameters, you can do away with the USE_OPT_IN parameter altogether, as this is redundant:
typedef struct packed {
logic data_in;
logic opt_data_in;
} ram_entry_full_t;
typedef struct packed {
logic data_in;
} ram_entry_short_t;
module foo #(parameter type TYPE = int) (clk, data_in, opt_data_in);
input logic clk;
input logic data_in;
input logic out_data_in;
TYPE my_ram[4096];
if (TYPE == type(ram_entry_full_t)) begin
always_ff #(posedge clk) begin
TYPE new_entry;
new_entry.data_in <= data_in;
new_entry.opt_data_in <= opt_data_in;
my_ram[$random() % 4096] <= new_entry;
end
end
else if (TYPE == type(ram_entry_short_t)) begin
always_ff #(posedge clk) begin
TYPE new_entry;
new_entry.data_in <= data_in;
my_ram[$random() % 4096] <= new_entry;
end
end
else begin
$error("Unsuported type ...");
end
endmodule
You could replace the if/else cascade with a case to make the code slightly more readable. You could also refactor the initialization code a bit by extracting functions to avoid duplication.
You could also use TYPE as a port for your top module, instead of data_in and opt_data_in.
For option 2, you can leave your module signature as in your question and do all the magic inside:
module foo(clk, data_in, opt_data_in);
parameter bit USE_OPT_IN = 1;
input logic clk;
input logic data_in;
input logic opt_data_in;
typedef struct packed {
logic data_in;
logic opt_data_in;
} ram_entry_full_t;
typedef struct packed {
logic data_in;
} ram_entry_short_t;
if (USE_OPT_IN) begin
always_ff #(posedge clk) begin
ram_entry_full_t new_entry;
new_entry.data_in <= data_in;
new_entry.opt_data_in <= opt_data_in;
my_ram[$random() % 4096] <= new_entry;
end
end
else begin
always_ff #(posedge clk) begin
ram_entry_short_t new_entry;
new_entry.data_in <= data_in;
my_ram[$random() % 4096] <= new_entry;
end
end
endmodule
This way, it's impossible to instantiate your module with the wrong parameter and if you don't break interface compatibility if your module is already in use somewhere.

How to pass information from inside sequence/property to the outside world

I have a property that is waiting for two different sequence to happen, as follows (the sequence and property are already simplified here):
sequence seq1;
logic [3:0] a;
#(posedge clk) (some_signal1 == 1, a = other_signal1);
endsequence
sequence seq2;
logic [3:0] b;
#(posedge clk) (some_signal2 == 1, b = other_signal2);
endsequence
property abc;
seq1 ##[1:$] seq2;
endproperty
some_signal1,some_signal2, other_signal1, other_signal2 are all RTL signals.
My question:
When the property abc is passed or covered, I want to know the corresponding other_signal1 and other_signal2 sampled in the sequences. How can I access those signals values?
I am thinking to hierarchically access the sequence local variables, such as seq1.a or seq2.b. But these are not allowed, as they are locals.
Declaring those variables as formal argument also does not help, because formal argument can not be assigned in the LHS.
My intention is that I am going to use those signals for other purpose, i.e.:
cover property (abc) begin
// grab seq1.a and seq2.b, and do some other tasks
end
You can do one of 3 things on the right of the comma inside the ( , ):
an assignment (to a local variable)
increment or decrement
call a tasks, task method, void function, void function method or system task.
(see IEEE1800-2012, 16.11 Calling subroutines on match of a sequence)
A task call is the key to this. You can assign to variables outside the task scope within a task called from within the sequence:
sequence seq1;
#(posedge clk) (some_signal1 == 1, assign_a(other_signal1));
endsequence
sequence seq2;
#(posedge clk) (some_signal2 == 1, assign_b(other_signal2));
endsequence
property abc;
seq1 ##[1:$] seq2;
endproperty
cover property (abc)
$display("a= %d, b= %d", a , b);
task assign_a(input int i);
a = i;
endtask
task assign_b(input int i);
b = i;
endtask
http://www.edaplayground.com/x/akw

SystemVerilog Clocking Blocks in Bi-Directional Interface

Let's say I have a bi-directional interface. I want the TB to be able to receive data from the DUT and I want the TB to be able to drive data to the DUT. I need to put in a clocking block in my code because I have race-condition issues. I can fix these issues by putting in a little #1 in the right spot, but I know that a clocking block is the correct solution. The part I'm having difficulty with is the bi-directional part. If it was one direction I might be OK, but the syntax for bi-directional interface is tripping me up. Is the right solution to make 2 clocking blocks, or 2 modports, or something else entirely?
interface MyInterface
(input bit i_Clk);
logic [15:0] r_Data;
logic r_DV = 1'b0;
clocking CB #(posedge i_Clk);
default input #1step output #1step;
endclocking : CB
task t_Clock_Cycles(int N);
repeat (N) #(posedge i_Clk);
endtask : t_Clock_Cycles
modport Driver (clocking CB, output r_Data, r_DV);
modport Receiver (clocking CB, input r_Data, r_DV);
endinterface : MyInterface
package MyPackage;
class MyDriver;
virtual MyInterface.Driver hook;
function new(virtual MyInterface.Driver hook);
this.hook = hook;
endfunction : new
task t_Drive(input [15:0] i_Data);
forever
begin
hook.CB.r_Data = i_Data;
hook.CB.r_DV = 1'b1;
hook.CB.t_Clock_Cycles(1);
end
endtask : t_Drive
endclass : MyDriver
endpackage : MyPackage
module MyModule;
import MyPackage::*;
logic r_Clk = 1'b0;
MyInterface hook(.i_Clk(r_Clk));
always #5 r_Clk = ~r_Clk;
MyDriver d1 = new(hook.Driver);
initial
begin
d1.t_Drive(16'hABCD);
end
endmodule // MyModule
The whole point of using a clocking block is to declare which signals you want to access synchronously. You should add your signals to the clocking block:
clocking CB #(posedge i_Clk);
default input #1step output #1step;
inout r_Data;
inout r_DV;
endclocking : CB
Since you also want to have different access permissions for your driver and receiver, this means you'll need two different clocking blocks:
clocking CB_driver #(posedge i_Clk);
default input #1step output #1step;
output r_Data;
output_DV;
endclocking : CB_driver
// ... direction reversed for CB_receiver
Unfortunately, it's not possible to say that you have a reference to a certain clocking block inside your driver/receiver classes:
class Driver
virtual MyInterface.CB_driver hook; // !!! Not allowed
endclass
If you want to restrict your driver to only be able to drive through CB_driver, you can use a modport:
interface MyInterface;
modport Driver(CB_driver);
endinterface
class Driver;
virtual MyInterface.Driver hook;
endclass
This way you can reference hook.CB_driver when driving your signals. The same goes for your receiver.

assign statement for RTL readability in an interface causes assignments or a buffer in synthesis

We have an interface with modports connectin gmodules that looks something like this:
interface test_interface (clk, in1, out1);
input logic in1;
output logic out1;
input logic clk;
logic mid1;
logic aliased_signal;
modport a_to_b (
input in1,
input clk,
output mid1,
input aliased_signal
);
modport b_to_a (
input clk,
input mid1,
output out1,
output aliased_signal
);
endinterface : test_interface
module top(clock, inpad, outpad);
input logic clock;
input logic inpad;
output logic outpad;
test_interface test_if(.clk(clock), .in1(inpad), .out1(outpad));
a A0(.a2b(test_if));
b B0(.b2a(test_if));
endmodule
module a ( test_interface.a_to_b a2b);
always_ff #(posedge a2b.clk) begin
a2b.mid1 <= a2b.in1 & a2b.aliased_signal;
end
endmodule
module b (test_interface.b_to_a b2a);
assign b2a.aliased_signal = b2a.out1;
always_ff #(posedge b2a.clk) begin
b2a.out1 <= ~b2a.mid1;
end
endmodule
This is a trivial example, but demonstrates the problem. In the real design, we have, for example 32-bit outputs and 8 bits of that may go to one place and 8 bits to another, etc. In the destination, they would like to use names that are more meaningful in the destination module, so are using assignments to create those names in the interface so that the destination code isn't using just part of the bits of an obfuscated bus name in the interface.
While the above simulates fine, the result is assignment statements during synthesis (even if using always_comb obviously) and while we can have the synthesis tool insert buffers to make APR happy, this is not desired when these signals are just used as convenient aliases basically.
Is there an RTL coding style with SV interfaces that would allow such "aliasing" without creating complications downstream in synthesis/APR tools?
Below is a closer example to what I'm trying to do. The "obfuscated_name" signal is an output of the a module because some modules use the name directly from the interface (this could be cleaned up to only do what the a->b connection is doing), but I get the same errors from IUS and DC if I connect to mid2 a->c instead of using the obfuscated_name signal directly.
interface test_interface(clk, in1, out1, out2);
input logic [7:0] in1;
output logic [3:0] out1;
output logic [3:0] out2;
input logic clk;
logic [7:0] obfuscated_name;
logic [3:0] mid1;
logic [3:0] mid2;
modport a_mp (
input clk,
input in1,
output obfuscated_name
);
modport a_to_b (
input clk,
input .mid1(obfuscated_name[3:0]),
output out1
);
modport a_to_c (
input clk,
input obfuscated_name,
output out2
);
endinterface : test_interface
module a ( test_interface.a_mp a_intf);
always_ff #(posedge a_intf.clk) begin
a_intf.obfuscated_name <= ~a_intf.in1;
end
endmodule
module b (test_interface.a_to_b b_intf);
always_ff #(posedge b_intf.clk) begin
b_intf.out1 <= b_intf.mid1;
end
endmodule
module c (test_interface.a_to_c c_intf);
always_ff #(posedge c_intf.clk) begin
c_intf.out2 <= ~c_intf.obfuscated_name[7:4];
end
endmodule
module top( input logic clock,
input logic [7:0] inpad,
output logic [3:0] outpad1,
output logic [3:0] outpad2 );
test_interface test_if(.clk(clock), .in1(inpad), .out1(outpad1), .out2(outpad2));
a A0(.a_intf(test_if));
b B0(.b_intf(test_if));
c C0(.c_intf(test_if));
endmodule
The error I get from IUS is:
ncvlog: *E,MODPXE (test_interface.sv,18|18): Unsupported modport
expression for port identifier 'mid1'.
and DC gives this error:
Error: ./b.sv:1: The construct 'b_intf.mid1 (modport expression without a modport from parent)' is not supported in synthesis. (VER-700)
I'm hoping I'm doing something ignorant here and that this is possible what I'm trying to do.
There is a feature in Verilog called a port expression .name_of_port(expression_to_be_connected_to_port) that most people recognize in a module instance port list that they don't realize can also be used in the module deceleration port list header. This declares the name of the port, and the expression that gets connected to the port. This way a smaller part select of a larger internal bus can be made into a port.
module DUT(input clk, inout .data(bus[7:0]) );
wire [31:0] bus;
endmodule
module top;
reg clock;
wire [7:0] mydata;
DUT d1(.data(mydata), .clk(clock) );
endmodule
In SystemVerilog you can do the same thing with a modport expression.
interface test_interface (clk, in1, out1);
input logic in1;
output logic out1;
input logic clk;
logic mid1;
logic aliased_signal;
modport a_to_b (
input in1,
input clk,
output mid1,
input .aliased_signal(out1) // modport expression
);
modport b_to_a (
input clk,
input mid1,
output out1
);
endinterface : test_interface
module top( input logic clock,
input logic inpad,
output logic outpad );
test_interface test_if(.clk(clock), .in1(inpad), .out1(outpad));
a A0(.a2b(test_if));
b B0(.b2a(test_if));
endmodule
module a ( test_interface.a_to_b a2b);
always_ff #(posedge a2b.clk) begin
a2b.mid1 <= a2b.in1 & a2b.aliased_signal; // port reference to alias
end
endmodule
module b (test_interface.b_to_a b2a);
always_ff #(posedge b2a.clk) begin
b2a.out1 <= ~b2a.mid1;
end
endmodule

Why two exactly "wire" statement in systemverilog, one can be compiled and the other on can not?

Here is the first HDL code for my program counter.
timeunit 1ns; timeprecision 10ps;
module PC(
output logic [31:0] pc_addr,
output logic [31:0] Next_addr,
input logic [31:0] Branch_addr,
input logic PCSrc,
input logic clock, reset
);
wire [31:0] y;
MUX MUX (
.y (y),
.a (Branch_addr),
.b (Next_addr),
.sel (PCSrc)
);
adder_1 adder_1(
.y(Next_addr),
.a(pc_addr),
.b(3'b100)
);
always_ff # (posedge clock)
begin
if(reset)
pc_addr <= 0; //PC address start at 0x0000_0000
else
pc_addr <= y;
end
endmodule
The signal y is come from the output of the instance MUX, and it is not generated by the PC module itself. So I used wire to connect this signal to the top module PC, and D-type flipflop pc_addr will adopt that signal as the output signal of the module.
In this case, this code can be compiled perfectly fine, and I found the wire statement in this code is essential. I have tried to remove that statement, then the simulation result went wrong because the signal y from MUX did not connect to the PC module.
Here come the second code for my MIPS pipeline:
module IDEX(
output logic IDEX_RegDst, IDEX_ALUSrc, IDEX_MemtoReg, IDEX_RegWrite, IDEX_MemRead,
IDEX_MemWrite, IDEX_Branch, IDEX_ALUOp1, IDEX_ALUOp0,
output logic [31:0]IDEX_Next_addr,
output logic [31:0] IDEX_Read_data_1, IDEX_Read_data_2,
output logic [4:0] IDEX_Write_register,
output logic [31:0] IDEX_Extended,
input logic [31:0] IFID_Next_addr,
input logic [31:0] IFID_Instruction,
input logic [31:0] Write_data,
input logic [4:0] Write_register,
input logic RegWrite,
input logic clock, reset
);
register register (
.Read_data_1(Read_data_1),
.Read_data_2(Read_data_2),
.Read_register_1(IFID_Instruction[25:21]),
.Read_register_2(IFID_Instruction[20:16]),
.Write_register(Write_register),
.Write_data(Write_data),
.RegWrite(RegWrite),
.clock(clock),
.reset(reset)
);
sign_extender sign_extender (
.extended(extended),
.unextended(IFID_Instruction[15:0])
);
control control (
.RegDst(RegDst),
.ALUSrc(ALUSrc),
.MemtoReg(MemtoReg),
.RegWrite(RegWrite_0), //this RegWrite signal does not connect with the Register.sv
.MemRead(MemRead),
.MemWrite(MemWrite),
.Branch(Branch),
.ALUOp1(ALUOp1),
.ALUOp0(ALUOp0),
.Operand(IFID_Instruction[31:26])
);
MUX_5 MUX_5 (
.y (y),
.a (IFID_Instruction[15:11]),
.b (IFID_Instruction[20:16]),
.sel (RegDst)
);
wire RegDst;
wire [4:0]y;
wire [31:0]Read_data_1, Read_data_2, extended;
always_ff # (posedge clock)
begin
if(reset)
{IDEX_RegDst, IDEX_ALUSrc, IDEX_MemtoReg, IDEX_RegWrite, IDEX_MemRead,
IDEX_MemWrite, IDEX_Branch, IDEX_ALUOp1, IDEX_ALUOp0, IDEX_Next_addr, IDEX_Read_data_1,
IDEX_Read_data_2, IDEX_Write_register, IDEX_Extended} <= 0;
else
begin
{IDEX_RegDst, IDEX_ALUSrc, IDEX_MemtoReg, IDEX_RegWrite, IDEX_MemRead,
IDEX_MemWrite, IDEX_Branch, IDEX_ALUOp1, IDEX_ALUOp0} <= {RegDst,
ALUSrc, MemtoReg, RegWrite_0, MemRead, MemWrite, Branch, ALUOp1, ALUOp0};
IDEX_Next_addr <= IFID_Next_addr;
IDEX_Read_data_1 <= Read_data_1;
IDEX_Read_data_2 <= Read_data_2;
IDEX_Write_register <= y;
IDEX_Extended <= extended;
end
end
endmodule
This code's situation is exactly the same as the first one. I want to connect the signal y from the instance MUX_5 (and all other instance output signal) to the top module by using the wire statement. However the simulator can not compile this time, the error is :
*ncvlog: *E,DUPIMP (IDEX.sv,65|13): Identifier 'y', implicitly declared here, first as a wire, is subsequently redeclared.
wire [4:0]y;
|
ncvlog: *E,DUPIDN (IDEX.sv,72|10): identifier 'y' previously declared [12.5(IEEE)].
I am not very sure my use of wire statement is correct or not, but if I delete this wire statement, then the simulation result is wrong.
Guys pls help me and tell me what is going on? That is so confused! Why two wire statement have different compile result???
Thank you very much!
In module IDEX you have:
MUX_5 MUX_5 (
.y (y), //<-- y used
.a (IFID_Instruction[15:11]),
.b (IFID_Instruction[20:16]),
.sel (RegDst)
);
wire RegDst;
wire [4:0]y; //<-- y declared
In Verilog and SystemVerilog variables, wire and regs should be declared before they are used.
If an unknown name is used it is often created as an implict 1 bit wire, leading to y effectively being declared twice. Once as an implicit 1 bit wire then explicitly as a 5 bit wire.
You are actually trying to do this:
wire y; //<-- y declared
MUX_5 MUX_5 (
.y (y), //<-- y used
);
wire [4:0]y; //<-- y re-declared
Removing the explicit declaration (wire [4:0]y;) will remove the error but leave you with only 1 bit connected.
Solution
Declare the wire before you is use it.
wire RegDst;
wire [4:0]y; //<-- y declared
MUX_5 MUX_5 (
.y (y), //<-- y used
.a (IFID_Instruction[15:11]),
.b (IFID_Instruction[20:16]),
.sel (RegDst)
);