This patch applies against the top of the flashrom-chromium tree. Compile tested only, I'd be surprised if stuff like write works. Probe and read may work, though.
Convert WPCE775X interface to flashrom style. The current code had a fake flashchips.c entry for all chips behind a WPCE775X which is changed on the fly to the real chip data. This means that all relevant logic was duplicated, and the interface was not portable to the current upstream partial write infrastructure.
Signed-off-by: Carl-Daniel Hailfinger c-d.hailfinger.devel.2006@gmx.net
diff --git a/flashchips.c b/flashchips.c index 8e95dd1..f7930a9 100644 --- a/flashchips.c +++ b/flashchips.c @@ -7745,22 +7745,5 @@ struct flashchip flashchips[] = { .write = NULL, },
- { - .vendor = "Nuvoton", - .name = "WPCE775x", - .bustype = CHIP_BUSTYPE_FWH, - .manufacture_id = GENERIC_MANUF_ID, - .model_id = GENERIC_DEVICE_ID, - .total_size = 2048, - .page_size = 256, - .tested = TEST_OK_PREW, - .probe = probe_wpce775x, - .probe_timing = TIMING_ZERO, - /* .block_erasers is generated according to detected flashchip. */ - .write = write_wpce775x, - .read = read_memmapped, - .wp = &wp_wpce775x, - }, - { NULL } }; diff --git a/programmer.h b/programmer.h index bbe24f9..40fe656 100644 --- a/programmer.h +++ b/programmer.h @@ -478,6 +478,7 @@ enum spi_controller { SPI_CONTROLLER_VIA, SPI_CONTROLLER_WBSIO, SPI_CONTROLLER_MCP6X_BITBANG, + SPI_CONTROLLER_WPCE775XX, #endif #endif #if CONFIG_FT2232_SPI == 1 @@ -563,6 +564,14 @@ int wbsio_spi_send_command(unsigned int writecnt, unsigned int readcnt, int wbsio_spi_read(struct flashchip *flash, uint8_t *buf, int start, int len); #endif
+/* wpce775x.c */ +#if CONFIG_INTERNAL == 1 +int wpce775x_spi_read(struct flashchip *flash, uint8_t *buf, int start, int len); +int wpce775x_spi_write_256(struct flashchip *flash, uint8_t *buf, int start, int len); +int wpce775x_spi_send_command(unsigned int writecnt, unsigned int readcnt, + const unsigned char *writearr, unsigned char *readarr); +#endif + /* serprog.c */ int serprog_init(void); int serprog_shutdown(void); diff --git a/spi.c b/spi.c index 9768bce..91cf615 100644 --- a/spi.c +++ b/spi.c @@ -95,6 +95,13 @@ const struct spi_programmer spi_programmer[] = { .read = bitbang_spi_read, .write_256 = bitbang_spi_write_256, }, + + { /* SPI_CONTROLLER_WPCE775XX */ + .command = wpce775x_spi_send_command, + .multicommand = default_spi_send_multicommand, + .read = wpce775x_spi_read, + .write_256 = wpce775x_spi_write_256, + }, #endif #endif
diff --git a/wpce775x.c b/wpce775x.c index a659893..fe9c35a 100644 --- a/wpce775x.c +++ b/wpce775x.c @@ -2,6 +2,7 @@ * This file is part of the flashrom project. * * Copyright (C) 2010 Google, Inc. + * Copyright (C) 2010 Carl-Daniel Hailfinger * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions @@ -164,12 +165,13 @@ static int nuvoton_get_sio_index(uint16_t *port) /** Call superio to get pre-configured WCB address. * Read LDN 0x0f (SHM) idx:f8-fb (little-endian). */ -static int get_shaw2ba(chipaddr *shaw2ba) +static int get_shaw2ba(chipaddr *shaw2ba, uint8_t *comm_fwh) { uint16_t idx; uint8_t org_ldn; uint8_t win_cfg; uint8_t shm_cfg; + int ret = 0;
if (nuvoton_get_sio_index(&idx) < 0) return -1; @@ -196,27 +198,40 @@ static int get_shaw2ba(chipaddr *shaw2ba) */ win_cfg = sio_read(idx, WPCE775X_WIN_CFG); if (win_cfg & WPCE775X_WIN_CFG_SHWIN_ACC) { - uint8_t idsel; - /* Make sure shared BIOS memory is enabled */ shm_cfg = sio_read(idx, WPCE775X_SHM_CFG); - if ((shm_cfg & WPCE775X_SHM_CFG_BIOS_FWH_EN)) - idsel = 0xf; - else { + if (!(shm_cfg & WPCE775X_SHM_CFG_BIOS_FWH_EN)) { msg_cdbg("Shared BIOS memory is diabled.\n"); msg_cdbg("Please check SHM_CFG:BIOS_FWH_EN.\n"); - goto error; + ret = -1; + goto out; } - - *shaw2ba &= 0x0fffffff; - *shaw2ba |= idsel << 28; + /* FIXME: Check that this is a FWH capable chipset. */ + *comm_fwh = 1; + /* Check if bit 24-27 are set. Intel ICH will never issue any + * LPC firmware memory (FWH) cycles with them cleared. + * FWH cycles only have 28 address bits. + */ + if ((*shaw2ba & (0xf << 24)) != 0xf << 24) { + msg_cerr("WPCE775X shadow window 2 is outside the " + "region addressable with FWH!\n"); + ret = -1; + goto out; + } + /* FWH cycles are only issued for memory mapped to the top + * 16 MB of the 4 GB address space. We could set only the top + * 4 bits since those are ignored by the WPCE775X and we checked + * bits 24-27 to be 0xf above, but setting the top 8 bits makes + * the 16 MB constraint more clear. + */ + *shaw2ba |= 0xff << 24; + } else { + *comm_fwh = 0; } - - sio_write(idx, NUVOTON_SIOCFG_LDN, org_ldn); - return 0; -error: + +out: sio_write(idx, NUVOTON_SIOCFG_LDN, org_ldn); - return -1; + return ret; }
/* Call superio to get pre-configured fwh_id. @@ -232,7 +247,8 @@ static int get_fwh_id(uint8_t *fwh_id)
org_ldn = sio_read(idx, NUVOTON_SIOCFG_LDN); sio_write(idx, NUVOTON_SIOCFG_LDN, NUVOTON_LDN_SHM); - *fwh_id = sio_read(idx, WPCE775X_SHM_CFG); + /* Upper 4 bits of WPCE775X_SHM_CFG store the FWH ID. */ + *fwh_id = sio_read(idx, WPCE775X_SHM_CFG) >> 4; sio_write(idx, NUVOTON_SIOCFG_LDN, org_ldn);
return 0; @@ -322,8 +338,8 @@ static int InitFlash(unsigned char srp)
/* Byte 5: opcode for Write Status Enable JEDEC_EWSR defines 0x50, but W25Q16 - accepts 0x06 to enable write status */ - wcb->field[1] = 0x06; + accepts JEDEC_WREN to enable write status */ + wcb->field[1] = JEDEC_WREN;
/* Byte 6: opcode for Write Enable */ wcb->field[2] = JEDEC_WREN; @@ -362,11 +378,10 @@ static int InitFlash(unsigned char srp) }
/** Read flash vendor/device IDs through EC. - * @param id0, id1, id2, id3 Pointers to store detected IDs. NULL will be ignored. + * @param readarr 4-byte buffer for JEDEC_RDID response. * @return 1 for error; 0 for success. */ -static int ReadId(unsigned char* id0, unsigned char* id1, - unsigned char* id2, unsigned char* id3) +static int ReadId(unsigned char *readarr) { assert_ec_is_free();
@@ -374,42 +389,28 @@ static int ReadId(unsigned char* id0, unsigned char* id1, if (blocked_exec()) return 1;
- msg_cdbg("id0: 0x%2x, id1: 0x%2x, id2: 0x%2x, id3: 0x%2x\n", - wcb->field[0], wcb->field[1], wcb->field[2], wcb->field[3]); - if (id0) { - *id0 = wcb->field[0]; - } - if (id1) { - *id1 = wcb->field[1]; - } - if (id2) { - *id2 = wcb->field[2]; - } - if (id3) { - *id3 = wcb->field[3]; - } + readarr[0] = wcb->field[0]; + readarr[1] = wcb->field[1]; + readarr[2] = wcb->field[2]; + readarr[3] = wcb->field[3];
return 0; }
/** probe if WPCE775x is present. - * @return 0 for error; 1 for success + * @return 1 for error; 0 for success */ -int probe_wpce775x(struct flashchip *flash) +int wpce775x_init(void) { - unsigned char ids[4]; - unsigned long base; uint16_t sio_port; uint8_t srid; uint8_t fwh_id; - uint32_t size; - chipaddr original_memory; - uint32_t original_size; - int i; + uint8_t comm_fwh = 0;
/* detect if wpce775x exists */ if (nuvoton_get_sio_index(&sio_port) < 0) { msg_cdbg("No Nuvoton chip is found.\n"); + /* No chip found is not an error. */ return 0; } srid = sio_read(sio_port, NUVOTON_SIOCFG_SRID); @@ -425,101 +426,51 @@ int probe_wpce775x(struct flashchip *flash) }
/* get the address of Shadow Window 2. */ - if (get_shaw2ba(&wcb_physical_address) < 0) { + if (get_shaw2ba(&wcb_physical_address, &comm_fwh) < 0) { msg_cdbg("Cannot get the address of Shadow Window 2"); - return 0; + return 1; } msg_cdbg("Get the address of WCB(SHA WIN2) at 0x%08x\n", (uint32_t)wcb_physical_address); - wcb = (struct wpce775x_wcb *) - programmer_map_flash_region("WPCE775X WCB", + + /* If we use FWH for Shadow Window 2, we have to set FWH IDSEL in the + * chipset before we try to access Shadow Window 2. + */ + if (comm_fwh) { + if (get_fwh_id(&fwh_id) < 0) { + msg_cdbg("Cannot get fwh_id value.\n"); + return 1; + } + msg_cdbg("get fwh_id: 0x%02x\n", fwh_id); + + /* TODO: set fwh_idsel of chipset. + * Currently, we employ "-p internal:fwh_idsel=0x0000223e". + */ + } + + wcb = (struct wpce775x_wcb *)physmap("WPCE775X WCB", wcb_physical_address, getpagesize() /* min page size */); msg_cdbg("mapped wcb address: %p for physical addr: 0x%08lx\n", wcb, wcb_physical_address); if (!wcb) { msg_perr("FATAL! Cannot map memory area for wcb physical address.\n"); - return 0; + return 1; } memset((void*)wcb, 0, sizeof(*wcb));
- if (get_fwh_id(&fwh_id) < 0) { - msg_cdbg("Cannot get fwh_id value.\n"); - return 0; - } - msg_cdbg("get fwh_id: 0x%02x\n", fwh_id); - - /* TODO: set fwh_idsel of chipset. - Currently, we employ "-p internal:fwh_idsel=0x0000223e". */ - /* Initialize the parameters of EC SHM component */ if (InitFlash(0x00)) - return 0; - - /* Query the flash vendor/device ID */ - if (ReadId(&ids[0], &ids[1], &ids[2], &ids[3])) - return 0; - - /* In current design, flash->virtual_memory is mapped before calling flash->probe(). - * So that we have to update flash->virtual_memory after we know real flash size. - * Unmap allocated memory before allocate new memory. */ - original_size = flash->total_size * 1024; /* original flash size */ - original_memory = flash->virtual_memory; - - for (scan = &flashchips[0]; scan && scan->name; scan++) { - if (!(scan->bustype & CHIP_BUSTYPE_SPI)) { - msg_cdbg("WPCE775x: %s bustype supports no SPI: %s\n", - scan->name, flashbuses_to_text(scan->bustype)); - continue; - } - if ((scan->manufacture_id != GENERIC_MANUF_ID) && - (scan->manufacture_id != ids[0])) { - msg_cdbg("WPCE775x: %s manufacture_id does not match: 0x%02x\n", - scan->name, scan->manufacture_id); - continue; - } - if ((scan->model_id != GENERIC_DEVICE_ID) && - (scan->model_id != ((ids[1]<<8)|ids[2]))) { - msg_cdbg("WPCE775x: %s model_id does not match: 0x%02x\n", - scan->name, scan->model_id); - continue; - } - - msg_cdbg("WPCE775x: found the flashchip %s.\n", scan->name); - - /* Copy neccesary information */ - flash->total_size = scan->total_size; - flash->page_size = scan->page_size; - memcpy(flash->block_erasers, scan->block_erasers, - sizeof(scan->block_erasers)); - /* .block_erase is pointed to EC-specific way. */ - for (i = 0; i < NUM_ERASEFUNCTIONS; ++i) - flash->block_erasers[i].block_erase = erase_wpce775x; - break; - } - if (!scan || !scan->name) { - msg_cdbg("WPCE775x: cannot recognize the flashchip.\n"); - scan = 0; /* since scan is a global variable, reset pointer - * to indicate nothing was detected */ - return 0; - } - - /* Unmap allocated memory before allocate new memory. */ - programmer_unmap_flash_region((void*)original_memory, original_size); - - /* In current design, flash->virtual_memory is mapped before calling flash->probe(). - * So that we have to update flash->virtual_memory after we know real flash size. - * Map new virtual address here. */ - size = flash->total_size * 1024; /* new flash size */ - base = 0xffffffff - size + 1; - flash->virtual_memory = (chipaddr)programmer_map_flash_region("flash chip", base, size); - msg_cdbg("Remap memory to 0x%08lx from base: 0x%08lx size=0x%08lx\n", - flash->virtual_memory, base, (long unsigned int)size); + return 1;
- return 1; + if (buses_supported & CHIP_BUSTYPE_SPI) + msg_pdbg("Overriding chipset SPI with WPCE775x SPI.\n"); + spi_controller = SPI_CONTROLLER_WPCE775XX; + buses_supported |= CHIP_BUSTYPE_SPI; + return 0; }
/** Tell EC to "enter flash update" mode. */ -int EnterFlashUpdate(void) +static int EnterFlashUpdate(void) { if (in_flash_update_mode) { /* already in update mode */ @@ -544,7 +495,7 @@ int EnterFlashUpdate(void) * Without calling this function, the EC stays in busy-loop and will not * response further request from host, which means system will halt. */ -int ExitFlashUpdate(unsigned char exit_code) +static int ExitFlashUpdate(unsigned char exit_code) { if (in_flash_update_mode <= 0) { msg_cdbg("Not in flash update mode yet.\n"); @@ -571,62 +522,56 @@ int ExitFlashUpdate(unsigned char exit_code) * 0x20 is used for EC F/W no change, but BIOS changed (in Share mode) * 0x21 is used for EC F/W changed. Goto EC F/W, wait system reboot. * 0x22 is used for EC F/W changed, Goto EC Watchdog reset. */ -int ExitFlashUpdateFirmwareNoChange(void) { +static int ExitFlashUpdateFirmwareNoChange(void) { return ExitFlashUpdate(0x20); }
-int ExitFlashUpdateFirmwareChanged(void) { +static int ExitFlashUpdateFirmwareChanged(void) { return ExitFlashUpdate(0x21); }
-int erase_wpce775x(struct flashchip *flash, unsigned int blockaddr, unsigned int blocklen) +static int wpce775x_set_write_window(unsigned int base, unsigned int size) { assert_ec_is_free();
/* Set Write Window on flash chip (optional). * You may limit the window to partial flash for experimental. */ wcb->code = 0xC5; /* Set Write Window */ - wcb->field[0] = 0x00; /* window base: little-endian */ - wcb->field[1] = 0x00; - wcb->field[2] = 0x00; - wcb->field[3] = 0x00; - wcb->field[4] = 0x00; /* window length: little-endian */ - wcb->field[5] = 0x00; - wcb->field[6] = 0x20; - wcb->field[7] = 0x00; + wcb->field[0] = base & 0xff; /* window base: little-endian */ + wcb->field[1] = (base >> 8) & 0xff; + wcb->field[2] = (base >> 16) & 0xff; + wcb->field[3] = (base >> 24) & 0xff; + wcb->field[4] = size & 0xff; /* window length: little-endian */ + wcb->field[5] = (size >> 8) & 0xff; + wcb->field[6] = (size >> 16) & 0xff; + wcb->field[7] = (size >> 24) & 0xff; if (blocked_exec()) return 1; + return 0; +}
- /* TODO: here assume block sizes are identical. The right way is to traverse - * block_erasers[] and find out the corresponding block size. */ - unsigned int block_size = flash->block_erasers[0].eraseblocks[0].size; - unsigned int current; - - msg_cdbg("Erasing ... 0x%08x 0x%08x\n", blockaddr, blocklen); +static int handle_erase_wpce775x(unsigned int blockaddr) +{ + /* FIXME: A 2 MByte window is too small. */ + if (wpce775x_set_write_window(0x0, 0x200000)) + return 1;
- if (EnterFlashUpdate()) return 1; + assert_ec_is_free();
- for (current = 0; - current < blocklen; - current += block_size) { - wcb->code = 0x80; /* Sector/block erase */ - - /* WARNING: assume the block address for EC is lalways little-endian. */ - unsigned int addr = blockaddr + current; - wcb->field[0] = addr & 0xff; - wcb->field[1] = (addr >> 8) & 0xff; - wcb->field[2] = (addr >> 16) & 0xff; - wcb->field[3] = (addr >> 24) & 0xff; - if (blocked_exec()) - goto error_exit; - } + if (EnterFlashUpdate()) + return 1;
- if (ExitFlashUpdateFirmwareChanged()) return 1; + wcb->code = 0x80; /* Sector/block erase */ + /* WARNING: assume the block address for EC is always little-endian. */ + wcb->field[0] = blockaddr & 0xff; + wcb->field[1] = (blockaddr >> 8) & 0xff; + wcb->field[2] = (blockaddr >> 16) & 0xff; + wcb->field[3] = (blockaddr >> 24) & 0xff; + if (blocked_exec()) + goto error_exit;
- if (check_erased_range(flash, blockaddr, blocklen)) { - msg_perr("ERASE FAILED!\n"); + if (ExitFlashUpdateFirmwareChanged()) return 1; - }
return 0;
@@ -635,55 +580,37 @@ error_exit: return 1; }
-/** Callback function for do_romentries(). - * @flash - point to flash info - * @buf - the address of buffer - * @addr - the start offset in buffer - * @len - length to write (start from @addr) +/** + * Page write helper */ -int write_wpce775x_entry(struct flashchip *flash, uint8_t *buf, - const chipaddr addr, size_t len) +static int write_wpce775x_entry(unsigned long writeaddr, unsigned long writelen, const unsigned char *buf) { - chipaddr current; - unsigned int block_size = flash->block_erasers[0].eraseblocks[0].size; - unsigned int bytes_until_next_block; - unsigned int written = 0; + if (writelen > 8) { + msg_perr("WTF did you call %s with writelen=%li\n", __func__, writelen); + return 1; + }
- msg_cdbg("Writing ... 0x%08lx 0x%08lx\n", (long unsigned int)addr, - (long unsigned int)len); + /* FIXME: assert_ec_is_free() missing? */ + if (EnterFlashUpdate()) + return 1;
- if (EnterFlashUpdate()) return 1; - for (current = addr; - current < addr + len; - current += written/* maximum program buffer */) { - /* erase sector before write it. */ - if ((current & (block_size-1))==0) { - if (erase_wpce775x(flash, current, block_size)) - goto error_exit; - } + /* wpce775x provides a 8-byte program buffer. */ + wcb->code = 0xA0; /* Set Address */ + wcb->field[0] = writeaddr & 0xff; + wcb->field[1] = (writeaddr >> 8) & 0xff; + wcb->field[2] = (writeaddr >> 16) & 0xff; + wcb->field[3] = (writeaddr >> 24) & 0xff; + if (blocked_exec()) + goto error_exit;
- /* wpce775x provides a 8-byte program buffer. */ - wcb->code = 0xA0; /* Set Address */ - wcb->field[0] = current & 0xff; - wcb->field[1] = (current >> 8) & 0xff; - wcb->field[2] = (current >> 16) & 0xff; - wcb->field[3] = (current >> 24) & 0xff; - if (blocked_exec()) - goto error_exit; - - bytes_until_next_block = block_size - (current % block_size); - if (bytes_until_next_block > 0 && bytes_until_next_block < 8) - written = bytes_until_next_block; - else - written = 8; /* maximum buffer size */ - wcb->code = 0xB0 | written; - int i; - for (i = 0; i < written; i++) { - wcb->field[i] = buf[current + i]; - } - if (blocked_exec()) - goto error_exit; + /* 8 is maximum buffer size */ + wcb->code = 0xB0 | writelen; + int i; + for (i = 0; i < writelen; i++) { + wcb->field[i] = buf[i]; } + if (blocked_exec()) + goto error_exit;
if (ExitFlashUpdateFirmwareChanged()) return 1; return 0; @@ -693,40 +620,11 @@ error_exit: return 1; }
-/** Write data to flash (layout supported). - * In some cases, EC and BIOS share a physical flash chip. So it is dangerous - * to erase whole flash chip when you only wanna update BIOS image. - * By calling do_romentries(), we won't erase/program those blocks/sectors - * not specified by -i parameter. - */ -int write_wpce775x(struct flashchip *flash, uint8_t * buf) -{ - return do_romentries(buf, flash, write_wpce775x_entry); -} - -int set_range_wpce775x(struct flashchip *flash, unsigned int start, unsigned int len) { - struct w25q_status status; - - if (w25_range_to_status(scan, start, len, &status)) return -1; - - /* Since WPCE775x doesn't support reading status register, we have to - * set SRP0 to 1 when writing status register. */ - status.srp0 = 1; - - msg_cdbg("Going to set: 0x%02x\n", *((unsigned char*)&status)); - msg_cdbg("status.busy: %x\n", status.busy); - msg_cdbg("status.wel: %x\n", status.wel); - msg_cdbg("status.bp0: %x\n", status.bp0); - msg_cdbg("status.bp1: %x\n", status.bp1); - msg_cdbg("status.bp2: %x\n", status.bp2); - msg_cdbg("status.tb: %x\n", status.tb); - msg_cdbg("status.sec: %x\n", status.sec); - msg_cdbg("status.srp0: %x\n", status.srp0); - +static int wpce775x_wrsr(const unsigned char status) { /* InitFlash (with particular status value), and EnterFlashUpdate() then * ExitFlashUpdate() immediately. Thus, the flash status register will * be updated. */ - if (InitFlash(*(unsigned char*)&status)) + if (InitFlash(status)) return -1; if (EnterFlashUpdate()) return 1; ExitFlashUpdateFirmwareNoChange(); @@ -734,14 +632,59 @@ int set_range_wpce775x(struct flashchip *flash, unsigned int start, unsigned int return 0; }
-static int enable_wpce775x(struct flashchip *flash) +int wpce775x_spi_send_command(unsigned int writecnt, unsigned int readcnt, + const unsigned char *writearr, unsigned char *readarr) { - msg_cdbg("WPCE775x always sets SRP0 in set_range_wpce775x()\n"); - return 0; + unsigned char ids[4]; + unsigned int addr = 0; + + /* Ugly hack. Not my fault. -- Carl-Daniel */ + switch(writearr[0]) { + case JEDEC_RDID: + /* We always read 4 ID bytes. Return only the wanted parts. */ + if (ReadId(ids)) + return 1; + memcpy(readarr, ids, min(readcnt, 4)); + break; + case JEDEC_BYTE_PROGRAM: + /* FIXME: Precedence! */ + addr = writearr[1] << 16 | writearr[2] << 8 | writearr[3]; + if (write_wpce775x_entry(addr, writecnt - 4, writearr + 4)) + return 1; + break; + case JEDEC_SE: + /* FIXME: Precedence! */ + addr = writearr[1] << 16 | writearr[2] << 8 | writearr[3]; + if (handle_erase_wpce775x(addr)) + return 1; + break; + case JEDEC_WRSR: + if (wpce775x_wrsr(writearr[1])) + return 1; + break; + case JEDEC_WREN: + case JEDEC_RDSR: + default: + msg_perr("You are truly screwed. This command is not " + "implemented (yet)\n"); + return 1; + } + return 0; }
+/* FIXME: This command is not implemented. We have to use read_memmapped, but + * the mapping window is too small for reasonably sized chips. + */ +int wpce775x_spi_read(struct flashchip *flash, uint8_t *buf, int start, int len) +{ + if (flash->total_size * 1024 > 2 * 1024 * 1024) { + msg_perr("Chip is too big for reading on wpce775x.\n"); + return 1; + } + return read_memmapped(flash, buf, start, len); +}
-struct wp wp_wpce775x = { - .set_range = set_range_wpce775x, - .enable = enable_wpce775x, -}; +int wpce775x_spi_write_256(struct flashchip *flash, uint8_t *buf, int start, int len) +{ + return spi_write_chunked(flash, buf, start, len, 8); +}