КаталогИндекс раздела
НазадОглавлениеВперед


6. Функции таймера - звук и время

Микросхема таймера генерирует импульсы частоты 1193180 гц. Эта последовательность импульсов поступает на три канала таймера. В каждом канале есть свой счетчик, работающий как делитель частоты, максимальное число которое может быть записано в счетчике (коэффициент деления) - 655357. Счетчики каналов таймера независимы друг от друга и доступны для программиста. Назначение каналов таймера следующее:

Каждый канал может работать в одном из 6 режимов, но программисты, как правило, используют его в режиме 3 (генератор меандра).

Программирование канала таймера представляет собой запись числа в счетчик канала. Имеется один управляющий порт - 0x43 для всех каналов и по одному порту данных для каждого канала - 0x40, 0x41, 0x42. При программировании следует записать в порт 0x43 управляющий байт, который обычно имеет вид:

 x x 1 1 0 1 1 0 
где xx - номер канала таймера, а затем послать в порт данных выбранного канала сначала младший, а затем старший байт счетчика.

6.1. Генерация звука

Если динамик ПЭВМ включен и управляется от таймера, то высота генерируемого звука определяется частотой импульсов канала 2 и зависит от коэффициента деления, записанного в счетчике канала. Включение-выключение динамика управляется двумя разрядами в однобайтном регистре контроллера программируемого периферийного интерфейса, доступ к которому - через порт 0x61. Следует иметь в виду, что этот регистр используется также и для других целей, так что при его программировании следует вначале прочитать его содержимое, изменить требуемые разряды, а затем записать в порт 0x61 новое значение. Для управления динамиком используются такие разряды регистра:

0- единица в этом разряде устанавливает управление динамиком от таймера (возможно и прямое управление из программы, но мы его не рассматриваем);
1- включение/выключение (1/0) динамика.
Из приведенной выше методики программирования таймера очевидно, что динамик может воспроизводить только чистые (однотонные) звуки. Для создания полифонического звучания программа должна быстро переключать (перепрограммировать) таймер с одного тона на другой. Это достаточно громоздкая операция, которая занимает к тому же весь ресурс времени центрального процессора, поэтому мы здесь ограничимся только чистыми звуками.
Для нот первой октавы (включая полутона) ряд коэффициентов деления следующий:
912 - 861 - 813 - 767 - 724 - 678 - 645 - 609 - 574 - 542 - 512 - 483
Для перехода на октаву ниже следует умножить члены этого ряда на 2, на октаву выше - на 0,5.

Следующая программа воспроизводит 7 основных нот первой октавы (пример 6.1).
Длительность звучания каждой ноты и паузы между нотами здесь задается задержкой (delay). При выполнении этих задержек процессор только ожидает истечения заданного интервала, хотя он мог бы в это время выполнять какую-либо полезную работу. Как это организовать - мы увидим позже, рассматривая работу в реальном времени.


/*==                ПРИМЕР 6.1                     ==*/
/*============== Генерация звука ====================*/
#include <dos.h>
main() {
  unsigned int gamma[] =  /* Коэффициенты деления для нот */
    { 912,813,724,678,609,542,483 };
  char *gnames[] =        /* Названия нот */
    { "до","ре","ми","фа","соль","ля","си" };
  int i;
  for(i=0; i<7; i++) {  /* Перебор нот */
    printf("%s ",gnames[i]);
    tone(gamma[i]); /* Звучание ноты */
    delay(700);     /* Задержка на звучание 700 мсек */
    silence();      /* Выключение звука */
    delay(50);      /* Пауза 50 мсек */
    }
  printf("\n");
  }
/*==== Звучание ноты. Аргумент a - коэфф.деления ====*/
tone(unsigned int a) {
  outportb(0x43,0xb6);     /* Запись управляющего байта
                                      (выбор канала 2) */
  outportb(0x42,a&0x00ff); /* Младший байт счетчика
                   записывается в порт данных канала 2 */
  outportb(0x42,a>>8);     /* Старший байт счетчика */
  /* Включение динамика. Читается содержимое порта, в него
     записываются 1 в разряды 0, 1, затем пишется в порт */
  outportb(0x61,inportb(0x61)|0x03);
  }
/*==== Выключение звука ====*/
silence() {
  /* записываются 0 в разряды 0, 1 порта 0x61 */
  outportb(0x61,inportb(0x61)&0xfc);
  }

И еще одно замечание. Большинство языков программирования высокого уровня имеют стандартные функции, аналогичные нашим tone и silence (в Турбо-Си это sound и nosound). Но высота звука для этих функций задается не коэффициентом деления, а частотой, которая связана с коэффициентом деления соотношением:

