Программинг микроконтроллеров

индекс
180,37

Почти ОС реального времени: event-driven

Пару слов введения:
Делаю систему контроля на базе AtMega32. Цель — отслеживать значение датчиков температуры и давления, управление нагрузкой и сброс отладочных логов в компьютер.

Плюс экранчик 2х16 символов и клавиатура на 7 клавиш. Аппаратную часть использовал готовую — набор NM8036 от МастерКита. А вот с программной частью засада: стандартный алгоритм, уже прошитый в наборе, примитивен и универсален, исходных кодов прошивки нет, обновления выходят в зашифрованном виде. Пришлось писать самому.

Первоначально рисовал «быстро и грязно» — только чтобы работало. Затем система эволюционировала — усложнялись алгоритмы работы, разрастался интерфейс, увеличилось количество параметров, появилась необходимость обучить другого человека работе с системой. Вносить изменение не порушив старый код становилось все сложнее и сложнее. Плюс некоторые архитектурные недоработки, заложенные с самого начала, очень осложняли жизнь.

На форумах, посвященных программированию микроконтроллеров, наткнулся на упоминание об rtos — операционных системах реального времени. Почитал, скачал пару бесплатных, попробовал и принял решение: написать свое.



Извечный вопрос: почему с нуля, а не взять готовое. Та потому что готовые универсальные системы становятся монстрообразными. Там, как в кухонном комбайне, есть все, но радости это не приносит. Все равно буду использовать 10% заложенных функций. А если так, то зачем грузить больше?

Итак, начнем с того, что есть в существующих rtos и мне не нужно:
  • вытесняющая многозадачность (много ресурсов на хранение и переключение контекста);
  • многопотоковость (все задачи разрабатываю я, ресурсов кристалла не много, поэтому изолированные потоки в данной разработке — только усложнение);
  • система приоритетов задач (не так уж много задач и не такие уж они критичные ко времени реакции);
  • гарантированное время отклика (не то, чтобы совсем не надо было, но задачи в основном неприхотливые ко времени. Главное — чтобы обработали).

Подумав еще немного, остановился на архитектуре event-driven («события рулят»).
Для этого ввел такие сущности:
  • событие (сообщение): может инициироваться по таймеру или непосредственно, вызовом соответствующей функции;
  • обработчик события: функция, которая вызывается при возникновении события. На одно событие может быть повешено несколько обработчиков и они будут вызываться последовательно;
  • таймер: можно включить таймер, по истечении которого будет однократно генерироваться заданное событие;
  • очередь сообщений: кольцевой буфер сообщений FILO.
  • диспетчер таймеров: функция, вызываемая по прерыванию таймера;
  • диспетчер событий: функция, которая крутится в главном цикле.


Какой сразу возникает плюс:
— обработчик аппаратных прерываний усыхает до десятка инструкций — создать определенное событие. Соответственно, нет необходимости запрещать прерывания в критичных секциях — задержка из-за прерываний редко превысит 1 мксек, чего вполне хватит даже при программной реализации протоколов типа 1-wire.

Итак, за дело. Сначала немного структур:

#define maxHandlers 64
#define maxMessages 64
#define maxTimers  64

#define MSG_ADC_CYCLE    1
#define MSG_KEY_PRESS    2
#define MSG_KEY_REPEAT    3
#define MSG_LCD_REFRESH    4
#define MSG_BRESENHAM    5
#define MSG_MENU_SELECT    6
#define MSG_MENU_CANCEL    7
#define MSG_1W_TEMP      8
#define MSG_CHECK_TEMP    9
#define MSG_DISP_REFRESH  10
#define MSG_TIMER_SEC    11

typedef unsigned char msg_num; // тип сообытия - мне пока хватает одного байта
typedef int msg_par; // тип параметра события
typedef unsigned char (*handler)(msg_par); // описание функции-обработчика

// структура записи из списка обработчиков
typedef struct{
  msg_num msg; // обрабатываемое событие
  handler hnd; // собственно сам обработчик
} iHandler;

// структура события из буфера событий
typedef struct{
  msg_num msg; // номер события
  msg_par par; // параметр
} iMessage;

