Programming EEPROM 93C76. We write a programmer. Part 1

What is 93C76?

The 93C76 is an 8K bit EEPROM with serial access. It contains, directly, a ROM block + a logic controller for working with the SPI bus. Supply voltage 5.0 Volts. It has two types of data addressing (8 bits and 16 bits each) – more on that later in the article.

Classification of microcircuits of the 93C76 / 86 family:
93C86 – the volume is increased by 2 times, up to 16 Kbps.
93LC76 – with a reduced supply voltage of 2.5 V.
S93C76 – in SOIC-8 package (SMD mounting).

Why program the 93C76?

The microcircuit is used in automotive units, hard drive control boards, and various multimedia devices. If there is a need to program this ROM urgently, I recommend using almost any Chinese programmer, this can be done even on inexpensive models. But I’m interested not just in pushing buttons, but in understanding the process, the essence of what is happening. I was inspired to create this article video from famous youtuber Ben Eater.

It shows, among other things, the subtraction of data from a ROM of this type. In my code, I will partially focus on his code, but at the same time we will implement all the functions of the programmer: reading, erasing, writing.

What funds are needed?

As in the video, we need an Arduino Uno (a Chinese copy is also suitable), the 93C76 chip itself (I found an S93C76 with an adapter for DIP-8) and a breadboard with wires. Of course, I could paint all the same, for example, for Atmega8, but in our case there is no need to complicate everything, Arduino will be quite enough to demonstrate the principle of working with this ROM, since there is a COM port for communicating with the controller through which we will exchange information in a bunch of PC – Arduino – 93C76. The programming language is C. So let’s get started.

Stage 1. Preparation. ROM pinout. Hardware connection.

Consider the purpose of the ROM pins:

The left half is the SPI bus pins, for communication with the microcontroller (hereinafter – Arduino). Right half – technical conclusions.

  1. CS – chip selection. Determines if this chip is currently being worked on.

  2. CLK – clock signal. It is issued by Arduino.

  3. DI – input for data. The data is provided by the Arduino.

  4. DO – data output. His accepts Arduino. Information will appear on this output after receiving a read command.

  5. VSS – Minus power, in our case – ground (GND)

  6. ORG – affects the organization of the data storage order and, accordingly, addressing. If a low level is set, then data will be received by 8 bits (x8 mode), if high – then by 16 bits (x16). The x8 mode is shown in the video from Ben Eater. Some empirical facts about x16 mode:

    1. If the microcircuit is turned on in x16 mode, and after the write command only 1 byte is transferred, then the second byte will be filled with zeros, that is, data will be lost in the cell.

    2. Addressing when writing in x16 mode is carried out in increments of 2, i.e. not 0,1,2…f, but 0,1,2..7, since each address will store 2 bytes. When reading, you can get any byte separately (not necessarily a pair) at its usual address (0 … f), or several bytes in a row, since the reading is performed sequentially (after the command is issued, data is received while the clock signal is on and does not stop).

    3. In my microcircuit, despite the state of the ORG level, a 16-bit memory organization was still used. It is possible that this function simply does not work for some manufacturers, so sometimes there are posts on the forums that the programmer cannot write the chip correctly. Or my chip had a damaged pin. In any case, this point must be treated carefully, otherwise data loss will occur.

  7. PE – (active low) output to protect the chip from writing and erasing. If your device does not need to write data, but only read them, then the output is connected to ground. In our case, we will set it to a high logical level, as we will write to ROM. The device has two write protections – hardware (PE output) and software (EWEN). If one of them is not removed, it will not work to write to the chip.

  8. VCC – power plus (4.5-7V for C76, 2.5-7V for LC76)

SPI pins are connected to any convenient digital pins of the microcontroller. In our case, these will be legs 2-6. Food can also be taken from MK. I set the technical outputs as follows: PE – high, ORG – high.

Now let’s connect the boards:

Stage 2. We study the principle of operation of the ROM. Transferring and reading data

When all connections are made, open the Arduino sketch editor. First, let’s write the definition of the legs for convenient work:

#define CS 2
#define CLK 3
#define DI 4
#define DO 5

In setup, we initialize the COM port for communication, assign the data direction for the outputs according to step 1:

Serial.begin(57600);
pinMode(CS, OUTPUT);
pinMode(CLK, OUTPUT);
pinMode(DI, OUTPUT);
pinMode(DO, INPUT);

Next, you need to figure out how to work with the SPI bus. To do this, look at the datasheet of the microcircuit:

From here, for minimal work, we will need the READ, EWEN, ERAL, WRITE, WRAL commands (optional). The command is transmitted by switching signals in a certain order, which is also indicated in the datasheet of the microcircuit. For example, reading:

Based on this, we see:

  • The CS signal turns on first, and it turns off last.

  • First, the level corresponding to the next bit of the command being sent is set on DI, then the clock signal is turned on and then turned off, as if “confirming” the action and fixing the data. Accordingly, we do not need to keep within the time frame – there are no strict requirements for timings. The switching speed of the Arduino ports is high, but the frequency of the chip logic is up to 2 MHz. Therefore, taking into account the time spent on switching levels through the Arduino libraries, we can simply give signals, and set the delay only after long functions (such as ERAL, WRAL).

  • When reading, after the command + address is transmitted, the DO output will start to receive information switched by the clock signal. We will extract it. Sequential reading of the entire array is allowed: if the clock signal is not stopped, then the next bits of information will be transmitted further, so it is not necessary to issue a read command separately for each byte.

  • When writing, we transmit a sequence consisting of a command, address bits A9-A0, and two bytes of information (for x16 organization), or A10-A0 address, but one byte (for x8 organization). In this case, to write every two bytes (bytes), the command must be retransmitted, sequential writing to the entire array is not carried out.

The instruction consists of 14 for x8 (or 13 for x16) bits, the order is as follows: set the first bit, toggle CLK up and then down, set the second bit, toggle CLK, and so on. Based on this, let’s implement the “sendInstruction” function, in order to send data to the ROM.

void sendInstruction(word comand) { //SEND 14
    for (word mask = 0b10000000000000; mask > 0; mask >>= 1) {
    if (comand & mask) {
      digitalWrite(DI, HIGH);
    } else {
      digitalWrite(DI, LOW);
    }
    digitalWrite(CLK, HIGH);
    digitalWrite(CLK, LOW);
    }
}

The function takes a word (command) as an argument, after which it “runs” the bus as many times as there are numbers in the mask. We moved the mask to the side – switched levels, switched CLK, and so on, until the mask runs out (the last unit leaves).

The commands (words) for various operations are as follows (according to the datasheet):

  • READ (read from address) – 0b1100000000000 + address

  • WRITE (write to cell) – 0b1010000000000 + address

  • EWEN (allow writing and erasing) – 0b10011000000000

  • ERAL (erase everything) – 0b10010000000000

  • WRAL (write everything in 2 bytes) – 0b10001000000000 + 2 bytes (8 switches each)

That is, for example, calling in turn

sendInstruction(0b10011000000000);
sendInstruction(0b10010000000000);

We will first enable write (making sure it is enabled on pin 7 of the chip), and then we will erase the chip. These functions are implemented as follows:

void ERAL() { // ERASE CHIP
  digitalWrite(CS, HIGH);
  sendInstruction(0b10010000000000);
  digitalWrite(CS, LOW);
  delay(15);
}

Delay of 15 ms to erase the entire chip according to the datasheet. When reading and writing:

sendInstruction(0b1100000000000 + address); 

After this command, a signal will appear on the bus, which we need to capture bit by bit, while continuing to switch CLK. For this, a separate function is implemented:

byte readByte() {
   byte data = 0;
   for (byte bit = 0; bit < 8; bit +=1) {
      digitalWrite(CLK, HIGH);
      digitalWrite(CLK, LOW);
      if (digitalRead(DO))  {
        data = data << 1 | 1;
        } else {
          data = data << 1;
        }
     }
    return data;
}

After sending a read instruction, this function must be called the required number of times (1 for one byte, 16 for 16 bytes, etc.) using the for loop, and the returned data byte is sequentially written to the array. Capturing the level on the leg (reading) is carried out by the digitalRead function built into the Arduino library.

For writing, we implement a separate function that sends one byte:

void sendByte(word comand) { //SEND 8
    for (word mask = 0b10000000; mask > 0; mask >>= 1) {
    if (comand & mask) {
      digitalWrite(DI, HIGH);
    } else {
      digitalWrite(DI, LOW);
    }
    digitalWrite(CLK, HIGH);
    digitalWrite(CLK, LOW);
    }
}

Further, we will call it after sending the instruction with the address for writing. Called once for x8, twice in a row for x16, for example:

void WRITE(word addr, byte bt1, byte bt2) {
  digitalWrite(CS, HIGH);
  sendInstruction(0b1010000000000 + addr);
  sendByte(bt1);
  sendByte(bt2);
  digitalWrite(CS, LOW);
  delay(5);
}

That is, the accepted arguments are: an address (for example, 0x030f for x8 or 0x0107 for x16) and two bytes of information. Based on this, we send an instruction for writing, and two bytes that we write. After that, an additional delay of 5 ms (according to the datasheet – 3 ms, but sometimes it does not keep up with this timing and is written with errors). As I mentioned earlier, if you do not transmit the second byte, zeros will simply be written instead.

On this, the functions of accessing the ROM (reading, erasing, writing) are completed. For other commands are performed by analogy. Next, we will try to write a UI for working with these functions.

Stage 3. Implement the functions of the x16 programmer in the software for Arduino

The “read” function is copied from the one Ben Eater did in his video. The function takes the starting address from which to start reading and the number of bytes to read. The result is sent to the COM port in the following format: HEX address, 16 bytes in HEX, 16 bytes as ASCII characters, if the table contains the corresponding one (otherwise it is replaced with a dot). The function itself:

void READ(word startAddress, int endAddress) { //read fashion HEX by Ben Eater
    byte line[16]; //VARIABLE FOR WORD
    for (word address = startAddress; address < endAddress; address += 8) { //1024 for C76, 2048 for C86; 8 for 16-BIT ORG, 16 for 8-bit ORG
    char temp[6]; //VARIABLE FOR ADDR - TMP
    sprintf(temp, "%04x  ", address);
    Serial.print(temp); //PRINT ADDR
    digitalWrite(CS, HIGH); //BEGIN READING DATA
    sendInstruction(0b1100000000000 + address); //please add external zero for x8 org
    for (int i = 0; i < 16; i += 1) { //READ DATA BYTE FOR EVERY LINE BYTE
        line[i] = readByte();
    }
    digitalWrite(CS, LOW);
    for (int i = 0; i < 16; i += 1) {
        char temp[4]; //VARIABLE FOR DATA IN HEX
        sprintf(temp, "%02x ", line[i]);
        Serial.print(temp); //PRINT DATA IN HEX - 16 BYTES
    }
    Serial.print(" ");
    // PRINT DATA IN LETTERS HEX ARR.
    for (int i = 0; i < 16; i += 1) { //PRINT DATA IN LETTERS - 16
        if (line[i] < 32 || line[i] > 126) {
            Serial.print(".");
        } else {
            Serial.print((char) line[i]);
        }
    }
    Serial.println(); //PRINT NEW LINE
  }
}

Let’s call it: READ(0x00, 512). Result:

Since the chip has been cleared, it is filled with FF bytes. Record end address: 01ff (which is 511 “double bytes” given that numbering starts from zero). Let’s calculate: we have 64 lines of 16 bytes, total 1024 bytes (kilobytes), which is stated in the datasheet. If there was an LC86 chip, then 2 kilobytes would be available. If the organization x8 were selected, then the last address would be 1023, not 511.
If you try to read or write beyond the available space, the last bits of the address (A10-A11, etc.) are ignored, and reading first starts in a circle, and then does not happen at all (since the LC76 chip will not understand the incoming command given to it).

Now let’s try to write a function that writes to the EEPROM a sequence of bytes received via the terminal:

void writeWord(word beginAddress) { //WRITES 16 BYTES SEQUENCE FROM TERMINAL
  while (Serial.available() == 0) {
  }
  byte lineToSend[16];
  int recLen = Serial.readBytes(lineToSend, 16);
  for (int i = 0; i < recLen; i += 2) {
    WRITE(beginAddress+(i/2), lineToSend[i], lineToSend[i+1]);
  }
}

The function is waiting for input from the terminal, until then it will hang in an infinite loop. It takes as an argument the address to write to. Next, writes 16 received bytes to the EEPROM memory.

This function is adapted to a 16-bit organization (x16): since we receive 1 byte each (the readBytes function), the array has a size of 16 bytes, the bytes are sent to the chip in pairs: i is added a multiple of two, the call goes to the address from 0 to 7, in this case, bytes are transmitted 0-1, 2-3, etc., that is, from 0 to f.

With 8-bit write (x8 mode), you can directly send WRITE(address, byte) 16 times.

Now let’s also try to receive the address from the terminal so that we can program the entire range. Let’s take the address as two separate bytes, which we then add into a word.

void waitAddr() {
   if (Serial.available() > 0) {
   byte receivedAddr[2];
    //RECEIVES ADDRESS IN 2 SEPARATE BYTES (example: 0x30 0x01)
   int recLen = Serial.readBytes(receivedAddr, 2);
   word programAddr = 0;
   programAddr |= receivedAddr[0];
   programAddr <<= 8;
   programAddr |= receivedAddr[1];
    //RETURNS REAL ADDRESS IN DECIMAL
   Serial.print("Received Addr: ");
   Serial.println(programAddr);
    //WAITING FOR 16-BYTE DATA INPUT SEQUENCE IN SEPARATE BYTES
   writeWord(programAddr);
  }
}

Of course, I wrote this function a little crookedly, however, it is enough for demonstration purposes. Let’s call waitAddr() in loop. It will immediately expect two bytes of address and 16 bytes to write. Now the standard Arduino terminal is no help to us – since it can only transmit ASCII characters. For testing, let’s connect via CoolTerm, pass the values ​​to HEX, and read what we got. We pass the address, then the Arduino responds that it has accepted it, and sends it back to us in DEC form. Values ​​converge:

Now let’s pass the information, and immediately read the entire block to see the result:

The function is working. After turning off the power, this data will already remain in the ROM memory. Having gone through all the addresses from 0000 to 01f8 in the terminal, we will be able to write down a whole kilobyte of information, thus, the goal of the work has been achieved. After writing the entire chip, it is recommended to check the CRC by reading it, but this is done on the counterpart in the PC, and not in the microcontroller.

Thus, we got the ability to read data from the ROM, and write them from the terminal. In order to implement a full-fledged programmer that is capable of writing received data to a binary file, and, in turn, writing data from a binary file to ROM, it is necessary to develop a “reciprocal” part of the software for a computer, I will try to implement it in python (although in a computer I am not strong in development, I managed to make more or less workable scripts).

Especially for habr.com, 2022.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *