From 395c77920e6b601d067aa2d9506ab7c55c6236e3 Mon Sep 17 00:00:00 2001 From: Compot Developer Date: Wed, 4 Nov 2026 11:30:16 +0000 Subject: [PATCH 1/2] SPI NOR: -uu clears QE as well -u keeps QE (quad enable), which protects nothing. -uu clears the protection and then QE: bit 1 of status register 2, with WRSR2 or the two-byte WRSR for the chips that do not know it, bit 6 of status register 1 on Macronix chips. A QE that stays set is told. Command line only. Version 1.21.0. [skip ci] --- CHANGELOG.md | 8 +++++ README.md | 8 ++++- README.ru.md | 8 ++++- README.zh-CN.md | 7 +++- src/flashcmd_api.c | 1 + src/flashcmd_api.h | 1 + src/main.c | 16 ++++++--- src/snorcmd_api.h | 1 + src/spi_nor_flash.c | 85 +++++++++++++++++++++++++++++++++++++++++++++ tests/norsim.c | 48 ++++++++++++++++++++++--- tests/run.sh | 8 +++++ tests/stub.c | 3 +- 12 files changed, 182 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5969a54..c950f9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ The version lives in `_VER` in `src/main.c`. Binaries are published only as the [`latest`](https://github.com/OpenIPC/microsnander/releases/latest) release, rebuilt from every push to `main`; this file is the change history. +## 1.21.0 — 2026-11-04 + +- SPI NOR: `-uu` clears the protection as `-u` does, and then QE (quad enable), which `-u` + keeps: bit 1 of status register 2, also with the two-byte WRSR for the chips that do not + know WRSR2, bit 6 of status register 1 on Macronix chips. A QE that stays set, as on the + parts where the factory fixes it, is told, and a chip without the bit has nothing to + clear. A board that reads the chip in quad mode may not boot without QE. Command line only. + ## 1.20.2 — 2026-11-04 - SPI NAND: a USB transfer that fails now fails the operation. The result of a read from the diff --git a/README.md b/README.md index 643e183..905c95d 100644 --- a/README.md +++ b/README.md @@ -151,7 +151,8 @@ microsnander [options] -a
manually set address -w write chip with data from filename -r read chip and save data to filename - -u unprotect chip + -u unprotect chip; -uu: also clear QE (quad enable), a chip of a + board that boots in quad mode may then not boot -t self-test: protection, then erase, write and read back one block (the first; -a for another) and put its data back -v verify after write on chip @@ -178,6 +179,11 @@ chip end): give `-l` the size of the partition, and a write that needs more room than that fails. A read that would need a page past the chip end fails. A page that failed to program is remembered only for `-v` in the same run: a later `-k` read may read it as data, and the dump is shifted from there on. +`-u` clears the protection bits and keeps QE (quad enable), which protects nothing. `-uu` +clears QE as well, SPI NOR on the command line only. QE turns the WP# and HOLD# pins into data +lines, and a board that reads the chip in quad mode may not boot without it. Some chips have +it fixed at the factory: then `-uu` says that it stays. + `-t` is a quick check of the chip, with nothing to prepare: it reads the protection bits (SPI NOR: BP, TB, CMP, SRP in the status registers; SPI NAND: the block lock register), then takes one block — the first by default, where a locked loader usually is, or the one at `-a` — diff --git a/README.ru.md b/README.ru.md index fbca287..b034f08 100644 --- a/README.ru.md +++ b/README.ru.md @@ -152,7 +152,8 @@ microsnander [options] -a
manually set address -w write chip with data from filename -r read chip and save data to filename - -u unprotect chip + -u unprotect chip; -uu: also clear QE (quad enable), a chip of a + board that boots in quad mode may then not boot -t self-test: protection, then erase, write and read back one block (the first; -a for another) and put its data back -v verify after write on chip @@ -181,6 +182,11 @@ microsnander [options] записалась, запоминается только для `-v` в том же запуске: позднее чтение с `-k` может прочитать её как данные, и дамп с этого места сдвинется. +`-u` снимает биты защиты и оставляет QE (quad enable): он ничего не защищает. `-uu` снимает +ещё и QE, только для SPI NOR и только в командной строке. QE превращает ножки WP# и HOLD# в линии +данных, и плата, читающая микросхему в режиме Quad, без него может не загрузиться. У некоторых +микросхем он задан на заводе: тогда `-uu` сообщит, что бит остался. + `-t` — быстрая проверка микросхемы без подготовки: читает биты защиты (SPI NOR — BP, TB, CMP, SRP в регистрах состояния; SPI NAND — регистр блокировки блоков), затем берёт один блок — по умолчанию первый, где обычно лежит защищённый загрузчик, или блок по `-a`, — запоминает его diff --git a/README.zh-CN.md b/README.zh-CN.md index e7c5073..51a8c19 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -138,7 +138,8 @@ microsnander [options] -a
manually set address -w write chip with data from filename -r read chip and save data to filename - -u unprotect chip + -u unprotect chip; -uu: also clear QE (quad enable), a chip of a + board that boots in quad mode may then not boot -t self-test: protection, then erase, write and read back one block (the first; -a for another) and put its data back -v verify after write on chip @@ -163,6 +164,10 @@ SPI NAND 芯片默认使用片内 ECC 读写:文件中只有数据页。使用 写入失败的页只在同一次运行的 `-v` 中被记住:之后用 `-k` 读取时可能把它当作数据读出, 转储从该页起会错位。 +`-u` 清除保护位并保留 QE(quad enable),QE 不起保护作用。`-uu` 还会清除 QE,仅限 SPI NOR +和命令行。QE 把 WP# 和 HOLD# 引脚变为数据线,以四线模式读取芯片的电路板没有它可能无法启动。 +部分芯片的 QE 在出厂时已固定:此时 `-uu` 会提示该位未被清除。 + `-t` 是无需准备的芯片快速检查:读取保护位(SPI NOR:状态寄存器中的 BP、TB、CMP、SRP;SPI NAND: 块锁定寄存器),然后取一个块(默认第一个块,通常是被锁定的引导程序所在处,或 `-a` 指定的块),保存其数据, 擦除,写入测试图案,读回,再写回原数据。结果为一行:`OK`(退出码 0)、`PROTECTED`(退出码 2)或 `FAILED` diff --git a/src/flashcmd_api.c b/src/flashcmd_api.c index f1ec0ed..54196ee 100644 --- a/src/flashcmd_api.c +++ b/src/flashcmd_api.c @@ -51,6 +51,7 @@ long flash_cmd_init(struct flash_cmd *cmd) } else if ((flen = snor_init()) > 0) { cmd->kind = FLASH_NOR; cmd->flash_unprotect = snor_unprotect; + cmd->flash_clear_qe = snor_clear_qe; cmd->flash_erase = snor_erase; cmd->flash_write = snor_write; cmd->flash_read = snor_read; diff --git a/src/flashcmd_api.h b/src/flashcmd_api.h index f6fba9d..cc1ca58 100644 --- a/src/flashcmd_api.h +++ b/src/flashcmd_api.h @@ -23,6 +23,7 @@ struct flash_cmd { int (*flash_erase)(unsigned long offs, unsigned long len); int (*flash_write)(unsigned char *buf, unsigned long to, unsigned long len); int (*flash_unprotect)(void); + int (*flash_clear_qe)(void); /* -uu: the unprotect and QE; set together with flash_unprotect */ int kind; /* FLASH_NOR, FLASH_NAND, FLASH_FIRMWARE (the EZP runs the chip itself) */ int lock; /* SPI NAND: its block lock register (A0) when found, -1: not known */ }; diff --git a/src/main.c b/src/main.c index 4f164b7..ff599f2 100644 --- a/src/main.c +++ b/src/main.c @@ -36,7 +36,7 @@ struct flash_cmd prog; extern unsigned int bsize; -#define _VER "1.20.2" +#define _VER "1.21.0" const char *ms_version = _VER; @@ -76,7 +76,8 @@ void usage(int code) " -a
manually set address\n"\ " -w write chip with data from filename\n"\ " -r read chip and save data to filename\n"\ - " -u unprotect chip\n"\ + " -u unprotect chip; -uu: also clear QE (quad enable), a chip of a\n"\ + " board that boots in quad mode may then not boot\n"\ " -t self-test: protection, then erase, write and read back one block\n"\ " (the first; -a for another) and put its data back\n"\ " -v verify after write on chip\n"\ @@ -95,6 +96,7 @@ int main(int argc, char* argv[]) char *fname = NULL, op = 0, *web = NULL, *web_key = NULL, *web_dir = NULL; unsigned char *buf; int long long len = 0, addr = 0, flen = 0, wlen = 0; + int unlock = 0; /* how many -u: 2 also clears QE */ FILE *fp; title(); @@ -146,9 +148,15 @@ int main(int argc, char* argv[]) case 'o': web_dir = optarg; break; + case 'u': + /* -u and -uu, nothing more and with no other operation */ + if (++unlock > 2 || (op && op != 'u')) + op = 'x'; + else + op = 'u'; + break; case 'i': case 'e': - case 'u': case 't': if(!op) op = c; @@ -253,7 +261,7 @@ int main(int argc, char* argv[]) printf("Unprotect is not supported for this chip type or programmer (SPI NOR through CH341A or CH347 only).\n"); goto out; } - ret = prog.flash_unprotect(); + ret = unlock > 1 ? prog.flash_clear_qe() : prog.flash_unprotect(); if(!ret) { printf("Status: OK\n"); rc = 0; diff --git a/src/snorcmd_api.h b/src/snorcmd_api.h index 9af4e29..263c49c 100644 --- a/src/snorcmd_api.h +++ b/src/snorcmd_api.h @@ -20,6 +20,7 @@ int snor_erase(unsigned long offs, unsigned long len); int snor_write(unsigned char *buf, unsigned long to, unsigned long len); int snor_block_locks(void); /* 1: the chip locks its blocks at power-up (SST26), its own way */ int snor_unprotect(void); /* 0; -1: the chip does not answer; -2: a register stays (snor_unprotect_why) */ +int snor_clear_qe(void); /* -uu: snor_unprotect(), then QE; the same results */ extern char snor_unprotect_why[200]; int snor_read_status(unsigned char sr[3]); extern const char *flash_chip_name; /* name of the chip found, for file names */ diff --git a/src/spi_nor_flash.c b/src/spi_nor_flash.c index a23f5c8..537989c 100644 --- a/src/spi_nor_flash.c +++ b/src/spi_nor_flash.c @@ -284,6 +284,91 @@ static int unprotect(int all) int snor_unprotect(void) { return unprotect(1); } +/* The write of registers 1 and 2 with one command: the only way for the chips + that do not know WRSR2, as the older Winbond parts. */ +static int snor_write_sr12(u8 sr1, u8 sr2) +{ + u8 d[2] = { sr1, sr2 }; + int ret; + + if (snor_wait_ready(1)) + return -1; + snor_write_enable(); + SPI_CONTROLLER_Chip_Select_Low(); + SPI_CONTROLLER_Write_One_Byte(OPCODE_WRSR); + ret = SPI_CONTROLLER_Write_NByte(d, 2, SPI_CONTROLLER_SPEED_SINGLE); + if (SPI_CONTROLLER_Chip_Select_High()) + ret = -1; + if (snor_wait_busy()) + ret = -1; + snor_write_disable(); + return ret ? -1 : 0; +} + +/* the write of a status register that clears QE, and the read back; 1: it stays */ +static int snor_clear_qe_bit(u8 wr, u8 rd, u8 *sr, u8 qe) +{ + *sr &= ~qe; + if (snor_unprotect_write(wr, sr) || snor_read_sr(rd, sr) < 0) + return -1; + return (*sr & qe) != 0; +} + +/* + * -uu: the protection as -u, then QE. It protects nothing, but with it set the + * WP# and HOLD# pins are data lines. QE is bit 6 of status register 1 on + * Macronix chips and bit 1 of status register 2 on the others; a chip without + * that register or bit has nothing to clear. Returns as snor_unprotect(). + */ +int snor_clear_qe(void) +{ + const int macronix = spi_chip_info->id == 0xc2; + const u8 rd = macronix ? OPCODE_RDSR : OPCODE_RDSR2; + const u8 wr = macronix ? OPCODE_WRSR : OPCODE_WRSR2; + const u8 qe = macronix ? 0x40 : SR2_QE; + u8 sr = 0; + int ret = unprotect(1); + + if (ret) + return ret; + if (snor_is_sst25_aai() || snor_is_sst26()) { + printf("QE: this chip has no QE bit in its status registers.\n"); + return 0; + } + if (snor_read_sr(rd, &sr) < 0) { + printf("%s: read_sr fail: %x\n", __func__, sr); + return -1; + } + if (!macronix && sr == 0xff) { /* no status register 2, as in unprotect() */ + printf("QE: this chip has no status register 2.\n"); + return 0; + } + if (!(sr & qe)) { + printf("QE: already clear.\n"); + return 0; + } + ret = snor_clear_qe_bit(wr, rd, &sr, qe); + if (ret < 0) + return -1; + if (ret && !macronix) { + u8 sr1 = 0; + + if (snor_read_sr(OPCODE_RDSR, &sr1) < 0 || snor_write_sr12(sr1, sr & ~qe) || + snor_read_sr(rd, &sr) < 0) + return -1; + ret = (sr & qe) != 0; + } + if (ret) { + snprintf(snor_unprotect_why, sizeof(snor_unprotect_why), + "QE stays set (status register 0x%02x): the chip does not take the write, " + "or its QE is fixed at the factory", sr); + printf("Unprotect: %s.\n", snor_unprotect_why); + return -2; + } + printf("QE: cleared. A board that reads this chip in quad mode may not boot until QE is set again.\n"); + return 0; +} + /* * Read the status register until the write in progress bit is clear: the * chip is ready. The other bits (block protection, write enable latch, quad diff --git a/tests/norsim.c b/tests/norsim.c index 8a8a393..110ef97 100644 --- a/tests/norsim.c +++ b/tests/norsim.c @@ -32,7 +32,7 @@ static int fast_clock(clockid_t id, struct timespec *t) unsigned int bsize; const char *flash_chip_name; -enum { ORDINARY, SST25, SST26, NO_SR23, QE_SR1, QE_FIXED }; +enum { ORDINARY, SST25, SST26, NO_SR23, QE_SR1, QE_FIXED, NO_WRSR2 }; static int kind; static unsigned char id[5]; static unsigned char mem[16 << 20]; @@ -95,14 +95,22 @@ SPI_CONTROLLER_RTN_T SPI_CONTROLLER_Chip_Select_High(void) switch (cmd[0]) { case OPCODE_WREN: if (!busy) wel = 1; break; /* ignored while the chip is busy */ case OPCODE_WRDI: wel = 0; aai = 0; break; - case OPCODE_WRSR: if (wel && ncmd > 1 && !(wp_low && (sr & 0x80))) { sr = (cmd[1] & 0xbc) | (kind == QE_SR1 ? 0x40 : 0); busy = 3; } wel = 0; break; + case OPCODE_WRSR: + if (wel && ncmd > 1 && !(wp_low && (sr & 0x80))) { + sr = (cmd[1] & (kind == QE_SR1 ? 0xfc : 0xbc)); + if (ncmd > 2 && kind != NO_SR23) /* the second byte goes to register 2 */ + sr2 = (cmd[2] & 0x43) | (kind == QE_FIXED ? 0x02 : 0); + busy = 3; + } + wel = 0; + break; case OPCODE_WRSR2: - if (kind != NO_SR23 && wel && ncmd > 1) { + if (kind != NO_SR23 && kind != NO_WRSR2 && wel && ncmd > 1) { sr2 = (cmd[1] & 0x43) | (kind == QE_FIXED ? 0x02 : 0); /* QE stays 1 on a QE-fixed part */ sr2_writes++; busy = 3; } - if (kind != NO_SR23) wel = 0; /* a chip without the register ignores the command */ + if (kind != NO_SR23 && kind != NO_WRSR2) wel = 0; /* a chip without the register or the command ignores it */ break; case OPCODE_ULBPR: if (wel && kind == SST26) { locked = 0; busy = 3; } wel = 0; break; case OPCODE_PP: @@ -280,6 +288,38 @@ int main(void) check(snor_erase(0, 65536) == 0 && mem[0] == 0xff, " and the chip erases"); } + /* -uu: QE too, where each kind of chip keeps it */ + { + chip(find("W25Q32BV"), ORDINARY); + snor_init(); + sr = 0x1c; sr2 = 0x42; + check(snor_clear_qe() == 0 && sr == 0 && sr2 == 0 && !wel, "W25Q32BV: -uu clears the protection and QE"); + sr2_writes = 0; + check(snor_clear_qe() == 0 && sr2_writes == 0, " QE already clear: nothing is written"); + chip(find("W25Q128BV"), QE_FIXED); + snor_init(); + sr = 0; sr2 = 0x02; + check(snor_clear_qe() == -2 && sr2 == 0x02 && strstr(snor_unprotect_why, "QE stays") && !wel, + "W25Q128BV with QE fixed at 1: -uu says it stays"); + chip(find("W25Q32BV"), NO_WRSR2); + snor_init(); + sr = 0; sr2 = 0x02; + check(snor_clear_qe() == 0 && sr2 == 0 && sr == 0 && !wel, "a chip that does not know WRSR2: QE goes with the two-byte WRSR"); + chip(find("XM25QH64A"), NO_SR23); + snor_init(); + sr = 0; sr2 = 0; + check(snor_clear_qe() == 0 && !wel, "XM25QH64A without status register 2: nothing to clear, no write enable left"); + chip(find("MX25L3205D"), QE_SR1); + snor_init(); + sr = 0x40; sr2 = 0x02; + check(snor_clear_qe() == 0 && sr == 0 && sr2 == 0x02, "MX25L3205D: QE is in status register 1, register 2 is not touched"); + chip(find("W25Q32BV"), ORDINARY); + snor_init(); + sr = 0x80; sr2 = 0x02; wp_low = 1; + check(snor_clear_qe() == -2 && sr2 == 0x02, "a protection that stays stops -uu before QE"); + wp_low = 0; sr = 0; + } + /* SRP0 alone, no BP: unprotect clears it too (it used to be left, and "cleared" said); with WP# low the register stays, and that is told */ { diff --git a/tests/run.sh b/tests/run.sh index 2812745..940aa26 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -94,6 +94,14 @@ t 1 "-i chip not found" MS_CHIP=none $B -i t 0 "-u NOR" $B -u t 1 "-u NOR fails" MS_FAIL=unprotect $B -u t 1 "-u NAND: not supported, no crash" MS_CHIP=nand $B -u +t 0 "-uu NOR" $B -uu +t 0 "-u -u NOR" $B -u -u +t 1 "-uu NOR fails" MS_FAIL=qe $B -uu +t 1 "-uu NAND: not supported, no crash" MS_CHIP=nand $B -uu +t 1 "-uuu is an error" $B -uuu +t 1 "conflict -uu -i" $B -uu -i +$B -u | grep -q 'clear QE' && { echo "FAIL -u clears QE"; fail=1; } || echo "ok -u leaves QE" +$B -uu | grep -q 'clear QE' && echo "ok -uu clears QE" || { echo "FAIL -uu without QE"; fail=1; } t 0 "-e full" $B -e t 1 "-e fails" MS_FAIL=erase $B -e t 1 "-e len not block multiple" $B -e -a 0 -l 100 diff --git a/tests/stub.c b/tests/stub.c index 307d94b..a8dd172 100644 --- a/tests/stub.c +++ b/tests/stub.c @@ -13,6 +13,7 @@ static int r(unsigned char *b, unsigned long f, unsigned long l) { if (!strcmp(e static int w(unsigned char *b, unsigned long t, unsigned long l) { if (*env("MS_PROTECT")) return l; if (!strcmp(env("MS_FAIL"),"write")) return -1; if (!strcmp(env("MS_FAIL"),"partial")) { memcpy(chip + t, b, l / 2); return l / 2; } memcpy(chip + t, b, l); if (*env("MS_CORRUPT")) chip[t + atol(env("MS_CORRUPT"))] ^= 0x5a; return l; } static int e(unsigned long o, unsigned long l) { if (*env("MS_PROTECT")) return 0; if (!strcmp(env("MS_FAIL"),"erase")) return -2; memset(chip + o, 0xff, l); return 0; } static int u(void) { return strcmp(env("MS_FAIL"),"unprotect") ? 0 : -3; } +static int q(void) { printf("clear QE\n"); return strcmp(env("MS_FAIL"),"qe") ? 0 : -2; } #include #include "programmer.h" static const struct programmer stubprogs[] = { { .key = "ch341a", .name = "CH341A", .send = (void *)1 }, { .key = "ch347", .name = "CH347", .send = (void *)1, .clock = true }, { .key = "ezp", .name = "EZP" } }; @@ -33,7 +34,7 @@ void support_flash_list(void) {} long flash_cmd_init(struct flash_cmd *c) { if (!strcmp(env("MS_CHIP"), "none")) return -1; c->flash_read = r; c->flash_write = w; c->flash_erase = e; - if (strcmp(env("MS_CHIP"), "nand")) c->flash_unprotect = u; + if (strcmp(env("MS_CHIP"), "nand")) { c->flash_unprotect = u; c->flash_clear_qe = q; } c->kind = !strcmp(env("MS_CHIP"), "nand") ? FLASH_NAND : FLASH_NOR; c->lock = *env("MS_LOCK") ? (int)strtol(env("MS_LOCK"), NULL, 16) : -1; if (programmer && !strcmp(programmer->key, "ezp")) c->kind = FLASH_FIRMWARE; From 526b1cecde23426b95d8d3e0a9cfd87632742cf6 Mon Sep 17 00:00:00 2001 From: Compot Developer Date: Wed, 4 Nov 2026 11:48:41 +0000 Subject: [PATCH 2/2] CI: builds on pushes to main only A push to the branch of a pull request started the Build workflow twice, on push and on pull_request, and every run takes GitHub's minutes. Pull requests and workflow_dispatch still build any branch, and main still publishes. --- .github/workflows/build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7aa7fb5..e424eec 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,8 +18,10 @@ name: Build +# Pushes to main only: a branch with a pull request would build twice. on: push: + branches: [main] pull_request: workflow_dispatch: