Skip to content

Session 5: RTL Design & Verification

In this session with Andreas Olofsson, the notebook introduces RTL (Register-Transfer Level) as the practical sweet spot for digital design: you describe hardware in terms of registers (flip-flops), combinational logic between them, and clocked transfers, and then let tools map that RTL into gates and transistors. The first “read this first” concept is the flip-flop vs latch distinction: flip-flops are edge-triggered and predictable, while latches are level-sensitive/transparent and usually show up by accident—most often when “combinational” logic fails to assign outputs in every path, forcing the tool to infer memory.

From there, the class walks through the core RTL workflow: Verilog module structure and data types, writing combinational logic (continuous assigns or combinational always blocks) vs sequential logic (the flip-flop pattern, and why = vs <= matters), and building finite state machines with a clean template. Verification is treated as mandatory, not optional: you’re shown how to write testbenches (including self-checking patterns), compile/run, and inspect waveforms, plus how to avoid common pitfalls (unintentional latches, mixing blocking/non-blocking in sequential code, incomplete sensitivity lists) and use Verilator lint warnings as a fast feedback loop. The notebook also previews robust practices around clock domain crossing (CDC)—metastability, synchronizers, and safe handling of multi-bit signals via handshake/FIFOs.

We only have until Thursday, so this homework is a short, focused sprint to apply RTL design + verification in a disciplined way. The objective is to implement a small Verilog block using a clean RTL style—explicit registers, purely combinational next-state/output logic, and a single clocked process for state updates—avoiding typical pitfalls like unintended latch inference or mixed blocking/non-blocking usage in sequential code.

Alongside the RTL, we must deliver a compact verification setup by Thursday: a testbench that drives the inputs through representative scenarios and edge cases, checks expected behavior (at least with simple pass/fail conditions), and produces waveforms that clearly demonstrate correctness. The emphasis is on correctness and clarity under time pressure: a small design, well-tested, with evidence (simulation results) that it behaves as intended.

To support my learning, I’ve been using this resource: https://www.chipverify.com/verilog/verilog-tutorial

Verilog

Verilog is a hardware description language (HDL) used to describe digital systems such as processors, memories, or even a simple flip-flop. This means that a hardware description language can be used to describe any (digital) hardware at any level of abstraction.

Verilog is one of the HDL standards widely used in the industry today for hardware design. It allows designers to describe a system at different abstraction levels, depending on how detailed the description needs to be.

Levels of Abstraction in Verilog

Verilog supports the design of a circuit at different levels of abstraction, among which the following stand out:

  • Gate level. This corresponds to a low-level description of the design, also known as a structural model. The designer describes the circuit using logic primitives (AND, OR, NOT, etc.), their interconnections, and by adding the timing properties of the different primitives. All signals are discrete and can take only the values ‘0’, ‘1’, ‘X’, or ‘Z’ (‘X’ being an unknown/undefined state and ‘Z’ a high-impedance state).
  • Register-transfer level (RTL). Designs described at the RTL level specify a circuit’s behavior in terms of operations and the transfer of data between registers. Using timing specifications, these operations occur at defined instants. Describing a design at RTL gives it the property of being synthesizable; therefore, in modern practice, “RTL code” generally refers to any synthesizable code.
  • Behavioral level. The main feature of this level is its complete independence from the design’s structure. Rather than defining the structure, the designer defines the design’s behavior. At this level, the design is described using parallel algorithms. Each algorithm consists of a set of instructions that execute sequentially. A behavioral description may use non-synthesizable statements or constructs, which is why it is commonly used to build so-called testbenches.

Notes on the Verilog language

Before continuing with the design, I’ll briefly mention a few points that are common in many programming languages, but still worth noting here.

  • Comments: Single-line comments can be introduced with //. Multi-line comments can be written using the /* comment */ format.
  • Use of uppercase letters: Verilog is case-sensitive. It is recommended to use only lowercase letters.
  • Identifiers: Verilog identifiers must start with a letter and may contain letters a to z, numeric characters, and the symbols _ and $. The maximum length is 1024 characters.

