ZX Spectrum из коронавируса и палок, часть 2 (работа над ошибками)
Ссылка на первую часть
Прежде всего, хочу попросить у уважаемой аудитории прощения за столь долгую паузу между первой частью и продолжением. На то у меня есть уважительная причина. Если кто-то помнит, в первой части я упомянул, что сборка на макетках производилась в связи с нежеланием паять. Я слукавил. Паять я люблю, но конфайнмент (не тот, что у кварков, а тот, что у людей) привел к тому, что у меня закончился припой. Я, конечно, заказал его сразу же и на ибее и на али, но пришел он только вот недавно. Увы, то, что получилось в первой части на беспаечных макетках, работало крайне нестабильно, и о сборке продолжения на макетках не могло идти речи.
Прошу заметить, что я искренне считаю, что всю мою поделку собрать на макетках возможно. Однако, нужны действительно качественные макетки. У меня таких нет. Но вот припой пришел, можно продолжить.
И еще одно. Я — художник. Будет много картинок, и даже видео. Трафик!
Для начала, однако, стоит определиться, что же продолжать. Как я писал в предыдущей части, собирать просто очередной клон спектрума как-то не хочется, ибо их сотни, и собраны они людьми, значительно более образованными, нежели я. Еще раз напомню, что:
Пожалуйста, относитесь ко всему, что я пишу, с изрядной долей скепсиса. Я — любитель в самом плохом смысле этого слова. У меня нет никакого соответствующего тому, о чем я пишу, образования. Если вы вдруг решите повторить то, что сделал я (нет, ну, а вдруг?), знайте, что почти все, что сделано тут, будь то хард или софт, сделано неправильно. Выкладываю я это на всеобщее обозрение потому, что убиться этим сложно, а детали, используемые в поделке, стоят сущие копейки, и их не жалко.
Кроме того, надо же куда-то всунуть ардуино. Без ардуино неинтересно. В пользу ардуино так же говорит весьма нетривиальная организация видеопамяти у Спекки. Безусловно, все это собирается на рассыпухе. Но корпусов понадобится очень много, о беспаечных макетках можно забыть. А с ардуино можно и не забывать! Так что будем и дальше мучить 8-биный МК.
Ну и, раз паяем и ардуино, то надо придумать какой-то соответствующий форм-фактор. Я подумал, и решил разделить будущий спектрум на модули вот такие:
Здесь еще не распаян декодер верних 32 КиБ ОЗУ (я экспериментировал со схематикой), и, соответственно, не установлена сама микросхема
Как и в ардуино, на разъем модуля выведены все сигналы, необходимые для расширения системы: шины адреса, данных, управляющие сигналы процессора, вывод декодера адреса, тактовый сигнал, питание. Соединяются платы стандартным шлейфом IDE-дисков. Это удобно (39 пинов почти на все хватает — питание у меня идет отдельно, проводов в наличии полно от древних материнок, да и еще эти провода, точнее, их 80-жильные версии, с тонкими проводниками — идеальный источник проводков для разводки сигналов на макетках!). Основная плата содержит сам процессор Z80, ПЗУ, микросхему ОЗУ на 32 КиБ, декодер адреса. Все. Поделку я гордо назвал «процессорный модуль».
Давайте вспомним, на чем мы остановились в предыдущей части. Там я с очень спорным успехом пытался показать, насколько просты были 8-битные компьютеры 80х. Как-то у меня не очень получилось, поэтому повторю. Почти все компы представляли собой ядро из небольшого числа микросхем. Это был как раз процессор, ОЗУ, ПЗУ и немного логики для декодирования адреса. Все. Эти микросхемы тупо соединялись между собой общей шиной. Конечно, это было лишь «ядро» компьютера, и дальше (видеовывод, звук, носители информации) все становилось сильно разнообразнее. Но само ядро почти везде было практически одинаковым. Так, ZX Spectrum, MSX, Amstrad CPC в части ядра были малоотличимы, и именно способ вывода картинки, звука, и ввода информации отличал один компьютер от другого. Однако как вещь в себе, практически любой 8-битный компьютер может работать вот в такой конфигурации голого ядра, что у нас и получилось в предыдущей части: компьютер из 4х микросхем выполнил программу из ПЗУ. Да, я очень криво достал результат работы этого компьютера из памяти, но факт остается фактом, Спекки на 4х корпусах худо-бедно работал.
И, естественно, это не относится исключительно к процессору Z-80, таким образом можно «собрать» скелет практически любого 8-битного компьютера из прошлого, на практически любом процессоре, и он будет работать.
В этой статье я попробую не только далее расширить сделаный ранее модуль, но и исправить некоторые другие свои ошибки из предыдущей части. Начнем, пожалуй, со схемы. Я потратил приличное количество времени, но худо-бедно разобрался с Иглом. Теперь я могу не только писать, но и рисовать про электронику. Вот так например:
прошу слишком строго не судить, до этого я видел Игл только мельком в роликах на ютубе. Рад буду любым конструктивным замечаниям и критике в коментариях.
Читатель, наделенный свехпамятью, пожалуй, вспомнит, что в предыдущей части я говорил о том, что модуль будет лишен ОЗУ, и мы ограничимся 16 КиБ версией спекки, где собственного ОЗУ процессора не было, а были лишь совместные с ULA 16 КиБ, но, паять — так паять: я добавил еще один корпус на плату процессорного модуля и теперь карта памяти полностью соответствует 48К спекки:
0×0000:0×3FFF — ПЗУ на плате процессора. Линия А15 ПЗУ всегда в высоком состоянии, так как я использую 27C512, в которой 64 КиБ. И, как и на Арлекине, я использую только верхние 32 КиБ. Они разделены на 2 банка по 16 КиБ, банк выбирается перемычкой. То есть можно хранить 2 разные прошивки.
0×4000:0×7FFF — 16 КиБ ОЗУ, разделяемое с ULA. Тут, в том числе, хранится видеобуфер. При обращении сюда сигнал MEM16 процессорного модуля будет выставлен в ноль. Само ОЗУ будет располагаться на плате видеовывода, как и вся необходимая обвязка.
0×8000:0xFFFF — 32 КиБ собственного ОЗУ процессора. Микросхема размещена на плате ядра процессора. Заметьте, что из-за лени я реализовал не очень умный способ декодирования адреса верхних 32 КиБ, чтобы активировать микросхему ОЗУ на плате ядра процессора. По-уму, надо было использовать гейт AND, типа 74HC08, так как ОЗУ активируется при низком уроне либо на выходе Y2, либо на выходе Y3 микросхемы 74HC138 (подробно описано в первой части), но диоды с резистором тоже работают, только диоды надо брать быстрые, типа 1N4148, например.
Плюсы такого процессорного модуля в том, что его можно проверить с помощью Арлекина, который у меня, естественно, есть. Если вынуть из Арлекина процессор, ПЗУ и верхнее 32 КиБ ОЗУ, и соединить все вот так вот проводочками:
Видно, что на плате арлекина отсутствуют 3 микросхемы, это процессор (в его колодку подключен модуль, ПЗУ и верхние 32 КиБ ОЗУ
то мы увидим, что Арлекин все еще работает. То есть, мы используем Арлекин в части ULA + нижние 16 КиБ ОЗУ + аналоговая обвязка, а все остальное делает наш процессорный модуль. Можно прогнать несколько тестов, чтобы убедится, что с нашим модулем все в порядке:
плата арлекина очень многострадальная, она — база для многих экспериментов, особенно в аналоговой части, так что изображение у нее со временем стало ну так себе
По процессорному модулю все. Теперь давайте займемся ULA. Для начала, просто повторим схему из предыдущей части (потом будем ее модернизировать):
схема в точности повторяет поделку из первой части. Проводки, которые я перетыкал на макетках, я заменил на джамперы, а ардуино схематично изобразил в виде Атмеги с минимальной обвязкой, она в правом нижнем углу. Но на деле она так и осталась УНОй
Модуль я решил его сделать в виде шилда для Ардуины. Но Ардуина меня тут немного подвела, не все её контакты идут с шагом 2,54 мм, одна гребенка оказалась смещена. Пришлось немного импровизировать:
Эта гребенка отвечает за 8–13 входы, но 11–13 разведены на разъем SPI внизу ардуины, там шаг стандартный и я решил их брать оттуда, а вот 8–10 пришлось приделать так.
Сам модуль:
После некоторого дебага, модуль заработал. И да, работает он сильно стабильнее, чем версия на макетке, все артефакты пропали:
Конечно, перемычки очень мешают. Я их использовал в поделке на беспаечных макетках, так как там контакт «гулял», но здесь-то пайка. Давайте от них избавимся. Вообще, генерировать сигналы CE и OE для ОЗУ нам не надо: на 595х у нас 16 бит адреса, а мы используем только нижние 14 — этого достаточно для 16 КиБ. Верхние 2 бита у нас всегда равны 0. Это то, что надо — низкий сигнал. Напомню, что выходы 595 мы включаем только тогда, когда процессор отключен от шины с помощью 245х, то есть никакого конфликта у нас быть не может. Единственное, что стоит сделать, это подтянуть эти сигналы к высокому уровню резисторами, на всякий случай. Я использовал 10 кОм. Обновленная схема:
//////////////////////////////////////////////////////////////////////////
// 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
//////////////////////////////////////////////////////////////////////////
Теперь не надо ничего перетыкать, это здорово… Но все равно как-то через тернии, давайте прикрутим какое-нибудь самостоятельное устройство вывода. Раз уж рисовать картинку с ардуинки, то надо какой-нибудь LCD присобачить. У меня есть старый robot LCD для ардуино, но нам подойдет любой. Единственное, надо будет рассчитать количество ног. Если у вас не МЕГА-подобная ардуинка, то ног на параллельный LCD не хватит, и придется ограничиться SPI. Это сильно замедлит скорость работы экрана, но о скорости подумаем потом. Пока же, подключим дисплей по SPI. Для отладки программы я записал образ экранной памяти Спекки в ПЗУ. Сделать это просто. Нам понадобится программа просмотра TZX файлов, вроде этой, сам TZX-файл, вроде этого, микросхема ПЗУ и программатор. В программе просмотра открываем файл TZX, ищем в ней кусок размером 6912 байт, и сохраняем его на диск, как бинарник. Потом прошиваем в самое начало ПЗУ. Когда мы сможем уверенно читать картинку из ПЗУ, можно будет сделать из ардуинки некое подобие ULA и подключить её к процессорному модулю.
В прошлой части мы уже читали дамп экранной памяти, но я описал все довольно поверхностно, попробуем дополнить ликбез по архитектуре спекки.
Итак, разрешение спектрума составляло 256×192 пикселей, очень прилично по меркам того времени: у большинства конкурентов было меньше. Однако, чтобы успевать выводить такое высокое разрешение в цвете, инженерам пришлось пойти на некоторые хитрости.
Первая хитрость заключается в экономии памяти. Монохромный экран 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 КиБ и располагался экранный буфер). То есть буфер пикселей имел адреса 0×000 — 0×17FF, а буфер цветов, соответственно, 0×1800 — 0×1AFF. Надо было только придумать, как расположить пикселы в буфере соответствующим образом.
Именно поэтому в спектруме пикселы в ОЗУ и пикселы на экране — это разные пикселы. Первые 32 байта буфера пикселей описывают первую строку экрана (32 байт = 256 бит). Вторые 32 байта — 8ю строку. Следующие 32 байта — 16ю строку и так далее до 56й, после которой идет 2я строка, потом 9я, потом 17я и так далее до 57й. Когда 64 строки будут заполнены, по такой же схеме располагается 2й блок из 64х строк, а за ним — 3й.
Your browser does not support HTML5 video.
Видео взято отсюда
Пусть это кажется немного запутанным, но все упирается в адресацию DRAM, а значит, это логично. И чтобы преобразовать экранную координату в адрес памяти, надо всего лишь поменять местами младшие 3 бита со следующими за ними 3 мя битами в координате Y. Я это более наглядно покажу в коде ниже.
Благодаря такой адресации, весь буфер экрана занимал в памяти менее 7 КиБ при том, что спекки мог выводить на экран 16 разных цветов! Однако, так как каждое знакоместо могло иметь только 2 цвета (ink и paper, то есть цвет включенных пикселей и фона), причем оба из них должны были быть либо из яркой половины палитры, либо из темной, существовали забавные видеоэффекты:
Однако, к делу. В прошлой части ардуинкой я читал только 6 бит из шины данных, что приводило в неполной прорисовке экрана. Сейчас надо будет читать все восемь. В УНО есть только 1 порт, имеющий 8 пинов, но на него замаплен аппаратный последовательный порт, так что использовать его я не хочу. Будем читать 6 бит в порт С и еще 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
Как видно, для установки адреса, откуда читать, я использую функцию 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++;
}
delayVar тут нужна, чтобы дать ПЗУ время забрать адрес с шины. Без нее у меня идут артефакты. Кроме того, я немного переписал встроенную функцию shiftOut, чтобы убрать из нее лишнее, сделать аргумент 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
Все эти операции с регистрами и разворачивание циклов я делал не просто так. Конечно, я предполагал, что с ардуино далеко не уйдешь, однако, когда я закончил первую версию кода на стандартных функциях ардуины, отрисовка экрана из ПЗУ занимала 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: цвет пикселей в соответствующем квадрате 8×8 (ink). По 1 биту на цветовой канал G, R, B. Таким образом получаем 8 цветов.
- биты 4–6: цвет фона соответствующего квадрата (paper)
- бит 7: атрибут яркости квадрата. Таким образом мы удваиваем количество цветов с 8 до 16 (на самом деле, до 15, так как черный не меняется этим атрибутом). Но этот атрибут меняет как цвет пикселей (ink), так и цвет фона (paper), так что в пределах одного квадрата мы все еще имеем 2 цвета из палитры в 8 разных цветов, а не 16.
- бит 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);
}
}
Ах да, кроме самой функции вывода. Тут я использовал ардуиновскую библиотеку TFT. Она работает с аппаратным SPI и ускорять там особо нечего… Хотя вру. Мой экран имеет разрешение 160×128 пикселей, и я могу отмасштабировать изображение, чтобы не выводить лишние пикселы. Для этого я введу пару дополнительных переменных. 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> 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
#include
//////////////////////////////////////////////////////////////////////////////
// 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 |= 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
//////////////////////////////////////////////////////////////////////////////
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;
}
//////////////////////////////////////////////////////////////////////////////
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++;
}
//////////////////////////////////////////////////////////////////////////////
void readLine(uint8_t lineNum) {
SR_PORT &= ~SR_OE_PIN;
for(uint8_t i=0; i> 3; // lineNum / 8
for(uint8_t i=0; i> 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; // i<<3 <=> i*8
if(x != lastX) {
TFTscreen.stroke(r, g, b);
TFTscreen.point(x, y);
lastX = x;
}
}
}
*lastLineNum = y;
}// drawLine
//////////////////////////////////////////////////////////////////////////////
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);
}
}
//////////////////////////////////////////////////////////////////////////////
#ifdef DIAG
void testScr() {
uint8_t lastLine = 255;
for(uint8_t line=0; line<192;line++) {
uint8_t trueLineNum = mapLineNum(line);
drawLine(line, &lastLine);
}
}
#endif
//////////////////////////////////////////////////////////////////////////////
#ifdef DIAG
void testLoop() {
unsigned long ms;
sprintf(scrBuffer, "%s", "Quick brown fox jumped over a");
Serial.println("Drawing buffer");
ms = millis();
readColourBuffer();
testScr();
ms = millis() - ms;
Serial.print("ms elapsed:");
Serial.println(ms);
Serial.println("Drawing picture");
ms = millis();
readColourBuffer();
drawScr();
ms = millis() - ms;
Serial.print("ms elapsed:");
Serial.println(ms);
delay(10000);
}
#endif
//////////////////////////////////////////////////////////////////////////////
void realLoop() {
readColourBuffer();
drawScr();
delay(250);
}
//////////////////////////////////////////////////////////////////////////////
void loop() {
#ifdef DIAG
testLoop();
#else
realLoop();
#endif
}
//////////////////////////////////////////////////////////////////////////////
После освоения Игла времени на поиск и освоение рисовалки схем для Ардуины мне было жалко, но я хорошо умею в GIMP, так что вот такая получилась Ардуиновская схема подключения:
Ну и ради чего все это строилось, результат:
Медленно, но верно. Картинка, что надо.
Схема:
На схеме изображена микросхема ОЗУ, а не ПЗУ, так как я не особо силен в Игле, а готового ПЗУ в библиотеках не нашел. Как можно заметить, сигнал WE на схеме подтянут к земле, то есть как-бы всегда активна запись… Но на ПЗУ этого сигнала нет, там в моем случае (W27C512) линия А15, которая и должна быть на земле, если используем нижнюю половину ПЗУ. Кроме того, в правом верхнем углу коннектор robot LCD, но все SPI-экранчики для ардуинки имеют аналогичное управление.
модуль, читающий картинку из ПЗУ:
Ну что же, мы знаем, как запустить ядро спектрума без видеобуфера и знаем, как вывести изображение из видеобуфера на экран (чик). Осталось только соединить оба знания в одно. Надо приделать нашу альтернативно быструю видеокарту к процессорному модулю. В части взаимодействия с шинами я не буду ничего изобретать, просто возьму схему из первой части. Там были 74HC245 в качестве буфера шины адреса и резисторы в качестве буфера шины данных. Из переделок, надо переподключить управление 595 ми и сигналами процессора на другие ноги, чтобы не мешать управлению экраном (он привязан к аппаратному SPI).
алгоритм работы сильно не поменяется:
#define CPU_ENABLE_PIN 10
void loop() {
digitalWrite(CPU_ENABLE_PIN, LOW);
trySpectrum();
digitalWrite(CPU_ENABLE_PIN, HIGH);
readColourBuffer();
drawScr();
delay(100000);
}// loop
То есть мы подключаем процессор к нашей ардУЛА (CPU_ENABLE_PIN соединен с управляющим пином 245х), потом вызываем trySpectrum:
void trySpectrum() {
runClock(12500000);
}// trySpectrum
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
Эта функция просто посылает 12,5 миллионов импульсов клока в процессор. После этого мы отключаем процессорную плату от ардУЛА и вызываем readColourBuffer. Эта функция нам уже известна, она загружает в память ардуинки информацию о цвете из цветового буфера, а видеоОЗУ. Ну и дальше тоже уже известная функция drawScr. Она читает сами пикселы из видеоОЗУ и выводит из на robot LCD, используя информацию о цвете, прочитанную ранее. Все. На этом, наверное, пора завершаться. Планов было громадье, прикрутить нормальный тактовый генератор, приделать клавиатуру, сделать поделку более интерактивной, но, как показала практика, надо соизмерять желаемое и возможным. Я и так сильно затянул с этой частью, так что решил опубликовать все так. Я продолжу модернизации и буду писать дальше. Идей довольно много. Надеюсь, будет интересно, и не так долго. Спасибо всем!
#define TEST_RAM_BYTES 255
#define O_SR_CLOCK_PIN 2
#define O_SR_LATCH_PIN 3
#define O_SR_DATA_PIN 4
#define O_SR_OUTPUT_ENABLE_PIN 5
//////////////////////////////////////////////////////////////////////////
#include
#include
//////////////////////////////////////////////////////////////////////////////
// pin definitions
#define TFT_CS 13 // not used
#define TFT_DC 0
#define TFT_RST 1
// CPU defines
#define CPU_CLOCK_PIN 8
#define CPU_RESET_PIN 9
#define CPU_ENABLE_PIN 10
#define BUS_PORT_0_5 PINC
#define BUS_PORT_6_7 PIND
#define BUS_DDR_0_5 DDRC
#define BUS_DDR_6_7 DDRD
// SR defines
#define SR_PORT PORTD
#define SR_DDR DDRD
#define SR_CLOCK_PIN B00000100 // Pin 2
#define SR_LATCH_PIN B00001000 // Pin 3
#define SR_DATA_PIN B00010000 // Pin 4
#define SR_OE_PIN B00100000 // Pin 5
// 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 (255, 0, 0);
TFTscreen.fill (255, 0, 0);
// 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
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
resetSpectrum ();
}// setup
//////////////////////////////////////////////////////////////////////////
void pShiftOut (uint16_t val) {
// bit 15 — CE pin
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 — OE pin
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
//////////////////////////////////////////////////////////////////////////
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++;
}
//////////////////////////////////////////////////////////////////////////////
void readLine (uint8_t lineNum) {
SR_PORT &= ~SR_OE_PIN;
for (uint8_t i=0; i
scrBuffer[i] = BUS_PORT_0_5;
scrBuffer[i] |= BUS_PORT_6_7 & B11000000;
}
SR_PORT |= SR_OE_PIN;
}
//////////////////////////////////////////////////////////////////////////////
<