探讨:toffee 框架扩展以支持模拟/混合信号验证 #25
Replies: 8 comments 2 replies
-
|
如果底层能提供模拟信号给到 toffee ,对toffee改动应该不大 |
Beta Was this translation helpful? Give feedback.
-
Beta Was this translation helpful? Give feedback.
-
Beta Was this translation helpful? Give feedback.
-
|
目前看来唯一会对Toffee底层改动的地方,分析结果如下: 核心耦合点
async def __clock_loop(dut):
while True:
await execute_all_coros()
dut.Step(1) # ← 硬编码:推一个时钟周期
dut.event.set() # ← 硬编码:通过 dut 的 event 通知所有等待者
dut.event.clear()这是整个 toffee 的"心跳"。每个循环做三件事:
问题:dut.Step(1) 是 picker 编译出的数字 DUT 的方法,直接调用 Verilator/VCS 推一个时钟周期。模拟仿真没有这个概念——SPICE 的时间推进是连续的,不是按时钟步进的,这里需要考虑。
async def step(self, ncycles=1):
if self.__clock_event is None:
critical("cannot use step in bundle without a connected signal")
for _ in range(ncycles):
await self.__clock_event.wait()Bundle 的 step 等的是 __clock_event,这个 event 来自 DUT 的 XPin 的 signal.event(bundle.py:1153-1154)。XPin 是 picker 生成的数字信号对象,它的 event 在 dut.Step(1) 时被 set。
async def __monitor_forever(self):
while True:
await self.agent.monitor_step() # ← 等一个时钟步
ret = await self.func(self.agent)monitor_step 来自 agent.py:28:self.monitor_step = bundle.step。所以 Monitor 也是每个时钟周期执行一次。
class Agent:
def __init__(self, bundle):
if callable(bundle):
self.monitor_step = bundle
else:
self.bundle = bundle
self.monitor_step = bundle.step # ← 绑定到 bundle 的 stepAgent 初始化时绑定了 bundle.step 作为 monitor 的推进函数。
async def __main_wrapper(self):
while True:
loop = asyncio.get_event_loop()
if hasattr(loop, "global_clock_event"):
await loop.global_clock_event.wait()
break
await self.main()Model 只是等 clock 启动后才执行 main,这是个一次性等待,影响不大。总结:影响范围
核心结论 需要做的改动是把这条链的"源头"抽象化,具体来说:
对现有数字用户的影响:如果用抽取接口 + 默认实现的方式做,现有数字代码不需要任何修改。__clock_loop 的默认行为保持原样(dut.Step(1)),只有创建混合信号 Env 时才替换时间推进器。 改动量估计:主要集中在 asynchronous.py 中约 30-50 行的重构,加上新增 AnalogBundle 和 AnalogAgent 子类。现有代码的行为不变。 |
Beta Was this translation helpful? Give feedback.
-
|
@yaozhicheng @Miical @Makiras |
Beta Was this translation helpful? Give feedback.
-
|
关于时间同步,还有一种模式,根据场景不同,可以选择,具体讲解如下: lazy模式是zero ASIC组织的开源repo“switchboard”中的数模交互的方式,它是以数字为主,模拟为辅的一种交互方式。SPICE 仿真引擎不会主动往前跑。只有当数字侧需要读模拟数据时,才检查 SPICE 是否跑到了对应的时间点,没到的话就推进到那个时间点。 用一个例子说明: 而Lazy模式呢: 整个 lazy 模式只有三个原语:
总结一下上面所有讨论的的区别:
Lazy 模式和 toffee 的协程模型天然契合。 async def __clock_loop(dut):
while True:
await execute_all_coros() # 执行所有就绪协程
dut.Step(1) # 推进时间
dut.event.set() # 通知等待者
dut.event.clear()如果用 lazy 模式,扩展非常自然: async def __clock_loop(dut, spice_session=None):
while True:
await execute_all_coros()
dut.Step(1)
if spice_session:
# lazy: 只在需要时推进 SPICE 到当前数字时间
spice_session.advance_to(current_time)
dut.event.set()
dut.event.clear()不需要改造 toffee 的异步框架。ngspice 回调模式要求 SPICE 在每个内部时间步调用 Python,这意味着要把 Python 协程调度嵌入到 C 回调中——跨语言的协程交互非常复杂。Lazy 模式完全在 Python侧控制流程,回避了这个问题。 Lazy 模式的真正问题 考虑一个场景:comparator 的输出在两个时钟沿之间翻转了。 如果 SB_CLK 频率不够高,数字侧会延迟感知模拟侧的事件。这对某些场景不是问题(LDO 稳压、缓慢变化),但对另一些场景是致命的(SAR ADC 的比较器结果必须在当拍用)。 因此toffee 可以同时支持两种模式,让 Agent 声明自己需要哪种:
class LdoAgent(AnalogAgent):
sync_mode = SyncMode.LAZY
async def measure_vout_dc(self):
# 推进 SPICE 到足够远,让 LDO 稳定
await self.spice.advance_to(self.current_time + 100e-6)
return self.spice.read("v(vout)")场景 2:Lazy 模式不够的
这些场景需要更紧密的同步。但这里有个关键洞察:即使是这些场景,也不需要 ngspice 回调级别的逐步同步。实际上它们需要的是——在特定时间点精确同步,也就是 Xyce 的 simulateUntil 模式。 class SarAdcAgent(AnalogAgent):
sync_mode = SyncMode.STEP_EXACT
async def compare_one_bit(self, dac_code):
# 设置 DAC 输出
self.spice.put("v_dac", dac_code * self.lsb)
# 精确推进到比较完成的时刻
await self.spice.advance_to(self.current_time + self.compare_period)
# 立即读 comparator 输出
comp_out = self.spice.read("v(comp_out)")
return comp_out > self.threshold这仍然是 lazy 的变体——不是 SPICE 主动推数据,而是数字侧在需要时精确推进到目标时间。和 switchboard 的区别只是推进粒度更细(不是按 SB_CLK,而是按逻辑需要的时间点)。 场景 3:真正需要 SPICE 主动通知的 因为验证的驱动权始终在测试用例(Python)手里。测试用例知道什么时候该读数据。不像真实芯片中模拟侧会产生异步中断——在仿真环境中,测试用例控制着时间推进,它总是可以选择在合适的时间点去 get。 ngspice 的回调模式(GetVSRCData/SendData/GetSyncData)真正有用的场景是:数字仿真器和 SPICE 在同一个进程中作为对等的仿真引擎并行运行,谁也不从属于谁。但在 toffee 的架构中,Python是"上帝视角"的控制者,它不需要被动接收 SPICE 的回调。 结论
具体实现上: class SpiceSession:
def advance_to(self, target_time: float):
"""Lazy 推进:如果 SPICE 已经到了,什么都不做"""
if target_time > self._current_time:
if self._engine == 'xyce':
xyce_simulateUntil(self._obj, target_time, ...)
elif self._engine == 'ngspice':
# ngspice 共享库也可以做类似的事:
# 设置断点时间,调用 run,让它跑到目标时间停下
self._cmd(f"stop when time >= {target_time}")
self._cmd("run")
def put(self, name: str, value: float):
"""记录值,不推进"""
# Xyce: updateTimeVoltagePairs
# ngspice: alter 或 VSRC 数据注入
def get(self, name: str) -> float:
"""读值,必要时先推进到当前数字时间"""
self.advance_to(self._digital_time)
return self._read_node(name)之前的 ngspice 回调方案和xyce时间同步方案可以作为可选的高级模式保留,供那些确实需要 SPICE 内部每步都参与的极端场景(比如自定义的非线性器件模型需要与数字侧实时交互)。但对于 toffee 的 Phase 0~Phase 3,lazy 模式足够了,而且实现简单得多。 这也意味着 Phase 0 的 ngspice 绑定可以更简单——不需要一开始就实现复杂的回调注册,只需要能 source、run(跑到指定时间)、read、alter 即可。 |
Beta Was this translation helpful? Give feedback.
-
|
整体来说改动不大,只需要把同步时钟那里做出一个选项即可,数字还是数字,模拟这边是模拟的。模拟和数字之间交互是新增加的,交互会数字对数字侧的逻辑没有改动 |
Beta Was this translation helpful? Give feedback.
-
|
看着可行,具体开发我们可以协同 |
Beta Was this translation helpful? Give feedback.





Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
起因
toffee 的核心理念是"让软件人员不需要懂硬件也能写验证",通过 Bundle/Agent/Env/Model 的分层抽象,把硬件细节封装在 Agent 内部,测试用例只和业务语义打交道。
这套抽象在数字验证中已经验证可行。但实际芯片中大量存在模拟和混合信号模块(LDO、ADC、PLL、Comparator 等),这些模块目前仍然依赖手工 SPICE 仿真 + 人眼看波形,没有自动化回归能力。
我想探讨的是:toffee 的现有抽象体系能否自然地扩展到模拟/混合信号领域?需要做哪些改动?
toffee 现有抽象的适用性分析
toffee 的分层设计:
TestCase → Env → Agent → Bundle → DUT (picker 编译的 .so)
对于模拟扩展,这套分层关系基本成立,但每一层都需要处理模拟域的新问题:
Bundle 层 — 信号类型扩展
数字 Bundle 中的 pin 是离散的 0/1。模拟域需要引入连续值信号:
典型例子是 comparator 的输出:模拟侧它是连续电压(0.1V~1.7V),数字侧它是逻辑 0/1。当输入差分接近零时,输出处于不定态——既不是稳定的高也不是稳定的低。
我的想法是引入 AnalogSignal:
这样同一个信号,comparator 的 Agent 用 .voltage 做模拟断言,下游数字 Agent 用 .digital 做逻辑断言,框架自动处理视角转换。
Agent 层 — 驱动方式的根本差异
数字 Agent 驱动的是 picker 编译的 .so,操作是读写 pin、推时钟。模拟 Agent 驱动的是 SPICE 仿真引擎(ngspice 共享库),操作是:
但对上层 Env 和 TestCase 来说,Agent 的接口应该是一致的——这正是 toffee 分层的价值。测试用例不需要知道底下跑的是 Verilator 还是 ngspice。
Agent 层 — loading effect 的处理
这是模拟验证中一个核心问题:模拟模块的输出驱动能力有限,负载会影响电路行为。
经过思考,结论是:负载是 netlist 中的真实 R/C 元件,但其参数值通过 Agent 调用 alter 动态修改。这样可以在不同测试用例中 sweep 负载参数,找到电路的 spec 边界:
这里的关键认识是:alter 修改的是 netlist 中已有元件的参数,负载始终是真实的电路元件(有阻抗特性),不是抽象的"参数"。早期验证阶段后级电路还没有时,用理想 R/C 做负载 + sweep。 找边界,这本身就是模拟工程师的标准做法。
Agent 层 — 内部节点的诊断
数字验证中,Agent 只看端口信号。模拟验证中,失败时经常需要深入到内部节点(比如 LDO 内部的 vref、vfb、verr)才能定位问题。
我的想法是在 Agent 中引入三级诊断:
这不改变 toffee 的分层——诊断逻辑封装在 Agent 内部,测试用例不感知。
Env 层 — 混合信号组合
MixedSignalEnv 组合数字 Agent 和模拟 Agent,核心新增的是同步机制:
这一层对 TestCase 透明。TestCase 只是同时操作数字 Agent 和模拟 Agent,Env 负责保证它们的时间一致。
分析结果层 — 新增抽象
数字验证中没有"分析类型"的概念。模拟验证中,DC/AC/Tran/Noise 的结果格式和断言方式完全不同:
需要引入 AnalysisResult 体系,各子类提供专用工具方法:
拟定的实施路线
Beta Was this translation helpful? Give feedback.
All reactions