硬件定时器产生的周期性中断,中断频率就是系统频率(拍率)。系统拍率可以设置,单位是HZ,可在编译内核时通过图形化界面设置,设置路径如下:Kernel Features -> Timer frequency([=y])
配置完以后,可在内核源码根目录下的 .config 文件找到 CONFIG_HZ 的值为所设置的系统频率。而文件 include/asm-generic/param.h 中的宏:
#define HZ CONFIG_HZ
在编译驱动时经常使用 HZ 宏来设置定时器的参数。
linux 内核使用全局变量 jiffies 来记录系统从启动以来的系统节拍数,系统启动时 jiffies 初始化为 0。jiffies 定义在文件 include/linux/jiffies.h 中:
extern u64 __jiffy_data jiffies_64;
extern unsigned long volatile __jiffy_data jiffies;
其中 jiffies_64 和 jiffies 是同一个东西,jiffies_64 用于 64 位系统,jiffies 用于 32 位系统。jiffies 是 jiffies_64 的的低32位。jiffies 会有溢出的风险,当 jiffies 溢出后就会从 0 开始极数(绕回)。假如 HZ 为 1000,则 49.7 天就会发生绕回,linux 内核提供了 API 来处理绕回:
函数 | 描述(unkown 通常为 jiffies,known 通常是需要对比的值。) |
---|---|
time_after(unkown, known) | 如果 unkown 超过 known,返回真,否则返回假 |
time_before(unkown, known) | 如果 unkown 没超过 known,返回真,否则返回假 |
time_after_eq(unkown, known) | 和 time_after 一样,多了判断等于 |
time_before_eq(unkown, known) | 和 time_before 一样,多了判断等于 |
判断一段代码执行时间有没有超时:
unsigned long timeout;
timeout = jiffies + (2 * HZ); /* 超时的时间点2S */
/*************************************
具体的代码
************************************/
/* 判断有没有超时 */
if(time_before(jiffies, timeout)) {
/* 超时发生 */
} else {
/* 超时未发生 */
}
linux 内核提供了 jiffies 和 ms、us、ns 之间的转换函数:
函数 | 描述 |
---|---|
int jiffies_to_msecs(const unsigned long j) | 将 jiffies 类型的参数 j 分别转换为对应的毫秒 |
int jiffies_to_usecs(const unsigned long j) | 将 jiffies 类型的参数 j 分别转换为对应的微秒 |
u64 jiffies_to_nsecs(const unsigned long j) | 将 jiffies 类型的参数 j 分别转换为对应的纳秒 |
long msecs_to_jiffies(const unsigned int m) | 将毫秒转换为 jiffies 类型 |
long usecs_to_jiffies(const unsigned int u) | 将微秒转换为 jiffies 类型 |
unsigned long nsecs_to_jiffies(u64 n) | 将纳秒转换为 jiffies 类型 |
使用内核定时器,不需要设置寄存器,内核提供了定时器结构体和 API 函数。内核使用 timer_list 结构体来表示内核定时器,timer_list 定义在 include/linux/timer.h 中:
struct timer_list {
struct list_head entry;
unsigned long expires; /* 定时器超时时间,单位是节拍数 */
struct tvec_base *base; void (*function)(unsigned long); /* 定时处理函数 */
unsigned long data; /* 要传递给function函数的参数 */
int slack;
};
init_timer 函数用于初始化 timer_list 类型变量,函数原型:
void init_timer(struct timer_list *timer)
timer:要初始化的定时器。
add_timer 函数用于向 Linux内核注册定时器,使用 add_timer 函数向内核注册定时器以后,定时器就会开始运行,函数原型如下:
void add_timer(struct timer_list *timer)
timer:要注册的定时器。
注意:一般重复启动定时器推荐使用 mod_timer。
del_timer 函数用于删除一个定时器,不管定时器有没有被激活,都可以使用此函数删除。在多处理器系统上,定时器可能会在其他的处理器上运行,因此在调用 del_timer 函数删除定时器之前要先等待其他处理器的定时处理器函数退出。函数原型如下:
int del_timer(struct timer_list * timer)
timer:要删除的定时器。
返回值:0,定时器还没被激活; 1,定时器已经激活。
del_timer_sync 函数是 del_timer 函数的同步版,会等待其他处理器使用完定时器再删除,del_timer_sync 不能使用在中断上下文中。函数原型如下所示:
int del_timer_sync(struct timer_list *timer)
timer:要删除的定时器。
返回值:0,定时器还没被激活; 1,定时器已经激活。
mod_timer 函数用于修改定时值,如果定时器还没有激活的话, mod_timer 函数会激活定时器。函数原型如下:
int mod_timer(struct timer_list *timer, unsigned long expires)
timer:要删除的定时器。
expires:超时时间。
返回值:0,调用 mod_timer函数前定时器未被激活; 1,调用 mod_timer函数前定时器已激活。
Linux内核提供了毫秒、微秒和纳秒延时函数,这三个函数如表:
函数 | 描述 |
---|---|
void ndelay(unsigned long nsecs) | 纳秒延时函数 |
void udelay(unsigned long usecs) | 微秒延时函数 |
void mdelay(unsigned long mseces) | 毫秒延时函数 |
实验使用定时器控制 led 亮灭,在根节点下创建设备节点:
gpioled {
compatible = "gpioled_test";
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&gpioled>;
gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;
};
在 iomuxc 节点下的子节点 imx6ul-evk 中添加 pinctrl 节点:
gpioled: ledgrp {
fsl,pins = <
MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10b0
>;
};
/* 设备结构体 */
struct timer_dev {
dev_t devid; //设备号
int major; //主设备号
int minor; //次设备号
struct cdev cdev; //字符设备
struct class *class; //类
struct device *device; //设备
struct spinlock lock; //自旋锁
int gpioled; //led gpio编号
struct device_node *led_nd; //led节点
struct timer_list timer; //定时器
unsigned long expires; //定时时间
};
struct timer_dev timer;
加载和卸载注册函数如下:
module_init(timer_init);
module_exit(timer_exit);
/* 入口函数 */
static int __init timer_init(void)
{
int ret = 0;
spin_lock_init(&timer.lock);
/* 设备号处理 */
timer.major = 0;
if(timer.major){ //指定了主设备号
timer.devid = MKDEV(timer.major, 0);
ret = register_chrdev_region(timer.devid, DEVICE_CNT, DEVICE_NAME);
if(ret < 0){
return -EINVAL;
}
}else{ //没有指定设备号
ret = alloc_chrdev_region(&timer.devid, 0, DEVICE_CNT, DEVICE_NAME);
if(ret < 0){
return -EINVAL;
}
timer.major = MAJOR(timer.devid);
timer.minor = MINOR(timer.devid);
printk("major is: %d\r\nmainr is: %d\r\n",timer.major, timer.minor);
}
/* 注册字符设备 */
timer.cdev.owner = THIS_MODULE;
cdev_init(&timer.cdev, &timer_fops);
ret = cdev_add(&timer.cdev, timer.devid, DEVICE_CNT);
if(ret < 0){
ret = -EINVAL;
goto fail_cdev;
}
/* 自动创建节点 */
timer.class = NULL;
timer.device = NULL;
timer.class = class_create(THIS_MODULE, DEVICE_NAME);
if(timer.class == NULL){
ret = -EINVAL;
goto fail_create_class;
}
timer.device = device_create(timer.class, NULL, timer.devid, NULL, DEVICE_NAME);
if(timer.device == NULL){
ret = -EINVAL;
goto fail_create_device;
}
return 0;
fail_cdev:
unregister_chrdev_region(timer.devid, DEVICE_CNT);
fail_create_class:
cdev_del(&timer.cdev);
unregister_chrdev_region(timer.devid, DEVICE_CNT);
fail_create_device:
class_destroy(timer.class);
cdev_del(&timer.cdev);
unregister_chrdev_region(timer.devid, DEVICE_CNT);
return ret;
}
如果激活了定时器,在卸载模块时,一定要先删除定时器,否则将无法卸载模块。
/* 出口函数 */
static void __exit timer_exit(void)
{
led_switch(LED_OFF);
gpio_free(timer.gpioled); //注销led gpio
del_timer_sync(&timer.timer); //删除定时器
device_destroy(timer.class, timer.devid); //摧毁设备
class_destroy(timer.class); //摧毁类
cdev_del(&timer.cdev); //删除字符设备
unregister_chrdev_region(timer.devid, DEVICE_CNT); //注销设备号
}
将 led gpio 函数放在 open 函数里。
/* gpio初始化函数 */
static int gpio_init(void)
{
/* 获取设备树节点 */
timer.led_nd = of_find_node_by_name(NULL, "gpioled");
if(timer.led_nd == NULL){
printk("fail to find led_nd\r\n");
return -EINVAL;
}
/* 获取gpio编号 */
timer.gpioled = of_get_named_gpio(timer.led_nd, "gpios", 0);
if(timer.gpioled < 0){
printk("fail to find led_nd\r\n");
return -EINVAL;
}
printk("gpio num: %d\r\n",timer.gpioled);
/* 设置gpio */
gpio_request(timer.gpioled,"led");
gpio_direction_output(timer.gpioled, 0);
printk("led init\r\n");
return 0;
}
/* LED状态切换函数 */
void led_switch(int led_state)
{
if(led_state == LED_ON){
gpio_set_value(gpioled.led_gpio, 0); //使用gpio子系统的API函数
}else{
gpio_set_value(gpioled.led_gpio, 1);
}
}
/* 定时器初始化函数 */
void Timer_Init(void)
{
unsigned long flags;
init_timer(&timer.timer); //初始化定时器
timer.timer.function = timer_function; //注册定时回调函数
timer.timer.data = (unsigned long)&timer; //将设备结构体作为回调函数的传入参数
spin_lock_irqsave(&timer.lock, flags); //上锁
timer.timer.expires = jiffies + 2 * HZ; //设置超时时间 2S,给add_timer用
timer.expires = timer.timer.expires; //回调函数中下周期的定时时间
spin_unlock_irqrestore(&timer.lock, flags); //解锁
}
linux 内核的定时器启动后只会运行一次,如果要连续定时,需要在回调函数中重新启动定时器。
/* 定时器回调函数 */
void timer_function(unsigned long arg)
{
unsigned long flags;
static int led_state = 1;
struct timer_dev *dev = (struct timer_dev *)arg;
int time_period = 0;
led_switch(led_state = !led_state); //切换led状态
printk("\r\nled state : %d\r\n", led_state);
/* 重启定时器 */
spin_lock_irqsave(&dev->lock, flags);
time_period = dev->expires;
spin_unlock_irqrestore(&dev->lock, flags);
mod_timer(&dev->timer, jiffies + msecs_to_jiffies(time_period)); //设置定时时间,启动定时器
}
/* open函数 */
static int timer_open(struct inode *inode, struct file *filp)
{
int ret = 0;
filp->private_data = &timer; //设置私有化数据
ret = gpio_init(); //初始化 led gpio
Timer_Init(); //初始化定时器
return ret;
}
/*
* @description : ioctl函数,
* @param – filp : 要打开的设备文件(文件描述符)
* @param - cmd : 应用程序发送过来的命令
* @param - arg : 参数
* @return : 0 成功;其他 失败 110
*/
static long timer_unlocked_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
struct timer_dev *dev = filp->private_data;
unsigned long flags,time;
printk("cmd = %x arg = %ld\r\n",cmd, arg);
switch(cmd){
case TIMER_OPEN:
add_timer(&dev->timer); //注册并启动定时器
break;
case TIMER_CLOSE:
del_timer_sync(&dev->timer); //删除定时器
break;
case TIMER_SET:
spin_lock_irqsave(&dev->lock, flags); //上锁
dev->expires = arg;
spin_unlock_irqrestore(&dev->lock, flags); //解锁
mod_timer(&dev->timer, jiffies + msecs_to_jiffies(arg)); //设置定时时间,启动定时器
break;
default:
break;
}
return 0;
}
/* 操作函数集 */
static const struct file_operations timer_fops = {
.owner = THIS_MODULE,
.open = timer_open,
.unlocked_ioctl = timer_unlocked_ioctl,
};
参考 linux 内核的驱动代码时,找到可能用到的头文件,添加进工程。在调用系统调用函数或库函数时,在终端使用 man 命令可查看调用的函数需要包含哪些头文件。
man 命令数字含义:1:标准命令 2:系统调用 3:库函数
添加以下头文件:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define DEVICE_CNT 1
#define DEVICE_NAME "timer"
#define LED_ON 1
#define LED_OFF 0
#define TIMER_OPEN 0X0F
#define TIMER_CLOSE 0X1F
#define TIMER_SET 0XFF
驱动的 License 是必须的,缺少的话会报错,在文件最末端添加以下代码:
MODULE_LICENSE("GPL");
MODULE_AUTHOR("lzk");
#include
#include
#include
#include
#include
#include
#include
#include "linux/ioctl.h"
#define TIMER_OPEN 0X0F
#define TIMER_CLOSE 0X1F
#define TIMER_SET 0XFF
int main(int argc, char *argv[])
{
int fd = 0;
int ret = 0;
u_int32_t cmd = 0;
u_int32_t arg = 0;
char *filename;
unsigned char str[100];
if(argc != 2){
printf("missing parameter!\r\n");
return -1;
}
filename = argv[1];
fd = open(filename, O_RDWR);
if(fd < 0){
printf("open file %s failed\r\n", filename);
return -1;
}
while(1){
printf("Input CMD:");
scanf("%d", &cmd);
if(cmd == 1) //命令1,打开定时器,按照初始设定闪灯
cmd = TIMER_OPEN;
if(cmd == 2) //命令2,关闭定时器
cmd = TIMER_CLOSE;
if(cmd == 3){ //命令3,设置定时时间
cmd = TIMER_SET;
printf("input time:");
scanf("%d", &arg);
}
ioctl(fd,cmd,arg); //将命令传给内核空间
}
ret = close(fd);
if(ret < 0) //返回值小于0关闭文件失败
{
printf("close file %s failed\r\n", filename);
return -1;
}
return 0;
}