文章目录
序言:
本文记录以rico board开发板为硬件平台,主控芯片为AM4378,实现按键驱动代码的过程。
同上一篇博客:【linux驱动之一、LED驱动】思路相同,步骤如下:
1、写驱动框架
2、看电路图找到按键所在GPIO口
3、看芯片手册,看怎样将GPIO口设置成输入模式,都需要配置哪些寄存器,以及每个寄存器的地址
4、完善驱动代码,填充到对应的接口中
5、写测试应用程序
6、验证功能
附:github工程连接:https://github.com/Warrior-Asa/key_drv_while1/blob/master/key_drv.c
一、写驱动框架
代码如下:
/* 头文件 */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <linux/device.h>
/* 设备号 & class */
int major;
static struct class *key_drv_class;
/* 配置寄存器为输入模式 */
static int key_drv_open(struct inode *inode, struct file *file)
{
return 0;
}
/* 读取寄存器的值,并返回给用户层 */
static ssize_t key_drv_read(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
return 0;
}
/* 结构体 */
static struct file_operations key_drv_fops = {
.owner = THIS_MODULE,
.open = key_drv_open,
.read = key_drv_read,
};
/* 驱动入口函数 */
static int key_drv_init(void)
{
major = register_chrdev(0, "key_drv", &key_drv_fops);
key_drv_class = class_create(THIS_MODULE, "class_key_drv");
device_create(key_drv_class, NULL, MKDEV(major, 0), NULL, "device_button"); /* 根据udev机制,该接口调用后会在/dev/目录下生成设备,名字为:device_button,上层应用程序要操作设备的话,需要open该设备名称,即:fd = open("/dev/device_button", O_RDWR); */
return 0;
}
/* 驱动出口函数 */
static void key_drv_exit(void)
{
unregister_chrdev(major, "key_drv"); /* 卸载驱动程序,告诉内核 */
device_destroy(key_drv_class, MKDEV(major, 0));
class_destroy(key_drv_class);
}
/* 声明 */
module_init(key_drv_init);
module_exit(key_drv_exit);
MODULE_LICENSE("Dual BSD/GPL");
二、查看电路图,找GPIO口
看下图,可以看到sw4对应GPIO口为:GPIO5_13
三、查看芯片手册,找寄存器&配置方法
3.1 GPIO口输入模式
查看芯片手册第28.3.4.3章节,可以看到如下描述:
大致意思是:当我们将GPIO口配置成输入模式的话,需要将GPIO_OE寄存器对应位设置成1,然后从GPIO_DATAIN寄存器对应位就可以读出从外部捕获的电平高低值了。
继续往下看,查找这两个寄存器的详细描述。
3.2 GPIO_OE寄存器
如下图:可以看到,需要将该寄存器对应位配置成1,则表示相应的GPIO口为input功能。另外:偏移地址为:0x134
3.3 GPIO_DATAIN寄存器
没什么好说的,只读寄存器,看清地址,配置好以后只需要读该寄存器即可。偏移地址为0x138
3.4 使用宏定义寄存器地址
(1)查看GPIO5基地址(找memory map之类的章节):可以看到GPIO5起始地址为:0x48322000
(2)宏定义地址如下:
/* 寄存器地址 */
#define GPIO5_BASE_ADDR 0x48322000
#define GPIO5_OE_ADDR ((GPIO5_BASE_ADDR) + 0x134)
#define GPIO5_DATAIN_ADDR ((GPIO5_BASE_ADDR) + 0x138)
四、完善驱动代码(完整代码)
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <linux/device.h>
/* 寄存器地址 */
#define GPIO5_BASE_ADDR 0x48322000
#define GPIO5_OE_ADDR ((GPIO5_BASE_ADDR) + 0x134)
#define GPIO5_DATAIN_ADDR ((GPIO5_BASE_ADDR) + 0x138)
/* 寄存器别名 */
volatile unsigned long *gpio5_oe = NULL;
volatile unsigned long *gpio5_datain = NULL;
int major;
static struct class *key_drv_class;
/* 配置寄存器为输入模式 */
static int key_drv_open(struct inode *inode, struct file *file)
{
*gpio5_oe |= (1 << 13);
return 0;
}
/* 读取寄存器的值,并返回给用户层 */
static ssize_t key_drv_read(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
int regValue;
unsigned char key_vals[4]; /* 保存引脚电平状态 */
regValue = *gpio5_datain; /* 读取当前寄存器的值 */
key_vals[0] = ((regValue & (1<<13)) ? 1 : 0); /* 判断读取到的电平状态是高还是低,将状态结果保存在key_vals[0]中 */
copy_to_user(buf, key_vals, sizeof(key_vals)); /* 读取到的高低电平状态上传给用户层 */
return sizeof(key_vals); /* 返回 */
}
/* 结构体 */
static struct file_operations key_drv_fops = {
.owner = THIS_MODULE,
.open = key_drv_open,
.read = key_drv_read,
};
/* 驱动入口函数 */
static int key_drv_init(void)
{
major = register_chrdev(0, "key_drv", &key_drv_fops);
key_drv_class = class_create(THIS_MODULE, "class_key_drv");
device_create(key_drv_class, NULL, MKDEV(major, 0), NULL, "device_button"); /* 根据udev机制,该接口调用后会在/dev/目录下生成设备,名字为:device_button,上层应用
程序要操作设备的话,需要open该设备名称,即:fd = open("/dev/device_button", O_RDWR); */
/* 地址映射 */
gpio5_oe = (volatile unsigned long *)ioremap(GPIO5_OE_ADDR, 32);
gpio5_datain = (volatile unsigned long *)ioremap(GPIO5_DATAIN_ADDR, 32);
return 0;
}
/* 驱动出口函数 */
static void key_drv_exit(void)
{
unregister_chrdev(major, "key_drv"); /* 卸载驱动程序,告诉内核 */
device_destroy(key_drv_class, MKDEV(major, 0));
class_destroy(key_drv_class);
/* 删除地址映射 */
iounmap(gpio5_oe);
iounmap(gpio5_datain);
}
/* 声明 */
module_init(key_drv_init);
module_exit(key_drv_exit);
MODULE_LICENSE("Dual BSD/GPL");
五、写测试应用程序
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
int main(int argc, char **argv)
{
int fd;
int cnt = 0; /* 计数 */
unsigned char key_value[4]; /* 底层驱动代码会将要获取的结果保存在此变量中 */
fd = open("/dev/device_button", O_RDWR); /* 打开要操作的设备 */
if (fd < 0) {
printf("can`t open!\n");
}
while(1) {
read(fd, key_value, sizeof(key_value)); /* 和驱动代码通信,获取电平高低状态,保存在key_value中 */
if (key_value[0] == 0) {
printf("times: %d, button pressed.\r\n", cnt++);
}
}
printf("ok\r\n");
return 0;
}
六、验证功能
按键驱动程序:未使用中断功能,直接使用死循环进行轮训监测。
操作步骤:
1、linux开发环境中执行:
make
—> 生成驱动程序:key_drv.ko
arm-linux-gnueabihf-gcc app_test_key.c -o app_test_key
—>生成测试程序:app_test_key
2、 将上述两个程序拷贝到单板中
3、板卡中执行指令:
insmod key_drv.ko
lsmod
./app_test_key
4、 按下按键,观察现象
有图有真相:
之所以按一次按键会反复打印,是因为用的while1循环执行的原因这样cpu资源也会被耗尽,所以正式开发中不会采用这种方式,一般使用中断的方式,下一节会记录如何使用按键中断的方式来实现捕获外部电平。