6db4831e98
Android 14
647 lines
15 KiB
C
647 lines
15 KiB
C
// SPDX-License-Identifier: GPL-2.0
|
|
/*
|
|
* Copyright (c) 2020 MediaTek Inc.
|
|
*/
|
|
|
|
#include <linux/module.h>
|
|
#include <linux/kernel.h>
|
|
#include <linux/device.h>
|
|
#include <linux/platform_device.h>
|
|
#include <linux/init.h>
|
|
#include <linux/io.h>
|
|
#include <linux/interrupt.h>
|
|
#include <linux/of_irq.h>
|
|
#include <linux/of_address.h>
|
|
#include <linux/of_device.h>
|
|
#include <linux/debugfs.h>
|
|
#include <linux/random.h>
|
|
#include <linux/slab.h>
|
|
#if defined(CONFIG_MTK_AEE_FEATURE)
|
|
#include <mt-plat/aee.h>
|
|
#endif
|
|
#include "dapc.h"
|
|
#include "dapc_cfg.h"
|
|
#include "apusys_power.h"
|
|
|
|
struct dapc_driver *dapc_drv;
|
|
|
|
#ifdef CONFIG_MTK_ENG_BUILD
|
|
/* apusys devapc debug file operations */
|
|
static int apusys_devapc_open(struct inode *inode, struct file *file)
|
|
{
|
|
file->private_data = inode->i_private;
|
|
return nonseekable_open(inode, file);
|
|
}
|
|
|
|
static int apusys_devapc_release(struct inode *inode, struct file *file)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
static int trigger_vio_test(struct dapc_driver *drv, int round)
|
|
{
|
|
phys_addr_t test_base = drv->cfg->ut_base;
|
|
void __iomem *test_virt;
|
|
int i, num, flag_ke;
|
|
|
|
if (round <= 0) {
|
|
pr_info("invalid round %d\n", round);
|
|
return -EINVAL;
|
|
}
|
|
|
|
test_virt = ioremap(test_base, 4096);
|
|
if (!test_virt) {
|
|
pr_info("ioremap test base(0x%lx) virt failed\n",
|
|
(unsigned long)test_base);
|
|
return -ENOMEM;
|
|
}
|
|
|
|
/* do not trigger KE while testing */
|
|
flag_ke = drv->enable_ke;
|
|
drv->enable_ke = 0;
|
|
|
|
for (i = 0; i < round; i++) {
|
|
get_random_bytes(&num, sizeof(num));
|
|
num &= 0xF;
|
|
/* mdla_core0 0x8 */
|
|
iowrite32(num, (unsigned int *)(test_virt + 0x8));
|
|
pr_info("write 0x%x to (0x%lx), read: 0x%x\n",
|
|
num, (unsigned long)(test_base + 0x8),
|
|
readl((unsigned int *)(test_virt + 0x8)));
|
|
|
|
get_random_bytes(&num, sizeof(num));
|
|
num &= 0xF;
|
|
/* mdla_core1 0xC */
|
|
iowrite32(num, (unsigned int *)(test_virt + 0xC));
|
|
pr_info("write 0x%x to (0x%lx), read: 0x%x\n",
|
|
num, (unsigned long)(test_base + 0xC),
|
|
readl((unsigned int *)(test_virt + 0xC)));
|
|
}
|
|
|
|
iounmap(test_virt);
|
|
|
|
drv->enable_ke = flag_ke;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static ssize_t apusys_devapc_read(struct file *file, char __user *buf,
|
|
size_t size, loff_t *offset)
|
|
{
|
|
struct dapc_driver *drv = (struct dapc_driver *)file->private_data;
|
|
char output[64] = {0};
|
|
int ret, len;
|
|
|
|
if (!buf)
|
|
return -EINVAL;
|
|
|
|
ret = snprintf(output, sizeof(output),
|
|
"%s enable_KE: %d, enable_AEE: %d, enable_IRQ: %d\n",
|
|
"[APUSYS_DEVAPC]", drv->enable_ke, drv->enable_aee,
|
|
drv->enable_irq);
|
|
|
|
if (ret <= 0)
|
|
return 0;
|
|
|
|
len = min((int)size, (int)(strlen(output) - *offset));
|
|
if (len <= 0)
|
|
return 0;
|
|
|
|
ret = copy_to_user(buf, output, len);
|
|
if (ret) {
|
|
pr_info("Fail to copy %s, ret %d\n", output, ret);
|
|
return -EFAULT;
|
|
}
|
|
|
|
*offset += len;
|
|
|
|
return len;
|
|
}
|
|
|
|
static void apusys_devapc_start(void *data);
|
|
|
|
static ssize_t apusys_devapc_write(struct file *file, const char __user *buf,
|
|
size_t size, loff_t *ppos)
|
|
{
|
|
struct dapc_driver *drv = (struct dapc_driver *)file->private_data;
|
|
char input[32] = {0};
|
|
char *cmd_str, *param_str, *tmp_str;
|
|
unsigned int param = 0;
|
|
size_t len;
|
|
int ret;
|
|
|
|
if (!buf)
|
|
return -EINVAL;
|
|
|
|
len = min(size, 31UL);
|
|
|
|
if (copy_from_user(input, buf, len)) {
|
|
pr_info("Fail to copy from user\n");
|
|
return -EFAULT;
|
|
}
|
|
|
|
input[len] = '\0';
|
|
tmp_str = input;
|
|
|
|
cmd_str = strsep(&tmp_str, " ");
|
|
if (!cmd_str) {
|
|
pr_info("Fail to get cmd\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
param_str = strsep(&tmp_str, " ");
|
|
if (!param_str) {
|
|
pr_info("Fail to get param\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
ret = kstrtouint(param_str, 10, ¶m);
|
|
|
|
if (ret)
|
|
return ret;
|
|
|
|
if (!strncmp(cmd_str, "enable_UT", sizeof("enable_UT"))) {
|
|
if (param & 0x1) {
|
|
drv->enable_ke = 0;
|
|
drv->enable_aee = 0;
|
|
drv->enable_irq = 1;
|
|
pr_info("APUSYS devapc debugging mode\n");
|
|
} else {
|
|
drv->enable_ke = 1;
|
|
drv->enable_aee = 1;
|
|
drv->enable_irq = 1;
|
|
pr_info("APUSYS devapc violation mode\n");
|
|
}
|
|
} else if (!strncmp(cmd_str, "enable_KE", sizeof("enable_KE"))) {
|
|
drv->enable_ke = param & 0x1;
|
|
pr_info("APUSYS devapc %s KE\n",
|
|
drv->enable_ke ? "enable" : "disable");
|
|
} else if (!strncmp(cmd_str, "enable_AEE", sizeof("enable_AEE"))) {
|
|
drv->enable_aee = param & 0x1;
|
|
pr_info("APUSYS devapc %s AEE\n",
|
|
drv->enable_aee ? "enable" : "disable");
|
|
} else if (!strncmp(cmd_str, "enable_IRQ", sizeof("enable_IRQ"))) {
|
|
drv->enable_irq = param & 0x1;
|
|
if (drv->enable_irq)
|
|
enable_irq(drv->irq);
|
|
else
|
|
disable_irq(drv->irq);
|
|
pr_info("APUSYS devapc %s IRQ\n",
|
|
drv->enable_irq ? "enable" : "disable");
|
|
} else if (!strncmp(cmd_str, "restart", sizeof("restart"))) {
|
|
apusys_devapc_start(NULL);
|
|
pr_info("APUSYS devapc restarted\n");
|
|
} else if (!strncmp(cmd_str, "trigger_vio", sizeof("trigger_vio"))) {
|
|
pr_info("APUSYS devapc trigger vio test %d +\n", param);
|
|
trigger_vio_test(drv, param);
|
|
pr_info("APUSYS devapc trigger vio test %d -\n", param);
|
|
} else {
|
|
pr_info("Unknown cmd %s\n", cmd_str);
|
|
return -EINVAL;
|
|
}
|
|
|
|
return len;
|
|
}
|
|
|
|
static const struct file_operations apusys_devapc_fops = {
|
|
.owner = THIS_MODULE,
|
|
.open = apusys_devapc_open,
|
|
.read = apusys_devapc_read,
|
|
.write = apusys_devapc_write,
|
|
.release = apusys_devapc_release,
|
|
};
|
|
/* apusys devapc debug file operations */
|
|
|
|
static int apusys_devapc_debug_init(struct dapc_driver *drv)
|
|
{
|
|
struct dentry *droot = NULL;
|
|
int ret = 0;
|
|
|
|
droot = debugfs_create_dir("apusys_devapc", NULL);
|
|
if (IS_ERR_OR_NULL(droot)) {
|
|
ret = PTR_ERR(droot);
|
|
pr_info("%s: unable to create debugfs folder: %d\n",
|
|
__func__, ret);
|
|
return ret;
|
|
}
|
|
debugfs_create_u32("debug",
|
|
0644, droot, &drv->debug_log);
|
|
debugfs_create_file("apusys_devapc", 0644,
|
|
droot, drv, &apusys_devapc_fops);
|
|
|
|
drv->droot = droot;
|
|
return ret;
|
|
}
|
|
|
|
static void apusys_devapc_debug_exit(struct dapc_driver *drv)
|
|
{
|
|
debugfs_remove_recursive(drv->droot);
|
|
drv->droot = NULL;
|
|
}
|
|
#else
|
|
static int apusys_devapc_debug_init(struct dapc_driver *drv)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
static void apusys_devapc_debug_exit(struct dapc_driver *drv)
|
|
{
|
|
}
|
|
#endif
|
|
|
|
static void slv_irq(unsigned int slv, bool enable)
|
|
{
|
|
uint32_t reg;
|
|
uint32_t mask;
|
|
struct dapc_driver *d = dapc_drv;
|
|
struct dapc_config *cfg = d->cfg;
|
|
|
|
if (slv > cfg->slv_cnt) {
|
|
pr_info("%s: slv: %d is out of index, max: %d\n",
|
|
slv, cfg->slv_cnt);
|
|
return;
|
|
}
|
|
|
|
reg = cfg->vio_mask(slv / cfg->slv_per_dapc);
|
|
mask = 1 << (slv % cfg->slv_per_dapc);
|
|
|
|
if (enable)
|
|
dapc_reg_clr(d, reg, mask);
|
|
else
|
|
dapc_reg_set(d, reg, mask);
|
|
}
|
|
|
|
static void do_kernel_exception(struct dapc_driver *drv, unsigned int i,
|
|
const char *slv_name,
|
|
struct dapc_exception *ex)
|
|
{
|
|
/* mask irq for slv "i" */
|
|
slv_irq(i, false);
|
|
|
|
if (drv->enable_ke) {
|
|
WARN_ON(1);
|
|
goto out;
|
|
}
|
|
|
|
#if defined(CONFIG_MTK_AEE_FEATURE)
|
|
if (drv->enable_aee) {
|
|
aee_kernel_exception("APUSYS_DEVAPC",
|
|
"Violation Slave: %s (%s%s): transaction ID:0x%x, Addr:0x%x, HighAddr: %x, Domain: 0x%x\n",
|
|
slv_name,
|
|
(ex->read_vio) ? "R" : "",
|
|
(ex->write_vio) ? " W" : "",
|
|
ex->trans_id,
|
|
ex->addr,
|
|
ex->addr_high,
|
|
ex->domain_id);
|
|
}
|
|
#endif
|
|
|
|
out:
|
|
/* unmask irq for slv "i" */
|
|
slv_irq(i, true);
|
|
}
|
|
|
|
static uint32_t check_vio_status(unsigned int slv)
|
|
{
|
|
uint32_t reg;
|
|
uint32_t mask;
|
|
struct dapc_driver *d = dapc_drv;
|
|
struct dapc_config *cfg = d->cfg;
|
|
|
|
if (slv > cfg->slv_cnt) {
|
|
pr_info("%s: slv: %d is out of index, max: %d\n",
|
|
slv, cfg->slv_cnt);
|
|
return -EINVAL;
|
|
}
|
|
|
|
reg = cfg->vio_sta(slv / cfg->slv_per_dapc);
|
|
mask = 0x1 << (slv % cfg->slv_per_dapc);
|
|
|
|
return dapc_reg_r(d, reg) & mask;
|
|
}
|
|
|
|
static void clear_vio_status(unsigned int slv)
|
|
{
|
|
uint32_t reg;
|
|
uint32_t mask;
|
|
struct dapc_driver *d = dapc_drv;
|
|
struct dapc_config *cfg = d->cfg;
|
|
|
|
if (slv > cfg->slv_cnt) {
|
|
pr_info("%s: slv: %d is out of index, max: %d\n",
|
|
slv, cfg->slv_cnt);
|
|
return;
|
|
}
|
|
|
|
reg = cfg->vio_sta(slv / cfg->slv_per_dapc);
|
|
mask = 0x1 << (slv % cfg->slv_per_dapc);
|
|
|
|
dapc_reg_w(d, reg, mask);
|
|
}
|
|
|
|
static int shift_vio_dbg(int shift_bit)
|
|
{
|
|
int count = 0;
|
|
struct dapc_driver *d = dapc_drv;
|
|
struct dapc_config *cfg = d->cfg;
|
|
uint32_t sel = cfg->vio_shift_sel;
|
|
uint32_t con = cfg->vio_shift_con;
|
|
uint32_t mask = cfg->vio_shift_con_mask;
|
|
|
|
/* start shift */
|
|
dapc_reg_w(d, sel, (0x1 << shift_bit));
|
|
dapc_reg_w(d, con, 0x1);
|
|
|
|
/* wait for shift done */
|
|
while (((dapc_reg_r(d, con) & mask) != mask) && (count < 100))
|
|
count++;
|
|
|
|
pr_debug("%s: shift done %d, %d, SHIFT_SEL: 0x%x, SHIFT_CON=0x%x\n",
|
|
__func__, shift_bit, count,
|
|
dapc_reg_r(d, sel), dapc_reg_r(d, con));
|
|
|
|
if ((dapc_reg_r(d, con) & mask) != mask) {
|
|
pr_info("%s: shift bit %d failed\n", shift_bit);
|
|
return -EFAULT;
|
|
}
|
|
|
|
/* disable shift mechanism */
|
|
dapc_reg_w(d, con, 0);
|
|
dapc_reg_w(d, sel, 0);
|
|
/* SHIFT_STA must be write-cleared before clear VIO_STA */
|
|
dapc_reg_w(d, cfg->vio_shift_sta, (0x1 << shift_bit));
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void apusys_devapc_dbg(const char *prefix)
|
|
{
|
|
struct dapc_driver *d = dapc_drv;
|
|
struct dapc_config *cfg = d->cfg;
|
|
|
|
if (!dapc_drv->debug_log)
|
|
return;
|
|
|
|
pr_info("%s: %s: VIO_MASK0~3: 0x%x, 0x%x, 0x%x, 0x%x, VIO_STA0~3: 0x%x, 0x%x, 0x%x, 0x%x\n",
|
|
__func__,
|
|
prefix,
|
|
dapc_reg_r(d, cfg->vio_mask(0)),
|
|
dapc_reg_r(d, cfg->vio_mask(1)),
|
|
dapc_reg_r(d, cfg->vio_mask(2)),
|
|
dapc_reg_r(d, cfg->vio_mask(3)),
|
|
dapc_reg_r(d, cfg->vio_sta(0)),
|
|
dapc_reg_r(d, cfg->vio_sta(1)),
|
|
dapc_reg_r(d, cfg->vio_sta(2)),
|
|
dapc_reg_r(d, cfg->vio_sta(3)));
|
|
}
|
|
|
|
/* Return 1 means there are some violations */
|
|
static inline int is_violation_irq(void)
|
|
{
|
|
struct dapc_driver *d = dapc_drv;
|
|
struct dapc_config *cfg = d->cfg;
|
|
|
|
return (dapc_reg_r(d, cfg->vio_sta(0)) ||
|
|
dapc_reg_r(d, cfg->vio_sta(1)) ||
|
|
dapc_reg_r(d, cfg->vio_sta(2)) ||
|
|
dapc_reg_r(d, cfg->vio_sta(3)));
|
|
}
|
|
|
|
static irqreturn_t apusys_devapc_isr(int irq_number, void *data)
|
|
{
|
|
int i;
|
|
struct dapc_driver *d = (struct dapc_driver *)data;
|
|
struct dapc_config *cfg;
|
|
unsigned int shift_max;
|
|
struct dapc_slave *slv;
|
|
struct dapc_exception ex;
|
|
|
|
if (!is_violation_irq())
|
|
return IRQ_NONE;
|
|
|
|
if (!d || IS_ERR_OR_NULL(d->reg)) {
|
|
pr_info("%s: driver abort\n", __func__);
|
|
return IRQ_NONE;
|
|
}
|
|
|
|
cfg = d->cfg;
|
|
slv = cfg->slv;
|
|
shift_max = cfg->vio_shift_max_bit;
|
|
|
|
if (irq_number != d->irq) {
|
|
pr_info("%s: get unknown irq %d\n", __func__, irq_number);
|
|
return IRQ_NONE;
|
|
}
|
|
|
|
if (!d->irq) {
|
|
pr_info("%s: Disable vio irq handler\n", __func__);
|
|
return IRQ_NONE;
|
|
}
|
|
|
|
memset(&ex, 0, sizeof(struct dapc_exception));
|
|
disable_irq_nosync(irq_number);
|
|
apusys_devapc_dbg("ISR begin");
|
|
|
|
for (i = 0; i <= shift_max; ++i) {
|
|
if (!(dapc_reg_r(d, cfg->vio_shift_sta) & (0x1 << i)))
|
|
continue;
|
|
if (shift_vio_dbg(i))
|
|
continue;
|
|
|
|
pr_info("%s: VIO_SHIFT_STA=0x%x, VIO_SHIFT_SEL=0x%x, VIO_SHIFT_CON=0x%x,\n",
|
|
__func__,
|
|
dapc_reg_r(d, cfg->vio_shift_sta),
|
|
dapc_reg_r(d, cfg->vio_shift_sel),
|
|
dapc_reg_r(d, cfg->vio_shift_con));
|
|
|
|
cfg->excp_info(d, &ex);
|
|
|
|
/* violation information */
|
|
pr_info("%s: Violation(%s%s): transaction ID:0x%x, Addr:0x%x, HighAddr: %x, Domain: 0x%x\n",
|
|
__func__,
|
|
(ex.read_vio) ? "R" : "",
|
|
(ex.write_vio) ? " W" : "",
|
|
ex.trans_id,
|
|
ex.addr,
|
|
ex.addr_high,
|
|
ex.domain_id);
|
|
}
|
|
|
|
/* Clear VIO_STA */
|
|
for_each_dapc_slv(cfg, i) {
|
|
if (!(slv[i].vio_irq_en && check_vio_status(i)))
|
|
continue;
|
|
|
|
clear_vio_status(i);
|
|
pr_info("%s: vio_sta device: %d, slave: %s\n",
|
|
__func__, i, slv[i].name);
|
|
do_kernel_exception(d, i, slv[i].name, &ex);
|
|
}
|
|
|
|
apusys_devapc_dbg("ISR end");
|
|
enable_irq(irq_number);
|
|
|
|
return IRQ_HANDLED;
|
|
}
|
|
|
|
static void apusys_devapc_start(void *data)
|
|
{
|
|
int i = 0;
|
|
struct dapc_driver *d = dapc_drv;
|
|
struct dapc_config *cfg = d->cfg;
|
|
struct dapc_slave *slv = cfg->slv;
|
|
|
|
/* Clear APC violation status */
|
|
dapc_reg_w(d, cfg->apc_con, cfg->apc_con_vio);
|
|
apusys_devapc_dbg("Init begin");
|
|
|
|
/* Enable violation IRQ */
|
|
for_each_dapc_slv(cfg, i) {
|
|
if (!slv[i].vio_irq_en)
|
|
continue;
|
|
|
|
clear_vio_status(i);
|
|
slv_irq(i, true);
|
|
}
|
|
apusys_devapc_dbg("Init end");
|
|
}
|
|
|
|
static int apusys_devapc_probe(struct platform_device *pdev)
|
|
{
|
|
int ret = 0;
|
|
struct device_node *devapc_node;
|
|
struct dapc_driver *drv;
|
|
struct dapc_config *cfg;
|
|
|
|
dev_info(&pdev->dev, "%s +\n", __func__);
|
|
|
|
if (!apusys_power_check())
|
|
return 0;
|
|
|
|
devapc_node = pdev->dev.of_node;
|
|
if (!devapc_node) {
|
|
dev_info(&pdev->dev, "of_node required\n");
|
|
return -ENODEV;
|
|
}
|
|
|
|
if (dapc_drv) {
|
|
dev_info(&pdev->dev, "duplicated device node\n");
|
|
return -ENODEV;
|
|
}
|
|
|
|
cfg = (struct dapc_config *)of_device_get_match_data(&pdev->dev);
|
|
if (!cfg) {
|
|
dev_info(&pdev->dev, "unsupported device: %s\n", pdev->name);
|
|
return -ENODEV;
|
|
}
|
|
|
|
drv = kzalloc(sizeof(struct dapc_driver), GFP_KERNEL);
|
|
if (!drv) {
|
|
ret = -ENOMEM;
|
|
goto err_allocate;
|
|
}
|
|
dapc_drv = drv;
|
|
|
|
drv->cfg = cfg;
|
|
drv->reg = of_iomap(devapc_node, 0);
|
|
if (!drv->reg) {
|
|
ret = -ENOMEM;
|
|
goto err_iomap;
|
|
}
|
|
|
|
drv->irq = irq_of_parse_and_map(devapc_node, 0);
|
|
|
|
ret = request_irq(drv->irq, apusys_devapc_isr,
|
|
irq_get_trigger_type(drv->irq) | IRQF_SHARED,
|
|
"apusys-devapc", drv);
|
|
if (ret) {
|
|
ret = -EFAULT;
|
|
goto err_request_irq;
|
|
}
|
|
|
|
drv->enable_ke = drv->enable_aee = 1;
|
|
|
|
if (drv->cfg->irq_enable) {
|
|
drv->enable_irq = 1;
|
|
} else {
|
|
drv->enable_irq = 0;
|
|
disable_irq(drv->irq);
|
|
}
|
|
|
|
apusys_devapc_start(NULL);
|
|
ret = apu_power_callback_device_register(DEVAPC,
|
|
apusys_devapc_start, NULL);
|
|
if (ret)
|
|
dev_info(&pdev->dev,
|
|
"unable to register to apu power: %d\n", ret);
|
|
|
|
drv->debug_log = 0;
|
|
apusys_devapc_debug_init(drv);
|
|
platform_set_drvdata(pdev, drv);
|
|
dev_info(&pdev->dev, "%s -\n", __func__);
|
|
|
|
return 0;
|
|
|
|
err_request_irq:
|
|
iounmap(drv->reg);
|
|
err_iomap:
|
|
kfree(drv);
|
|
err_allocate:
|
|
dapc_drv = NULL;
|
|
return ret;
|
|
}
|
|
|
|
static int apusys_devapc_remove(struct platform_device *pdev)
|
|
{
|
|
struct dapc_driver *drv = platform_get_drvdata(pdev);
|
|
|
|
dev_dbg(&pdev->dev, "%s +\n", __func__);
|
|
|
|
free_irq(drv->irq, drv);
|
|
iounmap(drv->reg);
|
|
apusys_devapc_debug_exit(drv);
|
|
apu_power_callback_device_unregister(DEVAPC);
|
|
kfree(drv);
|
|
dev_dbg(&pdev->dev, "%s -\n", __func__);
|
|
dapc_drv = NULL;
|
|
return 0;
|
|
}
|
|
|
|
static const struct of_device_id apusys_devapc_of_match[] = {
|
|
{ .compatible = "mediatek,mt6885-apusys_devapc", &dapc_cfg_mt6885 },
|
|
{ .compatible = "mediatek,mt6873-apusys_devapc", &dapc_cfg_mt6873 },
|
|
{ .compatible = "mediatek,mt6853-apusys_devapc", &dapc_cfg_mt6853 },
|
|
{ .compatible = "mediatek,mt6893-apusys_devapc", &dapc_cfg_mt6885 },
|
|
{ .compatible = "mediatek,mt6877-apusys_devapc", &dapc_cfg_mt6877 },
|
|
{},
|
|
};
|
|
|
|
static struct platform_driver apusys_devapc_driver = {
|
|
.probe = apusys_devapc_probe,
|
|
.remove = apusys_devapc_remove,
|
|
.driver = {
|
|
.name = "apusys-devapc",
|
|
.owner = THIS_MODULE,
|
|
.of_match_table = apusys_devapc_of_match,
|
|
},
|
|
};
|
|
|
|
static int __init apusys_devapc_init(void)
|
|
{
|
|
dapc_drv = NULL;
|
|
return platform_driver_register(&apusys_devapc_driver);
|
|
}
|
|
|
|
static void __exit apusys_devapc_exit(void)
|
|
{
|
|
platform_driver_unregister(&apusys_devapc_driver);
|
|
}
|
|
|
|
late_initcall(apusys_devapc_init);
|
|
module_exit(apusys_devapc_exit);
|
|
|
|
MODULE_DESCRIPTION("Mediatek APUSYS DEVAPC Driver");
|
|
MODULE_AUTHOR("Mediatek");
|
|
MODULE_LICENSE("GPL");
|