JZ2440开发板——异常与中断

以下内容源于韦东山课程的学习与整理,如有侵权请告知删除。

一、中断概念的引入与处理流程

1.1 中断概念的引入 

这里有一个很形象的场景比喻:假设一位母亲在大厅里看书,婴儿在房间里睡觉,这位母亲怎样才能知道这个孩子睡醒了?

方式一:过一会就打开一次房门,查看婴儿是否睡醒,然后接着看书。

方式二:一直等到婴儿发出声音以后再过去查看,期间都在读书。

第一种方式叫做轮询方式,优点是简单,缺点是很累。

while(1)
{(1)read book(2)open doorif 睡 继续读书if 醒 照顾小孩
}

第二种方式叫做中断方式,缺点是复杂,优点是轻松。

while(1)
{read book
}中断服务子程序
{照顾小孩
}

1.2 处理流程 

对于中断方式,中断服务子程序是怎样被调用的?这是中断体系的核心问题。

1.2.1 母亲的处理流程

仍然以上面例子进行对比说明,母亲的处理流程如下:

1)母亲平时是在看书;

2)房子出现各种声音:远处的猫叫、快递员按响的门铃声、小孩的哭声;

3)母亲的处理过程:

  1. 在书中放入书签(对应ARM:保存现场);
  2. 去处理(对于猫叫,忽略;对于门铃声,开门取快递;对于小孩哭声,去照顾小孩)(对应ARM:处理异常);
  3. 处理完之后继续看书(对应ARM:恢复现场)。

远处的猫叫、快递员按响的门铃声、小孩哭声,这些声音先传入耳朵,耳朵再将信号传给大脑。除了这些可以打断母亲看书,还有其他特殊情况,比如身体不舒服、有只蜘蛛掉下来,对于这些特殊情况无法回避,必须立即处理。如下所示:

1.2.2 ARM系统对中断的处理流程

对于我们的ARM系统,它有CPU和中断控制器。按键、定时器、网络数据等情况都可以发送信号给中断控制器,再由中断控制器发送信号给CPU来表明产生了这些中断(中断属于一种异常)。另外指令不对、数据访问有问题(比如内存访问出错)、reset信号等情况,都会打断CPU的正常运行。

ARM系统对异常的处理过程,具体过程如下:

1)首先进行初始化:设置中断源、设置中断控制器、设置CPU总开关。

  • 设置中断源。通过设置中断源,让它可以产生中断,比如某个按键可以产生中断,则需要将它对应的GPIO引脚设置为中断引脚。
  • 设置中断控制器。通过设置中断控制器,可以选择屏蔽某个中断(好比母亲可以选择忽略远处的猫叫),可以设置中断的优先级(同时有几个中断发生时先处理哪一个)。
  • 设置CPU总开关。CPU里面有一个总开关,可以设置是否使能中断。

2)执行正常的程序。好比母亲在正常地看书。

3)产生中断。比如按下按键,中断信号发给中断控制器,中断控制器又发信号给CPU。

4)CPU每执行完一条指令,都会去检查是否有中断\异常产生。

5)发现有中断\异常产生,开始处理。

对于不同的异常,需要跳到不同的地址去执行该地址上的指令(也就是根据不同的异常,给PC赋予不同的地址值)。这些地址一般是排在一起的,而且每个地址上只存储着一条跳转指令,这条跳转指令表示跳到某处去执行某个函数。

比如复位异常发生时,PC就会被赋值0x00,而0x00地址上存储的指令是“b reset”,那么接下来将跳转到reset函数处开始执行;比如普通中断发生时,PC就会被赋值0x18,而0x18地址上存储的指令是“ldr pc,_irq”,那么接下来将跳转到_irq函数处开始执行。

代码中一般会有一个异常向量表,它是根据ARM的硬件特性编写的一小段代码。比如u-boot的start.S文件中有以下内容:

6)这些函数做什么事情?保存现场、调用中断服务子程序、恢复现场。

母亲的处理过程与ARM系统对异常的处理过程,两者的类比如下表所示:

母亲的处理过程ARM系统对异常的处理过程
在书中放入书签保存现场
去处理各种声音状况调用中断服务子程序
处理完后继续看书恢复现场

上面的步骤中,步骤3)~5)是由硬件自动完成的,比如普通中断发生时,CPU会自动跳转到0x18这个地址处,不需要人为设置什么;而步骤6)需要由软件来进行设置,比如0x18这个地址上的内容是什么,就需要我们程序员自己编写代码来决定,不过一般都是表示跳转到某个函数(这里叫中断服务子程序)的指令,然后在这个函数中对中断做出相应的操作。

就比如步骤5)中的代码,发生中断时,CPU会自动跳转到0x18这个地址处,那这个地址上的内容是什么呢?这就需要我们程序员自己编写代码来决定,这里我们写的是“ldr pc,_irq”,它表示跳转到_irq这个函数。

二、CPU的工作模式(mode)、状态(state)与寄存器

注意,本节的内容,可以参考杜春蕾《ARM体系结构与编程》。 

2.1 CPU的工作模式(mode)

我们仍然以上面母亲的例子进行说明。母亲的模式包括:

  • 正常模式:母亲懒洋洋很休闲地看书,无压力,效率不高;
  • 兴奋模式:母亲将要考试而紧张看书,有压力,效率很高;
  • 异常模式:母亲因为生病而卧床休息。

而对于ARM的CPU,在S3C2440数据手册P72有以下内容,可知CPU的工作模式分为七种模式,其中用户模式(User)可以认为是正常模式,系统模式(System)可以认为是兴奋模式,剩余的五种模式可以认为是异常模式(这只是为了类比,方便理解)。

也可以根据操作权限分成普通模式、特权模式,其中特权模式又细分为六种模式。注意,同一时刻CPU只会处于其中一种模式。

普通模式(Normal)

(1)用户模式(User):非特权模式,大部分任务执行在这种模式。

特权模式(Privilege)

(2)系统模式(System):使用和User模式相同寄存器集的特权模式。 

(3)快速中断模式(FIQ):当一个高优先级(fast) 中断产生时将会进入这种模式。

(4)普通中断模式(IRQ):当一个低优先级(normal) 中断产生时将会进入这种模式。

(5)超级用户模式(SVC):当复位或软中断指令执行时将会进入这种模式。

(6)中止模式(Abort):当指令预取、数据访问出现异常时(比如读写某条错误的指令,读写某个地址时出错)将会进入这种模式。

(7)未定义指令模式(Undef):当CPU执行某条未定义指令时会进入这种模式。

诸多工作模式是为了满足操作系统的安全等级需要(在有操作系统的情况下,用户模式是给应用程序使用的,写应用程序的人水平参差不齐,不能保证所写的程序是好是坏,所以得限制一下应用程序的权限,以免破坏操作系统;另外之所以有那么多种异常工作模式,是因为在某种工作模式下更容易处理某种异常),各种工作模式下可以访问的寄存器不同。

用户模式下不可以直接进入其他模式,但是可以通过编程修改CPSR寄存器来切换模式,另外CPU在某些情况下也会自动切换模式。

2.2 CPU的状态(state)

在讲述各种工作模式可访问的寄存器之前,先看一下CPU的两种状态:

(1)ARM state:使用ARM指令集,每个指令4byte。

(2)Thumb state:使用Thumb指令集,每个指令2byte。

比如指令“mov r0,r1”被编译成机器码,如果使用ARM指令集则机器码长度为4字节,如果使用thumb指令集则为2个字节。

ARM指令集与Thumb指令集的区别:

Thumb指令可以看作是ARM指令压缩形式的子集,是针对代码密度的问题而提出的,它具有16位的代码密度,但是它不如ARM指令的效率高。Thumb不是一个完整的体系结构,不能指望一个程序只使用到Thumb指令集,必须借助于ARM指令集。Thumb指令只需要支持通用功能,必要时可以借助于完善的ARM指令集,比如,所有异常自动进入ARM状态。在编写Thumb指令时,先要使用伪指令CODE16进行声明,而且在ARM指令中要使用BX指令跳转到Thumb指令,以切换处理器状态;在编写ARM指令时,则可使用伪指令CODE32声明。

引入thumb指令集的目的,主要为了减少程序体积以节约存储空间,尤其对单片机而言。但是在我们的ARM系统里面,Nand Flash或Nor Flash容量很大,根本没必要去节省这一点空间,所以我们一直使用的都是ARM指令集。

在第三节中将演示使用Thumb指令集进行编译,看生成的bin文件是否会变小很多。

2.3 CPU的寄存器

在S3C2440数据手册P73有以下内容: 

从这个图中,我们可以得知以下信息:

1、每种工作模式下,都可以访问r0~r15寄存器。 

2、有些寄存器画有阴影三角形,它表示专属于该模式的寄存器;没有画阴影三角形的寄存器,则表示所有模式都可以访问的寄存器。

比如对于指令“mov r0,r8”,在用户模式下和在FIQ模式下,所访问的r0都是同一个r0,但所访问的r8不是同一个r8,因为FIQ模式下所访问的r8是专属于FIQ模式的r8,与用户模式下所访问的r8不是同一个寄存器(尽管名字相同但物理实体不同)。

3、FIQ模式拥有更多的专属寄存器,这是为什么呢?

回顾一下中断处理流程,包括保存现场、调用中断服务子程序、恢复现场。

保存现场,也就是保存(原来模式被中断时)原来模式下的寄存器的值。比如程序正在用户模式下运行,当发生中断时,假设需要把用户模式下的r0~r14这些寄存器的内容全部保存下来,然后去处理异常,最后恢复这些寄存器。

但如果是快中断,则不需要保存用户模式下的r8~r14这几个寄存器,因为在FIQ模式下有自己专属的r8~r14寄存器,这可以节省保存寄存器的时间(FIQ模式说,我有专属的r8~r14寄存器,不必把外部的r8~r14寄存器的内容保存下来,因为我根本不会去修改外部的r8~r14寄存器),从而加快处理速度。

