手写一个简易的多周期 MIPS CPU

一点前言#

多周期 CPU 相比单周期 CPU 以及流水线的实现来说其实写起来要麻烦那么一些,但是相对于流水线以及单周期 CPU 而言,多周期 CPU 除了能提升主频之外似乎并没有什么卵用。不过我的课题是多周期 CPU 那么就开始吧。

多周期 CPU#

不同于单周期 CPU,多周期 CPU 指的是将整个 CPU 的执行过程分成几个阶段,每个阶段用一个时钟去完 成,然后开始下一条指令的执行,而每种指令执行时所用的时钟数不尽相同,这就是所谓的多周期CPU。

CPU在处理指令时,一般需要经过以下几个阶段:

(1) 取指令(IF):根据程序计数器 PC 中的指令地址,从存储器中取出一条指令,同时,PC 根据指令字长度自动递增产生下一条指令所需要的指令地址,但遇到“地址转移”指令 时,则控制器把“转移地址”送入 PC,当然得到的“地址”需要做些变换才送入 PC。

(2) 指令译码(ID):对取指令操作中得到的指令进行分析并译码,确定这条指令需要完成的操作,从而产生相应的操作控制信号,用于驱动执行状态中的各种操作。

(3) 指令执行(EXE):根据指令译码得到的操作控制信号,具体地执行指令动作,然后转移到结果写回状态。

(4) 存储器访问(MEM):所有需要访问存储器的操作都将在这个步骤中执行,该步骤给出存储器的数据地址,把数据写入到存储器中数据地址所指定的存储单元或者从存储器中得 到数据地址单元中的数据。

(5) 结果写回(WB):指令执行的结果或者访问存储器中得到的数据写回相应的目的寄存器中。

这也就意味着一条 CPU 指令最长需要 5 个时钟周期才能执行完毕,至于具体需要多少周期则根据指令的不同而不同。

MIPS 指令集的设计为定长简单指令集,这为 CPU 的实现带来了极大的方便。

指令集#

MIPS 指令分为三种:R、I 和 J,三种指令有不同的存储方式:

其中,

  • op:操作码;
  • rs:第1个源操作数寄存器,寄存器地址(编号)是00000~11111,00~1F;
  • rt:第2个源操作数寄存器,或目的操作数寄存器,寄存器地址(同上);
  • rd:目的操作数寄存器,寄存器地址(同上);
  • sa:位移量(shift amt),移位指令用于指定移多少位;
  • funct:功能码,在寄存器类型指令中(R类型)用来指定指令的功能;
  • immediate:16位立即数,用作无符号的逻辑操作数、有符号的算术操作数、数据加载(Load)/数据保存(Store)指令的数据地址字节偏移量和分支指令中相对程序计数器(PC)的有符号偏移量;
  • address:地址。

在执行指令的过程中,需要在不同的时钟周期之间进行状态转移:

本简易 CPU 姑且只实现以下指令:

OpCode指令功能
000000add rd, rs, rt带符号加法运算
000001sub rd, rs, rt带符号减法运算
000010addiu rt, rs, immediate无符号加法运算
010000and rd, rs, rt与运算
010001andi rt, rs, immediate对立即数做 0 扩展后进行与运算
010010ori rt, rs, immediate对立即数做 0 扩展后做或运算
010011xori rt, rs, immediate对立即数做 0 扩展后做异或运算
011000sll rd, rt, sa左移指令
100110slti rt, rs, immediate比较指令
100111slt rd, rs, rt比较指令
110000sw rt, immediate(rs)存数指令
110001lw rt, immediate(rs)读数指令
110100beq rs, rt, immediate分支指令,相等时跳转
110101bne rs, rt, immediate分支指令,不等时跳转
110110bltz rs, immediate分支指令,小于 0 时跳转
111000j addr跳转指令
111001jr rs跳转指令
111010jal addr调用子程序指令
111111halt停机指令

控制单元#

一个简易的多周期 CPU 的数据通路图如下:

三个 D 触发器用于保存当前状态,是时序逻辑电路,RST用于初始化状态“000“,另外两个部分都是组合逻辑电路,一个用于产生 下一个阶段的状态,另一个用于产生每个阶段的控制信号。从图上可看出,下个状态取决于 指令操作码和当前状态;而每个阶段的控制信号取决于指令操作码、当前状态和反映运算结果的状态 zero 标志和符号 sign标志。

