Zx Spectrum От Коронавируса И Палочек, Часть 2 (Работаем Над Ошибками)

Ссылка на первую часть Прежде всего хочу попросить у уважаемой аудитории прощения за столь долгую паузу между первой частью и продолжением.

У меня есть для этого веская причина.

Если кто помнит, в первой части я упоминал, что сборка на макетных платах производилась из-за нежелания паять.

Я лгал.

Я люблю паять, но заключение (не то, что с кварками, а то, что с людьми) привело к тому, что у меня закончился припой.

Я, конечно, заказал сразу и на eBay, и на Али, но пришло оно только недавно.

Увы, то, что произошло в первой части, на беспаечных макетах работало крайне нестабильно, и о сборке продолжения на макетках не могло быть и речи.



ZX Spectrum от коронавируса и палочек, часть 2 (работаем над ошибками)

Обратите внимание, я искренне верю, что все мои поделки можно собрать на макетах.

Однако нужны действительно качественные макеты.

У меня их нет. Но теперь припой прибыл, можно продолжать.

Еще кое-что.

Я художник.

Будет много фотографий и даже видео.

Трафик! Однако для начала стоит определиться, что продолжать.

Как я писал в предыдущей части, мне почему-то не хочется собирать очередной клон Спектрума, ведь их сотни, и собирали их люди гораздо более образованные, чем я.

Еще раз напомню, что: Пожалуйста, относитесь ко всему, что я пишу, с недоверием.

Я дилетант в худшем смысле этого слова.

У меня нет никакого образования, соответствующего тому, о чем я пишу.

Если вы вдруг решите повторить то, что сделал я (нет, а вдруг?), знайте, что почти все, что здесь было сделано, будь то жесткое или мягкое, было сделано неправильно.

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

Так вот, одна из ошибок, совсем не очевидная для меня, была выявлена в комментарии уважаемого mpa4b : для тех частот, на которых работает поделка, необходима статическая CMOS-версия процессора Z80, а на моих фотографиях фигурирует NMOS-версия, и вся эта схема работает только на честном слове.

Кроме того, нужно куда-то воткнуть Arduino. Без Ардуино не интересно.

Это тоже говорит в пользу Arduino Очень нетривиальная организация видеопамяти у Specky .

Конечно, все это собирается оптом.

Но вам понадобится много дел; о беспаечных макетах можно забыть.

И не забывайте про Ардуино! Так что продолжим мучить 8-битный МК.

Ну а так как мы тоже паяем Ардуино, то надо придумать какой-нибудь подходящий форм-фактор.

Я подумал об этом и решил разделить будущий Спектрум на такие модули:

ZX Spectrum от коронавируса и палочек, часть 2 (работаем над ошибками)

Декодер верхних 32 КиБ ОЗУ сюда еще не припаян (экспериментировал со схемой), и соответственно сам чип не установлен Как и в Arduino, на разъем модуля выводятся все сигналы, необходимые для расширения системы: шины адреса и данных, сигналы управления процессором, выход декодера адреса, тактовый сигнал, питание.

Платы подключаются стандартным кабелем IDE. Это удобно (39 контактов хватает почти на все - у меня отдельный блок питания, там полно проводов от древних материнских плат, и эти провода, точнее, их 80-жильные варианты, с тонкими проводниками - идеальный источник провода для разводки сигналов на макетных платах!).

Основная плата содержит сам процессор Z80, ПЗУ, микросхему ОЗУ объемом 32 КиБ и декодер адреса.

Все.

Я с гордостью назвал эту поделку «процессорным модулем».

Давайте вспомним, на чем мы остановились в предыдущей части.

Там я попытался, с весьма спорным успехом, показать, насколько простыми были 8-битные компьютеры 80-х.

Как-то у меня это не очень получилось, поэтому повторю.

Почти все компьютеры состояли из ядра из небольшого количества микросхем.

Это был всего лишь процессор, ОЗУ, ПЗУ и немного логики для декодирования адреса.

Все.

Эти микросхемы были тупо соединены между собой общей шиной.

Конечно, это было только «ядро» компьютера, а дальше (видеовыход, звук, носители информации) все стало гораздо разнообразнее.

Но само ядро было везде почти одинаковое.

Таким образом, ZX Spectrum, MSX, Amstrad CPC были неотличимы по ядру, и именно способ вывода изображения, звука и ввода информации отличал один компьютер от другого.

