ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

AMBA ACE缓存一致性详解:侦听通道、事务类型与五态模型

AMBA ACE缓存一致性详解:侦听通道、事务类型与五态模型

多核系统里最让人头大的一类bug,是那种"代码逻辑看起来毫无问题、单核跑得好好的、一上多核就偶发读到旧值"的问题。我见过最典型的一次是某开发者在调试一个共享缓冲区场景:DMA侧明明已经写完并且置了标志位,CPU侧读到的却还是上一轮的残留数据,加了多少次内存屏障都不管用。最后定位下来,不是代码写错,是那条通路上根本没有一致性机制——DMA走的是普通AXI口,CPU的缓存里躺着一份谁也不知道的旧副本。

这类问题的答案,基本都指向AMBA体系里的ACE(AXI Coherency Extensions)总线。它是在AXI4的基础上扩展出来的一套带缓存一致性能力的接口协议,解决的就是"多个Master各自有缓存,谁来保证它们看到的内存是同一份"这个问题。如果你在做多核SoC、带缓存的加速器、或者需要挂DMA又不想天天手动刷cache,那ACE这套东西迟早要碰。这篇先不聊怎么配寄存器,而是把ACE到底是什么、多出来的那几个通道在干什么、事务类型怎么选、五态缓存模型怎么转,从头捋一遍——基础打不牢,后面看波形的时候每一拍都是天书。

1. 一致性需求的由来与ACE在AMBA家族中的位置

1.1 多核共享同一块内存时到底会发生什么

先把问题摊开讲。假设有两个带缓存的Master(可以是两个CPU核,也可以是一个CPU加一个带缓存的加速器),它们共享同一块物理内存。Master A把地址X的数据读进自己的缓存,标记为独占;接着Master B也要读X,它从内存里拿到了一份,也放进自己的缓存。这时候两份副本内容一样,看起来没问题。然后Master A对X做了写操作——注意,写的是它自己缓存里的那份,内存还没更新。此时Master B缓存里的那份就变成了"陈旧数据",它再读X,读到的是老的。

这就是缓存一致性问题的最小模型。解决它有两条路:一条是软件路,写完之后手动clean/invalidate缓存,把数据同步下去,简单粗暴但极其容易漏、极其影响性能;另一条是硬件路,让总线协议本身去追踪"某块数据现在被谁持有、处于什么状态",在有人要写的时候主动去通知其他持有者。ACE走的就是后面这条路。

从工程角度看,硬件一致性带来的最大好处是降低了软件的心智负担:驱动开发者不需要在每次DMA前后写一堆cache维护指令,也不需要担心编译器把访问顺序重排到维护指令之外。代价是硬件复杂度上去了——互连要维护目录或者侦听机制,Master要能响应别人的查询,验证工作量翻几倍。

1.2 从AXI4的五个通道扩到ACE的八个通道

AXI4本身是五个通道:写地址(AW)、写数据(W)、写响应(B)、读地址(AR)、读数据(R)。这是一套"主从问答"模型,Master发请求、Slave给响应,双方之间没有第三方插嘴的余地。

ACE在这个基础上加了三个通道,全是围绕"侦听(Snoop)"这件事展开的:

  • AC(Snoop Address Channel):由互连发往Master,问一句"你手上有没有这个地址的数据";
  • CR(Snoop Response Channel):Master回一句"有/没有、脏不脏、要不要回传数据";
  • CD(Snoop Data Channel):如果CR说"我有数据要给你",数据就从这条通道走。

加上原有的五条,一共八条。这个数字变化看着不大,但意味着接口的角色变了:一个ACE Master不再只是"发起请求的一方",它同时也可能成为"被别人查询的一方"。这个双重身份是所有复杂度的源头,也是后面各种死锁、状态错乱的根源。

我一般跟人说,理解ACE的关键不是记住多少种事务类型,而是先在脑子里建立一个画面:八个通道像八条并行的流水线,任何时刻都可能有多组请求在飞,互连像一个交通调度员,既要保证数据正确,又不能把路堵死。

1.3 ACE、ACE-Lite与后续一致性协议的分工

同一个AMBA体系下,其实有好几档一致性能力,选错了要么性能不够,要么白白增加面积。

