Linux 驱动开发基础知识——查询方式的按键驱动程序_编写框架(十三)

简介: Linux 驱动开发基础知识——查询方式的按键驱动程序_编写框架(十三)

一、LED 驱动回顾

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

        APP 继续调用 write 函数传入数值,在驱动程序的 led_write 函数根据该 数值去设置 GPIO 的数据寄存器,从而控制 GPIO 的输出电平。

        怎么操作寄存器?从芯片手册得到对应寄存器的物理地址,在驱动程序中使 用 ioremap 函数映射得到虚拟地址。驱动程序中使用虚拟地址去访问寄存器。

1.1 回顾 LED 驱动

二、 按键驱动编写思路

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

     图 2.1 按键原理图示意

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

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

       编写按键驱动程序最简单的方法如图 14.3 所示:

2.2 按键驱动编写

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

2.3 驱动程序编写套路

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

三、编程:先写框架

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

(1)button_drv.c 分配/设置/注册 file_operations 结构体

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

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

(2)board_xxx.c 分配/设置/注册 button_operations 结构体

       这个结构体是我们自己抽象出来的,里面定义单板 xxx 的按键操作函数。

        这样的结构易于扩展,对于不同的单板,只需要替换 board_xxx.c 提供自己的 button_operations 结构体即可。

3.1 button_drv.c (上层)

#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 int major = 0;
 
static struct button_operations *p_button_opr;
static struct class *button_class;
 
static int button_open (struct inode *inode, struct file *file)
{
    int minor = iminor(inode);
    p_button_opr->init(minor);
    return 0;
}
 
static ssize_t button_read (struct file *file, char __user *buf, size_t size, loff_t *off)
{
    unsigned int minor = iminor(file_inode(file));
    char level;
    int err;
    
    level = p_button_opr->read(minor);
    err = copy_to_user(buf, &level, 1);
    return 1;
}
 
 
static struct file_operations button_fops = {
    .open = button_open,
    .read = button_read,
};
 
void register_button_operations(struct button_operations *opr)
{
    int i;
 
    p_button_opr = opr;
    for (i = 0; i < opr->count; i++)
    {
        device_create(button_class, NULL, MKDEV(major, i), NULL, "100ask_button%d", i);
    }
}
 
void unregister_button_operations(void)
{
    int i;
 
    for (i = 0; i < p_button_opr->count; i++)
    {
        device_destroy(button_class, MKDEV(major, i));
    }
}
 
 
EXPORT_SYMBOL(register_button_operations);
EXPORT_SYMBOL(unregister_button_operations);
 
 
int button_init(void)
{
    major = register_chrdev(0, "100ask_button", &button_fops);
 
    button_class = class_create(THIS_MODULE, "100ask_button");
    if (IS_ERR(button_class))
        return -1;
    
    return 0;
}
 
void button_exit(void)
{
    class_destroy(button_class);
    unregister_chrdev(major, "100ask_button");
}
 
module_init(button_init);
module_exit(button_exit);
MODULE_LICENSE("GPL");
 
 

第26行:确定主设备号(让系统随机分配)

static int major = 0;

第29行:创建类,让设备自动创建设备号

 

static struct class *button_class;

定义一个file_operations结构体

static struct file_operations button_fops = {
    .open = button_open,
    .read = button_read,
};

第55~64行:创建register_button_operations来注册按钮操作

void register_button_operations(struct button_operations *opr)
{
    int i;
 
    p_button_opr = opr;
    for (i = 0; i < opr->count; i++)
    {
        device_create(button_class, NULL, MKDEV(major, i), NULL, "100ask_button%d", i);
    }
}

button_operations *opr中定义了count就可以知道有几个按键

用一个for循环遍历每个按键调用device_create 在button_class下面虚拟文件中构造设备节点,父亲为NULL,主设备为major,次设备号为i,格式为100ask_button%d。

在这个类下面创造了设备节点,主设备号为major,次设备号为i,名字是100ask_buttoni

第66~74行:unregister_button_operations,销毁上面创建的按钮操作,注销按钮操作

void unregister_button_operations(void)
{
    int i;
 
    for (i = 0; i < p_button_opr->count; i++)
    {
        device_destroy(button_class, MKDEV(major, i));
    }
}

销毁掉创建的类

device_destroy(button_class, MKDEV(major, i));

EXPORT_SYMBOL 是一个常用于 Linux 内核编程的宏,它用于将一个符号(例如一个函数或变量)标记为“导出的”,这意味着这个符号可以在其他模块中被引用或链接。在 Linux 内核中,模块是一种可以动态加载和卸载的代码段,而 EXPORT_SYMBOL 使得一个模块中的函数或变量可以被其他模块所使用。

EXPORT_SYMBOL(register_button_operations);
EXPORT_SYMBOL(unregister_button_operations);