// структура таймера
typedef struct{
  msg_num msg; // номер генерируемого сообщения
  msg_par par; // его параметр
  unsigned int time; // таймер в условных тиках (сейчас 10 мсек)
} iTimer;

iTimer    lTimer[maxTimers]; // список таймеров
iHandler  lHandler[maxHandlers]; // список обработчиков

iMessage  lMessage[maxMessages]; // буфер сообщений
unsigned int lMesPointer=0,hMesPointer=0; // указатели на начало и конец буфера

* This source code was highlighted with Source Code Highlighter.


Возможно, я нарушу первую сотню правил по наименованию переменных и функций. Ну что ж, не пинайте больно :)
Ну и так исторически сложилось, что я события называю сообщениями — как по мне, так эти сущности в данной системе эквипенисуальны.

Едем дальше. Работа с обработчиками событий:
// установка обработчика события
// вызывается: setHandler(MSG_KEY_PRESS, &checkKey);
void setHandler(msg_num msg, handler hnd) {
  unsigned char i,j;
  i=0; j=0;
  while (i<maxHandlers) {
    if (lHandler[i].msg==0) { // ищем свободное место
      lHandler[i].hnd = hnd; // и регистрирем обработчик
      lHandler[i].msg = msg;
      break;
    }
    i++;
  }
}

// снятие обработчика события
// вызывается: killHandler(MSG_KEY_PRESS, &checkKey);
void killHandler(msg_num msg, handler hnd) {
  unsigned char i,j;
  i=0; j=0;
  while (i<maxHandlers) {
    
    if ((lHandler[i].msg==msg) && (lHandler[i].hnd==hnd)) {
      lHandler[i].msg = 0; // если нашли нужный, очищаем
    }
    
    if (lHandler[i].msg != 0) {
      if (i != j) { // сдвигаем все записи к началу списка, чтобы дырок не было
        lHandler[j].msg = lHandler[i].msg;
        lHandler[j].hnd = lHandler[i].hnd;
        lHandler[i].msg = 0;
      }
      j++;
    }
    i++;
  }
}

* This source code was highlighted with Source Code Highlighter.


Работа с событиями:

// занести событие в очередь
// пример вызова: sendMessage(MSG_KEY_PRESS, KEY_MENU)
void sendMessage(msg_num msg, msg_par par) {
  hMesPointer = (hMesPointer+1) & (maxMessages-1); // сдвигаем указатель головы
  
  lMessage[hMesPointer].msg = msg; // заносим событие и параметр
  lMessage[hMesPointer].par = par;
  if (hMesPointer == lMesPointer) { // догнали начало очереди, убиваем необработанное сообытие
    lMesPointer = (lMesPointer+1) & (maxMessages-1);
  }
};

// обработка событий
void dispatchMessage() {
  char i;
  unsigned char res;
  
  if (hMesPointer == lMesPointer) { // если пустая очередь - возврат
    return;
  }
  
  lMesPointer = (lMesPointer+1) & (maxMessages-1); // сдвинем указатель
  
  msg_num msg = lMessage[lMesPointer].msg;
  msg_par par = lMessage[lMesPointer].par;
  
  if (msg==0)
    return;
  
  for(i=maxHandlers-1; i>=0; i--) { // просматриваем обработчики с конца
    if (lHandler[i].msg==msg) { // последний занесенный имеет приоритет
      res = lHandler[i].hnd(par); // вызываем обработчик
      if (res) { // если боработчик вернул 1, перываем обработку события
        break;
      }
    }
  }
}

* This source code was highlighted with Source Code Highlighter.


И работа с таймерами:
// установить таймер
// пример вызова: setTimer(MSG_LCD_REFRESH, 0, 50);
void setTimer(msg_num msg, msg_par par, unsigned int time) {
  unsigned char i,firstFree;
  firstFree = maxTimers+1;
  if (time == 0) {
    sendMessage(msg, par);
  } else {
  
    for (i=0; i<maxTimers; i++) { // ищем установленный таймер
      if (lTimer[i].msg == 0) {
        firstFree = i;
      } else { // если нашли - обновляем время
        if ((lTimer[i].msg == msg) && (lTimer[i].par == par)) {
          lTimer[i].time = time;
          return;
        }  
      }
    }
    if (firstFree <= maxTimers) { // иначе - просто добавляем новый
      lTimer[firstFree].msg = msg;
      lTimer[firstFree].par = par;
      lTimer[firstFree].time = time;
    }
  }
}

