Improvising an EPROM programmer

For a project (revitalizing a large 80’s liquid crystal display) I needed to program an EPROM. The EPROm should contain a character font that goes into the LCD so it can show characters. The EPROM required was a 2764, this is a 64 kilobit (8kiB) eraseable programmable read only memory, which is eraseable by ultraviolet light.

The first problem: I didn’t have a 2764. I did have a 2716 (16 kilobit), new old stock. I bought this in 1981 or so for the Elektuur Junior computer. I remember the Junior required a 2708, but by that time the 2708 was already obsolete and the 2716 was more or less pin compatible. In 1981 I changed a few pcb traces on the Junior and it worked as a treat. I imagined I could do something similar with the 2716/2764, although my EPROM has 4 times less memory than required, but it could still contain 1 ASCII character set.

But then my friendly colleague Bart came in and said: are you looking for this? And he handed me a second-hand 2764. Bravo. problem solved.

The second problem: the 2764 was not empty and needed to be UV erased. On the attic I found a 300 watt Philips Ultraviolet tanning lamp,  that my mother gave to me thirty years ago. I used it back in the day to develop the photosensitive layer of pcb’s. I assumed it would be good for erasing EPROMs as well. Well, no.

20171228_192028 - kopieTanning lamp (from the 1960’s?)

The first attempt almost burnt down the house, as the tanning lamp generates lots of heat. After four minutes there was an alarming smell, so I had to turn off the lamp. I checked the EPROM. Nothing had happened. So I continued outside of the house and left the EPROM 2 cm under the tanning lamp for 6 hours and checked the EPROM again. Still no luck.

Then I recalled that I had once ordered a collection of LEDs in different colours including a few infrared and ultraviolet LEDs. At first I thought: it can’t be true that a tiny 5 mW LED can do what a giant 300W tanning lamp can’t. But then I checked the datasheets.

EPROM: Erasure of the μPD2764 programmed data can be attained when exposed to light with wavelengths shorter than approximately 4000Å. (…) The integrated dose (i.e. UV intensity × exposure time) should be ≥ 15 Ws/cm². E.g. 20 minutes using a UV lamp of 12mW/cm² power rating.

LED (Optosupply OSV4YL5451B): Optical characteristics: Peak Wavelength: typ. 395 nm. Radiant Flux (I=20mA): 5.0mW. 50% power angle: 55/30°.

This means the wavelength is good and if I manage to get 25% of the light coming from the LED to fall on 1 cm² of the EPROM, the intensity will be 1.25mw/cm². That is ten times as low as the example in the EPROM datasheet. But what counts is intensity × exposure time, says the datasheet. So, exposure time should be not 20 minutes, but 200 minutes or just a little over 3 hours.

20171225_133953 - kopieErasing an EPROM with a LED

As an EPROM is erased, all the bits are reset to 1’s. I presume this is sort of a random process, a photon just has to hit the bit at the right angle to clear it or so. A measure of the progress of erasing is the number of 0’s still left in the EPROM. I counted the number of 0’s initally; it was 7651 (the other 57885 bits were 1’s).

I put the EPROM under the UV LED initially for 25 minutes. The number of 0’s had dropped to 1752. That is a factor 4.37. If this erasure of bits is a truly random process, I would expect the number of 0’s to drop with a factor 4.37 every 25 minutes. Which means that the number of 0’s will have dropped to approximately one in another five times 25 minutes. That is 2 hours.

I put the EPROM under the UV LED overnight for another 20 hours. Yes, twenty. The number of 0’s had dropped to 70. That was a bit of a disappointment, but I left it that way for the time being. For my font I will only be using a quarter of the EPROM and ¾ of the bits will be 0’s anyway. At the end of the day this implies that there will probably be 4 or 5 incorrect pixels in my font.

In the mean time I have ordered a 3W, 380nm UV power LED on Aliexpress for less than €1.00, so when that arrives, I’ll try it again. Theoretically it should take less than a minute.

