首页 正文

19555r15和19560r15通用吗

单片机的程序计数器PC用来干什么?

程序计数器PC决定着代码执行的地址。

以广泛应用的Cortex-M3为例:

Cortex-M3拥有通用寄存器(R0~R15)和特殊功能寄存器。其中R0~R7是低组寄存器,R8~R12是高组寄存器。绝大多数16位指令只能使用低组寄存器,32位Thumb-2指令可以访问所有通用寄存器。R13作为堆栈指针SP。R14是连接寄存器LR, R15是程序计数器PC。特殊功能寄存器有预定义的功能,必须通过专用指令进行访问。

程序计数寄存器又称程序计数器PC(Program Counter),在ARM汇编程序中R15和PC写法可以互换,用以指明指向当前的指令地址。如果修改它的值,即向PC中写数据,就会引起一次程序跳转,就能改变程序的执行流,但此时不更新LR寄存器。

由于ARM处理器发展的历史原因,PC的第0位(LSB)用于指示ARM/Thumb状态。0表示当前指令环境处于ARM状态,而1则表示当前指令环境处于Thumb状态。Cortex-M3中的指令是隶属于Thumb-2指令集,且至少是半字对齐的,所以PC的LSB总是读回0。然而在编写分支指令时,无论是直接写PC的值还是使用分支指令,都必须保证加载到PC的数值是奇数(即LSB=1),用以表明当前指令在Thumb状态下执行,但微处理器在执行分支指令时,必须屏蔽LSB位的“1”,才能保证程序的正确运行。倘若写了0,则视为企图转入ARM模式,Cortex-M3将产生一个Fault异常。

因为Cortex-M3内部使用了指令流水线,读取PC内容时返回的值是当前指令的地址+4。比如说:

\u2028 0x1000:MOV R0, PC ; R0=0x1004

表明当前指令地址为0x1000,此时读取PC内容到R0寄存器。执行过程中PC的值为0x1004=0x1000+4。

上图与回答无关

嵌入式软件工程师面试题目整理(一)

嵌入式软件工程师面试题目整理(一)Arm有多少32位寄存器?Arm2440和6410有什么区别CPU,MPU,MCU,SOC,SOPC联系与差别上拉&下拉&高阻态串口协议讲一讲RS232和RS485通讯接口有什么区别IIC时序图画一下,IIC有哪些状态,给一个字节,将它发送出去。IIC有什么注意事项?有没有用I/O模拟IIC,如果有需要注意什么?为什么2440的内存起始地址是3后面7个0呢?为什么6410的内存起始地址是5后面7个0呢?内存管理有什么看法?(MMU)锁有哪些?有什么注意事项信号量和自旋锁区别中断能不能睡眠,为什么?下半部能不能睡眠?上下文有哪些?怎么理解?死锁产生的原因及四个必要条件触摸屏中断做了什么,LCD中断做了什么?什么是交叉编译?为什么需要交叉编译?为什么还要主机编译简述linux系统启动过程Linux设备中字符设备和块设备有什么主要区别?分别举例。同步通信和异步通信Uart和IIC和SPI的区别(提示:关于异步和同步,电子器件上的)?用串口发送十个字节就丢失一个两个你会怎样检查;发送的时候对方设备不响应你该怎么办内核链表为什么具有通用性?分配内存哪些函数?kmalloc有两个参数,各个作用是什么?有哪些锁,各自的效率问题?自选锁怎样实现的?Linux内核硬中断 / 软中断的原理和实现




嵌入式软件工程师面试题目整理(一) Arm有多少32位寄存器?

ARM处理器共有37个寄存器。它包含31个通用寄存器和6个状态寄存器。


通用寄存器的分类 a.未备份寄存器,包括R0-R7 对每个未备份寄存器来说,在所有的模式下都是指同一个物理寄存器(例如:Usr下的R0与FIQ下的R0是同一个寄存器)。在异常程序中断造成模式切换时,由于不同模式使用的是相同的物理寄存器。这可能导致数据遭到破坏。未备份寄存器没有被系统作为别的用途,任何场合均可采用未备份寄存器。

b.备份寄存器,包括R8-R14 对于备份寄存器R8-R12来说,除FIQ模式下其它模式均使用相同的物理寄存器。在FIQ模式下R8_fiq,R9_fiq,R10_fiq,R11_fiq,R12_fiq。它有自己的物理寄存器。

对于R13和R14寄存器每种模式都有自己的物理寄存器(System与Usr的寄存器相同)。当异常中断发生时,系统使用相应模式下的物理寄存器,从而可以避免数据遭到破坏。(用户模式和系统模式非异常中断,所以共用R13 R14) R13也称为SP堆栈指针。 R14也称为LR寄存器(存放当前子程序的返回地址)

c.程序计数器,PC PC寄存器存储指令地址,由于ARM采用流水机制执行指令,故PC寄存器总是存储下一条指令的地址。

由于ARM是按照字对齐故PC被读取后的值的bit[1:0]总是0b00(thumb的bit[0]是0b0)。

CPSR(当前程序状态寄存器)可以在任何处理器模式下被访问 SPSR(备份程序状态寄存器)。当特定的异常中断发生时,这个寄存器用于存放当前程序状态寄存器的内容。在异常中断程序退出时,可以用SPSR中保存的值来恢复CPSR。

由于用户模式和系统模式不是异常中断模式,所以它们没有SPSR。(用户模式和系统模式的CPSR也是共用的)当在用户模式或系统模式中访问SPSR,将会产生不可预知的结果。

ARM处理器共有37个寄存器。其中包括:31个通用寄存器,包括程序计数器(PC)在内。这些寄存器都是32位寄存器。以及6个32位状态寄存器。但目前只使用了其中12位。ARM处理器共有7种不同的处理器模式,在每一种处理器模式中有一组相应的寄存器组。任意时刻(也就是任意的处理器模式下),可见的寄存器包括15个通用寄存器(R0~R14)、一个或两个状态寄存器及程序计数器(PC)。在所有的寄存器中,有些是各模式共用的同一个物理寄存器;有一些寄存器是各模式自己拥有的独立的物理寄存器。表1列出了各处理器模式下可见的寄存器情况。

表1 各种处理器模式下的寄存器

用户模式系统模式特权模式中止模式未定义指令模式外部中断模式快速中断模式R0R0R0R0R0R0R0R1R1R1R1R1R1R1R2R2R2R2R2R2R2R3R3R3R3R3R3R3R4R4R4R4R4R4R4R5R5R5R5R5R5R5R6R6R6R6R6R6R6R8R8R8R8R8R8R8_fiqR9R9R9R9R9R9R9_fiqR10R10R10R10R10R10R10_fiqR11R11R11R11R11R11R11_fiqR12R12R12R12R12R12R12_fiqR13R13R13_svcR13_abtR13_undR13_inqR13_fiqR14R14R14_svcR14_abtR14_undR14_inqR14_fiqPCPCPCPCPCPCPCCPSRCPSRCPSR
SPSR_svcCPSR
SPSR_abtCPSR
SPSR_undCPSR
SPSR_inqCPSR
SPSR_fiq



通用寄存器
通用寄存器可以分为下面3类:未备份寄存器(The unbanked registers),包括R0~R7。备份寄存器(The banked registers),包括R8~R14。程序计数器PC,即R15。
未备份寄存器
未备份寄存器包括R0~R7。对于每一个未备份寄存器来说,在所有的处理器模式下指的都是同一个物理寄存器。在异常中断造成处理器模式切换时,由于不同的处理器模式使用相同的物理寄存器,可能造成寄存器中数据被破坏。未备份寄存器没有被系统用于特别的用途,任何可采用通用寄存器的应用场合都可以使用未备份寄存器。
备份寄存器
对于备份寄存器R8~R12来说,每个寄存器对应两个不同的物理寄存器。例如,当使用快速中断模式下的寄存器时,寄存器R8和寄存器R9分别记作R8_fiq、R9_fiq;当使用用户模式下的寄存器时,寄存器R8和寄存器R9分别记作R8_usr、R9_usr等。在这两种情况下使用的是不同的物理寄存器。系统没有将这几个寄存器用于任何的特殊用途,但是当中断处理非常简单,仅仅使用R8~R14寄存器时,FIQ处理程序可以不必执行保存和恢复中断现场的指令,从而可以使中断处理过程非常迅速。对于备份寄存器R13和R14来说,每个寄存器对应6个不同的物理寄存器,其中的一个是用户模式和系统模式共用的;另外的5个对应于其他5种处理器模式。采用记号R13_<mode>来区分各个物理寄存器:
其中,<mode>可以是下面几种模式之一:usr、svc、abt、und、irq及fiq。
寄存器R13在ARM中常用作栈指针。在ARM指令集中,这只是一种习惯的用法,并没有任何指令强制性的使用R13作为栈指针,用户也可以使用其他的寄存器作为栈指
针;而在Thumb指令集中,有一些指令强制性地使用R13作为栈指针。
每一种异常模式拥有自己的物理的R13。应用程序初始化该R13,使其指向该异常模式专用的栈地址。当进入异常模式时,可以将需要使用的寄存器保存在R13所指的栈中;当退出异常处理程序时,将保存在R13所指的栈中的寄存器值弹出。这样就使异常处理程序不会破坏被其中断程序的运行现场。
寄存器R14又被称为连接寄存器(Link Register,LR),在ARM体系中具有下面两种特殊的作用:每一种处理器模式自己的物理R14中存放在当前子程序的返回地址。当通过BL或BLX指令调用子程序时,R14被设置成该子程序的返回地址。在子程序中,当把R14的值复制到程序计数器PC中时,子程序即返回。
当异常中断发生时,该异常模式特定的物理R14被设置成该异常模式将要返回的地址,对于有些异常模式,R14的值可能与将返回的地址有一个常数的偏移量。具体的返回方式与上面的子程序返回方式基本相同。
R14寄存器也可以作为通用寄存器使用。
程序计数器R15
程序计数器R15又被记作PC。它虽然可以作为一般的通用寄存器使用,但是有一些指令在使用R15时有一些特殊限制。当违反了这些限制时,该指令执行的结果将是不可预料的。
由于ARM采用了流水线机制,当正确读取了PC的值时,该值为当前指令地址值加8个字节。也就是说,对于ARM指令集来说,PC指向当前指令的下两条指令的地址。
由于ARM指令是字对齐的,PC值的第0位和第1位总为0。需要注意的是,当使用指令STR/STM保存R15时,保存的可能是当前指令地址值加8字节,也可能保存的是当前指令地址加12字节。到底是哪种方式,取决于芯片具体设计方式。无论如何,在同一芯片中,要么采用当前指令地址加8,要么采用当前指令地址加12,不能有些指令采用当前指令地址加8,另一些指令采用当前指令地址加12。因此对于用户来说,尽量避免使用STR/STM指令来保存R15的值。当不可避免这种使用方式时,可以先通过一些代码来确定所用的芯片使用的是哪种实现方式。
对于ARM版本4以及更高的版本,程序必须保证写入R15寄存器的地址值的bits[1:0]为0b00;否则将会产生不可预知的结果。
对于Thumb指令集来说,指令是半字对齐的。处理器将忽略bit[0],即写入R15的地址值首先与0XFFFFFFFC做与操作,再写入R15中。
还有—些指令对于R15的用法有一些特殊的要求。比如,指令BX利用bit[0]来确定是ARM指令,还是Thumb指令。这种读取PC值和写入PC值的不对称的操作需要特别注意。
程序状态寄存器
CPSR(当前程序状态寄存器)可以在任何处理器模式下被访问。它包含了条件标志位、中断禁止位、当前处理器模式标志以及其他的一些控制和状态位。每一种处理器模式下都有一个专用的物理状态寄存器,称为SPSR(备份程序状态寄存器)。当特定的异常中断发生时,这个寄存器用于存放当前程序状态寄存器的内容。在异常中断程序退出时,可以用SPSR中保存的值来恢复CPSR。
由于用户模式和系统模式不是异常中断模式,所以它们没有SPSR。当在用户模式或系统模式中访问SPSR,将会产生不可预知的结果。
CPSR的格式如下所示。SPSR格式与CPSR格式相同。

31302928272676543210NZCVQDNM(RAZ)IFTM4M3M2M1M0



条件标志位
N(Negative)、Z(Zero)、C(Carry)及V(oVerflow)统称为条件标志位。大部分的ARM指令可以根据CPSR中的这些条件标志位来选择性地执行。各条件标志位的具体含义如表2所示。

表2 CPSR中的条件标志位

标志位含 义N本位设置成当前指令运算结果的bit[31)的值
当两个补码表示的有符号整数运算时,N=I表示运算的结果为负数;N=0表示结果为正数或零ZZ=1表示运算的结果为零;Z=0表示运算的结果不为零。
对于CMP指令,Z=1表示进行比较的两个数大小相等。下面分4种情况讨论C的设置方法:在加法指令中(包括比较指令CMN),当结果产生了进位,则C=1,表示无符号数运算发生上溢出;其他情况下C=0。在减法指令中(包括比较指令CMP),当运算中发生借位则C=0表示无符号数运算发生下溢出;其他情况下C=1。对于包含移位操作的非加/减法运算指令,C中包含最后一次溢出的位数数值。对于其他非加/减法运算指令,C位的值通常不受影响。V对于加/减法运算指令,当操作数和运算结果为二进制的补码表示的带符号数时V=1表示符号位溢出。
通常其他的指令不影响V位,具体可参考各指令的说明。



Q标志位
在ARMv5的E系列处理器中,CPSR的bit[27]称为Q标志位,主要用于指示增强的
DSP指令是否发生了溢出。同样的SPSR中的bit[27]也称为Q标志位,用于在异常中断发生时保存和恢复CPSR中的Q标志位。
在ARM v5以前的版本及ARM v5的非E系列的处理器中,Q标志位没有被定义。CPSR的bit[27]属于DNM(RAZ)。
CPSR中的控制位
CPSR的低8位I、F、T及M[4:0]统称为控制位。当异常中断发生时这些位发生变化。在特权级的处理器模式下,软件可以修改这些控制位。
1) 中断禁止位
当I=1时禁止IRQ中断。
当F=1时禁止FIQ中断。
2) T控制位
T控制位用于控制指令执行的状态,即说明本指令是ARM指令,还是Thumb指令。对与不同版本的ARM处理器,T控制位的含义不同。对于ARMv4以及更高版本的T系列的ARM处理器,
T=0表示执行ARM指令。
T=1表示执行Thumb指令。
对于ARMv5以及更高的版本的非T系列的ARM处理器,T控制位含义如下:
T=0表示执行ARM指令。
T=1表示强制下一条执行的指令产生未定义指令中断。
3) M控制位
控制位M[4:0]控制处理器模式,具体含义如表3所示。
表3控制位M[4:0] 的含义

M[4:0]处理器模式可访问的寄存器0b10000UserPC,R14一R0,CPSR0b10001FIQPC,R14_fiq-R8_flq,R7~R0,CPSR,SPSR_nq0b100101RQPC,R14 _irq-R13 _irq,R12一R0,CPSR,SPSR_ irq0b10011SupervisorPC,R14_ svc-R13 _svc,R12~R0,CPSR,SPSR_svc0b10111AbortPC,R14_abt-R13_abt,R12~R0,CPSR,SPSR_abt0b11011UndefinedPC,R14_und-R13_und,R12~R0,CPSR,SPSR_ und



CPSR中的其他位
CPSR中的其他位用于将来ARM版本的扩展。应用软件不要操作这些位,以免与ARM将来版本的扩展冲突。
Arm2440和6410有什么区别
1.主频不同。2440是400M的。6410是533/667M的;(跑安卓操作系统要500MHZ以上)
2.处理器版本不一样:2440是arm920T内核,6410是arm1176ZJF内核;
3.6410在视频处理方面比2440要强很多。内部视频解码器,包括MPEG4等视频格式;
4.6410支持WMV9、xvid、mpeg4、h264等格式的硬解码和编码;
5. 6410多和很多扩展接口比如:tv-out、CF卡和S-Video输出等;
6. spi、串口、sd接口也比那两个要丰富;
7.6410采用的是DDR内存控制器;2440采用的是SDRam内存控制器;
8.6410为双总线架构,一路用于内存总线、一路用于Flash总线;
9.6410的启动方式更加灵活:主要包括SD、Nand Flash、Nor Flash和OneFlash等设备启动;
10.6410的Nand Flash支持SLC和MLC两种架构,从而大大扩大存储空间;
11.6410为双总线架构,一路用于内存总线、一路用于Flash总线;
12.6410具备8路DMA通道,包括LCD、UART、Camera等专用DMA通道;
13.6410还支持2D和3D的图形加速;
14.2440一些参数:256M NAND FLASH,64M SDRAM,2M NOR FLASH

CPU,MPU,MCU,SOC,SOPC联系与差别
1.CPU(Central Processing Unit),是一台计算机的运算核心和控制核心。CPU由运算器、控制器和寄存器及实现它们之间联系的数据、控制及状态的总线构成。差不多所有的CPU的运作原理可分为四个阶段:提取(Fetch)、解码(Decode)、执行(Execute)和写回(Writeback)。 CPU从存储器或高速缓冲存储器中取出指令,放入指令寄存器,并对指令译码,并执行指令。所谓的计算机的可编程性主要是指对CPU的编程。
2.MPU (Micro Processor Unit),叫微处理器(不是微控制器),通常代表一个功能强大的CPU(暂且理解为增强版的CPU吧),但不是为任何已有的特定计算目的而设计的芯片。这种芯片往往是个人计算机和高端工作站的核心CPU。最常见的微处理器是Motorola的68K系列和Intel的X86系列。
3.MCU(Micro Control Unit),叫微控制器,是指随着大规模集成电路的出现及其发展,将计算机的CPU、RAM、ROM、定时计数器和多种I/O接口集成在一片芯片上,形成芯片级的芯片,比如51,avr这些芯片,内部除了CPU外还有RAM,ROM,可以直接加简单的外围器件(电阻,电容)就可以运行代码了,而MPU如x86,arm这些就不能直接放代码了,它只不过是增强版的CPU,所以得添加RAM,ROM。
MCU MPU 最主要的区别就睡能否直接运行代码。MCU有内部的RAM ROM,而MPU是增强版的CPU,需要添加外部RAM ROM才可以运行代码。
4.SOC(System on Chip),指的是片上系统,MCU只是芯片级的芯片,而SOC是系统级的芯片,它既MCU(51,avr)那样有内置RAM,ROM同时又像MPU(arm)那样强大的,不单单是放简单的代码,可以放系统级的代码,也就是说可以运行操作系统(将就认为是MCU集成化与MPU强处理力各优点二合一)。
5.SOPC(System On a Programmable Chip)可编程片上系统(FPGA就是其中一种),上面4点的硬件配置是固化的,就是说51单片机就是51单片机,不能变为avr,而avr就是avr不是51单片机,他们的硬件是一次性掩膜成型的,能改的就是软件配置,说白点就是改代码,本来是跑流水灯的,改下代码,变成数码管,而SOPC则是硬件配置,软件配置都可以修改,软件配置跟上面一样,没什么好说的,至于硬件,是可以自己构建的也就是说这个芯片是自己构造出来的,这颗芯片我们叫“白片”,什么芯片都不是,把硬件配置信息下载进去了,他就是相应的芯片了,可以让他变成51,也可以是avr,甚至arm,同时SOPC是在SOC基础上来的,所以他也是系统级的芯片,所以记得当把他变成arm时还得加外围ROM,RAM之类的,不然就是MPU了。
顺便再讲一下这个SOPC吧,首先上面讲的“白片”一般指的是FPGA或CPLD这类芯片,由于它是可配置的,所以一旦断电,他的硬件配置就没了,当然,软件配置也没了,什么都没了,比如把他硬件配置成51单片机,软件配置为跑流水灯,结果一断电,这个芯片就什么都不是了,恢复原样“白片”。
一般有两种用法,一是用它来验证芯片,因为他是可以多次下载配置验证的,成功后再把这硬件配置下载到一次性的芯片上,如果采用基于hardcopy的SOC则成功率100%,不然每次下载硬件配置验证用SOC等到你调试出正确的硬件配置,那得烧多少芯片啊,毕竟这些是一次性的,不成功只能成仁--扔掉!跟调试软件配置一样,一般软件调试很多次才能成功的,所以他是验证技术,行了再将配置配在其他的芯片,第二种方法是,芯片就用这“白片”,然后把配置信息放到flash里,上电后先将这硬件配置信息烧入这“白片”,使其变成自己想要的芯片,然后再调入软件配置。其中硬件配置可以用quartus软件编写,软件配置可以用NIOS软件,这都是altera公司的产品,可以去查看。
上拉&下拉&高阻态

