Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add DPI intrinsics and API #4158

Merged
merged 5 commits into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 141 additions & 0 deletions src/main/scala/chisel3/util/circt/DPI.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// SPDX-License-Identifier: Apache-2.0

package chisel3.util.circt.dpi

import chisel3._

import chisel3.experimental.{IntParam, IntrinsicModule, Param, StringParam}

private object GetDPIParams {
def apply(
functionName: String,
isClocked: Boolean,
inputNames: Option[Seq[String]],
outputName: Option[String] = None
): Seq[(String, Param)] = {
val inputNamesParam =
inputNames.map(_.mkString(";")).map(x => Seq("inputNames" -> StringParam(x))).getOrElse(Seq())
val outputNameParam = outputName.map(x => Seq("outputName" -> StringParam(x))).getOrElse(Seq())
Seq[(String, Param)](
"functionName" -> functionName,
"isClocked" -> (if (isClocked) 1 else 0)
) ++ inputNamesParam ++ outputNameParam
}
}

object RawClockedNonVoidFunctionCall {

/** Creates an intrinsic that calls non-void DPI function at its clock posedge.
* The result values behave like registers and the DPI function is used as a state
* transfer function of them.
*
* `enable` operand is used to conditionally call the DPI since DPI call could be quite
* more expensive than native constructs.
*
* When an `enable` is false, it means the state transfer function is not called. Hence
* their values will not be modified in that clock.
*
* Please refer https://github.com/llvm/circt/blob/main/docs/Dialects/FIRRTL/FIRRTLIntrinsics.md#dpi-intrinsic-abi for DPI function ABI.
* @example {{{
* val a = RawClockedNonVoidFunctionCall("dpi_func_foo", UInt(1.W), clock, enable, b, c)
* }}}
*/
def apply[T <: Data](
functionName: String,
ret: => T,
inputNames: Option[Seq[String]] = None,
outputName: Option[String] = None
)(clock: Clock,
enable: Bool,
data: Data*
): T = {
IntrinsicExpr(
"circt_dpi_call",
ret,
GetDPIParams(functionName, true, inputNames, outputName): _*
)(
(Seq(clock, enable) ++ data): _*
)
}
}

object RawUnclockedNonVoidFunctionCall {

/** Creates an intrinsic that calls non-void DPI function for its input value changes.
* The DPI call is considered as a combinational logic.
*
* `enable` operand is used to conditionally call the DPI since DPI call could be quite
* more expensive than native constructs.
* When `enable` is false, results of unclocked calls are undefined and evaluated into X.
*
* Please refer https://github.com/llvm/circt/blob/main/docs/Dialects/FIRRTL/FIRRTLIntrinsics.md#dpi-intrinsic-abi for DPI function ABI.
* @example {{{
* val a = RawUnclockedNonVoidFunctionCall("dpi_func_foo", UInt(1.W), enable, b, c)
* }}}
*/
def apply[T <: Data](
functionName: String,
ret: => T,
inputNames: Option[Seq[String]] = None,
jackkoenig marked this conversation as resolved.
Show resolved Hide resolved
outputName: Option[String] = None
)(enable: Bool,
jackkoenig marked this conversation as resolved.
Show resolved Hide resolved
data: Data*
): T = {
IntrinsicExpr(
"circt_dpi_call",
ret,
GetDPIParams(functionName, false, inputNames, outputName): _*
)(
(Seq(enable) ++ data): _*
)
}
}

object RawClockedVoidFunctionCall {

/** Creates an intrinsic that calls void DPI function at its clock posedge.
*
* Please refer https://github.com/llvm/circt/blob/main/docs/Dialects/FIRRTL/FIRRTLIntrinsics.md#dpi-intrinsic-abi for DPI function ABI.
* @example {{{
* RawClockedVoidFunctionCall("dpi_func_foo", UInt(1.W), clock, enable, b, c)
* }}}
*/
def apply(
functionName: String,
inputNames: Option[Seq[String]] = None
)(clock: Clock,
enable: Bool,
data: Data*
): Unit = {
Intrinsic("circt_dpi_call", GetDPIParams(functionName, true, inputNames): _*)(
(Seq(clock, enable) ++ data): _*
)
}
}

// A common trait for DPI functions.
trait DPIFunctionImport {
def functionName: String
def inputNames: Option[Seq[String]] = None
}

// Base trait for a non-void function that returns `T`.
trait DPINonVoidFunctionImport[T <: Data] extends DPIFunctionImport {
def ret: T
def clocked: Boolean
def outputName: Option[String] = None
final def callWithEnable(enable: Bool, data: Data*): T =
if (clocked) {
RawClockedNonVoidFunctionCall(functionName, ret, inputNames, outputName)(Module.clock, enable, data: _*)
} else {
RawUnclockedNonVoidFunctionCall(functionName, ret, inputNames, outputName)(enable, data: _*)
}
final def call(data: Data*): T = callWithEnable(true.B, data: _*)
}

