1 /* 2 * MTD device concatenation layer 3 * 4 * (C) 2002 Robert Kaiser <rkaiser@sysgo.de> 5 * 6 * NAND support by Christian Gan <cgan@iders.ca> 7 * 8 * This code is GPL 9 */ 10 11 #include <linux/mtd/mtd.h> 12 #include <linux/compat.h> 13 #include <linux/mtd/concat.h> 14 #include <ubi_uboot.h> 15 16 /* 17 * Our storage structure: 18 * Subdev points to an array of pointers to struct mtd_info objects 19 * which is allocated along with this structure 20 * 21 */ 22 struct mtd_concat { 23 struct mtd_info mtd; 24 int num_subdev; 25 struct mtd_info **subdev; 26 }; 27 28 /* 29 * how to calculate the size required for the above structure, 30 * including the pointer array subdev points to: 31 */ 32 #define SIZEOF_STRUCT_MTD_CONCAT(num_subdev) \ 33 ((sizeof(struct mtd_concat) + (num_subdev) * sizeof(struct mtd_info *))) 34 35 /* 36 * Given a pointer to the MTD object in the mtd_concat structure, 37 * we can retrieve the pointer to that structure with this macro. 38 */ 39 #define CONCAT(x) ((struct mtd_concat *)(x)) 40 41 /* 42 * MTD methods which look up the relevant subdevice, translate the 43 * effective address and pass through to the subdevice. 44 */ 45 46 static int 47 concat_read(struct mtd_info *mtd, loff_t from, size_t len, 48 size_t * retlen, u_char * buf) 49 { 50 struct mtd_concat *concat = CONCAT(mtd); 51 int ret = 0, err; 52 int i; 53 54 *retlen = 0; 55 56 for (i = 0; i < concat->num_subdev; i++) { 57 struct mtd_info *subdev = concat->subdev[i]; 58 size_t size, retsize; 59 60 if (from >= subdev->size) { 61 /* Not destined for this subdev */ 62 size = 0; 63 from -= subdev->size; 64 continue; 65 } 66 if (from + len > subdev->size) 67 /* First part goes into this subdev */ 68 size = subdev->size - from; 69 else 70 /* Entire transaction goes into this subdev */ 71 size = len; 72 73 err = subdev->read(subdev, from, size, &retsize, buf); 74 75 /* Save information about bitflips! */ 76 if (unlikely(err)) { 77 if (err == -EBADMSG) { 78 mtd->ecc_stats.failed++; 79 ret = err; 80 } else if (err == -EUCLEAN) { 81 mtd->ecc_stats.corrected++; 82 /* Do not overwrite -EBADMSG !! */ 83 if (!ret) 84 ret = err; 85 } else 86 return err; 87 } 88 89 *retlen += retsize; 90 len -= size; 91 if (len == 0) 92 return ret; 93 94 buf += size; 95 from = 0; 96 } 97 return -EINVAL; 98 } 99 100 static int 101 concat_write(struct mtd_info *mtd, loff_t to, size_t len, 102 size_t * retlen, const u_char * buf) 103 { 104 struct mtd_concat *concat = CONCAT(mtd); 105 int err = -EINVAL; 106 int i; 107 108 if (!(mtd->flags & MTD_WRITEABLE)) 109 return -EROFS; 110 111 *retlen = 0; 112 113 for (i = 0; i < concat->num_subdev; i++) { 114 struct mtd_info *subdev = concat->subdev[i]; 115 size_t size, retsize; 116 117 if (to >= subdev->size) { 118 size = 0; 119 to -= subdev->size; 120 continue; 121 } 122 if (to + len > subdev->size) 123 size = subdev->size - to; 124 else 125 size = len; 126 127 if (!(subdev->flags & MTD_WRITEABLE)) 128 err = -EROFS; 129 else 130 err = subdev->write(subdev, to, size, &retsize, buf); 131 132 if (err) 133 break; 134 135 *retlen += retsize; 136 len -= size; 137 if (len == 0) 138 break; 139 140 err = -EINVAL; 141 buf += size; 142 to = 0; 143 } 144 return err; 145 } 146 147 static int 148 concat_read_oob(struct mtd_info *mtd, loff_t from, struct mtd_oob_ops *ops) 149 { 150 struct mtd_concat *concat = CONCAT(mtd); 151 struct mtd_oob_ops devops = *ops; 152 int i, err, ret = 0; 153 154 ops->retlen = ops->oobretlen = 0; 155 156 for (i = 0; i < concat->num_subdev; i++) { 157 struct mtd_info *subdev = concat->subdev[i]; 158 159 if (from >= subdev->size) { 160 from -= subdev->size; 161 continue; 162 } 163 164 /* partial read ? */ 165 if (from + devops.len > subdev->size) 166 devops.len = subdev->size - from; 167 168 err = subdev->read_oob(subdev, from, &devops); 169 ops->retlen += devops.retlen; 170 ops->oobretlen += devops.oobretlen; 171 172 /* Save information about bitflips! */ 173 if (unlikely(err)) { 174 if (err == -EBADMSG) { 175 mtd->ecc_stats.failed++; 176 ret = err; 177 } else if (err == -EUCLEAN) { 178 mtd->ecc_stats.corrected++; 179 /* Do not overwrite -EBADMSG !! */ 180 if (!ret) 181 ret = err; 182 } else 183 return err; 184 } 185 186 if (devops.datbuf) { 187 devops.len = ops->len - ops->retlen; 188 if (!devops.len) 189 return ret; 190 devops.datbuf += devops.retlen; 191 } 192 if (devops.oobbuf) { 193 devops.ooblen = ops->ooblen - ops->oobretlen; 194 if (!devops.ooblen) 195 return ret; 196 devops.oobbuf += ops->oobretlen; 197 } 198 199 from = 0; 200 } 201 return -EINVAL; 202 } 203 204 static int 205 concat_write_oob(struct mtd_info *mtd, loff_t to, struct mtd_oob_ops *ops) 206 { 207 struct mtd_concat *concat = CONCAT(mtd); 208 struct mtd_oob_ops devops = *ops; 209 int i, err; 210 211 if (!(mtd->flags & MTD_WRITEABLE)) 212 return -EROFS; 213 214 ops->retlen = 0; 215 216 for (i = 0; i < concat->num_subdev; i++) { 217 struct mtd_info *subdev = concat->subdev[i]; 218 219 if (to >= subdev->size) { 220 to -= subdev->size; 221 continue; 222 } 223 224 /* partial write ? */ 225 if (to + devops.len > subdev->size) 226 devops.len = subdev->size - to; 227 228 err = subdev->write_oob(subdev, to, &devops); 229 ops->retlen += devops.retlen; 230 if (err) 231 return err; 232 233 if (devops.datbuf) { 234 devops.len = ops->len - ops->retlen; 235 if (!devops.len) 236 return 0; 237 devops.datbuf += devops.retlen; 238 } 239 if (devops.oobbuf) { 240 devops.ooblen = ops->ooblen - ops->oobretlen; 241 if (!devops.ooblen) 242 return 0; 243 devops.oobbuf += devops.oobretlen; 244 } 245 to = 0; 246 } 247 return -EINVAL; 248 } 249 250 static void concat_erase_callback(struct erase_info *instr) 251 { 252 /* Nothing to do here in U-Boot */ 253 } 254 255 static int concat_dev_erase(struct mtd_info *mtd, struct erase_info *erase) 256 { 257 int err; 258 wait_queue_head_t waitq; 259 DECLARE_WAITQUEUE(wait, current); 260 261 /* 262 * This code was stol^H^H^H^Hinspired by mtdchar.c 263 */ 264 init_waitqueue_head(&waitq); 265 266 erase->mtd = mtd; 267 erase->callback = concat_erase_callback; 268 erase->priv = (unsigned long) &waitq; 269 270 /* 271 * FIXME: Allow INTERRUPTIBLE. Which means 272 * not having the wait_queue head on the stack. 273 */ 274 err = mtd->erase(mtd, erase); 275 if (!err) { 276 set_current_state(TASK_UNINTERRUPTIBLE); 277 add_wait_queue(&waitq, &wait); 278 if (erase->state != MTD_ERASE_DONE 279 && erase->state != MTD_ERASE_FAILED) 280 schedule(); 281 remove_wait_queue(&waitq, &wait); 282 set_current_state(TASK_RUNNING); 283 284 err = (erase->state == MTD_ERASE_FAILED) ? -EIO : 0; 285 } 286 return err; 287 } 288 289 static int concat_erase(struct mtd_info *mtd, struct erase_info *instr) 290 { 291 struct mtd_concat *concat = CONCAT(mtd); 292 struct mtd_info *subdev; 293 int i, err; 294 uint64_t length, offset = 0; 295 struct erase_info *erase; 296 297 if (!(mtd->flags & MTD_WRITEABLE)) 298 return -EROFS; 299 300 if (instr->addr > concat->mtd.size) 301 return -EINVAL; 302 303 if (instr->len + instr->addr > concat->mtd.size) 304 return -EINVAL; 305 306 /* 307 * Check for proper erase block alignment of the to-be-erased area. 308 * It is easier to do this based on the super device's erase 309 * region info rather than looking at each particular sub-device 310 * in turn. 311 */ 312 if (!concat->mtd.numeraseregions) { 313 /* the easy case: device has uniform erase block size */ 314 if (instr->addr & (concat->mtd.erasesize - 1)) 315 return -EINVAL; 316 if (instr->len & (concat->mtd.erasesize - 1)) 317 return -EINVAL; 318 } else { 319 /* device has variable erase size */ 320 struct mtd_erase_region_info *erase_regions = 321 concat->mtd.eraseregions; 322 323 /* 324 * Find the erase region where the to-be-erased area begins: 325 */ 326 for (i = 0; i < concat->mtd.numeraseregions && 327 instr->addr >= erase_regions[i].offset; i++) ; 328 --i; 329 330 /* 331 * Now erase_regions[i] is the region in which the 332 * to-be-erased area begins. Verify that the starting 333 * offset is aligned to this region's erase size: 334 */ 335 if (instr->addr & (erase_regions[i].erasesize - 1)) 336 return -EINVAL; 337 338 /* 339 * now find the erase region where the to-be-erased area ends: 340 */ 341 for (; i < concat->mtd.numeraseregions && 342 (instr->addr + instr->len) >= erase_regions[i].offset; 343 ++i) ; 344 --i; 345 /* 346 * check if the ending offset is aligned to this region's erase size 347 */ 348 if ((instr->addr + instr->len) & (erase_regions[i].erasesize - 349 1)) 350 return -EINVAL; 351 } 352 353 instr->fail_addr = MTD_FAIL_ADDR_UNKNOWN; 354 355 /* make a local copy of instr to avoid modifying the caller's struct */ 356 erase = kmalloc(sizeof (struct erase_info), GFP_KERNEL); 357 358 if (!erase) 359 return -ENOMEM; 360 361 *erase = *instr; 362 length = instr->len; 363 364 /* 365 * find the subdevice where the to-be-erased area begins, adjust 366 * starting offset to be relative to the subdevice start 367 */ 368 for (i = 0; i < concat->num_subdev; i++) { 369 subdev = concat->subdev[i]; 370 if (subdev->size <= erase->addr) { 371 erase->addr -= subdev->size; 372 offset += subdev->size; 373 } else { 374 break; 375 } 376 } 377 378 /* must never happen since size limit has been verified above */ 379 BUG_ON(i >= concat->num_subdev); 380 381 /* now do the erase: */ 382 err = 0; 383 for (; length > 0; i++) { 384 /* loop for all subdevices affected by this request */ 385 subdev = concat->subdev[i]; /* get current subdevice */ 386 387 /* limit length to subdevice's size: */ 388 if (erase->addr + length > subdev->size) 389 erase->len = subdev->size - erase->addr; 390 else 391 erase->len = length; 392 393 if (!(subdev->flags & MTD_WRITEABLE)) { 394 err = -EROFS; 395 break; 396 } 397 length -= erase->len; 398 if ((err = concat_dev_erase(subdev, erase))) { 399 /* sanity check: should never happen since 400 * block alignment has been checked above */ 401 BUG_ON(err == -EINVAL); 402 if (erase->fail_addr != MTD_FAIL_ADDR_UNKNOWN) 403 instr->fail_addr = erase->fail_addr + offset; 404 break; 405 } 406 /* 407 * erase->addr specifies the offset of the area to be 408 * erased *within the current subdevice*. It can be 409 * non-zero only the first time through this loop, i.e. 410 * for the first subdevice where blocks need to be erased. 411 * All the following erases must begin at the start of the 412 * current subdevice, i.e. at offset zero. 413 */ 414 erase->addr = 0; 415 offset += subdev->size; 416 } 417 instr->state = erase->state; 418 kfree(erase); 419 if (err) 420 return err; 421 422 if (instr->callback) 423 instr->callback(instr); 424 return 0; 425 } 426 427 static int concat_lock(struct mtd_info *mtd, loff_t ofs, uint64_t len) 428 { 429 struct mtd_concat *concat = CONCAT(mtd); 430 int i, err = -EINVAL; 431 432 if ((len + ofs) > mtd->size) 433 return -EINVAL; 434 435 for (i = 0; i < concat->num_subdev; i++) { 436 struct mtd_info *subdev = concat->subdev[i]; 437 uint64_t size; 438 439 if (ofs >= subdev->size) { 440 size = 0; 441 ofs -= subdev->size; 442 continue; 443 } 444 if (ofs + len > subdev->size) 445 size = subdev->size - ofs; 446 else 447 size = len; 448 449 err = subdev->lock(subdev, ofs, size); 450 451 if (err) 452 break; 453 454 len -= size; 455 if (len == 0) 456 break; 457 458 err = -EINVAL; 459 ofs = 0; 460 } 461 462 return err; 463 } 464 465 static int concat_unlock(struct mtd_info *mtd, loff_t ofs, uint64_t len) 466 { 467 struct mtd_concat *concat = CONCAT(mtd); 468 int i, err = 0; 469 470 if ((len + ofs) > mtd->size) 471 return -EINVAL; 472 473 for (i = 0; i < concat->num_subdev; i++) { 474 struct mtd_info *subdev = concat->subdev[i]; 475 uint64_t size; 476 477 if (ofs >= subdev->size) { 478 size = 0; 479 ofs -= subdev->size; 480 continue; 481 } 482 if (ofs + len > subdev->size) 483 size = subdev->size - ofs; 484 else 485 size = len; 486 487 err = subdev->unlock(subdev, ofs, size); 488 489 if (err) 490 break; 491 492 len -= size; 493 if (len == 0) 494 break; 495 496 err = -EINVAL; 497 ofs = 0; 498 } 499 500 return err; 501 } 502 503 static void concat_sync(struct mtd_info *mtd) 504 { 505 struct mtd_concat *concat = CONCAT(mtd); 506 int i; 507 508 for (i = 0; i < concat->num_subdev; i++) { 509 struct mtd_info *subdev = concat->subdev[i]; 510 subdev->sync(subdev); 511 } 512 } 513 514 static int concat_block_isbad(struct mtd_info *mtd, loff_t ofs) 515 { 516 struct mtd_concat *concat = CONCAT(mtd); 517 int i, res = 0; 518 519 if (!concat->subdev[0]->block_isbad) 520 return res; 521 522 if (ofs > mtd->size) 523 return -EINVAL; 524 525 for (i = 0; i < concat->num_subdev; i++) { 526 struct mtd_info *subdev = concat->subdev[i]; 527 528 if (ofs >= subdev->size) { 529 ofs -= subdev->size; 530 continue; 531 } 532 533 res = subdev->block_isbad(subdev, ofs); 534 break; 535 } 536 537 return res; 538 } 539 540 static int concat_block_markbad(struct mtd_info *mtd, loff_t ofs) 541 { 542 struct mtd_concat *concat = CONCAT(mtd); 543 int i, err = -EINVAL; 544 545 if (!concat->subdev[0]->block_markbad) 546 return 0; 547 548 if (ofs > mtd->size) 549 return -EINVAL; 550 551 for (i = 0; i < concat->num_subdev; i++) { 552 struct mtd_info *subdev = concat->subdev[i]; 553 554 if (ofs >= subdev->size) { 555 ofs -= subdev->size; 556 continue; 557 } 558 559 err = subdev->block_markbad(subdev, ofs); 560 if (!err) 561 mtd->ecc_stats.badblocks++; 562 break; 563 } 564 565 return err; 566 } 567 568 /* 569 * This function constructs a virtual MTD device by concatenating 570 * num_devs MTD devices. A pointer to the new device object is 571 * stored to *new_dev upon success. This function does _not_ 572 * register any devices: this is the caller's responsibility. 573 */ 574 struct mtd_info *mtd_concat_create(struct mtd_info *subdev[], /* subdevices to concatenate */ 575 int num_devs, /* number of subdevices */ 576 const char *name) 577 { /* name for the new device */ 578 int i; 579 size_t size; 580 struct mtd_concat *concat; 581 uint32_t max_erasesize, curr_erasesize; 582 int num_erase_region; 583 584 debug("Concatenating MTD devices:\n"); 585 for (i = 0; i < num_devs; i++) 586 debug("(%d): \"%s\"\n", i, subdev[i]->name); 587 debug("into device \"%s\"\n", name); 588 589 /* allocate the device structure */ 590 size = SIZEOF_STRUCT_MTD_CONCAT(num_devs); 591 concat = kzalloc(size, GFP_KERNEL); 592 if (!concat) { 593 printk 594 ("memory allocation error while creating concatenated device \"%s\"\n", 595 name); 596 return NULL; 597 } 598 concat->subdev = (struct mtd_info **) (concat + 1); 599 600 /* 601 * Set up the new "super" device's MTD object structure, check for 602 * incompatibilites between the subdevices. 603 */ 604 concat->mtd.type = subdev[0]->type; 605 concat->mtd.flags = subdev[0]->flags; 606 concat->mtd.size = subdev[0]->size; 607 concat->mtd.erasesize = subdev[0]->erasesize; 608 concat->mtd.writesize = subdev[0]->writesize; 609 concat->mtd.subpage_sft = subdev[0]->subpage_sft; 610 concat->mtd.oobsize = subdev[0]->oobsize; 611 concat->mtd.oobavail = subdev[0]->oobavail; 612 if (subdev[0]->read_oob) 613 concat->mtd.read_oob = concat_read_oob; 614 if (subdev[0]->write_oob) 615 concat->mtd.write_oob = concat_write_oob; 616 if (subdev[0]->block_isbad) 617 concat->mtd.block_isbad = concat_block_isbad; 618 if (subdev[0]->block_markbad) 619 concat->mtd.block_markbad = concat_block_markbad; 620 621 concat->mtd.ecc_stats.badblocks = subdev[0]->ecc_stats.badblocks; 622 623 concat->subdev[0] = subdev[0]; 624 625 for (i = 1; i < num_devs; i++) { 626 if (concat->mtd.type != subdev[i]->type) { 627 kfree(concat); 628 printk("Incompatible device type on \"%s\"\n", 629 subdev[i]->name); 630 return NULL; 631 } 632 if (concat->mtd.flags != subdev[i]->flags) { 633 /* 634 * Expect all flags except MTD_WRITEABLE to be 635 * equal on all subdevices. 636 */ 637 if ((concat->mtd.flags ^ subdev[i]-> 638 flags) & ~MTD_WRITEABLE) { 639 kfree(concat); 640 printk("Incompatible device flags on \"%s\"\n", 641 subdev[i]->name); 642 return NULL; 643 } else 644 /* if writeable attribute differs, 645 make super device writeable */ 646 concat->mtd.flags |= 647 subdev[i]->flags & MTD_WRITEABLE; 648 } 649 650 concat->mtd.size += subdev[i]->size; 651 concat->mtd.ecc_stats.badblocks += 652 subdev[i]->ecc_stats.badblocks; 653 if (concat->mtd.writesize != subdev[i]->writesize || 654 concat->mtd.subpage_sft != subdev[i]->subpage_sft || 655 concat->mtd.oobsize != subdev[i]->oobsize || 656 !concat->mtd.read_oob != !subdev[i]->read_oob || 657 !concat->mtd.write_oob != !subdev[i]->write_oob) { 658 kfree(concat); 659 printk("Incompatible OOB or ECC data on \"%s\"\n", 660 subdev[i]->name); 661 return NULL; 662 } 663 concat->subdev[i] = subdev[i]; 664 665 } 666 667 concat->mtd.ecclayout = subdev[0]->ecclayout; 668 669 concat->num_subdev = num_devs; 670 concat->mtd.name = name; 671 672 concat->mtd.erase = concat_erase; 673 concat->mtd.read = concat_read; 674 concat->mtd.write = concat_write; 675 concat->mtd.sync = concat_sync; 676 concat->mtd.lock = concat_lock; 677 concat->mtd.unlock = concat_unlock; 678 679 /* 680 * Combine the erase block size info of the subdevices: 681 * 682 * first, walk the map of the new device and see how 683 * many changes in erase size we have 684 */ 685 max_erasesize = curr_erasesize = subdev[0]->erasesize; 686 num_erase_region = 1; 687 for (i = 0; i < num_devs; i++) { 688 if (subdev[i]->numeraseregions == 0) { 689 /* current subdevice has uniform erase size */ 690 if (subdev[i]->erasesize != curr_erasesize) { 691 /* if it differs from the last subdevice's erase size, count it */ 692 ++num_erase_region; 693 curr_erasesize = subdev[i]->erasesize; 694 if (curr_erasesize > max_erasesize) 695 max_erasesize = curr_erasesize; 696 } 697 } else { 698 /* current subdevice has variable erase size */ 699 int j; 700 for (j = 0; j < subdev[i]->numeraseregions; j++) { 701 702 /* walk the list of erase regions, count any changes */ 703 if (subdev[i]->eraseregions[j].erasesize != 704 curr_erasesize) { 705 ++num_erase_region; 706 curr_erasesize = 707 subdev[i]->eraseregions[j]. 708 erasesize; 709 if (curr_erasesize > max_erasesize) 710 max_erasesize = curr_erasesize; 711 } 712 } 713 } 714 } 715 716 if (num_erase_region == 1) { 717 /* 718 * All subdevices have the same uniform erase size. 719 * This is easy: 720 */ 721 concat->mtd.erasesize = curr_erasesize; 722 concat->mtd.numeraseregions = 0; 723 } else { 724 uint64_t tmp64; 725 726 /* 727 * erase block size varies across the subdevices: allocate 728 * space to store the data describing the variable erase regions 729 */ 730 struct mtd_erase_region_info *erase_region_p; 731 uint64_t begin, position; 732 733 concat->mtd.erasesize = max_erasesize; 734 concat->mtd.numeraseregions = num_erase_region; 735 concat->mtd.eraseregions = erase_region_p = 736 kmalloc(num_erase_region * 737 sizeof (struct mtd_erase_region_info), GFP_KERNEL); 738 if (!erase_region_p) { 739 kfree(concat); 740 printk 741 ("memory allocation error while creating erase region list" 742 " for device \"%s\"\n", name); 743 return NULL; 744 } 745 746 /* 747 * walk the map of the new device once more and fill in 748 * in erase region info: 749 */ 750 curr_erasesize = subdev[0]->erasesize; 751 begin = position = 0; 752 for (i = 0; i < num_devs; i++) { 753 if (subdev[i]->numeraseregions == 0) { 754 /* current subdevice has uniform erase size */ 755 if (subdev[i]->erasesize != curr_erasesize) { 756 /* 757 * fill in an mtd_erase_region_info structure for the area 758 * we have walked so far: 759 */ 760 erase_region_p->offset = begin; 761 erase_region_p->erasesize = 762 curr_erasesize; 763 tmp64 = position - begin; 764 do_div(tmp64, curr_erasesize); 765 erase_region_p->numblocks = tmp64; 766 begin = position; 767 768 curr_erasesize = subdev[i]->erasesize; 769 ++erase_region_p; 770 } 771 position += subdev[i]->size; 772 } else { 773 /* current subdevice has variable erase size */ 774 int j; 775 for (j = 0; j < subdev[i]->numeraseregions; j++) { 776 /* walk the list of erase regions, count any changes */ 777 if (subdev[i]->eraseregions[j]. 778 erasesize != curr_erasesize) { 779 erase_region_p->offset = begin; 780 erase_region_p->erasesize = 781 curr_erasesize; 782 tmp64 = position - begin; 783 do_div(tmp64, curr_erasesize); 784 erase_region_p->numblocks = tmp64; 785 begin = position; 786 787 curr_erasesize = 788 subdev[i]->eraseregions[j]. 789 erasesize; 790 ++erase_region_p; 791 } 792 position += 793 subdev[i]->eraseregions[j]. 794 numblocks * (uint64_t)curr_erasesize; 795 } 796 } 797 } 798 /* Now write the final entry */ 799 erase_region_p->offset = begin; 800 erase_region_p->erasesize = curr_erasesize; 801 tmp64 = position - begin; 802 do_div(tmp64, curr_erasesize); 803 erase_region_p->numblocks = tmp64; 804 } 805 806 return &concat->mtd; 807 } 808