第1章 Hello 驱动(不涉及硬件操作)

1.1 APP 打开的文件在内核中如何表示

使用 open 打开文件时,传入的 flags、mode 等参数, 读写文件时,文件的当前偏移地址,对文件的操作api,都在 include\linux\fs.hstruct file 中。

struct file {
    union {
        struct llist_node   fu_llist;
        struct rcu_head     fu_rcuhead;
    } f_u;
    struct path     f_path;
    struct inode        *f_inode;   /* cached value */
    const struct file_operations    *f_op;

    /*
     * Protects f_ep_links, f_flags.
     * Must not be taken from IRQ context.
     */
    spinlock_t      f_lock;
    atomic_long_t       f_count;
    unsigned int        f_flags;
    fmode_t         f_mode;
    struct mutex        f_pos_lock;
    loff_t          f_pos;
    ...
}

1.2 打开字符设备节点时,内核中也有对应的 struct file

// include\linux\fs.h
struct file_operations {
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    ...
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
    ...
    int (*open) (struct inode *, struct file *);
    int (*flush) (struct file *, fl_owner_t id);
    int (*release) (struct inode *, struct file *);
    ...
}

1.3 请猜猜怎么编写驱动程序

  • 谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数
  • 有入口函数就应该有出口函数:卸载驱动程序时,出口函数调用 unregister_chrdev
  • file_operations 结构体告诉内核:register_chrdev
  • 确定主设备号,也可以让内核分配
  • 定义自己的 file_operations 结构体
  • 实现对应的 drv_open/drv_read/drv_write 等函数,填入 file_operations 结构体
  • 其他完善:提供设备信息,自动创建设备节点:class_create, device_create

1.4 编写代码

1.4.1 写驱动程序

  • 入口和出口空程序体
    #include <linux/module.h>
    // init
    static int __init hello_init(void)
    {
    }
    // exit
    static void __exit hello_exit(void)
    {
    }
    // 
    module_init(hello_init);
    module_exit(hello_exit);
    // license
    MODULE_LICENSE("GPL");
  • init 里面调用 drivers\base\core.c 中的 struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...) 。 其中 struct class, struct device 都是在 include/linux/device.h 里面定义的。 exit 里面调用相反的 void device_destroy(struct class *class, dev_t devt)
    #include <linux/device.h>
    //
    static int hello_major;
    static dev_t hello_devt;
    struct class * hello_class;
    struct device * hello_deivce;
    // init
    static int __init hello_init(void)
    {
    hello_devt = MKDEV(hello_major, 0);
    /**
    * device_create - creates a device and registers it with sysfs
    * @class: pointer to the struct class that this device should be registered to
    * @parent: pointer to the parent struct device of this new device, if any
    * @devt: the dev_t for the char device to be added
    * @drvdata: the data to be added to the device for callbacks
    * @fmt: string for the device's name
    *
    * This function can be used by char device classes.  A struct device
    * will be created in sysfs, registered to the specified class.
    *
    * A "dev" file will be created, showing the dev_t for the device, if
    * the dev_t is not 0,0.
    * If a pointer to a parent struct device is passed in, the newly created
    * struct device will be a child of that device in sysfs.
    * The pointer to the struct device will be returned from the call.
    * Any further sysfs files that might be required can be created using this
    * pointer.
    *
    * Returns &struct device pointer on success, or ERR_PTR() on error.
    *
    * Note: the struct class passed to this function must have previously
    * been created with a call to class_create().
    */
    hello_deivce = device_create(hello_class, NULL,
                 hello_devt, NULL, "hello")
    }
    // exit
    static void __exit hello_exit(void)
    {
    /**
    * device_destroy - removes a device that was created with device_create()
    * @class: pointer to the struct class that this device was registered with
    * @devt: the dev_t of the device that was previously registered
    *
    * This call unregisters and cleans up a device that was created with a
    * call to device_create().
    */
    device_destroy(hello_class, hello_devt);
    }
  • 根据 device_create 注释中说明的,需要先用 class_create(owner, name) , 在 include\linux\device.h 里面的宏. 还有对应的 class_destroy(struct class *cls) 。 当然,都是调用 drivers\base\class.c 里面的函数
    static int __init hello_init(void)
    {
    /**
    * class_create - create a struct class structure
    * @owner: pointer to the module that is to "own" this struct class
    * @name: pointer to a string for the name of this class.
    * @key: the lock_class_key for this class; used by mutex lock debugging
    *
    * This is used to create a struct class pointer that can then be used
    * in calls to device_create().
    *
    * Returns &struct class pointer on success, or ERR_PTR() on error.
    *
    * Note, the pointer created here is to be destroyed when finished by
    * making a call to class_destroy().
    */
    hello_class = class_create(THIS_MODULE, "hello");
    if (IS_ERR(hello_class)) {
        printk("%s %s line %d, error %d\n", __FILE__, __FUNCTION__, __LINE__, PTR_ERR(hello_class));
        return PTR_ERR(hello_class);
    }
    hello_devt = MKDEV(hello_major, 0);
    hello_deivce = device_create(hello_class, NULL,
                 hello_devt, NULL, "hello")
    if (IS_ERR(hello_deivce)) {
        printk("%s %s line %d, error %d\n", __FILE__, __FUNCTION__, __LINE__, PTR_ERR(hello_deivce));
        class_destroy(hello_class);
        return PTR_ERR(hello_deivce);
    }
    }
    //
    static void __exit hello_exit(void)
    {
    device_destroy(hello_class, hello_devt);
    class_destroy(hello_class);
    }
  • major 需要 include\linux\fs.h 中的 static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops) 来生成。
    #include <linux/fs.h>
    static struct file_operations * hello_fops;
    //
    static int __init hello_init(void)
    {
    /**
    * __register_chrdev() - create and register a cdev occupying a range of minors
    * @major: major device number or 0 for dynamic allocation
    * @baseminor: first of the requested range of minor numbers
    * @count: the number of minor numbers required
    * @name: name of this range of devices
    * @fops: file operations associated with this devices
    *
    * If @major == 0 this functions will dynamically allocate a major and return
    * its number.
    *
    * If @major > 0 this function will attempt to reserve a device with the given
    * major number and will return zero on success.
    *
    * Returns a -ve errno on failure.
    *
    * The name of this device has nothing to do with the name of the device in
    * /dev. It only helps to keep track of the different owners of devices. If
    * your module name has only one type of devices it's ok to use e.g. the name
    * of the module here.
    */
    hello_major = register_chrdev(0, "hello", hello_fops);
    if (hello_major < 0) {
        printk("%s %s line %d, error %d\n", __FILE__, __FUNCTION__, __LINE__, hello_major);
        return hello_major;
    }
    hello_class = class_create(THIS_MODULE, "hello");
    if (IS_ERR(hello_class)) {
        printk("%s %s line %d, error %d\n", __FILE__, __FUNCTION__, __LINE__, PTR_ERR(hello_class));
        unregister_chrdev(hello_major, "hello");
        return PTR_ERR(hello_class);
    }
    hello_devt = MKDEV(hello_major, 0);
    hello_deivce = device_create(hello_class, NULL,
                 hello_devt, NULL, "hello")
    if (IS_ERR(hello_deivce)) {
        printk("%s %s line %d, error %d\n", __FILE__, __FUNCTION__, __LINE__, PTR_ERR(hello_deivce));
        class_destroy(hello_class);
        unregister_chrdev(hello_major, "hello");
        return PTR_ERR(hello_deivce);
    }
    }
    //
    static void __exit hello_exit(void)
    {
    device_destroy(hello_class, hello_devt);
    class_destroy(hello_class);
    unregister_chrdev(hello_major, "hello");
    }
  • 初始化并填充 struct file_operations * hello_fops. 按照 include\linux\fs.h 中的 struct file_operations来。ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);, ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);, int (*open) (struct inode *, struct file *);, int (*release) (struct inode *, struct file *);.
    static int hello_buf[1024];
    //
    #define MIN(a, b) (a < b ? a : b)
    //
    static ssize_t hello_read (struct file * filp, char __user * buf, size_t count, loff_t * f_pos)
    {
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    int missing = copy_to_user(buf, hello_buf, MIN(1024, count));
    return MIN(1024, count) - missing;
    }
    //
    static ssize_t hello_write (struct file * filp, const char __user * buf, size_t count, loff_t * f_pos)
    {
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    int missing = copy_from_user(hello_buf, buf, MIN(1024, count));
    return MIN(1024, count) - missing;
    }
    //
    static int hello_open (struct inode * inode, struct file * filp)
    {
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    return 0;
    }
    //
    static int hello_release (struct inode * inode, struct file * filp)
    {
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    return 0;
    }
    //
    static struct file_operations hello_fops = {
    .owner =    THIS_MODULE,
    .write =    hello_write,
    .read =     hello_read,
    .open =     hello_open,
    .release =  hello_release,
    };
  • 根据开发板情况,修改 Makefile
    KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88
  • 根据编译情况,增加相关头文件
    #include <linux/uaccess.h>

注意:int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops); 是旧的设备驱动。新的设备驱动可以用 int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, const char *name); 配合 fs\char_dev.c include/linux/cdev.h 中的 void cdev_init(struct cdev *cdev, const struct file_operations *fops), int cdev_add(struct cdev *p, dev_t dev, unsigned count), 当然,设备卸载的时候,也需要对应的加上 void cdev_del(struct cdev *p), void unregister_chrdev_region(dev_t from, unsigned count)

注意: device_createclass_create 也不是必须的。最低要求其实就是 register_chrdevclass_create 的用处是 /sys/class/ 下生成目录,并且方便作为设备类,统一管理多个设备,还可以与 udev mdev 配合使用。 device_create 的作用是,可以自动在 /dev 下面创建设备节点,如果不用,那就需要使用 mknod 来手动创建节点。

使用alloc_chrdev_region 的代码如下:

