[PATCH v1 3/5] tinydrm: add support for parallel data displays

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



Utilising the pardata bus/driver add support for
displays connected to a parallel data bus.
There is no specific protocol implemented,
but the display is tied to the tinydrm pipe.

Only monochrome displays supported using the
XRGB8888 format.

Signed-off-by: Sam Ravnborg <sam@xxxxxxxxxxxx>
---
 drivers/gpu/drm/tinydrm/Kconfig       |   3 +
 drivers/gpu/drm/tinydrm/Makefile      |   1 +
 drivers/gpu/drm/tinydrm/pardata-dbi.c | 417 ++++++++++++++++++++++++++++++++++
 include/drm/tinydrm/pardata-dbi.h     | 257 +++++++++++++++++++++
 4 files changed, 678 insertions(+)
 create mode 100644 drivers/gpu/drm/tinydrm/pardata-dbi.c
 create mode 100644 include/drm/tinydrm/pardata-dbi.h

diff --git a/drivers/gpu/drm/tinydrm/Kconfig b/drivers/gpu/drm/tinydrm/Kconfig
index 4592a5e3f20b..435de2f8d8f5 100644
--- a/drivers/gpu/drm/tinydrm/Kconfig
+++ b/drivers/gpu/drm/tinydrm/Kconfig
@@ -10,6 +10,9 @@ menuconfig DRM_TINYDRM
 config TINYDRM_MIPI_DBI
 	tristate
 
+config TINYDRM_PARDATA_DBI
+	tristate
+
 config TINYDRM_ILI9225
 	tristate "DRM support for ILI9225 display panels"
 	depends on DRM_TINYDRM && SPI
diff --git a/drivers/gpu/drm/tinydrm/Makefile b/drivers/gpu/drm/tinydrm/Makefile
index 49a111929724..0b52df08b0a4 100644
--- a/drivers/gpu/drm/tinydrm/Makefile
+++ b/drivers/gpu/drm/tinydrm/Makefile
@@ -2,6 +2,7 @@ obj-$(CONFIG_DRM_TINYDRM)		+= core/
 
 # Controllers
 obj-$(CONFIG_TINYDRM_MIPI_DBI)		+= mipi-dbi.o
+obj-$(CONFIG_TINYDRM_PARDATA_DBI)	+= pardata-dbi.o
 
 # Displays
 obj-$(CONFIG_TINYDRM_ILI9225)		+= ili9225.o