Частота = 1193180 / Коэфф.деления

6.2. Системная служба времени

Импульсы, поступающие с выхода канала 0 таймера, вызывают прерывание 8. Обработчик этого прерывания в BIOS подсчитывает количество таких импульсов в 4-байтной области памяти (два 2- байтных слова). Этот счетчик, находящийся в области памяти BIOS по адресу 0040:006C, таким образом, хранит количество тиков таймера, прошедших от полуночи (0 в счетчике соответствует полночи). При запуске системы BIOS запрашивает у оператора время дня, переводит его в количество тиков и записывает по указанному адресу. Затем в процессе работы это число модифицируется обработчиком прерывания 8. То обстоятельство, что обработчик прерывания 8 в BIOS обеспечивает работу службы времени следует учитывать при перехвате прерывания 8 и при перепрограммировании канала 0 таймера.

Доступ к счетчику времени поддерживается прерыванием 0x1A. При обращении к этому прерыванию со значением 0 в регистре AH мы получаем в CX старшую, а в DX - младшую части счетчика. При обращении со значением 1 в AH мы задаем счетчик в регистрах CX, DX, и это значение записывается в память BIOS.

DOS поддерживает службу времени функциями 0x2C (чтение времени) и 0x2D (установка времени). Для представления времени в этих функциях используются регистры: CH (часы), CL (минуты), DH (секунды), DL (сотые доли секунды).
Программа примера 6.2 иллюстрирует чтение системного времени тремя способами.


/*==                 ПРИМЕР 6.2                   ==*/
/*=========== Системная служба времени =============*/
#include <dos.h>
main() {
  union REGS rr;
  printf("память BIOS    int 1A        DOS-2C\n");
  /* Окончание - по клавише Esc */
  while (getch()!=27) {
    /* Получение счетчика времени из области памяти BIOS */
    printf(" %04x %04x  ",peek(0x40,0x6e),peek(0x40,0x6c));
    /* Получение счетчика времени из прерывания 1A */
    rr.h.ah=0;
    int86(0x1a,&rr,&rr);
    printf("  %04x %04x  ",rr.x.cx,rr.x.dx);
    /* Получение форматированного времени из функции DOS */
    rr.h.ah=0x2c;
    intdos(&rr,&rr);
    printf("  %02d:%02d:%02d.%02d\n",
      rr.h.ch,rr.h.cl,rr.h.dh,rr.h.dl);
    }
  }

При загрузке система запрашивает у оператора также текущую дату, которая сохраняется где-то в DOS. Доступ к дате обеспечивается функциями DOS 0x2A - чтение и 0x2B - установка (регистры: CX - год, DH - месяц, DL - число). Всякий раз, когда счетчик времени суток в области данных BIOS достигает значения, соответствующего 24 часам, он сбрасывается в 0, и устанавливает в 1 флаг наращивания даты - 1 байт по адресу 0040:0070. При запросе даты DOS анализирует этот флаг и, если он установлен, наращивает дату и сбрасывает флаг. В программном примере 6.3 мы при помощи функции 0x2A получаем текущую дату и запоминаем ее. Затем записываем 1 во флаг наращивания даты. Можно убедиться, нажимая любую клавишу, кроме Esc, что пока нет запроса даты, флаг остается взведенным. Затем вновь выдаем запрос и получим модифицированную дату и нулевой флаг. Наконец, при помощи функции 0x2B исправляем то, что мы наделали - восстанавливаем дату.


/*==                  ПРИМЕР 6.3                    ==*/
/*============= Системная служба даты ================*/
#include <dos.h>
main() {
  union REGS rr;
  int y,m,d; /* Исходный год, месяц, день */
  /* Получение текущей даты */
  rr.h.ah=0x2a; /* Чтение даты */
  intdos(&rr,&rr);
  d=rr.h.dl; m=rr.h.dh; y=rr.x.cx;
  printf("%02d:%02d:%02d\n",d,m,y);
  /* Убедимся, что во флаге смены даты BIOS - 0 */
  printf("Флаг смены даты = %d\n",peekb(0x40,0x70));
  /* Установка флага смены даты */
  pokeb(0x40,0x70,1);
  /* Пока нет запроса даты, флаг остается взведенным
     (запрос будет выдан по клавише Esc) */
  while(getch()!=27)
    printf("Флаг смены даты = %d\n",peekb(0x40,0x70));
  /* Запрос даты. Мы получим дату на 1 большую исходной */
  rr.h.ah=0x2a; /* Чтение даты */
  intdos(&rr,&rr);
  printf("%02d:%02d:%02d\n",rr.h.dl,rr.h.dh,rr.x.cx);
  /* а флаг смены даты сбрасывается */
  printf("Флаг смены даты = %d\n",peekb(0x40,0x70));
  /* Восстановление исходной даты */
  rr.h.ah=0x2b; /* Запись даты */
  rr.h.dl=d; rr.h.dh=m; rr.x.cx=y;
  intdos(&rr,&rr);
  /* и вывод ее */
  rr.h.ah=0x2a; /* Чтение даты */
  intdos(&rr,&rr);
  printf("%02d:%02d:%02d\n",rr.h.dl,rr.h.dh,rr.x.cx);
  }

