当前位置: 首页 > news >正文

嵌入式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初始化函数执行以下步骤:

  1. 在设备树中查找"gpioled"节点。
  2. 获取节点中定义的GPIO编号。
  3. 申请GPIO资源。
  4. 设置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;
}

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

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

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 字符设备驱动框架

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

  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

http://www.dtcms.com/a/356363.html

相关文章:

  • 解析DB-GPT项目中三个 get_all_model_instances 方法的区别
  • 【WebRTC】从入门到忘记
  • 解密 Vue 3 shallowRef:浅层响应式 vs 深度响应式的性能对决
  • 园区智慧水电管理系统:让能源管理从“成本黑洞”变“利润引擎”
  • 【GM3568JHF】FPGA+ARM异构开发板 使用指南:以太网
  • nginx 怎么将 https 请求转为 http
  • AR巡检系统:数字化传统工作流程SOP的第一步
  • //Q是一个队列,S是一个空栈,实现将队列中的元素逆置的算法。
  • 从零开始学习单片机17
  • 医疗AI时代的生物医学Go编程:高性能计算与精准医疗的案例分析(五)
  • AR智能眼镜:能源行业运维的数字化革新
  • iOS 文件管理与 uni-app 性能优化实战 多工具协作的完整指南
  • Frida-dexdump 使用指南:从内存中脱取安卓 Dex 文件
  • Go对接全球期货数据源指南:基于StockTV API实现多品种实时监控
  • LeetCode第二题知识点2 ---- 栈、堆、地址
  • 不止 ChatGPT:多模态 AI(文本 + 图像 + 音频)正重构内容创作全流程
  • 数据质检之springboot通过yarn调用spark作业实现数据质量检测
  • 第三章 Vue3 + Three.js 实战:用 OrbitControls 实现相机交互与 3D 立方体展示
  • Unity学习----【数据持久化】二进制存储(一)
  • ExcelJS实现导入转换HTML展示(附源码可直接使用)
  • Excel数组学习笔记
  • 在Excel和WPS表格中隔一行插入多个空白行
  • 网络编程 04:TCP连接,客户端与服务器的区别,实现 TCP 聊天及文件上传,Tomcat 的简单使用
  • 从零开始部署 Kubernetes Dashboard:可视化管理你的集群
  • [Linux]学习笔记系列 -- mm/shrinker.c 内核缓存收缩器(Kernel Cache Shrinker) 响应内存压力的回调机制
  • 创意程序之MP3分割工具
  • sqlachemy
  • AI操作系统语言模型设计 之1 基于意识的Face-Gate-Window的共轭路径的思维-认知-情感嵌套模型
  • 【C语言】深入理解指针(2)
  • 龙迅#LT7621GX适用于两路HDMI2.1/DP1.4A转HDMI2.1混切应用,分辨率高达8K60HZ!