Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux

hwmon: Create an NSA320 hardware monitoring driver

Create a driver to support the hardware monitoring chip present in
the Zyxel NSA320 and some of the other Zyxel NAS devices.

The driver reads fan speed and temperature from a suitably
pre-programmed MCU on the device.

Signed-off-by: Adam Baker <linux@baker-net.org.uk>
[groeck: Dropped .owner field initialization]
Signed-off-by: Guenter Roeck <linux@roeck-us.net>

authored by

Adam Baker and committed by
Guenter Roeck
630300d5 8d801243

+284
+53
Documentation/hwmon/nsa320
··· 1 + Kernel driver nsa320_hwmon 2 + ========================== 3 + 4 + Supported chips: 5 + * Holtek HT46R065 microcontroller with onboard firmware that configures 6 + it to act as a hardware monitor. 7 + Prefix: 'nsa320' 8 + Addresses scanned: none 9 + Datasheet: Not available, driver was reverse engineered based upon the 10 + Zyxel kernel source 11 + 12 + Author: 13 + Adam Baker <linux@baker-net.org.uk> 14 + 15 + Description 16 + ----------- 17 + 18 + This chip is known to be used in the Zyxel NSA320 and NSA325 NAS Units and 19 + also in some variants of the NSA310 but the driver has only been tested 20 + on the NSA320. In all of these devices it is connected to the same 3 GPIO 21 + lines which are used to provide chip select, clock and data lines. The 22 + interface behaves similarly to SPI but at much lower speeds than are normally 23 + used for SPI. 24 + 25 + Following each chip select pulse the chip will generate a single 32 bit word 26 + that contains 0x55 as a marker to indicate that data is being read correctly, 27 + followed by an 8 bit fan speed in 100s of RPM and a 16 bit temperature in 28 + tenths of a degree. 29 + 30 + 31 + sysfs-Interface 32 + --------------- 33 + 34 + temp1_input - temperature input 35 + fan1_input - fan speed 36 + 37 + Notes 38 + ----- 39 + 40 + The access timings used in the driver are the same as used in the Zyxel 41 + provided kernel. Testing has shown that if the delay between chip select and 42 + the first clock pulse is reduced from 100 ms to just under 10ms then the chip 43 + will not produce any output. If the duration of either phase of the clock 44 + is reduced from 100 us to less than 15 us then data pulses are likely to be 45 + read twice corrupting the output. The above analysis is based upon a sample 46 + of one unit but suggests that the Zyxel provided delay values include a 47 + reasonable tolerance. 48 + 49 + The driver incorporates a limit that it will not check for updated values 50 + faster than once a second. This is because the hardware takes a relatively long 51 + time to read the data from the device and when it does it reads both temp and 52 + fan speed. As the most likely case for two accesses in quick succession is 53 + to read both of these values avoiding a second read delay is desirable.
+15
drivers/hwmon/Kconfig
··· 1190 1190 This driver can also be built as a module. If so, the module 1191 1191 will be called nct7904. 1192 1192 1193 + config SENSORS_NSA320 1194 + tristate "ZyXEL NSA320 and compatible fan speed and temperature sensors" 1195 + depends on GPIOLIB && OF 1196 + depends on MACH_KIRKWOOD || COMPILE_TEST 1197 + help 1198 + If you say yes here you get support for hardware monitoring 1199 + for the ZyXEL NSA320 Media Server and other compatible devices 1200 + (probably the NSA325 and some NSA310 variants). 1201 + 1202 + The sensor data is taken from a Holtek HT46R065 microcontroller 1203 + connected to GPIO lines. 1204 + 1205 + This driver can also be built as a module. If so, the module 1206 + will be called nsa320-hwmon. 1207 + 1193 1208 config SENSORS_PCF8591 1194 1209 tristate "Philips PCF8591 ADC/DAC" 1195 1210 depends on I2C
+1
drivers/hwmon/Makefile
··· 124 124 obj-$(CONFIG_SENSORS_NCT6775) += nct6775.o 125 125 obj-$(CONFIG_SENSORS_NCT7802) += nct7802.o 126 126 obj-$(CONFIG_SENSORS_NCT7904) += nct7904.o 127 + obj-$(CONFIG_SENSORS_NSA320) += nsa320-hwmon.o 127 128 obj-$(CONFIG_SENSORS_NTC_THERMISTOR) += ntc_thermistor.o 128 129 obj-$(CONFIG_SENSORS_PC87360) += pc87360.o 129 130 obj-$(CONFIG_SENSORS_PC87427) += pc87427.o
+215
drivers/hwmon/nsa320-hwmon.c
··· 1 + /* 2 + * drivers/hwmon/nsa320-hwmon.c 3 + * 4 + * ZyXEL NSA320 Media Servers 5 + * hardware monitoring 6 + * 7 + * Copyright (C) 2016 Adam Baker <linux@baker-net.org.uk> 8 + * based on a board file driver 9 + * Copyright (C) 2012 Peter Schildmann <linux@schildmann.info> 10 + * 11 + * This program is free software; you can redistribute it and/or modify it 12 + * under the terms of the GNU General Public License v2 as published by the 13 + * Free Software Foundation. 14 + * 15 + * This program is distributed in the hope that it will be useful, but WITHOUT 16 + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 17 + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 18 + * more details. 19 + */ 20 + 21 + #include <linux/bitops.h> 22 + #include <linux/delay.h> 23 + #include <linux/err.h> 24 + #include <linux/gpio/consumer.h> 25 + #include <linux/hwmon.h> 26 + #include <linux/hwmon-sysfs.h> 27 + #include <linux/jiffies.h> 28 + #include <linux/module.h> 29 + #include <linux/mutex.h> 30 + #include <linux/of.h> 31 + #include <linux/of_device.h> 32 + #include <linux/of_platform.h> 33 + #include <linux/platform_device.h> 34 + 35 + /* Tests for error return values rely upon this value being < 0x80 */ 36 + #define MAGIC_NUMBER 0x55 37 + 38 + /* 39 + * The Zyxel hwmon MCU is a Holtek HT46R065 that is factory programmed 40 + * to perform temperature and fan speed monitoring. It is read by taking 41 + * the active pin low. The 32 bit output word is then clocked onto the 42 + * data line. The MSB of the data word is a magic nuber to indicate it 43 + * has been read correctly, the next byte is the fan speed (in hundreds 44 + * of RPM) and the last two bytes are the temperature (in tenths of a 45 + * degree) 46 + */ 47 + 48 + struct nsa320_hwmon { 49 + struct mutex update_lock; /* lock GPIO operations */ 50 + unsigned long last_updated; /* jiffies */ 51 + unsigned long mcu_data; 52 + struct gpio_desc *act; 53 + struct gpio_desc *clk; 54 + struct gpio_desc *data; 55 + }; 56 + 57 + enum nsa320_inputs { 58 + NSA320_TEMP = 0, 59 + NSA320_FAN = 1, 60 + }; 61 + 62 + static const char * const nsa320_input_names[] = { 63 + [NSA320_TEMP] = "System Temperature", 64 + [NSA320_FAN] = "Chassis Fan", 65 + }; 66 + 67 + /* 68 + * Although this protocol looks similar to SPI the long delay 69 + * between the active (aka chip select) signal and the shorter 70 + * delay between clock pulses are needed for reliable operation. 71 + * The delays provided are taken from the manufacturer kernel, 72 + * testing suggest they probably incorporate a reasonable safety 73 + * margin. (The single device tested became unreliable if the 74 + * delay was reduced to 1/10th of this value.) 75 + */ 76 + static s32 nsa320_hwmon_update(struct device *dev) 77 + { 78 + u32 mcu_data; 79 + u32 mask; 80 + struct nsa320_hwmon *hwmon = dev_get_drvdata(dev); 81 + 82 + mutex_lock(&hwmon->update_lock); 83 + 84 + mcu_data = hwmon->mcu_data; 85 + 86 + if (time_after(jiffies, hwmon->last_updated + HZ) || mcu_data == 0) { 87 + gpiod_set_value(hwmon->act, 1); 88 + msleep(100); 89 + 90 + mcu_data = 0; 91 + for (mask = BIT(31); mask; mask >>= 1) { 92 + gpiod_set_value(hwmon->clk, 0); 93 + usleep_range(100, 200); 94 + gpiod_set_value(hwmon->clk, 1); 95 + usleep_range(100, 200); 96 + if (gpiod_get_value(hwmon->data)) 97 + mcu_data |= mask; 98 + } 99 + 100 + gpiod_set_value(hwmon->act, 0); 101 + dev_dbg(dev, "Read raw MCU data %08x\n", mcu_data); 102 + 103 + if ((mcu_data >> 24) != MAGIC_NUMBER) { 104 + dev_dbg(dev, "Read invalid MCU data %08x\n", mcu_data); 105 + mcu_data = -EIO; 106 + } else { 107 + hwmon->mcu_data = mcu_data; 108 + hwmon->last_updated = jiffies; 109 + } 110 + } 111 + 112 + mutex_unlock(&hwmon->update_lock); 113 + 114 + return mcu_data; 115 + } 116 + 117 + static ssize_t show_label(struct device *dev, 118 + struct device_attribute *attr, char *buf) 119 + { 120 + int channel = to_sensor_dev_attr(attr)->index; 121 + 122 + return sprintf(buf, "%s\n", nsa320_input_names[channel]); 123 + } 124 + 125 + static ssize_t show_temp(struct device *dev, struct device_attribute *attr, 126 + char *buf) 127 + { 128 + s32 mcu_data = nsa320_hwmon_update(dev); 129 + 130 + if (mcu_data < 0) 131 + return mcu_data; 132 + 133 + return sprintf(buf, "%d\n", (mcu_data & 0xffff) * 100); 134 + } 135 + 136 + static ssize_t show_fan(struct device *dev, struct device_attribute *attr, 137 + char *buf) 138 + { 139 + s32 mcu_data = nsa320_hwmon_update(dev); 140 + 141 + if (mcu_data < 0) 142 + return mcu_data; 143 + 144 + return sprintf(buf, "%d\n", ((mcu_data & 0xff0000) >> 16) * 100); 145 + } 146 + 147 + static SENSOR_DEVICE_ATTR(temp1_label, S_IRUGO, show_label, NULL, NSA320_TEMP); 148 + static DEVICE_ATTR(temp1_input, S_IRUGO, show_temp, NULL); 149 + static SENSOR_DEVICE_ATTR(fan1_label, S_IRUGO, show_label, NULL, NSA320_FAN); 150 + static DEVICE_ATTR(fan1_input, S_IRUGO, show_fan, NULL); 151 + 152 + static struct attribute *nsa320_attrs[] = { 153 + &sensor_dev_attr_temp1_label.dev_attr.attr, 154 + &dev_attr_temp1_input.attr, 155 + &sensor_dev_attr_fan1_label.dev_attr.attr, 156 + &dev_attr_fan1_input.attr, 157 + NULL 158 + }; 159 + 160 + ATTRIBUTE_GROUPS(nsa320); 161 + 162 + static const struct of_device_id of_nsa320_hwmon_match[] = { 163 + { .compatible = "zyxel,nsa320-mcu", }, 164 + { }, 165 + }; 166 + 167 + static int nsa320_hwmon_probe(struct platform_device *pdev) 168 + { 169 + struct nsa320_hwmon *hwmon; 170 + struct device *classdev; 171 + 172 + hwmon = devm_kzalloc(&pdev->dev, sizeof(*hwmon), GFP_KERNEL); 173 + if (!hwmon) 174 + return -ENOMEM; 175 + 176 + /* Look up the GPIO pins to use */ 177 + hwmon->act = devm_gpiod_get(&pdev->dev, "act", GPIOD_OUT_LOW); 178 + if (IS_ERR(hwmon->act)) 179 + return PTR_ERR(hwmon->act); 180 + 181 + hwmon->clk = devm_gpiod_get(&pdev->dev, "clk", GPIOD_OUT_HIGH); 182 + if (IS_ERR(hwmon->clk)) 183 + return PTR_ERR(hwmon->clk); 184 + 185 + hwmon->data = devm_gpiod_get(&pdev->dev, "data", GPIOD_IN); 186 + if (IS_ERR(hwmon->data)) 187 + return PTR_ERR(hwmon->data); 188 + 189 + mutex_init(&hwmon->update_lock); 190 + 191 + classdev = devm_hwmon_device_register_with_groups(&pdev->dev, 192 + "nsa320", hwmon, nsa320_groups); 193 + 194 + return PTR_ERR_OR_ZERO(classdev); 195 + 196 + } 197 + 198 + /* All allocations use devres so remove() is not needed. */ 199 + 200 + static struct platform_driver nsa320_hwmon_driver = { 201 + .probe = nsa320_hwmon_probe, 202 + .driver = { 203 + .name = "nsa320-hwmon", 204 + .of_match_table = of_match_ptr(of_nsa320_hwmon_match), 205 + }, 206 + }; 207 + 208 + module_platform_driver(nsa320_hwmon_driver); 209 + 210 + MODULE_DEVICE_TABLE(of, of_nsa320_hwmon_match); 211 + MODULE_AUTHOR("Peter Schildmann <linux@schildmann.info>"); 212 + MODULE_AUTHOR("Adam Baker <linux@baker-net.org.uk>"); 213 + MODULE_DESCRIPTION("NSA320 Hardware Monitoring"); 214 + MODULE_LICENSE("GPL v2"); 215 + MODULE_ALIAS("platform:nsa320-hwmon");