An Arduino-based parallel EEPROM programmer for the AT28Cxx chip family (AT28C04, AT28C16, AT28C64, AT28C256) with a Python CLI. Validated on Arduino MEGA and DUE. Useful for Ben Eater's breadboard computer projects and similar builds.
Caution
During read and standalone verify operations, the chip's !WE pin MUST be connected to VCC using a jumper wire to prevent data corruption from Arduino's reset behavior. The --write command includes in-session verify and does not require the jumper. Details
Important
This project concerns external EEPROM chips, not the built-in Arduino EEPROM memory.
The firmware is flashed onto an Arduino MEGA or DUE. The EEPROM chip is connected to the extended pin header (pins 22-53), and the Python CLI communicates over a binary serial protocol to erase, write, read, and verify binary data. The CLI interface is modeled after minipro (XGecu programmer).
A companion blog series documents the development: EEPROM Programmer posts on goose.sh.
AT28C04 @ DIP24, datasheetAT28C16 @ DIP24, datasheetAT28C64 @ DIP28, datasheetAT28C256 @ DIP28, datasheet
Details in the EEPROM Programmer: Supported Chips post.
The programmer's performance is strongly dependent on the Arduino platform; the DUE is ~30% faster than the MEGA for reads, and significantly faster for writes due to page write support. The performance measurement details are described in the EEPROM Programmer Performance post.
| Chip | Size | Read (sec) | Erase + Write + Verify (sec) |
|---|---|---|---|
| AT28C64 | 8K x 8 | 1.6 | 12.7 |
| Chip | Size | Full Memory Read (sec) | Full Memory Write (sec) |
|---|---|---|---|
| AT28C04 | 512 x 8 | 0.9 | 1.4 |
| AT28C16 | 2K x 8 | 3.6 | 11 |
| AT28C64 | 8K x 8 | 15 | 22 |
| AT28C256 | 32K x 8 | 56 | 85* |
(*) if page write mode is enabled.
| Chip | Size | Full Memory Read (sec) | Full Memory Write (sec) |
|---|---|---|---|
| AT28C04 | 512 x 8 | 1.4 | 1.8 |
| AT28C16 | 2K x 8 | 5.5 | 14 |
| AT28C64 | 8K x 8 | 21 | 28* |
| AT28C256 | 32K x 8 | 84 | 305 |
(*) if RDY/!BUSY polling mode is enabled
Communication over the Serial protocol can corrupt data stored in the EEPROM memory, despite the Hardware Protection claimed in the datasheet. This issue is analyzed in detail in the Data Corruption on Arduino Serial Connection Reset post.
To prevent data corruption, the Read-Only jumper wire should always be used during read operations, as shown in the wiring diagrams. The jumper should be removed only when writing data to the chip.
The --write command performs erase, write, and verify in a single serial session without reconnecting, so the jumper is not needed for write+verify operations.
The Programmer supports two chip wiring variants: DIP24 and DIP28. Each type requires its own wiring configuration and a recompilation of the code. Below is a mapping table along with the section of code that must be adjusted accordingly.
| Wiring Type | Supported Chips |
|---|---|
| DIP24 | AT28C04, AT28C16 |
| DIP28 | AT28C64, AT28C256 |
file: ./eeprom_programmer/eeprom_programmer.ino
// EEPROM Programmer
// specify the wiring type here
// * DIP24
// * DIP28
static EepromProgrammer eeprom_programmer(BoardWiringType::DIP24);
// static EepromProgrammer eeprom_programmer(BoardWiringType::DIP28);The wiring configuration can be reconfigured in the file ./eeprom_programmer/board_wiring.h
The pins are arranged sequentially to simplify wiring. On one side, the pins start with GND, and on the other with VCC, eliminating the need for separate power wires.
For the DIP28 variant, two additional wires are added on each side.
In the photographs, a short breadboard was used for the DIP24 wiring, along with two ribbon cables and a ZIF socket for convenience. This setup is not mandatory: simple jumper wires can be used instead, and the chip may be placed directly into the breadboard.
A Python CLI enables reading and writing large volumes of data. When using only the Arduino, it is not possible to program a 64 KB chip, as this does not fit into the MEGA's memory.
The CLI communicates with the firmware over a binary serial protocol with CRC-16 integrity checking. The previous JSON-RPC implementation is described in the Implementing Serial JSON-RPC API post.
pip3 install virtualenv
# get the python version
python --version
...
Python 3.9.6
# specify python version here (use only X.Y part)
PATH=${PATH}:~/Library/Python/3.9/bin/ ./env/init.sh
# activate the newly created venv
source venv/bin/activate
# add the CLI library to the python path
export PYTHONPATH=./eeprom_programmer_cli/:$PYTHONPATHThe CLI must be provided with the port to which the Arduino is connected. The port name can be found in the Arduino IDE or obtained by listing available ports using the following command:
python3 -m serial.tools.list_ports
...
/dev/cu.usbmodem2101The CLI interface closely resembles the minipro interface used with XGecu programmers.
# read data from the chip memory and write it to a binary file
./eeprom_programmer_cli/cli.py /dev/cu.usbmodem2101 -p AT28C64 --read tmp/dump_eeprom.bin
# read the binary file content with xxd
xxd tmp/dump_eeprom.bin | less# erase the chip memory with the specific pattern
./eeprom_programmer_cli/cli.py /dev/cu.usbmodem2101 -p AT28C64 --erase --erase-pattern FFThe --write command performs erase + write + verify in a single serial session. Verify runs automatically after writing, without reconnecting to the board.
# erase + write + verify (default)
./eeprom_programmer_cli/cli.py /dev/cu.usbmodem2101 -p AT28C64 --write test_bin/64_the_red_migration.bin
# skip verify after write
./eeprom_programmer_cli/cli.py /dev/cu.usbmodem2101 -p AT28C64 --write test_bin/64_the_red_migration.bin --skip-verify
# do not erase the chip content before the write operation
./eeprom_programmer_cli/cli.py /dev/cu.usbmodem2101 -p AT28C64 --write test_bin/64_the_red_migration.bin --skip-erase
# use custom erase pattern
./eeprom_programmer_cli/cli.py /dev/cu.usbmodem2101 -p AT28C64 --write test_bin/64_the_red_migration.bin --erase-pattern CCStandalone verify reads the chip and compares against a binary file. Requires the !WE jumper wire to prevent data corruption on reconnect.
# read data and verify it against the binary file
./eeprom_programmer_cli/cli.py /dev/cu.usbmodem2101 -p AT28C64 --verify test_bin/64_the_red_migration_AT28C64_ff.binCheck ./eeprom_programmer/eeprom_programmer_lib.h for error codes:
enum ErrorCode : short {
SUCCESS = 0,
...An unknown chip type, or a mismatch between the wiring configuration and the selected chip type. Check the DIP24 or DIP28 wiring.
init device: failed, failed to init AT28C256 chip with: error response: {'code': -32010, 'message': 'Programmer error', 'data': 'Failed to init AT28C256 chip with error: 12'}On startup, the CLI reports the board wiring type that the programmer firmware was built with:
programmer_settings: {'board_wiring_type': 24, 'max_page_size': 64}source venv/bin/activate
export PYTHONPATH=./eeprom_programmer_cli/:$PYTHONPATHNo jumper swapping needed. The --write command performs erase, write, and verify in a single serial session.
./eeprom_programmer_cli/cli.py /dev/cu.usbmodem2101 -p AT28C64 --write test_bin/64_the_red_migration_AT28C64_ff.bin --collect-performance# >> remove jumper wire
./eeprom_programmer_cli/cli.py /dev/cu.usbmodem2101 -p AT28C64 --erase --erase-pattern BB --collect-performance
# >> set jumper wire
./eeprom_programmer_cli/cli.py /dev/cu.usbmodem2101 -p AT28C64 --read tmp/dump_eeprom.bin
xxd tmp/dump_eeprom.bin | less# >> remove jumper wire
./eeprom_programmer_cli/cli.py /dev/cu.usbmodem2101 -p AT28C64 --write test_bin/64_the_red_migration.bin --erase-pattern BB --collect-performance
# >> set jumper wire
./eeprom_programmer_cli/cli.py /dev/cu.usbmodem2101 -p AT28C64 --read tmp/dump_eeprom.bin
xxd tmp/dump_eeprom.bin > tmp/dump_eeprom.hex
vimdiff tmp/dump_eeprom.hex tmp/64_the_red_migration.hex# >> remove jumper wire
./eeprom_programmer_cli/cli.py /dev/cu.usbmodem2101 -p AT28C64 --write test_bin/64_the_red_migration.bin --skip-verify --collect-performance
# >> set jumper wire
./eeprom_programmer_cli/cli.py /dev/cu.usbmodem2101 -p AT28C64 --verify test_bin/64_the_red_migration_AT28C64_ff.bin# AT28C04
./eeprom_programmer_cli/cli.py /dev/cu.usbmodem2101 -p AT28C04 --erase --erase-pattern 11 --collect-performance
./eeprom_programmer_cli/cli.py /dev/cu.usbmodem2101 -p AT28C04 --read tmp/dump_eeprom.bin --collect-performance && xxd tmp/dump_eeprom.bin | less
./eeprom_programmer_cli/cli.py /dev/cu.usbmodem2101 -p AT28C04 --write test_bin/4_journey_of_luna_AT28C04_ff.bin --skip-erase --collect-performance
./eeprom_programmer_cli/cli.py /dev/cu.usbmodem2101 -p AT28C04 --verify test_bin/4_journey_of_luna_AT28C04_ff.bin --collect-performance
# AT28C16
./eeprom_programmer_cli/cli.py /dev/cu.usbmodem2101 -p AT28C16 --erase --erase-pattern 11 --collect-performance
./eeprom_programmer_cli/cli.py /dev/cu.usbmodem2101 -p AT28C16 --read tmp/dump_eeprom.bin --collect-performance && xxd tmp/dump_eeprom.bin | less
./eeprom_programmer_cli/cli.py /dev/cu.usbmodem2101 -p AT28C16 --write test_bin/16_geese_in_space_AT28C16_ff.bin --skip-erase --collect-performance
./eeprom_programmer_cli/cli.py /dev/cu.usbmodem2101 -p AT28C16 --verify test_bin/16_geese_in_space_AT28C16_ff.bin --collect-performance
# AT28C64
./eeprom_programmer_cli/cli.py /dev/cu.usbmodem2101 -p AT28C64 --erase --erase-pattern 11 --collect-performance
./eeprom_programmer_cli/cli.py /dev/cu.usbmodem2101 -p AT28C64 --read tmp/dump_eeprom.bin --collect-performance && xxd tmp/dump_eeprom.bin | less
./eeprom_programmer_cli/cli.py /dev/cu.usbmodem2101 -p AT28C64 --write test_bin/64_the_red_migration_AT28C64_ff.bin --skip-erase --collect-performance
./eeprom_programmer_cli/cli.py /dev/cu.usbmodem2101 -p AT28C64 --verify test_bin/64_the_red_migration_AT28C64_ff.bin --collect-performance
# AT28C256
./eeprom_programmer_cli/cli.py /dev/cu.usbmodem2101 -p AT28C256 --erase --erase-pattern 11 --collect-performance
./eeprom_programmer_cli/cli.py /dev/cu.usbmodem2101 -p AT28C256 --read tmp/dump_eeprom.bin --collect-performance && xxd tmp/dump_eeprom.bin | less
./eeprom_programmer_cli/cli.py /dev/cu.usbmodem2101 -p AT28C256 --write test_bin/256_the_geometry_of_flight_AT28C256_ff.bin --skip-erase --collect-performance
./eeprom_programmer_cli/cli.py /dev/cu.usbmodem2101 -p AT28C256 --verify test_bin/256_the_geometry_of_flight_AT28C256_ff.bin --collect-performanceUse the minipro utility to perform read and write operations with the XGecu programmer
brew install minipro
# write the "real" dump to the chip
minipro --device AT28C64 -s -u --write test_bin/64_the_red_migration.bin
minipro --device AT28C256 -s -u --write test_bin/256_the_geometry_of_flight.bin
# read the data
minipro --device AT28C64 -u --read tmp/dump_xgecu.bin
minipro --device AT28C256 -u --read tmp/dump_xgecu.bin
# convert to HEX
xxd tmp/dump_xgecu.bin > tmp/dump_xgecu.hex
# compare
vimdiff tmp/dump_eeprom.hex tmp/dump_xgecu.hexThe firmware previously used JSON-RPC over serial. These commands no longer work with the current binary protocol firmware but are preserved here for reference. See the Implementing Serial JSON-RPC API post for details.
Set Arduino IDE's Serial Monitor on 115200 baud
init_chip(chip_type: str)
{"jsonrpc":"2.0", "id":0, "method": "init_chip", "params": ["AT28C64"]}set_read_mode(page_size_bytes: int)
{"jsonrpc":"2.0", "id":0, "method": "set_read_mode", "params": [4]}read_page(page_no: int)
{"jsonrpc":"2.0", "id":0, "method": "read_page", "params": [0]}set_write_mode(page_size_bytes: int)
{"jsonrpc":"2.0", "id":0, "method": "set_write_mode", "params": [4]}write_page(page_no: int, data: array[int])
{"jsonrpc":"2.0", "id":0, "method": "write_page","params": [0, [127, 127, 127, 127]]}{"jsonrpc":"2.0", "id":0, "method": "init_chip", "params": ["AT28C64"]}
{"jsonrpc":"2.0", "id":0, "method": "set_write_mode", "params": [4]}
{"jsonrpc":"2.0", "id":0, "method": "write_page","params": [0, [120, 130, 140, 150]]}
{"jsonrpc":"2.0", "id":0, "method": "write_page","params": [50, [20, 30, 40, 50]]}
{"jsonrpc":"2.0", "id":0, "method": "get_write_perf","params": []}{"jsonrpc":"2.0", "id":0, "method": "init_chip", "params": ["AT28C64"]}
{"jsonrpc":"2.0", "id":0, "method": "set_read_mode", "params": [4]}
{"jsonrpc":"2.0", "id":0, "method": "read_page", "params": [0]}
{"jsonrpc":"2.0", "id":0, "method": "read_page", "params": [50]}





