[ARM] twd: Allow twd rescaling to match cpu frequency
authorColin Cross <ccross@google.com>
Fri, 20 Aug 2010 05:07:43 +0000 (22:07 -0700)
committerColin Cross <ccross@android.com>
Wed, 6 Oct 2010 23:27:39 +0000 (16:27 -0700)
The clock to the ARM TWD local timer scales with the cpu
frequency.  To allow the cpu frequency to change while
maintaining a constant TWD frequency, pick a lower target
frequency for the TWD and use the prescaler to divide down
to the closest lower frequency.

This patch provides a new initialization function that takes
a target TWD frequency and the relation between the cpu
clock and the TWD clock, required to be an integer divider
>= 2 by the ARM spec.  It also provides a function to be
called from cpufreq drivers to set the prescaler whenever
the cpu frequency changes.

Also fixes a typo in the printk of the calibrated frequency.

Change-Id: I3fa8ef718ff5518170f1b2bab29efe960741853e
Signed-off-by: Colin Cross <ccross@google.com>
arch/arm/include/asm/smp_twd.h
arch/arm/kernel/smp_twd.c

index 634f357be6bb787d8a578cfa90a99b7742529b19..79aebe2ad7a8ec6749a4459476fb751cd51ad8bf 100644 (file)
@@ -17,6 +17,7 @@
 #define TWD_TIMER_CONTROL_ONESHOT      (0 << 1)
 #define TWD_TIMER_CONTROL_PERIODIC     (1 << 1)
 #define TWD_TIMER_CONTROL_IT_ENABLE    (1 << 2)
+#define TWD_TIMER_CONTROL_PRESCALE_MASK        (0xFF << 8)
 
 struct clock_event_device;
 
@@ -26,4 +27,22 @@ void twd_timer_stop(void);
 int twd_timer_ack(void);
 void twd_timer_setup(struct clock_event_device *);
 
+/*
+ * Use this setup function on systems where the cpu clock frequency may
+ * change.  periphclk_prescaler is the fixed divider value between the cpu
+ * clock and the PERIPHCLK clock that feeds the TWD.  target_rate should be
+ * low enough that the prescaler can accurately reach the target rate from the
+ * lowest cpu frequency.
+ */
+void twd_timer_setup_scalable(struct clock_event_device *,
+       unsigned long target_rate, unsigned int periphclk_prescaler);
+
+/*
+ * Recalculate the twd prescaler value when the cpu frequency changes. To
+ * prevent early timer interrupts, must be called before changing the cpu
+ * frequency if the frequency is increasing, or after if the frequency is
+ * decreasing.
+ */
+void twd_recalc_prescaler(unsigned long new_rate);
+
 #endif
index 35882fbf37f90063723d3247352a3c05af480f99..2b4f92788c0e11c1286787912994750372f07912 100644 (file)
@@ -25,6 +25,8 @@
 void __iomem *twd_base;
 
 static unsigned long twd_timer_rate;
+static unsigned long twd_periphclk_prescaler;
+static unsigned long twd_target_rate;
 
 static void twd_set_mode(enum clock_event_mode mode,
                        struct clock_event_device *clk)
@@ -79,10 +81,31 @@ int twd_timer_ack(void)
        return 0;
 }
 
-static void __cpuinit twd_calibrate_rate(void)
+void twd_recalc_prescaler(unsigned long new_rate)
+{
+       u32 ctrl;
+       int prescaler;
+       unsigned long periphclk_rate;
+
+       BUG_ON(twd_periphclk_prescaler == 0 || twd_timer_rate == 0);
+
+       periphclk_rate = new_rate / twd_periphclk_prescaler;
+
+       prescaler = DIV_ROUND_UP(periphclk_rate, twd_timer_rate);
+       prescaler = clamp(prescaler - 1, 0, 0xFF);
+
+       ctrl = __raw_readl(twd_base + TWD_TIMER_CONTROL);
+       ctrl &= ~TWD_TIMER_CONTROL_PRESCALE_MASK;
+       ctrl |= prescaler << 8;
+       __raw_writel(ctrl, twd_base + TWD_TIMER_CONTROL);
+}
+
+static void __cpuinit twd_calibrate_rate(unsigned long target_rate,
+       unsigned int periphclk_prescaler)
 {
        unsigned long load, count;
        u64 waitjiffies;
+       unsigned long cpu_rate;
 
        /*
         * If this is the first time round, we need to work out how fast
@@ -113,8 +136,22 @@ static void __cpuinit twd_calibrate_rate(void)
 
                twd_timer_rate = (0xFFFFFFFFU - count) * (HZ / 5);
 
+               /*
+                * If a target rate has been requested, adjust the TWD prescaler
+                * to get the closest lower frequency.
+                */
+               if (target_rate) {
+                       twd_periphclk_prescaler = periphclk_prescaler;
+                       twd_target_rate = target_rate;
+
+                       cpu_rate = twd_timer_rate * periphclk_prescaler;
+                       twd_recalc_prescaler(cpu_rate);
+
+                       twd_timer_rate = twd_target_rate;
+               }
+
                printk("%lu.%02luMHz.\n", twd_timer_rate / 1000000,
-                       (twd_timer_rate / 100000) % 100);
+                       (twd_timer_rate / 10000) % 100);
        }
 
        load = twd_timer_rate / HZ;
@@ -125,11 +162,12 @@ static void __cpuinit twd_calibrate_rate(void)
 /*
  * Setup the local clock events for a CPU.
  */
-void __cpuinit twd_timer_setup(struct clock_event_device *clk)
+static void __cpuinit __twd_timer_setup(struct clock_event_device *clk,
+       unsigned long target_rate, unsigned int periphclk_prescaler)
 {
        unsigned long flags;
 
-       twd_calibrate_rate();
+       twd_calibrate_rate(target_rate, periphclk_prescaler);
 
        clk->name = "local_timer";
        clk->features = CLOCK_EVT_FEAT_PERIODIC | CLOCK_EVT_FEAT_ONESHOT |
@@ -151,6 +189,17 @@ void __cpuinit twd_timer_setup(struct clock_event_device *clk)
        clockevents_register_device(clk);
 }
 
+void __cpuinit twd_timer_setup_scalable(struct clock_event_device *clk,
+       unsigned long target_rate, unsigned int periphclk_prescaler)
+{
+       __twd_timer_setup(clk, target_rate, periphclk_prescaler);
+}
+
+void __cpuinit twd_timer_setup(struct clock_event_device *clk)
+{
+       __twd_timer_setup(clk, 0, 0);
+}
+
 #ifdef CONFIG_HOTPLUG_CPU
 /*
  * take a local timer down