В AT имеются независимые часы реального времени, показания которых содержатся в CMOS-памяти. Регистры CMOS-памяти, связанные с временем и датой следующие: 0 - секунды, 2 - минуты, 4 - часы, 6 - день недели (0 - воскресенье), 7 - день месяца, 8 - месяц, 9 - год. Доступ к этим данным - либо через порты 0x70, 0x71, либо через прерывание 0x1A. Функция 2 этого прерывания (AH=2) - чтение часов реального времени, функция 3 - установка часов, функции 4, 5 - чтение и установка даты соответственно. Используются те же регистры, что и в функциях DOS 0x2C, 0x2A, но все данные представляются в двоично-десятичном коде. При загрузке системы на AT время дня и дата выбираются из этих часов, далее эти часы и системная служба времени работают независимо друг от друга.

Кроме того, в AT имеется также возможность запрограммировать прерывание на заданное время, в описаниях это часто называют сигналом тревоги (alarm). Время поступления этого сигнала заносится в регистры CMOS-памяти: 1 - секунды, 3 - минуты, 5 - часы, а прерывание по достижению заданного времени разрешается единицей в разряде 5 регистра 0x0B. При достижении заданного времени происходит прерывание 0x4A.
Сигнал тревоги может быть задан при помощи функции 6 прерывания 0x1A, а отменен - функцией 7, как это показано в следующем примере.


/*==                 ПРИМЕР 6.4                    ==*/
/*============== Сигнал тревоги в AT ================*/
#include <dos.h>
#include <stdio.h>
/* Выражения преобразования BCD->int и наоборот */
#define bcd_to_int(x) (x>>4)*10+(x&0x0f)
#define int_to_bcd(x) ((x/10)<<4)|(x%10)
void interrupt (*old4A)(); /* Адрес старого обработчика */
void interrupt new4A();  /* Описание нового обработчика */
char flag;   /* Флаг тревоги */
union REGS rr;
struct SREGS sr;
void *readvect(int in);
void writevect(int in, void *h);
/*==== main ====*.
main() {
  int m;       /* Минуты */
  /* Подключение к вектору 4A */
  old4A=readvect(0x4a); writevect(0x4a,new4A);
  /* Чтение текущего времени */
  rr.h.ah=2;    /* функция 2 */
  int86(0x1a,&rr,&rr);
  /* Увеличение его на 1 мин */
  m=bcd_to_int(rr.h.cl)+1;
  if (m>=60) {
    m-=60; rr.h.ch=int_to_bcd(bcd_to_int(rr.h.ch)+1);
    }
  rr.h.cl=int_to_bcd(m);
  /* Запись увеличенного времени в регистры тревоги */
  rr.h.ah=6;      /* функция 6 */
  int86(0x1a,&rr,&rr);
  rr.h.ah=0x2c;  intdos(&rr,&rr);
  printf("\nВремя запуска - %02d:%02d:%02d\n",
    rr.h.ch,rr.h.cl,rr.h.dh);
  /* Ожидание тревоги */
  flag=0;
  /* Переменная flag установится в 1 по сигналу тревоги
     в обработчике прерывания 4A */
  while(flag==0);
  rr.h.ah=0x2c;  intdos(&rr,&rr);
  printf("Время тревоги - %02d:%02d:%02d\n",
    rr.h.ch,rr.h.cl,rr.h.dh);
  /* Отмена тревоги */
  rr.h.ah=7;      /* функция 7 */
  int86(0x1a,&rr,&rr);
  }
/*==== Обработчик прерывания 4A - обработчик тревоги ====*/
void interrupt new4A() {
  putchar(7);             /* Звуковой сигнал */
  flag=1;                 /* Установка флага */
  writevect(0x4a,old4A);  /* Восстановление вектора */
  }
/*==== Получение старого вектора ====*/
void *readvect(int in) {
  rr.h.ah=0x35; rr.h.al=in; intdosx(&rr,&rr,&sr);
  return(MK_FP(sr.es,rr.x.bx));
}
/*==== Запись нового вектора ====*/
void writevect(int in, void *h) {
  rr.h.ah=0x25; rr.h.al=in; sr.ds=FP_SEG(h);
  rr.x.dx=FP_OFF(h); intdosx(&rr,&rr,&sr);
}

