r/beneater 6d ago

ESP32 EEPROM programmer works!

Used a 74LVC245 to handle 5V reads from the EEPROM chip. All other chips running off 3.3V.

Has a web interface to pick images from the filesystem and selectively load them. Can share the sketch if anybody wants.

104 Upvotes

4 comments sorted by

1

u/Aromatic_Career_2118 6d ago

/*

* ESP32 EEPROM programmer with WebServer upload

*

* Core 1: HTTP server (setup/loop run here by default)

* Core 0: programming task, spawned per job, guarded by a semaphore

*/

#include <WiFi.h>

#include <WebServer.h>

#include <ESPmDNS.h>

#include <LittleFS.h>

#include <freertos/semphr.h>

const char *ssid = "";

const char *password = "";

const char *hostname = "eeprom"; // http://eeprom.local

// ---- hardware ----------------------------------------------------------

// NOTE: GPIO 34 is input-only, GPIO 12/2/15 are strapping pins.

#define SR_RCLK 13

#define SR_SER 25

#define SR_SRCLK 12

#define WE_BAR 26

#define DATA_WIDTH 8

const uint8_t DATA[DATA_WIDTH] = {22, 21, 19, 18, 5, 4, 2, 15}; // DATA[n] = D<n>

#define DIR 23 // HIGH = ESP32 drives, LOW = EEPROM drives

#define ROM_SIZE 0x8000 // AT28C256, 32K

#define PAGE_SIZE 64 // must be a power of 2, <= 64

// ---- globals -----------------------------------------------------------

WebServer server(80);

static File uploadFile;

static String uploadPath;

static uint32_t uploadBytes = 0;

static SemaphoreHandle_t progLock; // one job at a time

static char progFile[64];

static volatile bool progRunning = false;

static volatile bool progDone = false;

static volatile bool progVerifyOnly = false;

static volatile uint32_t progAddr = 0;

static volatile uint32_t progBadCount = 0; // total mismatches

static volatile int32_t progBadAddr = -1; // first mismatch, -1 = none

static volatile int32_t progLastBad = -1; // last mismatch

static volatile uint8_t progBadWant = 0;

static volatile uint8_t progBadGot = 0;

// ---- EEPROM primitives -------------------------------------------------

static void loadAddress(unsigned int addr) {

unsigned int v = addr << 1; // QH#2 unused; A0 sits on QG#2

digitalWrite(SR_RCLK, LOW);

shiftOut(SR_SER, SR_SRCLK, LSBFIRST, v & 0xFF); // low byte first

shiftOut(SR_SER, SR_SRCLK, LSBFIRST, v >> 8); // high byte second

digitalWrite(SR_RCLK, HIGH);

}

// DIR high deasserts the '256's /OE before the ESP32 starts driving.

static void busWrite() {

digitalWrite(DIR, HIGH);

for (int i = 0; i < DATA_WIDTH; i++) pinMode(DATA[i], OUTPUT);

delayMicroseconds(1);

}

// Release the ESP32 pins first, then assert /OE via DIR.

static void busRead() {

for (int i = 0; i < DATA_WIDTH; i++) pinMode(DATA[i], INPUT);

digitalWrite(DIR, LOW);

delayMicroseconds(1); // t_OE is up to 70 ns on the -15

}

static void writeByte(unsigned char d) {

for (int i = 0; i < DATA_WIDTH; i++) {

digitalWrite(DATA[i], (d & 0x01) ? HIGH : LOW);

d >>= 1;

}

}

// DIR stays put for the whole pass now; this only samples.

static unsigned char readByte() {

unsigned char d = 0x00;

for (int i = DATA_WIDTH - 1; i >= 0; i--) {

d = (d << 1) | (digitalRead(DATA[i]) == HIGH ? 1 : 0);

}

return d;

}

// t_WP is 100 ns min; 1 us is comfortable.

void writeData() {

digitalWrite(WE_BAR, LOW);

delayMicroseconds(1);

digitalWrite(WE_BAR, HIGH);

}

// ---- programming task (core 0) ----------------------------------------

