新闻详情

新闻详情

首页 / 资讯中心 / 详情

FPGA四路组相联Cache设计:从架构到Verilog实现

发布时间:2026/9/27 1:06:02来源:尧图网络
FPGA四路组相联Cache设计:从架构到Verilog实现
1. 为什么值得动手写一个四路组相联Cache做FPGA或者数字IC设计的同学迟早会撞上Cache这个坎。你可能是为了给软核加一块指令缓存也可能是为了在SoC项目里搭一个像样的存储层次甚至只是想在秋招面试里把“组相联”“LRU”“写回”这几个词讲明白。不管动机是什么四路组相联Cache都是一个绕不过去的经典练手项目——它足够小小到一个人几天就能写完又足够完整完整到把存储层次设计里几乎所有核心问题都塞了进去地址划分、Tag比对、路选择、替换策略、状态机控制、时序收敛。我这次做的这个Cache参数是这样的总容量4KBCache Line大小16字节四路组相联也就是说每组4行每行16字节那么每组容量64字节总共64组。地址位宽32位按字节寻址。写策略采用写回Write-Back加写分配Write-Allocate替换策略用LRU。这些参数不是随便定的后面我会解释为什么这么选。这篇文章我会把整个设计从思路到代码到仿真到踩坑完整地摊开讲一遍。代码是Verilog-2001写的纯RTL不依赖任何厂商IP可以直接拿去综合。如果你正在学Verilog或者正在准备FPGA项目又或者想搞明白Cache到底怎么跑起来的这篇内容应该能帮你省下不少自己摸索的时间。2. 整体架构与参数选型先把账算清楚2.1 Cache容量、行大小与相联度的三角关系Cache设计的第一步不是写代码而是算账。容量、行大小、相联度这三个参数互相牵制任何一个动了另外两个的代价都会变。先看地址划分。32位地址按字节寻址Cache Line是16字节那么块内偏移Offset占4位因为2^416。总共64组组索引Index占6位因为2^664。剩下的就是Tag占32-4-622位。这里有个容易搞混的地方组索引的位数取决于“组数”不是“总行数”。四路组相联总行数是64组×4路256行但索引只需要6位就能定位到组进组之后再靠4路并行比对Tag来选具体哪一行。如果你把索引算成8位按256行算那就变成直接映射了四路的意义就没了。为什么选16字节的行因为行太小Tag开销占比高每次缺失只搬一点点数据访存效率低行太大一次缺失搬进来的数据可能用不上浪费带宽而且LRU的粒度变粗替换不够精细。16字节在FPGA上是个比较舒服的折中——一次突发传输刚好Tag存储的额外开销也能接受。为什么选四路而不是两路或八路两路的冲突缺失还是偏多八路的硬件开销四路已经是4个Tag比较器4路数据选择和LRU维护成本上去了而命中率的提升边际递减。四路是经典配置Intel和AMD的L1 Cache很多年都是四路或八路教学和项目里四路最合适。2.2 写回写分配为什么不用写直达写策略有两种主流选择写直达Write-Through和写回Write-Back。写直达每次写操作都同时写Cache和下一级存储实现简单但写带宽消耗大。写回只在Cache行被替换时才写回下一级写操作先改Cache标记脏位Dirty Bit等这行要被踢出去的时候再写回。我选写回原因是FPGA项目里下一级往往是DDR或者片外SRAM写带宽比读带宽更宝贵写直达会把大量写操作直接打到慢速存储上性能损失明显。写回的代价是需要维护脏位替换逻辑复杂一点但这个复杂度可控。写分配Write-Allocate是说写缺失时先把数据块从下一级读进来再在Cache里修改。对应的还有非写分配No-Write-Allocate写缺失时直接写到下一级不占Cache。写回策略通常配写分配因为既然你都要写回了不如把数据拉进来改后续如果还有对同一块的写操作就能命中。写直达通常配非写分配避免写操作污染Cache。2.3 状态机的段数选择三段式还是两段式Cache控制器本质上是一个状态机要处理空闲、Tag比对、命中读、命中写、缺失读、缺失写回、填充等状态。状态机写法有一段式、两段式、三段式。一段式把所有逻辑塞一个always块容易写但时序差、难维护。两段式把状态转移和输出分开比一段式好但输出仍有组合逻辑毛刺风险。三段式是状态转移、次态逻辑、输出逻辑各一个always块输出用寄存器打一拍时序最干净。这个项目我选三段式。原因很直接Cache控制器要跟下一级存储握手输出信号如读使能、写使能、地址如果有毛刺可能被下一级误采。三段式输出寄存后信号干净握手可靠。代价是输出晚一拍但Cache本来就不是单周期能完成的多一拍完全能接受。3. 核心模块拆解与关键信号定义3.1 顶层接口与参数化设计顶层模块我命名为cache_4way接口分三部分CPU侧、存储侧、配置侧。CPU侧包括时钟、复位、请求有效、读/写指示、地址、写数据、读数据、完成信号。存储侧包括向下一级发起的读/写请求、地址、写数据、读回数据、应答。配置侧暂时没加参数用parameter写死方便综合时改。module cache_4way #( parameter ADDR_WIDTH 32, parameter DATA_WIDTH 32, parameter LINE_SIZE 16, // bytes parameter NUM_WAYS 4, parameter NUM_SETS 64 )( input wire clk, input wire rst_n, // CPU interface input wire cpu_req, input wire cpu_we, input wire [ADDR_WIDTH-1:0] cpu_addr, input wire [DATA_WIDTH-1:0] cpu_wdata, output reg [DATA_WIDTH-1:0] cpu_rdata, output reg cpu_ready, // Memory interface output reg mem_req, output reg mem_we, output reg [ADDR_WIDTH-1:0] mem_addr, output reg [LINE_SIZE*8-1:0] mem_wdata, input wire [LINE_SIZE*8-1:0] mem_rdata, input wire mem_ready );参数化设计的好处是你想改成8路或者128组只改parameter就行不用动逻辑。但要注意NUM_SETS必须是2的幂否则索引计算要用除法综合出来面积爆炸。LINE_SIZE也建议是2的幂偏移计算才简单。3.2 地址字段拆分与Tag/Index/Offset计算地址拆分用位选就行不需要除法器。定义几个localparamlocalparam OFFSET_BITS $clog2(LINE_SIZE); // 4 localparam INDEX_BITS $clog2(NUM_SETS); // 6 localparam TAG_BITS ADDR_WIDTH - OFFSET_BITS - INDEX_BITS; // 22 localparam WAY_BITS $clog2(NUM_WAYS); // 2然后从cpu_addr里切wire [OFFSET_BITS-1:0] offset cpu_addr[OFFSET_BITS-1:0]; wire [INDEX_BITS-1:0] index cpu_addr[OFFSET_BITSINDEX_BITS-1:OFFSET_BITS]; wire [TAG_BITS-1:0] tag cpu_addr[ADDR_WIDTH-1:OFFSET_BITSINDEX_BITS];这里有个细节$clog2是Verilog-2005才有的系统函数如果你用的是Verilog-2001得自己写个function或者直接写死数字。我为了兼容性实际代码里用的是直接写死的localparam注释里标明含义。3.3 存储阵列Data Array、Tag Array、Valid/Dirty/LRUCache的存储体分四块数据阵列、Tag阵列、有效位、脏位外加LRU位。数据阵列大小是NUM_SETS × NUM_WAYS × LINE_SIZE字节也就是64×4×164096字节4KB。Tag阵列是NUM_SETS × NUM_WAYS × TAG_BITS位64×4×225632位。有效位和脏位各是NUM_SETS × NUM_WAYS位各256位。LRU每个组需要记录4路的顺序用4×2位或者更紧凑的编码。在Verilog里这些阵列用二维寄存器数组实现reg [LINE_SIZE*8-1:0] data_array [0:NUM_SETS*NUM_WAYS-1]; reg [TAG_BITS-1:0] tag_array [0:NUM_SETS*NUM_WAYS-1]; reg valid_array[0:NUM_SETS*NUM_WAYS-1]; reg dirty_array[0:NUM_SETS*NUM_WAYS-1]; reg [1:0] lru_array [0:NUM_SETS-1][0:NUM_WAYS-1];注意数据阵列的位宽是LINE_SIZE*8因为LINE_SIZE是字节数要乘8转成位。索引方式用index*NUM_WAYS way这样把二维压成一维综合工具更容易推断出Block RAM。如果你写成data_array[index][way]某些工具会综合成分布式RAM面积和时序都差一些。提示FPGA上的Block RAM通常有固定的位宽和深度配置4KB的数据阵列用36Kb的BRAM刚好能放下但要注意读写端口数量。如果BRAM只有一个写端口而Cache需要同时写Tag和Data就得用两个BRAM或者用分布式RAM补。4. 四路并行Tag比对与命中逻辑4.1 并行比对的结构与时序四路组相联的核心操作是拿到Index后同时读出四路的Tag跟请求地址的Tag比对同时检查Valid位。四路都命中就是命中都不命中就是缺失多路命中是异常正常不会发生但硬件上要处理。wire [NUM_WAYS-1:0] way_hit; genvar i; generate for (i 0; i NUM_WAYS; i i 1) begin : gen_hit assign way_hit[i] valid_array[index*NUM_WAYSi] (tag_array[index*NUM_WAYSi] tag); end endgenerate wire cache_hit |way_hit;这个比对是组合逻辑从Index有效到way_hit有效延迟取决于Tag阵列的读出延迟加上比较器延迟。在FPGA上如果Tag阵列用分布式RAM读出延迟小如果用BRAM读出有一拍延迟那命中判断就得往后推一拍。我的设计里Tag阵列用寄存器数组综合成分布式RAM读出是组合的所以命中判断可以在同一拍完成。4.2 命中路选择与数据读出命中之后要从四路里选出命中的那一路的数据。用way_hit做one-hot选择reg [LINE_SIZE*8-1:0] hit_line_data; integer j; always (*) begin hit_line_data {LINE_SIZE*8{1b0}}; for (j 0; j NUM_WAYS; j j 1) begin if (way_hit[j]) hit_line_data data_array[index*NUM_WAYSj]; end end然后根据offset从这16字节里选出CPU要的4字节wire [DATA_WIDTH-1:0] hit_word; assign hit_word hit_line_data[offset*8 : DATA_WIDTH];:是Verilog-2001的位选语法offset*8是起始位DATA_WIDTH是宽度。这里offset是4位最大1515812012032152但hit_line_data只有128位所以offset最大只能是12128969632128。实际上16字节的行按4字节对齐访问offset只能是0、4、8、12低两位忽略。CPU发来的地址如果没对齐硬件上要么报异常要么忽略低两位我的设计里假设CPU总是对齐访问。4.3 多路命中的异常处理理论上不会多路命中因为同一时刻同一个Index下不同路的Tag不可能相同除非有bug。但硬件设计要防御性编程。如果way_hit有多位为1说明Tag阵列或Valid位出了问题这时候可以报一个错误信号或者强制选最低路。我的做法是加一个multi_hit信号仿真时如果拉高就报warning综合时综合成一根线不占资源。wire multi_hit (way_hit (way_hit - 1)) ! 0;way_hit (way_hit-1)是经典的判断“是否有多位为1”的技巧如果结果非零说明至少两位为1。5. LRU替换策略的硬件实现5.1 LRU的矩阵法与计数器法对比LRULeast Recently Used的硬件实现有几种思路。一种是矩阵法每组维护一个4×4的矩阵访问某路时把该路对应的行全置1、列全置0替换时找行全0的那一路。另一种是计数器法每路维护一个计数器访问时该路计数器清零其他路加1替换时选计数器最大的那路。矩阵法用的位多每组16位但逻辑简单更新和查找都是组合逻辑。计数器法用的位少每组4×28位但更新时要做加法而且计数器会溢出需要处理。我选矩阵法因为四路规模下16位完全能接受而且逻辑清晰不容易出bug。矩阵法的具体做法每组维护一个4×4的寄存器矩阵lru_matrix[set][i][j]i和j都是0到3。当访问第k路时把lru_matrix[set][k][*]全置1把lru_matrix[set][*][k]全置0。替换时找lru_matrix[set][i][*]全0的那一路i就是最久未使用的。5.2 LRU更新逻辑与替换路选择更新逻辑integer m, n; always (posedge clk or negedge rst_n) begin if (!rst_n) begin for (m 0; m NUM_SETS; m m 1) for (n 0; n NUM_WAYS; n n 1) lru_matrix[m][n] 4b0; end else if (cache_hit cpu_req cpu_ready) begin for (n 0; n NUM_WAYS; n n 1) begin if (way_hit[n]) begin lru_matrix[index][n] 4b1111; end else begin lru_matrix[index][n][n] 1b0; end end end end这段逻辑的意思是命中的那一路矩阵行全置1其他路把矩阵中对应自己列的那一位清0。这样经过多次访问后最久未使用的那一路它的行会全是0。替换路选择reg [WAY_BITS-1:0] replace_way; integer p; always (*) begin replace_way 0; for (p 0; p NUM_WAYS; p p 1) begin if (lru_matrix[index][p] 4b0000) replace_way p[WAY_BITS-1:0]; end end如果有多路都是全0初始状态选最低路。实际运行中不会出现多路全0因为每次访问都会更新矩阵。5.3 LRU初始状态与冷启动问题复位后所有矩阵位都是0意味着所有路都是“最久未使用”。第一次缺失时replace_way会选0路。这没问题但要注意如果0路被填了之后矩阵更新了下次缺失会选1路以此类推。冷启动阶段Cache命中率低是正常的跑一段时间后LRU矩阵就会收敛到正确的顺序。有个坑如果Cache一直命中同一路其他路的矩阵行会一直是0但被命中路的矩阵行是1列是0。替换时找全0行会找到那些一直没被访问的路这是正确的LRU行为。6. 三段式状态机与缺失处理流程6.1 状态定义与转移条件状态机我定义了这几个状态IDLE等待CPU请求TAG_CHECKTag比对判断命中还是缺失HIT_READ命中读把数据返回CPUHIT_WRITE命中写修改Cache行置脏位MISS_READ缺失读向下一级发起读请求MISS_WRITEBACK缺失且被替换行是脏的先写回MISS_FILL把从下一级读回的数据填入CacheMISS_WRITE_AFTER_FILL写缺失时填充完再修改状态转移用三段式的第一段reg [3:0] cur_state, nxt_state; always (posedge clk or negedge rst_n) begin if (!rst_n) cur_state IDLE; else cur_state nxt_state; end第二段是次态逻辑always (*) begin nxt_state cur_state; case (cur_state) IDLE: if (cpu_req) nxt_state TAG_CHECK; TAG_CHECK: begin if (cache_hit) nxt_state cpu_we ? HIT_WRITE : HIT_READ; else nxt_state dirty_array[index*NUM_WAYSreplace_way] ? MISS_WRITEBACK : MISS_READ; end HIT_READ: nxt_state IDLE; HIT_WRITE: nxt_state IDLE; MISS_WRITEBACK: if (mem_ready) nxt_state MISS_READ; MISS_READ: if (mem_ready) nxt_state MISS_FILL; MISS_FILL: nxt_state cpu_we ? MISS_WRITE_AFTER_FILL : IDLE; MISS_WRITE_AFTER_FILL: nxt_state IDLE; default: nxt_state IDLE; endcase end第三段是输出逻辑用寄存器打一拍always (posedge clk or negedge rst_n) begin if (!rst_n) begin cpu_ready 1b0; mem_req 1b0; // ... 其他输出复位 end else begin cpu_ready 1b0; mem_req 1b0; case (nxt_state) HIT_READ: begin cpu_rdata hit_word; cpu_ready 1b1; end HIT_WRITE: begin cpu_ready 1b1; end MISS_WRITEBACK: begin mem_req 1b1; mem_we 1b1; mem_addr {tag_array[index*NUM_WAYSreplace_way], index, {OFFSET_BITS{1b0}}}; mem_wdata data_array[index*NUM_WAYSreplace_way]; end MISS_READ: begin mem_req 1b1; mem_we 1b0; mem_addr {tag, index, {OFFSET_BITS{1b0}}}; end MISS_FILL: begin data_array[index*NUM_WAYSreplace_way] mem_rdata; tag_array[index*NUM_WAYSreplace_way] tag; valid_array[index*NUM_WAYSreplace_way] 1b1; dirty_array[index*NUM_WAYSreplace_way] 1b0; end MISS_WRITE_AFTER_FILL: begin data_array[index*NUM_WAYSreplace_way][offset*8 : DATA_WIDTH] cpu_wdata; dirty_array[index*NUM_WAYSreplace_way] 1b1; cpu_ready 1b1; end endcase end end注意输出逻辑用的是nxt_state而不是cur_state这样输出和状态转移在同一拍对齐不会晚一拍。这是三段式的一个细节第二段算次态第三段根据次态产生输出这样输出在下一拍生效跟状态转移同步。6.2 缺失读与写回的顺序控制缺失时如果被替换行是脏的必须先写回再读新数据。因为写回和读新数据用的是同一个存储接口不能同时发。状态机里MISS_WRITEBACK等mem_ready后再进MISS_READ就是串行化这两个操作。写回的地址是{tag_array[...], index, 0}因为写回整行偏移为0。写回的数据是data_array[...]整行16字节。读新数据的地址是{tag, index, 0}也是整行读。这里有个优化点如果下一级存储支持读写同时进行双端口写回和读新数据可以并行能省不少时间。但为了通用性我按串行设计接口简单任何存储都能接。6.3 写命中与写缺失的处理差异写命中时直接修改Cache行对应位置置脏位返回cpu_ready。注意写命中不需要读下一级也不需要整行写只改4字节。写缺失时先按缺失流程把整行读进来填到Cache里然后再修改对应4字节置脏位。这就是写分配。如果不想写分配写缺失时直接写下一级不填Cache但那样后续对同一块的写还是会缺失性能差。写缺失的另一个细节如果被替换行是脏的要先写回。所以写缺失的完整流程是写回脏行如果需要→ 读新行 → 填Cache → 修改4字节 → 置脏位 → 返回ready。状态机里MISS_WRITEBACK→MISS_READ→MISS_FILL→MISS_WRITE_AFTER_FILL就是这条路径。7. 仿真验证与波形分析7.1 Testbench结构与激励设计Testbench我写了一个简单的CPU行为模型依次发起读请求和写请求覆盖命中、缺失、写回等场景。激励序列是这样的读地址0x0000_0000缺失从存储读入读地址0x0000_0004命中同一行读地址0x0000_0010缺失不同行同组0x10的index是1不同组写地址0x0000_0000命中写读地址0x0000_0040缺失可能触发写回连续读同一组的不同Tag触发LRU替换存储模型用一个简单的行为RAM读延迟2拍写延迟1拍。mem_ready在操作完成后拉高一拍。// 存储模型 reg [127:0] mem_model [0:65535]; reg [31:0] mem_addr_r; reg mem_ready_r; always (posedge clk) begin if (mem_req !mem_we) begin mem_addr_r mem_addr; mem_ready_r 1b0; // 延迟2拍 (posedge clk); (posedge clk); mem_rdata mem_model[mem_addr_r[31:4]]; mem_ready_r 1b1; end else if (mem_req mem_we) begin mem_model[mem_addr[31:4]] mem_wdata; (posedge clk); mem_ready_r 1b1; end else begin mem_ready_r 1b0; end end assign mem_ready mem_ready_r;这个存储模型用了(posedge clk)在always块里做延迟仿真可以但不可综合。Testbench里用没问题。7.2 关键波形解读命中、缺失、写回跑仿真后重点看几个波形段第一段读0x0000_0000。cpu_req拉高cpu_we为0状态从IDLE到TAG_CHECK。cache_hit为0dirty_array为0进MISS_READ。mem_req拉高mem_we为0mem_addr为0x0000_0000。等mem_ready后进MISS_FILL把mem_rdata写入data_arrayvalid_array置1。然后回IDLEcpu_ready拉高cpu_rdata输出。第二段读0x0000_0004。cache_hit为1way_hit某一位为1进HIT_READcpu_rdata输出对应字cpu_ready拉高。LRU矩阵更新命中路行置1其他路对应列清0。第三段写0x0000_0000。cpu_we为1cache_hit为1进HIT_WRITE修改data_array对应4字节dirty_array置1cpu_ready拉高。第四段读0x0000_0040。这个地址的index是40x4044跟前面的index0不同组所以不冲突。但如果读0x0000_0100index16又不同组。要触发同组冲突得读index相同但Tag不同的地址比如0x0000_0000和0x0000_1000index都是0Tag不同。连续读多个同组不同Tag的地址就能看到LRU替换和写回。7.3 覆盖率与边界情况检查仿真要覆盖的边界情况复位后第一次访问连续命中同一行连续缺失同一组触发LRU写命中后读同一行验证数据一致性写缺失后读同一行验证写分配脏行被替换时的写回多路命中的异常人为注入我写了一个简单的覆盖率收集用$display打印每次访问的命中/缺失/写回情况跑完看统计。命中率在随机激励下大概70%到80%符合四路组相联的预期。8. 常见问题与排查技巧实录8.1 时序不收敛Tag比对路径太长问题综合后时序报告显示Tag比对路径是关键路径建立时间违例。排查Tag阵列用寄存器数组读出是组合逻辑四路并行比对每路22位比较器加起来延迟不小。如果时钟频率高比如100MHz以上这条路径可能撑不住。解决把Tag阵列改成BRAM读出打一拍命中判断往后推一拍。代价是状态机多一个状态或者多等一拍但时序能收敛。或者把Tag比对流水化第一拍读Tag第二拍比对第三拍出结果。Cache本来就不是单周期多一两拍对性能影响不大。8.2 数据阵列综合成分布式RAM导致面积爆炸问题综合报告显示数据阵列用了大量LUT而不是BRAM。排查Verilog里reg [127:0] data_array [0:255]这种写法综合工具可能推断成分布式RAM因为BRAM的位宽通常不超过72位36Kb BRAM是36位宽×1024深或者72位宽×512深。128位宽超过了单个BRAM的位宽工具可能用多个BRAM拼也可能直接放弃用分布式RAM。解决把128位的数据拆成4个32位用4个BRAM并行存储或者用(* ram_style block *)属性强制推断BRAM。Xilinx的工具支持这个属性Intel的工具用(* ramstyle M9K *)。具体语法查工具手册。8.3 LRU矩阵更新逻辑写错导致替换异常问题仿真发现替换的路总是0路LRU没起作用。排查检查LRU更新逻辑发现lru_matrix[index][n][n] 1b0;写成了lru_matrix[index][n] 4b0;把整行清0了导致矩阵永远全0替换时总选0路。解决更新逻辑要精确到矩阵的单个位。命中路行全置1其他路把自己列的那一位清0。用for循环时注意索引lru_matrix[index][n][n]里的两个n含义不同第一个是行第二个是列。8.4 写回时地址或数据错误问题写回后下一级存储里的数据不对。排查写回地址是{tag_array[...], index, 0}但tag_array读出的是被替换行的Tag不是当前请求的Tag。如果写回时用了当前请求的Tag就会写到错误的地址。解决写回时用被替换行的Tag即tag_array[index*NUM_WAYSreplace_way]。这个Tag在TAG_CHECK状态就已经读出来了存到一个寄存器里写回时直接用。8.5 常见问题速查表问题现象可能原因排查方法解决方案时序违例Tag比对路径长看时序报告关键路径Tag阵列改BRAM或流水化面积爆炸数据阵列综合成分布式RAM看综合报告RAM推断加ram_style属性或拆位宽替换总选0路LRU矩阵更新错误看波形lru_matrix修正更新逻辑精确到单个位写回数据错写回地址用了当前Tag看波形mem_addr用被替换行的Tag命中率低相联度或行大小不合适统计命中率调整参数或检查LRU多路命中Tag阵列或Valid位bug看波形way_hit检查写入逻辑加防御提示仿真时如果发现cpu_ready一直不拉高先看状态机是不是卡在某个状态。常见的是mem_ready一直不来检查存储模型的握手逻辑。另一个常见问题是复位没做好状态机没回到IDLE。9. 性能优化与扩展方向9.1 流水化访问与命中率提升当前设计是阻塞式的一次访问完成才接受下一次。如果CPU能发多个未完成请求可以做成流水线第一拍Tag比对第二拍数据读出第三拍返回。这样吞吐率能翻倍。代价是控制逻辑复杂要处理流水线冲突和异常。命中率方面四路组相联的冲突缺失已经比较少了如果还想提升可以加Victim Cache牺牲缓存把被替换的行先放到一个小全相联Cache里下次如果访问到就能命中避免直接去下一级。或者用伪LRUPseudo-LRU替代严格LRU硬件开销小命中率差不多。9.2 参数化扩展八路、更大容量改成八路只需要把NUM_WAYS改成8LRU矩阵变成8×864位每组替换逻辑还是找全0行。但八路的Tag比对和路选择逻辑更复杂时序压力更大。容量扩展的话增加NUM_SETS或者LINE_SIZE都行但要注意地址位宽和索引位数的对应关系。9.3 与AXI总线对接的改造要点实际SoC里Cache通常挂在AXI总线上要把存储接口改成AXI Master。主要改动mem_req变成AXI的arvalid/awvalidmem_ready变成rvalid/bvalid地址和数据要按AXI通道拆分。突发传输可以用上一次读整行16字节AXI的burst长度设成44×4字节16字节。握手逻辑比简单valid/ready复杂但AXI协议有现成的模板可以套。我在实际项目里把Cache接到AXI上时踩过的坑是AXI的读数据和写响应是分开的通道状态机要同时等两个通道不能只等一个。另外AXI的地址对齐要求跟Cache的行对齐要匹配否则burst传输会出错。10. 一些个人体会这个Cache我前前后后改了三四版第一版LRU写错了替换总选0路命中率惨不忍睹。第二版时序不收敛降频到50MHz才能跑。第三版把Tag阵列改成BRAM时序过了但多了一拍延迟状态机得跟着改。第四版才稳定下来。最大的体会是Cache设计里参数选型和微架构是绑死的。你选了四路LRU矩阵就是16位选了八路就是64位。你选了16字节行offset就是4位选了32字节行offset就是5位数据阵列位宽翻倍。这些数字不是孤立的改一个牵一串。所以动手之前先把账算清楚比写代码重要得多。另一个体会是仿真覆盖率。我一开始只测了命中和缺失没测写回结果上板后发现脏行替换时数据丢了。后来补了写回的测试用例才发现写回地址用错了Tag。仿真多花一小时上板少调一天这笔账怎么算都划算。代码我放在这里了参数化写得比较灵活你想改成两路或者八路改NUM_WAYS就行。但改完记得重新算LRU矩阵的位宽和替换逻辑别直接跑。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

