LED设备驱动

去年开的博客,到今天只有两篇文章,再看看同学的博客,惭愧啊。
写博客虽然额外多花了些时间,不过对自学知识有复习与总结的作用,还能提高语言组织能力,而且,如果有朋友能从我的博客中获益那就再好不过了。希望自己养成写博客的习惯,就从这一篇开始吧。


LED驱动是最简单的字符设备,可以说是Linux设备驱动程序里的HelloWorld,适合用来熟悉字符设备驱动程序开发的基本流程
笔者使用的是JZ2440开发板,板上有三个LED,分别对应GPF4,GPF5,GPF6引脚。本节将实现4个led设备:
/dev/leds
/dev/led0
/dev/led1
/dev/led2
第一个设备对应3个led的整体,后面3个分别对应其中一个led

先看看设备驱动编写时涉及到的基本知识,包括一些数据结构和函数:

设备编号

设备编号包括主次编号,主编号用来告诉系统使用哪个驱动程序去驱动该设备,次编号用来指定特定的设备
以下结构用于记录设备的编号:

dev_t dev;

在32位机上dev_t为32位数据,其中12位用来记录主编号,20位用于记录次编号。
我们使用以下两个宏来从dev_t中获取主次编号:

MAJOR(dev_t dev);
MINOR(dev_t dev);

反之,在已知主次编号时用以下的宏来得出dev_t:

MKDEV(int major, int minor);

分配和释放设备编号

分配设备编号有几种方法,第一种是静态分配:

int register_chrdev_region(dev_t first, unsigned int count, char *name);

first 是要分配的起始设备编号;count是要分配的设备编号总数。
通常编译到内核的设备驱动或常用的设备驱动使用静态方法,为了保证主编号的唯一性,我们自己编写驱动时推荐使用动态方法:

int alloc_chrdev_region(dev_t *dev, unsigned int firstminor,
    unsigned int count, char *name);

firstminor为要分配的第一个次编号,通常取0;count同上
这两个函数通常在模块初始化中调用
释放设备编号:

void unregister_chrdev_region(dev_t first, unsigned int count);

通常在模块退出时调用

加载设备驱动后可以在/proc/devices中找到设备的主次编号

其他重要的数据结构

struct file_operations f_ops;

一个函数指针的集合,这些函数负责实现系统调用如open,close,read,write,ioctl等

struct cdev cdev;

系统用来记录字符设备的结构

字符设备注册与注销

方法一:
注册:

int register_chrdev(unsigned int major, const char *name,
    struct file_operations *fops);

这里其实用一个函数完成了两个功能:使用fops初始化主编号为major的字符设备并且向内核注册该设备
注销:

int unregister_chrdev(unsigned int major, const char *name);

方法二:
把方法一中提到的“两个功能”分两步完成
先调用

void cdev_init(struct cdev *cdev, struct file_operations *fops);

这一步使用fops初始化cdev
再调用

int cdev_add(struct cdev *cdev, dev_t num, unsigned int count);

这一步向系统注册cdev
如果你想定义自己的设备结构并且在其中包含struct cdev成员,显然你只能使用这个方法

使用这个方法注册的设备要用以下函数注销:

void cdev_del(struct cdev *dev);

用户空间的访问

在f_ops中的read,write函数通常需要调用以下函数来与用户空间进行数据交互:

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

为何不直接用memcpy()?
很显然to和from不在同一个地址空间,所以直接memcpy的话将会发生不可预料的事。


有了这些基础之后,就可以开始编写基本的led驱动程序了

以下是完整代码:

//leds.c
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define DEVICE_NAME "led"
#define LED_DEV_NR 4

#define LED_ON 0
#define LED_OFF 1

/* 
 * 笔者自己定义的表示led设备的结构,之所以写成这样是为了以后编写更复杂的设备
 * 驱动时可以拿这个当模板用,到时侯可能往此结构中添加成员
 */
struct led_dev {
    struct cdev cdev;
};

struct led_dev *led_devs;    //指向led_dev数组的指针

char leds_status;   //记录3个led状态的位图,灯亮对应位置位,灯灭对应位复位

DECLARE_MUTEX(leds_lock);   //对全局变量操作时要锁住此互斥锁,这是为了保证操作的可重入性

