| /* |
| * 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"); |