RTX 3080 20G 的 CUDA 兼容性 / 新驱动还能不能正常跑本地模型? 2026/9/27 5:52:40

RTX 3080 20G 的 CUDA 兼容性 / 新驱动还能不能正常跑本地模型?

RTX 3080 20G 属于改显存版本,判断它能不能跑本地模型,关键不在显存大小,而在两件事:驱动是否认得出这张卡,以及驱动暴露的 CUDA 支持上限是否不低于框架编译时用的版本。多数情况下,只要 nvidia-smi 能稳定…

阅读更多 →
网络编程:UDP协议 2026/9/27 5:52:15

网络编程:UDP协议

一、是什么 UDP(User Datagram Protocol,用户数据报协议)是一种简单的、无连接的传输层协议,用于在网络中传输数据。 与 TCP 不同,UDP 不提供可靠性、顺序性和流量控制,但它具有低延迟和高效的特点&#xf…

阅读更多 →
做网站都需要买什么问题避坑指南与性能优化实战 2026/9/27 5:52:15

做网站都需要买什么问题避坑指南与性能优化实战

做网站都需要买什么问题避坑指南与性能优化实战 找建站公司最怕什么?怕被忽悠多花冤枉钱,更怕花了钱做出来的网站打开像蜗牛,客户还没看内容就关了页面。很多老板问“做网站都需要买什么问题”,其实核心就是两件事:一是别买没用的服务,二是买对能保性能…

阅读更多 →
C语言学习之始 2026/9/27 5:51:50

C语言学习之始

1.自我介绍2.编程目标3.学习方法4.学习期限5.想进入的IT公司1.自我介绍我是一名通信工程专业的普通学生,之前发布过一个有关数学建模的文章,感兴趣的可以去看看了解一下。目前才刚刚接触c语言,我希望能够在学习c语言的同时利用博客来记录和分…

阅读更多 →
5.5 教学辅助 2026/9/27 5:51:43

5.5 教学辅助

教师的工作时间很大一部分消耗在非教学本身的事务上,例如备课、出题、写评语、准备家长会发言等。这些内容有模式可循,但每次都需要从头来过,消耗大量时间和精力。大模型可以帮教师快速完成这些有规律的文字工作,把更多时间留给真…

阅读更多 →
Smith圆图实战:2.4GHz天线L型匹配四步法 2026/9/27 5:51:37

Smith圆图实战:2.4GHz天线L型匹配四步法

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