Re: [PATCH v6 2/7] mfd: add base driver for qnap-mcu devices
From: Lee Jones
Date: Thu Aug 29 2024 - 09:39:29 EST
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@xxxxxxxxx>
> ---
> 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@xxxxxxxxxxxxxxx
> S: Odd Fixes
> F: drivers/media/tuners/qm1d1c0042*
>
> +QNAP MCU DRIVER
> +M: Heiko Stuebner <heiko@xxxxxxxxx>
> +S: Maintained
> +F: drivers/mfd/qnap-mcu.c
> +F: include/linux/qnap-mcu.h
> +
> QNX4 FILESYSTEM
> M: Anders Larsen <al@xxxxxxxxxxx>
> 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@xxxxxxxxx>
> + */
> +
> +#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@xxxxxxxxx>");
> +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@xxxxxxxxx>
> + */
> +
> +#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 [李琼斯]