м другим не занят; в следующем будет показано как выполнить ту же задачу, когда компьютер занят другой работой. Когда компьютер ничем другим не занят, то можно выводить мелодию или производить специальные звуковые эффекты; когда же компьютер занят другой работой, то нельзя производить звуковые эффекты. Создание звуковых строк является одной из мощнейших возможнос- тей, предоставляемых Бейсиком. Построение же строк звуков в ас- семблере требует большой работы. Может быть использован любой из двух методов генерации звука, предложенных в [2.2.2] и [2.2.3]. Для обоих методов надо просто генерировать один тон в течении заданного времени, затем следующий и т.д. Каждая звуковая строка формируется из двух строк данных, одна из которых содержит часто- ты последовательных тонов, а другая хранит их длительности (при условии, что требуются разные длительности). Продолжительность звучания определяется с использованием счетчика времени суток BIOS [2.1.6]. Высокий уровень. Опреатор Бейсика PLAY предоставляет большие возможности. Опе- ратор сопровождается строкой нот, перемешанных с информацией о том, как эти ноты должны быть исполнены. Ноты записываются буква- ми A - G и последующими знаками для диезов и бемолей. Диезы обоз- начаются знаками # или +, а бемоли минусом (-). Операторы PLAY "CC#D" и PLAY "CD-D" эквивалентны, но нельзя использовать диезы и бемоли для обозначения белых клавиш. Второй способ задания нот состоит в вычислении кодового номера от 0 до 84, причем 0 соот- ветствует отсутствию звучания, а числа от 1 до 84 соответствуют 84 возможным нотам семи октав, начиная снизу. Номеру должна пред- шествовать буква N: PLAY "N3N72N44". Допустимый диапазон - семь октав, внутри каждой могут быть ноты от C(до) до B(си). Октавы пронумерованы от 0 до 6 и нота до первой октавы соответствует октаве 3. Текущая октава может быть изменена в любой момент, за счет вставки в строку буквы O, за которой следует номер октавы. Если не было начальной установки, то используется октава 4. Оператор PLAY "O3CO4CO5CO6C" выводит ноты до последовательных октав вверх. Другой способ изменения октавы состоит во включении в строку символов > или <, которые переключают тон вверх и вниз на октаву, соответственно. Оператор PLAY "O3C>C>C>C" приводит к тому же результату, что и предыдущий. Длительность исполнения нот также может быть изменена за счет вставки кодового номера, которому предшествует буква L. Все пос- ледующие ноты будут исполняться с этой длительностью до тех пор, пока не встретится другой код длины. Код - это число от 1 до 64, причем 1 соответствует целой ноте, а 64 - 1/64. Запись L4 соот- ветствует четверти. Темп с которым исполняются ноты регулируется кодом темпа, который состоит из буквы T, за которой следует число от 32 до 255, дающее число четвертей, исполняемых в минуту. Если эти параметры не указаны, то по умолчанию берется длительность L4 и темп 120. Для изменения длительности только одной ноты надо поместить значение длины после ноты и без буквы L. Оператор PLAY "L4CDE16FG" исполнит E как шестнадцатую, а все остальные ноты как четверти. Длительность пауз берется такой же, как и длительность нот. Поместите номер от 1 до 64 после буквы P для паузы. P1 де- лает паузу интервалом в целую, а P64 - в 1/64. Помещение точки после ноты имеет тот же эффект, какой он имеет в обычной музы- кальной нотации: длительность ноты увеличивается наполовину. Вторая точка продолжит длительность еще наполовину. По умолчанию ноты играются 7/8 указанной длительности. Чтобы они исполнялись полную длительность (легато), поместите в строку ML. Чтобы они исполнялись 3/4 длительности (стаккато), поместите в строку MS. Чтобы вернуться к нормальному стилю надо указать MN. Обычно, вся прочая деятельность программы прекращается до тех пор, пока не будет сыграна строка. Для того чтобы выполнялись операторы, следующие за оператором PLAY, а строка исполнялась в фоновом режиме, поместите в строку MB. Для восстановления нор- мальной ситуации напишите MF. Наконец, оператор PLAY позволяет исполнять подстроки внутри длинной строки. Имеется в виду, что часть исполняемой строки может быть введена как обычная строковая переменная, а затем эта переменная может быть вызвана из строки сформированной в операто- ре PLAY. Например, если S$ = "EEEEE", то в операторе PLAY "CDXS$;FG" нота E будет повторена 5 раз. Отметим, что имени пере- менной должна предшествовать буква X, а за именем следовать точка с запятой (;). (Для компилируемых программ применяется другой метод, использующий переменную VARPTR$ - детали см. в руководстве по Бейсику). В приведенном примере исполняется знакомый бой дедушкиных часов. В строке сначала устанавливается стиль исполнения легато, затем темп и начальная октава, и, наконец, четыре ноты, пауза, и те же самые четыре ноты, но в обратном порядке. Пробелы в строке включены исключительно для удобства программиста - Бейсик игнори- рует их. 100 PLAY "ML T40 O3 ECD<G P32 G>DEC" Благодаря наличию генератора звука PCjr добавляет к оператору PLAY две возможности. Во-первых, допускается параметр V, устанав- ливающий громкость. Выражение V5 устанавливает (или изменяет) громкость на уровень 5. Допустимый диапазон от 0 до 15, причем по умолчанию берется 8. 0 полностью подавляет звук. Во-вторых, с помощью оператора PLAY можно одновременно исполнять три звуковых строки. Поместите все три строки в одну программную строку, раз- деляя их запятыми. Для того чтобы иметь возможность использовать эти специальные свойства, Вы должны предварительно разрешить внешний динамик с помощью оператора SOUND ON. 100 SOUND ON 110 PLAY "...........","..........","............" Низкий уровень. В примере для генерации звука используется микросхема таймера 8253. Здесь просто исполняются 8 нот, но небольшая модификация может сильно расширить возможности этой процедуры. Имеется три строки данных. Первая устанавливает длительность каждой ноты, как кратное произвольного периода задержки (изменяя этот период за- держки, можно изменять темп). Вторая строка содержит частоты каждой из 8 нот; эти значения должны быть помещены в регистр задвижки канала 2 микросхемы 8253 для исполнения желаемых тонов. Третья строка содержит мелодию в виде кодовых номеров от 1 до 8, которые соответствуют восьми частотам. Эта строка завершается кодом 0FFH, который служит признаком конца мелодии. Процедура просто читает очередную ноту мелодии, находит соответствующую частоту и помещает ее в канал 2. Затем длительность для этой ноты помещается в счетчик цикла задержки, который использует счетчик времени суток, а когда задержка кончается, то переходим к обра- ботке следующей ноты. На рис. 2-5 показана работа этой процедуры. ;---в сегменте данных BEAT DB 10,9,8,7,6,5,4,3,2 ;длительность нот FREQUENCY DW 2280,2031,1809,1709 ;таблица частот DW 1521,1353,1207,1139 ; MELODY DB 1,2,3,4,5,6,7,8,0FFH ;номер частоты ноты ;---инициализация PORT_B EQU 61H COMMAND_REG EQU 43H LATCH2 EQU 42H IN AL,PORT_B ;получаем текущий статус OR AL,00000011B ;разрешаем динамик и таймер OUT PORT_B,AL ;заменяем байт MOV SI,0 ;инициализируем указатель MOV AL,0B6H ;установка для канала 2 OUT COMMAND_REG,AL ;посылаем в командный регистр ;---смотрим ноту, получаем ее частоту и помещаем в канал 2 NEXT_NOTE: LEA BX,MELODY ;берем смещение для мелодии MOV AL,[BX][SI] ;берем код n-ной ноты строки CMP AL,0FFH ;проверка на конец строки JE NO_MORE ;если конец, то на выход CBW ;переводим в слово ;получение частоты MOV BX,OFFSET FREQUENCY ;смещение таблицы частот DEC AX ;начинаем отсчет с 0 SHL AX,1 ;умножаем на 2, т.к. слова MOV DI,AX ;адресуем через DI MOV DX,[BX][DI] ;получаем частоту из таблицы ;начинаем исполнение ноты MOV AL,DL ;готовим младший байт частоты OUT LATCH2,AL ;посылаем его MOV AL,DH ;готовим старший байт частоты OUT LATCH2,AL ;посылаем его ;---создание цмкла задержки MOV AH,0 ;номер функции чтения счетчика INT 1AH ;получаем значение счетчика MOV BX,OFFSET BEAT ;смещение таблицы длин MOV CL,[BX][SI] ;берем длину очередной ноты MOV CH,0 ; MOV BX,DX ;берем младшее слово счетчика ADD BX,CX ;определяем момент окончания STILL_SOUND: INT 1AH ;берем значение счетчика CMP DX,BX ;сравниваем с окончанием JNE STILL_SOUND ;неравны - продолжаем звук INC SI ;переходим к следующей ноте JMP NEXT_NOTE ; ;---завершение NO_MORE: IN AL,PORT_B ;получаем статус порта B AND AL,0FCH ;выключаем динамик OUT 61H,AL ;заменяем байт 2.2.6 Генерация строки тонов, одновременно с другими операциями. Хотя в Бейсике это делается очень просто, на самом деле это нетривиальный трюк программирования в реальном времени. Для реше- ния этой задачи нужно использовать генерацию звука через микрос- хему 8253 [2.2.3], так как метод, использующий микросхему 8255 [2.2.2], занимает процессор. Соответственно, только строки чистых музыкальных тонов могут производиться таким методом - всякого рода звуковые эффекты при этом недоступны. Основная техника прог- раммирования в реальном времени показана в [2.1.7]. Программы, работающие в реальном времени, модифицируют прерывание таймера, которое останавливает процессор 18.2 раз в секунду, чтобы изме- нить показание счетчика времени суток. Расширение процедуры пре- рывания сравнивает новое значение счетчика времени суток со зна- чением, показывающим время завершения генерации тона, и когда это значение достигнуто, прерывает звук, начинает генерацию другого тона и устанавливает время его окончания. Высокий уровень. Генерация строки звуков одновременно с другими операциями является одной из возможностей очень мощного оператора PLAY, который детально обсуждался в [2.2.5]. Надо просто добавить в начало управляющей строки MB. Это сокращение от Music Background (фоновая музыка); для того чтобы заставить PLAY прекратить все другие операции, пока генерация звуковой строки не будет заверше- на, вставьте MF. В нижеприведенном примере во время рисования и заполнения рамки исполняется гамма (для его работы требуется наличие графических возможностей). 100 PLAY "MB T100 O3 L4;CDEFG>ABC" 'исполняем набор нот 110 LINE (10,10)-(80,80),1,BF 'одновременно рисуем рамку Низкий уровень. Приведенная процедура является развитием процедуры, показанной в предыдущем разделе, на случай реального времени. Она требует понимания, как перепрограммировать прерывание таймера, что обсуж- далось в [2.1.7]. На эту процедуру должен указывать вектор преры- вания и тогда она будет выполняться 18.2 раза в секунду, в те моменты, когда будет обновляться значение счетчика времени суток BIOS. Обычно, будут выполняться только несколько строчек, которых достаточно, чтобы определить, что время изменения звука еще не наступило, - и процедура освождает процессор для решения других задач. Счетчик времени суток BIOS используется для измерения длитель- ности каждой ноты. При переходе от одной ноты к другой, длитель- ность новой ноты вычисляется как число импульсов счетчика и это значение добавляется к текущему его значению. Каждый раз при вызове процедуры проверяется текущее значение счетчика времени суток, и когда ожидаемое время наконец наступает, то выполняется набор операций по поиску новой ноты, программированию ее частоты в канале 2 микросхемы 8253 и установлению нового счетчика дли- тельности. Добавочный код требуется для обработки специальных случаев первой и последней нот в строке. ;---в сегменте данных BEAT DB 10,9,8,7,6,5,4,3,2 ;длительность нот FREQUENCY DW 2280,2031,1809,1709 ;таблица частот DW 1521,1355,1207,1139 ; MELODY DB 1,2,3,4,5,6,7,8,0FFH ;номер частоты в таблице HOLDIP DW 0 ;запоминаем оригинальный HOLDCS DW 0 ;вектор прерывания SOUND_NOW? DB 1 ;звук включен? FIRST_NOTE? DB 1 ;первая нота? END_NOTE DW 0 ;счетчик конца ноты WHICH_NOTE DW 0 ;указатель на текущую ноту ;---инициализация вектора прерывания ;изменение вектора PUSH DS ;сохраняем регистр MOV AX,SEG MELODY2 ;сегмент процедуры MOV DS,AX ;помещаем в DS MOV DX,OFFSET MELODY2 ;смещение процедуры MOV AL,1CH ;номер вектора прерывания MOV AH,25H ;функция установки вектора INT 21H ;изменение вектора POP DS ;восстановление регистра ; ;---программа работает дальше, постоянно вызывая процедуру ; ;---в конце программы восстанавливаем вектор прерывания MOV DX,0FF53H ;восстанавливаем оригинальные MOV AX,0F000H ;значения для вектора 1CH MOV DS,AX ; MOV AL,1CH ;номер прерывания MOV AH,25H ;функция установки вектора INT 21H ;восстанавливаем вектор RET ; ;---это само прерывание MELODY2 PROC FAR PUSH AX ;сохраняем изменяемые регистры PUSH BX ; PUSH CX ; PUSH DX ; PUSH DI ; PUSH SI ; PUSH DS ; MOV AX,SS:[114] ;берем начальный DS со стека MOV DS,AX ;восстанавливаем его CMP SOUND_NOW?,1 ;нужен ли звук? JE PLAY_IT ;если нет, то выход из прерывания JMP NOT_NOW ; PLAY_IT: CMP FIRST_NOTE?,0 ;это первая нота? JE TIME_CHECK ;если нет, то на установку времени ;---инициализация PORT_B EQU 61H ;определяем имена портов COMMAND_REG EQU 43H ; LATCH2 EQU 42H ; IN AL,PORT_B ;берем статус порта B OR AL,00000011B ;разрешаем динамик и таймер OUT PORT_B,AL ;посылаем байт обратно MOV SI,0 ;указатель на строки MOV AL,0B6H ;инициализация канала 2 таймера OUT COMMAND_REG,AL ;посылаем в командный регистр MOV FIRST_NOTE?,0 ;сбрасываем флаг первой ноты ;---ищем ноту, получаем ее частоту, посылаем в канал 2 NEXT_NOTE: LEA BX,MELODY ;берем смещение строки мелодии MOV SI,WHICH_NOTE ;указатель на текущую ноту MOV AL,[BX][SI] ;код текущей ноты строки CMP AL,0FFH ;проверяем признак конца JE NO_MORE ;если да, то на конец CBW ;иначе в словный формат ;получаем частоту MOV BX,OFFSET FREQUENCY ;смещение таблицы частот DEC AX ;начинаем отсчет с нуля SHL AX,1 ;умножаем на 2, т.к. словная MOV DI,AX ;адресуемся через DI MOV DX,[BX][DI] ;получаем частоту из таблицы ;начинаем исполнение ноты MOV AL,DL ;готовим младший байт частоты OUT LATCH2,AL ;посылаем в регистр задвижки MOV AL,DH ;готовим старший байт OUT LATCH2,AL ;посылаем его ;---пустой цикл, определяющий длительность нот TIME_IT: MOV AH,0 ;фнукция чтения счетчика INT 1AH ;получаем значение счетчика MOV BX,OFFSET BEAT ;смещение строки длин нот MOV CL,[BX][SI] ;длительность текущей ноты MOV CH,0 ; MOV BX,DX ;младшее слово значения счетчика ADD BX,CX ;добавляем длину в импульсах MOV END_NOTE,BX ;запоминаем время окончания TIME_CHECK: MOV AH,0 ;функция чтения счетчика INT 1AH ;читаем счетчик CMP DX,END_NOTE ;сравниваем с нужным JNE NOT_NOW ;если неравно, то выходим MOV SI,WHICH_NOTE ;иначе, берем следующую ноту INC SI ;увеличиваем номер ноты MOV WHICH_NOTE,SI ;запоминаем его JMP NEXT_NOTE ;начинаем следующую ноту ;---завершение процедуры NO_MORE: IN AL,PORT_B ;берем статус порта B AND AL,0FCH ;выключаем динамик OUT 61H,AL ;возвращаем байт MOV SOUND_NOW?,0 ;восстанавливаем переменные MOV FIRST_NOTE?,1 ; NOT_NOW: POP DS ;восстанавливаем регистры POP SI ; POP DI ; POP DX ; POP CX ; POP BX ; POP AX ; IRET ;возврат из прерывания MELODY2 ENDP 2.2.7 Создание плавного перехода тонов. Плавные переходы тонов производятся за счет непрерывного изме- нения частоты. Этого можно достигнуть как в Бейсике, так и прог- раммируя на низком уровне. Этот звуковой эффект можно сделать более выразительным, если немного уменьшать длительность каждого сегмента тона при повышении звука или слегка увеличивать длитель- ность при понижении. Высокий уровень. В Бейсике надо просто поместить оператор SOUND [2.2.2] в цикл, используя очень малые длины тонов. При каждом новом проходе цикла надо увеличивать частоту. Смотрите [2.2.8], где приведен пример использования оператора PLAY для более быстрых переходов. 100 FOR N = 1 TO 500 STEP 15 110 SOUND 400 + N,1 120 NEXT Низкий уровень. Проще всего использовать метод генерации звука, управляемый микросхемой интерфейса с периферией 8255. Просто меняйте значение бита 1 порта B между 0 и 1, используя для отсчета времени пустой цикл, как показано в [2.2.2]. При начале каждого нового пустого цикла, засчет засылки значения в CX, слегка изменяйте это значе- ние. Здесь тон повышается: ;---запрет микросхемы таймера PB EQU 61H ;адрес порта B микросхемы 8255 IN AL,PB ;получаем из него байт OR AL,1 ;сбрасываем бит 0 OUT PB,AL ;возвращаем байт в порт ;---установка частоты и длительности звука MOV BX,9000 ;начальное значение счетчика MOV DX,3000 ;длительность звука 3000 циклов REPEAT: ;сюда возвращаемся после цикла ;---установка бита динамика OR AL,00000010B ;устанавливаем бит 1 OUT PB,AL ;посылаем байт в порт B MOV CX,BX ;установка счетчика для 1/2 цикла CYCLE1: LOOP CYCLE1 ;пустой цикл на 1000 повторов ;---сброс бита динамика AND AL,11111101B ;сбрасываем бит 1 OUT PB,AL ;посылаем байт в порт MOV CX,BX ;установка счетчика CYCLE2: LOOP CYCLE2 ;пустой цикл ;---переход к следующему циклу DEC BX ;увеличиваем частоту, уменьшая DEC BX ;счетчик DEC DX ;уменьшаем оставшуюся длительность JNZ REPEAT ;если DX не 0, то новый цикл Этот простой метод приводит к тому, что высокие тона проходят значительно быстрее, чем низкие. Для коротких интервалов такой эффект может быть желательным, а когда он не нужен, надо добавить код, который при повышении тона пересылает в DX большие значения на следующем цикле. 2.2.8 Создание звуковых эффектов. Звуковые эффекты обычно достигаются непрерывным изменением частоты тона. Только PCjr достаточно хорошо оборудован для этой цели (см. обсуждение в [2.2.1]). На других машинах нельзя произ- водить звуковые эффекты одновременно с другими операциями. Высокий уровень. Благодаря мощности своих операторов SOUND и PLAY Бейсик позво- ляет достаточно легко создавать сложные звуковые эффекты. Но все должно быть сконструировано из чистых музыкальных тонов, а это значит, что эффект дисторции звука должен достигаться за счет такого быстрого изменения тона, что ухо не успевает разделить тона. Например, душераздирающее "чириканье" может быть получено при быстром переключении между одним и тем же тоном, отстоящим на несколько октав: 100 FOR N = 1 TO 100 'установка длительности 110 PLAY "L64 T255" 'самый быстрый темп 120 PLAY "O1A" 'выдаем низкое A 130 PLAY "O5A" 'выдаем высокое A 140 NEXT 'повтор При изменении частоты всего на несколько герц получаем вибрацию: 100 FOR N = 1 TO 100 'установка длительности 110 SOUND 440,1 'выдаем ноту A 120 SOUND 445,1 'немного меняем частоту 130 NEXT 'повтор Другая техника заключается во вложении плавно меняющихся тонов внутрь последовательности, которая сама гуляет по частотам вверх или вниз. На рис. 2-6 показана движущаяся вверх последователь- ность. Многие игры с лабиринтами используют эту технику: 100 FOR I = 1 TO 10 'число повторений 110 FOR J = 1 TO 6 'число разных октав 120 PLAY "MBL64T255O=J;BA#AG#GF#FED#DC#CC#DD#EFF#GG#AA#B" 130 NEXT 'повтор в более высокой октаве 140 NEXT 'повтор всей последовательности PCjr значительно более мощный, чем остальные машины, благодаря специальной микросхеме генератора звука. Оператор NOISE может производить множество звуков, формат этого оператора такой: NOISE источник, громкость, длительность Источник - это число от 0 до 7, значение которого приведено в таблице: 0 периодический шум в высоком диапазоне 1 периодический шум в среднем диапазоне 2 периодический шум в низком диапазоне 3 периодический шум, диапазон меняется с каналом 3 4 белый шум в высоком диапазоне 5 белый шум в среднем диапазоне 6 белый шум в низком диапазоне 7 белый шум, диапазон меняется с каналом 3 Громкость задается числом от 0 до 15, где 0 соответствует от- сутствию звука. Длительность указывается числом импульсов счетчи- ка времени суток, которые отсчитываются 18.2 раза в секунду. Низкий уровень. Любой из способов, показанных на Бейсике может быть реализован на ассемблере, хотя, как видно из предыдущих разделов, это тре- бует затрат на программирование. Кроме того, ассемблер позволяет генерировать нечистые тона, когда интервал, в течение которого динамик включен, не равен интервалу, в течение которого он выклю- чен. Такое нарушение симметрии может приводить к жужжащим и бря- кающим звукам. Когда отношение этих интервалов составляет, скажем 50 к 1, то получаем жужжание. Если увеличить отношение еще в 10 - 20 раз, то жужжание переходит в отдельные брякающие звуки. В любом случае звук генерируется микросхемой интерфейса с перифе- рией 8255, с помощью техники показанной в [2.2.2]. Вот пример жужжания: NUMBER_CYCLES EQU 300 ;число переключений динамика FREQUENCY1 EQU 50 ;время, когда динамик включен FREQUENCY2 EQU 3200 ;время, когда динамик выключен PORT_B EQU 61H ;адрес порта B микросхемы 8255 CLI ;запрет прерываний MOV DX,NUMBER_CYCLES;DX считает длину тона IN AL,PORT_B ;получаем статус порта AND AL,11111110B ;отключаем динамик от таймера NEXT_CYCLE: OR AL,00000010B ;включаем динамик OUT PORT_B,AL ;посылаем команду MOV CX,FREQUENCY1 ;задержка для первой части FIRST_HALF: LOOP FIRST_HALF ; AND AL,11111101B ;выключаем динамик OUT PORT_B,AL ;посылаем команду MOV CX,FREQUENCY2 ;задержка для второй части SECND_HALF: LOOP SECND_HALF ; DEC DX ;уменьшаем число циклов JNZ NEXT_CYCLE ;если 0, то пора кончать STI ;разрешаем прерывания Для создания брякающих звуков можно использовать этот же код, но надо заменить значение FREQUENCY2 на величину около 40000. 2.2.9 Одновременная генерация разных звуков. Только микросхема генератора звука, имеющаяся в PCjr, позво- ляет одновременно генерировать разные звуки (см. обсуждение в [2.2.1]). Однако ассемблер позволяет объединить два способа гене- рации звука, что создает имитацию одновременной генерации двух разных звуков. Интерференция этих двух сигналов приводит к слож- ной форме звуковой волны. Каждый из двух звуков имеет меньшую громкость, поэтому в результате получается скорее жужжание, чем два разных голоса. Этот прием реально полезен только для создания звуковых эффектов. Низкий уровень. Надо просто объединить два метода генерации звука, показанные в [2.2.2] и [2.2.3]. Начните звук через канал 2 микросхемы тайме- ра. Затем модулируйте выход динамика, за счет бита 1 порта B микросхемы интерфейса с периферией. Второе действие определяет продолжительность звука. Не забудьте выключить микросхему таймера при завершении. ;---начинаем генерацию звука через канал 2 таймера IN AL,61H ;получаем байт из порта B OR AL,3 ;устанавливаем младшие два байта OUT 61H,AL ;посылаем байт обратно MOV AL,10110110B ;цепочка для командного регистра 8253 OUT 43H,AL ;посылаем в регистр MOV AX,600H ;счетчик для канала 2 OUT 42H,AL ;посылаем младший байт MOV AL,AH ;готовим старший байт OUT 42H,AL ;посылаем старший байт ;---генерируем вторую частоту микросхемой 8255 NUMBER_CYCLES EQU 9000 ;число переключений FREQUENCY EQU 150 ;задержка для половины цикла CLI ;запрет прерываний MOV DX,NUMBER_CYCLES ;DX считает длину тона IN AL,61H ;получаем статус порта AND AL,11111111B ;отключаем динамик от таймера NEXT_CYCLE: OR AL,00000010B ;включаем динамик OUT 61H,AL ;посылаем назад в порт MOV CX,FREQUENCY ;задержка на 1/2 цикла FIRST_HALF: LOOP FIRST_HALF ; AND AL,11111101B ;выключаем динамик OUT 61H,AL ;посылаем команду в порт MOV CX,FREQUENCY ;задержка на 1/2 цикла SECOND_HALF: LOOP SECOND_HALF ; DEC DX ;меняем счетчик циклов JNZ NEXT_CYCLE ;если 0, то пора кончать STI ;разрешаем прерывания ;---выключение канала 2 микросхемы таймера IN AL,61H ;получаем статус порта AND AL,11111100B ;сбрасываем 2 младших бита OUT 61H,AL ;посылаем байт обратно Глава 3. Клавиатура. Раздел 1. Управление клавиатурой. Клавиатура содержит интелевский микропроцессор, который восп- ринимает каждое нажатие на клавишу и выдает скан-код в порт A микросхемы интерфейса с периферией [1.1.1], расположенной на системной плате. Скан-код это однобайтное число, младшие 7 битов которого представляют идентификационный номер, присвоенный каждой клавише. Таблица скан-кодов приведена в [3.3.2]. На всех машинах, кроме AT, старший бит кода говорит о том, была ли клавиша нажата (бит = 1, код нажатия) или освобождена (бит = 0, код освобожде- ния). Например, 7-битный скан-код клавиши B - 48, или 110000 в двоичной системе. Когда эта клавиша нажимается, то в порт A посы- лается код 10110000, а когда ее отпустили - код 00110000. Таким образом, каждое нажатие на клавишу дважды регистрируется в мик- росхеме 8255. И каждый раз микросхема 8255 выдает подтверждение микропроцессору клавиатуры. AT работает немного по-другому, посы- лая в обоих случаях один и тот же скан-код, но предваряя его кодом F0H, когда клавиша отпускается. Когда скан-код выдается в порт A, то вызывается прерывание клавиатуры (INT 9). Процессор моментально прекращает свою работу и выполняет процедуру, анализирующую скан-код. Когда поступает код от клавиши сдвига или переключателя, то изменение статуса записывается в память. Во всех остальных случаях скан-код транс- формируется в код символа, при условии, что он подается при нажа- тии клавиши (в противном случае, скан-код отбрасывается). Конеч- но, процедура сначала определяет установку клавиш сдвига и перек- лючателей, чтобы правильно получить вводимый код (это "a" или "A"?). После этого введенный код помещается в буфер клавиатуры, который является областью памяти, способной запомнить до 15 вво- димых символов, пока программа слишком занята, чтобы обработать их. На рис. 3-1 показан путь, который проходит нажатие на клавишу перед тем, как покасть в Вашу программу. Имеется два типа кодов символов, коды ASCII и расширенные коды. Коды ASCII - это байтные числа, которые соответствуют рас- ширенному набору кодов ASCII для IBM PC, который приведен в [3.3.3]. Для IBM PC этот набор включает обычные символы пишущей машинки, а также ряд специальных букв и символов псевдографики. ASCII коды включают также 32 управляющих кода, которые обычно используются для передачи команд периферийным устройствам, а не выводятся как символы на экране; однако каждый из них имеет соот- ветствующий символ, который может быть выведен на дисплей, с использованием прямой адресации дисплейной памяти [4.3.1]. (Стро- го говоря, только первые 128 символов являются настоящими симво- лами ASCII, так как ASCII - это аббревиатура от Американский стандартный код для обмена информацией. Но программисты обычно говорят о кодах ASCII, чтобы отличить их от других чисел. Напри- мер, "ASCII 8" относится к клавише "Backspace", в то время как "8" - это цифра, которой соответствует ASCII 56). Второй набор кодов, расширенные коды, присвоен клавишам или комбинациям клавиш, которые не имеют представляющего их символа ASCII, таким как функциональные клавиши или комбинации с клавишей Alt. Расширенные коды имеют длину 2 байта, причем первый байт всегда ASCII 0. Второй байт - номер расширенного кода, список которых приведен в [3.3.5]. Например, код 0:30 представляет Alt-A. Начальный ноль позволяет программе принадлежит ли данный код набору ASCII или расширенному набору. Имеется несколько комбинаций клавиш, которые выполняют спе- циальные функции и не генерируют скан-коды. Эти комбинации вклю- чают <Ctrl-Break>, <Ctrl-Alt-Del> и <PrtSc>, плюс <SysReq> для AT и <Ctrl-Alt-стрелка влево, -стрелка вправо, -CapsLock, -Ins> для PCjr. Эти исключения приводят к заранее предопределенным резуль- татам [3.3.2]. Все остальные нажатия клавиш должны интерпретиро- ваться Вашей программой и если они имеют специальное назначение, скажем сдвинуть курсор влево, то Ваша программа должна содержать код, обеспечивающий достижение этого эффекта. К счастью операционная система предоставляет различные проце- дуры для чтения кодов из буфера клавиатуры, включая средства для получения сразу целой строки. Поскольку эти процедуры позволяют делать практически все, что Вы можете пожелать, то практически бессмысленно писать свои процедуры обработки ввода с клавиатуры и поэтому в данной главе имеется очень мало примеров программирова- ния на низком уровне. Однако содержится обсуждение вопроса о том, как перепрограммировать прерывание клавиатуры. 3.1.1 Очистка буфера клавиатуры. Программа должна очистить буфер клавиатуры, перед тем, как выдать запрос на ввод, исключая тем самым посторонние нажатия клавиш, которые могут к тому времени накопиться в буфере. Буфер может накапливать до 15 нажатий на клавишу, независимо от того, являются ли они однобайтными кодами ASCII или двухбайтными расши- ренными кодами. Таким образом, буфер должен отвести два байта памяти для каждого нажатия на клавишу. Для однобайтных кодов первый байт содержит код ASCII, а второй - скан-код клавиши. Для расширенных кодов первый байт содержит ASCII 0, а второй номер расширенного кода. Этот код обычно совпадает со скан-кодом клави- ши, но не всегда, поскольку некоторые клавиши могут комбиниро- ваться с клавишами сдвига для генерации различных кодов. Буфер устроен как циклическая очередь, которую называют также буфером FIFO (первый вошел - первый ушел). Как и любой буфер он занимает непрерывную область адресов памяти. Однако не имеется определенной ячейки памяти, которая хранит "начало строки" в буфере. Вместо этого два указателя хранят позиции головы и хвоста строки символов, находящейся в буфере в текущий момент. Новые нажатия клавиш запасаются в позициях, следующих за хвостом (в более старших адресах памяти) и соответственно обновляется указа- тель хвоста буфера. После того, как израсходовано все буферное пространство, новые символы продолжают вставляться, начиная с самого начала буферной области; поэтому возможны ситуации, когда голова строки в буфере имеет больший адрес, чем хвост. После того как буфер заполнен, новые вводимые символы игнорируются, при этом прерывание клавиатуры выдает гудок через динамик. На рис. 3-2 показаны некоторые возможные конфигурации данных в буфере. В то время как указатель на голову установлен на первый вве- денный символ, указатель на хвост установлен на позицию за пос- ледним введенным символом. Когда оба указателя равны, то буфер пуст. Чтобы разрешить ввод 15 символов требуется 16-я пустая позиция, 2 байта которой всегда содержат код возврата каретки (ASCII 13) и скан-код клавиши <Enter>, равный 28. Эта пустая позиция непосредственно предшествует голове строки символов. 32 байта буфера начинаются с адреса 0040:001E. Указатели на голову и хвост расположены по адресам 0040:001A и 0040:001C, соответствен- но. Хотя под указатели отведено 2 байта, используется только младший байт. Значения указателей меняются от 30 до 60, что соот- ветствует позициям в области данных BIOS. Для очистки буфера надо просто установить значение ячейки 0040:001A равным значению ячей- ки 0040:001C. Отметим, что программа имеет возможность вставлять символы в буфер, завершая строку символом возврата каретки и соответственно меняя значения указателей. Если это проделать правильным образом перед завершением программы, то при возврате управления в MS DOS эти символы будут считаны и может быть автоматически загружена другая программа. Низкий уровень. В Бейсике для получения и изменения значений указателей буфера используются операторы PEEK и POKE: 100 DEF SEG = &H40 'устанавливаем значение сегмента 110 POKE &H1C, PEEK(&H1A) 'выравниваем указатели Этот метод не самый лучший. Некоторые программы могут создавать буфер где-нибудь в другом месте памяти, а кроме того, всегда существует возможность, что посреди строки 110 произойдет преры- вание клавиатуры, которое изменит указатель хвоста. По этим при- чинам лучше оставить указатели буфера в покое. Вместо этого, лучше читать из буфера до тех пор, пока не будет возвращен символ ASCII 0, показывающий, что буфер пуст: 100 IF INKEY$<>"" THEN 100 'берем следующее если не нуль Средний уровень. Функция 0C прерывания 21H выполняет любую из функций ввода с клавиатуры 1, 6, 7, 8 и A (описанных в этой главе), но перед этим чистит буфер клавиатуры. Надо просто поместить номер функции ввода в AL (в этом примере - 1): ;---очистка буфера перед ожиданием нажатия клавиши MOV AH,0CH ;выбираем функцию DOS 0CH MOV AL,1 ;выбираем функцию ввода символа INT 21H ;чистим буфер, ждем ввода Низкий уровень. Как и в примере высокого уровня делаем значение указателя на хвост равным значению указателя на голову. Для избежания влияния прерывания клавиатуры запрещаем прерывания на время модификации указателя: ;---выравниваем значения указателей на голову и хвост CLI ;запрещаем прерывания SUB AX,AX ;обнуляем регистр MOV ES,AX ;добавочный сегмент - с начала памяти MOV AL,ES:[41AH] ;берем указатель на голову буфера MOV ES:[41CH],AL ;посылаем его в указатель хвоста STI ;разрешаем прерывания 3.1.2 Проверка символов в буфере. Вы можете проверить был ли ввод с клавиатуры, не удаляя символ из буфера клавиатуры. Буфер использует два указателя, которые отмечают голову и хвост очереди символов, находящихся в буфере в текущий момент. Когда значения этих указателей равны, то буфер пуст. Надо просто сравнить содержимое ячеек памяти 0040:001A и 0040:001C. (Нельзя просто проверить символ, находящийся в голове очереди, поскольку буфер организован в виде циклической очереди и позиция ее головы постоянно меняется [3.1.1].) Высокий уровень. Надо просто использовать оператор PEEK для получения значений, а затем сравнить их: 100 DEF SEG = &H40 'устанавливаем сегмент на начало памяти 110 IF PEEK(&H1A)<>PEEK(&H1C) THEN ... '...то буфер не пуст Средний уровень. Функция 0BH прерывания 21H возвращает значение 0FFH в регистре AL, когда буфер клавиатуры содержит один или более символов и значение 0, когда буфер пуст: ;---проверка наличия символа в буфере MOV AH,0BH ;номер функции INT 21H ;вызываем прерывание 21H CMP AL,0FFH ;сравниваем с 0FFH JE GET_KEYSTROKE ;переход если буфер не пуст Функция 1 прерывания BIOS 16H предоставляет ту же возможность, но, кроме того, показывает какой символ в буфере. Флаг нуля (ZF) сбрасывается, если буфер пуст, и устанавливается, если в буфере имеется символ. В последнем случае копия символа, находящегося в голове буфера, помещается в AX, но символ из буфера не удаляется. В AL возвращается код символа для однобайтных символов ASCII, иначе ASCII 0 для расширенных кодов, и тогда номер кода - в AH. ;---проверяем наличие символа в буфере MOV AH,1 ;номер функции INT 16H ;проверка наличия символа JZ NO_CHARACTER ;переход если ZF = 1 ;---имеется символ - смотрим какой CMP AL,0 ;это расширенный код? JE EXTENDED_CODE ;если да, то на другую ветку Низкий уровень. Как и в примере высокого уровня просто сравниваем указатели: ;---сравниваем указатели на голову и хвост MOV AX,0 ;устанавливаем добавочный сегмент MOV ES,AX ;на начало памяти MOV AL,ES:[41AH] ;берем один указатель MOV AH,ES:[41CH] ;берем другой указатель CMP AH,AL ;сравниваем их JNE GET_KEYSTROKE ;если неравны, то к процедуре ввода 3.1.3 Ожидать ввод символа и не выводить его на экран. Обычно вводимые символы выводятся на экран, чтобы было видно, что напечатано. Но иногда автоматическое эхо на экране нежела- тельно. Например, выбор пункта меню по нажатию клавиши. Иногда надо сначала проверить вводимые символы на ошибку перед выводом на экран. В частности, любая программа, обрабатывающая расширен- ные коды, должна избегать автоматического эха, так как при этом первый байт этих кодов (ASCII 0) будет выводиться на экран, вставляя пробелы между символами. Высокий уровень. Функция Бейсика INKEY$ не дает эхо на терминал. Она возвращает строку длиной 1 байт для символов ASCII и длиной 2 байта для расширенных кодов. INKEY$ не ожидает нажатия клавиши, до тех пор, пока она не помещена в цикл, в котором ожидается нажатие клавиши. Цикл работает, обращаясь к INKEY$, а затем присваивая возвращае- мую им строку переменной, в данном случае C$. Если клавиша не была нажата, то INKEY$ возвращает нулевую строку, т.е. строку длиной ноль символов, которая обозначается двумя знаками кавычек, между которыми ничего нет (""). До тех пор пока INKEY$ возвращает "" - цикл повторяется: 100 C$=INKEY$:IF C$="" THEN 100. В нижеприведенном примере предполагается, что вводимые символы выбирают одну из возможностей меню и каждый выбор приводит к выполнению определенной процедуры программы. Выбор может быть сделан за счет нажатия клавиш A, B, C ... (давая 1-байтные коды ASCII) или Alt-A, Alt-B, Alt-C ... (давая 2-байтные расширенные коды). Для их распознавания используется функция LEN, которая определяет была ли строка длиной в 1 или 2 байта. В случа