在Linux系统中,并不会使用FIQ模式(什么时候用这种模式?)。

4、CPSR(Current Program Status Register),当前程序状态寄存器。

由图可知,整个CPU只有1个CPSR寄存器。该寄存器用来记录CPU的当前状态,它的位含义如下所示: 

(1)CPSR[4:0]:表示当前CPU处于哪一种工作模式。

通过读取CPSR[4:0],我们可以判断CPU处于哪一种工作模式;也可以通过修改CPSR[4:0]来进入某一种工作模式,但是如果当前你处于用户模式,是没有权限修改这些CPSR[4:0]的。

以下是CPSR[4:0]与工作模式的关系图,位于S3C2440数据手册的P78。

(2)CPSR[5]:表示CPU工作于Thumb State还是ARM State,即使用的指令集是什么。

(3)CPSR[6]:当CPSR[6]等于1时,表示禁止FIQ。

(4)CPSR[7]:当CPSR[7]等于1时,表示禁止IRQ。这个位是IRQ的总开关。

(5)CPSR[27:8]:保留位。

(6)CPSR[31:28]:条件标志位。有些指令需要根据条件标志位来判断是否要执行,我们比较关注其中的Z位,即CPSR[30]。

比如以下指令,指令“cmp r0,r1”的比较结果会影响到Z位:如果r0等于r1则Z位等于1,否则为0。然后指令“beq xxx”会判断Z位是否为1,如果为1则跳转到xxx地址处,如果不是1则不会跳转。

cmp r0,r1
beq xxx

5、SPSR(Saved Program Status Register),程序状态保存寄存器。

由图可知,五种异常模式下都有自己专属的SPSR寄存器。

当发生异常CPU进入其他工作模式时,其他工作模式下专属的SPSR寄存器会保存会原来工作模式下的CPSR。比如当前程序运行在用户模式,此时CPSR是某个值,当发生中断时会进入IRQ模式,那IRQ模式下专属的SPSR将保存用户模式下的CPSR的值。

从其他工作模式切换回原来工作模式时,需要将SPSR寄存器的值赋给CPSR寄存器。

6、r15寄存器、r14寄存器、r13寄存器、r12寄存器

这里之所以把r12~r15寄存器放在一起讲解,是因为它们的别名在我们编程时很常见。

(1)r15寄存器

r15寄存器,也叫PC寄存器(口语)、PC指针(口语)、程序计数器(Program Counter的翻译)。

由图可知,整个CPU只有一个PC寄存器,该寄存器用于存放下一条要执行的指令的地址。

比如子程序返回时,需要将LR寄存器中保存的地址赋值给PC寄存器,即“mov pc,lr”(程序跳转时把目标代码的地址放到PC寄存器中)。

(2)r14寄存器

r14寄存器,也叫LR寄存器(口语)、连接接寄存器(Link Register的翻译)。

由图可知,五种异常模式下都有自己专属的r14寄存器,该寄存器有以下两个作用:

作用一:调用子程序时,用来保存子程序的返回地址(从子程序返回后,主程序继续执行的指令的地址称为子程序的返回地址)。当通过bl或blx指令调用子程序时,硬件会自动将子程序返回地址保存在 LR 寄存器中(如果不是通过 bl 或 blx 指令调用子程序,则需要自己写代码将子程序的返回地址存入LR寄存器);在子程序返回时,把LR的值赋值给PC即可实现子程序返回,比如可以使用mov pc,lr完成子程序返回。(主动情形)

作用二:当异常发生时,LR中保存的值等于异常发生时PC的值+4或+8,因此在各种异常模式下可以根据LR的值返回到异常发生前的相应位置继续执行。(被动情形)

(3)r13寄存器

r13寄存器,也叫sp指针(口语)、栈指针(口语),栈指针寄存器(stack pointer register的翻译)。

由图可知,五种异常模式下都有自己专属的r13寄存器。一般情况下,我们通过给sp指针赋值来确定栈的位置。

在r14寄存器中说到,调用子程序时r14会自动保存子程序的返回地址。如果子程序中继续调用其他子程序时,需要先把r14中已保存的值入栈,否则会被新的r14的值覆盖。

//通常子程序这样写,以保证子程序中还可以调用子程序
stmfd sp!, {lr}  //表示将lr入栈
....
ldmfd sp!, {pc}  //表示将sp的值赋值给pc

以下程序模拟了一个在main中调用func子程序的过程,其中涉及到跳转问题,在跳转到func之后需要对之前的寄存器进行压栈(使用满减栈)保护处理,且程序返回时需要出栈以恢复现场。 

main:mov r1,#1mov r2,#2bl funcadd r3,r1,r2b stopfunc:stmfd sp!,{r1,r2}//入栈指令mov r1,#10mov r2,#20add r3,r1,r2ldmfd sp!,{r1,r2}//出栈指令mov pc,lr //程序调用返回stop:b stop

(4)r12寄存器

r12寄存器,也叫ip寄存器,内部过程调用寄存器(intra-procedure-call scratch register的翻译)。

2.4 发生异常时硬件的处理流程

我们来看看发生异常时CPU是如何协同工作的。在S3C2440数据手册P79有以下内容:

(1)进入异常时的处理流程

  1. 将原来工作模式的下一条指令的地址保存到异常模式专属的LR寄存器中(这个地址有可能是PC+4也有可能是PC+8,取决于发生的是哪一种异常模式,见上表)。
  2. 将原来模式下CPSR的值保存到异常模式下专属的SPSR中。
  3. 通过修改CPSR[4:0]进入异常模式。
  4. 跳转到异常向量表中该异常对应的地址(该地址上存储着一条跳转指令)。

(2)退出异常时的处理流程

  1. 让异常模式专属的LR寄存器中的值,减去某个offset的值(offset的值取决于发生的是哪一种异常模式,见上表),然后赋值给PC。
  2. 把CPSR的值恢复(将异常模式下专属的SPSR的值,赋值给CPSR)。
  3. 清中断(如果是中断这种异常,则对于其他异常不用设置)。

三、thumb指令集示例

2.2节说到CPU有两种状态,其中ARM State每条指令会占据4字节,Thumb State每条指令占据2b字节。

Thumb指令集并不重要(因为它是为了减少程序体积以节约存储空间,但我们ARM的存储容量很大,没必要节约这么一点空间,因此很少用到Thumb指令集),本节通过修改上一章节“代码重定位”最后一节的代码,演示一下如何使用Thumb指令集来编译某程序。

1、修改Makefile文件

对于.c文件,通过搜索查询(比如关键词“gcc使用 thumb”)得知编译时在arm-linux-gcc命令中加上 -mthumb 选项即可: 

all: led.o uart.o init.o main.o start.oarm-linux-ld -T sdram.lds start.o led.o uart.o init.o main.o -o sdram.elfarm-linux-objcopy -O binary -S sdram.elf sdram.binarm-linux-objdump -D sdram.elf > sdram.dis
clean:rm *.bin *.o *.elf *.dis%.o : %.carm-linux-gcc -mthumb -c -o $@ $<%.o : %.Sarm-linux-gcc -c -o $@ $< //这里为什么不加-mthumb选项?因为在汇编代码中,//需要使用一些伪指令来指示是使用ARM指令集还是THUMP指令集

2、修改start.S文件

对于汇编.S文件,需要修改里面的代码,如下所示:  

.text
.global _start
.code 32   //表示后续的指令使用ARM指令集 
_start://这里篇幅缘故,代码同以前一样,省略//……//设置栈//接下来需要调用sdram_init函数,而该函数是.c文件中的函数,
//由Makefile知道.c文件使用thumb指令集
//上面是使用ARM指令集,怎么从ARM State切换到Thumb State?
//使用bx跳转指令,如果要跳转到的地址其最低位bit0=1,则会切换CPU State到thumb stateadr r0, thumb_func //(2)然后获取这个编号add r0, r0, #1  /* bit0=1时, bx就会切换CPU State到thumb state */bx r0 //疑惑:r0已经是r0-1了,还能正确跳转吗?.code 16 //表明下面都使用thumb指令集	
thumb_func:	//(1)这里首先要写一个标号bl sdram_init//bl sdram_init2	 /* 用到有初始值的数组, 不是位置无关码 *//* 重定位text, rodata, data段整个程序 */bl copy2sdram/* 清除BSS段 */bl clean_bss//bl main  /* 使用BL命令相对跳转, 程序仍然在NOR/sram执行 */ldr pc, =main  /* 绝对跳转, 跳到SDRAM */halt:b halt

(1)重新编译,发现报错如下:

xjh@ubuntu:~/iot/embedded_basic/jz2440/armBareMachine/1_thumb_014_003$ make
arm-linux-gcc -mthumb -c -o led.o led.c
arm-linux-gcc -mthumb -c -o uart.o uart.c
uart.c: In function `printHex':
uart.c:73: warning: comparison is always true due to limited range of data type
arm-linux-gcc -mthumb -c -o init.o init.c
arm-linux-gcc -mthumb -c -o main.o main.c
arm-linux-gcc -c -o start.o start.S
start.S: Assembler messages:
start.S:78: Error: lo register required -- `ldr pc,=main'
make: *** [start.o] Error 1
xjh@ubuntu:~/iot/embedded_basic/jz2440/armBareMachine/1_thumb_014_003$

原因是对于thumb指令集,不能像“ldr pc,=mian”这样直接对pc赋值,而是需要像下面这样有一个中介:

ldr r0,=main
mov r0,pc

(2)继续编译,报错如下:

