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/endare optional; with multiple statements,begin/endare used to group them. - Statements inside an
initialblock execute sequentially, and delay controls like#Npause the block for N time units before continuing. This makes it useful for creating timed stimulus in a testbench. - A module can include multiple
initialblocks, and they all start at time 0 and run in parallel with each other (and with anyalwaysblocks). - It’s common to use
initialfor 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 / elselets me choose which statements execute based on a condition. For multiple statements per branch, I need to group them withbegin ... end. I also need to be careful with nestedifstatements because an omittedelsecan make it unclear whichiftheelsebelongs to—usingbegin/endhelps avoid that ambiguity. -
Loops provide repetition, but each loop has different semantics:
-
foreverrepeats indefinitely (easy to hang a simulation if I don’t include time control). repeat(N)runs a fixed number of iterations; ifNevaluates toXorZ, it’s treated as zero. There’s also a common testbench pattern likerepeat(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 anelse(or failing to assign a default) can infer latches. forever/whilewithout 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
forloop 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:
-
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)
- Blocking
-
Continuous assignments using
assignThese 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. -
Procedural continuous assignments (
assign/deassignandforce/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
initialblock 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

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 / 1pssets the simulation time unit to 1 ns and the time precision to 1 ps, so delays like#5and#10are interpreted in nanoseconds. -
The testbench declares the DUT signals:
-
clkandrst_nas control signals, signal_inandcarrier_inas signed 16-bit stimulus inputs,-
mod_outas a signed 16-bit output wire. -
The
ring_modulatoris instantiated asuut(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
initialblock, the test starts by initializing everything to zero and asserting reset (rst_n = 0). After#10ns, reset is released by settingrst_n = 1. -
Then it applies a few basic test cases, each held for
#10ns (roughly one clock cycle at this clock rate): -
Positive modulation:
signal_in = 5000andcarrier_in = 10000to check that the output becomes a positive scaled product. - Negative carrier:
carrier_in = -10000while keepingsignal_inpositive, to confirm the output flips polarity (the product becomes negative). -
Zero input:
signal_in = 0to 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

Finally, run the verification tests.
/foss/tools/iverilog/bin/vvp /foss/designs/RingModulator_verilog/test_dff

And open the waveform dump in GTKWave to visualize the signals.
/foss/tools/gtkwave/bin/gtkwave /foss/designs/RingModulator_verilog/ring_modulator.vcd


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
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.
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.

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()
