1 | // SPDX-License-Identifier: GPL-2.0 |
2 | /* |
3 | * Turris Mox rWTM firmware driver |
4 | * |
5 | * Copyright (C) 2019 Marek BehĂșn <kabel@kernel.org> |
6 | */ |
7 | |
8 | #include <linux/armada-37xx-rwtm-mailbox.h> |
9 | #include <linux/completion.h> |
10 | #include <linux/debugfs.h> |
11 | #include <linux/dma-mapping.h> |
12 | #include <linux/hw_random.h> |
13 | #include <linux/mailbox_client.h> |
14 | #include <linux/module.h> |
15 | #include <linux/mutex.h> |
16 | #include <linux/of.h> |
17 | #include <linux/platform_device.h> |
18 | #include <linux/slab.h> |
19 | |
20 | #define DRIVER_NAME "turris-mox-rwtm" |
21 | |
22 | /* |
23 | * The macros and constants below come from Turris Mox's rWTM firmware code. |
24 | * This firmware is open source and it's sources can be found at |
25 | * https://gitlab.labs.nic.cz/turris/mox-boot-builder/tree/master/wtmi. |
26 | */ |
27 | |
28 | #define MBOX_STS_SUCCESS (0 << 30) |
29 | #define MBOX_STS_FAIL (1 << 30) |
30 | #define MBOX_STS_BADCMD (2 << 30) |
31 | #define MBOX_STS_ERROR(s) ((s) & (3 << 30)) |
32 | #define MBOX_STS_VALUE(s) (((s) >> 10) & 0xfffff) |
33 | #define MBOX_STS_CMD(s) ((s) & 0x3ff) |
34 | |
35 | enum mbox_cmd { |
36 | MBOX_CMD_GET_RANDOM = 1, |
37 | MBOX_CMD_BOARD_INFO = 2, |
38 | MBOX_CMD_ECDSA_PUB_KEY = 3, |
39 | MBOX_CMD_HASH = 4, |
40 | MBOX_CMD_SIGN = 5, |
41 | MBOX_CMD_VERIFY = 6, |
42 | |
43 | MBOX_CMD_OTP_READ = 7, |
44 | MBOX_CMD_OTP_WRITE = 8, |
45 | }; |
46 | |
47 | struct mox_kobject; |
48 | |
49 | struct mox_rwtm { |
50 | struct device *dev; |
51 | struct mbox_client mbox_client; |
52 | struct mbox_chan *mbox; |
53 | struct mox_kobject *kobj; |
54 | struct hwrng hwrng; |
55 | |
56 | struct armada_37xx_rwtm_rx_msg reply; |
57 | |
58 | void *buf; |
59 | dma_addr_t buf_phys; |
60 | |
61 | struct mutex busy; |
62 | struct completion cmd_done; |
63 | |
64 | /* board information */ |
65 | int has_board_info; |
66 | u64 serial_number; |
67 | int board_version, ram_size; |
68 | u8 mac_address1[6], mac_address2[6]; |
69 | |
70 | /* public key burned in eFuse */ |
71 | int has_pubkey; |
72 | u8 pubkey[135]; |
73 | |
74 | #ifdef CONFIG_DEBUG_FS |
75 | /* |
76 | * Signature process. This is currently done via debugfs, because it |
77 | * does not conform to the sysfs standard "one file per attribute". |
78 | * It should be rewritten via crypto API once akcipher API is available |
79 | * from userspace. |
80 | */ |
81 | struct dentry *debugfs_root; |
82 | u32 last_sig[34]; |
83 | int last_sig_done; |
84 | #endif |
85 | }; |
86 | |
87 | struct mox_kobject { |
88 | struct kobject kobj; |
89 | struct mox_rwtm *rwtm; |
90 | }; |
91 | |
92 | static inline struct kobject *rwtm_to_kobj(struct mox_rwtm *rwtm) |
93 | { |
94 | return &rwtm->kobj->kobj; |
95 | } |
96 | |
97 | static inline struct mox_rwtm *to_rwtm(struct kobject *kobj) |
98 | { |
99 | return container_of(kobj, struct mox_kobject, kobj)->rwtm; |
100 | } |
101 | |
102 | static void mox_kobj_release(struct kobject *kobj) |
103 | { |
104 | kfree(objp: to_rwtm(kobj)->kobj); |
105 | } |
106 | |
107 | static const struct kobj_type mox_kobj_ktype = { |
108 | .release = mox_kobj_release, |
109 | .sysfs_ops = &kobj_sysfs_ops, |
110 | }; |
111 | |
112 | static int mox_kobj_create(struct mox_rwtm *rwtm) |
113 | { |
114 | rwtm->kobj = kzalloc(size: sizeof(*rwtm->kobj), GFP_KERNEL); |
115 | if (!rwtm->kobj) |
116 | return -ENOMEM; |
117 | |
118 | kobject_init(kobj: rwtm_to_kobj(rwtm), ktype: &mox_kobj_ktype); |
119 | if (kobject_add(kobj: rwtm_to_kobj(rwtm), parent: firmware_kobj, fmt: "turris-mox-rwtm" )) { |
120 | kobject_put(kobj: rwtm_to_kobj(rwtm)); |
121 | return -ENXIO; |
122 | } |
123 | |
124 | rwtm->kobj->rwtm = rwtm; |
125 | |
126 | return 0; |
127 | } |
128 | |
129 | #define MOX_ATTR_RO(name, format, cat) \ |
130 | static ssize_t \ |
131 | name##_show(struct kobject *kobj, struct kobj_attribute *a, \ |
132 | char *buf) \ |
133 | { \ |
134 | struct mox_rwtm *rwtm = to_rwtm(kobj); \ |
135 | if (!rwtm->has_##cat) \ |
136 | return -ENODATA; \ |
137 | return sprintf(buf, format, rwtm->name); \ |
138 | } \ |
139 | static struct kobj_attribute mox_attr_##name = __ATTR_RO(name) |
140 | |
141 | MOX_ATTR_RO(serial_number, "%016llX\n" , board_info); |
142 | MOX_ATTR_RO(board_version, "%i\n" , board_info); |
143 | MOX_ATTR_RO(ram_size, "%i\n" , board_info); |
144 | MOX_ATTR_RO(mac_address1, "%pM\n" , board_info); |
145 | MOX_ATTR_RO(mac_address2, "%pM\n" , board_info); |
146 | MOX_ATTR_RO(pubkey, "%s\n" , pubkey); |
147 | |
148 | static int mox_get_status(enum mbox_cmd cmd, u32 retval) |
149 | { |
150 | if (MBOX_STS_CMD(retval) != cmd) |
151 | return -EIO; |
152 | else if (MBOX_STS_ERROR(retval) == MBOX_STS_FAIL) |
153 | return -(int)MBOX_STS_VALUE(retval); |
154 | else if (MBOX_STS_ERROR(retval) == MBOX_STS_BADCMD) |
155 | return -ENOSYS; |
156 | else if (MBOX_STS_ERROR(retval) != MBOX_STS_SUCCESS) |
157 | return -EIO; |
158 | else |
159 | return MBOX_STS_VALUE(retval); |
160 | } |
161 | |
162 | static const struct attribute *mox_rwtm_attrs[] = { |
163 | &mox_attr_serial_number.attr, |
164 | &mox_attr_board_version.attr, |
165 | &mox_attr_ram_size.attr, |
166 | &mox_attr_mac_address1.attr, |
167 | &mox_attr_mac_address2.attr, |
168 | &mox_attr_pubkey.attr, |
169 | NULL |
170 | }; |
171 | |
172 | static void mox_rwtm_rx_callback(struct mbox_client *cl, void *data) |
173 | { |
174 | struct mox_rwtm *rwtm = dev_get_drvdata(dev: cl->dev); |
175 | struct armada_37xx_rwtm_rx_msg *msg = data; |
176 | |
177 | rwtm->reply = *msg; |
178 | complete(&rwtm->cmd_done); |
179 | } |
180 | |
181 | static void reply_to_mac_addr(u8 *mac, u32 t1, u32 t2) |
182 | { |
183 | mac[0] = t1 >> 8; |
184 | mac[1] = t1; |
185 | mac[2] = t2 >> 24; |
186 | mac[3] = t2 >> 16; |
187 | mac[4] = t2 >> 8; |
188 | mac[5] = t2; |
189 | } |
190 | |
191 | static int mox_get_board_info(struct mox_rwtm *rwtm) |
192 | { |
193 | struct armada_37xx_rwtm_tx_msg msg; |
194 | struct armada_37xx_rwtm_rx_msg *reply = &rwtm->reply; |
195 | int ret; |
196 | |
197 | msg.command = MBOX_CMD_BOARD_INFO; |
198 | ret = mbox_send_message(chan: rwtm->mbox, mssg: &msg); |
199 | if (ret < 0) |
200 | return ret; |
201 | |
202 | ret = wait_for_completion_timeout(x: &rwtm->cmd_done, HZ / 2); |
203 | if (ret < 0) |
204 | return ret; |
205 | |
206 | ret = mox_get_status(cmd: MBOX_CMD_BOARD_INFO, retval: reply->retval); |
207 | if (ret == -ENODATA) { |
208 | dev_warn(rwtm->dev, |
209 | "Board does not have manufacturing information burned!\n" ); |
210 | } else if (ret == -ENOSYS) { |
211 | dev_notice(rwtm->dev, |
212 | "Firmware does not support the BOARD_INFO command\n" ); |
213 | } else if (ret < 0) { |
214 | return ret; |
215 | } else { |
216 | rwtm->serial_number = reply->status[1]; |
217 | rwtm->serial_number <<= 32; |
218 | rwtm->serial_number |= reply->status[0]; |
219 | rwtm->board_version = reply->status[2]; |
220 | rwtm->ram_size = reply->status[3]; |
221 | reply_to_mac_addr(mac: rwtm->mac_address1, t1: reply->status[4], |
222 | t2: reply->status[5]); |
223 | reply_to_mac_addr(mac: rwtm->mac_address2, t1: reply->status[6], |
224 | t2: reply->status[7]); |
225 | rwtm->has_board_info = 1; |
226 | |
227 | pr_info("Turris Mox serial number %016llX\n" , |
228 | rwtm->serial_number); |
229 | pr_info(" board version %i\n" , rwtm->board_version); |
230 | pr_info(" burned RAM size %i MiB\n" , rwtm->ram_size); |
231 | } |
232 | |
233 | msg.command = MBOX_CMD_ECDSA_PUB_KEY; |
234 | ret = mbox_send_message(chan: rwtm->mbox, mssg: &msg); |
235 | if (ret < 0) |
236 | return ret; |
237 | |
238 | ret = wait_for_completion_timeout(x: &rwtm->cmd_done, HZ / 2); |
239 | if (ret < 0) |
240 | return ret; |
241 | |
242 | ret = mox_get_status(cmd: MBOX_CMD_ECDSA_PUB_KEY, retval: reply->retval); |
243 | if (ret == -ENODATA) { |
244 | dev_warn(rwtm->dev, "Board has no public key burned!\n" ); |
245 | } else if (ret == -ENOSYS) { |
246 | dev_notice(rwtm->dev, |
247 | "Firmware does not support the ECDSA_PUB_KEY command\n" ); |
248 | } else if (ret < 0) { |
249 | return ret; |
250 | } else { |
251 | u32 *s = reply->status; |
252 | |
253 | rwtm->has_pubkey = 1; |
254 | sprintf(buf: rwtm->pubkey, |
255 | fmt: "%06x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x" , |
256 | ret, s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7], |
257 | s[8], s[9], s[10], s[11], s[12], s[13], s[14], s[15]); |
258 | } |
259 | |
260 | return 0; |
261 | } |
262 | |
263 | static int check_get_random_support(struct mox_rwtm *rwtm) |
264 | { |
265 | struct armada_37xx_rwtm_tx_msg msg; |
266 | int ret; |
267 | |
268 | msg.command = MBOX_CMD_GET_RANDOM; |
269 | msg.args[0] = 1; |
270 | msg.args[1] = rwtm->buf_phys; |
271 | msg.args[2] = 4; |
272 | |
273 | ret = mbox_send_message(chan: rwtm->mbox, mssg: &msg); |
274 | if (ret < 0) |
275 | return ret; |
276 | |
277 | ret = wait_for_completion_timeout(x: &rwtm->cmd_done, HZ / 2); |
278 | if (ret < 0) |
279 | return ret; |
280 | |
281 | return mox_get_status(cmd: MBOX_CMD_GET_RANDOM, retval: rwtm->reply.retval); |
282 | } |
283 | |
284 | static int mox_hwrng_read(struct hwrng *rng, void *data, size_t max, bool wait) |
285 | { |
286 | struct mox_rwtm *rwtm = (struct mox_rwtm *) rng->priv; |
287 | struct armada_37xx_rwtm_tx_msg msg; |
288 | int ret; |
289 | |
290 | if (max > 4096) |
291 | max = 4096; |
292 | |
293 | msg.command = MBOX_CMD_GET_RANDOM; |
294 | msg.args[0] = 1; |
295 | msg.args[1] = rwtm->buf_phys; |
296 | msg.args[2] = (max + 3) & ~3; |
297 | |
298 | if (!wait) { |
299 | if (!mutex_trylock(lock: &rwtm->busy)) |
300 | return -EBUSY; |
301 | } else { |
302 | mutex_lock(&rwtm->busy); |
303 | } |
304 | |
305 | ret = mbox_send_message(chan: rwtm->mbox, mssg: &msg); |
306 | if (ret < 0) |
307 | goto unlock_mutex; |
308 | |
309 | ret = wait_for_completion_interruptible(x: &rwtm->cmd_done); |
310 | if (ret < 0) |
311 | goto unlock_mutex; |
312 | |
313 | ret = mox_get_status(cmd: MBOX_CMD_GET_RANDOM, retval: rwtm->reply.retval); |
314 | if (ret < 0) |
315 | goto unlock_mutex; |
316 | |
317 | memcpy(data, rwtm->buf, max); |
318 | ret = max; |
319 | |
320 | unlock_mutex: |
321 | mutex_unlock(lock: &rwtm->busy); |
322 | return ret; |
323 | } |
324 | |
325 | #ifdef CONFIG_DEBUG_FS |
326 | static int rwtm_debug_open(struct inode *inode, struct file *file) |
327 | { |
328 | file->private_data = inode->i_private; |
329 | |
330 | return nonseekable_open(inode, filp: file); |
331 | } |
332 | |
333 | static ssize_t do_sign_read(struct file *file, char __user *buf, size_t len, |
334 | loff_t *ppos) |
335 | { |
336 | struct mox_rwtm *rwtm = file->private_data; |
337 | ssize_t ret; |
338 | |
339 | /* only allow one read, of 136 bytes, from position 0 */ |
340 | if (*ppos != 0) |
341 | return 0; |
342 | |
343 | if (len < 136) |
344 | return -EINVAL; |
345 | |
346 | if (!rwtm->last_sig_done) |
347 | return -ENODATA; |
348 | |
349 | /* 2 arrays of 17 32-bit words are 136 bytes */ |
350 | ret = simple_read_from_buffer(to: buf, count: len, ppos, from: rwtm->last_sig, available: 136); |
351 | rwtm->last_sig_done = 0; |
352 | |
353 | return ret; |
354 | } |
355 | |
356 | static ssize_t do_sign_write(struct file *file, const char __user *buf, |
357 | size_t len, loff_t *ppos) |
358 | { |
359 | struct mox_rwtm *rwtm = file->private_data; |
360 | struct armada_37xx_rwtm_rx_msg *reply = &rwtm->reply; |
361 | struct armada_37xx_rwtm_tx_msg msg; |
362 | loff_t dummy = 0; |
363 | ssize_t ret; |
364 | |
365 | /* the input is a SHA-512 hash, so exactly 64 bytes have to be read */ |
366 | if (len != 64) |
367 | return -EINVAL; |
368 | |
369 | /* if last result is not zero user has not read that information yet */ |
370 | if (rwtm->last_sig_done) |
371 | return -EBUSY; |
372 | |
373 | if (!mutex_trylock(lock: &rwtm->busy)) |
374 | return -EBUSY; |
375 | |
376 | /* |
377 | * Here we have to send: |
378 | * 1. Address of the input to sign. |
379 | * The input is an array of 17 32-bit words, the first (most |
380 | * significat) is 0, the rest 16 words are copied from the SHA-512 |
381 | * hash given by the user and converted from BE to LE. |
382 | * 2. Address of the buffer where ECDSA signature value R shall be |
383 | * stored by the rWTM firmware. |
384 | * 3. Address of the buffer where ECDSA signature value S shall be |
385 | * stored by the rWTM firmware. |
386 | */ |
387 | memset(rwtm->buf, 0, 4); |
388 | ret = simple_write_to_buffer(to: rwtm->buf + 4, available: 64, ppos: &dummy, from: buf, count: len); |
389 | if (ret < 0) |
390 | goto unlock_mutex; |
391 | be32_to_cpu_array(dst: rwtm->buf, src: rwtm->buf, len: 17); |
392 | |
393 | msg.command = MBOX_CMD_SIGN; |
394 | msg.args[0] = 1; |
395 | msg.args[1] = rwtm->buf_phys; |
396 | msg.args[2] = rwtm->buf_phys + 68; |
397 | msg.args[3] = rwtm->buf_phys + 2 * 68; |
398 | ret = mbox_send_message(chan: rwtm->mbox, mssg: &msg); |
399 | if (ret < 0) |
400 | goto unlock_mutex; |
401 | |
402 | ret = wait_for_completion_interruptible(x: &rwtm->cmd_done); |
403 | if (ret < 0) |
404 | goto unlock_mutex; |
405 | |
406 | ret = MBOX_STS_VALUE(reply->retval); |
407 | if (MBOX_STS_ERROR(reply->retval) != MBOX_STS_SUCCESS) |
408 | goto unlock_mutex; |
409 | |
410 | /* |
411 | * Here we read the R and S values of the ECDSA signature |
412 | * computed by the rWTM firmware and convert their words from |
413 | * LE to BE. |
414 | */ |
415 | memcpy(rwtm->last_sig, rwtm->buf + 68, 136); |
416 | cpu_to_be32_array(dst: rwtm->last_sig, src: rwtm->last_sig, len: 34); |
417 | rwtm->last_sig_done = 1; |
418 | |
419 | mutex_unlock(lock: &rwtm->busy); |
420 | return len; |
421 | unlock_mutex: |
422 | mutex_unlock(lock: &rwtm->busy); |
423 | return ret; |
424 | } |
425 | |
426 | static const struct file_operations do_sign_fops = { |
427 | .owner = THIS_MODULE, |
428 | .open = rwtm_debug_open, |
429 | .read = do_sign_read, |
430 | .write = do_sign_write, |
431 | .llseek = no_llseek, |
432 | }; |
433 | |
434 | static int rwtm_register_debugfs(struct mox_rwtm *rwtm) |
435 | { |
436 | struct dentry *root, *entry; |
437 | |
438 | root = debugfs_create_dir(name: "turris-mox-rwtm" , NULL); |
439 | |
440 | if (IS_ERR(ptr: root)) |
441 | return PTR_ERR(ptr: root); |
442 | |
443 | entry = debugfs_create_file_unsafe(name: "do_sign" , mode: 0600, parent: root, data: rwtm, |
444 | fops: &do_sign_fops); |
445 | if (IS_ERR(ptr: entry)) |
446 | goto err_remove; |
447 | |
448 | rwtm->debugfs_root = root; |
449 | |
450 | return 0; |
451 | err_remove: |
452 | debugfs_remove_recursive(dentry: root); |
453 | return PTR_ERR(ptr: entry); |
454 | } |
455 | |
456 | static void rwtm_unregister_debugfs(struct mox_rwtm *rwtm) |
457 | { |
458 | debugfs_remove_recursive(dentry: rwtm->debugfs_root); |
459 | } |
460 | #else |
461 | static inline int rwtm_register_debugfs(struct mox_rwtm *rwtm) |
462 | { |
463 | return 0; |
464 | } |
465 | |
466 | static inline void rwtm_unregister_debugfs(struct mox_rwtm *rwtm) |
467 | { |
468 | } |
469 | #endif |
470 | |
471 | static int turris_mox_rwtm_probe(struct platform_device *pdev) |
472 | { |
473 | struct mox_rwtm *rwtm; |
474 | struct device *dev = &pdev->dev; |
475 | int ret; |
476 | |
477 | rwtm = devm_kzalloc(dev, size: sizeof(*rwtm), GFP_KERNEL); |
478 | if (!rwtm) |
479 | return -ENOMEM; |
480 | |
481 | rwtm->dev = dev; |
482 | rwtm->buf = dmam_alloc_coherent(dev, PAGE_SIZE, dma_handle: &rwtm->buf_phys, |
483 | GFP_KERNEL); |
484 | if (!rwtm->buf) |
485 | return -ENOMEM; |
486 | |
487 | ret = mox_kobj_create(rwtm); |
488 | if (ret < 0) { |
489 | dev_err(dev, "Cannot create turris-mox-rwtm kobject!\n" ); |
490 | return ret; |
491 | } |
492 | |
493 | ret = sysfs_create_files(kobj: rwtm_to_kobj(rwtm), attr: mox_rwtm_attrs); |
494 | if (ret < 0) { |
495 | dev_err(dev, "Cannot create sysfs files!\n" ); |
496 | goto put_kobj; |
497 | } |
498 | |
499 | platform_set_drvdata(pdev, data: rwtm); |
500 | |
501 | mutex_init(&rwtm->busy); |
502 | |
503 | rwtm->mbox_client.dev = dev; |
504 | rwtm->mbox_client.rx_callback = mox_rwtm_rx_callback; |
505 | |
506 | rwtm->mbox = mbox_request_channel(cl: &rwtm->mbox_client, index: 0); |
507 | if (IS_ERR(ptr: rwtm->mbox)) { |
508 | ret = PTR_ERR(ptr: rwtm->mbox); |
509 | if (ret != -EPROBE_DEFER) |
510 | dev_err(dev, "Cannot request mailbox channel: %i\n" , |
511 | ret); |
512 | goto remove_files; |
513 | } |
514 | |
515 | init_completion(x: &rwtm->cmd_done); |
516 | |
517 | ret = mox_get_board_info(rwtm); |
518 | if (ret < 0) |
519 | dev_warn(dev, "Cannot read board information: %i\n" , ret); |
520 | |
521 | ret = check_get_random_support(rwtm); |
522 | if (ret < 0) { |
523 | dev_notice(dev, |
524 | "Firmware does not support the GET_RANDOM command\n" ); |
525 | goto free_channel; |
526 | } |
527 | |
528 | rwtm->hwrng.name = DRIVER_NAME "_hwrng" ; |
529 | rwtm->hwrng.read = mox_hwrng_read; |
530 | rwtm->hwrng.priv = (unsigned long) rwtm; |
531 | |
532 | ret = devm_hwrng_register(dev, rng: &rwtm->hwrng); |
533 | if (ret < 0) { |
534 | dev_err(dev, "Cannot register HWRNG: %i\n" , ret); |
535 | goto free_channel; |
536 | } |
537 | |
538 | ret = rwtm_register_debugfs(rwtm); |
539 | if (ret < 0) { |
540 | dev_err(dev, "Failed creating debugfs entries: %i\n" , ret); |
541 | goto free_channel; |
542 | } |
543 | |
544 | dev_info(dev, "HWRNG successfully registered\n" ); |
545 | |
546 | return 0; |
547 | |
548 | free_channel: |
549 | mbox_free_channel(chan: rwtm->mbox); |
550 | remove_files: |
551 | sysfs_remove_files(kobj: rwtm_to_kobj(rwtm), attr: mox_rwtm_attrs); |
552 | put_kobj: |
553 | kobject_put(kobj: rwtm_to_kobj(rwtm)); |
554 | return ret; |
555 | } |
556 | |
557 | static int turris_mox_rwtm_remove(struct platform_device *pdev) |
558 | { |
559 | struct mox_rwtm *rwtm = platform_get_drvdata(pdev); |
560 | |
561 | rwtm_unregister_debugfs(rwtm); |
562 | sysfs_remove_files(kobj: rwtm_to_kobj(rwtm), attr: mox_rwtm_attrs); |
563 | kobject_put(kobj: rwtm_to_kobj(rwtm)); |
564 | mbox_free_channel(chan: rwtm->mbox); |
565 | |
566 | return 0; |
567 | } |
568 | |
569 | static const struct of_device_id turris_mox_rwtm_match[] = { |
570 | { .compatible = "cznic,turris-mox-rwtm" , }, |
571 | { .compatible = "marvell,armada-3700-rwtm-firmware" , }, |
572 | { }, |
573 | }; |
574 | |
575 | MODULE_DEVICE_TABLE(of, turris_mox_rwtm_match); |
576 | |
577 | static struct platform_driver turris_mox_rwtm_driver = { |
578 | .probe = turris_mox_rwtm_probe, |
579 | .remove = turris_mox_rwtm_remove, |
580 | .driver = { |
581 | .name = DRIVER_NAME, |
582 | .of_match_table = turris_mox_rwtm_match, |
583 | }, |
584 | }; |
585 | module_platform_driver(turris_mox_rwtm_driver); |
586 | |
587 | MODULE_LICENSE("GPL v2" ); |
588 | MODULE_DESCRIPTION("Turris Mox rWTM firmware driver" ); |
589 | MODULE_AUTHOR("Marek Behun <kabel@kernel.org>" ); |
590 | |