FPGA

VHDL Operators and Precedence

By CourseTron Team ·  3 min read ·  Updated Oct 2026 💬 𝕏 in f
Table of Contents
    ? Print / Save as PDF

    In short: VHDL ranks its operators in seven precedence levels, with logical operators the loosest and **, abs and not the tightest, so mixed expressions need parentheses, and the classic bugs come from & sharing a level with +, from unary not binding before =, and from arithmetic that silently drops the carry.

    Why operator rules deserve an interview-grade look

    Most VHDL bugs that survive simulation are not exotic. They are an expression that parsed differently from what the author imagined, or a compile error that was "fixed" by sprinkling parentheses until it stopped complaining. Synthesis tools such as Synopsys Design Compiler, Vivado and Quartus all follow the IEEE 1076 grammar exactly, so an expression means the same thing everywhere. Knowing that grammar lets you read a colleague's RTL quickly and predict the hardware before you run a tool. If you are still deciding between languages, our comparison of Verilog vs VHDL shows how differently the two treat operators: VHDL is strict and verbose, Verilog is permissive and terse.

    The operator families

    VHDL groups its predefined operators by what they do. Each family works on specific types, and the standard libraries (ieee.std_logic_1164 and ieee.numeric_std) overload them for std_logic, unsigned and signed.

    FamilyOperatorsResult
    Logicaland, or, nand, nor, xor, xnor, notSame type as operands
    Relational=, /=, <, <=, >, >=boolean
    Adding+, -, & (concatenation)Numeric, or a longer array for &
    Multiplying*, /, mod, remNumeric
    Miscellaneous**, absNumeric
    Shiftsll, srl, sla, sra, rol, rorSame as left operand

    Key fact: relational operators return boolean, not std_logic. That is why if a = b then works while y <= (a = b); is a type error for a std_logic target (VHDL-2008 relaxes some of this with the ?= matching operators).

    The precedence ladder

    From loosest to tightest, the levels are: logical, relational, shift, adding, sign (unary plus and minus), multiplying, and finally the miscellaneous group. The ladder below shows them with the tightest binding on top.

    VHDL precedence: tightest binding at the top 1 Miscellaneous** abs nottightest 2 Multiplying* / mod rem 3 Sign (unary)+ - 4 Adding+ - & (concatenate) 5 Shiftsll srl sla sra rol ror 6 Relational= /= < <= > >= 7 Logicaland or nand nor xor xnorloosest CourseTron · coursetron.com
    Seven precedence levels. All binary logical operators share the bottom level, which is why VHDL refuses to guess how you mixed them.

    Logical operators: no precedence among peers

    Unlike C, VHDL gives and and or the same rank. The result is a hard rule: you may chain the same operator (a and b and c), or chain or, and, xor only in a single repeated kind, but a and b or c is a compile error. The tools are protecting you from the sum-of-products ambiguity that costs real silicon respins in other languages. Write (a and b) or c.

    nand and nor are not associative, so even a nand b nand c is rejected. Spell it out: not (a and b and c) is a single three-input NAND, while (a nand b) nand c is two gates with a very different truth table.

    The second trap is the unary not. Because it sits at the top of the ladder, not a = b means (not a) = b, never not (a = b). With vectors that is often still legal, so the bug compiles and simulates wrong. Use a /= b or parenthesise.

    Relational operators and the vector-comparison trap

    Equality on arrays compares element by element, which is what you expect. Ordering is the hazard. On a plain std_logic_vector the operators < and > are not defined by std_logic_1164 at all, and the old non-standard std_logic_unsigned package that supplied them is best avoided. With numeric_std you convert explicitly:

    signal raw : std_logic_vector(7 downto 0);
    ...
    if unsigned(raw) > 200 then
      alarm <= '1';
    end if;

    Remember also that = compares the nine std_logic values literally, so '-' never equals '1' in =, even when you intended a don't-care. Use std_match or the VHDL-2008 ?= operator when don't-cares matter.

    Arithmetic: width is your job

    With numeric_std, adding two unsigned operands of widths N and M gives a result of width max(N, M). There is no carry-out bit. Add 255 and 1 in eight bits and you get 0, not 256. Multiplication is kinder: the result width is N + M, so an 18-bit by 18-bit product is 36 bits, which happens to match the DSP48-class multipliers in AMD and Intel devices. Division, mod and rem are synthesisable by most tools only when the divisor is a constant power of two; otherwise expect a large combinational block or a rejection. Addition and subtraction map onto dedicated hardware, as explained in our piece on FPGA carry chains, so a clean + is almost always the fastest option you can write.

    Key fact: mod takes the sign of the right operand, rem takes the sign of the left. For unsigned data they agree, but for signed values -7 mod 4 = 1 while -7 rem 4 = -3.

    Sign operators bind looser than multiplication, so -a * b is -(a * b). Conversely, a * -b is illegal: a sign cannot follow a multiplying operator directly, so write a * (-b). The same rule rejects 2 ** -1.

    Concatenation: same rank as plus

    The ampersand joins arrays and elements into a longer array, and it is the standard way to extend a word before arithmetic. Its precedence equals that of binary + and -, and operators at one level evaluate left to right. Therefore '0' & a + 1 is ('0' & a) + 1, not '0' & (a + 1).

    Adding 1 to a = "1111" (unsigned) a + 1 (4-bit result) 1111 + 1 0000 Wraps to 0: the carry vanishes ('0' & a) + 1 (5-bit result) 01111 + 1 10000 Carry kept in the extra top bit The leading '0' is the guard bit that gives the carry somewhere to land. CourseTron · coursetron.com
    Prepending a zero before the add widens the adder by one bit, which is the idiomatic way to capture a carry in VHDL.
    How the tool evaluates: '0' & a + 1 '0'&a+1 Step 1: & and + share a level, so the leftmost one goes first: '0' & a (5 bits) Step 2: add 1 to the 5-bit value, so the carry lands in the new top bit CourseTron · coursetron.com
    Animated: equal-rank operators resolve left to right, so the concatenation happens before the addition.

    Key fact: concatenation of a std_logic literal with an unsigned yields an unsigned in numeric_std, so '0' & a feeds straight into + without a type conversion.

    A short checklist before you commit RTL

    • Parenthesise every mix of logical operators and every mix of logical with relational.
    • Size results explicitly with resize instead of trusting implicit widths.
    • Use unsigned or signed for arithmetic, never std_logic_vector directly.
    • Run lint (Siemens Questa, Synopsys SpyGlass or the Vivado linter) so width mismatches are flagged before simulation.
    • Remember that concurrent and sequential code parse operators identically; only where the result is assigned differs, as covered in concurrent vs sequential statements in VHDL.

    Teams that train juniors on these habits catch most operator bugs in code review rather than in the lab. If your team wants structured RTL training, the instructors at ChipXpert run hands-on VLSI and FPGA programs, and you can browse our own CourseTron courses for self-paced options.

    Ready to write clean, synthesisable VHDL? Explore the FPGA tracks in our CourseTron courses and practise these operators on real designs.

    Frequently asked questions

    Why does a and b or c fail to compile in VHDL?

    Because all binary logical operators share one precedence level and VHDL does not define an order among different ones. Add parentheses to state your intent, for example (a and b) or c.

    Does the ampersand have higher precedence than plus?

    No. Concatenation and binary plus and minus sit on the same adding level and evaluate left to right, so '0' & a + 1 is ('0' & a) + 1.

    How do I keep the carry when adding two unsigned numbers?

    Resize or prepend a zero to each operand first, such as ('0' & a) + ('0' & b), so the result is one bit wider than the inputs.

    Is not a = b the same as a /= b?

    No. not binds tighter, so it parses as (not a) = b. Use a /= b for inequality.

    References & Further Reading

    Advertisement