
简介面向计算机科学与技术专业学生的计算机组成原理课程设计成果包聚焦基于Verilog HDL的流水线CPU实现适合正在做组成原理课设、需要参考可上板流水线设计的学生对照学习。工程完整覆盖五级流水分段、数据冲突时的气泡插入与重定向、多级嵌套中断的现场保存与恢复等关键机制并配有仿真测试平台与综合实现流程可直接用Vivado打开复现从源码到比特流的验证链路。压缩包共345个文件、约31.39MB其中v源文件与xdc约束构成设计主体xml、rpt、jou日志记录综合与实现过程dcp为各阶段检查点bit为可下载到开发板的码流另有tcl脚本、rst复位说明与少量前端页面文件目录结构清晰便于按模块检索。已有1949人学习下载适合需要完整工程骨架、排错线索与上板参考的中高阶学习者。1. 从一条指令的迟到说起这份 CPU 流水 Verilog 源码能解决什么仿真波形跑到第 37 个周期load 指令后面的 add 拿到的还是旧寄存器值问题不在 ALU而在 load-use 冒险少插了一个气泡。这份某高校计算机组成原理课程设计的 CPU 流水 Verilog 源码把五级流水、插入气泡、重定向和多级嵌套中断放在同一套可仿真工程里适合刚学完数字逻辑、想把手写单周期 CPU 改成流水线的人也适合要交课程设计、需要一份能改能验的参考实现的开发者。它不追求跑分价值在于每条冒险通路和中断现场都有对应的信号与波形翻车时能定位到具体流水寄存器。2. 工程骨架与跑通路径目录、模块边界和仿真入口拿到源码包后先别急着改代码先把模块边界和复位路径看清楚。流水线 CPU 最常见的翻车不是指令写错而是stall、flush、valid这三根线的语义混了有人把 stall 当 flush 用结果气泡没插进去错误路径反而被放进来。比较稳的顺序是先看顶层端口再看流水寄存器最后看冒险单元和中断控制器。下面这套目录组织是这类课程设计里很常见的做法实际压缩包里的文件名可能略有差别但职责划分基本一致。2.1 目录结构与模块边界我一般先按文件清单建立一张“信号地图”不急着打开每一个.v。顶层负责把五级模块串起来流水寄存器负责在级间保存有效位冒险单元和重定向单元只做组合判断中断控制器单独成模块。这样拆的好处是改冒险逻辑时不用动 ALU调中断嵌套时也不用碰取指模块。路径作用先看什么rtl/cpu_top.v顶层连接五级流水与中断控制器端口、复位、valid传播rtl/if_stage.v取指维护 PC 和指令存储器接口pc_f、stall_f、flushrtl/id_stage.v译码、读寄存器堆、生成控制信号源寄存器号、立即数rtl/ex_stage.vALU、分支比较、重定向选择转发选择器、分支结果rtl/mem_stage.v数据访存mem_read、mem_writertl/wb_stage.v写回寄存器堆reg_write、目的寄存器号rtl/pipe_regs.vIF/ID、ID/EX、EX/MEM、MEM/WB 流水寄存器valid、stall、flushrtl/hazard_unit.vload-use 停顿、分支冲刷、中断接受冲刷冻结与清除的优先级rtl/forward_unit.vEX 级操作数重定向MEM/WB 与 EX/MEM 的优先级rtl/interrupt_ctrl.v多级中断优先级、嵌套深度、返回int_accept、current_priosim/tb_cpu_top.v顶层测试平台复位序列、中断注入prog/inst.mem指令与数据初值定向测试程序docs/timing.md时序与信号约定时钟沿、复位极性这张表不是让你死记文件名而是提醒一个设计原则流水寄存器只负责“保存”和“清除有效位”冒险判断尽量放在组合逻辑里中断控制器不要直接去改 PC。PC 的保存和恢复属于 CPU 现场保护中断控制器只给出“接受哪个中断、当前嵌套多深”。把这两件事混在一个always块里后面加嵌套时很容易出现返回地址错一位的问题。2.2 仿真环境与第一条波形仿真入口我习惯做成一个薄脚本不让测试平台依赖某个固定图形界面。命令行只需要传顶层名和测试名编译、展开、跑波形一次完成。下面这段是工程根目录下常见的 Makefile 调用方式具体仿真器由环境变量SIM指定换机器时不用改测试平台。# 在工程根目录执行SIM 指向本机可用的 Verilog 仿真器 make clean make sim TOPtb_cpu_top SIM$SIM WAVEbuild/wave.vcd # 跑完后先看 build/sim.log再看 wave.vcd 里的 pc_f 和 if_id_validTOP是测试平台顶层名SIM是仿真器命令WAVE是波形输出路径。参数不要写死在脚本里否则换一个环境就要改文件。跑完先看日志里有没有x或z再看波形的前 20 个周期复位释放后pc_f应该从 0 开始按 4 递增if_id_valid在第一个有效指令进入译码级后拉高。如果pc_f一直不动先查时钟和复位极性如果pc_f动了但if_id_valid一直为 0查流水寄存器的复位是不是把有效位永久清零了。执行步骤可以按这个顺序走先跑空程序确认 PC 能走、流水寄存器能复位。加一条add看 WB 阶段是否写回正确寄存器。加load后紧跟add看 hazard 单元是否拉高stall_f和stall_d。加一条beq看分支成立时是否冲刷 IF/ID 和 ID/EX。最后注入中断先单级再两级嵌套观察nested_depth和返回地址。这个顺序的好处是每一类问题都能单独定位。直接上完整测试程序波形里全是信号反而像看黑匣子。2.3 从复位到第一条指令的启动序列启动阶段最容易忽略的是中断使能。很多实现一复位就把global_ie置 1结果测试平台还没加载完指令外部中断就进来了PC 被拉到中断向量波形看起来像“取指错乱”。我一般让复位后先关全局中断等第一条指令提交后再由软件开中断。下面这段顶层启动逻辑是常见写法重点是复位时把所有流水有效位和中断状态一起清干净。// cpu_top.v复位与启动阶段的中断屏蔽 always (posedge clk or posedge reset) begin if (reset) begin pc_f 32h0000_0000; if_id_valid 1b0; id_ex_valid 1b0; ex_mem_valid 1b0; mem_wb_valid 1b0; global_ie 1b0; // 启动阶段先关中断避免取指未完成就进中断 nested_depth 4d0; end else if (int_accept) begin global_ie 1b0; // 进入中断后由中断控制器按优先级决定是否嵌套 end else if (eret) begin global_ie 1b1; end endpc_f是取指级 PC复位值按指令存储器基址设置if_id_valid到mem_wb_valid是各级有效位必须和 PC 一起复位global_ie是全局中断使能启动阶段保持 0nested_depth是中断嵌套深度复位清零。int_accept和eret的优先级要明确中断接受时先关全局使能返回时再打开。注意这里的int_accept最好来自中断控制器的寄存器输出不要用组合逻辑直接驱动否则在时钟沿附近容易产生毛刺。3. 五级流水核心从 IF 到 WB 的数据通路、气泡插入与重定向五级流水本身不复杂难的是级间握手。IF 取指、ID 译码、EX 执行、MEM 访存、WB 写回每一级之间都有一个流水寄存器。寄存器里除了数据还必须带valid有效位。很多人只传指令和数据不传有效位结果气泡被当成一条真实指令执行写使能乱开寄存器堆被改得面目全非。下面按“流水寄存器、冒险检测、气泡插入、重定向、中断接入”五个位置拆代码只保留关键片段方便你对着自己的工程改。3.1 流水寄存器与握手信号先看 IF/ID 寄存器。它要同时支持三种动作正常推进、停顿保持、冲刷清空。正常推进时把取指级的 PC 和指令打进译码级停顿保持时输出不变让译码级继续用旧指令冲刷清空时把valid拉低让后面所有写使能失效。// pipe_if_id.vIF/ID 流水寄存器stall 保持flush 清有效位 module pipe_if_id ( input wire clk, input wire reset, input wire stall, input wire flush, input wire [31:0] pc_f, input wire [31:0] instr_f, output reg [31:0] pc_d, output reg [31:0] instr_d, output reg valid_d ); always (posedge clk or posedge reset) begin if (reset) begin pc_d 32h0000_0000; instr_d 32h0000_0000; valid_d 1b0; end else if (flush) begin pc_d 32h0000_0000; instr_d 32h0000_0000; valid_d 1b0; // 错误路径或中断冲刷清有效位 end else if (!stall) begin pc_d pc_f; instr_d instr_f; valid_d 1b1; // 正常推进 end // stall 为 1 时不赋值保持原值 end endmoduleclk是单时钟reset高有效stall为 1 时寄存器保持flush为 1 时清空。优先级上reset最高flush次之stall最后。valid_d是这条指令是否真实存在的唯一依据后面的 ID/EX、EX/MEM、MEM/WB 都要逐级传递它。instr_d在 flush 时清零只是便于波形观察真正起作用的是valid_d。同样的结构复制到 ID/EX 时还要多传控制信号、源寄存器号、目的寄存器号和立即数复制到 EX/MEM、MEM/WB 时传 ALU 结果、写回地址和访存数据。这里有个血泪经验stall和flush不要共用一个信号。load-use 冒险需要“冻结 PC 和 IF/ID同时清空 ID/EX”如果只拉一个stall而不清 EX气泡进不去如果只拉flush而不冻 PC取指级会把同一条 load 后面的指令重复取进来。正确做法是stall_f、stall_d冻结前两级flush_e清 ID/EX 的有效位。3.2 冒险检测load-use 与分支冲突冒险单元只做组合判断输出冻结和冲刷信号。load-use 的判断条件是EX 级是一条 load且它的目的寄存器等于 ID 级指令的源寄存器。分支冲突的判断条件是EX 级分支成立此时 IF/ID 和 ID/EX 里是错误路径上的年轻指令需要冲刷。中断接受也要冲刷因为中断点之后取进来的指令不能提交。// hazard_unit.vload-use、分支冲刷、中断接受冲刷 module hazard_unit ( input wire [4:0] rs_d, input wire [4:0] rt_d, input wire [4:0] rs_e, input wire [4:0] rt_e, input wire [4:0] rd_e, input wire mem_read_e, input wire branch_taken_e, input wire int_accept, output wire stall_f, output wire stall_d, output wire flush_e, output wire flush_d ); wire load_use; assign load_use mem_read_e ((rd_e rs_d) || (rd_e rt_d)) (rd_e ! 5d0); assign stall_f load_use; assign stall_d load_use; assign flush_e load_use || branch_taken_e || int_accept; assign flush_d branch_taken_e || int_accept; endmodulers_d、rt_d是译码级源寄存器号rs_e、rt_e、rd_e是执行级寄存器号mem_read_e表示 EX 级是 loadbranch_taken_e表示分支成立int_accept来自中断控制器。stall_f和stall_d同时拉高时PC 和 IF/ID 保持ID/EX 被清空相当于在 load 和后面的 use 之间插入一个气泡。flush_d在分支和中断时清掉译码级flush_e多一个 load-use 条件用来把已经进入 EX 的无用指令清掉。rd_e ! 0这个条件不能省。寄存器 0 是常量零load 到寄存器 0 不应该触发停顿。分支冲突的判断放在 EX 级意味着分支成立时已经取了两条年轻指令冲刷范围要覆盖 IF/ID 和 ID/EX如果分支在 ID 级判断冲刷范围可以小一点但组合路径会变长。课程设计里两种都常见关键是把冲刷范围和分支判断级对齐。3.3 插入气泡的实现气泡不是一条nop指令而是一个valid0的流水槽。ID/EX 寄存器在flush_e时要把所有写使能清零reg_write_e、mem_write_e、mem_read_e都置 0ALU 操作码置成无关值。这样即使后面的组合逻辑还在算也不会写寄存器堆或存储器。// pipe_id_ex.v插入气泡时清控制信号而不是只清指令字 module pipe_id_ex ( input wire clk, input wire reset, input wire flush_e, input wire valid_d, input wire [31:0] pc_d, input wire [31:0] rs_val_d, input wire [31:0] rt_val_d, input wire [4:0] rs_d, input wire [4:0] rt_d, input wire [4:0] rd_d, input wire [3:0] alu_op_d, input wire reg_write_d, input wire mem_read_d, input wire mem_write_d, output reg valid_e, output reg [31:0] pc_e, output reg [31:0] rs_val_e, output reg [31:0] rt_val_e, output reg [4:0] rs_e, output reg [4:0] rt_e, output reg [4:0] rd_e, output reg [3:0] alu_op_e, output reg reg_write_e, output reg mem_read_e, output reg mem_write_e ); always (posedge clk or posedge reset) begin if (reset || flush_e) begin valid_e 1b0; pc_e 32h0000_0000; rs_val_e 32h0000_0000; rt_val_e 32h0000_0000; rs_e 5d0; rt_e 5d0; rd_e 5d0; alu_op_e 4d0; reg_write_e 1b0; // 气泡不能写寄存器 mem_read_e 1b0; mem_write_e 1b0; end else begin valid_e valid_d; pc_e pc_d; rs_val_e rs_val_d; rt_val_e rt_val_d; rs_e rs_d; rt_e rt_d; rd_e rd_d; alu_op_e alu_op_d; reg_write_e reg_write_d; mem_read_e mem_read_d; mem_write_e mem_write_d; end end endmoduleflush_e为 1 时valid_e和三个写使能全部清零这是气泡的核心。pc_e、寄存器号清零只是为了波形干净不是功能必需。alu_op_e可以置成加法但valid_e0后后续级不应该再根据它产生副作用。注意 EX/MEM 和 MEM/WB 也要传递valid否则气泡到了访存级可能被当成一次真实 store把数据写到错误地址。实际改代码时我建议把“气泡”和“冲刷”分开命名flush_e用于 load-use 和分支bubble_e用于控制冒险虽然实现上都是清有效位但波形里看名字能少走弯路。3.4 重定向通路与优先级重定向解决的是“结果还没写回寄存器堆后面的指令就要用”的问题。EX 级操作数选择器一般有三路00 用寄存器堆读出的值01 用 MEM/WB 阶段写回的值10 用 EX/MEM 阶段的 ALU 结果。优先级上 EX/MEM 高于 MEM/WB因为 EX/MEM 更新。load 指令在 EX/MEM 阶段数据还没从存储器出来所以 load 的 MEM 阶段不能直接转发给紧邻的 use必须靠 load-use 停顿等一拍。// forward_unit.vEX 级操作数重定向EX/MEM 优先于 MEM/WB module forward_unit ( input wire [4:0] rs_e, input wire [4:0] rt_e, input wire [4:0] rd_m, input wire [4:0] rd_w, input wire reg_write_m, input wire reg_write_w, input wire mem_read_m, output reg [1:0] fwd_a, output reg [1:0] fwd_b ); always (*) begin fwd_a 2b00; fwd_b 2b00; // 第一优先级EX/MEM 的 ALU 结果load 的 MEM 阶段数据未就绪排除 if (reg_write_m !mem_read_m (rd_m ! 5d0) (rd_m rs_e)) fwd_a 2b10; else if (reg_write_w (rd_w ! 5d0) (rd_w rs_e)) fwd_a 2b01; if (reg_write_m !mem_read_m (rd_m ! 5d0) (rd_m rt_e)) fwd_b 2b10; else if (reg_write_w (rd_w ! 5d0) (rd_w rt_e)) fwd_b 2b01; end endmodulefwd_a、fwd_b分别控制 ALU 两个操作数的来源2b00选寄存器堆2b01选 MEM/WB 写回值2b10选 EX/MEM 的 ALU 结果。mem_read_m用来排除 loadload 在 MEM 阶段还没有数据不能把地址当数据转发。rd_m ! 0和rd_w ! 0防止寄存器 0 被误转发。分支比较器也要接同样的转发选择否则beq在 EX 级比较时可能拿到旧值出现“寄存器里明明相等分支却不跳”的玄学问题。还有一个常见误用只转发 ALU 的 A 口不转发 B 口。ADD 指令两个源操作数都可能来自前一条指令只改 A 口会在某些测试程序里过换一个寄存器分配就翻车。最稳的做法是把 EX 级所有读操作数的地方都接上转发器包括 ALU、分支比较和 store 的数据口。3.5 多级嵌套中断的接入点中断接入的第一原则是“在指令边界接受”不能在一条指令执行到一半时把 PC 改掉。常见做法是在 MEM/WB 提交点判断int_accept此时前面的指令已经确定提交后面的年轻指令全部冲刷。中断控制器负责优先级和嵌套深度CPU 负责保存返回 PC 和状态寄存器。下面这个控制器支持 4 个中断源位号越大优先级越高嵌套深度最多 8 层。// interrupt_ctrl.v固定优先级 嵌套深度 返回恢复 module interrupt_ctrl #( parameter N 4, parameter DEPTH 8 )( input wire clk, input wire reset, input wire [N-1:0] irq, // 电平中断位号越大优先级越高 input wire global_ie, input wire int_return, // eret 指令产生 output reg int_accept, output reg [1:0] int_vector, output reg [1:0] current_prio, output reg [3:0] nested_depth ); reg [1:0] prio_stack [0:DEPTH-1]; integer i; reg found; reg [1:0] sel; // 组合逻辑找当前允许的最高优先级中断 always (*) begin found 1b0; sel 2b00; for (i N-1; i 0; i i - 1) begin if (!found irq[i] (i current_prio)) begin found 1b1; sel i[1:0]; end end end always (posedge clk or posedge reset) begin if (reset) begin int_accept 1b0; int_vector 2b00; current_prio 2b00; nested_depth 4d0; for (i 0; i DEPTH; i i 1) prio_stack[i] 2b00; end else if (int_return) begin int_accept 1b0; if (nested_depth ! 0) begin nested_depth nested_depth - 1b1; current_prio prio_stack[nested_depth - 1b1]; end end else if (global_ie found (nested_depth DEPTH)) begin int_accept 1b1; int_vector sel; prio_stack[nested_depth] current_prio; current_prio sel; nested_depth nested_depth 1b1; end else begin int_accept 1b0; end end endmoduleN是中断源数量DEPTH是硬件优先级栈深度irq是电平中断输入global_ie是全局使能int_return由返回指令触发int_accept是寄存器输出避免组合毛刺直接冲刷流水int_vector是当前接受的中断号current_prio是当前正在处理的优先级nested_depth是嵌套层数。优先级栈prio_stack保存被打断的优先级返回时恢复。只有i current_prio的中断才能打断当前处理这样低优先级中断不会把高优先级服务程序打断。CPU 侧还要配一个现场栈。int_accept有效时把提交点之后的 PC 和状态寄存器压栈把global_ie关掉并冲刷 IF/ID、ID/EXeret时弹栈恢复 PC 和状态。返回地址要保存“下一条要执行的指令”不是当前正在执行的指令。比较稳的写法是在 MEM/WB 提交点接受中断保存pc_wb 4同时冲刷所有年轻指令。嵌套时每一层都有独立的 PC 和状态栈项nested_depth只负责优先级恢复不要拿它当 PC 栈指针用。4. 避坑与排查仿真波形里最容易误判的五种现象流水线 CPU 的调试体验很依赖波形组织。信号全加进去一眼看过去全是跳变信号加少了又不知道气泡到底插没插。我的习惯是固定看五组信号pc_f、if_id_valid、id_ex_valid、ex_mem_valid、mem_wb_valid再加stall_f、flush_e、int_accept、nested_depth。下面五条是这类工程里出现频率很高的坑每条按“现象、原因、解决”拆开方便直接对照。4.1 流水与冒险类问题现象一复位后 PC 正常走但寄存器堆一直不更新。原因通常不是 ALU 坏了而是valid没有逐级传下去或者 WB 阶段的reg_write被复位清掉后没有再置起来。有人只在 IF/ID 寄存器里定义validID/EX 和 EX/MEM 忘了传结果气泡和真实指令在后续级看起来一模一样。解决方法是把valid当成和指令字同等重要的数据每一级流水寄存器都显式传递WB 写使能写成valid_w reg_write_w (rd_w ! 0)仿真时先看valid_w再看reg_write_w能快速区分“指令没到”还是“控制信号没开”。现象二load 后紧跟 add波形里插了气泡但结果还是错。原因多半是只清了 ID/EX没有冻结 PC 和 IF/ID。这样气泡确实进 EX 了但取指级还在往前走下一条指令被重复取进来等气泡过去后执行了两次。解决方法是把stall_f和stall_d同时拉高PC 保持、IF/ID 保持flush_e只清 ID/EX。波形上应该看到pc_f停一拍、if_id_valid保持、id_ex_valid为 0 一拍三个动作同时发生才算对。现象三分支指令跳转后返回地址或顺序执行地址错一位。原因是分支在 EX 级判断但冲刷范围只清了 IF/ID没有清 ID/EX或者 PC 更新和冲刷用了同一个时钟沿导致年轻指令已经提交。解决方法是把分支成立时的flush_d和flush_e同时拉高PC 选择分支目标并且确认分支指令后面的两条年轻指令都没有写寄存器。定向测试里可以故意在分支后面放一条写寄存器指令跑完后检查该寄存器是否被错误修改。现象四中断返回后重复执行同一条指令或者跳过一条指令。原因通常是保存的 PC 不是提交点之后的 PC。有人在 IF 级看到int_accept就保存pc_f但此时 IF 级可能已经取了年轻指令保存的地址和实际提交点对不上。解决方法是在 MEM/WB 提交边界接受中断保存pc_wb 4同时冲刷所有年轻指令返回时用eret弹栈恢复 PC不要把nested_depth当 PC 栈指针。嵌套场景下每一层都要有独立的 PC 栈项返回顺序必须后进先出。4.2 中断与综合类问题现象五仿真里单级中断正常两级嵌套时低优先级中断把高优先级打断了。原因是优先级比较只用了irq的位号没有和current_prio比较或者int_accept是组合输出在同一个周期里被连续触发两次。解决方法是让int_accept打一拍比较条件写成i current_prio接受中断时把旧优先级压入prio_stack返回时恢复。下面这段测试平台检查可以直接加在顶层 TB 里专门盯嵌套深度。// tb_cpu_top.v最小中断嵌套检查 initial begin wait (dut.int_accept 1b1); (posedge clk); if (dut.nested_depth ! 4d1) $display(FAIL: first irq depth%0d, dut.nested_depth); wait (dut.int_accept 1b1); (posedge clk); if (dut.nested_depth ! 4d2) $display(FAIL: nested irq depth%0d, dut.nested_depth); wait (dut.eret 1b1); (posedge clk); if (dut.nested_depth ! 4d1) $display(FAIL: eret depth%0d, dut.nested_depth); end这段检查用wait等int_accept再等一个时钟沿看nested_depth是否按 1、2、1 变化dut是顶层实例名eret是返回信号。它不替代完整测试程序但能在早期抓住“嵌套深度没加”或“返回没减”的问题。注意int_accept如果是组合信号wait可能在同一个周期多次命中所以控制器输出最好寄存一拍。现象六仿真通过综合后中断不响应或时序违例。常见原因是int_accept组合逻辑直接驱动flushflush又参与 PC 选择形成组合环路或者复位只复位了 PC没有复位current_prio和nested_depth综合工具把未初始化寄存器优化掉。解决方法是把中断接受打一拍所有时序状态显式复位组合路径里不要出现“自己影响自己”的环路。上板前先跑一遍后综合仿真重点看复位释放后的前 10 个周期和第一次中断接受点这两个位置最容易暴露未初始化问题。5. 二次开发把固定五级流水改成可配置实验平台如果要把这份源码继续改成自己的实验平台我建议从“参数化”和“可观测性”两个方向下手不要一上来就加乱序执行。第一步把中断源数量N、嵌套深度DEPTH、寄存器位宽和指令存储器深度抽成参数顶层只改参数不改端口第二步把valid、stall、flush做成统一命名每一级流水寄存器都输出一份调试信号仿真时直接按级看。下面这张表是我一般会先改的几处改动小验证也直接。改造点改动文件验证信号中断源从 4 扩到 8interrupt_ctrl.v的N和向量位宽irq、int_vector、current_prio嵌套深度可配置DEPTH参数、优先级栈nested_depth、prio_stack增加性能计数器cpu_top.v加周期计数和停顿计数cycle_cnt、stall_cnt分支提前到 ID 级id_stage.v、hazard_unit.vbranch_taken_d、flush_d改中断源数量时int_vector的位宽要跟着N变不要固定写 2 位改嵌套深度时prio_stack的索引宽度要能覆盖DEPTH否则综合工具会报越界。加性能计数器是最划算的改造一个周期计数器加一个停顿计数器跑完整测试程序后看stall_cnt和cycle_cnt的比值就能判断冒险处理是不是过于保守。分支提前到 ID 级能减少冲刷损失但 ID 级要多做一次寄存器比较组合路径会变长改完必须跑后综合仿真。验证方法不要只靠一条长程序。我一般准备三组定向测试覆盖 load-use、分支、单级中断、两级嵌套随机指令测试本文还有配套的精品资源点击获取