6.3. Работа в реальном времени

В целом ряде приложений необходима привязка действий программы к определенным моментам времени или к временным интервалам. Наиболее простой подход заключается в циклическом опросе системного счетчика времени (по сути, такой подход применялся в примере 6.1 для управления длительностью звучания нот). Если же требуется заполнить паузы ожидания какой-либо полезной работой, приходится прибегать к расширению прерывания таймера. Для этого случая создается дополнение к прерыванию таймера, которое подсчитывает тики таймера параллельно с BIOS и по истечении заданного интервала либо сама выполняет требуемые действия, либо устанавливает какой-то флаг, по которому эти действия выполнит программа переднего плана. Характерным примером такой задачи является создание фонового музыкального сопровождения - музыка играет в то время, как программа производит вычисления. В приведенном примере 6.5 программа исполняет мелодию, закодированную в массиве MUS, где каждая нота описывается двумя числами: ко- эффициентом деления основной частоты таймера и длительностью звучания (в тиках).
При иницировании музыки переменные N и NM устанавливают- ся в начальные значения и подменяется вектор прерывания 8. "Полезная" работа программы переднего плана заключается в чтении кода нажатой клавиши, получении и выводе на экран те- кущего времени (это дает нам возможность убедиться в том, что работа системной службы времени не нарушена). При нажа- тии клавиши Esc программа и музыка завершаются. При поступ- лении очередного прерывания 8 управление получает функция newtime. Она прежде всего вызывает системный обработчик пре- рывания 8, а затем уменьшает счетчик тиков NM. Если счетчик тиков исчерпан, то отключается звук, программный цикл обес- печивает короткую паузу между нотами, а затем выбирается ко- эффициент деления для очередной ноты, который используется для программирования канала 2 таймера, а ее длительность ус- танавливается в счетчик NM.


/*==               ПРИМЕР 6.5                    ==*/
/*============== Фоновая музыка ===================*/
#include <dos.h>
#define TIMEINT 8  /* Возможно также #define TIMEINT 0x1c */
void interrupt (*oldtime)(); /* Область сохранения
                    системного вектоpа пpеpывания таймера */
void interrupt  newtime(); /* Новый обpаботчик пpеpываний */
static int NM;   /* Счетчик длительности ноты */
static int N;    /* Индекс в массиве MUS */
static int MUS[]=  { /* Кодировка мелодии. 1-е число в каждой паре - 
                          коэфф.деления, второе - длительность в тиках */
  1218,10,767,10,813,10,912,10,966,10,912,30,1218,10,609,10,
  678,10,767,10,813,10,724,30,912,10,574,15,609,5,678,15,
  767,5,813,5,767,5,678,20,574,10,609,10,678,10,767,10,678,
  10,813,30,1218,10,767,10,813,10,912,10,966,10,912,30,1218,
  10,609,10,678,10,767,10,813,10,724,30,912,10,574,15,609,5,
  678,15,767,5,813,5,767,5,678,20,574,10,609,10,678,10,767,
  10,678,10,813,30,0,2,609,10,609,10,912,10,724,10,609,10,
  609,5,678,5,724,5,678,5,574,15,678,5,678,10,1024,10,813,
  10,678,10,678,5,767,5,813,5,724,5,609,20,678,5,609,5,574,
  5,456,5,342,5,384,5,406,5,456,5,456,20,483,20,678,5,609,5,
  574,5,456,5,342,5,384,5,406,5,456,5,456,10,483,10,456,20,
  0,2,456,5,456,10,483,10,456,20,0,30,-1 };
union REGS rr;
struct SREGS sr;
void *readvect(int in);
void writevect(int in, void *h);
/*==== Пусковая пpоцедуpа, программа переднего плана ====*/
main() {
  int c,i;
  union REGS rr;
  initmus(); /* Инициpование */
  for ( ; ; )  {
    c=getch();
    rr.h.ah=0x2c;
    intdos(&rr,&rr);
    printf("%02d:%02d:%02d\n",rr.h.ch,rr.h.cl,rr.h.dh);
    /* Завершение по клавише Esc */
    if (c==27) break;
    }
  stopmus(); /* Завеpшение */
}
/*==== Инициpование ====*/
initmus() {
  /* Начальные значения пеpеменных */
  N=0; NM=1;
  /* Подмена вектоpа таймера */
  oldtime = readvect(TIMEINT); writevect(TIMEINT,newtime);
}
/*==== Завеpшение. Выкл.звука и восстановление вектоpа ===*/
stopmus() {
  silence(); writevect(TIMEINT,oldtime);
  }
