[RFC] AT91 cpufreq support
Albin Tonnerre
albin.tonnerre at free-electrons.com
Wed Aug 26 15:33:24 EDT 2009
Hi there,
Here is an updated version of the patch that brings cpufreq support on AT91.
This one is more generic that the previous one: instead of using fixed values,
it gets the current prescaler value and builds the frequency table accordingly.
It fixes a couple mistakes from the previous iteration, making it work more
reliably, at least there. I also added a cpufreq notifier to atmel_serial, so
that we don't lose the console over a frequency change (I'll move it to a
separate patch in a further update, and will add such support for the macb
driver too).
As there's no cpufreq notifier in the macb driver (yet), using cpufreq with a
NFS root filesystem will fail (obviously, other uses of the network will
probably fail too, but at least they don't make the system unusable)
On a Calao USB-A9263 board, switching from 180MHz to 11MHz reduces the current
consumption by roughly 65mA
Testing/feedback would be very much welcome
Regards,
Albin
---
arch/arm/Kconfig | 10 ++-
arch/arm/mach-at91/Makefile | 1 +
arch/arm/mach-at91/cpufreq-at91.c | 197 +++++++++++++++++++++++++++++++++++++
drivers/serial/atmel_serial.c | 81 +++++++++++++++
4 files changed, 288 insertions(+), 1 deletions(-)
create mode 100644 arch/arm/mach-at91/cpufreq-at91.c
diff --git a/arch/arm/Kconfig b/arch/arm/Kconfig
index aef63c8..d47f8ea 100644
--- a/arch/arm/Kconfig
+++ b/arch/arm/Kconfig
@@ -1241,7 +1241,7 @@ endmenu
menu "CPU Power Management"
-if (ARCH_SA1100 || ARCH_INTEGRATOR || ARCH_OMAP || ARCH_PXA || ARCH_S3C64XX)
+if (ARCH_SA1100 || ARCH_INTEGRATOR || ARCH_OMAP || ARCH_PXA || ARCH_S3C64XX || ARCH_AT91)
source "drivers/cpufreq/Kconfig"
@@ -1276,6 +1276,14 @@ config CPU_FREQ_S3C64XX
bool "CPUfreq support for Samsung S3C64XX CPUs"
depends on CPU_FREQ && CPU_S3C6410
+config CPU_FREQ_AT91
+ bool "CPUfreq driver from ATMEL AT91 ARM CPUs (EXPERIMENTAL)"
+ depends on CPU_FREQ && ARCH_AT91 && EXPERIMENTAL
+ help
+ This enables the CPUfreq driver for ARM AT91 CPUs.
+
+ If in doubt, say N.
+
endif
source "drivers/cpuidle/Kconfig"
diff --git a/arch/arm/mach-at91/Makefile b/arch/arm/mach-at91/Makefile
index c69ff23..d4b4e16 100644
--- a/arch/arm/mach-at91/Makefile
+++ b/arch/arm/mach-at91/Makefile
@@ -67,6 +67,7 @@ obj-y += leds.o
# Power Management
obj-$(CONFIG_PM) += pm.o
obj-$(CONFIG_AT91_SLOW_CLOCK) += pm_slowclock.o
+obj-$(CONFIG_CPU_FREQ_AT91) += cpufreq-at91.o
ifeq ($(CONFIG_PM_DEBUG),y)
CFLAGS_pm.o += -DDEBUG
diff --git a/arch/arm/mach-at91/cpufreq-at91.c b/arch/arm/mach-at91/cpufreq-at91.c
new file mode 100644
index 0000000..32e955b
--- /dev/null
+++ b/arch/arm/mach-at91/cpufreq-at91.c
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2009
+ * Albin Tonnerre, Free Electrons <albin.tonnerre at free-electrons.com>
+ *
+ * Based on linux/arch/arm/mach-pxa/cpufreq-pxa3xx.c
+ * Copyright (C) 2008 Marvell International Ltd.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ */
+
+#include <linux/module.h>
+#include <linux/kernel.h>
+#include <linux/init.h>
+#include <linux/errno.h>
+#include <linux/io.h>
+#include <linux/cpufreq.h>
+#include <linux/clk.h>
+
+#include <mach/hardware.h>
+#include <mach/at91_pmc.h>
+#include <mach/cpu.h>
+#include <linux/delay.h>
+#include "clock.h"
+
+struct at91_freq_info {
+ unsigned int cpufreq_khz;
+ u32 mckr_pres;
+};
+
+static struct at91_freq_info *at91_freqs;
+static struct cpufreq_frequency_table *at91_freqs_table;
+
+static int setup_freqs_table(struct cpufreq_policy *policy,
+ struct at91_freq_info *freqs, int num)
+{
+ struct cpufreq_frequency_table *table;
+ int i;
+
+ table = kzalloc((num + 1) * sizeof(*table), GFP_KERNEL);
+ if (table == NULL)
+ return -ENOMEM;
+
+ for (i = 0; i < num; i++) {
+ table[i].index = i;
+ table[i].frequency = freqs[i].cpufreq_khz;
+ }
+ table[num].frequency = i;
+ table[num].frequency = CPUFREQ_TABLE_END;
+
+ at91_freqs = freqs;
+ at91_freqs_table = table;
+
+ return cpufreq_frequency_table_cpuinfo(policy, table);
+}
+
+static void __update_core_freq(struct clk *clk, struct at91_freq_info *info)
+{
+ u32 mckr, cur_mdiv;
+
+ cur_mdiv = at91_sys_read(AT91_PMC_MCKR) & AT91_PMC_MDIV;
+ mckr = info->mckr_pres | cur_mdiv | AT91_PMC_CSS_PLLA;
+
+ at91_sys_write(AT91_PMC_MCKR, mckr);
+ while(!(at91_sys_read(AT91_PMC_SR) & AT91_PMC_MCKRDY))
+ cpu_relax();
+
+ clk->rate_hz = (info->cpufreq_khz * 1000) >> (cur_mdiv >> 8);
+}
+
+static int at91_cpufreq_verify(struct cpufreq_policy *policy)
+{
+ return cpufreq_frequency_table_verify(policy, at91_freqs_table);
+}
+
+static unsigned int at91_cpufreq_get(unsigned int cpu)
+{
+ u32 mdiv;
+
+ mdiv = 1 << ((at91_sys_read(AT91_PMC_MCKR) & AT91_PMC_MDIV) >> 8);
+ return mdiv * clk_get_rate(clk_get(NULL, "mck")) / 1000;
+}
+
+static int at91_cpufreq_set(struct cpufreq_policy *policy,
+ unsigned int target_freq,
+ unsigned int relation)
+{
+ struct at91_freq_info *next;
+ struct cpufreq_freqs freqs;
+ unsigned long flags;
+ int idx;
+ struct clk *clk;
+
+ if (policy->cpu != 0)
+ return -EINVAL;
+
+ /* don't do anything if the master clock source is not PLLA */
+ clk = clk_get(NULL, "mck");
+ if (strcmp(clk->parent->name, "plla"))
+ return -EINVAL;
+
+ /* Lookup the next frequency */
+ if (cpufreq_frequency_table_target(policy, at91_freqs_table,
+ target_freq, relation, &idx))
+ return -EINVAL;
+
+ next = &at91_freqs[idx];
+
+ freqs.old = policy->cur;
+ freqs.new = next->cpufreq_khz;
+ freqs.cpu = policy->cpu;
+
+ printk("CPU frequency from %d MHz to %d MHz%s\n",
+ freqs.old / 1000, freqs.new / 1000,
+ (freqs.old == freqs.new) ? " (skipped)" : "");
+
+ if (freqs.old == target_freq)
+ return 0;
+
+ cpufreq_notify_transition(&freqs, CPUFREQ_PRECHANGE);
+
+ local_irq_save(flags);
+ __update_core_freq(clk, next);
+ local_irq_restore(flags);
+
+ cpufreq_notify_transition(&freqs, CPUFREQ_POSTCHANGE);
+
+ return 0;
+}
+
+static __init int at91_cpufreq_init(struct cpufreq_policy *policy)
+{
+ int ret = -EINVAL, nfreqs, i;
+ u32 pres;
+ struct clk *clk;
+ struct at91_freq_info *at91_freqs;
+
+ /* don't do anything if the master clock source is not PLLA */
+ clk = clk_get(NULL, "mck");
+ if (strcmp(clk->parent->name, "plla"))
+ return -EINVAL;
+
+ /* set default policy and cpuinfo */
+ policy->cpuinfo.min_freq = (clk->rate_hz / 1000) / AT91_PMC_PRES_16;
+ policy->cpuinfo.max_freq = clk->rate_hz / 1000;
+ policy->cpuinfo.transition_latency = 1000000; /* FIXME: 1 ms, assumed */
+ policy->cur = policy->min = policy->max = at91_cpufreq_get(policy->cpu);
+
+ pres = at91_sys_read(AT91_PMC_MCKR) & AT91_PMC_PRES;
+ nfreqs = (AT91_PMC_PRES_16 - pres) >> 2;
+ nfreqs = (nfreqs > 0) ? nfreqs + 1 : 1;
+ at91_freqs = kmalloc(sizeof(struct at91_freq_info) * nfreqs, GFP_KERNEL);
+
+ for(i = 0; i < nfreqs; i++)
+ {
+ at91_freqs[i].cpufreq_khz = policy->cur >> (pres >> 2);
+ at91_freqs[i].mckr_pres = pres;
+ pres += 1 << 2;
+ }
+
+ ret = setup_freqs_table(policy, at91_freqs, nfreqs);
+
+ if (ret) {
+ pr_err("failed to setup frequency table\n");
+ kfree(at91_freqs);
+ return ret;
+ }
+
+ pr_info("CPUFREQ support for AT91 initialized\n");
+ return 0;
+}
+
+static struct cpufreq_driver at91_cpufreq_driver = {
+ .verify = at91_cpufreq_verify,
+ .target = at91_cpufreq_set,
+ .init = at91_cpufreq_init,
+ .get = at91_cpufreq_get,
+ .name = "at91-cpufreq",
+};
+
+static int __init cpufreq_init(void)
+{
+ return cpufreq_register_driver(&at91_cpufreq_driver);
+}
+module_init(cpufreq_init);
+
+static void __exit cpufreq_exit(void)
+{
+ kfree(at91_freqs);
+ cpufreq_unregister_driver(&at91_cpufreq_driver);
+}
+module_exit(cpufreq_exit);
+
+MODULE_DESCRIPTION("CPU frequency scaling driver for AT91");
+MODULE_LICENSE("GPL");
diff --git a/drivers/serial/atmel_serial.c b/drivers/serial/atmel_serial.c
index 607d43a..5f38b90 100644
--- a/drivers/serial/atmel_serial.c
+++ b/drivers/serial/atmel_serial.c
@@ -38,6 +38,7 @@
#include <linux/dma-mapping.h>
#include <linux/atmel_pdc.h>
#include <linux/atmel_serial.h>
+#include <linux/cpufreq.h>
#include <asm/io.h>
@@ -147,6 +148,9 @@ struct atmel_uart_port {
unsigned int irq_status_prev;
struct circ_buf rx_ring;
+#ifdef CONFIG_CPU_FREQ
+ struct notifier_block freq_transition;
+#endif
};
static struct atmel_uart_port atmel_ports[ATMEL_MAX_UART];
@@ -1525,6 +1529,79 @@ static int atmel_serial_resume(struct platform_device *pdev)
#define atmel_serial_resume NULL
#endif
+#ifdef CONFIG_CPU_FREQ
+static int atmel_serial_cpufreq_transition(struct notifier_block *nb,
+ unsigned long val, void *data)
+{
+ struct atmel_uart_port *port;
+ struct uart_port *uport;
+
+ port = container_of(nb, struct atmel_uart_port, freq_transition);
+ uport = &port->uart;
+
+ /* try and work out if the baudrate is changing, we can detect
+ * a change in rate, but we do not have support for detecting
+ * a disturbance in the clock-rate over the change.
+ */
+
+ if (IS_ERR(port->clk))
+ goto exit;
+
+ if (val == CPUFREQ_PRECHANGE) {
+ /* we should really shut the port down whilst the
+ * frequency change is in progress. */
+
+ } else if (val == CPUFREQ_POSTCHANGE) {
+ struct ktermios *termios;
+ struct tty_struct *tty;
+
+ if (uport->info == NULL)
+ goto exit;
+
+ tty = uport->info->port.tty;
+
+ if (tty == NULL)
+ goto exit;
+
+ termios = tty->termios;
+
+ if (termios == NULL) {
+ printk(KERN_WARNING "%s: no termios?\n", __func__);
+ goto exit;
+ }
+ uport->uartclk = clk_get_rate(port->clk);
+ atmel_set_termios(uport, termios, NULL);
+ }
+ return NOTIFY_OK;
+
+ exit:
+ return NOTIFY_BAD;
+}
+
+static inline int atmel_serial_cpufreq_register(struct atmel_uart_port *port)
+{
+ port->freq_transition.notifier_call = atmel_serial_cpufreq_transition;
+
+ return cpufreq_register_notifier(&port->freq_transition,
+ CPUFREQ_TRANSITION_NOTIFIER);
+}
+
+static inline void atmel_serial_cpufreq_deregister(struct atmel_uart_port *port)
+{
+ cpufreq_unregister_notifier(&port->freq_transition,
+ CPUFREQ_TRANSITION_NOTIFIER);
+}
+#else
+static inline int atmel_serial_cpufreq_register(struct atmel_uart_port *port)
+{
+ return 0;
+}
+
+static inline void atmel_serial_cpufreq_deregister(struct atmel_uart_port *port)
+{
+}
+#endif
+
static int __devinit atmel_serial_probe(struct platform_device *pdev)
{
struct atmel_uart_port *port;
@@ -1561,6 +1638,9 @@ static int __devinit atmel_serial_probe(struct platform_device *pdev)
clk_disable(port->clk);
}
#endif
+ ret = atmel_serial_cpufreq_register(port);
+ if (ret < 0)
+ dev_err(&pdev->dev, "failed to add cpufreq notifier\n");
device_init_wakeup(&pdev->dev, 1);
platform_set_drvdata(pdev, port);
@@ -1589,6 +1669,7 @@ static int __devexit atmel_serial_remove(struct platform_device *pdev)
platform_set_drvdata(pdev, NULL);
ret = uart_remove_one_port(&atmel_uart, port);
+ atmel_serial_cpufreq_deregister(atmel_port);
tasklet_kill(&atmel_port->tasklet);
kfree(atmel_port->rx_ring.buf);
--
Albin Tonnerre, Free Electrons
Kernel, drivers and embedded Linux development,
consulting, training and support.
http://free-electrons.com
More information about the linux-arm-kernel
mailing list