/* Copyright (c) 2013, The Linux Foundation. All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 and * only version 2 as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "wallclk.h" #define WALLCLK_MODULE_NAME "wallclk" #define WALLCLK_MODULE_NAME_LEN 10 #define FLAG_WALLCLK_INITED 0x1 #define FLAG_WALLCLK_SFN_REF_SET 0x2 #define FLAG_WALLCLK_ENABLED 0x4 #define WALLCLK_TIMER_INTERVAL_NS 100000 #define WALLCLK_SHARED_MEM_SIZE 1024 #define ALIGN_64(addr) (((addr) + 7) & ~0x7) #define GPS_EPOCH_DIFF 315964800 struct wallclk_cnt { u32 pulse; u32 clk; }; struct wallclk_reg { u32 ctrl; u32 pulse_cnt; u32 snapshot_clock_cnt; u32 clock_cnt; u32 __unused__[5]; u32 base_time0; u32 base_time1; }; struct wallclk_sm { struct wallclk_reg reg; u32 sfn_ref; }; struct wallclk_cfg { u32 ppns; u32 clk_rate; u32 clk_rate_v; /* clk_rate = clk_rate_v x clk_rate_p */ u32 clk_rate_p; /* power of 10 */ u32 ns_per_clk_rate_v; }; struct wallclk { struct wallclk_sm *shm; struct wallclk_cfg cfg; struct timespec tv; struct hrtimer timer; ktime_t interval; spinlock_t lock; u32 flags; char name[WALLCLK_MODULE_NAME_LEN]; }; static struct wallclk wall_clk; static inline int is_valid_register(u32 offset) { int rc = 0; switch (offset) { case CTRL_REG_OFFSET: case PULSE_CNT_REG_OFFSET: case CLK_CNT_SNAPSHOT_REG_OFFSET: case CLK_CNT_REG_OFFSET: case CLK_BASE_TIME0_OFFSET: case CLK_BASE_TIME1_OFFSET: rc = 1; break; default: break; } return rc; } static inline void wallclk_ctrl_reg_set(struct wallclk *wclk, u32 v) { struct wallclk_reg *reg = &wclk->shm->reg; if (v & CTRL_ENABLE_MASK) { if (!(wclk->flags & FLAG_WALLCLK_ENABLED)) { getnstimeofday(&wclk->tv); __raw_writel(0, ®->snapshot_clock_cnt); __raw_writel(0, ®->clock_cnt); __raw_writel(0, ®->pulse_cnt); hrtimer_start(&wclk->timer, wclk->interval, HRTIMER_MODE_REL); wclk->flags |= FLAG_WALLCLK_ENABLED; } } else { if (wclk->flags & FLAG_WALLCLK_ENABLED) { hrtimer_cancel(&wclk->timer); wclk->flags &= ~FLAG_WALLCLK_ENABLED; } } __raw_writel(v, ®->ctrl); } static inline void wallclk_cfg_init(struct wallclk_cfg *cfg, u32 ppns, u32 clk_rate) { cfg->ppns = ppns; cfg->clk_rate = clk_rate; cfg->clk_rate_v = clk_rate; cfg->clk_rate_p = 1; cfg->ns_per_clk_rate_v = 1000000000; while (!(cfg->clk_rate_v % 10)) { cfg->clk_rate_v /= 10; cfg->clk_rate_p *= 10; cfg->ns_per_clk_rate_v /= 10; } } static inline struct timespec timestamp_convert(const struct timespec *tv) { struct timespec rc; rc.tv_sec = tv->tv_sec - GPS_EPOCH_DIFF; rc.tv_nsec = tv->tv_nsec; return rc; } static inline void timespec_delta_to_wclk_cnt(const struct timespec *tv, const struct wallclk_cfg *cfg, struct wallclk_cnt *wclk_cnt) { long ns; wclk_cnt->pulse = tv->tv_sec / cfg->ppns; wclk_cnt->clk = (tv->tv_sec % cfg->ppns) * cfg->clk_rate; ns = tv->tv_nsec; while (ns >= cfg->ns_per_clk_rate_v) { ns -= cfg->ns_per_clk_rate_v; wclk_cnt->clk += cfg->clk_rate_v; } wclk_cnt->clk += (ns * cfg->clk_rate_v)/cfg->ns_per_clk_rate_v; } static inline u32 wallclk_cnt_to_sfn(const struct wallclk_cnt *cnt, const struct wallclk_cfg *cfg) { u32 sfn; u32 delta, p; sfn = SFN_PER_SECOND * cnt->pulse * cfg->ppns; if (cfg->clk_rate_p > 100) { p = cfg->clk_rate_p/100; delta = cnt->clk/(cfg->clk_rate_v * p); } else { p = 100/cfg->clk_rate_p; delta = (cnt->clk * p)/cfg->clk_rate_v; } sfn += delta; return sfn; } static void update_wallclk(struct wallclk *wclk) { struct timespec tv; struct timespec delta_tv; struct wallclk_cnt cnt; struct wallclk_reg *reg = &wclk->shm->reg; spin_lock(&wclk->lock); getnstimeofday(&tv); delta_tv = timespec_sub(tv, wclk->tv); timespec_delta_to_wclk_cnt(&delta_tv, &wclk->cfg, &cnt); __raw_writel(cnt.pulse, ®->pulse_cnt); __raw_writel(cnt.clk, ®->clock_cnt); __raw_writel(cnt.clk, ®->snapshot_clock_cnt); spin_unlock(&wclk->lock); } static int set_sfn(struct wallclk *wclk, u16 sfn) { int rc = 0; struct wallclk_reg *reg = &wclk->shm->reg; u32 v; struct timespec ts; if (sfn > MAX_SFN) { rc = -EINVAL; goto out; } if (!(wclk->flags & FLAG_WALLCLK_INITED)) { rc = -EIO; goto out; } spin_lock_bh(&wclk->lock); v = __raw_readl(®->ctrl); wallclk_ctrl_reg_set(wclk, v & ~CTRL_ENABLE_MASK); getnstimeofday(&wclk->tv); ts = timestamp_convert(&wclk->tv); __raw_writel(ts.tv_sec, ®->base_time0); __raw_writel(ts.tv_nsec, ®->base_time1); wclk->shm->sfn_ref = sfn; wclk->flags |= FLAG_WALLCLK_SFN_REF_SET; __raw_writel(0, ®->pulse_cnt); __raw_writel(0, ®->clock_cnt); __raw_writel(0, ®->snapshot_clock_cnt); hrtimer_start(&wclk->timer, wclk->interval, HRTIMER_MODE_REL); wclk->flags |= FLAG_WALLCLK_ENABLED; __raw_writel(v | CTRL_ENABLE_MASK, ®->ctrl); spin_unlock_bh(&wclk->lock); out: return rc; } static int get_sfn(struct wallclk *wclk) { struct wallclk_cnt cnt; int rc = 0; u32 sfn; if (!(wclk->flags & FLAG_WALLCLK_INITED)) { rc = -EIO; goto out; } spin_lock_bh(&wclk->lock); if (!(wclk->flags & FLAG_WALLCLK_ENABLED) || !(wclk->flags & FLAG_WALLCLK_SFN_REF_SET)) { rc = -EIO; goto unlock; } cnt.pulse = __raw_readl(&(wclk->shm->reg.pulse_cnt)); cnt.clk = __raw_readl(&(wclk->shm->reg.clock_cnt)); sfn = wallclk_cnt_to_sfn(&cnt, &wclk->cfg); sfn += wclk->shm->sfn_ref; rc = sfn & MAX_SFN; unlock: spin_unlock_bh(&wclk->lock); out: return rc; } enum hrtimer_restart wallclk_timer_cb(struct hrtimer *timer) { update_wallclk(&wall_clk); hrtimer_forward_now(timer, wall_clk.interval); return HRTIMER_RESTART; } int wallclk_set_sfn(u16 sfn) { return set_sfn(&wall_clk, sfn); } EXPORT_SYMBOL_GPL(wallclk_set_sfn); int wallclk_get_sfn(void) { return get_sfn(&wall_clk); } EXPORT_SYMBOL_GPL(wallclk_get_sfn); int wallclk_set_sfn_ref(u16 sfn) { int rc = 0; if (sfn > MAX_SFN) { rc = -EINVAL; goto out; } if (!(wall_clk.flags & FLAG_WALLCLK_INITED)) { rc = -EIO; goto out; } spin_lock_bh(&wall_clk.lock); wall_clk.shm->sfn_ref = sfn; wall_clk.flags |= FLAG_WALLCLK_SFN_REF_SET; spin_unlock_bh(&wall_clk.lock); out: return rc; } EXPORT_SYMBOL_GPL(wallclk_set_sfn_ref); int wallclk_get_sfn_ref(void) { int rc = 0; if (!(wall_clk.flags & FLAG_WALLCLK_INITED)) { rc = -EIO; goto out; } spin_lock_bh(&wall_clk.lock); if (!(wall_clk.flags & FLAG_WALLCLK_SFN_REF_SET)) { rc = -EAGAIN; goto unlock; } rc = wall_clk.shm->sfn_ref; unlock: spin_unlock_bh(&wall_clk.lock); out: return rc; } EXPORT_SYMBOL_GPL(wallclk_get_sfn_ref); int wallclk_reg_read(u32 offset, u32 *p) { int rc = 0; if (!(wall_clk.flags & FLAG_WALLCLK_INITED)) { rc = -EIO; goto out; } if (!is_valid_register(offset)) { rc = -EINVAL; goto out; } spin_lock_bh(&wall_clk.lock); *p = __raw_readl((char *)&wall_clk.shm->reg + offset); spin_unlock_bh(&wall_clk.lock); out: return rc; } EXPORT_SYMBOL_GPL(wallclk_reg_read); int wallclk_reg_write(u32 offset, u32 val) { int rc = 0; char *p; if (!(wall_clk.flags & FLAG_WALLCLK_INITED)) { rc = -EIO; goto out; } p = (char *)&wall_clk.shm->reg; spin_lock_bh(&wall_clk.lock); switch (offset) { case CTRL_REG_OFFSET: wallclk_ctrl_reg_set(&wall_clk, val); break; case PULSE_CNT_REG_OFFSET: case CLK_BASE_TIME0_OFFSET: case CLK_BASE_TIME1_OFFSET: __raw_writel(val, p + offset); break; case CLK_CNT_REG_OFFSET: __raw_writel(val, p + CLK_CNT_REG_OFFSET); __raw_writel(val, p + CLK_CNT_SNAPSHOT_REG_OFFSET); break; case CLK_CNT_SNAPSHOT_REG_OFFSET: rc = -EIO; break; default: rc = -EINVAL; break; } spin_unlock_bh(&wall_clk.lock); out: return rc; } EXPORT_SYMBOL_GPL(wallclk_reg_write); static int __init wallclk_init(void) { int rc = 0; u32 addr; memset(&wall_clk, 0, sizeof(wall_clk)); addr = (u32)MSM_SHARED_RAM_BASE + MSM_SHARED_RAM_SIZE - WALLCLK_SHARED_MEM_SIZE; wall_clk.shm = (struct wallclk_sm *)ALIGN_64(addr); __raw_writel(0, &(wall_clk.shm->reg.ctrl)); __raw_writel(0, &(wall_clk.shm->reg.pulse_cnt)); __raw_writel(0, &(wall_clk.shm->reg.snapshot_clock_cnt)); __raw_writel(0, &(wall_clk.shm->reg.clock_cnt)); __raw_writel(0, &(wall_clk.shm->reg.clock_cnt)); __raw_writel(0, &(wall_clk.shm->reg.base_time0)); __raw_writel(0, &(wall_clk.shm->reg.base_time1)); wall_clk.shm->sfn_ref = 0; wallclk_cfg_init(&wall_clk.cfg, PPNS_PULSE, CLK_RATE); strlcpy(wall_clk.name, WALLCLK_MODULE_NAME, WALLCLK_MODULE_NAME_LEN); hrtimer_init(&wall_clk.timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL); wall_clk.timer.function = wallclk_timer_cb; wall_clk.interval = ns_to_ktime(WALLCLK_TIMER_INTERVAL_NS); spin_lock_init(&wall_clk.lock); wall_clk.flags |= FLAG_WALLCLK_INITED; printk(KERN_INFO "%s: clk_rate=%u ppns=%u clk_reg_addr=0x%x\n", wall_clk.name, wall_clk.cfg.clk_rate, wall_clk.cfg.ppns, (int)(&wall_clk.shm->reg)); return rc; } static void __exit wallclk_exit(void) { if (wall_clk.flags & FLAG_WALLCLK_INITED) { spin_lock_bh(&wall_clk.lock); wallclk_ctrl_reg_set(&wall_clk, 0); wall_clk.flags = 0; spin_unlock_bh(&wall_clk.lock); } } module_init(wallclk_init); module_exit(wallclk_exit); MODULE_DESCRIPTION("Wall clock"); MODULE_LICENSE("GPL v2");