From mboxrd@z Thu Jan 1 00:00:00 1970 From: john stultz Date: Wed, 10 Dec 2008 20:33:43 +0000 Subject: Re: [PATCH 04/05] sh: CMT platform driver using early timer Message-Id: <1228941223.6930.4.camel@localhost.localdomain> List-Id: References: <20081210151307.32515.19409.sendpatchset@rx1.opensource.se> In-Reply-To: <20081210151307.32515.19409.sendpatchset@rx1.opensource.se> MIME-Version: 1.0 Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit To: linux-sh@vger.kernel.org On Thu, 2008-12-11 at 00:13 +0900, Magnus Damm wrote: > From: Magnus Damm > > CMT driver for the SuperH timer base code implementation V4. > Both 16-bit and 32-bit CMT versions are supported, but only 32-bit > is tested. This driver contains support for both clockevents and > clocksources, but no unregistration is supported at this point. > > Works fine as clock source and/or event in periodic or oneshot mode. > Tested on sh7722 and sh7723, but should work with any cpu/architecture. > > Signed-off-by: Magnus Damm Looks good to me. Thanks again for reworking this! Acked-by: John Stultz > --- > > Changes since V3 include: > - Removed dependency of timer_inc > - Removed unregistration code > > Changes since V2 include: > - Rewrote driver to platform driver and early timer > - Only control clocks and hardware registers in disable()/enable() > - Removed exported struct from sh_timer.c/sh_timer.h > - sh_timer_early_probe() is used for early timer setup > > Changes since V1 include: > - Reworked private data handling - no more union in sh_timer.h > - Made spinlock static > - Use setup_irq() to request early interrupt > - Removed request_mem_region()/free_mem_region() > - Removed sysdev code > > arch/sh/Kconfig | 7 > drivers/clocksource/Makefile | 1 > drivers/clocksource/sh_cmt.c | 702 ++++++++++++++++++++++++++++++++++++++++++ > 3 files changed, 710 insertions(+) > > --- 0010/arch/sh/Kconfig > +++ work/arch/sh/Kconfig 2008-12-10 22:59:45.000000000 +0900 > @@ -403,6 +403,13 @@ config SH_TIMER > def_bool n > select GENERIC_CLOCKEVENTS > > +config SH_TIMER_CMT > + def_bool y > + prompt "CMT timer support" > + depends on SH_TIMER > + help > + This enables build of the CMT system timer driver. > + > config SH_TMU > def_bool y > prompt "TMU timer support" > --- 0009/drivers/clocksource/Makefile > +++ work/drivers/clocksource/Makefile 2008-12-10 22:59:45.000000000 +0900 > @@ -3,3 +3,4 @@ obj-$(CONFIG_X86_CYCLONE_TIMER) += cyclo > obj-$(CONFIG_X86_PM_TIMER) += acpi_pm.o > obj-$(CONFIG_SCx200HR_TIMER) += scx200_hrt.o > obj-$(CONFIG_SH_TIMER) += sh_timer.o > +obj-$(CONFIG_SH_TIMER_CMT) += sh_cmt.o > --- /dev/null > +++ work/drivers/clocksource/sh_cmt.c 2008-12-10 23:01:48.000000000 +0900 > @@ -0,0 +1,702 @@ > +/* > + * SuperH Timer Support - CMT > + * > + * Copyright (C) 2008 Magnus Damm > + * > + * 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 > + * > + * 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. > + * > + * You should have received a copy of the GNU General Public License > + * along with this program; if not, write to the Free Software > + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA > + */ > + > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > + > +struct sh_cmt_priv { > + void __iomem *mapbase; > + struct clk *clk; > + unsigned long width; /* 16 or 32 bit version of hardware block */ > + unsigned long overflow_bit; > + unsigned long clear_bits; > + struct irqaction irqaction; > + struct platform_device *pdev; > + > + unsigned long flags; > + unsigned long match_value; > + unsigned long next_match_value; > + unsigned long max_match_value; > + unsigned long rate; > + spinlock_t lock; > + struct clock_event_device ced; > + struct clocksource cs; > + unsigned long total_cycles; > +}; > + > +static DEFINE_SPINLOCK(sh_cmt_lock); > + > +#define CMSTR -1 /* shared register */ > +#define CMCSR 0 /* channel register */ > +#define CMCNT 1 /* channel register */ > +#define CMCOR 2 /* channel register */ > + > +static inline unsigned long sh_cmt_read(struct sh_cmt_priv *p, int reg_nr) > +{ > + struct sh_timer_config *cfg = p->pdev->dev.platform_data; > + void __iomem *base = p->mapbase; > + unsigned long offs; > + > + if (reg_nr = CMSTR) { > + offs = 0; > + base -= cfg->channel_offset; > + } else > + offs = reg_nr; > + > + if (p->width = 16) > + offs <<= 1; > + else { > + offs <<= 2; > + if ((reg_nr = CMCNT) || (reg_nr = CMCOR)) > + return ioread32(base + offs); > + } > + > + return ioread16(base + offs); > +} > + > +static inline void sh_cmt_write(struct sh_cmt_priv *p, int reg_nr, > + unsigned long value) > +{ > + struct sh_timer_config *cfg = p->pdev->dev.platform_data; > + void __iomem *base = p->mapbase; > + unsigned long offs; > + > + if (reg_nr = CMSTR) { > + offs = 0; > + base -= cfg->channel_offset; > + } else > + offs = reg_nr; > + > + if (p->width = 16) > + offs <<= 1; > + else { > + offs <<= 2; > + if ((reg_nr = CMCNT) || (reg_nr = CMCOR)) { > + iowrite32(value, base + offs); > + return; > + } > + } > + > + iowrite16(value, base + offs); > +} > + > +static unsigned long sh_cmt_get_counter(struct sh_cmt_priv *p, > + int *has_wrapped) > +{ > + unsigned long v1, v2, v3; > + > + /* Make sure the timer value is stable. Stolen from acpi_pm.c */ > + do { > + v1 = sh_cmt_read(p, CMCNT); > + v2 = sh_cmt_read(p, CMCNT); > + v3 = sh_cmt_read(p, CMCNT); > + } while (unlikely((v1 > v2 && v1 < v3) || (v2 > v3 && v2 < v1) > + || (v3 > v1 && v3 < v2))); > + > + *has_wrapped = sh_cmt_read(p, CMCSR) & p->overflow_bit; > + return v2; > +} > + > + > +static void sh_cmt_start_stop_ch(struct sh_cmt_priv *p, int start) > +{ > + struct sh_timer_config *cfg = p->pdev->dev.platform_data; > + unsigned long flags, value; > + > + /* start stop register shared by multiple timer channels */ > + spin_lock_irqsave(&sh_cmt_lock, flags); > + value = sh_cmt_read(p, CMSTR); > + > + if (start) > + value |= 1 << cfg->timer_bit; > + else > + value &= ~(1 << cfg->timer_bit); > + > + sh_cmt_write(p, CMSTR, value); > + spin_unlock_irqrestore(&sh_cmt_lock, flags); > +} > + > +static int sh_cmt_enable(struct sh_cmt_priv *p, unsigned long *rate) > +{ > + struct sh_timer_config *cfg = p->pdev->dev.platform_data; > + int ret; > + > + /* enable clock */ > + ret = clk_enable(p->clk); > + if (ret) { > + pr_err("sh_cmt: cannot enable clock \"%s\"\n", cfg->clk); > + return ret; > + } > + *rate = clk_get_rate(p->clk) / 8; > + > + /* make sure channel is disabled */ > + sh_cmt_start_stop_ch(p, 0); > + > + /* configure channel, periodic mode and maximum timeout */ > + if (p->width = 16) > + sh_cmt_write(p, CMCSR, 0); > + else > + sh_cmt_write(p, CMCSR, 0x01a4); > + > + sh_cmt_write(p, CMCOR, 0xffffffff); > + sh_cmt_write(p, CMCNT, 0); > + > + /* enable channel */ > + sh_cmt_start_stop_ch(p, 1); > + return 0; > +} > + > +static void sh_cmt_disable(struct sh_cmt_priv *p) > +{ > + /* disable channel */ > + sh_cmt_start_stop_ch(p, 0); > + > + /* stop clock */ > + clk_disable(p->clk); > +} > + > +/* private flags */ > +#define FLAG_CLOCKEVENT (1 << 0) > +#define FLAG_CLOCKSOURCE (1 << 1) > +#define FLAG_REPROGRAM (1 << 2) > +#define FLAG_SKIPEVENT (1 << 3) > +#define FLAG_IRQCONTEXT (1 << 4) > + > +static void sh_cmt_clock_event_program_verify(struct sh_cmt_priv *p, > + int absolute) > +{ > + unsigned long new_match; > + unsigned long value = p->next_match_value; > + unsigned long delay = 0; > + unsigned long now = 0; > + int has_wrapped; > + > + now = sh_cmt_get_counter(p, &has_wrapped); > + p->flags |= FLAG_REPROGRAM; /* force reprogram */ > + > + if (has_wrapped) { > + /* we're competing with the interrupt handler. > + * -> let the interrupt handler reprogram the timer. > + * -> interrupt number two handles the event. > + */ > + p->flags |= FLAG_SKIPEVENT; > + return; > + } > + > + if (absolute) > + now = 0; > + > + do { > + /* reprogram the timer hardware, > + * but don't save the new match value yet. > + */ > + new_match = now + value + delay; > + if (new_match > p->max_match_value) > + new_match = p->max_match_value; > + > + sh_cmt_write(p, CMCOR, new_match); > + > + now = sh_cmt_get_counter(p, &has_wrapped); > + if (has_wrapped && (new_match > p->match_value)) { > + /* we are changing to a greater match value, > + * so this wrap must be caused by the counter > + * matching the old value. > + * -> first interrupt reprograms the timer. > + * -> interrupt number two handles the event. > + */ > + p->flags |= FLAG_SKIPEVENT; > + break; > + } > + > + if (has_wrapped) { > + /* we are changing to a smaller match value, > + * so the wrap must be caused by the counter > + * matching the new value. > + * -> save programmed match value. > + * -> let isr handle the event. > + */ > + p->match_value = new_match; > + break; > + } > + > + /* be safe: verify hardware settings */ > + if (now < new_match) { > + /* timer value is below match value, all good. > + * this makes sure we won't miss any match events. > + * -> save programmed match value. > + * -> let isr handle the event. > + */ > + p->match_value = new_match; > + break; > + } > + > + /* the counter has reached a value greater > + * than our new match value. and since the > + * has_wrapped flag isn't set we must have > + * programmed a too close event. > + * -> increase delay and retry. > + */ > + if (delay) > + delay <<= 1; > + else > + delay = 1; > + > + if (!delay) > + pr_warning("sh_cmt: too long delay\n"); > + > + } while (delay); > +} > + > +static void sh_cmt_set_next(struct sh_cmt_priv *p, unsigned long delta) > +{ > + unsigned long flags; > + > + if (delta > p->max_match_value) > + pr_warning("sh_cmt: delta out of range\n"); > + > + spin_lock_irqsave(&p->lock, flags); > + p->next_match_value = delta; > + sh_cmt_clock_event_program_verify(p, 0); > + spin_unlock_irqrestore(&p->lock, flags); > +} > + > +static irqreturn_t sh_cmt_interrupt(int irq, void *dev_id) > +{ > + struct sh_cmt_priv *p = dev_id; > + > + /* clear flags */ > + sh_cmt_write(p, CMCSR, sh_cmt_read(p, CMCSR) & p->clear_bits); > + > + /* update clock source counter to begin with if enabled > + * the wrap flag should be cleared by the timer specific > + * isr before we end up here. > + */ > + if (p->flags & FLAG_CLOCKSOURCE) > + p->total_cycles += p->match_value; > + > + if (!(p->flags & FLAG_REPROGRAM)) > + p->next_match_value = p->max_match_value; > + > + p->flags |= FLAG_IRQCONTEXT; > + > + if (p->flags & FLAG_CLOCKEVENT) { > + if (!(p->flags & FLAG_SKIPEVENT)) { > + if (p->ced.mode = CLOCK_EVT_MODE_ONESHOT) { > + p->next_match_value = p->max_match_value; > + p->flags |= FLAG_REPROGRAM; > + } > + > + p->ced.event_handler(&p->ced); > + } > + } > + > + p->flags &= ~FLAG_SKIPEVENT; > + > + if (p->flags & FLAG_REPROGRAM) { > + p->flags &= ~FLAG_REPROGRAM; > + sh_cmt_clock_event_program_verify(p, 1); > + > + if (p->flags & FLAG_CLOCKEVENT) > + if ((p->ced.mode = CLOCK_EVT_MODE_SHUTDOWN) > + || (p->match_value = p->next_match_value)) > + p->flags &= ~FLAG_REPROGRAM; > + } > + > + p->flags &= ~FLAG_IRQCONTEXT; > + > + return IRQ_HANDLED; > +} > + > +static int sh_cmt_start(struct sh_cmt_priv *p, unsigned long flag) > +{ > + int ret = 0; > + unsigned long flags; > + > + spin_lock_irqsave(&p->lock, flags); > + > + if (!(p->flags & (FLAG_CLOCKEVENT | FLAG_CLOCKSOURCE))) > + ret = sh_cmt_enable(p, &p->rate); > + > + if (ret) > + goto out; > + p->flags |= flag; > + > + /* setup timeout if no clockevent */ > + if ((flag = FLAG_CLOCKSOURCE) && (!(p->flags & FLAG_CLOCKEVENT))) > + sh_cmt_set_next(p, p->max_match_value); > + out: > + spin_unlock_irqrestore(&p->lock, flags); > + > + return ret; > +} > + > +static void sh_cmt_stop(struct sh_cmt_priv *p, unsigned long flag) > +{ > + unsigned long flags; > + unsigned long f; > + > + spin_lock_irqsave(&p->lock, flags); > + > + f = p->flags & (FLAG_CLOCKEVENT | FLAG_CLOCKSOURCE); > + p->flags &= ~flag; > + > + if (f && !(p->flags & (FLAG_CLOCKEVENT | FLAG_CLOCKSOURCE))) > + sh_cmt_disable(p); > + > + /* adjust the timeout to maximum if only clocksource left */ > + if ((flag = FLAG_CLOCKEVENT) && (p->flags & FLAG_CLOCKSOURCE)) > + sh_cmt_set_next(p, p->max_match_value); > + > + spin_unlock_irqrestore(&p->lock, flags); > +} > + > +static struct sh_cmt_priv *cs_to_sh_cmt(struct clocksource *cs) > +{ > + return container_of(cs, struct sh_cmt_priv, cs); > +} > + > +static cycle_t sh_cmt_clocksource_read(struct clocksource *cs) > +{ > + struct sh_cmt_priv *p = cs_to_sh_cmt(cs); > + unsigned long flags, raw; > + unsigned long value; > + int has_wrapped; > + > + spin_lock_irqsave(&p->lock, flags); > + value = p->total_cycles; > + raw = sh_cmt_get_counter(p, &has_wrapped); > + > + if (unlikely(has_wrapped)) > + raw = p->match_value; > + spin_unlock_irqrestore(&p->lock, flags); > + > + return value + raw; > +} > + > +static int sh_cmt_clocksource_enable(struct clocksource *cs) > +{ > + struct sh_cmt_priv *p = cs_to_sh_cmt(cs); > + int ret; > + > + p->total_cycles = 0; > + > + ret = sh_cmt_start(p, FLAG_CLOCKSOURCE); > + if (ret) > + return ret; > + > + /* TODO: calculate good shift from rate and counter bit width */ > + cs->shift = 0; > + cs->mult = clocksource_hz2mult(p->rate, cs->shift); > + return 0; > +} > + > +static void sh_cmt_clocksource_disable(struct clocksource *cs) > +{ > + sh_cmt_stop(cs_to_sh_cmt(cs), FLAG_CLOCKSOURCE); > +} > + > +static int sh_cmt_register_clocksource(struct sh_cmt_priv *p, > + char *name, unsigned long rating) > +{ > + struct clocksource *cs = &p->cs; > + > + cs->name = name; > + cs->rating = rating; > + cs->read2 = sh_cmt_clocksource_read; > + cs->enable = sh_cmt_clocksource_enable; > + cs->disable = sh_cmt_clocksource_disable; > + cs->mask = CLOCKSOURCE_MASK(sizeof(unsigned long) * 8); > + cs->flags = CLOCK_SOURCE_IS_CONTINUOUS; > + pr_info("sh_cmt: %s used as clock source\n", cs->name); > + clocksource_register(cs); > + return 0; > +} > + > +static struct sh_cmt_priv *ced_to_sh_cmt(struct clock_event_device *ced) > +{ > + return container_of(ced, struct sh_cmt_priv, ced); > +} > + > +static void sh_cmt_clock_event_start(struct sh_cmt_priv *p, int periodic) > +{ > + struct clock_event_device *ced = &p->ced; > + > + sh_cmt_start(p, FLAG_CLOCKEVENT); > + > + /* TODO: calculate good shift from rate and counter bit width */ > + > + ced->shift = 32; > + ced->mult = div_sc(p->rate, NSEC_PER_SEC, ced->shift); > + ced->max_delta_ns = clockevent_delta2ns(p->max_match_value, ced); > + ced->min_delta_ns = clockevent_delta2ns(0x1f, ced); > + > + if (periodic) > + sh_cmt_set_next(p, (p->rate + HZ/2) / HZ); > + else > + sh_cmt_set_next(p, p->max_match_value); > +} > + > +static void sh_cmt_clock_event_mode(enum clock_event_mode mode, > + struct clock_event_device *ced) > +{ > + struct sh_cmt_priv *p = ced_to_sh_cmt(ced); > + > + /* deal with old setting first */ > + switch (ced->mode) { > + case CLOCK_EVT_MODE_PERIODIC: > + case CLOCK_EVT_MODE_ONESHOT: > + sh_cmt_stop(p, FLAG_CLOCKEVENT); > + break; > + default: > + break; > + } > + > + switch (mode) { > + case CLOCK_EVT_MODE_PERIODIC: > + pr_info("sh_cmt: %s used for periodic clock events\n", > + ced->name); > + sh_cmt_clock_event_start(p, 1); > + break; > + case CLOCK_EVT_MODE_ONESHOT: > + pr_info("sh_cmt: %s used for oneshot clock events\n", > + ced->name); > + sh_cmt_clock_event_start(p, 0); > + break; > + case CLOCK_EVT_MODE_SHUTDOWN: > + case CLOCK_EVT_MODE_UNUSED: > + sh_cmt_stop(p, FLAG_CLOCKEVENT); > + break; > + default: > + break; > + } > +} > + > +static int sh_cmt_clock_event_next(unsigned long delta, > + struct clock_event_device *ced) > +{ > + struct sh_cmt_priv *p = ced_to_sh_cmt(ced); > + > + BUG_ON(ced->mode != CLOCK_EVT_MODE_ONESHOT); > + if (likely(p->flags & FLAG_IRQCONTEXT)) > + p->next_match_value = delta; > + else > + sh_cmt_set_next(p, delta); > + > + return 0; > +} > + > +static void sh_cmt_register_clockevent(struct sh_cmt_priv *p, > + char *name, unsigned long rating) > +{ > + struct clock_event_device *ced = &p->ced; > + > + memset(ced, 0, sizeof(*ced)); > + > + ced->name = name; > + ced->features = CLOCK_EVT_FEAT_PERIODIC; > + ced->features |= CLOCK_EVT_FEAT_ONESHOT; > + ced->rating = rating; > + ced->cpumask = CPU_MASK_CPU0; > + ced->set_next_event = sh_cmt_clock_event_next; > + ced->set_mode = sh_cmt_clock_event_mode; > + > + pr_info("sh_cmt: %s used for clock events\n", ced->name); > + ced->mult = 1; /* work around misplaced WARN_ON() in clockevents.c */ > + clockevents_register_device(ced); > +} > + > +int sh_cmt_register(struct sh_cmt_priv *p, char *name, > + unsigned long clockevent_rating, > + unsigned long clocksource_rating) > +{ > + if (p->width = (sizeof(p->max_match_value) * 8)) > + p->max_match_value = ~0; > + else > + p->max_match_value = (1 << p->width) - 1; > + > + p->match_value = p->max_match_value; > + spin_lock_init(&p->lock); > + > + if (clockevent_rating) > + sh_cmt_register_clockevent(p, name, clockevent_rating); > + > + if (clocksource_rating) > + sh_cmt_register_clocksource(p, name, clocksource_rating); > + > + return 0; > +} > + > +static int sh_cmt_setup(void *priv, struct platform_device *pdev) > +{ > + struct sh_cmt_priv *p = priv; > + struct sh_timer_config *cfg = pdev->dev.platform_data; > + struct resource *res; > + int irq, ret; > + ret = -ENXIO; > + > + memset(p, 0, sizeof(*p)); > + p->pdev = pdev; > + > + if (!cfg) { > + dev_err(&p->pdev->dev, "missing platform data\n"); > + goto err0; > + } > + > + platform_set_drvdata(pdev, priv); > + > + res = platform_get_resource(p->pdev, IORESOURCE_MEM, 0); > + if (!res) { > + dev_err(&p->pdev->dev, "failed to get I/O memory\n"); > + goto err0; > + } > + > + irq = platform_get_irq(p->pdev, 0); > + if (irq < 0) { > + dev_err(&p->pdev->dev, "failed to get irq\n"); > + goto err0; > + } > + > + /* map memory, let mapbase point to our channel */ > + p->mapbase = ioremap_nocache(res->start, resource_size(res)); > + if (p->mapbase = NULL) { > + pr_err("sh_cmt: failed to remap I/O memory\n"); > + goto err0; > + } > + > + /* request irq using setup_irq() (too early for request_irq()) */ > + p->irqaction.name = cfg->name; > + p->irqaction.handler = sh_cmt_interrupt; > + p->irqaction.dev_id = p; > + p->irqaction.flags = IRQF_DISABLED | IRQF_TIMER | IRQF_IRQPOLL; > + p->irqaction.mask = CPU_MASK_NONE; > + ret = setup_irq(irq, &p->irqaction); > + if (ret) { > + pr_err("sh_cmt: failed to request irq %d\n", irq); > + goto err1; > + } > + > + /* get hold of clock */ > + p->clk = clk_get(&p->pdev->dev, cfg->clk); > + if (IS_ERR(p->clk)) { > + pr_err("sh_cmt: cannot get clock \"%s\"\n", cfg->clk); > + ret = PTR_ERR(p->clk); > + goto err2; > + } > + > + if (resource_size(res) = 6) { > + p->width = 16; > + p->overflow_bit = 0x80; > + p->clear_bits = ~0xc0; > + } else { > + p->width = 32; > + p->overflow_bit = 0x8000; > + p->clear_bits = ~0xc000; > + } > + > + return sh_cmt_register(p, cfg->name, > + cfg->clockevent_rating, > + cfg->clocksource_rating); > + err2: > + free_irq(irq, p); > + err1: > + iounmap(p->mapbase); > + err0: > + return ret; > +} > + > +#ifndef MODULE > +/* early timer interface, used before platform devices are available */ > +static struct sh_timer_early sh_cmt = { > + .priv_size = sizeof(struct sh_cmt_priv), > + .setup = sh_cmt_setup, > +}; > + > +struct sh_timer_early *sh_timer_early_probe(struct platform_device *pdev) > +{ > + if (!strcmp(pdev->name, "sh_cmt")) > + return &sh_cmt; > + > + return NULL; > +} > +#endif > + > +static int __devinit sh_cmt_probe(struct platform_device *pdev) > +{ > + struct sh_cmt_priv *p = platform_get_drvdata(pdev); > + struct sh_timer_config *cfg = pdev->dev.platform_data; > + int ret; > + > + if (p) { > + pr_info("sh_cmt: %s kept as early timer\n", cfg->name); > + return 0; > + } > + > + p = kmalloc(sizeof(*p), GFP_KERNEL); > + if (p = NULL) { > + dev_err(&pdev->dev, "failed to allocate driver data\n"); > + return -ENOMEM; > + } > + > + ret = sh_cmt_setup(p, pdev); > + if (ret) { > + kfree(p); > + platform_set_drvdata(pdev, NULL); > + } > + return ret; > +} > + > +static int __devexit sh_cmt_remove(struct platform_device *pdev) > +{ > + return -EBUSY; /* cannot unregister clockevent and clocksource */ > +} > + > +static struct platform_driver sh_cmt_device_driver = { > + .probe = sh_cmt_probe, > + .remove = __devexit_p(sh_cmt_remove), > + .driver = { > + .name = "sh_cmt", > + } > +}; > + > +static int __init sh_cmt_init(void) > +{ > + return platform_driver_register(&sh_cmt_device_driver); > +} > + > +static void __exit sh_cmt_exit(void) > +{ > + platform_driver_unregister(&sh_cmt_device_driver); > +} > + > +module_init(sh_cmt_init); > +module_exit(sh_cmt_exit); > + > +MODULE_AUTHOR("Magnus Damm"); > +MODULE_DESCRIPTION("SuperH CMT Timer Driver"); > +MODULE_LICENSE("GPL v2");