TLM-2.0 近似定时 AT:四相握手与多事务在途
最后更新:2026-09-13
D1/D2 的 LT 用一次阻塞调用完成一件事,简单快,但调用没返回就发不出下一个事务,无法表达真实总线上"请求已发出、响应还在路上"的重叠。本篇(D3)讲非阻塞的近似定时(Approximately-Timed, AT)——用四个相位把一次事务拆成可挂起、可重叠的事件流,这是建模流水线、outstanding 队列、带宽竞争的标准工具。
一、为什么阻塞不够:一个时序图
真实 AXI 式读事务:地址在拍 1 发出,数据可能在拍 8 才回来;在这 7 拍里总线还能接受新地址(多个 outstanding)。LT 的 b_transport 让 initiator 一直阻塞到拍 8,期间什么都不能发——模型里总线永远没有"在途队列",带宽与延迟被混为一谈。

AT 用非阻塞调用立即返回 + 相位回调表达这种重叠:发起方调用 nb_transport_fw 后立刻拿回控制权,事务后续由对方在适当时刻通过相位推进。
二、四个相位与两条路径
一次事务最多经历四个相位(phase),沿前向/后向两条路径流动:
| 相位 | 方向 | 含义 |
|---|---|---|
BEGIN_REQ | fw(ini→tgt) | 请求起始:地址/命令有效 |
END_REQ | bw(tgt→ini) | target 确认"请求已收下",请求通道可复用 |
BEGIN_RESP | bw(tgt→ini) | 响应/数据起始 |
END_RESP | fw(ini→tgt) | initiator 确认"响应已收",事务闭环 |
不是每个相位都必须显式走:简单模型可在第一次调用就 TLM_COMPLETED(退化成类 LT);但要建模排队,就应让请求在 BEGIN_REQ 后停留、target 用 END_REQ/BEGIN_RESP 在未来时间点推进。
配套三个返回状态,驱动非阻塞状态机:
| 返回 | 含义 | 后续 |
|---|---|---|
TLM_ACCEPTED | 我收下了这次相位通知,但事务没完 | 等对方下一次回调推进 |
TLM_UPDATED | 我已把 phase 和 t 改成新值(就地推进) | 调用方按新相位/时间继续 |
TLM_COMPLETED | 事务到此结束(常用于提前完成) | 不再有回调 |
关键纪律:nb_transport 绝不能阻塞、不能 wait。需要延迟就在内部 e.notify(t) 挂一个事件,到时由自己的线程/方法发起下一次相位调用。这样一次调用栈瞬时返回,多个事务的相位事件在事件内核里交错,自然形成流水线。
三、时间参数 t:点对点共享的时间注解
和 LT 的 delay 一样,AT 的 sc_time& t 是沿路径累加的时序注解,表示"相对于当前仿真时刻,这次相位事件计划在多久后生效"。规则:
- 调用时 t 携带"距现在的偏移";被调方可增加它(加入自己的延迟)。
- 返回
TLM_UPDATED时,t 表示新相位的生效偏移。 - 不应在调用栈内直接
wait(t);通常由调用边界/时间量子统一兑现(D4)。 - 每次相位推进都只加自己那段延迟,全路径延迟 = 各段之和,且与在途事务数无关——这就把"延迟"和"带宽/竞争"分开了。
五、可编译的 AT 骨架(target 侧核心)
下面给出 target 侧非阻塞接口的形状:收到 BEGIN_REQ 后用一个定时事件延迟,再从独立线程发起 BEGIN_RESP。initiator 侧对称实现 nb_transport_bw。为突出相位逻辑,省略 payload 内存搬运(与 D1 相同)。
// file: at_4phase.cpp —— IEEE 1666 API + C++17
#include <systemc.h>
#include <tlm.h>
#include <tlm_utils/simple_target_socket.h>
#include <iostream>
struct AtTarget : sc_module {
tlm_utils::simple_target_socket<AtTarget> sock{"sock"};
sc_event e_resp; // 到点发响应
tlm::tlm_generic_payload* pending = nullptr;
sc_time resp_lat{20, SC_NS};
SC_CTOR(AtTarget) {
sock.register_nb_transport_fw(this, &AtTarget::nb_transport_fw);
SC_THREAD(responder);
}
// 前向:处理 initiator 来的相位
tlm::tlm_sync_enum nb_transport_fw(tlm::tlm_generic_payload& pl,
tlm::tlm_phase& phase, sc_time& t) {
if (phase == tlm::BEGIN_REQ) {
// 1) 立即"收下"请求:可在此排进队列、计带宽账
pending = &pl;
e_resp.notify(resp_lat + t); // 不阻塞,挂未来事件
return tlm::TLM_ACCEPTED; // 事务未完成,等后续相位
}
if (phase == tlm::END_RESP) {
// initiator 确认响应收讫,payload 可回收
return tlm::TLM_COMPLETED;
}
return tlm::TLM_ACCEPTED;
}
void responder() {
while (true) {
wait(e_resp);
if (!pending) continue;
tlm::tlm_phase phase = tlm::BEGIN_RESP;
sc_time t = SC_ZERO_TIME;
// 2) 后向路径主动发起响应;initiator 的 nb_transport_bw 被调用
tlm::tlm_sync_enum r = sock->nb_transport_bw(*pending, phase, t);
if (r == tlm::TLM_COMPLETED) pending = nullptr;
}
}
};initiator 侧(nb_transport_bw)处理 END_REQ(请求通道释放,可发下一请求——这就是多事务在途的入口)与 BEGIN_RESP(取数据、回 END_RESP)。把多个 payload 在 END_REQ 前就依次用 BEGIN_REQ 发出去并各自记录,即得到 outstanding 窗口;窗口大小、响应乱序、超时取消都能在这套状态机上表达。
六、AT 能建模、LT 不能建模的东西
| 现象 | LT | AT |
|---|---|---|
| 多事务 outstanding、响应乱序 | 否 | 是(每 payload 独立状态机) |
| 流水线/突发拆分(一个请求多拍吐数据) | 仅整体延迟 | 是(相位 + streaming + 定时事件) |
| 反压(target 暂不能收) | 只能阻塞调用栈 | 是(推迟 END_REQ) |
| 多主竞争总线、吞吐 vs 延迟 | 串行近似,偏乐观 | 是(队列 + 仲裁 + 带宽账本) |
| 死锁/活锁(信用不释放) | 难观测 | 可显式建模并检测 |
| 跑 OS 的速度 | 快 | 明显慢 |
研究 NoC/内存控制器/Cache 互联的带宽与排队时,AT 是性价比最高的层级;比它更细的逐信号握手通常只在 transactor 接 RTL 时才需要(F5)。
七、LT/AT 混用与适配
真实平台常常混用:CPU 簇内部走 LT(跑软件要快),经过一个 LT↔AT 适配桥进入 AT 的内存子系统(要竞争精度)。适配桥本质上:
- LT→AT:把一次
b_transport转成BEGIN_REQ…END_RESP,用事件/信号量等待 RESP,再返回阻塞调用; - AT→LT:把四相折叠成一次阻塞调用并累加延迟。
桥本身是性能模型里的重要部件,务必让两边的 sc_time 口径一致,避免延迟在边界被重复计算。
八、常见陷阱
| 陷阱 | 后果 | 建议 |
|---|---|---|
在 nb_transport 里 wait | 破坏非阻塞、可能重入死锁 | 只发事件,延迟到独立线程兑现 |
| 相位返回值用错(已 COMPLETED 还等回调) | 事务挂死 | 严格按三返回值推进状态机 |
不处理 END_REQ 就想发下一请求 | 退化成 LT、无 outstanding | 以 END_REQ 释放请求窗口 |
| payload 单例复用 | 在途事务互相覆盖 | 每在途事务一个 payload + TxState |
| 时间 t 重复累加/在栈内 wait | 延迟虚高 | t 只沿路径加,边界统一兑现 |
| 全系统都上 AT | 速度崩溃 | 仅竞争热点用 AT,其余 LT |
九、与后续衔接
D4 的时间量子让 LT 在保持阻塞模型的同时进一步榨取速度,是 LT/AT 之外的第三条提速线;D5 用 payload extension 在事务上挂 snoop/事务私有状态,AT initiator 的 TxState 就常放在 extension 里。缓存一致性流量(MESI 在总线上长什么样)的真机背景见 concepts/cache,建模在 B 模块。
九、AT 状态机的调试经验:事务从"调用"变成"生命期"
写 LT 时,心智模型还是过程式的:发起调用、拿到返回、继续执行;写 AT 必须切换到事件驱动的对象生命期视角——一个 payload 从 BEGIN_REQ 到 END_RESP 可能跨越几十个仿真时刻、穿过多个模块、被排队、被仲裁、被拆分,任何时刻它都处在一个确定的相位,由某个事件推动迁移。这个思维转换是 D3 真正的学习难点。推荐的实现纪律是给每个在途事务建一个显式的状态结构(通常用 D5 的扩展携带),其中至少记录:当前相位、各相位进入时间戳、经历过的跳数、最终响应状态、以及它占用的资源(缓冲槽、信用)。有了这份记录,事务在系统里的完整轨迹就能从日志重建,"某个请求为什么永远没收到响应"这类问题才能定位——常见根因无非三种:某个相位的返回值处理错导致没人继续推进、目标侧忙时把请求排进队列却没有在资源空闲时发事件、或 END_RESP 没发导致发起端资源不释放。
AT 模型最典型的挂死症状是信用/窗口泄漏:协议要求 target 用 END_REQ 表示请求通道已空出、initiator 用 END_RESP 归还响应信用,如果任一路径(包括错误响应、提前完成、事务取消)漏掉一次相位通知,可用窗口就会单调缩小,仿真在运行成千上万事务后才彻底停住。这类 bug 极难靠肉眼看代码发现,正确做法是建两个全局/每连接计数器——已发请求数与已收 END_REQ 数、已收 BEGIN_RESP 与已发 END_RESP 数——在仿真任意时刻断言差额不超过窗口容量,且在系统空闲时差额归零。把这些不变量做成检查项放进回归,比事后调试高效一个量级。
另一个要建立的直觉是相位的时间语义和协议语义要分开看。nb_transport 携带的时间参数说的是"这次相位事件相对现在何时生效",而相位本身说的是"协议状态迁移到哪一步";两者正交。常见错误是用"调用返回了"当作"事务被处理了"——在非阻塞语义里,返回 ACCEPTED 只意味着对方记录了这次通知,真正的处理可能被推迟到未来某个事件。反过来也不能假设所有相位都必须跨越仿真时间:一次资源空闲的简单 target 可以在同一调用链里就地推进多个相位(通过返回 UPDATED 改写实参),这是合法的退化,和 LT 的快速路径等价。读别人写的 AT 模型时,先把每个模块对四个相位分别做什么、何时发未来事件、何时就地完成列成表,比顺着控制流读代码有效得多。
十、什么时候值得付出 AT 的复杂度
最后回到工程判断。AT 比 LT 多出来的不只是代码复杂度,还有调试成本、仿真速度损失和团队认知负担,因此"能写 AT"不等于"应该处处 AT"。一条实用判据是问:研究结论是否依赖多个事务在时间上的重叠方式? 如果答案是肯定的——内存级并行带来的带宽收益、多主竞争下的公平性与尾延迟、有限缓冲导致的背压传播、流水线拆分对吞吐的影响、死锁与活锁的可能性——LT 在原理上就看不见这些现象,必须上 AT。如果问题只涉及孤立请求的总延迟、软件的功能行为、或粗粒度的容量规划,LT 配合良好的延迟注解就足够,而且它的速度优势允许跑完整操作系统和大量真实负载,统计样本量本身也是结论可信度的一部分。
更成熟的做法是在同一个系统里按风险分配精度:处理器簇和软件交互频繁的部分保持 LT 快跑,只有被怀疑是瓶颈的互联和内存子系统用 AT 建模,两者通过协议适配桥连接。这样既保住跑软件的速度,又在真正决定方案成败的竞争点上拿到可信结果。采用这种混合架构时,要特别关注边界两侧的事务计数和时间口径一致:穿过桥的每个事务都应能在两侧日志里对上号,延迟只允许被记账一次。D4 的时间量子还会给 LT 侧引入受控的时间超前,与 AT 侧对接时必须在边界强制同步,否则一个"来自未来"的请求进入严格按事件排队的队列,会产生真机上不存在的调度。把这几个边界设计对,AT 才是服务决策的利器而不是仿真器的负担。
十一、AT 建模的最小推进路径
如果要在团队里从零引入 AT,建议按一条经过验证的最小路径推进,避免一上来就写完整四相导致进度失控。第一步:LT 基线。先用阻塞传输把功能跑通——地址、数据、错误响应、路由全部正确,先不考虑多事务在途。第二步:加事务身份。给每个事务引入唯一 ID 和状态记录(先用普通成员表,再迁移到 D5 的扩展),此时传输仍然阻塞,但系统已经能区分和追踪每个事务。第三步:只拆请求相位。把阻塞调用改成 BEGIN_REQ 非阻塞,target 立即返回 ACCEPTED,用定时事件在未来发 BEGIN_RESP 并就地完成(initiator 收到 RESP 即视为结束,暂不显式回 END_RESP)。这一步结束后系统已经支持多个在途请求,且改动面可控。第四步:补齐完整握手。引入 END_REQ 释放请求窗口、END_RESP 关闭事务,加上响应窗口与信用计数。第五步:加真实排队与反压。在 target 侧实现有限缓冲与按带宽出队,在总线侧实现仲裁,此时才能产出有意义的带宽/延迟曲线。
每一步都保持系统可运行、有可对比的输出(事务数、总时间、按 ID 的事务轨迹),出问题立刻知道是哪一步引入的。直接从 LT 跳到完整 AT 的项目通常会卡在"所有事务都挂着不动"——相位协议里任何一个通知没对上都会死锁,而没有中间形态可以定位。推进过程中配一个协议检查器也非常值得:它旁路监听所有 fw/bw 调用,验证非法相位迁移(如没收到 BEGIN_REQ 就来 BEGIN_RESP)、信用超发、未闭合事务,违例立即报告。这个检查器本身不影响功能,却能把数小时的波形压缩成一行精确的错误信息,是 AT 开发期性价比最高的工具。等模型稳定后检查器可以降为只统计不报错,长期在回归里守护协议纪律。
最后重申 AT 的产出观:它回答的是系统级时序问题——吞吐拐点在哪、加多少 outstanding 窗口才饱和、哪个队列是瓶颈、多主竞争如何分配带宽。评价一个 AT 模型好不好,看的是这些曲线的形状和拐点位置是否与 RTL/真机一致(通常按负载率扫描画出延迟—吞吐曲线),而不是逐拍周期数相等。把这个目标想清楚,建模时就不会在无关紧要的单事务延迟上过度投入,也不会漏掉真正决定结论的排队结构。
总结
AT 用四个相位(BEGIN/END_REQ、BEGIN/END_RESP)和 ACCEPTED/UPDATED/COMPLETED 三个非阻塞返回值,把一次事务拆成可挂起、可在时间上重叠的事件流,从而在不逐拍仿真信号的前提下,建模多事务在途、流水线、反压与总线竞争;代价是每端点都要维护非阻塞状态机,因此只在真正研究带宽/排队的部件上使用。
延伸阅读
- 上一篇:TLM-2.0 socket、payload 与双向/多绑(D2)
- 下一篇:TLM-2.0 时间量子与 temporal decoupling(D4)
- 事务私有数据:TLM-2.0 通用 payload 扩展与 snoop(D5)
- 接 RTL:多引擎协同仿真(F5)
四、四相状态机(双方各自维护)

initiator 侧要为每个在途 payload 维护状态(已发请求 / 已收 END_REQ / 已收 RESP / 已回 END_RESP),常见做法是用 payload 指针做 key 放进一个 std::map<payload*, TxState> 或扩展对象(D5 的 extension 正好用来挂 TxState)。