Intel 80x86系列CPU保护模式下最核心的部件中几个寄存器的作用

嵌入式ARM 发表于 2018-04-13 15:16:19 收藏 已收藏
赞(0) •  评论(0

Intel 80x86系列CPU保护模式下最核心的部件中几个寄存器的作用

嵌入式ARM 发表于 2018-04-13 15:16:19

这里我们主要介绍Intel 80x86系列CPU保护模式下最核心的部件中几个寄存器的作用,这些寄存器在Linux内核运行时起着至关重要的作用。至于其他那些各式各样的硬件设备,我们在讲解设备驱动时会针对具体的驱动程序来介绍的。首先,大家先看看CPU的主要架构:

EU(通用寄存器、运算器和控制器)执行部件:完成指令所要求的功能。

SU(段寄存器、段转换器)分段部件:完成执行单元的地址请求, 将虚地址转换为线性地址。

PU(TLB、页转换器)分页部件:将线性地址转换为物理地址。

BIU(总线接口)接口部件:完成指令预取请求和执行单元的数据存取请求,数据存取请求优先于指令预取请求。

IPU(控制逻辑和预取队列)预取部件:16字节指令预取队列, 提出预取请求。

IDU(指令译码、6字节指令队列)译码部件:完成指令译码功能。

FPU(片内集成了浮点协处理器):专用于浮点运算的处理部件。

下面,我们针对EU、SU和PU模块做做详细说明,其他模块就暂时不介绍了。

1 EU模块

EU模块是CPU中最核心,最重要的部件。现在的奔腾CPU已经发展了若干年了,但其中最起作用的还是加法单元ALU,一组通用寄存器组、一个标志和控制逻辑。如图:

首先,8个32位通用寄存器按使用情况分为三种:指针寄存器、变址寄存器、数据寄存器。

[1] 指针寄存器:主要提供全部或部分偏移量

ESP:专门存放堆栈段中栈顶单元的偏移量。

EBP:存放堆栈段中某个单元的全部/部分偏移量,也可存放32位或16位操作数或运算结果。

[2] 变址寄存器

ESI/EDI:存放主存操作数的全部/部分偏移量,也可存放16位操作数和结果,在多数情况功能可以互换。 但在串操作指令中作用不能互换,源操作数必须用ESI提供偏移量,目的操作数必须用EDI提供偏移量。

[3] 数据寄存器

◆ 数据寄存器既可以作为4个32位的寄存器,也可以作为8个16位的寄存器 ,还可以作为16个8位的寄存器。

◆ 在程序中,数据寄存器用来存放操作数、运算结果或其他信息。

◆ 数据寄存器在许多指令中要求指明使用,但也有隐含或特定使用,详细情况请查阅相关资料。

其次,4个控制寄存器CR0~CR3

[1] CR0:由80286的MSW寄存器演变而来,并增加了2位,Linux最看重他的PG位——PG=0,允许分页;PG=1,不允许分页。

[2] CR1:未使用

[3] CR2:页故障地址寄存器, 存放出现故障的页的32位线性地址

[4] CR3:页目录基地址寄存器, 存放页目录表的基地址。

最后来看看标志寄存器FR

FR用来记录程序执行时的状态,即两个操作数通过ALU后的状态:

[1] 进位标志位CF(Carry Flag)

[2] 奇偶标志位PF(Parity Flag)

[3] 辅助进位标志位AF(Auxiliary Carry Flag)

[4] 零值标志位ZF(Zero Flag)

[5] 符号标志位SF(Sign Flag)

[6] 溢出标志位OF(Overflow Flag)

[7] 单步标志位TF(Trace Flag)

[8] 中断标志位IF(Interrupt-enable Flag)

[9] 方向标志位DF(Direction Flag)

2 SU模块

下面着重看看SU部件。这个部件也被Linux用到了,但Linux用它的目的并不是遵循Intel手册对地址进行虚拟化,而是利用它来做用户态和内核态的切换。而对地址的虚拟化,则是通过PU单元,也就是分页机制来实现的。

首先来看看SU模块的架构图:

处理器提供了6个段寄存器,段寄存器的唯一的目的是存放选择子(16位)。这些段寄存器称为cs, ss, ds, es, fs和gs。尽管只有6个段寄存器,但程序可以把同一个段寄存器用于不同的目的,方法是先将其值保存在存储器中,用完后再恢复。

6个寄存器中3个有专门的用途:

cs——代码段寄存器,指向包含程序指令的段。

ss——栈段寄存器,指向包含当前程序栈的段。

ds——数据段寄存器,指向包含静态数据或者全局数据的段。

其它三个段寄存器作一般用途,可以指向任意的数据段。

每个段由一个8字节的描述子(Segment Descriptor)表示,它描述了段的特征。描述子放在全局描述符表(Global Descriptor Table, GDT)或局部描述符表(Local Descriptor Table, LDT)中,这些表位于内存中,如图所示。如果是多CPU,则每个CPU定义一个GDT,而每个进程除了存放在GDT中的段之外如果还需要创建附加的段,就可以有自己的LDT。GDT在主存中的基地址和大小存放在gdtr处理器寄存器中,当前正被使用的LDT地址和大小放在ldtr处理器寄存器中。

虚拟地址由16位选择子和32位偏移量组成,段寄存器仅仅存放选择子。CPU的分段单元(SU)执行以下操作:

[1] 先检查选择子的TI字段,以决定描述子对应的描述子保存在哪一个描述符表中。TI字段指明描述子是在GDT中(在这种情况下,分段单元从gdtr寄存器中得到GDT的线性基地址)还是在激活的LDT中(在这种情况下,分段单元从ldtr寄存器中得到LDT的线性基地址)。

[2] 从选择子的index字段计算描述子的地址,index字段的值乘以8(一个描述子的大小,其实就是屏蔽掉末尾那三位指示特权级的CPL和指示TI的字段),这个结果与gdtr或ldtr寄存器中的内容相加。

[3] 将对应的描述子从内存拷贝到CPU的隐Cache中,这样,只有在选择子改变的情况下才会修改Cache中的内容。

[4] 把逻辑地址的偏移量与隐Cache中描述子Base字段的值相加就得到了线性地址。

请注意,多亏了与段寄存器相关的不可编程的隐Cache,只有当段寄存器的内容被改变时才需要执行前三个操作。

LDT在Linux中使用得很少,我们就不细说他了,它跟我们下面讲的IDT差不多。

中断描述符表(Interrupt Descriptor Table,IDT)是一个系统表,它与每一个中断或异常向量相联系,每一个向量在表中有相应的中断或异常处理程序的入口地址。内核在允许中断发生前,必须适当地初始化IDT。

IDT的格式与这GDT和LDT的格式非常相似,表中的每一项对应一个中断或异常向量,每个向量由8个字节组成。因此,最多需要256×8=2048字节来存放IDT(Linux有256个中断向量)。

idtr寄存器使IDT可以位于内存的任何地方,它指定IDT的线性基地址及其大小(最大长度)。在允许中断之前,必须用lidt汇编指令初始化idtr。

IDT包含三种类型的描述符,下图显示了每种描述符中的64位的含义。尤其值得注意的是,在40~43位的Type字段的值表示描述符的类型。

这里还要提一个问题,就是TSS技术是个很过时的技术,Linux并没有按照Intel要求的那样把TSSD(任务门)存放到IDT中,而是存放在全局描述符GDT中。每个CPU的tr寄存器包含了对应TSS的TSSD选择符(这个选择符可以编程),还包含了两个隐藏的非编程字段:TSSD的Base字段和Limit字段作为隐Cache,这样,处理器就能直接对TSS寻址而不用从GDT中检索TSS的地址。TSS这个东西主要用来在进程切换的时候保存部分CPU寄存器的内容(其实就主要是堆栈切换的时候使用到的那些寄存器)。Linux只为每个CPU准备一个TSS数据结构——tss_struct,只是用来存放当前进程的部分寄存器内容,并没有按照Intel推荐的那样为每个进程准备一个TSS数据结构,并存放所有的内容。所以,按照我的理解,每个进程的那个thread_struct结构存放的内容就是当进程被执行的时候需要tss_struct记住的那些寄存器的内容。

当执行了一条指令后,CS和eip这对寄存器包含下一条将要执行的指令的逻辑地址。在处理那条指令之前,控制单元会检查在运行前一条指令时是否已经发生了一个中断或异常。如果发生了一个中断或异常,那么控制单元执行下列操作:

1. 确定与中断或异常关联的向量i (0 ≤ i ≤ 255)。

2. 读由idtr寄存器指向的 IDT表中的第i项。

3. 从gdtr寄存器获得GDT的基地址,并在GDT中查找,以读取IDT表项中的选择符所标识的段描述符。这个描述符将会是一个中断门或者一个陷阱门,其含有指定中断或异常处理程序所在段的基地址。

4. 确信中断是由授权的(中断)发生源发出的。首先将当前特权级CPL(存放在cs寄存器的低两位)与段描述符(存放在GDT中)的描述符特权级DPL比较,如果CPL小于DPL,就产生一个“General protection”异常,因为中断处理程序的特权不能低于引起中断的程序的特权。对于编程异常,则做进一步的安全检查:比较CPL与处于IDT中的门描述符的DPL,如果DPL小于CPL,就产生一个“General protection”异常。这最后一个检查可以避免用户应用程序访问特殊的陷阱门或中断门。

5. 检查是否发生了特权级的变化,也就是说,CPL是否不同于所选择的段描述符的DPL。如果是,控制单元必须开始使用与新的特权级相关的栈。通过执行以下步骤来做到这点:

i. 读tr寄存器,以访问运行进程的TSS段。

ii. 用与新特权级相关的栈段和栈指针的正确值装载ss和esp寄存器。这些值可以在TSS中找到。

iii. 在新的栈中保存ss和esp以前的值,这些值定义了与旧特权级相关的栈的逻辑地址。

6. 如果故障已发生,用引起异常的指令地址装载CS和eip寄存器,从而使得这条指令能再次被执行。

7. 在栈中保存eflags、CS及eip的内容。

8. 如果异常产生了一个硬件出错码,则将它保存在栈中。

9. 装载cs和eip寄存器,其值分别是IDT表中第i项门描述符的段选择符和偏移量字段。这些值给出了中断或者异常处理程序的第一条指令的逻辑地址。

控制单元所执行的最后一步就是跳转到中断或者异常处理程序。换句话说,处理完中断信号后,控制单元所执行的指令就是被选中处理程序的第一条指令。

中断或异常被处理完后,相应的处理程序必须产生一条iret指令,把控制权转交给被中断的进程,这将迫使控制单元:

1. 用保存在栈中的值装载CS、eip或eflags寄存器。如果一个硬件出错码曾被压入栈中,并且在eip内容的上面,那么,执行iret指令前必须先弹出这个硬件出错码。

2. 检查处理程序的CPL是否等于CS中最低两位的值(这意味着被中断的进程与处理程序运行在同一特权级)。如果是,iret终止执行;否则,转入下一步。

3. 从栈中装载ss和esp寄存器,因此,返回到与旧特权级相关的栈。

4. 检查ds、es、fs及gs段寄存器的内容,如果其中一个寄存器包含的选择符是一个段描述符,并且其DPL值小于CPL,那么,清相应的段寄存器。控制单元这么做是为了禁止用户态的程序(CPL=3)利用内核以前所用的段寄存器(DPL=0)。如果不清这些寄存器,怀有恶意的用户态程序就可能利用它们来访问内核地址空间。

3 PU模块

分页单元PU模块的目的是把线性地址转换成物理地址。其中一个关键任务是把所请求的访问类型与线性地址的访问权限相比较,如果这次内存访问是无效的,就产生一个缺页异常。

分页单元把所有的主存看成一块一块的,称其为页框(page frame)(有时叫做物理页)。每一个页框是固定的大小(跟分段的最大区别,一般为32位处理器为4k、64位处理器为64k)包含一个页(page)。

把线性地址映射到物理地址的数据结构称为页表(page table),其存放在主存中,并在启用分页单元之前必须由内核对页表进行适当的初始化。从80386开始,所有的80x86处理器都支持分页,它通过设置cr0寄存器的PG标志启用。当PG=0时,线性地址就被解释成物理地址。

从80386起,Intel处理器的分页单元处理4KB的页。32位的线性地址被分成3个字段:

目录(Directory)——最高10位

页表(Table)——中间10位

偏移量(Offset)——最低12位

当一个进程运行时,必须有一个分配给它的页目录,其每一个目录项指向一个页表的地址。不过,没有必要马上为进程的所有页表都分配内存。Linux是在当进程实际需要一个页表时才给该页表分配RAM以提高效率。

正在使用的页目录的物理地址存放在控制寄存器cr3中。线性地址内的最高10位(Directory字段)决定页目录中的目录项,而目录项指向适当的页表。地址的中间10位(Table字段)依次又决定页表中的表项,而表项含有页所在页框的物理地址。最低12位(Offset字段)决定页框内的相对位置(见图)。由于它是12位长,故每一页含有4096字节的数据。

页目录项和页表项有同样的结构,每项的内容主要包括对应页(页表也是一个页)的索引以及对应页的状态,我们将在存储管理中分段Linux分段分页机制博文中详细介绍。

下面再来谈谈分页的硬件保护方案,分页单元和分段单元的保护方案不同。尽管80x86处理器允许一个段使用四种可能的特权级别,但与页和页表相关的特权级只有两个,由页目录项和页表项有同样的结构的User/Supervisor标志所控制。若这个标志为0,只有当CPL小于3(这意味着对于Linux而言,处理器处于内核态)时才能对页寻址;若该标志为1,则总能对页寻址。

此外,与段的三种存取权限(读,写,执行)不同的是,页的存取权限只有两种(读,写)。如果页目录项或页表项的Read/Write标志等于0,说明相应的页表或页是只读的,否则是可读写的。

4 高速缓存

当今的微处理器时钟频率接近几个GHZ,而动态RAM(DRAM)芯片的存取时间是时钟周期的数百倍。这意味着,当从RAM中取操作数或向RAM中存放结果这样的指令执行时,CPU可能等待很长时间。

为此,80x86体系结构中引入了一个叫行(line)的新单位。行由几十个连续的字节组成,它们以脉冲突发模式(burst mode)在慢速DRAM和快速的片上静态RAM(SRAM)之间传送,用来实现高速缓存。

具体的高速缓存实现细节太复杂,我只简单地说说原理:当访问一个RAM存储单元时,CPU从物理地址中提取出子集的索引号并把子集中所有行的标签与物理地址的高几位相比较。如果发现某一个行的标签与这个物理地址的高位相同,则CPU命中一个高速缓存(cache hit);否则,高速缓存没有命中(cache miss)。

当命中一个高速缓存时,高速缓存控制器的操作不同,具体取决于存取类型。 对于读操作,控制器从高速缓存行中选择数据并送到CPU寄存器;RAM不被访问且节约了CPU时间,因此,高速缓存系统起到了其应有的作用。对于写操作,控制器可能采用以下两个基本策略之一,分别称之为通写(writethrough)和回写(writeback)。在通写中,控制器总是既写RAM 也写高速缓存行,为了提高写操作的效率关闭高速缓存。回写方式只更新高速缓存行,不改变RAM的内容,提供了更快的功效。当然,回写结束以后,RAM最终必须被更新。只有当CPU执行一条要求刷新高速缓存表项的指令时,或者当一个FLUSH硬件信号产生时(通常在高速缓存不命中发生之后), 高速缓存控制器才把高速缓存行写回到RAM中。

当高速缓存没有命中时,高速缓存行被写回到内存中,如果有必要的话,把正确的行从RAM中取出放到高速缓存的表项中。很复杂吧?我们应该大肆庆幸,因为所有这一切都在硬件级处理,内核根本不需要关心。

高速缓存技术正在快速向前发展。例如,第一代Pentium芯片包含一颗称为L1-cache的片上高速缓存。近期的芯片又包含另外的容量更大,速度较慢,称之为L2-cache,L3-cache的片上高速缓存。多级高速缓存之间的一致性是由硬件实现的。Linux忽略这些硬件细节并假定只有一个单独的高速缓存。

处理器的cr0寄存器的CD标志位用来启用或禁用高速缓存电路。这个寄存器中的NW标志指明高速缓存是使用通写还是回写策略。

除了通用硬件高速缓存之外, 80x86处理器还包含了另外一个称之为翻译后备缓冲器或TLB(Translation Lookaside Buffer,有些书上也把这组寄存器叫做“联想存储器”)的高速缓存用于加快线性地址的转换。当一个线性地址被第一次使用时,通过慢速访问RAM中的页表计算出相应的物理地址。同时,物理地址被存放在一个TLB表项(TLB entry)中,以便以后对同一个线性地址的引用就可以快速地得到转换,如图。

例如CPU给出有效地址为(D,P,W),它把页号P送入输入寄存器,随后立即和TLB各单元的页号进行比较,如与某个单元中的页号相匹配,则把该单元中的块号B送入输出寄存器。这样,就可以用(D,B,W)访问相应的主存单元。

在多处理系统中,每个CPU都有自己的TLB,这叫做该CPU的本地TLB。与硬件高速缓存相反,TLB中的对应项不必同步,这是因为运行在现有CPU上的进程可以使同一线性地址与不同的物理地址发生联系。

当CPU的cr3控制寄存器被修改时,硬件自动使本地TLB中的所有项都无效,这是因为新的一组页表被启用而TLB指向的是旧数据。

做嵌入式系统开发,经常要接触硬件。做嵌入式开发对数字电路和模拟电路要有一定的了解。这样才能深入的研究下去。下面我们简单的介绍嵌入式开发中的一些硬件相关的概念。

总线(Bus)

在嵌入式系统中一定会有一块处理器芯片,此外,还有其它的芯片作为外部设备(后面简称外设),这些芯片与处理器协作实现产品的功能。复杂的产品往往是由大量的芯片组成的。那么不可避免的是我们需要将所有的外设与处理器进行相连,最为简单的是将所有的外设都采用独立(注意是独立)的信号线连接至处理器,这样的好处是容易理解,但问题是:不可行。

因为处理器芯片需要引出太多的线了,从芯片的生产和产品的生产角度来看都不实际。加之,处理器(在此我们假设处理器是单核的,而不是多核的)处理事务在微观上是串行的,也就是说在某一时刻如果要对外设进行读写操作,那只可能是对大量外设中的一个进行,即多个外设不可能在微观上被处理器同时访问。

需要注意的是,这里提出了微观这一概念,这是为了区别于宏观。从宏观上来讲,一个处理器中可以有多个任务同时运行,但这些任务在微观上却是一个一个运行的(后面会用串行来描述这里所说的“一个一个”),多任务的串行运行实现是由操作系统扮演着重要的角色来实现的。

回到我们的话题,即然将每个外设采用独立的信号线连到处理器不可行,且处理器在单一时间内只会对一个外设进行访问,那我们能不能采用共享的信号线将所有的芯片连在一起呢?这就是总线概念的由来。通俗的说,如果我们周围有十个家庭,为了让这十个家庭每两个之间都能往来,我们并不需要为每两个家庭修一条单独(注意是单独)的路(如果这样,要修45条路),而是可以修一条大路,然后,每个家都与大路相连。

对于总线,我们往往说总线是处理器的,而其它的外设是挂在总线上的。那有一个问题,我们每一时间只能访问挂在总线上的一个外设,那如何区分这些外设呢?和我们的路一样,我们需要用地址来区分每一个家庭,在总线上,也是采用地址来进行区分的。

这样,总线就根据其功能分为两类了。一类是地址总线,这一总线上的数据只会是从处理器向外设“流”,是单向的。另一类则是数据总线,用来将数据从处理器传送到外设(从处理器的角度来说是写操作)或者是将数据从外设传送到处理器(从处理器的角度来说是读操作),显然,数据总线是双向的。也就是说,在我们的嵌入式系统中同时存在地址总线和数据总线将所有需要与处理器进行通讯的芯片连在一起的。

总线是有宽度的,正如我们的路分为“三车道”或是“四车道”,我们说32位处理器,是指其数据总线宽度是32位,也就是“有32辆车能同时跑”,显然,宽度越是宽我们的处理器速度就越是快,因为我们从外设芯片存取数据的速度会更快,这就是为什么我们的计算机向64位发展的原因。同样的,地址总线也是有宽度的,对于32位处理器其最大宽度也就是32位。

总线的概念有了,那接下来的一个问题是,即使是每一个外设都有一个地址,那这一地址记在哪里呢?是放在外设芯片上吗?如果这样的话,那就有一个问题,每一类外设的地址必须是不能重叠的,而当一个产品中需要两块一样的芯片的话,两块芯片的地址就无法区分了,看来这样操作存在问题。还有,如果这样的话每一个外设也得与(比如,32根)数据总线完全相连,并监听数据线以了解处理器是不是在“叫”自己,这样很是复杂。

此外,地址也有可能因为外设种类的增多而用光。总的来说地址不能存放在外设芯片,那如何让外设知道,此时它是被处理器招换从而需要进行读写访问的呢?答案就是芯片的片选(CS, chip select)信号,或者又号使能(ENable)信号。

片选(CS 或EN)

片选信号对于外设芯片来讲,就是一个(也是一根)通知信号,告诉芯片“嘿,请开门,我要放些东西进来,或是拿些东西走”,这里的东西只能是数据,不可能是玉米棒什么的。那有个问题,这个信号源从哪里来呢?显然,只能从处理器来。那是不是也是像总线那样,每一个芯片都共用一根线连在一起呢?

如果这样,可能处理器“一叫开门”所有的芯片都将“门”打开了。如果是处理器写数据,那可能所有的芯片都被写入同样的数据。而取数据时,每个外设芯片都向外“扔”数据,这一定会造成数据总线冲突,因为有的芯片向总线上“扔”1,有的则“扔”0,这种情况下处理器一定会“发疯”的,因为它不知道应当得到1还是0。

即然这样,那显然不能将所有的片选信号连在一起了,只能是各芯片的片选信号独立。前面提到了地址总线,我们是采用一根地址线连一个外设芯片呢?还是采用其它的方法。如果采用一根地址线连一个外设芯片,那可能最多只能挂接32个芯片了,这显然不行。

其实,在现实中,是采用32位的数字来表示一个外设芯片的地址的,比如1可以表示芯片A,而6534可以表示另外一个芯片B,等等。由此看来,理论上我们可以表示2的32次方(4294967296)个设备,之所以说理论上,是因为有的设备要占用大量的地址。即然这样,那还有一个问题,如果将32位的地址总线转换成芯片的一根片选信号呢?这需要引入译码(器)的概念。

译码(器)

译码器将一个数据转换成一根信号线上的信号,比如3/8译码器,可以将一个位宽是3位的数据转换成8根(2的3次方)完全独立的信号线,当向数据侧写入二进制的011时,对应的是8根线的第3根,当输入二进制的111时,对应的是8根线中的最后一根。有了译码器,处理器的地址线就简化了,只要32根地址线加上外面的译码器,就可以访问大量的外设芯片了。外部设备的选择问题,我们已经解决了,现在还得回头看一看数据总线。

在嵌入式系统中,所有芯片的数据总线可以理解成是直接相连的。之所以用了“可以理解”一词,是因为为了提高总线的负载能力,其中会加入总线驱动器。为了理解,我们看一看我们生活中的自来水,比如,在北京理论上可能所有的水管是连在一起的,但中间可能为了提高水压,存在很多小的水站用来增加供水压力,而不可能全北京所有的自来水自接来自一个水厂。

即然所有的数据总线是连在一起的,那就可能会有问题。当向外部设备写数据时,处理器先向地址总线输送目标外设的地址,地址译码器将其转换成一根信号的片选信号送到了目标外设,目标外设收到这一信号后,将“门”打开。接下来处理器将要传送到外设的数据往数据总线上一放,由于只有目标外设芯片打开了“门”,所以数据只会进入到目标外设,而其它的外设什么也不会收到。很好!处理器向外写数据应当没有问题,我们接下来看一看读。读的话,由于数据是从外设输送到处理器的,尽管我们采用和写一样的方法打开目标外设的“门”,但此时,其它的外设也在数据总线上,它们有可能处于1也可能处于0,是不是会影响处理器读取目标外设的数据呢?结果当然不会,但我们得引入另一个概念:高阻态。

高阻态

很显然,当处理器从目标外设读数据时,我们希望其它没有被选上的芯片的数据总线不会对目标外设所要传送的数据有影响,那怎么办呢?实际上,当芯片没有被选中时,其数据总线都处于高阻态。所谓的高阻态,我们可以理解成这一管脚在外设芯片内部是断开的,如此一来,显然不会对处理器从目标外设读取数据造成任何的影响了。我们说当一个芯片没有被选中或是没有被使能时,其数据总线一定是处于高阻态的。前面用了“门”的开和关来打比方,那“门”是指什么呢?是指外设的数据总线,片选信号的作用就是控制将外设的数据总线与处理器的数据总线相连或是断开。更多的关于高阻态的讲解可参看前面写的文章《高阻态和三态门》。

驱动

总线上的数据是谁放上去的我们就说谁是那一时刻的驱动者。也就是说,当处理器向外设写数据时,它是在驱动数据总线的,而当处理器从目标外设读取数据时,目标外设是在驱动数据总线的。对于地址总线,因为只可能从处理器向目标外设写,所以地址总线永远是由处理器驱动的。当一个芯片没有被选中时,我们说它并不驱动数据总线。

三态门

前面我们说到外设芯片的数据总线在没有被选中时其处于高阻态,当被选中时,其电平可能是高(1)或是低(0)。如此一来,我们说外设的数据总线其芯片管脚是属于三态门的,即存在高电平、低电平和高阻态,三个状态。更多的关于三态门的讲解可参看前面写的文章《高阻态和三态门》。

电平的有效性

前面我们了解了什么是片选信号,也讲到了三态门,需要指出的是片选信号通常不是三态门,其只存在两个状态,即高电平或是低电平。前面我们也说了,片选信号是用来“开门”的,而片选信号又有高和低电平,那到底是高电平表示“开门”呢?还是低电平?对于这一问题,我们称如果一个电平对于一个片选信号表示“开门”那么它就是这一信号的有效电平。比如,对于一个片选信号,如果低电平表示“开门”,那么我们说这个片选信号是低电平有效的。虽然,在这里我们用片选信号来解释电平的有效性,但是很多信号都存在有效性的问题,比如,后面我们将要谈的读信号和写信号都存在有效性问题。

时序

在前面我们说到当处理器要向外设芯片写数据时,需要先将所需访问的外设的地址放在地址总线上,然后,由译码器将地址总线上的数据转换成片选信号,片选信号则使能目标外设芯片,接下来处理器写数据到数据总线上,从而完成一个写操作。显然,在处理器将数据写到数据总线之前地址线上的数据必须一直保留一段时间,否则的话译码器不能长时间的使片选信号有效。当完成了数据的写操作后,处理器就不需要保证地址总线上的地址有效了。我们可以看出,这一系列的操作都有一定严格的时间顺序的,这称之为时序。时序描述了处理器与外部设备的交互信号 “规程”,大家只有按照这一“规程”来操作,才能保证处理器与外部设备之间能正常的通讯。这好比,我们的道路上的红绿灯,如果我们行人和车辆不按照其指示来通行的话,就会出现事故。通常,采用时序图来描述芯片之间通讯的信号“规程”。

从图中我们可以看出ADDRESS是表示地址总线的,DQ是表示数据总线的,CE是片选信号,且是低电平有效,其宽度要保证在进行读操作时总是有效的。学会看时序图对于做嵌入式系统开发非常有帮助,因为我们不可避免的要与芯片打交道。在时序图中,通常会标识很多的时间需求信息。在写启动代码时需要初始化各地址空间的片选地址寄存器和读写时序,时序的配置依据就是来自于外设芯片的时间需求,这是芯片手册很重要的一部分内容。当一个地址空间中存在多个外设芯片时,我们需要考虑到其中最慢的外设芯片的时间需求,否则的话有的芯片就不能正常工作。

读信号

当处理器需要从外设芯片读取信号时,除了需要产生片选信号外,还需要告诉外设芯片这是一个读操作,而不是一个写操作,这是通过读信号来实现的。

写信号

前面讲了读信号,我想对于写信号也就不难理解了,这个信号用于告诉外设芯片,这是一个向外设芯片写数据的操作。

I/O端口

前面提到了外设(芯片)),现在是对外设进行分类的时候了。大体上外设分为两类,一类是存储器外设,而另一类是非存储器外设,后者常被称之为I/O设备,这里的I/O是Input/Output的简写,即输入、输出。可见,I/O外设是一个非常宽泛的概念。对于存储器外设,其特点是,它所占用的空间是连续的一片。比如,SDRAM内存就是属于存储器外设,如果其容量是8M字节,那么其占用的地址空间也会是8M的。