上拉就是将不确定的信号通过一个电阻嵌位在高电平,电阻同时起限流作用。下拉同理。
上拉电阻是用来解决总线驱动能力不足时提供电流的,一般说法是拉电流。下拉电阻是用来吸收电流的,也就是我们通常所说的灌电流。提升电流和电压的能力是有限的,且弱强只是上拉电阻的阻值不同。
三态门,是指逻辑门的输出除有高、低电平两种状态外,还有第三种状态——高阻状态的门电路。具备这三种状态的器件就叫做三态(门,总线,...)。
如果你的设备端口要挂在一个总线上,“必须通过三态缓冲器”。因为在一个总线上同时只能有一个端口作输出,这时其他端口必须在高阻态,同时“可以输入这个输出端口的数据”。所以你还需要有总线控制管理, 访问到哪端口,那个端口的三态缓冲器才可以转入输出状态,这是典型的三态门应用。 如果在线上没有两个以上的输出设备, 当然用不到三态门。
高阻态,指的是电路的一种输出状态,既不是高电平也不是低电平。高阻态只有电容效应,没有电阻效应;阻抗很高很高,相当于断开。如果高阻态再输入下一级电路的话,对下级电路无任何影响,和没接一样,如果用万用表测的话有可能是高电平也有可能是低电平,随它后面接的东西定。
悬空和高阻态的区别 悬空(浮空,floating):就是逻辑器件的输入引脚即不接高电平,也不接低电平。由于逻辑器件的内部结构,当它输入引脚悬空时,相当于该引脚接了高电平。一般实际运用时,引脚不建议悬空,易受干扰。 高阻态:从逻辑器件内部电路结构来说,就是其输出电阻很大,该状态即不是高电平,也不是低电平。当三态门处于高阻态时,无论该门的输入如何变化,都不会对其输出有贡献。
串口协议讲一讲
串口在嵌入式系统当中是一类重要的数据通信接口,其本质功能是作为 CPU 和串行设备间的编码转换器。当数据从 CPU 经过串行端口发送出去时,字节数据转换为串行的位;在接收数据时,串行的位被转换为字节数据。应用程序要使用串口进行通信,必须在使用之前向操作系统提出资源申请要求(打开串口),通信完成后必须释放资源(关闭串口)。典型地,串口用于 ASCII 码字符的传输。通信使用3根线完成:(1)地线,(2)发送数据线,(3)接收数据线。
串口通信最重要的参数是波特率、数据位、停止位和奇偶校验。对于两个进行通行的端口,这些参数必须匹配:波特率是一个衡量通信速度的参数,它表示每秒钟传送的 bit 的个数;数据位是衡量通信中实际数据位的参数,当计算机发送一个信息包,标准的值是 5,7 和 8 位。如何设置取决于你的需求;停止位用于表示单个包的最后一位,典型的值为 1,1.5和 2 位,停止位不仅仅是表示传输的结束,并且提供计算机校正时钟同步的机会;奇偶校验位是串口通信中一种简单的检错方式,有四种检错方式——偶、奇、高和低,也可以没有校验位。
奇校验:校核数据完整性的一种方法,一个字节的8个数据位与校验位(parity bit )加起来之和有奇数个1。校验线路在收到数后,通过发生器在校验位填上0或1,以保证和是奇数个1。因此,校验位是0时,数据位中应该有奇数个1;而校验位是1时,数据位应该有偶数个1。如果读取数据时发现与此规则不符,CPU会下令重新传输数据。 奇/偶校验(ECC)是数据传送时采用的一种校正数据错误的一种方式,分为奇校验和偶校验两种。 如果是采用奇校验,在传送每一个字节的时候另外附加一位作为校验位,当实际数据中“1”的个数为偶数的时候,这个校验位就是“1”,否则这个校验位就是“0”,这样就可以保证传送数据满足奇校验的要求。在接收方收到数据时,将按照奇校验的要求检测数据中“1”的个数,如果是奇数,表示传送正确,否则表示传送错误。 同理偶校验的过程和奇校验的过程一样,只是检测数据中“1”的个数为偶数。
RS232和RS485通讯接口有什么区别
1、传输方式不同。 RS-232采取不平衡传输方式,即所谓单端通讯. 而RS485则采用平衡传输,即差分传输方式。
2、传输距离不同。RS-232适合本地设备之间的通信,传输距离一般不超过20m。而RS-485的传输距离为几十米到上千米。
3、RS-232 只允许一对一通信,而RS-485 接口在总线上是允许连接多达128个收发器。 RS232/RS485,是两种不同的电气协议,也就是说,是对电气特性以及物理特性的规定,作用于数据的传输通路上,它并不内含对数据的处理方式。比如,最显著的特征是:RS232使用3-15v有效电平,而UART,因为对电气特性并没有规定,所以直接使用CPU使用的电平,就是所谓的TTL电平(可能在0~3.3V之间)。更具体的,电气的特性也决定了线路的连接方式,比如RS232,规定用电平表示数据,因此线路就是单线路的,用两根线才能达到全双工的目的;而RS485, 使用差分电平表示数据,因此,必须用两根线才能达到传输数据的基本要求,要实现全双工,必需用4根线。但是,无论使用RS232还是RS485,它们与UART是相对独立的,但是由于电气特性的差别,必须要有专用的器件和UART接驳,才能完成数据在线路和UART之间的正常流动。
总结:从某种意义上,可以说,线路上存在的仅仅是电流,RS232/RS485规定了这些电流在什么样的线路上流动和流动的样式;在UART那里,电流才被解释和组装成数据,并变成CPU可直接读写的形式。
IIC时序图画一下,IIC有哪些状态,给一个字节,将它发送出去。IIC有什么注意事项?有没有用I/O模拟IIC,如果有需要注意什么?
见前ARM部分
为什么2440的内存起始地址是3后面7个0呢?为什么6410的内存起始地址是5后面7个0呢?
内存映射 (256M SDRAM)


内存管理有什么看法?(MMU)
见之前Linux部分MMU简介
锁有哪些?有什么注意事项
自旋锁 上厕所,等待,自旋。 如果线程A持有自旋锁时间过长,显然会浪费处理器的时间,降低了系统性能。自旋锁只适合短期持有,如果遇到需要长时间持有的情况,我们就要换一种方式了(下文的互斥体)。
读写锁 当临界区的一个文件可以被同时读取,但是并不能被同时读和写。如果一个线程在读,另一个线程在写,那么很可能会读取到错误的不完整的数据。读写自旋锁是可以允许对临界区的共享资源进行并发读操作的。但是并不允许多个线程并发读写操作。如果想要并发读写,就要用到了顺序锁
顺序锁 顺序锁是读写锁的优化版本,读写锁不允许同时读写,而使用顺序锁可以完成同时进行读和写的操作,但并不允许同时的写。虽然顺序锁可以同时进行读写操作,但并不建议这样,读取的过程并不能保证数据的完整性。
信号量和自旋锁区别
信号量 信号量和自旋锁有些相似,不同的是信号量会发出一个信号告诉你还需要等多久。因此,不会出现傻傻等待的情况。比如,有100个停车位的停车场,门口电子显示屏上实时更新的停车数量就是一个信号量。这个停车的数量就是一个信号量,他告诉我们是否可以停车进去。当有车开进去,信号量加一,当有车开出来,信号量减一。
比如,厕所一次只能让一个人进去,当A在里面的时候,B想进去,如果是自旋锁,那么B就会一直在门口傻傻等待。如果是信号量,A就会给B一个信号,你先回去吧,我出来了叫你。这就是一个信号量的例子,B听到A发出的信号后,可以先回去睡觉,等待A出来。 因此,信号量显然可以提高系统的执行效率,避免了许多无用功。信号量具有以下特点: 因为信号量可以使等待资源线程进入休眠状态,因此适用于那些占用资源比较久的场合。 因此信号量不能用于中断中,因为信号量会引起休眠,中断不能休眠。 如果共享资源的持有时间比较短,那就不适合使用信号量了,因为频繁的休眠、切换线程引起的开销要远大于信号量带来的那点优势。
在驱动程序中,当多个线程同时访问相同的资源时(驱动程序中的全局变量是一种典型的共享资源),可能会引发"竞态",因此我们必须对共享资源进行并发控制。Linux内核中解决并发控制的最常用方法是自旋锁与信号量(绝大多数时候作为互斥锁使用)。
自旋锁与信号量"类似而不类",类似说的是它们功能上的相似性,"不类"指代它们在本质和实现机理上完全不一样,不属于一类。
自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环查看是否该自旋锁的保持者已经释放了锁,"自旋"就是"在原地打转"。而信号量则引起调用者睡眠,它把进程从运行队列上拖出去,除非获得锁。这就是它们的"不类"。
但是,无论是信号量,还是自旋锁,在任何时刻,最多只能有一个保持者,即在任何时刻最多只能有一个执行单元获得锁。这就是它们的"类似"。
鉴于自旋锁与信号量的上述特点,一般而言,自旋锁适合于保持时间非常短的情况,它可以在任何上下文使用;信号量适合于保持时间较长的情况,会只能在进程上下文使用。如果被保护的共享资源只在进程上下文访问,则可以以信号量来保护该共享资源,如果对共享资源的访问时间非常短,自旋锁也是好的选择。但是,如 果被保护的共享资源需要在中断上下文访问(包括底半部即中断处理句柄和顶半部即软中断),就必须使用自旋锁。
区别总结如下: 1、由于争用信号量的进程在等待锁重新变为可用时会睡眠,所以信号量适用于锁会被长时间持有的情况。 2、相反,锁被短时间持有时,使用信号量就不太适宜了(自旋锁比较合适),因为睡眠引起的耗时可能比锁被占用的全部时间还要长。 3、由于执行线程在锁被争用时会睡眠,所以只能在进程上下文中才能获取信号量锁,因为在中断上下文中(使用自旋锁)是不能进行调度的。 4、你可以在持有信号量时去睡眠(当然你也可能并不需要睡眠),因为当其它进程试图获得同一信号量时不会因此而死锁,(因为该进程也只是去睡眠而已,而你最终会继续执行的)。 5、在你占用信号量的同时不能占用自旋锁,因为在你等待信号量时可能会睡眠,而在持有自旋锁时是不允许睡眠的。 6、信号量锁保护的临界区可包含可能引起阻塞的代码,而自旋锁则绝对要避免用来保护包含这样代码的临界区,因为阻塞意味着要进行进程的切换,如果进程被切换出去后(锁定期间被切换出去),另一进程企图获取本自旋锁,死锁就会发生。 7、信号量不同于自旋锁,它不会禁止内核抢占(自旋锁被持有时,内核不能被抢占),所以持有信号量的代码可以被抢占,这意味着信号量不会对调度的等待时间带来负面影响。 除了以上介绍的同步机制方法以外,还有BKL(大内核锁),Seq锁等。 BKL是一个全局自旋锁,使用它主要是为了方便实现从Linux最初的SMP过度到细粒度加锁机制。 Seq锁用于读写共享数据,实现这样锁只要依靠一个序列计数器。 总结:二者区别主要从持有时间,使用环境(进程上下文还是中断上下文),内核抢占
中断能不能睡眠,为什么?下半部能不能睡眠?
1、 中断处理的时候,不应该发生进程切换,因为在中断context中,唯一能打断当前中断handler的只有更高优先级的中断,它不会被进程打断,如果在中断context中休眠,则没有办法唤醒它,因为所有的wake_up_xxx都是针对某个进程而言的,而在中断context中,没有进程的概念,没 有一个task_struct(这点对于softirq和tasklet一样),因此真的休眠了,比如调用了会导致block的例程,内核几乎肯定会死。
2、schedule()在切换进程时,保存当前的进程上下文(CPU寄存器的值、进程的状态以及堆栈中的内容),以便以后恢复此进程运行。中断发生后,内核会先保存当前被中断的进程上下文(在调用中断处理程序后恢复);
但在中断处理程序里,CPU寄存器的值肯定已经变化了吧(最重要的程序计数器PC、堆栈SP等),如果此时因为睡眠或阻塞操作调用了schedule(),则保存的进程上下文就不是当前的进程context了.所以不可以在中断处理程序中调用schedule()。????
3、2.4内核中schedule()函数本身在进来的时候判断是否处于中断上下文:
if(unlikely(in_interrupt()))

BUG();

因此,强行调用schedule()的结果就是内核BUG,但我看2.6.18的内核schedule()的实现却没有这句,改掉了。
4、中断handler会使用被中断的进程内核堆栈,但不会对它有任何影响,因为handler使用完后会完全清除它使用的那部分堆栈,恢复被中断前的原貌。
5、处于中断context时候,内核是不可抢占的。因此,如果休眠,则内核一定挂起。
为什么软中断中也不能睡眠
这个问题实际上是一个老生常谈的问题,答案也很简单,Linux在软中断上下文中是不能睡眠的,原因在于Linux的软中断实现上下文有可能是中断上下文,如果在中断上下文中睡眠,那么会导致Linux无法调度,直接的反应是系统Kernel Panic,并且提示dequeue_task出错。所以,在软中断上下文中,我们不能使用信号量等可能导致睡眠的函数,这一点在编写IO回调函数时需要特别注意。 在最近的一个项目中,我们在dm-io的callback函数中去持有semaphore访问竞争资源,导致了系统的kernel panic。其原因就在于dm-io的回调函数在scsi soft irq中执行,scsi soft irq是一个软中断,其会在硬中断发生之后被执行,执行上下文为中断上下文。
中断上下文中无法睡眠的原因大家一定很清楚,原因在于中断上下文不是一个进程上下文,其没有一个专门用来描述CPU寄存器等信息的数据结构,所以无法被调度器调度。如果将中断上下文也设计成进程上下文,那么调度器就可以对其进行调度,如果在开中断的情况下,其自然就可以睡眠了。但是,如果这样设计,那么中断处理的效率将会降低。中断(硬中断、软中断)处理都是些耗时不是很长,对实时性要求很高,执行频度较高的应用,所以,如果采用一个专门的后台daemon对其处理,显然并不合适。
Linux对中断进行了有效的管理,一个中断发生之后,都会通过相应的中断向量表获取该中断的处理函数。在Linux操作系统中都会调用do_IRQ这个函数,在这个函数中都会执行__do_IRQ(),__do_IRQ函数调用该中断的具体执行函数。在执行过程中,该函数通过中断号找到具体的中断描述结构irq_desc,该结构对某一具体硬件中断进行了描述。在irq_desc结构中存在一条链表irqaction,这条链表中的某一项成员都是一个中断处理方法。这条链表很有意思,其实现了中断共享,例如传统的PCI总线就是采用共享中断的方法,该链表中的一个节点就对应了一个PCI设备的中断处理方法。在PCI设备驱动加载时,都需要注册本设备的中断处理函数,通常会调用request_irq这个函数,通过这个函数会构造一个具体的irq action,然后挂接到某个具体irq_desc的action链表下,实现中断处理方法的注册。在__do_IRQ函数中会通过handle_IRQ_event()函数遍历所有的action节点,完成中断处理过程。到目前为止,中断处理函数do_IRQ完成的都是上半部的工作,也就是设备注册的中断服务程序。在中断上半部中,通常都是关中断的,基本都是完成很简单的操作,否则将会导致中断的丢失。耗时时间相对较长,对实时性要求不是最高的应用都会被延迟处理,都会在中断下半部中执行。所以,在中断上半部中都会触发软中断事件,然后执行完毕,退出服务。
__do_IRQ完成之后,返回到do_IRQ函数,在该函数中调用了一个非常重要的函数irq_exit(),在该函数中调用invoke_softirq(),invoke_softirq调用do_softirq()函数,执行软中断的操作。此时,程序的执行环境还是中断上下文,但是与中断上半部不同的是,软中断执行过程中是开中断的,能够被硬中断而中断。所以,如果用户的程序在软中断中睡眠,操作系统该如何调度呢?只有kernel panic了。另外,软中断除了上述执行点之外,还有其他的执行点,在内核中还有一个软中断的daemon处理软中断事务,驱动程序也可以自己触发一个软中断事件,并且在软中断的daemon上下文中执行。但是硬中断触发的事件都不会在这个daemon的上下文中执行,除非修改Linux中的do__IRQ代码。
上述对软中断的执行做了简要分析,我对Linux中的硬中断管理机制做了一些代码分析,这一块代码量不是很大,可移植性非常的好~~建议大家阅读,对我上述的分析和理解存在什么不同意见,欢迎大家讨论。
上下文有哪些?怎么理解?
见前操作系统部分
死锁产生的原因及四个必要条件
以按键驱动为例,首先执行的module_init()、module_exit()指定的init函数和exit函数。在init函数中,register_chrdev注册一个字符设备驱动,class_create创建类的名字,class_device_create创建设备。ioremap对管脚进行地址映射。
在应用程序中调用open函数,就会执行到底层的open函数,在open函数中完成对相应引脚的配置,在应用层调用read函数读取的时候,就会调用底层的read,在里面可以读取按键的值或者点亮LED等等,做一些需要做的事情。最后退出程序的时候就会调用exit函数,unregister_chrdev //卸载驱动,class_device_unregister卸载类设备。class_destroy卸载类,iounmap注销虚拟地址。
LCD驱动如何编写?触摸屏驱动如何编写?
见之前Linux部分
触摸屏中断做了什么,LCD中断做了什么?
触摸屏中断中启动ADC转换,ADC转换完成后会产生ADC中断。在ADC中断中读取数据.之后启动定时器,在定时器中断中判断是否继续按下,如果继续按下就会启动ADC继续转换。
什么是交叉编译?为什么需要交叉编译?为什么还要主机编译
什么是交叉编译 在一种计算机环境中运行的编译程序,能编译出在另外一种环境下运行的代码,我们就称这种编译器支持交叉编译。这个编译过程就叫交叉编译。简单地说,就是在一个平台上生成另一个平台上的可执行代码。这里需要注意的是所谓平台,实际上包含两个概念:体系结构(Architecture)、操作系统(OperatingSystem)。同一个体系结构可以运行不同的操作系统;同样,同一个操作系统也可以在不同的体系结构上运行。举例来说,我们常说的x86 Linux平台实际上是Intel x86体系结构和Linux for x86操作系统的统称;而x86 WinNT平台实际上是Intel x86体系结构和Windows NT for x86操作系统的简称。
交叉编译这个概念的出现和流行是和嵌入式系统的广泛发展同步的。我们常用的计算机软件,都需要通过编译的方式,把使用高级计算机语言编写的代码(比如C代码)编译(compile)成计算机可以识别和执行的二进制代码。比如,我们在Windows平台上,可使用Visual C++开发环境,编写程序并编译成可执行程序。这种方式下,我们使用PC平台上的Windows工具开发针对Windows本身的可执行程序,这种编译过程称为native compilation,中文可理解为本机编译。然而,在进行嵌入式系统的开发时,运行程序的目标平台通常具有有限的存储空间和运算能力,比如常见的ARM 平台,其一般的静态存储空间大概是16到32MB,而CPU的主频大概在100MHz到500MHz之间。这种情况下,在ARM平台上进行本机编译就不太可能了,这是因为一般的编译工具链(compilation tool chain)需要很大的存储空间,并需要很强的CPU运算能力。为了解决这个问题,交叉编译工具就应运而生了。通过交叉编译工具,我们就可以在CPU能力很强、存储控件足够的主机平台上(比如PC上)编译出针对其他平台的可执行程序。
要进行交叉编译,我们需要在主机平台上安装对应的交叉编译工具链(crosscompilation tool chain),然后用这个交叉编译工具链编译我们的源代码,最终生成可在目标平台上运行的代码。常见的交叉编译例子如下: 1、在Windows PC上,利用ADS(ARM 开发环境),使用armcc编译器,则可编译出针对ARM CPU的可执行代码。 2、在Linux PC上,利用arm-linux-gcc编译器,可编译出针对Linux ARM平台的可执行代码。 3、在Windows PC上,利用cygwin环境,运行arm-elf-gcc编译器,可编译出针对ARM CPU的可执行代码。
为什么要使用交叉编译 有时是因为目的平台上不允许或不能够安装我们所需要的编译器,而我们又需要这个编译器的某些特征;有时是因为目的平台上的资源贫乏,无法运行我们所需要编译器;有时又是因为目的平台还没有建立,连操作系统都没有,根本谈不上运行什么编译器。
简述linux系统启动过程
可以答之前的答之前的分析uboot的过程的部分。
Linux设备中字符设备和块设备有什么主要区别?分别举例。
字符设备:字符设备是个能够像字节流(类似文件)一样被访问的设备,由字符设备驱动程序来实现这种特性。字符设备驱动程序通常至少实现open, close,read和 write系统谓用。字符终端、串口、鼠标、键盘、摄像头、声卡和显卡等就是典型的字符设备。
块设备:和字符设备类似,块设备也是通过/dev目录下的文件系统节点来访问。块设备上能够容纳文件系统,如:u盘,SD卡,磁盘等。
字符设备和块设备的区别仅仅在于内核内部管理数据的方式,也就是内核及驱动程序之间的些不同对用户来讲是透明的。
主设备号和次设备号的用途

Linux各种设备都以文件的形式存放在/dev目录下,称为设备文件。
应用程序可以打开、关闭和读写这些设备文件,完成对设备的操作,就像操作普通的数据文件一样。为了管理这些设备,系统为设备编了号,每个设备号又分为主设备号和次设备号。主设备号用来区分不同种类的设备,而次设备号用来区分同一类型的多个设备。对于常用设备,Linux有约定俗成的编号,如硬盘的主设备号是3。
一个字符设备或者块设备都有一个主设备号和次设备号。主设备号和次设备号统称为设备号。主设备号用来表示一个特定的驱动程序。次设备号用来表示使用该驱动程序的各设备。例如一个嵌入式系统,有两个LED指示灯,LED灯需要独立的打开或者关闭。那么,可以写一个LED灯的字符设备驱动程序,可以将其主设备号注册成5号设备,次设备号分别为1和2。这里,次设备号就分别表示两个LED灯。
同步通信和异步通信
同步通信和异步通信 串行通信可以分为两种类型,一种叫同步通信,另一种叫异步通信。
同步通信方式,是把许多字符组成一个信息组,这样,字符可以一个接一个地传输,但是,在每组信息(通常称为信息帧)的开始要加上同步字符,在没有信息要传输时,要填上空字符,因为同步传输不允许有间隙。同步方式下,发送方除了发送数据,还要传输同步时钟信号,信息传输的双方用同一个时钟信号确定传输过程中每1位的位置。见右图5.2所示。


在异步通信方式中,两个数据字符之间的传输间隔是任意的,所以,每个数据字符的前后都要用一些数位来作为分隔位。
从下图中可以看到,按标准的异步通信数据格式(叫做异步通信帧格式),1个字符在传输时,除了传输实际数据字符信息外,还要传输几个外加数位。具体说,在1个字符开始传输前,输出线必须在逻辑上处于“1”状态,这称为标识态。传输一开始,输出线由标识态变为“0”状态,从而作为起始位。起始位后面为5~8个信息位,信息位由低往高排列,即先传字符的低位,后传字符的高位。信息位后面为校验位,校验位可以按奇校验设置,也可以按偶校验设置,或不设校验位。最后是逻辑的“1”作为停止位,停止位可为1位、1.5位或者2位。如果传输完1个字符以后,立即传输下一个字符,那么,后一个字符的起始位便紧挨着前一个字符的停止位了,否则,输出线又会进入标识态。在异步通信方式中,发送和接收的双方必须约定相同的帧格式,否则会造成传输错误。在异步通信方式中,发送方只发送数据帧,不传输时钟,发送和接收双方必须约定相同的传输率。当然双方实际工作速率不可能绝对相等,但是只要误差不超过一定的限度,就不会造成传输出错。图5.3是异步通信时的标准数据格式。
比较起来,在传输率相同时,同步通信方式下的信息有效率要比异步方式下的高,因为同步方式下的非数据信息比例比较小。

