嵌入式实验——实验二:OK6410开发之按键中断


实验二:OK6410开发之按键中断

一、实验目的

1. 认识寄存器,熟悉ARM的架构,学会如何通过寄存器进行编程

2. 熟悉Linux开发环境,学习Linux系统指令

3.从按键程序的设计对系统的编程进行更深入理解,与单片机裸机编程进行区分

二、实验设备

开发机环境 操作系统:ubuntu 20.04

交叉编译环境:arm-linux-gcc 4.3.2 6410

板子内核源码:linux-3.0.1

目标板环境:OK6410-A linux-3.0

三、实验内容(原理)

1. 裸机开发(寄存器开发)

关于裸机开发主要学习这位博主的文章https://blog.csdn.net/u014754841/article/details/80377676,真的写的很好,能学到很多。

1.1 硬件电路:

image-20211222151241009

image-20211222151450720

从原理图我么们知道低电平时按键按下,我们这里可以设置下降沿触发(上升沿也可以,但是下降沿反应更快,按下就产生中断,上升沿是按完才响应)。

1.2 寄存器

我们都知道中断的大致步骤为:

image-20211223173120440

中断源检测中断信号产生,然后将中断信号发送给中断控制器,中断控制器判断该中断是否被屏蔽,从而决定该中断信号是否要发送给CPU。中断信号发送给CPU后,CPU对中断进行处理,也就是调用中断函数。上述过程,基本上是嵌入式的通过中断处理过程,只是不同的嵌入式在这三部分配置有区别而已。

查询资料我们知道S3C6410并不是每一个外部中断引脚都分配了中断号,因此,在中断服务程序中,为了知道具体是哪一个中断,还需要去查询寄存器以知道是哪一个中断产生。除此之外,S3C6410中断处理有向量模式和非向量方式,这两个模式里向量模式的中断处理的效率更高。具体原因是因为向量模式将各个中断都设定了对应的入口地址(向量就是有方向),可以直接通过入口地址进行中断;非向量模式下会先跳转到中断异常去,然后这个中断异常中,编写程序,判断是哪一个中断产生,然后去执行对应的中断处理程序,效率明显低。所以为了方便性和高效性,这次实验选择向量模式的中断。

综上我们大概知道此次需要配置的步骤:

1.2.1 设置外部管脚为中断;
1.2.2 设置中断触发方式;
1.2.3 取消中断屏蔽,使外部中断不屏蔽;
1.2.4 设置中断滤波(可不设置,这里忽略);
1.2.5 使能外部中断;
1.2.6 设置中断号的入口地址;
1.2.7 设置中断号的中断选择,是irq还是fiq,默认为是irq;
1.2.8 开启向量中断方式并打开全局中断;
1.2.9 编写中断处理函数,中断函数前和后要使用嵌入汇编,保存环境和恢复环境。中断处理后,要清除中断挂起位和中断执行地址

2. 结合系统开发:

如果使用系统开发的话难度是比寄存器裸机开发小很多的。因为中断注册,按键值的获取函数系统已经封装好了,所有的寄存器操作流程系统来做。总体开发流程也是跟裸机开发基本一致的,但是开发效率明显提高了。总体开发步骤如下:

2.1 按键GPIO初始化

2.2 中断初始化

2.3 配置定时器

2.3 内核注册

2.4 中断处理

2.5 注册按键(GPIO读取)

2.6 退出所有注册

四、实验代码

1. 裸机开发

1.1 设置外部引脚为中断模式

image-20211222164450058

image-20211222164827969

这六个按键中断由电路图也可以知道是EINT0到EINT5同时也是GPN0到GPN5的重映射,所以这块配置具体说明可以看上一个实验寄存器配置。很明显中断模式下GPNCON寄存器的GPN0到GPN5应该均被配置为10

