Cordic (VHDL & SystemVerilog)

The CORDIC example is one of the most complete design shipped with Odatix. The same core is provided in two languages (VHDL and SystemVerilog), swept over two independent parameters, and simulated by four different simulators (Verilator, GHDL, QuestaSim/ModelSim, Vivado), each testbench reporting its own accuracy and timing metrics.

What this example demonstrates
  • Parameter domains — two independent parameters swept and combined automatically, with no hand-written configuration matrix.
  • Delimiter-based parameter substitution with no markers in the RTL — the existing parameter/generic declarations serve as delimiters, so the source stays plain, synthesizable, and readable outside Odatix.
  • The same design in two languages — VHDL and SystemVerilog, with configurations that differ only by their delimiters, making cross-language comparison possible.
  • Simulations with four tools — Verilator, GHDL, QuestaSim/ModelSim and Vivado, each with its own flow (Makefile, shell script, or .do script), all reporting progress the same way.
  • Metrics extraction from simulation results — the testbench decides what is worth measuring and writes it.
  • A genuine two-dimensional trade-off to explore — accuracy against area and latency, which makes it a good playground for the Explorer.

What the design does

The design is a pipelined CORDIC rotation core in circular mode. It rotates the input vector (i_x, i_y) by the angle i_angle and outputs the rotated vector (o_x, o_y), scaled by the CORDIC processing gain (~1.6468).

The rotation is computed the CORDIC way: no multiplier, only additions, subtractions and arithmetic shifts. Each pipeline stage rotates the running vector by ±atan(2⁻ⁱ), the sign being chosen so that the residual angle converges to zero.

The angle is encoded on 32 bits, with a full turn mapped to 2³²:

angle_rad = i_angle * 2*pi / 2^32