xjh@ubuntu:~/iot/embedded_basic/jz2440/armBareMachine/1_thumb_014_003$ make
arm-linux-gcc -mthumb -c -o init.o init.c
arm-linux-ld -T sdram.lds start.o led.o uart.o init.o main.o -o sdram.elf
init.o(.text+0x6c): In function `sdram_init2':
: undefined reference to `memcpy'
make: *** [all] Error 1
xjh@ubuntu:~/iot/embedded_basic/jz2440/armBareMachine/1_thumb_014_003$ 

其实sdram_init2函数是在说明位置有关码时引入的函数,后续没有用到,这里可以注释掉,不过这有点粗暴。

课程里面提到:在sdram_init2里使用到了memecpy,这是编译器搞的鬼,使用memecpy函数把这些值从代码段拷贝到arr局部变量中。好像没有什么方法禁用编译器自作聪明地使用memecpy,但是可以修改这些变量,比如说将其修改为静态变量,那么这些数据就会存放到数据段中,最终重定位时会把数据段的值拷贝到arr所对应的地址里面去,这样就不需要使用memecpy了,如下描述:

重新编译通过,得知.bin文件大小为1.5KB,而没修改之前的.bin文件大小为2KB左右。

xjh@ubuntu:~/iot/embedded_basic/jz2440/1_thumb_014_003$ ll -h sdram.bin 
-rwxrwxr-x 1 xjh xjh 1.5K 十月  5 17:19 sdram.bin*
xjh@ubuntu:~/iot/embedded_basic/jz2440/1_thumb_014_003$ 

我们看一下反汇编文件:

四、未定义指令异常模式程序示例

在本节中,我们写一个程序,故意让它发生未定义指令异常,然后处理这个异常。

4.1 ldr pc,lable vs ldr pc,=lable

作为学习笔记,这里先记录一个困扰我蛮久的问题,即“ ldr pc,lable 与 ldr pc,=lable有何区别 ”,这是因为我对ldr这个(伪)指令的了解不够而产生的问题。我们参考一下裸机集大成者u-boot的代码,/u-boot-1.1.6/cpu/arm920t/start.S文件:

上面代码中,先设置“ldr pc,_irq”,然后设置“_irq:  .word irq”。怎么理解呢?

首先,我明白“_irq: .word irq”这条语句的含义,就是在_irq这个地址处(标签即地址)存放irq这个地址,如下图所示。

然后,一开始我以为“ldr pc,_irq”就是把_irq赋给pc,即以为pc=_irq(如下图所示),从而让我迷糊了:_irq这个地址处存放的是irq这个地址,而不是一条指令!你让pc指向它而不指向某条指令,那程序还能继续运行吗?

后面经过思考与验证,才知道“ldr pc,_irq”的含义,其实是“ldr pc,[ _irq ]”。也就是将_irq这个标签地址上的内容(即irq)赋给pc,而不是将_irq这个标签地址赋给pc。如下图所示:

也就是说, “ldr pc,lable”其实等价于“ldr pc,[lable]”,有些类似于“ldr r1,[r2]”这种意思(其实应该就是这种意思,因为第二个操作数之前也是没有“=”的)。那“ldr pc,=lable”又是何意呢?“ldr pc,=lable”其实就是让pc=lable(当不确定一个常数是否可以用“立即数”来表示时,可以使用ldr命令来赋值,此时书写形式是“ldr r1,=常数”。编译时将该常数保存在某个位置里,然后使用内存读取指令把它读出来)。

因此上面的代码其实可以这样写:

.globl _start
_start:b   resetldr pc,=undefined_instruction……ldr pc,=irqldr pc,=fiq

那u-boot为什么不写成上面的形式?这是因为写成上面“ldr pc,=irq”这样的形式时,编译器会将irq这个常数会保存在.bin文件的某个不确定的位置中,后面再使用内存读取指令把它读出来。如果.bin文件太大,那么irq这个常数的保存位置可能就超出了4K的范围,此时如果是Nand Flash启动,将无法取到irq这个常数。如下图所示,现在是到300000c0这个地址处取得数据(这个地址还在4K范围内。如果.bin文件非常大,irq这个常数的保存位置,就可能就超出4K的范围)。

 

那就应该让irq这个常数保存在.bin文件很靠前的位置,这样就算是Nand Flash启动,也能够读取到irq这个常数。如何让irq这个常数保存在.bin文件很靠前的位置?就是要写成u-boot那种样式!此时反汇编文件内容如下,可见30000008这个地址就很靠前了。

4.2 代码示例

4.2.1 初版代码 

S3C2440数据手册P82有以下内容:

根据这个异常向量表,我们的代码框架可以这样写:

.text
.global _start
_start:b resetb do_und
do_und:/*保存现场*//*处理und异常*//*恢复现场*/
reset://原来代码

写成的初版代码如下:

.text
.global _start_start:b reset   //如果一上电复位,就会跳到0地址开始执行:跳到reset程序b do_und  //如果发生未定义指令异常,就会跳到0x04地址开始执行:跳到do_und程序do_und:/* 执行到这里之前,硬件自动完成以下内容:* 1. lr_und保存有原来工作模式的下一条即将执行的指令的地址* 2. SPSR_und保存有原来工作模式的CPSR* 3. CPSR中的M4-M0被设置为11011, 进入到und模式* 4. 跳到0x4的地方执行程序(也就是这里) *///----------保存现场---------------//每种工作模式都有自己的栈空间。sp_und未设置, 先设置它ldr sp, =0x34000000 //内存已经初始化,所以可以使用这个地址作为栈顶//在und异常处理函数中有可能会修改r0-r12, 所以先保存;//lr是异常处理完后的返回地址, 也要保存stmdb sp!, {r0-r12, lr}  	//----------处理und异常---------------//这里对异常的处理方式,是调用printException函数打印一些信息mrs r0, cpsr //传参1ldr r1, =und_string //传参2bl printException	//----------恢复现场------------------ldmia sp!, {r0-r12, pc}^  /* ^会把spsr的值恢复到cpsr里 */und_string:.string "undefined instruction exception"reset://关闭看门狗//设置 FCLK:HCLK:PCLK = 400m:100m:50m//设置sp栈//篇幅缘故,省略这些部分的内容bl sdram_init//内存初始化bl copy2sdram//重定位bl clean_bss//清除bss段bl uart0_init //原本uart0_init是在main函数中调用的,但是由于调试需要用到而提前bl print1   //调试作用:puts("abc\n\r");//这里故意加入一条未定义指令
und_code:.word 0xdeadc0de//未定义指令bl print2   //调试作用:puts("123\n\r");//bl main  /* 使用BL命令相对跳转, 程序仍然在NOR/sram执行 */ldr pc, =main  /* 绝对跳转, 跳到SDRAM */halt:b halt

下面是对这段代码的一些解释:

(1)代码流程:一上电复位,CPU跳到地址0开始执行,发现0地址上的指令是“b reset”,于是跳转到reset处执行,完成了一系列的初始化,当执行到und_code处的指令时,CPU发现它不能识别指令0xdeadc0de,于是CPU跳转到地址0x4开始执行,由于该地址上的指令是“b do_und”,于是就跳转到do_und处执行(执行do_und之前,硬件自动完成一系列工作,见注释1~4)。

(2)CPU的每种工作模式都有自己专属的栈空间,将数据入栈进行保存前,需要先设置栈指针。

	ldr sp, =0x34000000 //内存已经初始化,所以可以使用这个地址作为栈顶

(3)保存现场的入栈操作,使用stmdb指令;恢复现场的出栈操作,使用ldmia指令。db和ia要结合起来使用,也就是你入栈时使用db,我出栈时就应该使用ia。db,d before stm,在stm之前先d,即“先减后存”;ia,i after ldm,在ldm之后再i,即“先读后减”。

//保存现场
stmdb sp!, {r0-r12, lr} //恢复现场
ldmia sp!, {r0-r12, pc}^

(4)对cpsr寄存器,需要使用mrs、msr指令来操作。可以这样记忆:r表示某个通用寄存器,s表示cpsr寄存器;mrs、msr这两个指令都是从操作数2指向操作数1的(也就是从右到左)。比如对于“r1,cpsr”使用哪个指令?由于从右到左,这里就是从cpsr到r1,r<--s,那么就该使用mrs;对于“cpsr,r1”则是从r1到cpsr,s<--r,那么就该使用msr。

(5)这里对异常的处理方式,是通过调用printException函数打印一些信息。该函数内容如下,可见有两个参数,参数1表示cpsr寄存器的值,参数2表示字符指针(用于指向字符串的首地址)。

void printException(unsigned int cpsr, char *str)
{puts("Exception! cpsr = ");printHex(cpsr);puts(" ");puts(str);puts("\n\r");
}

那么参数2应该传入一个字符串的首地址,这里是und_string,如下所示:

mrs r0, cpsr //传参1
ldr r1, =und_string //传参2und_string:.string "undefined instruction exception"

(6)添加一条未定义指令,是通过下面这种形式来添加的。

//这里故意加入一条未定义指令
und_code://.word 0xff123456.word 0xdeadc0de//未定义指令

课程里曾经使用过0xff123456,由S3C2440数据手册P86的下图可知,它恰巧是一条SWI指令:

4.2 代码改进

有哪些地方需要改进呢?

1、改进1

原来代码中,“b do_und”使用相对跳转指令b,跳转到do_und函数;而do_und函数中的“bl printException”也使用相对跳转指令bl,跳转到printException函数。如果是Nand Flash启动,那么printException函数有可能在4K之外,此时执行“bl printException”指令则必然出错。因此为了保险,需要跳转到SDRAM中执行SDRAM中的那一份代码。

怎么跳转到SDRAM中呢?将“b do_und”改为“ldr pc,=do_und”即可。这样的话,运行就是SDRAM中的do_und函数,那do_und函数中的“bl printException”,肯定也是跳到SDRAM中的printException函数。

