Hand made Arduino reader for a 1980’s EPROM chip. Chip not shown in photo. Ribbon cables were not readily available, but some electrical tape made the wires manageable. Push button at top right initiates a full chip read with a hex dump to serial output.
My rather interesting new job involves maintaining some 1980’s full-motion flight simulator technology. The Digital Control Loading system for this simulator — the part which controls movement and force feedback of the flight controls — is made up of large TTL circuit cards (a Fokker system).
The mathematical procedures which calculate the feedback, etc., are not done in the main simulator computer, but are done by Channel CPU cards, which each contain an original intel 8086-2 microcontroller. Those microcontrollers run firmware which is burned onto old 32k and 64k EPROM memory chips.
1980’s era Channel CPU card. Performs mathematical functions to calculate positioning and force-feedback for flight simulator controls. Original Intel 8086-2 microcontroller at top left. 32KB and 64KB EPROM chips are the bottom four chips with labels on them.
We don’t have the source code for the math on the EPROMs, so we needed to make backups of the data on them. I was able without too much difficulty to put together a reader for the 32KB chips with an Arduino Mega and a few spare parts. I haven’t done the 64KB reader yet, but I do not think it will be difficult.
Reading these old EPROMs is straightforward: basically just setting address pins to the byte address you want, toggling the OUTPUT ENABLE pin, and then reading the byte from the 8 output pins. I have a button inserted onto the Mega which causes it to begin reading all the bytes on by one and dumping them to the serial output, in an ASCII format (0xHH).
Creating an Arduino programmer (burner) for the EPROM chips will be more challenging, I expect, since programming them requires higher voltage pulses (21V for the 32KB chip). But I should be able to manage that with a relay, I’m thinking.
In summary, it was an interesting and fun project illustrating how Arduino / AVR skills can be used in the work-a-day world.
Eight-digit seven-segment display driven by Uno SPI.
I have been experimenting with these 8 digit displays driven by the Max7219 driver chip. The driver chip allows you to control the segments with persistent serial data commands, rather than having to drive the segments yourself regularly from your main microcontroller.
They are driven over an SPI bus, which requires only three lines, not including vcc and gnd. SPI is built into the 328P microcontroller on the UNO. One line is for the serial data out, one is a synchronizing clock, and one line is the Chip Select line. (SPI uses another line for serial data in, but I don’t need that in this case.) The Max7219 doesn’t actually have full SPI compatibility, as it does not have a Chip Select line, but rather a LOAD line, which is a little different, and is problematic if you wanted to put more than one device on your SPI bus. I ordered some Max7222 chips also, which have full SPI compatibility; but in this use case it doesn’t really matter as the Max7219 drivers support daisy chaining of the shift registers, meaning I can hook up multiple 8 digital displays using only one SPI device slot.
The arduino reference material on the SPI library is a little confusing, so it took some time for me to figure out how to program the SPI communication correctly. Also, I was confused for a while because the display kept showing up as randomly garbled. It turned out that there wasn’t really anything wrong fundamentally with my code, but just when the Max7219 boots up, the segment registered are pre-filled with seemingly random data, so I needed to blank or overwrite the digits first to see only what you want to see.
Here is the test code.
/*
Copyright 2020 Christopher Howard
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include <SPI.h>
#define CH_H 0b00110111
#define CH_I 0b00010000
#define CH_BLK 0b00000000
#define CH_0 0b01111110
#define CH_1 0b00110000
#define CH_2 0b01101101
#define CH_3 0b01111001
#define CH_4 0b00110011
#define CH_POINT 0b10000000
void setup ()
{
pinMode(8, OUTPUT);
SPI.begin();
SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0));
digitalWrite(8, LOW);
/* Normal Mode */
SPI.transfer(0b00001100);
SPI.transfer(0b00000001);
digitalWrite(8, HIGH);
digitalWrite(8, LOW);
/* Scan limit: all digits */
SPI.transfer(0b00001011);
SPI.transfer(0b00000111);
digitalWrite(8, HIGH);
digitalWrite(8, LOW);
/* 25/32 intensity */
SPI.transfer(0b00001010);
SPI.transfer(0b00001100);
digitalWrite(8, HIGH);
/* Write some characters */
digitalWrite(8, LOW);
SPI.transfer(0b00000001);
SPI.transfer(CH_0);
digitalWrite(8, HIGH);
digitalWrite(8, LOW);
SPI.transfer(0b00000010);
SPI.transfer(CH_1);
digitalWrite(8, HIGH);
digitalWrite(8, LOW);
SPI.transfer(0b00000011);
SPI.transfer(CH_2 | CH_POINT);
digitalWrite(8, HIGH);
digitalWrite(8, LOW);
SPI.transfer(0b00000100);
SPI.transfer(CH_3);
digitalWrite(8, HIGH);
digitalWrite(8, LOW);
SPI.transfer(0b00000101);
SPI.transfer(CH_4 | CH_POINT);
digitalWrite(8, HIGH);
digitalWrite(8, LOW);
SPI.transfer(0b00000110);
SPI.transfer(CH_BLK);
digitalWrite(8, HIGH);
digitalWrite(8, LOW);
SPI.transfer(0b00000111);
SPI.transfer(CH_I);
digitalWrite(8, HIGH);
digitalWrite(8, LOW);
SPI.transfer(0b00001000);
SPI.transfer(CH_H);
digitalWrite(8, HIGH);
SPI.endTransaction();
SPI.end();
}
void loop ()
{
}
I’m having lots of fun diving into this atmel microcontroller stuff. I might need to work on wire management at some point, however…
Now that I have a four-digit hex display that can easily display any sixteen-bit value from memory, naturally I wanted to tie that to something in the real world. The easy choice was a photoresistor, which I tied to A0 pin.
The max value that can be returned from analogRead is 1024, which is 0x3FF on my hex display. In my natural level of room lighting, and in conjunction with a 30k resistor, my readout rests at about 0x18E, about 30% of max value. If I shine my flashlight on the photoresistor, that brings it up to about 0x36D, and covering it with a piece of paper brings it down to about 0x043.
With this adventure I also took the time to learn about and make use of custom timer interrupts. At my day job, I maintain a rather ancient piece of simulator technology, so I spend a fair amount of time reading 1980’s programming and electronics reference manuals, in which there is a heavy emphasis on the precisely-choreographed timing of signals. Consequently, I’ve developed something of a fascination/obsession with timing in circuits.
Anyway, I wanted my main-loop activities to be driven by a steady interrupt signal outside of the loop itself, so I used the 328P’s 16-bit #1 timer (328P microcontroller has three timers available, 0, 1, and 2). With a 16 Mhz clock, x8 prescaler, and OCR value of 20000 (CTC), that provides a precisely 100 Hz interrupt, i.e., an interrupt every 10 ms. 100 Hz was the fastest I needed to loop in order to provide a good frame rate for the hex display. The interrupt simply sets a boolean value, acting as a sort of timing pulse for the main loop.
Ten milliseconds would be plenty of time to accomplish very many operations, even on a 16Mhz microcontroller, but in my case I just needed to drive the display and read a value from A0 pin. The analogRead function needs about 100 microseconds, according to the Arduino reference. I found I needed about 250 microseconds per hex digit in order to provide sufficient voltage to the digits in a 10ms loop, if I remove the resistors.
A side lesson here is that you don’t need resistors for your LEDs if you carefully control the duty cycle of voltage to them. An interesting implication of that would be that increasing your voltage would allow you to decrease you duty cycle as much as you want, i.e., to use shorter voltage pulses.
Here are some helpful links on understanding and using timer interrupts:
/*
Copyright 2020 Christopher Howard
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/* Pins 1-12 on MSQ6X41C should be mapped to pins 2-13 on Uno */
#define LP1 2
#define LP2 3
#define LP3 4
#define LP4 5
#define LP5 6
#define LP6 7
#define LP7 8
#define LP8 9
#define LP9 10
#define LP10 11
#define LP11 12
#define LP12 13
#define SA LP11
#define SB LP7
#define SC LP4
#define SD LP2
#define SE LP1
#define SF LP10
#define SG LP5
#define SDOT LP3
enum characters
{
ZERO, ONE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT,
NINE, ALPHA, BRAVO, CHARLIE, DELTA, ECHO, FOXTROT, BLANK
};
enum digit {
D_ONE, D_TWO, D_THREE, D_FOUR
};
bool startPulse = false;
ISR(TIMER1_COMPA_vect) // timer compare interrupt service routine
{
startPulse = true;
}
void set_led_pins(byte ch, byte dig, bool point) {
/* blank all digits */
PORTD &= 0b10000011;
PORTD |= 0b10000000;
PORTB &= 0b11100110;
PORTB |= 0b00100110;
switch(dig) {
case D_ONE:
PORTB &= 0b11011111;
break;
case D_TWO:
PORTB &= 0b11111011;
break;
case D_THREE:
PORTB &= 0b11111101;
break;
case D_FOUR:
PORTD &= 0b01111111;
break;
}
switch(ch)
{
case BLANK :
break;
case ZERO :
PORTD |= 0b00101100;
PORTB |= 0b00011001;
break;
case ONE :
PORTD |= 0b00100000;
PORTB |= 0b00000001;
break;
case TWO :
PORTD |= 0b01001100;
PORTB |= 0b00010001;
break;
case THREE :
PORTD |= 0b01101000;
PORTB |= 0b00010001;
break;
case FOUR :
PORTD |= 0b01100000;
PORTB |= 0b00001001;
break;
case FIVE :
PORTD |= 0b01101000;
PORTB |= 0b00011000;
break;
case SIX :
PORTD |= 0b01101100;
PORTB |= 0b00011000;
break;
case SEVEN :
PORTD |= 0b00100000;
PORTB |= 0b00010001;
break;
case EIGHT :
PORTD |= 0b01101100;
PORTB |= 0b00011001;
break;
case NINE :
PORTD |= 0b01101000;
PORTB |= 0b00011001;
break;
case ALPHA :
PORTD |= 0b01100100;
PORTB |= 0b00011001;
break;
case BRAVO :
PORTD |= 0b01101100;
PORTB |= 0b00001000;
break;
case CHARLIE :
PORTD |= 0b00001100;
PORTB |= 0b00011000;
break;
case DELTA :
PORTD |= 0b01101100;
PORTB |= 0b00000001;
break;
case ECHO :
PORTD |= 0b01001100;
PORTB |= 0b00011000;
break;
case FOXTROT :
PORTD |= 0b01000100;
PORTB |= 0b00011000;
break;
}
}
void setup()
{
pinMode(LP1, OUTPUT);
pinMode(LP2, OUTPUT);
pinMode(LP3, OUTPUT);
pinMode(LP4, OUTPUT);
pinMode(LP5, OUTPUT);
pinMode(LP6, OUTPUT);
pinMode(LP7, OUTPUT);
pinMode(LP8, OUTPUT);
pinMode(LP9, OUTPUT);
pinMode(LP10, OUTPUT);
pinMode(LP11, OUTPUT);
pinMode(LP12, OUTPUT);
// initialize timer1
noInterrupts(); // disable all interrupts
TCCR1A = 0;
TCCR1B = 0;
TCNT1 = 0;
OCR1A = 20000; // compare match register 16MHz/8/100Hz
TCCR1B |= (1 << WGM12); // CTC mode
TCCR1B |= (1 << CS11); // 8 prescaler
TIMSK1 |= (1 << OCIE1A); // enable timer compare interrupt
interrupts(); // enable all interrupts
}
byte readCounter = 0;
int av;
void loop()
{
if(startPulse)
{
startPulse = false;
/* stabilize the readout display */
if (readCounter == 9)
{
av = analogRead(A0);
readCounter = 0;
}
else
readCounter++;
set_led_pins((av >> 12) & 0xF, D_ONE, false);
delayMicroseconds(250);
set_led_pins((av >> 8) & 0xF, D_TWO, false);
delayMicroseconds(250);
set_led_pins((av >> 4) & 0xF, D_THREE, false);
delayMicroseconds(250);
set_led_pins(av & 0xF, D_FOUR, false);
delayMicroseconds(250);
set_led_pins(BLANK, D_FOUR, false);
}
}
You’ll see there that I am still using the built-in delay-timer functionality to create some delay in-between setting led pins. But the loop itself is initiated by a “pulse” to the startPulse variable which is set by my custom interrupt timer.
A cathode voltage pulse after optimizing the code with direct register writes. Ideal (delay only) pulse would be 1 ms in length according to the loop I set up, which almost matches what we see here (referring to the downward pulse).
The assumed way of setting digital pins in Arduino is to use the digitalWrite procedure to set each pin. That is slow, however, as you make a procedure call (presumably — I’m not sure exactly how much inlining happens in the compiler) and also you have to set one pin at a time.
However, since the pins map to bits that are stuffed into eight-bit registers in the microcontroller, you can just write a byte to each register and change multiple pins at once. And it is just two clock cycles for that operation.
The tradeoff is you have to look up the processor register mappings, and have decent bit toggling skills.
E.g., the code to set eight pins low and four pins high, i.e., to “reset” the LED anodes and cathodes, becomes…
For learning purposes I am programming my Uno to drive a hex display on a MSQ6441C Four Digit Stick Display. The display has no built in driver, so you must drive it by controlling voltages to anodes and cathodes. At first, I assumed this would be quite simple, but to be honest it was more challenging then I expected. Due to the common cathode design (reducing the number of pins required) you are not simply setting all the pins you want at once, but instead you must cycle through each of the cathodes, setting one digit at a time while blanking the others. So, you must take advantage of “flicker fusion” in human vision, similar to what is done in movies and animation.
At this point, I could either tie this functionality into a processor interrupt, so the Arduino could do other tasks as well, or set up an SPI communication line so that another microcontroller could send simple commands to the driver microcontroller. The Atmega328P of the Uno seems a bit overkill for just running an LED display, however.
Here is some code for those interested. Pins 1-12 on the display should be mapped into pins 2-13 on the Uno. The code could be improved by replacing those expensive digitalWrite calls with assignments directly into the digital pin registers. Also I have not gotten around to coding the dot segment yet.
/*
Copyright 2020 Christopher Howard
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#define LP1 2
#define LP2 3
#define LP3 4
#define LP4 5
#define LP5 6
#define LP6 7
#define LP7 8
#define LP8 9
#define LP9 10
#define LP10 11
#define LP11 12
#define LP12 13
#define SA LP11
#define SB LP7
#define SC LP4
#define SD LP2
#define SE LP1
#define SF LP10
#define SG LP5
#define SDOT LP3
enum characters
{
ZERO, ONE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT,
NINE, ALPHA, BRAVO, CHARLIE, DELTA, ECHO, FOXTROT, BLANK
};
enum digit {
D_ONE, D_TWO, D_THREE, D_FOUR
};
void setup() {
pinMode(2, OUTPUT);
pinMode(3, OUTPUT);
pinMode(4, OUTPUT);
pinMode(5, OUTPUT);
pinMode(6, OUTPUT);
pinMode(7, OUTPUT);
pinMode(8, OUTPUT);
pinMode(9, OUTPUT);
pinMode(10, OUTPUT);
pinMode(11, OUTPUT);
pinMode(12, OUTPUT);
pinMode(13, OUTPUT);
}
void set_led_pins(byte ch, byte dig, bool point) {
digitalWrite(LP1, LOW);
digitalWrite(LP2, LOW);
digitalWrite(LP3, LOW);
digitalWrite(LP4, LOW);
digitalWrite(LP5, LOW);
digitalWrite(LP7, LOW);
digitalWrite(LP10, LOW);
digitalWrite(LP11, LOW);
digitalWrite(LP6, HIGH);
digitalWrite(LP8, HIGH);
digitalWrite(LP9, HIGH);
digitalWrite(LP12, HIGH);
switch(dig) {
case D_ONE:
digitalWrite(LP12, LOW);
break;
case D_TWO:
digitalWrite(LP9, LOW);
break;
case D_THREE:
digitalWrite(LP8, LOW);
break;
case D_FOUR:
digitalWrite(LP6, LOW);
break;
}
switch(ch)
{
case BLANK :
break;
case ZERO :
digitalWrite(SA, HIGH);
digitalWrite(SB, HIGH);
digitalWrite(SF, HIGH);
digitalWrite(SC, HIGH);
digitalWrite(SD, HIGH);
digitalWrite(SE, HIGH);
break;
case ONE :
digitalWrite(SB, HIGH);
digitalWrite(SC, HIGH);
break;
case TWO :
digitalWrite(SA, HIGH);
digitalWrite(SB, HIGH);
digitalWrite(SG, HIGH);
digitalWrite(SE, HIGH);
digitalWrite(SD, HIGH);
break;
case THREE :
digitalWrite(SA, HIGH);
digitalWrite(SB, HIGH);
digitalWrite(SG, HIGH);
digitalWrite(SC, HIGH);
digitalWrite(SD, HIGH);
break;
case FOUR :
digitalWrite(SF, HIGH);
digitalWrite(SG, HIGH);
digitalWrite(SB, HIGH);
digitalWrite(SC, HIGH);
break;
case FIVE :
digitalWrite(SA, HIGH);
digitalWrite(SF, HIGH);
digitalWrite(SG, HIGH);
digitalWrite(SC, HIGH);
digitalWrite(SD, HIGH);
break;
case SIX :
digitalWrite(SA, HIGH);
digitalWrite(SF, HIGH);
digitalWrite(SG, HIGH);
digitalWrite(SC, HIGH);
digitalWrite(SD, HIGH);
digitalWrite(SE, HIGH);
break;
case SEVEN :
digitalWrite(SA, HIGH);
digitalWrite(SB, HIGH);
digitalWrite(SC, HIGH);
break;
case EIGHT :
digitalWrite(SA, HIGH);
digitalWrite(SB, HIGH);
digitalWrite(SF, HIGH);
digitalWrite(SG, HIGH);
digitalWrite(SC, HIGH);
digitalWrite(SD, HIGH);
digitalWrite(SE, HIGH);
break;
case NINE :
digitalWrite(SA, HIGH);
digitalWrite(SB, HIGH);
digitalWrite(SF, HIGH);
digitalWrite(SG, HIGH);
digitalWrite(SC, HIGH);
digitalWrite(SD, HIGH);
break;
case ALPHA :
digitalWrite(SA, HIGH);
digitalWrite(SB, HIGH);
digitalWrite(SF, HIGH);
digitalWrite(SG, HIGH);
digitalWrite(SC, HIGH);
digitalWrite(SE, HIGH);
break;
case BRAVO :
digitalWrite(SF, HIGH);
digitalWrite(SG, HIGH);
digitalWrite(SC, HIGH);
digitalWrite(SD, HIGH);
digitalWrite(SE, HIGH);
break;
case CHARLIE :
digitalWrite(SF, HIGH);
digitalWrite(SA, HIGH);
digitalWrite(SD, HIGH);
digitalWrite(SE, HIGH);
break;
case DELTA :
digitalWrite(SB, HIGH);
digitalWrite(SG, HIGH);
digitalWrite(SC, HIGH);
digitalWrite(SD, HIGH);
digitalWrite(SE, HIGH);
break;
case ECHO :
digitalWrite(SA, HIGH);
digitalWrite(SF, HIGH);
digitalWrite(SG, HIGH);
digitalWrite(SE, HIGH);
digitalWrite(SD, HIGH);
break;
case FOXTROT :
digitalWrite(SA, HIGH);
digitalWrite(SF, HIGH);
digitalWrite(SG, HIGH);
digitalWrite(SE, HIGH);
break;
}
}
byte tc = 0;
unsigned int dc = 0;
void loop() {
set_led_pins((dc >> 12) & 0xF, D_ONE, false);
delay(1);
set_led_pins((dc >> 8) & 0xF, D_TWO, false);
delay(1);
set_led_pins((dc >> 4) & 0xF, D_THREE, false);
delay(1);
set_led_pins(dc & 0xF, D_FOUR, false);
delay(1);
tc++;
if(tc % 64 == 0) dc++;
/* PORTB &= ~(1<<PB0); */
/* PORTB |= (1<<PB0); */
}