Chisel Types and Operators

chisel_logo

Hello, FPGAmigos! Previously we covered the topic “How to install Chisel?”. Now, let’s dive deeper into the practical usage of Chisel for building digital designs.

In this article, we will explore two fundamental aspects of Chisel: data types and operators. These elements serve as the foundational building blocks for any digital design. A comprehensive comprehension of data types ensures accurate definition of digital signals, states, and data. On the other hand, operators empower us to manipulate these signals and data in order to construct the desired logic.

Bibliography and Other Resources

To learn Chisel I used those resources:

Chisel Data Types

Chisel has 3 main data types:

First Bits to describe … bits (0 or 1), you can specify the width of a bit vector by using the “.W” attribute.

val Word8Bits  = Bits(8.W)
val Word16Bits = Bits(16.W)
// Usually used in addition of other chisel class
val my_io = Output(Bits(8.W)) // create an output of 8 bits 
val my_wire = Wire(Bits(8.W)) // create a wire of 8 bits
val my_reg = Reg(Bits(8.W))   // create a 8 bits register 

Second the UInt data type stands for Unsigned Integer, representing exclusively positive integers. It inherits from the “Bits” class in Chisel. It serves as a way to instruct Chisel on how to handle a bit vector during arithmetic operations such as addition or multiplication.

val my_const    = 4.U
val UInt8Bits   = 13.U(8.W) 
val binary      = "b1010".U(8.W)
val octal       = "o17".U 
val hexadecimal = "h_dead_beef".U 

Third the SInt data type in Chisel stands for Signed Integer, which encompasses both positive and negative integers. Similar to “UInt”, it is an extension of the “Bits” class. “SInt” allows Chisel to handle signed bit vectors, enabling operations with positive and negative values.

val SInt16Bits = -42.S(16.W)
val signed = "b1010".S

One thing to notice is that bits in SInt and UInt are read-only. You cannot assign them separately.

// inside of a for loop with iteration variable i
// reading is valid
my_variable := my_uint(i)

