blob: 93abf546905f5b42be3d7bb108c20c3eecf7b580 [file] [log] [blame]
/*
* Copyright (C) 2018 Samsung Electronics Co., Ltd.
*
* Author: Nishant Prajapati <nishant.p@samsung.com>
*
* Airbrush PPMU driver.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 and
* only version 2 as published by the Free Software Foundation.
*/
#include <linux/io.h>
#include <linux/clk.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/suspend.h>
#include <linux/of_address.h>
#include <linux/platform_device.h>
#include <linux/mfd/abc-pcie.h>
#include "airbrush-ppmu.h"
/**
* ppmu_write:
* ppmu_read: functions for Reading/Writing register through PCIe channel
*/
void ppmu_write(u32 value, u32 offset)
{
abc_pcie_config_write(offset, 4, value);
}
u32 ppmu_read(u32 offset)
{
u32 data;
abc_pcie_config_read(offset, 4, &data);
return data;
}
/**
* reset_counter: reset all the counters
* @ppmu: airbrush_ppmu device structure
*/
static void reset_counter(struct airbrush_ppmu *ppmu)
{
u32 base, regvalue, i;
base = ppmu->base;
regvalue = ppmu_read(base + PPMU25_PMNC);
/* Reset CCNT and PMCNT's */
ppmu_write((regvalue | (1 << PPMU_PMNC_CC_RESET_SHIFT) |
(1 << PPMU_PMNC_COUNTER_RESET_SHIFT)), base + PPMU25_PMNC);
for (i = 0; i < MAX_COUNTER - 1; i++)
ppmu_write(PPMU_RESET_VAL, base + PPMU25_EVENT_EVx_TYPE(i));
}
/**
* ppmu_reset: reset whole PPMU device
*/
static int ppmu_reset(struct airbrush_ppmu *ppmu)
{
u32 base, i, regvalue;
base = ppmu->base;
/* Number of configured event = 0 */
ppmu->state->conf_events = 0;
ppmu->state->bmp = 0;
for (i = 0; i < MAX_COUNTER ; i++)
ppmu->state->over_flow[i] = 0;
reset_counter(ppmu);
regvalue = ppmu_read(base + PPMU25_PMNC);
ppmu_write(regvalue & ~0x1 << PPMU_PMNC_ENABLE_SHIFT,
base + PPMU25_PMNC);
/* Disable all 8 PMCNTs [7:0] */
ppmu_write(PPMU_PMCNT_ALL, base + PPMU25_CNTENC);
/* Enable CCNT Clock event [31:31] */
ppmu_write(PPMU_CNT_CC, base + PPMU25_CNTENS);
/* Enable interrupt for CCNT and all PMCNTs */
ppmu_write(PPMU_CNT_CC | PPMU_PMCNT_ALL, base + PPMU25_INTENS);
ppmu_write(PPMU_RESET_VAL, base + PPMU25_INTENC);
/* Clear all events' overflow status Flags at starting */
ppmu_write(PPMU_CNT_CC | PPMU_PMCNT_ALL, base + PPMU25_FLAG);
/* reset CIG configurations*/
ppmu_write(PPMU_RESET_VAL, base + PPMU25_CIG_CFG0);
ppmu_write(PPMU_RESET_VAL, base + PPMU25_CIG_CFG1);
ppmu_write(PPMU_RESET_VAL, base + PPMU25_CIG_CFG2);
ppmu_write(PPMU_CIG_UP_INT | PPMU_CIG_LW_INT, base + PPMU25_CIG_RESULT);
ppmu_write(PPMU_CNT_CC | PPMU_PMCNT_ALL, base + PPMU25_CNT_RESET);
return 0;
}
/*
* print_result(): Temporary function for debugging result
* @counter: array holding the value from PPMU event counters
*/
void print_result(unsigned long long *counter)
{
u32 i = 0;
for (i = 0; i < MAX_COUNTER - 1 ; i++)
pr_info("PMCNT%d: %lld", i, counter[i]);
pr_info("CCNT: %lld\n", counter[MAX_COUNTER - 1]);
}
static int allocate_event(unsigned int event,
u32 *set, u32 len,
struct airbrush_ppmu *ppmu)
{
struct device *dev = &ppmu->dev;
u32 base = ppmu->base;
u32 i, regval;
for (i = 0; i < len; i++) {
if (!(ppmu->state->bmp & 1 << set[i])) {
ppmu->state->bmp |= 1 << set[i];
ppmu->state->conf_events++;
ppmu_write(event, base +
PPMU25_EVENT_EVx_TYPE(set[i]));
regval = ppmu_read(base + PPMU25_CNTENS);
ppmu_write(regval | 1 << set[i],
base + PPMU25_CNTENS);
dev_info(dev, "Event: %d -> will capture in PMCNT: %d\n",
event, set[i]);
return 0;
}
}
dev_err(dev, "Can't allocate counter\n");
return -ENOSPC;
}
/**
* ppmu_config: allocate event to counter.
* @event: number corresponding to type of event
*/
int ppmu_config(struct airbrush_ppmu *ppmu, unsigned int event)
{
struct device *dev = &ppmu->dev;
u32 first_set[] = {PPMU_PMNCNT0, PPMU_PMNCNT1, PPMU_PMNCNT4,
PPMU_PMNCNT5};
u32 second_set[] = {PPMU_PMNCNT2, PPMU_PMNCNT3, PPMU_PMNCNT6,
PPMU_PMNCNT7};
if (ppmu->state->conf_events > (PPMU_PMNCNT_MAX - 1)) {
dev_err(dev, "CANT ALLOCATE: Limit reached\n");
return -ENOSPC;
}
if (event >= PPMU_EVENT_RW_BUSY && event <= PPMU_EVENT_WRITE_LATENCY)
return allocate_event(event, second_set,
ARRAY_SIZE(second_set), ppmu);
if (allocate_event(event, first_set, ARRAY_SIZE(first_set), ppmu) < 0)
return allocate_event(event, second_set,
ARRAY_SIZE(second_set), ppmu);
return 0;
}
int ppmu_start(struct airbrush_ppmu *ppmu)
{
u32 base, regvalue;
base = ppmu->base;
regvalue = ppmu_read(base + PPMU25_PMNC);
regvalue = regvalue | 1 << PPMU_PMNC_ENABLE_SHIFT;
ppmu_write(regvalue, base + PPMU25_PMNC);
return 0;
}
int ppmu_stop(struct airbrush_ppmu *ppmu)
{
u32 base, regvalue;
base = ppmu->base;
regvalue = ppmu_read(base + PPMU25_PMNC);
ppmu_write(regvalue & ~0x1 << PPMU_PMNC_ENABLE_SHIFT,
base + PPMU25_PMNC);
return 0;
}
/**
* ppmu_get_result(): function to fetch results from all counters
* @counter: array to hold the counter values
*/
int ppmu_get_result(struct airbrush_ppmu *ppmu, unsigned long long *counter)
{
u32 i;
u32 base = ppmu->base;
for (i = 0; i < MAX_COUNTER; i++)
counter[i] = (unsigned long long)
ppmu->state->over_flow[i] << 32;
counter[0] |= (unsigned long long)ppmu_read(base + PPMU25_PMCNT0);
counter[1] |= (unsigned long long)ppmu_read(base + PPMU25_PMCNT1);
counter[2] |= (unsigned long long)ppmu_read(base + PPMU25_PMCNT2);
counter[3] |= (unsigned long long)ppmu_read(base + PPMU25_PMCNT3) |
(unsigned long long)(ppmu_read(
base + PPMU25_PMCNT3_HIGH) & 0xFF) << 32;
counter[4] |= (unsigned long long)ppmu_read(base + PPMU25_PMCNT4);
counter[5] |= (unsigned long long)ppmu_read(base + PPMU25_PMCNT5);
counter[6] |= (unsigned long long)ppmu_read(base + PPMU25_PMCNT6);
counter[7] |= (unsigned long long)ppmu_read(base + PPMU25_PMCNT7) |
(unsigned long long)(ppmu_read(
base + PPMU25_PMCNT7_HIGH) & 0xFF) << 32;
counter[8] |= (unsigned long long)ppmu_read(base + PPMU25_CCNT);
return 0;
}
static const struct of_device_id airbrush_ppmu_id_match[] = {
{
.compatible = "abc,airbrush-ppmu-v2",
},
{ /* sentinel */ },
};
MODULE_DEVICE_TABLE(of, airbrush_ppmu_id_match);
int ppmu_irq_handler(unsigned int irq, struct airbrush_ppmu *ppmu)
{
u32 regvalue, i;
u32 base = ppmu->base;
struct airbrush_ppmu_state *ppmu_state = ppmu->state;
regvalue = ppmu_read(base + PPMU25_FLAG);
pr_info("flag: %x\n", regvalue);
ppmu_write(regvalue, base + PPMU25_FLAG);
/* This is to handle interrupt due to CCNT overflow.*/
if (regvalue & PPMU_CNT_CC)
ppmu_state->over_flow[8] += 1;
for (i = 0; i < MAX_COUNTER - 1; i++) {
if (regvalue & PPMU_PMCNT(i))
ppmu_state->over_flow[i] += 1;
}
return 0;
}
static int airbrush_ppmu_parse_dt(struct platform_device *pdev,
struct airbrush_ppmu *info)
{
u32 i = 0, irq;
u32 reg[2];
int err;
struct device *dev = &info->dev;
struct device_node *np = dev->of_node;
struct airbrush_ppmu_state *state =
devm_kzalloc(&pdev->dev, sizeof(*state), GFP_KERNEL);
if (!np) {
dev_err(dev, "failed to find devicetree node\n");
return -EINVAL;
}
err = of_property_read_u32_array(np, "reg", reg, 2);
if (err)
dev_err(dev, "error in getting base address (%d)\n", err);
/*for getting offset for PCIe r/w function calls */
info->base = reg[1] & 0xFFFFFF;
err = of_property_read_u32(np, "interrupt", &irq);
if (err)
dev_err(dev, "error in getting interrupt number (%d)\n", err);
info->irq = irq;
/* reset state*/
state->conf_events = 0;
state->bmp = 0;
for (i = 0; i < MAX_COUNTER ; i++)
state->over_flow[i] = 0;
info->state = state;
dev_set_drvdata(&pdev->dev, info);
return 0;
}
/*Create Sysfs attributes*/
static ssize_t test_read_store(struct device *child,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct airbrush_ppmu *ppmu = dev_get_drvdata(child);
u32 base = ppmu->base;
/* transaction on PCIe Master */
ppmu_read(base + PPMU25_VER);
pr_info("1 read");
return count;
}
static ssize_t test_write_store(struct device *child,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct airbrush_ppmu *ppmu = dev_get_drvdata(child);
u32 base = ppmu->base;
u32 val = 0x25000;
ppmu_write(val, base + PPMU25_VER);
pr_info("1 write");
return count;
}
static ssize_t set_cnt_size_store(struct device *child,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct airbrush_ppmu *ppmu = dev_get_drvdata(child);
unsigned long val;
int ret;
ret = kstrtoul(buf, 10, &val);
if (ret)
pr_err("Error in parsing counter size\n");
pr_info("val: %ld\n", val);
ppmu->ppmu_data.event_count = val;
ppmu_write(0xffffffff-(val-1), ppmu->base + PPMU25_PMCNT2);
return count;
}
static ssize_t set_clk_freq_store(struct device *child,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct airbrush_ppmu *ppmu = dev_get_drvdata(child);
unsigned long val;
int ret;
ret = kstrtoul(buf, 10, &val);
if (ret)
pr_err("Error in parsing frequency\n");
pr_info("clk_freq %ld\n", val);
ppmu->ppmu_data.clk_freq = val;
return count;
}
static int ppmu_irq_notify(struct notifier_block *nb,
unsigned long irq, void *data)
{
struct airbrush_ppmu *dev =
container_of(nb, struct airbrush_ppmu, nb);
u32 intnc_val = (u64)data;
if (irq == ABC_MSI_AON_INTNC &&
(intnc_val & (1 << (dev->irq - ABC_MSI_COUNT))))
return ppmu_irq_handler(irq, dev);
return 0;
}
static ssize_t register_irq_store(struct device *child,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct airbrush_ppmu *ppmu = dev_get_drvdata(child);
ppmu->nb.notifier_call = ppmu_irq_notify;
abc_reg_notifier_callback(&ppmu->nb);
return count;
}
static ssize_t ppmu_reset_store(struct device *child,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct airbrush_ppmu *ppmu = dev_get_drvdata(child);
dev_info(child, "ppmu reset\n");
ppmu_reset(ppmu);
return count;
}
static ssize_t ppmu_config_show(struct device *child,
struct device_attribute *attr, char *buf)
{
return scnprintf(buf, PAGE_SIZE, "%s",
"PPMU_EVENT_READ_BUSY = 0\n"
"PPMU_EVENT_WRITE_BUSY = 1,\n"
"PPMU_EVENT_READ_REQUEST = 2,\n"
"PPMU_EVENT_WRITE_REQUEST = 3,\n"
"PPMU_EVENT_READ_DATA = 4,\n"
"PPMU_EVENT_WRITE_DATA = 5,\n"
"PPMU_EVENT_WRITE_RESP = 6,\n"
"PPMU_EVENT_READ_LAST = 7,\n"
"PPMU_EVENT_WRITE_LAST = 8,\n"
"PPMU_EVENT_READ_REQ_BLOCK = 0x10,\n"
"PPMU_EVENT_WRITE_REQ_BLOCK = 0x11,\n"
"PPMU_EVENT_READ_DATA_BLOCK = 0x12,\n"
"PPMU_EVENT_WRITE_DATA_BLOCK = 0x13,\n"
"PPMU_EVENT_WRITE_RESP_BLOCK = 0x14,\n"
"PPMU_EVENT_EXT_0 = 0x30,\n"
"PPMU_EVENT_EXT_1 = 0x31,\n"
"PPMU_EVENT_EXT_2 = 0x32,\n"
"PPMU_EVENT_RW_BUSY = 0x20,\n"
"PPMU_EVENT_RW_REQUEST = 0x21,\n"
"PPMU_EVENT_RW_DATA = 0x22,\n"
"PPMU_EVENT_RW_REQ_BLOCK = 0x23,\n"
"PPMU_EVENT_READ_LATENCY = 0x24,\n"
"PPMU_EVENT_WRITE_LATENCY = 0x25,\n");
}
static ssize_t ppmu_config_store(struct device *child,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct airbrush_ppmu *ppmu = dev_get_drvdata(child);
u32 para = 0;
if (sysfs_streq(buf, "PPMU_EVENT_READ_BUSY"))
para = 0;
else if (sysfs_streq(buf, "PPMU_EVENT_WRITE_BUSY"))
para = 1;
else if (sysfs_streq(buf, "PPMU_EVENT_READ_REQUEST"))
para = 2;
else if (sysfs_streq(buf, "PPMU_EVENT_WRITE_REQUEST"))
para = 3;
else if (sysfs_streq(buf, "PPMU_EVENT_READ_DATA"))
para = 4;
else if (sysfs_streq(buf, "PPMU_EVENT_WRITE_DATA"))
para = 5;
else if (sysfs_streq(buf, "PPMU_EVENT_WRITE_RESP"))
para = 6;
else if (sysfs_streq(buf, "PPMU_EVENT_READ_LAST"))
para = 7;
else if (sysfs_streq(buf, "PPMU_EVENT_WRITE_LAST"))
para = 8;
else if (sysfs_streq(buf, "PPMU_EVENT_READ_REQ_BLOCK"))
para = 0x10;
else if (sysfs_streq(buf, "PPMU_EVENT_WRITE_REQ_BLOCK"))
para = 0x11;
else if (sysfs_streq(buf, "PPMU_EVENT_READ_DATA_BLOCK"))
para = 0x12;
else if (sysfs_streq(buf, "PPMU_EVENT_WRITE_DATA_BLOCK"))
para = 0x13;
else if (sysfs_streq(buf, "PPMU_EVENT_WRITE_RESP_BLOCK"))
para = 0x14;
else if (sysfs_streq(buf, "PPMU_EVENT_EXT_0"))
para = 0x30;
else if (sysfs_streq(buf, "PPMU_EVENT_EXT_1"))
para = 0x31;
else if (sysfs_streq(buf, "PPMU_EVENT_EXT_2"))
para = 0x32;
else if (sysfs_streq(buf, "PPMU_EVENT_RW_BUSY"))
para = 0x20;
else if (sysfs_streq(buf, "PPMU_EVENT_RW_REQUEST"))
para = 0x21;
else if (sysfs_streq(buf, "PPMU_EVENT_RW_DATA"))
para = 0x22;
else if (sysfs_streq(buf, "PPMU_EVENT_RW_REQ_BLOCK"))
para = 0x23;
else if (sysfs_streq(buf, "PPMU_EVENT_READ_LATENCY"))
para = 0x24;
else if (sysfs_streq(buf, "PPMU_EVENT_WRITE_LATENCY"))
para = 0x25;
else {
dev_err(child, "ERROR in event type\n");
return count;
}
ppmu_config(ppmu, para);
return count;
}
static ssize_t ppmu_start_store(struct device *child,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct airbrush_ppmu *ppmu = dev_get_drvdata(child);
if (sysfs_streq(buf, "1"))
ppmu_start(ppmu);
return count;
}
static ssize_t ppmu_stop_store(struct device *child,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct airbrush_ppmu *ppmu = dev_get_drvdata(child);
dev_info(child, "PPMU stop= %s\n", buf);
ppmu_stop(ppmu);
return count;
}
static ssize_t ppmu_get_result_show(struct device *child,
struct device_attribute *attr, char *buf)
{
struct airbrush_ppmu *ppmu = dev_get_drvdata(child);
unsigned long long counter[MAX_COUNTER];
ppmu_get_result(ppmu, counter);
print_result(counter);
return scnprintf(buf, PAGE_SIZE, "PPMU results:\n");
}
static ssize_t selective_measurement_mask_store(struct device *child,
struct device_attribute *attr,
const char *buf, size_t count)
{
/* Caution: This register should be configured only when there is no
* outstanding transaction in the monitored AXI channel.
*/
struct airbrush_ppmu *ppmu = dev_get_drvdata(child);
unsigned long val;
int ret;
ret = kstrtoul(buf, 16, &val);
if (ret) {
dev_err(&ppmu->dev, "Error in parsing mask\n");
return ret;
}
dev_info(&ppmu->dev, "mask %lu\n", val);
ppmu_write(val, ppmu->base + PPMU25_SM_ID_MASK);
return count;
}
static ssize_t selective_measurement_mask_show(struct device *child,
struct device_attribute *attr, char *buf)
{
struct airbrush_ppmu *ppmu = dev_get_drvdata(child);
u32 val = ppmu_read(ppmu->base + PPMU25_SM_ID_MASK);
return scnprintf(buf, PAGE_SIZE, "%u\n", val);
}
static ssize_t selective_measurement_value_store(struct device *child,
struct device_attribute *attr,
const char *buf, size_t count)
{
/* Caution: This register should be configured only when there is no
* outstanding transaction in the monitored AXI channel.
*/
struct airbrush_ppmu *ppmu = dev_get_drvdata(child);
unsigned long val;
int ret;
ret = kstrtoul(buf, 16, &val);
if (ret) {
dev_err(&ppmu->dev, "Error in parsing value\n");
return ret;
}
dev_info(&ppmu->dev, "value %lu\n", val);
ppmu_write(val, ppmu->base + PPMU25_SM_ID_V);
return count;
}
static ssize_t selective_measurement_value_show(struct device *child,
struct device_attribute *attr, char *buf)
{
struct airbrush_ppmu *ppmu = dev_get_drvdata(child);
u32 val = ppmu_read(ppmu->base + PPMU25_SM_ID_V);
return scnprintf(buf, PAGE_SIZE, "%u\n", val);
}
static ssize_t selective_measurement_allocate_store(struct device *child,
struct device_attribute *attr,
const char *buf, size_t count)
{
/* Caution: This register should be configured only when there is no
* outstanding transaction in the monitored AXI channel.
*/
struct airbrush_ppmu *ppmu = dev_get_drvdata(child);
unsigned long val;
int ret;
ret = kstrtoul(buf, 16, &val);
if (ret) {
dev_err(&ppmu->dev, "Error in parsing allocation bits\n");
return ret;
}
dev_info(&ppmu->dev, "allocation bits %lu\n", val);
ppmu_write(val, ppmu->base + PPMU25_SM_ID_A);
return count;
}
static ssize_t selective_measurement_allocate_show(struct device *child,
struct device_attribute *attr, char *buf)
{
struct airbrush_ppmu *ppmu = dev_get_drvdata(child);
u32 val = ppmu_read(ppmu->base + PPMU25_SM_ID_A);
return scnprintf(buf, PAGE_SIZE, "%u\n", val);
}
static DEVICE_ATTR(test_read, 0664, NULL, test_read_store);
static DEVICE_ATTR(test_write, 0664, NULL, test_write_store);
static DEVICE_ATTR(ppmu_reset, 0664, NULL, ppmu_reset_store);
static DEVICE_ATTR(ppmu_config, 0664, ppmu_config_show, ppmu_config_store);
static DEVICE_ATTR(ppmu_start, 0664, NULL, ppmu_start_store);
static DEVICE_ATTR(ppmu_stop, 0664, NULL, ppmu_stop_store);
static DEVICE_ATTR(ppmu_get_result, 0664, ppmu_get_result_show, NULL);
static DEVICE_ATTR(register_irq, 0664, NULL, register_irq_store);
static DEVICE_ATTR(set_cnt_size, 0664, NULL, set_cnt_size_store);
static DEVICE_ATTR(set_clk_freq, 0664, NULL, set_clk_freq_store);
static DEVICE_ATTR(selective_measurement_mask, 0664,
selective_measurement_mask_show,
selective_measurement_mask_store);
static DEVICE_ATTR(selective_measurement_value, 0664,
selective_measurement_value_show,
selective_measurement_value_store);
static DEVICE_ATTR(selective_measurement_allocate, 0664,
selective_measurement_allocate_show,
selective_measurement_allocate_store);
struct attribute *ppmu_attrs[] = {
&dev_attr_test_read.attr,
&dev_attr_test_write.attr,
&dev_attr_ppmu_reset.attr,
&dev_attr_ppmu_config.attr,
&dev_attr_ppmu_start.attr,
&dev_attr_ppmu_stop.attr,
&dev_attr_ppmu_get_result.attr,
&dev_attr_register_irq.attr,
&dev_attr_set_cnt_size.attr,
&dev_attr_set_clk_freq.attr,
&dev_attr_selective_measurement_mask.attr,
&dev_attr_selective_measurement_value.attr,
&dev_attr_selective_measurement_allocate.attr,
NULL,
};
struct attribute_group ppmu_attrs_grp = {
.attrs = ppmu_attrs,
};
static int airbrush_ppmu_probe(struct platform_device *pdev)
{
struct airbrush_ppmu *info;
int ret = 0;
info = devm_kzalloc(&pdev->dev, sizeof(*info), GFP_KERNEL);
if (!info)
return -ENOMEM;
info->dev = pdev->dev;
/* Parse dt data to get resource */
ret = airbrush_ppmu_parse_dt(pdev, info);
if (ret < 0) {
dev_err(&pdev->dev,
"failed to parse devicetree for resource\n");
return ret;
}
/* sysfs creation*/
if (sysfs_create_group(&pdev->dev.kobj, &ppmu_attrs_grp))
dev_err(&pdev->dev, "Sysfs Attribute Creation failed for PPMU\n");
else
dev_dbg(&pdev->dev, "Sysfs attribute created for PPMU\n");
return 0;
}
static int airbrush_ppmu_remove(struct platform_device *pdev)
{
/* todo */
return 0;
}
static struct platform_driver airbrush_ppmu_driver = {
.probe = airbrush_ppmu_probe,
.remove = airbrush_ppmu_remove,
.driver = {
.name = "airbrush-ppmu",
.of_match_table = airbrush_ppmu_id_match,
},
};
module_platform_driver(airbrush_ppmu_driver);
MODULE_DESCRIPTION("Airbrush Platform Performance Monitoring Unit driver");
MODULE_AUTHOR("Nishant Prajapati <nishant.p@samsung.com>");
MODULE_LICENSE("GPL");