System C Support in LegUp

By Zhi Li,

  Filed under: Demonstrations, Tutorials
  Comments: None

High-level synthesis using C++ works very well for designers describing data-flow applications like digital signal processing or video/image processing. But for certain control heavy applications, such as a bus controller, a designer will have trouble describing the cycle accurate behavior of the hardware in C++. We decided to help fix this problem by adding support for System C as an input language to LegUp HLS.

System C is a standard C++ library that allows a designer to specify cycle-accurate behavior. Typically System C is used for system-level modeling of a larger system. System C offers the advantage of faster simulation time than running RTL simulations while still being able to measure cycle-accurate behavior.

System C explicitly describes the cycle behavior of the application. There is no flexibility for the operations to be scheduled into different clock cycles like when C++ is used in LegUp HLS. This provides the user with very fine-grained control over the behavior of the generated hardware.

AHB-Lite Bus Slave Controller Example

For an example of a control-heavy application, we will implement a simplified AHB-Lite bus slave controller in System C. We will focus on how to implement the part of the bus protocol that requires cycle-accurate control: the error response code.

For background, the AHB-Lite bus protocol has two possible responses (HRESP) from the slave to the master. First, if the transaction was successful the HRESP will be OKAY (0) but if there was an error there will be a two cycle delay and HRESP will be ERROR (1). See the waveforms below:

AHB-Lite HRESP Error

Figure 1: AHB-Lite HRESP Error Response (Image source: AMBA 3 AHB-Lite 1.0 Spec)

 

The 2-cycle delay behavior when an error occurs is hard for a designer to express in typical LegUp HLS C++ code. But with System C, we can use the wait() function to introduce a 1-cycle delay.

 

Example Source Code

The example composes two components, the AHB-Lite Slave Controller and the user-defined slave module. As shown in the figure below, the AHB-Lite Slave Controller communicates with the AHB-Lite bus and converts the requests and responses to a set of Ready-Valid-Data (RVD) interfaces. The controller abstracts away the details of the AHB-Lite protocol and enable users to process requests without worrying the spec of the protocol.

AHBL-example-block-diagram

Figure 2: Block Diagram for AHB-Lite Example

 

As we mentioned earlier, the example shows a simplified version of the AHB-Lite slave. So only the necessary signals are implemented in this example. A snippet of the structure definitions for the AHB-Lite signals is shown below:


struct ReqFromM {
    ap_uint<32> HADDR;
    ap_uint<1> HWRITE;
};

struct ReqToS {
    ap_uint<1> HSEL;
    ap_uint<1> HREADY;
    ap_uint<32> HADDR;
    ap_uint<1> HWRITE;
};

struct Resp {
    enum { OKAY = 0, ERROR };
    ap_uint<1> HRESP;
    ap_uint<1> HREADY;
};

Now, let’s look at the System C code that implements the slave controller. As shown in the block diagram, the slave controller contains two sets of interfaces: one is for AHB-Lite signals, and the other one is for the RVD signals that communicates with the user-defined module. Note that we have specified the depth of FIFOs to be 0 to indicate they are wire connections with ready, valid and data signals. Wire connections are important because we don’t want to add any latency to the communication between the slave controller and the user-defined module.

The core implementation of the slave controller is inside the run(), which is specified as an SC_THREAD in the constructor. For every iteration of the while-loop, the slave controller processes one request from the AHB-Lite bus. All of the FIFO reads/writes are blocking operations, so the execution will be blocked if the user-defined module is not ready to process the request or to send back the response. Before the slave controller forwards the request to the user-defined module, the S_RESP_OUT output is pre-set to a value of HRESP = OKAY and HREADY = 0. So the AHB-Lite bus will be able to stall if the slave controller is blocked. If all the operations go through without blocking, then the iteration is executed in exactly one cycle.

The key function in the System C code is the wait(). This function means the execution should be suspended until the start of the next cycle. There is a wait() at the beginning of the while-loop to indicate that we process only one iteration in one cycle. If an error occurs, the slave controller will perform the 2-cycle error response. You can see how the wait() is used in line 55 and line 57 to explicitly enforce cycle-accurate behavior.


