韦东山嵌入式linux系列-查询方式的按键驱动程序_编写框架

1 LED 驱动回顾

对于 LED, APP 调用 open 函数导致驱动程序的 led_open 函数被调用。在里面,把 GPIO 配置为输出引脚。安装驱动程序后并不意味着会使用对应的硬件,而 APP 要使用对应的硬件,必须先调用 open 函数。所以建议在驱动程序的open 函数中去设置引脚。

APP 继续调用 write 函数传入数值,在驱动程序的 led_write 函数根据该数值去设置 GPIO 的数据寄存器,从而控制 GPIO 的输出电平。怎么操作寄存器?从芯片手册得到对应寄存器的物理地址,在驱动程序中使用 ioremap 函数映射得到虚拟地址。驱动程序中使用虚拟地址去访问寄存器。

2 按键驱动编写思路

GPIO 按键的原理图一般有如下 2 种:

按键没被按下时,上图中左边的 GPIO 电平为高,右边的 GPIO 电平为低。

按键被按下后,上图中左边的 GPIO 电平为低,右边的 GPIO 电平为高。

回顾一下编写驱动程序的套路

对于使用查询方式的按键驱动程序,我们只需要实现 button_open、button_read

3 编程:先写框架

我们的目的写出一个容易扩展到各种芯片、各种板子的按键驱动程序,所以驱动程序分为上下两层:

① button_drv.c 分配/设置/注册 file_operations 结构体

起承上启下的作用,向上提供 button_open,button_read 供 APP 调用。

而这 2 个函数又会调用底层硬件提供的 p_button_opr 中的 init、 read函数操作硬件。

② board_xxx.c 分配/设置/注册 button_operations 结构体

这个结构体是我们自己抽象出来的,里面定义单板 xxx 的按键操作函数。这样的结构易于扩展,对于不同的单板,只需要替换 board_xxx.c 提供自己的 button_operations 结构体即可。

3.1 把按键的操作抽象出一个 button_operations 结构体

首先看看 button_drv.h,它定义了一个 button_operations 结构体,把按键的操作抽象为这个结构体

cpp 复制代码
#ifndef BUTTON_DRV_H_
#define BUTTON_DRV_H_

struct button_operations {
	int count;
	void (*init) (int which);
	int (*read) (int which);
};

void register_button_operations(struct button_operations* operaions);
void unregister_button_operations(void);

#endif

再看看 board_xxx.c,它实现了一个 button_operations 结构体,代码如下。

调用 register_button_operations 函数,把这个结构体注册到上层驱动中

cpp 复制代码
// ...

// 定义button_operations结构体
static struct button_operations my_button_operations = {
	.count = 2,
	.init = board_xxx_button_init_gpio,
	.read = board_xxx_button_read_gpio,
};

// 入口函数
int board_xxx_init(void)
{
	// 注册设备结点
	register_button_operations(&my_button_operations);
	return 0;
}

// 出口函数
void board_xxx_exit(void)
{
	// 注销设备结点
	unregister_button_operations();
}

// ...

3.2 驱动程序的上层: file_operations 结构体

上层是 button_drv.c,它的核心是 file_operations 结构体,首先看看入口函数,代码如下

cpp 复制代码
int button_init(void)
{
	// 注册file_operations结构体
	major = register_chrdev(0, "winter_button", &button_operations);
	// 注册结点
	button_class = class_create(THIS_MODULE, "winter_button");
	// 注册失败
	if (IS_ERR(button_class))
	{
		return -1;
	}
	return 0;
}

向内核注册一个 file_operations 结构体。

同时创建一个 class,但是该 class 下还没有 device,在后面获得底层硬件的信息时再在 class 下创建 device:这只是用来创建设备节点,它不是驱动程序的核心。

再来看看 button_drv.c 中 file_operations 结构体的成员函数,代码如下

cpp 复制代码
static int major = 0;
static struct class* button_class;
static struct button_operations* p_button_operation;

// 4实现open/read函数
// open函数主要完成初始化操作
int button_open (struct inode* node, struct file* file)
{
	int minor;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	// 用次设备号控制某个按键
	minor = iminor(node);
	p_button_operation->init(minor);
	return 0;
}

// read读取按键信息
ssize_t button_read (struct file* file, char __user* buff, size_t size, loff_t* offset)
{
	unsigned int minor;
	int level;
	int err;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	// 通过file获取次设备号
	minor = iminor(file_inode(file));
	// 电平高低
	// 调用read函数
	level = p_button_operation->read(minor);
	// 将kernel_buf区的数据拷贝到用户区数据buf中,即从内核kernel_buf中读数据
	err = copy_to_user(buff, &level, 1);
	return 1;
}


// 2定义自己的file_operations结构体
static struct file_operations button_operations = {
	.open = button_open,
	.read = button_read,
};

