Repository navigation
探讨:toffee 框架扩展以支持模拟/混合信号验证 #25
Replies: 8 comments 2 replies
|
如果底层能提供模拟信号给到 toffee ,对toffee改动应该不大 |
|
目前看来唯一会对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 子类。现有代码的行为不变。 |
|
@yaozhicheng @Miical @Makiras |
|
关于时间同步,还有一种模式,根据场景不同,可以选择,具体讲解如下: 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 即可。 |
|
整体来说改动不大,只需要把同步时钟那里做出一个选项即可,数字还是数字,模拟这边是模拟的。模拟和数字之间交互是新增加的,交互会数字对数字侧的逻辑没有改动 |
|
看着可行,具体开发我们可以协同 |





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 体系,各子类提供专用工具方法:
拟定的实施路线
All reactions