// убить таймер
// особенность - убивает все установленные таймеры на данное событие,
// не зависимо от параметра события
void killTimer(msg_num msg) {
  unsigned char i;
  for (i=0; i<maxTimers; i++) {
    if (lTimer[i].msg == msg) {
      lTimer[i].msg = 0;
    }
  }
}

// диспетчер таймеров
void dispatchTimer() {
  unsigned char i;
  msg_num msg;
  msg_par par;
  
  for (i=0; i<maxTimers; i++) {
    if (lTimer[i].msg == 0)
      continue;
    
    if (lTimer[i].time == 0) { // если пришло время
      msg = lTimer[i].msg;
      par =lTimer[i].par;
      lTimer[i].msg = 0;
      sendMessage(msg, par); // создаем событие
    } else {
      lTimer[i].time--; // иначе просто уменьшаем время
    }
  }
}

* This source code was highlighted with Source Code Highlighter.


Как ни странно, этого хватает для дальнейшей легкой разработки приложения. Ну об этом — дальше. :)
+69
27 апреля 2009, 15:59
41

комментарии (36)

+6
steel_ne #
Ну и классически прошу немного кармы для переноса в тематический журнал :)
0
NeonXP #
Теперь можете
+1
steel_ne #
Перенес, спасибо всем участвовавшим :)
–11
VDG #
все такое делали, и делают каждый день. и что тут нового? К.О.
+3
steel_ne #
Мне такого обзора не хватало. Или готовый монстр типа FreeRTOS, или голая теория. Понаступал на грабли, выкристаллизовал свое виденье. Если кому-то пригодится еще — значит свою миссию этот пост выполнил.
0
VDG #
ясно. эту штуку называют «карусель»
0
GubkaBob #
ой, простите, а 0K — это не вы?
0
spanasik #
Ждём продолжения :-)
+5
HounD #
Монстрообразно. Вы предполагаете излишнюю динамику там где ее не будет. Набор обработчиков у вас практически статичный, для каждого можно записать инвариант выполнения, и проверят список условий запуска. Истинность инварианта само по себе является событием, которое не нужно ни где хранить и передавать далее. А вообще почитайте про конечные автоматы :). Каким компилятором пользуетесь, в котором есть typedef, но нет enum'а? Зачем мешать в кучу unsigned char, unsigned int, int (в вашем компиляторе наверняка есть нечто вроде inttypes.h c человеческим uint8_t и др.)? Работа со массивом обработчиков то же не продумана. Выберете что-то одно: либо вставляйте в пустое место в массиве (в setHandler), либо не двигайте элементы (в killHandler). То что бросилось в глаза, внимательно не смотрел :)
0
steel_ne #
Начну с конца

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

Компилятор avr-gcc, есть inttypes.h, но внятного объяснения, почему надо использовать uint8_t вместо unsigned char я не уловил, кроме идеи тотальной портабельности. Не спорю, что мог нарушить какие-то рекомендации, но хотелось бы узнать, какие именно.

И по поводу динамики — да, можно сделать конечный автомат. Но когда нужно выполнять несколько действий с разной периодичностью (опрашивать датчики, перерисовывать экран, опрашивать клавиатуру, выдавать управляющие команды, и не забывать, что может прилететь из UART), то либо автомат становится монстрообразным и его уже на бумажке не отладишь, либо вырождается до трех состояний «инициализация-работа-ошибка», а все остальное впихивается в прерывания с непредсказуемыми задержками.

А что есть список условий запуска, как не организованная очередь сообщений?
0
HounD #
Если вы всегда сдвигаете обработчики к началу массива, то при вставке нового вам не нужно искать свободное место для вставки ;)

Все дело даже не в идее глобальной портабельности, а в том, и это особенно характерно для МК, что объемы памяти там микроскопичны, и использую uint8_t и др. вы сами себе облегчаете задачу контроля использования например стека. Мне практика показала что так удобнее, сугубо личный опыт.