这俩个函数是给别人用的,我们需要在button.drv头文件里面声明一下

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

第81~90行:定义入口函数init()

int button_init(void)
{
    major = register_chrdev(0, "100ask_button", &button_fops);
 
    button_class = class_create(THIS_MODULE, "100ask_button");
    if (IS_ERR(button_class))
        return -1;
    
    return 0;
}

在入口函数中把file_operations结构体 button_fops告诉内核

major = register_chrdev(0, "100ask_button", &button_fops);

在入口函数创建类

button_class = class_create(THIS_MODULE, "100ask_button");

在这个类下创造dev,需要有真实的dev时候才能创建,由底层 button_operations *opr提供

第92~96行:定义出口函数 exit()

void button_exit(void)
{
    class_destroy(button_class);
    unregister_chrdev(major, "100ask_button");
}

卸载主设备号

unregister_chrdev(major, "100ask_button");

销毁掉入口函数中创建的类

class_destroy(button_class);

第98~100行:向内核声明函数

module_init(button_init);
module_exit(button_exit);
MODULE_LICENSE("GPL");

框架搭建完毕,现在我们实现file_operations结构体中的button_open和 button_read

主设备号用来寻找驱动程序,次设备号是提供给我们用的,想怎么用就怎么用,想用次设备号寻找哪个按键就用哪个次设备号

第83行 button_init中注册主设备号

major = register_chrdev(0, "100ask_button", &button_fops);    //主设备号

第31~36行 :编写button_open函数

static int button_open (struct inode *inode, struct file *file)
{
    int minor = iminor(inode);
    p_button_opr->init(minor);
    return 0;
}

调用底层提供的p_button_opr结构体中的init()函数来初始化引脚,配置为输入引脚,传入次设备号minor,从inode节点中获得次设备号

int minor = iminor(inode);
p_button_opr->init(minor)

 

第38~47行 :编写button_read函数

static ssize_t button_read (struct file *file, char __user *buf, size_t size, loff_t *off)
{
    unsigned int minor = iminor(file_inode(file));
    char level;
    int err;
    
    level = p_button_opr->read(minor);
    err = copy_to_user(buf, &level, 1);
    return 1;
}

从file中获得次设备号

unsigned int minor = iminor(file_inode(file));

调用底层提供的p_button_opr结构体中的read()函数来把GPIO的电平读回来

level = p_button_opr->read(minor);

读回来后传回用户空间,从level中将数据拷贝到buf中,拷贝1字节,return 1表示读了1字节

err = copy_to_user(buf, &level, 1);
return 1;

问题来了,底层提供的p_button_opr结构体哪里来的呢?

是底层硬件相关的代码提供的,现在让我们看一下底层代码

3.2 button_drv.h

#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 *opr);
void unregister_button_operations(void);
 
#endif
 

count:有多少个按键

设置button_operations结构体提供上层init()函数和read()函数

在 button_drv.c (上层)第28行中定义全局变量,就可以使用底层p_button_opr

static struct button_operations *p_button_opr;

在button_drv.c (上层)第55~64行中提供注册函数:

void register_button_operations(struct button_operations *opr)
{
    int i;
 
    p_button_opr = opr;
    for (i = 0; i < opr->count; i++)
    {
        device_create(button_class, NULL, MKDEV(major, i), NULL, "100ask_button%d", i);
    }
}

由底层向上一层提供p_button_opr结构体

3.3 board_xxx.c(底层)

#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, read gpio for button %d\n", __FILE__, __FUNCTION__, __LINE__, which);
    return 1;
}
 
static struct button_operations my_buttons_ops ={
    .count = 2,
    .init  = board_xxx_button_init_gpio,
    .read  = board_xxx_button_read_gpio,
};
 
int board_xxx_button_init(void)
{
    register_button_operations(&my_buttons_ops);
    return 0;
}
 
void board_xxx_button_exit(void)
{
    unregister_button_operations();
}
 
module_init(board_xxx_button_init);
module_exit(board_xxx_button_exit);
 
MODULE_LICENSE("GPL");
 
 

第26~29行:初始化引脚

 

static void board_xxx_button_init_gpio (int which)
{
    printk("%s %s %d, init gpio for button %d\n", __FILE__, __FUNCTION__, __LINE__, which);
}

打印一句话

第31~35行:读取引脚

static int board_xxx_button_read_gpio (int which)
{
    printk("%s %s %d, read gpio for button %d\n", __FILE__, __FUNCTION__, __LINE__, which);
    return 1;
}

打印一句话并且返回引脚电平

第37~41行:定义一个 button_operations 结构体

static struct button_operations my_buttons_ops ={
    .count = 2,
    .init  = board_xxx_button_init_gpio,
    .read  = board_xxx_button_read_gpio,
};

count表明有2个按键

init表明初始化引脚的函数

