diff options
author | Rob Purchase <shotofadds@rockbox.org> | 2008-02-13 23:50:44 +0000 |
---|---|---|
committer | Rob Purchase <shotofadds@rockbox.org> | 2008-02-13 23:50:44 +0000 |
commit | addb5228ece29ce1a17591aa3c8078566ce1dbe6 (patch) | |
tree | 9c2a5870546ac6efbd272f9e8847b9cb0efee4de /firmware | |
parent | e319f68a814b902bc01cb2f61059d12b9939774a (diff) | |
download | rockbox-addb5228ece29ce1a17591aa3c8078566ce1dbe6.tar.gz rockbox-addb5228ece29ce1a17591aa3c8078566ce1dbe6.zip |
D2: Further work-in-progress on the NAND driver.
git-svn-id: svn://svn.rockbox.org/rockbox/trunk@16308 a1c6a512-1295-4272-9138-f99709370657
Diffstat (limited to 'firmware')
-rw-r--r-- | firmware/target/arm/tcc780x/ata-nand-tcc780x.c | 379 |
1 files changed, 276 insertions, 103 deletions
diff --git a/firmware/target/arm/tcc780x/ata-nand-tcc780x.c b/firmware/target/arm/tcc780x/ata-nand-tcc780x.c index bd7e9bb336..906635c51a 100644 --- a/firmware/target/arm/tcc780x/ata-nand-tcc780x.c +++ b/firmware/target/arm/tcc780x/ata-nand-tcc780x.c | |||
@@ -38,6 +38,10 @@ static bool initialized = false; | |||
38 | static long next_yield = 0; | 38 | static long next_yield = 0; |
39 | #define MIN_YIELD_PERIOD 2000 | 39 | #define MIN_YIELD_PERIOD 2000 |
40 | 40 | ||
41 | static struct mutex ata_mtx NOCACHEBSS_ATTR; | ||
42 | |||
43 | #define SECTOR_SIZE 512 | ||
44 | |||
41 | /* TCC780x NAND Flash Controller */ | 45 | /* TCC780x NAND Flash Controller */ |
42 | 46 | ||
43 | #define NFC_CMD (*(volatile unsigned long *)0xF0053000) | 47 | #define NFC_CMD (*(volatile unsigned long *)0xF0053000) |
@@ -59,18 +63,27 @@ static long next_yield = 0; | |||
59 | static int page_size = 0; | 63 | static int page_size = 0; |
60 | static int spare_size = 0; | 64 | static int spare_size = 0; |
61 | static int pages_per_block = 0; | 65 | static int pages_per_block = 0; |
62 | static int total_blocks = 0; | 66 | static int blocks_per_bank = 0; |
63 | static int total_pages = 0; | 67 | static int pages_per_bank = 0; |
64 | static int row_cycles = 0; | 68 | static int row_cycles = 0; |
65 | static int col_cycles = 0; | 69 | static int col_cycles = 0; |
66 | static int total_banks = 0; | 70 | static int total_banks = 0; |
71 | static int sectors_per_page = 0; | ||
72 | static int bytes_per_segment = 0; | ||
73 | static int sectors_per_segment = 0; | ||
67 | 74 | ||
68 | /* Static page buffer */ | 75 | /* Maximum values for static buffers */ |
69 | 76 | ||
70 | #define MAX_PAGE_SIZE 4096 | 77 | #define MAX_PAGE_SIZE 4096 |
71 | #define MAX_SPARE_SIZE 128 | 78 | #define MAX_SPARE_SIZE 128 |
79 | #define MAX_BLOCKS_PER_BANK 8192 | ||
80 | #define MAX_BANKS 4 | ||
72 | 81 | ||
73 | static int page_buf[(MAX_PAGE_SIZE+MAX_SPARE_SIZE)/4]; | 82 | /* |
83 | Block translation table - maps logical Segment Number to physical page address | ||
84 | Format: 0xBTPPPPPP (B = Bank; T = Block Type flag; P = Page Address) | ||
85 | */ | ||
86 | static int segment_location[MAX_BLOCKS_PER_BANK * MAX_BANKS / 4]; | ||
74 | 87 | ||
75 | 88 | ||
76 | static void nand_chip_select(int chip) | 89 | static void nand_chip_select(int chip) |
@@ -201,9 +214,13 @@ static void nand_read_uid(int chip, unsigned int* uid_buf) | |||
201 | 214 | ||
202 | 215 | ||
203 | /* NB: size must be divisible by 4 due to 32-bit read */ | 216 | /* NB: size must be divisible by 4 due to 32-bit read */ |
204 | static void nand_read(int chip, int row, int column, int size) | 217 | static void nand_read_raw(int chip, int row, int column, int size, void* buf) |
205 | { | 218 | { |
206 | int i; | 219 | int i; |
220 | |||
221 | /* Currently this relies on a word-aligned input buffer */ | ||
222 | unsigned int* int_buf = (unsigned int*)buf; | ||
223 | if ((unsigned int)buf & 3) panicf("nand_read_raw() non-aligned input buffer"); | ||
207 | 224 | ||
208 | /* Enable NFC bus clock */ | 225 | /* Enable NFC bus clock */ |
209 | BCLKCTR |= DEV_NAND; | 226 | BCLKCTR |= DEV_NAND; |
@@ -245,7 +262,7 @@ static void nand_read(int chip, int row, int column, int size) | |||
245 | /* Read data into page buffer */ | 262 | /* Read data into page buffer */ |
246 | for (i = 0; i < (size/4); i++) | 263 | for (i = 0; i < (size/4); i++) |
247 | { | 264 | { |
248 | page_buf[i] = NFC_WDATA; | 265 | int_buf[i] = NFC_WDATA; |
249 | } | 266 | } |
250 | 267 | ||
251 | nand_chip_select(-1); | 268 | nand_chip_select(-1); |
@@ -255,89 +272,42 @@ static void nand_read(int chip, int row, int column, int size) | |||
255 | } | 272 | } |
256 | 273 | ||
257 | 274 | ||
258 | /* TEMP testing function */ | 275 | /* NB: Output buffer must currently be word-aligned */ |
259 | #include "lcd.h" | 276 | static bool nand_read_sector(int segment, int sector, void* buf) |
260 | |||
261 | extern int line; | ||
262 | static unsigned char str_buf[MAX_PAGE_SIZE]; | ||
263 | |||
264 | static void nand_test(void) | ||
265 | { | 277 | { |
266 | int i,j,row; | 278 | int physaddr = segment_location[segment]; |
267 | int pages_per_mb = 1048576/page_size; | 279 | int bank = physaddr >> 28; |
268 | 280 | int page = physaddr & 0xffffff; | |
269 | printf("%d banks", total_banks); | ||
270 | printf("* %d pages", total_pages); | ||
271 | printf("* %d bytes per page", page_size); | ||
272 | 281 | ||
273 | while (!button_read_device()) {}; | 282 | int page_in_seg = sector / sectors_per_page; |
283 | int sec_in_page = sector % sectors_per_page; | ||
284 | |||
285 | /* TODO: Check if there are any 0x15 pages referring to this segment/sector | ||
286 | combination. If present we need to read that data instead. */ | ||
274 | 287 | ||
275 | /* Now for fun, scan the raw pages for 'TAG' and display the contents */ | 288 | if (physaddr == -1) return false; |
276 | 289 | ||
277 | row = 0; | 290 | if (page_in_seg & 1) |
278 | while (row < total_pages) | ||
279 | { | 291 | { |
280 | bool found = false; | 292 | /* Data is located in block+1 */ |
281 | unsigned char* buf_ptr = (unsigned char*)page_buf; | 293 | page += pages_per_block; |
282 | 294 | } | |
283 | line = 0; | ||
284 | |||
285 | if (row % pages_per_mb == 0) printf("%dMb", row/pages_per_mb); | ||
286 | |||
287 | /* Read a page from chip 0 */ | ||
288 | nand_read(0, row, 0, page_size); | ||
289 | |||
290 | for (j = 0; j < page_size; j++) | ||
291 | { | ||
292 | if (buf_ptr[j] == 'T' && buf_ptr[j+1] == 'A' && buf_ptr[j+2] == 'G') | ||
293 | found = true; | ||
294 | } | ||
295 | |||
296 | if (found) | ||
297 | { | ||
298 | unsigned char* str_ptr = str_buf; | ||
299 | |||
300 | printf("Row %d:", row); | ||
301 | |||
302 | /* Copy ascii-readable parts out to a string */ | ||
303 | for (i = 0; i < page_size; i++) | ||
304 | { | ||
305 | str_buf[i] = ' '; | ||
306 | if (buf_ptr[i] > 31 && buf_ptr[i] < 128) | ||
307 | { | ||
308 | *str_ptr++ = buf_ptr[i]; | ||
309 | } | ||
310 | } | ||
311 | |||
312 | str_ptr = str_buf; | ||
313 | 295 | ||
314 | /* Nasty piece of code to display the text in a readable manner */ | 296 | if (page_in_seg & 2) |
315 | for (i = 1; i < 30; i++) | 297 | { |
316 | { | 298 | /* Data is located in second plane */ |
317 | for (j = 0; j < 48; j++) | 299 | page += (blocks_per_bank/2) * pages_per_block; |
318 | { | 300 | } |
319 | /* In the absence of a putc() we have this mess... */ | ||
320 | unsigned char buf2[2]; | ||
321 | buf2[0] = *str_ptr++; | ||
322 | buf2[1] = '\0'; | ||
323 | lcd_puts(j,i,buf2); | ||
324 | } | ||
325 | } | ||
326 | 301 | ||
327 | /* Alternate hex display code | 302 | page += page_in_seg/4; |
328 | for (i = 0; i<112; i+=4) | ||
329 | { | ||
330 | printf("0x%08x 0x%08x 0x%08x 0x%08x", | ||
331 | page_buf[i],page_buf[i+1],page_buf[i+2],page_buf[i+3]); | ||
332 | } | ||
333 | */ | ||
334 | 303 | ||
335 | while (!button_read_device()) {}; | 304 | nand_read_raw(bank, page, |
305 | sec_in_page * (SECTOR_SIZE+16), | ||
306 | SECTOR_SIZE, buf); | ||
336 | 307 | ||
337 | lcd_clear_display(); | 308 | /* TODO: Read the 16 spare bytes, perform ECC correction */ |
338 | } | 309 | |
339 | row++; | 310 | return true; |
340 | } | ||
341 | } | 311 | } |
342 | 312 | ||
343 | 313 | ||
@@ -345,7 +315,7 @@ static void nand_get_chip_info(void) | |||
345 | { | 315 | { |
346 | bool found = false; | 316 | bool found = false; |
347 | unsigned char manuf_id; | 317 | unsigned char manuf_id; |
348 | unsigned char id_buf[5]; | 318 | unsigned char id_buf[8]; |
349 | 319 | ||
350 | /* Read chip id from bank 0 */ | 320 | /* Read chip id from bank 0 */ |
351 | nand_read_id(0, id_buf); | 321 | nand_read_id(0, id_buf); |
@@ -363,7 +333,7 @@ static void nand_get_chip_info(void) | |||
363 | page_size = 2048; | 333 | page_size = 2048; |
364 | spare_size = 64; | 334 | spare_size = 64; |
365 | pages_per_block = 128; | 335 | pages_per_block = 128; |
366 | total_blocks = 8192; | 336 | blocks_per_bank = 8192; |
367 | col_cycles = 2; | 337 | col_cycles = 2; |
368 | row_cycles = 3; | 338 | row_cycles = 3; |
369 | 339 | ||
@@ -375,7 +345,7 @@ static void nand_get_chip_info(void) | |||
375 | page_size = 4096; | 345 | page_size = 4096; |
376 | spare_size = 128; | 346 | spare_size = 128; |
377 | pages_per_block = 128; | 347 | pages_per_block = 128; |
378 | total_blocks = 8192; | 348 | blocks_per_bank = 8192; |
379 | col_cycles = 2; | 349 | col_cycles = 2; |
380 | row_cycles = 3; | 350 | row_cycles = 3; |
381 | 351 | ||
@@ -391,10 +361,12 @@ static void nand_get_chip_info(void) | |||
391 | id_buf[0],id_buf[1],id_buf[2],id_buf[3],id_buf[4]); | 361 | id_buf[0],id_buf[1],id_buf[2],id_buf[3],id_buf[4]); |
392 | } | 362 | } |
393 | 363 | ||
394 | total_pages = total_blocks * pages_per_block; | 364 | pages_per_bank = blocks_per_bank * pages_per_block; |
365 | bytes_per_segment = page_size * pages_per_block * 4; | ||
366 | sectors_per_page = page_size / SECTOR_SIZE; | ||
367 | sectors_per_segment = bytes_per_segment / SECTOR_SIZE; | ||
395 | 368 | ||
396 | /* Establish how many banks are present */ | 369 | /* Establish how many banks are present */ |
397 | |||
398 | nand_read_id(1, id_buf); | 370 | nand_read_id(1, id_buf); |
399 | 371 | ||
400 | if (id_buf[0] == manuf_id) | 372 | if (id_buf[0] == manuf_id) |
@@ -433,7 +405,99 @@ static void nand_get_chip_info(void) | |||
433 | /* Bank 1 returned differing id - assume it is junk */ | 405 | /* Bank 1 returned differing id - assume it is junk */ |
434 | total_banks = 1; | 406 | total_banks = 1; |
435 | } | 407 | } |
408 | |||
409 | /* Check block 0, page 0 for "BMPM" string & total_banks byte. If this is | ||
410 | confirmed for all D2s we can remove the above code & nand_read_uid(). */ | ||
411 | |||
412 | nand_read_raw(0, /* bank */ | ||
413 | 0, /* page */ | ||
414 | page_size, /* offset */ | ||
415 | 8, id_buf); | ||
416 | |||
417 | if (strncmp(id_buf, "BMPM", 4)) panicf("BMPM tag not present"); | ||
418 | if (id_buf[4] != total_banks) panicf("BMPM total_banks mismatch"); | ||
419 | } | ||
420 | |||
421 | |||
422 | /* TEMP testing function */ | ||
423 | |||
424 | #ifdef BOOTLOADER | ||
425 | #include "lcd.h" | ||
426 | |||
427 | extern int line; | ||
428 | unsigned int buf[(MAX_PAGE_SIZE + MAX_SPARE_SIZE) / 4]; | ||
429 | |||
430 | static void nand_test(void) | ||
431 | { | ||
432 | int i; | ||
433 | unsigned int seq_segments = 0; | ||
434 | #if 0 | ||
435 | int chip,page; | ||
436 | #endif | ||
437 | |||
438 | printf("%d banks", total_banks); | ||
439 | printf("* %d pages", pages_per_bank); | ||
440 | printf("* %d bytes per page", page_size); | ||
441 | |||
442 | i = 0; | ||
443 | while (segment_location[i] != -1 | ||
444 | && i++ < (blocks_per_bank * total_banks / 4)) | ||
445 | { | ||
446 | seq_segments++; | ||
447 | } | ||
448 | printf("%d sequential segments found (%dMb)", seq_segments, | ||
449 | (seq_segments*bytes_per_segment)>>20); | ||
450 | |||
451 | while (!button_read_device()) {}; | ||
452 | while (button_read_device()) {}; | ||
453 | |||
454 | #if 0 | ||
455 | /* Read & display sequential pages */ | ||
456 | for (chip = 0; chip < total_banks; chip++) | ||
457 | { | ||
458 | for (page = 0x0; page < 0x100; page++) | ||
459 | { | ||
460 | nand_read_raw(chip, page, 0, page_size+spare_size, buf); | ||
461 | |||
462 | for (i = 0; i < (page_size+spare_size)/4; i += 132) | ||
463 | { | ||
464 | int j,interesting = 0; | ||
465 | line = 0; | ||
466 | printf("c:%d p:%lx i:%d", chip, page, i); | ||
467 | |||
468 | for (j=i; j<(i+131); j++) | ||
469 | { | ||
470 | if (buf[j] != 0xffffffff) interesting = 1; | ||
471 | } | ||
472 | |||
473 | if (interesting) | ||
474 | { | ||
475 | for (j=i; j<(i+63); j+=4) | ||
476 | { | ||
477 | printf("%lx %lx %lx %lx", | ||
478 | buf[j], buf[j+1], buf[j+2], buf[j+3]); | ||
479 | } | ||
480 | printf("--->"); | ||
481 | while (!button_read_device()) {}; | ||
482 | while (button_read_device()) {}; | ||
483 | |||
484 | line = 1; | ||
485 | printf("<---"); | ||
486 | for (j=j; j<(i+131); j+=4) | ||
487 | { | ||
488 | printf("%lx %lx %lx %lx", | ||
489 | buf[j], buf[j+1], buf[j+2], buf[j+3]); | ||
490 | } | ||
491 | while (!button_read_device()) {}; | ||
492 | while (button_read_device()) {}; | ||
493 | reset_screen(); | ||
494 | } | ||
495 | } | ||
496 | } | ||
497 | } | ||
498 | #endif | ||
436 | } | 499 | } |
500 | #endif | ||
437 | 501 | ||
438 | 502 | ||
439 | /* API Functions */ | 503 | /* API Functions */ |
@@ -446,10 +510,40 @@ void ata_led(bool onoff) | |||
446 | int ata_read_sectors(IF_MV2(int drive,) unsigned long start, int incount, | 510 | int ata_read_sectors(IF_MV2(int drive,) unsigned long start, int incount, |
447 | void* inbuf) | 511 | void* inbuf) |
448 | { | 512 | { |
449 | #warning function not implemented | 513 | #ifdef HAVE_MULTIVOLUME |
450 | (void)start; | 514 | (void)drive; /* unused for now */ |
451 | (void)incount; | 515 | #endif |
452 | (void)inbuf; | 516 | mutex_lock(&ata_mtx); |
517 | |||
518 | while (incount > 0) | ||
519 | { | ||
520 | int done = 0; | ||
521 | int segment = start / sectors_per_segment; | ||
522 | int secmod = start % sectors_per_segment; | ||
523 | |||
524 | while (incount > 0 && secmod < sectors_per_segment) | ||
525 | { | ||
526 | if (!nand_read_sector(segment, secmod, inbuf)) | ||
527 | { | ||
528 | mutex_unlock(&ata_mtx); | ||
529 | return -1; | ||
530 | } | ||
531 | |||
532 | inbuf += SECTOR_SIZE; | ||
533 | incount--; | ||
534 | secmod++; | ||
535 | done++; | ||
536 | } | ||
537 | |||
538 | if (done < 0) | ||
539 | { | ||
540 | mutex_unlock(&ata_mtx); | ||
541 | return -1; | ||
542 | } | ||
543 | start += done; | ||
544 | } | ||
545 | |||
546 | mutex_unlock(&ata_mtx); | ||
453 | return 0; | 547 | return 0; |
454 | } | 548 | } |
455 | 549 | ||
@@ -471,13 +565,13 @@ void ata_spindown(int seconds) | |||
471 | 565 | ||
472 | bool ata_disk_is_active(void) | 566 | bool ata_disk_is_active(void) |
473 | { | 567 | { |
474 | #warning function not implemented | 568 | /* null */ |
475 | return 0; | 569 | return 0; |
476 | } | 570 | } |
477 | 571 | ||
478 | void ata_sleep(void) | 572 | void ata_sleep(void) |
479 | { | 573 | { |
480 | #warning function not implemented | 574 | /* null */ |
481 | } | 575 | } |
482 | 576 | ||
483 | void ata_spin(void) | 577 | void ata_spin(void) |
@@ -488,13 +582,13 @@ void ata_spin(void) | |||
488 | /* Hardware reset protocol as specified in chapter 9.1, ATA spec draft v5 */ | 582 | /* Hardware reset protocol as specified in chapter 9.1, ATA spec draft v5 */ |
489 | int ata_hard_reset(void) | 583 | int ata_hard_reset(void) |
490 | { | 584 | { |
491 | #warning function not implemented | 585 | /* null */ |
492 | return 0; | 586 | return 0; |
493 | } | 587 | } |
494 | 588 | ||
495 | int ata_soft_reset(void) | 589 | int ata_soft_reset(void) |
496 | { | 590 | { |
497 | #warning function not implemented | 591 | /* null */ |
498 | return 0; | 592 | return 0; |
499 | } | 593 | } |
500 | 594 | ||
@@ -506,20 +600,99 @@ void ata_enable(bool on) | |||
506 | 600 | ||
507 | int ata_init(void) | 601 | int ata_init(void) |
508 | { | 602 | { |
509 | if (!initialized) | 603 | int i, bank, page; |
510 | { | 604 | unsigned int spare_buf[4]; |
511 | /* Get chip characteristics and number of banks */ | 605 | |
512 | nand_get_chip_info(); | 606 | if (initialized) return 0; |
513 | 607 | ||
514 | /* TODO: Scan all banks for bad blocks */ | 608 | /* Get chip characteristics and number of banks */ |
609 | nand_get_chip_info(); | ||
515 | 610 | ||
516 | /* TODO: Build physical->logical address translation */ | 611 | for (i = 0; i < (MAX_BLOCKS_PER_BANK * MAX_BANKS / 4); i++) |
517 | 612 | { | |
518 | initialized = true; | 613 | segment_location[i] = -1; |
614 | } | ||
615 | |||
616 | /* Scan banks to build up block translation table */ | ||
617 | for (bank = 0; bank < total_banks; bank++) | ||
618 | { | ||
619 | for (page = 0; page < pages_per_bank/2; page += pages_per_block*2) | ||
620 | { | ||
621 | unsigned char segment_flag; | ||
622 | unsigned char stored_flag; | ||
623 | unsigned short segment_id; | ||
624 | |||
625 | unsigned char* buf_ptr = (unsigned char*)spare_buf; | ||
626 | |||
627 | /* Read spare bytes from first sector of each segment */ | ||
628 | nand_read_raw(bank, page, | ||
629 | SECTOR_SIZE, /* offset */ | ||
630 | 16, spare_buf); | ||
631 | |||
632 | segment_id = (buf_ptr[6] << 8) | buf_ptr[7]; | ||
633 | segment_flag = buf_ptr[4]; | ||
634 | |||
635 | stored_flag = (segment_location[segment_id] >> 24) & 0xf; | ||
636 | |||
637 | #if defined(BOOTLOADER) && 0 | ||
638 | if (segment_flag == 0x15) | ||
639 | { | ||
640 | printf("Segment %lx: c:%lx p:%lx, type:%lx, stored:x%lx", | ||
641 | segment_id, bank, page, segment_flag, stored_flag); | ||
642 | while (!button_read_device()) {}; | ||
643 | while (button_read_device()) {}; | ||
644 | } | ||
645 | #endif | ||
646 | |||
647 | if (segment_flag == 0x13 || segment_flag == 0x17) | ||
648 | { | ||
649 | if (segment_id < (blocks_per_bank * total_banks / 4)) | ||
650 | { | ||
651 | #if defined(BOOTLOADER) && 0 | ||
652 | if (segment_location[segment_id] != -1 && stored_flag != 0x3) | ||
653 | { | ||
654 | int orig_bank = segment_location[segment_id] >> 28; | ||
655 | int orig_page = segment_location[segment_id] & 0xFFFFFF; | ||
656 | |||
657 | printf("Segment %d already set! (stored flag:x%lx)", | ||
658 | segment_id, stored_flag); | ||
659 | |||
660 | printf("0x%08x 0x%08x 0x%08x 0x%08x", | ||
661 | spare_buf[0],spare_buf[1],spare_buf[2],spare_buf[3]); | ||
662 | |||
663 | nand_read_raw(orig_bank, orig_page, | ||
664 | SECTOR_SIZE, | ||
665 | 16, spare_buf); | ||
666 | |||
667 | printf("0x%08x 0x%08x 0x%08x 0x%08x", | ||
668 | spare_buf[0],spare_buf[1],spare_buf[2],spare_buf[3]); | ||
669 | } | ||
670 | #endif | ||
671 | /* Write bank, block type & physical address into table */ | ||
672 | segment_location[segment_id] | ||
673 | = page | (bank << 28) | ((segment_flag & 0xf) << 24); | ||
674 | } | ||
675 | else | ||
676 | { | ||
677 | panicf("Invalid segment id:%d found", segment_id); | ||
678 | } | ||
679 | } | ||
680 | } | ||
519 | } | 681 | } |
682 | |||
683 | initialized = true; | ||
520 | 684 | ||
685 | #ifdef BOOTLOADER | ||
521 | /* TEMP - print out some diagnostics */ | 686 | /* TEMP - print out some diagnostics */ |
522 | nand_test(); | 687 | nand_test(); |
688 | #endif | ||
523 | 689 | ||
524 | return 0; | 690 | return 0; |
525 | } | 691 | } |
692 | |||
693 | |||
694 | /* TEMP: This will return junk, it's here for compilation only */ | ||
695 | unsigned short* ata_get_identify(void) | ||
696 | { | ||
697 | return (unsigned short*)0x21000000; /* Unused DRAM */ | ||
698 | } | ||