1 | // SPDX-License-Identifier: GPL-2.0 |
2 | /* |
3 | * LinkStation power off restart driver |
4 | * Copyright (C) 2020 Daniel González Cabanelas <dgcbueu@gmail.com> |
5 | */ |
6 | |
7 | #include <linux/module.h> |
8 | #include <linux/notifier.h> |
9 | #include <linux/of.h> |
10 | #include <linux/of_mdio.h> |
11 | #include <linux/of_platform.h> |
12 | #include <linux/reboot.h> |
13 | #include <linux/phy.h> |
14 | |
15 | /* Defines from the eth phy Marvell driver */ |
16 | #define MII_MARVELL_COPPER_PAGE 0 |
17 | #define MII_MARVELL_LED_PAGE 3 |
18 | #define MII_MARVELL_WOL_PAGE 17 |
19 | #define MII_MARVELL_PHY_PAGE 22 |
20 | |
21 | #define MII_PHY_LED_CTRL 16 |
22 | #define MII_PHY_LED_POL_CTRL 17 |
23 | #define MII_88E1318S_PHY_LED_TCR 18 |
24 | #define MII_88E1318S_PHY_WOL_CTRL 16 |
25 | #define MII_M1011_IEVENT 19 |
26 | |
27 | #define MII_88E1318S_PHY_LED_TCR_INTn_ENABLE BIT(7) |
28 | #define MII_88E1318S_PHY_LED_TCR_FORCE_INT BIT(15) |
29 | #define MII_88E1318S_PHY_WOL_CTRL_CLEAR_WOL_STATUS BIT(12) |
30 | #define LED2_FORCE_ON (0x8 << 8) |
31 | #define LEDMASK GENMASK(11,8) |
32 | |
33 | #define MII_88E1318S_PHY_LED_POL_LED2 BIT(4) |
34 | |
35 | struct power_off_cfg { |
36 | char *mdio_node_name; |
37 | void (*phy_set_reg)(bool restart); |
38 | }; |
39 | |
40 | static struct phy_device *phydev; |
41 | static const struct power_off_cfg *cfg; |
42 | |
43 | static void linkstation_mvphy_reg_intn(bool restart) |
44 | { |
45 | int rc = 0, saved_page; |
46 | u16 data = 0; |
47 | |
48 | if (restart) |
49 | data = MII_88E1318S_PHY_LED_TCR_FORCE_INT; |
50 | |
51 | saved_page = phy_select_page(phydev, MII_MARVELL_LED_PAGE); |
52 | if (saved_page < 0) |
53 | goto err; |
54 | |
55 | /* Force manual LED2 control to let INTn work */ |
56 | __phy_modify(phydev, MII_PHY_LED_CTRL, LEDMASK, LED2_FORCE_ON); |
57 | |
58 | /* Set the LED[2]/INTn pin to the required state */ |
59 | __phy_modify(phydev, MII_88E1318S_PHY_LED_TCR, |
60 | MII_88E1318S_PHY_LED_TCR_FORCE_INT, |
61 | MII_88E1318S_PHY_LED_TCR_INTn_ENABLE | data); |
62 | |
63 | if (!data) { |
64 | /* Clear interrupts to ensure INTn won't be holded in high state */ |
65 | __phy_write(phydev, MII_MARVELL_PHY_PAGE, MII_MARVELL_COPPER_PAGE); |
66 | __phy_read(phydev, MII_M1011_IEVENT); |
67 | |
68 | /* If WOL was enabled and a magic packet was received before powering |
69 | * off, we won't be able to wake up by sending another magic packet. |
70 | * Clear WOL status. |
71 | */ |
72 | __phy_write(phydev, MII_MARVELL_PHY_PAGE, MII_MARVELL_WOL_PAGE); |
73 | __phy_set_bits(phydev, MII_88E1318S_PHY_WOL_CTRL, |
74 | MII_88E1318S_PHY_WOL_CTRL_CLEAR_WOL_STATUS); |
75 | } |
76 | err: |
77 | rc = phy_restore_page(phydev, oldpage: saved_page, ret: rc); |
78 | if (rc < 0) |
79 | dev_err(&phydev->mdio.dev, "Write register failed, %d\n" , rc); |
80 | } |
81 | |
82 | static void readynas_mvphy_set_reg(bool restart) |
83 | { |
84 | int rc = 0, saved_page; |
85 | u16 data = 0; |
86 | |
87 | if (restart) |
88 | data = MII_88E1318S_PHY_LED_POL_LED2; |
89 | |
90 | saved_page = phy_select_page(phydev, MII_MARVELL_LED_PAGE); |
91 | if (saved_page < 0) |
92 | goto err; |
93 | |
94 | /* Set the LED[2].0 Polarity bit to the required state */ |
95 | __phy_modify(phydev, MII_PHY_LED_POL_CTRL, |
96 | MII_88E1318S_PHY_LED_POL_LED2, set: data); |
97 | |
98 | if (!data) { |
99 | /* If WOL was enabled and a magic packet was received before powering |
100 | * off, we won't be able to wake up by sending another magic packet. |
101 | * Clear WOL status. |
102 | */ |
103 | __phy_write(phydev, MII_MARVELL_PHY_PAGE, MII_MARVELL_WOL_PAGE); |
104 | __phy_set_bits(phydev, MII_88E1318S_PHY_WOL_CTRL, |
105 | MII_88E1318S_PHY_WOL_CTRL_CLEAR_WOL_STATUS); |
106 | } |
107 | err: |
108 | rc = phy_restore_page(phydev, oldpage: saved_page, ret: rc); |
109 | if (rc < 0) |
110 | dev_err(&phydev->mdio.dev, "Write register failed, %d\n" , rc); |
111 | } |
112 | |
113 | static const struct power_off_cfg linkstation_power_off_cfg = { |
114 | .mdio_node_name = "mdio" , |
115 | .phy_set_reg = linkstation_mvphy_reg_intn, |
116 | }; |
117 | |
118 | static const struct power_off_cfg readynas_power_off_cfg = { |
119 | .mdio_node_name = "mdio-bus" , |
120 | .phy_set_reg = readynas_mvphy_set_reg, |
121 | }; |
122 | |
123 | static int linkstation_reboot_notifier(struct notifier_block *nb, |
124 | unsigned long action, void *unused) |
125 | { |
126 | if (action == SYS_RESTART) |
127 | cfg->phy_set_reg(true); |
128 | |
129 | return NOTIFY_DONE; |
130 | } |
131 | |
132 | static struct notifier_block linkstation_reboot_nb = { |
133 | .notifier_call = linkstation_reboot_notifier, |
134 | }; |
135 | |
136 | static void linkstation_poweroff(void) |
137 | { |
138 | unregister_reboot_notifier(&linkstation_reboot_nb); |
139 | cfg->phy_set_reg(false); |
140 | |
141 | kernel_restart(cmd: "Power off" ); |
142 | } |
143 | |
144 | static const struct of_device_id ls_poweroff_of_match[] = { |
145 | { .compatible = "buffalo,ls421d" , |
146 | .data = &linkstation_power_off_cfg, |
147 | }, |
148 | { .compatible = "buffalo,ls421de" , |
149 | .data = &linkstation_power_off_cfg, |
150 | }, |
151 | { .compatible = "netgear,readynas-duo-v2" , |
152 | .data = &readynas_power_off_cfg, |
153 | }, |
154 | { }, |
155 | }; |
156 | |
157 | static int __init linkstation_poweroff_init(void) |
158 | { |
159 | struct mii_bus *bus; |
160 | struct device_node *dn; |
161 | const struct of_device_id *match; |
162 | |
163 | dn = of_find_matching_node(NULL, matches: ls_poweroff_of_match); |
164 | if (!dn) |
165 | return -ENODEV; |
166 | of_node_put(node: dn); |
167 | |
168 | match = of_match_node(matches: ls_poweroff_of_match, node: dn); |
169 | cfg = match->data; |
170 | |
171 | dn = of_find_node_by_name(NULL, name: cfg->mdio_node_name); |
172 | if (!dn) |
173 | return -ENODEV; |
174 | |
175 | bus = of_mdio_find_bus(mdio_np: dn); |
176 | of_node_put(node: dn); |
177 | if (!bus) |
178 | return -EPROBE_DEFER; |
179 | |
180 | phydev = phy_find_first(bus); |
181 | put_device(dev: &bus->dev); |
182 | if (!phydev) |
183 | return -EPROBE_DEFER; |
184 | |
185 | register_reboot_notifier(&linkstation_reboot_nb); |
186 | pm_power_off = linkstation_poweroff; |
187 | |
188 | return 0; |
189 | } |
190 | |
191 | static void __exit linkstation_poweroff_exit(void) |
192 | { |
193 | pm_power_off = NULL; |
194 | unregister_reboot_notifier(&linkstation_reboot_nb); |
195 | } |
196 | |
197 | module_init(linkstation_poweroff_init); |
198 | module_exit(linkstation_poweroff_exit); |
199 | |
200 | MODULE_AUTHOR("Daniel González Cabanelas <dgcbueu@gmail.com>" ); |
201 | MODULE_DESCRIPTION("LinkStation power off driver" ); |
202 | MODULE_LICENSE("GPL v2" ); |
203 | |