4.4.5. 设计说明
4.4.5.1. 源码说明
源代码位于:
RTC V1.0: drivers/rtc/zx-rtc.c
RTC V0.1: drivers/rtc/zx-rtc-v0.1.c
4.4.5.2. 模块架构
Linux提供了一个RTC子系统(简称RTC Core),使得在用户空间可以通过/dev/watchdogX来访问Watchdog控制器。为了更方便查看硬件状态和参数设置,本驱动另外扩展了几个sysfs节点。 整个软件框架可以简单抽象为下图:
RTC V1.0控制器可以适配到Linux标准的时间、闹钟接口,其他非标准的特性有:
- Alarm的中断输出:
是否有输出完全由板级电路的设计决定,软件上只需要使能中断信号即可。在DTS中提供了一个bool类型的参数方便用户配置“alarm-io-output”。
- 校准参数:
控制器支持±975ppm的校准范围,用户需要配置DTS中的参数
clock-rate
详见 RTC 自定义参数。
- 精准驱动能力
为了节省功耗,可以降低32K时钟的驱动能力到刚好够用,扫描方法见 驱动能力扫描
- 8bit寄存器的读写
在驱动设计时将8bit数据的拆解、打包进行封装,可以尽量减少对代码的干扰,封装如下:
#define RTC_WRITEL(val, reg) \
do { \
writeb((val) & 0xFF, (reg)); \
writeb(((val) >> 8) & 0xFF, (reg) + 0x4); \
writeb(((val) >> 16) & 0xFF, (reg) + 0x8); \
writeb(((val) >> 24) & 0xFF, (reg) + 0xC); \
} while (0)
#define RTC_READL(reg) (readb(reg) | (readb((reg) + 0x4) << 8) \
| (readb((reg) + 0x8) << 16) \
| (readb((reg) + 0xC) << 24))
4.4.5.3. 关键流程设计
4.4.5.3.1. 初始化流程
RTC驱动的初始化过程见aic_rtc_probe()函数,除了普通platform设备的处理过程(申请regs资源、clk、reset)外,需要调用RTC子系统的接口rtc_register_device()来注册RTC设备。
#define rtc_register_device(device) __rtc_register_device(THIS_MODULE, device)
其中参数struct rtc_device device中关键信息有:最大值、ops等,aic_rtc_ops定义如下:
static const struct rtc_class_ops aic_rtc_ops = {
.read_time = aic_rtc_read_time,
.set_time = aic_rtc_set_time,
.read_alarm = aic_rtc_read_alarm,
.set_alarm = aic_rtc_set_alarm,
.alarm_irq_enable = aic_rtc_alarm_irq_enable,
};
4.4.5.3.2. 校准算法设计
校准的算法原理是,将输入的 32KHz 晶振时钟校准到理想的 32KHz,公式如下:
(100 * 1024 * 1024 + 100 * calibrate) / (clock-rate / 32) = 1024
=> calibrate = (clock-rate * 32 - 100 * 1024 * 1024) / 100;
其中:
clock-rate: 是用户实测 32K晶振的频率值 * 100,需要配置在DTS中,详见 RTC 自定义参数
calibrate: 最终要填入RTC控制器的校准值
备注
校准值calibrate分正负,正 - 表示32K晶振实际偏快了,负 - 表示32K晶振偏慢了。
4.4.5.3.3. 系统状态的备份功能
RTC控制器提供了 128bit 的备份寄存器 SYS_BAK,用于掉电时一些重要状态或者参数的保存。RTC驱动将这几个寄存器封装为对外接口( EXPORT_SYMBOL_GPL()
的形式),Linux中其他驱动都可以调用。
4.4.5.3.4. Reboot Reason 的设计
将上节中 系统备份寄存器 保存不同情况的Reboot reason,可用于分析终端运行稳定性问题、进入快速启动模式等场景。
SYS_BAK 寄存器需要和 WRI 模块一起配合来完成Boot reason的处理:
- WRI
负责记录 硬件可监测 到的Reboot原因,如过温保护、看门狗复位、外部输入复位等;
- SYS_BAK
负责记录 软件可监测 到的Reboot原因,如Suspend、Panic、进入烧写模式、正常重启等。
关于Reboot原因,梳理分类如下:
小技巧
其中“外部IO复位”指常用的Reset按键。
所以,定义 SYS_BAK0寄存器(4~7bit) 的值如下:(详见include/linux/reboot-reason.h)
enum aic_reboot_reason {
REBOOT_REASON_COLD = 0,
REBOOT_REASON_CMD_REBOOT = 1,
REBOOT_REASON_CMD_SHUTDOWN = 2,
REBOOT_REASON_SUSPEND = 3,
REBOOT_REASON_UPGRADE = 4,
REBOOT_REASON_FASTBOOT = 5,
/* Some software exception reason */
REBOOT_REASON_SW_LOCKUP = 8,
REBOOT_REASON_HW_LOCKUP = 9,
REBOOT_REASON_PANIC = 10,
REBOOT_REASON_RAMDUMP = 11,
};
针对不同场景,SYS_BAK0寄存器中的Reboot reason 和 WRI中的RST_FLAG值对应如下:
场景 |
触发行为 |
WRI状态 |
SYS_BAK状态值 |
---|---|---|---|
正常 重启 |
按Reset按键 |
EXT_RST |
COLD |
shell中执行reboot命令 |
WDOG_RST |
CMD_REBOOT |
|
shell执行aicupg命令进入烧写 |
WDOG_RST |
UPGRADE |
|
正常 关机 |
长按PowerOn按键 |
SYS_POR |
COLD |
shell中执行poweroff命令 |
SYS_POR |
CMD_SHUTDOWN |
|
进入深度休眠状态 |
SYS_POR |
SUSPEND |
|
异常 重启 |
过温保护 |
OTP |
COLD |
通过Jtag执行reset命令 |
DM_RST |
COLD |
|
RTC模块断电 |
RTC_POR |
COLD |
|
内核中发生SW Lock |
WDOG_RST |
SW_LOCKUP |
|
内核中发生HW Lock |
WDOG_RST |
HW_LOCKUP |
|
内核中发生Panic |
WDOG_RST |
PANIC |
|
内核中触发进入Ramdump |
WDOG_RST |
RAMDUMP |
|
电源电压不稳定 |
CMP_RST |
COLD |
备注
其中“按Reset按键”的情况,因为软件来不及设置SYS_BAK,所以是初始值0 (COLD)。
4.4.5.4. 数据结构设计
4.4.5.4.1. aic_rtc_dev
记录RTC控制器的配置信息:
struct aic_rtc_dev {
void __iomem *base;
struct rtc_device *rtc_dev;
struct attribute_group attrs;
struct clk *clk;
u32 clk_rate;
u32 clk_drv;
bool alarm_io;
bool cal_fast;
s32 cal_val;
struct completion complete;
};
4.4.5.5. 接口设计
以下接口是 Linux RTC 子系统需要的标准接口。
4.4.5.5.1. 外部接口
4.4.5.5.1.1. ioctl 接口
Linux对用户态提供了一组RTC的ioctl接口,用户态可以通过设备节点/dev/rtc0来访问:(详见include/upai/linux/rtc.h)
#define RTC_AIE_ON _IO('p', 0x01) /* Alarm int. enable on */
#define RTC_AIE_OFF _IO('p', 0x02) /* ... off */
#define RTC_UIE_ON _IO('p', 0x03) /* Update int. enable on */
#define RTC_UIE_OFF _IO('p', 0x04) /* ... off */
#define RTC_PIE_ON _IO('p', 0x05) /* Periodic int. enable on */
#define RTC_PIE_OFF _IO('p', 0x06) /* ... off */
#define RTC_WIE_ON _IO('p', 0x0f) /* Watchdog int. enable on */
#define RTC_WIE_OFF _IO('p', 0x10) /* ... off */
#define RTC_ALM_SET _IOW('p', 0x07, struct rtc_time) /* Set alarm time */
#define RTC_ALM_READ _IOR('p', 0x08, struct rtc_time) /* Read alarm time */
#define RTC_RD_TIME _IOR('p', 0x09, struct rtc_time) /* Read RTC time */
#define RTC_SET_TIME _IOW('p', 0x0a, struct rtc_time) /* Set RTC time */
#define RTC_IRQP_READ _IOR('p', 0x0b, unsigned long) /* Read IRQ rate */
#define RTC_IRQP_SET _IOW('p', 0x0c, unsigned long) /* Set IRQ rate */
#define RTC_EPOCH_READ _IOR('p', 0x0d, unsigned long) /* Read epoch */
#define RTC_EPOCH_SET _IOW('p', 0x0e, unsigned long) /* Set epoch */
#define RTC_WKALM_SET _IOW('p', 0x0f, struct rtc_wkalrm)/* Set wakeup alarm*/
#define RTC_WKALM_RD _IOR('p', 0x10, struct rtc_wkalrm)/* Get wakeup alarm*/
#define RTC_PLL_GET _IOR('p', 0x11, struct rtc_pll_info) /* Get PLL correction */
#define RTC_PLL_SET _IOW('p', 0x12, struct rtc_pll_info) /* Set PLL correction */
Demo 就是调用的这些接口完成alarm配置,以及hwclock工具也是调用上述接口。
4.4.5.5.2. RTC 相关的内部接口
4.4.5.5.2.1. aic_rtc_read_time
函数原型 |
static int aic_rtc_read_time(struct device *dev, struct rtc_time *tm) |
---|---|
功能说明 |
读取当前的RTC时间 |
参数定义 |
dev - 指向RTC设备的指针
tm - 用于存放获取到的时间信息
|
返回值 |
0,成功 |
注意事项 |
4.4.5.5.2.2. aic_rtc_set_time
函数原型 |
static int aic_rtc_set_time(struct device *dev, struct rtc_time *tm) |
---|---|
功能说明 |
设置RTC时间 |
参数定义 |
dev - 指向RTC设备的指针
tm - 需要设置的时间信息
|
返回值 |
0,成功 |
注意事项 |
更新RTC控制器的秒数,需要先暂停RTC计数,设置完秒数,再使能RTC。 |
4.4.5.5.2.3. aic_rtc_read_alarm
函数原型 |
static int aic_rtc_read_alarm(struct device *dev, struct rtc_wkalrm *alarm) |
---|---|
功能说明 |
读取当前的Alarm状态信息 |
参数定义 |
dev - 指向RTC设备的指针
alarm - 用于保存读取到的当前Alarm信息,包括下一次超时时间和超时状态
|
返回值 |
0,成功 |
注意事项 |
4.4.5.5.2.4. aic_rtc_set_alarm
函数原型 |
static int aic_rtc_set_alarm(struct device *dev, struct rtc_wkalrm *alarm) |
---|---|
功能说明 |
设置一个Alarm,并使能Alarm中断 |
参数定义 |
dev - 指向RTC设备的指针
alarm - 需要设置的Alarm信息
|
返回值 |
0,成功 |
注意事项 |
4.4.5.5.2.5. aic_rtc_alarm_irq_enable
函数原型 |
static int aic_rtc_alarm_irq_enable(struct device *dev, unsigned int enabled) |
---|---|
功能说明 |
开关Alarm中断 |
参数定义 |
dev - 指向RTC设备的指针
enabled - 使能标记
|
返回值 |
0,成功 |
注意事项 |
4.4.5.5.3. 系统状态备份相关的内部接口
4.4.5.5.3.1. aic_rtc_set_bak
函数原型 |
void aic_rtc_set_bak(u32 offset, u32 mask, u32 shift, u32 val) |
---|---|
功能说明 |
设置SYS_BAK寄存器中某几个(连续的)bit |
参数定义 |
offset - 寄存器的偏移地址,取值范围:0、4、8、12
mask - 待设置的bit掩码
shift - 待设置的bit需要左移多少位
val - 待设置的实际值
|
返回值 |
无 |
注意事项 |
设置过程:先将val左移,然后再做掩码处理 |
4.4.5.5.3.2. aic_rtc_get_bak
函数原型 |
u32 aic_rtc_get_bak(u32 offset, u32 mask, u32 shift) |
---|---|
功能说明 |
读取SYS_BAK寄存器中某几个(连续的)bit |
参数定义 |
offset - 寄存器的偏移地址,取值范围:0、4、8、12
mask - 待读取的bit掩码
shift - 待读取的bit需要右移多少位
|
返回值 |
实际读取到的寄存器值 |
注意事项 |
读取过程:先将读取到的寄存器当前值做掩码处理,然后再右移 |
4.4.5.5.3.3. aic_set_software_reboot_reason
函数原型 |
void aic_set_software_reboot_reason(enum aic_reboot_reason reason) |
---|---|
功能说明 |
设置Reboot reason到SYS_BAK寄存器 |
参数定义 |
reason - aic_reboot_reason类型的启动原因
|
返回值 |
无 |
注意事项 |
aic_reboot_reason 详见 Reboot Reason 的设计 |
4.4.5.5.3.4. aic_get_software_reboot_reason
函数原型 |
enum aic_reboot_reason aic_get_software_reboot_reason(void) |
---|---|
功能说明 |
从SYS_BAK寄存器中读取上一次系统的Reboot reason类型 |
参数定义 |
无 |
返回值 |
aic_reboot_reason类型的启动原因 |
注意事项 |
aic_reboot_reason 详见 Reboot Reason 的设计 |
4.4.5.6. Demo
本Demo是通过ioctl接口来访问设备节点/dev/rtc0:
#include "base.h"
#include <sys/time.h>
#include <linux/rtc.h>
/* Global macro and variables */
#define ALARM_MAX_DELAY (60 * 60)
#define ALARM_MIN_DELAY 1
static const char sopts[] = "d:u";
static const struct option lopts[] = {
{"delay", required_argument, NULL, 'd'},
{"usage", no_argument, NULL, 'u'},
{0, 0, 0, 0}
};
/* Functions */
void usage(char *program)
{
printf("Usage: %s will start a timer of given seconds, and wait it\n",
program);
printf("\t -d, --delay\trange: [%d, %d]\n", ALARM_MIN_DELAY,
ALARM_MAX_DELAY);
printf("\t -u, --usage \n");
printf("\n");
printf("Example: %s -d 12\n\n", program);
}
/* Open a device file to be needed. */
int device_open(char *_fname, int _flag)
{
s32 fd = -1;
fd = open(_fname, _flag);
if (fd < 0) {
ERR("Failed to open %s errno: %d[%s]\n",
_fname, errno, strerror(errno));
exit(0);
}
return fd;
}
int main(int argc, char **argv)
{
int c, ret;
int delay = 0;
int rtc_fd = -1;
time_t tmp = 0;
struct rtc_time start = {0};
struct rtc_time end = {0};
struct rtc_wkalrm alrm_set = {0};
struct rtc_wkalrm alrm_get = {0};
DBG("Compile time: %s\n", __TIME__);
while ((c = getopt_long(argc, argv, sopts, lopts, NULL)) != -1) {
switch (c) {
case 'd':
delay = str2int(optarg);
continue;
case 'u':
usage(argv[0]);
return 0;
default:
break;
}
}
if ((delay < ALARM_MIN_DELAY) || (delay > ALARM_MAX_DELAY)) {
ERR("Invalid delay: %d\n", delay);
return -1;
}
rtc_fd = open("/dev/rtc0", O_RDWR);
if (rtc_fd < 0) {
ERR("Failed to open RTC device!\n");
return -1;
}
DBG("ioctl(%#x)\n", RTC_RD_TIME);
ret = ioctl(rtc_fd, RTC_RD_TIME, &start);
if (ret < 0) {
ERR("Failed to read RTC time!\n");
goto err;
}
DBG("Current time: %04d-%02d-%02d %02d:%02d:%02d\n",
start.tm_year, start.tm_mon, start.tm_mday,
start.tm_hour, start.tm_min, start.tm_sec);
alrm_set.enabled = 1;
tmp = mktime((struct tm *)&start) + delay;
memcpy(&alrm_set.time, gmtime(&tmp), sizeof(struct rtc_time));
DBG("ioctl(%#x)\n", RTC_WKALM_SET);
ret = ioctl(rtc_fd, RTC_WKALM_SET, &alrm_set);
if (ret < 0) {
ERR("Failed to set alarm! [%d]: %s\n", errno, strerror(errno));
goto err;
}
DBG("Set a alarm to: %04d-%02d-%02d %02d:%02d:%02d\n",
alrm_set.time.tm_year, alrm_set.time.tm_mon,
alrm_set.time.tm_mday, alrm_set.time.tm_hour,
alrm_set.time.tm_min, alrm_set.time.tm_sec);
do {
memset(&alrm_get, 0, sizeof(struct rtc_wkalrm));
DBG("ioctl(%#x)\n", RTC_WKALM_RD);
ret = ioctl(rtc_fd, RTC_WKALM_RD, &alrm_get);
if (ret < 0) {
ERR("Failed to read alarm!\n");
goto err;
}
if (alrm_get.pending)
break;
printf("Waiting ...\n");
usleep(200000); // 200ms
} while (1);
DBG("ioctl(%#x)\n", RTC_RD_TIME);
ret = ioctl(rtc_fd, RTC_RD_TIME, &end);
if (ret < 0) {
ERR("Failed to read RTC time!\n");
goto err;
}
DBG("Current time: %04d-%02d-%02d %02d:%02d:%02d\n",
end.tm_year, end.tm_mon, end.tm_mday,
end.tm_hour, end.tm_min, end.tm_sec);
tmp = mktime((struct tm *)&end) - mktime((struct tm *)&start);
DBG("Start a timer of %d, actualy is %ld ...\n", delay, tmp);
if (ret != delay) {
ERR("The timer is not accurate!\n");
ret = -1;
}
else {
DBG("The timer is good!\n");
ret = 0;
}
err:
if (rtc_fd > 0)
close(rtc_fd);
return ret;
}