单周期CPU设计【Verilog】
第一章 单周期CPU的设计原理
1.1 单周期CPU概述
1.2 CPU工作原理
第二章 单周期CPU的设计内容
2.1 指令系统的设计
2.1.1 概述
2.1.2 运算类指令的设计
2.1.3 传送类指令的设计
2.1.4 存储类指令的设计
2.1.5 控制类指令的设计
2.2 整体框架的设计
2.3 数据通路的设计
2.4 控制信号的设计
第三章 单周期CPU的具体实现
3.1 底层模块的实现
3.1.1 程序计数器PC
3.1.2 指令存储器InstructionMemory
3.1.3 寄存器组RegisterFile
3.1.4 算术逻辑单元ALU
3.1.5 数据存储器DataMemory
3.1.6 控制单元ControlUnit
3.2 顶层模块的实现
第四章 单周期CPU的仿真验证
4.1 测试程序
4.2 波形仿真
第一章 单周期CPU的设计原理
为实现单周期CPU,本文首先研究了单周期CPU的相关理论和工作原理。本章首先简要介绍单周期CPU的基本概念,然后对CPU的工作原理进行简要分析,以便为单周期CPU的设计和开发提供理论基础。
1.1 单周期CPU概述
1.2 CPU工作原理
(2) 指令译码(ID):对取指令操作中得到的指令进行分析并译码,确定这条指令需要完成的操作,从而产生相应的操作控制信号,用于驱动执行状态中的各种操作。
(3) 指令执行(EXE):根据指令译码得到的操作控制信号,具体地执行指令动作,然后转移到结果写回状态。
(4) 存储器访问(MEM):所有需要访问存储器的操作都将在这个步骤中执行,该步骤给出存储器的数据地址,把数据写入到存储器中数据地址所指定的存储单元或者从存储器中得到数据地址单元中的数据。
(5) 结果写回(WB):指令执行的结果或者访问存储器中得到的数据写回相应的目的寄存器中。
第二章 单周期CPU的设计内容
2.1 指令系统的设计
2.1.1 概述
本文所设计的单周期CPU的指令系统采用类似MIPS的设计风格,包括以下四类指令:
其中,所有指令的操作码部分用4位二进制表示,寄存器编号用3位二进制表示。在下述的具体设计表示中,以助记符表示的是汇编指令;以代码表示的则是二进制机器指令。
2.1.2 运算类指令的设计
0000 |
rs(3位) |
rt(3位) |
rd(3位) |
reserved(未用) |
功能:将寄存器rs和rt中的值相加并将结果写回寄存器rd。reserved为预留部分,即未用,一般填“0”。
0001 |
rs(3位) |
rt(3位) |
immediate(6位) |
功能:将立即数immediate进行符号扩展后再和寄存器rs中的值相加并将结果写回寄存器rt。
0010 |
rs(3位) |
rt(3位) |
rd(3位) |
reserved(未用) |
功能:用寄存器rs中的值减去rt中的值并将结果写回寄存器rd。reserved为预留部分,即未用,一般填“0”。
0011 |
rs(3位) |
rt(3位) |
rd(3位) |
reserved(未用) |
功能:将寄存器rs和rt中的值进行按位或运算并将结果写回寄存器rd。reserved为预留部分,即未用,一般填“0”。
0100 |
rs(3位) |
rt(3位) |
immediate(6位) |
功能:将立即数immediate进行“0”扩展后再和寄存器rs中的值进行按位或运算并将结果写回寄存器rt。
0101 |
rs(3位) |
rt(3位) |
rd(3位) |
reserved(未用) |
功能:将寄存器rs和rt中的值进行按位与运算并将结果写回寄存器rd。reserved为预留部分,即未用,一般填“0”。
0110 |
未用(3位) |
rt(3位) |
rd(3位) |
sa(3位) |
功能:先将sa表示的立即数进行“0”拓展,然后将寄存器rt中的值左移sa位并将结果写回寄存器rd。
0111 |
rs(3位) |
rt(3位) |
rd(3位) |
reserved(未用) |
功能:将寄存器rs和rt中的值作为带符号数进行比较,若rs中的值更小,则将寄存器rd中的值置1,否则置0。
2.1.3 传送类指令的设计
1000 |
未用(3位) |
rt(3位) |
rd(3位) |
reserved(未用) |
功能:将寄存器rt中的值传送到寄存器rd。reserved为预留部分,即未用,一般填“0”。
1001 |
未用(3位) |
rt(3位) |
immediate(6位) |
功能:将立即数immediate进行符号扩展后传送到寄存器rt。
2.1.4 存储类指令的设计
1010 |
rs(3位) |
rt(3位) |
immediate(6位) |
功能:将寄存器rt中的值存储到内存中,存储单元的地址为寄存器rs中的值加上经符号拓展的立即数immediate。
1011 |
rs(3位) |
rt(3位) |
immediate(6位) |
功能:将内存中的值加载到寄存器rt中,存储单元的地址为寄存器rs中的值加上经符号拓展的立即数immediate。
2.1.5 控制类指令的设计
1100 |
rs(3位) |
rt(3位) |
immediate(6位) |
功能:比较寄存器rs和rt中的值,若相等则跳转到目标处执行,否则继续执行下一条指令。跳转的目标地址与当前指令的下一条指令的相对位移由立即数immediate给出。
1101 |
rs(3位) |
000 |
immediate(6位) |
功能:判断寄存器rs中的值是否大于0,若大于0则跳转到目标处执行,否则继续执行下一条指令。跳转的目标地址与当前指令的下一条指令的相对位移由立即数immediate给出。
1110 |
addr(12位) |
功能:执行无条件跳转,跳转的目的地址组成如下:高3位为PC中值的高3位,第1-12位由addr给出,最低位为0。
1111 |
000000000000(12位) |
2.2 整体框架的设计
本文所设计的单周期CPU的整体框架主要包括七部分:程序计数器、指令寄存器、寄存器组、算术逻辑单元、数据存储器、控制单元和顶层模块。具体框架如下:
2.3 数据通路的设计
IDataOut,指令存储器数据输出端口(指令代码输出端口)
Write Reg,将数据写入的寄存器端口,其地址来源为rt或rd字段
zero,运算结果标志,结果为0,则zero=1;否则zero=0
sign,运算结果标志,结果最高位为0,则sign=0,正数;否则,sign=1,负数
2.4 控制信号的设计
控制信号名 |
状态“0” |
状态“1” |
Reset |
初始化PC为0 |
PC接收新地址 |
PCWre |
PC不更改,相关指令:halt |
PC更改,相关指令:除指令halt外 |
ALUSrcA |
来自寄存器堆data1输出,相关指令:add、addi、sub、or、ori、and、slt、mov、movi、beq、bgtz、sw、lw |
来自经过“0”拓展的移位数sa,即 {{13{0}},sa},相关指令:sll |
ALUSrcB |
来自寄存器堆data2输出,相关指令:add、sub、or、and、sll、slt、mov、beq、bgtz |
来自sign或zero扩展的立即数,相关指令:addi、ori、movi、sw、lw |
DBDataSrc |
来自ALU运算结果的输出,相关指令:add、addi、sub、or、ori、and、sll、slt、mov、movi |
来自数据存储器(Data MEM)的输出,相关指令:lw |
RegWre |
寄存器组写不使能,相关指令:sw、beq、bgtz、j、halt |
寄存器组写使能,相关指令:add、addi、sub、or、ori、and、sll、slt、mov、movi、lw |
InsMemRW |
写指令存储器 |
读指令存储器 |
RD |
读数据存储器,相关指令:lw |
无操作 |
WR |
写数据存储器,相关指令:sw |
无操作 |
RegDst |
写寄存器组寄存器的地址,来自rt字段,相关指令:addi、ori、movi、lw |
写寄存器组寄存器的地址,来自rd字段,相关指令:add、sub、or、and、sll、slt、mov |
ExtSel |
(zero-extend)immediate(0扩展),相关指令:ori |
(sign-extend)immediate(符号扩展),相关指令:addi、movi、sw、lw、beq、bgtz |
PCSrc[1..0] |
00:pc<-pc+2,相关指令:add、addi、sub、or、ori、and、sll、slt、sw、lw、beq(zero=0)、bgtz(sign=1,或zero=1); 01:pc<-pc+2+(sign-extend)immediate,相关指令:beq(zero=1)、bgtz(sign=0,zero=0); 10:pc<-{(pc+2)[15..13],addr[12..1],0},相关指令:j; 11:未用 |
|
ALUOp[2..0] |
ALU 8种运算功能选择(000-111),具体见ALU功能表 |
ALUOp[2..0] |
功能 |
描述 |
000 |
Y = A+B |
加 |
001 |
Y = A-B |
减 |
010 |
Y = B<<A |
B左移A位 |
011 |
Y = A∨B |
或 |
100 |
Y = A∧B |
与 |
101 |
Y=(A<B)?1: 0 |
比较A与B 不带符号 |
110 |
if (A<B && (A[15] == B[15] ) ) Y = 1; else if ( A[15] && !B[15) Y = 1; else Y = 0; |
比较A与B 带符号 |
111 |
Y = B |
直送 |
在具体进行控制时,PC的改变是在时钟上升沿进行的,指令执行的结果总是在时钟下降沿保存到寄存器和存储器中,这样稳定性较好。
第三章 单周期CPU的具体实现
3.1 底层模块的实现
3.1.1 程序计数器PC
PC为时序逻辑,在时钟上升沿到来时,若PCWre为1,则输出下一条待处理的指令地址,否则不输出新的指令地址。若Reset为0,则设置输出的指令地址为0。具体代码如下:
module PC( input clk, input [15:0] PCin, input PCWre, input Reset, output reg [15:0] PCout
); initial begin PCout <= 0; end always@(posedge clk) begin if(Reset == 0) begin PCout <= 0; end else if(PCWre == 0) begin PCout <= PCout; end else begin PCout <= PCin; end end endmodule
3.1.2 指令存储器InstructionMemory
module InsMemory( input InsMemRW, input [15:0] address, output reg [15:0] DataOut
); reg [7:0] mem [0:127]; initial begin DataOut = 16'b1111000000000000; $readmemb("Instructions.txt", mem); end always@(*) begin DataOut[15:8] <= mem[address]; DataOut[7:0] <= mem[address+1]; end endmodule
3.1.3 寄存器组RegisterFile
module RegFile( input CLK, input RST, input RegWre, input [2:0] ReadReg1, input [2:0] ReadReg2, input [2:0] WriteReg, input [15:0] WriteData, output [15:0] ReadData1, output [15:0] ReadData2
); reg [15:0] regFile[0:7]; integer i; assign ReadData1 = regFile[ReadReg1]; assign ReadData2 = regFile[ReadReg2]; always @ (negedge CLK) begin if (RST == 0) begin for(i=1;i<8;i=i+1) regFile[i] <= 0; end else if(RegWre == 1 && WriteReg != 0) begin regFile[WriteReg] <= WriteData; end end endmodule
3.1.4 算术逻辑单元ALU
ALU为组合逻辑,根据控制信号对两个操作数进行相应的运算,并输出结果。具体代码如下:
module ALU( input [2:0] ALUopcode, input [15:0] rega, input [15:0] regb, output reg [15:0] result, output zero, output sign
); assign zero = (result==0)?1:0; assign sign = result[15]; always @( ALUopcode or rega or regb ) begin case (ALUopcode) 3'b000 : result = rega + regb; 3'b001 : result = rega - regb; 3'b010 : result = regb << rega; 3'b011 : result = rega | regb; 3'b100 : result = rega & regb; 3'b101 : result = (rega < regb)?1:0; 3'b110 : begin if (rega<regb &&(( rega[15] == 0 && regb[15]==0) || (rega[15] == 1 && regb[15]==1))) result = 1; else if (rega[15] == 0 && regb[15]==1) result = 0; else if ( rega[15] == 1 && regb[15]==0) result = 1; else result = 0; end 3'b111 : result = regb; endcase end endmodule
3.1.5 数据存储器DataMemory
module DataMemory( input clk, input [15:0] address, input RD, input WR, input [15:0] DataIn, output [15:0] DataOut
); reg [7:0] ram[0:127]; integer i; initial begin; for(i=0;i<128;i=i+1) ram[i]<=0; end assign DataOut[7:0] = (RD == 0)? ram[address+1]:8'bz; assign DataOut[15:8] = (RD == 0)? ram[address]:8'bz; always@(negedge clk) begin if(WR == 0) begin if(address>=0 && address<128) begin ram[address] <= DataIn[15:8]; ram[address+1] <= DataIn[7:0]; end end end endmodule
3.1.6 控制单元ControlUnit
ControlUnit为组合逻辑,将机器码中的操作码(opcode)转换为各个控制信号,从而控制不同的指令在不同的数据通路中传输。具体代码如下:
module ControlUnit( input [3:0] opcode, input zero, input sign, output reg PCWre, output reg ALUSrcA, output reg ALUSrcB, output reg DBDataSrc, output reg RegWre, output reg InsMemRW, output reg RD, output reg WR, output reg RegDst, output reg ExtSel, output reg [1:0] PCSrc, output reg [2:0] ALUOp
); initial begin RD = 1; WR = 1; RegWre = 0; PCWre = 0; InsMemRW = 1; end always@ (opcode) begin case(opcode) 4'b0000:begin // add PCWre = 1; ALUSrcA = 0; ALUSrcB = 0; DBDataSrc = 0; RegWre = 1; InsMemRW = 1; RD = 1; WR = 1; RegDst = 1; ALUOp = 3'b000; end 4'b0001:begin //addi PCWre = 1; ALUSrcA = 0; ALUSrcB = 1; DBDataSrc = 0; RegWre = 1; InsMemRW = 1; RD = 1; WR = 1; RegDst = 0; ExtSel = 1; ALUOp = 3'b000; end 4'b0010:begin //sub PCWre = 1; ALUSrcA = 0; ALUSrcB = 0; DBDataSrc = 0; RegWre = 1; InsMemRW = 1; RD = 1; WR = 1; RegDst = 1; ALUOp = 3'b001; end 4'b0011:begin // or PCWre = 1; ALUSrcA = 0; ALUSrcB = 0; DBDataSrc = 0; RegWre = 1; InsMemRW = 1; RD = 1; WR = 1; RegDst = 1; ALUOp = 3'b011; end 4'b0100:begin // ori PCWre = 1; ALUSrcA = 0; ALUSrcB = 1; DBDataSrc = 0; RegWre = 1; InsMemRW = 1; RD = 1; WR = 1; RegDst = 0; ExtSel = 0; ALUOp = 3'b011; end 4'b0101:begin //and PCWre = 1; ALUSrcA = 0; ALUSrcB = 0; DBDataSrc = 0; RegWre = 1; InsMemRW = 1; RD = 1; WR = 1; RegDst = 1; ALUOp = 3'b100; end 4'b0110:begin //sll PCWre = 1; ALUSrcA = 1; ALUSrcB = 0; DBDataSrc = 0; RegWre = 1; InsMemRW = 1; RD = 1; WR = 1; RegDst = 1; ALUOp = 3'b010; end 4'b0111:begin //slt PCWre = 1; ALUSrcA = 0; ALUSrcB = 0; DBDataSrc = 0; RegWre = 1; InsMemRW = 1; RD = 1; WR = 1; RegDst = 1; ALUOp = 3'b110; end 4'b1000:begin //mov PCWre = 1; ALUSrcA = 0; ALUSrcB = 0; DBDataSrc = 0; RegWre = 1; InsMemRW = 1; RD = 1; WR = 1; RegDst = 1; ExtSel = 1; ALUOp = 3'b111; end 4'b1001:begin //movi PCWre = 1; ALUSrcA = 0; ALUSrcB = 1; DBDataSrc = 0; RegWre = 1; InsMemRW = 1; RD = 1; WR = 1; RegDst = 0; ExtSel = 1; ALUOp = 3'b111; end 4'b1010:begin //sw PCWre = 1; ALUSrcA = 0; ALUSrcB = 1; RegWre = 0; InsMemRW = 1; RD = 1; WR = 0; ExtSel =1; ALUOp = 3'b000; end 4'b1011:begin //lw PCWre = 1; ALUSrcA = 0; ALUSrcB = 1; DBDataSrc = 1; RegWre = 1; InsMemRW = 1; RD = 0; WR = 1; RegDst = 0; ExtSel = 1; ALUOp = 3'b000; end 4'b1100:begin //beq PCWre = 1; ALUSrcA = 0; ALUSrcB = 0; RegWre = 0; InsMemRW = 1; RD = 1; WR = 1; ExtSel = 1; ALUOp = 3'b001; end 4'b1101:begin //bgtz PCWre = 1; ALUSrcA = 0; ALUSrcB = 0; RegWre = 0; InsMemRW = 1; RD = 1; WR = 1; ExtSel = 1; ALUOp = 3'b001; end 4'b1110:begin //j PCWre = 1; RegWre = 0; InsMemRW = 1; RD = 1; WR = 1; ALUOp = 3'b010; end 4'b1111:begin //halt PCWre = 0; RegWre = 0; InsMemRW = 1; RD = 1; WR = 1; end default:begin RD = 1; WR = 1; RegWre = 0; InsMemRW = 0; end endcase end always@(opcode or zero or sign) begin if(opcode == 4'b1110) // j PCSrc = 2'b10; else if(opcode == 4'b1100) begin if(zero == 1) PCSrc = 2'b01; else PCSrc = 2'b00; end else if(opcode == 4'b1101) begin if(zero == 0 && sign == 0) PCSrc = 2'b01; else PCSrc = 2'b00; end else begin PCSrc = 2'b00; end end endmodule
3.2 顶层模块的实现
在顶层模块中,通过实例化各个底层模块,并使用导线将它们按照数据通路图连接起来,构成单周期CPU的完整结构,具体原理图如下:
第四章 单周期CPU的仿真验证
根据上述实现的单周期CPU,利用指令系统中所有的指令设计一个测试程序,对该CPU进行如下仿真验证。
4.1 测试程序
使用该指令系统中所有指令,设计出如下一段具有实际意义的测试程序。该程序计算数字1到5的和以及按位或的和,并将两者中较大者存储到内存中地址为14的存储单元中,最后读出该存储单元中的值。具体指令序列如下:
地址 |
汇编程序 |
机器指令代码(二进制) |
0x0000 |
movi $0,0 |
10010000 00000000 |
0x0002 |
mov $1,$0 |
10000000 00001000 |
0x0004 |
and $3,$1,$0 |
01010010 00011000 |
0x0006 |
addi $2,$0,6 |
00010000 10000110 |
0x0008 |
ori $3,$0,1 |
01000000 11000001 |
0x000A |
add $5,$0,$3 |
00000000 11101000 |
0x000C |
or $6,$1,$3 |
00110010 11110000 |
0x000E |
mov $0,$5 |
10000001 01000000 |
0x0010 |
mov $1,$6 |
10000001 10001000 |
0x0012 |
addi $4,$3,1 |
00010111 00000001 |
0x0014 |
mov $3,$4 |
10000001 00011000 |
0x0016 |
beq $3,$2,1 |
11000110 10000001 |
0x0018 |
j 5 |
11100000 00000101 |
0x001A |
slt $5,$0,$1 |
01110000 01101000 |
0x001C |
movi $4,4 |
10010001 00000100 |
0x001E |
sub $6,$2,$4 |
00100101 00110000 |
0x0020 |
sll $7,$6,1 |
01100001 10111001 |
0x0022 |
bgtz $5,2 |
11011010 00000010 |
0x0024 |
sw $0,10($7) |
10101110 00001010 |
0x0026 |
j 21 |
11100000 00010101 |
0x0028 |
sw $1,10($7) |
10101110 01001010 |
0x002A |
lw $6,10($7) |
10111111 10001010 |
0x002C |
halt |
11110000 00000000 |
4.2 波形仿真
在仿真过程中,设置如下输入和输出,并显示如下寄存器中的内容:
输入 |
clk |
CPU时钟信号,周期为10ns |
reset |
CPU复位信号,0为复位 |
|
输出 |
nowIns |
当前指令 |
nextPC |
下一条指令地址 |
|
nowPC |
当前PC地址 |
|
ALUSrcA |
ALU操作数A |
|
ALUSrcB |
ALU操作数 |
|
ALUResult |
ALU运算结果 |
|
finalData |
写回的结果 |
|
寄存器 |
RegFile[0]-RegFile[7] |
寄存器组 |
DataMemory[10]-DataMemory[15] |
数据存储器 |
(6) add $5,$0,$3至beq $3,$2,1 (第五轮循环)
单周期CPU设计【Verilog】相关推荐
- 31条指令单周期cpu设计(Verilog)-(十)上代码→顶层模块设计总结
说在前面 开发环境:Vivado 语言:Verilog cpu框架:Mips 控制器:组合逻辑 设计思路 按照预先设计好的数据通路图将各个模块连接起来 `timescale 1ns / 1ps mod ...
- 31条指令单周期cpu设计(Verilog)-(二)总体设计
目录 31条指令单周期cpu设计(Verilog)-(一)相关软件 31条指令单周期cpu设计(Verilog)-(二)总体设计 31条指令单周期cpu设计(Verilog)-(三)指令分析 ...
- 单周期CPU设计(Verilog)
2017/06/08: 当时单周期cpu写的比较仓促,没有深入的进行调试,我准备在放假的时候重构一下代码, 然后把博文改进一下,现在实在没有时间,很抱歉~ 不过多周期我有调试过的,所以有需要的可以移步 ...
- 31条指令单周期cpu设计(Verilog)-(四)数据输入输出关系表
说在前面 开发环境:Vivado 语言:Verilog cpu框架:Mips 控制器:组合逻辑 表格作用 这张表用于设计整体的数据通路图 (在第二篇中已经给出来了),而这张总图是用于设计Verilog ...
- 31条指令单周期cpu设计(Verilog)-(一)相关软件
说在前面 开发环境:Vivado 语言:Verilog cpu框架:Mips 计算机组成原理课程终于结束了,由于以下均为课程学习过程中的内容,所以难免存在各种错误,各位大佬轻喷 相关软件 vivado ...
- 31条指令单周期cpu设计(Verilog)-(三)指令分析
说在前面 开发环境:Vivado 语言:Verilog cpu框架:Mips 控制器:组合逻辑 鸽鸽鸽... 指令分析流程 确定一条指令所需要的具体操作 分析该条指令涉及的部件 确定各个部件的输入输出 ...
- 31条指令单周期cpu设计(Verilog)-(八)上代码→指令译码以及控制器
说在前面 开发环境:Vivado 语言:Verilog cpu框架:Mips 控制器:组合逻辑 指令译码器 我们需要根据一条32位的指令的结构确定是哪一条指令 可以根据操作码(op)以及功能码(fun ...
- 31条指令单周期cpu设计(Verilog)-(七)整体代码结构
说在前面 开发环境:Vivado 语言:Verilog cpu框架:Mips 控制器:组合逻辑 结构 sccpu:顶层模块 cpu_ins:指令译码器 cpu_opcode:控制器 其他均是基本模块( ...
- 31条指令单周期cpu设计(Verilog)-(六)指令操作时间表设计
说在前面 开发环境:Vivado 语言:Verilog cpu框架:Mips 控制器:组合逻辑 这张表格又是干啥的呢(+_+)? 废话少说,用来设计控制器的 (红色方框) 设计流程 1. 表头为3 ...
- 31条指令单周期cpu设计(Verilog)-(五)整体数据通路图设计
说在前面 开发环境:Vivado 语言:Verilog cpu框架:Mips 控制器:组合逻辑 这张图是用来干啥的? 我们在用verilog实现这个cpu的时候,一般是先把各个部件单独写一个modul ...
最新文章
- python loop call soon_从“call\u soon”回调函数执行协同路由
- android 树形目录结构的实现(包含源码)
- 完美解答35K月薪的MySQL面试题(一)MySQL是如何存储数据的
- CSS 基础:文本和字体(4)思维导图
- 线性代数思维导图_线性代数入门级思维导图
- 汉堡王什么汉堡好吃_315 曝光用过期面包做汉堡:汉堡王到底怎么了?
- 【ArcGIS风暴】ArcGIS中国地表覆盖数据GlobeLand30预处理(批量投影、拼接、掩膜提取)附成品下载
- 创建组件“ovalshape”失败_Django的forms组件检验字段\渲染模板
- Kite Compositor for Mac基本工具的使用教程
- 戴尔Dell EMC S5048-ON交换机光模块解决方案
- Android_Message里面彩信图片的压缩方法
- Linux中查看各文件夹大小命令du -h --max-depth=1
- 用exclusion切断maven jar包的依赖传递
- WIN10共享打印机连接出现0x0000011b错误代码无法共享打印
- Linux效劳器的零碎内存监控方法详细解析-2
- 2018天体赛决赛 L1-6 福到了 (15 分)
- 【纯干货】100个运营工具推荐
- 在Raspberry Pi上安装HDMI-CEC
- C语言之volatile用法(二十一),2021最新Android面试笔试题目分享
- 【多用户商城系统】多用户商城系统网站怎么做?