精华内容
下载资源
问答
  • 我们知道,中断在发生时,处理器根据收到的中断向量号在中断描述符表中找到相应的中断门描述符。 处理器从该描述符中加载目标代码段选择子到代码段寄存器 cs 及偏移量到指令指针寄存器 EIP。 注意,由于 cs 加载了...

    我们知道,中断在发生时,处理器根据收到的中断向量号在中断描述符表中找到相应的中断门描述符。

    处理器从该描述符中加载目标代码段选择子到代码段寄存器 cs 及偏移量到指令指针寄存器 EIP。

    注意,由于 cs 加载了新的目标代码段选择子,处理器只要发现段寄存器被加载,段描述符缓冲寄存器就会被刷新,因为处理器认为是换了一个段,属于段间转移,也就是远转移。

    所以,当前进程被中断打断后,为了从中断返回后能继续运行该进程,处理器自动把 cs 和 EIP 的当前值保存到中断处理程序使用的栈中。

    除了要保存 cs、 EIP 外,还需要保存标志寄存器 EFLAGS,

    因为中断是可以在任何特权级别下发生的,不同特权级别下处理器使用不同的栈,如果涉及到特权级变化,还要压入 SS 和 ESP 寄存器。

    那么我们就来看看到底中断的时候是如何来压栈的吧~

    1。当中断发生时,低特权级向高特权级转化时的压栈现象


    当处理器根据中断向量号找到中断描述符后,根据中断描述符中的段描述符选择子+GDTR的值,我们可以找到目标段描述符。

    我们是否能访问这个目标段描述符,要做的就是将找到中断描述符时当前的CPL与目标段描述符的DPL进行对比。

    这里我们讨论的是CPL特权级比DPL低的情况,即数值上CPL > DPL.
    这表示我们要往高特权级栈上转移,也意味着我们最后需要恢复旧栈,所以

    (1) 处理器先临时保存一下旧栈的SS和ESP(SS是堆栈段寄存器,因为换了一个栈,所以其也要变,ESP相当于在栈上的索引),然后加载新的特权级和DPL相同的段,将其加载到SS和ESP中,然后将之前保存的旧栈的SS和ESP压到新栈中

    由于SS堆段寄存器是16位寄存器,所以为了对齐,将其0拓展后压栈。
    然后在新栈中压入EFLAGS标志寄存器,得到图如下:

    2.因为是要切换栈,属于段间转移,所以我们还要将旧栈SS和EIP备份,以便中断程序执行后还可以恢复到被中断的进程。因为CS代码段寄存器也是16位寄存器,所以也要在压入栈前进行0扩展,此时如下图

    3.某些异常会有错误码,此错误码用于报告异常是在哪个段上发生的,也就是异常发生的位置,所以错误码中包含选择子等信息。错误码会紧跟在 EIP 之后入栈,记作 ERROR CODE

    2。当中断发生时,无特权级转化时的压栈现象

    此时由于不会切换栈,就不用保存SS和ESP,但是由于在处理完中断程序后还是要返回源程序中继续执行的,所以,我们的CS EIP寄存器还是要保存的。

    这种中断返 回是用 iret 指令实现的。 Iret,即 interrupt ret。注意在返回的时候,其错误码不会自动跳过,所以需要我们手动跳过。

    ????????????????????????

    好了,我们的压栈过程就到此讲完了,下面我们来了解一下,中断程序返回即出栈的过程,假设此时ESP已经跳过错误码指向eip_old

    当处理器执行到 iret指令时,它首先需要从栈中返回CS_old 及EIP_old,由于处理器在中断返回的时候,并不记得自己来的时候也曾经做过特权级检查,所以这时候它还要再进行特权级检查。

    特权级检查如下:

    (1)将cs_old对应的代码段的DPL及cs_old中的RPL做特权级检查, 如果检查通过,随即需要更新寄存器cs和EIP

    由于cs_old在入栈时已经将高16位扩充为 0,现在是 32 位数据,段寄存器 cs 是 16 位,故处理器丢弃 cs_old 高 16 位,将低16 位加载到 cs.
    将 EIP_old加载到 EIP 寄存器,之后栈指针指向 EFLAGS。

    如果进入中断时未涉及特权级转移,此时栈指针是 ESP_old (说明在之前进入中断后,是继续使用旧栈)。否则栈指针是 ESP_new (说明在之前进入中断后用的是 TSS 中记录的新栈)。

    (2)将栈中保存的EFLAGS 弹出到标志寄存器 EFLAGS。如果在第 1 步中判断返回后要改变特权级, 此时栈指针是 ESP_new,它指向栈中的 ESP_old。否则进入中断时属于平级转移,用的是旧栈,此时栈指针是 ESP_old,栈中已无因此次中断发生而入栈的数据,栈指针指向中断发生前的栈顶。

    (3)如果在第 1 步中判断出返回时需要改变特权级,也就是说需要恢复旧栈,此时便需要将 ESP_old 和 SS_old 分别加载到寄存器 ESP 及 SS,丢弃寄存器 SS 和 ESP 中原有的 SS_new 和 ESP_new,同时进行特权级检查。

    由于 SS_old 在入栈时已经由处理器将高 16 位填充为 0,现在是 32 位数据,所以在 重新加载到栈段寄存器 SS 之前,需要将 SS_old 高 16 位剥离丢弃,只用其低 16 位加载 SS。

    至此,处理器回到了被中断的那个进程。

    参考书籍:《操作系统真相还原》第七章7.4.2

    展开全文
  • Cortex-M系列:中断的内在机理

    千次阅读 2020-01-21 13:54:48
    文中用一个例子解释了惰性压栈的原理。在发生中断嵌套时,Cortex-M处理器将使用出栈抢占、末端连锁、延迟到达等机制来优化响应速度,同时降低了功耗[6]。理解这部分原理,一方面有利于处理在中断中出现的BUG,另一...

    在博客[2]Cortex-M系列: 软件中断和硬件中断中,主要写的是要怎么配置中断并产生中断,而本篇主要将CPU是如何识别我们的代码并运行中断,同时不破坏程序的上下文的。文中用一个例子解释了惰性压栈的原理。在发生中断嵌套时,Cortex-M处理器将使用出栈抢占、末端连锁、延迟到达等机制来优化响应速度,同时降低了功耗[6]。理解这部分原理,一方面有利于处理在中断中出现的BUG,另一方面是有利于深入理解嵌入式操作系统(这部分算是基础)。

    在阅读本文前,推荐先阅读[4]Cortex-M系列:非中断、特权模式下的汇编语言,因为:调用者(函数)+被调用者(函数)的关系和进程(或在无操作系统中的main函数)与中断的上下文关系是类似的。

    本文是在Stm32H743单片机下进行测试的,对Cortex-M4和Cortex-M7适用,对CortexM3不一定适用。

    目录

    1 接收异常请求

    2 异常进入

    2.1 寄存器压栈(Stacking)

    2.1.1 栈帧

    2.1.2 MSP/PSP

    2.1.3 双字对齐

    2.1+ 一个例子

    2.2  从中断向量表中取出异常向量( vector fetch)

    2.3 更新NVIC寄存器和内核状态寄存器

    3 异常中

    3.1 同步/异步异常分类

    3.2 按异常句柄类型分类

    3.3 中断嵌套时stacking的优化机制

    4 异常返回

    5 参考文献


    1 接收异常请求

    在博客[2]中的硬件中断请求设置完成后,要能够接收异常请求还有以下条件(虽然大多是情况下是满足的):

    1、处理器正在运行(未被暂停或处于复位状态)[7.7.1]

    2、异常处于使能状态

    3、异常的优先级高于当前优先级

    4、异常未被屏蔽

     

    2 异常进入

    参考[7.7.2],[2.3.7]

    当有一个具有足够优先级的挂起异常时发生异常项,并且满足一下其中一个条件:

    •处理器处于线程模式。

    •新异常的优先级高于正在处理的异常,在这种情况下,新异常会抢占原始异常。

    当一个异常抢占另一个异常时,异常被嵌套。

    注:2.1,2.2,2.3的工作是同步进行的,并不是顺序关系。

    2.1 寄存器压栈(Stacking)

    先解释下上下文(context),这里的上文指中断前程序所在的地方,下文指中断后的中断服务程序所在的地方,先不考虑中断嵌套的情况。另外文中不特别区分中断和异常两个概念的区别,而当做一个意思。

    2.1.1 栈帧

    寄存器压栈分为短栈帧压栈和长栈帧压栈。

    短栈帧由不保护浮点寄存器的调用者保护寄存器组成:R0~R3,R12,返回地址,LR,xPSR组成,共8个字。[8.1.3]

    注:栈帧包括返回地址。这是中断程序中的下一条指令的地址。此值在异常返回时恢复到PC寄存器,以便中断的程序恢复运行.

     

    长栈帧由包含浮点寄存器的调用者保护寄存器组成:R0~R3,R12,返回地址,LR,xPSR,S0~S15,FPSCR组成,共26个字。

    长短栈帧的选择由:CONTROL.FPCA位决定。该位为1时表示该进程或主函数使用了浮点运算,则需要保护浮点寄存器上文,因此选用长栈帧。反之使用短栈帧。

    2.1.2 MSP/PSP

    选择保存的栈的位置由:CONTROL.SPSEL位决定。该位为1时使用进程栈指针所指向的地址为栈入口,反之(默认)以主栈指针所指向的指针作为栈入口。

    注:有操作系统时,当前任务选择使用进程栈指针(PSP),OS内核及异常处理使用MSP。没有操作系统时,默认始终使用MSP。

    2.1.3 双字对齐

    最后还需要考虑双字对齐:该特性在SCB.CCR.STKALLGN中设置,Cortex-M4和Cortex-M7是默认开启的。压栈前,程序会判断所需要的栈帧压栈后是否满足双字对齐,如果不满足,则将xPSR.bit9写为1,并在压栈后,附加一个栈顶空位来实现对齐。

    另外,“惰性压栈”将在2.1+中讲解。

    2.1+ 一个例子

    现在我们实测下长栈帧的情况:无操作系统的最小系统程序进入主程序后就进入”while(1);“中,程序只开启一个定时器2中断,定时器2中除了清除定时器状态寄存器的状态标志,无其他操作。中断前寄存器为下图(FPU部分比较长,就没截图了):

    中断后在memory找到如下长栈帧:

    疑问:上文没使用浮点运算也是长栈帧,浮点单元中读取的值和Keil寄存器中观察的值不一致。

    a\先解释为什么是长栈帧,FPCA的值:

    1、在SystemInit()前,FPCA==0,在进入main()函数后,FPCA自动变为1,即使程序还没有进行浮点运算,整个没有进入操作系统调度的上下文一直处于FPCA==1的状态。

    2、而在有通过操作系统中建立的每个任务,只要还没有执行浮点运算FPCA一直为0,而只要执行一次浮点运算后,那么无论之后有没有再使用浮点运算,这个任务中的FPCA恒等于1(因为,如果有异常发生时,FPCA状态信息会被存到EXC_RETURN中,在异常返回时,FPCA通过解析EXC_RETURN来写回)。.(本文不主讲操作系统下的线程调度,可详见后续博文)

    3、FPCA在异常处理的开始被清零。每次异常处理都像是创建一个任务,在本次异常处理结束后,“任务”被删除。在这样的比喻下,异常的FPCA设置就和第2条现象一致了。

    这是因为“惰性压栈”的机制引起的。这里符合[13.3]中第二种情况:“被打断的任务中有浮点上下文,ISR中没有”。

    CPACR

    协处理器访问控制寄存器

    Coprocessor Access Control Register

    为协处理器指定访问权限

    FPCCR

    浮点上下文控制寄存器

    Floating-point Context Control Register

    设置或返回FPU控制数据

    LSPACT位为1时:惰性状态保存被激活。已分配浮点堆栈帧,但将状态保存到该帧的操作会被延迟。

    FPCAR

    浮点上下文地址寄存器

    Floating-point Context Address Register

    保存分配在异常堆栈帧上的未填充浮点寄存器空间的位置。

    b\那么由CONTRL.FPCA确定了需要保护上文的浮点寄存器,但浮点寄存的压栈操作会将中断响应时间从12个时钟周期延长到25个时钟周期,引起激活了FPCCR.LSPACT,该位被置位1。在异常中,只有当异常中也进行了浮点运算,LSPACT位才会复位为0,此时S0~S15和FPSCR会根据FPCAR的地址压与预留给浮点寄存器的栈中。若异常中没有浮点运算,则S0~S15将使用处于保留状态,和中断前的浮点寄存器组的值不一致。

     

    2.2  从中断向量表中取出异常向量( vector fetch)

    异常向量可以看做是存储异常服务程序(异常处理)地址的指针。中断向量表的偏移一般是提供给远程升级/在线升级程序使用的。在确定异常处理的起始地址后,指令就会被取出。[7.9.3]

    2.3 更新NVIC寄存器和内核状态寄存器

    1、PC有2.2中的中断向量所指向的地址确定。

    2、LR会更名为EXC_RETURN的特殊值。该值有CONTRL寄存器表征系统状态的位来确定。

    3、在异常处理内部,栈操作使用主栈指针(MSP),处理器运行在特权模式访问等级。

    4、另外中断的挂起和激活属性见博客[2].

    3 异常中

    通过清除相关状态寄存器的事件位,从而清除片内外设的中断请求信号,详见博客[2]

    3.1 同步/异步异常分类

    先解释下同步异常和异步异常[5],在cortex-M7中[2.3.2]:

    异步异常

    Asynchronous exception

    Reset

    NMI

    BusFault

    PendSV

    SysTick

    Interrupt(IRQ)

    除了Reset之外,处理器还可以在触发异常和处理器进入异常处理程序之间执行另一条指令。

    同步异常

    Synchronous exception

    MemManage

    BusFault

    UsageFault

    SVCall

    /

    3.2 按异常句柄类型分类

    中断服务路径

    Interrupt Service Routines (ISRs)

    中断IRQ0到IRQ239是ISRs可以处理的最大异常范围。可用的实际异常数由实现定义。

    错误句柄
    Fault handlers

    HardFault、MemManage fault、UsageFault和BusFault是由错误处理程序处理的错误异常。

    系统句柄
    System handlers

    NMI、PendSV、SVCall SysTick和故障异常都是由系统处理程序处理的系统异常。

    3.3 中断嵌套时stacking的优化机制

    在异常中,可能出现中断嵌套,中断为了保证中断的快速响应,Cortex-M7、Cortex-M4也引入了如下机制,由于不方便观察,这里不细讲,只粘贴进来[2.3.7]

    出栈抢占

    Preemption

    When the processor is executing an exception handler, an exception can

    preempt the exception handler if its priority is higher than the priority of

    the exception being handled.

    When one exception preempts another, the exceptions are called nested

    exceptions.

    末端连锁

    Tail-chaining

    This mechanism speeds up exception servicing. On completion of an

    exception handler, if there is a pending exception that meets the

    requirements for exception entry, the stack pop is skipped and control

    transfers to the new exception handler .

    延迟到达

    Late-arriving

    This mechanism speeds up preemption. If a higher priority exception

    occurs during state saving for a previous exception, the processor switches

    to handle the higher priority exception and initiates the vector fetch for

    that exception. Late arrival does not affect state saving because the state

    saved is the same for both exceptions. Therefore the state saving continues

    uninterrupted. The processor can accept a late arriving exception until the

    first instruction of the exception handler of the original exception enters

    the execute stage of the processor. On return from the exception handler

    of the late-arriving exception, the normal tail-chaining rules apply.

    4 异常返回

    在异常处理的结尾,程序代码执行的返回会引起EXCETURN数值被加载到程序技术器中(PC),触发异常返回机制。[7.7.3]

    过程可以看做是异常进入逆过程,这里就不进行赘述。出栈主要依据的寄存器有FPCCR、CONTRL、EXC_RETURN和被压入栈中的xPSR。

    5 参考文献

    文中涉及的链接

    [1] ARM Cortex-M3 与Cortex-M4 权威指南 ,文中[x.x.x](蓝色)为具体的参考章节

    [2] Cortex-M系列: 软件中断和硬件中断 https://blog.csdn.net/NoDistanceY/article/details/104046160

    [3]  ARM® Cortex®-M7 Devices Generic User Guide , [x.x.x](红色)为具体的参考章节

    [4] Cortex-M系列:非中断、特权模式下的汇编语言 https://blog.csdn.net/NoDistanceY/article/details/104003831

    [5] 同步中断、异步中断区别  https://blog.csdn.net/jasonLee_lijiaqi/article/details/80181814

    [6] 实现减轻Cortex-M设备上CPU功耗的方法和技巧 http://www.elecfans.com/d/758630.html 

    文中未涉及但有参考意义的链接

    [1] 一文分清Cortex-M系列处理器指令集 https://blog.csdn.net/chenhaifeng2016/article/details/70314238

    [2] 微控制器Cortex-M7为高性能而生 针对高端控制系统嵌入式应用 http://www.elecfans.com/emb/danpianji/20180205630354.html

    [3] IEEE754 http://www.softelectro.ru/ieee754_en.html

    [4] ARM官网资料 http://infocenter.arm.com/help/advanced/help.jsp?

    修改

    1、:对部分英文术语的中文翻译进行修改:“抢占”改为“出栈抢占”,“延迟”改为“延迟到达”

    展开全文
  • 中断系统

    中断:

    由于CPU获知了计算机中发生的某些事,CPU暂停正在执行的程序,转而去执行处理该事件的程序,当这段程序执行完毕后,CPU继续执行刚才的程序,整个过程称为中断处理,也称为中断。没有中断,操作系统几乎什么都做不了,操作系统就是中断驱动的。首先,操作系统就是个死循环,这个死循环什么都做不了,仅仅是保持操作系统能够周而复始的运行下去,运行的目的就是为了等待中断发生。
    中断按事件来源分类,来自CPU外部的中断就称为外部中断,来自CPU内部的中断称为内部中断。

    外部中断:

    CPU提供了两条信号线。外部硬件的中断是通过两根信号线通知CPU的,这两根信号是INTR(可屏蔽中断)和NMI(不可屏蔽中断)
    可屏蔽中断可以1,通过lags寄存器的IF位将所有这些外部设备的中断屏蔽。2,通过中断代理8259A来屏蔽单个设备的中断。

    内部中断:

    内部中断分为软中断和异常。
    软中断:就是软件主动发起的中断,具有主观性,它来自于软件运行中指令int 8位立即数(系统调用)、 int3等
    异常:指令执行期间CPU内部产生的错误引起的(除以0等)。不受eflags的IF影响,无法向用户隐瞒
    总结:外部中断的NMI和内部中断都可以无视IF位。

    中断向量号:

    中断的本质就是来一个中断信号后,调用相应的中断处理函数。范围0-255。为每一个中断信号分配一个中断号,然后用此中断号作为中断描述符表的索引,找到对应的表项,进而在表项找到对应的地址,从而转去相应的处理程序。
    异常和不可屏蔽中断的中断向量号是由CPU自动提供的,来自外部设备(时钟、键盘、硬盘)的可屏蔽中断号是由中断代理设置的,软中断是由软件提供的(int 8位立即数)。

    中断描述符表

    中断描述符表是保护模式下用于储存中断处理程序入口地址的表。中断描述符表中的中断描述符称为---门。Linux系统只用了中断门

    中断门包含了中断处理程序所在段的段选择子和段内偏移地址。当通过此方式进入中断后,eflags中的 IF 位自动置0,即进入中断后,自动把中断关闭,避免嵌套。Linux就是利用中断门实现了系统调用:int 0x80。中断门只能在IDT中。

    16位的表界限可以容纳中断描述符的个数为:64KB/8=8192,但是处理器只支持0-255。

    中断处理过程及保护

    完整的中断过程分为CPU外和CPU内两部分:
    CPU外:外部设备的中断由中断代理芯片接受,处理后将该中断向量号发送到CPU
    CPU内:CPU执行该中断向量号对应的中断处理程序

    处理器内部的过程:

    1,处理器根据中断向量号定位中断门描述符
    处理器用中断向量号乘以8后与IDTR中的中断描述符表地址相加,所求的地址之和便是该中断向量号对应的中断描述符的线性地址,然后经过页部件翻译得到真实地址。
    2,处理器进行特权级检查
    若中断由软中断int n,int3(系统调用等)引起,则要进行当前的CPL和门槛DPL_Gate、门框:目标代码段DPL的检查。
    若中断是外部设备和异常引起的,只检查当前CPL和目标代码段的DPL,数值上CPL > 目标代码段DPL。
    3,执行中断处理程序
    特权级检查后,将门描述符目标代码段选择子加载到代码段寄存器CS中,把门描述符中中断处理程序的偏移地址加载到EIP,开始执行中断处理程序。

    中断发生后eflags中的 NT 位和 TF 位会被置 0 。用的是中断门的话eflags 的 IF 位会被自动置0,防止中断嵌套,即处理器会将这个中断完全执行完毕后才能出来下一个中断。从中断返回的指令是 iret ,它从栈中弹出数据到寄存器的 cs、eip eflags 等,根据特权级是否改变,判断是否恢复旧栈,即将SS_old 和 ESP_old 位置的值弹出到寄存器 ss和 esp。

    中断发生时的压栈

    特权级发生转移时:用户进程正在执行时候发生了中断

    1,当前进程被中断打断后,处理器根据中断向量号找到对应的中断描述符,拿CPL和中断门描述符中选择子对应的目标代码段的DPL对比,若CPL权限比DPL低,即数值上CPL >DPL,表示要想高特权级转移,需要切换高特权级的栈。即:从用户进程中断去执行内核中断程序。中断执行完返回时也要回到之前的旧栈,所以处理器临时将SS_old 和 ESP_old 保存到一个内存中,然后CPU取出TSS中目标代码段的DPL 级别的栈加载到寄存器 SS 和 ESP 中,记作 SS_new 和 ESP_new,CPU在找到之前的旧栈,将其压入新栈中:将 SS_old 用0 扩展高16位,成为32位数据。

    2,在新栈中压入EFLAGS寄存器

    3,将要CS_old 和 EIP_old 压入新栈:CS_old 需要用0 来填充高16位为0。

    4,某些异常有错误码,错误码用于报告哪个段上发生了异常,所以ERROR_CODE也压入栈。

    特权级未发生转移时发生的中断:正在执行内核代码发生了中断

    1,在新栈中压入EFLAGS寄存器

    2,将要CS_old 和 EIP_old 压入新栈:CS_old 需要用0 来填充高16位为0。

    3,某些异常有错误码,错误码用于报告哪个段上发生了异常,所以ERROR_CODE也压入栈。

    中断返回的出栈:

    用 iret 指令实现,iret 指令并不知道栈内容的正确性,一次弹出4字节,所以在使用iret 之前,一定要调整好esp的位置,使其依次弹出:EIP、CS、EFLAGS、ESP、SS。因此我们要手动跳过 ERROR_CODE。

    1,当处理器执行到 iret 时,需要从栈中返回。CPU首先检查CS_old 的选择子的 RPL 位,判断在返回过程中是否要改变特权级。弹出 EIP_old 和 CS_old 。

    2,弹出 EFLAGS。若需要改变特权级,则弹出旧栈。若不需要改变特权级,则是平级转移。

    如果需要改变特权级,CPU还会检查数据段寄存器DS、ES、FS、GS的内容,如果它们之中某个寄存器的选择子所指向的数据段描述符的DPL权限比返回后的CPL高,则处理器将该段寄存器置为 0 。

    可编程中断控制器8259A

    8259A的作用是负责所有来自外设的中断,如时钟的中断、键盘的中断等等。8259A它用于管理和控制可屏蔽中断,表现在可以屏蔽想要屏蔽的外设中断,对外设中断实行优先级判决,向CPU提供中断向量号等功能。每个独立运行的外部设备就是一个中断源,可以发中断信号给8259A。
    个人电脑中只有两片 8259A 芯片,共16 个 IRQ 接口。
    8259A芯片的内部结构:


    8259A的编程

    8259A内部有两组寄存器,一组是初始化命令寄存器,用来保存初始化命令字,ICW1-ICW4。另一组寄存器是操作命令寄存器组,用来保存操作命令字,OCW1-OCW3。我们对8259A的编程,也分为初始化和操作两部分。

    1,用ICW作初始化:确定是否需要级联、设置起始的中断向量号、设置中断结束模式等等

    ICW1用来初始化8259A的连接方式:单片还是级联       中断信号触发方式:电平触发还是边沿触发

    ICW2用来设置起始中断向量号。指定了 IRQ0 映射到的中断向量号,其他 IRQ 接口对应的中断向量号会顺着自动排列下去。

    ICW3用来设置级联方式下主片和从片用哪个IRQ接口互连。

    ICW4用来设置中断结束模式:自动还是手动。

    2,用OCW来操作控制:中断屏蔽、中断结束

    OCW1用来屏蔽连接在8259A上的外部设备中的中断信号。这些没有屏蔽的中断最终还是要受到eflags的IF位管束,IF=0,不管8259A怎么样设置全部屏蔽。

    OCW2用来设置中断结束方式:发EOI来终止某一个中断      优先级模式:循环还是固定

    OCW3用来设置特殊屏蔽方式和查询方式。

    ICW1和OCW2、OCW3是用偶地端口 0x20(主片)或 0xA0(从片)写入。

    ICW2-ICW4是用奇地址端口 0x21(主片) 或0xA1(从片)写入。

    4个ICW要保证一定的次序写入,8259A就知道写入端口的数据是什么意思了。

    OCW的写入顺序无关,各控制字有唯一标识可以辨别。

    所以8259A 的编程步骤为:

    1,首先必须进行初始化,必须依次写入 ICW1、ICW2 、ICW3 、ICW4

    2,初始化后,可以进行OCW的操作。

    中断处理程序

    Intel的8259A芯片位于主板的南桥芯片中。主片IR0引脚上就是时钟中断,这已经由内部电路实现了。我们只打开时钟中断,屏蔽其他外部设备的中断。中断向量号的前32个(0-31)都被系统的内部中断给占用了,所以我们的时钟中断向量号只能从0x20开始。我们设置0x20为时钟中断的向量号。所以我们要在中断描述符中的第33个中断描述符中加载时钟中断处理程序的选择子和偏移地址。因此我们要定义33个描述符的中断描述符表。由于我们可以屏蔽的中断信号只能是外部设备的可屏蔽中断信号。所以当处理器内部发生内部中断时也会产生0-31的向量号,到时候也会到中断描述符中去找中断处理程序的地址。这些程序都需要我们来写。所以我们写的程序就有中断向量号为0-31这32个内部中断的处理程序和中断向量号为32的时钟中断处理程序。

    宏属于预处理指令。预处理指令被编译器的预处理器支持,属于伪指令。这类指令是在编译前,编译器需要预先处理的指令,预处理器会将这些伪指令展开替换成具体的编译器可以识别的语言符号,在预处理后,其中的预处理指令(伪指令)全部都会不见的。比如#include<>、#define等都是预处理指令,都会被预处理器在预处理阶段都替换成具体所代表的代码或数字。

    宏:Macro。用来代替重复性输入,是一段代码的模板。

    定义单行的宏,汇编中可以用%define 指令实现。

    定义多行的宏,汇编中可以用%macro来实现。

    %macro 宏名字+参数个数
    宏代码体 		;要引用某个参数,需要用“% 数字” 的方式来引用。参数序号从 1 开始
    %endmacro

    总结我们的中断建立的步骤为:

    1,编写中断处理程序,记录每个中断程序的地址

    2,构造出中断描述符表,将中断程序的地址加载进去

    3,设置完成8259A

    4,加载IDTR

    5,将eflags的IF位置1

    我们采取汇编语言和C语言相结合的方式来编写中断处理程序。因为C语言编写程序更方便,我们用C语言来写中断处理程序。发生中断后,首先执行汇编语言写的中断处理程序,然后在调用C语言写的中断处理程序执行。即汇编中的中断程序只是提供了中断处理程序的入口,进入后在转去执行C语言的中断处理程序,C语言的程序才是真正的处理程序。

    在内部中断的前32个中,有些中断类型有错误码,系统就自动压入了错误码,有些中断类型没有错误码,系统没有错误码,为了保持一致性,所以我们要统一压入错误码。

    中断程序的汇编中断处理代码:只是提供一个入口功能,然后转去C中断处理程序。我们要编写33个汇编中断处理程序:0-32是内部中断的处理程序,33是时钟的中断程序。

    [bits 32]
    %define ERROR_CODE nop		 ; 若在相关的异常中cpu已经自动压入了错误码,为保持栈中格式统一,nop是不做操作.
    %define ZERO push 0		 ; 若在相关的异常中cpu没有压入错误码,就手工压入一个0
    
    extern idt_table		 ;idt_table是C中注册的中断处理程序数组,数组元素是C语言的中断处理程序的地址。
    
    section .data
    global intr_entry_table		;定义intr_entry_table为全局变量数组名,每个数组元素为程序的地址
    intr_entry_table:		;因为这个标号是在 .data段内的,所以这个标号指的是 .data里面的地址
    
    %macro VECTOR 2
    section .text
    intr%1entry:		 ; 每个中断处理程序都要压入中断向量号,所以一个中断类型一个中断处理程序,自己知道自己的中断向量号是多少
    
       %2				 ; 中断若有错误码会压在eip后面 
    ; 以下是保存上下文环境
       push ds
       push es
       push fs
       push gs
       pushad			 ; PUSHAD指令压入32位寄存器,其入栈顺序是: EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI
    
       ; 如果是从片上进入的中断,除了往从片上发送EOI外,还要往主片上发送EOI 
       mov al,0x20                   ; 中断结束命令EOI
       out 0xa0,al                   ; 向从片发送
       out 0x20,al                   ; 向主片发送
    
       push %1			 ; 不管idt_table中的目标程序是否需要参数,都一律压入中断向量号,调试时很方便
       call [idt_table + %1*4]       ; 调用idt_table中的C版本中断处理函数
       jmp intr_exit
    
    section .data
       dd    intr%1entry	 ; 存储各个中断入口程序的地址,形成intr_entry_table数组
    %endmacro
    
    section .text
    global intr_exit
    intr_exit:	     
    ; 以下是恢复上下文环境
       add esp, 4			   ; 跳过中断号
       popad
       pop gs
       pop fs
       pop es
       pop ds
       add esp, 4			   ; 跳过error_code
       iretd
    ;0-31个中断向量号是内部中断,有些有错误码,有些没有。32中断向量号为时钟中断
    VECTOR 0x00,ZERO
    VECTOR 0x01,ZERO
    VECTOR 0x02,ZERO
    VECTOR 0x03,ZERO 
    VECTOR 0x04,ZERO
    VECTOR 0x05,ZERO
    VECTOR 0x06,ZERO
    VECTOR 0x07,ZERO 
    VECTOR 0x08,ERROR_CODE
    VECTOR 0x09,ZERO
    VECTOR 0x0a,ERROR_CODE
    VECTOR 0x0b,ERROR_CODE 
    VECTOR 0x0c,ZERO
    VECTOR 0x0d,ERROR_CODE
    VECTOR 0x0e,ERROR_CODE
    VECTOR 0x0f,ZERO 
    VECTOR 0x10,ZERO
    VECTOR 0x11,ERROR_CODE
    VECTOR 0x12,ZERO
    VECTOR 0x13,ZERO 
    VECTOR 0x14,ZERO
    VECTOR 0x15,ZERO
    VECTOR 0x16,ZERO
    VECTOR 0x17,ZERO 
    VECTOR 0x18,ERROR_CODE
    VECTOR 0x19,ZERO
    VECTOR 0x1a,ERROR_CODE
    VECTOR 0x1b,ERROR_CODE 
    VECTOR 0x1c,ZERO
    VECTOR 0x1d,ERROR_CODE
    VECTOR 0x1e,ERROR_CODE
    VECTOR 0x1f,ZERO 
    VECTOR 0x20,ZERO
    

    编译器编译程序时会按照每个节的属性,将相同属性的节放在一起的。所以我们编译器会分别将代码段和数据段放在一起的。因为预处理器会在编译前提前将代码展开,所以最后 .data放在一起,可以构成一个数组,数组元素是代码段的地址。 idt_table的数组元素是每一个C处理程序的地址。所以 call  [idt_table+4*num]==call 处理函数地址。

    eflags、cs、eip、error_code是CPU自己压入栈的,我们不需要管,我们要负责的是其他入栈参数。注意 iret 时候要跳过error_code 使esp 指向eip。


    中断的C语言中断处理程序:里面是真正的中断处理程序。这里我们只设置一个通用的,33个中断都是这一个中断函数。将中断函数的地址循环33编放入数组中即可。

    intr_handler idt_table[IDT_DESC_CNT];  //定义C语言中断处理程序数组
    /* 通用的中断处理函数,一般用在异常出现时的处理 */
    //功能是:出现异常时显示中断向量号;
    static void general_intr_handler(uint8_t vec_nr) 
    {
    if (vec_nr == 0x27 || vec_nr == 0x2f) {	// 0x2f是从片8259A上的最后一个irq引脚,保留
    return;		//IRQ7和IRQ15会产生伪中断(spurious interrupt),无须处理。
    }
    put_str("int vector: 0x");
    put_int(vec_nr);
    put_char('\n');
    }
    static void exception_init(void)
    {       // 完成一般中断处理函数注册注册
    int i;
    for (i = 0; i < IDT_DESC_CNT; i++)
    {
    idt_table[i] = general_intr_handler;      // 默认为general_intr_handler。
    }
    }
    

    中断描述符表IDT的建立

    中断门描述符结构体,要按照中断门描述符中的顺序来,低地址的先定义,则先储存在内存中

    struct gate_desc
    {
       uint16_t    func_offset_low_word; //要先定义,则会先储存在内存中
       uint16_t    selector;
       uint8_t     dcount;   //此项为双字计数字段,是门描述符中的第4字节。此项固定值,不用考虑
       uint8_t     attribute;
       uint16_t    func_offset_high_word;  
    };
    
    //定义中断描述符表,有33个门描述符,0-31中断向量号被系统占用,我们从32(0x20)开始定义时钟中断
    static struct gate_desc idt[IDT_DESC_CNT];
    
    //声明出中断程序入口地址数组,有33个地址,0-31的地址属于内部中断程序地址,0x20的地址属于时钟中断程序地址
    //这些地址在汇编的中断程序中已经得出
    extern intr_handler intr_entry_table[IDT_DESC_CNT];  //汇编中断处理地址的中的全局数组,元素是每个中断程序的入口地址
    
    //声明中断描述符构造函数
    static void make_idt_desc(struct gate_desc* p_gdecs,uint8_t attr,intr_handler function);
    //门描述符构造函数
    static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function) 
    { 
       p_gdesc->func_offset_low_word = (uint32_t)function & 0x0000FFFF;
       p_gdesc->selector = SELECTOR_K_CODE;
       p_gdesc->dcount = 0;
       p_gdesc->attribute = attr;
       p_gdesc->func_offset_high_word = ((uint32_t)function & 0xFFFF0000) >> 16;
    }
    
    //构造出33个门描述符
    static void idt_desc_init(void) 
    {
       int i;
       for (i = 0; i < IDT_DESC_CNT; i++) 
    	{
          make_idt_desc(&idt[i], IDT_DESC_ATTR_DPL0, intr_entry_table[i]); 
       }
       put_str("   idt_desc_init done\n");
    }
    

    8259A设置程序

    设置头文件io.h。通过这个头文件来读写端口。

    static函数的作用:普通的函数名是全局变量,在一个文件中定义后,在所有的文件中都可以使用。static函数,在一个文件中定义后,只能在那个文件中使用,其他文件不能用。

    inline函数的作用:当主调函数调用被调函数时候,主调函数要压栈,call,然后csip转过去执行等等,被调函数也要备份等等,速度很慢。inline函数是当主调函数调用被调函数时,直接在调用出原封不动的将被调函数的函数体展开,这样就不用call了,这就不属于函数调用了,csip的值也不用变,依次执行即可。

    我们将static函数放在头文件中的目的在于:因为这是对端口进行操作,所以需要的条件就是一定要快。static函数放在头文件中,那么所有包含该头文件的文件都有这个static函数,static函数执行起来比普通函数要快很多,因为静态函数会被自动分配在一个一直使用的静态存储区,直到退出应用程序才清空这个区域,而普通的函数还生成了一些其他信息来保证安全,所以static函数速度快很多。

    内存循环复制三剑客:cld、(ES:ESIES:EDI)、repmovs[dw]

    端口循环读写三剑客:cld、(es:edi)、rep ins[dw]      cld、(es:esi)、rep outs[dw]

    两个指令都是在执行 rep前先读取ecx的值与0比较,循环执行后ecx的值减1,然后esiedi自动加上相应的字节。

    1,首先我们编写端口读写函数,不管什么端口读写,调用即可。

    #ifndef _LIB_IO_H
    #define _LIB_IO_H
    //向端口port写入一个字节.N为立即数约束:表示0-255个端口号
    static inline void outb(unit16_t port, unit_t data)
    {
    	asm volatile("outb %b0,&w1"::"a"(data),"Nd"(port));//b和w来限制寄存器大小
    }
    
    //将addr处起始的 word_cnt 个字写入端口port。
    //执行rep指令前先拿ecx与0比较,不相等则可以执行,执行完后edi加一个字,ecx减1。在执行rep之前再比较
    static inline void outsw(uint16_t port,const void* addr,unit32_t word_cnt)
    {
    	asm volatile("cld;rep outsw": "+S"(addr),"+c"(word_cnt): "d"(port));
    }
    
    //将从端口port读入一个字节返回
    static inline uint8_t inb(uint16_t port)
    {
    	uint8_t data;
    	asm volatile("inb %w1,%b0":"=a"(data):"Nd"(port));
    	return data;
    }
    
    //将从端口port读入的word_cnt个字写入addr
    //
    static inline void insw(uint16_t port,void* addr,uint32_t word_cnt)
    {
    	asm volatile("cld;rep insw":"+D"(addr),"+c"(word_cnt):"d"(port):"memory");
    }
    #endif
    

    2,当我们设置8259A时,我们调用端口读写函数即可

    /* 初始化可编程中断控制器8259A */
    static void pic_init(void) {
    
       /* 初始化主片 */
       outb (PIC_M_CTRL, 0x11);   // ICW1: 边沿触发,级联8259, 需要ICW4.
       outb (PIC_M_DATA, 0x20);   // ICW2: 起始中断向量号为0x20,也就是IR[0-7] 为 0x20 ~ 0x27.
       outb (PIC_M_DATA, 0x04);   // ICW3: IR2接从片. 
       outb (PIC_M_DATA, 0x01);   // ICW4: 8086模式, 正常EOI
    
       /* 初始化从片 */
       outb (PIC_S_CTRL, 0x11);	// ICW1: 边沿触发,级联8259, 需要ICW4.
       outb (PIC_S_DATA, 0x28);	// ICW2: 起始中断向量号为0x28,也就是IR[8-15] 为 0x28 ~ 0x2F.
       outb (PIC_S_DATA, 0x02);	// ICW3: 设置从片连接到主片的IR2引脚
       outb (PIC_S_DATA, 0x01);	// ICW4: 8086模式, 正常EOI
    
       /* 只打开主片上IR0,也就是目前只接受时钟产生的中断 */
       outb (PIC_M_DATA, 0xfe);
       outb (PIC_S_DATA, 0xff);
    
       put_str("pic_init done\n");
    }


    加载IDTR

    根据IDTR的结构,采用内联汇编的形式加载进去。

    数组名便是指针,所以,

     uint64_t idt_operand = ((sizeof(idt) - 1) | ((uint64_t)(uint32_t)idt << 16));//将idt的基地址左移16位,
     asm volatile("lidt %0" : : "m" (idt_operand));
    

    将eflags的IF位置1

    asm volatile("sti");

    到此为止,时钟中断设置完成,在主函数中依次初始化idt、pic(8259A)、idtr,然后开中断即可。


    可编程计数器/定时器8253

    计算机时钟分为两类:内部时钟和外部时钟

    内部时钟:是内部元件,位于主板上,由晶体振荡器产生,其频率经过分频之后就是主板的外频,外频乘以某个倍数就是主频。内部定时无法改变。

    外部时钟:是处理器与外部设备之间通信采用的一种时序

    内部时钟和外部时钟是两套独立运行的定时体系。对于外部定时,我们有两种实现方式:一种软件实现,一种硬件实现为定时器。

    计时器的功能就是定时发出信号,当到达所计数的时间,计数器可以发出一个中断信号,处理器转去执行相应的中断处理程序。计数器内部有计算脉冲信号,每一个时钟脉冲信号到来时都会修改计数值。

    8253用的是 倒计时 的方式。比如为了放在DRAM(内存卡)数据丢失,每隔一段时间就要进行充电刷新,就是利用这个向CPU发出中断去刷新的。我们设置8253来调整时钟中断发生的频率。

     每个计数器有3个引脚

    CLK:时钟输入信号,即计数器自己的时钟频率,连接到8253的CLK的脉冲频率为 2MHZ。

    GATE:门控输入信号

    OUT:计数器输出信号,当计数值为0时,用于通知处理器或者某个设备:定时完成

    计数开始之前的计数初值保存在计数初值寄存器中,减法计数器将此值载入后,CLK 收到一个脉冲信号,减法计数器就将计数值减1,同时将当前值保存到输出锁存器中。当计数值为0时,OUT引脚输出中断信号或者启动某个设备工作。

    因此我们使用计数器0来产生时钟信号,这个时钟是连接饿到 IRQ0 引脚上的那个时钟,即计数器0决定了定时中断信号的发生频率。因此计数器0的计时到期后就会发出中断时钟中断信号,中断代理8259A就会感知引脚 IRQ0 有中断信号到来。

    我们通过控制字寄存器来设置计数器0的工作方式。我们通过控制字寄存器选择用哪个计数器,指定该计数器的控制模式,再用该计数器写入计数初值就行。

    默认情况下,三个计数器的工作频率位 1.19318MHz,即一秒内有 1193180 次脉冲信号,一秒内会减1193189次1,计数器0的默认值是0,即65536,那么一秒内的输出信号次数大约为 : 1193180/65536=18.206,所以时钟中断信号的频率为18.206Hz,大约55毫秒发生一次中断。这就是我们默认情况下的时钟中断频率了。

    我们的目的是为了使中断发生的快一点。默认频率为18.206Hz,我们调整为100Hz,即一秒钟100次中断。我们通过8253,通过控制字指定使用计数器0,工作方式选择方式2(比率发生器),并为附上合适的初值 1193180/100=11932。

    static void frequency_set(uint8_t counter_port,  uint8_t counter_no, uint8_t rwl, uint8_t counter_mode,  uint16_t counter_value) 
    {
       /* 往控制字寄存器端口0x43中写入控制字 */
       outb(PIT_CONTROL_PORT, (uint8_t)(counter_no << 6 | rwl << 4 | counter_mode << 1));
       /* 先写入counter_value的低8位 */
       outb(counter_port, (uint8_t)counter_value);
       /* 再写入counter_value的高8位 */
       outb(counter_port, (uint8_t)counter_value >> 8);
    }

    获取当前中断状态并开关中断

    可以定义枚举来表示中断状态

    enum intr_status
    {
       INTR_OFF;
       INTR_ON;
    }

    获取当前eflags的中断状态状态

    我们定义宏函数,首先先看一下宏函数:

    宏函数实现:

    #define MAX( a, b) ( (a) > (b) (a) : (b) )
    

    普通函数来实现:

    int max( int a, int b)
    {
    return (a > b? a : b);
    }
    

    很显然,我们不会选择用函数来完成这个任务,原因有两个:

    1,普通的函数会带来额外的开销:将实参压栈、压入地址、弹出地址、释放堆栈等等。这种开销不仅会降低代码效率,而且代码量也会大大增加,而使用宏定义则在代码调用处直接展开,省去了很多工作;

    2,普通函数的参数必须被声明为一种特定的类型,所以它只能在类型合适的表达式上使用,我们若要改变类型,只能另外写一个函数。反之,宏定义可以用于整形、长整形、单浮点型、双浮点型以及其他任何可以用“>”操作符比较值大小的类型,也就是说,宏是与类型无关的。

    不过和使用函数相比,使用宏的不利之处在于每次使用宏时,一份宏定义代码的拷贝都会插入到程序中。除非宏非常短,否则使用宏会大幅度增加程序的长度。

    //返回eflags寄存器到变量中
    #define GET_EFLAGS(EFLAG_VAR) asm volatile("pushfl;popl %0":"=g"(EFLAG_VAR))
    //获取当前状态
    enum intr_status intr_get_status()
    {
     int eflags=0;
     GET_EFLAGS(eflags);
     return (EFLAGS_IF & eflags) ? INTR_ON:INTR_OFF;
    }
    //开中断并返回开中断前的状态
    enum intr_status intr_enable()
    {
    	enum intr_status old_status=INTR_ON;
    	if(old_status==intr_get_status())
    		return old_status;
    	old_status=INTR_OFF;
    	asm volatile("sti");
    	return old_status;
    }
    
    //关闭中断并返回中断前的状态
    enum intr_status intr_enable()
    {
    	enum intr_status old_status=INTR_OFF;
    	if(old_status==intr_get_status())
    		return old_status;
    	old_status=INTR_ON;
    	asm volatile("cti");
    	return old_status;
    }



    展开全文
  • 关注+星标公众号,不错过精彩内容转自 | 痞子衡嵌入式今天给大家分享的是Cortex-M系统中断延迟及其测量方法。在嵌入式领域里,实时性是个经常被我们挂在嘴边的概念,这里的实时性主要强调得...

    关注+星标公众,不错过精彩内容

    b17d5bd021e4fbc3b16d68b6ffdad03e.gif

    转自 | 痞子衡嵌入式

    今天给大家分享的是Cortex-M系统中断延迟及其测量方法

    在嵌入式领域里,实时性是个经常被我们挂在嘴边的概念,这里的实时性主要强调得是当外界事件发生时,系统是否能在规定的时间范围内予以响应处理,这个时间阈值越小,系统的实时性就越高。当然关于这个实时性,也有软硬之分,硬实时要求的是设定的时间阈值内必须完成响应,而软实时则仅需根据任务的优先级尽可能快地完成响应即可。

    无论是 RTOS 环境还是裸机环境下,系统最原始的实时性保障其实来自于 MCU 内核的中断响应能力,关于中断响应能力有一个重要指标叫中断延迟时间,今天我们就来聊一聊 Cortex-M 内核的中断延迟及其测量方法:

    一、什么是系统中断延迟?

    所谓中断延迟,即从中断请求 IRQ 信号置起开始到内核进入执行该中断 ISR 第一条指令时的间隔,如下图所示, 箭头范围内的 11 个周期就是中断延迟时间。关于这个概念,ARM 公司专家 Joseph Yiu 的一篇博客 《Cortex-M内核系统中断延迟入门指南》 介绍得很详细。

    84369eb9bd60ac4cb3c3ddb9e4dfd653.png

    为什么会有中断延迟?其实这是无法避免的,当内核在执行 main thread 代码时,来了中断事件,NVIC 里对应 IRQ 信号被置起,内核接到 NVIC 通知后压栈保存现场(以便中断 ISR 处理完成时回到被打断的 main thread 地方),然后再从中断向量表里取出对应中断 ISR 来执行,这一系列动作是需要时间的。

    Cortex-M 家族发展至今,已有 M0/M0+/M1/M3/M4/M7/M23/M33/M35P/M55 等多款内核,这些内核的中断延迟时间不一,如下表所示。注意表中的数值单位是内核的时钟周期,并且是在零等待内存系统条件下结果(即代码链接在零等待内存里)。

    • Note1: 一般来说 MCU 内部与内核同频的 SRAM 是标准的零等待内存。

    • Note2: 许多运行频率超过 100MHz 的微控制器会搭配非常慢的 Flash 存储器(例如 30 - 50MHz),虽然可以使用 Flash 访问加速硬件来提高性能,但中断延迟仍然受到 Flash 存储系统等待状态的影响。

    0f8ed2ed237ed0c4f5f0e075f66d7c92.png

    二、如何测量系统中断延迟?

    测量 Cortex-M 的中断延迟方法有很多,任何一个带中断的 MCU 外设模块都可以用来测中断延迟,Cortex-M 中断延迟时间跟触发中断的具体外设模块类型基本上是无关的(最多受一点该外设中断信号同步周期的影响)。

    利用 GPIO 模块来测量 Cortex-M 的中断延迟是最简单的方法,选择两个 GPIO,一个配置为输入(GPIO_IN),另一个配置为上拉输出(GPIO_OUT,初态设为高),开启 GPIO_IN 的边沿中断(比如下降沿),在 GPIO_IN 边沿中断 ISR 里翻转两次 GPIO_OUT,然后用示波器测量两个 GPIO 信号边沿间隔得出中断延迟时间。

    uint32_t s_pin_low  = 0x0;
    uint32_t s_pin_high = 0x1;
    
    void GPIO_IN_IRQHandler(void)
    {
        GPIO_OUT->DR = s_pin_low;
        GPIO_OUT->DR = s_pin_high;
    
        ClearInterruptFlag(GPIO_IN);
        __DSB();
    }

    需要注意的是在如上 GPIO_IN_IRQHandler 函数伪代码里,我们对 GPIO_OUT 做了两次翻转,这是有必要的。我们随便选择一个 CM7 芯片去实际编译(IAR环境下,无优化)可以看到如下汇编代码,每一次翻转实际上由四条指令组成,我们作为计时基准的 GPIO_OUT 第一个边沿其实是 ISR 里执行了第一句翻转代码后的时刻,而中断延迟是不包含第一句翻转代码执行时间的。因为指令流水线优化等复杂情况,我们就不从理论上计算四条指令实际消耗时钟周期数,直接再翻转一次 GPIO_OUT,测量 GPIO_OUT 低电平时间即认为是这四条指令执行时间。

    4c295ca5fe6d9e12026808a8731539ae.png

    因此最终中断延迟时间 td = t1 - t2,其中 t1、t2 是我们可以测量出来的时间。此外,td 时间内包含了 tx,这个 tx 是 I/O 跳变信号到芯片内部 IRQ 置起的时间,这个时间是芯片系统所需的同步时间,因芯片设计而异,本应不被计算在系统中断延迟内,但因为这个时间无法测量,故而就放在中断延迟里了,也算是一点小小误差吧。

    下一篇我们将在实际 Cortex-M 芯片上用这种方法去实测中断延迟,敬请期待。

    160f82178d3853f9ecb6a90200c6e5bd.png

    至此,Cortex-M系统中断延迟及其测量方法便介绍完毕了,欢迎大家转发分享。

    ------------ END ------------

    723967deb95690b772612294da3955be.gif

    ●精选 | ST工具、下载编程工具

    ●精选 | 嵌入式软件设计与开发

    ●精选 | 软件工具、 编译器、 编辑器

    迎关注我的公众号回复“加群”按规则加入技术交流群,回复“1024”查看更多内容。

    欢迎关注我的视频号:

    d48cd13b9cf8396c948f0c8cff120d01.png

    点击“阅读原文”查看更多分享,欢迎点分享、收藏、点赞、在看。

    展开全文
  • ARM希望把中断处理函数也做成C函数的形式,一般C函数的处理过程类似了。 进入中断前,首先要把(R0-R3,R12,LR,PSR)保存起来,然后在中断结束后恢复它们。 这一切都是通过硬件完成的。 但是,中断的返回地址并没有像...
  • ARM Cortex-M3中断跳转过程

    千次阅读 2015-06-25 15:49:37
    在学习CM3的时候,仔细学习了CM3的中断跳转过程,发现嵌入式的MCU在这一块基本上是一样的,当然不同架构的MCU也有自己的特性。 我来介绍下CM3的中断跳转过程,首先假设中断发生,CM3内核开始...1、压栈。从这一点来讲几
  • Cortex-M3在进入异常服务例程时,自动压栈了 R0-R3, R12, LR, PSR 和PC,并且在返回时自动弹出它们,这多清爽!既加速了中断的响应,也再不需要汇编语言代码了 NVIC支持对每一路中断设置不同的优先级,使得中断管理...
  • RISC-V学习笔记【中断和异常】

    千次阅读 2021-04-05 20:15:43
    RISC-V架构的中断与异常 一般来说由处理器内部的事件或程序执行中的事件引起的程序跳转称为异常;一般的由处理器外部因素引起的程序跳转称为中断 广义上来说中断和异常都被处理器视为异常,一般将其分为同步异常和...
  • 上图左边是进入中断前的寄存器值,右边是进入中断后的寄存器值,通过SP值得前后对比,可以看出在进入中断后被压了(0x20000330-0x20000310=0x20=32)32个字节数据(因为stm32栈是满递减),这32个字节数据分别是8个...
  • Cortex-M系列中断和异常(四)

    千次阅读 2019-11-06 16:39:53
    文章目录1 中断及异常的注意事项1.1 中断及异常的优先级配置1.2 中断与栈空间的关系...RETURN3 进入异常及返回异常流程3.1 异常进入及压栈3.2 异常返回及出栈4 中断等待和异常处理优化4.1 中断等待4.2 多周期指令执...
  • Cortex-M0(3)---ARM Cortex-M0 异常与中断

    千次阅读 2018-09-12 11:17:46
    ARM Cortex-M0 异常与中断 异常类型及编号  Cortex-M0的每个异常源都有一个单独的编号:  1~15内部系统异常:Reset(1), NMI(2), H/W Error(3), SVC(11), PndSV(14), SysTick(15)其他编号未用;  16~47外部...
  • STM32的中断处理1

    千次阅读 2013-10-19 23:11:18
    1、MSP和PSP 1)control寄存器 CONTROL[1] 堆栈指针选择  ...0=选择主堆栈指针MSP(复位后缺省值)  ...4、咬尾中断:后到中断无法抢占时,减少压栈出栈处理。
  • DSP28335中断系统(一)

    千次阅读 2020-02-25 10:46:25
    1、什么是中断? 举个例子,你在吃饭的时候,突然觉得口渴,此时你就会去喝水,等喝完水你再继续吃饭。在这个例子中,吃饭为主程序运行,大脑觉着口渴就是发中断申请,喝水就是中断子程序。在DSP的中断概念也是一样...
  • 压栈顺序是怎样的?

    2013-11-22 17:45:43
    (1)main在调用函数的时候,函数f中的参数是先压l后压g(从左往右),而函数f的形参则是 从右往左 压栈的。 (2)压栈顺序和参数计算顺序不是一回事。所以看参数值并不能确定函数参数的压栈顺序,从地址顺序观察更...
  • Cortex-M3中断的具体行为

    千次阅读 2017-11-28 11:44:33
    中断响应序列 Cortex-M3的中断响应序列包括:入栈,取向量,更新寄存器; 1. 入栈 Cortex-M3的中断响应会自动保存现场:依次将xPSR,PC,LR,R12,R0-R3压入堆栈;响应异常时正在使用哪个堆栈指针,则压入哪个...
  • 中断和异常

    2021-04-25 21:19:04
    然后,cpu会自动保存rsp,ss,cs,rip寄存器值到新栈,在长模式中,不论是否发生CPL变换,都会将中断/异常发生时的rip,rsp,ss,cs压栈,同时如果异常有错误码也会自动把错误码压栈,执行完后,栈环境如下图: ...
  • Cortex-M3 中断的具体行为

    千次阅读 2016-05-19 00:08:00
    ARM Cortex-M3学习记录:Crotex-M3中断响应与中断返回序列
  • 学习笔记——ARM Cortex-M0 异常与中断

    千次阅读 2015-07-05 10:37:51
    1) 压栈并更新栈指针(8个registers被压栈:R0~R3, R12, R14/LR, R15/PC, xPSR); 2) 取出异常向量写入PC中; 3) 3个寄存器更新(LR<–EXC_RETURN, IPSR<–异常编号, NVIC<– 对应的中断控制和状态) 执行...
  • linux在x86上的中断处理过程(详细)

    千次阅读 2012-04-04 21:17:30
    Linux在x86上的中断处理过程 一:引言 在Intel的文档中,把中断分为两种。一种是异常,也叫同步同断。一种称之为中断,也叫异常中断。同步中断指的是由CPU控制单元产生,之所以称之为同步,是因为只有一条指令执行完毕...
  • 如果一个中断产生时任务正在用户代码中执行,那么该中断会引起CPU特权级从3到0的变化,此时CPU就会运行用户态堆栈到内核态堆栈的切换操作。CPU会从当前任务的任务状态段TSS中取得新堆栈的段选择符和偏移量。因为中断...
  • STM32的中断处理2

    2013-10-27 14:26:10
    1、中断嵌套:第一个中断使用PSP或MSP压栈,嵌套中断使用MSP压栈。 2、抢占场景,替换原来的中断流程。 咬尾的判决处理:
  • 压栈/出栈、跳转指令、LR、PC

    千次阅读 2020-04-24 15:41:05
    一、压栈和出栈 通常会在 A 函数中调用 B 函数,当 B 函数执行完以后再回到 A 函数继续执行。因此必须在跳到 B 函数之前将当前处理器状态保存起来(保存 R0~R15 寄存器值),当 B 函数执行完成以后再用前面保存的...
  •  下图为S12内核CPU中断压栈过程,压栈CCR寄存器,关闭全局中断的处理,箭头所指红圈中,硬件置位 I-BIT(禁止I-bit外设中断)、S-BIT(禁止STOP低功耗指令)和X-BIT(禁止XIRQ中断)(注: 该过程是用户不可控且不可中断的...
  • 中断与异常1.1 异常的处理流程1.1.1 接受异常请求1.1.2 异常进入的流程1.1.3 异常处理流程1.1.4 异常返回流程1.2 中断控制用的NVIC寄存器1.2.1 中断使能/失能寄存器1.2.2 中断挂起寄存器/清除挂起寄存器1.2.3 活跃...
  • 最重要的是本篇文章没有汇编代码,只讲原理~~ 今天的内容比较简单,学习一下中断的原理-包括硬中断和软中断。主要理解以下内容: 硬中断的工作原理 软中断的工作原理 中断向量表 1、硬中断中断...
  • 进入中断和退出中断的过程

    千次阅读 2019-09-04 16:46:48
    1 进入中断 a)将PC+8或PC+4()的值存放到LR_异常 寄存器中 b)将CPSR保存到SPSR_异常 寄存器中 c)修改CPSR的[4:0]位,将其修改为对应的中断 d)跳到相应的中断向量表(硬件完成) 2 退出中断 a)将LR_异常 寄存器的...
  • 最后一章的内容是栈与中断。先介绍栈的机制及其实现,然后介绍通过栈来实现中断。 栈与LC-3中的栈 栈是先进后出的数据结构,学过数据结构这门课的,懂的都懂 压栈 使用栈顶指针top,top始终指向最新被压栈的元素。在...
  • 文章目录第八章 中断系统8.1 中断的基本概念8.1.1 中断概念的引入及描述中断方式示意(以输入中断为例)**中断**的定义8.1.2 中断源及中断分类中断的分类8.1.3 中断类型码中断类型码中断向量中断向量表中断向量表的...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 11,604
精华内容 4,641
关键字:

中断压栈