ARM——驱动——inmod加载内核模块

在上一篇文章的代码上添加出错处理

#include <linux/init.h>       // 包含初始化宏和函数  
#include <linux/kernel.h>     // 包含内核函数和变量  
#include <linux/fs.h>         // 包含文件操作的结构和函数  
#include <linux/kdev_t.h>     // 旧的设备号定义,现在通常包含在<linux/kdev_t.h>或不需要直接包含  
#include <linux/cdev.h>       // 包含字符设备操作的结构和函数  
#include <linux/module.h>     // 包含模块操作的宏和函数  
  
// 定义主设备号和次设备号  
#define MAJOR_NUM 255  
#define MINOR_NUM 0  
// 定义设备名称  
#define DEV_NAME "demo"  
// 定义要注册的设备数量(这里为1,因为次设备号固定为0)  
#define DEV_NUM 1  
  
// 文件打开操作  
static int open (struct inode * inode, struct file * file)  
{  
    printk("demo open ...\n");  
    return 0; // 成功返回0  
}  
  
// 文件读取操作  
static ssize_t read (struct file * file, char __user * buf, size_t len, loff_t * offset)  
{  
    printk("demo read ...\n");  
    return 0; // 这里仅作为示例,实际应该返回读取的字节数  
}  
  
// 文件写入操作  
static ssize_t write (struct file * file, const char __user * buf, size_t len, loff_t * offset)  
{  
    printk("demo write ...\n");  
    return 0; // 这里仅作为示例,实际应该返回写入的字节数  
}  
  
// 文件关闭操作,注意这里使用了.release而不是.close,因为.close在Linux 2.6.33之后被弃用  
static int close (struct inode * inode, struct file * file)  
{  
    printk("demo close ...\n");  
    return 0; // 成功返回0  
}  
  
// 文件操作结构  
static struct file_operations fops =   
{  
    .owner = THIS_MODULE,  // 指向拥有该结构的模块  
    .open = open,          // 打开操作  
    .read = read,          // 读取操作  
    .write = write,        // 写入操作  
    .release = close       // 关闭操作(注意使用.release)  
};  
  
// 字符设备结构体  
static struct cdev cdev;  
// 设备号  
static dev_t dev;  
  
// 模块初始化函数  
static int __init demo_init(void)  
{  
    int ret = 0;  
    dev = MKDEV(MAJOR_NUM, MINOR_NUM); // 创建设备号  
  
    cdev_init(&cdev, &fops); // 初始化字符设备  
  
    // 添加字符设备到系统  
    ret = cdev_add(&cdev, dev, DEV_NUM);  
    if(ret < 0)  
        goto err_cdev_add;  
  
    // 注册设备号到系统,但这一步实际上是多余的,因为我们已经手动指定了主设备号  
    // 并且通常在调用cdev_add之前不需要调用register_chrdev_region  
    // 除非你不确定主设备号是否已经被占用,或者你想要动态分配一个主设备号  
    ret = register_chrdev_region(dev, DEV_NUM, DEV_NAME);  
    if(ret < 0)  
        goto err_register_chrdev_region;  
  
    printk("demo_init  ###################\n");  
  
    return ret; // 这里应该返回0表示成功,但由于之前的错误处理,这里实际上不会执行  
  
err_cdev_add:  
    cdev_del(&cdev); // 如果cdev_add失败,则删除字符设备  
    printk("demo cdev_add failed\n");  
    return ret; // 返回错误码  
  
err_register_chrdev_region:  
    unregister_chrdev_region(dev, DEV_NUM); // 如果register_chrdev_region失败,则注销设备号  
    cdev_del(&cdev); // 删除字符设备  
    printk("demo register_chrdev_region failed\n"); // 注意这里修正了打印信息  
    return ret; // 返回错误码  
}  
  