static int __init hello_init(void)
{
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);

    if (alloc_chrdev_region(&hello_devt, 0, 1, "hello") < 0) {
        printk("%s %s line, error %d\n", __FILE__, __FUNCTION__, __LINE__);
        return -1;
    }

    cdev_init(&hello_cdev, &hello_fops);
    if (cdev_add(&hello_cdev, hello_devt, 1) < 0) {
        printk("%s %s line, error %d\n", __FILE__, __FUNCTION__, __LINE__);
        unregister_chrdev_region(hello_devt, 1);
        return -1;
    }

    hello_class = class_create(THIS_MODULE, "hello");
    if (IS_ERR(hello_class)) {
        printk("%s %s line %d, error %ld\n", __FILE__, __FUNCTION__, __LINE__, PTR_ERR(hello_class));
        cdev_del(&hello_cdev);
        //unregister_chrdev(hello_major, "hello");
        unregister_chrdev_region(hello_devt, 1);
        return PTR_ERR(hello_class);
    }

    hello_deivce = device_create(hello_class, NULL,
                 hello_devt, NULL, "hello");
    if (IS_ERR(hello_deivce)) {
        printk("%s %s line %d, error %ld\n", __FILE__, __FUNCTION__, __LINE__, PTR_ERR(hello_deivce));
        class_destroy(hello_class);
        cdev_del(&hello_cdev);
        //unregister_chrdev(hello_major, "hello");
        unregister_chrdev_region(hello_devt, 1);
        return PTR_ERR(hello_deivce);
    }

    return 0;
}

static void __exit hello_exit(void)
{
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    device_destroy(hello_class, hello_devt);
    class_destroy(hello_class);
    cdev_del(&hello_cdev);
    //unregister_chrdev(hello_major, "hello");
    unregister_chrdev_region(hello_devt, 1);
}

1.4.3 测试

  • echo "7 4 1 7" > /proc/sys/kernel/printk 显示内核打印信息
  • insmod hello_drv.ko 安装驱动, 或者 insmod -f hello_drv.ko
  • ls /dev/hello -l 查看设备节点
  • ./hello_drv_test -w www.100ask.net, ./hello_drv_test -r 测试
  • lsmod, rmmod hello_drv

第5章 最简单的 LED 驱动程序

5.2 最简单的 LED 驱动程序编程_基于 IMX6ULL

5.2.2 实现什么功能

  • 使用 static inline void __iomem *ioremap(phys_addr_t offset, size_t size) 把 io 映射到内存中,然后就可以直接读写内存来驱动硬件。 需要包含头文件 #include <asm/io.h>
  • 卸载时,用 iounmap 来取消映射。
  • 使用 ioremap 映射的变量,也需要用 volatile 进行修饰。另外这些变量,如果考虑内存屏障,优化等问题,推荐使用 iowrite, ioread 这些函数。

第6章 LED 驱动程序框架

6.3 LED 驱动能支持多个板子的基础:分层思想

6.4 写代码

6.4.1 驱动程序

  • 多个 led, 所以 init 和 exit 中,device_create 需要多个
    for (i = 0; i < LED_NUM; i++)
        device_create(led_class, NULL, MKDEV(major, i), NULL, "100ask_led%d", i); /* /dev/100ask_led0,1,... */
    for (i = 0; i < LED_NUM; i++)
        device_destroy(led_class, MKDEV(major, i)); /* /dev/100ask_led0,1,... */
  • 多个 led,在 open, write 等中,需要针对某个 led 进行操作,只能使用次设备号来分辨。有 inode 时,直接 int minor = iminor(node); 来获取次设备号。没有 inode 有文件名时,通过 struct inode *inode = file_inode(file); 先获取 inode,再获取次设备号。
    static ssize_t led_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
    {
    ...
    struct inode *inode = file_inode(file);
    int minor = iminor(inode);
    ...
    /* 根据次设备号和status控制LED */
    p_led_opr->ctl(minor, status);
    return 1;
    }
    //
    static int led_drv_open (struct inode *node, struct file *file)
    {
    int minor = iminor(node);
    ...
    /* 根据次设备号初始化LED */
    p_led_opr->init(minor);
    return 0;
    }

第7章 具体单板的 LED 驱动程序

7.2 百问网 IMX6ULL 的 LED 驱动程序

7.2.3 写程序

从板级文件中获取 led 数目,然后 device_create

static struct led_operations board_demo_led_opr = {
    .num  = 1,
    .init = board_demo_led_init,
    .ctl  = board_demo_led_ctl,
};
    p_led_opr = get_board_led_opr();
    for (i = 0; i < p_led_opr->num; i++)
        device_create(led_class, NULL, MKDEV(major, i), NULL, "100ask_led%d", i); /* /dev/100ask_led0,1,... */
    for (i = 0; i < p_led_opr->num; i++)
        device_destroy(led_class, MKDEV(major, i)); /* /dev/100ask_led0,1,... */

第8章 驱动设计的思想

8.3 分离

8.4 写示例代码

把具体的 gpio 放在一个 resource.h 的头文件中。这种方法适合于专门做模块的或者适配多种开发板。 对于普通做产品的,这种方法不合算。

第9章 驱动进化之路:总线设备驱动模型

9.2 在 Linux 中实现“分离”:Bus/Dev/Drv 模型

主要涉及 include\linux\platform_device.h 中的 struct platform_device, struct platform_driver, 和 include\linux\device.h 中的 struct bus_type.

// include\linux\platform_device.h
struct platform_device {
    const char  *name;
    int     id;
    bool        id_auto;
    struct device   dev;
    u32     num_resources;
    struct resource *resource;

    const struct platform_device_id *id_entry;
    char *driver_override; /* Driver name to force a match */

    /* MFD cell pointer */
    struct mfd_cell *mfd_cell;

    /* arch specific additions */
    struct pdev_archdata    archdata;
};
// include\linux\platform_device.h
struct platform_driver {
    int (*probe)(struct platform_device *);
    int (*remove)(struct platform_device *);
    void (*shutdown)(struct platform_device *);
    int (*suspend)(struct platform_device *, pm_message_t state);
    int (*resume)(struct platform_device *);
    struct device_driver driver;
    const struct platform_device_id *id_table;
    bool prevent_deferred_probe;
};
// include\linux\device.h
struct bus_type {
    const char      *name;
    const char      *dev_name;
    struct device       *dev_root;
    struct device_attribute *dev_attrs; /* use dev_groups instead */
    const struct attribute_group **bus_groups;
    const struct attribute_group **dev_groups;
    const struct attribute_group **drv_groups;

    int (*match)(struct device *dev, struct device_driver *drv);
    int (*uevent)(struct device *dev, struct kobj_uevent_env *env);
    int (*probe)(struct device *dev);
    int (*remove)(struct device *dev);
    void (*shutdown)(struct device *dev);

    int (*online)(struct device *dev);
    int (*offline)(struct device *dev);

    int (*suspend)(struct device *dev, pm_message_t state);
    int (*resume)(struct device *dev);

    const struct dev_pm_ops *pm;

    const struct iommu_ops *iommu_ops;

    struct subsys_private *p;
    struct lock_class_key lock_key;
};

9.3 匹配规则

device 和 driver 匹配,按照优先顺序如下:

  1. 强行匹配: platform_device.driver_override = platform_driver.driver.name
  2. 列表匹配: platform_device.name = platform_driver.platform_device_id.name[]
  3. 名称匹配: platform_device.name = platform_driver.driver.name

9.3.4 函数调用关系

9.4 常用函数

9.4.1 注册/反注册

  • platform_device_register platform_device_unregister
  • platform_driver_register platform_driver_unregister
  • platform_add_devices

9.4.2 获得资源

  • struct resource *platform_get_resource(struct platform_device *dev, unsigned int type, unsigned int num) 返回该 dev 中某类型(type)资源中的第几个(num)
  • int platform_get_irq(struct platform_device *dev, unsigned int num) 返回该 dev 所用的第几个(num)中断
  • struct resource *platform_get_resource_byname(struct platform_device *dev, unsigned int type, const char *name) 通过名字(name)返回该 dev 的某类型(type)资源
  • int platform_get_irq_byname(struct platform_device *dev, const char *name) 通过名字(name)返回该 dev 的中断号

第10章 LED 模板驱动程序的改造:总线设备驱动模型

10.2 要实现的框架

10.3 写代码

10.3.1 注意事项

  • platform_device.dev.release 必须提供,否则在卸载时,会有警告
  • 供给其他模块调用的函数,也需要用 EXPORT_SYMBOL(), 并且模块需要先加载。

10.3.2 实现 platform_device 结构体

  • resource.start 可以用来存放数据,
    static struct resource resources[] = {
        {
                .start = GROUP_PIN(3,1),
                .flags = IORESOURCE_IRQ,
                .name = "100ask_led_pin",
        },
    };
  • resources 元素个数,可以用 ARRAY_SIZE()
    static struct platform_device board_A_led_dev = {
        .name = "100ask_led",
        .num_resources = ARRAY_SIZE(resources),
        .resource = resources,
        .dev = {
                .release = led_dev_release,
         },
    };
  • 加载和卸载函数,直接调用 platform_device_register(&board_A_led_dev), platform_device_unregister(&board_A_led_dev)

10.3.3 实现 platform_driver 结构体

  • platform_driver 很简单
    static struct platform_driver chip_demo_gpio_driver = {
    .probe      = chip_demo_gpio_probe,
    .remove     = chip_demo_gpio_remove,
    .driver     = {
        .name   = "100ask_led",
    },
    };
  • 加载,使用 platform_driver_register(&chip_demo_gpio_driver)
  • 卸载,使用 platform_driver_unregister(&chip_demo_gpio_driver)
  • chip_demo_gpio_probe 中 使用 platform_get_resource(pdev, IORESOURCE_IRQ, i++) 获取 device 中的 resource,然后调用 device_create 函数
  • chip_demo_gpio_remove 中使用 platform_get_resource(pdev, IORESOURCE_IRQ, i); 获取 resource ,然后调用 device_destroy
  • 从框架上来看,device 是提供下层资源,driver 是实际的去添加设备,并且把相关的操作资源等等提供给上层的 led_drv. led_drv 做更加靠近上层的 open, write 之类的。

