1005 lines
23 KiB
C
Executable file
1005 lines
23 KiB
C
Executable file
/*
|
|
* exynos-ssp.c - Samsung Secure Platform driver for the Exynos
|
|
*
|
|
* Copyright (C) 2019 Samsung Electronics
|
|
* Keunyoung Park <keun0.park@samsung.com>
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License as published by the
|
|
* Free Software Foundation; either version 2 of the License, or (at your
|
|
* option) any later version.
|
|
*
|
|
*/
|
|
|
|
#include <linux/kernel.h>
|
|
#include <linux/module.h>
|
|
#include <linux/io.h>
|
|
#include <linux/delay.h>
|
|
#include <linux/spinlock.h>
|
|
#include <linux/platform_device.h>
|
|
#include <linux/pm_runtime.h>
|
|
#include <linux/pm_wakeup.h>
|
|
#include <linux/smc.h>
|
|
#include <linux/miscdevice.h>
|
|
#include <linux/ioctl.h>
|
|
#include <linux/uaccess.h>
|
|
#include <linux/cdev.h>
|
|
#include <linux/debugfs.h>
|
|
#include <linux/timer.h>
|
|
#include <linux/mm.h>
|
|
#include <linux/device.h>
|
|
#include <soc/samsung/exynos-smc.h>
|
|
#include <soc/samsung/exynos-pd.h>
|
|
#include <linux/soc/samsung/exynos-soc.h>
|
|
#include <linux/regulator/consumer.h>
|
|
#include <linux/gpio.h>
|
|
#include <linux/gpio/consumer.h>
|
|
#include <linux/of_gpio.h>
|
|
|
|
#define SSP_RET_OK 0
|
|
#define SSP_RET_FAIL -1
|
|
#define SSP_RET_BUSY 0x20010 /* defined at LDFW */
|
|
#define SSP_MAX_USER_CNT 100
|
|
#define SSP_MAX_USER_PATH_LEN 128
|
|
#define SSP_K250_STABLE_TIME_MS 20
|
|
#define SSP_V9RR_STABLE_TIME_MS 10
|
|
|
|
/* smc to call ldfw functions */
|
|
#define SMC_CMD_SSP (0xC2001040)
|
|
#define SSP_CMD_BOOT (0x1)
|
|
#define SSP_CMD_BACKUP (0x2)
|
|
#define SSP_CMD_RESTORE (0x3)
|
|
#define SSP_CMD_SELF_TEST (0x10)
|
|
#define SSP_CMD_BOOT_INIT (0x11)
|
|
#define SSP_CMD_BOOT_CHECK (0x12)
|
|
#define SSP_CMD_RESTORE_INIT (0x31)
|
|
#define SSP_CMD_RESTORE_CHECK (0x32)
|
|
|
|
/* ioctl command for TAC */
|
|
#define SSP_IOCTL_MAGIC 'c'
|
|
#define SSP_IOCTL_INIT _IO(SSP_IOCTL_MAGIC, 1)
|
|
#define SSP_IOCTL_EXIT _IOWR(SSP_IOCTL_MAGIC, 2, uint64_t)
|
|
#define SSP_IOCTL_TEST _IOWR(SSP_IOCTL_MAGIC, 3, uint64_t)
|
|
#define SSP_IOCTL_DEBUG _IOWR(SSP_IOCTL_MAGIC, 4, uint64_t)
|
|
|
|
#define ssp_err(dev, fmt, arg...) printk("[EXYNOS][CMSSP][ERROR] " fmt, ##arg)
|
|
#define ssp_info(dev, fmt, arg...) printk("[EXYNOS][CMSSP][ INFO] " fmt, ##arg)
|
|
|
|
/* SFR for ssp power control */
|
|
#define PMU_ALIVE_PA_BASE (0x15860000 + 0x2000)
|
|
#define PMU_SSP_STATUS_VA_OFFSET (0x304)
|
|
#define PMU_SSP_STATUS_MASK (1 << 0)
|
|
|
|
void __iomem *pmu_va_base;
|
|
|
|
spinlock_t ssp_lock;
|
|
struct mutex ssp_ioctl_lock;
|
|
static int ssp_power_count;
|
|
static int ssp_idle_ip_index;
|
|
extern struct exynos_chipid_info exynos_soc_info;
|
|
|
|
struct ssp_device {
|
|
struct device *dev;
|
|
struct miscdevice misc_device;
|
|
struct regulator *secure_nvm_ldo;
|
|
unsigned long snvm_init_time;
|
|
int gpio_se_sel;
|
|
int gpio_snvm_reset;
|
|
int is_k250;
|
|
};
|
|
|
|
struct ssp_user {
|
|
char path[SSP_MAX_USER_PATH_LEN];
|
|
unsigned long init_count;
|
|
unsigned long init_time;
|
|
unsigned long exit_count;
|
|
unsigned long exit_time;
|
|
unsigned long init_fail_count;
|
|
unsigned long init_fail_time;
|
|
unsigned long exit_fail_count;
|
|
unsigned long exit_fail_time;
|
|
struct ssp_user *next;
|
|
};
|
|
|
|
struct ssp_user *head = NULL;
|
|
|
|
static void exynos_ssp_print_user_list(struct device *dev)
|
|
{
|
|
struct ssp_user *ptr = head;
|
|
int i = 0;
|
|
|
|
ssp_info(dev, "====== Power control status ================="
|
|
"=============================================\n");
|
|
ssp_info(dev, "No: %8s:\t%8s\t%8s\t%16s\t%16s\n",
|
|
"Caller", "ON", "OFF", "ON-TIME", "OFF-TIME");
|
|
ssp_info(dev, "---------------------------------------------"
|
|
"---------------------------------------------\n");
|
|
|
|
while (ptr != NULL) {
|
|
i++;
|
|
|
|
ssp_info(dev, "%2u:\t%8s: %s\n",
|
|
i,
|
|
"Path",
|
|
ptr->path);
|
|
|
|
ssp_info(dev, "%2u:\t%8s:\t%8u\t%8u\t%16u\t%16u\n",
|
|
i,
|
|
"Success",
|
|
ptr->init_count,
|
|
ptr->exit_count,
|
|
ptr->init_time,
|
|
ptr->exit_time);
|
|
|
|
ssp_info(dev, "%2u:\t%8s:\t%8u\t%8u\t%16u\t%16u\n",
|
|
i,
|
|
"Fail",
|
|
ptr->init_fail_count,
|
|
ptr->exit_fail_count,
|
|
ptr->init_fail_time,
|
|
ptr->exit_fail_time);
|
|
|
|
ptr = ptr->next;
|
|
}
|
|
ssp_info(dev, "---------------------------------------------"
|
|
"---------------------------------------------\n");
|
|
}
|
|
|
|
static int exynos_ssp_get_path(struct device *dev, struct task_struct *task, char *task_path)
|
|
{
|
|
int ret = SSP_RET_OK;
|
|
char *buf;
|
|
char *path;
|
|
struct file *exe_file;
|
|
|
|
buf = (char *)__get_free_page(GFP_KERNEL);
|
|
if (!buf) {
|
|
ssp_err(dev, "%s: fail to __get_free_page.\n", __func__);
|
|
return -ENOMEM;
|
|
}
|
|
|
|
exe_file = get_task_exe_file(task);
|
|
if (!exe_file) {
|
|
ret = -ENOENT;
|
|
ssp_err(dev, "%s: fail to get_task_exe_file.\n", __func__);
|
|
goto end;
|
|
}
|
|
|
|
path = d_path(&exe_file->f_path, buf, PAGE_SIZE);
|
|
if (IS_ERR(path)) {
|
|
ret = PTR_ERR(path);
|
|
ssp_err(dev, "%s: fail to d_path. ret = 0x%x\n", __func__, ret);
|
|
goto end;
|
|
}
|
|
|
|
memset(task_path, 0, SSP_MAX_USER_PATH_LEN);
|
|
strncpy(task_path, path, SSP_MAX_USER_PATH_LEN - 1);
|
|
end:
|
|
free_page((unsigned long)buf);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static struct ssp_user *exynos_ssp_find_user(char *path)
|
|
{
|
|
struct ssp_user *now = head;
|
|
|
|
if (head == NULL)
|
|
return NULL;
|
|
|
|
while (strncmp(now->path, path, SSP_MAX_USER_PATH_LEN)) {
|
|
if (now->next == NULL)
|
|
return NULL;
|
|
|
|
now = now->next;
|
|
}
|
|
|
|
return now;
|
|
}
|
|
|
|
static int exynos_ssp_powerctl_update(struct device *dev, bool power_on, bool pass)
|
|
{
|
|
int ret = SSP_RET_OK;
|
|
char path[SSP_MAX_USER_PATH_LEN];
|
|
static int ssp_user_count;
|
|
struct ssp_user *link = NULL;
|
|
|
|
ret = exynos_ssp_get_path(dev, current, path);
|
|
if (ret) {
|
|
ssp_err(dev, "%s: fail to get path. ret = 0x%x\n", __func__, ret);
|
|
return ret;
|
|
}
|
|
|
|
link = exynos_ssp_find_user(path);
|
|
|
|
if (link == NULL) {
|
|
if (++ssp_user_count >= SSP_MAX_USER_CNT) {
|
|
ssp_err(dev, "%s: exceed max user count.\n", __func__);
|
|
return -ENOMEM;
|
|
}
|
|
|
|
link = (struct ssp_user *)kzalloc(sizeof(struct ssp_user), GFP_KERNEL);
|
|
if (!link) {
|
|
ssp_err(dev, "%s: fail to kzalloc.\n", __func__);
|
|
return -ENOMEM;
|
|
}
|
|
|
|
memcpy(link->path, path, sizeof(path));
|
|
link->init_count = 0;
|
|
link->exit_count = 0;
|
|
|
|
link->next = head;
|
|
head = link;
|
|
}
|
|
|
|
if (power_on == 1 && pass == 1) {
|
|
link->init_count++;
|
|
link->init_time = ktime_get_boottime_ns() / NSEC_PER_USEC;
|
|
} else if (power_on == 1 && pass == 0) {
|
|
link->init_fail_count++;
|
|
link->init_fail_time = ktime_get_boottime_ns() / NSEC_PER_USEC;
|
|
} else if (power_on == 0 && pass == 1) {
|
|
link->exit_count++;
|
|
link->exit_time = ktime_get_boottime_ns() / NSEC_PER_USEC;
|
|
} else if (power_on == 0 && pass == 0) {
|
|
link->exit_fail_count++;
|
|
link->exit_fail_time = ktime_get_boottime_ns() / NSEC_PER_USEC;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int exynos_cm_smc(uint64_t *arg0, uint64_t *arg1,
|
|
uint64_t *arg2, uint64_t *arg3)
|
|
{
|
|
struct arm_smccc_res res;
|
|
|
|
arm_smccc_smc(*arg0, *arg1, *arg2, *arg3, 0, 0, 0, 0, &res);
|
|
|
|
*arg0 = res.a0;
|
|
*arg1 = res.a1;
|
|
*arg2 = res.a2;
|
|
*arg3 = res.a3;
|
|
|
|
return *arg0;
|
|
}
|
|
|
|
static int exynos_ssp_map_sfr(struct ssp_device *sspdev)
|
|
{
|
|
int ret = SSP_RET_OK;
|
|
|
|
pmu_va_base = ioremap(PMU_ALIVE_PA_BASE, SZ_4K);
|
|
if (!pmu_va_base) {
|
|
ssp_err(sspdev->dev, "%s: fail to ioremap\n", __func__);
|
|
ret = SSP_RET_FAIL;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static bool exynos_ssp_check_power_status(struct device *dev)
|
|
{
|
|
unsigned int reg;
|
|
|
|
if (!pmu_va_base) {
|
|
ssp_err(dev, "%s: invalid pmu_va_base\n", __func__);
|
|
return false;
|
|
}
|
|
|
|
reg = readl(pmu_va_base + PMU_SSP_STATUS_VA_OFFSET);
|
|
if (reg & PMU_SSP_STATUS_MASK)
|
|
return true;
|
|
|
|
reg = readl(pmu_va_base + PMU_SSP_STATUS_VA_OFFSET);
|
|
if (reg & PMU_SSP_STATUS_MASK)
|
|
return true;
|
|
|
|
ssp_err(dev, "%s: power hasn't been enabled: 0x%x\n", __func__, reg);
|
|
ssp_err(dev, "%s: pmu address: 0x%x\n", __func__, pmu_va_base + PMU_SSP_STATUS_VA_OFFSET);
|
|
|
|
return false;
|
|
}
|
|
|
|
static void exynos_ssp_pm_enable(struct ssp_device *sspdev)
|
|
{
|
|
pm_runtime_enable(sspdev->dev);
|
|
ssp_info(sspdev->dev, "pm_runtime_enable\n");
|
|
}
|
|
|
|
static int exynos_ssp_power_on(struct ssp_device *sspdev)
|
|
{
|
|
int ret = SSP_RET_OK;
|
|
|
|
ret = pm_runtime_get_sync(sspdev->dev);
|
|
if (ret != SSP_RET_OK)
|
|
ssp_err(sspdev->dev, "%s: fail to pm_runtime_get_sync. ret = 0x%x\n", __func__, ret);
|
|
else
|
|
ssp_info(sspdev->dev, "pm_runtime_get_sync done\n");
|
|
|
|
if (exynos_ssp_check_power_status(sspdev->dev) == false) {
|
|
ssp_err(sspdev->dev, "%s: ssp power status\n", __func__);
|
|
return SSP_RET_FAIL;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int exynos_ssp_power_off(struct ssp_device *sspdev)
|
|
{
|
|
int ret = SSP_RET_OK;
|
|
|
|
ret = pm_runtime_put_sync(sspdev->dev);
|
|
if (ret != SSP_RET_OK)
|
|
ssp_err(sspdev->dev, "%s: fail to pm_runtime_put_sync. ret = 0x%x\n", __func__, ret);
|
|
else
|
|
ssp_info(sspdev->dev, "pm_runtime_put_sync done\n");
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int exynos_ssp_boot_init(struct device *dev)
|
|
{
|
|
int ret = SSP_RET_OK;
|
|
uint64_t reg0;
|
|
uint64_t reg1;
|
|
uint64_t reg2;
|
|
uint64_t reg3;
|
|
uint64_t count;
|
|
unsigned long flag;
|
|
|
|
ssp_info(dev, "ssp boot start\n");
|
|
|
|
exynos_update_ip_idle_status(ssp_idle_ip_index, 0);
|
|
count = 0;
|
|
|
|
do {
|
|
count++;
|
|
|
|
reg0 = SMC_CMD_SSP;
|
|
reg1 = SSP_CMD_BOOT_INIT;
|
|
reg2 = 0;
|
|
reg3 = 0;
|
|
|
|
spin_lock_irqsave(&ssp_lock, flag);
|
|
ret = exynos_cm_smc(®0, ®1, ®2, ®3);
|
|
spin_unlock_irqrestore(&ssp_lock, flag);
|
|
|
|
if (ret == SSP_RET_BUSY)
|
|
usleep_range(500, 1000);
|
|
|
|
} while (ret == SSP_RET_BUSY);
|
|
|
|
if (ret != SSP_RET_OK)
|
|
ssp_err(dev, "%s: fail to boot at ldfw. ret = 0x%x\n", __func__, ret);
|
|
else
|
|
ssp_info(dev, "ssp boot done: %d\n", count);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int exynos_ssp_boot_check(struct device *dev)
|
|
{
|
|
int ret = SSP_RET_OK;
|
|
uint64_t reg0;
|
|
uint64_t reg1;
|
|
uint64_t reg2;
|
|
uint64_t reg3;
|
|
uint64_t count;
|
|
unsigned long flag;
|
|
|
|
ssp_info(dev, "ssp boot check start\n");
|
|
|
|
count = 0;
|
|
|
|
do {
|
|
count++;
|
|
|
|
reg0 = SMC_CMD_SSP;
|
|
reg1 = SSP_CMD_BOOT_CHECK;
|
|
reg2 = 0;
|
|
reg3 = 0;
|
|
|
|
spin_lock_irqsave(&ssp_lock, flag);
|
|
ret = exynos_cm_smc(®0, ®1, ®2, ®3);
|
|
spin_unlock_irqrestore(&ssp_lock, flag);
|
|
|
|
if (ret == SSP_RET_BUSY)
|
|
usleep_range(500, 1000);
|
|
|
|
} while (ret == SSP_RET_BUSY);
|
|
|
|
exynos_update_ip_idle_status(ssp_idle_ip_index, 1);
|
|
|
|
if (ret != SSP_RET_OK)
|
|
ssp_err(dev, "%s: fail to boot check at ldfw. ret = 0x%x\n", __func__, ret);
|
|
else
|
|
ssp_info(dev, "ssp boot check done: %d\n", count);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int exynos_ssp_backup(struct device *dev)
|
|
{
|
|
int ret = SSP_RET_OK;
|
|
uint64_t reg0;
|
|
uint64_t reg1;
|
|
uint64_t reg2;
|
|
uint64_t reg3;
|
|
unsigned long flag;
|
|
|
|
ssp_info(dev, "ssp backup start\n");
|
|
|
|
reg0 = SMC_CMD_SSP;
|
|
reg1 = SSP_CMD_BACKUP;
|
|
reg2 = 0;
|
|
reg3 = 0;
|
|
|
|
exynos_update_ip_idle_status(ssp_idle_ip_index, 0);
|
|
|
|
spin_lock_irqsave(&ssp_lock, flag);
|
|
ret = exynos_cm_smc(®0, ®1, ®2, ®3);
|
|
spin_unlock_irqrestore(&ssp_lock, flag);
|
|
|
|
exynos_update_ip_idle_status(ssp_idle_ip_index, 1);
|
|
|
|
if (ret != SSP_RET_OK)
|
|
ssp_err(dev, "%s: fail to backup at ldfw. ret = 0x%x\n", __func__, ret);
|
|
else
|
|
ssp_info(dev, "ssp backup done\n");
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int exynos_ssp_restore_init(struct device *dev)
|
|
{
|
|
int ret = SSP_RET_OK;
|
|
uint64_t reg0;
|
|
uint64_t reg1;
|
|
uint64_t reg2;
|
|
uint64_t reg3;
|
|
unsigned long flag;
|
|
|
|
ssp_info(dev, "ssp restore init start\n");
|
|
|
|
reg0 = SMC_CMD_SSP;
|
|
reg1 = SSP_CMD_RESTORE_INIT;
|
|
reg2 = 0;
|
|
reg3 = 0;
|
|
|
|
exynos_update_ip_idle_status(ssp_idle_ip_index, 0);
|
|
|
|
spin_lock_irqsave(&ssp_lock, flag);
|
|
ret = exynos_cm_smc(®0, ®1, ®2, ®3);
|
|
spin_unlock_irqrestore(&ssp_lock, flag);
|
|
|
|
if (ret != SSP_RET_OK)
|
|
ssp_err(dev, "%s: fail to restore check at ldfw. ret = 0x%x\n", __func__, ret);
|
|
else
|
|
ssp_info(dev, "ssp restore init done\n");
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int exynos_ssp_restore_check(struct device *dev)
|
|
{
|
|
int ret = SSP_RET_OK;
|
|
uint64_t reg0;
|
|
uint64_t reg1;
|
|
uint64_t reg2;
|
|
uint64_t reg3;
|
|
unsigned long flag;
|
|
|
|
ssp_info(dev, "ssp restore check start\n");
|
|
|
|
reg0 = SMC_CMD_SSP;
|
|
reg1 = SSP_CMD_RESTORE_CHECK;
|
|
reg2 = 0;
|
|
reg3 = 0;
|
|
|
|
spin_lock_irqsave(&ssp_lock, flag);
|
|
ret = exynos_cm_smc(®0, ®1, ®2, ®3);
|
|
spin_unlock_irqrestore(&ssp_lock, flag);
|
|
|
|
exynos_update_ip_idle_status(ssp_idle_ip_index, 1);
|
|
|
|
if (ret != SSP_RET_OK)
|
|
ssp_err(dev, "%s: fail to restore check at ldfw. ret = 0x%x\n", __func__, ret);
|
|
else
|
|
ssp_info(dev, "ssp restore check done\n");
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int exynos_ssp_self_test(struct device *dev, uint64_t test_mode)
|
|
{
|
|
int ret = SSP_RET_OK;
|
|
uint64_t reg0;
|
|
uint64_t reg1;
|
|
uint64_t reg2;
|
|
uint64_t reg3;
|
|
unsigned long flag;
|
|
|
|
ssp_info(dev, "call ssp function: %d\n", (int)test_mode);
|
|
|
|
reg0 = SMC_CMD_SSP;
|
|
reg1 = SSP_CMD_SELF_TEST;
|
|
reg2 = test_mode;
|
|
reg3 = 0;
|
|
|
|
spin_lock_irqsave(&ssp_lock, flag);
|
|
ret = exynos_cm_smc(®0, ®1, ®2, ®3);
|
|
spin_unlock_irqrestore(&ssp_lock, flag);
|
|
|
|
ssp_info(dev, "return from ldfw: 0x%x\n", ret);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int exynos_se_power_on_phase1(struct ssp_device *dev)
|
|
{
|
|
int ret = SSP_RET_OK;
|
|
|
|
ssp_info(dev, "Secure NVM on\n");
|
|
|
|
if (IS_ERR(dev->secure_nvm_ldo) || regulator_enable(dev->secure_nvm_ldo) < 0) {
|
|
ssp_err(dev, "%s - failed to enable LDO for SE\n", __func__);
|
|
return -ENODEV;
|
|
}
|
|
if (dev->is_k250) {
|
|
if (dev->gpio_snvm_reset >= 0) {
|
|
/* GPC5.1 set to 1 if controllable */
|
|
gpio_set_value(dev->gpio_snvm_reset, 1);
|
|
}
|
|
}
|
|
dev->snvm_init_time = ktime_get_boottime_ns();
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int exynos_se_power_on_phase2(struct ssp_device *dev)
|
|
{
|
|
int ret = SSP_RET_OK;
|
|
unsigned long delay;
|
|
|
|
/* delay for chip stabilization */
|
|
delay = (ktime_get_boottime_ns() - dev->snvm_init_time) / NSEC_PER_MSEC;
|
|
|
|
if (dev->is_k250) {
|
|
if (delay < SSP_K250_STABLE_TIME_MS) {
|
|
mdelay(SSP_K250_STABLE_TIME_MS - delay);
|
|
}
|
|
|
|
if (dev->gpio_se_sel >= 0) {
|
|
/* GPC1.0 set to 1 if controllable */
|
|
gpio_set_value(dev->gpio_se_sel, 1);
|
|
}
|
|
} else {
|
|
if (delay < SSP_V9RR_STABLE_TIME_MS) {
|
|
mdelay(SSP_V9RR_STABLE_TIME_MS - delay);
|
|
}
|
|
|
|
if (dev->gpio_se_sel >= 0) {
|
|
/* GPC1.0 set to 0 if controllable */
|
|
gpio_set_value(dev->gpio_se_sel, 0);
|
|
}
|
|
}
|
|
ssp_info(dev, "Secure NVM on\n");
|
|
return ret;
|
|
}
|
|
|
|
static int exynos_se_power_off(struct ssp_device *dev)
|
|
{
|
|
int ret = SSP_RET_OK;
|
|
|
|
ssp_info(dev, "Secure NVM off\n");
|
|
|
|
if (dev->is_k250) {
|
|
if (dev->gpio_se_sel >= 0) {
|
|
/* GPC1.0 set to 1 if controllable */
|
|
gpio_set_value(dev->gpio_se_sel, 1);
|
|
}
|
|
}
|
|
|
|
if (IS_ERR(dev->secure_nvm_ldo) || regulator_disable(dev->secure_nvm_ldo) < 0) {
|
|
ssp_err(dev, "%s - failed to disable LDO for SE\n", __func__);
|
|
return -ENODEV;
|
|
}
|
|
|
|
if (dev->is_k250) {
|
|
mdelay(SSP_K250_STABLE_TIME_MS);
|
|
} else {
|
|
mdelay(SSP_V9RR_STABLE_TIME_MS);
|
|
}
|
|
|
|
ssp_info(dev, "Secure NVM off done\n");
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int exynos_ssp_enable(struct ssp_device *sspdev)
|
|
{
|
|
int ret = SSP_RET_OK;
|
|
static int ssp_boot_flag;
|
|
|
|
++ssp_power_count;
|
|
|
|
if (ssp_power_count == 1) {
|
|
pm_stay_awake(sspdev->dev);
|
|
|
|
if (!ssp_boot_flag) {
|
|
ret = exynos_se_power_on_phase1(sspdev);
|
|
if (unlikely(ret))
|
|
goto ERR_OUT1;
|
|
|
|
/* step1: write FW base & size to mailbox for boot */
|
|
ret = exynos_ssp_boot_init(sspdev->dev);
|
|
if (unlikely(ret))
|
|
goto ERR_OUT2;
|
|
|
|
/* step2: power on */
|
|
ret = exynos_ssp_power_on(sspdev);
|
|
if (unlikely(ret))
|
|
goto ERR_OUT2;
|
|
|
|
/* step3: check booting status */
|
|
ret = exynos_ssp_boot_check(sspdev->dev);
|
|
if (unlikely(ret))
|
|
goto ERR_OUT3;
|
|
|
|
ret = exynos_se_power_on_phase2(sspdev);
|
|
if (unlikely(ret))
|
|
goto ERR_OUT3;
|
|
|
|
ssp_boot_flag = 1;
|
|
} else {
|
|
ret = exynos_se_power_on_phase1(sspdev);
|
|
if (unlikely(ret))
|
|
goto ERR_OUT1;
|
|
|
|
/* step1: write FW base & size to mailbox for boot */
|
|
ret = exynos_ssp_restore_init(sspdev->dev);
|
|
if (unlikely(ret))
|
|
goto ERR_OUT2;
|
|
|
|
/* step2: power on */
|
|
ret = exynos_ssp_power_on(sspdev);
|
|
if (unlikely(ret))
|
|
goto ERR_OUT2;
|
|
|
|
/* step3: check booting status */
|
|
ret = exynos_ssp_restore_check(sspdev->dev);
|
|
if (unlikely(ret))
|
|
goto ERR_OUT3;
|
|
|
|
ret = exynos_se_power_on_phase2(sspdev);
|
|
if (unlikely(ret))
|
|
goto ERR_OUT3;
|
|
}
|
|
}
|
|
|
|
exynos_ssp_powerctl_update(sspdev->dev, 1, 1);
|
|
ssp_info(sspdev->dev, "ssp enable: count: %d\n", ssp_power_count);
|
|
|
|
return ret;
|
|
|
|
ERR_OUT3:
|
|
exynos_ssp_power_off(sspdev);
|
|
|
|
ERR_OUT2:
|
|
exynos_se_power_off(sspdev);
|
|
|
|
ERR_OUT1:
|
|
exynos_ssp_powerctl_update(sspdev->dev, 1, 0);
|
|
pm_relax(sspdev->dev);
|
|
--ssp_power_count;
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int exynos_ssp_disable(struct ssp_device *sspdev)
|
|
{
|
|
int ret = SSP_RET_OK;
|
|
|
|
if (ssp_power_count <= 0) {
|
|
ssp_err(sspdev->dev, "%s ssp has already been disabled\n", __func__);
|
|
ssp_err(sspdev->dev, "ssp disable: count: %d\n", ssp_power_count);
|
|
return ret;
|
|
}
|
|
|
|
--ssp_power_count;
|
|
|
|
if (ssp_power_count == 0) {
|
|
ret = exynos_ssp_backup(sspdev->dev);
|
|
if (unlikely(ret))
|
|
goto ERR_OUT1;
|
|
|
|
ret = exynos_ssp_power_off(sspdev);
|
|
if (unlikely(ret))
|
|
goto ERR_OUT2;
|
|
|
|
ret = exynos_se_power_off(sspdev);
|
|
if (unlikely(ret))
|
|
goto ERR_OUT3;
|
|
|
|
/* keep the wake-up lock when above two functions fail */
|
|
/* for debugging purpose */
|
|
|
|
pm_relax(sspdev->dev);
|
|
}
|
|
|
|
exynos_ssp_powerctl_update(sspdev->dev, 0, 1);
|
|
ssp_info(sspdev->dev, "ssp disable: count: %d\n", ssp_power_count);
|
|
|
|
return ret;
|
|
|
|
ERR_OUT1:
|
|
exynos_ssp_power_off(sspdev);
|
|
|
|
ERR_OUT2:
|
|
exynos_se_power_off(sspdev);
|
|
|
|
ERR_OUT3:
|
|
exynos_ssp_powerctl_update(sspdev->dev, 0, 0);
|
|
pm_relax(sspdev->dev);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static long ssp_ioctl(struct file *filp, unsigned int cmd, unsigned long __arg)
|
|
{
|
|
int ret = SSP_RET_OK;
|
|
uint64_t test_mode;
|
|
char path[SSP_MAX_USER_PATH_LEN];
|
|
|
|
struct ssp_device *sspdev = filp->private_data;
|
|
void __user *arg = (void __user *)__arg;
|
|
|
|
ret = exynos_ssp_get_path(sspdev->dev, current, path);
|
|
if (ret) {
|
|
ssp_err(sspdev->dev, "%s: fail to get user path. ret = 0x%x\n", __func__, ret);
|
|
return ret;
|
|
}
|
|
|
|
ssp_info(sspdev->dev, "requested by %s\n", path);
|
|
|
|
mutex_lock(&ssp_ioctl_lock);
|
|
|
|
switch (cmd) {
|
|
case SSP_IOCTL_INIT:
|
|
ret = exynos_ssp_enable(sspdev);
|
|
if (ret != SSP_RET_OK)
|
|
exynos_ssp_print_user_list(sspdev->dev);
|
|
break;
|
|
case SSP_IOCTL_EXIT:
|
|
ret = exynos_ssp_disable(sspdev);
|
|
if (ret != SSP_RET_OK)
|
|
exynos_ssp_print_user_list(sspdev->dev);
|
|
break;
|
|
case SSP_IOCTL_TEST:
|
|
ret = get_user(test_mode, (uint64_t __user *)arg);
|
|
if (unlikely(ret)) {
|
|
ssp_err(sspdev->dev, "%s: fail to get_user. ret = 0x%x\n", __func__, ret);
|
|
break;
|
|
}
|
|
ret = exynos_ssp_self_test(sspdev->dev, test_mode);
|
|
break;
|
|
case SSP_IOCTL_DEBUG:
|
|
ssp_info(sspdev->dev, "power-on count: %d\n", ssp_power_count);
|
|
exynos_ssp_print_user_list(sspdev->dev);
|
|
break;
|
|
default:
|
|
ssp_err(sspdev->dev, "%s: invalid ioctl cmd: 0x%x\n", __func__, cmd);
|
|
ret = -EPERM;
|
|
break;
|
|
}
|
|
|
|
mutex_unlock(&ssp_ioctl_lock);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int ssp_open(struct inode *inode, struct file *file)
|
|
{
|
|
struct miscdevice *misc = file->private_data;
|
|
struct ssp_device *sspdev = container_of(misc,
|
|
struct ssp_device, misc_device);
|
|
|
|
file->private_data = sspdev;
|
|
|
|
ssp_info(sspdev->dev, "driver open is done\n");
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const struct file_operations ssp_fops = {
|
|
.owner = THIS_MODULE,
|
|
.open = ssp_open,
|
|
.unlocked_ioctl = ssp_ioctl,
|
|
.compat_ioctl = ssp_ioctl,
|
|
};
|
|
|
|
static int exynos_ssp_probe(struct platform_device *pdev)
|
|
{
|
|
int ret = SSP_RET_OK;
|
|
struct ssp_device *sspdev = NULL;
|
|
|
|
sspdev = kzalloc(sizeof(struct ssp_device), GFP_KERNEL);
|
|
if (!sspdev) {
|
|
ssp_err(&pdev->dev, "%s: fail to kzalloc.\n", __func__);
|
|
return -ENOMEM;
|
|
}
|
|
|
|
platform_set_drvdata(pdev, sspdev);
|
|
sspdev->dev = &pdev->dev;
|
|
|
|
spin_lock_init(&ssp_lock);
|
|
mutex_init(&ssp_ioctl_lock);
|
|
|
|
exynos_ssp_map_sfr(sspdev);
|
|
|
|
/* get regulator */
|
|
sspdev->secure_nvm_ldo = devm_regulator_get_optional(&(pdev->dev), "vdd_se");
|
|
if (IS_ERR(sspdev->secure_nvm_ldo)) {
|
|
ssp_err(sspdev->dev, "%s: failed to get regulator", __func__);
|
|
ret = -ENODEV;
|
|
goto err;
|
|
}
|
|
|
|
/* get SE_SEL GPIO number */
|
|
sspdev->gpio_se_sel = of_get_named_gpio(pdev->dev.of_node, "se_sel-gpios", 0);
|
|
|
|
/* get SNVM_RESET GPIO number */
|
|
sspdev->gpio_snvm_reset = of_get_named_gpio(pdev->dev.of_node, "snvm_reset-gpios", 0);
|
|
|
|
if (sspdev->gpio_se_sel < 0 || sspdev->gpio_snvm_reset < 0) {
|
|
/* If GPIO cannot be controlled, default is K250AF. */
|
|
ssp_info(sspdev->dev, "Default SE is K250AF.", __func__);
|
|
sspdev->is_k250 = 1;
|
|
} else {
|
|
/* request GPIO */
|
|
ssp_info(sspdev->dev, "se_sel gpio : %d\n", sspdev->gpio_se_sel);
|
|
if (devm_gpio_request(&pdev->dev, sspdev->gpio_se_sel, "se_sel") < 0) {
|
|
ssp_err(sspdev->dev, "%s: failed to request se_sel gpio", __func__);
|
|
ret = -ENODEV;
|
|
goto err;
|
|
}
|
|
|
|
/* change GPIO direction to output */
|
|
if (gpio_direction_output(sspdev->gpio_se_sel, 0) < 0) {
|
|
ssp_err(sspdev->dev, "%s: failed to set gpio_output for SE_SEL\n", __func__);
|
|
ret = -ENODEV;
|
|
goto err;
|
|
}
|
|
|
|
/* request GPIO */
|
|
ssp_info(sspdev->dev, "snvm_reset gpio : %d\n", sspdev->gpio_snvm_reset);
|
|
if (devm_gpio_request(&pdev->dev, sspdev->gpio_snvm_reset, "snvm_reset") < 0) {
|
|
ssp_err(sspdev->dev, "%s: failed to request snvm_reset gpio", __func__);
|
|
ret = -ENODEV;
|
|
goto err;
|
|
}
|
|
|
|
/* change GPIO direction to output */
|
|
if (gpio_direction_output(sspdev->gpio_snvm_reset, 0) < 0) {
|
|
ssp_err(sspdev->dev, "%s: failed to set gpio_output for reset\n", __func__);
|
|
ret = -ENODEV;
|
|
goto err;
|
|
}
|
|
|
|
/* set SE to K250 */
|
|
sspdev->is_k250 = 1;
|
|
}
|
|
|
|
/* enable runtime PM */
|
|
exynos_ssp_pm_enable(sspdev);
|
|
ssp_idle_ip_index = exynos_get_idle_ip_index(dev_name(sspdev->dev), 1);
|
|
exynos_update_ip_idle_status(ssp_idle_ip_index, 1);
|
|
|
|
/* set misc driver */
|
|
memset((void *)&sspdev->misc_device, 0, sizeof(struct miscdevice));
|
|
sspdev->misc_device.minor = MISC_DYNAMIC_MINOR;
|
|
sspdev->misc_device.name = "ssp";
|
|
sspdev->misc_device.fops = &ssp_fops;
|
|
ret = misc_register(&sspdev->misc_device);
|
|
if (ret) {
|
|
ssp_err(sspdev->dev, "%s: fail to misc_register. ret = %d\n", __func__, ret);
|
|
ret = -ENOMEM;
|
|
goto err;
|
|
}
|
|
|
|
ret = device_init_wakeup(sspdev->dev, true);
|
|
if (ret) {
|
|
ssp_err(sspdev->dev, "%s: fail to init wakeup. ret = %d\n", __func__, ret);
|
|
goto err;
|
|
}
|
|
|
|
return SSP_RET_OK;
|
|
err:
|
|
kfree(sspdev);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int exynos_ssp_remove(struct platform_device *pdev)
|
|
{
|
|
struct ssp_device *sspdev = platform_get_drvdata(pdev);
|
|
|
|
misc_deregister(&sspdev->misc_device);
|
|
kfree(sspdev);
|
|
|
|
return 0;
|
|
}
|
|
|
|
#if defined(CONFIG_PM_SLEEP) || defined(CONFIG_PM_RUNTIME)
|
|
static int exynos_ssp_suspend(struct device *dev)
|
|
{
|
|
int ret = SSP_RET_OK;
|
|
|
|
/* Do nothing */
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int exynos_ssp_resume(struct device *dev)
|
|
{
|
|
int ret = SSP_RET_OK;
|
|
|
|
/* Do nothing */
|
|
|
|
return ret;
|
|
}
|
|
#endif
|
|
|
|
static struct dev_pm_ops exynos_ssp_pm_ops = {
|
|
SET_SYSTEM_SLEEP_PM_OPS(exynos_ssp_suspend,
|
|
exynos_ssp_resume)
|
|
};
|
|
|
|
#ifdef CONFIG_OF
|
|
static const struct of_device_id exynos_ssp_match[] = {
|
|
{
|
|
.compatible = "samsung,exynos-ssp",
|
|
},
|
|
{},
|
|
};
|
|
#endif
|
|
|
|
static struct platform_driver exynos_ssp_driver = {
|
|
.probe = exynos_ssp_probe,
|
|
.remove = exynos_ssp_remove,
|
|
.driver = {
|
|
.name = "ssp",
|
|
.owner = THIS_MODULE,
|
|
.pm = &exynos_ssp_pm_ops,
|
|
#ifdef CONFIG_OF
|
|
.of_match_table = exynos_ssp_match,
|
|
#endif
|
|
},
|
|
};
|
|
|
|
static int __init exynos_ssp_init(void)
|
|
{
|
|
int ret = SSP_RET_OK;
|
|
|
|
ret = platform_driver_register(&exynos_ssp_driver);
|
|
if (ret) {
|
|
pr_err("[Exynos][CMSSP] %s: fail to register driver. ret = 0x%x\n", __func__, ret);
|
|
return ret;
|
|
}
|
|
|
|
pr_info("[Exynos][CMSSP] driver, (c) 2019 Samsung Electronics\n");
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void __exit exynos_ssp_exit(void)
|
|
{
|
|
platform_driver_unregister(&exynos_ssp_driver);
|
|
}
|
|
|
|
module_init(exynos_ssp_init);
|
|
module_exit(exynos_ssp_exit);
|
|
|
|
MODULE_DESCRIPTION("EXYNOS Samsung Secure Platform driver");
|
|
MODULE_AUTHOR("Keunyoung Park <keun0.park@samsung.com>");
|
|
MODULE_LICENSE("GPL");
|