[RFC 2/5] drm/tinydrm: Add lcd register abstraction

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

 



Add LCD register abstraction for MIPI DBI/DCS like controllers.
This hides LCD controller interface implementation details.
When built with debugfs, the register is available to userspace.

Signed-off-by: Noralf Trønnes <noralf@xxxxxxxxxxx>
---
 drivers/gpu/drm/tinydrm/Kconfig                 |   2 +
 drivers/gpu/drm/tinydrm/Makefile                |   1 +
 drivers/gpu/drm/tinydrm/lcdreg/Kconfig          |   3 +
 drivers/gpu/drm/tinydrm/lcdreg/Makefile         |   3 +
 drivers/gpu/drm/tinydrm/lcdreg/internal.h       |   8 +
 drivers/gpu/drm/tinydrm/lcdreg/lcdreg-core.c    | 190 ++++++++++++++++
 drivers/gpu/drm/tinydrm/lcdreg/lcdreg-debugfs.c | 281 ++++++++++++++++++++++++
 include/drm/tinydrm/lcdreg.h                    | 126 +++++++++++
 8 files changed, 614 insertions(+)
 create mode 100644 drivers/gpu/drm/tinydrm/lcdreg/Kconfig
 create mode 100644 drivers/gpu/drm/tinydrm/lcdreg/Makefile
 create mode 100644 drivers/gpu/drm/tinydrm/lcdreg/internal.h
 create mode 100644 drivers/gpu/drm/tinydrm/lcdreg/lcdreg-core.c
 create mode 100644 drivers/gpu/drm/tinydrm/lcdreg/lcdreg-debugfs.c
 create mode 100644 include/drm/tinydrm/lcdreg.h

diff --git a/drivers/gpu/drm/tinydrm/Kconfig b/drivers/gpu/drm/tinydrm/Kconfig
index f290045..671239f 100644
--- a/drivers/gpu/drm/tinydrm/Kconfig
+++ b/drivers/gpu/drm/tinydrm/Kconfig
@@ -9,3 +9,5 @@ menuconfig DRM_TINYDRM
 	help
 	  Choose this option if you have a tinydrm supported display.
 	  If M is selected the module will be called tinydrm.
+
+source "drivers/gpu/drm/tinydrm/lcdreg/Kconfig"
diff --git a/drivers/gpu/drm/tinydrm/Makefile b/drivers/gpu/drm/tinydrm/Makefile
index 7476ed1..f4a92d9 100644
--- a/drivers/gpu/drm/tinydrm/Makefile
+++ b/drivers/gpu/drm/tinydrm/Makefile
@@ -1 +1,2 @@
 obj-$(CONFIG_DRM_TINYDRM)		+= core/
