Linux驱动学习之第三个驱动程序(两个按键的驱动程序-读取按键值)

程序框架说明(和之前的LED驱动进行对比)

这个程序的框架与之前学习的第二个驱动程序(控制LED)的框架基本一致,第二个驱动程序的链接如下:
https://blog.csdn.net/wenhao_ir/article/details/144973219
所以如果前两这个LED驱动程序的框架掌握得很清楚了,这里驱动程序的框架就没啥好说的了。

不过二者还是有点不同,具体的不同如下:

①在控制LED的驱动程序中,下层的代码虽然被独立为一个文件,但并没有作为一个单独的模块,即没有初始函数和退出函数,而是和驱动的上层代码文件一起被编译成一个模块。本文中的下层驱动代码和上层驱动代码就是各自形成一个模块,虽然我觉得这样并不方便(相当于加载一个驱动还得加载两个模块,增加了操作的复杂性),但是既然官方提供的代码框架是这样,我就不改了,以后我就根据实际需要去选择是用LED驱动中的那种只有一个模块的结构还是这篇博文中被分别编译成两个模块的结构吧。

②在控制LED的驱动程序中,设备文件的建立是在驱动程序框架的上层文件的模块初始化函数中完成的,关键代码如下:
在这里插入图片描述
函数get_board_led_opr是在底层文件中定义的函数,相关关键代码如下:

static struct led_operations board_demo_led_opr = {
    .num  = 1,
    .init = board_demo_led_init,
    .ctl  = board_demo_led_ctl,
    .close = board_demo_led_close,
};

struct led_operations *get_board_led_opr(void)
{
    return &board_demo_led_opr;
}

而本文中的驱动程序的框架中,设备文件的创建和销毁是在驱动程序的上层文件中被封装成了两个函数,代码如下:

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, "swh_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));
	}
}

底层代码在初始化和退出时调用这两个在上层文件中的函数进行设备文件的创建和销毁,相关代码如下:

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

void board_imx6ull_button_drv_exit(void)
{
    unregister_button_operations();
}

可见,如果这两个文件被分别编译成两个模块,这里的加载顺序应该是要先加载上层的模块,再加载下层的模块。因为下层的模块需要调用上层模块的函数。

完整源代码

自定义头文件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 (*close) (int which);
};

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

#endif

驱动程序上层源代码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 int button_close (struct inode *inode, struct file *file)
{
	int minor = iminor(inode);
	p_button_opr->close(minor);
	return 0;
}


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

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, "swh_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, "swh_button", &button_fops);

	button_class = class_create(THIS_MODULE, "swh_button");
	if (IS_ERR(button_class))
		return -1;
	
	return 0;
}

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

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

驱动程序下层源代码board_100ask_imx6ull.c的内容

#include <linux/module.h>

#include <linux/fs.h>
#include <linux/io.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <asm/io.h>

#include "button_drv.h"

struct imx6ull_gpio {
    volatile unsigned int dr;       // 数据寄存器
    volatile unsigned int gdir;     // 数据方向寄存器
    volatile unsigned int psr;      // 状态寄存器
    volatile unsigned int icr1;     // 中断配置寄存器1
    volatile unsigned int icr2;     // 中断配置寄存器2
    volatile unsigned int imr;      // 中断屏蔽寄存器
    volatile unsigned int isr;      // 中断状态寄存器
    volatile unsigned int edge_sel; // 边沿选择寄存器
};

/* enable GPIO4 */
static volatile unsigned int *CCM_CCGR3; 

/* enable GPIO5 */
static volatile unsigned int *CCM_CCGR1; 

/* set GPIO5_IO03 as GPIO */
static volatile unsigned int *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER1;

/* set GPIO4_IO14 as GPIO */
static volatile unsigned int *IOMUXC_SW_MUX_CTL_PAD_NAND_CE1_B;

static struct imx6ull_gpio *gpio4;
static struct imx6ull_gpio *gpio5;