// 模块清理函数  
static void __exit demo_exit(void)  
{  
    unregister_chrdev_region(dev, DEV_NUM); // 注销设备号  
    cdev_del(&cdev); // 删除字符设备  
    printk("demo_exit  ################

 一、驱动模块的加载

静态 编译到内核
这种是必须要加载到内核中的驱动,或者特别特别常用的驱动,只要开机就启动。

动态 编译到模块中

开机以后自己安装的驱动 开机后加载,试一下功能,不行就卸载掉,省区每次都要开机的情况。

1、在menu中将选项选成M(M即编译成一个模块)

但如果想要编译成模块 ,
之前不能<*>即,原来的内核中就不能加载过,
如果加载过在使用模块加载,会导致重复加载。

所以:需要先不选中再把uImage复制到根文件系统中。

make出一个uImage

再拷贝到根目录文件中。

2、选中<M>后,make modules  对应模块选项配置位M

将这个文件考到根目录文件中(也可以直接开发板挂载)

3、加载驱动模块

(1)用命令

insmod demo.ko    //动态加载驱动模块

insmod命令用于将指定的模块文件(通常是.ko文件)加载到Linux内核中。这使得用户可以在不重启系统的情况下,向内核添加新的功能或驱动。

关于GPL

GPL许可证的主要目标是保证软件对用户是自由的,即用户可以自由地运行、复制、分发、研究、改变和改进软件。同时,它要求软件必须以源代码的形式发布,以便其他人可以在此基础上进行修改和再发布。

任何遵循GPL许可发布的软件,其源代码都是开放的,任何人在任何时间都可以获取、修改和重新发布,但必须保证修改后的软件也遵循GPL许可。

GPL许可证有两个版本:GPL v2和G

删除原先的mod再insmod即可

(2)查看已经动态加载的驱动模块 

lsmod 

(3)卸载驱动模块

rmmod

4、设置设备节点,运行程序

运行app程序即再/home/Linux/tftpboot中写一个app程序

运行成功。

二、写一个led驱动加载模块

(1)ioremap

void __iomem *ioremap(phys_addr_t offset, unsigned long size);

ioremap 函数是用来将物理内存地址映射到内核的虚拟地址空间上,使得内核可以像访问普通内存一样访问这部分物理内存。

用这个函数在    static int __init led_init(void)  将物理地址映射到虚拟地址上,使内核可以访问地址。

(2)iounmap解除映射函数

void iounmap(void __iomem *addr);

 

在Linux内核中,ioremap 函数是用来将物理内存地址映射到内核的虚拟地址空间上,使得内核可以像访问普通内存一样访问这部分物理内存。这在设备驱动开发中尤其常见,因为许多硬件设备都需要通过特定的物理地址来访问其寄存器或内存区域

在 static void __exit led_exit(void)中用这个函数来解除寄存器地址与内核的映射

(3)声明寄存器的地址中添加寄存器的地址

(5)在/drivers/char/Makefile 中加入  命令

(5)/drivers/char/Kconfig中增加选项

(6)进入开发板按照一中运行。

(7)源代码

驱动程序

led.c

#include<linux/init.h>
#include<linux/kernel.h>
#include<linux/fs.h>
#include<linux/kdev_t.h>
#include<linux/cdev.h>
#include<linux/module.h>
#include <linux/io.h>


#define MAJOR_NUM 256
#define MINOR_NUM 0
#define DEV_NAME "led"
#define DEV_NUM 1

#define GPBCON 0x56000010
#define GPBDAT 0x56000014

static volatile unsigned long * gpbcon;
static volatile unsigned long * gpbdat;

void init_led(void)						
{

	// 配置GPB5引脚功能为输出
	*gpbcon &= ~(3 << 10);
	*gpbcon |= (1 << 10);

	// 将GPB5引脚电平置高
	*gpbdat |= (1 << 5);
}

void led_on(void)
{
	// 将GPB5引脚电平置低
	*gpbdat &= ~(1 << 5);
printk("led_on!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n");

}

void led_off(void)
{
	// 将GPB5引脚电平置高
	*gpbdat |= (1 << 5);
printk("led_off!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n");
}


static int open(struct inode *inode,struct file *file)
{
	init_led();
	printk("led open..\n");
	return 0;
}

static ssize_t read (struct file * file, char __user * buf, size_t len, loff_t * offset)
{
	printk("led read ...\n");
	return 0;
}

static ssize_t write (struct file * file, const char __user * buf, size_t len, loff_t * offset)
{

	if(strcmp(buf,"led_on")==0)
	{
		led_on();
	}
	if(strcmp(buf,"led_off")==0)
	{
		led_off();
	}

	printk("led write ...\n");
	return 0;
}

static int close (struct inode * inode, struct file * file)
{
	printk("led close ...\n");
	return 0;
}

static struct file_operations fops=
{
	.owner = THIS_MODULE,
	.open  =  open,
	.read  =  read,
	.write =  write,
	.release = close
};

static struct cdev cdev;
static dev_t dev;

static int __init led_init(void)
{

	int ret = 0;
	dev=MKDEV(MAJOR_NUM,MINOR_NUM);

	cdev_init(&cdev,&fops);
	
	ret =cdev_add(&cdev,dev,DEV_NUM);
	if(ret<0)
	{
		goto err_cdev_add;
	}
	ret = register_chrdev_region(dev,DEV_NUM,DEV_NAME);
	if(ret<0)
	{
		goto err_register_chrdev_region;
	}
	printk("led_init ##########################\n");

	
	gpbcon=ioremap(GPBCON,sizeof(*gpbcon));
	gpbdat=ioremap(GPBDAT,sizeof(*gpbdat));
	
	return ret;

err_cdev_add:
	cdev_del(&cdev);
	printk("led cdev_add failed\n");
	return ret;
err_register_chrdev_region:
	unregister_chrdev_region(dev,DEV_NUM);
	cdev_del(&cdev);
	printk("led resgister_chrdev_region\n");
	return ret;
}


static void __exit led_exit(void)
{

	iounmap(gpbcon);
	iounmap(gpbdat);

	unregister_chrdev_region(dev,DEV_NUM);
	cdev_del(&cdev);
	printk("led_exit #########################\n");
}



module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");


led_app.c

#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main(int argc, const char *argv[]) {
    int fd = open("/dev/led", O_RDWR);
    if (fd < 0) {
        perror("open led");
        return -1;
    }

	while(1)
	{
		write(fd,"led_on",strlen("led_on"));
			sleep(2);

		write(fd,"led_off",strlen("led_off"));
			sleep(2);


	}
    close(fd);
    return 0;
}

(7)其他注意事项!!!!

但这里不能用strcpy和memcpy,因为是从内村空间往内核空间拷贝函数

用一个函数   copy_from_user()

copy_from_user() 是 Linux 内核中用于从用户空间安全地复制数据到内核空间的一个函数。这个函数是内核提供的一种机制,用于在用户程序和内核之间传递数据,但确保了内核在访问用户空间内存时不会引发错误(如访问违规或段错误)。

函数原型

unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);

  • to:指向内核空间中的缓冲区的指针,数据将被复制到这个缓冲区。
  • from:指向用户空间中的数据的指针。注意,这个指针被标记为 __user,这是一个特殊的类型,用于指示该指针指向用户空间。
  • n:要复制的字节数。

这里的字节数不能乱写,所以定义一个len_cp看谁小就拷贝谁,保证谁的空间都不会超过

修正好的源代码如下

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/module.h>
#include <asm/io.h>
#include <asm/string.h>
#include <asm/uaccess.h>

#define MAJOR_NUM 253
#define MINOR_NUM 0
#define DEV_NAME "led"
#define DEV_NUM 1
#define GPBCON 0x56000010
#define GPBDAT 0x56000014

static volatile unsigned long * gpbcon;
static volatile unsigned long * gpbdat;

static void init_led(void)						
{
	// 配置GPB5引脚功能为输出
	*gpbcon &= ~(3 << 10);
	*gpbcon |= (1 << 10);

	// 将GPB5引脚电平置高
	*gpbdat |= (1 << 5);
}

static void led_on(void)
{
	// 将GPB5引脚电平置低
	*gpbdat &= ~(1 << 5);
}

static void led_off(void)
{
	// 将GPB5引脚电平置高
	*gpbdat |= (1 << 5);
}

static int open (struct inode * inode, struct file * file)
{
	init_led();
	printk("led open ...\n");
	return 0;
}

static ssize_t read (struct file * file, char __user * buf, size_t len, loff_t * offset)
{
	//copy_to_user(buf, data, len);
	printk("led read ...\n");
	return 0;
}

