blob: 32e3e1d55ef362f6545250a122315fa8fe3fdfb7 [file] [log] [blame]
/*
* Intel Management Engine Interface (Intel MEI) Linux driver
* Copyright (c) 2015, Intel Corporation.
*
* This program is free software; you can redistribute it and/or modify it
* under the terms and conditions of the GNU General Public License,
* version 2, as published by the Free Software Foundation.
*
* This program is distributed in the hope 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.
*/
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/interrupt.h>
#include <linux/watchdog.h>
#include <linux/uuid.h>
#include <linux/mei_cl_bus.h>
/*
* iAMT Watchdog Device
*/
#define INTEL_AMT_WATCHDOG_ID "iamt_wdt"
#define MEI_WDT_DEFAULT_TIMEOUT 120 /* seconds */
#define MEI_WDT_MIN_TIMEOUT 120 /* seconds */
#define MEI_WDT_MAX_TIMEOUT 65535 /* seconds */
/* Commands */
#define MEI_MANAGEMENT_CONTROL 0x02
/* MEI Management Control version number */
#define MEI_MC_VERSION_NUMBER 0x10
/* Sub Commands */
#define MEI_MC_START_WD_TIMER_REQ 0x13
#define MEI_MC_STOP_WD_TIMER_REQ 0x14
/**
* enum mei_wdt_state - internal watchdog state
*
* @MEI_WDT_IDLE: wd is idle and not opened
* @MEI_WDT_START: wd was opened, start was called
* @MEI_WDT_RUNNING: wd is expecting keep alive pings
* @MEI_WDT_STOPPING: wd is stopping and will move to IDLE
*/
enum mei_wdt_state {
MEI_WDT_IDLE,
MEI_WDT_START,
MEI_WDT_RUNNING,
MEI_WDT_STOPPING,
};
/**
* struct mei_wdt - mei watchdog driver
* @wdd: watchdog device
*
* @cldev: mei watchdog client device
* @state: watchdog internal state
* @timeout: watchdog current timeout
*/
struct mei_wdt {
struct watchdog_device wdd;
struct mei_cl_device *cldev;
enum mei_wdt_state state;
u16 timeout;
};
/*
* struct mei_mc_hdr - Management Control Command Header
*
* @command: Management Control (0x2)
* @bytecount: Number of bytes in the message beyond this byte
* @subcommand: Management Control Subcommand
* @versionnumber: Management Control Version (0x10)
*/
struct mei_mc_hdr {
u8 command;
u8 bytecount;
u8 subcommand;
u8 versionnumber;
};
/**
* struct mei_wdt_start_request watchdog start/ping
*
* @hdr: Management Control Command Header
* @timeout: timeout value
* @reserved: reserved (legacy)
*/
struct mei_wdt_start_request {
struct mei_mc_hdr hdr;
u16 timeout;
u8 reserved[17];
} __packed;
/**
* struct mei_wdt_stop_request - watchdog stop
*
* @hdr: Management Control Command Header
*/
struct mei_wdt_stop_request {
struct mei_mc_hdr hdr;
} __packed;
/**
* mei_wdt_ping - send wd start/ping command
*
* @wdt: mei watchdog device
*
* Return: 0 on success,
* negative errno code on failure
*/
static int mei_wdt_ping(struct mei_wdt *wdt)
{
struct mei_wdt_start_request req;
const size_t req_len = sizeof(req);
int ret;
memset(&req, 0, req_len);
req.hdr.command = MEI_MANAGEMENT_CONTROL;
req.hdr.bytecount = req_len - offsetof(struct mei_mc_hdr, subcommand);
req.hdr.subcommand = MEI_MC_START_WD_TIMER_REQ;
req.hdr.versionnumber = MEI_MC_VERSION_NUMBER;
req.timeout = wdt->timeout;
ret = mei_cldev_send(wdt->cldev, (u8 *)&req, req_len);
if (ret < 0)
return ret;
return 0;
}
/**
* mei_wdt_stop - send wd stop command
*
* @wdt: mei watchdog device
*
* Return: 0 on success,
* negative errno code on failure
*/
static int mei_wdt_stop(struct mei_wdt *wdt)
{
struct mei_wdt_stop_request req;
const size_t req_len = sizeof(req);
int ret;
memset(&req, 0, req_len);
req.hdr.command = MEI_MANAGEMENT_CONTROL;
req.hdr.bytecount = req_len - offsetof(struct mei_mc_hdr, subcommand);
req.hdr.subcommand = MEI_MC_STOP_WD_TIMER_REQ;
req.hdr.versionnumber = MEI_MC_VERSION_NUMBER;
ret = mei_cldev_send(wdt->cldev, (u8 *)&req, req_len);
if (ret < 0)
return ret;
return 0;
}
/**
* mei_wdt_ops_start - wd start command from the watchdog core.
*
* @wdd: watchdog device
*
* Return: 0 on success or -ENODEV;
*/
static int mei_wdt_ops_start(struct watchdog_device *wdd)
{
struct mei_wdt *wdt = watchdog_get_drvdata(wdd);
wdt->state = MEI_WDT_START;
wdd->timeout = wdt->timeout;
return 0;
}
/**
* mei_wdt_ops_stop - wd stop command from the watchdog core.
*
* @wdd: watchdog device
*
* Return: 0 if success, negative errno code for failure
*/
static int mei_wdt_ops_stop(struct watchdog_device *wdd)
{
struct mei_wdt *wdt = watchdog_get_drvdata(wdd);
int ret;
if (wdt->state != MEI_WDT_RUNNING)
return 0;
wdt->state = MEI_WDT_STOPPING;
ret = mei_wdt_stop(wdt);
if (ret)
return ret;
wdt->state = MEI_WDT_IDLE;
return 0;
}
/**
* mei_wdt_ops_ping - wd ping command from the watchdog core.
*
* @wdd: watchdog device
*
* Return: 0 if success, negative errno code on failure
*/
static int mei_wdt_ops_ping(struct watchdog_device *wdd)
{
struct mei_wdt *wdt = watchdog_get_drvdata(wdd);
int ret;
if (wdt->state != MEI_WDT_START && wdt->state != MEI_WDT_RUNNING)
return 0;
ret = mei_wdt_ping(wdt);
if (ret)
return ret;
wdt->state = MEI_WDT_RUNNING;
return 0;
}
/**
* mei_wdt_ops_set_timeout - wd set timeout command from the watchdog core.
*
* @wdd: watchdog device
* @timeout: timeout value to set
*
* Return: 0 if success, negative errno code for failure
*/
static int mei_wdt_ops_set_timeout(struct watchdog_device *wdd,
unsigned int timeout)
{
struct mei_wdt *wdt = watchdog_get_drvdata(wdd);
/* valid value is already checked by the caller */
wdt->timeout = timeout;
wdd->timeout = timeout;
return 0;
}
static const struct watchdog_ops wd_ops = {
.owner = THIS_MODULE,
.start = mei_wdt_ops_start,
.stop = mei_wdt_ops_stop,
.ping = mei_wdt_ops_ping,
.set_timeout = mei_wdt_ops_set_timeout,
};
/* not const as the firmware_version field need to be retrieved */
static struct watchdog_info wd_info = {
.identity = INTEL_AMT_WATCHDOG_ID,
.options = WDIOF_KEEPALIVEPING |
WDIOF_SETTIMEOUT |
WDIOF_ALARMONLY,
};
/**
* mei_wdt_unregister - unregister from the watchdog subsystem
*
* @wdt: mei watchdog device
*/
static void mei_wdt_unregister(struct mei_wdt *wdt)
{
watchdog_unregister_device(&wdt->wdd);
watchdog_set_drvdata(&wdt->wdd, NULL);
}
/**
* mei_wdt_register - register with the watchdog subsystem
*
* @wdt: mei watchdog device
*
* Return: 0 if success, negative errno code for failure
*/
static int mei_wdt_register(struct mei_wdt *wdt)
{
struct device *dev;
int ret;
if (!wdt || !wdt->cldev)
return -EINVAL;
dev = &wdt->cldev->dev;
wdt->wdd.info = &wd_info;
wdt->wdd.ops = &wd_ops;
wdt->wdd.parent = dev;
wdt->wdd.timeout = MEI_WDT_DEFAULT_TIMEOUT;
wdt->wdd.min_timeout = MEI_WDT_MIN_TIMEOUT;
wdt->wdd.max_timeout = MEI_WDT_MAX_TIMEOUT;
watchdog_set_drvdata(&wdt->wdd, wdt);
ret = watchdog_register_device(&wdt->wdd);
if (ret) {
dev_err(dev, "unable to register watchdog device = %d.\n", ret);
watchdog_set_drvdata(&wdt->wdd, NULL);
}
return ret;
}
static int mei_wdt_probe(struct mei_cl_device *cldev,
const struct mei_cl_device_id *id)
{
struct mei_wdt *wdt;
int ret;
wdt = kzalloc(sizeof(struct mei_wdt), GFP_KERNEL);
if (!wdt)
return -ENOMEM;
wdt->timeout = MEI_WDT_DEFAULT_TIMEOUT;
wdt->state = MEI_WDT_IDLE;
wdt->cldev = cldev;
mei_cldev_set_drvdata(cldev, wdt);
ret = mei_cldev_enable(cldev);
if (ret < 0) {
dev_err(&cldev->dev, "Could not enable cl device\n");
goto err_out;
}
wd_info.firmware_version = mei_cldev_ver(cldev);
ret = mei_wdt_register(wdt);
if (ret)
goto err_disable;
return 0;
err_disable:
mei_cldev_disable(cldev);
err_out:
kfree(wdt);
return ret;
}
static int mei_wdt_remove(struct mei_cl_device *cldev)
{
struct mei_wdt *wdt = mei_cldev_get_drvdata(cldev);
mei_wdt_unregister(wdt);
mei_cldev_disable(cldev);
kfree(wdt);
return 0;
}
#define MEI_UUID_WD UUID_LE(0x05B79A6F, 0x4628, 0x4D7F, \
0x89, 0x9D, 0xA9, 0x15, 0x14, 0xCB, 0x32, 0xAB)
static struct mei_cl_device_id mei_wdt_tbl[] = {
{ .uuid = MEI_UUID_WD, .version = 0x1},
/* required last entry */
{ }
};
MODULE_DEVICE_TABLE(mei, mei_wdt_tbl);
static struct mei_cl_driver mei_wdt_driver = {
.id_table = mei_wdt_tbl,
.name = KBUILD_MODNAME,
.probe = mei_wdt_probe,
.remove = mei_wdt_remove,
};
static int __init mei_wdt_init(void)
{
int ret;
ret = mei_cldev_driver_register(&mei_wdt_driver);
if (ret) {
pr_err(KBUILD_MODNAME ": module registration failed\n");
return ret;
}
return 0;
}
static void __exit mei_wdt_exit(void)
{
mei_cldev_driver_unregister(&mei_wdt_driver);
}
module_init(mei_wdt_init);
module_exit(mei_wdt_exit);
MODULE_AUTHOR("Intel Corporation");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Device driver for Intel MEI iAMT watchdog");