Numbers format

Numeric constants in Verilog can be specified in decimal, hexadecimal, octal, or binary. Negative numbers are represented using two’s complement, and the “_” character may be used to make numbers easier to read, although it is not interpreted.

The syntax for a numeric constant is: <size>'<base><value>.

Although the language allows both integer and real numbers, for brevity and simplicity we will use only the integer numeric constant representation.

Integer Stored as. Description
1 00000000000000000000000000000001 Unzised 32 bits
8’hAA 10101010 Sized hex
6’b10_0011 100011 Sized binary
‘hF 00000000000000000000000000001111 Unzised hex 32 bits
6’hCA 001010 Truncated value
6’hA 001010 Filling with 0’s on the left
16’bz zzzzzzzzzzzzzzzz Filling z’s on the left
8’bx xxxxxxxx Filling in x’s on the left
8’b1 00000001 Filling with 0’s on the left

Negative Numbers

Negative numbers are specified by placing a minus - sign before the size of a number. It is illegal to have a minus sign between base_format and number.

-6'd3; // 6-bit negative number stored as two's complement of 3
 // Binary representation: 111101 (two's complement)

 -6'sd9; // Signed decimal: 's' indicates signed arithmetic

Arithmetic Operators

Arithmetic operators carry out mathematical calculations and are typically synthesized as hardware blocks such as adders, subtractors, multipliers, and dividers. If the divisor (the second operand) in a division or modulus operation is zero, the result is X (unknown). For the power operator, if either operand is a real value, the result is also real. Also, any value raised to the power of zero evaluates to 1 (a**0 = 1).

Operator Description Synthesis Hardware
a + b a plus b Adder (ripple-carry or carry-lookahead)
a - b a minus b Subtractor (two’s complement adder)
a * b a multiplied by b Multiplier (combinational or pipelined)
a / b a divided by b Divider (expensive, avoid in RTL if possible)
a % b a modulo b (remainder) Divider with remainder output
a ** b a to the power of b Not synthesizable (simulation only)

Data Types

Verilog data types are meant to model real hardware behavior, especially the difference between signals that represent connections and signals that can hold a value. Verilog also uses a 4-state logic system: 0, 1, X (unknown), and Z (high-impedance), which helps simulations represent uninitialized signals, contention, or tri-stated buses.

One of the most important distinctions is between nets and variables.

Nets (like wire) behave like physical wiring: they don’t store a value by themselves, and their value is determined by what drives them.

wire [3:0]  n0;         // 4-bit wire -> this is a vector
Net Type Description
wire Connects elements with continuous assignment
tri Connects elements with multiple drivers
wor Creates wired OR configurations
wand Creates wired AND configurations
trior Creates wired OR configurations with multiple drivers
triand Creates wired AND configurations with multiple drivers
tri0 Models nets with resistive pulldown devices
tri1 Models nets with resistive pullup devices
trireg Stores a value and is used to model charge storage nodes
uwire Models nets that can should be driven only by a single driver
supply0 Models power supply with a low level of strength
supply1 Models power supply with a high level of strength

Variables (like reg) are used in procedural code and can retain a value until they are assigned again. However, I’ve also learned that a reg does not automatically mean a flip-flop: whether it becomes combinational logic or sequential storage depends on how it’s assigned (for example, inside a clocked always block vs. purely combinational logic).

Finally, I’ve seen that types like integer, real, time/realtime, and string are very handy in testbenches for calculations, timing, and debugging, but they are generally not what you rely on for synthesizable RTL, where wire/reg vectors are the core building blocks.

// integer: 32-bit signed data type for general purpose use
integer count; // Can hold values from -2,147,483,648 to 2,147,483,647

// time: 64-bit unsigned for storing simulation time (integer representation)
time end_time; // Example: end_time = 50 (represents 50 time units)

// realtime: 64-bit floating point for high-precision time measurements
realtime rtime; // Example: rtime = 40.25 (can represent fractional time units)

