天天看点

linux低级字符驱动开发例子详细分析驱动文件编写应用文件编写说说Makefileinsmod和mknod

本文实现的是一个低级驱动开发案例。简单通过编写内核驱动模块由开发板(sp6818)的按键key控制板子led灯

大致流程如下

编写驱动文件keyled.c

编写应用文件app.c

编写makefile

make制作出.ko文件

将.ko移至开发板中

insmod 将设备加入驱动序列中

mknod /dev/keyled c 87 0 将主设备号与节点相关联

驱动文件编写

//keyled.c
#include <linux/kernel.h>		// printk 
#include <linux/init.h>			// __init/__exit
#include <linux/module.h>   	// module_init 

#include <linux/fs.h>
#include <asm/uaccess.h>

#include <asm/io.h>				// arch/arm/include/asm/io.h
#include <linux/ioport.h>

#include <linux/interrupt.h>
#include <linux/wait.h>			//休眠唤醒机制

#include <linux/kthread.h>

wait_queue_head_t myqueue;
int flag= 1;


struct resource *res = NULL;

unsigned long *ptr = NULL;
unsigned long *ptr_b = NULL;

int keyled_open(struct inode *node, struct file *file)
{
	printk(KERN_INFO "keyled open\n");//KERN_INFO为输出优先级


	//led init 
	*(ptr+1) |= (0x1<<7);
	*(ptr) |= (0x1<<7);

	//key_init
	*(ptr_b + 1) &= ~(0x1<<30);


	return 0;
}

int keyled_close(struct inode *node, struct file *file)
{
	//keyled_init();
	printk(KERN_INFO "keyled close\n");

	return 0;
}

int kbuffer;

ssize_t keyled_write(struct file *file, const char __user *ubuf, size_t size, loff_t *offset)
{
	printk(KERN_INFO "keyled write\n");

	
	//memcpy(&kbuffer, ubuf, size);	

	//copy_to_user();
	copy_from_user(&kbuffer, ubuf, size);//将用户空间数据拷贝给内核空间
	printk(KERN_INFO "kbuffer: %d\n", kbuffer);

	if(kbuffer == 0)
	{
		printk(KERN_INFO "led on\n");
		*(ptr) &= ~(0x1<<7);
	}else 
	{
		printk(KERN_INFO "led off\n");
		*(ptr) |= (0x1<<7);
	}


	return 0;
}

int key_status;
ssize_t keyled_read(struct file *file, char __user *ubuf, size_t size, loff_t *offset)
{
	//printk(KERN_INFO "keyled read...\n");

	//线程开始休眠
	//wait_event(myqueue, flag != 1);
	

	key_status = (*(ptr_b + 6) & (0x1<<30)) >> 30;
	//printk(KERN_INFO "key state: %d\n", key_status);

	copy_to_user(ubuf, &key_status, size);//将内核空间数据拷贝给用户空间


	return 0;
}


struct file_operations keyled_fops = {
	.open 		= keyled_open,
	.release 	= keyled_close,
	.write		= keyled_write,
	.read 		= keyled_read, 
};

static int __init led_drv_init(void)//_init的作用是在insmod加载内核时调用该函数,之后忽略掉这个函数,并释放内存。
{
	printk(KERN_INFO "-----------------led driver init .....................\n");
	int ret;

	//注册字符设备驱动
	ret = register_chrdev(87, "key-led", &keyled_fops);//将驱动与主设备号相关联
	if(ret < 0)
	{
		printk(KERN_INFO "register_chrdev failed\n");
		return -1;
	}

	res = request_mem_region(0xc001c000, 0x1000,"myled-iomap");//request_mem_region函数并没有做实际性的映射工作,只是告诉内核要使用一块内存地址,声明占有,也方便内核管理这些资源。
	if(res == NULL)
	{
		printk(KERN_INFO "request_mem_region failed\n");
		return -1;
	}	

	printk(KERN_INFO "request_mem_region success\n");

	ptr = (unsigned long *)ioremap(0xc001c000, 0x1000);//ioremap主要是检查传入地址的合法性,建立页表(包括访问权限),完成物理地址到虚拟地址的转换。
	if(ptr == NULL)
	{
		printk(KERN_INFO "ioremap failed\n");
		return -1;
	}
	printk(KERN_INFO "ioremap success\n");

	
	ptr_b = (unsigned long *)ioremap(0xc001b000, 0x1000);//只读的地址似乎不需要request_mem_region
	if(ptr_b == NULL)
	{
		printk(KERN_INFO "key ioremap failed\n");
		return -1;
	}
	printk(KERN_INFO "key ioremap success\n");

	//初始队列头结构
	init_waitqueue_head(&myqueue);


	return 0;
}

