О-хо-хо... Ну да ладно!
Даша, я предполагаю, что Вы только-только подходите к понятью принципов работы событийно-управляемых (event-driven) автоматов. Вы уже освоили линейное и процедурное
программирование, теперь перед Вам открывается совершенно другой мир. Другая планета. Здесь все не так.
Извините, маленькая лекция.
Допустим, у нас есть некоторый девайс, который имеет клавиатуру и дислей. Например, нечто типа калькулятора. Наша программа должна считывать клавиатуру, выполнять какие-то определенные действия и результат выводить на экран. Как бы мы написали такую программу?
Во первых, это бесконечный цикл, тело которого состоит из вызова трех функций. Пусть они будут называться так:
GetKey()
DoSomething()
DisplayResult()
чтобы не усложнять, аргументы функций пока опустим из рассмотрения. А вот названия функций говорят сами за себя -- получить код нажатой клавиши, выполнить какое-то действие, вывести результат.
Вот основной цикл нашей программы:
Код
while (1)
{
GetKey();
DoSomething();
DisplayResult();
}
Рассмотрим вызов функция GetKey(). Вроде бы все ясно -- функция должна вернуть код нажатой клавиши. А что делать, если ни одна клавиша не нажата? Значит, функция не вернет управление. Допустим, что это так. Тогда получается, что наше устройство постоянно как-бы зависает на этой функции.
И действительно, посчитать результат и вывести его -- занимает не так уж и много времени по сравнению с тем, что юзер нажимает кнопочки. Юзер вообще может вообще пойти попить кофейку.
Но для этого устройства такое построение программы допустимо. Поскольку предопределенные действия выполняются только тогда, когда юзер соизволит нажать кнопочку. С другой стороны, нет нажатия -- значит, и делать ничего не надо! Пока все срастается.
Так было раньше, когда программы были маленькие, процессоры глупенькие, а деревья большие. Но мир изменился. Сейчас МК стали обладать многими периферийными устройствами и интерфейсами, которые зачастую работают одновременно.
Противоречие в современных устройствах заключается в том, что периферийных устройств много, а "думатель" (АЛУ) -- один.
Давайте-ка теперь к нашему чудо-калькулятору добавим часы, которые раз в секунду должны подновлять свои показания на дисплее, и посмотрим как это повлияет на нашу программу.
Добавляя в цикл две функции GetCurrentTime() и (), мы не решим проблему.
Код
while (1)
{
GetKey();
DoSomething();
DisplayResult();
GetCurrentTime();
DisplayTime ();
}
Понятно, что управление, зависая в GetKey(), будет блокировать всю программу. Понятно, что часы будут подновляться только при нажатии какой-либо кнопочки. Страшно неудобно. Такое изделие никто не купит. Что делать?
Можно пойти двумя путями. (Сразу скажу, Вы,
Daria, как мне кажется, в своей программе идете первым путем.)
Путь первый (а-ля-DOS): добавить еще пару функций IsKeyHit() и IsTimeChanged(), которые будут возвращать управление сразу. Такие функции еще называют
неблокирующими. Эти функции "говорят" только о том, что нажата ли вообще какая-нибудь клавиша или нет, изменилось ли время или еще нет.
Теперь наш цикл будет выглядеть так:
Код
while (1)
{
if (IsKeyHit())
{
GetKey();
DoSomething();
DisplayResult();
}
if (IsTimeChanged())
{
GetCurrentTime();
DisplayTime();
}
}
В этом случае, наша программа будет со скоростью света совершать холостые обороты. Вот смотрите, скорость работы МК составляет миллионы инструкций в секунду, скорость нажатия клавиш -- максимум десяток-другой в секунду, а про именение текущего времени, так вообще раз в секунду. Единственное, что напрягает, -- это кпд программы. Но, с другой стороны, прога-то работает! А это есть самое главное. Ибо ведь никому не нужно устройство, которое не выполняет своей функции. А наше устройство свою функцию выполняет. Ведь так? (Заказчика не интересует, как и на какой парадигме строится программа, Его интересует только конечный результат. А мы его, не смотря на дремучесть нашего кода, обеспечиваем.)
Второй путь (а-ля-Windows): Это то, о чем я говорил в самом начале поста, это событийно-управляемый автомат. Работает эта штука следующим образом.
В таком автомате (системе) есть два дуальных механизма.
Первый -- регистрирует происходящие в системе изменения (события) и записывает в очередь соответствующее сообщение об этом событии. Очередь надо понимать чисто в компьютерном смысле -- т.е. бесконечный (кольцевой) буфер.
Второй механизм периодически опрашивает эту очередь, и, если она не пуста, извлекает из нее очередное сообщение и вызывает соответствующий обработчик. В результате получается, что если в системе нет событий, ничего не делаем. Т.е. опять-таки совершаем холостые обороты.
Но в этом случае у нас оказалось, что постановка в очередь и обработка сообщений будут (программно) разнесены в разные части. По началу, это кажется крайне неудобным. Но создав несколько проектов Вы придете к мысли, что так все же намного легче разрабатывать свои программы, а потом их сопровождать. Т.е. даже по прошествии какого-то времени, возвращаясь к коду, его легче понять и добить запоздалыми требованиями заказчика по недостающей функциональности устройства.
Итак, возвращаемся к событийно-управляемым системам. Поставщиками событий в первую очередь являются не что иное, как всевозможные прерывания от периферийных устройств. Во вторую очередь, это могут быть какие-то результаты работы программы. (Я догадываюсь, сейчас это несколько сложно понять. Но что делать? Ведь не всегда бывает легко с первого раза въехать в какие-то новые понятия. Тем более, если эти понятия очень емкие и не вписываются в установившиеся, привычные для нас рамки. Терпите. Учитесь. Другого пути нет.)
Произошло в системе нажатие на кнопочку -- возникает прерывание. Попадаем в функцию прерывания. В ней записываем соответствующее сообщение в очередь, выходим. Прерывание отработано очень быстро. Событие в ситеме зарегистрировано Больше-то и делать сейчас ничего не надо.
Еще пару-тройку раз для уяснения:
Произошло перещелкивание секунды в таймере -- возникло прерывание. Заходим, записываем сообщение в очередь, выходим.
Закончил АЦП измерение -- прерывание. Заходим, сообщение в очередь, выходим.
Очередной байт отправлен (по USART-у) -- прерывание. Заходим, сообщение в очередь, выходим.
Думаю, идея понятна?
С другой стороны программы, имеем опять тот же самый бесконечный цикл. Но работает он уже чуть-чуть по-другому, не совсем так, как в нашем исходном варианте в начале статьи.
Теперь нам не надо периодически пробегать (говорят – заниматься
поллингом) по всем нашим ПУ, опрашивать каждое – есть ли нажатие кнопочки или нет, говов ли АЦП или нет, тикнул ли таймер или нет.
Вместо всего этого, теперь имеется единственная функция GetMessage() и вслед за ней располагается так называемый
кракер сообщений (т.е. расшифровщик). Задача этой "парочки" -- вытянуть из очереди сообщение, и вызывать соответствующую функцию по его обработке.
Выглядит это намного приятнее:
Код
unsigned char msg;
while ()
{
if (GetMessage(&msg))
switch (msg)
{
case MSG_KEYPRESS:
DoKey();
break;
case MSG_TICK:
DisplayCurrentTime();
break;
case MSG_ADCREADY:
SendResult();
break;
case MSG_TXD:
SendNextByte();
break;
...
}
// а здесь вот -- самое удобное место пинать собаку (WDR)
}
Теперь пара слов о переменной msg. У меня здесь это -- просто байт, но в случае очень навороченной системы Вам никто не запрещает для кодирования сообщений вместо байта использовать слово или вообще структуру. Важно только одно: важно соблюсти атамарность операций занесения и извлечения из очереди сообщений. Т.е., другими словами, сделать так, чтобы не получилось, что система, начав записывать одно сообщение в очередь, окажется в состоянии другого прерывания, которое запишет свое сообщение. Потом в очередь будет дозаписан остаток от первого сообщения. В результате в очереди сообщений получится фигня полная. Все рухнет.
Есть еще некоторые тонкие моменты. Поскольку обработка сообщений конечна по времени (т.е. они не обрабатываются мгновенно), а источников событий несколько, и они работают асинхронно, то нужно делать очередь сообщений достаточно длинную. Но какую? Ее хоть и называют "бесконечной", но создается-то она на основе обычного линейного буфера, какой-то конкретной длины. И Ваша задача (О-о, это искусство!) найти оптимальную длину буфера. Маленький буфер может быть легко переполнится, большой -- буфер сожрет память, которой и так кот наплакал.
Не надо также забывать, что обработчики сообщений не должны долго работать, т.е. они не должны узурпировать процессорное время. Я как-то занимался одним проектом. Там нужно было выводить инфу на графический LCD, одновременно нужно было оперативно реагировать на кнопочки и держать связь с компом и дюжиной периферых устройств, подцепленных к двух UART'ам. Сложность состояла в том, что LCD подключался по квадратной (I2C) шине. Длина шлейфа была метра полтора. Я не рискнул, "зажигать" его на 400 кГц. А на 100 кГц, обновление экрана занимало почти четверть (или даже более, не помню точно!) секунды. Если бы обновление LCD было реализовано в одной функции-обработчике, то это легко бы привело к переполнению очереди сообщений. В моменты перерисовки дисплея реакция на клавиатуру отсутствовала бы, связь бы с ПУ нарушалась бы. Приятного мало.
Я сделал так, я разбил LCD на несколько областей, и для перерисовки каждой области создавал свое индивидуальное сообщение. В результате, перерисовка LCD выполнялась как серия перерисовок его отдельных областей. Эта серия сообщений, понятно что, могла быть "разбавлена" сообщениями от клавиатуры и от других ПУ. В целом это создало картину практически мгновенной реакции на нажатие клавиш, способности устройства рисовать на экране и одновременно (и без задержек) заниматься периферией, отвечать компу на его запросы.
Не знаю, что еще можно добавить. С одной стороны, сказать еще много чего можно, а с другой... а то-ли это место (форум), где надо много говорить?
Насчет критики. Я, вобщем-то, как-то не рассчитывал на нее. Просто писал и все. Даже не столько пытался ответить
Daria, сколько вообще хотел поделиться своим опытом. Но поскольку я тут много чего сказал, критика должна быть.