抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

RTC 也就是实时时钟,用于记录当前系统时间,对于 Linux 系统而言时间是非常重要的,就和我们使用 Windows 电脑或手机查看时间一样,我们在使用 Linux 设备的时候也需要查看时间。

  • I.MX6U 内部也有个 RTC 模块,但是不叫作“RTC”,而是叫做“SNVS”。

  • SNVS 直译过来就是安全的非易性存储, SNVS 里面主要是一些低功耗的外设,包括一个安全的实时计数器(RTC)、一个单调计数器(monotonic counter)和一些通用的寄存器

  • SNVS 分为两个子模块: SNVS_HP 和 SNVS_LP,也就是高功耗域(SNVS_HP)和低功耗域(SNVS_LP),这两个域的电源来源如下:

    • SNVS_LP:专用的 always-powered-on 电源域,系统主电源和备用电源都可以为其供电。

    • SNVS_HP:系统(芯片)电源。

1.Linux下RTC驱动简析

RTC 设备驱动是一个标准的字符设备驱动,应用程序通过 open、 release、 read、 write 和 ioctl等函数完成对 RTC 设备的操作。

Linux 内核将 RTC 设备抽象为 rtc_device 结构体,因此 RTC 设备驱动就是申请并初始化rtc_device,最后将 rtc_device 注册到 Linux 内核里面,这样 Linux 内核就有一个 RTC 设备了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
struct rtc_device
{
struct device dev; /* 设备 */
struct module *owner;

int id; /* ID */
char name[RTC_DEVICE_NAME_SIZE]; /* 名字 */

const struct rtc_class_ops *ops; /* RTC 设备底层操作函数 */
struct mutex ops_lock;

struct cdev char_dev; /* 字符设备 */
unsigned long flags;

unsigned long irq_data;
spinlock_t irq_lock;
wait_queue_head_t irq_queue;
struct fasync_struct *async_queue;

struct rtc_task *irq_task;
spinlock_t irq_task_lock;
int irq_freq;
int max_user_freq;

struct timerqueue_head timerqueue;
struct rtc_timer aie_timer;
struct rtc_timer uie_rtctimer;
struct hrtimer pie_timer; /* sub second exp, so needs hrtimer */
int pie_enabled;
struct work_struct irqwork;
/* Some hardware can't support UIE mode */
int uie_unsupported;
};

重点关注的是 ops 成员变量,这是一个 rtc_class_ops 类型的指针变量, rtc_class_ops为 RTC 设备的最底层操作函数集合,包括从 RTC 设备中读取时间、向 RTC 设备写入新的时间值等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct rtc_class_ops {
int (*open)(struct device *);
void (*release)(struct device *);
int (*ioctl)(struct device *, unsigned int, unsigned long);
int (*read_time)(struct device *, struct rtc_time *);
int (*set_time)(struct device *, struct rtc_time *);
int (*read_alarm)(struct device *, struct rtc_wkalrm *);
int (*set_alarm)(struct device *, struct rtc_wkalrm *);
int (*proc)(struct device *, struct seq_file *);
int (*set_mmss64)(struct device *, time64_t secs);
int (*set_mmss)(struct device *, unsigned long secs);
int (*read_callback)(struct device *, int data);
int (*alarm_irq_enable)(struct device *, unsigned int enabled);
};

NXP写好的RTC驱动:

rtc_device是RTC设备在内核中的具体实现,找到RTC相关的节点

1
2
3
4
5
6
7
8
9
10
snvs: snvs@020cc000 {
compatible = "fsl,sec-v4.0-mon", "syscon", "simple-mfd";
reg = <0x020cc000 0x4000>;

snvs_rtc: snvs-rtc-lp {
compatible = "fsl,sec-v4.0-mon-rtc-lp";
regmap = <&snvs>;
offset = <0x34>;
interrupts = <GIC_SPI 19 IRQ_TYPE_LEVEL_HIGH>, <GIC_SPI 20 IRQ_TYPE_LEVEL_HIGH>;
};

根据compatible找到驱动文件:drivers/rtc/rtc-snvs.c,驱动文件里面就是初始化rtc_device并注册。

1
2
3
4
static const struct of_device_id snvs_dt_ids[] = {
{ .compatible = "fsl,sec-v4.0-mon-rtc-lp", },
{ /* sentinel */ }
};

驱动和设备匹配之后,snvs_rtc_probe函数执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
static int snvs_rtc_probe(struct platform_device *pdev)
{
struct snvs_rtc_data *data;
struct resource *res;
int ret;
void __iomem *mmio;

data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;

data->regmap = syscon_regmap_lookup_by_phandle(pdev->dev.of_node, "regmap");

//1.首先从设备树里面获取SNVS RTC外设寄存器
if (IS_ERR(data->regmap)) {
dev_warn(&pdev->dev, "snvs rtc: you use old dts file, please update it\n");
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);

mmio = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(mmio))
return PTR_ERR(mmio);

data->regmap = devm_regmap_init_mmio(&pdev->dev, mmio, &snvs_rtc_config);
} else {
data->offset = SNVS_LPREGISTER_OFFSET;
of_property_read_u32(pdev->dev.of_node, "offset", &data->offset);
}