/*
 * open()系统调用的实现,这里什么都不做
 */
static int led_open(struct inode *inode, struct file *filp)
{
    return 0;
}

/* 
 * read()系统调用的实现,功能是读取led设备的状态
 */
static int led_read(struct file *filp, char __user *buff, 
                    size_t count, loff_t *offp)
{
    int minor = MINOR(filp->f_dentry->d_inode->i_rdev); //获得目标文件对应的设备的次编号
    char val;

    switch (minor) {
        case 0:   // minor=0对应/dev/leds,对其进行读操作将读到3个led的状态对应的二进制值
            down(&leds_lock);
            val = leds_status;
            up(&leds_lock);

            if (copy_to_user(buff, (const void *)&val, 1)) 
                return -EFAULT;
            break;

        case 1:   //minor=1,2,3时对应/dev/led0,1,2,对其进行读操作将读到对应led的状态(亮1灭0)
            down(&leds_lock);
            val = leds_status & 0x1;
            up(&leds_lock);

            if(copy_to_user(buff, (const void *)&val, 1)) 
                return -EFAULT;
            break;

        case 2:
            down(&leds_lock);
            val = (leds_status >> 1) & 0x1;
            up(&leds_lock);

            if (copy_to_user(buff, (const void *)&val, 1)) 
                return -EFAULT;
            break;

        case 3:
            down(&leds_lock);
            val = (leds_status >> 2) & 0x1;
            up(&leds_lock);

            if (copy_to_user(buff, (const void *)&val, 1))
                    return -EFAULT;
            break;
    }

    return 1;
}

/* 
 * write系统调用的实现,将改变led设备的状态
 */
static ssize_t led_write(struct file *filp, const char __user *buff,
                size_t count, loff_t *offp)
{
    int minor = MINOR(filp->f_dentry->d_inode->i_rdev);
    char val;

    if (copy_from_user(&val, buff, 1))
        return -EFAULT;

    switch (minor) {
        case 0:  //对应/dev/leds,同时改变3个led的状态
            down(&leds_lock);
            s3c2410_gpio_setpin(S3C2410_GPF4, !(val & 0x1));
            s3c2410_gpio_setpin(S3C2410_GPF5, !((val >> 1) & 0x1));
            s3c2410_gpio_setpin(S3C2410_GPF6, !((val >> 2) & 0x1));
            leds_status = val;
            up(&leds_lock);
            //printk("led: %d\n", leds_status);
            break;

        case 1:
            s3c2410_gpio_setpin(S3C2410_GPF4, !val);
            if (val == 0) {
                down(&leds_lock);
                leds_status &= ~(1<<0);
                up(&leds_lock);
            } else {
                down(&leds_lock);
                leds_status |= (1<<0);
                up(&leds_lock);
            }
            break;

        case 2:
            s3c2410_gpio_setpin(S3C2410_GPF5, !val);
            if (val == 0) {
                down(&leds_lock);
                leds_status &= ~(1<<1);
                up(&leds_lock);
            } else {
                down(&leds_lock);
                leds_status |= (1<<1);
                up(&leds_lock);
            }
            break;

        case 3:
            s3c2410_gpio_setpin(S3C2410_GPF6, !val);
            if (val == 0) {
                down(&leds_lock);
                leds_status &= ~(1<<2);
                up(&leds_lock);
            } else {
                down(&leds_lock);
                leds_status |= (1<<2);
                up(&leds_lock);
            }
            break;
    }

    return 1;

}

/* 
 * 模块close()时将调用的函数
 */
int led_release(struct inode *inode, struct file *filp)
{
    return 0;
}


struct file_operations led_fops = { 
    .owner = THIS_MODULE,
    .read = led_read,
    .write = led_write,
    .open = led_open,
    .release = led_release,
};

dev_t led_dev_num;    //记录起始led设备号
int led_major;      //记录led设备主编号
/* 
 * 模块卸载(rmmod)时将调用的函数
 */
static void __exit leds_cleanup_module(void)
{
    int i;

    if (led_devs) {
        for (i = 0; i < LED_DEV_NR; i ++) {
            cdev_del(&led_devs[i].cdev);
        }
        kfree(led_devs);
    }

    unregister_chrdev_region(led_dev_num, LED_DEV_NR);

    printk(DEVICE_NAME " uninstalled.\n");
}

