在学习驱动的时候我遇到了很多问题,所以我的学习路线是这样的:
编写驱动发现.ko文件需要放入开发板的目录中,然后就学习通过nfs创建共享文件,在配置nfs时发现网络没有连接上,所以就学习怎样配置IP地址,在传输完.ko文件后,发现自动生成不了dev nod,在经过一番寻找问题后发现是文件系统没配置好,所以我又配置了一遍mdev。最终成功!(耗时2天呀。)
正文:
一个驱动文件包含以下三个部分
Linux设备分成三种基本类型:
设备驱动程序也分为对应的三类:
设备节点被创建在/dev下,是连接内核与用户层的枢纽,就是设备是接到对应哪种接口的哪个ID 上。 相当于硬盘的inode一样的东西,立面记录了硬件设备的位置和信息
在Linux中,所有设备都以文件的形式存放在/dev目录下,都是通过文件的方式进行访问,设备节点是Linux内核对设备的抽象,一个设备节点就是一个文件。应用程序通过一组标准化的调用执行访问设备,这些调用独立于任何特定的驱动程序。而驱动程序负责将这些标准调用映射到实际硬件的特有操作。
有很重要的一点:设备节点,驱动,硬件设备是如何关联到一起的呢?
---> 这是通过设备号实现的,包括主设备号和次设备号。当创建一个设备节点时需要指定主设备号和次设备号。应用程序通过名称通过名称访问设备,而设备号指定了对应的驱动程序和对应的设备。主设备号标识设备对应的驱动程序,次设备号由内核使用,用于确定设备节点所指设备。
其中设备类型、设备号、设备结点可以通过 ls /dev -l 查看。
接着开始细品驱动文件
结构体file_operation 在头文件fs.h中定义,用来存储驱动内核模块提供的对设备进行各种操作的函数的指针,相当于对设备进行config。该结构体的每个域都对应着驱动内核模块用来处理某个被请求的 事务的函数的地址。通过查看file_operation源码可以看出存储着许多内核模块中执行对应操作的地址,在应用程序中使用其中的部分成员,对应的没有调用的成员函数则会设置为NULL。
举个栗子,若是按键驱动,每个字符设备需要定义.read用来读取设备数据的函数。另一个栗子,本文里编写LED驱动,所以需要定义.write用来写寄存器(open中初始化LED write中配置LED),对其config如下:
static struct file_operations drv_fops = {
.owner = THIS_MODULE, // 相当于This指针
.open = drv_open, // 将.open 函数指针指向drv_open 函数
.write = drv_write, // 将.write函数指针指向drv_write函数
};
在linux 2.6之后有register_chrdev_region()或者register_chrdev()函数可以注册驱动文件。register_chrdev_region()相比于register_chrdev()更加简便。这里为了方便理解以register_chrdev()来分析:
major = register_chrdev(0, "led_drv", &drv_fops); // register chrdev. to major
//(major, "drv name", file operations);
①不怕麻烦版本:在系统中调用mknod命令创建设备节点,命令格式为mknod name { b | c } Major Minor ,b表示块设备,c表示字符设备。
②高端版本:搭配mdev文件系统,在初始化函数中创建设备节点,使用自动创建的前提是用户空间移植了udev。在驱动的初始化代码里调用class_create为该设备创建一个class,再为每个设备调用device_create创建对应的设备。在移除模块时,要调用device_destroy 和class_destroy相应的删除自动创建的设备和类。
很简单就两步:一、定义一个类指向Step1中文件类;二、创建文件类对应的设备节点(主设备号&次设备号)
static struct class *drv_class;
static struct class_device *drv_class_dev;
-------------------------------------------------------------------------------------------
drv_class = class_create(THIS_MODULE, "leddrv");//(.owner, 类名)
if(IS_ERR(drv_class))
{
printk( "ERROR creat key class");
}
//(指向文件类的类名, 父类节点, (主设备号,次设备号), ,设备节点名字)
drv_class_dev = class_device_create(drv_class, NULL, MKDEV(major, 0), NULL, "led");
if(drv_class_dev == NULL)
{
printk("ERROR creat dev");
}
在Step1中调用的函数,以最简单printk为例(只进行打印操作):
static int drv_open(struct inode *inode, struct file *file)
{
printk("drv_open\n");
return 0;
}
static ssize_t drv_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
{
printk("drv_write\n");
return 0;
}
两部分:卸载驱动&删除设备节点
首先修饰init函数和exit函数,相当于告诉内核这个函数为初始化函数和退出函数
module_init(first_drv_init); //When install drv, system will find init function
module_exit(first_drv_exit); // Uinstall drv
需要在驱动文件中加入模块许可声明(适合linux2.4&2.6)否则insmod驱动时将不能与/proc/kallsyms中的符号正常连接:
MODULE_LICENSE("GPL");
则我们的驱动框架为:
//drv_frame.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
static struct class *drv_class;
static struct class_device *drv_class_dev;
static int drv_open(struct inode *inode, struct file *file) //设备节点,参数项
{
printk("drv_open\n");
return 0;
}
static ssize_t drv_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
{
printk("drv_write\n");
return 0;
}
static struct file_operations drv_fops = {
.owner = THIS_MODULE, /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
.open = drv_open,
.write = drv_write,
};
int major;
static int drv_init(void)
{
major = register_chrdev(0, "led_drv", &drv_fops); // register chrdev. to major (major, "drv name", file operations);
drv_class = class_create(THIS_MODULE, "leddrv");
if(IS_ERR(drv_class))
{
printk( "ERROR creat key class");
}
drv_class_dev = class_device_create(drv_class, NULL, MKDEV(major, 0), NULL, "led");
if(drv_class_dev == NULL)
{
printk("ERROR creat dev");
}
return 0;
}
static void drv_exit(void)
{
unregister_chrdev(major, "led_drv"); // 卸载
class_device_unregister(drv_class_dev);
class_destroy(drv_class);
}
module_init(drv_init); //When install drv, system will find init function
module_exit(drv_exit); // Uinstall drv
MODULE_LICENSE("GPL");
上文也提到了,构建设备节点是需要mdev协助的,mdev其实是udev的简化版,通过读取内核信息来创建设备文件,要使用mdev,需要内核支持sysfs文件系统,若想减少对flash的读写还得支持tmpfs文件系统(可有可没有,我tmpfs挂载不上),其配置方法如下:
配置开机自动运行文件/etc/init.d/rcS ,添加以下几句命令:
mount -a
mkdir /dev/pts
mount -t devpts devpts /dev/pts
echo /sbin/mdev > /proc/sys/kernelhotplug
mdev -s
其中mount -a命令是将文件/etc/fstab中的设备进行挂载,所以这里应该添加sysfs和tmpfs文件系统的挂载:
# device mount-point type options dump fsck order
proc /proc proc defaults 0 0
tmpfs /tmp tmpfs defaults 0 0
tmpfs /dev tmpfs defaults 0 0
sysfs /sys sysfs defaults 0 0
挂载的文件系统可以通过以下指令进行观察:
#cat /proc/mounts
接着就是随便编写一个应用程序(应用程序用来控制文件)
#include
#include
#include
#include
int main(int argc, char **argv)
{
int fd;
int val = 1;
fd = open("/dev/led", O_RDWR);//设备节点 读写打开
if (fd < 0)
{
printf("can't open!\n");
}
write(fd, &val, 4);
return 0;
}
由于open函数的返回值为一个int类型,当文件打开出错时会返回-1。如果为大于0的值,那么这个值代表的就是文件描述符。所以有更一般的写法:
if((fd=open("/dev/ttys0",O_RDWR | O_NOCTTY | O_NDELAY)<0)
{
perror("open");
}
KERN_DIR = ~/work/system/linux-2.6.22.6
all:
make -C $(KERN_DIR) M=`pwd` modules
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
obj-m += drv_led.o
其中KERN_DIR代表内核的位置,所以底下的make是根据linux内核中的makefile进行编译,注释如下:
$arm-linux-gcc -o test_fun test_fun.c
命令&注释如下
在命令段通过insmod对编译出的.ko文件进行加载。
#insmod /mnt/drv_frame.ko
这时可以通过以下命令,判断驱动初始化是否成功:
#lsmod //查看是否创建设备节点
#ls /dev -l //查看所有的驱动(驱动名 主设备号 次设备号)
#cat /proc/device //查看驱动是否注册成功
如果驱动初始化没有问题就可以直接开始运行应用程序:
#./mnt/drv_fun
运行成功会看见系统调用了drv_open()函数以及drv_write()函数打印出来的值:
drv_open
drv_write
rmmod drv_led //.ko文件名
这一步会注销驱动并且删除设备节点。若要单独删除设备节点可以直接rm -r dev/led 指令
在裸机程序中对于LED的点亮是直接通过物理地址进行配置的,但是在驱动程序中是通过虚拟地址这个中介对物理地址进行操作。从物理地址向虚拟地址映射可以通过ioremap(address, length)进行映射。在卸载时可以通过iounmap(address)进行去映射。
除了对地址的映射,其他的操作和裸机配置LED操作一样
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
static struct class *leddrv_class;
static struct class_device *leddrv_class_dev;
volatile unsigned long *gpfcon = NULL;
volatile unsigned long *gpfdat = NULL;
static int led_drv_open(struct inode *inode, struct file *file)
{
*gpfcon &= ~((3<<8) | (3<<10) | (3<<12));
*gpfcon |= ((1<<8) | (1<<10) | (1<<12));
//printk("first_drv_open\n");
return 0;
}
static ssize_t led_drv_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
{
int val;
copy_from_user(&val, buf, count); // copy buf from user space to val
if(val == 1)
*gpfdat &= ~((1<<4) | (1<<5) | (1<<6));
else
*gpfdat |= ((1<<4) | (1<<5) | (1<<6));
//copy_to_user()
//printk("led_drv_write\n");
return 0;
}
static struct file_operations led_drv_fops = {
.owner = THIS_MODULE, /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
.open = led_drv_open,
.write = led_drv_write,
};
int major;
static int led_drv_init(void)
{
major = register_chrdev(0, "led_drv", &led_drv_fops); // 注册, 告诉内核// register chrdev. to major (major, "drv name", file operations);
leddrv_class = class_create(THIS_MODULE, "leddrv");
if(IS_ERR(leddrv_class))
printk( "ERROR creat key class");
leddrv_class_dev = class_device_create(leddrv_class, NULL, MKDEV(major, 0), NULL, "led"); /* /dev/xyz */
if(leddrv_class_dev == NULL)
printk("ERROR creat dev");
gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16); //remap LED GPFCPN
gpfdat = gpfcon + 1; //remap LED GPFDAT
return 0;
}
static void led_drv_exit(void)
{
unregister_chrdev(major, "led_drv"); // 卸载
class_device_unregister(leddrv_class_dev);
class_destroy(leddrv_class);
iounmap(gpfcon); // unmap virtual address
}
module_init(led_drv_init); //When install drv, system will find init function
module_exit(led_drv_exit); // Uinstall drv
MODULE_LICENSE("GPL");
PS:从用户空间的参数在驱动程序中成员函数如果要使用,则需要通过copy_form_user()和copy_to_user()将用户空间的参数传递过来。用法:
1、Copy_to_user( to, &from, sizeof(from))
To:用户空间函数 (可以是数组)
From:内核空间函数(可以是数组)
sizeof(from):内核空间要传递的数组的长度
2、Copy_from_user(&from , to , sizeof(to) )
To:用户空间函数 (可以是数组)
From:内核空间函数(可以是数组)
sizeof(from):内核空间要传递的数组的长度
一般的main函数都是不带参数的,因此main 后的括号都是空括号。实际上,main函数可以带参数,这个参数可以认为是 main函数的形式参数。C语言规定main函数的参数只能有两个, 习惯上这两个参数写为argc和argv。因此,main函数的函数头可写为: main (argc,argv)C语言还规定argc(第一个形参)必须是整型变量,argv( 第二个形参)必须是指向字符串的指针数组。加上形参说明后,main函数的函数头可以写为:
main (int argc,char *argv[])
main函数的参数值是从操作系统命令行上获得的。运行一个可执行文件时,在命令行键入文件名,再输入实际参数即可把这些实参传送到main的形参中去。
命令行下运行可执行文件的一般形式为:可执行文件名 参数 参数……,命令行中的参数个数原则上未加限制。argc参数表示了命令行中参数的个数(注意:文件名本身也算一个参数),argc的值是在输入命令行时由系统按实际参数的个数自动赋予的。例如有命令行为./a.out a b b d a,由于文件名a.out本身也算一个参数,所以共有6个参数,因此argc取得的值为6。argv参数是字符串指针数组,其各元素值为命令行中各字符串(参数均按字符串处理)的首地址。 指针数组的长度即为参数个数。数组元素初值由系统自动赋予。
#include
#include
#include
#include
// 用法:
// led_drv on
// led_drv off
int main(int argc, char **argv)
{
int fd;
int val = 1;
fd = open("/dev/led", O_RDWR); //允许读写
if (fd < 0)
printf("can't open!\n");
if(argc != 2) // 参数数量
{
printf("error parterner\n"); // 提示信息
printf("%s \n",argv[0]);
return 0;
}
if(strcmp(argv[1],"on") == 0)
val = 1;
else
val = 0;
write(fd, &val, 4);
return 0;
}
在执行应用程序之前都相同,执行应用程序时:
#drv_fun on //打开LED
#drv_fun off //关闭LED
这一节里实现分别控制每一个LED,办法很多以下列出几种常用的办法:
为了学习,使用控制次设备号来控制LED。
设备号的使用应该始终使用
获得设备号需要知道当前文件对应的设备节点的位置(dev/led1),这一信息体现在file结构体中,在vim中从file通过一步一步g+]可以得到inode的位置所以在write和open函数中使用MIONR()如下所示:
int minor = MINOR(file -> f_dentry -> d_inode -> i_rdev); //write struct file *file
int minor = MINOR(inode -> i_rdev); //open struct inode *inode
思想:基以上两点,首先在初始化函数中初始化四个设备节点(次设备号不同),通过在write和open中分析此设备号,进而判断程序动作。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define DEV_NAME "leddrv" // device name
#define LED_MAJOR 252 // major device number
static struct class *leddrv_class;
static struct class_device *leddrv_class_dev[4];
volatile unsigned long *gpfcon = NULL;
volatile unsigned long *gpfdat = NULL;
static int led_drv_open(struct inode *inode, struct file *file)
{
int minor = MINOR(inode -> i_rdev);
switch(minor)
{
case 0:
{
*gpfcon &= ~((3<<4*2) | (3<<5*2) | (3<<6*2));
*gpfcon |= ((1<<4*2) | (1<<5*2) | (1<<6*2));
break;
}
case 1:
{
*gpfcon &= ~(3<<4*2);
*gpfcon |= (1<<4*2);
break;
}
case 2:
{
*gpfcon &= ~(3<<5*2);
*gpfcon |= (1<<5*2);
break;
}
case 3:
{
*gpfcon &= ~(3<<6*2);
*gpfcon |= (1<<6*2);
break;
}
}
// *gpfcon &= ~((3<<8) | (3<<10) | (3<<12));
// *gpfcon |= ((1<<8) | (1<<10) | (1<<12));
//printk("first_drv_open\n");
return 0;
}
static ssize_t led_drv_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
{
int val;
copy_from_user(&val, buf, sizeof(buf)); // copy buf from user space to val
int minor = MINOR(file -> f_dentry -> d_inode -> i_rdev);
switch(minor)
{
case 0:
{
if(val == 1)
*gpfdat &= ~((1<<4) | (1<<5) | (1<<6));
else
*gpfdat |= ((1<<4) | (1<<5) | (1<<6));
break;
}
case 1:
{
if(val == 1)
*gpfdat &= ~(1<<4);
else
*gpfdat |= (1<<4);
break;
}
case 2:
{
if(val == 1)
*gpfdat &= ~(1<<5);
else
*gpfdat |= (1<<5);
break;
}
case 3:
{
if(val == 1)
*gpfdat &= ~(1<<6);
else
*gpfdat |= (1<<6);
break;
}
}
/* if(val == 1)
*gpfdat &= ~((1<<4) | (1<<5) | (1<<6));
else
*gpfdat |= ((1<<4) | (1<<5) | (1<<6));*/
//copy_to_user()
//printk("led_drv_write\n");
return 0;
}
static struct file_operations led_drv_fops = {
.owner = THIS_MODULE, /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
.open = led_drv_open,
.write = led_drv_write,
};
int major;
static int led_drv_init(void)
{
major = register_chrdev(0, DEV_NAME, &led_drv_fops); // register chrdev. to major (major, "drv name", file operations);
//To file_operations class
leddrv_class = class_create(THIS_MODULE, "leddrv");
if(IS_ERR(leddrv_class)) // error
{
printk( "ERROR creat key class");
}
//creat device
leddrv_class_dev[0] = class_device_create(leddrv_class, NULL, MKDEV(major, 0), NULL, "ledall");
int minor;
for(minor = 1; minor < 4; minor++)
{
leddrv_class_dev[minor] = class_device_create(leddrv_class, NULL, MKDEV(major, minor), NULL, "led%d", minor);
if(unlikely(IS_ERR(leddrv_class_dev[minor])))
return PTR_ERR(leddrv_class_dev[minor]);
}
//remap phcical address
gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16); //remap LED GPFCPN
gpfdat = gpfcon + 1; //remap LED GPFDAT
printk("initialized complete\n");
return 0;
}
static void led_drv_exit(void)
{
unregister_chrdev(major, DEV_NAME); // 卸载
int minor;
for(minor = 0; minor < 4; minor ++)
{
class_device_unregister(leddrv_class_dev[minor]);
}
class_destroy(leddrv_class);
iounmap(gpfcon); // unmap virtual address
}
module_init(led_drv_init); //When install drv, system will find init function
module_exit(led_drv_exit); // Uinstall drv
MODULE_LICENSE("GPL");
#include
#include
#include
#include
// led_drv
void print_tips(char *app_name)
{
printf("Tips:\n");
printf("%s ", app_name);
printf("eg.\n");
printf("%s /dev/ledall\n", app_name);
printf("%s /dev/led1\n", app_name);
printf("%s /dev/led2\n", app_name);
printf("%s /dev/led3\n", app_name);
}
int main(int argc, char **argv)
{
int fd;
int val = 1;
char *dev_list = argv[2];
fd = open(dev_list, O_RDWR);
if (fd < 0)
{
printf("can't open!\n");
}
if(argc != 3) // input
{
print_tips(argv[0]);
return 0;
}
if(strcmp(argv[1],"on") == 0)
val = 1;
else
val = 0;
write(fd, &val, 4);
return 0;
}
后面就和之前的操作一样了 看码!!