/*==== Новый обpаботчик пpеpываний таймеpа ====*/
void interrupt newtime() {
  int i;
  /* Если мы подключились к вектору 8, то пpи отсутствии следующего   
      опеpатоpа системная служба вpемени не pаботает, в main-функции 
      значение t не меняется. */
  (*oldtime)();
  if (--NM==0) { /* Подсчет тактов */
    /* Вpемя звучания ноты истекло. Выкл.звука и пауза */
    silence(); for(i=0; i<2000; i++);
    /* Выбоpка высоты следующей ноты */
    tone(MUS[N++]);
    /* Выборка счетчика ее длительности */
    NM=MUS[N++];
    /* Пpи окончании мелодии - пеpеключение на начало */
    if (MUS[N]<0) N=0;
    }
/*  Восстановление контpоллеpа пpеpываний  */
/*  outportb(0x20,0x20); */
/*   нужно только в том случае, если нет вызова oldtime */
}
/*==== Звучание ноты ====*/
tone(unsigned int a) {
  outportb(0x43,0xb6);  outportb(0x42,a&0x00ff);
  outportb(0x42,a>>8);  outportb(0x61,inportb(0x61)|0x03);
  }
/*==== Выключение звука ====*/
silence() {  outportb(0x61,inportb(0x61)&0xfc);  }
/*==== Получение старого вектора ====*/
void *readvect(int in) {
  rr.h.ah=0x35; rr.h.al=in; intdosx(&rr,&rr,&sr);
  return(MK_FP(sr.es,rr.x.bx));
}
/*==== Запись нового вектора ====*/
void writevect(int in, void *h) {
  rr.h.ah=0x25; rr.h.al=in; sr.ds=FP_SEG(h);
  rr.x.dx=FP_OFF(h); intdosx(&rr,&rr,&sr);
}

Обратим внимание на то, что номер прерывания таймера определен как макро-константа TIMEINT. Значение этой константы - 8, но может быть и 0x1C. Дело в том, что в ПЭВМ специфицированы два прерывания таймера: 8 и 0x1C, последнее носит название пользовательского прерывания по таймеру. Это не аппаратное прерывание, оно вызывается программно из обработчика прерывания 8. Системный обработчик прерывания 0x1C содержит единственную команду IRET. Что дает нам наличие этого прерывания? Честно говоря, ничего нового. По-видимому, это прерывание было введено в рассчете на то, что если в ПЭВМ имеется единственная программа пользователя, обрабатывающая сигналы таймера, то эта программа может подключаться к вектору 0x1C, не соблюдая правила дополнения, о которых шла речь в разделе 3. Но на самом деле в памяти нашей ПЭВМ всегда находится целый набор несистемных, возможно резидентных, программ, некоторые из которых тоже обрабатывают сигналы таймера, возможно, тоже используя прерывание 0x1C. Поэтому обработчик этого прерывания должен включаться в цепочку так же, как и любой другой. (Заметим, что авторы некоторых программ, перехватывающих 0x1C, позволяют себе некорректную работу с этим вектором, поэтому использование прерывания 8 надежнее).
В приведенном примере 6.5 максимальная длительность звучания ноты - около 55 мсек (1 тик). Даже для воспроизведения не всякой мелодии такая характеристика может оказаться удовлетворительной. Нетрудно назвать задачи, в которых минимальный измеряемый временной интервал должен быть меньше. В этом случае приходится перепрограммировать канал 0 таймера. Вспомним, что при загрузке BIOS записывает в делитель частоты этого канала максимальное число - 65535. Уменьшая данное число, мы обеспечим большую частоту следования прерывания 8, укоротим тик таймера. Однако, не следует забывать, что прерывания таймера используются системной службой времени, если мы увеличим частоту, например, в 4 раза, то и время для системы пойдет в 4 раза быстрее. Возможны два подхода к разрешению этой трудности: либо наш обработчик прерываний должен сам модифицировать системный счетчик времени в соответствии с изменившейся частотой, либо он должен вызывать системный обработчик не при каждом прерывании.

Программа примера 6.6 может быть названа моделью процесса аналого-цифрового преобразования (идея этой модели принадлежит В.П.Полтавцеву). Главная функция постоянно вычисляет значение функции sin(x) при меняющемся аргументе, что имитирует непрерывный сигнал. Обработчик прерывания 8 имитирует преобразователь с постоянным шагом временной дискретизации. Перед началом работы канал 0 таймера программируется на частоту в 16 раз больше обычной, таким образом, "частота дискретизации" составит около 291.2 гц. При поступлении очередного прерывания запоминается актуальное на данный момент значение sin(x). Обратите внимание на то, что вопервых, старый обработчик oldtime вызывается не при каждом прерывании, а лишь один раз из 16 (переменная kf - счетчик по модулю 16), во-вторых, на то, что в случаях, когда oldtime не вызывается, наш обработчик сам сбрасывает контроллер прерываний. После набора 100 "отсчетов АЦП" результат выводится на терминал в наглядном псевдографическом виде.