传输率 所谓传输率就是指每秒传输多少位,传输率也常叫波特率。在计算机中,每秒传输多少位和波特率的含义是完全一致的,但是,在最初的定义上,每秒传输多少位和波特率是不同的,前者是指每秒钟传输的数位是多少,而波特率是指每秒钟传输的离散信号的数目。所谓离散信号,就是指不均匀的、不连续的也不相关的信号。在计算机里,只允许高电平和低电平两种离散信号,它们分别表示l和0,于是,造成了波特率与每秒传输数位这两者的吻合。但在其他一些场合,就未必如此。比如,采用脉冲调制时,可以允许取4种相位,而每种相位代表2个数位,这种情况下,按每秒传输多少位(bps)计算的传输率便是波特率的两倍。
国际上规定了一个标准波特率系列,标准波特率也是最常用的波特率,标准波特率系列为110、300、600、1200、1800、2400、4800、9600、19200......。
大多数接口的波特率可以通过编程来指定。
作为例子,我们可以考虑这样一个异步传输过程:设每个字符对应1个起始位、7个数据位、1个奇/偶校验位和1个停止位,如果波特率为1200,那么,每秒钟能传输的最大字符数为1200/10=120个。
作为比较,我们再来看一个同步传输的例子。假如也用1200的波特率工作,每个字符为7位,用4个同步字符作为信息帧头部,但不用奇/偶校验,那么,传输100个字符所用的时间为7×(100+4)/1200=0.6067,这就是说,每秒钟能传输的字符数可达到100/0.6067=165个。 异步通信的差错类型 异步通信过程中,可能发生通信错,一般有3种错误: 1、帧格式错:在应该接收到停止位的时候,接收到逻辑的“0”,便产生帧格式错误。 2、奇偶错:接收到的奇偶校验位错。 3、覆盖错:通信接口接收到数据并存放到数据输入寄存器中,但是CPU没有及时来取,后面新接收的数据覆盖了前面收到的数据,叫做覆盖错。 发生帧格式错和奇偶错的原因可能为下面几种: ◆ 发送和接收双方采用了不同的传输率,或虽然双方约定了相同的传输率,但传输率不可能绝对相等。在通信的速率比较高的情况下,如果双方的传输率误差达到一定的程度,也会造成通信出错; ◆ 通信双方采用了不相同的帧格式; ◆ 干扰。
Uart和IIC和SPI的区别(提示:关于异步和同步,电子器件上的)?
UART、SPI、IIC是经常用到的几个数据传输标准,下面分别总结一下:
UART(Universal Asynchronous Receive Transmitter):也就是我们经常所说的串口,基本都用于调试。
主机和从机至少要接三根线,RX、TX和GND。TX用于发送数据,RX用于接受数据(收发不是一根线,所以是全双工方式)。注意A和B通信A.TX要接B.RX,A.RX要接B.TX(A用TX发B当然要用RX来收了!)
如果A是PC机,B是单片机,A和B之间还要接一块电平转换芯片,用于将TTL/CMOS(单片机电平)转换为RS232(PC机电平)。因为TTL/CMOS电平范围是0 ~ 1.8/2.5/3.3/5V(不同单片机范围不同),高电压表示1,低电压表示0。而RS232逻辑电平范围**-12V~ 12V**,-5~ -12表示高电平,+5~+12V表示低电平(对!你没有听错)。为什么这么设置?这就要追溯到调制解调器出生时代了,有兴趣自己去查资料!
数据协议:以PC机A给单片机B发数据为例(1为高电平,0为低电平):A.TX to B.RX。刚开始B.RX的端口保持1,当A.TX发来一个0作为起始位告诉B我要发数据了!然后就开始发数据,发多少呢?通常一次是5位、6位、7位、8位,这个双方事先要用软件设置好。PC机一般会用串口助手设置,单片机会在uart的驱动中设置。一小帧数据发送完了以后,A.TX给个高电平告诉B.RX我发完了一帧。如果还有数据,就再给个0然后重复上一步。如果双方约定由校验位,还要在发停止位1之前发送个校验位,不过现在一般都不需要校验位了,因为出错的概率太小了,而且一般用于调试。
一般在串口助手上还有个RTS/CTS流控选项,也叫握手,我从来没用过。搬一段我能理解的介绍:RTS(请求发送),CTS(清除发送)。如果要用这两个功能,那就至少要接5根线:RX+TX+GND+RTS+CTS。当A要发送数据时,置RTS有效(可能是置1),告诉B我要发送数据了。当B准备好接受数据后,置CTS有效,告诉A你可以发了。然后他们就实现了两次握手!挺耽误时间是不是?这个RTS还可以当电源使用,如果你不用它的握手功能,且电源电流在50mA以下时,就可以把它置为高电平可以当电源用喔~!
SPI(Serial Peripheral Interface, 同步外设接口)是由摩托罗拉公司开发的全双工同步串行总线,该总线大量用在与EEPROM、ADC、FRAM和显示驱动器之类的慢速外设器件通信。
SPI是一种串行同步通讯协议,由一个主设备和一个或多个从设备组成,主设备启动一个与从设备的同步通讯,从而完成数据的交换。SPI 接口由SDI(串行数据输入),SDO(串行数据输出),SCK(串行移位时钟),CS(从使能信号)四种信号构成,CS 决定了唯一的与主设备通信的从设备,片选信号低电平有效。如没有CS 信号,则只能存在一个从设备,主设备通过产生移位时钟来发起通讯。通讯时,数据由SDO 输出,SDI 输入,数据在时钟的上升或下降沿由SDO 输出,在紧接着的下降或上升沿由SDI 读入,这样经过8/16 次时钟的改变,完成8/16 位数据的传输。(极性 空闲高低电平 相位 那个跳边沿采样)
IIC(Inter Integrated Circuit):两根线:一个时钟线SCL和一个数据线SDA。只有一根数据线,所以是半双工通信。接线不难,而且两根线上也可以挂很多设备(每个设备的IIC地址不同),数据协议比较麻烦:
还是假设A给B发数据(这里A.SCL接B.SCL, A.SDA接B.SDA)。起初SDA和SCL上的电平都为高电平。然后A先把SDA拉低,等SDA变为低电平后再把SCL拉低(以上两个动作构成了iic的起始位),此时SDA就可以发送数据了,与此同时,SCL发送一定周期的脉冲(周期和PCLK有关,一般会在IIC的控制寄存器中设置)。SDA发送数据和SCL发送脉冲的要符合的关系是:SDA必须在SCL是高电平是保持有效,在SCL是低电平时发送下一位(SCL会在上升沿对SDA进行采样)。规定一次必须传8位数据,8位数据传输结束后A释放SDA,但SCL再发一个脉冲(这是第九个脉冲),这会触发B通过将SDA置为低电平表示确认(该低电平称为ACK)。最后SCL先变为高电平,SDA再变为高电平(以上两个动作称为结束标志)如果B没有将SDA置为0,则A停止发送下一帧数据。IIC总线(即SDA和SCL)上的每个设备都有唯一地址,数据包传输时先发送地址位,接着才是数据。一个地址字节由7个地址位(可以挂128个设备)和1个指示位组成(7位寻址模式)。指示位是0表示写,1表示读。还有10位寻址模式,使用两个字节来保存地址,第一个字节的最低两位和第二个字节的8位合起来构成10位地址。
在I2C总线的应用中应注意的事项总结为以下几点 : 1) 严格按照时序图的要求进行操作, 2) 若与口线上带内部上拉电阻的单片机接口连接,可以不外加上拉电阻。 3) 程序中为配合相应的传输速率,在对口线操作的指令后可用NOP指令加一定的延时。 4) 为了减少意外的干扰信号将EEPROM内的数据改写可用外部写保护引脚(如果有),或者在EEPROM内部没有用的空间写入标志字,每次上电时或复位时做一次检测,判断EEPROM是否被意外改写。
关于IIC总线的操作注意事项 1、对IIC总线的一次操作完之后,需要等待一段时间才能进行第二次操作。否则是启动不了总线的:) 2、在时钟线(SCL)为高电平的时候,一定不能动数据线(SDA)状态,除非是启动或者结束总线
UART, SPI, IIC的区别与联系:
第一个区别当然是名字: UART(Universal Asynchronous Receiver Transmitter:通用异步收发器) SPI(Serial Peripheral Interface:串行外设接口); I2C(INTER IC BUS)
第二,区别在电气信号线上: SPI总线由三条信号线组成:串行时钟(SCLK)、串行数据输出(SDO)、串行数据输入(SDI)。SPI总线可以实现 多个SPI设备互相连接。提供SPI串行时钟的SPI设备为SPI主机或主设备(Master),其他设备为SPI从机或从设备(Slave)。主从设备间可以实现全双工通信,当有多个从设备时,还可以增加一条从设备选择线。
如果用通用IO口模拟SPI总线,必须要有一个输出口(SDO),一个输入口(SDI),另一个口则视实现的设备类型而定,如果要实现主从设备,则需输入输出口,若只实现主设备,则需输出口即可,若只实现从设备,则只需输入口即可。
I2C总线是双向、两线(SCL、SDA)、串行、多主控(multi-master)接口标准,具有总线仲裁机制,非常适合在器件之间进行近距离、非经常性的数据通信。在它的协议体系中,传输数据时都会带上目的设备的设备地址,因此可以实现设备组网。 如果用通用IO口模拟I2C总线,并实现双向传输,则需一个输入输出口(SDA),另外还需一个输出口(SCL)。(注:I2C资料了解得比较少,这里的描述可能很不完备)
UART总线是异步串口,因此一般比前两种同步串口的结构要复杂很多,一般由波特率产生器(产生的波特率等于传输波特率的16倍)、UART接收器、UART发送器组成,硬件上由两根线,一根用于发送,一根用于接收。
显然,如果用通用IO口模拟UART总线,则需一个输入口,一个输出口。
第三,从第二点明显可以看出,SPI和UART可以实现全双工,但I2C不行;
第四,看看牛人们的意见吧!
wudanyu:I2C线更少,我觉得比UART、SPI更为强大,但是技术上也更加麻烦些,因为I2C需要有双向IO的支持,而且使用上拉电阻,我觉得抗干扰能力较弱,一般用于同一板卡上芯片之间的通信,较少用于远距离通信。SPI实现要简单一些,UART需要固定的波特率,就是说两位数据的间隔要相等,而SPI则无所谓,因为它是有时钟的协议。
quickmouse:I2C的速度比SPI慢一点,协议比SPI复杂一点,但是连线也比标准的SPI要少。 UART一帧可以传5/6/7/8位,IIC必须是8位。IIC和SPI都从最高位开始传。 SPI用片选信号选择从机,IIC用地址选择从机。
速率与传输距离 SPI 速率与芯片有关,有的400K,有的到几兆; rs232速率一般最大115200; iic一般应用400K,CAN最高可到1M; spi和iic一般应用在芯片之间通讯,RS232可应用与设备与设备之间短距离通讯,最大15米,CAN适用设备间通讯,抗干扰能力强,理论上通讯距离可到10KM
用串口发送十个字节就丢失一个两个你会怎样检查;发送的时候对方设备不响应你该怎么办
1.发送方自己发自己收,看有无问题 2.接收方自己发自己收,看有无问题 3.都没问题就是在发送过程中出现问题, 发送和接收的参数是否一致,比如波特率和奇偶校验位 打印发送buffer的数据和接收buffer的数据 4.检查丢失的字节有什么规律 5.逻辑分析仪查看发送和接收的时序,导出发送的数据和接收的数据 什么是逻辑分析仪?逻辑分析仪的参数、使用步骤和优势
内核链表为什么具有通用性?
内核中由于要管理大量的设备,但是各种设备各不相同,必须将他们统一起来管理,于是内核设计者就想到了使用通用链表来处理,通用链表看似神秘,实际上就是双向循环链表,这个链表的每个节点都是只有指针域,没有任何数据域。
使用通用链表的好处是:1.通用链表中每个节点中没有数据域,也就是说无论数据结构有多复杂在链表中只有前后级指针。2.如果一个数据结构(即是描述设备的设备结构体)想要用通用链表管理,只需要在结构体中包含节点的字段即可。3.双向链表可以从任意一个节点的前后遍历整个链表,遍历非常方便。4.使用循环链表使得可以不断地循环遍历管理节点,像进程的调度:操作系统会把就绪的进程放在一个管理进程的就绪队列的通用链表中管理起来,循环不断地,为他们分配时间片,获得cpu进行周而复始的进程调度。
通用(内核)链表详解
分配内存哪些函数?kmalloc有两个参数,各个作用是什么?
见前Arm部分
有哪些锁,各自的效率问题?自选锁怎样实现的?
锁是线程同步时的一个重要的工具,然而操作系统中包含了多种不同的锁,各种锁之间有什么不同呢?
信号量(Semaphore)
信号量分为二元信号量和多元信号量,所谓二元信号量就是指该信号量只有两个状态,要么被占用,要么空闲;而多元信号量则允许同时被N个线程占有,超出N个外的占用请求将被阻塞。信号量是“系统级别”的,即同一个信号量可以被不同的进程访问。 因为信号量可以使等待资源线程进入休眠状态,因此适用于那些占用资源比较久的场合。如果共享资源的持有时间比较短,那就不适合使用信号量了,因为频繁的休眠、切换线程引起的开销要远大于信号量带来的那点优势。
互斥量 (Mutex)
和二元信号量类似(一元信号量就是互斥体), 唯一不同的是,互斥量的获取和释放必须是在同一个线程中进行的。如果一个线程去释放一个并不是它所占有的互斥量是无效的。而信号量是可以由其它线程进行释放的。
临界区(Critical Section)
术语中,把临界区的锁的获取称为进入临界区,而把锁的释放称为离开临界区。临界区是“进程级别”的,即它只在本进程的所有线程中可见,其它性质与互斥量相同(即谁获取,谁释放)
读写锁(Read-Write Lock)
适 用于一个特定的场合。比如对于一段线程间访问的数据,如果程序大部分时间都是在读取,而只有很少的时间才会写入,那么使用前面几种锁时,每次读取也是同样 要申请锁的,而这时其它的线程就无法再对此段数据进行读取。可是,多个线程同时对一段数据进行读取时,是不存在同步问题的,那么这些读取时设置的锁就影响了程序的性能。读写锁的出现就是为了解决这个问题的。
对于一个读写锁,有两种获取方式:共享(Shared)或独占 (Exclusive)。如果当前读写锁处于空闲状态,那么当多个线程同时以共享方式访问该读写锁时,都可以成功;而此时如果一个线程以独占的方式访问该 读写锁,那么它会等待所有共享访问都结束后才可以成功。在读写锁被独占访问的过程中,再次共享和独占请求访问该锁,都会进行等待状态。
当临界区的一个文件可以被同时读取,但是并不能被同时读和写。如果一个线程在读,另一个线程在写,那么很可能会读取到错误的不完整的数据。读写自旋锁是可以允许对临界区的共享资源进行并发读操作的。但是并不允许多个线程并发读写操作。如果想要并发读写,就要用到了顺序锁。 顺序锁 顺序锁是读写锁的优化版本,读写锁不允许同时读写,而使用顺序锁可以完成同时进行读和写的操作,但并不允许同时的写。虽然顺序锁可以同时进行读写操作,但并不建议这样,读取的过程并不能保证数据的完整性。
顺序锁和读写锁都是自旋锁的一种,因为在等待自旋锁的时候处于“自旋”状态,因此锁的持有时间不能太长,一定要短,否则的话会降低系统性能。如果临界区比较大,运行时间比较长的话要选择其他的并发处理方式,比如信号量和互斥体。
条件变量(Condition Variable)
条件变量相当于一种通知机制。多个线程可以设置等待该条件变量,而一旦另外的线程设置了该条件变量(相当于唤醒条件变量)后,多个等待的线程就可以继续执行了。
Linux内核硬中断 / 软中断的原理和实现
一、概述
从本质上来讲,中断是一种电信号,当设备有某种事件发生时,它就会产生中断,通过总线把电信号发送给中断控制器。
如果中断的线是激活的,中断控制器就把电信号发送给处理器的某个特定引脚。处理器于是立即停止自己正在做的事,跳到中断处理程序的入口点,进行中断处理。 (1)硬中断 由与系统相连的外设(比如网卡、硬盘)自动产生的。主要是用来通知操作系统系统外设状态的变化。比如当网卡收到数据包的时候,就会发出一个中断。我们通常所说的中断指的是硬中断(hardirq)。
(2)软中断 为了满足实时系统的要求,中断处理应该是越快越好。linux为了实现这个特点,当中断发生的时候,硬中断处理那些短时间就可以完成的工作,而将那些处理事件比较长的工作,放到中断之后来完成,也就是软中断(softirq)来完成。
(3)中断嵌套
Linux下硬中断是可以嵌套的,但是没有优先级的概念,也就是说任何一个新的中断都可以打断正在执行的中断,但同种中断除外。软中断不能嵌套,但相同类型的软中断可以在不同CPU上并行执行。
(4)软中断指令 int是软中断指令。 中断向量表是中断号和中断处理函数地址的对应表。 int n -- 触发软中断n。相应的中断处理函数的地址为:中断向量表地址 + 4 * n。
(5)硬中断和软中断的区别 软中断是执行中断指令产生的,而硬中断是由外设引发的。 硬中断的中断号是由中断控制器提供的,软中断的中断号由指令直接指出,无需使用中断控制器。 硬中断是可屏蔽的,软中断不可屏蔽。 硬中断处理程序要确保它能快速地完成任务,这样程序执行时才不会等待较长时间,称为上半部。 软中断处理硬中断未完成的工作,是一种推后执行的机制,属于下半部。 二、开关
(1)硬中断的开关
简单禁止和激活当前处理器上的本地中断:
local_irq_disable();

local_irq_enable();

保存本地中断系统状态下的禁止和激活:
unsigned long flags;

local_irq_save(flags);

local_irq_restore(flags);

(2)软中断的开关 禁止下半部,如softirq、tasklet和workqueue等:
local_bh_disable();

local_bh_enable();

需要注意的是,禁止下半部时仍然可以被硬中断抢占。
(3)判断中断状态
#define in_interrupt() (irq_count()) // 是否处于中断状态(硬中断或软中断)

#define in_irq() (hardirq_count()) // 是否处于硬中断

#define in_softirq() (softirq_count()) // 是否处于软中断

三、硬中断
(1)注册中断处理函数
注册中断处理函数:
/**
* irq: 要分配的中断号
* handler: 要注册的中断处理函数
* flags: 标志(一般为0)
* name: 设备名(dev->name)
* dev: 设备(struct net_device *dev),作为中断处理函数的参数
* 成功返回0
*/

int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev);
中断处理函数本身:


typedef irqreturn_t (*irq_handler_t) (int, void *);

/**
* enum irqreturn
* @IRQ_NONE: interrupt was not from this device
* @IRQ_HANDLED: interrupt was handled by this device
* @IRQ_WAKE_THREAD: handler requests to wake the handler thread
*/
enum irqreturn {
IRQ_NONE,
IRQ_HANDLED,
IRQ_WAKE_THREAD,
};
typedef enum irqreturn irqreturn_t;
#define IRQ_RETVAL(x) ((x) != IRQ_NONE)

(2)注销中断处理函数
/**
* free_irq - free an interrupt allocated with request_irq
* @irq: Interrupt line to free
* @dev_id: Device identity to free
*
* Remove an interrupt handler. The handler is removed and if the
* interrupt line is no longer in use by any driver it is disabled.
* On a shared IRQ the caller must ensure the interrupt is disabled
* on the card it drives before calling this function. The function does
* not return until any executing interrupts for this IRQ have completed.
* This function must not be called from interrupt context.
*/

void free_irq(unsigned int irq, void *dev_id);

四、软中断
(1)定义
软中断是一组静态定义的下半部接口,可以在所有处理器上同时执行,即使两个类型相同也可以。
但一个软中断不会抢占另一个软中断,唯一可以抢占软中断的是硬中断。
软中断由softirq_action结构体实现:
struct softirq_action {
void (*action) (struct softirq_action *); /* 软中断的处理函数 */
};
目前已注册的软中断有10种,定义为一个全局数组:

static struct softirq_action softirq_vec[NR_SOFTIRQS];

enum {
HI_SOFTIRQ = 0, /* 优先级高的tasklets */
TIMER_SOFTIRQ, /* 定时器的下半部 */
NET_TX_SOFTIRQ, /* 发送网络数据包 */
NET_RX_SOFTIRQ, /* 接收网络数据包 */
BLOCK_SOFTIRQ, /* BLOCK装置 */
BLOCK_IOPOLL_SOFTIRQ,
TASKLET_SOFTIRQ, /* 正常优先级的tasklets */
SCHED_SOFTIRQ, /* 调度程序 */
HRTIMER_SOFTIRQ, /* 高分辨率定时器 */
RCU_SOFTIRQ, /* RCU锁定 */
NR_SOFTIRQS /* 10 */
};

(2)注册软中断处理函数
/**
* @nr: 软中断的索引号
* @action: 软中断的处理函数
*/

void open_softirq(int nr, void (*action) (struct softirq_action *))
{
softirq_vec[nr].action = action;
}
例如:

open_softirq(NET_TX_SOFTIRQ, net_tx_action);

open_softirq(NET_RX_SOFTIRQ, net_rx_action);

(3)触发软中断
&emsp;&emsp;调用raise_softirq()来触发软中断。

void raise_softirq(unsigned int nr)
{
unsigned long flags;
local_irq_save(flags);
raise_softirq_irqoff(nr);
local_irq_restore(flags);
}

/* This function must run with irqs disabled */
inline void rasie_softirq_irqsoff(unsigned int nr)
{
__raise_softirq_irqoff(nr);

/* If we're in an interrupt or softirq, we're done
* (this also catches softirq-disabled code). We will
* actually run the softirq once we return from the irq
* or softirq.
* Otherwise we wake up ksoftirqd to make sure we
* schedule the softirq soon.
*/
if (! in_interrupt()) /* 如果不处于硬中断或软中断 */
wakeup_softirqd(void); /* 唤醒ksoftirqd/n进程 */
}
Percpu变量irq_cpustat_t中的__softirq_pending是等待处理的软中断的位图,通过设置此变量即可告诉内核该执行哪些软中断。

static inline void __rasie_softirq_irqoff(unsigned int nr)
{
trace_softirq_raise(nr);
or_softirq_pending(1UL << nr);
}

typedef struct {
unsigned int __softirq_pending;
unsigned int __nmi_count; /* arch dependent */
} irq_cpustat_t;

irq_cpustat_t irq_stat[];
#define __IRQ_STAT(cpu, member) (irq_stat[cpu].member)
#define or_softirq_pending(x) percpu_or(irq_stat.__softirq_pending, (x))
#define local_softirq_pending() percpu_read(irq_stat.__softirq_pending)

唤醒ksoftirqd内核线程处理软中断。
static void wakeup_softirqd(void)
{
/* Interrupts are disabled: no need to stop preemption */
struct task_struct *tsk = __get_cpu_var(ksoftirqd);

if (tsk && tsk->state != TASK_RUNNING)
wake_up_process(tsk);
}

在下列地方,待处理的软中断会被检查和执行:
a. 从一个硬件中断代码处返回时
b. 在ksoftirqd内核线程中
c. 在那些显示检查和执行待处理的软中断的代码中,如网络子系统中
而不管是用什么方法唤起,软中断都要在do_softirq()中执行。如果有待处理的软中断,do_softirq()会循环遍历每一个,调用它们的相应的处理程序。
在中断处理程序中触发软中断是最常见的形式。中断处理程序执行硬件设备的相关操作,然后触发相应的软中断,最后退出。内核在执行完中断处理程序以后,马上就会调用do_softirq(),于是软中断开始执行中断处理程序完成剩余的任务。
下面来看下do_softirq()的具体实现。
asmlinkage void do_softirq(void)
{
__u32 pending;
unsigned long flags;

/* 如果当前已处于硬中断或软中断中,直接返回 */
if (in_interrupt())
return;

local_irq_save(flags);
pending = local_softirq_pending();
if (pending) /* 如果有激活的软中断 */
__do_softirq(); /* 处理函数 */
local_irq_restore(flags);
}
/* We restart softirq processing MAX_SOFTIRQ_RESTART times,
* and we fall back to softirqd after that.
* This number has been established via experimentation.
* The two things to balance is latency against fairness - we want
* to handle softirqs as soon as possible, but they should not be
* able to lock up the box.
*/
asmlinkage void __do_softirq(void)
{
struct softirq_action *h;
__u32 pending;
/* 本函数能重复触发执行的次数,防止占用过多的cpu时间 */
int max_restart = MAX_SOFTIRQ_RESTART;
int cpu;

pending = local_softirq_pending(); /* 激活的软中断位图 */
account_system_vtime(current);
/* 本地禁止当前的软中断 */
__local_bh_disable((unsigned long)__builtin_return_address(0), SOFTIRQ_OFFSET);
lockdep_softirq_enter(); /* current->softirq_context++ */
cpu = smp_processor_id(); /* 当前cpu编号 */

restart:
/* Reset the pending bitmask before enabling irqs */
set_softirq_pending(0); /* 重置位图 */
local_irq_enable();
h = softirq_vec;
do {
if (pending & 1) {
unsigned int vec_nr = h - softirq_vec; /* 软中断索引 */
int prev_count = preempt_count();
kstat_incr_softirqs_this_cpu(vec_nr);

trace_softirq_entry(vec_nr);
h->action(h); /* 调用软中断的处理函数 */
trace_softirq_exit(vec_nr);

if (unlikely(prev_count != preempt_count())) {
printk(KERN_ERR "huh, entered softirq %u %s %p" "with preempt_count %08x,"
"exited with %08x?\n", vec_nr, softirq_to_name[vec_nr], h->action, prev_count,
preempt_count());
}
rcu_bh_qs(cpu);
}
h++;
pending >>= 1;
} while(pending);

local_irq_disable();
pending = local_softirq_pending();
if (pending & --max_restart) /* 重复触发 */
goto restart;

/* 如果重复触发了10次了,接下来唤醒ksoftirqd/n内核线程来处理 */
if (pending)
wakeup_softirqd();

lockdep_softirq_exit();
account_system_vtime(current);
__local_bh_enable(SOFTIRQ_OFFSET);
}

