1124 lines
27 KiB
C
1124 lines
27 KiB
C
|
/*
|
||
|
* CY8C4014 LED chip driver
|
||
|
*
|
||
|
* Copyright (C) 2014-2019 NVIDIA Corporation. All rights reserved.
|
||
|
*
|
||
|
* 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; version 2 of the License.
|
||
|
*
|
||
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||
|
* more details.
|
||
|
*
|
||
|
* You should have received a copy of the GNU General Public License along
|
||
|
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||
|
*
|
||
|
*/
|
||
|
|
||
|
#include <linux/kernel.h>
|
||
|
#include <linux/module.h>
|
||
|
#include <linux/init.h>
|
||
|
#include <linux/err.h>
|
||
|
#include <linux/slab.h>
|
||
|
#include <linux/bug.h>
|
||
|
#include <linux/i2c.h>
|
||
|
#include <linux/regmap.h>
|
||
|
#include <linux/leds.h>
|
||
|
#include <linux/debugfs.h>
|
||
|
#include <linux/mutex.h>
|
||
|
#include <linux/miscdevice.h>
|
||
|
#include <linux/version.h>
|
||
|
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 14, 0)
|
||
|
#include <linux/uaccess.h>
|
||
|
#else
|
||
|
#include <asm/uaccess.h>
|
||
|
#endif
|
||
|
#include <linux/delay.h>
|
||
|
|
||
|
/* register definitions */
|
||
|
#define P1961_REG_CMD 0x00
|
||
|
#define P1961_REG_CMD_DAT 0x01
|
||
|
#define P1961_REG_CMD_STATUS 0x02
|
||
|
#define P1961_REG_APP_MINOR_REV 0x03
|
||
|
#define P1961_REG_APP_MAJOR_REV 0x04
|
||
|
#define P1961_REG_LED_STATE 0x05
|
||
|
#define P1961_REG_LED_ON_TIME 0x06
|
||
|
#define P1961_REG_LED_OFF_TIME 0x07
|
||
|
#define P1961_REG_MAX_BRIGHT 0x08
|
||
|
#define P1961_REG_NOM_BRIGHT 0x09
|
||
|
#define P1961_REG_LED_RAMP_UP 0x0A
|
||
|
#define P1961_REG_LED_RAMP_DOWN 0x0B
|
||
|
#define P1961_REG_HAPTIC_EN 0x0C
|
||
|
#define P1961_REG_HAPTIC_DRIVE_TIME 0x0D
|
||
|
#define P1961_REG_HAPTIC_BRAKE_DLY 0x0E
|
||
|
#define P1961_REG_HAPTIC_BRAKE_T 0x0F
|
||
|
#define P1961_REG_SOUND_PULSE_LEN 0x10
|
||
|
#define P1961_REG_MAX 0x11
|
||
|
#define P1961_CMD_ENTER_BL 0x01
|
||
|
#define P1961_CMD_WRITE_EEPROM 0x05
|
||
|
|
||
|
#define P1961_LED_STATE_BLINK 0x01
|
||
|
#define P1961_LED_STATE_BREATH 0x02
|
||
|
#define P1961_LED_STATE_SOLID 0x03
|
||
|
#define P1961_LED_STATE_OFF 0x04
|
||
|
|
||
|
#define P1961_CMD_GOTO_BOOT 0x01
|
||
|
#define P1961_CMD_GOTO_APP 0x3B
|
||
|
|
||
|
/* boot device mode address */
|
||
|
#define P1961_BOOT_DEV_ADDR 0x08
|
||
|
|
||
|
#define MAX_COMMAND_SIZE 512
|
||
|
#define BASE_CMD_SIZE 0x07
|
||
|
#define CMD_START 0x01
|
||
|
#define CMD_ENTER_BOOTLOADER 0x38
|
||
|
#define CMD_EXIT_BOOTLOADER 0x3B
|
||
|
#define CMD_STOP 0x17
|
||
|
#define COMMAND_DATA_SIZE 0x01
|
||
|
#define RESET 0x00
|
||
|
#define COMMAND_SIZE (BASE_CMD_SIZE + COMMAND_DATA_SIZE)
|
||
|
|
||
|
#define DEVICE_NAME "cy8c-led-boot"
|
||
|
|
||
|
enum modes {
|
||
|
MODE_BLINK = 0,
|
||
|
MODE_BREATH,
|
||
|
MODE_NORMAL,
|
||
|
};
|
||
|
|
||
|
struct mode_control {
|
||
|
enum modes mode;
|
||
|
char *mode_name;
|
||
|
unsigned char reg_mode;
|
||
|
unsigned char reg_mode_val;
|
||
|
unsigned char reg_on_reg;
|
||
|
unsigned char reg_off_reg;
|
||
|
};
|
||
|
|
||
|
static struct mode_control mode_controls[] = {
|
||
|
{MODE_BLINK, "blink",
|
||
|
P1961_REG_LED_STATE, P1961_LED_STATE_BLINK,
|
||
|
P1961_REG_LED_ON_TIME, P1961_REG_LED_OFF_TIME},
|
||
|
{MODE_BREATH, "breath",
|
||
|
P1961_REG_LED_STATE, P1961_LED_STATE_BREATH,
|
||
|
P1961_REG_LED_RAMP_UP, P1961_REG_LED_RAMP_DOWN},
|
||
|
{MODE_NORMAL, "normal",
|
||
|
P1961_REG_LED_STATE, P1961_LED_STATE_SOLID,
|
||
|
P1961_REG_NOM_BRIGHT, P1961_REG_NOM_BRIGHT},
|
||
|
};
|
||
|
static int mode_controls_size =
|
||
|
sizeof(mode_controls) / sizeof(mode_controls[0]);
|
||
|
|
||
|
#define DEVICE_MODE_INVALID 0
|
||
|
#define DEVICE_MODE_APP 1
|
||
|
#define DEVICE_MODE_BOOT 2
|
||
|
|
||
|
struct cy8c_data {
|
||
|
/* app device */
|
||
|
struct i2c_client *client;
|
||
|
struct regmap *regmap;
|
||
|
struct led_classdev led;
|
||
|
int mode_index; /* mode index to mode_controls */
|
||
|
bool led_on_plugin;
|
||
|
u8 default_brightness;
|
||
|
struct mutex lock;
|
||
|
struct miscdevice miscdev;
|
||
|
|
||
|
/* boot device */
|
||
|
struct i2c_client *client_boot;
|
||
|
struct i2c_adapter *adap;
|
||
|
struct i2c_board_info brd_boot;
|
||
|
atomic_t in_use;
|
||
|
|
||
|
atomic_t device_mode;
|
||
|
};
|
||
|
|
||
|
typedef unsigned short (*cy8c_crc_algo_t)(
|
||
|
unsigned char *buf, unsigned long size);
|
||
|
|
||
|
static int cy8c_get_mode_index(
|
||
|
struct cy8c_data *data)
|
||
|
{
|
||
|
int mode_index = 0;
|
||
|
mutex_lock(&data->lock);
|
||
|
mode_index = data->mode_index;
|
||
|
mutex_unlock(&data->lock);
|
||
|
return mode_index;
|
||
|
}
|
||
|
|
||
|
static void cy8c_set_mode_index(
|
||
|
struct cy8c_data *data,
|
||
|
int mode_index)
|
||
|
{
|
||
|
mutex_lock(&data->lock);
|
||
|
data->mode_index = mode_index;
|
||
|
mutex_unlock(&data->lock);
|
||
|
}
|
||
|
|
||
|
static void set_led_brightness(struct led_classdev *led_cdev,
|
||
|
enum led_brightness value)
|
||
|
{
|
||
|
struct cy8c_data *data = container_of(led_cdev, struct cy8c_data, led);
|
||
|
int ret;
|
||
|
|
||
|
if (atomic_read(&data->device_mode) != DEVICE_MODE_APP) {
|
||
|
dev_err(&data->client->dev, "device not in app mode %s\n",
|
||
|
__func__);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
ret = regmap_write(data->regmap,
|
||
|
P1961_REG_NOM_BRIGHT, value & 0xff);
|
||
|
/* not to write data to eeprom */
|
||
|
#if 0
|
||
|
ret |= regmap_write(data->regmap,
|
||
|
P1961_REG_CMD_DAT, P1961_REG_NOM_BRIGHT);
|
||
|
ret |= regmap_write(data->regmap,
|
||
|
P1961_REG_CMD, P1961_CMD_WRITE_EEPROM);
|
||
|
#endif
|
||
|
if (unlikely(ret))
|
||
|
dev_err(&data->client->dev, "cannot write %d\n", value);
|
||
|
}
|
||
|
|
||
|
static enum led_brightness get_led_brightness(struct led_classdev *led_cdev)
|
||
|
{
|
||
|
unsigned int val;
|
||
|
int ret;
|
||
|
struct cy8c_data *data = container_of(led_cdev, struct cy8c_data, led);
|
||
|
|
||
|
ret = regmap_read(data->regmap, P1961_REG_NOM_BRIGHT, &val);
|
||
|
if (ret)
|
||
|
dev_err(&data->client->dev, "cannot read %d\n", val);
|
||
|
val = val & 0xff;
|
||
|
return val;
|
||
|
}
|
||
|
|
||
|
static int of_led_parse_pdata(struct i2c_client *client,
|
||
|
struct cy8c_data *data)
|
||
|
{
|
||
|
struct device_node *np = client->dev.of_node;
|
||
|
u32 value;
|
||
|
data->led.name = of_get_property(np, "label", NULL) ? : np->name;
|
||
|
|
||
|
data->led_on_plugin = of_property_read_bool(np, "led-on-plugin");
|
||
|
|
||
|
if (of_property_read_u32(np, "default-brightness", &value))
|
||
|
data->default_brightness = 0xff;
|
||
|
else
|
||
|
data->default_brightness = value & 0xff;
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
static int cy8c_debug_set(void *data, u64 val)
|
||
|
{
|
||
|
struct cy8c_data *cy_data = data;
|
||
|
|
||
|
val = val & 0xff;
|
||
|
if (val > P1961_CMD_ENTER_BL)
|
||
|
return -EINVAL;
|
||
|
|
||
|
pr_info("%s: send reset cmd %lld\n", __func__, val);
|
||
|
regmap_write(cy_data->regmap, P1961_REG_CMD, val & 0xff);
|
||
|
return 0;
|
||
|
}
|
||
|
DEFINE_SIMPLE_ATTRIBUTE(cy8c_debug_fops, NULL, cy8c_debug_set, "%lld\n");
|
||
|
|
||
|
static ssize_t cy8c_effects_set(struct device *dev,
|
||
|
struct device_attribute *attr,
|
||
|
const char *buf, size_t size)
|
||
|
{
|
||
|
struct led_classdev *led_cdev = dev_get_drvdata(dev);
|
||
|
struct cy8c_data *data = NULL;
|
||
|
ssize_t ret = -EINVAL;
|
||
|
struct regmap *regmap = NULL;
|
||
|
int i = 0;
|
||
|
int len = 0;
|
||
|
|
||
|
data = container_of(led_cdev, struct cy8c_data, led);
|
||
|
regmap = data->regmap;
|
||
|
|
||
|
if (atomic_read(&data->device_mode) != DEVICE_MODE_APP) {
|
||
|
dev_err(&data->client->dev, "device not in app mode %s\n",
|
||
|
__func__);
|
||
|
return -EAGAIN;
|
||
|
}
|
||
|
|
||
|
if (buf == NULL || buf[0] == 0) {
|
||
|
dev_err(&data->client->dev, "input buf invalid.\n");
|
||
|
return -EINVAL;
|
||
|
}
|
||
|
|
||
|
for (i = 0; i < mode_controls_size; i++) {
|
||
|
/* use strncmp instead of strcmp
|
||
|
strcmp always returns incorrect value
|
||
|
*/
|
||
|
len = min(strlen(buf),
|
||
|
strlen(mode_controls[i].mode_name));
|
||
|
if (!strncmp(buf, mode_controls[i].mode_name, len))
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
if (i == mode_controls_size) {
|
||
|
dev_err(&data->client->dev, "cannot find %s\n", buf);
|
||
|
return -EINVAL;
|
||
|
}
|
||
|
|
||
|
ret = regmap_write(regmap, mode_controls[i].reg_mode,
|
||
|
mode_controls[i].reg_mode_val);
|
||
|
|
||
|
if (ret == 0)
|
||
|
cy8c_set_mode_index(data, i);
|
||
|
|
||
|
return ret == 0 ? size : -ENODEV;
|
||
|
}
|
||
|
|
||
|
static ssize_t cy8c_effects_show(struct device *dev,
|
||
|
struct device_attribute *attr,
|
||
|
char *buf)
|
||
|
{
|
||
|
struct led_classdev *led_cdev = dev_get_drvdata(dev);
|
||
|
struct cy8c_data *data = NULL;
|
||
|
int mode_index = 0;
|
||
|
data = container_of(led_cdev, struct cy8c_data, led);
|
||
|
|
||
|
if (atomic_read(&data->device_mode) != DEVICE_MODE_APP) {
|
||
|
dev_err(&data->client->dev, "device not in app mode %s\n",
|
||
|
__func__);
|
||
|
return -EAGAIN;
|
||
|
}
|
||
|
|
||
|
mode_index = cy8c_get_mode_index(data);
|
||
|
if (mode_index >= mode_controls_size) {
|
||
|
dev_err(&data->client->dev, "mode error\n");
|
||
|
return -EINVAL;
|
||
|
} else
|
||
|
return sprintf(buf, "%s\n",
|
||
|
mode_controls[mode_index].mode_name);
|
||
|
}
|
||
|
|
||
|
static ssize_t cy8c_params_set(struct device *dev,
|
||
|
struct device_attribute *attr,
|
||
|
const char *buf, size_t size)
|
||
|
{
|
||
|
struct led_classdev *led_cdev = dev_get_drvdata(dev);
|
||
|
struct cy8c_data *data = NULL;
|
||
|
ssize_t ret = -EINVAL;
|
||
|
struct regmap *regmap = NULL;
|
||
|
int on_time, off_time;
|
||
|
enum modes mode = 0;
|
||
|
|
||
|
on_time = 0;
|
||
|
off_time = 0;
|
||
|
data = container_of(led_cdev, struct cy8c_data, led);
|
||
|
regmap = data->regmap;
|
||
|
|
||
|
if (atomic_read(&data->device_mode) != DEVICE_MODE_APP) {
|
||
|
dev_err(&data->client->dev, "device not in app mode %s\n",
|
||
|
__func__);
|
||
|
return -EAGAIN;
|
||
|
}
|
||
|
|
||
|
if (buf == NULL || buf[0] == 0) {
|
||
|
dev_err(&data->client->dev, "input buf invalid\n");
|
||
|
return -EINVAL;
|
||
|
}
|
||
|
|
||
|
if (sscanf(buf, "%d %d", &on_time, &off_time) != 2) {
|
||
|
dev_err(&data->client->dev, "input data format invalid\n");
|
||
|
return -EINVAL;
|
||
|
}
|
||
|
|
||
|
if (on_time < 0 || on_time > 255) {
|
||
|
dev_err(&data->client->dev, "input on time out of range\n");
|
||
|
return -EINVAL;
|
||
|
}
|
||
|
|
||
|
if (off_time < 0 || off_time > 255) {
|
||
|
dev_err(&data->client->dev, "input off time out of range\n");
|
||
|
return -EINVAL;
|
||
|
}
|
||
|
|
||
|
mutex_lock(&data->lock);
|
||
|
mode = mode_controls[data->mode_index].mode;
|
||
|
if (mode != MODE_NORMAL) {
|
||
|
ret = regmap_write(regmap,
|
||
|
mode_controls[data->mode_index].reg_on_reg,
|
||
|
on_time);
|
||
|
ret |= regmap_write(regmap,
|
||
|
mode_controls[data->mode_index].reg_off_reg,
|
||
|
off_time);
|
||
|
} else {
|
||
|
dev_err(&data->client->dev, "mode in normal\n");
|
||
|
}
|
||
|
mutex_unlock(&data->lock);
|
||
|
|
||
|
return ret == 0 ? size : ret;
|
||
|
}
|
||
|
|
||
|
static ssize_t cy8c_params_show(struct device *dev,
|
||
|
struct device_attribute *attr,
|
||
|
char *buf)
|
||
|
{
|
||
|
struct led_classdev *led_cdev = dev_get_drvdata(dev);
|
||
|
struct cy8c_data *data = NULL;
|
||
|
int on_time, off_time;
|
||
|
enum modes mode = 0;
|
||
|
ssize_t ret = -EINVAL;
|
||
|
data = container_of(led_cdev, struct cy8c_data, led);
|
||
|
on_time = off_time = 0;
|
||
|
|
||
|
if (atomic_read(&data->device_mode) != DEVICE_MODE_APP) {
|
||
|
dev_err(&data->client->dev, "device not in app mode %s\n",
|
||
|
__func__);
|
||
|
return -EAGAIN;
|
||
|
}
|
||
|
|
||
|
mutex_lock(&data->lock);
|
||
|
if (data->mode_index >= mode_controls_size) {
|
||
|
mutex_unlock(&data->lock);
|
||
|
return -EINVAL;
|
||
|
}
|
||
|
|
||
|
mode = mode_controls[data->mode_index].mode;
|
||
|
|
||
|
if (mode != MODE_NORMAL) {
|
||
|
ret = regmap_read(data->regmap,
|
||
|
mode_controls[data->mode_index].reg_on_reg,
|
||
|
&on_time);
|
||
|
ret |= regmap_read(data->regmap,
|
||
|
mode_controls[data->mode_index].reg_off_reg,
|
||
|
&off_time);
|
||
|
} else {
|
||
|
dev_err(&data->client->dev, "mode in normal\n");
|
||
|
}
|
||
|
mutex_unlock(&data->lock);
|
||
|
|
||
|
if (ret)
|
||
|
return ret;
|
||
|
else
|
||
|
return sprintf(buf, "%d %d\n", on_time, off_time);
|
||
|
}
|
||
|
|
||
|
static unsigned short cy8c_crc_algo_sum(
|
||
|
unsigned char *buf, unsigned long size)
|
||
|
{
|
||
|
unsigned short sum = 0;
|
||
|
while (size-- > 0)
|
||
|
sum += *buf++;
|
||
|
|
||
|
return 1 + ~sum;
|
||
|
}
|
||
|
|
||
|
static unsigned short cy8c_crc_algo_crc(
|
||
|
unsigned char *buf, unsigned long size)
|
||
|
{
|
||
|
unsigned short crc = 0xffff;
|
||
|
unsigned short tmp;
|
||
|
int i;
|
||
|
|
||
|
if (size == 0)
|
||
|
return ~crc;
|
||
|
|
||
|
do {
|
||
|
for (i = 0, tmp = 0x00ff & *buf++;
|
||
|
i < 8;
|
||
|
i++, tmp >>= 1) {
|
||
|
if ((crc & 0x0001) ^ (tmp & 0x0001))
|
||
|
crc = (crc >> 1) ^ 0x8408;
|
||
|
else
|
||
|
crc >>= 1;
|
||
|
}
|
||
|
} while (--size);
|
||
|
|
||
|
crc = ~crc;
|
||
|
tmp = crc;
|
||
|
crc = (crc << 8) | (tmp >> 8 & 0xFF);
|
||
|
|
||
|
return crc;
|
||
|
}
|
||
|
|
||
|
static cy8c_crc_algo_t crc_algos[] = {
|
||
|
&cy8c_crc_algo_sum,
|
||
|
&cy8c_crc_algo_crc,
|
||
|
};
|
||
|
|
||
|
static int cy8c_send_app_mode(struct cy8c_data *data,
|
||
|
cy8c_crc_algo_t crc_func)
|
||
|
{
|
||
|
unsigned char cmd_buf[COMMAND_SIZE] = {0, };
|
||
|
unsigned short checksum;
|
||
|
unsigned long res_size;
|
||
|
unsigned long cmd_size;
|
||
|
int ret;
|
||
|
|
||
|
if (COMMAND_SIZE < 3)
|
||
|
return -EINVAL;
|
||
|
|
||
|
res_size = BASE_CMD_SIZE;
|
||
|
cmd_size = COMMAND_SIZE;
|
||
|
cmd_buf[0] = CMD_START;
|
||
|
cmd_buf[1] = CMD_EXIT_BOOTLOADER;
|
||
|
cmd_buf[2] = (unsigned char)COMMAND_DATA_SIZE;
|
||
|
cmd_buf[3] = (unsigned char)(COMMAND_DATA_SIZE >> 8);
|
||
|
cmd_buf[4] = RESET;
|
||
|
checksum = (*crc_func)(cmd_buf, COMMAND_SIZE - 3);
|
||
|
cmd_buf[5] = (unsigned char)checksum;
|
||
|
cmd_buf[6] = (unsigned char)(checksum >> 8);
|
||
|
cmd_buf[7] = CMD_STOP;
|
||
|
|
||
|
ret = i2c_master_send(data->client_boot, cmd_buf, cmd_size);
|
||
|
|
||
|
return ret;
|
||
|
}
|
||
|
|
||
|
static int cy8c_boot_mode_enter(struct cy8c_data *data,
|
||
|
cy8c_crc_algo_t crc_func)
|
||
|
{
|
||
|
const unsigned long RESULT_DATA_SIZE = 8;
|
||
|
unsigned short checksum;
|
||
|
unsigned char cmd_buf[BASE_CMD_SIZE];
|
||
|
unsigned char res_buf[BASE_CMD_SIZE + RESULT_DATA_SIZE];
|
||
|
int res_size, cmd_size;
|
||
|
int ret;
|
||
|
|
||
|
if (COMMAND_SIZE < 3)
|
||
|
return -EINVAL;
|
||
|
|
||
|
res_size = BASE_CMD_SIZE + RESULT_DATA_SIZE;
|
||
|
cmd_size = BASE_CMD_SIZE;
|
||
|
cmd_buf[0] = CMD_START;
|
||
|
cmd_buf[1] = CMD_ENTER_BOOTLOADER;
|
||
|
cmd_buf[2] = 0;
|
||
|
cmd_buf[3] = 0;
|
||
|
checksum = (*crc_func)(cmd_buf, BASE_CMD_SIZE - 3);
|
||
|
cmd_buf[4] = (unsigned char)checksum;
|
||
|
cmd_buf[5] = (unsigned char)(checksum >> 8);
|
||
|
cmd_buf[6] = CMD_STOP;
|
||
|
|
||
|
ret = i2c_master_send(data->client_boot, cmd_buf, cmd_size);
|
||
|
if (ret < 0)
|
||
|
return ret;
|
||
|
|
||
|
ret = i2c_master_recv(data->client_boot, res_buf, res_size);
|
||
|
|
||
|
return ret;
|
||
|
}
|
||
|
|
||
|
static int cy8c_boot_mode_test(struct cy8c_data *data)
|
||
|
{
|
||
|
int i, ret;
|
||
|
for (i = 0; i < sizeof(crc_algos)/sizeof(crc_algos[0]); i++) {
|
||
|
ret = cy8c_boot_mode_enter(data, crc_algos[i]);
|
||
|
if (ret > 0)
|
||
|
ret = 0;
|
||
|
else
|
||
|
dev_err(&data->client->dev,
|
||
|
"device not in boot mode\n");
|
||
|
|
||
|
if (ret == 0)
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
return ret;
|
||
|
}
|
||
|
|
||
|
static int cy8c_app_mode_test(struct cy8c_data *data)
|
||
|
{
|
||
|
int ret, reg;
|
||
|
ret = regmap_read(data->regmap, P1961_REG_APP_MINOR_REV, ®);
|
||
|
return ret;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
app mode
|
||
|
called from user space
|
||
|
|
||
|
The packet sent to device in boot mode needs a crc value
|
||
|
as parameter. There are 2 algorithms to compute the crc
|
||
|
according to the content of the fw file in user space code
|
||
|
implementation at vendor/bin/vendor/nvidia/loki/utils/cyload.
|
||
|
|
||
|
In the current design the 'sum' algo is used to compute
|
||
|
crc, but we will support both of the ALGOes just in case.
|
||
|
*/
|
||
|
static int cy8c_boot_to_app(struct cy8c_data *data)
|
||
|
{
|
||
|
int i, ret;
|
||
|
|
||
|
for (i = 0; i < sizeof(crc_algos)/sizeof(crc_algos[0]); i++) {
|
||
|
ret = cy8c_send_app_mode(data, crc_algos[i]);
|
||
|
if (ret > 0)
|
||
|
ret = 0;
|
||
|
else
|
||
|
dev_err(&data->client->dev,
|
||
|
"cannot put device to app mode\n");
|
||
|
|
||
|
if (ret == 0) {
|
||
|
|
||
|
/* 100 ms is enough in test */
|
||
|
msleep(100);
|
||
|
|
||
|
ret = cy8c_app_mode_test(data);
|
||
|
if (unlikely(ret))
|
||
|
dev_err(&data->client->dev,
|
||
|
"device not in app mode %d\n", i);
|
||
|
else
|
||
|
atomic_set(&data->device_mode, DEVICE_MODE_APP);
|
||
|
}
|
||
|
|
||
|
if (ret == 0)
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
return ret;
|
||
|
}
|
||
|
|
||
|
static ssize_t cy8c_boot_mode_show(struct device *dev,
|
||
|
struct device_attribute *attr,
|
||
|
char *buf)
|
||
|
{
|
||
|
int device_mode = -1;
|
||
|
struct led_classdev *led_cdev = dev_get_drvdata(dev);
|
||
|
struct cy8c_data *data = NULL;
|
||
|
char *name = "unknown";
|
||
|
|
||
|
data = container_of(led_cdev, struct cy8c_data, led);
|
||
|
|
||
|
device_mode = atomic_read(&data->device_mode);
|
||
|
|
||
|
if (device_mode == DEVICE_MODE_APP)
|
||
|
name = "app";
|
||
|
else if (device_mode == DEVICE_MODE_BOOT)
|
||
|
name = "boot";
|
||
|
|
||
|
return sprintf(buf, "%s\n", name);
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
0 -> APP mode
|
||
|
1 -> Boot mode
|
||
|
*/
|
||
|
static ssize_t cy8c_boot_mode_set(struct device *dev,
|
||
|
struct device_attribute *attr,
|
||
|
const char *buf, size_t size)
|
||
|
{
|
||
|
struct led_classdev *led_cdev = dev_get_drvdata(dev);
|
||
|
struct cy8c_data *data = NULL;
|
||
|
ssize_t ret = -EINVAL;
|
||
|
struct regmap *regmap = NULL;
|
||
|
int action = -1;
|
||
|
|
||
|
data = container_of(led_cdev, struct cy8c_data, led);
|
||
|
regmap = data->regmap;
|
||
|
|
||
|
if (buf == NULL || buf[0] == 0) {
|
||
|
dev_err(&data->client->dev, "input buf invalid\n");
|
||
|
return -EINVAL;
|
||
|
}
|
||
|
|
||
|
if (sscanf(buf, "%d", &action) != 1) {
|
||
|
dev_err(&data->client->dev, "input data format invalid\n");
|
||
|
return -EINVAL;
|
||
|
}
|
||
|
|
||
|
if (action != 0 && action != 1) {
|
||
|
dev_err(&data->client->dev, "input data format value\n");
|
||
|
return -EINVAL;
|
||
|
}
|
||
|
|
||
|
/* boot mode */
|
||
|
if (action) {
|
||
|
ret = regmap_write(regmap,
|
||
|
P1961_REG_CMD,
|
||
|
P1961_CMD_GOTO_BOOT);
|
||
|
|
||
|
if (unlikely(ret))
|
||
|
dev_err(&data->client->dev, "cannot put dev to boot mode\n");
|
||
|
else
|
||
|
atomic_set(&data->device_mode, DEVICE_MODE_BOOT);
|
||
|
} else if (!action && attr == NULL) {
|
||
|
/*
|
||
|
app mode
|
||
|
called from boot device close
|
||
|
*/
|
||
|
ret = cy8c_app_mode_test(data);
|
||
|
|
||
|
if (unlikely(ret)) {
|
||
|
dev_err(&data->client->dev, "device not in app mode %s\n",
|
||
|
__func__);
|
||
|
dev_err(&data->client->dev, "try to jump to app %s\n",
|
||
|
__func__);
|
||
|
ret = cy8c_boot_to_app(data);
|
||
|
if (ret == 0)
|
||
|
atomic_set(&data->device_mode, DEVICE_MODE_APP);
|
||
|
} else
|
||
|
atomic_set(&data->device_mode, DEVICE_MODE_APP);
|
||
|
} else if (!action && attr != NULL)
|
||
|
ret = cy8c_boot_to_app(data);
|
||
|
|
||
|
return ret == 0 ? size : ret;
|
||
|
}
|
||
|
|
||
|
static ssize_t cy8c_version_show(struct device *dev,
|
||
|
struct device_attribute *attr,
|
||
|
char *buf)
|
||
|
{
|
||
|
struct led_classdev *led_cdev = dev_get_drvdata(dev);
|
||
|
struct cy8c_data *data = NULL;
|
||
|
ssize_t ret = 0;
|
||
|
ssize_t count = 0;
|
||
|
int version = -1;
|
||
|
data = container_of(led_cdev, struct cy8c_data, led);
|
||
|
|
||
|
if (atomic_read(&data->device_mode) != DEVICE_MODE_APP) {
|
||
|
dev_err(&data->client->dev, "device not in app mode %s\n",
|
||
|
__func__);
|
||
|
return -EAGAIN;
|
||
|
}
|
||
|
|
||
|
ret = regmap_read(data->regmap,
|
||
|
P1961_REG_APP_MINOR_REV,
|
||
|
&version);
|
||
|
if (unlikely(ret)) {
|
||
|
dev_err(&data->client->dev, "device in boot mode?\n");
|
||
|
return ret;
|
||
|
}
|
||
|
count += sprintf(buf, "%02x: %02x\n", P1961_REG_APP_MINOR_REV, version);
|
||
|
|
||
|
ret = regmap_read(data->regmap,
|
||
|
P1961_REG_APP_MAJOR_REV,
|
||
|
&version);
|
||
|
if (unlikely(ret)) {
|
||
|
dev_err(&data->client->dev, "device in boot mode?\n");
|
||
|
return ret;
|
||
|
}
|
||
|
count += sprintf(buf + count, "%02x: %02x\n",
|
||
|
P1961_REG_APP_MAJOR_REV, version);
|
||
|
|
||
|
return count;
|
||
|
}
|
||
|
|
||
|
static ssize_t cy8c_version_set(struct device *dev,
|
||
|
struct device_attribute *attr,
|
||
|
const char *buf, size_t size)
|
||
|
{
|
||
|
struct led_classdev *led_cdev = dev_get_drvdata(dev);
|
||
|
struct cy8c_data *data = NULL;
|
||
|
data = container_of(led_cdev, struct cy8c_data, led);
|
||
|
dev_err(&data->client->dev, "not implemented\n");
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
static DEVICE_ATTR(effects, S_IRUGO|S_IWUSR,
|
||
|
cy8c_effects_show, cy8c_effects_set);
|
||
|
static DEVICE_ATTR(params, S_IRUGO|S_IWUSR,
|
||
|
cy8c_params_show, cy8c_params_set);
|
||
|
static DEVICE_ATTR(boot_mode, S_IRUGO|S_IWUSR,
|
||
|
cy8c_boot_mode_show, cy8c_boot_mode_set);
|
||
|
static DEVICE_ATTR(version, S_IRUGO|S_IWUSR,
|
||
|
cy8c_version_show, cy8c_version_set);
|
||
|
|
||
|
static int led_boot_open(struct inode *inode, struct file *file)
|
||
|
{
|
||
|
struct miscdevice *miscdev = (struct miscdevice *)file->private_data;
|
||
|
struct cy8c_data *data = NULL;
|
||
|
data = (struct cy8c_data *)container_of(miscdev,
|
||
|
struct cy8c_data, miscdev);
|
||
|
|
||
|
if (atomic_read(&data->device_mode) != DEVICE_MODE_BOOT) {
|
||
|
dev_err(&data->client->dev,
|
||
|
"wait for device in boot mode\n");
|
||
|
return -EIO;
|
||
|
}
|
||
|
|
||
|
if (atomic_xchg(&data->in_use, 1)) {
|
||
|
dev_err(&data->client->dev, "already opened\n");
|
||
|
return -EBUSY;
|
||
|
}
|
||
|
file->private_data = data;
|
||
|
dev_err(&data->client->dev, "opened\n");
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
static int led_boot_release(struct inode *inode, struct file *file)
|
||
|
{
|
||
|
struct cy8c_data *data = NULL;
|
||
|
int ret;
|
||
|
data = file->private_data;
|
||
|
file->private_data = NULL;
|
||
|
WARN_ON(!atomic_xchg(&data->in_use, 0));
|
||
|
|
||
|
ret = cy8c_boot_mode_set(data->led.dev, NULL, "0", 1);
|
||
|
if (ret > 0)
|
||
|
ret = 0;
|
||
|
|
||
|
dev_err(&data->client->dev, "released\n");
|
||
|
|
||
|
return ret;
|
||
|
}
|
||
|
|
||
|
static ssize_t led_boot_read(struct file *filp, char __user *buf,
|
||
|
size_t count, loff_t *offset)
|
||
|
{
|
||
|
struct cy8c_data *data = filp->private_data;
|
||
|
unsigned char *p = NULL;
|
||
|
int ret;
|
||
|
|
||
|
if (atomic_read(&data->device_mode) != DEVICE_MODE_BOOT) {
|
||
|
dev_err(&data->client->dev, "device not in boot mode\n");
|
||
|
return -EINVAL;
|
||
|
}
|
||
|
|
||
|
if (count == 0) {
|
||
|
dev_err(&data->client->dev, "no data\n");
|
||
|
return -EINVAL;
|
||
|
}
|
||
|
|
||
|
p = kzalloc(count, GFP_KERNEL);
|
||
|
|
||
|
if (p == NULL) {
|
||
|
dev_err(&data->client->dev, "no memory\n");
|
||
|
return -ENOMEM;
|
||
|
}
|
||
|
|
||
|
/* Read data */
|
||
|
ret = i2c_master_recv(data->client_boot, p, count);
|
||
|
if (ret != count) {
|
||
|
dev_err(&data->client->dev,
|
||
|
"failed to read %d\n", ret);
|
||
|
ret = -EIO;
|
||
|
goto end;
|
||
|
}
|
||
|
|
||
|
ret = copy_to_user(buf, p, count);
|
||
|
if (ret) {
|
||
|
dev_err(&data->client->dev,
|
||
|
"failed to copy from user space\n");
|
||
|
}
|
||
|
|
||
|
end:
|
||
|
if (p)
|
||
|
kfree(p);
|
||
|
|
||
|
return ret == 0 ? count : ret;
|
||
|
}
|
||
|
|
||
|
static ssize_t led_boot_write(struct file *filp, const char __user *buf,
|
||
|
size_t count, loff_t *offset)
|
||
|
{
|
||
|
struct cy8c_data *data = filp->private_data;
|
||
|
unsigned char *p = NULL;
|
||
|
int ret;
|
||
|
|
||
|
if (atomic_read(&data->device_mode) != DEVICE_MODE_BOOT) {
|
||
|
dev_err(&data->client->dev, "device not in boot mode\n");
|
||
|
return -EINVAL;
|
||
|
}
|
||
|
|
||
|
if (count == 0) {
|
||
|
dev_err(&data->client->dev, "no data\n");
|
||
|
return -EINVAL;
|
||
|
}
|
||
|
|
||
|
p = kzalloc(count, GFP_KERNEL);
|
||
|
|
||
|
if (p == NULL) {
|
||
|
dev_err(&data->client->dev, "no memory\n");
|
||
|
return -ENOMEM;
|
||
|
}
|
||
|
|
||
|
if (copy_from_user(p, buf, count)) {
|
||
|
dev_err(&data->client->dev,
|
||
|
"failed to copy from user space\n");
|
||
|
ret = -EFAULT;
|
||
|
goto end;
|
||
|
}
|
||
|
|
||
|
/* Write data */
|
||
|
ret = i2c_master_send(data->client_boot, p, count);
|
||
|
if (ret != count) {
|
||
|
dev_err(&data->client->dev,
|
||
|
"failed to write %d\n", ret);
|
||
|
ret = -EIO;
|
||
|
}
|
||
|
|
||
|
end:
|
||
|
if (p)
|
||
|
kfree(p);
|
||
|
|
||
|
return ret;
|
||
|
}
|
||
|
|
||
|
static const struct file_operations led_boot_fileops = {
|
||
|
.owner = THIS_MODULE,
|
||
|
.open = led_boot_open,
|
||
|
.release = led_boot_release,
|
||
|
.read = led_boot_read,
|
||
|
.write = led_boot_write,
|
||
|
};
|
||
|
|
||
|
static int cy8c_apply_default_settings(struct cy8c_data *data)
|
||
|
{
|
||
|
int ret;
|
||
|
u8 state;
|
||
|
|
||
|
if (!data)
|
||
|
return -EINVAL;
|
||
|
|
||
|
pr_debug("%s: led-on-plugin %d, default-brightness %d\n", __func__,
|
||
|
data->led_on_plugin, data->default_brightness);
|
||
|
ret = regmap_write(data->regmap, P1961_REG_NOM_BRIGHT,
|
||
|
data->default_brightness);
|
||
|
ret |= regmap_write(data->regmap, P1961_REG_CMD_DAT,
|
||
|
P1961_REG_NOM_BRIGHT);
|
||
|
ret |= regmap_write(data->regmap,
|
||
|
P1961_REG_CMD, P1961_CMD_WRITE_EEPROM);
|
||
|
if (ret)
|
||
|
dev_err(&data->client->dev, "cannot write brightness\n");
|
||
|
|
||
|
if (data->led_on_plugin)
|
||
|
state = P1961_LED_STATE_OFF;
|
||
|
else
|
||
|
state = P1961_LED_STATE_SOLID;
|
||
|
|
||
|
ret = regmap_write(data->regmap, P1961_REG_LED_STATE, state);
|
||
|
ret |= regmap_write(data->regmap, P1961_REG_CMD_DAT,
|
||
|
P1961_REG_LED_STATE);
|
||
|
ret |= regmap_write(data->regmap,
|
||
|
P1961_REG_CMD, P1961_CMD_WRITE_EEPROM);
|
||
|
if (ret)
|
||
|
dev_err(&data->client->dev, "cannot write led state\n");
|
||
|
return ret;
|
||
|
}
|
||
|
|
||
|
static int cy8c_led_probe(struct i2c_client *client,
|
||
|
const struct i2c_device_id *id)
|
||
|
{
|
||
|
struct i2c_adapter *adapter = client->adapter;
|
||
|
struct cy8c_data *data;
|
||
|
struct regmap_config rconfig;
|
||
|
int ret, reg;
|
||
|
struct dentry *d;
|
||
|
|
||
|
if (!i2c_check_functionality(adapter, I2C_FUNC_SMBUS_BYTE_DATA)) {
|
||
|
dev_err(&client->dev, "i2c functionality check fail.\n");
|
||
|
return -ENODEV;
|
||
|
}
|
||
|
|
||
|
data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
|
||
|
if (!data)
|
||
|
return -ENOMEM;
|
||
|
|
||
|
/* enable print in show/get */
|
||
|
data->client = client;
|
||
|
|
||
|
of_led_parse_pdata(client, data);
|
||
|
|
||
|
data->led.brightness_set = set_led_brightness;
|
||
|
data->led.brightness_get = get_led_brightness;
|
||
|
|
||
|
memset(&rconfig, 0, sizeof(rconfig));
|
||
|
rconfig.reg_bits = 8;
|
||
|
rconfig.val_bits = 8;
|
||
|
rconfig.cache_type = REGCACHE_NONE;
|
||
|
rconfig.max_register = P1961_REG_MAX-1;
|
||
|
|
||
|
/*This should happen before set clientdata*/
|
||
|
data->regmap = regmap_init_i2c(client, &rconfig);
|
||
|
if (!data->regmap) {
|
||
|
devm_kfree(&client->dev, data);
|
||
|
dev_err(&client->dev, "Failed to allocate register map\n");
|
||
|
return -ENOMEM;
|
||
|
}
|
||
|
|
||
|
/* enable print in show/get */
|
||
|
data->client = client;
|
||
|
|
||
|
/* boot device client */
|
||
|
data->adap = i2c_get_adapter(client->adapter->nr);
|
||
|
memset(&data->brd_boot, 0, sizeof(data->brd_boot));
|
||
|
strncpy(data->brd_boot.type, DEVICE_NAME, sizeof(data->brd_boot.type));
|
||
|
data->brd_boot.addr = P1961_BOOT_DEV_ADDR;
|
||
|
data->client_boot = i2c_new_device(data->adap, &data->brd_boot);
|
||
|
|
||
|
i2c_set_clientdata(client, data);
|
||
|
|
||
|
/*
|
||
|
Sometimes the app fw is broken or out of integration,
|
||
|
so we will detect app mode first then boot mode
|
||
|
*/
|
||
|
ret = regmap_read(data->regmap, P1961_REG_APP_MAJOR_REV, ®);
|
||
|
if (ret == 0) {
|
||
|
dev_dbg(&client->dev, "[nv-foster] rev: 0x%02x ", reg);
|
||
|
|
||
|
ret = regmap_read(data->regmap, P1961_REG_APP_MINOR_REV, ®);
|
||
|
if (ret) {
|
||
|
dev_err(&client->dev, "Failed to read revision-minor\n");
|
||
|
goto err1;
|
||
|
}
|
||
|
|
||
|
dev_dbg(&client->dev, "0x%02x\n", reg);
|
||
|
|
||
|
ret = cy8c_apply_default_settings(data);
|
||
|
if (ret) {
|
||
|
dev_err(&client->dev, "Failed to read revision-minor\n");
|
||
|
goto err1;
|
||
|
}
|
||
|
atomic_set(&data->device_mode, DEVICE_MODE_APP);
|
||
|
} else {
|
||
|
|
||
|
dev_dbg(&client->dev,
|
||
|
"[nv-foster] not detect app device\n");
|
||
|
dev_dbg(&client->dev,
|
||
|
"[nv-foster] continue to detect boot device\n");
|
||
|
|
||
|
/* Detect whether boot device is available */
|
||
|
ret = cy8c_boot_mode_test(data);
|
||
|
if (ret == 0) {
|
||
|
dev_dbg(&client->dev,
|
||
|
"[nv-foster] boot device detected\n");
|
||
|
atomic_set(&data->device_mode, DEVICE_MODE_BOOT);
|
||
|
} else {
|
||
|
dev_dbg(&client->dev,
|
||
|
"[nv-foster] boot device not detected\n");
|
||
|
goto err1;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/* initially mode index */
|
||
|
data->mode_index = 2;
|
||
|
|
||
|
mutex_init(&data->lock);
|
||
|
|
||
|
cy8c_apply_default_settings(data);
|
||
|
|
||
|
ret = led_classdev_register(&client->dev, &data->led);
|
||
|
if (ret < 0) {
|
||
|
dev_err(&client->dev, "Failed to register foster led\n");
|
||
|
goto err1;
|
||
|
}
|
||
|
else
|
||
|
dev_info(&client->dev, "LED registered (%s)\n", data->led.name);
|
||
|
|
||
|
ret = device_create_file(data->led.dev, &dev_attr_effects);
|
||
|
if (ret) {
|
||
|
dev_err(&client->dev, "Failed to register effects sys node\n");
|
||
|
goto err2;
|
||
|
}
|
||
|
ret = device_create_file(data->led.dev, &dev_attr_params);
|
||
|
if (ret) {
|
||
|
dev_err(&client->dev, "Failed to register params sys node\n");
|
||
|
goto err3;
|
||
|
}
|
||
|
ret = device_create_file(data->led.dev, &dev_attr_boot_mode);
|
||
|
if (ret) {
|
||
|
dev_err(&client->dev, "Failed to register boot sys node\n");
|
||
|
goto err4;
|
||
|
}
|
||
|
ret = device_create_file(data->led.dev, &dev_attr_version);
|
||
|
if (ret) {
|
||
|
dev_err(&client->dev, "Failed to register version sys node\n");
|
||
|
goto err5;
|
||
|
}
|
||
|
|
||
|
data->miscdev.name = DEVICE_NAME;
|
||
|
data->miscdev.fops = &led_boot_fileops;
|
||
|
data->miscdev.minor = MISC_DYNAMIC_MINOR;
|
||
|
ret = misc_register(&data->miscdev);
|
||
|
if (ret) {
|
||
|
dev_err(&client->dev, "%s unable to register misc device %s\n",
|
||
|
__func__, DEVICE_NAME);
|
||
|
goto err6;
|
||
|
}
|
||
|
|
||
|
/* create debugfs for f/w loading purpose */
|
||
|
d = debugfs_create_file("cy8c_led", S_IRUGO, NULL, data,
|
||
|
&cy8c_debug_fops);
|
||
|
if (!d)
|
||
|
pr_err("Failed to create suspend_mode debug file\n");
|
||
|
|
||
|
return ret;
|
||
|
|
||
|
err6:
|
||
|
device_remove_file(data->led.dev, &dev_attr_version);
|
||
|
|
||
|
err5:
|
||
|
device_remove_file(data->led.dev, &dev_attr_boot_mode);
|
||
|
|
||
|
err4:
|
||
|
device_remove_file(data->led.dev, &dev_attr_params);
|
||
|
|
||
|
err3:
|
||
|
device_remove_file(data->led.dev, &dev_attr_effects);
|
||
|
|
||
|
err2:
|
||
|
led_classdev_unregister(&data->led);
|
||
|
|
||
|
err1:
|
||
|
if (data->client_boot)
|
||
|
i2c_unregister_device(data->client_boot);
|
||
|
if (data->adap)
|
||
|
i2c_put_adapter(data->adap);
|
||
|
|
||
|
devm_kfree(&client->dev, data);
|
||
|
|
||
|
return ret;
|
||
|
}
|
||
|
|
||
|
static int cy8c_led_remove(struct i2c_client *client)
|
||
|
{
|
||
|
struct cy8c_data *data = i2c_get_clientdata(client);
|
||
|
|
||
|
if (data->client_boot)
|
||
|
i2c_unregister_device(data->client_boot);
|
||
|
if (data->adap)
|
||
|
i2c_put_adapter(data->adap);
|
||
|
if (data->miscdev.this_device)
|
||
|
misc_deregister(&data->miscdev);
|
||
|
device_remove_file(data->led.dev, &dev_attr_effects);
|
||
|
device_remove_file(data->led.dev, &dev_attr_params);
|
||
|
device_remove_file(data->led.dev, &dev_attr_boot_mode);
|
||
|
device_remove_file(data->led.dev, &dev_attr_version);
|
||
|
led_classdev_unregister(&data->led);
|
||
|
mutex_destroy(&data->lock);
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
static const struct i2c_device_id cy8c_led_id[] = {
|
||
|
{"cy8c_led", 0},
|
||
|
{ }
|
||
|
};
|
||
|
MODULE_DEVICE_TABLE(i2c, cy8c_led_id);
|
||
|
|
||
|
#ifdef CONFIG_OF
|
||
|
static struct of_device_id cy8c_of_match[] = {
|
||
|
{.compatible = "nvidia,cy8c_led", },
|
||
|
{ },
|
||
|
};
|
||
|
#endif
|
||
|
|
||
|
static struct i2c_driver cy8c_led_driver = {
|
||
|
.driver = {
|
||
|
.name = "cy8c_led",
|
||
|
.owner = THIS_MODULE,
|
||
|
#ifdef CONFIG_OF
|
||
|
.of_match_table =
|
||
|
of_match_ptr(cy8c_of_match),
|
||
|
#endif
|
||
|
},
|
||
|
.probe = cy8c_led_probe,
|
||
|
.remove = cy8c_led_remove,
|
||
|
.id_table = cy8c_led_id,
|
||
|
};
|
||
|
|
||
|
module_i2c_driver(cy8c_led_driver);
|
||
|
|
||
|
MODULE_AUTHOR("Vinayak Pane <vpane@nvidia.com>");
|
||
|
MODULE_DESCRIPTION("CY8C I2C based LED controller driver");
|
||
|
MODULE_LICENSE("GPL");
|