Transparent Logo

Wiring in Chisel ! Module, Bundle, IO, Biconnect Operator Tutorial

satifaction_meme

Hello FPGAmigos! Today, we’re going to explore how Chisel manages component hierarchy in hardware design. This includes Modules, IO, Bundles, the Flipped function, and biconnect operator. These tools mark the end of the ‘wiring hell’ commonly experienced in VHDL and Verilog.

I’ve been wondering why it took me so long to write a blog post about this, especially since it was a major factor in convincing me to adopt Chisel. Admittedly, my choice is somewhat biased and, on its own, might not be sufficient reason to switch languages. However, I appreciate how these features allow me to focus more on designing my architecture, rather than spending time writing repetitive VHDL/Verilog code.

A key aspect of these features is their foundation in a simple yet powerful truth: Chisel is embedded in Scala, which supports Object-Oriented Programming (OOP). This is not the case with the two other Legacy- Languages-Who-Must-Not-Be-Named. This realization also reinforces my belief that the future of Hardware Description Languages (HDLs) lies in Embedded Domain Specific Languages (EDSLs) within general-purpose languages (GPLs) like Scala.

Having shared my perspective on why Chisel’s approach is beneficial, let’s delve into the core of our discussion. We’ll be focusing on Modules, IO, Bundles, the Flipped function, and bidirectional connection operators, and how these elements streamline the process of hardware design, offering a more efficient alternative to traditional methods.

I. Module : the basic building block of digital design

In Chisel, a ‘Module’ serves a similar purpose to what ‘entity’ and ‘architecture’ do in VHDL, or a ‘module’ in Verilog. Essentially, Chisel generates Verilog code, and a Chisel ‘Module’ translates into a Verilog module in the generated output.

import chisel3._

class MyModule extends Module{
	// code of my module
}

A module is like a lego block. It has a “functionnality” in the same way the lego block has a shape. And it has Inputs and Outputs in the same way a lego block have pins and holes to connect to other lego block.

Why do we work in modules? It’s possible to have a single module that handles everything, but such an approach would lead to code that is unreadable and unmaintainable. A single modification could disrupt the entire system. Adopting a modular approach offers several advantages:

  • Reusability: Modules allow designers to decompose complex hardware designs into manageable, reusable components. Each module can be developed, tested, and reused independently, enhancing the overall efficiency of the design process.
  • Maintainability: Modules have a defined scope of functionalities and interactions with the rest of the design. If a bug needs fixing or a module requires replacement, it can typically be addressed with just a few lines of code.
  • Scalability: Rather than rewriting the implementation of a counter multiple times, it’s more efficient to instantiate the same counter module as needed.

In digital design, modularity is about partitioning your design: the sum of all modules constitutes your entire design, and the functionalities or signals of these modules do not overlap.

II. IO : How to connect modules with each others ?

Modules in digital design manipulate signals to fulfill their functions. These signals fall into three categories: inputs, internals, and outputs. It’s crucial to adhere to a strict policy regarding signal management: a signal can be read by multiple modules, but only one module should drive a particular signal.

  • Inputs: These are signals originating from other modules. Within a module, inputs are read-only:
    1. On an FPGA, they are electrical signals driven externally to the module.
    2. The responsibility for these signals lies with the module that provides them. You can utilize these signals in a receiving module, but any modifications should be made at the source, not where they are received. If an input signal doesn’t behave as expected, the issue should be addressed in the module responsible for that signal.
  • Internals: These signals are not part of the module’s inputs or outputs (IOs). They are read-write because they are relevant only within the module itself. If these signals are needed by other modules, they should be converted into outputs.
  • Outputs:
    1. VHDL: In VHDL, output ports are write-only within the module. You can drive values to these ports, but you can’t read from them internally. To read the value of an output within the module, use an internal signal. This internal signal can be connected to the output port and used for internal module operations.
    2. Verilog: Verilog offers more flexibility; output ports can be both read and written within the module. However, to avoid confusion and unintended behavior, it’s often best to treat outputs as write-only and use internal signals for reading, similar to VHDL.
    3. Chisel: Like Verilog, Chisel allows reading and writing from output ports within the module. Nonetheless, designers often use internal variables or wires, even when Chisel permits reading from outputs, to maintain clarity and accurately represent the intended hardware behavior.

