Программирование контроллера прерываний i8259А

Одинадцатое. Итоги

Рейтинг:   / 0

  • Система прерываний микропроцессора Intel реализована весьма удачно. Ее применение позволяет достаточно гибко принимать и обрабатывать прерывания от различных источников.
  • Источники прерываний делятся на внешние и внутренние. Количество внешних источников ограничено числом выводов микросхемы i8259A и не может превышать 15. К этому количеству нужно добавить еще одно прерывание - немаскируемое. Его инициируют источники, требующие безотлагательного вмешательства со стороны микропроцессора. Остальные источники прерываний являются внутренними. Общее количество источников прерываний в микропроцессоре не превышает 256. Внутренние источники прерываний также делятся на две группы: программные прерывания и исключения.
  • Любое из этих прерываний можно вызвать как стандартными для этого вида прерывания средствами, так и командой int xx.
  • Каждое прерывание связано с программой его обработки посредством таблицы векторов прерываний, которая в реальном режиме работы микропроцессора находится в первом килобайте оперативной памяти.
  • Механизм обработки аппаратных прерываний основан на использовании микросхемы i8259А, которая позволяет организовать гибкую обработку прерываний.
  • Микросхема i8259А является программируемой, что позволяет выполнить такие операции, как задание различных дисциплин обслуживания прерываний, запрещения отдельных прерываний и т. п.
  • Программирование микросхемы i8259А осуществляется специальными последовательностями управляющих и операционных слов.

 

Десятое. Обработка прерываний в реальном режиме

Рейтинг:   / 0

Обработка прерываний (как внешних, так и внутренних) в реальном режиме микропроцессора производится в три этапа:

  1. Прекращение выполнения текущей программы.
  2. Переход к выполнению и выполнение программы обработки прерываний.
  3. Возврат управления прерванной программе.

Первый этап должен обеспечить временное прекращение выполнения текущей программы таким образом, чтобы потом прерванная программа продолжила свою работу так, как будто никакого прерывания не было. Любая программа, загруженная для выполнения операционной системой, занимает свое, отдельное от других программ, место в оперативной памяти. Разделяемыми между программами ресурсами являются регистры микропроцессора, в том числе регистр флагов, поэтому их содержимое нужно сохранять. Обязательными для сохранения являются регистры cs, ip и flags\eflags, поэтому они при возникновении прерывания сохраняются микропроцессором автоматически. Пара cs:ip содержит адрес команды, с которой необходимо начать выполнение после возврата из программы обслуживания прерывания, а flags\eflags - состояние флагов после выполнения последней команды прерванной программы в момент передачи управления программе обработки прерывания. Сохранение содержимого остальных регистров должно обеспечиваться программистом в начале программы обработки прерывания до их использования. Наиболее удобным местом хранения регистров является стек. В конце первого этапа микропроцессор после включения в стек регистров flags, cs и ip сбрасывает бит флага прерываний IF в регистре flags (но при этом в стек записывается предыдущее содержимое регистра flags с еще установленным IF). Тем самым предотвращаются возможность возникновения вложенных прерываний по входу INTR и порча регистров исходной программы вследствие неконтролируемых действий со стороны программы обработки вложенного прерывания. После того как необходимые действия по сохранению контекста завершены, обработчик аппаратного прерывания может разрешить вложенные прерывания командой sti.
Набор действий по реализации второго этапа заключается в определении источника прерывания и вызова соответствующей программы обработки. В реальном режиме микропроцессора допускается от 0 до 255 источников прерываний. Количество источников прерываний ограничено размером таблицы векторов прерываний. Эта таблица выступает связующим звеном между источником прерывания и процедурой обработки. Данная таблица располагается в памяти, начиная с адреса 0. Каждый элемент таблицы векторов прерываний занимает 4 байта и имеет следующую структуру:

  • 1-е слово элемента таблицы - значение смещения начала процедуры обработки прерывания (n) от начала кодового сегмента;
  • 2-е слово элемента таблицы - значение базового адреса сегмента, в котором находится процедура обработки прерывания.

Определить адрес, по которому находится вектор прерывания с номером n, можно следующим образом:

смещение_элемента_таблицы_векторов_прерываний = n * 4

