xref: /linux/drivers/mfd/qnap-mcu.c (revision dcab75a3c8a3b136781a6d8d088afdca974291ae)
1998f70d1SHeiko Stuebner // SPDX-License-Identifier: GPL-2.0-only
2998f70d1SHeiko Stuebner /*
3998f70d1SHeiko Stuebner  * Core driver for the microcontroller unit in QNAP NAS devices that is
4998f70d1SHeiko Stuebner  * connected via a dedicated UART port.
5998f70d1SHeiko Stuebner  *
6998f70d1SHeiko Stuebner  * Copyright (C) 2024 Heiko Stuebner <heiko@sntech.de>
7998f70d1SHeiko Stuebner  */
8998f70d1SHeiko Stuebner 
9998f70d1SHeiko Stuebner #include <linux/cleanup.h>
10998f70d1SHeiko Stuebner #include <linux/export.h>
11998f70d1SHeiko Stuebner #include <linux/mfd/core.h>
12998f70d1SHeiko Stuebner #include <linux/mfd/qnap-mcu.h>
13998f70d1SHeiko Stuebner #include <linux/module.h>
14998f70d1SHeiko Stuebner #include <linux/of.h>
15998f70d1SHeiko Stuebner #include <linux/reboot.h>
16998f70d1SHeiko Stuebner #include <linux/serdev.h>
17998f70d1SHeiko Stuebner #include <linux/slab.h>
18998f70d1SHeiko Stuebner 
19998f70d1SHeiko Stuebner /* The longest command found so far is 5 bytes long */
20998f70d1SHeiko Stuebner #define QNAP_MCU_MAX_CMD_SIZE		5
21998f70d1SHeiko Stuebner #define QNAP_MCU_MAX_DATA_SIZE		36
22998f70d1SHeiko Stuebner #define QNAP_MCU_CHECKSUM_SIZE		1
23998f70d1SHeiko Stuebner 
24998f70d1SHeiko Stuebner #define QNAP_MCU_RX_BUFFER_SIZE		\
25998f70d1SHeiko Stuebner 		(QNAP_MCU_MAX_DATA_SIZE + QNAP_MCU_CHECKSUM_SIZE)
26998f70d1SHeiko Stuebner 
27998f70d1SHeiko Stuebner #define QNAP_MCU_TX_BUFFER_SIZE		\
28998f70d1SHeiko Stuebner 		(QNAP_MCU_MAX_CMD_SIZE + QNAP_MCU_CHECKSUM_SIZE)
29998f70d1SHeiko Stuebner 
30998f70d1SHeiko Stuebner #define QNAP_MCU_ACK_LEN		2
31998f70d1SHeiko Stuebner #define QNAP_MCU_VERSION_LEN		4
32998f70d1SHeiko Stuebner 
33998f70d1SHeiko Stuebner #define QNAP_MCU_TIMEOUT_MS		500
34998f70d1SHeiko Stuebner 
35998f70d1SHeiko Stuebner /**
36998f70d1SHeiko Stuebner  * struct qnap_mcu_reply - Reply to a command
37998f70d1SHeiko Stuebner  *
38998f70d1SHeiko Stuebner  * @data:	Buffer to store reply payload in
39998f70d1SHeiko Stuebner  * @length:	Expected reply length, including the checksum
40998f70d1SHeiko Stuebner  * @received:	Received number of bytes, so far
41998f70d1SHeiko Stuebner  * @done:	Triggered when the entire reply has been received
42998f70d1SHeiko Stuebner  */
43998f70d1SHeiko Stuebner struct qnap_mcu_reply {
44998f70d1SHeiko Stuebner 	u8 *data;
45998f70d1SHeiko Stuebner 	size_t length;
46998f70d1SHeiko Stuebner 	size_t received;
47998f70d1SHeiko Stuebner 	struct completion done;
48998f70d1SHeiko Stuebner };
49998f70d1SHeiko Stuebner 
50998f70d1SHeiko Stuebner /**
51998f70d1SHeiko Stuebner  * struct qnap_mcu - QNAP NAS embedded controller
52998f70d1SHeiko Stuebner  *
53998f70d1SHeiko Stuebner  * @serdev:	Pointer to underlying serdev
54998f70d1SHeiko Stuebner  * @bus_lock:	Lock to serialize access to the device
55998f70d1SHeiko Stuebner  * @reply:	Reply data structure
56998f70d1SHeiko Stuebner  * @variant:	Device variant specific information
57998f70d1SHeiko Stuebner  * @version:	MCU firmware version
58998f70d1SHeiko Stuebner  */
59998f70d1SHeiko Stuebner struct qnap_mcu {
60998f70d1SHeiko Stuebner 	struct serdev_device *serdev;
61998f70d1SHeiko Stuebner 	struct mutex bus_lock;
62998f70d1SHeiko Stuebner 	struct qnap_mcu_reply reply;
63998f70d1SHeiko Stuebner 	const struct qnap_mcu_variant *variant;
64998f70d1SHeiko Stuebner 	u8 version[QNAP_MCU_VERSION_LEN];
65998f70d1SHeiko Stuebner };
66998f70d1SHeiko Stuebner 
67998f70d1SHeiko Stuebner /*
68998f70d1SHeiko Stuebner  * The QNAP-MCU uses a basic XOR checksum.
69998f70d1SHeiko Stuebner  * It is always the last byte and XORs the whole previous message.
70998f70d1SHeiko Stuebner  */
71998f70d1SHeiko Stuebner static u8 qnap_mcu_csum(const u8 *buf, size_t size)
72998f70d1SHeiko Stuebner {
73998f70d1SHeiko Stuebner 	u8 csum = 0;
74998f70d1SHeiko Stuebner 
75998f70d1SHeiko Stuebner 	while (size--)
76998f70d1SHeiko Stuebner 		csum ^= *buf++;
77998f70d1SHeiko Stuebner 
78998f70d1SHeiko Stuebner 	return csum;
79998f70d1SHeiko Stuebner }
80998f70d1SHeiko Stuebner 
81998f70d1SHeiko Stuebner static int qnap_mcu_write(struct qnap_mcu *mcu, const u8 *data, u8 data_size)
82998f70d1SHeiko Stuebner {
83998f70d1SHeiko Stuebner 	unsigned char tx[QNAP_MCU_TX_BUFFER_SIZE];
84998f70d1SHeiko Stuebner 	size_t length = data_size + QNAP_MCU_CHECKSUM_SIZE;
85998f70d1SHeiko Stuebner 
86998f70d1SHeiko Stuebner 	if (length > sizeof(tx)) {
87998f70d1SHeiko Stuebner 		dev_err(&mcu->serdev->dev, "data too big for transmit buffer");
88998f70d1SHeiko Stuebner 		return -EINVAL;
89998f70d1SHeiko Stuebner 	}
90998f70d1SHeiko Stuebner 
91998f70d1SHeiko Stuebner 	memcpy(tx, data, data_size);
92998f70d1SHeiko Stuebner 	tx[data_size] = qnap_mcu_csum(data, data_size);
93998f70d1SHeiko Stuebner 
94998f70d1SHeiko Stuebner 	serdev_device_write_flush(mcu->serdev);
95998f70d1SHeiko Stuebner 
96998f70d1SHeiko Stuebner 	return serdev_device_write(mcu->serdev, tx, length, HZ);
97998f70d1SHeiko Stuebner }
98998f70d1SHeiko Stuebner 
99998f70d1SHeiko Stuebner static size_t qnap_mcu_receive_buf(struct serdev_device *serdev, const u8 *buf, size_t size)
100998f70d1SHeiko Stuebner {
101998f70d1SHeiko Stuebner 	struct device *dev = &serdev->dev;
102998f70d1SHeiko Stuebner 	struct qnap_mcu *mcu = dev_get_drvdata(dev);
103998f70d1SHeiko Stuebner 	struct qnap_mcu_reply *reply = &mcu->reply;
104998f70d1SHeiko Stuebner 	const u8 *src = buf;
105998f70d1SHeiko Stuebner 	const u8 *end = buf + size;
106998f70d1SHeiko Stuebner 
107998f70d1SHeiko Stuebner 	if (!reply->length) {
108998f70d1SHeiko Stuebner 		dev_warn(dev, "Received %zu bytes, we were not waiting for\n", size);
109998f70d1SHeiko Stuebner 		return size;
110998f70d1SHeiko Stuebner 	}
111998f70d1SHeiko Stuebner 
112998f70d1SHeiko Stuebner 	while (src < end) {
113998f70d1SHeiko Stuebner 		reply->data[reply->received] = *src++;
114998f70d1SHeiko Stuebner 		reply->received++;
115998f70d1SHeiko Stuebner 
116998f70d1SHeiko Stuebner 		if (reply->received == reply->length) {
117998f70d1SHeiko Stuebner 			/* We don't expect any characters from the device now */
118998f70d1SHeiko Stuebner 			reply->length = 0;
119998f70d1SHeiko Stuebner 
120998f70d1SHeiko Stuebner 			complete(&reply->done);
121998f70d1SHeiko Stuebner 
122998f70d1SHeiko Stuebner 			/*
123998f70d1SHeiko Stuebner 			 * We report the consumed number of bytes. If there
124998f70d1SHeiko Stuebner 			 * are still bytes remaining (though there shouldn't)
125998f70d1SHeiko Stuebner 			 * the serdev layer will re-execute this handler with
126998f70d1SHeiko Stuebner 			 * the remainder of the Rx bytes.
127998f70d1SHeiko Stuebner 			 */
128998f70d1SHeiko Stuebner 			return src - buf;
129998f70d1SHeiko Stuebner 		}
130998f70d1SHeiko Stuebner 	}
131998f70d1SHeiko Stuebner 
132998f70d1SHeiko Stuebner 	/*
133998f70d1SHeiko Stuebner 	 * The only way to get out of the above loop and end up here
134998f70d1SHeiko Stuebner 	 * is through consuming all of the supplied data, so here we
135998f70d1SHeiko Stuebner 	 * report that we processed it all.
136998f70d1SHeiko Stuebner 	 */
137998f70d1SHeiko Stuebner 	return size;
138998f70d1SHeiko Stuebner }
139998f70d1SHeiko Stuebner 
140998f70d1SHeiko Stuebner static const struct serdev_device_ops qnap_mcu_serdev_device_ops = {
141998f70d1SHeiko Stuebner 	.receive_buf  = qnap_mcu_receive_buf,
142998f70d1SHeiko Stuebner 	.write_wakeup = serdev_device_write_wakeup,
143998f70d1SHeiko Stuebner };
144998f70d1SHeiko Stuebner 
145998f70d1SHeiko Stuebner int qnap_mcu_exec(struct qnap_mcu *mcu,
146998f70d1SHeiko Stuebner 		  const u8 *cmd_data, size_t cmd_data_size,
147998f70d1SHeiko Stuebner 		  u8 *reply_data, size_t reply_data_size)
148998f70d1SHeiko Stuebner {
149998f70d1SHeiko Stuebner 	unsigned char rx[QNAP_MCU_RX_BUFFER_SIZE];
150998f70d1SHeiko Stuebner 	size_t length = reply_data_size + QNAP_MCU_CHECKSUM_SIZE;
151998f70d1SHeiko Stuebner 	struct qnap_mcu_reply *reply = &mcu->reply;
152998f70d1SHeiko Stuebner 	int ret = 0;
153998f70d1SHeiko Stuebner 
154998f70d1SHeiko Stuebner 	if (length > sizeof(rx)) {
155998f70d1SHeiko Stuebner 		dev_err(&mcu->serdev->dev, "expected data too big for receive buffer");
156998f70d1SHeiko Stuebner 		return -EINVAL;
157998f70d1SHeiko Stuebner 	}
158998f70d1SHeiko Stuebner 
159998f70d1SHeiko Stuebner 	mutex_lock(&mcu->bus_lock);
160998f70d1SHeiko Stuebner 
161*f8271825SChen Ni 	reply->data = rx;
162*f8271825SChen Ni 	reply->length = length;
163*f8271825SChen Ni 	reply->received = 0;
164998f70d1SHeiko Stuebner 	reinit_completion(&reply->done);
165998f70d1SHeiko Stuebner 
166998f70d1SHeiko Stuebner 	qnap_mcu_write(mcu, cmd_data, cmd_data_size);
167998f70d1SHeiko Stuebner 
168998f70d1SHeiko Stuebner 	serdev_device_wait_until_sent(mcu->serdev, msecs_to_jiffies(QNAP_MCU_TIMEOUT_MS));
169998f70d1SHeiko Stuebner 
170998f70d1SHeiko Stuebner 	if (!wait_for_completion_timeout(&reply->done, msecs_to_jiffies(QNAP_MCU_TIMEOUT_MS))) {
171998f70d1SHeiko Stuebner 		dev_err(&mcu->serdev->dev, "Command timeout\n");
172998f70d1SHeiko Stuebner 		ret = -ETIMEDOUT;
173998f70d1SHeiko Stuebner 	} else {
174998f70d1SHeiko Stuebner 		u8 crc = qnap_mcu_csum(rx, reply_data_size);
175998f70d1SHeiko Stuebner 
176998f70d1SHeiko Stuebner 		if (crc != rx[reply_data_size]) {
177998f70d1SHeiko Stuebner 			dev_err(&mcu->serdev->dev,
178998f70d1SHeiko Stuebner 				"Invalid Checksum received\n");
179998f70d1SHeiko Stuebner 			ret = -EIO;
180998f70d1SHeiko Stuebner 		} else {
181998f70d1SHeiko Stuebner 			memcpy(reply_data, rx, reply_data_size);
182998f70d1SHeiko Stuebner 		}
183998f70d1SHeiko Stuebner 	}
184998f70d1SHeiko Stuebner 
185998f70d1SHeiko Stuebner 	mutex_unlock(&mcu->bus_lock);
186998f70d1SHeiko Stuebner 	return ret;
187998f70d1SHeiko Stuebner }
188998f70d1SHeiko Stuebner EXPORT_SYMBOL_GPL(qnap_mcu_exec);
189998f70d1SHeiko Stuebner 
190998f70d1SHeiko Stuebner int qnap_mcu_exec_with_ack(struct qnap_mcu *mcu,
191998f70d1SHeiko Stuebner 			   const u8 *cmd_data, size_t cmd_data_size)
192998f70d1SHeiko Stuebner {
193998f70d1SHeiko Stuebner 	u8 ack[QNAP_MCU_ACK_LEN];
194998f70d1SHeiko Stuebner 	int ret;
195998f70d1SHeiko Stuebner 
196998f70d1SHeiko Stuebner 	ret = qnap_mcu_exec(mcu, cmd_data, cmd_data_size, ack, sizeof(ack));
197998f70d1SHeiko Stuebner 	if (ret)
198998f70d1SHeiko Stuebner 		return ret;
199998f70d1SHeiko Stuebner 
200998f70d1SHeiko Stuebner 	/* Should return @0 */
201998f70d1SHeiko Stuebner 	if (ack[0] != '@' || ack[1] != '0') {
202998f70d1SHeiko Stuebner 		dev_err(&mcu->serdev->dev, "Did not receive ack\n");
203998f70d1SHeiko Stuebner 		return -EIO;
204998f70d1SHeiko Stuebner 	}
205998f70d1SHeiko Stuebner 
206998f70d1SHeiko Stuebner 	return 0;
207998f70d1SHeiko Stuebner }
208998f70d1SHeiko Stuebner EXPORT_SYMBOL_GPL(qnap_mcu_exec_with_ack);
209998f70d1SHeiko Stuebner 
210998f70d1SHeiko Stuebner static int qnap_mcu_get_version(struct qnap_mcu *mcu)
211998f70d1SHeiko Stuebner {
212998f70d1SHeiko Stuebner 	const u8 cmd[] = { '%', 'V' };
213998f70d1SHeiko Stuebner 	u8 rx[14];
214998f70d1SHeiko Stuebner 	int ret;
215998f70d1SHeiko Stuebner 
216998f70d1SHeiko Stuebner 	/* Reply is the 2 command-bytes + 4 bytes describing the version */
217998f70d1SHeiko Stuebner 	ret = qnap_mcu_exec(mcu, cmd, sizeof(cmd), rx, QNAP_MCU_VERSION_LEN + 2);
218998f70d1SHeiko Stuebner 	if (ret)
219998f70d1SHeiko Stuebner 		return ret;
220998f70d1SHeiko Stuebner 
221998f70d1SHeiko Stuebner 	memcpy(mcu->version, &rx[2], QNAP_MCU_VERSION_LEN);
222998f70d1SHeiko Stuebner 
223998f70d1SHeiko Stuebner 	return 0;
224998f70d1SHeiko Stuebner }
225998f70d1SHeiko Stuebner 
226998f70d1SHeiko Stuebner /*
227998f70d1SHeiko Stuebner  * The MCU controls power to the peripherals but not the CPU.
228998f70d1SHeiko Stuebner  *
229998f70d1SHeiko Stuebner  * So using the PMIC to power off the system keeps the MCU and hard-drives
230998f70d1SHeiko Stuebner  * running. This also then prevents the system from turning back on until
231998f70d1SHeiko Stuebner  * the MCU is turned off by unplugging the power cable.
232998f70d1SHeiko Stuebner  * Turning off the MCU alone on the other hand turns off the hard drives,
233998f70d1SHeiko Stuebner  * LEDs, etc while the main SoC stays running - including its network ports.
234998f70d1SHeiko Stuebner  */
235998f70d1SHeiko Stuebner static int qnap_mcu_power_off(struct sys_off_data *data)
236998f70d1SHeiko Stuebner {
237998f70d1SHeiko Stuebner 	const u8 cmd[] = { '@', 'C', '0' };
238998f70d1SHeiko Stuebner 	struct qnap_mcu *mcu = data->cb_data;
239998f70d1SHeiko Stuebner 	int ret;
240998f70d1SHeiko Stuebner 
241998f70d1SHeiko Stuebner 	ret = qnap_mcu_exec_with_ack(mcu, cmd, sizeof(cmd));
242998f70d1SHeiko Stuebner 	if (ret) {
243998f70d1SHeiko Stuebner 		dev_err(&mcu->serdev->dev, "MCU poweroff failed %d\n", ret);
244998f70d1SHeiko Stuebner 		return NOTIFY_STOP;
245998f70d1SHeiko Stuebner 	}
246998f70d1SHeiko Stuebner 
247998f70d1SHeiko Stuebner 	return NOTIFY_DONE;
248998f70d1SHeiko Stuebner }
249998f70d1SHeiko Stuebner 
250998f70d1SHeiko Stuebner static const struct qnap_mcu_variant qnap_ts433_mcu = {
251998f70d1SHeiko Stuebner 	.baud_rate = 115200,
252998f70d1SHeiko Stuebner 	.num_drives = 4,
253998f70d1SHeiko Stuebner 	.fan_pwm_min = 51,  /* Specified in original model.conf */
254998f70d1SHeiko Stuebner 	.fan_pwm_max = 255,
255998f70d1SHeiko Stuebner 	.usb_led = true,
256998f70d1SHeiko Stuebner };
257998f70d1SHeiko Stuebner 
258998f70d1SHeiko Stuebner static struct mfd_cell qnap_mcu_cells[] = {
259998f70d1SHeiko Stuebner 	{ .name = "qnap-mcu-input", },
260998f70d1SHeiko Stuebner 	{ .name = "qnap-mcu-leds", },
261998f70d1SHeiko Stuebner 	{ .name = "qnap-mcu-hwmon", }
262998f70d1SHeiko Stuebner };
263998f70d1SHeiko Stuebner 
264998f70d1SHeiko Stuebner static int qnap_mcu_probe(struct serdev_device *serdev)
265998f70d1SHeiko Stuebner {
266998f70d1SHeiko Stuebner 	struct device *dev = &serdev->dev;
267998f70d1SHeiko Stuebner 	struct qnap_mcu *mcu;
268998f70d1SHeiko Stuebner 	int ret;
269998f70d1SHeiko Stuebner 
270998f70d1SHeiko Stuebner 	mcu = devm_kzalloc(dev, sizeof(*mcu), GFP_KERNEL);
271998f70d1SHeiko Stuebner 	if (!mcu)
272998f70d1SHeiko Stuebner 		return -ENOMEM;
273998f70d1SHeiko Stuebner 
274998f70d1SHeiko Stuebner 	mcu->serdev = serdev;
275998f70d1SHeiko Stuebner 	dev_set_drvdata(dev, mcu);
276998f70d1SHeiko Stuebner 
277998f70d1SHeiko Stuebner 	mcu->variant = of_device_get_match_data(dev);
278998f70d1SHeiko Stuebner 	if (!mcu->variant)
279998f70d1SHeiko Stuebner 		return -ENODEV;
280998f70d1SHeiko Stuebner 
281998f70d1SHeiko Stuebner 	mutex_init(&mcu->bus_lock);
282998f70d1SHeiko Stuebner 	init_completion(&mcu->reply.done);
283998f70d1SHeiko Stuebner 
284998f70d1SHeiko Stuebner 	serdev_device_set_client_ops(serdev, &qnap_mcu_serdev_device_ops);
285998f70d1SHeiko Stuebner 	ret = devm_serdev_device_open(dev, serdev);
286998f70d1SHeiko Stuebner 	if (ret)
287998f70d1SHeiko Stuebner 		return ret;
288998f70d1SHeiko Stuebner 
289998f70d1SHeiko Stuebner 	serdev_device_set_baudrate(serdev, mcu->variant->baud_rate);
290998f70d1SHeiko Stuebner 	serdev_device_set_flow_control(serdev, false);
291998f70d1SHeiko Stuebner 
292998f70d1SHeiko Stuebner 	ret = serdev_device_set_parity(serdev, SERDEV_PARITY_NONE);
293998f70d1SHeiko Stuebner 	if (ret)
294998f70d1SHeiko Stuebner 		return dev_err_probe(dev, ret, "Failed to set parity\n");
295998f70d1SHeiko Stuebner 
296998f70d1SHeiko Stuebner 	ret = qnap_mcu_get_version(mcu);
297998f70d1SHeiko Stuebner 	if (ret)
298998f70d1SHeiko Stuebner 		return ret;
299998f70d1SHeiko Stuebner 
300998f70d1SHeiko Stuebner 	ret = devm_register_sys_off_handler(dev,
301998f70d1SHeiko Stuebner 					    SYS_OFF_MODE_POWER_OFF_PREPARE,
302998f70d1SHeiko Stuebner 					    SYS_OFF_PRIO_DEFAULT,
303998f70d1SHeiko Stuebner 					    &qnap_mcu_power_off, mcu);
304998f70d1SHeiko Stuebner 	if (ret)
305998f70d1SHeiko Stuebner 		return dev_err_probe(dev, ret,
306998f70d1SHeiko Stuebner 				     "Failed to register poweroff handler\n");
307998f70d1SHeiko Stuebner 
308998f70d1SHeiko Stuebner 	for (int i = 0; i < ARRAY_SIZE(qnap_mcu_cells); i++) {
309998f70d1SHeiko Stuebner 		qnap_mcu_cells[i].platform_data = mcu->variant;
310998f70d1SHeiko Stuebner 		qnap_mcu_cells[i].pdata_size = sizeof(*mcu->variant);
311998f70d1SHeiko Stuebner 	}
312998f70d1SHeiko Stuebner 
313998f70d1SHeiko Stuebner 	ret = devm_mfd_add_devices(dev, PLATFORM_DEVID_AUTO, qnap_mcu_cells,
314998f70d1SHeiko Stuebner 				   ARRAY_SIZE(qnap_mcu_cells), NULL, 0, NULL);
315998f70d1SHeiko Stuebner 	if (ret)
316998f70d1SHeiko Stuebner 		return dev_err_probe(dev, ret, "Failed to add child devices\n");
317998f70d1SHeiko Stuebner 
318998f70d1SHeiko Stuebner 	return 0;
319998f70d1SHeiko Stuebner }
320998f70d1SHeiko Stuebner 
321998f70d1SHeiko Stuebner static const struct of_device_id qnap_mcu_dt_ids[] = {
322998f70d1SHeiko Stuebner 	{ .compatible = "qnap,ts433-mcu", .data = &qnap_ts433_mcu },
323998f70d1SHeiko Stuebner 	{ /* sentinel */ }
324998f70d1SHeiko Stuebner };
325998f70d1SHeiko Stuebner MODULE_DEVICE_TABLE(of, qnap_mcu_dt_ids);
326998f70d1SHeiko Stuebner 
327998f70d1SHeiko Stuebner static struct serdev_device_driver qnap_mcu_drv = {
328998f70d1SHeiko Stuebner 	.probe = qnap_mcu_probe,
329998f70d1SHeiko Stuebner 	.driver = {
330998f70d1SHeiko Stuebner 		.name = "qnap-mcu",
331998f70d1SHeiko Stuebner 		.of_match_table = qnap_mcu_dt_ids,
332998f70d1SHeiko Stuebner 	},
333998f70d1SHeiko Stuebner };
334998f70d1SHeiko Stuebner module_serdev_device_driver(qnap_mcu_drv);
335998f70d1SHeiko Stuebner 
336998f70d1SHeiko Stuebner MODULE_AUTHOR("Heiko Stuebner <heiko@sntech.de>");
337998f70d1SHeiko Stuebner MODULE_DESCRIPTION("QNAP MCU core driver");
338998f70d1SHeiko Stuebner MODULE_LICENSE("GPL");
339