static void __exit led_drv_exit(void)//remmod时调用
{
	printk(KERN_INFO "-----------------led driver exit .....................\n");


	iounmap(ptr);//释放映射
	iounmap(ptr_b);//释放映射
	release_mem_region(0xc001c000, 0x1000);//释放地址声明

	unregister_chrdev(87, "key-led");//注销

}
	

module_init(led_drv_init);
module_exit(led_drv_exit);


MODULE_LICENSE("GPL");//版本
MODULE_AUTHOR("zbzyjya ");//作者
MODULE_DESCRIPTION("8250 serial probe module for Accent Async cards");
           

本文会按照驱动程序内部的启动顺序与调用顺序来讲解

先关注这两个函数(_init和_exit)

static int __init led_drv_init(void)
static void __exit led_drv_exit(void)
           

先分别说说修饰符_init和_exit的作用:

_init的作用是调用完该函数之后,就忽略掉这个函数,在初始化完成后丢弃该函数并收回所占内存,

__exit修饰词标记函数只在模块卸载时使用。

_init和_exit的作用

通过

module_init(led_drv_init);
module_exit(led_drv_exit);
           

在内核加载(insmod)和卸载(remmod)时会调用对应的函数。

这种函数存在的意义是,将一部分不常用的代码或者只调用一次的代码,动态编译进内核当中,使用完之后可以立即释放该函数的内存,使得内存利用更加合理。

关注一下led_drv_init内的几个api

//注册字符设备驱动
	ret = register_chrdev(87, "key-led", &keyled_fops);//将驱动与主设备号相关联
           

这个函数将驱动与设备号关联起来,第三个参数传入的是一个file_operations的结构体,这个结构体十分重要,后文再讲解。

res = request_mem_region(0xc001c000, 0x1000,"myled-iomap");
//request_mem_region函数并没有做实际性的映射工作,只是告诉内核要使用一块内存地址,声明占有,也方便内核管理这些资源。
           

request_mem_region函数并没有做实际性的映射工作,只是告诉内核要使用一块内存地址,声明占有,也方便内核管理这些资源。第一个参数为led灯的物理地址,第二个参数为读取大小。

ioremap主要是检查传入地址的合法性,建立页表(包括访问权限),完成物理地址到虚拟地址的转换。要操作led灯的话必须要有这两步声明与映射。

在调用insmod keyled.ko时驱动会执行led_drv_init,完成上述各种初始化与注册操作。与其相应的是调用remmod的时候会执行 led_drv_exit,执行该函数内部的驱动注销与映射释放等操作。

应用文件编写

再来看看app.c

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

#define LED_ON 	0x0 
#define LED_OFF	0x1 

int main()
{
	int fd; 
	int wr_data;
	int rd_state;

	
	fd  = open("/dev/keyled", O_RDWR);
	if(fd < 0)
	{
		printf("open failed\n");
		return -1;
	}
	printf("open keyled success\n");


	while(1)
	{
		read(fd, &rd_state, 4);
		if(rd_state == 0)
		{
			usleep(30000);
			read(fd, &rd_state, 4);
			if(rd_state == 0)
			{
				//按键弹开
				do {
					read(fd, &rd_state, 4);
				}while(rd_state == 0);

				//
				printf("led change state....\n");
			}
		}
	}
	close(fd);

	return 0;
}

           

这段代码为应用层代码,先简单说说应用层是如何通过API系统调用的,在应用层中,open了一个驱动节点(/dev/keyled)之后,应用层对这个文件描述符的open、read、write等API操作,最后都会找到这个驱动内部file_operations结构体(参考上面keyled.c)中的.open、.read、.write(均为函数指针)所指向的函数来执行。

struct file_operations keyled_fops = {
	.open 		= keyled_open,
	.release 	= keyled_close,
	.write		= keyled_write,
	.read 		= keyled_read, 
};
           

