网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
- reg这个模块用来发送寄存器的配置。
- 包括trans激励的类型、driver发送激励、generator产生激励、monitor检测激励、agent将mon和driver包在一起。激励也就是发送的数据。
`include "param\_def.v"//定义宏,比如定义SLV0\_RW\_ADDR的具体值,修改值只需要修改宏。
package reg\_pkg;
1.1 reg_trans
class reg_trans;
rand bit[7:0] addr;//哪个寄存器
rand bit[1:0] cmd;//读写
rand bit[31:0] data;//数据内容
bit rsp;//判断读写是否正常
1.11 寄存器约束
constraint cstr {
soft cmd inside {`WRITE, `READ, `IDLE};//读还是写引用定义
soft addr inside {`SLV0_RW_ADDR, `SLV1_RW_ADDR, `SLV2_R_ADDR, `SLV0_R_ADDR, `SLV1_R_ADDR, `SLV2_R_ADDR};//3个读地址,3个写地址
addr[7:4]==0 && cmd==`WRITE -> soft data[31:6]==0;
soft addr[7:5]==0;
addr[4]==1 -> soft cmd == `READ;//表示只读寄存器
};
在 mcdf说明文档中寄存器的地址有00、04、08、10、14和18。这6中,用二进制表示为0000_0000、0000_0100、0000_1000、0001_0000、0001_0100、0001_1000。
- 当高4位为0,即addr[7:4]为0,为读写寄存器,如果是写操作,让保留位即不能更改的位置,置0。
- 第4位为1表示只读寄存器,此时的cmd为read。
- 数据地址的高三位为0。
读写寄存器
- bit(0):通道使能信号。1为打开,0位关闭。复位值为1。 bit(2:1):优先级。0为最高,3为最低。复位值为3。
- bit(5:3):数据包长度,解码对应表为,
0对应长度4,1对应长度8,2对应长度16,3对应长度32,其它数值(4-7)均暂时对应长度32。复位值为0。 - bit(31:6):保留位,无法写入。复位值为0。
只读寄存器
bit(7:0):上行数据从端FIFO的可写余量,同FIFO的数据余量保持同步变化。复位值为FIFO的深度数。
bit(31:8):保留位,复位值为0。
1.12 clone、sprint、
function reg_trans clone();
reg_trans c = new();
c.addr = this.addr;
c.cmd = this.cmd;
c.data = this.data;
c.rsp = this.rsp;
return c;
endfunction
function string sprint();
string s;
s = {s, $sformatf("=======================================\n")};
s = {s, $sformatf("reg\_trans object content is as below: \n")};
s = {s, $sformatf("addr = %2x: \n", this.addr)};
s = {s, $sformatf("cmd = %2b: \n", this.cmd)};
s = {s, $sformatf("data = %8x: \n", this.data)};
s = {s, $sformatf("rsp = %0d: \n", this.rsp)};
s = {s, $sformatf("=======================================\n")};
return s;
endfunction
endclass
1.2 reg_driver
class reg_driver;
local string name;
local virtual reg_intf intf;
mailbox #(reg_trans) req_mb;
mailbox #(reg_trans) rsp_mb;
function new(string name = "reg\_driver");
this.name = name;
endfunction
function void set\_interface(virtual reg_intf intf);
if(intf == null)
$error("interface handle is NULL, please check if target interface has been intantiated");
else
this.intf = intf;
endfunction
task run();
fork
this.do\_drive();
this.do\_reset();
join
endtask
task do\_reset(); //复位
forever begin
@(negedge intf.rstn);
intf.cmd_addr <= 0;
intf.cmd <= `IDLE;
intf.cmd_data_m2s <= 0;
end
endtask
task do\_drive();
reg_trans req, rsp;
@(posedge intf.rstn);
forever begin
this.req_mb.get(req);
this.reg\_write(req);
rsp = req.clone();
rsp.rsp = 1;
this.rsp_mb.put(rsp);
end
endtask
task reg\_write(reg_trans t);//给寄存器
@(posedge intf.clk iff intf.rstn);
case(t.cmd)
`WRITE: begin //如果是写操作
intf.drv_ck.cmd_addr <= t.addr; //地址
intf.drv_ck.cmd <= t.cmd;
intf.drv_ck.cmd_data_m2s <= t.data; //数据
end
`READ: begin
intf.drv_ck.cmd_addr <= t.addr; //总线
intf.drv_ck.cmd <= t.cmd;
repeat(2) @(negedge intf.clk);//等两个时钟的下降沿,第一个下降沿还在
t.data = intf.cmd_data_s2m; //采样了intf的数据
end
`IDLE: begin
this.reg\_idle(); //等一拍
end
default: $error("command %b is illegal", t.cmd);
endcase
$display("%0t reg driver [%s] sent addr %2x, cmd %2b, data %8x", $time, name, t.addr, t.cmd, t.data);
endtask
当case为 READ时,等待了两个时钟的下降沿。
第一个下降沿还在当前周期,第二个下降沿就在下一个周期了。此时数据已经驱动到接口cmd_data_s2m处了,这时去采样接口处的数据就一定是要读的数据。
- 在控制寄存器接口上,需要在每一个时钟解析cmd。当cmd为写(WR)指令时,需要把数据cmd_data_in写入到cmd_addr对应的寄存器中;当cmd为读指令(RD)时,即需要从cmd_addr对应的寄存器中读取数据,并在下一个周期,将数据驱动至cmd_data_out接口。
task reg\_idle();
@(posedge intf.clk);
intf.drv_ck.cmd_addr <= 0;
intf.drv_ck.cmd <= `IDLE;
intf.drv_ck.cmd_data_m2s <= 0;
endtask
endclass
1.3 reg_generator
class reg_generator;
rand bit[7:0] addr = -1;
rand bit[1:0] cmd = -1;
rand bit[31:0] data = -1;
mailbox #(reg_trans) req_mb;
mailbox #(reg_trans) rsp_mb;
reg_trans reg_req[$];
constraint cstr{
soft addr == -1;
soft cmd == -1;
soft data == -1;
}
function new();
this.req_mb = new();
this.rsp_mb = new();
endfunction
task start();
send\_trans();
endtask
// generate transaction and put into local mailbox
task send\_trans();
reg_trans req, rsp;//做例化
req = new();
assert(req.randomize with {local::addr >= 0 -> addr == local::addr;
local::cmd >= 0 -> cmd == local::cmd;
local::data >= 0 -> data == local::data;
})
else $fatal("[RNDFAIL] register packet randomization failure!");
$display(req.sprint());
this.req_mb.put(req);//放入数据
this.rsp_mb.get(rsp);
$display(rsp.sprint());
if(req.cmd == `READ)
this.data = rsp.data;
assert(rsp.rsp)
else $error("[RSPERR] %0t error response received!", $time);
endtask
function string sprint();
string s;
s = {s, $sformatf("=======================================\n")};
s = {s, $sformatf("reg\_generator object content is as below: \n")};
s = {s, $sformatf("addr = %2x: \n", this.addr)};
s = {s, $sformatf("cmd = %2b: \n", this.cmd)};
s = {s, $sformatf("data = %8x: \n", this.data)};
s = {s, $sformatf("=======================================\n")};
return s;
endfunction
function void post\_randomize();
string s;
s = {"AFTER RANDOMIZATION \n", this.sprint()};
$display(s);
endfunction
endclass
class reg_monitor;
local string name;
local virtual reg_intf intf;
mailbox #(reg_trans) mon_mb;
function new(string name="reg\_monitor");
this.name = name;
endfunction
function void set\_interface(virtual reg_intf intf);
if(intf == null)
$error("interface handle is NULL, please check if target interface has been intantiated");
else
this.intf = intf;
endfunction
task run();
this.mon\_trans();
endtask
task mon\_trans();
reg_trans m;
forever begin
@(posedge intf.clk iff (intf.rstn && intf.mon_ck.cmd != `IDLE));//时钟的上升沿,并且数据合理
m = new();//生成一个对象,把数据地址放入
m.addr = intf.mon_ck.cmd_addr;
m.cmd = intf.mon_ck.cmd;
if(intf.mon_ck.cmd == `WRITE) begin//写指令,把总线上的数据放入
m.data = intf.mon_ck.cmd_data_m2s;
end
else if(intf.mon_ck.cmd == `READ) begin//读指令,等下一个周期,再放入数据
@(posedge intf.clk);
m.data = intf.mon_ck.cmd_data_s2m;
end
mon_mb.put(m);//把数据交给checker
$display("%0t %s monitored addr %2x, cmd %2b, data %8x", $time, this.name, m.addr, m.cmd, m.data);
end
endtask
endclass
1.4 reg_agent
class reg_agent;//包含了monitor,driver,让driver,monitor工作起来
local string name;
reg_driver driver;
reg_monitor monitor;
local virtual reg_intf vif;
function new(string name = "reg\_agent");
this.name = name;
this.driver = new({name, ".driver"});
this.monitor = new({name, ".monitor"});
endfunction
function void set\_interface(virtual reg_intf vif);
this.vif = vif;
driver.set\_interface(vif);
monitor.set\_interface(vif);
endfunction
task run();
fork
driver.run();
monitor.run();
join
endtask
endclass
激励的类型、发送激励、产生激励、检测激励、agent
2. fmt_apk
chnl和reg的driver都是主动的发送数据给dut,而fmt的driver是被动的接受发送出来的数据。所以fmt像一个fifo,储存发送出来的数据。
需要设计fifo的大小宽度,来储存发送出来的数据。
package fmt\_pkg;
import rpt\_pkg::\*;
//传出来的数据可能有不同的带宽、需要的fifo大小也不同,把可能需要的都枚举出来。
typedef enum {SHORT_FIFO, MED_FIFO, LONG_FIFO, ULTRA_FIFO} fmt_fifo_t;
typedef enum {LOW_WIDTH, MED_WIDTH, HIGH_WIDTH, ULTRA_WIDTH} fmt_bandwidth_t;
2.1 fmt_trans
class fmt_trans;
rand fmt_fifo_t fifo;
rand fmt_bandwidth_t bandwidth;
bit [9:0] length;
bit [31:0] data[];
bit [1:0] ch_id;
bit rsp;
constraint cstr{
soft fifo == MED_FIFO;//fifo
soft bandwidth == MED_WIDTH;//带宽
};
function fmt_trans clone();
fmt_trans c = new();
c.fifo = this.fifo;
c.bandwidth = this.bandwidth;
c.length = this.length;
c.data = this.data;
c.ch_id = this.ch_id;
c.rsp = this.rsp;
return c;
endfunction
function string sprint();
string s;
s = {s, $sformatf("=======================================\n")};
s = {s, $sformatf("fmt\_trans object content is as below: \n")};
s = {s, $sformatf("fifo = %s: \n", this.fifo)};
s = {s, $sformatf("bandwidth = %s: \n", this.bandwidth)};
s = {s, $sformatf("length = %s: \n", this.length)};
foreach(data[i]) s = {s, $sformatf("data[%0d] = %8x \n", i, this.data[i])};
s = {s, $sformatf("ch\_id = %0d: \n", this.ch_id)};
s = {s, $sformatf("rsp = %0d: \n", this.rsp)};
s = {s, $sformatf("=======================================\n")};
return s;
endfunction
function bit compare(fmt_trans t);//当前的与传进来的数作比较
string s;
compare = 1;
s = "\n=======================================\n";
s = {s, $sformatf("COMPARING fmt\_trans object at time %0d \n", $time)};
if(this.length != t.length) begin
compare = 0;
s = {s, $sformatf("sobj length %0d != tobj length %0d \n", this.length, t.length)};
end
if(this.ch_id != t.ch_id) begin
compare = 0;
s = {s, $sformatf("sobj ch\_id %0d != tobj ch\_id %0d\n", this.ch_id, t.ch_id)};
end
foreach(this.data[i]) begin
if(this.data[i] != t.data[i]) begin
compare = 0;
s = {s, $sformatf("sobj data[%0d] %8x != tobj data[%0d] %8x\n", i, this.data[i], i, t.data[i])};
end
end
if(compare == 1) s = {s, "COMPARED SUCCESS!\n"};
else s = {s, "COMPARED FAILURE!\n"};
s = {s, "=======================================\n"};
rpt_pkg::rpt\_msg("[CMPOBJ]", s, rpt_pkg::INFO, rpt_pkg::MEDIUM);
endfunction
endclass
2.2 fmt_driver
class fmt_driver;
local string name;
local virtual fmt_intf intf;
mailbox #(fmt_trans) req_mb;
mailbox #(fmt_trans) rsp_mb;
local mailbox #(bit[31:0]) fifo;
local int fifo_bound;//长度
local int data_consum_peroid;//消耗周期,侧面反应带宽,带宽越大,消耗周期越短
function new(string name = "fmt\_driver");
this.name = name;
this.fifo = new();//fifo初始化
this.fifo_bound = 4096;
this.data_consum_peroid = 1;
endfunction
function void set\_interface(virtual fmt_intf intf);
if(intf == null)
$error("interface handle is NULL, please check if target interface has been intantiated");
else
this.intf = intf;
endfunction
task run();
fork
this.do\_receive();
this.do\_consume();
this.do\_config();
this.do\_reset();
join
endtask
//配置fifo长或者短,消耗数据快还是慢
task do\_config();//配置driver,配置完成后重新例化fifo
fmt_trans req, rsp;
forever begin
this.req_mb.get(req);
case(req.fifo)//配置为不同长度
SHORT_FIFO: this.fifo_bound = 64;
MED_FIFO: this.fifo_bound = 256;
LONG_FIFO: this.fifo_bound = 512;
ULTRA_FIFO: this.fifo_bound = 2048;
endcase
this.fifo = new(this.fifo_bound);//重新例化,并且规定长度
case(req.bandwidth)
LOW_WIDTH: this.data_consum_peroid = 8;
MED_WIDTH: this.data_consum_peroid = 4;
HIGH_WIDTH: this.data_consum_peroid = 2;
ULTRA_WIDTH: this.data_consum_peroid = 1;
endcase
rsp = req.clone();
rsp.rsp = 1;
this.rsp_mb.put(rsp);
end
endtask
task do\_reset();
forever begin
@(negedge intf.rstn)
intf.fmt_grant <= 0;
end
endtask
task do\_receive();//模拟从formater接受数据
forever begin
@(posedge intf.fmt_req);//fmt的req拉高
forever begin
@(posedge intf.clk);//等待一拍
if((this.fifo_bound-this.fifo.num()) >= intf.fmt_length)//fifo最大容量-已经存放了多少>=即将存放的数据长度
break;
end
intf.drv_ck.fmt_grant <= 1;//可以存放数据,让grant为1
@(posedge intf.fmt_start);
fork
begin
@(posedge intf.clk);
intf.drv_ck.fmt_grant <= 0;//过一拍置0
end
join_none
repeat(intf.fmt_length) begin//把传进来的数据在每一拍做采样,重复采样length次。
@(negedge intf.clk);
this.fifo.put(intf.fmt_data);
end
end
endtask
task do\_consume();//不断的消耗数据,每过一拍拿一组数据
bit[31:0] data;
forever begin
void'(this.fifo.try\_get(data));
repeat($urandom\_range(1, this.data_consum_peroid)) @(posedge intf.clk);
end
endtask
endclass
2.3 fmt_generator
class fmt_generator;
rand fmt_fifo_t fifo = MED_FIFO;
rand fmt_bandwidth_t bandwidth = MED_WIDTH;
mailbox #(fmt_trans) req_mb;
mailbox #(fmt_trans) rsp_mb;
constraint cstr{
soft fifo == MED_FIFO;
soft bandwidth == MED_WIDTH;
}
function new();
this.req_mb = new();
this.rsp_mb = new();
endfunction
task start();
send\_trans();
endtask
// generate transaction and put into local mailbox
task send\_trans();
fmt_trans req, rsp;
req = new();
assert(req.randomize with {local::fifo != MED_FIFO -> fifo == local::fifo;
local::bandwidth != MED_WIDTH -> bandwidth == local::bandwidth;
})
else $fatal("[RNDFAIL] formatter packet randomization failure!");
$display(req.sprint());
this.req_mb.put(req);
this.rsp_mb.get(rsp);
$display(rsp.sprint());
assert(rsp.rsp)
else $error("[RSPERR] %0t error response received!", $time);
endtask
function string sprint();
string s;
s = {s, $sformatf("=======================================\n")};
s = {s, $sformatf("fmt\_generator object content is as below: \n")};
s = {s, $sformatf("fifo = %s: \n", this.fifo)};
s = {s, $sformatf("bandwidth = %s: \n", this.bandwidth)};
s = {s, $sformatf("=======================================\n")};
return s;
endfunction
function void post\_randomize();
string s;
s = {"AFTER RANDOMIZATION \n", this.sprint()};
$display(s);
endfunction
endclass
2.4 fmt_monitor
class fmt_monitor;
local string name;
local virtual fmt_intf intf;
mailbox #(fmt_trans) mon_mb;
function new(string name="fmt\_monitor");
this.name = name;
endfunction
function void set\_interface(virtual fmt_intf intf);
if(intf == null)
$error("interface handle is NULL, please check if target interface has been intantiated");
else
this.intf = intf;
endfunction
task run();
this.mon\_trans();
endtask
task mon\_trans();
fmt_trans m;
string s;
forever begin
@(posedge intf.mon_ck.fmt_start);//假定协议是正常的
m = new();
m.length = intf.mon_ck.fmt_length;
m.ch_id = intf.mon_ck.fmt_chid;
m.data = new[m.length];
foreach(m.data[i]) begin
@(posedge intf.clk);
m.data[i] = intf.mon_ck.fmt_data;//每一次数据来的时候,都存放在动态数组
end
mon_mb.put(m);//放好数据后,放入信箱
s = $sformatf("=======================================\n");
s = {s, $sformatf("%0t %s monitored a packet: \n", $time, this.name)};
s = {s, $sformatf("length = %0d: \n", m.length)};
s = {s, $sformatf("chid = %0d: \n", m.ch_id)};
foreach(m.data[i]) s = {s, $sformatf("data[%0d] = %8x \n", i, m.data[i])};
s = {s, $sformatf("=======================================\n")};
$display(s);
end
endtask
endclass
2.5 fmt_agent
class fmt_agent;
local string name;
fmt_driver driver;
fmt_monitor monitor;
local virtual fmt_intf vif;
function new(string name = "fmt\_agent");
this.name = name;
this.driver = new({name, ".driver"});
this.monitor = new({name, ".monitor"});
endfunction
function void set\_interface(virtual fmt_intf vif);
this.vif = vif;
driver.set\_interface(vif);
monitor.set\_interface(vif);
endfunction
task run();
fork
driver.run();
monitor.run();
join
endtask
endclass
endpackage
3. checker
- 检测到的数据传到checker,3个chnl、fmt、reg的数据都会存放在checke已经例化好的信箱里。而fmt和3个chnl的数据格式是完全不同的。fmt对数据进行了打包,把数据整合成了新格式。
- 所以比较输入输出先要把chnl、reg的数据需要先整形转化,模拟mcdf的行为。模拟打包后再进行数据的比较。
- refmod利用句柄把数据拿出来,模拟硬件对数据打包。从chnl拿数据,按照长度的不同把数据放入三个输出端。
其中:
- doreset对寄存器复位,清空信箱中的数据。
- doregupdate更新checker中寄存器的配置。
- dopackage对三个chnl做打包。
包含了checker、env和test。
`include "param\_def.v"
package mcdf\_pkg;
import chnl\_pkg::\*;
import reg\_pkg::\*;
import arb\_pkg::\*;
import fmt\_pkg::\*;
import rpt\_pkg::\*;
typedef struct packed {
bit[2:0] len; //长度
bit[1:0] prio;//
bit en;//fifo使能
bit[7:0] avail;//表示fifo可选余量
} mcdf_reg_t;
typedef enum {RW_LEN, RW_PRIO, RW_EN, RD_AVAIL} mcdf_field_t;
3.1 mcdf_refmod
模拟mcdf的功能接受寄存器的读写行为,对检测来的数据进行打包。
class mcdf_refmod;//模拟mcdf的功能接受寄存器的读写行为,对数据进行打包
local virtual mcdf_intf intf;
local string name;
mcdf_reg_t regs[3];
mailbox #(reg_trans) reg_mb;
mailbox #(mon_data_t) in_mbs[3];
mailbox #(fmt_trans) out_mbs[3];
function new(string name="mcdf\_refmod");
this.name = name;
foreach(this.out_mbs[i]) this.out_mbs[i] = new();
endfunction
task run();
fork
do\_reset();
this.do\_reg\_update();//模拟寄存器
do\_packet(0);
do\_packet(1);
do\_packet(2);
join
endtask
task do\_reg\_update();//硬件上对寄存器的读写更新到仿真中
reg_trans t;//引用regtrans里的数据
forever begin
this.reg_mb.get(t);
if(t.addr[7:4] == 0 && t.cmd == `WRITE) begin
this.regs[t.addr[3:2]].en = t.data[0];
this.regs[t.addr[3:2]].prio = t.data[2:1];
this.regs[t.addr[3:2]].len = t.data[5:3];
end
else if(t.addr[7:4] == 1 && t.cmd == `READ) begin
this.regs[t.addr[3:2]].avail = t.data[7:0];
end
end
endtask
task do\_packet(int id);//把chnl的数据打包
fmt_trans ot;
mon_data_t it;
forever begin
this.in_mbs[id].peek(it);
ot = new();
ot.length = 4 << (this.get\_field\_value(id, RW_LEN) & 'b11);//拿到打包的长度
ot.data = new[ot.length];//开辟空间
ot.ch_id = id;
foreach(ot.data[m]) begin//拿完数据,把ot放到对应的信箱。
this.in_mbs[id].get(it);
ot.data[m] = it.data;
end
this.out_mbs[id].put(ot);
end
endtask
function int get\_field\_value(int id, mcdf_field_t f);
case(f)
RW_LEN: return regs[id].len;
RW_PRIO: return regs[id].prio;
RW_EN: return regs[id].en;
RD_AVAIL: return regs[id].avail;
endcase
endfunction
task do\_reset();//寄存器复位
forever begin
@(negedge intf.rstn);
foreach(regs[i]) begin
regs[i].len = 'h0;
regs[i].prio = 'h3;
regs[i].en = 'h1;
regs[i].avail = 'h20;
end
end
endtask
function void set\_interface(virtual mcdf_intf intf);
if(intf == null)
$error("interface handle is NULL, please check if target interface has been intantiated");
else
this.intf = intf;
endfunction
endclass
## 3.2 mcdf_checker
```java
class mcdf_checker;
local string name;
local int err_count;
local int total_count;
local int chnl_count[3];
local virtual mcdf_intf intf;
local mcdf_refmod refmod;
mailbox #(mon_data_t) chnl_mbs[3];
mailbox #(fmt_trans) fmt_mb;
mailbox #(reg_trans) reg_mb;
mailbox #(fmt_trans) exp_mbs[3];
function new(string name="mcdf\_checker");
![img](https://img-blog.csdnimg.cn/img_convert/241d737f187437627bed90bc3d557c4c.png)
![img](https://img-blog.csdnimg.cn/img_convert/3f00864c1d1d1aa6544cf5775bdcba24.png)
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**[需要这份系统化的资料的朋友,可以添加戳这里获取](https://bbs.csdn.net/topics/618658159)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**
ated");
else
this.intf = intf;
endfunction
endclass
## 3.2 mcdf_checker
```java
class mcdf_checker;
local string name;
local int err_count;
local int total_count;
local int chnl_count[3];
local virtual mcdf_intf intf;
local mcdf_refmod refmod;
mailbox #(mon_data_t) chnl_mbs[3];
mailbox #(fmt_trans) fmt_mb;
mailbox #(reg_trans) reg_mb;
mailbox #(fmt_trans) exp_mbs[3];
function new(string name="mcdf\_checker");
[外链图片转存中...(img-o49jujBa-1715906366047)]
[外链图片转存中...(img-aDUJI9rW-1715906366048)]
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**[需要这份系统化的资料的朋友,可以添加戳这里获取](https://bbs.csdn.net/topics/618658159)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**