1 | // SPDX-License-Identifier: GPL-2.0-or-later |
2 | /* |
3 | * Hardware monitoring driver for FSP 3Y-Power PSUs |
4 | * |
5 | * Copyright (c) 2021 Václav Kubernát, CESNET |
6 | * |
7 | * This driver is mostly reverse engineered with the help of a tool called pmbus_peek written by |
8 | * David Brownell (and later adopted by Jan Kundrát). The device has some sort of a timing issue |
9 | * when switching pages, details are explained in the code. The driver support is limited. It |
10 | * exposes only the values, that have been tested to work correctly. Unsupported values either |
11 | * aren't supported by the devices or their encondings are unknown. |
12 | */ |
13 | |
14 | #include <linux/delay.h> |
15 | #include <linux/i2c.h> |
16 | #include <linux/kernel.h> |
17 | #include <linux/module.h> |
18 | #include "pmbus.h" |
19 | |
20 | #define YM2151_PAGE_12V_LOG 0x00 |
21 | #define YM2151_PAGE_12V_REAL 0x00 |
22 | #define YM2151_PAGE_5VSB_LOG 0x01 |
23 | #define YM2151_PAGE_5VSB_REAL 0x20 |
24 | #define YH5151E_PAGE_12V_LOG 0x00 |
25 | #define YH5151E_PAGE_12V_REAL 0x00 |
26 | #define YH5151E_PAGE_5V_LOG 0x01 |
27 | #define YH5151E_PAGE_5V_REAL 0x10 |
28 | #define YH5151E_PAGE_3V3_LOG 0x02 |
29 | #define YH5151E_PAGE_3V3_REAL 0x11 |
30 | |
31 | enum chips { |
32 | ym2151e, |
33 | yh5151e |
34 | }; |
35 | |
36 | struct fsp3y_data { |
37 | struct pmbus_driver_info info; |
38 | int chip; |
39 | int page; |
40 | |
41 | bool vout_linear_11; |
42 | }; |
43 | |
44 | #define to_fsp3y_data(x) container_of(x, struct fsp3y_data, info) |
45 | |
46 | static int page_log_to_page_real(int page_log, enum chips chip) |
47 | { |
48 | switch (chip) { |
49 | case ym2151e: |
50 | switch (page_log) { |
51 | case YM2151_PAGE_12V_LOG: |
52 | return YM2151_PAGE_12V_REAL; |
53 | case YM2151_PAGE_5VSB_LOG: |
54 | return YM2151_PAGE_5VSB_REAL; |
55 | } |
56 | return -EINVAL; |
57 | case yh5151e: |
58 | switch (page_log) { |
59 | case YH5151E_PAGE_12V_LOG: |
60 | return YH5151E_PAGE_12V_REAL; |
61 | case YH5151E_PAGE_5V_LOG: |
62 | return YH5151E_PAGE_5V_REAL; |
63 | case YH5151E_PAGE_3V3_LOG: |
64 | return YH5151E_PAGE_3V3_REAL; |
65 | } |
66 | return -EINVAL; |
67 | } |
68 | |
69 | return -EINVAL; |
70 | } |
71 | |
72 | static int set_page(struct i2c_client *client, int page_log) |
73 | { |
74 | const struct pmbus_driver_info *info = pmbus_get_driver_info(client); |
75 | struct fsp3y_data *data = to_fsp3y_data(info); |
76 | int rv; |
77 | int page_real; |
78 | |
79 | if (page_log < 0) |
80 | return 0; |
81 | |
82 | page_real = page_log_to_page_real(page_log, chip: data->chip); |
83 | if (page_real < 0) |
84 | return page_real; |
85 | |
86 | if (data->page != page_real) { |
87 | rv = i2c_smbus_write_byte_data(client, command: PMBUS_PAGE, value: page_real); |
88 | if (rv < 0) |
89 | return rv; |
90 | |
91 | data->page = page_real; |
92 | |
93 | /* |
94 | * Testing showed that the device has a timing issue. After |
95 | * setting a page, it takes a while, before the device actually |
96 | * gives the correct values from the correct page. 20 ms was |
97 | * tested to be enough to not give wrong values (15 ms wasn't |
98 | * enough). |
99 | */ |
100 | usleep_range(min: 20000, max: 30000); |
101 | } |
102 | |
103 | return 0; |
104 | } |
105 | |
106 | static int fsp3y_read_byte_data(struct i2c_client *client, int page, int reg) |
107 | { |
108 | const struct pmbus_driver_info *info = pmbus_get_driver_info(client); |
109 | struct fsp3y_data *data = to_fsp3y_data(info); |
110 | int rv; |
111 | |
112 | /* |
113 | * Inject an exponent for non-compliant YH5151-E. |
114 | */ |
115 | if (data->vout_linear_11 && reg == PMBUS_VOUT_MODE) |
116 | return 0x1A; |
117 | |
118 | rv = set_page(client, page_log: page); |
119 | if (rv < 0) |
120 | return rv; |
121 | |
122 | return i2c_smbus_read_byte_data(client, command: reg); |
123 | } |
124 | |
125 | static int fsp3y_read_word_data(struct i2c_client *client, int page, int phase, int reg) |
126 | { |
127 | const struct pmbus_driver_info *info = pmbus_get_driver_info(client); |
128 | struct fsp3y_data *data = to_fsp3y_data(info); |
129 | int rv; |
130 | |
131 | /* |
132 | * This masks commands which weren't tested to work correctly. Some of |
133 | * the masked commands return 0xFFFF. These would probably get tagged as |
134 | * invalid by pmbus_core. Other ones do return values which might be |
135 | * useful (that is, they are not 0xFFFF), but their encoding is unknown, |
136 | * and so they are unsupported. |
137 | */ |
138 | switch (reg) { |
139 | case PMBUS_READ_FAN_SPEED_1: |
140 | case PMBUS_READ_IIN: |
141 | case PMBUS_READ_IOUT: |
142 | case PMBUS_READ_PIN: |
143 | case PMBUS_READ_POUT: |
144 | case PMBUS_READ_TEMPERATURE_1: |
145 | case PMBUS_READ_TEMPERATURE_2: |
146 | case PMBUS_READ_TEMPERATURE_3: |
147 | case PMBUS_READ_VIN: |
148 | case PMBUS_READ_VOUT: |
149 | case PMBUS_STATUS_WORD: |
150 | break; |
151 | default: |
152 | return -ENXIO; |
153 | } |
154 | |
155 | rv = set_page(client, page_log: page); |
156 | if (rv < 0) |
157 | return rv; |
158 | |
159 | rv = i2c_smbus_read_word_data(client, command: reg); |
160 | if (rv < 0) |
161 | return rv; |
162 | |
163 | /* |
164 | * Handle YH-5151E non-compliant linear11 vout voltage. |
165 | */ |
166 | if (data->vout_linear_11 && reg == PMBUS_READ_VOUT) |
167 | rv = sign_extend32(value: rv, index: 10) & 0xffff; |
168 | |
169 | return rv; |
170 | } |
171 | |
172 | static struct pmbus_driver_info fsp3y_info[] = { |
173 | [ym2151e] = { |
174 | .pages = 2, |
175 | .func[YM2151_PAGE_12V_LOG] = |
176 | PMBUS_HAVE_VOUT | PMBUS_HAVE_IOUT | |
177 | PMBUS_HAVE_PIN | PMBUS_HAVE_POUT | |
178 | PMBUS_HAVE_TEMP | PMBUS_HAVE_TEMP2 | |
179 | PMBUS_HAVE_VIN | PMBUS_HAVE_IIN | |
180 | PMBUS_HAVE_FAN12, |
181 | .func[YM2151_PAGE_5VSB_LOG] = |
182 | PMBUS_HAVE_VOUT | PMBUS_HAVE_IOUT, |
183 | .read_word_data = fsp3y_read_word_data, |
184 | .read_byte_data = fsp3y_read_byte_data, |
185 | }, |
186 | [yh5151e] = { |
187 | .pages = 3, |
188 | .func[YH5151E_PAGE_12V_LOG] = |
189 | PMBUS_HAVE_VOUT | PMBUS_HAVE_IOUT | |
190 | PMBUS_HAVE_POUT | |
191 | PMBUS_HAVE_TEMP | PMBUS_HAVE_TEMP2 | PMBUS_HAVE_TEMP3, |
192 | .func[YH5151E_PAGE_5V_LOG] = |
193 | PMBUS_HAVE_VOUT | PMBUS_HAVE_IOUT | |
194 | PMBUS_HAVE_POUT, |
195 | .func[YH5151E_PAGE_3V3_LOG] = |
196 | PMBUS_HAVE_VOUT | PMBUS_HAVE_IOUT | |
197 | PMBUS_HAVE_POUT, |
198 | .read_word_data = fsp3y_read_word_data, |
199 | .read_byte_data = fsp3y_read_byte_data, |
200 | } |
201 | }; |
202 | |
203 | static int fsp3y_detect(struct i2c_client *client) |
204 | { |
205 | int rv; |
206 | u8 buf[I2C_SMBUS_BLOCK_MAX + 1]; |
207 | |
208 | rv = i2c_smbus_read_block_data(client, command: PMBUS_MFR_MODEL, values: buf); |
209 | if (rv < 0) |
210 | return rv; |
211 | |
212 | buf[rv] = '\0'; |
213 | |
214 | if (rv == 8) { |
215 | if (!strcmp(buf, "YM-2151E" )) |
216 | return ym2151e; |
217 | else if (!strcmp(buf, "YH-5151E" )) |
218 | return yh5151e; |
219 | } |
220 | |
221 | dev_err(&client->dev, "Unsupported model %.*s\n" , rv, buf); |
222 | return -ENODEV; |
223 | } |
224 | |
225 | static const struct i2c_device_id fsp3y_id[] = { |
226 | {"ym2151e" , ym2151e}, |
227 | {"yh5151e" , yh5151e}, |
228 | { } |
229 | }; |
230 | |
231 | static int fsp3y_probe(struct i2c_client *client) |
232 | { |
233 | struct fsp3y_data *data; |
234 | const struct i2c_device_id *id; |
235 | int rv; |
236 | |
237 | data = devm_kzalloc(dev: &client->dev, size: sizeof(struct fsp3y_data), GFP_KERNEL); |
238 | if (!data) |
239 | return -ENOMEM; |
240 | |
241 | data->chip = fsp3y_detect(client); |
242 | if (data->chip < 0) |
243 | return data->chip; |
244 | |
245 | id = i2c_match_id(id: fsp3y_id, client); |
246 | if (data->chip != id->driver_data) |
247 | dev_warn(&client->dev, "Device mismatch: Configured %s (%d), detected %d\n" , |
248 | id->name, (int)id->driver_data, data->chip); |
249 | |
250 | rv = i2c_smbus_read_byte_data(client, command: PMBUS_PAGE); |
251 | if (rv < 0) |
252 | return rv; |
253 | data->page = rv; |
254 | |
255 | data->info = fsp3y_info[data->chip]; |
256 | |
257 | /* |
258 | * YH-5151E sometimes reports vout in linear11 and sometimes in |
259 | * linear16. This depends on the exact individual piece of hardware. One |
260 | * YH-5151E can use linear16 and another might use linear11 instead. |
261 | * |
262 | * The format can be recognized by reading VOUT_MODE - if it doesn't |
263 | * report a valid exponent, then vout uses linear11. Otherwise, the |
264 | * device is compliant and uses linear16. |
265 | */ |
266 | data->vout_linear_11 = false; |
267 | if (data->chip == yh5151e) { |
268 | rv = i2c_smbus_read_byte_data(client, command: PMBUS_VOUT_MODE); |
269 | if (rv < 0) |
270 | return rv; |
271 | |
272 | if (rv == 0xFF) |
273 | data->vout_linear_11 = true; |
274 | } |
275 | |
276 | return pmbus_do_probe(client, info: &data->info); |
277 | } |
278 | |
279 | MODULE_DEVICE_TABLE(i2c, fsp3y_id); |
280 | |
281 | static struct i2c_driver fsp3y_driver = { |
282 | .driver = { |
283 | .name = "fsp3y" , |
284 | }, |
285 | .probe = fsp3y_probe, |
286 | .id_table = fsp3y_id |
287 | }; |
288 | |
289 | module_i2c_driver(fsp3y_driver); |
290 | |
291 | MODULE_AUTHOR("Václav Kubernát" ); |
292 | MODULE_DESCRIPTION("PMBus driver for FSP/3Y-Power power supplies" ); |
293 | MODULE_LICENSE("GPL" ); |
294 | MODULE_IMPORT_NS(PMBUS); |
295 | |