kernel_samsung_a34x-permissive/drivers/misc/mediatek/sda/cache-parity.c
2024-04-28 15:51:13 +02:00

807 lines
19 KiB
C

// SPDX-License-Identifier: GPL-2.0
/*
* Copyright (c) 2019 MediaTek Inc.
*/
#include <asm/cputype.h>
#include <linux/atomic.h>
#include <linux/bug.h>
#include <linux/delay.h>
#include <linux/device.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/irqreturn.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/of_address.h>
#include <linux/of_device.h>
#include <linux/of_irq.h>
#include <linux/platform_device.h>
#include <linux/printk.h>
#include <linux/sched/clock.h>
#include <linux/workqueue.h>
#include <mt-plat/aee.h>
#define ECC_UE_BIT (0x1 << 29)
#define ECC_CE_BIT (0x3 << 24)
#define ECC_DE_BIT (0x1 << 23)
#define ECC_SERR_BIT (0x1F)
#define ECC_CE_AT_LEAST_ONE_ERR (0x2 << 24)
#define ECC_SERR_FROM_DATA_BUFF (0x2)
#define ECC_IRQ_TRIGGER_THRESHOLD (1)
#define ECC_SEL_DSU_MODE (0x0)
#define ECC_SEL_L1_MODE (0x1)
#define ECC_SEL_L2_MODE (0x2)
#define ECC_SEL_DSU_MODE_LGY (0x1)
#define ECC_SEL_L1_MODE_LGY (0x0)
//FIXME: delete define when MIDR upstream
#define KLN_CPU_ID_MASK (0xD46)
#define MTH_CPU_ID_MASK (0xD47)
#define MTHELP_CPU_ID_MASK (0xD48)
struct parity_record_t {
unsigned int check_offset;
unsigned int check_mask;
unsigned int dump_offset;
unsigned int dump_length;
unsigned int clear_offset;
unsigned int clear_mask;
};
struct parity_irq_record_t {
int irq;
struct parity_record_t parity_record;
};
struct parity_irq_config_t {
unsigned int target_cpu;
struct parity_record_t parity_record;
};
union err_record {
struct _v1 {
u32 irq;
u32 status;
bool is_err;
} v1;
struct _v2 {
u32 irq;
int cpu;
u64 misc0_el1;
u64 misc0_el1_L1;
u64 misc0_el1_L2;
u64 status_el1;
u64 status_el1_L1;
u64 status_el1_L2;
u64 sctlr_el1;
u64 sctlr_el1_L1;
u64 sctlr_el1_L2;
bool is_err;
} v2;
};
struct cache_parity {
struct work_struct work;
/* setting from device tree */
unsigned int ver;
unsigned int nr_irq;
int ecc_irq_support;
int arm_dsu_ecc_hwirq;
void __iomem *cache_parity_base;
/* recorded parity errors */
atomic_t nr_err;
u64 timestampe;
union err_record *record;
};
struct mtk_cache_parity_compatible {
const unsigned int ver;
};
static const struct mtk_cache_parity_compatible mt6785_compat = {
.ver = 1,
};
static const struct mtk_cache_parity_compatible mt6873_compat = {
.ver = 2,
};
#define ECC_LOG(fmt, ...) \
do { \
pr_notice(fmt, __VA_ARGS__); \
aee_sram_printk(fmt, __VA_ARGS__); \
} while (0)
static struct cache_parity cache_parity;
static struct parity_irq_record_t *parity_irq_record;
static DEFINE_SPINLOCK(parity_isr_lock);
void __attribute__((weak)) ecc_dump_debug_info(void)
{
pr_notice("%s is not implemented\n", __func__);
}
static ssize_t cache_status_show(struct device_driver *driver, char *buf)
{
unsigned int nr_err;
nr_err = atomic_read(&cache_parity.nr_err);
if (nr_err)
return snprintf(buf, PAGE_SIZE, "True, %u times (%llu ns)\n",
nr_err, cache_parity.timestampe);
else
return snprintf(buf, PAGE_SIZE, "False\n");
}
static DRIVER_ATTR_RO(cache_status);
static void cache_parity_irq_work(struct work_struct *w)
{
static char *buf;
int n, i;
u64 status;
if (!buf) {
buf = kmalloc(PAGE_SIZE, GFP_KERNEL);
if (!buf)
goto call_aee;
}
n = 0;
if (cache_parity.ver == 1) {
n += snprintf(buf + n, PAGE_SIZE - n, "cache parity error,");
if (n > PAGE_SIZE)
goto call_aee;
for (i = 0; i < cache_parity.nr_irq; i++) {
if (!cache_parity.record[i].v1.is_err)
continue;
n += snprintf(buf + n, PAGE_SIZE - n, "%s:%d, %s:0x%x ",
"irq", cache_parity.record[i].v1.irq,
"status", cache_parity.record[i].v1.status);
if (n > PAGE_SIZE)
goto call_aee;
}
} else if (cache_parity.ver == 2) {
n += snprintf(buf + n, PAGE_SIZE - n, "ECC errors(");
if (n > PAGE_SIZE)
goto call_aee;
for (status = 0, i = 0; i < cache_parity.nr_irq; i++) {
if (cache_parity.record[i].v2.is_err) {
status = cache_parity.record[i].v2.status_el1;
break;
}
}
if (status & ECC_UE_BIT)
n += snprintf(buf + n, PAGE_SIZE - n, "UE");
else if (status & ECC_CE_BIT)
n += snprintf(buf + n, PAGE_SIZE - n, "CE");
else if (status & ECC_DE_BIT)
n += snprintf(buf + n, PAGE_SIZE - n, "DE");
else
n += snprintf(buf + n, PAGE_SIZE - n, "NA");
if (n > PAGE_SIZE)
goto call_aee;
n += snprintf(buf + n, PAGE_SIZE - n, "),");
if (n > PAGE_SIZE)
goto call_aee;
for (i = 0; i < cache_parity.nr_irq; i++) {
if (!cache_parity.record[i].v2.is_err)
continue;
n += snprintf(buf + n, PAGE_SIZE - n,
"%s:%d,%s:0x%016llx,%s:0x%016llx ",
"irq", cache_parity.record[i].v2.irq,
"misc0_el1",
cache_parity.record[i].v2.misc0_el1,
"status_el1",
cache_parity.record[i].v2.status_el1);
if (n > PAGE_SIZE)
goto call_aee;
}
} else {
pr_debug("Unknown Cache Error Irq\n");
}
call_aee:
aee_kernel_exception("cache parity", buf,
"CRDISPATCH_KEY:Cache Parity Issue");
}
#ifdef CONFIG_ARM64
static u64 read_ERXCTLR_EL1(void)
{
u64 v;
__asm__ volatile ("mrs %0, s3_0_c5_c4_1" : "=r" (v));
return v;
}
static u64 read_ERXMISC0_EL1(void)
{
u64 v;
__asm__ volatile ("mrs %0, s3_0_c5_c5_0" : "=r" (v));
return v;
}
static u64 read_ERXSTATUS_EL1(void)
{
u64 v;
__asm__ volatile ("mrs %0, s3_0_c5_c4_2" : "=r" (v));
return v;
}
static void write_ERRSELR_EL1(u64 v)
{
__asm__ volatile ("msr s3_0_c5_c3_1, %0" : : "r" (v));
}
static void write_ERXSTATUS_EL1(u64 v)
{
__asm__ volatile ("msr s3_0_c5_c4_2, %0" : : "r" (v));
}
static void write_ERXSELR_EL1(u64 v)
{
__asm__ volatile ("msr s3_0_c5_c3_1, %0" : : "r" (v));
}
#else
static u64 read_ERXCTLR_EL1(void)
{
return 0;
}
static u64 read_ERXMISC0_EL1(void)
{
return 0;
}
static u64 read_ERXSTATUS_EL1(void)
{
return 0;
}
static void write_ERRSELR_EL1(u64 v)
{
}
static void write_ERXSTATUS_EL1(u64 v)
{
}
static void write_ERXSELR_EL1(u32 v)
{
}
#endif
static irqreturn_t cache_parity_isr_v2(int irq, void *dev_id)
{
u32 hwirq;
int i, idx, cpu;
u64 misc0, status;
#ifdef CONFIG_ARM64_ERRATUM_1800710
static const struct midr_range erratum_1800710_cpu_list[] = {
_MIDR_ALL_VERSIONS(MIDR_CORTEX_A76),
_MIDR_ALL_VERSIONS(MIDR_CORTEX_A77),
};
#endif
ecc_dump_debug_info();
atomic_inc(&cache_parity.nr_err);
if (!atomic_read(&cache_parity.nr_err))
cache_parity.timestampe = local_clock();
hwirq = irqd_to_hwirq(irq_get_irq_data(irq));
write_ERXSELR_EL1((hwirq == cache_parity.arm_dsu_ecc_hwirq) ? 1 : 0);
misc0 = read_ERXMISC0_EL1();
status = read_ERXSTATUS_EL1();
/* Clear IRQ via clear error status */
write_ERXSTATUS_EL1(status);
/*
* If the ERxSTATUS register returns zero, clear all errors.
*/
if (!misc0 && !status)
write_ERXSTATUS_EL1(0xFFC00000);
/* Ensure all transactions are finished */
dsb(sy);
for (idx = -1, i = 0; i < cache_parity.nr_irq; i++) {
if (cache_parity.record[i].v2.irq == irq) {
idx = i;
break;
}
}
if (idx >= 0) {
cache_parity.record[idx].v2.is_err = true;
cache_parity.record[idx].v2.misc0_el1 = misc0;
cache_parity.record[idx].v2.status_el1 = status;
cpu = raw_smp_processor_id();
if ((cache_parity.record[idx].v2.cpu != nr_cpu_ids) &&
(cpu != cache_parity.record[idx].v2.cpu))
ECC_LOG("Cache ECC error, cpu%d serviced irq%d(%s%d)\n",
cpu, irq, "expected cpu",
cache_parity.record[idx].v2.cpu);
schedule_work(&cache_parity.work);
}
ECC_LOG("Cache ECC error, %s %d, %s: 0x%016llx, %s: 0x%016llx\n",
"irq", irq, "misc0_el1", misc0, "status_el1", status);
#ifdef CONFIG_ARM64_ERRATUM_1800710
if (is_midr_in_range_list(read_cpuid_id(), erratum_1800710_cpu_list)) {
if ((status & ECC_CE_BIT) == ECC_CE_AT_LEAST_ONE_ERR &&
(status & ECC_SERR_BIT) == ECC_SERR_FROM_DATA_BUFF) {
ECC_LOG("%s %s hit, may cause stale translation\n",
__func__, "Erratum 1800710");
}
}
#endif
if (atomic_read(&cache_parity.nr_err) > ECC_IRQ_TRIGGER_THRESHOLD) {
disable_irq_nosync(irq);
ECC_LOG("%s disable irq %d due to trigger over than %d times.",
__func__, irq, ECC_IRQ_TRIGGER_THRESHOLD);
}
return IRQ_HANDLED;
}
static irqreturn_t cache_parity_isr_v1(int irq, void *dev_id)
{
struct parity_record_t *parity_record;
void __iomem *base;
unsigned int status;
unsigned int offset;
unsigned int irq_idx;
unsigned int i;
if (!atomic_read(&cache_parity.nr_err))
cache_parity.timestampe = local_clock();
atomic_inc(&cache_parity.nr_err);
for (i = 0, parity_record = NULL; i < cache_parity.nr_irq; i++) {
if (parity_irq_record[i].irq == irq) {
irq_idx = i;
parity_record = &(parity_irq_record[i].parity_record);
pr_info("parity isr for %d\n", i);
break;
}
}
if (parity_record == NULL) {
pr_info("no matched irq %d\n", irq);
return IRQ_HANDLED;
}
base = cache_parity.cache_parity_base;
status = readl(base + parity_record->check_offset);
pr_info("status 0x%x\n", status);
if (status & parity_record->check_mask)
pr_info("detect cache parity error\n");
else
pr_info("no cache parity error\n");
for (i = 0; i < parity_record->dump_length; i += 4) {
offset = parity_record->dump_offset + i;
pr_info("offset 0x%x, val 0x%x\n", offset,
readl(base + offset));
}
for (i = 0; i < cache_parity.nr_irq; i++) {
if (cache_parity.record[i].v1.irq != irq)
continue;
cache_parity.record[i].v1.is_err = true;
cache_parity.record[i].v1.status = status;
schedule_work(&cache_parity.work);
}
spin_lock(&parity_isr_lock);
if (parity_record->clear_mask) {
writel(parity_record->clear_mask,
base + parity_record->clear_offset);
dsb(sy);
writel(0x0, base + parity_record->clear_offset);
dsb(sy);
while (readl(base + parity_record->check_offset) &
parity_record->check_mask) {
udelay(1);
}
}
spin_unlock(&parity_isr_lock);
if (atomic_read(&cache_parity.nr_err) > ECC_IRQ_TRIGGER_THRESHOLD) {
disable_irq_nosync(irq);
ECC_LOG("%s disable irq %d due to trigger over than %d times.",
__func__, irq, ECC_IRQ_TRIGGER_THRESHOLD);
}
return IRQ_HANDLED;
}
static void ecc_get_core_status(void *info)
{
int cpu_idx = smp_processor_id();
unsigned int read_cpuid = read_cpuid_id() >> 4 & 0xFFF;
//FIXME:if (is_midr_in_range_list(read_cpuid_id(), ecc_midr_list))
if (read_cpuid == KLN_CPU_ID_MASK || read_cpuid == MTH_CPU_ID_MASK
|| read_cpuid == MTHELP_CPU_ID_MASK)
write_ERRSELR_EL1(ECC_SEL_L1_MODE);
else
write_ERRSELR_EL1(ECC_SEL_L1_MODE_LGY);
cache_parity.record[cpu_idx].v2.sctlr_el1_L1 = read_ERXCTLR_EL1();
cache_parity.record[cpu_idx].v2.status_el1_L1 = read_ERXSTATUS_EL1();
cache_parity.record[cpu_idx].v2.misc0_el1_L1 = read_ERXMISC0_EL1();
// if (read_cpuid_id() == MIDR_CORTEX_A510) {
if (read_cpuid == KLN_CPU_ID_MASK) {
write_ERRSELR_EL1(ECC_SEL_L2_MODE);
cache_parity.record[cpu_idx].v2.sctlr_el1_L2 = read_ERXCTLR_EL1();
cache_parity.record[cpu_idx].v2.status_el1_L2 = read_ERXSTATUS_EL1();
cache_parity.record[cpu_idx].v2.misc0_el1_L2 = read_ERXMISC0_EL1();
}
}
static ssize_t status_show(struct device_driver *driver, char *buf)
{
int cpu_idx;
unsigned int len = 0;
unsigned int read_cpuid = read_cpuid_id() >> 4 & 0xFFF;
static const char * const err_mode[] = {"DSU", "L1C", "L2C"};
/* FIXME */
// if (is_midr_in_range_list(read_cpuid_id(), ecc_midr_list))
if (read_cpuid == KLN_CPU_ID_MASK || read_cpuid == MTH_CPU_ID_MASK
|| read_cpuid == MTHELP_CPU_ID_MASK)
write_ERRSELR_EL1(ECC_SEL_DSU_MODE);
else
write_ERRSELR_EL1(ECC_SEL_DSU_MODE_LGY);
len += snprintf(buf + len, PAGE_SIZE - len,
"- %s 0x%05llx 0x%08llx 0x%012llx\n",
err_mode[0], read_ERXCTLR_EL1(),
read_ERXSTATUS_EL1(), read_ERXMISC0_EL1());
get_online_cpus();
for_each_online_cpu(cpu_idx) {
smp_call_function_single(cpu_idx, ecc_get_core_status, NULL, 0);
len += snprintf(buf + len, PAGE_SIZE - len,
"%d /%s 0x%05llx 0x%08llx 0x%012llx\n",
cpu_idx, err_mode[1],
cache_parity.record[cpu_idx].v2.sctlr_el1_L1,
cache_parity.record[cpu_idx].v2.status_el1_L1,
cache_parity.record[cpu_idx].v2.misc0_el1_L1);
if (cache_parity.record[cpu_idx].v2.sctlr_el1_L2) {
len += snprintf(buf + len, PAGE_SIZE - len,
"%d /%s 0x%05llx 0x%08llx 0x%012llx\n",
cpu_idx, err_mode[2],
cache_parity.record[cpu_idx].v2.sctlr_el1_L2,
cache_parity.record[cpu_idx].v2.status_el1_L2,
cache_parity.record[cpu_idx].v2.misc0_el1_L2);
}
}
put_online_cpus();
return strlen(buf);
}
static DRIVER_ATTR_RO(status);
void __attribute__((weak)) cache_parity_init_platform(void)
{
pr_info("[%s] adopt default flow\n", __func__);
}
static int __count_cache_parity_irq(struct device_node *dev)
{
struct of_phandle_args oirq;
int nr = 0;
while (of_irq_parse_one(dev, nr, &oirq) == 0)
nr++;
return nr;
}
static int __probe_v2(struct platform_device *pdev)
{
unsigned int i;
int ret;
int irq, hwirq, cpu;
if (!cache_parity.ecc_irq_support)
return 0;
for (i = 0, cpu = 0; i < cache_parity.nr_irq; i++) {
irq = irq_of_parse_and_map(pdev->dev.of_node, i);
if (irq == 0) {
dev_err(&pdev->dev,
"failed to irq_of_parse_and_map %d\n", i);
return -ENXIO;
}
cache_parity.record[i].v2.irq = irq;
/*
* Per-cpu system registers will be read and recorded in the
* ISR (Interrupt Service Routine). The ISR must be bound to
* the corresponding CPU except the ISR for the ARM DSU ECC
* interrupt (which can be served on any CPU).
*/
hwirq = irqd_to_hwirq(irq_get_irq_data(irq));
if (hwirq != cache_parity.arm_dsu_ecc_hwirq) {
cache_parity.record[i].v2.cpu = cpu;
#if defined(MODULE)
/*
* FIXME: Here is an issue caused by GKI.
* We should use irq_force_affinity for
* guaranteeing the per-core ECC interrupt
* is routed to the corresponding CPU.
* This is because the ECC status will be read
* from the per-core system register.
* But the kernel function irq_force_affinity
* is NOT exported.
*
* Workaround this problem by using the function
* irq_set_affinity_hint. Need to fix this
* after we upstream a patch (to export
* irq_force_affinity).
*/
ret = irq_set_affinity_hint(irq, cpumask_of(cpu));
#else
ret = irq_force_affinity(irq, cpumask_of(cpu));
#endif
cpu++;
if (ret) {
dev_err(&pdev->dev,
"failed to set affinity for irq %d\n",
irq);
return -ENXIO;
}
dev_info(&pdev->dev, "bound irq %d for cpu%d\n",
irq, i);
} else
cache_parity.record[i].v2.cpu = nr_cpu_ids;
ret = devm_request_irq(&pdev->dev, irq, cache_parity_isr_v2,
IRQF_TRIGGER_NONE | IRQF_ONESHOT,
"cache_parity", NULL);
if (ret) {
dev_err(&pdev->dev,
"failed to request irq for irq %d\n", irq);
return -ENXIO;
}
}
return 0;
}
static int __probe_v1(struct platform_device *pdev)
{
struct parity_irq_config_t *parity_irq_config;
size_t size;
unsigned int i, target_cpu;
int irq, ret;
size = sizeof(struct parity_irq_record_t) * cache_parity.nr_irq;
parity_irq_record = devm_kmalloc(&pdev->dev, size, GFP_KERNEL);
if (!parity_irq_record)
return -ENOMEM;
size = sizeof(struct parity_irq_config_t) * cache_parity.nr_irq;
parity_irq_config = devm_kmalloc(&pdev->dev, size, GFP_KERNEL);
if (!parity_irq_config)
return -ENOMEM;
size = size >> 2;
ret = of_property_read_variable_u32_array(pdev->dev.of_node,
"irq_config", (u32 *)parity_irq_config, size, size);
if (ret) {
dev_err(&pdev->dev, "No irq_config\n");
return -ENXIO;
}
for (i = 0; i < cache_parity.nr_irq; i++) {
memcpy(&(parity_irq_record[i].parity_record),
&(parity_irq_config[i].parity_record),
sizeof(struct parity_record_t));
irq = irq_of_parse_and_map(pdev->dev.of_node, i);
if (irq == 0) {
dev_err(&pdev->dev,
"failed to irq_of_parse_and_map %d\n", i);
return -ENXIO;
}
parity_irq_record[i].irq = irq;
cache_parity.record[i].v1.irq = irq;
target_cpu = parity_irq_config[i].target_cpu;
if (target_cpu < nr_cpu_ids) {
ret = irq_set_affinity_hint(irq,
cpumask_of(target_cpu));
if (ret)
dev_notice(&pdev->dev,
"failed to set IRQ affinity for cpu%d\n",
target_cpu);
}
ret = devm_request_irq(&pdev->dev, irq, cache_parity_isr_v1,
IRQF_TRIGGER_NONE, "cache_parity", NULL);
if (ret) {
dev_err(&pdev->dev,
"failed to request irq for irq %d\n", irq);
return -EINVAL;
}
}
return 0;
}
static int cache_parity_probe(struct platform_device *pdev)
{
int ret;
size_t size;
struct mtk_cache_parity_compatible *dev_comp;
dev_info(&pdev->dev, "driver probed\n");
dev_comp = (struct mtk_cache_parity_compatible *)
of_device_get_match_data(&pdev->dev);
cache_parity.ver = (unsigned int)dev_comp->ver;
atomic_set(&cache_parity.nr_err, 0);
INIT_WORK(&cache_parity.work, cache_parity_irq_work);
cache_parity_init_platform();
cache_parity.nr_irq = __count_cache_parity_irq(pdev->dev.of_node);
size = sizeof(union err_record) * cache_parity.nr_irq;
cache_parity.record = devm_kzalloc(&pdev->dev, size, GFP_KERNEL);
if (!cache_parity.record)
return -ENOMEM;
switch (cache_parity.ver) {
case 1:
cache_parity.cache_parity_base = of_iomap(pdev->dev.of_node, 0);
if (!cache_parity.cache_parity_base)
return -ENOMEM;
ret = __probe_v1(pdev);
break;
case 2:
ret = of_property_read_u32(pdev->dev.of_node,
"arm_dsu_ecc_hwirq",
&cache_parity.arm_dsu_ecc_hwirq);
if (ret) {
dev_err(&pdev->dev, "no arm_dsu_ecc_hwirq");
return -ENXIO;
}
ret = of_property_read_u32(pdev->dev.of_node,
"ecc-irq-support",
&cache_parity.ecc_irq_support);
if (ret)
dev_err(&pdev->dev, "no ecc_irq_support setting");
ret = __probe_v2(pdev);
break;
default:
dev_err(&pdev->dev, "unsupported version\n");
ret = -ENXIO;
break;
}
if (!ret)
dev_info(&pdev->dev, "%s %d, %s %d, %s %d %s %d\n",
"version", cache_parity.ver,
"nr_irq", cache_parity.nr_irq,
"arm_dsu_ecc_hwirq", cache_parity.arm_dsu_ecc_hwirq,
"nr_err", cache_parity.nr_err);
return ret;
}
static int cache_parity_remove(struct platform_device *pdev)
{
dev_info(&pdev->dev, "driver removed\n");
flush_work(&cache_parity.work);
return 0;
}
static const struct of_device_id cache_parity_of_ids[] = {
{ .compatible = "mediatek,mt6785-cache-parity", .data = &mt6785_compat },
{ .compatible = "mediatek,mt6873-cache-parity", .data = &mt6873_compat },
{}
};
static struct platform_driver cache_parity_drv = {
.driver = {
.name = "cache_parity",
.bus = &platform_bus_type,
.owner = THIS_MODULE,
.of_match_table = cache_parity_of_ids,
},
.probe = cache_parity_probe,
.remove = cache_parity_remove,
};
static int __init cache_parity_init(void)
{
int ret;
ret = platform_driver_register(&cache_parity_drv);
if (ret)
return ret;
if (cache_parity.ecc_irq_support) {
ret = driver_create_file(&cache_parity_drv.driver,
&driver_attr_cache_status);
if (ret)
return ret;
}
ret = driver_create_file(&cache_parity_drv.driver,
&driver_attr_status);
if (ret)
return ret;
return 0;
}
static __exit void cache_parity_exit(void)
{
if (cache_parity.ecc_irq_support) {
driver_remove_file(&cache_parity_drv.driver,
&driver_attr_cache_status);
}
driver_remove_file(&cache_parity_drv.driver,
&driver_attr_status);
platform_driver_unregister(&cache_parity_drv);
}
module_init(cache_parity_init);
module_exit(cache_parity_exit);
MODULE_DESCRIPTION("MediaTek Cache Parity Driver");
MODULE_LICENSE("GPL v2");