20180108_210408The 3W UV LED arrived a week later. I installed it on an aluminium plate as a heatsink and ran 700 mA through it. Left it on for a couple of hours. I didn’t do anything to the EPROM. Back to square one!

Finally, I used two small UV 5mW LED’s and left them on for 48 hours. Then the entire EPROM was finally erased!

Third problem: I didn’t have an EPROM programmer. I decided to build one myself using an ATMEGA32 microcontroller from Atmel (Microchip these days). It turned out not to be that hard, there is just one pitfall: every EPROM is different!

20171225_133501 - kopie
Left: the ATMEGA32, middle: the 2764 EPROM. Right: a cheap DC/DC converter set at 5V.

Therefore: check the datasheet of exactly the type of EPROM you have from exactly that manufacturer. Important parameters are the programming voltage and the duration of the programming pulse. I have wasted many hours trying to program my 2764 based on the datasheet of the M2764A from ST, while the EPROM I had received from Bart was the μPD2764 from NEC. Major difference: programming voltage of 21V and not 12.5V! Also the M2764A has a very complex procedure of several programming pulses of different lengths (1ms-75ms). The μPD2764 only requires one programming pulse of 50ms.

EPROM programmer schematics

Bill of materials

  • ATMEGA32A-PU microcontroller
  • 8MHz crystal
  • uPD2764 EPROM that must be programmed
  • Lab supply set to 21V
  • 7805 or R-78E5.0 (switched mode variant of 7805) or alike
  • 2 × capacitor electrolytic 10μF/25V
  • capacitor ceramic 100nF
  • UART/USB module, e.g. FT232 or cheap ass Chinese clone such as a CH340G based module for < €1,00, as long as it has a USB connection, RXD and TXD, it’s OK
  • solderless breadboard

 

Slow! Slow!
My PC and my home-built programmer  are communicating using a serial connection over USB at the incredible speed of 300 bits per second. Why so slow?

Programming an EPROM takes time. This particular model requires 50ms of programming time for each byte to program and the programmer probably needs some time for housekeeping on top of that. So programming a 2764 takes something like  8k×60ms=8 minutes.

I wanted a super simple interface between the PC and the programmer. The last thing I want to do is write a Windows program! So I used a terminal emulator program (PuTTY) and my plan was to just cut and paste the Intel HEX file containing the data into the terminal emulator. The Intel Hex format has approximately 180% overhead, so an Intel hex file for a 8k EPROM is more than 22kiB large. Uploading such a file at 38400 b/s takes 6 seconds. If you do it this way, the programmer has to store all data in memory and then start programming during 8 minutes. But the ATMEGA32 has only 2 kB internal RAM!

There are two solutions to this problem: either put a separate RAM module in the programmer, such as a Microchip 23LCV512, but that would make the design more complex. The solution I chose: slow down the serial interface up to a point that the programmer can process the data at the speed they are coming in.

If we take 60ms for programming a byte, and the overhead is 180%, and we assume the serial communication uses 8 bits + 1 start bit + 1 stop bit, the maximum bit rate can be calculated at 2.8 × 10 / 60 ms = 467b/s. So, at 300b/s we are on the safe side.

I implemented the following commands. Each command has to be confirmed by a Return.

  • R read the entire EPROM
    • Can be interrupted by hitting Return again
  • RXXXX read starting at address hex XXXX
    • Can be interrupted by hitting Return again
  • C count the number of 0 bits and the number of 1 bits
  • :NNAAAA00DATACC program data according to Intel HEX format
    • : colon starts the program command
    • NN: number of bytes (hex)
    • AAAA: start address (hex)
    • 00: must be 00 (means: data)
    • DATA: hexadecimal data (the number of bytes must equal NN)
    • CC: checksum (the total of all bytes modulo 256 must be 0)
    • Response is W with a number of plus and minus signs. + is good, – means there is a difference between what is programmed and what is read back from the Eprom

Example of a session in PuTTY at 300b/s:

