嵌入式实验——实验二: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 硬件电路:
从原理图我么们知道低电平时按键按下,我们这里可以设置下降沿触发(上升沿也可以,但是下降沿反应更快,按下就产生中断,上升沿是按完才响应)。
1.2 寄存器
我们都知道中断的大致步骤为:
中断源检测中断信号产生,然后将中断信号发送给中断控制器,中断控制器判断该中断是否被屏蔽,从而决定该中断信号是否要发送给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 设置外部引脚为中断模式
这六个按键中断由电路图也可以知道是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 设置中断触发方式
由GPNCON寄存器我们知道GPN0~5对应的是外部中断分组0 ,所以这里我们只需要配置EINT0CON0寄存器即可,EINT0CON0说明如下:
下降沿触发(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 取消中断屏蔽
取消中断屏蔽就必须配置中断屏蔽寄存器,查阅手册如上图所示我们需要将EINT0到EINT5设置为0使能中断而不是屏蔽中断,代码如下:
#define EXT_INT_0_MASK *((volatile unsigned int *)0x7f008920) /* 外部中断0~27屏蔽寄存器 */
EXT_INT_0_MASK &= 0xffffffc0; /* 取消屏蔽外部中断 */
1.4 设置中断滤波(可以略过)
在干扰非常大的环境下,噪声很容易使开发板产生误判,所以我们需要配置下中断滤波,消除毛刺干扰,滤波寄存器配置如下图:
这里有几位是延迟滤波,数字滤波的配置,数字滤波性能好很多故选择数字滤波,我们是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,如下图:
中断使能寄存器VIC0INTENABLE地址及说明如下:
从上图可知,只需将中断使能寄存器VIC0INTENABLE的bit0~1设置为1即可。配置代码如下:
#define VIC0INTENABLE *((volatile unsigned int *)0x71200010) /* 中断使能寄存器 */
VIC0INTENABLE &= ~(0x00000003); /* 使能外部中断*/
VIC0INTENABLE |= (0x00000003);
1.6 设置中断号的入口地址
设置中断的入口地址就必须设置中断向量地址寄存器,由上可知:外部中断组0的0~5中断由中断源0、1产生,属于VIC0,所以这块的中断向量地址寄存器就是VIC0VECRADDR0和VIC0VECRADDR1,地址如下:
中断向量地址寄存器说明如下:
所以,这里只需要将自定义的中断处理函数的地址赋给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
IRQ是默认的中断处理模式,但FIQ性能要比IRQ好,原因是FIQ用到的寄存器更多,存储量更大,不需要像IRQ因为寄存器少需要插拔内存。具体解释可以看这篇文章《IRQ和FIQ中断的区别》,不过这次实验用默认的IRQ完全够了,保持默认也行,如果要修改的话把需要中断的几位设置为1就行。
1.8 开启向量中断方式并打开全局中断
开启向量中断方式需要操作系统控制协处理器(P15),而打开全局中断需要操作状态寄存器(CPSR)。开启向量中断方式:首先从arm11内核技术参考手册中找到系统控制协处理器章节(3.2节),找到与开启向量中断方式相关的协处理器控制寄存器,如下:
根据协处理器控制寄存器介绍得知,只需将控制寄存器的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架构参考手册中找到程序状态寄存器相关章节,如下:
红框中这两个位就是关于开启中断的操作为,具体说明如下:
arm11内核技术参考手册中也有相关的介绍,如下:
设置代码如下:
/* 打开全局中断(开总中断)*/
__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章节,如下:
中断处理代码主要实现按键控制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。
中断位挂起寄存器如下:
中断向量0地址寄存器如下:
代码如下:
/* 清除中断 */
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架构的东西,总之收获很多。