接口类型通道数量能否被别人侦听典型使用方
AXI45否无缓存的简单外设、低速通路
ACE-Lite5(带扩展属性字段)否(只能主动发起一致性访问)DMA、带缓存的加速器、需要IO一致性的模块
ACE8是带缓存的CPU核、缓存一致性集群内部
后续一致性协议基于报文是大规模多核互连

ACE-Lite是一个特别容易混淆的点。它只能"消费"一致性——Master可以在读写时带上一致性属性,让互连去帮它把别的缓存刷掉,但它自己不会被别人侦听,因为它假定自己没有需要被追踪的缓存(或者说它的缓存由软件自己管)。所以你在一个DMA引擎上挂ACE-Lite是很合理的,挂完整ACE就是浪费。

至于更新的AMBA 5体系里那套基于报文的一致性协议,通道这个概念已经被消息包取代了,扩展性和拓扑灵活性更好,但学习曲线也更陡。ACE的价值在于它是"通道式"的,波形一拍一拍看得清,对刚接触一致性的人反而更友好。

2. 八个通道的信号地图与四个关键属性字段

2.1 AC/CR/CD三个侦听通道的职责边界

侦听通道的交互顺序是有严格规定的,这点在写RTL或者调波形的时候必须记牢。

互连先在AC通道上发起一次侦听请求,携带地址、侦听类型(ACSNOOP)和保护属性(ACPROT)。这是一个单次握手:ACVALID拉起,ACREADY回应,一拍完成,没有数据相。

Master收到后,先在CR通道上给出响应。CRRESP是一个5位的向量,每一位都有明确含义,这里必须掰开说:

  • CRRESP[0](Data):告诉互连"我待会儿会在CD通道上传数据",互连看到这一位就知道要准备接收;
  • CRRESP[1](PassDirty):告诉互连"我这份数据是脏的,你拿走之后要负责回写",这一位如果搞错,脏数据就丢了;
  • CRRESP[3:2](Resp):响应状态,正常、独占成功、从设备错误之类;
  • CRRESP[4](IsShared):告诉互连"这份数据还有别的副本存在"。

然后是CD通道,只有CRRESP[0]为1时才走。CD上有数据、有LAST标志、还有一个CDRESP表示这次数据传输的结果。

顺序上有个铁律:CR一定在CD之前,而且CR只发一次。我在早期看别人写的验证用例时见过把CR和CD并行发出去的,仿真里因为时序巧合没报错,一上FPGA就随机挂——因为互连的状态机是按"先看响应再收数据"设计的。

2.2 AxDOMAIN、AxSNOOP、AxBAR、AxCACHE四个属性字段

这四个字段决定了一次访问"参不参与一致性、参与多深",是ACE里最需要吃透的部分。

AxDOMAIN[1:0]划分一致性域:Non-shareable、Inner Shareable、Outer Shareable、System。简单说,Non-shareable的访问互连不保证一致性,也不会被侦听;Inner Shareable是同一个簇内的共享域;Outer Shareable范围更大。这个字段配错的后果非常直接——你以为自己在共享域里,实际上互连根本没把你当回事,该刷的缓存一个没刷。

AxSNOOP[3:0]是事务类型的编码字段,读通道和写通道各有各的编码表,下面第3节会细讲。

AxBAR[1:0]标识这是一次普通访问还是一次屏障请求。屏障这件事在ACE里必须走总线,因为只有互连知道全局的事务顺序,Master自己在本地说"我等等"是没用的。

AxCACHE[3:0]四个位分别表示Bufferable、Modifiable(可缓存)、读分配、写分配。注意ACE里第二位的语义比AXI4更宽,它表达的是"这次访问允许被修改/合并",Cacheable只是其中一种情况。

一个很常见的坑:把Device类型的内存配成Cacheable,或者在Non-shareable域里做共享数据访问。前者会导致读写外设寄存器时被缓存住,后者会导致一致性完全失效,而且这两种错误在单核功能测试里往往发现不了。

2.3 RACK/WACK这对容易被忽略的握手

这两个信号不属于那八条通道,但对避免死锁至关重要。

RACK(读地址确认)和WACK(写地址确认)由Master发出,用来告诉互连"这个读/写事务的地址资源我可以释放了"。为什么需要这个?因为ACE里一个读事务的完成被拆成了两段:数据回来是一段,地址资源释放是另一段。中间这段时间里,互连可能要给这个Master发侦听请求,而侦听请求本身又可能依赖这个还没完全结束的事务。如果Master死死占着地址资源不放,互连的侦听发不出去,侦听发不出去事务就完不成,完不成就不放资源——死锁就这么形成了。

