diff --git a/.gitignore b/.gitignore index dbddb40..888ce5a 100644 --- a/.gitignore +++ b/.gitignore @@ -163,3 +163,9 @@ tags [._]*.un~ # End of https://www.toptal.com/developers/gitignore/api/vim,python + +# Binary files +*.bin + +# Mac +/**/.DS_Store diff --git a/.vim/coc-settings.json b/.vim/coc-settings.json new file mode 100644 index 0000000..154a184 --- /dev/null +++ b/.vim/coc-settings.json @@ -0,0 +1,6 @@ +{ + "python.linting.pylintEnabled": true, + "python.linting.mypyEnabled": false, + "python.linting.enabled": true, + "python.formatting.provider": "black" +} \ No newline at end of file diff --git a/README.md b/README.md index 56e1f8d..ca60e9e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ # eeprom_programmer A very basic EEPROM programmer, compatible with chips with up to 15 address lines. +# Protoboard assembly +![Protoboard assembly](images/protoboard_assembly.JPG) + ##### This project was inspired by and based on [Ben Eater's programmer](https://github.com/beneater/eeprom-programmer) diff --git a/arduino_programs/arduino_programmer/arduino_programmer.ino b/arduino_programs/arduino_programmer/arduino_programmer.ino index c10b2ed..c805a40 100644 --- a/arduino_programs/arduino_programmer/arduino_programmer.ino +++ b/arduino_programs/arduino_programmer/arduino_programmer.ino @@ -17,8 +17,70 @@ // Digital pins 8 to 13 correspond to PB0 to PB5 byte lastOp; +int lastAddressWritten; +byte lastDataWritten; +bool isFirstWrite = true; + static char printBuff[128]; +void program(); +void dump(); +void erase(); + +void dumpFirts256Bytes() { + byte data; + Serial.println("Reading EEPROM"); + for (int addr = 0; addr < 256; addr += 16) { + sprintf(printBuff, "%04x:", addr); + Serial.print(printBuff); + for (int offset = 0; offset < 16; offset++) { + sprintf(printBuff, " %02x", readEEPROM(addr + offset)); + Serial.print(printBuff); + } + Serial.println(); + } +} + +void setup() { + // put your setup code here, to run once: + pinMode(SHIFT_DATA, OUTPUT); + pinMode(SHIFT_CLK, OUTPUT); + pinMode(SHIFT_LATCH, OUTPUT); + + digitalWrite(EEPROM_WE, HIGH); + pinMode(EEPROM_WE, OUTPUT); + + // Setting an invalid value. + lastOp = -1; + + Serial.begin(115200); +} + +void loop() { + static byte command; + // The loop function only process commands. + while (Serial.available() == 0); + + command = Serial.read(); + + switch(command) { + case 0x00: + // Program the eeprom. + program(); + break; + case 0x01: + // Dump the eeprom. + dump(); + break; + case 0x02: + // Erase the EEPROM (fill with 0xFF). + erase(); + default: + // Ignore invalid commands. + break; + } +} + void latchOutput() { static byte latchMask = 0b00010000; @@ -27,6 +89,7 @@ void latchOutput() { PORTD |= latchMask; delayMicroseconds(1); PORTD &= ~latchMask; + delayMicroseconds(1); } void commitWrite() { @@ -87,29 +150,43 @@ void setAddress(int address, bool outputEnable) { latchOutput(); } -byte readEEPROM(int address) { - // TODO: Think about adding a delay if the last operation was a write and where it is appropriate to put it. - // If the last operation wasn't a read we need to set our pins as input. - if (lastOp != LAST_OP_READ) { - setBusMode(READ_MODE); - } - +byte readBus(int address) { // In contrast with the write function, it's okay to set output enable after setting the // pins as inputs. This way, the arduino pins are already ready to sink current when the // EEPROM starts driving the bus. - byte result = (PINB << 3) | (PIND >> 5) ; - - lastOp = LAST_OP_READ; - return result; + setAddress(address, true); + // For some reason, PINB takes a long time to update, a delay of a minimum of 2 microsends + // Must be added. The datasheet of the AT28C256 specifies a time from address to output of 150ns max + // and a time from output enabled to output of 70ns max, thus it makes no sense to wait 2000ns, but + // if it is not done, the most significant nibble is always 0xF. + delayMicroseconds(2); + return (PINB << 3) | (PIND >> 5); } +byte readEEPROM(int address) { + // If the last operation wasn't a read we need to set our pins as input + if (lastOp != LAST_OP_READ) { + setBusMode(READ_MODE); + // If last op was a write we need to poll for valid data. + if (lastOp == LAST_OP_WRITE) { + while (readBus(lastAddressWritten) != lastDataWritten); + } + } + + lastOp = LAST_OP_READ; + return readBus(address); +} -/** - * Writes a single byte of data to the specified address. - * If cooldown is true, the function adds a delay to avoid - * reading incorrect data after a write. - */ -void writeEEPROM(int address, byte data, bool cooldown) { +void writeEEPROM(int address, byte data, bool pollOnPageChange) { + // Since we're performing page writes, we must poll the data when we change page. + if ( + ((address & 0xFFC0) != (lastAddressWritten & 0xFFC0)) + && isFirstWrite == false + && pollOnPageChange + ) { + while(readEEPROM(lastAddressWritten) != lastDataWritten); + } + // For the write case, must turn output enable off before setting the pins as outputs. // If we don't do this, for a brief period of time, both the arduino and the EEPROM would // drive the bus and this could case problems. @@ -122,37 +199,71 @@ void writeEEPROM(int address, byte data, bool cooldown) { // EEPROM_D0 to D2 correspond to PD5 to PD7 // EEPROM_D3 to D7 correspond to PB0 to PB4 - PORTD &= (data << 5) & 0b00011111; - PORTB &= (data >> 3) & 0b11100000; + PORTD = (PORTD & 0b00011111 ) | (data << 5); + PORTB = (PORTB & 0b11100000) | (data >> 3) ; + // Slow version. + //for (int pin = EEPROM_D0; pin < EEPROM_D7; pin++, data >>= 1) { + // digitalWrite(pin, data & 1); + //} + commitWrite(); lastOp = LAST_OP_WRITE; - - if (cooldown) { - delay(10); + isFirstWrite = false; + + lastAddressWritten = address; + lastDataWritten = data; +} + + +void disableSoftwareProtection() { + + writeEEPROM(0x5555, 0xAA, false); + writeEEPROM(0x2AAA, 0x55, false); + writeEEPROM(0x5555, 0x80, false); + writeEEPROM(0x5555, 0xAA, false); + writeEEPROM(0x2AAA, 0x55, false); + writeEEPROM(0x5555, 0x20, false); + + delay(10); +} + +void erase() { + for (long addr = 0; addr < 32768; addr++) { + writeEEPROM(addr, 0xFF, true); + } + // Ack the operation. + Serial.write(0xFF); +} + +void program() { + // For now, the programmer will always write to the whole eeprom. + byte value; + // The program will write byte by byte. + for (long addr = 0; addr < 32768; addr++) { + // Wait for next value. + while(Serial.available() == 0); + value = Serial.read(); + writeEEPROM(addr, value, true); } } +void dump() { + unsigned long startAddress = 0; + unsigned long byteCount = 0; + byte value; + // Wait and read the starting address to dump. + while (Serial.available() < 2); + startAddress |= Serial.read(); + startAddress |= Serial.read() << 8; -void setup() { - // put your setup code here, to run once: - pinMode(SHIFT_DATA, OUTPUT); - pinMode(SHIFT_CLK, OUTPUT); - pinMode(SHIFT_LATCH, OUTPUT); - - digitalWrite(EEPROM_WE, HIGH); - pinMode(EEPROM_WE, OUTPUT); - - // Setting an invalid value. - lastOp = -1; - - Serial.begin(9600); - - writeEEPROM(0x1234, 0x55, true); - writeEEPROM(0x4321, 0x7E, true); -} - -void loop() { - // put your main code here, to run repeatedly: + // Wait and read the byte count. + while (Serial.available() < 2); + byteCount |= Serial.read(); + byteCount |= Serial.read() << 8; + for (int address = startAddress; address < startAddress + byteCount; address++ ) { + value = readEEPROM(address); + Serial.write(value); + } } diff --git a/arduino_programs/arduino_tester/arduino_tester.ino b/arduino_programs/arduino_tester/arduino_tester.ino index 24484da..ffd829a 100644 --- a/arduino_programs/arduino_tester/arduino_tester.ino +++ b/arduino_programs/arduino_tester/arduino_tester.ino @@ -10,8 +10,23 @@ #define ADDRESS_CLOCK 18 #define WRITE_CLOCK 19 +/* + + PORT mappings for data pins: + + 2 PORTE 4 + 3 PORTE 5 + 4 PORTG 5 + 5 PORTE 3 + 6 PORTH 3 + 7 PORTH 4 + 8 PORTH 5 + 9 PORTH 6 + +*/ + void setup() { - Serial.begin(9600); + Serial.begin(115200); attachInterrupt(digitalPinToInterrupt(ADDRESS_CLOCK), onAddressClock, RISING); attachInterrupt(digitalPinToInterrupt(WRITE_CLOCK), onWriteClock, FALLING); } @@ -27,10 +42,16 @@ void onAddressClock() { } void onWriteClock() { - Serial.print("Data: 0b"); - for (int pin = DATA_7; pin >= DATA_0; pin--) { - Serial.print(digitalRead(pin)); - } + Serial.print("Data: 0b"); + // TODO: Find a way of making this more readable. + Serial.print((PINH & 0x40) >> 6); + Serial.print((PINH & 0x20) >> 5); + Serial.print((PINH & 0x10) >> 4); + Serial.print((PINH & 0x08) >> 3); + Serial.print((PINE & 0x08) >> 3); + Serial.print((PING & 0x20) >> 5); + Serial.print((PINE & 0x20) >> 5); + Serial.print((PINE & 0x10) >> 4); Serial.println(); } diff --git a/cli/cli.py b/cli/cli.py new file mode 100644 index 0000000..e5315ce --- /dev/null +++ b/cli/cli.py @@ -0,0 +1,160 @@ +import argparse +import time +import serial +from tqdm import tqdm + + +def get_parsed_args(): + + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers(title="Operation to perform", dest="operation") + + program_parser = subparsers.add_parser("program") + dump_parser = subparsers.add_parser("dump") + erase_parser = subparsers.add_parser("erase") + + # Parser for the "program" subcommand. + program_parser.add_argument( + "-f", + "--file", + help="Binary file containing the program", + type=argparse.FileType("rb"), + required=True, + ) + + program_parser.add_argument( + "-p", "--port", help="Serial port where programmer is located", required=True + ) + + # Parser for the dump subcommand. + dump_parser.add_argument( + "-f", + "--file", + help="The file where the binary dump will be stored", + type=argparse.FileType("wb"), + required=True, + ) + + dump_parser.add_argument( + "-p", "--port", help="Serial port where programmer is located", required=True + ) + + dump_parser.add_argument( + "-s", + "--start", + help="The address where to start dumping", + type=int, + required=True, + ) + + dump_parser.add_argument( + "-c", + "--byte-count", + help="The amount of bytes to dump", + type=int, + required=True, + ) + + dump_parser.add_argument( + "--display", + help="Print the dump to the screen after storing it to the file", + action="store_true", + ) + + # Parser for the erase command . + erase_parser.add_argument( + "-p", + "--port", + help="Serial port where the programmer is located", + required=True, + ) + + return parser.parse_args() + + +def program(args): + print("Programming...") + with serial.Serial(args.port, 115200) as ser: + # Give a chance to the arduino to reset. + # TODO: Arduino resets by default when opening a serial + # connection, there's ways to avoid this. Investigate more. + time.sleep(2) + + # Sending command + ser.write((0).to_bytes(1, "big")) + + bytes_to_write = args.file.read() + # Forward the bytes from the selected file + for i in tqdm(range(32 * 1024)): + value = (bytes_to_write[i]).to_bytes(1, "big") + ser.write(value) + # This delay was found by experimentation. + # A byte write in the arduino takes about 80us. + # Every 64 bytes (a page) we do data polling and could + # take much longer. A byte send given a 115200 baud rate + # should take ~70us, a 100us delay should give the EEPROM enough time + # to internally write the page withouh the UART read buffer (64 bytes) + # overflowing. + time.sleep(100.0 * 1e-6) + print("Done programming!") + + +def dump(args): + print("Dumping...") + with serial.Serial(args.port, 115200) as ser: + address = int(args.start) + byte_count = int(args.byte_count) + + # TODO: Avoid arduino autoreset. + print("Waiting for the arduino to reset...") + time.sleep(2) + + # Sending command + ser.write((1).to_bytes(1, "big")) + + # Sending address, low byte first. + ser.write((address & 0xFF).to_bytes(1, "big")) + ser.write(((address & 0xFF00) >> 8).to_bytes(1, "big")) + # Sending byte count, low byte first. + ser.write((byte_count & 0xFF).to_bytes(1, "big")) + ser.write(((byte_count & 0xFF00) >> 8).to_bytes(1, "big")) + + # Read the stream coming from the Arduino and forward them + # to the user-selected dump file. + for _ in range(byte_count): + args.file.write(ser.read()) + print("Done dumping!") + + +def erase(args): + print("Erasing...") + with serial.Serial(args.port, 115200) as ser: + + # TODO: Avoid arduino autoreset. + print("Waiting for the arduino to reset...") + time.sleep(2) + + ser.write((2).to_bytes(1, "big")) + + # Wait for the ack. + if ser.read() == b"\xFF": + print("Erasing completed!") + else: + print("Erasing failed") + + +def main(): + args = get_parsed_args() + + if args.operation == "program": + program(args) + elif args.operation == "dump": + dump(args) + elif args.operation == "erase": + erase(args) + else: + print("Unrecognized command, exiting now...") + + +if __name__ == "__main__": + main() diff --git a/cli/requirements.txt b/cli/requirements.txt new file mode 100644 index 0000000..cc66994 --- /dev/null +++ b/cli/requirements.txt @@ -0,0 +1,17 @@ +appdirs==1.4.4 +astroid==2.4.2 +isort==5.6.4 +jedi==0.17.2 +lazy-object-proxy==1.4.3 +mccabe==0.6.1 +mypy-extensions==0.4.3 +parso==0.7.1 +pathspec==0.8.1 +pylint==2.6.0 +pyserial==3.5 +rope==0.18.0 +six==1.15.0 +toml==0.10.2 +tqdm==4.54.1 +typing-extensions==3.7.4.3 +wrapt==1.12.1 diff --git a/images/protoboard_assembly.JPG b/images/protoboard_assembly.JPG new file mode 100644 index 0000000..48f5362 Binary files /dev/null and b/images/protoboard_assembly.JPG differ diff --git a/serial_monitor/requirements.txt b/serial_monitor/requirements.txt new file mode 100644 index 0000000..f7507fa --- /dev/null +++ b/serial_monitor/requirements.txt @@ -0,0 +1,16 @@ +appdirs==1.4.4 +astroid==2.4.2 +isort==5.6.4 +jedi==0.17.2 +lazy-object-proxy==1.4.3 +mccabe==0.6.1 +mypy-extensions==0.4.3 +parso==0.7.1 +pathspec==0.8.1 +pylint==2.6.0 +pyserial==3.5 +rope==0.18.0 +six==1.15.0 +toml==0.10.2 +typing-extensions==3.7.4.3 +wrapt==1.12.1 diff --git a/serial_monitor/serial_monitor.py b/serial_monitor/serial_monitor.py new file mode 100644 index 0000000..934bd1b --- /dev/null +++ b/serial_monitor/serial_monitor.py @@ -0,0 +1,44 @@ +import glob +import argparse +import serial + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--port") + args = parser.parse_args() + + # Get all the valid serial ports available. + available_ports = glob.glob("/dev/cu.*") + + # Only do this if the user did not specify a port. + if args.port is None: + # Make the user select a port to start a listening session. + print("Available ports:") + for i, port in enumerate(available_ports): + print(f"{i} - {port}") + + selected_port_index = None + while selected_port_index is None and args.port is None: + try: + selected_port_index = input( + "\nType port number to start listening section: " + ) + selected_port_index = int(selected_port_index) + if selected_port_index < 0 or selected_port_index >= len(available_ports): + print("The port number you provided does not exist.") + selected_port_index = None + except: + print("The typed input is not valid, please enter a valid number.") + selected_port_index = None + + port = args.port if args.port is not None else available_ports[selected_port_index] + # TODO: Allow selecting baud raute, 115200 by default for now. + with serial.Serial(port, 115200) as ser: + while True: + byte = ser.read() + print(byte.decode("utf-8"), end="") + + +if __name__ == "__main__": + main()