button_operations 指针,来自于底层硬件相关的代码。

底层代码调用 register_button_operations 函数,向上提供这个结构体指针。

register_button_operations 函 数代码如下,它还根据底层提供button_operations 调用 device_create,这是创建设备节点

cpp 复制代码
// 注册设备结点
void register_button_operations(struct button_operations* p_operaions)
{
	int i;
	// 赋值
	p_button_operation = p_operaions;
	for (i = 0; i < p_operaions->count; i++)
	{
		device_create(button_class, NULL, MKDEV(major, i), NULL, "winter_button@%d", i);
	}
}

// 注销设备结点
void unregister_button_operations(void)
{
	int i;
	for (i = 0; i < p_button_operation->count; i++)
	{
		device_destroy(button_class, MKDEV(major, i));
	}
}

// 导出
EXPORT_SYMBOL(register_button_operations);
EXPORT_SYMBOL(unregister_button_operations);

完整代码

button_drv.c
cpp 复制代码
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/sched.h>
#include <linux/slab.h>
#include <linux/fcntl.h>
#include <linux/fs.h>
#include <linux/signal.h>
#include <linux/mutex.h>
#include <linux/mm.h>
#include <linux/timer.h>
#include <linux/wait.h>
#include <linux/skbuff.h>
#include <linux/proc_fs.h>
#include <linux/poll.h>
#include <linux/capi.h>
#include <linux/kernelcapi.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/moduleparam.h>

#include "button_drv.h"



// 1主设备号
static int major = 0;
static struct class* button_class;
static struct button_operations* p_button_operation;

// 注册设备结点
void register_button_operations(struct button_operations* p_operaions)
{
	int i;
	// 赋值
	p_button_operation = p_operaions;
	for (i = 0; i < p_operaions->count; i++)
	{
		device_create(button_class, NULL, MKDEV(major, i), NULL, "winter_button@%d", i);
	}
}

// 注销设备结点
void unregister_button_operations(void)
{
	int i;
	for (i = 0; i < p_button_operation->count; i++)
	{
		device_destroy(button_class, MKDEV(major, i));
	}
}

// 导出
EXPORT_SYMBOL(register_button_operations);
EXPORT_SYMBOL(unregister_button_operations);

// 4实现open/read函数
// open函数主要完成初始化操作
int button_open (struct inode* node, struct file* file)
{
	int minor;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	// 用次设备号控制某个按键
	minor = iminor(node);
	p_button_operation->init(minor);
	return 0;
}

// read读取按键信息
ssize_t button_read (struct file* file, char __user* buff, size_t size, loff_t* offset)
{
	unsigned int minor;
	int level;
	int err;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	// 通过file获取次设备号
	minor = iminor(file_inode(file));
	// 电平高低
	// 调用read函数
	level = p_button_operation->read(minor);
	// 将kernel_buf区的数据拷贝到用户区数据buf中,即从内核kernel_buf中读数据
	err = copy_to_user(buff, &level, 1);
	return 1;
}


// 2定义自己的file_operations结构体
static struct file_operations button_operations = {
	.open = button_open,
	.read = button_read,
};




// 3在入口函数中注册
int button_init(void)
{
	// 注册file_operations结构体
	major = register_chrdev(0, "winter_button", &button_operations);
	// 注册结点
	button_class = class_create(THIS_MODULE, "winter_button");
	// 注册失败
	if (IS_ERR(button_class))
	{
		return -1;
	}
	return 0;
}

// 出口函数
void button_exit(void)
{
	// 注销结点
	class_destroy(button_class);
	unregister_chrdev(major, "winter_button");
}

module_init(button_init);
module_exit(button_exit);
MODULE_LICENSE("GPL");
button_drv.h
cpp 复制代码
#ifndef BUTTON_DRV_H_
#define BUTTON_DRV_H_

struct button_operations {
	int count;
	void (*init) (int which);
	int (*read) (int which);
};

void register_button_operations(struct button_operations* operaions);
void unregister_button_operations(void);

#endif
board_xxx.c
cpp 复制代码
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/sched.h>
#include <linux/slab.h>
#include <linux/fcntl.h>
#include <linux/fs.h>
#include <linux/signal.h>
#include <linux/mutex.h>
#include <linux/mm.h>
#include <linux/timer.h>
#include <linux/wait.h>
#include <linux/skbuff.h>
#include <linux/proc_fs.h>
#include <linux/poll.h>
#include <linux/capi.h>
#include <linux/kernelcapi.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/moduleparam.h>

#include "button_drv.h"


// 实现具体的函数
static void board_xxx_button_init_gpio(int which)
{
	printk("%s %s %d, init gpio for button %d\n", __FILE__, __FUNCTION__, __LINE__, which);
}

