mirror of
https://github.com/torvalds/linux.git
synced 2024-12-23 19:31:53 +00:00
3ef0665398
At first, we prefer to use mips clockevent device, so we decrease the rating of hpet clockevent device. For hpet, if HPET_MIN_PROG_DELTA (minimum delta of hpet programming) is too small and HPET_MIN_CYCLES (threshold of -ETIME checking) is too large, then hpet_next_event() can easily return -ETIME. After commitc6eb3f70d4
("hrtimer: Get rid of hrtimer softirq") this will cause a RCU stall. So, HPET_MIN_PROG_DELTA must be sufficient that we don't re-trip the -ETIME check -- if we do, we will return -ETIME, forward the next event time, try to set it, return -ETIME again, and basically lock the system up. Meanwhile, HPET_MIN_CYCLES doesn't need to be too large, 16 cycles is enough. This solution is similar to commitf9eccf2461
("clocksource/drivers /vt8500: Increase the minimum delta"). By the way, this patch ensures hpet count/compare to be 32-bit long. Signed-off-by: Huacai Chen <chenhc@lemote.com> Cc: John Crispin <john@phrozen.org> Cc: Steven J . Hill <Steven.Hill@imgtec.com> Cc: Fuxin Zhang <zhangfx@lemote.com> Cc: Zhangjin Wu <wuzhangjin@gmail.com> Cc: linux-mips@linux-mips.org Cc: stable@vger.kernel.org Patchwork: https://patchwork.linux-mips.org/patch/13819/ Signed-off-by: Ralf Baechle <ralf@linux-mips.org>
287 lines
6.2 KiB
C
287 lines
6.2 KiB
C
#include <linux/init.h>
|
|
#include <linux/pci.h>
|
|
#include <linux/percpu.h>
|
|
#include <linux/delay.h>
|
|
#include <linux/spinlock.h>
|
|
#include <linux/interrupt.h>
|
|
|
|
#include <asm/hpet.h>
|
|
#include <asm/time.h>
|
|
|
|
#define SMBUS_CFG_BASE (loongson_sysconf.ht_control_base + 0x0300a000)
|
|
#define SMBUS_PCI_REG40 0x40
|
|
#define SMBUS_PCI_REG64 0x64
|
|
#define SMBUS_PCI_REGB4 0xb4
|
|
|
|
#define HPET_MIN_CYCLES 16
|
|
#define HPET_MIN_PROG_DELTA (HPET_MIN_CYCLES * 12)
|
|
|
|
static DEFINE_SPINLOCK(hpet_lock);
|
|
DEFINE_PER_CPU(struct clock_event_device, hpet_clockevent_device);
|
|
|
|
static unsigned int smbus_read(int offset)
|
|
{
|
|
return *(volatile unsigned int *)(SMBUS_CFG_BASE + offset);
|
|
}
|
|
|
|
static void smbus_write(int offset, int data)
|
|
{
|
|
*(volatile unsigned int *)(SMBUS_CFG_BASE + offset) = data;
|
|
}
|
|
|
|
static void smbus_enable(int offset, int bit)
|
|
{
|
|
unsigned int cfg = smbus_read(offset);
|
|
|
|
cfg |= bit;
|
|
smbus_write(offset, cfg);
|
|
}
|
|
|
|
static int hpet_read(int offset)
|
|
{
|
|
return *(volatile unsigned int *)(HPET_MMIO_ADDR + offset);
|
|
}
|
|
|
|
static void hpet_write(int offset, int data)
|
|
{
|
|
*(volatile unsigned int *)(HPET_MMIO_ADDR + offset) = data;
|
|
}
|
|
|
|
static void hpet_start_counter(void)
|
|
{
|
|
unsigned int cfg = hpet_read(HPET_CFG);
|
|
|
|
cfg |= HPET_CFG_ENABLE;
|
|
hpet_write(HPET_CFG, cfg);
|
|
}
|
|
|
|
static void hpet_stop_counter(void)
|
|
{
|
|
unsigned int cfg = hpet_read(HPET_CFG);
|
|
|
|
cfg &= ~HPET_CFG_ENABLE;
|
|
hpet_write(HPET_CFG, cfg);
|
|
}
|
|
|
|
static void hpet_reset_counter(void)
|
|
{
|
|
hpet_write(HPET_COUNTER, 0);
|
|
hpet_write(HPET_COUNTER + 4, 0);
|
|
}
|
|
|
|
static void hpet_restart_counter(void)
|
|
{
|
|
hpet_stop_counter();
|
|
hpet_reset_counter();
|
|
hpet_start_counter();
|
|
}
|
|
|
|
static void hpet_enable_legacy_int(void)
|
|
{
|
|
/* Do nothing on Loongson-3 */
|
|
}
|
|
|
|
static int hpet_set_state_periodic(struct clock_event_device *evt)
|
|
{
|
|
int cfg;
|
|
|
|
spin_lock(&hpet_lock);
|
|
|
|
pr_info("set clock event to periodic mode!\n");
|
|
/* stop counter */
|
|
hpet_stop_counter();
|
|
|
|
/* enables the timer0 to generate a periodic interrupt */
|
|
cfg = hpet_read(HPET_T0_CFG);
|
|
cfg &= ~HPET_TN_LEVEL;
|
|
cfg |= HPET_TN_ENABLE | HPET_TN_PERIODIC | HPET_TN_SETVAL |
|
|
HPET_TN_32BIT;
|
|
hpet_write(HPET_T0_CFG, cfg);
|
|
|
|
/* set the comparator */
|
|
hpet_write(HPET_T0_CMP, HPET_COMPARE_VAL);
|
|
udelay(1);
|
|
hpet_write(HPET_T0_CMP, HPET_COMPARE_VAL);
|
|
|
|
/* start counter */
|
|
hpet_start_counter();
|
|
|
|
spin_unlock(&hpet_lock);
|
|
return 0;
|
|
}
|
|
|
|
static int hpet_set_state_shutdown(struct clock_event_device *evt)
|
|
{
|
|
int cfg;
|
|
|
|
spin_lock(&hpet_lock);
|
|
|
|
cfg = hpet_read(HPET_T0_CFG);
|
|
cfg &= ~HPET_TN_ENABLE;
|
|
hpet_write(HPET_T0_CFG, cfg);
|
|
|
|
spin_unlock(&hpet_lock);
|
|
return 0;
|
|
}
|
|
|
|
static int hpet_set_state_oneshot(struct clock_event_device *evt)
|
|
{
|
|
int cfg;
|
|
|
|
spin_lock(&hpet_lock);
|
|
|
|
pr_info("set clock event to one shot mode!\n");
|
|
cfg = hpet_read(HPET_T0_CFG);
|
|
/*
|
|
* set timer0 type
|
|
* 1 : periodic interrupt
|
|
* 0 : non-periodic(oneshot) interrupt
|
|
*/
|
|
cfg &= ~HPET_TN_PERIODIC;
|
|
cfg |= HPET_TN_ENABLE | HPET_TN_32BIT;
|
|
hpet_write(HPET_T0_CFG, cfg);
|
|
|
|
spin_unlock(&hpet_lock);
|
|
return 0;
|
|
}
|
|
|
|
static int hpet_tick_resume(struct clock_event_device *evt)
|
|
{
|
|
spin_lock(&hpet_lock);
|
|
hpet_enable_legacy_int();
|
|
spin_unlock(&hpet_lock);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int hpet_next_event(unsigned long delta,
|
|
struct clock_event_device *evt)
|
|
{
|
|
u32 cnt;
|
|
s32 res;
|
|
|
|
cnt = hpet_read(HPET_COUNTER);
|
|
cnt += (u32) delta;
|
|
hpet_write(HPET_T0_CMP, cnt);
|
|
|
|
res = (s32)(cnt - hpet_read(HPET_COUNTER));
|
|
|
|
return res < HPET_MIN_CYCLES ? -ETIME : 0;
|
|
}
|
|
|
|
static irqreturn_t hpet_irq_handler(int irq, void *data)
|
|
{
|
|
int is_irq;
|
|
struct clock_event_device *cd;
|
|
unsigned int cpu = smp_processor_id();
|
|
|
|
is_irq = hpet_read(HPET_STATUS);
|
|
if (is_irq & HPET_T0_IRS) {
|
|
/* clear the TIMER0 irq status register */
|
|
hpet_write(HPET_STATUS, HPET_T0_IRS);
|
|
cd = &per_cpu(hpet_clockevent_device, cpu);
|
|
cd->event_handler(cd);
|
|
return IRQ_HANDLED;
|
|
}
|
|
return IRQ_NONE;
|
|
}
|
|
|
|
static struct irqaction hpet_irq = {
|
|
.handler = hpet_irq_handler,
|
|
.flags = IRQF_NOBALANCING | IRQF_TIMER,
|
|
.name = "hpet",
|
|
};
|
|
|
|
/*
|
|
* hpet address assignation and irq setting should be done in bios.
|
|
* but pmon don't do this, we just setup here directly.
|
|
* The operation under is normal. unfortunately, hpet_setup process
|
|
* is before pci initialize.
|
|
*
|
|
* {
|
|
* struct pci_dev *pdev;
|
|
*
|
|
* pdev = pci_get_device(PCI_VENDOR_ID_ATI, PCI_DEVICE_ID_ATI_SBX00_SMBUS, NULL);
|
|
* pci_write_config_word(pdev, SMBUS_PCI_REGB4, HPET_ADDR);
|
|
*
|
|
* ...
|
|
* }
|
|
*/
|
|
static void hpet_setup(void)
|
|
{
|
|
/* set hpet base address */
|
|
smbus_write(SMBUS_PCI_REGB4, HPET_ADDR);
|
|
|
|
/* enable decoding of access to HPET MMIO*/
|
|
smbus_enable(SMBUS_PCI_REG40, (1 << 28));
|
|
|
|
/* HPET irq enable */
|
|
smbus_enable(SMBUS_PCI_REG64, (1 << 10));
|
|
|
|
hpet_enable_legacy_int();
|
|
}
|
|
|
|
void __init setup_hpet_timer(void)
|
|
{
|
|
unsigned int cpu = smp_processor_id();
|
|
struct clock_event_device *cd;
|
|
|
|
hpet_setup();
|
|
|
|
cd = &per_cpu(hpet_clockevent_device, cpu);
|
|
cd->name = "hpet";
|
|
cd->rating = 100;
|
|
cd->features = CLOCK_EVT_FEAT_PERIODIC | CLOCK_EVT_FEAT_ONESHOT;
|
|
cd->set_state_shutdown = hpet_set_state_shutdown;
|
|
cd->set_state_periodic = hpet_set_state_periodic;
|
|
cd->set_state_oneshot = hpet_set_state_oneshot;
|
|
cd->tick_resume = hpet_tick_resume;
|
|
cd->set_next_event = hpet_next_event;
|
|
cd->irq = HPET_T0_IRQ;
|
|
cd->cpumask = cpumask_of(cpu);
|
|
clockevent_set_clock(cd, HPET_FREQ);
|
|
cd->max_delta_ns = clockevent_delta2ns(0x7fffffff, cd);
|
|
cd->min_delta_ns = clockevent_delta2ns(HPET_MIN_PROG_DELTA, cd);
|
|
|
|
clockevents_register_device(cd);
|
|
setup_irq(HPET_T0_IRQ, &hpet_irq);
|
|
pr_info("hpet clock event device register\n");
|
|
}
|
|
|
|
static cycle_t hpet_read_counter(struct clocksource *cs)
|
|
{
|
|
return (cycle_t)hpet_read(HPET_COUNTER);
|
|
}
|
|
|
|
static void hpet_suspend(struct clocksource *cs)
|
|
{
|
|
}
|
|
|
|
static void hpet_resume(struct clocksource *cs)
|
|
{
|
|
hpet_setup();
|
|
hpet_restart_counter();
|
|
}
|
|
|
|
static struct clocksource csrc_hpet = {
|
|
.name = "hpet",
|
|
/* mips clocksource rating is less than 300, so hpet is better. */
|
|
.rating = 300,
|
|
.read = hpet_read_counter,
|
|
.mask = CLOCKSOURCE_MASK(32),
|
|
/* oneshot mode work normal with this flag */
|
|
.flags = CLOCK_SOURCE_IS_CONTINUOUS,
|
|
.suspend = hpet_suspend,
|
|
.resume = hpet_resume,
|
|
.mult = 0,
|
|
.shift = 10,
|
|
};
|
|
|
|
int __init init_hpet_clocksource(void)
|
|
{
|
|
csrc_hpet.mult = clocksource_hz2mult(HPET_FREQ, csrc_hpet.shift);
|
|
return clocksource_register_hz(&csrc_hpet, HPET_FREQ);
|
|
}
|
|
|
|
arch_initcall(init_hpet_clocksource);
|