#define  GPNCON *((volatile unsigned long*)0x7f008830)           /* GPN控制寄存器 */
#define  GPNDAT *((volatile unsigned long*)0x7f008834)           /* GPN数据寄存器 此处未使用*/
#define  GPNPUD *((volatile unsigned long*)0x7f008838)           /* GPN上下拉配置寄存器 此处未使用*/
 
/* 按键初始化 
*  设置按键对应引脚为外部中断模式
*/
void button_init(void)
{
	/*	方式1  移位
	GPNCON = (0b10 << 0) | (0b10 << 2) | (0b10 << 4) | (0b10 << 6) | (0b10 << 8) | (0b10 << 10);	
	*/
	/* 方式2 逻辑运算 */
	GPNCON &= (~0x00000aaa);	
	GPNCON |= 0x00000aaa;
}

1.2 设置中断触发方式

image-20211223142717758

由GPNCON寄存器我们知道GPN0~5对应的是外部中断分组0 ,所以这里我们只需要配置EINT0CON0寄存器即可,EINT0CON0说明如下:

image-20211223170633394

下降沿触发(Falling edge triggered)所以这几位均设置为010,代码如下:

/*interrupt registes*/
#define EXT_INT_0_CON       *((volatile unsigned int *)0x7f008900)        /* 外部中断0~27配置寄存器 */
 
    EXT_INT_0_CON &= ~(0x00000222);   
    EXT_INT_0_CON |= 0x00000222;                                   /* 配置为下降沿触发 */  

1.3 取消中断屏蔽

image-20211224144932688

image-20211224090409831

取消中断屏蔽就必须配置中断屏蔽寄存器,查阅手册如上图所示我们需要将EINT0到EINT5设置为0使能中断而不是屏蔽中断,代码如下:

#define EXT_INT_0_MASK      *((volatile unsigned int *)0x7f008920)        /* 外部中断0~27屏蔽寄存器 */  
 
EXT_INT_0_MASK &= 0xffffffc0;                                   /* 取消屏蔽外部中断 */

1.4 设置中断滤波(可以略过)

在干扰非常大的环境下,噪声很容易使开发板产生误判,所以我们需要配置下中断滤波,消除毛刺干扰,滤波寄存器配置如下图:

image-20211224141430217

这里有几位是延迟滤波,数字滤波的配置,数字滤波性能好很多故选择数字滤波,我们是EINT0到EINT5所以编写代码如下:

#define EXT_INT_0_FLTCON0   *((volatile unsigned int *)0x7f008910)        /* 外部中断组0滤波寄存器 */ 
 
EXT_INT_0_FLTCON0 = (0xff) | (0xff << 8) | (0xff << 16);        /* 设置外部中断0~5的滤波*/

1.5 外部中断使能(中断源设置)

使能外部中断,就得配置中断使能寄存器VICxINTENABLE,又因为这里是外部中断分组0的0~5,根据s3c6410手册中断章节中断源介绍部分可知,这里只需要配置VIC0INTENABLE,如下图:

image-20211224142902155

中断使能寄存器VIC0INTENABLE地址及说明如下:

G5tgSupBFh7jPxk

image-20211224143534721

从上图可知,只需将中断使能寄存器VIC0INTENABLE的bit0~1设置为1即可。配置代码如下:

#define VIC0INTENABLE       *((volatile unsigned int *)0x71200010)        /* 中断使能寄存器 */
 
 VIC0INTENABLE &= ~(0x00000003);                                /* 使能外部中断*/  
 VIC0INTENABLE |= (0x00000003);

1.6 设置中断号的入口地址

设置中断的入口地址就必须设置中断向量地址寄存器,由上可知:外部中断组0的0~5中断由中断源0、1产生,属于VIC0,所以这块的中断向量地址寄存器就是VIC0VECRADDR0和VIC0VECRADDR1,地址如下:

image-20211224145140380

中断向量地址寄存器说明如下:

image-20211224145834023

所以,这里只需要将自定义的中断处理函数的地址赋给VIC0VECRADDR0和VIC0VECRADDR1即可,代码如下:

