Project Architecture and Build Infrastructure
The development environment follows a standardized directory hierarchy to separate hardware description, testbench logic, constraint mapping, and build artifacts. Each multiplexer variant operates within this unified structure.
project_root/
├── constr/
│ └── top.nxdc
├── csrc/
│ ├── test_nvboard.cpp
│ └── test_wave.cpp
├── Makefile
├── vsrc/
│ └── top.v
├── obj_dir/
└── dump.vcd
The Makefile coordinates Verilator compilation, linking against the C++ testbenches, and generating executable simulation targets. Constraint files (.nxdc) map abstract Verilog ports to physical or virtual peripherals.
Implementation 1: 2-to-1 Multiplexer
Hardware Description
The 2-to-1 selector routes one of two single-bit inputs to a single output based on a control signal. The implementation utilizes a combinatorial always block with explicit conditional assignment.
module mux_2to1 (
input wire sel,
input wire in_a,
input wire in_b,
output wire mux_out
);
always_comb begin
if (sel == 1'b0) begin
mux_out = in_a;
end else begin
mux_out = in_b;
end
end
endmodule
Pin Constraints
top = mux_2to1
mux_out LD0
sel BTNL
in_a BTNR
in_b BTNU
C++ Testbench: NVBoard Interface
#include <nvboard.h>
#include <Vtop.h>
static Vtop sim_dut;
void map_hardware_pins(Vtop* instance);
void run_clock_cycle();
int main() {
map_hardware_pins(&sim_dut);
nvboard_init();
while (true) {
nvboard_update();
run_clock_cycle();
}
}
void run_clock_cycle() {
sim_dut.eval();
}
C++ Testbench: Waveform Generation
#include "verilated.h"
#include "verilated_vcd_c.h"
#include "obj_dir/Vtop.h"
VerilatedContext* sim_ctx = nullptr;
VerilatedVcdC* vcd_tracer = nullptr;
Vtop* design_instance = nullptr;
void advance_and_record() {
design_instance->eval();
sim_ctx->timeInc(1);
vcd_tracer->dump(sim_ctx->time());
}
void initialize_simulation() {
sim_ctx = new VerilatedContext;
vcd_tracer = new VerilatedVcdC;
design_instance = new Vtop;
sim_ctx->traceEverOn(true);
design_instance->trace(vcd_tracer, 0);
vcd_tracer->open("trace_2to1.vcd");
}
void shutdown_simulation() {
advance_and_record();
vcd_tracer->close();
delete vcd_tracer;
delete design_instance;
delete sim_ctx;
}
int main() {
initialize_simulation();
design_instance->sel = 0; design_instance->in_a = 0; design_instance->in_b = 0; advance_and_record();
design_instance->in_b = 1; advance_and_record();
design_instance->sel = 1; design_instance->in_a = 0; design_instance->in_b = 0; advance_and_record();
design_instance->in_b = 1; advance_and_record();
shutdown_simulation();
return 0;
}
Implementation 2: 4-to-1 Multiplexer
Hardware Description
This variant selects among four single-bit data lines using a 2-bit control vector. The logic is expressed via a case statement inside a combinatorial procedural block.
module mux_4to1 (
input wire [1:0] ctrl,
input wire d0, d1, d2, d3,
output wire out
);
always_comb begin
unique case (ctrl)
2'b00: out = d0;
2'b01: out = d1;
2'b10: out = d2;
2'b11: out = d3;
endcase
end
endmodule
Pin Constraints
top = mux_4to1
out LD0
ctrl SW1 SW0
d0 SW5
d1 SW4
d2 SW3
d3 SW2
C++ Testbench Adaptation
For the waveform generator, the input initialization block is modified to cover all selector combinations. The NVBoard template remains structural identical; only the pin mapping in test_nvboard.cpp and the constraint file require updates.
// Inside main() of test_wave.cpp
initialize_simulation();
design_instance->ctrl = 2'b00; design_instance->d0 = 1; design_instance->d1 = 0; design_instance->d2 = 1; design_instance->d3 = 0; advance_and_record();
design_instance->ctrl = 2'b01; advance_and_record();
design_instance->ctrl = 2'b10; advance_and_record();
design_instance->ctrl = 2'b11; advance_and_record();
shutdown_simulation();
Implementation 3: 2-Bit 4-to-1 Multiplexer
Hardware Description
Widening the data path requires declaring multi-bit vectors for inputs and outputs. The selection logic remains identical but operates on 2-bit chunks. The output reg declaration is replaced with output logic for modern Verilog compliance.
module mux_4to1_wide (
input wire [1:0] sel,
input wire [1:0] ch0, ch1, ch2, ch3,
output logic [1:0] result
);
always_comb begin
case (sel)
2'b00: result = ch0;
2'b01: result = ch1;
2'b10: result = ch2;
2'b11: result = ch3;
endcase
end
endmodule
Pin Constraints
top = mux_4to1_wide
result[1:0] LD1 LD0
sel[1:0] SW1 SW0
ch0[1:0] SW3 SW2
ch1[1:0] SW5 SW4
ch2[1:0] SW7 SW6
ch3[1:0] SW9 SW8
C++ Testbench Adaptasion
Multi-bit assignments in the C++ driver use binary literals to match the 2-bit width. The simulation loop structure is preserved.
// Inside main() of test_wave.cpp
initialize_simulation();
design_instance->sel = 2'b00; design_instance->ch0 = 2'b11; design_instance->ch1 = 2'b00; design_instance->ch2 = 2'b10; design_instance->ch3 = 2'b01; advance_and_record();
design_instance->sel = 2'b01; advance_and_record();
design_instance->sel = 2'b10; advance_and_record();
design_instance->sel = 2'b11; advance_and_record();
shutdown_simulation();
Simulation and Deployment Commands
Compilation and execution follow a consistent sequence regardless of the specific multiplexer variant. Ensure the vsrc/top.v file contains the target module before running the toolchain.
# Step 1: Generate C++ simulation model from Verilog
verilator --cc vsrc/top.v --trace --exe csrc/test_wave.cpp
# Step 2: Compile the generated C++ code into an executable
make -C obj_dir -f Vtop.mk
# Step 3: Run simulation to produce VCD output
./obj_dir/Vtop
# Step 4: Visualize timing diagrams
gtkwave dump.vcd
For hardware deployment using the NVBoard framework, replace the wave testbench with the NVBoard variant during compilation:
verilator --cc vsrc/top.v --exe csrc/test_nvboard.cpp
make -C obj_dir -f Vtop.mk
./obj_dir/Vtop