diff --git a/docs/src/explanations.md b/docs/src/explanations.md index 637e5bd1d7b..cd63fce1bd0 100644 --- a/docs/src/explanations.md +++ b/docs/src/explanations.md @@ -41,3 +41,4 @@ read these documents in the following order: * [Deep Dive into Legacy Connection Operators](explanations/connection-operators) * [Properties](explanations/properties) * [Layers](explanations/layers) +* [Calling Native Functions from Chisel (DPI)](explanations/dpi) diff --git a/docs/src/explanations/dpi.md b/docs/src/explanations/dpi.md new file mode 100644 index 00000000000..1bb8c5736cc --- /dev/null +++ b/docs/src/explanations/dpi.md @@ -0,0 +1,189 @@ +--- +layout: docs +title: "Calling Native Functions from Chisel (DPI)" +section: "chisel3" +--- + +# Calling Native Functions from Chisel (DPI) + +## DPI Basics + +Chisel's 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 mdoc:silent +import chisel3._ +import chisel3.util.circt.dpi._ + +object Hello extends DPIVoidFunctionImport { + override val functionName = "hello" + override val clocked = true + final def apply() = super.call() +} + +class HelloTest extends Module { + Hello() // Print +} +``` + +Output Verilog: + +```scala mdoc:verilog +chisel3.docs.emitSystemVerilog(new HelloTest) +``` + +Explanation: + +* `Hello` inherits from `DPIVoidFunctionImport` which defines a DPI function with a void return type in C++. +* `functionName` specifies the C-linkage name of the C++ function. +* `clocked = true` indicates that its function call is invoked at the clock's posedge. +* We define an `apply` method for a function-like syntax. This allows us to call the DPI function with `Hello()` + +## 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 `Vec` 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. +* `Int`, `SInt`, `Clock`, `Reset` 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 mdoc:silent +trait AddBase extends DPINonVoidFunctionImport[UInt] { + override val functionName = "add" + override val ret = UInt(32.W) + override val inputNames = Some(Seq("lhs", "rhs")) + override val outputName = Some("result") + final def apply(lhs: UInt, rhs: UInt): UInt = super.call(lhs, rhs) +} + +object AddClocked extends AddBase { + override val clocked = true +} + +object AddUnclocked extends AddBase { + override val clocked = false +} + +class AddTest extends Module { + val io = IO(new Bundle { + val a = Input(UInt(32.W)) + val b = Input(UInt(32.W)) + val c = Output(UInt(32.W)) + val d = Output(UInt(32.W)) + val en = Input(Bool()) + }) + + // Call DPI only when `en` is true. + when (io.en) { + io.c := AddClocked(io.a, io.b) + io.d := AddUnclocked(io.a, io.b) + } .otherwise { + io.c := 0.U(32.W) + io.d := 0.U(32.W) + } +} +``` + +```scala mdoc:verilog +chisel3.docs.emitSystemVerilog(new AddTest) +``` + + +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 mdoc:silent + +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 SumTest extends Module { + val io = IO(new Bundle { + val a = Input(Vec(3, UInt(32.W))) + val sum_a = Output(UInt(32.W)) + }) + + io.sum_a := Sum(io.a) // compute a[0] + a[1] + a[2] +} +``` + +```scala mdoc:verilog +chisel3.docs.emitSystemVerilog(new SumTest) +``` + +# 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. diff --git a/src/main/scala/chisel3/util/circt/DPI.scala b/src/main/scala/chisel3/util/circt/DPI.scala index dd0a7e69d14..e86df4059be 100644 --- a/src/main/scala/chisel3/util/circt/DPI.scala +++ b/src/main/scala/chisel3/util/circt/DPI.scala @@ -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 @@ -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: _*) diff --git a/src/test/scala/chiselTests/DPISpec.scala b/src/test/scala/chiselTests/DPISpec.scala index 5f1bbbb90f1..e1edc00bcb6 100644 --- a/src/test/scala/chiselTests/DPISpec.scala +++ b/src/test/scala/chiselTests/DPISpec.scala @@ -17,6 +17,7 @@ private object EmitDPIImplementation { val dpiImpl = s""" |#include |#include + |#include | |extern "C" void hello() |{ @@ -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 { @@ -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() } @@ -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 { @@ -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) @@ -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