#define EINT0_3_VECTADDR    *((volatile unsigned int *)0x71200100)        /* 外部中断0~3向量地址寄存器 */  
#define EINT4_7_VECTADDR    *((volatile unsigned int *)0x71200104)        /* 外部中断4~7向量地址寄存器 */
 
EINT0_3_VECTADDR = (int)INT_TINT0_ISR;                         /* 中断产生时,CPU就会自动的将VIC0VECTADDR0的值赋给VIC0ADDRESS并跳转到这个地址去执 */  
EINT4_7_VECTADDR = (int)INT_EINT1_ISR;                         /* INT_TINT0_ISR  INT_TINT1_ISR即为中断处理函数 */          

1.7 IRQ or FIQ

image-20211224150705853

IRQ是默认的中断处理模式,但FIQ性能要比IRQ好,原因是FIQ用到的寄存器更多,存储量更大,不需要像IRQ因为寄存器少需要插拔内存。具体解释可以看这篇文章《IRQ和FIQ中断的区别》,不过这次实验用默认的IRQ完全够了,保持默认也行,如果要修改的话把需要中断的几位设置为1就行。

1.8 开启向量中断方式并打开全局中断

开启向量中断方式需要操作系统控制协处理器(P15),而打开全局中断需要操作状态寄存器(CPSR)。开启向量中断方式:首先从arm11内核技术参考手册中找到系统控制协处理器章节(3.2节),找到与开启向量中断方式相关的协处理器控制寄存器,如下:

img

70

根据协处理器控制寄存器介绍得知,只需将控制寄存器的bit24置1就行了,代码如下:

/* 开启向量中断方式 */
__asm__(
    "mrc p15,0,r0,c1,c0,0\n"
    "orr r0,r0,#(1<<24)\n"
    "mcr p15,0,r0,c1,c0,0\n"          
    : 
    :
     );

开全局中断:从arm架构参考手册中找到程序状态寄存器相关章节,如下:

img

红框中这两个位就是关于开启中断的操作为,具体说明如下:

img

arm11内核技术参考手册中也有相关的介绍,如下:

img

设置代码如下:

 /* 打开全局中断(开总中断)*/
	__asm__(
    "mrs r0,cpsr\n"
    "bic r0, r0, #0x80\n"
    "msr cpsr_c, r0\n"            
    : 
    :
);

至此,有关外部中断的初始化就完成,下面就可以进行中断服务函数的设计了。

1.9 编写中断处理函数

中断函数前和后要使用嵌入汇编,保存环境和恢复环境。中断处理后,要清除中断挂起位和中断执行地址。

保存环境:这里需要嵌入汇编,代码如下:

/* 保存环境 */  
    __asm__(
    "sub lr, lr, #4\n"  
    "stmfd sp!, {r0-r12, lr}\n"       
    : 
    :
);

这是一个固定的代码,目的是为了保存环境,将r0-r12,lr寄存器的值给压入栈中。这里有sub lr,lr,#4。将lr的值减去4。这个原因就要从ARM的流水线说起了。ARM采用流水线,取址,译码,执行。所以pc的值永远是当前执行指令的地址+8。中断跳转的时候,会将pc的值给lr,pc的值为当前执行程序地址+8,lr的值就是pc的值。而返回的地址应该是执行阶段的下一条地址,也就是当前执行程序地址+4,所以直接返回lr的值就不对了,应该返回lr-4的值。

恢复环境:这里也需要嵌入汇编代码,如下:

/*回复环境*/
__asm__( 
    "ldmfd sp!, {r0-r12, pc}^ \n"       
    : 
    : 
);

这段代码也是固定的,目的是为了中断执行完后,在将这些值返回给r0-r12,pc寄存器。这样r0-r12寄存器的内容就恢复了,同时pc得到返回地址,就返回到中断前的程序地址去了。

有关于保存环境和恢复环境的代码。参考arm架构参考手册的A2,6章节,如下:

img

中断处理代码主要实现按键控制led和蜂鸣器运行状态,按下按键s2,点亮led1,再按一次s2,关闭led1;依次类推,详见代码:

/* 外部中断0~3处理函数 */ 
	if((EXT_INT_0_PEND & (1<<0)) == (1 << 0))
    	    led_Toggle(1);
	if((EXT_INT_0_PEND & (1<<1)) == (1 << 1))
    	    led_Toggle(2);
	if((EXT_INT_0_PEND & (1<<2)) == (1 << 2))
    	    led_Toggle(3);
	if((EXT_INT_0_PEND & (1<<3)) == (1 << 3))
          led_Toggle(4);
/* 外部中断4~7处理函数 */ 
if((EXT_INT_0_PEND & (1<<4)) == (1 << 4))    
    beep_Toggle();
if((EXT_INT_0_PEND & (1<<5)) == (1 << 5))
{     led_Toggle(0);
      beep_Toggle();
}

中断处理完后,需要将中断挂起位给清零。这里,很简单的将所有中断位都给清零。然后再将中断执行地址给清0。

中断位挂起寄存器如下:
image-20211224185006375

中断向量0地址寄存器如下:

image-20211224185420814

代码如下:

 /* 清除中断 */
    EXT_INT_0_PEND = ~0x0; 
    VIC0ADDRESS = 0;

到此S3C6410外部中断程序设计就完成了,但是此时代码仍不能使开发板正常工作,原因是中断处理函数的代码是用c代码写的,而c代码需要栈来环境保护和恢复环境。。虽然之前设置过栈,但是之前设置的栈是SVC模式下的栈,而不是irq模式下的栈。不同模式,有自己的备份寄存器,其中,栈SP是每个模式都有自己的。所以需要设置下irq模式下的栈。代码也是比较简单的。在之前的设置栈的汇编代码中,将模式切换为irq模式,再设置sp。

代码如下:

@栈初始化 64M内存用于栈
init_stack:
    msr cpsr_c, #0xd2
    ldr sp, =0x53000000    @ 初始化r13_irq
  
    msr cpsr_c, #0xd3
    ldr sp, =0x54000000    @ 将0x54000000写入sp寄存器中,栈大小64M    0x5400000000-0x500000000
    mov pc, lr	           @ 返回调用处继续往下执行

这样再将代码编译后下载至开发板里就可以正常工作了。

2. 系统开发

代码原理跟寄存器其实差不多,只是开发上简单很多,不用考虑各种寄存器配置,只需要配置几个重要的参数就能完成开发,开发如下:

2.1 按键GPIO初始化

struct pin_desc{
	unsigned int pin;
	unsigned int key_val;
};
/* 
	按键值按下时0x01,0x02,0x03,0x04,0x05,0x06
	松开时                0x81,0x82,0x83,0x84,0x85,0x86
*/
 
struct pin_desc pins_desc[6] = {
	{S3C64XX_GPN(0), 0x01},
	{S3C64XX_GPN(1), 0x02},
	{S3C64XX_GPN(2), 0x03},
	{S3C64XX_GPN(3), 0x04},
	{S3C64XX_GPN(4), 0x05},
	{S3C64XX_GPN(5), 0x06},
};

2.2 中断初始化

static int third_drv_open(struct inode *inode, struct file *file)
{
	//printk("third_drv_open\n");
 
 
	//request_irq(unsigned int irq,irq_handler_t handler,unsigned long flags,const char * name,void * dev)
	request_irq(IRQ_EINT(0), botton_irq, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "s2", &pins_desc[0]);
	request_irq(IRQ_EINT(1), botton_irq, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "s3", &pins_desc[1]);
	request_irq(IRQ_EINT(2), botton_irq, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "s4", &pins_desc[2]);
	request_irq(IRQ_EINT(3), botton_irq, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "s5", &pins_desc[3]);
	request_irq(IRQ_EINT(4), botton_irq, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "s6", &pins_desc[4]);
	request_irq(IRQ_EINT(5), botton_irq, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "s7", &pins_desc[5]);
	
	return 0;
}