// real: 64-bit floating point for mathematical calculations
real float; // Example: float = 12.344 (double-precision IEEE 754)

// Exact fit: 11 characters * 8 bits = 88 bits [8*11:1]
reg [8*11:1] str = "Hello World"; // Stores full string: "Hello World"

// Too small: Only 5 bytes (40 bits), leftmost 6 bytes truncated
reg [8*5:1] str = "Hello World";  // Stores rightmost 5 chars: "World"

// Too large: 20 bytes (160 bits), leftmost bits padded with zeros (spaces)
reg [8*20:1] str = "Hello World"; // Stores "         Hello World" (9 leading spaces)

Initial Block

An initial block runs once, starting at simulation time 0, and it finishes when all statements inside it have executed. It’s mainly a simulation/testbench construct: synthesis tools generally ignore initial, so it shouldn’t be relied on to build real hardware behavior in RTL.

I’ve also learned the key usage patterns:

  • If the block contains one statement, begin/end are optional; with multiple statements, begin/end are used to group them.
  • Statements inside an initial block execute sequentially, and delay controls like #N pause the block for N time units before continuing. This makes it useful for creating timed stimulus in a testbench.
  • A module can include multiple initial blocks, and they all start at time 0 and run in parallel with each other (and with any always blocks).
  • It’s common to use initial for testbench tasks like initializing signals, applying test vectors, setting up files, or ending the simulation with $finish.
// Single statement (no begin/end needed)
initial
 [single statement]

// Multiple statements (requires begin/end)
initial begin
 [statement 1]
 [statement 2]
 ...
 [statement N]
end

Always Block

An always block is a procedural block that represents a continuously running process in simulation. The statements inside execute sequentially, but the block itself is triggered by the event defined in its sensitivity list.

The sensitivity list (the signals inside @( ... )) controls when the block re-evaluates:

  • Level-sensitive lists like always @(a or b) are typically used for combinational logic, because the block re-runs whenever any input changes.
  • Edge-sensitive lists like always @(posedge clk) are used for sequential logic (flip-flops/registers), because updates happen only on clock edges (often with reset included as an additional event).
always @ (event)
    [statement]

always @ (event) begin
    [multiple statements]
end

I also learned an important simulation pitfall: if an always block has no sensitivity list and no delay/event control, it can create a zero-delay infinite loop and hang the simulation. Using an explicit delay like always #10 clk = ~clk; is fine for generating clocks in a testbench, but explicit delays are not synthesizable.

Finally, the page reinforces the usual synthesis guideline: use blocking (=) for combinational always code and nonblocking (<=) for clocked/sequential always code to get the intended hardware behavior.

Control Blocks

Verilog control blocks are the basic tools for expressing decision-making and repetition inside procedural code.

  • if / else if / else lets me choose which statements execute based on a condition. For multiple statements per branch, I need to group them with begin ... end. I also need to be careful with nested if statements because an omitted else can make it unclear which if the else belongs to—using begin/end helps avoid that ambiguity.

  • Loops provide repetition, but each loop has different semantics:

  • forever repeats indefinitely (easy to hang a simulation if I don’t include time control).

  • repeat(N) runs a fixed number of iterations; if N evaluates to X or Z, it’s treated as zero. There’s also a common testbench pattern like repeat(N) @(event) ....
  • while (cond) keeps running as long as the condition remains true, and doesn’t run at all if it’s false initially.
  • for (init; cond; inc) is the structured loop with initialization, condition checking, and incrementing the loop variable.

I’ve also learned some practical pitfalls:

  • In combinational always @(*), missing an else (or failing to assign a default) can infer latches.
  • forever/while without any delay or event control can create a zero-time infinite loop and freeze simulation—these patterns are mainly for testbenches unless properly clocked/event-driven.
  • A synthesizer doesn’t “iterate over time” like software: a for loop is typically unrolled into replicated hardware, so big loop bounds can explode area.

Assignments

