多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

VHDL中signed与unsigned类型详解:从数值溢出到硬件设计避坑指南

VHDL中signed与unsigned类型详解:从数值溢出到硬件设计避坑指南 1. 从一次“诡异”的数值溢出说起几年前我在做一个FPGA上的数字信号处理模块核心功能是实现一个可配置系数的FIR滤波器。设计本身不复杂无非是乘累加运算。为了追求更高的动态范围和精度我决定将中间累加器的位宽设置得足够大比如输入是16位有符号数系数也是16位有符号数乘积是32位我打算用40位的寄存器来累加理论上绰绰有余。仿真阶段一切顺利但当我把设计下载到板卡上用实际信号源测试时问题出现了当输入特定幅度的正弦波时输出波形会在某些点发生严重的畸变看起来像是被“削顶”了但又不是简单的饱和而是一种非对称的失真。我第一反应是时序问题但检查了时序报告裕量充足。回过头看RTL仿真用同样的测试向量波形完美。问题出在哪我花了将近一天的时间最终把问题定位在了一个非常基础却又极易被忽视的地方VHDL中unsigned和signed数据类型的隐式转换与运算规则。我的累加器声明为std_logic_vector(39 downto 0)但在进行累加操作时我直接写的是acc acc (data_in * coeff);。这里data_in和coeff是signed(15 downto 0)它们的乘积结果是signed(31 downto 0)。然后这个signed类型的结果被与一个std_logic_vector类型的acc直接相加。在仿真器中由于我使用了特定的库和设置这种混合类型的运算可能通过隐式转换得出了我“预期”的结果。但综合工具在解读这个表达式时其类型转换和位宽扩展的规则可能与仿真器存在细微差别尤其是在处理符号位扩展和溢出时导致了实际硬件行为与仿真行为的偏离。这个坑让我深刻意识到在VHDL中尤其是涉及数值计算时对signed和unsigned数据类型的理解绝不能停留在“知道有这么个东西”的层面必须透彻理解其本质、运算规则以及与std_logic_vector之间的“爱恨情仇”。今天我们就来彻底梳理一下VHDL中的这些数值类型这不仅是语法问题更是写出可靠、可预测硬件代码的基石。2.signed与unsigned的本质不仅仅是“有符号”和“无符号”很多初学者会把signed和unsigned简单地理解为整数类型的硬件映射这其实是一个不够精确的认知。更准确地说signed和unsigned是std_logic_vector的“子类型”或具有特殊算术语义的数组类型具体取决于numeric_std库的实现视角。它们和std_logic_vector一样本质上都是一维数组元素都是std_logic。它们的关键区别在于语义。**std_logic_vector 仅代表一串二进制位。它没有固有的数值含义。对它进行、-、*等算术操作是非法的除非重载操作符但标准库不直接支持。它的行为就是位的集合。unsigned 代表一串自然二进制码**。它将这些位解释为一个非负整数。最高位MSB是最高有效位不是符号位。例如unsigned“1001”表示十进制数9。signed 代表一串二进制补码**。它将这些位解释为一个带符号的整数。最高位MSB是符号位。例如signed“1001”在二进制补码中表示十进制数-7因为1001是-7的4位补码表示。这种语义上的区别决定了它们能参与的操作。在numeric_std标准库中为signed和unsigned重载了完整的算术运算,-,*,abs等、比较运算,,等和移位操作。这意味着你可以直接对它们进行数学运算而综合工具会生成对应的加法器、乘法器等硬件电路。注意 VHDL本身有内置的整数类型integer但integer是软件抽象其位宽由工具链决定通常是32位不直接对应硬件上的特定位宽。在RTL设计中为了精确控制资源消耗和时序我们几乎总是使用signed/unsigned或std_logic_vector来定义具有明确位宽的信号。2.1 声明与初始化声明这些类型时必须指定位宽且通常使用downto降序索引这与二进制数的书写习惯一致最左边是MSB。signal unsig_num : unsigned(7 downto 0); -- 8位无符号数范围0到255 signal sig_num : signed(7 downto 0); -- 8位有符号数范围-128到127 signal slv_reg : std_logic_vector(7 downto 0); -- 8位位向量初始化时可以使用字符串字面量但要注意解释方式unsig_num “10011001”; -- 十进制 153 sig_num “10011001”; -- 十进制 -103 二进制补码 slv_reg “10011001”; -- 就是位模式“10011001”无数值更安全的做法是使用类型转换函数或to_unsigned/to_signed函数unsig_num to_unsigned(200, 8); -- 将整数200转换为8位无符号数 sig_num to_signed(-50, 8); -- 将整数-50转换为8位有符号数3. 类型转换的“明规则”与“暗陷阱”这是最容易出错的地方。VHDL是强类型语言不同类型之间的赋值通常需要显式转换。numeric_std库提供了以下关键转换函数unsigned(x)/signed(x): 将std_logic_vector转换为unsigned或signed。不改变位值只改变语义。这是最常用也最需要小心的转换。signal slv : std_logic_vector(7 downto 0) : “11110000”; signal uns : unsigned(7 downto 0); signal sig : signed(7 downto 0); uns unsigned(slv); -- uns 的数值是 240 sig signed(slv); -- sig 的数值是 -16 因为11110000是-16的补码陷阱 如果你有一个本应表示负数的std_logic_vector比如来自一个以补码形式输出的IP核你错误地将其转换为unsigned你会得到一个巨大的正数计算完全错误。std_logic_vector(x): 将unsigned或signed转换回std_logic_vector。同样只改变容器不改变位值。sig to_signed(-10, 8); slv std_logic_vector(sig); -- slv 得到的是 -10 的补码位模式to_integer(x): 将unsigned或signed转换为VHDL的integer类型。常用于与常数比较或作为数组索引。if to_integer(unsig_counter) 100 then ... addr to_integer(signed_offset); -- 假设addr是integer类型陷阱 转换的unsigned或signed数值必须在integer的表示范围内通常-2^31到2^31-1否则会在仿真时抛出错误。resize(x, size): 调整位宽。这是极其重要的函数用于处理运算中的位宽扩展。signal a : signed(7 downto 0); signal b : signed(15 downto 0); b resize(a, 16); -- 将8位有符号数a符号扩展为16位。如果a是正数高位补0如果是负数高位补1。3.1 隐式转换与运算符重载的“暗坑”在我开篇提到的那个坑里混合类型运算是罪魁祸首。numeric_std为重载运算符定义了非常具体的规则。例如unsigned unsigned返回一个unsigned位宽取两者中较大的那个。但是unsigned integer也是被重载的返回unsigned。然而std_logic_vector与signed/unsigned之间的算术运算通常没有定义。如果你直接写slv unsig大多数工具会报编译错误。但为什么我的代码能通过编译甚至仿真呢这里有几个可能使用了非标准库 如std_logic_unsigned/std_logic_signed。这些Synopsys公司早期的库允许std_logic_vector直接进行算术运算将其隐式当作unsigned或signed处理。强烈不建议使用这些库因为它们会导致代码歧义和不可移植性。同一个std_logic_vector在不同地方可能被期望有不同的解释灾难的种子就此埋下。工具链的“宽容” 某些仿真器在遇到类型不匹配时可能会尝试进行有限的隐式转换但这不属于VHDL标准行为不可依赖。表达式中的中间结果 更复杂表达式中的类型推导可能出乎意料。最佳实践永远进行显式、精确的类型转换和位宽控制。-- 推荐清晰、明确 acc std_logic_vector(signed(acc) (data_in * coeff)); -- 更佳显式控制位宽防止溢出 signal acc_signed : signed(39 downto 0); acc_signed resize(acc_signed (data_in * coeff), 40); slv_acc_out std_logic_vector(acc_signed(31 downto 0)); -- 输出截断后的结果4. 运算过程中的位宽扩展与溢出管理这是数字设计中的核心问题。硬件寄存器有固定位宽不像软件整数可以自动扩容。4.1 加法和减法的位宽对于A B或A - BA, B同为signed或unsigned结果的理论位宽是max(len(A), len(B)) 1。因为两个N位数相加可能产生一个N1位的结果例如两个最大的正数相加。但是numeric_std中重载的和-运算符返回结果的位宽是max(len(A), len(B))。也就是说最高位的进位或借位会被丢弃。这本质上是模运算。signal u1 : unsigned(3 downto 0) : “1111”; -- 15 signal u2 : unsigned(3 downto 0) : “0001”; -- 1 signal u_sum : unsigned(3 downto 0); u_sum u1 u2; -- 结果是 “0000” (0) 进位被丢弃。发生了溢出这就是硬件现实。综合工具会生成一个4位加法器进位输出Cout被悬空或忽略。如果你需要检测溢出或保留完整结果你必须手动处理signal u1 : unsigned(3 downto 0) : “1111”; signal u2 : unsigned(3 downto 0) : “0001”; signal u_sum_full : unsigned(4 downto 0); -- 声明5位宽接收 u_sum_full (‘0’ u1) (‘0’ u2); -- 将操作数零扩展一位再相加 -- 或者使用 resize u_sum_full resize(u1, 5) resize(u2, 5);对于有符号数signed原理类似但扩展的是符号位signal s1 : signed(3 downto 0) : “0111”; -- 7 signal s2 : signed(3 downto 0) : “0001”; -- 1 signal s_sum : signed(3 downto 0); s_sum s1 s2; -- 结果是 “1000” (-8) 正溢出变成了负数。 signal s_sum_safe : signed(4 downto 0); s_sum_safe resize(s1, 5) resize(s2, 5); -- 正确结果为 “001000” (8)4.2 乘法的位宽乘法器是面积大户位宽管理更重要。对于A * B结果的理论位宽是len(A) len(B)。numeric_std中的*运算符返回的位宽正是len(A) len(B)。这是合理的因为乘法结果的范围远大于加/减。signal a : unsigned(7 downto 0); signal b : unsigned(7 downto 0); signal prod : unsigned(15 downto 0); -- 必须声明为16位 prod a * b;对于有符号乘法同样返回len(A) len(B)位宽且结果是标准的二进制补码乘积。4.3 溢出的检测与处理在通信、信号处理等场景溢出可能意味着灾难。处理溢出有几种策略饱和处理 如果结果超出表示范围则将其钳位到最大值或最小值。function saturate_add(a, b: signed; max_val, min_val: integer) return signed is variable sum_full : signed(a‘length downto 0); variable sum_trunc : signed(a‘length-1 downto 0); begin sum_full : resize(a, a‘length1) resize(b, b‘length1); if sum_full max_val then return to_signed(max_val, a‘length); elsif sum_full min_val then return to_signed(min_val, a‘length); else return resize(sum_full, a‘length); end if; end function;警告或标志位 通过检查操作数的符号位和结果的符号位来检测溢出并产生一个溢出标志信号。signal ovf_flag : std_logic; signal a, b, sum : signed(7 downto 0); process(a, b, sum) begin ovf_flag ‘0’; if a(7)b(7) and sum(7)/a(7) then -- 同号相加结果符号不同溢出 ovf_flag ‘1’; end if; end process;动态扩展 如前面所述始终在更宽的寄存器中进行运算最后根据需求截取或舍入。这是最常用也最稳健的方法但消耗更多资源。5. 移位操作逻辑移位与算术移位的区别移位是另一种容易混淆的操作。numeric_std为signed和unsigned提供了移位函数但行为不同。shift_left/shift_right(或sll/srl操作符): 对于unsigned这是逻辑移位。移出的位丢弃空出的位补‘0’。signal uns : unsigned(7 downto 0) : “10011001”; -- 153 uns_shifted uns srl 2; -- 结果是 “00100110” (38) 相当于除以4向下取整对于signedshift_right是算术右移。移出的位丢弃但空出的高位左侧用符号位填充。这保持了二进制补码的符号。signal sig : signed(7 downto 0) : “11100111”; -- -25 sig_shifted shift_right(sig, 2); -- 结果是 “11111001” (-7) 相当于除以4向负无穷取整 -- 注意直接使用 sig srl 2 在某些上下文中可能执行逻辑移位导致错误明确使用 shift_right 函数更安全。关键点 算术右移shift_right(signed)是实现有符号数除以2的幂次的高效硬件方式。而逻辑右移srl会破坏符号位将负数变成正数。6. 比较操作与等号判定的微妙之处比较运算符,/,,,,在numeric_std中也被重载可以用于signed与signed、unsigned与unsigned之间的比较甚至signed/unsigned与integer的比较。这很直观。但有一个隐藏的坑std_logic_vector与数值类型的比较。signal slv : std_logic_vector(7 downto 0) : “00001000”; -- 位模式 signal uns : unsigned(7 downto 0) : “00001000”; -- 数值8 if slv “00001000” then ... -- 这是位模式比较成立。 if uns 8 then ... -- 这是数值比较成立。 if slv uns then ... -- 编译错误类型不匹配。 if unsigned(slv) uns then ... -- 正确先转换。 if slv std_logic_vector(uns) then ... -- 正确位模式比较。等号用于std_logic_vector时进行的是逐位比较。而用于signed/unsigned时进行的是数值比较。这意味着“1000”和“01000”作为std_logic_vector不相等位宽不同但转换为unsigned后可能相等如果上下文允许位宽调整但通常需要resize。务必保持比较双方类型和位宽的一致性。7. 实战中的经验法则与避坑指南结合我多年的踩坑经验这里总结几条黄金法则统一使用numeric_std库 在文件开头声明use ieee.numeric_std.all;。坚决摒弃std_logic_arith,std_logic_unsigned,std_logic_signed等非标准库。它们是不确定性和移植性噩梦的根源。设计之初明确类型 在定义每个信号/变量时立刻问自己这个数据代表什么是无符号整数、有符号整数还是纯粹的位集合根据答案选择unsigned、signed或std_logic_vector。不要都用std_logic_vector后期再转换。运算前统一类型和位宽 在任何算术或比较操作前确保操作数类型一致。使用unsigned()、signed()、resize()进行显式转换和位宽调整。不要依赖任何隐式行为。拥抱resize()函数 这是你控制位宽、防止意外溢出和截断的最好朋友。在加法、减法前特别是累加操作前思考是否需要先将操作数扩展到更宽的位宽。为乘法结果准备足够的空间 声明足够宽的信号来接收乘法结果即位宽等于乘数位宽之和。区分逻辑移位和算术移位 操作有符号数时使用shift_right进行算术移位以保持符号操作无符号数或进行位操作时使用shift_left/shift_right或sll/srl。仿真与综合的交叉检查 对于关键的数据路径特别是涉及复杂类型转换和位宽变化的逻辑不要满足于功能仿真通过。一定要进行综合后仿真或使用形式验证工具确保综合工具的理解与你的设计意图一致。我开篇的那个bug如果做了综合后仿真很可能就能提前发现。善用断言 在仿真代码中使用assert语句检查溢出、边界条件等。assert to_integer(result) 0 and to_integer(result) 256 report “Result out of expected range!” severity error;理解并熟练运用VHDL中的signed和unsigned类型是写出严谨、可靠数字硬件代码的关键一步。它强迫你在设计初期就考虑数据的物理表示和运算的边界条件这正是硬件描述语言与软件编程语言哲学上的重要区别。把这些规则内化为习惯能让你避开许多隐蔽的陷阱从而更加自信地构建出行为符合预期的数字系统。
返回列表