From 058dd1b62b38357b5b0ef19c18d93edc6c54d819 Mon Sep 17 00:00:00 2001 From: Luxferre Date: Mon, 3 Aug 2026 19:57:38 +0300 Subject: [PATCH] initial upload --- README.md | 112 ++++ go.mod | 8 + go.sum | 4 + main.go | 781 +++++++++++++++++++++++ research/FLASHING_ALGORITHM.md | 195 ++++++ research/brom_flasher.py | 1075 ++++++++++++++++++++++++++++++++ 6 files changed, 2175 insertions(+) create mode 100644 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 research/FLASHING_ALGORITHM.md create mode 100644 research/brom_flasher.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..e82cfbd --- /dev/null +++ b/README.md @@ -0,0 +1,112 @@ +# MTBoss: Direct BootROM SPI NOR flasher/dumper for MediaTek MT6261 feature phones + +## About + +MTBoss is a command-line tool written in Go for reading, writing, erasing, and identifying SPI NOR flash memory on MediaTek MT6261 series feature phone SoCs (such as the Maxcom MM817 and DZ09 smartwatch platform). + +Standard MediaTek flashing workflows rely on loading two-stage Download Agent (DA) binary blobs into SRAM (`0x70007000`) and DRAM (`0x10020000`). On legacy feature phones, standard DA binaries frequently fail or crash due to uninitialized DRAM or memory map mismatches. + +MTBoss implements direct BootROM Serial Flash Interface (SFI) hardware flashing: +* Bypasses Download Agent (DA) binaries completely. +* Manipulates the hardware Serial Flash Interface controller (`0xA0140000`) directly using native BootROM memory access commands (`0xA2`, `0xD2`, `0xD1`, `0xD4`). +* Enables SFI MAC Mode (`SFI_MAC_SEL`) to send raw SPI NOR commands directly to the physical NOR flash chip. +* Manages 64-byte payload chunking to strictly fit within the 160-byte hardware `SFI_GPRAM` buffer limit. +* Uses BootROM memory-mapped reads (`0xD1` at memory map mode 2) for byte-for-byte MD5 verification. + +## Installation + +### Prerequisites + +* Go 1.26 or higher installed. +* Serial port access permissions on Linux (user added to `dialout` or `uucp` group). + +### Via `go install` + +Install the executable directly into your `$GOPATH/bin` directory: + +```bash +go install code.luxferre.top/luxferre/mtboss@latest +``` + +### From source + +Clone the repository and build the binary manually: + +```bash +git clone https://code.luxferre.top/luxferre/mtboss.git +cd mtboss +go build -o mtboss main.go +``` + +## Usage + +### Command line options + +```bash +mtboss [flags] +``` + +Available flags: + +| Flag | Type | Default | Description | +| :--- | :--- | :--- | :--- | +| `-mode` | string | `flash` | Mode of operation: `flash`, `read`, `erase`, or `identify` | +| `-port` | string | `auto` | Serial port device path (e.g. `/dev/ttyUSB0`, `auto`) | +| `-file` | string | `""` | Input or output binary file path | +| `-start` / `-offset` | string | `0x00000000` | Flash start address in hex, decimal, or units (e.g. `0`, `0x4000`, `4K`) | +| `-size` / `-length` | string | `0x400000` | Operation size in hex, decimal, or units (default 4MiB / `0x400000`) | +| `-verify` | bool | `true` | Verify flash writes via readback | +| `-timeout` | int | `600` | Timeout in seconds waiting for BootROM sync | + +### Examples + +Identify connected device and flash chip JEDEC ID: +```bash +mtboss -mode identify -port /dev/ttyUSB0 +``` + +Read full 4MiB flash dump to a file: +```bash +mtboss -mode read -file dump_4mb.bin +``` + +Read a specific region (64KB starting at offset 0x10000): +```bash +mtboss -mode read -file sysparams.bin -start 0x10000 -size 64KB +``` + +Flash firmware binary starting at address 0x20000: +```bash +mtboss -mode flash -file firmware.bin -start 0x20000 +``` + +Erase a specific flash range (16KB at address 0): +```bash +mtboss -mode erase -start 0 -size 16KB +``` + +## FAQ + +### Why does the phone fail to connect or time out? + +Ensure the phone is completely powered off before starting MTBoss. Press and hold the BOOT key (or Power button depending on model) while plugging in the USB cable. Some models (e.g. Maxcom MM817) automatically enter the BootROM mode if connected without a battery. On Linux systems, verify your user account has serial port permissions (`sudo usermod -aG dialout $USER`). + +### How does direct SFI flashing work without a Download Agent (DA)? + +MediaTek MT6261 BootROM provides low-level register access commands (`0xA2`, `0xD2`, `0xD1`, `0xD4`). mtboss uses these commands to disable system watchdogs, configure memory mapping, switch the SFI controller to hardware MAC mode, and write raw SPI command payloads into the hardware GPRAM buffer. + +### Are write protection bits handled automatically? + +Yes. Before erasing or programming, mtboss reads the SPI NOR flash status register (`0x05`). If block protection bits (`BP0`-`BP3`) are enabled, it issues volatile write enable (`0x50`) and status register write (`0x01`) commands to clear protection before executing flash operations. + +### How does write verification work? + +When `-verify=true` is enabled, mtboss sets boot engine memory map mode 2 (`0xA0510000 = 2`), mapping physical flash memory starting at address `0x00000000`. It performs 32-bit block reads via BootROM command `0xD1` and compares the MD5 hash of the readback data against the reference file. + +## Credits + +Created by Luxferre in 2026, released into the public domain with no warranties. + +Based on MediaTek MT6261 BootROM protocol and Serial Flash Interface (SFI) hardware register research. + +Built using the [`go.bug.st/serial`](https://pkg.go.dev/go.bug.st/serial) library for cross-platform serial hardware control. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..03de1b1 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module code.luxferre.top/luxferre/mtboss + +go 1.26.5 + +require ( + go.bug.st/serial v1.8.0 // indirect + golang.org/x/sys v0.43.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..80cce95 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +go.bug.st/serial v1.8.0 h1:ZtnmN8aYXtPlTghwSvDWPHKBHL9TM6oFDa+KpSn4SQE= +go.bug.st/serial v1.8.0/go.mod h1:d0MmS16Qt9b1m06yoYRNUXhRRTJV5Qg2S5EKqQtnayQ= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= diff --git a/main.go b/main.go new file mode 100644 index 0000000..f8d4f65 --- /dev/null +++ b/main.go @@ -0,0 +1,781 @@ +// MTBoss: MT6261 firmware management application +// with zero DA required +// Created by Luxferre in 2026, released into the public domain + +package main + +import ( + "bytes" + "crypto/md5" + "encoding/binary" + "encoding/hex" + "flag" + "fmt" + "os" + "strings" + "time" + + "go.bug.st/serial" +) + +// ==================== REGISTERS & BITS ==================== +const ( + SFI_BASE = 0xA0140000 + SFI_MAC_CTL = SFI_BASE + 0x0000 + SFI_DIRECT_CTL = SFI_BASE + 0x0004 + SFI_MISC_CTL = SFI_BASE + 0x0008 + SFI_MAC_OUTL = SFI_BASE + 0x0010 + SFI_MAC_INL = SFI_BASE + 0x0014 + SFI_MISC_CTL3 = SFI_BASE + 0x0044 + SFI_GPRAM = SFI_BASE + 0x0800 + + SFI_WIP = 1 << 0 + SFI_WIP_READY = 1 << 1 + SFI_TRIG = 1 << 2 + SFI_MAC_EN = 1 << 3 + SFI_MAC_SEL = 1 << 28 + + BOOT_ENG_BASE = 0xA0510000 + + FLASH_SIZE_4MB = 0x400000 // 4MB (4,194,304 bytes) + FLASH_PAGE_SIZE = 0x100 // 256 bytes + FLASH_SECTOR_SZ = 0x1000 // 4KB + + SR_WIP = 0x01 + SR_WEL = 0x02 + SR_BP0 = 0x04 + SR_BP1 = 0x08 + SR_BP2 = 0x10 + SR_BP3 = 0x20 +) + +// ==================== HELPER PARSER ==================== +func parseSizeOrOffset(valStr string) (uint64, error) { + valStr = strings.TrimSpace(valStr) + if valStr == "" { + return 0, fmt.Errorf("empty string") + } + + multiplier := uint64(1) + upper := strings.ToUpper(valStr) + + if strings.HasSuffix(upper, "MIB") || strings.HasSuffix(upper, "MB") { + multiplier = 1024 * 1024 + valStr = valStr[:len(valStr)-2] + if strings.HasSuffix(strings.ToUpper(valStr), "I") { + valStr = valStr[:len(valStr)-1] + } + } else if strings.HasSuffix(upper, "M") { + multiplier = 1024 * 1024 + valStr = valStr[:len(valStr)-1] + } else if strings.HasSuffix(upper, "KIB") || strings.HasSuffix(upper, "KB") { + multiplier = 1024 + valStr = valStr[:len(valStr)-2] + if strings.HasSuffix(strings.ToUpper(valStr), "I") { + valStr = valStr[:len(valStr)-1] + } + } else if strings.HasSuffix(upper, "K") { + multiplier = 1024 + valStr = valStr[:len(valStr)-1] + } + + valStr = strings.TrimSpace(valStr) + var val uint64 + var err error + if strings.HasPrefix(valStr, "0x") || strings.HasPrefix(valStr, "0X") { + _, err = fmt.Sscanf(valStr, "0x%x", &val) + } else { + _, err = fmt.Sscanf(valStr, "%d", &val) + } + if err != nil { + return 0, err + } + return val * multiplier, nil +} + +// ==================== MTBOSS STRUCT ==================== +type MTBoss struct { + port serial.Port + portName string + chipID uint16 +} + +func NewMTBoss(portName string) (*MTBoss, error) { + flasher := &MTBoss{portName: portName} + if err := flasher.connectPort(); err != nil { + return nil, err + } + return flasher, nil +} + +func (f *MTBoss) connectPort() error { + var targetPort string + if f.portName == "" || f.portName == "auto" { + ports, err := serial.GetPortsList() + if err == nil { + for _, p := range ports { + if strings.Contains(p, "ttyUSB") || strings.Contains(p, "ttyACM") || strings.Contains(p, "COM") { + targetPort = p + break + } + } + } + if targetPort == "" { + targetPort = "/dev/ttyUSB0" + } + } else { + targetPort = f.portName + } + + fmt.Printf("Connecting to serial port %s at 115200 baud (RTS/CTS enabled)...\n", targetPort) + mode := &serial.Mode{ + BaudRate: 115200, + DataBits: 8, + Parity: serial.NoParity, + StopBits: serial.OneStopBit, + } + + start := time.Now() + for time.Since(start) < 10*time.Minute { + p, err := serial.Open(targetPort, mode) + if err == nil { + // Enable RTSCTS hardware flow control + _ = p.SetRTS(true) + _ = p.SetDTR(true) + f.port = p + f.portName = targetPort + return nil + } + // Also scan for any new port if set to auto + if f.portName == "auto" { + ports, _ := serial.GetPortsList() + for _, pName := range ports { + if strings.Contains(pName, "ttyUSB") || strings.Contains(pName, "ttyACM") { + p, err := serial.Open(pName, mode) + if err == nil { + _ = p.SetRTS(true) + _ = p.SetDTR(true) + f.port = p + f.portName = pName + return nil + } + } + } + } + time.Sleep(50 * time.Millisecond) + } + return fmt.Errorf("timeout waiting for serial port %s", targetPort) +} + +func (f *MTBoss) Close() { + if f.port != nil { + _ = f.port.Close() + } +} + +// ==================== BASIC SERIAL I/O ==================== +func (f *MTBoss) sendRaw(data []byte) error { + _, err := f.port.Write(data) + return err +} + +func (f *MTBoss) readExact(length int) ([]byte, error) { + buf := make([]byte, length) + read := 0 + for read < length { + n, err := f.port.Read(buf[read:]) + if err != nil { + return nil, err + } + if n == 0 { + time.Sleep(2 * time.Millisecond) + } + read += n + } + return buf, nil +} + +// ==================== BROM REGISTER OPERATIONS ==================== +func (f *MTBoss) readReg16(addr uint32) (uint16, error) { + cmd := make([]byte, 9) + cmd[0] = 0xA2 + binary.BigEndian.PutUint32(cmd[1:5], addr) + binary.BigEndian.PutUint32(cmd[5:9], 1) + + if err := f.sendRaw(cmd); err != nil { + return 0, err + } + if _, err := f.readExact(9); err != nil { // Echo + return 0, err + } + resp, err := f.readExact(2) + if err != nil { + return 0, err + } + return binary.BigEndian.Uint16(resp), nil +} + +func (f *MTBoss) writeReg16(addr uint32, val uint16) error { + cmd := make([]byte, 9) + cmd[0] = 0xD2 + binary.BigEndian.PutUint32(cmd[1:5], addr) + binary.BigEndian.PutUint32(cmd[5:9], 1) + + if err := f.sendRaw(cmd); err != nil { + return err + } + if _, err := f.readExact(9); err != nil { + return err + } + if ack, err := f.readExact(2); err != nil || !bytes.Equal(ack, []byte{0x00, 0x01}) { + return fmt.Errorf("writeReg16 cmd ACK error: %v", ack) + } + + valBuf := make([]byte, 2) + binary.BigEndian.PutUint16(valBuf, val) + if err := f.sendRaw(valBuf); err != nil { + return err + } + if _, err := f.readExact(2); err != nil { + return err + } + if ack, err := f.readExact(2); err != nil || !bytes.Equal(ack, []byte{0x00, 0x01}) { + return fmt.Errorf("writeReg16 data ACK error: %v", ack) + } + return nil +} + +func (f *MTBoss) readReg32(addr uint32) (uint32, error) { + cmd := make([]byte, 9) + cmd[0] = 0xD1 + binary.BigEndian.PutUint32(cmd[1:5], addr) + binary.BigEndian.PutUint32(cmd[5:9], 1) + + if err := f.sendRaw(cmd); err != nil { + return 0, err + } + if _, err := f.readExact(9); err != nil { + return 0, err + } + resp, err := f.readExact(8) // status[2] + data[4] + status[2] + if err != nil { + return 0, err + } + return binary.LittleEndian.Uint32(resp[2:6]), nil +} + +func (f *MTBoss) writeReg32(addr uint32, val uint32) error { + cmd := make([]byte, 9) + cmd[0] = 0xD4 + binary.BigEndian.PutUint32(cmd[1:5], addr) + binary.BigEndian.PutUint32(cmd[5:9], 1) + + if err := f.sendRaw(cmd); err != nil { + return err + } + if _, err := f.readExact(9); err != nil { + return err + } + if ack, err := f.readExact(2); err != nil || !bytes.Equal(ack, []byte{0x00, 0x01}) { + return fmt.Errorf("writeReg32 cmd ACK error: %v", ack) + } + + valBuf := make([]byte, 4) + binary.BigEndian.PutUint32(valBuf, val) + if err := f.sendRaw(valBuf); err != nil { + return err + } + if _, err := f.readExact(4); err != nil { + return err + } + if ack, err := f.readExact(2); err != nil || !bytes.Equal(ack, []byte{0x00, 0x01}) { + return fmt.Errorf("writeReg32 data ACK error: %v", ack) + } + return nil +} + +// ==================== BOOTROM HANDSHAKE ==================== +func (f *MTBoss) WaitAndConnect(timeoutSec int) error { + fmt.Println("\n============================================================") + fmt.Println("Waiting for MTBoss (MT6261) BootROM Connection") + fmt.Println("============================================================") + fmt.Println("1. Power OFF phone completely") + fmt.Println("2. Press and hold BOOT key (or Power button)") + fmt.Println("3. Plug in USB cable now...") + + start := time.Now() + dots := 0 + for time.Since(start) < time.Duration(timeoutSec)*time.Second { + _ = f.sendRaw([]byte{0xA0}) + time.Sleep(5 * time.Millisecond) + + b := make([]byte, 1) + n, _ := f.port.Read(b) + if n > 0 && b[0] == 0x5F { + // Second sync packet + _ = f.sendRaw([]byte{0x0A, 0x50, 0x05}) + ack3, err := f.readExact(3) + if err == nil && bytes.Equal(ack3, []byte{0xF5, 0xAF, 0xFA}) { + fmt.Println("\nConnected to MT6261 BootROM!") + + chip, err := f.readReg16(0x80000008) + if err == nil { + f.chipID = chip + fmt.Printf("Chip ID: 0x%04X\n", chip) + } + + // Disable watchdogs & enable USB download + _ = f.writeReg16(0xA0030000, 0x2200) // System watchdog + _ = f.writeReg16(0xA0700A28, 0x8000) // USB download mode + _ = f.writeReg16(0xA0700A24, 0x0002) // Battery watchdog + _ = f.writeReg32(BOOT_ENG_BASE, 2) // Boot engine memory map mode 2 + fmt.Println("BootROM initialized & watchdogs disabled.") + return nil + } + } + + dots++ + if dots%20 == 0 { + fmt.Print(".") + } + } + return fmt.Errorf("timeout waiting for BootROM sync") +} + +// ==================== SFI HARDWARE MAC MODE ==================== +func (f *MTBoss) sfiMacCmdWrite(cmdByte byte, addr *uint32, data []byte) error { + var payload []byte + payload = append(payload, cmdByte) + if addr != nil { + addrBytes := make([]byte, 4) + binary.BigEndian.PutUint32(addrBytes, *addr) + payload = append(payload, addrBytes[1:]...) // 3-byte big-endian address + } + payload = append(payload, data...) + + totalLen := uint32(len(payload)) + + // 1. Write payload to GPRAM (0xA0140800) in 32-bit LE words + for i := uint32(0); i < totalLen; i += 4 { + end := i + 4 + if end > totalLen { + end = totalLen + } + chunk := make([]byte, 4) + copy(chunk, payload[i:end]) + valLE := binary.LittleEndian.Uint32(chunk) + if err := f.writeReg32(SFI_GPRAM+i, valLE); err != nil { + return err + } + } + + // 2. Mask AHB Channel 2 + misc3, _ := f.readReg32(SFI_MISC_CTL3) + _ = f.writeReg32(SFI_MISC_CTL3, misc3|(1<<9)) + + // 3. Enable MAC Mode FIRST (SFI_MAC_SEL bit 28 | SFI_MAC_EN bit 3) + macVal := uint32(SFI_MAC_SEL | SFI_MAC_EN) + if err := f.writeReg32(SFI_MAC_CTL, macVal); err != nil { + return err + } + + // 4. Set OUTL and INL lengths while MAC mode is active + _ = f.writeReg32(SFI_MAC_OUTL, totalLen) + _ = f.writeReg32(SFI_MAC_INL, 0) + + // 5. Trigger transaction (SFI_TRIG bit 2) + _ = f.writeReg32(SFI_MAC_CTL, macVal|SFI_TRIG) + + // 6. Poll for completion (WIP_READY bit 1 set, WIP bit 0 clear) + for i := 0; i < 300; i++ { + v, err := f.readReg32(SFI_MAC_CTL) + if err == nil && (v&SFI_WIP_READY != 0) && (v&SFI_WIP == 0) { + break + } + time.Sleep(1 * time.Millisecond) + } + + // 7. Clean up MAC mode and unmask AHB + _ = f.writeReg32(SFI_MAC_CTL, 0) + _ = f.writeReg32(SFI_MISC_CTL3, misc3&^(1<<9)) + return nil +} + +func (f *MTBoss) sfiMacCmdRead(cmdByte byte, readLen uint32) ([]byte, error) { + // 1. Write command to GPRAM + if err := f.writeReg32(SFI_GPRAM, uint32(cmdByte)); err != nil { + return nil, err + } + + // 2. Mask AHB Channel 2 + misc3, _ := f.readReg32(SFI_MISC_CTL3) + _ = f.writeReg32(SFI_MISC_CTL3, misc3|(1<<9)) + + // 3. Enable MAC Mode FIRST + macVal := uint32(SFI_MAC_SEL | SFI_MAC_EN) + _ = f.writeReg32(SFI_MAC_CTL, macVal) + + // 4. Set OUTL and INL lengths + _ = f.writeReg32(SFI_MAC_OUTL, 1) + _ = f.writeReg32(SFI_MAC_INL, readLen) + + // 5. Trigger + _ = f.writeReg32(SFI_MAC_CTL, macVal|SFI_TRIG) + + // 6. Poll completion + for i := 0; i < 300; i++ { + v, err := f.readReg32(SFI_MAC_CTL) + if err == nil && (v&SFI_WIP_READY != 0) && (v&SFI_WIP == 0) { + break + } + time.Sleep(1 * time.Millisecond) + } + + // 7. Read response from GPRAM + totalBytes := 1 + readLen + var resBuf bytes.Buffer + for i := uint32(0); i < (totalBytes + 3); i += 4 { + val, _ := f.readReg32(SFI_GPRAM + i) + wordBytes := make([]byte, 4) + binary.LittleEndian.PutUint32(wordBytes, val) + resBuf.Write(wordBytes) + } + + // 8. Clean up + _ = f.writeReg32(SFI_MAC_CTL, 0) + _ = f.writeReg32(SFI_MISC_CTL3, misc3&^(1<<9)) + + fullData := resBuf.Bytes() + if uint32(len(fullData)) < 1+readLen { + return nil, fmt.Errorf("short SFI MAC read") + } + return fullData[1 : 1+readLen], nil +} + +// ==================== SPI FLASH HIGH-LEVEL ==================== +func (f *MTBoss) ReadJEDECID() ([]byte, error) { + return f.sfiMacCmdRead(0x9F, 3) +} + +func (f *MTBoss) ReadStatusRegister() (byte, error) { + data, err := f.sfiMacCmdRead(0x05, 1) + if err != nil || len(data) == 0 { + return 0, err + } + return data[0], nil +} + +func (f *MTBoss) WaitUntilReady(timeoutSec int) error { + start := time.Now() + for time.Since(start) < time.Duration(timeoutSec)*time.Second { + sr, err := f.ReadStatusRegister() + if err == nil && (sr&SR_WIP == 0) { + return nil + } + time.Sleep(5 * time.Millisecond) + } + return fmt.Errorf("timeout waiting for SPI flash ready") +} + +func (f *MTBoss) UnlockWriteProtection() error { + sr, err := f.ReadStatusRegister() + if err == nil { + fmt.Printf("SPI Flash Status Register: 0x%02X\n", sr) + if sr&(SR_BP0|SR_BP1|SR_BP2|SR_BP3) != 0 { + fmt.Println("Write protection enabled - disabling...") + _ = f.sfiMacCmdWrite(0x50, nil, nil) // Volatile WREN + _ = f.sfiMacCmdWrite(0x01, nil, []byte{0x00}) + _ = f.WaitUntilReady(5) + sr, _ = f.ReadStatusRegister() + fmt.Printf("Status Register after unlocking: 0x%02X\n", sr) + } + } + return nil +} + +func (f *MTBoss) EnableWrite() error { + return f.sfiMacCmdWrite(0x06, nil, nil) +} + +func (f *MTBoss) EraseSector(addr uint32) error { + if err := f.WaitUntilReady(5); err != nil { + return err + } + if err := f.EnableWrite(); err != nil { + return err + } + if err := f.sfiMacCmdWrite(0x20, &addr, nil); err != nil { + return err + } + return f.WaitUntilReady(5) +} + +func (f *MTBoss) PageProgram(addr uint32, data []byte) error { + const maxChunk = 64 + offset := 0 + for offset < len(data) { + end := offset + maxChunk + if end > len(data) { + end = len(data) + } + chunk := data[offset:end] + currAddr := addr + uint32(offset) + + if err := f.WaitUntilReady(5); err != nil { + return err + } + if err := f.EnableWrite(); err != nil { + return err + } + if err := f.sfiMacCmdWrite(0x02, &currAddr, chunk); err != nil { + return err + } + if err := f.WaitUntilReady(5); err != nil { + return err + } + offset += len(chunk) + } + return nil +} + +// ==================== BROM MEMORY-MAPPED READING ==================== +func (f *MTBoss) ReadFlash(address uint32, length uint32) ([]byte, error) { + var result bytes.Buffer + remaining := length + currAddr := address + const blkSize = 1024 + + for remaining > 0 { + rsize := remaining + if rsize > blkSize { + rsize = blkSize + } + wordsCnt := rsize >> 2 + + cmd := make([]byte, 9) + cmd[0] = 0xD1 + binary.BigEndian.PutUint32(cmd[1:5], currAddr) + binary.BigEndian.PutUint32(cmd[5:9], wordsCnt) + + if err := f.sendRaw(cmd); err != nil { + return nil, err + } + if _, err := f.readExact(9); err != nil { + return nil, err + } + + respLen := (int(wordsCnt) * 4) + 4 + resp, err := f.readExact(respLen) + if err != nil { + return nil, err + } + + // Skip 2-byte header and 2-byte status footer + rawWords := resp[2 : 2+int(wordsCnt)*4] + for i := 0; i < len(rawWords); i += 4 { + wordVal := binary.LittleEndian.Uint32(rawWords[i : i+4]) + wordBE := make([]byte, 4) + binary.BigEndian.PutUint32(wordBE, wordVal) + result.Write(wordBE) + } + + currAddr += rsize + remaining -= rsize + + progress := float64(length-remaining) / float64(length) * 100.0 + fmt.Printf("\rReading: %.1f%%", progress) + } + fmt.Println("\nRead complete.") + return result.Bytes(), nil +} + +// ==================== HIGH-LEVEL FLASH OPERATIONS ==================== +func (f *MTBoss) WriteRegion(address uint32, data []byte, verify bool) error { + fmt.Printf("\n============================================================\n") + fmt.Printf("Flashing Region at 0x%08X (Size: %d bytes / 0x%X)\n", address, len(data), len(data)) + fmt.Printf("============================================================\n") + + _ = f.UnlockWriteProtection() + + startSector := address & ^uint32(FLASH_SECTOR_SZ-1) + endAddr := address + uint32(len(data)) + + fmt.Printf("Erasing sectors from 0x%08X to 0x%08X...\n", startSector, endAddr) + for sec := startSector; sec < endAddr; sec += FLASH_SECTOR_SZ { + fmt.Printf(" Erasing sector at 0x%08X...\n", sec) + if err := f.EraseSector(sec); err != nil { + return fmt.Errorf("failed erasing sector 0x%08X: %v", sec, err) + } + } + fmt.Println("Sectors erased.") + + fmt.Printf("Programming %d bytes to 0x%08X...\n", len(data), address) + offset := 0 + for offset < len(data) { + end := offset + 256 + if end > len(data) { + end = len(data) + } + chunk := data[offset:end] + currAddr := address + uint32(offset) + if err := f.PageProgram(currAddr, chunk); err != nil { + return fmt.Errorf("failed programming at 0x%08X: %v", currAddr, err) + } + offset += len(chunk) + fmt.Printf("\rProgramming: %.1f%%", float64(offset)/float64(len(data))*100.0) + } + fmt.Println("\nProgramming complete.") + + if verify { + fmt.Printf("Verifying %d bytes at 0x%08X...\n", len(data), address) + readData, err := f.ReadFlash(address, uint32(len(data))) + if err != nil { + return fmt.Errorf("verification read failed: %v", err) + } + + refMD5 := md5.Sum(data) + readMD5 := md5.Sum(readData) + fmt.Printf("Reference MD5: %s\n", hex.EncodeToString(refMD5[:])) + fmt.Printf("Readback MD5: %s\n", hex.EncodeToString(readMD5[:])) + + if bytes.Equal(data, readData) { + fmt.Println("\n*** VERIFICATION PASSED - EXACT BYTE-FOR-BYTE MATCH ***") + } else { + return fmt.Errorf("verification FAILED - binary mismatch!") + } + } + return nil +} + +func (f *MTBoss) Reset() { + fmt.Println("Resetting device...") + _ = f.writeReg16(0xA003001C, 0x1209) +} + +// ==================== MAIN CLI ==================== +func main() { + portFlag := flag.String("port", "auto", "Serial port device (e.g. /dev/ttyUSB0, /dev/ttyUSB1 or auto)") + modeFlag := flag.String("mode", "flash", "Mode of operation: flash, read, erase, identify") + fileFlag := flag.String("file", "", "Input/Output binary file path") + startFlag := flag.String("start", "", "Flash start offset address (e.g. 0, 0x0000, 4K)") + offsetFlag := flag.String("offset", "0x00000000", "Flash offset address (alias for -start, default 0)") + sizeFlag := flag.String("size", "", "Operation size in bytes/hex/units (e.g. 4MB, 64KB, 0x400000)") + lengthFlag := flag.String("length", "0x400000", "Operation size (alias for -size, default 4MiB / 0x400000)") + verifyFlag := flag.Bool("verify", true, "Verify flash write via readback") + timeoutFlag := flag.Int("timeout", 600, "Timeout in seconds waiting for BootROM sync") + + flag.Parse() + + // Determine start address string (prefer -start if explicitly set) + startStr := *offsetFlag + if *startFlag != "" { + startStr = *startFlag + } + + // Determine size string (prefer -size if explicitly set) + sizeStr := *lengthFlag + if *sizeFlag != "" { + sizeStr = *sizeFlag + } + + start, err := parseSizeOrOffset(startStr) + if err != nil { + fmt.Printf("Invalid start/offset format '%s': %v\n", startStr, err) + os.Exit(1) + } + + size, err := parseSizeOrOffset(sizeStr) + if err != nil { + fmt.Printf("Invalid size/length format '%s': %v\n", sizeStr, err) + os.Exit(1) + } + + flasher, err := NewMTBoss(*portFlag) + if err != nil { + fmt.Printf("Error opening serial port: %v\n", err) + os.Exit(1) + } + defer flasher.Close() + + if err := flasher.WaitAndConnect(*timeoutFlag); err != nil { + fmt.Printf("BootROM Connection failed: %v\n", err) + os.Exit(1) + } + + switch *modeFlag { + case "identify": + id, err := flasher.ReadJEDECID() + if err == nil && len(id) >= 3 { + fmt.Printf("JEDEC ID: %02X %02X %02X\n", id[0], id[1], id[2]) + } else { + fmt.Printf("Could not read JEDEC ID: %v\n", err) + } + flasher.Reset() + + case "read": + if *fileFlag == "" { + fmt.Println("Error: --file is required for read mode") + os.Exit(1) + } + fmt.Printf("Reading %d (0x%X) bytes from 0x%08X to %s...\n", size, size, uint32(start), *fileFlag) + data, err := flasher.ReadFlash(uint32(start), uint32(size)) + if err != nil { + fmt.Printf("Read failed: %v\n", err) + os.Exit(1) + } + if err := os.WriteFile(*fileFlag, data, 0644); err != nil { + fmt.Printf("Failed writing file: %v\n", err) + os.Exit(1) + } + hash := md5.Sum(data) + fmt.Printf("Successfully saved %s (MD5: %s)\n", *fileFlag, hex.EncodeToString(hash[:])) + flasher.Reset() + + case "erase": + fmt.Printf("Erasing %d (0x%X) bytes at 0x%08X...\n", size, size, uint32(start)) + startSec := uint32(start) & ^uint32(FLASH_SECTOR_SZ-1) + endSec := uint32(start + size) + for sec := startSec; sec < endSec; sec += FLASH_SECTOR_SZ { + fmt.Printf("Erasing sector at 0x%08X...\n", sec) + if err := flasher.EraseSector(sec); err != nil { + fmt.Printf("Erase failed: %v\n", err) + os.Exit(1) + } + } + fmt.Println("Erase complete.") + flasher.Reset() + + case "flash": + if *fileFlag == "" { + fmt.Println("Error: --file is required for flash mode") + os.Exit(1) + } + data, err := os.ReadFile(*fileFlag) + if err != nil { + fmt.Printf("Failed reading file %s: %v\n", *fileFlag, err) + os.Exit(1) + } + + // If size was explicitly set or if size < len(data), limit payload to specified size + if uint64(len(data)) > size { + fmt.Printf("Limiting file payload from %d to specified size %d (0x%X) bytes\n", len(data), size, size) + data = data[:size] + } + + fHash := md5.Sum(data) + fmt.Printf("Loaded %s (%d bytes, MD5: %s)\n", *fileFlag, len(data), hex.EncodeToString(fHash[:])) + + if err := flasher.WriteRegion(uint32(start), data, *verifyFlag); err != nil { + fmt.Printf("Flashing failed: %v\n", err) + os.Exit(1) + } + fmt.Println("\n============================================================") + fmt.Println("Operation completed successfully!") + fmt.Println("============================================================") + flasher.Reset() + + default: + fmt.Printf("Unknown mode: %s\n", *modeFlag) + os.Exit(1) + } +} diff --git a/research/FLASHING_ALGORITHM.md b/research/FLASHING_ALGORITHM.md new file mode 100644 index 0000000..5ad4a2d --- /dev/null +++ b/research/FLASHING_ALGORITHM.md @@ -0,0 +1,195 @@ +# MTBoss: MT6261 Direct BootROM SPI NOR Flashing Algorithm + +Thorough technical documentation of the BROM-direct SPI NOR flash programming algorithm for MediaTek MT6261 series feature phones (such as the Maxcom MM817). + +This approach bypasses Download Agent (DA) binaries entirely by manipulating the hardware Serial Flash Interface (SFI) controller directly via BootROM register access commands. + +--- + +## 1. Overview & Architecture + +Standard MediaTek flashing workflows load a two-stage Download Agent (DA) into SRAM (`0x70007000`) and DRAM (`0x10020000`) via BootROM commands `0xD7` and `0xD5`. However, on many legacy feature phone platforms, standard DA binaries silently crash or fail to execute due to uninitialized DRAM or memory map mismatches. + +This algorithm implements **Direct BootROM SFI Hardware Flashing**: +* **Handshake & Register Initialization**: Connects via native BootROM serial protocol, disables system/battery watchdogs, and enables USB download mode. +* **Direct Hardware SFI Controller Manipulation**: Uses BROM 32-bit memory access commands (`0xD1` / `0xD4`) to control the Serial Flash Interface (`0xA0140000`). +* **Hardware MAC Controller Switching**: Enables SFI MAC Mode (`SFI_MAC_SEL`) to send raw SPI NOR commands (WREN `0x06`, Sector Erase `0x20`, Page Program `0x02`, Read Status `0x05`, Read JEDEC ID `0x9F`) directly to the physical NOR flash chip. +* **Buffer Management**: Chunks page program payloads to **64 bytes** to strictly fit within the 160-byte (`0x00A0`) `SFI_GPRAM` hardware buffer limit. +* **Memory-Mapped Verification**: Uses BROM 32-bit read commands (`0xD1`) at Memory Map Mode 2 (`0xA0510000 = 2`) to perform byte-for-byte readback verification. + +--- + +## 2. Hardware Memory & Register Map + +### 2.1 Base Addresses +| Peripheral | Base Address | Description | +| :--- | :--- | :--- | +| **SFI_BASE** | `0xA0140000` | Serial Flash Interface Registers | +| **CONFIG_BASE** | `0xA0010000` | Clock & System Configuration Registers | +| **RGU_BASE** | `0xA0030000` | Reset Generation Unit (Watchdog) | +| **BOOT_ENG_BASE** | `0xA0510000` | Boot Engine Configuration | +| **PMU_BASE** | `0xA0700000` | Power Management Unit | + +### 2.2 SFI Registers (`0xA0140000`) +| Offset | Name | Bitfield / Description | +| :--- | :--- | :--- | +| `+0x0000` | `SFI_MAC_CTL` | Bit 0: `SFI_WIP` (Write In Progress)
Bit 1: `SFI_WIP_READY` (Trigger Completed)
Bit 2: `SFI_TRIG` (Trigger MAC Operation)
Bit 3: `SFI_MAC_EN` (Enable MAC Controller)
Bit 4: `SFI_MAC_SIO_SEL` (Serial I/O Select)
Bit 16: `SFI_RELEASE_MAC` (Release MAC Mode)
Bit 28: `SFI_MAC_SEL` (Route SPI Bus to MAC Controller) | +| `+0x0004` | `SFI_DIRECT_CTL` | Direct Read Controller Configuration | +| `+0x0008` | `SFI_MISC_CTL` | Bit 8: `SFI_REQ_IDLE`
Bit 9: `SFI_BOOT_REMAP`
Bit 23: `SFI_NO_RELOAD` | +| `+0x0010` | `SFI_MAC_OUTL` | Outgoing Command/Data Length (in bytes) | +| `+0x0014` | `SFI_MAC_INL` | Incoming Data Length to read (in bytes) | +| `+0x0044` | `SFI_MISC_CTL3` | Bit 9: `SFI_CH2_TRANS_MASK` (AHB Channel Mask)
Bit 13: `SFI_CH2_TRANS_IDLE` | +| `+0x0800` | `SFI_GPRAM` | General Purpose RAM Buffer (160 bytes total, `0x0800 - 0x08A0`) | + +--- + +## 3. Step-by-Step Flashing Algorithm + +```mermaid +flowchart TD + A["Start: Connect Port (rtscts=True)"] --> B["BROM Sync: Send 0xA0 (wait 0x5F)
Send 0x0A5005 (wait 0xF5AFFA)"] + B --> C["Read Chip ID (0xA2 to 0x80000008)"] + C --> D["Disable Watchdogs & Set Boot Map Mode 2"] + D --> E["Unlock SPI Flash Write Protection"] + E --> F["Erase 4KB Sectors via SFI MAC (0x20)"] + F --> G["Program Pages via SFI MAC (0x02)
(64-byte chunks to fit GPRAM)"] + G --> H["Read Back & Verify via BROM 0xD1"] + H --> I["Reset Device (0xA003001C = 0x1209)"] +``` + +### Step 1: BootROM Serial Connection & Handshake +1. Open serial port with **`rtscts=True`** and **`dsrdtr=True`** at 115200 baud. +2. Send single sync byte `0xA0` repeatedly until `0x5F` is received. +3. Send 3-byte command packet `b'\x0A\x50\x05'`. +4. Verify receive 3-byte ACK packet `b'\xF5\xAF\xFA'`. + +### Step 2: Chip Identification & Watchdog Disabling +1. Read Chip ID via 16-bit BROM read command `0xA2`: + - Send `b'\xA2' + struct.pack('>II', 0x80000008, 1)` + - Receive echo and 2-byte response (`0x6261` = MT6261). +2. Disable watchdogs and configure power registers via 16-bit BROM write command `0xD2`: + - `0xA0030000 = 0x2200` (Disable system watchdog) + - `0xA0700A28 = 0x8000` (Enable USB download mode) + - `0xA0700A24 = 0x0002` (Disable battery watchdog) + +### Step 3: Configure Memory Mapping Mode 2 +1. Set boot engine memory map mode to Mode 2 via 32-bit BROM write command `0xD4`: + - Send `b'\xD4' + struct.pack('>II', 0xA0510000, 1)` + `struct.pack('>I', 2)` + - Maps physical SPI NOR flash starting at CPU address `0x00000000`. + +### Step 4: SPI NOR Status Register Inspection & Unlock +1. Read SPI NOR Status Register (`0x05`) via `sfi_mac_cmd_read(0x05, 1)`. +2. Inspect Block Protect bits (`BP0`, `BP1`, `BP2`, `BP3`). +3. If protected, issue status register unlock: + - Send Volatile Write Enable `0x50` via `sfi_mac_cmd_write(0x50)`. + - Send Write Status Register `0x01` with data `0x00` via `sfi_mac_cmd_write(0x01, data=b'\x00')`. + +### Step 5: SFI Hardware MAC Mode Register Sequencing +All SPI NOR operations (Erase, Program, Status Read) must follow this exact register sequence: + +```python +# 1. Format payload: [cmd_byte] + [3-byte big-endian address] + [optional payload data] +payload = bytes([cmd_byte]) +if addr is not None: + payload += addr.to_bytes(3, 'big') +payload += data +total_len = len(payload) + +# 2. Write payload into SFI GPRAM (0xA0140800) in 32-bit little-endian words +for i in range(0, total_len, 4): + chunk = payload[i : i + 4] + val = int.from_bytes(chunk.ljust(4, b'\x00'), 'little') + write_reg32(0xA0140800 + i, val) + +# 3. Mask AHB Channel 2 in MISC_CTL3 (0xA0140044) +write_reg32(0xA0140044, read_reg32(0xA0140044) | (1 << 9)) + +# 4. Enable MAC mode FIRST (SFI_MAC_SEL bit 28 | SFI_MAC_EN bit 3) +mac_val = (1 << 28) | (1 << 3) +write_reg32(0xA0140000, mac_val) + +# 5. Set OUTL and INL lengths WHILE MAC mode is active +write_reg32(0xA0140010, total_len) # SFI_MAC_OUTL +write_reg32(0xA0140014, 0) # SFI_MAC_INL + +# 6. Trigger transaction by adding SFI_TRIG (bit 2) +write_reg32(0xA0140000, mac_val | (1 << 2)) + +# 7. Poll SFI_MAC_CTL until SFI_WIP_READY (bit 1) is 1 and SFI_WIP (bit 0) is 0 +while True: + v = read_reg32(0xA0140000) + if (v & 0x02) and not (v & 0x01): + break + +# 8. Clean up MAC mode and unmask AHB channel 2 +write_reg32(0xA0140000, 0) +write_reg32(0xA0140044, read_reg32(0xA0140044) & ~(1 << 9)) +``` + +> [!CRITICAL] +> **Register Order Dependency**: Step 3 (`SFI_MAC_CTL` enable) MUST occur BEFORE Step 4 (`SFI_MAC_OUTL` set). Writing to `SFI_MAC_OUTL` while MAC mode is disabled will cause the controller to ignore the transfer length and transmit 0 payload bytes. + +### Step 6: Sector Erasing (4KB Sectors) +For each 4KB boundary (`0x0000`, `0x1000`, `0x2000`, `0x3000`): +1. Send Write Enable (`0x06`) via `sfi_mac_cmd_write(0x06)`. +2. Issue Sector Erase (`0x20`) with target address: + - `sfi_mac_cmd_write(0x20, addr=sector_address)` +3. Wait until flash WIP bit clears in status register (`0x05`). + +### Step 7: Chunked Page Programming +SPI NOR page programming uses command `0x02`. +Because `SFI_GPRAM` is limited to **160 bytes** (`0x0800 - 0x08A0`), page data MUST be chunked into maximum **64 bytes** per command: +1. Divide target page payload into 64-byte chunks. +2. For each 64-byte chunk: + - Send Write Enable (`0x06`). + - Issue Page Program (`0x02`) with current chunk address and 64-byte payload: + `sfi_mac_cmd_write(0x02, addr=curr_addr, data=64_byte_chunk)` + - Wait until flash WIP bit clears in status register (`0x05`). + - Advance address by 64 bytes. + +### Step 8: Memory-Mapped Verification & Readback +Read back the written memory region directly using BROM 32-bit read command `0xD1`: +1. Issue `0xD1` command for 1024-byte blocks (`256` 32-bit words): + - Send `b'\xD1' + struct.pack('>II', curr_addr, 256)` + - Receive echo header + 1028 bytes response. +2. Skip 2-byte header and 2-byte status footer. +3. Convert little-endian words to big-endian binary stream. +4. Perform byte-for-byte MD5 comparison against reference binary image. + +### Step 9: Software Reset +1. Issue system reset command via RGU register: + - `write_reg16(0xA003001C, 0x1209)` +2. Device reboots cleanly into updated firmware. + +--- + +## 4. Flash Layout Reference (Maxcom MM817 4MB SPI NOR) + +```text +0x00000000 +-----------------------------------+ + | ARM Bootloader (ARM_BL) | 10,240 bytes (0x2800) +0x00002800 +-----------------------------------+ + | Extended Bootloader (EXT_BL) | 3,072 bytes (0x0C00) +0x00003400 +-----------------------------------+ + | Boot Padding / Header | 3,072 bytes (0x0C00) +0x00004000 +-----------------------------------+ + | Boot Region 2 Backup | 16,384 bytes (0x4000) +0x00008000 +-----------------------------------+ + | Boot Region 3 Backup | 16,384 bytes (0x4000) +0x0000C000 +-----------------------------------+ + | Main Firmware & File System | ~3.93 MB +0x00400000 +-----------------------------------+ +``` + +--- + +## 5. Verification Checklist + +| Test Phase | Expected Result | Verified Status | +| :--- | :--- | :---: | +| **BootROM Handshake** | Sync ACK `b'\xF5\xAF\xFA'`, Chip ID `0x6261` | PASS | +| **Watchdog Disable** | Reg `0xA0030000` = `0x2200` | PASS | +| **SPI Flash JEDEC ID** | Read `0x9F` returns manufacturer & device ID | PASS | +| **Sector Erase (4KB)** | Memory reads as `0xFF` across sector | PASS | +| **64-Byte Page Program** | Memory reads match programmed payload | PASS | +| **Full 16KB Verification**| 100% exact MD5 match against reference image | PASS | diff --git a/research/brom_flasher.py b/research/brom_flasher.py new file mode 100644 index 0000000..f78d65f --- /dev/null +++ b/research/brom_flasher.py @@ -0,0 +1,1075 @@ +#!/usr/bin/env python3 +""" +MT6261D BROM Direct SPI NOR Flasher - 4MB Flash Version +Optimized for 4MB (32Mbit) SPI NOR flash +""" + +import serial +import time +import struct +import hashlib +import argparse +import glob +import os +import sys +import json +from typing import Dict, List, Optional, Tuple, Union + +# ==================== BROM PROTOCOL CONSTANTS ==================== +# BootROM Commands +BROM_CMD_SYNC = 0xA0 +BROM_CMD_START = 0x10 +BROM_CMD_SEND_DATA = 0x11 +BROM_CMD_END = 0x12 +BROM_CMD_GET_CHIP_ID = 0x13 +BROM_CMD_GET_HW_CODE = 0x14 +BROM_CMD_JUMP_DA = 0x15 +BROM_CMD_BAUD_CHANGE = 0x16 +BROM_CMD_CRC32 = 0x17 +BROM_CMD_READ_REG = 0x18 +BROM_CMD_WRITE_REG = 0x19 +BROM_CMD_ERASE_FLASH = 0x1A +BROM_CMD_READ_FLASH = 0x1B +BROM_CMD_WRITE_FLASH = 0x1C +BROM_CMD_GET_FLASH_INFO = 0x1D +BROM_CMD_SPI_CMD = 0x1E +BROM_CMD_NAND_CTRL = 0x1F + +# Response codes +BROM_SUCCESS = 0x00 +BROM_ERROR = 0x01 +BROM_TIMEOUT = 0x02 +BROM_CRC_ERROR = 0x03 +BROM_INVALID_CMD = 0x04 +BROM_FLASH_ERROR = 0x05 + +# ==================== SPI NOR FLASH COMMANDS ==================== +# Standard SPI NOR commands +SPI_CMD_READ_ID = (0x9F, 0, 0, 3) # Read JEDEC ID +SPI_CMD_READ_STATUS = (0x05, 0, 0, 1) # Read Status Register +SPI_CMD_WRITE_STATUS = (0x01, 0, 0, 1) # Write Status Register +SPI_CMD_READ_DATA = (0x03, 3, 0, 0) # Read Data +SPI_CMD_FAST_READ = (0x0B, 3, 1, 0) # Fast Read +SPI_CMD_PAGE_PROGRAM = (0x02, 3, 0, 0) # Page Program +SPI_CMD_SECTOR_ERASE = (0x20, 3, 0, 0) # Sector Erase (4KB) +SPI_CMD_BLOCK_ERASE = (0xD8, 3, 0, 0) # Block Erase (64KB) +SPI_CMD_BLOCK_ERASE_32K = (0x52, 3, 0, 0) # Block Erase (32KB) +SPI_CMD_CHIP_ERASE = (0xC7, 0, 0, 0) # Chip Erase +SPI_CMD_WRITE_ENABLE = (0x06, 0, 0, 0) # Write Enable +SPI_CMD_WRITE_DISABLE = (0x04, 0, 0, 0) # Write Disable +SPI_CMD_BULK_ERASE = (0x60, 0, 0, 0) # Bulk Erase + +# Status register bits +SR_WIP = 0x01 # Write In Progress +SR_WEL = 0x02 # Write Enable Latch +SR_BP0 = 0x04 # Block Protect 0 +SR_BP1 = 0x08 # Block Protect 1 +SR_BP2 = 0x10 # Block Protect 2 +SR_BP3 = 0x20 # Block Protect 3 +SR_QE = 0x40 # Quad Enable +SR_SRWD = 0x80 # Status Register Write Disable + +# ==================== MT6261D MEMORY MAP ==================== +# Bootloader addresses (these remain the same) +BOOTLOADER_BD_ADDRESS = 0x00000000 # Boot Region 1 +BOOTLOADER_EB_ADDRESS = 0x00004000 # Extended Boot Region +BOOTLOADER_REGION3_ADDRESS = 0x00008000 # Third boot region +BOOTLOADER_END_ADDRESS = 0x0000C000 # End of boot regions + +# Bootloader partition sizes +BOOT1_SIZE = BOOTLOADER_EB_ADDRESS - BOOTLOADER_BD_ADDRESS # 0x4000 (16KB) +BOOT2_SIZE = BOOTLOADER_REGION3_ADDRESS - BOOTLOADER_EB_ADDRESS # 0x4000 (16KB) +BOOT3_SIZE = BOOTLOADER_END_ADDRESS - BOOTLOADER_REGION3_ADDRESS # 0x4000 (16KB) +GAP_SIZE = 0x4000 # Gap between boot regions and main firmware + +# 4MB Flash Configuration (changed from 16MB) +FLASH_SIZE_4MB = 0x400000 # 4MB +FLASH_SIZE_4MB_BITS = 32 # 32Mbit +FLASH_PAGE_SIZE = 0x100 # 256 bytes +FLASH_SECTOR_SIZE = 0x1000 # 4KB +FLASH_BLOCK_SIZE = 0x10000 # 64KB +FLASH_BLOCK_32K_SIZE = 0x8000 # 32KB blocks (some flash chips) +MAX_TRANSFER_SIZE = 0x1000 # 4KB max transfer (reduced for 4MB) + +# 4MB Flash layout (typical MT6261D with 4MB flash) +# 0x000000 - 0x00FFFF : Bootloader area (64KB) +# 0x010000 - 0x01FFFF : System parameters (64KB) +# 0x020000 - 0x3FFFFF : Main firmware (3.875MB) +FLASH_LAYOUT_4MB = { + 'bootloader': {'start': 0x000000, 'size': 0x10000, 'name': 'Bootloader'}, + 'sysparam': {'start': 0x010000, 'size': 0x10000, 'name': 'System Params'}, + 'main_firmware': {'start': 0x020000, 'size': 0x3E0000, 'name': 'Main Firmware'} +} + +# Common 4MB SPI NOR Flash chips for MT6261D +SUPPORTED_FLASH_CHIPS = { + 'EF4016': {'size': FLASH_SIZE_4MB, 'page': 0x100, 'sector': 0x1000, 'block': 0x10000}, + 'GD25Q32': {'size': FLASH_SIZE_4MB, 'page': 0x100, 'sector': 0x1000, 'block': 0x10000}, + 'W25Q32': {'size': FLASH_SIZE_4MB, 'page': 0x100, 'sector': 0x1000, 'block': 0x10000}, + 'MX25L3205': {'size': FLASH_SIZE_4MB, 'page': 0x100, 'sector': 0x1000, 'block': 0x10000}, + 'SST25VF032': {'size': FLASH_SIZE_4MB, 'page': 0x100, 'sector': 0x1000, 'block': 0x10000}, + 'EN25Q32': {'size': FLASH_SIZE_4MB, 'page': 0x100, 'sector': 0x1000, 'block': 0x10000}, + 'M25P32': {'size': FLASH_SIZE_4MB, 'page': 0x100, 'sector': 0x1000, 'block': 0x10000}, + 'IS25LQ032': {'size': FLASH_SIZE_4MB, 'page': 0x100, 'sector': 0x1000, 'block': 0x10000}, + 'PM25LQ032': {'size': FLASH_SIZE_4MB, 'page': 0x100, 'sector': 0x1000, 'block': 0x10000}, +} + +# Exception Classes +class BROMFlashingError(Exception): + """Custom exception for BROM flashing errors""" + pass + +class FlashError(BROMFlashingError): + """Flash memory specific error""" + pass + +class SPIError(BROMFlashingError): + """SPI communication error""" + pass + +class VerificationError(BROMFlashingError): + """Data verification error""" + pass + +class UnsupportedFlashError(FlashError): + """Unsupported flash chip error""" + pass + +# ==================== BROM FLASHER CLASS ==================== + +class MT6261BROMFlasher: + def __init__(self, port: str, baudrate: int = 115200, timeout: int = 2): + """Initialize the BROM flasher connection with retry support""" + self.port = port + self.baudrate = baudrate + self.timeout = timeout + self.serial = None + self.connected = False + self.chip_id = None + self.hw_code = None + self.flash_info = None + self.flash_size = FLASH_SIZE_4MB # Set to 4MB by default + self.flash_chip_name = None + self.block_size = FLASH_BLOCK_SIZE + self.page_size = FLASH_PAGE_SIZE + self.sector_size = FLASH_SECTOR_SIZE + self.current_baud = baudrate + self.is_4mb_flash = False + self._connect_port() + + def _connect_port(self): + print(f"Waiting for serial port {self.port} or BootROM device...", flush=True) + start = time.time() + while time.time() - start < 600: + ports = glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*') + for p in ports: + try: + self.serial = serial.Serial( + port=p, + baudrate=self.baudrate, + timeout=self.timeout, + rtscts=True, + dsrdtr=True, + bytesize=serial.EIGHTBITS, + parity=serial.PARITY_NONE, + stopbits=serial.STOPBITS_ONE + ) + self.port = p + return + except (serial.SerialException, OSError): + pass + time.sleep(0.05) + raise serial.SerialException(f"Could not open serial port {self.port}") + + def close(self): + """Close the serial connection""" + if self.serial.is_open: + self.serial.close() + + # ==================== BASIC COMMUNICATION ==================== + + def send_raw(self, data: bytes): + """Send raw bytes to device""" + self.serial.write(data) + self.serial.flush() + + def read_raw(self, length: int) -> bytes: + """Read raw bytes from device""" + return self.serial.read(length) + + def send_brom_command(self, command: int, data: bytes = b''): + """Send a BootROM command""" + length = len(data) + packet = struct.pack(' Tuple[int, int, bytes]: + """Receive and parse BootROM response""" + header = self.serial.read(4) + if len(header) < 4: + raise BROMFlashingError("Failed to receive BROM response header") + + status, length_lo, length_hi = struct.unpack(' 0: + response_data = self.serial.read(length_lo) + + return status, length_lo, response_data + + def check_brom_error(self, status: int): + """Check for errors in BROM response""" + if status == BROM_TIMEOUT: + raise BROMFlashingError("BROM timeout") + if status == BROM_CRC_ERROR: + raise BROMFlashingError("CRC error in BROM communication") + if status == BROM_FLASH_ERROR: + raise FlashError("Flash operation failed") + if status == BROM_INVALID_CMD: + raise BROMFlashingError("Invalid BROM command") + if status != BROM_SUCCESS: + raise BROMFlashingError(f"BROM error: status 0x{status:02X}") + + def calculate_crc32(self, data: bytes) -> int: + """Calculate CRC32 for data verification""" + import binascii + return binascii.crc32(data) & 0xFFFFFFFF + + # ==================== FLASH DETECTION ==================== + + def detect_flash_chip(self) -> Tuple[str, int]: + """Detect flash chip and verify 4MB size""" + print("\nDetecting flash chip...") + + # Read JEDEC ID + jedec_id = self.read_flash_id() + + if len(jedec_id) >= 3: + manufacturer = jedec_id[0] + device_id = jedec_id[1] << 8 | jedec_id[2] + print(f"JEDEC ID: {jedec_id.hex()}") + print(f"Manufacturer: 0x{manufacturer:02X}") + print(f"Device ID: 0x{device_id:04X}") + + # Try to identify the chip + for name, info in SUPPORTED_FLASH_CHIPS.items(): + # This is simplified - actual ID matching would be more specific + if manufacturer in [0x01, 0x1C, 0x20, 0xC2, 0xEF, 0x9D]: # Known manufacturers + if info['size'] == FLASH_SIZE_4MB: + self.flash_chip_name = name + self.flash_size = info['size'] + self.page_size = info['page'] + self.sector_size = info['sector'] + self.block_size = info['block'] + self.is_4mb_flash = True + print(f"Detected: {name} (4MB)") + return name, self.flash_size + + # If not in supported list, use default 4MB settings + print(f"Unknown flash chip (MFR:0x{manufacturer:02X}, ID:0x{device_id:04X})") + print("Assuming 4MB flash based on device type") + self.flash_size = FLASH_SIZE_4MB + self.is_4mb_flash = True + return "Unknown", self.flash_size + else: + # No JEDEC ID response, assume 4MB + print("No JEDEC ID response - assuming 4MB flash") + self.flash_size = FLASH_SIZE_4MB + self.is_4mb_flash = True + return "Unknown", self.flash_size + + def validate_address_range(self, address: int, size: int): + """Validate that address and size are within 4MB flash bounds""" + if address < 0 or address >= self.flash_size: + raise FlashError(f"Address 0x{address:08X} outside 4MB flash range") + if size < 0: + raise FlashError("Invalid size") + if address + size > self.flash_size: + raise FlashError(f"Address range 0x{address:08X} + 0x{size:X} exceeds 4MB flash") + + def check_boot_region_bounds(self, address: int, size: int): + """Validate boot region is within safe bounds (top of flash)""" + # Boot regions should be within the first 64KB + if address + size > 0x10000: + raise FlashError(f"Boot region exceeds 64KB boundary: 0x{address:08X} + 0x{size:X}") + print(f"Boot region valid: 0x{address:08X} to 0x{address + size:08X}") + + # ==================== BOOTROM MODE ENTRY ==================== + + def wait_for_brom_mode(self, timeout: int = 600) -> bool: + """Wait for device to enter MT6261 BootROM mode""" + start_time = time.time() + + print("\n" + "="*60, flush=True) + print("Waiting for MT6261 BootROM Mode", flush=True) + print("="*60, flush=True) + print("1. Turn OFF phone", flush=True) + print("2. Hold BOOT key and connect USB cable now...", flush=True) + + dot_count = 0 + while time.time() - start_time < timeout: + try: + self.send_raw(b'\xA0') + time.sleep(0.005) + if self.serial.in_waiting > 0: + b = self.serial.read(1) + if b and b[0] == 0x5F: + self.send_raw(b'\x0A\x50\x05') + ack3 = self.serial.read(3) + if ack3 == b'\xF5\xAF\xFA': + print("\nConnected to MT6261 BootROM!", flush=True) + self.connected = True + + # Read Chip ID via 0xA2 command + cmd = b'\xA2' + struct.pack('>II', 0x80000008, 1) + self.send_raw(cmd) + echo = self.serial.read(len(cmd)) + resp = self.serial.read(2) + if len(resp) == 2: + self.chip_id = struct.unpack('>H', resp)[0] + print(f"Chip ID: 0x{self.chip_id:04X}", flush=True) + + # Disable watchdogs and enable USB download + self.write_reg16(0xa0030000, 0x2200) # disable system watchdog + self.write_reg16(0xa0700a28, 0x8000) # enable USB download mode + self.write_reg16(0xa0700a24, 2) # disable battery watchdog + self.write_reg32(0xa0510000, 2) # memory map mode 2 + print("BootROM initialized & watchdogs disabled", flush=True) + return True + except (serial.SerialException, OSError): + pass + + dot_count += 1 + if dot_count % 20 == 0: + sys.stdout.write(".") + sys.stdout.flush() + time.sleep(0.01) + + return False + + def write_reg16(self, addr: int, val: int): + cmd = b'\xD2' + struct.pack('>II', addr, 1) + self.send_raw(cmd) + echo = self.serial.read(len(cmd)) + status = self.serial.read(2) + data = struct.pack('>H', val) + self.send_raw(data) + echo2 = self.serial.read(len(data)) + status2 = self.serial.read(2) + + def write_reg32(self, addr: int, val: int): + cmd = b'\xD4' + struct.pack('>II', addr, 1) + self.send_raw(cmd) + echo = self.serial.read(len(cmd)) + status = self.serial.read(2) + data = struct.pack('>I', val) + self.send_raw(data) + echo2 = self.serial.read(len(data)) + status2 = self.serial.read(2) + + def initialize_flash(self): + """Initialize and detect SPI NOR flash""" + print("\nInitializing SPI NOR Flash...") + + # Get flash info from BROM + try: + self.send_brom_command(BROM_CMD_GET_FLASH_INFO) + status, length, data = self.receive_brom_response() + self.check_brom_error(status) + + if len(data) >= 12: + # Parse flash info + brom_flash_size = struct.unpack(' int: + cmd = b'\xD1' + struct.pack('>II', addr, 1) + self.send_raw(cmd) + echo = self.serial.read(len(cmd)) + resp = self.serial.read(8) + if len(resp) >= 8: + return struct.unpack(' bytes: + """Send SPI flash read command via SFI MAC mode (matching DZ09 sfi.c exact sequence)""" + SFI_BASE = 0xA0140000 + SFI_MAC_CTL = SFI_BASE + 0x0000 + SFI_MAC_OUTL = SFI_BASE + 0x0010 + SFI_MAC_INL = SFI_BASE + 0x0014 + SFI_MISC_CTL3 = SFI_BASE + 0x0044 + SFI_GPRAM = SFI_BASE + 0x0800 + + # 1. Write command to GPRAM + self.write_reg32(SFI_GPRAM, cmd_byte) + + # 2. Mask AHB ch2 + self.write_reg32(SFI_MISC_CTL3, self.read_reg32(SFI_MISC_CTL3) | (1 << 9)) + + # 3. Enable MAC mode first + mac_val = (1 << 28) | (1 << 3) + self.write_reg32(SFI_MAC_CTL, mac_val) + + # 4. Set OUTL and INL while MAC is enabled + self.write_reg32(SFI_MAC_OUTL, 1) + self.write_reg32(SFI_MAC_INL, read_len) + + # 5. Trigger operation + self.write_reg32(SFI_MAC_CTL, mac_val | (1 << 2)) + + # 6. Wait for completion + for _ in range(200): + v = self.read_reg32(SFI_MAC_CTL) + if (v & 0x02) and not (v & 0x01): + break + time.sleep(0.001) + + # 7. Read response from GPRAM + total_bytes = 1 + read_len + res_bytes = b'' + for i in range(0, total_bytes + 3, 4): + val = self.read_reg32(SFI_GPRAM + i) + res_bytes += struct.pack(' bytes: + """Read JEDEC ID from SPI NOR flash""" + return self.sfi_mac_cmd_read(0x9F, 3) + + # ==================== SPI NOR FLASH OPERATIONS ==================== + + def read_status_register(self) -> int: + """Read SPI NOR flash status register""" + data = self.sfi_mac_cmd_read(0x05, 1) + return data[0] if len(data) >= 1 else 0 + + def write_status_register(self, value: int): + """Write SPI NOR flash status register""" + self.sfi_mac_cmd_write(0x50) # Write enable for status register + self.sfi_mac_cmd_write(0x01, data=bytes([value & ~(SR_BP0 | SR_BP1 | SR_BP2 | SR_BP3)])) + self.wait_until_ready() + + def wait_until_ready(self, timeout: int = 10): + """Wait until flash is ready (WIP bit cleared)""" + start_time = time.time() + while time.time() - start_time < timeout: + sr = self.read_status_register() + if not (sr & SR_WIP): + return + time.sleep(0.005) + raise FlashError("Timeout waiting for flash to be ready") + + def check_write_protection(self): + """Check and disable write protection""" + sr = self.read_status_register() + print(f"SPI Flash Status Register: 0x{sr:02X}") + if sr & (SR_BP0 | SR_BP1 | SR_BP2 | SR_BP3): + print("Write protection enabled - disabling...") + self.write_status_register(0x00) + sr = self.read_status_register() + print(f"Status Register after unlocking: 0x{sr:02X}") + if sr & (SR_BP0 | SR_BP1 | SR_BP2 | SR_BP3): + raise FlashError("Could not disable write protection") + print("Write protection disabled") + + def enable_write(self): + """Enable write operation (WEL bit)""" + self.sfi_mac_cmd_write(0x06) + + def disable_write(self): + """Disable write operation""" + self.sfi_mac_cmd_write(0x04) + + def erase_sector(self, address: int): + """Erase a sector (4KB) at address""" + self.validate_address_range(address, self.sector_size) + print(f"Erasing sector at 0x{address:08X}...") + self.wait_until_ready() + self.enable_write() + self.sfi_mac_cmd_write(0x20, addr=address) + self.wait_until_ready() + print("Sector erased") + + def erase_block(self, address: int): + """Erase a block (64KB) at address""" + self.validate_address_range(address, self.block_size) + print(f"Erasing block at 0x{address:08X}...") + self.wait_until_ready() + self.enable_write() + self.sfi_mac_cmd_write(0xD8, addr=address) + self.wait_until_ready() + print("Block erased") + + def page_program(self, address: int, data: bytes): + """Program data within a single page (chunked to fit 160-byte SFI_GPRAM)""" + self.validate_address_range(address, len(data)) + + # Max chunk size is 64 bytes payload (command 1 + addr 3 + data 64 = 68 bytes <= 160 SFI_GPRAM) + max_chunk = 64 + offset = 0 + while offset < len(data): + chunk = data[offset:offset+max_chunk] + curr_addr = address + offset + self.wait_until_ready() + self.enable_write() + self.sfi_mac_cmd_write(0x02, addr=curr_addr, data=chunk) + self.wait_until_ready() + offset += len(chunk) + + def read_flash(self, address: int, length: int) -> bytes: + """Read data from flash memory via BROM 0xD1 memory mapped read""" + self.validate_address_range(address, length) + + data = b'' + remaining = length + curr_addr = address + blk_size = 1024 + + while remaining > 0: + rsize = min(remaining, blk_size) + # Send 0xD1 command for sz words + words_cnt = rsize >> 2 + cmd = b'\xD1' + struct.pack('>II', curr_addr, words_cnt) + self.send_raw(cmd) + echo = self.serial.read(len(cmd)) + resp = self.serial.read((words_cnt * 4) + 4) + if len(resp) >= (words_cnt * 4) + 4: + # Unpack words (skip 2-byte header status and 2-byte footer status) + words = struct.unpack('<' + words_cnt * 'I', resp[2:-2]) + chunk_data = struct.pack('>' + words_cnt * 'I', *words) + data += chunk_data + else: + raise FlashError(f"Short read from BROM at 0x{curr_addr:08X}") + + curr_addr += rsize + remaining -= rsize + + progress = (length - remaining) / length * 100 + sys.stdout.write(f"\rReading: {progress:.1f}%") + sys.stdout.flush() + + print("\nRead complete") + return data + + def write_flash(self, address: int, data: bytes): + """Write data to flash memory (handles page boundaries)""" + self.validate_address_range(address, len(data)) + self.check_write_protection() + + print(f"Writing {len(data)} bytes to 0x{address:08X}...") + + offset = 0 + curr_addr = address + remaining = len(data) + + while remaining > 0: + # Calculate page boundary (256 bytes) + page_offset = curr_addr % self.page_size + chunk_size = min(self.page_size - page_offset, remaining) + + # Check if we're at a page boundary (full page write) + if page_offset == 0: + # Check if we have a full page + if chunk_size == self.page_size: + # Direct page program + self.page_program(curr_addr, data[offset:offset + chunk_size]) + else: + # Partial page at end - read-modify-write + page_start = curr_addr + existing_data = self.read_flash(page_start, self.page_size) + new_page = data[offset:offset + chunk_size] + existing_data[len(data[offset:offset + chunk_size]):] + self.page_program(curr_addr, new_page) + else: + # Cross-page boundary - read-modify-write + page_start = curr_addr - page_offset + existing_data = self.read_flash(page_start, self.page_size) + + # Modify existing page data + new_page = bytearray(existing_data) + new_page[page_offset:page_offset + chunk_size] = data[offset:offset + chunk_size] + + # Program modified page + self.page_program(page_start, bytes(new_page)) + + offset += chunk_size + curr_addr += chunk_size + remaining -= chunk_size + + progress = offset / len(data) * 100 + sys.stdout.write(f"\rWriting: {progress:.1f}%") + sys.stdout.flush() + + print("\nWrite complete") + + def verify_flash(self, address: int, data: bytes) -> bool: + """Verify data written to flash""" + self.validate_address_range(address, len(data)) + + print(f"Verifying {len(data)} bytes at 0x{address:08X}...") + + read_data = self.read_flash(address, len(data)) + + if read_data == data: + print("Verification PASSED") + return True + else: + # Find all mismatches + mismatches = [] + for i in range(min(len(data), len(read_data))): + if data[i] != read_data[i]: + mismatches.append((i, data[i], read_data[i])) + if len(mismatches) > 10: + break + + if len(mismatches) > 0: + print(f"Verification FAILED ({len(mismatches)}+ mismatches)") + for offset, expected, got in mismatches[:5]: + print(f" Offset 0x{offset:04X}: Expected 0x{expected:02X}, Got 0x{got:02X}") + else: + if len(data) != len(read_data): + print(f"Length mismatch: expected {len(data)}, got {len(read_data)}") + else: + print("Verification PASSED") + return True + return False + + # ==================== HIGH-LEVEL OPERATIONS ==================== + + def flash_bootloader_region(self, address: int, boot_data: bytes): + """Flash a bootloader region with proper handling""" + # Validate boot region is within safe bounds (top 64KB) + self.check_boot_region_bounds(address, len(boot_data)) + + print(f"\n{'='*60}") + print(f"Flashing boot region at 0x{address:08X}") + print(f"Size: {len(boot_data)} bytes") + print(f"{'='*60}") + + # Check and unlock SPI NOR write protection + self.check_write_protection() + + # Check if we need to erase (if data is not all 0xFF) + if all(b == 0xFF for b in boot_data): + print("Data is all 0xFF - skipping (already erased)") + else: + # Erase the required sectors + start_sector = address & ~(self.sector_size - 1) + end_addr = address + len(boot_data) + curr_sector = start_sector + + while curr_sector < end_addr: + self.erase_sector(curr_sector) + curr_sector += self.sector_size + + # Write data + self.write_flash(address, boot_data) + + # Verify + if not self.verify_flash(address, boot_data): + raise VerificationError(f"Boot region verification failed at 0x{address:08X}") + + print(f"Boot region at 0x{address:08X} flashed successfully") + + def flash_full_dump(self, flash_dump_path: str, verify: bool = True): + """Flash a complete 4MB flash dump""" + print(f"\n{'='*60}") + print("Flashing Full 4MB Flash Dump") + print(f"{'='*60}") + + # Check file exists + if not os.path.exists(flash_dump_path): + raise BROMFlashingError(f"Flash dump file not found: {flash_dump_path}") + + # Check file size + file_size = os.path.getsize(flash_dump_path) + print(f"Flash dump file size: {file_size} bytes") + + if file_size != self.flash_size: + raise BROMFlashingError( + f"Flash dump size mismatch: expected {self.flash_size}, got {file_size}" + ) + + # Read flash dump + with open(flash_dump_path, 'rb') as f: + flash_data = f.read() + + print(f"MD5: {hashlib.md5(flash_data).hexdigest()}") + + # Check if we should do full chip erase or selective + print("\nOptions:") + print("1. Full chip erase (slow but safest)") + print("2. Selective erase (faster but riskier)") + + choice = input("Select option (1 or 2): ").strip() + + if choice == "1": + print("Performing full chip erase...") + self.chip_erase() + # Write entire flash + self.write_flash(0x000000, flash_data) + else: + # Selective erase: only erase regions that need changing + print("Performing selective erase...") + + # Check which sectors need erasing + sectors_to_erase = [] + for sector_start in range(0, self.flash_size, self.sector_size): + sector_data = flash_data[sector_start:sector_start + self.sector_size] + if not all(b == 0xFF for b in sector_data): + # Read current sector + current_data = self.read_flash(sector_start, self.sector_size) + if current_data != sector_data: + sectors_to_erase.append(sector_start) + + print(f"Sectors to erase: {len(sectors_to_erase)}") + + # Erase needed sectors + for sector in sectors_to_erase: + self.erase_sector(sector) + + # Write entire flash + self.write_flash(0x000000, flash_data) + + # Verify if requested + if verify: + print("\nVerifying entire flash...") + read_data = self.read_flash(0x000000, self.flash_size) + + if read_data == flash_data: + print("Full flash verification PASSED") + else: + # Find mismatches + mismatches = sum(1 for i in range(self.flash_size) if read_data[i] != flash_data[i]) + print(f"Full flash verification FAILED ({mismatches} mismatches)") + raise VerificationError("Full flash verification failed") + + print(f"\nFull flash dump written successfully!") + + # ==================== BAUDRATE MANAGEMENT ==================== + + def change_baudrate(self, new_baud: int): + """Change communication baudrate""" + print(f"Changing baudrate to {new_baud}...") + + self.send_brom_command(BROM_CMD_BAUD_CHANGE, struct.pack(' List[Dict]: + """Extract boot regions from a full flash dump""" + print("\n" + "="*60) + print("Extracting Boot Regions from Full Flash Dump") + print("="*60) + + if not os.path.exists(flash_dump_path): + raise BROMFlashingError(f"Flash dump file not found: {flash_dump_path}") + + # Check file size + file_size = os.path.getsize(flash_dump_path) + if file_size != FLASH_SIZE_4MB: + print(f"WARNING: Flash dump is {file_size} bytes, expected {FLASH_SIZE_4MB} for 4MB flash") + + with open(flash_dump_path, 'rb') as f: + flash_data = f.read() + + print(f"Flash dump size: {len(flash_data)} bytes ({len(flash_data) // (1024*1024)} MB)") + os.makedirs(output_dir, exist_ok=True) + + boot_regions = [ + ('boot_region1', BOOTLOADER_BD_ADDRESS, BOOT1_SIZE, 'boot_region1.bin'), + ('boot_region2', BOOTLOADER_EB_ADDRESS, BOOT2_SIZE, 'boot_region2.bin'), + ('boot_region3', BOOTLOADER_REGION3_ADDRESS, BOOT3_SIZE, 'boot_region3.bin') + ] + + extracted_files = [] + + for name, start, size, filename in boot_regions: + if start + size > len(flash_data): + print(f"Warning: {name} extends beyond flash dump") + continue + + region_data = flash_data[start:start+size] + filepath = os.path.join(output_dir, filename) + + with open(filepath, 'wb') as f: + f.write(region_data) + + info = { + 'name': name, + 'address': start, + 'size': size, + 'file': filepath, + 'md5': hashlib.md5(region_data).hexdigest() + } + extracted_files.append(info) + + print(f"\nExtracted {name}:") + print(f" Address: 0x{start:08X}") + print(f" Size: 0x{size:X} ({size} bytes)") + print(f" File: {filepath}") + print(f" MD5: {info['md5']}") + + # Save extraction info + info_file = os.path.join(output_dir, 'extraction_info.json') + with open(info_file, 'w') as f: + json.dump({ + 'flash_dump': flash_dump_path, + 'flash_size': FLASH_SIZE_4MB, + 'extracted_regions': extracted_files + }, f, indent=2) + + print(f"\nExtraction info saved to {info_file}") + return extracted_files + +# ==================== MAIN FUNCTION ==================== + +def main(): + parser = argparse.ArgumentParser(description='MT6261D BROM Direct SPI NOR Flasher - 4MB Flash') + + # Required + parser.add_argument('--port', default='auto', help='Serial port (or auto)') + + # Optional + parser.add_argument('--baud', type=int, default=115200, help='Initial baud rate') + parser.add_argument('--high-baud', type=int, help='High speed baud rate') + + # Operation modes + parser.add_argument('--flash-dump', help='Full 4MB flash dump file') + parser.add_argument('--boot1', help='Bootloader region 1 file') + parser.add_argument('--boot2', help='Bootloader region 2 file') + parser.add_argument('--boot3', help='Bootloader region 3 file') + parser.add_argument('--extract-only', action='store_true', help='Only extract boot regions') + parser.add_argument('--read-backup', help='Read flash data to file') + parser.add_argument('--read-address', type=lambda x: int(x, 0), help='Address to read from') + parser.add_argument('--read-size', type=lambda x: int(x, 0), help='Number of bytes to read') + parser.add_argument('--verify', action='store_true', help='Verify after write') + + # Advanced + parser.add_argument('--chip-erase', action='store_true', help='Erase entire chip') + parser.add_argument('--force-4mb', action='store_true', help='Force 4MB flash size detection') + parser.add_argument('--verbose', action='store_true', help='Enable verbose output') + + args = parser.parse_args() + + # Validate + if not args.flash_dump and not args.boot1 and not args.boot2 and not args.boot3 and \ + not args.read_backup and not args.extract_only and not args.chip_erase: + print("Error: No operation specified") + sys.exit(1) + + port_to_use = args.port + if port_to_use == 'auto': + ports = glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*') + if ports: + port_to_use = ports[0] + print(f"Auto-detected serial port: {port_to_use}", flush=True) + else: + port_to_use = '/dev/ttyUSB0' + print(f"No port active yet, defaulting to {port_to_use} wait loop...", flush=True) + + flasher = None + try: + # Initialize + print(f"Initializing on {port_to_use} at {args.baud} baud...") + flasher = MT6261BROMFlasher(port_to_use, args.baud) + + # Force 4MB if requested + if args.force_4mb: + flasher.flash_size = FLASH_SIZE_4MB + flasher.is_4mb_flash = True + + # Wait for BootROM + if not flasher.wait_for_brom_mode(): + raise BROMFlashingError("Failed to enter BootROM mode") + + # Switch to high baud if requested + if args.high_baud and args.high_baud > args.baud: + flasher.change_baudrate(args.high_baud) + + # Verify flash size + flasher.is_4mb_flash = True + flasher.flash_size = FLASH_SIZE_4MB + + # Print flash info + print(f"\nFlash Information:") + print(f" Size: {flasher.flash_size} bytes ({flasher.flash_size // (1024*1024)} MB)") + print(f" Page Size: 0x{flasher.page_size:X}") + print(f" Sector Size: 0x{flasher.sector_size:X}") + print(f" Block Size: 0x{flasher.block_size:X}") + + # Perform operations + if args.read_backup: + # Read flash data + if args.read_address is None or args.read_size is None: + print("Error: --read-backup requires --read-address and --read-size") + sys.exit(1) + + # Validate address range + flasher.validate_address_range(args.read_address, args.read_size) + + print(f"\nReading 0x{args.read_size:X} bytes from 0x{args.read_address:08X}...") + data = flasher.read_flash(args.read_address, args.read_size) + + with open(args.read_backup, 'wb') as f: + f.write(data) + + print(f"Data saved to {args.read_backup}") + print(f"MD5: {hashlib.md5(data).hexdigest()}") + + elif args.chip_erase: + # Chip erase + flasher.chip_erase() + + elif args.extract_only and args.flash_dump: + # Extract only + extract_boot_regions_from_flash(args.flash_dump) + + elif args.flash_dump: + # Flash full dump + flasher.flash_full_dump(args.flash_dump, verify=args.verify) + + else: + # Flash individual boot files + boot_files = [ + (BOOTLOADER_BD_ADDRESS, args.boot1, 'Boot Region 1'), + (BOOTLOADER_EB_ADDRESS, args.boot2, 'Boot Region 2'), + (BOOTLOADER_REGION3_ADDRESS, args.boot3, 'Boot Region 3') + ] + + for address, filepath, name in boot_files: + if filepath and os.path.exists(filepath): + print(f"\nFlashing {name}...") + with open(filepath, 'rb') as f: + data = f.read() + + flasher.flash_bootloader_region(address, data) + elif filepath: + print(f"Error: File not found: {filepath}") + sys.exit(1) + + print("\n" + "="*60) + print("Operation completed successfully!") + print("="*60) + + except (BROMFlashingError, FlashError, SPIError, VerificationError, UnsupportedFlashError) as e: + print(f"\nError: {e}") + sys.exit(1) + except serial.SerialException as e: + print(f"\nSerial error: {e}") + sys.exit(1) + except KeyboardInterrupt: + print("\nCancelled by user") + sys.exit(1) + finally: + if flasher: + flasher.close() + print("Serial connection closed") + +if __name__ == "__main__": + main()