[<>] ~~TOC wide~~ {{anchor:n2}} ====== 2. Программирование на языке ассемблера ====== В этой главе мы рассмотрим основные команды языка ассемблера и директивы самого ассемблера, приведем тексты и листинги программ. Полностью система команд [[zilog_z80:zilog_z80|Z80]] приведена в [[msx:assembler_programming_guide-fakhrutdinov_bocharov:11|Приложении 1]]. {{anchor:n21}} ====== 2.1. Директивы ассемблера ====== С помощью директив (псевдокоманд) программист даёт указания ассемблеру по трансляции программы на языке ассемблера, управляет процессом трансляции. В отличие от команд языка ассемблера директивы, как правило, не транслируются в машинные команды. У разных ассемблеров могут быть отличающиеся наборы директив. Мы будем в основном придерживаться набора директив ассемблеров системы [[msx:duad:duad|]] и [[msx:macro-80_assembler:macro-80_assembler|M80]]. Во первой главе мы уже сталкивались с некоторыми директивами. Рассмотрим их немного подробнее. * ''ORG'' — определение начального адреса трансляции (или загрузочного адреса). Ассемблер настраивает программу с указанного адреса. В основном это касается команд перехода, использующих метки, вместо которых нужно будет подставить конкретные адреса, и меток данных. Пример: ORG 9000h [[msx:duad:duad|]]–ассемблер допускает только одну команду ''ORG''. Другие ассемблеры могут допускать и больше (как установку счетчика размещения). Нужно иметь в виду, что в разных режимах трансляции директива ''ORG'' может иметь различные смыслы. Подробнее об этом смотрите в [[msx:assembler_programming_guide-fakhrutdinov_bocharov:03|Главе 3]]. * ''END'' — указание на конец текста транслируемой программы. Многие ассемблеры кроме этого воспринимают адрес или метку в поле операндов директивы ''END'' (если она есть) как стартовый адрес программы. * ''INCLUDE'' — указание включить в текст программы текст, находящийся в указанном в директиве файле. Включение производится в то место, где стоит ''INCLUDE''. Система [[msx:duad:duad|]] не разрешает, чтобы включаемый таким образом файл в свою очередь тоже имел директиву ''INCLUDE''. Пример: INCLUDE a:stdbeg.ASM * ''MACLIB'' — указание включить в текст программы макробиблиотеку, находящуюся в указанном в директиве файле. Включение производится в то место, где стоит ''MACLIB''. Пример: MACLIB a:macros.MAC * ''EQU'' — приписывание имени константе. С помощью этой директивы константе или константному выражению приписывается имя, которое затем можно использовать везде, где использовалась константа. Если использовано выражение, ассемблер вычисляет его значение (значения всех имён должны быть уже вычислены) и подставляет это значение в команду. В выражении могут использоваться операции +, -, *, /, а также скобки. Имена обычно приписываются тем константам, значения которых могут меняться в ходе разработки программы или в ходе её эксплуатации. Пример: scrnum EQU 2 nospr EQU 16 dma EQU fcb+len*3 argum EQU 0A001h * ''.REQUEST'' — просмотр неопределённых внешних меток. Сборщик просматривает файлы типа ''.REL'', ищет глобальные имена. Пример: .REQUEST subr * ''.COMMENT'' или ''%COMMENT'' — комментарии к программе. Первый непустой символ после слова COMMENT — ограничитель. Текст комментария длится до нового появления ограничителя. * ''NAME ('имя-модуля')'' — даёт имя модулю. * ''.Z80'' — используется мнемоника Z80 * ''.8080'' — используется мнемоника Intel 8080. Имеются также директивы для резервирования и заполнения памяти значениями, управления выдачей листинга, для условной генерации и т.д. Они будут рассмотрены ниже. {{anchor:n22}} ===== 2.2. Системы счисления ===== Кроме привычной всем нам десятичной системы счисления существуют также двоичная, восьмеричная и шестнадцатеричная системы счисления. В десятичной системе мы имеем 10 знаков (цифры от 0 до 9), в двоичной системе их всего два (0 и 1), зато в шестнадцатеричной — 16 (цифры от 0 до 9 и латинские буквы A–F). Ниже приведена таблица соответствия между первыми 16 числами разных систем счисления: ^ дес. ^ двоич. ^ восьм. ^ шест. ^ | 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 | 10 | 8 | | 9 | 1001 | 11 | 9 | | 10 | 1010 | 12 | A | | 11 | 1011 | 13 | B | | 12 | 1100 | 14 | C | | 13 | 1101 | 15 | D | | 14 | 1110 | 16 | E | | 15 | 1111 | 17 | F | Как Вы могли заметить, для того, чтобы умножить число в двоичной системе на 2, необходимо просто сдвинуть биты (разряды числа, 0 или 1) на одну позицию влево. Аналогично происходит деление; разумеется, сдвиг происходит вправо. Это свойство положено в конструкцию ЭВМ. В языке ассемблера есть команды сдвига влево/вправо, что даёт возможность достаточно просто умножать/делить на любую целую степень двойки. Необходимо заметить, что при написании программ на языке ассемблера пользуются в основном двоичной, шестнадцатеричной и реже — десятичной системами счисления. Легко освоить и перевод из двоичной системы в шестнадцатеричную: необходимо разбить двоичное число на группы по 4 бита и воспользоваться вышеприведенной таблицей. {{anchor:n23}} ===== 2.3. Выделение памяти и запись значений ===== Несколько директив ассемблера предназначены для выделения памяти для переменных программы, а также для первоначального заполнения выделенной памяти необходимыми значениями. Если шестнадцатеричная константа начинается с буквы, то перед ней обязательно нужно ставить цифру 0. Например, 0B31Ch. * ''DEFS'' — резервирование указанного количества байт. Допустимо сокращение ''DS''. Если имеется второе число через запятую, то это означает, что выделенную память нужно заполнить указанным значением (в противном случае память либо обнуляется, либо заполнена "мусором"). Например, storage: DEFS 16 block: DS 255 fillvrm: DS 56,0 units: DS 24,0Fh * ''DEFB'' — запись указанных значений в память побайтно с одновременным резервированием памяти. Допустимо сокращение DB. Например, x: DEFB 25 st: DB 0F2h data: DEFB 1Fh,93h,0A0h,0,56 year: DB '(C) ДВГУ. 1989',0 * ''DEFW'' — запись указанных значений в память в двухбайтном формате с одновременным резервированием памяти. При записи младший байт значения будет поставлен первым (интеловский способ хранения значений). Допустимо сокращение ''DW''. Например, word: DW 13A7h integ: DEFW 1F39h,0Ah,8000h,125 * ''DEFM'' — запись строкового значения в память. Например: text: DEFM 'I am very glad!' * ''DC'' — запись строкового значения. Первый бит каждого байта обнуляется, но последний байт строки запоминается с установленным 7-м битом. Метки перед всеми перечисленными директивами могут и отсутствовать. Рекомендуем Вам внимательно изучить, как были оттранслированы ассемблером директивы из приведённых ниже примеров. Z80-Assembler Page: 1 ORG 9000h 9000 storage: DEFS 16 9010 block: DS 255 910F 19 x: DEFB 25 9110 F2 st: DB 0F2h 9111 1F93A000 data: DEFB 1Fh,93h,0A0h,0,56 9115 38 9116 28432920 year: DB '(C) ДВГУ. 1989',0 911A E4F7E7F5 911E 2E203139 9122 383900 9125 A713 word: DW 13A7h 9127 391F0A00 integ: DEFW 1F39h,0Ah,8000h,125 912B 00807D00 912F 4920616D text: DEFM 'I am very glad!' 9133 20766572 9137 7920676C 913B 616421 END В ассемблере [[msx:macro-80_assembler:macro-80_assembler|M80]] имеется директива ''.RADIX,'' которая позволяет устанавливать любое основание системы счисления с 2 до 16 для констант, действующее по умолчанию. Явно основание указывается буквами: * b — двоичное, * d — десятичное, * o — восьмеричное, * h — шестнадцатеричное. Изучите пример трансляции ассемблером [[msx:macro-80_assembler:macro-80_assembler|M80]]: MSX.M-80 1.00 01-Apr-85 PAGE 1 ORG 9000h ; ---- 9000' 38 DB 56d 9001' 07 DB 111b 9002' 3F DB 77o 0002 .RADIX 2 9003' 9C byte: DB 10011100 9004' 0A DB 1010 000C .RADIX 12 9005' 79 23 nmb: DB 0A1,2B 0010 .RADIX 16 9007' FCAC 01DE addr: DW 0FCAC,01DE ; ---- END No Fatal error(s) Обратите внимание на то, что в листинге [[msx:macro-80_assembler:macro-80_assembler|M80]] в отличие от листинга ассемблера [[msx:duad:duad|]] младший и старший байты значения не переставлены. Локальные метки обычно могут иметь длину до шестнадцати символов, а глобальные и внешние (см. ниже) - до шести. Метка может содержать символы A…z, 0…9, $, точку, ?, @, подчерк. В командах языка ассемблера можно использовать не только просто метки, но и выражения над метками и числами. Допускаются круглые скобки и следующие операции: |NOT e |отрицание, инверсия| |e1 AND e2|конъюнкция| |e1 OR e2|дизъюнкция| |e1 XOR e2|исключающее или| |e1 SHL e2|сдвиг первого операнда влево на значение e2| |e1 SHR e2|сдвиг первого операнда вправо на значение e2| |e1 + e2|сложение е1 с е2| |e1 - e2|вычитание е2 из е1| |e1 / e2|деление е1 на е2| |e1 * e2|умножение е1 на е2| |e1 MOD e2|остаток от деления е1 на е2| |HIGH e|восемь старших битов двухбайтного слова| |LOW e|восемь младших битов двухбайтного слова| |NULL e|истина, если аргумент равен нулю| Допускаются также сравнения: |EQ|%% = %%| |NE|%% ≠ %%| |LT|%% < %%| |LE|%% ≤ %%| |GT|%% > %%| |GE|%% ≥ %%| Текущий адрес обозначается знаком "$" или "Ъ". Например, если текущий счётчик размещения равен 901Ah, то после выполнения директивы EndLoad EQU $-7 значением имени EndLoad станет 9013h. Если значением имени TrapLoc является FCCAh, то команда: LD (HL),Low(TrapLoc) будет эквивалентна команде: LD (HL),0CAh . Еще один пример — использование ассемблером логических операций. Пара — директива и команда: P.Stop EQU 3 LD A,not(1 SHL P.Stop) эквивалентны команде: LD A,11110111b . {{anchor:n24}} ===== 2.4. Команды загрузки и обмена ===== С помощью команд загрузки производится обмен данными между регистрами, памятью и регистром, регистром и памятью. Команд пересылки непосредственно из одной ячейки памяти в другую нет, но это можно сделать через регистры. Кроме этого, команды загрузки позволяют записать некоторое число в регистр или регистровую пару. Команды загрузки делятся на две большие группы — команды 8-разрядной загрузки и команды 16-разрядной загрузки. Посмотрите примеры команд загрузки одного байта, оттранслированные в системе [[msx:duad:duad|]]. Z80-Assembler Page: 1 ORG 0A000h A000 0603 LD b,3 ; 3 => регистр b A002 2632 LD h,32h ; 32h => регистр h A004 3AAFFC LD a,(0FCAFh) ; содержимое FCAFh => ; регистр a A007 3A17A0 LD a,(data) ; содержимое data => ; регистр a A00A 4B LD c,e ; регистр e => рег. c A00B 7E LD a,(HL) ; содерж. ячейки по ; адресу в HL => ; регистр a A00C 02 LD (BC),a ; регистр a => ; по адресу в BC A00D 32ACFC LD (0FCACh),a ; регистр a => в FCACh A010 3217A0 LD (data),a ; регистр a => в data A013 3219A0 LD (data+2),a ; рег. a => в data+2 A016 C9 RET A017 46 data: DEFB 46h A018 DEFS 2,0 END При загрузке в регистровую пару, например ''BC'', двухбайтного значения с адресом adr, байт, хранящийся по адресу adr, загружается в регистр C, а байт по адресу adr+1 — в регистр B. Аналогично — для регистров ''DE'' и ''HL''. Запись из регистровой пары в память снова переставляет байты. Поскольку в памяти байты переставлены, это означает, что в регистровой паре — обычная запись. Изучите примеры трансляции команд 16-разрядной загрузки, приведённые ниже. Z80-Assembler Page: 1 ORG 0A000h A000 111F20 LD DE,201Fh ; 20h => в D ; 1Fh => в E A003 2AAFFC LD HL,(0FCAFh) ; байт адр. FCAFh => L ; байт адр. FCB0h => H A006 2117A0 LD HL,data ; A0h => H ; 17h => L A009 2A17A0 LD HL,(data) ; 46h (data) => L ; A7h (data+1) => H A00C ED4362DE LD (0DE62h),BC ; содерж. B => в DE63h ; содерж. C => в DE62h A010 2217A0 LD (data),HL ; содерж. H => в data+1 ; содерж. L => в data A013 2219A0 LD (data+2),HL ; содерж. H => в data+3 ; содерж. L => в data+2 A016 C9 RET A017 46A7 data: DEFW 0A746h A019 DS 2,0 END Обратите особое внимание на команду ''LD HL,data'' и её отличие от следующей за ней команды. Приведём три листинга программ, осуществляющих перестановку однобайтных и двухбайтных значений. MSX.M-80 1.00 01-Apr-85 PAGE 1 ; === перестановка двух байтов - first и second .Z80 8000 first EQU 8000h 8010 second EQU 8010h 0000' 3A 8000 LD A,(first) 0003' 47 LD B,A 0004' 3A 8010 LD A,(second) 0007' 32 8000 LD (first),A 000A' 78 LD A,B 000B' 32 8010 LD (second),A 000E' C9 RET END Для перестановки двух смежных байтов можно использовать команды загрузки двух байтов. MSX.M-80 1.00 01-Apr-85 PAGE 1 ; === перестановка двух смежных байтов - first и first+1 .Z80 8000 first EQU 8000h 0000' 2A 8000 LD HL,(first) 0003' 7C LD A,H 0004' 65 LD H,L 0005' 6F LD L,A 0006' 22 8000 LD (first),HL 0009' C9 RET END Перестановка двухбайтных значений очень проста, если можно использовать две регистровые пары. MSX.M-80 1.00 01-Apr-85 PAGE 1 ; === перестановка двухбайтных значений first и second .Z80 8000 first EQU 8000h 8010 second EQU 8010h 0000' 2A 8000 LD HL,(first) 0003' ED 4B 8010 LD BC,(second) 0007' ED 43 8000 LD (first),BC 000B' 22 8010 LD (second),HL 000E' C9 RET END Команды обмена позволяют производить обмен содержимым между регистровыми парами ''DE'' и ''HL'', стеком и регистрами ''HL'', ''IX'', ''IY'', основным и дополнительным наборами регистров. Например, MSX.M-80 1.00 01-Apr-85 PAGE 1 .Z80 8000 data1 EQU 8000h 8010 data2 EQU 8010h 0000' 21 8000 LD HL,data1 0003' EB EX DE,HL 0004' 21 8010 LD HL,data2 0007' 23 INC HL 0008' EB EX DE,HL 0009' C9 RET END Дополнительный набор регистров может использоваться для кратковременного хранения значений основных регистров. Например, MSX.M-80 1.00 01-Apr-85 PAGE 1 .Z80 0000' D9 EXX 0001' 2A 000E' LD HL,(data) 0004' 23 INC HL 0005' 44 LD B,H 0006' 4D LD C,L 0007' 03 INC BC 0008' ED 43 0010' LD (data+2),BC 000C' D9 EXX 000D' C9 RET 000E' 34A1 data: DW 34A1h 0010' DS 2,0 END Содержимое регистровых пар можно сохранить в стеке. Стек — это область памяти, организованная по принципу "последним пришёл, первым вышел" или принципу "стопки тарелок". Например, для обмена содержимым регистровых пар ''HL'', ''BC'', ''DE'' можно написать: MSX.M-80 1.00 01-Apr-85 PAGE 1 .Z80 ; === в стек 0000' D5 PUSH DE 0001' C5 PUSH BC 0002' E5 PUSH HL ; === из стека 0003' C1 POP BC 0004' D1 POP DE 0005' E1 POP HL 0006' C9 RET END В результате произойдёт запись ''HL'' ⇒ ''BC'', ''BC'' ⇒ ''DE'', ''DE'' ⇒ ''HL''. {{anchor:n25}} ===== 2.5. Управление печатью листинга ===== Несколько директив ассемблера предназначены для управления порядком выдачи листинга программы. Имеется следующий набор директив: * ''TITLE строка'' — определение заголовка длиной до 16 символов для каждой страницы программы. Например, TITLE Conversion * ''PAGE число'' — задание размера страницы листинга в заданное число строк. Например, PAGE 44 * ''.LIST'' — печатать листинг. Поскольку этот режим действует по умолчанию, основное применение директивы — включение печати после директивы ''.XLIST'' * ''.XLIST'' — выключить выдачу листинга сразу после этой директивы * ''.PRINTX'' — сообщение — вывод на экран сообщения по ходу трансляции программы. Используется для отслеживания процесса трансляции. Первый непустой знак после директивы означает ограничитель, которым должно закончиться сообщение. Например, .PRINTX * OCEAN have been assembled * * ''.CREF'' — создание файла перекрёстных ссылок. * ''.XCREF'' — прекращение выдачи файла перекрёстных ссылок. * ''SUBTTL текст'' — печать подзаголовка титула. * ''*eject выражение'' — новая страница размером "выражение". {{anchor:n26}} ===== 2.6. Арифметические команды ===== К арифметическим командам Z80 относятся команды увеличения и уменьшения значения на единицу, команды сложения и вычитания, а также команда изменения знака (вычитания из нуля). Арифметические команды работают с одно - и двухбайтными операндами. Команд умножения и деления нет, они моделируются сложением и вычитанием. {{anchor:n261}} ==== 2.6.1. Представление операндов ==== При выполнении арифметических команд каждый операнд обычно представляется как 8-разрядное число со знаком в старшем разряде, в дополнительном двоичном коде. ^ Двоичное ^ Шестнадцатеричное ^ Десятичное ^ | 0111 1111 | 7F | 127 | | 0111 1110 | 7E | 126 | | … | … | … | | 0000 0011 | 03 | 3 | | 0000 0010 | 02 | 2 | | 0000 0001 | 01 | 1 | | 0000 0000 | 00 | 0 | | 1111 1111 | FF | -1 | | 1111 1110 | FE | -2 | | … | … | … | | 1000 0001 | 81 | -127 | | 1000 0000 | 80 | -128 | Аналогично представляются 16-разрядные значения: ^ Двоичное ^ Шестнадцатеричное ^ Десятичное ^ | 0111 1111 1111 1111 | 7FFF | 32767 | | 0111 1111 1111 1110 | 7FFE | 32766 | | … | … | … | | 0000 0000 0000 0011 | 0003 | 3 | | 0000 0000 0000 0010 | 0002 | 2 | | 0000 0000 0000 0001 | 0001 | 1 | | 0000 0000 0000 0000 | 0000 | 0 | | 1111 1111 1111 1111 | FFFF | -1 | | 1111 1111 1111 1110 | FFFE | -2 | | … | … | … | | 1000 0000 0000 0001 | 8001 | -32767 | | 1000 0000 0000 0000 | 8000 | -32768 | Кроме этого, для одно–байтовых величин иногда используют двоично-десятичное представление (BCD). В этом случае каждая из двух десятичных цифр значения представляется четырьмя битами (полубайтом). В таком представлении в байте не может быть сочетаний битов, соответствующих шестнадцатеричным цифрам A…F, т.е. 1010, 1011,… , 1111. Например, ^ Двоичная запись ^ Шестнадцатеричная ^ Десятичная ^ Двоично–десятичная \\ (BCD) ^ | 0011 0111 | 37 | 55 | 37 | | 1001 0110 | 96 | 150 | 96 | | 0001 1010 | 1А | 26 | - | {{anchor:n262}} ==== 2.6.2. Работа с восьмиразрядными числами ==== При выполнении команд один из операндов обычно должен быть помещён в регистр ''A'', а другой (если команда имеет длину один байт) — в один из 8–разрядных регистров микропроцессора или в ячейку памяти, адресуемую косвенно. В двухбайтовой команде значение второго операнда непосредственно задаётся во втором байте команды. Результат выполнения команды помещается в регистр ''A'' (аккумулятор). Команда ''ADD'' позволяет сложить два операнда. Сложение двух операндов со значением бита переноса ''C'' происходит по команде ''ADC''. Вычитание из аккумулятора второго операнда и учёт значения бита заёма C производится соответственно командами ''SUB'' и ''SBC''. Очень часто при написании программ используют команды ''INC'' и ''DEC'', служащие для увеличения или уменьшения содержимого регистра, регистровой пары или ячейки памяти, адресуемой по содержимому регистровой пары на единицу. Для изменения знака числа, находящегося в аккумуляторе ''А'', используется команда ''NEG''. Эта команда работает как вычитание из нуля содержимого аккумулятора. Арифметические команды, работающие с однобайтными значениями, выставляют флаги ''Z'' (ноль), ''S'' (отрицательное число), ''N'' (команда вычитания или уменьшения), ''H'' (полуперенос), ''C'' (перенос), ''V'' (переполнение). Рассмотрим на примерах выполнение групп арифметических команд. Обратите внимание на установку признаков и переходы значений из положительных в отрицательные и наоборот. ; ───────── установка знака ; команда результат ; аккумулятор десят.знач. флаги LD A,0 0000 0000 0 ─ ADD A,A 0000 0000 0 Z INC A 0000 0001 1 ─ DEC A 0000 0000 0 Z N DEC A 1111 1111 -1 S H N ; ───────── переход положит. чисел в отрицательные через макс. ; команда результат ; аккумулятор десят.знач. флаги LD A,7Eh 0111 1110 126 ─ INC A 0111 1111 127 ─ INC A 1000 0000 -128 S H V INC A 1000 0001 -127 S ; ───────── переход отрицат. чисел в положительные через макс. ; команда результат ; аккумулятор десят.знач. флаги LD A,81h 1000 0001 -127 ─ DEC A 1000 0000 -128 S N DEC A 0111 1111 127 H N V DEC A 0111 1110 126 N ; ───────── переход положит. чисел в отрицательные через 0 ; команда результат ; аккумулятор десят.знач. флаги LD A,1 0000 0001 1 ─ SUB 1 (1)0000 0000 0 Z N SUB 1 1111 1111 - 1 S H N C SUB 1 1111 1110 - 2 S N ; ───────── переход отрицат. чисел в положительные через 0 ; команда результат ; аккумулятор десят.знач. флаги LD A,FFh 1111 1111 - 1 ─ ADD A,1 (1)0000 0000 0 Z H C ADD A,1 0000 0001 1 ─ ; ───────── изменение знака в аккумуляторе ; команда результат ; аккумулятор десят.знач. флаги LD A,7Еh 0111 1110 126 ─ NEG 1000 0010 -126 S H N C NEG 0111 1110 126 H N C ; ───────── изменение знака в аккумуляторе ; команда результат ; аккумулятор десят.знач. флаги LD A,FEh 1111 1110 - 2 ─ NEG 0000 0010 2 H N C NEG 1111 1110 - 2 S H N C Как вы заметили, флаг переполнения ''V'' устанавливается при переходах 127 ⇒ -128 и -128 ⇒ 127, а флаг переноса ''C'' — при переходах "знак плюс ⇔ знак минус" через число ноль. При этом команды ''INC'' и ''DEC'' флаг ''C'' не изменяют. Уменьшение или увеличение значения, хранящегося в памяти возможно посредством косвенной адресации через регистры ''HL'', ''IX'' или ''IY''. Например, LD HL,0FCACh ; загружаем адрес INC (HL) ; увеличиваем значение INC (HL) ; увеличиваем значение ещё раз Команда сложения или вычитания двух чисел, представленных в двоично–десятичном формате BCD, даёт неправильный результат, поскольку она складывает их просто как двоичные значения. Для коррекции результата (приведения его снова в формат BCD) используется команда десятичной коррекции ''DAA''. Изучите примеры её работы. ; ───────── десятичная коррекция ; команда результат может означать: ; аккумулятор шестн. десят.знач. флаги LD A,06h 0000 0110 6 6 ─ ADD A,11h 0001 0111 17 17 ─ DAA 0001 0111 17 17 P ; ───────── десятичная коррекция ; команда результат может означать: ; аккумулятор шестн. десят.знач. флаги LD A,36h 0011 0110 36 36 ─ ADD A,24h 0101 1010 5A - ─ DAA 0110 0000 60 60 H P ; ───────── десятичная коррекция ; команда результат может означать: ; аккумулятор шестн. десят.знач. флаги LD A,72h 0111 0010 72 72 ─ ADD A,63h 1101 0101 D5 - S P DAA 0011 0101 (1)35 (1)35 P C В последнем примере во флаге ''C'' появился старший разряд результата (бит сотни). Для иллюстрации работы арифметических команд приведём программы умножения и деления восьмиразрядных чисел. В них использованы команды, которые мы изучим чуть позже. MSX.M-80 1.00 01-Apr-85 PAGE 1 .Z80 ; умножение first * second 0000' 3A 0010' LD A,(first) ; A <= first 0003' 47 LD B,A ; B <= first 0004' 05 DEC B ; B <= first-1 0005' 3A 0011' LD A,(second) ; A <= second 0008' 57 LD D,A ; D <= second 0009' 82 ADD A,D ; сложение <──┐ 000A' 10 FD DJNZ $-1 ; цикл по B ──┘ 000C' 32 0012' LD (result),A ; запись результата 000F' C9 RET 0010' 0C first: DB 12 0011' 08 second: DB 8 0012' result: DS 1 END Программа деления числа, находящегося в аккумуляторе, на число в регистре ''B''. На выходе в регистре ''C'' должно находиться частное, а в регистре ''A'' — остаток от деления. Z80-Assembler Page: 1 ORG 9000h 9000 0E00 LD c,0 ; частное равно 0 9002 0C L01: INC c ; частное <= частное + 1 9003 90 SUB b ; вычитаем из a - b 9004 30FC JR nc,L01 ; если нет переноса, ; то повторить 9006 80 ADD a,b ; добавить к a - b 9007 0D DEC c ; уменьшить частное 9008 C9 RET ; возврат END Флаг переноса в данном примере выставляется в том случае, если мы вычитаем из регистра ''A'' регистр ''B'' и при этом содержимое регистра ''B'' больше содержимого регистра ''A''. {{anchor:n263}} ==== 2.6.3. Работа с шестнадцатиразрядными числами ==== В системе команд микропроцессора есть команды ''ADD'', позволяющие сложить два 16-разрядных числа. Одно из них должно быть записано в регистровую пару ''HL'', ''IX'', ''IY'', а другое — в регистровую пару ''HL'', ''DE'', ''BC'' или ''SP''. Результат сложения помещается в регистровую пару ''HL'', ''IX'' или ''IY''. Так же, как и для 8-разрядных операндов, существуют команды сложения 16-разрядных чисел с битом признака ''C''. Одной из команд, позволяющих облегчить программирование на ассемблере Z80, является команда вычитания из регистровой пары ''HL'' 16-разрядного операнда — ''SBC''. Для 16-разрядных регистров (регистровых пар) есть команды уменьшения/увеличения на единицу ''DEC'' и ''INC''. Флаги выставляют практически только две команды — ''ADC'' и ''SBC''. Команды сложения устанавливают только флаг переноса. Рассмотрим на примерах выполнение групп арифметических команд. Обратите внимание на установку признаков и переходы значений из положительных в отрицательные и наоборот; отличия от команд работы с восьмиразрядными числами. ; ────────── установка знака ; команда результат ; шест. HL десят.знач. флаги LD HL,0 0000 0 ─ ADD HL,HL 0000 0 ─ INC HL 0001 1 ─ DEC HL 0000 0 ─ DEC HL FFFF -1 ─ ; ───────── переход положит. чисел в отрицательные через макс. ; команда результат ; шест. HL десят.знач. флаги LD HL,7FFEh 7FFE 32766 ─ INC HL 7FFF 32767 ─ INC HL 8000 -32768 ─ INC HL 8001 -32767 ─ ; ───────── переход отрицат. чисел в положительные через макс. ; команда результат ; шест. HL десят.знач. флаги LD HL,8001h 8001 -32767 ─ DEC HL 8000 -32768 ─ DEC HL 7FFF 32767 ─ DEC HL 7FFE 32766 ─ ; ───────── переход положит. чисел в отрицательные через 0 ; команда результат ; шест. HL десят.знач. флаги LD HL,1 0001 1 ─ LD BC,1 ─ SBC HL,BC 0000 0 Z N SBC HL,BC FFFF - 1 S H N C SBC HL,BC FFFD - 3 S N ; ───────── переход отрицат. чисел в положительные через 0 ; команда результат ; шест. HL десят.знач. флаги LD HL,FFFFh FFFF - 1 ─ LD BC,1 ─ ADD HL,BC 0000 0 H C ADD HL,BC 0001 1 ─ Микропроцессор Z80 не имеет команд умножения и деления для двухбайтных значений, поэтому приходится их моделировать группами простых команд. Ниже приводятся примеры программ умножения и деления шестнадцатиразрядных чисел. Первая программа — умножение шестнадцатиразрядных чисел, содержащихся в регистрах ''DE'' и ''HL'', с результатом в четырёх регистрах ''HLBC''. Используется обычный алгоритм — сдвиги и деление. MSX.M-80 1.00 01-Apr-85 PAGE 1 ; Умножение: [de] * [bc] => [HLbc] .Z80 0000' 21 0000 mult16: LD HL,0000 ; чистка HL 0003' 78 LD A,B ; A <= B 0004' 06 11 LD B,11h ; цикл 16 раз 0006' 18 07 JR Loop 0008' 30 01 Next: JR NC,Jump 000A' 19 ADD HL,DE ; если есть бит - сложить 000B' CB 1C Jump: RR H ; сдвиги HL 000D' CB 1D RR L 000F' 1F Loop: RRA 0010' CB 19 RR C 0012' 10 F4 DJNZ Next ; повторить все 0014' 47 LD B,A 0015' C9 RET Вторая программа — деление содержимого регистров ''BC'' на ''DE'' с частным в регистрах ''BC'' и остатком — в регистрах ''HL''. MSX.M-80 1.00 01-Apr-85 PAGE 1 ; --- [BC] % [DE] => [BC] ; --- [BC] MOD [DE] => [HL] .Z80 0000' 21 0000 LD HL,0 ; чистим HL 0003' 78 LD A,B ; A <= B 0004' 06 10 LD B,10h ; цикл 16 раз 0006' CB 11 RL C 0008' 17 RLA 0009' CB 15 Shift: RL L 000B' CB 14 RL H 000D' 38 0D JR C,Again ; переход по C 000F' ED 52 SBC HL,DE 0011' 30 01 JR NC,Round 0013' 19 ADD HL,DE 0014' 3F Round: CCF 0015' CB 11 Next: RL C 0017' 17 RLA 0018' 10 EF DJNZ Shift 001A' 47 LD B,A 001B' C9 RET ; выход в DOS 001C' B7 Again: OR A 001D' ED 52 SBC HL,DE 001F' 18 F4 JR Next END {{anchor:n27}} ===== 2.7. Логические команды ===== Логические операции, подобно основным арифметическим операциям сложения и вычитания, выполняются над содержимым аккумулятора и данными из регистра, ячейки памяти или операндами, заданными непосредственно. Основное отличие от арифметических заключается в том, что логические операции выполняются поразрядно, т.е. логическую операцию можно разбить на восемь независимых операций над соответствующими битами байтов–операндов. В результате операции формируется новое значение аккумулятора и устанавливаются флаги регистра ''F''. Микропроцессор Z80 имеет следующие логические команды: * ''AND'' — логическое И (конъюнкция): бит результата устанавливается в единицу, если оба соответствующих бита аргумента равны 1; иначе — 0; * ''OR'' — логическое ИЛИ (дизъюнкция): бит результата равен 1, если хотя бы у одного аргумента соответствующий бит равен 1; * ''CPL'' — логическое НЕ (отрицание): если бит аргумента равен 1, то бит результата 0; если бит аргумента равен 0, то соответствующий бит результата - 1; * ''XOR'' — исключающее ИЛИ (неэквивалентность): результат 1, если только у одного аргумента соответствующий бит равен 1; иначе ноль. Ниже приведена таблица истинности для логических операций: ^ X ^ Y ^ X AND Y ^ X OR Y ^ CPL X ^ X XOR Y ^ | 0 | 0 | 0 | 0 | 1 | 0 | | 0 | 1 | 0 | 1 | 1 | 1 | | 1 | 0 | 0 | 1 | 0 | 1 | | 1 | 1 | 1 | 1 | 0 | 0 | Команда ''AND'' обычно применяется для выделения, обнуления или проверки значения определённых битов аккумулятора. При этом второй операнд используется как маска. Рассмотрим примеры. A: 1011 1100 A: 1011 0110 A: 0101 1100 S: 1101 1011 S: 1111 0000 S: 1111 1111 ─────────── ─────────── ─────────── AND: 1001 1000 AND: 1011 0000 AND: 0101 1100 Команда ''OR'' обычно применяется для того, чтобы установить в 1 определённые разряды аккумулятора, чтобы собрать содержимое аккумулятора из нескольких нужных полей, для выяснения равенства содержимого регистра или двойного регистра нулю. A: 1011 1100 A: 1011 0110 A: 0101 1100 S: 1101 1011 S: 1111 0000 S: 0101 1100 ─────────── ─────────── ─────────── OR: 1111 1111 OR: 1111 0110 OR: 0101 1100 Например, для проверки регистровой пары ''BC'' на ноль можно написать: LD A,B ; копируем B в A or C ; ноль, если ни в А, ни в C нет единиц Команда ''CPL'' позволяет изменить значение каждого бита аккумулятора на обратное (инвертировать аккумулятор). Например, A: 1011 1100 A: 1011 0110 A: 0101 1100 ─────────── ─────────── ─────────── CPL: 0100 0011 CPL: 0100 1001 CPL: 1010 0011 Команда ''XOR'' позволяет выборочно инвертировать биты аккумулятора, очистить содержимое аккумулятора. Например, команда XOR 1, выполняемая многократно, формирует чередующуюся последовательность 0 и 1 в младшем разряде аккумулятора, а команда XOR A очищает аккумулятор. A: 1011 1100 A: 1011 0110 A: 0101 1100 S: 1101 1011 S: 1111 0000 S: 0101 1100 ─────────── ─────────── ─────────── XOR: 0110 0111 XOR: 0100 0110 XOR: 0000 0000 После выполнения рассмотренных команд логической обработки двух операндов значение признаков ''C'' и ''N'' регистра признаков ''F'' всегда равны 0. Команда ''CP'' позволяет сравнить два операнда. Сравнение происходит посредством "воображаемого" вычитания из первого операнда, хранящегося в аккумуляторе, второго операнда. Содержимое аккумулятора при этом не изменяется, зато устанавливаются или сбрасываются соответствующие флаги регистра F. Если в результате операции сравнения окажется, что операнды равны, то устанавливается признак нуля ''Z'', если же значение операнда, хранящегося в аккумуляторе, меньше значения второго операнда, то устанавливается флаг ''S''. С помощью команды ''CCF'' можно изменить значение бита признака переноса ''C'' на противоположное, т.е. инвертировать флаг переноса ''C''. Команда ''SCF'' позволяет установить значение признака переноса в 1. В качестве примера приведём программу умножения числа, находящегося в ''HL'', на число, большее нуля в ''DE''; при этом на выходе в двойном регистре ''HL'' будет произведение. Z80-Assembler Page: 1 ORG 9000h 9000 44 LD B,H ; BC <= HL 9001 4D LD C,L ; 9002 1801 JR L02 ; уменьшить DE 9004 09 L01: ADD HL,BC ; добавить к HL, BC 9005 1B L02: DEC DE ; уменьшаем -DE 9006 7A LD A,D ; если DE <> 0, то 9007 B3 OR E 9008 20FA JR NZ,L01 ; повторяем 900A C9 RET ; возврат END Обратите внимание на то, что программа будет работать неверно, если в ''DE'' будет число, равное или меньшее нуля. В качестве упражнения попробуйте написать программу умножения любых чисел (после изучения следующего параграфа). Кроме перечисленных выше команд Z80 имеет команды установки, сброса и проверки состояния одного бита в байте. Команда ''BIT'' проверяет состояние заданного бита, ''SET'' устанавливает бит в единицу, ''RES'' — сбрасывает бит в ноль. Биты операнда нумеруются следующим образом: | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | Рассмотрим пример. команда результат аккумулятор шестн.знач. флаги LD A,B9h 1011 1001 B9 ─ BIT 7,A 1011 1001 B9 S H BIT 6,A 1011 1001 B9 Z H P RES 0,A 1011 1000 B8 Z H P SET 2,A 1011 1100 BC Z H P RES 7,A 0011 1100 3C Z H P BIT 2,A 0011 1100 3C H {{anchor:n28}} ===== 2.8. Команды перехода и условного перехода ===== Эти команды играют особую роль в организации выполнения программ в микроЭВМ. Пока в программе не встретится команда этой группы, счётчик команд PC постоянно увеличивается на длину команды, и микропроцессор выполняет команду за командой в порядке их расположения в памяти. Порядок выполнения программы может быть изменён, если занести в регистр счётчика команд микропроцессора код адреса, отличающийся от адреса очередной команды. Это вызовет передачу управления другой части программы. Такая передача управления (переход) может быть выполнена с помощью трехбайтовой команды безусловного перехода ''JP адрес''. Как только эта команда встретится в программе, в регистр счётчика команд PC микропроцессора запишется значение указанного адреса. Таким образом, следующей командой, которую будет выполнять микропроцессор вслед за командой ''JP'', будет команда, код операции которой записан в ячейке с этим адресом. Безусловную передачу управления можно произвести также при помощи команд ''JP (HL)'', ''JP (IX)'' , ''JP (IY)'', в результате выполнения которых происходит передача управления по адресу, хранящемуся соответственно в регистровой паре ''HL'', ''IX'' или ''IY''. Кроме команды безусловного перехода микропроцессор имеет трехбайтовые команды условного перехода. При появлении команды условного перехода передача управления по адресу, указанному в команде, происходит только в случае выполнения определённого условия. Условия, с которыми оперируют команды условной передачи управления, определяются состоянием битов (разрядов) регистра признаков ''F'': ^ Мнемоника ^ Условие ^ Флаг ^ Код CCC ^ |NZ (Not Zero)|ненулевой результат| Z =0 | 000 | |Z (Zero)|нулевой результат| Z =1 | 001 | |NC (No Carry)|отсутствие переноса| C =0 | 010 | |C (Carry)|перенос| C =1 | 011 | |PO (Parity Odd)|нечётный результат| P =0 | 100 | |PE (Parity Even)|чётный результат| P =1 | 101 | |P (Plus)|число положительное| S =0 | 110 | |M (Minus)|число отрицательное| S =1 | 111 | Команда условного перехода может иметь, например, такой вид: JP NC,Again . Кроме команд перехода, в которых адрес указан непосредственно — "длинного" перехода, существуют команды "короткого" перехода, в которых адрес указан как смещение к текущему адресу, т.е. относительный адрес. Эти команды позволяют осуществить переход вперёд/назад на -126..+129 ячеек памяти и используются для написания перемещаемых программ. Хотя один байт обычно задаёт смещение -128..+127, здесь нужно учитывать, что счётчик команд увеличивается на длину самой команды перехода (2) до прибавления смещения, указанного в команде. Мнемоникой этих двухбайтных команд является ''JR''. Если используется символическое имя, ассемблер сам определит смещение и запишет его в код. Нужно только следить, чтобы переход не оказался слишком большим. Таким образом, можно написать команду ''JR Label'' вместо ''JP Label'', если расстояние до метки Label небольшое. Команды относительного перехода также могут быть условными. Однако обратите внимание, что допускаются только условия ''C'', ''NC'', ''Z'', ''NZ''. И последняя команда перехода — это команда цикла DJNZ смещение Суть этой команды следующая: 1) DEC B ; уменьшить регистр B 2) JR NZ,$+смещение ; если B<>0, сделать относительный переход. Теперь, как мы обещали, попробуем написать программу (или подпрограмму) задержки (или цикла, если в тело вставить какие-либо команды). Для этого будем использовать регистровую пару ''BC'' (также можно использовать ''DE'' или ''HL''). ORG 9000h ; установили начальный адрес компиляции LD BC,8000h ; загрузили в BC 8000h beg: DEC BC ; BC=BC-1 LD A,B ; проверяем OR C ; если BC<>0 JR NZ,beg ; перейти на beg RET ; иначе возврат ; конец программы END Рассмотрим подробнее, как работает эта программа. По команде ''LD BC, 8000h'' в регистровую пару BC загружается некоторое число (в данном случае 8000h). Команда ''DEC BC'' содержимое регистровой пары ''BC'' уменьшает на 1. Затем в аккумулятор пересылается содержимое регистра ''B'' и производится операция логического сложения с содержимым регистра ''C'' этой регистровой пары. Если в регистровой паре ''BC'' код ещё не стал равным 0, то после выполнения этой команды будет установлен признак ''NZ'' и выполнится команда условного перехода ''JR NZ,beg'' к началу цикла, после чего все действия повторяются. При этом программисты говорят, что в программе организован цикл. Выход из него возможен только тогда, когда в результате выполнения команды ''DEC BC'' в регистровой паре ''BC'' окажется ноль. Тогда работа подпрограммы закончится выполнением команды ''RET'', и произойдёт возврат к выполнению основной программы. Вы, наверное, уже догадались, что временная задержка, обеспечиваемая этой подпрограммой, определяется, во–первых, временем, необходимым для однократного выполнения всех команд этой подпрограммы, и, во–вторых, содержимым регистровой пары ''BC''. Последнее и определяет количество программных циклов. Как же определить число, которое надо поместить в регистровую пару BC для задания временной задержки в 0.5 секунд? Выполнение любой команды микропроцессора занимает строго определённое время. Поэтому, зная длительность выполнения каждой команды, можно вычислить общее время однократного выполнения данной подпрограммы. Оно составляет 9.6 мкс. Следовательно, для задания временной задержки в 0.5 сек. подпрограмма должна выполниться 0.5/(9.6*10^-6)=52080 раз. В качестве примера приведём небольшую программу, которая ищет минимальное из двух чисел. MSX.M-80 1.00 01-Apr-85 PAGE 1 ; --- Нахождение минимального из двух чисел ; min(fst, sec) => res .Z80 0000' 3A 0010' LD A,(fst) 0003' 47 LD B,A ; fst => B 0004' 3A 0011' LD A,(sec) ; sec => A 0007' B8 CP B ; sec ? fst 0008' FA 000C' JP M,minA ; переход, если sec A 000C' 32 0012' minA: LD (res),A ; min => res 000F' C9 RET 0010' 22 fst: DB 34 0011' 80 sec: DB 128 0012' res: DS 1,0 END Приведём программу преобразования числа, находящегося в регистре ''А'', в строку символов в коде ASCII. Адрес строки должен находиться в регистровой паре ''DE''. MSX.M-80 1.00 01-Apr-85 PAGE 1 .Z80 ; Вход: число в А ; Выход: строка по адресу [DE] в коде ASCII 0000' 06 2F DaaDig: LD B,'0'-1 0002' 04 INC B 0003' D6 0A SUB 10 0005' 30 FB JR NC,DaaDig+2 0007' C6 3A ADD A,'9'+1 0009' EB EX DE,HL 000A' 70 LD (HL),B 000B' 23 INC HL 000C' 77 LD (HL),A 000D' 2B DEC HL 000E' EB EX DE,HL 000F' C9 RET END Обратите внимание на то, что записи '0' и '9' означают коды знаков "0" и "9". Попробуйте разобраться, как работает программа, и подумайте над вопросом — будет ли она работать с отрицательными числами или с числами, большими чем 99. {{anchor:n29}} ===== 2.9. Команды сдвига ===== Команды сдвига позволяют сдвинуть биты одного регистра или байта памяти влево или вправо на один бит. Имеются следующие типы команд: * арифметический сдвиг влево (''SLA'' — Shift Left Arithmetical); * арифметический и логический сдвиг вправо (''SRA'' — Shift Right Arithmetical, ''SRL'' — Shift Right Logical); * циклический сдвиг (''RLCA'', ''RLC'', ''RRCA'', ''RRC''); * циклический сдвиг через флаг C (''RLA'', ''RL'', ''RRA'', ''RR''); * перестановка полубайт (''RLD'', ''RRD''). В командах ''SLA'' и ''SRL'' освободившийся разряд заполняется нулевым битом. В команде ''SRA'' тиражируется знаковый бит. Схема работы команды ''SLA'': ┌──┐ ┌──┬──┬──┬──┬──┬──┬──┬──┐ │ ◄─────│b7│b6│b5│b4│b3│b2│b1│b0◄──── 0 └──┘ └──┴──┴──┴──┴──┴──┴──┴──┘ флаг C регистр/память Схема работы команды ''SRA'': ┌──────────────────────────────────────┐ │ ┌──┐ ┌──┬──┬──┬──┬──┬──┬──┬──┐ │ └──► │ ┌─►b7│b6│b5│b4│b3│b2│b1│b0├──┘ └──┘ │ └┬─┴──┴──┴──┴──┴──┴──┴──┘ └──┘ флаг C регистр/память Схема работы команды ''SRL'': ┌──────────────────────────────────────┐ │ ┌──┐ ┌──┬──┬──┬──┬──┬──┬──┬──┐ │ └──► │ ┌─►b7│b6│b5│b4│b3│b2│b1│b0├►─┘ └──┘ │ └──┴──┴──┴──┴──┴──┴──┴──┘ 0 флаг C регистр/память Например, если в регистре B было двоичное значение 00111011, то после выполнения команды ''SLA'' B в регистре B появится значение 01110110. Арифметический сдвиг влево ''SLA'' можно использовать для умножения на степень двойки, а арифметический сдвиг вправо ''SRA'' — для деления на два без остатка (нацело). Приведём листинг программы, делящей содержимое регистра А нацело на 8. Она должна вызываться из программы на языке [[msx:basic:]] с помощью функции ''[[msx:basic:usr|USR]]''. 'divide on 8' Z80-Assembler Page: 1 TITLE 'divide on 8' ORG 9000h 521F = getA EQU 521Fh 2F99 = outHL EQU 2F99h ; === вход из USR 9000 CD1F52 CALL getA ; записать аргумент в А ; === деление на 8 нацело 9003 CB2F SRA A ; делим на 2 9005 CB2F SRA A ; еще раз 9007 CB2F SRA A ; и еще ; === возврат 9009 2600 LD H,0 ; 900B 6F LD L,A ; 900C C3992F JP outHL ; возвращаем результат END Команды циклического сдвига сдвигают содержимое регистра или байта памяти влево или вправо на один бит. При этом выдвинувшийся за разрядную сетку бит не теряется, а переносится на первое место с другого конца байта. Схема работы команды ''RLCA'': ┌─────────────────────────────┐ ┌──┐ │ ┌──┬──┬──┬──┬──┬──┬──┬──┐ │ │ ◄───┴─┤b7│b6│b5│b4│b3│b2│b1│b0◄───┘ └──┘ └──┴──┴──┴──┴──┴──┴──┴──┘ флаг C аккумулятор Команда ''RLC'' выполняется аналогично над регистром или косвенно адресуемой памятью. Если в аккумуляторе было записано число 10110100, то после выполнения команды ''RLCA'' в нем появится значение 01101001. Схема работы команды ''RRCA'': ┌───────┬──────────────────────────────┐ │ ┌──┐ │ ┌──┬──┬──┬──┬──┬──┬──┬──┐ │ └─► │ └──►b7│b6│b5│b4│b3│b2│b1│b0│───┘ └──┘ └──┴──┴──┴──┴──┴──┴──┴──┘ флаг C аккумулятор Команда ''RRC'' выполняется аналогично над регистром или памятью. Кроме этого можно использовать команды циклического сдвига влево или вправо через флаг C. Схема работы команды ''RLA'': ┌──────────────────────────────────────┐ │ ┌──┐ ┌──┬──┬──┬──┬──┬──┬──┬──┐ │ └─┤ ├──◄──┤b7│b6│b5│b4│b3│b2│b1│b0├◄──┘ └──┘ └──┴──┴──┴──┴──┴──┴──┴──┘ флаг C аккумулятор Команда ''RL'' выполняется аналогично над регистром или памятью. Схема работы команды RRA: ┌──────────────────────────────────────┐ │ ┌──┐ ┌──┬──┬──┬──┬──┬──┬──┬──┐ │ └─► ├──►──┤b7│b6│b5│b4│b3│b2│b1│b0├───┘ └──┘ └──┴──┴──┴──┴──┴──┴──┴──┘ флаг C аккумулятор Команда RR выполняется аналогично над регистром или памятью. Ниже приводится листинг программы преобразования однобайтного числа в двоично–десятичном коде (BCD) в однобайтное двоичное число. Аргумент программа берёт в ячейке A000h, а результат записывает в A0001h. Примеры выполняемых преобразований: двоично-десятичный код двоичный код 10 = 0001 0000 => 0A = 0000 1010 47 = 0100 0111 => 2F = 0010 1111 87 = 1000 0111 => 57 = 0101 0111 'conversion' Z80-Assembler Page: 1 TITLE 'conversion' ; === преобразование BCD-числа в двоичное число A000 = bcdarg EQU 0A000h A001 = hexres EQU 0A001h ORG 9000h ; === берем аргумент 9000 3A00A0 LD A,(bcdarg); записать однобайтный ; параметр в A 9003 47 LD B,A ; скопировали в B ; === меняем десятич. цифры местами 9004 07 RLCA ; 4 циклических сдвига 9005 07 RLCA ; аккумулятора влево, 9006 07 RLCA ; можно и вправо 9007 07 RLCA ; ; === оставляем только десятки 9008 E60F AND 0Fh ; десятки - в младший ; полубайт 900A 4F LD C,A ; копируем в C ; === умножаем десятки на десять 900B CB27 SLA A ; умножение на 8 900D CB27 SLA A ; 900F CB27 SLA A ; 9011 81 ADD A,C ; добавляем еще два 9012 81 ADD A,C ; 9013 4F LD C,A ; в C - преобр.десятки ; === добавляем единицы 9014 78 LD A,B ; восстанавливаем арг. 9015 E60F AND 0Fh ; оставляем единицы 9017 81 ADD A,C ; складыв. с десятками ; === возврат результата 9018 3201A0 LD (hexres),A; возвращаем результат 901B C9 RET END Иногда оказываются полезными команды циклической перестановки полубайтов влево (''RLD'') и вправо (''RRD''). Команды используют младшие 4 бита аккумулятора и байт, адрес которого должен быть записан в регистровую пару ''HL''. Схема работы команды ''RLD'': ┌───────────────────────┐ ┌─────┬──┴──┐ ┌─────┬──▼──┐ │ │ │ │ │ │ └─────┴──▲──┘ └┬─▲──┴──┬──┘ └───────────────┘ └─────┘ аккумулятор память, адресуемая (HL) Схема работы команды ''RRD'': ┌─────────────────┐ ┌───┐ ┌─────┬──┴──┐ ┌──▼─┴┬──▼──┐ │ │ │ │ │ │ └─────┴──▲──┘ └─────┴──┬──┘ └───────────────────────┘ аккумулятор память, адресуемая (HL) Изучите приведённый ниже листинг программы перевода BCD–числа в двоичный код с использованием команды ''RLD''. 'Conversion-2' Z80-Assembler Page: 1 TITLE 'Conversion-2' ; === преобразование BCD-числа в двоичное число A000 = bcdarg EQU 0A000h A001 = hexres EQU 0A001h ORG 9000h ; === берем аргумент 9000 2100A0 LD HL,bcdarg ; берем адрес аргумента 9003 46 LD B,(HL) ; записать аргумент в B 9004 AF XOR A ; чистим А ; === десятки - в младший полубайт, с порчей арг. 9005 ED6F RLD ; десятки - в младший ; полубайт A 9007 4F LD C,A ; копируем в C ; === умножаем десятки на десять 9008 CB27 SLA A ; умножение на 8 900A CB27 SLA A ; 900C CB27 SLA A ; 900E 81 ADD A,C ; добавляем еще два 900F 81 ADD A,C ; 9010 4F LD C,A ; в C - преобр.десятки ; === добавляем единицы 9011 78 LD A,B ; восстанавливаем арг. 9012 E60F AND 0Fh ; оставляем единицы 9014 81 ADD A,C ; складыв. с десятками ; === возврат результата 9015 3201A0 LD (hexres),A; возвращаем результат 9018 C9 RET END {{anchor:n210}} ===== 2.10. Пересылки блока данных ===== При помощи команд пересылки блока данных можно скопировать (переслать) содержимое участка памяти в другое место как пошагово, так и в автоматическом режиме. Перед выполнением этих команд необходимо загрузить в регистровые пары ''HL'', ''DE'' и ''BC'' необходимые параметры. В ''HL'' записывается адрес начала блока, в ''DE'' — адрес памяти, куда необходимо переслать блок, в ''BC'' — длину блока. Имеются следующие команды: * ''LDI'' — пересылка байта с инкрементом выполняется так: 1) LD (DE),(HL) 2) INC HL 3) INC DE 4) DEC BC * ''LDIR'' — пересылка блока с автоинкрементом: 1) LD (DE),(HL) 2) INC HL 3) INC DE 4) DEC BC 5) если BC<>0, то перейти на 1 * ''LDD'' — пересылка байта с декрементом: 1) LD (DE),(HL) 2) DEC HL 3) DEC DE 4) DEC BC * ''LDDR'' — пересылка блока с автодекрементом: 1) LD (DE),(HL) 2) DEC HL 3) DEC DE 4) DEC BC 5) если BC<>0, то перейти на 1. Например, для сохранения текущего состояния матрицы клавиатуры (см. Рабочую область MSX) можно написать: ORG 9000h LD HL,0FBE5h ; адрес матрицы клавиатуры LD DE,kon ; куда переписать LD BC,11 ; длина матрицы LDIR ; переписываем RET ; возврат kon: DS 11,0 END Другой пример: та же задача, но конечным адресом матрицы должен быть начальный адрес нашей программы: start EQU 9000h ; константа start = 9000h ORG start LD HL,0FBFFh ; адрес конца матрицы клавиатуры LD DE,start-1 ; адрес конца, куда переписать LD BC,11 ; длина матрицы LDDR ; переписываем RET ; возврат END Предлагаем Вам написать подпрограмму, которая обнуляет участок памяти ЭВМ. Для неё требуются следующие исходные данные: - начальный адрес памяти; - длина участка памяти. Будем считать, что при обращении к нашей подпрограмме эти данные заносятся соответственно в регистры ''HL'' и ''BC''. Попробуйте написать эту подпрограмму самостоятельно. Ниже мы приводим два варианта решения этой задачи. Z80-Assembler Page: 1 0000 3600 fillm: LD (HL),0 ; обнулить содерж. по адр.(HL) 0002 23 INC HL ; следующий адрес 0003 0B DEC BC ; уменьшить длину 0004 78 LD a,b ; проверить длина <> 0 ? 0005 B1 OR c 0006 20F8 JR nz,fillm; если нет, то повторить 0008 C9 RET ; иначе возврат END Недостаток этой программы в том, что она выполняется в течении некоторого времени, которое зависит от длины участка памяти. Другой вариант этой же программы позволяет выполнять те же действия, но за более короткое и фиксированное время: Z80-Assembler Page: 1 0000 3600 fillm: LD (hL),0 ; обнулить первый байт 0002 54 LD d,h ; загрузить в DE след.адрес 0003 5D LD e,L 0004 13 INC DE 0005 0B DEC BC ; уменьшить длину 0006 EDB0 LDIR ; обнулить участок памяти 0008 C9 RET ; возврат END Теперь давайте немного усложним нашу задачу. Пусть исходными данными являются: - начальный адрес участка памяти (''HL''); - конечный адрес участка памяти (''DE''); - константа, которой надо заполнить участок (''B''); Z80-Assembler Page: 1 0000 70 fillmc:LD (HL),b ; записать данные в первый адрес 0001 23 INC HL ; подготовить следующий адрес 0002 7C LD a,h ; сравнить старш. байты текущего 0003 92 SUB d ; адреса и адреса конца участка 0004 20FA JR nz,fillmc; если они <>, то повторить 0006 7D LD a,l ; сравнить младш. байты текущего 0007 BB CP e ; адреса и адреса конца участка 0008 20F6 JR nz,fillmc; если они <>, то повторить 000A 70 LD (HL),b ; обнулить последний адрес 000B C9 RET ; вернуться END Теперь разберём немного подробнее, как работает эта программа. Так как нам предстоит запись данных в последовательность ячеек, то организуем циклическую работу программы. В каждом цикле будем заполнять одну ячейку, а затем подготавливать адрес очередной ячейки памяти для её заполнения в следующем цикле. Для этого в цикле можно использовать команду ''INC HL'', увеличивающую каждый раз на 1 содержимое регистровой пары ''HL''. Работа программы должна прекратиться после заполнения последней ячейки памяти заданного участка. В ходе выполнения каждого цикла программы необходимо следить, чтобы постоянно увеличивающееся значение адреса в регистровой паре ''HL'' не превысило значения конечного адреса в регистровой паре ''DE''. Другой вариант программы выглядит так: Z80-Assembler Page: 1 0000 70 fillmc:LD (HL),b ; записать данные в первый адрес 0001 E5 PUSH HL ; сохранить в стеке первый адрес 0002 37 SCF ; сбросить бит переноса рег. F 0003 3F CCF 0004 ED52 SBC HL,DE ; получить длину участка 0006 44 LD b,h ; переслать ее в BC 0007 4D LD c,l 0008 E1 POP HL ; считать начальн. адрес участка 0009 54 LD d,h ; переслать его в DE 000A 5D LD e,l 000B 13 INC DE ; увеличить DE, т.е. след.адрес 000C EDB0 LDIR ; заполнить константой блок 000E C9 RET ; вернуться END {{anchor:n211}} ===== 2.11. Команды поиска ===== Следующей группой команд, которые мы рассмотрим, будет группа команд поиска. Они предназначены для поиска в памяти заданного в аккумуляторе значения. Имеются следующие команды: * ''CPI'' — сравнение ''A'' с байтом памяти с инкрементом: 1) CP A,(HL) 2) INC HL 3) DEC BC * ''CPIR'' — сравнение ''A'' с блоком пошаговое с автоинкрементом: 1) CP A,(HL) 2) INC HL 3) DEC BC 4) если BC<>0 и A<>(HL), то перейти на 1 * ''CPD'' — сравнение ''A'' с байтом с декрементом: 1) CP A,(HL) 2) DEC HL 3) DEC BC * ''CPDR'' — сравнение A с блоком с автодекрементом: 1) CP A,(HL) 2) DEC HL 3) DEC BC 4) если BC<>0 и A<>(HL), то перейти на 1. Так же как и в командах пересылки блока перед выполнением этих команд необходимо занести в регистры нужные параметры. В регистре ''А'' должно находиться число, которое мы хотим искать, в ''HL'' — начальный адрес участка памяти, в ''BC'' — длина участка. Если число найдётся, будет установлен флаг ''Z''. Если ''BC'' уменьшится в процессе поиска до нуля, будет сброшен флаг ''P/V'' (в противном случае он будет установлен). Например, на участке памяти с A000h по DE77h (исключительно) мы хотим найти и заменить все числа 32 на число 34. Один из возможных вариантов программы: ORG 9000h LD HL,0A000h ; адрес начала блока LD BC,0DE77h-0A000h ; длина блока поиска Next: LD A,32 ; что искать ? CPIR ; поиск RET NZ ; возврат, если не нашли DEC HL ; возврат на один байт назад LD (HL),34 ; запись по адресу нового числа INC HL LD A,B ; BC = 0 ? OR C JR NZ,Next ; если нет, повторяем RET ; возврат END {{anchor:n212}} ===== 2.12. Подпрограммы и прерывания ===== При написании программ обычно можно выделить одинаковые последовательности команд, часто встречающиеся в разных частях программы. Для того, чтобы многократно не переписывать такие последовательности команд, их объединяют в так называемые подпрограммы. В любой части основной программы программист может вставить трехбайтовую команду безусловного вызова подпрограммы ''CALL adr'' , во втором и третьем байте которой указывается адрес вызываемой подпрограммы. Выполнение команды ''CALL adr'' начинается с побайтовой засылки в стек адреса следующей после этой команды ячейки памяти. Этот адрес называется адресом возврата из подпрограммы. Он необходим для того, чтобы по окончании выполнения подпрограммы вернуться к продолжению основной программы. После записи в стек адреса возврата из подпрограммы в счётчик команд PC микропроцессора загружается величина adr, т.е. адрес первой команды вызываемой подпрограммы. Таким образом, управление из основной программы передаётся на вызываемую подпрограмму. Выполнение подпрограммы обычно заканчивается командой возврата из подпрограммы, например, однобайтовой командой безусловного возврата из подпрограммы ''RET''. При этом содержимое верхушки стека, т.е. адрес возврата из подпрограммы, пересылается из стека в счётчик команд PC микропроцессора, и управление вновь передаётся основной программе. Ниже приводится пример программы, вызывающей подпрограмму умножения содержимого двойного регистра ''BC'' на ''DE''. Z80-Assembler Page: 1 ORG 9000h 9000 018400 LD BC,132 ; загрузка арг. 9003 118401 LD DE,388 ; загрузка арг. 9006 CD0D90 CALL mpy ; вызов подпрогр. 9009 2200A0 LD (0A000h),HL ; запись результата 900C C9 RET ; возврат ; ───────────────────── ; умножение BC на DE ; результат - в HL ; 900D 210000 mpy: LD HL,0000 ; чистим HL 9010 19 next: ADD HL,DE ; прибавляем DE 9011 0B DEC BC ; уменьшаем BC 9012 78 LD A,B ; проверяем на 0 9013 B1 OR C 9014 20FA JR NZ,next ; повторить 9016 C9 RET ; возврат END Кроме команды безусловного вызова и возврата из подпрограммы, в системе команд имеется восемь команд условного вызова подпрограммы и восемь команд условного возврата из подпрограмм, действие которых определяется так же, как и у команд условной передачи управления, состоянием регистра признаков F. Если условие для выполнения команды отсутствует, то вызов подпрограммы или возврат из неё не выполняются. Кроме трехбайтовой команды безусловного вызова подпрограммы ''CALL adr'', в системе команд микропроцессора имеется восемь однобайтовых команд ''RST 0'' – ''RST 7'' вызова подпрограмм, расположенных по фиксированному адресу. Появление в основной программе любой из этих команд вызывает запись в стек адреса возврата из подпрограммы и передачу управления на соответствующую ячейку памяти, где расположена первая команда подпрограммы. В [[11|Приложении 1]] приведена [[11#table_rts|таблица]] соответствия между этими командами и шестнадцатеричными адресами ячеек памяти, куда передаётся управление при их выполнении. К группе команд работы с подпрограммами относятся ещё две команды возврата из маскируемого и немаскируемого прерываний: ''RETI'' и ''RETN''. В программе на языке ассемблера могут использоваться внешние переменные, то есть переменные, определённые вне данной программы. Внешние значения транслируются в двухбайтные величины (однобайтные не поддерживаются). Внешние переменные описываются директивой ассемблера ''EXT'' или ''EXTRN''. Можно также отметить внешнюю переменную двумя символами "#" в конце её имени. Кроме этого, в программе могут быть определены глобальные имена, то есть имена, доступные извне данной программы (для той программы они являются внешними). Для указания, что имя является глобальным, используются директивы ''ENTRY'' или ''PUBLIC''. Глобальное имя можно также обозначить двумя знаками ":" в конце имени. Эти возможности удобно использовать для организации связей с программами, написанными на языке C. В качестве примера рассмотрим программу, устанавливающую позицию курсора на текстовом или графическом экране, и программу вывода одного символа на графический экран. MSX.M-80 1.00 01-Apr-85 PAGE 1 ; Установка позиции курсора на текстовом или графическом ; экране. Координаты - в HL и DE .Z80 PUBLIC Loc@ 0000' 3A FCAF Loc@: LD A,(0FCAFh) ; тип экрана 0003' FE 02 CP 2 0005' 38 0F JR C,Locate ; ----- размещение на графическом экране 0007' 22 FCB3 LD (0FCB3h),HL 000A' 22 FCB7 LD (0FCB7h),HL 000D' ED 53 FCB5 LD (0FCB5h),DE 0011' ED 53 FCB9 LD (0FCB9h),DE 0015' C9 RET ; ----- размещение на текстовом экране 0016' 65 Locate: LD H,L 0017' 6B LD L,E 0018' F7 RST 30h 0019' 00 DEFB 0 001A' 00C6 DEFW 0C6h 001C' C9 RET END Вывод одного символа на графический экран. MSX.M-80 1.00 01-Apr-85 PAGE 1 .Z80 PUBLIC putgc@ ; --- выводится символ с кодом в аккумуляторе 0000' F7 putgc@: RST 30h 0001' 00 DEFB 0 0002' 008D DEFW 08Dh 0004' C9 RET END {{anchor:n213}} ===== 2.13. Подпрограммы BIOS ===== В системе MSX имеется набор стандартных подпрограмм, использование которых иногда может значительно облегчить програмирование на языке ассемблера. Их английская аббревиатура — подпрограммы BIOS. При помощи подпрограмм BIOS можно работать с клавиатурой, программируемым звукогенератором, магнитофоном, видеопроцессором и другими устройствами. Программы, вызывающие только подпрограммы BIOS и не работающие непосредственно с устройствами (например, при помощи портов ввода/вывода), смогут работать и на других версиях системы MSX. {{anchor:n2131}} ==== 2.13.1. Клавиатура ==== Для работы с клавиатурой применяются следующие подпрограммы BIOS: чтение статуса строки матрицы клавиатуры (''SNSMAT, 0141H''), обнаружение нажатия клавиш CTRL+STOP при отключённых прерываниях (''BREAKX, 0B7h''), проверка буфера клавиатуры (''CHSNS, 09Ch''), ввод символа из буфера клавиатуры (''CHGET, 09Fh''), чистка буфера клавиатуры (''KILBUF, 156h''), ввод строки (''PINLIN, 0AEh'' и ''INLIN, 0B1h''), ввод графического символа (''CNVCHR, 0ABh'') и другие. В первом примере опрашивается матрица клавиатуры на предмет нажатия клавиши Z. Выход из программы — по клавишам CTRL+STOP. Z80-Assembler Page: 1 ; === Проверка, нажата ли клавиша Z 00A2 = CHPUT EQU 00A2h ; вывод символа 0141 = SNSMAT EQU 0141H ; опрос матрицы клавиатуры 00B7 = BREAKX EQU 00B7h ; нажато ли CTRL/STOP ? ORG 9000h 9000 CDB700 Again: CALL BREAKX ; нажато ли CTRL/STOP 9003 D8 RET C ; возврат, если "да" 9004 3E04 LD A,4 ; опрашиваем строку 4 9006 CD4101 CALL SNSMAT 9009 E620 AND 00100000b ; нажата ли клавиша Z ? 900B 20F3 JR NZ,Again ; если нет, ждем 900D 3E5A LD A,'Z' ; иначе печатаем 'Z' 900F CDA200 CALL CHPUT 9012 18EC JR Again ; все повторяем END Во втором примере производится чтение символа из буфера клавиатуры. Обратите внимание на действие содержимого ячейки ''REPCNT''. Как и в первом примере, выход осуществляется при нажатии клавиш CTRL+STOP. Z80-Assembler Page: 1 009C = CHSNS EQU 09Ch ; опрос буфера клавиатуры 009F = ChGet EQU 09Fh ; ввод символа 00A2 = ChPut EQU 0A2h ; вывод символа 0156 = KilBuf EQU 156h ; чистка буфера 00B7 = BreakX EQU 0B7h ; нажато ли CTRL/STOP ? F3F7 = REPCNT EQU 0F3F7h ORG 9000h 9000 CD9C00 Key: CALL CHSNS ; опрос буфера клавиатуры 9003 280A JR Z,Key1 ; если буфер пуст, вывод '.' 9005 3E01 LD A,1 ; маленькая задержка до 9007 32F7F3 LD (REPCNT),A ; автоповторения клавиши 900A CD9F00 CALL ChGet ; вводим символ из буфера 900D 1802 JR Key2 ; выводим его на экран 900F 3E2E Key1: LD A,'.' 9011 CDA200 Key2: CALL ChPut ; вывод символа 9014 CD5601 CALL KilBuf ; чистка буфера 9017 CDB700 CALL BreakX ; нажато ли CTRL/STOP ? 901A 30E4 JR NC,Key ; если нет, то повторить 901C C9 RET END Третий пример демонстрирует отличия в работе подпрограмм BIOS ''InLin'' и ''PinLin''. Z80-Assembler Page: 1 00A2 = ChPut EQU 0A2h ; вывод символа 00B1 = InLin EQU 0B1h ; ввод строки 00AE = PinLin EQU 0AEh ; ввод строки 0156 = KilBuf EQU 156h ; чистка буфера F55E = Buf EQU 0F55Eh ; буфер ORG 9000h ; === InLin 9000 CD5601 CALL KilBuf ; чистка буфера 9003 212E90 LD HL,PRMPT1 ; выводим подсказку 9006 CD2590 CALL PUTMSG 9009 CDB100 CALL InLin ; вводим строку 900C 215EF5 LD HL,Buf ; выводим содержимое буфера 900F CD2590 CALL PUTMSG ; === PinLin 9012 CD5601 CALL KilBuf ; чистка буфера 9015 213890 LD HL,PRMPT2 ; выводим подсказку 9018 CD2590 CALL PUTMSG 901B CDAE00 CALL PinLin ; вводим строку 901E 215EF5 LD HL,Buf ; выводим содержимое буфера 9021 CD2590 CALL PUTMSG 9024 C9 RET ; === Подпрограмма печати строки 9025 7E PUTMSG: LD A,(HL) ; берем символ 9026 B7 OR A ; если код ноль, выход 9027 C8 RET Z 9028 CDA200 CALL CHPUT ; выводим один символ 902B 23 INC HL 902C 18F7 JR PUTMSG ; повторяем снова ; === Данные 902E 0D0A496E PRMPT1: DB 0Dh,0Ah,'InLin: ',0 9032 4C696E3A 9036 2000 9038 0D0A5069 PRMPT2: DB 0Dh,0Ah,'PinLin:',0 903C 6E4C696E 9040 3A00 END {{anchor:n2132}} ==== 2.13.2. Звукогенератор ==== Для работы со звукогенератором используются следующие подпрограммы BIOS: инициализация PSG (''GICINI, 90h''), запись данных в регистр PSG (''WRTPSG, 93h''), чтение данных из регистра PSG (''RDPSG, 96h''), запуск звучания музыки (''STRTMS, 99h''), включение/выключение бита звукового порта (''CHGSND, 135h'') и другие. В первом примере показана установка однотонного звучания в канале А. Z80-Assembler Page: 1 0093 = WRTPSG EQU 93h ; запись в регистр PSG ORG 9000h 9000 3E07 LD A,7 ; выбор канала А 9002 1E3E LD E,00111110b 9004 CD9300 CALL WRTPSG ; запись в регистр 7 9007 3E08 LD A,8 ; установка громкости звука 9009 1E0F LD E,15 900B CD9300 CALL WRTPSG ; запись в регистр 8 900E 3E00 LD A,0 ; младшие биты частоты звука 9010 1EFE LD E,0FEh 9012 CD9300 CALL WRTPSG ; запись в регистр 0 9015 3E01 LD A,1 ; старшие биты частоты звука 9017 1E00 LD E,0 9019 CD9300 CALL WRTPSG ; запись в регистр 1 901C C9 RET END Второй пример связан с установкой/выключением звукового бита порта ''AAh''. Z80-Assembler Page: 1 0090 = GICINI EQU 090h ; инициализация 0135 = CHGSND EQU 135h ; вкл/выкл. бита 7 009F = CHGET EQU 9Fh ; ввод символа 00B7 = BREAKX EQU 0B7h ; нажато ли CTRL/STOP ? ORG 0A000h A000 CD9000 CALL GICINI ; инициализация A003 3E01 Sound: LD A,1 A005 CD3501 CALL CHGSND ; включаем бит A008 CD9F00 CALL CHGET ; ждем нажатия клавиши A00B AF Silen: XOR A A00C CD3501 CALL CHGSND ; выключаем бит A00F CD9F00 CALL CHGET ; ждем нажатия клавиши A012 CDB700 CALL BREAKX ; нажаты ли CTRL/STOP ? A015 D8 RET C ; если да - выход A016 18EB JR Sound END В последнем примере покажем использование подпрограмм BIOS с адресами C0h (beep) и 93h (запись данных в регистр PSG) для генерации шума моря. Z80-Assembler Page: 1 ORG 9000h ; шум моря 9000 CDC000 CALL 0C0h ; beep 9003 211F90 LD HL,ENDDATA ; адрес байта данных ; для 13 регистра PSG 9006 3E0D LD a,13 ; кол-во регистр. PSG 9008 5E LD e,(HL) ; загрузить данные 9009 CD9300 CALL 93h ; записать в регистр 900C 2B DEC HL ; след. байт данных 900D D601 SUB 1 ; след. н-р рег. PSG 900F 30F7 JR NC,$-7 ; если не -1, повтор. 9011 C9 RET ; возврат ;------------------------------------ ; данные для "шума моря" 9012 DEFS 6 9018 1EB71000 DEFB 30,183,16,0,0,0,90,14 901C 00005A0E ;------------------------------------ 901F = ENDDATA EQU $-1 END {{anchor:n2133}} ==== 2.13.3. Графика ==== В качестве примера напишем программу установки режима GRAPHIC-2 видеопроцессора, создания спрайта размером 8×8 точек без увеличения и установки его на экране с координатами (128,100) цветом 13. При этом будем использовать подпрограммы BIOS. Z80-Assembler Page: 1 ORG 9000h ; screen 2,2 9000 21E0F3 LD HL,0F3E0h; адрес хранения рег. #1 VDP 9003 CB8E RES 1,(HL) ; спрайт 8*8 9005 CB86 RES 0,(HL) ; нормальный размер спрайта 9007 CD7200 CALL 72h ; screen 2 ; создание шаблона спрайта 900A 3E00 LD a,0 ; номер образа спрайта =0 900C CD8400 CALL 84h ; узнаем адрес образа 900F 112990 LD DE,dat ; адрес данных для ; создания шаблона 9012 EB EX DE,HL ; меняем HL и DE 9013 010800 LD BC,8 ; длина образа 9016 CD5C00 CALL 5Ch ; заполняем образ спрайта ; во VRAM ; выведение спрайта на экран 9019 3E00 LD a,0 ; номер спрайта = 0 901B CD8700 CALL 87h ; адрес таблицы атрибутов 901E 113190 LD DE,pts ; адрес данных для табл. ; атрибутов 9021 EB EX DE,HL 9022 010400 LD BC,4 ; длина таблицы атрибутов 9025 CD5C00 CALL 5Ch 9028 C9 RET ; данные для шаблона спрайта 9029 01020408 dat: DEFB 1,2,4,8,16,32,64,128 902D 10204080 ; атрибуты спрайта 9031 80 pts: DEFB 128 ; координата X 9032 64 DEFB 100 ; координата Y 9033 00 DEFB 0 ; номер шаблона 9034 0D DEFB 13 ; цвет END При работе со спрайтами размером 16×16 точек учтите, что номер шаблона определяется так же, как и для спрайтов размером 8×8, но умноженный на 4. В этой программе использована подпрограмма BIOS с адресом вызова 5Ch. Она переписывает блок данных из RAM во VRAM. Ниже мы попытаемся реализовать эту подпрограмму с помощью команд, работающих с портами ввода/вывода. {{anchor:n2134}} ==== 2.13.4. Магнитофон ==== Для работы с магнитофоном используются следующие подпрограммы BIOS: включение мотора и открытие файла (''TAPION, E1h'' и ''TAPOON, EAh''), чтение одного байта (''TAPIN, E4h''), запись одного байта (''TAPOUT, EDh''), конец работы с лентой (''TAPIOF, E7h'' и ''TAPOOF, F0h'') и другие. Приведённая ниже программа "щёлкает" реле включения/выключения мотора накопителя на магнитной ленте (т.е. просто включает и выключает его). Z80-Assembler Page: 1 ORG 9000h 9000 AF motor: XOR a ; очищаем аккумулятор 9001 CDF300 CALL 00F3h ; вкл/выкл 9004 EE01 XOR 1 ; смена 0 на 1 или 1 на 0 9006 08 EX AF,AF' ; сменить аккумулятор 9007 010008 LD BC,800h ; небольшая задержка 900A 0B nt: DEC BC 900B 78 LD a,b 900C B1 OR c 900D 20FB JR nz,nt 900F CDB700 CALL 00B7h ; проверить не нажато ли ; CTRL/STOP ? 9012 D8 RET C ; возврат, если нажато 9013 08 EX AF,AF' ; вернуть "наш" аккумулятор 9014 18EB JR motor+1 ; повторить действия END В этой подпрограмме используются две подпрограммы BIOS. Это 00F3h — включение и выключение мотора магнитофона и 00B7h — проверка, нажаты ли клавиши CTRL+STOP. Во втором примере при помощи подпрограмм BIOS просматривается и печатается список файлов на магнитной ленте. Z80-Assembler Page: 1 00A2 = Chput EQU 00A2h ; вывод символа 00E1 = Tapion EQU 00E1h ; вкл. мотор, читать заголовок 00E4 = Tapin EQU 00E4h ; читаем байт с ленты 00E7 = Tapiof EQU 00E7h ; заверш. работу с магнитофоном ORG 9000h ; === Просмотр имен файлов на ленте 9000 CDE100 Start: CALL Tapion ; вкл. мотор, читать заголовок 9003 0610 LD b,16 9005 21A090 LD HL,Work ; адрес рабочей области 9008 E5 Next: PUSH HL ; сохранить 9009 C5 PUSH BC 900A CDE400 CALL Tapin ; читаем байт с ленты 900D C1 POP BC 900E E1 POP HL 900F 382B JR c,Error ; если была ошибка, переход 9011 77 LD (HL),a ; иначе повторяем 9012 23 INC HL 9013 10F3 DJNZ Next 9015 215790 LD HL,Filnam ; выводим имя файла 9018 CD4D90 CALL Putstr 901B 21AA90 LD HL,Work+10 ; 901E CD4D90 CALL Putstr 9021 CD4690 CALL Crlf 9024 3AA090 LD a,(Work) ; проверяем атрибуты файла 9027 217090 LD HL,Binfil ; файл двоичный ? 902A FED3 CP 0D3h 902C 2811 JR z,Prt 902E 216390 LD HL,Ascfil ; файл ASCII ? 9031 FEEA CP 0EAh 9033 280A JR z,Prt 9035 217E90 LD HL,Macfil ; файл кодов ? 9038 FED0 CP 0D0h 903A 2803 JR z,Prt 903C 218B90 Error: LD HL,Errstr ; сообщение об ошибке 903F CD4D90 Prt: CALL Putstr ; вывод строки 9042 CDE700 CALL Tapiof ; заверш. работу с магнитофоном 9045 C9 RET ; === Подпрограмма перевода строки 9046 219D90 Crlf:LD HL,Stcrlf ; перевод строки 9049 CD4D90 CALL Putstr 904C C9 RET ; === Подпрограмма вывода строки 904D 7E Putstr: LD a,(HL) 904E FE24 CP '$' 9050 C8 RET z 9051 CDA200 CALL Chput 9054 23 INC HL 9055 18F6 JR Putstr ; === Данные 9057 46696C65 Filnam: DB 'File name: $' 905B 206E616D 905F 653A2024 9063 41736369 Ascfil: DB 'Ascii file',0Dh,0Ah,'$' 9067 69206669 906B 6C650D0A 906F 24 9070 42696E61 Binfil: DB 'Binary file',0Dh,0Ah,'$' 9074 72792066 9078 696C650D 907C 0A24 907E 42736176 Macfil: DB 'Bsave file',0Dh,0Ah,'$' 9082 65206669 9086 6C650D0A 908A 24 908B 54617065 Errstr: DB 'Tape read error',0Dh,0Ah,'$' 908F 20726561 9093 64206572 9097 726F720D 909B 0A24 909D 0D0A24 Stcrlf: DB 0Dh,0Ah,'$' 90A0 Work: DS 16,0 90B0 24 DB '$' END Когда при помощи этих подпрограмм BIOS создаются подпрограммы READ/WRITE для файлов на кассете, нужно использовать только READ или WRITE без каких-либо других действий. Например, чтение данных с ленты и отображение их на экране может вызвать ошибку чтения. {{anchor:n2135}} ==== 2.13.5. Часы и энергонезависимая память ==== В книге "Архитектура микрокомпьютера MSX-2" была описана структура и функции микросхемы CLOCK-IC с энергонезависимой памятью. Имеются две подпрограммы расширенного BIOS — REDCLK (01F5h) и WRTCLK (01F9h), которые позволяют записывать информацию в регистры ''CLOCK-IC'' или читать её оттуда (см. MSX-BASIC BIOS). Ниже приводится пример программы, которая устанавливает некоторые параметры экрана, восстанавливаемые при перезагрузке или включении компьютера — тип и ширину экрана, цвет изображения и фона. Z80-Assembler Page: 1 01F9 = WRTCLK EQU 01F9h 015F = EXTROM EQU 015Fh ORG 9000h ; === Установка типа экрана 9000 0E23 LD C,23H ; блок 2, регистр 3 9002 3E00 LD A,0 ; тип интерфейса и экрана 9004 CD2490 CALL WrtRAM ; запись в CLOCK-IC ; === Установка ширины экрана 40 (28h) 9007 0E24 LD C,24H ; блок 2, регистр 4 9009 3E08 LD A,8 ; младшие биты (28h) 900B CD2490 CALL WrtRAM ; запись в CLOCK-IC 900E 0E25 LD C,25H ; блок 2, регистр 5 9010 3E02 LD A,2 ; старшие биты (28h) 9012 CD2490 CALL WrtRAM ; запись в CLOCK-IC ; === Установка цвета изображения 9015 0E26 LD C,26H ; блок 2, регистр 6 9017 3E04 LD A,4 ; COLOR 4 9019 CD2490 CALL WrtRAM ; запись в CLOCK-IC ; === Установка цвета фона 901C 0E27 LD C,27H ; блок 2, регистр 7 901E 3E0E LD A,14 ; COLOR ,14 9020 CD2490 CALL WrtRAM ; запись в CLOCK-IC ; === Возврат в MSX-BASIC 9023 C9 RET ; === Подпрограмма записи в CLOCK-IC 9024 E5 WrtRAM: PUSH HL ; сохраняем регистры 9025 C5 PUSH BC 9026 DD21F901 LD IX,WRTCLK ; межслотовый вызов 902A CD5F01 CALL EXTROM ; SUBROM EBIOS 902D E1 POP HL ; восстанавливаем 902E C1 POP BC ; регистры 902F C9 RET ; возврат END {{anchor:n2136}} ==== 2.13.6. Межслотовые вызовы подпрограмм ==== При выполнении программы может возникнуть ситуация, когда необходимо вызвать подпрограмму, находящуюся в текущий момент в неактивном слоте. Например, при режиме работы, когда на всех страницах включена только оперативная память, может понадобиться вызов подпрограммы BIOS, хранящейся в одном из слотов ПЗУ. В этом случае можно либо попытаться включить необходимые слоты и вызвать подпрограмму, либо выполнить межслотовый вызов. Межслотовый вызов выполняется подпрограммой BIOS ''CALSLT'' по адресу 1Ch. [[msx:dos:|MSX-DOS]] также поддерживает эту подпрограмму. Перед вызовом в регистр ''IX'' нужно загрузить адрес требуемой подпрограммы BIOS, а в ''IY'' — указатель слота в виде: ┌───┬───┬───┬───┬───┬───┬───┬───┐ │ F │ 0 │ 0 │ 0 │ S │ S │ P │ P │ └───┴───┴───┴───┴───┴───┴───┴───┘ │ └──┬──┘ └──┬──┘ ┌ Первичный │ │ └────┤ номер слота │ │ └ ──────┴───── └─── Вторичный номер слота 0 — Вторичный слот не используется 1 — Используется вторичный слот При межслотовом вызове ''CALSLT'' и ''CALLF'' прерывания автоматически запрещаются. При возврате управления из этих подпрограмм в MSX-1 прерывания остаются запрещёнными, а в MSX-2 восстанавливается статус прерываний, который был установлен до вызова подпрограмм. Например, LD IX,0156h ; адрес чистки буфера клавиатуры LD IY,0 ; указатель слота CALL 1Ch ; межслотовый вызов EI ; включение прерываний в MSX-1 Понятно, что в этом случае регистры ''IX'' и ''IY'' не могут использоваться для передачи параметров. Для межслотового вызова можно использовать и команду рестарта. Например, RST 30h DB 0 ; указатель слота DW 6Ch ; установка SCREEN 0 В некоторых случаях межслотовый вызов оказывается невозможным. Например, если вызываемая подпрограмма работает не в одной, а в двух страницах памяти неактивного слота (межслотовый вызов делает временно активной только одну страницу). В такой ситуации можно создать свой собственный межслотовый вызов — перейти в страницу, активную в любом случае, запомнить адрес возврата, активировать нужные слоты и страницы, вызвать нужную подпрограмму, восстановить конфигурацию слотов и вернуться. При вызове подпрограмм расширенного BIOS — SUBROM EBIOS нужно учитывать, что они находятся в различных вторичных слотах памяти [[msx:yamaha_yis-503iiir:yamaha_yis-503iiir|компьютера ученика]] (3-0) и [[msx:yamaha_yis-805-128r2:yamaha_yis-805-128r2|компьютера учителя]] (3-1). Указатель слота EBIOS записан в ячейке EXBRASA (FAF8h). Поэтому для вызова подпрограммы EBIOS можно использовать команды вида: LD IX,имя LD IY,(EXBRASA-1) CALL 1Ch Другая возможность вызова SUBROM — использование подпрограммы BIOS EXTROM (015Fh). Адрес вызова записывается в регистр ''IX'', задание ''IY'' уже не требуется. Ниже приводится пример программы, рисующей в ''SCREEN 5'' букву А и линию при помощи межслотового вызова EBIOS. MSX.M-80 1.00 01-Apr-85 PAGE 1 .Z80 001C CALSLT EQU 1Ch ; межслотовый вызов 0030 CALLF EQU 30h ; межслотовый вызов 006C IniTxt EQU 006Ch ; инициирование SCREEN 0 009F GetChr EQU 009Fh ; ввод символа 0156 KillBuf EQU 0156h ; чистка буфера клавиатуры ; --- Расширенный BIOS 0089 GRPPTR EQU 89h ; вывод символа в SCREEN 5 00D1 CHGMOD EQU 0D1h ; установка типа экрана 0085 DOGRPH EQU 85h ; рисует линию ; --- Системная область F3E9 FORCLR EQU 0F3E9h ; цвет текста F3F2 ATRBYT EQU 0F3F2h ; цвет байта FAF8 EXBRASA EQU 0FAF8h ; указатель слота EBIOS FB02 LOGOPR EQU 0FB02h ; логическая операция FCB3 GXPOS EQU 0FCB3h ; позиция X графического курсора FCB5 GYPOS EQU 0FCB5h ; позиция Y графического курсора ; --- установить и инициализировать SCREEN 5 0000' 3E 05 Start: LD A,5 0002' DD 21 00D1 LD ix,CHGMOD 0006' FD 2A FAF7 LD iy,(EXBRASA-1) 000A' CD 001C CALL CALSLT ; MSX-DOS поддерживает ; межслотовый вызов ; --- вывести символ на экран 000D' AF XOR a ; код логич. операции 000E' 32 FB02 LD (LOGOPR),a 0011' 3E 0D LD a,13 ; фиолетовый цвет 0013' 32 F3E9 LD (FORCLR),a 0016' 3E 41 LD a,'A' ; буква A 0018' DD 21 0089 LD ix,GRPPTR 001C' FD 2A FAF7 LD iy,(EXBRASA-1) 0020' CD 001C CALL CalSlt ; вывод буквы А ; --- нарисовать линию 0023' 3E 96 LD A,150 ; куда рисовать 0025' 32 FCB3 LD (GXPOS),A 0028' 3E 62 LD A,98 002A' 32 FCB5 LD (GYPOS),A 002D' 01 0014 LD BC,20 ; откуда 0030' 11 000A LD DE,10 0033' 3E 0A LD A,10 ; цвет линии 0035' 32 F3F2 LD (ATRBYT),A 0038' AF XOR a ; код логич. операции 0039' 32 FB02 LD (LOGOPR),A 003C' DD 21 0085 LD ix,DOGRPH 0040' FD 2A FAF7 LD iy,(EXBRASA-1) 0044' CD 001C CALL CALSLT ; рисуем линию ;--- Выход в SCREEN 0 0047' F7 RST CALLF 0048' 00 DB 0 0049' 0156 DW KillBuf ; чистка буфера 004B' F7 RST CALLF 004C' 00 DB 0 004D' 009F DW GetChr ; ждем символ 004F' F7 RST CALLF 0050' 00 DB 0 0051' 006C DW IniTxt ; SCREEN 0 0053' C7 RST 0 ; перезагрузка END Start {{anchor:n2137}} ==== 2.13.7. Вывод на печать ==== Для вывода на печать используются следующие подпрограммы BIOS: проверка статуса принтера (''LPTSTT, 0A8h'') и вывод символа на принтер (''LPOUT, 0A5h'' и ''OUTDLP, 14Dh''). Подпрограмма ''OUTDLP'' в отличие от ''LPOUT'' сообщает о ненормальном завершении операции вывода. В качестве примера приведём программу вывода на печать графического изображения при помощи ESC–последовательности ''%% ESC+"Snnnn" %%'' (см. описание системы команд принтера). Z80-Assembler Page: 1 00A8 = LPTSTT EQU 0A8h 014D = OUTDLP EQU 14Dh ORG 9000h ; === Проверка статуса принтера 9000 CDA800 CALL LPTSTT 9003 283A JR Z,NotReady ; если не может 9005 B7 OR A 9006 2837 JR Z,NotReady ; если не может ; === Вывод на принтер строки 9008 3E1B LD A,27 ; ESC 900A CD4D01 CALL OUTDLP 900D 3E42 LD A,'B' ; B 900F CD4D01 CALL OUTDLP 9012 0614 LD B,20 ; все повторить 20 раз 9014 C5 NxtEle: PUSH BC 9015 0614 LD B,20 ; кол-во элементов одного символа 9017 212B90 LD HL,Data ; адрес данных для элемента ; === Вывод одного элемента 901A E5 NxtCol: PUSH HL ; сохраняем 901B C5 PUSH BC 901C 7E LD A,(HL) ; выводим один элемент 901D CD4D01 CALL OUTDLP ; изображения 9020 381E JR C,Error ; переход при ошибке 9022 C1 POP BC ; восстанавливаем параметры 9023 E1 POP HL 9024 23 INC HL 9025 10F3 DJNZ NxtCol ; переходим к след. элементу 9027 C1 POP BC 9028 10EA DJNZ NxtEle ; переходим к след. символу 902A C9 RET 902B 1B533030 Data:DB 27,'S0014',0FFh,0FEh,0FCh,0F8h,0F0h,0E0h 902F 3134FFFE 9033 FCF8F0 9036 E0C080C0 DB 0C0h,080h,0C0h,0E0h,0F0h,0F8h,0FCh,0FEh 903A E0F0F8FCFE NotReady: ; ... ... подпрограмма обработки неготовности принтера 903F C9 RET Error: ; ... ... подпрограмма обработки ошибки принтера 9040 C9 RET END {{anchor:n214}} ===== 2.14. Ловушки ===== Работа компьютера MSX-2 практически состоит в выполнении набора определённых стандартных подпрограмм, вызываемых явно или неявно пользователем, операционной системой или прикладной программой. К этим подпрограммам, например, относятся программы ввода и запоминания кода символа, нажатого на клавиатуре, выдачи списка файлов и листинга программы, вывода символа на экран и т.п. При желании программист может дополнить или изменить любую из этих программ. Для этого разработчиками компьютера был предусмотрен механизм ловушек. Идея состоит в том, что перед тем как начать выполнение, многие стандартные программы осуществляют вызов подпрограммы-ловушки. В системной области для каждой ловушки отводится 5 байт. Список ловушек приводится в книге "Архитектура микрокомпьютера MSX-2". В обычном состоянии в ловушке записан код команды возврата из подпрограммы (''RET'') — C9h. Таким образом, при вызове ловушки управление обычно тут же возвращается назад, и работает стандартная программа. В ловушке может находиться и команда перехода (''RST'') на подпрограммы [[msx:disk_basic|]], локальной сети и других системных программ. Так [[msx:disk_basic|]] обрабатывает, например, команды работы с файлами. Если программист хочет изменить нормальный ход работы, он может в ловушку записать свои команды. Поскольку в 5 байт много не запишешь, обычно в ловушку записывают команду перехода на подпрограмму (''JP'' или ''RST''). Если после выполнения такой подпрограммы-ловушки был обычный возврат управления (''RET''), то начнётся работа стандартной подпрограммы. Приведём несколько примеров. Первый пример — обязательная чистка экрана перед выполнением команды ''[[msx:basic_dialogue_programming_language:002#list|LIST]]'' языка [[msx:basic:]]. Ловушка для ''[[msx:basic_dialogue_programming_language:002#list|LIST]]'' и ''[[msx:basic_dialogue_programming_language:002#llist|LLIST]]'' находится по адресу FF89h. Мы можем заполнить её следующими кодами: Адрес Код Команда ассембл. FF89 F7 RST 30h ; межслотовый вызов FF8A 00 DB 0 ; чистка экрана FF8B C3 00 DW 0C3h FF8D C9 RET ; возврат из ловушки На языке [[msx:basic:]] заполнить ловушку можно при помощи команды ''[[msx:basic_dialogue_programming_language:010#poke|POKE]]''. После этого перед выполнением команды ''[[msx:basic_dialogue_programming_language:002#list|LIST]]'' ловушкой будет выполняться чистка экрана. {{anchor:n2141}} ==== 2.14.1. Работа с файлами ==== Рассмотрим установку ловушки для команды [[msx:basic_dialogue_programming_language:009#files|FILES]], которая должна очистить экран и вывести список файлов. Особенность здесь заключается в том, что ловушку на эту команду устанавливает и [[msx:disk_basic|]]. Поэтому вначале будет работать наша ловушка, а затем нужно обеспечить выполнение ловушки [[msx:disk_basic|]]. Z80-Assembler Page: 1 ORG 0A000h FE7B = FileTrap EQU 0FE7Bh ; ловушка для FILES 00C6 = Posit EQU 0C6h ; установка позиции 00A2 = ChPut EQU 0A2h ; выдача символа ; === установка ловушки "JP Trap" A000 217BFE LD HL,FileTrap ; сохраняем ловушку A003 1137A0 LD DE,ForHook ; MSX-Disk-BASIC A006 010500 LD BC,5 A009 E5 PUSH HL A00A EDB0 LDIR A00C E1 POP HL ; устанавливаем свою A00D 36C3 LD (HL),0C3h ; "JP Trap" A00F 23 INC HL A010 3616 LD (HL),Low(Trap) A012 23 INC HL A013 36A0 LD (HL),High(Trap) A015 C9 RET ; ловушка установлена ; === ловушка для FILES A016 F5 Trap: PUSH AF ; сохраняем все регистры A017 C5 PUSH BC A018 D5 PUSH DE A019 E5 PUSH HL A01A 21010A LD HL,0A01h ; позиция (10,1) A01D CDC600 CALL Posit A020 213CA0 LD HL,Messg ; печатаем заголовок A023 7E NextCh: LD A,(HL) A024 B7 OR A A025 2806 JR Z,Exit A027 CDA200 CALL ChPut A02A 23 INC HL A02B 18F6 JR NextCh A02D 210202 Exit: LD HL,0202h ; позиция (2,2) A030 CDC600 CALL Posit A033 E1 POP HL ; восстанавливаем A034 D1 POP DE ; регистры A035 C1 POP BC A036 F1 POP AF ; === Выполняем ловушку FILES MSX-Disk-BASIC, возврат A037 ForHook: DEFS 5 A03C 0CF3D0C9 Messg: DB 12,'Список файлов:',0 A040 D3CFCB20 A044 C6C1CACC A048 CFD73A00 END Загрузите и выполните эту программу. Если Вы затем наберёте команду ''FILES'', будет очищен экран, выведена надпись "Список файлов:" и сам список файлов. {{anchor:n2142}} ==== 2.14.2. Работа с клавиатурой ==== В ходе работы программ клавиатура постоянно опрашивается (сканируется) системой. При нажатии какой-нибудь клавиши информация об этом временно записывается в таблицу (матрицу) сканирования клавиатуры NEWKEY (FBE5h). Каждому байту матрицы соответствует 8 клавиш. Если некоторая клавиша была нажата, соответствующий бит байта обнуляется. При этом в регистр А записывается значение 8*NS+NC, где NS — номер строки, NC — номер колонки. Матрица NEWKEY обрабатывается системой, и в зависимости от того, какие клавиши были нажаты, либо предпринимаются какие-то действия (например, CTRL+STOP), либо в буфер клавиатуры BUF (F55Eh) записывается код символа (например, Shift+$ даёт 0). Используя ловушку KEYCOD (FDCCh), можно отменить действие некоторой клавиши или провести её дополнительную обработку. Приведём пример создания ловушки, которая при нажатии клавиши STOP записывает в матрицу клавиатуры и код клавиши CTRL, а при нажатии любой другой клавиши имитирует одновременное нажатие клавиши Shift. MSX.M-80 1.00 01-Apr-85 PAGE 1 .Z80 9000 Load EQU 9000h ; загрузочный адрес FBE5 NewKey EQU 0FBE5h ; матрица клавиатуры FDCC KeyCod EQU 0FDCCh ; ловушка для клавиатуры F931 Work EQU 0F931h ; область для ловушки 009F ChGet EQU 09Fh ; ввод символа 00A2 ChPut EQU 0A2h ; вывод символа 0007 S.Stop EQU 7 ; позиции STOP в NEWKEY 0004 P.Stop EQU 4 0006 S.CTRL EQU 6 ; позиции CTRL в NEWKEY 0001 P.CTRL EQU 1 0006 S.Shift EQU 6 ; позиции Shift в NEWKEY 0000 P.Shift EQU 0 ; === Header Obj-файла 0000' ASEG 0000 FE DB 0FEh ; obj-файл 0001 9000 DW Load ; адрес загрузки 0003 9035 DW EndLoad ; конечный адрес 0005 9000 DW Start ; стартовый адрес ; === Переписываем ловушку в рабочую область .PHASE Load 9000 F3 Start: DI 9001 21 9023 LD HL,TrapModule 9004 11 F931 LD DE,Work 9007 01 0013 LD BC,EndTrap-Trap 900A ED B0 LDIR ; === Устанавливаем ловушку "JP Trap" 900C 3E C3 LD A,0C3h ; код JP 900E 32 FDCC LD (KeyCod),A 9011 21 F931 LD HL,Trap 9014 22 FDCD LD (KeyCod+1),HL 9017 FB EI ; === Ввод и вывод символов до нажатия клавиши RETURN 9018 CD 009F Next: CALL ChGet 901B FE 0D CP 13 901D C8 RET Z ; выход ! 901E CD 00A2 CALL ChPut 9021 18 F5 JR Next 9023 TrapModule EQU $ .DEPHASE ; === Ловушка для клавиатуры .PHASE Work F931 F5 Trap: PUSH AF F932 FE 3C CP 8*S.Stop+P.Stop ; STOP ? F934 20 07 JR NZ,Shift ; === "Нажимаем" CTRL и выходим F936 3E FD LD A,not(1 SHL P.CTRL) F938 32 FBEB LD (NewKey+S.CTRL),A F93B F1 POP AF F93C C9 RET ; === "Нажимаем" Shift и выходим F93D 3E FE Shift: LD A,not(1 SHL P.Shift) F93F 32 FBEB LD (NewKey+S.Shift),A F942 F1 POP AF F943 C9 RET F944 EndTrap EQU $ .DEPHASE .PHASE Load+$-7 9035 EndLoad EQU $-1 .DEPHASE END Оттранслировав эту программу ассемблером [[msx:macro-80_assembler:macro-80_assembler|M80]], получим объектную программу с расширением ''COM''. Переименуйте её в ''KEY.OBJ'' и выполните следующую программу на языке [[msx:basic:]]: 10 ON STOP GOSUB 100: STOP ON 20 BLOAD "KEY.OBJ",R 30 GOTO 30 100 PRINT "CTRL/STOP": RETURN Программа будет выводить символы, как если бы была нажата клавиша Shift. Выход из программы в кодах — по нажатию клавиши Ввод ⏎. Нажатие клавиши STOP будет обрабатываться как нажатие двух клавиш — STOP+CTRL. {{anchor:n215}} ===== 2.15. Подпрограммы интерпретатора языка MSX BASIC ===== Программирующий на языке [[msx:basic:]] может использовать все операции и функции, имеющиеся в этом языке. Многие из них могли бы быть полезны и при программировании на языке ассемблера Z80. Для обращения к операциям и функциям интерпретатора языка [[msx:basic:]] нужно знать их входные точки и правила использования. Необходимо также учитывать, что программы на ассемблере, использующие эти возможности, становятся немобильными из-за того, что входные точки не стандартизированы. Поэтому применять операции и функции [[msx:basic:]] нужно по мере достаточной необходимости. Список входных точек и правила вызова операций и функций [[msx:basic:]] приведены в книге "Архитектура микрокомпьютера MSX-2". Не забывайте, что вызов подпрограмм [[msx:basic:]] возможен, например, либо если Вы работаете в обычном режиме [[msx:basic:]] (слот 0 активен), либо если в режиме [[msx:dos:|]] выполняется межслотовый вызов MSX-BASIC BIOS. При возникновении ошибки при выполнении подпрограмм происходит обращение к программе обработки ошибки [[msx:basic:]]. Для применения своей реакции на ошибку следует использовать хук H.ERRO (FFB1h). В системной области имеются два регистра — аккумуляторы, при помощи которых интерпретатор языка [[msx:basic:]] выполняет арифметические и некоторые другие операции: * ''DAC'' ("Decimal ACcumulator" — "десятичный аккумулятор") — по адресу F7F6h, 16 байт; * ''ARG'' ("ARGument" — "аргумент") — по адресу F847h, 16 байт. Целое число в аккумуляторах размещается следующим образом: F7F6h F7F7h F7F8h F7F9h ┌────────┬────────┬─────────────┬─────────────┐ DAC │ Незначащие байты│ Младший байт│ Старший байт│ └────────┴────────┴─────────────┴─────────────┘ F847h F848h F849h F84Ah ┌────────┬────────┬─────────────┬─────────────┐ ARG │ Незначащие байты│ Младший байт│ Старший байт│ └────────┴────────┴─────────────┴─────────────┘ Вещественное число в аккумуляторах размещается следующим образом: F7F6h F7F7h F7FDh ┌──────────────┬───┬───┬───┬───┬───┬───┬───┐ DAC │Знак и порядок│ М а н т и с с а │ └──────────────┴───┴───┴───┴───┴───┴───┴───┘ F847h F848h F84Eh ┌──────────────┬───┬───┬───┬───┬───┬───┬───┐ ARG │Знак и порядок│ М а н т и с с а │ └──────────────┴───┴───┴───┴───┴───┴───┴───┘ Напомним, что мантисса числа обычной точности состоит из 6 десятичных цифр (3 байта), а двойной точности — из 14 цифр (7 байт). Подробнее о представлении и хранении чисел было рассказано в книге "Архитектура микрокомпьютера MSX-2". {{anchor:valtyp}} В ячейке VALTYP (по адресу F663h) системной области хранится тип числа, находящегося в аккумуляторе DAC. Тип закодирован следующим образом: * 2 — целое число; * 4 — вещественное число одинарной точности; * 8 — вещественное число двойной точности; * 3 — строка. {{anchor:n2151}} ==== 2.15.1. Работа с целыми числами ==== Для работы с целыми числами имеются подпрограммы сложения, вычитания, умножения, деления, вычисления остатка от деления, возведения в степень. Например, подпрограмма UMULT по адресу &h314A записывает произведение содержимого ''BC'' и ''DE'' в ''DE'', а подпрограмма INTEXP по адресу &h383F записывает в ''DAC'' степень ''HL'' содержимого ''DE''. Необходимо учитывать, что при работе подпрограммы, как правило, изменяют все регистры. Рассмотрим примеры. Z80-Assembler Page: 1 ORG 9000h 314A = UMULT EQU 314Ah 2F99 = retHL EQU 2F99h ; Умножение целых чисел ; DE := BC * DE ; изменяются A,B,C,D,E 9000 ED4B1090 LD BC,(X) ; загрузка 9004 ED5B1290 LD DE,(Y) ; операндов 9008 CD4A31 CALL UMULT ; умножаем 900B 62 LD H,D ; копируем в HL 900C 6B LD L,E ; для возврата 900D C3992F JP retHL ; с помощью USR 9010 7D00 X: DEFW 125 9012 2001 Y: DEFW 288 END Возведение целых чисел в степень. Z80-Assembler Page: 1 ORG 9000h F7F6 = DAC EQU 0F7F6h 383F = INTEXP EQU 383Fh ; DAC := DE ^ HL 9000 111400 LD DE,0014h ; загрузка 9003 210300 LD HL,0003h ; операндов 9006 CD3F38 CALL INTEXP ; степень 9009 3AF8F7 LD A,(DAC+2) ; копируем в память 900C 3200A0 LD (0a000h),A 900F 3AF9F7 LD A,(DAC+3) ; копируем в память 9012 3201A0 LD (0a001h),A 9015 C9 RET END {{anchor:n2152}} ==== 2.15.2. Работа с вещественными числами ==== При работе с вещественными числами можно использовать различные виды пересылок между ''DAC'', ''ARG'' и памятью. Например, подпрограмма ''MFM'' по адресу &h2C5C переписывает значение двойной точности из памяти, адресуемой ''HL'' в ''DAC'', а ''MOVMF'' по адресу &h2EE8 переписывает значение обычной точности из ''DAC'' в память, адресуемую ''HL''. Большое количество подпрограмм позволяет выполнять операции вычитания, сложения, нормализации, округления, умножения, возведения в степень и вычислять значения функций косинус, синус, логарифм, псевдослучайное число и других. Например, программа вычисления косинуса может выглядеть так: Z80-Assembler Page: 1 ORG 9000h F663 = VALTYP EQU 0F663h 2EBE = MOVFM EQU 2EBEh 2993 = COS EQU 2993h 2EE8 = MOVMF EQU 2EE8h ; вычисление косинуса 9000 211590 LD HL,data ; загрузка адреса 9003 3E04 LD A,4 ; тип - вещественный 9005 3263F6 LD (VALTYP),A ; 9008 CDBE2E CALL MOVFM ; данные - в DAC 900B CD9329 CALL COS ; COS(DAC) => DAC 900E 211990 LD HL,result ; загрузка адреса 9011 CDE82E CALL MOVMF ; DAC - в память 9014 C9 RET 9015 41157080 data: DB 41h,15h,70h,80h; число: 1.5708 9019 result: DS 4,0 END Кроме этого имеется набор подпрограмм сравнений и преобразований значений различных типов. Наиболее полезными из них являются подпрограммы преобразования чисел в текстовый вид для последующего вывода. Для такого преобразования предназначены подпрограммы FOUT (3425h) — неформатный вывод и PUFOUT (3426h) — форматный вывод; Эти подпрограммы обрабатывают число, находящееся в ''DAC''. Адрес полученной строки символов (уменьшенный на 1) записывается в ''HL''. В регистре ''А'' записывается формат преобразования. Содержимое его битов определяет следующее: * бит 7: если 1, то вывод осуществляется по формату; * бит 6: если 1, то через каждые 3 цифры вставляются запятые; * бит 5: если 1, то первые нули нужно заменить на символ "*"; * бит 4: если 1, то перед числом вставить символ "$"; * бит 3: если 1, то число выводится всегда со знаком; * бит 2: если 1, то вставить знак после числа; * бит 1: не используется; * бит 0: если 0, то число выводится с фиксированной точкой; если 1, то число выводится с плавающей точкой; В регистре ''B'' должно быть количество цифр перед точкой +2. В регистре ''C'' — количество цифр после точки +1. Приведём пример программы. Z80-Assembler Page: 1 3426 = FOUT EQU 3426h F663 = VALTYP EQU 0F663h F7F6 = DAC EQU 0F7F6h 2993 = Cos EQU 2993h 00A2 = ChPut EQU 0A2h ORG 8100h ; запись числа в DAC 8100 212781 LD HL,data 8103 11F6F7 LD DE,DAC 8106 010400 LD BC,4 8109 EDB0 LDIR ; переписали в DAC 810B 3E04 LD A,4 ; вещественное число 810D 3263F6 LD (VALTYP),A ; установили тип ; вычисляем косинус 8110 CD9329 CALL Cos ; преобразование числа (DAC) в строку ; по формату 8113 3E88 LD A,10001000b ; формат 8115 0603 LD B,3 ; до точки 8117 0E05 LD C,5 ; после точки 8119 CD2634 CALL FOUT ; адрес строки - в (HL)+1 ; выводим ее на экран 811C 23 Next: INC HL 811D 7E LD A,(HL) 811E B7 OR A ; код символа ? 811F 2805 JR Z,Exit ; если ноль - все ! 8121 CDA200 CALL ChPut ; иначе - вывод 8124 18F6 JR Next 8126 C9 Exit: RET 8127 43123456 data: DB 43h,12h,34h,56h END Для числа-аргумента этой программы 123.456 будет вычислено и напечатано в качестве результата значение -0.5944. {{anchor:n216}} ===== 2.16. Подпрограммы BDOS ===== При выполнении программ типа ''.COM'' ПЗУ интерпретатора языка [[msx:basic:]] обычно отключено. Однако операционная система MSX-DOS имеет свой набор стандартных функций (подпрограмм) BDOS BIOS, при помощи которых можно осуществлять операции ввода/вывода на экран, принтер, на диски, работать с клавиатурой и выполнять некоторые другие операции. Их общее количество — около пятидесяти. Список системных функций BDOS приведён в книге "Архитектура микрокомпьютера MSX-2". Каждая функция имеет свой код. Для вызова функции BDOS необходимо: - загрузить код функции в регистр C; - загрузить параметры в соответствующие регистры; - вызвать подпрограмму по адресу 5. Вызов функций BDOS поддерживается и интерпретатором [[msx:disk_basic|]] на машинах, где он установлен. При этом код функции тоже загружается в регистр ''C'', но вызывать нужно адрес &hF37D. Например, функция с кодом 2 выводит на экран символ, код которого записан в регистр ''E''. Функция с кодом 9 выводит на экран строку. Причём адрес строки должен быть записан в регистровую пару ''DE'', а конец строки обозначается символом "$". Ниже приведён листинг программы, вызывающей эти функции. MSX.M-80 1.00 01-Apr-85 PAGE 1 .Z80 0005 BDOS EQU 5 0002 PUTSCR EQU 2 0009 PUTSTR EQU 9 0000' 0E 02 LD C,PUTSCR 0002' 1E 41 LD E,65 0004' CD 0005 CALL BDOS 0007' 0E 02 LD C,PUTSCR 0009' 1E 42 LD E,66 000B' CD 0005 CALL BDOS 000E' 0E 09 LD C,PUTSTR 0010' 11 0017' LD DE,string 0013' CD 0005 CALL BDOS 0016' C9 RET 0017' 68 65 6C 6C string: DB "Hello, fellows!$" 001B' 6F 2C 20 66 001F' 65 6C 6C 6F 0023' 77 73 20 21 24 END {{anchor:n217}} ===== 2.17. Сетевые функции ===== [[msx:yamaha_local_network:yamaha_local_network|Локальная сеть КУВТ-2]] имеет систему стандартных сетевых функций ввода/вывода (Net ROM BIOS), включающую в себя функции инициализации сети, проверки номера компьютера, передачи/приёма программ и данных, чтения/записи из памяти своего компьютера или компьютера ученика и другие. Сетевые функции могут быть вызваны как при работе в режиме [[msx:basic:]], так и при работе в [[msx:dos:|]]. Список сетевых функций дан в книге "Архитектура микрокомпьютера MSX-2". Для проверки, имеет ли система сетевое ПЗУ, можно посмотреть, хранится ли по вызываемому адресу "заглушка" RST30 (F7h) или записан ли идентификатор "RNT" в сетевом ПЗУ по адресам 4040h-4042h. Для инициализации сети в стандартной [[msx:dos:|]] необходимо вызвать подпрограмму по адресу F98Eh. Для вызова сетевой функции нужно поместить код функции (01h-1Ah) в регистр ''C'', начальный адрес блока параметров в регистровую пару DE и вызвать подпрограмму по адресу F989h. Для окончания работы с сетью вызывается подпрограмма по адресу F984h. Посмотрите пример программы, работающей с локальной сетью в стандартной [[msx:dos:|]]. MSX.M-80 1.00 01-Apr-85 PAGE 1 .Z80 F98E NETINIT EQU 0F98Eh F989 NETFUNC EQU 0F989h F984 NETEND EQU 0F984h 0005 BDOS EQU 5 0009 PUTSTR EQU 9 ; === Netinit & Who ? 0000' CD F98E CALL NETINIT 0003' 0E 06 LD C,6 ; Who ? 0005' CD F989 CALL NETFUNC ; === Check computer number 0008' 32 0048' LD (WHO),A 000B' B7 OR A ; jump, if not a teacher 000C' 20 08 JR NZ,putnum ; === Send message to Pupils 000E' 0E 0D LD C,0Dh 0010' 11 0039' LD DE,message 0013' CD F989 CALL NETFUNC 0016' CD F984 putnum: CALL NETEND 0019' 0E 09 LD C,PUTSTR 001B' 11 003E' LD DE,number 001E' 3A 0048' LD A,(WHO) 0021' C6 30 ADD A,'0' 0023' 32 0048' LD (WHO),A 0026' CD 0005 CALL BDOS 0029' C9 RET 002A' 48 65 6C text: DEFM 'Hello, fellows!' 002E' 6C 6F 2C 20 66 0032' 65 6C 6C 6F 0036' 77 73 21 0039' 00 message: DB 0 ; всем 003A' 002A' DEFW text ; адрес 003C' 000F DEFW 15 ; длина 003E' 4E 75 6D number: DEFM 'Number is ' 0042' 62 65 72 20 69 0046' 73 20 0048' 00 WHO: DB 0 0049' 24 DB '$' END Если Вы используете нестандартную операционную систему, то она может иметь другие точки входа в NET BIOS или вообще их не иметь. В этом случае можно обратиться непосредственно ко входным точкам функций локальной сети. Они находятся по адресам: * NETINIT — 33/401Ch, * NETFUNC — 33/4019h, * NETEND — 33/4016h. Программа, приведённая выше, может быть с учётом этого переписана следующим образом: MSX.M-80 1.00 01-Apr-85 PAGE 1 .Z80 0005 BDOS EQU 5 0009 PUTSTR EQU 9 0000' CD 002A' CALL NETINIT 0003' 0E 06 LD C,6 ; Who ? 0005' CD 002F' CALL NETFUNC ; === Check computer number 0008' 32 0057' LD (WHO),A 000B' B7 OR A ; === Jump, if not a teacher 000C' 20 08 JR NZ,putnum ; === Send message to Students 000E' 0E 0D LD C,0Dh 0010' 11 0048' LD DE,message 0013' CD 002F' CALL NETFUNC 0016' CD 0034' putnum:CALL NETEND 0019' 0E 09 LD C,PUTSTR 001B' 11 004D' LD DE,number 001E' 3A 0057' LD A,(WHO) 0021' C6 30 ADD A,'0' 0023' 32 0057' LD (WHO),A 0026' CD 0005 CALL BDOS 0029' C9 RET 002A' F7 NETINIT: RST 30h 002B' 8F DB 8Fh 002C' 401C DW 401Ch 002E' C9 RET 002F' F7 NETFUNC: RST 30h 0030' 8F DB 8Fh 0031' 4019 DW 4019h 0033' C9 RET 0034' F7 NETEND: RST 30h 0035' 8F DB 8Fh 0036' 4016 DW 4016h 0038' C9 RET ; --------------------- 0039' 48 65 6C 6C text:DEFM 'Hello, fellows!' 003D' 6F 2C 20 66 0041' 65 6C 6C 6F 0045' 77 73 21 0048' 00 message:DB 0 0049' 0039' DEFW text 004B' 000F DEFW 15 004D' 4E 75 6D number: DEFM 'Number is ' 0051' 62 65 72 20 69 0055' 73 20 0057' 00 WHO: DB 0 0058' 24 DB '$' END В заключение приведём листинг программы Host.mac. Эта программа состоит из двух частей. Первая часть, вызываемая оператором ''[[msx:basic:usr|USR]]'' из [[msx:basic:]] по адресу &hDA00, инициализирует сеть; вторая часть, запускаемая по адресу &hDA03, после проверки сети функцией Check читает два байта из сетевой памяти компьютера ученика. Номер компьютера ученика передаётся второй подпрограмме, вызов которой осуществляется следующим образом: NC = ... номер компьютера ученика W = VARPTR(NC): W = USR(W) IF W=0 THEN ... компьютер подключен, можно брать содержимое ячеек по адресам &hF406,&hF407 IF NC>256 THEN ... ученику разрешено передавать сообщения другим ученикам, NC=NC-256 Z80-Assembler Page: 1 ORG 0DA00h ; === Вызовы двух частей программы: DA00 C306DA JP Initial DA03 C318DA JP ChkNet 401C = NETINIT EQU 401Ch 4019 = NETFUNC EQU 4019h ; === Инициирование сети DA06 F7 Initial: RST 30h DA07 8F DB 8Fh DA08 1C40 DW NETINIT ; === Разрешение прерываний сети (INTON) DA0A 0E01 LD C,01 DA0C F7 RST 30h DA0D 8F DB 8Fh DA0E 1940 DW NETFUNC ; === Начало упорядоченного опроса (PON) DA10 0E03 LD C,03 DA12 F7 RST 30h DA13 8F DB 8Fh DA14 1940 DW NETFUNC DA16 FB EI DA17 C9 RET ; =========================================================== ; Программа проверки подключения к сети ; и чтения значений из сетевого ОЗУ ученика ; Вход: сеть инициализирована ; HL - адрес ячейки с номером компьютера NC ; (при помощи передачи параметров 2F8Ah) ; Выход: IS_OFF - NC выключен ; IS_ON - NC включен ; F406h,F407h - содержимое ячеек NRAM ученика ; NC <= NC + 100h, если ученику можно работать в сети ; =========================================================== 0000 = IS_ON EQU 0 FFFF = IS_OFF EQU 0FFFFh 7900 = FROM1 EQU 7900h 7901 = FROM2 EQU 7901h ; сетевые адреса ученика F406 = First EQU 0F406h ; RAM учителя F407 = Second EQU 0F407h DA18 CD8A2F ChkNet: CALL 2F8Ah ; взять аргумент, записать в HL DA1B E5 PUSH HL ; запомнить адрес NC ; === Check: Кто подключен к сети ? DA1C 0E17 LD C,17h DA1E F7 RST 30h DA1F 8F DB 8Fh DA20 1940 DW 4019h DA22 FB EI ; === HL - подключены к сети, DE - разрешение работы DA23 C1 POP BC ; адрес NC DA24 C5 PUSH BC DA25 0A LD A,(BC) ; A <-- NC DA26 3D DEC A ; A <-- NC-1 DA27 2821 JR Z,ChkL ; если NC=1 DA29 FE08 CP 8 DA2B FA43DA JP M,Chk2_7 ; если NC=2..7 DA2E D608 SUB 8 ; 0.6= NC #9..15 DA30 2807 JR Z,ChkH ; если NC=9 DA32 47 LD B,A DA33 CB3C NextB: SRL H DA35 CB3A SRL D DA37 10FA DJNZ NextB ; сдвиги по NC DA39 CB44 ChkH: BIT 0,H ; бит NC - нулевой DA3B 201D JR NZ,C_OFF ; компьютер отключен DA3D CB42 BIT 0,D DA3F 2020 JR NZ,C_ON ; диалог ученику запрещен DA41 280F JR Z,ENACOM ; диалог ученику разрешен ; === Проверка для компьютеров со 2-го по 7-й DA43 47 Chk2_7: LD B,A ; контроль NC = 2..7 DA44 CB3D SRL L DA46 CB3B SRL E DA48 10FA DJNZ Chk2_7+1 ; сдвиги по NC DA4A CB45 ChkL: BIT 0,L ; бит NC - нулевой DA4C 200C JR NZ,C_OFF ; компьютер отключен DA4E CB43 BIT 0,E DA50 200F JR NZ,C_ON ; диалог ученику запрещен ; === Установка флага "диалог разрешен": NC <= NC+100h DA52 E1 ENACOM: POP HL DA53 E5 PUSH HL DA54 23 INC HL DA55 3E01 LD A,1 DA57 77 LD (HL),A ; флаг "работа разрешена" DA58 1807 JR C_ON ; теперь - компьютер вкл. ;=== Компьютер отключен от сети DA5A E1 C_OFF: POP HL DA5B 21FFFF LD HL,IS_OFF DA5E C3992F JP 2F99h ; возврат в MSX-BASIC ;=== Читаем значения из сетевых ячеек DA61 E1 C_ON: POP HL ; адрес NC DA62 E5 PUSH HL DA63 7E LD A,(HL) DA64 3298DA LD (Block),A ; номер ученика DA67 210079 LD HL,FROM1 ; адрес 1-й ячейки, DA6A 2299DA LD (Block+1),HL ; откуда брать из NRAM ; === Вызов PEEK из NRAM ученика DA6D 0E12 LD C,12h DA6F 1198DA LD DE,Block ; адрес блока парам. DA72 F7 RST 30h DA73 8F DB 8Fh DA74 1940 DW 4019h DA76 FB EI DA77 38E1 JR C,C_OFF ; если был сбой ввода/вывода DA79 3206F4 LD (First),A ; записать в свою память ; === 2-я ячейка DA7C 210179 LD HL,FROM2 ; адрес 2-й ячейки, DA7F 2299DA LD (Block+1),HL ; откуда брать из NRAM ; === Вызов PEEK из NRAM ученика DA82 0E12 LD C,12h DA84 1198DA LD DE,Block ; адрес блока параметров DA87 F7 RST 30h DA88 8F DB 8Fh DA89 1940 DW 4019h DA8B FB EI DA8C 38CC JR C,C_OFF ; если был сбой ввода/вывода DA8E 3207F4 LD (Second),A ; записать в свою память DA91 E1 POP HL DA92 210000 LD HL,IS_ON ; компьютер ученика включен DA95 C3992F JP 2F99h ; возврат в MSX-BASIC ;=== Параметры сетевого вызова DA98 Block: DS 1,0 ; N ученика DA99 DS 2,0 ; адрес ячейки DA9B 0101 DB 1,1 ; сетевая память ученика END {{anchor:n218}} ===== 2.18. Работа с портами ввода/вывода ===== Перейдём к командам ввода/вывода. В процессоре Z80 предусмотрен ряд команд, позволяющих осуществлять не только побайтовый ввод/вывод, но и ввод/вывод блока. a) OUT (порт),a ; вывод в порт байта из A b) IN a,(порт) ; ввод в A из порта c) OUT (c),r ; вывести в порт, номер которого ; в регистре C, содержимое регистра r d) IN r,(C) ; ввести байт в регистр r из порта, номер которого в регистре C e) INI OUT (C),(HL) INC HL DEC B f) IND OUT (C),(HL) DEC HL DEC B g) INIR OUT (C),(HL) INC HL DEC B если B не равно 0, то повторить h) INDR OUT (C),(HL) DEC HL DEC B если B не равно 0, то повторить Перед работой с портами ввода/вывода рекомендуется отключать прерывания. Особенно это касается работы с портами видеопроцессора. Примеры работы с портами будут даны ниже. {{anchor:n219}} ===== 2.19. Работа с видеорегистрами и видеопамятью ===== Вначале рассмотрим способы доступа к видеоинформации. Как уже говорилось, для записи информации в регистры видеопроцессора или видеопамять или чтения из них используются порты ввода/вывода — четыре для чтения и четыре для записи. Узнать номер первого порта для чтения можно в ячейке ПЗУ 00/0006, а для записи — в ячейке 00/0007. Обычно для работы с VDP используются порты с номерами 98h..9Bh. При этом не забывайте в начале подпрограммы или перед её вызовом отключать прерывания командой DI, а в конце работы с VDP — снова их активировать командой EI. {{anchor:n2191}} ==== 2.19.1. Порядок чтения и записи информации ==== Прямая запись в регистр видеопроцессора осуществляется в следующем порядке: ДАННЫЕ -> порт 99h 10rr rrrr -> порт 99h (r..r - номер регистра) Например, запись в регистр VDP #2: MSX.M-80 1.00 01-Apr-85 PAGE 1 .Z80 0000' 3E 03 LD A,00000011b ; PNT 0002' D3 99 OUT (99h),A 0004' 3E 82 LD A,10000010b ; VDP(2) 0006' D3 99 OUT (99h),A 0008' C9 RET END Ещё один пример — подпрограмма записи в регистр VDP данных из регистра ''B'', номер регистра VDP в регистре ''C'': Z80-Assembler Page: 1 0000 F5 wrrvdp:PUSH Af ; сохранить A в стеке 0001 78 LD A,b ; выводим в порт 99h 0002 D399 OUT (99h),A ; данные 0004 79 LD A,c ; выводим в порт 99h 0005 F680 OR 80h ; номер регистра VDP 0007 D399 OUT (99h),A ; выставив 7 бит в 1 0009 F1 POP Af ; вытаскиваем A из стека 000A C9 RET ; возврат END Косвенная запись в регистр видеопроцессора с автоматическим увеличением номера регистра осуществляется так: 00rr rrrr -> R17 (r..r - номер регистра R) ДАННЫЕ для R -> порт 9Bh ДАННЫЕ для R + 1 -> порт 9Bh ДАННЫЕ для R + 2 -> порт 9Bh … … … Запись в регистр 17 осуществляется прямым способом, косвенный запрещён. Например, запись нуля в регистры 8,9: MSX.M-80 1.00 01-Apr-85 PAGE 1 .Z80 0000' 3E 08 LD A,8 0002' D3 99 OUT (99h),A 0004' 3E 91 LD A,80h OR 17 ; VDP(17) <= 8 0006' D3 99 OUT (99h),A ; 0008' AF XOR A 0009' D3 9B OUT (9Bh),A ; VDP(8) <= 0 000B' D3 9B OUT (9Bh),A ; VDP(9) <= 0 000D' C9 RET END Косвенная запись в регистр видеопроцессора без автоматического увеличения номера регистра. 10rr rrrr -> R17 ДАННЫЕ для R -> порт 9Bh ДАННЫЕ для R -> порт 9Bh … … … Чтение из регистра статуса (состояния) видеопроцессора (0..9). 0000 rrrr -> R15 ДАННЫЕ <- порт 99h После того как данные были прочитаны, необходимо записать в R#15 ноль и разрешить прерывания. Например, чтобы узнать, было ли наложение двух спрайтов, можно проанализировать пятый бит регистра статуса #0: MSX.M-80 1.00 01-Apr-85 PAGE 1 .Z80 0000' AF XOR A 0001' D3 99 OUT (99h),A 0003' 3E 8F LD A,8Fh ; VDP(15) <= 0 0005' D3 99 OUT (99h),A 0007' DB 99 IN A,(99h) 0009' CB 6F BIT 5,A ; Было ли столкновение? 000B' C9 RET END Запись в регистры палитры Регистры палитры (0..15) являются девятибитными. Поэтому запись в них осуществляется следующим образом: НОМЕР ПАЛИТРЫ -> R16 0rrr 0bbb -> порт 9Ah (rrr - КРАСНЫЙ, bbb - СИНИЙ) 0000 0ggg -> порт 9Аh (ggg - ЗЕЛЕНЫЙ ) После записи содержимое R#16 автоматически увеличивается на 1. Поэтому возможно простое обновление всех палитр. Чтение/запись из/в видеопамяти VRAM/ERAM по двоичному адресу ''b bbhh hhhh cccc cccc''. * а) Установить банк VRAM: 00.. .... -> R45 (для VRAM) 01.. .... -> R45 (для ERAM) [ в MSX-2 отсутствует ] Содержимое регистра R45 не меняется при обращении к памяти, поэтому нет необходимости каждый раз переопределять шестой бит. * б) Установить адрес видеопамяти: 0000 0bbb -> R14 cccc cccc -> порт 99h 00hh hhhh -> порт 99h (для чтения из видеопамяти) 01hh hhhh -> порт 99h (для записи в видеопамять) * в) Писать в порт 98h последовательные байты данных или читать из этого порта в зависимости от выбранного режима. Адрес видеопамяти при этом автоматически увеличивается. Для доступа к VRAM можно также использовать соответствующие команды VDP. Например, необходимо прочитать из видеопамяти 200 байт и записать их, начиная с адреса 0C000h; начальный адрес видеопамяти равен 0: MSX.M-80 1.00 01-Apr-85 PAGE 1 .Z80 ; === Установка банки VRAM/ERAM 0000' AF XOR A 0001' D3 99 OUT (99h),A 0003' 3E AD LD A,80h OR 45 ; VDP(45) <= 0 0005' D3 99 OUT (99h),A 0007' AF XOR A 0008' D3 99 OUT (99h),A 000A' 3E 8E LD A,8Eh ; VDP(14) <= 0 000C' D3 99 OUT (99h),A ; === Копируем 000E' 21 0000 LD HL,0 ; начальный адрес VRAM 0011' 11 C000 LD DE,0C000h ; начальный адрес RAM 0014' 06 C8 LD b,200 ; длина блока 0016' 7D LD A,L ; адрес начала памяти 0017' D3 99 OUT (99h),A ; младш. байт адреса VRAM 0019' 7C LD A,h 001A' D3 99 OUT (99h),A ; старший байт 001C' DB 98 blreAd:IN A,(98h) ; вводим байт из ук.адр. 001E' 12 LD (DE),A ; записываем в память 001F' 13 INC DE ; подгот. след.адрес RAM 0020' 10 FA DJNZ blreAd ; b=b-1, если b<>0, ; то повторить blreAd 0022' C9 RET END Эту подпрограмму можно написать и по-другому: Z80-Assembler Page: 1 0000 210000 LD HL,0 ; начальный адрес VRAM 0003 1100C0 LD DE,0C000h ; начальный адрес RAM 0006 06C8 LD b,200 ; длина блока 0008 EB EX DE,HL ; обменять HL и DE 0009 7B LD A,e ; адрес начала памяти 000A D399 OUT (99h),A ; младш. байт адреса VRAM 000C 7A LD A,d 000D D399 OUT (99h),A ; старший байт 000F 0E98 LD c,98h ; номер порта вв./вывода 0011 EDB2 INIR ; ввести данные 0013 C9 RET END При изменении типа экрана часто требуется восстанавливать таблицу шаблонов (образов) символов PGT. Её можно извлечь из ROM BIOS по адресу, который записан в системной области в трехбайтовой ячейке F91Fh (слот + адрес ROM PGT). Обычно адрес ROM PGT равен 00/1BBFh. Теперь попробуем написать подпрограмму пересылки блока данных из ROM PGT в VRAM PGT. Ранее для подобных действий мы пользовались подпрограммами BIOS. Z80-Assembler Page: 1 ORG 9000h ; ==== ROM PGT => VRAM PGT ; [HL] - адрес ROM PGT ; [DE] - адрес VRAM PGT ; [bc] - длина блока 9000 21BF1B LD HL,1BBFh ; ROM PGT 9003 110010 LD DE,1000h ; TEXT-2 PGT 9006 010008 LD BC,2048 ; длина 9009 7B LD A,E ; выбрасываем младший 900A D399 OUT (99h),A ; байт адреса VRAM 900C 7A LD A,D ; выбрасываем старший 900D F640 OR 40h ; байт адреса VRAM, 900F D399 OUT (99h),A ; установив 6 бит в 1 9011 7E LDirmv: LD A,(HL) ; запис. по адресу VRAM 9012 D398 OUT (98h),A ; данные из (HL) 9014 23 INC HL ; следующий адрес RAM 9015 0B DEC BC ; уменьшаем длину 9016 78 LD A,B ; если длина не равна 0, 9017 B1 OR C ; то повторить 9018 20F7 JR NZ,LDirmv 901A C9 RET END В завершение параграфа приведём пример достаточно большой программы, устанавливающей 80-символьный текстовый режим. В верхней части экрана мигает блок с текстом ''width 80''. Выход из программы — по CTRL+STOP. Z80-Assembler Page: 1 ORG 9000h 9000 F3 DI ; отменяем прерывания ; === Установка текстового режима 80 символов ; Регистры VDP 0,1,8,9 9001 3E04 LD A,00000100b 9003 D399 OUT (99h),A 9005 3E80 LD A,10000000b ; VDP(0) 9007 D399 OUT (99h),A 9009 3E70 LD A,01110000b 900B D399 OUT (99h),A 900D 3E81 LD A,10000001b ; VDP(1) 900F D399 OUT (99h),A 9011 AF XOR A 9012 D399 OUT (99h),A 9014 3E88 LD A,10001000b ; VDP(8) 9016 D399 OUT (99h),A 9018 AF XOR A 9019 D399 OUT (99h),A 901B 3E89 LD A,10001001b ; VDP(9) 901D D399 OUT (99h),A ; === Установка базовых адресов PNT, PGT, CT 901F 3E03 LD A,00000011b ; PNT 9021 D399 OUT (99h),A 9023 3E82 LD A,10000010b ; VDP(2) <= 0 9025 D399 OUT (99h),A ; 9027 3E02 LD A,00000010b ; PGT 9029 D399 OUT (99h),A 902B 3E84 LD A,10000100b ; VDP(4) <= 2 (* 800h) 902D D399 OUT (99h),A ; 902F AF XOR A ; CT 9030 D399 OUT (99h),A 9032 3E8A LD A,10001010b ; VDP(10) <= 0 9034 D399 OUT (99h),A ; 9036 3E27 LD A,00100111b ; CT 9038 D399 OUT (99h),A 903A 3E83 LD A,10000011b ; VDP(3) <= 27h 903C D399 OUT (99h),A ; ; === Установка цветов и мигания 903E 3EFC LD A,11111100b ; цвета текста и фона 9040 D399 OUT (99h),A 9042 3E87 LD A,87h ; VDP(7) <= 15,12 9044 D399 OUT (99h),A ; 9046 3E1D LD A,00011101b ; цвета для мигания 9048 D399 OUT (99h),A 904A 3E8C LD A,8Ch ; VDP(12) <= 1,13 904C D399 OUT (99h),A ; 904E 3E77 LD A,01110111b ; время вкл/выкл мигания 9050 D399 OUT (99h),A 9052 3E8D LD A,8Dh ; VDP(13) 9054 D399 OUT (99h),A ; ; === Установка банки VRAM/ERAM 9056 AF XOR A 9057 D399 OUT (99h),A 9059 3EAD LD A,80h OR 45 ; VDP(45) <= 0 905B D399 OUT (99h),A ; 905D AF XOR A 905E D399 OUT (99h),A 9060 3E8E LD A,8Eh ; VDP(14) <= 0 9062 D399 OUT (99h),A ; ; ==== ROM PGT => VRAM PGT ; [HL] - адрес ROM PGT ; [DE] - адрес VRAM PGT ; [bc] - длина блока 9064 21BF1B LD HL,1BBFh 9067 110010 LD DE,1000h 906A 010008 LD BC,2048 906D 7B LD A,e ; выбрасываем младший 906E D399 OUT (99h),A ; байт адреса VRAM 9070 7A LD A,d ; выбрасываем старший 9071 F640 OR 40h ; байт адреса VRAM, 9073 D399 OUT (99h),A ; установив 6 бит в 1 9075 7E LDirmv: LD A,(HL) ; запис. по адресу VRAM 9076 D398 OUT (98h),A ; данные из (HL) 9078 23 INC HL ; следующий адрес RAM 9079 0B DEC bc ; уменьшаем длину 907A 78 LD A,b ; если длина не равна 0, 907B B1 OR C ; то повторить 907C 20F7 JR NZ,LDirmv ; ==== Очистить VRAM CT нулем (нет мигания) ; [DE] - адрес VRAM ; [bc] - длина блока 907E 110008 LD DE,800h 9081 010E01 LD BC,270 9084 7B LD A,E ; выбрасываем младший 9085 D399 OUT (99h),A ; байт адреса VRAM 9087 7A LD A,D ; выбрасываем старший 9088 F640 OR 40h ; байт адреса VRAM, 908A D399 OUT (99h),A ; установив 6 бит в 1 908C AF LDirCT: XOR A ; запис. по адресу VRAM 908D D398 OUT (98h),A ; ноль 908F 0B DEC BC ; уменьшаем длину 9090 78 LD A,B ; если длина не равна 0, 9091 B1 OR C ; то повторить 9092 20F8 JR NZ,LDirCT ; ==== Mерцание блока 9094 3E0E LD A,14 ; выбрасываем младший 9096 D399 OUT (99h),A ; байт адреса VRAM 9098 3E08 LD A,08h ; выбрасываем старший 909A F640 OR 40h ; байт адреса VRAM, 909C D399 OUT (99h),A ; установив 6 бит в 1 909E 3E1F LD A,1Fh ; запис. по адресу VRAM 90A0 D398 OUT (98h),A ; ноль 90A2 3E0F LD A,15 ; выбрасываем младший 90A4 D399 OUT (99h),A ; байт адреса VRAM 90A6 3E08 LD A,08h ; выбрасываем старший 90A8 F640 OR 40h ; байт адреса VRAM 90AA D399 OUT (99h),A ; установив 6 бит в 1 90AC 3EF8 LD A,0F8h ; запис. по адресу VRAM 90AE D398 OUT (98h),A ; ноль ; ==== Пробел (20h) => VRAM PNT ; [DE] - адрес VRAM ; [bc] - длина блока 90B0 110000 LD DE,0 90B3 018007 LD BC,1920 90B6 7B LD A,E ; выбрасываем младший 90B7 D399 OUT (99h),A ; байт адреса VRAM 90B9 7A LD A,D ; выбрасываем старший 90BA F640 OR 40h ; байт адреса VRAM, 90BC D399 OUT (99h),A ; установив 6 бит в 1 90BE 3E20 LDiPNT: LD A,20h ; запис. по адресу VRAM 20h 90C0 D398 OUT (98h),A 90C2 0B DEC BC ; уменьшаем длину 90C3 78 LD A,B ; если длина не равна 0, 90C4 B1 OR C ; то повторить 90C5 20F7 JR NZ,LDiPNT ; ==== Надпись из RAM => VRAM PNT ; [HL] - адрес RAM ; [DE] - адрес VRAM ; [BC] - длина блока 90C7 21F190 LD HL,tit 90CA 117400 LD DE,116 90CD 019808 LD BC,0898h 90D0 7B LD A,E ; выбрасываем младший 90D1 D399 OUT (99h),A ; байт адреса VRAM 90D3 7A LD A,D ; выбрасываем старший 90D4 F640 OR 40h ; байт адреса VRAM 90D6 D399 OUT (99h),A ; установив 6 бит в 1 90D8 EDB3 OTIR ; переписываем блок ; === Ширина экрана - 80 символов 90DA 3E50 LD A,80 90DC 32B0F3 LD (0F3B0h),A ; === Локализуем курсор в (1,1) 90DF 3E01 LD A,1 90E1 32A9FC LD (0FCA9h),A 90E4 210101 LD HL,0101h 90E7 CDC600 CALL 0C6h ; === Ждем нажатия CTRL/STOP 90EA CDB700 Again: CALL 0B7h 90ED 30FB JR NC,Again 90EF FB EI 90F0 C9 RET tit: DB 'Width 80' 90F1 57696474 90F5 68203830 END {{anchor:n2192}} ==== 2.19.2. Использование команд видеопроцессора ==== FIXME MSX-VIDEO выполняет основные графические операции, которые называются командами VDP. Они доступны в режимах VDP ''GRAPHIC 4..7'', а для работы с ними используются специальные регистры VDP. При работе с командами VDP используется особая координатная сетка. В ней нет деления на страницы, доступны все 128 Kб VRAM. Она приведена на рис.19.1. GRAPHIC 4 (SCREEN 5) Адрес GRAPHIC 5 (SCREEN 6) ┌──────────────────┐ 00000H ┌──────────────────┐ │(0,0) (255,0)│ │ │(0,0) (511,0)│ │ │ │ │ │ │ Стр. 0 │ │ │ Стр. 0 │ │ │ │ │ │ │(0,255) (255,255)│ │ │(0,255) (511,255)│ ├──────────────────┤ 08000H ├──────────────────┤ │(0,256) (255,256)│ │ │(0,256) (511,256)│ │ │ │ │ │ │ Стр. 1 │ │ │ Стр. 1 │ │ │ │ │ │ │(0,511) (255,511)│ │ │(0,511) (511,511)│ ├──────────────────┤ 10000H ├──────────────────┤ │(0,512) (255,512)│ │ │(0,512) (511,512)│ │ │ │ │ │ │ Стр. 2 │ │ │ Стр. 2 │ │ │ │ │ │ │(0,767) (255,767)│ │ │(0,767) (511,767)│ ├──────────────────┤ 18000H ├──────────────────┤ │(0,768) (255,768)│ │ │(0,768) (511,768)│ │ │ │ │ │ │ Стр. 3 │ │ │ Стр. 3 │ │ │ │ │ │ │(0,1023)(255,1023)│ │ │(0,1023)(511,1023)│ └──────────────────┘ 1FFFFH └──────────────────┘ GRAPHIC 7 (SCREEN 8) GRAPHIC 6 (SCREEN 7) ┌──────────────────┐ 00000H ┌──────────────────┐ │(0,0) (255,0)│ │ │(0,0) (511,0)│ │ │ │ │ │ │ Стр. 0 │ │ │ Стр. 0 │ │ │ │ │ │ │(0,255) (255,255)│ │ │(0,255) (511,255)│ ├──────────────────┤ 10000H ├──────────────────┤ │(0,256) (255,256)│ │ │(0,256) (511,256)│ │ │ │ │ │ │ Стр. 1 │ │ │ Стр. 1 │ │ │ │ │ │ │(0,511) (255,511)│ │ │(0,511) (511,511)│ └──────────────────┘ 1FFFFH └──────────────────┘ Рис.19.1. Координатная система VRAM Имеется 12 типов команд VDP. Они были описаны в приложении к книге "Архитектура микрокомпьютера MSX-2". Параметры для выполнения команды записываются в регистры с 32–го по 45–й. Команда начинает выполняться после установки в 1 нулевого бита регистра статуса #2. После того как выполнение команды закончится, этот бит сбрасывается в ноль. Для прекращения выполнения текущей команды можно выполнить команду ''VDP STOP''. Для ускорения выполнения команд VDP рекомендуется на время запрещать отображение спрайтов путём установки первого бита регистра #8. Можно также отключать при начальной загрузке изображение на экране. Приведём пример программы, выполняющей команду HMMC VDP быстрой пересылки RAM ⇒ VRAM. MSX.M-80 1.00 01-Apr-85 PAGE 1 .Z80 ; === Выполнение команды VDP HMMC C000 ABegin EQU 0C000h 0000' ASEG 0000 FE DB 0FEh ; файл типа Obj 0001 C000 DW ABegin ; загрузочный адрес 0003 C08E DW AEnd ; конечный адрес 0005 C000 DW AStart ; стартовый адрес .Phase ABegin C000 Astart EQU $ 0000 XK EQU 0 0000 YK EQU 0 0064 XS EQU 100 0064 YS EQU 100 ; === Вызов HMMC C000 DD 21 C07F LD ix,DataForVram C004 21 0000 LD HL,XK*100h+YK C007 11 6464 LD DE,XS*100h+YS ; === Пересылка RAM [IX] => VRAM (H,L)-(D,E) C00A CD C00E CALL HMMC C00D C9 RET ; === Команда VDP HMMC C00E F3 HMMC: DI ; запрет прерываний C00F CD C071 CALL WaitVDP ; ожидание конца работы команды C012 3E 24 LD A,36 ; номер первого регистра C014 D3 99 OUT (99h),A C016 3E 91 LD A,17+80h C018 D3 99 OUT (99h),A ; VDP(17) <= 36 C01A 0E 9B LD c,9Bh ; C=9Bh для косвенного доступа C01C AF XOR A ; к регистрам VDP C01D ED 61 OUT (c),H ; X младший байт C01F ED 79 OUT (c),A ; X старший байт C021 ED 69 OUT (c),L ; Y младший байт C023 ED 79 OUT (c),A ; Y старший байт C025 ED 51 OUT (c),D C027 ED 79 OUT (c),A ; NX C029 ED 59 OUT (c),E C02B ED 79 OUT (c),A ; NY C02D DD 66 00 LD h,(IX) C030 ED 61 OUT (c),H ; первое данное C032 ED 79 OUT (c),A ; регистр аргумента C034 3E F0 LD A,11110000b C036 ED 79 OUT (c),A ; приказ выполнить команду HMMC C038 3E AC LD A,44 or 80h C03A D3 99 OUT (99h),A C03C 3E 91 LD A,17 OR 80h C03E D3 99 OUT (99h),A ; VDP(17) <= 44 C040 3E 02 LOOP: LD A,2 C042 CD C05D CALL GetStatus C045 CB 47 BIT 0,A ; проверить бит CE C047 28 0D JR z,Exit ; конец C049 CB 7F BIT 7,A ; проверить бит TR C04B 28 F3 JR z,LOOP C04D DD 23 INC ix C04F DD 7E 00 LD A,(ix) C052 D3 9B OUT (9Bh),A C054 18 EA JR LOOP C056 3E 00 Exit: LD A,0 C058 CD C05D CALL GetStatus ; берем статус C05B FB EI ; выход C05C C9 RET C05D GetStatus: C05D D3 99 OUT (99h),A ; регистр статуса C05F 3E 8F LD A,8Fh C061 D3 99 OUT (99h),A C063 E5 PUSH HL C064 E1 POP HL C065 DB 99 IN A,(99h) C067 F5 PUSH Af C068 AF XOR A C069 D3 99 OUT (99h),A C06B 3E 8F LD A,8Fh C06D D3 99 OUT (99h),A C06F F1 POP Af C070 C9 RET C071 WaitVDP: C071 3E 02 LD A,2 ; ждать, пока VDP не готов C073 CD C05D CALL GetStatus C076 E6 01 AND 1 C078 20 F7 JR NZ,WaitVDP C07A AF XOR A C07B CD C05D CALL GetStatus C07E C9 RET C07F DataForVram EQU $ C07F 00 11 22 33 DB 00,11h,22h,33h,44h,55h,66h,77h,88h,99h C083 44 55 66 77 C087 88 99 AA C08A BB CC DD EE DB 0AAh,0BBh,0CCh,0DDh,0EEh,0FFh C08E FF C08E AEnd EQU $-1 .DePhase END {{anchor:n220}} ===== 2.20. Программирование шумов и музыки ===== В первой главе мы уже говорили о возможностях работы с программируемым звуковым генератором PSG . Здесь мы покажем Вам примеры программ на языке ассемблера, работающих с PSG. Напомним, что для задания номера регистра PSG используется порт A0h, а для записи значения для этого регистра — порт A1h. Приведём листинг программы, воспроизводящей звук летящего бомбардировщика. В этой программе запись в регистры PSG производится в цикле, в обратном порядке. MSX.M-80 1.00 01-Apr-85 PAGE 1 .Z80 ; === Звук летящего бомбардировщика 0000' 3E 0D LD A,13 ; запись в регистры 0002' 21 0021' LD HL,BombSnd ; в обратном порядке 0005' F3 Next: DI ; необх. запрет прерываний 0006' D3 A0 OUT (0A0h),A ; номер регистра 0008' F5 PUSH AF ; запоминаем A 0009' 7E LD A,(HL) ; значение для регистра PSG 000A' D3 A1 OUT (0A1h),A ; записываем данные 000C' FB EI ; можно восст. прерывания 000D' F1 POP AF ; восстанавливаем A 000E' 2B DEC HL ; новый адрес 000F' D6 01 SUB 1 ; следующий регистр PSG 0011' 30 F2 JR NC,Next ; повторить 0013' C9 RET ; ==== регистры NN: ── 0 ─ 1─ 2 ─ 3─ 4 ─ 5 6 ────── 0014' C8 0E DC 0E DefB 200,14,220,14,240,14,0, 0018' F0 0E 00 B8 ; NN: ───── 7 ──── 8 9 10 11 12 13 ─ 001C' 0F 0F 0F 00 DB 10111000b,15,15,15, 0, 0, 0 0020' 00 00 0021' BombSnd EQU $-1 END Ещё один пример — листинг программы, воспроизводящей звук сирены: MSX.M-80 1.00 01-Apr-85 PAGE 1 ; === Звук сирены .Z80 0000' F3 DI 0001' AF XOR a ; запись 255 => 0 рег. 0002' D3 A0 OUT (0A0h),a ; номер регистра 0004' 3E FF LD a,255 0006' D3 A1 OUT (0A1h), a ; данные 0008' 3E 01 LD a,1 ; запись 0 => 1 рег. 000A' D3 A0 OUT (0A0h),a ; номер регистра 000C' AF XOR a 000D' D3 A1 OUT (0A1h),a ; данные 000F' 3E 08 LD a,8 ; запись 8 => 8 рег. 0011' D3 A0 OUT (0A0h),a ; номер регистра 0013' D3 A1 OUT (0A1h),a ; данные 0015' 3E 07 LD a,7 ; запись упр => 7 рег. 0017' D3 A0 OUT (0A0h),a ; номер регистра 0019' 3E 3E LD a,00111110b 001B' D3 A1 OUT (0A1h),a ; данные 001D' FB EI ; === Подъем звука (257..170) 001E' 3E FE LD a,254 ; запись в регистр 0020' 3D NextA: DEC a ; уменьшить на 2 0021' 3D DEC a 0022' F5 PUSH af ; сохранить 0023' 21 0090 LD HL,90h ; задержка времени 00026' 2B timer: DEC HL 0027' 7C LD a,h 0028' B5 OR L 0029' 20 FB JR NZ,timer 002B' AF XOR a 002C' D3 A0 OUT (0A0h),a ; номер регистра = 0 002E' F1 POP af ; восстановить А 002F' D3 A1 OUT (0A1h),a ; данные 0031' FE AA CP 170 ; проверка 0033' 20 EB JR NZ,NextA ; повторить ; === Падение звука (170..252) 0035' 3E AA LD a,170 ; запись в регистр 0037' 3C NextB: INC a ; увеличить 0038' F5 PUSH AF ; сохранить 0039' 21 05A0 LD HL,90h*10 ; задержка больше, 003C' 2B timer1: DEC HL ; чем для роста звука 003D' 7C LD a,h ; в 10 раз 003E' B5 OR L 003F' 20 FB JR NZ,timer1 0041' AF XOR a 0042' D3 A0 OUT (0A0h),a ; номер регистра = 0 0044' F1 POP af ; восстановить 0045' D3 A1 OUT (0A1h),a ; данные 0047' FE FC CP 252 ; проверка 0049' 20 EC JR NZ,NextB ; повторить 004B' C3 0020' JP NextA ; все снова END Теперь приведём пример программы, проигрывающей несколько нот. В регистры звукогенератора записываются коды, соответствующие обозначениям нот и их октаве. MSX.M-80 1.00 01-Apr-85 PAGE 1 .Z80 0000' 3E 01 LD a,1 ; запись 0 => 1 рег 0002' D3 A0 OUT (0A0h),a 0004' AF XOR a 0005' D3 A0 OUT (0A0h),a 0007' 3E 07 LD a,7 ; разрешить звук 0009' D3 A0 OUT (0A0h),a ; канала А 000B' 3E 3E LD a,00111110b 000D' D3 A1 OUT (0A1h),a 000F' 3E 08 LD a,8 ; макс. звук для A 0011' D3 A0 OUT (0A0h),a 0013' 3E 0F LD a,15 0015' D3 A1 OUT (0A1h),a 0017' AF XOR a ; запись 190 => 0 0018' D3 A0 OUT (0A0h),a ; нота D, октава 5 001A' 3E BE LD a,190 001C' D3 A1 OUT (0A1h),a 001E' CD 0057' CALL timer 0021' AF XOR a ; запись 214 => 0 0022' D3 A0 OUT (0A0h),a ; нота C, октава 5 0024' 3E D6 LD a,214 0026' D3 A1 OUT (0A1h),a 0028' CD 0057' CALL timer 002B' AF XOR a ; запись 227 => 0 002C' D3 A0 OUT (0A0h),a ; нота B, октава 4 002E' 3E E3 LD a,227 0030' D3 A1 OUT (0A1h),a 0032' CD 0057' CALL timer 0035' CD 0057' CALL timer 0038' AF XOR a ; запись 254 => 0 0039' D3 A0 OUT (0A0h),a ; нота A, октава 4 003B' 3E FE LD a,254 003D' D3 A1 OUT (0A1h),a 003F' CD 0057' CALL timer 0042' CD 0057' CALL timer 0045' AF XOR a ; запись 29 => 0 0046' D3 A0 OUT (0A0h),a ; нота G, октава 4 0048' 3E 1D LD a,29 004A' D3 A1 OUT (0A1h),a 004C' 3E 01 LD a,1 ; запись 1 => 1 004E' D3 A0 OUT (0A0h),a 0050' D3 A1 OUT (0A1h),a 0052' F7 RST 30h ; BEEP, чистка регистров 0053' 00 DB 0 0054' 00C0 DW 0C0h 0056' C9 RET ; === Задержка звучания ноты 0057' 21 6000 timer: LD HL,6000h ; задержка 005A' 2B again: DEC HL 005B' 7C LD A,H 005C' B5 OR L 005D' 20 FB JR NZ,again 005F' 3E 08 LD A,8 ; гашение звука 0061' D3 A0 OUT (0A0h),A 0063' 3E 00 LD A,0 0065' D3 A1 OUT (0A1h),A 0067' 21 1000 LD HL,1000h ; задержка, пауза 006A' 2B again1: DEC HL ; перед следующей нотой 006B' 7C LD A,H 006C' B5 OR L 006D' 20 FB JR NZ,again1 006F' 3E 08 LD A,8 ; макс. звук для A 0071' D3 A0 OUT (0A0h),A 0073' 3E 0F LD A,15 0075' D3 A1 OUT (0A1h),A 0077' C9 RET END {{anchor:n221}} ===== 2.21. Управление памятью ===== Для управления логической памятью и размещением страниц в слотах и вторичных слотах используются порт A8h и ячейка RAM с адресом FFFFh. Для управления физической памятью изменяется содержимое портов ввода/вывода FCh...FFh. Эти механизмы были описаны в книге "Архитектура микрокомпьютера MSX-2", и их программирование не требует больших усилий. Более мобильное управление памятью осуществляется при помощи специальных подпрограмм BIOS (их поддерживает и MSX-DOS): - WRSLT (0014h) — Запись значения по указанному адресу в указанном слоте. - RDSLT (000Ch) — Чтение значения по указанному адресу из указанного слота. - ENASLT (0024h) — Выбор и активация слота. Запись числа в RAM любого слота может быть осуществлена следующим образом: ``` LD A,указатель-слота LD HL,адрес-ячейки LD E,значение-для-записи CALL WRSLT ``` Указатель слота имеет вид: ``` ┌───┬───┬───┬───┬───┬───┬───┬───┐ │ F │ 0 │ 0 │ 0 │ S │ S │ P │ P │ └───┴───┴───┴───┴───┴───┴───┴───┘ │ └─┬─┘ └─┬─┘ │ │ └─── Первичный номер слота │ │ │ └─── Вторичный номер слота ┌───────┴──── │ 0 - вторичный слот не используется │ 1 - используется вторичный слот ``` Аналогично выглядит чтение значения: ``` LD A,указатель-слота LD HL,адрес-для-чтения CALL RDSLT LD (адрес-результата), A ``` Для активации слота используется подпрограмма ENASLT (0024h). Ее вызов имеет вид: ``` LD A,указатель-слота LD HL,начальный-адрес CALL ENASLT ``` Такой вызов удобен для активации нулевой и первой страницы памяти. Помните, что обычные команды записи LD, LDIR работают быстрее, чем межслотовая запись. Поэтому иногда лучше переключить слоты, переписать данные и восстановить исходное состояние слотов. {{anchor:n2211}} ==== 2.21.1. Работа с картриджами ==== Компьютер MSX обычно имеет по крайней мере один внешний слот. Та аппаратура (hardware), которая к нему подключается, называется картриджем (cartridge). Существуют картриджи ROM для прикладных программ и игр, дискового ввода/вывода, интерфейса RS232C, расширения памяти RAM и слотов расширения. В кассету может быть аппаратно записано (ROM) программное обеспечение на языке BASIC или на языке ассемблера. Кроме этого, в подходящем слоте RAM можно создать "псевдокартридж" программным способом. В рабочей области, начиная с адреса FCC9h, находится участок памяти, отвечающий за каждую страницу памяти, находящейся в некотором слоте. Адрес байта рабочей области, отвечающего за некоторую страницу памяти, вычисляется по формуле: Addr = FCC9h + 16*SLTNUM + 4*EXTSLT + PageNmb , где * ''SLTNUM'' — номер первичного слота; * ''EXTSLT'' — номер вторичного слота (слота расширения); * ''PageNmb''— номер логической страницы памяти. По этому адресу содержится информация о том, работу каких устройств могут поддерживать программы, размещённые в соответствующей странице памяти. Информация кодируется побайтно следующим образом: Бит: 7 6 5 4 3 2 1 0 ┌───┬───┬───┬───┬───┬───┬───┬───┐ │ x │ x │ x │ 0 │ 0 │ 0 │ 0 │ 0 │ └───┴───┴───┴───┴───┴───┴───┴───┘ └┬┘ └┬┘ └┬┘ │ │ └─── Разрешение обработки CALL │ │ │ └──── Разрешение работы с нестандартным I/O │ └──── Текст на языке BASIC Таким образом, например, ячейка RAM с адресом FD02h отвечает за страницу 1 слота 3-2, т.е. за страницу RAM по адресу 4000h. Запись числа 32 в ячейку FD02h означает разрешение обработки расширенного оператора ''[[msx:basic_dialogue_programming_language:007#call|CALL]]'' [[msx:basic:]] подпрограммами RAM по адресу 4000h. [[msx:basic:]] просматривает все слоты (включая вторичные) по адресам с 4000H по 0BFFFH для нахождения ID устройства, начинающего каждую страницу. Формат заголовка кассеты, содержащего ID, приведён ниже. 4000h или 8000h+0000h ┌─────────┐ │ ID │ +0002h ├─────────┤ │ INIT │ +0004h ├─────────┤ │STATEMENT│ +0006h ├─────────┤ │ DEVICE │ +0008h ├─────────┤ │ TEXT │ +000Ah ├─────────┤ │ резерв │ +0010h └─────────┘ * ''ID'' — это двухбайтовая строка, при помощи которой можно отличить картридж ROM или SUB-ROM от пустой страницы. Картридж ROM обозначается строкой "AB" (41h,42h), а картридж SUB-ROM — строкой "CD". * ''INIT'' содержит адрес процедуры инициализации этого картриджа. Ноль записывается, если такой процедуры нет. Программы, которые нуждаются в связи с интерпретатором языка BASIC, возвращают управление командой Z80 ''RET''. Все регистры, за исключением указателя стека SP, могут быть изменены. Для некоторых программ (например, для игр) соблюдать соглашение о вызове ''INIT'' не нужно, поэтому игры могут запускаться процедурой инициализации. * ''STATEMENT'' содержит адрес обработки расширенного оператора ''[[msx:basic_dialogue_programming_language:007#call|CALL]]'', если его обработка в картридже предусмотрена. Ноль записывается в том случае, если обработки оператора ''[[msx:basic_dialogue_programming_language:007#call|CALL]]'' нет. Когда [[msx:basic:]] встречает оператор ''[[msx:basic_dialogue_programming_language:007#call|CALL]]'', то он записывает его имя в PROCNM (FD89h), в регистр ''HL'' — указатель на текст, следующий за ''CALL (список параметров)'', и вызывает адрес STATEMENT. Картридж может быть расположен по адресам с 4000H по 7FFFH. Синтаксис оператора расширения ''[[msx:basic_dialogue_programming_language:007#call|CALL]]'': CALL <инструкция> [(<параметр>[,<параметр>])] Слово ''CALL'' может быть заменено на символ подчёркивания (_). Имя оператора ''CALL'' записывается в системную память и заканчивается нулевым кодом. Так как буфер PROCNM имеет фиксированную длину 16 байт, то имя оператора может иметь длину не более 15 символов. Если обработчика требуемого оператора ''CALL'' в данном картридже не содержится, то устанавливается флаг C и управление возвращается в [[msx:basic:]]. Содержимое ''HL'' должно быть возвращено неизменённым. В этом случае интерпретатор языка [[msx:basic:]] пытается вызвать другой слот расширения. Если ни один слот "не отзовётся", генерируется сообщение об ошибке — "Syntax error". Если обработчик для конкретного оператора ''CALL'' содержится в картридже, то можно его обработать (выполнить), после чего необходимо [''HL''] установить на конец оператора ''CALL''. Обычно это нулевой код, означающий конец строки, или код ':', означающий конец оператора. Флаг C должен быть сброшен. Все регистры, за исключением ''SP'', могут быть изменены. * ''DEVICE'' — содержит адрес подпрограммы обработки устройства расширения, если она есть в этом картридже, в противном случае в ''DEVICE'' хранится ноль. Картридж может иметь адреса в диапазоне с 4000H по 7FFFH и до четырёх логических имён устройств. Когда [[msx:basic:]] встречает имя устройства (например, OPEN"OPT:"...), то он записывает его в PROCNM (FD89h), код FFh - в аккумулятор и передаёт управление на картридж с наименьшим номером слота. Если обработка устройства с этим именем в картридже не предусмотрена, то устанавливается флаг C и происходит возврат в BASIC. Если все картриджи возвратили флаг C, генерируется ошибка "Bad file name". Если подпрограмма обработки устройства содержится в картридже, то она выполняется, затем ID устройства (от 0 до 3) записывается в аккумулятор, сбрасывается флаг C, и выполняется возврат. Все регистры могут быть изменены. Когда выполняются реальные операции ввода/вывода, BASIC-интерпретатор записывает ID устройства (0-3) в ячейку DEVICE (FD99h), записывает запрос к устройству в регистр А (см.табл.21.1) и вызывает подпрограмму расширения устройства в картридже. Эта подпрограмма и должна правильно обработать запрос. ^ Регистр А ^ Запрос ^ | 0 |OPEN| | 2 |CLOSE| | 4 |Прямой доступ| | 6 |Последовательный вывод| | 8 |Последовательный ввод| | 10 |Функция LOC| | 12 |Функция LOF| | 14 |Функция EOF| | 16 |Функция FPOS| | 18 |Символ поддержки| Табл.21.1. Запросы к устройству * ''TEXT'' — это указатель на текст программы на языке [[msx:basic:]], если эта программа [[msx:basic:]] в картридже должна автоматически запускаться при перезагрузке. В противном случае там хранится ноль. Размер программы должен быть не более 16 Кбайт, по адресам с 8000h по BFFFh. Интерпретатор языка [[msx:basic:]] проверяет содержимое поля ''TEXT'' заголовка картриджа после инициализации (''INIT'') и после того как стартует система. Если там не ноль, то по указателю TEXT запускается программа на [[msx:basic:]]. Она должна храниться в промежуточном коде и её начало обозначается кодом ноль. {{anchor:n2212}} ==== 2.21.2. Создание CALL-подпрограмм пользователем ==== Приведём пример программы создания следующих подпрограмм пользователя, вызываемых из [[msx:basic:]] оператором ''[[msx:basic_dialogue_programming_language:007#call|CALL]]'' : * ''CALL RUSON'' — включение русских букв; * ''CALL RUSOFF'' — выключение русских букв; * ''CALL CAPSON'' — включение прописных букв; * ''CALL CAPSOFF'' — выключение прописных букв. Эти операторы формируются в псевдо-ROM по адресу 4000h. Для правильной трансляции ссылок используются директивы ''.PHASE'' и ''.DEPHASE'', которые описаны ниже. После чтения значений из ячейки управления вторичными слотами (FFFFh) они должны инвертироваться. Это нужно помнить и при запоминания текущего состояния слотов. Хотя для трансляции мы воспользовались ассемблером [[msx:macro-80_assembler:macro-80_assembler|M80]] и сборщиком L80, полученный файл реально имеет тип ''OBJ''. Это обеспечивают первые 7 байт текста программы. MSX.M-80 1.00 01-Apr-85 .Z80 9000 Load EQU 9000h ; адрес загрузки 4000 CallROM EQU 4000h ; адрес переписывания FD02 RAMCF EQU 0FD02h ; страница 1 слот 3-2 FD89 IdCall EQU 0FD89h ; сюда BASIC записывает ; имя оператора CALL 0F1F RUSSWCH EQU 0F1Fh ; вкл/выкл. RUS FCAB CAPST EQU 0FCABh ; статус CAPS FCAC KANAST EQU 0FCACh ; статус RUS 0004 CallNmb EQU 4 ; количество наших CALL 0000' ASEG 0000 FE DB 0FEh ; Obj-файл 0001 9000 DW Load ; адрес загрузки 0003 90F2 DW Load+Length ; конечный адрес 0005 9000 DW Start ; стартовый адрес ; ======================================== .PHASE Load 9000 F3 Start: DI ; === Установка вторичного слота 9001 3A FFFF LD A,(0FFFFh) ; текущее полож. слотов 9004 2F CPL ; инверсия 9005 F5 PUSH AF ; запись в стек 9006 CB DF SET 3,A ; страница 1, втор.сл. 2 9008 CB 97 RES 2,A 900A 32 FFFF LD (0FFFFh),A ; === Установка первичного слота 900D DB A8 IN A,(0A8h) ; первичный слот 900F F5 PUSH AF 9010 F6 0C OR 00001100b ; стр. 1, слот 3 9012 D3 A8 OUT (0A8h),A ; === Заполняем псевдо-ROM 9014 21 902E LD HL,PrgEnd ; откуда 9017 11 4000 LD DE,CallROM ; куда 901A 01 00C4 LD BC,Length-(PrgEnd-Load) ; сколько 901D ED B0 LDIR ; пересылка ; === Восстановление конфигурации BASIC 901F F1 POP AF 9020 D3 A8 OUT (0A8h),A 9022 F1 POP AF 9023 32 FFFF LD (0FFFFh),A 9026 FB EI ; === Заполнение буфера SLTATR - FD02h, т.е. ; === Разрешение CALL для слота 3-2, первая страница (4000h) 9027 3E 20 LD A,32 9029 32 FD02 LD (RAMCF),A 902C 3F CCF 902D C9 RET ; возврат в BASIC 902E PrgEnd EQU $ .DEPHASE ; === Заголовок псевдо-ROM .PHASE CallROM 4000 41 42 DB 'AB' ; ID картриджа 4002 0000 DW 0 ; адрес инициализации ROM 4004 4011 DW CallBeg ; адрес обработки CALL 4006 0000 DW 0 ; адрес обработки нестанд. I/O 4008 0000 DW 0 ; адрес текста BASIC в ROM 400A DS 7,0 ; резерв ; === Начало обработки оператора CALL 4011 CallBeg: 4011 37 SCF ; флаг "Syntax error" 4012 E5 PUSH HL ; сохраним HL 4013 06 04 LD B,CallNmb ; цикл сравнений 4015 21 4053 LD HL,IdBlock 4018 NewComp: 4018 E5 PUSH HL 4019 CD 4028 CALL CompBlock ; сравниваем имена 401C E1 POP HL 401D 30 1C JR NC,AddrBlock ; если нашли имя, переход 401F 11 0010 LD DE,010h ; на следующее имя, +16 байт 4022 19 ADD HL,DE ; увеличиваем адрес 4023 10 F3 DJNZ NewComp ; повторяем поиск 4025 E1 POP HL ; возврат, имени CALL нет 4026 37 SCF ; "Syntax error" 4027 C9 RET ; === Сравнение имен 4028 CompBlock: 4028 11 FD89 LD DE,IdCall ; адрес имени CALL 402B 1A NextS: LD A,(DE) 402C A7 AND A ; ноль ? 402D 28 07 JR Z,EndNm 402F BE CP (HL) ; сравнить с псевдо-ROM 4030 37 SCF 4031 C0 RET NZ ; выход, не равны 4032 23 INC HL ; сравнить следующие символы 4033 13 INC DE 4034 18 F5 JR NextS 4036 BE EndNm: CP (HL) ; тоже ноль ? 4037 37 SCF 4038 C0 RET NZ ; выход, если длиннее 4039 3F CCF ; имена совпали ! 403A C9 RET ; === Выбираем адрес нашего CALL и переходим 403B AddrBlock: 403B 3E 04 LD A,CallNmb 403D 90 SUB B ; номер имени CALL 403E 21 404B LD HL,AddrCall 4041 87 ADD A,A ; смещение в табл.адресов 4042 16 00 LD D,0 4044 5F LD E,A 4045 19 ADD HL,DE ; адрес в таблице - в HL 4046 5E LD E,(HL) ; адрес подпр. CALL - в DE 4047 23 INC HL 4048 56 LD D,(HL) 4049 EB EX DE,HL ; адрес - в HL 404A E9 JP (HL) ; переход на наш CALL ! ; === Таблица адресов подпрограмм CALL 404B AddrCall: 404B 4093 DW RUSON 404D 409D DW RUSOFF 404F 40A8 DW CAPSON 4051 40B7 DW CAPSOFF ; === Таблица имен операторов CALL, по 16 байт на имя 4053 IdBlock: 4053 52 55 53 4F DEFM 'RUSON' 4057 4E 4058 DEFS 11,0 4063 52 55 53 4F DEFM 'RUSOFF' 4067 46 46 4069 DEFS 10,0 4073 43 41 50 53 DEFM 'CAPSON' 4077 4F 4E 4079 DEFS 10,0 4083 43 41 50 53 DEFM 'CAPSOFF' 4087 4F 46 46 408A DEFS 9,0 ; === Включить RUS 4093 AF RUSON: XOR A 4094 32 FCAC LD (KANAST),A 4097 F7 RST 30h 4098 00 DEFB 0 4099 0F1F DEFW RUSSWCH 409B E1 POP HL 409C C9 RET ; === Выключить RUS RUSOFF: 409D 3E FF LD A,0FFh 409F 32 FCAC LD (KANAST),A 40A2 F7 RST 30h 40A3 00 DEFB 0 40A4 0F1F DEFW RUSSWCH 40A6 E1 POP HL 40A7 C9 RET ; === Включить CAPS CAPSON: 40A8 3E FF LD A,0FFh 40AA 32 FCAB LD (CAPST),A 40AD F3 DI 40AE DB AA IN A,(0AAh) 40B0 E6 BF AND 0BFh 40B2 D3 AA OUT (0AAh),A 40B4 FB EI 40B5 E1 POP HL 40B6 C9 RET ; === Выключить CAPS CAPSOFF: 40B7 AF XOR A 40B8 32 FCAB LD (CAPST),A 40BB F3 DI 40BC DB AA IN A,(0AAh) 40BE F6 40 OR 40h 40C0 D3 AA OUT (0AAh),A 40C2 FB EI 40C3 E1 POP HL 40C4 C9 RET .DEPHASE 00F2 Length EQU $-1-7 END {{anchor:n222}} ===== 2.22. Работа с файлами ===== При работе с внешними устройствами в традиционном программировании используются два понятия: набор данных и файл. Набором данных называют логически связанную совокупность информации, размещаемую на внешних запоминающих устройствах и устройствах ввода/вывода. Таким образом, набор данных имеет физический смысл — информация, хранимая на ленте, диске, бумаге и т.п. Файл — это абстракция (структура, описание) набора данных в программе на некотором языке программирования. Программист описывает файл и выполняет операции ввода/вывода над файлом, возможно, не зная точно, какой конкретно набор данных будет сопоставлен файлу. Связь файла с набором данных обычно осуществляет оператор открытия файла. В последнее время понятие "файл" часто используется вместо понятия "набор данных". Файл становится и логическим, и физическим понятием. С файлами (наборами данных) можно работать на двух уровнях — "низком" и "высоком". В первой главе была описана организация хранения информации на диске. Хорошо разобравшись в структуре директория, DPB, FAT, FCB, можно написать программу, которая ищет файл в директории, затем в FAT, читает или пишет информацию в соответствующие сектора диска и затем обновляет директорий и FAT. Это и есть "низкий" уровень, требующий очень кропотливой и аккуратной работы. На "высоком" уровне программист берет на себя минимум забот — задает FCB и буфер ввода/вывода, а всю остальную работу выполняют системные функции BDOS MSX-DOS. Такая работа более надежна, но предоставляет меньше возможностей. {{anchor:n2221}} ==== 2.22.1. Абсолютное чтение/запись ==== Под абсолютным чтением/записью здесь понимается чтение/запись логических секторов диска, в том числе загрузочного сектора, секторов директория, таблиц FAT, секторов данных. Для выполнения таких действий можно использовать системные функции BDOS 1Ah (установка адреса буфера), 1Bh (получение информации о драйвере), 2Fh (абсолютное чтение секторов), 30h (абсолютная запись секторов диска) и другие. В качестве примера приведём программу восстановления директория, если были случайно уничтожены несколько файлов. Как Вы помните, при этом в первом байте соответствующей записи директория появляется код E5h. Программа находит такие записи в директории и ждёт ввода программистом первой литеры стёртого файла. В конце работы восстановленный директорий записывается назад на диск. Обратите внимание, что программа не восстанавливает FAT, записи которого при уничтожении файла обнуляются. MSX.M-80 1.00 01-Apr-85 PAGE 1 .Z80 0005 BDOS EQU 5 0001 CONS_INP EQU 1 0002 CONS_OUT EQU 2 001A SET_DMA EQU 1Ah 001B GET_ALLOC EQU 1Bh 002F ABS_READ EQU 2Fh 0030 ABS_WRT EQU 30h ; === Установка адреса буфера для ввода директория 0000' 11 009B' LD DE,Dir 0003' 0E 1A LD C,SET_DMA 0005' CD 0005 CALL BDOS ; === Информация о драйвере 0008' 1E 00 LD E,0 ; текущий дисковод 000A' 0E 1B LD C,GET_ALLOC 000C' CD 0005 CALL BDOS 000F' FE FF CP 0FFh ; ошибка ? 0011' C8 RET Z ; тогда - выход ; === Берем информацию о диске 0012' DD 7E 0B LD A,(IX+11) ; макс.кол-во файлов 0015' 32 009A' LD (MaxF),A ; в директории 0018' DD 56 12 LD D,(IX+18) ; номер первого сектора 001B' DD 5E 11 LD E,(IX+17) ; директория 001E' D5 PUSH DE ; запоминаем его в стеке ; === Читаем директорий 001F' 26 07 LD H,7 ; кол-во секторов 0021' 2E 00 LD L,0 ; текущий драйвер 0023' 0E 2F LD C,ABS_READ 0025' CD 0005 CALL BDOS ; === Ищем уничтоженные файлы 0028' 21 009B' LD HL,Dir ; нач.адр. буфера для дир. 002B' 16 00 LD D,0 002D' 3A 009A' LD A,(MaxF) ; для цикла по макс.кол-ву 0030' 5F LD E,A ; файлов в директории 0031' D5 PUSH DE 0032' E5 PUSH HL ; начальный адрес записи 0033' 7E Again: LD A,(HL) ; берем первый байт записи 0034' B7 OR A ; если ноль - выход 0035' 28 56 JR Z,Finish ; выход 0037' FE E5 CP 0E5h ; уничтожен ? 0039' 20 44 JR NZ,Next ; если нет - то следующий ; === Печатаем строку - звездочка, имя файла 003B' 1E 0A LD E,0Ah ; вниз на след. строку 003D' 0E 02 LD C,CONS_OUT 003F' CD 0005 CALL BDOS 0042' 1E 0D LD E,0Dh ; в начало строки 0044' 0E 02 LD C,CONS_OUT 0046' CD 0005 CALL BDOS 0049' 1E 2A LD E,'*' ; выводим звездочку 004B' 0E 02 LD C,CONS_OUT 004D' CD 0005 CALL BDOS ; === Печатаем остаток имени файла (без первой буквы) 0050' E1 POP HL ; восстановили HL 0051' E5 PUSH HL 0052' 06 0A LD B,10 ; выводим 10 символов 0054' C5 NextCh: PUSH BC ; имени файла, 0055' 23 INC HL ; сохраняя нужные 0056' E5 PUSH HL ; регистры в стеке 0057' 5E LD E,(HL) 0058' 0E 02 LD C,CONS_OUT 005A' CD 0005 CALL BDOS ; вывод на экран 005D' E1 POP HL 005E' C1 POP BC 005F' 10 F3 DJNZ NextCh ; след. символ ; === Возвращаемся назад на экране на 11 символов 0061' 06 0B LD B,11 0063' C5 Back: PUSH BC 0064' 1E 1D LD E,1Dh ; стрелка "<-" 0066' 0E 02 LD C,CONS_OUT 0068' CD 0005 CALL BDOS 006B' C1 POP BC 006C' 10 F5 DJNZ Back ; === Вводим одну букву с отображением на экране 006E' 0E 01 LD C,CONS_INP 0070' CD 0005 CALL BDOS 0073' FE 0D CP 13 ; если нажат ВВОД, 0075' 28 08 JR Z,Next ; то на след. файл 0077' FE 20 CP 20h ; если не знак, 0079' FA 007F' JP M,Next ; то на след. файл 007C' E1 POP HL ; восстановили HL 007D' E5 PUSH HL 007E' 77 LD (HL),A ; восстанавливаем букву ; === Переходим к следующей записи директория (файлу) 007F' E1 Next: POP HL ; след. 32 байта директ. 0080' 11 0020 LD DE,32 0083' 19 ADD HL,DE 0084' D1 POP DE ; количество просм. файлов 0085' 1B DEC DE ; стало меньше на 1 файл 0086' D5 PUSH DE 0087' E5 PUSH HL 0088' 7A LD A,D ; директорий исчерпан ? 0089' B3 OR E 008A' C2 0033' JP NZ,Again ; если нет - повторим поиск ; === Записываем директорий назад на диск 008D' E1 Finish: POP HL ; восстанавл. параметры 008E' D1 POP DE 008F' D1 POP DE 0090' 26 07 LD H,7 ; 7 секторов 0092' 2E 00 LD L,0 ; на текущий диск 0094' 0E 30 LD C,ABS_WRT ; записываем директорий 0096' CD 0005 CALL BDOS 0099' C9 RET ; все ! 009A' 00 MaxF: DB 0 ; макс.кол-во файлов дир. 009B' Dir: DS 3584,0 ; 7 секторов по 512 байт END {{anchor:n2222}} ==== 2.22.2. Использование системных функций ==== Для правильной работы с файлами необходимо различать тип организации файла (набора данных) и метод доступа к файлу (набору данных). По сути, первое определяет набор данных, а второе — файл в том смысле, о котором было ранее сказано. Набор данных (файл) может быть потокоориентированным или записеориентированным. Потокоориентированный набор данных (файл) состоит из числовых, символьных, булевских, битовых констант, разделяемых пробелами, запятыми, символами конца строк и т.п. Такие файлы рассчитаны обычно либо на последовательный ввод всего или части файла, либо на последовательный вывод. К ним можно отнести текстовые файлы, [[msx:basic:]]–программы во внутреннем коде, объектные файлы (машинные коды и видеоинформация), промежуточные файлы трансляции, командные файлы (типа COM). Храниться потокоориентированные файлы могут как на магнитных лентах, так и на дисках. Текстовые файлы состоят из последовательных кодов символов строк, разделяемых кодами 0Ah (вниз на строку) и 0Dh (в начало строки). В конце текстового файла всегда записывается код 1Ah, обозначающий конец файла — EOF. Рассмотрим, например, как записан на диске следующий текст: ┌──────────────┐ │Привет ! │ │Это я. │ └──────────────┘ В файле он будет представлять собой следующие коды: ┌──────────────────────────────────────────────────────────┐ │ F0 D2 C9 D7 C5 D4 20 21 0A 0D FC D4 CF 20 D1 2E 0A 0D 1A │ └───────────────────┬──────┬─┬───────────┬─────────┬─┬──┬──┘ П р и в е т │ ! └┬┘ Э т о │ я . └┬┘ │ │ │ │ │ └──┐ пробел новая пробел новая конец строка строка файла Объектные файлы отличаются тем, что имеют заголовок из семи байт: первый байт — код FEh, затем по два байта — загрузочный адрес, конечный адрес, стартовый адрес в формате Intel. [[msx:basic:]]–программы во внутреннем коде имеют в качестве первого байта код FFh. Записеориентированный набор данных (файл) состоит из записей, обычно фиксированной длины. В таких файлах удобно хранить различные таблицы во внутреннем формате. Каждая строка таблицы соответствует одной записи файла. Если такие таблицы предназначены для последовательного чтения/записи, то они могут храниться и на лентах, и на дисках, а если иногда необходим ввод/вывод одной строки по её номеру, то лучше хранить такие файлы на диске и использовать прямые методы доступа. Метод доступа определяет, каким образом осуществляется доступ к информации, хранящейся в наборе данных (файле). Доступ может быть последовательным, прямым, телекоммуникационным и другим. Прямой доступ возможен, только когда набор данных размещён на устройстве прямого доступа, например, на магнитном диске. При этом набор данных может иметь практически любую организацию. Например, текстовый файл можно читать блоками по 1024 байт в любом порядке (в том числе и последовательно), если использовать прямой метод доступа. Как уже говорилось, для работы с файлами имеются стандартные функции ввода/вывода BDOS. Они вызываются по адресу 0005h (дисковая операционная система) или F37Dh ([[msx:disk_basic|]]). При этом надо занести в регистр C номер вызываемой функции, а через остальные регистры передать необходимые параметры. Для того, чтобы открыть файл, необходимо подготовить FCB — блок управления файлом. Его можно создать в любом месте оперативной памяти (при работе в [[msx:dos:|]]) и в области 8000h–FFFFh (при работе в [[msx:basic:]]). Как Вы можете убедиться, номер функции "открыть файл" равен 0Fh. Поэтому для обращения к этой функции в регистр C надо занести 0Fh, в регистровую пару DE — адрес FCB и затем обратиться по указанному адресу BDOS. Ниже приводится пример программы, которая читает загрузочный, конечный и стартовый адреса файла VALLEY.GM (игра "King's Valley"). Для этого необходимо открыть файл, прочитать из него 7 первых байт и закрыть файл. Перед чтением информации должен быть установлен адрес DMA (буфера обмена с диском). Именно с адреса начала DMA будут записываться все читаемые данные. 'files-fcb' Z80-Assembler Page: 1 ORG 9000h TITLE 'files-fcb' ; === Пытаемся открыть файл и заполнить FCB 9000 113090 LD de,fcb ; адрес FCB 9003 0E0F LD c,0Fh ; открыть файл 9005 CD7DF3 CALL 0F37Dh ; вызов BDOS (BASIC) 9008 B7 OR a ; смогли открыть ? 9009 37 SCF ; установить флаг ошибки 900A C0 RET nz ; если нет, то возврат ; === Устанавливаем адрес буфера для чтения 900B 1100A0 LD de,0A000H ; загрузить в DE адрес DMA 900E 0E1A LD c,1Ah ; установить DMA 9010 CD7DF3 CALL 0F37Dh ; обратиться к DISK-BASIC ; === Пытаемся прочитать 7 первых байт файла в DMA 9013 210700 LD hl,7 ; длина 1 записи 9016 223E90 LD (fcb+14),hl; записать ее в FCB 9019 113090 LD de,fcb ; адрес FCB 901C 210100 LD hl,1 ; читать 1 запись 901F 0E27 LD c,27h ; код функ. чтение блока 9021 CD7DF3 CALL 0F37Dh ; считать 9024 B7 OR A ; смогли считать ? 9025 37 SCF ; установить флаг ошибки 9026 C0 RET NZ ; если не смогли, то возврат ; === Закрываем файл 9027 113090 LD DE,fcb ; адрес FCB 902A 0E10 LD C,10h ; код функ. закрыть файл 902C CD7DF3 CALL 0F37Dh ; выполнить 902F C9 RET ; вернуться ; === Блок данных 9030 00 fcb: DB 0 ; номер дисковода (акт.) 9031 56414C4C DB 'VALLEY GM ' ; имя (8 байт имя, 3 - тип) 9035 45592020 9039 474D20 903C DS 28,0 ; все остальные данные - нули END Оттранслируем эту программу в файл с именем ''TMP.OBJ'' и выполним следующую программу на языке [[msx:basic:]]: 10 CLEAR 200,&H9000 20 BLOAD"TMP.OBJ",R 30 PRINT HEX$(PEEK(&HA000)) 40 PRINT HEX$(PEEK(&HA001)+256*PEEK(&HA002)) 50 PRINT HEX$(PEEK(&HA003)+256*PEEK(&HA004)) 60 PRINT HEX$(PEEK(&HA005)+256*PEEK(&HA006)) 70 END При её запуске дважды произойдёт обращение к диску (для загрузки файла ''TMP.OBJ'' и для чтения из файла ''VALLEY.GM'' 7 байт). На экран будет выведено следующее: run FE 9000 D080 9000 Ok █ {{anchor:n223}} ===== 2.23. Ошибки программирования и правонарушения, связанные с компьютерами ===== Программирование на языке ассемблера требует большого внимания и аккуратности. Наиболее простой тип допускаемых ошибок — синтаксические, то есть ошибки в записи команд ассемблера. Такие ошибки локализуются самим ассемблером. Трудность иногда может состоять только в том, чтобы понять, какая именно ошибка допущена. Однако бывают такие ошибки, которые ассемблер не обнаруживает. Например, если Вы случайно напишете ''INC H'' вместо ''INC HL'', то возможно, не скоро найдёте причину плохой работы программы. Другая часто встречающаяся ошибка — загрузка адреса вместо загрузки значения (опускание скобок). Вы должны чётко осознавать разницу в смысле команд ''LD HL,(data)'' и ''LD HL, data''. С компьютерами связан целый ряд правонарушений — нарушения прав человека, внедрение для развлечения в информационные системы, кража информации, её изменение или уничтожение, незаконные финансовые операции. Некоторые из этих правонарушений совершаются при помощи специальных программ — вредоносного программного обеспечения (ВПО). Основными типами ВПО в настоящее время являются: - компьютерные троянские кони (trojan horse); - компьютерные вирусы (virus); - компьютерные черви (worm). {{anchor:n2231}} ==== 2.23.1. Троянские кони ==== Троянский конь — это компьютерная программа, обычно располагающаяся на диске совершенно открыто, выполняющая некоторые полезные функции и в то же время скрыто наносящая вред. Например, программа-игра может периодически портить FAT, уничтожать директории или создавать на диске «сбойные» блоки. Троянский конь такого типа может быть и просто результатом ошибки программиста, разрабатывающего системную программу. Представьте себе, к примеру, программу обслуживания дисков DiskFxr с ошибкой. Кроме этого троянский конь может быть переносчиком вирусов. После запуска троянского коня им активизируется содержащийся в нем вирус, который дальше действует самостоятельно. {{anchor:n2232}} ==== 2.23.2. Компьютерные вирусы ==== Компьютерный вирус — это программа, обычно скрыто располагающаяся на диске, обладающая возможностью к «саморазмножению» (копированию на другие диски или файлы) и имеющая некоторый вредоносный эффект, который проявляется через определённое время после заражения. Таким образом, некоторое время вирус «размножается» и только потом начинает производить эффекты. В самых простых случаях это уничтожение директорий и FAT. Более коварные вирусы слегка портят информацию или уничтожают её не сразу, а незаметными порциями. Особенно подлым является незаметное изменение цифр в больших массивах числовых данных. Три основных типа вирусов, разработанных в настоящее время для системы MSX — это: - Загрузочные вирусы; - Вирусы [[msx:dos:|]]; - Файловые вирусы. Для понимания основных принципов их работы необходимо хорошо знать архитектуру компьютера и основы функционирования операционной системы [[msx:dos:|]]. Рассмотрим процесс начальной загрузки системы MSX. Система MSX запускается следующим образом: - Сброс питания компьютера приводит к тому, что в процессе перезагрузки сначала проверяются все слоты, и если в вершине проверяемого слота записаны 2 байта 41Н и 42Н, слот интерпретируется как относящийся к определённой части ПЗУ. После этого выполняется программа ''INIT'' (инициализация), адрес которой установлен в верхней части ПЗУ. В случае использования программы ''INIT'' из ПЗУ дискового интерфейса в первую очередь определяется рабочая область для относящегося к нему дисковода. - Когда все слоты проверены, машина обращается к адресу FEDAh (H.STKE). Если содержимое этого адреса не равно C9h (т.е. если в этот хук не был записан вызов определённой программы при выполнении процедуры INIT), подготавливается конфигурация DISK-BASIC и управление передаётся на H.STKE. - Если же содержимое H.STKE равно C9h, во всех слотах ищется кассета со входом TEXT. В случае её нахождения подготавливается конфигурация [[msx:disk_basic|]] и выполняется программа [[msx:basic:]] из этой кассеты. - Затем содержимое загрузочного сектора диска (логический сектор #0) передаётся в память на адреса с C000H по C0FFH. При этом, если возникает ошибка неготовности диска или ошибка чтения, или если значение первого байта этого сектора не равно ни EBh, ни E9h, вызывается [[msx:disk_basic|]]. - Вызывается подпрограмма по адресу C01Eh, и происходит сброс флага ''C''. При нормальной работе, поскольку по этому адресу записан код ''RET NC'', ничего не выполняется, и управление возвращается обратно. Любая записанная здесь на языке ассемблера программа запустится автоматически (первый вход в BOOT-программу). - Проверяется ёмкость ОЗУ (его содержимое при этом не разрушается). Если она менее 64 Кбайт, вызывается [[msx:disk_basic|]]. - 7. Подготавливается конфигурация [[msx:dos:|]] и вызывается C01EH, на этот раз с установленным флагом C (второй вызов BOOT-программы). Загружается ''MSXDOS.SYS'' с адреса 100Н, и на этот же адрес передаётся управление (т.е. начинает работать [[msx:dos:|]]). После этого [[msx:dos:|]] переносит себя на более высокий адрес. Если файл ''MSXDOS.SYS'' на диске отсутствует, вызывается [[msx:disk_basic|]]. - 8. ''MSXDOS.SYS'' загружает ''COMMAND.COM'' с диска по адресу 100Н и выполняет переход на его начальный адрес. ''COMMAND.COM'' тоже переносит себя на более высокий адрес и запускается. Если ''COMMAND.COM'' отсутствует, появляется сообщение "INSERT DOS DISKEТТЕ" (вставьте системный диск), и выполнение прерывается до тех пор, пока в дисковод не будет вставлена соответствующая дискета. - 9. При первой загрузке [[msx:disk_basic|]], если существует файл с именем ''AUTOEXEC.BAT'', он выполняется как обычный пакетный файл. Когда [[msx:dos:|]] не запущен и работает [[msx:disk_basic|]], и если на диске имеется файл ''AUTOEXEC.BAS'', то он будет автоматически запущен. === Загрузочные вирусы === Бутовыми вирусами называют вирусы, которые размещают себя в загрузочном (BOOT-секторе) диска и изменяют программу начальной загрузки таким образом, чтобы получать управление до начала работы «настоящей» загрузочной программы. Получив управление, вирус устанавливает ловушки так, что при записи информации на другой диск на него записывается и заражённый вирусом загрузочный сектор. === Вирусы MSX-DOS === Вирусом могут быть поражены и файлы операционной системы MSX-DOS — COMMAND.COM и MSXDOS.SYS. Вначале программист-хакер корректирует MSX-DOS, внедряя туда вирус, а затем такая "зараженная" MSX-DOS в ходе работы заменяет собой "чистые" версии системы на других дисках. === Файловые вирусы === Разработанные для MSX файловые вирусы, как правило, "живут" в файлах типа COM. В первых байтах зараженного файла обычно вирусом записывается команда перехода на основное тело вируса, которое дописывается к файлу. Таким образом, при запуске вирус первым получает управление, выполняет некоторые действия и затем запускает саму программу. Ситуации, возможные при заражении вирусами По неизвестным причинам увеличился размер, изменилась дата и время создания файла типа COM, изменилась длина командного процессора (COMMAND.COM). - Увеличилось количество файлов на диске. - Появилось сообщение "1 file(s) copied". - На диске появились "плохие" кластеры или уменьшился объем доступной дисковой памяти, хотя файлы не записывались и не удалялись. - Загрузка или выполнение программы идет дольше, чем обычно. - Аварийно завершаются ранее нормально функционировавшие программы. - Зажигается лампочка обращения к дисководу, когда в этом нет очевидной необходимости. - Машина находится в бесконечном цикле автоматического рестарта. - Появились необъяснимые зависания или перезагрузки системы. - Появилось сообщение о защите дискеты от записи при загрузке программ с защищенных от записи дискет ("Write protect error"). Помните, что сбои возможны и из-за неисправности оборудования — сбойные блоки на диске, конфликты адаптеров, несовместимость контроллеров и управляемого ими оборудования,сбои в питающей сети, повреждение на плате, большие колебания температуры, влажности, запыленность, и из-за неквалифицированного или неаккуратного обращения с компьютером. {{anchor:n2233}} ==== 2.23.3. Компьютерные черви ==== Компьютерные черви — это программы, пересылаемые по сетям ЭВМ, захватывающие все ресурсы зараженного компьютера для своей работы и/или крадущие информацию для своего "хозяина". Как правило, черви не содержат разрушающей компоненты. Поскольку для системы MSX в СССР пока имеются только локальные сети, такой тип вредоносного программного обеспечения имеет чисто познавательный интерес. {{anchor:n2234}} ==== 2.23.4. Методы защиты информации ==== Можно выделить следующие методы защиты информации: * Технологические и технические методы * Организационные методы * Правовые методы Технологические методы включают в себя методы разработки и применения различных антивирусных программ — программ, которые могут обнаружить, ликвидировать или предупредить заражение. Однако, к сожалению, большинство имеющихся антивирусных программ могут обнаруживать только изученные к моменту разработки антивирусной программы вирусы. Вновь разработанные вирусы не детектируются. Кроме этого, широко применяются методы шифрования информации, для защиты от тех, кому не разрешено ей пользоваться. Нужно также иметь в виду, что электронные устройства излучают радиацию, которая может быть обнаружена, и в случае простых "серийных сигналов" — восстановлена дешевым электронным подслушивающим устройством. Однако защита при помощи физических барьеров считается в сфере образования экономически неоправданной. Правовые методы включают в себя нормы административного или уголовного права, применяемого к разработчикам вредоносных программ. Такие нормы у нас пока только разрабатываются. Организационные методы включают в себя учет того, какая информация требуется, как она хранится, как долго, как она помечается, кто ей владеет, как она обрабатывается, кто и как может иметь к ней доступ. === Основные рекомендации и требования по защите информации === * Используйте программное обеспечение, полученное (приобретённое) только от лиц или организаций, которым Вы полностью доверяете. Приобретайте программы законным образом, поскольку защита от копирования может быть выполнена в виде вирусов, а украденные программы могут оказаться троянскими конями или программами, инфицированными вирусами. * Не работайте с оригиналами дистрибутивных дисков. Делайте с них (если это разрешено поставщиком) копии и работайте с копиями. * Лучше иметь не одну, а несколько копий. Под рукой всегда должна быть защищённая от записи дискета с "чистой" [[msx:dos:|]], оболочкой [[msx:nd:nd|ND]], антивирусами и утилитами восстановления ''FIXER'', ''DBG'', ''VFY'', ''DSKVER'' и т.п. * Не пользуйтесь "чужими" дискетами на своём компьютере. Не запускайте с них операционную систему и программы. Не давайте свои дискеты для работы на других компьютерах. Не запускайте программы, назначение которых Вам точно неизвестно. * В конце работы делайте копии того, что было сделано, на дисках-архивах, с которыми не ведётся никакая другая работа, кроме записи на них копий файлов. Лучше всего копировать исходные файлы программ в текстовом виде (в том числе и программы на языке BASIC — в коде ASCII). Перед копированием выключите ненадолго компьютер и затем загрузите "чистую" [[msx:dos:|]] или [[msx:nd:nd|ND]]. Перед копированием можно просмотреть файл, но нельзя запускать никакие программы. * Если Вы хотите поработать на уже включённом кем-то компьютере MSX, обязательно выключите его ненадолго и затем произведите загрузку со своей дискеты. Компьютер мог быть умышленно или случайно заражён вирусом, а его выключение, к счастью, уничтожает все вирусы. * Если Вы поняли, что диск заражён вирусом, выключите компьютер, загрузите «чистую» систему с эталонного диска [[msx:dos:|]] или [[msx:nd:nd|ND]], перепишите файлы с заражённой дискеты, за исключением файлов типа ''COM'', ''SYS'', ''OBJ'', ''GM'' (а лучше всего переписывать только текстовые файлы), на чистую дискету, отформатируйте и отверифицируйте заражённую дискету и восстановите на ней файлы. ---- [<>] {{tag>MSX Book_apguidefb on_github}}