1. Если Вы видите это сообщение, значит, вы ещё не зарегистрировались на нашем форуме.

    Зарегистрируйтесь, если вы хотите принять участие в обсуждениях. Перед регистрацией примите к сведению:
    1. Не регистрируйтесь с никами типа asdfdadhgd, 354621 и тому подобными, не несущими смысловой нагрузки (ник должен быть читаемым!): такие пользователи будут сразу заблокированы!
    2. Не регистрируйте больше одной учётной записи. Если у вас возникли проблемы при регистрации, то вы можете воспользоваться формой обратной связи внизу страницы.
    3. Регистрируйтесь с реально существующими E-mail адресами, иначе вы не сможете завершить регистрацию.
    4. Обязательно ознакомьтесь с правилами поведения на нашем форуме, чтобы избежать дальнейших конфликтов и непонимания.
    С уважением, администрация форума Old-Games.RU
    Скрыть объявление
  2. Пожалуйста, внимательно прочитайте правила раздела.

Ассемблер i8080: уроки, практика, ПК-01

Тема в разделе "IBM PC-несовместимое", создана пользователем Zelya, 27 янв 2012.

  1. Zelya

    Zelya

    Регистрация:
    20 апр 2007
    Сообщения:
    668
    Добрый день!

    Предлагаю всем желающим освоить азы программирования на ассемблере для процессора i8080. В качестве платформы, выбран мой родной ПК-01 "Львов". Инструментом будет моя давняя "наработка" - Lviv Studio. Сразу хочу предупредить, что писалась она урывками, разными стилями и никогда не "допиливалась". Так что работоспособность у нее на уровне ранней альфы. Надеюсь, в процессе использования, с вашей помощью, удасться довести ее до нормального состояния. И так, что нам понадобится:

    Студия (.NET framework 2.0),

    Справочник по командам i8080,

    Руководство программиста ПК-01 "Львов"

    ---------- Сообщение добавлено в 13:48 ---------- Предыдущее сообщение размещено в 13:44 ----------

    Урок 1. Знакомство со студией. Первая программка. Память и видеопамять "Львова"

    Для начала запустим студию (LvivStudio.exe) и создадим проект:

    [​IMG]

    В левой части экрана откроется Solution Explorer, где мы можем найти файл File1.asm. Откроем его двойным кликом:

    [​IMG]

    Теперь, с помощью контекстного меню (по правому клику на проекте и файле) выберем их настройки и пропишем указанные числа:

    [​IMG] [​IMG]

    В тело файла впишем код

    Код:
    	MVI a, 0
    	OUT 0C2h
    	MVI a, 0
    	STA 0BE38h
    	CALL 0EBBCh
    	MVI a, 255
    	STA 5010h
    

    Запустим программу:

    [​IMG]

    Мы видим черный экран и небольшую красную линию на нем. Теперь давайте разберемся, что мы сделали. Для начала разъясним, что это за магический параметр 32768, который мы вводили аж три раза. Так вот StartAddress проекта - это стартовый адрес в ОЗУ, куда мы разместим нашу программу, RunAddress - это "точка входа", начало исполняемого когда. С нее процессор начнет читать и исполнять программу. Address файла - это, соответственно адрес куда, мы положим файл. Так как файл у нас один, то его адрес совпадает с StartAddress проекта. Почему же именно 32768. Для этого надо разобраться со структурой памяти ПК-01 Львов:

    [​IMG]

    Всего процессор i8080 может адресовать 64KB памяти. Верхния 16КВ во "Львове" заняты ПЗУ. Память от 32КВ до 48КВ - обычная ОЗУ. А вот нижние 32КВ переключаются в зависимости от выбранного режима. Это может быть ОЗУ, а может быть видеопамять. Причем, так как видеопамять для ПК-01 составляет всего 16КВ, то в режиме "видео" нижние 16КВ попросту не используется. Так как наша программка учебная, для простоты мы разместим ее в блоке 32КВ - 48КВ. Поэтому и начальный адрес - 32768.
    Давайте коротко рассмотрим, что же все-таки делает наша программа. Первые две строки переключают память в режим "видео". Теперь нам становятся надоступные все данные из нижних 32КВ ОЗУ, зато, все что мы запишем между 16КВ и 32КВ (адреса 4000h - 8000h) - будет отображаться на экране. Следующие три строчки вызывают системную подпрограмму из ПЗУ, которая очищает экран (заполная всю видеопамять ноликами). В последних двух строках мы записываем в видеопамять значение 255 по адресу 5010h и получаем красную линию. Почему так, рассмотрим немного ниже. А сейчас я хочу обратить ваше внимание, что студия поддердживает двоичную, десятиричную и шестадцатиричную системы записи. Просто число - десятиричное, h в конце - шестнадцатиричное, b - двоичное.
    Теперь, чтобы понять откуда взялась линия, мы поверхносно рассмотрим видесистему "Львова". Экран имеет расширение 256х256 пикселей, одновременно можно исползовать 4 цвета из 8-ми возможных. Поэтому каждый байт видеопамяти кодирует 4 пикселя по горизонтали - по два бита на пиксель. За первый пиксель отвечают нулевой и четвертый биты, за второй - первый и пятый и т.д. Мы использовали число 255 = 11111111b и получили для кажого пикселя цвет 11b (в старовой плаитре - красный). Поробуйте поменять его на 240 (11110000b), цвет станет 10b - зеленый. 11000011b - даст первые два пикселя зелеными (10b), а последние два снимими (01b). 00b - черный цвет (в данной палитре). Под конец хочу напомнить - адрес видеопамяти находится в передлах 4000h и 8000h.

    В следующем уроке мы рассмотрим регистры процессора i8080, и тогда уже более близко познакомимся с нашией программой.

    В случае возникновения вопросов и багов в студии - пишите в этой теме.
     
    Последнее редактирование модератором: 19 июл 2015
    AndyFox, A.P.$lasH, Amberus и 18 другим нравится это.
  2.  
  3. Zelya

    Zelya

    Регистрация:
    20 апр 2007
    Сообщения:
    668
    И так, вопросов по первому уроку не возникло. Это может значить, что все понятно или непонятно ничего. Для первой группы продолжаем обучение. Следующий урок обещает быть намного сложнее:

    Урок 2. Регистры. Некторые операции над ними.

    Для начала усложним нашу программку:
    Код:
    	MVI A, 0	;32768	
    	OUT 0C2h	;32770 	Video Memory
    	MVI A, 0	;32772	------------
    	STA 0BE38h	;32774
    	CALL 0EBBCh	;32777	Clear screen
    	LXI H, 5010h	;32780	------------
    	LXI D, 63	;32783	Preapre Data
    	MVI A, 8	;32786
    	MVI M, 255	;32788	------------
    	INX H		;32790
    	MVI M, 255	;32791	Cycle 1
    	DAD D		;32793
    	DCR A		;32794
    	JNZ 32788	;32795	-------------
    	JMP 32798	;32798	Cycle 2
    
    Теперь при запуске она рисует красный квадрат 8х8 пикселей:

    [​IMG]

    Чтобы понять смысл прогрммы, рассмотрим регистры процессора i8080.
    Процессор i8080 имеет в наличии два 16-биытнх регистра (PC, SP) и восемь 8-ми битных (A, B, C, D, E, H, L, F). Эмулятор позволяет просматривать их в процессе исполнения программы (нужно поставить галочку Debug):

    [​IMG]

    Самым важным регистром, без сомнения, является PC - счетчик команд. Он указывает на адрес текущей команды. После выполнения, он автоматичекси меняет свое значение. Все команды имеют размер от 1 до 3-х байт, и после их выполнения счетчик увеличивается на значение от одного до трех. В примере напротив каждой команды я указал ее адрес. Само собой, что адрес первой команды (32768) равный параметру RunAddress, он то и присвоится PC при старте. Особый интерес представляют команды перехода и вызова процедур. В нашем примере таких три: CALL (безусловный вызов процедуры), JMP (безусловный переход) и JNZ (один из видов условного перехода). После каждой из таких команд мы указываем адрес, который примет на следующем шаге счетчик (если оператор безусловный - всегда, если условный, в зависимости от проверки). Обратим внимание на последнюю строку (Cycle 2). Такой цикл позволяет грамотно завершить программу. В первом варианте (урок 1), если б за нашей программой оказался какой-либо код, он начал бы испонятся. Теперь же мы зацикливаем процессор на адресе 32798.
    Следующим 16-битным регистром идет SP - указатель стека. Он показывает адрес вершины стека. Ваша задача следить, чтобы он не "налазил" на область полезных данных. При любом вызове процедуры или перемещении в стек числа, SP уменьшает свое занчение на 2, а последующие два байта памяти занимаются стеком. Соответственно, если забрать число из стека или вызвать возврат из процедуры, то SP увеличивается на 2, "освобождая" два байта. Я специально поставил кавычки, так как значение в памяти не меняются, но данные в ней уже не несут пользы.
    А вот с восьмибитными нам придется работать намного чаще. Сразу отмечу, что из них можно составить четыре 16-битных пары: BC, DE, HL, PSW(A+F). Процессор i8080, хоть и восьибитный, но подерживает некоторые шеснадцатибитные операции (восновном для адессации).
    Самы используемый регистр - A - аккумулятор. Единственный регистр, над которым можно проводить все доступные математические и логические операции. К регистру F (регистр флагов) мы имеем только косвенный доступ. Он держит в себе пезультаты операций, которые могут быть использованы в условиях (например, знак или равенстов нулю). Остальные шесть регистров схожи между собой и имеют ограниченный, по сравнению с A набор возможностей. Стоит отметить только пару HL, которая обладает рядом интересных функций.
    Вернемся к нашей программке. Попробуем разобраться с блоком "Clear screen". Первый оператор, который мы используем - MVI (move immediate), позволяет поместить в любой из семи регистров (про F забываем) непосредственное число. Само собой, восьмибитное: 0-255. Мы поместили в аккумулятор чило 0. Следующий оператор - STA (store A), уникальный только для A - позволяет записать значение аккумулятора в память по указанному 16-битному адресу. Обратный ему LDA (load A) - читает в аккумулятор значение из памяти. Третий оператор CALL - вызов подпрограммы по адресу. Подпрограмма схожа с процедурами высокоуровневых языков программирования и должна иметь завершающую команду RET (return), которая вернет нас к месту вызова по окончанию работы. Чтобы окончательно закончить разбор этих трех строк обратимся к руководству программиста ПК-01. Там описаны все полезные функции из ПЗУ. По используемому адресу (0EBBCh) находится следующая процедура:
    И в примечании указан адрес BORDER - BE38H. Так что мы первыми двумя строками "положили" в переменную BORDER значение 0, а потом вызвали любезно предоставленную разработчиками ПК-01 процедуру по заполнению этим нулем всей видеопамяти. Конечно, мы могли бы и сами заполнить видеопамять, но зачем изобретать велосипед?
    Теперь рассмотрим блок "Preapre Data"

    Код:
    	LXI H, 5010h	;32780	------------
    	LXI D, 63	;32783	Preapre Data
    	MVI A, 8	;32786
    
    В первых двух строках мы используем операцию LXI, которая позволяет загрузить в регистровую пару BC, DE, HL непосредственное 16-битное число. Отмечу, что вместо названия пары, мы пишем только первую букву. И так, в HL мы положим адрес видеопамяти, откуда мы начнем рисовать свой квадрат, а в DE смещение видеопамяти по строке. Что это такое рассмотрим немного ниже. Аккумулятор мы будем использовать, как переменную цикла, и в последней строке задаем ему значение 8 (8 итераций цикла).
    И так, сам цикл отрисовки квадрата (всего шесть команд):

    Код:
    	MVI M, 255	;32788	------------
    	INX H		;32790
    	MVI M, 255	;32791	Cycle 1
    	DAD D		;32793
    	DCR A		;32794
    	JNZ 32788	;32795	-------------
    
    Оператор MVI мы уже знаем, а вот про регистр M я еще не разу не говорил. Дело в том, что в реальности такого регистра нет, а все это "магия" пары HL. Через M мы можем обращаться непостредственно в память по адресу указаному в HL. Следующие фрагменты индетичны:

    Код:
    	LXI H, 5010h
    	MVI M, 255
    
    Код:
    	MVI A, 255
    	STA 5010h
    
    За исключением того, что использование регистра (буду все-таки его так называть) M в определенных случаях приносит дополнительное удобство. Так вот, строкой MVI M, 255 - мы записывем по адресу указанному в HL (5010h) число 255, что приводит (как мы знаем по первому уроку) к появлению красной линии в четыре пискеля. Дальше мы вызывает команду INX, которая увеличивает значение регистровой пары на единицу. После нее в HL будет значение 5011h, и следующий MVI M, 255 нарисует еще 4 пикселя правее. Мы уже получили линию длиной 8 пикселей. За ним идет очень мощный оператор DAD, который позволяет сложить 16-битные значения HL и другой регистровой пары (в нашем случае DE). Пришло время рассотреть откуда взялось 63 в DE. Легко посчитать, что при расширении экрана в 256 пикселей, и при кодировании 4 пикслея на байт, одна экранная строка занимает 64 байта. Так что, для перехода на одну строчку вниз нам надо прибавить к указатею на видеопамять (в нашем случае HL) 64. Но единицу мы уже прибавляли (INX H), так что достаточно добавить 63. Строкой DCR A (decrease) мы уменьшаем аккумулятор на единицу. А теперь мы вспомним о незримом регистре F. Если после этого вычетания результат будет равено нулю, F отметит это специальным флагом. Это дает нам возможность использовать оператор условного перехода JNZ 32788 (jump not zero). Как он отработает: если флаг zero не установлен (not zero) мы "джампаем" по указанному адресу и повторяем тело цикла. Если же аккумулятор усеньшился до нуля (прошли восемь итераций), переход не сработает и мы перейдем к строке 32798 (вечному циклу). К сожалению, в нынешней редакции эмулятора, мы не можем посмотреть отдельно значения флагов регистра F. Может, в будущем исправлю.
    Заканчивая этот сложный урок, хочу сразу ответить на возможный вопрос "неужели мы должны вести нумерацию всех строк?!". Конечно же нет. И поэтому обещаю в следующем уроке рассмотреть метки и переменные.

    Как всегда жду вопросов и пожеланий.
     
    Последнее редактирование модератором: 19 июл 2015
    AndyFox, A.P.$lasH, Amberus и 13 другим нравится это.
  4. Zelya

    Zelya

    Регистрация:
    20 апр 2007
    Сообщения:
    668
    Хочу поздравить всех тех, кто преодолел второй урок. Теперь вы обязательно сможете написать игру для ПК-01. Открою секрет: мы ее и напишем в процессе обучения. Сложные темы еще будут, но таких резких скачков больше не предвидится. И так, переходим к уроку три:

    Урок 3. Метки. Переменные. Процедуры.

    Обратимся к нашей программке из второго урока. Всем она хороша, но вот отслеживать адреса на которые нужно осуществлять переходы очень неудобно. Да и не нужно. Для этого нам помогут метки (заголовки) строк. Строго говоря, они не относятся к ассемберу i8080 и в исполняемый код никак не попадут. Но для удобства программирования их поддерживают многие компиляторы, в том числе и студия. Меткой может служить любая сторка, с двоеточием в конце. Адаптируем программу:

    Код:
    	MVI A, 0	;	
    	OUT 0C2h	; 	Video Memory
    	MVI A, 0	;	------------
    	STA 0BE38h	;
    	CALL 0EBBCh	;	Clear screen
    	LXI H, 5010h	;	------------
    	LXI D, 63	;	Preapre Data
    	MVI A, 8	;
    cycle1:	MVI M, 255	;	------------
    	INX H		;
    	MVI M, 255	;	Cycle 1
    	DAD D		;
    	DCR A		;
    	JNZ cycle1	;	-------------
    cycle2:	JMP cycle2	;	Cycle 2
    
    Намного удобнее, не правда ли? Кстати, возможно вы заметили, что студия имеет регистронезависимый редактор. То есть строки "cycle1" и "CYCLE1" для него идентичны (как и операторы ассемблера и все другое). Если мы добавили метки, компилятор сам вычислит их адрес и перед созданием бинарного файла заменит названия на нужное число. Еще одно "кстати", насчет бинарного файла. После каждого запуска/компиляции в папке с проектом появляется файл с расширением .lvt. Это и есть бинарник с нашей программой, который вы можите запустить в любом эмулятора "Львова".
    Переходим к переменным. Это еще одно удобсто, которое не имеет отношения к i8080. Если метки подразумевают адрес сторки, то переменным можно присвоить любое значение, которое компилятор вставит на их место:

    Код:
    	BORDER = 0BE38h
    	CLEAR_SCREEN = 0EBBCh
    
    	MVI A, 0	;	
    	OUT 0C2h	; 	Video Memory
    	MVI A, 0	;	------------
    	STA BORDER 	
    	CALL CLEAR_SCREEN
    	LXI H, 5010h	;	------------
    	LXI D, 63	;	Preapre Data
    	MVI A, 8	;
    cycle1:	MVI M, 255	;	------------
    	INX H		;
    	MVI M, 255	;	Cycle 1
    	DAD D		;
    	DCR A		;
    	JNZ cycle1	;	-------------
    cycle2:	JMP cycle2	;	Cycle 2
    
    Теперь код читается намного легче. С вызовами процедур мы уже немного познакомились в пердыдущих уроках. Теперь же попробуем написать свою. Как уже было сказано выше, процедура отличается от обычного перехода (jump, в других языках goto) тем, что из нее можно вернуться в точку вызова. Когда мы делаем вызов CALL, процессор заносит в стек значение адреса, куда следует вернуться (на вершину стека указывает SP). Когда же мы доходим до оператора RET, значение берется из стека и по этому адресу делается переход. Так что со стеком нужно быть осторожным, чтобы не "сломать" адреса. Оформим рисование кавдрата в процедуру:

    Код:
    	BORDER = 0BE38h
    	CLEAR_SCREEN = 0EBBCh
    
    	MVI A, 0	;	
    	OUT 0C2h	; 	Video Memory
    	MVI A, 0	;	------------
    	STA BORDER 	
    	CALL CLEAR_SCREEN
    	CALL draw
    main_cycle:
    	JMP main_cycle
    
    draw:	LXI H, 5010h	
    	LXI D, 63	
    	MVI A, 8	
    draw_cycle:
    	MVI M, 255	
    	INX H		
    	MVI M, 255	
    	DAD D		
    	DCR A		
    	JNZ draw_cycle
    	RET
    
    Теперь усовершенствуем нашу процедуру. Будем ей передавать через пару HL адрес начала квадрата и через регситр B его цвет:

    Код:
    	BORDER = 0BE38h
    	CLEAR_SCREEN = 0EBBCh
    
    	MVI A, 0	;	
    	OUT 0C2h	; 	Video Memory
    	MVI A, 0	;	------------
    	STA BORDER 	
    	CALL CLEAR_SCREEN
    	MVI B, 255
    	LXI H, 5001h	
    	CALL draw
    	MVI B, 15
    	LXI H, 5007h	
    	CALL draw
    	MVI B, 240
    	LXI H, 500Dh	
    	CALL draw
    main_cycle:
    	JMP main_cycle
    draw:	
    	LXI D, 63	
    	MVI A, 8	
    draw_cycle:
    	MOV M, B	
    	INX H		
    	MOV M, B	
    	DAD D		
    	DCR A		
    	JNZ draw_cycle
    	RET
    
    Что мы сделали? Во первых мы заменили в процедуре MVI M, 255 на MOV M, B. Оператор MOV позволяет присваивать значение правого регистра левому. Так что теперь в память мы будем ложить не конкретное число, а то, что находится в регистре B. Дальше, LXI H мы вынесли из процедуры, чтобы иметь возможность задать любое значение. И теперь, перед каждым вызовом CALL draw, мы указываем где мы хотим поместить квадрат и какого он должен быть цвета. Как результат имеем:

    [​IMG]

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

    Вопросы, пожелания - приветствуются.
     
    Последнее редактирование модератором: 19 июл 2015
    AndyFox, A.P.$lasH, Amberus и 14 другим нравится это.
  5. Zelya

    Zelya

    Регистрация:
    20 апр 2007
    Сообщения:
    668
    Начиная с этого урока, наша программа начнет сильно разрастаться. Поэтому, я буду приводить только фрагменты кода и только в конце урока уже всю программу. Начнем:

    Урок 4. Игровое поле. Математические операции.

    Как я обещал в предыдущем уроке, мы оперделимся с конечной целью наших уроков. А это будет простенькая игра - xonix. Так что все последующее обучение пойдет в русле написания игры. Приступим к важному шагу - планированию игрового пространства. Все игровое поле "ксоникса" разделено на клетки. Так как мы уже умеем рисовать квадратики 8х8, то выберем размер поля 32х32 (как раз полностью займет экран). Координаты, пока, примем привычные всем: 0-31, как по X так и по Y. Теперь адаптируем процедуру draw, чтобы передавать ей не видеоадрес в паре HL, а координаты игрового поля, в H - Y, в L - X, и в теле процедуры из игровых координат получать нужный адрес. Для этого выведем простейшие формулы:
    Код:
    H = H*2 + 64; L = L*2
    Коротко рассмотрим, как мы их получили. Для начала упомяну, что в паре HL, значение формируется в виде H*256 + L. Как мы уже знаем, каждый байт видеопамяти кодирует четыре пикселя по оси X. Так как квадрат имеет ширину 8 пикселей, то каждая позиция по Х игрового поля увеличивает значение L на 2. Для оси Y схожие рассуждения. Так как на каждую экранную строку мы тратим 64 байта, то H увеличивается через каждые 4 позиции по Y. Чтобы было понятнее я напишу значения для нулевых пикселей каждой строки:

    Код:
    строка 0: H = 64, L = 0
    строка 1: H = 64, L = 64
    строка 2: H = 64, L = 128
    строка 3: H = 64, L = 192
    строка 4: H = 65, L = 0
    Теперь видно, что клетка высотой 8 пикселей увеличивает значение H на те же 2. Откуда взялось 64 в строке 0? Видеопамять начинается с адреса 4000h, это соответствует H = 64, L = 0.
    Имея формулы, возьмемся за процедуру draw. Нам нужно умножить L и H на 2. Но, процессор i8080 не имеет операции умножения. Конечно, можно реализировать свою процедуру, но алгоритмы умножения - довольно медленные. Тут нам на помощь прийдут побитовые операции. Рассмотрим число, скажем, 9 в двоичной системе - 00001001b. Давайте сместим все биты влево на одну позицию, мы получим 00010010b, что равняется 18! Побитовое смещение влево умножает число на два, также как вправо делит. Кто не верит - может легко нагуглить доказательство, а мы получили сильный инструмент в свои руки: делить и умножать на числа-степень двойки с потрясающей скоростью. Процессор i8080 дает нам несколько команд сдвигов. В данном случае нам будет удобно использовать RLC (Roll Left Cyclic). Немного смущает слово "Cyclic". Да, этот процесс циклический, и следует внимательно следить, чтобы перемещенные биты-единички не портили результат (особенно при делении). Но в нашем случае (числа всего-то 0-31) никаких проблем быть не может. От слов к делу, умножаем:

    Код:
    	MOV A,L
    	RLC
    	MOV L,A
    
    	MOV A,H
    	RLC
    	MOV H,A
    RLC, как и большинство математических операций работает только с аккумулятором. Так что мы используем дополнительные MOV. Теперь осталось прибавить к H 64. В Этом нам поможет команда ADD, которая складывает A c любым другим регистром.

    Код:
    	MVI A, 64
    	ADD H
    	MOV H,A
    
    Вот и все! Теперь потестируем адаптированную процедуру:

    Код:
    	BORDER = 0BE38h
    	CLEAR_SCREEN = 0EBBCh
    
    	MVI A, 0	;	
    	OUT 0C2h	; 	Video Memory
    	MVI A, 0	;	------------
    	STA BORDER 	
    	CALL CLEAR_SCREEN
    	MVI B, 255
    	MVI H,0
    	MVI L,0
    	CALL draw
    	MVI B, 15
    	MVI H,1
    	MVI L,1
    	CALL draw
    	MVI B, 240
    	MVI H,2
    	MVI L,2
    	CALL draw
    main_cycle:
    	JMP main_cycle
    draw:	
    	MOV A,L
    	RLC
    	MOV L,A
    
    	MOV A,H
    	RLC
    	MOV H,A
    
    	MVI A, 64
    	ADD H
    	MOV H,A
    
    	LXI D, 63	
    	MVI A, 8	
    draw_cycle:
    	MOV M, B	
    	INX H		
    	MOV M, B	
    	DAD D		
    	DCR A		
    	JNZ draw_cycle
    	RET
    
    Мы снова рисуем три квадратика, но теперь по координатам игрового поля (0,0), (1,1), (2,2)
    [​IMG]

    Ну, на последок, еще один сюрприз. Попробуйте выполнить "домашнее задание". Зарисуйте весь экран квадратиками. Все для этого мы уже знаем, и циклы и процедуру "волшебную". Но есть одна маленькая "изюминка", которую решить самому будет приятно. А в следующем уроке мы будем "рисовать" в памяти карту.

    Вопросы? ;)
     
    Последнее редактирование модератором: 19 июл 2015
    AndyFox, A.P.$lasH, Amberus и 9 другим нравится это.
  6. SAS io.sys

    SAS

    Администратор

    Регистрация:
    8 июл 2003
    Сообщения:
    18.589
    Zelya, спасибо за интересный материал.
    По поводу вопросов: ты достаточно сильно всё "разжёвываешь", поэтому вопросов у людей, имеющих базовые знания асма и железа компьютера, практически не должно возникнуть.
     
    Zelya и hobot нравится это.
  7. VorteX DrAgON Троллей не кормлю, сами сдохнут.

    VorteX DrAgON

    Legacy

    Регистрация:
    20 сен 2004
    Сообщения:
    3.049
    Слова "адрес", "адресовать" пишутся с одной "с". А тут практически везде "сс"... как будто змей понаползло. Прошу исправить.
     
    AndyFox, Zelya и Bato-San нравится это.
  8. Bato-San Чеширский волк-киборг

    Bato-San

    Регистрация:
    24 июн 2010
    Сообщения:
    13.310
    VorteX DrAgON, Это мелочь. Оно ещё и с двумя "д" может писаться. Ибо правильно вот так - address (уроки сперва писались на английском ?:suicide:).:rolleyes:

    На самом деле мне больше понравилось вот это:
    :rolleyes:

    Но ошибок в словах много больше. Во всех уроках. Там тебе и "сторки" и "указатею" и многое другое. Неприятно, но не в этом суть.:rolleyes:

    А вот ошибка последнего урока:
    Что правильно для новичка читающего урок ? Или это специально ?

    Ну и жуткая манера всё переводить из шестнадцатиричной в десятичную в произвольном порядке (в норме стоит всё писать в шестнадцатиричной, а в скобках указывать десятичный эквивалент, если это требуется - иначе адресация (и её понимание) превращается в ад, а через РП - вообще). Хорошо не в восьмиричную.:rolleyes:

    Наводит на размышления. Не такая уж она и простенькая, если всё делать правильно. Ну и предполагается, что все знают, что есть такое xonix - ни тебе конкретного описания игропроцесса ни даже намёка на правила игры или ссылки на неё в каталоге игр. Ну и конечно все мечтают, что бы их первой большой программой был именно xonix.
    Да и попытка скрестить обучение новичков с написанием игры - приводит, в печатном варианте особенно, к путанице в голове у тех самых новичков. Одно плавно перетекает в другое и результат "в совершенстве владею компьютером - знаю ворд и эксель - это вот эти ярлычки".:rolleyes:

    Всё что написано выше - не жестокая критика. Это конструктивные замечания.:rolleyes:
     
    Последнее редактирование: 10 фев 2012
    Zelya нравится это.
  9. balakshin

    balakshin

    Legacy

    Регистрация:
    11 июл 2007
    Сообщения:
    2.333
    Традиционный хабракомментарий

    Zelya, я ничего не понял, но на всякий случай желаю удачи!
     
    Zelya и Bato-San нравится это.
  10. Zelya

    Zelya

    Регистрация:
    20 апр 2007
    Сообщения:
    668
    Всем огормное спасибо за критику. Была б такая после первого урока, к 4-му, уже б исправился :) Теперь по -порядку:

    VorteX DrAgON
    Всех прошу простить за безграмотность. Пишу быстро, перечитываю только раз. Иначе, как показывает практика, ничего бы не писал. Особенно трудно стало с двойными согласными. Сейчас, восновном, использую английский и украинский языки, в первом - почти всегда двойные, во втором - практически никогда. Так что мой и без того низкий офрографический скилл упал ниже плинтуса.
    "Адресс" исправил, и, надеюсь, запомнил ;)

    Bato-San
    Здесь имелось ввиду, что каждый кавдрат занимает в ширину два байта. И при увеличении координаты по X на один, адресс увеличивается на 2. Именно из-за этого мы и умножаем.
    Очень важное и дельное замечание. Впредь, буду указывать все в двух системах, и выкрою время перевести первые уроки тоже.
    Имелось ввиду, что простенький игровой процесс. Зато в ней будет использоваться почти все, что может заинтересовать программиста игр.
    Я думал, что писать как "найти максиамльный элемент массива" никто совем не захочет, а игра, может, и заинтересует.

    balakshin
    А можете уточнить место, начиная с которого Вы "потерялись". Я его попробую подбравить. Я был бы очень рад заинтересовать и научить всех, кому интересно.
     
    AndyFox, A.P.$lasH, Bato-San и ещё 1-му нравится это.
  11. Stephen

    Stephen

    Регистрация:
    22 дек 2011
    Сообщения:
    141
    Обвинять программиста в безграмотности так же глупо, как десантника в отсутствии интеллекта. Главное чтобы человек хорошо делал своё дело.:)
     
  12. Zelya

    Zelya

    Регистрация:
    20 апр 2007
    Сообщения:
    668
    Stephen,
    Простите, не согласен. Очень правильно обвинять меня в безграмотности, когда я действительно безграмоен. Будет стимул следить за собой. А программист - он в первую очередь образованный человек, и должен писать грамотно.

    ПС Кстати, а если честно, кто-нибудь пробовал запускать программу? Мне просто интересно, будут ли собственные варианты "домашнего задания" или рассмотрим все в следующем уроке?
     
    Bato-San нравится это.
  13. Guyver

    Guyver

    Регистрация:
    2 окт 2005
    Сообщения:
    4.964
    Я пробовал. Даже запустилось. Домашнее задание не осилил.
     
    Zelya нравится это.
  14. Bato-San Чеширский волк-киборг

    Bato-San

    Регистрация:
    24 июн 2010
    Сообщения:
    13.310
    Zelya, Просмотрел код, оптимизировать можно слегка, но не для новичков - тут у тебя всё правильно. А "сюрприз" - это про циклы что ли с сохранением регистров на стек ? С такими "сюрпризами" - отдельный урок нужен. :diablo: Или это про отсутствие контроля выхода за пределы видеопамяти ? Тоже неплохо.

    Представил щас, как народ пытается без цикла забить видеопамять по твоему методу.:rofl: Если бы кто такое сделал - на всю жизнь прокляли бы такую профессию. 64*32=2048 вызовов draw:rofl: Шутник:cray::good:
     
    Последнее редактирование: 10 фев 2012
  15. Zelya

    Zelya

    Регистрация:
    20 апр 2007
    Сообщения:
    668
    Bato-San,
    Ну, немного раскрою карты... подразумевалось сохранять регистры в переменные (в память) - это мы уже умеем :). Стек - незначительно быстрее, но куда менее очевидный.
     
    Bato-San нравится это.
  16. Bato-San Чеширский волк-киборг

    Bato-San

    Регистрация:
    24 июн 2010
    Сообщения:
    13.310
    Zelya, В случае i8080 стек - единственно адекватное решение как по размеру кода так и по читабельности программы в результате. Собсно - бог с ним. Давно так не смеялся. А прикинь - рекурсивный вызов сделать ? Ведь кто нить догадается, при такой постановке вопроса !:rolleyes:
     
    Zelya нравится это.
  17. hobot

    hobot

    Регистрация:
    6 авг 2009
    Сообщения:
    1.535
    Уважаемые слушатели (читатели) данных уроков - предложенная к написанию игра известна большинству из вас, поскольку является классической ( то есть очень очень древней и многократно переделанной для многих и многих платформ).
    А на самом деле XONIX - это аркада в которой нужно отвоёвывать пространство у хаотично летающих шариков, для реализации даже графикой можно пренебречь. В соседней теме "про эмуляторы" есть вид на игровое поле символьной версии для отечественных ЭВМ ДВК, ну и гугль никто не отменял.

    ---------- Сообщение добавлено в 18:46 ---------- Предыдущее сообщение размещено в 18:44 ----------

    Zelya, ещё раз спасибо за уроки, просто интересно почитать )))

    ---------- Сообщение добавлено в 18:49 ---------- Предыдущее сообщение размещено в 18:46 ----------

    ага вот и первоисточник же есть на сайте у нас )))
    QIX (1989, DOS)
     
    Zelya и Bato-San нравится это.
  18. Zelya

    Zelya

    Регистрация:
    20 апр 2007
    Сообщения:
    668
    И так, продолжаем обучение

    Урок 5. Стек. Сравнения. Стартовое поле.

    Прежде чем приступить к дальнейшему написанию игры, рассмотрим домашнее задание. Нам надо было зарисовать игровое поле квадратиками. Процедура у нас уже есть, цикл мы тоже умеем писать. Думаю и двойной цикл не был бы проблемой. Но мы сталкиваемся с очень частой бедой процессора i8080 - нехваткой регистров. Упомянутая "изюминка" заключается в том, чтобы сохранять значения итераций цикла в памяти с помощью LDA и STA. Рассмотрим следующий код:

    Код:
    	BORDER = 0BE38h
    	CLEAR_SCREEN = 0EBBCh
    	POS_X = 9C40h ; 40000
    	POS_Y = 9C41h ; 40001
    
    	MVI A, 0	;	Video Memory
    	OUT 0C2h	; 	Video Memory
    	MVI A, 0	
    	STA BORDER 	
    	CALL CLEAR_SCREEN
    
    	MVI A,31
    	STA POS_Y
    	
    init1:
    	MVI A,31
    	STA POS_X
    init2: 	LDA POS_X
    	MOV L,A
    	LDA POS_Y
    	MOV H,A
    	MVI B,15
    	CALL draw
    	LDA POS_X
    	DCR A
    	STA POS_X
    	JP init2
    	LDA POS_Y
    	DCR A
    	STA POS_Y
    	JP init1
    
    main_cycle:
    	JMP main_cycle
    Строчками POS_X = 9C40h ; 40000 и POS_Y = 9C41h ; 40001 мы "привязали" к переменными POS_X и POS_Y адреса. Тепер, в начале цикла мы записываем туда, с помощью аккумулятора, значения 31. В основном теле цикла (между init2: и JP init2) читаем значения POS_X и POS_Y и записываем их в L и H, соответственно. Потом, задем цвет в регистр B и вызываем нашу процедуру draw. Нарисовав квадарат, мы уменьшаем POS_X на единицу. Далее я использовал для сокращения кода неизвестный нам еще опертор JP. Это также один из видов условного перехода. Мы уже знакомы с JNZ, который "джампает", если полученное число не нуль. JP - "джампает", если полученное чило положительное. То есть, пока мы уменьшаем значение с 31 до 0, включительно, JP будет исправно переводить нас на строку init2. Когда же мы уменьшим 0, мы получим 255, но в регистр флагов F поставится отметка, что число стало отрицательным, и JP не отработает. Мы перейдем к строкам, которые уменьшают POS_Y, и делают аналогичную проверку. Когда же и POS_Y прейдет в "минус" мы попадем на вечный цикл - отрисовка закончена.
    Если вы обратили внимание на комментраии, то там Bato-San предлагал оптимизировать этот код, используя стек. Придется нам теперь научится работе со стеком. Как было сказано ранее, стек используется при вызове процедур, причем на его вершимну указывает регистр SP. После команды CALL адрес возврата (два байта) записывается в память перед SP, после чего сам указатель уменьшается на два, теперь показывая на начало записанных данных. Кода же процедура зваершается командой RET, адрес забирается из стека, а указатель SP увеличивается на два. Точно так же, как процедуры заносят/забирают в стек адреса, мы в любой момент можем занести/забрать туда любую регистровую пару, испоьзуя команды PUSH и POP и аргумент - первую букву пары. Например, PUSH H, положит в стек HL, а POP H, заберет в HL данные. При использовании стека нужно быть очень внимательным. Например, если вы в теле процедуры положили пару в стек, обязательно не забудьте забрать ее оттуда до команды RET. Иначе RET вычитает вместо адреса возварта ваши данные и исполнение программы перейдет по неожиданному адресу. Давайте теперь посмотрим, как стек уменьшит наш код:

    Код:
    	BORDER = 0BE38h
    	CLEAR_SCREEN = 0EBBCh
    
    	MVI A, 0	;	Video Memory
    	OUT 0C2h	; 	Video Memory
    	MVI A, 0	
    	STA BORDER 	
    	CALL CLEAR_SCREEN
    
    	MVI H,31
    init1:
    	MVI L,31
    init2: 	PUSH H
    	MVI B,15
    	CALL draw
    	POP H
    	DCR L
    	JP init2
    	DCR H
    	JP init1
    
    main_cycle:
    	JMP main_cycle
    Код теперь намного совершеннее. Но если вам на первых порах будет трудно прейти от привычных методов языков высокого уровня, можно использовать и переменные. Мы же оставим код со стеком (хотя в дальнейшем наглядность немного пострадает)
    Теперь мы можем позволить себе еще одну опитмизацию. Так как экран полностью зарисовывается игровым полем, больше не нужно вызывать очистку. Так что переменная BORDER и процедура CLEAR_SCREEN могут быть выкинуты.
    Перед созданием карты осталось решить две проблемы. Проблема первая. Хоть карта небольшая, для определенного удобства, я бы хотел сохранять ее в нижних адрессах ОЗУ. Но, включив в начале программы видеорежим мы "отрезали" нижние 32 килобайта. Чтобы решить эту проблему мы будем включать видеорежим в начале процедуры draw и отключать его в конце этой же процедуры. Код будет выглядить так:

    Код:
    	MVI H,31
    init1:
    	MVI L,31
    init2: 	PUSH H
    	MVI B,15
    	CALL draw
    	POP H
    	DCR L
    	JP init2
    	DCR H
    	JP init1
    
    main_cycle:
    	JMP main_cycle
    draw:	
    	MVI A, 0	;Video on
    	OUT 0C2h	;Video on
    
    	MOV A,L
    	RLC
    	MOV L,A
    
    	MOV A,H
    	RLC
    	MOV H,A
    
    	MVI A, 64
    	ADD H
    	MOV H,A
    
    	LXI D, 63	
    	MVI A, 8	
    draw_cycle:
    	MOV M, B	
    	INX H		
    	MOV M, B	
    	DAD D		
    	DCR A		
    	JNZ draw_cycle
    	MVI A, 2	;Video off
    	OUT 0C2h	;Video off
    	RET
    Теперь вне процедуры draw у нас будут доступны все 48К ОЗУ. Вторая проблема вылезет позже, когда мы будем писать игровую логику. На ассемблере довольно долго и неудобно проверять достигли ли мы конца экрана или нет. Поэтому я предлагаю пожертвовать памятью (для нашей игры ее и так замного), но сделать игровое поле 34х34, где крайние квадраты будут непрохоимы для всех персонажей. Видимое поле теперь будет иметь координаты не 0-31, а 1-32. Для этого нам надо будет лишь немного поправить процедуру draw, чтобы она на единицу подкорректировала координаты.

    Код:
    draw:	
    	MVI A, 0	;Video on
    	OUT 0C2h	;Video on
    
    	DCR L		;Coordinate -1
    	DCR H		;Coordinate -1
    	
    	MOV A,L
    	RLC
    	MOV L,A
    
    	MOV A,H
    	RLC
    	MOV H,A
    
    	MVI A, 64
    	ADD H
    	MOV H,A
    
    	LXI D, 63	
    	MVI A, 8	
    draw_cycle:
    	MOV M, B	
    	INX H		
    	MOV M, B	
    	DAD D		
    	DCR A		
    	JNZ draw_cycle
    	MVI A, 2	;Video off
    	OUT 0C2h	;Video off
    	RET
    Давайте теперь подумаем, какое игровое поле мы бы хотели получить. По краям мы поставим непроходимый байт, пусть, пока будет 127 (7F), если что, поменять можно в любой момент. Всередине, сделаем стартовую рамку, на которой будет находится игрок и один из типов врагов. Рамку сделаем толщиной две клетки и запоним значением 15 (0F). Остальное поле забьем ноликами. Также, желательно, для скорости, в одном цикле формировать поле и рисовать его. Получится что-то такое:

    Код:
    	MVI H,33
    init1:
    	MVI L,33
    init2: 	PUSH H
    	MOV A,L
    	CPI 0
    	JZ init_put127
    	CPI 33
    	JZ init_put127
    	MOV A,H
    	CPI 0
    	JZ init_put127
    	CPI 33
    	JZ init_put127
    	JMP init3
    init_put127:
    	MVI M, 127	;(7F)
    	JMP init4	;Don't draw
    init3:
    	MVI B,15	;Temporary use 15 (0F)
    	MOV A,L
    	CPI 3
    	JM init5	;Don't put 0
    	CPI 31
    	JP init5	;Don't put 0
    	MOV A,H
    	CPI 3
    	JM init5	;Don't put 0
    	CPI 31
    	JP init5	;Don't put 0
    	MVI B, 0
    init5:	
    	MOV M,B
    	CALL draw
    
    
    init4:	POP H
    	DCR L
    	JP init2
    	DCR H
    	JP init1
    
    main_cycle:
    	JMP main_cycle
    Теперь нас ждет много пояснений. Для начала мы должны узнать, что такое команда CPI. Это сравнение аккумулятора с числом. Она похожа на вычитание, только в реальности аккумулятор сохраняет свое значение, хотя все флаги рагистра F поменяются, как будто мы отняли это число. Срзау после строки init2 мы начинаем проверку регистров H и L (или же POS_Y и POS_X, которые были раньше). Переход JZ схожий с JNZ, но исполняется, если результат - нуль. Получается, что если одна из координат имеет значение 0 или 33, то мы находимся в "невидимой" области. В таком случае мы попадаем на строчку init_put127, где ставим непроходимое значение в память и переходим к строке init4 (так как рисовать "невидимую" область не надо). Если ни одна из проверок не отработала - мы переходим в игровой экран init3. Для начала, предполагаем значение 15 (0F) - "зарисованная" область, которая находится в рамке толщиной два квадрата. Потом начинаем проверки, схожие с предыдущими. Только теперь, мы должны учитывать толщину. Поэтому я испоьзую операторы JP и JM (то же, только минус). Чтобы было понятнее пройдемся пошагово:

    Код:
    	MOV A,L
    	CPI 3
    	JM init5	;Don't put 0
    Отнимаем от координаты POS_X тройку. Если мы попали в "минус", то POS_X 2 или меньше. Значит это "зарисованная" область, мы угадали и переходим к рисованию. Если же нет, то продолжаем гадать:

    Код:
    	CPI 31
    	JP init5	;Don't put 0
    Отнимаем 31 и проверям остались ли мы в плюсах. Если остались, значит координата 31 или больше - снова "зарисованная" рамка. Иначе продолжаем для POS_Y. Если все проверки не удались, значит это все-таки свободная область и мы ставим нолик.
    В результате мы получим стартовое игровое поле, которое так и просится чтобы на нем расставили персонажей:

    [​IMG]

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

    Полный листинг программы:

    Код:
    	MVI H,33
    init1:
    	MVI L,33
    init2: 	PUSH H
    	MOV A,L
    	CPI 0
    	JZ init_put127
    	CPI 33
    	JZ init_put127
    	MOV A,H
    	CPI 0
    	JZ init_put127
    	CPI 33
    	JZ init_put127
    	JMP init3
    init_put127:
    	MVI M, 127	;(7F)
    	JMP init4	;Don't draw
    init3:
    	MVI B,15	;Temporary use 15 (0F)
    	MOV A,L
    	CPI 3
    	JM init5	;Don't put 0
    	CPI 31
    	JP init5	;Don't put 0
    	MOV A,H
    	CPI 3
    	JM init5	;Don't put 0
    	CPI 31
    	JP init5	;Don't put 0
    	MVI B, 0
    init5:	
    	MOV M,B
    	CALL draw
    
    
    init4:	POP H
    	DCR L
    	JP init2
    	DCR H
    	JP init1
    
    main_cycle:
    	JMP main_cycle
    draw:	
    	MVI A, 0	;Video on
    	OUT 0C2h	;Video on
    
    	DCR L		;Coordinate -1
    	DCR H		;Coordinate -1
    	
    	MOV A,L
    	RLC
    	MOV L,A
    
    	MOV A,H
    	RLC
    	MOV H,A
    
    	MVI A, 64
    	ADD H
    	MOV H,A
    
    	LXI D, 63	
    	MVI A, 8	
    draw_cycle:
    	MOV M, B	
    	INX H		
    	MOV M, B	
    	DAD D		
    	DCR A		
    	JNZ draw_cycle
    	MVI A, 2	;Video off
    	OUT 0C2h	;Video off
    	RET
    Урок получился трудноватым. Так что задавайте вопросы.
     
    Последнее редактирование модератором: 19 июл 2015
    AndyFox, A.P.$lasH, hobot и 5 другим нравится это.
  19. Helmut Herr Mannelig

    Helmut

    Переводчик

    Регистрация:
    18 мар 2008
    Сообщения:
    5.853
    Лично я последний раз ASM-ом пользовался очень давно, в институте (для 8086), поэтому я, пожалуй, воздержусь от вопросов =) Но читаю с интересом. Главное, что-то даже вспоминаю и понимаю =)
     
    hobot и Zelya нравится это.
  20. Zelya

    Zelya

    Регистрация:
    20 апр 2007
    Сообщения:
    668
    А вот это зря. Смело задавайте вопросы по любому, начиная с первого, уроку. Вдруг внутри Вас терзается душа низкоуровневого программиста, и Ваша первая игра для ПК-01 потом вам проложит путь к отличной карьере. :)
     
  21. TheBypasser

    TheBypasser

    Регистрация:
    7 июл 2006
    Сообщения:
    756
    Ээ, кстати - а разве регистры на старте не заполнены мусором? Вроде как в коде инициализации памяти нет.
     
  1. На этом сайте используются файлы cookie, чтобы персонализировать содержимое, хранить Ваши предпочтения и держать Вас авторизованным в системе, если Вы зарегистрировались.
    Продолжая пользоваться данным сайтом, Вы соглашаетесь на использование нами Ваших файлов cookie.
    Скрыть объявление