第11章 驱动进化之路:设备树的引入及简明教程

11.1 设备树的引入与作用

11.2.1 Devicetree 格式

  • DTS 文件的格式
    /dts-v1/;                // 表示版本 
    [memory reservations]    // 格式为: /memreserve/ <address> <length>; 
    / { 
    [property definitions] 
    [child nodes] 
    }; 
  • node 的格式
    [label:] node-name[@unit-address] { 
    [properties definitions] 
    [child nodes] 
    }; 
    /dts-v1/; 
    / { 
    // label 是标号,可以省略。label 的作用是为了方便地引用 node
    uart0: uart@fe001000 { 
        compatible="ns16550"; 
        reg=<0xfe001000 0x100>; 
    }; 
    }; 
    // 在根节点之外使用 label 引用 node: 
    &uart0 { 
    status = “disabled”; 
    }; 
    // 或在根节点之外使用全路径: 
    &{/uart@fe001000}  { 
    status = “disabled”; 
    }; 
  • properties 的格式
    [label:] property-name = value; 
    [label:] property-name; 
  • Property 取值只有 3 种, arrays of cells(1 个或多个 32 位数据, 64 位数据使用 2 个 32 位数据表示), string(字符串), bytestring(1 个或多个字节)
    // Arrays of cells : cell 就是一个 32 位的数据,用尖括号包围起来
    interrupts = <17 0xc>;
    // 64bit 数据使用 2 个 cell 来表示,用尖括号包围起来: 
    clock-frequency = <0x00000001 0x00000000>;
    // A null-terminated string (有结束符的字符串),用双引号包围起来
    compatible = "simple-bus"; 
    //  A bytestring(字节序列)  ,用中括号包围起来:
    local-mac-address = [00 00 12 34 56 78];  // 每个 byte 使用 2 个 16 进制数来表示 
    local-mac-address = [000012345678];       // 每个 byte 使用 2 个 16 进制数来表示 
    // 可以是各种值的组合, 用逗号隔开: 
    compatible = "ns16550", "ns8250"; 
    example = <0xf00f0000 19>, "a strange property format"; 

11.2.2 dts 文件包含 dtsi 文件

  • 设备树文件不需要我们从零写出来,内核支持了某款芯片比如 imx6ull,在内核的 arch/arm/boot/dts 目录下就有了能用的设备树模板,一般命名为 xxxx.dtsi。“i”表示“include”,被别的文件引用的。
  • dtsi 文件跟 dts 文件的语法是完全一样的
  • dts 中可以包含.h 头文件,也可以包含 dtsi 文件,在.h 头文件中可以定义一些宏。
    /dts-v1/; 
    #include <dt-bindings/input/input.h> 
    #include "imx6ull.dtsi" 
    //
    / { 
    …… 
    }; 

11.2.3 常用的属性

  • #address-cells#size-cells 地址和大小. address 要用多少个 32 位数来表示, size 要用多少个 32 位数来表示
    // address-cells 为 1,所以 reg 中用 1 个数来表示地址,即用 0x80000000 来表示地址;
    // size-cells 为 1,所以 reg 中用 1 个数来表示大小,即用 0x20000000 表示大小
    / { 
    #address-cells = <1>; 
    #size-cells = <1>; 
    memory { 
    reg = <0x80000000 0x20000000>; 
    }; 
    }; 
  • “compatible”表示“兼容”,对于某个 LED,内核中可能有 A、B、C 三个驱动都支持它,那可以这样写
    // compatible 的值,建议取这样的形式:"manufacturer,model",即“厂家名,模块名”
    led { 
    compatible = “A”, “B”, “C”; 
    }; 
  • model 用来准确地定义这个硬件是什么。
    { 
    compatible = "samsung,smdk2440", "samsung,mini2440"; 
    model = "jz2440_v3"; 
    }; 
  • dtsi 文件中定义了很多设备,但是在你的板子上某些设备是没有的。这时你可以给这个设备节点添加一个 status 属性,设置为“disabled”
    // okay, disabled, fail, fail-xxx
    &uart1 { 
      status = "disabled"; 
    }; 
  • reg 用来表示寄存器地址, 在设备树里,它可以用来描述一段空间。反正对于 ARM 系统,寄存器和内存是统一编址的,即访问寄存器时用某块地址,访问内存时用某块地址,在访问方法上没有区别。reg 属性的值,是一系列的“address size”,用多少个 32 位的数来表示address 和 size,由其父节点的#address-cells、#size-cells 决定。
    /dts-v1/; 
    / { 
    #address-cells = <1>; 
    #size-cells = <1>;  
    memory { 
    reg = <0x80000000 0x20000000>; 
    }; 
    }; 
  • name(过时了,建议不用)在跟 platform_driver 匹配时,优先级最低。compatible 属性在匹配过程中,优先级最高。
  • device_type(过时了,建议不用)它的值是字符串,用来表示节点的类型。在跟 platform_driver 匹配时,优先级为中。

11.2.4 常用的节点(node)

  • 根节点 dts 文件中必须有一个根节点
    /dts-v1/; 
    / { 
    model = "SMDK24440"; 
    compatible = "samsung,smdk2440"; 
    #address-cells = <1>; 
    #size-cells = <1>;  
    }; 
  • CPU 节点 一般不需要我们设置,在 dtsi 文件中都定义好了
  • memory 节点, 芯片厂家不可能事先确定你的板子使用多大的内存,所以 memory 节点需要板厂设置
    memory { 
    reg = <0x80000000 0x20000000>; 
    };
  • chosen 节点, 我们可以通过设备树文件给内核传入一些参数,这要在 chosen 节点中设置 bootargs 属性
    chosen { 
    bootargs = "noinitrd root=/dev/mtdblock4 rw init=/linuxrc console=ttySAC0,115200"; 
    }; 

11.3 编译、更换设备树

11.3.1 在内核中直接 make

内核源码的目录 make dtbs V=1

11.3.2 手工编译

  • 除非你对设备树比较了解,否则不建议手工使用 dtc 工具直接编译。内核目录下 scripts/dtc/dtc 是设备树的编译工具,直接使用它的话,包含其他文件时不能使用“#include”,而必须使用“/incldue”.
  • 编译、反编译的示例命令如下,“-I”指定输入格式,“-O”指定输出格式,“-o”指定输出文件
    ./scripts/dtc/dtc -I dts -O dtb -o tmp.dtb arch/arm/boot/dts/xxx.dts  // 编译 dts 为 dtb 
    ./scripts/dtc/dtc -I dtb -O dts -o tmp.dts arch/arm/boot/dts/xxx.dtb  // 反编译 dtb 为dts 

11.3.3 给开发板更换设备树文件

内核源码目录中执行 make dtbs

11.3.4 板子启动后查看设备树

ls /sys/firmware/

  • /sys/firmware/devicetree 目录下是以目录结构程现的 dtb 文件, 根节点对应 base 目录, 每一个节点对应一个目录, 每一个属性对应一个文件。属性的值如果是字符串,可以使用 cat 命令把它打印出来;对于数值,可以用 hexdump 把它打印出来。
  • /sys/firmware/fdt 文件,它就是 dtb 格式的设备树文件,可以把它复制出来放到 ubuntu 上,执行下面的命令反编译出来(-I dtb:输入格式是 dtb,-O dts:输出格式是 dts)
    cd  板子所用的内核源码目录 
    ./scripts/dtc/dtc  -I  dtb  -O  dts   /从板子上/复制出来的/fdt  -o   tmp.dts 

11.4 内核对设备树的处理

  • dts 在 PC 机上被编译为 dtb 文件;
  • u-boot 把 dtb 文件传给内核;
  • 内核解析 dtb 文件,把每一个节点都转换为 device_node 结构体;
  • 对于某些 device_node 结构体,会被转换为 platform_device 结构体。

11.4.1 dtb 中每一个节点都被转换为 device_node 结构体

  • 数值保存在 device_node.property
  • 根节点被保存在全局变量 of_root 中,从 of_root 开始可以访问到任意节点。

11.4.2 哪些设备树节点会被转换为 platform_device

  • 根节点下含有 compatile 属性的子节点
  • 含有特定 compatile 属性的节点的子节点
  • 总线 I2C、SPI 节点下的子节点:不转换为 platform_device

11.4.3 怎么转换为 platform_device

  • platform_device 中含有 resource 数组, 它来自 device_node 的 reg, interrupts 属性;
  • platform_device.dev.of_node 指向 device_node, 可以通过它获得其他属性

11.5 platform_device 如何与 platform_driver 配对

从设备树转换得来的 platform_device 会被注册进内核里,以后当我们每注册一个 platform_driver 时,它们就会两两确定能否配对,如果能配对成功就调用 platform_driver 的 probe 函数。

// drivers\base\platform.c
static int platform_match(struct device *dev, struct device_driver *drv)
{
    struct platform_device *pdev = to_platform_device(dev);
    struct platform_driver *pdrv = to_platform_driver(drv);

    /* When driver_override is set, only bind to the matching driver */
    if (pdev->driver_override)
        return !strcmp(pdev->driver_override, drv->name);

    /* Attempt an OF style match first */
    if (of_driver_match_device(dev, drv))
        return 1;

    /* Then try ACPI style match */
    if (acpi_driver_match_device(dev, drv))
        return 1;

    /* Then try to match against the id table */
    if (pdrv->id_table)
        return platform_match_id(pdrv->id_table, pdev) != NULL;

    /* fall-back to driver name match */
    return (strcmp(pdev->name, drv->name) == 0);
}

从上面的源码,可以很明显的看出,设备树是第二个优先级的匹配。

11.5.2 然后比较:设备树信息

platform_device.dev.of_node = platform_driver.driver.of_match_table

// platform_device.dev.of_node
// include\linux\of.h
struct device_node {
    const char *name; 
    const char *type;  // device_type
    ...
    struct  property *properties; // compatible
};
// platform_driver.driver.of_match_table
// include\linux\mod_devicetable.h
struct of_device_id {
    char    name[32];
    char    type[32];
    char    compatible[128];
    const void *data;
};