_start:b reset  /* vector 0 : reset *///b do_undldr pc, =do_unddo_und://……

但是上面这种写法存在着4.1节最后的问题,因此需要改成下面这样:

_start:b reset  /* vector 0 : reset *///b do_und//ldr pc, =do_undldr pc,und_addrund_addr:.word do_unddo_und://……
2、改进2

重定位以及清除bss段之后,程序应该立即跳转到SDRAM中去执行SDRAM中的那一份代码,这是因为接下来的内容有可能在4K之外。

	bl clean_bss //清除BSS段
//******添加下面两行内容*******ldr pc, =sdram  /* 绝对跳转, 跳到SDRAM */ //和 ldr pc, =main 含义一致
sdram:
//***************************bl uart0_initbl print1
und_code:.word 0xdeadc0debl print2ldr pc, =main  /* 绝对跳转, 跳到SDRAM */
3、改进3

在字符串后面添加“.align 4”,表示4字节对齐,这样才能保证后面的指令(指的是reset下面的指令)是4字节对齐的。之前没有添加“.align 4”,程序也能够正常运行,那是因为恰巧而已。如果字符串是其它长度,就有可能出错的。

	/* 恢复现场 */ldmia sp!, {r0-r12, pc}^und_string:.string "undefined instruction exception".align 4 //添加这一句代码reset:/* 关闭看门狗 */ldr r0, =0x53000000ldr r1, =0str r1, [r0]
4、总结 

下面来总结一下程序的运行过程:

改进后的代码如下:

_start:b reset  /* vector 0 : reset */ldr pc, und_addr /* vector 4 : und */und_addr:.word do_unddo_und:/* 执行到这里之前:* 1. lr_und保存有被中断模式中的下一条即将执行的指令的地址* 2. SPSR_und保存有被中断模式的CPSR* 3. CPSR中的M4-M0被设置为11011, 进入到und模式* 4. 跳到0x4的地方执行程序 *//* sp_und未设置, 先设置它 */ldr sp, =0x34000000/* 在und异常处理函数中有可能会修改r0-r12, 所以先保存 *//* lr是异常处理完后的返回地址, 也要保存 */stmdb sp!, {r0-r12, lr}  /* 保存现场 *//* 处理und异常 */mrs r0, cpsrldr r1, =und_stringbl printException/* 恢复现场 */ldmia sp!, {r0-r12, pc}^  /* ^会把spsr的值恢复到cpsr里 */und_string:.string "undefined instruction exception".align 4reset:/* 关闭看门狗 */ldr r0, =0x53000000ldr r1, =0str r1, [r0]/* 设置MPLL, FCLK : HCLK : PCLK = 400m : 100m : 50m *//* LOCKTIME(0x4C000000) = 0xFFFFFFFF */ldr r0, =0x4C000000ldr r1, =0xFFFFFFFFstr r1, [r0]/* CLKDIVN(0x4C000014) = 0X5, tFCLK:tHCLK:tPCLK = 1:4:8  */ldr r0, =0x4C000014ldr r1, =0x5str r1, [r0]/* 设置CPU工作于异步模式 */mrc p15,0,r0,c1,c0,0orr r0,r0,#0xc0000000   //R1_nF:OR:R1_iAmcr p15,0,r0,c1,c0,0/* 设置MPLLCON(0x4C000004) = (92<<12)|(1<<4)|(1<<0) *  m = MDIV+8 = 92+8=100*  p = PDIV+2 = 1+2 = 3*  s = SDIV = 1*  FCLK = 2*m*Fin/(p*2^s) = 2*100*12/(3*2^1)=400M*/ldr r0, =0x4C000004ldr r1, =(92<<12)|(1<<4)|(1<<0)str r1, [r0]/* 一旦设置PLL, 就会锁定lock time直到PLL输出稳定* 然后CPU工作于新的频率FCLK*//* 设置内存: sp 栈 *//* 分辨是nor/nand启动* 写0到0地址, 再读出来* 如果得到0, 表示0地址上的内容被修改了, 它对应ram, 这就是nand启动* 否则就是nor启动*/mov r1, #0ldr r0, [r1] /* 读出原来的值备份 */str r1, [r1] /* 0->[0] */ ldr r2, [r1] /* r2=[0] */cmp r1, r2   /* r1==r2? 如果相等表示是NAND启动 */ldr sp, =0x40000000+4096 /* 先假设是nor启动 */moveq sp, #4096  /* nand启动 */streq r0, [r1]   /* 恢复原来的值 */bl sdram_init//bl sdram_init2	 /* 用到有初始值的数组, 不是位置无关码 *//* 重定位text, rodata, data段整个程序 */bl copy2sdram/* 清除BSS段 */bl clean_bssldr pc, =sdram
sdram:bl uart0_initbl print1/* 故意加入一条未定义指令 */
und_code:.word 0xdeadc0de  /* 未定义指令 */bl print2//bl main  /* 使用BL命令相对跳转, 程序仍然在NOR/sram执行 */ldr pc, =main  /* 绝对跳转, 跳到SDRAM */halt:b halt

五、SWI异常模式程序示例

所谓SWI,即software interrupt,软件中断。简单地理解,就是应用程序中通过使用swi指令来触发中断。

之前提到,当复位或者执行软件中断指令时,将会进入超级用户模式(SVC)。那软件中断异常模式,应该就是SVC模式?

5.1 软件中断的意义

为什么需要软件中断异常模式呢?或者说,软件中断的作用是什么?

我们知道,ARM的CPU有7种工作模式,除了用户模式以外,其他6种都是特权模式,这些特权模式可以直接修改CPSR进入其他模式,而用户模式不能修改CPSR进入其他模式。

Linux应用程序一般运行于用户模式,这是一种受限的模式,比如不可访问硬件。应用程序如果想访问硬件,必须先切换模式。怎么切换模式呢?需要通过异常,比如普通中断IRQ、未定义指令异常Undef、软件中断异常(通过执行“swi xxx”指令来触发SWI异常)。也就是说,软件中断异常是切换CPU工作模式的一致方式。

另外注意一下软件中断和软中断,这两个概念的区别:https://zhuanlan.zhihu.com/p/360683396。

另外有个疑惑,ARM的CPU的工作模式,与Linux内核的用户态、内核态有什么关联?

更多介绍,应该通过搜索引擎来学习,不要局限于课程的知识!

(1)https://zhuanlan.zhihu.com/p/550570075

(2)SWI指令---软件中断实例详解(原创)-CSDN博客

5.2 SWI异常模式程序示例

5.2.1 初版代码 

代码如下,可见和未定义指令异常模式的处理流程类似。

.text
.global _start_start:b reset          /* vector 0 : reset */ldr pc, und_addr /* vector 4 : und */ldr pc, swi_addr /* vector 8 : swi */und_addr:.word do_undswi_addr:.word do_swido_und:/* 执行到这里之前:* 1. lr_und保存有被中断模式中的下一条即将执行的指令的地址* 2. SPSR_und保存有被中断模式的CPSR* 3. CPSR中的M4-M0被设置为11011, 进入到und模式* 4. 跳到0x4的地方执行程序 *//* sp_und未设置, 先设置它 */ldr sp, =0x34000000/* 在und异常处理函数中有可能会修改r0-r12, 所以先保存 *//* lr是异常处理完后的返回地址, 也要保存 */stmdb sp!, {r0-r12, lr}  /* 保存现场 *//* 处理und异常 */mrs r0, cpsrldr r1, =und_stringbl printException/* 恢复现场 */ldmia sp!, {r0-r12, pc}^  /* ^会把spsr的值恢复到cpsr里 */und_string:.string "undefined instruction exception"
.align 4do_swi:/* 执行到这里之前:* 1. lr_svc保存有被中断模式中的下一条即将执行的指令的地址* 2. SPSR_svc保存有被中断模式的CPSR* 3. CPSR中的M4-M0被设置为10011, 进入到svc模式* 4. 跳到0x08的地方执行程序 *//* sp_svc未设置, 先设置它 */ldr sp, =0x33e00000/* 在swi异常处理函数中有可能会修改r0-r12, 所以先保存 *//* lr是异常处理完后的返回地址, 也要保存 */stmdb sp!, {r0-r12, lr}  /* 保存现场 *//* 处理swi异常 */mrs r0, cpsrldr r1, =swi_stringbl printException/* 恢复现场 */ldmia sp!, {r0-r12, pc}^  /* ^会把spsr的值恢复到cpsr里 */swi_string:.string "swi exception"
.align 4reset:/* 关闭看门狗 *//* 设置FCLK : HCLK : PCLK = 400m : 100m : 50m *//* 设置sp栈 */bl sdram_initbl copy2sdrambl clean_bss/* 复位之后cpu处于svc模式,现在切换到usr模式*/mrs r0, cpsr      /* 读出cpsr */bic r0, r0, #0xf  /* 修改M4-M0为0b10000, 进入usr模式 */msr cpsr, r0/* 设置 sp_usr */ //设置用户模式下的栈ldr sp, =0x33f00000ldr pc, =sdram
sdram:bl uart0_initbl print1
/* 故意加入一条未定义指令 */
und_code:.word 0xdeadc0debl print2/* 故意加入一条swi指令 */swi 0x123  /* 执行此命令, 触发SWI异常, 进入0x8执行 */ldr pc, =main  /* 绝对跳转, 跳到SDRAM */
halt:b halt

下面是对这部份代码的一些解释:

(1)代码流程:复位之后执行reset的内容,CPU处于SVC工作模式,为了体现SWI的特征(从用户模式切换到SWI),需要先把CPU从SVC切换到用户模式。然后执行到“swi 0x123”这条指令时,会触发SWI异常,从而CPU跳转到0x8地址处执行,进而执行do_swi函数。

