In this post, we talk about the different numeric data types which are included in the SystemC libraries. This includes a discussion of the sc_int, sc_uint, sc_fixed and sc_ufixed data types.
In the previous post in this series, we talked about the basic binary data types we can use in SystemC.
SystemC is actually a set of classes and libraries which are built on top of the C++ programming language. We can download and install these libraries for free from the accellera website.
As a result of this, the basic syntax of the SystemC language is taken directly from C++.
However, in these tutorials we will only look at the SystemC extensions which we can use in FPGA design and verification.
Therefore, if you are not already familiar with the C++ language then it is a good idea to take a beginners C++ course before reading through these tutorials.
When we write code in C++, we often use the int data type to store numeric values. As SystemC is an extension of C++, we can also use the int data type to represent numeric data in SystemC.
In both languages, this number is internally stored as a signed 32-bit twos complement number.
However, when we are designing digital circuits there are many cases when this implementation is inefficient.
For example, we may have a small counter circuit which counts from 0 to 255. In this case, we could implement the circuit using an unsigned 8 bit number.
Equally, we could also design a circuit where we need to store a number that requires more than 32 bits.
In the SystemC libraries, there are 4 inbuilt numeric data types that we can use in these situations.
We use the sc_int and sc_unit SystemC data types to declare numeric variables which have 64 bits or less.
We use the sc_int data type to declare signed variables whilst the sc_uint data type creates unsigned variables.
The SystemC libraries use generic C++ classes to implement both of these data types. As a result of this, we have to provide a size parameter when we declare a variable which uses these types.
The code snippet below shows the general syntax we use to declare variables that use these data types.
// Declaration of an sc_int data type
sc_int<number_bits> <name>;
// Declaration of an sc_uint data type
sc_uint<number_bits> <name>;
In both of these constructs, we use the <number_bits> field to declare how many bits there are in the variable we are declaring.
After we have declared a variable using the sc_int or sc_uint data types, we can directly assign numerical values to it.
However, we can only assign a limited range of values to the variable. As a result of this, we need to take extra care when assigning values to these data types.
For example, if we were to assign the value of -1 to an 8 bit sc_uint data type we would actually end up with the value of 255. As the SystemC libraries don't perform any checks on our values, we have to take care to avoid doing this.
The SystemC code below shows how we use the sc_int and sc_uint numeric data types in practise. We can also simulate this example on EDA playground.
// Declaration of a 16 bit sc_int data type
sc_int<16> example_sci;
// Assigning data to the sc_int data type
example_sci = 8000;
cout << "excample_sci = " << example_sci << endl;
example_sci = -4000;
cout << "excample_sci = " << example_sci << endl;
// Declaration of an 8 bit sc_uint data type
sc_uint<8> example_scu;
// Assigning data to the sc_uint data type
example_scu = 100;
cout << "example_scu = " << example_scu << endl;
example_scu = 255;
cout << "example_scu = " << example_scu << endl;
We use the sc_bgint and sc_biguint datat types when we want to declare variables with more than 64 bits.
We use the sc_bigint data type to declare signed variables whilst the sc_biguint data type creates unsigned variables.
The SystemC libraries use generic C++ class to implement both of these data types. As a result of this, we have to provide a size parameter when we declare a variable which uses these types.
The SystemC code below shows the general syntax we use to declare variables that use these numeric data types.
// Declaration of an sc_bigint data type
sc_bigint<number_bits> <name>;
// Declaration of an sc_biguint data type
sc_biguint<number_bits> <name>;
In both of these constructs, we use the <number_bits> field to declare how many bits there are in the variable we are declaring.
After we have declared a variable using the sc_bigint or sc_biguint data types, we can directly assign numerical values to it.
The code snippet below shows how we use the sc_int and sc_uint data types in practise. We can also simulate this example on EDA playground.
// Declaration of a 65 bit sc_bigint data type
sc_bigint<65> example_bi;
// Assigning data to the sc_bigint data type
example_bi = 8e10;
cout << "example_bi = " << example_bi << endl;
example_bi = -4e10;
cout << "example_bi = " << example_bi << endl;
// Declaration of a 100 bit sc_bigint data type
sc_biguint<100> example_bu;
// Assigning data to the sc_biguint data type
example_bu = 11e10;
cout << "example_bu = " << example_bu << endl;
example_bu = 22e10;
cout << "example_bu = " << example_bu << endl;
When we write C++ code, we often use either the double or float data type to represent decimal numbers.
In both instance, these data types are implemented as IEEE 754 floating point numbers. The float type uses the single precision (32 bit) data format whilst the double type uses the double precision data format (64 bits).
The main reason for this is that floating point numbers provide a highly efficient way of storing decimal numbers. As a result, we can use them to store very large numbers as well as very small numbers.
However, one drawback of using floating point numbers is that performing calculations on this type of data is complex and inefficient.
For this reason, we often prefer to use fixed point numbers when we represent decimal numbers in an FPGA.
When we use this approach, we can use standard binary adders, subtractors and multiplier circuits. As these circuits are highly efficient, we can achieve greater throughput when we use fixed point numbers.
As a result of this popularity, the SystemC libraries have some in-built data types that allow us to easily model fixed point numbers.
Like most of the data types we have talked about so far, these types are implemented using generic C++ classes. As a result of this, we have to provide several parameters when we declare a variable that uses these types.
The syntax below shows the general syntax we use to declare a fixed point number in SystemC.
sc_fixed<<wl>, <iwl>, <qmode>, <omode>, <bits>>
We can omit the <qmode>, <omode> and <bits> fields when we declare an sc_fixed or sc_ufixed data type. When we do this, these fields are assigned default values.
We use the sc_fixed data type to declare signed numbers and the sc_ufixed data type to declare unisgned numbers.
As we can see, the sc_fixed and sc_ufixed data types are a bit more complex than the other data types we have discussed.
Let's discuss each of the parameters in more detail.
We use the <wl> and <iwl> fields to declare the length and precision of the variable which we are declaring.
The <wl> field is used to determine the total number of bits in our variable.
The <iwl> determines how many integer bits there are in the our variable.
As an example, say we declare a 6 bit number with 4 integer bits. In this case, we would set <wl> to 6 and <iwl> to 4 as is shown in the code snippet below.
sc_ufixed<6, 4> example;
This example would result in a variable that has the format shown below.
These fields have the same function when we declare an sc_fixed data type as well. However, we have to take into consideration that the variable will require an extra sign bit at the start of the word.
For example, the code snippet below shows how we declare a 6 bit signed number with 4 integer bits.
sc_fixed<6, 4>
In this case, our variable has the format shown below. When we use this variable, we can express any number in the range from -8 up to 7.75.
We use the <qmode> field to declare the quantization method that our variable should use.
In order to simplify our code, we often omit this field when we declare a sc_fixed or sc_ufixed data types in SystemC. When we do this, the quantization field will use truncation type quantization by default.
The quantization method refers to the way in which data is rounded by our variable.
For example, suppose we have a 4 bit variable which has 2 fractional bits. If we then assign this variable to the value of 0.5625 (0.1001 in binary), our variable can no longer accurately represent this value.
In this case, the nearest values which we actually represent using our variable are 0.5 or 0.75. We use the quantization field to tell our variable how it would choose which of these values to select.
When we pass a value to the <qmode> field, we normally use constants which are defined inside of the SystemC libraries.
The table below summarizes the different constants we can pass to the <qmode> field. We typically use either the SC_RND or the SC_TRN constants which are the simplest to understand.
Constant | Quantization Method |
SC_RND | Round the value by adding the value of the most significant deleted bit to the remaining bits. |
SC_RND_ZERO | Round to zero |
SC_RND_MIN_INF | Round to minus infinity |
SC_RND_CONV | Convergent rounding |
SC_TRN | Simply truncate the unused bits. |
SC_TRN_ZERO | Truncate to zero |
We use the <omode> and the <bits> fields to declare the type of overflow that our variable should use.
In order to simplify our code, we often omit this field when we declare a sc_fixed or sc_ufixed data type. When we do this, the overflow will use wrapping type overflow by default.
We use the overflow method to determine what happens when we attempt to assign our variable a value which is too large for it to store.
For example, suppose we have a pair of 4 bit fixed point numbers each with the value of 15 which we then add together and assign to another 4 bit fixed point number.
The result of this addition will be 30, which requires at least 9 bits to represent.
As a result of this, we need the result to be trimmed down to 8 bits in order to fit inside our variable.
We use the overflow method to determine how the fixed point number will be trimmed down to the correct size.
When we pass a value to the <omode> field, we normally use constants which are defined inside of the SystemC libraries.
The table below summarizes the different constants we can pass to the <omode> field. We typically use the SC_SAT or SC_WRAP values for this field.
Constant | Overflow Method |
SC_SAT | Saturation - set the variable to the highest or lowest possible value. |
SC_SAT_ZERO | Saturate to zero - set the variable to 0 |
SC_SAT_SYM | Symmetrical saturation - same as saturation except the LSB is set to 1 for negative saturation |
SC_WRAP | Wrap - remove the most significant bits until the value is small enough to fit in the given variable. |
SC_WRAP_SM | Sign magnitude wrap |