首先,比较 compatible, 然后是 type, 最后是 name. 设备树中建议不再使用 devcie_type 和 name 属性,所以基本上只使用设备节点的 compatible 属性来寻找匹配的 platform_driver。

11.7 内核里操作设备树的常用函数

内核源码中 include/linux/目录下有很多 of 开头的头文件,of 表示“open firmware”即开放固件。

11.7.1 内核中设备树相关的头文件介绍

设备树的处理过程是:dtb -> device_node -> platform_device。

  • 处理 DTB
    of_fdt.h           // dtb 文件的相关操作函数, 我们一般用不到,  
    // 因为 dtb 文件在内核中已经被转换为 device_node 树(它更易于使用) 
  • 处理 device_node
    of.h               // 提供设备树的一般处理函数,  
    // 比如 of_property_read_u32(读取某个属性的 u32 值), 
    // of_get_child_count(获取某个 device_node 的子节点数) 
    of_address.h       // 地址相关的函数,  
    // 比如 of_get_address(获得 reg 属性中的 addr, size 值) 
    // of_match_device (从 matches 数组中取出与当前设备最匹配的一项) 
    of_dma.h           // 设备树中 DMA 相关属性的函数 
    of_gpio.h          // GPIO 相关的函数 
    of_graph.h         // GPU 相关驱动中用到的函数, 从设备树中获得 GPU 信息 
    of_iommu.h         // 很少用到 
    of_irq.h           // 中断相关的函数 
    of_mdio.h          // MDIO (Ethernet PHY) API 
    of_net.h           // OF helpers for network devices.  
    of_pci.h           // PCI 相关函数 
    of_pdt.h           // 很少用到 
    of_reserved_mem.h  // reserved_mem 的相关函数 
  • 处理 platform_device
    of_platform.h      // 把 device_node 转换为 platform_device 时用到的函数,  
                   // 比如 of_device_alloc(根据 device_node 分配设置 platform_device),  
                   // of_find_device_by_node (根据 device_node 查找到 platform_device), 
                   //   of_platform_bus_probe (处理 device_node 及它的子节点) 
    of_device.h        // 设备相关的函数, 比如 of_match_device 

11.7.2 platform_device 相关的函数

  • of_find_device_by_node extern struct platform_device *of_find_device_by_node(struct device_node *np); 设备树中的每一个节点,在内核里都有一个 device_node;你可以使用device_node 去找到对应的 platform_device。
  • platform_get_resource struct resource *platform_get_resource(struct platform_device *dev, unsigned int type, unsigned int num) 这个函数跟设备树没什么关系, 但是设备树中的节点被转换为platform_device 后,设备树中的 reg 属性、interrupts 属性也会被转换为“resource”. 这时,你可以使用这个函数取出这些资源。
    // 对于设备树节点中的 reg 属性,它对应 IORESOURCE_MEM 类型的资源
    // 对于设备树节点中的 interrupts 属性,它对应 IORESOURCE_IRQ 类型的资源。

11.7.3 有些节点不会生成 platform_device,怎么访问它们

  • 找到节点 , of_find_node_by_path of_find_node_by_name of_find_node_by_type of_find_compatible_node of_find_node_by_phandle of_get_parent of_get_next_parent of_get_next_child of_get_next_available_child of_get_child_by_name
  • 找到属性, of_find_property struct property *of_find_property(const struct device_node *np, const char *name, int *lenp);
  • 获取属性的值, of_get_property of_property_count_elems_of_size of_property_read_u32 of_property_read_u64 of_property_read_u32_index of_property_read_variable_u8_array of_property_read_variable_u16_array of_property_read_variable_u32_array of_property_read_variable_u64_array of_property_read_string

11.8 怎么修改设备树文件

  • 有些芯片,厂家提供了对应的设备树生成工具,可以选择某个引脚用于某些功能,就可以自动生成设备树节点。
  • 参考内核文档 Documentation/devicetree/bindings/

第12章 LED 模板驱动程序的改造:设备树

12.2 怎么使用设备树写驱动程序

12.2.1 设备树节点要与 platform_driver 能匹配

12.2.2 设备树节点指定资源,platform_driver 获得资源

  • 如果在设备树节点里使用 interrupts 属性 , 那么内核生成对应的 platform_device 时会用 reg 属性来设置 IORESOURCE_IRQ 类型的资源。对于interrupts 属性,内核会检查它的有效性,所以不建议在设备树里使用该属性来表示其他资源。
  • 可以从 platform_device 中得到 device_node,再用 of_property_read_u32 得到属性的值
    #define GROUP_PIN(g,p) ((g<<16) | (p)) 
    100ask_led0 { 
    compatible = “100ask,led”; 
    pin = <GROUP_PIN(5, 3)>; 
    }; 
    struct  device_node* np = pdev->dev. of_node; 
    int led_pin; 
    int err = of_property_read_u32(np, “pin”, &led_pin); 

12.3 开始编程

12.3.1 修改设备树添加 led 设备节点

~/100ask_imx6ull-sdk/Linux-4.9.88/arch/arm/boot/dts/100ask_imx6ull-14x14.dts

linux 编译dts 受控于arch/<arch>/boot/dts/Makefile 里面的 dtb-$(CONFIG_<SOC>), dtb-$(CONFIG_<SOC>) 需要去 .config 里面去查看。使用 make dtbs 会编译所有使能的 dts。编译后的 dtb 文件在 arch/<arch>/boot/dts/ 中。

如果想要单独编译某个 dts, make arch/arm/boot/dts/sun8i-h3-nanopi-neo.dtb

#define GROUP_PIN(g,p) ((g<<16) | (p)) 
model = "Freescale i.MX6 ULL 14x14 EVK Board ptz";
100ask_led@0 { 
  compatible = "100ask,leddrv"; 
  pin = <GROUP_PIN(3, 1)>; 
}; 
 //
100ask_led@1 { 
  compatible = "100ask,leddrv"; 
  pin = <GROUP_PIN(5, 8)>; 
}; 
  • 修改之后,make dtbs 编译设备树
  • 查看当前设备加载的设备树,dmesg | grep "OF: fdt:" 可以看到设备树的 model 信息。
  • 备份好 /boot 下面原来的 dtb,然后把新修改的 dtb 复制替换。
  • reboot 然后,再用 dmesg | grep "OF: fdt:" 查看,是否加载新的 dtb 了。

12.3.2 修改 platform_driver 的源码

static const struct of_device_id ask100_leds[] = {
    { .compatible = "100ask,leddrv" },
    { },
};
 //
static struct platform_driver chip_demo_gpio_driver = {
    .probe      = chip_demo_gpio_probe,
    .remove     = chip_demo_gpio_remove,
    .driver     = {
        .name   = "100ask_led",
        .of_match_table = ask100_leds,
    },
};

probe remove 函数中,也修改为从设备树获取。而且因为每匹配一次,就 probe 一下,所以 probe 里面的 while 循环也可以去掉了。 remove 也是一样。

static int chip_demo_gpio_probe(struct platform_device *pdev)
{
    struct device_node *np;
    int err = 0;
    int led_pin;

    np = pdev->dev.of_node;
    if (!np)
        return -1;

    err = of_property_read_u32(np, "pin", &led_pin);

    g_ledpins[g_ledcnt] = led_pin;
    led_class_create_device(g_ledcnt);
    g_ledcnt++;

    return 0;

}

第15章 具体单板的按键驱动程序(查询方式)

15.2 百问网 IMX6ULL 的按键驱动程序(查询方式)

15.2.3 编程

  • gpio 相关的放结构体中,ioremap 针对整个结构体。

第16章 GPIO 和 Pinctrl 子系统的使用

  • 内核 Documentation\devicetree\bindings\Pinctrl\Pinctrl-bindings.txt
  • 内核 Documentation\gpio\Pinctrl-bindings.txt
  • 内核 Documentation\devicetree\bindings\gpio\gpio.txt

16.1 Pinctrl 子系统重要概念

16.1.1 引入

  • Pinctrl 管理引脚复用和配置
  • gpio 管理 gpio 模块。

16.1.2 重要概念

  • 内核 Documentation\devicetree\bindings\Pinctrl\Pinctrl-bindings.txt
  • pin controller 用它来复用引脚、配置引脚。它是一个软件上的概念,你可以认为它对应 IOMUX──用来复用引脚,还可以配置引脚(比如上下拉电阻等)。
  • pin controllerGPIO Controller 不是一回事, 先用 pin controller 把引脚配置为 GPIO,再用 GPIO Controler 把引脚配置为输入或输出。
  • client device 声明自己要使用哪些引脚的哪些功能,怎么配置它们。就是使用 Pinctrl 系统的设备,使用引脚的设备。它在设备树里会被定义为一个节点,在节点里声明要用哪些引脚。

  • pin state, 对于一个“client device”来说,比如对于一个 UART 设备,它有多个“状态”:default、sleep 等,那对应的引脚也有这些状态。
  • groups 和 function:一个设备会用到一个或多个引脚,这些引脚就可以归为一组(group); 这些引脚可以复用为某个功能:function。
  • Generic pin multiplexing node 和 Generic pin configuration node , 可 以 用 来 描 述 复 用 信 息 : 哪 组 (group) 引 脚 复 用 为 哪 个 功 能(function); 可以用来描述配置信息:哪组(group)引脚配置为哪个设置功能(setting),比如上拉、下拉等。pin controller 节点的格式,没有统一的标准!!!!每家芯片都不一样。 甚至上面的 group、function 关键字也不一定有,但是概念是有的。

16.1.4 代码中怎么引用 pinctrl

  • 这是透明的,我们的驱动基本不用管。当设备切换状态时,对应的 pinctrl就会被调用。非要自己调用,也有函数:
    devm_pinctrl_get_select_default(struct device *dev);      // 使用"default"状态的引脚 
    pinctrl_get_select(struct device *dev, const char *name); // 根据 name 选择某种状态的引脚 
    pinctrl_put(struct pinctrl *p);   // 不再使用, 退出时调用 