Однако, как вещь сама по себе, практически любой 8-битный компьютер может работать в этой конфигурации с голым ядром, что мы и сделали в предыдущей части: компьютер, состоящий из 4-х чипов, выполнял программу из ПЗУ.

Да, я очень криво вытащил из памяти результат работы этого компьютера, но факт остается фактом: Спекки работал как минимум на 4 случаях.

И, конечно, это не относится исключительно к процессору Z-80, так что «собрать» скелет практически любого 8-битного компьютера прошлого можно практически на любом процессоре, и он будет работать.

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

Начнем со схемы.

Я потратил приличное количество времени, но, по крайней мере, разобрался с Орлом.

Теперь я могу не только писать, но и рисовать об электронике.

Вот так, например:

ZX Spectrum от коронавируса и палочек, часть 2 (работаем над ошибками)

Прошу не судить строго, до этого я видел Орла лишь мельком в роликах на Ютубе.

Буду рад любым конструктивным замечаниям и критике в комментариях.

Читатель, наделенный суперпамятью, наверное, помнит, что в предыдущей части я говорил, что модуль будет лишен оперативной памяти, и мы ограничимся 16-кибайтной версией Speckie, где собственной оперативной памяти процессора не было, но с ULA поделилось всего 16 КиБ, но, паять - так паять: я добавил еще один корпус на плату процессорного модуля и теперь карта памяти полностью соответствует 48К пятнышкам: 0x0000:0x3FFF — ПЗУ на плате процессора.

Линия ПЗУ A15 всегда высока, поскольку я использую 27C512 с 64 КиБ.

И, как и в случае с Арлекином, я использую только верхние 32 КиБ.

Они разделены на 2 банка по 16 КиБ каждый, банк выбирается перемычкой.

То есть можно хранить 2 разные прошивки.

0x4000:0x7FFF — 16 КиБ ОЗУ, совместно используемого с ULA. Здесь также хранится видеобуфер.

При обращении сюда сигнал MEM16 процессорного модуля будет установлен в ноль.

Сама оперативная память будет расположена на плате видеовыхода, как и все необходимое оборудование.

0x8000:0xFFFF — 32 КиБ собственной оперативной памяти процессора.

Чип расположен на плате ядра процессора.

Обратите внимание, что из-за лени я реализовал не очень умный способ декодирования верхних 32 КиБ адреса, чтобы включить микросхему ОЗУ на плате ядра ЦП.

На мой взгляд, необходимо было использовать логический элемент И типа 74HC08, так как ОЗУ активируется на низком уровне либо на выходе Y2, либо на Y3 микросхемы 74HC138 (подробно описано в первая часть ), но диоды с резистором тоже подойдут, только диоды нужно брать быстрые, например 1N4148. Преимущество такого процессорного модуля в том, что его можно проверить с помощью Арлекина, который у меня естественно есть.

Если вынуть процессор, ПЗУ и верхние 32 КиБ ОЗУ от Арлекина, и подключить все вот так проводами:

ZX Spectrum от коронавируса и палочек, часть 2 (работаем над ошибками)