static void leds_lowlevel_init(void)
{
    down(&leds_lock);
    s3c2410_gpio_cfgpin(S3C2410_GPF4, S3C2410_GPF4_OUTP);
    s3c2410_gpio_cfgpin(S3C2410_GPF5, S3C2410_GPF5_OUTP);
    s3c2410_gpio_cfgpin(S3C2410_GPF6, S3C2410_GPF6_OUTP);
    s3c2410_gpio_setpin(S3C2410_GPF4, !0);
    s3c2410_gpio_setpin(S3C2410_GPF5, !0);
    s3c2410_gpio_setpin(S3C2410_GPF6, !0);
    leds_status = 0;
    up(&leds_lock);
}

static int __init leds_init_module(void)
{
    int result;
    int i;
    struct cdev *cdev = NULL;

    if ((result = alloc_chrdev_region(&led_dev_num, 0, LED_DEV_NR, DEVICE_NAME)) < 0) {   //分配LED_DEV_NR个设备号,起始次编号为0,起始设备号存储在led_dev_num中
        printk(DEVICE_NAME " fail to get major.\n");
        return result;
    }
    led_major = MAJOR(led_dev_num);

    led_devs = kmalloc(LED_DEV_NR * sizeof(struct led_dev), GFP_KERNEL);    //分配struct led_dev结构,这是我自己定义的一个表示led设备的结构,其中包含struct cdev
    if (!led_devs) {
        printk(DEVICE_NAME " fail to allocate mem.\n");
        result = -ENOMEM;
        goto fail;
    }
    memset(led_devs, 0, LED_DEV_NR * sizeof(struct led_dev));

    leds_lowlevel_init();   //底层硬件初始化可在模块加载时完成

    for (i = 0; i < LED_DEV_NR; i ++) {    
        cdev = &led_devs[i].cdev;
        cdev_init(cdev, &led_fops);  //初始化cdev
        cdev->owner = THIS_MODULE;
        if(cdev_add(cdev, MKDEV(led_major, i), 1)) {   //注册cdev
            printk(DEVICE_NAME": error when adding led%d", i);
            result = -1000;
            goto fail;
        }
    }

    printk(DEVICE_NAME " initialized.\n");
    return 0;

fail:
    leds_cleanup_module();   //如果初始化过程中出错,务必将已经分配的资源还给系统再返回!
    return result;

}

module_init(leds_init_module);
module_exit(leds_cleanup_module);

MODULE_AUTHOR("ZZ");
MODULE_LICENSE("Dual BSD/GPL");

//end of leds.c

make生成leds.ko,将其拷贝到开发板上,接着加载之:

insmod leds.ko

接下来创建设备节点。先找到主编号:

cat /proc/device

找到上面定义的DEVICE_NAME(即”led”)对应的编号,即led设备的主编号,我这里为252。接着创建节点:

mknod /dev/leds c 252 0
mknod /dev/led0 c 252 1
mknod /dev/led1 c 252 2
mknod /dev/led2 c 252 3

然后就可以向设备文件里读写数据测试了!
下面是我写的测试程序:

//chled.c
#include 
#include 
#include 
#include 

int main(int argc, char **argv)
{
    int led_no;
    int fd;
    char val;
    char *filename = NULL;

    if (argc < 2) {
        printf("Input filename.\n");
    }
    else {
        filename = argv[1];   //第一个参数是设备文件名
        fd = open (filename, O_RDWR);
        if (fd < 0) {
            printf("Fail to open %s\n", filename);
            return 1;
        }

        if (argc == 2) {   //若(除命令名外)只有1个参数,则读出设备状态
            read(fd, &val, 1);
            printf("%d\n", val);
            return 0;
        }
        else if (argc == 3) {   //若有2个参数则将第二个参数指定的数字写入设备
            val = argv[2][0] - '0';
            write(fd, &val, 1);
            return 0;
        }

        close(fd);
    }

    return 1;
}

//end of chled.c

你可能感兴趣的:(Linux设备驱动,helloworld,linux设备驱动)