System Level Design Languages:SystemC Language Description
SystemC Language Description
In this section, we will describe a number of necessary SystemC constructs for hardware design. The constructs described are the basic constructs of SystemC version 2.1. More details on these constructs and more complex constructs can be found in Refs. [6,7].
The examples shown for each construct are parts of Sayeh CPU [8], which we will demonstrate its SystemC implementation at the end of this section.
Design Parts: Modules
A hardware design consists of several parts that work independently and concurrent with other modules. In SystemC, a hardware component description of which is referred to as a module, begins
with the SC_MODULE keyword. The syntax and an example of the use of SC_MODULE are shown in Figure 86.49. A module defined by SC_MODULE is the smallest functional part of a design that can be instantiated hierarchically in other modules or in a testbench.
As illustrated in Figure 86.49, each module has a body. This body contains port definitions, member data instances, instantiations of other modules, constructor (which is mandatory), destructor, and member functions (including processes and helper functions). We will describe the syntax of each section below.
Port Definitions
In this part, communication between modules is defined. Figure 86.50 shows ports for cache module. As shown in this figure, the input and output ports in cache include the SystemC basic port types sc_in for input ports (e.g., read), sc_out for output ports (e.g., ready), and sc_inout for bidirectional ports (e.g., AddressBus). In addition to the above port types, there can be more general and more complex port types
defined in SystemC. These ports use sc_port in their definition. More details on ports in SystemC can be found in Section 86.3.2.4.1.
Member Data Instances
As SC_MODULE is simply a struct in C++, member data instances are the same as member variables in class and struct definitions. The syntax of member data instances is shown in Figure 86.51. The type of these data instances can be C/C++ or SystemC types. Those of SystemC types are discussed in Section 86.3.2.3.
As shown in the example of Figure 86.51(b), registers and CPU status flags are modeled as member data instances. In this example, m_RegFile is a block of eight 16-bit registers.
Instantiations of Other Modules
To support hierarchy in SystemC designs, the designer must have the ability to instantiate a module inside another. In SystemC, a pointer or reference to an instantiated module is defined in the definition of instantiating module.
Suppose in our Sayeh CPU example there is a need for an internal cache (a cache module inside the CPU). For this purpose, we need to develop a cache model in SystemC and implement it in an SC_MODULE. Then we have to instantiate this module in the sayeh module. The example is shown in Figure 86.52. As shown, internal_cache is an instance of the cache module.
Note that if we define the internal_cache in Figure 86.52 as a pointer, we have to allocate memory for it using the C++ new function (In this text, we will use new as a verb for such allocations). This has to be done in the sayeh module constructor before binding its ports. Figure 86.52(b) shows an example of such a binding.
Constructor
Similar to C++ classes, each module needs a special part where the necessary initializations are done. This part is called the constructor of a module, and is an essential part of every module. A constructor is called when the pointer to a module is newed (in case of a pointer instance of a module) or when the module is defined (in case of a nonpointer instance of a module).
Constructors, in both C++ and SystemC, perform initializations. In SystemC the concurrent parts of a module are also defined in the constructor. These concurrent parts are named processes, which will be described in Section 86.3.2.2.
Like C++, constructors in SystemC modules may have arguments. If a constructor has arguments, it must be defined within its corresponding module with SC_HAS_PROCESS (Figure 86.53(a)).
In the case of no arguments, a constructor can be defined using SC_CTOR keyword (Figure 86.53(b)). In this case, its implementation must be included in the module definition. Otherwise, it can be implemented outside the module definition. As an example, in the Sayeh CPU, we can define the clock period and clock start time as the constructor arguments or as static definitions. The implementation of sayeh constructor of Figure 86.53(a) must be included elsewhere, such as a separate .cpp file. In a situation where a constructor is too complex to be included in a module definition, SC_HAS_PROCESS can be used without arguments.
Destructor
Destructors are responsible for necessary memory deallocations and pointer settings. Like C++, every module in SystemC may have a destructor. Having destructors is not mandatory. The syntax of destructors in SystemC is the same as destructors in C++ [4].
Member Functions
Functionality of a module is defined in terms of its member functions (Figure 86.54). These functions are defined in a module and can be implemented in a separate file. In SystemC, member functions are divided into two main categories. They can represent concurrent parts of a module or they can be utility functions, which are developed for making the implementation of the concurrent parts easier. We will describe concurrency in Section 86.3.2.2. These two categories are not syntactically different and it is the module constructor that differentiates the concurrent functions from the utility functions. The syntax of module member functions is similar to that of class member functions in C++, which is shown in Figure 86.54.
Up to this point, we have defined a module (the example is sayeh module). The characteristics of modules are in their definition. To define the functionality of a module, we need to implement each of its member functions. The type of each member function (whether it is a concurrent process or a helper function) specifies which SystemC constructs should or should not be used in the function body. The details of member function bodies will be discussed in Section 86.3.2.2.
Concurrent Parts: Processes
As mentioned before, to model hardware components we need to model concurrency. In VHDL, concurrent statements execute simultaneously with other statements. A SystemC process is defined by a member function in a module, and is registered within the module’s constructor. When a member function is registered, we have the option of defining the signals it is sensitive to, and its activation mechanism. A concurrent process in SystemC can be defined as a method (SC_METHOD), a thread (SC_THREAD), or a clocked thread (SC_CTHREAD). Member functions defined as such in a construc- tor are executed concurrently with other methods, threads, or clocked threads.
Methods
A method, like reset in Figure 86.55, is a process in SystemC. A method is used for describing a concurrent process like a PROCESS statement with sensitivity list in VHDL. A method is defined with the SC_METHOD keyword in SystemC. A sensitivity list is a list of events on specified signals. If any of these events occur, the statements in the method will be executed. For example, to model a memory that can work with the Sayeh CPU, we can define its resetting operation as a simple method sensitive to the positive edge of the Reset signal. In Figure 86.55, every time the Reset signal is changed from false to true the message in the reset function, the implementation of which is shown in Figure 86.55(b), will be displayed on the output screen.
If a value is assigned to a local variable in a method, it will not remain in that local variable on its next invocation. Suppose that the Reset signal in Figure 86.55(a) is changed from false to true at times 9 and 17 ns. If the reset method of Figure 86.56 is used instead of that of Figure 86.55(b) at 9 ns, variable
tmp will become 1001. But at 17 ns, tmp will not become 1002, and will again change to 1001. If variables that retain their values are needed in methods, they have to be defined in the definition of the method’s corresponding module.
Another important point about methods is that they cannot include wait, and can only have sensitivity lists. If a wait function is used in the body of a method, a run-time error will occur.
In spite of this, there is still a way to add dynamic sensitivity to a method. This can be done by the next_trigger function which will be discussed in the following section.
• next_trigger: If the next_trigger function is used in a method, the default sensitivity list will be overwritten. Consider the reset method in the memory module of Figure 86.55. The static sensitivity list of this method is the positive edge of the Reset signal. We rewrite this method as shown in Figure 86.57. If the Reset signal makes 1000 low-to-high transitions (the initial value of m_count is 0), the default sensitivity list will be replaced by the positive edge of MasterReset signal. After one master resetting, the sensitivity will again be overwritten to reset.posedge_event which is its original sensitivity (by executing the second next_trigger function).
Note that using next_trigger does not cause context switching in SystemC. It means that the statements after next_trigger will still be executed.
For example, in Figure 86.57, after running next_trigger (Reset.posedge_event()) the statement cout <<“Master reset done!” will also be executed.
The format of the next_trigger arguments is similar to those of the wait function (see Section 86.3.2.2.2).
Another important point about methods is their initialization process. Each method is executed once at the beginning of the simulation. In a case where the initial execution should not occur, dont_initialize function is written in the constructor, after the method registration part.
Threads
A thread is a SystemC process that is used for describing concurrent processes with wait functions. Unlike methods, which are executed each time an event happens on their sensitivity list, threads are executed only once during the simulation. This means that if a thread process exits, it will never be executed again during the simulation, even if an event happens on its sensitivity list. That is why most of the functions defined as a thread use an explicit infinite loop.
A SystemC thread is defined by the SC_THREAD keyword and, similar to SC_METHOD, it is registered in the constructor of a module. Threads can use both static and dynamic sensitivity methods. Static sensitivity refers to the sensitivity list, while dynamic sensitivity is when the wait statement is used in the body of a thread. When a thread executes a wait statement, the control of the simulation returns to the simulation kernel. This means that wait suspends the execution of that thread. When the conditions of the arguments used in a wait statement are met, the execution of the thread will be continued from the statement after that wait statement. The reset method which was rewritten in Figure 86.57 is imple- mented with a thread, instead of a method, and is shown in Figure 86.58(a).
As discussed above, threads can only work with wait statements to switch simulation to other modules. This kind of context switching makes threads more easy to use for designers. Although they are easier to use, methods are considered as light-weight processes compared to the threads. That is, they are simulated faster than threads. Since wait statements play an important role in developing threads, the following section describes this function in more details.
wait: As discussed earlier, when a thread executes a wait statement it will be suspended and the simulation control continues with other processes. The execution of a thread will be resumed when the events or timeouts specified as the wait arguments expire.
An event (defined by the sc_event keyword in SystemC) happens at a specified simulation time. Events have no values. There are several implemented functions for SystemC types which return sc_event. For example, posedge_event is used to indicate if a low to high transition occurs on a signal.
wait accepts several kinds of arguments. The different argument configurations of wait are listed below.
• wait(): This function, with no arguments, suspends its enclosing thread, and that thread resumes when an event happens on its static sensitivity list.
• wait(event): The argument of this function is of type sc_event. For example, wait(clock.negedge_ event()) suspends its enclosing thread, and that thread resumes at the negative edge of line clock.
• wait(timeout): The argument of this function is of type sc_time. For example, wait(12, SC_NS)
suspends its enclosing thread, and that thread resumes 12 ns after the current simulation time.
• wait(event1 | event2 | …): This function suspends its enclosing thread, and that thread resumes if any of event1, event2, etc. happen.
• wait(event1 & event2 & …): This function suspends its enclosing thread, and that thread resumes if all events event1, event2, etc. occur.
• wait(timeout, event): The first argument is of type sc_time, while the second argument is of type sc_event. For example, wait(12, SC_NS, clock.negedge_event()) will either return because of a negative edge of clock or as a result of 12 ns timeout.
• wait(timeout, combination_of_events): The behavior of this function is similar to wait(timout, event) except that its second argument is not a single event. It is the OR’ed list or the AND’ed list of the events depending on the operator used.
An important point about the wait statements in a thread is that the values of the local variables are saved before context switching and they are restored when the thread is resumed. For example, in Figure 86.58(b) local_count is a local variable which is initialized to zero. Before the wait(MasterReset.posedge_event()) statement the value of local_count is 1000 and it remains 1000 when MasterReset positive edge event happens. This shows another difference between SC_METHOD and SC_THREAD.
Clocked Threads
A clocked thread is another type of a process, which is a specialized version of SC_THREAD. This revised version is defined by the SC_CTHREAD keyword in SystemC. There are discussions for deprecating SC_CTHREAD in the latest version of SystemC library, but for deleting this concept, SC_METHOD and SC_THREAD must be revised. This is due to the fact there are facilities implemented in SC_CTHREAD, which cannot be found in the other two processes.
Clocked threads are threads, statically sensitive to a specified edge of a specified signal. An example of a clocked thread registration is shown in Figure 86.59.
As shown in Figure 86.59, the controller function of the RTL version of Sayeh is sensitive to the positive edge of clock.
Similar to threads, in clocked threads wait statements can be used for context switching. The acceptable arguments for wait in clocked threads are different from those acceptable for threads. The legal wait statements are
• wait(): If we use wait without any arguments, the clocked thread will be suspended until the next specified edge of the specified clock signal.
• wait(N): This version of wait uses an integer as its argument. This function suspends the clocked process for the N clock cycles.
• wait_until(condition): This version of wait statement suspends the clocked process until the condition on the specified edge of the clock is met. Note that each condition is checked only at the specified edge of the clock. Every signal used in the condition statement must be a delayed signal. In other words, signals used must be of format signal.delayed(). This function returns the value of signal at the end of a delta cycle. A delta cycle in SystemC is similar to delta time in VHDL.
If we use other formats of wait in a clocked thread, it will cause a run-time error. There are other utilities developed for clocked threads, including
• reset_signal_is(signal, active_value): This function determines the signal for resetting a clocked thread. The value of this signal is checked at the specified edge of the clock. If the value is equal to the specified active value (defined as the second argument), the clocked thread function will be executed from the beginning. For example, in Figure 86.60, each time the Reset signal is true at the positive edge of the clock, the statement “This is the beginning of the control process …” is displayed in the output window.
• watching(signal.delayed() == active_value): There are two types of the watching method that is used in clocked threads. They are global and local watchings, and an example of a local watching is shown in Figure 86.61. Global watching methods are used in the body of the module constructor after the clocked thread registration. This kind of watching method works similar to the reset_signal_is method. A local watching method defined in a clocked thread causes the thread to jump to a specified block, if its condition becomes true on the edge of the clock. A local watching method is defined and used with three blocks of code bracketed by W_BEGIN, W_DO, W_ESCAPE, and W_END keywords. Referring to Figure 86.61, these blocks are definition block, normal operation block, and watching operation block, that are described below.
• Definition block: This block comes between W_BEGIN and W_DO. Local watching methods are defined in this block.
• Normal operation block: This block comes between W_DO and W_ESCAPE. If the condition of the specified local watching methods is not met, this block will be executed at the invocation of the clocked thread.
• watching() operation block: This block comes between W_ESCAPE and W_END. If the condition of the specified local watching methods becomes true, this block is executed instead of the normal operation block.
Figure 86.61 shows an example of a local watching related to the controller clocked thread that was defined in Figure 86.59.
The halt function is used to stop a clocked thread. Using this function in SC_CTHREAD is similar to the use of return in an SC_THREAD.
In this section, the basics needed for developing designs in SystemC were explained. The next section explains data types in SystemC.
SystemC Data Types
SystemC library has several predefined data types that can be used for modules’ member variables, internal signals, and communication ports. In addition to these types, designers are free to use any of the allowed data types in C++, e.g., int, char, and user-defined types. A brief description of SystemC predefined types follows. For more details, and other SystemC types not presented here, readers should refer to Ref. [6].
Numeric and Arithmetic Data Types
• sc_string: This is a unified C-style representation for numbers. Figure 86.62 shows the sc_string syntax. Using this format, -2 in sc_string decimal format is represented by -0d2. The same number in 4-bit sc_string binary format is represented by 0b1110, and in 4-bit sc_string unsigned binary by 0b0010.
• sc_int<n>, sc_uint<n>: These types represent integers and unsigned integers. As stated earlier, the C++ int and unsigned int can also be used. But these SystemC data types have several utility functions that make them easier to use for a hardware design. You can also define the number of bits for these types. For example, sc_uint<5> is used for declaring a 5-bit unsigned integer with a maximum value of 31.
• sc_bigint<n>, sc_biguint<n>: These types are similar to sc_int<n> and sc_uint<n>, but they are used for very large numbers that C++ integer types cannot support. For example, sc_biguint<70> is used for integers with a maximum value of 270-1. This type is usually used for integer representation of wide data or address busses in a hardware design.
• Fixed point types: This group of data types is used for fixed-point numbers. It includes sc_fixed, sc_ufixed, sc_fixed_fast, sc_ufixed_fast, sc_fix_fast, and sc_ufix_fast.
Multivalue Data Types
• sc_logic, sc_lv<n>: sc_logic is a 4-valued data type. It can be used as a signal type. sc_logic values can be SC_LOGIC_1, SC_LOGIC_0, SC_LOGIC_X, and SC_LOGIC_Z. For easier use, ‘1’, ‘0’, ‘X’, and ‘Z’ can be used respectively. sc_lv<n> is the vector form of sc_logic and is equivalent to a vector of size n of sc_logic type.
All of the above data types have predefined overloaded operations. For example, ==, >=, <=, >, <, = (assignment), +, +=, −, −=, &, and | have been implemented for the above data types (if applicable). In addition, several utility functions for these predefined data types have been implemented that makes them easier to use than the C++ data types. Several utility functions for sc_in<n> type are range, to_int, to_ulong, length, value, and to_string. These functions help designers develop their system level designs faster and develop more readable codes.
The focus of the above SystemC overview was on concurrent parts and data types. In the next section, we will describe methods used for interconnecting various modules.
Comments
Post a Comment