Организация диспетчера задач посредством указателей на функции

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

Понадобилось мне организовать изменяемый список задач, которые необходимо запускать через определенные промежутки времени. И тут я вспомнил про тот самый старый проект. Открыл, и был слегка удивлен — для кода того времени функции оказались настолько абстрагированы друг от друга, что без изменения запустились и заработали на новом месте. Ну а поскольку менюОС я распространяю под LGPL, то публикую код здесь.

Поставим простую задачу — на 8 светодиодах, подключенных к одному порту организуем бинарный инкремент. Но не используя непосредственно команду инкремента. Пойдем намного более извращенным путем. Исключительно для наглядности задачи.

Суть всего диспетчера — в его гибкости. Всегда должна иметься возможность изменить набор вызываемых функций, либо периода их работы. Поскольку у каждой функции есть свой адрес, то логично предположить, что кому-то как-то и каким-то образом можно скормить указатель на данную функцию. И в C есть такая возможность.
Объявляется указатель на функцию следующим образом:
footype (*fooindex)(type param1 *, type param2 *);

Работа с указателем на функцию не отличается от работы с любым другим указателем. А вот чтобы впоследствии функцию вызвать, используем
(*fooindex)();

В нашем примере функции типа void без параметров, объявляем массив указателей по количеству «Слотов», а заодно массив, содержащий периодичность исполнения:
#define PSLOTS 8 //8 слотов для функции по максимальному количеству функций в диспетчере
void (*progslot[PSLOTS])();//слоты для программ
uint16_t TPpresc[PSLOTS][2]={0};//а сюда пишем время срабатывания кратное 10мс.

Добавим сюда вспомогательные переменные, необходимые для работы нашей логики переключения:
uint8_t bol_pin[8]={0};

Для периодичной работы нам нужен таймер. Пусть это будет TIMER2, а работать он будет на переполнение с периодом 10мс.
Создаем функцию настройки таймера:
void TSetup()//установка таймера.
{
  TIMSK2 &= ~(1<<TOIE2);//отключили перерывание по переполнению
  TCCR2A &= ~((1<<WGM21) | (1<<WGM20));//обычный режим
  TCCR2B &= ~(1<<WGM22);
  ASSR &= ~(1<<AS2);//режим тактирования от внутреннего источника.
  TIMSK2 &= ~(1<<OCIE2A);//отключили прерывание по сравнению, мало ли...
 TIMSK2 &= ~(1<<OCIE2B);//отключили прерывание по сравнению, мало ли...  
  TCCR2B |= (1<<CS22)|((1<<CS21) | (1<<CS20));//прескалер на 1024.
  TCNT2 = 156;//как бы 10мс. (9.986 если быть точнее)
}

Ну и функции для активации и останова диспетчера, лишними не будут:
void TStart()//разрешает работу диспетчера
{
  TIMSK2 |= (1<<TOIE2);
}
void TStop()//Останавливает работу диспетчера
{
  TIMSK2 &= ~(1<<TOIE2);
}

Теперь необходим обработчик прерывания.
ISR(TIMER2_OVF_vect) {
  TCNT2 = 156;//маст врайн ани оферфлоу тайм.
  for (uint8_t _j=0;_j<PSLOTS;_j++)
  {//смотрим, подошло ли время:
    if (TPpresc[_j][0]){//Если Слот активен (время не равно нулю)
		TPpresc[_j][1]++;//Отсчитываем текущее время слота
		if (TPpresc[_j][0]==TPpresc[_j][1]){//Если досчитали
		TPpresc[_j][1]=0;//то обнуляем счетчик и
		(*progslot[_j])();//выполняем требуемую программу
		}
  }
  } 
}

Подготовка завершена, осталось заиметь функцию, которая будет заносить программу в слот для исполнения:
void TReload(uint8_t _pslot, void (*_f)(), uint16_t _ppresc)//записывает функцию в слот
{
  progslot[_pslot]=_f;//забиваем слот
  TPpresc[_pslot][0]=_ppresc;
}