与存储器外设所不同的是,I/O外设所点用的地址一般都很少。比如一个I/O外设可能存在多个控制寄存器,这些控制寄存器从处理器来看就是多个I/O端口(地址),向这个地址写数据就是向外设所对应的寄存器写数据,反之,也可以是读。比如,一个串口芯片可能存在多个寄存器,一个用来查询芯片的状态,一个用来设置芯片的功能,另一个用来读取芯片从串口线所收到的数据,最后,还有一个用来向芯片写数据以向串口线上发送数据。对于这一串口芯片的寄存器,从处理器的角度来看,都是独立的I/O端口。

I/O端口存在读、写性问题,有的端口是只读的,有的端口是只写的,还有的端口是即可读也可写,其读写性是由外设芯片的寄存器所决定的,在芯片的数据手册中能找到。需要指出的是,有些存储器外设也存在I/O端口,以对其进行一定的控制。从I/O端口这一名字来看,对于处理器来说,就是对从外面读入数据或是向外面输出数据的一个接口总称。

中断

中断从硬件的角度来看就是一个能产生高、低电平的一根信号线,但理解它需要从处理器的角度出发。我们说过了,处理器从微观上看,所做的工作是按顺序进行的,其对程序的处理只能是一条指令一条指令的执行。如果存在需要对外设芯片进行访问,而有可能从处理器发出读、写命令后,由于外设通常比处理器慢很多,所以外设芯片需要一些时间来准备好所需的数据。在这种情况下,如果处理器一直等外设芯片的返回数据再执行后续的指令的话,将耗费宝贵的时间,这些时间完全可以用来做其它的工作。

