嵌入式Linux驱动开发:定时器驱动

嵌入式Linux驱动开发:定时器驱动

1. 概述

本文档详细介绍了基于i.MX6ULL平台的定时器驱动开发过程。该驱动利用Linux内核的定时器机制实现了一个LED闪烁控制功能,通过字符设备接口暴露给用户空间程序,允许用户通过ioctl命令控制定时器的启动、停止和周期设置。

本笔记将结合提供的源代码和设备树文件,深入分析驱动的实现细节,并介绍相关的理论知识。

2. 设备树配置分析

设备树(Device Tree)是描述硬件配置的关键文件。在本项目中,imx6ull-alientek-emmc.dts文件包含了与定时器驱动相关的硬件信息。

2.1 GPIO LED节点

dts 复制代码
gpioled{
    compatible = "alientek,gpioled";
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_gpioled>;
    states = "okay";
    /* led-gpios = <&gpio1 3 GPIO_ACTIVE_LOW>; */
};
  • compatible: 兼容性字符串,用于匹配驱动程序。这里设置为"alientek,gpioled"。
  • pinctrl-0 : 引用pinctrl配置节点pinctrl_gpioled,定义了GPIO的电气特性。
  • states: 设备状态,"okay"表示设备启用。

2.2 Pin控制配置

dts 复制代码
pinctrl_gpioled: ledgrp {
    fsl,pins = <
        MX6UL_PAD_GPIO1_IO03__GPIO1_IO03    0x10b0
    >;
};
  • 这段配置将GPIO1_IO03引脚设置为GPIO功能。
  • 0x10b0是引脚的电气属性配置,包括驱动强度、上下拉等。

2.3 设备树与驱动的关联

驱动程序通过of_find_node_by_path("/gpioled")函数在设备树中查找名为"gpioled"的节点,然后使用of_get_named_gpio()函数获取该节点中定义的GPIO编号。这种机制实现了硬件配置与驱动代码的分离,提高了代码的可移植性。

3. 定时器驱动实现

3.1 数据结构定义

c 复制代码
struct timer_dev
{
    dev_t devid;
    int major;
    int minor;
    struct cdev cdev;
    struct class *class;
    struct device *device;
    struct device_node *nd;
    int led_gpio;
    int timerperiod;
    struct timer_list timer;
};
  • devid: 设备号,由主设备号和次设备号组成。
  • major/minor: 主设备号和次设备号的存储变量。
  • cdev: 字符设备结构体,用于向内核注册字符设备。
  • class/device: 用于在/sys/class目录下创建设备文件,实现设备的自动管理。
  • nd: 设备树节点指针,用于获取设备树中的配置信息。
  • led_gpio: 存储LED所连接的GPIO编号。
  • timerperiod: 定时器周期(毫秒)。
  • timer: 内核定时器结构体。

3.2 文件操作函数

3.2.1 open函数
c 复制代码
static int timer_open(struct inode *inode, struct file *filp)
{
    filp->private_data = &timerdev;
    return 0;
}

open函数的主要作用是将设备结构体的指针保存到文件的私有数据中,以便后续操作可以访问设备的相关信息。

3.2.2 release函数
c 复制代码
static int timer_release(struct inode *inode, struct file *filp)
{
    return 0;
}

release函数在文件关闭时被调用。在这个简单的驱动中,不需要进行特殊的清理工作。

3.2.3 ioctl函数
c 复制代码
static long timer_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    struct timer_dev *dev = filp->private_data;
    int ret = 0;
    int value = 0;
    
    switch (cmd)
    {
    case CLOSE_CMD:
        del_timer(&dev->timer);
        break;

    case OPEN_CMD:
        mod_timer(&dev->timer, jiffies + msecs_to_jiffies(dev->timerperiod));
        break;
    case SETPERIOD_CMD:
        ret = copy_from_user(&value, (int *)arg, sizeof(value));
        if (ret != 0)
        {
            printk("Kernel:fail copy from user!\r\n");
            return -EFAULT;
        }
        dev->timerperiod = value;
        mod_timer(&dev->timer, jiffies + msecs_to_jiffies(dev->timerperiod));
        break;
    }

    return 0;
}

