ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

系统级通信学习笔记:总线握手、中断与DMA机制

系统级通信学习笔记:总线握手、中断与DMA机制 这已经是第19篇MIT 6.004学习笔记了。前面几篇把Beta处理器的数据通路、流水线和存储层次一路啃下来这一章终于轮到System-level Communication——系统级通信。用大白话说就是把视角从“处理器内部怎么取指、怎么算数”拉高到“处理器怎么跟外面的世界对话”。总线怎么握手、外设怎么接到处理器上、中断和DMA怎么配合全在这章里铺开。对正在学计算机组成与接口的同学来说这是从CPU核心迈向完整系统的一块关键拼图对已经工作、天天跟SoC和嵌入式平台打交道的工程师这章其实是在帮你把日常调试中那些“知其然不知其所以然”的总线行为重新理一遍。1. 这章到底在讲什么从处理器“单打独斗”到总线“多人协作”1.1 为什么学了流水线还不够前几章把Beta处理器的流水线搭起来之后你会产生一种错觉处理器自己又会取指令、又会算算术、又会访存好像单机就能跑起来了。但真到了系统级任何处理器都离不开外部世界——你要从键盘拿输入要把数据显示到屏幕上要把大量数据存进磁盘或者网络接口。这些东西没有一个活在处理器内部数据通路上它们要么挂在内存总线上要么通过专用控制器接进来。问题就来了内存总线原本设计成“处理器唯一主设备”现在多个外设都要跟处理器通信有的还要跟内存直接交换数据。处理器指令周期那么快外设却慢得离谱机械硬盘一次寻道能抵几百万个时钟周期。如果处理器每读一个键盘按键都像读内存一样同步等待CPU基本一辈子都在等外设。6.004这一章要解决的本质上就是“速度落差怎么弥合”“多个设备怎么共享同一套线路”“慢速设备怎么不拖死快速处理器”这三个问题。理解了这些问题你再看PCIE、USB、AMBA这类真实总线时会发现它们的核心机制全都能在这章找到影子。1.2 系统级通信的抽象层次这一章把通信拆成三层来看物理层、协议层和传输层。物理层管的是电压、信号线和时钟怎么布置协议层管的是双方怎么约定一次读写操作的开始、中间过程和结束传输层管的则是数据以什么粒度、按什么顺序在多个设备之间流动。三层分离的思路非常工程化——你改协议层比如从并行总线换到串行总线物理层可以不动你换了一个更快的仲裁策略传输层的路由逻辑也不用推倒重来。讲真我第一次听课的时候觉得这些概念太“虚”。后来做嵌入式项目踩了波形不对、时序崩溃的坑才明白这套分层不是课本在凑字数。协议层的握手没做对物理层信号再漂亮接收方照样采到错误数据传输层的仲裁没设计好两个设备同时抢总线轻则性能下降重则数据损坏。这章的价值就是让你在写Verilog之前先在脑子里建立一套“通信契约”的框架而不是拿起代码就写信号。2. 选型背后的逻辑同步总线、握手协议与仲裁设计2.1 同步总线为什么能用“一个时钟”解决很多事6.004里先把同步总线讲透了。所谓同步总线就是所有设备共享同一个时钟通信双方都在时钟沿上采样和驱动信号。处理器发出地址和写数据必须在时钟上升沿之前把信号稳定住外设采样也必须在同一个沿上锁存。因为时钟是全局统一的所以“什么时候采样”这个问题被一个时钟周期就锚定了不需要双方单独约定时间点实现起来最直观。同步总线最大的优势是简单、确定性高。你只要关心建立时间和保持时间是否满足时序推导基本就是数周期。Beta处理器的内存访问就采用同步模型一个时钟周期内发出地址下个周期数据回来两级流水就能完成一次读。但同步总线也有代价全局时钟要同时到达所有设备频率越高时钟树越难做而且总线上的所有设备必须按同一个节奏工作——外设再慢也得在固定几个周期内给回应。所以实际系统中慢速外设往往不直接挂在同步总线上而是通过控制器转换成异步接口或者加入等待周期wait state。课程里先讲同步是为了让你有一个基准模型之后再引入握手思路就顺理成章了。2.2 四阶段握手异步世界最可靠的“对话礼仪”当通信双方没有共享时钟或者一方快一方慢就需要异步握手协议来保证“你说的话我确实收到了”。6.004重点讲的是四阶段握手4-phase handshake这玩意儿太经典了以至于你后来去读AMBA AXI或者I2C协议都能看到它的影子。四阶段握手的流程是这样请求方把Req信号拉高同时确保数据线上已经放好有效数据。接收方看到Req为高后锁存当前数据然后把Ack拉高表示“我收到了”。请求方看到Ack为高后知道对方已经拿走数据于是把Req拉低同时可以撤销数据线上的数据。接收方看到Req拉低后把Ack也拉低回到初始空闲状态。这四步环环相扣每一步都以上一步为前提所以天然不会出现“数据还没稳定就被采样”的问题。设计上有一个细节容易被忽略数据必须在Req为高之前就已经稳定。如果你在Req拉高的同时才放数据接收方看到Req到发起采样之间是有延迟的数据线上的毛刺很可能被锁存进去。我自己的经验是做握手逻辑时宁可在Req拉高前多等半个周期放数据也不要图快把两个动作合并。为什么是四阶段而不是两阶段两阶段也叫双线握手只需要Req和Ack各反转一次理论带宽更高但每一拍都依赖对方立刻响应时序收敛难做而且对延迟特别敏感。四阶段虽然每笔传输多了一轮“撤销确认”的开销但每个状态都有明确的等待条件非常适合课程里这种用有限状态机就能实现的场景。实际工程里PCIE这类高速总线会用更高效的分组协议但底层思想仍然是“请求-确认-释放”的循环。2.3 总线仲裁谁先过十字路口多个设备共享同一条总线时必须有人决定“这一刻谁能占用总线”。6.004介绍了两种仲裁方式集中式仲裁和分布式仲裁。集中式仲裁有一个中央仲裁器所有设备把请求信号送过去仲裁器根据优先级选出 winner授权信号再传回来。这种方式简单、决策快适合主设备数量不多的系统。分布式仲裁则没有中央仲裁器每个设备把自己的请求信号通过菊花链传给下一个设备谁在链上“拦住”了授权谁就获得总线。两种方式各自有适用场景。集中式仲裁的缺点是仲裁器是单点坏了整个总线瘫痪分布式仲裁的优点是扩展性好但优先级传递有累积延迟链越长仲裁越慢。6.004课程里用集中式仲裁比较多因为Beta系统外设数量有限一个状态机就能搞定仲裁逻辑。真正让我印象深刻的不是仲裁算法本身而是它揭示了一个通用原则任何共享资源的系统都必须有一个明确的所有权转移机制。“谁在用总线”“用完怎么释放”“等待的设备会不会被饿死”这三个问题不解决总线上的问题会以极其诡异的方式出现。3. I/O通信三板斧内存映射、中断与DMA3.1 内存映射I/O让外设“假装自己是内存”Beta处理器的I/O设计选了内存映射I/O这条路。它的做法很简单把一部分地址空间划给外设处理器访问这些地址时不是去读内存芯片而是触发外设控制器的读写操作。比如地址最高16位是0xFFFF开头的一段空间被解读成I/O操作而不是普通内存访问。处理器本身不需要新增专门的I/O指令所有外设访问都可以用现有的LD/ST指令完成硬件上只需要一个地址解码器判断当前访问地址是否落在I/O区间。这样设计的好处是软件模型极其统一。你想往串口控制寄存器写一个字节就执行一次普通的ST指令想读键盘状态寄存器就执行一次LD指令。编译器、汇编器完全不用知道什么是I/O汇编程序员不需要记住特殊的I/O指令格式。代价则是地址空间被占掉一块更重要的是I/O操作不能像内存那样随意缓存和乱序执行——如果处理器把一个外设的读操作重排了很可能读回的是过期数据。所以真实处理器里内存映射I/O区域一般会被标记为“不可缓存”甚至需要内存屏障指令来保证访问顺序。6.004课程里虽然不直接讲缓存一致性但你应该从一开始就意识到I/O操作和普通内存访问的语义并不相同。3.2 中断让处理器从“死等”里解放出来如果没有中断处理器跟外设交互的典型方式是轮询循环读取设备状态寄存器直到某个标志位置位。轮询写起来很简单但CPU的时间全浪费在读状态上。6.004引入中断机制来解决这个问题设备准备好数据时主动拉高中断请求信号处理器在执行完当前指令后检查到中断请求保存现场跳转到中断服务程序ISR处理完再恢复现场继续原来的程序。课程里把中断拆成几个关键环节。首先是中断检测时机Beta是每条指令结束时检查一次中断这样可以保证中断不会打断一条指令的原子性。其次是现场保存因为中断随时可能到来处理器必须把当前PC、寄存器值压栈保存否则返回时程序状态全乱了。第三是中断响应延迟从设备发出请求到处理器跳进ISR中间有若干周期的固定开销这对实时性要求高的系统很关键。最后是中断嵌套如果中断服务程序里又来了更高优先级的中断要不要响应课程入门阶段默认不嵌套一次只处理一个中断这大大简化了现场保存和恢复的复杂度。我当年做实验的时候最大的坑是忘记在中断返回前清中断标志。如果设备的中断请求信号一直保持有效处理器刚执行完RETI马上又触发同一个中断看起来就像中断卡死。所以中断服务程序里第一步往往是读取状态寄存器确认中断源最后一步是写清除寄存器把中断信号拉低这个顺序别搞反。3.3 DMA数据搬运工的自我修养中断解决了“CPU不用死等”的问题但如果是大块数据要搬进内存比如从网卡读一整个包到内存CPU一个字节一个字节地读再写入内存既慢又蠢。DMA直接存储器访问就是为这种场景准备的DMA控制器接管总线的控制权从内存一块区域搬到另一块区域搬运过程完全不需要CPU逐字干预。CPU只需要在传输开始前告诉DMA控制器“源地址、目的地址、传输长度”然后DMA完成搬运后发一个中断通知CPU。DMA的代价是它也要占用总线。当DMA和外设同时想用总线时就需要仲裁了。这正好把前面的总线仲裁和DMA串在一起。DMA有几种工作模式课程里会涉及周期窃取cycle stealing和突发传输burst transfer。周期窃取是DMA每次只占一个总线周期搬一个数据就还给CPU对CPU响应影响最小但搬运速率低突发传输是DMA一口气把整块数据全搬完速率高但会让CPU长时间等不到总线。选哪种模式永远是对“系统实时性”和“DMA吞吐量”的权衡。实际做嵌入式系统的时候这两个字——权衡几乎贯穿所有设计决策。4. 实操在Beta处理器上打通一条真实的外设通路4.1 最小系统长什么样地址解码器与控制寄存器课程实验里最典型的任务是给Beta处理器挂一个简单的外设比如键盘/显示器控制器。这个外设内部至少有数据寄存器和状态寄存器状态寄存器告诉处理器“当前有没有新的输入”“上次输出是否完成”数据寄存器存放真正要传输的字节。硬件上需要在总线地址解码逻辑上做文章——地址落在I/O区间时不是去访问内存芯片而是通过译码逻辑选中对应外设寄存器。设计地址解码器时有一个很实际的经验不要用单个地址直接匹配而是把地址区间的“基地址偏移”拆开。比如基地址0xFFFF0000是I/O区起始偏移0是状态寄存器偏移4是数据寄存器。这样以后扩展新外设时只需要增加偏移值不用改动总线仲裁逻辑。同理读写信号要跟寄存器方向匹配——状态寄存器通常是只读数据寄存器可能是可读可写。如果不加区分地让所有寄存器都能被任意读写调试时会冒出很多“神秘故障”实际都是寄存器读写属性没设对。4.2 用手写伪代码演一遍读写时序理解了硬件结构之后自己动手写一遍时序逻辑会非常有帮助。下面是一段简化的状态机伪代码演示对外设进行“读数据”的四阶段握手操作// 读操作CPU发起读取外设返回数据 状态 IDLE: 等到 CPU 请求读 I/O 地址Req_in 1 把地址和数据方向信号锁存到总线 跳转 WAIT_DATA 状态 WAIT_DATA: 等待外设返回 ReadReady 1 一旦 Ready锁存数据线到 CPU 内部寄存器 拉高 Ack_out表示读完成 跳转 RELEASE 状态 RELEASE: 拉低 Ack_out 等待 CPU 撤销 Req_in 跳转 IDLE这段代码看起来简单但里面至少有三个坑。第一个坑是WAIT_DATA里的“等待外设Ready”到底等多久。如果用同步总线你必须限制等待周期数否则设备故障时处理器会永远死锁。第二个坑是读数据必须在拉高Ack之前锁存因为Ack一拉高CPU可能立刻开始下一笔操作并改变数据线。第三个坑是RELEASE阶段必须等CPU撤销Req_in后再回到IDLE如果提前回IDLE下一笔请求可能被当成当前请求的一部分数据错位。这三条都是我自己在仿真波形上对出来的不是听课就能记住的。对写数据的操作流程对称但多了“数据有效”的条件CPU要在Req_in拉高之前就把写数据放到数据线上外设在WAIT阶段直接采样不需要等设备侧Ready——除非外设内部有FIFO满了的情况。有FIFO的话状态机会多一个Full信号判断这在实际芯片里非常常见。4.3 中断与DMA的现场保护要点给Beta配好中断后最重要的事情是现场保护与恢复。课程里常用“中断服务程序里第一条指令就把需要改的寄存器压栈返回前按逆序弹栈”的通用套路。听起来不难但有一个细节处理器自动保存的PC和在ISR里手动保存的通用寄存器必须存在两个不同空间或者至少确保在进入ISR的瞬间用到的栈空间是确定可用的。如果把栈指针SP本身也当作一个普通寄存器来保存就要格外小心保存SP用的那几条指令不能再改变SP。实际调这类问题的方式是先在模拟器里让一个简单的计数器程序跑起来再人为触发中断逐步检查保存现场前的寄存器和保存后的寄存器是否一致。宁可多执行几个NOP把现场保存拉得更稳妥也不要冒险在保存现场还没完成时开中断。DMA的现场保护逻辑不同DMA控制器本身要配好源地址、目的地址、字计数字这些寄存器必须在DMA启动前写对。我见过最典型的问题是源地址和目的地址重叠时突发传输会把数据覆盖。比如源区域和目的区域部分重叠DMA一台后面的数据可能已经被前面搬过来的新数据覆盖了搬完结果完全错误。解决办法是检查重叠方向如果是往前搬目的地址低于源地址要从低地址开始如果是往后搬要从高地址开始。这个常识在memcpy里要用在DMA配置里一样要用原理完全一致。5. 常见问题与排查心得5.1 死锁、数据错位与亚稳态系统级通信实验最常见的故障一是死锁二是数据错位。死锁的典型场景就是握手双方都在等对方CPU在WAIT_DATA状态等外设Ready但外设的Ready信号依赖CPU先发地址而地址因为CPU还没退出WAIT状态而不更新——双方互相干瞪眼。排查死锁第一件事是拉出波形看哪根信号一直不变观察谁在等谁。数据错位则是另一个画风波形上握手每步都对但读回的数据完全不对。这种多半是时序细节没看严。最常见的是数据在Req信号撤掉之前就失效了。比如在RELEASE状态里请求方为了省时间先把数据线撤了再拉低Req接收方虽然不在采样但总线高阻浮动下一笔操作的预充电阶段就会吃进脏数据。更隐蔽的是数据线方向没切换——双向数据总线读方向和写方向要分别设置方向控制信号一旦方向信号跟读写信号错半拍两边同时驱动总线波形上直接看到两条线打架。我自己的经验是把双向总线的方向信号和读写信号做成同一个状态机的相邻状态用寄存器延迟一拍输出就能避免绝大多数驱动冲突。亚稳态这个东西在跨时钟域信号处理里避不开。外部设备可能是异步的它的中断请求信号直接进到处理器时钟域理论上存在触发器采到“中间态”的可能。标准做法是加两级同步器两个串联寄存器让亚稳态在一个周期内收敛虽然不能完全消除但能把概率压到工程上可以忽略的程度。千万别省这一拍同步我见过有人为了省两个触发器结果整机偶发崩溃定位了一周才找到是跨时钟域没同步。5.2 中断丢失、优先级反转与调试方法中断丢失的常见原因有三个中断信号太短处理器没来得及采样中断状态被错误地清掉或者中断服务程序里关中断时间太长直接把后面的中断憋死了。针对第一个原因通常在设备侧用一个锁存器把中断请求锁住直到软件确认清除。针对第二个原因要注意是“读状态寄存器清标志”还是“写清除寄存器清标志”这由硬件决定软件必须跟硬件匹配。第三个原因在实时系统里特别头疼处理办法是中断服务程序尽量短把耗时操作放到主循环或者低优先级任务里。优先级反转这个概念学RTOS的时候听得最多。在总线和中断系统里也有变体高优先级设备的中断请求被一个低优先级的中断服务程序挡住而这个服务程序又在等一个被高优先级设备占用的总线资源结果高优先级反而被低优先级卡住。处理办法是让ISR只做必要操作不持有共享总线锁高级别的ISR可以打断低级别的ISR嵌套但要预先分配足够深的中断栈防止嵌套时栈溢出覆盖现场。排查这类问题的调试方法论我总结下来就三步。第一步先复现中断丢失类问题往往需要连续触发几十上百次才会出现不要指望跑一次就能抓到。第二步加观测点在关键信号上挂计数器比如中断请求计数、ISR进入计数、总线交出计数几路计数一对比到底丢在哪一步一目了然。第三步二分剪枝把中断服务程序改成空壳如果能跑通说明问题在ISR逻辑如果还是丢再往前查中断控制器配置。这一步比对着波形猜快得多。5.3 工具选型与波形分析技巧说起来仿真工具这边课程环境提供了虚拟模拟器和在线仿真平台可以直接观察顶层模块的时序波形。但自己用Verilog或者Chisel做实践时我建议把信号的命名规范做扎实。总线信号用固定的前缀去区分请求方和接收方比如 req_from_cpu、ack_to_cpu、data_out_sel别用模棱两可的 dout、din。否则波形一多你根本分不清是谁驱动谁。另外波形分析要先看整体的“协议波形图”而不是扎进单个信号里。先把Req和Ack两个信号的关系缩放看全景有没有违背“先Req后Ack、先撤Req再撤Ack”的顺序。这个顺序正确再看数据线上数据是否落在Req有效区间内。两个层级都过了再抓具体时钟沿的建立时间。这样分层看波形很难漏掉问题。如果你在仿真里看到了那种隔一段时间就出现一次的毛刺先别急着怀疑逻辑看看是不是测试平台里激励信号本身的timing写错了——仿真环境下激励写得不对跟设计做得不对现象一模一样。6. 最后分享一个我自己的体会6.004这一章最让我受益的地方不是让我记住了那些总线信号名字而是让我养成了“先把通信契约画清楚再动手写代码”的习惯。动手做实验之前先在纸上把请求方、接收方、仲裁器的状态图画出来把每一步的“谁等谁”标注清楚写Verilog的时候就知道每个状态里该等什么信号。后来工作里调PCIE驱动、调AXI互联总线遇到卡住的场景我都会回到这个思路先把协议状态图画出来把双方边界划清楚问题往往在第一张图里就暴露了一半。最后再给一个小建议如果你也在学这一章一定要亲手写完一套握手状态机哪怕只是一个小小的键盘控制器。光看不做你会觉得四阶段握手就是个自然无比的协议亲手写完仿真完你才会记住数据要先于Req稳定、Ack之前要锁存数据、撤销阶段要等对方释放这些细节。这些细节才是系统级通信真正考人的地方。
返回列表