// Base trait for a clocked void function.
trait DPIClockedVoidFunctionImport extends DPIFunctionImport {
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: _*)
}
181 changes: 181 additions & 0 deletions src/test/scala/chiselTests/DPISpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package chiselTests.simulator

import chisel3._
import chisel3.experimental.ExtModule
import chisel3.simulator._
import chisel3.util.circt.dpi._
import circt.stage.ChiselStage
import chisel3.util.{HasExtModuleInline, HasExtModulePath, HasExtModuleResource}
import org.scalatest.funspec.AnyFunSpec
import org.scalatest.matchers.must.Matchers
import org.scalatest.matchers.should.Matchers.convertToAnyShouldWrapper
import chisel3.util.HasBlackBoxInline
import svsim._

private object EmitDPIImplementation {
def apply() = {
val dpiImpl = s"""
|#include <stdint.h>
|#include <iostream>
|
|extern "C" void hello()
|{
| std::cout << "hello from c++\\n";
|}
|
|extern "C" void add(int lhs, int rhs, int* result)
|{
| *result = lhs + rhs;
|}
""".stripMargin

class DummyDPI extends BlackBox with HasBlackBoxInline {
val io = IO(new Bundle {})
setInline("dpi.cc", dpiImpl)
setInline(s"$desiredName.sv", s"module $desiredName(); endmodule")
}
val dummy = Module(new DummyDPI)

}
}

class DPIIntrinsicTest 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))
})

EmitDPIImplementation()

// Void function
RawClockedVoidFunctionCall("hello")(clock, true.B)

// Stateless function with result
val result_clocked =
RawClockedNonVoidFunctionCall("add", UInt(32.W), Some(Seq("lhs", "rhs")), Some("result"))(clock, true.B, io.a, io.b)
val result_unclocked =
RawUnclockedNonVoidFunctionCall("add", UInt(32.W), Some(Seq("lhs", "rhs")), Some("result"))(true.B, io.a, io.b)

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

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

object AddClocked extends DPINonVoidFunctionImport[UInt] {
override val functionName = "add"
override val ret = UInt(32.W)
override val clocked = true
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 AddUnclocked 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)
}

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))
})

EmitDPIImplementation()

Hello()
val result_clocked = AddClocked(io.a, io.b)
val result_unclocked = AddUnclocked(io.a, io.b)
Comment on lines +98 to +100
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment for link


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

class DPISpec extends AnyFunSpec with Matchers {
describe("DPI") {
it("DPI intrinsics run correctly") {
val simulator = new VerilatorSimulator("test_run_dir/simulator/DPIIntrinsic")

simulator
.simulate(new DPIIntrinsicTest()) { module =>
import PeekPokeAPI._
val dpi = module.wrapped
dpi.reset.poke(true)
dpi.clock.step()

dpi.reset.poke(false)
dpi.io.a.poke(24.U)
dpi.io.b.poke(36.U)
dpi.io.add_unclocked_result.peek()
dpi.io.add_unclocked_result.expect(60)

dpi.clock.step()
dpi.io.a.poke(24.U)
dpi.io.b.poke(12.U)
dpi.io.add_clocked_result.peek()
dpi.io.add_clocked_result.expect(60)
dpi.io.add_unclocked_result.peek()
dpi.io.add_unclocked_result.expect(36)
}
.result

val outputFile = io.Source.fromFile("test_run_dir/simulator/DPIIntrinsic/workdir-verilator/simulation-log.txt")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally we would be a little DRY-er on these paths, deriving them from values defined above, but I'm not gonna block this PR on that.

val output = outputFile.mkString
outputFile.close()
output should include("hello from c++")
}
it("DPI API run correctly") {
val simulator = new VerilatorSimulator("test_run_dir/simulator/DPIAPI")

simulator
.simulate(new DPIAPITest()) { module =>
import PeekPokeAPI._
val dpi = module.wrapped
dpi.reset.poke(true)
dpi.clock.step()

dpi.reset.poke(false)
dpi.io.a.poke(24.U)
dpi.io.b.poke(36.U)
dpi.io.add_unclocked_result.peek()
dpi.io.add_unclocked_result.expect(60)

dpi.clock.step()
dpi.io.a.poke(24.U)
dpi.io.b.poke(12.U)
dpi.io.add_clocked_result.peek()
dpi.io.add_clocked_result.expect(60)
dpi.io.add_unclocked_result.peek()
dpi.io.add_unclocked_result.expect(36)
}
.result

val outputFile = io.Source.fromFile("test_run_dir/simulator/DPIAPI/workdir-verilator/simulation-log.txt")
val output = outputFile.mkString
outputFile.close()
output should include("hello from c++")
}
it("emits DPI correctly") {
val verilog = ChiselStage.emitSystemVerilog(
new DPIIntrinsicTest(),
firtoolOpts = Array("--lowering-options=locationInfoStyle=none,disallowPortDeclSharing")
)
verilog should include("import \"DPI-C\" function void add(")
verilog should include("input int lhs,")
verilog should include("input int rhs,")
verilog should include("output int result")
}
}
}