// will raise an error
my_uint(i) := bit_assignment
// solution
val my_shadow_wire = Wire(Vec(size_of_my_uint, UInt(1.W))
my_shadow_wire(i) := bit_assignment
// outisde for loop
my_uint := my_shadow_wire.asUint() // will work correctly

Another useful data type in Chisel is “Bool,” which represents Boolean values (true/false). It is a class that extends the “UInt” class. In my usage so far, I have employed “Bool” for various purposes such as using reset and clock signals, as well as casting other signals to Boolean when required as parameters for certain functions.

val myBool = true.B // scala boolean true as a chisel boolean true.
val my_bit = 1.U
val cast_bit = my_bit.asBool 

In Chisel, you can represent the value of a variable in various numerical bases such as binary, octal, decimal, and hexadecimal by using a string followed by “.U” or “.S”. It is important to specify the width of your variables using “.W” consistently.

"b1010".U       // unsigned 10
"b1010".S       // signed   -6
"h6".U          // hexadecimal 6
"h10".U(4.W)    // hexadecimal 16 in a 4 bits width vector
"o17".U         // octal 15
"h_dead_beef".U // use underscore for long words readability 

Casting

You have the ability to cast your variables of type Bits, UInt, and SInt to other types using the “.asUInt”, “.asSInt”, and “.asBool” attributes. You can also use the “.W” attribute to set the width of your variables.

"b1010".asUInt(5.W) // unsigned 10 on 5 bits vector.
"b1010".asSInt(6.W) // signed -6 on 6 bits vector.
val word = 5.U
val cast_word= word.asSInt 

Chisel Operators

Scala code VS Chisel Code

First, let’s highlight the significance of “val,” “=”, and “:=” in Scala. “Val” is a reserved keyword used to declare an immutable variable, which means that once it’s declared and initialized, you cannot alter its value. It’s important to remember that Chisel design is a Scala program that generates Verilog code.

val word = Wire(UInt(8.W))
word = "hello" // will raise an error 

The first line indicates that in my design, I require eight wires that represent an unsigned integer. Consequently, altering the value of the variable “word” in the subsequent line is meaningless. To prevent such ambiguity, we employ the “val” keyword for declaring immutable variables, as opposed to the “var” keyword used for mutable variables. It is essential to grasp the distinction between the usage of Scala code and Chisel code before utilizing mutable variables.

The “=” symbol in Scala functions as the “assign” operator. It is important to differentiate it from “:=”, a symbol overridden by the Chisel library to describe hardware that undergoes updates and clock/reset changes.

val logic = Wire(1.U)
val reg = RegInit(0.U)
reg := logic

When we examine this code in what I refer to as “Scala mode” (= from the software point of view), we can interpret it as follows: “I desire a variable called ‘logic’ to be assigned the object returned by the constructor of the Wire class. I declare an object named ‘reg’ and assign it the object returned by the constructor of the RegInit class, which accepts a parameter of 0.U. Finally, I utilize the “:=” operator to assign the ‘logic’ object to the ‘reg’ object.”

Alternatively, if we analyze this code in “Chisel mode”(= from the digital design point of view) we would express it as follows: “In my design, I require a wire named ‘logic’ and a register named ‘reg.’ The ‘reg’ register will be updated with the value of ‘logic’ during every rising edge or reset assertion.”

Tip : Reading my code in “Scala Mode” and “Chisel Mode” help me when I am confused. So if it happen to you, feel free to try it.

Bit-wise Operators

Let’s explore the Bitwise Operators in Chisel. For further information on combinational operations, I recommend reading my article titled “How combinational logic works in an FPGA?”. These operators adhere to a standard convention.

val a_and_b = a & b // bitwise and
val a_or_b  = a | b // bitwise or
val a_xor_b = a ˆ b // bitwise xor
val not_a   =  ̃a    // bitwise negation
// no operator for nand, nor, xnor just use ~
val a_nand_b = ~ (a&b)
val a_xnor_b = ~ (a^b)
val a_nor_b  = ~ (a|b) 

If you’re interested in creating an aesthetically pleasing LED garland by implementing a cellular automaton using only an XOR gate as the rule, I encourage you to check out my article on “Rule90 in Chisel” for a comprehensive explanation. Here’s an example of the Rule90 code:

class Rule90(numCells: Int) extends Module {
    val io = IO(new cellAutomataBundle(numCells))
    
    for(idx <- 0 until numCells){
        val previous_idx = Math.floorMod(idx - 1, numCells)
        val next_idx     = Math.floorMod(idx + 1, numCells)
        //                                      XOR here =>   <=
        io.o_cells(idx) := RegNext(io.i_cells(previous_idx) ^ io.i_cells(next_idx), 0.U)   
    }
}

Bit Reduction Operations

val a = Bits(8.W)
val all_ones       = a.andR  // And between all bits of the word.
                             // all_ones = 1 if all bit are one, otherwise it is 0

val at_least_one_1 = a.orR   // Or  between all bits of the word.
                             // at_least_one_1 = 1 if ... at least one 1 is 1 otherwise it is 0

val odd_ones       = a.xorR  // Xor between all bits of the word.
                             // 1 if only and odd number of 1 in the word.
                             
val at_least_one_0 = ~a.andR // as always ~ for nand, nor, xnor
val all_zeros      = ~a.orR
val even_ones      = ~a.xorR

I provide an example of using the “all_zeros” configuration in my article on “Rule110 in Chisel”. Feel free to check it out for reference.

Arithmetic Operators

val a_plus_b           = a + b
val a_minus_b          = a - b
val a_times_b          = a * b
val a_divided_by_b     = a / b
val a_modulo_b         = a % b
val shift_a_by_b_left  = a<<b // shift a by b bits to the left
val shift_a_by_b_right = b>>a // shitt a by b bits to the right    

Concatenation and Extraction

Concatenation and extraction operations are available for Bits, as well as for UInt and SInt types since they inherit from Bits.

val bigword = SInt(32.W)        // signed word of 32 bits
val sign = bigword(31)          // extract the 32th bit which is the bit sign with two's complement.
val lowerPart= bigword(15,0)    // extract the firth 16 bits
val higherPart = bigword(31,16) // extract the last 16 bits
val rebuild_1 = higherPart ## lowerPart // concatenate the higherPart and lowerPart to rebuild the bigword
val rebuild_2 = Cat(higherPart, lowerPart) // equivalent to the last line.

Conclusion

In this article, we’ve gone through the basic data types and operators in Chisel, essential knowledge for any FPGA developer. This is just the beginning of our journey into the fascinating world of Chisel. Keep experimenting with these concepts, and don’t forget to share your progress.

In our next post, we’ll be diving into some more complex aspects of Chisel. Stay tuned and happy designing!

If you liked this post, don’t forget to share it with your fellow developers and FPGA enthusiasts. Leave a comment below if you have any questions or thoughts.

Leave a Reply

Your email address will not be published. Required fields are marked *

Chisel Types and Operators

dark blue dot

Summary

Share it !

Get my Ebook ?

ebook_hero-home

Jumpstart you FPGA journey by

• Understanding the place of FPGA in industry
• Learn about internal composition of an FPGA
• A simple beginner friendly project
• An overview of the FPGA workflow
ebook_banner_11