所以做ACE Master的RTL,RACK/WACK的生成逻辑必须和地址资源的分配逻辑严格对应,一个outstanding事务对应一次确认,不能多不能少。互连侧也要有超时保护,避免真的卡死后整个系统静默。

3. 读、写、侦听三条线上的事务类型编码

3.1 读事务:ReadShared、ReadUnique、ReadClean怎么选

读通道上的事务类型,核心区别在于"我要拿到什么状态的副本"。

ReadShared是最常用的一个,语义是"给我一份可共享的副本"。如果别的缓存里有脏数据,互连会把脏数据取回来给你,同时把原持有者的状态改成共享脏或者共享干净。适合纯读取的共享数据,比如只读的配置表、共享的代码段。

ReadUnique要的是独占副本,拿到之后其他所有副本都会被失效。这个事务的成本明显更高——互连得挨个去问所有可能持有该地址的缓存。什么时候必须用它?典型的场景是要对这块数据做多次写操作,先一次性拿到独占权,后面就不用反复折腾了。

ReadClean的语义比较微妙:我要一份干净的数据,保证我拿到的东西和内存是一致的,但我不需要独占。它和ReadShared的区别在于,ReadClean不会拿到脏数据(脏数据会被先回写再给),所以接收方拿到的状态一定是共享干净的。这个事务在有些场景下能省掉PassDirty的处理逻辑,代价是多一次回写延迟。

还有一个ReadNotSharedDirty,字面意思就是"别给我共享脏状态"。它的用处是某些Master的缓存模型里没有共享脏这一态,或者处理起来代价太大,那就提前声明我不要这种状态,互连会用别的方式满足。

实测经验:不要为了省事全用ReadUnique。有些初学者觉得独占最安全,全用ReadUnique,结果多核跑起来总线带宽掉一半,因为每次读都要触发全局失效。按实际访问模式选类型,收益非常明显。

3.2 写事务:WriteUnique、WriteBack、WriteEvict各自的定位

写侧的事务类型更能体现"一致性维护的成本在哪里"。

WriteUnique是普通写,但语义上要求写之前把其他副本失效掉,并且不分配本地缓存(写完数据交给互连处理)。适合"写一次就不管了"的场景,比如往一个共享队列里塞一条消息。

WriteLineUnique是整行写,一次写满一个cache line,效率比逐字节的WriteUnique高很多。如果你的数据结构能对齐到cache line大小,用这个能省不少带宽。

WriteBack是回写,把一个脏行写回内存或者互连的缓存,写完之后本地那一行通常变成干净状态。这个事务在正常的数据流里出现频率很高——缓存替换、主动clean都会产生。

WriteEvict比较特殊,它的意思是"我这一行要淘汰了,但我判断这个数据大概率不会再被用到"。它的语义允许系统直接丢掉而不回写,性能好但风险高。实际项目里除非你能严格论证数据确实没用了,否则不要随便用。

写事务是否要求其他副本失效是否保留本地副本典型用途
WriteNoSnoop否否非一致性内存、外设寄存器
WriteUnique是否一次性写入共享数据
WriteLineUnique是否整行写入,追求带宽
WriteClean是是(转干净)保留副本同时清理
WriteBack否是(转干净)缓存替换回写
WriteEvict否否确定不再使用的行淘汰

3.3 侦听事务:MakeUnique和CleanInvalid的语义差别

侦听侧的事务是互连主动发起的,Master只能被动响应。几个高频类型:

Snoop是最轻的一种,只要求Master报告自己手上这一行的状态并可能回传数据,状态不一定改变。它通常用在互连想了解全局情况的时候。

MakeUnique要求Master把这一行变成独占——如果Master持有副本,就得失效掉,脏数据要传回去。

CleanShared要求回传脏数据,但保留共享副本。这个在"我要读但你手上有脏数据"的场景里很常见。

CleanInvalid是回传脏数据并且失效。它和MakeInvalid的区别就在"回不回传"上。

MakeInvalid只要求失效,不要求回传。这一条危险系数最高——如果Master手上是脏数据,直接失效就等于数据丢了。规范里对它的使用有严格前提,一般是调用方已经确认数据不需要了(比如显式的cache invalidate指令,软件自己承担后果)。