if (!data->regmap) {
dev_err(&pdev->dev, "Can't find snvs syscon\n");
return -ENODEV;
}

data->irq = platform_get_irq(pdev, 0);
if (data->irq < 0)
return data->irq;

data->clk = devm_clk_get(&pdev->dev, "snvs-rtc");
if (IS_ERR(data->clk)) {
data->clk = NULL;
} else {
ret = clk_prepare_enable(data->clk);
if (ret) {
dev_err(&pdev->dev,
"Could not prepare or enable the snvs clock\n");
return ret;
}
}

platform_set_drvdata(pdev, data);

/* Initialize glitch detect */

//地址就是 0x020cc000+0x34 = 0x020cc034
regmap_write(data->regmap, data->offset + SNVS_LPPGDR, SNVS_LPPGDR_INIT);


/* Clear interrupt status */
regmap_write(data->regmap, data->offset + SNVS_LPSR, 0xffffffff);

/* Enable RTC */
snvs_rtc_enable(data, true);

device_init_wakeup(&pdev->dev, true);

ret = devm_request_irq(&pdev->dev, data->irq, snvs_rtc_irq_handler,
IRQF_SHARED, "rtc alarm", &pdev->dev);
if (ret) {
dev_err(&pdev->dev, "failed to request irq %d: %d\n",
data->irq, ret);
goto error_rtc_device_register;
}

data->rtc = devm_rtc_device_register(&pdev->dev, pdev->name,
&snvs_rtc_ops, THIS_MODULE);
if (IS_ERR(data->rtc)) {
ret = PTR_ERR(data->rtc);
dev_err(&pdev->dev, "failed to register rtc: %d\n", ret);
goto error_rtc_device_register;
}

return 0;

error_rtc_device_register:
if (data->clk)
clk_disable_unprepare(data->clk);

return ret;
}

NXP为6ULL的RTC外设创建了一个结构体snvs_rtc_data,包含了rtc_device结构体。

1
2
3
4
5
6
7
struct snvs_rtc_data {
struct rtc_device *rtc;
struct regmap *regmap;
int offset;
int irq;
struct clk *clk;
};

snvs_rtc_probe函数中,首先从设备树里面获取SNVS RTC外设寄存器,初始化RTC,申请中断处理函数是snvs_rtc_irq_handler,最后通过devm_rtc_device_register 函数向内核注册rtc_device,重点是注册的时候设置了**snvs_rtc_ops**。

1
2
3
4
5
6
7
static const struct rtc_class_ops snvs_rtc_ops = {
.read_time = snvs_rtc_read_time,
.set_time = snvs_rtc_set_time,
.read_alarm = snvs_rtc_read_alarm,
.set_alarm = snvs_rtc_set_alarm,
.alarm_irq_enable = snvs_rtc_alarm_irq_enable,
};

rtc_class_ops 准备好以后需要将其注册到 Linux 内核中,这里我们可以使用rtc_device_register函数完成注册工作。此函数会申请一个rtc_device并且初始化这个rtc_device,最后向调用者返回这个 rtc_device,此函数原型如下:

1
2
3
4
struct rtc_device *rtc_device_register(const char *name,
struct device *dev,
const struct rtc_class_ops *ops,
struct module *owner)
  • name:设备名字。

  • dev: 设备。

  • ops: RTC 底层驱动函数集。

  • owner:驱动模块拥有者。

  • 返回值: 注册成功的话就返回。

  • rtc_device,错误的话会返回一个负值。

当卸载 RTC 驱动的时候需要调用 rtc_device_unregister 函数来注销注册的 rtc_device,函数原型如下:

1
void rtc_device_unregister(struct rtc_device *rtc);
  • rtc:要删除的 rtc_device。

  • 返回值: 无。