In your design, the outputs of one module become the inputs of another. Therefore, outside a given module, you drive or write to its inputs and read from its outputs.

How to declare IOs

class MyModule extends Module {
	val io = IO(new Bundle{
		val my_input = Input(UInt(8.W))
		val my_output = Ouput(UInt(8.W))
	})
	val my_internal = Wire(UInt(8.W))
}

In Chisel, the ‘IO’ class is used to instantiate an object that I named “io”. This is akin to what you might call a “port” if you’re more familiar with VHDL or Verilog.

Subsequently, I instantiate two objects: one input, which is an 8-bit unsigned integer, and one output, also an 8-bit unsigned integer. As mentioned earlier, internals are distinct from IOs (Inputs/Outputs). This distinction is due to internals being accessible only within the module, unlike IOs that facilitate communication between modules. That’s why “my_internal” is not declared within the IO’s scope.

How to connect Modules

class Top extends Module{
	val myModule1 = Module(new MyModule()) // yes yes one line to instantiate.
	val myModule2 = Module(new MyModule())

	myModule1.input := myModule2.output
	myModule2.input := myModule1.output
}

This approach is considered “standard” in the sense that it aligns with practices commonly used in both VHDL and Verilog.

Within the implementation of “MyModule”, inputs are treated as read-only, and outputs are write-only. However, beyond this scope (such as in the top-level design), you need to drive the inputs and read from the outputs.

Now, you might be wondering, “But wait a minute! What’s this ‘bundle’ stuff in the MyModule implementation?” This is where things start to get interesting.

Bundle : How to stop rewriting you IOs ?

A ‘Bundle’ in Chisel is analogous to a record in VHDL or a struct in Verilog, with a key difference: Bundle is a class in Scala that supports Object-Oriented Programming (OOP), providing it with additional features.

This leads us to a critical question: What if there’s an interface in your design that recurs in many IOs, like AMBA, AXI, Wishbone, SPI, and so on? Do you rewrite every input and output in every module of your design each time? While some codebases that I saw do this, it’s not an efficient or elegant solution.

Just as our modules are classes meant to be instantiated and reused, we will apply the same concept to our IOs.

We’ll implement a class that inherits from Chisel’s ‘Bundle’ class. This approach allows for the creation of reusable, modular interface definitions, streamlining the design process and enhancing maintainability.

class AMBA_AHB_Bundle extends Bundle {
  // Control and Global Signals
  val HCLK = Input(Clock())
  val HRESETn = Input(Bool())

  // Address and Control Signals (from Master to Slave)
  val HADDR = Output(UInt(32.W)) // Assuming 32-bit address
  val HTRANS = Output(UInt(2.W)) // 2 bits for transaction type
  val HWRITE = Output(Bool())
  val HSIZE = Output(UInt(3.W)) // 3 bits for size
  val HBURST = Output(UInt(3.W)) // 3 bits for burst type
  val HPROT = Output(UInt(4.W)) // 4 bits for protection type
  val HMASTLOCK = Output(Bool())

  // Data Signals
  val HWDATA = Output(UInt(32.W)) // Assuming 32-bit data width
  val HRDATA = Input(UInt(32.W))

  // Response Signals (from Slave to Master)
  val HREADY = Input(Bool())
  val HRESP = Input(UInt(2.W)) // 2 bits for response

  // Bus Arbitration Signals (for multi-master scenarios)
  val HBUSREQ = Output(Bool())
  val HGRANT = Input(Bool())
  val HLOCK = Output(Bool())

  // Miscellaneous Signals
  val HSEL = Output(Bool())
}

Now, imagine the scenario where you have to rewrite this for 10 or more different modules. It’s a situation that can be quite tedious, and if you’ve experienced it before, you might recall the frustration it brings.

