参考资料:韦东山第二期经典视频教程
开发环境:JZ2440V3开发板+Linux3.4.2内核+gcc4.3.2
1、rtc内核驱动分析
linux内核中的rtc驱动位于drivers/rtc下,里面有许多开发平台的RTC驱动,我们这里是以S3C24xx为主,所以它的RTC驱动为rtc-s3c.c
1.1 ./drivers/rtc/rtc-s3c.c
- 首先进入入口函数,里面注册了一个
s3c2410-rtc
的平台设备驱动,当内核匹配到s3c2410-rtc平台设备,就会调用.probe函数s3c_rtc+probe:
- 而
s3c2410-rtc
这个平台设备是在arch/arm/plat-s3c24xx/dev.c
里定义的,但此时它仅仅是定义了,还没有注册到内核中,所以内核还看不到它:
- 接下来进入s3c2410_rtcdrv->probe函数中,看它的作用是什么:
static int s3c_rtc_probe(struct platform_device *pdev)
{
struct rtc_device *rtc; //rtc设备结构体
struct resource *res;
int ret;
s3c_rtc_tickno = platform_get_irq(pdev, 1); //获取IRQ_TICK节拍中断资源
s3c_rtc_alarmno = platform_get_irq(pdev, 0); //获取IRQ_RTC闹钟中断资源
res = platform_get_resource(pdev, IORESOURCE_MEM, 0); //获取内存资源
s3c_rtc_mem = request_mem_region(res->start,res->end-res->start+1,pdev->name);//申请内存资源
s3c_rtc_base = ioremap(res->start, res->end - res->start + 1); //对内存进行重映射
s3c_rtc_enable(pdev, 1); //设置硬件相关设置,使能RTC寄存器
s3c_rtc_setfreq(s3c_rtc_freq); //设置TICONT寄存器,使能节拍中断,设置节拍计数值
/*1.注册RTC设备*/
rtc = rtc_device_register("s3c", &pdev->dev, &s3c_rtcops,THIS_MODULE);
rtc->max_user_freq = 128;
platform_set_drvdata(pdev, rtc);
return 0;
}
显然probe最终会调用rtc_device_register()
函数来向内核注册rtc_device设备。注册成功会返回一个已注册好的rtc_device。
而s3c_rtcops
是一个rtc_class_ops结构体,里面保存了如何操作这个rtc设备的函数,比如读写RTC时间、读写闹钟时间等。注册成功后,会保存在rtc_device->ops里
该函数在drivers/rtc/Class.c文件内被定义,Class.c文件主要定义了RTC子系统。
而内核初始化,便会进入Class.c,通过执行rtc_init()->rtc_dev_init()来注册字符设备:err = alloc_chrdev_region(&rtc_devt, 0, RTC_DEV_MAX, "rtc");
- rtc_device_register()
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; //定义一个rtc_device结构体
... ...
rtc = kzalloc(sizeof(struct rtc_device), GFP_KERNEL); //分配rtc_device结构体为全局变量
/*设置rtc_device*/
rtc->id = id;
rtc->ops = ops; //将s3c_rtcops保存在rtc_device->ops里
rtc->owner = owner;
rtc->max_user_freq = 64;
rtc->dev.parent = dev;
rtc->dev.class = rtc_class;
rtc->dev.release = rtc_device_release;
... ...
rtc_dev_prepare(rtc); //1.做提前准备,初始化cdev结构体
... ...
rtc_dev_add_device(rtc); //2.在/dev下创建rtc相关文件,将cdev添加到系统中
rtc_sysfs_add_device(rtc); //在/sysfs下创建rtc相关文件
rtc_proc_add_device(rtc); //在/proc下创建rtc相关文件
... ...
return rtc;
}
上面的rtc_dev_prepare(rtc)
和rtc_dev_add_device(rtc)
主要做了以下两个事情(位于./drivers/rtc/rtc-dev.c):
cdev_init(&rtc->char_dev, &rtc_dev_fops); //绑定file_operations
cdev_add(&rtc->char_dev, rtc->dev.devt, 1); //注册rtc->char_dev字符设备,添加一个从设备到系统中
此处,和注册字符设备驱动的流程相似。
- 小结:所以“s3c2410-rtc”平台设备驱动的.probe主要做了以下几件事:
- 1.设置RTC相关寄存器
- 2.分配rtc_device结构体
- 3.设置rtc_device结构体
- -> 3.1 将struct rtc_class_ops s3c_rtcops放入rtc_device->ops,实现对RTC读写时间等操作
- 4 注册rtc->char_dev字符设备,且该字符设备的操作结构体为: struct file_operations rtc_dev_fops
1.2 rtc_dev_fops
- 当我们应用层open(”/dev/rtcXX”)时,就会调用rtc_dev_fops-> rtc_dev_open():
static int rtc_dev_open(struct inode *inode, struct file *file)
{
struct rtc_device *rtc = container_of(inode->i_cdev,struct rtc_device, char_dev);//获取对应的rtc_device
const struct rtc_class_ops *ops = rtc->ops; //最终等于s3c_rtcops
file->private_data = rtc; //设置file结构体的私有成员等于rtc_device,再次执行ioctl等函数时,直接就可以提取file->private_data即可
err = ops->open ? ops->open(rtc->dev.parent) : 0; //调用s3c_rtcops->open
mutex_unlock(&rtc->char_lock);
return err;
}
显然最终还是调用rtc_device下的s3c_rtcops->open:
而s3c_rtc_open()函数里主要是申请了两个中断,一个闹钟中断,一个计时中断:
static int s3c_rtc_open(struct device *dev)
{
struct platform_device *pdev = to_platform_device(dev);
struct rtc_device *rtc_dev = platform_get_drvdata(pdev);
int ret;
ret = request_irq(s3c_rtc_alarmno, s3c_rtc_alarmirq,IRQF_DISABLED, "s3c2410-rtc alarm", rtc_dev); //申请闹钟中断
if (ret) {
dev_err(dev, "IRQ%d error %d\n", s3c_rtc_alarmno, ret);
return ret;
}
ret = request_irq(s3c_rtc_tickno, s3c_rtc_tickirq,IRQF_DISABLED, "s3c2410-rtc tick", rtc_dev);//申请计时中断
if (ret) {
dev_err(dev, "IRQ%d error %d\n", s3c_rtc_tickno, ret);
goto tick_err;
}
return ret;
tick_err:
free_irq(s3c_rtc_alarmno, rtc_dev);
return ret;
}
- 当我们应用层open后,使用 ioctl(int fd, unsigned long cmd, ...)时,就会调用rtc_dev_fops-> rtc_dev_ioctl ():
static int rtc_dev_ioctl(struct inode *inode, struct file *file,unsigned int cmd, unsigned long arg)
{
struct rtc_device *rtc = file->private_data; //提取rtc_device
void __user *uarg = (void __user *) arg;
... ...
switch (cmd) {
case RTC_EPOCH_SET:
case RTC_SET_TIME: //设置时间
if (!capable(CAP_SYS_TIME))
return -EACCES;
break;
case RTC_IRQP_SET: //改变中断触发速度
... ...}
... ...
switch (cmd) {
case RTC_ALM_READ: //读闹钟时间
err = rtc_read_alarm(rtc, &alarm); //调用s3c_rtcops-> read_alarm
if (err < 0)
return err;
if (copy_to_user(uarg, &alarm.time, sizeof(tm))) //长传时间数据
return -EFAULT;
break;
case RTC_ALM_SET: //设置闹钟时间 , 调用s3c_rtcops-> set_alarm
... ...
case RTC_RD_TIME: //读RTC时间, 调用s3c_rtcops-> read_alarm
... ...
case RTC_SET_TIME: //写RTC时间,调用s3c_rtcops-> set_time
... ...
case RTC_IRQP_SET: //改变中断触发频率,调用s3c_rtcops-> irq_set_freq
... ...
}
最终还是调用s3c_rtcops下的成员函数,我们以s3c_rtcops-> read_alarm()函数为例,看看如何读出时间的:
static int s3c_rtc_gettime(struct device *dev, struct rtc_time *rtc_tm)
{
unsigned int have_retried = 0;
void __iomem *base = s3c_rtc_base; //获取RTC相关寄存器基地址
retry_get_time:
/*获取年,月,日,时,分,秒寄存器*/
rtc_tm->tm_min = readb(base + S3C2410_RTCMIN);
rtc_tm->tm_hour = readb(base + S3C2410_RTCHOUR);
rtc_tm->tm_mday = readb(base + S3C2410_RTCDATE);
rtc_tm->tm_mon = readb(base + S3C2410_RTCMON);
rtc_tm->tm_year = readb(base + S3C2410_RTCYEAR);
rtc_tm->tm_sec = readb(base + S3C2410_RTCSEC);
/* 判断秒寄存器中是0,则表示过去了一分钟,那么小时,天,月,等寄存器中的值都可能已经变化,需要重新读取这些寄存器的值*/
if (rtc_tm->tm_sec == 0 && !have_retried) {
have_retried = 1;
goto retry_get_time;
}
/*将获取的寄存器值,转换为真正的时间数据*/
BCD_TO_BIN(rtc_tm->tm_sec);
BCD_TO_BIN(rtc_tm->tm_min);
BCD_TO_BIN(rtc_tm->tm_hour);
BCD_TO_BIN(rtc_tm->tm_mday);
BCD_TO_BIN(rtc_tm->tm_mon);
BCD_TO_BIN(rtc_tm->tm_year);
rtc_tm->tm_year += 100; //存储器中存放的是从1900年开始的时间,所以加上100
rtc_tm->tm_mon -= 1;
return 0;
}
同样, 在s3c_rtcops-> set_time()函数里,也是向相关寄存器写入RTC时间
所以,总结如下所示:
- rtc_device->char_dev: 字符设备,与应用层、以及更底层的函数打交道
- rtc_device->ops: 更底层的操作函数,直接操作硬件相关的寄存器,被rtc_device->char_dev调用
2、修改内核,打开rtc设备支持
我们单板上使用ls /dev/rtc*
找不到该字符设备, 因为内核里只定义了s3c_device_rtc这个RTC平台设备,没有注册,所以平台驱动没有被匹配上,接下来我们来修改内核里的注册数组:
- 进入
arch/arm/plat-s3c24xx/Common-smdk.c
,内核版本2.6以后的进入arch/arm/mach-s3c24xx/Common-smdk.c
- 在
smdk_devs[]
里,添加RTC的平台设备即可(当内核启动时,就会调用该数组,将里面的platform_device统统注册一遍)
- 重新编译、加载新内核
3、测试
启动后,使用ls /dev/rtc*
,如果能看到rtc0这个字符设备,就代表修改成功了
3.1 查看时间
在linux里有两个时钟:
硬件时钟(2440里寄存器的时钟)、系统时钟(内核中的时钟)
所以对应有两个不同的命令: date命令、hwclock命令
输入date
命令,就可以查看系统时钟:
# date
Wed Nov 3 12:23:20 UTC 2021
也可以指定格式显示日期, date "+ %Y/%m/%d %H:%M:%S"
# date "+ %Y/%m/%d %H:%M:%S"
2021/11/03 12:23:20
3.2 设置时间
命令格式:date 月日时分年.秒
# date 081512302021.30
Sun Aug 15 12:30:30 UTC 2021
3.3 同步到硬件时间
命令:hwclock
参数:
-r, --show 读取并打印硬件时钟(read hardware clock and print result )
-s, --hctosys 将硬件时钟同步到系统时钟(set the system time from the hardware clock )
-w, --systohc 将系统时钟同步到硬件时钟(set the hardware clock to the current system time )
# hwclock -r
Wed Nov 3 12:23:20 2021 0.000000 seconds //同步前的时间
#
# date 081512302021.30
Sun Aug 15 12:30:30 UTC 2021 //系统当前时间
#
# hwclock -w //同步到硬件时间
#
# hwclock -r
Sun Aug 15 12:30:33 2021 0.000000 seconds //已经和系统时间同步的硬件时间