[RFC] misc: Add Allwinner Q8 tablet hardware manager

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

 




Allwinnner A13 / A23 / A33 based Q8 tablets are popular cheap 7" tablets
of which a new batch is produced every few weeks. Each batch uses a
different mix of touchscreen, accelerometer and wifi peripherals.

Given that each batch is different creating a devicetree for each variant
is not desirable. This commit adds a Q8 tablet hardware manager which
auto-detects the touchscreen and accelerometer so that a single generic
dts can be used for these tablets.

The wifi is connected to a discoverable bus (sdio or usb) and will be
autodetected by the mmc resp. usb subsystems.

Signed-off-by: Hans de Goede <hdegoede@xxxxxxxxxx>
---
 .../misc/allwinner,sunxi-q8-hardwaremgr.txt        |  52 +++
 drivers/misc/Kconfig                               |  12 +
 drivers/misc/Makefile                              |   1 +
 drivers/misc/q8-hardwaremgr.c                      | 512 +++++++++++++++++++++
 4 files changed, 577 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/misc/allwinner,sunxi-q8-hardwaremgr.txt
 create mode 100644 drivers/misc/q8-hardwaremgr.c

diff --git a/Documentation/devicetree/bindings/misc/allwinner,sunxi-q8-hardwaremgr.txt b/Documentation/devicetree/bindings/misc/allwinner,sunxi-q8-hardwaremgr.txt
new file mode 100644
index 0000000..f428bf5
--- /dev/null
+++ b/Documentation/devicetree/bindings/misc/allwinner,sunxi-q8-hardwaremgr.txt
@@ -0,0 +1,52 @@
+Q8 tablet hardware manager
+--------------------------
+
+Allwinnner A13 / A23 / A33 based Q8 tablets are popular cheap 7" tablets of
+which a new batch is produced every few weeks. Each batch uses a different
+mix of touchscreen, accelerometer and wifi peripherals.
+
+Given that each batch is different creating a devicetree for each variant is
+not desirable. The Q8 tablet hardware manager bindings are bindings for an os
+module which auto-detects the touchscreen so that a single
+generic dts can be used for these tablets.
+
+The wifi is connected to a discoverable bus and will be autodetected by the os.
+
+Required properties:
+ - compatible         : "allwinner,sunxi-q8-hardwaremgr"
+ - touchscreen        : phandle of a template touchscreen node, this must be a
+		        child node of the touchscreen i2c bus
+
+Optional properties:
+ - touchscreen-supply : regulator phandle for the touchscreen vdd supply
+
+touschreen node required properties:
+ - interrupt-parent   : phandle pointing to the interrupt controller
+			serving the touchscreen interrupt
+ - interrupts         : interrupt specification for the touchscreen interrupt
+ - power-gpios        : Specification for the pin connected to the touchscreen's
+			enable / wake pin. This needs to be driven high to
+			enable the touchscreen controller
+
+Example:
+
+/ {
+	hwmgr {
+		compatible = "allwinner,sunxi-q8-hardwaremgr";
+		touchscreen = <&touchscreen>;
+		touchscreen-supply = <&reg_ldo_io1>;
+	};
+};
+
+&i2c0 {
+	touchscreen: touchscreen@0 {
+		interrupt-parent = <&pio>;
+		interrupts = <1 5 IRQ_TYPE_EDGE_FALLING>; /* PB5 */
+		power-gpios = <&pio 7 1 GPIO_ACTIVE_HIGH>; /* PH1 */
+		/*
+		 * Enabled by sunxi-q8-hardwaremgr if it detects a
+		 * known model touchscreen.
+		 */
+		status = "disabled";
+	};
+};
diff --git a/drivers/misc/Kconfig b/drivers/misc/Kconfig
index a216b46..c3e7772 100644
--- a/drivers/misc/Kconfig
+++ b/drivers/misc/Kconfig
@@ -804,6 +804,18 @@ config PANEL_BOOT_MESSAGE
 	  An empty message will only clear the display at driver init time. Any other
 	  printf()-formatted message is valid with newline and escape codes.
 