MakeReadUnique是个组合动作:把数据交出去并且自己失效,等于"让位"。在缓存行迁移的场景里用得比较多。

血泪教训:早年有个项目在互连侧把本该发CleanInvalid的地方错配成了MakeInvalid,功能测试全过,跑长时间压力测试时偶发数据校验失败,查了快两周才定位到。凡是涉及脏数据回传的路径,验证时必须专门构造"持有脏行"的场景去打。

3.4 编码表与版本核对这件事

不同版本的ACE规范在个别编码上确实存在调整,尤其是涉及原子操作和DVM的部分。给一份常见版本的参考表,但落地前一定要拿手上的规范文档逐条对一遍,这是必须做的功课。

读通道 ARSNOOP事务写通道 AWSNOOP事务
0b0000ReadNoSnoop0b0000WriteNoSnoop
0b0001ReadOnce0b0001WriteUnique
0b0010ReadClean0b0010WriteLineUnique
0b0011ReadNotSharedDirty0b0011WriteClean
0b0100ReadShared0b0100WriteBack
0b0101ReadUnique0b0101WriteEvict
0b0111ReadMakeUnique0b0110WriteCleanFull
0b1100Barrier0b1100Barrier
0b1000~0b1010DVM相关0b1000~0b1011DVM相关

侦听通道的ACSNOOP编码则是另一套:Snoop、MakeUnique、MakeClean、MakeInvalid、CleanShared、CleanInvalid、MakeReadUnique、DVM,各自占一个编码位。做接口对接的时候,双方必须在这张表上达成一致,最好在接口文档里把这张表打印出来贴在墙上。

4. 五态缓存模型:UD/UC/SD/SC/I的实际迁移路径

4.1 五态与MOESI的对应关系

教科书上讲缓存一致性通常用MESI或者MOESI,ACE用的是一套自己的命名,但本质是对应的。

ACE状态含义对应MOESI
I(Invalid)无效,本地没有可用副本I
UC(Unique Clean)独占且干净,与内存一致E
UD(Unique Dirty)独占且脏,内存是旧的M
SC(Shared Clean)共享且干净S
SD(Shared Dirty)共享且脏,但内存是旧的,由我负责O

这套状态模型的关键在于UD和SD的区分。UD是"只有我有,而且我这份比内存新",SD是"别人也可能有,但脏数据在我这儿,回写责任也在我这儿"。区分这两者的意义在于:如果别人要读,SD状态下必须由我提供数据,不能让人家去读内存;UD状态下则可以视情况转移独占权。

ACE规范里对每个事务在每种状态下的行为都有明确定义,形成一张巨大的状态迁移表。实际做设计不需要背下来,但必须知道怎么查——遇到波形异常时,第一件事就是拿这张表对一遍,看是状态机写错了还是事务类型用错了。

4.2 一条典型迁移路径的完整推演

拿最常见的"读-改-写"来走一遍。

初始状态,地址X在两个核的缓存里都是I。核A发起ReadUnique,互连发现没人持有,从内存取数据,返回给A,A的状态变成UC(干净独占)。

接着A对X做写操作。因为是本地写,不需要走总线,A的状态从UC变成UD。此时内存里还是旧值,A是唯一持有者。

然后核B要读X。B发起ReadShared,互连发现A持有UD,于是向A发侦听。这里侦听的类型取决于互连策略——如果互连希望保留A的副本,就发CleanShared,A回传脏数据但自己保留一份SC;如果互连认为A的副本可以放弃,就发CleanInvalid,A回传数据后变成I。

数据从A传到互连,互连再传给B,同时把数据回写到内存(或者缓存在互连里)。最终B拿到SC,A可能是SC也可能是I。

这条路径里有两个地方最容易出错。第一,PassDirty位的传递。A回传数据时必须正确置位,否则互连不知道这份数据需要回写,内存就一直是旧的。第二,回写和转发的顺序。有些实现里互连先转发给B再回写内存,如果这时候B立刻又改了数据,就可能出现乱序,需要额外的事务排序机制。

4.3 一个真实的状态机陷阱