16.2 GPIO 子系统重要概念

16.2.1 引入

  • 在设备树里指定 GPIO 引脚
  • 在驱动代码中:使用 GPIO 子系统的标准函数获得 GPIO、设置 GPIO 方向、读取/设置 GPIO 值。

16.2.2 在设备树中指定引脚

  • “GPIO 组”就是一个 GPIO Controller,这通常都由芯片厂家设置好。我们要做的是找到它名字,比如“gpio1”,然后指定要用它里面的哪个引脚,比如<&gpio1 0>。dtsi 定义,dts 中使用。

                        gpio1: gpio@0209c000 {
                                compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
                                reg = <0x0209c000 0x4000>;
                                interrupts = <GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH>,
                                             <GIC_SPI 67 IRQ_TYPE_LEVEL_HIGH>;
                                gpio-controller;
                                #gpio-cells = <2>;
                                interrupt-controller;
                                #interrupt-cells = <2>;
                        };
    
    &usdhc1 {
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_usdhc1>;
    cd-gpios = <&gpio1 19 GPIO_ACTIVE_LOW>;
    keep-power-in-suspend;
    enable-sdio-wakeup;
    bus-width = <4>;
    status = "okay";
    };
  • 我们暂时只需要关心里面的这 2 个属性, “gpio-controller” 表示这个节点是一个 GPIO Controller,它下面有很 多引脚。 “#gpio-cells = <2>” 表示这个控制器下每一个引脚要用 2 个 32 位的数(cell)来描述。普遍的用法是,用第 1 个 cell 来表示哪一个引脚,用第 2 个 cell 来表示有效电平

16.2.3 在驱动代码中调用 GPIO 子系统

GPIO 子系统有两套接口:基于描述符的(descriptor-based)、老的(legacy)。前者的函数都有前缀“gpiod_”,它使用 gpiodesc 结构体来表示一个引脚;后者的函数都有前缀“gpio”,它使用一个整数来表示一个引脚。

  • 驱动程序中要包含头文件, gpiod 或 gpio
    #include <linux/gpio/consumer.h>   // descriptor-based 
    #include <linux/gpio.h>            // legacy 
  • 常用函数 获得 GPIO

    descriptor-based legacy
    gpiod_get gpio_request
    gpiod_get_index
    gpiod_get_array gpio_request_array
    devm_gpiod_get
    devm_gpiod_get_index
    devm_gpiod_get_array
  • 常用函数 设置方向

    descriptor-based legacy
    gpiod_direction_input gpio_direction_input
    gpiod_direction_output gpio_direction_output
  • 常用函数 读值、写值

    descriptor-based legacy
    gpiod_get_value gpio_get_value
    gpiod_set_value gpio_set_value
  • 常用函数 释放 GPIO

    descriptor-based legacy
    gpio_free gpio_free
    gpiod_put gpio_free_array
    gpiod_put_array
    devm_gpiod_put
    devm_gpiod_put_array
  • 有前缀“devm”的含义是“设备资源管理”(Managed Device Resource),这是一种自动释放资源的机制。它的思想是“资源是属于设备的,设备不存在时资源就可以自动释放”。如果使用 devm 的相关函数,在内存申请失败时可以直接返回:设备的销毁函数会自动地释放已经申请了的 GPIO 资源。建议使用“devm”版本的相关函数。
  • 使用下面的函数获得引脚例子
    foo_device { 
    compatible = "acme,foo"; 
    ... 
    led-gpios = <&gpio 15 GPIO_ACTIVE_HIGH>, /* red */ 
          <&gpio 16 GPIO_ACTIVE_HIGH>, /* green */ 
          <&gpio 17 GPIO_ACTIVE_HIGH>; /* blue */ 
    power-gpios = <&gpio 1 GPIO_ACTIVE_LOW>; 
    }; 
    struct gpio_desc *red, *green, *blue, *power; 
    //
    red = gpiod_get_index(dev, "led", 0, GPIOD_OUT_HIGH); 
    green = gpiod_get_index(dev, "led", 1, GPIOD_OUT_HIGH); 
    blue = gpiod_get_index(dev, "led", 2, GPIOD_OUT_HIGH); 
    power = gpiod_get(dev, "power", GPIOD_OUT_HIGH); 

注意:gpiod_set_value 设置的值是“逻辑值”,不一定等于物理值。如何设备树中设置为低电平使能,那么 gpiod_set_value(desc, 0) 会让实际的电平为高电平。

  • 旧的“gpio_”函数没办法根据设备树信息获得引脚,它需要先知道引脚号。

16.2.4 sysfs 中的访问方法

在 sysfs 中访问 GPIO,实际上用的就是引脚号,老的方法。先确定某个 GPIO Controller 的基准引脚号(base number),再计算出某个引脚的号码。

  • 先在开发板的 /sys/class/gpio 目录下,找到各个 gpiochipXXX 目录
  • 然后进入某个 gpiochip 目录,查看文件 label 的内容
  • 根据 label 的内容对比设备树,比如 gpiochip96 的 label 为 20a800.gpio, 设备树中 gpio4: gpio@20a800. 所以,通过 20a800 把 gpio4 和 96 联系起来,gpio4 的基准引脚号就是 96,可以通过 base 内容核对。
  • GPIO4_14 的引脚号码是 96+14=110,相关操作是:
    // 输入引脚
    [root@100ask:~]# echo  110 > /sys/class/gpio/export 
    [root@100ask:~]# echo in > /sys/class/gpio/gpio110/direction 
    [root@100ask:~]# cat /sys/class/gpio/gpio110/value 
    [root@100ask:~]# echo  110 > /sys/class/gpio/unexport 
    // 输出引脚,假设引脚号为 N,可以用下面的方法设置它的值为 1:
    [root@100ask:~]# echo  N > /sys/class/gpio/export 
    [root@100ask:~]# echo out > /sys/class/gpio/gpioN/direction 
    [root@100ask:~]# echo 1 > /sys/class/gpio/gpioN/value 
    [root@100ask:~]# echo  N > /sys/class/gpio/unexport 

16.3 基于 GPIO 子系统的 LED 驱动程序

16.3.1 编写思路

16.3.3 在设备树中添加 GPIO 信息

  • 在设备树中指定:添加”[name]-gpios”属性,指定使用的是哪一个 GPIO Controller 里的哪一个引脚,还有其他 Flag 信息,比如 GPIO_ACTIVE_LOW 等。
  • 具体需要多少个 cell 来描述一个引脚,需要查看设备树中这个 GPIO Controller 节点里的“#gpio-cells”属性值,也可以查看内核文档。

16.4 在 100ASK_IMX6ULL 上机实验

16.4.1 确定引脚并生成设备树节点