diff --git a/drivers/gpu/drm/tinydrm/pardata-dbi.c b/drivers/gpu/drm/tinydrm/pardata-dbi.c
new file mode 100644
index 000000000000..09bdfdba6291
--- /dev/null
+++ b/drivers/gpu/drm/tinydrm/pardata-dbi.c
@@ -0,0 +1,417 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Parallel Data Display Bus Interface LCD controller support
+ */
+
+#include <linux/regulator/consumer.h>
+#include <linux/gpio/consumer.h>
+#include <linux/dma-buf.h>
+#include <linux/pardata.h>
+#include <linux/module.h>
+
+#include <drm/drm_gem_framebuffer_helper.h>
+#include <drm/tinydrm/tinydrm-helpers.h>
+#include <drm/tinydrm/pardata-dbi.h>
+
+/**
+ * pardata_strobe_8080_write - using the 8080 interface create
+ *			     an enable strobe
+ *
+ * The interface requires that readwrite and chipselect are
+ * setup before the strobe of "e".
+ * There are timings constraints that is handled using udelay().
+ * Drivers can use this for the strobe_write callback.
+ *
+ * @pdd: pardata data
+ */
+void pardata_strobe_8080_write(struct pardata_data *pdd)
+{
+	gpiod_set_value_cansleep(pdd->bus->pin_readwrite, 0);
+
+	if (pdd->pin_cs)
+		gpiod_set_value_cansleep(pdd->pin_cs, 0);
+	/* min 90 nsec from cs/rs/rw to e */
+	udelay(1);
+	gpiod_set_value_cansleep(pdd->bus->pin_enable, 1);
+	/* data setup time 220 ns*/
+	udelay(2);
+	gpiod_set_value_cansleep(pdd->bus->pin_enable, 0);
+	/* data hold time 20 ns */
+	udelay(1);
+	if (pdd->pin_cs)
+		gpiod_set_value_cansleep(pdd->pin_cs, 1);
+}
+EXPORT_SYMBOL_GPL(pardata_strobe_8080_write);
+
+/**
+ * pardata_strobe_6800_write - using the 6800 interface create
+ *			     an enable strobe
+ *
+ * The interface requires that read + write and chipselect are
+ * setup before the strobe of "e".
+ * There are timings constraints that is handled using udelay()
+ * Drivers can use this for the strobe_write callback.
+ *
+ * @pdd: pardata data
+ */
+
+void pardata_strobe_6800_write(struct pardata_data *pdd)
+{
+	gpiod_set_value_cansleep(pdd->bus->pin_read, 0);
+	gpiod_set_value_cansleep(pdd->bus->pin_write, 1);
+
+	if (pdd->pin_cs)
+		gpiod_set_value_cansleep(pdd->pin_cs, 0);
+	/* min 90 nsec from cs/rs/rw to e */
+	udelay(1);
+	gpiod_set_value_cansleep(pdd->bus->pin_enable, 1);
+	/* data setup time 220 ns*/
+	udelay(2);
+	gpiod_set_value_cansleep(pdd->bus->pin_enable, 0);
+	/* data hold time 20 ns */
+	udelay(1);
+	if (pdd->pin_cs)
+		gpiod_set_value_cansleep(pdd->pin_cs, 1);
+}
+EXPORT_SYMBOL_GPL(pardata_strobe_6800_write);
+
+
+static int pardata_write_clip(struct pardata_data *pdd,
+			      struct drm_framebuffer *fb,
+			      u8 *data,
+			      struct drm_clip_rect *clip)
+{
+	bool line_by_line;
+	size_t linelen;
+	size_t len;
+	int x, y;
+	u8 *buf;
+
+	linelen = clip->x2 - clip->x1;
+	len = linelen * (clip->y2 - clip->y1) / 8;
+	buf = kzalloc(len, GFP_KERNEL);
+	if (!buf)
+		return -ENOMEM;
+
+	/*
+	 * If the clip is the full width then we need only one
+	 * write operation.
+	 */
+	line_by_line = (clip->x2 - clip->x1) != fb->width;
+
+	for (y = clip->y1; y < clip->y2; y++) {
+		for (x = clip->x1; x < clip->x2; x++) {
+			unsigned int index;
+			u8 cc;
+
+			index = x + y * linelen;
+			cc = data[index];
+
+			/* Most significant bit determine bit on/off */
+			if (cc & 0x80)
+				buf[index / 8] |= BIT(index % 8);
+		}
+		if (line_by_line) {
+			int offset;
+
+			offset = y * fb->width + clip->x1;
+			pardata_write_buf(pdd, offset, &buf[offset], linelen);
+		}
+	}
+	if (!line_by_line) {
+		int offset;
+
+		offset = clip->y1 * fb->width;
+		pardata_write_buf(pdd, offset, &buf[offset], len);
+	}
+
+	kfree(buf);
+
+	return 0;
+}
+
+/**
+ * pardata_buf_copy - Copy a framebuffer, transforming it if necessary
+ *
+ * @dst: The destination buffer
+ * @fb: The source framebuffer
+ * @clip: Clipping rectangle of the area to be copied
+ *
+ * Returns:
+ * Zero on success, negative error code on failure.
+ */
+static int pardata_buf_copy(void *dst, struct drm_framebuffer *fb,
+			    struct drm_clip_rect *clip)
+{
+	struct dma_buf_attachment *import_attach;
+	struct drm_gem_cma_object *cma_obj;
+	void *src;
+	int ret;
+
+	cma_obj = drm_fb_cma_get_gem_obj(fb, 0);
+	import_attach = cma_obj->base.import_attach;
+	src = cma_obj->vaddr;
+	ret = 0;
+
+	if (import_attach) {
+		ret = dma_buf_begin_cpu_access(import_attach->dmabuf,
+					       DMA_FROM_DEVICE);
+		if (ret)
+			return ret;
+	}
+
+	tinydrm_xrgb8888_to_gray8(dst, src, fb, clip);
+
+	if (import_attach)
+		ret = dma_buf_end_cpu_access(import_attach->dmabuf,
+					     DMA_FROM_DEVICE);
+	return ret;
+}
+
+static int pardata_fb_dirty(struct drm_framebuffer *fb,
+			    struct drm_file *file_priv,
+			    unsigned int flags,
+			    unsigned int color,
+			    struct drm_clip_rect *clips,
+			    unsigned int num_clips)
+{
+	struct drm_format_name_buf format_name;
+	struct drm_gem_cma_object *cma_obj;
+	struct tinydrm_device *tdev;
+	struct drm_clip_rect clip;
+	struct pardata_data *pdd;
+	bool full;
+	int ret;
+
+	cma_obj = drm_fb_cma_get_gem_obj(fb, 0);
+	tdev = fb->dev->dev_private;
+	pdd = pardata_from_tinydrm(tdev);
+	ret = 0;
+
+	if (!pdd->enabled)
+		return 0;
+
+	if (fb->format->format != DRM_FORMAT_XRGB8888) {
+		dev_err_once(fb->dev->dev, "Format is not supported: %s\n",
+			     drm_get_format_name(fb->format->format,
+						 &format_name));
+		return -EINVAL;
+	}
+
+	full = tinydrm_merge_clips(&clip, clips, num_clips, flags,
+				   fb->width, fb->height);
+
+	DRM_DEV_DEBUG(&pdd->pddev->dev,
+		      "Flushing [FB:%d] x1=%u, x2=%u, y1=%u, y2=%u\n",
+		      fb->base.id, clip.x1, clip.x2, clip.y1, clip.y2);
+
+	if (!full) {
+		ret = pardata_buf_copy(pdd->tx_buf, fb, &clip);
+		if (!ret)
+			ret = pardata_write_clip(pdd, fb, pdd->tx_buf, &clip);
+	} else {
+		size_t len;
+
+		len = fb->width * fb->height;
+		ret = pardata_write_buf(pdd, 0, cma_obj->vaddr, len);
+	}
+
+	return ret;
+}
+
+static const struct drm_framebuffer_funcs mipi_dbi_fb_funcs = {
+	.destroy	= drm_gem_fb_destroy,
+	.create_handle	= drm_gem_fb_create_handle,
+	.dirty		= tinydrm_fb_dirty,
+};
+
+/**
+ * pardata_enable_flush -  enable helper
+ *
+ * @pdd: pardata data
+ * @crtc_state: crtc state
+ * @plane_state: plane state
+ *
+ * This function sets &pdd->enabled, flushes the whole framebuffer and
+ * enables the backlight. Drivers can use this in their
+ * &drm_simple_display_pipe_funcs->enable callback.
+ */
+void pardata_enable_flush(struct pardata_data *pdd,
+			  struct drm_crtc_state *crtc_state,
+			  struct drm_plane_state *plane_state)
+{
+	struct tinydrm_device *tdev;
+	struct drm_framebuffer *fb;
+
+	tdev = &pdd->tinydrm;
+	fb = plane_state->fb;
+	pdd->enabled = true;
+
+	if (fb)
+		tdev->fb_dirty(fb, NULL, 0, 0, NULL, 0);
+
+	backlight_enable(pdd->backlight);
+}
+EXPORT_SYMBOL_GPL(pardata_enable_flush);
+
+static void pardata_blank(struct pardata_data *pdd)
+{
+	struct drm_device *drm;
+	u16 height, width;
+	size_t len;
+
+	drm = pdd->tinydrm.drm;
+	height = drm->mode_config.min_height;
+	width = drm->mode_config.min_width;
+
+	len = width * height;
+
+	memset(pdd->tx_buf, 0, len);
+	pardata_write_buf(pdd, 0, pdd->tx_buf, len);
+}
+
+/**
+ * pardata_pipe_disable - pipe disable helper
+ *
+ * @pipe: Display pipe
+ *
+ * This function disables backlight if present, if not the display memory is
+ * blanked. The regulator is disabled if in use. Drivers can use this as their
+ * &drm_simple_display_pipe_funcs->disable callback.
+ */
+void pardata_pipe_disable(struct drm_simple_display_pipe *pipe)
+{
+	struct tinydrm_device *tdev;
+	struct pardata_data *pdd;
+
+	tdev = pipe_to_tinydrm(pipe);
+	pdd = pardata_from_tinydrm(tdev);
+
+	pdd->enabled = false;
+
+	if (pdd->backlight)
+		backlight_disable(pdd->backlight);
+	else
+		pardata_blank(pdd);
+
+	if (pdd->regulator)
+		regulator_disable(pdd->regulator);
+}
+EXPORT_SYMBOL_GPL(pardata_pipe_disable);
+
+static const uint32_t pardata_formats[] = {
+	DRM_FORMAT_XRGB8888,
+};
+
+/**
+ * pardata_init - initialization
+ * @dev: Parent device
+ * @pdd: pardata data to initialize
+ * @pipe_funcs: Display pipe functions
+ * @driver: DRM driver
+ * @mode: Display mode
+ *
+ * This function initializes a &pardata data structure and it's underlying
+ * @tinydrm_device. It also sets up the display pipeline.
+ *
+ * Supported formats: Emulated XRGB8888.
+ *
+ * Objects created by this function will be automatically freed on driver
+ * detach (devres).
+ *
+ * Returns:
+ * Zero on success, negative error code on failure.
+ */
+int pardata_init(struct device *dev,
+		 struct pardata_data *pdd,
+		 const struct drm_simple_display_pipe_funcs *pipe_funcs,
+		 struct drm_driver *driver,
+		 const struct drm_display_mode *mode)
+{
+	struct tinydrm_device *tdev;
+	size_t bufsize;
+	int ret;
+
+	/* shortcut to pardatabus_data */
+	pdd->bus = dev_get_drvdata(dev->parent);
+
+	tdev = &pdd->tinydrm;
+	bufsize = mode->vdisplay * mode->hdisplay * sizeof(u16);
+
+	pdd->tx_buf = devm_kmalloc(dev, bufsize, GFP_KERNEL);
+	if (!pdd->tx_buf)
+		return -ENOMEM;
+
+	ret = devm_tinydrm_init(dev, tdev, &mipi_dbi_fb_funcs, driver);
+	if (ret)
+		return ret;
+
+	tdev->fb_dirty = pardata_fb_dirty;
+
+	ret = tinydrm_display_pipe_init(tdev, pipe_funcs,
+					DRM_MODE_CONNECTOR_VIRTUAL,
+					pardata_formats,
+					ARRAY_SIZE(pardata_formats),
+					mode,
+					0);
+	if (ret)
+		return ret;
+
+	tdev->drm->mode_config.preferred_depth = 16;
+
+	drm_mode_config_reset(tdev->drm);
+
+	return 0;
+}
+EXPORT_SYMBOL_GPL(pardata_init);
+
+/**
+ * pardata_hw_reset - Hardware reset of controller
+ *
+ * @pdd: pardata data
+ *
+ * Reset controller if the &pardata->pin_reset gpio is set.
+ */
+void pardata_hw_reset(struct pardata_data *pdd)
+{
+	if (!pdd->pin_reset)
+		return;
+
+	gpiod_set_value_cansleep(pdd->pin_reset, 0);
+	usleep_range(20, 1000);
+	gpiod_set_value_cansleep(pdd->pin_reset, 1);
+	msleep(120);
+}
+EXPORT_SYMBOL_GPL(pardata_hw_reset);
+
+/**
+ * pardata_poweron_reset - poweron and reset
+ * @pdd: pardata data
+ *
+ * This function enables the regulator if used and does a hardware reset.
+ *
+ * Returns:
+ * Zero on success, or a negative error code.
+ */
+int pardata_poweron_reset(struct pardata_data *pdd)
+{
+	int ret;
+
+
+	if (pdd->regulator) {
+		ret = regulator_enable(pdd->regulator);
+		if (ret) {
+			DRM_DEV_ERROR(&pdd->pddev->dev,
+				      "Failed to enable regulator (%d)\n",
+				      ret);
+			return ret;
+		}
+	}
+
+	pardata_hw_reset(pdd);
+
+	return 0;
+}
+EXPORT_SYMBOL_GPL(pardata_poweron_reset);
+
+MODULE_LICENSE("GPL v2");
diff --git a/include/drm/tinydrm/pardata-dbi.h b/include/drm/tinydrm/pardata-dbi.h
new file mode 100644
index 000000000000..d72d9a1dff27
--- /dev/null
+++ b/include/drm/tinydrm/pardata-dbi.h
@@ -0,0 +1,257 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Parallel Data Display Bus Interface LCD controller support
+ */
+
+#include <linux/pardata.h>
+
+#include <drm/tinydrm/tinydrm.h>
+
+/**
+ * pardata_pins - the data pins
+ *
+ * The order is important due to use of writing to gpio arrays.
+ * The first four pins (PIN_DB0 to PIN_DB3) are not used for
+ * interfaces using only four data bits.
+ */
+enum pardata_pins {
+	PIN_DB0,
+	PIN_DB1,
+	PIN_DB2,
+	PIN_DB3,
+	PIN_DB4,
+	PIN_DB5,
+	PIN_DB6,
+	PIN_DB7,
+	PIN_NUM,	/* Number of pins */
+};
+
+struct pardata_data {
+	/**
+	 * @tinydrm - tinydrm base
+	 */
+	struct tinydrm_device tinydrm;
+
+	/**
+	 * dev - pardata device
+	 */
+	struct pardata_device *pddev;
+
+	/**
+	 * @busdata - data for the bus
+	 */
+	struct pardatabus_data *bus;
+
+	/**
+	 * @pin_reset: Optional GPIO reset pin
+	 */
+	struct gpio_desc *pin_reset;
+
+	/**
+	 * @pin_cs: Optional chip select pin
+	 */
+	struct gpio_desc *pin_cs;
+
+	/**
+	 * @tx_buf: Poitner to buffer to transer to display RAM
+	 */
+	u8 *tx_buf;
+
+	/**
+	 * @backlight - backlight device (optional)
+	 */
+	struct backlight_device *backlight;
+
+	/**
+	 * @regulator - power regulator (optional)
+	 */
+	struct regulator *regulator;
+
+	/**
+	 * @strobe_write - activate chip select to move data to controller
+	 *
+	 * This callback is used to set read/write and activate
+	 * pin_e and pin_cs - with respect to the timing required by
+	 * the controller.
+	 *
+	 * Use pardata_strobe_8080_write() for 8080 style interface or
+	 * pardata_strobe_6800_write() for 6800 style interface.
+	 * has individual pins for read and write.
+	 *
+	 * Parameters:
+	 *
+	 * pdd: pardata data
+	 */
+	void (*strobe_write)(struct pardata_data *pdd);
+
+	/**
+	 * write_reg - write value to register
+	 *
+	 * This callback is used to write the value to the register.
+	 *
+	 * Parameters:
+	 *
+	 * pdd: pardata data
+	 * reg: The register to write
+	 * value: The value to write to the register
+	 *
+	 * Returns:
+	 * 0 on success, negative value on error
+	 */
+	int (*write_reg)(struct pardata_data *pdd,
+			unsigned int reg, unsigned int value);
+
+	/**
+	 * write_buf - write content of buffer to controller
+	 *
+	 * This callback writes the data to the controller at offset
+	 * and forward. The write operation shall be as efficient as
+	 * possible as this will be called every time the content on
+	 * the display changes
+	 *
+	 * Parameters:
+	 *
+	 * pdd: pardata data
+	 * offset: The offset into the display memory of the controller where
+	 *	 the data shall be written.
+	 * data: Pointer to the data to write to display memory
+	 * len: Number of bytes to write to the display memory
+	 *
+	 * Returns:
+	 * 0 on success, negative value on error
+	 */
+	int (*write_buf)(struct pardata_data *pdd,
+			 u8 offset,
+			 u8 *data,
+			 size_t len);
+
+	/**
+	 * @enabled: Pipeline is enabled (internal)
+	 */
+	bool enabled;
+};
+
+static inline struct pardata_data *
+pardata_from_tinydrm(struct tinydrm_device *tdev)
+{
+	return container_of(tdev, struct pardata_data, tinydrm);
+}
+
+static inline void pardata_strobe_write(struct pardata_data *pdd)
+{
+	if (pdd && pdd->strobe_write)
+		pdd->strobe_write(pdd);
+	else
+		DRM_DEV_ERROR(&pdd->pddev->dev, "No strobe_write callback");
+}
+
+static inline int pardata_write_reg(struct pardata_data *pdd,
+				    unsigned int reg, unsigned int value)
+{
+	if (pdd && pdd->write_reg)
+		pdd->write_reg(pdd, reg, value);
+	else
+		DRM_DEV_ERROR(&pdd->pddev->dev, "No write_reg callback");
+
+	return 0;
+}
+
+static inline int pardata_write_buf(struct pardata_data *pdd,
+				    u8 offset, u8 *data, size_t len)
+{
+	if (pdd && pdd->write_buf)
+		pdd->write_buf(pdd, offset, data, len);
+	else
+		DRM_DEV_ERROR(&pdd->pddev->dev, "No write_buf callback");
+
+	return 0;
+}
+
+/**
+ * pardata_strobe_8080_write - using the 8080 interface create
+ *                             an enable strobe
+ *
+ * The interface requires that readwrite and chipselect are
+ * setup before the strobe of "e".
+ * There are timings constraints that is handled using udelay().
+ * Drivers can use this for the strobe_write callback.
+ *
+ * @pdd: pardata data
+ */
+void pardata_strobe_8080_write(struct pardata_data *pdd);
+
+/**
+ * pardata_strobe_6800_write - using the 6800 interface create
+ *                             an enable strobe
+ *
+ * The interface requires that read + write and chipselect are
+ * setup before the strobe of "e".
+ * There are timings constraints that is handled using udelay()
+ * Drivers can use this for the strobe_write callback.
+ *
+ * @pdd: pardata data
+ */
+void pardata_strobe_6800_write(struct pardata_data *pdd);
+
+/**
+ * pardata_poweron_reset - poweron and reset display
+ *
+ * @pdd: pardata data
+ *
+ * This function enables the regulator if used and does a hardware reset.
+ * Returns:
+ * Zero on success, or a negative error code.
+ */
+int pardata_poweron_reset(struct pardata_data *pdd);
+
+/**
+ * pardata_enable_flush - enable helper
+ *
+ * @pdd: parDATA DATA
+ * @crtc_state: crtc state
+ * @plane_state: plane state
+ *
+ * This function sets &pardata->enabled, flushes the whole framebuffer and
+ * enables the backlight. Drivers can use this in their
+ * &drm_simple_display_pipe_funcs->enable callback.
+ */
+void pardata_enable_flush(struct pardata_data *pdd,
+			  struct drm_crtc_state *crtc_state,
+			  struct drm_plane_state *plane_state);
+
+/**
+ * pardata_pipe_disable - pipe disable helper
+ *
+ * @pipe: Display pipe
+ *
+ * This function disables backlight if present, if not the display memory is
+ * blanked. The regulator is disabled if in use. Drivers can use this as their
+ * &drm_simple_display_pipe_funcs->disable callback.
+ */
+void pardata_pipe_disable(struct drm_simple_display_pipe *pipe);
+
+/**
+ * pardata_init - initialization
+ *
+ * @dev: Parent device
+ * @pdd: pardata data to initialize
+ * @pipe_funcs: Display pipe functions
+ * @driver: DRM driver
+ * @mode: Display mode
+ *
+ * This function initializes a &pardata data structure and it's underlying
+ * @tinydrm_device. It also sets up the display pipeline.
+ *
+ * Supported formats: Emulated XRGB8888.
+ *
+ * Objects created by this function will be automatically freed on driver
+ * detach (devres).
+ *
+ * Returns:
+ * Zero on success, negative error code on failure.
+ */
+int pardata_init(struct device *dev,
+		 struct pardata_data *pdd,
+		 const struct drm_simple_display_pipe_funcs *pipe_funcs,
+		 struct drm_driver *driver,
+		 const struct drm_display_mode *mode);
-- 
2.12.0

_______________________________________________
dri-devel mailing list
dri-devel@xxxxxxxxxxxxxxxxxxxxx
https://lists.freedesktop.org/mailman/listinfo/dri-devel




[Index of Archives]     [Linux DRI Users]     [Linux Intel Graphics]     [Linux USB Devel]     [Video for Linux]     [Linux Audio Users]     [Yosemite News]     [Linux Kernel]     [Linux SCSI]     [XFree86]     [Linux USB Devel]     [Video for Linux]     [Linux Audio Users]     [Linux Kernel]     [Linux SCSI]     [XFree86]
  Powered by Linux