Commit | Line | Data |
---|---|---|
e8fc721a | 1 | /* |
200c0a3e | 2 | * QNAP Turbo NAS Board power off. Can also be used on Synology devices. |
e8fc721a AL |
3 | * |
4 | * Copyright (C) 2012 Andrew Lunn <andrew@lunn.ch> | |
5 | * | |
6 | * Based on the code from: | |
7 | * | |
8 | * Copyright (C) 2009 Martin Michlmayr <tbm@cyrius.com> | |
9 | * Copyright (C) 2008 Byron Bradley <byron.bbradley@gmail.com> | |
10 | * | |
11 | * This program is free software; you can redistribute it and/or | |
12 | * modify it under the terms of the GNU General Public License | |
13 | * as published by the Free Software Foundation; either version | |
14 | * 2 of the License, or (at your option) any later version. | |
15 | */ | |
16 | ||
17 | #include <linux/kernel.h> | |
18 | #include <linux/module.h> | |
19 | #include <linux/platform_device.h> | |
20 | #include <linux/serial_reg.h> | |
21 | #include <linux/kallsyms.h> | |
22 | #include <linux/of.h> | |
23 | #include <linux/io.h> | |
24 | #include <linux/clk.h> | |
25 | ||
26 | #define UART1_REG(x) (base + ((UART_##x) << 2)) | |
27 | ||
200c0a3e AL |
28 | struct power_off_cfg { |
29 | u32 baud; | |
30 | char cmd; | |
31 | }; | |
32 | ||
33 | static const struct power_off_cfg qnap_power_off_cfg = { | |
34 | .baud = 19200, | |
35 | .cmd = 'A', | |
36 | }; | |
37 | ||
38 | static const struct power_off_cfg synology_power_off_cfg = { | |
39 | .baud = 9600, | |
40 | .cmd = '1', | |
41 | }; | |
42 | ||
43 | static const struct of_device_id qnap_power_off_of_match_table[] = { | |
44 | { .compatible = "qnap,power-off", | |
45 | .data = &qnap_power_off_cfg, | |
46 | }, | |
47 | { .compatible = "synology,power-off", | |
48 | .data = &synology_power_off_cfg, | |
49 | }, | |
50 | {} | |
51 | }; | |
52 | MODULE_DEVICE_TABLE(of, qnap_power_off_of_match_table); | |
53 | ||
e8fc721a AL |
54 | static void __iomem *base; |
55 | static unsigned long tclk; | |
200c0a3e | 56 | static const struct power_off_cfg *cfg; |
e8fc721a AL |
57 | |
58 | static void qnap_power_off(void) | |
59 | { | |
200c0a3e | 60 | const unsigned divisor = ((tclk + (8 * cfg->baud)) / (16 * cfg->baud)); |
e8fc721a AL |
61 | |
62 | pr_err("%s: triggering power-off...\n", __func__); | |
63 | ||
200c0a3e | 64 | /* hijack UART1 and reset into sane state */ |
e8fc721a AL |
65 | writel(0x83, UART1_REG(LCR)); |
66 | writel(divisor & 0xff, UART1_REG(DLL)); | |
67 | writel((divisor >> 8) & 0xff, UART1_REG(DLM)); | |
68 | writel(0x03, UART1_REG(LCR)); | |
69 | writel(0x00, UART1_REG(IER)); | |
70 | writel(0x00, UART1_REG(FCR)); | |
71 | writel(0x00, UART1_REG(MCR)); | |
72 | ||
200c0a3e AL |
73 | /* send the power-off command to PIC */ |
74 | writel(cfg->cmd, UART1_REG(TX)); | |
e8fc721a AL |
75 | } |
76 | ||
77 | static int qnap_power_off_probe(struct platform_device *pdev) | |
78 | { | |
200c0a3e | 79 | struct device_node *np = pdev->dev.of_node; |
e8fc721a AL |
80 | struct resource *res; |
81 | struct clk *clk; | |
82 | char symname[KSYM_NAME_LEN]; | |
83 | ||
200c0a3e AL |
84 | const struct of_device_id *match = |
85 | of_match_node(qnap_power_off_of_match_table, np); | |
86 | cfg = match->data; | |
87 | ||
e8fc721a AL |
88 | res = platform_get_resource(pdev, IORESOURCE_MEM, 0); |
89 | if (!res) { | |
90 | dev_err(&pdev->dev, "Missing resource"); | |
91 | return -EINVAL; | |
92 | } | |
93 | ||
94 | base = devm_ioremap(&pdev->dev, res->start, resource_size(res)); | |
95 | if (!base) { | |
96 | dev_err(&pdev->dev, "Unable to map resource"); | |
97 | return -EINVAL; | |
98 | } | |
99 | ||
100 | /* We need to know tclk in order to calculate the UART divisor */ | |
101 | clk = devm_clk_get(&pdev->dev, NULL); | |
102 | if (IS_ERR(clk)) { | |
103 | dev_err(&pdev->dev, "Clk missing"); | |
104 | return PTR_ERR(clk); | |
105 | } | |
106 | ||
107 | tclk = clk_get_rate(clk); | |
108 | ||
109 | /* Check that nothing else has already setup a handler */ | |
110 | if (pm_power_off) { | |
111 | lookup_symbol_name((ulong)pm_power_off, symname); | |
112 | dev_err(&pdev->dev, | |
113 | "pm_power_off already claimed %p %s", | |
114 | pm_power_off, symname); | |
115 | return -EBUSY; | |
116 | } | |
117 | pm_power_off = qnap_power_off; | |
118 | ||
119 | return 0; | |
120 | } | |
121 | ||
122 | static int qnap_power_off_remove(struct platform_device *pdev) | |
123 | { | |
124 | pm_power_off = NULL; | |
125 | return 0; | |
126 | } | |
127 | ||
e8fc721a AL |
128 | static struct platform_driver qnap_power_off_driver = { |
129 | .probe = qnap_power_off_probe, | |
130 | .remove = qnap_power_off_remove, | |
131 | .driver = { | |
e8fc721a AL |
132 | .name = "qnap_power_off", |
133 | .of_match_table = of_match_ptr(qnap_power_off_of_match_table), | |
134 | }, | |
135 | }; | |
136 | module_platform_driver(qnap_power_off_driver); | |
137 | ||
138 | MODULE_AUTHOR("Andrew Lunn <andrew@lunn.ch>"); | |
139 | MODULE_DESCRIPTION("QNAP Power off driver"); | |
8fd526fd | 140 | MODULE_LICENSE("GPL v2"); |