1 | // SPDX-License-Identifier: GPL-2.0 |
---|---|
2 | /* |
3 | * CZ.NIC's Turris Omnia MCU driver |
4 | * |
5 | * 2024 by Marek BehĂșn <kabel@kernel.org> |
6 | */ |
7 | |
8 | #include <linux/array_size.h> |
9 | #include <linux/bits.h> |
10 | #include <linux/device.h> |
11 | #include <linux/errno.h> |
12 | #include <linux/hex.h> |
13 | #include <linux/i2c.h> |
14 | #include <linux/module.h> |
15 | #include <linux/string.h> |
16 | #include <linux/sysfs.h> |
17 | #include <linux/types.h> |
18 | |
19 | #include <linux/turris-omnia-mcu-interface.h> |
20 | #include "turris-omnia-mcu.h" |
21 | |
22 | #define OMNIA_FW_VERSION_LEN 20 |
23 | #define OMNIA_FW_VERSION_HEX_LEN (2 * OMNIA_FW_VERSION_LEN + 1) |
24 | #define OMNIA_BOARD_INFO_LEN 16 |
25 | |
26 | int omnia_cmd_write_read(const struct i2c_client *client, |
27 | void *cmd, unsigned int cmd_len, |
28 | void *reply, unsigned int reply_len) |
29 | { |
30 | struct i2c_msg msgs[2]; |
31 | int ret, num; |
32 | |
33 | msgs[0].addr = client->addr; |
34 | msgs[0].flags = 0; |
35 | msgs[0].len = cmd_len; |
36 | msgs[0].buf = cmd; |
37 | num = 1; |
38 | |
39 | if (reply_len) { |
40 | msgs[1].addr = client->addr; |
41 | msgs[1].flags = I2C_M_RD; |
42 | msgs[1].len = reply_len; |
43 | msgs[1].buf = reply; |
44 | num++; |
45 | } |
46 | |
47 | ret = i2c_transfer(adap: client->adapter, msgs, num); |
48 | if (ret < 0) |
49 | return ret; |
50 | if (ret != num) |
51 | return -EIO; |
52 | |
53 | return 0; |
54 | } |
55 | EXPORT_SYMBOL_GPL(omnia_cmd_write_read); |
56 | |
57 | static int omnia_get_version_hash(struct omnia_mcu *mcu, bool bootloader, |
58 | char version[static OMNIA_FW_VERSION_HEX_LEN]) |
59 | { |
60 | u8 reply[OMNIA_FW_VERSION_LEN]; |
61 | char *p; |
62 | int err; |
63 | |
64 | err = omnia_cmd_read(client: mcu->client, |
65 | cmd: bootloader ? OMNIA_CMD_GET_FW_VERSION_BOOT |
66 | : OMNIA_CMD_GET_FW_VERSION_APP, |
67 | reply, len: sizeof(reply)); |
68 | if (err) |
69 | return err; |
70 | |
71 | p = bin2hex(dst: version, src: reply, OMNIA_FW_VERSION_LEN); |
72 | *p = '\0'; |
73 | |
74 | return 0; |
75 | } |
76 | |
77 | static ssize_t fw_version_hash_show(struct device *dev, char *buf, |
78 | bool bootloader) |
79 | { |
80 | struct omnia_mcu *mcu = dev_get_drvdata(dev); |
81 | char version[OMNIA_FW_VERSION_HEX_LEN]; |
82 | int err; |
83 | |
84 | err = omnia_get_version_hash(mcu, bootloader, version); |
85 | if (err) |
86 | return err; |
87 | |
88 | return sysfs_emit(buf, fmt: "%s\n", version); |
89 | } |
90 | |
91 | static ssize_t fw_version_hash_application_show(struct device *dev, |
92 | struct device_attribute *a, |
93 | char *buf) |
94 | { |
95 | return fw_version_hash_show(dev, buf, bootloader: false); |
96 | } |
97 | static DEVICE_ATTR_RO(fw_version_hash_application); |
98 | |
99 | static ssize_t fw_version_hash_bootloader_show(struct device *dev, |
100 | struct device_attribute *a, |
101 | char *buf) |
102 | { |
103 | return fw_version_hash_show(dev, buf, bootloader: true); |
104 | } |
105 | static DEVICE_ATTR_RO(fw_version_hash_bootloader); |
106 | |
107 | static ssize_t fw_features_show(struct device *dev, struct device_attribute *a, |
108 | char *buf) |
109 | { |
110 | struct omnia_mcu *mcu = dev_get_drvdata(dev); |
111 | |
112 | return sysfs_emit(buf, fmt: "0x%x\n", mcu->features); |
113 | } |
114 | static DEVICE_ATTR_RO(fw_features); |
115 | |
116 | static ssize_t mcu_type_show(struct device *dev, struct device_attribute *a, |
117 | char *buf) |
118 | { |
119 | struct omnia_mcu *mcu = dev_get_drvdata(dev); |
120 | |
121 | return sysfs_emit(buf, fmt: "%s\n", mcu->type); |
122 | } |
123 | static DEVICE_ATTR_RO(mcu_type); |
124 | |
125 | static ssize_t reset_selector_show(struct device *dev, |
126 | struct device_attribute *a, char *buf) |
127 | { |
128 | u8 reply; |
129 | int err; |
130 | |
131 | err = omnia_cmd_read_u8(to_i2c_client(dev), cmd: OMNIA_CMD_GET_RESET, |
132 | reply: &reply); |
133 | if (err) |
134 | return err; |
135 | |
136 | return sysfs_emit(buf, fmt: "%d\n", reply); |
137 | } |
138 | static DEVICE_ATTR_RO(reset_selector); |
139 | |
140 | static ssize_t serial_number_show(struct device *dev, |
141 | struct device_attribute *a, char *buf) |
142 | { |
143 | struct omnia_mcu *mcu = dev_get_drvdata(dev); |
144 | |
145 | return sysfs_emit(buf, fmt: "%016llX\n", mcu->board_serial_number); |
146 | } |
147 | static DEVICE_ATTR_RO(serial_number); |
148 | |
149 | static ssize_t first_mac_address_show(struct device *dev, |
150 | struct device_attribute *a, char *buf) |
151 | { |
152 | struct omnia_mcu *mcu = dev_get_drvdata(dev); |
153 | |
154 | return sysfs_emit(buf, fmt: "%pM\n", mcu->board_first_mac); |
155 | } |
156 | static DEVICE_ATTR_RO(first_mac_address); |
157 | |
158 | static ssize_t board_revision_show(struct device *dev, |
159 | struct device_attribute *a, char *buf) |
160 | { |
161 | struct omnia_mcu *mcu = dev_get_drvdata(dev); |
162 | |
163 | return sysfs_emit(buf, fmt: "%u\n", mcu->board_revision); |
164 | } |
165 | static DEVICE_ATTR_RO(board_revision); |
166 | |
167 | static struct attribute *omnia_mcu_base_attrs[] = { |
168 | &dev_attr_fw_version_hash_application.attr, |
169 | &dev_attr_fw_version_hash_bootloader.attr, |
170 | &dev_attr_fw_features.attr, |
171 | &dev_attr_mcu_type.attr, |
172 | &dev_attr_reset_selector.attr, |
173 | &dev_attr_serial_number.attr, |
174 | &dev_attr_first_mac_address.attr, |
175 | &dev_attr_board_revision.attr, |
176 | NULL |
177 | }; |
178 | |
179 | static umode_t omnia_mcu_base_attrs_visible(struct kobject *kobj, |
180 | struct attribute *a, int n) |
181 | { |
182 | struct device *dev = kobj_to_dev(kobj); |
183 | struct omnia_mcu *mcu = dev_get_drvdata(dev); |
184 | |
185 | if ((a == &dev_attr_serial_number.attr || |
186 | a == &dev_attr_first_mac_address.attr || |
187 | a == &dev_attr_board_revision.attr) && |
188 | !(mcu->features & OMNIA_FEAT_BOARD_INFO)) |
189 | return 0; |
190 | |
191 | return a->mode; |
192 | } |
193 | |
194 | static const struct attribute_group omnia_mcu_base_group = { |
195 | .attrs = omnia_mcu_base_attrs, |
196 | .is_visible = omnia_mcu_base_attrs_visible, |
197 | }; |
198 | |
199 | static const struct attribute_group *omnia_mcu_groups[] = { |
200 | &omnia_mcu_base_group, |
201 | #ifdef CONFIG_TURRIS_OMNIA_MCU_GPIO |
202 | &omnia_mcu_gpio_group, |
203 | #endif |
204 | #ifdef CONFIG_TURRIS_OMNIA_MCU_SYSOFF_WAKEUP |
205 | &omnia_mcu_poweroff_group, |
206 | #endif |
207 | NULL |
208 | }; |
209 | |
210 | static void omnia_mcu_print_version_hash(struct omnia_mcu *mcu, bool bootloader) |
211 | { |
212 | const char *type = bootloader ? "bootloader": "application"; |
213 | struct device *dev = &mcu->client->dev; |
214 | char version[OMNIA_FW_VERSION_HEX_LEN]; |
215 | int err; |
216 | |
217 | err = omnia_get_version_hash(mcu, bootloader, version); |
218 | if (err) { |
219 | dev_err(dev, "Cannot read MCU %s firmware version: %d\n", |
220 | type, err); |
221 | return; |
222 | } |
223 | |
224 | dev_info(dev, "MCU %s firmware version hash: %s\n", type, version); |
225 | } |
226 | |
227 | static const char *omnia_status_to_mcu_type(u16 status) |
228 | { |
229 | switch (status & OMNIA_STS_MCU_TYPE_MASK) { |
230 | case OMNIA_STS_MCU_TYPE_STM32: |
231 | return "STM32"; |
232 | case OMNIA_STS_MCU_TYPE_GD32: |
233 | return "GD32"; |
234 | case OMNIA_STS_MCU_TYPE_MKL: |
235 | return "MKL"; |
236 | default: |
237 | return "unknown"; |
238 | } |
239 | } |
240 | |
241 | static void omnia_info_missing_feature(struct device *dev, const char *feature) |
242 | { |
243 | dev_info(dev, |
244 | "Your board's MCU firmware does not support the %s feature.\n", |
245 | feature); |
246 | } |
247 | |
248 | static int omnia_mcu_read_features(struct omnia_mcu *mcu) |
249 | { |
250 | static const struct { |
251 | u16 mask; |
252 | const char *name; |
253 | } features[] = { |
254 | #define _DEF_FEAT(_n, _m) { OMNIA_FEAT_ ## _n, _m } |
255 | _DEF_FEAT(EXT_CMDS, "extended control and status"), |
256 | _DEF_FEAT(WDT_PING, "watchdog pinging"), |
257 | _DEF_FEAT(LED_STATE_EXT_MASK, "peripheral LED pins reading"), |
258 | _DEF_FEAT(NEW_INT_API, "new interrupt API"), |
259 | _DEF_FEAT(POWEROFF_WAKEUP, "poweroff and wakeup"), |
260 | _DEF_FEAT(TRNG, "true random number generator"), |
261 | _DEF_FEAT(BRIGHTNESS_INT, "LED panel brightness change interrupt"), |
262 | _DEF_FEAT(LED_GAMMA_CORRECTION, "LED gamma correction"), |
263 | #undef _DEF_FEAT |
264 | }; |
265 | struct i2c_client *client = mcu->client; |
266 | struct device *dev = &client->dev; |
267 | bool suggest_fw_upgrade = false; |
268 | u16 status; |
269 | int err; |
270 | |
271 | /* status word holds MCU type, which we need below */ |
272 | err = omnia_cmd_read_u16(client, cmd: OMNIA_CMD_GET_STATUS_WORD, dst: &status); |
273 | if (err) |
274 | return err; |
275 | |
276 | /* |
277 | * Check whether MCU firmware supports the OMNIA_CMD_GET_FEATURES |
278 | * command. |
279 | */ |
280 | if (status & OMNIA_STS_FEATURES_SUPPORTED) { |
281 | /* try read 32-bit features */ |
282 | err = omnia_cmd_read_u32(client, cmd: OMNIA_CMD_GET_FEATURES, |
283 | dst: &mcu->features); |
284 | if (err) { |
285 | /* try read 16-bit features */ |
286 | u16 features16; |
287 | |
288 | err = omnia_cmd_read_u16(client, cmd: OMNIA_CMD_GET_FEATURES, |
289 | dst: &features16); |
290 | if (err) |
291 | return err; |
292 | |
293 | mcu->features = features16; |
294 | } else { |
295 | if (mcu->features & OMNIA_FEAT_FROM_BIT_16_INVALID) |
296 | mcu->features &= GENMASK(15, 0); |
297 | } |
298 | } else { |
299 | dev_info(dev, |
300 | "Your board's MCU firmware does not support feature reading.\n"); |
301 | suggest_fw_upgrade = true; |
302 | } |
303 | |
304 | mcu->type = omnia_status_to_mcu_type(status); |
305 | dev_info(dev, "MCU type %s%s\n", mcu->type, |
306 | (mcu->features & OMNIA_FEAT_PERIPH_MCU) ? |
307 | ", with peripheral resets wired": ""); |
308 | |
309 | omnia_mcu_print_version_hash(mcu, bootloader: true); |
310 | |
311 | if (mcu->features & OMNIA_FEAT_BOOTLOADER) |
312 | dev_warn(dev, |
313 | "MCU is running bootloader firmware. Was firmware upgrade interrupted?\n"); |
314 | else |
315 | omnia_mcu_print_version_hash(mcu, bootloader: false); |
316 | |
317 | for (unsigned int i = 0; i < ARRAY_SIZE(features); i++) { |
318 | if (mcu->features & features[i].mask) |
319 | continue; |
320 | |
321 | omnia_info_missing_feature(dev, feature: features[i].name); |
322 | suggest_fw_upgrade = true; |
323 | } |
324 | |
325 | if (suggest_fw_upgrade) |
326 | dev_info(dev, |
327 | "Consider upgrading MCU firmware with the omnia-mcutool utility.\n"); |
328 | |
329 | return 0; |
330 | } |
331 | |
332 | static int omnia_mcu_read_board_info(struct omnia_mcu *mcu) |
333 | { |
334 | u8 reply[1 + OMNIA_BOARD_INFO_LEN]; |
335 | int err; |
336 | |
337 | err = omnia_cmd_read(client: mcu->client, cmd: OMNIA_CMD_BOARD_INFO_GET, reply, |
338 | len: sizeof(reply)); |
339 | if (err) |
340 | return err; |
341 | |
342 | if (reply[0] != OMNIA_BOARD_INFO_LEN) |
343 | return -EIO; |
344 | |
345 | mcu->board_serial_number = get_unaligned_le64(p: &reply[1]); |
346 | |
347 | /* we can't use ether_addr_copy() because reply is not u16-aligned */ |
348 | memcpy(mcu->board_first_mac, &reply[9], sizeof(mcu->board_first_mac)); |
349 | |
350 | mcu->board_revision = reply[15]; |
351 | |
352 | return 0; |
353 | } |
354 | |
355 | static int omnia_mcu_probe(struct i2c_client *client) |
356 | { |
357 | struct device *dev = &client->dev; |
358 | struct omnia_mcu *mcu; |
359 | int err; |
360 | |
361 | if (!client->irq) |
362 | return dev_err_probe(dev, err: -EINVAL, fmt: "IRQ resource not found\n"); |
363 | |
364 | mcu = devm_kzalloc(dev, size: sizeof(*mcu), GFP_KERNEL); |
365 | if (!mcu) |
366 | return -ENOMEM; |
367 | |
368 | mcu->client = client; |
369 | i2c_set_clientdata(client, data: mcu); |
370 | |
371 | err = omnia_mcu_read_features(mcu); |
372 | if (err) |
373 | return dev_err_probe(dev, err, |
374 | fmt: "Cannot determine MCU supported features\n"); |
375 | |
376 | if (mcu->features & OMNIA_FEAT_BOARD_INFO) { |
377 | err = omnia_mcu_read_board_info(mcu); |
378 | if (err) |
379 | return dev_err_probe(dev, err, |
380 | fmt: "Cannot read board info\n"); |
381 | } |
382 | |
383 | err = omnia_mcu_register_sys_off_and_wakeup(mcu); |
384 | if (err) |
385 | return err; |
386 | |
387 | err = omnia_mcu_register_watchdog(mcu); |
388 | if (err) |
389 | return err; |
390 | |
391 | err = omnia_mcu_register_gpiochip(mcu); |
392 | if (err) |
393 | return err; |
394 | |
395 | err = omnia_mcu_register_keyctl(mcu); |
396 | if (err) |
397 | return err; |
398 | |
399 | return omnia_mcu_register_trng(mcu); |
400 | } |
401 | |
402 | static const struct of_device_id of_omnia_mcu_match[] = { |
403 | { .compatible = "cznic,turris-omnia-mcu"}, |
404 | {} |
405 | }; |
406 | |
407 | static struct i2c_driver omnia_mcu_driver = { |
408 | .probe = omnia_mcu_probe, |
409 | .driver = { |
410 | .name = "turris-omnia-mcu", |
411 | .of_match_table = of_omnia_mcu_match, |
412 | .dev_groups = omnia_mcu_groups, |
413 | }, |
414 | }; |
415 | module_i2c_driver(omnia_mcu_driver); |
416 | |
417 | MODULE_AUTHOR("Marek Behun <kabel@kernel.org>"); |
418 | MODULE_DESCRIPTION("CZ.NIC's Turris Omnia MCU"); |
419 | MODULE_LICENSE("GPL"); |
420 |
Definitions
- omnia_cmd_write_read
- omnia_get_version_hash
- fw_version_hash_show
- fw_version_hash_application_show
- fw_version_hash_bootloader_show
- fw_features_show
- mcu_type_show
- reset_selector_show
- serial_number_show
- first_mac_address_show
- board_revision_show
- omnia_mcu_base_attrs
- omnia_mcu_base_attrs_visible
- omnia_mcu_base_group
- omnia_mcu_groups
- omnia_mcu_print_version_hash
- omnia_status_to_mcu_type
- omnia_info_missing_feature
- omnia_mcu_read_features
- omnia_mcu_read_board_info
- omnia_mcu_probe
- of_omnia_mcu_match
Improve your Profiling and Debugging skills
Find out more