Adding a New SPI Flash to openFPGALoader¶
Overview¶
openFPGALoader maintains a static flash database in src/spiFlashdb.hpp. Each entry maps a
JEDEC ID (the key) to a flash_t struct describing the chip's capabilities. This guide walks
you through adding an unknown flash chip by hand.
Step 1: Discover the JEDEC ID¶
Run openFPGALoader on your board. If the flash is not in the database, the tool prints:
Read ID failed: SPI RDID raw bytes: XX XX XX XX -> 0xXXXXXXXX
Manufacturer byte: 0xXX (name)
Memory type byte: 0xXX
Memory capacity byte: 0xXX
Starter spiFlashdb.hpp entry:
{0xXXXXXX, {
...
}},
The tool already emits a skeleton entry. You still need to fill in the fields from the datasheet (Step 3), but this template is a great starting point.
How the ID is built¶
The code issues the RDID command 0x9F and reads 4 bytes:
_jedec_id = 0;
for (int i = 0; i < 4; i++)
_jedec_id = (_jedec_id << 8) | rx[i];
jedec24 = _jedec_id >> 8; // drops the 4th byte
Lookup key = jedec24 (24-bit for most chips). Some chips return 4 significant bytes, in
which case the full 32-bit value is the key (e.g. 0x4000190c, 0x05059093).
To decide which key to use:
- Most SPI NOR flashes: use the first 3 response bytes (e.g. BA 21 19 → 0xBA2119).
- If the 4th byte is non-zero and meaningful per the datasheet, use all 4 bytes
(e.g. 40 00 19 0C → 0x4000190c).
You can add both keys pointing to the same struct so the chip is recognised regardless of which response variant your bridge returns.
Manufacturer codes (cheat sheet)¶
| Manufacturer ID | Name |
|---|---|
| 0xC2 | Macronix |
| 0xEF | Winbond |
| 0xBF | SST / Esma |
| 0x1C | Micron/N25Q |
| 0xBA | Micron/Numonyx |
| 0x20 | Micron (1.8V) |
| 0x01 | Spansion/Infineon |
| 0x05 | Xilinx |
| 0x7F | Cypress/Spansion |
| 0x4000 | Numonyx/Micron (4-byte sig) |
Step 2: Read the Datasheet¶
Look up the exact part number on the chip label. Get the datasheet and note:
- Total capacity (Mbit or MB) — needed to compute
nr_sector - Erase commands supported — 64 KB block erase (
0xD8), 4 KB subsector erase (0x20) - Status register layout — where BP (Block Protect) and TB (Top/Bottom) bits live
- Quad Enable (QE) bit — which register and which bit enables quad I/O
- Whether BP/TB bits are OTP (one-time programmable, cannot be changed after programming)
- Extended address register — chips >= 16 MB need a 3-byte address register (
0xB7)
Step 3: Fill in the flash_t struct¶
Open src/spiFlashdb.hpp. The struct definition:
typedef struct {
std::string manufacturer; /**< manufacturer name */
std::string model; /**< chip name */
uint32_t nr_sector; /**< number of 64 KiB sectors */
bool sector_erase; /**< 64 KB erase support */
bool subsector_erase; /**< 4 KB erase support */
bool has_extended; /**< Extended Address Register (0xB7) */
bool tb_otp; /**< TOP/BOTTOM is One-Time Programmable */
uint16_t tb_offset; /**< TOP/BOTTOM bit mask */
tb_loc_t tb_register; /**< register where BP/TB bits live */
uint8_t bp_len; /**< number of BP bits (0-4) */
uint8_t bp_offset[4]; /**< BP[0..3] bit masks */
tb_loc_t quad_register; /**< register containing QE bit */
uint16_t quad_mask; /**< QE bit mask */
bool global_lock; /**< global lock/unlock bit present */
} flash_t;
Field-by-field reference¶
| Field | How to determine | Typical values |
|---|---|---|
manufacturer |
From datasheet / JEDEC ID | "Micron", "Winbond", "Spansion", "Macronix", "ST" |
model |
Exact part number | "MT25QL256ABA", "W25Q256" |
nr_sector |
Capacity / 65536 (64 KiB) | 8 (512Kbit), 128 (8Mbit), 256 (16Mbit), 512 (256Mbit) |
sector_erase |
Does the chip support 64 KB erase (0xD8/0xC7)? |
true (almost always) |
subsector_erase |
Does the chip support 4 KB block erase (0x20)? |
true for most modern chips; false for some Spansion parts |
has_extended |
Does the chip need Extended Address Register (0xB7)? Required for chips >= 16 MB. |
true for 16 Mbit+ that use the 3-byte address mode; false otherwise |
tb_otp |
Is the TB bit OTP (one-time programmable)? | true for Spansion S25FL256S/512S/128S; false for most others |
tb_offset |
Bit position of TB bit in the register, as (1 << N) |
(1 << 5) (most common), (1 << 6), (1 << 14) |
tb_register |
Which register holds BP/TB | STATR, FUNCR, CONFR, NVCONFR (see register table below) |
bp_len |
Number of BP bits (1-4) | 3 or 4 |
bp_offset |
Bit masks for BP0-BP3 as (1 << N) |
{(1<<2), (1<<3), (1<<4), 0} (3 BP bits) or {(1<<2), (1<<3), (1<<4), (1<<5)} (4 BP bits) |
quad_register |
Which register holds the QE bit | STATR, CONFR, or NONER if the chip is quad-only |
quad_mask |
Bit mask for QE bit | (1 << 6) (most common), or 0 if quad-only (NONER) |
global_lock |
Global lock/unlock (GL) bit in status register | false for most; true for some ISSI / low-density chips |
Register enum values (tb_register and quad_register)¶
| Value | Constant | Meaning |
|---|---|---|
| 0 | STATR |
Status Register (read 0x05) |
| 1 | FUNCR |
Function Register |
| 2 | CONFR |
Configuration Register |
| 3 | NVCONFR |
Non-Volatile Configuration Register |
| 99 | NONER |
None — not applicable (use for quad-only chips) |
Calculating nr_sector¶
nr_sector = (capacity in Mbit × 131072) / 65536
= capacity_in_Mbit × 2
| Capacity | nr_sector |
|---|---|
| 512 Kbit | 8 |
| 1 Mbit | 16 |
| 2 Mbit | 32 |
| 4 Mbit | 64 |
| 8 Mbit | 128 |
| 16 Mbit | 256 |
| 32 Mbit | 512 |
| 64 Mbit | 1024 |
| 128 Mbit | 2048 |
| 256 Mbit | 4096 |
| 512 Mbit | 8192 |
Step 4: Add the entry to spiFlashdb.hpp¶
Insert the new entry into flash_list (a std::map<uint32_t, flash_t>) at an appropriate
position (sorted by JEDEC ID is conventional but not strictly required).
Complete example: Micron MT25QL256ABA¶
This chip responds to 0x9F with BA 21 19 (3-byte JEDEC), giving key 0xBA2119.
It also has a 4-byte electronic signature 40 00 19 0C (from command 0xAB),
giving key 0x4000190c. Both keys should point to the same struct.
// 4-byte electronic signature variant (cmd 0xAB: 40 00 19 0C)
{0x4000190c, {
/* Micron MT25QL256ABA1EW9 - 256Mb Serial NOR Flash, 3.0V.
* Electronic Signature (0xAB): 40 00 19 0C
* https://media-www.micron.com/...mt25q_qlhs_u_256_aba_0.pdf */
.manufacturer = "Micron",
.model = "MT25QL256ABA",
.nr_sector = 512,
.sector_erase = true,
.subsector_erase = true,
.has_extended = false,
.tb_otp = false,
.tb_offset = (1 << 5),
.tb_register = STATR,
.bp_len = 3,
.bp_offset = {(1 << 2), (1 << 3), (1 << 4), 0},
.quad_register = NONER, // MT25QL is quad-only, no QE bit needed
.quad_mask = 0,
.global_lock = false,
}},
// 3-byte JEDEC RDID variant (cmd 0x9F: BA 21 19)
{0xba2119, {
/* Micron MT25QL256ABA1EW9 - 256Mb Serial NOR Flash, 3.0V.
* RDID (0x9F): BA 21 19
* Manufacturer: 0xBA (Numonyx/Micron)
* Memory type: 0x21 (Serial NOR)
* Capacity: 0x19 (256Mb / 32MB)
* https://media-www.micron.com/...mt25q_qlhs_u_256_aba_0.pdf */
.manufacturer = "Micron",
.model = "MT25QL256ABA",
.nr_sector = 512,
.sector_erase = true,
.subsector_erase = true,
.has_extended = false,
.tb_otp = false,
.tb_offset = (1 << 5),
.tb_register = STATR,
.bp_len = 3,
.bp_offset = {(1 << 2), (1 << 3), (1 << 4), 0},
.quad_register = NONER,
.quad_mask = 0,
.global_lock = false,
}},
Step 5: Build and test¶
cmake -B build
cmake --build build -j
Run the tool against your board. It should now detect the flash:
Detected: Micron MT25QL256ABA 512 sectors size: 32Mb
If it still shows "unknown", double-check:
- The JEDEC ID key matches what spiFlash.cpp actually reads from the 0x9F command.
- The entry is inside the flash_list map braces (no missing comma from the previous entry).
Quick-reference: common BP/TB patterns¶
Pattern A — BP bits in Status Register (most common)¶
.tb_offset = (1 << 5),
.tb_register = STATR,
.bp_len = 3,
.bp_offset = {(1 << 2), (1 << 3), (1 << 4), 0},
.quad_register = STATR,
.quad_mask = (1 << 6),
Pattern B — Quad-only chip, no QE bit needed¶
.tb_offset = (1 << 5),
.tb_register = STATR,
.bp_len = 3,
.bp_offset = {(1 << 2), (1 << 3), (1 << 4), 0},
.quad_register = NONER,
.quad_mask = 0,
Pattern C — 4 BP bits, BP3 in bit 6¶
.tb_offset = (1 << 5),
.tb_register = STATR,
.bp_len = 4,
.bp_offset = {(1 << 2), (1 << 3), (1 << 4), (1 << 6)},
.quad_register = NONER,
.quad_mask = 0,
Pattern D — BP/TB in Configuration Register (Spansion)¶
.tb_offset = (1 << 5),
.tb_register = CONFR,
.bp_len = 3,
.bp_offset = {(1 << 2), (1 << 3), (1 << 4), 0},
.quad_register = CONFR,
.quad_mask = (1 << 1),
Troubleshooting¶
Flash still unknown after adding entry:
- Check the raw bytes printed by the tool. The key must match (rx[0] << 16) | (rx[1] << 8) | rx[2]
for 3-byte IDs.
- If rx[0] is 0x40 or 0x05, the chip reports a 4-byte ID — use the full 32-bit value.
Erase/write fails but flash is detected:
- nr_sector is wrong — verify capacity from datasheet.
- has_extended should be true for chips >= 16 MB that use the Extended Address Register
(command 0xB7 to enable 3-byte addressing).
Protection bits don't match expected:
- tb_register might be wrong — try CONFR instead of STATR.
- bp_offset bit positions vary by manufacturer; check the register map in the datasheet.