前言
后面可能会写一系列的驱动学习的文章,现在就以一个最简单的字符设备驱动开始。我的这个字符设备主要是为了点亮开发板的LED灯。
对于字符设备的文章,网上很多而且也比较简单,所以这篇文章只适合刚学的小白,大神请绕道。
正文
先把代码贴出来再把重点讲一下吧
字符设备驱动程序:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>
static int major;
static struct class *first_class;
static struct class_device *first_class_dev;
volatile unsigned long *gpfcon = NULL;
volatile unsigned long *gpfdat = NULL;
static int first_drv_open(struct inode *inode, struct file *file)
{
/*配置GPF4,5,6为输出引脚*/
*gpfcon &= ~( (0x3<<8) | (0x3<<10) | (0x3<<12) ); //先清空初始化
*gpfcon |= ( (0x1<<8) | (0x1<<10) | (0x1<<12) ); //设置为输出引脚
return 0;
}
static ssize_t first_drv_write(struct file *file, const __user *buf, size_t count, loff_t *ppos)
{
int val;
copy_from_user(&val, buf, count); //从用户空间获取数据
if (1 == val)
{
//开灯,低电平有效
*gpfdat &= ~( (0x1<<4) | (0x1<<5) | (0x1<<6) );
}
else
{
//关灯
*gpfdat |= ( (0x1<<4) | (0x1<<5) | (0x1<<6) );
}
return 0;
}
/*下面的结构体就是为了和内核联系起来*/
static struct file_operations first_drv_fops = {
.owner = THIS_MODULE,
.open = first_drv_open,
.write = first_drv_write,
};
int first_drv_init(void)
{
major = register_chrdev(0, "first_drv", &first_drv_fops); //第一个参数为0,系统会自动分配主设备号
first_class = class_create(THIS_MODULE, "firstdrv");
first_class_dev = class_device_create(first_class, NULL, MKDEV(major, 0), NULL, "xyz"); //这是2.6内核的函数,新版的内核使用device_create
gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16); //第一个参数是物理地址,第二个参数是长度,这里为16字节
gpfdat = gpfcon + 1;
return 0;
}
void first_drv_exit(void)
{
unregister_chrdev(major, "first_drv");
class_device_unregister(first_class_dev);
class_destroy(first_class);
iounmap(gpfcon);
}
module_init(first_drv_init);
module_exit(first_drv_exit);
MODULE_LICENSE("GPL");
(1)每个驱动程序加载到内核时,都会先执行module_init函数,这个函数其实是个宏定义(module_init的含义),最终执行的是module_init括号里面定义的first_drv_init函数。自然,卸载函数就会执行module_exit函数。
(2)register_chrdev()函数用于注册字符设备。当第一个参数为0时,系统会自动分配主设备号。我们也看到第三个参数是first_drv_fops结构体,这能将用户空间的open()、write()和first_drv_open()、first_drv_write()函数联系起来。在卸载驱动的时候,我们也要调用相应的卸载字符设备的函数unregister_chrdev()。
(3)调用class_create()函数后,系统会创建一个类:/sys/class/firstdrv。再调用class_device_create()函数就会自动在/dev/目录下创建对应的设备节点:/dev/xyz。当然,在卸载驱动也有相应的卸载函数:class_device_unregister()和class_destroy()。
(4)我们以前写裸板程序的时候,操作GPIO寄存器的时候,都是直接写物理地址,但是这里我们不能这么做。需要通过ioremap()函数将物理地址映射为虚拟地址。在卸载驱动后,我们也要调用iounmap()函数释放本次映射。
到这里,一个粗糙的字符设备程序就完成了,为了代码的阅读,我就没有考虑很多函数调用失败后的情况了,实际生产中肯定不能这么做。下面就看一下用户空间的程序怎么使用字符设备节点了。
测试函数:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
int main(int argc, char **argv)
{
int val = 1;
int fd;
fd = open("/dev/xyz", O_RDWR);
if (fd < 0)
{
printf("open failed\n");
return 0;
}
if (argc != 2)
{
printf("Usage :\n");
printf("%s <on|off>\n", argv[0]);
return 0;
}
if (!strcmp(argv[1], "on"))
{
val = 1;
}
else
{
val = 0;
}
write(fd, &val, 4);
return 1;
}
当调用open()函数成功打开创建的节点后(内核空间中,会调用我们的驱动程序中的first_drv_open()函数),如果我们往节点写1,内核空间中的first_drv_write()函数就会通过copy_from_user()获取到这个值,然后再判断是点亮还是灭灯。