static void board_imx6ull_button_init(int which) /* 初始化button, which-哪个button */
{
    if (which == 0) {
        /* 1. 检查并映射需要的寄存器 */
        if (!CCM_CCGR1) {
            CCM_CCGR1 = ioremap(0x20C406C, 4);
        }
        if (!IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER1) {
            IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER1 = ioremap(0x229000C, 4);
        }
        if (!gpio5) {
            gpio5 = ioremap(0x20AC000, sizeof(struct imx6ull_gpio));
        }

        /* 2. enable GPIO5 Clock
         * CG15, b[31:30] = 0b11
         */
        *CCM_CCGR1 |= (3 << 30);

        /* 3. set GPIO5_IO01 as GPIO 
         * MUX_MODE, b[3:0] = 0b101
         */
        *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER1 = 5;

        /* 4. set GPIO5_IO01 as input 
         * GPIO5 GDIR, b[1] = 0b0
         */
        gpio5->gdir &= ~(1 << 1);
    } else if (which == 1) {
        /* 1. 检查并映射需要的寄存器 */
        if (!CCM_CCGR3) {
            CCM_CCGR3 = ioremap(0x20C4074, 4);
        }
        if (!IOMUXC_SW_MUX_CTL_PAD_NAND_CE1_B) {
            IOMUXC_SW_MUX_CTL_PAD_NAND_CE1_B = ioremap(0x20E01B0, 4);
        }
        if (!gpio4) {
            gpio4 = ioremap(0x020A8000, sizeof(struct imx6ull_gpio));
        }

        /* 2. enable GPIO4 Clock
         * CG6, b[13:12] = 0b11
         */
        *CCM_CCGR3 |= (3 << 12);

        /* 3. set GPIO4_IO14 as GPIO 
         * MUX_MODE, b[3:0] = 0b101
         */
        *IOMUXC_SW_MUX_CTL_PAD_NAND_CE1_B = 5;

        /* 4. set GPIO4_IO14 as input 
         * GPIO4 GDIR, b[14] = 0b0
         */
        gpio4->gdir &= ~(1 << 14);
    }
}


static int board_imx6ull_button_read (int which) /* 读button, which-哪个 */
{
    //printk("%s %s line %d, button %d, 0x%x\n", __FILE__, __FUNCTION__, __LINE__, which, *GPIO1_DATAIN);
    if (which == 0)
        return (gpio5->psr & (1<<1)) ? 1 : 0;
    else
        return (gpio4->psr & (1<<14)) ? 1 : 0;
}


static void board_imx6ull_button_close(int which) /* 关闭button, which-哪个button */
{
    if (which == 0) {
        /* 解除 GPIO5 相关寄存器的映射 */
        if (IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER1) {
            iounmap(IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER1);
            IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER1 = NULL;
        }
        if (gpio5) {
            iounmap(gpio5);
            gpio5 = NULL;
        }
        if (CCM_CCGR1) {
            iounmap(CCM_CCGR1);
            CCM_CCGR1 = NULL;
        }
    } else if (which == 1) {
        /* 解除 GPIO4 相关寄存器的映射 */
        if (IOMUXC_SW_MUX_CTL_PAD_NAND_CE1_B) {
            iounmap(IOMUXC_SW_MUX_CTL_PAD_NAND_CE1_B);
            IOMUXC_SW_MUX_CTL_PAD_NAND_CE1_B = NULL;
        }
        if (gpio4) {
            iounmap(gpio4);
            gpio4 = NULL;
        }
        if (CCM_CCGR3) {
            iounmap(CCM_CCGR3);
            CCM_CCGR3 = NULL;
        }
    }
}
    
static struct button_operations my_buttons_ops = {
    .count = 2,
    .init = board_imx6ull_button_init,
    .read = board_imx6ull_button_read,
    .close = board_imx6ull_button_close,
};

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

void board_imx6ull_button_drv_exit(void)
{
    unregister_button_operations();
}

module_init(board_imx6ull_button_drv_init);
module_exit(board_imx6ull_button_drv_exit);

MODULE_LICENSE("GPL");

测试程序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;
}



驱动程序框架和逻辑没有一点问题,重点还是对硬件的操作

驱动程序的框架的逻辑通过之前几个驱动程序的学习,已经一点问题没有了,所以本篇博文的重点还是在如何对硬件进行操作。

