2019-05-27 09:55:01 +03:00
// SPDX-License-Identifier: GPL-2.0-or-later
2010-10-26 04:58:05 +04:00
/*
* nv_tco 0.01 : TCO timer driver for NV chipsets
*
* ( c ) Copyright 2005 Google Inc . , All Rights Reserved .
*
* Based off i8xx_tco . c :
* ( c ) Copyright 2000 kernel concepts < nils @ kernelconcepts . de > , All Rights
* Reserved .
2020-07-13 23:58:21 +03:00
* https : //www.kernelconcepts.de
2010-10-26 04:58:05 +04:00
*
* TCO timer driver for NV chipsets
* based on softdog . c by Alan Cox < alan @ redhat . com >
*/
/*
* Includes , defines , variables , module parameters , . . .
*/
2012-02-16 03:06:19 +04:00
# define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
2010-10-26 04:58:05 +04:00
# include <linux/module.h>
# include <linux/moduleparam.h>
# include <linux/types.h>
# include <linux/miscdevice.h>
# include <linux/watchdog.h>
# include <linux/init.h>
# include <linux/fs.h>
# include <linux/pci.h>
# include <linux/ioport.h>
# include <linux/jiffies.h>
# include <linux/platform_device.h>
# include <linux/uaccess.h>
# include <linux/io.h>
# include "nv_tco.h"
/* Module and version information */
# define TCO_VERSION "0.01"
# define TCO_MODULE_NAME "NV_TCO"
# define TCO_DRIVER_NAME TCO_MODULE_NAME ", v" TCO_VERSION
/* internal variables */
static unsigned int tcobase ;
static DEFINE_SPINLOCK ( tco_lock ) ; /* Guards the hardware */
static unsigned long timer_alive ;
static char tco_expect_close ;
static struct pci_dev * tco_pci ;
/* the watchdog platform device */
static struct platform_device * nv_tco_platform_device ;
/* module parameters */
# define WATCHDOG_HEARTBEAT 30 /* 30 sec default heartbeat (2<heartbeat<39) */
static int heartbeat = WATCHDOG_HEARTBEAT ; /* in seconds */
module_param ( heartbeat , int , 0 ) ;
MODULE_PARM_DESC ( heartbeat , " Watchdog heartbeat in seconds. (2<heartbeat<39, "
" default= " __MODULE_STRING ( WATCHDOG_HEARTBEAT ) " ) " ) ;
2012-03-05 19:51:11 +04:00
static bool nowayout = WATCHDOG_NOWAYOUT ;
module_param ( nowayout , bool , 0 ) ;
2010-10-26 04:58:05 +04:00
MODULE_PARM_DESC ( nowayout , " Watchdog cannot be stopped once started "
" (default= " __MODULE_STRING ( WATCHDOG_NOWAYOUT ) " ) " ) ;
/*
* Some TCO specific functions
*/
static inline unsigned char seconds_to_ticks ( int seconds )
{
/* the internal timer is stored as ticks which decrement
* every 0.6 seconds */
return ( seconds * 10 ) / 6 ;
}
static void tco_timer_start ( void )
{
u32 val ;
unsigned long flags ;
spin_lock_irqsave ( & tco_lock , flags ) ;
val = inl ( TCO_CNT ( tcobase ) ) ;
val & = ~ TCO_CNT_TCOHALT ;
outl ( val , TCO_CNT ( tcobase ) ) ;
spin_unlock_irqrestore ( & tco_lock , flags ) ;
}
static void tco_timer_stop ( void )
{
u32 val ;
unsigned long flags ;
spin_lock_irqsave ( & tco_lock , flags ) ;
val = inl ( TCO_CNT ( tcobase ) ) ;
val | = TCO_CNT_TCOHALT ;
outl ( val , TCO_CNT ( tcobase ) ) ;
spin_unlock_irqrestore ( & tco_lock , flags ) ;
}
static void tco_timer_keepalive ( void )
{
unsigned long flags ;
spin_lock_irqsave ( & tco_lock , flags ) ;
outb ( 0x01 , TCO_RLD ( tcobase ) ) ;
spin_unlock_irqrestore ( & tco_lock , flags ) ;
}
static int tco_timer_set_heartbeat ( int t )
{
int ret = 0 ;
unsigned char tmrval ;
unsigned long flags ;
u8 val ;
/*
* note seconds_to_ticks ( t ) > t , so if t > 0x3f , so is
* tmrval = seconds_to_ticks ( t ) . Check that the count in seconds isn ' t
* out of range on it ' s own ( to avoid overflow in tmrval ) .
*/
if ( t < 0 | | t > 0x3f )
return - EINVAL ;
tmrval = seconds_to_ticks ( t ) ;
/* "Values of 0h-3h are ignored and should not be attempted" */
if ( tmrval > 0x3f | | tmrval < 0x04 )
return - EINVAL ;
/* Write new heartbeat to watchdog */
spin_lock_irqsave ( & tco_lock , flags ) ;
val = inb ( TCO_TMR ( tcobase ) ) ;
val & = 0xc0 ;
val | = tmrval ;
outb ( val , TCO_TMR ( tcobase ) ) ;
val = inb ( TCO_TMR ( tcobase ) ) ;
if ( ( val & 0x3f ) ! = tmrval )
ret = - EINVAL ;
spin_unlock_irqrestore ( & tco_lock , flags ) ;
if ( ret )
return ret ;
heartbeat = t ;
return 0 ;
}
/*
* / dev / watchdog handling
*/
static int nv_tco_open ( struct inode * inode , struct file * file )
{
/* /dev/watchdog can only be opened once */
if ( test_and_set_bit ( 0 , & timer_alive ) )
return - EBUSY ;
/* Reload and activate timer */
tco_timer_keepalive ( ) ;
tco_timer_start ( ) ;
2019-03-26 23:51:19 +03:00
return stream_open ( inode , file ) ;
2010-10-26 04:58:05 +04:00
}
static int nv_tco_release ( struct inode * inode , struct file * file )
{
/* Shut off the timer */
if ( tco_expect_close = = 42 ) {
tco_timer_stop ( ) ;
} else {
2012-02-16 03:06:19 +04:00
pr_crit ( " Unexpected close, not stopping watchdog! \n " ) ;
2010-10-26 04:58:05 +04:00
tco_timer_keepalive ( ) ;
}
clear_bit ( 0 , & timer_alive ) ;
tco_expect_close = 0 ;
return 0 ;
}
static ssize_t nv_tco_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 . . .
*/
tco_expect_close = 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 ' )
tco_expect_close = 42 ;
}
}
/* someone wrote to us, we should reload the timer */
tco_timer_keepalive ( ) ;
}
return len ;
}
static long nv_tco_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 = TCO_MODULE_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 ) {
tco_timer_stop ( ) ;
retval = 0 ;
}
if ( new_options & WDIOS_ENABLECARD ) {
tco_timer_keepalive ( ) ;
tco_timer_start ( ) ;
retval = 0 ;
}
return retval ;
case WDIOC_KEEPALIVE :
tco_timer_keepalive ( ) ;
return 0 ;
case WDIOC_SETTIMEOUT :
if ( get_user ( new_heartbeat , p ) )
return - EFAULT ;
if ( tco_timer_set_heartbeat ( new_heartbeat ) )
return - EINVAL ;
tco_timer_keepalive ( ) ;
2020-07-07 20:11:21 +03:00
fallthrough ;
2010-10-26 04:58:05 +04:00
case WDIOC_GETTIMEOUT :
return put_user ( heartbeat , p ) ;
default :
return - ENOTTY ;
}
}
/*
* Kernel Interfaces
*/
static const struct file_operations nv_tco_fops = {
. owner = THIS_MODULE ,
. llseek = no_llseek ,
. write = nv_tco_write ,
. unlocked_ioctl = nv_tco_ioctl ,
2019-06-03 15:23:09 +03:00
. compat_ioctl = compat_ptr_ioctl ,
2010-10-26 04:58:05 +04:00
. open = nv_tco_open ,
. release = nv_tco_release ,
} ;
static struct miscdevice nv_tco_miscdev = {
. minor = WATCHDOG_MINOR ,
. name = " watchdog " ,
. fops = & nv_tco_fops ,
} ;
/*
* Data for PCI driver interface
*
* This data only exists for exporting the supported
* PCI ids via MODULE_DEVICE_TABLE . We do not actually
* register a pci_driver , because someone else might one day
* want to register another driver on the same PCI id .
*/
2013-12-03 03:30:22 +04:00
static const struct pci_device_id tco_pci_tbl [ ] = {
2010-10-26 04:58:05 +04:00
{ PCI_VENDOR_ID_NVIDIA , PCI_DEVICE_ID_NVIDIA_NFORCE_MCP51_SMBUS ,
PCI_ANY_ID , PCI_ANY_ID , } ,
{ PCI_VENDOR_ID_NVIDIA , PCI_DEVICE_ID_NVIDIA_NFORCE_MCP55_SMBUS ,
PCI_ANY_ID , PCI_ANY_ID , } ,
2016-07-17 19:34:24 +03:00
{ PCI_VENDOR_ID_NVIDIA , PCI_DEVICE_ID_NVIDIA_NFORCE_MCP78S_SMBUS ,
PCI_ANY_ID , PCI_ANY_ID , } ,
2015-07-27 19:03:30 +03:00
{ PCI_VENDOR_ID_NVIDIA , PCI_DEVICE_ID_NVIDIA_NFORCE_MCP79_SMBUS ,
PCI_ANY_ID , PCI_ANY_ID , } ,
2010-10-26 04:58:05 +04:00
{ 0 , } , /* End of list */
} ;
MODULE_DEVICE_TABLE ( pci , tco_pci_tbl ) ;
/*
* Init & exit routines
*/
2012-11-19 22:21:41 +04:00
static unsigned char nv_tco_getdevice ( void )
2010-10-26 04:58:05 +04:00
{
struct pci_dev * dev = NULL ;
u32 val ;
/* Find the PCI device */
for_each_pci_dev ( dev ) {
if ( pci_match_id ( tco_pci_tbl , dev ) ! = NULL ) {
tco_pci = dev ;
break ;
}
}
if ( ! tco_pci )
return 0 ;
/* Find the base io port */
pci_read_config_dword ( tco_pci , 0x64 , & val ) ;
val & = 0xffff ;
if ( val = = 0x0001 | | val = = 0x0000 ) {
/* Something is wrong here, bar isn't setup */
2012-02-16 03:06:19 +04:00
pr_err ( " failed to get tcobase address \n " ) ;
2010-10-26 04:58:05 +04:00
return 0 ;
}
val & = 0xff00 ;
tcobase = val + 0x40 ;
if ( ! request_region ( tcobase , 0x10 , " NV TCO " ) ) {
2012-02-16 03:06:19 +04:00
pr_err ( " I/O address 0x%04x already in use \n " , tcobase ) ;
2010-10-26 04:58:05 +04:00
return 0 ;
}
/* Set a reasonable heartbeat before we stop the timer */
tco_timer_set_heartbeat ( 30 ) ;
/*
* Stop the TCO before we change anything so we don ' t race with
* a zeroed timer .
*/
tco_timer_keepalive ( ) ;
tco_timer_stop ( ) ;
/* Disable SMI caused by TCO */
if ( ! request_region ( MCP51_SMI_EN ( tcobase ) , 4 , " NV TCO " ) ) {
2012-02-16 03:06:19 +04:00
pr_err ( " I/O address 0x%04x already in use \n " ,
2010-10-26 04:58:05 +04:00
MCP51_SMI_EN ( tcobase ) ) ;
goto out ;
}
val = inl ( MCP51_SMI_EN ( tcobase ) ) ;
val & = ~ MCP51_SMI_EN_TCO ;
outl ( val , MCP51_SMI_EN ( tcobase ) ) ;
val = inl ( MCP51_SMI_EN ( tcobase ) ) ;
release_region ( MCP51_SMI_EN ( tcobase ) , 4 ) ;
if ( val & MCP51_SMI_EN_TCO ) {
2012-02-16 03:06:19 +04:00
pr_err ( " Could not disable SMI caused by TCO \n " ) ;
2010-10-26 04:58:05 +04:00
goto out ;
}
/* Check chipset's NO_REBOOT bit */
pci_read_config_dword ( tco_pci , MCP51_SMBUS_SETUP_B , & val ) ;
val | = MCP51_SMBUS_SETUP_B_TCO_REBOOT ;
pci_write_config_dword ( tco_pci , MCP51_SMBUS_SETUP_B , val ) ;
pci_read_config_dword ( tco_pci , MCP51_SMBUS_SETUP_B , & val ) ;
if ( ! ( val & MCP51_SMBUS_SETUP_B_TCO_REBOOT ) ) {
2012-02-16 03:06:19 +04:00
pr_err ( " failed to reset NO_REBOOT flag, reboot disabled by hardware \n " ) ;
2010-10-26 04:58:05 +04:00
goto out ;
}
return 1 ;
out :
release_region ( tcobase , 0x10 ) ;
return 0 ;
}
2012-11-19 22:21:41 +04:00
static int nv_tco_init ( struct platform_device * dev )
2010-10-26 04:58:05 +04:00
{
int ret ;
/* Check whether or not the hardware watchdog is there */
if ( ! nv_tco_getdevice ( ) )
return - ENODEV ;
/* Check to see if last reboot was due to watchdog timeout */
2012-02-16 03:06:19 +04:00
pr_info ( " Watchdog reboot %sdetected \n " ,
inl ( TCO_STS ( tcobase ) ) & TCO_STS_TCO2TO_STS ? " " : " not " ) ;
2010-10-26 04:58:05 +04:00
/* Clear out the old status */
outl ( TCO_STS_RESET , TCO_STS ( tcobase ) ) ;
/*
* Check that the heartbeat value is within it ' s range .
* If not , reset to the default .
*/
if ( tco_timer_set_heartbeat ( heartbeat ) ) {
heartbeat = WATCHDOG_HEARTBEAT ;
tco_timer_set_heartbeat ( heartbeat ) ;
2012-02-16 03:06:19 +04:00
pr_info ( " heartbeat value must be 2<heartbeat<39, using %d \n " ,
heartbeat ) ;
2010-10-26 04:58:05 +04:00
}
ret = misc_register ( & nv_tco_miscdev ) ;
if ( ret ! = 0 ) {
2012-02-16 03:06:19 +04:00
pr_err ( " cannot register miscdev on minor=%d (err=%d) \n " ,
WATCHDOG_MINOR , ret ) ;
2010-10-26 04:58:05 +04:00
goto unreg_region ;
}
clear_bit ( 0 , & timer_alive ) ;
tco_timer_stop ( ) ;
2012-02-16 03:06:19 +04:00
pr_info ( " initialized (0x%04x). heartbeat=%d sec (nowayout=%d) \n " ,
tcobase , heartbeat , nowayout ) ;
2010-10-26 04:58:05 +04:00
return 0 ;
unreg_region :
release_region ( tcobase , 0x10 ) ;
return ret ;
}
2012-11-19 22:26:24 +04:00
static void nv_tco_cleanup ( void )
2010-10-26 04:58:05 +04:00
{
u32 val ;
/* Stop the timer before we leave */
if ( ! nowayout )
tco_timer_stop ( ) ;
/* Set the NO_REBOOT bit to prevent later reboots, just for sure */
pci_read_config_dword ( tco_pci , MCP51_SMBUS_SETUP_B , & val ) ;
val & = ~ MCP51_SMBUS_SETUP_B_TCO_REBOOT ;
pci_write_config_dword ( tco_pci , MCP51_SMBUS_SETUP_B , val ) ;
pci_read_config_dword ( tco_pci , MCP51_SMBUS_SETUP_B , & val ) ;
if ( val & MCP51_SMBUS_SETUP_B_TCO_REBOOT ) {
2012-02-16 03:06:19 +04:00
pr_crit ( " Couldn't unset REBOOT bit. Machine may soon reset \n " ) ;
2010-10-26 04:58:05 +04:00
}
/* Deregister */
misc_deregister ( & nv_tco_miscdev ) ;
release_region ( tcobase , 0x10 ) ;
}
2012-11-19 22:26:24 +04:00
static int nv_tco_remove ( struct platform_device * dev )
2010-10-26 04:58:05 +04:00
{
if ( tcobase )
nv_tco_cleanup ( ) ;
return 0 ;
}
static void nv_tco_shutdown ( struct platform_device * dev )
{
2011-07-30 18:59:12 +04:00
u32 val ;
2010-10-26 04:58:05 +04:00
tco_timer_stop ( ) ;
2011-07-30 18:59:12 +04:00
/* Some BIOSes fail the POST (once) if the NO_REBOOT flag is not
* unset during shutdown . */
pci_read_config_dword ( tco_pci , MCP51_SMBUS_SETUP_B , & val ) ;
val & = ~ MCP51_SMBUS_SETUP_B_TCO_REBOOT ;
pci_write_config_dword ( tco_pci , MCP51_SMBUS_SETUP_B , val ) ;
2010-10-26 04:58:05 +04:00
}
static struct platform_driver nv_tco_driver = {
. probe = nv_tco_init ,
2012-11-19 22:21:12 +04:00
. remove = nv_tco_remove ,
2010-10-26 04:58:05 +04:00
. shutdown = nv_tco_shutdown ,
. driver = {
. name = TCO_MODULE_NAME ,
} ,
} ;
static int __init nv_tco_init_module ( void )
{
int err ;
2012-02-16 03:06:19 +04:00
pr_info ( " NV TCO WatchDog Timer Driver v%s \n " , TCO_VERSION ) ;
2010-10-26 04:58:05 +04:00
err = platform_driver_register ( & nv_tco_driver ) ;
if ( err )
return err ;
nv_tco_platform_device = platform_device_register_simple (
TCO_MODULE_NAME , - 1 , NULL , 0 ) ;
if ( IS_ERR ( nv_tco_platform_device ) ) {
err = PTR_ERR ( nv_tco_platform_device ) ;
goto unreg_platform_driver ;
}
return 0 ;
unreg_platform_driver :
platform_driver_unregister ( & nv_tco_driver ) ;
return err ;
}
static void __exit nv_tco_cleanup_module ( void )
{
platform_device_unregister ( nv_tco_platform_device ) ;
platform_driver_unregister ( & nv_tco_driver ) ;
2012-02-16 03:06:19 +04:00
pr_info ( " NV TCO Watchdog Module Unloaded \n " ) ;
2010-10-26 04:58:05 +04:00
}
module_init ( nv_tco_init_module ) ;
module_exit ( nv_tco_cleanup_module ) ;
MODULE_AUTHOR ( " Mike Waychison " ) ;
MODULE_DESCRIPTION ( " TCO timer driver for NV chipsets " ) ;
MODULE_LICENSE ( " GPL " ) ;