Ничего не найдено :(
    В гостях у Самоделкина! » Электроника » Arduino » Разбираем MaxLogic MX656. Часть 1 - память DRAM

    Разбираем MaxLogic MX656. Часть 1 - память DRAM

    Введение


    На днях мне в руки попалась плата графического адаптера для 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:

    DRAM 41256:

    DRAM TC511001 / HM511000 / HM511001 / MSM411000RS / MSM411001RS / D421000C / D421001C:

    DRAM HYB-514256B

    Яркой отличительной особенностью многих из этих проектов является то, что они НЕ работают  smile

    Например, в данном проекте автор записывает номера строк в ячейки памяти (побитово) с помощью функции writeBits. Затем он просто считывает 256 бит из ячеек памяти, в которые он якобы что-то записал. Функция readBits замечательно считывает из памяти единицы, которые там и были изначально. "Test DONE. All OK!"  smile 

    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. Но я склонен говорить, что это из-за тайминга.

    Интересно, что код, предложенный данным автором, у меня не заработал вообще. В нем явно меньше обращений к управляющим выводам, чем нужно. Тем не менее, такая проблема существует. Поиски ее решения мы продолжим в следующих статьях.

    Ссылки

    Код программы в AVR-стиле

    Код программы в Arduino-стиле

    Проект KiCAD 6

    Становитесь автором сайта, публикуйте собственные статьи, описания самоделок с оплатой за текст. Подробнее здесь.
    Подборки: DRAM HY53C464 MaxLogic MX656

    Портативный паяльник на Arduino и двух 18650

    Аналог Watchdog-а на Ардуино

    0
    Идея
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    0
    Описание
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    0
    Исполнение
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    Итоговая оценка: 0.0 из 10 (голосов: 0 / История оценок)

    Добавить комментарий

    2 комментария
    Гость Сергей #83611

    Да, уж. Уважаемый, вы понимаете, что такое DRAM ?  Про цикл регенерации хранимых данных у такой памяти ничего не слышали?

    ino53 #81515

    Да, адаптер из серии ретро... даже во времена моей молодости память на отдельных микросхемах уже была редкостью. На кружке на стене долго висела как музейный экспонат материнка IBM AT, память четверть, а то и треть платы занимала, зато процессор был без карлсона. pardon 

    Привет, Гость!


    Зарегистрируйтесь

    Или войдите на сайт, если уже зарегистрированы...

    Войти

    Добавьте самоделку

    Добавьте тему

    Онлайн чат

    Последние комментарии

    Все комментарии