Видно, что на плате Арлекин не хватает 3-х микросхем, это процессор (к его блоку подключены модуль, ПЗУ и верхние 32 КиБ ОЗУ тогда мы увидим, что Арлекин все еще работает. То есть мы используем Harlequin в части ULA + нижние 16 КиБ ОЗУ + аналоговую разводку, а все остальное делает наш процессорный модуль.

Мы можем запустить несколько тестов, чтобы убедиться, что с нашим модулем всё в порядке: Плата Арлекин очень многострадальная, она является основой для многих экспериментов, особенно в аналоговой части, поэтому со временем ее имидж стал так себе Всё о процессорном модуле.

Теперь давайте займемся ULA. Для начала просто повторим схему из предыдущей части (потом модернизируем):

ZX Spectrum от коронавируса и палочек, часть 2 (работаем над ошибками)

Схема в точности повторяет поделку из первой части.

Я заменил проводку, которую соединял на макетных платах, на перемычки, и схематически изобразил Arduino как Atmega с минимальной разводкой, она находится в правом нижнем углу.

Но на самом деле она осталась УНА Я решил сделать модуль в виде щитка для Ардуино.

Но вот Ардуина меня немного подвела, не все ее контакты идут с шагом 2,54 мм, одна гребенка оказалась смещенной.

Пришлось немного импровизировать:

ZX Spectrum от коронавируса и палочек, часть 2 (работаем над ошибками)

Эта гребенка отвечает за входы 8-13, но 11-13 выведены на разъем SPI в нижней части ардуино, шаг там стандартный и я решил взять их оттуда, а вот 8-10 пришлось подключать вроде этот. Сам модуль:

ZX Spectrum от коронавируса и палочек, часть 2 (работаем над ошибками)

После некоторой доработки модуль заработал.

И да, работает гораздо стабильнее, чем версия на макетной плате, все артефакты ушли:

ZX Spectrum от коронавируса и палочек, часть 2 (работаем над ошибками)

Конечно, перемычки – это большая неприятность.

Я их использовал в поделках на беспаечных макетах, так как там контакт "гулял", но тут-то и приходит на помощь пайка.

Давайте от них избавимся.

В целом нам не нужно генерировать сигналы CE и OE для оперативной памяти: на 595х у нас 16 бит адреса, а мы используем только младшие 14 — этого достаточно для 16 КиБ.

Старшие 2 бита всегда равны 0. Это то, что нам нужно — низкий сигнал.

Напомню, что выходы 595 мы включаем только при отключении процессора от шины с помощью 245х, то есть никакого конфликта у нас быть не может. Единственное, что стоит сделать, это на всякий случай подтянуть эти сигналы к высокому уровню с помощью резисторов.

Я использовал 10 кОм.

Обновленная схема:

ZX Spectrum от коронавируса и палочек, часть 2 (работаем над ошибками)

И обновленный скетч для Arduino

  
  
  
  
  
  
  
  
   

////////////////////////////////////////////////////////////////////////// // test ram defines #define TEST_RAM_BYTES 255 // CPU defines #define CPU_CLOCK_PIN 2 #define CPU_RESET_PIN 3 #define CPU_ENABLE_PIN 4 // Shift Register defines #define SR_DATA_PIN 8 #define SR_OUTPUT_ENABLE_PIN 9 #define SR_LATCH_PIN 10 #define SR_CLOCK_PIN 11 ////////////////////////////////////////////////////////////////////////// void setup() { // All CPU and RAM control signals need to be configured as inputs by default // and only changed to outputs when used. // Shift register control signals may be preconfigured // CPU controls seetup DDRC = B00000000; pinMode(CPU_CLOCK_PIN, INPUT); pinMode(CPU_RESET_PIN, INPUT); pinMode(CPU_ENABLE_PIN, OUTPUT); digitalWrite(CPU_ENABLE_PIN, HIGH); // active low // SR setup pinMode(SR_LATCH_PIN, OUTPUT); pinMode(SR_CLOCK_PIN, OUTPUT); pinMode(SR_DATA_PIN, OUTPUT); pinMode(SR_OUTPUT_ENABLE_PIN, OUTPUT); digitalWrite(SR_OUTPUT_ENABLE_PIN, HIGH); // active low // common setup Serial.begin(9600); Serial.println("Hello"); }// setup ////////////////////////////////////////////////////////////////////////// void shiftReadValueFromAddress(uint16_t address, uint8_t *value) { // set address digitalWrite(SR_LATCH_PIN, LOW); shiftOut(SR_DATA_PIN, SR_CLOCK_PIN, MSBFIRST, address>>8); shiftOut(SR_DATA_PIN, SR_CLOCK_PIN, MSBFIRST, address); digitalWrite(SR_LATCH_PIN, HIGH); digitalWrite(SR_OUTPUT_ENABLE_PIN, LOW); // active low delay(1); DDRC = B00000000; *value = PINC; // disable SR digitalWrite(SR_OUTPUT_ENABLE_PIN, HIGH); // active low }// shiftWriteValueToAddress ////////////////////////////////////////////////////////////////////////// void runClock(uint32_t cycles) { uint32_t currCycle = 0; pinMode(CPU_CLOCK_PIN, OUTPUT); while(currCycle < cycles) { digitalWrite(CPU_CLOCK_PIN, HIGH); digitalWrite(CPU_CLOCK_PIN, LOW); currCycle++; } pinMode(CPU_CLOCK_PIN, INPUT); }// runClock ////////////////////////////////////////////////////////////////////////// void trySpectrum() { pinMode(CPU_RESET_PIN, OUTPUT); digitalWrite(CPU_RESET_PIN, LOW); runClock(30); digitalWrite(CPU_RESET_PIN, HIGH); runClock(1250000); }// trySpectrum ////////////////////////////////////////////////////////////////////////// void readDisplayLines() { uint8_t value; for(uint16_t i=0; i<6144;i++) { shiftReadValueFromAddress(i, &value); Serial.println(value); } }// readDisplayLines ////////////////////////////////////////////////////////////////////////// void loop() { digitalWrite(CPU_ENABLE_PIN, LOW); trySpectrum(); digitalWrite(CPU_ENABLE_PIN, HIGH); Serial.println("Reading memory"); readDisplayLines(); Serial.println("Done"); delay(100000); }// loop ////////////////////////////////////////////////////////////////////////// // END //////////////////////////////////////////////////////////////////////////

Теперь не надо ни с чем возиться, это прекрасно.

Но всё равно как-нибудь через тернии прикрутим какое-нибудь самостоятельное устройство вывода.

Поскольку вы собираетесь рисовать картинку с Ардуино, вам нужно добавить какой-нибудь ЖК-дисплей.

у меня есть старый робот ЖК-дисплей для Arduino, но нам подойдет любой.

Единственное, что вам нужно будет посчитать, это количество ножек.

Если у вас нет МЕГА-подобной Arduino, то и ножек для параллельного ЖК не хватит, и придется ограничиться SPI. Это сильно замедлит скорость работы экрана, но о скорости мы подумаем позже.

А пока подключим дисплей по SPI. Для отладки программы я записал в ПЗУ образ экранной памяти Спекки.

Это легко сделать.

Нам понадобится программа просмотра файлов TZX, как этот , сам файл TZX, так , чип ПЗУ и программатор.

В просмотрщике открываем файл TZX, ищем кусок размером 6912 байт и сохраняем его на диск в виде бинарника.

Потом прошиваем в самое начало ПЗУ.

Когда мы сможем уверенно считать картинку из ПЗУ, можно будет сделать из Ардуино какой-нибудь УЛА и подключить его к процессорному модулю.

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

Так, разрешение «Спектрума» составляло 256x192 точки, весьма приличное по меркам того времени: у большинства конкурентов оно было меньше.

Однако, чтобы иметь возможность выводить такое высокое разрешение в цвете, инженерам пришлось прибегнуть к некоторым уловкам.

Первый трюк — сэкономить память.

Монохромный экран размером 256х192 пикселей занимает в памяти 6144 байта.

Если мы хотим иметь 4 цвета, нам понадобится вдвое больше места, 12288 байт. Если мы хотим 8 цветов, нам понадобится более 18 КиБ.

Для компьютера, у которого после 16 КиБ ПЗУ осталось всего 48 КиБ ОЗУ, отдавать 18 КиБ или даже 12 КиБ на экранный буфер — расточительство.

Поэтому в Спектруме пиксели не могут иметь самостоятельный цвет. Цвета определяются не для каждого пикселя на экране, а для каждой знакомой локации, то есть квадрата размером 8х8 пикселей.

Таким образом, вам нужно хранить цвета только для привычных пространств размером 32х24, а не для 256х192 пикселей, что существенно экономит память.

На каждое знакомое место выделяется ровно 1 байт. 6 бит в нем определяют цвета включенных (нижние 3 бита) и выключенных (следующие 3 бита) пикселей в знакоместе.

Далее идет бит, отвечающий за яркость.

Если установлено значение 1, то цвета как включенных, так и выключенных пикселей в этом знакоместе увеличивают свою яркость (кроме черного, он всегда одинаково черный).

Последний бит — это бит «мигания».

Если установлено значение 1, то цвета включенных и выключенных пикселей будут меняться местами с частотой около 1,5 Гц (если память не изменяет).

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

Без этого трюка Спектрум не смог бы выводить такое разрешение в цвете, но чтобы этот трюк сработал, нужно было организовать карту памяти так, чтобы каждое знакоместо находилось в той же строке DRAM, что и соответствующие атрибуты.

Для ULA экранный буфер начинался в самом низу адресного пространства (поскольку ULA имел доступ только к 16 КиБ ОЗУ, расположенным после ПЗУ, а экранный буфер располагался именно в самом начале этих 16 КиБ) .

То есть буфер пикселей имел адреса 0x000 — 0x17FF, а буфер цвета соответственно 0x1800 — 0x1AFF. Нам просто нужно было придумать, как соответствующим образом расположить пиксели в буфере.

Именно поэтому в Спектруме пиксели в оперативной памяти и пиксели на экране - это разные пиксели.

Первые 32 байта буфера пикселей описывают первую строку экрана (32 байта = 256 бит).

Вторые 32 байта — это 8-я строка.

Следующие 32 байта — это 16-я строка и так до 56-й, после которой идет 2-я строка, затем 9-я, затем 17-я и так до 57-й.

При заполнении 64 строк по той же схеме располагается 2-й блок из 64 строк, за ним следует 3-й.

Ваш браузер не поддерживает видео HTML5. Видео снято отсюда Это может показаться немного запутанным, но все сводится к адресации DRAM, а значит, в этом есть смысл.

А чтобы преобразовать координату экрана в адрес памяти, нужно просто поменять местами младшие 3 бита на следующие 3 бита в координате Y. Я покажу это более четко в коде ниже.

Благодаря такой адресации весь экранный буфер занимал в памяти менее 7 КиБ, несмотря на то, что Speckie мог отображать на экране 15 разных цветов! Однако, поскольку каждое знакоместо могло иметь только 2 цвета (чернила и бумага, то есть цвет входящих в него пикселей и фона), и оба они должны были быть либо из светлой половины палитры, либо из темной, были забавные видеоэффекты: Однако к делу.

В прошлой части я прочитал только 6 бит с шины данных с помощью Arduino, что привело к неполной отрисовке экрана.

Теперь вам нужно будет прочитать все восемь.

У UNO есть только один порт с 8 контактами, но ему сопоставлен аппаратный последовательный порт, поэтому я не хочу его использовать.

Мы прочитаем 6 бит в порт C и еще 2 бита в порт D. За один раз мы прочитаем в буфер одну строку экрана, то есть 256 пикселей.

Так как 1 пиксель в памяти экрана Спектрума занимает 1 бит (информация о цвете хранится отдельно от информации о пикселях), то одна строка = 32 байта.

Давайте начнем:

#define BUS_PORT_0_5 PINC #define BUS_PORT_6_7 PIND #define BUS_DDR_0_5 DDRC #define BUS_DDR_6_7 DDRD #define SR_PORT PORTD #define SR_OE_PIN B00100000 #define BYTES_PER_LINE 32 char scrBuffer[BYTES_PER_LINE]; void readLine(uint8_t lineNum) { SR_PORT &= ~SR_OE_PIN; for(uint8_t i=0; i<BYTES_PER_LINE; i++) { setAddress(lineNum, i); scrBuffer[i] = BUS_PORT_0_5; scrBuffer[i] |= BUS_PORT_6_7 & B11000000; } SR_PORT |= SR_OE_PIN; }

Как видите, чтобы задать адрес для чтения, я использую функцию setAddress. Эта функция устанавливает адрес соответствующей строки экрана на контакты сдвиговых регистров 595, точно так же, как и в предыдущей части.

Вот я его немного оптимизировал:

#define SR_PORT PORTD #define SR_DDR DDRD #define SR_CLOCK_PIN B00000100 #define SR_LATCH_PIN B00001000 #define SR_DATA_PIN B00010000 #define SR_OE_PIN B00100000 volatile uint16_t delayVar = 0; void setAddress(uint8_t lineNum, uint8_t pixel) { uint16_t address = (lineNum<<5) + pixel; SR_PORT &= ~SR_LATCH_PIN; pShiftOut(address); SR_PORT |= SR_LATCH_PIN; delayVar++; }

ЗадержкаVar здесь нужна, чтобы дать ПЗУ время подобрать адрес из шины.

Без него у меня есть артефакты.

Я также немного переписал встроенную функциюshiftOut, чтобы удалить ненужное, сделать аргумент 16-битным и развернуть цикл.

Получилось длинно, поэтому под спойлером: длинный pShiftOut с расширенным циклом из 16 итераций

void pShiftOut(uint16_t val) { // bit 15 if (val & (1 << 15)) { SR_PORT |= SR_DATA_PIN; } else { SR_PORT &= ~SR_DATA_PIN; } SR_PORT |= SR_CLOCK_PIN; SR_PORT &= ~SR_CLOCK_PIN; // bit 14 if (val & (1 << 14)) { SR_PORT |= SR_DATA_PIN; } else { SR_PORT &= ~SR_DATA_PIN; } SR_PORT |= SR_CLOCK_PIN; SR_PORT &= ~SR_CLOCK_PIN; // bit 13 if (val & (1 << 13)) { SR_PORT |= SR_DATA_PIN; } else { SR_PORT &= ~SR_DATA_PIN; } SR_PORT |= SR_CLOCK_PIN; SR_PORT &= ~SR_CLOCK_PIN; // bit 12 if (val & (1 << 12)) { SR_PORT |= SR_DATA_PIN; } else { SR_PORT &= ~SR_DATA_PIN; } SR_PORT |= SR_CLOCK_PIN; SR_PORT &= ~SR_CLOCK_PIN; // bit 11 if (val & (1 << 11)) { SR_PORT |= SR_DATA_PIN; } else { SR_PORT &= ~SR_DATA_PIN; } SR_PORT |= SR_CLOCK_PIN; SR_PORT &= ~SR_CLOCK_PIN; // bit 10 if (val & (1 << 10)) { SR_PORT |= SR_DATA_PIN; } else { SR_PORT &= ~SR_DATA_PIN; } SR_PORT |= SR_CLOCK_PIN; SR_PORT &= ~SR_CLOCK_PIN; // bit 9 if (val & (1 << 9)) { SR_PORT |= SR_DATA_PIN; } else { SR_PORT &= ~SR_DATA_PIN; } SR_PORT |= SR_CLOCK_PIN; SR_PORT &= ~SR_CLOCK_PIN; // bit 8 if (val & (1 << 8)) { SR_PORT |= SR_DATA_PIN; } else { SR_PORT &= ~SR_DATA_PIN; } SR_PORT |= SR_CLOCK_PIN; SR_PORT &= ~SR_CLOCK_PIN; // bit 7 if (val & (1 << 7)) { SR_PORT |= SR_DATA_PIN; } else { SR_PORT &= ~SR_DATA_PIN; } SR_PORT |= SR_CLOCK_PIN; SR_PORT &= ~SR_CLOCK_PIN; // bit 6 if (val & (1 << 6)) { SR_PORT |= SR_DATA_PIN; } else { SR_PORT &= ~SR_DATA_PIN; } SR_PORT |= SR_CLOCK_PIN; SR_PORT &= ~SR_CLOCK_PIN; // bit 5 if (val & (1 << 5)) { SR_PORT |= SR_DATA_PIN; } else { SR_PORT &= ~SR_DATA_PIN; } SR_PORT |= SR_CLOCK_PIN; SR_PORT &= ~SR_CLOCK_PIN; // bit 4 if (val & (1 << 4)) { SR_PORT |= SR_DATA_PIN; } else { SR_PORT &= ~SR_DATA_PIN; } SR_PORT |= SR_CLOCK_PIN; SR_PORT &= ~SR_CLOCK_PIN; // bit 3 if (val & (1 << 3)) { SR_PORT |= SR_DATA_PIN; } else { SR_PORT &= ~SR_DATA_PIN; } SR_PORT |= SR_CLOCK_PIN; SR_PORT &= ~SR_CLOCK_PIN; // bit 2 if (val & (1 << 2)) { SR_PORT |= SR_DATA_PIN; } else { SR_PORT &= ~SR_DATA_PIN; } SR_PORT |= SR_CLOCK_PIN; SR_PORT &= ~SR_CLOCK_PIN; // bit 1 if (val & (1 << 1)) { SR_PORT |= SR_DATA_PIN; } else { SR_PORT &= ~SR_DATA_PIN; } SR_PORT |= SR_CLOCK_PIN; SR_PORT &= ~SR_CLOCK_PIN; // bit 0 if (val & 1) { SR_PORT |= SR_DATA_PIN; } else { SR_PORT &= ~SR_DATA_PIN; } SR_PORT |= SR_CLOCK_PIN; SR_PORT &= ~SR_CLOCK_PIN; }// pShiftOut

Все эти операции с регистрами и разверткой цикла я делал не просто так.

Я, конечно, предполагал, что с Arduino далеко не уедешь, однако, когда я закончил первую версию кода с использованием стандартных функций Arduino, прорисовка экрана из ПЗУ заняла 9 секунд. С помощью этих бубнов мне удалось сократить время до менее 4 секунд. Это еще очень долго, но все же.

Далее, как я писал выше (а еще лучше описано здесь) , строки в пятнистом экранном буфере не следуют друг за другом.

Да, в свое время это делалось для ускорения чтения самих пикселей и информации о цвете, но сейчас это нам только мешает. Теперь нам нужно иметь возможность переводить номер вертикальной линии на экране в номер строки в памяти экрана.

Я обещал более наглядно показать этот прием в коде, и вот он, просто поменяйте местами биты в координате Y:

uint8_t mapLineNum(uint8_t lineNum) { // convert screen line number to actual line number in memory uint8_t y = (lineNum & B00000111) << 3; y |= (lineNum >> 3) & B00000111; y |= lineNum & B11000000; return y; }

Ну не было цвета в прошлой части, а он нужен.

Что такое Спектрум без, простите за каламбур, спектра? Опять же, как я писал выше, информация о цвете поступает сразу после экранного буфера, и каждый байт здесь несет следующую информацию о знакоместности:

  • биты 1-3: цвет пикселей в соответствующем квадрате 8x8 (чернила).

    По 1 биту на цветовой канал G, R, B. Таким образом мы получаем 8 цветов.

  • биты 4-6: цвет фона соответствующего квадрата (бумаги)
  • бит 7: атрибут квадратной яркости.

    Таким образом мы удваиваем количество цветов с 8 до 16 (на самом деле 15, поскольку этот атрибут не влияет на черный цвет).

    Но этот атрибут меняет и цвет пикселя (чернила), и цвет фона (бумага), поэтому в пределах одного квадрата у нас всё равно есть 2 цвета из палитры из 8 разных цветов, а не из 15.

  • бит 8: атрибут мигания.

    Когда этому атрибуту было присвоено значение 1, цвета пикселей и фона этого квадрата менялись местами с частотой примерно 1,5 Гц (если память не изменяет).

Считать эту информацию из оперативной памяти будет гораздо проще.

Каждая строка атрибутов цвета имеет размер в точности строки пикселей на экране (поскольку 1 атрибут длиной 8 бит описывает квадрат со стороной 8 пикселей).

Причем количество строк атрибутов ровно в 8 раз меньше количества строк на экране (опять же, 1 строка атрибутов описывает 8 строк пикселей), то есть их всего 24. Кроме того, начинается адрес первой атрибутивной строки.

где была бы 193-я линия пикселя, если бы она была реальной.

Поэтому вы можете просто использовать готовую функцию readLine:

char colourBuffer[768]; void readColourBuffer() { for(uint8_t i=0; i<24; i++) { readLine(192+i); memcpy(&colourBuffer[i*BYTES_PER_LINE], scrBuffer, BYTES_PER_LINE); } }

В общем, теперь у нас есть все, чтобы прочитать экран и вывести его в правильном формате:

char inverse = 0; void drawScr() { uint8_t lastLine = 255; inverse = !inverse; for(uint8_t line=0; line<192;line++) { uint8_t trueLineNum = mapLineNum(line); readLine(trueLineNum); drawLine(line, &lastLine); } }

Ах да, за исключением самой функции вывода.

Здесь я использовал библиотеку Arduino TFT. Он работает с аппаратным SPI и разгонять там особо нечего.

Хотя вру.

Мой экран имеет разрешение 160x128 пикселей, и я могу масштабировать изображение, чтобы не выводить лишние пиксели.

Для этого я введу пару дополнительных переменных.

LastLineNum — номер последней нарисованной линии.

Если номер текущей линии после масштабирования совпадает с предыдущим, мы не будем рисовать эту линию.

Проделаем то же самое с каждым пикселем:

#define SCALE 1.6f void drawLine(uint8_t lineNum, uint8_t *lastLineNum) { uint8_t colour, x, y; uint8_t lastX = 255; y = lineNum / SCALE; if(y == *lastLineNum) return; uint8_t colourLine = lineNum >> 3; // lineNum / 8 for(uint8_t i=0; i<BYTES_PER_LINE; i++) { colour = colourBuffer[(colourLine << 5) + i]; // [colourLine * 32 + i] uint8_t isBright = (colour & B01000000) >> 6; uint8_t isFlashing = (colour & B10000000) >> 7; for(uint8_t trueX=0; trueX<8; trueX++) { uint8_t isFore = ((scrBuffer[i] >> trueX) & 1); uint8_t r,g,b; if(isFlashing && inverse) { isFore = !isFore; } uint8_t col = (255 - DIM_FACTOR) + (DIM_FACTOR * isBright); if(isFore) { b = ( colour & B00000001) * col; r = ((colour & B00000010) >> 1) * col; g = ((colour & B00000100) >> 2) * col; } else { b = ((colour & B00001000) >> 3) * col; r = ((colour & B00010000) >> 4) * col; g = ((colour & B00100000) >> 5) * col; } x = ((i<<3)+(8-trueX)) / SCALE; if(x != lastX) { TFTscreen.stroke(r, g, b); TFTscreen.point(x, y); lastX = x; } } } *lastLineNum = y; }// drawLine

И, если кому интересно, полный код.

////////////////////////////////////////////////////////////////////////////// //#define DIAG ////////////////////////////////////////////////////////////////////////////// #include <TFT.h> #include <SPI.h> ////////////////////////////////////////////////////////////////////////////// // pin definitions #define TFT_CS 10 #define TFT_DC 9 #define TFT_RST 8 #define SR_PORT PORTD #define SR_DDR DDRD #define SR_CLOCK_PIN B00000100 #define SR_LATCH_PIN B00001000 #define SR_DATA_PIN B00010000 #define SR_OE_PIN B00100000 #define BUS_PORT_0_5 PINC #define BUS_PORT_6_7 PIND #define BUS_DDR_0_5 DDRC #define BUS_DDR_6_7 DDRD // screen params #define BYTES_PER_LINE 32 #define SCALE 1.6f #define DIM_FACTOR 64 ////////////////////////////////////////////////////////////////////////////// char scrBuffer[BYTES_PER_LINE]; char colourBuffer[768]; char inverse = 0; TFT TFTscreen = TFT(TFT_CS, TFT_DC, TFT_RST); volatile uint16_t delayVar = 0; ////////////////////////////////////////////////////////////////////////////// void setup() { // TFT TFTscreen.begin(); TFTscreen.background(0, 0, 0); TFTscreen.fill(0, 0, 0); // SR SR_DDR |= SR_CLOCK_PIN; SR_DDR |= SR_LATCH_PIN; SR_DDR |= SR_DATA_PIN; SR_DDR |= SR_OE_PIN; SR_PORT |= SR_OE_PIN; // default to HIGH to disable address bus output // BUS BUS_DDR_0_5 &= B11000000; BUS_DDR_6_7 &= B00111111; // Diag #ifdef DIAG Serial.begin(9600); #endif } ////////////////////////////////////////////////////////////////////////////// void pShiftOut(uint16_t val) { // bit 15 if (val & (1 << 15)) { SR_PORT |= SR_DATA_PIN; } else { SR_PORT &= ~SR_DATA_PIN; } SR_PORT |= SR_CLOCK_PIN; SR_PORT &= ~SR_CLOCK_PIN; // bit 14 if (val & (1 << 14)) { SR_PORT |= SR_DATA_PIN; } else { SR_PORT &= ~SR_DATA_PIN; } SR_PORT |= SR_CLOCK_PIN; SR_PORT &= ~SR_CLOCK_PIN; // bit 13 if (val & (1 << 13)) { SR_PORT |= SR_DATA_PIN; } else { SR_PORT &= ~SR_DATA_PIN; } SR_PORT |= SR_CLOCK_PIN; SR_PORT &= ~SR_CLOCK_PIN; // bit 12 if (val & (1 << 12)) { SR_PORT |= SR_DATA_PIN; } else { SR_PORT &= ~SR_DATA_PIN; } SR_PORT |= SR_CLOCK_PIN; SR_PORT &= ~SR_CLOCK_PIN; // bit 11 if (val & (1 << 11)) { SR_PORT |= SR_DATA_PIN; } else { SR_PORT &= ~SR_DATA_PIN; } SR_PORT |=

Теги: #Сделай сам или Сделай сам #Старое железо #arduino #Разработка для Arduino #ZX Spectrum #z80

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.