Введение
На днях мне в руки попалась плата графического адаптера для EGA-монитора MaxLogic MX656 (старинный аналог современной видеокарты).
Судя по внешнему виду это был графический адаптер для цветного монитора. Далее привожу цитату из википедии.
При подключении цветного монитора EGA использовал частоту кадров в 60 Гц, и мог использовать одну из двух частот строк — 21,8 кГц для 350 строк (текстовые режимы с размером знакоместа 8×14 пикселей, и режимы 640×350×16 и 640×350×4) и 15,7 кГц для текстовых режимов с размером знакоместа 8×8 пикселей и графических режимов с 200 строк. При подключении монохромного монитора вырабатывал сигналы со стандартной для монохромного монитора частотой строк 18,43 кГц и частотой кадров 50 Гц. Тип монитора устанавливался на банке переключателей, доступном через отверстие в задней планке (брэкете).
Можно заметить, что MaxLogic MX656 работала в несколько иных режимах, поскольку на плате видно четыре кристалла, ответственных за генерацию частот 34, 24, 16.257 и 19 МГц. Судя по всему, это гораздо более поздняя плата. Переключатель режимов имеется, на изображении он в правом верхнем углу.
Особого пиетета у меня к ней не было (некоторые отдают подобное в музеи электроники), и я решил по-максимуму разобрать ее, использовав полезные части в самоделках или хотя бы заставив их работать независимо от платы. Покрутив в руках эту плату я обнаружил следующие полезные компоненты:
- выводные керамические конденсаторы - 22 шт.;
- DIP-панельки - 9 шт.;
- кварцевые генераторы (JAPAN) - 4 шт.;
- движковый переключатель - 1 шт.;
- память EPROM 27С256 (USA, стирается ультрафиолетом!) - 1 шт.;
- память DRAM HY53C494-10 (KOREA, HYUNDAI) - 8 шт.
Собственно, четыре первых пункта можно применить в самоделках, а из двух последних - сделать самоделки. В этот раз попробуем заставить работать память DRAM HY53C464S-10 с помощью Arduino. Эта память интересна тем, что она динамическая, конденсаторная, и работает на принципе регенерации - микроскопическим конденсаторам требуется регулярная подзарядка. То есть при отключении питания эта память стирается.
Для ускорения работы кода мы будем работать с Arduino как с микроконтроллером AVR. Необходимость этого связана с тем, что медленная память DRAM работает быстрее, чем команды Arduino Framework способны обрабатывать запросы к ней в цикле.
Существующие проекты работы с памятью DRAM с помощью Arduino
Я не первый, кто пытался воскресить DRAM с помощью Arduino. Существует несколько аналогичных проектов для работы с еще более старой памяти DRAM 4164:
- Arduino and 4116 DRAM
- DRAMARDUINO - Dram tester with Arduino
- DRAM Test Shield for Arduino Uno and Nano
- 4164 Dynamic RAM with Arduino
- DRAM_Tester_Arduino
DRAM 41256:
DRAM TC511001 / HM511000 / HM511001 / MSM411000RS / MSM411001RS / D421000C / D421001C:
DRAM HYB-514256B
Яркой отличительной особенностью многих из этих проектов является то, что они НЕ работают
Например, в данном проекте автор записывает номера строк в ячейки памяти (побитово) с помощью функции writeBits. Затем он просто считывает 256 бит из ячеек памяти, в которые он якобы что-то записал. Функция readBits замечательно считывает из памяти единицы, которые там и были изначально. "Test DONE. All OK!"
for(int row=0; row<=255; row++) {
Serial.println("Testing row: " + String(row));
writeBits(row);
int numberOfBits = readBits(row);
if (numberOfBits != 256) {
digitalWrite(STATUS_LED, HIGH);
Serial.println("ERROR: row " + String(row) + " number of bits was: " + String(numberOfBits)) + ", but should be 255.";
while(1);
}
}
Serial.println("Test DONE. All OK!");
void writeBits(int row) {
// Pull RAS and CAS HIGH
digitalWrite(RAS, HIGH);
digitalWrite(CAS, HIGH);
// Loop though all the columns
for (int i=0; i<=255; i++) {
// Set row address
digitalWrite(A0, bitRead(row, 0));
digitalWrite(A1, bitRead(row, 1));
digitalWrite(A2, bitRead(row, 2));
digitalWrite(A3, bitRead(row, 3));
digitalWrite(A4, bitRead(row, 4));
digitalWrite(A5, bitRead(row, 5));
digitalWrite(10, bitRead(row, 6));
digitalWrite(11, bitRead(row, 7));
// Pull RAS LOW
digitalWrite(RAS, LOW);
// Pull Write LOW (Enables write)
digitalWrite(WRITE, LOW);
// Set Data in pin to HIGH (write a one)
digitalWrite(D, HIGH);
// Set column address
digitalWrite(A0, bitRead(i, 0));
digitalWrite(A1, bitRead(i, 1));
digitalWrite(A2, bitRead(i, 2));
digitalWrite(A3, bitRead(i, 3));
digitalWrite(A4, bitRead(i, 4));
digitalWrite(A5, bitRead(i, 5));
digitalWrite(10, bitRead(i, 6));
digitalWrite(11, bitRead(i, 7));
// Pull CAS LOW
digitalWrite(CAS, LOW);
digitalWrite(RAS, HIGH);
digitalWrite(CAS, HIGH);
}
}
int readBits(int row) {
// Bit counter
int numberOfBits = 0;
// Pull RAS, CAS and Write HIGH
digitalWrite(RAS, HIGH);
digitalWrite(CAS, HIGH);
digitalWrite(WRITE, HIGH);
// Loop though all the columns
for (int i=0; i<=255; i++) {
// Set row address
digitalWrite(A0, bitRead(row, 0));
digitalWrite(A1, bitRead(row, 1));
digitalWrite(A2, bitRead(row, 2));
digitalWrite(A3, bitRead(row, 3));
digitalWrite(A4, bitRead(row, 4));
digitalWrite(A5, bitRead(row, 5));
digitalWrite(10, bitRead(row, 6));
digitalWrite(11, bitRead(row, 7));
// Pull RAS LOW
digitalWrite(RAS, LOW);
// Set column address
digitalWrite(A0, bitRead(i, 0));
digitalWrite(A1, bitRead(i, 1));
digitalWrite(A2, bitRead(i, 2));
digitalWrite(A3, bitRead(i, 3));
digitalWrite(A4, bitRead(i, 4));
digitalWrite(A5, bitRead(i, 5));
digitalWrite(10, bitRead(i, 6));
digitalWrite(11, bitRead(i, 7));
// Pull CAS LOW
digitalWrite(CAS, LOW);
// Read the stored bit and add to bit counter
numberOfBits += digitalRead(Q);
// Pull RAS and CAS HIGH
digitalWrite(RAS, HIGH);
digitalWrite(CAS, HIGH);
}
return numberOfBits;
}
Принцип работы памяти DRAM
Все упомянутые варианты памяти исполнены в DIP-корпусах, но имеют следующие важные для нас отличия:
- количество выводов (16, 18 или 20);
- количество пинов ввода/вывода;
- наличие Output Enable.
В целом все варианты памяти DRAM работают аналогично, потому что производители Siemens, Hyunday, Samsung, Sony, Hitachi просто передирали порядок и тайминги подачи напряжения на выводы микросхемы друг у друга. Ниже привожу примеры таймингов на чтение и запись для тестируемой памяти HY53C464S-10.
Принцип действия DRAM примерно такой:
- Имеем двумерный массив ячеек памяти с индексами RAS/CAS (строка/столбец).
- Имеем 3-4 инвертированных (активны при отсутствии напряжения, логическая единица при нуле Вольт) управляющих вывода RAS, CAS, WE (Write Enable) и, иногда, OE (Output Enable);
- Имеем 8-9 неинвертированных выводов A0-A7 (A8) для установки адреса. В зависимости от их количества у нас есть (RAS) 1111 1111 x (CAS) 1111 1111 (256 x 256) = 65 536 ячеек памяти или (RAS) 1 1111 1111 x 1 1111 1111 (512 x 512) = 262 144 ячеек памяти;
- Имеем 2 или 4 неинвертированных вывода Din / Dout или IO1-4. В зависимости от этого мы можем писать в ячейку памяти или считывать из нее 1 бит (0 или 1) или 4 бита (0000 - 1111, 0 - 15, 0x0 - 0xF). Безусловно записывать 4 бита в одну ячейку более перспективно, так как это распараллеливает задачи, ускоряет работу памяти и позволяет писать в нее что-то в небинарном виде;
- Чтобы установить текущий адрес ячейки, что-то считать из нее или записать в нее, нужно предварительно в определенной последовательности подать логическую единицу на управляющие выводы;
- Подачей логической единицы на соответствующие выводы A устанавливается адрес RAS, например 0100 1100, затем CAS, например 1000 0001;
- Считываем с Dout состояние текущей ячейки памяти или записываем в Din 0 или 1. Если у нас 4-битовая память, то из IO считываем 4 бита или записываем (например такое, 0101), путем подачи логических единиц на соответствующие выводы. Читаем мы или записываем в данном случае помогает определять OE.
Исходя из вышеописанного имеем четыре основных типа памяти, которые пытались заставить корректно работать разные авторы:
- Samsung (KM4164B) - 16 выводов, 1 бит, 65 536 адресов (64 кбит);
- Hitachi (HM50256) - 16 выводов, 1 бит, 262 144 адресов (256 кбит);
- Hyunday (HY53C464) - 18 выводов, 4 бит, 65 536 адресов (64 кбит x 4);
- Siemens (HYB514256B) - 20 выводов, 4 бит, 262 144 адресов (256 кбит x 4).
Проектирование тестовой платы
Проект тестовой платы был создан в KiCAD 6. Плата создавалась в тот момент, когда я еще не вполне представлял себе, как именно работают выводы IO у HY53C464 (толковое описание этого в интернете и мануалах на память попросту отсутствует), поэтому она проектировалась исходя из того, что один пин IO будет ответственным за запись, а другой за считывание (два других не использовались). Потом я все-таки нашел как это работает, проанализировав проект Amiga DRAM chip tester for HYB-514256B with Arduino UNO. Поэтому, чтобы не переделывать плату, я просто соединил два недостающих пина проводами.
Лучшим решением в данном случае является использование голого микроконтроллера ATMega328p, чтобы к выводам PB0-PB7 подключались адресные выводы HY53C464 A0-A7, к PC0-PC3 - управляющие выводы RAS, CAS, WE, OE, а к PD2-PD5 - IO0 - IO3. Это влечет за собой минимизацию количества сдвиговых операций в коде.
Но в Arduino у нас PB6 и PB7 отданы под тактирование внешним кварцевым генератором, и это не дает возможность подключить все адресные выводы к порту D. В ATMega328p PD0 и PD1 - это UART (без него ничего не прошьете), а PC6 - это RESET, который тоже трогать нежелательно, поэтому подключение адресных выводов к портам C и D отпадает автоматически. В итоге, если использовать Arduino, приходится делить адресные выводы по двум портам. Изначально меня это мало заботило, потому что в коде Arduino используется цифровая нумерация пинов, а вот при переходе на AVR это несколько усложнило код, потребовав много сдвиговых операций.
Изготовление тестовой платы
1. Распечатываем на глянцевой фотобумаге с помощью лазерного принтера, фольгированный стеклотекстолит обрабатываем наждачкой, чтобы снять окисел.
2. С помощью утюга переносим тонер с глянцевой фотобумаги на фольгированный стеклотекстолит. Маркером подкрашиваем дорожки, которые плохо перенеслись.
3. Травим плату в растворе хлорного железа безводного.
4. Удаляем тонер с помощью универсального обезжиривателя.
5. Производим сверловку гравером со сверлами диаметром 1 и 0.8 мм, припаиваем перемычки из набора для ардуино, которые выступают в роли front-слоя, припаиваем DIP-панельку и гнезда штыревые для штыревых вилок Arduino Nano.
6. Припаиваем два недостающих пина проводами, припаиваем резистор, светодиод и вставляем Arduino Nano и память в розетки.
Светодиоды ставлю вниз, потому что не люблю, когда светят прямо в глаза - стеклотекстолит все равно полупрозрачный после травления.
Программирование
Это наиболее важная часть работы, потому что сделать тестовую плату много ума не надо, а вот заставить правильно взаимодействовать Arduino и память DRAM - это очень сложная и кропотливая работа, которая требует правильного переноса из мануалов таймингов активации выводов памяти.
К счастью, тайминги HY53C464 оказались идентичными таймингам HYB514256B, поэтому я решил взять за основу код ProjectDRAM / 514256B, значительно ускорив его.
Сначала мы прописываем некоторые шаблоны, которые позволят делать быстрые переключения состояний управляющих выводов. Также мы прописываем маски для установки состояний портов IO. Вот так по-дурацки с ними получилось: IO0 - IO3 соответствуют PB1, PC3, PB2, PC4.
#define RAS_HIGH() PORTB |= (1 << PB5) // D13 HIGH
#define RAS_LOW() PORTB &= ~(1 << PB5) // D13 LOW
#define CAS_HIGH() PORTB |= (1 << PB4) // D12 HIGH
#define CAS_LOW() PORTB &= ~(1 << PB4) // D12 LOW
#define WE_HIGH() PORTB |= (1 << PB3) // D11 HIGH
#define WE_LOW() PORTB &= ~(1 << PB3) // D11 LOW
#define OE_HIGH() PORTC |= (1 << PC2) // A2 HIGH
#define OE_LOW() PORTC &= ~(1 << PC2) // A2 LOW
// Маски для установки состояний портов IO, подключеных к регистрам B и C
#define DATA_MASK_B (0b00000110) // PB1(I/O0), PB2(I/O2)
#define DATA_MASK_C (0b00011000) // PC3(I/O1), PC4(I/O3)
В оригинальном коде автор объявляет два массива для перебора номеров выводов Arduino в циклах.
const int adr_pins[8] = {2, 3, 4, 5, 6, 7, A0, A1}; // ARDUINO PINS
const int data_pins[4] = {9, A3, 10, A4}; // ARDUINO PINS
У нас нет этого маразма.
Сетап у него выглядит так.
void setup() {
Serial.begin(9600);
pinMode(LED_BUILTIN, OUTPUT);
for(int n = 0; n < 9; n++)
pinMode(adr_pins[n], OUTPUT);
pinMode(RAS, OUTPUT);
pinMode(CAS,OUTPUT);
pinMode(OE, OUTPUT);
pinMode(WE, OUTPUT);
digitalWrite(RAS, HIGH); // disable
digitalWrite(CAS,HIGH); // disable
digitalWrite(WE,HIGH); // disable
digitalWrite(OE,HIGH); // disable
Serial.println("DRAM 64k x 4 tester.");
noInterrupts();
for (int i = 0; i < (1 << ADDR_BUS_SIZE); i++) {
digitalWrite(RAS, LOW);
digitalWrite(RAS, HIGH);
}
} //**** setup ****
У нас выглядит так. Каждый pinMode и digitalWrite это ненужная потеря времени.
void setup() {
Serial.begin(9600);
pinMode(LED_BUILTIN, OUTPUT);
DDRD = 0b11111100; //Set digital pins 2-7 as lower part of address D2-D7 (HY0-HY5)
DDRC = 0b00011111; //Set analog pins A0-A2 as upper part of address
// OE, HY6, HY7, I/O1(PC3, A3, W-mode), I/O3(PC4, A4, W-mode)
DDRB = 0b00111111; //Set digital pins 9 - 13 as Control Lines and Data I/O
// LED (D8), I/O0 (PB1, D9, W-mode), I/O2 (PB2, D10, W-mode), WE(D11), CAS(D12), RAS(D13)
RAS_HIGH(); // disable
CAS_HIGH(); // disable
WE_HIGH(); // disable
OE_HIGH(); // disable
Serial.println("DRAM 64k x 4 tester.");
noInterrupts();
for (int i = 0; i < (1 << ADDR_BUS_SIZE); i++) {
RAS_LOW();
RAS_HIGH();
}
} //**** setup ****
Внутри цикла там 8 раз переключаются RAS и CAS, чтобы гарантированно обнулить память.
В основном цикле loop() мы просто пару раз включаем прерывания, пишем сообщение в последовательный порт, очищаем буфер последовательного порта, отключаем прерывания, заполняем массив памяти 2x2 одним и тем же числом (от 0 до 15).
Функция заполнения fill(int v, int beg) содержит в себе запись массива памяти 2x2 переданным числом writeValue(r, c, v) и последующее чтение этого массива readValue(r, c), чтобы подтвердить, что число записало в ячейки памяти. В обе функции мы передаем адрес текущей ячейки (r, c).
Адрес RAS или CAS устанавливается с помощью функции setBus(unsigned int a).
В оригинале.
void setBus(unsigned int a){
bool dbg=0; // debug
for (int i = 0; i < ADDR_BUS_SIZE; i++) {
digitalWrite(adr_pins[i],((a >> i) & 1));
if(dbg) Serial.print(((a >> i) & 1));
};
if (dbg)Serial.println(" setbus: a=" + String(a)+" ");
}
У нас.
void setBus(unsigned int a) {
bool dbg = 0; // debug
// Первые 6 бит D2-D7
PORTD |= (a << 2) & 0xFC; // Маска 1111 1100
// Следующие 2 бит A0-A1
PORTC |= (a >> 6) & 0x03; // Маска 0000 0011
if (dbg) {
Serial.println(" setbus: a=" + String(a)+" ");
}
}
Тут мы впервые смещаемся влево, чтобы получить адрес вида AA AAAA AA00 (где A - значащие цифры адреса) и наложить его на маску 1111 1100, которая устанавливает первые 6 бит адреса на пины D2-D7 (PD2-PD7).
Затем мы смещаемся вправо на 6, чтобы получить адрес вида 0000 00AA и применяем маску 0000 0011, которая устанавливает последние два бита адреса в PC0 и PC1.
Функция writeValue(int r, int c, int v) содержит необходимые переключения управляющих выводов, а также установку состояний выводов данных на запись.
// Set Data pins to output mode
DDRB |= DATA_MASK_B;
DDRC |= DATA_MASK_C;
// Set value to data pins
PORTB |= (v << 1) & 0b00000010;
PORTC |= (v << 2) & 0b00001000;
PORTB |= v & 0b00000100;
PORTC |= (v << 1) & 0b00010000;
Тут мы просто применяем уже объявленные ранее маски для установки IO pins в режим output. Далее даю пояснения по поводу сдвиговых операций. Исходно мы имеем число вида IO3 IO2 IO1 IO0. Нам его нужно записать в память через выводы PC4, PB2, PC3, PB1.
Сначала мы сдвигаемся влево на 1, получая IO3 IO2 IO1 IO0 0, применяе маску 0000 0010 и записывая PB1 в IO0.
Аналогично: IO3 IO2 IO1 IO0 0 0 + 0000 1000 записывает PC3 в IO1;
Аналогично: IO3 IO2 IO1 IO0 + 0000 0100 записывает PB2 в IO2;
Аналогично: IO3 IO2 IO1 IO0 0 + 0001 0000 записывает PC4 в IO3.
Ну то есть, понятно: в каком разряде сейчас единичка, туда и пишет.
Функция readValue(int r, int c) работает аналогично: сначала осуществляются переключения управляющих выводов, затем порты B и C переводятся в режим input, затем с помощью PINB и PINC получаем состояние выводов.
DDRB &= ~((1 << PB1) | (1 << PB2));
DDRC &= ~((1 << PC3) | (1 << PC4));
// I/O0(PB1) | I/O1(PC3) | I/O2(PB2) | I/O3(PC4)
// 0xF 0010 >> 1 | 1000 >> 2 | 0100 | 1 0000 >> 1
read_value = (PINB & (1 << 1)) >> 1 | (PINC & (1 << 3)) >> 2 | PINB & (1 << 2) | (PINC & (1 << 4)) >> 1;
Итоговое число read_value формируется со сдвигом, противоположным сдвигу в функции writeValue(). Соответствующие значения из PINB и PINC логически складываются.
Заключение
В итоге имеем следующий вывод в последовательный порт:
DRAM 64k x 4 tester.
1st Pass writting 0:
Write: r=0; c=0; value=0;
Read: r=0; c=0; value=0;
Write: r=0; c=1; value=0;
Read: r=0; c=1; value=0;
Write: r=1; c=0; value=0;
Read: r=1; c=0; value=0;
Write: r=1; c=1; value=0;
Read: r=1; c=1; value=0;
OK
2nd Pass writting 1:
Write: r=0; c=0; value=13;
Read: r=0; c=0; value=13;
Write: r=0; c=1; value=13;
Read: r=0; c=1; value=13;
Write: r=1; c=0; value=13;
Read: r=1; c=0; value=13;
Write: r=1; c=1; value=13;
Read: r=1; c=1; value=13;
OK
End.
Здесь мы пишем по адресам от 0000 0000 до 0000 0011 числа 0 и 13, и считываем их. Тест подтверждает работоспособность памяти именно в таком режиме. Если пытаться перезаписать эти же ячейки еще раз, то новое число в них не запишется. Если пробовать писать разные числа в соседние ячейки, тоже ничего не получается. Почему результат не соответствует ожидаемому я пока не понял.
Автор данной публикации также жаловался на то, что память работает нестабильно. Машинный перевод:
В случае с.о. заинтересован, я нашел проблему с моим кодом. Пытаюсь читать с PORTB, но должен быть PINB, тогда все работает как надо. Однако следующая проблема заключается в том, что если я массово пишу и читаю DRAM, я получаю ошибки. Я реализовал цикл, в котором я просто записываю шаблоны в память и пытаюсь проверить их, читая потом. Через некоторое время я не могу прочитать значения, которые я записал в память, возможно, снова возникли проблемы с синхронизацией или неисправна микросхема DRAM. Но я склонен говорить, что это из-за тайминга.
Интересно, что код, предложенный данным автором, у меня не заработал вообще. В нем явно меньше обращений к управляющим выводам, чем нужно. Тем не менее, такая проблема существует. Поиски ее решения мы продолжим в следующих статьях.