嵌入式Linux驱动开发:定时器驱动
嵌入式Linux驱动开发:定时器驱动
1. 概述
本文档详细介绍了基于i.MX6ULL平台的定时器驱动开发过程。该驱动利用Linux内核的定时器机制实现了一个LED闪烁控制功能,通过字符设备接口暴露给用户空间程序,允许用户通过ioctl命令控制定时器的启动、停止和周期设置。
本笔记将结合提供的源代码和设备树文件,深入分析驱动的实现细节,并介绍相关的理论知识。
2. 设备树配置分析
设备树(Device Tree)是描述硬件配置的关键文件。在本项目中,imx6ull-alientek-emmc.dts
文件包含了与定时器驱动相关的硬件信息。
2.1 GPIO LED节点
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控制配置
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 数据结构定义
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函数
static int timer_open(struct inode *inode, struct file *filp)
{filp->private_data = &timerdev;return 0;
}
open
函数的主要作用是将设备结构体的指针保存到文件的私有数据中,以便后续操作可以访问设备的相关信息。
3.2.2 release函数
static int timer_release(struct inode *inode, struct file *filp)
{return 0;
}
release
函数在文件关闭时被调用。在这个简单的驱动中,不需要进行特殊的清理工作。
3.2.3 ioctl函数
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 定时器回调函数
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初始化
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初始化函数执行以下步骤:
- 在设备树中查找"gpioled"节点。
- 获取节点中定义的GPIO编号。
- 申请GPIO资源。
- 设置GPIO为输出模式,并初始化为高电平。
4. 驱动初始化与退出
4.1 驱动初始化
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;
}
驱动初始化函数执行以下步骤:
- 分配设备号(动态或静态)。
- 初始化并添加字符设备。
- 创建设备类和设备文件。
- 初始化GPIO。
- 初始化并启动定时器。
4.2 驱动退出
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分析
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_moduleskernel_modules:$(MAKE) -C $(KERNERDIR) M=$(CURRENTDIR) modulesclean:$(MAKE) -C $(KERNERDIR) M=$(CURRENTDIR) clean
- KERNERDIR: 指向内核源码目录。
- CURRENTDIR: 当前工作目录。
- obj-m: 指定要编译为模块的目标文件。
- kernel_modules: 调用内核的构建系统编译模块。
- clean: 清理编译生成的文件。
6. 用户空间应用程序
6.1 应用程序功能
用户空间应用程序timerAPP.c
提供了与驱动交互的接口。它允许用户通过命令行输入控制定时器的行为。
6.2 关键代码分析
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 字符设备驱动框架
字符设备驱动的基本框架包括:
- 设备号的申请与释放。
- 字符设备的注册与注销。
- 文件操作函数的实现。
- 设备类和设备文件的创建。
7.3 设备树
设备树是一种描述硬件配置的数据结构,它将硬件信息从驱动代码中分离出来,提高了代码的可移植性和可维护性。驱动程序通过设备树API(如of_find_node_by_path()
、of_get_named_gpio()
等)获取硬件配置信息。
该驱动程序可以作为学习嵌入式Linux驱动开发的良好范例,涵盖了从硬件配置到用户空间交互的完整流程。
源码仓库位置:https://gitee.com/dream-cometrue/linux_driver_imx6ull