如当应用层执行fd = open("/dev/keyled", O_RDWR);时,驱动就会找到keyled_open来执行。file_operations结构体是驱动开发中很重要的结构体,注意在字符驱动注册时要作为第三个参数传入。

那么同理应用层调用write和read时对应会调用

ssize_t keyled_write(struct file *file, const char __user *ubuf, size_t size, loff_t *offset)
{
	printk(KERN_INFO "keyled write\n");

	
	//memcpy(&kbuffer, ubuf, size);	

	//copy_to_user();
	copy_from_user(&kbuffer, ubuf, size);//将用户空间数据拷贝给内核空间
	printk(KERN_INFO "kbuffer: %d\n", kbuffer);

	if(kbuffer == 0)
	{
		printk(KERN_INFO "led on\n");
		*(ptr) &= ~(0x1<<7);
	}else 
	{
		printk(KERN_INFO "led off\n");
		*(ptr) |= (0x1<<7);
	}


	return 0;
}

int key_status;
ssize_t keyled_read(struct file *file, char __user *ubuf, size_t size, loff_t *offset)
{
	//printk(KERN_INFO "keyled read...\n");

	//线程开始休眠
	//wait_event(myqueue, flag != 1);
	

	key_status = (*(ptr_b + 6) & (0x1<<30)) >> 30;
	//printk(KERN_INFO "key state: %d\n", key_status);

	copy_to_user(ubuf, &key_status, size);//将内核空间数据拷贝给用户空间


	return 0;
}
           

app.c中的write是如何把内容写进fd里的呢?

该函数的第二个传参ubuf便是用户写入的内容,通过

将用户空间数据拷贝给内核空间,把ubuf里的内容传入kbuffer中。

keyled_read同理,用copy_to_user将内核空间数据拷贝给用户空间,完成读写。有一道面试题问的是,内核空间和用户空间如何通讯,其答案之一就是通过copy_from_user和copy_to_user来实现。

有了这个功能,就能通过读取开关状态来判断是否点亮led灯。

说说Makefile

如何用makefile制作出对应的keyled.ko和app呢?

这里给出参考与博主自己的理解

obj-m    += keyled.o


KERNEL_DIR = /opt/wkspace/build_plat/kernel-3.4.39/

all: 
	make modules CROSS_COMPILE=/usr/local/arm/arm-eabi-4.8/bin/arm-eabi- -C $(KERNEL_DIR) M=`pwd`	
	arm-linux-gcc app.c -o app 
	cp keyled.ko app /nfs 
clean:
	make modules clean M=`pwd` -C  $(KERNEL_DIR) CROSS_COMPILE=/usr/local/arm/arm-eabi-4.8/bin/arm-eabi-
           

把keyled.c编译成keyled.ko文件

驱动开发makefile

linux低级字符驱动开发例子详细分析驱动文件编写应用文件编写说说Makefileinsmod和mknod

红色是编译内核驱动的时候要加的指令 黄色是指定交叉编译链 蓝色是跳转到内核源码里去 绿色是回到当前目录。

值得注意的是-C是跳转到指定路径当中去 M是回来。由于驱动开发需要依赖内核一些库,KERNEL_DIR指定的是内核源码所在的路径。

制作出.ko和app之后,就能把他们拷到开发板上。

insmod和mknod

拷到开发板之后,在.ko当前文件夹下,命令行输入

insmod keyled.ko
           

实现将设备加入驱动序列中。通过cat /proc/devices或者lsmod可以查看驱动是否加入成功。除了insmod之外还可以用modprobe keyled.ko来将设备加入,modprode的功能更多,有兴趣的读者可以自己了解。

设备加入驱动序列之后,还需要

mknod /dev/keyled c 87 0    将主设备号与节点相关联 87是主设备号 0是副设备号
           

通过mknod将主设备号与节点相关联。可以这样理解,insmod之后驱动中只有一个设备号和设备相关联,但应用层里找不到这样一个设备。mknod的作用是给这个设备生成一个/dev文件夹下的节点,类似于给应用层代码插一个指路的路牌,这样就把节点-设备-设备号相关联了起来,能让应用层代码顺利找到该设备。

完成这一步之后就能./app执行可执行文件了。执行完之后可以remmod掉.ko,再次lsmod查看设备就已经被删除了。

小弟刚入行不久,若有问题请指正。