JZ2440开发板——异常与中断

news2024/10/10 4:53:36

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

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

1.1 中断概念的引入 

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

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

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

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

while(1)
{
  (1)read book
  (2)open door
         if 睡 继续读书
         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,#1
    mov r2,#2
    bl func
    add r3,r1,r2
    b stop

func:
    stmfd sp!,{r1,r2}//入栈指令
    mov r1,#10
    mov r2,#20
    add r3,r1,r2
    ldmfd 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.o
	arm-linux-ld -T sdram.lds start.o led.o uart.o init.o main.o -o sdram.elf
	arm-linux-objcopy -O binary -S sdram.elf sdram.bin
	arm-linux-objdump -D sdram.elf > sdram.dis
clean:
	rm *.bin *.o *.elf *.dis
	
%.o : %.c
	arm-linux-gcc -mthumb -c -o $@ $<

%.o : %.S
	arm-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 state
	adr 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   reset
        ldr pc,=undefined_instruction
        ……
        ldr pc,=irq
        ldr 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 reset
        b 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 //传参1
	ldr r1, =und_string //传参2
	bl 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 //传参2

und_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_und
	ldr pc, =do_und

do_und:
    //……

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

_start:
	b reset  /* vector 0 : reset */
    //b do_und
	//ldr pc, =do_und
    ldr pc,und_addr

und_addr:
    .word do_und

do_und:
    //……
2、改进2

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

	bl clean_bss //清除BSS段
//******添加下面两行内容*******
	ldr pc, =sdram  /* 绝对跳转, 跳到SDRAM */ //和 ldr pc, =main 含义一致
sdram:
//***************************
	bl uart0_init
	bl print1
und_code:
	.word 0xdeadc0de
	bl print2

	ldr 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, =0x53000000
	ldr r1, =0
	str r1, [r0]
4、总结 

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

改进后的代码如下:

_start:
	b reset  /* vector 0 : reset */
	ldr pc, und_addr /* vector 4 : und */

und_addr:
	.word 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异常 */
	mrs r0, cpsr
	ldr r1, =und_string
	bl printException
	
	/* 恢复现场 */
	ldmia sp!, {r0-r12, pc}^  /* ^会把spsr的值恢复到cpsr里 */
	
und_string:
	.string "undefined instruction exception"

.align 4

reset:
	/* 关闭看门狗 */
	ldr r0, =0x53000000
	ldr r1, =0
	str r1, [r0]

	/* 设置MPLL, FCLK : HCLK : PCLK = 400m : 100m : 50m */
	/* LOCKTIME(0x4C000000) = 0xFFFFFFFF */
	ldr r0, =0x4C000000
	ldr r1, =0xFFFFFFFF
	str r1, [r0]

	/* CLKDIVN(0x4C000014) = 0X5, tFCLK:tHCLK:tPCLK = 1:4:8  */
	ldr r0, =0x4C000014
	ldr r1, =0x5
	str r1, [r0]

	/* 设置CPU工作于异步模式 */
	mrc p15,0,r0,c1,c0,0
	orr r0,r0,#0xc0000000   //R1_nF:OR:R1_iA
	mcr 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, =0x4C000004
	ldr 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, #0
	ldr 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


	ldr pc, =sdram
sdram:
	bl uart0_init

	bl 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_und

swi_addr:
	.word do_swi

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异常 */
	mrs r0, cpsr
	ldr r1, =und_string
	bl printException
	
	/* 恢复现场 */
	ldmia sp!, {r0-r12, pc}^  /* ^会把spsr的值恢复到cpsr里 */
	
und_string:
	.string "undefined instruction exception"
.align 4

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}  
	
	/* 保存现场 */
	/* 处理swi异常 */
	mrs r0, cpsr
	ldr r1, =swi_string
	bl printException
	
	/* 恢复现场 */
	ldmia sp!, {r0-r12, pc}^  /* ^会把spsr的值恢复到cpsr里 */
	
swi_string:
	.string "swi exception"
.align 4

reset:
	/* 关闭看门狗 */
	/* 设置FCLK : HCLK : PCLK = 400m : 100m : 50m */
	/* 设置sp栈 */

	bl sdram_init
	bl copy2sdram
	bl clean_bss

	/* 复位之后cpu处于svc模式,现在切换到usr模式*/
	mrs r0, cpsr      /* 读出cpsr */
	bic r0, r0, #0xf  /* 修改M4-M0为0b10000, 进入usr模式 */
	msr cpsr, r0

	/* 设置 sp_usr */ //设置用户模式下的栈
	ldr sp, =0x33f00000

	ldr pc, =sdram
sdram:
	bl uart0_init

	bl print1
/* 故意加入一条未定义指令 */
und_code:
	.word 0xdeadc0de
	bl 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, cpsr
	ldr r1, =swi_string
	bl printException

	sub 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 print2

	swi 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_und

swi_addr:
	.word do_swi

irq_addr:
	.word do_irq

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异常 */
	mrs r0, cpsr
	ldr r1, =und_string
	bl printException
	
	/* 恢复现场 */
	ldmia sp!, {r0-r12, pc}^  /* ^会把spsr的值恢复到cpsr里 */
	
