diff --git a/Documentation/devicetree/bindings/mfd/qcom,tcsr.yaml b/Documentation/devicetree/bindings/mfd/qcom,tcsr.yaml index 79add913e35c..a503b67f2dbe 100644 --- a/Documentation/devicetree/bindings/mfd/qcom,tcsr.yaml +++ b/Documentation/devicetree/bindings/mfd/qcom,tcsr.yaml @@ -42,6 +42,7 @@ properties: - qcom,tcsr-apq8064 - qcom,tcsr-apq8084 - qcom,tcsr-ipq5332 + - qcom,tcsr-ipq5424 - qcom,tcsr-ipq6018 - qcom,tcsr-ipq8064 - qcom,tcsr-ipq8074 diff --git a/Documentation/devicetree/bindings/mfd/qnap,ts433-mcu.yaml b/Documentation/devicetree/bindings/mfd/qnap,ts433-mcu.yaml new file mode 100644 index 000000000000..877078ac172f --- /dev/null +++ b/Documentation/devicetree/bindings/mfd/qnap,ts433-mcu.yaml @@ -0,0 +1,42 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/mfd/qnap,ts433-mcu.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: QNAP NAS on-board Microcontroller + +maintainers: + - Heiko Stuebner + +description: + QNAP embeds a microcontroller on their NAS devices adding system feature + as PWM Fan control, additional LEDs, power button status and more. + +properties: + compatible: + enum: + - qnap,ts433-mcu + +patternProperties: + "^fan-[0-9]+$": + $ref: /schemas/hwmon/fan-common.yaml# + unevaluatedProperties: false + +required: + - compatible + +additionalProperties: false + +examples: + - | + uart { + mcu { + compatible = "qnap,ts433-mcu"; + + fan-0 { + #cooling-cells = <2>; + cooling-levels = <0 64 89 128 166 204 221 238>; + }; + }; + }; diff --git a/Documentation/devicetree/bindings/mfd/realtek,rtl9301-switch.yaml b/Documentation/devicetree/bindings/mfd/realtek,rtl9301-switch.yaml index f053303ab1e6..eeb08e7435fa 100644 --- a/Documentation/devicetree/bindings/mfd/realtek,rtl9301-switch.yaml +++ b/Documentation/devicetree/bindings/mfd/realtek,rtl9301-switch.yaml @@ -41,6 +41,9 @@ patternProperties: 'i2c@[0-9a-f]+$': $ref: /schemas/i2c/realtek,rtl9301-i2c.yaml# + 'mdio@[0-9a-f]+$': + $ref: /schemas/net/realtek,rtl9301-mdio.yaml# + required: - compatible - reg @@ -110,5 +113,17 @@ examples: }; }; }; + + mdio0: mdio@ca00 { + compatible = "realtek,rtl9301-mdio"; + reg = <0xca00 0x200>; + #address-cells = <1>; + #size-cells = <0>; + + ethernet-phy@0 { + reg = <0>; + realtek,smi-address = <0 1>; + }; + }; }; diff --git a/Documentation/devicetree/bindings/mfd/rohm,bd71815-pmic.yaml b/Documentation/devicetree/bindings/mfd/rohm,bd71815-pmic.yaml index bb81307dc11b..4fc78efaa550 100644 --- a/Documentation/devicetree/bindings/mfd/rohm,bd71815-pmic.yaml +++ b/Documentation/devicetree/bindings/mfd/rohm,bd71815-pmic.yaml @@ -50,15 +50,15 @@ properties: minimum: 0 maximum: 1 - rohm,charger-sense-resistor-ohms: - minimum: 10000000 - maximum: 50000000 + rohm,charger-sense-resistor-micro-ohms: + minimum: 10000 + maximum: 50000 description: | - BD71827 and BD71828 have SAR ADC for measuring charging currents. - External sense resistor (RSENSE in data sheet) should be used. If - something other but 30MOhm resistor is used the resistance value - should be given here in Ohms. - default: 30000000 + BD71815 has SAR ADC for measuring charging currents. External sense + resistor (RSENSE in data sheet) should be used. If something other + but a 30 mOhm resistor is used the resistance value should be given + here in micro Ohms. + default: 30000 regulators: $ref: /schemas/regulator/rohm,bd71815-regulator.yaml @@ -67,7 +67,7 @@ properties: gpio-reserved-ranges: description: | - Usage of BD71828 GPIO pins can be changed via OTP. This property can be + Usage of BD71815 GPIO pins can be changed via OTP. This property can be used to mark the pins which should not be configured for GPIO. Please see the ../gpio/gpio.txt for more information. @@ -113,7 +113,7 @@ examples: gpio-controller; #gpio-cells = <2>; - rohm,charger-sense-resistor-ohms = <10000000>; + rohm,charger-sense-resistor-micro-ohms = <10000>; regulators { buck1: buck1 { diff --git a/Documentation/hwmon/index.rst b/Documentation/hwmon/index.rst index b1ea445479b0..874f8fd26325 100644 --- a/Documentation/hwmon/index.rst +++ b/Documentation/hwmon/index.rst @@ -202,6 +202,7 @@ Hardware Monitoring Kernel Drivers pxe1610 pwm-fan q54sj108a2 + qnap-mcu-hwmon raspberrypi-hwmon sbrmi sbtsi_temp diff --git a/Documentation/hwmon/qnap-mcu-hwmon.rst b/Documentation/hwmon/qnap-mcu-hwmon.rst new file mode 100644 index 000000000000..83407e3408f2 --- /dev/null +++ b/Documentation/hwmon/qnap-mcu-hwmon.rst @@ -0,0 +1,27 @@ +.. SPDX-License-Identifier: GPL-2.0-or-later + +Kernel driver qnap-mcu-hwmon +============================ + +This driver enables the use of the hardware monitoring and fan control +of the MCU used on some QNAP network attached storage devices. + +Author: Heiko Stuebner + +Description +----------- + +The driver implements a simple interface for driving the fan controlled by +setting its PWM output value and exposes the fan rpm and case-temperature +to user space through hwmon's sysfs interface. + +The fan rotation speed returned via the optional 'fan1_input' is calculated +inside the MCU device. + +The driver provides the following sensor accesses in sysfs: + +=============== ======= ======================================================= +fan1_input ro fan tachometer speed in RPM +pwm1 rw relative speed (0-255), 255=max. speed. +temp1_input ro Measured temperature in millicelsius +=============== ======= ======================================================= diff --git a/MAINTAINERS b/MAINTAINERS index 1d55e6279b24..a97e2ba9a469 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -19170,6 +19170,15 @@ L: linux-media@vger.kernel.org S: Odd Fixes F: drivers/media/tuners/qm1d1c0042* +QNAP MCU DRIVER +M: Heiko Stuebner +S: Maintained +F: drivers/hwmon/qnap-mcu-hwmon.c +F: drivers/input/misc/qnap-mcu-input.c +F: drivers/leds/leds-qnap-mcu.c +F: drivers/mfd/qnap-mcu.c +F: include/linux/qnap-mcu.h + QNX4 FILESYSTEM M: Anders Larsen S: Maintained diff --git a/drivers/hid/hid-sensor-hub.c b/drivers/hid/hid-sensor-hub.c index 7bd86eef6ec7..4c94c03cb573 100644 --- a/drivers/hid/hid-sensor-hub.c +++ b/drivers/hid/hid-sensor-hub.c @@ -730,23 +730,30 @@ static int sensor_hub_probe(struct hid_device *hdev, return ret; } +static int sensor_hub_finalize_pending_fn(struct device *dev, void *data) +{ + struct hid_sensor_hub_device *hsdev = dev->platform_data; + + if (hsdev->pending.status) + complete(&hsdev->pending.ready); + + return 0; +} + static void sensor_hub_remove(struct hid_device *hdev) { struct sensor_hub_data *data = hid_get_drvdata(hdev); unsigned long flags; - int i; hid_dbg(hdev, " hardware removed\n"); hid_hw_close(hdev); hid_hw_stop(hdev); + spin_lock_irqsave(&data->lock, flags); - for (i = 0; i < data->hid_sensor_client_cnt; ++i) { - struct hid_sensor_hub_device *hsdev = - data->hid_sensor_hub_client_devs[i].platform_data; - if (hsdev->pending.status) - complete(&hsdev->pending.ready); - } + device_for_each_child(&hdev->dev, NULL, + sensor_hub_finalize_pending_fn); spin_unlock_irqrestore(&data->lock, flags); + mfd_remove_devices(&hdev->dev); mutex_destroy(&data->mutex); } diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig index dd376602f3f1..269d197fabf7 100644 --- a/drivers/hwmon/Kconfig +++ b/drivers/hwmon/Kconfig @@ -1822,6 +1822,18 @@ config SENSORS_PWM_FAN This driver can also be built as a module. If so, the module will be called pwm-fan. +config SENSORS_QNAP_MCU_HWMON + tristate "QNAP MCU hardware monitoring" + depends on MFD_QNAP_MCU + depends on THERMAL || THERMAL=n + help + Say yes here to enable support for fan and temperature sensor + connected to a QNAP MCU, as found in a number of QNAP network + attached storage devices. + + This driver can also be built as a module. If so, the module + will be called qnap-mcu-hwmon. + config SENSORS_RASPBERRYPI_HWMON tristate "Raspberry Pi voltage monitor" depends on RASPBERRYPI_FIRMWARE || (COMPILE_TEST && !RASPBERRYPI_FIRMWARE) diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile index b827b92f2a78..b7ef0f0562d3 100644 --- a/drivers/hwmon/Makefile +++ b/drivers/hwmon/Makefile @@ -189,6 +189,7 @@ obj-$(CONFIG_SENSORS_POWERZ) += powerz.o obj-$(CONFIG_SENSORS_POWR1220) += powr1220.o obj-$(CONFIG_SENSORS_PT5161L) += pt5161l.o obj-$(CONFIG_SENSORS_PWM_FAN) += pwm-fan.o +obj-$(CONFIG_SENSORS_QNAP_MCU_HWMON) += qnap-mcu-hwmon.o obj-$(CONFIG_SENSORS_RASPBERRYPI_HWMON) += raspberrypi-hwmon.o obj-$(CONFIG_SENSORS_SBTSI) += sbtsi_temp.o obj-$(CONFIG_SENSORS_SBRMI) += sbrmi.o diff --git a/drivers/hwmon/qnap-mcu-hwmon.c b/drivers/hwmon/qnap-mcu-hwmon.c new file mode 100644 index 000000000000..29057514739c --- /dev/null +++ b/drivers/hwmon/qnap-mcu-hwmon.c @@ -0,0 +1,364 @@ +// SPDX-License-Identifier: GPL-2.0-only + +/* + * Driver for hwmon elements found on QNAP-MCU devices + * + * Copyright (C) 2024 Heiko Stuebner + */ + +#include +#include +#include +#include +#include +#include +#include + +struct qnap_mcu_hwmon { + struct qnap_mcu *mcu; + struct device *dev; + + unsigned int pwm_min; + unsigned int pwm_max; + + struct fwnode_handle *fan_node; + unsigned int fan_state; + unsigned int fan_max_state; + unsigned int *fan_cooling_levels; + + struct thermal_cooling_device *cdev; + struct hwmon_chip_info info; +}; + +static int qnap_mcu_hwmon_get_rpm(struct qnap_mcu_hwmon *hwm) +{ + static const u8 cmd[] = { '@', 'F', 'A' }; + u8 reply[6]; + int ret; + + /* poll the fan rpm */ + ret = qnap_mcu_exec(hwm->mcu, cmd, sizeof(cmd), reply, sizeof(reply)); + if (ret) + return ret; + + /* First 2 bytes must mirror the sent command */ + if (memcmp(cmd, reply, 2)) + return -EIO; + + return reply[4] * 30; +} + +static int qnap_mcu_hwmon_get_pwm(struct qnap_mcu_hwmon *hwm) +{ + static const u8 cmd[] = { '@', 'F', 'Z', '0' }; /* 0 = fan-id? */ + u8 reply[4]; + int ret; + + /* poll the fan pwm */ + ret = qnap_mcu_exec(hwm->mcu, cmd, sizeof(cmd), reply, sizeof(reply)); + if (ret) + return ret; + + /* First 3 bytes must mirror the sent command */ + if (memcmp(cmd, reply, 3)) + return -EIO; + + return reply[3]; +} + +static int qnap_mcu_hwmon_set_pwm(struct qnap_mcu_hwmon *hwm, u8 pwm) +{ + const u8 cmd[] = { '@', 'F', 'W', '0', pwm }; /* 0 = fan-id?, pwm 0-255 */ + + /* set the fan pwm */ + return qnap_mcu_exec_with_ack(hwm->mcu, cmd, sizeof(cmd)); +} + +static int qnap_mcu_hwmon_get_temp(struct qnap_mcu_hwmon *hwm) +{ + static const u8 cmd[] = { '@', 'T', '3' }; + u8 reply[4]; + int ret; + + /* poll the fan rpm */ + ret = qnap_mcu_exec(hwm->mcu, cmd, sizeof(cmd), reply, sizeof(reply)); + if (ret) + return ret; + + /* First bytes must mirror the sent command */ + if (memcmp(cmd, reply, sizeof(cmd))) + return -EIO; + + /* + * There is an unknown bit set in bit7. + * Bits [6:0] report the actual temperature as returned by the + * original qnap firmware-tools, so just drop bit7 for now. + */ + return (reply[3] & 0x7f) * 1000; +} + +static int qnap_mcu_hwmon_write(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long val) +{ + struct qnap_mcu_hwmon *hwm = dev_get_drvdata(dev); + + switch (attr) { + case hwmon_pwm_input: + if (val < 0 || val > 255) + return -EINVAL; + + if (val != 0) + val = clamp_val(val, hwm->pwm_min, hwm->pwm_max); + + return qnap_mcu_hwmon_set_pwm(hwm, val); + default: + return -EOPNOTSUPP; + } + + return 0; +} + +static int qnap_mcu_hwmon_read(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long *val) +{ + struct qnap_mcu_hwmon *hwm = dev_get_drvdata(dev); + int ret; + + switch (type) { + case hwmon_pwm: + switch (attr) { + case hwmon_pwm_input: + ret = qnap_mcu_hwmon_get_pwm(hwm); + if (ret < 0) + return ret; + + *val = ret; + return 0; + default: + return -EOPNOTSUPP; + } + case hwmon_fan: + ret = qnap_mcu_hwmon_get_rpm(hwm); + if (ret < 0) + return ret; + + *val = ret; + return 0; + case hwmon_temp: + ret = qnap_mcu_hwmon_get_temp(hwm); + if (ret < 0) + return ret; + + *val = ret; + return 0; + default: + return -EOPNOTSUPP; + } +} + +static umode_t qnap_mcu_hwmon_is_visible(const void *data, + enum hwmon_sensor_types type, + u32 attr, int channel) +{ + switch (type) { + case hwmon_temp: + return 0444; + + case hwmon_pwm: + return 0644; + + case hwmon_fan: + return 0444; + + default: + return 0; + } +} + +static const struct hwmon_ops qnap_mcu_hwmon_hwmon_ops = { + .is_visible = qnap_mcu_hwmon_is_visible, + .read = qnap_mcu_hwmon_read, + .write = qnap_mcu_hwmon_write, +}; + +/* thermal cooling device callbacks */ +static int qnap_mcu_hwmon_get_max_state(struct thermal_cooling_device *cdev, + unsigned long *state) +{ + struct qnap_mcu_hwmon *hwm = cdev->devdata; + + if (!hwm) + return -EINVAL; + + *state = hwm->fan_max_state; + + return 0; +} + +static int qnap_mcu_hwmon_get_cur_state(struct thermal_cooling_device *cdev, + unsigned long *state) +{ + struct qnap_mcu_hwmon *hwm = cdev->devdata; + + if (!hwm) + return -EINVAL; + + *state = hwm->fan_state; + + return 0; +} + +static int qnap_mcu_hwmon_set_cur_state(struct thermal_cooling_device *cdev, + unsigned long state) +{ + struct qnap_mcu_hwmon *hwm = cdev->devdata; + int ret; + + if (!hwm || state > hwm->fan_max_state) + return -EINVAL; + + if (state == hwm->fan_state) + return 0; + + ret = qnap_mcu_hwmon_set_pwm(hwm, hwm->fan_cooling_levels[state]); + if (ret) + return ret; + + hwm->fan_state = state; + + return ret; +} + +static const struct thermal_cooling_device_ops qnap_mcu_hwmon_cooling_ops = { + .get_max_state = qnap_mcu_hwmon_get_max_state, + .get_cur_state = qnap_mcu_hwmon_get_cur_state, + .set_cur_state = qnap_mcu_hwmon_set_cur_state, +}; + +static void devm_fan_node_release(void *data) +{ + struct qnap_mcu_hwmon *hwm = data; + + fwnode_handle_put(hwm->fan_node); +} + +static int qnap_mcu_hwmon_get_cooling_data(struct device *dev, struct qnap_mcu_hwmon *hwm) +{ + struct fwnode_handle *fwnode; + int num, i, ret; + + fwnode = device_get_named_child_node(dev->parent, "fan-0"); + if (!fwnode) + return 0; + + /* if we found the fan-node, we're keeping it until device-unbind */ + hwm->fan_node = fwnode; + ret = devm_add_action_or_reset(dev, devm_fan_node_release, hwm); + if (ret) + return ret; + + num = fwnode_property_count_u32(fwnode, "cooling-levels"); + if (num <= 0) + return dev_err_probe(dev, num ? : -EINVAL, + "Failed to count elements in 'cooling-levels'\n"); + + hwm->fan_cooling_levels = devm_kcalloc(dev, num, sizeof(u32), + GFP_KERNEL); + if (!hwm->fan_cooling_levels) + return -ENOMEM; + + ret = fwnode_property_read_u32_array(fwnode, "cooling-levels", + hwm->fan_cooling_levels, num); + if (ret) + return dev_err_probe(dev, ret, "Failed to read 'cooling-levels'\n"); + + for (i = 0; i < num; i++) { + if (hwm->fan_cooling_levels[i] > hwm->pwm_max) + return dev_err_probe(dev, -EINVAL, "fan state[%d]:%d > %d\n", i, + hwm->fan_cooling_levels[i], hwm->pwm_max); + } + + hwm->fan_max_state = num - 1; + + return 0; +} + +static const struct hwmon_channel_info * const qnap_mcu_hwmon_channels[] = { + HWMON_CHANNEL_INFO(pwm, HWMON_PWM_INPUT), + HWMON_CHANNEL_INFO(fan, HWMON_F_INPUT), + HWMON_CHANNEL_INFO(temp, HWMON_T_INPUT), + NULL +}; + +static int qnap_mcu_hwmon_probe(struct platform_device *pdev) +{ + struct qnap_mcu *mcu = dev_get_drvdata(pdev->dev.parent); + const struct qnap_mcu_variant *variant = pdev->dev.platform_data; + struct qnap_mcu_hwmon *hwm; + struct thermal_cooling_device *cdev; + struct device *dev = &pdev->dev; + struct device *hwmon; + int ret; + + hwm = devm_kzalloc(dev, sizeof(*hwm), GFP_KERNEL); + if (!hwm) + return -ENOMEM; + + hwm->mcu = mcu; + hwm->dev = &pdev->dev; + hwm->pwm_min = variant->fan_pwm_min; + hwm->pwm_max = variant->fan_pwm_max; + + platform_set_drvdata(pdev, hwm); + + /* + * Set duty cycle to maximum allowed. + */ + ret = qnap_mcu_hwmon_set_pwm(hwm, hwm->pwm_max); + if (ret) + return ret; + + hwm->info.ops = &qnap_mcu_hwmon_hwmon_ops; + hwm->info.info = qnap_mcu_hwmon_channels; + + ret = qnap_mcu_hwmon_get_cooling_data(dev, hwm); + if (ret) + return ret; + + hwm->fan_state = hwm->fan_max_state; + + hwmon = devm_hwmon_device_register_with_info(dev, "qnapmcu", + hwm, &hwm->info, NULL); + if (IS_ERR(hwmon)) + return dev_err_probe(dev, PTR_ERR(hwmon), "Failed to register hwmon device\n"); + + /* + * Only register cooling device when we found cooling-levels. + * qnap_mcu_hwmon_get_cooling_data() will fail when reading malformed + * levels and only succeed with either no or correct cooling levels. + */ + if (IS_ENABLED(CONFIG_THERMAL) && hwm->fan_cooling_levels) { + cdev = devm_thermal_of_cooling_device_register(dev, + to_of_node(hwm->fan_node), "qnap-mcu-hwmon", + hwm, &qnap_mcu_hwmon_cooling_ops); + if (IS_ERR(cdev)) + return dev_err_probe(dev, PTR_ERR(cdev), + "Failed to register qnap-mcu-hwmon as cooling device\n"); + hwm->cdev = cdev; + } + + return 0; +} + +static struct platform_driver qnap_mcu_hwmon_driver = { + .probe = qnap_mcu_hwmon_probe, + .driver = { + .name = "qnap-mcu-hwmon", + }, +}; +module_platform_driver(qnap_mcu_hwmon_driver); + +MODULE_ALIAS("platform:qnap-mcu-hwmon"); +MODULE_AUTHOR("Heiko Stuebner "); +MODULE_DESCRIPTION("QNAP MCU hwmon driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/input/misc/Kconfig b/drivers/input/misc/Kconfig index 6a852c76331b..13d135257e06 100644 --- a/drivers/input/misc/Kconfig +++ b/drivers/input/misc/Kconfig @@ -917,6 +917,18 @@ config INPUT_HISI_POWERKEY To compile this driver as a module, choose M here: the module will be called hisi_powerkey. +config INPUT_QNAP_MCU + tristate "Input Support for QNAP MCU controllers" + depends on MFD_QNAP_MCU + help + This option enables support for input elements available on + embedded controllers used in QNAP NAS devices. + + This includes a polled power-button as well as a beeper. + + To compile this driver as a module, choose M here: the + module will be called qnap-mcu-input. + config INPUT_RAVE_SP_PWRBUTTON tristate "RAVE SP Power button Driver" depends on RAVE_SP_CORE diff --git a/drivers/input/misc/Makefile b/drivers/input/misc/Makefile index 4f7f736831ba..6d91804d0a6f 100644 --- a/drivers/input/misc/Makefile +++ b/drivers/input/misc/Makefile @@ -68,6 +68,7 @@ obj-$(CONFIG_INPUT_PMIC8XXX_PWRKEY) += pmic8xxx-pwrkey.o obj-$(CONFIG_INPUT_POWERMATE) += powermate.o obj-$(CONFIG_INPUT_PWM_BEEPER) += pwm-beeper.o obj-$(CONFIG_INPUT_PWM_VIBRA) += pwm-vibra.o +obj-$(CONFIG_INPUT_QNAP_MCU) += qnap-mcu-input.o obj-$(CONFIG_INPUT_RAVE_SP_PWRBUTTON) += rave-sp-pwrbutton.o obj-$(CONFIG_INPUT_RB532_BUTTON) += rb532_button.o obj-$(CONFIG_INPUT_REGULATOR_HAPTIC) += regulator-haptic.o diff --git a/drivers/input/misc/qnap-mcu-input.c b/drivers/input/misc/qnap-mcu-input.c new file mode 100644 index 000000000000..76e62f0816c1 --- /dev/null +++ b/drivers/input/misc/qnap-mcu-input.c @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: GPL-2.0-only + +/* + * Driver for input events on QNAP-MCUs + * + * Copyright (C) 2024 Heiko Stuebner + */ + +#include +#include +#include +#include +#include +#include + +/* + * The power-key needs to be pressed for a while to create an event, + * so there is no use for overly frequent polling. + */ +#define POLL_INTERVAL 500 + +struct qnap_mcu_input_dev { + struct input_dev *input; + struct qnap_mcu *mcu; + struct device *dev; + + struct work_struct beep_work; + int beep_type; +}; + +static void qnap_mcu_input_poll(struct input_dev *input) +{ + struct qnap_mcu_input_dev *idev = input_get_drvdata(input); + static const u8 cmd[] = { '@', 'C', 'V' }; + u8 reply[4]; + int state, ret; + + /* poll the power button */ + ret = qnap_mcu_exec(idev->mcu, cmd, sizeof(cmd), reply, sizeof(reply)); + if (ret) + return; + + /* First bytes must mirror the sent command */ + if (memcmp(cmd, reply, sizeof(cmd))) { + dev_err(idev->dev, "malformed data received\n"); + return; + } + + state = reply[3] - 0x30; + input_event(input, EV_KEY, KEY_POWER, state); + input_sync(input); +} + +static void qnap_mcu_input_beeper_work(struct work_struct *work) +{ + struct qnap_mcu_input_dev *idev = + container_of(work, struct qnap_mcu_input_dev, beep_work); + const u8 cmd[] = { '@', 'C', (idev->beep_type == SND_TONE) ? '3' : '2' }; + + qnap_mcu_exec_with_ack(idev->mcu, cmd, sizeof(cmd)); +} + +static int qnap_mcu_input_event(struct input_dev *input, unsigned int type, + unsigned int code, int value) +{ + struct qnap_mcu_input_dev *idev = input_get_drvdata(input); + + if (type != EV_SND || (code != SND_BELL && code != SND_TONE)) + return -EOPNOTSUPP; + + if (value < 0) + return -EINVAL; + + /* beep runtime is determined by the MCU */ + if (value == 0) + return 0; + + /* Schedule work to actually turn the beeper on */ + idev->beep_type = code; + schedule_work(&idev->beep_work); + + return 0; +} + +static void qnap_mcu_input_close(struct input_dev *input) +{ + struct qnap_mcu_input_dev *idev = input_get_drvdata(input); + + cancel_work_sync(&idev->beep_work); +} + +static int qnap_mcu_input_probe(struct platform_device *pdev) +{ + struct qnap_mcu *mcu = dev_get_drvdata(pdev->dev.parent); + struct qnap_mcu_input_dev *idev; + struct device *dev = &pdev->dev; + struct input_dev *input; + int ret; + + idev = devm_kzalloc(dev, sizeof(*idev), GFP_KERNEL); + if (!idev) + return -ENOMEM; + + input = devm_input_allocate_device(dev); + if (!input) + return dev_err_probe(dev, -ENOMEM, "no memory for input device\n"); + + idev->input = input; + idev->dev = dev; + idev->mcu = mcu; + + input_set_drvdata(input, idev); + + input->name = "qnap-mcu"; + input->phys = "qnap-mcu-input/input0"; + input->id.bustype = BUS_HOST; + input->id.vendor = 0x0001; + input->id.product = 0x0001; + input->id.version = 0x0100; + input->event = qnap_mcu_input_event; + input->close = qnap_mcu_input_close; + + input_set_capability(input, EV_KEY, KEY_POWER); + input_set_capability(input, EV_SND, SND_BELL); + input_set_capability(input, EV_SND, SND_TONE); + + INIT_WORK(&idev->beep_work, qnap_mcu_input_beeper_work); + + ret = input_setup_polling(input, qnap_mcu_input_poll); + if (ret) + return dev_err_probe(dev, ret, "unable to set up polling\n"); + + input_set_poll_interval(input, POLL_INTERVAL); + + ret = input_register_device(input); + if (ret) + return dev_err_probe(dev, ret, "unable to register input device\n"); + + return 0; +} + +static struct platform_driver qnap_mcu_input_driver = { + .probe = qnap_mcu_input_probe, + .driver = { + .name = "qnap-mcu-input", + }, +}; +module_platform_driver(qnap_mcu_input_driver); + +MODULE_ALIAS("platform:qnap-mcu-input"); +MODULE_AUTHOR("Heiko Stuebner "); +MODULE_DESCRIPTION("QNAP MCU input driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/leds/Kconfig b/drivers/leds/Kconfig index b784bb74a837..28a208fa893e 100644 --- a/drivers/leds/Kconfig +++ b/drivers/leds/Kconfig @@ -580,6 +580,17 @@ config LEDS_PCA995X LED driver chips accessed via the I2C bus. Supported devices include PCA9955BTW, PCA9952TW and PCA9955TW. +config LEDS_QNAP_MCU + tristate "LED Support for QNAP MCU controllers" + depends on LEDS_CLASS + depends on MFD_QNAP_MCU + help + This option enables support for LEDs available on embedded + controllers used in QNAP NAS devices. + + This driver can also be built as a module. If so, the module + will be called qnap-mcu-leds. + config LEDS_WM831X_STATUS tristate "LED support for status LEDs on WM831x PMICs" depends on LEDS_CLASS diff --git a/drivers/leds/Makefile b/drivers/leds/Makefile index 18afbb5a23ee..c6f74865d729 100644 --- a/drivers/leds/Makefile +++ b/drivers/leds/Makefile @@ -79,6 +79,7 @@ obj-$(CONFIG_LEDS_PCA995X) += leds-pca995x.o obj-$(CONFIG_LEDS_PM8058) += leds-pm8058.o obj-$(CONFIG_LEDS_POWERNV) += leds-powernv.o obj-$(CONFIG_LEDS_PWM) += leds-pwm.o +obj-$(CONFIG_LEDS_QNAP_MCU) += leds-qnap-mcu.o obj-$(CONFIG_LEDS_REGULATOR) += leds-regulator.o obj-$(CONFIG_LEDS_SC27XX_BLTC) += leds-sc27xx-bltc.o obj-$(CONFIG_LEDS_SUN50I_A100) += leds-sun50i-a100.o diff --git a/drivers/leds/leds-qnap-mcu.c b/drivers/leds/leds-qnap-mcu.c new file mode 100644 index 000000000000..4e4709456261 --- /dev/null +++ b/drivers/leds/leds-qnap-mcu.c @@ -0,0 +1,227 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Driver for LEDs found on QNAP MCU devices + * + * Copyright (C) 2024 Heiko Stuebner + */ + +#include +#include +#include +#include +#include +#include + +enum qnap_mcu_err_led_mode { + QNAP_MCU_ERR_LED_ON = 0, + QNAP_MCU_ERR_LED_OFF = 1, + QNAP_MCU_ERR_LED_BLINK_FAST = 2, + QNAP_MCU_ERR_LED_BLINK_SLOW = 3, +}; + +struct qnap_mcu_err_led { + struct qnap_mcu *mcu; + struct led_classdev cdev; + char name[LED_MAX_NAME_SIZE]; + u8 num; + u8 mode; +}; + +static inline struct qnap_mcu_err_led * + cdev_to_qnap_mcu_err_led(struct led_classdev *led_cdev) +{ + return container_of(led_cdev, struct qnap_mcu_err_led, cdev); +} + +static int qnap_mcu_err_led_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + struct qnap_mcu_err_led *err_led = cdev_to_qnap_mcu_err_led(led_cdev); + u8 cmd[] = { '@', 'R', '0' + err_led->num, '0' }; + + /* Don't disturb a possible set blink-mode if LED stays on */ + if (brightness != 0 && err_led->mode >= QNAP_MCU_ERR_LED_BLINK_FAST) + return 0; + + err_led->mode = brightness ? QNAP_MCU_ERR_LED_ON : QNAP_MCU_ERR_LED_OFF; + cmd[3] = '0' + err_led->mode; + + return qnap_mcu_exec_with_ack(err_led->mcu, cmd, sizeof(cmd)); +} + +static int qnap_mcu_err_led_blink_set(struct led_classdev *led_cdev, + unsigned long *delay_on, + unsigned long *delay_off) +{ + struct qnap_mcu_err_led *err_led = cdev_to_qnap_mcu_err_led(led_cdev); + u8 cmd[] = { '@', 'R', '0' + err_led->num, '0' }; + + /* LED is off, nothing to do */ + if (err_led->mode == QNAP_MCU_ERR_LED_OFF) + return 0; + + if (*delay_on < 500) { + *delay_on = 100; + *delay_off = 100; + err_led->mode = QNAP_MCU_ERR_LED_BLINK_FAST; + } else { + *delay_on = 500; + *delay_off = 500; + err_led->mode = QNAP_MCU_ERR_LED_BLINK_SLOW; + } + + cmd[3] = '0' + err_led->mode; + + return qnap_mcu_exec_with_ack(err_led->mcu, cmd, sizeof(cmd)); +} + +static int qnap_mcu_register_err_led(struct device *dev, struct qnap_mcu *mcu, int num_err_led) +{ + struct qnap_mcu_err_led *err_led; + int ret; + + err_led = devm_kzalloc(dev, sizeof(*err_led), GFP_KERNEL); + if (!err_led) + return -ENOMEM; + + err_led->mcu = mcu; + err_led->num = num_err_led; + err_led->mode = QNAP_MCU_ERR_LED_OFF; + + scnprintf(err_led->name, LED_MAX_NAME_SIZE, "hdd%d:red:status", num_err_led + 1); + err_led->cdev.name = err_led->name; + + err_led->cdev.brightness_set_blocking = qnap_mcu_err_led_set; + err_led->cdev.blink_set = qnap_mcu_err_led_blink_set; + err_led->cdev.brightness = 0; + err_led->cdev.max_brightness = 1; + + ret = devm_led_classdev_register(dev, &err_led->cdev); + if (ret) + return ret; + + return qnap_mcu_err_led_set(&err_led->cdev, 0); +} + +enum qnap_mcu_usb_led_mode { + QNAP_MCU_USB_LED_ON = 1, + QNAP_MCU_USB_LED_OFF = 3, + QNAP_MCU_USB_LED_BLINK = 2, +}; + +struct qnap_mcu_usb_led { + struct qnap_mcu *mcu; + struct led_classdev cdev; + u8 mode; +}; + +static inline struct qnap_mcu_usb_led * + cdev_to_qnap_mcu_usb_led(struct led_classdev *led_cdev) +{ + return container_of(led_cdev, struct qnap_mcu_usb_led, cdev); +} + +static int qnap_mcu_usb_led_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + struct qnap_mcu_usb_led *usb_led = cdev_to_qnap_mcu_usb_led(led_cdev); + u8 cmd[] = { '@', 'C', 0 }; + + /* Don't disturb a possible set blink-mode if LED stays on */ + if (brightness != 0 && usb_led->mode == QNAP_MCU_USB_LED_BLINK) + return 0; + + usb_led->mode = brightness ? QNAP_MCU_USB_LED_ON : QNAP_MCU_USB_LED_OFF; + + /* + * Byte 3 is shared between the usb led target on/off/blink + * and also the buzzer control (in the input driver) + */ + cmd[2] = 'D' + usb_led->mode; + + return qnap_mcu_exec_with_ack(usb_led->mcu, cmd, sizeof(cmd)); +} + +static int qnap_mcu_usb_led_blink_set(struct led_classdev *led_cdev, + unsigned long *delay_on, + unsigned long *delay_off) +{ + struct qnap_mcu_usb_led *usb_led = cdev_to_qnap_mcu_usb_led(led_cdev); + u8 cmd[] = { '@', 'C', 0 }; + + /* LED is off, nothing to do */ + if (usb_led->mode == QNAP_MCU_USB_LED_OFF) + return 0; + + *delay_on = 250; + *delay_off = 250; + usb_led->mode = QNAP_MCU_USB_LED_BLINK; + + /* + * Byte 3 is shared between the USB LED target on/off/blink + * and also the buzzer control (in the input driver) + */ + cmd[2] = 'D' + usb_led->mode; + + return qnap_mcu_exec_with_ack(usb_led->mcu, cmd, sizeof(cmd)); +} + +static int qnap_mcu_register_usb_led(struct device *dev, struct qnap_mcu *mcu) +{ + struct qnap_mcu_usb_led *usb_led; + int ret; + + usb_led = devm_kzalloc(dev, sizeof(*usb_led), GFP_KERNEL); + if (!usb_led) + return -ENOMEM; + + usb_led->mcu = mcu; + usb_led->mode = QNAP_MCU_USB_LED_OFF; + usb_led->cdev.name = "usb:blue:disk"; + usb_led->cdev.brightness_set_blocking = qnap_mcu_usb_led_set; + usb_led->cdev.blink_set = qnap_mcu_usb_led_blink_set; + usb_led->cdev.brightness = 0; + usb_led->cdev.max_brightness = 1; + + ret = devm_led_classdev_register(dev, &usb_led->cdev); + if (ret) + return ret; + + return qnap_mcu_usb_led_set(&usb_led->cdev, 0); +} + +static int qnap_mcu_leds_probe(struct platform_device *pdev) +{ + struct qnap_mcu *mcu = dev_get_drvdata(pdev->dev.parent); + const struct qnap_mcu_variant *variant = pdev->dev.platform_data; + int ret; + + for (int i = 0; i < variant->num_drives; i++) { + ret = qnap_mcu_register_err_led(&pdev->dev, mcu, i); + if (ret) + return dev_err_probe(&pdev->dev, ret, + "failed to register error LED %d\n", i); + } + + if (variant->usb_led) { + ret = qnap_mcu_register_usb_led(&pdev->dev, mcu); + if (ret) + return dev_err_probe(&pdev->dev, ret, + "failed to register USB LED\n"); + } + + return 0; +} + +static struct platform_driver qnap_mcu_leds_driver = { + .probe = qnap_mcu_leds_probe, + .driver = { + .name = "qnap-mcu-leds", + }, +}; +module_platform_driver(qnap_mcu_leds_driver); + +MODULE_ALIAS("platform:qnap-mcu-leds"); +MODULE_AUTHOR("Heiko Stuebner "); +MODULE_DESCRIPTION("QNAP MCU LEDs driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/mfd/Kconfig b/drivers/mfd/Kconfig index ae23b317a64e..6b0682af6e32 100644 --- a/drivers/mfd/Kconfig +++ b/drivers/mfd/Kconfig @@ -2386,6 +2386,19 @@ config MFD_INTEL_M10_BMC_PMCI additional drivers must be enabled in order to use the functionality of the device. +config MFD_QNAP_MCU + tristate "QNAP microcontroller unit core driver" + depends on SERIAL_DEV_BUS + select MFD_CORE + help + Select this to get support for the QNAP MCU device found in + several devices of QNAP network attached storage products that + implements additional functionality for the device, like fan + and LED control. + + This driver implements the base serial protocol to talk to the + device and provides functions for the other parts to hook into. + config MFD_RSMU_I2C tristate "Renesas Synchronization Management Unit with I2C" depends on I2C && OF @@ -2414,5 +2427,17 @@ config MFD_RSMU_SPI Additional drivers must be enabled in order to use the functionality of the device. +config MFD_UPBOARD_FPGA + tristate "Support for the AAeon UP board FPGA" + depends on (X86 && ACPI) + select MFD_CORE + help + Select this option to enable the AAEON UP and UP^2 onboard FPGA. + This is the core driver of this FPGA, which has a pin controller and a + LED controller. + + To compile this driver as a module, choose M here: the module will be + called upboard-fpga. + endmenu endif diff --git a/drivers/mfd/Makefile b/drivers/mfd/Makefile index e057d6d6faef..9220eaf7cf12 100644 --- a/drivers/mfd/Makefile +++ b/drivers/mfd/Makefile @@ -288,5 +288,9 @@ obj-$(CONFIG_MFD_INTEL_M10_BMC_PMCI) += intel-m10-bmc-pmci.o obj-$(CONFIG_MFD_ATC260X) += atc260x-core.o obj-$(CONFIG_MFD_ATC260X_I2C) += atc260x-i2c.o +obj-$(CONFIG_MFD_QNAP_MCU) += qnap-mcu.o + obj-$(CONFIG_MFD_RSMU_I2C) += rsmu_i2c.o rsmu_core.o obj-$(CONFIG_MFD_RSMU_SPI) += rsmu_spi.o rsmu_core.o + +obj-$(CONFIG_MFD_UPBOARD_FPGA) += upboard-fpga.o diff --git a/drivers/mfd/axp20x.c b/drivers/mfd/axp20x.c index 251465a656d0..9d6634612928 100644 --- a/drivers/mfd/axp20x.c +++ b/drivers/mfd/axp20x.c @@ -1455,10 +1455,7 @@ int axp20x_device_probe(struct axp20x_dev *axp20x) } if (axp20x->variant != AXP288_ID) - devm_register_sys_off_handler(axp20x->dev, - SYS_OFF_MODE_POWER_OFF, - SYS_OFF_PRIO_DEFAULT, - axp20x_power_off, axp20x); + devm_register_power_off_handler(axp20x->dev, axp20x_power_off, axp20x); dev_info(axp20x->dev, "AXP20X driver loaded\n"); diff --git a/drivers/mfd/cs42l43-i2c.c b/drivers/mfd/cs42l43-i2c.c index f0ad4002652d..a2ab001a600a 100644 --- a/drivers/mfd/cs42l43-i2c.c +++ b/drivers/mfd/cs42l43-i2c.c @@ -56,13 +56,6 @@ static int cs42l43_i2c_probe(struct i2c_client *i2c) return cs42l43_dev_probe(cs42l43); } -static void cs42l43_i2c_remove(struct i2c_client *i2c) -{ - struct cs42l43 *cs42l43 = dev_get_drvdata(&i2c->dev); - - cs42l43_dev_remove(cs42l43); -} - #if IS_ENABLED(CONFIG_OF) static const struct of_device_id cs42l43_of_match[] = { { .compatible = "cirrus,cs42l43", }, @@ -88,7 +81,6 @@ static struct i2c_driver cs42l43_i2c_driver = { }, .probe = cs42l43_i2c_probe, - .remove = cs42l43_i2c_remove, }; module_i2c_driver(cs42l43_i2c_driver); diff --git a/drivers/mfd/cs42l43-sdw.c b/drivers/mfd/cs42l43-sdw.c index 3938d48039c4..023f7e1a30f8 100644 --- a/drivers/mfd/cs42l43-sdw.c +++ b/drivers/mfd/cs42l43-sdw.c @@ -187,15 +187,6 @@ static int cs42l43_sdw_probe(struct sdw_slave *sdw, const struct sdw_device_id * return cs42l43_dev_probe(cs42l43); } -static int cs42l43_sdw_remove(struct sdw_slave *sdw) -{ - struct cs42l43 *cs42l43 = dev_get_drvdata(&sdw->dev); - - cs42l43_dev_remove(cs42l43); - - return 0; -} - static const struct sdw_device_id cs42l43_sdw_id[] = { SDW_SLAVE_ENTRY(0x01FA, 0x4243, 0), {} @@ -209,7 +200,6 @@ static struct sdw_driver cs42l43_sdw_driver = { }, .probe = cs42l43_sdw_probe, - .remove = cs42l43_sdw_remove, .id_table = cs42l43_sdw_id, .ops = &cs42l43_sdw_ops, }; diff --git a/drivers/mfd/cs42l43.c b/drivers/mfd/cs42l43.c index b5ab5e613db7..103787f37443 100644 --- a/drivers/mfd/cs42l43.c +++ b/drivers/mfd/cs42l43.c @@ -29,7 +29,7 @@ #define CS42L43_RESET_DELAY_MS 20 -#define CS42L43_SDW_ATTACH_TIMEOUT_MS 500 +#define CS42L43_SDW_ATTACH_TIMEOUT_MS 5000 #define CS42L43_SDW_DETACH_TIMEOUT_MS 100 #define CS42L43_MCU_BOOT_STAGE1 1 @@ -48,6 +48,7 @@ #define CS42L43_MCU_SUPPORTED_REV 0x2105 #define CS42L43_MCU_SHADOW_REGS_REQUIRED_REV 0x2200 +#define CS42L43_BIOS_SHADOW_REGS_REQUIRED_REV 0x1002 #define CS42L43_MCU_SUPPORTED_BIOS_REV 0x0001 #define CS42L43_VDDP_DELAY_US 50 @@ -773,7 +774,8 @@ static int cs42l43_mcu_update_step(struct cs42l43 *cs42l43) * Later versions of the firmwware require the driver to access some * features through a set of shadow registers. */ - shadow = mcu_rev >= CS42L43_MCU_SHADOW_REGS_REQUIRED_REV; + shadow = (mcu_rev >= CS42L43_MCU_SHADOW_REGS_REQUIRED_REV) || + (bios_rev >= CS42L43_BIOS_SHADOW_REGS_REQUIRED_REV); ret = regmap_read(cs42l43->regmap, CS42L43_BOOT_CONTROL, &secure_cfg); if (ret) { @@ -982,7 +984,7 @@ static int cs42l43_power_up(struct cs42l43 *cs42l43) /* vdd-p must be on for 50uS before any other supply */ usleep_range(CS42L43_VDDP_DELAY_US, 2 * CS42L43_VDDP_DELAY_US); - gpiod_set_value_cansleep(cs42l43->reset, 1); + gpiod_set_raw_value_cansleep(cs42l43->reset, 1); ret = regulator_bulk_enable(CS42L43_N_SUPPLIES, cs42l43->core_supplies); if (ret) { @@ -1003,7 +1005,7 @@ static int cs42l43_power_up(struct cs42l43 *cs42l43) err_core_supplies: regulator_bulk_disable(CS42L43_N_SUPPLIES, cs42l43->core_supplies); err_reset: - gpiod_set_value_cansleep(cs42l43->reset, 0); + gpiod_set_raw_value_cansleep(cs42l43->reset, 0); regulator_disable(cs42l43->vdd_p); return ret; @@ -1025,7 +1027,7 @@ static int cs42l43_power_down(struct cs42l43 *cs42l43) return ret; } - gpiod_set_value_cansleep(cs42l43->reset, 0); + gpiod_set_raw_value_cansleep(cs42l43->reset, 0); ret = regulator_disable(cs42l43->vdd_p); if (ret) { @@ -1036,6 +1038,15 @@ static int cs42l43_power_down(struct cs42l43 *cs42l43) return 0; } +static void cs42l43_dev_remove(void *data) +{ + struct cs42l43 *cs42l43 = data; + + cancel_work_sync(&cs42l43->boot_work); + + cs42l43_power_down(cs42l43); +} + int cs42l43_dev_probe(struct cs42l43 *cs42l43) { int i, ret; @@ -1050,11 +1061,13 @@ int cs42l43_dev_probe(struct cs42l43 *cs42l43) regcache_cache_only(cs42l43->regmap, true); - cs42l43->reset = devm_gpiod_get_optional(cs42l43->dev, "reset", GPIOD_OUT_LOW); + cs42l43->reset = devm_gpiod_get_optional(cs42l43->dev, "reset", GPIOD_OUT_HIGH); if (IS_ERR(cs42l43->reset)) return dev_err_probe(cs42l43->dev, PTR_ERR(cs42l43->reset), "Failed to get reset\n"); + gpiod_set_raw_value_cansleep(cs42l43->reset, 0); + cs42l43->vdd_p = devm_regulator_get(cs42l43->dev, "vdd-p"); if (IS_ERR(cs42l43->vdd_p)) return dev_err_probe(cs42l43->dev, PTR_ERR(cs42l43->vdd_p), @@ -1080,6 +1093,10 @@ int cs42l43_dev_probe(struct cs42l43 *cs42l43) if (ret) return ret; + ret = devm_add_action_or_reset(cs42l43->dev, cs42l43_dev_remove, cs42l43); + if (ret) + return ret; + pm_runtime_set_autosuspend_delay(cs42l43->dev, CS42L43_AUTOSUSPEND_TIME_MS); pm_runtime_use_autosuspend(cs42l43->dev); pm_runtime_set_active(cs42l43->dev); @@ -1098,14 +1115,6 @@ int cs42l43_dev_probe(struct cs42l43 *cs42l43) } EXPORT_SYMBOL_NS_GPL(cs42l43_dev_probe, "MFD_CS42L43"); -void cs42l43_dev_remove(struct cs42l43 *cs42l43) -{ - cancel_work_sync(&cs42l43->boot_work); - - cs42l43_power_down(cs42l43); -} -EXPORT_SYMBOL_NS_GPL(cs42l43_dev_remove, "MFD_CS42L43"); - static int cs42l43_suspend(struct device *dev) { struct cs42l43 *cs42l43 = dev_get_drvdata(dev); diff --git a/drivers/mfd/cs42l43.h b/drivers/mfd/cs42l43.h index 8d1b1b0f5a47..f3da783930f5 100644 --- a/drivers/mfd/cs42l43.h +++ b/drivers/mfd/cs42l43.h @@ -25,6 +25,5 @@ bool cs42l43_precious_register(struct device *dev, unsigned int reg); bool cs42l43_volatile_register(struct device *dev, unsigned int reg); int cs42l43_dev_probe(struct cs42l43 *cs42l43); -void cs42l43_dev_remove(struct cs42l43 *cs42l43); #endif /* CS42L43_CORE_INT_H */ diff --git a/drivers/mfd/da9052-core.c b/drivers/mfd/da9052-core.c index dc85801b9fa0..b06cd518413b 100644 --- a/drivers/mfd/da9052-core.c +++ b/drivers/mfd/da9052-core.c @@ -585,6 +585,7 @@ static int da9052_clear_fault_log(struct da9052 *da9052) "Cannot reset FAULT_LOG values %d\n", ret); } + da9052->fault_log = fault_log; return ret; } diff --git a/drivers/mfd/intel_soc_pmic_chtdc_ti.c b/drivers/mfd/intel_soc_pmic_chtdc_ti.c index 992855bfda3e..8582ae65a802 100644 --- a/drivers/mfd/intel_soc_pmic_chtdc_ti.c +++ b/drivers/mfd/intel_soc_pmic_chtdc_ti.c @@ -81,7 +81,7 @@ static struct mfd_cell chtdc_ti_dev[] = { static const struct regmap_config chtdc_ti_regmap_config = { .reg_bits = 8, .val_bits = 8, - .max_register = 128, + .max_register = 0xff, .cache_type = REGCACHE_NONE, }; diff --git a/drivers/mfd/lpc_ich.c b/drivers/mfd/lpc_ich.c index f14901660147..4b7d0cb9340f 100644 --- a/drivers/mfd/lpc_ich.c +++ b/drivers/mfd/lpc_ich.c @@ -834,8 +834,9 @@ static const struct pci_device_id lpc_ich_ids[] = { { PCI_VDEVICE(INTEL, 0x2917), LPC_ICH9ME}, { PCI_VDEVICE(INTEL, 0x2918), LPC_ICH9}, { PCI_VDEVICE(INTEL, 0x2919), LPC_ICH9M}, - { PCI_VDEVICE(INTEL, 0x3197), LPC_GLK}, { PCI_VDEVICE(INTEL, 0x2b9c), LPC_COUGARMOUNTAIN}, + { PCI_VDEVICE(INTEL, 0x3197), LPC_GLK}, + { PCI_VDEVICE(INTEL, 0x31e8), LPC_GLK}, { PCI_VDEVICE(INTEL, 0x3a14), LPC_ICH10DO}, { PCI_VDEVICE(INTEL, 0x3a16), LPC_ICH10R}, { PCI_VDEVICE(INTEL, 0x3a18), LPC_ICH10}, diff --git a/drivers/mfd/qnap-mcu.c b/drivers/mfd/qnap-mcu.c new file mode 100644 index 000000000000..4be39d8b2905 --- /dev/null +++ b/drivers/mfd/qnap-mcu.c @@ -0,0 +1,338 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Core driver for the microcontroller unit in QNAP NAS devices that is + * connected via a dedicated UART port. + * + * Copyright (C) 2024 Heiko Stuebner + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* The longest command found so far is 5 bytes long */ +#define QNAP_MCU_MAX_CMD_SIZE 5 +#define QNAP_MCU_MAX_DATA_SIZE 36 +#define QNAP_MCU_CHECKSUM_SIZE 1 + +#define QNAP_MCU_RX_BUFFER_SIZE \ + (QNAP_MCU_MAX_DATA_SIZE + QNAP_MCU_CHECKSUM_SIZE) + +#define QNAP_MCU_TX_BUFFER_SIZE \ + (QNAP_MCU_MAX_CMD_SIZE + QNAP_MCU_CHECKSUM_SIZE) + +#define QNAP_MCU_ACK_LEN 2 +#define QNAP_MCU_VERSION_LEN 4 + +#define QNAP_MCU_TIMEOUT_MS 500 + +/** + * struct qnap_mcu_reply - Reply to a command + * + * @data: Buffer to store reply payload in + * @length: Expected reply length, including the checksum + * @received: Received number of bytes, so far + * @done: Triggered when the entire reply has been received + */ +struct qnap_mcu_reply { + u8 *data; + size_t length; + size_t received; + struct completion done; +}; + +/** + * struct qnap_mcu - QNAP NAS embedded controller + * + * @serdev: Pointer to underlying serdev + * @bus_lock: Lock to serialize access to the device + * @reply: Reply data structure + * @variant: Device variant specific information + * @version: MCU firmware version + */ +struct qnap_mcu { + struct serdev_device *serdev; + struct mutex bus_lock; + struct qnap_mcu_reply reply; + const struct qnap_mcu_variant *variant; + u8 version[QNAP_MCU_VERSION_LEN]; +}; + +/* + * The QNAP-MCU uses a basic XOR checksum. + * It is always the last byte and XORs the whole previous message. + */ +static u8 qnap_mcu_csum(const u8 *buf, size_t size) +{ + u8 csum = 0; + + while (size--) + csum ^= *buf++; + + return csum; +} + +static int qnap_mcu_write(struct qnap_mcu *mcu, const u8 *data, u8 data_size) +{ + unsigned char tx[QNAP_MCU_TX_BUFFER_SIZE]; + size_t length = data_size + QNAP_MCU_CHECKSUM_SIZE; + + if (length > sizeof(tx)) { + dev_err(&mcu->serdev->dev, "data too big for transmit buffer"); + return -EINVAL; + } + + memcpy(tx, data, data_size); + tx[data_size] = qnap_mcu_csum(data, data_size); + + serdev_device_write_flush(mcu->serdev); + + return serdev_device_write(mcu->serdev, tx, length, HZ); +} + +static size_t qnap_mcu_receive_buf(struct serdev_device *serdev, const u8 *buf, size_t size) +{ + struct device *dev = &serdev->dev; + struct qnap_mcu *mcu = dev_get_drvdata(dev); + struct qnap_mcu_reply *reply = &mcu->reply; + const u8 *src = buf; + const u8 *end = buf + size; + + if (!reply->length) { + dev_warn(dev, "Received %zu bytes, we were not waiting for\n", size); + return size; + } + + while (src < end) { + reply->data[reply->received] = *src++; + reply->received++; + + if (reply->received == reply->length) { + /* We don't expect any characters from the device now */ + reply->length = 0; + + complete(&reply->done); + + /* + * We report the consumed number of bytes. If there + * are still bytes remaining (though there shouldn't) + * the serdev layer will re-execute this handler with + * the remainder of the Rx bytes. + */ + return src - buf; + } + } + + /* + * The only way to get out of the above loop and end up here + * is through consuming all of the supplied data, so here we + * report that we processed it all. + */ + return size; +} + +static const struct serdev_device_ops qnap_mcu_serdev_device_ops = { + .receive_buf = qnap_mcu_receive_buf, + .write_wakeup = serdev_device_write_wakeup, +}; + +int qnap_mcu_exec(struct qnap_mcu *mcu, + const u8 *cmd_data, size_t cmd_data_size, + u8 *reply_data, size_t reply_data_size) +{ + unsigned char rx[QNAP_MCU_RX_BUFFER_SIZE]; + size_t length = reply_data_size + QNAP_MCU_CHECKSUM_SIZE; + struct qnap_mcu_reply *reply = &mcu->reply; + int ret = 0; + + if (length > sizeof(rx)) { + dev_err(&mcu->serdev->dev, "expected data too big for receive buffer"); + return -EINVAL; + } + + mutex_lock(&mcu->bus_lock); + + reply->data = rx, + reply->length = length, + reply->received = 0, + reinit_completion(&reply->done); + + qnap_mcu_write(mcu, cmd_data, cmd_data_size); + + serdev_device_wait_until_sent(mcu->serdev, msecs_to_jiffies(QNAP_MCU_TIMEOUT_MS)); + + if (!wait_for_completion_timeout(&reply->done, msecs_to_jiffies(QNAP_MCU_TIMEOUT_MS))) { + dev_err(&mcu->serdev->dev, "Command timeout\n"); + ret = -ETIMEDOUT; + } else { + u8 crc = qnap_mcu_csum(rx, reply_data_size); + + if (crc != rx[reply_data_size]) { + dev_err(&mcu->serdev->dev, + "Invalid Checksum received\n"); + ret = -EIO; + } else { + memcpy(reply_data, rx, reply_data_size); + } + } + + mutex_unlock(&mcu->bus_lock); + return ret; +} +EXPORT_SYMBOL_GPL(qnap_mcu_exec); + +int qnap_mcu_exec_with_ack(struct qnap_mcu *mcu, + const u8 *cmd_data, size_t cmd_data_size) +{ + u8 ack[QNAP_MCU_ACK_LEN]; + int ret; + + ret = qnap_mcu_exec(mcu, cmd_data, cmd_data_size, ack, sizeof(ack)); + if (ret) + return ret; + + /* Should return @0 */ + if (ack[0] != '@' || ack[1] != '0') { + dev_err(&mcu->serdev->dev, "Did not receive ack\n"); + return -EIO; + } + + return 0; +} +EXPORT_SYMBOL_GPL(qnap_mcu_exec_with_ack); + +static int qnap_mcu_get_version(struct qnap_mcu *mcu) +{ + const u8 cmd[] = { '%', 'V' }; + u8 rx[14]; + int ret; + + /* Reply is the 2 command-bytes + 4 bytes describing the version */ + ret = qnap_mcu_exec(mcu, cmd, sizeof(cmd), rx, QNAP_MCU_VERSION_LEN + 2); + if (ret) + return ret; + + memcpy(mcu->version, &rx[2], QNAP_MCU_VERSION_LEN); + + return 0; +} + +/* + * The MCU controls power to the peripherals but not the CPU. + * + * So using the PMIC to power off the system keeps the MCU and hard-drives + * running. This also then prevents the system from turning back on until + * the MCU is turned off by unplugging the power cable. + * Turning off the MCU alone on the other hand turns off the hard drives, + * LEDs, etc while the main SoC stays running - including its network ports. + */ +static int qnap_mcu_power_off(struct sys_off_data *data) +{ + const u8 cmd[] = { '@', 'C', '0' }; + struct qnap_mcu *mcu = data->cb_data; + int ret; + + ret = qnap_mcu_exec_with_ack(mcu, cmd, sizeof(cmd)); + if (ret) { + dev_err(&mcu->serdev->dev, "MCU poweroff failed %d\n", ret); + return NOTIFY_STOP; + } + + return NOTIFY_DONE; +} + +static const struct qnap_mcu_variant qnap_ts433_mcu = { + .baud_rate = 115200, + .num_drives = 4, + .fan_pwm_min = 51, /* Specified in original model.conf */ + .fan_pwm_max = 255, + .usb_led = true, +}; + +static struct mfd_cell qnap_mcu_cells[] = { + { .name = "qnap-mcu-input", }, + { .name = "qnap-mcu-leds", }, + { .name = "qnap-mcu-hwmon", } +}; + +static int qnap_mcu_probe(struct serdev_device *serdev) +{ + struct device *dev = &serdev->dev; + struct qnap_mcu *mcu; + int ret; + + mcu = devm_kzalloc(dev, sizeof(*mcu), GFP_KERNEL); + if (!mcu) + return -ENOMEM; + + mcu->serdev = serdev; + dev_set_drvdata(dev, mcu); + + mcu->variant = of_device_get_match_data(dev); + if (!mcu->variant) + return -ENODEV; + + mutex_init(&mcu->bus_lock); + init_completion(&mcu->reply.done); + + serdev_device_set_client_ops(serdev, &qnap_mcu_serdev_device_ops); + ret = devm_serdev_device_open(dev, serdev); + if (ret) + return ret; + + serdev_device_set_baudrate(serdev, mcu->variant->baud_rate); + serdev_device_set_flow_control(serdev, false); + + ret = serdev_device_set_parity(serdev, SERDEV_PARITY_NONE); + if (ret) + return dev_err_probe(dev, ret, "Failed to set parity\n"); + + ret = qnap_mcu_get_version(mcu); + if (ret) + return ret; + + ret = devm_register_sys_off_handler(dev, + SYS_OFF_MODE_POWER_OFF_PREPARE, + SYS_OFF_PRIO_DEFAULT, + &qnap_mcu_power_off, mcu); + if (ret) + return dev_err_probe(dev, ret, + "Failed to register poweroff handler\n"); + + for (int i = 0; i < ARRAY_SIZE(qnap_mcu_cells); i++) { + qnap_mcu_cells[i].platform_data = mcu->variant; + qnap_mcu_cells[i].pdata_size = sizeof(*mcu->variant); + } + + ret = devm_mfd_add_devices(dev, PLATFORM_DEVID_AUTO, qnap_mcu_cells, + ARRAY_SIZE(qnap_mcu_cells), NULL, 0, NULL); + if (ret) + return dev_err_probe(dev, ret, "Failed to add child devices\n"); + + return 0; +} + +static const struct of_device_id qnap_mcu_dt_ids[] = { + { .compatible = "qnap,ts433-mcu", .data = &qnap_ts433_mcu }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(of, qnap_mcu_dt_ids); + +static struct serdev_device_driver qnap_mcu_drv = { + .probe = qnap_mcu_probe, + .driver = { + .name = "qnap-mcu", + .of_match_table = qnap_mcu_dt_ids, + }, +}; +module_serdev_device_driver(qnap_mcu_drv); + +MODULE_AUTHOR("Heiko Stuebner "); +MODULE_DESCRIPTION("QNAP MCU core driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/mfd/stpmic1.c b/drivers/mfd/stpmic1.c index d8a603d95aa6..081827bc0596 100644 --- a/drivers/mfd/stpmic1.c +++ b/drivers/mfd/stpmic1.c @@ -170,11 +170,7 @@ static int stpmic1_probe(struct i2c_client *i2c) return ret; } - ret = devm_register_sys_off_handler(ddata->dev, - SYS_OFF_MODE_POWER_OFF, - SYS_OFF_PRIO_DEFAULT, - stpmic1_power_off, - ddata); + ret = devm_register_power_off_handler(ddata->dev, stpmic1_power_off, ddata); if (ret) { dev_err(ddata->dev, "failed to register sys-off handler: %d\n", ret); return ret; diff --git a/drivers/mfd/upboard-fpga.c b/drivers/mfd/upboard-fpga.c new file mode 100644 index 000000000000..5a330e2f2229 --- /dev/null +++ b/drivers/mfd/upboard-fpga.c @@ -0,0 +1,325 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * UP Board FPGA driver. + * + * FPGA provides more GPIO driving power, LEDS and pin mux function. + * + * Copyright (c) AAEON. All rights reserved. + * Copyright (C) 2024 Bootlin + * + * Author: Gary Wang + * Author: Thomas Richard + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define UPBOARD_AAEON_MANUFACTURER_ID 0x01 +#define UPBOARD_MANUFACTURER_ID_MASK GENMASK(7, 0) + +#define UPBOARD_ADDRESS_SIZE 7 +#define UPBOARD_REGISTER_SIZE 16 + +#define UPBOARD_READ_FLAG BIT(UPBOARD_ADDRESS_SIZE) + +#define UPBOARD_FW_ID_MAJOR_SUPPORTED 0x0 + +#define UPBOARD_FW_ID_BUILD_MASK GENMASK(15, 12) +#define UPBOARD_FW_ID_MAJOR_MASK GENMASK(11, 8) +#define UPBOARD_FW_ID_MINOR_MASK GENMASK(7, 4) +#define UPBOARD_FW_ID_PATCH_MASK GENMASK(3, 0) + +static int upboard_fpga_read(void *context, unsigned int reg, unsigned int *val) +{ + struct upboard_fpga *fpga = context; + int i; + + /* Clear to start new transaction */ + gpiod_set_value(fpga->clear_gpio, 0); + gpiod_set_value(fpga->clear_gpio, 1); + + reg |= UPBOARD_READ_FLAG; + + /* Send clock and addr from strobe & datain pins */ + for (i = UPBOARD_ADDRESS_SIZE; i >= 0; i--) { + gpiod_set_value(fpga->strobe_gpio, 0); + gpiod_set_value(fpga->datain_gpio, !!(reg & BIT(i))); + gpiod_set_value(fpga->strobe_gpio, 1); + } + + gpiod_set_value(fpga->strobe_gpio, 0); + *val = 0; + + /* Read data from dataout pin */ + for (i = UPBOARD_REGISTER_SIZE - 1; i >= 0; i--) { + gpiod_set_value(fpga->strobe_gpio, 1); + gpiod_set_value(fpga->strobe_gpio, 0); + *val |= gpiod_get_value(fpga->dataout_gpio) << i; + } + + gpiod_set_value(fpga->strobe_gpio, 1); + + return 0; +} + +static int upboard_fpga_write(void *context, unsigned int reg, unsigned int val) +{ + struct upboard_fpga *fpga = context; + int i; + + /* Clear to start new transcation */ + gpiod_set_value(fpga->clear_gpio, 0); + gpiod_set_value(fpga->clear_gpio, 1); + + /* Send clock and addr from strobe & datain pins */ + for (i = UPBOARD_ADDRESS_SIZE; i >= 0; i--) { + gpiod_set_value(fpga->strobe_gpio, 0); + gpiod_set_value(fpga->datain_gpio, !!(reg & BIT(i))); + gpiod_set_value(fpga->strobe_gpio, 1); + } + + gpiod_set_value(fpga->strobe_gpio, 0); + + /* Write data to datain pin */ + for (i = UPBOARD_REGISTER_SIZE - 1; i >= 0; i--) { + gpiod_set_value(fpga->datain_gpio, !!(val & BIT(i))); + gpiod_set_value(fpga->strobe_gpio, 1); + gpiod_set_value(fpga->strobe_gpio, 0); + } + + gpiod_set_value(fpga->strobe_gpio, 1); + + return 0; +} + +static const struct regmap_range upboard_up_readable_ranges[] = { + regmap_reg_range(UPBOARD_REG_PLATFORM_ID, UPBOARD_REG_FIRMWARE_ID), + regmap_reg_range(UPBOARD_REG_FUNC_EN0, UPBOARD_REG_FUNC_EN0), + regmap_reg_range(UPBOARD_REG_GPIO_EN0, UPBOARD_REG_GPIO_EN1), + regmap_reg_range(UPBOARD_REG_GPIO_DIR0, UPBOARD_REG_GPIO_DIR1), +}; + +static const struct regmap_range upboard_up_writable_ranges[] = { + regmap_reg_range(UPBOARD_REG_FUNC_EN0, UPBOARD_REG_FUNC_EN0), + regmap_reg_range(UPBOARD_REG_GPIO_EN0, UPBOARD_REG_GPIO_EN1), + regmap_reg_range(UPBOARD_REG_GPIO_DIR0, UPBOARD_REG_GPIO_DIR1), +}; + +static const struct regmap_access_table upboard_up_readable_table = { + .yes_ranges = upboard_up_readable_ranges, + .n_yes_ranges = ARRAY_SIZE(upboard_up_readable_ranges), +}; + +static const struct regmap_access_table upboard_up_writable_table = { + .yes_ranges = upboard_up_writable_ranges, + .n_yes_ranges = ARRAY_SIZE(upboard_up_writable_ranges), +}; + +static const struct regmap_config upboard_up_regmap_config = { + .reg_bits = UPBOARD_ADDRESS_SIZE, + .val_bits = UPBOARD_REGISTER_SIZE, + .max_register = UPBOARD_REG_MAX, + .reg_read = upboard_fpga_read, + .reg_write = upboard_fpga_write, + .fast_io = false, + .cache_type = REGCACHE_NONE, + .rd_table = &upboard_up_readable_table, + .wr_table = &upboard_up_writable_table, +}; + +static const struct regmap_range upboard_up2_readable_ranges[] = { + regmap_reg_range(UPBOARD_REG_PLATFORM_ID, UPBOARD_REG_FIRMWARE_ID), + regmap_reg_range(UPBOARD_REG_FUNC_EN0, UPBOARD_REG_FUNC_EN1), + regmap_reg_range(UPBOARD_REG_GPIO_EN0, UPBOARD_REG_GPIO_EN2), + regmap_reg_range(UPBOARD_REG_GPIO_DIR0, UPBOARD_REG_GPIO_DIR2), +}; + +static const struct regmap_range upboard_up2_writable_ranges[] = { + regmap_reg_range(UPBOARD_REG_FUNC_EN0, UPBOARD_REG_FUNC_EN1), + regmap_reg_range(UPBOARD_REG_GPIO_EN0, UPBOARD_REG_GPIO_EN2), + regmap_reg_range(UPBOARD_REG_GPIO_DIR0, UPBOARD_REG_GPIO_DIR2), +}; + +static const struct regmap_access_table upboard_up2_readable_table = { + .yes_ranges = upboard_up2_readable_ranges, + .n_yes_ranges = ARRAY_SIZE(upboard_up2_readable_ranges), +}; + +static const struct regmap_access_table upboard_up2_writable_table = { + .yes_ranges = upboard_up2_writable_ranges, + .n_yes_ranges = ARRAY_SIZE(upboard_up2_writable_ranges), +}; + +static const struct regmap_config upboard_up2_regmap_config = { + .reg_bits = UPBOARD_ADDRESS_SIZE, + .val_bits = UPBOARD_REGISTER_SIZE, + .max_register = UPBOARD_REG_MAX, + .reg_read = upboard_fpga_read, + .reg_write = upboard_fpga_write, + .fast_io = false, + .cache_type = REGCACHE_NONE, + .rd_table = &upboard_up2_readable_table, + .wr_table = &upboard_up2_writable_table, +}; + +static const struct mfd_cell upboard_up_mfd_cells[] = { + { .name = "upboard-pinctrl" }, + { .name = "upboard-leds" }, +}; + +static const struct upboard_fpga_data upboard_up_fpga_data = { + .type = UPBOARD_UP_FPGA, + .regmap_config = &upboard_up_regmap_config, +}; + +static const struct upboard_fpga_data upboard_up2_fpga_data = { + .type = UPBOARD_UP2_FPGA, + .regmap_config = &upboard_up2_regmap_config, +}; + +static int upboard_fpga_gpio_init(struct upboard_fpga *fpga) +{ + fpga->enable_gpio = devm_gpiod_get(fpga->dev, "enable", GPIOD_ASIS); + if (IS_ERR(fpga->enable_gpio)) + return PTR_ERR(fpga->enable_gpio); + + fpga->clear_gpio = devm_gpiod_get(fpga->dev, "clear", GPIOD_OUT_LOW); + if (IS_ERR(fpga->clear_gpio)) + return PTR_ERR(fpga->clear_gpio); + + fpga->strobe_gpio = devm_gpiod_get(fpga->dev, "strobe", GPIOD_OUT_LOW); + if (IS_ERR(fpga->strobe_gpio)) + return PTR_ERR(fpga->strobe_gpio); + + fpga->datain_gpio = devm_gpiod_get(fpga->dev, "datain", GPIOD_OUT_LOW); + if (IS_ERR(fpga->datain_gpio)) + return PTR_ERR(fpga->datain_gpio); + + fpga->dataout_gpio = devm_gpiod_get(fpga->dev, "dataout", GPIOD_IN); + if (IS_ERR(fpga->dataout_gpio)) + return PTR_ERR(fpga->dataout_gpio); + + gpiod_set_value(fpga->enable_gpio, 1); + + return 0; +} + +static int upboard_fpga_get_firmware_version(struct upboard_fpga *fpga) +{ + unsigned int platform_id, manufacturer_id; + int ret; + + if (!fpga) + return -ENOMEM; + + ret = regmap_read(fpga->regmap, UPBOARD_REG_PLATFORM_ID, &platform_id); + if (ret) + return ret; + + manufacturer_id = platform_id & UPBOARD_MANUFACTURER_ID_MASK; + if (manufacturer_id != UPBOARD_AAEON_MANUFACTURER_ID) + return dev_err_probe(fpga->dev, -ENODEV, + "driver not compatible with custom FPGA FW from manufacturer id %#02x.", + manufacturer_id); + + ret = regmap_read(fpga->regmap, UPBOARD_REG_FIRMWARE_ID, &fpga->firmware_version); + if (ret) + return ret; + + if (FIELD_GET(UPBOARD_FW_ID_MAJOR_MASK, fpga->firmware_version) != + UPBOARD_FW_ID_MAJOR_SUPPORTED) + return dev_err_probe(fpga->dev, -ENODEV, + "unsupported FPGA FW v%lu.%lu.%lu build %#02lx", + FIELD_GET(UPBOARD_FW_ID_MAJOR_MASK, fpga->firmware_version), + FIELD_GET(UPBOARD_FW_ID_MINOR_MASK, fpga->firmware_version), + FIELD_GET(UPBOARD_FW_ID_PATCH_MASK, fpga->firmware_version), + FIELD_GET(UPBOARD_FW_ID_BUILD_MASK, fpga->firmware_version)); + return 0; +} + +static ssize_t upboard_fpga_version_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct upboard_fpga *fpga = dev_get_drvdata(dev); + + return sysfs_emit(buf, "FPGA FW v%lu.%lu.%lu build %#02lx\n", + FIELD_GET(UPBOARD_FW_ID_MAJOR_MASK, fpga->firmware_version), + FIELD_GET(UPBOARD_FW_ID_MINOR_MASK, fpga->firmware_version), + FIELD_GET(UPBOARD_FW_ID_PATCH_MASK, fpga->firmware_version), + FIELD_GET(UPBOARD_FW_ID_BUILD_MASK, fpga->firmware_version)); +} + +static DEVICE_ATTR_RO(upboard_fpga_version); + +static struct attribute *upboard_fpga_attrs[] = { + &dev_attr_upboard_fpga_version.attr, + NULL +}; + +ATTRIBUTE_GROUPS(upboard_fpga); + +static int upboard_fpga_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct upboard_fpga *fpga; + int ret; + + fpga = devm_kzalloc(dev, sizeof(*fpga), GFP_KERNEL); + if (!fpga) + return -ENOMEM; + + fpga->fpga_data = device_get_match_data(dev); + + fpga->dev = dev; + + platform_set_drvdata(pdev, fpga); + + fpga->regmap = devm_regmap_init(dev, NULL, fpga, fpga->fpga_data->regmap_config); + if (IS_ERR(fpga->regmap)) + return PTR_ERR(fpga->regmap); + + ret = upboard_fpga_gpio_init(fpga); + if (ret) + return dev_err_probe(dev, ret, "Failed to initialize FPGA common GPIOs"); + + ret = upboard_fpga_get_firmware_version(fpga); + if (ret) + return ret; + + return devm_mfd_add_devices(dev, PLATFORM_DEVID_NONE, upboard_up_mfd_cells, + ARRAY_SIZE(upboard_up_mfd_cells), NULL, 0, NULL); +} + +static const struct acpi_device_id upboard_fpga_acpi_match[] = { + { "AANT0F01", (kernel_ulong_t)&upboard_up2_fpga_data }, + { "AANT0F04", (kernel_ulong_t)&upboard_up_fpga_data }, + {} +}; +MODULE_DEVICE_TABLE(acpi, upboard_fpga_acpi_match); + +static struct platform_driver upboard_fpga_driver = { + .driver = { + .name = "upboard-fpga", + .acpi_match_table = ACPI_PTR(upboard_fpga_acpi_match), + .dev_groups = upboard_fpga_groups, + }, + .probe = upboard_fpga_probe, +}; + +module_platform_driver(upboard_fpga_driver); + +MODULE_AUTHOR("Gary Wang "); +MODULE_AUTHOR("Thomas Richard "); +MODULE_DESCRIPTION("UP Board FPGA driver"); +MODULE_LICENSE("GPL"); diff --git a/include/linux/mfd/core.h b/include/linux/mfd/core.h index e8bcad641d8c..faeea7abd688 100644 --- a/include/linux/mfd/core.h +++ b/include/linux/mfd/core.h @@ -72,7 +72,7 @@ struct mfd_cell { int (*resume)(struct platform_device *dev); /* platform data passed to the sub devices drivers */ - void *platform_data; + const void *platform_data; size_t pdata_size; /* Matches ACPI */ diff --git a/include/linux/mfd/da9052/da9052.h b/include/linux/mfd/da9052/da9052.h index 76feb3a7066d..9cb2fc2938ce 100644 --- a/include/linux/mfd/da9052/da9052.h +++ b/include/linux/mfd/da9052/da9052.h @@ -93,6 +93,8 @@ struct da9052 { int chip_irq; + int fault_log; + /* SOC I/O transfer related fixes for DA9052/53 */ int (*fix_io) (struct da9052 *da9052, unsigned char reg); }; diff --git a/include/linux/mfd/qnap-mcu.h b/include/linux/mfd/qnap-mcu.h new file mode 100644 index 000000000000..8d48c212fd44 --- /dev/null +++ b/include/linux/mfd/qnap-mcu.h @@ -0,0 +1,26 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * Core definitions for QNAP MCU MFD driver. + * Copyright (C) 2024 Heiko Stuebner + */ + +#ifndef _LINUX_QNAP_MCU_H_ +#define _LINUX_QNAP_MCU_H_ + +struct qnap_mcu; + +struct qnap_mcu_variant { + u32 baud_rate; + int num_drives; + int fan_pwm_min; + int fan_pwm_max; + bool usb_led; +}; + +int qnap_mcu_exec(struct qnap_mcu *mcu, + const u8 *cmd_data, size_t cmd_data_size, + u8 *reply_data, size_t reply_data_size); +int qnap_mcu_exec_with_ack(struct qnap_mcu *mcu, + const u8 *cmd_data, size_t cmd_data_size); + +#endif /* _LINUX_QNAP_MCU_H_ */ diff --git a/include/linux/mfd/upboard-fpga.h b/include/linux/mfd/upboard-fpga.h new file mode 100644 index 000000000000..12231e40f5da --- /dev/null +++ b/include/linux/mfd/upboard-fpga.h @@ -0,0 +1,55 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * UP Board CPLD/FPGA driver + * + * Copyright (c) AAEON. All rights reserved. + * Copyright (C) 2024 Bootlin + * + * Author: Gary Wang + * Author: Thomas Richard + * + */ + +#ifndef __LINUX_MFD_UPBOARD_FPGA_H +#define __LINUX_MFD_UPBOARD_FPGA_H + +#define UPBOARD_REGISTER_SIZE 16 + +enum upboard_fpgareg { + UPBOARD_REG_PLATFORM_ID = 0x10, + UPBOARD_REG_FIRMWARE_ID = 0x11, + UPBOARD_REG_FUNC_EN0 = 0x20, + UPBOARD_REG_FUNC_EN1 = 0x21, + UPBOARD_REG_GPIO_EN0 = 0x30, + UPBOARD_REG_GPIO_EN1 = 0x31, + UPBOARD_REG_GPIO_EN2 = 0x32, + UPBOARD_REG_GPIO_DIR0 = 0x40, + UPBOARD_REG_GPIO_DIR1 = 0x41, + UPBOARD_REG_GPIO_DIR2 = 0x42, + UPBOARD_REG_MAX, +}; + +enum upboard_fpga_type { + UPBOARD_UP_FPGA, + UPBOARD_UP2_FPGA, +}; + +struct upboard_fpga_data { + enum upboard_fpga_type type; + const struct regmap_config *regmap_config; +}; + +struct upboard_fpga { + struct device *dev; + struct regmap *regmap; + struct gpio_desc *enable_gpio; + struct gpio_desc *reset_gpio; + struct gpio_desc *clear_gpio; + struct gpio_desc *strobe_gpio; + struct gpio_desc *datain_gpio; + struct gpio_desc *dataout_gpio; + unsigned int firmware_version; + const struct upboard_fpga_data *fpga_data; +}; + +#endif /* __LINUX_MFD_UPBOARD_FPGA_H */