After reviewing the documentation, I’ve learned that Verilog basically provides three assignment forms, and each one maps to a different modeling style:

  1. Procedural assignments (inside always, initial, tasks, and functions) These assign values to variables (e.g., reg, integer). The variable holds its value until the next procedural assignment. Within procedural code, I have to choose between:

    • Blocking = (immediate update, commonly used for combinational code)
    • Non-blocking <= (scheduled update, typically used for clocked/sequential code)
  2. Continuous assignments using assign These drive nets (e.g., wire) continuously. Whenever the RHS changes, the LHS net updates (conceptually “immediately” in simulation), which makes this style ideal for combinational wiring/logic like simple gates and muxes.

  3. Procedural continuous assignments (assign/deassign and force/release) These are meant for testbench control and debugging because they can override other assignments. The key takeaway is that they’re not synthesizable, so they should never appear in real RTL meant for hardware.

I also learned two practical “gotchas”:

  • What is legal on the LHS depends on the assignment type (procedural targets variables; continuous targets nets). Mixing them leads to compile/synthesis problems.
  • Initializing at declaration can be convenient, but doing that and also assigning at time 0 in an initial block can create a race condition in simulation.

Conditional Statements

I went over the different ways Verilog handles conditional logic, and there are three common options you’ll see all the time.

First, there’s the ternary operator (?:), which is great for simple “if/else” assignments in a single line—basically a compact way to write a mux. It can be nested, but if you overdo it, it quickly becomes hard to read.

Then you have the classic if ... else statement, used inside procedural blocks like always or initial. It’s the most straightforward way to express conditional behavior, especially when you need multiple statements in each branch (using begin/end).

if (<condition>) begin
    // statement 1
end else begin
    // statement 2
end

Finally, case ... endcase is the best option when you need to choose between many alternatives. You evaluate one expression and match it against different cases, and it’s common to include a default branch to cover anything unexpected—very typical in state machines and multi-way selection logic.

case (<expression>)
   value1: statement1;
   value2: statement2;
   ...
   default: statementN;
endcase

Write Verilog for your project’s core module

This module accepts two signed, 16-bit input signals and produces a 32-bit output. The output is truncated back to 16 bits to maintain the input data width, which is common in digital audio applications.

module ring_modulator (
    input  wire              clk,
    input  wire              rst_n,
    input  wire signed [15:0] signal_in,
    input  wire signed [15:0] carrier_in,
    output reg  signed [15:0] mod_out
);

    reg signed [31:0] product;


    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            product <= 32'sd0;
            mod_out <= 16'sd0;
        end else begin
            product <= signal_in * carrier_in;
            mod_out <= product[30:15];       
        end
    end
endmodule

Lint warnings

During the development process, I ran a linter to catch potential issues early. Lint warnings don’t necessarily mean the design is wrong, but they highlight patterns that may lead to bugs, unintended hardware, or reduced code clarity (for example, unused signals, incomplete assignments, or suspicious constructs). Treating these warnings seriously helps keep the RTL cleaner and makes debugging much easier.

/foss/tools/verilator/bin/verilator --lint-only -Wall /foss/designs/RingModulator_verilog/ring_modulator.v

Warning Sintaxis

Verilator warns because product is 32 bits wide but the design only uses product[30:15], leaving the MSB ([31]) and the lower bits ([14:0]) unused.

I’ve attached the reference table provided in the class materials, which summarizes the most common lint warnings and their typical causes/fixes.

Warning Severity Must Fix?
LATCH High Yes - usually a bug
BLKSEQ High Yes - race conditions
UNOPTFLAT High Yes - design won’t work
WIDTH Medium Review carefully
CASEINCOMPLETE Medium Add default case
UNUSED Low Clean up for production
PINMISSING Low Check intentional

Testbench