read表明读取引脚的函数

第43~47行:注册函数

int board_xxx_button_init(void)
{
    register_button_operations(&my_buttons_ops);
    return 0;
}

入口函数中注册了一个my_buttons_ops结构体

第49~52行:卸载函数

void board_xxx_button_exit(void)
{
    unregister_button_operations();
}

出口函数中销毁一个my_buttons_ops结构体

3.4 button_test.c(测试程序)

 
#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;
}
 
 

3.5 Makefile

 
# 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_imx6ull-sdk/Linux-4.9.88
 
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.1编译

编译程序,把代码上传代服务器后执行 make 命令。

cp *.ko button_test ~/nfs_rootfs/

4.2 挂载到开发板

在开发板上挂载 NFS

4.3 测试

1. [root@100ask:~]# echo "7 4 1 7" > /proc/sys/kernel/printk // 打开内核的打印信息,有些
2. 板子默认打开了

提示找不到符号,所以我们需要先装载button_drv.ko

4.4 测试结果:

大佬觉得有用的话点个赞 👍🏻 呗。

❤️❤️❤️本人水平有限,如有纰漏,欢迎各位大佬评论批评指正!😄😄😄

💘💘💘如果觉得这篇文对你有帮助的话,也请给个点赞、收藏下吧,非常感谢!👍 👍 👍

🔥🔥🔥任务在无形中完成,价值在无形中升华,让我们一起加油吧!🌙🌙🌙

目录
相关文章
|
2月前
|
安全 Linux Shell
Linux上执行内存中的脚本和程序
【9月更文挑战第3天】在 Linux 系统中,可以通过多种方式执行内存中的脚本和程序:一是使用 `eval` 命令直接执行内存中的脚本内容;二是利用管道将脚本内容传递给 `bash` 解释器执行;三是将编译好的程序复制到 `/dev/shm` 并执行。这些方法虽便捷,但也需谨慎操作以避免安全风险。
179 6
|
3月前
|
网络协议 Linux
Linux查看端口监听情况,以及Linux查看某个端口对应的进程号和程序
Linux查看端口监听情况,以及Linux查看某个端口对应的进程号和程序
638 2
|
3月前
|
Linux Python
linux上根据运行程序的进程号,查看程序所在的绝对路径。linux查看进程启动的时间
linux上根据运行程序的进程号,查看程序所在的绝对路径。linux查看进程启动的时间
67 2
|
1月前
|
Linux API 开发工具
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
ijkplayer是由B站研发的移动端播放器,基于FFmpeg 3.4,支持Android和iOS。其源码托管于GitHub,截至2024年9月15日,获得了3.24万星标和0.81万分支,尽管已停止更新6年。本文档介绍了如何在Linux环境下编译ijkplayer的so库,以便在较新的开发环境中使用。首先需安装编译工具并调整/tmp分区大小,接着下载并安装Android SDK和NDK,最后下载ijkplayer源码并编译。详细步骤包括环境准备、工具安装及库编译等。更多FFmpeg开发知识可参考相关书籍。
83 0
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
|
1月前
|
运维 Java Linux
【运维基础知识】Linux服务器下手写启停Java程序脚本start.sh stop.sh及详细说明
### 启动Java程序脚本 `start.sh` 此脚本用于启动一个Java程序,设置JVM字符集为GBK,最大堆内存为3000M,并将程序的日志输出到`output.log`文件中,同时在后台运行。 ### 停止Java程序脚本 `stop.sh` 此脚本用于停止指定名称的服务(如`QuoteServer`),通过查找并终止该服务的Java进程,输出操作结果以确认是否成功。
36 1
|
2月前
|
消息中间件 分布式计算 Java
Linux环境下 java程序提交spark任务到Yarn报错
Linux环境下 java程序提交spark任务到Yarn报错
42 5
|
2月前
|
存储 Linux 开发工具
如何进行Linux内核开发【ChatGPT】
如何进行Linux内核开发【ChatGPT】
|
3月前
|
Linux Windows Python
最新 Windows\Linux 后台运行程序注解
本文介绍了在Windows和Linux系统后台运行程序的方法,包括Linux系统中使用nohup命令和ps命令查看进程,以及Windows系统中通过编写bat文件和使用PowerShell启动隐藏窗口的程序,确保即使退出命令行界面程序也继续在后台运行。
|
5天前
|
Linux
在 Linux 系统中,“cd”命令用于切换当前工作目录
在 Linux 系统中,“cd”命令用于切换当前工作目录。本文详细介绍了“cd”命令的基本用法和常见技巧,包括使用“.”、“..”、“~”、绝对路径和相对路径,以及快速切换到上一次工作目录等。此外,还探讨了高级技巧,如使用通配符、结合其他命令、在脚本中使用,以及实际应用案例,帮助读者提高工作效率。
24 3