Re: [PATCH v4 3/3] hwmon: Driver for Nuvoton NCT7363Y

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

 



Hi Zev,

On Sat, Mar 2, 2024 at 4:19 PM Zev Weiss <zev@xxxxxxxxxxxxxxxxx> wrote:
>
> On Mon, Feb 26, 2024 at 04:56:06PM PST, baneric926@xxxxxxxxx wrote:
> >From: Ban Feng <kcfeng0@xxxxxxxxxxx>
> >
> >NCT7363Y is an I2C based hardware monitoring chip from Nuvoton.
> >
> >Signed-off-by: Ban Feng <kcfeng0@xxxxxxxxxxx>
> >---
> > Documentation/hwmon/index.rst   |   1 +
> > Documentation/hwmon/nct7363.rst |  33 +++
> > MAINTAINERS                     |   2 +
> > drivers/hwmon/Kconfig           |  11 +
> > drivers/hwmon/Makefile          |   1 +
> > drivers/hwmon/nct7363.c         | 412 ++++++++++++++++++++++++++++++++
> > 6 files changed, 460 insertions(+)
> > create mode 100644 Documentation/hwmon/nct7363.rst
> > create mode 100644 drivers/hwmon/nct7363.c
> >
> >diff --git a/Documentation/hwmon/index.rst b/Documentation/hwmon/index.rst
> >index c7ed1f73ac06..302f954b45be 100644
> >--- a/Documentation/hwmon/index.rst
> >+++ b/Documentation/hwmon/index.rst
> >@@ -165,6 +165,7 @@ Hardware Monitoring Kernel Drivers
> >    mp5990
> >    nct6683
> >    nct6775
> >+   nct7363
> >    nct7802
> >    nct7904
> >    npcm750-pwm-fan
> >diff --git a/Documentation/hwmon/nct7363.rst b/Documentation/hwmon/nct7363.rst
> >new file mode 100644
> >index 000000000000..89699c95aa4b
> >--- /dev/null
> >+++ b/Documentation/hwmon/nct7363.rst
> >@@ -0,0 +1,33 @@
> >+.. SPDX-License-Identifier: GPL-2.0
> >+
> >+Kernel driver nct7363
> >+=====================
> >+
> >+Supported chip:
> >+
> >+  * Nuvoton NCT7363Y
> >+
> >+    Prefix: nct7363
> >+
> >+    Addresses: I2C 0x20, 0x21, 0x22, 0x23
> >+
> >+Author: Ban Feng <kcfeng0@xxxxxxxxxxx>
> >+
> >+
> >+Description
> >+-----------
> >+
> >+The NCT7363Y is a Fan controller which provides up to 16 independent
> >+FAN input monitors, and up to 16 independent PWM output with SMBus interface.
> >+
> >+
> >+Sysfs entries
> >+-------------
> >+
> >+Currently, the driver supports the following features:
> >+
> >+======================= =======================================================
> >+fanX_input            provide current fan rotation value in RPM
> >+
> >+pwmX                  get or set PWM fan control value.
> >+======================= =======================================================
> >diff --git a/MAINTAINERS b/MAINTAINERS
> >index 7b1efefed7c4..7ca66b713e30 100644
> >--- a/MAINTAINERS
> >+++ b/MAINTAINERS
> >@@ -15089,6 +15089,8 @@ M:     Ban Feng <kcfeng0@xxxxxxxxxxx>
> > L:    linux-hwmon@xxxxxxxxxxxxxxx
> > S:    Maintained
> > F:    Documentation/devicetree/bindings/hwmon/nuvoton,nct7363.yaml
> >+F:    Documentation/hwmon/nct7363.rst
> >+F:    drivers/hwmon/nct7363.c
> >
> > NETDEVSIM
> > M:    Jakub Kicinski <kuba@xxxxxxxxxx>
> >diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig
> >index a608264da87d..a297b5409b04 100644
> >--- a/drivers/hwmon/Kconfig
> >+++ b/drivers/hwmon/Kconfig
> >@@ -1616,6 +1616,17 @@ config SENSORS_NCT6775_I2C
> >         This driver can also be built as a module. If so, the module
> >         will be called nct6775-i2c.
> >
> >+config SENSORS_NCT7363
> >+      tristate "Nuvoton NCT7363Y"
> >+      depends on I2C
> >+      select REGMAP_I2C
> >+      help
> >+        If you say yes here you get support for the Nuvoton NCT7363Y,
> >+        hardware monitoring chip.
> >+
> >+        This driver can also be built as a module. If so, the module
> >+        will be called nct7363.
> >+
> > config SENSORS_NCT7802
> >       tristate "Nuvoton NCT7802Y"
> >       depends on I2C
> >diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile
> >index 47be39af5c03..d5e7531204df 100644
> >--- a/drivers/hwmon/Makefile
> >+++ b/drivers/hwmon/Makefile
> >@@ -167,6 +167,7 @@ obj-$(CONFIG_SENSORS_NCT6775_CORE) += nct6775-core.o
> > nct6775-objs                  := nct6775-platform.o
> > obj-$(CONFIG_SENSORS_NCT6775) += nct6775.o
> > obj-$(CONFIG_SENSORS_NCT6775_I2C) += nct6775-i2c.o
> >+obj-$(CONFIG_SENSORS_NCT7363) += nct7363.o
> > obj-$(CONFIG_SENSORS_NCT7802) += nct7802.o
> > obj-$(CONFIG_SENSORS_NCT7904) += nct7904.o
> > obj-$(CONFIG_SENSORS_NPCM7XX) += npcm750-pwm-fan.o
> >diff --git a/drivers/hwmon/nct7363.c b/drivers/hwmon/nct7363.c
> >new file mode 100644
> >index 000000000000..c79d3ca4f111
> >--- /dev/null
> >+++ b/drivers/hwmon/nct7363.c
> >@@ -0,0 +1,412 @@
> >+// SPDX-License-Identifier: GPL-2.0-or-later
> >+/*
> >+ * Copyright (c) 2023 Nuvoton Technology corporation.
> >+ */
> >+
> >+#include <linux/bitfield.h>
> >+#include <linux/bits.h>
> >+#include <linux/err.h>
> >+#include <linux/hwmon.h>
> >+#include <linux/hwmon-sysfs.h>
> >+#include <linux/i2c.h>
> >+#include <linux/module.h>
> >+#include <linux/mutex.h>
> >+#include <linux/regmap.h>
> >+#include <linux/slab.h>
> >+
> >+#define NCT7363_REG_GPIO_0_3          0x20
> >+#define NCT7363_REG_GPIO_4_7          0x21
> >+#define NCT7363_REG_GPIO_10_13                0x22
> >+#define NCT7363_REG_GPIO_14_17                0x23
> >+#define NCT7363_REG_PWMEN_0_7         0x38
> >+#define NCT7363_REG_PWMEN_8_15                0x39
> >+#define NCT7363_REG_FANINEN_0_7               0x41
> >+#define NCT7363_REG_FANINEN_8_15      0x42
> >+#define NCT7363_REG_FANINX_HVAL(x)    (0x48 + ((x) * 2))
> >+#define NCT7363_REG_FANINX_LVAL(x)    (0x49 + ((x) * 2))
> >+#define NCT7363_REG_FSCPXDUTY(x)      (0x90 + ((x) * 2))
> >+
> >+#define PWM_SEL(x)                    (BIT(0) << (((x) % 4) * 2))
> >+#define FANIN_SEL(x)                  (BIT(1) << (((x) % 4) * 2))
> >+
> >+#define NCT7363_FANINX_LVAL_MASK      GENMASK(4, 0)
> >+#define NCT7363_FANIN_MASK            GENMASK(12, 0)
> >+
> >+#define NCT7363_PWM_COUNT             16
> >+
> >+static inline unsigned long FAN_FROM_REG(u16 val)
> >+{
> >+      /* In case fan is stopped or divide by 0 */
> >+      if (val == NCT7363_FANIN_MASK || val == 0)
> >+              return  0;
> >+
> >+      return (1350000UL / val);
> >+}
> >+
> >+static const struct of_device_id nct7363_of_match[] = {
> >+      { .compatible = "nuvoton,nct7363" },
>
> As far as I can see from the code in this driver, it looks like this
> driver should also be compatible with the nct7362; shall we add the ID
> table entry for it now?  (Though I only have a datasheet for the
> nct7362, not the nct7363, so I don't know exactly how they differ.)

As far as I know, the difference between these two ICs is 0x2A~0x2C
Fading LED for 7362, and 0x2A Watchdog Timer for 7363.
In my v1 patch, I indeed add the nct7362 to the ID table, however,
this makes it more complicated and our datasheet isn't public yet.
I think maybe supporting more chips will be done in the future, but not now.

>
> >+      { },
> >+};
> >+MODULE_DEVICE_TABLE(of, nct7363_of_match);
> >+
> >+struct nct7363_data {
> >+      struct regmap           *regmap;
> >+      struct mutex            lock; /* protect register access */
> >+
> >+      u16                     fanin_mask;
> >+      u16                     pwm_mask;
> >+};
> >+
> >+static int nct7363_read_fan(struct device *dev, u32 attr, int channel,
> >+                          long *val)
> >+{
> >+      struct nct7363_data *data = dev_get_drvdata(dev);
> >+      unsigned int hi, lo;
> >+      u16 cnt, rpm;
> >+      int ret = 0;
> >+
> >+      switch (attr) {
> >+      case hwmon_fan_input:
> >+              /*
> >+               * High-byte register should be read first to latch
> >+               * synchronous low-byte value
> >+               */
> >+              ret = regmap_read(data->regmap,
> >+                                NCT7363_REG_FANINX_HVAL(channel), &hi);
> >+              if (ret)
> >+                      return ret;
> >+
> >+              ret = regmap_read(data->regmap,
> >+                      NCT7363_REG_FANINX_LVAL(channel), &lo);
> >+              if (ret)
> >+                      return ret;
>
> I think this pair of reads should be done under data->lock to ensure
> that the LVAL read gets the right latched value in a scenario where
> multiple threads executing this function end up with their register
> reads interleaved.

ok, fix in v5

>
> >+
> >+              cnt = (hi << 5) | (lo & NCT7363_FANINX_LVAL_MASK);
> >+              rpm = FAN_FROM_REG(cnt);
> >+              *val = (long)rpm;
>
> Given that rpm is assigned from an unsigned long (FAN_FROM_REG()) and
> then to a long (*val), is there any reason we want it as a u16 in
> between the two?  Or for that matter, why not just:
>
>    *val = FAN_FROM_REG(cnt);
>
> ?

I'll modify this to align with the style of the nct7363_read_pwm function in v5.

>
> >+              return 0;
> >+      default:
> >+              return -EOPNOTSUPP;
> >+      }
> >+}
> >+
> >+static umode_t nct7363_fan_is_visible(const void *_data, u32 attr, int channel)
> >+{
> >+      const struct nct7363_data *data = _data;
> >+
> >+      switch (attr) {
> >+      case hwmon_fan_input:
> >+              if (data->fanin_mask & BIT(channel))
> >+                      return 0444;
> >+              break;
> >+      default:
> >+              break;
> >+      }
> >+
> >+      return 0;
> >+}
> >+
> >+static int nct7363_read_pwm(struct device *dev, u32 attr, int channel,
> >+                          long *val)
> >+{
> >+      struct nct7363_data *data = dev_get_drvdata(dev);
> >+      unsigned int regval;
> >+      u16 ret;
> >+
> >+      switch (attr) {
> >+      case hwmon_pwm_input:
> >+              ret = regmap_read(data->regmap,
> >+                                NCT7363_REG_FSCPXDUTY(channel), &regval);
> >+              if (ret)
> >+                      return ret;
> >+
> >+              *val = (long)regval;
> >+              return 0;
> >+      default:
> >+              return -EOPNOTSUPP;
> >+      }
> >+}
> >+
> >+static int nct7363_write_pwm(struct device *dev, u32 attr, int channel,
> >+                           long val)
> >+{
> >+      struct nct7363_data *data = dev_get_drvdata(dev);
> >+      int ret;
> >+
> >+      switch (attr) {
> >+      case hwmon_pwm_input:
> >+              if (val < 0 || val > 255)
> >+                      return -EINVAL;
> >+
> >+              mutex_lock(&data->lock);
> >+              ret = regmap_write(data->regmap,
> >+                                 NCT7363_REG_FSCPXDUTY(channel), val);
> >+              mutex_unlock(&data->lock);
>
> ...though here, I'm not sure the locking is needed -- is there something
> that necessitates it for a single register write?

ok, fix in v5

>
> >+
> >+              return ret;
> >+
> >+      default:
> >+              return -EOPNOTSUPP;
> >+      }
> >+}
> >+
> >+static umode_t nct7363_pwm_is_visible(const void *_data, u32 attr, int channel)
> >+{
> >+      const struct nct7363_data *data = _data;
> >+
> >+      switch (attr) {
> >+      case hwmon_pwm_input:
> >+              if (data->pwm_mask & BIT(channel))
> >+                      return 0644;
> >+              break;
> >+      default:
> >+              break;
> >+      }
> >+
> >+      return 0;
> >+}
> >+
> >+static int nct7363_read(struct device *dev, enum hwmon_sensor_types type,
> >+                      u32 attr, int channel, long *val)
> >+{
> >+      switch (type) {
> >+      case hwmon_fan:
> >+              return nct7363_read_fan(dev, attr, channel, val);
> >+      case hwmon_pwm:
> >+              return nct7363_read_pwm(dev, attr, channel, val);
> >+      default:
> >+              return -EOPNOTSUPP;
> >+      }
> >+}
> >+
> >+static int nct7363_write(struct device *dev, enum hwmon_sensor_types type,
> >+                       u32 attr, int channel, long val)
> >+{
> >+      switch (type) {
> >+      case hwmon_pwm:
> >+              return nct7363_write_pwm(dev, attr, channel, val);
> >+      default:
> >+              return -EOPNOTSUPP;
> >+      }
> >+}
> >+
> >+static umode_t nct7363_is_visible(const void *data,
> >+                                enum hwmon_sensor_types type,
> >+                                u32 attr, int channel)
> >+{
> >+      switch (type) {
> >+      case hwmon_fan:
> >+              return nct7363_fan_is_visible(data, attr, channel);
> >+      case hwmon_pwm:
> >+              return nct7363_pwm_is_visible(data, attr, channel);
> >+      default:
> >+              return 0;
> >+      }
> >+}
> >+
> >+static const struct hwmon_channel_info *nct7363_info[] = {
> >+      HWMON_CHANNEL_INFO(fan,
> >+                         HWMON_F_INPUT,
> >+                         HWMON_F_INPUT,
> >+                         HWMON_F_INPUT,
> >+                         HWMON_F_INPUT,
> >+                         HWMON_F_INPUT,
> >+                         HWMON_F_INPUT,
> >+                         HWMON_F_INPUT,
> >+                         HWMON_F_INPUT,
> >+                         HWMON_F_INPUT,
> >+                         HWMON_F_INPUT,
> >+                         HWMON_F_INPUT,
> >+                         HWMON_F_INPUT,
> >+                         HWMON_F_INPUT,
> >+                         HWMON_F_INPUT,
> >+                         HWMON_F_INPUT,
> >+                         HWMON_F_INPUT),
> >+      HWMON_CHANNEL_INFO(pwm,
> >+                         HWMON_PWM_INPUT,
> >+                         HWMON_PWM_INPUT,
> >+                         HWMON_PWM_INPUT,
> >+                         HWMON_PWM_INPUT,
> >+                         HWMON_PWM_INPUT,
> >+                         HWMON_PWM_INPUT,
> >+                         HWMON_PWM_INPUT,
> >+                         HWMON_PWM_INPUT,
> >+                         HWMON_PWM_INPUT,
> >+                         HWMON_PWM_INPUT,
> >+                         HWMON_PWM_INPUT,
> >+                         HWMON_PWM_INPUT,
> >+                         HWMON_PWM_INPUT,
> >+                         HWMON_PWM_INPUT,
> >+                         HWMON_PWM_INPUT,
> >+                         HWMON_PWM_INPUT),
> >+      NULL
> >+};
> >+
> >+static const struct hwmon_ops nct7363_hwmon_ops = {
> >+      .is_visible = nct7363_is_visible,
> >+      .read = nct7363_read,
> >+      .write = nct7363_write,
> >+};
> >+
> >+static const struct hwmon_chip_info nct7363_chip_info = {
> >+      .ops = &nct7363_hwmon_ops,
> >+      .info = nct7363_info,
> >+};
> >+
> >+static int nct7363_init_chip(struct nct7363_data *data)
> >+{
> >+      u8 i, gpio0_3 = 0, gpio4_7 = 0, gpio10_13 = 0, gpio14_17 = 0;
> >+      int ret;
> >+
> >+      for (i = 0; i < NCT7363_PWM_COUNT; i++) {
> >+              if (i < 4) {
> >+                      if (data->pwm_mask & BIT(i))
> >+                              gpio0_3 |= PWM_SEL(i);
> >+                      if (data->fanin_mask & BIT(i))
> >+                              gpio10_13 |= FANIN_SEL(i);
> >+              } else if (i < 8) {
> >+                      if (data->pwm_mask & BIT(i))
> >+                              gpio4_7 |= PWM_SEL(i);
> >+                      if (data->fanin_mask & BIT(i))
> >+                              gpio14_17 |= FANIN_SEL(i);
> >+              } else if (i < 12) {
> >+                      if (data->pwm_mask & BIT(i))
> >+                              gpio10_13 |= PWM_SEL(i);
> >+                      if (data->fanin_mask & BIT(i))
> >+                              gpio0_3 |= FANIN_SEL(i);
> >+              } else {
> >+                      if (data->pwm_mask & BIT(i))
> >+                              gpio14_17 |= PWM_SEL(i);
> >+                      if (data->fanin_mask & BIT(i))
> >+                              gpio4_7 |= FANIN_SEL(i);
> >+              }
> >+      }
>
> With the caveat that I haven't actually sketched it out myself to be
> sure, might it be a bit cleaner to do this with a 4-element array
> indexed by 'i / 4' instead of a big if/else chain and a bunch of
> near-duplicated blocks?

ok, fix in v5

>
> >+
> >+      /* Pin Function Configuration */
> >+      ret = regmap_write(data->regmap, NCT7363_REG_GPIO_0_3, gpio0_3);
> >+      if (ret < 0)
> >+              return ret;
> >+      ret = regmap_write(data->regmap, NCT7363_REG_GPIO_4_7, gpio4_7);
> >+      if (ret < 0)
> >+              return ret;
> >+      ret = regmap_write(data->regmap, NCT7363_REG_GPIO_10_13, gpio10_13);
> >+      if (ret < 0)
> >+              return ret;
> >+      ret = regmap_write(data->regmap, NCT7363_REG_GPIO_14_17, gpio14_17);
> >+      if (ret < 0)
> >+              return ret;
> >+
> >+      /* PWM and FANIN Monitoring Enable */
> >+      ret = regmap_write(data->regmap, NCT7363_REG_PWMEN_0_7,
> >+                         data->pwm_mask & 0xff);
> >+      if (ret < 0)
> >+              return ret;
> >+      ret = regmap_write(data->regmap, NCT7363_REG_PWMEN_8_15,
> >+                         (data->pwm_mask >> 8) & 0xff);
> >+      if (ret < 0)
> >+              return ret;
> >+      ret = regmap_write(data->regmap, NCT7363_REG_FANINEN_0_7,
> >+                         data->fanin_mask & 0xff);
> >+      if (ret < 0)
> >+              return ret;
> >+      ret = regmap_write(data->regmap, NCT7363_REG_FANINEN_8_15,
> >+                         (data->fanin_mask >> 8) & 0xff);
> >+      if (ret < 0)
> >+              return ret;
> >+
> >+      return 0;
> >+}
> >+
> >+static int nct7363_present_pwm_fanin(struct device *dev,
> >+                                   struct device_node *child,
> >+                                   struct nct7363_data *data)
> >+{
> >+      struct of_phandle_args args;
> >+      int ret, fanin_cnt;
> >+      u8 *fanin_ch;
> >+      u8 ch, index;
> >+
> >+      ret = of_parse_phandle_with_args(child, "pwms", "#pwm-cells",
> >+                                       0, &args);
> >+      if (ret)
> >+              return ret;
> >+
> >+      data->pwm_mask |= BIT(args.args[0]);
>
> Perhaps we should have
>
>    if (args.args[0] >= NCT7363_PWM_COUNT)
>      return -EINVAL;
>
> here?

ok, fix in v5

>
> >+
> >+      fanin_cnt = of_property_count_u8_elems(child, "tach-ch");
> >+      if (fanin_cnt < 1)
>
> fanin_cnt < 1 || fanin_cnt >= NCT7363_PWM_COUNT

ok, fix in v5

>
> >+              return -EINVAL;
> >+
> >+      fanin_ch = devm_kcalloc(dev, fanin_cnt, sizeof(*fanin_ch), GFP_KERNEL);
>
> Keeping this allocation around persistently for the whole lifetime of
> the device seems unnecessary -- it's not used beyond this function,
> right?  In fact, dynamically allocating it at all seems like overkill,
> considering that in addition to being temporary, it's also got a pretty
> small upper bound on its size.  I'd think a simple
>
>    u8 fanin_ch[NCT7363_PWM_COUNT];
>
> would suffice?  (Along with the check above to ensure fanin_cnt is
> within range of course.)

ok, fix in v5

>
> >+      if (!fanin_ch)
> >+              return -ENOMEM;
> >+
> >+      ret = of_property_read_u8_array(child, "tach-ch", fanin_ch, fanin_cnt);
> >+      if (ret)
> >+              return ret;
> >+
> >+      for (ch = 0; ch < fanin_cnt; ch++) {
> >+              index = fanin_ch[ch];
>
> ...and likewise a range check here too?

ok, fix in v5

>
> >+              data->fanin_mask |= BIT(index);
> >+      }
> >+
>
> Should we also grab the pulses-per-revolution property here and factor
> that in in FAN_FROM_REG()?

So far our FanPoles in tachometer count calculation formula is always 4.
Therefore, I don't add the pulses-per-revolution property into FAN_FROM_REG().

>
> >+      return 0;
> >+}
> >+
> >+static const struct regmap_config nct7363_regmap_config = {
> >+      .reg_bits = 8,
> >+      .val_bits = 8,
> >+};
> >+
> >+static int nct7363_probe(struct i2c_client *client)
> >+{
> >+      struct device *dev = &client->dev;
> >+      struct device_node *child;
> >+      struct nct7363_data *data;
> >+      struct device *hwmon_dev;
> >+      int ret;
> >+
> >+      data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
> >+      if (!data)
> >+              return -ENOMEM;
> >+
> >+      data->regmap = devm_regmap_init_i2c(client, &nct7363_regmap_config);
> >+      if (IS_ERR(data->regmap))
> >+              return PTR_ERR(data->regmap);
> >+
> >+      mutex_init(&data->lock);
> >+
> >+      for_each_child_of_node(dev->of_node, child) {
> >+              ret = nct7363_present_pwm_fanin(dev, child, data);
> >+              if (ret) {
> >+                      of_node_put(child);
> >+                      return ret;
> >+              }
> >+      }
> >+
> >+      /* Initialize the chip */
> >+      ret = nct7363_init_chip(data);
> >+      if (ret)
> >+              return ret;
> >+
> >+      hwmon_dev =
> >+              devm_hwmon_device_register_with_info(dev, client->name, data,
> >+                                                   &nct7363_chip_info, NULL);
> >+      return PTR_ERR_OR_ZERO(hwmon_dev);
> >+}
> >+
> >+static struct i2c_driver nct7363_driver = {
> >+      .class = I2C_CLASS_HWMON,
>
> Maybe add an i2c_device_id table to accompany the of_match table?

ok, fix in v5

Thanks,
Ban

>
> >+      .driver = {
> >+              .name = "nct7363",
> >+              .of_match_table = nct7363_of_match,
> >+      },
> >+      .probe = nct7363_probe,
> >+};
> >+
> >+module_i2c_driver(nct7363_driver);
> >+
> >+MODULE_AUTHOR("CW Ho <cwho@xxxxxxxxxxx>");
> >+MODULE_AUTHOR("Ban Feng <kcfeng0@xxxxxxxxxxx>");
> >+MODULE_DESCRIPTION("NCT7363 Hardware Monitoring Driver");
> >+MODULE_LICENSE("GPL");
> >--
> >2.34.1
> >
> >





[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