static ssize_t write (struct file * file, const char __user * buf, size_t len, loff_t * offset)
{
	unsigned char data[12] = {0};
	size_t len_cp = sizeof(data) < len ? sizeof(data) : len;
	copy_from_user(data, buf, len_cp);

	if(!strcmp(data, "ledon"))
		led_on();
	else if(!strcmp(data, "ledoff"))
		led_off();
	else
		 return -1;

	printk("led write ...\n");
	return len_cp;
}

static int close (struct inode * inode, struct file * file)
{
	printk("led close ...\n");
	return 0;
}

static struct file_operations fops = 
{
	.owner = THIS_MODULE,
	.open = open,
	.read = read,
	.write = write,
	.release = close
};
static struct cdev cdev;
static dev_t dev;

static int __init led_init(void)
{
	int ret = 0;
	dev = MKDEV(MAJOR_NUM, MINOR_NUM);

	cdev_init(&cdev, &fops);

	ret = cdev_add(&cdev, dev, DEV_NUM);
	if(ret < 0)
		goto err_cdev_add;

	ret = register_chrdev_region(dev, DEV_NUM, DEV_NAME);
	if(ret < 0)
		goto err_register_chrdev_region;

	gpbcon = ioremap(GPBCON, sizeof(*gpbcon));
	gpbdat = ioremap(GPBDAT, sizeof(*gpbdat));

	printk("led_init  ...\n");

	return ret;

err_cdev_add:
	cdev_del(&cdev);
	printk("led cdev_add failed\n");
	return ret;

err_register_chrdev_region:
	unregister_chrdev_region(dev, DEV_NUM);
	cdev_del(&cdev);
	printk("led register_chrdev_region\n");	
	return ret;
}