NXP 公司对于 IMX6ULL 芯片,有设备树生成工具。

  • Pinctrl 信息:
    &iomuxc_snvs { 
    …… 
        myled_for_gpio_subsys: myled_for_gpio_subsys{  
            fsl,pins = < 
                MX6ULL_PAD_SNVS_TAMPER3__GPIO5_IO03        0x000110A0 
            >; 
        }; 
  • 设备节点信息(放在根节点下):
        myled { 
            compatible = "100ask,leddrv"; 
            pinctrl-names = "default"; 
            pinctrl-0 = <&myled_for_gpio_subsys>; 
            led-gpios = <&gpio5 3 GPIO_ACTIVE_LOW>; 
        }; 

16.4.2 编译程序

  • /sys/firmware/devicetree/base/led_100ask 这个和设备树中,新的设备节点名称对应
    led_100ask {
        compatible = "100ask,leddrv";
        pinctrl-names = "default";
        pinctrl-0 = <&gpio_leds_100ask>;
        led-gpios = <&gpio5 3 GPIO_ACTIVE_LOW>;
    };
  • /sys/class/100ask_led_class/100ask_led0
  • /sys/devices/soc0/led_100ask
  • /sys/devices/soc0/led_100ask/driver
  • /sys/devices/soc0/led_100ask/driver/led_100ask/of_node

第18章 Linux 系统对中断的处理

18.2 Linux 系统对中断处理的演进

Linux 系统中有硬件中断,也有软件中断。对硬件中断的处理有 2 个原则:不能嵌套,越快越好。

18.2.1 Linux 对中断的扩展:硬件中断、软件中断

// include\linux\interrupt.h
enum
{
    HI_SOFTIRQ=0,
    TIMER_SOFTIRQ,
    NET_TX_SOFTIRQ,
    NET_RX_SOFTIRQ,
    BLOCK_SOFTIRQ,
    IRQ_POLL_SOFTIRQ,
    TASKLET_SOFTIRQ,
    SCHED_SOFTIRQ,
    HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on the numbering. Sigh! */
    RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */
    NR_SOFTIRQS
};
  • 触发软件中断 void raise_softirq(unsigned int nr);
  • 设置软件中断的处理函数 void open_softirq(int nr, void (*action)(struct softirq_action *));

18.2.4 要处理的事情实在太多,拆分为:上半部、下半部

  • 在 handler 函数里只做紧急的事,然后就重新开中断,让系统得以正常运行;那些不紧急的事,以后再处理,处理时是开中断的。
  • 中断下半部的实现有很多种方法,讲 2 种主要的:tasklet(小任务)、work queue(工作队列)。

18.2.5 下半部要做的事情耗时不是太长:tasklet

  • 硬件中断 A 处理过程中,没有其他中断发生:上述流程图①~⑨依次执行,上半部、下半部的代码各执行一次。
  • 硬件中断 A 处理过程中,又再次发生了中断 A:同一个中断的上半部、下半部,在执行时是多对一的关系。
  • 硬件中断 A 处理过程中,又再次发生了中断 B:多个中断的下半部,是汇集在一起处理的。

总结:

  • 中断上半部,用来处理紧急的事,它是在关中断的状态下执行的
  • 中断下半部,用来处理耗时的、不那么紧急的事,它是在开中断的状态下执行的
  • 中断下半部执行时,有可能会被多次打断,有可能会再次发生同一个中断
  • 中断上半部执行完后,触发中断下半部的处理
  • 中断上半部、下半部的执行过程中,不能休眠

18.2.6 下半部要做的事情太多并且很复杂:工作队列

如果中断要做的事情实在太耗时,那就不能用软件中断来做,而应该用内核线程来做:在中断上半部唤醒内核线程。内核线程和 APP 都一样竞争执行,APP 有机会执行,系统不会卡顿。 这个内核线程是系统帮我们创建的,一般是 kworker 线程,内核中有很多这样的线程, 使用 ps -A | grep kworker 可以查看。kworker 线程要去“工作队列”(work queue)上取出一个一个“工作”(work),来执行它里面的函数。

  • 创建 work:static DECLARE_WORK(pmac_backlight_key_work, pmac_backlight_key_worker); 类似这样,前面是结构体,后面是函数
  • 把 work 提交给 work queue , schedule_work(&pmac_backlight_key_work); 会把 work 提供给系统默认的 work queue:system_wq,它是一个队列。
  • schedule_work 函数不仅仅是把 work 放入队列,还会把kworker 线程唤醒。此线程抢到时间运行时,它就会从队列中取出 work,执行里面的函数。
  • 在中断场景中,可以在中断上半部调用 schedule_work 函数。

总结:

  • 很耗时的中断处理,应该放到线程里去
  • 可以使用 work、work queue
  • 在中断上半部调用 schedule_work 函数,触发 work 的处理
  • 既然是在线程中运行,那对应的函数可以休眠。

18.2.7 新技术:threaded irq

你可以只提供 thread_fn,系统会为这个函数创建一个内核线程。发生中断时,内核线程就会执行这个函数。

int request_threaded_irq(unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, unsigned long irqflags, const char *devname, void *dev_id) , handler 是上半部,thread_fn 是下半部。

  • 用 work 来线程化地处理中断,一个 worker 线程只能由一个 CPU 执行,多个中断的 work 都由同一个 worker 线程来处理,在单 CPU 系统中也只能忍着了。但是在 SMP 系统中,明明有那么多 CPU 空着,你偏偏让多个中断挤在这个CPU 上?
  • threaded irq,为每一个中断都创建一个内核线程;多个中断的内核线程可以分配到多个 CPU 上执行,这提高了效率。

18.3 Linux 中断系统中的重要数据结构

最核心的结构体是 irq_desc,之前为了易于理解,我们说在 Linux 内核中有一个中断数组,对于每一个硬件中断,都有一个数组项,这个数组就是irq_desc 数组。 如果内核配置了 CONFIG_SPARSE_IRQ,那么它就会用基数树(radix tree) 来代替 irq_desc 数组。

18.3.1 irq_desc 数组

  • irq_desc 结构体在 include/linux/irqdesc.h 中定义
    struct irq_desc {
    ...
    struct irq_data     irq_data;
    ...
    irq_flow_handler_t  handle_irq;
    ...
    struct irqaction    *action;    /* IRQ action list */
    ...
    const char      *name;
    } ____cacheline_internodealigned_in_smp;
  • 每一个 irq_desc 数组项中都有一个函数:handle_irq,还有一个 action 链表。 外部设备 1、外部设备 n 共享一个 GPIO 中断 B,多个 GPIO 中断汇聚到GIC(通用中断控制器)的 A 号中断,GIC 再去中断 CPU。那么软件处理时就是反 过来,先读取 GIC 获得中断号 A,再细分出 GPIO 中断 B,最后判断是哪一个外部芯片发生了中断。

  • GIC 的处理函数:假设 irq_desc[A].handle_irq 是 XXX_gpio_irq_handler(XXX 指厂家),这个函数需要读取芯片的 GPIO 控制器,细分发生的是哪一个 GPIO 中断(假设是B),再去调用 irq_desc[B]. handle_irq。
  • 模块的中断处理函数: 对 于 GPIO 模 块 向 GIC 发 出 的 中 断 B , 它 的 处 理 函 数 irq_desc[B].handle_irq。 导致 GPIO 中断 B 发生的原因很多,所以 irq_desc[B].handle_irq 会调用某个链表里的函数,这些函数由外部设备提供。这些函数自行判断该中断是否自己产生,若是则处理。
  • 外部设备提供的处理函数:处理函数由自己驱动程序提供,对于共享中断,比如 GPIO 中断 B,它的中断来源可能有多个,每个中断源对应一个中断处理函数。所以 irq_desc[B]中应该有一个链表,存放着多个中断源的处理函数。 一旦程序确定发生了 GPIO 中断 B,那么就会从链表里把那些函数取出来,一一执行。这个链表就是 action 链表。

18.3.2 irqaction 结构体

irqaction 结构体在 include/linux/interrupt.h 中定义

struct irqaction {
    irq_handler_t       handler;
    void            *dev_id;
    ...
    struct irqaction    *next;
    irq_handler_t       thread_fn;
    struct task_struct  *thread;
    struct irqaction    *secondary;
    unsigned int        irq;
    unsigned int        flags;
    ...
    const char      *name;
    ...
} ____cacheline_internodealigned_in_smp;

当调用 request_irq、request_threaded_irq 注册中断处理函数时,内核就会构造一个 irqaction 结构体。在里面保存 name、dev_id 等,最重要的是 handler、thread_fn、thread。 handler 是中断处理的上半部函数,用来处理紧急的事情。thread_fn 对应一个内核线程 thread,当 handler 执行完毕,Linux 内核会唤醒对应的内核线程。在内核线程里,会调用 thread_fn 函数。

  • 可以提供 handler 而不提供 thread_fn,就退化为一般的 request_irq 函数。
  • 可以不提供 handler 只提供 thread_fn,完全由内核线程来处理中断。
  • 也可以既提供 handler 也提供 thread_fn,这就是中断上半部、下半部。

在 reqeust_irq 时可以传入 dev_id,在共享中断中必须提供 dev_id,非共享中断可以不提供。

  • 中断处理函数执行时,可以使用 dev_id
  • 卸载中断时要传入 dev_id,这样才能在 action 链表中根据 dev_id 找到对应项

18.3.3 irq_data 结构体

irq_data 结构体在 include/linux/irq.h 中定义

struct irq_data {
    u32         mask;
    unsigned int        irq;
    unsigned long       hwirq;
    struct irq_common_data  *common;
    struct irq_chip     *chip;
    struct irq_domain   *domain;
#ifdef  CONFIG_IRQ_DOMAIN_HIERARCHY
    struct irq_data     *parent_data;
#endif
    void            *chip_data;
};

它就是个中转站,里面有 irq_chip 指针 irq_domain 指针,都是指向别的结构体。 irq 是软件中断号,hwirq 是硬件中断号。 联系由 irq_domain 来建立。irq_domain会把本地的 hwirq 映射为全局的 irq,

18.3.4 irq_domain 结构体

irq_domain 结构体在 include/linux/irqdomain.h 中定义,

struct irq_domain {
    struct list_head link;
    const char *name;
    const struct irq_domain_ops *ops;
    void *host_data;
    unsigned int flags;
    /* Optional data */
    struct fwnode_handle *fwnode;
    enum irq_domain_bus_token bus_token;
    struct irq_domain_chip_generic *gc;
#ifdef  CONFIG_IRQ_DOMAIN_HIERARCHY
    struct irq_domain *parent;
#endif
    /* reverse map data. The linear map gets appended to the irq_domain */
    irq_hw_number_t hwirq_max;
    unsigned int revmap_direct_max_irq;
    unsigned int revmap_size;
    struct radix_tree_root revmap_tree;
    unsigned int linear_revmap[];
};
interrupt-parent = <&gpio1>; 
interrupts = <5 IRQ_TYPE_EDGE_RISING>; 

表示要使用 gpio1 里的第 5 号中断,hwirq 就是 5。但是我们在驱动中会使用 request_irq(irq, handler)这样的函数来注册中断,irq 是什么?它是软件中断号,它应该从“gpio1 的第 5 号中断”转换得来。 irq_domain 结构体中有一个 irq_domain_ops 结构体,里面有各种操作函数,主要是:

  • xlate 用来解析设备树的中断属性,提取出 hwirq、type 等信息。
  • map 把 hwirq 转换为 irq。

18.3.5 irq_chip 结构体

irq_chip 结构体在 include/linux/irq.h 中定义

struct irq_chip {
    ...
    void        (*irq_enable)(struct irq_data *data);
    void        (*irq_disable)(struct irq_data *data);
    void        (*irq_ack)(struct irq_data *data);
    void        (*irq_mask)(struct irq_data *data);
    void        (*irq_mask_ack)(struct irq_data *data);
    ...
};
  • 我们在 request_irq 后,并不需要手工去使能中断,原因就是系统调用对应的 irq_chip 里的函数帮我们使能了中断。
  • 我们提供的中断处理函数中,也不需要执行主芯片相关的清中断操作,也是系统帮我们调用 irq_chip 中的相关函数。
  • 但是对于外部设备相关的清中断操作,还是需要我们自己做的。

18.4 在设备树中指定中断_在代码中获得中断

18.4.1 设备树里中断节点的语法

内核 Documentation\devicetree\bindings\interrupt-controller\interrupts.txt

设备树里的中断控制器

  • 在设备树中,中断控制器节点中必须有一个属性:interrupt-controller,表明它是“中断控制器”。
  • 还必须有一个属性:#interrupt-cells,表明引用这个中断控制器的话需要多少个 cell。
  • #interrupt-cells=<1> 别的节点要使用这个中断控制器时,只需要一个 cell 来表明使用“哪一个中断”。
  • #interrupt-cells=<2> 别的节点要使用这个中断控制器时,需要一个 cell 来表明使用“哪一个中断”; 还需要另一个 cell 来描述中断,一般是表明触发类型:
    第 2 个 cell 的 bits[3:0] 用来表示中断触发类型(trigger type and level flags): 
    1 = low-to-high edge triggered,上升沿触发 
    2 = high-to-low edge triggered,下降沿触发 
    4 = active high level-sensitive,高电平触发 
    8 = active low level-sensitive,低电平触发 
  • 如 果 中 断 控 制 器 有 级 联 关 系 , 下 级 的 中 断 控 制 器 还 需 要 表 明 它 的“ interrupt-parent ” 是 谁 , 用 了 interrupt-parent ” 中 的 哪 一 个“interrupts”,

设备树里使用中断

  • interrupt-parent=<&XXXX> 你要用哪一个中断控制器里的中断?
  • interrupts 你要用哪一个中断?Interrupts 里要用几个 cell,由 interrupt-parent 对应的中断控制器决定。
    i2c@7000c000 { 
    gpioext: gpio-adnp@41 { 
    compatible = "ad,gpio-adnp"; 
    //
    interrupt-parent = <&gpio>; 
    interrupts = <160 1>; 
    //
    gpio-controller; 
    #gpio-cells = <1>; 
    //
    interrupt-controller; 
    #interrupt-cells = <2>; 
    }; 
    ...... 
    }; 
  • 新写法:interrupts-extended, 一个“interrupts-extended”属性就可以既指定“interrupt-parent”,也指定“interrupts”, interrupts-extended = <&intc1 5 1>, <&intc2 1 0>;

18.4.3 在代码中获得中断

  • 对于 platform_device,
    /** 
    * platform_get_resource - get a resource for a device 
    * @dev: platform device 
    * @type: resource type   // 取哪类资源?IORESOURCE_MEM、IORESOURCE_REG 
    *                      // IORESOURCE_IRQ 等 
    * @num: resource index  // 这类资源中的哪一个? 
    */ 
    struct resource *platform_get_resource(struct platform_device *dev, unsigned int type, unsigned int num); 
  • 对于 I2C 设备、SPI 设备. I2C 总线驱动在处理设备树里的 I2C 子节点时,也会处理其中的中断信息。一个 I2C 设备会被转换为一个 i2c_client 结构体,中断号会保存在 i2c_client 的 irq 成员里,代码如下(drivers/i2c/i2c-core.c). 对于 SPI 设备节点,SPI 总线驱动在处理设备树里的 SPI 子节点时,也会处理其中的中断信息。一个 SPI 设备会被转换为一个 spi_device 结构体,中断号会保存在 spi_device 的 irq 成员里,代码如下(drivers/spi/spi.c).
  • 调用 of_irq_get 获得中断号, 如果你的设备节点既不能转换为 platform_device,它也不是 I2C 设备,不是 SPI 设备,那么在驱动程序中可以自行调用 of_irq_get 函数去解析设备树,得到中断号。
  • 对于 GPIO, 参考:drivers/input/keyboard/gpio_keys.c 可以使用 gpio_to_irq 或 gpiod_to_irq 获得中断号。
    gpio-keys { 
    compatible = "gpio-keys"; 
    pinctrl-names = "default"; 
    user { 
        label = "User Button"; 
        gpios = <&gpio5 1 GPIO_ACTIVE_HIGH>; 
        gpio-key,wakeup; 
        linux,code = <KEY_1>; 
    }; 
    }; 
    // 可以使用下面的函数获得引脚和 flag
    button->gpio = of_get_gpio_flags(pp, 0, &flags); 
    bdata->gpiod = gpio_to_desc(button->gpio); 
    // 再去使用 gpiod_to_irq 获得中断号:
    irq = gpiod_to_irq(bdata->gpiod); 

18.5 编写使用中断的按键驱动程序

对于 GPIO 按键,我们并不需要去写驱动程序,使用内核自带的驱动程序 drivers/input/keyboard/gpio_keys.c 就可以,然后你需要做的只是修改设备树指定引脚及键值。

当然,也可以从头写按键驱动,特别是如何使用中断。

18.5.2 先编写驱动程序

  • 使用自定义结构体来保存 gpio 数据
    struct gpio_key{
    int gpio;
    struct gpio_desc *gpiod;
    int flag;
    int irq;
    } ;
  • 从设备树获得 GPIO count = of_gpio_count(node);
  • 申请自定义 gpio 结构体内存 gpio_keys_100ask = kzalloc(sizeof(struct gpio_key) * count, GFP_KERNEL);
  • 获取 gpio 号和 flag, gpio_keys_100ask[i].gpio = of_get_gpio_flags(node, i, &flag);
  • 从 GPIO 获得中断号 gpio_keys_100ask[i].irq = gpio_to_irq(gpio_keys_100ask[i].gpio);
  • 准备申请中断的 flag
        gpio_keys_100ask[i].flag = flag & OF_GPIO_ACTIVE_LOW;
        if (flag & OF_GPIO_ACTIVE_LOW)
            flags |= GPIOF_ACTIVE_LOW;
  • 申请 gpio 资源 err = devm_gpio_request_one(&pdev->dev, gpio_keys_100ask[i].gpio, flags, NULL);
  • 保存 irq gpio_keys_100ask[i].irq = gpio_to_irq(gpio_keys_100ask[i].gpio);
  • 申请中断, 并提供中断函数的参数 `err = request_irq(gpio_keys_100ask[i].irq, gpio_key_isr, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "100ask_gpio_key", &gpio_keys_100ask[i]);
  • 中断函数
    static irqreturn_t gpio_key_isr(int irq, void *dev_id) 
    { 
    struct gpio_key *gpio_key = dev_id; 
    int val; 
    val = gpiod_get_value(gpio_key->gpiod); 
    printk("key %d %d\n", gpio_key->gpio, val); 
    return IRQ_HANDLED; 
    } 
  • remove 的时候,别忘了 free_irq(gpio_keys_100ask[i].irq, &gpio_keys_100ask[i]); kfree(gpio_keys_100ask);

第19章 驱动程序基石

19.1 休眠与唤醒

19.1.2 内核函数

  • 休眠函数 参考内核源码:include\linux\wait.h , wq:waitqueue,等待队列, 休眠时除了把程序状态改为非 RUNNING 之外,还要把进程/进程放入 wq 中,以后中断服务程序要从 wq 中把它取出来唤醒。condition 这可以是一个变量,也可以是任何表达式。表示“一直等待,直到 condition 为真”。

    函数 说明
    wait_event_interruptible(wq, condition) 休眠,直到 condition 为真; 休眠期间是可被打断的,可以被信号打断
    wait_event(wq, condition) 休眠,直到 condition 为真; 退出的唯一条件是 condition 为真,信号也不好使
    wait_event_interruptible_timeout(wq, condition, timeout) 休眠,直到 condition 为真或超时; 休眠期间是可被打断的,可以被信号打断
    wait_event_timeout(wq, condition, timeout) 休眠,直到 condition 为真; 退出的唯一条件是 condition 为真,信号也不好使
  • 唤醒函数 参考内核源码:include\linux\wait.h。

    函数 说明
    wake_up_interruptible(x) 唤醒 x 队列中状态为“TASK_INTERRUPTIBLE”的线程,只唤醒其中的一个线程
    wake_up_interruptible_nr(x, nr) 唤醒 x 队列中状态为“TASK_INTERRUPTIBLE”的线程,只唤醒其中的 nr 个线程
    wake_up_interruptible_all(x) 唤醒 x 队列中状态为“TASK_INTERRUPTIBLE”的线程,唤醒其中的所有线程
    wake_up(x) 唤 醒 x 队 列 中 状 态 为 “ TASK_INTERRUPTIBLE ” 或“TASK_UNINTERRUPTIBLE”的线程,只唤醒其中的一个线程
    wake_up_nr(x, nr) 唤 醒 x 队 列 中 状 态 为 “ TASK_INTERRUPTIBLE ” 或“TASK_UNINTERRUPTIBLE”的线程,只唤醒其中 nr 个线程
    wake_up_all(x) 唤 醒 x 队 列 中 状 态 为 “ TASK_INTERRUPTIBLE ” 或“TASK_UNINTERRUPTIBLE”的线程,唤醒其中的所有线程

19.1.3 驱动框架

  • 初始化 wq 队列
  • 在驱动的 read 函数中,调用 wait_event_interruptible: 它本身会判断 event 是否为 FALSE,如果为 FASLE 表示无数据,则休眠。
  • 在中断服务程序里: 设置 event 为 TRUE,并调用 wake_up_interruptible 唤醒线程。

19.1.4 编程

static DECLARE_WAIT_QUEUE_HEAD(gpio_key_wait);
static ssize_t gpio_key_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
    int err;
    wait_event_interruptible(gpio_key_wait, g_key);
    err = copy_to_user(buf, &g_key, 4);
    g_key = 0;
    return 4;
}
static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{
    struct gpio_key *gpio_key = dev_id;
    int val;
    val = gpiod_get_value(gpio_key->gpiod);
    printk("key %d %d\n", gpio_key->gpio, val);
    g_key = (gpio_key->gpio << 8) | val;
    wake_up_interruptible(&gpio_key_wait);
    return IRQ_HANDLED;
}

19.1.6 使用环形缓冲区改进驱动程序

/* 环形缓冲区 */
#define BUF_LEN 128
static int g_keys[BUF_LEN];
static int r, w;

#define NEXT_POS(x) ((x+1) % BUF_LEN)

static int is_key_buf_empty(void)
{
    return (r == w);
}

static int is_key_buf_full(void)
{
    return (r == NEXT_POS(w));
}

static void put_key(int key)
{
    if (!is_key_buf_full())
    {
        g_keys[w] = key;
        w = NEXT_POS(w);
    }
}

static int get_key(void)
{
    int key = 0;
    if (!is_key_buf_empty())
    {
        key = g_keys[r];
        r = NEXT_POS(r);
    }
    return key;
}
    wait_event_interruptible(gpio_key_wait, !is_key_buf_empty());
    key = get_key();
    put_key(key);

19.2 POLL 机制

  • drv_poll 要把线程挂入队列 wq,但是并不是在 drv_poll 中进入休眠,而是在调用 drv_poll 之后休眠

19.2.3 驱动编程

  • 把当前线程挂入队列 wq:poll_wait 把线程挂入队列,如果线程已经在队列里了,它就不会再次挂入。
  • 返回设备状态:drv_poll 要返回自己的当前状态:(POLLIN | POLLRDNORM) 或 (POLLOUT | POLLWRNORM)。 POLLRDNORM 等同于 POLLIN, POLLWRNORM 等同于 POLLOUT
    static unsigned int gpio_key_drv_poll(struct file *fp, poll_table * wait) 
    { 
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); 
    poll_wait(fp, &gpio_key_wait, wait); 
    return is_key_buf_empty() ? 0 : POLLIN | POLLRDNORM; 
    } 

19.2.7 POLL 机制的内核代码详解

对于系统调用 poll 或 select,它们对应的内核函数都是 sys_poll。分析sys_poll,即可理解 poll 机制。 都在 fs/select.c 文件中。

sys_poll -> do_sys_poll -> do_poll

19.3 异步通知

Linux 系统中也有很多信号,在 Linux 内核源文件 include\uapi\asm-generic\signal.h 中,有很多信号的宏定义:

#define SIGHUP       1
#define SIGINT       2
#define SIGQUIT      3
#define SIGILL       4
#define SIGTRAP      5
#define SIGABRT      6
#define SIGIOT       6
#define SIGBUS       7
#define SIGFPE       8
#define SIGKILL      9
#define SIGUSR1     10
#define SIGSEGV     11
#define SIGUSR2     12
#define SIGPIPE     13
#define SIGALRM     14
#define SIGTERM     15
#define SIGSTKFLT   16
#define SIGCHLD     17
#define SIGCONT     18
#define SIGSTOP     19
#define SIGTSTP     20
#define SIGTTIN     21
#define SIGTTOU     22
#define SIGURG      23
#define SIGXCPU     24
#define SIGXFSZ     25
#define SIGVTALRM   26
#define SIGPROF     27
#define SIGWINCH    28
#define SIGIO       29
#define SIGPOLL     SIGIO
...

设置 Flag 里面的 FASYNC 位为 1:当 FASYNC 位发生变化时,会导致驱动程序的 fasync 被调用;

19.3.3 驱动编程

struct fasync_struct *button_fasync;
static int gpio_key_drv_fasync(int fd, struct file *file, int on)
{
    if (fasync_helper(fd, file, on, &button_fasync) >= 0)
        return 0;
    else
        return -EIO;
}

static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{
    struct gpio_key *gpio_key = dev_id;
    int val;
    int key;

    val = gpiod_get_value(gpio_key->gpiod);

    printk("key %d %d\n", gpio_key->gpio, val);
    key = (gpio_key->gpio << 8) | val;
    put_key(key);
    wake_up_interruptible(&gpio_key_wait);
    kill_fasync(&button_fasync, SIGIO, POLL_IN);

    return IRQ_HANDLED;
}

fasync_helper 函 数 会 分 配 、 构 造 一 个 fasync_struct 结 构 体 button_async:

  • 驱动文件的 flag 被设置为 FAYNC 时:button_async->fa_file = filp; // filp 表示驱动程序文件,里面含有之前设置的 PID
  • 驱动文件被设置为非 FASYNC 时: button_async->fa_file = NULL;

19.4 阻塞与非阻塞

O_NONBLOCK

19.4.1 应用编程

int  fd = open(“/dev/xxx”, O_RDWR | O_NONBLOCK);  /* 非阻塞方式 */ 
int  fd = open(“/dev/xxx”, O_RDWR );  /* 阻塞方式 */ 
int flags = fcntl(fd, F_GETFL); 
fcntl(fd, F_SETFL, flags | O_NONBLOCK);  /* 非阻塞方式 */ 
fcntl(fd, F_SETFL, flags & ~O_NONBLOCK);  /* 阻塞方式 */ 

19.4.2 驱动编程

// drv_read
    if (is_key_buf_empty() && (file->f_flags & O_NONBLOCK))
        return -EAGAIN;

19.5 定时器

参 考 内 核 源 码 include\linux\timer.h

  • setup_timer(timer, fn, data):设置定时器,主要是初始化 timer_list 结构体
  • void add_timer(struct timer_list *timer):向内核添加定时器。timer->expires 表示超时时间。 当 超 时 时 间 到 达 , 内 核 就 会 调 用 这 个 函 数 :timer->function(timer->data)。
  • int mod_timer(struct timer_list *timer, unsigned long expires): 修改定时器的超时时间,
  • int del_timer(struct timer_list *timer):删除定时器。

19.5.2 定时器时间单位

.config 中有配置 CONFIG_HZ=100, 核每秒中会发生 100 次系统滴答中断(tick).

  • 在 add_timer 之前,直接修改
    timer.expires = jiffies + xxx;   // xxx 表示多少个滴答后超时,也就是 xxx*10ms 
    timer.expires = jiffies + 2*HZ;  // HZ 等于 CONFIG_HZ,2*HZ 就相当于 2 秒 
  • 在 add_timer 之后,使用 mod_timer 修改:
    mod_timer(&timer, jiffies + xxx);   // xxx 表示多少个滴答后超时,也就是 xxx*10ms 
    mod_timer(&timer, jiffies + 2*HZ);  // HZ 等于 CONFIG_HZ,2*HZ 就相当于 2 秒 

19.5.5 深入研究:定时器的内部机制

对于 TIMER_SOFTIRQ 软中断,初始化代码如下:

void __init init_timers(void) 
{ 
  init_timer_cpus(); 
  init_timer_stats(); 
  open_softirq(TIMER_SOFTIRQ, run_timer_softirq); 
} 

当发生硬件中断时,硬件中断处理完后,内核会调用软件中断的处理函数。对于 TIMER_SOFTIRQ,会调用 run_timer_softirq,它的函数如下:

run_timer_softirq 
__run_timers(base); 
    while (time_after_eq(jiffies, base->clk)) { 
        …… 
expire_timers(base, heads + levels); 
    fn = timer->function; 
    data = timer->data; 
    call_timer_fn(timer, fn, data); 
        fn(data);

19.5.6 深入研究:找到系统滴答

  • # cat /proc/interrupts 找到 ticker 中断名
  • $ grep "i.MX Timer Tick" * -nr 找到对应的文件 drivers/clocksource/timer-imx-gpt.c:319: act->name = "i.MX Timer Tick";

19.6 中断下半部 tasklet

19.6.1 内核函数

  • 定义 tasklet 它 在 内 核 源 码 include\linux\interrupt.h 中定义:
    struct tasklet_struct 
    { 
    struct tasklet_struct *next; 
    unsigned long state; 
    atomic_t count; 
    void (*func)(unsigned long); 
    unsigned long data; 
    }; 
    //  DECLARE_TASKLET 定义的 tasklet 结构体,它是使能的;
    #define DECLARE_TASKLET(name, func, data) \ 
    struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data } 
    // DECLARE_TASKLET_DISABLED 定义的 tasklet 结构体,它是禁止的;使用之前要先调用 tasklet_enable 使能它。
    #define DECLARE_TASKLET_DISABLED(name, func, data) \ 
    struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data } 
    // 使用函数来初始化 tasklet 结构体:
    extern void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data); 
  • 使能/禁止 tasklet
    static inline void tasklet_enable(struct tasklet_struct *t); 
    static inline void tasklet_disable(struct tasklet_struct *t); 
  • 调度 tasklet
    static inline void tasklet_schedule(struct tasklet_struct *t);  
  • kill tasklet
    extern void tasklet_kill(struct tasklet_struct *t); 

19.6.2 tasklet 使用方法

先定义 tasklet,需要使用时调用 tasklet_schedule,驱动卸载前调用 tasklet_kill。tasklet_schedule 只是把 tasklet 放入内核队列,它的 func 函数会在软件中断的执行过程中被调用。

19.6.3 tasklet 内部机制

  • tasklet属于TASKLET_SOFTIRQ软件中断,入口函数为 tasklet_action,这在内核 kernel\softirq.c 中设置.
  • 当驱动程序调用 tasklet_schedule 时,会设置 tasklet 的 state 为TASKLET_STATE_SCHED,并把它放入某个链表
  • 当发生硬件中断时,内核处理完硬件中断后,会处理软件中断。对于TASKLET_SOFTIRQ 软件中断,会调用 tasklet_action 函数。
  • 如果 tasklet 的函数尚未执行,多次调用 tasklet_schedule 也是无效的,只会放入队列一次。

19.7 工作队列

19.7.1 内核函数

  • 定义 work
    #define DECLARE_WORK(n, f)            \ 
    struct work_struct n = __WORK_INITIALIZER(n, f) 
    //
    #define DECLARE_DELAYED_WORK(n, f)          \ 
    struct delayed_work n = __DELAYED_WORK_INITIALIZER(n, f, 0) 
  • 使用 work:schedule_work 调用 schedule_work 时,就会把 work_struct 结构体放入队列中,并唤醒对应的内核线程。内核线程就会从队列里把 work_struct 结构体取出来,执行里面的函数。

19.7.3 内部机制

代码在 kernel\workqueue.c 中

init_workqueues 
keventd_wq = create_workqueue("events"); 
    __create_workqueue((name), 0, 0) 
        for_each_possible_cpu(cpu) { 
            err = create_workqueue_thread(cwq, cpu); 
                p = kthread_create(worker_thread, cwq, fmt, wq->name, cpu);  

19.8 中断的线程化处理

19.8.1 内核机制

request_threaded_irq 函数,肯定会创建一个内核线程。源码在内核文件 kernel\irq\manage.c

19.8.2 编程、上机

调用 request_threaded_irq 函数注册中断,调用 free_irq 卸载中断。

19.9 mmap

让程序可以直接读写驱动程序中的 buffer,这可以通过mmap 实现(memory map),把内核的 buffer 映射到用户态,让 APP 在用户态直接读写。

发表评论