[PATCH v6 2/7] mfd: add base driver for qnap-mcu devices

Lee Jones lee at kernel.org
Thu Aug 29 06:38:23 PDT 2024


On Sun, 25 Aug 2024, Heiko Stuebner wrote:

> These microcontroller units are used in network-attached-storage devices
> made by QNAP and provide additional functionality to the system.
> 
> This adds the base driver that implements the serial protocol via
> serdev and additionally hooks into the poweroff handlers to turn
> off the parts of the system not supplied by the general PMIC.
> 
> Turning off (at least the TSx33 devices using Rockchip SoCs) consists of
> two separate actions. Turning off the MCU alone does not turn off the main
> SoC and turning off only the SoC/PMIC does not turn off the hard-drives.
> Also if the MCU is not turned off, the system also won't start again until
> it is unplugged from power.
> 
> So on shutdown the MCU needs to be turned off separately before the
> main PMIC.
> 
> The protocol spoken by the MCU is sadly not documented, but was
> obtained by listening to the chatter on the serial port, as thankfully
> the "hal_app" program from QNAPs firmware allows triggering all/most
> MCU actions from the command line.
> 
> The implementation of how to talk to the serial device got some
> inspiration from the rave-sp servdev driver.
> 
> Signed-off-by: Heiko Stuebner <heiko at sntech.de>
> ---
>  MAINTAINERS                  |   6 +
>  drivers/mfd/Kconfig          |  13 ++
>  drivers/mfd/Makefile         |   2 +
>  drivers/mfd/qnap-mcu.c       | 339 +++++++++++++++++++++++++++++++++++
>  include/linux/mfd/qnap-mcu.h |  27 +++
>  5 files changed, 387 insertions(+)
>  create mode 100644 drivers/mfd/qnap-mcu.c
>  create mode 100644 include/linux/mfd/qnap-mcu.h
> 
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 3e7359ac005c..0fbd2d953da4 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -18654,6 +18654,12 @@ L:	linux-media at vger.kernel.org
>  S:	Odd Fixes
>  F:	drivers/media/tuners/qm1d1c0042*
>  
> +QNAP MCU DRIVER
> +M:	Heiko Stuebner <heiko at sntech.de>
> +S:	Maintained
> +F:	drivers/mfd/qnap-mcu.c
> +F:	include/linux/qnap-mcu.h
> +
>  QNX4 FILESYSTEM
>  M:	Anders Larsen <al at alarsen.net>
>  S:	Maintained
> diff --git a/drivers/mfd/Kconfig b/drivers/mfd/Kconfig
> index bc8be2e593b6..d43d2747ac4e 100644
> --- a/drivers/mfd/Kconfig
> +++ b/drivers/mfd/Kconfig
> @@ -2362,6 +2362,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 devices that
> +	  implements additional functionality for the device, like
> +	  fan- and LED control.

Is this intentional formatting or did you forget to finish the word?

> +	  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
> diff --git a/drivers/mfd/Makefile b/drivers/mfd/Makefile
> index 02b651cd7535..fc8b825725ff 100644
> --- a/drivers/mfd/Makefile
> +++ b/drivers/mfd/Makefile
> @@ -286,5 +286,7 @@ 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
> diff --git a/drivers/mfd/qnap-mcu.c b/drivers/mfd/qnap-mcu.c
> new file mode 100644
> index 000000000000..ab6036e9a843
> --- /dev/null
> +++ b/drivers/mfd/qnap-mcu.c
> @@ -0,0 +1,339 @@
> +// 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 at 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
> +
> +/**
> + * 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);
> +
> +	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);

This seems like an odd place to break when the line above is clearly longer.

Consistency is key.

> +		return size;
> +	}
> +
> +	while (src < end) {
> +		reply->data[reply->received] = *src++;
> +		reply->received++;
> +
> +		if (reply->received == reply->length) {
> +			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;
> +
> +	if (length > sizeof(rx)) {
> +		dev_err(&mcu->serdev->dev, "expected data too big for receive buffer");
> +		return -EINVAL;
> +	}
> +
> +	mutex_lock(&mcu->bus_lock);
> +
> +	/* Initialize reply fields */