template <typename data_ty> class slave : public sc_module {
  public:
    sc_in_clk clk{"clk"};

    // AHB-Lite signals
    sc_in<ReqToS> S_REQ_IN{"S_REQ_IN"};
    sc_out<Resp> S_RESP_OUT{"S_RESP_OUT"};
    sc_in<data_ty> S_DATA_IN{"S_DATA_IN"};
    sc_out<data_ty> S_DATA_OUT{"S_DATA_OUT"};

    // APIs to user's module
    legup::fifo<ReqFromM, 0> req_path{"req_path"};
    legup::fifo<Resp, 0> resp_path{"resp_path"};
    legup::fifo<data_ty, 0> data_i_path{"data_i_path"};
    legup::fifo<data_ty, 0> data_o_path{"data_o_path"};

    SC_HAS_PROCESS(slave);
    slave(const sc_module_name &name) : sc_module(name) {
        SC_THREAD(run);
        sensitive << clk.pos();
    }

    void run() {
        wait();
        // drive HREADY to low and wait for user's module to be ready
        S_RESP_OUT.write({Resp::OKAY, 0x0});
        S_DATA_OUT.write(0);
        auto resp = resp_path.read();

        while (true) {
            // drive the response signal to {OKAY, READY} to tell the master
            // side, it's able to accept request
            S_RESP_OUT.write({Resp::OKAY, 0x1});
            wait();

            // read request from the master side
            auto req = S_REQ_IN.read();

            // initialize the response signals
            //  1. set HREADY to 0, in case the user's module is not ready yet
            //  2. reset the DATA_OUT back to 0
            S_RESP_OUT.write({Resp::OKAY, 0x0});
            S_DATA_OUT.write(0);

            // only process the request if HSEL and HREADY are both 1
            if (req.isActive()) {
                // send request command to user's module
                req_path.write({req.HADDR, req.HWRITE});

                // wait for response from the user side
                auto resp = resp_path.read();
                if (resp.HRESP == Resp::ERROR) {
                    // perform the 2-cycle error response based on the AHBL spec
                    S_RESP_OUT.write({Resp::ERROR, 0x0});
                    wait();
                    S_RESP_OUT.write({Resp::ERROR, 0x1});
                    wait();
                } else if (!req.HWRITE) {
                    // if it's a read request forward the data from user to the
                    // master
                    auto data = data_o_path.read();
                    S_DATA_OUT.write(data);
                }   
            }   
        }   
    }   
};

Next, let’s look into the top-level module that implements an AHB-Lite slave using the slave controller shown above. This slave implementation behaviors as following:

  • Always return 1 as data if it receives a read request without an error.
  • If the address is an odd number, then wait for 1 cycle before processing.
  • If the second bit of the address is 1, then consider it as an invalid request and response with error.
  • Data comes with the write request will be ignored.

The source code of the slave implementation is shown below. Note that we are using the struct Resp as the data type for the resp_path, but only the HRESP field is used and the HREADY field is ignored by the controller.


class example : public sc_module {
  public:
    sc_in_clk clk{"clk"};
    ahbl::slave<ap_uint<32> > controller{"controller"};

    SC_HAS_PROCESS(example);
    example(const sc_module_name &name) : sc_module(name) {
        controller.clk(clk);

        SC_THREAD(run);
        sensitive << clk.pos();
    }   

    void run() {
        wait();
        // notify the ahbl slave controller that it's ready to accept requests
        controller.resp_path.write({ahbl::Resp::OKAY, 0});

        while (true) {
            wait();
            auto req = controller.req_path.read();
            unsigned addr = req.HADDR;
            bool is_read = !req.HWRITE;

            // take one cycle to process access request to an odd number in address
            if (addr & 0x1) {
                wait();
            }   

            // indicate error if the 2nd bit of the address is 1
            if (addr & 0x2) {
                controller.resp_path.write({ahbl::Resp::ERROR, 0});
            } else {
                controller.resp_path.write({ahbl::Resp::OKAY, 0});
                // return 1 for any read request if it's not an error
                if (is_read)
                    controller.data_o_path.write(1);
            }   
        }   
    }   
};