(4)ksoftirqd内核线程
内核不会立即处理重新触发的软中断。当大量软中断出现的时候,内核会唤醒一组内核线程来处理。这些线程的优先级最低(nice值为19),这能避免它们跟其它重要的任务抢夺资源。但它们最终肯定会被执行,所以这个折中的方案能够保证在软中断很多时用户程序不会因为得不到处理时间而处于饥饿状态,同时也保证过量的软中断最终会得到处理。
每个处理器都有一个这样的线程,名字为ksoftirqd/n,n为处理器的编号。
static int run_ksoftirqd(void *__bind_cpu)
{
set_current_state(TASK_INTERRUPTIBLE);
current->flags |= PF_KSOFTIRQD; /* I am ksoftirqd */

while(! kthread_should_stop()) {
preempt_disable();

if (! local_softirq_pending()) { /* 如果没有要处理的软中断 */
preempt_enable_no_resched();
schedule();
preempt_disable():
}

__set_current_state(TASK_RUNNING);

while(local_softirq_pending()) {
/* Preempt disable stops cpu going offline.
* If already offline, we'll be on wrong CPU: don't process.
*/
if (cpu_is_offline(long)__bind_cpu))/* 被要求释放cpu */
goto wait_to_die;

do_softirq(); /* 软中断的统一处理函数 */

preempt_enable_no_resched();
cond_resched();
preempt_disable();
rcu_note_context_switch((long)__bind_cpu);
}

preempt_enable();
set_current_state(TASK_INTERRUPTIBLE);
}

__set_current_state(TASK_RUNNING);
return 0;

wait_to_die:
preempt_enable();
/* Wait for kthread_stop */
set_current_state(TASK_INTERRUPTIBLE);
while(! kthread_should_stop()) {
schedule();
set_current_state(TASK_INTERRUPTIBLE);
}

__set_current_state(TASK_RUNNING);
return 0;
}


关于作者本硕双非,参加了2020年秋招和2021年的春招,投递岗位是嵌入式软件(驱动)相关。总共收获Oppo,小米,海康威视,兆易创新,全志科技等十余家公司的offer。 我把自己的秋招,春招历程详细记录了下来,同时,把自己秋招过程总结的笔试面试资料分享了出来,即嵌入式软件工程师笔试面试指南。关注我的知乎专栏即可获取PDF版本。嵌入式软件工程师笔试面试指南


C++|并发|libco协程剖析

C++20推出了官方的协程库,但是在此之前C++并没有提供协程语法。libco是经典的C++协程库,本文将从源码角度分析libco,并参考了原作者的文章。

(据C++20 Coroutine 性能测试说libco被coroutine吊锤)

Reference: SJTU,IPADS,OS,07-process
腾讯技术工程:万字长文 | 漫谈libco协程设计及实现Coroutine

协程本质上就是用户态线程,又名纤程,将调度的代码在用户态重新实现。将内核态代码移到用户态其实是常见的思路了,例如驱动的libos,网络的dpdk,乃至于微内核。

对于线程而言,其上下文切换流程如下,需要两次权限等级切换和三次栈切换。上下文存储在内核栈上。

从时间角度:

线程的上下文切换必须先进入内核态并切换上下文, 这就造成了严重的调度开销内核态和用户态存在页表隔离,用于防止meltdown攻击,在ARM中通过ttbr实现线程的调度算法是通用的,对于内核而言,它会以公平的方式(CFS)进行调度,而某些时候如果我们利用用户态的信息自主调度能够做出更好的决策。线程的结构体存在于内核中,在pthread_create时需要进入内核态,频繁创建开销大

从空间角度:

线程的栈空间通常在MB级别,而服务器往往只是无状态地转发,并不需要这么大的栈空间线程利用TCB存储上下文和调度状态,可能存在冗余的信息

因此,从并发的角度看,协程是更好的并发模型。

linux根据POSIX标准提供了ucontext库支持原生协程,但是在POSIX.1-2008中被废除了。大概是因为协程在语言级别就能实现,所以没必要扔系统层,KISS?

makecontext(ucontext_t *ucp, void (*func)(), int argc, ...) - 创建ucontext并赋予栈空间setcontext(const ucontext_t *ucp) - 从ucp获取ucontext并设置上下文getcontext(ucontext_t*ucp) - 保存当前的ucontext在ucp中swapcontext(ucontext_t*oucp, ucontext_t*ucp)- 相当于get然后set其他协程生产者直接调度至消费者,最优调度Context Switch

上下文切换是调度的核心,在libco中通过coctx_swap.S汇编实现,这段代码还是挺有趣的。

对应swapcontext(ucontext_t*oucp, ucontext_t*ucp)

结构体声明

印证了上面提到的TCB冗余,这里的上下文仅仅保存了除了r10和r11之外的通用寄存器、栈的大小、栈低位指针(最大栈顶)。

struct coctx_t { void *regs[ 14 ]; size_t ss_size; char *ss_sp; }; // 64 bit // low | regs[0]: r15 | // | regs[1]: r14 | // | regs[2]: r13 | // | regs[3]: r12 | // | regs[4]: r9 | // | regs[5]: r8 | // | regs[6]: rbp | // | regs[7]: rdi | // | regs[8]: rsi | // | regs[9]: ret | //ret func addr // | regs[10]: rdx | // | regs[11]: rcx | // | regs[12]: rbx | // hig | regs[13]: rsp |

函数声明

rdi寄存器是当前上下文地址,rsi寄存器是目标上下文地址,X86传参机制

extern "C" { extern void coctx_swap( coctx_t *,coctx_t* ) asm("coctx_swap"); };

保存上下文

首先将rsp移到rax,然后将通用寄存器存入regs。值得注意的是这里两次压入的rax分别是rsp和返回地址RA。没有压入rax的原因在于rax按照约定是调用后放返回值的,所以没必要保存。

leaq (%rsp),%rax movq %rax, 104(%rdi) movq %rbx, 96(%rdi) movq %rcx, 88(%rdi) movq %rdx, 80(%rdi) movq 0(%rax), %rax movq %rax, 72(%rdi) movq %rsi, 64(%rdi) movq %rdi, 56(%rdi) movq %rbp, 48(%rdi) movq %r8, 40(%rdi) movq %r9, 32(%rdi) movq %r12, 24(%rdi) movq %r13, 16(%rdi) movq %r14, 8(%rdi) movq %r15, (%rdi) xorq %rax, %rax

设置上下文

然后从目标上下文取出所有通用寄存器,并将返回地址RA压入栈,从而能够在ret时根据函数调用机制跳转到coctx_swap后的指令。

这里的leaq 8(%rsp)本质上是在对返回地址进行出栈操作,从而恢复到存入上下文之前的栈,然后通过pushq 72(%rsi)入栈伪造的返回地址,在ret时跳转到另一个协程中。

如果看过ROP(Return-Orient-Programming)或者玩过栈溢出的话对这个应该会很眼熟,所以学安全还是很有用的。

movq 48(%rsi), %rbp movq 104(%rsi), %rsp movq (%rsi), %r15 movq 8(%rsi), %r14 movq 16(%rsi), %r13 movq 24(%rsi), %r12 movq 32(%rsi), %r9 movq 40(%rsi), %r8 movq 56(%rsi), %rdi movq 80(%rsi), %rdx movq 88(%rsi), %rcx movq 96(%rsi), %rbx leaq 8(%rsp), %rsp pushq 72(%rsi) movq 64(%rsi), %rsi retContext Create

创建上下文中在libco中通过coctx.cpp实现,初始化上下文的栈和跳转点

对应makecontext(ucontext_t *ucp, void (*func)(), int argc, ...)

int coctx_make(coctx_t* ctx, coctx_pfn_t pfn, const void* s, const void* s1) { char* sp = ctx->ss_sp + ctx->ss_size - sizeof(void*); sp = (char*)((unsigned long)sp & -16LL); memset(ctx->regs, 0, sizeof(ctx->regs)); void** ret_addr = (void**)(sp); *ret_addr = (void*)pfn; ctx->regs[kRSP] = sp; ctx->regs[kRETAddr] = (char*)pfn; ctx->regs[kRDI] = (char*)s; ctx->regs[kRSI] = (char*)s1; return 0; }

协程的sp分为两种,主协程的sp在栈上(也就是原本的线程所持有的系统栈),其他协程的sp在堆上。第一次调度时,需要通过coctx_make完成context的初始化,之后通过swapcontext自动进行上下文切换。

在这里,栈的最高位是(ctx->ss_sp + ctx->ss_size -sizeof(void*)) & -16LL,最低位是ctx->ss_sp,也就是在大小为ss_size的堆上形成的协程栈。

通用寄存器初始均为0

memset(ctx->regs, 0, sizeof(ctx->regs));

下面这段代码相当于push RA,& -16LL 进行内存对齐(后四位置0)

char* sp = ctx->ss_sp + ctx->ss_size - sizeof(void*); sp = (char*)((unsigned long)sp & -16LL); void** ret_addr = (void**)(sp); *ret_addr = (void*)pfn;

然后将rsp设置为栈底,ra设置为入口函数pfn,传入的参数作为入口函数的参数rdi,rsi。

typedef void* (*coctx_pfn_t)( void* s, void* s2 ); 调度

libco专为epoll服务,co_epoll.cpp就是对epoll做了点封装,co_hook_syscall.cpp对read/write做了些封装。

hook NIO后协程将会不进行阻塞,而是通过poll挂载在epoll协程上(yield),当poll就绪或者超时后再恢复协程执行(resume)。无视他们,主要看co_routine.cpp。

协程结构体

协程默认有128K的协程栈在stack_mem中,ctx表示上下文,pfn和arg分别为当前函数和参数,同时还有4K的私有变量数组aSpec(key是索引)。所以创建协程的开销很大,为了避免开销,从协程池取出协程后只需把pfn重置为协程入口函数即可。