Enter C to count, R[HEX] to read Eprom or send Intel Hex file
>C
Reading
Number of 0 bits: 0x00001C35
Number of 1 bits: 0x0000E3CB
>R0B00
:100B00000205020000000000FFFFFFFFFFFFFFFFE4
:100B10000004041F041F0000FFFFFFFFFFFFFFFF93
:100B2000060904020F000000FFFFFFFFFFFFFFFFA9
:100B3000060904090600Interrupted...
>:080000000205020008140800CB
:08001000020502101810100097
W++++++++++++++++
W++++++++++++++++
>

That’s it. Till next time!

Charles

The C program for the ATMEGA32 is:

/*
 * EpromProgrammer.c
 *
 * Created: 11-12-2017 16:28:05
 * Author: Charles
 In needed an EPROM programmer to program an EPROM. So I decided to make one.
 I got a NEC uPD2764 8kB Eprom from Bart. This program works for this specific EPROM. Other EPROMs have slightly different programming procedures.
 Watch out: the programming voltage Vpp depends on exactly the type of eprom you have. So check the exact datasheet of your exact eprom!
 M2716: 25V
 uPD2764: 21V
 M2764A: 12.5V
 
 I tried to program my 2764 at 12.5V but it didn't work and after days of struggling I found out that it requires 21V.
 
 For erasing I used a 395nm 5mW UV LED. After 20 hours of erasing it erased most of the bits but there were still some 80 bits stuck at 0 (and 65000 bits correctly erased at 1, of which 50000 were already 1's).
 
 ATMEGA32 with 8MHz crystal
 Serial communication at 300 b/s on PD0/PD1 (1 start, 1 stop, no parity, 8 data bits). This speed is so low, because the 2716 take 50ms for programming one byte. At a data rate of 300 b/s we can keep up with programming and reading serial data without need of flow control
 We use Intel HEX format over the serial interface. This takes two hex characters per byte. Using 8 data bits, 1 start and 1 stop bit, that will give worst case 300 b/s / 10 / 2 = 15 bytes/s, that gives us 66.6 ms per byte to do the programming
 For the 2764 the figures are: Initial pulse of 1 ms, check and repeat up to 25 times, and then an overprogam pulse of 3-75 ms. 14400 b/s /10 /2 = 720 bytes/s, that gives 1.39 ms per byte
 PORT A MSB OF ADDRESS to eprom
 PORT C LSB OF ADDRESS to eprom
 PORT B DATA to eprom
 PD0 RXD from pc
 PD1 TXD to pc
 PD2 P (pin 27 of the 2764)
 PD3 G (pin 22 of the 2764)
 
 Vpp (pin 28 of the 2764) of Eprom must be connected to programming voltage, which depends on the type of eprom
 Programming requires a 0V pulse of 50 ms on the P pin
 */

#include <avr/io.h>
#include <avr/interrupt.h>

#define EPROM_P 2
#define EPROM_G 3
#define LED 4
#define BUFFER_LENGTH 160
#define PUFFER_LENGTH 100
#define EPROM_TYPE 64

void init();
void read_eprom();
void write_eprom();
void princ(char c);
void print(char*s);
void prinh(uint8_t h);
void count_bits();

volatile uint8_t command_entered=0;//boolean that indicates there is a complete command with RETURN in buffer

//serial input buffering
char buffer[BUFFER_LENGTH+1];
char*buffer_head=buffer;
char*buffer_end=buffer+BUFFER_LENGTH;

//serial output buffering
char puffer[PUFFER_LENGTH+1];
char*puffer_head=puffer;//where to insert characters in the output buffer
volatile char*puffer_tail=puffer;//the first character that can be sent to serial output port
char*puffer_end=puffer+PUFFER_LENGTH;
volatile uint8_t puffer_fill=0;//number of characters in output puffer