und_string:
	.string "undefined instruction exception"

.align 4

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
	
	/* 处理swi异常 */
	mrs r0, cpsr
	ldr r1, =swi_string
	bl printException

	sub r0, r4, #4
	bl printSWIVal
	
	/* 恢复现场 */
	ldmia sp!, {r0-r12, pc}^  /* ^会把spsr的值恢复到cpsr里 */
	
swi_string:
	.string "swi exception"

.align 4

do_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, #4
	stmdb sp!, {r0-r12, lr}  
	
	/* 处理irq异常 */
	bl handle_irq_c
	
	/* 恢复现场 */
	ldmia sp!, {r0-r12, pc}^  /* ^会把spsr_irq的值恢复到cpsr里 */


reset:
	/* 关闭看门狗 */
	ldr r0, =0x53000000
	ldr r1, =0
	str r1, [r0]

	/* 设置MPLL, FCLK : HCLK : PCLK = 400m : 100m : 50m */
	/* LOCKTIME(0x4C000000) = 0xFFFFFFFF */
	ldr r0, =0x4C000000
	ldr r1, =0xFFFFFFFF
	str r1, [r0]

	/* CLKDIVN(0x4C000014) = 0X5, tFCLK:tHCLK:tPCLK = 1:4:8  */
	ldr r0, =0x4C000014
	ldr r1, =0x5
	str r1, [r0]

	/* 设置CPU工作于异步模式 */
	mrc p15,0,r0,c1,c0,0
	orr r0,r0,#0xc0000000   //R1_nF:OR:R1_iA
	mcr 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, =0x4C000004
	ldr 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, #0
	ldr 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, =0x33f00000

	ldr pc, =sdram
sdram:
	bl uart0_init

	bl print1
	/* 故意加入一条未定义指令 */
und_code:
	.word 0xdeadc0de  /* 未定义指令 */
	bl print2

	swi 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, #4
    stmdb 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.coloradmin.cn/o/2201047.html

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

相关文章

前端的全栈混合之路Meteor篇:容器化开发环境下的meteor工程架构解析

本文主要介绍容器化meteor工程的目录架构解析&#xff0c;之前的文章中浅浅提到过一些&#xff1a;前端的全栈混合之路Meteor篇&#xff1a;开发环境的搭建 -全局安装或使用docker镜像-CSDN博客https://blog.csdn.net/m0_38015699/article/details/142730928?spm1001.2014.300…

【LeetCode HOT 100】详细题解之回溯篇

【LeetCode HOT 100】详细题解之回溯篇 回溯法的理论基础回溯法解决的问题理解回溯法回溯法模板 46 全排列思路代码 78 子集思路代码 17 电话号码的字母组合思路代码 39 组合总和思路代码 22 括号生成思路代码 79 单词搜索思路代码 131 分割回文串思路代码 51 N皇后思路代码 回…

教程:宏基因组数据分析教程

Orchestrating Microbiome Analysis Orchestrating Microbiome Analysis是一套包含宏基因组各种数据分析的教程&#xff0c;非常安利大家学习。 16S-analysis 16S-analysis是一本用于扩增子16s等微生物数据分析的教程&#xff0c;很适合新手入门学习。 Introduction to micro…

Android targetSdkVersion 升级为34 问题处理

原因是发布到GooglePlay遭到拒绝&#xff0c;需要最低API level为34。之前为31&#xff0c;感觉还挺高的&#xff0c;但是GooglePlay需要的更高。 记录下处理问题&#xff1a; 1.升级gradle版本为8.0.2 之前是&#xff1a; classpath com.android.tools.build:gradle:7.1.0-…

Git进行版本控制操作流程

目录 一、初始化仓库 操作流程 二、添加到缓存区 三、提交到版本库 四、推送至远程仓库 生成SSH密钥 将本地库中内容推送至已经创建好的远程库 推送 推送错误 第一种&#xff1a; 五、克隆 克隆整个项目 拉去最新代码 六、分支 1. 初始化仓库或克隆远端仓库 2…

新赚米渠道,天工AI之天工宝典!

新赚米渠道&#xff0c;天工AI之天工宝典&#xff01; 引言 随着人工智能和数字创作工具的发展&#xff0c;内容创作的门槛不断降低&#xff0c;为普通用户提供了更多的赚钱机会。在这样的背景下&#xff0c;天工AI应运而生&#xff0c;凭借其强大的创作能力和最新更新的“天…

「Ubuntu」文件权限说明(drwxr-xr-x)

我们在使用Ubuntu 查看文件信息时&#xff0c;常常使用 ll 命令查看&#xff0c;但是输出的详细信息有些复杂&#xff0c;特别是 类似与 drwxr-xr-x 的字符串&#xff0c;在此进行详细解释下 属主&#xff1a;所属用户 属组&#xff1a;文件所属组别 drwxr-xr-x 7 apps root 4…

MySql 之 Binglog 复制

复制是一种将数据从一个 MySQL 数据库服务器异步复制到另一个的技术。使用 MySQL 复制选项&#xff0c;您可以复制所有数据库、选定的数据库甚至选定的表&#xff0c;具体取决于您的使用情况。 前提条件 确保在源服务器上启用了二进制日志记录。确保复制配置中的所有服务器都有…