驱动程序底层文件中的设备打开函数board_imx6ull_button_init分析

注意:这个函数已经被我优化得与官方的有较大不同了

注意:函数board_imx6ull_button_init已经被我优化得与官方的有较大不同了。比如官方并没有分区是哪个按键,而是统一进行了寄存器的映射,我的优化就进行了区分。再有,官方在映射操作前只判断了CCM_CCGR1寄存器指针是否为空值,而我是对各个寄存器的指针都检查了。

函数board_imx6ull_button_init的源代码

代码如下:

static void board_imx6ull_button_init(int which) /* 初始化button, which-哪个button */
{
    if (which == 0) {
        /* 1. 检查并映射需要的寄存器 */
        if (!CCM_CCGR1) {
            CCM_CCGR1 = ioremap(0x20C406C, 4);
        }
        if (!IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER1) {
            IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER1 = ioremap(0x229000C, 4);
        }
        if (!gpio5) {
            gpio5 = ioremap(0x20AC000, sizeof(struct imx6ull_gpio));
        }

        /* 2. enable GPIO5 Clock
         * CG15, b[31:30] = 0b11
         */
        *CCM_CCGR1 |= (3 << 30);

        /* 3. set GPIO5_IO01 as GPIO 
         * MUX_MODE, b[3:0] = 0b101
         */
        *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER1 = 5;

        /* 4. set GPIO5_IO01 as input 
         * GPIO5 GDIR, b[1] = 0b0
         */
        gpio5->gdir &= ~(1 << 1);
    } else if (which == 1) {
        /* 1. 检查并映射需要的寄存器 */
        if (!CCM_CCGR3) {
            CCM_CCGR3 = ioremap(0x20C4074, 4);
        }
        if (!IOMUXC_SW_MUX_CTL_PAD_NAND_CE1_B) {
            IOMUXC_SW_MUX_CTL_PAD_NAND_CE1_B = ioremap(0x20E01B0, 4);
        }
        if (!gpio4) {
            gpio4 = ioremap(0x020A8000, sizeof(struct imx6ull_gpio));
        }

        /* 2. enable GPIO4 Clock
         * CG6, b[13:12] = 0b11
         */
        *CCM_CCGR3 |= (3 << 12);

        /* 3. set GPIO4_IO14 as GPIO 
         * MUX_MODE, b[3:0] = 0b101
         */
        *IOMUXC_SW_MUX_CTL_PAD_NAND_CE1_B = 5;

        /* 4. set GPIO4_IO14 as input 
         * GPIO4 GDIR, b[14] = 0b0
         */
        gpio4->gdir &= ~(1 << 14);
    }
}

根据原理图确定要对哪个GPIO口进行读操作

在这里,我们需要板上的KEY1和KEY2进行操作,相关原理图如下:
原理图百度网盘链接:https://pan.baidu.com/s/1SWuF_GMdPLAYOUeDDPH9qw?pwd=ame9
在这里插入图片描述
在这里插入图片描述
从原理图中我们可以看出,我们需要对GPIO5_IO01和GPIO4_IO14进行操作。相关寄存器的初始化的操作步骤与博文 https://blog.csdn.net/wenhao_ir/article/details/144973219中没啥本质区别,代码中注释也写得很清楚,这里就不赘述了。

这里需要分析一下下面这些代码。

语句 if (!CCM_CCGR1)及类似代码的作用

在这里插入图片描述
在代码中,if (!CCM_CCGR1) 的作用是检查指针 CCM_CCGR1 是否为 NULL,即是否未被初始化(映射到物理地址)。如果 CCM_CCGR1 的值为 NULL,表示 ioremap() 尚未被调用,需要执行映射操作。以下是它的意义与上下文解释:

上下文

  1. CCM_CCGR1 是一个全局变量,指向一个特定的寄存器地址,用于控制 IMX6ULL 的 GPIO 时钟。
  2. 在嵌入式开发中,直接访问硬件寄存器的地址是通过 ioremap 函数完成的。ioremap 将物理地址映射到内核虚拟地址空间中,使得驱动程序可以通过指针间接访问硬件寄存器。
  3. CCM_CCGR1 在程序初始状态下是 NULL,因为它是一个全局变量,默认未被初始化。