Таким образом, полный размер таблицы векторов прерываний 4 * 256 = = 1024 байт.
Теперь понятно, что на втором этапе обработки прерывания микропроцессор выполняет следующие действия:

  1. По номеру источника прерывания путем умножения на 4 определяет смещение в таблице векторов прерываний.
  2. Помещает первые два байта по вычисленному адресу в регистр ip.
  3. Помещает вторые два байта по вычисленному адресу в регистр cs.
  4. Передает управление по адресу, определяемому парой cs:ip.

Далее выполняется сама программа обработки прерывания. Она, в свою очередь, также может быть прервана, например, поступлением запроса от более приоритетного источника. В этом случае этапы 1 и 2 будут повторены для вновь поступившего запроса.
Набор действий по реализации этапа 3 заключается в восстановлении контекста прерванной программы. Так же, как и на этапе 1, на данном последнем этапе есть действия, выполняемые микропроцессором автоматически, и действия, задаваемые программистом. Основная задача на этапе 3 - привести стек в состояние, в котором он был сразу после передачи управления данной процедуре. Для этого программист указывает необходимые действия по восстановлению регистров и очистке стека. Этот участок кода необходимо защитить от возможности искажения содержимого регистров (в результате появления аппаратного прерывания) с помощью команды cli. Последние команды в процедуре обработки прерывания - sti и iret, при обработке которых микропроцессор выполняет следующие действия:

  1. sti - разрешить аппаратные прерывания по входу INTR;
  2. iret - извлечь последовательно три слова из стека и поместить их, соответственно, в регистры ip, cs и flags.

В результате этапа 3 управление возвращается очередной команде прерванной программы, которая должна была выполниться, если бы прерывания не было.
Аппаратные прерывания могут быть инициированы программно командой микропроцессора int n, где n - номер аппаратного прерывания в соответствии с таблицей векторов прерываний. При этом микропроцессор также сбрасывает флаг IF, но не вырабатывает сигнал INTA.
После такого обстоятельного обсуждения нам осталось рассмотреть хороший пример. Он должен показать нам ключевые моменты программирования программных и аппаратных прерываний. Выберем одно программное и одно аппаратное прерывание. Наиболее «частое» аппаратное прерывание - прерывание от таймера. Что касается программного прерывания, то основное требование при его выборе для нашего эксперимента то, чтобы его номер не совпадал с номером какого-нибудь системного прерывания.
Программа, которую мы должны будем разработать, выполняет следующие действия: подключает новый аппаратный обработчик прерываний от таймера 08h, который на каждый 4-й сигнал в цикле выводит на экран символы (0-9). Пользовательское прерывание (новый обработчик прерывания 0ffh) вызывается после запуска прерывания от таймера. Его работа заключается в выдаче сигнала сирены циклически несколько раз. После этого пользовательское прерывание производит восстановление вектора старого обработчика прерывания от таймера и завершает свою работу вместе со всей программой.
Перед обсуждением программы нужно сделать следующее замечание. По сути, мы ставим себе цель дополнить программу обработки прерывания от таймера некоторым новым свойством. Одновременно, мы не хотим портить старый обработчик этого прерывания. Такая ситуация встречается довольно часто. Существуют различные способы сцепления системных обработчиков прерываний с пользовательскими. Прерывание от таймера 08h интересно тем, что программа его обработки предусматривает возможность того, что пользователь захочет вставить в обработчик свой код. С этой целью из системной программы обработки прерывания 08h делается вызов еще одного прерывания с номером 1сh. Это пустое прерывание, обработчик которого содержит всего одну команду iret. Таким образом, пользователь имеет возможность решить проблему сцепления своего обработчика с системным обработчиком прерывания 08h косвенно - попросту заменив вектор обработчика прерывания 1сh. Этот прием реализован в листинге ниже (программный код).

 

Листинг