int main(void)
{
 init();
 print("Enter C to count, R[HEX] to read Eprom or send Intel Hex file starting with :\r\n>");
 while(1){
 if(command_entered){
 command_entered=0;
 char command=buffer[0];
 print("\n>");
 switch(command){
 case 0:
 break;
 case 'r':
 case 'R':
 read_eprom();
 break;
 case 'c':
 case 'C':
 count_bits();
 break;
 case ':':
 write_eprom();
 break;
 default:
 print("Error: ");
 if((command>='!')&&(command<0x7f)) princ(command);
 else prinh(command);
 print("\r\n>");
 }
 //now remove the line from the buffer. There may be type-ahead characters behind the RETURN
 char*p=buffer;
 while(p[0]!=0)p++;
 if(p!=buffer_head){ // xxx todo not tested yet
 p++;
 char*q=buffer;
 cli();//don't let the serial line interrupt routine interfere with the following
 while(p!=buffer_head){
 q[0]=p[0]; 
 //serial_write(q[0]);
 if(q[0]==0){
 command_entered=1;
 //princ('H');
 }
 q++; 
 p++;
 }
 buffer_head=q;
 sei();
 }
 }
 }
}

void init(){
 SFIOR|=(1<<PUD);//Globally disable all pull up resistors on all input port pins
 DDRA=0xff;
 DDRC=0xff;
 DDRD=0b11110;
 //setup serial communication at 300 bps
 uint16_t ubrr=1666; // 8MHz/16/300-1 = 1665.666
 UBRRH=ubrr>>8;
 UBRRL=ubrr&0xff;
 UCSRB=(1<<RXEN)|(1<<TXEN)|(1<<RXCIE)|(1<<UDRIE);
 UCSRC=(1<<URSEL)|(1<<UCSZ0)|(1<<UCSZ1);
 sei();//enable interrupts
}

void read_eprom(){
 uint8_t checksum=0;
 uint16_t adres=1;
 DDRB=0;
 PORTD|=(1<<EPROM_P); //P high
 PORTD&=~(1<<EPROM_G); //G low
 print("Reading");
 for(uint8_t i=1; i<5; i++)if(((buffer[i]<'0')||(buffer[i]>'9')) && ((buffer[i]<'A')||(buffer[i]>'F')))adres=0;
 if(buffer[5]!=0)adres=0;
 if(adres){
 adres=0;
 print (" starting at ");
 for(uint8_t i=1; i<5; i++){
 adres= adres<<4;
 adres+=buffer[i]-'0';
 if(buffer[i]>='A')adres-=7;
 }
 prinh(adres>>8);
 prinh(adres&0xff);
 }
 print("\r\n");

for(; adres<128*EPROM_TYPE; adres++){
 PORTA=adres>>8;//high address byte
 PORTC=adres&0xff;//low address byte
 if((adres&0xf)==0){
 princ(':');
 princ('1');
 princ('0');
 prinh(PORTA);
 prinh(PORTC);
 princ('0');
 princ('0');
 checksum=-0x10-PORTA-PORTC;
 }
 //wait 450 ns
 asm volatile ("nop");
 asm volatile ("nop");
 asm volatile ("nop");
 asm volatile ("nop");
 asm volatile ("nop");
 uint8_t data=PINB;
 prinh(data);
 checksum-=data;
 if((adres&0xf)==0xf){//write checksum
 prinh(checksum);
 checksum=0;
 princ('\r');
 princ('\n');
 }
 if(command_entered){
 print("...interrupted\r\n");
 return;
 }
 }
 
 //print end of file
 print(":00000001FF");
 princ('\r');
 princ('\n');
 princ('\n');
}

