mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/next/linux-next.git
synced 2024-12-28 00:32:00 +00:00
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:
commit
6e16033768
@ -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
|
||||
|
42
Documentation/devicetree/bindings/mfd/qnap,ts433-mcu.yaml
Normal file
42
Documentation/devicetree/bindings/mfd/qnap,ts433-mcu.yaml
Normal 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>;
|
||||
};
|
||||
};
|
||||
};
|
@ -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>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -202,6 +202,7 @@ Hardware Monitoring Kernel Drivers
|
||||
pxe1610
|
||||
pwm-fan
|
||||
q54sj108a2
|
||||
qnap-mcu-hwmon
|
||||
raspberrypi-hwmon
|
||||
sbrmi
|
||||
sbtsi_temp
|
||||
|
27
Documentation/hwmon/qnap-mcu-hwmon.rst
Normal file
27
Documentation/hwmon/qnap-mcu-hwmon.rst
Normal 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
|
||||
=============== ======= =======================================================
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
364
drivers/hwmon/qnap-mcu-hwmon.c
Normal file
364
drivers/hwmon/qnap-mcu-hwmon.c
Normal 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");
|
@ -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
|
||||
|
@ -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
|
||||
|
153
drivers/input/misc/qnap-mcu-input.c
Normal file
153
drivers/input/misc/qnap-mcu-input.c
Normal 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");
|
@ -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
|
||||
|
@ -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
|
||||
|
227
drivers/leds/leds-qnap-mcu.c
Normal file
227
drivers/leds/leds-qnap-mcu.c
Normal 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");
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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");
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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 */
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
|
||||
|
@ -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
338
drivers/mfd/qnap-mcu.c
Normal 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");
|
@ -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
325
drivers/mfd/upboard-fpga.c
Normal 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");
|
@ -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 */
|
||||
|
@ -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);
|
||||
};
|
||||
|
26
include/linux/mfd/qnap-mcu.h
Normal file
26
include/linux/mfd/qnap-mcu.h
Normal 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_ */
|
55
include/linux/mfd/upboard-fpga.h
Normal file
55
include/linux/mfd/upboard-fpga.h
Normal 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 */
|
Loading…
Reference in New Issue
Block a user