ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

芯片RTL源码阅读:从硬件描述语言到电路洞察的工程实践

芯片RTL源码阅读:从硬件描述语言到电路洞察的工程实践 1. 项目概述从“黑盒”到“白盒”的芯片设计探索作为一名在芯片设计验证领域摸爬滚打了十多年的工程师我经常被问到“你们是怎么看懂别人设计的芯片代码的” 这背后指的就是阅读芯片的RTL源码。RTL全称寄存器传输级是芯片设计从抽象架构到物理实现的关键桥梁它用硬件描述语言如Verilog或VHDL精确描述了芯片内部寄存器之间的数据流动和逻辑运算。阅读RTL源码远不止是“看代码”那么简单它更像是一场深入芯片灵魂的“考古”与“解构”之旅。对于设计者这是自查和迭代的基础对于验证者这是理解设计意图、构建测试场景的前提对于IP集成者或后端工程师这是确保模块正确连接和时序收敛的必修课。无论你是刚入行的数字IC新人还是希望深入理解硬件工作原理的软件工程师掌握高效阅读RTL源码的方法都能让你从“黑盒”使用者转变为“白盒”洞察者真正理解手中芯片或FPGA的每一次心跳。2. 核心价值与适用场景解析2.1 为什么要阅读RTL源码在芯片设计流程中RTL代码是唯一一份无歧义的、可综合的硬件行为描述文档。阅读它的价值体现在多个层面对于设计工程师阅读自己或他人的RTL代码是进行设计复审、优化和Debug的核心手段。通过阅读可以检查逻辑实现的正确性、发现潜在的电路结构问题如锁存器 unintentional latch、评估面积和时序关键路径。很多时候代码审查中发现的一个多余寄存器或一个不完整的条件判断就能在流片前避免一个灾难性的功能错误。对于验证工程师深入理解RTL是编写高效、高覆盖率测试激励的基石。验证不是盲目的随机测试而是基于对设计规格和实现方式的理解进行有针对性的攻击。只有读懂了状态机的状态转移条件、数据通路的处理流程、仲裁器的优先级策略你才能构造出触发边界条件和 corner case 的场景从而搭建起可靠的质量防线。对于FPGA开发者或系统集成工程师在使用第三方IP核或继承遗留代码时阅读RTL是进行集成、定制和问题定位的必备技能。你可能需要根据接口时序修改FIFO深度或者因为资源限制而裁剪某些非核心功能这些操作都建立在对源码结构的清晰认知之上。对于学生和研究者阅读经典IP如开源RISC-V处理器、AES加密模块的RTL代码是最直接的学习先进设计思想和编码风格的方式。这比阅读任何教科书都来得生动和深刻。2.2 主要挑战与常见误区阅读RTL源码的挑战是显而易见的。首先它描述的是并行执行的硬件电路这与我们熟悉的串行软件思维有本质不同。一个always块可能描述一个组合逻辑或一个时序逻辑多个always块在仿真中是并发执行的。其次代码规模庞大一个中等复杂度的模块动辄数千行顶层模块更是由数十个子模块实例化而成容易让人迷失在层次结构中。再者代码中可能充斥着为了综合优化而写的、可读性较差的“技巧性”代码或者缺乏足够的注释。常见的误区包括像读软件一样逐行阅读试图从第一行读到最后一行的顺序理解往往会失败因为硬件描述没有唯一的“执行起点”。忽视综合报告与电路视图仅停留在文本层面没有将代码与综合后生成的电路原理图Schematic或网表Netlist进行对照导致理解浮于表面。不注重接口与数据流一开始就扎进复杂的内部状态机里而没有先理清模块的输入输出端口、时钟复位域以及主要的数据通路导致只见树木不见森林。3. 高效阅读RTL源码的方法论与工具链3.1 自上而下与自下而上相结合的策略高效的RTL阅读需要一套系统的方法。我通常采用“自上而下把握架构自下而上深入细节中间突破关键路径”的混合策略。自上而下Top-Down从顶层模块Top Module开始。首先仔细阅读模块的输入输出端口定义理解每个信号的位宽、方向input/output/inout和功能描述。这就像是查看一个黑盒子的对外引脚说明书。接着查看顶层的模块实例化module instantiation部分理清子模块之间的互连关系。用纸笔或绘图工具画出一个简单的模块框图标明数据流向和控制信号。这一步的目标是建立系统的整体框架知道“谁”和“谁”连接数据“从哪里来到哪里去”。自下而上Bottom-Up识别出系统中的基本功能单元或关键IP例如一个特定的算法处理单元如CRC校验模块、一个复杂的有限状态机FSM或者一个存储器控制器。找到这些模块的源码进行独立、深入的分析。理解其内部独立的工作机制后再将其放回顶层框架中看它如何与上下游模块交互。这种方法对于理解复杂系统中的核心算法实现特别有效。中间突破很多时候我们阅读代码是为了解决特定问题比如某个场景下功能异常或者需要评估某个性能瓶颈。这时可以直接从问题相关的信号或模块切入通过仿真波形Waveform回溯信号来源追踪相关逻辑的代码实现。这是一种目标导向的阅读方式效率最高。3.2 必备工具链及其使用技巧工欲善其事必先利其器。阅读RTL离不开强大的工具支持。代码编辑器与查看器Vim/Emacs with Plugins对于资深工程师配置了Verilog/SystemVerilog语法高亮、代码折叠、标签跳转ctags的Vim或Emacs仍然是高效利器。grep、find等命令行工具能快速全局搜索信号和模块名。VS Code对于更大众化的选择VS Code搭配诸如“Verilog-HDL/SystemVerilog”等插件能提供优秀的语法高亮、代码片段、符号跳转和简单的 linting 功能界面友好易于上手。专用HDL IDE如 Siemens 的 VerdiDebut、Synopsys 的 VC SpyGlass 等它们虽然庞大但提供了代码导航、原理图生成、波形交叉探测等无缝集成的强大功能是深度调试的终极武器。仿真与调试工具波形查看器Verdi, DVE, GTKWave这是将静态代码与动态行为连接起来的桥梁。在仿真中捕获信号波形然后在波形窗口中双击任意信号可以直接跳转到定义或驱动该信号的RTL代码行。反之在代码中点击信号可以查看其在波形中的变化。这种“代码-波形”交叉探测Cross-Probe能力是理解复杂时序行为的神器。仿真器VCS, Xcelium, QuestaSim除了运行测试其自带的调试功能如设置条件断点、单步执行对于初始化阶段很有用、强制信号值等能帮助动态分析代码执行路径。综合与可视化工具逻辑综合工具Design Compiler, Genus阅读综合后的报告特别是时序报告Timing Report和面积报告Area Report可以反推RTL代码中哪些部分是关键路径Critical Path哪些逻辑被优化掉了。这有助于理解代码的实际硬件成本。原理图生成器大多数综合工具和IDE都能从RTL生成门级或RTL级的原理图。对于复杂的组合逻辑或状态机看原理图有时比读代码更直观。它能清晰地展示出寄存器、多路选择器、加法器等基本逻辑单元的连接关系。实操心得不要只依赖一种工具。我习惯用VS Code进行日常的代码浏览和编辑因为它轻快且搜索功能强大。当需要进行深度调试时则切换到Verdi利用其强大的交叉探测和原理图功能。同时养成随时手绘框图的习惯哪怕是简单的草图也能极大地帮助理清思路固化当前的理解。4. 核心代码结构深度解析与阅读要点4.1 接口定义与模块声明阅读任何一个RTL模块起点一定是它的接口。在Verilog中这体现在module声明和input/output/inout端口列表中。module dma_engine #( parameter DATA_WIDTH 64, parameter ADDR_WIDTH 32 )( input wire clk, input wire rst_n, // 低电平有效复位 // 配置接口 input wire [ADDR_WIDTH-1:0] cfg_start_addr, input wire [31:0] cfg_transfer_len, input wire cfg_start, // 存储器接口 output reg [ADDR_WIDTH-1:0] mem_addr, output reg mem_wr_en, output reg [DATA_WIDTH-1:0] mem_wr_data, input wire [DATA_WIDTH-1:0] mem_rd_data, // 状态与中断 output reg transfer_done, output reg error );阅读要点参数Parameter立即关注模块的可配置参数如DATA_WIDTH和ADDR_WIDTH。它们定义了模块的数据位宽和地址位宽直接影响内部逻辑和接口信号的定义。时钟与复位找到clk和rst_n。这是所有同步逻辑的基石。注意复位的有效电平这里是低有效rst_n这决定了后续 always 块中复位条件的写法。信号方向与位宽仔细查看每个信号是输入还是输出其位宽是多少。例如cfg_start_addr是输入位宽由ADDR_WIDTH决定mem_wr_data是输出位宽由DATA_WIDTH决定。信号命名与分组良好的代码会将相关信号分组注释。如上例中的“配置接口”、“存储器接口”、“状态与中断”。这暗示了模块的功能分区是后续阅读的重要线索。4.2 时序逻辑与状态机解析时序逻辑是RTL的核心通常由always (posedge clk or negedge rst_n)这样的过程块描述。其中有限状态机FSM是控制逻辑的典型实现。// 状态定义 localparam S_IDLE 3d0; localparam S_CFG 3d1; localparam S_READ 3d2; localparam S_WRITE 3d3; localparam S_ERROR 3d4; reg [2:0] current_state, next_state; // 状态寄存器更新时序逻辑 always (posedge clk or negedge rst_n) begin if (!rst_n) begin current_state S_IDLE; end else begin current_state next_state; end end // 下一状态逻辑与输出逻辑组合逻辑 always (*) begin // 默认赋值避免锁存器 next_state current_state; transfer_done 1b0; error 1b0; mem_wr_en 1b0; // ... 其他输出默认值 case (current_state) S_IDLE: begin if (cfg_start) begin next_state S_CFG; end end S_CFG: begin // 加载配置寄存器 if (cfg_valid) begin next_state S_READ; end else if (cfg_timeout) begin next_state S_ERROR; end end S_READ: begin mem_addr read_addr; if (mem_rd_valid) begin next_state S_WRITE; buffer_data mem_rd_data; end end S_WRITE: begin mem_addr write_addr; mem_wr_en 1b1; mem_wr_data buffer_data; if (write_counter cfg_transfer_len) begin next_state S_IDLE; transfer_done 1b1; end else begin next_state S_READ; end end S_ERROR: begin error 1b1; // 可能等待复位或外部清除错误 if (error_clear) begin next_state S_IDLE; end end default: next_state S_IDLE; endcase end阅读要点状态编码与定义首先找到localparam或parameter定义的状态常量。理解每个状态代表的意义如S_IDLE空闲S_READ读内存。状态寄存器与下一状态逻辑区分current_state当前状态时序逻辑生成和next_state下一状态组合逻辑生成。这是典型的“三段式”状态机写法结构清晰。状态转移图脑图或手绘根据case语句中的条件在纸上或思维中画出状态转移图。明确从每个状态在什么条件下会跳转到哪个状态。例如从S_IDLE到S_CFG需要cfg_start信号有效。输出逻辑注意输出是在哪个状态下、在什么条件下被赋值的。输出可能是组合输出如mem_wr_en在S_WRITE状态直接置1也可能是摩尔型输出与状态直接相关或米利型输出与状态和输入都相关。上例中transfer_done在从S_WRITE跳回S_IDLE时置1是一个典型的脉冲输出。默认赋值与锁存器避免注意always (*)块开头的“默认赋值”如next_state current_state;。这是良好的编码风格确保在所有条件下输出都有定义防止综合出意想不到的锁存器Latch。注意事项遇到状态机一定要画图这是将文本逻辑转化为直观理解的最有效方法。同时要警惕那些没有明确状态寄存器而是用一堆flag信号和计数器实现的“隐式状态机”它们的可读性和可维护性通常很差需要更仔细地梳理条件跳转。4.3 数据通路与运算逻辑分析数据通路负责数据的加工、传输和存储。阅读时需关注数据从输入到输出的完整路径。// 示例一个简单的带流水线的乘法累加器MAC reg [DATA_WIDTH-1:0] pipeline_reg1, pipeline_reg2, pipeline_reg3; reg [2*DATA_WIDTH-1:0] product; // 乘积位宽扩展 reg [2*DATA_WIDTH7:0] accumulator; // 累加器位宽进一步扩展防溢出 always (posedge clk or negedge rst_n) begin if (!rst_n) begin pipeline_reg1 b0; pipeline_reg2 b0; pipeline_reg3 b0; product b0; accumulator b0; end else if (en) begin // 使能信号控制 // 第一级流水锁存输入 pipeline_reg1 data_a; pipeline_reg2 data_b; pipeline_reg3 coeff; // 第二级流水计算乘法 product pipeline_reg1 * pipeline_reg2; // 第三级流水计算乘加并输出 accumulator accumulator (product * pipeline_reg3); if (accumulator ACC_THRESHOLD) begin accumulator accumulator - ACC_THRESHOLD; mac_result_valid 1b1; mac_result accumulator[2*DATA_WIDTH-1:0]; // 截断输出 end else begin mac_result_valid 1b0; end end end阅读要点识别流水线级数通过观察寄存器pipeline_reg1, reg2, reg3的级联识别出这是一个三级流水线结构。数据在每个时钟周期向前流动一级。跟踪数据位宽变化注意乘法操作a * b会导致位宽翻倍从DATA_WIDTH到2*DATA_WIDTH。累加器accumulator的位宽更大这是为了防止在多次累加后发生溢出Overflow。这是阅读运算逻辑时的关键点位宽处理不当是常见的错误来源。理解运算顺序与时钟周期明确每个运算发生在哪个流水线阶段、哪个时钟周期。上例中乘法和乘加是分开在两个周期完成的。关注控制信号en信号控制整个流水线是否工作。mac_result_valid是输出有效标志指示结果何时可用。这种“有效信号”是数据通路中常见的握手机制。4.4 存储器、数组与FIFO的实现芯片内部经常需要存储结构如寄存器文件、RAM、FIFO等。// 示例一个同步双端口RAM的简单行为模型 reg [7:0] ram [0:1023]; // 1024个深度每个数据8位 reg [9:0] wr_addr_reg, rd_addr_reg; reg wr_en_reg; always (posedge clk) begin // 写操作 if (wr_en) begin ram[wr_addr] wr_data; end // 地址寄存器打拍常用于改善时序 wr_addr_reg wr_addr; rd_addr_reg rd_addr; wr_en_reg wr_en; end // 读操作组合逻辑或时序逻辑 assign rd_data ram[rd_addr]; // 异步读组合逻辑输出 // 或者 always (posedge clk) begin rd_data_reg ram[rd_addr_reg]; // 同步读延迟一个周期 end阅读要点声明方式reg [7:0] ram [0:1023];声明了一个1024x8的存储器。第一个维度是位宽第二个维度是深度。读写时序这是重中之重。需要明确写时序在哪个时钟沿地址和数据是否需要提前建立wr_en是电平有效还是脉冲有效上例是同步写在posedge clk时如果wr_en为高则将wr_data写入ram[wr_addr]。读时序是异步读地址变化后经过组合逻辑延迟数据立即出现在rd_data还是同步读地址在时钟沿锁存数据在下一个时钟沿输出同步读会延迟一个周期但时序更好。上例给出了两种方式的注释。FIFO的特殊性如果代码实现的是FIFO要重点关注空满标志empty,full的产生逻辑、读写指针wr_ptr,rd_ptr的更新方式通常是格雷码以及指针比较逻辑。这是FIFO正确工作的核心也是最容易出Bug的地方。5. 实战演练以一个小型FIFO控制器为例让我们通过一个简化的同步FIFO控制器代码片段来串联上述阅读方法。module sync_fifo #( parameter DATA_WIDTH 8, parameter DEPTH 16 // 必须是2的幂 )( input wire clk, input wire rst_n, input wire wr_en, input wire [DATA_WIDTH-1:0] wr_data, input wire rd_en, output wire [DATA_WIDTH-1:0] rd_data, output wire full, output wire empty ); // 指针用格雷码编码减少亚稳态风险 reg [$clog2(DEPTH):0] wr_ptr_gray 0; // 多一位用于判断满 reg [$clog2(DEPTH):0] rd_ptr_gray 0; reg [DATA_WIDTH-1:0] mem [0:DEPTH-1]; // 格雷码转二进制函数用于比较 function automatic [$clog2(DEPTH):0] gray2bin; input [$clog2(DEPTH):0] gray; integer i; begin gray2bin[$clog2(DEPTH)] gray[$clog2(DEPTH)]; for (i $clog2(DEPTH)-1; i 0; ii-1) begin gray2bin[i] gray2bin[i1] ^ gray[i]; end end endfunction wire [$clog2(DEPTH):0] wr_ptr_bin gray2bin(wr_ptr_gray); wire [$clog2(DEPTH):0] rd_ptr_bin gray2bin(rd_ptr_gray); // 空满判断比较指针的高位多出的那一位 assign empty (wr_ptr_gray rd_ptr_gray); assign full (wr_ptr_gray[$clog2(DEPTH)] ! rd_ptr_gray[$clog2(DEPTH)]) (wr_ptr_gray[$clog2(DEPTH)-1:0] rd_ptr_gray[$clog2(DEPTH)-1:0]); // 写指针更新 always (posedge clk or negedge rst_n) begin if (!rst_n) begin wr_ptr_gray 0; end else if (wr_en !full) begin // 二进制指针先加1再转格雷码 wr_ptr_gray (wr_ptr_bin 1b1) ^ ((wr_ptr_bin 1b1) 1); // 写入内存 mem[wr_ptr_bin[$clog2(DEPTH)-1:0]] wr_data; // 用二进制指针的低位作为地址 end end // 读指针更新与数据输出 always (posedge clk or negedge rst_n) begin if (!rst_n) begin rd_ptr_gray 0; rd_data 0; end else if (rd_en !empty) begin // 先输出数据基于当前读指针 rd_data mem[rd_ptr_bin[$clog2(DEPTH)-1:0]]; // 然后更新读指针 rd_ptr_gray (rd_ptr_bin 1b1) ^ ((rd_ptr_bin 1b1) 1); end end endmodule阅读步骤拆解接口扫描快速浏览模块端口知道这是一个有时钟复位、写使能、写数据、读使能、读数据以及空满标志的同步FIFO。关键参数与变量注意到DEPTH必须是2的幂这是使用格雷码和简化空满判断的常见前提。看到wr_ptr_gray和rd_ptr_gray的位宽是$clog2(DEPTH):0即比地址索引多一位。多出的这一位MSB是判断“满”状态的关键。核心算法格雷码与空满判断为什么用格雷码注释已说明减少亚稳态风险。因为读写指针可能在不同时钟域本例是同步的但设计习惯如此用格雷码相邻状态只有一位变化在跨时钟域同步时出错概率低。空判断empty (wr_ptr_gray rd_ptr_gray);读写指针格雷码完全相等时FIFO为空。这很好理解。满判断这是难点。full的判断逻辑是读写指针格雷码的最高位不同但其余位相同。这等价于二进制指针的差值等于DEPTH。多出的那一位MSB相当于绕了一圈的标志。可以这样理解当写指针比读指针多走了一圈即多出DEPTH个位置时它们就“追上”了此时FIFO满。在二进制表示下wr_ptr_bin - rd_ptr_bin DEPTH。在格雷码下这个条件就表现为最高位不同、其余位相同。指针更新与内存访问写操作当wr_en有效且!full时先计算二进制指针加1再转换为格雷码赋给wr_ptr_gray。同时用当前的二进制指针的低位wr_ptr_bin[$clog2(DEPTH)-1:0]作为地址将wr_data写入mem。注意这里用的是更新前的wr_ptr_bin即当前写地址来索引内存这是正确的。读操作当rd_en有效且!empty时先用当前的二进制读指针索引内存将数据输出到rd_data。然后再更新读指针。这是标准的“先读后更新”顺序确保当前地址的数据被正确读出。函数与转换gray2bin函数实现了格雷码到二进制码的转换。在空满判断时我们直接比较格雷码。但在计算下一个指针值ptr_bin 1和索引内存时需要用到二进制指针所以需要这个转换函数。通过这个例子我们可以看到阅读RTL不仅需要理解每行代码的语法更需要理解其背后的硬件电路意图和设计模式如用格雷码实现FIFO指针。将代码段与脑海中的电路结构两个指针寄存器、一个内存阵列、比较逻辑对应起来是理解的关键。6. 常见问题排查与调试技巧实录在实际阅读和调试RTL的过程中会遇到各种各样的问题。以下是一些典型场景和我的排查思路。6.1 信号值为未知态X或高阻态Z这是仿真中最常见的问题之一。可能原因1未初始化的寄存器。在复位阶段rst_n生效时没有给所有寄存器赋一个确定的值。确保在复位分支中为每个在 always 块中赋值的 reg 型变量都设置初始值。可能原因2多驱动Multiple Driver。同一个reg或wire被多个always块或assign语句驱动。检查工具如VCS通常会报 warning。需要仔细梳理代码逻辑确保每个信号只有一个驱动源。对于总线可能需要使用三态门inout和使能信号但芯片内部应尽量避免三态。可能原因3组合逻辑环路Combinational Loop。组合逻辑的输出不经任何寄存器直接反馈到其输入形成环路。这会导致仿真器无法确定稳定值产生X。使用 linting 工具如 SpyGlass可以提前检测此类问题。排查技巧在波形中定位最早出现 X 的时刻然后向前追溯该信号的驱动逻辑。使用仿真器的“驱动探测”Driver Probe功能可以列出所有驱动该信号的源。重点关注复位结束后的第一个时钟沿。6.2 功能与预期不符但仿真无语法错误代码能编译通过仿真也不报错但结果不对。可能原因1时序问题。代码是纯RTL行为描述但实际电路有延迟。例如一个组合逻辑的输出立即被用作同一时钟沿的寄存器输入这在实际电路中可能因为建立时间Setup Time不满足而失败但在RTL仿真中却能“正确”工作。这需要通过静态时序分析STA来发现。可能原因2阻塞赋值与非阻塞赋值混用错误。在描述组合逻辑的always (*)块中错误使用了或在描述时序逻辑的always (posedge clk)块中错误使用了会导致仿真结果与综合后电路行为严重不符。黄金法则时序逻辑用组合逻辑用。可能原因3敏感列表不完整。在 Verilog 中always (a or b)这样的敏感列表如果遗漏了信号当未列出的信号变化时该 always 块不会执行导致逻辑错误。推荐使用always (*)或always_combSystemVerilog让工具自动推断。排查技巧波形对比法将仿真波形与根据规格手绘的时序图进行对比找出第一个出现偏差的时钟周期。信号追踪法从出错的输出信号反向追踪逐级查看其驱动信号的波形和值检查每一级逻辑是否符合预期。打印调试法在关键节点使用$display或$monitor打印信号值特别是复杂的状态机跳转或计算过程可以将中间结果输出到日志进行分析。强制赋值法在仿真中可以临时强制Force某个信号为特定值以测试如果该信号正确后续逻辑是否正常从而定位问题源头。6.3 代码可读性差难以理解面对遗留代码或他人编写的“天书”。策略1重构与注释如果条件允许对关键但晦涩的代码段进行小幅重构并用清晰的注释说明其意图。例如将一个复杂的条件判断表达式提取成一个有意义的wire或parameter。策略2绘制框图这是最有效的方法。无论代码多乱硬件电路的基本元素寄存器、组合逻辑、MUX、加法器等是有限的。根据代码尝试画出数据通路和控制器状态机的框图。策略3运行仿真观察行为给模块施加简单的激励如简单的递增数据或固定的控制序列观察其输入输出波形。动态行为往往比静态代码更容易理解。策略4利用综合工具生成原理图大多数EDA工具都能从RTL生成门级或RTL级的原理图。虽然可能很庞大但聚焦于关键路径或子模块的视图能直观地展示电路结构。6.4 性能瓶颈分析阅读RTL也是为了评估和优化性能。关键路径识别通过综合工具的时序报告找到延迟最大的路径。回到RTL代码中定位这条路径看是否是由于逻辑级数过多如长长的组合逻辑链、扇出过大一个信号驱动太多负载或复杂的运算如大位宽乘法导致。面积估算查看综合面积报告了解哪个模块或哪种类型的逻辑如存储器、乘法器消耗了最多面积。在RTL层面可以考虑是否能用时间换空间如复用计算单元或者算法上是否有简化的可能。功耗热点推测高翻转率Toggle Rate的信号和大型存储器是功耗的主要来源。通过仿真获取信号的翻转活动再结合RTL代码可以识别出哪些部分在频繁工作。可以考虑门控时钟Clock Gating或数据门控来降低动态功耗。阅读芯片RTL源码是一项融合了硬件思维、软件工具使用和系统分析能力的综合技能。它没有绝对的捷径唯有多看、多练、多思考。从理清接口开始到把握状态机再到跟踪数据流最后将整个模块在脑海中映射成一张清晰的电路图。每一次深入的阅读不仅是对他人设计的理解更是对自己硬件设计能力的一次锤炼。当你能够流畅地阅读复杂IP的源码时你对自己动手设计出稳健、高效的硬件自然会充满信心。
返回列表