<1>      ;prg15_1.asm
<2>      MASM
<3>      MODEL   small            ;модель памяти
<4>      STACK   256            ;размер стека
<5>       .486p
<6>      delay   macro   time
<7>      local   ext,iter
<8>      ;макрос задержки
<9>      ;На входе - значение переменной задержки (в мкс)
<10>      push   cx
<11>      mov   cx,time
<12>   ext:
<13>      push   cx
<14>   ;в cx одна мкс,это значение можно
<15>   ;поменять в зависимости от производительности процессора
<16>      mov   cx,5000
<17>   iter:
<18>      loop   iter
<19>      pop   cx
<20>      loop   ext
<21>      pop   cx
<22>   endm   ;конец макроса
<23>   .data
<24>   tonelow   dw   2651      ;нижняя граница звучания 450 Гц
<25>   cnt   db   0         ;счетчик для выхода из программы
<26>   temp   dw   ?         ;верхняя граница звучания
<27>   old_off8   dw   0      ;для хранения старых значений вектора
<28>   old_seg8   dw   0      ;сегмент и смещение
<29>   time_1ch   dw   0      ;переменная для пересчета
<30>   .code               ;начало сегмента кода
<31>   off_1ch   equ   1ch*4      ;смещение вектора 1ch в ТВП
<32>   off_0ffh   equ   0ffh*4      ;смещение вектора ffh в ТВП
<33>   char   db   "0"         ;символ для вывода на экран
<34>   maskf   db   07h         ;маска вывода символов на экран
<35>   position   dw   2000      ;позиция на экране - почти центр
<36>   main   proc
<37>      mov   ax,@data
<38>      mov   ds,ax
<39>      xor   ax,ax
<40>      cli            ;запрет аппаратных прерываний на время
<41>                  ;замены векторов прерываний
<42>   ;замена старого вектора 1ch на адрес new_1ch
<43>   ;настройка es на начало таблицы векторов
<44>   ;прерываний - в реальном режиме:
<45>      mov   ax,0
<46>      mov   es,ax
<47>   ;сохранить старый вектор
<48>      mov   ax,es:[off_1ch]   ;смещение старого вектора 1ch в ax
<49>      mov   old_off8,ax      ;сохранение смещения в old_off8
<50>      mov   ax,es:[off_1ch+2]   ;сегмент старого вектора 1ch в ax
<51>      mov   old_seg8,ax      ;сохранение сегмента в old_seg8
<52>   ;записать новый вектор в таблицу векторов прерываний
<53>      mov   ax,offset new_1ch   ;смещение нового обработчика в ax
<54>      mov   es:off_1ch,ax
<55>      push   cs
<56>      pop   ax         ;настройка ax на cs
<57>      mov   es:off_1ch+2,ax   ;запись сегмента
<58>   ;инициализировать вектор пользовательского прерывания 0ffh
<59>      mov   ax,offset new_0ffh
<60>      mov   es:off_0ffh,ax   ;прерывание 0ffh
<61>      push   cs
<62>      pop   ax
<63>      mov   es:off_0ffh+2,ax
<64>      sti            ;разрешение аппаратных прерываний
<65>   ;задержка, чтобы новый обработчик таймера вывел символы на экран
<66>      delay   3500
<67>   ;завершение программы
<68>      int   0ffh
<69>   exit:
<70>      mov   ax,4c00h
<71>      int   21h
<72>   main   endp
<73>   new_1ch   proc         ;новый обработчик прерывания от таймера
<74>   ;сохранение в стеке используемых регистров
<75>      push   ax
<76>      push   bx
<77>      push   es
<78>      push   ds
<79>   ;настройка ds на cs
<80>      push   cs
<81>      pop   ds
<82>   ;запись в es адреса начала видеопамяти - B800:0000
<83>      mov   ax,0b800h
<84>      mov   es,ax
<85>      mov   al,char   ;символ в al
<86>      mov   ah,maskf   ;маску вывода - в ah
<87>      mov   bx,position   ;позицию на экране - в bx
<88>      mov   es:[bx],ax   ;вывод символа в центр экрана
<89>      add   bx,2      ;увеличение позиции
<90>      mov   position,bx   ;сохранение новой позиции
<91>      inc   char      ;следующий символ
<92>   ;восстановление используемых регистров:
<93>      pop   ds
<94>      pop   es
<95>      pop   bx
<96>      pop   ax
<97>      iret         ;возврат из прерывания
<98>   new_1ch   endp      ;конец обработчика
<99>   new_0ffh   proc      ;новый обработчик пользовательского прерывания
<100>   sirena:
<101>   ;сохранение в стеке используемых регистров
<102>      push   ax
<103>      push   bx
<104>   ;проверка для пересчета на 4:
<105>      test   time_1ch,03h
<106>      jnz   leave_it   ;если два правых бита не 11, то на выход,                         ;иначе:
<107>   go:
<108>      mov   ax,0B06h   ;заносим слово состояния 110110110b
<109>   ;(0В6h) - выбираем второй канал порта 43h (динамик)
<110>      out   43h,ax   ;в порт 43h
<111>      in   al,61h   ;получим значение порта 61h в al
<112>      or   al,3      ;инициализируем динамик - подаем ток
<113>      out   61h,al   ;в порт 61h
<114>      mov   cx,2083   ;количество шагов
<115>   musicup:
<116>   ;значение нижней границы частоты в ax (1193000/2651=450 Гц),
<117>   ;где 1193000 - частота динамика
<118>      mov   ax,tonelow
<119>      out   42h,al   ;в порт 42h - младшее слово ax:al
<120>      mov   al,ah      ;обмен между al и ah
<121>      out   42h,al   ;в порте 42h уже старшее слово ax:ah
<122>      add   tonelow,1   ;увеличение частоты
<123>      delay   1      ;задержка на 1 мкс
<124>      mov   dx,tonelow   ;текущее значение частоты - в dx
<125>      mov   temp,dx   ;в temp - верхнее значение частоты
<126>      loop   musicup   ;повторить цикл повышения
<127>      mov   cx,2083
<128>   musicdown:
<129>      mov   ax,temp   ;верхнее значение частоты - в ax
<130>      out   42h,al   ;младший байт ax:al в порт 42h
<131>      mov   al,ah      ;обмен между al и ah
<132>      out   42h,al   ;старший байт ax:ah в порт 42h
<133>      sub   temp,1   ;уменьшение частоты
<134>      delay   1      ;задержка на 1 мкс
<135>      loop   musicdown   ;повторить цикл понижения
<136>   nosound:
<137>      in   al,61h   ;значение порта 61h - в al
<138>   ;слово состояния 0fch - выключение динамика и таймера
<139>      and   al,0fch
<140>      out   61h,al   ;в порт 61h
<141>      mov   dx,2651   ;для последующих циклов
<142>      mov   tonelow,dx
<143>      inc   cnt      ;инкремент количества проходов
<144>      cmp   cnt,2      ;если сирена не звучала двух                   ;раз - повторный запуск
<145>      jne   go
<146>   leave_it:         ;выход
<147>      inc   time_1ch   ;пересчет на 4
<148>   ;восстановление используемых регистров
<149>      pop   bx
<150>      pop   ax
<151>   ;восстановление вектора прерывания от таймера
<152>      cli         ;запрет аппаратных прерываний
<153>      xor   ax,ax      ;снова настройка es на начало таблицы
<154>      mov   es,ax      ;векторов прерываний
<155>      mov   ax,old_off8   ;запись в таблицу смещения старого
<156>      mov   es:off_1ch,ax   ;обработчика прерывания от таймера
<157>      mov   ax,old_seg8   ;запись сегмента
<158>      mov   es:off_1ch+2,ax
<159>      sti   ;разрешение аппаратных прерываний
<160>      iret   ;возврат из прерывания
<161>   new_0ffh   endp   ;конец обработчика
<162>   end   main   ;конец программы