Тут стоит отметить, что в данной реализации для деактивации слота необходимо ввести нулевое время срабатывания. Мне данный подход кажется вполне логичным.
Теперь жуткая вещь — 8 одинаковых функций для имитации бинарного инкремента. Задача каждой функции — по вызову переключать состояние ножки:
Индусский код!
void foo0(){
	bol_pin[0]=!bol_pin[0];
	if (!bol_pin[0])
	PORTL&=~(1<<PL0);
	else
	PORTL|=(1<<PL0);
}
void foo1(){
	bol_pin[1]=!bol_pin[1];
	if (!bol_pin[1])
	PORTL&=~(1<<PL1);
	else
	PORTL|=(1<<PL1);
}
void foo2(){
	bol_pin[2]=!bol_pin[2];
	if (!bol_pin[2])
	PORTL&=~(1<<PL2);
	else
	PORTL|=(1<<PL2);
}
void foo3(){
	bol_pin[3]=!bol_pin[3];
	if (!bol_pin[3])
	PORTL&=~(1<<PL3);
	else
	PORTL|=(1<<PL3);
}
void foo4(){
	bol_pin[4]=!bol_pin[4];
	if (!bol_pin[4])
	PORTL&=~(1<<PL4);
	else
	PORTL|=(1<<PL4);
}
void foo5(){
	bol_pin[5]=!bol_pin[5];
	if (!bol_pin[5])
	PORTL&=~(1<<PL5);
	else
	PORTL|=(1<<PL5);
}
void foo6(){
	bol_pin[6]=!bol_pin[6];
	if (!bol_pin[6])
	PORTL&=~(1<<PL6);
	else
	PORTL|=(1<<PL6);
}
void foo7(){
	bol_pin[7]=!bol_pin[7];
	if (!bol_pin[7])
	PORTL&=~(1<<PL7);
	else
	PORTL|=(1<<PL7);
}

Зачем? а затем! Что мы будем каждую следующую ножку переключать с вдвое большим периодом выполнения имитируя таки бинарный инкремент. Создаем main:
int main(void)
{
DDRL=0xFF;//светодиоды
   TSetup();//настройка таймера
  TReload(0,*foo0,10);//заносим программы в слоты. Период 100мс.
  TReload(1,*foo1,20);
  TReload(2,*foo2,40);  
  TReload(3,*foo3,80);
  TReload(4,*foo4,160);
  TReload(5,*foo5,320);
  TReload(6,*foo6,640);
  TReload(7,*foo7,1280);
   TStart();//запуск счета
   sei();
   while(1)
    {
    }
}

И все!.. Компилируем по F7, потом зашиваем пунктом меню AVR Studio Tools-COM16 (см.Эту статью) И радуемся жизни:
Эти функции практически в оригинальном виде, слегка исправленные и дополненные, станут основой менюОС2. Кто не знает что такое менюОС, прошу сюда:МенюОС

В целом, у нас получился достаточно простой но эффективный диспетчер.
Если кто-то хочет почитать больше про указатели на функцию, то вот первая попавшаяся мне ссылка:
Указатели на функции
Скачать оформленный в библиотеку диспетчер можно по ссылке: task_manager.rar

6 комментариев

avatar
Это — один из классических вариантов реализации диспетчера. Беда такого подхода в том, что если функция не отдаст управления в течении 10мс, вся система умрет.

Есть намного более простая и легкая реализация подобного диспетчера, называется она "протопотоки".

Но диспетчер в качестве счетчика — это все равно, круто :)
Комментарий отредактирован 2013-01-02 17:30:35 пользователем bsvi
avatar
Да, такой подход накладывает определенные ограничения на программиста и программу. Но ведь часто требуется опросить те же кнопки, или датчики.
Вообще, суть именно массива указателей на функции изначально была написана как раз для обработчика кнопок.
Когда при запуске приложения, оно пишет свой/свои обработчик(и) кнопок в массив и тот спокойно выполняется.Хитрость заключалась, например, в том, что в менюОС2 появились фоновые приложения, которые настраивали под себя, например, только одну кнопку, при этом мы спокойно гуляем где были. Но об этом в другой статье :)

Добавил архив с библиотекой.
avatar
Можно куда нибудь выложить повторно файлы?! — Ссылки dead :(
avatar
Переустанавливал сервер, не поднял lighttpd раздачи статики. Временно переключил на Apache. Теперь все работает.
Спасибо за баг-репорт :)
Комментарий отредактирован 2013-08-21 14:30:05 пользователем radiolok
avatar
Совсем не обязательно развалится, зависит от реализации. Но времянку порвёт, конечно же. Сам использую подобный подход уже давно. Сейчас в основном у меня системный тик 1 мсек, в некоторых проектах есть 100 мксек. Немного писал там и приводил отдельные куски caxapa.ru/402259.html
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.