/*==                   ПРИМЕР 6.6              ==*/
/*==================== Модель АЦП ===============*/
#include <dos.h>
#include <math.h>
#define TIMEINT 8
#define NN 100   /* Максимальное число отсчетов */
void interrupt (*oldtime)();
void interrupt  newtime();
static int y[NN];  /* Накопитель отсчетов */
static int ny;     /* Индекс в массиве y */
static int yc;     /* Текущее значение sin */
static int kf;     /* Счетчик вызовов oldtime */
union REGS rr;
struct SREGS sr;
void *readvect(int in);
void writevect(int in, void *h);

main() {
unsigned oldtic=65535;  /* Старый коэфф.деления */
unsigned newtic=4095;   /* Новый коэфф.деления */
unsigned char d;
int k;
double x;       /* Аргумент ф-ции sin */
char line[81];  /* Строка для вывода */
  for (ny=0; ny<81; line[ny++]=' '); line[ny]='\0';
  /* Программирование канала 0 */
  outportb(0x43,0x36); /* Управляющий байт */
  outportb(0x40,newtic&0x00ff); /* Младший байт счетчика */
  outportb(0x40,newdtic>>8);     /* Старший байт счетчика */
  ny=-1;  /* Признак того, что АЦП еще не началось */
  kf=15;
  /* Подключение к вектору */
  oldtime=readvect(TIMEINT); writevect(TIMEINT,newtime);
  /* Запуск "непрерывного процесса" */
  for (x=ny=0; ny<NN; x+=0.1) yc=(int)(39*sin(x));
  /* Восстановление вектора */
  writevect(TIMEINT,oldtime);
  /* Восстановление канала 0 */
  outportb(0x43,0x36); /* Управляющий байт */
  outportb(0x40,oldtic&0x00ff); /* Младший байт счетчика */
  outportb(0x40,oldtic>>8);     /* Старший байт счетчика */
  /* Вывод запомненных результатов */
  for(ny=0; ny<NN; ny++) {
    line[y[ny]+39]='*';
    printf("%s",line);
    line[y[ny]+39]=' ';
    }
  }
/*==== Новый обpаботчик пpеpываний таймеpа ====*/
void interrupt newtime() {
int i;
  if (--kf<0) {
    /* Вызов oldtime - на 16-й раз */
    (*oldtime)();
    kf=15;
    }
  else /* иначе - сброс контроллера */
    outportb(0x20,0x20);
  if ((ny>=0)      /* Если АЦП началось, */
    &&(ny<NN))     /* и NN отсчетов еще не набрано, */
      y[ny++]=yc;  /* запоминание очередного отсчета */
}
/*==== Получение старого вектора ====*/
void *readvect(int in) {
  rr.h.ah=0x35; rr.h.al=in; intdosx(&rr,&rr,&sr);
  return(MK_FP(sr.es,rr.x.bx));
}
/*==== Запись нового вектора ====*/
void writevect(int in, void *h) {
  rr.h.ah=0x25; rr.h.al=in; sr.ds=FP_SEG(h);
  rr.x.dx=FP_OFF(h); intdosx(&rr,&rr,&sr);
}