Обсудим листинг. Основная процедура main (строки 36-72) выполняет инициализацию используемых векторов прерываний. При этом необходимо запомнить содержимое старого вектора прерывания 1ch (строки 45-51), так как его придется восстанавливать перед завершением программы. Содержимое вектора пользовательского прерывания 0ffh сохранять нет смысла, так как его номер выбран исходя из того, что он не используется при работе системы. При смене вектора прерывания 1сh необходимо запретить обработку аппаратных прерываний командой cli (строка 40), так как внешние прерывания являются асинхронными и могут прийти в самый неподходящий момент, в том числе и во время смены содержимого вектора. Перед завершением работы аппаратного прерывания необходимо явно выдать сигнал EOI. Но в нашем случае это делать необязательно, так как за нас это сделает системный обработчик прерывания 08h, из которого вызывается обработчик для прерывания 1ch. В строках 52-57 и 58-63 производится запись новых значений векторов 1ch и 0ffh в таблицу векторов прерываний. После того как в строке 64 командой sti будут разрешены аппаратные прерывания, на экран будут выведены символы. Эти действия выполняет новая программа обработки прерывания для вектора 1ch (строки 73-98). Эти символы будут выводиться до тех пор, пока действует программная задержка, которую мы организовали в строке 66. После этого вызывается пользовательское прерывание 0ffh, программа обработки которого (строки 99-161) отрабатывает несколько циклов генерации сигнала «сирена» (мы обсуждали эту программу на уроке 7, и теперь вы уже в состоянии оформить ее в виде макроса или процедуры). Вызов программы обработки прерывания пользователя new_0ffh осуществляется с помощью специальной команды int. Эта команда предназначена для того, чтобы пользователь сам мог инициировать вызов прерываний. Как видите, эти прерывания являются планируемыми (синхронными), так как пользователь сам определяет момент его вызова.
После написания этой программы можно провести несколько экспериментов для исследования работы контроллера прерываний и системы прерываний в целом. К примеру, можно выполнить следующие операции:

  • Изменить базовый адрес ведущего контроллера прерываний. Как мы обсуждали выше, BIOS инициализирует ведущий контроллер таким образом, что он имеет базовый адрес 08h. Попробуйте теперь изменить значение базового вектора, например, на значение 0f0h. Для этого необходимо выполнить инициализацию контроллера, которая заключается в последовательной посылке в него управляющих слов. Посмотрите последовательность приказов, которые BIOS посылает в контроллер прерываний для его инициализации при загрузке системы. Нам тоже нужно будет их сформировать, но с нужными нам значениями, и послать в контроллер прерываний. Фрагмент, осуществляющий такие действия, может выглядеть следующим образом:  
    ...
    mov      al,00010001b
    out      20h,al      ;ICW1 в порт 20h
    jmp      $+2
    jmp      $+2      ;задержка, чтобы успела
    ;отработать аппаратура
    mov      al,0f0h
    out      21h,al      ;ICW2 в порт 20h - новый базовый номер
    jmp      $+2
    jmp      $+2      ;задержка, чтобы успела
    ;отработать аппаратура
    mov      al,00000100b
    out      21h,al      ;ICW3 - ведомый подключается
    ;к уровню 2 (см. рис. 15.1)
    jmp      $+2
    jmp      $+2       ;задержка, чтобы успела
    ;отработать аппаратура
    mov      al,00000001b
    out      21h,al         ;ICW4 - EOI выдает
    ;программа пользователя
    Данный фрагмент нужно вставить в начало процедуры main листинга после команды cli. После этого вектору прерывания от таймера будет соответствовать значение 0f0h. Соответственно, если вы хотите, чтобы программа листинга работала как прежде, вам нужно настроить вектор 0f0h на системную программу обработки прерывания 08h. Техника такой замены аналогична приведенной в листинге. После этого можно разрешить прерывания командой sti. Но правильно работать будет только прерывание от таймера, все остальные прерывания (например, от клавиатуры) будут приводить к зависанию компьютера. Если вы подобным образом перепрограммировали контроллер, то перед завершением программы нужно провести обратное перепрограммирование, чтобы вернуть старое значение базового адреса. Если этого не сделать, то работа системы будет нарушена - все аппаратные прерывания будут попадать «не туда».
  • Рассмотреть альтернативу команде cli, замаскировав аппаратные прерывания, используя прямое программирование регистра масок IMR:
    ;запретить прерывания
    mov      al,0ffh
    out      21h,al       ;для ведущего контроллера
    out      A1h,al       ;для ведомого контроллера
    ;разрешить прерывания
    mov      al,00h
    out      21h,al       ;для ведущего контроллера
    out      A1h,al      ;для ведомого контроллера
    Попробуйте использовать эти команды в листинге вместо команд cli и sti.
  • Запретить аппаратные прерывания определенных уровней. Например, в следующем фрагменте запрещаются прерывания от клавиатуры:  
    in      al,21h
    or      al,00000010b
    out      21h,al
  • Исследовать, как меняется содержимое регистров IRR, IMR и ISR в ходе обработки аппаратного прерывания, читая состояние описанных выше портов. Если у вас проснулся интерес к подобной исследовательской деятельности, то предлагаю вам самостоятельно написать эти фрагменты программ и исследовать их с использованием листинга.

 

 

 



