Ссылка на первую часть Прежде всего хочу попросить у уважаемой аудитории прощения за столь долгую паузу между первой частью и продолжением.
У меня есть для этого веская причина.
Если кто помнит, в первой части я упоминал, что сборка на макетных платах производилась из-за нежелания паять.
Я лгал.
Я люблю паять, но заключение (не то, что с кварками, а то, что с людьми) привело к тому, что у меня закончился припой.
Я, конечно, заказал сразу и на eBay, и на Али, но пришло оно только недавно.
Увы, то, что произошло в первой части, на беспаечных макетах работало крайне нестабильно, и о сборке продолжения на макетках не могло быть и речи.
Обратите внимание, я искренне верю, что все мои поделки можно собрать на макетах.
Однако нужны действительно качественные макеты.
У меня их нет. Но теперь припой прибыл, можно продолжать.
Еще кое-что.
Я художник.
Будет много фотографий и даже видео.
Трафик! Однако для начала стоит определиться, что продолжать.
Как я писал в предыдущей части, мне почему-то не хочется собирать очередной клон Спектрума, ведь их сотни, и собирали их люди гораздо более образованные, чем я.
Еще раз напомню, что: Пожалуйста, относитесь ко всему, что я пишу, с недоверием.
Я дилетант в худшем смысле этого слова.
У меня нет никакого образования, соответствующего тому, о чем я пишу.
Если вы вдруг решите повторить то, что сделал я (нет, а вдруг?), знайте, что почти все, что здесь было сделано, будь то жесткое или мягкое, было сделано неправильно.
Выкладываю это на всеобщее обозрение, потому что этим сложно убить себя, а детали, использованные в поделке, стоят сущие копейки, и вы не против.
Так вот, одна из ошибок, совсем не очевидная для меня, была выявлена в комментарии уважаемого mpa4b : для тех частот, на которых работает поделка, необходима статическая CMOS-версия процессора Z80, а на моих фотографиях фигурирует NMOS-версия, и вся эта схема работает только на честном слове.
Кроме того, нужно куда-то воткнуть Arduino. Без Ардуино не интересно.
Это тоже говорит в пользу Arduino Очень нетривиальная организация видеопамяти у Specky .
Конечно, все это собирается оптом.
Но вам понадобится много дел; о беспаечных макетах можно забыть.
И не забывайте про Ардуино! Так что продолжим мучить 8-битный МК.
Ну а так как мы тоже паяем Ардуино, то надо придумать какой-нибудь подходящий форм-фактор.
Я подумал об этом и решил разделить будущий Спектрум на такие модули:
Декодер верхних 32 КиБ ОЗУ сюда еще не припаян (экспериментировал со схемой), и соответственно сам чип не установлен
Как и в Arduino, на разъем модуля выводятся все сигналы, необходимые для расширения системы: шины адреса и данных, сигналы управления процессором, выход декодера адреса, тактовый сигнал, питание.
Платы подключаются стандартным кабелем IDE. Это удобно (39 контактов хватает почти на все - у меня отдельный блок питания, там полно проводов от древних материнских плат, и эти провода, точнее, их 80-жильные варианты, с тонкими проводниками - идеальный источник провода для разводки сигналов на макетных платах!).
Основная плата содержит сам процессор Z80, ПЗУ, микросхему ОЗУ объемом 32 КиБ и декодер адреса.
Все.
Я с гордостью назвал эту поделку «процессорным модулем».
Давайте вспомним, на чем мы остановились в предыдущей части.
Там я попытался, с весьма спорным успехом, показать, насколько простыми были 8-битные компьютеры 80-х.
Как-то у меня это не очень получилось, поэтому повторю.
Почти все компьютеры состояли из ядра из небольшого количества микросхем.
Это был всего лишь процессор, ОЗУ, ПЗУ и немного логики для декодирования адреса.
Все.
Эти микросхемы были тупо соединены между собой общей шиной.
Конечно, это было только «ядро» компьютера, а дальше (видеовыход, звук, носители информации) все стало гораздо разнообразнее.
Но само ядро было везде почти одинаковое.
Таким образом, ZX Spectrum, MSX, Amstrad CPC были неотличимы по ядру, и именно способ вывода изображения, звука и ввода информации отличал один компьютер от другого.
Однако, как вещь сама по себе, практически любой 8-битный компьютер может работать в этой конфигурации с голым ядром, что мы и сделали в предыдущей части: компьютер, состоящий из 4-х чипов, выполнял программу из ПЗУ.
Да, я очень криво вытащил из памяти результат работы этого компьютера, но факт остается фактом: Спекки работал как минимум на 4 случаях.
И, конечно, это не относится исключительно к процессору Z-80, так что «собрать» скелет практически любого 8-битного компьютера прошлого можно практически на любом процессоре, и он будет работать.
В этой статье я постараюсь не только еще больше расширить сделанный ранее модуль, но и исправить некоторые другие свои ошибки из предыдущей части.
Начнем со схемы.
Я потратил приличное количество времени, но, по крайней мере, разобрался с Орлом.
Теперь я могу не только писать, но и рисовать об электронике.
Вот так, например:
Прошу не судить строго, до этого я видел Орла лишь мельком в роликах на Ютубе.
Буду рад любым конструктивным замечаниям и критике в комментариях.
Читатель, наделенный суперпамятью, наверное, помнит, что в предыдущей части я говорил, что модуль будет лишен оперативной памяти, и мы ограничимся 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 КиБ ОЗУ от Арлекина, и подключить все вот так проводами:
Видно, что на плате Арлекин не хватает 3-х микросхем, это процессор (к его блоку подключены модуль, ПЗУ и верхние 32 КиБ ОЗУ
тогда мы увидим, что Арлекин все еще работает. То есть мы используем Harlequin в части ULA + нижние 16 КиБ ОЗУ + аналоговую разводку, а все остальное делает наш процессорный модуль.
Мы можем запустить несколько тестов, чтобы убедиться, что с нашим модулем всё в порядке: Плата Арлекин очень многострадальная, она является основой для многих экспериментов, особенно в аналоговой части, поэтому со временем ее имидж стал так себе Всё о процессорном модуле.
Теперь давайте займемся ULA. Для начала просто повторим схему из предыдущей части (потом модернизируем):
Схема в точности повторяет поделку из первой части.
Я заменил проводку, которую соединял на макетных платах, на перемычки, и схематически изобразил Arduino как Atmega с минимальной разводкой, она находится в правом нижнем углу.
Но на самом деле она осталась УНА Я решил сделать модуль в виде щитка для Ардуино.
Но вот Ардуина меня немного подвела, не все ее контакты идут с шагом 2,54 мм, одна гребенка оказалась смещенной.
Пришлось немного импровизировать:
Эта гребенка отвечает за входы 8-13, но 11-13 выведены на разъем SPI в нижней части ардуино, шаг там стандартный и я решил взять их оттуда, а вот 8-10 пришлось подключать вроде этот.
Сам модуль:
После некоторой доработки модуль заработал.
И да, работает гораздо стабильнее, чем версия на макетной плате, все артефакты ушли:
Конечно, перемычки – это большая неприятность.
Я их использовал в поделках на беспаечных макетах, так как там контакт "гулял", но тут-то и приходит на помощь пайка.
Давайте от них избавимся.
В целом нам не нужно генерировать сигналы CE и OE для оперативной памяти: на 595х у нас 16 бит адреса, а мы используем только младшие 14 — этого достаточно для 16 КиБ.
Старшие 2 бита всегда равны 0. Это то, что нам нужно — низкий сигнал.
Напомню, что выходы 595 мы включаем только при отключении процессора от шины с помощью 245х, то есть никакого конфликта у нас быть не может. Единственное, что стоит сделать, это на всякий случай подтянуть эти сигналы к высокому уровню с помощью резисторов.
Я использовал 10 кОм.
Обновленная схема:
И обновленный скетч для 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
-
Глаз
19 Oct, 24 -
Трубчатый Наконечник
19 Oct, 24 -
База Ит-Знаний В Вашей Компании
19 Oct, 24 -
Мои Мысли — Мои Кони. Первая Часть
19 Oct, 24 -
Вы Знали?
19 Oct, 24