(2) 同4.2的改进3一样,需要在字符串后面添加“.align 4”,表示接下来的内容要4字节对齐。

(3)用户模式、未定义指令异常模式、SWI异常模式下的栈指针可以随便设置,只要指向未使用的空间即可。这里用户模式的栈指针为0x33f00000,未定义指令异常模式的栈指针是0x34000000,SWI异常模式的栈指针是0x33e00000。

(4)“swi xxx”这个指令中的“xxx”有啥作用呢?课程中说,可以根据应用程序传入的xxx来判断为什么调用swi指令。

5.2.2 读取“swi xxx”中的“xxx”

如何在异常处理函数中,读取“swi xxx”中的“xxx”呢?

修改代码如下:

do_swi:/* 执行到这里之前:* 1. lr_svc保存有被中断模式中的下一条即将执行的指令的地址* 2. SPSR_svc保存有被中断模式的CPSR* 3. CPSR中的M4-M0被设置为10011, 进入到svc模式* 4. 跳到0x08的地方执行程序 *//* sp_svc未设置, 先设置它 */ldr sp, =0x33e00000/* 保存现场 *//* 在swi异常处理函数中有可能会修改r0-r12, 所以先保存 *//* lr是异常处理完后的返回地址, 也要保存 */stmdb sp!, {r0-r12, lr}  mov r4, lr   // 见注释(1)/* 处理swi异常 */mrs r0, cpsrldr r1, =swi_stringbl printExceptionsub r0, r4, #4 //见注释(1)bl printSWIVal/* 恢复现场 */ldmia sp!, {r0-r12, pc}^  /* ^会把spsr的值恢复到cpsr里 */swi_string:.string "swi exception".align 4

下面是对这段代码的一些说明: 

(1)代码中的“mov r4, lr”,之所以要把lr取出来,是因为一会要用它得到“swi 0x123”这条指令的地址。由下表可知,执行“swi 0x123”时,下一条指令即“ldr pc,=main”的地址为PC+4,而这个地址会保存在lr寄存器中,也就是说lr寄存器中保存的值是PC+4。那为了得到“swi 0x123”这条指令的地址,需要将lr的值减去4。这就是代码中的“sub r0, r4, #4”的含义。

(2)那为什么是赋值给r4呢?赋值给其他寄存器比如r0、r1这样不行吗?原来,赋值给r0~r3时,进入printException函数时,r0~r3有可能会被修改。而r4~r11在进入printException函数时会自动保存下来以备退出时自动恢复。所以可以赋值给r4。这些知识内容在书P56的“ATPCS规则”中。

(3)printSWIVal函数内容如下,它提取“swi xxx”指令的低24bit的内容,即xxx的值。

void printSWIVal(unsigned int *pSWI)
{puts("SWI val = ");printHex(*pSWI & ~0xff000000);puts("\n\r");
}

六、按键中断程序示例-1

中断也是一种异常,那么它的处理流程,应该和前面两种异常(未定义指令异常、SWI异常)的处理流程是类似的;但中断是由硬件产生的,中断源有很多种,因此它的处理流程又与前面两种异常稍微不同。 

6.1 中断的处理流程 

其实就是1.2.2小节的内容。

(1)首先进行初始化:设置中断源、设置中断控制器、设置CPU总开关。

  • 设置中断源。通过设置中断源,让中断源可以产生中断信号。比如让某个按键可以产生中断,则需要将它对应的GPIO引脚设置为中断引脚。
  • 设置中断控制器。通过设置中断控制器,可以选择屏蔽某个中断(好比母亲可以选择忽略远处的猫叫)、可以设置中断的优先级(同时有几个中断发生时先处理哪一个)、可以让中断控制器发送中断信号给CPU。
  • 设置CPU的中断总开关。通过设置CPSR寄存器的I位,即bit[7],可以设置是否开启中断。

(2)处理时:要分清中断源,对于不同的中断源执行不同的处理函数。

(3)处理完后:相比其他异常,中断这种异常最后还需要清中断。

6.2 代码示例

下面开始写代码。代码流程图如下所示:

6.2.1 start.S文件内容

(1)首先设置中断总开关,让CPSR[7] =0来开启中断,如果CPSR[7] =1则CPU无法响应任何中断。

//省略的内容与第五节的start.s文件一样,这里仅列出修改之处/* 复位之后, cpu处于svc模式* 现在, 切换到usr模式*/mrs r0, cpsr         /* 读出cpsr */bic r0, r0, #0xf     /* 修改M4-M0为0b10000, 进入usr模式 */bic r0, r0, #(1<<7)  //清除I位, 使能中断msr cpsr, r0//省略的内容与第五节的start.s文件一样,这里仅列出修改之处

(2)然后调用按键初始化函数(设置它为中断源)、中断控制器初始化函数。这里先在start.S文件中调用这两个函数,后面改进时会改为在main.c文件中调用这两个函数。

	bl print1/* 故意加入一条未定义指令 */
und_code:.word 0xdeadc0de  /* 未定义指令 */bl print2swi 0x123  /* 执行此命令, 触发SWI异常, 进入0x8执行 */bl interrupt_init //中断控制器初始化函数bl key_eint_init //按键初始化函数(设置它为中断源)//bl main  /* 使用BL命令相对跳转, 程序仍然在NOR/sram执行 */ldr pc, =main  /* 绝对跳转, 跳到SDRAM */halt:b halt

6.2.2 interrupt.c文件内容

主要是实现key_eint_init、interrupt_init函数。

1、初始化按键:key_eint_init函数
/* 初始化按键, 设为中断源 */
void key_eint_init(void)
{/* 配置GPIO为中断引脚 */GPFCON &= ~((3<<0) | (3<<4));GPFCON |= ((2<<0) | (2<<4));   /* S2,S3被配置为中断引脚 */GPGCON &= ~((3<<6) | (3<<11));GPGCON |= ((2<<6) | (2<<11));   /* S4,S5被配置为中断引脚 *//* 设置中断触发方式: 双边沿触发 */EXTINT0 |= (7<<0) | (7<<8);     /* S2,S3 */EXTINT1 |= (7<<12);             /* S4 */EXTINT2 |= (7<<12);             /* S5 *//* 设置EINTMASK使能eint11,19 */EINTMASK &= ~((1<<11) | (1<<19));
}

下面是对这函数的一些说明。 

(1)将GPIO引脚配置为中断引脚 

下面是按键的原理图,我们想达到“按下按键灯亮、松开按键灯灭”这样的效果,需要把S2~S5这4个按键设置为外部中断源。

在S3C2440数据手册上查询“EINT0”得到下面内容,可知S2按键对应着GPF0这个引脚。

或者在原理图中直接搜索“EINT0”,得到下面的内容: 

那么我们需要看一下GPFCON寄存器的位含义:

(2)EXTINTn寄存器

该寄存器用了设置中断触发方式。

所谓中断触发方式,也就是什么情况下会触发中断:上升沿?下降沿?低电平时?高电平时?

对于EXTINT0、EXTINT1,我们可以通过EXTINT0寄存器来设置中断触发方式,这里设置为双边沿触发。

对于其他EXTINTn(n≥8),需要设置EXTINT1、EXTINT2寄存器。

(3)EINTMASK寄存器

代码最后涉及EINTMASK寄存器(External Interrupt Mask Register,外部中断屏蔽寄存器)。该寄存器的位含义如下:

由此可知:当该寄存器某位设置为1时,表示禁止对应的外部中断源发信号给中断控制器;当该寄存器某位设置为0时,则表示允许对应的外部中断源发信号给中断控制器;另外EINT0~3是保留的,默认是使能的,可以直接发信号给中断控制器,无需设置。

(4)EINTPEND寄存器

该寄存器的位含义如下:

当发生外部中断时,我们可以通过读取这个寄存器,来分辨是哪个EXINTn产生了(n=4~23)。

另外处理完异常后需要清除中断,往EINTPEND寄存器相应位写“1”即可。

2、初始化中断控制器 
/* 初始化中断控制器 */
void interrupt_init(void)
{INTMSK &= ~((1<<0) | (1<<2) | (1<<5));
}

在S3C2440数据手册P378有以下内容(可以看作是中断控制器):

 

图中的MODE是与FIQ、IRQ有关的,这里我们不管FIQ,所以不理会MODE;Priority是与优先级相关的,这里先不管。

由上图可知,不同中断源的处理路线不同,外部中断源走的是标红的那条路线。因此只需要关注INTPND、MASK、SRCPND寄存器。 

(1)SRCPND寄存器   

该寄存器用来显示产生了哪个中断,如下图所示。

处理完异常最后清除中断时,我们要把对应位设置为1。 

由上图可知,SRCPND[5] 对应着 EINT8~23。如果SRCPND[5]=1时,如何判断EINT8~23中的哪一个外部中断源发生了中断(课程中说:这就需要继续看INTPND寄存器了)?

(2)INTPND寄存器

有多个中断源发出中断时,SRCPND寄存器中就会有多个位被设置为1,这多个中断经过priority之后,就只有一个中断通知CPU。

哪一个中断优先级最高呢? 可以读INTPND寄存器。因为该寄存器用来显示当前优先级最高的、正在发生的中断。

处理完异常后清除中断时,需要清除它(写1)。

(3)INTMOD寄存器

内容如下,我们使用默认值即可。

(4)INTMSK寄存器

这个寄存器用来屏蔽中断源(即使你中断源发生了中断,如果我把你屏蔽了,你的中断信号也不会发给CPU), 有点类似于EINTMASK寄存器。不过INTMSK寄存器是属于中断控制器的,而EINTMASK寄存器是I/O端口控制器的。写0表示允许,写1表示禁止。

(5)INTOFFSET寄存器 