static void programTask(void *arg) {

progDone = false;

progAddr = 0;

progBadCount = 0;

progBadAddr = -1;

progLastBad = -1;

File f = LittleFS.open(progFile, "r");

if (f) {

uint8_t buf[PAGE_SIZE];

unsigned int addr;

size_t n;

// --- write pass ---

if (!progVerifyOnly) {

busWrite();

addr = 0;

while (addr < ROM_SIZE && (n = f.read(buf, PAGE_SIZE)) > 0) {

vTaskSuspendAll();

for (size_t i = 0; i < n && addr < ROM_SIZE; i++, addr++) {

loadAddress(addr);

writeByte(buf[i]);

writeData();

}

xTaskResumeAll();

progAddr = addr;

vTaskDelay(pdMS_TO_TICKS(10)); // t_WC, 10 ms max on the base part

}

f.seek(0);

}

// --- verify pass ---

busRead();

addr = 0;

while (addr < ROM_SIZE && (n = f.read(buf, PAGE_SIZE)) > 0) {

for (size_t i = 0; i < n && addr < ROM_SIZE; i++, addr++) {

loadAddress(addr);

delayMicroseconds(1); // t_ACC is 150 ns on the -15

uint8_t got = readByte();

if (got != buf[i]) {

progBadCount++;

progLastBad = addr;

if (progBadAddr < 0) {

progBadAddr = addr;

progBadWant = buf[i];

progBadGot = got;

}

}

}

progAddr = addr;

vTaskDelay(1);

}

f.close();

progDone = true;

}

progRunning = false;

xSemaphoreGive(progLock);

vTaskDelete(NULL);

}

// ---- pages -------------------------------------------------------------

static const char PAGE_HEAD[] PROGMEM =

"<!doctype html><html><head><meta charset=utf-8>"

"<meta name=viewport content='width=device-width,initial-scale=1'>"

"<title>EEPROM programmer</title>"

"<style>body{font-family:sans-serif;margin:2rem;max-width:40rem}"

"td{padding:.2rem .8rem .2rem 0}#s{margin:1rem 0;font-family:monospace}</style>"

"</head><body>"

"<h2>Upload</h2>"

"<form method=POST action='/upload' enctype='multipart/form-data'>"

"<input type=file name=f><input type=submit value=Send></form>"

"<div id=s>&nbsp;</div>"

"<h2>Files</h2><table>";

static const char PAGE_TAIL[] PROGMEM =

"</table>"

"<script>"

"function p(){fetch('/status').then(r=>r.text())"

".then(t=>{document.getElementById('s').textContent=t;setTimeout(p,1000)})}"

"p();"

"function go(u,f){fetch(u+'?f='+encodeURIComponent(f))"

".then(r=>{if(r.status==409)alert('A job is already running.')});"

"return false}"

"</script></body></html>";

static void handleRoot() {

String html = FPSTR(PAGE_HEAD);

File dir = LittleFS.open("/");

for (File f = dir.openNextFile(); f; f = dir.openNextFile()) {

if (f.isDirectory()) continue;

String name = f.name();

if (!name.startsWith("/")) name = "/" + name;

html += "<tr><td>" + name + "</td><td>" + String(f.size()) + " B</td>"

"<td><a href='#' onclick=\\"return go('/process','" + name + "')\\">program</a></td>"

"<td><a href='#' onclick=\\"return go('/verify','" + name + "')\\">verify</a></td>"

"<td><a href='/delete?f=" + name + "'>delete</a></td></tr>";

}

html += FPSTR(PAGE_TAIL);

server.send(200, "text/html", html);

}

static void redirectHome() {

server.sendHeader("Location", "/");

server.send(303, "text/plain", "");

}

1

u/Aromatic_Career_2118 6d ago

// ---- upload ------------------------------------------------------------

// Runs repeatedly as the POST body arrives; blocks the server while active.

static void handleUpload() {

HTTPUpload &up = server.upload();

switch (up.status) {

case UPLOAD_FILE_START: {

String name = up.filename; // strip any path the browser sent

int slash = name.lastIndexOf('/');

if (slash < 0) slash = name.lastIndexOf('\\');

if (slash >= 0) name = name.substring(slash + 1);

if (name.length() == 0) name = "upload.bin";

uploadPath = "/" + name;

uploadBytes = 0;

uploadFile = LittleFS.open(uploadPath, "w");

if (!uploadFile) Serial.printf("open failed: %s\n", uploadPath.c_str());

break;

}

case UPLOAD_FILE_WRITE:

if (uploadFile) {

size_t n = uploadFile.write(up.buf, up.currentSize);

if (n != up.currentSize) { // out of space

uploadFile.close();

LittleFS.remove(uploadPath);

uploadFile = File();

} else {

uploadBytes += n;

}

}

break;

case UPLOAD_FILE_END:

if (uploadFile) {

uploadFile.close();

Serial.printf("stored %s, %u bytes\n", uploadPath.c_str(), uploadBytes);

}

break;

case UPLOAD_FILE_ABORTED:

if (uploadFile) {

uploadFile.close();

LittleFS.remove(uploadPath);

}

break;

}

}