说个具体的。某次调试发现一个奇怪现象:两个核交替读写同一个变量,偶尔会出现两次读到的值相同但中间明明有一次写。查了很久,最后定位到Master侧的侦听响应逻辑有漏洞——它在处理CleanShared时,把状态从UD改成了SC,但没有更新"脏数据归属"这个内部标记,导致后续再有侦听请求时,它认为自己是干净的,不回传数据。

结果就是:数据在它这儿是新的,但它不承认,互连只好去内存拿旧值,一致性就这么断了。

这类bug的特征是低频、偶发、和访问时序强相关,靠功能测试很难覆盖。唯一的办法是在验证环境里做状态覆盖:把每个事务类型在每种缓存状态下的组合都构造一遍,检查响应是否正确。这件事枯燥但没有捷径,做一遍比出事之后查两周划算得多。

5. 屏障事务与DVM消息的作用

5.1 内存屏障为什么必须走总线

单核时代,内存屏障基本是编译器屏障加CPU内部的流水线控制,本地就能搞定。多核时代不行了——你在核A上写了数据再写标志位,这个顺序在核A看来是有序的,但核B可能先看到标志位再看到数据,因为两个写操作走了不同的通路,到达互连的时间可能颠倒。

ACE里的屏障事务就是用来解决这个的。Master在读写通道上发出屏障请求,这个请求进入互连后会保证它之前的所有事务都已经在全局可见,之后的事务才开始。注意"全局可见"这个词——不是"完成了",而是"所有观察者都能看到正确的顺序"。

屏障事务在通道上通过AxBAR字段标识,具体类型由相关编码组合决定。这里有个细节:屏障事务本身不携带数据,也不产生数据响应,但它仍然占用一次事务的资源,会消耗带宽。所以不能滥用,一般只在关键的同步点(比如锁的获取释放、共享队列的生产消费)才发。

实测中还有一个容易忽略的点:屏障的粒度和域要对齐。在Inner Shareable域里发的屏障,只保证这个域内的顺序,跨域不保证。如果你的共享数据跨了域,屏障发得再勤也没用。

5.2 DVM消息解决的是什么问题

DVM(分布式虚拟内存)这块内容,很多介绍ACE的材料会一笔带过,但真做多核系统绕不开。

场景是这样的:操作系统改了页表,把虚拟地址V从物理页P1映射到P2,然后刷了本地TLB。但其他核的TLB里可能还缓存着V→P1的映射,如果不通知它们,它们访问V就会跑到错误的物理页上。

DVM就是干这个的。它通过读/写通道上的一类特殊事务来传递消息,比如"失效某个地址范围的TLB项"、"失效所有TLB"、"同步等待所有核完成"等。这些消息由互连广播到所有相关Master,每个Master执行完再回应。

DVM的实现复杂度不低,因为要处理消息的广播、去重、完成确认。如果系统里混有ACE-Lite的Master,还要考虑它是否支持DVM——不支持的话,互连要么跳过它,要么由软件额外做同步。

我个人的建议是:如果系统里跑的是完整的操作系统,DVM基本是必须的;如果是裸机或者RTOS且只有一个核跑地址空间切换,可以考虑简化,但要非常清楚简化的边界在哪里。

6. 接入ACE时的典型故障与排查链路

6.1 死锁:从现象到根因的完整追法

死锁是ACE系统里最恶心的故障,没有之一。现象通常是:系统跑着跑着就停了,没有任何报错,抓波形看到某个通道的VALID一直拉着但READY永远不来。

排查思路我一般按这个顺序走:

第一步,先看是哪条通道卡住。如果是AC通道卡住,说明互连发了侦听但Master没接;如果是AR通道卡住,说明Master发了请求但互连没回。这两种情况的根因完全不同。

第二步,看这个卡住的事务依赖什么。如果AC卡住,去看Master为什么不应答——可能是它的地址缓冲区满了,而缓冲区里的老事务在等数据,数据又要等这个侦听完成。这是个环,必须找到环的入口。

第三步,重点检查RACK/WACK。大部分死锁最后都落在这两个信号上:Master没有及时释放地址资源,导致互连的outstanding槽位耗尽,新的侦听发不出去。

常见的修复手段有几个。一是缩短地址资源的持有时间,数据一回来就拉RACK,不要等后续处理全部完成。二是给互连加超时机制,超时后强制释放并上报错误——这不是治本,但能防止整个系统静默。三是在Master侧做侦听响应的优先级提升,确保侦听请求不会被本地的大量读写请求饿死。