+obj-$(CONFIG_LCDREG)			+= lcdreg/
diff --git a/drivers/gpu/drm/tinydrm/lcdreg/Kconfig b/drivers/gpu/drm/tinydrm/lcdreg/Kconfig
new file mode 100644
index 0000000..41383b1
--- /dev/null
+++ b/drivers/gpu/drm/tinydrm/lcdreg/Kconfig
@@ -0,0 +1,3 @@
+config LCDREG
+	tristate
+	depends on GPIOLIB || COMPILE_TEST
diff --git a/drivers/gpu/drm/tinydrm/lcdreg/Makefile b/drivers/gpu/drm/tinydrm/lcdreg/Makefile
new file mode 100644
index 0000000..c9ea774
--- /dev/null
+++ b/drivers/gpu/drm/tinydrm/lcdreg/Makefile
@@ -0,0 +1,3 @@
+obj-$(CONFIG_LCDREG)                   += lcdreg.o
+lcdreg-y                               += lcdreg-core.o
+lcdreg-$(CONFIG_DEBUG_FS)              += lcdreg-debugfs.o
diff --git a/drivers/gpu/drm/tinydrm/lcdreg/internal.h b/drivers/gpu/drm/tinydrm/lcdreg/internal.h
new file mode 100644
index 0000000..140fcfd
--- /dev/null
+++ b/drivers/gpu/drm/tinydrm/lcdreg/internal.h
@@ -0,0 +1,8 @@
+
+#ifdef CONFIG_DEBUG_FS
+void lcdreg_debugfs_init(struct lcdreg *reg);
+void lcdreg_debugfs_exit(struct lcdreg *reg);
+#else
+static inline void lcdreg_debugfs_init(struct lcdreg *reg) { }
+static inline void lcdreg_debugfs_exit(struct lcdreg *reg) { }
+#endif
diff --git a/drivers/gpu/drm/tinydrm/lcdreg/lcdreg-core.c b/drivers/gpu/drm/tinydrm/lcdreg/lcdreg-core.c
new file mode 100644
index 0000000..bda848c
--- /dev/null
+++ b/drivers/gpu/drm/tinydrm/lcdreg/lcdreg-core.c
@@ -0,0 +1,190 @@
+//#define DEBUG
+/*
+ * Copyright (C) 2016 Noralf Trønnes
+ *
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ */
+
+#include <drm/tinydrm/lcdreg.h>
+#include <linux/device.h>
+#include <linux/gpio/consumer.h>
+#include <linux/module.h>
+#include <linux/slab.h>
+
+#include "internal.h"
+
+/**
+ * Write to LCD register
+ *
+ * @reg: LCD register
+ * @regnr: Register number
+ * @transfer: Transfer to write
+ */
+int lcdreg_write(struct lcdreg *reg, unsigned regnr,
+		 struct lcdreg_transfer *transfer)
+{
+	if (WARN_ON_ONCE(!reg || !reg->write || !transfer))
+		return -EINVAL;
+
+	if (!transfer->width)
+		transfer->width = reg->def_width;
+
+	dev_dbg(reg->dev,
+		"lcdreg_write: regnr=0x%02x, index=%u, count=%u, width=%u\n",
+		regnr, transfer->index, transfer->count, transfer->width);
+	lcdreg_dbg_transfer_buf(transfer);
+
+	return reg->write(reg, regnr, transfer);
+}
+EXPORT_SYMBOL(lcdreg_write);
+
+/**
+ * Write 32-bit wide buffer to LCD register
+ * @reg: lcdreg
+ * @regnr: Register number
+ * @buf: Buffer to write
+ * @count: Number of words to write
+ */
+int lcdreg_write_buf32(struct lcdreg *reg, unsigned regnr, const u32 *buf,
+		       unsigned count)
+{
+	struct lcdreg_transfer tr = {
+		.index = 1,
+		.width = reg->def_width,
+		.count = count,
+	};
+	int i, ret;
+
+	if (!buf)
+		return -EINVAL;
+
+	tr.buf = kmalloc_array(count, sizeof(*buf), GFP_KERNEL);
+	if (!tr.buf)
+		return -ENOMEM;
+
+	if (reg->def_width <= 8)
+		for (i = 0; i < tr.count; i++)
+			((u8 *)tr.buf)[i] = buf[i];
+	else
+		for (i = 0; i < tr.count; i++)
+			((u16 *)tr.buf)[i] = buf[i];
+	ret = lcdreg_write(reg, regnr, &tr);
+	kfree(tr.buf);
+
+	return ret;
+}
+EXPORT_SYMBOL(lcdreg_write_buf32);
+
+/**
+ * Read from LCD register
+ *
+ * @reg: LCD register
+ * @regnr: Register number
+ * @transfer: Transfer to read into
+ */
+int lcdreg_read(struct lcdreg *reg, unsigned regnr,
+		struct lcdreg_transfer *transfer)
+{
+	int ret;
+
+	if (WARN_ON_ONCE(!reg || !transfer))
+		return -EINVAL;
+
+	if (!reg->read)
+		return -EOPNOTSUPP;
+
+	if (!transfer->width)
+		transfer->width = reg->def_width;
+
+	dev_dbg(reg->dev,
+		"lcdreg_read: regnr=0x%02x, index=%u, count=%u, width=%u\n",
+		regnr, transfer->index, transfer->count, transfer->width);
+
+	ret = reg->read(reg, regnr, transfer);
+
+	lcdreg_dbg_transfer_buf(transfer);
+
+	return ret;
+}
+EXPORT_SYMBOL(lcdreg_read);
+
+/**
+ * Read from LCD register into 32-bit wide buffer
+ * @reg: LCD register
+ * @regnr: Register number
+ * @buf: Buffer to read into
+ * @count: Number of words to read
+ */
+int lcdreg_readreg_buf32(struct lcdreg *reg, unsigned regnr, u32 *buf,
+			 unsigned count)
+{
+	struct lcdreg_transfer tr = {
+		.index = 1,
+		.count = count,
+	};
+	int i, ret;
+
+	if (!buf || !count)
+		return -EINVAL;
+
+	tr.buf = kmalloc_array(count, sizeof(*buf), GFP_KERNEL);
+	if (!tr.buf)
+		return -ENOMEM;
+
+	ret = lcdreg_read(reg, regnr, &tr);
+	if (ret) {
+		kfree(tr.buf);
+		return ret;
+	}
+
+	if (reg->def_width <= 8)
+		for (i = 0; i < count; i++)
+			buf[i] = ((u8 *)tr.buf)[i];
+	else
+		for (i = 0; i < count; i++)
+			buf[i] = ((u16 *)tr.buf)[i];
+	kfree(tr.buf);
+
+	return ret;
+}
+EXPORT_SYMBOL(lcdreg_readreg_buf32);
+
+static void devm_lcdreg_release(struct device *dev, void *res)
+{
+	struct lcdreg *reg = *(struct lcdreg **)res;
+
+	lcdreg_debugfs_exit(reg);
+	mutex_destroy(&reg->lock);
+}
+
+/**
+ * Device managed lcdreg initialization
+ *
+ * @dev: Device backing the LCD register
+ * @reg: LCD register
+ */
+struct lcdreg *devm_lcdreg_init(struct device *dev, struct lcdreg *reg)
+{
+	struct lcdreg **ptr;
+
+	if (!dev || !reg)
+		return ERR_PTR(-EINVAL);
+
+	ptr = devres_alloc(devm_lcdreg_release, sizeof(*ptr), GFP_KERNEL);
+	if (!ptr)
+		return ERR_PTR(-ENOMEM);
+
+	*ptr = reg;
+	devres_add(dev, ptr);
+	reg->dev = dev;
+	mutex_init(&reg->lock);
+	lcdreg_debugfs_init(reg);
+
+	return reg;
+}
+EXPORT_SYMBOL(devm_lcdreg_init);
+
+MODULE_LICENSE("GPL");
diff --git a/drivers/gpu/drm/tinydrm/lcdreg/lcdreg-debugfs.c b/drivers/gpu/drm/tinydrm/lcdreg/lcdreg-debugfs.c
new file mode 100644
index 0000000..9fcc13d
--- /dev/null
+++ b/drivers/gpu/drm/tinydrm/lcdreg/lcdreg-debugfs.c
@@ -0,0 +1,281 @@
+/*
+ * Copyright (C) 2016 Noralf Trønnes
+ *
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ */
+
+#include <drm/tinydrm/lcdreg.h>
+#include <linux/debugfs.h>
+#include <linux/module.h>
+#include <linux/slab.h>
+#include <linux/uaccess.h>
+
+#define READ_RESULT_SIZE 16
+
+static struct dentry *lcdreg_debugfs_root;
+
+static int lcdreg_userbuf_to_u32(const char __user *user_buf, size_t count,
+				 u32 *dest, size_t dest_size)
+{
+	char *buf, *start;
+	int ret = -EINVAL;
+	int i;
+
+	buf = kmalloc(count, GFP_KERNEL);
+	if (!buf)
+		return -ENOMEM;
+
+	if (copy_from_user(buf, user_buf, count)) {
+		kfree(buf);
+		return -EFAULT;
+	}
+
+	/* turn whitespace into end-of-string for number parsing */
+	for (i = 0; i < count; i++)
+		if (buf[i] == ' ' || buf[i] == '\n' || buf[i] == '\t')
+			buf[i] = '\0';
+
+	i = 0;
+	start = buf;
+	while (start < buf + count) {
+		/* skip "whitespace" */
+		if (*start == '\0') {
+			start++;
+			continue;
+		}
+
+		if (i == dest_size) {
+			ret = -EFBIG;
+			break;
+		}
+
+		ret = kstrtou32(start, 0, &dest[i++]);
+		if (ret)
+			break;
+
+		/* move past this number */
+		while (*start != '\0')
+			start++;
+	};
+
+	kfree(buf);
+
+	return ret ? : i;
+}
+
+static ssize_t lcdreg_debugfs_write_file(struct file *file,
+					 const char __user *user_buf,
+					 size_t count, loff_t *ppos)
+{
+	struct lcdreg *reg = file->private_data;
+	int ret;
+	u32 txbuf[128];
+
+	ret = lcdreg_userbuf_to_u32(user_buf, count, txbuf, ARRAY_SIZE(txbuf));
+	if (ret < 0)
+		return ret;
+
+	mutex_lock(&reg->lock);
+	ret = lcdreg_write_buf32(reg, txbuf[0], txbuf + 1, ret - 1);
+	mutex_unlock(&reg->lock);
+
+	return ret ? : count;
+}
+
+static const struct file_operations lcdreg_debugfs_write_fops = {
+	.open = simple_open,
+	.write = lcdreg_debugfs_write_file,
+	.llseek = default_llseek,
+};
+
+static ssize_t lcdreg_debugfs_read_wr(struct file *file,
+				      const char __user *user_buf,
+				      size_t count, loff_t *ppos)
+{
+	struct lcdreg *reg = file->private_data;
+	int ret;
+
+	ret = lcdreg_userbuf_to_u32(user_buf, count,
+				    &reg->debugfs_read_reg, 1);
+
+	return ret < 0 ? ret : count;
+}
+
+
+static int lcdreg_debugfs_readreg(struct lcdreg *reg)
+{
+	struct lcdreg_transfer tr = {
+		.index = 1,
+		.width = reg->debugfs_read_width,
+		.count = 1,
+	};
+	char *buf = reg->debugfs_read_result;
+	int ret;
+
+	tr.buf = kmalloc(lcdreg_bytes_per_word(tr.width), GFP_KERNEL);
+	if (!tr.buf)
+		return -ENOMEM;
+
+	mutex_lock(&reg->lock);
+	ret = lcdreg_read(reg, reg->debugfs_read_reg, &tr);
+	mutex_unlock(&reg->lock);
+	if (ret)
+		goto error_out;
+
+	switch (tr.width) {
+	case 8:
+		snprintf(buf, READ_RESULT_SIZE, "0x%02x\n", *(u8 *)tr.buf);
+		break;
+	case 16:
+		snprintf(buf, READ_RESULT_SIZE, "0x%04x\n", *(u16 *)tr.buf);
+		break;
+	case 24:
+	case 32:
+		snprintf(buf, READ_RESULT_SIZE, "0x%08x\n", *(u32 *)tr.buf);
+		break;
+	default:
+		ret = -EINVAL;
+	}
+
+error_out:
+	kfree(tr.buf);
+
+	return ret;
+}
+
+static ssize_t lcdreg_debugfs_read_rd(struct file *file,
+				      char __user *user_buf,
+				      size_t count, loff_t *ppos)
+{
+	struct lcdreg *reg = file->private_data;
+	int ret;
+
+	if (*ppos < 0 || !count)
+		return -EINVAL;
+
+	if (*reg->debugfs_read_result == '\0') {
+		ret = lcdreg_debugfs_readreg(reg);
+		if (ret)
+			return ret;
+	}
+
+	if (*ppos >= strlen(reg->debugfs_read_result)) {
+		*reg->debugfs_read_result = '\0';
+		return 0;
+	}
+
+	return simple_read_from_buffer(user_buf, count, ppos,
+				       reg->debugfs_read_result,
+				       strlen(reg->debugfs_read_result));
+}
+
+static const struct file_operations lcdreg_debugfs_read_fops = {
+	.open = simple_open,
+	.read = lcdreg_debugfs_read_rd,
+	.write = lcdreg_debugfs_read_wr,
+	.llseek = default_llseek,
+};
+
+static ssize_t lcdreg_debugfs_reset_wr(struct file *file,
+				       const char __user *user_buf,
+				       size_t count, loff_t *ppos)
+{
+	struct lcdreg *reg = file->private_data;
+
+	lcdreg_reset(reg);
+
+	return count;
+}
+
+static const struct file_operations lcdreg_debugfs_reset_fops = {
+	.open = simple_open,
+	.write = lcdreg_debugfs_reset_wr,
+	.llseek = default_llseek,
+};
+
+static int lcdreg_debugfs_readwidth_set(void *data, u64 val)
+{
+	struct lcdreg *reg = data;
+
+	reg->debugfs_read_width = val;
+
+	return 0;
+}
+
+static int lcdreg_debugfs_readwidth_get(void *data, u64 *val)
+{
+	struct lcdreg *reg = data;
+
+	/*
+	* def_width is not set when lcdreg_debugfs_init() is run, it's
+	* set later by the controller init code. Hence the need for this
+	* late assignment.
+	*/
+	if (!reg->debugfs_read_width)
+		reg->debugfs_read_width = reg->def_width;
+
+	*val = reg->debugfs_read_width;
+
+	return 0;
+}
+
+DEFINE_SIMPLE_ATTRIBUTE(lcdreg_debugfs_readwidth_fops,
+			lcdreg_debugfs_readwidth_get,
+			lcdreg_debugfs_readwidth_set, "%llu\n");
+
+void lcdreg_debugfs_init(struct lcdreg *reg)
+{
+	if (IS_ERR_OR_NULL(lcdreg_debugfs_root))
+		return;
+
+	reg->debugfs_read_result = devm_kzalloc(reg->dev, READ_RESULT_SIZE,
+						GFP_KERNEL);
+	if (!reg->debugfs_read_result)
+		return;
+
+	reg->debugfs = debugfs_create_dir(dev_name(reg->dev),
+					  lcdreg_debugfs_root);
+	if (!reg->debugfs) {
+		dev_warn(reg->dev, "Failed to create debugfs directory\n");
+		return;
+	}
+
+	debugfs_create_file("write", 0220, reg->debugfs, reg,
+			    &lcdreg_debugfs_write_fops);
+	if (reg->read) {
+		debugfs_create_file("read_width", 0660, reg->debugfs, reg,
+				    &lcdreg_debugfs_readwidth_fops);
+		debugfs_create_file("read", 0660, reg->debugfs, reg,
+				    &lcdreg_debugfs_read_fops);
+	}
+	if (reg->reset) {
+		debugfs_create_file("reset", 0220, reg->debugfs, reg,
+				    &lcdreg_debugfs_reset_fops);
+	}
+}
+
+void lcdreg_debugfs_exit(struct lcdreg *reg)
+{
+	debugfs_remove_recursive(reg->debugfs);
+}
+
+static int lcdreg_debugfs_module_init(void)
+{
+	lcdreg_debugfs_root = debugfs_create_dir("lcdreg", NULL);
+	if (!lcdreg_debugfs_root)
+		pr_warn("lcdreg: Failed to create debugfs root\n");
+
+	return 0;
+}
+module_init(lcdreg_debugfs_module_init);
+
+static void lcdreg_debugfs_module_exit(void)
+{
+	debugfs_remove_recursive(lcdreg_debugfs_root);
+}
+module_exit(lcdreg_debugfs_module_exit);
+
+MODULE_LICENSE("GPL");
diff --git a/include/drm/tinydrm/lcdreg.h b/include/drm/tinydrm/lcdreg.h
new file mode 100644
index 0000000..74e5e50
--- /dev/null
+++ b/include/drm/tinydrm/lcdreg.h
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2016 Noralf Trønnes
+ *
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ */
+
+#ifndef __LINUX_LCDREG_H
+#define __LINUX_LCDREG_H
+
+#include <linux/device.h>
+#include <linux/mutex.h>
+
+/**
+ * struct lcdreg_transfer - LCD register transfer
+ * @index: register index (address)
+ *         Known under the following names:
+ *         D/C (command=0, data=1)
+ *         RS (register selection: index=0, data=1)
+ *         D/I (data/index: index=0, data=1)
+ * @buf: data array to transfer
+ * @count: number of items in array
+ * @width: override default regwidth
+ */
+struct lcdreg_transfer {
+	unsigned index;
+	void *buf;
+	unsigned count;
+	unsigned width;
+};
+
+/**
+ * struct lcdreg - interface to LCD register
+ * @dev: device interface
+ * @lock: mutex for register access locking
+ * @def_width: default register width
+ * @bits_per_word_mask: Bitmask of bits per word supported by the hardware.
+ *                      The driver can emulate more word widths.
+ * @readable: register is readable
+ * @little_endian: register has little endian byte order
+ * @write: write to register
+ * @read: read from register (optional)
+ * @reset: reset controller (optional)
+ */
+struct lcdreg {
+	struct device *dev;
+	struct mutex lock;
+	unsigned def_width;
+	bool readable;
+	bool little_endian;
+	u32 bits_per_word_mask;
+#define LCDREG_BPW_MASK(bits) BIT((bits) - 1)
+
+	int (*write)(struct lcdreg *reg, unsigned regnr,
+		     struct lcdreg_transfer *transfer);
+	int (*read)(struct lcdreg *reg, unsigned regnr,
+		    struct lcdreg_transfer *transfer);
+	void (*reset)(struct lcdreg *reg);
+
+#ifdef CONFIG_DEBUG_FS
+	struct dentry *debugfs;
+	u32 debugfs_read_width;
+	u32 debugfs_read_reg;
+	char *debugfs_read_result;
+#endif
+};
+
+struct lcdreg *devm_lcdreg_init(struct device *dev, struct lcdreg *reg);
+int lcdreg_write(struct lcdreg *reg, unsigned regnr,
+		 struct lcdreg_transfer *transfer);
+int lcdreg_write_buf32(struct lcdreg *reg, unsigned regnr, const u32 *data,
+		       unsigned count);
+
+#define lcdreg_writereg(lcdreg, regnr, seq...) \
+({\
+	u32 d[] = { seq };\
+	lcdreg_write_buf32(lcdreg, regnr, d, ARRAY_SIZE(d));\
+})
+
+int lcdreg_read(struct lcdreg *reg, unsigned regnr,
+		struct lcdreg_transfer *transfer);
+int lcdreg_readreg_buf32(struct lcdreg *reg, unsigned regnr, u32 *buf,
+			 unsigned count);
+
+static inline void lcdreg_reset(struct lcdreg *reg)
+{
+	if (reg->reset)
+		reg->reset(reg);
+}
+
+static inline bool lcdreg_is_readable(struct lcdreg *reg)
+{
+	return reg->readable;
+}
+
+static inline unsigned lcdreg_bytes_per_word(unsigned bits_per_word)
+{
+	if (bits_per_word <= 8)
+		return 1;
+	else if (bits_per_word <= 16)
+		return 2;
+	else /* bits_per_word <= 32 */
+		return 4;
+}
+
+static inline bool lcdreg_bpw_supported(struct lcdreg *reg, unsigned bpw)
+{
+	return LCDREG_BPW_MASK(bpw) & reg->bits_per_word_mask;
+}
+
+#if defined(DEBUG) || defined(CONFIG_DYNAMIC_DEBUG)
+static inline void lcdreg_dbg_transfer_buf(struct lcdreg_transfer *tr)
+{
+	int groupsize = lcdreg_bytes_per_word(tr->width);
+	size_t len = min_t(size_t, 32, tr->count * groupsize);
+
+	print_hex_dump_debug("    buf=", DUMP_PREFIX_NONE, 32, groupsize,
+			     tr->buf, len, false);
+}
+#else
+static inline void lcdreg_dbg_transfer_buf(struct lcdreg_transfer *tr) { }
+#endif /* DEBUG || CONFIG_DYNAMIC_DEBUG */
+
+#endif /* __LINUX_LCDREG_H */
-- 
2.2.2

_______________________________________________
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