ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

加法器设计:原理图与VHDL实现及FPGA验证要点

加法器设计:原理图与VHDL实现及FPGA验证要点 1. 加法器这事比你想的更值得较真加法器原理图看起来就是几个逻辑门绕来绕去VHDL里写起来甚至只要一个加号但真正要把两条设计链路都走通我花了一个完整的周末。标题叫“加法器设计——原理图与VHDL设计初步”听起来像是数字电路课程的入门作业实际上里面藏着的坑教材上基本不会写。加法器是整个ALU的基础CPU里的地址计算、计数器累加、补码减法归根结底都在做加法。把这个东西吃透后面接触流水线、乘法器、浮点运算单元时都会轻松很多。我在做这个项目时先用原理图方式搭了一个1位全加器再封装成符号级联成4位加法器然后又用VHDL重新实现了一遍做了穷举仿真最后下载到FPGA开发板上用拨码开关验证。整个过程下来最大的感受是原理图能帮你建立直观的硬件概念VHDL能让你体会“描述硬件”和“写软件”的差别这两条路不能互相替代。1.1 半加器与全加器的本质区别很多初学者把半加器和全加器混在一起其实它们的使用场景完全不同。半加器只有两个输入A和B输出本位和S与进位Cout。它的真值表很简单ABSCout0000011010101101从真值表可以推出逻辑表达式S A xor BCout A and B。也就是说半加器本质上就是一个异或门加一个与门。但实际做多位加法时低位的进位必须能传到高位半加器没有进位输入所以根本不够用。全加器多了一个进位输入Cin一共三个输入。它的真值表有8行核心逻辑是S A xor B xor CinCout (A and B) or (Cin and (A xor B))。很多教材喜欢把Cout写成(A and B) or (Cin and A) or (Cin and B)这两种写法在逻辑上等价但后一种直接用三个与门加一个或门前一种把A xor B复用给S和Cout门电路数量更少。做原理图设计时第二种表达能省一个异或门这也是为什么经验丰富的工程师更愿意用复用中间信号的方式来画图。1.2 多位加法器的进位链为什么不能偷懒把多个全加器串起来就得到了多位加法器。常见的方式是行波进位加法器也就是把低位的Cout接到高位的Cin像多米诺骨牌一样一级一级传上去。4位加法器需要4个全加器16位就是16个。这种结构最大的问题在于延迟最高位的计算结果必须等低位进位一级级传上来才能稳定位宽越大延迟越明显。实际项目中16位以上的加法器通常会用超前进位、进位选择或者流水线来优化但那属于后续进阶内容。我建议初学阶段先老老实实把行波进位结构画明白因为它是理解进位链延迟的关键。你在原理图里用线把Cout连到Cin在VHDL里用一个加号表面上看结果一样但综合器实际生成的硬件很可能仍然是进位链。搞清楚这条链子在哪里后面看时序报告才能看懂哪条路径是critical path。2. 原理图设计从门级电路到模块封装的思维转换原理图输入是很多数字电路课程最早接触的设计方式。打开Quartus或者OrCAD从元件库里拖一堆74系列的与门、或门、异或门把线连好编译通过后就能生成仿真波形。这个过程看着像是在画电路图其实是在做“网表装配”——你每画一根线就等于指定了两个引脚之间的物理连接。2.1 用基本门电路搭一个1位全加器我当时用Quartus原题图输入选择了基本逻辑门元件。全加器的数据流方程是S A xor B xor CinCout (A and B) or (Cin and (A xor B))所以至少需要一个异或门实现A xor B这个中间信号同时馈给S的第二个异或门和Cout的与门。具体连线步骤可以这样拆解放置两个异或门第一个异或门输入A和B输出记为X第二个异或门输入X和Cin输出就是S。放置一个与门输入A和B输出记为AB。再放置一个与门输入X和Cin输出记为XC。放置一个或门输入AB和XC输出就是Cout。画完之后一定要给网络线起名字不要放任Quartus自动生成NAND0、NOT1这种名字。起个明确的名字比如X、AB、XC后面看仿真波形时一目了然。我在第一次画的时候没给中间网络命名结果仿真波形里只能看到一堆数字编号排查问题非常痛苦。这一步的收获不在于连线本身而在于你亲自验证了“逻辑表达式到门电路”的映射过程。S为什么需要两个异或门串联因为三个输入做奇偶校验本质就是异或两次。Cout为什么能复用X因为(A and B) or (Cin and (A xor B))比直接写三项更精简。这些理解了后面看别人设计的复杂电路才不会晕。2.2 层次化设计把全加器封装成符号搭好1位全加器之后如果要用同样的方式直接画4位加法器连线会非常乱而且一旦某个门画错修改起来要命。正确的做法是层次化把1位全加器整体封装成一个自定义符号block symbol然后在顶层原理图里例化4次。Quartus里选中已经画好的全加器电路右键创建符号即可。生成的符号会自动带上A、B、Cin、S、Cout这些端口顶层只需要拖4个符号出来把低位的Cout连到高位的Cin再把A[3..0]、B[3..0]、S[3..0]这些总线引到输入输出引脚。这一步最容易犯的错是位序搞反。假设A的最高位是A[3]那就应该接到最高位全加器FA3的A端口A[0]接到最低位FA0。如果你把A[0]接到FA3仿真波形照样能跑但数值完全不对。我当时就栽过一次低位和高位接反得到的结果像是字节序乱了单独看每一位又都对浪费了半小时才反应过来。层次化设计做加法器看起来是大炮打蚊子但这个思路是必须养成的。真实项目里几十个模块没人会把所有门电路平铺在同一张原理图上。通过封装符号、顶层例化你把设计的复杂度控制在一眼能看懂的范围内这才是原理图输入真正有价值的地方。2.3 绘制过程中容易翻车的低级错误原理图绘制里有些错误特别让人郁闷因为不是逻辑错误而是工具使用问题。最常见的是页面的page number重复。OrCAD里如果你有两张以上的原理图页面page number都设成了1DRC会报ORCAP-11010。我当时做多页工程时遇到过报错不会影响电路仿真的功能但导出的PDF里页码会乱后续整理文档很麻烦。另一个低级错误是总线的命名风格不统一。原理图里总线名通常写成A[3..0]VHDL里则是A(3 downto 0)。如果在原理图里引出的总线名和模块端口名不一致编译时会提示连接问题。我的经验是在做顶层集成之前先统一网络命名规范总线用A[3..0]这种风格信号用bt_din这种带前缀的名字避免net123这种自动名混入。画完原理图后记得跑一遍Design Rule CheckDRC。DRC能查出浮空输入、输出短路、总线位宽不匹配这些问题。我第一次做时忽略了DRC结果下载到板子上LED乱亮回去一查是有一个输入悬空。原理图工具本质上是“所见即所得”你画的是物理连接DRC就是最后一道防线。3. VHDL实现从实体声明到行为级描述的路径原理图方式虽然直观但项目一大就不现实。VHDL作为硬件描述语言让你用文本方式描述硬件结构综合器再把它映射成门级网表。加法器是VHDL入门最合适的例子因为它够简单又能清楚展示实体、结构体、信号赋值这些核心概念。3.1 实体与结构体先捋清端口映射VHDL设计的基本单元是实体加结构体。实体描述模块对外的端口结构体描述模块内部的行为。一个1位全加器的实体如下library ieee; use ieee.std_logic_1164.all; entity full_adder is port ( A : in std_logic; B : in std_logic; Cin : in std_logic; S : out std_logic; Cout : out std_logic ); end full_adder;这里A、B、Cin是输入S和Cout是输出。std_logic是一种标准的单比特类型除了0和1还能表示高阻Z、未知X等状态仿真时可以暴露很多问题。如果你用bit类型那么仿真波形上永远只有0和1遇到三态冲突会直接被忽略反而不利于排错。结构体里可以写多种风格。最简单的数据流描述直接映射逻辑表达式architecture dataflow of full_adder is begin S A xor B xor Cin; Cout (A and B) or (Cin and (A xor B)); end dataflow;注意这里Cout的表达式复用了(A xor B)因为S也需要它。综合器通常不会真的例化两个异或门而是会自动提取公共子表达式但你写成这样逻辑上更清晰。3.2 三种描述风格数据流、结构、行为VHDL写同一个加法器至少有三条路。数据流风格就像上面那样用连续赋值描述信号之间的关系适合底层组合逻辑。结构风格是例化半加器子模块代码看起来像原理图architecture structural of full_adder is signal x, ab, xc : std_logic; begin u1: entity work.half_adder port map ( A A, B B, S x, Cout ab ); u2: entity work.half_adder port map ( A x, B Cin, S S, Cout xc ); Cout ab or xc; end structural;这种写法最接近你画原理图时的思路适合学习模块例化。行为风格则可以用更接近软件的方式描述功能比如直接用加法运算符architecture behavioral of full_adder is signal result : unsigned(1 downto 0); begin result (0 A) (0 B) (0 Cin); S result(0); Cout result(1); end behavioral;行为风格写起来最省事但前提是你清楚综合器会把映射成什么。如果不加约束综合器会生成一个用进位链实现的二进制加法器效果和你手动级联全加器是一样的。初学阶段我建议三种风格都各写一遍然后对比综合后的RTL视图你会深刻理解“描述硬件”和“写软件”的差别。3.3 写一个参数化的N位加法器实际项目里不会有几个人真的写4位加法器更常见的是写一个位宽可配置的加法器模块。VHDL用generic实现参数化下面是典型的N位加法器library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; entity adder_n is generic ( N : positive : 4 ); port ( A : in std_logic_vector(N-1 downto 0); B : in std_logic_vector(N-1 downto 0); Cin : in std_logic; S : out std_logic_vector(N-1 downto 0); Cout : out std_logic ); end adder_n; architecture rtl of adder_n is signal a_u, b_u, sum_u : unsigned(N downto 0); signal cin_u : unsigned(N downto 0); begin a_u unsigned(0 A); b_u unsigned(0 B); cin_u (0 Cin, others 0); sum_u a_u b_u cin_u; S std_logic_vector(sum_u(N-1 downto 0)); Cout sum_u(N); end rtl;这里最关键的是把加数的位宽扩展成N1因为两个N位数相加再加进位结果最多是N1位。如果不做扩展A B Cin在中途就可能溢出综合后的行为会和预期不一致。另外一定要用ieee.numeric_std里的unsigned不要混用std_logic_vector和integer。曾经有人在VHDL里直接写A B而不做类型转换编译报错后又把两个向量声明成signed结果处理负数时整个逻辑都变了。参数化之后例化4位加法器就变成一行u_add : entity work.adder_n generic map (N 4) port map (A a_vec, B b_vec, Cin cin_bit, S s_vec, Cout cout_bit);需要8位、16位只需要改generic map。这才是硬件描述语言相比原理图最大的优势同一个模块可以复用在不同的位宽场景。4. 仿真与验证把加法器放到真实场景里测写完代码不等于设计完成仿真才是真正检验设计正确性的环节。加法器功能简单但穷举输入组合是必须做的基本功。很多新手写了个testbench跑完看到波形觉得“看起来差不多”就结束结果上板之后才发现边界情况完全没覆盖。4.1 测试平台怎么写才不是走过场测试平台testbench的作用是给被测模块施加激励然后检查输出是否符合预期。对4位加法器来说输入组合总共2的9次方也就是512种情况完全可以暴力穷举。下面是一个简单的测试平台框架library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; entity adder_n_tb is end adder_n_tb; architecture sim of adder_n_tb is constant N : integer : 4; signal A, B, S : std_logic_vector(N-1 downto 0); signal Cin, Cout : std_logic; begin dut : entity work.adder_n generic map (N N) port map (A A, B B, Cin Cin, S S, Cout Cout); stim_proc : process variable expected : std_logic_vector(N downto 0); begin for i in 0 to 2**N - 1 loop for j in 0 to 2**N - 1 loop for k in 0 to 1 loop A std_logic_vector(to_unsigned(i, N)); B std_logic_vector(to_unsigned(j, N)); Cin 0 when k 0 else 1; wait for 10 ns; expected : std_logic_vector(to_unsigned(i j k, N 1)); if (Cout S) / expected then report Mismatch: A integerimage(i) B integerimage(j) Cin integerimage(k) got to_string(Cout S) exp to_string(expected) severity error; end if; end loop; end loop; end loop; report Simulation finished. severity note; wait; end process; end sim;这个testbench做了三件事遍历所有输入组合、计算期望值、自动比对输出。仿真结束后只要看Transcript窗口里有没有error就能确认功能是否正确。我习惯在testbench里加自动比较而不是人眼盯着波形看因为512种组合人眼根本看不完。4.2 边界情况与进位传播覆盖穷举是一种方式但实际项目中位宽一大穷举就不现实了。比如32位加法器输入组合数多到天文数字。这时候需要采用定向测试加随机测试结合的策略。加法器的边界情况主要这几类全0加全0进位为0结果应该为0。全1加全1进位为1结果应该为全0并产生最高位进位。A0xFFFFB0x0001验证从最低位一直传到最高位的进位链。A0x8000B0x8000验证最高位相加产生进位。我在验证4位加法器时除了穷举还会单独把A1111, B0000, Cin1这种用例拿出来看波形确保进位从第0位传到第3位的时序是正确的。你可以在仿真波形里展开S和Cout信号看到Cout的变化比S稍微慢那么一点因为进位沿进位链传播需要时间。这个“慢一点”在行为仿真里不一定看得到但在门级仿真里非常明显也是理解组合逻辑延迟的好例子。4.3 综合后的资源占用与时序问题功能仿真通过后下一步是综合。打开Quartus或者Vivado跑一遍综合看报告里LUT和进位链的使用情况。4位加法器通常只消耗几个LUT和一个进位链根本不算资源但你应该养成看报告的习惯。我的经验是综合后一定要看两个东西一个是RTL视图。Vivado里打开Schematic你写的最终会变成什么硬件结构这里能看得清清楚楚。如果你看到综合器自动插入了超前进位逻辑说明工具在替你优化如果你的代码风格很差综合器生成了一大堆多余的LUT这里也会暴露问题。另一个是时序报告。虽然加法器在这种低速课程设计里不可能时序违例但你要明白关键路径在哪。行波进位加法器的关键路径就是从最低位Cin到最高位Cout的进位链这条路径的延迟决定了整个模块的最高工作频率。把这条路径找出来加适当的约束等以后做复杂系统时就不会对时序报告一无所知。5. 画原理图与写VHDL容易翻车的几个细节加法器设计做完我整理了几个反复踩过的坑。有些坑看起来很小但新手往往会被卡住很久所以单独拿出来说。5.1 引脚锁定与FPGA开发板的对应关系如果你要把加法器下载到FPGA开发板上验证引脚锁定是绕不开的一步。开发板上的拨码开关、按键、LED对应的FPGA引脚每个板子都不一样必须查原理图手册。我用的板子上4个LED正好对应某个Bank的4个引脚但拨码开关却在另一个Bank初始化时如果不小心把某个开关对应的引脚设成了三态仿真时没问题上板后LED完全不亮。引脚锁定还要注意bank电压。不同Bank的IO电压可能不同有的支持3.3V有的只支持1.8V。做纯数字逻辑实验一般不会碰到但如果你外接模块电压不匹配会导致通信不正常甚至烧坏引脚。把板卡手册上的引脚对照表先打印出来再开始分配引脚这是最稳妥的做法。5.2 组合逻辑毛刺加法器也会“乱跳”组合逻辑加法器有一个非常隐蔽的问题输出毛刺。原因在于不同输入信号到达输出端的时间不一样。比如A从0111变成1000B保持不变按道理加法结果应该稳定地加1但在过渡瞬间A的各位并不是同时翻转的真实电路里某些位可能先变了某些位后变导致输出在几个纳秒内出现一个短暂的其他值。这就是竞争冒险。在纯组合逻辑仿真中如果你用行为级仿真模型通常看不到毛刺因为RTL仿真不模拟门延迟。但下载到FPGA上用示波器或者逻辑分析仪看输出毛刺是真实存在的。解决毛刺的办法不是修改加法器本身而是在输出端加寄存器。寄存器只在时钟边沿采样毛刺发生在采样点之间就不会被采进去。这也是为什么真实系统里任何组合逻辑的输出不能直接拿来当控制信号必须先打一拍。我在验证加法器模块时专门把S输出接到寄存器然后用LED观察。如果不打拍LED亮度会偶尔闪烁一下看起来似乎是接触不良实际上是毛刺。打拍之后稳定多了。5.3 工程组织与版本管理的朴素建议最后一个建议和代码无关但很实用把原理图文件、VHDL源文件、仿真工程、综合工程分目录存放命名规范统一。我在做加法器时刚开始偷懒所有文件堆在一个文件夹里结果Quartus工程和ModelSim工程互相引用文件时路径混乱换一台电脑打开工程直接报错找不到文件。比较合理的目录结构是doc存放设计说明、引脚分配表src存放所有VHDL源文件命名用adder_n.vhd这种带功能的名字sch存放原理图工程文件sim存放testbench和仿真脚本synth存放综合工程、约束文件和位流文件另外建议用git做版本管理。加法器虽然小但你可能今天改成8位明天加个进位输出后天又想试试不同描述风格。没有版本管理改坏了就想回退很痛苦。git的代价极低收益在项目稍微复杂一点时立刻体现出来。加法器设计看起来是入门作业但“原理图和VHDL两种方式实现同一个功能”这个练习逼着你去理解硬件描述的层次、仿真的覆盖、综合的约束这些能力在后续任何数字系统设计里都用得上。如果非要给后来者一条总结那就是先用原理图把电路结构看懂再用VHDL把它写出来最后用仿真和上板验证把每一个输入组合都跑透。做完这三步你才算真正把加法器这块地基打牢。
返回列表