diff --git a/Makefile b/Makefile index 4804b75..b1cdfdd 100755 --- a/Makefile +++ b/Makefile @@ -100,5 +100,37 @@ $(BUILD)/%.elf: | $(BUILD) $(BUILD)/%.bin: $(BUILD)/%.elf $(OBJCOPY) -Obinary $< $@ + +# eeprom_stub - standalone CDX replacement for s10 platform +# +# Build: make eeprom_stub +# Output: build/eeprom_stub_nocrc.bin (raw, for patched bootloader) +# build/eeprom_stub_full.bin (768KB + CRC, for stock bootloader) + +EEPROM_STUB_OFFSET ?= 0x08040000 + +EEPROM_STUB_OBJS := $(patsubst $(SRC)/%.c,$(BUILD)/%.o,$(wildcard $(SRC)/eeprom_stub*.c)) + +# The stub uses its own linker script +$(BUILD)/eeprom_stub.elf: $(EEPROM_STUB_OBJS) | $(BUILD) + $(LD) --nostdlib \ + -T $(SRC)/eeprom_stub.ld \ + --defsym=STUB_FLASH_ORIGIN=$(EEPROM_STUB_OFFSET) \ + -o $@ $(EEPROM_STUB_OBJS) + +$(BUILD)/eeprom_stub_nocrc.bin: $(BUILD)/eeprom_stub.elf + $(OBJCOPY) -Obinary $< $@ + +eeprom_stub: $(BUILD)/eeprom_stub_nocrc.bin $(BUILD)/eeprom_stub_full.bin + @echo "EEPROM stub built:" + @echo " $(BUILD)/eeprom_stub_nocrc.bin (raw, for patched bootloader)" + @echo " $(BUILD)/eeprom_stub_full.bin (768KB + CRC, for stock bootloader)" + @$(CROSS)size $(BUILD)/eeprom_stub.elf + +# Full CDX image: pad to region size, fix CRC16 for bootloader validation +$(BUILD)/eeprom_stub_full.bin: $(BUILD)/eeprom_stub_nocrc.bin + @python3 python/fix_crc.py $< -o $@ --pad 0xC0000 + + clean: $(RM) $(BUILD)/* diff --git a/patches/eeprom_stub.c b/patches/eeprom_stub.c new file mode 100644 index 0000000..ccec406 --- /dev/null +++ b/patches/eeprom_stub.c @@ -0,0 +1,965 @@ +/* + * EEPROM tool CDX stub firmware on s10 platform + * + * Bare-metal, single-threaded. Replaces CDX region. + * Binary protocol over USART3, DMA TX + * + * Hardware: + * SPI1: PB3/SCK PB4/MISO PB5/MOSI + * CS: PA15 + * WP: PC1 + * USART3: PB10/TX PB11/RX + * EEPROM: M95M02, 256KB, 256-byte pages + */ + +#include "eeprom_stub.h" + + +u16 crc16_ccitt(const u8 *data, usize len) +{ + u16 crc = 0xFFFF; + for (usize i = 0; i < len; i++) { + crc ^= (u16)data[i] << 8; + for (int b = 0; b < 8; b++) + crc = (crc & 0x8000) ? (crc << 1) ^ 0x1021 : crc << 1; + } + return crc; +} + +u16 crc16_ccitt_update(u16 crc, const u8 *data, usize len) +{ + while (len--) { + crc ^= (u16)(*data++) << 8; + for (int i = 0; i < 8; i++) + crc = (crc & 0x8000) ? (crc << 1) ^ 0x1021 : crc << 1; + } + return crc; +} + +void *memcpy(void *dst, const void *src, usize n) +{ + u8 *d = dst; const u8 *s = src; + while (n--) *d++ = *s++; + return dst; +} + +void *memset(void *dst, int c, usize n) +{ + u8 *d = dst; + while (n--) *d++ = (u8)c; + return dst; +} + +int memcmp(const void *a, const void *b, usize n) +{ + const u8 *p = a, *q = b; + while (n--) { + if (*p != *q) return (int)*p - (int)*q; + p++; q++; + } + return 0; +} + + +extern u32 _sidata, _sdata, _edata, _sbss, _ebss, _estack; + + +// SID tag at CDX+0x000 +// Bootloader reads bytes 0..15 from CDX base for G S #SID response. +__attribute__((section(".sid_tag"), used)) +const char g_sid_tag[16] = FW_TAG; + + +// Vector table at CDX+0x200 +// - CDX boot entry (bootloader reads [0]=SP, [1]=entry point) +// - NVIC dispatch (via VTOR pointing here) + +void Reset_Handler(void); +void Default_Handler(void); +void TIM2_IRQHandler(void); +void DMA1_Stream3_IRQHandler(void); + +__attribute__((section(".isr_vector"), used)) +const u32 g_vectors[45] = { + [0] = (u32)&_estack, + [1] = (u32)Reset_Handler, + [2] = (u32)Default_Handler, /* NMI */ + [3] = (u32)Default_Handler, /* HardFault */ + [4] = (u32)Default_Handler, /* MemManage */ + [5] = (u32)Default_Handler, /* BusFault */ + [6] = (u32)Default_Handler, /* UsageFault */ + [11] = (u32)Default_Handler, /* SVCall */ + [14] = (u32)Default_Handler, /* PendSV */ + [15] = (u32)Default_Handler, /* SysTick */ + [30] = (u32)DMA1_Stream3_IRQHandler,/* IRQ14 */ + [44] = (u32)TIM2_IRQHandler, /* IRQ28 */ +}; + +void Default_Handler(void) { for (;;); } + +__attribute__((naked)) +void Reset_Handler(void) +{ + __asm volatile ("ldr sp, =_estack"); + + u32 *src = &_sidata, *dst = &_sdata; + while (dst < &_edata) + *dst++ = *src++; + + dst = &_sbss; + while (dst < &_ebss) + *dst++ = 0; + + SCB_CPACR |= (0xFu << 20); + __asm volatile ("dsb; isb"); + + SCB_VTOR = (u32)g_vectors; + + extern int main(void); + main(); + for (;;); +} + + +static inline void iwdg_kick(void) { IWDG_KR = 0xAAAA; } + +static void iwdg_extend_timeout_max(void) +{ + IWDG_KR = 0x5555; + IWDG_PR = 0x06; + IWDG_RLR = 0x0FFF; + while (IWDG_SR) + ; + IWDG_KR = 0xAAAA; +} + +void TIM2_IRQHandler(void) +{ + if (TIM2_SR & TIM_UIF) { + TIM2_SR = ~TIM_UIF; + iwdg_kick(); + } +} + +static void watchdog_timer_init(void) +{ + RCC_APB1ENR |= RCC_APB1_TIM2EN; + TIM2_PSC = 42000 - 1; // 84 MHz / 42000 = 2 kHz + TIM2_ARR = 40 - 1; // 2 kHz / 40 = 50 Hz -> 20 ms + TIM2_EGR = TIM_UG; + TIM2_DIER |= TIM_UIE; + TIM2_CR1 |= TIM_CEN; + nvic_enable_irq(IRQn_TIM2); +} + + +static inline u32 millis(void) { return TIM5_CNT / 2; } + +static void tim5_timebase_init(void) +{ + RCC_APB1ENR |= RCC_APB1_TIM5EN; + TIM5_PSC = 42000 - 1; // 2 kHz from 84 MHz + TIM5_ARR = 0xFFFFFFFF; + TIM5_CNT = 0; + TIM5_EGR = TIM_UG; + TIM5_CR1 = TIM_CEN; +} + + +static void ensure_pll(void) +{ + if ((RCC_CFGR & RCC_CFGR_SWS_MASK) == RCC_CFGR_SWS_PLL) + return; + + RCC_CR |= RCC_CR_HSEON; + while (!(RCC_CR & RCC_CR_HSERDY)) + ; + + FLASH_ACR = FLASH_ACR_ICEN | FLASH_ACR_DCEN | + FLASH_ACR_PRFTEN | FLASH_ACR_LATENCY_5WS; + + /* 8 MHz HSE / 8 * 336 / 2 = 168 MHz */ + RCC_PLLCFGR = (8u << 0) | (336u << 6) | (0u << 16) | RCC_PLLCFGR_SRC_HSE; + + RCC_CR |= RCC_CR_PLLON; + while (!(RCC_CR & RCC_CR_PLLRDY)) + ; + + RCC_CFGR = RCC_CFGR_PPRE1_DIV4 | RCC_CFGR_PPRE2_DIV2; + RCC_CFGR |= RCC_CFGR_SW_PLL; + while ((RCC_CFGR & RCC_CFGR_SWS_MASK) != RCC_CFGR_SWS_PLL) + ; +} + + +// SPI1 / EEPROM + +static inline void cs_low(void) { GPIOA_BSRR = (1u << (15 + 16)); } +static inline void cs_high(void) { GPIOA_BSRR = (1u << 15); } + +static u8 spi1_xfer(u8 v) +{ + while (!(SPI1_SR & SPI_TXE)) + ; + SPI1_DR = v; + while (!(SPI1_SR & SPI_RXNE)) + ; + return SPI1_DR; +} + +static void spi1_init(void) +{ + RCC_AHB1ENR |= RCC_AHB1_GPIOAEN | RCC_AHB1_GPIOBEN | RCC_AHB1_GPIOCEN; + RCC_APB2ENR |= RCC_APB2_SPI1EN; + + GPIOB_MODER &= ~((3u<<6)|(3u<<8)|(3u<<10)); + GPIOB_MODER |= ((2u<<6)|(2u<<8)|(2u<<10)); + GPIOB_AFRL &= ~((0xFu<<12)|(0xFu<<16)|(0xFu<<20)); + GPIOB_AFRL |= ((5u<<12)|(5u<<16)|(5u<<20)); + GPIOB_OSPEEDR |= ((3u<<6)|(3u<<8)|(3u<<10)); + + GPIOA_MODER &= ~(3u << 30); + GPIOA_MODER |= (1u << 30); + cs_high(); + + GPIOC_MODER &= ~(3u << 2); + GPIOC_MODER |= (1u << 2); + GPIOC_BSRR = (1u << 1); + + SPI1_CR1 = SPI_MSTR | SPI_SSM | SPI_SSI | SPI_BR_DIV8; + SPI1_CR1 |= SPI_SPE; +} + +static void eeprom_wait_ready(void) +{ + u8 sr; + do { + cs_low(); + spi1_xfer(EE_RDSR); + sr = spi1_xfer(0xFF); + cs_high(); + } while (sr & 0x01); +} + +static void eeprom_wren(void) +{ + // M95M02 requires tSHSL >= 100ns between cs_high and next cs_low. + // At 168MHz, back-to-back cs_high/cs_low is ~20ns - too fast. + for (volatile int i = 0; i < 20; i++) ; + cs_low(); + spi1_xfer(EE_WREN); + cs_high(); + for (volatile int i = 0; i < 20; i++) ; +} + +static void eeprom_unprotect(void) +{ + eeprom_wren(); + cs_low(); + spi1_xfer(EE_WRSR); + spi1_xfer(0x00); + cs_high(); + eeprom_wait_ready(); +} + +#if 0 +static u8 eeprom_rdsr(void) +{ + cs_low(); + spi1_xfer(EE_RDSR); + u8 sr = spi1_xfer(0xFF); + cs_high(); + return sr; +} +#endif + +void eeprom_read(u32 addr, u8 *buf, u32 len) +{ + cs_low(); + spi1_xfer(EE_READ); + spi1_xfer((addr >> 16) & 0xFF); + spi1_xfer((addr >> 8) & 0xFF); + spi1_xfer(addr & 0xFF); + for (u32 i = 0; i < len; i++) + buf[i] = spi1_xfer(0xFF); + cs_high(); +} + +void eeprom_write_page(u32 addr, const u8 *buf, u32 len) +{ + u32 page_off = addr & (EEPROM_PAGE_SIZE - 1u); + if (len == 0 || len > EEPROM_PAGE_SIZE || page_off + len > EEPROM_PAGE_SIZE) + return; + + eeprom_wren(); + cs_low(); + spi1_xfer(EE_WRITE); + spi1_xfer((addr >> 16) & 0xFF); + spi1_xfer((addr >> 8) & 0xFF); + spi1_xfer(addr & 0xFF); + for (u32 i = 0; i < len; i++) + spi1_xfer(buf[i]); + cs_high(); + eeprom_wait_ready(); +} + +void eeprom_write_buffer(u32 addr, const u8 *buf, u32 len) +{ + while (len) { + u32 page_off = addr & (EEPROM_PAGE_SIZE - 1u); + u32 space = EEPROM_PAGE_SIZE - page_off; + u32 n = (len < space) ? len : space; + eeprom_write_page(addr, buf, n); + addr += n; + buf += n; + len -= n; + } +} + + +// USART3 + DMA TX (DMA1 Stream 3 Channel 4) + +static volatile u8 dma_tx_busy; + +void DMA1_Stream3_IRQHandler(void) +{ + if (DMA1_LISR & DMA1_S3_TCIF) { + DMA1_LIFCR = DMA1_S3_TCIF; + DMA1_S3CR &= ~DMA_EN; + dma_tx_busy = 0; + } +} + +static void usart3_init(u32 baud) +{ + RCC_AHB1ENR |= RCC_AHB1_GPIOBEN | RCC_AHB1_DMA1EN; + RCC_APB1ENR |= RCC_APB1_USART3EN; + + GPIOB_MODER &= ~((3u<<20) | (3u<<22)); + GPIOB_MODER |= ((2u<<20) | (2u<<22)); + GPIOB_AFRH &= ~((0xFu<<8) | (0xFu<<12)); + GPIOB_AFRH |= ((7u<<8) | (7u<<12)); + GPIOB_OSPEEDR |= ((3u<<20) | (3u<<22)); + GPIOB_PUPDR &= ~((3u<<20) | (3u<<22)); + GPIOB_PUPDR |= (1u<<22); + + USART3_CR1 = 0; + USART3_BRR = (PCLK1_HZ + baud / 2) / baud; + USART3_CR3 = USART_DMAT; + USART3_CR1 = USART_TE | USART_RE | USART_UE; + + DMA1_S3CR = 0; + while (DMA1_S3CR & DMA_EN) + ; + DMA1_LIFCR = DMA1_S3_FLAGS_ALL; + DMA1_S3PAR = 0x40004804; + DMA1_S3CR = DMA_CH4 | DMA_MINC | DMA_DIR_M2P | DMA_TCIE; + + nvic_set_priority(IRQn_DMA1_S3, 2); + nvic_enable_irq(IRQn_DMA1_S3); + dma_tx_busy = 0; +} + +static void usart3_set_baud(u32 baud) +{ + while (!(USART3_SR & USART_TC)) + ; + USART3_CR1 &= ~USART_UE; + USART3_BRR = (PCLK1_HZ + baud / 2) / baud; + USART3_CR1 &= ~USART_RE; + USART3_CR1 |= USART_UE; + (void)USART3_SR; + (void)USART3_DR; + USART3_CR1 |= USART_RE; +} + +static void uart_dma_send(const u8 *data, u32 len) +{ + if (!len) return; + while (dma_tx_busy) + ; + DMA1_LIFCR = DMA1_S3_FLAGS_ALL; + DMA1_S3M0AR = (u32)data; + DMA1_S3NDTR = len; + dma_tx_busy = 1; + DMA1_S3CR |= DMA_EN; +} + +static void uart_dma_wait(void) +{ + while (dma_tx_busy) + ; + while (!(USART3_SR & USART_TC)) + ; +} + +static int uart_getc_timeout(u8 *out, u32 timeout_ms) +{ + u32 start = millis(); + for (;;) { + u32 sr = USART3_SR; + if (sr & USART_RXNE) { *out = (u8)USART3_DR; return 1; } + if (sr & USART_ERR_MASK) (void)USART3_DR; + if ((u32)(millis() - start) >= timeout_ms) return 0; + } +} + +static void uart_drain_ms(u32 ms) +{ + u32 start = millis(); + while ((u32)(millis() - start) < ms) { + while (USART3_SR & (USART_RXNE | USART_ERR_MASK)) + (void)USART3_DR; + } +} + + +// Frame TX: [0x55] [TYPE] [LEN:2 BE] [payload] [CRC16:2 BE] + +static u8 tx_buf[1 + 3 + MAX_FRAME_PAYLOAD + 2]; + +static void send_frame(u8 type, const u8 *payload, u16 len) +{ + uart_dma_wait(); + u8 *p = tx_buf; + *p++ = PROTO_SYNC; + *p++ = type; + *p++ = (len >> 8) & 0xFF; + *p++ = len & 0xFF; + if (len && payload) + memcpy(p, payload, len); + p += len; + u16 crc = crc16_ccitt(&tx_buf[1], 3 + len); + *p++ = (crc >> 8) & 0xFF; + *p++ = crc & 0xFF; + uart_dma_send(tx_buf, (u32)(p - tx_buf)); +} + +void send_ack(void) { send_frame(RSP_ACK, NULL, 0); } +void send_nack(u8 err) { send_frame(RSP_NACK, &err, 1); } +void send_data_frame(const u8 *data, u16 len) { send_frame(RSP_DATA, data, len); } + + +static u8 rx_payload[MAX_FRAME_PAYLOAD]; + +static int handle_q_frame(void); /* defined below, after command handlers */ + +static int recv_frame(u8 *cmd, u32 timeout_ms) +{ + u8 b; + + for (;;) { + if (!uart_getc_timeout(&b, timeout_ms)) return -1; + if (b == PROTO_SYNC) break; + } + + u8 type; + if (!uart_getc_timeout(&type, 50)) return -3; + + // simulate part of Resmed native ASCII protocol + if (type == 'Q') + return handle_q_frame(); + + // Binary frame + u8 lenbuf[2]; + if (!uart_getc_timeout(&lenbuf[0], 50)) return -3; + if (!uart_getc_timeout(&lenbuf[1], 50)) return -3; + + *cmd = type; + u16 len = ((u16)lenbuf[0] << 8) | lenbuf[1]; + if (len > MAX_FRAME_PAYLOAD) return -3; + + for (u16 i = 0; i < len; i++) + if (!uart_getc_timeout(&rx_payload[i], 50)) return -3; + + u8 crc_raw[2]; + if (!uart_getc_timeout(&crc_raw[0], 50)) return -3; + if (!uart_getc_timeout(&crc_raw[1], 50)) return -3; + + u16 crc_rx = ((u16)crc_raw[0] << 8) | crc_raw[1]; + u8 hdr[3] = { type, lenbuf[0], lenbuf[1] }; + u16 crc_calc = crc16_ccitt_update(0xFFFF, hdr, 3); + crc_calc = crc16_ccitt_update(crc_calc, rx_payload, len); + + return (crc_rx == crc_calc) ? (int)len : -3; +} + + +u16 fix_header_crc(u16 *old_crc) +{ + u8 hdr[HDR_LEN]; + eeprom_read(0, hdr, HDR_LEN); + + if (old_crc) { + u8 lo, hi; + eeprom_read(CRC_ADDR_LO, &lo, 1); + eeprom_read(CRC_ADDR_HI, &hi, 1); + *old_crc = (u16)lo | ((u16)hi << 8); + } + + u16 crc = crc16_ccitt(hdr, HDR_LEN); + u8 crc_le[2] = { crc & 0xFF, (crc >> 8) & 0xFF }; + eeprom_write_buffer(CRC_ADDR_LO, crc_le, 2); + return crc; +} + + +static void cmd_ping(void) +{ + send_data_frame((const u8 *)FW_VERSION, sizeof(FW_VERSION) - 1); +} + +static u8 ping_buf[2][EEPROM_PAGE_SIZE]; + +static void cmd_read(const u8 *pl, u16 plen) +{ + if (plen != 6) { send_nack(ERR_BAD_LEN); return; } + + u32 addr = ((u32)pl[0] << 16) | ((u32)pl[1] << 8) | pl[2]; + u32 len = ((u32)pl[3] << 16) | ((u32)pl[4] << 8) | pl[5]; + + if (!len || addr + len > EEPROM_SIZE) { send_nack(ERR_BAD_RANGE); return; } + + u8 ack_pl[3] = { (len >> 16) & 0xFF, (len >> 8) & 0xFF, len & 0xFF }; + send_frame(RSP_ACK, ack_pl, 3); + uart_dma_wait(); + + u16 crc = 0xFFFF; + int cur = 0; + u32 remaining = len; + + u32 chunk = (remaining > EEPROM_PAGE_SIZE) ? EEPROM_PAGE_SIZE : remaining; + eeprom_read(addr, ping_buf[cur], chunk); + + while (remaining > 0) { + u32 n = chunk; + uart_dma_send(ping_buf[cur], n); + crc = crc16_ccitt_update(crc, ping_buf[cur], n); + remaining -= n; + addr += n; + + if (remaining > 0) { + int next = cur ^ 1; + chunk = (remaining > EEPROM_PAGE_SIZE) ? EEPROM_PAGE_SIZE : remaining; + eeprom_read(addr, ping_buf[next], chunk); + cur = next; + } + uart_dma_wait(); + } + + u8 crc_tail[2] = { (crc >> 8) & 0xFF, crc & 0xFF }; + uart_dma_send(crc_tail, 2); + uart_dma_wait(); +} + +static void cmd_write(const u8 *pl, u16 plen) +{ + if (plen < 4) { send_nack(ERR_BAD_LEN); return; } + + u32 addr = ((u32)pl[0] << 16) | ((u32)pl[1] << 8) | pl[2]; + u16 data_len = plen - 3; + const u8 *data = &pl[3]; + + if (data_len > EEPROM_PAGE_SIZE) { send_nack(ERR_BAD_LEN); return; } + if (addr + data_len > EEPROM_SIZE) { send_nack(ERR_BAD_RANGE); return; } + if ((addr & (EEPROM_PAGE_SIZE - 1)) + data_len > EEPROM_PAGE_SIZE) + { send_nack(ERR_BAD_RANGE); return; } + + eeprom_write_page(addr, data, data_len); + + u8 vfy[EEPROM_PAGE_SIZE]; + eeprom_read(addr, vfy, data_len); + if (memcmp(vfy, data, data_len)) { + // Report: [ERR_VERIFY] [first_mismatch_offset:2] [expected] [got] [data_len:2] + u16 off = 0; + for (u16 i = 0; i < data_len; i++) { + if (vfy[i] != data[i]) { off = i; break; } + } + u8 detail[7] = { + ERR_VERIFY, + (off >> 8) & 0xFF, off & 0xFF, + data[off], vfy[off], + (data_len >> 8) & 0xFF, data_len & 0xFF + }; + send_frame(RSP_NACK, detail, 7); + return; + } + + send_ack(); +} + +/* + * Bulk write: streaming page writes with minimal per-page overhead. + * + * Initial frame payload: [addr_hi, addr_mid, addr_lo, count_hi, count_lo] + * addr = start address (must be page-aligned) + * count = number of 256-byte pages + * + * After ACK, enters streaming mode: + * Host sends: [256B page data] [CRC16-hi] [CRC16-lo] (258 raw bytes) + * FW responds: 0x06 (ACK) or error packet: + * [err_code] [page_hi] [page_lo] [off_hi] [off_lo] [expected] [got] + * Address auto-increments by PAGE_SIZE after each page. + * Host must wait for ACK before sending next page. + * + * On error, streaming stops. Host must re-enter with a new command. + */ +#define BULK_ACK 0x06 +#define BULK_ERR_CRC 0x10 +#define BULK_ERR_WRITE 0x11 + +static u8 bulk_page[EEPROM_PAGE_SIZE]; +static u8 bulk_vfy[EEPROM_PAGE_SIZE]; + +static void cmd_write_bulk(const u8 *pl, u16 plen) +{ + if (plen != 5) { send_nack(ERR_BAD_LEN); return; } + + u32 addr = ((u32)pl[0] << 16) | ((u32)pl[1] << 8) | pl[2]; + u16 count = ((u16)pl[3] << 8) | pl[4]; + + if (addr & (EEPROM_PAGE_SIZE - 1)) { send_nack(ERR_BAD_RANGE); return; } + if ((u32)addr + (u32)count * EEPROM_PAGE_SIZE > EEPROM_SIZE) + { send_nack(ERR_BAD_RANGE); return; } + + send_ack(); // ready for streaming + uart_dma_wait(); + + static u8 bulk_resp[7]; + + for (u16 p = 0; p < count; p++) { + // raw page + CRC + for (u16 i = 0; i < EEPROM_PAGE_SIZE; i++) + if (!uart_getc_timeout(&bulk_page[i], 2000)) return; + + u8 crc_raw[2]; + if (!uart_getc_timeout(&crc_raw[0], 500)) return; + if (!uart_getc_timeout(&crc_raw[1], 500)) return; + + u16 crc_rx = ((u16)crc_raw[0] << 8) | crc_raw[1]; + u16 crc_calc = crc16_ccitt(bulk_page, EEPROM_PAGE_SIZE); + if (crc_rx != crc_calc) { + bulk_resp[0] = BULK_ERR_CRC; + bulk_resp[1] = (p >> 8); bulk_resp[2] = p & 0xFF; + bulk_resp[3] = (crc_rx >> 8); bulk_resp[4] = crc_rx & 0xFF; + bulk_resp[5] = (crc_calc >> 8); bulk_resp[6] = crc_calc & 0xFF; + uart_dma_send(bulk_resp, 7); + uart_dma_wait(); + return; + } + + eeprom_write_page(addr, bulk_page, EEPROM_PAGE_SIZE); + eeprom_read(addr, bulk_vfy, EEPROM_PAGE_SIZE); + if (memcmp(bulk_vfy, bulk_page, EEPROM_PAGE_SIZE)) { + u16 off = 0; + for (u16 i = 0; i < EEPROM_PAGE_SIZE; i++) { + if (bulk_vfy[i] != bulk_page[i]) { off = i; break; } + } + bulk_resp[0] = BULK_ERR_WRITE; + bulk_resp[1] = (p >> 8); bulk_resp[2] = p & 0xFF; + bulk_resp[3] = (off >> 8); bulk_resp[4] = off & 0xFF; + bulk_resp[5] = bulk_page[off]; + bulk_resp[6] = bulk_vfy[off]; + uart_dma_send(bulk_resp, 7); + uart_dma_wait(); + return; + } + + bulk_resp[0] = BULK_ACK; + uart_dma_send(bulk_resp, 1); + uart_dma_wait(); + + addr += EEPROM_PAGE_SIZE; + } +} + +static void cmd_erase(void) +{ + u8 page[EEPROM_PAGE_SIZE]; + memset(page, 0xFF, EEPROM_PAGE_SIZE); + + for (u32 a = 0; a < EEPROM_SIZE; a += EEPROM_PAGE_SIZE) + eeprom_write_page(a, page, EEPROM_PAGE_SIZE); + + send_ack(); +} + +static void cmd_fix_crc_handler(void) +{ + u16 old; + u16 new = fix_header_crc(&old); + u8 resp[4] = { (old >> 8) & 0xFF, old & 0xFF, (new >> 8) & 0xFF, new & 0xFF }; + send_data_frame(resp, 4); +} + +static const u32 valid_bauds[] = { + 9600, 19200, 38400, 57600, 115200, + 230400, 460800, 921600, 1000000, 2000000 +}; + +static void cmd_set_baud(const u8 *pl, u16 plen) +{ + if (plen != 4) { send_nack(ERR_BAD_LEN); return; } + + u32 baud = ((u32)pl[0] << 24) | ((u32)pl[1] << 16) | ((u32)pl[2] << 8) | pl[3]; + + int ok = 0; + for (unsigned i = 0; i < sizeof(valid_bauds)/sizeof(valid_bauds[0]); i++) + if (valid_bauds[i] == baud) { ok = 1; break; } + if (!ok) { send_nack(ERR_BAD_BAUD); return; } + + send_ack(); + uart_dma_wait(); + + u32 t = millis(); + while ((u32)(millis() - t) < 10) + ; + + usart3_set_baud(baud); + uart_drain_ms(50); +} + +static void cmd_reset(void) +{ + send_ack(); + uart_dma_wait(); + + u32 t = millis(); + while ((u32)(millis() - t) < 20) + ; + + SCB_AIRCR = AIRCR_VECTKEY | AIRCR_SYSRESETREQ; + for (;;); +} + + +/* + * Resmed ASCII protocol + * + * Supported commands: + * P S #RES 0001 -> reset into bootloader + * P S #RES 0003 -> fast reset back to CDX + * P S #BLL 0001 -> same as RES 0001, protocol compatibility... + * G S #BID -> bootloader version + * G S #CID -> CDX version (our SID tag) + * G S #SID -> same as CID, also for compatibility + */ + +static int hex_nibble(u8 c) +{ + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + return -1; +} + +static u8 nibble_hex(u8 n) { return n < 10 ? '0' + n : 'A' + n - 10; } + +static void send_r_frame(const u8 *payload, u16 plen); + +#define BID_FLASH_ADDR ((const char *)0x08003F80) +#define BID_MAX_LEN 32 + +static u16 slen(const char *s) +{ + u16 n = 0; + while (s[n]) n++; + return n; +} + +static void send_gs_response(const char *prefix, u16 pfx_len, + const char *value, u16 val_len) +{ + u8 resp[128]; + u16 n = 0; + if ((u16)(pfx_len + 3 + val_len) > sizeof(resp)) + return; + memcpy(resp, prefix, pfx_len); + n = pfx_len; + resp[n++] = ' '; + resp[n++] = '='; + resp[n++] = ' '; + memcpy(resp + n, value, val_len); + n += val_len; + send_r_frame(resp, n); +} + +static void send_r_frame(const u8 *payload, u16 plen) +{ + uart_dma_wait(); + u8 *p = tx_buf; + *p++ = 0x55; + *p++ = 'R'; + + // Escape payload into tx_buf, compute body length + u8 *body = p + 3; // fill len later + u16 blen = 0; + for (u16 i = 0; i < plen; i++) { + body[blen++] = payload[i]; + if (payload[i] == 0x55) body[blen++] = 0x55; + } + + u16 total = 9 + blen; + p[0] = nibble_hex((total >> 8) & 0xF); + p[1] = nibble_hex((total >> 4) & 0xF); + p[2] = nibble_hex(total & 0xF); + p = body + blen; + + u16 crc = crc16_ccitt(tx_buf, 5 + blen); + *p++ = nibble_hex((crc >> 12) & 0xF); + *p++ = nibble_hex((crc >> 8) & 0xF); + *p++ = nibble_hex((crc >> 4) & 0xF); + *p++ = nibble_hex(crc & 0xF); + + uart_dma_send(tx_buf, (u32)(p - tx_buf)); +} + +static void bkp6r_write(u32 val) +{ + RCC_APB1ENR |= RCC_APB1_PWREN; + PWR_CR |= PWR_CR_DBP; + RTC_BKP6R = val; +} + +static void reset_after_ack(void) +{ + uart_dma_wait(); + u32 t = millis(); + while ((u32)(millis() - t) < 20) + ; + SCB_AIRCR = AIRCR_VECTKEY | AIRCR_SYSRESETREQ; + for (;;); +} + +static int handle_q_frame(void) +{ + u8 lh[3]; + for (int i = 0; i < 3; i++) + if (!uart_getc_timeout(&lh[i], 500)) return -2; + + int d0 = hex_nibble(lh[0]), d1 = hex_nibble(lh[1]), d2 = hex_nibble(lh[2]); + if (d0 < 0 || d1 < 0 || d2 < 0) return -2; + u16 total = (d0 << 8) | (d1 << 4) | d2; + + if (total < 9 || total > 500) return -2; + u16 body_len = total - 9; + + u8 raw[500]; + for (u16 i = 0; i < body_len + 4; i++) + if (!uart_getc_timeout(&raw[i], 500)) return -2; + + u8 hdr[5] = { 0x55, 'Q', lh[0], lh[1], lh[2] }; + u16 crc = crc16_ccitt_update(0xFFFF, hdr, 5); + crc = crc16_ccitt_update(crc, raw, body_len); + + int c0 = hex_nibble(raw[body_len]); + int c1 = hex_nibble(raw[body_len + 1]); + int c2 = hex_nibble(raw[body_len + 2]); + int c3 = hex_nibble(raw[body_len + 3]); + if (c0 < 0 || c1 < 0 || c2 < 0 || c3 < 0) return -2; + u16 crc_rx = (c0 << 12) | (c1 << 8) | (c2 << 4) | c3; + if (crc != crc_rx) return -2; + + // unescape payload + u8 pl[256]; + u16 plen = 0; + for (u16 i = 0; i < body_len; i++) { + pl[plen++] = raw[i]; + if (raw[i] == 0x55 && i + 1 < body_len && raw[i + 1] == 0x55) + i++; + } + + if (plen >= 13 && memcmp(pl, "P S #RES 0001", 13) == 0) { + send_gs_response("P S #RES", 8, "0001", 4); + reset_after_ack(); + } + if (plen >= 13 && memcmp(pl, "P S #RES 0003", 13) == 0) { + send_gs_response("P S #RES", 8, "0003", 4); + bkp6r_write(BKP6R_FAST_BOOT); + reset_after_ack(); + } + if (plen >= 13 && memcmp(pl, "P S #BLL 0001", 13) == 0) { + send_gs_response("P S #BLL", 8, "0001", 4); + reset_after_ack(); + } + + // G S # queries + if (plen >= 5 && memcmp(pl, "G S #", 5) == 0) { + if (plen >= 8 && memcmp(pl + 5, "BID", 3) == 0) { + const char *bid = BID_FLASH_ADDR; + u16 blen = 0; + while (blen < BID_MAX_LEN && bid[blen] && bid[blen] != (char)0xFF) + blen++; + send_gs_response("G S #BID", 8, bid, blen); + } + else if (plen >= 8 && memcmp(pl + 5, "CID", 3) == 0) + send_gs_response("G S #CID", 8, g_sid_tag, slen(g_sid_tag)); + else if (plen >= 8 && memcmp(pl + 5, "SID", 3) == 0) + send_gs_response("G S #SID", 8, g_sid_tag, slen(g_sid_tag)); + else if (plen >= 8 && memcmp(pl + 5, "RES", 3) == 0) + send_gs_response("G S #RES", 8, "0000", 4); + else if (plen >= 8 && memcmp(pl + 5, "BLL", 3) == 0) + send_gs_response("G S #BLL", 8, "0000", 4); + else { + send_gs_response((const char *)pl, plen, "6006", 4); + } + } + // P S # for unknown variables + else if (plen >= 5 && memcmp(pl, "P S #", 5) == 0) { + u16 pfx = (plen >= 8) ? 8 : plen; + send_gs_response((const char *)pl, pfx, "6006", 4); + } + + return -2; +} + + +__attribute__((weak)) +void dispatch_private(u8 cmd, const u8 *pl, u16 len) +{ + send_nack(ERR_BAD_CMD); +} + +static void dispatch(u8 cmd, const u8 *pl, u16 len) +{ + switch (cmd) { + case CMD_PING: cmd_ping(); break; + case CMD_READ: cmd_read(pl, len); break; + case CMD_WRITE: cmd_write(pl, len); break; + case CMD_WRITE_BULK: cmd_write_bulk(pl, len); break; + case CMD_ERASE: cmd_erase(); break; + case CMD_FIX_CRC: cmd_fix_crc_handler(); break; + case CMD_SET_BAUD: cmd_set_baud(pl, len); break; + case CMD_RESET: cmd_reset(); break; + default: + dispatch_private(cmd, pl, len); + break; + } +} + + +int main(void) +{ + ensure_pll(); + __asm volatile ("cpsie i"); + + iwdg_extend_timeout_max(); + watchdog_timer_init(); + tim5_timebase_init(); + spi1_init(); + eeprom_unprotect(); + usart3_init(DEFAULT_BAUD); + uart_drain_ms(100); + + for (;;) { + u8 cmd = 0; + int len = recv_frame(&cmd, 5000); + if (len >= 0) { + dispatch(cmd, rx_payload, (u16)len); + } else if (len != -2) { + // -1 = timeout, -3 = garbage ; reset to default baud + usart3_set_baud(DEFAULT_BAUD); + uart_drain_ms(50); + } + } +} diff --git a/patches/eeprom_stub.h b/patches/eeprom_stub.h new file mode 100644 index 0000000..28f3db1 --- /dev/null +++ b/patches/eeprom_stub.h @@ -0,0 +1,262 @@ +#pragma once + +typedef __UINT8_TYPE__ u8; +typedef __UINT16_TYPE__ u16; +typedef __UINT32_TYPE__ u32; +typedef __SIZE_TYPE__ usize; + + +typedef u8 uint8_t; +typedef u16 uint16_t; +typedef u32 uint32_t; +typedef usize size_t; + +#define NULL ((void *)0) + +#define REG(addr) (*(volatile u32 *)(addr)) +#define REG8(addr) (*(volatile u8 *)(addr)) + + +#define DEFAULT_BAUD 57600u +#define FW_VERSION "eeprom_stub 2.1" +#define FW_TAG "EEP_T-0201" + +// RCC +#define RCC_CR REG(0x40023800) +#define RCC_PLLCFGR REG(0x40023804) +#define RCC_CFGR REG(0x40023808) +#define RCC_AHB1ENR REG(0x40023830) +#define RCC_APB1ENR REG(0x40023840) +#define RCC_APB2ENR REG(0x40023844) + +#define FLASH_ACR REG(0x40023C00) + +// GPIOA, CS = PA15 +#define GPIOA_MODER REG(0x40020000) +#define GPIOA_BSRR REG(0x40020018) + +// GPIOB, SPI1 PB3/4/5, USART3 PB10/11 +#define GPIOB_MODER REG(0x40020400) +#define GPIOB_OSPEEDR REG(0x40020408) +#define GPIOB_PUPDR REG(0x4002040C) +#define GPIOB_AFRL REG(0x40020420) +#define GPIOB_AFRH REG(0x40020424) + +// GPIOC WP = PC1 +#define GPIOC_MODER REG(0x40020800) +#define GPIOC_BSRR REG(0x40020818) + +#define SPI1_CR1 REG(0x40013000) +#define SPI1_SR REG(0x40013008) +#define SPI1_DR REG8(0x4001300C) + +#define USART3_SR REG(0x40004800) +#define USART3_DR REG(0x40004804) +#define USART3_BRR REG(0x40004808) +#define USART3_CR1 REG(0x4000480C) +#define USART3_CR3 REG(0x40004814) + +// DMA1 Stream 3 (USART3_TX, Channel 4) +#define DMA1_LISR REG(0x40026000) +#define DMA1_LIFCR REG(0x40026008) +#define DMA1_S3CR REG(0x40026058) +#define DMA1_S3NDTR REG(0x4002605C) +#define DMA1_S3PAR REG(0x40026060) +#define DMA1_S3M0AR REG(0x40026064) + +// TIM2 (IWDG kicker) +#define TIM2_CR1 REG(0x40000000) +#define TIM2_DIER REG(0x4000000C) +#define TIM2_SR REG(0x40000010) +#define TIM2_EGR REG(0x40000014) +#define TIM2_PSC REG(0x40000028) +#define TIM2_ARR REG(0x4000002C) + +// TIM5 (timebase) +#define TIM5_CR1 REG(0x40000C00) +#define TIM5_EGR REG(0x40000C14) +#define TIM5_CNT REG(0x40000C24) +#define TIM5_PSC REG(0x40000C28) +#define TIM5_ARR REG(0x40000C2C) + +#define IWDG_KR REG(0x40003000) +#define IWDG_PR REG(0x40003004) +#define IWDG_RLR REG(0x40003008) +#define IWDG_SR REG(0x4000300C) + +#define SCB_VTOR REG(0xE000ED08) +#define SCB_AIRCR REG(0xE000ED0C) +#define SCB_CPACR REG(0xE000ED88) + +// PWR (for backup domain write access) +#define PWR_CR REG(0x40007000) +#define PWR_CR_DBP (1u << 8) +#define RCC_APB1_PWREN (1u << 28) + +// RTC backup registers +#define RTC_BKP6R REG(0x40002868) +#define BKP6R_FAST_BOOT 0x1B183CA7u + +#define NVIC_ISER(n) REG(0xE000E100 + (n) * 4) +#define NVIC_IPR(n) REG8(0xE000E400 + (n)) + + +// RCC_CR +#define RCC_CR_HSEON (1u << 16) +#define RCC_CR_HSERDY (1u << 17) +#define RCC_CR_PLLON (1u << 24) +#define RCC_CR_PLLRDY (1u << 25) + +// RCC_PLLCFGR +#define RCC_PLLCFGR_SRC_HSE (1u << 22) + +// RCC_CFGR +#define RCC_CFGR_SW_PLL (2u << 0) +#define RCC_CFGR_SWS_MASK (3u << 2) +#define RCC_CFGR_SWS_PLL (2u << 2) +#define RCC_CFGR_PPRE1_DIV4 (5u << 10) +#define RCC_CFGR_PPRE2_DIV2 (4u << 13) + +// RCC clock enables +#define RCC_AHB1_GPIOAEN (1u << 0) +#define RCC_AHB1_GPIOBEN (1u << 1) +#define RCC_AHB1_GPIOCEN (1u << 2) +#define RCC_AHB1_DMA1EN (1u << 21) +#define RCC_APB1_TIM2EN (1u << 0) +#define RCC_APB1_TIM5EN (1u << 3) +#define RCC_APB1_USART3EN (1u << 18) +#define RCC_APB2_SPI1EN (1u << 12) + +// FLASH_ACR +#define FLASH_ACR_LATENCY_5WS 5u +#define FLASH_ACR_PRFTEN (1u << 8) +#define FLASH_ACR_ICEN (1u << 9) +#define FLASH_ACR_DCEN (1u << 10) + +// SPI_CR1 +#define SPI_MSTR (1u << 2) +#define SPI_BR_DIV8 (1u << 4) +#define SPI_SPE (1u << 6) +#define SPI_SSI (1u << 8) +#define SPI_SSM (1u << 9) + +// SPI_SR +#define SPI_RXNE (1u << 0) +#define SPI_TXE (1u << 1) + +// USART_SR +#define USART_PE (1u << 0) +#define USART_FE (1u << 1) +#define USART_NE (1u << 2) +#define USART_ORE (1u << 3) +#define USART_RXNE (1u << 5) +#define USART_TC (1u << 6) +#define USART_TXE (1u << 7) +#define USART_ERR_MASK (USART_PE | USART_FE | USART_NE | USART_ORE) + +// USART_CR1 +#define USART_RE (1u << 2) +#define USART_TE (1u << 3) +#define USART_UE (1u << 13) + +// USART_CR3 +#define USART_DMAT (1u << 7) + +// DMA_SxCR +#define DMA_EN (1u << 0) +#define DMA_TCIE (1u << 4) +#define DMA_DIR_M2P (1u << 6) +#define DMA_MINC (1u << 10) +#define DMA_CH4 (4u << 25) + +// DMA1 Stream 3 flags +#define DMA1_S3_TCIF (1u << 27) +#define DMA1_S3_FLAGS_ALL (0x3Du << 22) + +// TIM bits +#define TIM_CEN (1u << 0) +#define TIM_UIE (1u << 0) +#define TIM_UG (1u << 0) +#define TIM_UIF (1u << 0) + +// SCB_AIRCR +#define AIRCR_VECTKEY (0x05FAu << 16) +#define AIRCR_SYSRESETREQ (1u << 2) + +// IRQ numbers +#define IRQn_DMA1_S3 14 +#define IRQn_TIM2 28 + + +static inline void nvic_enable_irq(int n) +{ + NVIC_ISER(n >> 5) = (1u << (n & 31)); +} + +static inline void nvic_set_priority(int n, u8 prio) +{ + NVIC_IPR(n) = (prio << 4); +} + + +#define PROTO_SYNC 0x55 + +#define CMD_PING 0x01 +#define CMD_READ 0x02 +#define CMD_WRITE 0x03 +#define CMD_ERASE 0x04 +#define CMD_FIX_CRC 0x05 +#define CMD_SET_BAUD 0x06 +#define CMD_RESET 0x07 +#define CMD_WRITE_BULK 0x08 + +#define RSP_ACK 0x41 +#define RSP_NACK 0x4E +#define RSP_DATA 0x44 + +#define ERR_BAD_CRC 0x01 +#define ERR_BAD_CMD 0x02 +#define ERR_BAD_RANGE 0x03 +#define ERR_BAD_LEN 0x04 +#define ERR_VERIFY 0x05 +#define ERR_BAD_BAUD 0x06 + +#define CMD_PRIVATE_BASE 0x10 +#define CMD_PRIVATE_MAX 0x1F + +#define EEPROM_SIZE (256u * 1024u) +#define EEPROM_PAGE_SIZE 256u +#define HDR_LEN 0x50 +#define CRC_ADDR_LO 0x50 +#define CRC_ADDR_HI 0x51 + +#define EE_WRITE 0x02 +#define EE_READ 0x03 +#define EE_WRSR 0x01 +#define EE_RDSR 0x05 +#define EE_WREN 0x06 + +#define MAX_FRAME_PAYLOAD 262u + +// SYSCLK=168M, APB1=42M, APB1 timers=84M +#define PCLK1_HZ 42000000u +#define TIM_CLK_HZ 84000000u + + +u16 crc16_ccitt(const u8 *data, usize len); +u16 crc16_ccitt_update(u16 crc, const u8 *data, usize len); + + +void eeprom_read(u32 addr, u8 *buf, u32 len); +void eeprom_write_page(u32 addr, const u8 *buf, u32 len); +void eeprom_write_buffer(u32 addr, const u8 *buf, u32 len); +u16 fix_header_crc(u16 *old_crc); + +void send_ack(void); +void send_nack(u8 err); +void send_data_frame(const u8 *data, u16 len); + + +void *memcpy(void *dst, const void *src, usize n); +void *memset(void *dst, int c, usize n); +int memcmp(const void *a, const void *b, usize n); diff --git a/patches/eeprom_stub.ld b/patches/eeprom_stub.ld new file mode 100644 index 0000000..e0054d9 --- /dev/null +++ b/patches/eeprom_stub.ld @@ -0,0 +1,76 @@ +/* + * Linker script for EEPROM tool CDX stub + * + * Layout: + * +0x000 .sid_tag Product ID string (bootloader reads via G S #SID) + * +0x200 .isr_vector NVIC vector table / cdx_entry + * [0]=SP, [1]=entry from here + * +0x2xx .text Code + rodata + */ + +MEMORY +{ + FLASH (rx) : ORIGIN = STUB_FLASH_ORIGIN, LENGTH = 768K + RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K +} + +_estack = ORIGIN(RAM) + LENGTH(RAM); + +ENTRY(Reset_Handler) + +SECTIONS +{ + .sid_tag : + { + KEEP(*(.sid_tag)) + . = 0x200; + } > FLASH + + .isr_vector : + { + . = ALIGN(4); + KEEP(*(.isr_vector)) + . = ALIGN(4); + } > FLASH + + .text : + { + . = ALIGN(4); + *(.text) + *(.text*) + *(.rodata) + *(.rodata*) + . = ALIGN(4); + } > FLASH + + _sidata = LOADADDR(.data); + + .data : + { + . = ALIGN(4); + _sdata = .; + *(.data) + *(.data*) + . = ALIGN(4); + _edata = .; + } > RAM AT> FLASH + + .bss : + { + . = ALIGN(4); + _sbss = .; + *(.bss) + *(.bss*) + *(COMMON) + . = ALIGN(4); + _ebss = .; + } > RAM + + /DISCARD/ : + { + *(.ARM.attributes) + *(.ARM.exidx*) + *(.comment) + *(.note*) + } +} diff --git a/python/eeprom_tool.py b/python/eeprom_tool.py new file mode 100755 index 0000000..f467773 --- /dev/null +++ b/python/eeprom_tool.py @@ -0,0 +1,855 @@ +#!/usr/bin/env python3 +""" +AirSense 10 / AirCurve 10 EEPROM Tool + +Binary protocol host for the EEPROM tool CDX stub firmware. +Supports baud rate negotiation, full read/write/erase, header CRC fix, +and optional FAT filesystem operations. + +""" + +import serial +import sys +import time +import os +import struct +import argparse +import io +import warnings +from dataclasses import dataclass +from typing import Optional + +def eprint(*args, **kwargs): + print(*args, file=sys.stderr, **kwargs) + + +warnings.filterwarnings( + "ignore", message="pkg_resources is deprecated as an API.*", + category=UserWarning, +) +warnings.filterwarnings( + "ignore", message="One or more FATs differ, filesystem most likely corrupted.*", + category=UserWarning, +) +warnings.filterwarnings( + "ignore", message="Filesystem was not cleanly unmounted.*", + category=UserWarning, +) + + +PROTO_SYNC = 0x55 + +CMD_PING = 0x01 +CMD_READ = 0x02 +CMD_WRITE = 0x03 +CMD_ERASE = 0x04 +CMD_FIX_CRC = 0x05 +CMD_SET_BAUD = 0x06 +CMD_RESET = 0x07 +CMD_WRITE_BULK = 0x08 + +RSP_ACK = 0x41 +RSP_NACK = 0x4E +RSP_DATA = 0x44 + +BULK_ACK = 0x06 +BULK_ERR_CRC = 0x10 +BULK_ERR_WRITE = 0x11 +BULK_ERR_NAMES = {BULK_ERR_CRC: "CRC", BULK_ERR_WRITE: "WRITE_VERIFY"} + +ERR_NAMES = { + 0x01: "BAD_CRC", 0x02: "BAD_CMD", 0x03: "BAD_RANGE", + 0x04: "BAD_LEN", 0x05: "VERIFY", 0x06: "BAD_BAUD", +} + +EEPROM_SIZE = 256 * 1024 +PAGE_SIZE = 256 +DEFAULT_BAUD = 57_600 +BAUD_STEPS = [57_600, 115_200, 460_800, 1_000_000, 2_000_000] +FRAME_TIMEOUT = 5.0 + +FAT_BASE_ADDR = 0x200 +FAT_SECTOR_SIZE = 512 + + + +def crc16_ccitt(data: bytes, crc: int = 0xFFFF) -> int: + for b in data: + crc ^= b << 8 + for _ in range(8): + crc = ((crc << 1) ^ 0x1021) & 0xFFFF if crc & 0x8000 else (crc << 1) & 0xFFFF + return crc + + + +class EepromProto: + """Binary protocol handler""" + + def __init__(self, port: str, baud: int = DEFAULT_BAUD, + timeout: float = FRAME_TIMEOUT): + self.ser = serial.Serial( + port=port, baudrate=baud, + timeout=timeout, write_timeout=timeout, + ) + self.baud = baud + time.sleep(0.1) + self.ser.reset_input_buffer() + self.ser.reset_output_buffer() + + def close(self): + self.ser.close() + + def __enter__(self): + return self + + def __exit__(self, *args): + self.close() + + def _send_frame(self, cmd: int, payload: bytes = b""): + length = len(payload) + header = bytes([cmd, (length >> 8) & 0xFF, length & 0xFF]) + body = header + payload + crc = crc16_ccitt(body) + frame = bytes([PROTO_SYNC]) + body + bytes([(crc >> 8) & 0xFF, crc & 0xFF]) + self.ser.write(frame) + self.ser.flush() + + def _recv_frame(self, timeout: Optional[float] = None) -> tuple[int, bytes]: + old_timeout = self.ser.timeout + if timeout is not None: + self.ser.timeout = timeout + try: + while True: + b = self.ser.read(1) + if not b: + raise RuntimeError("Timeout waiting for response sync") + if b[0] == PROTO_SYNC: + break + + hdr = self.ser.read(3) + if len(hdr) < 3: + raise RuntimeError("Timeout reading response header") + + rsp_type = hdr[0] + length = (hdr[1] << 8) | hdr[2] + + payload = self.ser.read(length) if length > 0 else b"" + if len(payload) < length: + raise RuntimeError(f"Short payload: {len(payload)}/{length}") + + crc_bytes = self.ser.read(2) + if len(crc_bytes) < 2: + raise RuntimeError("Timeout reading response CRC") + + crc_rx = (crc_bytes[0] << 8) | crc_bytes[1] + crc_calc = crc16_ccitt(hdr + payload) + if crc_rx != crc_calc: + raise RuntimeError( + f"Response CRC mismatch: 0x{crc_rx:04X} vs 0x{crc_calc:04X}") + + return rsp_type, payload + finally: + self.ser.timeout = old_timeout + + def _expect_ack(self, timeout: Optional[float] = None) -> bytes: + rsp_type, payload = self._recv_frame(timeout=timeout) + if rsp_type == RSP_NACK: + if len(payload) >= 7 and payload[0] == 0x05: # ERR_VERIFY detail + off = (payload[1] << 8) | payload[2] + exp, got = payload[3], payload[4] + dlen = (payload[5] << 8) | payload[6] + raise RuntimeError( + f"Device NACK: VERIFY mismatch at offset {off} " + f"(wrote 0x{exp:02X}, read 0x{got:02X}, len={dlen})") + err = payload[0] if payload else 0xFF + raise RuntimeError(f"Device NACK: {ERR_NAMES.get(err, f'0x{err:02X}')}") + if rsp_type != RSP_ACK: + raise RuntimeError(f"Unexpected response: 0x{rsp_type:02X}") + return payload + + + def ping(self) -> str: + self._send_frame(CMD_PING) + rsp_type, payload = self._recv_frame() + if rsp_type == RSP_DATA: + return payload.decode("ascii", errors="replace") + raise RuntimeError(f"Unexpected ping response: 0x{rsp_type:02X}") + + def read(self, addr: int, length: int) -> bytes: + payload = bytes([ + (addr >> 16) & 0xFF, (addr >> 8) & 0xFF, addr & 0xFF, + (length >> 16) & 0xFF, (length >> 8) & 0xFF, length & 0xFF, + ]) + self._send_frame(CMD_READ, payload) + + ack_payload = self._expect_ack() + if len(ack_payload) == 3: + confirmed = (ack_payload[0] << 16) | (ack_payload[1] << 8) | ack_payload[2] + if confirmed != length: + raise RuntimeError(f"Length mismatch: {length} vs {confirmed}") + + total = length + 2 + old_timeout = self.ser.timeout + self.ser.timeout = max(old_timeout, length / (self.baud / 10) * 2 + 2) + try: + data = bytearray() + remaining = total + while remaining > 0: + chunk = self.ser.read(min(remaining, 4096)) + if not chunk: + raise RuntimeError( + f"Timeout during read stream ({len(data)}/{total})") + data.extend(chunk) + remaining -= len(chunk) + finally: + self.ser.timeout = old_timeout + + eeprom_data = bytes(data[:length]) + crc_rx = (data[length] << 8) | data[length + 1] + if crc_rx != crc16_ccitt(eeprom_data): + raise RuntimeError("Read data CRC mismatch") + return eeprom_data + + def write_page(self, addr: int, data: bytes): + payload = bytes([ + (addr >> 16) & 0xFF, (addr >> 8) & 0xFF, addr & 0xFF, + ]) + data + self._send_frame(CMD_WRITE, payload) + self._expect_ack(timeout=2.0) + + def write_bulk(self, addr: int, image: bytes, progress=None): + """Streaming page write. Sends CMD_WRITE_BULK header, then raw + page+CRC packets with single-byte ACK per page.""" + if len(image) % PAGE_SIZE: + raise ValueError("Image size must be multiple of page size") + count = len(image) // PAGE_SIZE + if addr % PAGE_SIZE: + raise ValueError("Start address must be page-aligned") + + payload = bytes([ + (addr >> 16) & 0xFF, (addr >> 8) & 0xFF, addr & 0xFF, + (count >> 8) & 0xFF, count & 0xFF, + ]) + self._send_frame(CMD_WRITE_BULK, payload) + self._expect_ack(timeout=2.0) + + old_timeout = self.ser.timeout + self.ser.timeout = 2.0 # per-page: write + verify < 10ms typ + + for i in range(count): + page = image[i * PAGE_SIZE:(i + 1) * PAGE_SIZE] + crc = crc16_ccitt(page) + self.ser.write(page + bytes([(crc >> 8) & 0xFF, crc & 0xFF])) + self.ser.flush() + + resp = self.ser.read(1) + if not resp: + self.ser.timeout = old_timeout + raise RuntimeError(f"Timeout on page {i} (0x{addr + i * PAGE_SIZE:05X})") + if resp[0] != BULK_ACK: + # Error: read remaining 6 bytes of detail packet + detail = self.ser.read(6) + self.ser.timeout = old_timeout + code = resp[0] + if len(detail) == 6: + pg = (detail[0] << 8) | detail[1] + if code == BULK_ERR_CRC: + crc_rx = (detail[2] << 8) | detail[3] + crc_exp = (detail[4] << 8) | detail[5] + raise RuntimeError( + f"Bulk CRC error at page {pg} (0x{addr + pg * PAGE_SIZE:05X}): " + f"rx=0x{crc_rx:04X} calc=0x{crc_exp:04X}") + else: + off = (detail[2] << 8) | detail[3] + exp, got = detail[4], detail[5] + raise RuntimeError( + f"Bulk verify fail at page {pg} (0x{addr + pg * PAGE_SIZE:05X}), " + f"offset {off}: wrote 0x{exp:02X}, read 0x{got:02X}") + name = BULK_ERR_NAMES.get(code, f"0x{code:02X}") + raise RuntimeError( + f"Bulk write failed at page {i} (0x{addr + i * PAGE_SIZE:05X}): {name}") + + if progress: + progress(i + 1, count) + + self.ser.timeout = old_timeout + + def erase(self): + self._send_frame(CMD_ERASE) + self._expect_ack(timeout=30.0) + + def fix_crc(self) -> tuple[int, int]: + self._send_frame(CMD_FIX_CRC) + rsp_type, payload = self._recv_frame() + if rsp_type == RSP_DATA and len(payload) == 4: + old = (payload[0] << 8) | payload[1] + new = (payload[2] << 8) | payload[3] + return old, new + if rsp_type == RSP_NACK: + err = payload[0] if payload else 0xFF + raise RuntimeError(f"Fix CRC NACK: {ERR_NAMES.get(err, f'0x{err:02X}')}") + raise RuntimeError(f"Unexpected response: 0x{rsp_type:02X}") + + def diag(self) -> bytes: + self._send_frame(CMD_DIAG) + rsp_type, payload = self._recv_frame() + if rsp_type == RSP_DATA: + return payload + if rsp_type == RSP_NACK: + err = payload[0] if payload else 0xFF + raise RuntimeError(f"Diag NACK: {ERR_NAMES.get(err, f'0x{err:02X}')}") + raise RuntimeError(f"Unexpected response: 0x{rsp_type:02X}") + + def set_baud(self, baud: int): + self._send_frame(CMD_SET_BAUD, baud.to_bytes(4, "big")) + self._expect_ack() + time.sleep(0.05) + self.ser.baudrate = baud + self.baud = baud + time.sleep(0.05) + self.ser.reset_input_buffer() + + def reset(self): + self._send_frame(CMD_RESET) + try: + self._expect_ack(timeout=1.0) + except RuntimeError: + pass + + def send_raw_command(self, cmd: int, payload: bytes = b"") -> tuple[int, bytes]: + self._send_frame(cmd, payload) + return self._recv_frame() + + + +def connect(port: str, baud: int = None) -> EepromProto: + """Connect with auto baud detection and negotiation.""" + + # Try default first (common case), then sweep high-to-low + probe_order = [DEFAULT_BAUD] + [b for b in reversed(BAUD_STEPS) if b != DEFAULT_BAUD] + connected_baud = None + proto = None + + for try_baud in probe_order: + proto = EepromProto(port, try_baud, timeout=0.8) + try: + version = proto.ping() + connected_baud = try_baud + eprint(f"Connected: {version}" + + (f" (at {try_baud})" if try_baud != DEFAULT_BAUD else "")) + break + except Exception: + proto.close() + proto = None + time.sleep(0.1) + + if proto is None: + raise RuntimeError("No response at any baud rate") + + # negotiate to target baud. + target = baud if baud else BAUD_STEPS[-1] + + if target == connected_baud: + return proto + + if baud: + candidates = [baud] + else: + candidates = [b for b in reversed(BAUD_STEPS) if b > connected_baud] + + for try_baud in candidates: + try: + proto.set_baud(try_baud) + proto.ping() + eprint(f"Switched to {try_baud} baud") + return proto + except Exception: + try: + proto.set_baud(DEFAULT_BAUD) + proto.ping() + connected_baud = DEFAULT_BAUD + except Exception: + proto.close() + time.sleep(5.5) + proto = EepromProto(port, DEFAULT_BAUD, timeout=2.0) + try: + proto.ping() + connected_baud = DEFAULT_BAUD + except Exception: + pass + + return proto + + +def read_eeprom(port: str, outfile: str, addr: Optional[int] = None, + length: Optional[int] = None, baud: int = None): + if addr is None: + addr, length = 0, EEPROM_SIZE + elif length is None: + raise ValueError("If addr is specified, length must be too") + if addr < 0 or addr + length > EEPROM_SIZE: + raise ValueError("Range out of bounds") + + with connect(port, baud) as proto: + eprint(f"Reading {length} bytes from 0x{addr:05X}...") + start = time.time() + data = proto.read(addr, length) + elapsed = time.time() - start + + rate = len(data) / elapsed / 1024 if elapsed > 0 else 0 + if outfile == "-": + sys.stdout.buffer.write(data) + sys.stdout.buffer.flush() + else: + with open(outfile, "wb") as f: + f.write(data) + eprint(f"Read {len(data)} bytes in {elapsed:.2f}s ({rate:.1f} KiB/s)") + eprint(f"Saved to: {outfile}") + + +def write_eeprom(port: str, infile: str, baud: int = None): + with open(infile, "rb") as f: + image = f.read() + if len(image) != EEPROM_SIZE: + raise ValueError(f"File must be {EEPROM_SIZE} bytes, got {len(image)}") + + with connect(port, baud) as proto: + eprint(f"Writing {EEPROM_SIZE} bytes...") + start = time.time() + total = EEPROM_SIZE // PAGE_SIZE + + def progress(done, total): + if done % 32 == 0 or done == total: + eprint(f"\r {done * 100 // total}% ({done}/{total})", end="", flush=True) + + proto.write_bulk(0, image, progress=progress) + elapsed = time.time() - start + rate = EEPROM_SIZE / elapsed / 1024 if elapsed > 0 else 0 + eprint(f"\nDone: {elapsed:.2f}s ({rate:.1f} KiB/s)") + + +def erase_eeprom(port: str, baud: int = None): + with connect(port, baud) as proto: + eprint("Erasing...") + start = time.time() + proto.erase() + eprint(f"Done ({time.time() - start:.1f}s)") + + +def cmd_fixcrc(port: str, baud: int = None): + with connect(port, baud) as proto: + old, new = proto.fix_crc() + eprint(f"Header CRC: 0x{old:04X} -> 0x{new:04X}") + + +def patch_bytes(port: str, addr: int, data: bytes, baud: int = None): + if addr + len(data) > EEPROM_SIZE: + raise ValueError("Patch exceeds EEPROM") + with connect(port, baud) as proto: + off, remaining = 0, len(data) + while remaining > 0: + page_off = (addr + off) & (PAGE_SIZE - 1) + n = min(remaining, PAGE_SIZE - page_off) + proto.write_page(addr + off, data[off:off + n]) + off += n + remaining -= n + eprint(f"Patched {len(data)} bytes at 0x{addr:05X}") + + +def read_eeprom_range(port: str, addr: int, length: int, + baud: int = None) -> bytes: + with connect(port, baud) as proto: + return proto.read(addr, length) + + +# FAT12 (optional, requires pyfatfs) + +try: + from pyfatfs.PyFatFS import PyFatBytesIOFS + HAVE_PYFATFS = True +except Exception: + PyFatBytesIOFS = None + HAVE_PYFATFS = False + +@dataclass +class FatSource: + port: Optional[str] = None + file: Optional[str] = None + baud: int = None + + +def _fat_parse_bpb(boot): + bps = struct.unpack_from(" {outfile} ({len(data)} bytes)") + + +def fat_getdir(src, fat_path, outdir): + fat, _, _ = fat_read_region(src) + fp, fs = _fat_open_fs(fat) + if not fat_path.startswith("/"): + fat_path = "/" + fat_path + os.makedirs(outdir, exist_ok=True) + + count = [0] + def _walk(fdir, hdir): + for e in fs.listdir(fdir): + fp2 = fdir.rstrip("/") + "/" + e + hp = os.path.join(hdir, e) + if fs.getinfo(fp2).is_dir: + os.makedirs(hp, exist_ok=True) + _walk(fp2, hp) + else: + with fs.openbin(fp2, "r") as src_f, open(hp, "wb") as dst_f: + while True: + chunk = src_f.read(8192) + if not chunk: + break + dst_f.write(chunk) + count[0] += 1 + try: + _walk(fat_path, outdir) + finally: + fs.close() + eprint(f"fat-getdir: {fat_path} -> {outdir}/ ({count[0]} files)") + + +def fat_repair_checksum(data: bytes) -> bytes: + """Recompute trailing CRC32 checksum. + + SETTINS/*.set files end with hexstr with CRC32 of everything before them. + """ + import binascii + if len(data) < 9: + raise ValueError("File too short for checksum repair") + payload = data[:-8] + crc = binascii.crc32(payload) & 0xFFFFFFFF + return payload + f"{crc:08X}".encode("ascii") + + +def fat_put(src, infile, fat_path, fixsum=False): + data = sys.stdin.buffer.read() if infile == "-" else open(infile, "rb").read() + if fixsum: + data = fat_repair_checksum(data) + eprint(f"Checksum repaired: {data[-8:].decode('ascii')}") + if not fat_path.startswith("/"): + fat_path = "/" + fat_path + + if src.port: + with connect(src.port, src.baud) as proto: + old_fat, _, _ = _fat_read_region_from(proto.read) + fp, fs = _fat_open_fs(old_fat) + with fs.openbin(fat_path, "w") as f: + f.write(data) + new_fat = fp.getvalue() + fs.close() + if len(new_fat) != len(old_fat): + raise RuntimeError("FAT size changed") + + changed = 0 + i = 0 + while i < len(new_fat): + if new_fat[i] == old_fat[i]: + i += 1 + continue + j = i + 1 + while j < len(new_fat) and new_fat[j] != old_fat[j]: + j += 1 + off = i + while off < j: + po = (FAT_BASE_ADDR + off) & (PAGE_SIZE - 1) + n = min(j - off, PAGE_SIZE - po) + proto.write_page(FAT_BASE_ADDR + off, new_fat[off:off + n]) + changed += n + off += n + i = j + eprint(f"fat-put: {fat_path} ({len(data)} bytes, {changed} bytes patched)") + else: + old_fat, _, _ = fat_read_region(src) + fp, fs = _fat_open_fs(old_fat) + with fs.openbin(fat_path, "w") as f: + f.write(data) + new_fat = fp.getvalue() + fs.close() + if len(new_fat) != len(old_fat): + raise RuntimeError("FAT size changed") + with open(src.file, "rb") as f: + blob = bytearray(f.read()) + blob[FAT_BASE_ADDR:FAT_BASE_ADDR + len(new_fat)] = new_fat + with open(src.file, "wb") as f: + f.write(blob) + eprint(f"fat-put: {fat_path} ({len(data)} bytes) -> {src.file}") + + +# non-public extensions + +_private_registered = False + +def _try_register_private(sub): + global _private_registered + if _private_registered: + return + _private_registered = True + try: + import eeprom_private as priv + if hasattr(priv, "register_commands"): + priv.register_commands(sub) + except ImportError: + pass + +def _try_dispatch_private(args): + try: + import eeprom_private as priv + if hasattr(priv, "dispatch_command"): + return priv.dispatch_command(args, connect) + except ImportError: + pass + return False + + + +class SmartHelpParser(argparse.ArgumentParser): + def format_help(self): + text = super().format_help().rstrip() + for action in self._actions: + if not isinstance(action, argparse._SubParsersAction): + continue + rows, examples = [], [] + for name, sp in action.choices.items(): + pos = [a.metavar or a.dest.upper() for a in sp._actions + if not a.option_strings and a.dest != "help"] + desc = (sp.description or "").strip() + rows.append((name, " ".join(pos), desc)) + ex = getattr(sp, "examples", None) + if ex: + examples.append((name, ex)) + if rows: + w1, w2 = max(len(r[0]) for r in rows), max(len(r[1]) for r in rows) + text += "\n\nCommands:\n" + for n, a, d in rows: + text += f" {n.ljust(w1)} {a.ljust(w2)} {d}\n" + if examples: + text += "\nExamples:\n" + for _, exl in examples: + for ex in exl: + text += f" {ex}\n" + return text + "\n" + + +def main(): + parser = SmartHelpParser(description="Resmed S10 EEPROM tool") + sub = parser.add_subparsers(dest="cmd", required=True) + + common = argparse.ArgumentParser(add_help=False) + common.add_argument("-p", "--port", help="Serial port (e.g. /dev/ttyACM0)") + common.add_argument("--baud", type=int, default=None, + help="Target baud (default: auto-negotiate to highest)") + + p = sub.add_parser("ping", parents=[common], + help="Ping device", + description="Ping device and display firmware version") + p.examples = ["eeprom_tool.py ping -p /dev/ttyACM0"] + + p = sub.add_parser("read", parents=[common], + help="Read EEPROM to file", + description="Read full EEPROM contents into a file") + p.add_argument("outfile") + p.add_argument("addr", nargs="?", type=lambda x: int(x, 0), + help="Start address (optional)") + p.add_argument("length", nargs="?", type=lambda x: int(x, 0), + help="Length (optional)") + p.examples = ["eeprom_tool.py read -p /dev/ttyACM0 dump.bin", + "eeprom_tool.py read -p /dev/ttyACM0 part.bin 0x10 0x100"] + + p = sub.add_parser("write", parents=[common], + help="Write EEPROM from file", + description="Write full EEPROM contents from a file") + p.add_argument("infile") + p.examples = ["eeprom_tool.py write -p /dev/ttyACM0 image.bin"] + + p = sub.add_parser("erase", parents=[common], + help="Erase entire EEPROM", + description="Erase entire EEPROM contents") + p.examples = ["eeprom_tool.py erase -p /dev/ttyACM0"] + + p = sub.add_parser("fixcrc", parents=[common], + help="Recalculate and store header CRC", + description="Recalculate and store EEPROM header CRC") + p.examples = ["eeprom_tool.py fixcrc -p /dev/ttyACM0"] + + p = sub.add_parser("patch", parents=[common], + help="Patch bytes at address", + description="Patch raw bytes at a given EEPROM address") + p.add_argument("addr", type=lambda x: int(x, 0), help="Start address (hex)") + p.add_argument("data", help="Hex byte string (e.g. '01ABFF')") + p.examples = ["eeprom_tool.py patch -p /dev/ttyACM0 0x120 01ABFF"] + + p = sub.add_parser("reset", parents=[common], + help="Reset device", + description="Send reset command to device") + p.examples = ["eeprom_tool.py reset -p /dev/ttyACM0"] + + if HAVE_PYFATFS: + p = sub.add_parser("fat-ls", parents=[common], + help="List FAT directory", + description="List files in embedded FAT filesystem") + p.add_argument("--file", help="Read from dump file instead of device") + p.add_argument("path", nargs="?", default="/") + p.examples = ["eeprom_tool.py fat-ls -p /dev/ttyACM0", + "eeprom_tool.py fat-ls --file dump.bin /SETTINGS"] + + p = sub.add_parser("fat-get", parents=[common], + help="Read FAT file", + description="Dump FAT file to stdout (-) or to an output file") + p.add_argument("--file", help="Read from dump file instead of device") + p.add_argument("fat_path"); p.add_argument("outfile") + p.examples = ["eeprom_tool.py fat-get -p /dev/ttyACM0 /SETTINGS/BGL.set -", + "eeprom_tool.py fat-get --file dump.bin /SETTINGS/BGL.set out.bin"] + + p = sub.add_parser("fat-getdir", parents=[common], + help="Read FAT directory", + description="Dump FAT directory tree to an output path") + p.add_argument("--file", help="Read from dump file instead of device") + p.add_argument("fat_path"); p.add_argument("outpath") + p.examples = ["eeprom_tool.py fat-getdir -p /dev/ttyACM0 /SETTINGS ./out/"] + + p = sub.add_parser("fat-put", parents=[common], + help="Write FAT file", + description="Upload file into embedded FAT filesystem (use '-' to read from stdin)") + p.add_argument("--file", help="Write to dump file instead of device") + p.add_argument("--fixsum", action="store_true", + help="Recompute trailing CRC32 checksum after write") + p.add_argument("infile"); p.add_argument("fat_path") + p.examples = ["eeprom_tool.py fat-put -p /dev/ttyACM0 in.bin /SETTINGS/BGL.set", + "cat in.bin | eeprom_tool.py fat-put --file dump.bin - /SETTINGS/BGL.set"] + + _try_register_private(sub) + + argv = sys.argv[1:] + cmds = set(sub.choices.keys()) + for i, a in enumerate(argv): + if a in cmds: + if i > 0: + argv = [a] + argv[:i] + argv[i+1:] + break + args = parser.parse_args(argv) + + def need_port(): + if not args.port: + parser.error("-p / --port is required for this command") + + if args.cmd == "ping": + need_port() + with connect(args.port, args.baud) as proto: + print(f"Device: {proto.ping()}") + elif args.cmd == "read": + need_port() + read_eeprom(args.port, args.outfile, args.addr, args.length, args.baud) + elif args.cmd == "write": + need_port() + write_eeprom(args.port, args.infile, args.baud) + elif args.cmd == "erase": + need_port() + erase_eeprom(args.port, args.baud) + elif args.cmd == "fixcrc": + need_port() + cmd_fixcrc(args.port, args.baud) + elif args.cmd == "patch": + need_port() + patch_bytes(args.port, args.addr, bytes.fromhex(args.data), args.baud) + elif args.cmd == "reset": + need_port() + with connect(args.port, DEFAULT_BAUD) as proto: + proto.reset() + eprint("Reset sent") + elif args.cmd == "fat-ls": + file = getattr(args, 'file', None) + if not args.port and not file: + parser.error("fat-ls requires -p/--port or --file") + fat_ls(FatSource(args.port, file, args.baud), args.path) + elif args.cmd == "fat-get": + file = getattr(args, 'file', None) + if not args.port and not file: + parser.error("fat-get requires -p/--port or --file") + fat_get(FatSource(args.port, file, args.baud), args.fat_path, args.outfile) + elif args.cmd == "fat-getdir": + file = getattr(args, 'file', None) + if not args.port and not file: + parser.error("fat-getdir requires -p/--port or --file") + fat_getdir(FatSource(args.port, file, args.baud), args.fat_path, args.outpath) + elif args.cmd == "fat-put": + file = getattr(args, 'file', None) + if not args.port and not file: + parser.error("fat-put requires -p/--port or --file") + fat_put(FatSource(args.port, file, args.baud), args.infile, args.fat_path, + fixsum=getattr(args, 'fixsum', False)) + else: + if not _try_dispatch_private(args): + parser.error(f"Unknown: {args.cmd}") + +if __name__ == "__main__": + main() diff --git a/python/fix_crc.py b/python/fix_crc.py new file mode 100755 index 0000000..27d3466 --- /dev/null +++ b/python/fix_crc.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +"""Compute/fix CRC16-CCITT (0xFFFF/0x1021) in binary files.""" + +import sys, argparse + + +def crc16(data, crc=0xFFFF): + for b in data: + crc ^= b << 8 + for _ in range(8): + crc = ((crc << 1) ^ 0x1021) & 0xFFFF if crc & 0x8000 else (crc << 1) & 0xFFFF + return crc + + +def parse_int(s): + return int(s, 0) + + +def parse_end(s): + if s.startswith("+"): + return ("len", int(s[1:], 0)) + return ("abs", int(s, 0)) + + +def main(): + p = argparse.ArgumentParser(description="Fix CRC16-CCITT in binary files") + p.add_argument("infile") + p.add_argument("start", nargs="?", type=parse_int, default=0) + p.add_argument("end", nargs="?", type=parse_end, default=None, + help="end offset or +length (default: EOF)") + p.add_argument("--pad", type=parse_int, help="extend file with 0xFF to N bytes") + p.add_argument("--check", action="store_true", help="verify without modifying") + p.add_argument("-o", "--output", help="output file (default: in-place)") + args = p.parse_args() + + with open(args.infile, "rb") as f: + data = bytearray(f.read()) + + if args.pad and args.pad > len(data): + data += b'\xFF' * (args.pad - len(data)) + + start = args.start + end = len(data) if args.end is None else (start + args.end[1] if args.end[0] == "len" else args.end[1]) + + if start >= end or end - start < 4: + p.error(f"bad range: 0x{start:X}..0x{end:X}") + if end > len(data): + p.error(f"0x{end:X} past EOF (0x{len(data):X})") + + crc_off = start + (end - start) - 2 + old = (data[crc_off] << 8) | data[crc_off + 1] + + if args.check: + ok = crc16(data[start:end]) == 0 + print(f"{'OK' if ok else 'BAD'} 0x{old:04X}") + sys.exit(0 if ok else 1) + + new = crc16(bytes(data[start:crc_off])) + data[crc_off] = (new >> 8) & 0xFF + data[crc_off + 1] = new & 0xFF + assert crc16(data[start:end]) == 0 + + outfile = args.output or args.infile + with open(outfile, "wb") as f: + f.write(data) + + if old == new: + print(f"0x{new:04X} (unchanged)") + else: + print(f"0x{old:04X} -> 0x{new:04X}") + + +if __name__ == "__main__": + main()