This testbench is a simple, minimal setup to verify the basic behavior of the ring_modulator module in simulation.

  • The line `timescale 1ns / 1ps sets the simulation time unit to 1 ns and the time precision to 1 ps, so delays like #5 and #10 are interpreted in nanoseconds.

  • The testbench declares the DUT signals:

  • clk and rst_n as control signals,

  • signal_in and carrier_in as signed 16-bit stimulus inputs,
  • mod_out as a signed 16-bit output wire.

  • The ring_modulator is instantiated as uut (Unit Under Test), wiring the testbench signals to the module ports.

  • A free-running clock is generated with:

verilog always #5 clk = ~clk;

This toggles the clock every 5 ns, producing a 10 ns clock period (100 MHz).

  • Inside the initial block, the test starts by initializing everything to zero and asserting reset (rst_n = 0). After #10 ns, reset is released by setting rst_n = 1.

  • Then it applies a few basic test cases, each held for #10 ns (roughly one clock cycle at this clock rate):

  • Positive modulation: signal_in = 5000 and carrier_in = 10000 to check that the output becomes a positive scaled product.

  • Negative carrier: carrier_in = -10000 while keeping signal_in positive, to confirm the output flips polarity (the product becomes negative).
  • Zero input: signal_in = 0 to verify the output goes to zero.

  • Finally, $finish; ends the simulation.

Overall, it’s a good “smoke test” to confirm reset works and that signed multiplication/polarity behave as expected, even though it doesn’t yet check exact numeric results or cover many edge cases (like overflow, saturation, or different scaling choices).

`timescale 1ns / 1ps

module tb_ring_modulator();
    reg clk;
    reg rst_n;
    reg signed [15:0] signal_in;
    reg signed [15:0] carrier_in;
    wire signed [15:0] mod_out;

    // Instantiate the Unit Under Test (UUT)
    ring_modulator uut (
        .clk(clk),
        .rst_n(rst_n),
        .signal_in(signal_in),
        .carrier_in(carrier_in),
        .mod_out(mod_out)
    );

    // Clock generation
    always #5 clk = ~clk;

    // Waveform dump (for GTKWave)
    initial begin
        $dumpfile("ring_modulator.vcd");
        $dumpvars(0, tb_ring_modulator);   // dump everything under this testbench
        // Optional:
        // $dumpvars(1, uut);               // dump only one level under uut
    end

    initial begin
        // Initialize Inputs
        clk = 0;
        rst_n = 0;
        signal_in = 0;
        carrier_in = 0;

        // Reset
        #10 rst_n = 1;

        // Test Case: Modulate with a positive value
        signal_in = 16'd5000;
        carrier_in = 16'd10000;
        #10;

        // Test Case: Modulate with a negative value (flip polarity)
        carrier_in = -16'd10000;
        #10;

        // Test Case: Zero out
        signal_in = 0;
        #10;

        $finish;
    end
endmodule

iVerilog lets us compile the ring modulator example along with its test setup. I need create a empty file test_dff before.

/foss/tools/iverilog/bin/iverilog -o test_dff /foss/designs/RingModulator_verilog/ring_modulator.v ring_modulator_tb.v

test iVerilog

Finally, run the verification tests.

/foss/tools/iverilog/bin/vvp /foss/designs/RingModulator_verilog/test_dff

Run test

And open the waveform dump in GTKWave to visualize the signals.

/foss/tools/gtkwave/bin/gtkwave /foss/designs/RingModulator_verilog/ring_modulator.vcd

GTKWave

GTKWave

Testbech with ChatPGT

This testbench (generated by ChatGPT) drives the ring_modulator with two sinusoidal inputs to make the simulation look closer to a real audio-style ring modulation scenario. It creates a 100 MHz clock, applies an active-low reset, and instantiates the DUT. For visibility and debugging, it also produces a waveform dump (dump.vcd) that can be opened in GTKWave, and it attempts to log key values into a CSV file (ringmod_sine.csv) so the signals can be plotted externally.

During the main loop, ChatGPT’s testbench computes two sine waves using real math and $sin(), converts them to signed Q1.15 fixed-point with real_to_q15, and assigns them to signal_in and carrier_in. Because the RTL effectively introduces a two-cycle alignment (due to the registered product and how mod_out is derived), the testbench keeps a short delay line of past samples and calculates an expected output (exp_out) using the same bit-slicing ([30:15]) as the DUT. Each iteration writes the current inputs, the DUT output, and the expected value to the CSV (when available), making it easy to validate the behavior and inspect waveforms.