struct stCoRoutine_t { stCoRoutineEnv_t *env; pfn_co_routine_t pfn; void *arg; coctx_t ctx; char cStart; char cEnd; char cIsMain; char cEnableSysHook; char cIsShareStack; void *pvEnv; //char sRunStack[ 1024 * 128 ]; stStackMem_t* stack_mem; //save satck buffer while confilct on same stack_buffer; char* stack_sp; unsigned int save_size; char* save_buffer; stCoSpec_t aSpec[1024]; };

环境结构体

struct stCoRoutineEnv_t { stCoRoutine_t *pCallStack[ 128 ]; int iCallStackSize; stCoEpoll_t *pEpoll; //for copy stack log lastco and nextco stCoRoutine_t* pending_co; stCoRoutine_t* occupy_co; }; stCoRoutine_t *GetCurrCo( stCoRoutineEnv_t *env ) { return env->pCallStack[ env->iCallStackSize - 1 ]; }

环境表征递归调用的协程,深度最大为128。pCallStack的每个元素均为协程,通过size获取当前的递归深度,GetCurrCo获取当前的协程(吐槽一下,后面就没怎么看到这玩意儿被调用,inline不香么)。但是常规情况递归深度都不会很大。

协程切换

非共享栈的情况下很简单,直接context swap即可。

为了节约内存引入共享栈机制,用拷贝共享栈的时间换取每个协程栈的空间,共享栈的内存通常较大,视作许多协程栈的堆叠,每个协程栈无需占据128K而是根据size动态分配。

如果即将执行的协程并不是共享栈的持有者,则让共享栈的持有者将自己的栈(在共享栈顶)存入buffer中,然后将共享栈转移给即将执行的协程。

在协程切换完成后,即将执行的协程将自己的栈从buffer中取出并复制到共享栈中。

void co_swap(stCoRoutine_t* curr, stCoRoutine_t* pending_co) { stCoRoutineEnv_t* env = co_get_curr_thread_env(); //get curr stack sp char c; curr->stack_sp= &c; if (!pending_co->cIsShareStack) { env->pending_co = NULL; env->occupy_co = NULL; } else { env->pending_co = pending_co; //get last occupy co on the same stack mem stCoRoutine_t* occupy_co = pending_co->stack_mem->occupy_co; //set pending co to occupy thest stack mem; pending_co->stack_mem->occupy_co = pending_co; env->occupy_co = occupy_co; if (occupy_co && occupy_co != pending_co) { save_stack_buffer(occupy_co); } } //swap context coctx_swap(&(curr->ctx),&(pending_co->ctx) ); //stack buffer may be overwrite, so get again; stCoRoutineEnv_t* curr_env = co_get_curr_thread_env(); stCoRoutine_t* update_occupy_co = curr_env->occupy_co; stCoRoutine_t* update_pending_co = curr_env->pending_co; if (update_occupy_co && update_pending_co && update_occupy_co != update_pending_co) { //resume stack buffer if (update_pending_co->save_buffer && update_pending_co->save_size > 0) { memcpy(update_pending_co->stack_sp, update_pending_co->save_buffer, update_pending_co->save_size); } } } 首先resume Handler协程,然后执行Eventloop开始监听

Callback

从Epoll事件中获取Item,通过Item的回调函数来resume可继续执行的协程

stTimeoutItem_t *item = (stTimeoutItem_t*)result->events[i].data.ptr; void OnPollProcessEvent( stTimeoutItem_t * ap ) { stCoRoutine_t *co = (stCoRoutine_t*)ap->pArg; co_resume( co ); }

Resume

要执行协程时,协程调用深度++,然后切换协程。

顺便吐槽一句,明明有了GetCurrCo为啥不直接inline呢。

void co_resume( stCoRoutine_t *co ) { stCoRoutineEnv_t *env = co->env; stCoRoutine_t *lpCurrRoutine = env->pCallStack[ env->iCallStackSize - 1 ]; if( !co->cStart ) { coctx_make( &co->ctx,(coctx_pfn_t)CoRoutineFunc,co,0 ); co->cStart = 1; } env->pCallStack[ env->iCallStackSize++ ] = co; co_swap( lpCurrRoutine, co ); }

Yield

当协程完成后,执行流返回上层,将上下文切换到上层协程

顺便继续吐槽一句,明明有了GetCurrCo为啥不直接inline呢。

void co_yield_env( stCoRoutineEnv_t *env ) { stCoRoutine_t *last = env->pCallStack[ env->iCallStackSize - 2 ]; stCoRoutine_t *curr = env->pCallStack[ env->iCallStackSize - 1 ]; env->iCallStackSize--; co_swap( curr, last); } 总结

libco本质上是单线程(对应主协程),无法利用多核,仅仅是并发而非并行,所以要配合多进程多线程。(fork or pthread_create)。

Linux 中断/异常的准备与退出

本文讨论一下linux下x86平台关于中断/异常的准备与退出,主要关注点在于进入C语言部分前与退出C语言部分后汇编/C代码所处理的上下文切换部分。

关于内容的说明使用的内核版本为5.5.13主要介绍64位下的内核实现资料、引据都在最下方,文中对它们用到的地方可能有说明也可能只是概括一下,如果有兴趣可以去原出处细致了解如果发现文中有任何错误,不论是评论还是私信,希望能够指出(能有资料佐证更好),感激不尽!本文的讨论需要读者对以下内容有了解:基本的C语言中断、异常的基本概念保护模式的段、虚拟地址用户态与内核态的基本概念能看懂简单的x86汇编代码(AT&T语法)(Optional)TSS---Task State Segment的基本概念

OK,下面进入正文!

Overview

总体上来说,linux下中断/异常的处理模式大致是这样:

中断向量表中每个entry(也就是idt)对应于一个中断/异常,它记录了进行处理的handler的地址。每个handler我们都会定义一个标号,并把这个标号的值(也就是虚拟地址)写入这个idt中,图1:部分中断向量表实例图2:窥探下中断handler的定义(使用了汇编宏),后续会详细介绍这样一来,将中断向量表的起始地址写入对应的预留寄存器后,中断/异常发生时,硬件方面根据号码找到idt entry,执行必要的context switch(主要是寄存器)等任务后自动跳转到相应C函数。总的来说处理流程大致如下:(用户态/内核态)执行--(发生中断/异常)-->硬件context switch,并跳转到相应handler---->handler(汇编代码)执行必要的工作--(跳转到C语言代码内)-->处理中断/异常--(C语言代码返回,回到handler)-->handler(汇编代码)执行必要的工作---->回到用户态/内核态

黑体部分是本文关注点所在。

然而,在linux中,出于设计上的原因,异常/中断(还有系统调用,但这里按下不表)这些由用户态(以及可能的,内核态)进入内核态的过程在处理上多多少少有一些不同,因此本文将会对它们做一些区分,先分析对异常(exception)的处理,再据此分析中断(interrupt)中的不同之处,同时穿插一下为什么会有这些不同,窥探linux在设计上的思考。

异常---Exception

对处理过程的分析,很大程度上就是对handler实现的分析,由于不同handler的代码十分相似,它们的定义是通过汇编语言的宏来实现的。如上图2所示,宏的名字为idtentry,后续空格分隔的为不同参数。我们通过分析宏的定义来分析handler的实现(不懂宏的语法没关系,很正常,能看懂其意思即可)

1. 进入C语言前:

idtentry

我们先通过idtentry宏看看进入C语言前的准备工作:

// arch/x86/entry/entry_64.S .macro idtentry sym do_sym has_error_code:req paranoid=0 read_cr2=0 SYM_CODE_START(\sym) .if \has_error_code == 0 pushq $-1 /* ORIG_RAX: no syscall to restart */ .endif .if \paranoid == 1 testb $3, CS-ORIG_RAX(%rsp) /* If coming from userspace, switch stacks */ jnz .Lfrom_usermode_switch_stack_\@ .endif idtentry_part \do_sym, \has_error_code, \read_cr2, \paranoid .if \paranoid == 1 /* * Entry from userspace. Switch stacks and treat it * as a normal entry. This means that paranoid handlers * run in real process context if user_mode(regs). */ .Lfrom_usermode_switch_stack_\@: idtentry_part \do_sym, \has_error_code, \read_cr2, paranoid=0 .endif _ASM_NOKPROBE(\sym) SYM_CODE_END(\sym) .endm

这里我删去了一些非公共的参数以达到简化的目的。关于idtentry宏的参数:

sym:handler的名字,使用idtentry宏时会以此为名定义一个全局标号do_sym:handler要调用的处理中断/异常的C语言函数,也就是真正的中断/异常处理函数has_error_code:进入异常handler时硬件是否在栈上压入一个错误码paranoid:进入handler这一行为是内核发起的(=0)还是用户发起的(=1),默认为内核发起(=0)(除了极少部分如int3断点异常,其他大都是由内核检测到并发起的)read_cr2:是否要读取cr2寄存器并将其传入异常处理的C语言函数。主要是用于页错误异常,它会将引发页错误的虚拟地址保存在cr2寄存器中,handler需要将其传递给C语言函数

在这里,我们可以看到首先是保持栈结构,没有error code时额外压入一个error code:

.if \has_error_code == 0 pushq $-1 /* ORIG_RAX: no syscall to restart */ .endif

这里我们需要先简单介绍下这时的栈上的异常栈帧结构(此处由 @pretty kernel 提醒,64位与32位处理方式不同):

* | original ss | * | original rsp | * | original eflags | * | original cs | * | original ip | * | error_code/syscall_num/irq_n |<---- rsp

由下到上从低地址到高地址。这里的error_code(对中断来说它是中断号,对非sysenter/sysexit实现的系统调用来说它是系统调用号)就是我们在为空时要额外压入的部分。值得注意的是,如果进入handler前已经是内核态,那么实际上并没有必要保存ss、rsp,因为前后都是一致的,但在64位下我们仍置ss为NULL以及rsp的空白placeholder(关于此处x86给出的说明是[1],置SS为NULL以提示硬件无需restore当前的ss、rsp)。至此,我们仅在内核栈上保存了硬件自动保存的context。

在32位下,当进入handler前为内核态时,实际并不会保存ss与esp,栈帧结构为:
*| original eflags |
*| original cs |
*| original ip |
*| error_code/syscall_num/irq_n |<---- esp
而64位下之所以选择仍然要保存是考虑到以下因素[1]:
1. 对所有(不论嵌套与否)中断而言栈结构的连续性
2. 由此带来的固定16字节对齐(对诸如XMM的16字节指令性能的提升)

接下来我们可以看到,殊途同归,都是执行:

idtentry_part \do_sym, \has_error_code, \read_cr2, paranoid=0

对paranoid == 0来说,条件后的代码是:

idtentry_part \do_sym, \has_error_code, \read_cr2, \paranoid

对paranoid == 1来说,条件后的代码是:

testb $3, CS-ORIG_RAX(%rsp) /* If coming from userspace, switch stacks */ jnz .Lfrom_usermode_switch_stack_\@ idtentry_part \do_sym, \has_error_code, \read_cr2, \paranoid .Lfrom_usermode_switch_stack_\@: idtentry_part \do_sym, \has_error_code, \read_cr2, paranoid=0

这里用到了栈帧偏移量CS,ORIG_RAX,定义如下:

// arch/x86/entry/calling.h /* * 64-bit system call stack frame layout defines and helpers, * for assembly code: */ /* The layout forms the "struct pt_regs" on the stack: */ /* * C ABI says these regs are callee-preserved. They aren't saved on kernel entry * unless syscall needs a complete, fully filled "struct pt_regs". */ #define R15 0*8 #define R14 1*8 #define R13 2*8 #define R12 3*8 #define RBP 4*8 #define RBX 5*8 /* These regs are callee-clobbered. Always saved on kernel entry. */ #define R11 6*8 #define R10 7*8 #define R9 8*8 #define R8 9*8 #define RAX 10*8 #define RCX 11*8 #define RDX 12*8 #define RSI 13*8 #define RDI 14*8 /* * On syscall entry, this is syscall#. On CPU exception, this is error code. * On hw interrupt, it's IRQ number: */ #define ORIG_RAX 15*8 /* Return frame for iretq */ #define RIP 16*8 #define CS 17*8 #define EFLAGS 18*8 #define RSP 19*8 #define SS 20*8 #define SIZEOF_PTREGS 21*8

总之不管怎么样,都是带着paranoid=0执行idtentry_part宏(其实paranoid还可能取值为2,这里不讨论)。接下来我们展开identry_part。

identry_part

到这里,我们已经在栈上保存了硬件context和error_code。进入C语言之前,我们还剩下两件事:

保存通用寄存器,必要的话切换栈(后面会解释)在栈上准备参数以调用C语言函数

这一部分我们就是要解决这些问题(删去了debug追踪等部分,并按照paranoid=0展开):

// arch/x86/entry/entry_64.S .macro idtentry_part do_sym, has_error_code:req, read_cr2:req, paranoid:req call error_entry .if \read_cr2 /* * Store CR2 early so subsequent faults cannot clobber it. Use R12 as * intermediate storage as RDX can be clobbered in enter_from_user_mode(). * GET_CR2_INTO can clobber RAX. */ GET_CR2_INTO(%r12); .endif movq %rsp, %rdi /* pt_regs pointer */ .if \has_error_code movq ORIG_RAX(%rsp), %rsi /* get error code */ movq $-1, ORIG_RAX(%rsp) /* no syscall to restart */ .else xorl %esi, %esi /* no error code */ .endif .if \read_cr2 movq %r12, %rdx /* Move CR2 into 3rd argument */ .endif call \do_sym jmp error_exit .endm

在error_entry中,我们保存通用寄存器并在必要时切换栈:

// arch/x86/entry/entry_64.S /* * Save all registers in pt_regs, and switch GS if needed. */ SYM_CODE_START_LOCAL(error_entry) cld PUSH_AND_CLEAR_REGS save_ret=1 /* * We entered from user mode or we're pretending to have entered * from user mode due to an IRET fault. */ SWAPGS FENCE_SWAPGS_USER_ENTRY /* We have user CR3. Change to kernel CR3. */ SWITCH_TO_KERNEL_CR3 scratch_reg=%rax .Lerror_entry_from_usermode_after_swapgs: /* Put us onto the real thread stack. */ popq %r12 /* save return addr in %12 */ movq %rsp, %rdi /* arg0 = pt_regs pointer */ call sync_regs movq %rax, %rsp /* switch stack */ pushq %r12 ret

在这里,首先是保存并清除所有寄存器,也就是PUSH_AND_CLEAR_REGS save_ret=1,这里仍然是一个宏:

// arch/x86/entry/entry_64.S .macro PUSH_AND_CLEAR_REGS rdx=%rdx rax=%rax save_ret=0 /* * Push registers and sanitize registers of values that a * speculation attack might otherwise want to exploit. The * lower registers are likely clobbered well before they * could be put to use in a speculative execution gadget. * Interleave XOR with PUSH for better uop scheduling: */ .if \save_ret pushq %rsi /* pt_regs->si */ movq 8(%rsp), %rsi /* temporarily store the return address in %rsi */ movq %rdi, 8(%rsp) /* pt_regs->di (overwriting original return address) */ .else pushq %rdi /* pt_regs->di */ pushq %rsi /* pt_regs->si */ .endif pushq \rdx /* pt_regs->dx */ xorl %edx, %edx /* nospec dx */ pushq %rcx /* pt_regs->cx */ xorl %ecx, %ecx /* nospec cx */ pushq \rax /* pt_regs->ax */ pushq %r8 /* pt_regs->r8 */ xorl %r8d, %r8d /* nospec r8 */ pushq %r9 /* pt_regs->r9 */ xorl %r9d, %r9d /* nospec r9 */ pushq %r10 /* pt_regs->r10 */ xorl %r10d, %r10d /* nospec r10 */ pushq %r11 /* pt_regs->r11 */ xorl %r11d, %r11d /* nospec r11*/ pushq %rbx /* pt_regs->rbx */ xorl %ebx, %ebx /* nospec rbx*/ pushq %rbp /* pt_regs->rbp */ xorl %ebp, %ebp /* nospec rbp*/ pushq %r12 /* pt_regs->r12 */ xorl %r12d, %r12d /* nospec r12*/ pushq %r13 /* pt_regs->r13 */ xorl %r13d, %r13d /* nospec r13*/ pushq %r14 /* pt_regs->r14 */ xorl %r14d, %r14d /* nospec r14*/ pushq %r15 /* pt_regs->r15 */ xorl %r15d, %r15d /* nospec r15*/ UNWIND_HINT_REGS .if \save_ret pushq %rsi /* return address on top of stack */ .endif .endm

这里的save_ret主要是控制是否要将返回地址转移到栈顶。具体地说,由于我们保存寄存器主要是想构造一个struct pt_regs结构给C语言函数:

// /usr/include/x86_64-linux-gnu/asm/ptrace.h struct pt_regs { /* * C ABI says these regs are callee-preserved. They aren't saved on kernel entry * unless syscall needs a complete, fully filled "struct pt_regs". */ unsigned long r15; unsigned long r14; unsigned long r13; unsigned long r12; unsigned long rbp; unsigned long rbx; /* These regs are callee-clobbered. Always saved on kernel entry. */ unsigned long r11; unsigned long r10; unsigned long r9; unsigned long r8; unsigned long rax; unsigned long rcx; unsigned long rdx; unsigned long rsi; unsigned long rdi; /* * On syscall entry, this is syscall#. On CPU exception, this is error code. * On hw interrupt, it's IRQ number: */ unsigned long orig_rax; /* Return frame for iretq */ unsigned long rip; unsigned long cs; unsigned long eflags; unsigned long rsp; unsigned long ss; /* top of stack page */ };

但在这里,我们在进入PUSH_AND_CLEAR_REGS宏之前的栈内容是这样的:

* | original ss | * | original rsp | * | original eflags | * | original cs | * | original ip | * | error_code/syscall_num/irq_n | * | return address from 'call error_entry' |<---- rsp

也就是,我们call error_entry时在栈上插入了一个返回地址(call convention)[2],如果要构造struct pt_regs结构,需要把这个返回地址迁移到保存完寄存器后的栈顶。因此,这里就先将其暂存在%rsi中,最后再压入栈顶。(对于没有这个问题的调用者来说,直接保存寄存器即可)。

接下来的这部分代码:

/* * We entered from user mode or we're pretending to have entered * from user mode due to an IRET fault. */ SWAPGS FENCE_SWAPGS_USER_ENTRY /* We have user CR3. Change to kernel CR3. */ SWITCH_TO_KERNEL_CR3 scratch_reg=%rax

切换了GS寄存器,切换到进程的内核态cr3(中间的FENCE_SWAPGS_USER_ENTRY加了一个memory barrier[3]防止后续访问gs段数据的代码被乱序执行以致读取到用户gs段的数据)。

切换GS寄存器主要是因为在用户态GS寄存器是预留给用户自定义使用的,而在linux内核中,GS主要是用于per-cpu variable数据段[4](紧接着就会用到,后面解释用途),因此需要切换到内核自己的GS寄存器内容而“切换到进程的内核态cr3”听起来似乎有些绕口,什么叫“内核态cr3”,cr3也分内核态和用户态吗?实际上在早期是没有这个问题的,每个进程只有一个cr3(cr3存放顶级页目录的起始物理地址),而每个进程的顶级页目录都有对内核代码段、数据段的映射项,只是访问权限为ring0,因此进入内核态无需切换cr3内容,权限等级自然地允许其访问内核部分。但是,由于近几年出现的集中内核旁路攻击机制,如Meltdown[5],使得linux应当尽量避免在每个用户的页目录中包含内核映射,因此linux采用了KPTI[6](Kernel page-table isolation)对每个进程的内核态和用户态各分配一个顶级页目录,前者与原先(不采用该技术时)相同,后者对内核态映射只包含了进入内核所必须的一小部分(这些都可以再开一篇文章写了)。综上,进入内核态不久我们要先切换一次cr3使用该进程的内核态顶级页目录(有点拗口)。这里我们digress一下,考虑关于内核栈的设计问题:
1. 我们首先必然要区分用户栈和内核栈以达到起码的保护目的[7],在进入内核态前,如果用户态将栈指针放在一个恶意区域(比如内核地址空间等等),那么内核就会轻易地覆写不该写入的区域。
2. 再之,对于linux来说,给每个进程分配一个内核栈也是非常必要的,一方面进程进行系统调用时可能会阻塞在内核态(比如等待用户输入等等),此时进程在内核态的状态需要保留在内核态上,切换到其他进程(内核可抢占),假如其他进程没有自己的内核栈,则栈上又会压入其他进程的使用数据,这样一来原进程状态就无法恢复[8];另一方面,为了提高进程响应速度,linux内核是可抢占的,也就是说,进程即便不是在执行系统调用而是在处理异常/中断,只要不是在time-critical region,进程也可能会被切换,也就是其他进程执行时发生中断/异常也可能触发切换到某个进程,为了确保进程状态即便是在内核态也能够恢复,内核栈内容仍然需要被保存[9];除此之外,给每个进程分配一个内核栈使得进程从一个cpu迁移到另一个cpu变得十分便利[9],因为进程内核栈上保存了context info,只需要保存栈指针等等的信息到进程的descriptor,其他cpu就可以轻易地恢复进程,而无需拷贝栈内容。
3. 最后,退出内核态时,当前进程的内核栈应当是全部清空的。

有了上面的分析,我们知道,每个进程都有一个内核栈,而按照我们说的,这一部分要保存寄存器和切换栈,保存寄存器已经做完了,这里的“切换栈”是什么意思呢?我们已经在进程的内核栈上了,有什么可切换的?这需要涉及到TSS在64位linux下的作用。

由于linux是采用software context switch[10],在进行context switch时大部分上下文是手动切换的,其中包括进程的内核栈指针。64位mode的TSS[11]中给ring0、1、2各reserve了一个栈指针位置,用来保存当由当前进程进入这些权限等级时要切换的栈指针:

图3:64位 TSS格式图4:linux中对应的结构

原本按照这个思路,当前进程的内核栈,应该是保存在sp0的,然后每次进程task switch的时候将新进程的内核栈指针(保存在进程描述符中)加载到sp0中,当中断、异常等触发用户态到内核态的转移时,自动切换栈指针到sp0处并将用户态的ss、sp、cs、ip等压入内核栈中。从这个角度来看,有什么可切换栈的?我们现在不就在进程内核栈上吗?要切换到哪去?

操作方面是这样:我们为每个cpu引入了一个trampoline stack,将其设置为sp0,所有进程共用,而将原本sp0应存放的进程内核栈指针放在sp1,这样硬件在进入内核时仍然读取sp0,因此首次进入内核时会进入这个trampoline stack上,而后再进行栈的转换。(类似的,我们会看到,退出内核态时也会先切换到trampoline stack再退出,但那里有其他考量)

关于为什么要在内核的入口点引入trampoline,当时的考量是希望在触及当前进程的内核栈前能够做一些其他工作[12](个人推测也是为了避免在用户态页表中存在内核栈的映射,只保留trampoline stack的映射即可),对我们来说,我们只需要知道,我们目前需要一次栈切换,代码如下:

// arch/x86/entry/entry_64.S .Lerror_entry_from_usermode_after_swapgs: /* Put us onto the real thread stack. */ popq %r12 /* save return addr in %12 */ movq %rsp, %rdi /* arg0 = pt_regs pointer */ call sync_regs movq %rax, %rsp /* switch stack */ pushq %r12 ret // arch/x86/kernel/traps.c /* * Help handler running on a per-cpu (IST or entry trampoline) stack * to switch to the normal thread stack if the interrupted code was in * user mode. The actual stack switch is done in entry_64.S */ asmlinkage __visible notrace struct pt_regs *sync_regs(struct pt_regs *eregs) { struct pt_regs *regs = (struct pt_regs *)this_cpu_read(cpu_current_top_of_stack) - 1; if (regs != eregs) *regs = *eregs; return regs; } // arch/x86/include/asm/processor.h #define cpu_current_top_of_stack cpu_tss_rw.x86_tss.sp1 struct x86_hw_tss { // ... u64 sp0; /* * We store cpu_current_top_of_stack in sp1 so it's always accessible. * Linux does not use ring 1, so sp1 is not otherwise needed. */ u64 sp1; // ... } __attribute__((packed));

这里,我们首先要找到当前进程的内核栈才能将内容复制过去,因此我们使用cpu_current_top_of_stack来找到当前进程的内核栈栈顶。注意这里包了一个this_cpu_read宏,具体来说它会把当前变量作为一个offset,以gs寄存器中的base为基址,再加上编译期计算出来的本cpu的per-cpu area offset,来对传入的变量进行重定位,从而获得其在本cpu下的虚拟地址(也就是这个cpu的local variable,毕竟linux下tss是每个cpu各一个的,sp1自然也是如此)。因此,重载gs是必需的,重载cr3是必需的(切换到完全的内核映射),加barrier也是必需的(防止读操作被reorder到切换gs前)。这里我们也可以看到,sync_regs通过指针运算,预留出一个struct pt_regs大小的区域,如果是用户态到内核态的切换,则两者地址(我们在trampoline stack上,而sp1是进程内核栈)不同,进行拷贝,如果是内核态到内核态的切换,sp1就是进入handler前的栈指针,offset一个struct pt_regs后恰好会与传入的地址相同,从而无需拷贝。

最后,函数返回值在%rax中,将其mov到%rsp中就真正实现了栈的切换。同时在代码周围,我们save和restore了当前的返回地址,ret正常执行。

我们完成了寄存器的保存和栈的切换,剩余的工作相对就比较简单了--准备参数:

.if \read_cr2 /* * Store CR2 early so subsequent faults cannot clobber it. Use R12 as * intermediate storage as RDX can be clobbered in enter_from_user_mode(). * GET_CR2_INTO can clobber RAX. */ GET_CR2_INTO(%r12); .endif movq %rsp, %rdi /* pt_regs pointer */ .if \has_error_code movq ORIG_RAX(%rsp), %rsi /* get error code */ movq $-1, ORIG_RAX(%rsp) /* no syscall to restart */ .else xorl %esi, %esi /* no error code */ .endif .if \read_cr2 movq %r12, %rdx /* Move CR2 into 3rd argument */ .endif

这里因为我们是异常处理而且有错误码,原则上来说不应该是由系统调用触发的,因此save了-1。仍然是根据linux kernel call convention[2],前三个参数分别保存在%rdi,%rsi和%rdx中。这里我们首先根据read_cr2看是否需要保存cr2,再将相应的内容移入相应寄存器中。接着就到了C语言部分了:

call \do_sym

2. 退出C语言后:

jmp error_exit SYM_CODE_START_LOCAL(error_exit) DISABLE_INTERRUPTS(CLBR_ANY) testb $3, CS(%rsp) jz retint_kernel jmp .Lretint_user SYM_CODE_END(error_exit)

这里为了确保我们这部分操作的安全,首先是关中断,实际展开如下:

// arch/x86/include/asm/irqflags.h #define DISABLE_INTERRUPTS(x) cli

接着,我们就是要检测一下我们即将要返回的是用户态还是内核态,分别跳转。

返回到内核态

我们先看一下内核态:

// arch/x86/entry/entry_64.S /* Returning to kernel space */ retint_kernel: /* Interrupts are off */ /* Check if we need preemption */ btl $9, EFLAGS(%rsp) /* were interrupts off? */ jnc 1f cmpl $0, PER_CPU_VAR(__preempt_count) jnz 1f call preempt_schedule_irq 1: SYM_INNER_LABEL(restore_regs_and_return_to_kernel, SYM_L_GLOBAL) POP_REGS addq $8, %rsp /* skip regs->orig_ax */ /* * ARCH_HAS_MEMBARRIER_SYNC_CORE rely on IRET core serialization * when returning from IPI handler. */ INTERRUPT_RETURN

这里我们的行为是,当且仅当之前的context开启中断又enable了内核抢占的情况下,才触发重新调度,否则直接恢复寄存器iret到之前的状态。

这里的两个条件我们要解释一下:1、为什么之前的context会存在关闭中断的情况?如果说我们当前是在中断的执行过程中,这(i.e. 之前的context表明其中断被关闭)当然是不可能的(除非是NMI),但我们现在是异常的执行,如果用户在内核态关闭中断的情况下发生了异常(如double fault[13]),就会使得这里test出原先的IF位为clear,在这种情况下,我们当然不应该触发调度。2、为什么要测试当前cpu的__preempt_count?这其实就是抢占的含义所在了,当且仅当__preempt_count为0时才说明当前是处于可抢占状态的,而可抢占的含义就是,之前被中断的进程处于内核态时被中断后允许触发调度来切换进程。如果这两个条件任意一个不满足,我们就直接执行恢复寄存器,执行返回:

// arch/x86/entry/calling.h .macro POP_REGS pop_rdi=1 skip_r11rcx=0 popq %r15 popq %r14 popq %r13 popq %r12 popq %rbp popq %rbx .if \skip_r11rcx popq %rsi .else popq %r11 .endif popq %r10 popq %r9 popq %r8 popq %rax .if \skip_r11rcx popq %rsi .else popq %rcx .endif popq %rdx popq %rsi .if \pop_rdi popq %rdi .endif .endm

而对异常来说,这个 pt_regs->orig_rax指的是错误码,这个信息我们没必要保留,直接drop掉就可以了,然后最后的INTERRUPT_RETURN其实就是iret(删去了不相关信息):

// arch\x86\include\asm\irqflags.h #define INTERRUPT_RETURN jmp native_iret // arch\x86\entry\entry_64.S SYM_INNER_LABEL_ALIGN(native_iret, SYM_L_GLOBAL) iretq

返回到之前的内核态。

上面分析的是关闭内核抢占的情形,而大多数情况下,内核抢占是打开的,因此我们看一下抢占调度:

// kernel/sched/core.c asmlinkage __visible void __sched preempt_schedule_irq(void) { enum ctx_state prev_state; prev_state = exception_enter(); do { preempt_disable(); local_irq_enable(); __schedule(true); local_irq_disable(); sched_preempt_enable_no_resched(); } while (need_resched()); exception_exit(prev_state); }

这里的prev_state保存只是进行tracking,我们略去不看,可以看到,这里的抢占调度是进行了一个循环的,我们先忽略循环,先看看每一次都干了什么。

首先是关掉抢占并打开中断。这里先说为什么要打开中断,主要还是尽可能避免关掉中断而带来的吞吐量下降,在任何可以打开中断的地方都尽可能地打开中断。而也因此,我们才需要在打开中断前关闭抢占,因为我们本身就是在进行抢占调度,应当避免在抢占调度执行过程中发生中断然后触发抢占调度,从而不得不考虑可重入以及死锁的问题。

接着就是__schedule进行调度,这就不展开了。退出调度后,我们依次重新关闭中断,打开抢占(这里的sched_preempt_enable_no_resched实际就是加了个barrier的打开抢占)。

这个过程会不断进行,只要在__schedule中又设置了当前进程的TIF_NEED_RESCHED位。等到彻底不需要调度(__schedule中没有设置这个位)时,我们退出抢占函数,接着之前分析的返回内核态的恢复工作。

至此,返回内核态路径上的情况我们分析完了。

返回到用户态

接下来我们分析一下返回用户态的情况:

// arch\x86\entry\entry_64.S .Lretint_user: mov %rsp,%rdi call prepare_exit_to_usermode SYM_INNER_LABEL(swapgs_restore_regs_and_return_to_usermode, SYM_L_GLOBAL) POP_REGS pop_rdi=0 /* * The stack is now user RDI, orig_ax, RIP, CS, EFLAGS, RSP, SS. * Save old stack pointer and switch to trampoline stack. */ movq %rsp, %rdi movq PER_CPU_VAR(cpu_tss_rw + TSS_sp0), %rsp /* Copy the IRET frame to the trampoline stack. */ pushq 6*8(%rdi) /* SS */ pushq 5*8(%rdi) /* RSP */ pushq 4*8(%rdi) /* EFLAGS */ pushq 3*8(%rdi) /* CS */ pushq 2*8(%rdi) /* RIP */ /* Push user RDI on the trampoline stack. */ pushq (%rdi) /* * We are on the trampoline stack. All regs except RDI are live. * We can do future final exit work right here. */ STACKLEAK_ERASE_NOCLOBBER SWITCH_TO_USER_CR3_STACK scratch_reg=%rdi /* Restore RDI. */ popq %rdi SWAPGS INTERRUPT_RETURN

来到返回用户态的路径上,首先是调用一个C语言函数进行准备(仍然是call convention第一个参数放入%rdi中),而后恢复寄存器(这里稍显复杂)回到用户态。首先看下准备工作:

// arch\x86\entry\common.c /* Called with IRQs disabled. */ __visible inline void prepare_exit_to_usermode(struct pt_regs *regs) { struct thread_info *ti = current_thread_info(); u32 cached_flags; cached_flags = READ_ONCE(ti->flags); if (unlikely(cached_flags & EXIT_TO_USERMODE_LOOP_FLAGS)) exit_to_usermode_loop(regs, cached_flags); /* Reload ti->flags; we may have rescheduled above. */ cached_flags = READ_ONCE(ti->flags); if (unlikely(cached_flags & _TIF_IO_BITMAP)) tss_update_io_bitmap(); if (unlikely(cached_flags & _TIF_NEED_FPU_LOAD)) switch_fpu_return(); }

在这里,首先是获取了当前cpu的进程(线程)信息放在ti中,紧接着开始检查是否需要进行“loop”。这里的loop是什么呢?我们打开看一下:

// arch\x86\entry\common.c #define EXIT_TO_USERMODE_LOOP_FLAGS \ (_TIF_SIGPENDING | _TIF_NOTIFY_RESUME | _TIF_UPROBE | \ _TIF_NEED_RESCHED | _TIF_USER_RETURN_NOTIFY | _TIF_PATCH_PENDING) static void exit_to_usermode_loop(struct pt_regs *regs, u32 cached_flags) { /* * In order to return to user mode, we need to have IRQs off with * none of EXIT_TO_USERMODE_LOOP_FLAGS set. Several of these flags * can be set at any time on preemptible kernels if we have IRQs on, * so we need to loop. Disabling preemption wouldn't help: doing the * work to clear some of the flags can sleep. */ while (true) { /* We have work to do. */ local_irq_enable(); if (cached_flags & _TIF_NEED_RESCHED) schedule(); if (cached_flags & _TIF_UPROBE) uprobe_notify_resume(regs); if (cached_flags & _TIF_PATCH_PENDING) klp_update_patch_state(current); /* deal with pending signal delivery */ if (cached_flags & _TIF_SIGPENDING) do_signal(regs); if (cached_flags & _TIF_NOTIFY_RESUME) { clear_thread_flag(TIF_NOTIFY_RESUME); tracehook_notify_resume(regs); rseq_handle_notify_resume(NULL, regs); } if (cached_flags & _TIF_USER_RETURN_NOTIFY) fire_user_return_notifiers(); /* Disable IRQs and retry */ local_irq_disable(); cached_flags = READ_ONCE(current_thread_info()->flags); if (!(cached_flags & EXIT_TO_USERMODE_LOOP_FLAGS)) break; } }

这个地方可以比较直观地理解一下,因为在处理中断/异常的过程中,可能进程会收到信号(signal)、需要重新调度等等,因此这里所做的就是检查这些,执行必要的工作。之所以做成loop,是因为每一次处理都可能refresh这些flag,我们需要loop直到没有新的flag被设置。

最后,当这些信息处理完毕后,最后检查一下该进程的iobitmap是否被修改(这部分主要是关于进程的io访问权限的),修改过的话就进行相应的更新:

if (unlikely(cached_flags & _TIF_IO_BITMAP)) tss_update_io_bitmap();

最后,如有必要的话加载之前save的fpu状态(fpu主要是浮点运算方面的协处理器,有自己的状态寄存器等等):

if (unlikely(cached_flags & _TIF_NEED_FPU_LOAD)) switch_fpu_return();

当所有的这些准备工作做完后,我们开始切换栈和恢复寄存器。

在进入C语言的异常处理函数前,我们就分析过了栈的切换,并提到退出内核时仍然要用trampoline stack,但在这里的考量与之前不同,这里是为了给"kernel stack erasing"做准备以及别的工作[14]。

关于"kernel stack erasing",需要提到STACKLEAK[15]。通俗地说,我们希望在推出内核时将我们用过的内核栈内容擦除,防止后续的恶意系统调用利用它们。因此,我们需要将我们用过的内核栈内容抹除,因此我们首先要切换到trampoline stack上,再抹除我们用过的内核栈,并且切换回用户态cr3、gs,然后返回到用户态:

SYM_INNER_LABEL(swapgs_restore_regs_and_return_to_usermode, SYM_L_GLOBAL) POP_REGS pop_rdi=0 /* * The stack is now user RDI, orig_ax, RIP, CS, EFLAGS, RSP, SS. * Save old stack pointer and switch to trampoline stack. */ movq %rsp, %rdi movq PER_CPU_VAR(cpu_tss_rw + TSS_sp0), %rsp /* Copy the IRET frame to the trampoline stack. */ pushq 6*8(%rdi) /* SS */ pushq 5*8(%rdi) /* RSP */ pushq 4*8(%rdi) /* EFLAGS */ pushq 3*8(%rdi) /* CS */ pushq 2*8(%rdi) /* RIP */ /* Push user RDI on the trampoline stack. */ pushq (%rdi) /* * We are on the trampoline stack. All regs except RDI are live. * We can do future final exit work right here. */ STACKLEAK_ERASE_NOCLOBBER SWITCH_TO_USER_CR3_STACK scratch_reg=%rdi /* Restore RDI. */ popq %rdi SWAPGS INTERRUPT_RETURN

至此,用户态路径也分析完毕。

硬件中断---Hardware Interrupt

有了前面对异常处理的详细讨论,我们对中断的讨论会轻松很多。总的来说,硬件中断就其处理模式上来说分为三类:

APIC/SMP InterruptNormal InterruptSpurious Interrupt

最后的Spurious Interrupt意为“不期望发生的中断”,如硬件错误或者中断号根本没有设置处理程序等等,它们有统一的处理流程,我们就不讨论了,我们主要看前两者。

在这里我们可以先看一下关于它们的idt初始化方法:

图5:硬件中断idt初始化

可以看到,apic中断是基于表的,而normal和spurious中断是等间隔设置的,我们可以看一下apic的table:

图6:apic idt table

这里之所以会产生apic和normal的这种差别我们在各自介绍完它们后再讨论。

APIC/SMP Interrupt

linux中可以找到许多关于apic handler的定义:

图7:诸多apic handler定义

它们都是基于apicinterrupt/apicinterrupt3宏的:

// arch\x86\entry\entry_64.S .macro apicinterrupt num sym do_sym PUSH_SECTION_IRQENTRY apicinterrupt3 \num \sym \do_sym POP_SECTION_IRQENTRY .endm /* * APIC interrupts. */ .macro apicinterrupt3 num sym do_sym SYM_CODE_START(\sym) pushq $~(\num) .Lcommon_\sym: call interrupt_entry call \do_sym /* rdi points to pt_regs */ jmp ret_from_intr SYM_CODE_END(\sym) _ASM_NOKPROBE(\sym) .endm

从这里,我们可以清晰地看出,仍然是分为进入C语言前、进入C语言、退出C语言后进行的(PUSH/POP_SECTION_IRQENTRY是编译期段检查,无需关心),因此,我们仍然这个思路分析。

1. 进入C语言前:

interrupt_entry

// arch\x86\entry\entry_64.S SYM_CODE_START(interrupt_entry) cld testb $3, CS-ORIG_RAX+8(%rsp) jz 1f SWAPGS FENCE_SWAPGS_USER_ENTRY /* * Switch to the thread stack. The IRET frame and orig_ax are * on the stack, as well as the return address. RDI..R12 are * not (yet) on the stack and space has not (yet) been * allocated for them. */ pushq %rdi /* Need to switch before accessing the thread stack. */ SWITCH_TO_KERNEL_CR3 scratch_reg=%rdi movq %rsp, %rdi movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp pushq 7*8(%rdi) /* regs->ss */ pushq 6*8(%rdi) /* regs->rsp */ pushq 5*8(%rdi) /* regs->eflags */ pushq 4*8(%rdi) /* regs->cs */ pushq 3*8(%rdi) /* regs->ip */ pushq 2*8(%rdi) /* regs->orig_ax */ pushq 8(%rdi) /* return address */ movq (%rdi), %rdi 1: PUSH_AND_CLEAR_REGS save_ret=1 ENTER_IRQ_STACK old_rsp=%rdi save_ret=1 ret SYM_CODE_END(interrupt_entry) _ASM_NOKPROBE(interrupt_entry)

回忆我们在异常中的内容,保存寄存器,切换GS、cr3,切换栈,在这里也是类似的。但是可以看到,这里切换栈并没有调用C函数,推测可能是为了避免cache miss,尽量加快中断的处理。

仍然可以看到,仍然是先判断我们进入handler前是用户态还是内核态:

testb $3, CS-ORIG_RAX+8(%rsp) jz 1f

这里之所以+8是因为栈上有一个call interrupt_entry的返回地址。

对用户态而言,我们仍然是先切换到内核栈,然后将hardware context(cs ip等)迁移到内核栈上,代码与异常处理类似;而对内核栈我们就可以跳过这一部分了。接着保存其他通用寄存器,两条代码路径都来到了ENTER_IRQ_STACK,进入硬件中断栈。

这里首先要解释一下,所谓的硬件中断栈(IRQ_STACK)。除了给每个可调度的thread一个内核栈外,每个cpu还会有一些其他的内核栈[16],其中就包括interrupt stack,中断栈,旨在提升可嵌套的中断层数[16]。毕竟我们进入中断的可能有三种情况:用户态进入、系统调用中/异常中进入、中断上下文进入。对于前两者,我们都是会进入进程内核栈的,而这多多少少会降低可嵌套的中断层数,而为每个进程都多分配栈大小以应对这种情况又显得愚蠢,因此额外的interrupt stack是个不错的选择。

进入中断虽然会关闭中断,但中断处理并不是每一段都是不可被打断的[17],实际上只有最紧要的不可被中断的部分中断会被关闭,在进入non-critical region后,中断处理函数会打开中断,以使得中断的响应能够尽快恢复,提升系统的吞吐量。

ENTER_IRQ_STACK宏根据输入的参数展开后如下:

// arch\x86\entry\entry_64.S .macro ENTER_IRQ_STACK regs=1 old_rsp save_ret=0 /* * If save_ret is set, the original stack contains one additional * entry -- the return address. Therefore, move the address one * entry below %rsp to \old_rsp. */ leaq 8(%rsp), %rdi incl PER_CPU_VAR(irq_count) jnz .Lirq_stack_push_old_rsp_\@ movq PER_CPU_VAR(hardirq_stack_ptr), %rsp .Lirq_stack_push_old_rsp_\@: pushq %rdi /* * Push the return address to the stack. This return address can * be found at the "real" original RSP, which was offset by 8 at * the beginning of this macro. */ pushq -8(%rdi) .endm

leaq额外的8个字节是为了跳过返回地址,这样%rdi就指向了内核栈上保存的先前context info(struct pt_regs)。紧接着inc下irq_count(初始为-1),若为0切换栈,否则无需切换,push即可。而实际的切换栈,只是将rsp指向hardirq_stack_ptr罢了,这个hardirq_stack_ptr其定义如下:

// arch\x86\include\asm\processor.h DECLARE_PER_CPU(struct irq_stack *, hardirq_stack_ptr); /* Per CPU interrupt stacks */ struct irq_stack { char stack[IRQ_STACK_SIZE]; } __aligned(IRQ_STACK_SIZE);

其初始化如下:

// arch\x86\kernel\irq_64.c int irq_init_percpu_irqstack(unsigned int cpu) { if (per_cpu(hardirq_stack_ptr, cpu)) return 0; return map_irq_stack(cpu); } static int map_irq_stack(unsigned int cpu) { void *va = per_cpu_ptr(&irq_stack_backing_store, cpu); per_cpu(hardirq_stack_ptr, cpu) = va + IRQ_STACK_SIZE; return 0; }

其中的irq_stack_backing_store就是每个cpu分配的interrupt stack的起始地址,而由于栈的特性,irq_stack_backing_store+IRQ_STACK_SIZE就到了栈底,也就是我们需要的其实指针,也就是这里的hardirq_stack_ptr

// arch\x86\kernel\irq_64.c DEFINE_PER_CPU_PAGE_ALIGNED(struct irq_stack, irq_stack_backing_store) __visible;

在切换栈后,进行了两个push操作,这样一来,interrupt stack上的内容结构如下:

* | saved struct pt_reg* | * | return address for "call interrrupt_entry" |<---- rsp

显然,如果进行了栈切换的话,struct pt_reg*就指向内核栈,否则就指向interrupt stack。(前者比较好理解,后者主要是,当发生嵌套中断时,我们本身已经在interrupt stack上了,而interrupt stack本身也是一个内核态栈,因此嵌套中断发生时不会load sp0(不会发生硬件栈切换),那么我们刚进入handler时的数据其实就已经在interrupt stack上了,这里save 的old_rsp也是指在interrupt stack上)

至此,栈切换完成,同时,这也到达了interrupt_entry的返回处,进入C语言前的准备结束,代码进入C语言(rdi保存参数struct pt_regs*):

call \do_sym /* rdi points to pt_regs */

2. 退出C语言后

ret_from_intr

// arch\x86\entry\entry_64.S ret_from_intr: DISABLE_INTERRUPTS(CLBR_ANY) LEAVE_IRQ_STACK testb $3, CS(%rsp) jz retint_kernel /* Interrupt came from user space */ .Lretint_user: ...

完成了中断后,我们准备要退出。可以看到,这里跟异常的退出非常类似,也是先关中断,以及后续根据saved cs值决定返回用户态还是内核态,唯一多的就是LEAVE_IRQ_STACK,退出interrupt stack,这也很好理解。

这里要说明的是,这个leave并不会显式地进行栈切换,因为没必要:

// arch\x86\entry\entry_64.S .macro LEAVE_IRQ_STACK regs=1 /* We need to be off the IRQ stack before decrementing irq_count. */ popq %rsp /* * As in ENTER_IRQ_STACK, irq_count == 0, we are still claiming * the irq stack but we're not on it. */ decl PER_CPU_VAR(irq_count) .endm

如果我们要返回的是进程内核栈的话,因为save的rsp就是指在进程内核栈上,所以pop %rsp就完成了栈的切换(ss都是一样的),而如果我们要返回的仍然是interrupt stack的话(嵌套中断),save的rsp仍然是指在interrupt stack上的,因此这里我们无需手动切换。最后decl一下irq_count释放一次interrupt stack。

这样,我们就完成了退出。当然,其实只有在回到用户态/系统调用/用户异常时才会真正切换到进程内核栈。

剩下的事情就和异常的退出完全一致了,该回内核的回内核,该回用户的回用户,其中夹杂着重调度、信号检查等等,以及最后回到用户态前的trampoline stack、kernel stack erasing。

Normal Interrupt

从图5中可以看出,normal interrupt handler似乎像是通用(批量生产)的,似乎只是中断号码不一样,实际上,确实是这样:

// arch\x86\entry\entry_64.S .align 8 SYM_CODE_START(irq_entries_start) vector=FIRST_EXTERNAL_VECTOR .rept (FIRST_SYSTEM_VECTOR - FIRST_EXTERNAL_VECTOR) pushq $(~vector+0x80) /* Note: always in signed byte range */ jmp common_interrupt .align 8 vector=vector+1 .endr SYM_CODE_END(irq_entries_start)

可以看出,rep了FIRST_SYSTEM_VECTOR - FIRST_EXTERNAL_VECTOR个entry,每个entry的不同之处仅仅在于它们push的num,接着都跳到了common_interrupt。关于它为什么与APIC有次差别,我们后续再讨论,先看common_interrupt,它就是nromal handler中断的处理部分了:

// arch\x86\entry\entry_64.S SYM_CODE_START_LOCAL(common_interrupt) addq $-0x80, (%rsp) /* Adjust vector to [-256, -1] range */ call interrupt_entry call do_IRQ /* rdi points to pt_regs */ /* 0(%rsp): old RSP */ ret_from_intr: DISABLE_INTERRUPTS(CLBR_ANY) LEAVE_IRQ_STACK ...

可以看到,这里首先是将刚push的num rescale了一下,接着与APIC一样,调用interrupt_entry进行必要的context保存和栈切换,除了统一调用do_IRQ外,后续的返回与APIC也是一样的。

所以实际上所有中断是共用一个do_IRQ函数的,它是怎样针对不同中断工作的呢?

// arch\x86\kernel\irq.c __visible unsigned int __irq_entry do_IRQ(struct pt_regs *regs) { struct pt_regs *old_regs = set_irq_regs(regs); struct irq_desc * desc; /* high bit used in ret_from_ code */ unsigned vector = ~regs->orig_ax; entering_irq(); desc = __this_cpu_read(vector_irq[vector]); if (likely(!IS_ERR_OR_NULL(desc))) { if (IS_ENABLED(CONFIG_X86_32)) handle_irq(desc, regs); else generic_handle_irq_desc(desc); } else { ack_APIC_irq(); if (desc == VECTOR_UNUSED) { pr_emerg_ratelimited("%s: %d.%d No irq handler for vector\n", __func__, smp_processor_id(), vector); } else { __this_cpu_write(vector_irq[vector], VECTOR_UNUSED); } } exiting_irq(); set_irq_regs(old_regs); return 1; }

可以看到,除去错误情况(irq_desc为空或者错误),其实就是一句generic_handle_irq_desc(x86_32情况下的handle_irq只是执行了部分额外的检查,最后仍然调用了generic_handle_irq_desc)。当然还包括了entering_irq()、exiting_irq(),前者只是更新部分内核的统计信息,后者也会更新这部分信息,但是在检测发现当前是非嵌套中断的退出时回执行soft_irq[18](即处理中断过程中的deferrable任务),这里按下不表。

所以这个generic_handle_irq_desc究竟做了什么呢?其实它就一句话:

// include\linux\irqdesc.h /* * Architectures call this to let the generic IRQ layer * handle an interrupt. */ static inline void generic_handle_irq_desc(struct irq_desc *desc) { desc->handle_irq(desc); }

而这个struct irq_desc是什么呢?很长:

// include\linux\irqdesc.h /** * struct irq_desc - interrupt descriptor * @irq_common_data: per irq and chip data passed down to chip functions * @kstat_irqs: irq stats per cpu * @handle_irq: highlevel irq-events handler * @preflow_handler: handler called before the flow handler (currently used by sparc) * @action: the irq action chain * @status: status information * @core_internal_state__do_not_mess_with_it: core internal status information * @depth: disable-depth, for nested irq_disable() calls * @wake_depth: enable depth, for multiple irq_set_irq_wake() callers * @tot_count: stats field for non-percpu irqs * @irq_count: stats field to detect stalled irqs * @last_unhandled: aging timer for unhandled count * @irqs_unhandled: stats field for spurious unhandled interrupts * @threads_handled: stats field for deferred spurious detection of threaded handlers * @threads_handled_last: comparator field for deferred spurious detection of theraded handlers * @lock: locking for SMP * @affinity_hint: hint to user space for preferred irq affinity * @affinity_notify: context for notification of affinity changes * @pending_mask: pending rebalanced interrupts * @threads_oneshot: bitfield to handle shared oneshot threads * @threads_active: number of irqaction threads currently running * @wait_for_threads: wait queue for sync_irq to wait for threaded handlers * @nr_actions: number of installed actions on this descriptor * @no_suspend_depth: number of irqactions on a irq descriptor with * IRQF_NO_SUSPEND set * @force_resume_depth: number of irqactions on a irq descriptor with * IRQF_FORCE_RESUME set * @rcu: rcu head for delayed free * @kobj: kobject used to represent this struct in sysfs * @request_mutex: mutex to protect request/free before locking desc->lock * @dir: /proc/irq/ procfs entry * @debugfs_file: dentry for the debugfs file * @name: flow handler name for /proc/interrupts output */ struct irq_desc { struct irq_common_data irq_common_data; struct irq_data irq_data; unsigned int __percpu *kstat_irqs; irq_flow_handler_t handle_irq; #ifdef CONFIG_IRQ_PREFLOW_FASTEOI irq_preflow_handler_t preflow_handler; #endif struct irqaction *action; /* IRQ action list */ unsigned int status_use_accessors; unsigned int core_internal_state__do_not_mess_with_it; unsigned int depth; /* nested irq disables */ unsigned int wake_depth; /* nested wake enables */ unsigned int tot_count; unsigned int irq_count; /* For detecting broken IRQs */ unsigned long last_unhandled; /* Aging timer for unhandled count */ unsigned int irqs_unhandled; atomic_t threads_handled; int threads_handled_last; raw_spinlock_t lock; struct cpumask *percpu_enabled; const struct cpumask *percpu_affinity; #ifdef CONFIG_SMP const struct cpumask *affinity_hint; struct irq_affinity_notify *affinity_notify; #ifdef CONFIG_GENERIC_PENDING_IRQ cpumask_var_t pending_mask; #endif #endif unsigned long threads_oneshot; atomic_t threads_active; wait_queue_head_t wait_for_threads; #ifdef CONFIG_PM_SLEEP unsigned int nr_actions; unsigned int no_suspend_depth; unsigned int cond_suspend_depth; unsigned int force_resume_depth; #endif #ifdef CONFIG_PROC_FS struct proc_dir_entry *dir; #endif #ifdef CONFIG_GENERIC_IRQ_DEBUGFS struct dentry *debugfs_file; const char *dev_name; #endif #ifdef CONFIG_SPARSE_IRQ struct rcu_head rcu; struct kobject kobj; #endif struct mutex request_mutex; int parent_irq; struct module *owner; const char *name; } ____cacheline_internodealigned_in_smp;

这个handle_irq是一个irq_flow_handler_t类型的变量,可以看出应该是一个函数指针:

// include\linux\irqhandler.h typedef void (*irq_flow_handler_t)(struct irq_desc *desc);

这里我们就可以解释为什么两者的handler定义方式不同了。

PIC(Programmable Interrupt Controller)是比较早期的硬件,不同硬件之间不兼容,中断号与中断的对应可能随硬件变换;而APIC(Advanced Programmable Interrupt Controller)是后来出现的技术,对中断号的分配有linux的标准,可以一开始就定义好。但是中断的处理逻辑都类似,因此linux中使用了一套generic interrupt处理系统[19]来解决PIC的硬件不兼容问题。在这里,不同的中断会注册不同的irq_flow_handler_t,调用其来处理中断。但实际上,irq_flow_handler_t并不自己处理中断,而是将其delegate to其他函数,它本事是负责“中断流”处理的,也就是管理中断,使其有序地处理。根据中断触发的方式不同(edge、level、eoi等等),其对应的默认的flow handler也不同,以level触发为例,大体逻辑可以如下描述(简化后)[19]:

desc->irq_data.chip->irq_mask_ack(); handle_irq_event(desc->action); desc->irq_data.chip->irq_unmask();

这里的irq_data.chip是指这个中断所在的芯片,也是generic系统的一部分,不同的芯片创建不同的chip即可,也很长:

// include\linux\irq.h /** * struct irq_chip - hardware interrupt chip descriptor * * @parent_device: pointer to parent device for irqchip * @name: name for /proc/interrupts * @irq_startup: start up the interrupt (defaults to ->enable if NULL) * @irq_shutdown: shut down the interrupt (defaults to ->disable if NULL) * @irq_enable: enable the interrupt (defaults to chip->unmask if NULL) * @irq_disable: disable the interrupt * @irq_ack: start of a new interrupt * @irq_mask: mask an interrupt source * @irq_mask_ack: ack and mask an interrupt source * @irq_unmask: unmask an interrupt source * @irq_eoi: end of interrupt * @irq_set_affinity: Set the CPU affinity on SMP machines. If the force * argument is true, it tells the driver to * unconditionally apply the affinity setting. Sanity * checks against the supplied affinity mask are not * required. This is used for CPU hotplug where the * target CPU is not yet set in the cpu_online_mask. * @irq_retrigger: resend an IRQ to the CPU * @irq_set_type: set the flow type (IRQ_TYPE_LEVEL/etc.) of an IRQ * @irq_set_wake: enable/disable power-management wake-on of an IRQ * @irq_bus_lock: function to lock access to slow bus (i2c) chips * @irq_bus_sync_unlock:function to sync and unlock slow bus (i2c) chips * @irq_cpu_online: configure an interrupt source for a secondary CPU * @irq_cpu_offline: un-configure an interrupt source for a secondary CPU * @irq_suspend: function called from core code on suspend once per * chip, when one or more interrupts are installed * @irq_resume: function called from core code on resume once per chip, * when one ore more interrupts are installed * @irq_pm_shutdown: function called from core code on shutdown once per chip * @irq_calc_mask: Optional function to set irq_data.mask for special cases * @irq_print_chip: optional to print special chip info in show_interrupts * @irq_request_resources: optional to request resources before calling * any other callback related to this irq * @irq_release_resources: optional to release resources acquired with * irq_request_resources * @irq_compose_msi_msg: optional to compose message content for MSI * @irq_write_msi_msg: optional to write message content for MSI * @irq_get_irqchip_state: return the internal state of an interrupt * @irq_set_irqchip_state: set the internal state of a interrupt * @irq_set_vcpu_affinity: optional to target a vCPU in a virtual machine * @ipi_send_single: send a single IPI to destination cpus * @ipi_send_mask: send an IPI to destination cpus in cpumask * @irq_nmi_setup: function called from core code before enabling an NMI * @irq_nmi_teardown: function called from core code after disabling an NMI * @flags: chip specific flags */ struct irq_chip { struct device *parent_device; const char *name; unsigned int (*irq_startup)(struct irq_data *data); void (*irq_shutdown)(struct irq_data *data); void (*irq_enable)(struct irq_data *data); void (*irq_disable)(struct irq_data *data); void (*irq_ack)(struct irq_data *data); void (*irq_mask)(struct irq_data *data); void (*irq_mask_ack)(struct irq_data *data); void (*irq_unmask)(struct irq_data *data); void (*irq_eoi)(struct irq_data *data); int (*irq_set_affinity)(struct irq_data *data, const struct cpumask *dest, bool force); int (*irq_retrigger)(struct irq_data *data); int (*irq_set_type)(struct irq_data *data, unsigned int flow_type); int (*irq_set_wake)(struct irq_data *data, unsigned int on); void (*irq_bus_lock)(struct irq_data *data); void (*irq_bus_sync_unlock)(struct irq_data *data); void (*irq_cpu_online)(struct irq_data *data); void (*irq_cpu_offline)(struct irq_data *data); void (*irq_suspend)(struct irq_data *data); void (*irq_resume)(struct irq_data *data); void (*irq_pm_shutdown)(struct irq_data *data); void (*irq_calc_mask)(struct irq_data *data); void (*irq_print_chip)(struct irq_data *data, struct seq_file *p); int (*irq_request_resources)(struct irq_data *data); void (*irq_release_resources)(struct irq_data *data); void (*irq_compose_msi_msg)(struct irq_data *data, struct msi_msg *msg); void (*irq_write_msi_msg)(struct irq_data *data, struct msi_msg *msg); int (*irq_get_irqchip_state)(struct irq_data *data, enum irqchip_irq_state which, bool *state); int (*irq_set_irqchip_state)(struct irq_data *data, enum irqchip_irq_state which, bool state); int (*irq_set_vcpu_affinity)(struct irq_data *data, void *vcpu_info); void (*ipi_send_single)(struct irq_data *data, unsigned int cpu); void (*ipi_send_mask)(struct irq_data *data, const struct cpumask *dest); int (*irq_nmi_setup)(struct irq_data *data); void (*irq_nmi_teardown)(struct irq_data *data); unsigned long flags; };

所以这里是发一个ack接着处理中断,等处理完后再unmask这个中断,重新接受。这里handle_irq_event执行了诸多状态设置以及检查,最后的handle是delegate给了__handle_irq_event_percpu:

// kernel/irq/handle.c irqreturn_t __handle_irq_event_percpu(struct irq_desc *desc, unsigned int *flags) { irqreturn_t retval = IRQ_NONE; unsigned int irq = desc->irq_data.irq; struct irqaction *action; record_irq_time(desc); for_each_action_of_desc(desc, action) { irqreturn_t res; trace_irq_handler_entry(irq, action); res = action->handler(irq, action->dev_id); trace_irq_handler_exit(irq, action, res); if (WARN_ONCE(!irqs_disabled(),"irq %u handler %pS enabled interrupts\n", irq, action->handler)) local_irq_disable(); switch (res) { case IRQ_WAKE_THREAD: /* * Catch drivers which return WAKE_THREAD but * did not set up a thread function */ if (unlikely(!action->thread_fn)) { warn_no_thread(irq, action); break; } __irq_wake_thread(desc, action); /* Fall through - to add to randomness */ case IRQ_HANDLED: *flags |= action->flags; break; default: break; } retval |= res; } return retval; }

这里我们大体上可以看出我们遍历了这个interrupt上的所有action,对这个interrupt进行处理:

for_each_action_of_desc(desc, action) { irqreturn_t res; ... res = action->handler(irq, action->dev_id); ... }

这里的aciton定义如下:

// include\linux\interrupt.h /** * struct irqaction - per interrupt action descriptor * @handler: interrupt handler function * @name: name of the device * @dev_id: cookie to identify the device * @percpu_dev_id: cookie to identify the device * @next: pointer to the next irqaction for shared interrupts * @irq: interrupt number * @flags: flags (see IRQF_* above) * @thread_fn: interrupt handler function for threaded interrupts * @thread: thread pointer for threaded interrupts * @secondary: pointer to secondary irqaction (force threading) * @thread_flags: flags related to @thread * @thread_mask: bitmask for keeping track of @thread activity * @dir: pointer to the proc/irq/NN/name entry */ struct irqaction { irq_handler_t handler; void *dev_id; void __percpu *percpu_dev_id; struct irqaction *next; irq_handler_t thread_fn; struct task_struct *thread; struct irqaction *secondary; unsigned int irq; unsigned int flags; unsigned long thread_flags; unsigned long thread_mask; const char *name; struct proc_dir_entry *dir; } ____cacheline_internodealigned_in_smp;

为什么要遍历所有action呢?这就引出了apic与normal interrupt的差别之处,normal interrupt覆盖了enternally connected I/O devices[20]的部分(见图8),也就是来自外围I/O设备的中断,可能出现两个设备共享一个irq的问题,在这里的体现就是,同一个irq_des上可以串联很多个action,每个action上挂的handler都会输入一个dev_id作为参数,用来确认到来的中断是否来自自己的设备,进而处理(因此应当尽量避免共享,处理效率低);对apic而言,它们有固定的中断号[21]dedicated出来,因此不存在这个问题:

图8:linux中断号分配


因此,对APIC中断来说,我们在一开始进入handler就可以得知需要调用哪个C函数来处理它,并且flow handling也由C函数负责,我们可以用宏定义之,也相对高效;但对normal interrupt来说,我们执行的只是generic代码,甚至不知道具体调用了哪些handler(代码并不能体现之),这种“多态”有效地屏蔽了硬件的差异,使得代码管理变得十分方便,但带来的penalty就是难以定位调用函数(从静态代码方面来说)以及相对效率的稍低。总的来说,分工不同,它们的实现相应地就要因地制宜,这也都可以理解。

对中断的handle分析完后,剩余的中断处理代码(也就是退出C语言部分准备回到内核/用户态)与APIC并无两样,也就不再赘述了。

结语

至此,关于中断/异常我们就都分析完了,篇幅相对也比较长,其中或多或少夹杂了一些其他内容(KPTI等等),但总的来说,对中断/异常的总体思路还是较为明确的:

(用户态/内核态)执行--(发生中断/异常)-->硬件context switch,并跳转到相应handler---->handler(汇编代码)执行必要的工作--(跳转到C语言代码内)-->处理中断/异常--(C语言代码返回,回到handler)-->handler(汇编代码)执行必要的工作---->回到用户态/内核态

它们都需要考虑:

中断向量的设置GS、CR3的切换context saving对中断/异常的handle返回到内核态前的抢占调度返回到用户态前的重调度、信号处理等等context restore

对异常来说,我们需要额外考虑:

trampoline stack与当前进程内核栈的切换

对中断来说,我们需要考虑:

interrupt stack与进程内核栈的切换对APIC interrupt,进入predefined C函数对Normal interrupt,进入do_IRQ genenrric interrupt handling进行后续的flow handling以及actions等等

2021.11.6购买无线蓝牙耳机体验

无线蓝牙耳机全名:

铂典R15无线蓝牙耳机运动挂脖超长续航苹果vivo华为OPPO安卓通用

时逢双十一,我在购物狂欢的时节购买了蓝牙耳机,但是蓝牙耳机总有无线电干扰的电流滋滋声。我在拼多多客服平台询问老板过后,老板很热心❤️,ta首先建议我使用1A-2A的平常手机充电器插头给无线蓝牙耳机充电,再建议我到没有电流干扰的环境中试听,让我尝试几天后再考虑退货退款。

几经尝试后,我仍然无法解决该无线蓝牙耳机有电流滋滋声的问题。

在“11.11购物旺季”买的无线蓝牙耳机价格便宜,我不好意思再麻烦老板了。 但每当我看见这副崭新的耳机,一时不知该如何处理掉ta。目前正在考虑退货退款中,但我一想到拼多多的老板每天都在直播卖货,非常辛苦,老板应该生存不易,退货退款实在于心不忍。

缘于此,我想在知乎询问下,有什么办法能够消除无线电干扰的蓝牙滋滋声?万分感谢!

差点毁掉了的运算放大器,这11招帮你轻松挽回!

运算放大器组成的电路五花八门,令人眼花瞭乱,但是EDA365提醒大家这是模拟电路中学习的重点。在分析它的工作原理时没有抓住核心,往往令人头大。特搜罗天下运放电路之应用,来个“庖丁解牛”,希望各位攻城狮们,看完后有所收获。


遍观所有模拟电子技朮的书籍和课程,在介绍运算放大器电路的时候,无非是先给电路来个定性,比如这是一个同向放大器,然后去推导它的输出与输入的关系,然 后得出Vo=(1+Rf)Vi,那是一个反向放大器,然后得出Vo=-Rf*Vi……


最后往往得出这样一个印象:记住公式就可以了!如果我们将电路稍稍变换一下,他们就找不着北了。偶曾经面试过至少100个以上的大专以上学历的电子专业应聘者,结果能将我给出的运算放大器电路分析得一点不错的没有超过 10个人,其它专业毕业的更是可想而知了。


今天,EDA365电子论坛分享给大家战无不胜的两招,这两招在所有运放电路的教材里都写得明白,就是“虚短”和“虚断”,不过要把它运用得出神入化,就要有较深厚的功底了。


1

虚短和虚断的概念


由于运放的电压放大倍数很大,一般通用型运算放大器的开环电压放大倍数都在80dB以上。而运放的输出电压是有限的,一般在 10 V~14 V。因此运放的差模输入电压不足1 mV,两输入端近似等电位,相当于 “短路”。开环电压放大倍数越大,两输入端的电位越接近相等。


“虚短”是指在分析运算放大器处于线性状态时,可把两输入端视为等电位,这一特性称为虚假短路,简称虚短。显然不能将两输入端真正短路。


由于运放的差模输入电阻很大,一般通用型运算放大器的输入电阻都在1MΩ以上。因此流入运放输入端的电流往往不足1uA,远小于输入端外电路的电流。故通常可把运放的两输入端视为开路,且输入电阻越大,两输入端越接近开路。


“虚断”是指在分析运放处于线性状态时,可以把两输入端视为等效开路,这一特性 称为虚假开路,简称虚断。显然不能将两输入端真正断路。


在分析运放电路工作原理时,首先请各位暂时忘掉什么同向放大、反向放大,什么加法器、减法器,什么差动输入……暂时忘掉那些输入输出关系的公式……这些东东只会干扰你,让你更糊涂;也请各位暂时不要理会输入偏置电流、共模抑制比、失调电压等电路参数,这是设计者要考虑的事情。


我们理解的就是理想放大器(其实在维修中和大多数设计过程中,把实际放大器当做理想放大器来分析也不会有问题)。

好了,让我们抓过两把“板斧”------“虚短”和“虚断”,开始“庖丁解牛”了。



图一运放的同向端接地=0V,反向端和同向端虚短,所以也是0V,反向输入端输入电阻很高,虚断,几乎没有电流注入和流出,那么R1和R2相当于是串联的,流过一个串联电路中的每一只组件的电流是相同的,即流过R1的电流和流过R2的电流是相同的。

流过R1的电流I1 = (Vi - V-)/R1 ……a 流过R2的电流I2 = (V- - Vout)/R2 ……b V- = V+ = 0 ……c I1 = I2 ……d 求解上面的初中代数方程得Vout = (-R2/R1)*Vi 这就是传说中的反向放大器的输入输出关系式了。



图二中Vi与V-虚短,则 Vi = V- ……a 因为虚断,反向输入端没有电流输入输出,通过R1和R2 的电流相等,设此电流为I,由欧姆定律得:I = Vout/(R1+R2) ……b Vi等于R2上的分压, 即:Vi = I*R2 ……c 由abc式得Vout=Vi*(R1+R2)/R2 这就是传说中的同向放大器的公式了。



图三中,由虚短知:V- = V+ = 0 ……a 由虚断及基尔霍夫定律知,通过R2与R1的电流之和等于通过R3的电流,故 (V1 – V-)/R1 + (V2 – V-)/R2 = (Vout – V-)/R3 ……b 代入a式,b式变为V1/R1 + V2/R2 = Vout/R3 如果取R1=R2=R3,则上式变为Vout=V1+V2,这就是传说中的加法器了。


(编辑者注)质疑:(V1 – V-)/R1 + (V2 – V-)/R2 = (V- – Vout)/R3 ……b 图三公式中少了个负号?



请看图四。因为虚断,运放同向端没有电流流过,则流过R1和R2的电流相等,同理流过R4和R3的电流也相等。

故 (V1 – V+)/R1 = (V+ - V2)/R2 ……a (Vout – V-)/R3 = V-/R4 ……b 由虚短知:V+ = V- ……c 如果R1=R2,R3=R4,则由以上式子可以推导出 V+ = (V1 + V2)/2 V- = Vout/2 故 Vout = V1 + V2 也是一个加法器,呵呵!



图五由虚断知,通过R1的电流等于通过R2的电流,同理通过R4的电流等于R3的电流,故有 (V2 – V+)/R1 = V+/R2 ……a (V1 – V-)/R4 = (V- - Vout)/R3 ……b 如果R1=R2, 则V+ = V2/2 ……c 如果R3=R4, 则V- = (Vout + V1)/2 ……d 由虚短知 V+ = V- ……e 所以 Vout=V2-V1 这就是传说中的减法器了。



图六电路中,由虚短知,反向输入端的电压与同向端相等,由虚断知,通过R1的电流与通过C1的电流相等。


通过R1的电流 i=V1/R1 通过C1的电流i=C*dUc/dt=-C*dVout/dt 所以 Vout=((-1/(R1*C1))∫V1dt 输出电压与输入电压对时间的积分成正比,这就是传说中的积分电路了。


若V1为恒定电压U,则上式变换为Vout = -U*t/(R1*C1) t 是时间,则Vout输出电压是一条从0至负电源电压按时间变化的直线。


图七中由虚断知,通过电容C1和电阻R2的电流是相等的,由虚短知,运放同向端与反向端电压是相等的。

则:Vout = -i * R2 = -(R2*C1)dV1/dt 这是一个微分电路。如果V1是一个突然加入的直流电压,则输出Vout对应一个方向与V1相反的脉冲。



图八,由虚短知 Vx = V1 ……a Vy = V2 ……b 由虚断知,运放输入端没有电流流过,则R1、R2、R3可视为串联,通过每一个电阻的电流是相同的, 电流I=(Vx-Vy)/R2 ……c 则:Vo1-Vo2=I*(R1+R2+R3) = (Vx-Vy)(R1+R2+R3)/R2 ……d 由虚断知,流过R6与流过R7的电流相等,若R6=R7, 则Vw = Vo2/2 ……e 同理若R4=R5,则Vout – Vu = Vu – Vo1,故Vu = (Vout+Vo1)/2 ……f 由虚短知,Vu = Vw ……g 由efg得 Vout = Vo2 – Vo1 ……h 由dh得 Vout = (Vy –Vx)(R1+R2+R3)/R2 上式中(R1+R2+R3)/R2是定值,此值确定了差值(Vy –Vx)的放大倍数。这个电路就是传说中的差分放大电路了。

分析一个大家接触得较多的电路。很多控制器接受来自各种检测仪表的0~20mA或4~20mA电流,电路将此电流转换成电压后再送ADC转换成数字信号,图九就是这样一个典型电路。如图4~20mA电流流过采样100Ω电阻R1,在R1上会产生0.4~2V的电压差。

由虚断知,运放输入端没有电流流过,则流过R3和R5的电流相等,流过R2和R4的电流相等。故:(V2-Vy)/R3 = Vy/R5 ……a (V1-Vx)/R2 = (Vx-Vout)/R4 ……b 由虚短知:Vx = Vy ……c 电流从0~20mA变化,则V1 = V2 + (0.4~2) ……d 由cd式代入b式得(V2 + (0.4~2)-Vy)/R2 = (Vy-Vout)/R4 ……e 如果R3=R2,R4=R5,则由e-a得Vout = -(0.4~2)R4/R2 ……f 图九中R4/R2=22k/10k=2.2,则f式Vout = -(0.88~4.4)V,即是说,将4~20mA电流转换成了-0.88 ~ -4.4V电压,此电压可以送ADC去处理。



电流可以转换成电压,电压也可以转换成电流。图十就是这样一个电路。上图的负反馈没有通过电阻直接反馈,而是串联了三极管Q1的发射结,大家可不要以为是一个比较器就是了。只要是放大电路,虚短虚断的规律仍然是符合的!


由虚断知,运放输入端没有电流流过,则 (Vi – V1)/R2 = (V1 – V4)/R6 ……a同理 (V3 – V2)/R5 = V2/R4 ……b由虚短知 V1 = V2 ……c如果R2=R6,R4=R5,则由abc式得V3-V4=Vi上式说明R7两端的电压和输入电压Vi相等,则通过R7的电流I=Vi/R7,如果负载RL<<100KΩ,则通过Rl和通过R7的电流基本相同。

在EDA365电子论坛中找到一个复杂的,图十一是一个三线制PT100前置放大电路。



PT100传感器引出三根材质、线径、长度完全相同的线,接法如图所示。有2V的电压加在由R14、R20、R15、Z1、PT100及其线电阻组成的桥电路上。


Z1、Z2、Z3、D11、D12、D83及各电容在电路中起滤波和保护作用,静态分析时可不予理会,Z1、Z2、Z3可视为短路,D11、D12、D83及各电容可视为开路。


由电阻分压知, V3=2*R20/(R14+20)=200/1100=2/11 ……a 由虚短知,U8B第6、7脚 电压和第5脚电压相等 V4=V3 ……b 由虚断知,U8A第2脚没有电流流过,则流过R18和R19上的电流相等。(V2-V4)/R19=(V5-V2)/R18 ……c 由虚断知,U8A第3脚没有电流流过, V1=V7 ……d 在桥电路中R15和Z1、PT100及线电阻串联,PT100与线电阻串联分得的电压通过电阻R17加至U8A的第3脚, V7=2*(Rx+2R0)/(R15+Rx+2R0) …..e 由虚短知,U8A第3脚和第2脚电压相等,V1=V2 ……f 由abcdef得, (V5-V7)/100=(V7-V3)/2.2 化简得 V5=(102.2*V7-100V3)/2.2 即 V5=204.4(Rx+2R0)/(1000+Rx+2R0) – 200/11 ……g 上式输出电压V5是Rx的函数我们再看线电阻的影响。

Pt100最下端线电阻上产生的电压降经过中间的线电阻、Z2、R22,加至U8C的第10脚,由虚断知, V5=V8=V9=2*R0/(R15+Rx+2R0) ……a (V6-V10)/R25=V10/R26 ……b 由虚短知, V10=V5 ……c 由式abc得 V6=(102.2/2.2)V5=204.4R0/[2.2(1000+Rx+2R0)] ……h 由式gh组成的方程组知,如果测出V5、V6的值,就可算出Rx及R0,知道Rx,查pt100分度表就知道温度的大小了。

今天的分享着实有点烧脑,如果没有完全吃透,建议大家可以多回看几遍~

R17引入的5G广播与组播技术--MBS

2022年随手记第三十一篇,概览R17中的重要内容之一MBS=Multicast-Broadcast Services。3GPP在pre-5G的R14以及已经发布的R15和R16的5G规范中并没有关注广播与组播技术。因此此项技术在R17是全新的内容,也可以说是作为3G4G中MBMS/eMBMS在5G平台的延续吧,作为承上启下大家可以参考如下总结MBMS和eMBMS的文章,当然这篇文章作为DSS的铺垫而写,但也适合大家了解3G4G中广播多播技术的概况:

2021随手记(16)EMBMS/MBSFN回顾

在MBS的规范形成过程中,总结部分的提案文档是RP-220408,另外还有RP-220428包含了MBS业务概览和Core part的总结:


这两个提案作为索引和总结文档:


本文先概览一下MBS的相关内容,后续文章会总结其他方面。

R17 NR MBS WI为PTM (Point-To-Multipoint,点到多点)性质的业务指定了两种交付方式,即MBS组播和MBS广播,例如用于公共安全和关键任务业务、V2X应用、IPTV、视频直播、无线和物联网应用软件交付等。

在R17版本引入NR MBS之前,NR并不支持广播/组播传输来传递用户数据。也就是之前的PTM性质的业务只能通过单播的方式在NR上承载使用,消耗资源庞大,效率很低,特别是从无线资源利用的角度来看尤甚。尽管如此,对于上面提到的用例和业务,广播/组播传输提供了实质性的好处,特别是在系统效率和用户体验方面。MBS组播能够解决较高QoS的业务,而MBS广播则专注于较低QoS的业务。NR MBS WI的目标包含在提案RP-220428中,并且该WI与SA2 WI在5G组播业务架构增强提案SP-201106上配合工作。

本文也按照MBS WI交付两种模式来作为行文顺序,


Part1:MBS Multicast(MBS多播)部分:

MBS多播技术为处于RRC_CONNECTED连接态的UE提供MBS传送模式,此技术采用的特性包括:

Group scheduling(组调度)

设计使用了被称为CFR(common frequecy resource,通用频率资源)的频域资源用于多播调度,它是在专用单播BWP中配置的具有多个连续prb的“MBS频域”。可以通过RRC信令配置一组终端,并配置新设计引入的RNTI类型G-RNTI进行组播调度,也可以配置下行SPS和G-CS-RNTI进行MBS组播调度。也就是说,在MBS情况中,gNB使用G-RNTI(或G-CS-RNTI)调度一个传输块到终端组。

在HARQ反馈方面的改进

通过HARQ反馈可进一步提高组调度效率,MBS支持以下两种HARQ反馈报告方式:

在第一种HARQ反馈报告模式下,按照传统思路进行,如果终端正确接收到传输块,则在PUCCH中发送带有HARQ-ACK信息。而如果终端没有正确接收到传输块,则发送HARQ-NACK值。对于第二种HARQ反馈报告模式,只有当终端没有正确接收到传输块时,才会在PUCCH中发送带有HARQ-NACK信息。

对于UE,也可以半静态或动态地禁用多播HARQ报告。

Dynamic PTP(Point-To-Point)/PTM switch for MBS multicast

对于gNB来说,基于G-RNTI (PTM)的调度并不总是高效的,有时基于PTP的调度(与单播相同)由于成熟先进的单播机制可以带来更多的好处。基于通用的PDCP实体,gNB可以决定在某个时间向终端发送MBS组播会话的数据是使用PTM还是PTP。gNB根据MBS会话QoS需求、联合调度终端数量、终端对链路质量的反馈等信息进行决策,以确保业务无论选择使用何种传输方式都能满足QoS要求,且合理高效。这个机制的图示如下:

Lossless handover for MBS multicast

为了支持高级QoS服务,在切换过程中需要保证数据的无损传递。为了实现无损切换,应确保源和目标RAN节点之间的PDCP SN序列号的同步,通过以下方法之一或组合来实现:

从NG-U提供的DL MBS QFI SNs衍生出PDCP SNs;在NG-RAN部署一个共享的NG-U终接点,由多个gNB共享,其中包括一个用于分配PDCP SN的共用实体。

这个机制图示如下:


Part2:MBS Broadcast(MBS广播)部分:

MBS广播为终端在所有RRC状态下提供仅下行的MBS下发方式(这点与多播很不同了),解决较低QoS业务,其主要特点如下:

Group Scheduling

与多播一样,为广播调度设计使用通用频率资源(CFR)被定义为具有多个连续PRB的“MBS频域”,其中G-RNTI可以用于调度相关的MBS广播服务。CFR的带宽可以等于或大于初始BWP, BWP由系统信息显示。MBS广播不支持HARQ反馈和HARQ重传。

MBS broadcast Configuration

终端可以通过广播控制通道MCCH接收广播服务的MBS配置,RRC_IDLE、RRC_INACTIVE、RRC_CONNECTED状态终端也可以通过广播控制通道MCCH接收广播服务的MBS配置。(这个比较强大哈,只是多了MCCH资源消耗,没有免费的午餐)

Service Continuity

不保证MBS广播的无损移动性,但通过一些机制来支持广播业务的连续性。NR MBS广播支持MBS频率优先化,使RRC_IDLE/RRC_INACTIVE中的UE能够选择合适的频率来接收自己感兴趣的服务。为保证终端在“RRC_CONNECTED”下的MBS广播业务连续性,终端可以向gNB发送所谓的Interest Indication,随后gNB可以对终端进行配置使终端能够使用MBS广播接收到自己感兴趣的业务。

bthread栈创建和切换详解

相关汇编x86-64CPU通用寄存器

通用寄存器分为寄存器分为被调用者保存寄存器,和调用者保存寄存器。假设有P调用Q的过程,如果值放在了被调用者保存寄存器中,那么需要保证它们的值在Q返回到P时与P刚调用Q时是一样的。Q如何保证这些值不变对于P来是透明的。详解见x86-64寄存器和栈帧

标号作用%rax返回值%rbx被调用者保存寄存器%rcx第4个参数%rdx第3个参数%rsi第2个参数%rdi第1个参数%rbp被调用者保存寄存器%rsp栈指针%r8第5个参数%r9第6个参数%r10调用者保存寄存器%r11调用者保存寄存器%r12被调用者保存寄存器%r13被调用者保存寄存器%r14被调用者保存寄存器%r15被调用者保存寄存器切换用到的x86-64常用汇编指令指令作用pushq将寄存器的值入栈popq值从栈pop到寄存器里movq将一个寄存器的值保存到另一个寄存器leaq将地址直接赋值给操作数cmp比较两个操作数的大小,比较结果存入flag寄存器,eg:执行完ZF=1说明相等,因为零标志为1说明结果为0je根据ZF标志以决定是否转移,ZF=1则跳转jmp无条件跳转stmxcsr将MXCSR寄存器中的值保存到操作数中ldmxcsr将操作数中的值加载到MXCSR寄存器中fnstcw把控制寄存器的内容存储到由操作数指定的字存储单元fldcw将由操作数指定的字存储单元内容存储到控制寄存器中bthread栈的创建

栈的结构如下:

struct ContextualStack { bthread_fcontext_t context; //本质就是void *,是栈顶指针 StackType stacktype; //栈的类型,为了栈内存的复用而设计,不同栈类型复用不同的栈结构 StackStorage storage; //栈的内存 }; enum StackType { //栈的类型 STACK_TYPE_MAIN = 0, STACK_TYPE_PTHREAD = BTHREAD_STACKTYPE_PTHREAD, STACK_TYPE_SMALL = BTHREAD_STACKTYPE_SMALL, STACK_TYPE_NORMAL = BTHREAD_STACKTYPE_NORMAL, STACK_TYPE_LARGE = BTHREAD_STACKTYPE_LARGE }; struct StackStorage { int stacksize; int guardsize; // Assume stack grows upwards. // http://www.boost.org/doc/libs/1_55_0/libs/context/doc/html/context/stack.html void* bottom; // 栈的底部 unsigned valgrind_stack_id; // Clears all members. void zeroize() { stacksize = 0; guardsize = 0; bottom = NULL; valgrind_stack_id = 0; } };

下面就是创建栈的工厂方法:

template <typename StackClass> struct StackFactory { struct Wrapper : public ContextualStack { explicit Wrapper(void (*entry)(intptr_t)) { // 分配存储空间 if (allocate_stack_storage(&storage, *StackClass::stack_size_flag, FLAGS_guard_page_size) != 0) { storage.zeroize(); context = NULL; return; } // 栈内部构造 context = bthread_make_fcontext(storage.bottom, storage.stacksize, entry); stacktype = (StackType)StackClass::stacktype; } ~Wrapper() { if (context) { context = NULL; deallocate_stack_storage(&storage); storage.zeroize(); } } }; ...... } 而`bthread_make_fcontext(void* sp, size_t size, void (* fn)( intptr_t));`方法是由汇编完成的,我们来看看x86-64平台下的此函数的实现 ```c #if defined(BTHREAD_CONTEXT_PLATFORM_linux_x86_64) && defined(BTHREAD_CONTEXT_COMPILER_gcc) __asm ( ".text\n" # .text表示代码段 ".globl bthread_make_fcontext\n" #表示bthread_make_fcontext是链接用到的符号 ".type bthread_make_fcontext,@function\n" #将bthread_make_fcontext的属性设置为函数 ".align 16\n" # 16字节对齐,本伪指令下面的内存变量必须从下一个能被16整除的地址开始分配 "bthread_make_fcontext:\n" #函数体开始 " movq %rdi, %rax\n" # 第一个参数放入到rax中,也就是栈底指针 " andq $-16, %rax\n" # 对齐操作 " leaq -0x48(%rax), %rax\n" #rax保存的地址-72后,再写入rax中。预先分配的内存供寄存器保存 " movq %rdx, 0x38(%rax)\n" #将第三个参数写入rax+56的地址中,即函数的入口函数的指针 " stmxcsr (%rax)\n" #将MXCSR寄存器中的值保存中rax指向的存储单元 " fnstcw 0x4(%rax)\n" #将控制寄存器的内容保存到rax+4的存储单元 " leaq finish(%rip), %rcx\n" # 根据rip计算finish的地址,写入rcx中 " movq %rcx, 0x40(%rax)\n" # 将rcx的值存入到rax+64的地址中 " ret \n" #函数执行结束返回 "finish:\n" " xorq %rdi, %rdi\n" # 存储器参数的rdi寄存器清0 " call _exit@PLT\n" # 退出 " hlt\n" # 暂停 ".size bthread_make_fcontext,.-bthread_make_fcontext\n" #设定指定符号的大小,"."表示当前地址-符号的地址=整个函数的大小 ".section .note.GNU-stack,\"\",%progbits\n" #保护代码,禁止生成可执行堆栈 ".previous\n" ); #endif bthread的切换

切换由此函数void TaskGroup::sched_to(TaskGroup** pg, TaskMeta* next_meta) {}来触发,在此函数中

if (cur_meta->stack != NULL) { if (next_meta->stack != cur_meta->stack) { jump_stack(cur_meta->stack, next_meta->stack); // probably went to another group, need to assign g again. g = tls_task_group; } ...... } inline void jump_stack(ContextualStack* from, ContextualStack* to) { bthread_jump_fcontext(&from->context, to->context, 0/*not skip remained*/); }

jump_stack调用的是bthread_jump_fcontext,也是直接用汇编实现的,代码和解释如下:

#if defined(BTHREAD_CONTEXT_PLATFORM_linux_x86_64) && defined(BTHREAD_CONTEXT_COMPILER_gcc) __asm ( ".text\n" ".globl bthread_jump_fcontext\n" ".type bthread_jump_fcontext,@function\n" ".align 16\n" "bthread_jump_fcontext:\n" " pushq %rbp \n" //根据x86-64寄存器使用规定——调用者寄存器压榨 " pushq %rbx \n" " pushq %r15 \n" " pushq %r14 \n" " pushq %r13 \n" " pushq %r12 \n" " leaq -0x8(%rsp), %rsp\n" # 栈顶指针上移动8个字节,可能用于保存2个32位的值,也就是下面的mxcsr和控制寄存器的值。 " cmp $0, %rcx\n" # 第四个参数和0先比,即是否要保存fpu,影响zf位 " je 1f\n" # 表示第四个参数和0相比,若相等则跳转到标签为1的地方 " stmxcsr (%rsp)\n" #mxcsr寄存器的32位保存到rsp指向地址 " fnstcw 0x4(%rsp)\n" # 保存控制寄存器的32位值保存到rsp+4的地址中 "1:\n" " movq %rsp, (%rdi)\n" # 将栈顶指针存入到第一个参数中,也就是保存切出bthread的栈顶 " movq %rsi, %rsp\n" #将第二个参数的地址换入到栈指针中,也就是切换到新的bthread的栈顶 " cmp $0, %rcx\n" #第四个参数和0先比,即是否要保存fpu,影响zf位 " je 2f\n" " ldmxcsr (%rsp)\n" " fldcw 0x4(%rsp)\n" "2:\n" " leaq 0x8(%rsp), %rsp\n" #rsp向下移64字节,掠过mxcsr和fpu的部分 " popq %r12 \n" #切换前压栈寄存器值的恢复,如果是新栈,则r12-rbp为空值 " popq %r13 \n" " popq %r14 \n" " popq %r15 \n" " popq %rbx \n" " popq %rbp \n" " popq %r8\n" # 切出去调用函数的push rip的值,也就是新栈的entry或是旧栈恢复后需要继续执行的位置 " movq %rdx, %rax\n" #rdx也就是第三个参数,也就是是否skip remained,目前都是0 " movq %rdx, %rdi\n" #是否skip remained也是bthread的entry task_runner的参数 " jmp *%r8\n" # 无条件跳转到新栈的entry或旧栈恢复后需要继续执行的位置 ".size bthread_jump_fcontext,.-bthread_jump_fcontext\n" ".section .note.GNU-stack,\"\",%progbits\n" ".previous\n" ); #endif

其中需要注意的是popq r8,在bthread调用切换函数的时候会push rip,对于旧堆栈这个popq取到的就是这个原来push的rip的值。

切换整体上可以分为两大类,切换到一个新建的bthread和切换到一个运行过但未结束的bthread,以上处理过程都能覆盖,前面讲到的新建stack中间空置的部分就是为了对应这个处理流程,新栈和旧栈顶部的结构都是一样的,都是数据寄存器数据的区域,区别就在于下面是否有其他数据,新栈直接就到底部了,运行过的会有原来的其他数据。

从而回答了为什么要bthread_make_fcontext函数,即寄存器的空间都分配好,可以直接使用pop命令,从而jump_stack可以将新建的bthread和运行未结束的bthread都统一起来。

参考文献

brpc源码解析(十五)—— bthread栈创建和切换详解

本文使用 Zhihu On VSCode 创作并发布

这是什么学科(需要具体科目内容)的题啊?

谢邀~

这个是「RT-Thread 开发者能力认证考试样题」里的题目

题主可以找RT-Thread操作系统教程官方教程或者是网上搜索有关RT-Thread操作系统的视频讲解

不过这里面也涉及到了一些其他的计算机学科知识

第1题涉及到C语言相关知识

第2题、第3题、第9题涉及到计算机结构相关知识

第4题、第7题、第11题涉及到操作系统相关知识

第5题涉及到计算机网络相关知识

第13题涉及到数据结构相关知识

这里放一下答案好啦(因为这套模拟题没有答案,我是在网上按每一道题的内容搜的,所以不一定对,第8题和第12题目前还没有找到答案)

1.对两个字符a和b进行初始化:char a[]="ABCDEF";char b[]={'A','B','C','D','E','F'};则以下叙述正确的是:

A.a数组比b数组长度长

B.a与b长度相同

C.a与b数组完全相同

D.a和b中都存放字符串

答案:A


2.倘若一款存储器的数据线条数为16条,地址线条数为20条,那么此存储器的容量有多少?

A.32Mb

B.16Kb

C.20Mb

D.16Mb

答案:D


3.计算机在一个指令周期内, 从内存中读取指令操作码需要先将______的内容送到地址总线?

A.程序计数器

B.控制寄存器

C.状态寄存器

D.通用寄存器

答案:A


4.在多任务系统中,线程的栈存放的数据包括什么?

A.线程的数据

B.函数的局部变量

C.系统中的全局变量

D.任务的上下文环境

答案:B


5.每个子网有不超过58个节点要分配的IPv4地址,不适合的子网掩码是

A.255.255.255.248

B.255.255.255.192

C.255.255.255.224

D.255.255.255.220

答案:B


6.下列关于SPI的描述正确的是?

A.SPI是一种异步通信总线

B.SPI以主从方式工作,通常有一个主设备和一个或多个从设备

C.SPI主设备通过CS选择要通信的从设备

D.SPI主设备数据通过MOSI 输出给从设备,同时通过 MISO 接收从设备发送的数据

答案:B


7.以下关于线程间通信说法正确的是?

A.互斥量的值只有两个

B.在持有互斥量的过程中,不得再行更改持有互斥量线程的优先级

C.事件集的事件可以这样设置:事件 1 为 0x0001,事件 2 为 0x0002,事件 3 为 0x0003,以此类推

D.互斥量不能在中断服务例程中使用

答案:A


8.下面哪行代码可以导出一个cpu命令到msh中?

A.MSH_CMD_EXPORT(cpu, show cpu info);

B.FINSH_FUNCTION_EXPORT_ALIAS(rt_cpu, cpu, show cpu info);

C.MSH_CMD_EXPORT(__cmd_cpu, show cpu info);

D.FINSH_FUNCTION_EXPORT_ALIAS(rt_cpu, __cmd_cpu, show cpu info);

答案:?


9.关于ARM处理器寄存器说法错误的是?

A.R0-R15寄存器的宽度为32位

B.通用寄存器R0-R7既可以存放数据也可以存放地址

C.PC使用R15寄存器

D.SP使用R14寄存器,是堆栈指针

答案:D


10.关于RTOS与前后台系统,以下说法正确的是?

A前后台系统中,触发特定的事件(中断)后,开始在后台运行中断服务例程

B.RTOS的CPU使用率高,实时性低

C.RTOS实时性高,系统结构耦合性低

D.前后台的延时与RTOS的延时原理相同

答案:A


11.关于实时操作系统(RTOS) 的任务调度器,以下描述中正确的是?

A.任务之间的公平性是最重要的调度目标

B.大多RTOS调度算法都是可抢占式的

C.RTOS调度器一般采用基于时间片轮转的调度算法

D.RTOS调度算法是一种静态优先级调度算法

答案:B


12.一些处理器具有2套寄存器,一套用于整数操作,一套用于浮点数操作。整数寄存器总是在抢占式内核的每次上下文切换期间被保存并恢复。以下说法正确的是?

A.无论什么情况下,浮点寄存器必须总是进行保存和恢复

B.浮点操作仅在单个线程中进行时,上下文切换必须先保存和恢复浮点寄存器

C.浮点操作仅在多个线程中进行时,上下文切换必须先保存和恢复浮点寄存器

D.浮点操作在单个或者多个线程中进行时,上下文切换都必须先保存和恢复浮点寄存器

答案:?


13.现有初始状态均为空的栈X和队列Y,元素a、b、c、d、e、f、g依次进入栈X,每个元素出栈后即进入队列 Y, 如果出队列的顺序为b、c、f、e、 g、d、a,则要求栈X最小容量为__________。

答案:4

11个经典运算放大器电路(收藏备用)

运算放大器组成的电路五花八门,令人眼花瞭乱,是模拟电路中学习的重点。在分析它的工作原理时倘没有抓住核心,往往令人头大。特搜罗天下运放电路之应用,来个“庖丁解牛”,希望各位从事电路板维修的同行,看完后有所收获。

遍观所有模拟电子技朮的书籍和课程,在介绍运算放大器电路的时候,无非是先给电路来个定性,比如这是一个同向放大器,然后去推导它的输出与输入的关系,然 后得出Vo=(1+Rf)Vi,那是一个反向放大器,然后得出Vo=-Rf*Vi……最后学生往往得出这样一个印象:记住公式就可以了!如果我们将电路稍 稍变换一下,他们就找不着北了!偶曾经面试过至少100个以上的大专以上学历的电子专业应聘者,结果能将我给出的运算放大器电路分析得一点不错的没有超过 10个人!其它专业毕业的更是可想而知了。

今天,芯片级维修教各位战无不胜的两招,这两招在所有运放电路的教材里都写得明白,就是“虚短”和“虚断”,不过要把它运用得出神入化,就要有较深厚的功底了。

虚短和虚断的概念

由于运放的电压放大倍数很大,一般通用型运算放大器的开环电压放大倍数都在80dB以上。而运放的输出电压是有限的,一般在 10 V~14 V。因此运放的差模输入电压不足1 mV,两输入端近似等电位,相当于 “短路”。开环电压放大倍数越大,两输入端的电位越接近相等。

“虚短”是指在分析运算放大器处于线性状态时,可把两输入端视为等电位,这一特性称为虚假短路,简称虚短。显然不能将两输入端真正短路。

由于运放的差模输入电阻很大,一般通用型运算放大器的输入电阻都在1MΩ以上。因此流入运放输入端的电流往往不足1uA,远小于输入端外电路的电流。故通常可把运放的两输入端视为开路,且输入电阻越大,两输入端越接近开路。

“虚断”是指在分析运放处于线性状态时,可以把两输入端视为等效开路,这一特性 称为虚假开路,简称虚断。显然不能将两输入端真正断路。

在分析运放电路工作原理时,首先请各位暂时忘掉什么同向放大、反向放大,什么加法器、减法器,什么差动输入……暂时忘掉那些输入输出关系的公式……这些东东只会干扰你,让你更糊涂﹔也请各位暂时不要理会输入偏置电流、共模抑制比、失调电压等电路参数,这是设计者要考虑的事情。我们理解的就是理想放大器(其实在维修中和大多数设计过程中,把实际放大器当做理想放大器来分析也不会有问题)。

好了,让我们抓过两把“板斧”------“虚短”和“虚断”,开始“庖丁解牛”了。

图一运放的同向端接地=0V,反向端和同向端虚短,所以也是0V,反向输入端输入电阻很高,虚断,几乎没有电流注入和流出,那么R1和R2相当于是串联 的,流过一个串联电路中的每一只组件的电流是相同的,即流过R1的电流和流过R2的电流是相同的。流过R1的电流I1 = (Vi - V-)/R1 ……a 流过R2的电流I2 = (V- - Vout)/R2 ……b V- = V+ = 0 ……c I1 = I2 ……d 求解上面的初中代数方程得Vout = (-R2/R1)*Vi 这就是传说中的反向放大器的输入输出关系式了。

图二中Vi与V-虚短,则 Vi = V- ……a 因为虚断,反向输入端没有电流输入输出,通过R1和R2 的电流相等,设此电流为I,由欧姆定律得:I = Vout/(R1+R2) ……b Vi等于R2上的分压, 即:Vi = I*R2 ……c 由abc式得Vout=Vi*(R1+R2)/R2 这就是传说中的同向放大器的公式了。

图三中,由虚短知:V- = V+ = 0 ……a 由虚断及基尔霍夫定律知,通过R2与R1的电流之和等于通过R3的电流,故 (V1 – V-)/R1 + (V2 – V-)/R2 = (Vout – V-)/R3 ……b 代入a式,b式变为V1/R1 + V2/R2 = Vout/R3 如果取R1=R2=R3,则上式变为Vout=V1+V2,这就是传说中的加法器了。

(编辑者注)质疑:(V1 – V-)/R1 + (V2 – V-)/R2 = (V- – Vout)/R3 ……b 图三公式中少了个负号?

请看图四。因为虚断,运放同向端没有电流流过,则流过R1和R2的电流相等,同理流过R4和R3的电流也相等。故 (V1 – V+)/R1 = (V+ - V2)/R2 ……a (Vout – V-)/R3 = V-/R4 ……b 由虚短知:V+ = V- ……c 如果R1=R2,R3=R4,则由以上式子可以推导出 V+ = (V1 + V2)/2 V- = Vout/2 故 Vout = V1 + V2 也是一个加法器,呵呵!

图五由虚断知,通过R1的电流等于通过R2的电流,同理通过R4的电流等于R3的电流,故有 (V2 – V+)/R1 = V+/R2 ……a (V1 – V-)/R4 = (V- - Vout)/R3 ……b 如果R1=R2, 则V+ = V2/2 ……c 如果R3=R4, 则V- = (Vout + V1)/2 ……d 由虚短知 V+ = V- ……e 所以 Vout=V2-V1 这就是传说中的减法器了。

图六电路中,由虚短知,反向输入端的电压与同向端相等,由虚断知,通过R1的电流与通过C1的电流相等。通过R1的电流 i=V1/R1 通过C1的电流i=C*dUc/dt=-C*dVout/dt 所以 Vout=((-1/(R1*C1))∫V1dt 输出电压与输入电压对时间的积分成正比,这就是传说中的积分电路了。若V1为恒定电压U,则上式变换为Vout = -U*t/(R1*C1) t 是时间,则Vout输出电压是一条从0至负电源电压按时间变化的直线。

图七中由虚断知,通过电容C1和电阻R2的电流是相等的,由虚短知,运放同向端与反向端电压是相等的。则:Vout = -i * R2 = -(R2*C1)dV1/dt 这是一个微分电路。如果V1是一个突然加入的直流电压,则输出Vout对应一个方向与V1相反的脉冲。

图八.由虚短知 Vx = V1 ……a Vy = V2 ……b 由虚断知,运放输入端没有电流流过,则R1、R2、R3可视为串联,通过每一个电阻的电流是相同的, 电流I=(Vx-Vy)/R2 ……c 则:Vo1-Vo2=I*(R1+R2+R3) = (Vx-Vy)(R1+R2+R3)/R2 ……d 由虚断知,流过R6与流过R7的电流相等,若R6=R7, 则Vw = Vo2/2 ……e 同理若R4=R5,则Vout – Vu = Vu – Vo1,故Vu = (Vout+Vo1)/2 ……f 由虚短知,Vu = Vw ……g 由efg得 Vout = Vo2 – Vo1 ……h 由dh得 Vout = (Vy –Vx)(R1+R2+R3)/R2 上式中(R1+R2+R3)/R2是定值,此值确定了差值(Vy –Vx)的放大倍数。这个电路就是传说中的差分放大电路了。

分析一个大家接触得较多的电路。很多控制器接受来自各种检测仪表的0~20mA或4~20mA电流,电路将此电流转换成电压后再送ADC转换成数字信号,图九就是这样一个典型电路。如图4~20mA电流流过采样100Ω电阻R1,在R1上会产生0.4~2V的电压差。由虚断知,运放输入端没有电流流过,则流过R3和R5的电流相等,流过R2和R4的电流相等。故:(V2-Vy)/R3 = Vy/R5 ……a (V1-Vx)/R2 = (Vx-Vout)/R4 ……b 由虚短知:Vx = Vy ……c 电流从0~20mA变化,则V1 = V2 + (0.4~2) ……d 由cd式代入b式得(V2 + (0.4~2)-Vy)/R2 = (Vy-Vout)/R4 ……e 如果R3=R2,R4=R5,则由e-a得Vout = -(0.4~2)R4/R2 ……f 图九中R4/R2=22k/10k=2.2,则f式Vout = -(0.88~4.4)V,即是说,将4~20mA电流转换成了-0.88 ~ -4.4V电压,此电压可以送ADC去处理。

电流可以转换成电压,电压也可以转换成电流。图十就是这样一个电路。上图的负反馈没有通过电阻直接反馈,而是串联了三极管Q1的发射结,大家可不要以为是一个比较器就是了。只要是放大电路,虚短虚断的规律仍然是符合的!

由虚断知,运放输入端没有电流流过,则 (Vi – V1)/R2 = (V1 – V4)/R6 ……a同理 (V3 – V2)/R5 = V2/R4 ……b由虚短知 V1 = V2 ……c如果R2=R6,R4=R5,则由abc式得V3-V4=Vi上式说明R7两端的电压和输入电压Vi相等,则通过R7的电流I=Vi/R7,如果负载RL<<100KΩ,则通过Rl和通过R7的电流基本相同。

来一个复杂的,呵呵!图十一是一个三线制PT100前置放大电路。

PT100传感器引出三根材质、线径、长度完全相同的线,接法如图所示。有2V的电压加在由R14、R20、R15、Z1、PT100及其线电阻组成的桥电路上。Z1、Z2、Z3、D11、D12、D83及各电容在电路中起滤波和保护作用,静态分析时可不予理会,Z1、Z2、Z3可视为短路,D11、D12、D83及各电容可视为开路。

由电阻分压知, V3=2*R20/(R14+20)=200/1100=2/11 ……a 由虚短知,U8B第6、7脚 电压和第5脚电压相等 V4=V3 ……b 由虚断知,U8A第2脚没有电流流过,则流过R18和R19上的电流相等。(V2-V4)/R19=(V5-V2)/R18 ……c 由虚断知,U8A第3脚没有电流流过, V1=V7 ……d 在桥电路中R15和Z1、PT100及线电阻串联,PT100与线电阻串联分得的电压通过电阻R17加至U8A的第3脚, V7=2*(Rx+2R0)/(R15+Rx+2R0) …..e 由虚短知,U8A第3脚和第2脚电压相等,V1=V2 ……f 由abcdef得, (V5-V7)/100=(V7-V3)/2.2 化简得 V5=(102.2*V7-100V3)/2.2 即 V5=204.4(Rx+2R0)/(1000+Rx+2R0) – 200/11 ……g 上式输出电压V5是Rx的函数我们再看线电阻的影响。Pt100最下端线电阻上产生的电压降经过中间的线电阻、Z2、R22,加至U8C的第10脚,由虚断知, V5=V8=V9=2*R0/(R15+Rx+2R0) ……a (V6-V10)/R25=V10/R26 ……b 由虚短知, V10=V5 ……c 由式abc得 V6=(102.2/2.2)V5=204.4R0/[2.2(1000+Rx+2R0)] ……h 由式gh组成的方程组知,如果测出V5、V6的值,就可算出Rx及R0,知道Rx,查pt100分度表就知道温度的大小了。

相关推荐

发布评论