VHDL, Verilog and SystemVerilog side by side
A Rosetta table of the constructs you write every day in VHDL-2008, Verilog-2001 and SystemVerilog, with compiled snippets for the ones a table cannot hold.
This page is for engineers who read one hardware description language and need to write another. It maps the synthesisable subset only. Every snippet below was compiled, as part of a complete module, with Icarus Verilog (SystemVerilog mode) or nvc (VHDL-2008) before publication. Where SystemVerilog and Verilog-2001 differ, both are shown, because many vendor flows still default to Verilog-2001 for .v files and SystemVerilog for .sv.
Rosetta table
| Construct | VHDL-2008 | Verilog-2001 | SystemVerilog |
|---|---|---|---|
| Design unit | entity plus architecture |
module |
module |
| Input port | a : in std_logic |
input wire a |
input logic a |
| Output port | y : out std_logic_vector(7 downto 0) |
output reg [7:0] y or output wire |
output logic [7:0] y |
| Net or variable | signal (in architecture), variable (in process) |
wire (driven by assign), reg (driven by always) |
logic for both |
| Register, sync reset | if rising_edge(clk) then if rst = '1' then ... elsif en = '1' then ... |
always @(posedge clk) if (rst) ... else if (en) ... |
always_ff @(posedge clk) ... |
| Async reset | if rst = '1' then ... elsif rising_edge(clk) then ... |
always @(posedge clk or posedge rst) |
always_ff @(posedge clk or posedge rst) |
| Combinational block | process (all) |
always @* |
always_comb |
| Non-blocking assign | <= (signal in process) |
<= |
<= |
| Blocking assign | := (variable) |
= |
= |
| Two-to-one mux | y <= a when sel = '1' else b; |
assign y = sel ? a : b; |
same |
| Case | case sel is when "00" => ... when others => ... |
case (sel) 2'b00: ... default: ... |
unique case or priority case |
| Loop generate | g : for i in 0 to N-1 generate ... end generate; |
genvar i; generate for (...) begin : g ... end endgenerate |
for (genvar i = 0; ...) begin : g |
| Conditional generate | if COND generate ... else generate (2008) |
if (COND) begin : g ... end inside generate |
same |
| Parameter | generic (WIDTH : positive := 8) |
parameter WIDTH = 8 |
parameter int WIDTH = 8 |
| Local constant | constant C : natural := 3; |
localparam C = 3; |
localparam int C = 3; |
| Package | package p is ... end package; plus use work.p.all; |
none (use include files) | package p; ... endpackage plus import p::*; |
| Array of vectors | type mem_t is array (0 to 15) of std_logic_vector(7 downto 0); |
reg [7:0] mem [0:15]; |
logic [7:0] mem [16]; |
| Unsigned arithmetic | unsigned from ieee.numeric_std |
plain vectors are unsigned | same, or logic unsigned |
| Signed arithmetic | signed from numeric_std |
reg signed [7:0] |
logic signed [7:0] |
| Integer | integer, natural, positive (32-bit, ranged) |
integer (32-bit signed) |
int, shortint, longint, byte |
| Vector to integer | to_integer(unsigned(v)) |
implicit | implicit, or int'(v) |
| Integer to vector | std_logic_vector(to_unsigned(i, 8)) |
implicit truncation | 8'(i) |
| Resize | resize(u, 16) |
zero or sign extend by assignment | 16'(u) or assignment |
| Bit width of a constant | (others => '0'), x"FF" |
{WIDTH{1'b0}}, 8'hFF |
'0, '1, 8'hFF |
| Concatenate | a & b |
{a, b} |
{a, b} |
| Replicate | (others => x) or (7 downto 0 => x) |
{8{x}} |
{8{x}} |
| Reduction AND | and v (2008) |
&v |
&v |
| Synthesis attribute | attribute async_reg : string; attribute async_reg of ff : signal is "TRUE"; |
(* ASYNC_REG = "TRUE" *) reg [1:0] ff; |
same as Verilog |
| Enumerated state | type state_t is (IDLE, RUN); |
localparam IDLE = 2'd0, RUN = 2'd1; |
typedef enum logic [1:0] {IDLE, RUN} state_t; |
| Record or struct | type hdr_t is record ... end record; |
none | typedef struct packed { ... } hdr_t; |
| Function | function f(x : natural) return natural is ... end function; |
function [7:0] f; input [7:0] x; ... endfunction |
function automatic logic [7:0] f(input logic [7:0] x); |
| Assertion (simulation) | assert cond report "msg" severity error; |
if (!cond) $error("msg"); |
assert (cond) else $error("msg"); |
| Comments | -- line or /* block */ (2008) |
// line, /* block */ |
same |
Register with synchronous reset and enable
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity counter is
generic (WIDTH : positive := 8);
port (
clk : in std_logic;
rst : in std_logic;
en : in std_logic;
q : out std_logic_vector(WIDTH-1 downto 0)
);
end entity;
architecture rtl of counter is
signal q_r : unsigned(WIDTH-1 downto 0) := (others => '0');
begin
process (clk) begin
if rising_edge(clk) then
if rst = '1' then
q_r <= (others => '0');
elsif en = '1' then
q_r <= q_r + 1;
end if;
end if;
end process;
q <= std_logic_vector(q_r);
end architecture;
module counter #(parameter WIDTH = 8) (
input wire clk,
input wire rst,
input wire en,
output reg [WIDTH-1:0] q
);
always @(posedge clk) begin
if (rst) q <= {WIDTH{1'b0}};
else if (en) q <= q + 1'b1;
end
endmodule
module counter #(parameter int WIDTH = 8) (
input logic clk,
input logic rst,
input logic en,
output logic [WIDTH-1:0] q
);
always_ff @(posedge clk) begin
if (rst) q <= '0;
else if (en) q <= q + 1'b1;
end
endmodule
The VHDL version keeps an internal unsigned signal because an out port of type std_logic_vector cannot be added to. VHDL-2008 allows reading an output port, so the intermediate signal is a choice, not a requirement.
Four-way mux with case
architecture rtl of mux4 is
begin
with sel select y <=
a when "00",
b when "01",
c when "10",
d when others;
end architecture;
always @* begin
case (sel)
2'd0: y = a;
2'd1: y = b;
2'd2: y = c;
default: y = d;
endcase
end
always_comb begin
unique case (sel)
2'd0: y = a;
2'd1: y = b;
2'd2: y = c;
default: y = d;
endcase
end
unique tells the synthesiser that exactly one arm matches, which removes the priority chain. It is only an optimisation hint. The default arm is still what stops a latch.
For-generate
architecture rtl of and_tree is
begin
g_bit : for i in 0 to N-1 generate
y(i) <= a(i) and b(i);
end generate;
end architecture;
genvar i;
generate
for (i = 0; i < N; i = i + 1) begin : g_bit
assign y[i] = a[i] & b[i];
end
endgenerate
In SystemVerilog the generate and endgenerate keywords are optional and the genvar can be declared in the loop header. The label after begin is required by many tools so that generated instances get a hierarchical name.
Packages
package fifo_pkg is
constant DEPTH : positive := 16;
subtype byte_t is std_logic_vector(7 downto 0);
function clog2_of(n : positive) return natural;
end package;
package body fifo_pkg is
function clog2_of(n : positive) return natural is
variable r : natural := 0;
begin
while 2**r < n loop
r := r + 1;
end loop;
return r;
end function;
end package body;
package fifo_pkg;
localparam int DEPTH = 16;
typedef logic [7:0] byte_t;
function automatic int clog2_of(input int n);
return $clog2(n);
endfunction
endpackage
module fifo_user import fifo_pkg::*; (
input byte_t d,
output byte_t q
);
localparam int AW = clog2_of(DEPTH);
assign q = d;
endmodule
Verilog-2001 has no packages. The usual workaround is a header with `define macros or parameters passed down the hierarchy. The VHDL package needs a library ieee; use ieee.std_logic_1164.all; line above it, omitted here for space.
Arrays and memories
architecture rtl of ram16x8 is
type mem_t is array (0 to 15) of std_logic_vector(7 downto 0);
signal mem : mem_t;
begin
process (clk) begin
if rising_edge(clk) then
if we = '1' then
mem(to_integer(unsigned(addr))) <= d;
end if;
q <= mem(to_integer(unsigned(addr)));
end if;
end process;
end architecture;
logic [7:0] mem [0:15];
always_ff @(posedge clk) begin
if (we) mem[addr] <= d;
q <= mem[addr];
end
Both infer a block RAM with a registered read (read-before-write on the same address). The Verilog-2001 form is identical with reg in place of logic and always @(posedge clk).
Type conversion and signedness
-- s is signed(7 downto 0), u is unsigned(7 downto 0), sum is 9 bits
sum <= std_logic_vector(resize(signed(s), 9) + signed(resize(unsigned(u), 9)));
idx <= to_integer(unsigned(u(3 downto 0)));
// s is logic signed [7:0], u is logic [7:0], sum is logic signed [8:0]
assign sum = s + $signed({1'b0, u});
assign idx = u[3:0];
The rules differ in a way that bites. In VHDL nothing converts implicitly and the compiler refuses mixed types. In Verilog and SystemVerilog an expression is signed only if every operand is signed, so one unsigned operand silently makes the whole expression unsigned and drops sign extension. The $signed({1'b0, u}) idiom keeps u positive while letting the add be signed.
Synthesis attributes
signal ff : std_logic_vector(1 downto 0);
attribute async_reg : string;
attribute async_reg of ff : signal is "TRUE";
(* ASYNC_REG = "TRUE" *) logic [1:0] ff;
Attribute names are vendor-specific: ASYNC_REG, KEEP, DONT_TOUCH, MAX_FANOUT and RAM_STYLE for AMD. Altera uses preserve, keep, ramstyle and altera_attribute. Synopsys-derived tools use syn_keep, syn_preserve and syn_ramstyle, as found in Libero SoC and Lattice Diamond. Yosys honours keep and a few of its own.
Things that do not translate
| VHDL only | Verilog and SystemVerilog only |
|---|---|
| Strong typing with user-defined ranged integers | Four-state x propagation in arithmetic |
generate with case on a generic (2008) |
`ifdef preprocessor and `include |
| Protected types and shared variables | Interfaces and modports (SystemVerilog) |
| Physical types with units (time, but also user-defined) | Classes, constrained random and coverage (SystemVerilog, simulation only) |
| Configurations (rarely used) | Hierarchical references across modules in simulation |
Sources
- IEEE 1076-2019, VHDL Language Reference Manual
- IEEE 1800-2023, SystemVerilog
- Yosys documentation, supported SystemVerilog subset
- nvc VHDL simulator
- Related on this site: /topics/hdl-languages/ and /reference/reset-and-cdc-patterns/