《云原生安全攻防》-- K8s攻击案例:从Pod容器逃逸到K8s权限提升

在本节课程中&#xff0c;我们将介绍一个完整K8s攻击链路的案例&#xff0c;其中包括了从web入侵到容器逃逸&#xff0c;再到K8s权限提升的过程。通过以攻击者的视角&#xff0c;可以更全面地了解K8s环境中常见的攻击技术。 在这个课程中&#xff0c;我们将学习以下内容&#…

使用 Go 和 Gin 框架构建简单的用户和物品管理 Web 服务

使用 Go 和 Gin 框架构建简单的用户和物品管理 Web 服务 在本项目中&#xff0c;我们使用 Go 语言和 Gin 框架构建了一个简单的 Web 服务&#xff0c;能够管理用户和物品的信息。该服务实现了两个主要接口&#xff1a;根据用户 ID 获取用户名称&#xff0c;以及根据物品 ID 获…

spring boot itext7的生成一个pdf(hello,world),并且相关一些简单的使用方法及相关说明

1、我们经常会碰到生成Pdf的场景&#xff0c;比如说有很多题目&#xff0c;又比如说来个质检的报告&#xff0c;我们都需要导出为pdf&#xff0c;那这种情况有二种方法&#xff0c;一种是通过报表来实现&#xff0c;一种就是通过itext来生成。一般我们会通过报表来直接导出pdf。…

Liquid AI与液态神经网络:超越Transformer的大模型架构探索

1. 引言 自2017年谷歌发表了开创性的论文《Attention Is All You Need》以来&#xff0c;基于Transformer架构的模型迅速成为深度学习领域的主流选择。然而&#xff0c;随着技术的发展&#xff0c;挑战Transformer主导地位的呼声也逐渐高涨。最近&#xff0c;由麻省理工学院(M…

简述何为多态

1.多态的概念 多态是什么?首先我们从概念讲起,简单来讲,多态就是多种形态,当你要去完成同一件事情的时候,不同的人去完成这件事情会有不同的结果. 比如在买票的时候,如果是成人去买票,则会买到成人票;如果是学生,则会买到学生票. 2.多态的实现以及构成条件 首先,多态的实现…

【Flutter、Web——前端个人总结】分享从业经历经验、自我规范准则,纯干货

前言 hi&#xff0c;正式接触web前端已经经过了两年的时间&#xff0c;从大学的java后端转型到web前端&#xff0c;再到后续转战Flutter&#xff0c;逐渐对前端有了一些心得体会&#xff0c;其实在当下前端的呈现形式一直在变化&#xff0c;无论你是用原生、还是web还是混编的…

Django 1.2标准日志模块出现奇怪行为时的解决方案

在 Django 1.2 中&#xff0c;标准日志模块有时会出现意想不到的行为&#xff0c;例如日志消息未按预期记录、日志级别未正确应用或日志格式错乱等。这些问题可能源于日志配置不当、日志模块被多次初始化、或日志模块被其他包覆盖等原因。下面是一些常见问题的排查方法和解决方…

力扣21~25题

21题&#xff08;简单&#xff09;&#xff1a; 分析&#xff1a; 按要求照做就好了&#xff0c;这种链表基本操作适合用c写&#xff0c;python用起来真的很奇怪 python代码&#xff1a; # Definition for singly-linked list. # class ListNode: # def __init__(self, v…

二、MySQL的数据目录

文章目录 1. MySQL8的主要目录结构1.1 数据库文件的存放路径1.2 相关命令目录1.3 配置文件目录 2. 数据库和文件系统的关系2.1 查看默认数据库2.2 数据库在文件系统中的表示2.3 表在文件系统中的表示2.3.1 InnoDB存储引擎模式2.3.2 MyISAM存储引擎模式 2.4 小结 1. MySQL8的主要…

宝塔docker中如何修改应用配置文件参数

今天在宝塔docker安装了kkfileview&#xff0c;相修改应用里的application.properties&#xff0c;却找不到在哪&#xff0c;如何修改&#xff1f; 下面教大家应用找文件修改。 docker安装好对应容器后&#xff0c;是这样 在这里是找不到对应修改的地方&#xff0c;其实docker…

Linux WIFI 驱动实验

直接参考【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.81 本文仅作为个人笔记使用&#xff0c;方便进一步记录自己的实践总结。 WIFI 的使用已经很常见了&#xff0c;手机、平板、汽车等等&#xff0c;虽然可以使用有线网络&#xff0c;但是有时候很多设备存在布线困难的情况&…

Windows10的MinGW安装和VS Code配置C/C++编译环境

1. MinGW下载安装 首先需要说明的是VS Code是一个编辑器&#xff0c;而不是编译器。‌ 编辑器和编译器是有很明显的区别 1.1 编辑器和编译器区别 编辑器‌是一种用于编写和编辑文本的应用软件&#xff0c;主要用于编写程序的源代码。编辑器提供基本的文本编辑功能&#xff0c;…