别忘了,从宏观上看来处理器常常是多任务的,任务是指操作系统所提供的调度单位。当一个任务因为等待外设芯片的数据而阻塞时,我们可以切换到另外的任务,从而提高处理效率。这就有一个问题,当处理器去处理另一个任务时,如果外设芯片的数据好了的话,如果告诉处理器呢?对了!就是通过中断信号。中断信号的高、低电平可以用来表示是否有中断需要处理器注意以处理特定的事件(比如,外设数据准备好了的事件)。

由此看来,中断的引入能大大的提高处理器的运用效率。为了使用处理器上的中断,一开始我们需要初始化好处理器的中断控制器,比如安装好所需的中断服务程序或称之为ISR(Interrupt Service Routine),然后,打开中断屏蔽位。中断服务程序中需要做如下的操作:

1. 从外设读入或向外设写数据。读还是写通常需要读取外设的中断状态寄存器来决定。

2. 清除外设的中断信号。我们知道,中断信号是由外设芯片驱动的,为了告诉外设芯片,处理器已经处理完了所需做的工作,那么处理器需要通过一定的方式通知外设芯片。这种方式就是向外设芯片的寄存器中的某一位写入一个数据,比如,可能是写入1表示清中断,也可能是写入0表示清中断,这通常在外设的数据手册中能查到。当外设收到了处理器的清中断请求后,其就会驱动中断线使其无效。比如,一个外设的中断线是当其为低电平表示有中断,将其从低电平变为高电平就是驱动为无效。