该寄存器用来显示INTPND寄存器中哪一位被设置为1。比如,如果INTOFFSET=n,那说明INTPND寄存器的bit[n]被设置为1。其实也可以逐位去判断 INTPND 寄存器中哪一位被设置为1。

处理完异常后清除中断时,不需要清除它,因为会被自动清除。  

(6)由(1)~(5)可知,我们在初始化中断控制器时,只需要设置INTMSK寄存器即可。其他寄存器在处理中断时才需要设置。  

6.2.3 main.c文件内容

#include "s3c2440_soc.h"
#include "uart.h"
#include "init.h"char g_Char = 'A';
char g_Char3 = 'a';
const char g_Char2 = 'B';
int g_A = 0;
int g_B;int main(void)
{interrupt_init();  /* 初始化中断控制器 */key_eint_init();   /* 初始化按键, 设为中断源 */puts("\n\rg_A = ");printHex(g_A);puts("\n\r");while (1){putchar(g_Char);g_Char++;putchar(g_Char3);g_Char3++;delay(1000000);}return 0;
}

七、按键中断程序-2

7.1 start.S文件内容 

在main.c文件中,我们已经进行中断控制器初始化、中断源初始化。假设一按下按键就会产生中断,那CPU跳到哪里执行呢?我们可以根据异常向量表,在start.S文件中添加相应代码。

.text
.global _start_start:b reset          /* vector 0 : reset */ldr pc, und_addr /* vector 4 : und */ldr pc, swi_addr /* vector 8 : swi *///即使没有发生也要占个坑位才行,其实也可以.word xxx 这样的形式?b halt			 /* vector 0x0c : prefetch aboot */b halt			 /* vector 0x10 : data abort */b halt			 /* vector 0x14 : reserved */ldr pc, irq_addr /* vector 0x18 : irq */b halt			 /* vector 0x1c : fiq */und_addr:.word do_undswi_addr:.word do_swiirq_addr:.word do_irqdo_und:/* 执行到这里之前:* 1. lr_und保存有被中断模式中的下一条即将执行的指令的地址* 2. SPSR_und保存有被中断模式的CPSR* 3. CPSR中的M4-M0被设置为11011, 进入到und模式* 4. 跳到0x4的地方执行程序 *//* sp_und未设置, 先设置它 */ldr sp, =0x34000000/* 在und异常处理函数中有可能会修改r0-r12, 所以先保存 *//* lr是异常处理完后的返回地址, 也要保存 */stmdb sp!, {r0-r12, lr}  /* 保存现场 *//* 处理und异常 */mrs r0, cpsrldr r1, =und_stringbl printException/* 恢复现场 */ldmia sp!, {r0-r12, pc}^  /* ^会把spsr的值恢复到cpsr里 */und_string:.string "undefined instruction exception".align 4do_swi:/* 执行到这里之前:* 1. lr_svc保存有被中断模式中的下一条即将执行的指令的地址* 2. SPSR_svc保存有被中断模式的CPSR* 3. CPSR中的M4-M0被设置为10011, 进入到svc模式* 4. 跳到0x08的地方执行程序 *//* sp_svc未设置, 先设置它 */ldr sp, =0x33e00000/* 保存现场 *//* 在swi异常处理函数中有可能会修改r0-r12, 所以先保存 *//* lr是异常处理完后的返回地址, 也要保存 */stmdb sp!, {r0-r12, lr}  mov r4, lr/* 处理swi异常 */mrs r0, cpsrldr r1, =swi_stringbl printExceptionsub r0, r4, #4bl printSWIVal/* 恢复现场 */ldmia sp!, {r0-r12, pc}^  /* ^会把spsr的值恢复到cpsr里 */swi_string:.string "swi exception".align 4do_irq:/* 执行到这里之前:* 1. lr_irq保存有被中断模式中的下一条即将执行的指令的地址* 2. SPSR_irq保存有被中断模式的CPSR* 3. CPSR中的M4-M0被设置为10010, 进入到irq模式* 4. 跳到0x18的地方执行程序 *//* sp_irq未设置, 先设置它 */ldr sp, =0x33d00000/* 保存现场 *//* 在irq异常处理函数中有可能会修改r0-r12, 所以先保存 *//* lr-4是异常处理完后的返回地址, 也要保存 */sub lr, lr, #4stmdb sp!, {r0-r12, lr}  /* 处理irq异常 */bl handle_irq_c/* 恢复现场 */ldmia sp!, {r0-r12, pc}^  /* ^会把spsr_irq的值恢复到cpsr里 */reset:/* 关闭看门狗 */ldr r0, =0x53000000ldr r1, =0str r1, [r0]/* 设置MPLL, FCLK : HCLK : PCLK = 400m : 100m : 50m *//* LOCKTIME(0x4C000000) = 0xFFFFFFFF */ldr r0, =0x4C000000ldr r1, =0xFFFFFFFFstr r1, [r0]/* CLKDIVN(0x4C000014) = 0X5, tFCLK:tHCLK:tPCLK = 1:4:8  */ldr r0, =0x4C000014ldr r1, =0x5str r1, [r0]/* 设置CPU工作于异步模式 */mrc p15,0,r0,c1,c0,0orr r0,r0,#0xc0000000   //R1_nF:OR:R1_iAmcr p15,0,r0,c1,c0,0/* 设置MPLLCON(0x4C000004) = (92<<12)|(1<<4)|(1<<0) *  m = MDIV+8 = 92+8=100*  p = PDIV+2 = 1+2 = 3*  s = SDIV = 1*  FCLK = 2*m*Fin/(p*2^s) = 2*100*12/(3*2^1)=400M*/ldr r0, =0x4C000004ldr r1, =(92<<12)|(1<<4)|(1<<0)str r1, [r0]/* 一旦设置PLL, 就会锁定lock time直到PLL输出稳定* 然后CPU工作于新的频率FCLK*//* 设置内存: sp 栈 *//* 分辨是nor/nand启动* 写0到0地址, 再读出来* 如果得到0, 表示0地址上的内容被修改了, 它对应ram, 这就是nand启动* 否则就是nor启动*/mov r1, #0ldr r0, [r1] /* 读出原来的值备份 */str r1, [r1] /* 0->[0] */ ldr r2, [r1] /* r2=[0] */cmp r1, r2   /* r1==r2? 如果相等表示是NAND启动 */ldr sp, =0x40000000+4096 /* 先假设是nor启动 */moveq sp, #4096  /* nand启动 */streq r0, [r1]   /* 恢复原来的值 */bl sdram_init//bl sdram_init2	 /* 用到有初始值的数组, 不是位置无关码 *//* 重定位text, rodata, data段整个程序 */bl copy2sdram/* 清除BSS段 */bl clean_bss/* 复位之后, cpu处于svc模式* 现在, 切换到usr模式*/mrs r0, cpsr         /* 读出cpsr */bic r0, r0, #0xf     /* 修改M4-M0为0b10000, 进入usr模式 */bic r0, r0, #(1<<7)  /* 清除I位, 使能中断 */msr cpsr, r0/* 设置 sp_usr */ldr sp, =0x33f00000ldr pc, =sdram
sdram:bl uart0_initbl print1/* 故意加入一条未定义指令 */
und_code:.word 0xdeadc0de  /* 未定义指令 */bl print2swi 0x123  /* 执行此命令, 触发SWI异常, 进入0x8执行 *///bl main  /* 使用BL命令相对跳转, 程序仍然在NOR/sram执行 */ldr pc, =main  /* 绝对跳转, 跳到SDRAM */halt:b halt

下面是对这一段代码的说明。

(1)由上面的代码可知,其实中断的处理流程,和其他异常模式差不多的。只是异常模式对于异常的处理,是调用打印函数来打印某些字符串,而中断是调用handle_irq_c函数来进行一些操作。

(2)为了构建异常向量表,用了“b halt” 语句,这是必须需要的。

(3)对于栈空间的设置,只要分配不冲突的、没有被使用过的内存即可。

(4)保存现场时,为什么不直接保存lr,而是保存lr-4呢?

    sub lr, lr, #4stmdb sp!, {r0-r12, lr} 

这是由硬件结构决定的,如下所示。既然硬件让这么做,遵循就好,没有那么多为什么。

 

7.2 handle_irq_c函数 

接下来我们在interrupt.c中写出handle_irq_c函数。

void handle_irq_c(void)
{/* 分辨中断源 */int bit = INTOFFSET;/* 调用对应的处理函数 */if (bit == 0 || bit == 2 || bit == 5)  /* eint0,2,eint8_23 */{key_eint_irq(bit); /* 处理中断, 清中断源EINTPEND */}/* 清中断 : 从源头开始清 */SRCPND = (1<<bit);INTPND = (1<<bit);	
}

下面是对这段代码的一些说明:

(1)中断处理函数的3个步骤:分辨中断源、调用对应的处理函数、清除中断。

(2)怎么分辨中断源呢?由INTOFFSET寄存器含义可知,它的值表示INTPND寄存器中哪一位被设置为1,也就是指明中断源是哪个。

(3)清除中断,必须从源头开始清,也就是6.2.2第2节的图从左到右。这里是往SRCPND、INTPND寄存器的对应位(即(2)中找到的对应位)写“1”。另外在key_eint_irq函数中也清中断源EINTPEND 。

(4)关于 key_eint_irq 函数的说明。