if (!CCM_CCGR1) 的作用

  • 检查初始化状态:
    • 如果 CCM_CCGR1NULL,表示映射还未完成,需要调用 ioremap 将对应物理地址映射到内核虚拟地址空间中。
    • 避免重复映射:一旦 CCM_CCGR1 已经成功映射,就不需要再次调用 ioremap。确保寄存器地址只映射一次,避免重复的 ioremap 操作,提高代码的安全性和效率。

其它几行类似的代码,比如下面这些,也是相同的作用,这里就不再赘述。

if (!CCM_CCGR3) 
if (!IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER1)
if (!IOMUXC_SW_MUX_CTL_PAD_NAND_CE1_B)
if (!iomux)
if (!gpio4)
if (!gpio5)

如何利用结构体imx6ull_gpio实现对GPIO的各个寄存器的访问?

试分析下面有关联的三段代码是如何实现gpio5->gdir是指向我们需要操作的GPIO方向寄存器的?

struct imx6ull_gpio {
    volatile unsigned int dr;
    volatile unsigned int gdir;
    volatile unsigned int psr;
    volatile unsigned int icr1;
    volatile unsigned int icr2;
    volatile unsigned int imr;
    volatile unsigned int isr;
    volatile unsigned int edge_sel;
};

static struct imx6ull_gpio *gpio5;

gpio5->gdir &= ~(1 << 1);

以下是这三段代码如何实现 gpio5->gdir 指向目标寄存器的详细分析:


1. struct imx6ull_gpio 结构体的定义

struct imx6ull_gpio {
    volatile unsigned int dr;       // 数据寄存器
    volatile unsigned int gdir;     // 数据方向寄存器
    volatile unsigned int psr;      // 状态寄存器
    volatile unsigned int icr1;     // 中断配置寄存器1
    volatile unsigned int icr2;     // 中断配置寄存器2
    volatile unsigned int imr;      // 中断屏蔽寄存器
    volatile unsigned int isr;      // 中断状态寄存器
    volatile unsigned int edge_sel; // 边沿选择寄存器
};
  • 这是一个结构体,用于描述 IMX6ULL 芯片中 GPIO 控制器的寄存器布局。
  • 每个字段表示某个寄存器,字段的顺序和寄存器在硬件地址中的排列顺序一致。
  • 偏移量:
    • gdir 是第 2 个字段,因为IMX6ULL是32位的处理器,所以其对应的unsigned int类型占用4个字节,所以其偏移量为 4 字节(因为 dr 是第 1 个字段,占 4 字节)。

2. gpio5 的定义和映射

static struct imx6ull_gpio *gpio5;
  • gpio5 是一个指针,类型为 struct imx6ull_gpio *
  • 它是用来指向 GPIO5 控制器寄存器的虚拟地址。

3. 通过 ioremap 实现寄存器映射

gpio5 = ioremap(0x20AC000, sizeof(struct imx6ull_gpio));
  • ioremap 功能:

    • 将物理地址 0x20AC000 对应的寄存器区域映射到内核虚拟地址空间。
    • 返回的虚拟地址赋值给 gpio5,让 gpio5 指向寄存器区域的起始地址。
  • sizeof(struct imx6ull_gpio)

    • 指定了映射的大小,等于结构体的总大小,即包含所有相关寄存器。
  • 物理地址 0x20AC000

    • 根据芯片文档,这是 GPIO5 控制器寄存器组的基地址。

4. 访问 gpio5->gdir【GPIO5的方向寄存器】

gpio5->gdir &= ~(1 << 1);
  • 偏移量计算:

    • gpio5 的值是虚拟地址,指向寄存器组的起始地址。
    • gpio5->gdir 访问的是偏移量为 4 字节 的地址【因为IMX6ULL是32位的处理器,所以其对应的unsigned int类型占用4个字节】:
      • gpio5 + 4 指向物理地址 0x20AC004(即 GPIO5 数据方向寄存器的地址)。
  • 操作解析:

    • gpio5->gdir &= ~(1 << 1) 等价于:
      • 读取 gpio5->gdir(地址 0x20AC004)的当前值。
      • 对其进行按位与操作,清除第 1 位(将其置为 0)。
      • 写回处理后的值到 gpio5->gdir,完成对寄存器的修改。

