diff --git a/drivers/watchdog/mei_wdt.c b/drivers/watchdog/mei_wdt.c index e7e3f144f2b0..9addf8902f74 100644 --- a/drivers/watchdog/mei_wdt.c +++ b/drivers/watchdog/mei_wdt.c @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -38,27 +39,35 @@ /* Sub Commands */ #define MEI_MC_START_WD_TIMER_REQ 0x13 +#define MEI_MC_START_WD_TIMER_RES 0x83 +#define MEI_WDT_STATUS_SUCCESS 0 +#define MEI_WDT_WDSTATE_NOT_REQUIRED 0x1 #define MEI_MC_STOP_WD_TIMER_REQ 0x14 /** * enum mei_wdt_state - internal watchdog state * + * @MEI_WDT_PROBE: wd in probing stage * @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 + * @MEI_WDT_NOT_REQUIRED: wd device is not required */ enum mei_wdt_state { + MEI_WDT_PROBE, MEI_WDT_IDLE, MEI_WDT_START, MEI_WDT_RUNNING, MEI_WDT_STOPPING, + MEI_WDT_NOT_REQUIRED, }; -#if IS_ENABLED(CONFIG_DEBUG_FS) static const char *mei_wdt_state_str(enum mei_wdt_state state) { switch (state) { + case MEI_WDT_PROBE: + return "PROBE"; case MEI_WDT_IDLE: return "IDLE"; case MEI_WDT_START: @@ -67,11 +76,12 @@ static const char *mei_wdt_state_str(enum mei_wdt_state state) return "RUNNING"; case MEI_WDT_STOPPING: return "STOPPING"; + case MEI_WDT_NOT_REQUIRED: + return "NOT_REQUIRED"; default: return "unknown"; } } -#endif /* CONFIG_DEBUG_FS */ /** * struct mei_wdt - mei watchdog driver @@ -79,6 +89,10 @@ static const char *mei_wdt_state_str(enum mei_wdt_state state) * * @cldev: mei watchdog client device * @state: watchdog internal state + * @resp_required: ping required response + * @response: ping response completion + * @unregister: unregister worker + * @reg_lock: watchdog device registration lock * @timeout: watchdog current timeout * * @dbgfs_dir: debugfs dir entry @@ -88,6 +102,10 @@ struct mei_wdt { struct mei_cl_device *cldev; enum mei_wdt_state state; + bool resp_required; + struct completion response; + struct work_struct unregister; + struct mutex reg_lock; u16 timeout; #if IS_ENABLED(CONFIG_DEBUG_FS) @@ -123,6 +141,19 @@ struct mei_wdt_start_request { u8 reserved[17]; } __packed; +/** + * struct mei_wdt_start_response watchdog start/ping response + * + * @hdr: Management Control Command Header + * @status: operation status + * @wdstate: watchdog status bit mask + */ +struct mei_wdt_start_response { + struct mei_mc_hdr hdr; + u8 status; + u8 wdstate; +} __packed; + /** * struct mei_wdt_stop_request - watchdog stop * @@ -244,13 +275,18 @@ static int mei_wdt_ops_ping(struct watchdog_device *wdd) if (wdt->state != MEI_WDT_START && wdt->state != MEI_WDT_RUNNING) return 0; + if (wdt->resp_required) + init_completion(&wdt->response); + + wdt->state = MEI_WDT_RUNNING; ret = mei_wdt_ping(wdt); if (ret) return ret; - wdt->state = MEI_WDT_RUNNING; + if (wdt->resp_required) + ret = wait_for_completion_killable(&wdt->response); - return 0; + return ret; } /** @@ -290,6 +326,19 @@ static struct watchdog_info wd_info = { WDIOF_ALARMONLY, }; +/** + * __mei_wdt_is_registered - check if wdt is registered + * + * @wdt: mei watchdog device + * + * Return: true if the wdt is registered with the watchdog subsystem + * Locking: should be called under wdt->reg_lock + */ +static inline bool __mei_wdt_is_registered(struct mei_wdt *wdt) +{ + return !!watchdog_get_drvdata(&wdt->wdd); +} + /** * mei_wdt_unregister - unregister from the watchdog subsystem * @@ -297,8 +346,15 @@ static struct watchdog_info wd_info = { */ static void mei_wdt_unregister(struct mei_wdt *wdt) { - watchdog_unregister_device(&wdt->wdd); - watchdog_set_drvdata(&wdt->wdd, NULL); + mutex_lock(&wdt->reg_lock); + + if (__mei_wdt_is_registered(wdt)) { + watchdog_unregister_device(&wdt->wdd); + watchdog_set_drvdata(&wdt->wdd, NULL); + memset(&wdt->wdd, 0, sizeof(wdt->wdd)); + } + + mutex_unlock(&wdt->reg_lock); } /** @@ -318,6 +374,13 @@ static int mei_wdt_register(struct mei_wdt *wdt) dev = &wdt->cldev->dev; + mutex_lock(&wdt->reg_lock); + + if (__mei_wdt_is_registered(wdt)) { + ret = 0; + goto out; + } + wdt->wdd.info = &wd_info; wdt->wdd.ops = &wd_ops; wdt->wdd.parent = dev; @@ -332,9 +395,106 @@ static int mei_wdt_register(struct mei_wdt *wdt) watchdog_set_drvdata(&wdt->wdd, NULL); } + wdt->state = MEI_WDT_IDLE; + +out: + mutex_unlock(&wdt->reg_lock); return ret; } +static void mei_wdt_unregister_work(struct work_struct *work) +{ + struct mei_wdt *wdt = container_of(work, struct mei_wdt, unregister); + + mei_wdt_unregister(wdt); +} + +/** + * mei_wdt_event_rx - callback for data receive + * + * @cldev: bus device + */ +static void mei_wdt_event_rx(struct mei_cl_device *cldev) +{ + struct mei_wdt *wdt = mei_cldev_get_drvdata(cldev); + struct mei_wdt_start_response res; + const size_t res_len = sizeof(res); + int ret; + + ret = mei_cldev_recv(wdt->cldev, (u8 *)&res, res_len); + if (ret < 0) { + dev_err(&cldev->dev, "failure in recv %d\n", ret); + return; + } + + /* Empty response can be sent on stop */ + if (ret == 0) + return; + + if (ret < sizeof(struct mei_mc_hdr)) { + dev_err(&cldev->dev, "recv small data %d\n", ret); + return; + } + + if (res.hdr.command != MEI_MANAGEMENT_CONTROL || + res.hdr.versionnumber != MEI_MC_VERSION_NUMBER) { + dev_err(&cldev->dev, "wrong command received\n"); + return; + } + + if (res.hdr.subcommand != MEI_MC_START_WD_TIMER_RES) { + dev_warn(&cldev->dev, "unsupported command %d :%s[%d]\n", + res.hdr.subcommand, + mei_wdt_state_str(wdt->state), + wdt->state); + return; + } + + /* Run the unregistration in a worker as this can be + * run only after ping completion, otherwise the flow will + * deadlock on watchdog core mutex. + */ + if (wdt->state == MEI_WDT_RUNNING) { + if (res.wdstate & MEI_WDT_WDSTATE_NOT_REQUIRED) { + wdt->state = MEI_WDT_NOT_REQUIRED; + schedule_work(&wdt->unregister); + } + goto out; + } + + if (wdt->state == MEI_WDT_PROBE) { + if (res.wdstate & MEI_WDT_WDSTATE_NOT_REQUIRED) { + wdt->state = MEI_WDT_NOT_REQUIRED; + } else { + /* stop the watchdog and register watchdog device */ + mei_wdt_stop(wdt); + mei_wdt_register(wdt); + } + return; + } + + dev_warn(&cldev->dev, "not in correct state %s[%d]\n", + mei_wdt_state_str(wdt->state), wdt->state); + +out: + if (!completion_done(&wdt->response)) + complete(&wdt->response); +} + +/** + * mei_wdt_event - callback for event receive + * + * @cldev: bus device + * @events: event mask + * @context: callback context + */ +static void mei_wdt_event(struct mei_cl_device *cldev, + u32 events, void *context) +{ + if (events & BIT(MEI_CL_EVENT_RX)) + mei_wdt_event_rx(cldev); +} + #if IS_ENABLED(CONFIG_DEBUG_FS) static ssize_t mei_dbgfs_read_state(struct file *file, char __user *ubuf, @@ -403,8 +563,13 @@ static int mei_wdt_probe(struct mei_cl_device *cldev, return -ENOMEM; wdt->timeout = MEI_WDT_DEFAULT_TIMEOUT; - wdt->state = MEI_WDT_IDLE; + wdt->state = MEI_WDT_PROBE; wdt->cldev = cldev; + wdt->resp_required = mei_cldev_ver(cldev) > 0x1; + mutex_init(&wdt->reg_lock); + init_completion(&wdt->response); + INIT_WORK(&wdt->unregister, mei_wdt_unregister_work); + mei_cldev_set_drvdata(cldev, wdt); ret = mei_cldev_enable(cldev); @@ -413,9 +578,20 @@ static int mei_wdt_probe(struct mei_cl_device *cldev, goto err_out; } + ret = mei_cldev_register_event_cb(wdt->cldev, BIT(MEI_CL_EVENT_RX), + mei_wdt_event, NULL); + if (ret) { + dev_err(&cldev->dev, "Could not register event ret=%d\n", ret); + goto err_disable; + } + wd_info.firmware_version = mei_cldev_ver(cldev); - ret = mei_wdt_register(wdt); + if (wdt->resp_required) + ret = mei_wdt_ping(wdt); + else + ret = mei_wdt_register(wdt); + if (ret) goto err_disable; @@ -437,6 +613,12 @@ static int mei_wdt_remove(struct mei_cl_device *cldev) { struct mei_wdt *wdt = mei_cldev_get_drvdata(cldev); + /* Free the caller in case of fw initiated or unexpected reset */ + if (!completion_done(&wdt->response)) + complete(&wdt->response); + + cancel_work_sync(&wdt->unregister); + mei_wdt_unregister(wdt); mei_cldev_disable(cldev); @@ -452,7 +634,7 @@ static int mei_wdt_remove(struct mei_cl_device *cldev) 0x89, 0x9D, 0xA9, 0x15, 0x14, 0xCB, 0x32, 0xAB) static struct mei_cl_device_id mei_wdt_tbl[] = { - { .uuid = MEI_UUID_WD, .version = 0x1}, + { .uuid = MEI_UUID_WD, .version = MEI_CL_VERSION_ANY }, /* required last entry */ { } };