分析Linux下的LED驱动例程

Linux下的驱动与无操作系统的驱动的区别:

没有操作系统的时候,驱动程序直接访问相关寄存器的相应的位即可实现目标需求,当有操作系统的时候,我们则需要在驱动程序里面设计面向操作系统内核的接口,且这样的接口由操作系统规定,对一类设备而言结构一致,独立于具体的设备。可见,当系统中存在操作系统的时候,驱动变成了连接硬件和内核的桥梁,并不会直接服务于顶层应用程序,顶层程序需要通过系统调用访问VFS再间接调用驱动程序访问硬件。

那么问题来了,有了操作系统之后,驱动反而变得更复杂,那还需要操作系统做什么。首先操作系统的存在主要是为了高效实现多并发,还有就是虚拟内存管理机制。那么其对于驱动而言,说白了就是以复杂化底层驱动程序为代价便利顶层程序调用,当驱动程序都按照操作系统给出的独立于设备的接口而设计时,那么应用程序就可以使用统一的系统调用接口来访问各种设备。

接下来以Linux下的一个LED驱动为例看看驱动程序干了些什么,因为Linux的驱动框架庞大而复杂,这里只解释一下代码涉及到的部分,并不展开细谈。


#include <linux/modules.h>           
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/slab.h>

/*自定义的设备结构体*/
struct led_dev
{
    struct cdev cdev;             //字符设备cdev结构体
    char value;                   //led亮时为1,灭时为0,用户可读写此值 
};

struct led_dev *led_devp;         //定义为全局变量,在卸载函数可以直接调用

/*以下这部分信息可以通过命令 modinfo <模块名> 获得*/
MODULE_AUTHOR("YeZhuLaiPi");       //声明作者
MODULE_LICENSE("GPL v2");         //表示遵循GPL协议,没有的话内核会发出警告

/*打开函数*/
int led_open(struct inode *inode, struct file *filp) //inode是节点结构体,描述一个文件,file是文件结构体,描述一个打开的文件
{
    struct led_dev *dev;
    dev = container_of(inode->i_cdev, struct led_dev, cdev); //通过传入的inode找到自定义结构体指针:这里的指针指向的就是init函数里面申请的内存,所以确实可以找到自定义结构体地址
    filp->private_data = dev;     //让自定义的设备结构体作为设备的私有信息,后面会通过这个私有信息拿到该结构体并访问到value值
    return 0;
}

/*关闭函数*/
int led_release(struct inode *inode, struct file *filp)
{
    return 0;
}

/*读函数和写函数,两者可有可无,因为通过ioctl函数就可以完成数据传输并驱动*/
ssize_t led_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
    struct led_dev *dev = filp->private_data;    //在这里拿到自定义结构体
    if (copy_to_user(buf, &(dev->value), 1))     //写入数据到用户空间
        return -EFAULT;
    return 1;
}

ssize_t led_write(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
    struct led_dev *dev = filp->private_data;   //同理在这里拿到自定义结构体
    if (copy_from_user(&(dev->value), buf, 1))  //从用户空间拿到数据
        return -EFFAULT;
    if (dev->value == 1)                        //根据写入的值控制灯的亮灭
        led_on();                               //开灯函数,这里省略
    else
        led_off();                              //关灯函数同上
    return 1;
}

/*ioctl函数*/
int led_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    struct led_dev *dev = filp->private_data;   //同上在这里拿到自定义结构体
    switch (cmd)
    {
        case 1:
            dev->value = 1;
            led_on();
        break;
        case 0:
            dev->value = 0;
            led_off();
        break;
        default:                     //不支持的命令
            return -ENOTTY;
    }

}

/*file_operations结构体,通过赋值绑定操作函数*/
struct file_operation led_fops = 
{
    .read = led_read,
    .write = led_write,
    .unlock_ioctl = led_ioctl,
    .open = led_open,
    .release = led_release,
};

/*设置字符设备cdev结构体*/
static void led_setup_cdev(struct led_dev *dev, int devno)
{
    cdev_init(&dev->cdev, &led_fops);           //初始化cdev成员,建立与led_fops之间的连接
    cdev_add(&dev->cdev, devno, 1);             //注册设备,在此之前得有设备号。这里可进行容错处理
}

/*模块加载函数*/
int led_init(void)
{
    int result;
    result = alloc_chrdev_region(&dev, 0, 1, "LED");         //这里用动态方式申请设备号
    if (result < 0)
        return result;
    led_devp = kmalloc(sizeof(struct led_dev), GFP_KERNEL);  //申请动态内存
    led_setup_cdev(led_devp, result);                        
    led_gpio_init();                                         //初始化IO,这里省略
    return 0;
}

/*模块卸载函数*/
void led_cleanup(void)
{
    cdev_del(&led_dev->cdev);                                //删除设备
    kfree(led_devp);                                         //释放内存
    unregister_chrdev_region(result, 1);                     //注销设备号
}

module_init(led_init);                         //绑定加载函数
module_exit(led_exit);                         //绑定卸载函数

补充

在驱动针对单个设备的时候,open函数里面的私有数据可以直接赋值为全局的自定义结构体指针,在针对多个设备的时候,则应该使用如上所示的container_of(),因为当存在多个设备时,加载函数会申请的动态内存的大小为n个自定义结构体的空间,要找到目标结构体,用container_of比较快。至于为什么write、read等操作函数每次都需要从filp的私有数据里面拿到自定义结构体,这属于是Linux驱动的一个“潜规则”,私有数据的概念在Linux驱动的各个子系统中广泛存在,实际上体现了Linux面向对象的设计思想。


接下来理一下整个驱动程序的逻辑关系(红色代表自定义函数)

加载函数主要实现:

1.申请设备号 - alloc_chrdev_region()

2.为自定义结构体分配动态内存 - kmalloc()

3.对自定义结构体里的cdev进行初始化 - led_setup_cdev()

4.初始化IO - led_gpio_init()

cdev初始化函数主要实现:

1.初始化cdev - cdev_init()

2.添加一个字符设备 - cdev_add()

卸载函数主要实现:

1.注销字符设备 - cdev_del()

2.释放内存 - kfree()

3.注销设备号 - unregister_chrdev_region()

然后剩下的就是操作函数,还有变量、结构体的定义,程序中省略的部分就是对寄存器的操作,只要知道寄存器对应的虚拟地址,再进行位操作即可

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

雨天不打滑

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值