今天硬件提了需求需要在os启动之后让指示灯闪烁用于反应CPU的工作状态,于是写了个led定时器驱动,顺便把相关资料整理了下。
linux内核定时器是内核用来控制在未来某个时间点(基于jiffies)调度执行某个函数的一种机制,其实现位于和kernel/timer.c文件中。
被调度的函数肯定是异步执行的,它类似于一种“软件中断”,而且是处于非进程的上下文中,所以调度函数必须遵守以下规则:
1)没有current指针、不允许访问用户空间。因为没有进程上下文,相关代码和被中断的进程没有任何联系。
2)不能执行休眠(或可能引起休眠的函数)和调度。
3)任何被访问的数据结构都应该针对并发访问进行保护,以防止竞争条件。
内核定时器的调度函数运行过一次后就不会再被运行了(相当于自动注销),但可以通过在被调度的函数中重新调度自己来周期运行。
在SMP系统中,调度函数总是在注册它的同一CPU上运行,以尽可能获得缓存的局域性。
内核定时器的数据结构
点击(此处)折叠或打开
struct timer_list{
struct list_head entry;
unsigned long expires;
void(*function)(unsigned long);
unsigned long data;
struct tvec_base*base;
/* ... */
};
其中expires字段表示期望定时器执行的jiffies值,到达该jiffies值时,将调用function函数,并传递data作为参数。当一个定时器被注册到内核之后,entry字段用来连接该定时器到一个内核链表中。base字段是内核内部实现所用的。
需要注意的是expires的值是32位的,因为内核定时器并不适用于长的未来时间点。
初始化
在使用struct timer_list之前,需要初始化该数据结构,确保所有的字段都被正确地设置。初始化有两种方法。
方法一:
DEFINE_TIMER(timer_name, function_name, expires_value, data);
该宏会静态创建一个名叫timer_name内核定时器,并初始化其function, expires, name和base字段。
方法二:
点击(此处)折叠或打开
structtimer_list mytimer;
setup_timer(&mytimer, (*function)(unsignedlong),unsignedlong data);
mytimer.expires=jiffies+5*HZ;
方法3:
点击(此处)折叠或打开
struct timer_list mytimer;
init_timer(&mytimer);
mytimer->timer.expires=jiffies+5*HZ;
mytimer->timer.data= (unsigned long)dev;
mytimer->timer.function = &corkscrew_timer; /*timer handler*/
通过init_timer()动态地定义一个定时器,此后,将处理函数的地址和参数绑定给一个timer_list,
注意,无论用哪种方法初始化,其本质都只是给字段赋值,所以只要在运行add_timer()之前,expires, function和data字段都可以直接再修改。
关于上面这些宏和函数的定义,参见include/linux/timer.h。
注册
定时器要生效,还必须被连接到内核专门的链表中,这可以通过add_timer(struct timer_list *timer)来实现。
重新注册
要修改一个定时器的调度时间,可以通过调用mod_timer(struct timer_list *timer, unsigned long expires)。mod_timer()会重新注册定时器到内核,而不管定时器函数是否被运行过。
注销
注销一个定时器,可以通过del_timer(struct timer_list *timer)或del_timer_sync(struct timer_list *timer)。其中del_timer_sync是用在SMP系统上的(在非SMP系统上,它等于del_timer),当要被注销的定时器函数正在另一个cpu上运行时,del_timer_sync()会等待其运行完,所以这个函数会休眠。另外还应避免它和被调度的函数争用同一个锁。对于一个已经被运行过且没有重新注册自己的定时器而言,注销函数其实也没什么事可做。
int timer_pending(const struct timer_list *timer)这个函数用来判断一个定时器是否被添加到了内核链表中以等待被调度运行。注意,当一个定时器函数即将要被运行前,内核会把相应的定时器从内核链表中删除(相当于注销)
实例
点击(此处)折叠或打开
#include
#include
#include
#include
#include
#include
#include
#include
#define LED_VERSION "0.1"
/*GPIO基地址*/
#define GPIO_REG_BASE 0x12150000
/*芯片管脚复用寄存器基地址*/
#define MUXCTRL_REG_BASE 0x120f0000
static struct timer_list led_blink_timer;
static char __iomem *gpio_led_addr = NULL;
static char __iomem *muxctrl_addr = NULL;
static inline void led_loggle(void)
{
static int count = 0;
/*配置GPIO0_4管脚复用为功能0*/
(*(volatile unsigned int *)(muxctrl_addr +0x158)) = 0x0;
/*配置GPIO0_4为输出*/
(*(volatile unsigned int *)(gpio_led_addr +0x400)) = 0x10;
if(0 == count % 2)/*点灯*/
{
/*PADDR[9:2]分别对应GPIO_DATA[7:0]*/
(*(volatile unsigned int *)(gpio_led_addr +0x40)) = 0x0;
}
else/*灭灯*/
{
(*(volatile unsigned int *)(gpio_led_addr +0x40)) = 0x10;
}
}
static void led_blink(unsigned long timeout)
{
led_loggle();
led_blink_timer.data=0;
led_blink_timer.expires=jiffies +(HZ/2);
add_timer(&led_blink_timer);
}
static int __init led_init(void)
{
gpio_led_addr = ioremap_nocache(GPIO_REG_BASE, 0x1000);
if(NULL == gpio_led_addr)
{
printk(KERN_ERR"gpio register space ioremap failed.\n");
return -EIO;
}
muxctrl_addr = ioremap_nocache(MUXCTRL_REG_BASE, 0x1000);
if(NULL == muxctrl_addr)
{
printk(KERN_ERR"muxctrl register space ioremap failed.\n");
iounmap((void*)gpio_led_addr);
return -EIO;
}
init_timer(&led_blink_timer);/*定时器初始化*/
led_blink_timer.function = led_blink;
led_blink_timer.data=0;
led_blink_timer.expires=jiffies +(HZ/2);
add_timer(&led_blink_timer);/*定时器注册*/
printk(KERN_INFO"led:version %s.led driver init successful!\n",LED_VERSION);
return 0;
}
static void __exit led_exit(void)
{
del_timer_sync((&led_blink_timer);/*定时器注销*/
iounmap((void*)gpio_led_addr);
iounmap((void*)muxctrl_addr);
}
module_init(led_init);
module_exit(led_exit);
MODULE_AUTHOR("uniquexch");
MODULE_DESCRIPTION("IVS LED Driver");
MODULE_LICENSE("GPL");
MODULE_VERSION(LED_VERSION)
代码调试修改bug
由于想实现指示灯闪烁功能,一开始在led_loggle函数中点灯与关灯过程中加了msleep(500),加载驱动后运行异常,违反了不能执行休眠(或可能引起休眠的函数)和调度;于是将定时器成员expires调整为原先的1/2,led_loggle修改为点灯与关灯切换(同时只关心取余值是否为0,因而不用担心变量count溢出的影响)