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:
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;