+config Q8_HARDWAREMGR
+	tristate "Allwinner Q8 tablet hardware manager"
+	depends on GPIOLIB || COMPILE_TEST
+	depends on I2C
+	depends on OF
+	default	n
+	help
+	  This option enables support for autodetecting the touchscreen
+	  on Allwinner Q8 tablets.
+
+	  If unsure, say N.
+
 source "drivers/misc/c2port/Kconfig"
 source "drivers/misc/eeprom/Kconfig"
 source "drivers/misc/cb710/Kconfig"
diff --git a/drivers/misc/Makefile b/drivers/misc/Makefile
index 7410c6d..cac76b7 100644
--- a/drivers/misc/Makefile
+++ b/drivers/misc/Makefile
@@ -57,6 +57,7 @@ obj-$(CONFIG_ECHO)		+= echo/
 obj-$(CONFIG_VEXPRESS_SYSCFG)	+= vexpress-syscfg.o
 obj-$(CONFIG_CXL_BASE)		+= cxl/
 obj-$(CONFIG_PANEL)             += panel.o
+obj-$(CONFIG_Q8_HARDWAREMGR)    += q8-hardwaremgr.o
 
 lkdtm-$(CONFIG_LKDTM)		+= lkdtm_core.o
 lkdtm-$(CONFIG_LKDTM)		+= lkdtm_bugs.o
