| // 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_ERROR_SIZE 2 | |
| #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 | |
| * @rx: Receive buffer the reply is assembled in | |
| */ | |
| 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]; | |
| u8 rx[QNAP_MCU_RX_BUFFER_SIZE]; | |
| }; | |
| /* | |
| * 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 bool qnap_mcu_verify_checksum(const u8 *buf, size_t size) | |
| { | |
| u8 crc = qnap_mcu_csum(buf, size - QNAP_MCU_CHECKSUM_SIZE); | |
| return crc == buf[size - QNAP_MCU_CHECKSUM_SIZE]; | |
| } | |
| 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 bool qnap_mcu_is_error_msg(size_t size) | |
| { | |
| return (size == QNAP_MCU_ERROR_SIZE + QNAP_MCU_CHECKSUM_SIZE); | |
| } | |
| static bool qnap_mcu_reply_is_generic_error(unsigned char *buf, size_t size) | |
| { | |
| if (!qnap_mcu_is_error_msg(size)) | |
| return false; | |
| if (buf[0] == '@' && buf[1] == '9') | |
| return true; | |
| return false; | |
| } | |
| static bool qnap_mcu_reply_is_checksum_error(unsigned char *buf, size_t size) | |
| { | |
| if (!qnap_mcu_is_error_msg(size)) | |
| return false; | |
| if (buf[0] == '@' && buf[1] == '8') | |
| return true; | |
| return false; | |
| } | |
| static bool qnap_mcu_reply_is_any_error(struct qnap_mcu *mcu, unsigned char *buf, size_t size) | |
| { | |
| if (qnap_mcu_reply_is_generic_error(buf, size)) { | |
| dev_err(&mcu->serdev->dev, "Controller sent generic error response\n"); | |
| return true; | |
| } | |
| if (qnap_mcu_reply_is_checksum_error(buf, size)) { | |
| dev_err(&mcu->serdev->dev, "Controller received invalid checksum for the command\n"); | |
| return true; | |
| } | |
| return false; | |
| } | |
| 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; | |
| } | |
| } | |
| /* | |
| * We received everything the uart had to offer for now. | |
| * This could mean that either the uart will send more in a 2nd | |
| * receive run, or that the MCU cut the reply short because it | |
| * sent an error code instead of the expected reply. | |
| * | |
| * So check if the received data has the correct size for an error | |
| * reply and if it matches, is an actual error code. | |
| */ | |
| if (qnap_mcu_is_error_msg(reply->received) && | |
| qnap_mcu_verify_checksum(reply->data, reply->received) && | |
| qnap_mcu_reply_is_any_error(mcu, reply->data, reply->received)) { | |
| /* The reply was an error code, we're done */ | |
| reply->length = 0; | |
| complete(&reply->done); | |
| } | |
| /* | |
| * 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) | |
| { | |
| size_t length = reply_data_size + QNAP_MCU_CHECKSUM_SIZE; | |
| struct qnap_mcu_reply *reply = &mcu->reply; | |
| int ret = 0; | |
| if (length > sizeof(mcu->rx)) { | |
| dev_err(&mcu->serdev->dev, "expected data too big for receive buffer"); | |
| return -EINVAL; | |
| } | |
| guard(mutex)(&mcu->bus_lock); | |
| reply->data = mcu->rx; | |
| reply->length = length; | |
| reply->received = 0; | |
| reinit_completion(&reply->done); | |
| ret = qnap_mcu_write(mcu, cmd_data, cmd_data_size); | |
| if (ret < 0) | |
| return ret; | |
| 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"); | |
| return -ETIMEDOUT; | |
| } | |
| if (!qnap_mcu_verify_checksum(mcu->rx, reply->received)) { | |
| dev_err(&mcu->serdev->dev, "Invalid Checksum received from controller\n"); | |
| return -EPROTO; | |
| } | |
| if (qnap_mcu_reply_is_any_error(mcu, mcu->rx, reply->received)) | |
| return -EPROTO; | |
| memcpy(reply_data, mcu->rx, reply_data_size); | |
| return 0; | |
| } | |
| 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_ts133_mcu = { | |
| .baud_rate = 115200, | |
| .num_drives = 1, | |
| .fan_pwm_min = 51, /* Specified in original model.conf */ | |
| .fan_pwm_max = 255, | |
| .usb_led = false, | |
| }; | |
| static const struct qnap_mcu_variant qnap_ts233_mcu = { | |
| .baud_rate = 115200, | |
| .num_drives = 2, | |
| .fan_pwm_min = 51, /* Specified in original model.conf */ | |
| .fan_pwm_max = 255, | |
| .usb_led = true, | |
| }; | |
| 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-eeprom", }, | |
| { .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,ts133-mcu", .data = &qnap_ts133_mcu }, | |
| { .compatible = "qnap,ts233-mcu", .data = &qnap_ts233_mcu }, | |
| { .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"); |