2.3 配置定时器(进行消抖)

static irqreturn_t botton_irq(int irq, void *dev_id)
{
	irq_pd = (struct pin_desc *)dev_id;
	mod_timer(&button_timer, jiffies + HZ/100);
 
	return IRQ_HANDLED;
}

2.3 内核注册

static int third_drv_init(void)
{
//	printk(KERN_ERR "third_drv_init\n");
	init_timer(&button_timer);
//	button_timer.expires  = jiffies + HZ/100;
	button_timer.function = buttons_timer_function;
	add_timer(&button_timer);
 
	major = register_chrdev(0, "thirdt_drv", &third_drv_fops); // 注册, 告诉内核
 
	thirddrv_class = class_create(THIS_MODULE, "thirddrv");
 
	thirddrv_class_dev = device_create(thirddrv_class, NULL, MKDEV(major, 0), NULL, "bottons"); /* /dev/bottons */
 
 
	return 0;
}

2.4 中断处理

static void buttons_timer_function(unsigned long data)
{
	struct pin_desc * pindes = irq_pd;
	unsigned int pinval;
	if (!pindes)
		return;
	pinval = gpio_get_value(pindes->pin);
	if (pinval)
	{
	/* 松开的 */
		keyval = 0x80 | pindes->key_val;
	}
	else 
	{
	/* 按下的 */
		keyval = pindes->key_val;
	}
//	printk(KERN_ERR "irq = %d\n", irq);
	ev_press = 1;
	wake_up_interruptible(&button_waitq);
}

2.5 注册按键(GPIO读取)

static ssize_t third_drv_read(struct file *file, const char __user *buf, size_t size, loff_t * ppos)
{
 
	if(size != 1)
		return -EINVAL;
	/* 如果没有按键动作发生就休眠 */
	wait_event_interruptible(button_waitq, ev_press);
	/* 如果有按键动作发生就返回 */
	/* 传递给用户 */
	copy_to_user(buf, &keyval, 1);
	ev_press = 0;
 
	return 1;
}

2.6 退出所有注册

int third_drv_close(struct inode *inode, struct file *file)
{
	free_irq(IRQ_EINT(0), &pins_desc[0]);
	free_irq(IRQ_EINT(1), &pins_desc[1]);
	free_irq(IRQ_EINT(2), &pins_desc[2]);
	free_irq(IRQ_EINT(3), &pins_desc[3]);
	free_irq(IRQ_EINT(4), &pins_desc[4]);
	free_irq(IRQ_EINT(5), &pins_desc[5]);
	return 0;
 
}
static void third_drv_exit(void)
{
//	printk(KERN_ERR "third_drv_exit\n");
	unregister_chrdev(major, "thirdt_drv"); // 卸载
 
	device_unregister(thirddrv_class_dev);
	class_destroy(thirddrv_class);
	
}

五、实验步骤

1. 重新编译内核

在原有到内核中,按键的GPIO口被占用,需要进行相应到修改才能达到预期到效果,首先需要做的是安装libncurses 的相关软件,来实现对内核到编写

内核编写过程:

找到内核文件----->make menuconfig------>Device Drivers----->Input device support----->keyboards----->去掉GPIO Buttons----->make zImage----->重新烧写系统

2. 编写程序

具体代码见我的GitHub

程序写好后拷贝到SD卡里

3. 编译程序

make+arm-linux-gcc跟上一篇步骤差不多

4. 运行程序

insmod /sdcard/driver_key.ko`
mknod /dev/my_led c 240 0`
./key`
rmmod driver_key`

六、实验演示

视频见GitHub

七、心得体会

这次实验让我学到很多,不仅仅是体会到裸机开发与系统开发的区别,还在裸机开发上学到了很多关于ARM架构的东西,总之收获很多。