3. 清除处理器的中断信号标识。处理器中往往也会保存外部中断信号是否发生过,当我们处理完了外设芯片的中断时,我们也需要清除处理器上的标识,从而为下一次中断做准备。需要注意的是,清外设的中断必须发生在请处理器中断标识之前!

中断还存在一个触发方式问题。有两种触发方式 ,一种是电平触发,另一种是沿触发。电平触发是指电平的高低表示外设是否有中断,而沿触发则是能过中断线上的电平的升或降来表示的,显然,存在两种沿触发方式。一种是中断线从低电平变为高电平,我们称之为上升沿触发,另一处是中断线从高电平转换为低电平,我们称之为下降沿触发。总的来说中断的触发方式有电平触发、上升沿触发和下降沿触发。电平触发方式中处理中中断设置很重要的一个步骤。

万用表

万用表通常是用来查看电平的高低、电阻的大小等的,是常用且必不可少的工具之一。在嵌入式系统开发中,我们常用的是数字万用表。

电平(Level)

在数字电路中,分为高电平和低电平,分别用1和0表示。一个数字电路的管脚,总是存在一个电平的,要么高要么低,或者说要么1要到0(其实,还有另一种状态)。

示波器

在嵌入式系统开发中,我们不可避免的要与外设芯片打交道。调试驱动程序时,除了需要完全看明白芯片的数据手册,且在软件高度的过程中,还需要看我们所期望的信号电平是否发生在芯片上。比如,我们在写驱动程序时,需要通过写I/O端口来对外设芯片进行操作,当写相应的I/O端口时,我们知道所对应芯片的片选信号应当有效,有时,我们需要验证是否按预期发生了,这就需要用到示波器。一般的示波器是能同时观测两个信号线的信号状态的。示波器都提供一定的功能,比如设置信号扑捉的方式等等。示波器很重要的一个参数据是其采集频率,根据Nyquist采集定理,如果我们想用示波器查看频率是100M赫兹的信号,那么其采样频率必须至少是其两倍,即200M赫兹。有人可能会问:为什么不用万用表来看呢?因为万用表的采集频率很底,无法采集到很快的信号变化。

逻辑分析仪

简单的说逻辑分析器就是具有很多信号通道的示波器。通过逻辑分析仪,我们可以看到地址总线和数据总线上的数据。逻辑分析仪都提供一定的编程能力,用于编程什么时候开始对总线上的数据进行采集。

收藏

相关话题
文章来源专栏
+关注

评论(0)

加载更多评论

参与评论

相关文章

分享到

QQ空间 QQ好友 微博
取消