ZX Format
#07
05 декабря 1997 |
|
Программистам - библиотечка математических процедур. Цикл статей для желающих научиться программировать на ассемблере.
О кодинге для начинащюих. music by COOPER (C) CREATOR product 1997 _______________________________ Итак, данная статья предназначена именно для тех, кто уже начал изучать ас- семблер, и хочет как-то продолжить, но не знает как именно. Возможно, я смогу Вам емного помочь. Для начала обговорим некоторые техничес- кие термины : бит - это самая маленькая единица инфор- мации, может быть равна 0 или 1. Байт - это 8 бит, сложенных вместе, мо- жет принимать значение от 0... 255. Слово = 2 байта - может принимать значе- ния от 0... 65535. Такт - это единица исчисления времени для процессора, например, как для нас се- кунды, только у процессора такты. Каждая команда выполняется за определенное коли- чество тактов (самая быстрая за 4 такта) Вот, так сказать, к примеру, некоторые команды : командакод такты NOP #00 4 LD HL,NN #21 N N 10 LD A,N #3E N 7 LD A,(NN) #3A N N 13 SRL (IX+S) #DD #CB S #3E 23 Узнать количество тактов, за которое выполняется команда можно из некоторых книжек, например: "Как написать игру на ассемблере" или "Програмирование в машинных кодах на язы- ке ассемблера" Инфорком 93 год. У меня на столе лежит вторая и надо сказать, что помогает довольно часто. Однако, почти во всех таблицах встречаются мелкие ошибки. Вот, вроде бы мы и установили начальную базу и теперь можем продолжать углублять свои знания. Первое, что необходимо (естественно) - знать команды ассемблера. Второе: надо бы знать Basic или какой иной язык программирования, иначе придет- ся совсем туго... Регистры процессора - используется только как рег. пара + можно использовать по одному * можно использовать только по одному AF + AF'+ IR * SP - BC + BC'+ IX + DE + DE'+ IY + HL + HL'+ PC - PC - как бы и регистром не является в смысле пользы для нас, он указывает на адрес работы процессора, то есть в каком адресе процессор исполняет команду. Он нужен самому процессору, например при ис- полнении команды CALL #nnnn процессор бе- рет регистр PC и скидывает на стек, чтобы знать куда вернуться. IR - Это не регистровая пара, а регистры одиночки, I-указывает на вектор прерыва- ния (при IM 2, при IM 1 не использует- ся),а регистр R нужен для регенерации ОЗУ, он увеличивается с исполнением каж- дой команды. A - Арифметический регистр, с ним можно делать все, что угодно. Команды ADD, ADC SUB, OR, XOR, AND, CPL работают с ним и еще одним данным (число или регистр). HL - регистр можно использовать как арифметический, но двухбайтовый. Также через него можно задавать адрес данных, например LD A,(HL), в регистр A будет по- мещено данное из ячейки памяти с адресом, содержащимся в HL. Или осуществлять кос- венную адресацию, например JP (HL), будет осуществлен переход на адрес, содержащий- ся в регистровой паре HL. DE, BC - почти одинаковы, только регистр B используется как счетчик в операциях DJNZ #nnnn. А регистровая пара DE легко меняется с HL одной командой EX DE,HL. У этих регистров, в основном, общее назна- чение. F - флаговый регистр. Содержит информа- цию о прошедших событиях в следущей фор- ме: бит имя содержание 0 C флаг переноса, он устанавливается в зависимости от того, было ли переполне- ние регистра. Например если сложить 10 и 20, то мы получим 30, это не выходит за 0 или за 255, и бит 0 будет сброшен (нахо- дится в 0). Если мы из 10 вычтем 20 то получим 246 (так как у процессора нет от- рицательных чисел) и бит 0 будет установ- лен в 1. То же случится если в сумме бу- ет больше 255. 1 N флаг сложения/вычитания 2 P/V флаг четности/переполнения 3 не используется 4 H флаг полупереноса 5 не используется 6 Z флаг нуля, он устанавливается в зависимости от того, был ли получен ре- зультат, равный нулю. Например, если из 10 вычесть 10,то он установится в 1, если из 10 вычесть 20,то будет сброшен. 7 S флаг знака На первых порах вам понадобятся только два флага - флаг переноса и флаг нуля. Остальные флаги используются редко даже очень опытными програмистами. IX, IY - индексные регистры. То есть с их помощью просто организовать доступ с таблице или массиву. Способ индексации следущий : например, в регистры A,B,C,D и E надо загрузить элементы массива (TAB) 0,1,2,3 и 4. LD IX,TAB ; устанавливаем ;регистр на начало таблицы. Также ;можно использовать и регистр IY,но ;осторожней, его использует Basic для ;своих нужд. LD A,(IX+0) LD B,(IX+1) LD C,(IX+2) LD D,(IX+3) LD E,(IX+4) SP - указатель стека. Он адресует некую часть памяти, отведенную под некий склад данных. В нем удобно хранить адреса/дан- ные которые надо временно сохранить. Его же используют команды CALL, RET, EX (SP),HL... Если, например, не хватает ре- гистров, и надо где-то сохранить промежу- точный результат,то делаем, например, PUSH HL, и регистровая пара HL сохраняет- ся на стеке, при этом регистр SP умень- шится на 2. Не стоит забывать, что если вы что-то сохранили на стеке,то надо эти данные снять до выхода из процедуры/прог- раммы, иначе команда RET получит вместо адреса возврата последнее положенное в стек значение. В процессоре есть два набора регистров, альтернативный набор можно использовать когда надо обработать большое количество информации и не хватает основного набора. Так как одновременно ими пользоваться нельзя, существует команда для их быстро- го обмена. Альтернативными HL'DE'BC' под- меняются текущие HL DE BC, по команде EXX, регистр AF меняется на AF' отдельной командой EX AF,AF'. Эти два набора ничем друг от друга не отличаются и определить, какой из них альтернативный, невозможно. Редакторы Для написания програм на ассемблере ну- жен редактор (ассемблер) их сейчас вели- кое множество TASM, ALASM, MASM, PASM ZXASM, XAS... Я пишу в XAS'e версии 7. 446, собственно ничего лучше я пока не видел, но каждому свое, посмотрите по возможности все и вы- берите себе свой. А для ознакомления с редактором жела- тельно прочитать его описание. Каждый из них весьма своеобразен (чисто функцио- нально, так что писать можно в любом из них, но с разными удобствами) так что, комментарии здесь излишни. Как начать. Я лично начал писать на ассемблере в тот же день, как стал изучать его. Дело было так: я уже хорошо разбирался в Ba- sic'e и написал на нем командер и базу данных. Скорость работы (особенно у базы данных) была просто отвратительная. И я засел за ассемблер, постепенно изучая его, переводил строки Basic'a в команды маш. кода. В итоге получилась двойная вы- года - сделал быстрым командер (по тем временам) и изучил основы работы ассемб- лера. Естественно, в начале я использовал ПЗУ Basic'a и его подпрограммы, в этом нет ничего страшного, это даже во многом вам поможет. Не стоит сразу же пытаться все делать на ассемблере, вам же легче будет. Начали... Так, теперь пора приступать к переводу какой-либо программы на Basic'e в ассемб- лер, используя ПЗУ и его процедуры. Бери- те книжечку с их описанием (процедур Ba- sic'a) и начинайте. Если у вас нет ничего подходящего,то можно писать одновременно и на Basic'e, и на ассемблере. Нежелательно использовать в работе дробные числа, синусы и. т. д., так как работа с ними в ассемблере нес- колько отличается от Basic'a и сложна для начинающих. Приведу некоторые разъяснения по поводу перевода Basic'a в ассемблер. По сути, регистры процессора являются аналогами переменных в Basic'e. Циклы FOR... NEXT так же легко заменяются на использование регистров/ячеек памяти (косвенной адреса- цией), либо LD B,N... DJNZ LABEL. Вызовы подпрограмм GOSUB... RETURN эквивалентны CALL... RET, GOTO N = JP #NNNN, и так да- лее... Начиная писать на ассемблере, мы избав- ляемся от ограничений Basic'a, от его "опеки" и получаем процессор в полное владение. Например, процессор понятия не имеет, что такое экран, для него это та- кой же кусок памяти, как и все остальные, так что работать с экраном придется вам самим. Продвигаемся... Если вы уже умеете писать, используя ПЗУ Basic'a,то все отлично. Надо двигать- ся дальше. Будем понемногу заменять ПЗУш- ные процедуры своими. Вот, к примеру, печать символа на экране - одна из самых нужных процедур. Ниже приведен один из самых быстрых вариантов для печати символов 8x8. Сделать печать строк, используя эту процедуру, не соста- вит никакого труда. Вызывать же процедуру надо следущим образом : в регистре A - код символа для печати, в регистре DE - координаты в экране координаты начинаются из верхнего левого угла) ;Процедура печати символа,код символа ;в регистре A, координаты в регистре DE PRINT LD L,A ;Отсчитаем нужное LD H,0 ;количество байт в ADD HL,HL ;фонте,так как ADD HL,HL ;один символ занимает ADD HL,HL ;8 байт, просто умножаем ;код нужного символа на 8 и прибавляем адрес нахождения фонта LD BC,15360 ;Адрес фонта в ПЗУ, можете создать свой фонт и указать здесь ;его адрес !НО! Будьте внимательны,эта процедура начинает пе- ;чатать с кода символа #00,а стандартные фонты (768 байт) с ;кода символа 32. Так что, если будете использовать такой ;фонт, то поставьте первой строкой в процедуре PRINT SUB 32 ADD HL,BC CALL POSIT ;Вычисляем реальный адрес в экране по координатам LD B,8 PRINT1 LD A,(HL) LD (DE),A INC HL INC D DJNZ PRINT1 RET ;Процедура перевода координат знакоместа на экране в реальный ;адрес,координаты содержатся в регистре DE. D-строка Е-стол- ;бец,на выходе в DE-адрес POSIT LD A,D AND 7 RRCA RRCA RRCA OR E LD E,A LD A,D AND #18 OR #40 LD D,A RET Если надо, то можно печатать и с цвета- ми. Для этого надо после вызова процедуры POSIT поставить: CALL COLOR и процедура печати с цветом готова. COLOR LD A,D AND %00011000 RRCA RRCA RRCA OR #58 LD B,A LD C,E LD A,(color) ; ячейка памяти, LD (BC),A ; где хранится RET ; байт цвета Печать строки. Для этой процедуры надо указать следущие данные : в регистре HL - адрес текста, который будет печататься, в конце текста должен обязательно стоять байт 0, для нахождения конца (можно заме- нить на любой другой). в регистре DE - координаты в экране. PR_LINE LD A,(HL) AND A RET Z PUSH DE CALL PRINT POP DE INC E ; приращение координаты ; для печати слева на право JR PR_LINE Печать чисел, тоже не редко встречающая- ся задача. Ее работа заключается в пере- воде реального числа в символьную строку. К примеру, возьмем байт для перевода его в символьный вид. Для этого надо выделить три переменные, так как он может прини- мать значения от 0 до 255. Мы возьмем ре- гистры B,C,A. B - сотни, C - десятки,A - единицы. Итак, помещаем нужный байт для перевода в регистр A. После выхода из процедуры, бу- дем иметь символьное представление числа в регистрах B,C и A,а способ их вывода на экран выбирайте сами. NUM_LINE LD B,48 LD C,B CP 200 ; сотни есть JR C,SM_200 LD B,"2" SUB 200 SM_200 CP 100 JR C,SM_100 INC B SM_100 CP 10 ; десятки есть JR C,SM_10 INC C SUB 10 JR SM_100 SM_10 ADD A,48 ; единицы RET Вот и все, для перевода больших чисел ничего кардинально не изменится, просто увеличится количество циклов внутри. Ес- тественно, это не самый быстрый способ, но один из самый доступных для понимания. Далее, Вы можете взять какой-либо отлад- чик и посмотреть, как сделаны процедуры в ПЗУ. Попробуйте их переписать. Теперь я думаю, что надо бы пройтись по самым узким местам программы (по быстро- действию): 1. циклы - они, безусловно, едят много времени,и в них очень не рекомендуется что-либо сохранять на стеке, надо по мере возможности, использовать другие регист- ры. Кстати о регистрах, очень удобно ис- пользовать в качестве оных LX, HX, LY, HY, если все остальные уже заняты. Но и тут не без трудностей, некоторые ассемб- леры не могут этого пережить (изменения регистров IX,IY),да и регистр IY лучше не изменять, потом пригодится,если можно, то лучше обойтись IX. Кстати, говоря о регистрах, если вы еще не в курсе, то регистр IX можно использо- вать как два регистра HX-старший байт, LX-младший. в основном, книжки об этом умалчивают. Одни команды с применением половинок IX и IY, образуются довольно легко - достаточно к команде работающей с регистром H или L добавить префикс #DD для HX и LX, #FD для HY и LY. Итак, для примера возьмем такой цикл, в котором заняты все регистры, кроме IX и приходится пользоваться стеком : (преиму- шества регистров A,C,D,E,L,H даже не сто- ит доказывать) LD B,100 ; 7 LOOP PUSHBC;11 ... POP BC;10 DJNZLOOP ; 13 Итак, работа цикла будет занимать : (99*13)+7+(100*(11+10))+7= 3401 такта. Заменяем регистр В на LX... LD LX,100 ; 8 LOOP ... DEC LX ; 8 JP NZ,LOOP; 10 Теперь получается : 100*(8+10)+8=1808 тактов! разница очевид- на примерно 1. 88 раз. Также в циклах следует избегать команд JR x,nnnn поскольку они занимают 12 так- тов, если условие выполняется и 7, если нет. В циклах это не целесообразно. 2. разветвление программы : допустим, у вас есть число и в зависимости от него надо перейти к соответствующей процедуре, это можно реализовать так: LD A,(NUMBER) ; 13 CP 0 ; 7 JR Z,NUMBER0 ; 12/7 CP 1 ; 7 JR Z,NUMBER1 ; 12/7 CP 2 ; 7 JR Z,NUMBER2 ; 12/7 ... а можно так: LD A,(NUMBER) ;13 AND A ;4 JR Z,NUMBER0 ;12/7 DEC A ;4 JR Z,NUMBER1 ;12/7 DEC A ;4 JR Z,NUMBER2 ;12/7 ... Так же заметно, что второе быстрее. Тут я заменил CP 0 на AND A, потому что они одинаково влияют на флаги,а AND A быстрее выполняется (также для проверки на 0 мож- но использовать OR A). В этой процедуре лучше использовать команду JR, чем JP,так как вероятность того, что сработает конк- ретная ветвь, гораздо меньше чем 50%,а именно так следует подбирать данные ко- манды (не забудьте, что JR может 'пры- гать' только до 128 байт, больше никак и если процедуры находятся на большом расс- тоянии, то от JP никуда не деться) 3. опрос клавиатуры : Если надо опросить 1... 10 клавиш,то можно прибегнуть к пря- мому чтению из портов клавиатуры : LD A,#7F IN A,(#FE) ;сейчас младшие пять бит регистра A будут иметь значения кла- ;виш : 0-бит space,symb shift,m,n,b если бит равен 0,то клави- <;ша нажата если 1, то нет. Ни в коем случае не сравнивайте ;данные с числом! Старшие 3 бита могут быть какими угодно и к ;тому же если будет нажата не одна клавиша ?... теперь прове- ;рим, например, кнопку space,ее значение в нулевом бите рег. A BIT 0,A JP Z,PRESS_SPACE ;а можно и так, использовав команду ротации RRCA, она работа- ;ет так: флаг C>76543210>C, то есть наш результат будет во ;флаге CARRY RRCA JP NC,PRESS_SPACE Это, кстати, похоже на пример с развет- лением если надо опросить больше битов,то так далее и продолжай. Если же опраши- вать всю клавиатуру,то лучше обратиться к ПЗУ,и пользоваться ее подпрограммой, ко- нечно если не очень существенно время ее исполнения. Ну а если время главное,то берите все данные по портам клавиатуры и пишите обработчик. Вот, кстати, порты клавиатуры: биты с 0. 4 порт caps shift,z,x,c,v #fe 254 a,s,d,f,g #fd 253 q,w,e,r,t #fb 251 1,2,3,4,5 #f7 247 0,9,8,7,6 #ef 239 p,o,i,u,y #df 223 enter,l,k,j,h #bf 191 space,symb shift,m,n,b #7f 127 4. Если возможно,то заменяйте сохранения на стеке использованием альтернативного набора регистров. Теперь мы рассмотрим процедуру построе- ния точки на экране, она уже будет пос- ложнее. Она может быть медленной - 130-150 тактов на точку, или быстрой, но более сложной, с таблицей. Кстати, а что такое таблица, и с чем её едят? Таблица - это куча байтов (или слов), которые содержат нужную информацию с быстрым доступом. Это я, конечно, сказа- нул не специально, просто по другому мне не сказать. Вот посмотрите на 'быструю точку'. Ей нужна маленькая инсталляция, т. е. перед использованием требуется за- пустить процедуру INSTALL (один раз), после чего можно пользоваться процедурой построения точки сколько угодно. Процеду- ра INSTALL как раз создает таблицу. С ис- пользованием таблицы отпадает необходи- мость что-то считать, а ведь именно подс- чет и занимает большую часть времени. INSTALL ; процедура инсталяции LD HL,PLOTT ; адрес таблици ;в 1024 байта для точки, младший байт адреса должен быть ра- ;вен #00! например #F0 или #BC00 LD DE,#40 ; адрес экрана LD B,E LD C,#80 ;* LD HX,4 LOOP3 LD LX,8 LOOP2 LD A,8 LOOP1 LD (HL),E INC H LD (HL),D INC H LD (HL),B INC H LD (HL),C RRC C DEC H DEC H DEC H INC HL INC D DEC A JR NZ,LOOP1 INC B LD A,B AND 31 LD B,A LD A,D SUB 8 LD D,A LD A,E ADD A,#20 LD E,A DEC LX JR NZ,LOOP2 LD A,D ADD A,8 LD D,A DEC HX JR NZ,LOOP3 RET PLOT ; процедура построения точки LD L,C LD H,PLOTT/256 LD A,(HL) INC H LD D,(HL) INC H LD L,C ADD A,(HL) LD E,A INC H LD A,(DE) OR (HL) ;OR (HL) можно заменить на XOR (HL) для наложения по принципу ;XOR,или на AND (HL) для стирания точек, но тогда уже надо за- ;менить регистр C на входе процедуры INSTALL с #80 на #7F LD (DE),A RET Как видно из процедуры PLOT, она почти ничего не считает, на входе у нее коорди- наты точки (в B 0.191 в C 0.255) и процедура в зависимости от входных данных берет нужные байты из таблицы. Попробуйте сами разобраться из чего состоит таблица, это, несомненно, пойдет на пользу. Список таких или подобных процедур,можно вести довольно долго, начинайте сами раз- бираться. Что вам нужно от процедуры, как ее сделать быстрее и.т.д. А я приведу вам математическую библиотечку нашей группы. Несомненно, она во многом вам поможет. Так что дерзайте. А если вам надо узнать что-то по-подробней, то пишите,а я поста- раюсь ответить на все ваши вопросы. _ (C) Copyright by Angel 2 MAIN CODE Cписок процедур: 1.DIV - деление INPUT : HL <-- что DE <-- на что OUTPUT: HL = HL/DE портятся DE,HL,A 2.KARE - возведение в квадрат INPUT : DE <-- что OUTPUT: HL = DE*DE портятся DE,HL,BC,A 3.MUL16 - умжножение INPUT : DE <-- что BC <-- на что OUTPUT: HL = DE*BC портятся DE,HL,BC,A 4.RAS - квадратный корень INPUT : HL <-- из чего OUTPUT: HL = SQR (HL) портятся DE,HL,BC,A 5.MHLA - умножение INPUT : HL <-- что A <-- на что OUTPUT: HL = HL*A портятся DE,HL,A 6.FACT - факториал INPUT : A <-- факторал чего OUTPUT: HL = A! портятся DE,HL,A 7.MULT_N - возведение в степень INPUT : BC <-- что A <-- степень OUTPUT: HL = BC^A портятся DE,HL,BC,A 8.PER - радианы -> градусы INPUT : DE <-- радианы OUTPUT: HL = (DE*PI)/180 портятся DE,HL,BC,A 9.PER_INV - градусы -> радианы INPUT : DE <-- градусы OUTPUT: HL = (DE*180)/PI портятся DE,HL,BC,A 10.SIN - f - функция синуса INPUT : C <-- угол,C = (0,180) OUTPUT: A = SIN (C) портятся DE,HL,BC,A 11.COS - f - функция косинуса INPUT : C <-- угол , C = (0,180) OUTPUT: A = COS (C) портятся DE,HL,BC,A + таблица TABLESC - для расчета f-ций SIN и COS _ ;+(C)-+ ;| HL = HL/DE | ;+(C)-+ DIV LD A,D OR E RET Z PUSHDE,BC LD A,1 DIV_0 PUSHHL SBC HL,DE JP C,HL0 SBC HL,DE JP C,DIV_1 DIV_01 INC A SLA E RL D POP HL JP DIV_0 DIV_1 POP HL LD BC,0 DIV_2 AND A JP NZ,DIV_3 LD H,B LD L,C POP BC,DE RET DIV_3 SBC HL,DE JP NC,DIV_4 ADD HL,DE DIV_4 CCF RL C RL B SRL D RR E DEC A JP DIV_2 HL0 CP 1 JP NZ,DIV_01 POP HL POP BC,DE LD HL,0 RET ;+(C)-+ ;| HL = DE*DE | ;+(C)-+ KARELD B,D LD C,E ;+(C)-+ ;| HL = DE*BC | ;+(C)-+ MUL16 LD HL,0 MUL16_2 LD A,B OR C RET Z SRL B RR C JP NC,MUL16_0 ADD HL,DE MUL16_0 SLA E RL D JP MUL16_2 RET ;+(C)-+ ;|HL = SQR (HL) | ;+(C)-+ RAS LD A,H OR L JR Z,RAS4 RAS1 LD A,H AND A JP NZ,RAS11 LD A,L CP 1 JP NZ,RAS11 LD HL,1 RET RAS11 LD B,H LD C,L SRL B RR C RAS_1 PUSHHL LD D,B LD E,C CALLDIV ADD HL,BC SRL H RR L PUSHHL LD D,B LD E,C SBC HL,DE JP NC,RES_10 ADD HL,DE EX DE,HL SBC HL,DE RES_10 LD A,H AND A JP NZ,RAS_0 LD A,L CP 2 JP NC,RAS_0 POP HL,BC RET RAS_0 POP BC,HL JP RAS_1 RAS_4 LD HL,0 RET ;+(C)-+ ;| HL = HL*A | ;+(C)-+ MHLAAND A JR Z,M1H ;FASTED bY CREATOR EX DE,HL LD HL,0 M2H SRL A JP NC,M3H ADD HL,DE M3H SLA E RL D AND A JP NZ,M2H RET MH1 LD H,A LD L,A RET ;+(C)-+ ;| HL = A! | ;+(C)-+ FACTLD HL,1 EX DE,HL FACT_ AND A RET Z PUSHAF CALLMULT EX DE,HL POP AF DEC A JP FACT_ ;+(C)-+ ;| HL = BC^A | ;+(C)-+ MULT_N LD D,B LD E,C MULT_N0 DEC A AND A JP Z,MULT_N1 PUSHAF CALLMUL16 POP AF EX DE,HL JP MULT_N0 MULT_N1 EX DE,HL RET ;+(C)-+ ;| HL = (DE*PI)/180| ;+(C)-+ PER LD BC,314 CALLMUL16 LD DE,180 CALLDIV RET ;+(C)-+ ;| HL = (DE*180)/PI| ;+(C)-+ PER_INV LD BC,180 CALLMUL16 LD DE,314 CALLDIV RET ;+(C)-+ ;| A = SIN (C) | ;+(C)-+ SIN LD A,90 CP C JP NC,SIN0 RLA SUB C LD C,A SIN0LD E,C LD D,0 LD HL,TABLESC ADD HL,DE LD A,(HL) RET ;+(C)-+ ;| A = COS (C) | ;+(C)-+ COS LD A,90 CP C JP NC,COS0 RLA SUB C LD C,A COS0LD E,C LD D,0 LD HL,TABLESC+90 SBC HL,DE LD A,(HL) RET TABLESC DEFB0,1,3,5,7,9,10,12,14 DEFB16,17,19,21,23,24 DEFB26,28,29,31,33,34,36 DEFB37,39,41,42,44,45,47 DEFB48,50,52,53,54,56,57 DEFB59,60,62,63,64,66,67,68 DEFB69,71,72,73,74,75,77,78 DEFB79,80,81,82,83,84,85 DEFB86,87,87,88,89,90,91,91 DEFB92,93,93,94,95,95,96 DEFB96,97,97,97,98,98,98,99 DEFB99,99,99,100,100,100 DEFB100,100,100 _
Другие статьи номера:
Похожие статьи:
В этот день... 21 ноября