This comment is superfluous / obvious.

> +	reply->data = rx,
> +	reply->length = length,
> +	reply->received = 0,
> +	reinit_completion(&reply->done);
> +
> +	qnap_mcu_write(mcu, cmd_data, cmd_data_size);
> +
> +	if (!wait_for_completion_timeout(&reply->done,
> +					 msecs_to_jiffies(500))) {

Another line-wrap weirdness - stick to 100 chars throughout and have done.


> +		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,
> +				"Checksum 0x%02x wrong for data\n", crc);

Invalid checksum received?

> +			ret = -EIO;
> +		} else {
> +			memcpy(reply_data, rx, reply_data_size);
> +			ret = 0;

Why not pre-initialise it when during declaration?

> +		}
> +	}
> +
> +	/* We don't expect any characters from the device now */
> +	reply->length = 0;
> +
> +	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] != 0x40 || ack[1] != 0x30) {
> +		dev_err(&mcu->serdev->dev, "Did not receive ack\n");
> +		return -EIO;
> +	}
> +
> +	return 0;
> +}
> +EXPORT_SYMBOL_GPL(qnap_mcu_exec_with_ack);
> +
> +const struct qnap_mcu_variant *qnap_mcu_get_variant_data(struct qnap_mcu *mcu)
> +{
> +	return mcu->variant;
> +}
> +EXPORT_SYMBOL_GPL(qnap_mcu_get_variant_data);

Why not pass this through platform data?

> +static int qnap_mcu_get_version(struct qnap_mcu *mcu)
> +{
> +	const u8 cmd[] = { 0x25, 0x56 }; /* %, 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

"PMIC"

> + * running. This also then prevents the system from turning back on until
> + * the MCU is turned off by unplugging the power-cable.

"power cable" is not hyphenated.  Neither is "hard drives".

> + * 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[] = { 0x40, 0x43, 0x30 }; /* @, 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 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 const struct mfd_cell qnap_mcu_subdevs[] = {
> +	{ .name = "qnap-mcu-input", },
> +	{ .name = "qnap-mcu-leds", },
> +	{ .name = "qnap-mcu-hwmon", }
> +};

Please swap these two structures around.

It's common to place of_device_id either just before or just after
.probe() depending on whether it's used or not.  Ooo, thinking on the
spot, it looks like qnap_mcu_dt_ids doesn't need to be up here at all.
Please place it _below_ .probe().

> +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) {
> +		dev_err(dev, "Failed to set parity\n");
> +		return ret;
> +	}
> +
> +	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");
> +
> +	ret = devm_mfd_add_devices(dev, PLATFORM_DEVID_AUTO, qnap_mcu_subdevs,
> +				   ARRAY_SIZE(qnap_mcu_subdevs), NULL, 0, NULL);
> +	if (ret)
> +		return dev_err_probe(dev, ret, "Failed to add child devices\n");
> +
> +	return 0;
> +}
> +
> +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 at sntech.de>");
> +MODULE_DESCRIPTION("QNAP MCU core driver");
> +MODULE_LICENSE("GPL");
> diff --git a/include/linux/mfd/qnap-mcu.h b/include/linux/mfd/qnap-mcu.h
> new file mode 100644
> index 000000000000..903b923537c7
> --- /dev/null
> +++ b/include/linux/mfd/qnap-mcu.h
> @@ -0,0 +1,27 @@
> +/* SPDX-License-Identifier: GPL-2.0+ */
> +/*
> + * Core definitions for QNAP MCU MFD driver.
> + * Copyright (C) 2024 Heiko Stuebner <heiko at 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);
> +const struct qnap_mcu_variant *qnap_mcu_get_variant_data(struct qnap_mcu *mcu);
> +
> +#endif /* _LINUX_QNAP_MCU_H_ */
> -- 
> 2.43.0
> 
> 

-- 
Lee Jones [李琼斯]



More information about the Linux-rockchip mailing list