diff --git a/drivers/misc/q8-hardwaremgr.c b/drivers/misc/q8-hardwaremgr.c
new file mode 100644
index 0000000..e75625e
--- /dev/null
+++ b/drivers/misc/q8-hardwaremgr.c
@@ -0,0 +1,512 @@
+/*
+ * Allwinner q8 formfactor tablet hardware manager
+ *
+ * Copyright (C) 2016 Hans de Goede <hdegoede@xxxxxxxxxx>
+ *
+ * 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.
+ *
+ * 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.
+ */
+
+#include <asm/unaligned.h>
+#include <linux/delay.h>
+#include <linux/err.h>
+#include <linux/gpio/consumer.h>
+#include <linux/i2c.h>
+#include <linux/module.h>
+#include <linux/of_platform.h>
+#include <linux/platform_device.h>
+#include <linux/regulator/consumer.h>
+#include <linux/slab.h>
+
+/*
+ * We can detect which touchscreen controller is used automatically,
+ * but some controllers can be wired up differently depending on the
+ * q8 PCB variant used, so they need different firmware files / settings.
+ *
+ * We allow the user to specify a firmware_variant to select a config
+ * from a list of known configs. We also allow overriding each setting
+ * individually.
+ */
+
+static int touchscreen_variant = -1;
+module_param(touchscreen_variant, int, 0444);
+MODULE_PARM_DESC(touchscreen_variant, "Touchscreen variant 0-x, -1 for auto");
+
+static int touchscreen_width = -1;
+module_param(touchscreen_width, int, 0444);
+MODULE_PARM_DESC(touchscreen_width, "Touchscreen width, -1 for auto");
+
+static int touchscreen_height = -1;
+module_param(touchscreen_height, int, 0444);
+MODULE_PARM_DESC(touchscreen_height, "Touchscreen height, -1 for auto");
+
+static int touchscreen_invert_x = -1;
+module_param(touchscreen_invert_x, int, 0444);
+MODULE_PARM_DESC(touchscreen_invert_x, "Touchscreen invert x, -1 for auto");
+
+static int touchscreen_invert_y = -1;
+module_param(touchscreen_invert_y, int, 0444);
+MODULE_PARM_DESC(touchscreen_invert_y, "Touchscreen invert y, -1 for auto");
+
+static int touchscreen_swap_x_y = -1;
+module_param(touchscreen_swap_x_y, int, 0444);
+MODULE_PARM_DESC(touchscreen_swap_x_y, "Touchscreen swap x y, -1 for auto");
+
+static char *touchscreen_fw_name;
+module_param(touchscreen_fw_name, charp, 0444);
+MODULE_PARM_DESC(touchscreen_fw_name, "Touchscreen firmware filename");
+
+#define TOUCHSCREEN_POWER_ON_DELAY	20
+#define SILEAD_REG_ID			0xFC
+#define EKTF2127_RESPONSE		0x52
+#define EKTF2127_REQUEST		0x53
+#define EKTF2127_WIDTH			0x63
+
+enum touchscreen_model {
+	touchscreen_unknown,
+	gsl1680_a082,
+	gsl1680_b482,
+	ektf2127,
+	zet6251,
+};
+
+struct q8_hardwaremgr_data {
+	struct device *dev;
+	bool touchscreen_needs_regulator;
+	enum touchscreen_model touchscreen_model;
+	int touchscreen_addr;
+	int touchscreen_variant;
+	int touchscreen_width;
+	int touchscreen_height;
+	int touchscreen_invert_x;
+	int touchscreen_invert_y;
+	int touchscreen_swap_x_y;
+	const char *touchscreen_compatible;
+	const char *touchscreen_fw_name;
+};
+
+typedef int (*probe_func)(struct q8_hardwaremgr_data *data,
+			  struct i2c_adapter *adap);
+
+#if 0
+	ret = i2c_smbus_xfer(adap, 0x40, 0, I2C_SMBUS_WRITE, 0,
+			     I2C_SMBUS_QUICK, NULL);
+	if (ret < 0)
+		return -ENODEV;
+
+#endif
+
+static int q8_hardwaremgr_probe_touchscreen(struct q8_hardwaremgr_data *data,
+					    struct i2c_adapter *adap)
+{
+	struct i2c_client *client;
+	unsigned char buff[24];
+	__le32 chip_id;
+	int ret;
+
+	msleep(TOUCHSCREEN_POWER_ON_DELAY);
+
+	/* Check for silead touchsceen at addr 0x40 */
+	client = i2c_new_dummy(adap, 0x40);
+	if (!client)
+		return -ENOMEM;
+
+	ret = i2c_smbus_read_i2c_block_data(client, SILEAD_REG_ID,
+					    sizeof(chip_id), (u8 *)&chip_id);
+	if (ret == sizeof(chip_id)) {
+		switch (le32_to_cpu(chip_id)) {
+		case 0xa0820000:
+			data->touchscreen_addr = 0x40;
+			data->touchscreen_compatible = "silead,gsl1680";
+			data->touchscreen_model = gsl1680_a082;
+			dev_info(data->dev, "Found Silead touchscreen ID: 0xa0820000\n");
+			break;
+		case 0xb4820000:
+			data->touchscreen_addr = 0x40;
+			data->touchscreen_compatible = "silead,gsl1680";
+			data->touchscreen_model = gsl1680_b482;
+			dev_info(data->dev, "Found Silead touchscreen ID: 0xb4820000\n");
+			break;
+		default:
+			dev_warn(data->dev, "Found Silead touchscreen with unknown ID: 0x%08x\n",
+				 le32_to_cpu(chip_id));
+		}
+		ret = 0;
+	}
+	i2c_unregister_device(client);
+	if (ret == 0 || ret == -ETIMEDOUT /* Bus stuck bail immediately */)
+		return ret;
+
+	/* Check for Elan eKTF2127 touchsceen at addr 0x15 */
+	client = i2c_new_dummy(adap, 0x15);
+	if (!client)
+		return -ENOMEM;
+
+	do {
+		/* Read hello, ignore data, depends on initial power state */
+		ret = i2c_master_recv(client, buff, 4);
+		if (ret != 4)
+			break;
+
+		/* Request width */
+		buff[0] = EKTF2127_REQUEST;
+		buff[1] = EKTF2127_WIDTH;
+		buff[2] = 0x00;
+		buff[3] = 0x00;
+		ret = i2c_master_send(client, buff, 4);
+		if (ret != 4)
+			break;
+
+		msleep(20);
+
+		/* Read response */
+		ret = i2c_master_recv(client, buff, 4);
+		if (ret != 4)
+			break;
+			
+		if (buff[0] == EKTF2127_RESPONSE && buff[1] == EKTF2127_WIDTH) {
+			data->touchscreen_addr = 0x15;
+			data->touchscreen_compatible = "elan,ektf2127";
+			data->touchscreen_model = ektf2127;
+			dev_info(data->dev, "Found Elan eKTF2127 touchscreen\n");
+			ret = 0;
+		}
+	} while (0);
+	i2c_unregister_device(client);
+	if (ret == 0 || ret == -ETIMEDOUT /* Bus stuck bail immediately */)
+		return ret;
+
+	/* Check for Zeitec zet6251 touchsceen at addr 0x76 */
+	client = i2c_new_dummy(adap, 0x76);
+	if (!client)
+		return -ENOMEM;
+
+	/*
+	 * We only do a simple read finger data packet test, because some
+	 * versions require firmware to be loaded. If not firmware is loaded
+	 * the buffer will be filed with 0xff, so we ignore the contents.
+	 */
+	ret = i2c_master_recv(client, buff, 24);
+	if (ret == 24) {
+		data->touchscreen_addr = 0x76;
+		data->touchscreen_compatible = "zeitec,zet6251";
+		data->touchscreen_model = zet6251;
+		dev_info(data->dev, "Found Zeitec zet6251 touchscreen\n");
+		ret = 0;
+	}
+	i2c_unregister_device(client);
+	if (ret == 0 || ret == -ETIMEDOUT /* Bus stuck bail immediately */)
+		return ret;
+
+	return -ENODEV;
+}
+
+static int q8_hardwaremgr_do_probe(struct q8_hardwaremgr_data *data,
+				   const char *prefix, probe_func func)
+{
+	struct device *dev = data->dev;
+	struct device_node *np;
+	struct i2c_adapter *adap;
+	struct regulator *reg;
+	struct gpio_desc *gpio;
+	int ret = 0;
+
+	np = of_parse_phandle(dev->of_node, prefix, 0);
+	if (!np) {
+		dev_err(dev, "Error %s not set\n", prefix);
+		return -EINVAL;
+	}
+
+	adap = of_get_i2c_adapter_by_node(np->parent);
+	if (!adap) {
+		ret = -EPROBE_DEFER;
+		goto put_node;
+	}
+
+	reg = regulator_get_optional(dev, prefix);
+	if (IS_ERR(reg)) {
+		ret = PTR_ERR(reg);
+		if (ret == -EPROBE_DEFER)
+			goto put_adapter;
+		reg = NULL;
+	}
+
+	gpio = fwnode_get_named_gpiod(&np->fwnode, "power-gpios");
+	if (IS_ERR(gpio)) {
+		ret = PTR_ERR(gpio);
+		if (ret == -EPROBE_DEFER)
+			goto put_reg;
+		gpio = NULL;
+	}
+
+	/* First try with only the power gpio driven high */
+	if (gpio) {
+		ret = gpiod_direction_output(gpio, 1);
+		if (ret)
+			goto put_gpio;
+	}
+
+	dev_info(dev, "Looking for %s without a regulator\n", prefix);
+	ret = func(data, adap);
+	if (ret != 0 && reg) {
+		/* Second try, also enable the regulator */
+		ret = regulator_enable(reg);
+		if (ret)
+			goto restore_gpio;
+
+		dev_info(dev, "Looking for %s with a regulator\n", prefix);
+		ret = func(data, adap);
+		if (ret == 0)
+			data->touchscreen_needs_regulator = true; 
+
+		regulator_disable(reg);
+	}
+	ret = 0; /* Not finding a device is not an error */
+
+restore_gpio:
+	if (gpio)
+		gpiod_direction_output(gpio, 0);
+put_gpio:
+	if (gpio)
+		gpiod_put(gpio);
+put_reg:
+	if (reg)
+		regulator_put(reg);
+put_adapter:
+	i2c_put_adapter(adap);
+
+put_node:
+	of_node_put(np);
+
+	return ret;
+}
+
+static void q8_hardwaremgr_apply_gsl1680_a082_variant(
+	struct q8_hardwaremgr_data *data)
+{
+	if (touchscreen_variant != -1) {
+		data->touchscreen_variant = touchscreen_variant;
+	} else {
+		if (of_machine_is_compatible("allwinner,sun8i-a33"))
+			data->touchscreen_variant = 1;
+		else
+			data->touchscreen_variant = 0;
+	}
+
+	switch (data->touchscreen_variant) {
+	default:
+		dev_warn(data->dev, "Error unknown touchscreen_variant %d using 0\n",
+			 touchscreen_variant);
+		/* Fall through */
+	case 0:
+		data->touchscreen_width = 1024;
+		data->touchscreen_height = 600;
+		data->touchscreen_fw_name = "gsl1680-a082-q8-700.fw";
+		break;
+	case 1:
+		data->touchscreen_width = 480;
+		data->touchscreen_height = 800;
+		data->touchscreen_swap_x_y = 1;
+		data->touchscreen_fw_name = "gsl1680-a082-q8-a70.fw";
+		break;
+	}
+}
+
+static void q8_hardwaremgr_apply_gsl1680_b482_variant(
+	struct q8_hardwaremgr_data *data)
+{
+	if (touchscreen_variant != -1)
+		data->touchscreen_variant = touchscreen_variant;
+
+	switch (data->touchscreen_variant) {
+	default:
+		dev_warn(data->dev, "Error unknown touchscreen_variant %d using 0\n",
+			 touchscreen_variant);
+		/* Fall through */
+	case 0:
+		data->touchscreen_width = 960;
+		data->touchscreen_height = 640;
+		data->touchscreen_fw_name = "gsl1680-b482-q8-d702.fw";
+		break;
+	case 1:
+		data->touchscreen_width = 960;
+		data->touchscreen_height = 640;
+		data->touchscreen_fw_name = "gsl1680-b482-q8-a70.fw";
+		break;
+	}
+}
+
+static void q8_hardwaremgr_issue_gsl1680_warning(
+	struct q8_hardwaremgr_data *data)
+{
+	dev_warn(data->dev, "gsl1680 touchscreen may require kernel cmdline parameters to function properly\n");
+	dev_warn(data->dev, "Try q8_hardwaremgr.touchscreen_invert_x=1 if x coordinates are inverted\n");
+	dev_warn(data->dev, "Try q8_hardwaremgr.touchscreen_variant=%d if coordinates are all over the place\n",
+		 !data->touchscreen_variant);
+
+#define	show(x) \
+	dev_info(data->dev, #x " %d (%s)\n", data->x, \
+		 (x == -1) ? "auto" : "user supplied")
+
+	show(touchscreen_variant);
+	show(touchscreen_width);
+	show(touchscreen_height);
+	show(touchscreen_invert_x);
+	show(touchscreen_invert_y);
+	show(touchscreen_swap_x_y);
+	dev_info(data->dev, "touchscreen_fw_name %s (%s)\n",
+		 data->touchscreen_fw_name,
+		 (touchscreen_fw_name == NULL) ? "auto" : "user supplied");
+#undef show
+}
+
+static void q8_hardwaremgr_apply_touchscreen(struct q8_hardwaremgr_data *data)
+{
+	struct device *dev = data->dev;
+	struct of_changeset cset;
+	struct device_node *np;
+
+	switch (data->touchscreen_model) {
+	case touchscreen_unknown:
+		return;
+	case gsl1680_a082:
+		q8_hardwaremgr_apply_gsl1680_a082_variant(data);
+		break;
+	case gsl1680_b482:
+		q8_hardwaremgr_apply_gsl1680_b482_variant(data);
+		break;
+	case ektf2127:
+	case zet6251:
+		/* These have only 1 variant */
+		break;
+	}
+
+	if (touchscreen_width != -1)
+		data->touchscreen_width = touchscreen_width;
+
+	if (touchscreen_height != -1)
+		data->touchscreen_height = touchscreen_height;
+
+	if (touchscreen_invert_x != -1)
+		data->touchscreen_invert_x = touchscreen_invert_x;
+
+	if (touchscreen_invert_y != -1)
+		data->touchscreen_invert_y = touchscreen_invert_y;
+
+	if (touchscreen_swap_x_y != -1)
+		data->touchscreen_swap_x_y = touchscreen_swap_x_y;
+
+	if (touchscreen_fw_name)
+		data->touchscreen_fw_name = touchscreen_fw_name;
+
+	if (data->touchscreen_model == gsl1680_a082 ||
+	    data->touchscreen_model == gsl1680_b482)
+		q8_hardwaremgr_issue_gsl1680_warning(data);
+
+	np = of_parse_phandle(data->dev->of_node, "touchscreen", 0);
+	/* Never happens already checked in q8_hardwaremgr_do_probe() */
+	if (WARN_ON(!np))
+		return;
+
+	of_changeset_init(&cset);
+	of_changeset_add_property_u32(&cset, np, "reg", data->touchscreen_addr);
+	of_changeset_add_property_string(&cset, np, "compatible",
+					 data->touchscreen_compatible);
+
+	if (data->touchscreen_width)
+		of_changeset_add_property_u32(&cset, np, "touchscreen-size-x",
+					      data->touchscreen_width);
+	if (data->touchscreen_height)
+		of_changeset_add_property_u32(&cset, np, "touchscreen-size-y",
+					      data->touchscreen_height);
+	if (data->touchscreen_invert_x)
+		of_changeset_add_property_bool(&cset, np,
+					       "touchscreen-inverted-x");
+	if (data->touchscreen_invert_y)
+		of_changeset_add_property_bool(&cset, np,
+					       "touchscreen-inverted-y");
+	if (data->touchscreen_swap_x_y)
+		of_changeset_add_property_bool(&cset, np,
+					       "touchscreen-swapped-x-y");
+	if (data->touchscreen_fw_name)
+		of_changeset_add_property_string(&cset, np, "firmware-name",
+						 data->touchscreen_fw_name);
+	if (data->touchscreen_needs_regulator) {
+		struct property *p;
+
+		p = of_find_property(dev->of_node, "touchscreen-supply", NULL);
+		/* Never happens already checked in q8_hardwaremgr_do_probe() */
+		if (WARN_ON(!p))
+			return;
+
+		of_changeset_add_property_copy(&cset, np, "vddio-supply",
+					       p->value, p->length);
+	}
+
+	of_changeset_update_property_string(&cset, np, "status", "okay");
+	of_changeset_apply(&cset);
+
+	of_node_put(np);
+}
+
+static int q8_hardwaremgr_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct q8_hardwaremgr_data *data;
+	int ret = 0;
+
+	data = kzalloc(sizeof(*data), GFP_KERNEL);
+	if (!data)
+		return -ENOMEM;
+
+	data->dev = &pdev->dev;
+
+	ret = q8_hardwaremgr_do_probe(data, "touchscreen",
+				      q8_hardwaremgr_probe_touchscreen);
+	if (ret)
+		goto error;
+
+	/*
+	 * Our pinctrl may conflict with the pinctrl of the detected devices
+	 * we're adding, so remove it before adding detected devices.
+	 */
+	if (dev->pins) {
+		devm_pinctrl_put(dev->pins->p);
+		devm_kfree(dev, dev->pins);
+		dev->pins = NULL;
+	}
+
+	q8_hardwaremgr_apply_touchscreen(data);
+
+error:
+	kfree(data);
+
+	return ret;
+}
+
+static const struct of_device_id q8_hardwaremgr_of_match[] = {
+	{ .compatible = "allwinner,sunxi-q8-hardwaremgr", },
+	{ /* sentinel */ }
+};
+MODULE_DEVICE_TABLE(of, q8_hardwaremgr_of_match);
+
+static struct platform_driver q8_hardwaremgr_driver = {
+	.driver = {
+		.name	= "q8-hardwaremgr",
+		.of_match_table = of_match_ptr(q8_hardwaremgr_of_match),
+	},
+	.probe	= q8_hardwaremgr_probe,
+};
+
+module_platform_driver(q8_hardwaremgr_driver);
+
+MODULE_DESCRIPTION("Allwinner q8 formfactor tablet hardware manager");
+MODULE_AUTHOR("Hans de Goede <hdegoede@xxxxxxxxxx>");
+MODULE_LICENSE("GPL");
-- 
2.9.3

--
To unsubscribe from this list: send the line "unsubscribe devicetree" in
the body of a message to majordomo@xxxxxxxxxxxxxxx
More majordomo info at  http://vger.kernel.org/majordomo-info.html



[Index of Archives]     [Device Tree Compilter]     [Device Tree Spec]     [Linux Driver Backports]     [Video for Linux]     [Linux USB Devel]     [Linux PCI Devel]     [Linux Audio Users]     [Linux Kernel]     [Linux SCSI]     [XFree86]     [Yosemite Backpacking]
  Powered by Linux