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 测试结果:

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

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

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

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

目录
相关文章
|
13天前
|
存储 网络协议 Ubuntu
【Linux开发实战指南】基于UDP协议的即时聊天室:快速构建登陆、聊天与退出功能
UDP 是一种无连接的、不可靠的传输层协议,位于IP协议之上。它提供了最基本的数据传输服务,不保证数据包的顺序、可靠到达或无重复。与TCP(传输控制协议)相比,UDP具有较低的传输延迟,因为省去了建立连接和确认接收等过程,适用于对实时性要求较高、但能容忍一定数据丢失的场景,如在线视频、语音通话、DNS查询等。 链表 链表是一种动态数据结构,用于存储一系列元素(节点),每个节点包含数据字段和指向下一个节点的引用(指针)。链表分为单向链表、双向链表和循环链表等类型。与数组相比,链表在插入和删除操作上更为高效,因为它不需要移动元素,只需修改节点间的指针即可。但访问链表中的元素不如数组直接,通常需要从
|
5天前
|
Web App开发 缓存 Linux
FFmpeg开发笔记(三十六)Linux环境安装SRS实现视频直播推流
《FFmpeg开发实战》书中第10章提及轻量级流媒体服务器MediaMTX,适合测试RTSP/RTMP协议,但不适合生产环境。推荐使用SRS或ZLMediaKit,其中SRS是国产开源实时视频服务器,支持多种流媒体协议。本文简述在华为欧拉系统上编译安装SRS和FFmpeg的步骤,包括安装依赖、下载源码、配置、编译以及启动SRS服务。此外,还展示了如何通过FFmpeg进行RTMP推流,并使用VLC播放器测试拉流。更多FFmpeg开发内容可参考相关书籍。
20 2
FFmpeg开发笔记(三十六)Linux环境安装SRS实现视频直播推流
|
12天前
|
Linux
FFmpeg开发笔记(三十四)Linux环境给FFmpeg集成libsrt和librist
《FFmpeg开发实战》书中介绍了直播的RTSP和RTMP协议,以及新协议SRT和RIST。SRT是安全可靠传输协议,RIST是可靠的互联网流传输协议,两者于2017年发布。腾讯视频云采用SRT改善推流卡顿。以下是Linux环境下为FFmpeg集成libsrt和librist的步骤:下载安装源码,配置、编译和安装。要启用这些库,需重新配置FFmpeg,添加相关选项,然后编译和安装。成功后,通过`ffmpeg -version`检查版本信息以确认启用SRT和RIST支持。详细过程可参考书中相应章节。
22 1
FFmpeg开发笔记(三十四)Linux环境给FFmpeg集成libsrt和librist
|
13天前
|
安全 小程序 Linux
Linux中信号是什么?Ctrl + c后到底为什么会中断程序?
信号在进程的学习中是一个非常好用的存在,它是软件层次上对中断机制的一种模拟,是异步通信方式,同时也可以用来检测用户空间到底发生了什么情况,然后系统知道后就可以做出相应的对策。
|
13天前
|
SQL 自然语言处理 网络协议
【Linux开发实战指南】基于TCP、进程数据结构与SQL数据库:构建在线云词典系统(含注册、登录、查询、历史记录管理功能及源码分享)
TCP(Transmission Control Protocol)连接是互联网上最常用的一种面向连接、可靠的、基于字节流的传输层通信协议。建立TCP连接需要经过著名的“三次握手”过程: 1. SYN(同步序列编号):客户端发送一个SYN包给服务器,并进入SYN_SEND状态,等待服务器确认。 2. SYN-ACK:服务器收到SYN包后,回应一个SYN-ACK(SYN+ACKnowledgment)包,告诉客户端其接收到了请求,并同意建立连接,此时服务器进入SYN_RECV状态。 3. ACK(确认字符):客户端收到服务器的SYN-ACK包后,发送一个ACK包给服务器,确认收到了服务器的确
130 1
|
13天前
|
网络协议 Linux
云服务器内部端口占用,9090端口已经存在了,如何关闭,Linux查询端口,查看端口,端口查询,关闭端口写法-netstat -tuln,​fuser -k 3306/tcp​
云服务器内部端口占用,9090端口已经存在了,如何关闭,Linux查询端口,查看端口,端口查询,关闭端口写法-netstat -tuln,​fuser -k 3306/tcp​
|
13天前
|
运维 监控 大数据
部署-Linux01,后端开发,运维开发,大数据开发,测试开发,后端软件,大数据系统,运维监控,测试程序,网页服务都要在Linux中进行部署
部署-Linux01,后端开发,运维开发,大数据开发,测试开发,后端软件,大数据系统,运维监控,测试程序,网页服务都要在Linux中进行部署
|
17天前
|
NoSQL Linux 开发工具
【linux】在linux操作系统下快速熟悉开发环境并上手开发工具——体验不一样的开发之旅
【linux】在linux操作系统下快速熟悉开发环境并上手开发工具——体验不一样的开发之旅
|
21天前
|
移动开发 程序员 Linux
老程序员分享:linux驱动开发笔记_ioctl函数
老程序员分享:linux驱动开发笔记_ioctl函数