`timescale 1ns/1ps

module tb_ring_modulator_sine;

  reg clk;
  reg rst_n;

  reg  signed [15:0] signal_in;
  reg  signed [15:0] carrier_in;
  wire signed [15:0] mod_out;

  ring_modulator dut (
    .clk(clk),
    .rst_n(rst_n),
    .signal_in(signal_in),
    .carrier_in(carrier_in),
    .mod_out(mod_out)
  );

  // 100 MHz clock
  initial clk = 1'b0;
  always #5 clk = ~clk;

  // VCD dump
  initial begin
    $dumpfile("dump.vcd");
    $dumpvars(0, tb_ring_modulator_sine);
  end

  // real -> Q1.15
  function signed [15:0] real_to_q15;
    input real x;
    integer tmp;
    begin
      if (x >  0.999969) x =  0.999969;
      if (x < -1.0)      x = -1.0;
      tmp = $rtoi(x * 32768.0);
      real_to_q15 = tmp[15:0];
    end
  endfunction

  // parameters for sine generation
  real fs, f_sig, f_car, A_sig, A_car;

  integer n;
  integer fcsv;

  // expected alignment (tu RTL tiene 2 ciclos “efectivos”)
  reg signed [15:0] sig_d0, sig_d1;
  reg signed [15:0] car_d0, car_d1;
  reg signed [31:0] exp_prod;
  reg signed [15:0] exp_out;

  initial begin
    // Config (frecuencias “conceptuales” para el seno)
    fs    = 48000.0;
    f_sig = 200.0;
    f_car = 1000.0;
    A_sig = 0.9;     // súbelo para que se vea bien
    A_car = 0.9;

    // Init
    rst_n      = 1'b0;
    signal_in  = 16'sd0;
    carrier_in = 16'sd0;
    sig_d0 = 0; sig_d1 = 0;
    car_d0 = 0; car_d1 = 0;

    // CSV (si no se puede abrir, lo verás en consola)
    fcsv = $fopen("ringmod_sine.csv", "w");
    if (fcsv == 0) begin
      $display("ERROR: cannot open ringmod_sine.csv for writing. Check permissions / working directory.");
    end else begin
      $fwrite(fcsv, "n,signal_in,carrier_in,mod_out,exp_out\n");
      $display("CSV opened OK: ringmod_sine.csv");
    end

    // Reset
    repeat (5) @(posedge clk);
    rst_n = 1'b1;

    // Run
    for (n = 0; n < 2000; n = n + 1) begin
      real t, s1, s2;

      @(posedge clk);

      t  = n / fs;
      s1 = A_sig * $sin(2.0*3.141592653589793*f_sig*t);
      s2 = A_car * $sin(2.0*3.141592653589793*f_car*t);

      // BLOQUEANTE (=) para que el valor cambie inmediatamente en este timestep
      // (más fácil para ver y para alinear delays en el TB)
      signal_in  = real_to_q15(s1);
      carrier_in = real_to_q15(s2);

      // delay line para alinear con la latencia del DUT
      sig_d1 = sig_d0; sig_d0 = signal_in;
      car_d1 = car_d0; car_d0 = carrier_in;

      exp_prod = sig_d1 * car_d1;
      exp_out  = exp_prod[30:15];

      if (fcsv != 0) begin
        $fwrite(fcsv, "%0d,%0d,%0d,%0d,%0d\n", n, signal_in, carrier_in, mod_out, exp_out);
      end
    end

    if (fcsv != 0) begin
      $fclose(fcsv);
      $display("CSV closed OK.");
    end

    $display("Done. Generated dump.vcd and ringmod_sine.csv");
    #20;
    $finish;
  end

endmodule

I use iverilog -g2012 -o sim ring_modulator.v ring_modulator_tb.v to compile the design and testbench in SystemVerilog 2012 mode. The -g2012 flag enables newer language features (like declaring variables inside procedural blocks, int, string, and other SystemVerilog syntax) that plain Verilog-2001 would reject. The -o sim part names the compiled simulation executable sim, and the two .v files are the RTL module and its testbench.

Then vvp sim executes that simulation, which produces the waveform dump file (dump.vcd).

Finally, gtkwave dump.vcd opens the VCD file so we can inspect and debug the signal waveforms visually.

iverilog -g2012 -o sim ring_modulator.v ring_modulator_tb.v
vvp sim
gtkwave dump.vcd

alt text

In GTKWave, I used the zoom controls to focus on the region of interest in time: first I zoomed out to get an overview of the whole simulation, then zoomed in around a few periods of the waveform so the signal shape was clearly visible. I also used the mouse to drag/select a time range and zoom directly into that window, which makes it much easier to inspect details like amplitude and timing.

alt text

To make the sine-like behavior readable, I changed the signal display format from a digital bus view to an analog-style plot. After adding the signals (e.g., signal_in, carrier_in, and mod_out) to the waveform pane, I right-clicked on the signal and set its data format to Analog → Analog Step. This makes GTKWave render the 16-bit signed samples as a stepped analog curve, so the sinusoidal shape becomes visible instead of just changing numeric values.

alt text

alt text

CSV in Jupiter

I recreated the signal visualization in a Jupyter Notebook by loading the simulation results from the exported CSV file using Python. With pandas I parsed the columns for signal_in, carrier_in, and mod_out, converted the Q1.15 integer values into floating-point amplitudes by dividing by 32768, and then used matplotlib to plot each waveform. To make the shapes easier to read, I also zoomed into a smaller range of samples and generated separate plots for the input signal, the carrier, and the modulated output.

import pandas as pd
import matplotlib.pyplot as plt

# -----------------------------
# 1) Load the CSV generated by the testbench
# -----------------------------
csv_path = "ringmod_sine.csv"  # change this if your file has a different name
df = pd.read_csv(csv_path)

# -----------------------------
# 2) Ensure the main columns are numeric
# -----------------------------
for c in ["n", "signal_in", "carrier_in", "mod_out"]:
    df[c] = pd.to_numeric(df[c], errors="coerce")

# -----------------------------
# 3) Convert Q1.15 fixed-point to float
#    (Q1.15 uses 32768 as the scaling factor)
# -----------------------------
Q15_SCALE = 32768.0
df["signal_in_f"]  = df["signal_in"]  / Q15_SCALE
df["carrier_in_f"] = df["carrier_in"] / Q15_SCALE
df["mod_out_f"]    = df["mod_out"]    / Q15_SCALE

# -----------------------------
# 4) Zoom into a smaller sample window to see the waveforms more clearly
#    Adjust 'start' and 'span' as needed
# -----------------------------
start = 0     # starting sample index
span  = 400   # number of samples to display (zoom window size)

zoom = df[(df["n"] >= start) & (df["n"] < start + span)].copy()

# -----------------------------
# 5) Plot signal_in (zoomed)
# -----------------------------
plt.figure()
plt.plot(zoom["n"], zoom["signal_in_f"])
plt.title("signal_in (zoomed)")
plt.xlabel("Sample index (n)")
plt.ylabel("Amplitude (Q1.15 -> float)")
plt.grid(True)
plt.show()

# -----------------------------
# 6) Plot carrier_in (zoomed)
# -----------------------------
plt.figure()
plt.plot(zoom["n"], zoom["carrier_in_f"])
plt.title("carrier_in (zoomed)")
plt.xlabel("Sample index (n)")
plt.ylabel("Amplitude (Q1.15 -> float)")
plt.grid(True)
plt.show()

# -----------------------------
# 7) Plot mod_out (zoomed)
# -----------------------------
plt.figure()
plt.plot(zoom["n"], zoom["mod_out_f"])
plt.title("mod_out (zoomed)")
plt.xlabel("Sample index (n)")
plt.ylabel("Amplitude (Q1.15 -> float)")
plt.grid(True)
plt.show()

Jupiter waves