Последний пример этой главы демонстрирует принципиальную возможность обеспечения работы с разделением времени. MS-DOS - однопрограммная система, она не поддерживает разделения ресурсов вычислительной системы между процессами, но у пользователя есть возможность самостоятельно обеспечить разделение, хотя это и непросто. В нашей программе имеется два процесса, каждый из которых программно реализован своей функцией. Каждому процессу отводится свой квант времени, заданный в числе тиков таймера. Задача нашей программы заключается в обеспечении такой работы, чтобы процесс 1 активизировался и оставался активным в течение отведенного ему времени. Затем процесс 1 прерывается и активизируется процесс 2. По истечении времени, отведенного процессу 2, он прерывается, и вновь активизируется процесс 1, причем с того самого места, на котором он был прерван, и так далее.
Понятно, что для подсчета временных интервалов мы можем использовать прерывание таймера. Но возникает вот какая сложность. При прерывании таймером процесса, например, 1 в стеке запоминается состояние процесса 1. При возврате из прерывания это же состояние и восстановится из стека. Нам же нужно, чтобы при исчерпании процессом 1 своего кванта восстанавливалось не его состояние, а состояние процесса 2, а состояние процесса 1 запоминалось до следующего переключения процессов. Такая операция в общем случае носит название "переключение контекста" с процесса 1 на процесс 2. Решение этой задачи здесь заключается в отключении от общего стека и в назначении каждому процессу своего собственного стека. При активном состоянии процесса 1 регистры SS:SP микропроцессора указывают на стек процесса 1. Если обработчик прерывания обнаруживает, что квант времени процесса 1 уже истек, он заносит в эти регистры указатель на стек процесса 2, и тогда возврат из прерывания происходит в процесс 2.
Каждый процесс состоит из двух частей - части инициализации и прикладной части. Прикладная часть в нашей программе обеспечивает индикацию на экране активности процесса, при этом позволяет увидеть, на каком шаге процесс прерывается, и убедиться, что он восстанавливается с того же шага. Методы такой индикации будут рассмотрены в главе, посвященной дисплейным адаптерам. Наличие части инициализации обусловлено тем, что перед началом работы в разделении времени, в стеке каждого процесса должно быть записано некоторое его состояние, в которое произойдет первый "возврат". В программе имеются флаги инициализации для каждого процесса и общий флаг инициализации. Секция инициализации процесса представляет собой цикл, который повторяется до тех пор, пока не будет установлен флаг инициализации этого процесса. При инициализации этот цикл будет прерван таймером, обработчик прерываний запомнит состояние процесса в его стеке (это состояние будет соответствовать нахождению внутри цикла) и установит флаг инициализации. После возврата из прерывания произойдет выход из цикла и - поскольку общий флаг еще не установлен - выход из процесса. После инициализации всех процессов устанавливается общий флаг, и основная функция входит в цикл while(initf[0]==1). Этот цикл будет прерван таймером, который переключит регистры SS:SP на стек процесса 1 и запишет в счетчик тиков квант процесса 1. Возврат из прерывания произойдет в процесс 1, в цикл инициализации, но поскольку теперь все флаги уже установлены, управление перейдет в прикладную часть процесса 1. Обработчик прерываний будет подсчитывать тики и по исчерпании процессом 1 его кванта переключится на процесс 2 и т.д. Условием завершения работы является выполнение 10 переключений (нетрудно ввести другое условие, например, нажатие любой клавиши), по этому условию восстанавливается в регистрах SS:SP адрес стека основной программы.


/*==                     ПРИМЕР 6.7                     ==*/
/*================== Разделение времени ==================*/
/* Два процесса - PROCESS1 и PROCESS2 -  поочередно активизируются, 
    каждый на заданный интервал времени */
/* ВНИМАНИЕ !!!  При компиляции этого модуля в Турбо-Си необходимо установить 
    Options -> Compiler ->Code generation ->Test stack overflow -> Off */
#include <dos.h>
#define word unsigned int
#define byte unsigned char
/* Номер используемого прерывания таймера */
#define TIMEINT 8
/* Размер стека (подобран экспериментально) */
#define StackSize 600
/* Максимальное число переключений */
#define NSWITCH 10
/* Кванты времени (в тиках таймера) */
static int TimeC[3]={1,40,120};
/* TimeC[0] - не используется;
   TimeC[i] - квант, отведенный i-му процессу */
/* Флаги инициализации */
static byte initF[3]={0,0,0};
/* initF[0] - общий флаг;
   initF[i] - флаг i-го процесса */
/* Сегменты и указатели стека прерванных процессов */
static word newSS[3], newSP[3];
/* newSS[0] и newSP[0] - для пусковой программы
   newSS[i] и newSP[i] - для i-го процесса */
static int TimeCount;         /* Счетчик квантов времени */
static byte nproc;   /* Номер текущего процесса */
static byte newstack[StackSize]; /* Стек для процессов.
                1-я половина этого масива - для процесса 2,
                2-я для процесса 1 */