Sitelinkx by eXtro-media.de

Седьмое. OCW3 - общее управление контроллером

Рейтинг:   / 0

Этот приказ (см. таблицу ниже) служит для общего управления контроллером. Приказ OCW3 посылается в порт 20h.

 

Этой информации уже вполне достаточно для написания и понимания реальных программ. В качестве примера рассмотрим последовательность приказов, которые выдает BIOS при загрузке системы после включения питания (см. таблицу ниже). Это будет хорошей иллюстрацией к вышеприведенным рассуждениям.

 

 

 



Sitelinkx by eXtro-media.de

Девятое.Реальный режим работы микропроцессора

Рейтинг:   / 0

Для тех пользователей, которые работали с микропроцессорами i8086 или i8088, нет необходимости пояснять особенности этого режима. Относительно недавно это был единственный режим, в котором функционировала популярная операционная система MS-DOS. Для нее был разработан большой объем программного обеспечения. Понимая все это и не желая терять рынок, фирма Intel во всех модернизациях своего микропроцессора поддерживает этот режим. В нашей книге при написании программ мы, до сего момента, также подразумевали реальный режим. Вот некоторые его характеристики:

  • пространство оперативной памяти делится на сегменты по 64 Кбайт. Сегменты в памяти могут перекрываться;
  • страничное преобразование адреса запрещено, то есть физический адрес равен линейному и формируется как сумма двух составляющих (см. урок 2):
    • 16-разрядного эффективного адреса, который, в свою очередь, является суммой трех составляющих: базы, смещения и индекса;
    • 20-разрядного результата сдвига содержимого конкретного сегментного регистра на 4 разряда влево;
  • максимальное значение физического адреса равно 0ff fffh, то есть 1 Мбайт, но, фактически, в реальном режиме микропроцессора адресуется на 64 Кбайт больше, что следует из следующего вычисления:
    ffff0 - максимальное значение сегментной части адреса, сдвинутое на 4 раз- ряда влево;
    +
    0ffff - максимальное значение смещения;
    10ffef = 1 114 096 байт - максимальный физический адрес в реальном режиме.

