Skip to content

Commit

Permalink
Add more document for DPI and tweak trait name.
Browse files Browse the repository at this point in the history
  • Loading branch information
uenoku committed Sep 4, 2024
1 parent a1c35ad commit 5a1799e
Show file tree
Hide file tree
Showing 3 changed files with 207 additions and 4 deletions.
143 changes: 143 additions & 0 deletions docs/src/explanations/dpi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
---
layout: docs
title: "Calling Native Functions from Chisel (DPI)"
section: "chisel3"
---

# Calling Native Functions from Chisel (DPI)

## DPI Basics

DPI API allows you to integrate native code into your Chisel hardware designs. This enables you to leverage existing libraries or implement functionality that is difficult to express directly in Chisel.

Here's a simple example that demonstrates printing a message from a C++ function:
```c++
extern "C" void hello()
{
std::cout << "hello from c++\\n";
}
```

To call this function from Chisel, we need to define a corresponding DPI object.

```scala
object Hello extends DPIVoidFunctionImport {
override val functionName = "hello"
override val clocked = true
final def apply() = super.call()
}

Hello() // Print
```

Explanation:

* `Hello` inherits from `DPIVoidFunctionImport` because the C++ function doesn't return a value (void).
* `functionName` specifies the C-linkage name of the C++ function.
* `clocked = true` indicates that its function call is invoked at clock's posedge.
* We recommend defining the apply method for a more Scala-like syntax.

## Type ABI

Unlike normal Chisel compilation flow, we use a specific ABI for types to interact with DPI.

### Argument Types

* Operand and result types must be passive.
* A vector is lowered to an *unpacked* *open* array type, e.g., `a: Vec<4, UInt>` to `byte a []`.
* A bundle is lowered to a packed struct.
* Integer types are lowered into 2-state types.
Small integer types (< 64 bit) must be compatible with C-types and arguments are passed by value. Users are required to use specific integer types for small integers shown in the table below. Large integers are lowered to bit and passed by reference.


| Width | Verilog Type | Argument Passing Modes |
| ----- | ------------ | ---------------------- |
| 1 | bit | value |
| 8 | byte | value |
| 16 | shortint | value |
| 32 | int | value |
| 64 | longint | value |
| > 64 | bit [w-1:0] | reference |

### Function Types
The type of DPI object you need depends on the characteristics of your C++ function:

* Return Type
* For functions that don't return a value (like hello), use `DPIVoidFunctionImport`.
* For functions with a return type (e.g., integer addition), use `DPINonVoidFunctionImport[T]`, where `T` is the return type.
* The output argument must be the last argument in DPI fuction.
* Clocked vs. Unclocked:
* Clocked: The function call is evaluated at the associated clock's positive edge. By default, Chisel uses the current module's clock. For custom clocks, use `withClocked(clock) {..}`. If the function has a return value, it will be available in the next clock cycle.
* Unclocked: The function call is evaluated immediately.

## Example: Adding Two Numbers
Here's an example of a DPI function that calculates the sum of two numbers:

```c++
extern "C" void add(int lhs, int rhs, int* result)
{
*result = lhs + rhs;
}
```

```scala
object Add extends DPINonVoidFunctionImport[UInt] {
override val functionName = "add"
override val ret = UInt(32.W)
override val clocked = false
override val inputNames = Some(Seq("lhs", "rhs"))
override val outputName = Some("result")
final def apply(lhs: UInt, rhs: UInt): UInt = super.call(lhs, rhs)
}

val result = Add(2.U(32.W), 3.U(32.W))
```

Explanation:

* `Add` inherits from `DPINonVoidFunctionImport[UInt]` because it returns a 32-bit unsigned integer.
* `ret` specifies the return type.
* `clocked` indicates that this is a clocked function call.
* `inputNames` and `outputName` provide optional names for the function's arguments and return value (these are just for Verilog readability).

## Example: Sum of an array
Chisel vectors are converted into SystemVerilog open arrays when used with DPI. Since memory layout can vary between simulators, it's recommended to use `svSize` and `svGetBitArrElemVecVal` to access array elements.

```c++
extern "C" void sum(const svOpenArrayHandle array, int* result) {
// Get a length of the open array.
int size = svSize(array, 1);
// Initialize the result value.
*result = 0;
for(size_t i = 0; i < size; ++i) {
svBitVecVal vec;
svGetBitArrElemVecVal(&vec, array, i);
*result += vec;
}
}
```
```scala
object Sum extends DPINonVoidFunctionImport[UInt] {
override val functionName = "sum"
override val ret = UInt(32.W)
override val clocked = false
override val inputNames = Some(Seq("array"))
override val outputName = Some("result")
final def apply(array: Vec[UInt]): UInt = super.call(array)
}
val io = IO(new Bundle {
val a = Input(Vec(3, UInt(32.W)))
val b = Input(Vec(6, UInt(32.W)))
})
val sum_a = Sum(io.a) // compute a[0] + a[1] + a[2]
val sum_b = Sum(io.b) // compute b[0] + ... + b[5]
```

# FAQ

* Can we export functions? -- No, not currently. Consider using a black box for such functionality.
* Can we call a DPI function in initial block? -- No, not currently. Consider using a black box for initialization.
* Can we call two clocked DPI calls and pass the result to another within the same clock? -- No, not currently. Please merge the DPI functions into a single function.
37 changes: 34 additions & 3 deletions src/main/scala/chisel3/util/circt/DPI.scala
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,25 @@ trait DPIFunctionImport {
def inputNames: Option[Seq[String]] = None
}