当应用通过ioctl读取RTC时间的时候,RTC核心层的rtc_dev_ioctl会执行,通过cmd来执行具体操作,比如rtc_read_alarm就是读取闹钟,此时函数执行,rtc_read_alarm函数就会找到具体的rtc_device,运行具体的ops里面的read_alarm。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
static long rtc_dev_ioctl(struct file *file,
unsigned int cmd, unsigned long arg)
{
int err = 0;
struct rtc_device *rtc = file->private_data;
const struct rtc_class_ops *ops = rtc->ops;
struct rtc_time tm;
struct rtc_wkalrm alarm;
void __user *uarg = (void __user *) arg;

err = mutex_lock_interruptible(&rtc->ops_lock);
if (err)
return err;
switch (cmd) {
case RTC_EPOCH_SET:
case RTC_SET_TIME:
if (!capable(CAP_SYS_TIME))
err = -EACCES;
break;

case RTC_IRQP_SET:
if (arg > rtc->max_user_freq && !capable(CAP_SYS_RESOURCE))
err = -EACCES;
break;

case RTC_PIE_ON:
if (rtc->irq_freq > rtc->max_user_freq &&
!capable(CAP_SYS_RESOURCE))
err = -EACCES;
break;
}

if (err)
goto done;

switch (cmd) {
case RTC_ALM_READ:
mutex_unlock(&rtc->ops_lock);

err = rtc_read_alarm(rtc, &alarm);
if (err < 0)
return err;

if (copy_to_user(uarg, &alarm.time, sizeof(tm)))
err = -EFAULT;
return err;

case RTC_ALM_SET:
mutex_unlock(&rtc->ops_lock);

if (copy_from_user(&alarm.time, uarg, sizeof(tm)))
return -EFAULT;

alarm.enabled = 0;
alarm.pending = 0;
alarm.time.tm_wday = -1;
alarm.time.tm_yday = -1;
alarm.time.tm_isdst = -1;
{
time64_t now, then;

err = rtc_read_time(rtc, &tm);
if (err < 0)
return err;
now = rtc_tm_to_time64(&tm);

alarm.time.tm_mday = tm.tm_mday;
alarm.time.tm_mon = tm.tm_mon;
alarm.time.tm_year = tm.tm_year;
err = rtc_valid_tm(&alarm.time);
if (err < 0)
return err;
then = rtc_tm_to_time64(&alarm.time);

/* alarm may need to wrap into tomorrow */
if (then < now) {
rtc_time64_to_tm(now + 24 * 60 * 60, &tm);
alarm.time.tm_mday = tm.tm_mday;
alarm.time.tm_mon = tm.tm_mon;
alarm.time.tm_year = tm.tm_year;
}
}

return rtc_set_alarm(rtc, &alarm);

case RTC_RD_TIME:
mutex_unlock(&rtc->ops_lock);

err = rtc_read_time(rtc, &tm);
if (err < 0)
return err;

if (copy_to_user(uarg, &tm, sizeof(tm)))
err = -EFAULT;
return err;

case RTC_SET_TIME:
mutex_unlock(&rtc->ops_lock);

if (copy_from_user(&tm, uarg, sizeof(tm)))
return -EFAULT;

return rtc_set_time(rtc, &tm);

case RTC_PIE_ON:
err = rtc_irq_set_state(rtc, NULL, 1);
break;

case RTC_PIE_OFF:
err = rtc_irq_set_state(rtc, NULL, 0);
break;

case RTC_AIE_ON:
mutex_unlock(&rtc->ops_lock);
return rtc_alarm_irq_enable(rtc, 1);

case RTC_AIE_OFF:
mutex_unlock(&rtc->ops_lock);
return rtc_alarm_irq_enable(rtc, 0);

case RTC_UIE_ON:
mutex_unlock(&rtc->ops_lock);
return rtc_update_irq_enable(rtc, 1);

case RTC_UIE_OFF:
mutex_unlock(&rtc->ops_lock);
return rtc_update_irq_enable(rtc, 0);

case RTC_IRQP_SET:
err = rtc_irq_set_freq(rtc, NULL, arg);
break;

case RTC_IRQP_READ:
err = put_user(rtc->irq_freq, (unsigned long __user *)uarg);
break;

case RTC_WKALM_SET:
mutex_unlock(&rtc->ops_lock);
if (copy_from_user(&alarm, uarg, sizeof(alarm)))
return -EFAULT;

return rtc_set_alarm(rtc, &alarm);

case RTC_WKALM_RD:
mutex_unlock(&rtc->ops_lock);
err = rtc_read_alarm(rtc, &alarm);
if (err < 0)
return err;

if (copy_to_user(uarg, &alarm, sizeof(alarm)))
err = -EFAULT;
return err;
done:
mutex_unlock(&rtc->ops_lock);
return err;
}

image-20241212200417660

2.实验

I.MX6U 的 RTC 驱动我们不用自己编写,因为 NXP 已经写好了。

2.1时间 RTC 查看

Linux 内核启动的时候可以看到系统时钟设置信息 image-20241212201058682

如果要查看时间的话输入“date”命令即可

image-20241212201255708

1

2.2设置时间

设置当前时间为 2024年 12月 12 日 20:14:00,因此输入如下命令:

image-20241212201447779

“ date -s”命令仅仅是将当前系统时间设置了,此时间还没有写入到I.MX6U 内部 RTC 里面或其他的 RTC 芯片里面,因此系统重启以后时间又会丢失。我们需要将当前的时间写入到 RTC 里面,这里要用到 hwclock 命令,输入如下命令将系统时间写入到 RTC里面:

1
hwclock -w

前提是板子上面的纽扣电池有电。

3.注意:

IMX6U的纽扣电池是给整个SNVS供电,因此耗电量大,可能几个月就没电了。

stm32的纽扣电池就单独给rtc供电,所以时间可以持续很久。

评论