e9a05bb4d3
Modify spdx tag for all CAF authored files for Kona. Change-Id: I9308c7189412b1e428a7f67ded0dc076b0e38254 Signed-off-by: Rishabh Bhatnagar <rishabhb@codeaurora.org>
535 lines
15 KiB
C
535 lines
15 KiB
C
// SPDX-License-Identifier: GPL-2.0-only
|
|
/*
|
|
* Copyright (c) 2013-2015, 2017-2018, The Linux Foundation. All rights reserved.
|
|
*/
|
|
|
|
#include <linux/delay.h>
|
|
#include <linux/workqueue.h>
|
|
#include <linux/reboot.h>
|
|
#include <linux/of.h>
|
|
#include <linux/esoc_client.h>
|
|
#include "esoc.h"
|
|
#include "esoc-mdm.h"
|
|
#include "mdm-dbg.h"
|
|
|
|
/* Default number of powerup trial requests per session */
|
|
#define ESOC_DEF_PON_REQ 2
|
|
static unsigned int n_pon_tries = ESOC_DEF_PON_REQ;
|
|
module_param(n_pon_tries, uint, 0644);
|
|
MODULE_PARM_DESC(n_pon_tries,
|
|
"Number of power-on retrials allowed upon boot failure");
|
|
|
|
enum esoc_boot_fail_action {
|
|
BOOT_FAIL_ACTION_RETRY,
|
|
BOOT_FAIL_ACTION_COLD_RESET,
|
|
BOOT_FAIL_ACTION_SHUTDOWN,
|
|
BOOT_FAIL_ACTION_PANIC,
|
|
BOOT_FAIL_ACTION_NOP,
|
|
};
|
|
|
|
static unsigned int boot_fail_action = BOOT_FAIL_ACTION_NOP;
|
|
module_param(boot_fail_action, uint, 0644);
|
|
MODULE_PARM_DESC(boot_fail_action,
|
|
"Actions: 0:Retry PON; 1:Cold reset; 2:Power-down; 3:APQ Panic; 4:No action");
|
|
|
|
enum esoc_pon_state {
|
|
PON_INIT,
|
|
PON_SUCCESS,
|
|
PON_RETRY,
|
|
PON_FAIL
|
|
};
|
|
|
|
enum {
|
|
PWR_OFF = 0x1,
|
|
PWR_ON,
|
|
BOOT,
|
|
RUN,
|
|
CRASH,
|
|
IN_DEBUG,
|
|
SHUTDOWN,
|
|
RESET,
|
|
PEER_CRASH,
|
|
};
|
|
|
|
struct mdm_drv {
|
|
unsigned int mode;
|
|
struct esoc_eng cmd_eng;
|
|
struct completion pon_done;
|
|
struct completion req_eng_wait;
|
|
struct esoc_clink *esoc_clink;
|
|
enum esoc_pon_state pon_state;
|
|
struct workqueue_struct *mdm_queue;
|
|
struct work_struct ssr_work;
|
|
struct notifier_block esoc_restart;
|
|
};
|
|
#define to_mdm_drv(d) container_of(d, struct mdm_drv, cmd_eng)
|
|
|
|
static int esoc_msm_restart_handler(struct notifier_block *nb,
|
|
unsigned long action, void *data)
|
|
{
|
|
struct mdm_drv *mdm_drv = container_of(nb, struct mdm_drv,
|
|
esoc_restart);
|
|
struct esoc_clink *esoc_clink = mdm_drv->esoc_clink;
|
|
const struct esoc_clink_ops *const clink_ops = esoc_clink->clink_ops;
|
|
|
|
if (action == SYS_RESTART) {
|
|
if (mdm_dbg_stall_notify(ESOC_PRIMARY_REBOOT))
|
|
return NOTIFY_OK;
|
|
esoc_mdm_log(
|
|
"Reboot notifier: Notifying esoc of cold reboot\n");
|
|
dev_dbg(&esoc_clink->dev, "Notifying esoc of cold reboot\n");
|
|
clink_ops->notify(ESOC_PRIMARY_REBOOT, esoc_clink);
|
|
}
|
|
return NOTIFY_OK;
|
|
}
|
|
static void mdm_handle_clink_evt(enum esoc_evt evt,
|
|
struct esoc_eng *eng)
|
|
{
|
|
struct mdm_drv *mdm_drv = to_mdm_drv(eng);
|
|
bool unexpected_state = false;
|
|
|
|
switch (evt) {
|
|
case ESOC_INVALID_STATE:
|
|
esoc_mdm_log(
|
|
"ESOC_INVALID_STATE: Calling complete with state: PON_FAIL\n");
|
|
mdm_drv->pon_state = PON_FAIL;
|
|
complete(&mdm_drv->pon_done);
|
|
break;
|
|
case ESOC_RUN_STATE:
|
|
esoc_mdm_log(
|
|
"ESOC_RUN_STATE: Calling complete with state: PON_SUCCESS\n");
|
|
mdm_drv->pon_state = PON_SUCCESS;
|
|
mdm_drv->mode = RUN,
|
|
complete(&mdm_drv->pon_done);
|
|
break;
|
|
case ESOC_RETRY_PON_EVT:
|
|
esoc_mdm_log(
|
|
"ESOC_RETRY_PON_EVT: Calling complete with state: PON_RETRY\n");
|
|
mdm_drv->pon_state = PON_RETRY;
|
|
complete(&mdm_drv->pon_done);
|
|
break;
|
|
case ESOC_UNEXPECTED_RESET:
|
|
esoc_mdm_log("evt_state: ESOC_UNEXPECTED_RESET\n");
|
|
unexpected_state = true;
|
|
case ESOC_ERR_FATAL:
|
|
if (!unexpected_state)
|
|
esoc_mdm_log("evt_state: ESOC_ERR_FATAL\n");
|
|
|
|
/*
|
|
* Modem can crash while we are waiting for pon_done during
|
|
* a subsystem_get(). Setting mode to CRASH will prevent a
|
|
* subsequent subsystem_get() from entering poweron ops. Avoid
|
|
* this by seting mode to CRASH only if device was up and
|
|
* running.
|
|
*/
|
|
if (mdm_drv->mode == CRASH)
|
|
esoc_mdm_log(
|
|
"Modem in crash state already. Ignoring.\n");
|
|
if (mdm_drv->mode != RUN)
|
|
esoc_mdm_log("Modem not up. Ignoring.\n");
|
|
if (mdm_drv->mode == CRASH || mdm_drv->mode != RUN)
|
|
return;
|
|
mdm_drv->mode = CRASH;
|
|
queue_work(mdm_drv->mdm_queue, &mdm_drv->ssr_work);
|
|
break;
|
|
case ESOC_REQ_ENG_ON:
|
|
esoc_mdm_log(
|
|
"evt_state: ESOC_REQ_ENG_ON; Registered a req engine\n");
|
|
complete(&mdm_drv->req_eng_wait);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void mdm_ssr_fn(struct work_struct *work)
|
|
{
|
|
struct mdm_drv *mdm_drv = container_of(work, struct mdm_drv, ssr_work);
|
|
struct mdm_ctrl *mdm = get_esoc_clink_data(mdm_drv->esoc_clink);
|
|
|
|
mdm_wait_for_status_low(mdm, false);
|
|
|
|
esoc_mdm_log("Starting SSR work\n");
|
|
|
|
/*
|
|
* If restarting esoc fails, the SSR framework triggers a kernel panic
|
|
*/
|
|
esoc_clink_request_ssr(mdm_drv->esoc_clink);
|
|
}
|
|
|
|
static void esoc_client_link_power_on(struct esoc_clink *esoc_clink,
|
|
bool mdm_crashed)
|
|
{
|
|
int i;
|
|
struct esoc_client_hook *client_hook;
|
|
|
|
dev_dbg(&esoc_clink->dev, "Calling power_on hooks\n");
|
|
esoc_mdm_log(
|
|
"Calling power_on hooks with crash state: %d\n", mdm_crashed);
|
|
|
|
for (i = 0; i < ESOC_MAX_HOOKS; i++) {
|
|
client_hook = esoc_clink->client_hook[i];
|
|
if (client_hook && client_hook->esoc_link_power_on)
|
|
client_hook->esoc_link_power_on(client_hook->priv,
|
|
mdm_crashed);
|
|
}
|
|
}
|
|
|
|
static void esoc_client_link_power_off(struct esoc_clink *esoc_clink,
|
|
bool mdm_crashed)
|
|
{
|
|
int i;
|
|
struct esoc_client_hook *client_hook;
|
|
|
|
dev_dbg(&esoc_clink->dev, "Calling power_off hooks\n");
|
|
esoc_mdm_log(
|
|
"Calling power_off hooks with crash state: %d\n", mdm_crashed);
|
|
|
|
for (i = 0; i < ESOC_MAX_HOOKS; i++) {
|
|
client_hook = esoc_clink->client_hook[i];
|
|
if (client_hook && client_hook->esoc_link_power_off) {
|
|
client_hook->esoc_link_power_off(client_hook->priv,
|
|
mdm_crashed);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void mdm_crash_shutdown(const struct subsys_desc *mdm_subsys)
|
|
{
|
|
struct esoc_clink *esoc_clink =
|
|
container_of(mdm_subsys,
|
|
struct esoc_clink,
|
|
subsys);
|
|
const struct esoc_clink_ops * const clink_ops = esoc_clink->clink_ops;
|
|
|
|
esoc_mdm_log("MDM crashed notification from SSR\n");
|
|
|
|
if (mdm_dbg_stall_notify(ESOC_PRIMARY_CRASH))
|
|
return;
|
|
clink_ops->notify(ESOC_PRIMARY_CRASH, esoc_clink);
|
|
}
|
|
|
|
static int mdm_subsys_shutdown(const struct subsys_desc *crashed_subsys,
|
|
bool force_stop)
|
|
{
|
|
int ret;
|
|
struct esoc_clink *esoc_clink =
|
|
container_of(crashed_subsys, struct esoc_clink, subsys);
|
|
struct mdm_drv *mdm_drv = esoc_get_drv_data(esoc_clink);
|
|
const struct esoc_clink_ops * const clink_ops = esoc_clink->clink_ops;
|
|
|
|
esoc_mdm_log("Shutdown request from SSR\n");
|
|
|
|
if (mdm_drv->mode == CRASH || mdm_drv->mode == PEER_CRASH) {
|
|
esoc_mdm_log("Shutdown in crash mode\n");
|
|
if (mdm_dbg_stall_cmd(ESOC_PREPARE_DEBUG))
|
|
/* We want to mask debug command.
|
|
* In this case return success
|
|
* to move to next stage
|
|
*/
|
|
return 0;
|
|
|
|
esoc_clink_queue_request(ESOC_REQ_CRASH_SHUTDOWN, esoc_clink);
|
|
esoc_client_link_power_off(esoc_clink, true);
|
|
|
|
esoc_mdm_log("Executing the ESOC_PREPARE_DEBUG command\n");
|
|
ret = clink_ops->cmd_exe(ESOC_PREPARE_DEBUG,
|
|
esoc_clink);
|
|
if (ret) {
|
|
esoc_mdm_log("ESOC_PREPARE_DEBUG command failed\n");
|
|
dev_err(&esoc_clink->dev, "failed to enter debug\n");
|
|
return ret;
|
|
}
|
|
mdm_drv->mode = IN_DEBUG;
|
|
} else if (!force_stop) {
|
|
esoc_mdm_log("Graceful shutdown mode\n");
|
|
if (esoc_clink->subsys.sysmon_shutdown_ret) {
|
|
esoc_mdm_log(
|
|
"Executing the ESOC_FORCE_PWR_OFF command\n");
|
|
ret = clink_ops->cmd_exe(ESOC_FORCE_PWR_OFF,
|
|
esoc_clink);
|
|
} else {
|
|
if (mdm_dbg_stall_cmd(ESOC_PWR_OFF))
|
|
/* Since power off command is masked
|
|
* we return success, and leave the state
|
|
* of the command engine as is.
|
|
*/
|
|
return 0;
|
|
dev_dbg(&esoc_clink->dev, "Sending sysmon-shutdown\n");
|
|
esoc_mdm_log("Executing the ESOC_PWR_OFF command\n");
|
|
ret = clink_ops->cmd_exe(ESOC_PWR_OFF, esoc_clink);
|
|
}
|
|
if (ret) {
|
|
esoc_mdm_log(
|
|
"Executing the ESOC_PWR_OFF command failed\n");
|
|
dev_err(&esoc_clink->dev, "failed to exe power off\n");
|
|
return ret;
|
|
}
|
|
esoc_client_link_power_off(esoc_clink, false);
|
|
/* Pull the reset line low to turn off the device */
|
|
clink_ops->cmd_exe(ESOC_FORCE_PWR_OFF, esoc_clink);
|
|
mdm_drv->mode = PWR_OFF;
|
|
}
|
|
esoc_mdm_log("Shutdown completed\n");
|
|
return 0;
|
|
}
|
|
|
|
static void mdm_subsys_retry_powerup_cleanup(struct esoc_clink *esoc_clink)
|
|
{
|
|
struct mdm_ctrl *mdm = get_esoc_clink_data(esoc_clink);
|
|
struct mdm_drv *mdm_drv = esoc_get_drv_data(esoc_clink);
|
|
|
|
esoc_mdm_log("Doing cleanup\n");
|
|
|
|
esoc_client_link_power_off(esoc_clink, false);
|
|
mdm_disable_irqs(mdm);
|
|
mdm_drv->pon_state = PON_INIT;
|
|
reinit_completion(&mdm_drv->pon_done);
|
|
reinit_completion(&mdm_drv->req_eng_wait);
|
|
}
|
|
|
|
/* Returns 0 to proceed towards another retry, or an error code to quit */
|
|
static int mdm_handle_boot_fail(struct esoc_clink *esoc_clink, u8 *pon_trial)
|
|
{
|
|
struct mdm_ctrl *mdm = get_esoc_clink_data(esoc_clink);
|
|
|
|
switch (boot_fail_action) {
|
|
case BOOT_FAIL_ACTION_RETRY:
|
|
mdm_subsys_retry_powerup_cleanup(esoc_clink);
|
|
esoc_mdm_log("Request to retry a warm reset\n");
|
|
(*pon_trial)++;
|
|
break;
|
|
/*
|
|
* Issue a shutdown here and rerun the powerup again.
|
|
* This way it becomes a cold reset. Else, we end up
|
|
* issuing a cold reset & a warm reset back to back.
|
|
*/
|
|
case BOOT_FAIL_ACTION_COLD_RESET:
|
|
mdm_subsys_retry_powerup_cleanup(esoc_clink);
|
|
esoc_mdm_log("Doing cold reset by power-down and warm reset\n");
|
|
(*pon_trial)++;
|
|
mdm_power_down(mdm);
|
|
break;
|
|
case BOOT_FAIL_ACTION_PANIC:
|
|
esoc_mdm_log("Calling panic!!\n");
|
|
panic("Panic requested on external modem boot failure\n");
|
|
break;
|
|
case BOOT_FAIL_ACTION_NOP:
|
|
esoc_mdm_log("Leaving the modem in its curent state\n");
|
|
return -EIO;
|
|
case BOOT_FAIL_ACTION_SHUTDOWN:
|
|
default:
|
|
mdm_subsys_retry_powerup_cleanup(esoc_clink);
|
|
esoc_mdm_log("Shutdown the modem and quit\n");
|
|
mdm_power_down(mdm);
|
|
return -EIO;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int mdm_subsys_powerup(const struct subsys_desc *crashed_subsys)
|
|
{
|
|
int ret;
|
|
struct esoc_clink *esoc_clink =
|
|
container_of(crashed_subsys, struct esoc_clink,
|
|
subsys);
|
|
struct mdm_drv *mdm_drv = esoc_get_drv_data(esoc_clink);
|
|
const struct esoc_clink_ops * const clink_ops = esoc_clink->clink_ops;
|
|
int timeout = INT_MAX;
|
|
u8 pon_trial = 1;
|
|
|
|
esoc_mdm_log("Powerup request from SSR\n");
|
|
|
|
do {
|
|
esoc_mdm_log("Boot trial: %d\n", pon_trial);
|
|
if (!esoc_clink->auto_boot &&
|
|
!esoc_req_eng_enabled(esoc_clink)) {
|
|
esoc_mdm_log("Wait for req eng registration\n");
|
|
dev_dbg(&esoc_clink->dev,
|
|
"Wait for req eng registration\n");
|
|
wait_for_completion(&mdm_drv->req_eng_wait);
|
|
}
|
|
esoc_mdm_log("Req eng available\n");
|
|
if (mdm_drv->mode == PWR_OFF) {
|
|
esoc_mdm_log("In normal power-on mode\n");
|
|
if (mdm_dbg_stall_cmd(ESOC_PWR_ON))
|
|
return -EBUSY;
|
|
esoc_mdm_log("Executing the ESOC_PWR_ON command\n");
|
|
ret = clink_ops->cmd_exe(ESOC_PWR_ON, esoc_clink);
|
|
if (ret) {
|
|
esoc_mdm_log("ESOC_PWR_ON command failed\n");
|
|
dev_err(&esoc_clink->dev, "pwr on fail\n");
|
|
return ret;
|
|
}
|
|
esoc_client_link_power_on(esoc_clink, false);
|
|
} else if (mdm_drv->mode == IN_DEBUG) {
|
|
esoc_mdm_log("In SSR power-on mode\n");
|
|
esoc_mdm_log("Executing the ESOC_EXIT_DEBUG command\n");
|
|
ret = clink_ops->cmd_exe(ESOC_EXIT_DEBUG, esoc_clink);
|
|
if (ret) {
|
|
esoc_mdm_log(
|
|
"ESOC_EXIT_DEBUG command failed\n");
|
|
dev_err(&esoc_clink->dev,
|
|
"cannot exit debug mode\n");
|
|
return ret;
|
|
}
|
|
mdm_drv->mode = PWR_OFF;
|
|
esoc_mdm_log("Executing the ESOC_PWR_ON command\n");
|
|
ret = clink_ops->cmd_exe(ESOC_PWR_ON, esoc_clink);
|
|
if (ret) {
|
|
dev_err(&esoc_clink->dev, "pwr on fail\n");
|
|
return ret;
|
|
}
|
|
esoc_client_link_power_on(esoc_clink, true);
|
|
}
|
|
|
|
/*
|
|
* In autoboot case, it is possible that we can forever wait for
|
|
* boot completion, when esoc fails to boot. This is because
|
|
* there is no helper application which can alert esoc driver
|
|
* about boot failure. Prevent going to wait forever in such
|
|
* case.
|
|
*/
|
|
if (esoc_clink->auto_boot)
|
|
timeout = 10 * HZ;
|
|
esoc_mdm_log(
|
|
"Modem turned-on. Waiting for pon_done notification..\n");
|
|
ret = wait_for_completion_timeout(&mdm_drv->pon_done, timeout);
|
|
if (mdm_drv->pon_state == PON_FAIL || ret <= 0) {
|
|
dev_err(&esoc_clink->dev, "booting failed\n");
|
|
esoc_mdm_log("booting failed\n");
|
|
ret = mdm_handle_boot_fail(esoc_clink, &pon_trial);
|
|
if (ret)
|
|
return ret;
|
|
} else if (mdm_drv->pon_state == PON_RETRY) {
|
|
esoc_mdm_log(
|
|
"Boot failed. Doing cleanup and attempting to retry\n");
|
|
pon_trial++;
|
|
mdm_subsys_retry_powerup_cleanup(esoc_clink);
|
|
} else if (mdm_drv->pon_state == PON_SUCCESS) {
|
|
break;
|
|
}
|
|
} while (pon_trial <= n_pon_tries);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int mdm_subsys_ramdumps(int want_dumps,
|
|
const struct subsys_desc *crashed_subsys)
|
|
{
|
|
int ret;
|
|
struct esoc_clink *esoc_clink =
|
|
container_of(crashed_subsys, struct esoc_clink,
|
|
subsys);
|
|
const struct esoc_clink_ops * const clink_ops = esoc_clink->clink_ops;
|
|
|
|
esoc_mdm_log("Ramdumps called from SSR\n");
|
|
|
|
if (want_dumps) {
|
|
esoc_mdm_log("Executing the ESOC_EXE_DEBUG command\n");
|
|
ret = clink_ops->cmd_exe(ESOC_EXE_DEBUG, esoc_clink);
|
|
if (ret) {
|
|
esoc_mdm_log(
|
|
"Failed executing the ESOC_EXE_DEBUG command\n");
|
|
dev_err(&esoc_clink->dev, "debugging failed\n");
|
|
return ret;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int mdm_register_ssr(struct esoc_clink *esoc_clink)
|
|
{
|
|
struct subsys_desc *subsys = &esoc_clink->subsys;
|
|
|
|
subsys->shutdown = mdm_subsys_shutdown;
|
|
subsys->ramdump = mdm_subsys_ramdumps;
|
|
subsys->powerup = mdm_subsys_powerup;
|
|
subsys->crash_shutdown = mdm_crash_shutdown;
|
|
return esoc_clink_register_ssr(esoc_clink);
|
|
}
|
|
|
|
int esoc_ssr_probe(struct esoc_clink *esoc_clink, struct esoc_drv *drv)
|
|
{
|
|
int ret;
|
|
struct mdm_drv *mdm_drv;
|
|
struct esoc_eng *esoc_eng;
|
|
|
|
mdm_drv = devm_kzalloc(&esoc_clink->dev, sizeof(*mdm_drv), GFP_KERNEL);
|
|
if (IS_ERR_OR_NULL(mdm_drv))
|
|
return PTR_ERR(mdm_drv);
|
|
esoc_eng = &mdm_drv->cmd_eng;
|
|
esoc_eng->handle_clink_evt = mdm_handle_clink_evt;
|
|
ret = esoc_clink_register_cmd_eng(esoc_clink, esoc_eng);
|
|
if (ret) {
|
|
dev_err(&esoc_clink->dev, "failed to register cmd engine\n");
|
|
return ret;
|
|
}
|
|
ret = mdm_register_ssr(esoc_clink);
|
|
if (ret)
|
|
goto ssr_err;
|
|
mdm_drv->mdm_queue = alloc_workqueue("mdm_drv_queue", 0, 0);
|
|
if (!mdm_drv->mdm_queue) {
|
|
dev_err(&esoc_clink->dev, "could not create mdm_queue\n");
|
|
goto queue_err;
|
|
}
|
|
esoc_set_drv_data(esoc_clink, mdm_drv);
|
|
init_completion(&mdm_drv->pon_done);
|
|
init_completion(&mdm_drv->req_eng_wait);
|
|
INIT_WORK(&mdm_drv->ssr_work, mdm_ssr_fn);
|
|
mdm_drv->esoc_clink = esoc_clink;
|
|
mdm_drv->mode = PWR_OFF;
|
|
mdm_drv->pon_state = PON_INIT;
|
|
mdm_drv->esoc_restart.notifier_call = esoc_msm_restart_handler;
|
|
ret = register_reboot_notifier(&mdm_drv->esoc_restart);
|
|
if (ret)
|
|
dev_err(&esoc_clink->dev, "register for reboot failed\n");
|
|
ret = mdm_dbg_eng_init(drv, esoc_clink);
|
|
if (ret) {
|
|
debug_init_done = false;
|
|
dev_err(&esoc_clink->dev, "dbg engine failure\n");
|
|
} else {
|
|
dev_dbg(&esoc_clink->dev, "dbg engine initialized\n");
|
|
debug_init_done = true;
|
|
}
|
|
|
|
return 0;
|
|
queue_err:
|
|
esoc_clink_unregister_ssr(esoc_clink);
|
|
ssr_err:
|
|
esoc_clink_unregister_cmd_eng(esoc_clink, esoc_eng);
|
|
return ret;
|
|
}
|
|
|
|
static struct esoc_compat compat_table[] = {
|
|
{
|
|
.name = "MDM9x55",
|
|
.data = NULL,
|
|
},
|
|
{
|
|
.name = "SDX50M",
|
|
.data = NULL,
|
|
},
|
|
{
|
|
.name = "SDX55M",
|
|
.data = NULL,
|
|
},
|
|
};
|
|
|
|
static struct esoc_drv esoc_ssr_drv = {
|
|
.owner = THIS_MODULE,
|
|
.probe = esoc_ssr_probe,
|
|
.compat_table = compat_table,
|
|
.compat_entries = ARRAY_SIZE(compat_table),
|
|
.driver = {
|
|
.name = "mdm-4x",
|
|
},
|
|
};
|
|
|
|
int __init esoc_ssr_init(void)
|
|
{
|
|
return esoc_drv_register(&esoc_ssr_drv);
|
|
}
|
|
module_init(esoc_ssr_init);
|
|
MODULE_LICENSE("GPL v2");
|