Знаете, старую присказку про «кошек не любите?», так и про конечные автоматы. Конечно если попытатся запихать всю логику в один автомат то получиться монстр, который корректировать вручную сложно, никогда не нужно упираться в крайности.

Последнее. Говоря о высокоуровневых концепциях, как очередь в данном случае, то существование их в явном виде в коде не всегда обязательно.
0
steel_ne #
Точно, указатель на первую свободную ячейку.
В оправдание могу сказать, что текущие структуры эволюционировали (и сейчас эволюционируют), поэтому какие-то огрехи вполне могут иметь место.

А можно на пальцах, как будет выглядеть автомат для такой задачи:
Опрос датчика 1 каждые 750 мсек
Опрос датчика 2 каждые 200 мсек
Обновление экрана каждые 500 мсек (выводятся данные датчиков)
Реакция на клавиатуру (хотя бы старт-стоп).

Как я не прикидывал, от конечного автомата остается еще меньше, чем в моей системе от классической rtos ;)
0
HounD #
Вы лукавите ;) Редкий код занимается _только_ опросом датчиков. У меня нет ни малейшего желания вас упрекнуть или критиковать. Я уже написал ниже, что для любого решения всегда найдется достаточно аргументов, мы же здесь делимся опытом, правда?
0
steel_ne #
по поводу uint8_t — когда я увидел такую строчку

typedef unsigned char uint8_t;

в stdint.h, то понял, что фатальным нарушением использовать unsigned char не будет. Тем более, просмотрел листинг, структуры все ровно того размера как и планировалось. Где надо — 1 байт, где надо — два байта.

В любом случае, спасибо за науку, в следующем цикле рефакторинга обязательно исправлюсь.
+1
spanasik #
Обычно такие мысл возникают после прочтения чужого кода, «да, автор идиот, надо всё переписать !!».

Потом всё переписывается с нуля, и вместо того, чтобы доводить систему до функциональной полноты, снова повторяется весь цикл отладки, теперь уже с новыми, неизведанными багами. В процессе этого вся работа стоит, люди мягко говоря нервничают, но нам же это неважно, мы же пишем идеальную систему :-)

Я в таких случаях говорю себе — постой, ведь автор вряд ли идиот, и он тоже знает про конечные автоматы. В этом случае, если я в таком тоне ему об этом скажу, про меня подумают, что я не подумал про это, ммм… плохо.

Имхо, если процесс обсуждения происходит мягче, с учётом психологии, можно добиться гораздо больших результатов в коллективной работе.

Автор статьи молодец, по AVR вообще мало статей, а стиль — стиль у каждого свой, и это хорошо :-)
0
HounD #
Ничего не имею против чужого кода, приходилось поддерживать тонны всякого, скорее подход автора мне показался несколько неуместным для тех задач, которые он описал. И ни в коем случае не призываю все выкинуть и переписать, как могло показаться, в конце концов, каждый сам принимает решение, и любое принятое решение будет иметь достаточно веских аргументов в свою пользу. Про источники, вероятно на русском языке все имеют определенную специфику. Программированием микроконтроллеров и нас часто занимаются люди пришедшие из электроники, что накладывает определенную специфику :(. Для тех, кто дружит с английским, могу порекомендовать пожалуй, www.avrfreaks.net, ну и сам немного пытаюсь исправить ситуацию houndsblog.blogspot.com
0
spanasik #
Думаю, пока на русском разойдётся инфа по AVR, ARM уже будет стоить копейки :-)
Всё-таки, приятнее писать код, когда оперативной памяти больше чем 500 байт :-)
0
HounD #
Дело не в стоимости арм, а в том, что он много где совершенно избыточен
0
spanasik #
Согласен, но унификация великая вещь. ARM имеет запас, AVR — нет. Проще один раз написать и отладить софт под более мощный процессор, такой как ARM, и потом 20 лет им пользоваться, чем писать под AVR, а когда возникнет необходимость что-то добавить, всё переделывать второпях за оч. большие деньги.
0
HounD #
«Через 20 лет ничего не будет, будет одно телевидение» ;) Ну буду с вами спорить, каждый сам себе бобр. Не надо крайностей, вот и все. Во многих проектах и AVR много.
0
spanasik #
Билл Гейтс:

