资讯详情

资讯详情

计算机组成原理课设资源拆解:单周期到五级流水线与上板验证

简介这份资料包面向高校计算机组成原理课程学习者与课程设计备赛者围绕北航计算机学院计组课设的P0至P5等阶段覆盖Logisim与Verilog-HDL两条实现路线涉及部件及状态机设计、CRC校验、ALU、正则表达式匹配、格雷码计数器、合法表达式识别、汇编语言矩阵乘法与回文判断以及支持7条指令的单周期CPU和10条指令的流水线CPU。包内共1541个文件以Verilog源码、汇编程序、C源码、Logisim电路、工程文件及日志报告文档为主压缩包约33.25MB便于按项目阶段检索。已有288人学习适合需要对照课下测试与课上测试要求、理解从数字部件到流水线CPU完整设计链路的学习者可参考工程组织、源码实现与实验记录补齐状态机设计、指令扩展和流水线工程化方法等关键环节。1. 从「仿真波形对上了」到「板子上数码管亮了」这套课设资源该按什么顺序拆把一份计算机组成原理课程设计的源码包解压出来最先看到的往往是一堆.v文件和几个.asm还有若干约束文件、脚本和一份说不清版本的实验指导。多数人第一反应是从顶层文件顺着往下读读到一半就迷路另一部分人把 testbench 跑通、波形看着像那么回事就认定万事大吉等真上板发现数码管乱跳、按键没反应、串口吐乱码才开始怀疑自己写的到底是 CPU 还是一堆与非门。计算机组成原理课程设计这条链路其实有固定顺序指令集定义、单周期数据通路、流水线改造、汇编激励与参考模型比对、综合约束与上板验证。这套资源就是按这个顺序组织的每个环节对应独立目录改一处不会牵动全身。它适合正在做课设、需要交一份能被人照着复现的实验报告的人也适合想把课本上那张数据通路图变成能仿真、能上板、能跑矩阵乘程序的实物的人。下面按我拆包的顺序讲。2. 指令集先砍到 24 条单周期数据通路的骨架与控制器拆解动手写第一行 Verilog 之前先把指令集定死。这一步偷懒后面控制单元会变成一团解不开的线。2.1 指令集裁剪为什么 24 条就够用课设周期通常按周算不是按学期算。指令集每多一条主译码要多一个分支、ALU 要多一个控制码、测试程序要多一组激励、报告要多一段说明工作量是乘法关系而不是加法关系。我一般会压到 24 条左右覆盖五类R 型算术逻辑、I 型立即数运算、访存、分支、跳转。乘除法、浮点、非对齐访存、异常与中断一律砍掉需要乘法就用移位加法软件模拟需要异常就作为加分项单开一个模块别塞进主数据通路里。具体清单如下编码按 MIPS 子集来这样现成的汇编器和模拟器都能直接用省掉自己写工具链的时间。类型指令opcode / funct说明R 型add / sub0x00 / 0x20、0x22寄存器间加减R 型and / or / slt0x00 / 0x24、0x25、0x2a逻辑与、或、小于置 1R 型sll / srl / sra0x00 / 0x00、0x02、0x03移位量取 rs 低 5 位R 型jr0x00 / 0x08寄存器跳转函数返回用I 型addi / andi / ori0x08、0x0c、0x0d立即数参与运算I 型lw / sw0x23、0x2b基址加偏移访存I 型beq / bne0x04、0x05相等/不等分支I 型lui0x0f立即数装入高 16 位J 型j / jal0x02、0x03无条件跳转、带链接跳转这张表的价值在于它是后面所有模块的唯一真理源主译码的 case 分支、ALU 的控制码、汇编测试程序的写法全部从这里派生。改表之前先改这份清单别直接改代码。2.2 数据通路五个部件的连接顺序不能乱单周期数据通路的连接顺序是固定的PC 输出地址给指令存储器取出的 32 位指令同时送进译码逻辑和寄存器堆的读地址端口寄存器堆读出的两个操作数与符号扩展后的立即数一起进 ALU 的输入端由 ALUSrc 选择走哪一路ALU 结果一方面作为访存地址进数据存储器另一方面绕过存储器直接进写回多路器最终由 MemtoReg 决定写回寄存器堆的是 ALU 结果还是存储器读出数据。这条链上有三个容易写错的地方值得单独拎出来第一个是寄存器堆。读端口必须是组合逻辑assign 直接映射写端口必须挂在时钟上升沿且$zero寄存器要硬连成常数 0任何写操作都不能改它。有人把$zero也做成普通寄存器跑冒泡排序时看着正常一跑带jr $ra的函数调用就飞了。第二个是分支目标地址的计算位置。单周期里在哪算都行但如果你打算后面改流水线最好一开始就把分支判断放在 EX 级、把目标地址用 ALU 单独算出来避免后面大改。这个决定影响的是第 3 章控制冒险的处理方式。第三个是位宽。PC 是 32 位但指令存储器深度往往只有几百到几千字地址截断要显式写清楚别指望综合工具帮你猜。分支偏移是 16 位立即数左移 2 位再符号扩展这个左移不能忘。ALU 是整个通路里唯一需要仔细设计组合逻辑的部件代码如下module alu( input wire [31:0] a, input wire [31:0] b, input wire [3:0] alu_ctrl, // 自定义控制码与主译码的 alu_op 二级译码对应 output reg [31:0] y, output wire zero // 分支指令用它判断相等 ); assign zero (y 32d0); always (*) begin case (alu_ctrl) 4b0010: y a b; // add / addi / lw / sw 地址计算 4b0110: y a - b; // sub / beq / bne 比较 4b0000: y a b; // and / andi 4b0001: y a | b; // or / ori 4b0111: y ($signed(a) $signed(b)) ? 32d1 : 32d0; // slt 必须有符号比较 4b0011: y b a[4:0]; // sll移位量只取低 5 位 4b0100: y b a[4:0]; // srl逻辑右移高位补 0 4b0101: y $signed(b) a[4:0]; // sra算术右移保留符号位 default: y 32d0; // 未定义控制码输出 0便于波形排查 endcase end endmodule参数说明alu_ctrl编码是自己定的只要和译码逻辑对得上就行不必和任何现成标准一致zero是纯组合输出分支指令直接用它不要额外打拍。slt那条必须用$signed强转否则负数比较结果全错这个错误在测试正数时完全看不出来属于典型的埋伏型 bug。sll用b a[4:0]而不是b a是因为移位量超过 31 时 Verilog 行为不确定取低 5 位是最省事也最稳的写法。2.3 控制器主译码与 ALU 译码分两级写控制器拆成两级是让代码可读的关键。第一级看 opcode产出 RegDst、ALUSrc、MemtoReg、RegWrite、MemRead、MemWrite、Branch、Jump 八个一位控制信号外加一个两位的 ALUOp第二级只看 ALUOp 和 funct 字段产出四位 alu_ctrl 送进 ALU。// 第一级主译码只列关键分支其余指令同理 always (*) begin // 先给全部信号赋默认值否则综合一定给你推断出锁存器 reg_write 1b0; mem_read 1b0; mem_write 1b0; branch 1b0; jump 1b0; alu_src 1b0; reg_dst 1b0; mem_to_reg 1b0; alu_op 2b00; case (opcode) 6b000000: begin // R 型结果写 rdALU 做寄存器运算 reg_write 1b1; reg_dst 1b1; alu_op 2b10; end 6b100011: begin // lw立即数进 ALU 算地址读内存写回 reg_write 1b1; mem_read 1b1; alu_src 1b1; mem_to_reg 1b1; end 6b101011: begin // sw只写内存不写寄存器 mem_write 1b1; alu_src 1b1; end 6b000100: begin // beqALU 做减法zero 有效则跳 branch 1b1; alu_op 2b01; end 6b000010: begin // j直接改 PC 低位 jump 1b1; end default: ; // 未识别指令按全零控制处理等于空操作 endcase end这段代码有两个参数含义要强调。reg_dst选的是写回地址来源R 型写 rd 字段lw 这类 I 型写 rt 字段选错会导致寄存器被莫名其妙覆盖波形上表现为某个中间变量突然变成内存地址。alu_op是给第二级用的粗粒度分类2b10表示「看 funct」2b01表示「做减法」2b00表示「做加法」第二级译码据此展开成具体的四位控制码。default分支里那句空语句不是摆设它是防止综合工具把未覆盖的 opcode 当成 dont care 进而优化掉整段逻辑。写到这里单周期的骨架就齐了接下来跑通一个手写的冒泡排序汇编确认寄存器堆写回和分支都能正常工作再进流水线。3. 五级流水线的三个冒险旁路、停顿与冲刷怎么写才不翻车单周期改流水线核心不是把组合逻辑切开而是设计好流水线寄存器里该存什么以及三个冒险单元怎么配合。3.1 流水线寄存器字段设计错了后面全是补丁五级流水线有四个流水线寄存器IF/ID、ID/EX、EX/MEM、MEM/WB。字段设计的经验是「下游需要的上游全带上」宁可多存几个控制位也别指望在下游重新译码一次指令。重新译码会让控制信号和指令流错位是新手最常翻的车。寄存器必须携带的字段数量级IF/IDpc4、32 位指令约 64 位ID/EX全部控制信号、rs/rt 值、立即数、rs/rt/rd 编号约 150 位EX/MEMRegWrite、MemRead、MemWrite、MemtoReg、ALU 结果、rt 值、目标寄存器编号约 75 位MEM/WBRegWrite、MemtoReg、内存读出数据、ALU 结果、目标寄存器编号约 71 位注意 EX/MEM 和 MEM/WB 里都存了「目标寄存器编号」这是旁路单元的比较依据漏了就做不了转发。// IF/ID 流水线寄存器复位时清空避免上电跑出脏指令 always (posedge clk or posedge rst) begin if (rst) begin if_id_pc4 32d0; if_id_instr 32d0; end else if (if_id_flush) begin // 分支跳转时冲刷插入气泡 if_id_pc4 32d0; if_id_instr 32d0; // 全零指令译码成空操作 end else if (!if_id_stall) begin // load-use 冒险时保持不动 if_id_pc4 pc 32d4; if_id_instr imem_out; end end三个控制信号的含义if_id_flush优先级最高分支在 EX 级判定成立时拉高清掉刚取进来的错误指令if_id_stall次之load-use 冒险时保持当前值让 EX 级多等一拍两者都不拉高才正常推进。这里把 flush 放在 stall 前面判断是刻意的分支冲刷必须立刻生效不能被停顿拖住。3.2 数据冒险旁路单元的真值表与优先级数据冒险分两类。第一类是相邻指令的写后读比如add $t0, $t1, $t2紧跟sub $t3, $t0, $t4这类靠旁路forwarding解决不用停。第二类是 load 后立刻用比如lw $t0, 0($sp)紧跟add $t1, $t0, $t2数据要到 MEM 级末尾才出来旁路救不了必须停一拍。旁路单元的判据是两个比较EX/MEM 级的目标寄存器是否等于当前 ID/EX 级的源寄存器MEM/WB 级的是否相等。两个都命中时EX/MEM 优先因为它更近、数据更新。// 旁路单元输出 2b10 取 EX/MEM2b01 取 MEM/WB2b00 用寄存器堆原值 always (*) begin // 源操作数 Ars if (ex_mem_reg_write (ex_mem_rd ! 5d0) (ex_mem_rd id_ex_rs)) forward_a 2b10; // 最近的一条指令优先 else if (mem_wb_reg_write (mem_wb_rd ! 5d0) (mem_wb_rd id_ex_rs)) forward_a 2b01; else forward_a 2b00; // 源操作数 Brt逻辑完全相同只换成 rt 比较 if (ex_mem_reg_write (ex_mem_rd ! 5d0) (ex_mem_rd id_ex_rt)) forward_b 2b10; else if (mem_wb_reg_write (mem_wb_rd ! 5d0) (mem_wb_rd id_ex_rt)) forward_b 2b01; else forward_b 2b00; // load-use 停顿EX 级正在读内存且目标正好是下一条要用的源 load_use_stall id_ex_mem_read ((id_ex_rt if_id_rs) || (id_ex_rt if_id_rt)); end这四段判断里有三个参数细节值得说清楚。ex_mem_rd ! 5d0这个判断不能省$zero永远为 0如果不排除它任何以$zero为源的操作都会被错误转发。mem_wb_reg_write这个门控也不能省否则一条不写回寄存器的指令比如 sw会被误判成数据来源。优先级上 EX/MEM 必须压过 MEM/WB因为两条指令连着写同一个寄存器时最近的那条才是对的顺序写反了会出现「用旧值算新值」的诡异结果而这种错在单条指令测试里根本暴露不出来只有连续写同一寄存器的序列才会触发。3.3 控制冒险分支在 EX 级解决冲刷两级分支指令在 EX 级才算得出结果此时 IF 级已经取进了两条错误指令。处理办法是拉高 flush把 IF/ID 和 ID/EX 两个寄存器清空让错误指令变成空操作。代价是两个周期的惩罚对课设来说完全可以接受不必上分支预测。// 分支冲刷beq 成立或 bne 不成立且当前 EX 级确实是分支指令 wire branch_taken id_ex_branch (id_ex_alu_zero ^ id_ex_branch_ne); assign flush_if_id branch_taken | jump_taken; assign flush_id_ex branch_taken | jump_taken; // 两级一起冲别只冲一级id_ex_branch_ne这个信号是为 bne 准备的beq 和 bne 共用 ALU 的减法结果靠这个位决定是「zero 有效跳」还是「zero 无效跳」。只冲刷 IF/ID 不冲刷 ID/EX 是常见的半吊子写法结果是分支指令后面那条已经被译码的指令照样执行寄存器被悄悄改掉波形上看半天找不出原因。写完这三个单元建议先用一段不含 load-use 的纯算术程序跑流水线确认旁路正确再叠加 load 指令验证停顿最后加分支验证冲刷。三个冒险分开验证出问题时才能一眼定位到是哪一级。4. 验证别只靠肉眼看波形汇编激励、参考模型与矩阵乘压力用例仿真波形是对的这件事说服力约等于零。人眼能同时盯的信号不超过十条而一个五级流水线跑起来有上百条信号在动。真正靠谱的办法是造一个参考模型让 RTL 和模型跑同一段程序逐周期比对寄存器堆和内存。4.1 汇编激励从冒泡排序到 4×4 矩阵乘测试程序要分层。第一层是指令级冒烟测试每条指令单独写一段验证基本功能第二层是算法级测试用冒泡排序验证分支和访存第三层是压力测试用矩阵乘把指令流水线的相关性问题全部逼出来。矩阵乘之所以合适是因为 4×4 整数矩阵乘的内层循环天然会产生连续的数据相关累加器被反复读写、乘加结果立即被下一次使用正好命中旁路和 load-use 两类冒险。下面这段是内层循环的核心片段累加和写回都在里面。# 4x4 矩阵乘内层循环简化版A、B 为按行存放的 32 位整数矩阵 # $s0 A 基址, $s1 B 基址, $s2 C 基址 # $t0 i, $t1 j, $t2 k, $t3 累加器 outer_i: li $t1, 0 # j 0 inner_j: li $t3, 0 # 累加器清零 li $t2, 0 # k 0 inner_k: sll $t4, $t0, 2 # i*4行偏移 addu $t5, $t4, $t2 # (i*4 k) 是 A 的元素下标 sll $t5, $t5, 2 # 再乘 4 得到字节偏移 addu $t5, $s0, $t5 lw $t6, 0($t5) # 取 A[i][k] sll $t7, $t2, 2 # k*4 addu $t7, $t7, $t1 # (k*4 j) 是 B 的元素下标 sll $t7, $t7, 2 addu $t7, $s1, $t7 lw $t8, 0($t7) # 取 B[k][j] mul $t9, $t6, $t8 # 若指令集未实现 mul改为移位加法子程序 addu $t3, $t3, $t9 # 累加连续写同一寄存器专治旁路 bug addi $t2, $t2, 1 blt $t2, 4, inner_k # k 4 继续 # 此处省略写回 C[i][j] 与 j、i 的循环控制这段代码的寄存器分配是有讲究的$t3作为累加器在循环里被反复读写任何旁路优先级错误都会让累加结果偏小$t5、$t7作为地址计算中间量紧跟 lw 使用正好触发 load-use 停顿。如果指令集里没有 mul用移位加法子程序替代反而多了一层函数调用和jr $ra的测试覆盖不亏。4.2 参考模型用 Python 写一个几十行的指令级模拟器参考模型不需要快只需要对。用 Python 写一个逐条解释执行的模拟器内存用一个 dict 表示寄存器堆用一个长度 32 的列表PC 一个变量主循环取指、译码、执行、写回。这个模型和 RTL 完全独立实现两边同时出错且错得一样的概率极低所以比对结果可信。# 极简指令级参考模型只实现关键指令够比对即可 regs [0] * 32 mem {} # 稀疏内存地址 - 值 pc 0 def sign_ext(v, bits16): return v - (1 bits) if v (1 (bits - 1)) else v for step in range(MAX_STEPS): instr mem.get(pc, 0) op (instr 26) 0x3F rs (instr 21) 0x1F rt (instr 16) 0x1F rd (instr 11) 0x1F imm sign_ext(instr 0xFFFF) if op 0x00: # R 型 funct instr 0x3F if funct 0x20: regs[rd] (regs[rs] regs[rt]) 0xFFFFFFFF elif funct 0x22: regs[rd] (regs[rs] - regs[rt]) 0xFFFFFFFF elif funct 0x2a: regs[rd] 1 if regs[rs] regs[rt] else 0 # 其余 funct 按同样套路补全 elif op 0x23: # lw addr (regs[rs] imm) 0xFFFFFFFF regs[rt] mem.get(addr, 0) elif op 0x2b: # sw addr (regs[rs] imm) 0xFFFFFFFF mem[addr] regs[rt] elif op 0x04: # beq if regs[rs] regs[rt]: pc pc 4 (imm 2) continue pc 4 regs[0] 0 # $zero 永远是 0每步强制复位最保险参数说明MAX_STEPS设一个上限防止死循环把仿真拖死一般取测试程序指令数的三到五倍mem用 dict 而不是列表是因为程序里可能访问到很高的地址段稀疏表示省内存regs[0] 0每步强制赋值比在每个写回分支里判断更不容易漏。4.3 自动比对把「我觉得对」变成「脚本说对」有了 RTL 和模型剩下的事就是让两者输出同一种格式再 diff。RTL 侧在 testbench 里加一段 dump 逻辑每写一次寄存器堆就打印一行模型侧跑完打印同样格式。脚本一把梭退出码非零就是失败。#!/usr/bin/env bash # run_regression.sh编译、仿真、比对一条龙 set -e iverilog -g2005 -o sim.out tb_cpu.v alu.v regfile.v cpu_top.v vvp sim.out PROGmatmul4x4.hex DUMPrtl_dump.txt python3 refmodel.py matmul4x4.hex model_dump.txt if diff -q rtl_dump.txt model_dump.txt /dev/null; then echo PASS: matmul4x4 else echo FAIL: matmul4x4首个差异行如下 diff rtl_dump.txt model_dump.txt | head -5 exit 1 fi脚本里PROG和DUMP是传给 testbench 的 plusarg前者指定十六进制机器码文件后者指定 dump 输出路径。set -e保证编译失败时立刻停下不会拿着上一版的仿真结果去比对。光比对通过还不够得知道测了哪些情况。我一般会统计指令覆盖率把所有 24 条指令和三类冒险各列一行跑完测试集打勾。覆盖项触发用例是否覆盖R 型算术逻辑 8 条instr_smoke.asm是lw / sw 及地址回绕sort_bubble.asm是beq / bne 前后向分支sort_bubble.asm是j / jal / jr 函数调用func_call.asm是旁路 EX/MEM 优先级matmul4x4.asm是load-use 停顿matmul4x4.asm是分支冲刷两级sort_bubble.asm是这张表放进实验报告比贴十张波形图有说服力得多。写报告的人应该都能体会到评审最常问的就是「你怎么证明它是对的」一张覆盖率表比任何描述都直接。5. 上板前的静态检查与五条踩坑记录仿真全部通过不代表能上板。从 RTL 到真正能在板子上跑的比特流中间隔着综合、实现、时序收敛三道关很多在仿真里被忽略的问题会在这三步集中爆发。5.1 综合前必须过一遍的静态检查清单这份清单我每次都会从头走一遍大概花十分钟能挡掉八成低级错误。检查项检查方法期望结果组合逻辑是否有默认赋值看每个always (*)开头所有被赋值信号先赋默认值是否推断出锁存器综合日志搜 latch零条 latch 警告是否有组合环综合日志搜 combinational loop零条复位极性是否统一全局搜posedge rst/negedge rst全工程只用一种位宽截断综合日志搜 truncat逐条确认是有意为之跨时钟域信号检查是否有第二时钟课设一般单时钟出现即排查存储器初值看是否依赖上电初值显式复位或初始化文件加载「制造」这个词在这里要落地理解综合通过只是说明逻辑能映射到器件资源真正生成可烧录的比特流还要过布局布线和时序分析。时序不收敛时工具会报负的 slack这时候功能再对板子上跑出来也是随机行为看起来很玄学。5.2 五条血泪记录现象、原因、解决第一条数码管上电全亮然后乱跳。现象是复位按下后显示正常松手几秒就开始随机变化。原因是复位信号只接到了 PC 和流水线寄存器寄存器堆和存储器没有复位上电初值不确定第一条指令取出来就是随机的。解决办法是把复位扇出到所有时序元件或者在存储器初始化文件里显式给每条指令槽填 0。第二条仿真波形完全正确上板跑排序结果错一位。现象是只有最后一个元素位置不对。原因是数据存储器的写使能在时钟上升沿采样而地址在同一时刻变化出现了写穿透。解决是把写使能改成下降沿有效或者让地址在写使能之前一个周期稳定下来。这类问题在行为级仿真里因为零延迟模型完全看不出来只有加了时序延迟或者上板才暴露。第三条综合报一堆 latch 警告但功能仿真正常。现象是某个控制信号在某些指令下保持上一个值。原因就是第 2 章提到的always (*)里没有先赋默认值综合工具认为你要保持状态于是推断出锁存器。解决是在每个组合 always 块开头把所有输出信号赋一遍默认值一个都不能漏。第四条跑矩阵乘时结果偶尔错重跑又对了。现象是同一份比特流复位后跑两次结果不同。原因是初始化文件加载和复位释放之间存在竞争存储器还没加载完 CPU 就开始取指。解决是加一个上电延时计数器复位释放后等几百个周期再放开 PC或者用工具提供的初始化完成信号做门控。第五条时序分析报负 slack但降频后能跑。现象是 50MHz 下不稳定25MHz 下正常。原因是关键路径在 ALU 的组合逻辑上尤其是带$signed比较的 slt 那一支串了好几级。解决办法是把 slt 单独拆出来用减法高位判断实现或者干脆降频并在报告里说明这是课设约束下的取舍。知道边界在哪比硬撑着一个跑不稳的频率有价值。6. 把 CPI 和周期数算准让报告里的数据经得起追问做到这一步功能和上板都通了最后一段功夫花在数据上。实验报告里最有分量的不是「实现了五级流水线」这句话而是「同一段矩阵乘程序单周期跑了多少周期五级流水跑了多少周期CPI 分别是多少差在哪」。统计周期数最简单的办法是在 testbench 里挂一个计数器从复位释放开始数到程序最后一条指令写回为止。单周期 CPU 的 CPI 恒等于 1总周期数就等于指令条数五级流水线因为流水填充和冒险停顿CPI 会略高于 1具体高多少取决于测试程序的相关性密度。实现版本矩阵规模指令条数总周期数CPI单周期4×4约 1200约 12001.00五级流水无旁路4×4约 1200约 3100约 2.58五级流水有旁路4×4约 1200约 1580约 1.32五级流水有旁路8×88×8约 4600约 6100约 1.33这张表的用法是横向比无旁路版本 CPI 高得离谱说明大量周期耗在等数据上加了旁路之后掉到 1.3 附近剩下的 0.3 主要来自 load-use 停顿和分支冲刷。把这两个来源分别统计出来就能算出「如果换成延迟槽或者分支预测还能再省多少」这是报告里最容易出彩的一段分析。有个技巧值得单独说给测试程序里的每类指令加计数器跑完输出各类型指令占比。如果 load 指令占比高CPI 偏高就完全解释得通如果分支占比高而 CPI 也高那就说明冲刷代价大。这个分布数据用脚本从汇编源里统计就行几行正则的事但能让报告里的每一个数字都有出处。我自己的习惯是每次改完流水线控制逻辑不管改动多小都强制把单周期版本、无旁路版本、有旁路版本三个仿真全跑一遍三份 dump 文件一起存进带日期的目录。有次改旁路优先级只跑了有旁路版本结果单周期版本的参考输出被覆盖了回头想对比发现没基线只能重跑一遍全部用例那次之后这个习惯就再没断过。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →