Create Your First RAD-Sim Module
Overview
In this page, we will walkthrough the steps to create a minimal RAD-sim producer_consumer module that sends a series of integers to a reciever over the NoC, to help you create/adapt your own designs.
Here’s a one line summary for each module we will create:
sender: specifies how to send the integers via AXI master interface to the NoC.receiver: specifies how to receive the integer from the NoC via AXI slave interfaceproducer_consumer_top: connects the sender and receiver modules together, and instantiates the NoC according to specs.producer_consumer_system: usually connects top level with testbench, in this case we don’t have a seperate testbench, so only top level is instantiated.
We will also create the following files for configuring the design:
producer_consumer.clks: specifies the clock and reset signals for the design.producer_consumer.place: specifies the placement of the modules on the NoCconfig.yml: specifies the NoC parameters, RAD sepecific parameters and their placement on RAD clusters.CMakeLists.txt: A filelist that specifies the source and header files for the design.
Before we start, let’s create our working directory first:
$ mkdir <rad_flow_root_dir>/rad-sim/example-designs/producer_consumer/modules
Sender Module
The sender module sends an integer to a recevier via the NoC, it should inherit from the RADSimModule class, instantiate an AXI master interface, and override the RegisterModuleInfo method to register the AXI master port. We should also have methods for its sequential and combinational logic.
Let’s create a header file accordingly under <rad_flow_root_dir>/rad-sim/example-designs/producer_consumer/modules/sender.hpp
#pragma once #include <axis_interface.hpp> #include <design_context.hpp> #include <radsim_defines.hpp> #include <radsim_module.hpp> #include <string> #include <systemc.h> #include <vector> #include <radsim_utils.hpp> #include <fstream> #include <iostream> class sender : public RADSimModule { private: std::ofstream outfile; //the total amount of integers to send int amount_to_send; //wait this many cycles after sending the last integer before signaling done int wait_cycles; public: //The context contain utilities for generating design, NOC, AXI adaptor clks, connecting AXI interfaces to the NoC, etc. RADSimDesignContext* radsim_design; sc_in<bool> rst; // Interface to the NoC axis_master_port axis_sender_interface; sender(const sc_module_name &name, RADSimDesignContext* radsim_design, int amount_to_send = 10, int wait_cycles = 50); ~sender(); void Assign(); // Combinational logic process void Tick(); // Sequential logic process SC_HAS_PROCESS(sender); void RegisterModuleInfo(); };
Next, we implement these methods in <rad_flow_root_dir>/rad-sim/example-designs/producer_consumer/modules/sender.cpp. Let’s walk through each method:
Starting with the constructor, destructor and RegisterModuleInfo, the important thing here is to mark our combinitional and sequential methods(Assign and Tick) with the appropriate SystemC macros, and to register the AXI master port with the design context. Note that we must have RegisterModuleInfo seperately since it is required by the flow, the destructor is also necessary since the parent class defined it as virtual method.
sender::sender(const sc_module_name &name, RADSimDesignContext* radsim_design, int amount_to_send, int wait_cycles) : RADSimModule(name, radsim_design) { this->radsim_design = radsim_design; this->amount_to_send = amount_to_send; this->wait_cycles = wait_cycles; SC_METHOD(Assign); //Sets sensitivity for combinational logic in Assign method, similiar to always_comb block in System Verilog sensitive << rst; SC_CTHREAD(Tick, clk.pos()); reset_signal_is(rst, true); // Reset is active high RegisterModuleInfo(); //starts a file to record the output, later we can use this to verify on the receiver side outfile.open("sender_output.txt"); } sender::~sender() { // Destructor does not need to do anything for this module if (outfile.is_open()) { outfile.close(); } } void sender::RegisterModuleInfo() { std::string port_name = module_name + ".axis_sender_interface"; RegisterAxisMasterPort(port_name, &axis_sender_interface, 128, 0); }
Next, we can put any combinational logic in the Assign method, in this example it doesn’t need any, this is purely to show where to do it.
void sender::Assign() { //cmbination logical here }
Finally, we implement the sequential logic in the Tick method, which is where we send the integer to the NoC.
A couple things to note here:
This is a very simplified implementation where we just send a sequence of numbers, it’s meant only to demonstrate the flow.
For AXIS interface, we need to concatinate the remote node ID, local node ID and RAD ID into a single destination ID, which is then written to the
tdestport of the AXIS interface.We also need to supply the src_addr to the
tuserport, which in RADSim is used for tracking the AXIS packet.void sender::Tick() { if(rst) { axis_sender_interface.tvalid.write(false); } wait(); int data_left = amount_to_send; unsigned int placeholderData = 0; while (data_left > 0) { //collect the src and destination data, then write to the AXIS interface std::string src_port_name = module_name + ".axis_sender_interface"; std::string dst_port_name = "receiver_inst.axis_receiver_interface"; uint64_t dst_addr = radsim_design->GetPortDestinationID(dst_port_name); uint64_t src_addr = radsim_design->GetPortDestinationID(src_port_name); sc_bv<AXIS_DESTW> dest_id_concat; DEST_REMOTE_NODE(dest_id_concat) = 0; // staying on same RAD DEST_LOCAL_NODE(dest_id_concat) = dst_addr; DEST_RAD(dest_id_concat) = radsim_design->rad_id; placeholderData += 1; axis_sender_interface.tdest.write(dest_id_concat); axis_sender_interface.tid.write(0); axis_sender_interface.tstrb.write(0); axis_sender_interface.tkeep.write(0); axis_sender_interface.tuser.write(src_addr); //prints the output at each cycle std::cout << "@cycle " << GetSimulationCycle(radsim_config.GetDoubleKnobShared("sim_driver_period")) << " Sending " << placeholderData << " to destination: " << dst_addr << std::endl; if(data_left > 1) { axis_sender_interface.tlast.write(0); // Not the last data } else { axis_sender_interface.tlast.write(1); // Last data } axis_sender_interface.tdata.write(placeholderData); axis_sender_interface.tvalid.write(true); wait(); //records the data sent outfile << placeholderData << std::endl; data_left--; } axis_sender_interface.tvalid.write(false); // Signal that no more data will be sent int wait_start_cycle = GetSimulationCycle(radsim_config.GetDoubleKnobShared("sim_driver_period")); int curr_cycle = wait_start_cycle; while(curr_cycle < wait_start_cycle + wait_cycles) { curr_cycle = GetSimulationCycle(radsim_config.GetDoubleKnobShared("sim_driver_period")); wait(); } //signals the end of simulation for this module this->radsim_design->set_rad_done(); }
Reciever Module
The reciever pretty much mirrors the same template, except it uses an AXI slave interface to receive the integer from the NoC.
Let’s create a header file accordingly under <rad_flow_root_dir>/rad-sim/example-designs/producer_consumer/modules/receiver.hpp
#pragma once #include <axis_interface.hpp> #include <design_context.hpp> #include <queue> #include <radsim_defines.hpp> #include <radsim_module.hpp> #include <string> #include <systemc.h> #include <vector> #include <radsim_utils.hpp> #include <tuple> #include <fstream> #include <iostream> class receiver: public RADSimModule { private: std::ifstream refFile; bool errorFlag; public: RADSimDesignContext* radsim_design; sc_in<bool> rst; // Interface to the NoC axis_slave_port axis_receiver_interface; receiver(const sc_module_name &name, RADSimDesignContext* radsim_design); ~receiver(); void Tick(); // Sequential logic process SC_HAS_PROCESS(receiver); void RegisterModuleInfo(); };
Then we implement these methods in <rad_flow_root_dir>/rad-sim/example-designs/producer_consumer/modules/receiver.cpp
#include <receiver.hpp> receiver::receiver(const sc_module_name &name, RADSimDesignContext* radsim_design) : RADSimModule(name, radsim_design) { this->radsim_design = radsim_design; sensitive << rst; SC_CTHREAD(Tick, clk.pos()); reset_signal_is(rst, true); // Reset is active high RegisterModuleInfo(); refFile.open("sender_output.txt"); } receiver::~receiver() { //check if any data is left in the reference file std::string line; if(std::getline(refFile, line)) { std::cout << "Some data have not arrived!" << std::endl; errorFlag = true; } if (errorFlag) { std::cerr << "Error occurred during data reception!" << std::endl; } else { std::cout << "Data reception completed successfully!" << std::endl; } refFile.close(); } void receiver::Tick() { if (rst) { axis_receiver_interface.tready.write(false); } else { axis_receiver_interface.tready.write(true); // Ready to receive data } wait(); errorFlag = false; while (true) { if (axis_receiver_interface.tvalid.read() && axis_receiver_interface.tready.read()) { unsigned int tdata = axis_receiver_interface.tdata.read().range(31, 0).to_uint();; bool tlast = axis_receiver_interface.tlast.read(); std::cout << "Received Data: " << std::to_string(tdata) << ", Last: " << tlast << std::endl; //compare with reference file if (refFile.is_open()) { std::string line; if (std::getline(refFile, line)) { if (line != std::to_string(tdata)) { std::cout << "Data mismatch! Expected: " << line << ", Received: " << std::to_string(tdata) << std::endl; errorFlag = true; } } else { std::cout << "No more lines in reference file." << std::endl; errorFlag = true; } } else { std::cerr << "Reference file not open!" << std::endl; errorFlag = true; } } wait(); } } void receiver::RegisterModuleInfo() { std::string port_name = module_name + ".axis_receiver_interface"; RegisterAxisSlavePort(port_name, &axis_receiver_interface, 128, 0); }
Producer_Consumer Top
The producer_consumer top module connects the sender and receiver modules together, and instantiates the NoC according to specs. It should inherit from RADSimDesignTop
Let’s define header file at <rad_flow_root_dir>/rad-sim/example-designs/producer_consumer/producer_consumer_top.hpp
#pragma once #include <radsim_config.hpp> #include <sender.hpp> #include <receiver.hpp> #include <systemc.h> #include <vector> #include <design_top.hpp> #include <axis_interface.hpp> class producer_consumer_top : public RADSimDesignTop { private: sender *sender_inst; receiver *receiver_inst; RADSimDesignContext* radsim_design; public: sc_in<bool> rst; producer_consumer_top(const sc_module_name &name, RADSimDesignContext* radsim_design); ~producer_consumer_top(); };
Then implement at at <rad_flow_root_dir>/rad-sim/example-designs/producer_consumer/producer_consumer_top.cpp, notice that we dump the NoC througput stats at the destructor
#include <producer_consumer_top.hpp> producer_consumer_top::producer_consumer_top(const sc_module_name &name, RADSimDesignContext* radsim_design) : RADSimDesignTop(radsim_design) { this->radsim_design = radsim_design; std::string module_name_str; char module_name[25]; // Create the sender and receiver instances module_name_str = "sender_inst"; std::strcpy(module_name, module_name_str.c_str()); sender_inst = new sender(module_name, radsim_design); sender_inst->rst(rst); module_name_str = "receiver_inst"; std::strcpy(module_name, module_name_str.c_str()); receiver_inst = new receiver(module_name, radsim_design); receiver_inst->rst(rst); // Connect the sender and receiver to the design context this->connectPortalReset(&rst); radsim_design->BuildDesignContext("producer_consumer.place", "producer_consumer.clks"); radsim_design->CreateSystemNoCs(rst); radsim_design->ConnectModulesToNoC(); } //Dumps Noc Throughput stats producer_consumer_top::~producer_consumer_top() { end_cycle = GetSimulationCycle(radsim_config.GetDoubleKnobShared("sim_driver_period")); NoCTransactionTelemetry::DumpStatsToFile("stats.csv"); NoCFlitTelemetry::DumpNoCFlitTracesToFile("flit_traces.csv"); std::vector<double> aggregate_bandwidths = NoCTransactionTelemetry::DumpTrafficFlows("traffic_flows", end_cycle - start_cycle, radsim_design->GetNodeModuleNames(), radsim_design->rad_id); std::cout << "Aggregate NoC BW = " << aggregate_bandwidths[0] / 1000000000 << " Gbps" << std::endl; delete sender_inst; delete receiver_inst; }
Producer_Consumer System
This is the last and topmost module we have, usually this connects the driver and other testbench components with the top level design, in this case we don’t have a separate testbench, so only top level is instantiated.
Let’s define the header file at <rad_flow_root_dir>/rad-sim/example-designs/producer_consumer/producer_consumer_system.hpp
#pragma once #include <producer_consumer_top.hpp> #include <chrono> #include <vector> #include <design_system.hpp> class producer_consumer_system : public RADSimDesignSystem { public: sc_signal<bool> rst_sig; sc_clock *sysclk; producer_consumer_top *dut_inst; producer_consumer_system(const sc_module_name &name, sc_clock *driver_clk_sig, RADSimDesignContext* radsim_design); ~producer_consumer_system(); };
Then implement at <rad_flow_root_dir>/rad-sim/example-designs/producer_consumer/producer_consumer_system.cpp
#include <producer_consumer_system.hpp> producer_consumer_system::producer_consumer_system(const sc_module_name &name, sc_clock *driver_clk_sig, RADSimDesignContext* radsim_design) : sc_module(name) { // Instantiate design top-level dut_inst = new producer_consumer_top("dut", radsim_design); dut_inst->rst(rst_sig); this->design_dut_inst = dut_inst; } producer_consumer_system::~producer_consumer_system() { delete dut_inst; delete sysclk; }
Configurations
There are a couple configuration files needed for each design, the complete structure of each is covered in Code Structure, only important settings will be explained here.
producer_consumer.place:
Here we set the sender to position 0 of NOC 0, and the reciever to position 3 of the same NOC, axis means we’re using AXI Streaming interface, we can also use aximm(AXI Memory Mapped) interface if needed.
sender_inst 0 0 axis receiver_inst 0 3 axis
producer_consumer.clks:
Here we set sender and reciever to the clk frequency(5 ns period as defined in config.yml)
sender_inst 0 0 receiver_inst 0 0
config.yml:
This YAML file configures all the RAD-Sim parameters for the simulation of the application design under 4 main tags:
noc, noc_adapters, config <configname>, and cluster. The noc and noc_adapters parameters are shared across all RADs.
There may be multiple config <configname> sections, each describing a RAD configuration that can be applied to a single or multiple devices in the cluster.
The cluster tag describes the cluster of RADs, including the number of RADs and their configurations.
This file should be located in the same directory as the config.py script. For a new design, you should copy
the config.yml file from one of the provided example design directories and make modifications for your use case.
Note that the parameters within a config <configname> subsection can be applied to a single RAD or shared among multiple RADs.
An example configuration file is shown below, followed by an explanation for each configuration paramete
config rad1: design: name: 'producer_consumer' noc_placement: ['producer_consumer.place'] clk_periods: [5.0] noc: type: ['2d'] num_nocs: 1 clk_period: [1.0] payload_width: [166] topology: ['mesh'] dim_x: [4] dim_y: [4] routing_func: ['dim_order'] vcs: [5] vc_buffer_size: [8] output_buffer_size: [8] num_packet_types: [5] router_uarch: ['iq'] vc_allocator: ['islip'] sw_allocator: ['islip'] credit_delay: [1] routing_delay: [1] vc_alloc_delay: [1] sw_alloc_delay: [1] noc_adapters: clk_period: [1.25] fifo_size: [16] obuff_size: [2] in_arbiter: ['fixed_rr'] out_arbiter: ['priority_rr'] vc_mapping: ['direct'] cluster: sim_driver_period: 5.0 telemetry_log_verbosity: 2 telemetry_traces: [] num_rads: 1 cluster_configs: ['rad1']
CmakeLists.txt:
This is a filelist that specifies the source and header files for the design, as well as other includes
cmake_minimum_required(VERSION 3.16) find_package(SystemCLanguage CONFIG REQUIRED) include_directories( ./ modules ../../sim ../../sim/noc ../../sim/noc/booksim ../../sim/noc/booksim/networks ../../sim/noc/booksim/routers ) set(srcfiles modules/sender.cpp modules/receiver.cpp producer_consumer_system.cpp producer_consumer_top.cpp ) set(hdrfiles modules/sender.hpp modules/receiver.hpp producer_consumer_system.hpp producer_consumer_top.hpp ) add_compile_options(-Wall -Wextra -pedantic) add_library(producer_consumer STATIC ${srcfiles} ${hdrfiles}) target_link_libraries(producer_consumer PUBLIC SystemC::systemc booksim noc)
Running the Design
Run the following commands in the terminal:
$ cd <rad_flow_root_dir>/rad-sim/
$ python config.py producer_consumer
$ cd build
$ cmake ..
$ make
Expected output:
placement_filepath: /home/kerry/rad-flow/rad-sim/example-designs/producer_consumer/producer_consumer.place
@cycle 1 Sending 1 to destination: 1
@cycle 2 Sending 2 to destination: 1
@cycle 3 Sending 3 to destination: 1
@cycle 4 Sending 4 to destination: 1
@cycle 5 Sending 5 to destination: 1
@cycle 6 Sending 6 to destination: 1
@cycle 7 Sending 7 to destination: 1
Received Data: 1, Last: 0
@cycle 8 Sending 8 to destination: 1
Received Data: 2, Last: 0
@cycle 9 Sending 9 to destination: 1
Received Data: 3, Last: 0
@cycle 10 Sending 10 to destination: 1
Received Data: 4, Last: 0
Received Data: 5, Last: 0
Received Data: 6, Last: 0
Received Data: 7, Last: 0
Received Data: 8, Last: 0
Received Data: 9, Last: 0
Received Data: 10, Last: 1
Info: /OSCI/SystemC: Simulation stopped by user.
Simulation Cycles from main.cpp = 63
Aggregate NoC BW = 4.06349 Gbps
Data reception completed successfully!