// Base trait for a non-void function that returns `T`.
trait DPINonVoidFunctionImport[T <: Data] extends DPIFunctionImport {

/** Base trait for a non-void function that returns `T`.
*
* @tparam T Return type
* @see Please refer [[https://www.chisel-lang.org/docs/explanations/dpi]] for more detail.
* @example {{{
* object Add extends DPINonVoidFunctionImport[UInt] {
* override val functionName = "add"
* override val ret = UInt(32.W)
* override val clocked = false
* override val inputNames = Some(Seq("lhs", "rhs"))
* override val outputName = Some("result")
* final def apply(lhs: UInt, rhs: UInt): UInt = super.call(lhs, rhs)
* }
*
* Add(a, b) // call a native `add` function.
* }}}
*/
def ret: T
def clocked: Boolean
def outputName: Option[String] = None
Expand All @@ -133,8 +150,22 @@ trait DPINonVoidFunctionImport[T <: Data] extends DPIFunctionImport {
final def call(data: Data*): T = callWithEnable(true.B, data: _*)
}

// Base trait for a clocked void function.
trait DPIClockedVoidFunctionImport extends DPIFunctionImport {
trait DPIVoidFunctionImport extends DPIFunctionImport {

/** Base trait for a void function.
*
* @see Please refer [[https://www.chisel-lang.org/docs/explanations/dpi]] for more detail.
* @example {{{
* object Hello extends DPIVoidFunctionImport {
* override val functionName = "hello"
* override val clocked = true
* final def apply() = super.call()
* }
*
* Hello() // call a native `hello` function.
* }}}
*/
def clocked: Boolean
final def callWithEnable(enable: Bool, data: Data*): Unit =
RawClockedVoidFunctionCall(functionName, inputNames)(Module.clock, enable, data: _*)
final def call(data: Data*): Unit = callWithEnable(true.B, data: _*)
Expand Down
31 changes: 30 additions & 1 deletion src/test/scala/chiselTests/DPISpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ private object EmitDPIImplementation {
val dpiImpl = s"""
|#include <stdint.h>
|#include <iostream>
|#include <svdpi.h>
|
|extern "C" void hello()
|{
Expand All @@ -27,6 +28,16 @@ private object EmitDPIImplementation {
|{
| *result = lhs + rhs;
|}
|
|extern "C" void sum(const svOpenArrayHandle array, int* result) {
| int size = svSize(array, 1);
| *result = 0;
| for(size_t i = 0; i < size; ++i) {
| svBitVecVal vec;
| svGetBitArrElemVecVal(&vec, array, i);
| *result += vec;
| }
|}
""".stripMargin

class DummyDPI extends BlackBox with HasBlackBoxInline {
Expand Down Expand Up @@ -62,8 +73,9 @@ class DPIIntrinsicTest extends Module {
io.add_unclocked_result := result_unclocked
}

object Hello extends DPIClockedVoidFunctionImport {
object Hello extends DPIVoidFunctionImport {
override val functionName = "hello"
override val clocked = true
final def apply() = super.call()
}

Expand All @@ -85,22 +97,35 @@ object AddUnclocked extends DPINonVoidFunctionImport[UInt] {
final def apply(lhs: UInt, rhs: UInt): UInt = super.call(lhs, rhs)
}

object Sum extends DPINonVoidFunctionImport[UInt] {
override val functionName = "sum"
override val ret = UInt(32.W)
override val clocked = false
override val inputNames = Some(Seq("array"))
override val outputName = Some("result")
final def apply(array: Vec[UInt]): UInt = super.call(array)
}

class DPIAPITest extends Module {
val io = IO(new Bundle {
val a = Input(UInt(32.W))
val b = Input(UInt(32.W))
val add_clocked_result = Output(UInt(32.W))
val add_unclocked_result = Output(UInt(32.W))
val sum_result = Output(UInt(32.W))
})

EmitDPIImplementation()

Hello()

val result_clocked = AddClocked(io.a, io.b)
val result_unclocked = AddUnclocked(io.a, io.b)
val result_sum = Sum(VecInit(Seq(io.a, io.b, io.a)))

io.add_clocked_result := result_clocked
io.add_unclocked_result := result_unclocked
io.sum_result := result_sum
}

class DPISpec extends AnyFunSpec with Matchers {
Expand Down Expand Up @@ -151,6 +176,8 @@ class DPISpec extends AnyFunSpec with Matchers {
dpi.io.b.poke(36.U)
dpi.io.add_unclocked_result.peek()
dpi.io.add_unclocked_result.expect(60)
dpi.io.sum_result.peek()
dpi.io.sum_result.expect(84)

dpi.clock.step()
dpi.io.a.poke(24.U)
Expand All @@ -159,6 +186,8 @@ class DPISpec extends AnyFunSpec with Matchers {
dpi.io.add_clocked_result.expect(60)
dpi.io.add_unclocked_result.peek()
dpi.io.add_unclocked_result.expect(36)
dpi.io.sum_result.peek()
dpi.io.sum_result.expect(60)
}
.result

Expand Down

0 comments on commit 5a1799e

Please sign in to comment.