void count_bits(){//count the number of '1's and '0's in the eprom
 DDRB=0;
 PORTD|=(1<<EPROM_P); //P high
 PORTD&=~(1<<EPROM_G); //G low
 uint32_t nullen=0;
 print("Counting\r\n");

for(uint16_t adres=0; adres<128*EPROM_TYPE; adres++){
 PORTA=adres>>8;//high address byte
 PORTC=adres&0xff;//low address byte
 //wait 450 ns
 asm volatile ("nop");
 asm volatile ("nop");
 asm volatile ("nop");
 asm volatile ("nop");
 asm volatile ("nop");
 uint8_t data=PINB;
 for(uint8_t i=0; i<8; i++){
 if(!(data&1)){
 nullen++;
 data= data>>1;
 }
 }
 }
 
 //print end of file
 print("Number of 0 bits: 0x");
 prinh(nullen>>24);
 prinh(nullen>>16);
 prinh(nullen>>8);
 prinh(nullen);
 nullen=(EPROM_TYPE*1024ul)-nullen;
 print("\r\nNumber of 1 bits: 0x");
 prinh(nullen>>24);
 prinh(nullen>>16);
 prinh(nullen>>8);
 prinh(nullen);
 princ('\r');
 princ('\n');
}

void wait_ms(uint16_t ms){
 //use 16 bit timer counter 1 in normal mode to wait for so much ms
 //the timer is run from 8MHz/8=1Mhz
 //e.g. for 50ms the timer runs from -1000*50+1=15537 to 65536, this is 50000 steps
 //so this takes exactly 50000 us
 TIFR|=1<<TOV1; //clear the Timer overflow flag (see documentation ATMEGA32A page 110: the flag is cleared by writing a one to the bit location)
 TCNT1=-1000*ms+1;
 TCCR1B=(1<<CS11);
 while(!(TIFR&(1<<TOV1))){}
 TCCR1B=0;//stop the timer
}

void wait_10us(){
 for(uint8_t i=0; i<20;i++){
 asm volatile ("nop");
 }
}

void write_eprom(){
 //note: every intel hex line starts with a colon (:).
 //Example: :10000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00
 //: colon
 //10 number of data bytes in hex
 //0000 adres in hex
 //00 instruction; 00=data; 01= end of file
 //FF... data themselves
 //00 checksum
 //the line length must be equal to 2 x the data length plus 11 
 char*ep=buffer;//end pointer
 while(ep[0]){ep++;}
 uint8_t length=ep-buffer;
 if(length<11) { print("Line too short\r\n"); print(buffer); print("\r\n"); return;}
 char*bp=buffer+1;
 while(bp!=ep) {
 if(((bp[0]<'0')||(bp[0]>'9')) && ((bp[0]<'A')||(bp[0]>'F'))){ print("Non-hex: "); print(buffer); print("\r\n"); return;}
 if(bp[0]>'9') bp[0]-=7;//convert A..F
 bp++;
 }
 uint8_t data_length=buffer[2]-'0'+16*(buffer[1]-'0');
 if(length<data_length*2+11){ print("Line too short for data\r\n"); print(buffer); print("\r\n"); return;}
 if(length>data_length*2+11){ print("Line too long for data\r\n"); print(buffer); print("\r\n"); return;}
 if(data_length>32){print ("Max data length is 32\r\n"); print(buffer); print("\r\n"); return;}
 uint8_t checksum=0;
 bp=buffer+1;
 for(uint8_t i=0; i<data_length+5; i++){
 checksum+=16*(bp[0]-'0');
 bp++;
 checksum+=bp[0]-'0';
 bp++;
 }
 
 //Check the checksum
 if(checksum){
 print("Checksum error:");
 prinh(-checksum);
 princ(' ');
 print(buffer);
 print("\r\n");
 return;
 }
 
 //check if it's a data line: has 00 in positions 7 and 8
 if((buffer[7]!='0')||(buffer[8]!='0')){
 print("Not data\r\n");
 return;
 }
 
 //print("Da:"); print(buffer); print("\r\n");
 
 //translate the characters to a byte buffer
 static uint8_t byte_buffer[32];
 char*poi=buffer+9;
 //fill the byte buffer
 //print("Bu:");
 for (uint8_t i=0; i<data_length; i++)
 { byte_buffer[i]=16*(poi[0]-'0');
 poi++;
 byte_buffer[i]+=(poi[0]-'0');
 poi++;
 //prinh(byte_buffer[i]);
 }
 //print("\r\n");
 
 //address
 uint8_t address_hi=16*(buffer[3]-'0')+buffer[4]-'0';
 uint8_t address_lo=16*(buffer[5]-'0')+buffer[6]-'0';
 //print("Ad:"); prinh(address_hi); prinh(address_lo); print("\r\n");
 PORTA=address_hi;
 
 PORTD|=(1<<EPROM_P); //P high
 //PORTD&=~(1<<EPROM_G); //G low

princ('W');
 for(uint8_t x=0; x<data_length; x++){
 PORTD|=(1<<EPROM_G); //G high
 PORTB=byte_buffer[x];
 //prinh(PORTB);
 DDRB=0xff;
 PORTC=address_lo;
 wait_ms(1);
 PORTD&=~(1<<EPROM_P);//low pulse on P
 wait_ms(50);
 PORTD|=(1<<EPROM_P);
 //princ('.');
 wait_10us();
 //read back
 DDRB=0;
 wait_10us();
 PORTD&=~(1<<EPROM_G);
 asm volatile ("nop"); //150ns
 uint8_t back=PINB;
 //prinh(back);
 if(back==byte_buffer[x]){princ('+');}else{princ('-');}
 address_lo++;
 }
 print("\r\n>");
}

