Linux驱动.之中断,外部中断,内部中断控制器,本篇讲
1、arm裸机,轮询的方式,点亮led
2、ARM裸机 通过按键,中断的方式,点了led灯,
3、单片机,中断的方式
4、CPU。裸机,的中断系统
一、在开始前,先回顾下51单片机中断,
单片机有5个中断,1个uart中断,2个定时器中断,2个外部中断,在发生中断时,单片机内部会自动跳转到hander处理函数,怎么实现的,不知道,是不是硬件自动跳转到中断处理函数的,还是软件查询后,去跳转,找到中断处理函数的。
如下,定时器,点了led灯
#include<reg51.h> // 包含51单片机寄存器定义的头文件
sbit D1=P2^0; //将D1位定义为P2.0引脚
sbit D2=P2^1; //将D2位定义为P2.1引脚
unsigned char Countor1; //设置全局变量,储存定时器T1中断次数
unsigned char Countor2; //设置全局变量,储存定时器T1中断次数
//也是要现配置time1,定时器。
void main(void)
{
EA=1; //开总中断
ET1=1; //定时器T1中断允许
TMOD=0x10; //使用定时器T1的模式1
TH1=(65536-46083)/256; //定时器T1的高8位赋初值
TL1=(65536-46083)%256; //定时器T1的高8位赋初值
TR1=1; //启动定时器T1
Countor1=0; //从0开始累计中断次数
Countor2=0; //从0开始累计中断次数
while(1);
}
// 定时器1,中断处理函数
void Time1(void) interrupt 3 using 0
{
Countor1++; //Countor1自加1
Countor2++; //Countor2自加1
if(Countor1==2) //若累计满2次,即计时满100ms
{
D1=~D1; //按位取反操作,将P2.0引脚输出电平取反
Countor1=0; //将Countor1清0,重新从0开始计数
}
if(Countor2==8) //若累计满8次,即计时满400ms
{
D2=~D2; //按位取反操作,将P2.1引脚输出电平取反
Countor2=0; //将Countor1清0,重新从0开始计数
}
TH1=(65536-46083)/256; //定时器T1的高8位重新赋初值
TL1=(65536-46083)%256; //定时器T1的高8位重新赋初值
}
51单片机比较简单,但是是一个雏形,stm32单片机,和arm裸机,几乎一样。
第一部分、章节目录
1.8.1.什么是按键
1.8.2.轮询方式处理按键
1.8.3.串口输出和按键消抖
1.8.4.S5PV210的中断体系介绍
1.8.5.异常向量表的编程处理
1.8.6.S5PV210的向量中断控制器
1.8.7.S5PV210中断处理的主要寄存器
1.8.8.S5PV210中断处理的编程实践1
1.8.9.S5PV210中断处理的编程实践2
1.8.10.外部中断
1.8.11.中断方式处理按键编程实践1
1.8.12.中断方式处理按键编程实践2
第二部分、章节介绍
1.8.1.什么是按键
本节介绍按键的原理、结构和工作原理,着重介绍了按键的电路接法和按下、弹起时对电路的影响;最后介绍了SoC处理按键时的2种思路。
1.8.2.轮询方式处理按键
本节以轮询方式处理按键,从零开始分析电路接法、原理图,然后查阅数据手册,手写代码,最终实现对开发板上6个按键的捕获,以LED点亮的方式来检验按键输出结果。
1.8.3.串口输出和按键消抖
本节包含2部分内容。首先基于我们第七部分移植的串口stdio输出项目来实现按键轮询方式的调试,让大家学会用串口输出的方式调试程序;其次讲了按键的硬件、软件消抖,并在我们的轮询方式检测按键中添加消抖处理。
1.8.4.S5PV210的中断体系介绍
本节首先引入中断的概念,然后讲了SoC中中断的实现原理(异常向量表)。随后讲了S5PV210的异常向量表分布,最后讲了下异常和中断这两个概念的基本区别。
1.8.5.异常向量表的编程处理
本节接上节讲述的理论,来编程处理S5PV210的异常向量表,其中最主要的是中断和快速中断这两个异常向量的处理,重点讲了S5PV210的中断处理第一阶段(汇编保存和恢复上下文阶段)
1.8.6.S5PV210的向量中断控制器
本节详细介绍S5PV210的向量中断控制器,而且首先讲了2440中中断处理的思路和方法,和210进行了比对。目的是让大家知道哪些特性是所有CPU共有的,哪些是CPU特有的,以将我们学到的知识运用在以后遇到的各种SoC中。
1.8.7.S5PV210中断处理的主要寄存器
本节详细介绍S5PV210的中断控制器中的主要寄存器,分析这些寄存器的作用有助于理解210的中断控制器的工作原理,进而分析其编程方法。
1.8.8.S5PV210中断处理的编程实践1
本节对S5PV210的中断控制器进行第二阶段的编程实战。因为代码量大所以提前写好了代码,主要是向大家讲解各部分代码的作用和工作原理。
1.8.9.S5PV210中断处理的编程实践2
本节对S5PV210的中断控制器进行第二阶段的编程实战。因为代码量大所以提前写好了代码,主要是向大家讲解各部分代码的作用和工作原理。
1.8.10.外部中断
本节向大家引入外部中断的概念。外部中断是210各种中断源的其中一类,按键就属于外部中断的范畴。本节同时介绍了外部中断相关的概念如电平触发和边沿触发。
1.8.11.中断方式处理按键编程实践1
本节完成中断方式处理按键的全部编程工作,系统梳理整个中断发生及处理的流程。最后总结对比了轮询方式处理按键和中断方式处理按键的差异。
1.8.12.中断方式处理按键编程实践2
本节完成中断方式处理按键的全部编程工作,系统梳理整个中断发生及处理的流程。最后总结对比了轮询方式处理按键和中断方式处理按键的差异。
第三部分、随堂记录
1.8.1.什么是按键
1.8.1.1、按键的物理特性
(1)、平时没人按的时候,弹簧把按键按钮弹开。此时内部断开的。
(2)、有人按下的时候,手的力量克服弹簧的弹力,将按钮按下,此时内部保持接通(闭合)状态;如果手拿开,则弹簧作用下按钮又弹开,同时内部又断开。
(3)、一般的按键都有4个引脚,这4个引脚成2对:其中一对是常开触点(像上面描述的不按则断开,按下则闭合);一对是常闭触点(平时不按时是闭合的,按下后是断开的)
1.8.1.2、按键的电学原理(结合原理图分析)
(1)硬件接法: SW5:GPH0_2 SW6:GPH0_3 SW78910:GPH2_0123
(2)按键的电路连接分析:平时按钮没有按下时,按钮内部断开,GPIO引脚处电压为高电平;当有人按下按钮时,按钮内部导通,外部VDD经过电阻和按钮连接到地,形成回路,此时GPIO引脚处电压就变成了低电平。此时VDD电压全部分压在了电阻上(这个电阻就叫分压电阻,这个电阻不能太小,因为电阻的功率是U*U/R)
(3)总结:按键的工作方法:其实就是按键的按下与弹开,分别对应GPIO的两种电平状态(按下则GPIO为低电平,弹开则GPIO为高电平)。此时SoC内部可以通过检测这个GPIO的电平高低来判断按键有没有被按下,这个判断结果即可作为SoC的输入信号。
1.8.1.3、按键属于输入类设备
(1)按键一般用来做输入设备(由人向SoC发送信息的设备,叫输入设备),由人向SoC发送按键信号(按键信号有2种:按下信号和弹开信号)。
(2)有些设备就是单纯的输入设备,譬如按键、触摸屏等;有些设备就是单纯的输出设备,譬如LCD;还有一些设备是既能输入又能输出的,叫输入输出设备(IO),譬如串口。
1.8.1.4、按键的2种响应方法
(1)SoC处理按键有2种思路:轮询方式和中断方式。
(2)轮询方式,就是SoC主动的每隔一段时间去读取(按键所对应的)GPIO的电平高低,以此获得按键信息;缺点在于CPU要一直注意按键事件,会影响CPU做其他事情。
(3)中断方式,就是SoC事先设定好GPIO触发的中断所对应的中断处理程序ISR,当外部按键按下或弹开时会自动触发GPIO对应的外部中断,导致ISR执行,从而自动处理按键信息。
1.8.2.轮询方式处理按键
1.8.2.1、X210开发板的按键接法
(1)查原理图,找到按键对应的GPIO:SW5:GPH0_2 SW6:GPH0_3 SW78910:GPH2_0123
(2)原理图上可以看出:按下时是低电平,弹起时是高电平
1.8.2.2、按键对应的GPIO模式设置
(1)按键接到GPIO上,按键按下还是弹起,决定外部电路的接通与否,从而决定这个GPIO引脚的电压是高还是低;这个电压可以作为这个GPIO引脚的输入信号,此时GPIO配置为输入模式,即可从SoC内部读取该引脚的电平为1还是0(1对应高电平,0对应低电平)。
(2)GPH0CON(0xE0200C00) GPH2DAT(0xE0200C04) GPH2CON(0xE0200C40) GPH2DAT(0xE0200C44)
(3)应该在CON寄存器中将GPIO设置为input模式,然后去读取DAT寄存器(读取到的相应位的值为1表示外部是高电平(对应按键弹起),读取到的位的值为0表明外部是低电平(按键按下))
1.8.2.3、轮询方式处理按键的程序流程
(1)第一步,先初始化GPIO模式为input;
(2)第二步,循环读取GPIO的电平值,然后判断有无按键按下
(2)第三步,初始led的gpio为input模式,输出高低电平。
1.8.2.4、代码编写和调试
总共有这么多文件,led写的文件,供key使用,start文件时汇编,启动文件,调用key和led,Makefile把这些文件编译链接成key.bin可执行文件,然后用mkv210image,编写的程序,去执行将key.bin 添加头部数据,转换成key.image,才能在sdk上启动运行,writetosd,是一个shell脚本文件,使用dd命令,刷写image到SD卡里。
led.c
#define GPJ0CON 0xE0200240
#define GPJ0DAT 0xE0200244
#define rGPJ0CON *((volatile unsigned int *)GPJ0CON)
#define rGPJ0DAT *((volatile unsigned int *)GPJ0DAT)
void delay(void);
// 该函数要实现led闪烁效果
void led_blink(void)
{
// led初始化,也就是把GPJ0CON中设置为输出模式
//volatile unsigned int *p = (unsigned int *)GPJ0CON;
//volatile unsigned int *p1 = (unsigned int *)GPJ0DAT;
rGPJ0CON = 0x11111111;
while (1)
{
// led亮
rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5)); //让三个led点亮 ,0就是控制三极管,通,led就亮
// 延时
delay();
// led灭
rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
// 延时
delay();
}
}
// 亮led
void led1(void)
{
rGPJ0CON = 0x11111111;
rGPJ0DAT = ((0<<3) | (1<<4) | (1<<5));
}
// 亮led
void led2(void)
{
rGPJ0CON = 0x11111111;
rGPJ0DAT = ((0<<3) | (0<<4) | (1<<5));
}
// 亮led
void led3(void)
{
rGPJ0CON = 0x11111111;
rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
}
void led_off(void)//所有led灭
{
rGPJ0CON = 0x11111111;
rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
}
void delay(void)
{
volatile unsigned int i = 900000; // volatile 让编译器不要优化,这样才能真正的减
while (i--); // 才能消耗时间,实现delay
}
key.c
// 定义操作寄存器的宏
#define GPH0CON 0xE0200C00
#define GPH0DAT 0xE0200C04
#define GPH2CON 0xE0200C40
#define GPH2DAT 0xE0200C44
#define rGPH0CON (*(volatile unsigned int *)GPH0CON)
#define rGPH0DAT (*(volatile unsigned int *)GPH0DAT)
#define rGPH2CON (*(volatile unsigned int *)GPH2CON)
#define rGPH2DAT (*(volatile unsigned int *)GPH2DAT)
// 初始化按键
void key_init(void)
{
// 设置GPHxCON寄存器,设置为输入模式
// GPH0CON的bit8~15全部设置为0,即可
rGPH0CON &= ~(0xFF<<8);
// GPH2CON的bit0~15全部设置为0,即可
rGPH2CON &= ~(0xFFFF<<0);
}
void key_polling(void)
{
// 依次,挨个去读出每个GPIO的值,判断其值为1还是0.如果为1则按键按下,为0则弹起
// 轮询的意思就是反复循环判断有无按键,隔很短时间
while (1)
{
// 对应开发板上标着LEFT的那个按键
if (rGPH0DAT & (1<<2))
{
// 为1,说明没有按键
led_off();
}
else
{
// 为0,说明有按键
led1();
}
// 对应开发板上标着DOWN的那个按键
if (rGPH0DAT & (1<<3))
{
// 为1,说明没有按键
led_off();
}
else
{
// 为0,说明有按键
led2();
}
// 对应开发板上标着UP的那个按键
if (rGPH2DAT & (1<<0))
{
// 为1,说明没有按键
led_off();
}
else
{
// 为0,说明有按键
led3();
}
}
}
start.S
#define WTCON 0xE2700000
#define SVC_STACK 0xd0037d80
.global _start // 把_start链接属性改为外部,这样其他文件就可以看见_start了
_start:
// 第1步:关看门狗(向WTCON的bit5写入0即可)
ldr r0, =WTCON
ldr r1, =0x0
str r1, [r0]
// 第2步:设置SVC栈
ldr sp, =SVC_STACK
// 第3步:开/关icache
mrc p15,0,r0,c1,c0,0; // 读出cp15的c1到r0中
//bic r0, r0, #(1<<12) // bit12 置0 关icache
orr r0, r0, #(1<<12) // bit12 置1 开icache
mcr p15,0,r0,c1,c0,0;
// 从这里之后就可以开始调用C程序了
bl key_init
bl key_polling
// 汇编最后的这个死循环不能丢
b .
Makefile
key.bin: start.o led.o key.o
arm-linux-ld -Ttext 0x0 -o key.elf $^
arm-linux-objcopy -O binary key.elf key.bin
arm-linux-objdump -D key.elf > key.dis
gcc mkv210_image.c -o mkx210
./mkx210 key.bin 210.bin
%.o : %.S
arm-linux-gcc -o $@ $< -c -nostdlib
%.o : %.c
arm-linux-gcc -o $@ $< -c -nostdlib
clean:
rm *.o *.elf *.bin *.dis mkx210 -f
mkv210_image.c
主要作用就是由usb启动时使用的led.bin制作得到由sd卡启动的镜像210.bin
/*
* mkv210_image.c的主要作用就是由usb启动时使用的led.bin制作得到由sd卡启动的镜像210.bin
*
* 本文件来自于友善之臂的裸机教程,据友善之臂的文档中讲述,本文件是一个热心网友提供,在此表示感谢。
*/
/* 在BL0阶段,Irom内固化的代码读取nandflash或SD卡前16K的内容,
* 并比对前16字节中的校验和是否正确,正确则继续,错误则停止。
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define BUFSIZE (16*1024)
#define IMG_SIZE (16*1024)
#define SPL_HEADER_SIZE 16
//#define SPL_HEADER "S5PC110 HEADER "
#define SPL_HEADER "****************"
int main (int argc, char *argv[])
{
FILE *fp;
char *Buf, *a;
int BufLen;
int nbytes, fileLen;
unsigned int checksum, count;
int i;
// 1. 3个参数
if (argc != 3)
{
printf("Usage: %s <source file> <destination file>\n", argv[0]);
return -1;
}
// 2. 分配16K的buffer
BufLen = BUFSIZE;
Buf = (char *)malloc(BufLen);
if (!Buf)
{
printf("Alloc buffer failed!\n");
return -1;
}
memset(Buf, 0x00, BufLen);
// 3. 读源bin到buffer
// 3.1 打开源bin
fp = fopen(argv[1], "rb");
if( fp == NULL)
{
printf("source file open error\n");
free(Buf);
return -1;
}
// 3.2 获取源bin长度
fseek(fp, 0L, SEEK_END); // 定位到文件尾
fileLen = ftell(fp); // 得到文件长度
fseek(fp, 0L, SEEK_SET); // 再次定位到文件头
// 3.3 源bin长度不得超过16K-16byte
count = (fileLen < (IMG_SIZE - SPL_HEADER_SIZE))
? fileLen : (IMG_SIZE - SPL_HEADER_SIZE);
// 3.4 buffer[0~15]存放"S5PC110 HEADER "
memcpy(&Buf[0], SPL_HEADER, SPL_HEADER_SIZE);
// 3.5 读源bin到buffer[16]
nbytes = fread(Buf + SPL_HEADER_SIZE, 1, count, fp);
if ( nbytes != count )
{
printf("source file read error\n");
free(Buf);
fclose(fp);
return -1;
}
fclose(fp);
// 4. 计算校验和
// 4.1 从第16byte开始统计buffer中共有几个1
// 4.1 从第16byte开始计算,把buffer中所有的字节数据加和起来得到的结果
a = Buf + SPL_HEADER_SIZE;
for(i = 0, checksum = 0; i < IMG_SIZE - SPL_HEADER_SIZE; i++)
checksum += (0x000000FF) & *a++;
// 4.2 将校验和保存在buffer[8~15]
a = Buf + 8; // Buf是210.bin的起始地址,+8表示向后位移2个字,也就是说写入到第3个字
*( (unsigned int *)a ) = checksum;
// 5. 拷贝buffer中的内容到目的bin
// 5.1 打开目的bin
fp = fopen(argv[2], "wb");
if (fp == NULL)
{
printf("destination file open error\n");
free(Buf);
return -1;
}
// 5.2 将16k的buffer拷贝到目的bin中
a = Buf;
nbytes = fwrite( a, 1, BufLen, fp);
if ( nbytes != BufLen )
{
printf("destination file write error\n");
free(Buf);
fclose(fp);
return -1;
}
free(Buf);
fclose(fp);
return 0;
}
shell 刷机脚本,写入到ubuntu上的插入的sd卡,拔出sd卡,插入板子,既可以启动了
#!/bin/sh
sudo dd iflag=dsync oflag=dsync if=210.bin of=/dev/sdb seek=1
##########################################################################################
二、S5PV210的中断体系介绍
1.8.4.1、什么是中断
(1)中断的发明是用来解决宏观上的并行需要的。宏观就是从整体上来看,并行就是多件事情都完成了。
(2)微观上的并行,就是指的真正的并行,就是精确到每一秒甚至每一刻,多个事情都是在同时进行的。宏观上面的并行并不等于微观的并行,有时候宏观上是并行的,微观上是串行的。
(3)例子中一个人在看电影,快递来了暂停电影跑去收快递,收完快递继续回来看电影,这个例子就是宏观上的并行和微观上的串行。例子中一个人等同于SoC中1个CPU(也就是单核CPU),这个CPU看电影就不能收快递,收快递就不能看电影(也就是说不能真正的并行)。单核心CPU在微观角度是串行的,但是因为CPU很快,所以在宏观看来可以并行。
(4)上例中大部分时间在看电影,中间少量时间去收快递,那么类比于CPU来说,看电影就应该是CPU的常规任务,而收快递则应该是中断例程。也就是说CPU平时一直在进行看电影任务,等快递来了(中断发生了)快递员(类似于中断源)会打电话叫人去收快递(中断源会触发中断通知CPU去处理中断),人收到电话(CPU收到中断信号)后会暂定电影(CPU保存常规任务的现场)跑去收快递(CPU去执行中断处理程序ISR处理中断),收完快递(执行完ISR)回来继续看电影(CPU恢复常规任务的现场,继续执行常规任务)
(5)为什么需要中断?因为单核CPU实际无法并行的,但是通过中断机制,可以实现假并行(宏观上的并行,微观上实际还是串行的)。还有是出现紧急情况时,可以优先处理紧急事件
1.8.4.2、SoC对中断的实现机制:异常向量表
(1)异常向量表是CPU中某些特定地址的特定定义。当中断发生的时候,中断要想办法通知CPU去处理中断,怎么做到?这就要靠异常向量表。
(2)在CPU设计时,就事先定义了CPU中一些特定地址作为特定异常的入口地址(譬如定义0x00000000地址为复位异常向量地址,则发生复位异常时CPU会自动跳转到0x00000000地址去执行指令。又譬如外部中断对应的异常向量地址为0x30000008,则发生外部中断后,CPU会硬件自动跳转到0x30000008地址去执行指令。)如
(3)以上讲的是CPU硬件设计时对异常向量表的支持,下来就需要软件支持了。硬件已经决定了发生什么异常CPU自动跳转PC到哪个地址去执行,软件需要做的就是把处理这个异常的代码的首地址填入这个异常向量地址。
中断向量表
中断向量表是一个表,这个表里面存放的是中断向量。中断服务程序的入口地址或存放中断服务程序的首地址成为中断向量,因此中断向量表是一系列中断服务程序入口地址组成的表。这些中断服务程序(函数)在中断向量表中的位置是由半导体厂商定好的,当某个中断被触发以后就会自动跳转到中断向量表中对应的中断服务程序(函数)入口地址处。中断向量表在整个程序的最前面。
中断向量表都是链接到代码的最前面,比如一般 ARM 处理器都是从地址 0X00000000 开始执行指令的,那么中断向量表就是从 0X00000000 开始存放的。
创建中断向量表大概
1 .global _start /* 全局标号 */
2
3 _start:
4 ldr pc, =Reset_Handler /* 复位中断 */
5 ldr pc, =Undefined_Handler /* 未定义指令中断 */
6 ldr pc, =SVC_Handler /* SVC(Supervisor)中断 */
7 ldr pc, =PrefAbort_Handler /* 预取终止中断 */
8 ldr pc, =DataAbort_Handler /* 数据终止中断 */
9 ldr pc, =NotUsed_Handler /* 未使用中断 */
10 ldr pc, =IRQ_Handler /* IRQ 中断 */
11 ldr pc, =FIQ_Handler /* FIQ(快速中断)未定义中断 */
12
13 /* 复位中断 */
14 Reset_Handler:
15 /* 复位中断具体处理过程 */
16
17 /* 未定义中断 */
18 Undefined_Handler:
19 ldr r0, =Undefined_Handler
20 bx r0
21
22 /* SVC 中断 */
23 SVC_Handler:
24 ldr r0, =SVC_Handler
25 bx r0
26
27 /* 预取终止中断 */
28 PrefAbort_Handler:
29 ldr r0, =PrefAbort_Handler
30 bx r0
31
32 /* 数据终止中断 */
33 DataAbort_Handler:
34 ldr r0, =DataAbort_Handler
35 bx r0
36
37 /* 未使用的中断 */
38 NotUsed_Handler:
39
40 ldr r0, =NotUsed_Handler
41 bx r0
42
43 /* IRQ 中断!重点!!!!! */
44 IRQ_Handler:
45 /* 复位中断具体处理过程 */
46
47 /* FIQ 中断 */
48 FIQ_Handler:
49 ldr r0, =FIQ_Handler
50 bx r0
第 4 到 11 行是中断向量表,当指定的中断发生以后就会调用对应的中断复位函数,比如复位中断发生以后就会执行第 4 行代码,也就是调用函数 Reset_Handler,函数 Reset_Handler就是复位中断的中断复位函数,其它的中断同理。
第 14 到 50 行就是对应的中断服务函数,中断服务函数都是用汇编编写的,我们实际需要编写的只有复位中断服务函数 Reset_Handler 和 IRQ 中断服务函数 IRQ_Handler,其它的中断本教程没有用到,所以都是死循环。在编写复位中断复位函数和 IRQ 中断服务函数之前我们还需要了解一些其它的知识,否则的话就没法编写。
中断向量偏移
ARM 处理器都是从地址 0X00000000 开始运行的,但是代码是下载到 0X8000000 开始的存储区域中。因此中断向量表是存放到 0X8000000 地址处的,而不是 0X00000000,这样不是就出错了吗? ——— 中断向量表偏移,通过中断向量表偏移就可以将中断向量表存放到任意地址处。
NXP 官方 SDK中的文件 MCIMX6Y2C.h,在此文件中定义了一个枚举类型 IRQn_Type,此枚举类型就枚举出了 I.MX6U 的所有中断,代码如下所示:
1 #define NUMBER_OF_INT_VECTORS 160 /* 中断源 160 个, SGI+PPI+SPI*/
2
3 typedef enum IRQn {
4 /* Auxiliary constants */
5 NotAvail_IRQn = -128,
6
7 /* Core interrupts */
8 Software0_IRQn = 0,
9 Software1_IRQn = 1,
10 Software2_IRQn = 2,
11 Software3_IRQn = 3,
12 Software4_IRQn = 4,
13 Software5_IRQn = 5,
14 Software6_IRQn = 6,
15 Software7_IRQn = 7,
16 Software8_IRQn = 8,
17 Software9_IRQn = 9,
18 Software10_IRQn = 10,
19 Software11_IRQn = 11,
20 Software12_IRQn = 12,
21 Software13_IRQn = 13,
22 Software14_IRQn = 14,
23 Software15_IRQn = 15,
24 VirtualMaintenance_IRQn = 25,
25 HypervisorTimer_IRQn = 26,
26 VirtualTimer_IRQn = 27,
27 LegacyFastInt_IRQn = 28,
28 SecurePhyTimer_IRQn = 29,
29 NonSecurePhyTimer_IRQn = 30,
30 LegacyIRQ_IRQn = 31,
31
32 /* Device specific interrupts */
33 IOMUXC_IRQn = 32,
34 DAP_IRQn = 33,
35 SDMA_IRQn = 34,
36 TSC_IRQn = 35,
37 SNVS_IRQn = 36,
…… ...... ......
151 ENET2_1588_IRQn = 153,
152 Reserved154_IRQn = 154,
153 Reserved155_IRQn = 155,
154 Reserved156_IRQn = 156,
155 Reserved157_IRQn = 157,
156 Reserved158_IRQn = 158,
157 PMU_IRQ2_IRQn = 159
158} IRQn_Type;
1.8.4.3、S5PV210的异常向量表
(1)异常向量表在1.2.14节讲过,可以返回去听一下
(2)异常向量表中各个向量的相对位置是固定的,但是他们的起始地址是不固定的,各种SoC可以不一样,而且复杂ARM中还可以让用户来软件设置这个异常向量表的基地址。
(3)扩展到所有架构的CPU中:所有架构(譬如51单片机、PIC单片机)的CPU实现中断都是通过异常向量表实现的,这个机制是不变的;但是不同CPU异常向量表的构造和位置是不同的
1.8.4.4、异常和中断的区别和联系
(1)针对SoC来说,发生复位、软中断、中断、快速中断、取指令异常、数据异常等,我们都统一叫异常。所以说:中断其实是异常的一种。
(2)异常的定义就是突发事件,打断了CPU的正常常规业务,CPU不得不跳转到异常向量表中去执行异常处理程序;中断是异常的一种,一般特指SoC内的内部外设产生的打断SoC常规业务,或者外部中断(SoC的GPIO引脚传回来的中断)。
1.8.5.异常向量表的编程处理
1.8.5.1、像内存一样去访问异常向量表
(1)S5PV210的异常向量表可以改变(在CP15协处理器中),以适应操作系统的需求。但是目前系统刚启动时,此时DRAM尚未初始化,程序都在SRAM中运行。210在iRAM中设置了异常向量表,供暂时性使用。
(2)查210的iROM application note文档中iRAM的地址分配,可知,iRAM中的异常向量表起始地址为0xD0037400。知道了异常向量表的起始地址后,各个异常对应的入口就很好知道了。
1.8.5.2、函数名的实质就是函数的首地址
(1)函数名在C语言中的理解方法和变量名其实没区别。编译器会把这个函数的函数体对应的代码段和这个函数的函数名(实质是符号)对应起来,等我们在使用这个函数名符号时,编译器会将函数的函数体实际上做替换。因为函数体都不止4字节,而函数名这个符号只能对应1个地址,所以实际对应的是函数体那一个代码段的首地址。
(2)拿C语言中的语法来讲,函数名就是这个函数的函数指针。
总结:当我们将异常处理程序的首地址和异常向量表绑定起来后,异常处理初步阶段就完成了。到目前可以保证相应异常发生后,硬件自动跳转到对应异常向量表入口去执行时,可以执行到我们事先绑定的函数。
1.8.5.3、为什么中断处理要先在汇编中进行
(1)中断处理要注意保护现场(中断从SVC模式来,则保存SVC模式下的必要寄存器的值)和恢复现场(中断处理完成后,准备返回SVC模式前,要将保存的SVC模式下的必要寄存器的值恢复回去,不然到了SVC模式后寄存器的值乱了,SVC模式下原来正在进行的常规任务就被你搞坏了)
(2)保存现场包括:第一:设置IRQ栈;第二,保存LR;第三,保存R0~R12
(3)为什么要保存LR寄存器?要考虑中断返回的问题。中断ISR执行完后如何返回SVC模式下去接着执行原来的代码。中断返回其实取决于我们进入中断时如何保存现场。**中断返回时关键的2个寄存器就是PC和CPSR。**所以我们在进入IRQ模式时,应该将SVC模式下的下一句指令的地址(中断返回地址)和CPSR保存起来,将来恢复时才可以将中断返回地址给PC,将保存的CPSR给CPSR。
(4)中断返回地址就保存在LR中,而CPSR(自动)保存在(IRQ模式下的)SPSR中
1.8.5.4、汇编保存现场和恢复现场
(1)保护现场关键是保存:中断处理程序的返回地址,r0-r12(cpsr是自动保存的)
(2)恢复现场主要是恢复:r0-r12,pc,cpsr
1.8.6.S5PV210的向量中断控制器
1.8.6.1、异常处理的2个阶段
(1)可以将异常处理分为2个阶段来理解。第一个阶段是异常向量表跳转;第二个阶段就是进入了真正的异常处理程序irq_handler之后的部分。
1.8.6.2、回顾:中断处理的第一阶段(异常向量表阶段)处理。
(1)第一个阶段之所以能够进行,主要依赖于CPU设计时提供的异常向量表机制。第一个阶段的主要任务是从异常发生到响应异常并且保存/恢复现场、跳转到真正的异常处理程序处。
(2)第二个阶段的目的是识别多个中断源中究竟哪一个发生了中断,然后调用相应的中断处理程序来处理这个中断。
1.8.6.3、S3C2440的第二阶段处理过程
(1)第一个问题,怎么找到具体是哪个中断:S3C2440的中断控制器中有一个寄存器(32位的),寄存器的每一个位对应一个中断源(为了解决支持更多中断源,2440又设计了一个子中断机制。在一级中断寄存器中有一些中断是共用的一个bit位,譬如AC97和WDT。对于共用中断,用子中断来区分究竟是哪一个发生了中断)
(2)第二个问题,怎么找到对应的isr的问题:首先给每个中断做了个编号,进入isr_handler之后先通过查阅中断源寄存器和子中断寄存器(中哪一位为1)确定中断的编号,然后用这个编号去isr数组(isr数组是中断初始化时事先设定好的,就是把各个中断的isr的函数名组成一个数组,用中断对应的编号作为索引来查询这个数组)中查阅得到isr地址。
评价:2440的中断处理设计不是特别优秀:第一个过程中使用子中断搞成2级的很麻烦;第二个过程中计算中断编号是个麻烦事,很耗费时间。而中断处理的时间是很宝贵的(系统有一个性能指标,叫实时性。实时性就是中断发生到响应的时间,这个时间越短越好。)
1.8.6.4、S5PV210的第二阶段处理过程
(1)第一个问题,怎么找到具体是哪个中断:S5PV210中因为支持的中断源很多,所以直接设计了4个中断寄存器,每个32位,每位对应一个中断源。(理论上210最多支持128个中断,实际支持不足128个,有些位是空的);210没有子中断寄存器,每个中断源都是并列的。当中断发生时,在irq_handler中依次去查询4个中断源寄存器,看哪一个的哪一位被置1,则这个位对应的寄存器就发生了中断,即找到了中断编号。
(2)第二个问题,怎么找到对应的isr的问题:210中支持的中断源多了很多,如果还使用2440的那一套来寻找isr地址就太慢了,太影响实时性了。于是210开拓了一种全新的寻找isr的机制。210提供了很多寄存器来解决每个中断源对应isr的寻找问题,具体寻找过程和建立过程见下节,实现的效果是当发生相应中断时,硬件会自动的将相应isr推入一定的寄存器中,我们软件只要去这个寄存器中执行函数就行了。
1.8.6.5、总结:第一阶段都相同,第二阶段各不同
(1)第一阶段(异常向量表阶段)2440和210几乎是完全相同的。实际上几乎所有的CPU在第一阶段都是相同的。
(2)第二阶段就彼此不同了。各个SoC根据自己对实时性的要求,和支持的中断源的多少,各自发明了各自处理中断,找到中断编号,进一步找到对应isr地址的方式。
1.8.7.S5PV210中断处理的主要寄存器
1.8.7.1、VICnINTENABLE和VICnINTENCLEAR
(1)VICnINTENABLE 对应interrupt enable,INTENCLEAR对应interrupt enable clear
(2)INTENABLE寄存器负责相应的中断的使能,INTENCLEAR寄存器负责相应的中断的禁止。
(3)当我们想使能(意思就是启用这个中断,意思就是当硬件产生中断时CPU能接收的到)某个中断时,只要在这个中断编号对应的VICnINTENABLE的相应bit位写1即可(注意这个位写1其他位写0对其他位没有影响);如果我们想禁止某个中断源时,只要向VICnINTENCLEAR中相应的位写1即可。
注意:这里的设计一共有2种:有些CPU是中断使能和禁止是一个寄存器位,写1就使能写0就进制(或者反过来写1就进制写0就使能),这样的中断使能设计就要非常小心,要使用我们之前说过的读改写三部曲来操作;另一种就是使能和禁止分开为2个寄存器,要使能就写使能寄存器,要禁止就写禁止寄存器。这样的好处是我们使能/禁止操作时不需要读改写,直接写即可。
1.8.7.2、VICnINTSELECT
(1)设置各个中断的模式为irq还是fiq。一般都设置成irq
(2)IRQ和FIQ究竟有何区别。210中支持2种中断,irq和fiq。irq是普通中断,fiq是快速中断。快速中断提供一种更快响应处理的中断通道,用于对实时性要求很高的中断源。fiq在CPU设计时预先提供了一些机制保证fiq可以被快速处理,从而保证实时性。fiq的限制就是只能有一个中断源被设置为fiq,其他都是irq。
(3)CPU如何保证fiq比irq快?有2个原因:第一,fiq模式有专用的r8~r12,因此在fiq的isr中可以直接使用r8-r12而不用保存,这就能节省时间;第二,异常向量表中fiq是最后一个异常向量入口。因此fiq的isr不需要跳转,可以直接写在原地,这样就比其他异常少跳转一次,省了些时间。
1.8.7.3、VICnIRQSTATUS和VICnFIQSTATUS
(1)中断状态寄存器,是只读的。当发生了中断时,硬件会自动将该寄存器的对应位置为1,表示中断发生了。软件在处理中断第二阶段的第一阶段,就是靠查询这个寄存器来得到中断编号的。
1.8.7.4、VICnVECTPRIORITY0~VICnVECTPRIORITY31
(1)中断优先级设置寄存器,设置多个中断同时发生时先处理谁后处理谁的问题。一般来说高优先级的中断可以打断低优先级的中断,从而嵌套处理中断。当然了有些硬件/软件可以设置不支持中断嵌套。
1.8.7.5、VICnVECTADDR0~VICnVECTADDR31、VICnADDR
(1)这三个寄存器和210中断处理第二阶段的第二阶段有关。
(2)VICnVECTADDR0到31这32个寄存器分别用来存放真正的各个中断对应的isr的函数地址。相当于每一个中断源都有一个VECTADDR寄存器,程序员在设置中断的时候,把这个中断的isr地址直接放入这个中断对应的VECTADDR寄存器即可。
(3)VICnADDR这个寄存器是只需要读的,它里面的内容是由硬件自动设置的。当发生了相应中断时,硬件会自动识别中断编号,并且会自动找到这个中断的VECTADDR寄存器,然后将其读出复制到VICnADDR中,供我们使用。这样的设计避免了软件查找中断源和isr,节省了时间,提高了210的中断响应速度。
1.8.8.S5PV210中断处理的编程实践1
1.8.8.2、中断控制器初始化
主要工作有:第一阶段绑定异常向量表到异常处理程序;禁止所有中断源;选择所有中断类型为IRQ;清理VICnADDR寄存器为0.
1.8.8.3、中断的使能与禁止
思路是先根据中断号判断这个中断属于VIC几,然后在用中断源减去这个VIC的偏移量,得到这个中断号在本VIC中的偏移量,然后1<<x位,写入相应的VIC的INTENABLE/INTENCLEAR寄存器即可。
1.8.8.4、绑定自己实现的isr到VICnVECTADDR
(1)搞清楚2个寄存器的区别:VICnVECTADDR和VICnADDR
(1)VICVECTADDR寄存器一共有4×32个,每个中断源都有一个VECTADDR寄存器,我们应该将自己为这个中断源写的isr地址丢到这个中断源对应的VECTADDR寄存器中即可。
1.8.8.5、真正的中断处理程序如何获取isr
(1)当发生中断时,硬件会自动把相应中断源的isr地址从VICnVECTADDR寄存器中推入VICnADDR寄存器中,所以我们第二阶段的第二阶段isr_handler中,只需要到相应的VICnADDR中去拿出isr地址,调用执行即可。
总结:第4步绑定isr地址到VICnVECTADDR和第5步中断发生时第二阶段的第二阶段如何获取isr地址,这两步是相关的。这两个的结合技术,就是我们一直在说的210的硬件自动寻找isr的机制。
整个中断的流程梳理:
整个中断的工作分为2部分:
第一部分是我们为中断响应而做的预备工作:
1. 初始化中断控制器
2. 绑定写好的isr到中断控制器
3. 相应中断的所有条件使能
第二部分是当硬件产生中断后如何自动执行isr:
1. 第一步,经过异常向量表跳转入IRQ/FIQ的入口
2. 第二步,做中断现场保护(在start.S中),然后跳入isr_handler
3. 第三步,在isr_handler中先去搞清楚是哪个VIC中断了,然后直接去这个VIC的ADDR
寄存器中取isr来执行即可。
4. 第四步,isr执行完,中断现场恢复,直接返回继续做常规任务。
四、S5PV210中断处理的编程实践2
1.8.10.外部中断
1.8.10.1、什么是外部中断?数据手册在哪里?
(1)SoC支持的中断类型中有一类叫外部中断。内部中断就是指的中断源来自于SoC内部(一般是内部外设),譬如串口、定时器等部件产生的中断;外部中断是SoC外部的设备,通过外部中断对应的GPIO引脚产生的中断。
(2)按键在SoC中就使用外部中断来实现。具体实现方法是:将按键电路接在外部中断的GPIO上,然后将GPIO配置为外部中断模式。此时人通过按按键改变按键电路的电压高低,这个电压高低会触发GPIO对应的外部中断,通过引脚传进去给CPU处理。
(3)外部中断相关的介绍和寄存器都在2.2.6章节(属于GPIO部分)
1.8.10.2、电平触发和边沿触发
(1)外部中断的触发模式主要有2种:电平触发和边沿触发。
(1)电平触发就是说GPIO上的电平只要满足条件,就会不停触发中断。电平触发分为高电平触发和低电平触发。电平触发的特点是,只要电平满足条件就会不停触发中断。
(2)边沿触发分为上升沿触发、下降沿触发和双边沿触发三种。边沿触发不关心电平常规状态,只关心电平变化的瞬间(边沿触发不关心电平本身是高还是低,只关心变化是从高到低还是从低到高的这个过程)。
分析按键的工作:如果我们关注的是按键按下和弹起这两个事件本身,那么应该用边沿触发来处理按键;如果我们关心的是按键按下/弹起的那一段时间,那么应该用电平触发。
1.8.10.3、关键寄存器:CON、PEND、MASK
(1)外部中断的主要配置寄存器有3个:EXT_CON、EXT_PEND、EXT_MASK
(2)EXT_CON配置外部中断的触发方式。触发方式就是说外部电平怎么变化就能触发中断,也就是说这个外部中断产生的条件是什么
(3)EXT_PEND寄存器是中断挂起寄存器。这个寄存器中每一位对应一个外部中断,平时没有中断时值为0。当发生了中断后,硬件会自动将这个寄存器中该中断对应的位置1,我们去处理完这个中断后应该手工将该位置0。这个PEND寄存器的位就相当于是一个标志,如果发生了中断但是我们暂时忙来不及去处理时,这个位一直是1(这就是挂起),直到我有空了去处理了这个中断才会手工清除(写代码清除)这个挂起位表示这个中断被我处理了。
(4)EXT_MASK寄存器就是各个外部中断的使能/禁止开关。
分析X210开发板的按键对应的EINT编号:
EINT2、EINT3、EINT16、EINT17、EINT18、EINT19
1.8.11.中断方式处理按键编程实践1
1.8.11.1、外部中断对应的GPIO模式设置
1.8.11.2、中断触发模式设置
1.8.11.3、中断允许、清挂起
1.8.11.4、中断处理程序isr编写
1.8.11.5、总结对比:轮询方式处理按键和中断方式的差异
1.8.12.中断方式处理按键编程实践2
1、对中断源进行编号,定义指针,指向对应的中断控制器的寄存器
interrupt_init.h
#ifndef __INT_H__
#define __INT_H__
void intc_init(void);
void intc_enable(unsigned long intnum);
void intc_disable(unsigned long intnum);
void intc_setvectaddr(unsigned long intnum, void (*handler)(void));
void intc_clearvectaddr(void);
unsigned long intc_getvicirqstatus(unsigned long ucontroller);
void irq_handler(void);
void IRQ_handle(void);
void system_init_exception(void);
Interrupt
#define VIC0_BASE (0xF2000000)
#define VIC1_BASE (0xF2100000)
#define VIC2_BASE (0xF2200000)
#define VIC3_BASE (0xF2300000)
// VIC0
#define VIC0IRQSTATUS ( *((volatile unsigned long *)(VIC0_BASE + 0x00)) )
#define VIC0FIQSTATUS ( *((volatile unsigned long *)(VIC0_BASE + 0x04)) )
#define VIC0INTSELECT ( *((volatile unsigned long *)(VIC0_BASE + 0x0c)) )
#define VIC0INTENABLE ( *((volatile unsigned long *)(VIC0_BASE + 0x10)) )
#define VIC0INTENCLEAR ( *((volatile unsigned long *)(VIC0_BASE + 0x14)) )
#define VIC0VECTADDR (VIC0_BASE + 0x100)
#define VIC0ADDR ( *((volatile unsigned long *)(VIC0_BASE + 0xf00)) )
// VIC1
#define VIC1IRQSTATUS ( *((volatile unsigned long *)(VIC1_BASE + 0x00)) )
#define VIC1FIQSTATUS ( *((volatile unsigned long *)(VIC1_BASE + 0x04)) )
#define VIC1INTSELECT ( *((volatile unsigned long *)(VIC1_BASE + 0x0c)) )
#define VIC1INTENABLE ( *((volatile unsigned long *)(VIC1_BASE + 0x10)) )
#define VIC1INTENCLEAR ( *((volatile unsigned long *)(VIC1_BASE + 0x14)) )
#define VIC1VECTADDR (VIC1_BASE + 0x100)
#define VIC1ADDR ( *((volatile unsigned long *)(VIC1_BASE + 0xf00)) )
// VIC2
#define VIC2IRQSTATUS ( *((volatile unsigned long *)(VIC2_BASE + 0x00)) )
#define VIC2FIQSTATUS ( *((volatile unsigned long *)(VIC2_BASE + 0x04)) )
#define VIC2INTSELECT ( *((volatile unsigned long *)(VIC2_BASE + 0x0c)) )
#define VIC2INTENABLE ( *((volatile unsigned long *)(VIC2_BASE + 0x10)) )
#define VIC2INTENCLEAR ( *((volatile unsigned long *)(VIC2_BASE + 0x14)) )
#define VIC2VECTADDR (VIC2_BASE + 0x100)
#define VIC2ADDR ( *((volatile unsigned long *)(VIC2_BASE + 0xf00)) )
// VIC3
#define VIC3IRQSTATUS ( *((volatile unsigned long *)(VIC3_BASE + 0x00)) )
#define VIC3FIQSTATUS ( *((volatile unsigned long *)(VIC3_BASE + 0x04)) )
#define VIC3INTSELECT ( *((volatile unsigned long *)(VIC3_BASE + 0x0c)) )
#define VIC3INTENABLE ( *((volatile unsigned long *)(VIC3_BASE + 0x10)) )
#define VIC3INTENCLEAR ( *((volatile unsigned long *)(VIC3_BASE + 0x14)) )
#define VIC3VECTADDR (VIC3_BASE + 0x100)
#define VIC3ADDR ( *((volatile unsigned long *)(VIC3_BASE + 0xf00)) )
#define exception_vector_table_base 0xD0037400
#define exception_reset (exception_vector_table_base + 0x00)
#define exception_undef (exception_vector_table_base + 0x04)
#define exception_sotf_int (exception_vector_table_base + 0x08)
#define exception_prefetch (exception_vector_table_base + 0x0C)
#define exception_data (exception_vector_table_base + 0x10)
#define exception_irq (exception_vector_table_base + 0x18)
#define exception_fiq (exception_vector_table_base + 0x1C)
#define r_exception_reset (*(volatile unsigned int *)exception_reset)
#define r_exception_undef (*(volatile unsigned int *)exception_undef)
#define r_exception_sotf_int (*(volatile unsigned int *)exception_sotf_int)
#define r_exception_prefetch (*(volatile unsigned int *)exception_prefetch)
#define r_exception_data (*(volatile unsigned int *)exception_data)
#define r_exception_irq (*(volatile unsigned int *)exception_irq)
#define r_exception_fiq (*(volatile unsigned int *)exception_fiq)
// 中断源编号
#define INT_LIMIT (96)
//INT NUM - VIC0
#define NUM_EINT0 (0)
#define NUM_EINT1 (1)
#define NUM_EINT2 (2)
#define NUM_EINT3 (3)
#define NUM_EINT4 (4)
#define NUM_EINT5 (5)
#define NUM_EINT6 (6)
#define NUM_EINT7 (7)
#define NUM_EINT8 (8)
#define NUM_EINT9 (9)
#define NUM_EINT10 (10)
#define NUM_EINT11 (11)
#define NUM_EINT12 (12)
#define NUM_EINT13 (13)
#define NUM_EINT14 (14)
#define NUM_EINT15 (15)
#define NUM_EINT16_31 (16)
#define NUM_Reserved17 (17)
#define NUM_MDMA (18)
#define NUM_PDMA0 (19)
#define NUM_PDMA1 (20)
#define NUM_TIMER0 (21)
#define NUM_TIMER1 (22)
#define NUM_TIMER2 (23)
#define NUM_TIMER3 (24)
#define NUM_TIMER4 (25)
#define NUM_SYSTIMER (26)
#define NUM_WDT (27)
#define NUM_RTC_ALARM (28)
#define NUM_RTC_TICK (29)
#define NUM_GPIOINT (30)
#define NUM_FIMC3 (31)
//INT NUM - VIC1
#define NUM_CORTEX0 (32+0)
#define NUM_CORTEX1 (32+1)
#define NUM_CORTEX2 (32+2)
#define NUM_CORTEX3 (32+3)
#define NUM_CORTEX4 (32+4)
#define NUM_IEM_APC (32+5)
#define NUM_IEM_IEC (32+6)
#define NUM_Reserved39 (32+7)
#define NUM_NFC (32+8)
#define NUM_CFC (32+9)
#define NUM_UART0 (32+10)
#define NUM_UART1 (32+11)
#define NUM_UART2 (32+12)
#define NUM_UART3 (32+13)
#define NUM_I2C (32+14)
#define NUM_SPI0 (32+15)
#define NUM_SPI1 (32+16)
#define NUM_SPI2 (32+17)
#define NUM_AUDIO (32+18)
#define NUM_I2C_PMIC (32+19)
#define NUM_I2C_HDMI (32+20)
#define NUM_HSIRX (32+21)
#define NUM_HSITX (32+22)
#define NUM_UHOST (32+23)
#define NUM_OTG (32+24)
#define NUM_MSM (32+25)
#define NUM_HSMMC0 (32+26)
#define NUM_HSMMC1 (32+27)
#define NUM_HSMMC2 (32+28)
#define NUM_MIPI_CSI (32+29)
#define NUM_MIPI_DSI (32+30)
#define NUM_ONENAND_AUDI (32+31)
//INT NUM - VIC2
#define NUM_LCD0 (64+0)
#define NUM_LCD1 (64+1)
#define NUM_LCD2 (64+2)
#define NUM_LCD3 (64+3)
#define NUM_ROTATOR (64+4)
#define NUM_FIMC_A (64+5)
#define NUM_FIMC_B (64+6)
#define NUM_FIMC_C (64+7)
#define NUM_JPEG (64+8)
#define NUM_2D (64+9)
#define NUM_3D (64+10)
#define NUM_MIXER (64+11)
#define NUM_HDMI (64+12)
#define NUM_HDMI_I2C (64+13)
#define NUM_MFC (64+14)
#define NUM_TVENC (64+15)
#define NUM_I2S0 (64+16)
#define NUM_I2S1 (64+17)
#define NUM_I2S2 (64+18)
#define NUM_AC97 (64+19)
#define NUM_PCM0 (64+20)
#define NUM_PCM1 (64+21)
#define NUM_SPDIF (64+22)
#define NUM_ADC (64+23)
#define NUM_PENDN (64+24)
#define NUM_KEYPAD (64+25)
#define NUM_Reserved90 (64+26)
#define NUM_HASH (64+27)
#define NUM_FEEDCTRL (64+28)
#define NUM_PCM2 (64+29)
#define NUM_SDM_IRQ (64+30)
#define NUM_SMD_FIQ (64+31)
//INT NUM - VIC3
#define NUM_IPC (96+0)
#define NUM_HOSTIF (96+1)
#define NUM_HSMMC3 (96+2)
#define NUM_CEC (96+3)
#define NUM_TSI (96+4)
#define NUM_MDNIE0 (96+5)
#define NUM_MDNIE1 (96+6)
#define NUM_MDNIE2 (96+7)
#define NUM_MDNIE3 (96+8)
#define NUM_ADC1 (96+9)
#define NUM_PENDN1 (96+10)
#define NUM_ALL (200)
#endif
2、中断定义函数
interrupt_init.c
#include "interrupt_init.h"
void reset_exception(void)
{
printf("reset_exception.\n");
}
void undef_exception(void)
{
printf("undef_exception.\n");
}
void sotf_int_exception(void)
{
printf("sotf_int_exception.\n");
}
void prefetch_exception(void)
{
printf("prefetch_exception.\n");
}
void data_exception(void)
{
printf("data_exception.\n");
}
// 主要功能:绑定第一阶段异常向量表;禁止所有中断;选择所有中断类型为IRQ;
// 清除VICnADDR为0
void system_init_exception(void)
{
// 第一阶段处理,绑定异常向量表
r_exception_reset = (unsigned int)reset_exception;
r_exception_undef = (unsigned int)undef_exception;
r_exception_sotf_int = (unsigned int)sotf_int_exception;
r_exception_prefetch = (unsigned int)prefetch_exception;
r_exception_data = (unsigned int)data_exception;
r_exception_irq = (unsigned int)IRQ_handle;
r_exception_fiq = (unsigned int)IRQ_handle;
// 初始化中断控制器的基本寄存器
intc_init();
}
// 清除需要处理的中断的中断处理函数的地址
void intc_clearvectaddr(void)
{
// VICxADDR:当前正在处理的中断的中断处理函数的地址
VIC0ADDR = 0;
VIC1ADDR = 0;
VIC2ADDR = 0;
VIC3ADDR = 0;
}
// 初始化中断控制器
void intc_init(void)
{
// 禁止所有中断
// 为什么在中断初始化之初要禁止所有中断?
// 因为中断一旦打开,因为外部或者硬件自己的原因产生中断后一定就会寻找isr
// 而我们可能认为自己用不到这个中断就没有提供isr,这时它自动拿到的就是乱码
// 则程序很可能跑飞,所以不用的中断一定要关掉。
// 一般的做法是先全部关掉,然后再逐一打开自己感兴趣的中断。一旦打开就必须
// 给这个中断提供相应的isr并绑定好。
VIC0INTENCLEAR = 0xffffffff;
VIC1INTENCLEAR = 0xffffffff;
VIC2INTENCLEAR = 0xffffffff;
VIC3INTENCLEAR = 0xffffffff;
// 选择中断类型为IRQ
VIC0INTSELECT = 0x0;
VIC1INTSELECT = 0x0;
VIC2INTSELECT = 0x0;
VIC3INTSELECT = 0x0;
// 清VICxADDR
intc_clearvectaddr();
}
// 绑定我们写的isr到VICnVECTADDR寄存器
// 绑定过之后我们就把isr地址交给硬件了,剩下的我们不用管了,硬件自己会处理
// 等发生相应中断的时候,我们直接到相应的VICnADDR中去取isr地址即可。
// 参数:intnum是int.h定义的物理中断号,handler是函数指针,就是我们写的isr
// VIC0VECTADDR定义为VIC0VECTADDR0寄存器的地址,就相当于是VIC0VECTADDR0~31这个
// 数组(这个数组就是一个函数指针数组)的首地址,然后具体计算每一个中断的时候
// 只需要首地址+偏移量即可。
void intc_setvectaddr(unsigned long intnum, void (*handler)(void))
{
//VIC0
if(intnum<32)
{
*( (volatile unsigned long *)(VIC0VECTADDR + 4*(intnum-0)) ) = (unsigned)handler;
}
//VIC1
else if(intnum<64)
{
*( (volatile unsigned long *)(VIC1VECTADDR + 4*(intnum-32)) ) = (unsigned)handler;
}
//VIC2
else if(intnum<96)
{
*( (volatile unsigned long *)(VIC2VECTADDR + 4*(intnum-64)) ) = (unsigned)handler;
}
//VIC3
else
{
*( (volatile unsigned long *)(VIC3VECTADDR + 4*(intnum-96)) ) = (unsigned)handler;
}
return;
}
// 使能中断
// 通过传参的intnum来使能某个具体的中断源,中断号在int.h中定义,是物理中断号
void intc_enable(unsigned long intnum)
{
unsigned long temp;
// 确定intnum在哪个寄存器的哪一位
// <32就是0~31,必然在VIC0
if(intnum<32)
{
temp = VIC0INTENABLE;
temp |= (1<<intnum); // 如果是第一种设计则必须位操作,第二种设计可以
// 直接写。
VIC0INTENABLE = temp;
}
else if(intnum<64)
{
temp = VIC1INTENABLE;
temp |= (1<<(intnum-32));
VIC1INTENABLE = temp;
}
else if(intnum<96)
{
temp = VIC2INTENABLE;
temp |= (1<<(intnum-64));
VIC2INTENABLE = temp;
}
else if(intnum<NUM_ALL)
{
temp = VIC3INTENABLE;
temp |= (1<<(intnum-96));
VIC3INTENABLE = temp;
}
// NUM_ALL : enable all interrupt
else
{
VIC0INTENABLE = 0xFFFFFFFF;
VIC1INTENABLE = 0xFFFFFFFF;
VIC2INTENABLE = 0xFFFFFFFF;
VIC3INTENABLE = 0xFFFFFFFF;
}
}
// 禁止中断
// 通过传参的intnum来禁止某个具体的中断源,中断号在int.h中定义,是物理中断号
void intc_disable(unsigned long intnum)
{
unsigned long temp;
if(intnum<32)
{
temp = VIC0INTENCLEAR;
temp |= (1<<intnum);
VIC0INTENCLEAR = temp;
}
else if(intnum<64)
{
temp = VIC1INTENCLEAR;
temp |= (1<<(intnum-32));
VIC1INTENCLEAR = temp;
}
else if(intnum<96)
{
temp = VIC2INTENCLEAR;
temp |= (1<<(intnum-64));
VIC2INTENCLEAR = temp;
}
else if(intnum<NUM_ALL)
{
temp = VIC3INTENCLEAR;
temp |= (1<<(intnum-96));
VIC3INTENCLEAR = temp;
}
// NUM_ALL : disable all interrupt
else
{
VIC0INTENCLEAR = 0xFFFFFFFF;
VIC1INTENCLEAR = 0xFFFFFFFF;
VIC2INTENCLEAR = 0xFFFFFFFF;
VIC3INTENCLEAR = 0xFFFFFFFF;
}
return;
}
// 通过读取VICnIRQSTATUS寄存器,判断其中哪个有一位为1,来得知哪个VIC发生中断了
unsigned long intc_getvicirqstatus(unsigned long ucontroller)
{
if(ucontroller == 0)
return VIC0IRQSTATUS;
else if(ucontroller == 1)
return VIC1IRQSTATUS;
else if(ucontroller == 2)
return VIC2IRQSTATUS;
else if(ucontroller == 3)
return VIC3IRQSTATUS;
else
{}
return 0;
}
// 真正的中断处理程序。意思就是说这里只考虑中断处理,不考虑保护/恢复现场
void irq_handler(void)
{
//printf("irq_handler.\n");
// SoC支持很多个(在低端CPU例如2440中有30多个,在210中有100多个)中断
// 这么多中断irq在第一个阶段走的是一条路,都会进入到irq_handler来
// 我们在irq_handler中要去区分究竟是哪个中断发生了,然后再去调用该中断
// 对应的isr。
// 虽然硬件已经自动帮我们把isr放入了VICnADDR中,但是因为有4个,所以我们必须
// 先去软件的检查出来到底哪个VIC中断了,也就是说isr到底在哪个VICADDR寄存器中
unsigned long vicaddr[4] = {VIC0ADDR,VIC1ADDR,VIC2ADDR,VIC3ADDR};
int i=0;
void (*isr)(void) = NULL;
for(i=0; i<4; i++)
{
// 发生一个中断时,4个VIC中有3个是全0,1个的其中一位不是0
if(intc_getvicirqstatus(i) != 0)
{
isr = (void (*)(void)) vicaddr[i];
break;
}
}
(*isr)(); // 通过函数指针来调用函数
3、按键,中断设置
key.c
#include "main.h"
// 定义操作寄存器的宏
#define GPH0CON 0xE0200C00
#define GPH0DAT 0xE0200C04
#define GPH2CON 0xE0200C40
#define GPH2DAT 0xE0200C44
#define rGPH0CON (*(volatile unsigned int *)GPH0CON)
#define rGPH0DAT (*(volatile unsigned int *)GPH0DAT)
#define rGPH2CON (*(volatile unsigned int *)GPH2CON)
#define rGPH2DAT (*(volatile unsigned int *)GPH2DAT)
#define EXT_INT_0_CON 0xE0200E00
#define EXT_INT_2_CON 0xE0200E08
#define EXT_INT_0_PEND 0xE0200F40
#define EXT_INT_2_PEND 0xE0200F48
#define EXT_INT_0_MASK 0xE0200F00
#define EXT_INT_2_MASK 0xE0200F08
#define rEXT_INT_0_CON (*(volatile unsigned int *)EXT_INT_0_CON)
#define rEXT_INT_2_CON (*(volatile unsigned int *)EXT_INT_2_CON)
#define rEXT_INT_0_PEND (*(volatile unsigned int *)EXT_INT_0_PEND)
#define rEXT_INT_2_PEND (*(volatile unsigned int *)EXT_INT_2_PEND)
#define rEXT_INT_0_MASK (*(volatile unsigned int *)EXT_INT_0_MASK)
#define rEXT_INT_2_MASK (*(volatile unsigned int *)EXT_INT_2_MASK)
//-----------------------中断方式处理按键-----------------------------------
// 以中断方式来处理按键的初始化
void key_init_interrupt(void)
{
// 1. 外部中断对应的GPIO模式设置
rGPH0CON |= 0xFF<<8; // GPH0_2 GPH0_3设置为外部中断模式
rGPH2CON |= 0xFFFF<<0; // GPH2_0123共4个引脚设置为外部中断模式
// 2. 中断触发模式设置
rEXT_INT_0_CON &= ~(0xFF<<8); // bit8~bit15全部清零
rEXT_INT_0_CON |= ((2<<8)|(2<<12)); // EXT_INT2和EXT_INT3设置为下降沿触发
rEXT_INT_2_CON &= ~(0xFFFF<<0);
rEXT_INT_2_CON |= ((2<<0)|(2<<4)|(2<<8)|(2<<12));
// 3. 中断允许
rEXT_INT_0_MASK &= ~(3<<2); // 外部中断允许
rEXT_INT_2_MASK &= ~(0x0f<<0);
// 4. 清挂起,清除是写1,不是写0
rEXT_INT_0_PEND |= (3<<2);
rEXT_INT_2_PEND |= (0x0F<<0);
}
// EINT2通道对应的按键,就是GPH0_2引脚对应的按键,就是开发板上标了LEFT的那个按键
void isr_eint2(void)
{
// 真正的isr应该做2件事情。
// 第一,中断处理代码,就是真正干活的代码
printf("isr_eint2_LEFT.\n");
led2();
// 第二,清除中断挂起
rEXT_INT_0_PEND |= (1<<2);
intc_clearvectaddr();
}
void isr_eint3(void)
{
// 真正的isr应该做2件事情。
// 第一,中断处理代码,就是真正干活的代码
printf("isr_eint3_DOWN.\n");
led1();
// 第二,清除中断挂起
rEXT_INT_0_PEND |= (1<<3);
intc_clearvectaddr();
}
void isr_eint16171819(void)
{
// 真正的isr应该做2件事情。
// 第一,中断处理代码,就是真正干活的代码
// 因为EINT16~31是共享中断,所以要在这里再次去区分具体是哪个子中断
if (rEXT_INT_2_PEND & (1<<0))
{
printf("eint16\n");
}
if (rEXT_INT_2_PEND & (1<<1))
{
printf("eint17\n");
}
if (rEXT_INT_2_PEND & (1<<2))
{
printf("eint18\n");
}
if (rEXT_INT_2_PEND & (1<<3))
{
printf("eint19\n");
}
// 第二,清除中断挂起
rEXT_INT_2_PEND |= (0x0f<<0);
intc_clearvectaddr();
}
4、led灯
#define GPJ0CON 0xE0200240
#define GPJ0DAT 0xE0200244
#define rGPJ0CON *((volatile unsigned int *)GPJ0CON)
#define rGPJ0DAT *((volatile unsigned int *)GPJ0DAT)
static void delay(void);
// 该函数要实现led闪烁效果
void led_blink(void)
{
// led初始化,也就是把GPJ0CON中设置为输出模式
//volatile unsigned int *p = (unsigned int *)GPJ0CON;
//volatile unsigned int *p1 = (unsigned int *)GPJ0DAT;
rGPJ0CON = 0x11111111;
while (1)
{
// led亮
rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
// 延时
delay();
// led灭
rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
// 延时
delay();
}
}
// 亮1个led
void led1(void)
{
rGPJ0CON = 0x11111111;
rGPJ0DAT = ((0<<3) | (1<<4) | (1<<5));
}
// 亮2个led
void led2(void)
{
rGPJ0CON = 0x11111111;
rGPJ0DAT = ((0<<3) | (0<<4) | (1<<5));
}
// 亮3个led
void led3(void)
{
rGPJ0CON = 0x11111111;
rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
}
void led_off(void)
{
rGPJ0CON = 0x11111111;
rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
}
static void delay(void)
{
volatile unsigned int i = 900000; // volatile 让编译器不要优化,这样才能真正的减
while (i--); // 才能消耗时间,实现delay
}
5、main函数,调用
#include "interrupt.h"
#include "main.h"
#define KEY_EINT2 NUM_EINT2 // 将中断编号,定义成key容易,分晓,看
#define KEY_EINT3 NUM_EINT3
#define KEY_EINT16_19 NUM_EINT16_31 // 其余4个共用的
void delay(int i)
{
volatile int j = 10000;
while (i--)
while(j--);
}
int main(void)
{
//调用,按键,中断初始化
key_init_interrupt();
// 如果程序中要使用中断,就要调用中断初始化来初步初始化中断控制器
system_init_exception();
// 绑定isr到中断控制器硬件,对应的中断编号寄存器,中断发生时,硬件会自动压缩这个函数地址,到4个控制器中对应的一个,的寄存器
intc_setvectaddr(KEY_EINT2, isr_eint2);
intc_setvectaddr(KEY_EINT3, isr_eint3);
intc_setvectaddr(KEY_EINT16_19, isr_eint16171819);
// 使能中断
intc_enable(KEY_EINT2);
intc_enable(KEY_EINT3);
intc_enable(KEY_EINT16_19);
//按键的处理,就在后台,中断去处理了,while只要前端的事,按键按下后,就可以点亮led灯
while (1)
{
//做事
delay(10000);
}
return 0;
}
6、汇编,,去跳转到main
start.S
define WTCON 0xE2700000
#define SVC_STACK 0xd0037d80
#define IRQ_STACK 0xd0037f80
.global _start
.global IRQ_handle
// 把_start链接属性改为外部,这样其他文件就可以看见_start了
_start:
// 第1步:关看门狗(向WTCON的bit5写入0即可)
ldr r0, =WTCON
ldr r1, =0x0
str r1, [r0]
// 第2步:初始化时钟
bl clock_init
// 第3步:设置SVC栈
ldr sp, =SVC_STACK
// 第4步:开/关icache
mrc p15,0,r0,c1,c0,0; // 读出cp15的c1到r0中
//bic r0, r0, #(1<<12) // bit12 置0 关icache
orr r0, r0, #(1<<12) // bit12 置1 开icache
mcr p15,0,r0,c1,c0,0;
bl main
// 从这里之后就可以开始调用C程序了
//bl led_blink // led_blink是C语言实现的一个函数
// 汇编最后的这个死循环不能丢
b .
// 在这个汇编函数中,用来做中断模式下的现场保护和恢复,并且调用真正的中断处理程序
IRQ_handle:
// 设置IRQ模式下的栈
ldr sp, =IRQ_STACK
// 保存LR
// 因为ARM有流水线,所以PC的值会比真正执行的代码+8,
sub lr, lr, #4
// 保存r0-r12和lr到irq模式下的栈上面
stmfd sp!, {r0-r12, lr}
// 在此调用真正的isr来处理中断
bl irq_handler
// 处理完成开始恢复现场,其实就是做中断返回,关键是将r0-r12,pc,cpsr一起回复
ldmfd sp!, {r0-r12, pc}^
7、Makefile文件
CC = arm-linux-gcc
LD = arm-linux-ld
OBJCOPY = arm-linux-objcopy
OBJDUMP = arm-linux-objdump
AR = arm-linux-ar
INCDIR := $(shell pwd)
# C预处理器的flag,flag就是编译器可选的选项
CPPFLAGS := -nostdlib -nostdinc -I$(INCDIR)/include
# C编译器的flag
CFLAGS := -Wall -O2 -fno-builtin
#导出这些变量到全局,其实就是给子文件夹下面的Makefile使用
export CC LD OBJCOPY OBJDUMP AR CPPFLAGS CFLAGS
objs := start.o led.o clock.o uart.o main.o int.o key.o
objs += lib/libc.a
uart.bin: $(objs)
$(LD) -Tlink.lds -o uart.elf $^
$(OBJCOPY) -O binary uart.elf uart.bin
$(OBJDUMP) -D uart.elf > uart_elf.dis
gcc mkv210_image.c -o mkx210
./mkx210 uart.bin 210.bin
lib/libc.a:
cd lib; make; cd ..
%.o : %.S
$(CC) $(CPPFLAGS) $(CFLAGS) -o $@ $< -c
%.o : %.c
$(CC) $(CPPFLAGS) $(CFLAGS) -o $@ $< -c
clean:
rm *.o *.elf *.bin *.dis mkx210 -f
cd lib; make clean; cd ..
Tlink.lds链接规则文件
SECTIONS
{
. = 0xd0020010;
.text : {
start.o
* (.text)
}
.data : {
* (.data)
}
bss_start = .;
.bss : {
* (.bss)
}
bss_end = .;
}
六、总结
第一步,按soc手册,硬件的中断编号,设置各个中断控制器组的,寄存器
第二步,设置gpio为中断模式,key
第三步,将isr_hander,绑定到这个跳转寄存器,也就是点亮led的函数