总结:如何实现了指向GPIO5的方向寄存器的?

  1. 定义 struct imx6ull_gpio 结构体,与硬件寄存器布局一致。
  2. 使用 ioremap 将 GPIO5 控制器的物理地址 0x20AC000 映射到虚拟地址。
  3. 通过 gpio5->gdir 访问虚拟地址中偏移量为 4 的位置,即目标寄存器。
  4. 对目标寄存器进行读写操作,从而完成 GPIO 数据方向的配置。

这样的设计屏蔽了直接操作物理地址的复杂性,提供了一个简洁的抽象层,更易于开发和维护。

为什么不分析别的语句了?

整个驱动程序就只有上面这个board_imx6ull_button_init要难理解点,其它的在之前的驱动程序学习中,都已经了解得比较清楚了,就不再赘述了。

Makefile文件的编写

# 使用不同的Linux内核时, 一定要修改KERN_DIR,KERN_DIR代表已经配置、编译好的Linux源码的根目录

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_100ask_imx6ull.o

这个没啥好说的…

交叉编译

工程目录复制到Ubuntu中…
在这里插入图片描述
然后make
在这里插入图片描述
在这里插入图片描述
把生成的ko文件和ELF可执行文件复制到网络系统文件中以备用…
在这里插入图片描述

上板测试

打开串口终端→打开开发板→挂载网络文件系统

mount -t nfs -o nolock,vers=3 192.168.5.11:/home/book/nfs_rootfs /mnt

进入相应的目录:

cd /mnt/read_button_values_drivers/

加载驱动程序模块(注意有顺序要求)

先加载button_drv.ko

insmod button_drv.ko

在这里插入图片描述
再加载board_100ask_imx6ull.ko

insmod board_100ask_imx6ull.ko

在这里插入图片描述
这里要注意,为什么加载button_drv.ko提示“loading out-of-tree module taints kernel”,而加载board_100ask_imx6ull.ko时没有提示?
原因见:https://blog.csdn.net/wenhao_ir/article/details/145109760

看下相应的设备文件生成没有

ls /dev/

在这里插入图片描述

运行测试程序

先测试KEY1

在这里插入图片描述
在这里插入图片描述
我们先不按下按键运行下面的命令,从原理图来说,此时应该读到的值是1

./button_test /dev/swh_button0

在这里插入图片描述
果然如我们所料是1

我们再按下按键运行下面的命令,从原理图来说,此时应该读到的值是0

./button_test /dev/swh_button0

在这里插入图片描述

然后测试KEY2

在这里插入图片描述
在这里插入图片描述

我们先不按下按键运行下面的命令,从原理图来说,此时应该读到的值是1

./button_test /dev/swh_button1

在这里插入图片描述
果然如我们所料是1

我们再按下按键运行下面的命令,从原理图来说,此时应该读到的值是0

./button_test /dev/swh_button1

在这里插入图片描述
果然如我们所料是0

卸载模块

先查看下模块的依赖关系:

lsmod

在这里插入图片描述
所以应该先卸载board_100ask_imx6ull,再卸载button_drv

rmmod board_100ask_imx6ull

在这里插入图片描述
这个模块卸载后,根据相关源代码,设备文件应该被删除了,我们看下设备文件还有没有:

ls /dev/

在这里插入图片描述
果然如我所料,设备文件/dev/swh_button0/dev/swh_button1都不存在了。

卸载模块button_drv前,设备类的目录和驱动程序的主设备号应该都还在,我们查看一下:

ls /sys/class/

在这里插入图片描述
果然设备类的目录还在。

cat /proc/devices

在这里插入图片描述
果然驱动程序还在,而且主设备号是245。

接下来卸载button_drv模块:

rmmod button_drv

在这里插入图片描述
设备类不在了:

ls /sys/class/

在这里插入图片描述
驱动程序和主设备号也不在了:

cat /proc/devices

在这里插入图片描述
至此,卸载模块部分也测试完毕。

附完整工程文件

