Навигация по сайту
- Игры / Образы
- Игры на русском языке
- Коды / Советы / Секреты
- Наши переводы
- Наши проекты
- Игры на русском языке (OnLine)
- Эмуляторы
- Обзоры игр
- Информация
- Статьи
- Интервью
- Мануалы / Инструкции
Случайная игра
Вступай!!!
Облако тегов
Показать все теги
Программирование процессора 6502 (Programming the 6502)
Программирование Денди
Программирование Денди
Перевод: Maror
Предисловие.
Эта книга построена как полный учебник программирования для процессора 6502. Она будет полезна и тем, кто никогда не программировал раньше, и тем кто уже знаком с программированием 6502.
Эта книга даст нужный уровень знаний, чтобы Вы могли составлять программы для 6502. На самом деле, ни одна книга не сможет дать достаточно знаний, если нет практики. Но мы надеемся, что читатель будет тренироваться и выполнять упражнения, и книга ему в этом поможет.
Книга основана на опыте автора - он уже обучил программированию больше 1000 человек. Поэтому книга чётко структурирована.
Обучение идёт от простого к сложному. Читатели, знакомые с основами, могут пропустить введение. Тем же, кто никогда не программировал - возможно, даже придётся перечитывать последние разделы. Книга построена так, чтобы самые необходимые приёмы повторялись и запоминались. Поэтому очень желательно читать всю книгу по порядку. Для закрепления материала тут даны упражнения, их сложность постепенно возрастает. Решать их - обязательно, они показывают, действительно ли всё понято, и без их выполнения книга почти бесполезна. Некоторые из них могут занять много времени (например, задача на умножение). Но только если Вы выполните их, то сможете понять всю суть.
Для тех, кто прочитает эту книгу до конца и войдёт во вкус - рекомендуем книги на ту же тему:
"Приложения 6502" - посвящена системе ввода/вывода.
"Программирование 6502 для продвинутых" - посвящена сложным алгоритмам.
Другие книги серии посвящены программированию других популярных микропроцессоров.
Для тех, кто хочет укрепить свои знания - рекомендуем книги "От чипов к системам" и "Микропроцессорные интерфейсные технологии".
Автор благодарит "Rockwell International", который предоставил ему одну из первых систем разработки - ASM65.
Книга тщательно проверена. Но если Вы обнаружите какие-то ошибки и сообщите об этом - автор будет благодарен, и исправит их в следующих редакциях. Другие предложения и пожелания (например, какую-нибудь программу можно было бы написать лучше) - тоже будут учтены.
--------------------------------------------
Предисловие к Четвёртому изданию.
Все пять лет после публикации книги популярность микропроцессора 6502 экспоненциально возрастала, и продолжает расти !до сих пор. Эта книга увеличила его популярность ещё больше.
Во Втором издании размер книги увеличился более, чем на 100 страниц - особенно много информации было добавлено в главы 1-9. Но все другие разделы тоже были изменены и улучшены. В этом, Четвёртом издании, добавлены ответы к упражнениям (Дополнение I). Этих ответов ждали многие читатели, которые хотели убедиться, что сделали всё правильно.
Я хочу поблагодарить читателей предыдущих изданий за ценные идеи, как улучшить книгу. Особая благодарность - Эрику Новикову и Крису Вильямсу за их вклад в написание ответов к упражнениям; Дэниэлю Давиду за ценные предложения. Многие улучшения были предложены Филиппом Хупером, Джоном Смитом, Рональдом Лонгом, Чарльзом Кёрли, Н. Харрисом, Джоном МакКленоном, Дугласом Трасти, Флетчером Карсоном и профессором Мироном Кэлхоном.
-----------------------------------
Благодарности.
Автор выражает благодарность для "Rockwell International", особенно Скотту Максвеллу, который предоставил первую систему разработки для 65Х. Благодаря тому, что этот мощный инструмент был доступен - удалось отладить и испытать все программы из примеров Первого издания. Также выражаю благодарность профессору Мирону Кэлхону за его предложения.
---------------------------------------------------------
Глава 1. Основные понятия.
---------------------------------------------------------
Введение.
В этой главе объясняются главные понятия и основы компьютерного программирования. Если читатель уже знаком с ними - возможно, он захочет побыстрее перейти к Главе 2. Можно так и сделать, но даже опытным читателям прочитать первую главу всё равно надо. Во Введении описаны многие важные понятия: как двоичное сложение, BCD и другие. Одни из них станут новыми для читателя, о других (и об их применении) он узнает подробнее.
----------------------
Что такое программирование.
Если есть задача - должно быть и её решение. Это решение, составленное в виде пошаговой процедуры - называется АЛГОРИТМ. Алгоритм состоит из конечного число шагов, выраженных определённым языком или символами. Вот самый простой пример алгоритма:
1 - Вставить ключ в разъём.
2 - Повернуть ключ на полный оборот против часовой стрелки.
3 - Взять дверную ручку.
4 - Повернуть дверную ручку и нажать на дверь.
После шага 4 (если алгоритм выполнен правильно) - дверь будет открыта. Этот алгоритм можно назвать алгоритмом открытия двери.
Поскольку выполнение любой задачи выражается в виде алгоритма - для работы компьютера тоже нужен алгоритм. К сожалению, компьютеры пока не понимают ни английский, ни другие человеческие языки. Так получилось из-за двусмысленности человеческих языков (одно и то же слово может обозначать разные вещи). Компьютер может "понять" только чётко заданное "подмножество" человеческого языка. Такое подмножество называется Языком программирования.
Изложение алгоритма в виде инструкций на Языке программирования - называется программированием. Или (именно перевод алгоритма с человеческого языка на компьютерный) - кодингом. Программирование включает в себя не только кодинг, но и решение самой задачи, и структурирование текста программы.
Для эффективного программирования нужно знать не только решение самой задачи и стандартные алгоритмы - но ещё и устройство компьютера, его внутренних регистров, памяти и внешних устройств. И уметь как можно выгоднее использовать ресурсы компьютера. Этому будут посвящены следующие главы.
Также в программированиии важно документирование, чтобы программы были понятны другим разработчикам. Документирование должно быть внешним и внутренним.
Внутреннее документирование - это комментарии, поясняющие каждое действие. Они размещаются в самом тексте программы.
Внешнее документирование - это составление документов, объясняющих принципы программы (обзоров, учебников, блок-схем).
---------------------
Составление блок-схем.
Составление блок-схемы - это промежуточный шаг при переводе алгоритма на Язык программирования. Блок-схема - это символическое предстваление алгоритма в виде прямоугольников и ромбов, которые обозначают шаги алгоритма. Прямоугольники обозначают КОМАНДЫ. Ромбы обозначают ПРОВЕРКИ, например: "Если условие X выполнено - переходим к команде A. Если условие НЕ выполнено - переходим к команде B". Пока мы не будем подробно разбирать правила составления блок-схем.
Составление блок-схем очень рекомендуется при переводе алгоритма на Язык программирования. По наблюдениям, лишь 10% программистов могут обходиться без блок-схем. Но к сожалению, 90% программистов уверены, что они принадлежат именно к тем 10%. Как результат, 80% программ при первых запусках не работают или работают с ошибками (эти данные приблизительны). Начинающим программисты быстро убедятся в необходимости блок-схем. Им придётся потратить много времени на проверку и исправление программ (это называется Отладкой). Поэтому блок-схемы необходимы, их составление занимает немного времени, а перевести их в программный текст - легко. Также, некоторые программисты представляют блок-схемы в уме, не излагая их на бумаге. К сожалению, тогда программу будет тяжело понять другим, поскольку она не будет документирована. В общем, использовать блок-схемы мы будем, и их примеры будут в этой книге.
Пример блок-схемы:
НАЧАЛО ПРОГРАММЫ
|
v
____________________________
--------------------> |УЗНАЁМ НУЖНУЮ ТЕМПЕРАТУРУ | <---------------
| |____________________________| |
| | |
| v |
| ____________________________ |
| | УЗНАЁМ ТЕКУЩУЮ ТЕМПЕРАТУРУ | |
| |____________________________| |
| ПОВТОРЯЕМ | | ПОВТОРЯЕМ
| ЧЕРЕЗ v | ЧЕРЕЗ
| ОПРЕДЕЛЁННОЕ _____ | ОПРЕДЕЛЁННОЕ
| ВРЕМЯ / \ | ВРЕМЯ
| / \ |
| НЕТ / \ ДА |
| ________________/ТЕМПЕРАТУРА\________________ |
| | \ДОСТАТОЧНА?/ | |
| | \ / | |
| v \ / v |
| ____________ \_____/ ____________ |
| | ВКЛЮЧАЕМ | | ВЫКЛЮЧАЕМ | |
-- |ОБОГРЕВАТЕЛЬ| |ОБОГРЕВАТЕЛЬ|------
|____________| |____________|
Блок-схема регулирования температуры.
|
v
____________________________
--------------------> |УЗНАЁМ НУЖНУЮ ТЕМПЕРАТУРУ | <---------------
| |____________________________| |
| | |
| v |
| ____________________________ |
| | УЗНАЁМ ТЕКУЩУЮ ТЕМПЕРАТУРУ | |
| |____________________________| |
| ПОВТОРЯЕМ | | ПОВТОРЯЕМ
| ЧЕРЕЗ v | ЧЕРЕЗ
| ОПРЕДЕЛЁННОЕ _____ | ОПРЕДЕЛЁННОЕ
| ВРЕМЯ / \ | ВРЕМЯ
| / \ |
| НЕТ / \ ДА |
| ________________/ТЕМПЕРАТУРА\________________ |
| | \ДОСТАТОЧНА?/ | |
| | \ / | |
| v \ / v |
| ____________ \_____/ ____________ |
| | ВКЛЮЧАЕМ | | ВЫКЛЮЧАЕМ | |
-- |ОБОГРЕВАТЕЛЬ| |ОБОГРЕВАТЕЛЬ|------
|____________| |____________|
Блок-схема регулирования температуры.
---------------------------------
Представление информации.
Вся компьютерная информация содержится в виде цифр и символов. Существует внутреннее и внешнее представление информации.
---------------------------------
Внутреннее представление информации.
Вся компьютерная информация содержится в группах битов. Бит содержит в себе двоичное число (0 или 1). Из-за технических ограничений современная электроника использует только два состояния (включено/выключено, 0/1). Эти системы используют "логические" формулы - такие формулы называют ДВОИЧНОЙ логикой. В микропроцессорах, в том числе и 6502, биты группируются по 8 штук. Группа из восьми битов называется БАЙТОМ. Группа из четырёх битов называется СЛОГОМ (полубайтом).
Теперь о том, как двоичная информация представлена внешне. Два представления используются внутри компьютера. Первый - программа, второй - последовательность команд (включает в себя цифры и алфавитно-цифровой текст). Ниже разобраны три представления: программа, цифры, и алфавитно-цифровой текст.
----------------------------
Представление программы.
Все инструкции представлены одиночными или несколькими байтами. "Короткие" инструкции представлены одним байтом, "длинные" - двумя и больше. Поскольку процессор 6502 - 8-битный, его байты загружаются из памяти по одному. Поэтому однобайтовые операции исполняются быстрее, чем многобайтовые. Позже мы разберём особенности операций микропроцессора. И то, как использовать максимум однобайтовых операций (вместо многобайтовых), чтобы ускорить работу всей программы. Но 8-битность накладывает серьёзные ограничения, которые мы разберём в общих чертах. 6502 (как и любой микропроцессор) имеет чёткий набор инструкций. Эти инструкции заданы разработчиком, и перечислены в конце этой книги. Любая программа состоит из последовательности этих инструкций. Про инструкции процессора 6502 будет рассказано в Главе 4.
--------------------------------
Представление числовой информации.
Представления разных чисел могут отличаться. Существуют Целые, Знаковые (могут быть положительными/отрицательными) и Десятичные числа. Рассмотрим каждый вид подробнее.
Целые числа представлены в двоичной системе. В двоичной системе последняя цифра числа означает 2 в степени 0; предпоследняя - 2 в степени 1 и т. д:
Число в двоичной системе:
b7b6b5b4b3b2b1b0 = (b7 * 2 ^ 7) + (b6 * 2 ^ 6) + (b5 * 2 ^ 5) + (b4 * 2 ^ 4) + (b3 * 2 ^ 3) + (b2 * 2 ^ 2) + (b1 * 2 ^ 1) + (b1 * 2 ^ 0)
Степени двойки:
2 ^ 7 = 128
2 ^ 6 = 64
2 ^ 5 = 32
2 ^ 4 = 16
2 ^ 3 = 8
2 ^ 2 = 4
2 ^ 1 = 2
2 ^ 0 = 1
Всё, как в десятичной системе, только там множитель 10, а не 2:
123 = (1 * 10 ^ 2) + (2 * 10 ^ 1) + (3 * 10 ^ 0)
1 * 100 (10 ^ 2)
+ 2 * 10 (10 ^ 1)
+ 3 * 1 (10 ^ 0)
___________
123
Пример двоичного числа:
00001001 =
1 * 1 = 1 (2 ^ 0)
+ 0 * 2 = 0 (2 ^ 1)
+ 0 * 4 = 0 (2 ^ 2)
+ 1 * 8 = 8 (2 ^ 3)
+ 0 * 16 = 0 (2 ^ 4)
+ 0 * 32 = 0 (2 ^ 5)
+ 0 * 64 = 0 (2 ^ 6)
+ 0 * 128 = 0 (2 ^ 7)
_________________________
= 9 (в десятичной системе счисления)
Другой пример:
10000001 =
1 * 1 = 1
+ 0 * 2 = 0
+ 0 * 4 = 0
+ 0 * 8 = 0
+ 0 * 16 = 0
+ 0 * 32 = 0
+ 0 * 64 = 0
+ 1 * 128 = 128
__________________
= 129 (в десятичной системе)
Именно поэтому биты нумеруются справа налево: нулевой умножается на 2 ^ 0, первый - на 2 ^ 1 и т. д..
-------------------------
Неполная таблица десятичныех/двоичных чисел.
Десятичное | Двоичное
|
0 | 00000000
1 | 00000001
2 | 00000010
3 | 00000011
4 | 00000100
5 | 00000101
6 | 00000110
7 | 00000111
8 | 00001000
9 | 00001001
10 | 00001010
11 | 00001011
12 | 00001100
13 | 00001101
14 | 00001110
15 | 00001111
16 | 00010000
17 | 00010001
31 | 00011111
32 | 00100000
33 | 00100001
63 | 00111111
64 | 01000000
65 | 01000001
127 | 01111111
128 | 10000000
129 | 10000001
254 | 11111110
255 | 11111111
--------------------------
Упражнение 1.1
Чему равно (в десятичной системе) двоичное число "11111100"?
--------------------------
Перевод из десятичной системы в двоичную.
Теперь попробуем перевести десятичное число 11 в двоичную систему:
11 делим на 2 = 5 (остаток 1 - старший бит = 1)
5 делим на 2 = 2 (остаток 1 - следующий бит = 1)
2 делим на 2 = 1 (остаток 0 - следующий бит = 0)
1 делим на 2 = 0 (остаток 1 - нулевой бит = 1)
Результат: 1011 (Нулевой бит - самый правый, старший - самый левый. Составляем из них число.).
То есть. Делим десятичное число на 2, пока оно не останется ноль. А из остатков получаем биты.
--------------------
Упражнение 1.2
Переведите в двоичную систему число 257.
Упражнение 1.3
Переведите число 19 в двоичную систему, и обратно.
---------------------------
Операции с двоичными числами.
Арифметические действия в двоичными числами - просты. Сложение:
0 + 0 = 0
0 + 1 = 1
1 + 0 = 1 (от перестановки слагаемых сумма не меняется)
1 + 1 = (1)0 = 10
В последнем примере единица переносится в верхний разряд. Двоичное вычитание выполняется как "прибавление слагаемого". Мы вспомним про это него, когда объясним представление отрицательных чисел.
Другой пример сложения:
В десятичной системе:
2
+ 1
___
= 3
В двоичной:
10
+ 01
____
= 11
То есть, всё то же самое. Складываем числа по столбцам, с правого по левый. Сначала складываем правый столбец: 0 + 1 = 1, переноса нет. Потом левый: 1 + 0 = 1 (переноса нет).
----------------
Упражнение 1.4
Сложите двоичные числа 5 и 10. Убедитесь, что результат получился равным 15.
----------------
Другие примеры двоичного сложения:
0010
+ 0001
=
______
0011
0011
+ 0001
______
= 0100
В последнем примере используется перенос.
В самом правом столбце 1 + 1 = (1)0. То есть, перенос равен 1. Этот перенос прибавляется к следующему биту. В следующем столбце получается 1 + 0 + 1(перенос) = (1)0. В третьем столбце будет 0 + 0 + 1 = 1. Итого: 100.
Другой пример:
0111
+ 0011
______
= 1010
----------------
Упражнение 1.5
Выполните действие:
1111
+ 0001
______
=
Результат получится 4-битным?
----------------------------
В восьми битах можно хранить числа от 00000000 до 11111111, то есть от 0 до 255. Но сразу видны две проблемы. Во-первых, так можно хранить только положительные числа. Во-вторых, 255 - это мало, могут понадобиться и большие числа. Теперь о каждой проблеме - подробнее.
------------------------------------
Знаковые двоичные числа.
В знаковых двоичных числах самый левый бит (седьмой) обозначает знак. По традиции, 0 - означает, что число положительное; 1 - что число отрицательное. Например. 11111111 - это -127. 01111111 - это +127. То есть, можно использовать и положительные, и отрицательные числа - но тогда нельзя вместить в 1 байт число больше 127.
Другой пример. 00000001 = +1 (седьмой бит равен нулю, то есть число положительное). 10000001 = -1 (седьмой бит равен единице, число отрицательное).
--------------------
Упражнение 1.6
Переведите в двоичную систему число -5.
----------------------------------------------
Теперь другая проблема: ограничение на максимльное число. Чтобы хранить большие числа - нужно больше битов. Например, если использовать 16 битов - можно хранить числа от -32767 до +32767 (с -32K по +32K; при 1K = 1024). Пятнадцатый бит используется в качестве знака, остальные (с 14-го по нулевой) - для хранения цифр. Если и этого недостаточно, используется третий, четвёртый байт и т. д.. Поэтому, если мы собираемся использовать большие числа (больше одного байта) - для их представления нужно использовать внутренние операции с дополнительнми байтами. Именно поэтому в первых версиях БЕЙСИК-а (и других языков программирования) есть ограничение на максимальные числа. Поэтому там добавлены внутренние операции для представления больших чисел. В более современных версиях БЕЙСИК-а (и других языков) большие числа поддерживаются - за счёт того, что под каждое число отводится несколько байтов.
Теперь решим другой вопрос. Если мы попытаемся сложить числа с разными знаками, получится вот что:
+7 + (-5) = 00000111 + 10000011
00000111 (+7 в двоичной системе)
+ 10000011 (-5 в двоичной системе)
__________
= 10001010 (-12 в двоичной системе)
Результат получился неправильным, должно было получиться +2. При сложении чисел с разными знаками действуют другие правила. Иначе результат всегда будет получаться больше, чем должен, и всегда будет отрицательным. К счастью, у компьютера есть не только средства представления информации, но и всё, что нужно для вычислений.
Решение этой проблемы - в правиле ДВУХ ПРИБАВЛЕНИЙ. Но чтобы понять его - пока разберёмся с другим приёмом: Правилом ОДНОГО ПРИБАВЛЕНИЯ.
------------------------------
Правило одного прибавления.
При использовании этого правила - записываем все положительные числа, как обычно. А в отрицательных - меняем нули с единицами, и наоборот. Например, +3 будет равно 00000011; а -3 будет равно 11111100. +2 - 00000010, -2 - 11111101. При этом верхний бит не трогаем, положительные числа всё равно начинаем с нуля, отрицательные - с единицы.
------------------
Упражнение 1.7
Число +6 в двоичной системе равно 00000110. Запишите число -6 в двоичной системе, используя Правило одного прибавления.
-------------------------------
Теперь попробуем применить это правило и сложить числа с разными знаками.
11111011 (-4)
+ 00000110 (+6)
______________
= 00000001 ( 1)
Хотя должно было получиться 2 (00000010). Попробуем ещё раз:
11111100 (-3)
+ 11111101 (-2)
____________
=(1)00000001 ( 1).
Получилось +1 (плюс перенос). Хотя должно было получиться -5 (то есть, 11111010). Нужно другое представление - с двумя прибавлениями.
-----------------------------------------
Правило двух прибавлений.
При применении этого правила положительные числа остаются без изменений. К отрицательным числам применяется Правило одного прибавления, а потом ещё ДОБАВЛЯЕТСЯ ЕДИНИЦА. Пример: число -3 (00000011). Применяем Правило одного прибавления, получаем 11111100. Прибавляем единицу, получается 11111101.
Сложение двух чисел.
00000011 (3)
+ 00000101 (5)
______________
= 00001000 (8)
Результат получился правильный (мы ничего не меняли, оба числа положительные). Теперь сложение чисел с разными знаками (3 + (-5):
Применяем к числу -5 Правило двух сложений. Получаем 11111010 + 1 = 11111011. И складываем:
00000011
+ 11111011
__________
= 11111110
Теперь применяем к результату Правило двух сложений ещё раз и получаем: 10000001 + 1 = 10000010 (то есть, -2). Всё правильно.
--------------------
Упражнение 1.8
Примените Правило двух сложений к числу +127
Упражнение 1.9
Примените Правило двух сложений к числу -128
------------------------------------------------
Теперь сложим +4 и -3 (с применением Правила двух прибавлений):
11111101 ( 4)
+ 11111101 (-3)
_______________
=(1)00000001
Не обращаем внимания на перенос, получается 1. Теперь расскажем, как это всё работает, не вдаваясь в математические подробности. При применении Правила можно складывать и вычитать числа независимо от знака. Знак (верхний байт) - учитывается, перенос - отбрасывается. всё просто, единственный минус - нужно выполнять много действий.
В завершение скажем, что Правило двух прибавлений - самое применяемое в микропроцессорах. В некоторых сложных процессорах применяется Правило одного прибавления, а потом идёт специальная проверка и исправление результата. В Таблице 1-3 Вы представлены числа с применением Правила двух прибавлений.
--------------
Упражнение 1.10
Какое самое маленькое и какое самое большое число можно представить одним байтом с применением Правила двух прибавлений?
Упражнение 1.11
Примените Правило двух прибавлений к числу -20. Примените его к результату ещё раз. Получилось ли -20?
--------------------------------------
Следующие примеры покажут нам особенности Правила двух прибавлений. Особый бит "C" обозначает перенос (он будет восьмым битом результата). Бит "V" - обозначает переполнение (когда число получилось слишком большим и не поместилось в байт). Оно происходит, когда идёт переонос из 6-го в 7-ой бит.
Пример использования переноса:
10000000 (128)
+ 10000001 (129)
___________________
= (1)00000001 ( 1, перенос 1)
Единица в скобках обозначает перенос.
Чтобы вместить наш результат - нужен ещё один, девятый бит ("восьмой", поскольку счёт идёт с нуля). Это и есть бит Переноса.
Прибавим этот бит к нашему результату, и получим 100000001 (257). Однако информация хранится в 8-битном виде. Для хранения переноса используется специальный регистр, но при помещении информации в память - поместятся только 8 бит.
Поэтому нужны дополнительные действия. Перенос нужно или поместить в другой байт, или игнорировать, или (если его не должно быть) выдавать сообщение об ошибке.
--------------------------------------
Переполнение ("V").
Пример переполнения:
Складываем два ЗНАКОВЫХ числа.
01000000 (64)
+ 01000001 (65)
__________
= 10000001 (-127)
Происходит перенос из 6-го в 7-ой бит. Это и есть переполнение.
Результат получается неправильным и отрицательным. Эта ситуация должна быть обнаружена и исправлена.
Теперь другой пример:
11111111 (-1, с применённым Правилом)
+ 11111111 (-1)
__________
= (1)11111110 (-2)
Здесь происходит перенос из 7-го в 8-ой бит (бит "C"), и перенос из 6-го в 7-ой бит. В этом случае к результату не применяется Правило двух прибавлений, и результат будет правильным.
Так получилось потому, что перенос из 6-го в 7-ой бит не изменило 7-ой бит (бит знака). То есть переполнения не было.
Другой пример:
11000000 (-64)
+ 10111111 (-65)
= (1)01111111 (127)
В этом случае не было внутреннего переноса из 6-го в 7-ой бит, но был внешний перенос. Результат получился неправильным, поскольку 7-ой бит изменился. Эта ситуация должна быть обнаружена.
Переполнение получается в четырёх случаях:
1 - сложение больших положительных чисел.
2 - сложение больших отрицательных чисел.
3 - вычитание большого положительного числа из большого отрицательного.
4 - вычитание большого отрицательного числа из большого положительного.
Теперь о самом понятии переполнения.
С точки зрения техники, перенос - это специальный зарезервированный бит (по-другому он называется ФЛАГ). Он устанавливается, когда происходит перенос из 6-го в 7-ой бит, и при этом нет внешнего переноса. Или наоборот, когда нет переноса из 6-го в 7-ой бит, и есть внешний перенос. То есть он означает, что 7-ой бит (отвечающий за знак) был ошибочно изменён. Для подготовленных читателей: Флаг переполнения зависит от результата применения операции EOR (Исключающее ИЛИ) к переносу в/из 7-го бита. Почти в любом процессоре есть флаг переноса, и его можно использовать для обработки таких ситуаций.
Переполнение означает, что для вмещения результата (после сложения или вычитания) требуется больше битов, чем доступно.
----------------------------------
Перенос и переполнение.
Биты Переноса и Переполнения по-другому называются Флагами. Они зарезервированы практически во всех процессорах, и следующий раздел посвящён их применению в программировании. Они находятся в специальном регистре, и кроме них там есть другие флаги. Их назначение мы разберём в Главе 4.
----------------------------------
Примеры.
Применим Флаги переноса и переполнения на примерах. В этих примерах символ "V" обозначает Переполнение, символ "C" обозначает Перенос.
Если переполнения нет, V = 0. Если произошло переполнение, V = 1. То же самое и для флага переноса C. Помните, что в Правиле двух прибавлений Перенос игнорируется (не будем вдаваться в математические подробности, почему так происходит).
Сложение двух положительных чисел:
00000110 ( 6)
+ 00001000 ( 8)
______________
= 00001110 (14) V = 0; C = 0; Результат - правильный.
Сложение двух положительных чисел с переполнением:
01111111 ( 127)
+ 00000001 ( 1)
__________
= 10000000 (-128) V = 1; C = 0; Результат - неправильный, потому что произошло переполнение.
Сложение положительного и отрицательного числа:
00000100 (+4)
+ 11111110 (-2)
_____________
= (1)00000010 (+2) V = 0; C = 1; Результат - правильный, перенос игнорируется.
Сложение положительного и отрицательного числа (с отрицательным результатом):
00000010 (+2)
+ 11111100 (-4)
__________
= 11111101 (-2) V = 0; C = 0; Результат - правильный.
Сложение двух отрицательных чисел:
11111110 (-2)
+ 11111100 (-4)
_____________
= (1)11111010 (-6) V = 0; C = 1; Результат - правильный, перенос - игнорируется.
Сложение двух отрицательных чисел с переполнением:
10000001 (-127)
+ 11000010 ( -62)
_____________
= (1)01000011 (67) V = 0; C = 1; Результат - неправильный.
В последнем примере было переполнение в отрицательную сторону, сложением двух отрицательных чисел. Получилось число -189, которое не помещается в 8 бит.
--------------------------------
Упражнение 1.12
Выполните действия. Напишите результат, состояния флагов "C" и "V", и то, правильным ли получился результат:
10111111
+ 11000001
_____________
=
11111010
+ 11111001
_____________
=
00010000
+ 01000000
_____________
=
01111110
+ 00101010
_____________
=
Упражнение 1.13
Можно ли получить переполнение, сложив положительное и отрицательное число? Поясните ответ.
---------------------------------------------
Установленный формат представлений.
Теперь мы знаем, как хранятся знаковые числа. Но остался вопрос: как же хранить большие числа? Для них нужно несколько байтов. Нужно установить для хранения чисел такое число байтов, чтобы можно было поместить самый большой возможный результат.
------------------------
Упражнение 1.14
Какое самое большое и какое самое маленькое число можно хранить в ДВУХ байтах, с применением Правила двух прибавлений?
------------------------
Проблема хранения больших чисел.
При сложении чисел мы ограничены восемью битами, посколько 8-битный процессор одновременно может использовать только один байт. Для многих программ этого явно недостаточно. Но можно использовать множественное представление, и хранить большие числа. Рассмотрим двухбайтовый формат:
00000000 00000000 ( 0)
00000000 00000001 ( 1)
01111111 11111111 (32767)
11111111 11111111 ( -1)
11111111 11111110 ( -2)
------------------------------------
Упражнение 1.15
Какое самое большое отрицательное число можно хранить в ТРЁХ байтах (с применением Правила двух прибавлений)?
------------------------------------
Но у этого способа есть и недостатки. Во-первых, при сложении таких чисел приходится складывать все 8 битов (это будет объяснено ниже, в Главе 4). В результате, вычисления идут медленнее. Во-вторых, по 16 бит придётся выделить уже для обоих чисел (даже если одному из них хватило бы и 8-ми).
Обратите внимание: если для представления числа выделено N битов, а результат в них не поместится - "лишняя" информация будет потеряна. Программа сохранит самые значимые (левые) биты, а нижние (правые) - отбросит. Это называется Усеканием результата.
Пример Усекания для десятичной системы счисления - при условии, что можно хранить 6 знаков:
123456
* 1.2
__________
= 24691.2
+ 123456
__________
= 148147.2
Получился результат из 7-ми цифр. последняя цифра будет отброшена (у нас по условию их может быть 6), и получится результат 148147. Это и есть Усекание.
То же самое и в двоичной системе. Подробнее про двоичное умножение будет рассказано в Главе 4.
Усекание ведёт к потере точности - но вполне приемлемо для обычных математических вычислений. В некоторых случаях (например, в учёте) оно непримелемо. Например. Если клиент положит на счёт большую сумму - она может округлиться до десятков долларов, а остальное будет потеряно. Чтобы избежать этого - нужно использовать другое представление чисел, которое даёт точный результат. Такое представление существует, и называется BCD (Binary-Coded Decimal - десятичные числа в двоичной кодировке).
----------------------------------
Представление BCD.
Принцип системы BCD заключается в том, чтобы хранить каждое десятичное число отдельно, и использовать столько байтов, сколько понадобится. Десятичные числа хранятся в виде цифр (0-9), и представлены четырьмя битами. Четыре бита могут давать 16 разных комбинаций - этого достаточно, чтобы хранить все возможные цифры. И остаётся ещё 6 комбинаций, которые не применяются в системе BCD. В будущем это может дать проблемы с вычислениями, и эти проблемы нам ещё предстоит решить.
Таблица цифр в системе BCD:
______________________ _______________________
Цифра: | Битовый код: | | Цифра: | Битовый код: |
-------+--------------| |--------+--------------|
0 | 0000 | | 8 | 1000 |
-------+--------------| |--------+--------------|
1 | 0001 | | 9 | 1001 |
-------+--------------| |--------+--------------|
2 | 0010 | | НЕТ | 1010 |
-------+--------------| |--------+--------------|
3 | 0011 | | НЕТ | 1011 |
-------+--------------| |--------+--------------|
4 | 0100 | | НЕТ | 1100 |
-------+--------------| |--------+--------------|
5 | 0101 | | НЕТ | 1101 |
-------+--------------| |--------+--------------|
6 | 0110 | | НЕТ | 1110 |
-------+--------------| |--------+--------------|
7 | 0111 | | НЕТ | 1111 |
_______|______________| |________|______________|
-------+--------------| |--------+--------------|
0 | 0000 | | 8 | 1000 |
-------+--------------| |--------+--------------|
1 | 0001 | | 9 | 1001 |
-------+--------------| |--------+--------------|
2 | 0010 | | НЕТ | 1010 |
-------+--------------| |--------+--------------|
3 | 0011 | | НЕТ | 1011 |
-------+--------------| |--------+--------------|
4 | 0100 | | НЕТ | 1100 |
-------+--------------| |--------+--------------|
5 | 0101 | | НЕТ | 1101 |
-------+--------------| |--------+--------------|
6 | 0110 | | НЕТ | 1110 |
-------+--------------| |--------+--------------|
7 | 0111 | | НЕТ | 1111 |
_______|______________| |________|______________|
Поскольку для хранения одной цифры используется 4 бита, в каждом байте можно хранить две цифры.
Например, 00000000 в системе BCD обозначает число 00. 10011001 - число 99.
Более подробный пример:
Битовое значение: 00100001
Разделяем его на части по 4 бита: 0010 0001
0010 - это 2; 0001 - это 1. Получается число 21.
----------------------------------
Упражнение 1.16
Запишите в формате BCD числа 29 и 91
Упражнение 1.17
Является ли правильной для BCD запись: 10100000? Поясните ответ.
----------------------------------
Обычно для хранения чисел в формате BCD используется несколько байтов. Плюс, один (или больше) полубайтов используется для хранения самого числа байтов (то есть, сколько цифр мы можем использовать). Другой полубайт хранит в себе положение десятичной точки. Однако способы хранения этих значений могут и отличаться.
Пример представления многобайтового числа в системе BCD:
_______________________
| | 3 | + | 2 | 2 | 1 |
|___|___|___|___|___|___|
| | |__|___|
| | |
v v v
Число Знак Само число
нужных
байтов
| | 3 | + | 2 | 2 | 1 |
|___|___|___|___|___|___|
| | |__|___|
| | |
v v v
Число Знак Само число
нужных
байтов
Тут хранится число 221 (со знаком плюс).
Знак может храниться в формате 0000 (плюс) и 0001 (минус). Так бывает не всегда - это как пример.
---------------------------------
Упражнение 1.18
Используя такую же систему, запишите в формате BCD число -23123. В виде таблицы и в двоичном виде.
Упражнение 1.19
Представьте в формате BCD числа 222 и 111. Умножьте их (в уме), и представьте результат в формате BCD.
---------------------------------
Формат BCD вполне подходит и для нецелых чисел. Например, число 2.21 будет размещено так:
_____________________________
| 3 | 2 | + | 2 | 2 | 1 |
|____|____|____|____|____|____|
| | | | | |
| | v |____|___|
| | Знак |
v v v
Число Положение Само число
цифр десятичной
(байтов) точки
| 3 | 2 | + | 2 | 2 | 1 |
|____|____|____|____|____|____|
| | | | | |
| | v |____|___|
| | Знак |
v v v
Число Положение Само число
цифр десятичной
(байтов) точки
Преимущество системы BCD заключается в том, что она даёт точный результат. Недостаток - в том, что такое хранение требует много памяти, а арифметические операции выполняются медленно. То есть, её нужно использовать для точных рассчётов, и нежелательно для обычных вычислений.
--------------------------------
Упражнение 1.20
Сколько байтов нужно, чтобы хранить число 9999 в формате BCD?
Сколько байтов нужно, чтобы хранить это же число в двоичном формате (с применением Правила двух прибавлений)?
--------------------------------
Мы разобрались в том, как хранить целые числа, знаковые числа и большие числа. И даже разобрали способ хранения десятичных чисел с применением системы BCD. Теперь давайте разберёмся, как хранить десятичные числа с фиксированным форматом.
--------------------------------
Представление плавающей точки.
Главный принцип заключается в том, что десятичные числа должны представляться в определённом формате - так, чтобы байты памяти не расходовались зря.
Например, в числе 0.000123 будет потеряны байты, которые используются для хранения трёх нулей после точки. Можно оптимизировать эту запись, записав число в виде: .123 * 10 ^ -3. Число .123 в этом примере называется МАНТИССОЙ; число -3 - ЭКСПОНЕНТОЙ. Мы оптимизировали запись, убрав из неё три незначимых нуля и добавив экспоненту.
Другой пример: число 22.1 можно записать в виде: .221 * 10 ^ 2.
То есть, формула выглядит так:
M * 10 ^ E (М - Мантисса; Е - Экспонента)
Понятно, что так можно записать любое число, в котором мантисса больше нуля и меньше единицы. То есть, при условии:
.1 001; 1-> 001).
Восьмеричный формат используется в старых компьютерах, которые использовали разное число битов - от 8 до 64. С появлением 8-битных процессоров стандартом стал другой формат - Шестнадцатеричный.
В Шестнадцатеричном формате биты группируются по четыре, и выводятся в виде шестнадцатеричных цифр. Шестнадцатеричные цифры - это цифры от 0 до 9 и буквы "A", "B", "C", "D", "E" и "F". Например, 0000 - это 0; 0001 - это 1; 1111 - это F.
Полный список см. в Таблице 1-8.
Десятичный | Двоичный | 16-чный | 8-чный | ФОРМАТ
| | | |
0 | 0000 | 0 | 0 |
1 | 0001 | 1 | 1 |
2 | 0010 | 2 | 2 |
3 | 0011 | 3 | 3 |
4 | 0100 | 4 | 4 |
5 | 0101 | 5 | 5 |
6 | 0110 | 6 | 6 |
7 | 0111 | 7 | 7 |
8 | 1000 | 8 | 10 |
9 | 1001 | 9 | 11 |
10 | 1010 | A | 12 |
11 | 1011 | B | 13 |
12 | 1100 | C | 14 |
13 | 1101 | D | 15 |
14 | 1110 | E | 16 |
15 | 1111 | F | 17 |
| | | |
0 | 0000 | 0 | 0 |
1 | 0001 | 1 | 1 |
2 | 0010 | 2 | 2 |
3 | 0011 | 3 | 3 |
4 | 0100 | 4 | 4 |
5 | 0101 | 5 | 5 |
6 | 0110 | 6 | 6 |
7 | 0111 | 7 | 7 |
8 | 1000 | 8 | 10 |
9 | 1001 | 9 | 11 |
10 | 1010 | A | 12 |
11 | 1011 | B | 13 |
12 | 1100 | C | 14 |
13 | 1101 | D | 15 |
14 | 1110 | E | 16 |
15 | 1111 | F | 17 |
Пример: битовое значение 10100001 можно записать как A1 (1010 -> A; 0001 -> 1).
--------------------------------------
Упражнение 1.25
Запишите в шестнадцатеричном виде битовое значение 10101010.
Упражнение 1.26.
Распишите по битам шестнадцатеричное число "FA".
Упражнение 1.27
Запишите в восьмеричном виде битовое значение 01000001.
-------------------------------------
Шестнадцатеричное представление даёт большое преимущество - с его помощью можно записать значение байта всего двумя цифрами. Они воспринимаются и запоминаются гораздо легче, чем битовые значения. Поэтому в современных микропроцессорах значения битов выдаются именно в таком формате.
Но на самом деле, нужная информация обычно подаётся в виде текста. Потому что даже шестнадцатеричный формат малопонятен для людей.
-------------------------------------
Символьный формат.
Символьный формат - это представление информации в понятном текстовом виде. Например, числа отображаются в виде обычных десятичных цифр (а не в виде непонятных символов, нулей и единиц). Текст отображается в виде букв. То есть, этот формат - самый подходящий для пользователей. Он используется по возможности, когда доступен принтер или ЭЛТ-дисплей (ЭЛТ-дисплей - это экран с электронно-лучевой трубкой, приспособленный для вывода текста и изображений - такой же, как в телевизорах). К сожалению, некоторые системы не поддерживают такие устройства вывода - и пользователю приходится ограничиться шестнадцатеричной информацией.
---------------------------------------------
Главное о форматах внешнего представления информации.
Символьное представление информации - самое удобное для пользователей. Но для него требуется специальное дорогое оборудование: алфавитно-цифровая клавиатура, плюс принтер или ЭЛТ-дисплей. Поэтому для некоторых систем такой формат может быть недоступным. Тогда вместо него используется шестнадцатеричный формат. В редких случаях (например, при настройке оборудования или отладке программ) бывает удобно использовать двоичный формат. Также в двоичном формате отображается содержимое Регистров памяти.
Нужность устройств для выдачи двоичной информации на передней панели часто является предметом для споров. Мы не будем обсуждать это тут.
Мы разобрались, как представлена информация внутри компьютера, и как она подаётся пользователю. В следующей главе мы покажем, как компьютер обрабатывает эту информацию.
-------------------------------------
Упражение 1.28
Какое преимущество даёт представление знаковых чисел с применением Правила двух прибавлений?
Упражнение 1.29
Как будет представлено число 1024:
- в формате полного двоичного числа?
- в формате знакового двоичного числа?
- с применением Правила двух прибавлений?
Упражнение 1.30
Что такое бит "V"? Должен ли программист проверять его состояние после сложений и вычитаний?
Упражнение 1.31
Выполните двоичное сложение чисел:
16 и 17
18 и -16
-17 и -18
Упражнение 1.32
Выведите следующий текст в Шестнадцатеричном формате без проверки чётности.
Текст: "MESSAGE"
-----------------------------------------------------------------------------------------------------
Глава 2. Техническое устройство процессора 6502.
-----------------------------------------------------------------------------------------------------
Введение.
Чтобы писать простые программы, устройство процессора знать необязательно. Но для составления сложных программ необходимо хотя бы приблизительно понимать принципы его работы. Поэтому в этой главе мы изучим самые основные моменты. Компьютер включает в себя не только микропроцессор (в нашем случае, 6502) - но и другие устройства. В этой главе мы рассмотрим микропроцессор 6502 - другим устройствам посвящена Глава 7.
Здесь мы разберём базовую архитектуру микрокомпьютера и внутреннее устройство процессора 6502. Особое внимание обратим на его регистры. Также будет показано, как создаются и выполняются программы (с технической точки зрения). Полного описания микропроцессоров тут нет. Если читателю это интересно, рекомендуем ему книгу "Микропроцессоры" (автор тот же).
--------------------------------------------
Архитектура системы.
Архитектура микрокомпьютера (в упрощённом виде) показана в Таблице 2-1:
______
8-битная шина данных
| |
| | /\ /\ /\
| | || || ||
| | __||__ __\/__ __\/__ __________
| MPU | | ROM | | RAM | | PRO- | Шина ввода/вывода |Устройства|
| | |(PRO- | |(DATA)| |GRAM- || ввода/ |
XTAL | | |GRAM) | | | |MABLE | | вывода |
|| | | | | | | | I/O | Шина ввода/вывода | |
|| | | |______| |______| |______||__________|
|| | | /\ /\ /\
CLOCK| | || || ||
|______|===============================>
/\ 16-битная шина адреса
||
===================================>
Управляющая шина
| |
| | /\ /\ /\
| | || || ||
| | __||__ __\/__ __\/__ __________
| MPU | | ROM | | RAM | | PRO- | Шина ввода/вывода |Устройства|
| | |(PRO- | |(DATA)| |GRAM- || ввода/ |
XTAL | | |GRAM) | | | |MABLE | | вывода |
|| | | | | | | | I/O | Шина ввода/вывода | |
|| | | |______| |______| |______||__________|
|| | | /\ /\ /\
CLOCK| | || || ||
|______|===============================>
/\ 16-битная шина адреса
||
===================================>
Управляющая шина
Архитектура стандартного микропроцессора.
----------------------------------------------------------------------
Микропроцессор (в нашем случае, 6502) находится в левой части изображения. Он выполняет функцию центрального вычислительного устройства. В его состав входят: Арифметико-Логическое Устройство(АЛУ), регисты и Управляющее Устройство. Каждое из этих устройств будет описано в этой главе.
Микропроцессор включает три шины: 8-битную двунаправленную Шину Данных (вверху изображения); 16-битную однонаправленную Адресную Шину и Управляющую Шину (внизу изображения).
Шина данных - передаёт информацию, чтобы элементы системы могли обмениваться ею. Обычно она передаёт информацию от памяти к микропроцессору; от микропроцессора к памяти или от микропроцессора к чипу ввода/вывода (чип ввода/вывода - это устройство, которое нужно для связи системы с внешними устройствами).
Адресная шина - передаёт адреса (созданные микропроцессором при помощи внутренних регистров). Эти адреса назначают источник и приёмник информации для шины данных.
Управляющая шина - передаёт сигналы синхронизации, которые необходимы системе.
Теперь про остальные элементы.
Каждому процессору необходимо измерять время - это реализовано с помощью внутренних часов и кристалла. В устаревших микропроцессорах часы были внешним устройством, и для соединения с ними нужен был отдельный чип. В более современных системах часы встроены в микропроцессор. Кварцевый кристал, наоборот, делают внешним устройством из-за его объёма. Оба этих устройства изображены в левой части рисунка.
ROM (Read-Only-Memory, Память только для чтения) содержит программы для системы. Преимущество этой памяти в том, что информация в ней постоянна, она не исчезает даже при выключении системы. Обычно там содержатся загрузочные и отладочные программы (о них - ниже), необходимые для системных операций. Также (например, если компьютер используется в производстве) там могут содержаться команды для станков с ЧПУ. То есть, та информация, которая не должна меняться.
При обычном использовании и при разработке программ большинство программ размещаются в RAM (Random-Access Memory). Там они могут быть легко изменены или переданы в ROM. Всё содержимое RAM теряется при выключении системы.
RAM - это память, предназначенная для ввода/вывода информации. В компьютерах, предназначенных для чёткой цели (например, для производства) её обычно мало, она предназначается только для вводимых данных. В пользовательских компьютерах её, наоборот, много - в ней содержатся и большинство программ, и редактируемые данные. Для редактирования внешними устройствами, информация должна быть загружена именно в RAM.
И наконец, система содержит один или несколько чипов, которые связывают систему с внешними устройствами. Чаще всего используется чип PIO(Parallel Input/Output, чип Параллельного Ввода/Вывода), см. изображение. PIO (как и любые чипы) соединён со всеми тремя шинами, и обеспечивает минимум два 16-битных порта для связи с внешними устройствами. Подробнее об устройстве PIO будет в Главе 7 (Устройства Ввода/Вывода).
Все чипы соединяются с тремя шинами, в том числе и с Управляющей. Для упрощения схемы, на рисунке соединение с Управляющей шиной не показано.
Все эти устройства не обязательно должны находиться в одном чипе. Как правило, используется НАБОР чипов, каждый из которых включает в себя PIO и определённое количество ROM- и RAM-памяти. Подробнее об этом - в Главе 7.
В настоящих системах используется больше компонентов. Например, для шин почти всегда требуется БУФЕРИЗАЦИЯ. Для устройств RAM требуется ЛОГИКА РАСШИФРОВКИ. И наконец, сигналы должны усиливаться при помощи ДРАЙВЕРОВ. Мы не будем здесь рассматривать эти вспомогательные устройтва - их устройство не важно для программирования. Тем, кто интересуется - рекомендуем книгу "Микропроцессорные интерфейсные технологии".
---------------------------------------------
Внутреннее устройство 6502.
Упрощённая схема внутреннего устройства процессора 6502 изображена на Рисунке 2-2.
|======|======|============+|
|| || || || |\
----------------------------------------------------- \ ШИНА ДАННЫХ
------------------------------------------- -------- / DBD-7
/\ /\ /\ /\ || /\ || /\ || /\ || || |/
|| || || || || || || || || || || ||
\/ \/ \/ \/ || \/ || \/ || \/ \/ \/
__ __ ___ ___|| ___|| ___|| __ __ __
|Y | |X | |1|5| |PSH | |PCL | | A | |P | \ \_/ /
|__| |__| |_|_| |____| |____| |____| |__| \ALU/
ИНДЕКСНЫЕ | | || || /\ \_/
РЕГИСТРЫ | | || || || || ___
| | || || |==============| |MUX|
| | || || |===>| | НИЖНИЙ АДРЕС
| | || |====================||==>| | A0-7
| | || || | |
| +----||--------------------------||-->|___|
| || || ___
| || || |MUX|
| || \===>| |
| |===============================>| | ВЕРХНИЙ АДРЕС
| | | AB-15
+-------------------------------------->|___|
|| || || || |\
----------------------------------------------------- \ ШИНА ДАННЫХ
------------------------------------------- -------- / DBD-7
/\ /\ /\ /\ || /\ || /\ || /\ || || |/
|| || || || || || || || || || || ||
\/ \/ \/ \/ || \/ || \/ || \/ \/ \/
__ __ ___ ___|| ___|| ___|| __ __ __
|Y | |X | |1|5| |PSH | |PCL | | A | |P | \ \_/ /
|__| |__| |_|_| |____| |____| |____| |__| \ALU/
ИНДЕКСНЫЕ | | || || /\ \_/
РЕГИСТРЫ | | || || || || ___
| | || || |==============| |MUX|
| | || || |===>| | НИЖНИЙ АДРЕС
| | || |====================||==>| | A0-7
| | || || | |
| +----||--------------------------||-->|___|
| || || ___
| || || |MUX|
| || \===>| |
| |===============================>| | ВЕРХНИЙ АДРЕС
| | | AB-15
+-------------------------------------->|___|
X, Y - ИНДЕКСНЫЕ РЕГИСТРЫ.
S - УКАЗАТЕЛЬ СТЕКА.
PC - ПРОГРАММНЫЙ УКАЗАТЕЛЬ.
A - АККУМУЛЯТОР.
P - СТАТУС ПРОЦЕССОРА.
MUX - МНОЖИТЕЛЬ.
ALU - АРИФМЕТИКО-ЛОГИЧЕСКОЕ УСТРОЙСТВО.
-----------------------------------------------------------------------------
Арифметико-Логическое Устройство (АЛУ) изображено в правой части рисунка в форме буквы "V". Его задача - выполнять арифметические и логические операции с данными, которые поступают в него по двух входным портам. Эти порты называются соответственно левым и правым входом. После выполнения операции (например, сложения или вычитания) АЛУ выводит информацию (на рисунке, вниз).
АЛУ оснащён специальным регистром (аккумулятор, "A"). АЛУ автоматически использует его в качестве вывода и одного из вводов. Это - классическая схема, основанная на аккумуляторе. При арифметических и логических операциях одним из операндов обычно является значение Аккумулятора, другим - содержимое памяти. Результат операции помещается в аккумулятор. Именно поэтому он так называется - он "аккумулирует" результаты. Преимущество этой схемы - в том, что инструкции получаются короткими и помещаются в 1 байт (8 бит). Если операнд извлекается из другого регистра (НЕ из Аккумулятора) - для такой инструкции необходимы дополнительные биты. Недостаток такой схемы - в том, что перед использованием любой информации, эта информация должна быть загружена в аккумулятор. Это - множество действий, которые могут вызвать замедление всей системы.
Слева от АЛУ изображён 8-битный регистр, Статус Процессора (P). Этот регистр содержит в себе 8 битов Статуса. Каждый бит является своеобразным датчиком - он включается/выключается при определённом состоянии. Мы полностью разберём их назначение в следующих двух главах (про набор инструкций 6502). Например, к этим битам относятся флаги "N", "Z" и "C", о которых мы говорили в прошлой главе.
"N" - "бит отрицательности". Это 7-ой бит (самый левый) регистра "P". Этот бит устанавливается равным единице, когда результат вычислений в АЛУ получился отрицательным.
"Z" - "бит нуля". Этот бит устанавливается равным единице, когда результат вычислений в АЛУ получился нулевым.
"C" - "бит переноса". Это нулевой бит регистра (самый правый). Если результат вычислений не поместился в 8 бит - остаток (единица) попадает в этот бит. Этот бит очень часто используется в арифметических операциях.
Биты статуса устанавливаются автоматически. Полный список инструкций и способов, которые могут изменить значение битов статуса - см. в Дополнении А (Глава 4). Эти биты используются программистом для проверки состояний и выявления ошибок. Например - бит "Z" можно использовать, чтобы сравнить результат с нулём и выбрать нужное продолжение программы в зависимости от этого. Все решения, принимаемые программой - зависят от состояния битов Статуса. Поэтому назначение и применение этих битов нужно просто запомнить. Все устройства ввода/вывода также содержат биты статуса. Подробнее про них - в Главе 7.
Горизонтальными прямоугольниками обозначены внутренние регистры процессора 6502. "PC" - это Программный Указатель. Это 16-битный регистр, состоящий из двух 8-битных: "PCL" и "PCH". "PCL" содрежит в себе нижнюю часть Программного Указателя (то есть, биты 7 - 0). "PCH" содержит верхнюю часть (биты 15 - 8). Программный указатель содержит в себе адрес следующей инструкции, которая должна быть выполнена. Чтобы продемострировать назначение Указателя - давайте разберём механизм доступа к памяти.
MPU ROM
________ ________
| | | | |
| | | | |
| ____ | | |_____|
| | PC | | |PC| |
| |_ _| | | |_____|
| || | ШИНА АДРЕСА | | |
| |==================>| |_____|
| | | |
|________| |________|
________ ________
| | | | |
| | | | |
| ____ | | |_____|
| | PC | | |PC| |
| |_ _| | | |_____|
| || | ШИНА АДРЕСА | | |
| |==================>| |_____|
| | | |
|________| |________|
-------------------------------------------------------------------
Цикл исполнения инструкций.
Давайте разберём рисунок. Микропроцессор изображён слева, память - справа. Чип памяти может быть любым - ROM или RAM. Память используется для хранения инструкций и данных. Здесь мы извлекаем из памяти одну инструкцию. После этого Программный Указатель содержит в себе адрес следующей инструкции, которую надо выполнить. Получается цикл из трёх действий:
1 - Инструкция извлекается из памяти.
2 - Инструкция расшифровывается.
3 - Инструкция выполняется.
------------------------------------
Извлечение инструкции.
Вначале содержимое Программного Указателя помещается в адресную шину и отправляется в память. Одновременно сигнал чтения может быть получен и использован, если это нужно. Память получает нужный адрес. Этот адрес обозначает определённую ячейку памяти. Получив сигнал чтения, память расшифровывает полученный адрес (при помощи встроенного расшифровщика) и выбирает нужную ячейку памяти. За несколько сотен наносекунд память размещает данные (инструкцию) из этой ячейки в шину данных. Микропроцессор считывает данные из шины данных, и помещает их во внутренний регистр "IR" (Instuction Register, регистр инструкций), специально предназначенный для хранения только что извлечённых инструкций.
---------------------------------------------
Расшифровка и выполнение инструкций.
Когда инструкция помещена в регистр "IR" - контрольный чип процессора расшифровывает его содержимое и составляет последовательность внутренних и внешних сигналов для выполнения инструкции. Потом идёт выполнение, его продолжительность зависит от сложности инструкции. Одни инструкции выполняются внутри микропроцессора. Другие - извлекают данные из памяти или помещают их в память. Продолжительность измеряется в количествах циклов (продолжительность одного цикла = 1 микросекунда) или в наносекундах.
В процессоре 6502 в качестве часов используется встроенный осциллятор.
----------------------------------------------------------------------
Извлечение следующей инструкции.
С помощью программного указателя нужные инструкции выбираются и извлекаются из памяти. При выполнении программы инструкции из памяти извлекаются ПО ПОРЯДКУ. Для этого программный указатель увеличивается на единицу, сразу после помещения своего значения в шину адреса. Пример. Программный указатель содержит в себе значение 0. Это значение передаётся шине адреса. Программный указатель увеличивается (он становится равным 1). После выполнения загрузки и выполнения инструкции (из ячейки 0) шина адреса получает значение программного указателя (1), программный указатель стновится равным 2. Инструкция из ячейки 1 извлекается и выполняется. И т. д.
!!!!!!!!!!!!!!!!!!!!РИСУНОК
------------------------------------------------------------
Другие регистры процессора 6502.
Мы не разобрали ещё три регистра: "X", "Y" и "S". Регистры "X" и "Y" - это индексные регистры. Они имеют размер 8 бит, и предназначены для хранения информации, которую будет использовать программа.
Назначению индексных индексов посвящена Глава 5 (технологии адресации). Содержимое этих регистров может быть прибавлено к любому адресу (то есть, они используются в качестве смещения). Это бывает полезно, когда данные хранятся в таблице. Регистры "X" и "Y" не совсем одинаковы, разница между ними будет разобрана в Главе 5.
Регистр стека "S" содержит указатель на верх стека памяти
-------------------------------------------
Стек.
Стек по-другому называют структурой LIFO (last-in, first-out - последним зашёл, первым вышел). Стек - это набор специально выделенных регистров или ячеек памяти. Он нужен для ХРОНОЛОГИЧЕСКОГО хранения информации. Пример. Первый элемент информации попадает в стек (в самую нижнуюю часть). Потом помещается второй элемент (он будет выше). Потом - третий (ещё выше). Потом даётся команда извлечь элемент - сначала извлекается третий (который наверху), потом - второй, и только потом - первый (самый нижний). Всего со стеком есть две основных операции: Push (заталкивание, помещает информацию в стек) и Pop (выталкивание, извлекает информацию из стека). Большинство этих операций выполняются с аккумулятором (информация помещается в стек из аккумулятора и загружается в него же). Но иногда бывает нужно загрузить содержимое стека в другие регистры (например, регистры статуса).
Особенно часто стек применяется для трёх целей: создания подпрограмм, использования прерываний и временного хранения информации. Полезность стека для создания подпрограмм мы разберём в Главе 3 (основные техники программирования); использование прерываний - в Главе 6 (технологии ввода/вывода).
Физически стек может быть реализован двумя способами:
Первый способ. Под стек выделяется несколько регистров микропроцессора. Преимущество такого стека - большая скорость работы. Недостаток - небольшой размер (всего несколько регистров).
Второй способ (самый применимый). Под стек выделяются ячейки памяти, а в регистре ("S" в нашем случае) хранится только расположение верхнего элемента стека (точнее, номер ячейки верхнего элемента + 1). Чтобы указать на любую ячейку памяти - регистру "S" (указателю стека) нужно 16 бит.
Однако в процессоре 6502 под Указатель стека отведено всего 8 бит. Ещё один бит (9-ый, самый левый) всегда равен единице. То есть стек ограничен ячейками с номерами 100000000-111111111 (256-511, первая страница памяти), Указатель стека может выбрать только их. Зачем нужно такое ограничение, мы разберём позже. При выполнении команды "PUSH" указатель стека уменьшается на единицу; начальное его значение обычно задаёт программист.
--------------------------------------------------------------
Страницы памяти.
Микропроцессор 6502 оснащён 16-битной шиной адреса. 16 двоичных битов могут дать 65 536 разных комбинаций (они и обозначают нужную ячейку памяти). Условно память делится на нумерованные страницы (одна страницы = 256 байтов идущих подряд). То есть, ячейки памяти (0 - 255) - Нулевая страница; (256 - 511) - Первая страница, и т. д. Первая страница всегда выделена под Стек. Все остальные могут использоваться как угодно, по усмотрению программиста. Про деление памяти на страницы важно помнить: при переходе на другую страницу выполняется лишний цикл, поэтому таких переходов должно быть как можно меньше.
------------------------------------------------
Чип процессора 6502.
Разберём до конца Рисунок 2-2. В верхней его части изображена внешняя шина данных. Она используется для связи с внешними устройствами, такими, как память. A0-7 и A8-15 обозначают соответственно части адреса, сгенерированные адресной шиной.
------------------------------------------------
Схема расположения выводов процессора 6502
Схема расположения выводов изображена на рисунке 2-7. Шина данных обозначена как DB0-7 (в правой части рисунка). Адресная шина обозначена как A0-11 и A12-15. Она занимает контакты 9-20 с левой части чипа; и контакты 22-25 с правой части.
Все остальные контакты нужны для сигналов включения/выключения и управляющих сигналов.
Управляющие сигналы:
R/W - задаёт направление передачи информации по шине данных.
IRQ и NMI - "Запрос на прерывание" и "Немаскируемое прерывание". Мы разберём их в Главе 7.
SYNC - сигнал, обозначющий код извлечённой операции.
RDY - используется для синхронизации при медленной памяти; останавливает процессор.
SO - устанавливает флаг переполнения; обычно не используется.
Ф0, Ф1, Ф2 - сигналы часов.
RES - инициализация.
Vss и Vcc - подача энергии (5V).
-----------------------------------------------------------------------------
Заключение.
Со внутренним устройством процессора 6502 мы разобрались. Устройство шин пока не очень важно, но понять регистры и их назначение - необходимо для дальнейшего чтения. Все они изображены на Рисунке 2-2 - прочитайте ещё раз про каждый из них.
-------------------------------------------------------------------------------------
Глава 3. Основные технологии программирования.
-------------------------------------------------------------------------------------
Введение
----------------------------------
В этой главе мы разберём основные технологии программирования. Такие понятия, как управление регистрами, циклы и подпрограммы. Особое внимание уделим использованию ВНУТРЕННИХ ресурсов процессора - регистров. Все программы будут построены на основе простой арифметики - в них будет показано использование самых основных инструкций. После этого мы разберёмся, как обмениваться информацией между процессором и памятью. Следующая глава посвящена другим инструкциям; Глава 6 - обмену информацией с устройствами ввода/вывода.
В этой главе всё обучение построено на примерах. Примеры будут идти от простого к сложному - в них будут использоваться всё новые инструкции и регистры. Мы пока НЕ будем разберать одну важную технологию, технологию Адресации. Ей посвящена Глава 5.
А теперь начнём писать программы для процессора 6502.
---------------------------------------------------------------
Арифметические программы.
Арифметические программы выполняют сложение, вычитание, умножение и деление. Все эти действия выполняются с целыми числами (они могут быть представлены как обычные двоичные числа, или же с применением Правила двух прибавлений, т. е. самый левый бит будет битом знака). Подробнее о Правиле двух прибавлений - см. Главу 1.
---------------------------------------------------------------
8-битное сложение.
Мы сложим два числа (операнда). Условно назовём их OP1 и OP2. Соответственно они будут расположены в ячейках памяти ADR1 и ADR2. Результат условно назовём RES - он будет помещён в ячейку памяти ADR3. Всё это изображено на Рисунке 3-1.
ПАМЯТЬ
|____________|
| OP1 | ПЕРВОЕ СЛАГАЕМОЕ
|____________|
| |
| |
| |
|____________|
| OP2 | ВТОРОЕ СЛАГАЕМОЕ
|____________|
| |
| |
| |
|____________|
| RES | РЕЗУЛЬТАТ
|____________|
| |
|____________|
| OP1 | ПЕРВОЕ СЛАГАЕМОЕ
|____________|
| |
| |
| |
|____________|
| OP2 | ВТОРОЕ СЛАГАЕМОЕ
|____________|
| |
| |
| |
|____________|
| RES | РЕЗУЛЬТАТ
|____________|
| |
Текст этой программы на языке Ассемблера:
LDA ADR1;
Загружаем OP1 в регистр "A".ADC ADR2;
Прибавляем OP2 к значению регистра "A" (он был равен OP1, поэтому получим нужный результат).STA ADR3;
Сохраняем результат из регистра "A" в память.Эта программа состоит из трёх инструкций - каждая из них размещена в отдельной строке в виде символов. Каждая инструкция переводится Ассемблером в 1, 2 или 3 двоичных байта. Мы пока не будем вдаваться в подробности, как происходит этот перевод. Первая строка - LDA ADR1 (LoaD the Accumulator, Загрузить в Аккумулятор). Она означает, что надо загрузить в аккумулятор информацию из нужной ячейки (у нас это ячейка ADR1).
ADR1 - это символическое представление 16-битного адреса - оно должно быть утверждено в другом месте программы. Например, оно может обозначать адрес 100.
То есть инструкция LDA загрузит в аккумулятор значение ячейки 100 (оно будет передано в аккумулятор по шине данных). Теперь с этим значением можно производить логические и арифметические операции.
Правая часть (точка с запятой и всё, что после неё) - это раздел комментариев. Процессор ничего не делает с комментариями - они нужны, чтобы программа была читаемой для разработчиков. Это - внутреннее документирование.
Результат первой инструкции показан на рисунке 3-2.
!!!!!!!!!!!!!!! РИСУНОК
Вторая инструкция программы:
ADC ADR2
Она означает, что мы прибавляем к аккумулятору значение из ячейки памяти ADR2. Оно будет извлечено из памяти и прибавлено к аккумулятору (до этого он был равен значению из ADR1). Сумма останется в аккумуляторе. Вообще читателю нужно помнить, что у процессора 6502 результаты арифметических операций попадают в аккумулятор. В других процессорах результаты могут попадать в другие регистры или сразу в память.
Теперь в аккумуляторе хранится сумма. Мы готовы передать её в ячейку памяти ADR3. Правая часть инструкции (после точки с запятой) опять является комментарием. Действие инструкции показано на рисунке 3-3.
!!!!!!!!!!!!!!!!!!!!!!!!! РИСУНОК
Мы видим, что изменилось значение аккумулятора, а ячейки памяти ADR1 и ADR2 (из которых мы считали информацию) не изменились. Это важное правило: чтение из регистра или из памяти не изменяет данные. Запись - наоборот, изменяет данные. При записи старое содержимое теряется.
Теперь сохраняем результат в ячейку ADR3. Её действие показано на Рисуноке 3-4.
!!!!!!!!!!!!!!!!!!РИСУНОК
-------------------------------------------------------
Особенности процессора 6502.
Те три инструкции для большинства микропроцессоров являются законченной программой. Но у процессора 6502 есть две особенности - и поэтому для полной программы ему нужны ещё две инструкции.
Во-первых, инструкция ADC обозначет прибавление с переносом (ADd with Carry), а не просто прибавление. Разница в том, что обычное прибавление просто складывает два числа. Прибавление с переносом - ещё и добавляет значение в бит Переноса. В нашем примере перенос не нужен (это единственное сложение, до него нет никаких действий). Поэтому мы должны очистить этот бит (сделать его равным нулю). Для этого есть инструкция CLC (CLear Carry).
К сожалению, у процессора 6502 есть только одна инструкция сложения - ADC. Это не такой уж большой недостаток, просто не нужно забывать очищать бит переноса перед тем, как её использовать.
Вторая особенность процессора 6502 в том, что он оснащён системой десятичных инструкций, которая может использовать систему BCD. То есть, процессор может выполнять операции в двух режимах: двоичном и десятичном. Режим устанавливается в особом бите Статуса (регистр "P"). Поскольку мы используем именно двоичный режим - нам нужно проверить, правильно ли выставлен этот бит. Для этого есть инструкция CLD, которая его очищает (делает равным нулю). На самом деле, по умолчанию стоит как раз двоичный режим, и регистр "P" автоматически ставится равным нулю при старте программы. Поэтому в большинстве программ эта инструкция может быть пропущена. В наших же упражнениях будут использоваться оба режима, поэтому мы будем использовать эту инструкцию.
В результате, более полно наша программа будет выглядеть так:
CLC ;
Очищаем бит Переноса.CLD ;
Очищаем бит Десятичности.LDA ADR1 ;
Загружаем значение ячейки ADR1 в аккумулятор.ADC ADR2 ;
Прибавляем к аккумулятору значение ячейки ADR2.STA ADR3 ;
Загужаем значение аккумулятора в ячейку ADR3.У нас используются адреса ADR1, ADR2 и ADR3. Это - символьные адреса, и нужно объявить, что они обозначают. Для этого используются специальные "псевдо-инструкции", которые сопоставляют названия с номерами ячеек. Например:
ADR1 = $100
ADR2 = $120
ADR3 = $200
ADR2 = $120
ADR3 = $200
-------------------------------------------
Упражнение 3.1
Теперь закройте книгу - можете пользоваться только описаниями инструкций в конце книги. Напишите программу, которая сложит значения ячеек LOC1 и LOC2, и поместит результат в ячейку LOC3. Сравните эту программу с той, которую мы разобрали.
-------------------------------------------
16-битное сложение.
8-битное сложение даёт складывать только 8-битные числа (то есть, 0 - 255 при использовании абсолютных чисел). Для реальных вычисленний необходимо МУЛЬТИСЛОЖЕНИЕ - чтобы можно было складывать числа размером 16 и больше бит. Можно использовать 24, 32 бита и т. д (число битов должно быть кратным восьми). В нашем следующем примере первое слагаемое хранится по адресам ADR1 и ADR1-1. Поскольку это 16-битное число, оно занимает две ячейки памяти. Точно так же второе слагаемое будет храниться по адресам ADR2 и ADR2-1; результат - по адресам ADR3 и ADR3-1.
Принцип этой программы - такой же, как и у предыдущей. Сначала мы загружаем нижнюю (правую) половину первого слагаемого и прибвавляем к ней нижнюю половину второго слагаемого. Помещаем результат в нижний байт результата (микропроцессор 6502 может одновременно использовать только 8 битов). Если при сложении нижних частей образуется перенос - он будет помещён в бит Переноса (регистр "C"). Потом складываются верхние половины слагаемых, и к ним прибавляется перенос (если он есть). Результат помещается в верхний байт результата. Программа будет выглядеть так:
CLC
CLD
LDA ADR1 ; Нижняя часть первого слагаемого загружается в аккумулятор.
ADC ADR2 ; К значению аккумулятора прибавляется нижняя часть второго слагаемого.
STA ADR3 ; Результат помещается в ячейку памяти ADR3 (нижняя часть результата).
LDA ADR1-1; Верхняя часть первого слагаемого загружается в аккумулятор (его старое содержимое уничтожается).
ADC ADR2-1; К значению аккумулятора прибавляется верхняя часть второго слагаемого.
STA ADR3-1; Результат помещается в ячейку памяти ADR3-1 (верхняя часть результата).
CLD
LDA ADR1 ; Нижняя часть первого слагаемого загружается в аккумулятор.
ADC ADR2 ; К значению аккумулятора прибавляется нижняя часть второго слагаемого.
STA ADR3 ; Результат помещается в ячейку памяти ADR3 (нижняя часть результата).
LDA ADR1-1; Верхняя часть первого слагаемого загружается в аккумулятор (его старое содержимое уничтожается).
ADC ADR2-1; К значению аккумулятора прибавляется верхняя часть второго слагаемого.
STA ADR3-1; Результат помещается в ячейку памяти ADR3-1 (верхняя часть результата).
Первые две инструкции (CLC и CLD) мы используем для надёжности: обнуляем Десятичность и флаг Переноса. Мы уже объяснили, что это такое. Следующие три операции выполняют обычное 8-битное сложение. В результате получается правая часть (по другому называется нижняя, поскольку складываться нижние биты с нулевого по седьмой) результата. Она загружается в ячейку памяти ADR3.
Если результат не поместился в один байт (получился больше, чем 255) - "лишний" бит заполняется в флаге Переноса, флаг Переноса становится равным единице. Если поместился, флаг Переноса так и остаётся нулевым.
Следующие три инструкции опять выполняют 8-битное сложение, складывают уже левые (верхние, с 8-го по 15-ый) биты слагаемых. К 8-му биту прибавляется ещё значение переноса, и потом результат загружается в ячейку памяти ADR3-1. В итоге, результат оказался сохранён в ячейках памяти ADR3 и ADR3-1.
В нашем примере подразумевается, что результат поместится в 16 бит. Если есть вероятность, что может получиться большее (17-битное) число - после последнего сложения надо проверить флаг переноса (не стал ли он опять равным единице), и предпринять нужные действия.
!!!!!!!!!!!!!!! РИСУНОК
Обратите внимание, что сначала идут нижние биты, а уже после них (в следующей ячейке) - верхние. То есть, хранение идёт в обратном порядке. Необязательно хранить данные именно так - но это стандарт, и очень желательно его придерживаться. Пример:
Номер ячейки | Значение ячейки
|
0 |
1 |
.....
N |
ADR1 | OPR1L
ADR1 + 1 | OPR1H
ADR1 + 2 |
.....
ADR2 | OPR2L
ADR2 + 1 | OPR2H
.....
ADR3 | RESL
ADR3 + 1 | RESH
.....
|
0 |
1 |
.....
N |
ADR1 | OPR1L
ADR1 + 1 | OPR1H
ADR1 + 2 |
.....
ADR2 | OPR2L
ADR2 + 1 | OPR2H
.....
ADR3 | RESL
ADR3 + 1 | RESH
.....
-----------------------------------
Упражнение 3.2
Не глядя на старую программу 16-битного сложения, перепишите её, используя значения из рисунка (см. выше)
Упражнение 3.3
Подумайте, почему нижние биты хранятся выше верхних. Напишите программу, которая бы делала наоборот.
--------------------------------------------------
Программисту (то есть, Вам) надо решить самому, как хранить 16-битные числа. Это - самый первый из множества вопросов, которые предстоит решить при постройке системы хранения данных.
--------------------------------------------------
Вычитание 16-битных чисел.
8-битное вычитание делается просто. Начнём сразу с 16-битного. Два слагаемых (OPR1 и OPR2) хранятся соответственно в ячейках ADR1, ADR1-1 (первое слагаемое), ADR2 и ADR2-1 (второе слагаемое). Разница только в том, что для вычитания используется инструкция SBC, а флаг переноса, наоборот, устанавливается равным единице (инструкция SEC, "SEt Carry, установить флаг Переноса). Получаем программу:
CLD
SEC
LDA ADR1 ; Загружаем нижний бит первого слагаемого в аккумулятор.
SBC ADR2 ; Вычинаем из него нижний байт второго слагаемого.
STA ADR3 ; Помещаем нижний байт результата в ячейку памяти.
LDA ADR1-1; Загружаем верхний байт первого слагаемого в аккумулятор.
SBC ADR2-1; Вычитаем из него верхний байт второго слагаемого.
LDA ADR3-1; Помещаем верхний байт результата в ячейку памяти.
SEC
LDA ADR1 ; Загружаем нижний бит первого слагаемого в аккумулятор.
SBC ADR2 ; Вычинаем из него нижний байт второго слагаемого.
STA ADR3 ; Помещаем нижний байт результата в ячейку памяти.
LDA ADR1-1; Загружаем верхний байт первого слагаемого в аккумулятор.
SBC ADR2-1; Вычитаем из него верхний байт второго слагаемого.
LDA ADR3-1; Помещаем верхний байт результата в ячейку памяти.
------------------------------------------
Упражнение 3.4
Напишите программу для вычитания 8-битных чисел.
------------------------------------------
Помните, что при применении Правила двух прибавлений флаг переноса ничего не обозначает. Если происходит переполнение - устанавливается флаг Переполнения (регистр "V"). Его можно проверить и использовать для дальнейших вычислений.
Это были примеры двоичного сложения и вычитание. Есть ещё другой вид арифметических операций - арифметика системы BCD.
------------------------------------------
Арифметика системы BCD
-----------------------------------
8-битное сложение в системе BCD
Мы разобрали арифметику системы BCD в Главе 1. Эта система используется там, где нужно получать точный результат без округлений. В системе BCD каждая десятичная цифра хранится в полубайте - соответственно, в байте могут хранится две цифры. Теперь разберёмся, как выполнять операции с многозначными числами:
Пример: сложим числа 01 и 02
01 (в битовом виде) будет выглядеть как: 0000 0001
02 (в битовом виде): 0000 0010
0000 0001
+ 0000 0010
____________
= 0000 0011
Получилось число 03 (0000 - 0; 0011 - 3). Теперь другой пример: сложим числа 08 и 03.
0000 1000
+ 0000 0011
____________
---------------------------------
Упражнение 3.5
Выполните предыдущий пример. Какое число получилось в системе BCD?
---------------------------------
Должно было получиться двоичное число 0000 1011 - то есть 11, если перевести в обычную систему. Но в системе BCD число 11 обозначается как 0001 0001! Почему же получилась ошибка?
Потому что в системе BCD используются только первые 10 комбинаций - для обозначения цифр от 0 до 9. Остальные 6 комбинаций не используются, и одна из таких комбинаций - как раз 1011. Чтобы получился правильный ответ - нужно прибавлять 6 к цифре, если она получилась больше 9-ти. В нашем примере будет так:
1011 (число больше 9-ти)
+ 0110 (+6)
__________
= 00010001
Теперь получился правильный результат (11 в системе BCD).
Этот пример показывает одну из основных сложностей системы BCD. В большинстве процессоров есть особая инструкция, которая автоматически проверяет правильность и прибавляет 6, если нужно. В процессоре 6502 это функция ADC. То есть, у нас этой проблемы не будет.
Вторая проблема будет показана на следующем примере. В нашем примере при сложении нижнего полубайта образуется перенос. Этот перенос должен быть учтён и перенесён в следующую цифру (полубайт). Процессор 6502 делает это автоматически. К сожалению, в процессоре 6502 не предусмотрен специальный флаг для такого "внутреннего" переноса.
При десятичном сложении обязательно используются инструкции SED и CLC (соответственно, установка Десятчного режима и Очистка переноса). Вот пример сложения чисел 11 и 22 в системе BCD.
CLC ; Очищаем перенос.
SED ; Устанавливаем десятичный режим.
LDA #$11; Загружаем в аккумулятор десятичное число 11 (НЕ значение ячейки 11!).
ADC #$22; Прибавляем число 22.
STA ADR; Помещаем результат в ячейку памяти ADR.
SED ; Устанавливаем десятичный режим.
LDA #$11; Загружаем в аккумулятор десятичное число 11 (НЕ значение ячейки 11!).
ADC #$22; Прибавляем число 22.
STA ADR; Помещаем результат в ячейку памяти ADR.
В этой программе мы использовали два новых символа: "#" и "$". Символ "#" означает, что используется буквальное значение (а не адрес). Знак "$" означает, что используется шестнадцатеричное число (в системе BCD используются только шестнадцатеричные числа 0-9, остальные - преобразуются путём прибавления шестёрки). Результат помещается в ячейку памяти ADR. Когда слагаемое получается напрямую (не из ячейки памяти) - это называется НЕПОСРЕДСТВЕННОЙ АДРЕСАЦИЕЙ (режимам адресации посвящена Глава 5). Помещение числа в нужную ячейку (в нашем примере это ячейка ADR) - называется АБСОЛЮТНОЙ АДРЕСАЦИЕЙ.
------------------------------------------------------
Упражнение 3.6
Можно ли перенести инструкцию CLC в другое место - поставить её ПОСЛЕ инструкции LDA?
------------------------------------------------------
Вычитание в системе BCD.
Вычитание в системе BCD уже сложнее. Для его выполнения нужно применить ко второму слагаемому Правило двух прибавлений, но с прибавлением десятки (можно назвать Правилом прибавления десятки). Большинству микропроцессоров нужно для этого 3-4 инструкции. Но процессор 6502 имеет специальную инструкцию вычитания, которая заменяет собой все эти действия! Десятичное вычитание начинается с инструкций SED (установка десятичного режима) и SEC (установка переноса равным 1). Теперь попробуем вычесть десятичные числа 25 и 26:
SED ; Устанавливаем десятичный режим.
SEC ; Устанавливаем флаг переноса.
LDA #$26 ; Загружаем число 26 в аккумулятор.
SBC #$25 ; Вычинаем из значения аккумулятор число 25.
STA ADR ; Помещаем результат в ячейку памяти.
SEC ; Устанавливаем флаг переноса.
LDA #$26 ; Загружаем число 26 в аккумулятор.
SBC #$25 ; Вычинаем из значения аккумулятор число 25.
STA ADR ; Помещаем результат в ячейку памяти.
-------------------------------------------------------
16-битное сложение в системе BCD
16-битное сложение выполняется так же, как и в двоичном режиме. Пример:
CLC
SED
LDA ADR1
ADC ADR2
STA ADR3
LDA ADR1-1
ADC ADR2-1
STA ADR3-1
SED
LDA ADR1
ADC ADR2
STA ADR3
LDA ADR1-1
ADC ADR2-1
STA ADR3-1
-----------------------------------------
Упражнение 3.7
Сравните программы 16-битных сложений (простого и десятичного). Чем они отличаются?
Упражнение 3.8
Напишите 16-битную программу вычитания в системе BCD (не используйте инструкции CLC и ADC!).
------------------------------------------------------
Использование флагов в системе BCD.
В системе BCD флаг переноса означает, что результат получился больше 99. Его снятие при вычитании означает, что результат получился меньше 00.
------------------------------------------------------
Подсказки для сложения и вычитания.
- Всегда очищайте флаг Переноса перед сложением.
- Всегда устанавливайте флаг Переноса перед вычитанием.
- Устанавливайте нужный режим (двоичный или десятичный).
------------------------------------------------------------
Виды инструкций.
Мы использовали три вида инструкций. Инструкции LDA и STA - соответственно загружают информацию из памяти и помещают её в память. Это - ИНСТРУКЦИИ ПЕРЕДАЧИ ИНФОРМАЦИИ.
Ещё мы использовали инструкции ADC (сложение) и SBC (вычитание). Это - АРИФМЕТИЧЕСКИЕ ИНСТРУКЦИИ. На самом деле их больше, и остальные мы тоже разберём в этой главе.
И наконец, мы использовали инструкции CLC, SEC, CLD и SED. Это - инструкции УПРАВЛЕНИЯ СТАТУСАМИ, они выполняют действия с битами флагов. Полное описание инструкций процессора 6502 будет в Главе 4.
Существуют и другие виды инструкций, которые мы пока не использовали. Например, инструкции ветвления и прыжка (они меняют порядок выполнения программы). Их мы покажем в следующем примере.
-----------------------------------------------------------
Умножение.
Теперь разберём более сложное действие: Умножение двоичных чисел. Чтобы было проще понять - сначала умножим числа, как обычно:
12 (Первое слагаемое, MPD)
* 23 (Второе слагаемое, MPR)
______
= 36 (Части результата, PP)
+ 24
______
= 276 (Конечный результат, RES)
* 23 (Второе слагаемое, MPR)
______
= 36 (Части результата, PP)
+ 24
______
= 276 (Конечный результат, RES)
Сначала мы умножаем самую левую цифру первого слагаемого на второе слагаемое, то есть 3 на 12). Получается первая часть результата - 36. Потом умножаем следующую цифру на второе слагаемое - 2 на 12). Получается 24. Число 24 будет сдвинуто на одну позицию влево.
Точно так же умножаются двоичные числа. Умножение 5 * 3 будет выглядеть так:
101 (MPD)
* 011 (MPR)
________
= 101 (PP)
+ 101
+ 000
________
= 01111 (RES)
* 011 (MPR)
________
= 101 (PP)
+ 101
+ 000
________
= 01111 (RES)
Этот алгоритм изображён на блок-схеме (см. Рисунок 3-8)
!!!!!!!!!!!!!!!!!!!! РИСУНОК
Каждый прямоугольник на этом рисунке обозначает действие. Каждый ромб - проверку условия, от которой будут зависеть дальнейшие действия (по-другому называется ТОЧКОЙ ВЕТВЛЕНИЯ). Если условие выполнено - выполняется одно действие, если нет - то другое. Обратите внимание на стрелку, идущую от нижнего ромба к верхнему. Она означает, что возможен переход к уже выполненному действию (то есть, цикл может выполняться несколько раз).
-------------------------------------------------
Упражнение 3.9
Умножьте числа 4 и 7 в двоичной системе. Убедитесь, что результат получился равным 28. Это упражнение НЕОБХОДИМО - без него следующие примеры будут непонятны.
-------------------------------------------------
Теперь создадим программу по этому алгоритму. Будем считать, что слагаемые хранятся в ячейках памяти MPR и MPD.
LDA #0 ; Обнуляем аккумулятор.
STA TMP ; Обнуляем ячейку памяти TMP (помещаем в неё значение аккумулятора, которое равно нулю).
STA RESAD ; Обнуляем ячейку памяти RESAD.
STA RESAD+1; Обнуляем ячейку памяти RESAD+1.
LDX #8 ; Загружаем в регистр "X" значение 8 (он будет счётчиком).
MULT LSR MPRAD ; Левый оборот.
BCC NOADD ; Проверяем бит Переноса.
LDA RESAD ; Загружаем в аккумулятор значение RES.
CLC ; Подготовка к сложению.
ADC MPDAD ; Прибавляем MPD к значению аккумулятора (RES).
STA RESAD ; Сохраняем результат.
LDA RESAD+1; Загружаем в аккумулятор значение RES+1.
ADC TMP ; Прибавляем содержимое TMP.
STA RESAD+1; Сохраняем в память.
NOADD ASL MPDAD ; Сдвигаем MPD влево.
ROL TMP ; Сохраняем бит из MPD.
DEX ; Уменьшаем значение регистра "X" на единицу.
BNE MULT ; Если его значение ещё не равно нулю, повторяем цикл.
STA TMP ; Обнуляем ячейку памяти TMP (помещаем в неё значение аккумулятора, которое равно нулю).
STA RESAD ; Обнуляем ячейку памяти RESAD.
STA RESAD+1; Обнуляем ячейку памяти RESAD+1.
LDX #8 ; Загружаем в регистр "X" значение 8 (он будет счётчиком).
MULT LSR MPRAD ; Левый оборот.
BCC NOADD ; Проверяем бит Переноса.
LDA RESAD ; Загружаем в аккумулятор значение RES.
CLC ; Подготовка к сложению.
ADC MPDAD ; Прибавляем MPD к значению аккумулятора (RES).
STA RESAD ; Сохраняем результат.
LDA RESAD+1; Загружаем в аккумулятор значение RES+1.
ADC TMP ; Прибавляем содержимое TMP.
STA RESAD+1; Сохраняем в память.
NOADD ASL MPDAD ; Сдвигаем MPD влево.
ROL TMP ; Сохраняем бит из MPD.
DEX ; Уменьшаем значение регистра "X" на единицу.
BNE MULT ; Если его значение ещё не равно нулю, повторяем цикл.
Сначала идёт ИНИЦИАЛИЗАЦИЯ - мы подготавливаем нужные нам регистры и ячейки памяти. Регистр "X" (это один из индексных регистров) мы будем использовать в качестве счётчика. Поскольку мы перемножаем 8-битные числа, мы должны умножать каждый бит первого слагаемого на все 8 битов второго слагаемого. К сожалению, в большинстве микропроцессоров (в том числе и 6502) нет инструкции, которая дала бы нам сразу выполнять операции с битами - мы должны использовать для этого регистры. Поэтому мы перенесём второе слагаемое в Аккумулятор. После этого мы сдвигаем значение аккумулятора вправо - эта инструкция сдвигает каждый бит на одну позицию. Тот бит, который "выпал" (был самым правым, и ему некуда сдвигаться) - помещается в бит Переноса (для этой техники есть ещё много других применений, про них будет в Главе 4). Потом выполняется нужная операция с этим битом, и сдвиг выполняется снова (пока не переберём все биты).
Следующая проблема. Перемнножение двух 8-битных чисел может дать 16-битный результат (2 ^ 8 * 2 ^ 8 = 2 ^ 16). Это значит, что для результата нужно 16 битов. Внутренние регистры процессора 6502 могут хранить только 8 битов, поэтому промежуточные результаты придётся сохранять в памяти (по этой причине умножение выполняется медленнее других операций - действия с памятью выполняются медленнее, чем между регистрами).
Область памяти, используемая для умножения - показана на Рисунке 3-10. Наверху - ячейка памяти (MPRAD), предназначенная для хранения второго множителя. В нашем примере, она содержит число "3" в двоичном формате. Под этой ячейкой - "временная" ячейка (адрес TMP), её назначение мы разберём позже. Первый множитель на рисунке - ещё ниже (ячейка MPDAD). И наконец, нижние две ячейки памяти (RESAD) предназначены для хранения результата.
--------------------------------------------
Пример Левого Сдвига и Левого Оборота:
_______________________________
| | | | | | | | |
| 1 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | + Левый сдвиг =
|___|___|___|___|___|___|___|___|
_______________________________ ___
| | | | | | | | | | |
| 0 | 0 | 1 | 1 | 0 | 1 | 0 | 0 | | 1 |
|___|___|___|___|___|___|___|___| |___|
Флаг Переноса
_______________________________
| | | | | | | | |
| 1 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | + Левый сдвиг =
|___|___|___|___|___|___|___|___|
_______________________________ ___
| | | | | | | | | | |
| 0 | 0 | 1 | 1 | 0 | 1 | 0 | 0 | | 1 |
|___|___|___|___|___|___|___|___| |___|
Флаг Переноса
Все биты сдвинулись влево; значение непоместившегося (самого левого, был равен единице) попало в Флаг переноса;
освободившийся бит (самый правый) обнулился.
_______________________________
| | | | | | | | |
| 1 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | + Левый оборот =
|___|___|___|___|___|___|___|___|
_______________________________
| | | | | | | | |
| 0 | 0 | 1 | 1 | 0 | 1 | 0 | 1 |
|___|___|___|___|___|___|___|___|
| | | | | | | | |
| 1 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | + Левый оборот =
|___|___|___|___|___|___|___|___|
_______________________________
| | | | | | | | |
| 0 | 0 | 1 | 1 | 0 | 1 | 0 | 1 |
|___|___|___|___|___|___|___|___|
Все биты сдвинулись влево. Значение непоместившегося (самого левого, был равен единице) попало в освобождённый (самый правый).
-----------------------------------------
Все перечисленные ячейки памяти и будут нашими "рабочими регистрами".
Стрелка в верхней правой части Рисунка идёт из ячейки MPR в C - это бит из множителя (после сдвига) попадает в Флаг Переноса. На самом деле, Флаг Переноса находится в самом процессоре, а не в памяти.
Вернёмся к тексту программы. Первые пять инструкций нужны для инициализации.
Первые четыре инструкции - очищают наши "регистры": TMP, RESAD и RESAD + 1.
Первая инструкция - LDA #0 - загружает в аккумулятор число 0 (он становится равным 00000000). Потом мы помещаем его значение в наши "регистры", чтобы обнулить их. Мы можем загружать из него значение сколько угодно раз (значение не меняется при считывании).
Вторая инструкция - STA TMP - помещает содержимое аккумулятора (00000000) в ячейку памяти TMP. Содержимое аккумулятора не меняется.
Третья инструкция - STA RESAD - точно так же помещает содержимое аккумулятора в ячейку памяти RESAD (предназначенную для хранения младшего байта результата. Содержимое аккумулятора опять не меняется.
Четвёртая инструкция - STA RESAD+1 - помещает содержимое аккумулятора в ячейку памяти RESAD+1 (предназначенную для хранения старшего байта результата).
И ещё нам надо установить число повторений, чтобы цикл сдвигов множителя когда-нибудь закончился. Нам нужно 8 сдвигов. В качестве счётчика мы используем регистр "X", и загружаем в него значение 8. После каждого сдвига его значение будет уменьшаться на единицу, и когда он будет равен нулю (то есть, когда всё перемножим), цикл завершится. Пятая инструкция делает регистр "X" равным 8:
LDX #8
Инициализация завершена. Теперь нужно найти самый младший значащий бит множителя. Мы не можем сделать это одной инструкцией - нужны две. Сначала множитель сдвигается вправо, потом проверяется бит, который попал в Флаг Переноса.
Инструкция LSR MPRAD - выполняет сам сдвиг
----------------------------------------------
Упражнение 3.10
В нашем примере множитель (MPRAD) равен 3. Чему будет равен Флаг Переноса после Сдвига Вправо (инструкции LSR MPRAD)?
----------------------------------------------
Следующая инструкция проверяет, чему равен Флаг Переноса:
BCC NOADD
Это - первая инструкция Ветвления, которая нам встретилась. Все предыдущие программы, которые мы написали - выполнялись последовательно. То есть, каждая инструкция выполнялась после предыдущей. Чтобы пропустить действия и перейти к нужному нам шагу - используются инструкции Ветвления. В нашем случае, если Флаг Переноса равен нулю - мы сразу переходим к шагу NOADD. Если же нет - ветвление НЕ происходит, и программа выполняется дальше, как обычно.
Что же такое NOADD. Это - символьная метка. Она обозначает адрес памяти (с которого продолжится выполнение программы в случае Ветвления). В Ассемблере программист может давать ячейкам памяти символьные названия. При создании программы Ассемблер преобразует это название в номер нужной ячейки. Это даёт возможность писать программы так, чтобы они были понятны человеку. Плюс, так будет проще вставлять инструкции между инструкцией Ветвления и Меткой. Подробнее об этом - в Главе 10.
Вариант 1. Флаг Переноса равен единице.
В этом случае проверка не проходит, и выполняется следующая инструкция.
Вариант 2. Флаг Переноса равен нулю.
В этом случае проверка проходит, и выполняется инструкция, идущая после метки NOADD.
По нашей формуле, если перенос равен единице - первый множитель должен быть прибавлен к результату. И должен быть выполнен сдвиг. Мы должны сдвинуть результат на одну позицию вправо, или же сдвинуть множитель на одну позицию влево. Мы в нашей программе сдвигаем множитель на на одну позицию влево.
Множитель содержится в "регистрах" (ячейках памяти) TMP и MPDAD. 16 бит результата содержатся в ячейках RESAD и RESAD + 1.
Чтобы было понятнее - предположим, что множитель равен 5. Что произойдёт - показано на Рисунке 3-10.
Мы знаем, как сложить 16-битные числа (кто не знает - повторите раздел о 16-битном сложении, см. выше). Сначала мы складываем младшие, потом старшие байты. Идём дальше.
LDA RESAD - в аккумулятор загружается младший байт результата.
CLC - обнулён Флаг Переноса.
Перед сложением Флаг Переноса должен быть обнулён. Это очень важно, иначе там уже может быть результат от предыдущих операций (в нашем случае так и есть - он до этой инструкции равен единице).
ADC MPDAD
Множитель прибавляется к значению аккумулятора, который содержит в себе нижний байт результата.
STA RESAD
Результат сложения сохраняется в нужной ячейке памяти. Половина сложения выполнена. Не забывайте, что при сложении устанавливается Флаг Переноса (0, если переноса не было, и 1, если перенос был). Значение переноса потом будет прибавлено к старшему байту результата.
Завершаем сложение:
LDA RESAD+1
ADC TMP
STA RESAD+1
ADC TMP
STA RESAD+1
Эти три инструкции завершают наше сложение и помещают старший байт результата в нужную ячейку. Теперь можно опять выполнить сдвиг.
NOADD ASL MPDAD
Эта инструкция расшифровывается как "Ariphmetic Shift Left" (Арифметический левый сдвиг). Она выполняет сдвиг значения ячейки MPDAD, которое содержит в себе остаток множителя. Но это ещё не всё. Нужный нам бит попадает в Флаг Переноса, который потом сотрётся. Чтобы не потерять его - надо сохранить его в другое место. Для этого выполняем инструкцию:
ROL TMP
Расшифровывается как "Rotate Left" (Левый оборот). Обратите внимание: у нас есть ДВЕ инструкции для сдвига влево: ASL и ROL. Чем же они отличаются?
Инструкция ASL сдвигает значение, при этом "выпадающий" бит теряется из ячейки, и попадает только в Флаг Переноса. Инструкция ROL - "вращает" значение. При выполнении инструкции ROL значение "выпадающего" бита не уходит ячейки, а становится на освободившееся место (в самый правый бит). И тоже передаётся в Флаг Переноса. Это и есть то, что нам нужно.
Теперь надо повторить все эти вычислительные операции 8 раз. Для этого нужны ещё две инструкции:
DEX
Эта инструкция уменьшает значение регистра "X" на единицу. Если он был равен 8 - то станет равным 7.
BNE MULT
Это ещё одна инструкция Ветвления. Она означает "Перейти к метке MULT, если результат не равен нулю". Поскольку после первого цикла регистр "X" ещё не равен нулю - программа выполнит переход, и цикл повторится.
----------------------------------------------
Упражнение 3.11
Что будет, когда регистр "X" станет равным нулю? Какая инструкция выполнится следующей?
----------------------------------------------
В дальнейшем мы будем использовать умножение в виде подпрограммы, и эта подпрограмма будет завершаться инструкцией RTS. Об использовании подпрограмм будет рассказано в этом же разделе.
----------------------------------------------
Самопроверка.
Если Вы хотите научиться программировать - очень важно запомнить и понять самые основные приёмы. Мы использовали сразу много новых инструкций. Сама принцип программы - прост, но она намного длиннее предыдущих. Поэтому ОБЯЗАТЕЛЬНО выполните следующее упражнение прежде, чем читать дальше. Если Вы выполните его правильно - то Вы поняли принцип. Если же нет - перечитайте и разберите программу ещё раз.
---------------------------------------------
Упражнение 3.12
При написании и отладке любой программы всегда нужно проверять её резльтаты вручную - чтобы убедиться, что она работает правильно. Заполните таблицу (рисунок 3-12).
Вы можете скопировать таблицу, или же писать прямо в ней. Задача - написать содержимое каждого регистра и ячейки памяти после каждой инструкции - с начала до конца. Там, где требуется - Вы должны заполнить метку и инструкцию, которая должна выполниться следующей. Если Вы заполняете значение регистра, и оно не может быть определено - ставьте прочёркивание. Первая строка уже заполнена (как пример). Дальше сами :-)
!!!!!!!!!!!!!!!РИСУНОК!!!!!!!!!!!!!!
Решение:
Первой выполняется инструкция LDA #0.
После этой инструкции значение регистра "X" неизвестно. Ставим прочерк. Значение аккумулятора равно нулю (00000000). Значения множителей задаются или вводятся заранее (в программе этого нет - допустим, они равны 3 и 5). Значит, ставим их двоичные значения (00000011 и 00000101). Значение Флага Переноса неизвестно (прочерк). Значение ячейки памяти мы не задавали (прочерк). Обе ячейки с результатом тоже не заданы (прочерки). Заполняем следующую строку. Она отличается только тем, что ячейка TMP стала равна нулю (00000000). В следующей инструкции ячейка RESAD становится равной нулю (00000000). После следующей инструкции становится равной нулю ячейка RESAD+1 (00000000).
В пятой инструкции регистр "X" становится равным 8.
Инструкция LSR MPRAD сдвигает содержимое ячейки MPRAD на одну позицию влево. Ячейка MPR становится равной 00000001, а самая левая единица "выпадает" в Флаг Переноса. Флаг Переноса становится равным 1 (это БИТ, а не байт, поэтому именно 1, а НЕ 00000001).
Остальную часть таблицы заполните сами. Если Вам непонятен смысл какой-то инструкции - см. Главу 4, в ней есть описание всех инструкций.
Результат должен получиться равным 15 (0000000000001111 в двоичной форме). То есть, ячейка RESAD (младший байт) должна быть равной 00001111; ячейка RESAD+1 (старший байт - 00000000). Если так всё и получилось - упражнение выполнено, если нет - попробуйте ещё раз. Самая распространённая ошибка - забывают про Флаг Переноса. Не забывайте, что АЛУ заполняет Флаг Переноса после каждого сложения.
-------------------------------------------------
Альтернативные программы.
Это был только один из множества способов написать такую программу. Любой программист может изменить или улучшить её. Например, мы сдвигали множитель влево после сложения. Такой же эффект можно получить, если после сложения сдвигать вправо результат. Преимущество будет в том, что нам не нужна будет ячейка TMP - то есть, мы сэкономим один байт памяти. Но и это ещё не всё. Поскольку мы использовали вместо регистров ячейки памяти - не будет лишнего обращения к памяти, и программа станет работать быстрее.
------------------------------------------------
Упражнение 3.13
Напишите программу 8-битного умножения со сдвигом результата вправо. Сравните её с предыдущей программой, и решите, будет ли она работать быстрее предыдущей.
------------------------------------------------
Восможно, Вы захотите определить скорость программы, и обратитесь к Дополнению (в нём перечислена скорость выполнения всех инструкций - в циклах). Но на самом деле скорость выполнения инструкций зависит от того, где именно они находятся в памяти. Существует специальный режим адресации - Адресация Нулевой Страницы. При её используется нулевая страница памяти (ячейки 0 - 255), специально предназначенные для быстрого исполнения. Ускорение получается за счёт того, что для хранения номера этих ячеек достаточно одного байта, а инструкции обращения к ним занимают два. Обращение же к остальным ячейкам обычно занимает 3 байта (1 байт - сама инструкция; 2 байта - номер ячейки). Подробнее об этом - см. Главу 5.
------------------------------------------------
Улучшенная программа Умножения.
Программа, которую мы написали - это обычный перевод алгоритма в программный текст. Теперь посмотрим, как можно улучшить РЕАЛИЗАЦИЮ этого алгоритма.
Очень много времени и инструкций занимает сдвиг результата и множителя. Но есть стандартный приём, чтобы упростить эту процедуру. Каждый раз при сдвиге множителя вправо - самый левый бит освобождается. При этом результат может занимать максимум 9 битов. После следующего сдвига у множетеля ещё один бит освободится, а результат увеличится ещё на один бит. То есть, мы можем использовать для хранения части результата освобождённые биты множителя.
Теперь о том, как можно рациональнее использовать регистры. Регистру "X" мы уже нашли наилучшее применение - он является счётчиком. Акумулятор - это единственный регистр, к которому могут применяться операции сдвига. Чтобы повысить эффективность - надо решить, что в него лучше помещать - множитель или результат.
Что же лучше поместить в аккумулятор? Результат должен прибавляться к множителю после каждого сдвига. Поскольку операции сложения мы тоже можем выполнять только с аккумулятором - лучше поместить в него результат.
Все остальные числа будут храниться в памяти (Рисунок 3-17).
A и B будут хранить результат (A - старший байт; B - младший). A - это аккумулятор; B - ячейка памяти, расположенная в нулевой странице. Ячейка памяти C содержит множитель; D - второй множитель. Пишем саму программу:
MULT LDA #0 ; Обнуляем аккумулятор (старший байт результата).
STA B ; Обнуляем ячейку памяти B (младший байт резльтата).
LDX #8 ; Устанавливаем счётчик равным 8.
LOOP LSR C ; Сдвиг в множителе.
BCC NOADD
CLC ; Очищаем Флаг Переноса.
ADC D ; К аккумулятору прибавляется множитель.
NOADD ROR A ; Сдвигаем результат.
ROR B ; Отправляем бит в ячейку B.
DEX ; Уменьшаем счётчик на единицу.
BNE LOOP ; Проверяем, обнулился ли счётчик (если нет - повторяем цикл).
STA B ; Обнуляем ячейку памяти B (младший байт резльтата).
LDX #8 ; Устанавливаем счётчик равным 8.
LOOP LSR C ; Сдвиг в множителе.
BCC NOADD
CLC ; Очищаем Флаг Переноса.
ADC D ; К аккумулятору прибавляется множитель.
NOADD ROR A ; Сдвигаем результат.
ROR B ; Отправляем бит в ячейку B.
DEX ; Уменьшаем счётчик на единицу.
BNE LOOP ; Проверяем, обнулился ли счётчик (если нет - повторяем цикл).
Разберём эту программу. Поскольку аккумулятор и ячейка B будут хранить результат - мы должны сначала обнулить их:
MULT LDA #0
STA B
STA B
Регистр "X" опять будет счётчиком. Делаем его равным 8:
LDX #8
Теперь можно начать сам цикл умножения. Сначала мы делаем сдвиг в множителе, потом проверяем Флаг Переноса (в него попадёт самый правый бит множителя).
LOOP LSR C
BCC NOADD
BCC NOADD
Делаем сдвиг множителя вправо. Дальше есть два варианта. Если Флаг Переноса равен нулю - переходим к метке NOADD. Если же нет - продолжаем последовательно выполнять программу:
CLC
ADC D
ADC D
Поскольку Флаг Переноса равен единице - мы должны очистить его, и прибавить множитель к аккумулятору.
Теперь выполняем сдвиг результата:
NOADD ROR A
ROR B
ROR B
Значение аккумулятора сдвигается вправо; самый правый бит попадает в Флаг Переноса. Потом така же операция делается с ячейкой B.
Цикл завершён, уменьшаем счётчик и проверяем, равен ли он нулю:
DEX
BNE LOOP
BNE LOOP
Если мы сравним программы - то увидим, что новая получилась раза в два короче. К тому же, она выполнится быстрее. Это - пример того, насколько важно выбирать подходящие регистры для хранения информации и рационально их использовать.
------------------------------------------------------
Упражнение 3.14
Вычислите среднее время, за которое будет выполниться эта программа. Предположите, что ветвление (там, где его может не быть) будет происходить в 50% случаев. Время выполнения каждой инструкции можно найти в Дополнении в конце книги. Время выполнения одного цикла равно 1 микросекунде.
------------------------------------------------------
Двоичное деление.
При делении применяется почти такой же принцип, как и при умножении. Делитель вычитается из верхних бит делимого. После каждого вычитания результат используется вместо делимого. Результат при этом увелчивается на 1. При вычитании результат может получиться отрицательным тогда делитель прибавляется обратно, при этом результат уменьшается на единицу. Делимое и резульат сдвигаются на одну позицию влево, и цикл повторяется.
Это - "восстановимый способ". Существует ещё "невосстановимый способ", который работает быстрее.
--------------------------------------------
16-битное деление.
Мы будем писать программу для невосстановимого способа деления. Делимое будет 16-битным числом; делитель - 8-битным; результат тоже будет 8-битным. Регистры и используемые ячейки памяти показаны на Рисунке 3-22. Делимое находится в аккумуляторе (старший байт) и в ячейке памяти 0 (B) (младший байт). Результат будет сохранён в ячейку Q (ячейка 1). Делитель - в ячейке D (ячейка 2). Остаток будет в аккумуляторе. Текст программы будет позже (Рисунок 3-21).
--------------------------------------------
Упражнение 3.15
Проверьте правильность программы, выполнив её вручную, и расписав программу (как в Упражнении 3.12). Разделите 33 на 3. Результат должен получиться равным 11; остаток - 0.
--------------------------------------------
Логические операции.
Микропроцессор 6502 может выполнять и другие инструкции - у него есть набор логических операций: И, ИЛИ и Исключающее ИЛИ. Сюда же относится инструкция сравнения CMP. Применение инструкций AND, ORA и EOR будет в Главе 4. Сейчас же мы напишем программу, которая проверяет, что содержится в ячейке памяти LOC - ноль, единица, или же другое число:
LDA LOC ; Считываем значение из ячейки LOC в аккумулятор.
CMP #$00; Проверяем, равен ли аккумулятор нулю.
BEQ ZERO; Если да - переходим к метке ZERO.
CMP #$01; Проверяем, равен ли аккумулятор единице.
BEQ ONE ; Если да - переходим к метке ONE.
NONE FOUND ... ; Если не было перехода - программа выполняется последовательно.
...
...
ZERO ...
...
...
ONE ...
...
...
CMP #$00; Проверяем, равен ли аккумулятор нулю.
BEQ ZERO; Если да - переходим к метке ZERO.
CMP #$01; Проверяем, равен ли аккумулятор единице.
BEQ ONE ; Если да - переходим к метке ONE.
NONE FOUND ... ; Если не было перехода - программа выполняется последовательно.
...
...
ZERO ...
...
...
ONE ...
...
...
Первая инструкция (LDA LOC) загружает содержимое ячейки памяти LOC в аккумулятор, чтобы мы могли выполнять с ним операции.
CMP #$00
Эта инструкция сравнивает содержимое аккумулятора с шестнадцатеричным числом 00 (то есть, с числом 00000000). После этой инструкции будет установлен Флаг Z. Его значение будет проверено в следующей инструкции:
BEQ ZERO
Инструкция BEQ расшифровывается как "Branch if EQual" (Ветвление при равенстве). Ветвление зависит от состояния флага Z. Если он установлен (равен единице) - программа перейдёт к метке ZERO. Если нет - то продолжит выполняться последовательно.
CMP #$01
Опять то же самое, но аккумуятор сравнивается уже с числом 00000001. Если флаг Z после этого будет равным 0 - программа будет выполняться последовательно; если нет - произойдёт ветвление.
----------------------------------------
Упражнение 3.16
Напишите программу, которая проверяет - соответствует ли значение ячейки 24 символу "*". Если да - сделайте ветвление к метке STAR. Битовый код этого символа - 00101010.
----------------------------------------
Заключение.
Мы изучили самые основные инструкции процессора 6502 и их применение. Мы научились передавать данные из регистров в память, и обратно. Мы разобрались, как выполнять с информацией арифметические и логические действия. Мы разобрались, как создавать цикл и выполнять умножение. Теперь мы готовы к изучению важной технологии в программировании - к созданию подпрограмм.
----------------------------------------
Подпрограммы.
Подпрограмма - это блок инструкций, которому программист может дать название. Каждая подпрограмма начинается с особой инструкции (объявления подпрограммы), которая делает её название понятным для ассемблера. Завершается подпрограмма другой инструкцией (Возвратом). Давайте разберёмся, что нам даёт использование подпрограмм, и как подпрограммы реализуются.
Использование подпрограммы изображено на Рисунке 3-23. Основная программа изображена в левой части рисунка. Линии в ней - это инструкции, которые последовательно выполняются, пока не встретятся НОВЫЕ инструкции - CALL и SUB. Это - особые инструкции - Вызов подпрограмм, они вызывают переход в подпрограмму. Это означает, что после инструкции CALL SUB следующей будет выполнена первая инструкция подпрограммы. Потом подпрограмма выполняется точно так же, как и обычная программа. В нашем примере подпрограмма не вызывает другие подпрограммы. Последняя инструкция подпрограммы - RETURN. Она вызывает выход из подпрограммы и возвращение в основную программу. После этого выполнится инструкция, следующая после инструкции CALL SUB, и программа последовательно выполняется до следующего перехода.
Когда инструкция CALL SUB встречается второй раз - опять происходит переход в подпрограмму, и она выполняется. После возврата выполняется инструкция уже после ВТОРОГО CALL SUB, и основная программа опять выполняется последовательно.
Что же нам даёт использование подпрограмм? Главная их ценность - в том, что мы можем переходить к ним из любого участка программы. Другое преимущество - в том, что они экономят память (в нашем примере подпрограмма выполняется дважды, при этом каждая инструкция написана один раз). А ещё это делает разработку программ проще: можно написать подпрограмму, и использовать её в программе много раз.
----------------------------------------------------
Упражнение 3.17
Какой самый большой недостаток у использования подпрограмм?
----------------------------------------------------
Недостаток подпрограмм станет понятным, если проследить ход программы. В ней добавлены инструкции CALL SUB и RETURN, на выполнение которых требуется время.
----------------------------------------------------
Реализация подпрограмм.
У 6502 есть две особые встроенные инструкции - CALL SUB и RETURN. Инструкция CALL SUB вызывает переход к инструкции с нужным адресом. Вы помните (если нет - повторите Главу 1), что адрес следующей инструкции хранится в Программном Указателе (Регистре PC). Это значит, что инструкция CALL SUB изменяет значение регистра PC. Но достаточно ли этого?
Чтобы ответить на этот вопрос - разберёмся со второй инструкцией - RETURN. Инструкция RETURN (как следует из её названия) возвращает назад - к следующей инструкции после перехода. Это возможно только в одном случае: если адрес этой следующей инструкции сохранён где-то ещё. Он равен программному указателю в момент перехода + размер самой инструкции вызова.
Теперь вопрос: где его хранить? Он должен быть сохранён в таком месте, где его нельзя случайно стереть или изменить. Причём, в следующем примере (Рисунок 3-24) одна подпрограмма вызывает другую, а реально может быть и больше уровней вложенности. Поэтому должно быть место для хранения нескольких адресов (причём возврат из подпрограмм должен идти по порядку: из самой вложенной - в предыдущую, из неё - в предыдущую, и так до основной программы).
Такое место для хранения есть - оно называется Стеком. Обратите внимание на программу (Рисунок 3-26). По адресу 100 встречается первый вызов подпрограммы: CALL SUB1. В процессоре 6502 вызов подпрограммы использует 3 байта. Поэтому адрес следующей инструкции будет 103. Процессор "знает", что вызов - это 3-байтная инструкция, и будет сохранено именно число 103. Потом в Программный Указатель загружается значение 280, и выполняются инструкции, начиная с этого адреса.
Число 103 помещается в стек. Подпрограмма выполняется, но по адресу 300 есть ещё один переход. Соответственно, в Стек поместится ещё число 303, а Программный указатель становится равным 900. Выполняются инструкции, начиная с адреса 900.
Когда подпрограмма SUB2 завершается (встречается инструкция RETURN) - из Стека извлекается число, которое попало туда последним. Последним у нас было число 303. Программный указатель становится равным 303, и выполняются инструкции, начиная с этого адреса (это подпрограмма SUB1). Когда в этой подпрограмме встречается команда RETURN - из Стека извлекается следующее число (103). Происходит возврат к инструкции по этому адресу (это основная программа). Всё получилось так, как нужно.
Возможность вложений ограничена размером стека. В старых мигропроцессорах стек состоял из 4 или 8 регистров - соответственно, могло быть 4 или 8 уровней вложенности. У 6502 стек состоит из 256 ячеек памяти - то есть, можно сделать 128 уровней вложенности (если использовать адресацию Нулевой Страницы. Но это в идеальных условиях - если нет прерываний, и стек не используется для других целей. В реальности столько уровней вложенности использовать сложно, да и не нужно.
На Рисунках 3-24 и 3-25 подпрограммы изображены справа от основной программы. Это сделано для наглядности - на самом деле, они идут в тексте самой программы. Причём могут быть и в начале, и в середине, и в конце. Именно поэтому необходимо объявление подпрограмм: процессор должен их распознать. Про соответствующие инструкции (директивы) рассказано в Главе 10.
-----------------------------------------
Подпрограммы 6502.
Мы разобрались, как работают подпрограммы, и как для этого используется стек. В 6502 подпрограммы вызывает инструкция JSR (Jump to SubRoutine, Прыжок в Подпрограмму). Это - трёхбайтная инструкция. Этот переход происходит безусловно, без проверки Флагов (но можно использовать ветвление, и обходить/вызывать эту инструкцию).
Для выхода из подпрограмм есть инструкция RTS (ReTurn from Subroutine, возврат из подпрограммы). Это 1-байтовая инструкция.
-----------------------------------------
Упражнение 3.18
Почему выход из подпрограммы занимает столько же времени, как и переход в неё? Подсказка: Ответ не очевиден. Обратите вннимание на то, как устроен стек, и какие операции должны быть выполнены.
-----------------------------------------
Примеры подпрограмм.
Многие написанные подпрограммы мы позже будем использовать в качестве подпрограмм. Например, во многих программах пригодится наша программа умножения. Чтобы сделать её подпрограммой - нужно только дать ей осмысленное название (например, MULT) и добавить в конец инструкцию возврата (RTS).
-----------------------------------------
Упражнение 3.19
Может ли подпрограмма MULT "испортить" значения внутренних регистров и флагов?
-----------------------------------------
Рекурсия.
Рекурсия - это вызов подпрограммой самой себя. Если Вы поняли принцип работы подпрограмм - то запросто ответите на следующий вопрос:
-----------------------------------------
Упражнение 3.20
Можно ли сделать так, чтоб подпрограмма вызывала сама себя (будет ли при этом всё правильно работать)? Если Вы не уверены в ответе - нарисуйте стек и то, как он будет заполняться. Тогда Вы увидите ответ на этот вопрос. Также обратите внимание на регистры и память. Если так сделать нельзя - объясните, почему. Если можно - объясните, какие могут быть сложности.
-----------------------------------------
Параметры подпрограммы.
Многим подпрограммам нужны вводные данные (параметры). Например, подпрограмме умножения нужны два числа, которые она будет перемножать. Можно сделать так, как мы сделали в нашей программе: заранее разместить эти числа в памяти. Но есть три способа передачи параметров подпрограмме:
1. Через регистры.
2. Через память.
3. Через стек.
Передача параметров через регистры - это удачное решение. Подпрограмма не будет привязана к конкретной ячейке памяти Если параметры для подпрограмм находятся в памяти - программисту нужно быть осторожнее (ячейка может быть обнулена или изменена, см. Упражнение 3-20). Поэтому в памяти даже отводят специальный блок - для хранения параметров подпрограмм.
Передача параметров через память тоже даёт большое преимущество - в памяти этих параметров можно хранить очень много. Недостатка два: во-первых, эти параметры могут быть случайно изменены (см. выше), во-вторых, операции с памятью выполняются дольше.
Размещение параметров в стеке даёт такое же преимущество, как и передача через регистры - независимость от конкретной ячейки памяти. При этом информации там может храниться больше, чем в регистрах. Самый большой недостаток тоже очевиден: стек нужен и для других целей (например, для хранения информации о точках выхода из подпрограмм).
Выбор - за программистом. Но желательно, чтобы подпрограммы не были зависимы от конкретных ячеек памяти.
Если регистры недоступны (используются для чего-то ещё) - используйте стек. Если подпрограмме надо передать много информации - используйте память. Для памяти ещё одна удобная возможность - использование Указателя. Указатель - это адрес, с которого начинается нужный блок информации. Указатель может быть передан в регистр (в процессоре 6502 максимальный размер указателя - 8 бит), или же в стек.
Если же ничего из этого не подходит - только тогда используйте привязку к определённой ячейке памяти.
------------------------------------------
Упражнение 3.21
Какой способ передачи параметров лучше всего подходит для рекурсии?
------------------------------------------
Библиотека подпрограмм.
У подпрограмм есть два больших преимущества. Во-первых, они могут быть отлажены независимо от основной программы. Во-вторых, им можно дать названия, понятные человеку. Можно составить коллекцию из нужных подпрограмм, и потом их использовать (даже в разных программах). Но всё хорошо в меру. Излишнее использование подпрограмм делает программу медленнее (В подпрограммах может быть то, что в конкретном случае не нужно. Плюс, сам по себе вызов подпрограмм требует ресурсов). Опытный программист оценивает преимущества и недостатки, и принимает лучшее решение.
------------------------------------------
Заключение.
Эта глава посвящена использованию инструкций процессора 6502. Мы решили (сначала в виде алгоритмов, потом в виде программ) сложные задачи, и использовали для этого самые основные инструкции. Если что-то осталось непонятным - повторите. Следующая глава посвящена более глубокому изучению инструкций.
-------------------------------------------------------------
Набор инструкций процессора 6502.
-----------------------------------
Часть 1 - общее описание.
Введение.
Эта часть посвящена беглому обзору видов инструкций процессора 6502. Их действию, влиянию на состояние Флагов и использованию разных режимов адресации. Более подробно про режимы адресации - в Главе 5.
-------------------------------------
Виды инструкций.
Инструкции можно разделить на группы по разным признакам - тут нет общего стандарта. Мы же условно разделим их на пять основных видов:
1. Инструкции передачи информации.
2. Инструкции обработки информации.
3. Инструкции проверки условий и ветвления.
4. Инструкции ввода/вывода.
5. Инструкции управления.
Теперь подробнее про каждый вид:
--------------------------------------
Инструкции передачи информации.
Инструкции передачи информации передают 8-битные данные между двумя регистрами, между регистром и памятью, или же между регистром и утройством ввода/вывода. Инструкции передачи используют инструкции для особых целей. Например, для помещения информации в стек/извлечения её из стека - в этом случае они передают один байт информации из стека в аккумулятор, и изменяют значение Указателя стека.
---------------------------------------
Инструкции обработки информации.
Инструкции обработки информации тоже можно разделить на четыре группы:
- Арифметические операции (сложение и вычитание).
- Логические операции (И, ИЛИ, Исключающее ИЛИ).
- Операции сдвига.
- Прибавление и вычитание единицы.
Было бы удобно иметь инструкции, способные выполнять сложные математические действия (например, умножение и деление). Но к сожалению, в большинстве микропроцессоров они недоступны. Могли бы пригодиться и более мощные инструкции сдвига (например, обмен полубайтами или сдвиг сразу на несколько позиций). Но и они недоступны в большинстве микропроцессоров.
Перед тем, как объяснять действие этих инструкций - вспомним разницу между инструкциями Сдвига и Вращения. Инструкции сдвига смещают содержимое регистра или ячейки памяти на одну позицию влево или вправо. Тот бит, который "выпадает" (который был самым крайним до сдвига) - помещается в Флаг Переноса. Освобождённый же бит (который был с другого края) становится равным нулю.
При Вращении "выпадающий" бит тоже помещается в Флаг Переноса. Но кроме этого, его значение помещается в освободившийся бит. Существует также операция "арифметический правый сдвиг". При многих операциях (особенно с Правилом двух прибавлений) часто бывает нужно сдвинуть отрицательное число вправо. При этом самый левый бит должен быть равен единице (она обозначает минус). Она должна была бы всегда помещать знак минуса в самый левый бит (пока не будет выполнено нужное число сдвигов). Но в микропроцессоре 6502 такой инструкции тоже нет.
---------------------------------------------
Инструкции проверки и ветвления.
Инструкции проверки проверяют, чему равны Флаги. Хорошо было бы иметь инструкции, которые могут проверять сразу несколько условий; побитово сравнивать содержимое регистров с Флагами; сравнивать значения регистров между собой. Но в 6502 эти инструкции ограничены сравнением отдельных битов с флагами.
Инструкции Прыжка делятся на три группы.
- Назначение прыжка (требует 16-битный адрес).
- Ветвление, которое обычно изменяет Программный Указатель на 8-битное значение.
- Вызов подпрограмм.
Может быть полезным двух-, трёх- и больше вариантное ветвление. Например, после сравнения - если результат будет меньше, равен или больше. Также удобно пропускать инструкции с помощью прыжка вперёд или назад. И наконец, удобно создавать циклы путём уменьшения счётчика, и выхода из цикла после достижения счётчиком нужного значения. Но в большинстве микропроцессоров нет таких инструкций - такой эффект можно создать только комбинацией инструкций. Доступны только простые инструкции ветвления и простые инструкции проверки условий.
Ввод/вывод.
Инструкции ввода/вывода предназначены для перехвата сигналов с устройств ввода/вывода. Почти все микропроцессоры используют ввод-вывод с распределением памяти. Это означает, что устройства ввода-вывода соединяются с адресной шиной, как и чипы памяти - и адресуются точно так же. То есть, операции с памятью могут быть применены и к этим устройствам. Получается большое преимущество: устройствам ввода/вывода доступно много стандартных инструкций. Недостаток - в том, что обычные операции с памятью требуют три байта - и поэтому выполняются медленно. Чтобы повысить эффективность - для самых важных устройств используют механизм короткой адресации.
Инструкции управления.
Инструкции управления приостанавливают синхронизацию сигналов, и могут приостанавливать или прерывать программу. Также они могут вызывать остановку или симулировать прерывание. Подробнее про прерывания - см. Главу 6 (Технологии ввода/вывода).
----------------------------------------------------------------------
Инструкции, доступные процессору 6502.
Инструкции передачи информации.
У процессора 6502 есть полный набор инструкций для передачи информации, кроме загрузки указателя стека. Можно загружать информацию из памяти в аккумулятор (инструкция LDA) и в память из аккумулятора (инструкция STA). То же самое для регистров X и Y (соответственно, инструкции называются LDX, LDY, STX и STY). Есть также инструкции для обмена информацией между регистрами - TAX (Transfer A to X, передача из регистра A в регистр X), TAY, TSX, TXA, TXS, TYA.
Двухадресных операций по передаче из памяти в память - НЕ существует.
-----------------------------------------------------------------
Операции со стеком.
Существуют две операции помещения в стек и две операции извлечения из стека. Инструкции PHA и PHP передают значение аккумулятора (регистра "A") или регистра статуса "P" в стек, и обновляют значение Указателя Стека "S". Инструкции PLA и PLP, соответственно, загружают информацию из стека в эти регистры.
---------------------------------------
Инструкции обработки информации.
Арифметика.
В 6502 базовые инструкции - сложение, логика и сдвиг. Арифметические операции выполняются инструкциями ADC и SBC. ADC вызывает сложение с переносом. Инструкции сложения без переноса - нет, но можно использовать инструкцию CLC, чтобы очистить Флаг Переноса. Вычитание производится инструкцией SBC.
С помощью специальной инструкции можно вызвать специальный десятичный режим сложения и вычитания в системе BCD. В большинстве других микропроцессоров сложение в в системе BCD реализовано отдельными инструкциями.
-----------------------------
Прибавление/вычитание единицы.
Операции прибавления и вычитания единицы доступны для памяти и индексных регистров "X" и "Y". Для памяти это инструкции INC и DEC; для регистров - INX, INY, DEX и DEY. Для аккумулятора такой инструкции нет.
-----------------------------
Логические операции.
Доступны все стандартные логические операции - AND, ORA и EOR.
Операция AND.
Для каждой логической операции существует таблица результатов. Для операции AND (Логическое "И") таблица будет такой:
0 AND 0 = 0
0 AND 1 = 0
1 AND 0 = 0
1 AND 1 = 1
0 AND 1 = 0
1 AND 0 = 0
1 AND 1 = 1
Операция AND проверяет, равны ли оба вводных единице, и если да - то отвечает положительно (даёт ответ 1). Если же хотя бы одно из вводных равно нулю - то и результат будет равным нулю. Эта операция часто используется, чтобы обнулять несовпадающие битовые позиции в байте (выполнять Маскирование).
Одно из самых главных применений операции AND - скрывать некоторые биты. Например, мы хотим скрыть четыре самых правых бита. Для этого подойдёт программа:
LDA #WORD ; WORD - это ячейка памяти - допустим, она равна 10101010.
AND #%11110000; 11110000 - "маска".
AND #%11110000; 11110000 - "маска".
Мы загружаем в аккумулятор число 10101010. Нам нужно скрыть правые 4 бита. Знак % означает, что мы используем двоичное число. После операции AND в аккумуляторе будет число 10100000:
10101010
AND 11110000
____________
= 10100000
AND 11110000
____________
= 10100000
-------------------------------------
Упражнение 4.1
Напишите программу из трёх инструкций, которая обнулит 1-й и 6-й биты, и сохранит результат на место старого числа.
Упражнение 4.2
Что будет, если использовать маску 11111111 ?
----------------------------------------------------------
Инструкция ORA.
Это инструкция выполняет логическое действие ИЛИ. Таблица результатов для этой операции:
0 OR 0 = 0
0 OR 1 = 1
1 OR 0 = 1
1 OR 1 = 1
0 OR 1 = 1
1 OR 0 = 1
1 OR 1 = 1
Если хотя бы одно из вводных равно единице - результат будет равен единице. Если же оба равны нулю - результат будет равен нулю. Пример её использования:
LDA #WORD
ORA #%00001111
ORA #%00001111
Предположим, в ячейке WORD содержалось число 10101010. После выполнения этой программы в аккумуляторе будет число 10101111:
10101010
OR 00001111
___________
= 10101111
OR 00001111
___________
= 10101111
------------------------------------------
Упражнение 4.3
Выполните действие (на бумаге или в уме): 10101010 OR 10101111.
Упражнение 4.4
Что будет, если применить операцию OR к шестнадцатеричному числу "FF"?
--------------------------------------------
Инструкция EOR.
Эта инструкция выполняет логическое действие "Исключающее ИЛИ". Оно выдаёт единицу, если ХОТЯ БЫ одно (но не оба) вводных равны единице. Таблица результатов:
0 EOR 0 = 0
0 EOR 1 = 1
1 EOR 0 = 1
1 EOR 1 = 0
0 EOR 1 = 1
1 EOR 0 = 1
1 EOR 1 = 0
Эта операция используется для сравнений. Если биты отличаются - результатом будет единица. Если они одинаковы - будет ноль. Можно сравнивать и целые байты - если они полностью одинаковы, результатом будет ноль (00000000). В процессоре 6502 ей есть и другое применение: её используют для битового прибавления (отдельной специальной инструкции для этого нет). Такого эффекта достигают, выполнив операцию EOR между нужным битом и числом 11111111. Пример:
LDA #WORD
EOR #%11111111
EOR #%11111111
Если число WORD равно 10101010 - результат будет 01010101:
10101010
EOR 11111111
____________
= 01010101
EOR 11111111
____________
= 01010101
-------------------------------------------------
Упражнение 4.5
Выполните такую же операцию, но с числом #$00.
-------------------------------------------------
Операции Сдвига.
В 6502 есть операции: левый сдвиг (ASL, арифметический сдвиг влево), правый сдвиг (LSR, логический сдвиг вправо).
Также есть инструкция Вращения (ROL).
В новейших версиях (на момент написания книги, то есть 30 лет назад - примечание переводчика :-))) 6502 существует ещё одна инструкция вращения - вращение вправо (ROR). Доступна ли она - нужно узнавать у производителя.
------------------------------------------------
Сравнения.
Регистры "X", "Y" и "A" (аккумулятор) можно сравнивать с содержимым ячеек памяти. Для этого есть инструкции CPX, CPY и CMP.
------------------------------------------------
Инструкции проверки и ветвления.
Поскольку проверки условия выполняются с Флагами - пришло время разобраться с тем, какие вообще Флаги доступны в 6502. Флаги являются битами специального регистра, и располагаются в нём так:
7 6 5 4 3 2 1 0 Номер бита
_______________________________
| | | | | | | | |
| N | V | - | B | D | I | Z | C |
|___|___|___|___|___|___|___|___|
N - Флаг Отрицательности.
V - Флаг Переполнения.
B - Флаг Остановки.
D - Флаг Десятичности.
I - Флаг Прерывания.
Z - Флаг Нуля.
C - Флаг Переноса.
_______________________________
| | | | | | | | |
| N | V | - | B | D | I | Z | C |
|___|___|___|___|___|___|___|___|
N - Флаг Отрицательности.
V - Флаг Переполнения.
B - Флаг Остановки.
D - Флаг Десятичности.
I - Флаг Прерывания.
Z - Флаг Нуля.
C - Флаг Переноса.
----------------------------------------
Знак.
Флаг "N", как правило, соответствует 7-му биту аккумулятора. Соответственно, 7-ой бит аккумулятора - это единственный бит, который может быть проверен одиночной инструкцией. Чтобы проверить другие биты - нужно выполнять операцию сдвига. И если нужно выполнить проверку всего байта - делают так, чтоб перед проверкой проверяемый бит попадал в 7-ую позицию. При чтении статуса устройства ввода/вывода - обычно считывают состояние его регистра, и передают это значение в аккумулятор, и после этого проверяют состояние Флага "N".
В числах самый левый бит является битом знака. Если он равен единице - это означает, что число отрицательное. Флаг "N" определяется заново после каждой передачи или изменения информации (в зависимости от того, какое число получилось).
На состояние Флага "N" влияют инструкции: ADC, AND, ASL, BIT, CMP, CPY, CPX, DEC, DEX, DEY, EOR, INC, INX, INY, LDA, LDX, LDY, LSR, ORA, PLA, PLP, ROL, ROR, TAX, TAY, TXS, TXA, TYA.
------------------------------
Переполнение.
Мы уже обсуждали применение Флага Переполнения (например, в Главе 3). Он показывает то, что результат может стать неправильным из-за того, что произошёл перенос из бита 6 в бит 7 (то есть, сменится знак, хотя на самом деле должно просто получиться большое число). Чтобы не допустить таких ошибок, обычно создают специальные подпрограммы. Инструкция BIT применяет этот Флаг по-своему: он становится равным 6-му биту проверяемого байта.
На состояние Флага "V" влияют инструкции: ADC, BIT, CLV, PLP, RTI, SBC.
-------------------------------------------------
Остановка.
Флаг Остановки автоматически устанавливается при выполнении инструкции BRK. Он даёт возможность отличить программную остановку от аппаратного прерывания. Никакие другие инструкции не влияют на него.
-------------------------------------------------
Десятичность.
Мы уже использовали Флаг Десятичности в Главе 3. Если этот Флаг равен единице - арифметические операции будут выполняться по системе BCD. Если же он равен нулю - они будут выполняться в двоичном режиме. На этот флаг влияют инструкции: CLD, PLP, RTI, SED.
-------------------------------------------------
Прерывание.
Этот Флаг может быть установлен программистом с помощью инструкций SEI и PLP. Ещё он может быть установлен самим микропроцессором при аппаратном прерывании. Если он установлен - дальнейшие прерывания будут подавляться.
На состояние этого Флага влияют инструкции: BRK, CLI, PLP, RTI, SEI.
-----------------------------------------
Ноль.
Установленный Флаг Нуля (то есть, когда он равен единице) показывает, что в результате операции получился ноль. Для его установке нет специальной инструкции. Но если нужно, его всё равно можно установить. Например, инструкцией:
LDA #0
На Флаг Нуля влияют инструкции: ADC, AND, ASL, BIT, CMP, CPY, CPX, DEC, DEX, DEY, EOR, INC, INX, INY, LDA, LDX, LDY, LSR, ORA, PLA, PLP, ROL, ROR, RTI, SBC, TAX, TAY, TXA, TYA.
----------------------------------------------
Перенос.
Флаг Переноса используется для двух целей. Во-первых, он хранит перенос при операциях с большими числами. Во-вторых, он хранит "выпадающий" бит при операциях сдвига. Особенно часто он используется при выполнении умножения и деления (для экономии времени). Существует специальные инструкции (CLC и SEC) соответственно для обнуления и установки Флага Переноса. На Флаг Переноса влияют инструкции: ADC, ASL, CLC, CMP, CPX, CPY, LSR, PLP, ROL, ROR, RTI, SBC, SEC.
------------------------------------------------
Инструкции проверки и ветвления.
У процессора 6502 есть инструкции для проверки только четырёх Флагов (равны они нулю или нет). Соответственно, существует 8 инструкций ветвления:
- BMI (Branch on MInus, Ветвление при минусе), BPL (Branch on PLus, Ветвление при плюсе) - проверяют Флаг Отрицательности "N".
- BCC (Branch on Carry Clear, Ветвление, если нет переноса), BCS (Branch on Carry Set, Ветвление, если есть перенос) - проверяют Флаг Переноса "C".
- BEQ (Branch when result is null, Ветвление при нулевом результате), BNE (Branch on result not zero, Ветвление при ненулевом результате) - проверяют Флаг Нуля "Z".
- BVS (Branch when oVerflow is Set, Ветвление при переполнении), BVC (Branch on oVerflow Clear, Ветвление, если нет переполнения) - проверяют Флаг Переполнения "V".
Все инструкции ветвления изменяют Программный Указатель (адрес инструкции, которая должна быть выполнена следующей) на определённое число (относительное смещение). Это смещение является 8-битным числом, то есть оно может быть в пределах (-128 ... +127). Соответственно, перейти можно максимум на 128 позиций назад или на 127 позиций вперёд.
Поскольку сами инструкции ветвления занимают 2 байта - на самом деле возможен переход на 126 (128 - 2) позиций назад или на 129 (127 + 2) позиций вперёд.
Также есть две инструкции безусловного перехода: JMP и JSR. Инструкция JMP выполняет переход к нужному 16-битному адресу. Инструкция JSR - это вызов подпрограммы. Перед переходом они автоматически сохраняют старое значение Программного Указателя в Стек. Можно сделать переход условным, поставив перед такой инструкцией инструкцию ветвления.
Есть две инструкции возвращения: RTI (возврат из прерывания, про него - позже), и RTS (выход из подпрограммы - забирает из стека старое значение Программного Указателя, прибавляет к нему единицу, и программа продолжает выполняться с получившегося адреса).
Существуют две инструкции специально для проверки и сравнения битов.
Инструкция BIT выполняет логическую операцию "И" между содержимым ячейки памяти и аккумулятором. Важное замечание: она НЕ изменяет значение аккумулятора. При выполнении этой инструкции флаг "N" становится равным значению 7-го бита ячейки; флаг "V" - значению 6-го бита ячейки. Результат выполнения операции устанавливается в флаге "Z". Флаг "Z" становится равным нулю, если результат равен единице, и наоборот. Обычно в аккумулятор загружается нужная маска, и ячейки памяти последовательно проверяются инструкцией BIT. Например, если маска содержит одну цифру "1" - будет проверяться, равен ли единице соответствующий бит ячеек. Как мы помним, биты 6 и 7 автоматически отображаются в Флагах "V" и "N", поэтому маскировать их не нужно.
Инструкция CMP сравнивает содержимое ячейки памяти с аккумулятором. Результат отображается в Флагах "Z" и "N". С помощью этой инструкции можно проверить, равны ли числа, и если нет - то какое из них больше. Значение аккумулятора при этом не изменяется. Соответственно инструкции CPX и CPY делают то же самое - но с регистрами "X" и "Y".
--------------------------------------------
Инструкции ввода/вывода.
У процессора 6502 нет специальных инструкций для ввода/вывода.
--------------------------------------------
Инструкции управления.
Инструкции управления выполняют установку/обнуление Флагов. Инструкции CLC, CLD, CLI, CLV очищают соответственно Флаги "C", "D", "I", "V". Инструкции SEC, SED, SEI устанавливают равными единице соответственно Флаги "C", "D", "I".
Инструкция BRK является аналогом программного прерывания, подробнее про неё - см. Главу 7.
Инструкция NOP - ничего не делает. Обычно она используется для замедления программы (Например, в играх - чтобы процесс не шёл слишком быстро - примечание переводчика).
Есть две инструкции (IRQ и NMI) для работы с прерываниями. Подробнее - см. Главу 6.
Теперь - подробнее про каждую инструкцию.
Чтобы понять режимы адресации - читатель может бытро пройтись по следующему разделу или прочитать Главу 5 (Технологии адресации).
-------------------------------------------------------
Часть 2 - Инструкции.
-------------------------------------------------------
Условные обозначения:
A - Аккумулятор.
M - Ячейка памяти (адрес).
P - Регистр Статуса.
S - Указатель стека.
X - Индексный регистр "X".
Y - Индексный регистр "Y".
DATA - Информация.
HEX - Шестнадцатеричное.
PC - Программный указатель.
PCH - Старший байт программного указателя.
PCL - Младший байт программного указателя.
STACK - Содержимое вершины стека.
"ИЛИ" - Логическое "ИЛИ".
"И" - Логическое "И".
"ИсИЛИ" - Исключающее "ИЛИ".
* - Выбор.
<- - Получает значение.
( ) - Содержимое.
(M6) - 6-ой бит ячейки памяти M.
--------------------------------------------------
Инструкция ADC.
Функция: A <- (A) + DATA + C
Описание.
Прибавляет к аккумулятору содержимое ячейки памяти или заданное число, плюс перенос. Результат сохраняется в аккумуляторе.
Замечания:
- Инструкция ADC подходит и для двоичного, и для десятичного режима. Чтобы выбрать режим - нужно установить в нужное значение Флаг Десятичности.
- Чтобы перенос не учитывался, Флаг Переноса нужно заранее обнулить (инструкцией CLC).
---------------------------------
Инструкция AND.
Функция: A <- (A) "И" DATA
Описание:
Выполняет логическую операцию "И" между аккумулятором и выбранным значением. Результат сохраняется в левой части аккумулятора.
---------------------------------------------
Инструкция ASL.
___ _______________________________
| | | | | | | | | | |
Функция: | C | | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | <---- 0
|___| |___|___|___|___|___|___|___|___|
^ | <- <- <- <- <- <- <-
|_______|
| | | | | | | | | | |
Функция: | C | | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | <---- 0
|___| |___|___|___|___|___|___|___|___|
^ | <- <- <- <- <- <- <-
|_______|
Описание:
Сдвигает содержимое аккумулятора или ячейки памяти на одну позицию влево. Самый правый (нулевой) бит становится равным нулю. 7-ой бит помещается в Флаг Переноса. Результат сохраняется в источнике (то есть, в аккумуляторе или в той же ячейке памяти).
-----------------------------------------
Инструкция BCC.
Функция: Переход к нужному адресу, если Флаг "C" равен нулю.
Описание:
Проверяет Флаг Переноса. Если он равен нулю, прибавляет число от -128 до +127 (смещение) к адресу текущей инструкции, и выполняет переход к инструкции с получившимся адресом. Если Флаг Переноса равен единице - ничего не происходит.
--------------------------------------
Инструкция BCS.
Функция: Переход к нужному адресу, если Флаг Переноса равен единице.
Описание:
Проверяет Флаг Переноса. Если он равен единице - прибавляет число от -128 до +127 (смещение) к адресу текущей инструкции, и выполняет переход к инструкции с получившимся адресом. Если Флаг Переноса равен нулю - ничего не происходит.
--------------------------------------------------
Инструкция BEQ (Branch if EQual to zero, Ветвление при нулевом результате).
Функция: Переход к нужному адресу, если Флаг Нуля равен единице (то есть, если результат последней операции получился нулевым).
Описание:
Проверяет Флаг Нуля. Если он равен единице - прибавляет число от -128 до +127 (смещение) к адресу текущей инструкции, и выполняет переход к инструкции с получившимся адресом. Если Флаг Нуля равен нулю - ничего не происходит.
--------------------------------------------------
Инструкция BIT.
Функция: Z <- (A) "И" (M), N <- (M7), V = DATA).
----------------------------------------------
Инструкция DEC (DECrement, Уменьшение на единицу).
Функция: M <- (M) - 1
Описание:
Содержимое ячейки памяти уменьшается на единицу. Результат сохраняется в ту же самую ячейку.
----------------------------------------------
Инструкция DEX (DEcrement X, Уменьшение регистра "X" на единицу).
Функция: X <- (X) - 1
Описание:
Значение регистра "X" уменьшается на единицу. Это очень полезно, чтобы использовать регистр "X" в качестве счётчика.
----------------------------------------------
Инструкция DEY (DEcrement Y, Уменьшение регистра "Y" на единицу).
Функция: Y <- (Y) - 1
Значение регистра "Y" уменьшается на единицу. Регистр "Y" можно использовать в качестве счётчика.
----------------------------------------------
Инструкция EOR (Exclusive-OR with accumulator, Исключающее "ИЛИ" со значением аккумулятора).
Функция: A <- (A) "ИсИЛИ" DATA
Описание:
Между аккумулятором и числом DATA (это может быть заданное число или значение ячейки памяти) выполняется логическая операция "Исключающее ИЛИ". Результат сохраняется в аккумулятор.
Инструкция EOR со значением DATA = -1 может быть использована для Прибавления.
------------------------------------------
Инструкция INC (INCrement memory, Прибавление единицы к ячейке памяти).
Функция: M
Описание:
Сдвигает биты значения (аккумулятора или ячейки памяти) вправо на одну позицию. Бит 7 становится равным нулю; бит 0 - помещается в Флаг Переноса. Результат сохраняется в то же место (аккумулятор или ячейку памяти).
-----------------------------------------
Инструкция NOP (No Operation, Нет действия).
Функция: Ничего не делает.
Описание:
Вызывает простой программы в течение двух циклов. Может быть использована для замедления программы (например, в играх), или для заполнения пробелов в программе.
-----------------------------------------
Инструкция ORA (OR with Accumulator, Логическая операция "ИЛИ" с аккумулятором).
Функция: A <- (A) "ИЛИ" DATA
Описание:
Выполняет логическую операцию "ИЛИ" между аккумулятором и значением DATA (это может быть заданное число или значение ячейки памяти). Может использоваться для заполнения выбранных битов единицами.
-----------------------------------------
Инструкция PHA (PusH A, Помещение аккумулятора в стек).
Функция:
STACK <- (A)
S <- (S) - 1
S <- (S) - 1
Описание:
Содержимое аккумулятора помещается в стек. Указатель стека обновляется. Значение аккумулятора - не изменяется.
-----------------------------------------
Инструкция PHP (PusH P, Помещение регистра "P" в стек).
Функция:
STACK <- (P)
S <- (S) - 1
S <- (S) - 1
Описание:
Содержимое Регистра Статуса "P" помещается в стек. Указатель стека обновляется. Значение аккумулятора - не изменяется.
-----------------------------------------
Инструкция PLA (PulL Accumulator, Загрузка из стека в аккумулятор).
Функция:
A <- (STACK)
S <- (S) + 1
S <- (S) + 1
Описание:
Загружает в аккумулятор значение из вершины стека. Указатель стека обновляется.
-----------------------------------------
Инструкция PLP (PulL P, Загрузка из стека в регистр "P").
Функция:
P <- (STACK)
s |
| |
| ___ |
| | | |
-------------| C |<---------------
|___|
s |
| |
| ___ |
| | | |
-------------| C |<---------------
|___|
Описание:
Биты выбранного числа (это может быть аккумулятор или ячейка памяти) сдвигаются вправо. Значение "выпавшего" 0-го бита помещается в Флаг Переноса и в Бит 7.
---------------------------------------
Инструкция RTI (ReTurn from Interrupt, Возврат из прерывания).
Функция:
P <- (STACK)
S <- (S) + 1
PCL <- (STACK)
S <- (S) + 1
PCH <- (STACK)
S <- (S) + 1
S <- (S) + 1
PCL <- (STACK)
S <- (S) + 1
PCH <- (STACK)
S <- (S) + 1
Описание:
Восстанавливает Регистр Статуса "P" и Программный Указатель PC, которые были сохранены в Стеке. Указатель стека обновляется.
----------------------------------------
RTS (ReTurn from Subroutine, Возврат из подпрограммы).
PCL <- (STACK)
S <- (S) + 1
PCH <- (STACK)
S <- (S) + 1
PC <- (PC + 1)
PCL <- (STACK)
S <- (S) + 1
PCH <- (STACK)
S <- (S) + 1
PC <- (PC + 1)
Описание:
Восстанавливает Программный Указатель из стека, и прибавляет к нему единицу. Указатель стека обновляется.
----------------------------------------
SBC (SuBstract with Carry, Вычитание с переносом).
Функция: A <- (A) - DATA - C
Описание:
Вычитает из аккумулятора значение ячейки памяти, причём вычитание идёт с переносом. Результат сохраняется в аккумуляторе. Если нужно сделать вычитание без переноса - выполните сначала инструкцию SEC.
Инструкция SBC может быть выполнена в двоичном или в десятичном режиме - в зависимости от Флага десятичности "D".
----------------------------------------
Инструкция SEC (SEt Carry, Установка Флага переноса).
Функция: C <- 1
Описание:
Флаг Переноса становится равным единице. Это используется, чтобы выполнить инструкцией SBC вычитание без переноса.
----------------------------------------
Инструкция SED (SEt Decimal, Установка десятичного режима).
Функция: D <- 1
Описание:
Флаг Десятичности устанавливается равным единице. Это означает, что для инструкций ADC и SBC будет использоваться десятичный режим. Если же Флаг десятичности равен нулю - будет двоичный режим.
----------------------------------------
Инструкция SEI (SEt Interrupt disable, Запрет прерываний).
Функция: I <- 1
Описание:
Флаг Прерывания становится равным единице. Используется во время обработки прерывания, или же при перезапуске системы.
----------------------------------------
Инструкция STA (STore Accumulator in memory, Сохранение аккумулятора в память).
Функция: M <- (A)
Описание:
Содержимое аккумулятора сохраняется в нужную ячейку памяти. Содержимое аккумулятора не изменяется.
----------------------------------------
Инструкция STX (STore X in memory, Сохранение регистра "X" в память).
Функция: M <- (X)
Описание:
Содержимое регистра "X" сохраняется в нужную ячейку памяти. Содержимое регистра "X" не изменяется.
----------------------------------------
Инструкция STY (STore Y in memory, Сохранение регистра "Y" в память).
Функция: M <- (Y)
Описание:
Содержимое регистра "Y" сохраняется в нужную ячейку памяти. Содержимое регистра "Y" не изменяется.
----------------------------------------
Инструкция TAX (Transfer Accumulator to X, Передача аккумулятора в регистр "X").
Функция: X <- (A)
Описание:
Сохраняет содержимое аккумулятора в регистр "X". Значение аккумулятора не меняется.
----------------------------------------
Инструкция TAY (Transfer Accumulator to Y, Передача аккумулятора в регистр "Y").
Функция: Y <- (A)
Описание:
Сохраняет содержимое аккумулятора в регистр "Y". Значение аккумулятора не меняется.
----------------------------------------
Инструкция TSX (Transfer S to X, Передача Указателя Стека в регистр "X").
Функция: X <- (S)
Описание:
Сохраняет Указатель Стека в регистр "X". Значение указателя стека не меняется.
----------------------------------------
Инструкция TXA (Transfer X to A, передача регистра "X" в аккумулятор).
Функция: A <- (X)
Описание:
Сохраняет значение регистра "X" в аккумулятор. Значение регистра "X" не меняется.
----------------------------------------
Инструкция TXS (Transfer X to S, передача регистра "X" в Указатель Стека).
Функция: S <- (X)
Описание:
Сохраняет значение регистра "X" в Указатель стека. Значение регистра "X" не меняется.
----------------------------------------
Инструкция TYA (Transfer Y to A, передача регистра "Y" в аккумулятор).
Функция: A <- (Y)
Описание:
Сохраняет значение регистра "Y" в аккумулятор. Значение регистра "Y" не меняется.
----------------------------------------------------------
Глава 5. Технологии адресации.
----------------------------------------------------------
Введение.
Эта глава посвящена технологиям адресации, которые разработаны для получения данных. Во второй части мы разберём Режимы адресации процессора 6502, их преимущества и ограничения. А в конце мы напишем программу, в которой будет показана разница между режимами адресации.
Поскольку процессор 6502 не имеет 16-битного регистра, который может обозначать адрес (кроме Программного указателя) - разработчику необходимо знать использование режимов адресации, особенно применение индексных регистров. Начинающие могут пока пропустить сложные режимы - например, комбинации Непрямой и Индексной адресации. Но при разработке программ могут оказаться полезными все режимы. Поэтому давайте разберём их.
---------------------------------
Режимы адресации.
Режим адресации - это способ, которым происходит обращение к хранилищам информации (регистрам, ячейкам памяти) и обозначение этих обращений в инструкциях. Какие же они бывают?
!!!!!!!!!! Рисунок
-------------------------------
Неявная адресация.
Инструкции, которые выполняют действия только с регистрами - обычно используют Неявную адресацию. При Неявной адресации не указывается напрямую адрес, по которому расположена информация. Обычно такие инструкции используют один или несколько регистров. Поскольку число внутренних регистров ограничено (например, их может быть 8) - для такой адресации достаточно несколько бит. Например, 3 бита инструкции достаточно, чтобы дать возможность выбрать один из восьми регистров. Полностью инструкции с Неявной адресацией обычно занимают 8 бит. Это - большое преимущество, поскольку такие инструкции выполняются намного быстрее двух- и тем более, трёхбайтовых.
Пример Неявной адресации - инструкция TAX, которая передаёт значение регистра "A" (аккумулятора) в регистр "X".
---------------------------------
Непосредственная адресация.
При непосредственной адресации используется 8-битный код операции, и 8- или 16-битная константа. Такой тип адресации может понадобиться, например, для загрузки 8-битного числа в 8-битный регистр. Если микропроцессор оснащён 16-битными регистрами, то для передачи информации таким инструкциям требуется уже 16 бит. Это зависит от архитектуры процессора. Пример инструкции с непосредственной адресацией - ADC #0. Первый байт - сама инструкция, второй - значение, которое должно быть прибавлено (0).
------------------------------------
Абсолютная адресация.
Абсолютная адресация обычно используется при извлечении информации из памяти. В инструкциях с такой адресацией есть Код операции (8 бит) и адрес нужной ячейки памяти (16 бит). Пример инструкции с Абсолютной адресацией: STA $1234. Она обозначает, что значение аккумулятора будет помещено в ячейку памяти $1234 (знак $ означает, что это шестнадцатеричное число).
Недостаток Абсолютной адресации заключается в том, что инструкции с ней занимают 3 байта. Чтобы обойти этот недостаток - при возможности используют другой режим адресации (в котором адрес занимает один байт): Прямую адресацию.
-----------------------------------------
Прямая адресация.
При прямой адресации используется 8-битный адрес. Преимущество такой адресации - в том, что инструкции с ней требуют 2 байта (вместо 3-ёх при Абсолютной адресации). Недостаток - в том, что так можно обращаться только к адресам 0 - 255 (нулевая страница). По-другому такой режим ещё называется "Короткой адресацией" или "Адресацией нулевой страницы". А поскольку есть "Короткая адресация" - в противоположность ей Абсолютная адресация называется "Расширенной".
----------------------------------------
Относительная адресация.
Обычные инструкции Прыжка и Ветвления требуют 8 бит для Кода Операции, и 16 бит для нужного адреса. То есть, они занимают 3 байта. Чтобы избежать этого - можно использовать Относительную адресацию, при которой используется 2 байта. Первый байт (как всегда) - Код операции; второй - относительное смещение. Поскольку смещение может быть положительным или отрицательным - инструкции с Относительной адресацией дают возможность переместиться на 128 ячеек вперёд или на 127 ячеек назад (или наоборот, 127 вперёд и 128 назад). Относительную адресацию желательно использовать в циклах: обычно они не очень длинные, и при этом инструкции в них выполняются по много раз. Получается большой выигрыш в скорости. Например, мы уже использовали инструкцию BCC, которая позволяла переместиться на 127 позиций в программе.
----------------------------------
Индексная адресация.
Индексную адресацию применяют для доступа к последовательным ячейкам памяти (например, это могут быть элементы таблицы). Инструкции с Индексной адресацией используют индексные регистры "X" и "Y", и начальный адрес. Содержимое индексного регистра прибавляется к начальному адресу, и получается нужный нам адрес. Начальный адрес при этом может обозначать начало таблицы; значение регистра - номер ячейки. К сожалению, размер индексных регистров ограничен (в процессоре 6502 - всего 8 бит), и это ограничивает возможности Индексной адресации.
------------------------------------
До- и Постиндексная адресация.
Режимы индексных адресаций можно разделить на два типа. При Доиндексной адресации конечным адресом является сумма Начального адреса и смещения (которое содержится в индексном регистре).
При Постиндексной адресации смещение (содержимое индексного регистра) является АДРЕСОМ нужного смещения. В результате при Постиндексной адресации Конечный адрес равен сумме значения индексного регистра плюс значению ячейки памяти, номер которой содержится в индексном регистре. То есть, это комбинация Доиндексной и Непрямой адресации. Но мы ещё не разобрали Непрямую адресацию, так что исправим этот пробел.
-----------------------------
Непрямая адресация.
Как мы уже видели, всего две подпрограммы могут изменить множество информации, хранящейся в памяти. То есть, программам и подпрограммам может понадобиться доступ сразу к целым блокам информации. Причём размер блока может изменяться, и сам блок может находиться в разных местах памяти. Поэтому использовать Абсолютную адресацию для доступа к блокам информации было бы нерационально.
Выход из этой ситуации есть. Адрес начала блока располагают в предназначенных для этого ячейках памяти (нужно две ячейки, поскольку полный адрес занимает 2 байта). Соответственно, такая адресация использует 8 бит для Кода операции, и 16 бит для хранения нужного адреса. Непрямая адресация используется для создания Указателей. Потом эти указатели используются для доступа к нужным блокам информации.
----------------------------------
Комбинированные режимы адресации.
Режимы адресации можно комбинировать. Например, можно использовать несколько уровней Непрямой адресации. Один байт из начального адреса используется как адрес ячейки с нужным адресом, в той ячейке опять содержится адрес нужной, и так далее.
Можно скомбинировать Индексную адресацию с Непрямой. Например, байт N из блока информации может обозначать указатель на адрес начала блока.
Мы вкратце ознакомились с основными видами адресации. Большинство микропроцессоров поддерживают не все эти режимы. Но процессору 6502 доступно большинство из них.
---------------------------------------------
Режимы адресации процессора 6502.
-------------------------------
Неявная адресация (6502).
Неявная адресация используется однобайтовыми инструкциями, выполняющими действия со внутренними регистрами. Поскольку используются только внутренние регистры - такие инструкции выполняются в течение двух циклов. При обращении к памяти требуется уже три цикла.
Неявную адресацию используют инструкции: CLC, CLD, CLI, CLV, DEX, DEY, INX, INY, NOP, SEC, SED, SEI, TAX, TAY, TSX, TXA, TXS, TYA.
Обращение к памяти используют инструкции: BRK, PHA, PHP, PLA, PLP, RTI, RTS.
Эти инструкции уже были описаны в предыдущей главе. Они не используют дополнительной информации (то есть, инструкция записывается одним словом, без аргумента).
----------------------------------------
Непосредственная адресация (6502).
Процессор 6502 оснащён только 8-битными рабочими регистрами (регистр "PC" не является рабочим). Поэтому Непосредственная адресация ограничена 8-битными константами (которые содержатся в регистрах). Соответственно, все инструкции с Непосредственной адресацией занимают 2 байта. Первый байт содержит Код операции. Второй - константу (содержимое регистра) или используется для связи с регистром (чтобы выполнить с ним нужную арифметическую или логическую операцию).
Непосредственную адресацию используют инструкции: ADC, AND, CMP, CPX, CPY, EOR, LDA, LDX, LDY, ORA, SBC.
--------------------------------------------
Абсолютная адресация (6502).
Абсолютная адресация использует 3 байта. Первый байт - Код операции, остальные два - 16-битный адрес нужной ячейки памяти. Инструкции с такой адресацией (кроме инструкции Прыжка) выполняются в течение четырёх циклов.
Абсолютную адресацию могут использовать инструкции: ADC, AND, ASL, BIT, CMP, CPX, CPY, DEC, EOR, INC, JMP, JSR, LDA, LDX, LDY, LSR, ORA, ROL, ROR, SBC, STA, STX, STY.
---------------------------------
Адресация Нулевой страницы (6502).
Адресация нулевой страницы требует два байта: один содержит Код операции; ещё один - укороченный (8-битный) адрес.
Адресация нулевой страницы выполняется за 3 цикла. Поскольку она даёт преимущество в скорости - её нужно использовать везде, где это возможно. Также она экономит память. 256 ячеек нулевой страницы можно использовать в качестве "рабочих регистров": любая инструкция с ними будет выполняться за 3 цикла. Все инструкции (кроме JSR и JMP, им нужен 16-битный адрес), которые могут использовать Абсолютную адресацию - могут использовать и Адресацию нулевой страницы.
Адресацию нулевой страницы могут использовать инструкции: ADC, AND, ASL, BIT, CMP, CPX, CPY, DEC, EOR, INC, LDA, LDX, LDY, LSR, ORA, ROL, ROR, SBC, STA, STX, STY.
-------------------------------------
Относительная адресация (6502).
Относительная адресация использует 2 байта. Первый является инструкцией перехода, второй - содержит в себе смещение вместе со знаком. Её используют нструкции Ветвления. Инструкции Прыжка используют Абсолютную адресацию (и её комбинации - например, с Индексной или Непрямой). Все эти инструкции проверяют определённое условие. Если оно не выполняется - инструкция выполняется за 2 цикла. Если условие выполняется - происходит переход в нужное место программы. Для этого изменяется значение Программного Указателя, при этом тратится ещё один цикл. Если при этом переходит переход на другую страницу памяти, требуется ещё один цикл - итого получается 4 цикла.
Если инструкция ветвления используется для логического разветвления - программисту не нужно следить за тем, происходит переход на другую страницу или нет. Но если она используется для специального замедляющего цикла - этот лишний цикл нужно учитывать.
Многие ассемблеры сообщают программисту о переходе на другую страницу памяти, если при этом получается серьёзное замедление.
При подсчёте времени нужно считать, что переход будет происходить не всегда - то есть, инструкция может занять и два, и три цикла (или три и четыре, если происходит переход на другую страницу). Обычно берётся среднее значение.
Относительную адресацию используют только инструкции Ветвления. Это - 8 инструкций, которые проверяют состояние Флагов: BCC, BCS, BEQ, BMI, BNE, BPL, BVC, BVS.
--------------------------------------
Индексная адресация (6502).
Процессор 6502 не даёт всех возможностей этого режима адресации - у него есть серьёзное ограничение. Он оснащён двумя индексными регистрами, каждый из них по 8 бит. Содержимое индексного регистра прибавляется к начальному адресу, который задаётся в инструкции. Обычно Индексная адресация используется для доступа к блокам информации (например, к таблице). Для большего удобства сделаны инструкции, прибавляющие и отнимающие единицу к индексным регистрам. Также есть две инструкции для сравнения содержимого регистров с содержимым ячеек памяти.
Большинство таблиц занимает меньше 256 байт, поэтому ограничение в 8 бит обычно не является серьёзным препятствием.
Индексная адресация может использовать и полные (16-битные), и сокращённые (8-битные) адреса. Но тут есть ограничение. Регистр "X" может использоваться с обоими видами адресов, а регистр "Y" - только с полным (16-битным). Исключение - инструкции LDX и STX.
Инструкции с Индексной адресацией выполняются за 4 цикла. Если при этом происходит переход на другую страницу памяти - то 5 циклов.
При Индексной адресации регистры "X" и "Y" используются в качестве смещения. Индексную адресацию используют инструкции:
- с регистром "X": ADC, AND, ASL, CMP, DEC, EOR, INC, LDA, LDY, LSR, ORA, ROL, ROR, SBC, STA.
- с регистром "Y": ADC, AND, CMP, EOR, LDA, LDX, ORA, SBC, STA.
Индексную адресацию c укороченным адресом могут использовать инструкции: ADC, AND, ASL, CMP, DEC, EOR, INC, LDA, LDY, LSR, ORA, ROL, ROR, SBC, STA, STY.
------------------------------------------
Непрямая адресация (6502).
Процессор 6502 не полностью поддерживает Непрямую адресацию - размер адреса ограничен 8-ю битами. То есть, Непрямая адресация - это разновидность Адресации нулевой страницы. Из этого адреса берётся новый адрес (16-битный) - это и есть нужный адрес. Дальнейшее перенаправление невозможно. Все непрямые обращения (кроме инструкции JMP) должны быть проиндексированы.
На самом деле, очень немногие процессоры изначально поддерживают Непрямую адресацию (даже в таком ограниченном виде). Но при желании можно создать эту поддержку самостоятельно.
Есть два вида Непрямой адресации: Доиндексная непрямая и Постиндексная. Есть ещё исключение: инструкция JMP, которая использует Непрямую адресацию без индексов.
----------------------------------------------------
Индексная Непрямая адресация.
В этом режиме к начальному 8-битному адресу прибавляется значение регистра "X". В результате получается нужный 16-битный адрес. Это очень удобно для обращения к информации, помеченной указателями: в качестве номера нужного блока можно использовать регистр "X".
!!!!!!!!!!!!!!!!
Индексная Непрямая адресация выполняется за 6 циклов. Это больше, чем при любом виде прямой адресации. Но при правильном использовании, она может даже повысить скорость выполнения программы в целом.
Индексную Непрямую адресацию могут использовать инструкции: ADC, AND, CMP, EOR, LDA, ORA, SBC, STA.
----------------------------------------
Непрямая Индексная адресация.
Похожа на предыдущий режим. Но на этот раз индексирование происходит уже после перенаправления. То есть, сначала получается короткий (начальный) адрес, происходит перенаправление. Потом к получившемуся адресу прибавляется значение регистра "Y". В результате получается нужный адрес.
Такую адресацию очень удобно использовать для создания и использования таблиц. 16-битный адрес (который мы получаем из нулевой страницы) можно сделать началом таблицы. А значение регистра "Y" (смещение) - для перехода к нужному разделу таблицы.
Непрямую индексную адресацию могут использовать инструкции: ADC, AND, CMP, EOR, LDA, ORA, SBC, STA.
------------------------------------------
Исключение: инструкция JMP.
Инструкция JMP использует Непрямую Абсолютную адресацию. Это - единственная инструкция, которая может использовать такой режим.
-----------------------------------------------------
Использование режимов адресации процессора 6502.
---------------------------------
Короткая и длинная адресация.
Мы уже использовали инструкции Ветвления в предыдущих программах. Но остался вопрос. Что делать, если нам нужно перейти далеко от текущего места программы, и 8-битного смещения для этого не хватает? Есть простое решение - оно называется Длинным Ветвлением. Вызываем ветвление к инструкции Прыжка (например, JMP) - и уже она вызовет переход в любое место программы. Пример:
BCC +3 ; Переходим на 3 ячейки вперёд, если Флаг Переноса "C" очищен.
JMP FAR ; А если переход не произошёл - попадаем на инструкцию JMP, и происходит переход к ячейке FAR.
Эта программа вызывает переход к ячейке памяти FAR при установленном Флаге переноса. Теперь о том, как использовать сложные режимы адресации (например, Индексную и Непрямую).
--------------------------------------
Использование индексов для доступа к последовательным блокам информации.
Индексирование используется для доступа к последовательным блокам (например, к ячейкам таблицы). Таблица не должна быть больше 256 байт (поскольку для индексирования используется 8-битный регистр).
Мы уже писали программу для поиска символа "*". Теперь мы напишем программу, которая будет находить этот символ в таблице из 100 элементов. Таблица в нашем примере начинается с ячейки памяти BASE. Текст программы:
SEARCH LDX #0
NEXT LDA BASE, X
CMP #'*
BEQ STARFOUND
INX
CPX #100
BNE NEXT
NOTFOUND ...
...
STARFOUND ...
...
NEXT LDA BASE, X
CMP #'*
BEQ STARFOUND
INX
CPX #100
BNE NEXT
NOTFOUND ...
...
STARFOUND ...
...
Блок-схема этой программы изображена на Рисунке 5-5. Принцип действия программы прост. Регистр "X" содержит в себе номер текущей ячейки таблицы. Вторая инструкция программы: NEXT LDA BASE, X - использует абсолютную индексную адресацию. Она загружает в аккумулятор значение ячейки (BASE + X). Вначале регистр "X" равен нулю. То есть, первый элемент таблицы содержится в ячейке BASE. При следующем проходе регистр "X" уже равен единице, и происходит обращение ко второму элементу таблицы (по адресу BASE + 1), и т. д.
Третья инструкция (CMP #'*) сравнивает значение этой ячейки (загруженное в аккумулятор) с символом "*". Следующая инструкция получает результат этого сравнения, и если символ найден - вызывает переход к метке STARFOUND:
BEQ STARFOUND
Если этот переход не произошёл - выполняется следующая инструкция: INX. Эта инструкция увеличивает значение регистра на единицу. Все эти действия нужно повторить 100 раз. Чтобы проверить, было ли уже 100 проходов - выполняется инструкция: CPX #100. Эта инструкция проверяет, равен ли уже регистр "X" числу $100. Если нет - цикл продолжается. Для возврата в начало цикла (если "X" ещё не равен 100) используется инструкция:
BNE NEXT
Эта инструкция вызывает переход к метке NEXT. То есть, цикл продолжается, пока "X" не будет равен 100, или же, пока не будет найден символ "*". Дальше идёт инструкция под меткой NOTFOUND - она выполняется, если за 100 циклов символ так и не найдётся.
Сами действия (что должно произойти при найденном и при ненайденном символе "*") не описаны в этой программе. Программист может добавить их по своему желанию.
С этим разобрались. Теперь другое, более сложное применение индексов. Мы напишем программу, которая копирует один блок информации в другой. Подразуемевается, что наша таблица будет меньше 256 байт, поэтому мы опять используем индексный регистр "X". После мы изменим программу так, чтобы можно было выполнять действия и с большими таблицами.
------------------------------------------------
Программа для переноса небольших блоков информации (меньше 256 байт).
Число элементов блока назовём NUMBER. Это число будет меньше 256. Адрес начала блока назовём BASE. Место, в которое мы переносим информацию - назовём DESTINATION. Принцип действия программы: переносим байт информации, и сохраняем его в нужную ячейку (DESTINATION + значение регистра "X"). Текст программы:
LDX NUMBER
NEXT LDA BASE, X
STA DEST, X
DEX
BNE NEXT
NEXT LDA BASE, X
STA DEST, X
DEX
BNE NEXT
Инструкция LDX #NUMBER загружает в регистр "X" число байтов, которые нужно перенести (размер блока). Следующая инструкция загружает в аккумулятор очередной байт из блока. Третья инструкция сохраняет значение аккумулятора в нужной ячейке памяти.
Внимание! Эта программа будет правильно работать только тогда, когда блок находится СРАЗУ ПОСЛЕ ячейки BASE. Если же нет - нужно немного изменить программу.
Как только перенос произошёл, уменьшаем значение регистра "X" инструкцией DEX. Потом идёт проверка, стал ли регистр "X" равным нулю. Если да - программа завершается. Если нет - цикл опять повторяется.
Обратите внимание: если "X" равен нулю, цикл уже не повторится. Поэтому байт из ячейки BASE (BASE + 0) перенесён уже не будет - последним будет перенесён байт из ячейки BASE + 1.
-----------------------------------------------
Упражнение 5.1
Перепишите программу так, чтобы первый элемент блока содержался в ячейках BASE и DEST (а не после них).
-------------------------------------
Эта программа использует регистр "X" одновременно в качестве индекса и в качестве счётчика.
На первый взгляд, проще было бы сделать наоборот. Начать счёт с нуля, и увеличивать значение "X" при каждом проходе, пока он не достигнет нужного значения. Но тогда нужна будет ещё одна инструкция - для сравнения регистра "X" с этим числом. То есть, в цикле будет пять инструкция вместо четырёх. Например, если блок состоит из 100 байтов - будет выполнено 100 лишних инструкций. Поэтому в циклах индексные регистры обычно уменьшают с нужного значения до нуля.
--------------------------------------
Программа для переноса больших блоков информации (больше 256 байт).
Теперь мы напишем программу, которая может перенести сразу больше 256 элементов. Просто так использовать индексный регистр мы уже не можем - он не может содержать в себе число больше 256. Нам нужно 16 бит. Поэтому мы сохраним длину блока в памяти. Ячейка BLOCKS у нас будет содержать число частей по 256 байт, которые нужно перенести; ячейка REMAIN - число остальных байтов блока (если его размер не кратен 256). Адреса начального и конечного расположения обозначим как FROM и TO. Сначала предположим, что размер переносимого блока кратен 256 (то есть, его можно разделить на части по 256 байт, и REMAIN равен нулю). Тогда получится программа:
LDA #SOURCELO
STA FROM
LDA #SOURCEHI
STA FROM+1 ; Сохраняем в памяти адрес источника.
LDA #DESTLO
STA TO
LDA #DESTHI
STA TO+1 ; Сохраняем в памяти адрес приёмника.
LDX #BLOCKS ; Загружаем в регистр "X" число 256-байтовых частей.
LDY #0 ; Размер блока.
NEXT LDA (FROM), Y; Считываем элемент.
STA (TO), Y ; Сохраняем его в нужное место.
DEY ; Уменьшаем регистр "Y" на единицу.
BNE NEXT ; Проверяем, кончилась ли часть блока.
NEXBLK INC FROM+1 ; Следующая часть блока источника...
INC TO+1 ; Будет сохраняться в следующую часть приёмника.
DEX ; Число оставшихся частей уменьшается на единицу.
BMI DONE
BNE NEXT
LDY #REMAIN
BNE NEXT
Первые четыре инструкции сохраняют в память 16-битный адрес источника в ячейке памяти FROM. Следующие четыре инструкции точно так же сохраняют в память 16-битный адрес приёмника в ячейке памяти TO. Поскольку наша таблица больше 256 байт - мы будем использовать оба индексных регистра. Следующая инструкция загружает в регистр "X" число 256-байтовых частей блока. Потом значение регистра "Y" обнуляется - для отсчёта 256 байт части. Мы будем использовать Индексную Непрямую адресацию. Помните, что при Индексной Непрямой адресации адрес нужной ячейки получается из суммы (значение ячейки нулевой страницы + значение индексного регистра).
NEXT LDA (FROM), Y
Эта инструкция загрузит в аккумулятор число из ячейки с получившимся адресом. Регистр "Y" у нас равен нулю, поэтому при первом проходе в аккумулятор загрузится число из ячейки SOURCE. В отличие от прошлого примера - первый элемент блока уже содержится в ячейке SOURCE (а не после неё).
Точно так же мы обращаемся к ячейке приёмника, и сохраняем в неё значение аккумулятора:
STA (TO), Y
Как и в предыдущем примере, мы уменьшаем регистр "Y" на единицу. Потом проверяем его значение, не стало ли оно нулевым. В результате получится 256 проходов цикла.
Примечание: в этой программе мы использовали специальный приём. Внимательный читатель уже заметил, что регистр "Y" уменьшается (хотя он уже был равен нулю). Первым будет перенесён байт из ячейки (SOURCE + 0), а следующим - байт из ячейки (SOURCE + 255). Так получается потому, что при вычитании единицы из нуля (байта со значением 00000000) - получается число 255 (11111111). Когда регистр "Y" опять достигнет значения 0 - цикл завершится, и будет выполнена следующая инструкция - под меткой NEXTBLK. Таким образом, цикл выполнится 256 раз, и вся 256-байтовая часть блока будет перенесена.
Одну часть перенесли, надо переносить остальные. Для этого мы прибавим единицу к числам FROM+1 и TO+1. Отсчёт байтов будет идти уже с нового места (начала следующей 256-байтовой части):
NEXTBLK INC FROM+1
INC TO+1
INC TO+1
Поскольку мы уже перенесли прошлую часть - нужно уменьшить на единицу регистр "X":
DEX
Если больше частей не осталось - пора завершать цикл переноса. Для этого используем инструкцию ветвления:
BMI DONE
Если части ещё остались - есть два варианта. Если регистр "X" не равен нулю - переходим к метке NEXT:
BNE NEXT
Если же он равен нулю - то нужно переносить байты, число которых содержится в ячейке REMAIN. Это - последнаяя часть нашей программы:
LDY #REMAIN
Дальше опять происходит ветвление к метке NEXT:
BNE NEXT
При последнем цикле происходит переход к метке NEXT. После его выполнения регистр "X" уменьшится ещё на единицу. Поскольку он уже был равен нулю - сработает условие BMI DONE, и перенос будет завершён.
--------------------------------------------------
Сложение двух блоков.
Эта программа даёт простой пример использования индексного регистра для сложения двух блоков (до 256 байтов в каждом). Для обращения к блокам используется Непрямая Индексная адресация (будем считать, что абсолютный адрес неизвествен). Текст программы:
BLKADD LDY #NBR-1 ; Загружаем в счётчик число байтов.
NEXT CLC
LDA PTR1, Y ; Считываем очередной байт.
ADC PTR2, Y ; Выполняем сложение.
STA PTR3, Y ; Сохраняем результат.
DEY ; Уменьшаем счётчик на единицу.
BPL NEXT ; Повторяем цикл, если ещё не всё сложили.
NEXT CLC
LDA PTR1, Y ; Считываем очередной байт.
ADC PTR2, Y ; Выполняем сложение.
STA PTR3, Y ; Сохраняем результат.
DEY ; Уменьшаем счётчик на единицу.
BPL NEXT ; Повторяем цикл, если ещё не всё сложили.
Регистр "Y" используется в качестве счётчика. Изначально он равен числу байтов блока - 1. Ячейка PTR1 - это начало первого блока (адрес его первого элемента); PTR2 - начало второго блока; PTR3 - начало блока с результатами.
Программа уже самодокументирована. Последний элемент Блока 1 загружается в аккумулятор. К аккумулятору прибавляется последний элемент Блока 2. Результат сохраняется в последний элемент Блока 3. Потом складываются предпоследние элементы, и так далее.
--------------------------------------------------
Упражнения на использование Непрямой Индексной адресации.
По условию, мы не знаем абсолютного адреса ячеек PTR1, PTR2 и PTR3. Но мы знаем, что их адреса сохранены в ячейках нулевой страницы - соответственно LOC1, LOC2 и LOC3. Это - обычный механизм для передачи информации между подпрограммами. Для сравнения, напишем программу:
BLKADD LDY #NBR-1
CLC
LDA (LOC1), Y
ADC (LOC2), Y
STA (LOC3), Y
DEY
BPL NEXT
CLC
LDA (LOC1), Y
ADC (LOC2), Y
STA (LOC3), Y
DEY
BPL NEXT
Сходство с предыдущей программой - очевидно. Мы можем не знать заранее, какой абсолютный адрес будет у этих трёх блоков. Но мы можем задать его в ячейках памяти. Обе эти программы используют одинаковое число инструкций. Но какая же из них работает быстрее?
----------------------------------------
Упражнение 5.2
Вычислите число байт и время выполнения (в циклах) для каждой программы. Количество байт и время выполнения для каждой инструкции Вы можете найти в Дополнении.
----------------------------------------
Заключение.
Мы полностью описали все режимы адресации. Процессор 6502 поддерживает большинство механизмов адресации. Теперь выполните несколько упражнений с использованием этих режимов.
----------------------------------------
Упражнение 5.3
Напишите программу, которая складывает 10 байтов таблицы (начало таблицы - ячейка BASE). Результат должен получиться 16-битным.
Упражнение 5.4
Можно ли сделать то же самое без использования Индексной адресации?
Упражнение 5.5
Сделайте, чтоб элементы 10-байтной таблицы шли в обратном порядке. Результат сохраните в таблице с началом в ячейке REVER.
Упражнение 5.6
Найдите самый большой элемент таблицы. Результат сохраните в ячейке LARGE.
Упражнение 5.7
Сложите элементы трёх таблиц (они начинаются соответственно с ячеек BASE1, BASE2, BASE3). Длина этих таблиц хранится в ячейке памяти LENGTH.
-----------------------------------------------------------------------
Глава 6. Технологии ввода/вывода.
-----------------------------------------------------------------------
Введение.
Мы уже разобрались, как обмениваться информацией между памятью и внутренними регистрами процессора. И как изменять эту информацию. Эта глава посвящена тому, как обмениваться информацией с внешним миром. То есть, вводу/выводу.
Ввод - это передача информации компьютеру с внешних источников (клавиатуры, дисков и т. д.). Вывод - это выдача информации из микропроцессора или памяти на внешние устройства (например, принтер, диск или ЭЛТ-дисплей). Глава условно поделена на две части. Первая - о том, какие операции ввода/вывода существуют для стандартных устройств. Вторая - о том, как управлять устройствами ввода/вывода. В том числе, прерываниям и опросу устройств.
----------------------------------------
Ввод/вывод.
Этот раздел рассказывает о том, как генерируются простые сигналы. После будут описаны приёмы для задания времени. Тогда мы будем готовы к изучению сложных видов ввода/вывода - таких, как последовательные и параллельные передачи.
----------------------------------------
Создание сигнала.
Самый простой пример: устройство вывода включается или отключается. Чтобы изменить состояние устройства вывода - программист должен установить его с единицы на ноль, или наоборот. Допустим, внешнее устройство при подключении записывает число "0" в специальную ячейку памяти "OUT1". При включении устройства в неё записывается значение "1". Тогда для включения этого устройства подойдёт программа:
TURNON LDA #%00000001
STA OUT1
STA OUT1
Поскольку это самый элементарный пример - изменяется только один бит. Обычно происходит не так. Остальные биты могут обозначать состояния других устройств. Теперь дополним программу. При включении устройства мы будем изменять ТОЛЬКО тот бит, который отвечает за него. Тогда получится программа:
TURNON LDA OUT1 ; Считываем содержимое ячейки OUT1 в аккумулятор.
ORA #%00000001; Делаем нулевой бит (самый правый) равным единице.
STA OUT1 ; Сохраняем результат обратно в ячейку OUT1.
ORA #%00000001; Делаем нулевой бит (самый правый) равным единице.
STA OUT1 ; Сохраняем результат обратно в ячейку OUT1.
Эта программа считывает содержимое ячейки OUT1. Потом выполняется логическая операция "ИЛИ", после неё нужные нам биты станут равными единице, а значение остальных не изменится. Подробнее про логические операции - см. Главу 4.
----------------------------------------------
Импульсы.
!!!!!!!
Создание импульсов происходит на уровень выше. Нужный бит может установиться в состояние "включено", потом - в состояние "выключено". Возникает вопрос: как сделать так, чтобы импульс создавался через нужный промежуток времени? Сначала разберёмся, как рассчитать нужную паузу.
-------------------------------------------
Создание и измерение паузы.
Пауза может быть вызвана программными и аппаратными способами. Сначала разберём программный способ, потом - создание паузы с помощью Программируемого Таймера Интервалов (Programmable Interval Timer, PIT).
Программная пауза вызывается с помощью счётчика. Ему задаётся нужное значение, после чего запускается цикл, и при каждом проходе он уменьшается на единицу. Цикл повторяется до тех пор, пока счётчик не станет равным нулю. Например, создадим паузу длиной 37 микросекунд:
DELAY LDY #07 ; Счётчиком будет регистр "Y".
NEXT DEY ; Уменьшаем на единицу.
BNE NEXT; Если регистр "Y" ещё не равен нулю - повторяем цикл.
NEXT DEY ; Уменьшаем на единицу.
BNE NEXT; Если регистр "Y" ещё не равен нулю - повторяем цикл.
Первая инструкция загружает в регистр "Y" значение 7. Следующая - уменьшает его на единицу, и последняя - проверяет, стал ли он равным нулю. Когда цикл завершится - программа продолжит выполняться дальше.
Теперь рассчитаем паузу, которую создаст эта программа. Время выполнения каждой инструкции можно посмотреть в Дополнении в конце книги.
Инструкция LDY в режиме Непосредственной адресации выполняется за 2 микросекунды. Инструкция DEY - за 2 микросекунды. Инструкция BNE - за 3 микросекунды. Но у инструкции BNE есть две особенности. Во-первых, если ветвления не происходит (условие не выполнено) - она выполнится всего за 2 микросекунды. Во-вторых, если происходит переход на другую страницу памяти - она займёт 4 микросекунды.
Будем считать, что перехода на другую страницу памяти нет. Тогда программа выполнится за время:
2 + (2 + 3) * 6 + (2 + 2) = 36 микросекунд.
Максимальная точность, до которой мы можем установить паузу таким способом - 2 микросекунды. Минимальная пауза - тоже 2 микросекунды.
-------------------------------------------
Упражнение 6.1
Какую максимальную паузу можно создать, используя эти 3 инструкции? Можно ли изменить программу так, чтобы она дала паузу в 1 микросекунду?
Упражнение 6.2
Измените программу так, чтоб получить паузу 100 микросекунд.
-----------------------------------------
Паузу можно сделать и дольше - для этого достаточно поместить в цикл другие инструкции, которые не влияют на работу программы, и при этом занимают время. Существует даже специальная инструкция NOP - она не выполняет никаких действий, и выполняется за 2 микросекунды.
-----------------------------------------
Продолжительные паузы.
Для создания большой паузы можно использовать увеличенный счётчик. Для него можно использовать сразу два внутренних регистра или (ещё лучше) ячейки памяти. Допустим, есть два счётчика (под счётчик выделено 16 бит). В оба счётчика загружаются числа (в зависимости от того, насколько долгой должна быть пауза). Первый счётчик уменьшается до нуля по единице. После этого второй счётчик уменьшается на единицу, первый - становится равным 255. Повторяем до тех пор, пока оба счётчика не станут равными нулю. Скоро мы напишем программу, которая использует такой механизм.
Для счётчика можно использовать и три байта, и больше. Почти как в устройстве автомобиля, которое замеряет километраж. Как только значение его ячейки превышает 9 - она обнуляется, а соседняя - увеличивается на единицу.
На самом деле, паузы не всегда бывают лучшим решением. Можно создать паузу длиной в сотни милисекунд, и даже секунд. Но в это время процессор не может выполнять и другие задачи, в том числе и реагировать на внешние сигналы. Лучше использовать аппаратные паузы. Кроме того, при использовании прерываний цикл может остановиться - и пауза получится не такая, как планировалась.
----------------------------------------
Упражнение 6.3
Напишите программу, которая вызовет паузу длиной 100 милисекунд.
----------------------------------------
Аппаратные паузы.
Аппаратные паузы вызываются Программируемым Таймером Интервалов (или просто Таймером). В регистр этого таймера загружается число. Потом таймер вычитает из этого таймера единицу раз в определённый промежуток времени. Обычно этот промежуток времени может задавать программист. Когда регистр становится равным нулю - микропроцессору посылается сигнал прерывания. Использование прерываний будет описано далее в этой же главе.
Существует другой режим работы таймера. Изначально его регистр устанавливается равным нулю. Дальше он подсчитывает продолжительность сигнала или количество полученных импульсов. У таймера может быть несколько регистров, и у них могут быть разные режимы работы. В следующей главе рассказано про таймеры, которыми оснащён 6522.
--------------------------------------------
Приём импульсов.
Задача приёма импульсов - ещё сложенее, чем их создание. Исходящий импульс создаётся под управлением программы. Входящий импульс поступает независимо от программы. Чтобы его перехватить - используются два способа: Прерывания и Опросы.
Сначала про технологию Опроса. При использовании этой технологии программа постоянно проверяет значение входного регистра. Допустим, по умолчанию он равен нулю. Если поступает импульс - значение этого регистра становится равным единице. Пример такой программы:
POLL LDA #$01
BIT INPUT
BEQ AGAIN
ON ...
...
BIT INPUT
BEQ AGAIN
ON ...
...
Теперь предположим, что по умолчанию регистр равен единице, а при поступлении импульса становится равным нулю. Тогда текст программы будет:
POLL LDA #$01
BIT INPUT
BNE NEXT
START ...
BIT INPUT
BNE NEXT
START ...
-----------------------------------
Измерение продолжительности.
Продолжительность импульса можно измерить и программным, и аппаратным способом. При измерении программным способом счётчик увеличивается на единицу через определённый промежуток времени. Как только импульс поступает - запускается цикл. После его исчезновения счётчик содержит в себе продолжительность импульса. Текст программы для такого измерения:
DURTN LDX #0 ; Обнуляем счётчик.
LDA #$01
AGAIN BIT INPUT
BEQ AGAIN
LONGER INX
BIT INPUT
BNE LONGER
LDA #$01
AGAIN BIT INPUT
BEQ AGAIN
LONGER INX
BIT INPUT
BNE LONGER
В этом примере мы допустили, что импульс пройдёт быстро, и регистр "X" не успеет переполниться (превысить значение 255). Если он превысит это значение - то счёт опять пойдёт с нуля, и результат программы будет неправильным.
Поскольку мы знаем, как передавать и получать импульсы - давайте напишем программу для получения и передачи информации. Но сначала нужно разобраться с тем, какие бывают способы передачи информации. Есть два способа: последовательный и параллельный.
----------------------------------------
Параллельная передача информации.
Предположим, нужно передать 8 бит информации по адресу INPUT. Микропроцессор должен считывать информацию из этой ячейки каждый раз, когда там оказывается информация. Наличие информации показывает 7-ой бит ячейки памяти STATUS. Мы напишем программу, которая автоматически сохраняет каждый байт этой информации в память. Чтобы упростить задачу - предположим, что число байтов переданной информации известно заранее, оно хранится в ячейке COUNT. Если эта информация неизвестная - используют специальный "Символ завершения". Например, это может быть символ "*". Но пока мы так делать не будем.
!!!!!!!РИСУНОК
Блок-схема этой программы показана на Рисунке 6-5. Программа проверяет сигнальный бит, который показывает, пришла ли информация. Если он станет равным единице - значит, информация пришла. Мы загружаем её в регистр, и сохраняем в нужную ячейку памяти. Число непереданных байтов информации уменьшается на единицу, а сигнальный бит обнуляется. Если число непереданных байтов равно нулю - то вся информация уже передалась, и программа завершается.
PARAL LDX COUNT ; Регистр "X" будет счётчиком.
WATCH LDA STATUS ; Если поступила информация - 7-ой бит будет равен единице.
BPL WATCH ; Информация пришла?
LDA INPUT ; Если да - то считываем её.
PHA ; И сохраняем в стек.
DEX
BNE WATCH
WATCH LDA STATUS ; Если поступила информация - 7-ой бит будет равен единице.
BPL WATCH ; Информация пришла?
LDA INPUT ; Если да - то считываем её.
PHA ; И сохраняем в стек.
DEX
BNE WATCH
Первые две инструкции считывают сигнальную информацию. Потом выполняется цикл, которые повторяется, пока 7-ой бит не изменится. Если он изменится - изменится Флаг Знака, и программа перехватит эту ситуацию.
WATCH LDA STATUS
BPL WATCH
BPL WATCH
Если проверка BPL не прошла - значит, информация поступила, и мы можем её считать:
LDA INPUT
Теперь информация загружена в аккумулятор, и её нужно сохранить в нужное место. Допустим, информации мало, и она вся может поместиться в стек. Поэтому для сохранения используем инструкцию:
PHA
Если стек уже заполнен, или информации много (и она в принципе не может туда поместиться) - то мы не можем так сделать. Тогда её нужно сохранять в выделенную область памяти. Например, для этого можно использовать инструкции с индексами. Но если информации мало - выгоднее использовать стек. Поскольку тогда не будет лишней инструкции для уменьшения или увеличения индексного регистра.
Байт памяти считан и сохранён. Мы уменьшаем регистр "X" на единицу (в нём содержится число байтов, которые ещё надо передать). Если он равен нулю (уже всё передали), то программа завершается. Если нет - цикл повторяется:
DEX
BNE WATCH
BNE WATCH
Эта программа из 6 инструкций по-другому называется программой Сравнительного Теста. Она нужна для того, чтобы процессор мог распознать нужную ситуацию. Теперь давайте рассчитаем максимальную скорость передачи для такой программы. Будем считать, что COUNT - это ячейка нулевой страницы. Время выполнения каждой инструкции, как всегда, можно найти в конце книги.
ВРЕМЯ ВЫПОЛНЕНИЯ:
LDX COUNT 3
WATCH LDA STATUS 4
BPL WATCH 2/3 (Условие не выполнено/Условие выполнено)
LDA INPUT 4
PHA 3
DEX 2
BNE WATCH 2/3 (Условие не выполнено/Условие выполнено)
WATCH LDA STATUS 4
BPL WATCH 2/3 (Условие не выполнено/Условие выполнено)
LDA INPUT 4
PHA 3
DEX 2
BNE WATCH 2/3 (Условие не выполнено/Условие выполнено)
Быстрее всего программа выполнится, если при каждом проходе цикла новая информация уже будет доступна. То есть, если условие BPL каждый раз не будет выплняться. В этом случае вся информация передастся за время:
3 + (4 + 2 + 4 + 3 + 2 + 3) * COUNT
Если не считать 3 микросекунды на инициализацию счётчика - программа будет передавать 1 байт информации за 18 микросекунд.
То есть скорость передачи информации будет равна:
1 / (18 * (1 / 1 000 000)) = 55 килобайт в секунду.
----------------------------------------------
Упражнение 6.4
Теперь предположим, что может быть передано больше 256 байтов информации. Перепишите программу так, чтобы она в этом случае правильно сработала. Рассчитайте её максимальную скорость передачи.
----------------------------------------------
Последовательная передача информации.
При последовательной передаче биты информации идут последовательным потоком. Информация может поступать и блоками заданной длины, и блоками случайного размера. Для перехвата такой информации проверяется строка ввода, которая будет считаться нулевой строкой. Когда в эту строку попадает бит информации - мы загружаем его регистр. Как только будет получено 8 битов памяти - они сохраняются в память. Для упрощения опять предположим, что мы знаем, сколько информации будет передано.
SERIAL LDA #$00
STA WORD
LOOP LDA INPUT; 7-ой бит - сигнальный, он зависит от того, пришла ли информация.
BPL LOOP ; Бит получен?
LSR A ; Если да - то выполняем сдвиг, и он попадёт в Флаг Переноса.
ROL WORD ; Сохраняем бит в память.
BCC LOOP ; Если Флаг Переноса равен нулю - то переходим к началу цикла.
LDA WORD ;
PHA
LDA #$01 ; Очищаем бит счётчика.
STA WORD
DEC COUNT; Уменьшаем число непереданной информации на единицу.
BNE LOOP ; Если ещё не вся информация передалась - повторяем цикл.
STA WORD
LOOP LDA INPUT; 7-ой бит - сигнальный, он зависит от того, пришла ли информация.
BPL LOOP ; Бит получен?
LSR A ; Если да - то выполняем сдвиг, и он попадёт в Флаг Переноса.
ROL WORD ; Сохраняем бит в память.
BCC LOOP ; Если Флаг Переноса равен нулю - то переходим к началу цикла.
LDA WORD ;
PHA
LDA #$01 ; Очищаем бит счётчика.
STA WORD
DEC COUNT; Уменьшаем число непереданной информации на единицу.
BNE LOOP ; Если ещё не вся информация передалась - повторяем цикл.