However, with the solution we’re discussing, the process becomes much simpler. All you need to do is instantiate an object from this class, and voilà – you have your IOs ready to use. This approach significantly streamlines the design process, reduces the potential for errors, and saves a considerable amount of time and effort, especially in complex designs with multiple modules.

class MyModule extends Module {
val io = IO(new Bundle{
	val amba_master = new AMBA_AHB_Bundle
	// ... other ios
})
}

This is where Chisel starts to diverge from VHDL and Verilog. As you may have noticed, I have only implemented the master class for AMBA. In contrast, with VHDL and Verilog, you would typically need to rewrite the slave version of the record or the type struct. Chisel, however, introduces a more streamlined approach with the “Flipped” method, which flip the direction of an entire Bundle with a single word.

This method exemplifies the benefits of using a full-fledged programming language in hardware design. The “Flipped” method simplifies the process of defining complementary interfaces, such as master and slave configurations, demonstrating the power and flexibility that comes with integrating hardware description into a general-purpose programming environment like Scala.

class MyModule extends Module {
val io = IO(new Bundle{
	val amba_master = new AMBA_AHB_Bundle
	val amba_slave  = Flipped(new AMBA_AHB_Bundle)
	// ... other ios
})
}

III. Biconnect operator : Stop spending your time wiring.

But the innovation in Chisel doesn’t stop there. Typically, master interfaces are connected to slave interfaces. While as a child I enjoyed fitting the square peg in the square hole and the circle in the circle hole, as an engineer, connecting the input ‘ready’ signal to the output ‘ready’ signal lost its charm long ago. That’s where the bidirectional connect (biconnect) operator comes into play.

This operator streamlines the process of connecting interfaces. Instead of manually matching each corresponding signal from a master interface to a slave interface – a task that can be both tedious and error-prone – the biconnect operator allows for a more efficient and less error-prone method. It simplifies the task, reducing it to a single line of code in many cases, and ensures a cleaner, more readable design. This feature is a testament to Chisel’s ability to make hardware design both more intuitive and efficient.

class MyModule extends Module {
val io = IO(new Bundle{
	val amba_master = new AMBA_AHB_Bundle
	val amba_slave  = Flipped(new AMBA_AHB_Bundle)
	// ... other ios
})
	
  io.amba_master <> io.amba_slave
}
satifaction_meme
Only one line of code

Conclusion

Indeed, while features like the Flipped method or the biconnect operator might be found in SystemVerilog or VHDL-2019, a significant difference with Chisel is its flexibility. If these features didn’t exist in Chisel, I could write them myself. In contrast, adding new features or making changes in VHDL and Verilog is akin to adding another layer to the already complex Path Dependence or just impossible.

In my view, it’s time to move away from desperately trying to retrofit features that already exist in programming languages into HDLs. Instead, we should focus on creating Embedded Domain-Specific Languages (EDSLs) within a robust General-Purpose Language (GPL).

Yes, VHDL-2019 introduced interfaces, but what about Object-Oriented or Functional Programming features? When can we expect a Package Management System in VHDL? Maybe in VHDL-2050, with early adoption by 2060, for 10k a year ? come on…

SystemVerilog? You mean a blend of Verilog, an HDL not initially intended for hardware design, with a non-synthesizable which is just C++, that you cannot compile for free, and about 120 reserved words that somewhat disguise the fact that it could have been just a library. UVM is just a series of C++ design patterns. Considerable efforts by license vendors to obscure the fact that all these functionalities could be achieved freely with a good library and software skills.

I’m not claiming that Chisel is the ultimate language that will outperform all others, but it certainly is heading in the right direction. If Chisel isn’t to your liking, there are other options like Amaranth, SpinalHDL, Clash, MyHDL. But please, let’s stop supporting the nonsensical aspects of VHDL/Verilog.

Oh, and yes, we were discussing design hierarchy in Chisel. I hope you found the explanation on how it handles modules and connections informative. If you liked this tutorial, please feel free to comment, bookmark, share, and subscribe to the newsletter by downloading my free e-book in the store.

Leave a Reply

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

Wiring in Chisel ! Module, Bundle, IO, Biconnect Operator Tutorial

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