Этот пример говорит о том, что в модели микропроцессоров, начиная с i286, при определенных обстоятельствах возможна адресация оперативной памяти за пределами первого мегабайта. Это обстоятельство даже использовалось последними версиями MS-DOS для размещения служебных программ в этом дополнительном сегменте памяти. Формирование значений адреса сразу за первым мегабайтом возможно и в микропроцессоре i8088/86. В нем при появлении физического адреса большего 0fffffh, например 1 000 054h, микропроцессор отбрасывает 21-й единичный бит. Происходит так называемое «заворачивание» адреса, поэтому сформированный физический адрес на шине адреса будет равен 00054h. Для того чтобы обеспечить полную эмуляцию данной особенности микропроцессора i8088/86, в моделях микропроцессоров, начиная с i80286, была предусмотрена возможность блокировки адресной линии А20 (управление тем самым 21-м битом адреса). Для обеспечения доступа к адресам оперативной памяти, лежащим за пределами первого мегабайта, необходимо специальным образом открывать эту адресную линию;

  • в реальном режиме схема распределения оперативной памяти - фиксированная. Перечислим расположение некоторых из системных областей, которые потребуются нам в дальнейшем:
    • в диапазоне адресов 00000h-003ffh (первый мегабайт оперативной памяти) находится таблица векторов прерываний (ТВП). Она содержит 256 векторов прерываний размером 4 байта (указателей на программы обработки прерываний);
    • в диапазоне адресов 00400h-006ffh сразу за таблицей векторов прерываний располагается область памяти, содержащая жестко структурированные данные, обеспечивающие работу BIOS и MS-DOS;
    • с адреса 0b8000h располагается область видеопамяти, в которой формируется изображение, которое мы видим на экране.

 



Sitelinkx by eXtro-media.de

Шестое. OCW2 - управление приоритетом

Рейтинг:   / 0

Этот приказ (см. таблицу ниже) используется для управления приоритетом и учета особенностей завершения обслуживания прерывания в контроллере. Он определяет выполнение следующих действий:

  • сбросить бит в ISR с наибольшим приоритетом;
  • сбросить бит в ISR для определенного уровня прерываний;
  • установить низший приоритет для определенного уровня;
  • поменять приоритеты уровней с максимальным и минимальным приоритетами;
  • поменять приоритеты уровней с заданным и минимальным приоритетами.

Данный приказ посылается в порт 20h.

 

 

Яндекс.Метрика