so that [-180°, +180°[ maps exactly onto the signed 32-bit range, and the argument reduction is a simple test on the two MSBs.

Interface
Port Direction Width Role
clock in 1 Clock, rising edge
reset in 1 Synchronous reset, active high
i_valid in 1 Input sample is valid
i_x, i_y in WIDTH Signed input vector
i_angle in 32 Rotation angle (full turn = 2³²)
o_valid out 1 Output sample is valid
o_x, o_y out WIDTH Signed rotated vector

The core is fully pipelined: one result per clock cycle, with a latency of ITERATIONS + 2 cycles (1 cycle for the argument reduction, ITERATIONS cycles for the rotation stages, 1 cycle for the output register).

Structure
  1. Argument reduction — the two MSBs of i_angle tell in which quadrant the angle is. Angles outside [-90°, +90°[ are pre-rotated by ±90°, which is a swap and a negation, and the angle is corrected by ± a quarter turn.
  2. Rotation stages — ITERATIONS identical stages, generated by a generate loop. Stage i adds or subtracts the shifted vector and updates the residual angle with atan(2⁻ⁱ), read from a constant table. The datapath carries two guard bits (IW = WIDTH + 2) so the CORDIC gain cannot overflow inside the pipeline.
  3. Output register — narrows the datapath back to WIDTH bits with saturation, so a gain overflow clips instead of wrapping around.

Parameters

Parameter Default Effect
WIDTH 16 Datapath width, in bits. Sets the quantization floor: the smallest representable step of the output vector, hence the noise floor of the result.
ITERATIONS 12 Number of rotation stages. Sets the angular resolution: after n stages the residual angle is bounded by atan(2^-(n-1)). Also sets the latency (ITERATIONS + 2 cycles) and dominates the area.

The two parameters trade accuracy against area and latency, and they are not redundant: past a certain point, adding iterations no longer improves anything because the error is dominated by the quantization of an already too narrow datapath, and vice versa. That is exactly the kind of two-dimensional trade-off Odatix is meant to map out.

The atan table has 24 entries. Beyond that, the rotation step falls below the resolution of the 32-bit angle encoding, so extra iterations contribute a zero angle — which is why the swept values stop at 24.

Files

workspace
examples/
├── cordic_sv/
│   └── cordic.sv                       # SystemVerilog version
└── cordic_vhdl/
    └── cordic.vhdl                     # VHDL version

odatix_userconfig/
├── architectures/
│   ├── Example_Cordic_sv/
│   │   ├── _settings.yml               # main architecture settings
│   │   ├── width/                      # 'width' parameter domain
│   │   │   ├── _settings.yml
│   │   │   ├── 4.txt, 8.txt, 12.txt
│   │   └── iterations/                 # 'iterations' parameter domain
│   │       ├── _settings.yml
│   │       └── 12.txt, 16.txt, 24.txt
│   └── Example_Cordic_vhdl/            # same structure, VHDL delimiters
└── simulations/
    ├── TB_Example_Cordic_Verilator/    # C++ testbench   (SystemVerilog only)
    ├── TB_Example_Cordic_GHDL/         # VHDL testbench  (VHDL only)
    ├── TB_Example_Cordic_QuestaSim/    # SV testbench    (both languages)
    └── TB_Example_Cordic_Vivado/       # SV testbench    (both languages)

Architecture settings

Both architectures declare their sources, their clock and reset, and a default frequency range. Neither of them defines parameters directly: use_parameters is No in the main settings, because both parameters live in their own parameter domain. This is what lets Odatix sweep WIDTH and ITERATIONS independently and combine them automatically, instead of writing one configuration file per pair.

architectures/Example_Cordic_sv/_settings.yml
# Source files
rtl_path: "examples/cordic_sv"
top_level_file: "cordic.sv"
top_level_module: "cordic"

# Signals
clock_signal: "clock"
reset_signal: "reset"

# The parameters are handled by the domains, not by the main configuration
use_parameters: No
start_delimiter: ""
stop_delimiter: ""

# Default frequencies (in MHz)
fmax_synthesis:
  lower_bound: 50
  upper_bound: 800
custom_freq_synthesis:
  list: [50, 100, 200]

The VHDL version is identical apart from rtl_path and top_level_file, which point to examples/cordic_vhdl/cordic.vhdl. The entity name is also cordic, so top_level_module does not change.

Parameter domains: the SystemVerilog version

Each domain replaces the value written after a delimiter in the parameter list of the module. Nothing is inserted in the RTL source — the delimiters are the existing declarations themselves.

examples/cordic_sv/cordic.sv
module cordic #(
  parameter WIDTH = 16,
  parameter ITERATIONS = 12
)(
architectures/Example_Cordic_sv/width/_settings.yml
use_parameters: Yes
param_target_file: ''
start_delimiter: 'parameter WIDTH = '
stop_delimiter: ','
architectures/Example_Cordic_sv/iterations/_settings.yml
use_parameters: Yes
param_target_file: ''
start_delimiter: 'parameter ITERATIONS = '
stop_delimiter: '\n'

param_target_file is empty, so the replacement happens in the top level file declared by the architecture. ITERATIONS is the last item of the parameter list, with no trailing comma, so its stop_delimiter is the end of the line (\n) instead of a separator.

The parameter files themselves hold nothing but the value to write:

architectures/Example_Cordic_sv/width/8.txt
8

Parameter domains: the VHDL version

The design is the same, so the domains are the same — only the syntax the delimiters match changes, from a Verilog parameter list to a VHDL generic clause.

examples/cordic_vhdl/cordic.vhdl
entity cordic is
  generic (
    WIDTH : integer := 16;
    ITERATIONS : integer := 12
  );
architectures/Example_Cordic_vhdl/width/_settings.yml
use_parameters: Yes
param_target_file: ''
start_delimiter: 'WIDTH : integer := '
stop_delimiter: ';'
architectures/Example_Cordic_vhdl/iterations/_settings.yml
use_parameters: Yes
param_target_file: ''
start_delimiter: 'ITERATIONS : integer := '
stop_delimiter: '\n'

Here again, ITERATIONS is the last generic of the clause and is followed by no ;, so the replacement stops at the end of the line.

Info

The domain directories carry the same names (width, iterations) in both architectures. That is what makes the run settings below symmetric between the two languages: only the architecture name changes.

Simulation settings

Each testbench declares how it is run, where its progress is reported, and which architectures it is meant for:

simulations/TB_Example_Cordic_Verilator/_settings.yml
tasks:
- name: main
  commands:
  - make sim --no-print-directory

progress:
  file: "log/progress.log"
  regex: "(.*): ([0-9]+)%(.*)"

architectures:
- Example_Cordic_sv

The architectures list is only an indication: running the testbench on another architecture works, and only prints a warning.

Testbench Tool Testbench language Runs on
TB_Example_Cordic_Verilator Verilator C++ SystemVerilog
TB_Example_Cordic_GHDL GHDL VHDL VHDL
TB_Example_Cordic_QuestaSim QuestaSim/ModelSim SystemVerilog either
TB_Example_Cordic_Vivado Vivado (xsim) SystemVerilog either

The three mixed-language testbenches drive the design through its ports only, so the same SystemVerilog testbench elaborates on top of the VHDL entity or the SystemVerilog module without any change.

Passing the configuration to the testbench

GHDL does the same and passes -gWIDTH=... -gITERATIONS=... to the run; the Vivado scripts accept both the VHDL generic form and the Verilog parameter form, which is what allows a single script to serve the two versions of the design.

Running the sweep

The two domains are combined with +, and * expands every value:

simulations_settings.yml
  - TB_Example_Cordic_Verilator:
    - Example_Cordic_sv + iterations/* + width/*

  - TB_Example_Cordic_GHDL:
    - Example_Cordic_vhdl + iterations/* + width/*

  - TB_Example_Cordic_QuestaSim:
    - Example_Cordic_sv + iterations/* + width/*
    - Example_Cordic_vhdl + iterations/* + width/*

  - TB_Example_Cordic_Vivado:
    - Example_Cordic_sv + iterations/* + width/*
    - Example_Cordic_vhdl + iterations/* + width/*

With 3 widths (4, 8, 12) and 3 iteration counts (12, 16, 24), each line expands to 9 simulations.

Metrics

The testbenches do not print numbers for Odatix to scrape out of a log: they write a results.yml file themselves, and _metrics.yml simply maps its keys to metrics of type yaml.

simulations/TB_Example_Cordic_Verilator/_metrics.yml
metrics:
  max_error:
    type: yaml
    settings:
      file: results.yml
      key: max_error_lsb
    unit: LSB
    format: '%.3f'

  snr:
    type: yaml
    settings:
      file: results.yml
      key: snr_db
    unit: dB
    format: '%.2f'

The reported metrics fall into four groups:

  • Configuration — width, iterations, echoed back so results are self-describing.
  • Accuracy — max_error, rms_error, mean_error (in LSB), max_angle_error, rms_angle_error (in degrees), snr, enob, worst_case_angle.
  • Timing — latency (cycles), throughput (samples/cycle), cycles.
  • Checks — vectors, checks_total, checks_passed, checks_failed, pass_rate, and the pass/fail verdicts reset, latency_check, completeness, accuracy, and the overall status.

Each run sweeps 512 test vectors spread evenly over a full turn, compares them against a floating-point reference, and checks the error against the theoretical bound of that configuration (residual angle after the last rotation, plus the truncation of the per-stage shifts) with a small margin. A configuration is therefore not judged against a fixed threshold, but against what it can legitimately be expected to achieve.

Where to go next

  • Counter — the same substitution mechanism, on the shortest possible design.
  • Sine ROM — the same two-dimensional trade-off, stored instead of computed.
  • Parameter domains — how WIDTH and ITERATIONS combine.
  • Simulation — the feature behind the four simulator flows.
  • All architecture examples — the six designs and what each one adds.