static void __exit led_exit(void)
{
	iounmap(gpbcon);
	iounmap(gpbdat);
	unregister_chrdev_region(dev, DEV_NUM);
	cdev_del(&cdev);
	printk("led_exit  ###############################\n");
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");

三、自动创建设备节点

(1)关于设备

①字符设备(数据的访问是有顺序的)

定义
字符设备是指在I/O传输过程中以字符为单位进行传输的设备。这类设备在数据传输时,数据被视为连续的字符流,逐个字符地进行读写操作。

特点

  • 数据传输单位:以字符为单位,即字节流的形式进行数据传输。
  • 缓冲区:通常没有内部缓冲区,数据会立即传输到设备或从设备中读取,这意味着字符设备的读写操作是实时的。
  • 访问模式:通常是顺序访问,即按照数据的顺序进行读写操作,不支持随机访问。
  • 应用场景:常用于与用户进行交互的设备,如键盘、鼠标、串口等,以及某些需要按字符处理的设备。

在操作系统中的表示
在UNIX或Linux系统中,字符设备以特别文件方式在文件目录树中占据位置并拥有相应的结点。这些设备文件可以使用与普通文件相同的文件操作命令进行打开、关闭、读、写等操作。

②块设备(数据访问是随机的)

定义
块设备是I/O设备中的一类,它将信息存储在固定大小的块中,每个块都有自己的地址。这类设备允许在设备的任意位置读取一定长度的数据。

特点

  • 数据传输单位:以固定大小的块为单位进行数据传输,数据块的大小通常在512字节到32768字节之间。
  • 缓冲区:通常具有内部缓冲区,用于在访问硬件之前缓存数据,以提高数据传输效率。
  • 访问模式:支持随机访问,即可以按块为单位在设备的任意位置进行读写操作。
  • 应用场景:常用于需要大量数据存储和读写操作的场景,如硬盘、U盘、SD卡等。

在操作系统中的表示
在操作系统中,块设备通过块设备驱动程序来管理和控制其读写操作。这些驱动程序负责提供访问设备的接口和功能,并处理设备的读写请求。

③网络设备(以套接字形式存在,只有名字,会集成负载的网络协议)

定义
网络设备是用来将各类服务器、PC、应用终端等节点相互连接,构成信息通信网络的专用硬件设备。

类型
网络设备包括信息网络设备、通信网络设备、网络安全设备等,具体如交换机、路由器、防火墙、网桥、集线器、网关、VPN服务器、网络接口卡(NIC)、无线接入点(WAP)、调制解调器、5G基站、光端机、光纤收发器等。

作用
网络设备的主要作用是构建网络,实现不同节点之间的信息通信和数据传输。它们通过物理连接和逻辑协议,将各个独立的计算设备连接成一个整体的网络系统。

综上所述,字符设备、块设备和网络设备在数据传输方式、应用场景和操作系统中的作用等方面存在显著差异。每种设备都有其独特的特点和用途,共同构成了现代计算机系统和网络的基础。


为什么我们的驱动需要手动创建节点呢

下一篇文件介绍自动设置节点。欢迎批评指正

ARM 是一种广泛使用的 CPU 架构,而 Linux 内核是一个开放源代码的操作系统内核。在 ARM 平台上,我们可以通过内核模块编程的方式与内核进行交互,实现一些自定义的功能。 下面,我们将介绍如何在 ARM Linux 上编写内核模块,并输出一个简单的 "Hello World" 消息。 ## 1. 环境准备 在开始编写内核模块之前,需要先准备好开发环境。具体步骤如下: 1. 安装交叉编译工具链。ARM 平台上的应用程序和内核模块需要使用交叉编译工具链进行编译。可以从官网下载对应的交叉编译工具链,也可以使用已经编译好的交叉编译工具链。 2. 下载内核源代码。可以从官网下载对应版本的内核源代码,也可以使用已经编译好的内核源代码。 3. 配置内核源代码。需要在内核源代码根目录下运行配置命令 `make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- menuconfig` 进行配置,选择需要的模块和功能。 ## 2. 编写内核模块 在准备好开发环境之后,可以开始编写内核模块了。具体步骤如下: 1. 创建一个新的文件夹,用于存放内核模块代码。 2. 创建一个新的 C 文件,命名为 `hello.c`。 3. 在 `hello.c` 文件中编写以下代码: ```c #include <linux/init.h> #include <linux/module.h> static int __init hello_init(void) { printk(KERN_INFO "Hello, world!\n"); return 0; } static void __exit hello_exit(void) { printk(KERN_INFO "Goodbye, world!\n"); } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Your Name"); MODULE_DESCRIPTION("A simple hello world module"); ``` 这段代码定义了一个简单的内核模块,当模块加载时会输出 "Hello, world!" 消息,当模块卸载时会输出 "Goodbye, world!" 消息。 4. 使用交叉编译工具链进行编译。在终端中进入 `hello.c` 文件所在的文件夹,运行以下命令进行编译: ```bash arm-linux-gnueabi-gcc -Wall -Werror -O2 -o hello.ko -c hello.c ``` 这个命令将生成一个名为 `hello.ko` 的内核模块文件。 ## 3. 加载和卸载内核模块 在编写好内核模块后,我们需要将它加载到内核中进行测试。具体步骤如下: 1. 将 `hello.ko` 文件复制到 ARM Linux 系统上。 2. 在终端中进入 `hello.ko` 文件所在的文件夹,运行以下命令以加载内核模块: ```bash insmod hello.ko ``` 这个命令将调用内核中的 `init_module` 函数,执行 `hello_init` 函数,输出 "Hello, world!" 消息。 3. 查看系统日志,可以看到 "Hello, world!" 消息。 ```bash dmesg ``` 4. 在终端中运行以下命令以卸载内核模块: ```bash rmmod hello ``` 这个命令将调用内核中的 `cleanup_module` 函数,执行 `hello_exit` 函数,输出 "Goodbye, world!" 消息。 5. 再次查看系统日志,可以看到 "Goodbye, world!" 消息。 至此,我们已经成功地在 ARM Linux 上编写了一个简单的内核模块,并输出了 "Hello, world!" 消息。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值