static int board_xxx_button_read_gpio(int which)
{
	printk("%s %s %d, init gpio for button %d\n", __FILE__, __FUNCTION__, __LINE__, which);
	// 返回高电平
	return 1;
}

// 定义button_operations结构体
static struct button_operations my_button_operations = {
	.count = 2,
	.init = board_xxx_button_init_gpio,
	.read = board_xxx_button_read_gpio,
};

// 入口函数
int board_xxx_init(void)
{
	// 注册设备结点
	register_button_operations(&my_button_operations);
	return 0;
}

// 出口函数
void board_xxx_exit(void)
{
	// 注销设备结点
	unregister_button_operations();
}

module_init(board_xxx_init);
module_exit(board_xxx_exit);
MODULE_LICENSE("GPL");
board_test.c
cpp 复制代码
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

/*
 * ./button_test /dev/100ask_button0
 *
 */
int main(int argc, char **argv)
{
	int fd;
	char val;
	
	/* 1. 判断参数 */
	if (argc != 2) 
	{
		printf("Usage: %s <dev>\n", argv[0]);
		return -1;
	}

	/* 2. 打开文件 */
	fd = open(argv[1], O_RDWR);
	if (fd == -1)
	{
		printf("can not open file %s\n", argv[1]);
		return -1;
	}

	/* 3. 写文件 */
	read(fd, &val, 1);
	printf("get button : %d\n", val);
	
	close(fd);
	
	return 0;
}

Makefile

bash 复制代码
# 1. 使用不同的开发板内核时, 一定要修改KERN_DIR
# 2. KERN_DIR中的内核要事先配置、编译, 为了能编译内核, 要先设置下列环境变量:
# 2.1 ARCH,          比如: export ARCH=arm64
# 2.2 CROSS_COMPILE, 比如: export CROSS_COMPILE=aarch64-linux-gnu-
# 2.3 PATH,          比如: export PATH=$PATH:/home/book/100ask_roc-rk3399-pc/ToolChain-6.3.1/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin 
# 注意: 不同的开发板不同的编译器上述3个环境变量不一定相同,
#       请参考各开发板的高级用户使用手册

KERN_DIR = /home/book/100ask_stm32mp157_pro-sdk/Linux-5.4

all:
	make -C $(KERN_DIR) M=`pwd` modules 
	$(CROSS_COMPILE)gcc -o button_test button_test.c 

clean:
	make -C $(KERN_DIR) M=`pwd` modules clean
	rm -rf modules.order
	rm -f ledtest

# 参考内核源码drivers/char/ipmi/Makefile
# 要想把a.c, b.c编译成ab.ko, 可以这样指定:
# ab-y := a.o b.o
# obj-m += ab.o


obj-m	+= button_drv.o
obj-m	+= board_xxx.o

编译

4 测试

这只是一个示例程序,还没有真正操作硬件。测试程序操作驱动程序时,只会导致驱动程序中打印信息。首先设置交叉工具链,修改驱动 Makefile 中内核的源码路径,编译驱动和测试程序。启动开发板后,通过 NFS 访问编译好驱动程序、测试程序,就可以在开发板上如下操作了:

在开发板挂载 Ubuntu 的NFS目录

bash 复制代码
mount -t nfs -o nolock,vers=3 192.168.5.11:/home/book/nfs_rootfs/ /mnt

将ko文件和测试代码拷贝到挂载目录,安装驱动

bash 复制代码
insmod button_drv.ko
insmod board_xxx.ko

执行测试程序

bash 复制代码
./button_test /dev/winter_button@0
./button_test /dev/winter_button@1
相关推荐
挨踢攻城8 分钟前
Linux 下合并多个 PDF 文件为一个 PDF 文件的方法
linux·前端·rhce·rhca·linux运维·红帽认证·公众号:厦门微思网络
liulilittle11 分钟前
XDP VNP虚拟以太网关(章节:二)
linux·服务器·网络·c++·通信·xdp
比奇堡派星星12 分钟前
Linux 平台设备驱动框架详解
linux·开发语言·驱动开发
代码游侠23 分钟前
应用——Linux Framebuffer 图形库显示
linux·运维·服务器·数据库·笔记·算法
会飞的小新25 分钟前
Linux PCI 设备查看工具 lspci 的工作机制与使用方法
linux
xingzhemengyou126 分钟前
LINUX modprobe 智能加载和卸载内核模块
linux·服务器·前端
xingzhemengyou130 分钟前
Linux who指令查询和显示当前登录系统的用户信息
linux·服务器·网络
wadesir32 分钟前
Linux MySQL Sysbench一键部署与压测实战教程
linux·mysql·adb
米高梅狮子35 分钟前
6. Linux 硬盘分区管理
linux·运维·服务器
食咗未43 分钟前
Linux lrzsz文件传输工具的使用
linux·测试工具