ioctl函数是用户空间与内核空间通信的主要接口。它支持三个命令:

  • CLOSE_CMD : 停止定时器,通过del_timer()函数实现。
  • OPEN_CMD : 启动定时器,通过mod_timer()函数设置定时器的超时时间。
  • SETPERIOD_CMD: 设置定时器周期,从用户空间复制新的周期值,并立即修改定时器。

3.3 定时器回调函数

c 复制代码
static void timer_func(unsigned long arg)
{
    struct timer_dev *dev = (struct timer_dev *)arg;
    static int sta = 1;
    sta = !sta;
    gpio_set_value(dev->led_gpio, sta);
    mod_timer(&dev->timer, jiffies + msecs_to_jiffies(dev->timerperiod));
}
  • 回调函数在定时器超时时被调用。
  • 使用gpio_set_value()函数翻转LED的状态。
  • 通过mod_timer()函数重新设置定时器,实现周期性的LED闪烁。

3.4 GPIO初始化

c 复制代码
int led_init(struct timer_dev *dev)
{
    u8 ret = 0;
    dev->nd = of_find_node_by_path("/gpioled");
    if (dev->nd == NULL)
    {
        ret = -EFAULT;
        goto fail_find_nd;
    }
    dev->led_gpio = of_get_named_gpio(dev->nd, "led-gpios", 0);
    if (dev->led_gpio < 0)
    {
        ret = -EFAULT;
        goto fail_get_gpio;
    }
    ret = gpio_request(dev->led_gpio, "timer");
    if (ret)
    {
        ret = -EFAULT;
        goto fail_gpio_req;
    }
    ret = gpio_direction_output(dev->led_gpio, 1);
    if (ret)
    {
        ret = -EFAULT;
        goto fail_gpio_output;
    }
    return 0;
fail_gpio_output:
    gpio_free(dev->led_gpio);
fail_gpio_req:
fail_get_gpio:
fail_find_nd:
    printk("fail_find_nd\r\n");
    return ret;
}

GPIO初始化函数执行以下步骤:

  1. 在设备树中查找"gpioled"节点。
  2. 获取节点中定义的GPIO编号。
  3. 申请GPIO资源。
  4. 设置GPIO为输出模式,并初始化为高电平。

4. 驱动初始化与退出

4.1 驱动初始化

c 复制代码
static int __init timer_init(void)
{
    u8 ret = 0;

    timerdev.major = 0;
    if (timerdev.major)
    {
        timerdev.devid = MKDEV(timerdev.major, 0);
        ret = register_chrdev_region(timerdev.devid, GPIOTIMER_CNT, GPIOTIMER_NAME);
    }
    else
    {
        ret = alloc_chrdev_region(&timerdev.devid, 0, GPIOTIMER_CNT, GPIOTIMER_NAME);
        timerdev.major = MAJOR(timerdev.devid);
        timerdev.minor = MINOR(timerdev.devid);
    }
    if (ret < 0)
    {
        goto fail_devid;
    }

    timerdev.cdev.owner = THIS_MODULE;
    cdev_init(&timerdev.cdev, &timer_fops);
    ret = cdev_add(&timerdev.cdev, timerdev.devid, GPIOTIMER_CNT);
    if (ret < 0)
    {
        goto fail_cedv_add;
    }

    timerdev.class = class_create(timerdev.cdev.owner, GPIOTIMER_NAME);
    if (IS_ERR(timerdev.class))
    {
        ret = PTR_RET(timerdev.class);
        goto fail_class;
    }

    timerdev.device = device_create(timerdev.class, NULL, timerdev.devid, NULL, GPIOTIMER_NAME);
    if (IS_ERR(timerdev.device))
    {
        ret = PTR_RET(timerdev.device);
        goto fail_device;
    }

    ret = led_init(&timerdev);
    if (ret < 0)
    {
        goto fail_led_init;
    }

    timerdev.timerperiod = 500;
    init_timer(&timerdev.timer);
    timerdev.timer.function = timer_func;
    timerdev.timer.data = (unsigned long)&timerdev;
    timerdev.timer.expires = jiffies + msecs_to_jiffies(timerdev.timerperiod);
    add_timer(&timerdev.timer);

    return 0;
fail_led_init:
    device_destroy(timerdev.class, timerdev.devid);
fail_device:
    class_destroy(timerdev.class);
fail_class:
    cdev_del(&timerdev.cdev);
fail_cedv_add:
    unregister_chrdev(timerdev.major, GPIOTIMER_NAME);
fail_devid:
    return ret;
}

