目录
实验程序编写
修改设备树文件
定时器驱动程序
timer.c
测试 timerApp.c
Makefile 文件
运行测试
实验程序编写
本讲实验,我们使用正点原子I.MX6U-ALPHA 开发板,通过linux内核定时器周期性的点亮和熄灭开发板上的 LED 灯, LED 灯的闪烁周期由内核定时器来设置,测试应用程序可以控制内核定时器周期。
修改设备树文件
和之前文章一致,GPIO子系统驱动LED,主要是以下几点:
- 添加 pinctrl 节点
- 添加 LED 设备节点
- 检查 PIN 是否被其他外设使用
定时器驱动程序
timer.c
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>#define TIMER_CNT 1 /* 设备号个数 */
#define TIMER_NAME "timer" /* 名字 */
#define CLOSE_CMD (_IO(0XEF, 0x1)) /* 关闭定时器 */
#define OPEN_CMD (_IO(0XEF, 0x2)) /* 打开定时器 */
#define SETPERIOD_CMD (_IO(0XEF, 0x3)) /* 设置定时器周期命令 */
#define LEDON 1 /* 开灯 */
#define LEDOFF 0 /* 关灯 *//* timer设备结构体 */
struct timer_dev{dev_t devid; /* 设备号 */struct cdev cdev; /* cdev */struct class *class; /* 类 */struct device *device; /* 设备 */int major; /* 主设备号 */int minor; /* 次设备号 */struct device_node *nd; /* 设备节点 */int led_gpio; /* key所使用的GPIO编号 */int timeperiod; /* 定时周期,单位为ms */struct timer_list timer;/* 定义一个定时器*/spinlock_t lock; /* 定义自旋锁 */
};struct timer_dev timerdev; /* timer设备 *//** @description : 初始化LED灯IO,open函数打开驱动的时候* 初始化LED灯所使用的GPIO引脚。* @param : 无* @return : 无*/
static int led_init(void)
{int ret = 0;timerdev.nd = of_find_node_by_path("/gpioled");if (timerdev.nd== NULL) {return -EINVAL;}timerdev.led_gpio = of_get_named_gpio(timerdev.nd ,"led-gpio", 0);if (timerdev.led_gpio < 0) {printk("can't get led\r\n");return -EINVAL;}/* 初始化led所使用的IO */gpio_request(timerdev.led_gpio, "led"); /* 请求IO */ret = gpio_direction_output(timerdev.led_gpio, 1);if(ret < 0) {printk("can't set gpio!\r\n");}return 0;
}/** @description : 打开设备* @param - inode : 传递给驱动的inode* @param - filp : 设备文件,file结构体有个叫做private_data的成员变量* 一般在open的时候将private_data指向设备结构体。* @return : 0 成功;其他 失败*/
static int timer_open(struct inode *inode, struct file *filp)
{int ret = 0;filp->private_data = &timerdev; /* 设置私有数据 */timerdev.timeperiod = 1000; /* 默认周期为1s */ret = led_init(); /* 初始化LED IO */if (ret < 0) {return ret;}return 0;
}/** @description : ioctl函数,* @param - filp : 要打开的设备文件(文件描述符)* @param - cmd : 应用程序发送过来的命令* @param - arg : 参数* @return : 0 成功;其他 失败*/
static long timer_unlocked_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{struct timer_dev *dev = (struct timer_dev *)filp->private_data;int timerperiod;unsigned long flags;switch (cmd) {case CLOSE_CMD: /* 关闭定时器 */del_timer_sync(&dev->timer);break;case OPEN_CMD: /* 打开定时器 */spin_lock_irqsave(&dev->lock, flags);timerperiod = dev->timeperiod;spin_unlock_irqrestore(&dev->lock, flags);mod_timer(&dev->timer, jiffies + msecs_to_jiffies(timerperiod));break;case SETPERIOD_CMD: /* 设置定时器周期 */spin_lock_irqsave(&dev->lock, flags);dev->timeperiod = arg;spin_unlock_irqrestore(&dev->lock, flags);mod_timer(&dev->timer, jiffies + msecs_to_jiffies(arg));break;default:break;}return 0;
}/* 设备操作函数 */
static struct file_operations timer_fops = {.owner = THIS_MODULE,.open = timer_open,.unlocked_ioctl = timer_unlocked_ioctl,
};/* 定时器回调函数 */
void timer_function(unsigned long arg)
{struct timer_dev *dev = (struct timer_dev *)arg;static int sta = 1;int timerperiod;unsigned long flags;sta = !sta; /* 每次都取反,实现LED灯反转 */gpio_set_value(dev->led_gpio, sta);/* 重启定时器 */spin_lock_irqsave(&dev->lock, flags);timerperiod = dev->timeperiod;spin_unlock_irqrestore(&dev->lock, flags);mod_timer(&dev->timer, jiffies + msecs_to_jiffies(dev->timeperiod)); }/** @description : 驱动入口函数* @param : 无* @return : 无*/
static int __init timer_init(void)
{/* 初始化自旋锁 */spin_lock_init(&timerdev.lock);/* 注册字符设备驱动 *//* 1、创建设备号 */if (timerdev.major) { /* 定义了设备号 */timerdev.devid = MKDEV(timerdev.major, 0);register_chrdev_region(timerdev.devid, TIMER_CNT, TIMER_NAME);} else { /* 没有定义设备号 */alloc_chrdev_region(&timerdev.devid, 0, TIMER_CNT, TIMER_NAME); /* 申请设备号 */timerdev.major = MAJOR(timerdev.devid); /* 获取分配号的主设备号 */timerdev.minor = MINOR(timerdev.devid); /* 获取分配号的次设备号 */}/* 2、初始化cdev */timerdev.cdev.owner = THIS_MODULE;cdev_init(&timerdev.cdev, &timer_fops);/* 3、添加一个cdev */cdev_add(&timerdev.cdev, timerdev.devid, TIMER_CNT);/* 4、创建类 */timerdev.class = class_create(THIS_MODULE, TIMER_NAME);if (IS_ERR(timerdev.class)) {return PTR_ERR(timerdev.class);}/* 5、创建设备 */timerdev.device = device_create(timerdev.class, NULL, timerdev.devid, NULL, TIMER_NAME);if (IS_ERR(timerdev.device)) {return PTR_ERR(timerdev.device);}/* 6、初始化timer,设置定时器处理函数,还未设置周期,所有不会激活定时器 */init_timer(&timerdev.timer);timerdev.timer.function = timer_function;timerdev.timer.data = (unsigned long)&timerdev;return 0;
}/** @description : 驱动出口函数* @param : 无* @return : 无*/
static void __exit timer_exit(void)
{gpio_set_value(timerdev.led_gpio, 1); /* 卸载驱动的时候关闭LED */del_timer_sync(&timerdev.timer); /* 删除timer */
#if 0del_timer(&timerdev.tiemr);
#endif/* 注销字符设备驱动 */gpio_free(timerdev.led_gpio); cdev_del(&timerdev.cdev);/* 删除cdev */unregister_chrdev_region(timerdev.devid, TIMER_CNT); /* 注销设备号 */device_destroy(timerdev.class, timerdev.devid);class_destroy(timerdev.class);
}module_init(timer_init);
module_exit(timer_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("huax");
关键代码分析如下:
定时器设备结构体timer_dev,定义了一个定时器成员变量 timer:
/* timer设备结构体 */
struct timer_dev{dev_t devid; /* 设备号 */struct cdev cdev; /* cdev */struct class *class; /* 类 */struct device *device; /* 设备 */int major; /* 主设备号 */int minor; /* 次设备号 */struct device_node *nd; /* 设备节点 */int led_gpio; /* key所使用的GPIO编号 */int timeperiod; /* 定时周期,单位为ms */struct timer_list timer;/* 定义一个定时器*/spinlock_t lock; /* 定义自旋锁 */
};
函数 timer_open,对应应用程序的 open 函数,应用程序调用 open 函数打开/dev/timer 驱动文件的时候此函数就会执行。
static int timer_open(struct inode *inode, struct file *filp)
{int ret = 0;filp->private_data = &timerdev; /* 设置私有数据 */timerdev.timeperiod = 1000; /* 默认周期为1s */ret = led_init(); /* 初始化LED IO */if (ret < 0) {return ret;}return 0;
}
此函数设置文件私有数据为 timerdev,并且初始化定时周期默认为 1 秒,最后调用 led_init 函数初始化 LED 所使用的 IO。
函数 timer_unlocked_ioctl,对应应用程序的 ioctl 函数,应用程序调用 ioctl函数向驱动发送控制信息,此函数响应并执行。
static long timer_unlocked_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{struct timer_dev *dev = (struct timer_dev *)filp->private_data;int timerperiod;unsigned long flags;switch (cmd) {case CLOSE_CMD: /* 关闭定时器 */del_timer_sync(&dev->timer);break;case OPEN_CMD: /* 打开定时器 */spin_lock_irqsave(&dev->lock, flags);timerperiod = dev->timeperiod;spin_unlock_irqrestore(&dev->lock, flags);mod_timer(&dev->timer, jiffies + msecs_to_jiffies(timerperiod));break;case SETPERIOD_CMD: /* 设置定时器周期 */spin_lock_irqsave(&dev->lock, flags);dev->timeperiod = arg;spin_unlock_irqrestore(&dev->lock, flags);mod_timer(&dev->timer, jiffies + msecs_to_jiffies(arg));break;default:break;}return 0;
}
此函数有三个参数: filp, cmd 和 arg,其中 :
- filp是对应的设备文件,
- cmd 是应用程序发送过来的命令信息,
- arg 是应用程序发送过来的参数,
在本讲定时器实验里, arg 参数表示定时周期。
cmd 有三种命令:
- CLOSE_CMD: 关闭定时器命令, 调用 del_timer_sync 函数关闭定时器。
- OPEN_CMD:打开定时器命令,调用 mod_timer 函数打开定时器,定时周期为 timerdev 的timeperiod 成员变量,定时周期默认是 1 秒。
- SETPERIOD_CMD:设置定时器周期命令,参数 arg 就是新的定时周期,设置 timerdev 的timeperiod 成员变量为 arg 所表示定时周期指。并且使用 mod_timer 重新打开定时器,使定时器以新的周期运行。
函数 timer_function,定时器服务函数,此函有一个参数 arg, 对应timerdev 的地址,这样通过 arg 参数就可以访问到设备结构体。
void timer_function(unsigned long arg)
{struct timer_dev *dev = (struct timer_dev *)arg;static int sta = 1;int timerperiod;unsigned long flags;sta = !sta; /* 每次都取反,实现LED灯反转 */gpio_set_value(dev->led_gpio, sta);/* 重启定时器 */spin_lock_irqsave(&dev->lock, flags);timerperiod = dev->timeperiod;spin_unlock_irqrestore(&dev->lock, flags);mod_timer(&dev->timer, jiffies + msecs_to_jiffies(dev->timeperiod)); }
当定时周期到了以后此函数就会被调用。在此函数中将 LED 灯的状态取反,实现 LED 灯闪烁的效果。
函数 timer_init,驱动入口函数里,初始化定时器,设置定时器的定时处理函数为 timer_function,另外设置要传递给 timer_function 函数的参数为 timerdev的地址。
/* 6、初始化timer,设置定时器处理函数,还未设置周期,所有不会激活定时器 */init_timer(&timerdev.timer);timerdev.timer.function = timer_function;timerdev.timer.data = (unsigned long)&timerdev;
函数 timer_exit里,调用 del_timer_sync 函数删除定时器。
del_timer_sync(&timerdev.timer); /* 删除timer */
#if 0del_timer(&timerdev.tiemr);
#endif
测试 timerApp.c
测试 APP 我们要实现的内容如下:
- 运行 APP 以后提示我们输入要测试的命令,输入 1 表示关闭定时器、输入 2 表示打开定时器,输入 3 设置定时器周期。
- 如果要设置定时器周期的话,需要让用户输入要设置的周期值,单位为毫秒。
timerApp.c文件代码如下:
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include "linux/ioctl.h"/* 命令值 */
#define CLOSE_CMD (_IO(0XEF, 0x1)) /* 关闭定时器 */
#define OPEN_CMD (_IO(0XEF, 0x2)) /* 打开定时器 */
#define SETPERIOD_CMD (_IO(0XEF, 0x3)) /* 设置定时器周期命令 *//** @description : main主程序* @param - argc : argv数组元素个数* @param - argv : 具体参数* @return : 0 成功;其他 失败*/
int main(int argc, char *argv[])
{int fd, ret;char *filename;unsigned int cmd;unsigned int arg;unsigned char str[100];if (argc != 2) {printf("Error Usage!\r\n");return -1;}filename = argv[1];fd = open(filename, O_RDWR);if (fd < 0) {printf("Can't open file %s\r\n", filename);return -1;}while (1) {printf("Input CMD:");ret = scanf("%d", &cmd);if (ret != 1) { /* 参数输入错误 */gets(str); /* 防止卡死 */}if(cmd == 1) /* 关闭LED灯 */cmd = CLOSE_CMD;else if(cmd == 2) /* 打开LED灯 */cmd = OPEN_CMD;else if(cmd == 3) {cmd = SETPERIOD_CMD; /* 设置周期值 */printf("Input Timer Period:");ret = scanf("%d", &arg);if (ret != 1) { /* 参数输入错误 */gets(str); /* 防止卡死 */}}ioctl(fd, cmd, arg); /* 控制定时器的打开和关闭 */ }close(fd);
}
while(1)循环,让用户输入要测试的命令,然后通过 ioctl 函数发送给驱动程序。
如果是设置定时器周期命令 SETPERIOD_CMD,那么 ioctl 函数的 arg 参数就是用户输入的周期值。
Makefile 文件
makefile文件只需要修改 obj-m 变量的值,改为timer.o
KERNELDIR := /home/huax/linux/linux_test/linux-imx-rel_imx_4.1.15_2.1.0_gaCURRENT_PATH := $(shell pwd)
obj-m := timer.obuild: kernel_modules
kernel_modules:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
运行测试
编译代码:
make -j32 //编译makefile文件
arm-linux-gnueabihf-gcc timerApp.c -o timerApp //编译测试程序
编译成功以后,就会生成一个名为“timer.ko”的驱动模块文件,和timerApp 这个应用程序。
将编译出来的 timer.ko 和 timerApp 这两个文件拷贝到 rootfs/lib/modules/4.1.15 目录中,重启开发板。
进入到目录 lib/modules/4.1.15 中,输入如下命令加载 timer.ko 驱动模块:
depmod //第一次加载驱动的时候需要运行此命令
modprobe timer.ko //加载驱动
驱动加载成功以后,输入命令来测试:
/timerApp /dev/timer
打印如下:
提示我们输入cmd指令。
输入“2”,打开定时器,此时 LED 灯就会以默认的 1 秒周期开始闪烁。
再输入“3”来设置定时周期,根据提示输入要设置的周期值:
输入“500”,表示设置定时器周期值为 500ms,设置好以后 LED 灯就会以 500ms 为间隔,开始闪烁。
最后可以通过输入“1”来关闭定时器。
卸载驱动的话输入如下命令:
rmmod timer.ko