Edit online

设计说明

Edit online

源码说明

源代码位于:

  • RTC V1.0: drivers/rtc/artinchip-rtc.c

  • RTC V0.1: drivers/rtc/artinchip-rtc-v0.1.c

Edit online

模块架构

Linux 提供了一个 RTC 子系统,简称 RTC Core,使得在用户空间可以通过 /dev/watchdogX 来访问 RTC 控制器。为了更方便查看硬件状态和参数设置,本驱动另外扩展了几个 sysfs 节点。 整个软件框架如 Linux RTC 子系统架构 所示:


rtc_sw_system

1. Linux RTC 子系统架构

RTC V1.0 控制器可以适配到通用的 RTC 时间、闹钟接口,其它非标准的特性有:

  • Alarm 的中断输出:

    Alarm 的中断输出是否可用完全由板级电路的设计决定,软件上只需要使能中断信号即可。

    在 DTS 中提供了一个 boolean 类型的参数方便用户配置 alarm-io-output

  • 时钟校准:

    RTC 控制器支持 ±975 ppm 的校准范围。

    用户需要配置 DTS 中的参数 clock-rate,详见 配置 DTS 参数

  • 精准驱动能力

    为了节省功耗,可以降低 32K 时钟的驱动能力到刚好够用,扫描方法见 驱动能力扫描

  • 8-bit 寄存器的读写
    为了减少对代码的干扰,可以将 8-bit 数据的拆解、打包进行封装,示例如下:
    #define RTC_WRITEL(val, reg) \
        do { \
                RTC_WRITE_ENABLE; \
                writeb((val) & 0xFF, RTC_BASE + (reg)); \
                writeb(((val) >> 8) & 0xFF, RTC_BASE + (reg) + 0x4); \
                writeb(((val) >> 16) & 0xFF, RTC_BASE + (reg) + 0x8); \
                writeb(((val) >> 24) & 0xFF, RTC_BASE + (reg) + 0xC); \
                RTC_WRITE_DISABLE; \
        } while (0)
    
    #define RTC_READL(reg)  (readb(RTC_BASE + reg) \
                        | (readb((RTC_BASE + reg) + 0x4) << 8) \
                        | (readb((RTC_BASE + reg) + 0x8) << 16) \
                        | (readb((RTC_BASE + reg) + 0xC) << 24))
Edit online

关键流程设计

Edit online

初始化流程

RTC 驱动的初始化过程见 aic_rtc_probe() 函数,除了普通 platform 设备的处理过程(申请 regs 资源、clk、reset)外,需要调用 RTC 子系统的接口 rtc_register_device() 来注册 RTC 设备。
#define rtc_register_device(device)   __rtc_register_device(THIS_MODULE, 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,
};
Edit online

校准算法设计

校准的算法原理是,将输入的 32 KHz 晶振时钟校准到理想的 32 KHz,公式如下:
(100 * 1024 * 1024 + 100 * calibrate) / (clock-rate / 32) = 1024
=> calibrate = (clock-rate * 32 - 100 * 1024 * 1024) / 100;

其中:

  • clock-rate: 是用户实测 32K 晶振的频率值乘以 100。关于配置说明,详见 配置 DTS 参数

  • calibrate: 最终要填入 RTC 控制器的校准值

    注:
    校准值 calibrate 分正负:
    • 正:表示 32K 晶振实际偏快。
    • 负:表示 32K 晶振偏慢。
Edit online

系统状态的备份功能

RTC 控制器提供了 128-bit 备份寄存器 SYS_BAK,用于掉电时一些重要状态或者参数的保存。RTC 驱动将这几个寄存器封装为对外接口( EXPORT_SYMBOL_GPL() 的形式),Linux 中其它驱动都可以调用。

Edit online

Reboot Reason 的设计

系统状态的备份功能系统备份寄存器 保存不同情况的 Reboot reason,可用于分析终端运行稳定性问题、进入快速启动模式等场景。

SYS_BAK 寄存器需要和 WRI 模块一起配合来完成 reason 的处理:

  1. WRI

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

  2. SYS_BAK

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

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


reboot_reason

2. 各种情况的 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,

    /* Some hardware exception reason */
    REBOOT_REASON_RTC = 17,
    REBOOT_REASON_EXTEND = 18,
    REBOOT_REASON_DM = 19,
    REBOOT_REASON_OTP = 20,
    REBOOT_REASON_UNDER_VOL = 21,
};

针对不同场景,SYS_BAK0 寄存器中的 reason 和 WRI 中的 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 按键的情况,因为软件来不及设置 BAK,所以是初始值 0 (COLD)。

Edit online

数据结构设计

RTC 数据类型及其结构描述如下:
  • struct 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;
    };
Edit online

接口设计

以下接口是 Linux RTC 子系统需要的标准接口。

外部接口

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 工具也是调用上述接口。

RTC 相关的内部接口

1. aic_rtc_read_time

函数原型

static int aic_rtc_read_time(struct device *dev, struct rtc_time *tm)

功能说明

读取当前的 RTC 时间

参数定义

  • dev - 指向 RTC 设备的指针
  • tm - 用于存放获取到的时间信息

返回值

0,成功

注意事项

-

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。

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. aic_rtc_set_alarm

函数原型

static int aic_rtc_set_alarm(struct device *dev, struct rtc_wkalrm *alarm)

功能说明

设置一个 Alarm,并使能 Alarm 中断

参数定义

  • dev - 指向 RTC 设备的指针
  • alarm - 需要设置的 Alarm 信息

返回值

0,成功

注意事项

-

5. aic_rtc_alarm_irq_enable

函数原型

static int aic_rtc_alarm_irq_enable(struct device *dev, unsigned int enabled)

功能说明

开关 Alarm 中断

参数定义

  • dev - 指向 RTC 设备的指针
  • enabled - 使能标记

返回值

0,成功

注意事项

-

系统状态备份相关的内部接口

6. aic_rtc_set_bak

函数原型

void aic_rtc_set_bak(u32 offset, u32 mask, u32 shift, u32 val)

功能说明

设置 BAK 寄存器中某几个 (连续的)bit

参数定义

  • offset - 寄存器的偏移地址,取值范围:0、4、8、12
  • mask - 待设置的 bit 掩码
  • shift - 待设置的 bit 需要左移多少位
  • val - 待设置的实际值

返回值

注意事项

设置过程:先将 val 左移,然后再做掩码处理

7. aic_rtc_get_bak

函数原型

u32 aic_rtc_get_bak(u32 offset, u32 mask, u32 shift)

功能说明

读取 BAK 寄存器中某几个 (连续的)bit

参数定义

  • offset - 寄存器的偏移地址,取值范围:0、4、8、12
  • mask - 待读取的 bit 掩码
  • shift - 待读取的 bit 需要右移多少位

返回值

实际读取到的寄存器值

注意事项

读取过程:先将读取到的寄存器当前值做掩码处理,然后再右移

8. aic_set_software_reboot_reason

函数原型

void aic_set_software_reboot_reason(enum aic_reboot_reason reason)

功能说明

设置 reason 到 SYS_BAK 寄存器

参数定义

reason - aic_reboot_reason 类型的启动原因

返回值

注意事项

aic_reboot_reason 详见 Reboot Reason 的设计

9. aic_get_software_reboot_reason

函数原型

enum aic_reboot_reason aic_get_software_reboot_reason(void)

功能说明

从 BAK 寄存器中读取上一次系统的 Reboot reason 类型

参数定义

返回值

aic_reboot_reason 类型的启动原因

注意事项

aic_reboot_reason 详见 Reboot Reason 的设计

Edit online

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;
}