- 博主简介:努力学习的22级计算机科学与技术本科生一枚🌸
- 博主主页: @Yaoyao2024
- 往期回顾: 【计算机系统架构】从0开始构建一台现代计算机|二进制、布尔运算和ALU|第2章
- 每日一言🌼: 孤独和喧嚣都令人难以忍受。如果一定要忍受,我宁可选择孤独。
—— 周国平
0、前言
- 一言以蔽之: 在构建了计算机的 ALU 之后,本 Modulation 将转向构建计算机的主存储器单元,也称为随机存取存储器或RAM。这项工作将自下而上逐步完成,从初级触发器门到一位寄存器,再到 n 位寄存器,直至一系列 RAM 芯片。与基于组合逻辑的计算机处理芯片不同,计算机的存储逻辑需要基于时钟的顺序逻辑(时序逻辑电路)。我们将首先概述这一理论背景,然后构建我们的内存芯片组。
- 关键概念:组合逻辑与时序逻辑、时钟与周期、触发器、寄存器、RAM 单元、计数器。
1、时序逻辑(Sequential Logic)
1.0 前言
在前面两章中,我们学习到一些逻辑门,他们代表着输入和输出的映射关系。但是我们并没有“时间”的概念,更通俗来说,我们的概念中,并不是先有输入,然后才有了输出;而是在输入的同一时间即刻得到的输出,我们并没有“One thing after an other”的概念。
但是现实生活中我们必须关注“时间”,因为我们就是生活在时间当中的,我们的任何行动和行为都是具有时间概念的,计算机也必须拥有时间,也才能更好地像人一样工作,否则则是混乱的。
💁🏻♀️其实在本节学习计算机中的"时间"这个概念的时候,老师说我们之前几节学的组合逻辑,并不是先有输入,然后才有了输出(one after another);我其实比较纳闷,哪电流信号从输入信号流入,通过组合逻辑电路得到输出,肯定有时间的呀!!
但其实我这种想法是比较错误的,对于我们人来说,的确是有时间的,但是对于计算机来说,其实是没有时间的,因为你没有给它这个概念,时间也无法度量,对于它来说其实并不是`one thing after another"的。
要想计算机拥有时间的概念之前,我们再来深入地分析一下,时间的作用。
1.1 时间的作用
-
Use the same hardware over time(硬件的复用性)
组合逻辑电路是可以复用的,但它的复用方式和时间的关系不同。我们先来理解组合逻辑电路和时间之间的关系。
组合逻辑电路是一种不涉及存储的电路。它根据输入直接产生输出,输出完全由当前输入决定,和时间无关。所以,对于同样的输入,组合逻辑电路总是会立即产生相同的输出,不管什么时候输入是一样的,它都能立刻给出结果。
但是,当我们说到需要“复用硬件”时,指的是计算机需要处理连续的、不同的计算任务,而这些任务不可能在同一时间内同时完成。时间在这里的作用是让计算机能够依次处理这些任务。例如,如果你有多个加法操作要进行,虽然组合逻辑电路可以同时处理多个加法,但硬件资源有限(例如一块特定的加法电路),计算机会按顺序利用这块电路来完成不同的加法任务。
所以,组合逻辑电路本身是可以复用的,但复用它时依然需要时间。因为我们希望通过一个硬件单元(例如加法器)来处理不同的输入和任务,而不是为每个任务都配备独立的硬件。时间允许我们复用同一块硬件去完成多个操作,这样就可以节约资源。
总的来说,复用硬件是指在不增加额外硬件的情况下,通过时间的控制,让一块硬件能够连续完成多个任务。
-
Remember state
记录中间状态比如我们要计算一个数累加100次,如果不能记录每次中间得到的结果,就不可能得到正确答案
-
Deal with speed
这段话主要讲了一个我们不想特别关心,但又必须考虑的关于时间的因素:计算机的速度有限。
具体来说,计算机处理任务的速度并不是无限的,它有一个固定的、有限的运行速度。因此,我们在设计程序或系统时,必须考虑到计算机的这个限制。我们不能要求计算机超出它的能力去完成任务,或者至少要知道计算机的运行速度是多少,这样才能合理地安排任务的执行。
打个比方,就像我们不可能要求一个人每秒钟完成100件事一样,计算机也有它的极限。即使我们希望计算机能尽快完成任务,但实际上它的速度是受限的。因此,我们需要确保我们的程序不会让计算机超负荷工作,或者根据它的速度合理地安排计算任务,以避免程序崩溃或运行不稳定。
总结一下:虽然我们更关注如何复用硬件和处理连续任务,但我们也必须注意到计算机的运行速度有限,不能让它超出能力范围工作。因为计算机不能超出其最大速度去处理任务,所以我们需要合理安排任务的顺序和执行时间。这也就是为什么我们说计算机的有限速度让时间变得重要。时间在这里的作用是分配和协调任务,确保计算机有足够的时间去完成每一项操作。
1.2 时钟
时钟是计算机里来衡量和表示时间的方法。和现实世界中的时钟一样, 它的作用就算将连续的时间信号离散化,变为一个一个单位进行时间的衡量。
如上图,物理世界中的时间是连续的,而时钟的作用就是将时间离散化、量化,变成一个时间序列:time=1、time=2、time=3...
。一个周期就是一个时间单位;在一个整数时间单位内,我们认为电路中不会有信号的改变。
在把时间量化后,输入输出的时间先后顺序是这样表示的:在t-1
这个时间单位输出做出的改变,会在t
时刻输出。这就完全不同于之前没有先后顺序的组合逻辑电路:在t-1
这个时间单位输出做出的改变,会在t-1
时刻l立刻输出。
👧🏻所以我个人认为拥有时钟来表示时间,就是量化时间,从而更好地更清晰的表示先后顺序,从而保证系统的稳定性,这点可以在下一节讲信号的延迟中可以看出。
1.3 信号的延迟
这点也进一步解释了我们为什么希望使用clock来将时间离散化,目的也是为了忽略不计这些信号的延迟!避免延迟带来的影响。对时钟时间周期选择的目的(不能太快,当然也不能太慢)的目的就是为了让硬件运行地更加稳定。
上面我们提到,虽然在组合电路中,对于计算里来说,是没有时间概念的;但是我们知道,在电路中,输入的物理信号用电信号表示,从输入到输出,是电子流动和聚集的过程,这在现实的物理世界中肯定是需要时间的,且在这段时间内的电信号变化是连续的而非条约的。即:从输入信号到输出信号存在延迟。
而在计算机内部建立起这种离散化的数字时间,其实也是为了避免信号的延迟!
为什么这么说呢?如下图,只要我们的时钟的速度不是太快,在连续的延迟之间流出足够多的时间等待信号达到稳定(即灰色区域后),当周期结束时,即可取得最终稳定的信号:
事实上,在选取时钟周期的前提也是确保所有的硬件都能稳定下来。
1.4 组合逻辑和时序逻辑的区别
∙ Combinatorial: o u t [ t ] = f u n c t i o n ( i n [ t ] ) ∙ Sequential: o u t [ t ] = f u n c t i o n ( i n [ t − 1 ] ) \bullet\text{Combinatorial: }out[t]=function(\:in[t]\:)\\\bullet\text{ Sequential: }out[t]=function(\:in[t-1]\:) ∙Combinatorial: out[t]=function(in[t])∙ Sequential: out[t]=function(in[t−1])
其实在上面的1.2
小节已经讲到过了:
下面这个例子也是对组合逻辑和时序逻辑的比较:
-
组合逻辑:
-
时序逻辑:
2、时序逻辑的基本单元:D触发器 (D Flip-Flop)
D触发器是时序逻辑电路的基础,它可以保存一个
bit
的信息,这也为之后保存大量的字节内容奠定了基础
2.0 前言
在前面一小节我们讲解了时序逻辑电路,它的核心就是数字化和离散化了时间,让先后顺序更加量化和明确。这一小节,我们将从硬件实现 的角度出发,实现 s t a t e [ t ] = f u n c t i o n ( s t a t e [ t − 1 ] ) state[t]=function(state[t-1]) state[t]=function(state[t−1]) 的时序逻辑。
2.1 状态的记忆:Remembering State
- 首先,实现时序逻辑的关键就是记忆状态: 记住时刻的信息直到
t
时刻使用。Missing ingredient:remember one bit of information from timet-1
so it can be used at timet
- 那么我们知道,这个记忆状态的元件肯定有两种状态:
1 or 0
; 并且根据t-1
时刻的信号进行翻转flip
从0
和1
之间进行转换
上述实现这种在单位时间内进行信号状态记忆的元素称为 触发器(Clocked Data Flip Flop).
2.2 Clocked Data Flip Flop:D触发器
💜介绍:
如下图,D触发器有一个输入in
和一个输出out
;作用是记录上一个单位时间的输入,然后在下一个单位时间输出: o u t [ t ] = i n [ t − 1 ] out[t]=in[t-1] out[t]=in[t−1]。
注意:图中的正三角形🔺是【时钟脉冲沿】的意思,表示这个器件是时序逻辑下的芯片,与时钟有关;触发器输出的状态由该脉冲沿的状态来决定,三角符号下边有圆圈的表示时钟脉冲下降沿触发有效,无圆圈的则表示时钟脉冲上升沿触发有效。即,理解如下: 下一个周期的输出信号由上一个周期末尾结束的上升沿采样值确定。
🥗D触发器的实现:
-
在这门课中,我们不会去关系它的内部地层实现,而是默认它是有记忆功能的原始器件即可。In this course:it is a primitive
-
In many physical implementations,it may be built from actual
Nand
gates:
Step 1:create a"loop”achieving an“un-clocked”flip-flip
Step 2:Isolation across time steps using a "master-slave"setup -
Our Hardware Simulator forbids “combinatorial loops”
- A cycle in the hardware connections is allowed only if it passesthrough a sequential gate
2.5 时序逻辑的实现
上图展示了我们将在计算机中构建的所有逻辑的架构:A combination of remembering information via this basic D Flip-Flops, and the manipulating them using combinational logics.
我们可以看到,在上述架构中可以看到,该芯片的输出状态不仅取决于当前时刻的输入,还取决于上一时刻的输出: s t a t e [ t ] = f ( i n p u t , s t a t e [ t − 1 ] ) state[t]=f(input,state[t-1]) state[t]=f(input,state[t−1])。
2.4 一位寄存器
💜目标(Goal):“永远”记住输入的1bit信息,直到被要求记住一个新的输入(remember an input bit “forever”:until requested to load a new value.)
🌺实现(Implementation):
寄存器和触发器的逻辑并不完全相同:
- 触发器永远保存上一个单位时间的输入;如果上一个单位时间的输入改变,则该单位时间的输出也随之改变;
- 寄存器:只有在
load = 1
时,才会不断加载上一个单位时间的输入,直至load = 0
则会永远保存load = 0
之前那个单位时间的输入。即: if load(t-1) then out(t)=in(t-1) else out(t)=out(t-1) \begin{aligned}&\text{if load(t-1) then out(t)=in(t-1)}\\&\text{else out(t)=out(t-1)}\end{aligned} if load(t-1) then out(t)=in(t-1)else out(t)=out(t-1)
要想实现上述1bit
寄存器的功能,只需在DFF
之前加上一个选择器即可,根据load
进行选择如何输出:
3、内存单元:RAM
3.0 前言
如下图,当我们讲到存储器Memory
时,它是冯诺依曼体系结构的五大组成部分之一。
同时存储器有按照在计算机中的作用or层次可以分为以下两种:
- Main memory(主存储器or内存储器): 随机存储器(RAM)…
主存储器是存储在计算机内部的并连接到主板上的,CPU可以直接访问。作用是用来存储计算机运行器件所需要的数据和指令(指令本身就是程序的组成部分)。 - Secondary memory(辅助存储器):磁盘disks,…
按照信息的可保存性分类为:
- Volatile(易失性存储器)
eg: RAM - non-volatile(非易失性存储器)
eg: ROM
在本门课课程中我们只关心打✔的以下几项:
3.1 RAM的基础存储元素:寄存器
在2.4
,我们学习了一位寄存器,我们不难想到可以将其扩展为多位寄存器;其中w
表示位宽,可以为:16-bit, 32-bit...
。
16-bit
的寄存器逻辑和输入输出接口如下:
/*** 16-bit register:* If load is asserted, the register's value is set to in;* Otherwise, the register maintains its current value:* if (load(t)) out(t+1) = int(t), else out(t+1) = out(t)*/
CHIP Register {IN in[16], load;OUT out[16];PARTS: Replace this comment with your code.
}
3.2 RAM unit
RAM的体系架构如下:
- RAM的抽象概念:(RAM abstraction): 是一系列(
n
个)可寻址的寄存器组成,地址为0~1
(A sequence ofn addressable registers,with addresses 0 to n-1) - 在任意给定时间单位内: 只有一个寄存器可以被选中。(At any given point of time,only oneregister in the RAM is selected)
k
(输入地址的位宽): k = l o g 2 n \mathrm{k=log_2n} k=log2nw
(输入数据的位宽)在本门课所构建的Hack计算机中,w = 16
RAM
的逻辑如下:
//Let M stand for the state of
//the selected register
if load then {M = in//from the next cycle onward:out = M
}
else out = M
-
RAM
的读取逻辑:
-
RAM
的写入逻辑:
3.3 16输入位宽的RAM系列
如上图可以看到RAM n
的n
表示这个内存有n
个寄存器,且每个寄存器的输入位宽这里默认是16
。
到这里我们也知道RAM
的含义: Ramdom Access Memory
,因为我们只用确定好要访问的寄存器地址,就能在同等的时间内快速选取到该寄存器对其进行处理。
4、程序计数器PC: Program Counter
4.0 前言
CPU
是由ALU
+控制器
组成:
其中控制器是整个系统的 指挥中枢,基本功能就是执行指令。
其中程序计数器PC
是控制器的组成部分。用于指出将要执行的指令在主程序中存放的地址,CPU再根据地址去取出指令;由于指令在内存中通常都是顺序存放,它有自增 的功能。
🐣eg: 比如我写了一个程序给机器人如何做蛋糕,这个程序由49
条指令组成,顺序存放。那么机器人做蛋糕就从第1条指令出发,顺次往下自增得到指令并执行即可;同时,可以随意从一个指令跳到另一个指令(比如完成了一个蛋糕后,再重新做一个蛋糕时不必从指令1开始,或许可以从指令17开始,因为前面的准备工作都已完成。
4.1 PC的作用
- 首先,计算机必须要跟踪当前指令的执行以及即将执行的指令(The computer must keep track of which instruction should be fetched and executed next)
- 上述这种控制可以被PC,即程序计数器实现。
- 程序计数器包含下一个将要被取得和执行的指令地址(The PC contains the address of the instruction that will befetched and executed next)
同时,PC必须包含以下三个基本设置:
- Reset: fetch the first instruction (
PC = 0
) - Next: fetch the next instruction (
PC++
) - Goto: fetch instruction n (
PC = n
)
4.2 深入PC
PC的结构如下:
PC的逻辑如下:
/*** A 16-bit counter.* if reset(t): out(t+1) = 0* else if load(t): out(t+1) = in(t)* else if inc(t): out(t+1) = out(t) + 1* else out(t+1) = out(t)*/
CHIP PC {IN in[16], reset, load, inc;OUT out[16];PARTS: Replace this comment with your code.
}
5、Project 3
5.1 一位存储器
- 🐣分析:1 bit寄存器的结构如下,由一个数据选择器
mux
和一个DFF
组成:
-
🪴HDL代码:
/*** 1-bit register:* If load is asserted, the register's value is set to in;* Otherwise, the register maintains its current value:* if (load(t)) out(t+1) = in(t), else out(t+1) = out(t)*/ CHIP Bit {IN in, load;OUT out;PARTS: Replace this comment with your code.Mux(a= outDFF, b= in, sel= load, out= o1);DFF(in= o1, out = outDFF,out = out); }
Tips:注意到
DFF
的out
有两个赋值,这是正确的,因为其输出确实连接了两个引脚,HDL本身也就是描述硬件的连接的。
5.2 16位寄存器
-
🐣分析:16bit的寄存器本身就是将16个1bit的寄存器并接即可,
load
都由一位控制即可。 -
🪴HDL代码:
/*** 16-bit register:* If load is asserted, the register's value is set to in;* Otherwise, the register maintains its current value:* if (load(t)) out(t+1) = int(t), else out(t+1) = out(t)*/ CHIP Register {IN in[16], load;OUT out[16];PARTS: Replace this comment with your code.// Put your code here:Bit(in= in[0],load=load ,out=out[0] );Bit(in= in[1],load=load ,out=out[1] );Bit(in= in[2],load=load ,out=out[2] );Bit(in= in[3],load=load ,out=out[3] );Bit(in= in[4],load=load ,out=out[4] );Bit(in= in[5],load=load ,out=out[5] );Bit(in= in[6],load=load ,out=out[6] );Bit(in= in[7],load=load ,out=out[7] );Bit(in= in[8],load=load ,out=out[8] );Bit(in= in[9],load=load ,out=out[9] );Bit(in= in[10],load=load ,out=out[10] );Bit(in= in[11],load=load ,out=out[11] );Bit(in= in[12],load=load ,out=out[12] );Bit(in= in[13],load=load ,out=out[13] );Bit(in= in[14],load=load ,out=out[14] );Bit(in= in[15],load=load ,out=out[15] ); }
5.3 RAM8
-
🐣分析:
- 让输入
in[16]
与所有的寄存器连接 - 反向选择器连接
load
,用address
来选择判断让哪个寄存器来进行加载数据 - 数据输出部分很简单,利用多路选择器并用
address
来判断读取哪个寄存器的数据
- 让输入
-
🪴HDL代码:
/*** Memory of eight 16-bit registers.* If load is asserted, the value of the register selected by* address is set to in; Otherwise, the value does not change.* The value of the selected register is emitted by out.*/ CHIP RAM8 {IN in[16], load, address[3];OUT out[16];PARTS: Replace this comment with your code.进行数据读取的判断,选择哪个寄存器来加载数据DMux8Way(in= load, sel= address, a= load0, b= load1, c= load2, d= load3, e= load4, f= load5, g= load6, h= load7);将输入与每个寄存器相连Register(in= in, load= load0, out= o0);Register(in= in, load= load1, out= o1);Register(in= in, load= load2, out= o2);Register(in= in, load= load3, out= o3);Register(in= in, load= load4, out= o4);Register(in= in, load= load5, out= o5);Register(in= in, load= load6, out= o6);Register(in= in, load= load7, out= o7);使用多路选择器来判断将哪个寄存器的结果输出Mux8Way16(a= o0, b= o1, c= o2, d= o3, e= o4, f= o5, g= o6, h= o7, sel= address, out= out); }
5.4 …->RAM16
现在我们要完成RAM64~RAM512
,但这也是基于之前的RAM8
、RAM64
…逐渐抽象的过程:
1 RAM64
-
🐣分析:
RAM64
的本质是8
个RAM8
其中address[3~5]
负责选择哪一个RAM
进行输入和输出。,address[0~2]
负责选择这个RAM
里面的哪个寄存器
-
🪴HDL代码:
/*** Memory of sixty four 16-bit registers.* If load is asserted, the value of the register selected by* address is set to in; Otherwise, the value does not change.* The value of the selected register is emitted by out.*/ CHIP RAM64 {IN in[16], load, address[6];OUT out[16];PARTS: Replace this comment with your code.DMux8Way(in= load, sel= address[3..5], a= load0, b= load1, c= load2, d= load3, e= load4, f= load5, g= load6, h= load7);RAM8(in= in, load= load0, address= address[0..2], out= o0);RAM8(in= in, load= load1, address= address[0..2], out= o1);RAM8(in= in, load= load2, address= address[0..2], out= o2);RAM8(in= in, load= load3, address= address[0..2], out= o3);RAM8(in= in, load= load4, address= address[0..2], out= o4);RAM8(in= in, load= load5, address= address[0..2], out= o5);RAM8(in= in, load= load6, address= address[0..2], out= o6);RAM8(in= in, load= load7, address= address[0..2], out= o7);Mux8Way16(a= o0, b= o1, c= o2, d= o3, e= o4, f= o5, g= o6, h= o7, sel= address[3..5], out= out); }
2 RAM512
这个和RAM64
的思路完全一样:
/*** Memory of 512 16-bit registers.* If load is asserted, the value of the register selected by* address is set to in; Otherwise, the value does not change.* The value of the selected register is emitted by out.*/
CHIP RAM512 {IN in[16], load, address[9];OUT out[16];PARTS: Replace this comment with your code.DMux8Way(in= load, sel= address[6..8], a= load0, b= load1, c= load2, d= load3, e= load4, f= load5, g= load6, h= load7);RAM64(in= in, load= load0, address= address[0..5], out= o0);RAM64(in= in, load= load1, address= address[0..5], out= o1);RAM64(in= in, load= load2, address= address[0..5], out= o2);RAM64(in= in, load= load3, address= address[0..5], out= o3);RAM64(in= in, load= load4, address= address[0..5], out= o4);RAM64(in= in, load= load5, address= address[0..5], out= o5);RAM64(in= in, load= load6, address= address[0..5], out= o6);RAM64(in= in, load= load7, address= address[0..5], out= o7);Mux8Way16(a= o0, b= o1, c= o2, d= o3, e= o4, f= o5, g= o6, h= o7, sel= address[6..8], out= out);
}
3 RAM4k
/*** Memory of 4K 16-bit registers.* If load is asserted, the value of the register selected by* address is set to in; Otherwise, the value does not change.* The value of the selected register is emitted by out.*/
CHIP RAM4K {IN in[16], load, address[12];OUT out[16];PARTS: Replace this comment with your code.// Put your code here:DMux8Way(in=load ,sel=address[9..11] ,a=load0 ,b=load1 ,c=load2 ,d=load3 ,e=load4 ,f=load5 ,g=load6 ,h=load7 );RAM512(in=in ,load=load0 ,address=address[0..8] ,out=out0 );RAM512(in=in ,load=load1 ,address=address[0..8] ,out=out1 );RAM512(in=in ,load=load2 ,address=address[0..8] ,out=out2 );RAM512(in=in ,load=load3 ,address=address[0..8] ,out=out3 );RAM512(in=in ,load=load4 ,address=address[0..8] ,out=out4 );RAM512(in=in ,load=load5 ,address=address[0..8] ,out=out5 );RAM512(in=in ,load=load6 ,address=address[0..8] ,out=out6 );RAM512(in=in ,load=load7 ,address=address[0..8] ,out=out7 );Mux8Way16(a=out0 ,b=out1 ,c=out2 ,d=out3 ,e=out4 ,f=out5 ,g=out6 ,h=out7 ,sel=address[9..11] ,out=out );
}
4 RAM16k
/*** Memory of 16K 16-bit registers.* If load is asserted, the value of the register selected by* address is set to in; Otherwise, the value does not change.* The value of the selected register is emitted by out.*/
CHIP RAM16K {IN in[16], load, address[14];OUT out[16];PARTS:// Put your code here:DMux4Way(in=load ,sel=address[12..13] ,a=load0 ,b=load1 ,c=load2 ,d=load3);RAM4K(in=in ,load=load0 ,address=address[0..11] ,out=out0 );RAM4K(in=in ,load=load1 ,address=address[0..11] ,out=out1 );RAM4K(in=in ,load=load2 ,address=address[0..11] ,out=out2 );RAM4K(in=in ,load=load3 ,address=address[0..11] ,out=out3 );Mux4Way16(a=out0 ,b=out1 ,c=out2 ,d=out3 ,sel=address[12..13] ,out=out );
}
5.5 PC
-
🐣分析:
- 首先,这是一个时序电路,那么它的结构肯定是复合
2.5 时序逻辑的实现
那样,如下图:
- 由上图我们可以确定,组合逻辑在左边,而时序逻辑(寄存器)在右半部分。
- 那么难点其实是组合逻辑的书写
这里直接看最终代码:
- 首先,这是一个时序电路,那么它的结构肯定是复合
/*** A 16-bit counter with load and reset control bits.* if (reset[t] == 1) out[t+1] = 0* else if (load[t] == 1) out[t+1] = in[t]* else if (inc[t] == 1) out[t+1] = out[t] + 1 (integer addition)* else out[t+1] = out[t]*/CHIP PC {IN in[16],load,inc,reset;OUT out[16];PARTS:// Put your code here:Inc16(in=state,out=inc16);Mux16(a=state ,b=inc16 ,sel=inc ,out=tmp1 );Mux16(a=tmp1 ,b=in ,sel=load ,out=tmp2 );Mux16(a=tmp2 ,b=false ,sel=reset ,out=newstate );Register(in=newstate ,load=true ,out=out,out=state );
}
可以看到,它其实是从几个多层if
判断有里往外写的,这个是很难理解的一点,因为拿到这个输入,我们很自然而然的就会想到它是由外往里进行逻辑判断的。但实际上并不是这样,因为其实越靠外面越会直接得到最终结果,就应该更靠逻辑图的右边(距离输出更近)