969 lines
24 KiB
C
969 lines
24 KiB
C
/*
|
|
* intel TCO Watchdog Driver
|
|
*
|
|
* (c) Copyright 2006-2011 Wim Van Sebroeck <wim@iguana.be>.
|
|
*
|
|
* 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.
|
|
*
|
|
* Neither Wim Van Sebroeck nor Iguana vzw. admit liability nor
|
|
* provide warranty for any of this software. This material is
|
|
* provided "AS-IS" and at no charge.
|
|
*
|
|
* The TCO watchdog is implemented in the following I/O controller hubs:
|
|
* (See the intel documentation on http://developer.intel.com.)
|
|
* document number 290655-003, 290677-014: 82801AA (ICH), 82801AB (ICHO)
|
|
* document number 290687-002, 298242-027: 82801BA (ICH2)
|
|
* document number 290733-003, 290739-013: 82801CA (ICH3-S)
|
|
* document number 290716-001, 290718-007: 82801CAM (ICH3-M)
|
|
* document number 290744-001, 290745-025: 82801DB (ICH4)
|
|
* document number 252337-001, 252663-008: 82801DBM (ICH4-M)
|
|
* document number 273599-001, 273645-002: 82801E (C-ICH)
|
|
* document number 252516-001, 252517-028: 82801EB (ICH5), 82801ER (ICH5R)
|
|
* document number 300641-004, 300884-013: 6300ESB
|
|
* document number 301473-002, 301474-026: 82801F (ICH6)
|
|
* document number 313082-001, 313075-006: 631xESB, 632xESB
|
|
* document number 307013-003, 307014-024: 82801G (ICH7)
|
|
* document number 322896-001, 322897-001: NM10
|
|
* document number 313056-003, 313057-017: 82801H (ICH8)
|
|
* document number 316972-004, 316973-012: 82801I (ICH9)
|
|
* document number 319973-002, 319974-002: 82801J (ICH10)
|
|
* document number 322169-001, 322170-003: 5 Series, 3400 Series (PCH)
|
|
* document number 320066-003, 320257-008: EP80597 (IICH)
|
|
* document number 324645-001, 324646-001: Cougar Point (CPT)
|
|
* document number TBD : Patsburg (PBG)
|
|
* document number TBD : DH89xxCC
|
|
* document number TBD : Panther Point
|
|
* document number TBD : Lynx Point
|
|
*/
|
|
|
|
/*
|
|
* Includes, defines, variables, module parameters, ...
|
|
*/
|
|
|
|
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
|
|
|
|
/* Module and version information */
|
|
#define DRV_NAME "iTCO_wdt"
|
|
#define DRV_VERSION "1.07"
|
|
|
|
/* Includes */
|
|
#include <linux/module.h> /* For module specific items */
|
|
#include <linux/moduleparam.h> /* For new moduleparam's */
|
|
#include <linux/types.h> /* For standard types (like size_t) */
|
|
#include <linux/errno.h> /* For the -ENODEV/... values */
|
|
#include <linux/kernel.h> /* For printk/panic/... */
|
|
#include <linux/miscdevice.h> /* For MODULE_ALIAS_MISCDEV
|
|
(WATCHDOG_MINOR) */
|
|
#include <linux/watchdog.h> /* For the watchdog specific items */
|
|
#include <linux/init.h> /* For __init/__exit/... */
|
|
#include <linux/fs.h> /* For file operations */
|
|
#include <linux/platform_device.h> /* For platform_driver framework */
|
|
#include <linux/pci.h> /* For pci functions */
|
|
#include <linux/ioport.h> /* For io-port access */
|
|
#include <linux/spinlock.h> /* For spin_lock/spin_unlock/... */
|
|
#include <linux/uaccess.h> /* For copy_to_user/put_user/... */
|
|
#include <linux/io.h> /* For inb/outb/... */
|
|
#include <linux/debugfs.h>
|
|
#include <linux/reboot.h>
|
|
#include <linux/interrupt.h>
|
|
#include <linux/acpi.h>
|
|
#include <linux/nmi.h>
|
|
|
|
/* Address definitions for the TCO */
|
|
/* TCO base address */
|
|
#define TCOBASE (iTCO_wdt_private.ACPIBASE + 0x60)
|
|
/* SMI Control and Enable Register */
|
|
#define SMI_EN (iTCO_wdt_private.ACPIBASE + 0x30)
|
|
#define SMI_STS (iTCO_wdt_private.ACPIBASE + 0x34)
|
|
|
|
#define TCO_RLD (TCOBASE + 0x00) /* TCO Timer Reload and Curr. Value */
|
|
#define TCOv1_TMR (TCOBASE + 0x01) /* TCOv1 Timer Initial Value */
|
|
#define TCO_DAT_IN (TCOBASE + 0x02) /* TCO Data In Register */
|
|
#define TCO_DAT_OUT (TCOBASE + 0x03) /* TCO Data Out Register */
|
|
#define TCO1_STS (TCOBASE + 0x04) /* TCO1 Status Register */
|
|
#define TCO2_STS (TCOBASE + 0x06) /* TCO2 Status Register */
|
|
#define TCO1_CNT (TCOBASE + 0x08) /* TCO1 Control Register */
|
|
#define TCO2_CNT (TCOBASE + 0x0a) /* TCO2 Control Register */
|
|
#define TCOv2_TMR (TCOBASE + 0x12) /* TCOv2 Timer Initial Value */
|
|
|
|
#define TCO_POLICY_OFFSET 20
|
|
#define TCO_POLICY_MASK 0x3
|
|
#define TCO_POLICY_NORM 0x0
|
|
#define TCO_POLICY_HALT 0x1
|
|
#define TCO_POLICY_NO_LOAD 0x2
|
|
|
|
#define TCO_TIMEOUT_BIT (1 << 3)
|
|
#define SECOND_TO_STS_BIT (1 << 17)
|
|
#define TCO_STS_BIT (1 << 13)
|
|
#define EOS_BIT (1 << 1)
|
|
#define TCO_EN_BIT (1 << 13)
|
|
|
|
#define STRING_RESET_TYPE_MAX_LEN 12
|
|
#define STRING_COLD_OFF "COLD_OFF"
|
|
#define STRING_COLD_RESET "COLD_RESET"
|
|
|
|
static u32 pmc_base_address;
|
|
#define PMC_CFG (pmc_base_address + 0x8)
|
|
|
|
static struct dentry *iTCO_debugfs_dir;
|
|
|
|
/* internal variables */
|
|
static unsigned long is_active;
|
|
static char expect_release;
|
|
static struct { /* this is private data for the iTCO_wdt device */
|
|
/* TCO watchdog action */
|
|
unsigned int iTCO_wdt_action;
|
|
/* The device's ACPIBASE address (TCOBASE = ACPIBASE+0x60) */
|
|
unsigned long ACPIBASE;
|
|
/* the lock for io operations */
|
|
spinlock_t io_lock;
|
|
#ifdef CONFIG_DEBUG_FS
|
|
bool panic_reboot_notifier;
|
|
#endif
|
|
/* TCO enable bit */
|
|
bool enable;
|
|
} iTCO_wdt_private;
|
|
|
|
/* the watchdog reboot notifier */
|
|
static struct notifier_block reboot_notifier;
|
|
|
|
/* variable to bypass keep alive after reboot notifier call */
|
|
static bool bypass_keepalive;
|
|
|
|
/* module parameters */
|
|
#define WATCHDOG_HEARTBEAT 30 /* 30 sec default heartbeat */
|
|
static int heartbeat = WATCHDOG_HEARTBEAT; /* in seconds */
|
|
module_param(heartbeat, int, 0);
|
|
MODULE_PARM_DESC(heartbeat, "Watchdog timeout in seconds. "
|
|
"3..614, default="
|
|
__MODULE_STRING(WATCHDOG_HEARTBEAT) ")");
|
|
|
|
static bool nowayout = WATCHDOG_NOWAYOUT;
|
|
module_param(nowayout, bool, false);
|
|
MODULE_PARM_DESC(nowayout,
|
|
"Watchdog cannot be stopped once started (default="
|
|
__MODULE_STRING(WATCHDOG_NOWAYOUT) ")");
|
|
|
|
static void iTCO_wdt_set_NO_REBOOT_bit(void)
|
|
{
|
|
u32 val32;
|
|
void __iomem *pmc_cfg;
|
|
|
|
pmc_cfg = ioremap(PMC_CFG, 4);
|
|
if (!pmc_cfg) {
|
|
pr_err("cannot ioremap PCM_CFG register (@=0x%x)\n",
|
|
PMC_CFG);
|
|
return;
|
|
}
|
|
|
|
val32 = readl(pmc_cfg);
|
|
val32 |= 0x00000010;
|
|
writel(val32, pmc_cfg);
|
|
|
|
iounmap(pmc_cfg);
|
|
}
|
|
|
|
static int iTCO_wdt_unset_NO_REBOOT_bit(void)
|
|
{
|
|
int ret = 0;
|
|
u32 val32;
|
|
void __iomem *pmc_cfg;
|
|
|
|
pmc_cfg = ioremap(PMC_CFG, 4);
|
|
if (!pmc_cfg) {
|
|
pr_err("cannot ioremap PCM_CFG register (@=0x%x)\n",
|
|
PMC_CFG);
|
|
return -EIO;
|
|
}
|
|
|
|
val32 = readl(pmc_cfg);
|
|
val32 &= 0xffffffef;
|
|
writel(val32, pmc_cfg);
|
|
val32 = readl(pmc_cfg);
|
|
if (val32 & 0x00000010)
|
|
ret = -EIO;
|
|
|
|
iounmap(pmc_cfg);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int iTCO_wdt_start(void)
|
|
{
|
|
unsigned int val;
|
|
|
|
if (iTCO_wdt_private.enable == false)
|
|
return 0;
|
|
|
|
spin_lock(&iTCO_wdt_private.io_lock);
|
|
|
|
/* Force the timer to its reload value by writing to the TCO_RLD
|
|
register */
|
|
outw(0x01, TCO_RLD);
|
|
|
|
/* Bit 11: TCO Timer Halt -> 0 = The TCO timer is enabled to count */
|
|
val = inw(TCO1_CNT);
|
|
val &= 0xf7ff;
|
|
outw(val, TCO1_CNT);
|
|
val = inw(TCO1_CNT);
|
|
spin_unlock(&iTCO_wdt_private.io_lock);
|
|
|
|
if (val & 0x0800)
|
|
return -1;
|
|
return 0;
|
|
}
|
|
|
|
static int iTCO_wdt_stop(void)
|
|
{
|
|
unsigned int val;
|
|
|
|
spin_lock(&iTCO_wdt_private.io_lock);
|
|
|
|
/* Bit 11: TCO Timer Halt -> 1 = The TCO timer is disabled */
|
|
val = inw(TCO1_CNT);
|
|
val |= 0x0800;
|
|
outw(val, TCO1_CNT);
|
|
val = inw(TCO1_CNT);
|
|
|
|
spin_unlock(&iTCO_wdt_private.io_lock);
|
|
|
|
if ((val & 0x0800) == 0)
|
|
return -1;
|
|
return 0;
|
|
}
|
|
|
|
static int iTCO_wdt_keepalive(void)
|
|
{
|
|
pr_info("%s\n", __func__);
|
|
|
|
if (bypass_keepalive) {
|
|
pr_info("Reboot on going keep alive is bypassed\n");
|
|
return -EBUSY;
|
|
}
|
|
|
|
spin_lock(&iTCO_wdt_private.io_lock);
|
|
|
|
/* Reload the timer by writing to the TCO Timer Counter register */
|
|
outw(0x01, TCO_RLD);
|
|
|
|
spin_unlock(&iTCO_wdt_private.io_lock);
|
|
return 0;
|
|
}
|
|
|
|
static int iTCO_wdt_set_heartbeat(int t)
|
|
{
|
|
unsigned int val16;
|
|
|
|
/* For TCO v1 the timer counts down twice before rebooting */
|
|
/* from the specs: */
|
|
/* "Values of 0h-1h are ignored and should not be attempted" */
|
|
if (t < 0x02)
|
|
return -EINVAL;
|
|
|
|
/* Write new heartbeat to watchdog */
|
|
spin_lock(&iTCO_wdt_private.io_lock);
|
|
val16 = inw(TCOv2_TMR);
|
|
val16 &= 0xfc00;
|
|
val16 |= t;
|
|
outw(val16, TCOv2_TMR);
|
|
val16 = inw(TCOv2_TMR);
|
|
spin_unlock(&iTCO_wdt_private.io_lock);
|
|
|
|
if ((val16 & 0x3ff) != t)
|
|
return -EINVAL;
|
|
heartbeat = t;
|
|
return 0;
|
|
}
|
|
|
|
static void iTCO_wdt_last_kick(int last_delay)
|
|
{
|
|
/* Set new heart beat giving last_delay to kernel to shutdown cleanly */
|
|
iTCO_wdt_set_heartbeat(last_delay);
|
|
|
|
/* Do a last keep alive before bypassing to allow shutdown
|
|
to proceed with new timing */
|
|
iTCO_wdt_keepalive();
|
|
bypass_keepalive = true;
|
|
}
|
|
|
|
static int iTCO_wdt_get_timeleft(int *time_left)
|
|
{
|
|
unsigned int val16;
|
|
|
|
/* read the TCO Timer */
|
|
spin_lock(&iTCO_wdt_private.io_lock);
|
|
val16 = inw(TCO_RLD);
|
|
val16 &= 0x3ff;
|
|
spin_unlock(&iTCO_wdt_private.io_lock);
|
|
|
|
*time_left = val16;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* /dev/watchdog handling
|
|
*/
|
|
|
|
static int iTCO_wdt_open(struct inode *inode, struct file *file)
|
|
{
|
|
/* /dev/watchdog can only be opened once */
|
|
if (test_and_set_bit(0, &is_active))
|
|
return -EBUSY;
|
|
|
|
/*
|
|
* Reload and activate timer
|
|
*/
|
|
iTCO_wdt_start();
|
|
return nonseekable_open(inode, file);
|
|
}
|
|
|
|
static int iTCO_wdt_release(struct inode *inode, struct file *file)
|
|
{
|
|
/*
|
|
* Shut off the timer.
|
|
*/
|
|
if (expect_release == 42) {
|
|
iTCO_wdt_stop();
|
|
} else {
|
|
pr_crit("Unexpected close, not stopping watchdog!\n");
|
|
iTCO_wdt_keepalive();
|
|
}
|
|
clear_bit(0, &is_active);
|
|
expect_release = 0;
|
|
return 0;
|
|
}
|
|
|
|
static ssize_t iTCO_wdt_write(struct file *file, const char __user *data,
|
|
size_t len, loff_t *ppos)
|
|
{
|
|
/* See if we got the magic character 'V' and reload the timer */
|
|
if (len) {
|
|
if (!nowayout) {
|
|
size_t i;
|
|
|
|
/* note: just in case someone wrote the magic
|
|
character five months ago... */
|
|
expect_release = 0;
|
|
|
|
/* scan to see whether or not we got the
|
|
magic character */
|
|
for (i = 0; i != len; i++) {
|
|
char c;
|
|
if (get_user(c, data + i))
|
|
return -EFAULT;
|
|
if (c == 'V')
|
|
expect_release = 42;
|
|
}
|
|
}
|
|
|
|
/* someone wrote to us, we should reload the timer */
|
|
iTCO_wdt_keepalive();
|
|
}
|
|
return len;
|
|
}
|
|
|
|
static unsigned int iTCO_wdt_get_current_ospolicy(void)
|
|
{
|
|
unsigned int val;
|
|
|
|
spin_lock(&iTCO_wdt_private.io_lock);
|
|
|
|
val = inl(TCO1_CNT);
|
|
|
|
val &= (TCO_POLICY_MASK << TCO_POLICY_OFFSET);
|
|
val >>= TCO_POLICY_OFFSET;
|
|
|
|
spin_unlock(&iTCO_wdt_private.io_lock);
|
|
|
|
return val;
|
|
}
|
|
|
|
static void iTCO_wdt_set_reset_type(int reset_type)
|
|
{
|
|
int val;
|
|
|
|
spin_lock(&iTCO_wdt_private.io_lock);
|
|
|
|
iTCO_wdt_private.iTCO_wdt_action = reset_type;
|
|
|
|
val = inl(TCO1_CNT);
|
|
|
|
val &= ~(TCO_POLICY_MASK << TCO_POLICY_OFFSET);
|
|
val |= reset_type << TCO_POLICY_OFFSET;
|
|
|
|
outl(val, TCO1_CNT);
|
|
|
|
spin_unlock(&iTCO_wdt_private.io_lock);
|
|
}
|
|
|
|
#ifdef CONFIG_DEBUG_FS
|
|
|
|
static ssize_t iTCO_wdt_reset_type_write(struct file *file, const char __user *buff,
|
|
size_t count, loff_t *ppos)
|
|
{
|
|
char reset_type_str[STRING_RESET_TYPE_MAX_LEN];
|
|
unsigned long res;
|
|
|
|
if (count >= STRING_RESET_TYPE_MAX_LEN) {
|
|
pr_err("Invalid size %s %d/%d\n", reset_type_str, count,
|
|
STRING_RESET_TYPE_MAX_LEN);
|
|
return -EINVAL;
|
|
}
|
|
|
|
memset(reset_type_str, 0x00, STRING_RESET_TYPE_MAX_LEN);
|
|
|
|
res = copy_from_user((void *)reset_type_str,
|
|
(void __user *)buff,
|
|
(unsigned long)min((unsigned long)(count-1),
|
|
(unsigned long)(STRING_RESET_TYPE_MAX_LEN-1)));
|
|
|
|
if (res) {
|
|
pr_err("%s: copy from user failed\n", __func__);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (!strncmp(reset_type_str, STRING_COLD_OFF, STRING_RESET_TYPE_MAX_LEN)) {
|
|
iTCO_wdt_set_reset_type(TCO_POLICY_HALT);
|
|
} else if (!strncmp(reset_type_str, STRING_COLD_RESET,
|
|
STRING_RESET_TYPE_MAX_LEN)) {
|
|
iTCO_wdt_set_reset_type(TCO_POLICY_NORM);
|
|
} else {
|
|
pr_err("Reset type %s is unknown - Invalid value\n", reset_type_str);
|
|
return -EINVAL;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
static ssize_t iTCO_wdt_reset_type_read(struct file *file, char __user *buff,
|
|
size_t count, loff_t *ppos)
|
|
{
|
|
ssize_t len = 0;
|
|
unsigned long res;
|
|
|
|
if (*ppos > 0)
|
|
return 0;
|
|
|
|
switch(iTCO_wdt_private.iTCO_wdt_action) {
|
|
case TCO_POLICY_NORM:
|
|
len = ARRAY_SIZE(STRING_COLD_RESET);
|
|
res = copy_to_user(buff, STRING_COLD_RESET "\n", len + 1);
|
|
break;
|
|
case TCO_POLICY_HALT:
|
|
len = ARRAY_SIZE(STRING_COLD_OFF);
|
|
res = copy_to_user(buff, STRING_COLD_OFF "\n", len + 1);
|
|
break;
|
|
default:
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (res) {
|
|
pr_err("%s: copy to user failed\n", __func__);
|
|
return -EINVAL;
|
|
}
|
|
|
|
*ppos += len;
|
|
return len;
|
|
}
|
|
|
|
static const struct file_operations iTCO_wdt_reset_type_fops = {
|
|
.read = iTCO_wdt_reset_type_read,
|
|
.write = iTCO_wdt_reset_type_write,
|
|
};
|
|
|
|
#define STR_MAX_LEN 30
|
|
static ssize_t tl_read(struct file *file, char __user *buff,
|
|
size_t count, loff_t *ppos)
|
|
{
|
|
ssize_t len = 0;
|
|
unsigned long res;
|
|
unsigned char str[STR_MAX_LEN];
|
|
int timeleft;
|
|
|
|
if (*ppos > 0)
|
|
return 0;
|
|
|
|
if (iTCO_wdt_get_timeleft(&timeleft))
|
|
return -EINVAL;
|
|
|
|
sprintf(str, "%d\n", timeleft);
|
|
if (strlen(str) > count)
|
|
return -EFAULT;
|
|
|
|
res = copy_to_user(buff, str, strlen(str));
|
|
if (res) {
|
|
pr_err("%s: copy to user failed\n", __func__);
|
|
return -EINVAL;
|
|
}
|
|
|
|
len = strlen(str);
|
|
|
|
*ppos += len;
|
|
return len;
|
|
}
|
|
|
|
static const struct file_operations tl_fops = {
|
|
.read = tl_read,
|
|
};
|
|
|
|
|
|
static ssize_t iTCO_wdt_trigger_write(struct file *file, const char __user *buff,
|
|
size_t count, loff_t *ppos)
|
|
{
|
|
iTCO_wdt_last_kick(1);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const struct file_operations iTCO_wdt_trigger_fops = {
|
|
.write = iTCO_wdt_trigger_write,
|
|
};
|
|
|
|
#endif /* CONFIG_DEBUG_FS */
|
|
|
|
static long iTCO_wdt_ioctl(struct file *file, unsigned int cmd,
|
|
unsigned long arg)
|
|
{
|
|
int new_options, retval = -EINVAL;
|
|
int new_heartbeat;
|
|
void __user *argp = (void __user *)arg;
|
|
int __user *p = argp;
|
|
static const struct watchdog_info ident = {
|
|
.options = WDIOF_SETTIMEOUT |
|
|
WDIOF_KEEPALIVEPING |
|
|
WDIOF_MAGICCLOSE,
|
|
.firmware_version = 0,
|
|
.identity = DRV_NAME,
|
|
};
|
|
|
|
switch (cmd) {
|
|
case WDIOC_GETSUPPORT:
|
|
return copy_to_user(argp, &ident, sizeof(ident)) ? -EFAULT : 0;
|
|
case WDIOC_GETSTATUS:
|
|
case WDIOC_GETBOOTSTATUS:
|
|
return put_user(0, p);
|
|
|
|
case WDIOC_SETOPTIONS:
|
|
{
|
|
if (get_user(new_options, p))
|
|
return -EFAULT;
|
|
|
|
if (new_options & WDIOS_DISABLECARD) {
|
|
iTCO_wdt_stop();
|
|
retval = 0;
|
|
}
|
|
if (new_options & WDIOS_ENABLECARD) {
|
|
iTCO_wdt_keepalive();
|
|
iTCO_wdt_start();
|
|
retval = 0;
|
|
}
|
|
return retval;
|
|
}
|
|
case WDIOC_KEEPALIVE:
|
|
iTCO_wdt_keepalive();
|
|
return 0;
|
|
|
|
case WDIOC_SETTIMEOUT:
|
|
{
|
|
if (get_user(new_heartbeat, p))
|
|
return -EFAULT;
|
|
if (iTCO_wdt_set_heartbeat(new_heartbeat))
|
|
return -EINVAL;
|
|
iTCO_wdt_keepalive();
|
|
/* Fall */
|
|
}
|
|
case WDIOC_GETTIMEOUT:
|
|
return put_user(heartbeat, p);
|
|
case WDIOC_GETTIMELEFT:
|
|
{
|
|
int time_left;
|
|
if (iTCO_wdt_get_timeleft(&time_left))
|
|
return -EINVAL;
|
|
return put_user(time_left, p);
|
|
}
|
|
default:
|
|
return -ENOTTY;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Kernel Interfaces
|
|
*/
|
|
|
|
static const struct file_operations iTCO_wdt_fops = {
|
|
.owner = THIS_MODULE,
|
|
.llseek = no_llseek,
|
|
.write = iTCO_wdt_write,
|
|
.unlocked_ioctl = iTCO_wdt_ioctl,
|
|
.open = iTCO_wdt_open,
|
|
.release = iTCO_wdt_release,
|
|
};
|
|
|
|
static struct miscdevice iTCO_wdt_miscdev = {
|
|
.minor = WATCHDOG_MINOR,
|
|
.name = "watchdog",
|
|
.fops = &iTCO_wdt_fops,
|
|
};
|
|
|
|
/*
|
|
* Reboot notifier
|
|
*/
|
|
|
|
static int TCO_reboot_notifier(struct notifier_block *this,
|
|
unsigned long code,
|
|
void *another_unused)
|
|
{
|
|
if (code == SYS_HALT || code == SYS_POWER_OFF) {
|
|
iTCO_wdt_set_reset_type(TCO_POLICY_HALT);
|
|
}
|
|
|
|
iTCO_wdt_last_kick(5);
|
|
|
|
#ifdef CONFIG_DEBUG_FS
|
|
if (iTCO_wdt_private.panic_reboot_notifier) {
|
|
BUG();
|
|
}
|
|
#endif
|
|
|
|
return NOTIFY_DONE;
|
|
}
|
|
|
|
static irqreturn_t tco_irq_handler(int irq, void *arg)
|
|
{
|
|
pr_warn("[SHTDWN] %s, WATCHDOG TIMEOUT HANDLER!\n", __func__);
|
|
|
|
/* reduce the timeout to the minimum, but sufficient for tracing */
|
|
iTCO_wdt_set_heartbeat(15);
|
|
iTCO_wdt_keepalive();
|
|
|
|
trigger_all_cpu_backtrace();
|
|
|
|
/* Let the watchdog reset the board */
|
|
panic_timeout = 0;
|
|
panic("Kernel Watchdog");
|
|
|
|
/* This code should not be reached */
|
|
|
|
return IRQ_HANDLED;
|
|
}
|
|
|
|
|
|
static ssize_t shutdown_ongoing_store(struct device *dev,
|
|
struct device_attribute *attr, const char *buf, size_t size)
|
|
{
|
|
iTCO_wdt_set_reset_type(TCO_POLICY_HALT);
|
|
return size;
|
|
}
|
|
|
|
|
|
static ssize_t reboot_ongoing_store(struct device *dev,
|
|
struct device_attribute *attr,
|
|
const char *buf, size_t size)
|
|
{
|
|
iTCO_wdt_set_reset_type(TCO_POLICY_NORM);
|
|
return size;
|
|
}
|
|
|
|
/* Watchdog behavior depending on system phase */
|
|
static DEVICE_ATTR(shutdown_ongoing, S_IWUSR, NULL, shutdown_ongoing_store);
|
|
static DEVICE_ATTR(reboot_ongoing, S_IWUSR, NULL, reboot_ongoing_store);
|
|
|
|
static int create_watchdog_sysfs_files(void)
|
|
{
|
|
int ret;
|
|
|
|
ret = device_create_file(iTCO_wdt_miscdev.this_device,
|
|
&dev_attr_shutdown_ongoing);
|
|
if (ret) {
|
|
pr_warn("cant register dev file for shutdown_ongoing\n");
|
|
return ret;
|
|
}
|
|
|
|
ret = device_create_file(iTCO_wdt_miscdev.this_device,
|
|
&dev_attr_reboot_ongoing);
|
|
if (ret) {
|
|
pr_warn("cant register dev file for reboot_ongoing\n");
|
|
return ret;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int remove_watchdog_sysfs_files(void)
|
|
{
|
|
device_remove_file(iTCO_wdt_miscdev.this_device,
|
|
&dev_attr_shutdown_ongoing);
|
|
device_remove_file(iTCO_wdt_miscdev.this_device,
|
|
&dev_attr_reboot_ongoing);
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Init & exit routines
|
|
*/
|
|
|
|
static int iTCO_wdt_init(struct platform_device *pdev)
|
|
{
|
|
int ret;
|
|
u32 base_address;
|
|
unsigned long val32;
|
|
struct pci_dev *parent;
|
|
struct resource *irq;
|
|
|
|
if (!pdev->dev.parent || !dev_is_pci(pdev->dev.parent)) {
|
|
pr_err("Unqualified parent device.\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
parent = to_pci_dev(pdev->dev.parent);
|
|
|
|
/*
|
|
* Find the ACPI/PM base I/O address which is the base
|
|
* for the TCO registers (TCOBASE=ACPIBASE + 0x60)
|
|
* ACPIBASE is bits [15:7] from 0x40-0x43
|
|
*/
|
|
pci_read_config_dword(parent, 0x40, &base_address);
|
|
base_address &= 0x0000ff80;
|
|
if (base_address == 0x00000000) {
|
|
/* Something's wrong here, ACPIBASE has to be set */
|
|
pr_err("failed to get TCOBASE address, device disabled by hardware/BIOS\n");
|
|
return -ENODEV;
|
|
}
|
|
iTCO_wdt_private.ACPIBASE = base_address;
|
|
|
|
pci_read_config_dword(parent, 0x44, &pmc_base_address);
|
|
pmc_base_address &= 0xFFFFFE00;
|
|
|
|
/*
|
|
* Disable watchdog on command-line demand
|
|
*/
|
|
if (strstr(saved_command_line, "disable_kernel_watchdog=1")) {
|
|
pr_warn("disable_kernel_watchdog=1 watchdog will not be started\n");
|
|
iTCO_wdt_private.enable = false;
|
|
/* Set the NO_REBOOT bit to prevent later reboots */
|
|
iTCO_wdt_set_NO_REBOOT_bit();
|
|
/* Ensure Wdt is well stopped in case started by IAFW */
|
|
iTCO_wdt_stop();
|
|
} else {
|
|
iTCO_wdt_private.enable = true;
|
|
/* Check chipset's NO_REBOOT bit */
|
|
if (iTCO_wdt_unset_NO_REBOOT_bit()) {
|
|
pr_err("unable to reset NO_REBOOT flag, device disabled by hardware/BIOS\n");
|
|
ret = -ENODEV; /* Cannot reset NO_REBOOT bit */
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
/* The TCO logic uses the TCO_EN bit in the SMI_EN register */
|
|
if (!request_region(SMI_EN, 4, "iTCO_wdt")) {
|
|
pr_err("I/O address 0x%04lx already in use, device disabled\n",
|
|
SMI_EN);
|
|
ret = -EIO;
|
|
goto out;
|
|
}
|
|
if (!request_region(SMI_STS, 4, "iTCO_wdt")) {
|
|
pr_err("I/O address 0x%04lx already in use, device disabled\n",
|
|
SMI_STS);
|
|
ret = -EIO;
|
|
goto unreg_smi_sts;
|
|
}
|
|
|
|
/* The TCO I/O registers reside in a 32-byte range pointed to
|
|
by the TCOBASE value */
|
|
if (!request_region(TCOBASE, 0x20, "iTCO_wdt")) {
|
|
pr_err("I/O address 0x%04lx already in use, device disabled\n",
|
|
TCOBASE);
|
|
ret = -EIO;
|
|
goto unreg_smi_en;
|
|
}
|
|
|
|
pr_info("Found a TCO device (TCOBASE=0x%04lx)\n", TCOBASE);
|
|
|
|
/* Check that the heartbeat value is within it's range;
|
|
if not reset to the default */
|
|
if (iTCO_wdt_set_heartbeat(heartbeat)) {
|
|
iTCO_wdt_set_heartbeat(WATCHDOG_HEARTBEAT);
|
|
pr_info("timeout value out of range, using %d\n", heartbeat);
|
|
}
|
|
|
|
ret = misc_register(&iTCO_wdt_miscdev);
|
|
if (ret != 0) {
|
|
pr_err("cannot register miscdev on minor=%d (err=%d)\n",
|
|
WATCHDOG_MINOR, ret);
|
|
goto unreg_region;
|
|
}
|
|
|
|
pr_info("initialized. heartbeat=%d sec (nowayout=%d) policy=0x%x\n",
|
|
heartbeat, nowayout, iTCO_wdt_get_current_ospolicy());
|
|
|
|
/* Reset OS policy */
|
|
iTCO_wdt_set_reset_type(TCO_POLICY_NORM);
|
|
|
|
irq = platform_get_resource(pdev, IORESOURCE_IRQ, 0);
|
|
if (!irq) {
|
|
pr_err("No warning interrupt resource found\n");
|
|
goto misc_unreg;
|
|
}
|
|
|
|
ret = acpi_register_gsi(NULL, irq->start,
|
|
irq->flags & (IORESOURCE_IRQ_HIGHEDGE | IORESOURCE_IRQ_LOWEDGE)
|
|
? ACPI_EDGE_SENSITIVE : ACPI_LEVEL_SENSITIVE,
|
|
irq->flags & (IORESOURCE_IRQ_HIGHEDGE | IORESOURCE_IRQ_HIGHLEVEL)
|
|
? ACPI_ACTIVE_HIGH : ACPI_ACTIVE_LOW);
|
|
if (ret < 0) {
|
|
pr_err("failed to configure TCO warning IRQ %d\n", (int)irq->start);
|
|
goto misc_unreg;
|
|
}
|
|
|
|
ret = request_irq(irq->start, tco_irq_handler, 0, "tco_watchdog", NULL);
|
|
if (ret < 0) {
|
|
pr_err("failed to request TCO warning IRQ %d\n", (int)irq->start);
|
|
goto gsi_unreg;
|
|
}
|
|
|
|
/* Clear old TCO timeout status */
|
|
val32 = TCO_TIMEOUT_BIT | SECOND_TO_STS_BIT;
|
|
outl(val32, TCO1_STS);
|
|
/* Clear the SMI status */
|
|
outl(TCO_STS_BIT, SMI_STS);
|
|
|
|
/* Enable SMI for TCO */
|
|
val32 = inl(SMI_EN);
|
|
val32 |= TCO_EN_BIT;
|
|
outl(val32, SMI_EN);
|
|
/* then ensure that PMC is ready to handle next SMI */
|
|
val32 |= EOS_BIT;
|
|
outl(val32, SMI_EN);
|
|
|
|
reboot_notifier.notifier_call = TCO_reboot_notifier;
|
|
reboot_notifier.priority = 1;
|
|
ret = register_reboot_notifier(&reboot_notifier);
|
|
if (ret)
|
|
/* We continue as reboot notifier is not critical for
|
|
* watchdog */
|
|
pr_err("cannot register reboot notifier %d\n", ret);
|
|
|
|
return 0;
|
|
|
|
gsi_unreg:
|
|
acpi_unregister_gsi((int)(irq->start));
|
|
misc_unreg:
|
|
misc_deregister(&iTCO_wdt_miscdev);
|
|
unreg_region:
|
|
release_region(TCOBASE, 0x20);
|
|
unreg_smi_sts:
|
|
release_region(SMI_STS, 4);
|
|
unreg_smi_en:
|
|
release_region(SMI_EN, 4);
|
|
out:
|
|
iTCO_wdt_private.ACPIBASE = 0;
|
|
return ret;
|
|
}
|
|
|
|
static void iTCO_wdt_cleanup(void)
|
|
{
|
|
/* Stop the timer before we leave */
|
|
if (!nowayout)
|
|
iTCO_wdt_stop();
|
|
|
|
/* Deregister */
|
|
misc_deregister(&iTCO_wdt_miscdev);
|
|
release_region(TCOBASE, 0x20);
|
|
release_region(SMI_EN, 4);
|
|
release_region(SMI_STS, 4);
|
|
unregister_reboot_notifier(&reboot_notifier);
|
|
iTCO_wdt_private.ACPIBASE = 0;
|
|
#ifdef CONFIG_DEBUG_FS
|
|
debugfs_remove_recursive(iTCO_debugfs_dir);
|
|
#endif /* CONFIG_DEBUG_FS */
|
|
}
|
|
|
|
static int iTCO_wdt_probe(struct platform_device *pdev)
|
|
{
|
|
int ret = 0;
|
|
|
|
spin_lock_init(&iTCO_wdt_private.io_lock);
|
|
|
|
ret = iTCO_wdt_init(pdev);
|
|
if (ret)
|
|
return ret;
|
|
|
|
#ifdef CONFIG_DEBUG_FS
|
|
iTCO_debugfs_dir = debugfs_create_dir("iTCO", NULL);
|
|
debugfs_create_file("timeleft", S_IRUSR,
|
|
iTCO_debugfs_dir, NULL, &tl_fops);
|
|
debugfs_create_file("reset_type", S_IRUSR | S_IWUSR,
|
|
iTCO_debugfs_dir, NULL, &iTCO_wdt_reset_type_fops);
|
|
debugfs_create_file("trigger", S_IWUSR,
|
|
iTCO_debugfs_dir, NULL, &iTCO_wdt_trigger_fops);
|
|
debugfs_create_bool("panic_reboot_notifier", S_IRUSR | S_IWUSR,
|
|
iTCO_debugfs_dir,
|
|
(u32 *)&iTCO_wdt_private.panic_reboot_notifier);
|
|
#endif /* CONFIG_DEBUG_FS */
|
|
|
|
create_watchdog_sysfs_files();
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int iTCO_wdt_remove(struct platform_device *dev)
|
|
{
|
|
if (iTCO_wdt_private.ACPIBASE)
|
|
iTCO_wdt_cleanup();
|
|
|
|
remove_watchdog_sysfs_files();
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void iTCO_wdt_shutdown(struct platform_device *dev)
|
|
{
|
|
pr_info("Shutdown not stopping watchdog");
|
|
}
|
|
|
|
static int iTCO_wdt_suspend(struct platform_device *dev, pm_message_t state)
|
|
{
|
|
return iTCO_wdt_stop();
|
|
}
|
|
|
|
static int iTCO_wdt_resume(struct platform_device *dev)
|
|
{
|
|
return iTCO_wdt_start();
|
|
}
|
|
|
|
static struct platform_driver iTCO_wdt_driver = {
|
|
.probe = iTCO_wdt_probe,
|
|
.remove = (iTCO_wdt_remove),
|
|
.shutdown = iTCO_wdt_shutdown,
|
|
.driver = {
|
|
.owner = THIS_MODULE,
|
|
.name = DRV_NAME,
|
|
},
|
|
.suspend = iTCO_wdt_suspend,
|
|
.resume = iTCO_wdt_resume,
|
|
};
|
|
|
|
static int __init iTCO_wdt_init_module(void)
|
|
{
|
|
pr_info("Intel TCO WatchDog Timer Driver v%s\n", DRV_VERSION);
|
|
return platform_driver_register(&iTCO_wdt_driver);
|
|
}
|
|
|
|
static void __exit iTCO_wdt_cleanup_module(void)
|
|
{
|
|
platform_driver_unregister(&iTCO_wdt_driver);
|
|
pr_info("Intel TCO WatchDog Timer Driver unloaded\n");
|
|
}
|
|
|
|
module_init(iTCO_wdt_init_module);
|
|
module_exit(iTCO_wdt_cleanup_module);
|
|
|
|
MODULE_AUTHOR("Wim Van Sebroeck <wim@iguana.be>");
|
|
MODULE_DESCRIPTION("Intel TCO WatchDog Timer Driver");
|
|
MODULE_VERSION(DRV_VERSION);
|
|
MODULE_LICENSE("GPL");
|
|
MODULE_ALIAS_MISCDEV(WATCHDOG_MINOR);
|