From f3bdd313f9a2add021f802fe59392a4f8ed9ee54 Mon Sep 17 00:00:00 2001 From: Le Khanh Binh Date: Mon, 24 Aug 2026 23:23:29 +0700 Subject: [PATCH 1/3] Use safe PIO timing --- ata2.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ata2.c b/ata2.c index a7b5970..fa25b09 100644 --- a/ata2.c +++ b/ata2.c @@ -161,7 +161,14 @@ uint32 ata_init(void) { outl(inl(0xc3000028) | 0x20, 0xc3000028); // clear intr outl(inl(0xc3000028) & ~0x10000000, 0xc3000028); // reset? - outl(0x10, 0xc3000000); + /* PIO timing for IDE0_PRI_TIMING0. + * Rockbox originally used 0x10 here (faster PIO), but that causes + * corrupt data when used with mSATA adapters and some SD adapters. + * Use the original firmware timing 0xC293 instead, which is safe + * for all adapters. + * See Rockbox firmware/target/arm/pp/ata-pp5020.c pio80mhz[]. + */ + outl(0xC293, 0xc3000000); outl(0x80002150, 0xc3000004); } else { /* PP5002 */ From 0b0ac342770e2124a901b0253b7d698f3e0d14d5 Mon Sep 17 00:00:00 2001 From: Le Khanh Binh Date: Tue, 25 Aug 2026 10:18:21 +0700 Subject: [PATCH 2/3] Rewrite ATA, fix display power off command,... --- .github/workflows/build.yml | 32 ++ Makefile | 7 +- ata2.c | 777 ++++++++++++++++++++++++++++++------ ata2_definitions.h | 49 ++- loader.c | 32 +- menu.c | 29 +- menu.h | 2 +- minilibc.c | 10 +- minilibc.h | 1 + 9 files changed, 789 insertions(+), 150 deletions(-) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..a48acd5 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,32 @@ +name: Build iPodLoader2 + +on: + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install ARM toolchain + run: sudo apt-get update && sudo apt-get install -y gcc-arm-none-eabi + + - name: Build (release) + run: | + make clean && make + mv loader.bin loader-release.bin + + - name: Build (debug) + run: | + make clean && make debug + mv loader.bin loader-debug.bin + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: loader-binaries + path: | + loader-release.bin + loader-debug.bin + retention-days: 30 diff --git a/Makefile b/Makefile index c058cc4..1a0dfb7 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,12 @@ endif endif # Suffix with "d" for development version, "b" for beta version -VERSION = 2.9.0d +VERSION = 2.9.0 + +# debug builds get the "d" suffix (development version) +ifneq (,$(filter debug,$(MAKECMDGOALS))) +VERSION := $(VERSION)d +endif ifdef GIT_COMMIT_HASH VERSION := "$(VERSION) $(GIT_COMMIT_HASH)" diff --git a/ata2.c b/ata2.c index fa25b09..85addce 100644 --- a/ata2.c +++ b/ata2.c @@ -67,13 +67,32 @@ static struct { */ uint8 alignment_log2; + /* Number of sectors per block for READ MULTIPLE, set via the + * SET MULTIPLE MODE command. 1 = single-sector reads. */ + uint8 multisectors; + + /* Non-zero if READ MULTIPLE / READ MULTIPLE_EXT should be used */ + uint8 use_multiple; + + /* Drive power state (see ATA_DRIVE_* below). Used to know whether + * the drive needs a wake-up reset before the next read. */ + uint8 state; + /* The total number of sectors the device has */ uint64 sectors; } ATAdev; +/* Drive power state constants */ +#define ATA_DRIVE_OFF 0 +#define ATA_DRIVE_ON 1 +#define ATA_DRIVE_SLEEPING 2 + /* Forward declaration of static functions (not exported via header file) */ -static inline void spinwait_drive_busy(void); -static inline void bug_on_ata_error(void); +static inline int spinwait_drive_busy(void); +static int wait_for_bsy(void); +static int wait_for_rdy(void); +static int perform_soft_reset(void); +static int check_ata_error(void); static void ata_clear_intr(void); static inline void clear_cache(void); static inline int create_cache_entry(uint32 sector); @@ -83,6 +102,11 @@ static void ata_send_read_command(uint32 lba, uint16 count); static uint32 ata_transfer_block(void *ptr, uint32 count); static uint32 ata_receive_read_data(void *dst, uint32 count); static int ata_readblock2(void *dst, uint32 sector, int useCache); +static int set_multiple_mode(int sectors); +static int ata_test_read_sector(uint32 sector); +static int freeze_lock(uint16 *identify_buf); +static int set_features(uint16 *identify_buf); +static int ata_perform_wakeup(void); inline static void pio_outbyte(unsigned int addr, unsigned char data) { @@ -121,6 +145,370 @@ inline static void ata_command(uint8 cmd) { pio_inbyte(REG_ALTSTATUS); pio_inbyte(REG_ALTSTATUS); \ } +/* + * Wait until BSY clears. + * Uses timer_get_current()/timer_passed() for timeout (~30 seconds). + * Returns 1 on success (BSY cleared), 0 on timeout. + */ +static int wait_for_bsy(void) { + unsigned long timeout_start = timer_get_current(); + while(pio_inbyte(REG_ALTSTATUS) & STATUS_BSY) { + if(timer_passed(timeout_start, 30 * TIMER_SECOND)) { + return 0; /* timeout */ + } + } + return 1; +} + +/* + * Wait until drive is ready (BSY cleared and RDY set). + * First waits for BSY to clear, then waits for RDY. + * Returns 1 on success, 0 on timeout. + */ +static int wait_for_rdy(void) { + if(!wait_for_bsy()) { + return 0; + } + + unsigned long timeout_start = timer_get_current(); + while(!(pio_inbyte(REG_ALTSTATUS) & STATUS_DRDY)) { + if(timer_passed(timeout_start, 10 * TIMER_SECOND)) { + return 0; /* timeout */ + } + } + return 1; +} + +/* + * Perform an ATA software reset (SRST). + * Follows the ATA spec section 9.2 reset protocol. + * Returns 0 on success, -1 on timeout. + */ +static int perform_soft_reset(void) { + int retry_count; + + /* Select device */ + pio_outbyte(REG_DEVICEHEAD, 0xA0 | DEVICE_0); + DELAY400NS; + + /* Assert SRST */ + pio_outbyte(REG_CONTROL, CONTROL_NIEN | CONTROL_SRST); + mlc_delay_ms(1); /* >= 5us per spec */ + + /* Deassert SRST */ + pio_outbyte(REG_CONTROL, CONTROL_NIEN); + mlc_delay_ms(2); /* > 2ms per spec */ + + /* Wait for ready, with retries. Some drives take a while. */ + retry_count = 8; + do { + if(wait_for_rdy()) { + break; + } + } while(--retry_count); + + if(!retry_count) { + return -1; + } + + return 0; +} + +/* + * Issue SET MULTIPLE MODE command to the drive. + * This tells the drive how many sectors to transfer per DRQ assertion + * when using READ MULTIPLE / WRITE MULTIPLE commands. + * sectors: the number of sectors per block (0 = 256). + * Returns 0 on success, -1 on error. + */ +static int set_multiple_mode(int sectors) { + pio_outbyte(REG_DEVICEHEAD, 0xA0 | LBA_ADDRESSING | DEVICE_0); + if(!wait_for_rdy()) { + return -1; + } + pio_outbyte(REG_SECT_COUNT, sectors & 0xFF); + pio_outbyte(REG_LBA0, 0); + pio_outbyte(REG_LBA1, 0); + pio_outbyte(REG_LBA2, 0); + ata_command(COMMAND_SET_MULTIPLE_MODE); + DELAY400NS; + if(!wait_for_bsy()) { + return -1; + } + uint8 status = pio_inbyte(REG_STATUS); + if(status & STATUS_ERR) { + return -1; + } + return 0; +} + +/* + * Test whether the drive can read a single (possibly unaligned) sector. + * Used to detect 512e emulation (drive handles unaligned reads internally). + * Does NOT call fatal error handlers - returns the result instead. + * Returns 0 on success (drive supports 512e), -1 on error. + */ +static int ata_test_read_sector(uint32 sector) { + int timeout; + + pio_outbyte(REG_DEVICEHEAD, 0xA0 | LBA_ADDRESSING | DEVICE_0); + + if(!wait_for_rdy()) { + return -1; + } + + pio_outbyte(REG_FEATURES, 0); + pio_outbyte(REG_CONTROL, CONTROL_NIEN); + pio_outbyte(REG_SECT_COUNT, 1); + pio_outbyte(REG_LBA0, sector & 0xFF); + pio_outbyte(REG_LBA1, (sector >> 8) & 0xFF); + pio_outbyte(REG_LBA2, (sector >> 16) & 0xFF); + + ata_command(COMMAND_READ_SECTORS); + DELAY400NS; + + /* Wait for DRQ with timeout */ + timeout = 10 * TIMER_SECOND; + unsigned long start = timer_get_current(); + while(!((pio_inbyte(REG_ALTSTATUS)) & STATUS_DRQ)) { + if(pio_inbyte(REG_ALTSTATUS) & STATUS_ERR) { + /* Error before DRQ - read status to clear */ + pio_inbyte(REG_STATUS); + return -1; + } + if(timer_passed(start, timeout)) { + return -1; + } + } + + /* Read 256 words (512 bytes) of data and discard them */ + for(int i = 0; i < 256; i++) { + (void)inw(pio_reg_addrs[REG_DATA]); + } + + /* Wait for BSY to clear */ + wait_for_bsy(); + + /* Check final status */ + uint8 status = pio_inbyte(REG_STATUS); + if(status & (STATUS_ERR | STATUS_DF)) { + return -1; + } + + return 0; +} + +/* + * PIO timing values for each mode, from Rockbox ata-pp5020.c pio80mhz[]. + * Index = PIO mode number (0-4). + */ +static const unsigned long pio_timing_table[] = { + 0xC293, /* Mode 0 - always safe */ + 0x43A2, /* Mode 1 */ + 0x11A1, /* Mode 2 */ + 0x7232, /* Mode 3 */ + 0x3131, /* Mode 4 */ +}; + +/* + * Issue SECURITY FREEZE LOCK command if the device supports it. + * This prevents the device from entering any security state (e.g., user + * password lock) which could lock out the host permanently. + * See: Rockbox firmware/drivers/ata.c freeze_lock() + * See: ATA-ATAPI-6 section 9.15. + */ +static int freeze_lock(uint16 *identify_buf) { + /* Word 82 bit 1: Security Mode feature set supported */ + if(identify_buf[82] & 0x02) { + pio_outbyte(REG_DEVICEHEAD, 0xA0 | DEVICE_0); + if(!wait_for_rdy()) return -1; + + ata_command(COMMAND_SECURITY_FREEZE_LOCK); + DELAY400NS; + + if(!wait_for_rdy()) return -1; + mlc_printf("FREEZE LOCK OK\n"); + } + return 0; +} + +/* + * Negotiate PIO mode, enable write cache, and enable read look-ahead + * via SET FEATURES command. + * See: Rockbox firmware/drivers/ata.c set_features() + * + * The highest supported PIO mode is determined from IDENTIFY words + * 53 and 64. SET FEATURES subcommand 0x03 sets the PIO mode, and + * the controller timing register is updated to match. + * + * Returns 0 on success, -1 on error. + */ +static int set_features(uint16 *identify_buf) { + ipod_t *ipod = ipod_get_hwinfo(); + int pio_mode = 0; /* safe default */ + + /* Determine the highest supported PIO mode from IDENTIFY data. + * Word 53 bit 1: word 64 is valid. + * Word 64 bits 2:1: PIO modes supported (bit 1 = mode 3, bit 2 = mode 4). + */ + if(identify_buf[53] & (1 << 1)) { + if(identify_buf[64] & (1 << 2)) + pio_mode = 4; + else if(identify_buf[64] & (1 << 1)) + pio_mode = 3; + else if(identify_buf[64] & (1 << 0)) + pio_mode = 2; + } + + /* SET FEATURES: set PIO mode (subcommand 0x03, parameter = 8 + mode). + * Try the highest mode once. If rejected, fall back to mode 0 (one retry). + */ + pio_outbyte(REG_DEVICEHEAD, 0xA0 | DEVICE_0); + if(wait_for_rdy()) { + pio_outbyte(REG_FEATURES, 0x03); + pio_outbyte(REG_SECT_COUNT, 8 + pio_mode); + ata_command(COMMAND_SET_FEATURES); + DELAY400NS; + if(!wait_for_bsy() || (pio_inbyte(REG_STATUS) & STATUS_ERR)) { + /* Rejected - try mode 0 if we weren't already there */ + if(pio_mode != 0) { + pio_mode = 0; + pio_outbyte(REG_DEVICEHEAD, 0xA0 | DEVICE_0); + if(wait_for_rdy()) { + pio_outbyte(REG_FEATURES, 0x03); + pio_outbyte(REG_SECT_COUNT, 8); + ata_command(COMMAND_SET_FEATURES); + DELAY400NS; + wait_for_bsy(); + } + } + } + } + + /* Update the PIO timing register to match the negotiated mode */ + if(ipod->hw_ver > 3) { + /* PP5020 */ + if(pio_mode >= 0 && pio_mode <= 4) { + outl(pio_timing_table[pio_mode], 0xc3000000); + } + } else { + /* PP5002 - same timing register at different address */ + if(pio_mode >= 0 && pio_mode <= 4) { + outl(pio_timing_table[pio_mode], 0xc0003000); + } + } + + /* Enable volatile write cache (subcommand 0x02) */ + pio_outbyte(REG_DEVICEHEAD, 0xA0 | DEVICE_0); + if(wait_for_rdy()) { + pio_outbyte(REG_FEATURES, 0x02); + pio_outbyte(REG_SECT_COUNT, 0); + ata_command(COMMAND_SET_FEATURES); + DELAY400NS; + wait_for_bsy(); + if(!(pio_inbyte(REG_STATUS) & STATUS_ERR)) { + mlc_printf("Write cache enabled\n"); + } + } + + /* Enable read look-ahead (subcommand 0xAA) */ + pio_outbyte(REG_DEVICEHEAD, 0xA0 | DEVICE_0); + if(wait_for_rdy()) { + pio_outbyte(REG_FEATURES, 0xAA); + pio_outbyte(REG_SECT_COUNT, 0); + ata_command(COMMAND_SET_FEATURES); + DELAY400NS; + wait_for_bsy(); + if(!(pio_inbyte(REG_STATUS) & STATUS_ERR)) { + mlc_printf("Read look-ahead enabled\n"); + } + } + + return 0; +} + +/* + * Wake the drive from sleep/standby state. + * A soft reset brings the drive back to a ready state, but it also + * clears the multi-sector mode setting, so that has to be re-issued. + * Called automatically on first read after sleep. + * See: Rockbox firmware/drivers/ata.c ata_perform_wakeup() + */ +static int ata_perform_wakeup(void) { + mlc_printf("Waking drive...\n"); + + if(perform_soft_reset() != 0) { + mlc_printf("Wake reset failed\n"); + return -1; + } + + /* A reset clears the multi-sector mode setting */ + if(ATAdev.use_multiple) { + set_multiple_mode(ATAdev.multisectors); + } + + ATAdev.state = ATA_DRIVE_ON; + mlc_printf("Drive awake\n"); + return 0; +} + +#ifdef HAVE_ATA_SMART +/* + * Read and display S.M.A.R.T. health data from the drive. + * This is informational - it doesn't affect drive operation. + * See: Rockbox firmware/drivers/ata.c ata_smart() + */ +static void ata_read_smart(uint16 *identify_buf) { + /* Check if SMART is supported (word 82 bit 0) */ + if(!(identify_buf[82] & 0x01)) { + return; + } + + uint16 smart_data[256]; + + pio_outbyte(REG_DEVICEHEAD, 0xA0 | DEVICE_0); + if(!wait_for_rdy()) return; + + pio_outbyte(REG_FEATURES, SMART_READ_DATA); + pio_outbyte(REG_LBA2, 0x4F); + pio_outbyte(REG_LBA1, 0xC2); + pio_outbyte(REG_SECT_COUNT, 1); + ata_command(COMMAND_SMART); + DELAY400NS; + + if(!wait_for_bsy()) return; + + uint8 status = pio_inbyte(REG_STATUS); + if(status & STATUS_ERR) { + /* SMART command failed - read error to clear */ + pio_inbyte(REG_ERROR); + return; + } + + if(!(status & STATUS_DRQ)) return; + + /* Read 256 words of SMART data */ + for(int i = 0; i < 256; i++) { + smart_data[i] = inw(pio_reg_addrs[REG_DATA]); + } + wait_for_bsy(); + + /* Display key SMART attributes: + * Word 361: Temperature (current) + * Word 362: Reallocated sector count (worst) + * Word 364: Power-on hours + * Word 5: Normalized air flow temperature + */ + mlc_printf("SMART: Temp=%dC", smart_data[194] & 0xFF); + + uint16 reallocated = smart_data[197]; /* Reallocated event count */ + if(reallocated > 0) { + mlc_printf(" REALLOC=%d", reallocated); + } + mlc_printf("\n"); +} +#endif + uint32 ata_init(void) { ipod_t *ipod; @@ -144,15 +532,6 @@ uint32 ata_init(void) { pio_reg_addrs[ REG_CONTROL ] = pio_base_addr2 + 6 * 4; pio_reg_addrs[ REG_DA ] = pio_base_addr2 + 7 * 4; - /* - * Registers for LBA48. - * These are one byte address above their LBA28 counterparts. - */ - pio_reg_addrs[ REG_SECCOUNT_HIGH ] = pio_reg_addrs[ REG_SECCOUNT_LOW ] + 1; - pio_reg_addrs[ REG_LBA3 ] = pio_reg_addrs[ REG_LBA0 ] + 1; - pio_reg_addrs[ REG_LBA4 ] = pio_reg_addrs[ REG_LBA1 ] + 1; - pio_reg_addrs[ REG_LBA5 ] = pio_reg_addrs[ REG_LBA2 ] + 1; - /* * Black magic */ @@ -179,6 +558,24 @@ uint32 ata_init(void) { outl(0x80002150, 0xc0003004); } + /* Reset ATA device state */ + ATAdev.lba48 = 0; + ATAdev.alignment_log2 = 0; + ATAdev.multisectors = 0; + ATAdev.use_multiple = 0; + ATAdev.sectors = 0; + ATAdev.state = ATA_DRIVE_OFF; + + /* Perform a soft reset to initialize the drive to a known state. + * This is critical for mSATA/SD adapters and SSDs that may not be + * in a clean state after power-on. Without this, IDENTIFY and + * subsequent reads may return garbage. + * See Rockbox firmware/drivers/ata.c perform_soft_reset(). + */ + if(perform_soft_reset() != 0) { + mlc_printf("ATA reset timeout, trying direct...\n"); + } + /* 1st things first, check if there is an ATA controller here * We do this by writing values to two GP registers, and expect * to be able to read them back @@ -213,6 +610,7 @@ uint32 ata_init(void) { /* Initialize cache */ clear_cache(); + ATAdev.state = ATA_DRIVE_ON; return(0); } @@ -231,33 +629,29 @@ void ata_exit(void) } /* - * Spinwait until the drive is not busy + * Spinwait until the drive is not busy, with a timeout. + * Returns 0 on success, -1 on timeout. + * Timeout is ~30 seconds (generous for spin-up, tight enough to not hang forever). */ -static inline void spinwait_drive_busy(void) { - /* Force this busyloop to not be optimised away */ - while( pio_inbyte( REG_ALTSTATUS) & STATUS_BSY ) __asm__ __volatile__(""); +static inline int spinwait_drive_busy(void) { + uint32 timeout = 0x4000000; + while( pio_inbyte( REG_ALTSTATUS) & STATUS_BSY ) { + if(--timeout == 0) return -1; + } + return 0; } /* - * Checks for ATA error, and upon error prints the error and fatal bugchecks + * Checks for ATA error after a command completes. + * Returns 0 if no error, or a non-zero error code (STATUS_ERR, STATUS_DF, etc.) + * Does NOT call fatal error handlers - the caller decides what to do. */ -static inline void bug_on_ata_error(void) { +static inline int check_ata_error(void) { uint8 status = pio_inbyte( REG_STATUS ); - if(status & STATUS_ERR) { - uint8 error = pio_inbyte( REG_ERROR ); - mlc_printf("\nATA2 IO Error\n"); - mlc_printf("STATUS: %02hhX, ", status); - mlc_printf("ERROR: %02hhX\n", error); - mlc_printf("LAST COMMAND: %02hhX\n", last_command); - if(last_command == COMMAND_READ_SECTORS - || last_command == COMMAND_READ_SECTORS_EXT) { - mlc_printf("SECTOR: %d, ", last_sector); - mlc_printf("COUNT: %d\n", last_sector_count); - } - - mlc_show_fatal_error (); - return; + if(status & (STATUS_ERR | STATUS_DF)) { + return status; } + return 0; } @@ -276,7 +670,8 @@ void ata_standby(int cmd_variation) DELAY400NS; /* Wait until drive is not busy */ - spinwait_drive_busy(); + spinwait_drive_busy(); + /* Read the status register to clear any pending interrupts */ pio_inbyte( REG_STATUS ); @@ -286,6 +681,7 @@ void ata_standby(int cmd_variation) * This interrupt is then to be ignored. */ ata_clear_intr(); + ATAdev.state = ATA_DRIVE_SLEEPING; } void ata_sleep() { @@ -294,13 +690,14 @@ void ata_sleep() { spinwait_drive_busy(); DELAY400NS; DELAY400NS; /* - * When the device is ready to ender sleep mode, it will set an interrupt and wait. + * When the device is ready to enter sleep mode, it will set an interrupt and wait. * It will then wait until we clear that interrupt by reading the STATUS register * to actually enter sleep mode. */ pio_inbyte( REG_STATUS ); /* The device should now be asleep, and will not respond until a DEVICE_RESET command is sent. */ + ATAdev.state = ATA_DRIVE_SLEEPING; } /* @@ -349,7 +746,14 @@ static int strncmp_be16(char* str1, size_t str1_start, uint16* str2, size_t str2 * Does some extended identification of the ATA device */ void ata_identify(void) { - uint16 *buff = (uint16*)mlc_malloc(sizeof(uint16) * 256); // TODO: Remove unnecessary allocation + uint16 *buff = (uint16*)mlc_malloc(sizeof(uint16) * 256); + + /* Wait for drive to be ready before sending IDENTIFY */ + if(!wait_for_rdy()) { + mlc_printf("HDD not ready for identify\n"); + mlc_show_fatal_error(); + return; + } pio_outbyte( REG_DEVICEHEAD, 0xA0 | DEVICE_0 ); pio_outbyte( REG_FEATURES , 0 ); @@ -362,7 +766,11 @@ void ata_identify(void) { ata_command( COMMAND_IDENTIFY_DEVICE ); DELAY400NS; - ata_receive_read_data(buff, 1); + if(!ata_receive_read_data(buff, 1)) { + mlc_printf("HDD identify failed\n"); + mlc_show_fatal_error(); + return; + } /* * Verify the IDENTIFY DEVICE response integrity @@ -496,48 +904,107 @@ void ata_identify(void) { mlc_printf("Size: %lu.%luGB\n", (uint32)(size_mb / 1024), (uint32)((size_mb % 1024) / 10)); /* - * HDD quirks: + * Determine physical sector size from IDENTIFY word 106. + * Word 106 bits 15:13 indicate whether the field is valid: + * Bit 15 = 0, Bit 14 = 1, Bit 13 = 1 -> valid. + * Bits 3:0 give log2 of (physical sector size / logical sector size). + * If valid, the physical sector multiplier is 1 << (word106 & 0xF). * - * The iPod 5.5G 80GB uses the "TOSHIBA MK8010GAH" ZIF hard drive. - * MK = Prefix - * 80 = 80GB - * 10 = DSMR (Dual Stripe MR Head) - * G = >10GB capacity - * A = PATA - * H = 1.8", 8mm thick, 4200RPM. - * - * The Toshiba 10GAH and 31GAL drive families have a quirk where they only support reading whole physical sectors. - * The logical sector (block) size is still 512 bytes, however reads will only succeed if: - * - The start LBA is aligned to the physical sector boundary; and - * - The number of blocks read is an exact multiple of the physical sector size. - * - * The 10GAH family has 1024 byte (2 block) physical sectors. The 31GAL family has 4096 byte (8 block) physical sectors. - * - * This means that for the iPod 5.5G MK8010GAH, we can only read even numbered LBAs, and the count must be a multiple of 2. - * To read an odd LBA, read the even numbered LBA below it, and read 2 blocks to cover the LBA actually requested. - */ - - /* "TOSHIBA ????10GAH" */ - if(strncmp_be16("TOSHIBA ", 0, hdd_model, 0, sizeof("TOSHIBA ") - 1) == 0 - && strncmp_be16("10GAH", 0, hdd_model, 12, sizeof("10GAH") - 1) == 0) - { - mlc_printf("Enabling TOSHIBA 10GAH quirks\n"); - /* 1024 byte physical sectors, 2 blocks per physical sector */ - ATAdev.alignment_log2 = 1; - } - else { - if(size_mb > (127 * 1024)) { - /* HDD is larger than 127GB, it's probably a 4K sector HDD, or a flash modded iPod */ - /* Do 4K, 8 block sector reads */ - mlc_printf("Large drive, enabling 4K reads\n"); - ATAdev.alignment_log2 = 3; + * See: ATA-ATAPI-8 specification, Word 106. + * See: Rockbox firmware/drivers/ata-common.c ata_get_phys_sector_mult() + */ + uint32 phys_sector_mult = 1; + if((buff[106] & 0xe000) == 0x6000) { + /* Word 106 is valid */ + phys_sector_mult = 1u << (buff[106] & 0x000f); + mlc_printf("Physical sector mult: %lu\n", phys_sector_mult); + } else { + /* Word 106 not reported. Fall back to model-based heuristic. */ + if(strncmp_be16("TOSHIBA ", 0, hdd_model, 0, sizeof("TOSHIBA ") - 1) == 0 + && strncmp_be16("10GAH", 0, hdd_model, 12, sizeof("10GAH") - 1) == 0) + { + mlc_printf("TOSHIBA 10GAH detected\n"); + phys_sector_mult = 2; } - else { - /* 512 byte physical sectors, 1 blocks per physical sector */ + } + + if(phys_sector_mult > 1) { + /* + * The device reports large physical sectors. Check if it actually + * needs aligned reads by testing a read of sector 1 (which is + * NOT aligned to a physical sector boundary for mult > 1). + * If the read succeeds, the device supports 512e emulation and + * handles alignment internally - we don't need to align. + * + * See: Rockbox firmware/drivers/ata-common.c ata_get_phys_sector_mult() + */ + mlc_printf("Testing 512e emulation...\n"); + if(ata_test_read_sector(1) == 0) { + mlc_printf("512e OK, using 512-byte reads\n"); + ATAdev.alignment_log2 = 0; + } else { + mlc_printf("No 512e, aligning to %lu sectors\n", phys_sector_mult); + /* Find log2 of phys_sector_mult */ + uint32 mult = phys_sector_mult; ATAdev.alignment_log2 = 0; + while(mult > 1) { + ATAdev.alignment_log2++; + mult >>= 1; + } } + } else { + /* 512-byte physical sectors */ + ATAdev.alignment_log2 = 0; } + mlc_printf("Alignment: %d (read %d sectors)\n", + ATAdev.alignment_log2, (1 << ATAdev.alignment_log2)); + + /* + * SET MULTIPLE MODE: let the drive transfer several sectors per DRQ, + * which speeds up big reads (kernel and Rockbox images). + * + * The block size comes from IDENTIFY word 47 bits 7:0 (bit 8 = valid). + * If that is not valid, fall back to word 59 bits 7:0, which holds the + * current setting (bit 8 = valid). + */ + ATAdev.multisectors = 0; + ATAdev.use_multiple = 0; + if(buff[47] & 0x100) { + ATAdev.multisectors = buff[47] & 0xFF; + } + if(ATAdev.multisectors == 0 && (buff[59] & 0x100)) { + ATAdev.multisectors = buff[59] & 0xFF; + } + + if(ATAdev.multisectors > 1) { + if(set_multiple_mode(ATAdev.multisectors) == 0) { + mlc_printf("SET MULTIPLE MODE %d OK\n", ATAdev.multisectors); + ATAdev.use_multiple = 1; + } else { + mlc_printf("SET MULTIPLE MODE failed, using single-sector\n"); + ATAdev.multisectors = 1; + } + } + + /* Negotiate PIO mode, enable write cache and read look-ahead. + * This also updates the controller timing register to match. + * See: Rockbox firmware/drivers/ata.c set_features() + */ + set_features(buff); + + /* Issue SECURITY FREEZE LOCK to prevent security state lockout. + * See: Rockbox firmware/drivers/ata.c freeze_lock() + */ + freeze_lock(buff); + + /* Read and display S.M.A.R.T. health data (informational). + * See: Rockbox firmware/drivers/ata.c ata_smart() + */ +#ifdef HAVE_ATA_SMART + ata_read_smart(buff); +#endif + #if DEBUG mlc_show_critical_error(); #endif @@ -551,53 +1018,68 @@ static void ata_send_read_command(uint32 lba, uint16 count) { last_sector = lba; last_sector_count = count; - /* - * REG_DEVICEHEAD bits are: - * - * | 1 | 2 | 3 | 4 | 5678 | - * | 1 | LBA | 1 | DRV | HEAD | - * - * LBA = 0 for CHS addressing - * LBA = 1 for logical block addressing - * - * DRV = 0 for master - * DRV = 1 for slave - * - * Head = 0 for LBA 48 - * Head = lower nibble of top byte of sector, for LBA28 - */ - uint8 head = ATAdev.lba48 ? 0 : ((lba & 0x0F000000) >> 24); - pio_outbyte( REG_DEVICEHEAD , 0xA0 | LBA_ADDRESSING | DEVICE_0 | head ); - DELAY400NS; - pio_outbyte( REG_FEATURES , 0 ); - pio_outbyte( REG_CONTROL , CONTROL_NIEN | 0x08); /* 8 = HD15 */ + /* Select device and wait for it to be ready. + * See: Rockbox firmware/drivers/ata.c ata_transfer_sectors() + */ + pio_outbyte( REG_DEVICEHEAD, 0xA0 | DEVICE_0 ); + if(!wait_for_rdy()) return; if(ATAdev.lba48) { /* - * IMPORTANT: The ATA controller is sensitive to the order in which registers are written. - * For LBA48, we MUST write the high registers first, before we write the low registers. - * It doesn't work if the lower registers are written first. + * LBA48: write high byte first, then low byte, to the SAME register address. + * The device latches the first write as the high byte and the second as the low byte. + * See: Rockbox firmware/drivers/ata.c ata_transfer_sectors() */ - /* Write the high bytes of the registers */ - pio_outbyte( REG_SECCOUNT_HIGH , (count & 0x0000FF00) >> 8 ); - pio_outbyte( REG_LBA3 , (lba & 0xFF000000) >> 24 ); - pio_outbyte( REG_LBA4 , 0 ); - pio_outbyte( REG_LBA5 , 0 ); - } + /* Sector count: high then low, same address */ + pio_outbyte( REG_SECCOUNT_LOW , (count >> 8) & 0xff ); + pio_outbyte( REG_SECCOUNT_LOW , count & 0xff ); + + /* LBA Low: bits 31:24 (high) then bits 7:0 (low) */ + pio_outbyte( REG_LBA0 , (lba >> 24) & 0xff ); + pio_outbyte( REG_LBA0 , lba & 0xff ); + + /* LBA Mid: bits 39:32 (high, always 0 for uint32) then bits 15:8 (low) */ + pio_outbyte( REG_LBA1 , 0 ); + pio_outbyte( REG_LBA1 , (lba >> 8) & 0xff ); + + /* LBA High: bits 47:40 (high, always 0 for uint32) then bits 23:16 (low) */ + pio_outbyte( REG_LBA2 , 0 ); + pio_outbyte( REG_LBA2 , (lba >> 16) & 0xff ); - /* Write the low bytes of the registers */ - pio_outbyte( REG_SECCOUNT_LOW , (count & 0x000000FF) >> 0 ); - pio_outbyte( REG_LBA0 , (lba & 0x000000FF) >> 0 ); - pio_outbyte( REG_LBA1 , (lba & 0x0000FF00) >> 8 ); - pio_outbyte( REG_LBA2 , (lba & 0x00FF0000) >> 16 ); + /* Device/Head: LBA mode, drive 0 - written last, before command */ + pio_outbyte( REG_DEVICEHEAD, 0xA0 | LBA_ADDRESSING | DEVICE_0 ); + } + else { + /* LBA28: sector count, LBA low/mid/high, then head in Device/Head */ + pio_outbyte( REG_SECCOUNT_LOW , count & 0xff ); + pio_outbyte( REG_LBA0 , lba & 0xff ); + pio_outbyte( REG_LBA1 , (lba >> 8) & 0xff ); + pio_outbyte( REG_LBA2 , (lba >> 16) & 0xff ); + + /* Device/Head: LBA bits 27:24 in lower nibble, LBA mode, drive 0 - written last, before command */ + pio_outbyte( REG_DEVICEHEAD, 0xA0 | ((lba >> 24) & 0x0f) | LBA_ADDRESSING | DEVICE_0 ); + } - /* Send read command */ - if (ATAdev.lba48) { - ata_command( COMMAND_READ_SECTORS_EXT ); + /* Send read command - use READ MULTIPLE if configured. + * With READ MULTIPLE, the drive presents multisectors per DRQ, but + * the per-sector read loop still works because DRQ stays set during + * the entire multi-sector block - the host can read words at any pace. + * This matches Rockbox's approach for faster large transfers. + */ + if(ATAdev.lba48) { + if(ATAdev.use_multiple) { + ata_command( COMMAND_READ_MULTIPLE_EXT ); + } else { + ata_command( COMMAND_READ_SECTORS_EXT ); + } } else { - ata_command( COMMAND_READ_SECTORS ); + if(ATAdev.use_multiple) { + ata_command( COMMAND_READ_MULTIPLE ); + } else { + ata_command( COMMAND_READ_SECTORS ); + } } DELAY400NS; DELAY400NS; @@ -620,7 +1102,7 @@ static uint32 ata_transfer_block(void *ptr, uint32 count) { uint16 *dst = (uint16*)ptr; while(words--) { /* Wait until drive is not busy */ - spinwait_drive_busy(); + if(spinwait_drive_busy()) return words_received * 2; /* Check DRQ to see if there's more data to read, or if an error has occured */ if((pio_inbyte(REG_STATUS) & (STATUS_ERR | STATUS_DRQ)) != STATUS_DRQ) { @@ -635,8 +1117,8 @@ static uint32 ata_transfer_block(void *ptr, uint32 count) { else { while(words--) { /* Wait until drive is not busy */ - spinwait_drive_busy(); - + if(spinwait_drive_busy()) return words_received * 2; + /* Check DRQ to see if there's more data to read, or if an error has occured */ if((pio_inbyte(REG_STATUS) & (STATUS_ERR | STATUS_DRQ)) != STATUS_DRQ) { break; @@ -656,26 +1138,23 @@ static uint32 ata_transfer_block(void *ptr, uint32 count) { * * *dst: Destination buffer. If NULL, data will be read from the device and discarded. * count: The number of 512 byte blocks to read from the device into the buffer + * Returns: number of bytes actually read, or 0 on error. */ static uint32 ata_receive_read_data(void *dst, uint32 count) { uint32 bytesread; bytesread = ata_transfer_block(dst, count); /* Wait for any final busy state to clear */ - spinwait_drive_busy(); + if(spinwait_drive_busy()) return 0; /* Check if reading ended on an error */ - bug_on_ata_error(); + if(check_ata_error()) { + return 0; + } /* Verify we read the expected number of bytes */ if(bytesread != count * BLOCK_SIZE) { - /* We read an unexpected number of bytes from the device */ - mlc_printf("\nATA2 IO Error\n"); - mlc_printf("\nUnexpected number of bytes received.\n"); - - mlc_printf("Expected: %lu, Actual: %lu\n", count * BLOCK_SIZE, bytesread); - mlc_show_fatal_error(); - return bytesread; + return 0; } return bytesread; @@ -749,9 +1228,23 @@ static inline void *get_cache_entry_buffer(int cacheindex) { } /* - * Sets up the transfer of one block of data + * Sets up the transfer of one block of data. + * Includes retry logic: on error, performs a soft reset and retries. + * Returns 0 on success, -1 on unrecoverable error. */ static int ata_readblock2(void *dst, uint32 sector, int useCache) { + int retries = 3; + + /* If the drive is sleeping/standby, wake it up first. + * This performs a full soft reset + re-identify + re-set features. + */ + if(ATAdev.state == ATA_DRIVE_SLEEPING) { + if(ata_perform_wakeup() != 0) { + return -1; + } + clear_cache(); + } + /* * Check if we have this block in cache first */ @@ -772,7 +1265,7 @@ static int ata_readblock2(void *dst, uint32 sector, int useCache) { "Sector %lu is too large for LBA28 addressing.\n" , sector); mlc_show_fatal_error (); - return(0); + return(-1); } /* Calculate an aligned LBA for the specified sector */ @@ -780,6 +1273,7 @@ static int ata_readblock2(void *dst, uint32 sector, int useCache) { uint32 sector_mask = ~((1u << (ATAdev.alignment_log2)) - 1u); uint32 sector_to_read = sector & sector_mask; +retry: /* Send the read command to the device*/ ata_send_read_command(sector_to_read, read_size); @@ -795,6 +1289,23 @@ static int ata_readblock2(void *dst, uint32 sector, int useCache) { /* Read data directly into the cache*/ int bytesread = ata_receive_read_data(cachedst, 1); + if(bytesread == 0) { + /* Read failed - try soft reset and retry */ + if(--retries > 0) { + mlc_printf("Read err at sec %lu, retrying... (%d left)\n", sector, retries); + clear_cache(); + if(perform_soft_reset() == 0) { + /* A reset clears the multi-sector mode setting */ + if(ATAdev.use_multiple) { + set_multiple_mode(ATAdev.multisectors); + } + goto retry; + } + } + mlc_printf("Read FAILED at sector %lu\n", sector); + return(-1); + } + if(i == sector) { /* This is the sector that was actually requested, copy it out of the cache block into the destination */ mlc_memcpy(dst, cachedst, bytesread); @@ -808,13 +1319,29 @@ static int ata_readblock2(void *dst, uint32 sector, int useCache) { * they were the requested sector. */ for(uint32 i = sector_to_read; i < (sector_to_read + read_size); i++) { + uint32 bytesread; if(i == sector) { /* This is the sector that was actually requested, read data directly into the destination */ - ata_receive_read_data(dst, 1); + bytesread = ata_receive_read_data(dst, 1); } else { /* Discard data we can't use */ - ata_receive_read_data(NULL, 1); + bytesread = ata_receive_read_data(NULL, 1); + } + + if(bytesread == 0) { + if(--retries > 0) { + mlc_printf("Read err at sec %lu, retrying... (%d left)\n", sector, retries); + if(perform_soft_reset() == 0) { + /* A reset clears the multi-sector mode setting */ + if(ATAdev.use_multiple) { + set_multiple_mode(ATAdev.multisectors); + } + goto retry; + } + } + mlc_printf("Read FAILED at sector %lu\n", sector); + return(-1); } } } diff --git a/ata2_definitions.h b/ata2_definitions.h index f6b8a25..e5bc185 100644 --- a/ata2_definitions.h +++ b/ata2_definitions.h @@ -114,10 +114,11 @@ #define REG_LBA0 0x3 // Same as LBA28 REG_SECT. #define REG_LBA1 0x4 // Same as LBA28 REG_CYL_LOW. #define REG_LBA2 0x5 // Same as LBA28 REG_CYL_HIGH. -#define REG_SECCOUNT_HIGH 0xA -#define REG_LBA3 0xB -#define REG_LBA4 0xC -#define REG_LBA5 0xD +/* LBA48 high registers: in standard ATA, high bytes are written to the SAME + * register address as the low bytes, in the order high-then-low. The device + * latches the first write as high and the second as low. + * On the PP5020, this is done by writing to the same byte address twice. + * We use REG_SECCOUNT_LOW, REG_LBA0, REG_LBA1, REG_LBA2 for both. */ #define REG_DA 0x9 @@ -167,6 +168,39 @@ */ #define COMMAND_READ_SECTORS_EXT 0x24 +/* READ MULTIPLE - PIO Data-In, LBA28. + * Transfers sectors in blocks of multisectors per DRQ. + * Requires SET MULTIPLE MODE to be issued first. + */ +#define COMMAND_READ_MULTIPLE 0xC4 + +/* READ MULTIPLE EXT - PIO Data-In, LBA48. + * Transfers sectors in blocks of multisectors per DRQ. + * Requires SET MULTIPLE MODE to be issued first. + */ +#define COMMAND_READ_MULTIPLE_EXT 0x29 + +/* SET MULTIPLE MODE - Sets the block size for READ MULTIPLE / WRITE MULTIPLE. + * The Sector Count register specifies the number of sectors per block. + * A value of 0 means 256 sectors per block. + */ +#define COMMAND_SET_MULTIPLE_MODE 0xC6 + +/* SET FEATURES - Used to enable/disable features such as write cache, + * read look-ahead, PIO mode, etc. + */ +#define COMMAND_SET_FEATURES 0xEF + +/* SECURITY FREEZE LOCK - Prevents the device from entering a security state. + * Should be sent during init if the device supports Security Mode (word 82 bit 1). + */ +#define COMMAND_SECURITY_FREEZE_LOCK 0xF5 + +/* SMART (Self-Monitoring, Analysis and Reporting Technology) */ +#define COMMAND_SMART 0xB0 +#define SMART_SMART_STATUS 0xDA +#define SMART_READ_DATA 0xD0 + /* STANDBY IMMEDIATE (StandbyIm) * * This command causes the device to immediately enter the Standby mode. @@ -235,6 +269,11 @@ */ #define ERROR_ABRT 0x04 -/* There are 8 total error bits that can be set, but besides ABRT, they are all command dependant.*/ +/* IDNF: ID Not Found. Indicates the requested sector's ID field could not + * be found. Retry of the command is not likely to succeed. + */ +#define ERROR_IDNF 0x10 + +/* There are 8 total error bits that can be set, but besides ABRT and IDNF, they are all command dependant.*/ #endif diff --git a/loader.c b/loader.c index 951d38a..eab4f6b 100644 --- a/loader.c +++ b/loader.c @@ -25,10 +25,26 @@ static void shutdown_loader (void) static void standby (void) { - shutdown_loader (); ipod_set_backlight (0); + /* Blank the screen and push it to the LCD before powering the panel + * down, so the LCD RAM holds a clean frame. Then put the LCD driver + * into standby via the power control register (0x03), the same way + * Rockbox does in lcd_shutdown(). Leaving the op-amp running is what + * causes the stray vertical line on wake. + */ fb_cls (framebuffer, ipod_get_hwinfo()->lcd_is_grayscale?BLACK:WHITE); fb_update(framebuffer); + if (ipod_get_hwinfo()->hw_ver < 6 || ipod_get_hwinfo()->hw_ver == 0x7) { + uint16 power_reg_h; + /* bias/step-up config kept in the high byte: + * 1G/2G/3G = 0x1120 (1/7 bias, 5x step-up @ clk/8) + * 4G/Mini = 0x1200 (1/7 bias, 6x step-up @ clk/32) + */ + power_reg_h = (ipod_get_hwinfo()->hw_ver < 4) ? 0x1120 : 0x1200; + lcd_cmd_and_data16(0x3, power_reg_h | 0x00); /* turn off op amp power */ + lcd_cmd_and_data16(0x3, power_reg_h | 0x02); /* put LCD driver in standby */ + } + shutdown_loader (); mlc_delay_ms (1000); pcf_standby_mode (); } @@ -71,9 +87,11 @@ static void test_contrast (config_t *conf) linecolor = fb_rgb(linemode << 6,linemode << 6,linemode << 6); { int w = ipod->lcd_width; - menu_hline (framebuffer, 0, w-1, 78, linecolor); - menu_drawrect (framebuffer, 111, 82, w-1, 95, linecolor); - menu_drawrect (framebuffer, 0, 96, 110, 109, linecolor); + int h = ipod->lcd_height; + int mid_y = h * 5 / 8; /* ~62.5% down the screen */ + menu_hline (framebuffer, 0, w-1, mid_y - 2, linecolor); + menu_drawrect (framebuffer, w/2+1, mid_y, w-1, mid_y + 12, linecolor); + menu_drawrect (framebuffer, 0, mid_y + 14, w/2-1, mid_y + 24, linecolor); } console_suppress_fbupdate (-1); // calls fb_update now } @@ -702,6 +720,12 @@ void *loader(void) { mlc_printf("Debug=%d\n", conf->debug); mlc_set_output_options (0, conf->debug & 4); if (conf->backlight) ipod_set_backlight (1); + } else { + // debug=0: discard all buffered output and suppress future output. + mlc_set_output_options (1, 0); + mlc_discard_buffer (); + fb_cls(framebuffer, BLACK); + fb_update(framebuffer); } { diff --git a/menu.c b/menu.c index eaaa7c4..69ec50c 100644 --- a/menu.c +++ b/menu.c @@ -104,23 +104,32 @@ void menu_frame (uint16 *fb, int x1, int y1, int x2, int y2, uint16 color) { static void menu_recenter() { int i; + int avail_h = menu.ipod->lcd_height - 30; /* leave room for title bar */ + /* Find the widest item string (8 pixels per character) */ menu.w = 0; for(i = 0; i < menu.numItems; i++) { - if(menu.w < (mlc_strlen (menu.string[i]) << 3)) - menu.w = mlc_strlen (menu.string[i]) << 3; + int len = mlc_strlen(menu.string[i]) << 3; /* * 8 pixels/char */ + if(menu.w < len) + menu.w = len; } - menu.w += 6; + menu.w += 16; /* 8px padding each side */ + + /* Choose font: try large (16px) first, fall back to medium (8px) */ menu.fh = 16; - menu.h = menu.numItems * 20; - if(menu.h > (menu.ipod->lcd_height - 50)) { + menu.h = menu.numItems * (menu.fh + 4); /* line_height = fh + 4 */ + if(menu.h > avail_h) { menu.fh = 8; - menu.h = menu.numItems * 12; + menu.h = menu.numItems * (menu.fh + 4); } - if(menu.w < (menu.ipod->lcd_width*3/5)) - menu.w = menu.ipod->lcd_width*3/5; - if(menu.h < (menu.ipod->lcd_height*2/5)) - menu.h = menu.ipod->lcd_height*2/5; + + /* Clamp box to screen bounds */ + if(menu.w > menu.ipod->lcd_width - 4) + menu.w = menu.ipod->lcd_width - 4; + if(menu.h > avail_h) + menu.h = avail_h; + + /* Center the box on screen, below the title bar */ menu.x = (menu.ipod->lcd_width - menu.w) >> 1; menu.y = ((menu.ipod->lcd_height - menu.h - (menu.fh + 6)) >> 1) + menu.fh + 6; } diff --git a/menu.h b/menu.h index eef6287..9fcb0fb 100644 --- a/menu.h +++ b/menu.h @@ -3,7 +3,7 @@ #include "bootloader.h" -#define MAX_MENU_ITEMS 16 // 5G model can show much more than 10 items +#define MAX_MENU_ITEMS 8 void menu_cls(uint16 *fb); void menu_init(); diff --git a/minilibc.c b/minilibc.c index 0c6de11..38f94c7 100644 --- a/minilibc.c +++ b/minilibc.c @@ -350,7 +350,7 @@ static int print_to_console(unsigned c, void **ptr) { return 0; } -static int do_buffered_printf = 0; // boolean flag +static int do_buffered_printf = 0; // direct output by default static int do_slow_printf = 0; // boolean flag static const int printf_buffer_size = 512; // 512 should be enough for what we use it for static uint8 *printf_buffer = 0; @@ -661,6 +661,11 @@ void mlc_clear_screen () { #endif } +void mlc_discard_buffer () { + // silently discard any buffered printf output without displaying it + printf_buflen = 0; +} + void mlc_set_output_options (int buffered, int slow) { // buffered: // pass <1> to have printf calls not output to console but store the text in a buffer @@ -688,9 +693,6 @@ void mlc_set_output_options (int buffered, int slow) { // call this if you can still continue but want to make the user see what you just printed: void mlc_show_critical_error () { - mlc_set_output_options (0, 0); - ipod_set_backlight (1); - mlc_delay_ms (5000); // just pause for 5s keypad_flush (); } diff --git a/minilibc.h b/minilibc.h index 3eaea29..ae853f6 100644 --- a/minilibc.h +++ b/minilibc.h @@ -37,6 +37,7 @@ int mlc_sprintf(char *buf, const char *fmt, ...); int mlc_vprintf(const char *fmt, mlc_va_list args); int mlc_printf(const char *fmt, ...); +void mlc_discard_buffer(void); void mlc_malloc_init(void); void *mlc_malloc(size_t num); size_t mlc_strlen(const char *); From b43e5d015fc837b3877f85de141f024f6e7df03b Mon Sep 17 00:00:00 2001 From: Le Khanh Binh Date: Tue, 25 Aug 2026 10:40:09 +0700 Subject: [PATCH 3/3] Limit to only 10 entries --- console.c | 2 +- menu.c | 37 ++++++++++++++++++++++++------------- menu.h | 2 +- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/console.c b/console.c index 89cea3c..26944a2 100644 --- a/console.c +++ b/console.c @@ -7,7 +7,7 @@ #include "fontlarge.h" #include "fontmedium.h" -// not used currently: #include "fontsmall.h" +#include "fontsmall.h" int font_lines; diff --git a/menu.c b/menu.c index 69ec50c..b3b43a0 100644 --- a/menu.c +++ b/menu.c @@ -105,23 +105,32 @@ void menu_frame (uint16 *fb, int x1, int y1, int x2, int y2, uint16 color) { static void menu_recenter() { int i; int avail_h = menu.ipod->lcd_height - 30; /* leave room for title bar */ + int char_w; - /* Find the widest item string (8 pixels per character) */ + /* Choose font: try large (16px), then medium (8px), then small (6px). + * Pick the largest font whose total height still fits the screen. */ + menu.fh = 16; + menu.h = menu.numItems * (menu.fh + 2); /* line_height = fh + 2 */ + if(menu.h > avail_h) { + menu.fh = 8; + menu.h = menu.numItems * (menu.fh + 2); + } + if(menu.h > avail_h) { + menu.fh = 6; + menu.h = menu.numItems * (menu.fh + 2); + } + + /* large and medium fonts are 8px wide, small is 4px wide */ + char_w = (menu.fh == 6) ? 4 : 8; + + /* Find the widest item string */ menu.w = 0; for(i = 0; i < menu.numItems; i++) { - int len = mlc_strlen(menu.string[i]) << 3; /* * 8 pixels/char */ + int len = mlc_strlen(menu.string[i]) * char_w; if(menu.w < len) menu.w = len; } - menu.w += 16; /* 8px padding each side */ - - /* Choose font: try large (16px) first, fall back to medium (8px) */ - menu.fh = 16; - menu.h = menu.numItems * (menu.fh + 4); /* line_height = fh + 4 */ - if(menu.h > avail_h) { - menu.fh = 8; - menu.h = menu.numItems * (menu.fh + 4); - } + menu.w += 8; /* 4px padding each side */ /* Clamp box to screen bounds */ if(menu.w > menu.ipod->lcd_width - 4) @@ -173,11 +182,13 @@ void menu_drawprogress(uint16 *fb,uint8 completed) { void menu_redraw(uint16 *fb, int selectedItem, char *title, char *countDown, int drawLock) { int i; - int line_height = menu.fh + 4; + int line_height = menu.fh + 2; uint16 fg, bg; uint8 tp; - const uint8 *menu_font = (menu.fh == 16)? font_large : font_medium; + const uint8 *menu_font = font_medium; + if (menu.fh == 16) menu_font = font_large; + else if (menu.fh == 6) menu_font = font_small; const uint8 *prev_font = console_currentfont (); console_setfont (menu_font); diff --git a/menu.h b/menu.h index 9fcb0fb..6617f98 100644 --- a/menu.h +++ b/menu.h @@ -3,7 +3,7 @@ #include "bootloader.h" -#define MAX_MENU_ITEMS 8 +#define MAX_MENU_ITEMS 10 void menu_cls(uint16 *fb); void menu_init();