分类: LINUX
2014-04-14 19:27:42
RTC驱动分析
Rtc(real
time clock)实时时钟在嵌入式系统中应用非常广泛,可以为系统提供可靠的实时时间,并且在系统断电的情况下,rtc仍然可以为我们保存和提供正常的时间,最常见的情况是在我们的手机即使在关机之后,重新开机的时候(只要不下电池),我们发现手机的时间仍然准确。另外rtc一般还具有alarm功能,也就是我们说的闹钟功能,是不是对它很有一种亲切和熟悉的感觉。下面我们来揭开rtc的神秘面纱,基于三星4412平台,linux3.0.15,android4.2。
首先我们来了解一下rtc的一些硬件特性:
XrtcxTI和xrtcxTO是外部晶振,向rtc模块提供一个32.768kHz的脉冲信号,经过一个2^15的时钟分频,提供一个1Hz的信号作为BCD时间信号源,同时又可以提供1-32KHz频率信号作为tick时钟产生器的时钟信号源。
SEC,MIN,HOUR DATE DAY MON YEAR以BCD码的形式向系统提供实时时间(CPU通过读取对应寄存器获取实时时间)。BCD时间是指RTC BCD寄存器中每四位二进制数来表示一位十进制数。如3对应BCD寄存器为0011。
Leap year generator:闰年产生器,通过BCDDAY,BCDMON,BCDYEAR来决定每个月的最后一天是28,29,30或者31。在4412平台上,BCDYEAR生成一个12位宽数据,通过BCD转换可以得到一个0-999的数据,因此准确的year数据应为400*n+BCDYEAR(n=0,1,2,3…)。
Alarm generator:alarm信号产生器。系统可向rtc模块设置报警时间,当实时时间与设置的报警时间相等时,在普通模式下,rtc模块产生一个ALARM_INT中断信号(闹钟响了),在下电模式下,rtc模块产生一个ALARM_WK和ALARM_INT中断信号。
Time tick generator:滴答计数器,可以通过设置rtc控制寄存器来改变tick计数频率,其范围为1HZ-32.768KHZ:
当设置TICK频率为NHZ时,每过1/N秒时间tick计数器值TICUR增1,当tick计数器值与目标值TICNT相等时,产生一个TICK中断。因此,tick计数器可以作为一个不同精度的定时器来使用。
后备电池可以驱动RTC逻辑模块,电池通过RTCVDD引脚向模块供电,当系统下电的时候,应限制RTC与CPU之间的接口,只供应晶振时钟和BCD计数,这样可以将功耗降至最低。
下面结合linux内核和android系统来看看rtc模块是怎么工作的。
在board文件中定义了RTC的系统资源:
static struct resource s3c_rtc_resource[] = {
[0] = {
.start = S3C_PA_RTC,//模块寄存器起始地址
.end = S3C_PA_RTC + 0xff,
.flags = IORESOURCE_MEM,
},
[1] = {
.start = IRQ_RTC_ALARM,//ALARM中断号
.end = IRQ_RTC_ALARM,
.flags = IORESOURCE_IRQ,
},
[2] = {
.start = IRQ_RTC_TIC,//TICK中断号
.end = IRQ_RTC_TIC,
.flags = IORESOURCE_IRQ
}
};
struct platform_device s3c_device_rtc = {
.name = "s3c64xx-rtc",
.id = -1,
.num_resources = ARRAY_SIZE(s3c_rtc_resource),
.resource = s3c_rtc_resource,
};//定义一个paltform_device,关联其系统资源。
在系统启动过程中,向系统注册这个platform_device,然后当加载RTC驱动模块的时候,完成设备与驱动的匹配,platform_device和platform_driver的匹配这里就不详细说明了。
设备与驱动匹配成功之后,进入设备初始化阶段,也就是driver的probe函数:
static int __devinit s3c_rtc_probe(struct platform_device *pdev)
{
struct rtc_device *rtc;
struct rtc_time rtc_tm;
struct resource *res;
int ret;
s3c_rtc_alarmno = platform_get_irq(pdev, 0);//获取alarm中断号
……
/* get the memory region */
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
……
s3c_rtc_mem = request_mem_region(res->start,
resource_size(res),
pdev->name);
s3c_rtc_base = ioremap(res->start, resource_size(res));
//获取相关寄存器地址,并完成重映射
rtc_clk = clk_get(&pdev->dev, "rtc");
……
clk_enable(rtc_clk);
//使能rtc时钟
/* check to see if everything is setup correctly */
s3c_rtc_enable(pdev, 1);//使能rtc模块,设置相关控制寄存器
device_init_wakeup(&pdev->dev, 1);//设置设备能被电源管理系统唤醒
/* register RTC and exit */
rtc = rtc_device_register("s3c", &pdev->dev, &s3c_rtcops,
THIS_MODULE); //向系统注册一个rtc设备,并关联其设备操作接口,这里将是我们分析的重点
……
ret = request_irq(s3c_rtc_alarmno, s3c_rtc_alarmirq,
IRQF_DISABLED, "s3c2410-rtc alarm", rtc);//向系统注册alarm中断
……
return 0;
……
}
在RTC驱动模块中定义并实现RTC硬件接口rtc_class_ops:
static const struct rtc_class_ops s3c_rtcops = {
.read_time = s3c_rtc_gettime,//获取RTC实时时间
.set_time = s3c_rtc_settime,//设置RTC实时时间
.read_alarm = s3c_rtc_getalarm,//获取RTC ALARM报警时间
.set_alarm = s3c_rtc_setalarm,//设置RTC ALARM报警时间
.alarm_irq_enable = s3c_rtc_setaie,//RTC硬件中断处理接口,一般用于清除或者设置硬件中断
};
当注册RTC设备的时候,rtc_class_ops作为参数传递给rtc_device_register()接口,系统对RTC的处理最终会通过其硬件接口去控制硬件行为。我们可以各个接口的实现上可以看出,例如read_time即是读取rtc的BCDYEAR,BCDMON,BCDDAY,BCDDATE,BCDHOUR,BCDMIN,BCDSEC等各寄存器的值,并进行处理,得到一个标准格式的实时时间。
下面我们来看看注册RTC设备过程:
struct rtc_device *rtc_device_register(const char *name, struct device *dev,
const struct rtc_class_ops *ops,
struct module *owner)
{
struct rtc_device *rtc;
struct rtc_wkalrm alrm;
int id, err;
if (idr_pre_get(&rtc_idr, GFP_KERNEL) == 0) {
err = -ENOMEM;
goto exit;
}
mutex_lock(&idr_lock);
err = idr_get_new(&rtc_idr, NULL, &id);//系统分配设备id
mutex_unlock(&idr_lock);
if (err < 0)
goto exit;
id = id & MAX_ID_MASK;//系统中可能会存在多个RTC设备,像我所使用的4412平台上就有两个RTC设备,一个是平台RTC,另外一个是PMIC上的RTC。为了便于管理,系统会为每个RTC设备分配一个id。
rtc = kzalloc(sizeof(struct rtc_device), GFP_KERNEL);//为rtc设备分配内存
……
rtc->id = id;
rtc->ops = ops;//关联其硬件接口
rtc->owner = owner;
rtc->irq_freq = 1;
rtc->max_user_freq = 64;
rtc->dev.parent = dev;
rtc->dev.class = rtc_class;//设置rtc_device为rtc_class类,便于上层系统与linux内核之间RTC的数据交互。
rtc->dev.release = rtc_device_release;
mutex_init(&rtc->ops_lock);
spin_lock_init(&rtc->irq_lock);
spin_lock_init(&rtc->irq_task_lock);//初始化硬件接口锁,中断锁,中断任务锁
init_waitqueue_head(&rtc->irq_queue);
/* Init timerqueue */
timerqueue_init_head(&rtc->timerqueue);
INIT_WORK(&rtc->irqwork, rtc_timer_do_work);//初始化RTC中断处理的工作队列
/* Init aie timer */
rtc_timer_init(&rtc->aie_timer, rtc_aie_update_irq, (void *)rtc);
/* Init uie timer */
rtc_timer_init(&rtc->uie_rtctimer, rtc_uie_update_irq, (void *)rtc);//初始化其相关定时器
/* Init pie timer */
hrtimer_init(&rtc->pie_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);//初始化RTC设备高精度定时器
rtc->pie_timer.function = rtc_pie_update_irq;
rtc->pie_enabled = 0;
/* Check to see if there is an ALARM already set in hw */
err = __rtc_read_alarm(rtc, &alrm);
if (!err && !rtc_valid_tm(&alrm.time))
rtc_initialize_alarm(rtc, &alrm);
strlcpy(rtc->name, name, RTC_DEVICE_NAME_SIZE);
dev_set_name(&rtc->dev, "rtc%d", id);
/*
设置设备名,可以系统/dev/目录下查看到该设备文件
*/
rtc_dev_prepare(rtc);
/*
在这里为设备分配主次设备号,所有的rtc设备使用相同的主设备号,其次设备号为系统分配的id,系统最多可存在16个RTC设备,同时初始化RTC字符设备,关联其字符设备操作接口rtc_dev_fops,因此我们可以通过操作/dev/rtc*设备文件调用文件操作接口rtc_dev_fops实现对rtc硬件的控制。
*/
err = device_register(&rtc->dev);
/*
向系统注册一个设备,在这里需要关注的是注册设备过程中device_add中有:
if (dev->class) {//在前面我们有设置设备类为rtc_class
mutex_lock(&dev->class->p->class_mutex);
/* tie the class to the device */
klist_add_tail(&dev->knode_class,
&dev->class->p->klist_devices);
/* notify any interfaces that the device is here */
list_for_each_entry(class_intf,
&dev->class->p->class_interfaces, node)//遍历该设备所属类的类接口
if (class_intf->add_dev)
class_intf->add_dev(dev, class_intf);//调用类接口的接口函数将设备添加到该类进行管理
mutex_unlock(&dev->class->p->class_mutex);
}
为什么要将这一段单独列出来作以说明,是因为后面我们会有用到
*/
rtc_dev_add_device(rtc);//向系统添加rtc的字符设备
rtc_sysfs_add_device(rtc);//创建rtc的属性文件
rtc_proc_add_device(rtc);//创建rtc的proc文件
dev_info(dev, "rtc core: registered %s as %s\n",
rtc->name, dev_name(&rtc->dev));
return rtc;
……
}
就这样,一个RTC设备就被注册到了系统,设备注册完成之后,会在/dev/目录下生成rtc*的设备文件,且该设备文件操作接口也已实现,这样我们可以上层通过对/dev/rtc*设备文件的read/write等操作完成对实时时间或者alarm时间的设备和获取。同时还会在/sys/class目录下生成其属性文件,因此我们也可以通过对属性文件的读写来获取或者设置实时时间或者alarm时间。还会在/proc/目录下生成proc文件,因此我们有很多种方式可以对rtc设备进行管理和数据交互。但是android是怎么对RTC进行处理的呢?
在RTC的JNI中有:
static jint android_server_AlarmManagerService_init(JNIEnv* env, jobject obj)
{
return open("/dev/alarm", O_RDWR);//打开alarm文件
}
static void android_server_AlarmManagerService_close(JNIEnv* env, jobject obj, jint fd)
{
close(fd);
}
static void android_server_AlarmManagerService_set(JNIEnv* env, jobject obj, jint fd, jint type, jlong seconds, jlong nanoseconds)
{
struct timespec ts;
ts.tv_sec = seconds;
ts.tv_nsec = nanoseconds;
int result = ioctl(fd, ANDROID_ALARM_SET(type), &ts);//设置RTC实时时间
if (result < 0)
{
ALOGE("Unable to set alarm to %lld.%09lld: %s\n", seconds, nanoseconds, strerror(errno));
}
}
static jint android_server_AlarmManagerService_waitForAlarm(JNIEnv* env, jobject obj, jint fd)
{
int result = 0;
do
{
result = ioctl(fd, ANDROID_ALARM_WAIT);
} while (result < 0 && errno == EINTR);
if (result < 0)
{
ALOGE("Unable to wait on alarm: %s\n", strerror(errno));
return 0;
}
return result;
}
让人疑惑的是在android的jni中没有采用rtc_device_register()为我们提供的各种操作方式,而是通过对/dev/alarm设备进行操作,并且通过ioctl ANDROID_ALARM_SET和ANDROID_ALARM_WAIT命令来设置实时时间和wait alarm事件。那么我们来看看这究竟是怎么回事。
在linux内核的/driver/rtc/目录下有一个alarm-dev.c的文件,在这个模块被加载的时候:
static struct miscdevice alarm_device = {
.minor = MISC_DYNAMIC_MINOR,
.name = "alarm",
.fops = &alarm_fops,
};
/*
在这里定义了一个name为alarm的混杂设备,并实现了其设备文件操作接口alarm_fops。
static int __init alarm_dev_init(void)
{
int err;
int i;
err = misc_register(&alarm_device);
/*
注册一个混杂设备,在系统/dev/目录下生成alarm设备文件,模块中实现了对该设备文件的操作接口:
static const struct file_operations alarm_fops = {
.owner = THIS_MODULE,
.unlocked_ioctl = alarm_ioctl,
.open = alarm_open,
.release = alarm_release,
};
在自定义命令接口alarm_ioctl中我们找到了在android jni中使用的ANDROID_ALARM_SET和ANDROID_ALARM_WAIT命令的实现:
static long alarm_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
……
switch (ANDROID_ALARM_BASE_CMD(cmd)) {
……
case ANDROID_ALARM_WAIT:
…….
break;
case ANDROID_ALARM_SET_RTC:
if (copy_from_user(&new_rtc_time, (void __user *)arg,
sizeof(new_rtc_time))) {
rv = -EFAULT;
goto err1;
}
rv = alarm_set_rtc(new_rtc_time);//设置实时时间
/*
而alarm_set_rtc()又会调用alarm.c文件中的rtc_set_time()再调用/driver/rtc/目录下interface.c的rtc_set_time:
int rtc_set_time(struct rtc_device *rtc, struct rtc_time *tm)
{
int err;
err = rtc_valid_tm(tm);//检测设置的时间是否有效
…….
if (!rtc->ops)
err = -ENODEV;
else if (rtc->ops->set_time)
err = rtc->ops->set_time(rtc->dev.parent, tm);
/*
调用rtc_device的硬件接口设置rtc实时时间,终于来到了rtc_device驱动里头了
*/
else if (rtc->ops->set_mmss) {
unsigned long secs;
err = rtc_tm_to_time(tm, &secs);
……
return err;
}
*/
……
break;
…..
}
}
……
return 0;
}
*/
……
}
上面我们看到了android jni通过对混杂设备alarm实现了对rtc实时时间的设置,那么混杂设备alarm又是如何与rtc设备关联起来的呢?
在linux内核/drvier/rtc目录下有alarm.c文件中,该模块加载时:
static int __init alarm_driver_init(void)
{
……
rtc_alarm_interface.class = rtc_class;//该类接口为rtc_class类型
err = class_interface_register(&rtc_alarm_interface);
/*
在这里注册了一个rtc_alarm_interface的类接口,我们再来看看这个类接口的定义:
static struct class_interface rtc_alarm_interface = {
.add_dev = &rtc_alarm_add_device,
.remove_dev = &rtc_alarm_remove_device,
};
而上面在注册rtc设备过程中device的class也被设置为rtc_class类,并且在向系统添加设备的时候会遍历该类型class的类接口,这时候就会找到这里注册的这个rtc_alarm_interface类接口,并调用其add_dev接口
*/
……
return 0;
…….
}
static int rtc_alarm_add_device(struct device *dev,
struct class_interface *class_intf)
{
int err;
struct rtc_device *rtc = to_rtc_device(dev);//通过dev获取到注册rtc设备时初始化好的rtc_device
mutex_lock(&alarm_setrtc_mutex);
if (alarm_rtc_dev) {
/*
假如alarm_rtc_dev已经被赋予了一个rtc_device,则alarm_rtc_dev不再被赋值,因为我们的系统中只需要使用其中一个rtc设备就足够了,如果需要使用多个,则这里就需要更改
*/
err = -EBUSY;
goto err1;
}
alarm_platform_dev =
platform_device_register_simple("alarm", -1, NULL, 0);//向系统注册一个alarm的platform设备。
if (IS_ERR(alarm_platform_dev)) {
err = PTR_ERR(alarm_platform_dev);
goto err2;
}
err = rtc_irq_register(rtc, &alarm_rtc_task);//注册该platform设备的中断处理任务
if (err)
goto err3;
alarm_rtc_dev = rtc;
/*
将rtc赋予alarm_rtc_dev,上面分析android对rtc的实时时间的设置流程中有调用了alarm.c中的rtc_set_time()中有:
spin_unlock_irqrestore(&alarm_slock, flags);
ret = do_settimeofday(&new_time);//更新系统时间,这时系统时间将与即将设置的硬件时间保持同步
spin_lock_irqsave(&alarm_slock, flags);
ret = rtc_set_time(alarm_rtc_dev, &rtc_new_rtc_time);
我们可以看到,在这里对实时时间的设置对rtc_device是将alarm_rtc_dev做为参数传递给interface.c里的接口的,而这个变量它只会被赋值一次,也就是说它是唯一的,不变的,也就是说在流程当中我们只会对固定的一个rtc设备进行操作。不过我们也可以看到在RTC驱动当中,更新实时时间的时候,会对所有的RTC设备同时更新。
*/
printk("using rtc device, %s, for alarms", rtc->name);
mutex_unlock(&alarm_setrtc_mutex);
return 0;
……
}
到这里,从RTC设备驱动到ALARM再到android 上层对ALARM的操作全部关联起来了。当上层对系统时间更改的时候,会通过jni将时间通过alarm设备的ioctl ANDROID_ALARM_SET命令设置到RTC设备的set_time接口,从而改变RTC实时时间。
当系统上电开机的时候,会从RTC中读取实时时间,并设置为系统时间,而在系统下电的时候,RTC晶振和BCD计数处于活动状态,因此保存在RTC中的实时时间会与真实的时间保持同步,所以开机的时候读取出来的RTC实时时间也是准确的时间。