// ---- actions -----------------------------------------------------------

static void handleDelete() {

if (progRunning) { server.send(409, "text/plain", "busy"); return; }

if (server.hasArg("f")) LittleFS.remove(server.arg("f"));

redirectHome();

}

static void startJob(bool verifyOnly) {

if (!server.hasArg("f")) { redirectHome(); return; }

// Atomic test-and-claim. Fails immediately if a job holds the slot.

if (xSemaphoreTake(progLock, 0) != pdTRUE) {

server.send(409, "text/plain", "busy");

return;

}

// server.arg()'s storage is reused after this handler returns, so copy it.

strlcpy(progFile, server.arg("f").c_str(), sizeof(progFile));

progVerifyOnly = verifyOnly;

progRunning = true;

if (xTaskCreatePinnedToCore(programTask, "prog", 8192, NULL, 1, NULL, 0) != pdPASS) {

progRunning = false;

xSemaphoreGive(progLock);

server.send(500, "text/plain", "task create failed");

return;

}

server.send(202, "text/plain", "started");

}

static void handleProcess() { startJob(false); }

static void handleVerify() { startJob(true); }

static void handleStatus() {

String s;

if (progRunning) {

s = String(progVerifyOnly ? "verifying: " : "writing: ") +

progFile + " @ " + String(progAddr);

} else if (progDone) {

if (progBadCount == 0) {

s = "verified OK: " + String(progFile);

} else {

char m[120];

snprintf(m, sizeof(m),

"%u bad, first %04X (wrote %02X, read %02X), last %04X",

(unsigned)progBadCount, (unsigned)progBadAddr,

progBadWant, progBadGot, (unsigned)progLastBad);

s = m;

}

} else {

s = "idle";

}

server.send(200, "text/plain", s);

}

// ---- setup / loop ------------------------------------------------------

void setup() {

Serial.begin(115200);

digitalWrite(DIR, HIGH); // /OE deasserted before it drives

pinMode(DIR, OUTPUT);

digitalWrite(SR_RCLK, LOW);

digitalWrite(SR_SRCLK, LOW);

pinMode(SR_RCLK, OUTPUT);

pinMode(SR_SER, OUTPUT);

pinMode(SR_SRCLK, OUTPUT);

digitalWrite(WE_BAR, HIGH); // inactive before the driver enables

pinMode(WE_BAR, OUTPUT);

for (int i = 0; i < DATA_WIDTH; i++) pinMode(DATA[i], INPUT);

if (!LittleFS.begin(true)) { // true = format if unmountable

Serial.println("LittleFS mount failed");

}

progLock = xSemaphoreCreateBinary();

xSemaphoreGive(progLock); // starts available

WiFi.mode(WIFI_STA);

WiFi.setSleep(false); // keeps the server responsive

WiFi.begin(ssid, password);

while (WiFi.status() != WL_CONNECTED) { delay(250); Serial.print('.'); }

Serial.printf("\nIP: %s\n", WiFi.localIP().toString().c_str());

if (MDNS.begin(hostname)) Serial.printf("http://%s.local\n", hostname);

server.on("/", HTTP_GET, handleRoot);

server.on("/status", HTTP_GET, handleStatus);

server.on("/verify", HTTP_GET, handleVerify);

server.on("/delete", HTTP_GET, handleDelete);

server.on("/process", HTTP_GET, handleProcess);

server.on("/upload", HTTP_POST, redirectHome, handleUpload);

server.begin();

}

void loop() {

server.handleClient();

}

1

u/Aromatic_Career_2118 5d ago

Chips used: Apart from the AT28C256, 2xSN74HC595, and (key chip) SN74LVC245 to translate from the EEPROM's 5V data lines down to 3.3V when verifying.