Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,8 @@ microsnander [options]
-a <address> manually set address
-w <filename> write chip with data from filename
-r <filename> 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
Expand All @@ -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` —
Expand Down
8 changes: 7 additions & 1 deletion README.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,8 @@ microsnander [options]
-a <address> manually set address
-w <filename> write chip with data from filename
-r <filename> 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
Expand Down Expand Up @@ -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`, — запоминает его
Expand Down
7 changes: 6 additions & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,8 @@ microsnander [options]
-a <address> manually set address
-w <filename> write chip with data from filename
-r <filename> 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
Expand All @@ -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`
Expand Down
1 change: 1 addition & 0 deletions src/flashcmd_api.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/flashcmd_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
};
Expand Down
16 changes: 12 additions & 4 deletions src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -76,7 +76,8 @@ void usage(int code)
" -a <address> manually set address\n"\
" -w <filename> write chip with data from filename\n"\
" -r <filename> 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"\
Expand All @@ -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();
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/snorcmd_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
85 changes: 85 additions & 0 deletions src/spi_nor_flash.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
48 changes: 44 additions & 4 deletions tests/norsim.c
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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 */
{
Expand Down
8 changes: 8 additions & 0 deletions tests/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion tests/stub.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 <strings.h>
#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" } };
Expand All @@ -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;
Expand Down
Loading