static byte Nswitch=0; /* Счетчик переключений */
/* Вектор системного обработчика прерываний таймера */
void interrupt (*oldtime)();
/* Описание нового обработчика прерываний от таймера */
void interrupt RTS();
void *readvect(int in);
void writevect(int in, void *h);
union REGS rr;
struct SREGS sr;
/*==== main ====*/
main() {
  word st_seg,st_off;
  clrscr();
  /* Запоминание старого стека */
  newSP[0]=_SP; newSS[0]=_SS;
  /* Определение адреса нового стека */
  st_seg=FP_SEG(newstack);
  st_off=FP_OFF(newstack);
  /* Подключение к вектору таймера */
  TimeCount=TimeC[0];
  *oldtime=readvect(TIMEINT);
  writevect(TIMEINT,RTS);
  /* Инициализация процесса 1 */
  /* При перекл.на другой стек запрещаются прерывания */
  nproc=1;
  disable();
  _SS=st_seg; _SP=st_off+StackSize;
  enable();
  process1();
  /* Инициализация процесса 2 */
  nproc=2;
  disable(); _SS=st_seg; _SP=st_off+StackSize/2; enable();
  process2();
  /* Восстановление стека */
  disable(); _SS=newSS[0]; _SP=newSP[0]; enable();
  initF[0]++;  /* Инициализация закончена */
  /* Запуск в рабочее состояние */
  nproc=0; TimeCount=1;
  /* Этот цикл прервется таймером. Значение initF[0] станет
     отличным от 1 при выполнении условия окончания */
  while(initF[0]==1);
  /* Завершение работы, восстановление вектора */
  writevect(TIMEINT,oldtime);
}
/*==== Обработчик прерывания от таймера ====*/
/* Выполняет роль диспетчера процессов */
void interrupt RTS() {
  switch(initF[0]) {
    case 0:
      /* Ветвь инициализации */
      initF[nproc]++;  /* Установка флага инициализации */
      newSP[nproc]=_SP;   /* Запоминание стека процесса */
      newSS[nproc]=_SS;
      break;
    case 1:
      TimeCount--;   /* Подсчет тиков */
      if(TimeCount==0) {
        /* Рабочая ветвь. Переключение процессов */
        /* Подсчет переключений */
        if (++Nswitch>NSWITCH) initF[0]++;
        newSP[nproc]=_SP;  /* Сохранение стека прерванного
        newSS[nproc]=_SS;  /* процесса */
        if (++nproc>2) nproc=1;  /* Переключ.номера проц.*/
        _SS=newSS[nproc];  /* Назнач.стека нового проц.*/
        _SP=newSP[nproc];
        TimeCount=TimeC[nproc]; /* Назнач.времени
                                             активности */
        }
      break;
    case 2:
      _SS=newSS[0];  /* Назнач.стека пусковой программы */
      _SP=newSP[0];
      break;
    }
  (*oldtime)();  /* Системный обработчик */
}
/*==== Процесс 1 ====*/
process1() {
  /* Выводимый текст */
  byte out[18]=
  { 'P',0x12,'R',0x12,'O',0x12,'C',0x12,'E',0x12,
    'S',0x12,'S',0x12,' ',0x12,'1',0x12};
  word i,off;
  /* == Часть инициализации == */
  /* В этом цикле процесс крутится при инициализации, пока
     его не прервут для запоминания состояния */
  while(initF[1]==0);
  /* Если общая инициализация еще не выполнена,
     процесс завершается */
  if(initF[0]==0) return;
  /* == Прикладная часть == */
  for(;;) {
    /* Вывод текста. delay вставлено для того, чтобы было
       видно, что прерванный процесс возобновляется с того
       же места, где был прерван */
    for(i=0,off=180;i<18;i++,off++) {
      pokeb(0xb800,off,out[i]); delay(50);
      }
    /* Сброс текста */
    for(i=0,off=180;i<9;i++,off++) {
      pokeb(0xb800,off++,0x20); delay(50);
      }
    }
}
/*==== Процесс 2 - почти точная копия процесса 1 ====*/
process2() {
  byte out[18]=
  { 'P',0x21,'R',0x21,'O',0x21,'C',0x21,'E',0x21,
    'S',0x21,'S',0x21,' ',0x21,'2',0x21};
  word i,off;
  while(initF[2]==0);
  if(initF[0]==0) return;
  for(;;) {
    for(i=0,off=280;i<18;i++,off++) {
      pokeb(0xb800,off,out[i]); delay(50);
      }
    for(i=0,off=280;i<9;i++,off++) {
      pokeb(0xb800,off++,0x20); delay(50);
      }
    }
}
/*==== Получение старого вектора ====*/
void *readvect(int in) {
  rr.h.ah=0x35; rr.h.al=in; intdosx(&rr,&rr,&sr);
  return(MK_FP(sr.es,rr.x.bx));
}
/*==== Запись нового вектора ====*/
void writevect(int in, void *h) {
  rr.h.ah=0x25; rr.h.al=in; sr.ds=FP_SEG(h);
  rr.x.dx=FP_OFF(h); intdosx(&rr,&rr,&sr);
}

Описанный подход легко распространить на 3 и более процессов. Следует, однако, предупредить читателя, что эта программа предназначена для демонстрации только принципиальной возможности разделения времени. Для более серьезных прикладных частей переключение контекстов потребует значительно более сложных действий. Эти проблемы обсуждаются в разделе, посвященном резидентным программам.


КаталогИндекс раздела
НазадОглавлениеВперед