有个反直觉的经验:侦听响应的优先级应该高于普通读写请求。因为侦听是被动的、有时限的,堵住了它会引发连锁反应;而普通请求堵一下只是慢一点。很多设计出问题就出在仲裁策略上,把侦听和普通请求平权处理了。

6.2 数据陈旧:属性配错导致的一致性失效

这类问题的现象是"数据偶尔不对,但重跑又好了",比死锁更难查,因为没有任何异常信号。

排查的核心是确认这次访问到底有没有参与一致性。具体做法是把这一次访问的AxDOMAIN、AxSNOOP、AxCACHE三个字段全部抓出来,和预期对比:

  • AxDOMAIN是Non-shareable的话,这次访问本来就不保证一致性,问题出在软件或配置上;
  • AxCACHE里Cacheable位没置起来的话,这次访问走的是非缓存路径,读到旧值是正常的(因为根本没缓存);
  • AxSNOOP如果用了NoSnoop类型,说明发起方主动放弃了侦听,要查为什么。

还有一种隐蔽的情况:属性是对的,但Master内部对这块地址的缓存策略和总线属性不一致。比如总线说这是Cacheable,但Master内部的MMU配置把这个区域标成了Non-cacheable,两边打架,行为就不可预测了。这种情况必须在系统层面做一次地址映射表的交叉检查。

6.3 仿真阶段值得投入的检查项

靠事后调试不如事前验证。下面这几项,我在项目里基本是必做的:

  • 状态覆盖:五态 × 主要事务类型的组合全覆盖,特别是UD和SD状态下的各种侦听响应;
  • PassDirty专项:凡是回传数据的路径,都要检查PassDirty是否正确置位,这是脏数据丢失的头号原因;
  • 地址资源泄漏检查:统计发出的请求数和收回的RACK/WACK数,任何时刻差值都应该等于outstanding数;
  • 屏障顺序检查:构造"写数据-写标志-屏障"和"读标志-读数据-屏障"的交叉场景,用随机延迟打乱到达顺序;
  • 域混用检查:故意在Inner域和Non-shareable域之间做共享访问,看系统是报错还是静默出错——静默出错是最糟的。

这些检查写起来工作量不小,但比起流片后发现问题,投入产出比高得离谱。

7. 什么时候该考虑从ACE迁移到更新的协议

ACE这套东西的生命周期其实已经很长了,很多现存的IP和工具链都围绕它构建。但如果你现在开始做一个全新的、核数比较多的设计,确实该评估一下是不是直接上更新的一致性协议。

判断标准我一般看三条。第一条是核的数量和拓扑复杂度。ACE的侦听式实现在核数少的时候效率不错,核数一多,每次写都要广播侦听,带宽和延迟都会成为瓶颈。第二条是是否需要跨die或者跨芯片的一致性,ACE在这方面的扩展性有限。第三条是生态支持,包括验证IP、调试工具、现有的IP核,这些能不能跟上。

但反过来说,如果你的系统核数不多、团队对通道式协议更熟悉、现有IP都是ACE接口,那硬上新技术反而增加风险。我见过一个团队为了"用新技术"把本来稳定的设计推倒重来,最后项目延期半年,功能上并没有实质提升。

还有一种折中做法:核心集群内部继续用ACE,对外扩展的部分用新的协议,中间加桥接。这样既保留了现有IP的复用,又在需要的地方获得了扩展性。桥接本身有开销,但通常比全面重做划算。

最后一提,无论用哪套协议,一致性这件事的本质没有变:都是要追踪"谁持有数据、处于什么状态、脏数据在哪里"。把这套状态模型吃透了,换协议只是换了一套表达方式,底层思维是通用的。我见过有人死记硬背ACE的编码表,遇到新协议又得从头学;也见过把状态模型真正理解透的,看任何一致性协议的文档都能很快上手。后者才是可持续的路子。

如果你现在正在啃ACE,我的建议是先把五态模型和侦听通道的交互顺序彻底搞清楚,编码表和事务类型反而是最好查的部分。真的上手做设计时,第一件事不是写RTL,是画一张图:把每个通道的握手关系、每个状态下的响应行为都画出来贴在显示器旁边,遇到波形对不上的时候抬头看一眼,比翻文档快得多。

返回列表