驱动初始化函数执行以下步骤:

  1. 分配设备号(动态或静态)。
  2. 初始化并添加字符设备。
  3. 创建设备类和设备文件。
  4. 初始化GPIO。
  5. 初始化并启动定时器。

4.2 驱动退出

c 复制代码
static void __exit timer_exit(void)
{
    del_timer(&timerdev.timer);
    gpio_set_value(timerdev.led_gpio, 1);

    gpio_free(timerdev.led_gpio);
    device_destroy(timerdev.class, timerdev.devid);
    class_destroy(timerdev.class);
    cdev_del(&timerdev.cdev);
    unregister_chrdev(timerdev.major, GPIOTIMER_NAME);
}

驱动退出函数执行清理工作,按照与初始化相反的顺序释放资源。

5. Makefile分析

makefile 复制代码
KERNERDIR := /home/ubuntu2004/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga
CURRENTDIR := $(shell pwd)

obj-m := timer.o
build : kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNERDIR) M=$(CURRENTDIR) modules

clean:
	$(MAKE) -C $(KERNERDIR) M=$(CURRENTDIR) clean
  • KERNERDIR: 指向内核源码目录。
  • CURRENTDIR: 当前工作目录。
  • obj-m: 指定要编译为模块的目标文件。
  • kernel_modules: 调用内核的构建系统编译模块。
  • clean: 清理编译生成的文件。

6. 用户空间应用程序

6.1 应用程序功能

用户空间应用程序timerAPP.c提供了与驱动交互的接口。它允许用户通过命令行输入控制定时器的行为。

6.2 关键代码分析

c 复制代码
while (1)
{
    unsigned int cmd = 0, arg = 0;

    printf("User: input cmd:");
    ret = scanf("%d", &cmd);
    if (ret != 1)
    {
        gets(str);
    }
    if (cmd == 1)
    {
        ioctl(fd, CLOSE_CMD, &arg);
    }
    else if (cmd == 2)
    {
        ioctl(fd, OPEN_CMD, &arg);
    }
    else if (cmd == 3)
    {
        printf("Please input timerpriod: ");
        ret = scanf("%d", &arg);
        if (ret != 1)
        {
            gets(str);
        }
        ioctl(fd, SETPERIOD_CMD, &arg);
    }
}

应用程序通过一个无限循环接收用户输入,并根据输入的命令调用相应的ioctl操作。

7. 理论知识

7.1 Linux内核定时器

Linux内核提供了定时器机制,允许驱动程序在指定的时间后执行特定的函数。主要函数包括:

  • init_timer(): 初始化定时器结构体。
  • add_timer(): 启动定时器。
  • del_timer(): 删除定时器。
  • mod_timer(): 修改定时器的超时时间。

定时器的精度受限于系统的HZ值(通常为100或1000),因此不适合需要高精度定时的场景。

7.2 字符设备驱动框架

字符设备驱动的基本框架包括:

  1. 设备号的申请与释放。
  2. 字符设备的注册与注销。
  3. 文件操作函数的实现。
  4. 设备类和设备文件的创建。

7.3 设备树

设备树是一种描述硬件配置的数据结构,它将硬件信息从驱动代码中分离出来,提高了代码的可移植性和可维护性。驱动程序通过设备树API(如of_find_node_by_path()of_get_named_gpio()等)获取硬件配置信息。

该驱动程序可以作为学习嵌入式Linux驱动开发的良好范例,涵盖了从硬件配置到用户空间交互的完整流程。

源码仓库位置:https://gitee.com/dream-cometrue/linux_driver_imx6ull