//interrupt routine that handles data coming in on the serial connection
ISR(USART_RXC_vect){
 buffer_head[0]=UDR;
 if(puffer_fill<PUFFER_LENGTH/2)princ(buffer_head[0]);
 if(buffer_head[0]=='\r') {//return key pressed
 buffer_head[0]=0;//make the buffer usable as a null terminated string
 command_entered=1; //let the main loop know that there is complete line entered
 //princ('\n');//add linefeed to the return
 }
 if(((buffer_head[0]==0)||(buffer_head[0]>=' '))&&(buffer_head!=buffer_end))buffer_head++;//advance the pointer for the next character
 //if(buffer_head==buffer_end)princ('<');//overflow
}




//serial output routines
void princ(char c){
 while(puffer_fill==PUFFER_LENGTH){}//if the output buffer is full, loop until there is room
 puffer_head[0]=c;
 puffer_head++;
 if(puffer_head==puffer_end)puffer_head=puffer;

uint8_t sreg_saved=SREG;
 cli();//next instructions not interruptable
 if(UCSRA & (1<<UDRE)){
 UDR = puffer_tail[0];
 puffer_tail++;
 if(puffer_tail==puffer_end)puffer_tail=puffer;
 }else puffer_fill++;
 if(sreg_saved&(1<<7)) sei();//RESTORE PREVIOUS INTERRUPT ENabled/DISABLED STATE
}

void print(char*s){
 while(s[0]){
 princ(s[0]);
 s++;
 }
}
void prinh(uint8_t h){
 char c=(h>>4)+'0';
 if(c>'9')c+=7;
 princ(c);
 char c2=(h&0xf)+'0';
 if(c2>'9')c2+=7;
 princ(c2);
}

//interrupt routine that is called when serial output is complete
ISR(USART_UDRE_vect){
 if((UCSRA & (1<<UDRE))&&(puffer_fill)){
 UDR=puffer_tail[0];
 puffer_tail++;
 if(puffer_tail==puffer_end)puffer_tail=puffer;
 puffer_fill--;
 }
}

The ATMEGA32 fuse settings are:

lfuse: FB
hfuse: 95

 

Advertenties

6 Comments

Geef een reactie

Vul je gegevens in of klik op een icoon om in te loggen.

WordPress.com logo

Je reageert onder je WordPress.com account. Log uit /  Bijwerken )

Google photo

Je reageert onder je Google account. Log uit /  Bijwerken )

Twitter-afbeelding

Je reageert onder je Twitter account. Log uit /  Bijwerken )

Facebook foto

Je reageert onder je Facebook account. Log uit /  Bijwerken )

Verbinden met %s