You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何覆盖/扩展Chisel信号命名规则?自定义Verilog信号前缀需求

实现Chisel生成Verilog时自定义输入输出端口前缀

这确实是个很实际的需求——毕竟很多团队都有自己固定的Verilog信号命名规范,Chisel默认的io_前缀确实不太适配。我这里有几个从局部到全局的解决方案,你可以根据自己的项目规模选择:

方案一:自定义Module基类(适合单个项目快速适配)

如果只是想在自己的项目里实现这个规则,不用改Chisel库源码,可以写一个自定义的Module基类,帮你自动处理端口命名:

import chisel3._

abstract class CustomModule extends Module {
  // 工具函数:给Input信号添加i_前缀
  protected def i[T <: Data](data: T): T = {
    data.suggestName(s"i_${data.getSuggestedName}")
    data.asInput
  }

  // 工具函数:给Output信号添加o_前缀
  protected def o[T <: Data](data: T): T = {
    data.suggestName(s"o_${data.getSuggestedName}")
    data.asOutput
  }

  // 重写io的生成,后续配合字符串替换去掉默认io前缀
  override val io = IO(new Bundle {})
}

然后你写模块的时候就可以这么用:

class MyTop extends CustomModule {
  // 替换原来的Input(...)和Output(...)
  val io = IO(new Bundle {
    val addr = i(UInt(32.W))
    val data_in = i(UInt(32.W))
    val data_out = o(UInt(32.W))
    val en = i(Bool())
  })

  // 模块逻辑示例
  io.data_out := Mux(io.en, io.data_in, 0.U)
}

最后一步去掉Chisel自动加上的io_前缀,生成Verilog时做简单的字符串替换:

object MyTopDriver extends App {
  val verilog = chisel3.util.experimental.getVerilogString(new MyTop)
  // 替换前缀,同时保留时钟复位的原始命名
  val modifiedVerilog = verilog
    .replaceAll("io_i_", "i_")
    .replaceAll("io_o_", "o_")

  // 写入目标文件
  import java.io._
  val pw = new PrintWriter(new File("MyTop.v"))
  pw.write(modifiedVerilog)
  pw.close()
}

这个方法简单粗暴,适合小项目快速落地,不需要深入Chisel底层。

方案二:扩展VerilogEmitter(库级全局修改,无需每个模块改代码)

如果你想让整个Chisel项目都自动遵循这个命名规则,不用每个模块都写i()/o(),那可以扩展Chisel的VerilogEmitter,从FIRRTL层面修改端口命名逻辑:

首先创建自定义Emitter:

import chisel3._
import chisel3.stage._
import firrtl._
import firrtl.ir._

class CustomVerilogEmitter extends VerilogEmitter {
  override def emitPort(port: Port, namespace: Namespace): String = {
    val originalName = port.name
    // 判断端口类型,跳过时钟复位信号
    val newName = if (originalName.startsWith("io_")) {
      val baseName = originalName.drop(3)
      port.direction match {
        case Input if baseName != "clock" && baseName != "reset" => s"i_$baseName"
        case Output => s"o_$baseName"
        case _ => originalName // 保持时钟复位和其他特殊端口不变
      }
    } else {
      originalName
    }
    // 调用父类方法生成端口代码,传入修改后的名字
    super.emitPort(port.copy(name = newName), namespace)
  }
}

然后在生成Verilog时指定使用这个Emitter:

object CustomDriver extends App {
  val args = Array("--target-dir", "generated")
  Driver.execute(args, () => new MyTop) {
    (firrtlCompiler, firrtlCircuit) =>
      val emitter = new CustomVerilogEmitter
      emitter.emit(firrtlCircuit, firrtlCompiler.getLoweredCircuit(firrtlCircuit))
  }
}

这个方案属于库级别的全局配置,一旦设置好,所有模块生成的Verilog都会自动替换前缀,非常适合大型项目或者需要统一规范的团队,而且不需要修改Chisel源码。

方案三:使用FIRRTL Transform(更灵活的全局修改)

如果还需要更复杂的命名规则(比如某些信号例外、按模块类型区分规则),可以写一个FIRRTL Transform来遍历所有端口并修改名字:

import firrtl._
import firrtl.ir._
import firrtl.transforms._

class PortRenameTransform extends Transform {
  override def prerequisites = Seq.empty
  override def optionalPrerequisites = Seq.empty
  override def optionalPrerequisiteOf = Seq.empty
  override def invalidates(a: Transform) = false

  override def execute(state: CircuitState): CircuitState = {
    val circuit = state.circuit
    val renamedModules = circuit.modules.map {
      case m: Module =>
        val renamedPorts = m.ports.map { port =>
          port.name match {
            case name if name.startsWith("io_") =>
              val baseName = name.drop(3)
              port.direction match {
                case Input if baseName != "clock" && baseName != "reset" =>
                  port.copy(name = s"i_$baseName")
                case Output =>
                  port.copy(name = s"o_$baseName")
                case _ => port
              }
            case _ => port
          }
        }
        m.copy(ports = renamedPorts)
      case other => other
    }
    state.copy(circuit = circuit.copy(modules = renamedModules))
  }
}

然后在Driver中添加这个Transform:

object TransformDriver extends App {
  val args = Array("--target-dir", "generated")
  Driver.execute(args, () => new MyTop) {
    (compiler, circuit) =>
      val transformedCircuit = compiler.transform(circuit, Seq(new PortRenameTransform))
      VerilogEmitter.emit(transformedCircuit)
  }
}

这个方法的优势是灵活性极高,你可以根据需求添加任意多的判断条件,比如给特定模块的端口设置不同前缀、跳过某些特殊信号等。


需要注意的是,不建议直接修改Chisel核心库的源码(比如修改默认的VerilogEmitter),这会导致你的项目依赖自定义的Chisel版本,后续升级会非常麻烦。上面的方案都是基于Chisel公开API实现的,兼容性更好,也更易维护。

内容的提问来源于stack exchange,提问作者Hoohoo

火山引擎 最新活动