«I have to say that in 1981, making those decisions, I felt like I was providing enough freedom for 10 years. That is, a move from 64k to 640k felt like something that would last a great deal of time. Well, it didn't — it took about only 6 years before people started to see that as a real problem.»

Так что, когда мы говорим о 20 годах, вполне возможно, что речь идёт всего о 12-ти. И проекты меняются и люди, так что запас всегда нужен, не раз в этом убеждался :-)
+1
gadub #
требование гарантированного времени отклика — главное в определении осрв
так что то, что тут описано, даже не «почти осрв». просто диспетчер событий, который быстро работает
+2
gadub #
вернее, реализация fsm
0
steel_ne #
Вопрос классификации всегда спорный. Этот диспетчер гарантирует мне время отклика 1 мсек при всех возможных комбинациях входящих взаимодействий. Этого для решения моих практических задач хватает. Да, это достигнуто не только особенностью системы, но и оптимизацией функций-обработчиков, которые не занимают процессор дольше 10-100 мксек.

Вот и получилась «недоОСРВ».
0
HounD #
Кстати про классификацию. Если речь идет про РВ, то лучше всегда формулировать то определение РВ, которое вы приняли для себя как руководствующее. Согласитесь два определения РВ: «превышение максимально допустимого времени реакции равнозначно отказу системы» и «детерминированность алгоритма по времени вне зависимости от входных данных» накладывают совершенно разные ограничения.
0
ICD2 #
Отлично начало =) поскорее бы увидеть продолжение.
0
DIHALT #
При выполнении установки события в очередь записывается структура. Если в этот момент будет интеррупт и там тоже будем записывать событие, то может быть конфликт с непредсказуемыми последствиями. Прерывания надо бы там запрещать.
0
steel_ne #
Согласен, этот механизм уже переработан.
Но тут второй вопрос — когда разрешать прерывания? Если я разрешу прерывания в конце функции, то при возврат в обработчик прерывания будут разрешены, тоже с непредсказуемыми последствиями.

Пока реализовано так: вызываю занесение в очередь в самом конце обработчика прерываний (пока это возможно).

В принципе, вариантов два:
— предусмотреть параметр вызова — флажок, надо ли запрещать/разрешать прерывания;
— после вызова в обработчике принудительно запрещать прерывания;

Какие еще могут быть варианты?
0
DIHALT #
В WinAVR есть такая интересная приблуда как atomic.h

Так вот в ней есть макросы клевые. atomic forseon и atomic restorestate

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

А вот второй хорош тем, что он запрещает прерывания однозначно, но на выходе разрешает их ТОЛЬКО если они были до этого разрешены.

Примерно то же самое только с другим синтаксисом есть в IAR. Вообще погугли по атомарным операциям. Собственно проблема у тебя именно с ними.
0
steel_ne #
И приятно, что мэтр посетил пост :) AVR я изучал в основном по вашему сайту и даташитам.
0
DIHALT #
Ну прям уж мэтр :))))

А сколько занимает скомпиленное ядро?
0
steel_ne #
Само ядро диспетчера — чуть больше килослова. В принципе можно оптимизировать, но мне пока не критично.

А за atomic.h — спасибо, поробую применить )
0
DIHALT #
Жирновато. Можно в 720 байт загнать. Могу дать пофтыкать один свой проектик.

Рекомендую избавиться от нумерации задачи и оперировать сразу же адресами. Это будет проще и эффективней.

З.Ы.
А я пожалуй себе потырю у тебя функцию kill Timer :) А то чет я до такой не догадался :)
0
steel_ne #
Давай, конечно. Пофтыкать оно всегда полезно.

А по поводу нумерации — пока немного не понял. У меня ж не задачи, у меня — сообщения. Event-driven. То есть одно сообщение могут обрабатывать несколько обработчиков последовательно.
0
steel_ne #
упс. Не килослова, а килобайта. 1124 байт, запутался в нумерации :)
НЛО прилетело и опубликовало эту надпись здесь

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.