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节点。 整个软件框架可以简单抽象为下图:

../../../_images/sw_system4.png

图 4.18 Linux RTC子系统架构图

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的处理:

  1. WRI

    负责记录 硬件可监测 到的Reboot原因,如过温保护、看门狗复位、外部输入复位等;

  2. SYS_BAK

    负责记录 软件可监测 到的Reboot原因,如Suspend、Panic、进入烧写模式、正常重启等。

关于Reboot原因,梳理分类如下:

../../../_images/reboot_reason.png

图 4.19 各种情况的Reboot reason梳理

小技巧

其中“外部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;
}