https://pan.baidu.com/s/1wuARJoTiR4oM3dBkOeVT0g?pwd=g74j

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/953412.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

KMP前缀表 ≈ find() 函数——28.找出字符串中第一个匹配项的下标【力扣】

class Solution { public: //得到前缀表void getNext(int *next,string needle){int j0;for(int i1;i<needle.size();i){while(j>0 && needle[j]!needle[i]) jnext[j-1];//**j>0**>j0是出口if(needle[i]needle[j]) j;next[i]j;//若写入if中&#xff0c;则该…

vulnhub靶场【IA系列】之Tornado

前言 靶机&#xff1a;IA-Tornado&#xff0c;IP地址为192.168.10.11 攻击&#xff1a;kali&#xff0c;IP地址为192.168.10.2 都采用虚拟机&#xff0c;网卡为桥接模式 本文所用靶场、kali镜像以及相关工具&#xff0c;我放置在网盘中&#xff0c;可以复制后面链接查看 htt…

【优选算法篇】:模拟算法的力量--解决复杂问题的新视角

✨感谢您阅读本篇文章&#xff0c;文章内容是个人学习笔记的整理&#xff0c;如果哪里有误的话还请您指正噢✨ ✨ 个人主页&#xff1a;余辉zmh–CSDN博客 ✨ 文章所属专栏&#xff1a;优选算法篇–CSDN博客 文章目录 一.模拟算法二.例题1.替换所有的问号2.提莫攻击3.外观数列4…

云集电商:数据库的分布式升级实践|OceanBase案例

电商行业对数据库有哪些需求 云集电商作为一家传统电商企业&#xff0c;业务涵盖了美妆个护、服饰、水果生鲜、健康保健等多个领域&#xff0c;在创立四年后在纳斯达克上市&#xff08;股票代码&#xff1a;YJ&#xff09;。与京东、淘宝、拼多多等电商平台不同&#xff0c;云…

Kibana操作ES基础

废话少说&#xff0c;开干&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;截图更清晰&#xff0c;复制在下面 #库操作#创建索引【相当于数据库的库】 PUT /first_index#获…

AI大模型赋能!移远通信打造具有“超能力”的AI智能玩具解决方案

随着无线通信、先进算法以及AI大模型等前沿技术的蓬勃发展&#xff0c;许多玩具已经从简单的互动设备进化为集教育、陪伴和娱乐功能于一身的AI智能玩具&#xff0c;在儿童群体中日渐风靡。不仅如此&#xff0c;因其能提供满满的情绪价值&#xff0c;在成年人和老年人市场中也展…

从 SQL 语句到数据库操作

1. SQL 语句分类 数据定义语言 DDL &#xff1a; 用于定义或修改数据库中的结构&#xff0c;如&#xff1a;创建、修改、删除数据库对象。create、drop alter 数据操作语言 DML &#xff1a; 用于添加、删除、更新数据库中的数据。select、insert alter、drop 数据控制语言 D…

解锁 JMeter 的 ForEach Controller 高效测试秘籍

各位小伙伴们&#xff0c;今天咱就来唠唠 JMeter 里超厉害的 “宝藏工具”——ForEach Controller&#xff0c;它可是能帮咱们在性能测试的江湖里 “大杀四方” 哦&#xff01; 一、ForEach Controller 是啥 “神器” 想象一下&#xff0c;你手头有一串神秘钥匙&#xff0c;每…

【已解决】【记录】2AI大模型web UI使用tips 本地

docker desktop使用 互动 如果需要发送网页链接&#xff0c;就在链接上加上【#】号 如果要上传文件就点击这个➕号 中文回复 命令它只用中文回复&#xff0c;在右上角打开【对话高级设置】 输入提示词&#xff08;提示词使用英文会更好&#xff09; Must reply to the us…

MySQL批量修改数据表编码及字符集为utf8mb4

​​​​​​MySQL批量修改数据表编码及字符集为utf8mb4 utf8mb4编码是utf8编码的超集&#xff0c;兼容utf8&#xff0c;并且能存储4字节的表情字符。 采用utf8mb4编码的好处是&#xff1a;存储与获取数据的时候&#xff0c;不用再考虑表情字符的编码与解码问题。 更改数据库…

基于Spring Boot的房屋租赁系统源码(java+vue+mysql+文档)

项目简介 房屋租赁系统实现了以下功能&#xff1a; 基于Spring Boot的房屋租赁系统的主要使用者管理员可登录系统后台&#xff0c;登录后可对系统进行全面管理&#xff0c;包括个人中心、公告信息管理、租客管理、户主管理、房屋信息管理、看房申请管理、租赁合同管理、收租信…

Leetcode - 147双周赛

目录 一、3407. 子字符串匹配模式二、3408. 设计任务管理器三、3409. 最长相邻绝对差递减子序列四、3410. 删除所有值为某个元素后的最大子数组和 一、3407. 子字符串匹配模式 题目链接 字符串匹配问题&#xff0c;把字符串 p 分成两段 、&#xff0c;i 是 ’ * ’ 的下标&am…

数据预测2025年AI面试市场增幅超500%!

近年来&#xff0c;随着人工智能技术的迅猛发展&#xff0c;AI在各行各业的应用逐渐广泛&#xff0c;其中企业招聘领域也不例外。最新的数据显示&#xff0c;2025年AI面试市场将迎来前所未有的增长&#xff0c;预计增幅将超过500%。这一预测不仅揭示了AI技术在招聘领域的应用潜…

浅谈云计算08 | 基本云架构

浅谈基本云架构 一、负载分布架构二、资源池架构三、动态可扩展架构四、弹性资源容量架构五、服务负载均衡架构六、云爆发架构七、弹性磁盘供给架构八、冗余存储架构 在当今数字化时代&#xff0c;云计算已成为企业发展的核心驱动力&#xff0c;而其背后的一系列关键架构则是支…

【专题】2025年节日营销趋势洞察报告汇总PDF洞察(附原数据表)

原文链接&#xff1a; https://tecdat.cn/?p38813 在当今复杂多变且竞争激烈的消费市场环境下&#xff0c;节日营销已成为企业获取市场份额、提升品牌影响力的关键战略时机。我们深知深入洞察节日营销趋势对于企业决策的重要性。 本报告汇总基于对 2024 年多个关键消费节点及…

【Linux系统】权限位(mode bits)

这张图是使用结构体 struct stat 中的 st_mode 字段时画的&#xff0c;获取表示文件的类型和权限&#xff0c;它是典型的 POSIX 系统调用&#xff08;如 stat() 和 fstat()&#xff09;返回的 struct stat 结构的一部分&#xff0c;用于描述文件的元数据。 在 Linux 和 Unix 系…

无源器件-电容

电容器件的参数 基本概念由中学大学物理或电路分析内容获得&#xff0c;此处不做过多分析。 电容的产量占全球电子元器件产品的40%以上。 单位&#xff1a;法拉 F&#xff1b;1F10^6uF&#xff1b;电路中常见的104电容就是10*10^4pF100nF0.1uF C为电容&#xff0c;Rp为绝缘电…

Go语言之路————go环境的初始化

Go语言之路————go环境的初始化 前言一、Go的安装二、环境配置三、初始化一个新项目四、常用的一些指令 前言 我是一名多年Java开发人员&#xff0c;因为工作需要现在要学习go语言&#xff0c;Go语言之路是一个系列&#xff0c;记录着我从0开始接触Go&#xff0c;到后面能正…

数据结构与算法之链表: LeetCode 234. 回文链表 (Ts版)

回文链表 https://leetcode.cn/problems/palindrome-linked-list/description/ 描述 给你一个单链表的头节点 head &#xff0c;请你判断该链表是否为回文链表如果是&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 示例 1 输入&#xff1a;head [1,2,2,1]…

Hive4.0.1集群安装部署(Hadoop版本为3.3.6)(详细教程)

前置环境 ​​​Linux环境Zookeeper集群安装&#xff08;详细教程&#xff09;-CSDN博客 Hadoop HA高可用集群3.3.6搭建&#xff08;详细教程&#xff09;-CSDN博客 MySQL8.0.40离线安装&#xff08;详细教程&#xff09;_mysql 8.0.40 ftp-CSDN博客 Hadoop3.3.6官网下载链接地…