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
.doscript), 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.
Table of Contents
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
- Argument reduction — the two MSBs of
i_angletell 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. - Rotation stages —
ITERATIONSidentical stages, generated by agenerateloop. Stage i adds or subtracts the shifted vector and updates the residual angle withatan(2⁻ⁱ), read from a constant table. The datapath carries two guard bits (IW = WIDTH + 2) so the CORDIC gain cannot overflow inside the pipeline. - Output register — narrows the datapath back to
WIDTHbits 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
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.
# 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.
module cordic #(
parameter WIDTH = 16,
parameter ITERATIONS = 12
)(
use_parameters: Yes
param_target_file: ''
start_delimiter: 'parameter WIDTH = '
stop_delimiter: ','
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:
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.
entity cordic is
generic (
WIDTH : integer := 16;
ITERATIONS : integer := 12
);
use_parameters: Yes
param_target_file: ''
start_delimiter: 'WIDTH : integer := '
stop_delimiter: ';'
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.
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:
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:
- 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.
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 verdictsreset,latency_check,completeness,accuracy, and the overallstatus.
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
WIDTHandITERATIONScombine. - Simulation — the feature behind the four simulator flows.
- All architecture examples — the six designs and what each one adds.