From patchwork Fri Jul 24 20:34:04 2020 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Rodrigo Alencar <455.rodrigo.alencar@gmail.com> X-Patchwork-Id: 11684205 Return-Path: Received: from mail.kernel.org (pdx-korg-mail-1.web.codeaurora.org [172.30.200.123]) by pdx-korg-patchwork-2.web.codeaurora.org (Postfix) with ESMTP id C5D50138C for ; Fri, 24 Jul 2020 20:34:26 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id 9D402206F0 for ; Fri, 24 Jul 2020 20:34:26 +0000 (UTC) Authentication-Results: mail.kernel.org; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="OzDh+TuP" Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1726539AbgGXUeV (ORCPT ); Fri, 24 Jul 2020 16:34:21 -0400 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:48022 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1726397AbgGXUeV (ORCPT ); Fri, 24 Jul 2020 16:34:21 -0400 Received: from mail-qt1-x841.google.com (mail-qt1-x841.google.com [IPv6:2607:f8b0:4864:20::841]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id D7F4DC0619D3; Fri, 24 Jul 2020 13:34:20 -0700 (PDT) Received: by mail-qt1-x841.google.com with SMTP id d27so7843420qtg.4; Fri, 24 Jul 2020 13:34:20 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20161025; h=from:to:cc:subject:date:message-id; bh=p2LzMLmXdaFxWrje29B3zR46tvJLJiMHJhNRNQdbLXA=; b=OzDh+TuPJBdQJEWmg9OCniRMFfB822WHIYEZD9d/W6Z0XZGtmKfCaLSKkMQ2kl3tS8 +Z35iFMpN7W1vTul/gmjSKzKoHYTGxl3Gkzy3eXS65DkbxWpuVSoQPbGAgv1LGbaSfsj 5MtUuXBnALaEDQE79FUvHSVMMvlkcTHXxEx/pE5WQfGdBRobhT8POz8iKGky+lvnSihX 1LNNAQqU2lxz0/BUXqxY1sHMBqHIuJA8RSmSYIhD43uqIMVOIgfZcdz5V585xujIGK43 XhssE/RbOBgKupLz1nhmVubQ6BRuiaervchetbDZsH1B8Yylu2+cu4I6PVlWUjJlrl33 b9qw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:from:to:cc:subject:date:message-id; bh=p2LzMLmXdaFxWrje29B3zR46tvJLJiMHJhNRNQdbLXA=; b=UtRPZyI/77wsTafNq/2niLVl9XiwRSNKqVlmLyVsEULITGv4XXhjvVKH1ga10Xjb0c 6NvEUzAvDIrkT1a6TlHhbUFmKoNKB2xMo2xaUJNJBM088EjE6P9HPmUCG100mRP72Lgh 3ju9nb2a0PhicKWezssz5r0bRGmwGBNXwZ6Cvb/ZQHxqIm06nOD8iLJ2bd3iHTihsJeN 0F4KFyelmp1V3Z+9kRIWzLt8Pj9B3DNoTtMG2vFFYbW/PX9+yB8s5dfu2dNWBlhhgGE7 Vq3NdnHyNjS9o5hVjGD4rFjHOlyI7c/Bxo7qPkXVeLbW8Jxn65utcwGSequPsSW9x3sG QB1A== X-Gm-Message-State: AOAM533zpbvtXHxxtUB6vrusr1hiqK5cbQRNfwxLS78378dG/+sGBSc4 0RU1oGt0desr7AoO1VdVJicEC70H+wTLUA== X-Google-Smtp-Source: ABdhPJwIXU4CYbaJE4XRUpUD6GZSh+VMHLIkAgvXLGKFK8wLJ8xhKC8/VL1FngiSolg3YvvgSAHX8w== X-Received: by 2002:ac8:5247:: with SMTP id y7mr11341823qtn.176.1595622859266; Fri, 24 Jul 2020 13:34:19 -0700 (PDT) Received: from sca.dev ([201.17.127.164]) by smtp.gmail.com with ESMTPSA id s5sm7961714qke.120.2020.07.24.13.34.16 (version=TLS1_2 cipher=ECDHE-ECDSA-AES128-GCM-SHA256 bits=128/128); Fri, 24 Jul 2020 13:34:18 -0700 (PDT) From: Rodrigo Alencar <455.rodrigo.alencar@gmail.com> To: linux-fbdev@vger.kernel.org Cc: linux-kernel@vger.kernel.org, dri-devel@lists.freedesktop.org, devicetree@vger.kernel.org, Rodrigo Alencar <455.rodrigo.alencar@gmail.com> Subject: [PATCH] video: fbdev: added driver for sharp memory lcd displays Date: Fri, 24 Jul 2020 17:34:04 -0300 Message-Id: <1595622844-4669-1-git-send-email-455.rodrigo.alencar@gmail.com> X-Mailer: git-send-email 1.9.3 Sender: linux-fbdev-owner@vger.kernel.org Precedence: bulk List-ID: X-Mailing-List: linux-fbdev@vger.kernel.org The supported displays are ls027b7dh01 (tested), ls044q7dh01, ls013b7dh05, ls013b7dh03 Signed-off-by: Rodrigo Alencar <455.rodrigo.alencar@gmail.com> --- .../devicetree/bindings/display/smemlcdfb.txt | 46 ++ drivers/video/fbdev/Kconfig | 14 + drivers/video/fbdev/Makefile | 1 + drivers/video/fbdev/smemlcdfb.c | 485 ++++++++++++++++++ 4 files changed, 546 insertions(+) create mode 100644 Documentation/devicetree/bindings/display/smemlcdfb.txt create mode 100644 drivers/video/fbdev/smemlcdfb.c diff --git a/Documentation/devicetree/bindings/display/smemlcdfb.txt b/Documentation/devicetree/bindings/display/smemlcdfb.txt new file mode 100644 index 000000000000..e33025dd3374 --- /dev/null +++ b/Documentation/devicetree/bindings/display/smemlcdfb.txt @@ -0,0 +1,46 @@ +Sharp Memory LCD Linux Framebuffer Driver + +Required properties: + - compatible: It should be "sharp,". The supported displays are + ls027b7dh01, ls044q7dh01, ls013b7dh05, ls013b7dh03. Maybe other + monochromatic models can be supported with the current code. + - reg: SPI chip select number for the device. + - spi-max-frequency: Max SPI frequency to use. One must verify the datasheet. + - spi-cs-high: Sharp Memory LCD needs chipselect high. + +Optional properties: + - sharp,frames-per-sec: It should contain the desired frames per second. + It does not represent the actual frame rate. Default: 10 + - sharp,extmode-high: External COM Input signal is expected in EXTCOMIN port. + This is recommended to reduce CPU and SPI Load. + - pwm: If property "sharp,extmode-high" is specified, this is recommended. + It should contain the pwm to use, according to + Documentation/devicetree/bindings/pwm/pwm.txt + Verify the display datasheet for the EXTCOMIN signal period + - disp-gpios: The GPIO used to enable the display, if available. See + Documentation/devicetree/bindings/gpio/gpio.txt for details. + +Examples: + +ls027b7dh01: smemlcd@0 { + compatible = "sharp,ls027b7dh01"; + reg = <0>; + spi-max-frequency = <1000000>; + spi-cs-high; + disp-gpios = <&gpio0 7>; + disp-active-high; + sharp,extmode-high; + pwms = <&pwm 0 100000000>; +}; + +ls013b7dh05: smemlcd@3 { + compatible = "sharp,ls013b7dh05"; + reg = <3>; + spi-max-frequency = <1000000>; + spi-cs-high; + disp-gpios = <&gpio0 13>; + disp-active-high; + sharp,extmode-high; + pwms = <&pwm 0 50000000>; + sharp,frames-per-sec = <20>; +}; diff --git a/drivers/video/fbdev/Kconfig b/drivers/video/fbdev/Kconfig index b2c9dd4f0cb5..0fff47a59d8b 100644 --- a/drivers/video/fbdev/Kconfig +++ b/drivers/video/fbdev/Kconfig @@ -2221,6 +2221,20 @@ config FB_SSD1307 This driver implements support for the Solomon SSD1307 OLED controller over I2C. +config FB_SMEMLCD + tristate "Sharp Memory LCD framebuffer support" + depends on FB && SPI + depends on OF + depends on GPIOLIB || COMPILE_TEST + select FB_SYS_FOPS + select FB_SYS_FILLRECT + select FB_SYS_COPYAREA + select FB_SYS_IMAGEBLIT + select FB_DEFERRED_IO + select PWM + help + This driver implements support for the Sharp Memory LCD + config FB_SM712 tristate "Silicon Motion SM712 framebuffer support" depends on FB && PCI diff --git a/drivers/video/fbdev/Makefile b/drivers/video/fbdev/Makefile index cad4fb64442a..5c58dfd8ac08 100644 --- a/drivers/video/fbdev/Makefile +++ b/drivers/video/fbdev/Makefile @@ -128,6 +128,7 @@ obj-$(CONFIG_FB_OF) += offb.o obj-$(CONFIG_FB_MX3) += mx3fb.o obj-$(CONFIG_FB_DA8XX) += da8xx-fb.o obj-$(CONFIG_FB_SSD1307) += ssd1307fb.o +obj-$(CONFIG_FB_SMEMLCD) += smemlcdfb.o obj-$(CONFIG_FB_SIMPLE) += simplefb.o # the test framebuffer is last diff --git a/drivers/video/fbdev/smemlcdfb.c b/drivers/video/fbdev/smemlcdfb.c new file mode 100644 index 000000000000..54a0c57b6713 --- /dev/null +++ b/drivers/video/fbdev/smemlcdfb.c @@ -0,0 +1,485 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2019 IMBEL http://www.imbel.gov.br + * Rodrigo Alencar + */ + +#include +#include +#include +#include +#include +#include +#include + +#define SMEMLCD_DATA_UPDATE 0x80 +#define SMEMLCD_FRAME_INVERSION 0x40 +#define SMEMLCD_ALL_CLEAR 0x20 +#define SMEMLCD_DUMMY_DATA 0x00 + +struct smemlcd_info { + u32 width; + u32 height; +}; + +struct smemlcd_par { + struct spi_device *spi; + struct fb_info *info; + struct pwm_device *extcomin_pwm; + struct gpio_desc *disp; + struct delayed_work d_work; + struct mutex update_lock; + + u8 *spi_buf; + + bool extmode; + u32 spi_width; + u32 vmem_width; + + u8 vcom; + u32 start; + u32 height; +}; + +static const struct fb_fix_screeninfo smemlcd_fix = { + .id = "Sharp Memory LCD", + .type = FB_TYPE_PACKED_PIXELS, + .visual = FB_VISUAL_MONO10, + .xpanstep = 0, + .ypanstep = 0, + .ywrapstep = 0, + .accel = FB_ACCEL_NONE, +}; + +static const struct fb_var_screeninfo smemlcd_var = { + .bits_per_pixel = 1, + .red = { .length = 1 }, + .green = { .length = 1 }, + .blue = { .length = 1 }, +}; + +static void smemlcd_update(struct smemlcd_par *par) +{ + struct spi_device *spi = par->spi; + u8 *vmem = par->info->screen_buffer; + u8 *buf_ptr = par->spi_buf; + int ret; + u32 i,j; + + if (par->start + par->height > par->info->var.yres) { + par->start = 0; + par->height = 0; + } + /* go to start line */ + vmem += par->start * par->vmem_width; + /* update vcom */ + par->vcom ^= SMEMLCD_FRAME_INVERSION; + /* mode selection */ + *(buf_ptr++) = (par->height)? (SMEMLCD_DATA_UPDATE | par->vcom) : par->vcom; + + /* not all SPI masters have LSB-first mode, bitrev8 is used */ + for (i = par->start + 1; i < par->start + par->height + 1; i++) { + /* gate line address */ + *(buf_ptr++) = bitrev8(i); + /* data writing */ + for (j = 0; j < par->spi_width; j++) + *(buf_ptr++) = bitrev8(*(vmem++)); + /* dummy data */ + *(buf_ptr++) = SMEMLCD_DUMMY_DATA; + /* video memory alignment */ + for (; j < par->vmem_width; j++) + vmem++; + } + /* dummy data */ + *(buf_ptr++) = SMEMLCD_DUMMY_DATA; + + ret = spi_write(spi, &(par->spi_buf[0]), par->height * (par->spi_width + 2) + 2); + if (ret < 0) + dev_err(&spi->dev, "Couldn't send SPI command.\n"); + + par->start = U32_MAX; + par->height = 0; +} + +static void smemlcd_frame(struct smemlcd_par *par, u32 req_start, u32 req_height) +{ + u32 end = par->start + par->height; + u32 req_end = req_start + req_height; + if (req_end > par->info->var.yres) + req_end = par->info->var.yres; + if (par->start > req_start) + par->start = req_start; + if (end < req_end || end > par->info->var.yres) + end = req_end; + par->height = end - par->start; +} + +static void smemlcd_fillrect(struct fb_info *info, const struct fb_fillrect *rect) +{ + struct smemlcd_par *par = info->par; + sys_fillrect(info, rect); + + mutex_lock(&par->update_lock); + smemlcd_frame(par, rect->dy, rect->height); + if(par->extmode) + smemlcd_update(par); + mutex_unlock(&par->update_lock); +} + +static void smemlcd_imageblit(struct fb_info *info, const struct fb_image *image) +{ + struct smemlcd_par *par = info->par; + sys_imageblit(info, image); + + mutex_lock(&par->update_lock); + smemlcd_frame(par, image->dy, image->height); + if(par->extmode) + smemlcd_update(par); + mutex_unlock(&par->update_lock); +} + +static void smemlcd_copyarea(struct fb_info *info, const struct fb_copyarea *area) +{ + struct smemlcd_par *par = info->par; + sys_copyarea(info, area); + + mutex_lock(&par->update_lock); + smemlcd_frame(par, area->dy, area->height); + if(par->extmode) + smemlcd_update(par); + mutex_unlock(&par->update_lock); +} + +static ssize_t smemlcd_write(struct fb_info *info, const char __user * buf, size_t count, loff_t * ppos) +{ + ssize_t ret; + struct smemlcd_par *par = info->par; + u32 req_start, req_height; + u32 offset = (u32) * ppos; + + ret = fb_sys_write(info, buf, count, ppos); + if (ret > 0) { + mutex_lock(&par->update_lock); + req_start = max((int)(offset / par->vmem_width), 0); + req_height = ret / par->vmem_width + 1; + smemlcd_frame(par, req_start, req_height); + if(par->extmode) + smemlcd_update(par); + mutex_unlock(&par->update_lock); + } + + return ret; +} + +static int smemlcd_blank(int blank_mode, struct fb_info *info) +{ + struct smemlcd_par *par = info->par; + + if (par->disp) { + if (blank_mode != FB_BLANK_UNBLANK) + gpiod_set_value_cansleep(par->disp, 0); + else + gpiod_set_value_cansleep(par->disp, 1); + } + + return 0; +} + +static void smemlcd_deferred_io(struct fb_info *info, struct list_head *pagelist) +{ + struct smemlcd_par *par = info->par; + + mutex_lock(&par->update_lock); + + if (!list_empty(pagelist)) { + struct page *cur; + u32 req_start; + u32 req_height = (PAGE_SIZE / par->vmem_width) + 1; + + list_for_each_entry(cur, pagelist, lru) { + req_start = (cur->index << PAGE_SHIFT) / par->vmem_width; + smemlcd_frame(par, req_start, req_height); + } + } + + if(par->extmode) + smemlcd_update(par); + mutex_unlock(&par->update_lock); +} + +static void smemlcd_update_work(struct work_struct *work) +{ + struct smemlcd_par *par = container_of(work, struct smemlcd_par, d_work.work); + struct fb_info *info = par->info; + + mutex_lock(&par->update_lock); + smemlcd_update(par); + mutex_unlock(&par->update_lock); + + if (!par->extmode) + schedule_delayed_work(&par->d_work, info->fbdefio->delay); +} + +static const struct fb_ops smemlcd_ops = { + .owner = THIS_MODULE, + .fb_read = fb_sys_read, + .fb_write = smemlcd_write, + .fb_fillrect = smemlcd_fillrect, + .fb_copyarea = smemlcd_copyarea, + .fb_imageblit = smemlcd_imageblit, + .fb_blank = smemlcd_blank, +}; + +static struct smemlcd_info ls027b7dh01_info = { + .width = 400, + .height = 240, +}; + +static struct smemlcd_info ls044q7dh01_info = { + .width = 320, + .height = 240, +}; + +static struct smemlcd_info ls013b7dh05_info = { + .width = 144, + .height = 168, +}; + +static struct smemlcd_info ls013b7dh03_info = { + .width = 128, + .height = 128, +}; + +static const struct of_device_id smemlcd_of_match[] = { + { + .compatible = "sharp,ls027b7dh01", + .data = (void *)&ls027b7dh01_info, + }, + { + .compatible = "sharp,ls044q7dh01", + .data = (void *)&ls044q7dh01_info, + }, + { + .compatible = "sharp,ls013b7dh05", + .data = (void *)&ls013b7dh05_info, + }, + { + .compatible = "sharp,ls013b7dh03", + .data = (void *)&ls013b7dh03_info, + }, + {}, +}; +MODULE_DEVICE_TABLE(of, smemlcd_of_match); + +static int smemlcd_probe(struct spi_device *spi) +{ + struct device *dev = &spi->dev; + struct fb_info *info; + struct smemlcd_par *par; + const struct smemlcd_info *devinfo; + struct fb_deferred_io *smemlcd_defio; + struct pwm_state state; + u32 vmem_size, fps; + void *vmem; + int ret; + + info = framebuffer_alloc(sizeof(struct smemlcd_par), dev); + if (!info) + return -ENOMEM; + + par = info->par; + par->info = info; + par->spi = spi; + + devinfo = device_get_match_data(dev); + + mutex_init(&par->update_lock); + INIT_DELAYED_WORK(&par->d_work, smemlcd_update_work); + par->spi_width = devinfo->width / 8; + par->vmem_width = ((devinfo->width + 31) & ~31) >> 3; + par->vcom = 0; + par->start = 0; + par->height = 0; + + par->disp = devm_gpiod_get_optional(dev, "disp", GPIOD_OUT_LOW); + if (IS_ERR(par->disp)) { + ret = PTR_ERR(par->disp); + dev_err(dev, "Failed to get DISP gpio: %d\n", ret); + goto free_fb; + } + + par->spi_buf = kzalloc(devinfo->height * (par->spi_width + 2) + 2, GFP_KERNEL); + if (!par->spi_buf) { + ret = -ENOMEM; + dev_err(dev, "Failed to allocate data for spi transfers.\n"); + goto free_fb; + } + + vmem_size = par->vmem_width * devinfo->height; + + vmem = (void *)__get_free_pages(GFP_KERNEL | __GFP_ZERO, get_order(vmem_size)); + if (!vmem) { + ret = -ENOMEM; + dev_err(dev, "Failed to allocate video memory.\n"); + goto free_spi_buf; + } + + par->extmode = device_property_read_bool(dev, "sharp,extmode-high"); + + if (device_property_read_u32(dev, "sharp,frames-per-sec", &fps)) + fps = 10; + + smemlcd_defio = devm_kzalloc(dev, sizeof(struct fb_deferred_io), GFP_KERNEL); + if (!smemlcd_defio) { + dev_err(dev, "Couldn't allocate deferred io.\n"); + ret = -ENOMEM; + goto free_vmem; + } + + smemlcd_defio->delay = HZ / fps; + smemlcd_defio->deferred_io = &smemlcd_deferred_io; + + info->var = smemlcd_var; + info->var.xres = devinfo->width; + info->var.xres_virtual = devinfo->width; + info->var.yres = devinfo->height; + info->var.yres_virtual = devinfo->height; + + info->screen_buffer = vmem; + info->screen_size = vmem_size; + + info->fbops = &smemlcd_ops; + info->fix = smemlcd_fix; + info->fix.line_length = par->vmem_width; + info->fix.smem_start = __pa(vmem); + info->fix.smem_len = vmem_size; + + info->fbdefio = smemlcd_defio; + + fb_deferred_io_init(info); + + spi_set_drvdata(spi, par); + + if (par->extmode) { + par->extcomin_pwm = pwm_get(dev, NULL); + if (IS_ERR(par->extcomin_pwm)) { + ret = PTR_ERR(par->extcomin_pwm); + dev_warn(dev, "Failed to get EXTCOMIN pwm: %d\n", ret); + par->extcomin_pwm = NULL; + } else { + + pwm_init_state(par->extcomin_pwm, &state); + + if (!state.period) + state.period = NSEC_PER_SEC/fps; + + state.enabled = true; + state.duty_cycle = state.period/2; + + ret = pwm_apply_state(par->extcomin_pwm, &state); + if (ret) + dev_warn(dev, "failed to apply EXTCOMIN pwm state: %d\n", ret); + } + } else { + par->extcomin_pwm = NULL; + } + + if (par->disp) + gpiod_set_value_cansleep(par->disp, 1); + + /* spi test by clearing the display */ + par->spi_buf[0] = SMEMLCD_ALL_CLEAR; + par->spi_buf[1] = SMEMLCD_DUMMY_DATA; + ret = spi_write(spi, &(par->spi_buf[0]), 2); + if (ret < 0){ + dev_err(dev, "Couldn't send SPI command\n"); + goto disable_hw; + } + + ret = register_framebuffer(info); + if (ret < 0) { + dev_err(dev, "Failed to register framebuffer\n"); + goto disable_hw; + } + + dev_info(dev, "fb%d: %s framebuffer device registered, using %d bytes of video memory\n", info->node, info->fix.id, vmem_size); + + if (!par->extmode) + schedule_delayed_work(&par->d_work, smemlcd_defio->delay); + + return 0; + +disable_hw: + if (par->disp) + gpiod_set_value_cansleep(par->disp, 0); + if (par->extcomin_pwm) { + pwm_disable(par->extcomin_pwm); + pwm_put(par->extcomin_pwm); + } + fb_deferred_io_cleanup(info); +free_vmem: + kfree(vmem); +free_spi_buf: + kfree(par->spi_buf); +free_fb: + cancel_delayed_work_sync(&par->d_work); + mutex_destroy(&par->update_lock); + framebuffer_release(info); + + return ret; +} + +static int smemlcd_remove(struct spi_device *spi) +{ + struct smemlcd_par *par = dev_get_drvdata(&spi->dev); + struct fb_info *info = par->info; + + cancel_delayed_work_sync(&par->d_work); + + fb_deferred_io_cleanup(info); + + unregister_framebuffer(info); + + if (par->disp) + gpiod_set_value_cansleep(par->disp, 0); + + if (par->extcomin_pwm) { + pwm_disable(par->extcomin_pwm); + pwm_put(par->extcomin_pwm); + } + + fb_deferred_io_cleanup(info); + __free_pages(__va(info->fix.smem_start), get_order(info->fix.smem_len)); + + kfree(par->spi_buf); + + mutex_destroy(&par->update_lock); + + framebuffer_release(info); + + return 0; +} + +static const struct spi_device_id smemlcd_spi_id[] = { + {"ls027b7dh01", 0}, + {"ls044q7dh01", 0}, + {"ls013b7dh05", 0}, + {"ls013b7dh03", 0}, + {} +}; +MODULE_DEVICE_TABLE(spi, smemlcd_spi_id); + +static struct spi_driver smemlcd_driver = { + .probe = smemlcd_probe, + .remove = smemlcd_remove, + .id_table = smemlcd_spi_id, + .driver = { + .name = "smemlcdfb", + .of_match_table = smemlcd_of_match, + }, +}; +module_spi_driver(smemlcd_driver); + +MODULE_AUTHOR("Rodrigo Alencar <455.rodrigo.alencar@gmail.com>"); +MODULE_DESCRIPTION("Sharp Memory LCD Linux Framebuffer Driver"); +MODULE_LICENSE("GPL");