// 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 */ #include #include #include #include #include #include #include #include #include /* 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 "); MODULE_DESCRIPTION("QNAP MCU core driver"); MODULE_LICENSE("GPL");