Merge branch 'for-mfd-next' of git://git.kernel.org/pub/scm/linux/kernel/git/lee/mfd.git

# Conflicts:
#	drivers/mfd/cs42l43.c
This commit is contained in:
Stephen Rothwell 2024-12-20 13:12:25 +11:00
commit 6e16033768
34 changed files with 1706 additions and 62 deletions

View File

@ -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

View File

@ -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 <heiko@sntech.de>
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>;
};
};
};

View File

@ -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>;
};
};
};

View File

@ -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 {

View File

@ -202,6 +202,7 @@ Hardware Monitoring Kernel Drivers
pxe1610
pwm-fan
q54sj108a2
qnap-mcu-hwmon
raspberrypi-hwmon
sbrmi
sbtsi_temp

View File

@ -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 <heiko@sntech.de>
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
=============== ======= =======================================================

View File

@ -19170,6 +19170,15 @@ L: linux-media@vger.kernel.org
S: Odd Fixes
F: drivers/media/tuners/qm1d1c0042*
QNAP MCU DRIVER
M: Heiko Stuebner <heiko@sntech.de>
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 <al@alarsen.net>
S: Maintained

View File

@ -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);
}

View File

@ -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)

View File

@ -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

View File

@ -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 <heiko@sntech.de>
*/
#include <linux/fwnode.h>
#include <linux/hwmon.h>
#include <linux/mfd/qnap-mcu.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/property.h>
#include <linux/thermal.h>
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 <heiko@sntech.de>");
MODULE_DESCRIPTION("QNAP MCU hwmon driver");
MODULE_LICENSE("GPL");

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,153 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Driver for input events on QNAP-MCUs
*
* Copyright (C) 2024 Heiko Stuebner <heiko@sntech.de>
*/
#include <linux/input.h>
#include <linux/mfd/qnap-mcu.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/slab.h>
#include <uapi/linux/input-event-codes.h>
/*
* 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 <heiko@sntech.de>");
MODULE_DESCRIPTION("QNAP MCU input driver");
MODULE_LICENSE("GPL");

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,227 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Driver for LEDs found on QNAP MCU devices
*
* Copyright (C) 2024 Heiko Stuebner <heiko@sntech.de>
*/
#include <linux/leds.h>
#include <linux/mfd/qnap-mcu.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/slab.h>
#include <uapi/linux/uleds.h>
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 <heiko@sntech.de>");
MODULE_DESCRIPTION("QNAP MCU LEDs driver");
MODULE_LICENSE("GPL");

View File

@ -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

View File

@ -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

View File

@ -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");

View File

@ -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);

View File

@ -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,
};

View File

@ -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);

View File

@ -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 */

View File

@ -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;
}

View File

@ -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,
};

View File

@ -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},

338
drivers/mfd/qnap-mcu.c Normal file
View File

@ -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 <heiko@sntech.de>
*/
#include <linux/cleanup.h>
#include <linux/export.h>
#include <linux/mfd/core.h>
#include <linux/mfd/qnap-mcu.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/reboot.h>
#include <linux/serdev.h>
#include <linux/slab.h>
/* 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 <heiko@sntech.de>");
MODULE_DESCRIPTION("QNAP MCU core driver");
MODULE_LICENSE("GPL");

View File

@ -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;

325
drivers/mfd/upboard-fpga.c Normal file
View File

@ -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 <garywang@aaeon.com.tw>
* Author: Thomas Richard <thomas.richard@bootlin.com>
*/
#include <linux/acpi.h>
#include <linux/bitfield.h>
#include <linux/device.h>
#include <linux/err.h>
#include <linux/gpio/consumer.h>
#include <linux/mfd/core.h>
#include <linux/mfd/upboard-fpga.h>
#include <linux/module.h>
#include <linux/mod_devicetable.h>
#include <linux/platform_device.h>
#include <linux/property.h>
#include <linux/regmap.h>
#include <linux/sysfs.h>
#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 <garywang@aaeon.com.tw>");
MODULE_AUTHOR("Thomas Richard <thomas.richard@bootlin.com>");
MODULE_DESCRIPTION("UP Board FPGA driver");
MODULE_LICENSE("GPL");

View File

@ -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 */

View File

@ -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);
};

View File

@ -0,0 +1,26 @@
/* SPDX-License-Identifier: GPL-2.0+ */
/*
* Core definitions for QNAP MCU MFD driver.
* Copyright (C) 2024 Heiko Stuebner <heiko@sntech.de>
*/
#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_ */

View File

@ -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 <garywang@aaeon.com.tw>
* Author: Thomas Richard <thomas.richard@bootlin.com>
*
*/
#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 */