1 | // SPDX-License-Identifier: GPL-2.0 |
2 | /* |
3 | * NVM helpers |
4 | * |
5 | * Copyright (C) 2020, Intel Corporation |
6 | * Author: Mika Westerberg <mika.westerberg@linux.intel.com> |
7 | */ |
8 | |
9 | #include <linux/idr.h> |
10 | #include <linux/slab.h> |
11 | #include <linux/vmalloc.h> |
12 | |
13 | #include "tb.h" |
14 | |
15 | #define NVM_MIN_SIZE SZ_32K |
16 | #define NVM_MAX_SIZE SZ_1M |
17 | #define NVM_DATA_DWORDS 16 |
18 | |
19 | /* Intel specific NVM offsets */ |
20 | #define INTEL_NVM_DEVID 0x05 |
21 | #define INTEL_NVM_VERSION 0x08 |
22 | #define INTEL_NVM_CSS 0x10 |
23 | #define INTEL_NVM_FLASH_SIZE 0x45 |
24 | |
25 | /* ASMedia specific NVM offsets */ |
26 | #define ASMEDIA_NVM_DATE 0x1c |
27 | #define ASMEDIA_NVM_VERSION 0x28 |
28 | |
29 | static DEFINE_IDA(nvm_ida); |
30 | |
31 | /** |
32 | * struct tb_nvm_vendor_ops - Vendor specific NVM operations |
33 | * @read_version: Reads out NVM version from the flash |
34 | * @validate: Validates the NVM image before update (optional) |
35 | * @write_headers: Writes headers before the rest of the image (optional) |
36 | */ |
37 | struct tb_nvm_vendor_ops { |
38 | int (*read_version)(struct tb_nvm *nvm); |
39 | int (*validate)(struct tb_nvm *nvm); |
40 | int (*)(struct tb_nvm *nvm); |
41 | }; |
42 | |
43 | /** |
44 | * struct tb_nvm_vendor - Vendor to &struct tb_nvm_vendor_ops mapping |
45 | * @vendor: Vendor ID |
46 | * @vops: Vendor specific NVM operations |
47 | * |
48 | * Maps vendor ID to NVM vendor operations. If there is no mapping then |
49 | * NVM firmware upgrade is disabled for the device. |
50 | */ |
51 | struct tb_nvm_vendor { |
52 | u16 vendor; |
53 | const struct tb_nvm_vendor_ops *vops; |
54 | }; |
55 | |
56 | static int intel_switch_nvm_version(struct tb_nvm *nvm) |
57 | { |
58 | struct tb_switch *sw = tb_to_switch(dev: nvm->dev); |
59 | u32 val, nvm_size, hdr_size; |
60 | int ret; |
61 | |
62 | /* |
63 | * If the switch is in safe-mode the only accessible portion of |
64 | * the NVM is the non-active one where userspace is expected to |
65 | * write new functional NVM. |
66 | */ |
67 | if (sw->safe_mode) |
68 | return 0; |
69 | |
70 | ret = tb_switch_nvm_read(sw, INTEL_NVM_FLASH_SIZE, buf: &val, size: sizeof(val)); |
71 | if (ret) |
72 | return ret; |
73 | |
74 | hdr_size = sw->generation < 3 ? SZ_8K : SZ_16K; |
75 | nvm_size = (SZ_1M << (val & 7)) / 8; |
76 | nvm_size = (nvm_size - hdr_size) / 2; |
77 | |
78 | ret = tb_switch_nvm_read(sw, INTEL_NVM_VERSION, buf: &val, size: sizeof(val)); |
79 | if (ret) |
80 | return ret; |
81 | |
82 | nvm->major = (val >> 16) & 0xff; |
83 | nvm->minor = (val >> 8) & 0xff; |
84 | nvm->active_size = nvm_size; |
85 | |
86 | return 0; |
87 | } |
88 | |
89 | static int intel_switch_nvm_validate(struct tb_nvm *nvm) |
90 | { |
91 | struct tb_switch *sw = tb_to_switch(dev: nvm->dev); |
92 | unsigned int image_size, hdr_size; |
93 | u16 ds_size, device_id; |
94 | u8 *buf = nvm->buf; |
95 | |
96 | image_size = nvm->buf_data_size; |
97 | |
98 | /* |
99 | * FARB pointer must point inside the image and must at least |
100 | * contain parts of the digital section we will be reading here. |
101 | */ |
102 | hdr_size = (*(u32 *)buf) & 0xffffff; |
103 | if (hdr_size + INTEL_NVM_DEVID + 2 >= image_size) |
104 | return -EINVAL; |
105 | |
106 | /* Digital section start should be aligned to 4k page */ |
107 | if (!IS_ALIGNED(hdr_size, SZ_4K)) |
108 | return -EINVAL; |
109 | |
110 | /* |
111 | * Read digital section size and check that it also fits inside |
112 | * the image. |
113 | */ |
114 | ds_size = *(u16 *)(buf + hdr_size); |
115 | if (ds_size >= image_size) |
116 | return -EINVAL; |
117 | |
118 | if (sw->safe_mode) |
119 | return 0; |
120 | |
121 | /* |
122 | * Make sure the device ID in the image matches the one |
123 | * we read from the switch config space. |
124 | */ |
125 | device_id = *(u16 *)(buf + hdr_size + INTEL_NVM_DEVID); |
126 | if (device_id != sw->config.device_id) |
127 | return -EINVAL; |
128 | |
129 | /* Skip headers in the image */ |
130 | nvm->buf_data_start = buf + hdr_size; |
131 | nvm->buf_data_size = image_size - hdr_size; |
132 | |
133 | return 0; |
134 | } |
135 | |
136 | static int (struct tb_nvm *nvm) |
137 | { |
138 | struct tb_switch *sw = tb_to_switch(dev: nvm->dev); |
139 | |
140 | if (sw->generation < 3) { |
141 | int ret; |
142 | |
143 | /* Write CSS headers first */ |
144 | ret = dma_port_flash_write(dma: sw->dma_port, |
145 | DMA_PORT_CSS_ADDRESS, buf: nvm->buf + INTEL_NVM_CSS, |
146 | DMA_PORT_CSS_MAX_SIZE); |
147 | if (ret) |
148 | return ret; |
149 | } |
150 | |
151 | return 0; |
152 | } |
153 | |
154 | static const struct tb_nvm_vendor_ops intel_switch_nvm_ops = { |
155 | .read_version = intel_switch_nvm_version, |
156 | .validate = intel_switch_nvm_validate, |
157 | .write_headers = intel_switch_nvm_write_headers, |
158 | }; |
159 | |
160 | static int asmedia_switch_nvm_version(struct tb_nvm *nvm) |
161 | { |
162 | struct tb_switch *sw = tb_to_switch(dev: nvm->dev); |
163 | u32 val; |
164 | int ret; |
165 | |
166 | ret = tb_switch_nvm_read(sw, ASMEDIA_NVM_VERSION, buf: &val, size: sizeof(val)); |
167 | if (ret) |
168 | return ret; |
169 | |
170 | nvm->major = (val << 16) & 0xff0000; |
171 | nvm->major |= val & 0x00ff00; |
172 | nvm->major |= (val >> 16) & 0x0000ff; |
173 | |
174 | ret = tb_switch_nvm_read(sw, ASMEDIA_NVM_DATE, buf: &val, size: sizeof(val)); |
175 | if (ret) |
176 | return ret; |
177 | |
178 | nvm->minor = (val << 16) & 0xff0000; |
179 | nvm->minor |= val & 0x00ff00; |
180 | nvm->minor |= (val >> 16) & 0x0000ff; |
181 | |
182 | /* ASMedia NVM size is fixed to 512k */ |
183 | nvm->active_size = SZ_512K; |
184 | |
185 | return 0; |
186 | } |
187 | |
188 | static const struct tb_nvm_vendor_ops asmedia_switch_nvm_ops = { |
189 | .read_version = asmedia_switch_nvm_version, |
190 | }; |
191 | |
192 | /* Router vendor NVM support table */ |
193 | static const struct tb_nvm_vendor switch_nvm_vendors[] = { |
194 | { 0x174c, &asmedia_switch_nvm_ops }, |
195 | { PCI_VENDOR_ID_INTEL, &intel_switch_nvm_ops }, |
196 | { 0x8087, &intel_switch_nvm_ops }, |
197 | }; |
198 | |
199 | static int intel_retimer_nvm_version(struct tb_nvm *nvm) |
200 | { |
201 | struct tb_retimer *rt = tb_to_retimer(dev: nvm->dev); |
202 | u32 val, nvm_size; |
203 | int ret; |
204 | |
205 | ret = tb_retimer_nvm_read(rt, INTEL_NVM_VERSION, buf: &val, size: sizeof(val)); |
206 | if (ret) |
207 | return ret; |
208 | |
209 | nvm->major = (val >> 16) & 0xff; |
210 | nvm->minor = (val >> 8) & 0xff; |
211 | |
212 | ret = tb_retimer_nvm_read(rt, INTEL_NVM_FLASH_SIZE, buf: &val, size: sizeof(val)); |
213 | if (ret) |
214 | return ret; |
215 | |
216 | nvm_size = (SZ_1M << (val & 7)) / 8; |
217 | nvm_size = (nvm_size - SZ_16K) / 2; |
218 | nvm->active_size = nvm_size; |
219 | |
220 | return 0; |
221 | } |
222 | |
223 | static int intel_retimer_nvm_validate(struct tb_nvm *nvm) |
224 | { |
225 | struct tb_retimer *rt = tb_to_retimer(dev: nvm->dev); |
226 | unsigned int image_size, hdr_size; |
227 | u8 *buf = nvm->buf; |
228 | u16 ds_size, device; |
229 | |
230 | image_size = nvm->buf_data_size; |
231 | |
232 | /* |
233 | * FARB pointer must point inside the image and must at least |
234 | * contain parts of the digital section we will be reading here. |
235 | */ |
236 | hdr_size = (*(u32 *)buf) & 0xffffff; |
237 | if (hdr_size + INTEL_NVM_DEVID + 2 >= image_size) |
238 | return -EINVAL; |
239 | |
240 | /* Digital section start should be aligned to 4k page */ |
241 | if (!IS_ALIGNED(hdr_size, SZ_4K)) |
242 | return -EINVAL; |
243 | |
244 | /* |
245 | * Read digital section size and check that it also fits inside |
246 | * the image. |
247 | */ |
248 | ds_size = *(u16 *)(buf + hdr_size); |
249 | if (ds_size >= image_size) |
250 | return -EINVAL; |
251 | |
252 | /* |
253 | * Make sure the device ID in the image matches the retimer |
254 | * hardware. |
255 | */ |
256 | device = *(u16 *)(buf + hdr_size + INTEL_NVM_DEVID); |
257 | if (device != rt->device) |
258 | return -EINVAL; |
259 | |
260 | /* Skip headers in the image */ |
261 | nvm->buf_data_start = buf + hdr_size; |
262 | nvm->buf_data_size = image_size - hdr_size; |
263 | |
264 | return 0; |
265 | } |
266 | |
267 | static const struct tb_nvm_vendor_ops intel_retimer_nvm_ops = { |
268 | .read_version = intel_retimer_nvm_version, |
269 | .validate = intel_retimer_nvm_validate, |
270 | }; |
271 | |
272 | /* Retimer vendor NVM support table */ |
273 | static const struct tb_nvm_vendor retimer_nvm_vendors[] = { |
274 | { 0x8087, &intel_retimer_nvm_ops }, |
275 | }; |
276 | |
277 | /** |
278 | * tb_nvm_alloc() - Allocate new NVM structure |
279 | * @dev: Device owning the NVM |
280 | * |
281 | * Allocates new NVM structure with unique @id and returns it. In case |
282 | * of error returns ERR_PTR(). Specifically returns %-EOPNOTSUPP if the |
283 | * NVM format of the @dev is not known by the kernel. |
284 | */ |
285 | struct tb_nvm *tb_nvm_alloc(struct device *dev) |
286 | { |
287 | const struct tb_nvm_vendor_ops *vops = NULL; |
288 | struct tb_nvm *nvm; |
289 | int ret, i; |
290 | |
291 | if (tb_is_switch(dev)) { |
292 | const struct tb_switch *sw = tb_to_switch(dev); |
293 | |
294 | for (i = 0; i < ARRAY_SIZE(switch_nvm_vendors); i++) { |
295 | const struct tb_nvm_vendor *v = &switch_nvm_vendors[i]; |
296 | |
297 | if (v->vendor == sw->config.vendor_id) { |
298 | vops = v->vops; |
299 | break; |
300 | } |
301 | } |
302 | |
303 | if (!vops) { |
304 | tb_sw_dbg(sw, "router NVM format of vendor %#x unknown\n" , |
305 | sw->config.vendor_id); |
306 | return ERR_PTR(error: -EOPNOTSUPP); |
307 | } |
308 | } else if (tb_is_retimer(dev)) { |
309 | const struct tb_retimer *rt = tb_to_retimer(dev); |
310 | |
311 | for (i = 0; i < ARRAY_SIZE(retimer_nvm_vendors); i++) { |
312 | const struct tb_nvm_vendor *v = &retimer_nvm_vendors[i]; |
313 | |
314 | if (v->vendor == rt->vendor) { |
315 | vops = v->vops; |
316 | break; |
317 | } |
318 | } |
319 | |
320 | if (!vops) { |
321 | dev_dbg(dev, "retimer NVM format of vendor %#x unknown\n" , |
322 | rt->vendor); |
323 | return ERR_PTR(error: -EOPNOTSUPP); |
324 | } |
325 | } else { |
326 | return ERR_PTR(error: -EOPNOTSUPP); |
327 | } |
328 | |
329 | nvm = kzalloc(size: sizeof(*nvm), GFP_KERNEL); |
330 | if (!nvm) |
331 | return ERR_PTR(error: -ENOMEM); |
332 | |
333 | ret = ida_alloc(ida: &nvm_ida, GFP_KERNEL); |
334 | if (ret < 0) { |
335 | kfree(objp: nvm); |
336 | return ERR_PTR(error: ret); |
337 | } |
338 | |
339 | nvm->id = ret; |
340 | nvm->dev = dev; |
341 | nvm->vops = vops; |
342 | |
343 | return nvm; |
344 | } |
345 | |
346 | /** |
347 | * tb_nvm_read_version() - Read and populate NVM version |
348 | * @nvm: NVM structure |
349 | * |
350 | * Uses vendor specific means to read out and fill in the existing |
351 | * active NVM version. Returns %0 in case of success and negative errno |
352 | * otherwise. |
353 | */ |
354 | int tb_nvm_read_version(struct tb_nvm *nvm) |
355 | { |
356 | const struct tb_nvm_vendor_ops *vops = nvm->vops; |
357 | |
358 | if (vops && vops->read_version) |
359 | return vops->read_version(nvm); |
360 | |
361 | return -EOPNOTSUPP; |
362 | } |
363 | |
364 | /** |
365 | * tb_nvm_validate() - Validate new NVM image |
366 | * @nvm: NVM structure |
367 | * |
368 | * Runs vendor specific validation over the new NVM image and if all |
369 | * checks pass returns %0. As side effect updates @nvm->buf_data_start |
370 | * and @nvm->buf_data_size fields to match the actual data to be written |
371 | * to the NVM. |
372 | * |
373 | * If the validation does not pass then returns negative errno. |
374 | */ |
375 | int tb_nvm_validate(struct tb_nvm *nvm) |
376 | { |
377 | const struct tb_nvm_vendor_ops *vops = nvm->vops; |
378 | unsigned int image_size; |
379 | u8 *buf = nvm->buf; |
380 | |
381 | if (!buf) |
382 | return -EINVAL; |
383 | if (!vops) |
384 | return -EOPNOTSUPP; |
385 | |
386 | /* Just do basic image size checks */ |
387 | image_size = nvm->buf_data_size; |
388 | if (image_size < NVM_MIN_SIZE || image_size > NVM_MAX_SIZE) |
389 | return -EINVAL; |
390 | |
391 | /* |
392 | * Set the default data start in the buffer. The validate method |
393 | * below can change this if needed. |
394 | */ |
395 | nvm->buf_data_start = buf; |
396 | |
397 | return vops->validate ? vops->validate(nvm) : 0; |
398 | } |
399 | |
400 | /** |
401 | * tb_nvm_write_headers() - Write headers before the rest of the image |
402 | * @nvm: NVM structure |
403 | * |
404 | * If the vendor NVM format requires writing headers before the rest of |
405 | * the image, this function does that. Can be called even if the device |
406 | * does not need this. |
407 | * |
408 | * Returns %0 in case of success and negative errno otherwise. |
409 | */ |
410 | int (struct tb_nvm *nvm) |
411 | { |
412 | const struct tb_nvm_vendor_ops *vops = nvm->vops; |
413 | |
414 | return vops->write_headers ? vops->write_headers(nvm) : 0; |
415 | } |
416 | |
417 | /** |
418 | * tb_nvm_add_active() - Adds active NVMem device to NVM |
419 | * @nvm: NVM structure |
420 | * @reg_read: Pointer to the function to read the NVM (passed directly to the |
421 | * NVMem device) |
422 | * |
423 | * Registers new active NVmem device for @nvm. The @reg_read is called |
424 | * directly from NVMem so it must handle possible concurrent access if |
425 | * needed. The first parameter passed to @reg_read is @nvm structure. |
426 | * Returns %0 in success and negative errno otherwise. |
427 | */ |
428 | int tb_nvm_add_active(struct tb_nvm *nvm, nvmem_reg_read_t reg_read) |
429 | { |
430 | struct nvmem_config config; |
431 | struct nvmem_device *nvmem; |
432 | |
433 | memset(&config, 0, sizeof(config)); |
434 | |
435 | config.name = "nvm_active" ; |
436 | config.reg_read = reg_read; |
437 | config.read_only = true; |
438 | config.id = nvm->id; |
439 | config.stride = 4; |
440 | config.word_size = 4; |
441 | config.size = nvm->active_size; |
442 | config.dev = nvm->dev; |
443 | config.owner = THIS_MODULE; |
444 | config.priv = nvm; |
445 | |
446 | nvmem = nvmem_register(cfg: &config); |
447 | if (IS_ERR(ptr: nvmem)) |
448 | return PTR_ERR(ptr: nvmem); |
449 | |
450 | nvm->active = nvmem; |
451 | return 0; |
452 | } |
453 | |
454 | /** |
455 | * tb_nvm_write_buf() - Write data to @nvm buffer |
456 | * @nvm: NVM structure |
457 | * @offset: Offset where to write the data |
458 | * @val: Data buffer to write |
459 | * @bytes: Number of bytes to write |
460 | * |
461 | * Helper function to cache the new NVM image before it is actually |
462 | * written to the flash. Copies @bytes from @val to @nvm->buf starting |
463 | * from @offset. |
464 | */ |
465 | int tb_nvm_write_buf(struct tb_nvm *nvm, unsigned int offset, void *val, |
466 | size_t bytes) |
467 | { |
468 | if (!nvm->buf) { |
469 | nvm->buf = vmalloc(NVM_MAX_SIZE); |
470 | if (!nvm->buf) |
471 | return -ENOMEM; |
472 | } |
473 | |
474 | nvm->flushed = false; |
475 | nvm->buf_data_size = offset + bytes; |
476 | memcpy(nvm->buf + offset, val, bytes); |
477 | return 0; |
478 | } |
479 | |
480 | /** |
481 | * tb_nvm_add_non_active() - Adds non-active NVMem device to NVM |
482 | * @nvm: NVM structure |
483 | * @reg_write: Pointer to the function to write the NVM (passed directly |
484 | * to the NVMem device) |
485 | * |
486 | * Registers new non-active NVmem device for @nvm. The @reg_write is called |
487 | * directly from NVMem so it must handle possible concurrent access if |
488 | * needed. The first parameter passed to @reg_write is @nvm structure. |
489 | * The size of the NVMem device is set to %NVM_MAX_SIZE. |
490 | * |
491 | * Returns %0 in success and negative errno otherwise. |
492 | */ |
493 | int tb_nvm_add_non_active(struct tb_nvm *nvm, nvmem_reg_write_t reg_write) |
494 | { |
495 | struct nvmem_config config; |
496 | struct nvmem_device *nvmem; |
497 | |
498 | memset(&config, 0, sizeof(config)); |
499 | |
500 | config.name = "nvm_non_active" ; |
501 | config.reg_write = reg_write; |
502 | config.root_only = true; |
503 | config.id = nvm->id; |
504 | config.stride = 4; |
505 | config.word_size = 4; |
506 | config.size = NVM_MAX_SIZE; |
507 | config.dev = nvm->dev; |
508 | config.owner = THIS_MODULE; |
509 | config.priv = nvm; |
510 | |
511 | nvmem = nvmem_register(cfg: &config); |
512 | if (IS_ERR(ptr: nvmem)) |
513 | return PTR_ERR(ptr: nvmem); |
514 | |
515 | nvm->non_active = nvmem; |
516 | return 0; |
517 | } |
518 | |
519 | /** |
520 | * tb_nvm_free() - Release NVM and its resources |
521 | * @nvm: NVM structure to release |
522 | * |
523 | * Releases NVM and the NVMem devices if they were registered. |
524 | */ |
525 | void tb_nvm_free(struct tb_nvm *nvm) |
526 | { |
527 | if (nvm) { |
528 | nvmem_unregister(nvmem: nvm->non_active); |
529 | nvmem_unregister(nvmem: nvm->active); |
530 | vfree(addr: nvm->buf); |
531 | ida_free(&nvm_ida, id: nvm->id); |
532 | } |
533 | kfree(objp: nvm); |
534 | } |
535 | |
536 | /** |
537 | * tb_nvm_read_data() - Read data from NVM |
538 | * @address: Start address on the flash |
539 | * @buf: Buffer where the read data is copied |
540 | * @size: Size of the buffer in bytes |
541 | * @retries: Number of retries if block read fails |
542 | * @read_block: Function that reads block from the flash |
543 | * @read_block_data: Data passsed to @read_block |
544 | * |
545 | * This is a generic function that reads data from NVM or NVM like |
546 | * device. |
547 | * |
548 | * Returns %0 on success and negative errno otherwise. |
549 | */ |
550 | int tb_nvm_read_data(unsigned int address, void *buf, size_t size, |
551 | unsigned int retries, read_block_fn read_block, |
552 | void *read_block_data) |
553 | { |
554 | do { |
555 | unsigned int dwaddress, dwords, offset; |
556 | u8 data[NVM_DATA_DWORDS * 4]; |
557 | size_t nbytes; |
558 | int ret; |
559 | |
560 | offset = address & 3; |
561 | nbytes = min_t(size_t, size + offset, NVM_DATA_DWORDS * 4); |
562 | |
563 | dwaddress = address / 4; |
564 | dwords = ALIGN(nbytes, 4) / 4; |
565 | |
566 | ret = read_block(read_block_data, dwaddress, data, dwords); |
567 | if (ret) { |
568 | if (ret != -ENODEV && retries--) |
569 | continue; |
570 | return ret; |
571 | } |
572 | |
573 | nbytes -= offset; |
574 | memcpy(buf, data + offset, nbytes); |
575 | |
576 | size -= nbytes; |
577 | address += nbytes; |
578 | buf += nbytes; |
579 | } while (size > 0); |
580 | |
581 | return 0; |
582 | } |
583 | |
584 | /** |
585 | * tb_nvm_write_data() - Write data to NVM |
586 | * @address: Start address on the flash |
587 | * @buf: Buffer where the data is copied from |
588 | * @size: Size of the buffer in bytes |
589 | * @retries: Number of retries if the block write fails |
590 | * @write_block: Function that writes block to the flash |
591 | * @write_block_data: Data passwd to @write_block |
592 | * |
593 | * This is generic function that writes data to NVM or NVM like device. |
594 | * |
595 | * Returns %0 on success and negative errno otherwise. |
596 | */ |
597 | int tb_nvm_write_data(unsigned int address, const void *buf, size_t size, |
598 | unsigned int retries, write_block_fn write_block, |
599 | void *write_block_data) |
600 | { |
601 | do { |
602 | unsigned int offset, dwaddress; |
603 | u8 data[NVM_DATA_DWORDS * 4]; |
604 | size_t nbytes; |
605 | int ret; |
606 | |
607 | offset = address & 3; |
608 | nbytes = min_t(u32, size + offset, NVM_DATA_DWORDS * 4); |
609 | |
610 | memcpy(data + offset, buf, nbytes); |
611 | |
612 | dwaddress = address / 4; |
613 | ret = write_block(write_block_data, dwaddress, data, nbytes / 4); |
614 | if (ret) { |
615 | if (ret == -ETIMEDOUT) { |
616 | if (retries--) |
617 | continue; |
618 | ret = -EIO; |
619 | } |
620 | return ret; |
621 | } |
622 | |
623 | size -= nbytes; |
624 | address += nbytes; |
625 | buf += nbytes; |
626 | } while (size > 0); |
627 | |
628 | return 0; |
629 | } |
630 | |
631 | void tb_nvm_exit(void) |
632 | { |
633 | ida_destroy(ida: &nvm_ida); |
634 | } |
635 | |