其中指令和数据各存储在不同存储器中,即有指令存储器和数据存储器。访问存储器时,先给出内存地址,然后由读或写信号控制操作。对于寄存器组, 给出寄存器地址(编号),读操作时不需要时钟信号,输出端就直接输出相应数据;而在写操作时,在 WE使能信号为 1时,在时钟边沿触发将数据写入寄存器。

IR 指令寄存器目的是使指令代码保持稳定,PC 写使能控制信号PCWre,是确保PC 适时修改,原因都是和多周期工作的CPU有关。ADR、BDR、 ALUoutDR、DBDR四个寄存器不需要写使能信号,其作用是切分数据通路,将大组合逻辑切分为若干个小组合逻辑,大延迟变为多个分段小延迟。

各控制信号功能如下:

控制信号名状态 0状态 1
RST对于PC,初始化PC为程序首地址对于PC,PC接收下一条指令地址
PCWrePC不更改,另 外,除‘000’状态之外,其余状态慎改PC的值。PC更改,另外,在‘000’状态时,修改PC的值合适。
ALUSrcA来自寄存器堆 data1 输出来自移位数sa,同时,进行(zeroextend)sa,即 {{27{1'b0},sa}
ALUSrcB来自寄存器堆 data2 输出来自 sign或 zero 扩展的立即数
DBDataSrc来自ALU运算结果的输出来自数据存储器(Data MEM)的输出
RegWre无写寄存器组寄存器寄存器组寄存器写使能
WrRegDSrc写入寄存器组寄存器的数据来自 PC+4(PC4)写入寄存器组寄存器的数据来自ALU 运算结果或存储器读出的数据
InsMemRW写指令存储器读指令存储器(Ins. Data)
mRD存储器输出高阻态读数据存储器
mWR无操作写数据存储器
IRWreIR(指令寄存器)不更改IR 寄存器写使能。向指令存储器发出读指令代码后,这个信号也接着发出,在时钟上升沿,IR 接收从指令存储器送来的指令代码。
ExtSel零扩展符号扩展
PCSrc[1..0]00:PC<-PC+4
01:PC<-PC+4+((sign-extend)immediate<<2)
10:PC<-rs
11:PC<-{PC[31:28], addr[27:2],2'b00}
RegDst[1..0]写寄存器组寄存器的地址,来自:
00:0x1F($31)
01:rt 字段
10:rd 字段
11:未用
ALUOp[2..0]ALU 8种运算功能选择(000-111)

相关部件及引脚说明#

Instruction Memory:指令存储器

  • Iaddr,指令地址输入端口
  • DataIn,存储器数据输入端口
  • DataOut,存储器数据输出端口
  • RW,指令存储器读写控制信号,为0 写,为 1读

Data Memory:数据存储器

  • Daddr,数据地址输入端口
  • DataIn,存储器数据输入端口
  • DataOut,存储器数据输出端口
  • /RD,数据存储器读控制信号,为 0 读
  • /WR,数据存储器写控制信号,为0 写

Register File:寄存器组

  • Read Reg1,rs 寄存器地址输入端口
  • Read Reg2,rt 寄存器地址输入端口
  • Write Reg,将数据写入的寄存器,其地址输入端口(rt、rd)
  • Write Data,写入寄存器的数据输入端口
  • Read Data1,rs 寄存器数据输出端口
  • Read Data2,rt 寄存器数据输出端口
  • WE,写使能信号,为1 时,在时钟边沿触发写入

IR: 指令寄存器,用于存放正在执行的指令代码

ALU: 算术逻辑单元

  • result,ALU运算结果
  • zero,运算结果标志,结果为 0,则 zero=1;否则 zero=0
  • sign,运算结果标志,结果最高位为0,则 sign=0,正数;否则,sign=1,负数

ALU#

ALU 为算术逻辑运算单元,功能如下:

ALUOp[2..0]功能功能
000Y=A+B加法运算
001Y=A-B减法运算
010Y=B<<A左移运算
011Y=A∨B或运算
100Y=A∧B与运算
101Y=(A<B) ? 1 : 0无符号比较
110Y=(((A<B)&&(A[31] == B[31])) ||
((A[31]==1&& B[31] == 0))) ? 1 : 0
带符号比较
111Y=A⊕B异或

模块设计#

符号定义#

为了更加明晰程序代码,并避免因二进制代码书写错误导致的问题,对状态码、操 作码等做出如下定义:

Copy
`define ALU_OP_ADD 3'b000 `define ALU_OP_SUB 3'b001 `define ALU_OP_SLL 3'b010 `define ALU_OP_OR 3'b011 `define ALU_OP_AND 3'b100 `define ALU_OP_LT 3'b101 `define ALU_OP_SLT 3'b110 `define ALU_OP_XOR 3'b111 `define OP_ADD 6'b000000 `define OP_SUB 6'b000001 `define OP_ADDIU 6'b000010 `define OP_AND 6'b010000 `define OP_ANDI 6'b010001 `define OP_ORI 6'b010010 `define OP_XORI 6'b010011 `define OP_SLL 6'b011000 `define OP_SLTI 6'b100110 `define OP_SLT 6'b100111 `define OP_SW 6'b110000 `define OP_LW 6'b110001 `define OP_BEQ 6'b110100 `define OP_BNE 6'b110101 `define OP_BLTZ 6'b110110 `define OP_J 6'b111000 `define OP_JR 6'b111001 `define OP_JAL 6'b111010 `define OP_HALT 6'b111111 `define PC_NEXT 2'b00 `define PC_REL_JUMP 2'b01 `define PC_REG_JUMP 2'b10 `define PC_ABS_JUMP 2'b11 `define STATE_IF 3'b000 `define STATE_ID 3'b001 `define STATE_EXE_AL 3'b110 `define STATE_EXE_BR 3'b101 `define STATE_EXE_LS 3'b010 `define STATE_MEM 3'b011 `define STATE_WB_AL 3'b111 `define STATE_WB_LD 3'b100

控制单元#

状态转移#

Copy
always @(posedge CLK or negedge RST) begin if (!RST) State <= `STATE_IF; else begin case (State) `STATE_IF: State <= `STATE_ID; `STATE_ID: begin case (OpCode) `OP_ADD, `OP_SUB, `OP_ADDIU, `OP_AND, `OP_ANDI, `OP_ORI, `OP_XORI, `OP_SLL, `OP_SLTI, `OP_SLT: State <= `STATE_EXE_AL; `OP_BNE, `OP_BEQ, `OP_BLTZ: State <= `STATE_EXE_BR; `OP_SW, `OP_LW: State <= `STATE_EXE_LS; `OP_J, `OP_JAL, `OP_JR, `OP_HALT: State <= `STATE_IF; default: State <= `STATE_EXE_AL; endcase end `STATE_EXE_AL: State <= `STATE_WB_AL; `STATE_EXE_BR: State <= `STATE_IF; `STATE_EXE_LS: State <= `STATE_MEM; `STATE_WB_AL: State <= `STATE_IF; `STATE_MEM: begin case (OpCode) `OP_SW: State <= `STATE_IF; `OP_LW: State <= `STATE_WB_LD; endcase end `STATE_WB_LD: State <= `STATE_IF; default: State <= `STATE_IF; endcase end end

控制信号#

不同控制信号根据不同的操作码得到,因此可以列出对于不同操作码的各控制信号的真值表:

OpPCWreALUSrcAALUSrcBDBDataSrcRegWreWrRegDSrcInsMemRWmRDmWRIRWreExtSelPCSrcRegDstALUOp
add0000111XX1X0010000
sub0000111XX1X0010001
addiu0010111XX110001000
and0000111XX1X0010100
andi0010111XX100001100
ori0010111XX100001011
xori0010111XX100001111
sll0100111XX1X0010010
slti0010111XX110001110
slt0000111XX1X0010110
sw001X0X1X11100XX000
lw00111111X110001000
beq000X0X1XX1100(Zero=0) 01(Zero=1)XX001
bne000X0X1XX1100(Zero=1) 01(Zero=0)XX001
bltz000X0X1XX1100(Sign=0) 01(Sign=1)XX001
j0XXX0X1XX1X11XXXXX
jr0XXX0X1XX1X10XXXXX
jal0XXX101XX1X1100XXX
halt1XXX0X1XX1XXXXXXXX

控制信号不仅仅取决于操作码,还取决于当前的状态。各控制信号实现如下:

ALUSrcA:EXE 阶段 LS、SLL

Copy
ALUSrcA = ((State == `STATE_EXE_AL || State == `STATE_EXE_BR || State == `STATE_EXE_LS) && OpCode == `OP_SLL) ? 1 : 0;

ALUSrcB:EXE 阶段 ADDIU、ANDI、ORI、XORI、SLTI、LW、SW

Copy
ALUSrcB = ((State == `STATE_EXE_AL || State == `STATE_EXE_BR || State == `STATE_EXE_LS) && (OpCode == `OP_ADDIU || OpCode == `OP_ANDI || OpCode == `OP_ORI || OpCode == `OP_XORI || OpCode == `OP_SLTI || OpCode == `OP_LW || OpCode == `OP_SW)) ? 1 : 0;

RegWre:ID 阶段 JAL,或 WB 阶段 LD

Copy
RegWre = ((State == `STATE_ID && OpCode == `OP_JAL) || (State == `STATE_WB_AL || State == `STATE_WB_LD)) ? 1 : 0;

WrRegDSrc:ID 阶段 JAL

Copy
WrRegDSrc = (State == `STATE_ID && OpCode == `OP_JAL) ? 0 : 1;

mRD:MEM 或 WB 阶段 LW

Copy
mRD = ((State == `STATE_MEM || State == `STATE_WB_LD) && OpCode == `OP_LW) ? 1 : 0;

mWR:MEM 阶段 SW

Copy
mWR = (State == `STATE_MEM && OpCode == `OP_SW) ? 1 : 0;

IRWre:IF 阶段

Copy
IRWre = (State == `STATE_IF) ? 1 : 0;

ExtSel:EXE 阶段 ANDI、ORI、XORI

Copy
ExtSel = ((State == `STATE_EXE_AL || State == `STATE_EXE_BR || State == `STATE_EXE_LS) && (OpCode == `OP_ANDI || OpCode == `OP_ORI || OpCode == `OP_XORI)) ? 0 : 1;

PCSrc:IF 或 ID 阶段 JR 为 PC_REG_JUMP,IF 或 ID 阶段 J、JAL 为 PC_ABS_JUMP,EXE 阶段 BEQ、BNE、BLTZ 为 PC_REL_JUMP,否则均为 PC_NEXT

Copy
if ((State == `STATE_IF || State == `STATE_ID) && OpCode == `OP_JR) PCSrc = `PC_REG_JUMP; else if ((State == `STATE_IF || State == `STATE_ID) && (OpCode == `OP_J || OpCode == `OP_JAL)) PCSrc = `PC_ABS_JUMP; else if ((State == `STATE_EXE_AL || State == `STATE_EXE_BR || State == `STATE_EXE_LS) && (OpCode == `OP_BEQ && Zero) || (OpCode == `OP_BNE && !Zero) || (OpCode == `OP_BLTZ && Sign)) PCSrc = `PC_REL_JUMP; else PCSrc = `PC_NEXT;

RegDst:ID 阶段 JAL 为 b00,WB 阶段 ADDIU、ANDI、ORI、XORI、SLTI、LW 为 b01,否则均为 b10

Copy
if (State == `STATE_ID && OpCode == `OP_JAL) RegDst = 2'b00; else if ((State == `STATE_WB_AL || State == `STATE_WB_LD) && (OpCode == `OP_ADDIU || OpCode == `OP_ANDI || OpCode == `OP_ORI || OpCode == `OP_XORI || OpCode == `OP_SLTI || OpCode == `OP_LW)) RegDst = 2'b01; else RegDst = 2'b10;

ALUOp:根据真值表即可得出

Copy
case (OpCode) `OP_ADD, `OP_ADDIU, `OP_SW, `OP_LW: ALUOp = `ALU_OP_ADD; `OP_SUB, `OP_BEQ, `OP_BNE, `OP_BLTZ: ALUOp = `ALU_OP_SUB; `OP_SLL: ALUOp = `ALU_OP_SLL; `OP_ORI: ALUOp = `ALU_OP_OR; `OP_AND, `OP_ANDI: ALUOp = `ALU_OP_AND; `OP_SLTI, `OP_SLT: ALUOp = `ALU_OP_SLT; `OP_XORI: ALUOp = `ALU_OP_XOR; endcase

PCWre:ID 阶段 J、JAL、JR,或 EXE 阶段 BEQ、BNE、BLTZ,或 MEM 阶段 SW,或 WB 阶段。另外,为保证在每条指令最初阶段的时钟上升沿 PC 发生改变,需要在上一条指令的最后一个下降沿将 PCWre 设置为 1,这样才能保证 PC 在每条指令最开始的时钟上升沿改变。

Copy
always @(negedge CLK) begin case (State) `STATE_ID: begin if (OpCode == `OP_J || OpCode == `OP_JAL || OpCode == `OP_JR) PCWre <= 1; end `STATE_EXE_AL, `STATE_EXE_BR, `STATE_EXE_LS: begin if (OpCode == `OP_BEQ || OpCode == `OP_BNE || OpCode == `OP_BLTZ) PCWre <= 1; end `STATE_MEM: begin if (OpCode == `OP_SW) PCWre <= 1; end `STATE_WB_AL, `STATE_WB_LD: PCWre <= 1; default: PCWre <= 0; endcase end

逻辑算术运算单元#

该模块是一个32位的ALU单元,会根据控制信号对输入的操作数进行不同的运算,例如加、减、与、或等。

Copy
module ALU( input [2:0] ALUOp, input [31:0] A, input [31:0] B, output Sign, output Zero, output reg [31:0] Result ); always @(*) begin case (ALUOp) `ALU_OP_ADD: Result = (A + B); `ALU_OP_SUB: Result = (A - B); `ALU_OP_SLL: Result = (B << A); `ALU_OP_OR: Result = (A | B); `ALU_OP_AND: Result = (A & B); `ALU_OP_LT: Result = (A < B) ? 1 : 0; `ALU_OP_SLT: Result = (((A < B) && (A[31] == B[31])) || ((A[31] && !B[31]))) ? 1 : 0; `ALU_OP_XOR: Result = (A ^ B); endcase $display("[ALU] calculated result [%h] from a = [%h] aluOpCode = [%b] b = [%h]", Result, A, ALUOp, B); end assign Zero = (Result == 0) ? 1 : 0; assign Sign = Result[31]; endmodule

寄存器组#

该模块为一个32位而拥有32个寄存的寄存器组。寄存器组接受 InstructionMemory 的输入,输出对应寄存器的数据,从而实现读取寄存器里的数据的功能。

Copy
module RegisterFile( input CLK, input RST, input WE, input [4:0] ReadReg1, input [4:0] ReadReg2, input [4:0] WriteReg, input [31:0] WriteData, output [31:0] ReadData1, output [31:0] ReadData2 ); reg [31:0] register[1:31]; integer i; assign ReadData1 = ReadReg1 == 0 ? 0 : register[ReadReg1]; assign ReadData2 = ReadReg2 == 0 ? 0 : register[ReadReg2]; always @(negedge CLK or negedge RST) begin if (!RST) begin for (i = 1; i < 32; i = i + 1) begin register[i] = 0; end end else if (WE && WriteReg) begin register[WriteReg] <= WriteData; $display("[RegisterFile] wrote data [%h] into reg $[%d]", WriteData, WriteReg); end end endmodule

符号扩展单元#

该组件有两个功能:符号扩展和零扩展,输入的扩展方法和待扩展的数据,输出扩展后的数据。

Copy
module SignZeroExtend( input ExtSel, // 0 - 0 extend, 1 - sign extend input [15:0] Immediate, output [31:0] DataOut ); assign DataOut[15:0] = Immediate[15:0]; assign DataOut[31:16] = (ExtSel && Immediate[15]) ? 16'hFFFF : 16'h0000; endmodule

指令存储器#

把指令集以二进制的形式写成一个文件,然后在指令存储器中读进来,以读文件的方式把指令存储到内存中,实现指令的读取。

Copy
module InstructionMemory( input RW, input [31:0] IAddr, output reg [31:0] DataOut ); reg [7:0] memory[0:95]; initial begin $readmemb(`MEMORY_FILE_PATH, memory); end always @(IAddr or RW) begin if (RW) begin DataOut[31:24] = memory[IAddr]; DataOut[23:16] = memory[IAddr + 1]; DataOut[15:8] = memory[IAddr + 2]; DataOut[7:0] = memory[IAddr + 3]; $display("[InstructionMemory] Loaded instruction [%h] from address [%h]", DataOut, IAddr); end end endmodule

数据存储单元#

数据存储单元负责存取数据,且由时钟下降沿出发写操作。实现为1字节8位的大端方式存储。

Copy
module DataMemory( input CLK, input mRD, input mWR, input [31:0] DAddr, input [31:0] DataIn, output [31:0] DataOut ); reg [7:0] memory[0:127]; assign DataOut[7:0] = mRD ? memory[DAddr + 3] : 8'bz; assign DataOut[15:8] = mRD ? memory[DAddr + 2] : 8'bz; assign DataOut[23:16] = mRD ? memory[DAddr + 1] : 8'bz; assign DataOut[31:24] = mRD ? memory[DAddr] : 8'bz; always @(negedge CLK) begin if (mWR) begin memory[DAddr] <= DataIn[31:24]; memory[DAddr + 1] <= DataIn[23:16]; memory[DAddr + 2] <= DataIn[15:8]; memory[DAddr + 3] <= DataIn[7:0]; $display("[DataMemory] saved data [%h] into address [%h]", DataIn, DAddr); end end endmodule

程序计数器#

在时钟上升沿处给出下条指令的地址,或在重置信号下降沿处将PC归零。

PC的下一条指令可能是当前 PC+4,也可能是跳转指令地址,还有可能因为停机而不变。 因此还需要设计一个选择器来选择下一条指令地址的计算方式,为此创建了 JumpPCHelper用于计算 j 指令的下一条 PC 地址,和 NextPCHelper 用于根据指令选择不同的计算方式。

Copy
module PC( input CLK, input RST, input PCWre, input [31:0] PCAddr, output reg [31:0] NextPCAddr ); initial NextPCAddr = 0; always @(posedge CLK or negedge RST) begin if (!RST) NextPCAddr <= 0; else if (PCWre || !PCAddr) NextPCAddr <= PCAddr; end endmodule module JumpPCHelper( input [31:0] PC, input [25:0] NextPCAddr, output reg [31:0] JumpPC); wire [27:0] tmp; assign tmp = NextPCAddr << 2; // address * 4 always @(*) begin JumpPC[31:28] = PC[31:28]; JumpPC[27:2] = tmp[27:2]; JumpPC[1:0] = 0; end endmodule module NextPCHelper( input RST, input [1:0] PCSrc, input [31:0] PC, input [31:0] Immediate, input [31:0] RegPC, input [31:0] JumpPC, output reg [31:0] NextPC); always @(RST or PCSrc or PC or Immediate or RegPC or JumpPC) begin if (!RST) NextPC = PC + 4; else begin case (PCSrc) `PC_NEXT: NextPC = PC + 4; `PC_REL_JUMP: NextPC = PC + 4 + (Immediate << 2); `PC_REG_JUMP: NextPC = RegPC; `PC_ABS_JUMP: NextPC = JumpPC; default: NextPC = PC + 4; endcase end end endmodule

选择器#

数据选择,用于数据存储单元之后的选择,这里需要二选一和三选一数据选择器。

Copy
module Selector1In2#( parameter WIDTH = 5 )( input Sel, input [WIDTH-1:0] A, input [WIDTH-1:0] B, output [WIDTH-1:0] Y); assign Y = Sel ? B : A; endmodule module Selector1In3#( parameter WIDTH = 5 )( input [1:0] Sel, input [WIDTH-1:0] A, input [WIDTH-1:0] B, input [WIDTH-1:0] C, output reg [WIDTH-1:0] Y); always @(Sel or A or B or C) begin case (Sel) 2'b00: Y <= A; 2'b01: Y <= B; 2'b10: Y <= C; default: Y <= 0; endcase end endmodule

指令寄存器#

用时钟信号 CLK 驱动,采用边缘触发写入指令二进制码。

Copy
module IR( input CLK, input IRWre, input [31:0] DataIn, output reg [31:0] DataOut ); always @(posedge CLK) begin if (IRWre) begin DataOut <= DataIn; end end endmodule

数据延迟处理#

这部分模块用于切割数据通路。

Copy
module XDR( input CLK, input [31:0] DataIn, output reg [31:0] DataOut ); always @(negedge CLK) DataOut <= DataIn; endmodule

CPU#

有了以上各个模块,一个简单的 CPU 基本就完成了,最后再将他们串起来即可。

完结撒花。

posted @   hez2010  阅读(4575)  评论(2编辑  收藏  举报
编辑推荐:
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
· C++代码改造为UTF-8编码问题的总结
· DeepSeek 解答了困扰我五年的技术问题
阅读排行:
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
点击右上角即可分享
微信分享提示
CONTENTS