void key_eint_irq(int irq)
{unsigned int val = EINTPEND;unsigned int val1 = GPFDAT;unsigned int val2 = GPGDAT;if (irq == 0) /* eint0 : s2 控制 D12 */{if (val1 & (1<<0)) /* s2 --> gpf6 */{/* 松开,则让引脚输出高电平,熄灭 */GPFDAT |= (1<<6);//熄灭}else{/* 按下,则让引脚输出低电平,点亮 */GPFDAT &= ~(1<<6);}}else if (irq == 2) /* eint2 : s3 控制 D11 */{if (val1 & (1<<2)) /* s3 --> gpf5 */{/* 松开 */GPFDAT |= (1<<5);}else{/* 按下 */GPFDAT &= ~(1<<5);}}else if (irq == 5) /* eint8_23, eint11--s4 控制 D10, eint19---s5 控制所有LED */{if (val & (1<<11)) /* eint11 */{if (val2 & (1<<3)) /* s4 --> gpf4 */{/* 松开 */GPFDAT |= (1<<4);}else{/* 按下 */GPFDAT &= ~(1<<4);}}else if (val & (1<<19)) /* eint19 */{if (val2 & (1<<11)){/* 松开则让引脚输出高电平,熄灭 *//* 熄灭所有LED */GPFDAT |= ((1<<4) | (1<<5) | (1<<6));}else{/* 按下则让引脚输出低电平: 点亮所有LED */GPFDAT &= ~((1<<4) | (1<<5) | (1<<6));}}}EINTPEND = val;
}

其代码逻辑如下所示,每个if里面的代码其实就是普通的点灯代码而已。 

另外注意一下里面的 “if (val & (1<<11)) /* eint11 */”和“else if (val & (1<<19)) /* eint19 */”,它是通过EINTPEND寄存器的对应位是否为1,来判断对应的中断源是否发生了中断的。

这是因为传给key_eint_irq函数的参数是INTOFFSET,这个参数等于5时,表明EXTINT8~23发生了中断,那到底是哪一个发生了中断呢?我们需要读取EINTPND寄存器来判断是哪个产生了中断。

7.3 其他修改

在main.c中添加led的初始化函数。

七、定时器中断程序示例

7.1 定时器内部控制逻辑 

参考资料是S3C2440数据手册的第10章“PWM Timer”。

(1)这里面肯定有一个时钟clk,每来一个clk,TCNTn就减去1。

(2)当TCNTn ==TCMPn时,可以产生中断,也可以让对应的PWM引脚反转(比如由高电平转换为低电平)。

(3)TCNTn继续减1,当TCNTn==0时,可以产生中断,PWM引脚再次反转。

(4)TCMPn、TCNTn的初始值来自TCMPBn、TCNTBn。当TCNTn ==0时,可自动加载初始值。

顺便提一下,由于JZ2440没有引出pwm引脚,所以pwm功能无法使用,也就无法做pwm相关实验。所谓pwm,是指可调制脉冲。比如下图,T1高脉冲和T2低脉冲,它的时间T1、T2可调整,可以输出不同频率、不同占控比的波形,这在控制电机时特别有用。

7.2 怎么使用定时器

使用定时器的步骤:1)初始化时钟;2)设置初值;3)加载初始,启动Timer;4)设置为自动加载;5)处理中断。

我们这个程序只做一个实验:当TCNTn这个计数器计数到0时,就产生中断,在这个中断服务程序里我们点灯。

首先我们在main函数中添加timer_init函数,这是时钟初始化函数:

新建一个timer.c文件,在里面实现这个timer_init函数,函数逻辑如下:

在数据手册P313有下面框图(这里我们设置Timer0,所以关注Timer0相关的内容):

由上可知,如果我们想设置Time0,则需要进行以下设置: 

1)首先设置8-Bit Prescaler;

2)设置5:1 MUX(选择一个时钟分频);

3)设置TCMPB0、TCNTB0;

4)设置TCONn寄存器。

下面介绍设置过程中使用到的寄存器。

(1)TCFG0寄存器(设置预分频器分频系数)

从上图可知,输送给Timer的时钟频率 = PCLK / {prescaler value+1} / {divider value}  。

  • 输送给Timer的时钟频率,是指从“5:1 MUX出来的时钟频率。
  • prescaler value,是指预分频器的分频系数(8bit,所以0~255中的一个整数);
  • divider value,是指分频器的分频系数(2、4、8、16分频)。

编程时,我们取值如下:

Timer clk = PCLK / {prescaler value+1} / {divider value} = 50000000 / (99+1) / 16= 31250 //从这个值减到0,才过去1秒钟。

所以代码中直接有:TCFG0 = 99;  /* Prescaler 0 = 99, 用于timer0,1 */

(2)TCFG1寄存器(选择“5:1 MUX”的输出路线)

多路选择器有5路输入1路输出,我们通过设置TCFG1寄存器来选择其中的一路。 

我们要把 [3:0] 设置为 0b0011=3,也就是16分频。

TCFG1 &= ~0xf; //清零
TCFG1 |= 3;  /* MUX0 : 1/16 *///直接写成 TCFG1=3 好像也行吧?

(3)TCNTB0寄存器、TCMPB0寄存器

TCNTB0寄存器保存定时器0的初始计数值,TCMPB0寄存器保存定时器0的比较值。

在启动定时器时,它们的值被传到TCNT0、TCMP0寄存器。

 

代码中设置如下:由于 Timer_clk 表示1秒31250次,那这里设置为(31250/2),也就是0.5秒。

	/* 设置TIMER0的初值 */TCNTB0 = 15625;  /* 0.5s中断一次 */ // 即 Timer_clk/2

由于没有用到 TCMPB0 寄存器,这里没有设置它。

(4)TCNTO0寄存器

通过读取这个寄存器,我们可以得知TCNT0寄存器的实时的值(因为TCNT0寄存器不断减1,其值是变化的)。不需要设置它。

(5)TCON寄存器

代码如下,但TCON &= ~(1<<1); 为何又要清0?好像是硬件规定的。

/* 加载初值 */
TCON |= (1<<1);   /* Update from TCNTB0 & TCMPB0 *//* 设置为自动加载,并启动Time0 */
TCON &= ~(1<<1);//这里为何又要清0?
TCON |= (1<<0) | (1<<3);  /* bit0: start, bit3: auto reload */

7.3 设置中断

由下图可知,接下来需要设置中断,显然我们需要提供一个中断处理函数。

我们在Timer章节里没有看到与中断相关的寄存器。所以我们回顾一下中断控制器,看看有没有与定时器相关的中断,但没有看到更加细致的、与设置Timer0有关的寄存器。

我们看一下PWM Timer章节,P314有以下内容:

由图可知,当TCNTn=TCMPn时不会产生中断,只有当TCNTn等于0的时候才可以产生中断。我们之前以为这个定时器可以产生两种中断,因此需要设置某个寄存器来选择这两种中断之一;但现在我们知道只有一种中断,这样的话只需要设置中断控制器即可。

这里设置INTMSK寄存器bit[10]即可(由INTMSK寄存器bit[10]可知),如下所示:

/* 初始化中断控制器 */
void interrupt_init(void)
{INTMSK &= ~((1<<0) | (1<<2) | (1<<5));INTMSK &= ~(1<<10);  /* enable timer0 int */ //新添加这个
}

当定时器减到0的时候就会产生中断,就会进到start.S中执行do_irq函数,在do_irq函数中进入handle_irq_c函数中进行处理,然后在timer.c文件中的timer_irq函数中点灯。


 

void timer_irq(void)
{/* 点灯计数 */static int cnt = 0;int tmp;cnt++;tmp = ~cnt;tmp &= 7;GPFDAT &= ~(7<<4);GPFDAT |= (tmp<<4);
}

我们在Makefile中添加timer.o,进行编译烧写运行,发现灯没有闪烁。

为了调试,我们在main函数中设置打印TCNTO0寄存器的值,看是否有变化。

putchar(g_Char3);
g_Char3++;
delay(1000000);
printHex(TCNTO0);

运行结果:打印TCNTO0寄存器的值全是0。这说明我们的定时器根本就没有启用。

分析原因:在timer.c文件中的 timer_init 函数里,设置为自动加载并启动:需要先清掉手动更新位(之前没有取反所以出错),再或上bit0 与 bit3。

	/* 设置为自动加载并启动 */TCON &= ~(1<<1);//之前没有取反,所以实验失败TCON |= (1<<0) | (1<<3);  /* bit0: start, bit3: auto reload */

再次实验,灯已闪烁。

7.4 改进程序 

由于每添加一个中断,都要修改interrupt.c文件中的handle_irq_c函数,这样太麻烦了。能不能不修改interrupt.c文件?

可以的,这里需要用到“函数指针数组”,在interrupt.c文件中定义一个函数指针数组。

typedef void(*irq_func)(int);
irq_func irq_array[32];//这里数组长度之所以是32,是因为SRCPND寄存器也是32bit的

我们想把每一个中断的处理函数,都放到这个函数指针数组中。当发生中断时,我们通过INTOFFSET寄存器得到这个中断号,我们从这个函数指针数组中调用对应的函数即可。

那么我们得提供一个注册函数,把中断号与对应的函数绑定,同时使能中断(这样以后都不需要修改interrupt_init函数了):

void register_irq(int irq, irq_func fp)
{irq_array[irq] = fp; //把中断号与对应的函数绑定INTMSK &= ~(1<<irq); //同时使能中断
}

然后怎么用呢?很简单:

void handle_irq_c(void)
{/* 分辨中断源 */int bit = INTOFFSET;#if 0/* 调用对应的处理函数 */if (bit == 0 || bit == 2 || bit == 5)  /* eint0,2,eint8_23 */{key_eint_irq(bit); /* 处理中断, 清中断源EINTPEND */}else if (bit == 10){timer_irq();}
#endif/* 调用对应的处理函数 */irq_array[bit](bit);/* 清中断 : 从源头开始清 */SRCPND = (1<<bit);INTPND = (1<<bit);	
}

在key_eint_init函数中注册中断:

/* 初始化按键, 设为中断源 */
void key_eint_init(void)
{/* 配置GPIO为中断引脚 */GPFCON &= ~((3<<0) | (3<<4));GPFCON |= ((2<<0) | (2<<4));   /* S2,S3被配置为中断引脚 */GPGCON &= ~((3<<6) | (3<<22));GPGCON |= ((2<<6) | (2<<22));   /* S4,S5被配置为中断引脚 *//* 设置中断触发方式: 双边沿触发 */EXTINT0 |= (7<<0) | (7<<8);     /* S2,S3 */EXTINT1 |= (7<<12);             /* S4 */EXTINT2 |= (7<<12);             /* S5 *//* 设置EINTMASK使能eint11,19 */EINTMASK &= ~((1<<11) | (1<<19));register_irq(0, key_eint_irq);register_irq(2, key_eint_irq);register_irq(5, key_eint_irq);
}

在timer_init 函数中注册中断:

void timer_init(void)
{/* 设置TIMER0的时钟 *//* Timer clk = PCLK / {prescaler value+1} / {divider value} = 50000000/(99+1)/16= 31250*/TCFG0 = 99;  /* Prescaler 0 = 99, 用于timer0,1 */TCFG1 &= ~0xf;TCFG1 |= 3;  /* MUX0 : 1/16 *//* 设置TIMER0的初值 */TCNTB0 = 15625;  /* 0.5s中断一次 *//* 加载初值, 启动timer0 */TCON |= (1<<1);   /* Update from TCNTB0 & TCMPB0 *//* 设置为自动加载并启动 */TCON &= ~(1<<1);TCON |= (1<<0) | (1<<3);  /* bit0: start, bit3: auto reload *//* 设置中断 */register_irq(10, timer_irq);
}

如此一来,main函数中就可以注释掉interrupt_init函数了(该函数根本就不会用到,因为它的功能已经发散给key_eint_init、timer_init函数了):

int main(void)
{led_init();//interrupt_init();  /* 初始化中断控制器 */key_eint_init();   /* 初始化按键, 设为中断源 */timer_init();

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/881366.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

《Linux从小白到高手》理论篇:Linux的系统环境管理

List item 值此国庆佳节&#xff0c;深宅家中&#xff0c;闲来无事&#xff0c;就多写几篇博文。本篇详细深入介绍Linux的系统环境管理。 环境变量 linux系统下&#xff0c;如果你下载并安装了应用程序&#xff0c;很有可能在键入它的名称时出现“command not found”的提示…

【PPT工具】三维绘图神器ThreeD Tools插件安装及使用

【PPT工具】三维绘图神器ThreeD Tools插件安装及使用 1 ThreeD Tools插件安装及加载1.1 ThreeD Tools插件安装1.2 ThreeD Tools插件加载 2 ThreeD Tools插件使用绘制渐变箭头 参考 ThreeD Tools是一款Microsoft PowerPoint的第三方插件&#xff0c;是“只为设计”为“般若黑洞”…

众智OA办公系统 Account/Login SQL注入漏洞复现

0x01 产品简介 众智OA办公系统是一种专门为企业和机构的日常办公工作提供服务的综合性软件平台。它凭借先进的技术和人性化的设计理念,实现了信息的快速传递和自动化处理,帮助企业和机构实现信息化、自动化、智能化和标准化的办公管理。 0x02 漏洞概述 众智OA办公系统 Acc…

【C语言】使用结构体实现位段

文章目录 一、什么是位段二、位段的内存分配1.位段内存分配规则练习1练习2 三、位段的跨平台问题四、位段的应用五、位段使用的注意事项 一、什么是位段 在上一节中我们讲解了结构体&#xff0c;而位段的声明和结构是类似的&#xff0c;它们有两个不同之处&#xff0c;如下&…

【重学 MySQL】四十四、相关子查询

【重学 MySQL】四十四、相关子查询 相关子查询执行流程示例使用相关子查询进行过滤使用相关子查询进行存在性检查使用相关子查询进行计算 在 select&#xff0c;from&#xff0c;where&#xff0c;having&#xff0c;order by 中使用相关子查询举例SELECT 子句中使用相关子查询…

【C++】认识匿名对象

文章目录 目录 文章目录前言一、对匿名对象的解读二、匿名对象的对象类型三、匿名对象的使用总结 前言 在C中&#xff0c;匿名对象是指在没有呗命名的情况下创建的临时对象。它们通常在单个语句中执行一系列操作或调用某个函数&#xff0c;并且不需要将结果存放进变量中。 匿名…

每日OJ题_牛客_AB13【模板】拓扑排序_C++_Java

目录 牛客_AB13【模板】拓扑排序 题目解析 C代码 Java代码 牛客_AB13【模板】拓扑排序 【模板】拓扑排序_牛客题霸_牛客网 (nowcoder.com) 描述&#xff1a; 给定一个包含nn个点mm条边的有向无环图&#xff0c;求出该图的拓扑序。若图的拓扑序不唯一&#xff0c;输出任意合法…

Matlab|基于遗传粒子群算法的无人机路径规划【遗传算法|基本粒子群|遗传粒子群三种方法对比】

目录 主要内容 模型研究 部分代码 结果一览 下载链接 主要内容 为了更高效地完成复杂未知环境下的无人机快速探索任务&#xff0c;很多智能算法被应用于无人机路径规划方面的研究&#xff0c;但是传统粒子群算法存在粒子更新思路单一、随机性受限、收敛速度慢…

FireRedTTS - 小红书最新开源AI语音克隆合成系统 免训练一键音频克隆 本地一键整合包下载

小红书技术团队FireRed最近推出了一款名为FireRedTTS的先进语音合成系统&#xff0c;该系统能够基于少量参考音频快速模仿任意音色和说话风格&#xff0c;实现独特的音频内容创造。 FireRedTTS 只需要给定文本和几秒钟参考音频&#xff0c;无需训练&#xff0c;就可模仿任意音色…

【leetcode】 45.跳跃游戏 ||

如果我们「贪心」地进行正向查找&#xff0c;每次找到可到达的最远位置&#xff0c;就可以在线性时间内得到最少的跳跃次数。 例如&#xff0c;对于数组 [2,3,1,2,4,2,3]&#xff0c;初始位置是下标 0&#xff0c;从下标 0 出发&#xff0c;最远可到达下标 2。下标 0 可到达的…

如何在IDEA使用git上传代码的时候过滤掉非.java文件

1.情况分析 我们的java上传代码的时候&#xff0c;经常会出现这个xml,等等的无关文件&#xff0c;但是这个时候我们使用这个里面的git上传的时候无法过滤掉&#xff0c;我们在自己的这个代码仓库查看的时候经常显示无关文件&#xff0c;这个时候我们就可以通过相关配置进行文件…

MySQL连接查询:联合查询

先看我的表结构 emp表 联合查询的关键字&#xff08;union all, union&#xff09; 联合查询 基本语法 select 字段列表 表A union all select 字段列表 表B 例子&#xff1a;将薪资低于5000的员工&#xff0c; 和 年龄大于50 岁的员工全部查询出来 第一种 select * fr…

Java版本的SSE服务端实现样例

简单记录一下使用netty方式实现SSE的服务端功能 目录 简要说明基于Netty功能需求后端代码1. 创建一个SpringBoot 应用2. 创建服务端功能3. 创建前端功能4. 测试SSE 封装为组件 简要说明 Server-Sent Events (SSE) 是一种用于在客户端和服务器之间建立单向通信的技术。 它允许服…

通信工程学习:什么是RFID射频识别

RFID&#xff1a;射频识别 RFID射频识别&#xff08;Radio Frequency Identification&#xff09;&#xff0c;又称为无线射频识别&#xff0c;是一种非接触式的自动识别技术。它通过无线电信号来识别特定目标并读写相关数据&#xff0c;而无需在识别系统与特定目标之间建立机械…

任务【浦语提示词工程实践】

0.1 环境配置 首先点击左上角图标&#xff0c;打开Terminal&#xff0c;运行如下脚本创建虚拟环境&#xff1a; # 创建虚拟环境 conda create -n langgpt python3.10 -y 运行下面的命令&#xff0c;激活虚拟环境&#xff1a; conda activate langgpt 之后的操作都要在这个环境…

【EXCEL数据处理】000013 案例 EXCEL筛选与高级筛选。

前言&#xff1a;哈喽&#xff0c;大家好&#xff0c;今天给大家分享一篇文章&#xff01;创作不易&#xff0c;如果能帮助到大家或者给大家一些灵感和启发&#xff0c;欢迎收藏关注哦 &#x1f495; 目录 【EXCEL数据处理】000013 案例 EXCEL筛选与高级筛选。使用的软件&#…

【华为OD机试真题】95、最少面试官数

package mainimport ("fmt""sort" )type s struct {start intend intworkCount int }type duration struct {start intend int }// 查询时间段内是否有可用的面试官 func getFreeS(sList []*s, d *duration, workCountLimit int) (sIndex int)…

案例:问题处理与原因分析报告的模板

系统上线后暴露的问题也是一种财富&#xff0c;我们需要从中吸收经验教训&#xff0c;规避其他类似的问题。对于上线后的问题如何进行原因分析&#xff0c;我提供两个分析报告的模板&#xff0c;供大家参考。 模板案例1&#xff1a;共性现象的原因分析报告 模板案例二&#xf…

Java后端面试很水的,7天就能搞定!

随着Java的越来越卷&#xff0c;面试也直接上难度了&#xff0c;从以前的八股文到场景题了&#xff0c;尤其是有经验的去面试&#xff0c;场景题都是会问的&#xff0c;近期面试过的应该都深有体会&#xff01; 场景题230道&#xff1a; 1.分布式锁加锁失败后的等待逻辑是如何…

人脸表情行为识别系统源码分享

人脸表情行为识别系统源码分享 [一条龙教学YOLOV8标注好的数据集一键训练_70全套改进创新点发刊_Web前端展示] 1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 项目来源AACV Association for the Advancement of Computer Vis…