Synthesizing with LegUp HLS and Simulating

You can download the source code for the AHB-Lite System C example here.

To run this using System C:

  1. First install System C. You may need to install with the flag CXXFLAGS=”-DSC_CPLUSPLUS=201103L”
  2. Modify the Makefile of the LegUp project to point to the proper location of the System C installation.
  3. Compile and execute the System C program by running “legup sw”.
  4. You should see the output:
-----------------------
check RESP_S_B - (Resp - HRESP: OKAY, HREADY: 1)
check DATA_S_B - 00000010
-----------------------
check RESP_S_B - (Resp - HRESP: OKAY, HREADY: 1)
check DATA_S_B - 00000000
example.controller - read (ReqToS - HSEL: 0, HREADY: 1, HADDR: 00000000, HWRITE: 0)
-----------------------
check RESP_S_B - (Resp - HRESP: OKAY, HREADY: 1)
check DATA_S_B - 00000000
example.controller - read (ReqToS - HSEL: 1, HREADY: 0, HADDR: 00000000, HWRITE: 0)
-----------------------
check RESP_S_B - (Resp - HRESP: OKAY, HREADY: 1)
check DATA_S_B - 00000000
example.controller - read (ReqToS - HSEL: 1, HREADY: 1, HADDR: 00000000, HWRITE: 0)
-----------------------
check RESP_S_B - (Resp - HRESP: OKAY, HREADY: 1)
check DATA_S_B - 00000001
example.controller - read (ReqToS - HSEL: 1, HREADY: 1, HADDR: 00000000, HWRITE: 1)
-----------------------
check RESP_S_B - (Resp - HRESP: OKAY, HREADY: 1)
check DATA_S_B - 00000000
example.controller - read (ReqToS - HSEL: 1, HREADY: 1, HADDR: 00000000, HWRITE: 0)

Now to synthesize the design, run “legup hw”. You should get the generated verilog file after the compilation.

To simulate with Modelsim run “legup wave”. This will use a custom Verilog testbench that we have created for this example that will create a few sample AHB-Lite transactions to exercise the code. See the waveforms below (full size image):

waveform

Figure 3: Simulation Waveform for Example Code

 

In Figure 3:

  • T0-T2: Either HSEL or HREADY is not 1, so the requests are ignored by the slave.
  • T2-T3: The master initiates a read request at address 0x0.
  • T3-T4: The master initiates a write request at address 0x0. The slave responds with 1 on DATA_OUT for the read request from T2.
  • T4-T5: The master initiates another read request at address 0x0.
  • T5-T6: The master initiates a read request at address 0x1. The slave responds to the read request from T4.
  • T6-T7: The master initiates a write request at address 0x1. The slave stalls the master by setting HREADY to 0.
  • T7-T8: The master holds the previous write request. The slave responds to the read request from T5.
  • T8-T9: The master initiates another read request at address 0x1. The slave stalls the master for the write request from T6.
  • T9-T10: The master holds the previous read request.
  • T10-T11: The master initiates a read request at address 0x2. The slave stalls the master for the read request from T8.
  • T11-T12: The master holds the previous read request. The slave responds to the read request from T8.
  • T12-T13: The master initiates a read request at address 0x3. The slave responds error to the read request from T10.
  • T13-T14: The master cancels the previous read request. The slave continuous the error response to the read request from T10.
  • T14-T15: The master re-issue the read request at address 0x3.
  • T15-T16: The master initiates a read request back at address 0x0. The slave stalls the master for the read request from T14.
  • T16-T17: The master holds the previous read request. The slave responds error to the read request from T14.
  • T17-T18: The master cancels the previous read request. The slave continuous the error response to the read request from T14.
  • T18-T22: The master re-issue the requests at address 0x0 that is identical to T2-T5.

Contact Us

The latest release of LegUp has alpha support for System C and supports the example above. This feature is still under development but if you’re interested please contact us to learn more at: support@legupcomputing.com

 

Be the first to write a comment.

Your feedback