AVR. Учебный Курс. Архитектура Программ

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

Самое интересное, что правильной организации программы учат программистов в ВУЗах, но вот только к микроконтроллеру народ обычно идет не от программирования, а от железа. А, как показала практика обучения в ВУЗе, электронщиков толковому программингу практически не обучают :( Приходится все додумывать самостоятельно.

 
Итак, что такое структура программы. Это, прежде всего, ее скелет. То какими путями движется код. Как организованы переходы между задачами прошивки. То как распределяется процессорное время. Без краткого ликбеза по общим принципам построения прошивки дальше двигаться нет смысла.

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

 

Итак, я для себя выделяю следующие структуры, по порядку возростания сложности конструкции и количеству управляющего кода:
 

  • Суперцикл
  • Суперцикл+прерывания
  • Флаговый автомат
  • Диспетчер
  • Приоритетный диспетчер
  • Кооперативная RTOS
  • Вытесняющая RTOS

 

А теперь подробно по каждому пункту:
 

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

Алгоритм прост как мычание (псевдо код):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void main(void);
{
while(1)
	{
	Led_ON();
	Delay(1000);
	Led_OFF();
 
	u=KeyScan();
	switch(u)
		{
		case 1: Action1();
		case 2: Action2();
		case 3: Action3();
		}
	}
}

 

И все примерно в таком духе. Т.е. тупое последовательное выполнение кода. Задержка — прям там же, в общей куче. Опрос клавиатуры. Тут же. Внутри суперцикла могут быть переходы и ветвления, но суть остается та же самая — тупое движение по коду.
Достоинства очевидны сразу логичность и простота (поначалу, потом это ад). Недостатки тоже вылазят сразу же — чем больше мы запихнем в наш код, тем он будет более неповоротливым и тормозным.
Однако суперцикл поддается оптимизации. Например, в функцию Delay() можно не тупо впустую щелкать тактами, а запихать туда опрос клавиатуры и еще кучу полезного экшна.
В итоге, на суперцикле можно сделать сверх компактную, надежную, но при этом совершенно монолитную программу. В нее будет нельзя ни добавить ни отнять. А еще чтобы все учесть, написать и отладить надо быть гением. Читали историю про Один байт? Вот это наверняка про нее или про следующий вариант.
Обычно народ, наигравшись с суперциклом, раскуривает прерывания и быстро переходит к варианту суперцикл+прерывания.
 

Суперцикл+прерывания
Чистый суперцикл используется крайне редко, потому как в любом микроконтроллере куча периферии и у них есть такая удобная вещь как прерывания (Хотя, я встречал на форуме microchip.ru высказывание что прерывания снижают надежность программы и лучше избавляться от них по возможности).
 

Здесь ситуация становится несколько иной. За счет прерываний у нас появляются параллельные процессы. Например, мы можем повесить опрос клавиатуры и мигание лампочкой на прерывание по таймеру (псевдокод):
 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
ISR(Timer1)
{
u=KeyScan();
}
 
ISR(Timer2)
{
if(LED_ON)
	{
	Led_OFF();
	}
	else
	{
	Led_ON();
	}
}
 
void main(void);
{
Timer1_Delay=100; 	// Выдержка таймера1 100
Timer1=1<<ON;     	// Включить таймер1
 
Timer2_Delay=1000;	// Выдержка таймера2 1000
Timer2=1<<ON;		// Включить таймер2
 
SEI();			// Разрешить прерывания
while(1)
	{
	switch(u)
		{
		case 1: Action1();
		case 2: Action2();
		case 3: Action3();
		}
	}
}

 

Уже лучше. Вроде как программа теперь крутится тремя независимыми процессами. Нигде не тормозит и все в порядке. Да, до тех пор пока хватает аппаратных ресурсов. У нас ведь что тут получается — на каждый процесс по прерыванию. А таймеров то всего ничего и прерывания грузить тяжелыми по времени процессами нельзя — они должны быть быстрыми.
 

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

Что делать? А что делать когда возникает анархия? Правильно! Нужно какое то единое правление — бюрократический аппарат. С теоретической точки зрения от него толку как от козла молока — клавиатуры он не опрашивает, ножками не дрыгает, данные не шлет. Только место занимает и процессорное время жрет. Но без него как без рук.
 

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

Итак, в суперциклах у нас задачи вызывались одна за другой. Либо в порядке общей очереди, либо в виде прямого вызова. Очевидно, что эта цепь получается неразрывной и если в ней есть затыки (например какой нибудь DELAY), то встает вся цепь.
Флаговый автомат позволяет разорвать эту цепь.
 

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

Пусть у нас будет байт flag, а в нем разные биты, выделяемые на нашем псевдокоде точкой. Итак, выглядит примерно это так (псевдокод):
 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void main(void)
{
 
//Start Task
flag.ScanKey=1;	//Запустить сканирование клавиатуры
flag.Led_On=1;	// Запустить мигалку
 
while(1)
	{
	if(flag.keyscan==1) 
		{
		u=ScanKey();
		switch(u)
			{
			case 1: flag.Action1=1;	// Поставить задачу1 на выполнение
			case 2: flag.Action2=1;	// Поставить задачу2 на выполнение
			case 3: flag.Action3=1;	// Поставить задачу2 на выполнение
			}
		}
 
	if(flag.led_On==1) Led_ON();	// Если стоит флаг включения - включить
	if(flag.Led_Off==1) Led_OFF();	// Если стоит флаг выключения - выключить
 
	if(flag.Action1==1) Action1();	// Если надо выполнить задачу 1 - выполнить
	if(flag.Action2==1) Action2();	// Если надо выполнить задачу 2 - выполнить
	if(flag.Action3==1) Action3();	// Если надо выполнить задачу 3 - выполнить
	}
}

 
Задача может выглядеть примерно так (всевдокод):
 

1
2
3
4
5
6
7
void Action1(void)
{
flag.Action1=0; 			// Задача выполнена. Сбросить флаг
DoSomeThing();			// Делаем что то полезное
if(Some_Event) flag.Action3=1;	// Если какое то условие выполнено, то поставим на выполнение 
				// Задачу 3
}

 
Как видишь, тут нет прямой передачи управления между блоками. Все делается через флаги. Надо запустить задачу мы не передаем ей управление, а ставим флаг ее запуска. И при следующей итерации главного цикла задача будет выполнена, а флаг сброшен (или не сброшен, если задача запущена на циклическое исполнение).
 
Наращивание функционала идет без особых усилий — мы просто добавляем новые флаги и новые секции if(flag.****==1) { }
 

Только у нас лампочка должна была там мигать. Что делать с разнокалиберными временными задержками? Ведь флаговый автомат эту проблему так и не решил. Да, сам по себе флаговый автомат бесполезен. До тех пор пока на арену не вылазит…
 

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

Для начала надо сконфигурировать системный таймер, чтобы он генерировал нам системные тики. У меня за один тик принята 1мс. Поэтому я настраиваю таймер таким образом, чтобы он мне каждую миллисекунду генерировал прерывание. А вот в его прерывании мы и учиним беспредел.
 

Программный таймер обычно делается в виде массива структур (псевдокод):
 

1
2
3
4
5
6
volatile static struct					// Глобальная переменная
                  {		
                  Number;				// Номер флага в флаговом байте	
                  Time;					// Выдержка в мс
                  }
                  SoftTimer[Max_Numbers_of_Timer];   	// Очередь таймеров

А в прерывании по таймеру гоним примерно следующий код (псевдокод):
 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ISR(Timer1)
{
	for(i=0;i!=Max_Numbers_of_Timer;i++)      // Прочесываем очередь таймеров
	{
  	 if(SoftTimer[i].Number == 255 ) continue; // Если нашли пустышку - следующая итерация
 
	 if(SoftTimer[i].Time !=0)		 // Если таймер не выщелкал, то щелкаем еще раз.
      		{                                    		
      		SoftTimer[i].Time --;	 // Уменьшаем число в ячейке если не конец.
      		}
   		else
      		{
      		flags |= 1<<Number ;	 // Дощелкали до нуля? Взводим флаг в флаговом байте
      		SoftTimer[i].Number = 255;	// А в ячейку пишем затычку -- таймер пуст.
      		}
   	}
}

 

Видишь, все просто. Мы прочесываем массив структур таймеров, поэлементно. Если в поле Number у нас 255, то очевидно что это пустой таймер. Т.к. номер флага таким быть не может (в качестве номера флага может быть только число с одним единичным битом). Такой таймерный слот пропускается.
Если таймер не пуст, то мы проверяем поле Time на ноль. Если не ноль, то уменьшаем и переходим к следующему элементу массива.
А коли таймер дощелкал, то мы устанавливаем взводим во флаговом регистре бит лежащий в поле Number. И при следующем прогоне главного цикла управляющая конструкция if(flag.***==1) Запустит нам нужную задачу.
 

Число таймеров определяется исходя из числа одновременно отсчитываемых временных интервалов. Мне обычно хватает десятка. Если сделать меньше — может не хватит и получишь срыв таймерной очереди. Если сделать с большим запасом, то очередь будет дольше обрабатываться в прерывании, а это снижает точность отсчетов интервалов, да и вообще возникает лишний затык.
 

Ставится таймер откуда угодно функцией SetTimer примерно такого вида (псевдокод):
 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void SetTimer(NewNumber,NewTime)
{
InterruptDisable();			// Запрещаем прерывания. Помним об атомарном доступе!
 
for(i=0;i!=Max_Numbers_of_Timer;i++)   	//Прочесываем очередь таймеров. Ищем нет ли уже там такого
	{
	if(SoftTimer[i].Number == NewNumber)	// Если уже есть запись с таким флагом
		{
		SoftTimer[i].Time = NewTime;	// Перезаписываем ей выдержку
		InterruptRestore();
		return;				// Выходим.
		}
	}
 
for(i=0;i!=Max_Numbers_of_Timer;i++)	//Если не находим, то ищем любой пустой
	{		
      	if (SoftTimer[i].Number == 255)	
	 	{
	 	SoftTimer[i].Number = NewNumber;	// Заполняем поле флага
	 	SoftTimer[i].Time = NewTime;		// И поле выдержки времени
		InterruptRestore();
		return;					// Выход.
	 	}
      	}
InterruptRestore();	// Восстанавливаем прерывания как было. 
 // тут можно сделать return c кодом ошибки - нет свободных таймеров
}

 
Работает тоже не сложно.
На входе у нас два значения — время NewTime и флаг который мы должны воткнуть.
Мы прочесываем нашу очередь таймеров, в поисках свободной ячейки либо таймера на то же событие с еще не истекшим сроком. Если находим такой же таймер — апдейтим его на новое время. Если не находим, то втыкаем данные впервую свободную ячейку. А если не нашли и свободной ячейки, то получаем Timer Fail. Это ошибка, ее надо учитывать делая очередь с запасом, либо точно высчитывая сколько таймеров нам надо.
 

Теперь, вооруженные знанием о софтверных таймерах, рассмотрим как будет организована наша мигалка (псевдокод):

1
2
3
4
5
6
7
8
9
10
11
12
13
void LED_ON(void)
{
flag.led_On=0; 			// Отработали, флаг можно сбросить
SET_BIT_LED(); 			// Собственно, зажгли что то там
SetTimer(OFF_LED_FLAG,1000);	// Поставить флаг на погашение через 1с
}
 
void LED_OFF(void)
{
flag.led_Off=0;		// Отработали, флаг можно сбросить
CLR_BIT_LED();			// Собственно, погасили что то там
SetTimer(ON_LED_FLAG,1000);	// Поставить флаг на зажжение через 1с
}

 

Все! Видите какие простые и линейные получаются кусочки. И насовать в эту систему еще с полтора десятка цепочек не составит труда. Добавляем флаги и условия, происываем таймеры и вперед. Причем флаги мы можем ставить как в задачах, так и в прерываниях. А главный цикл будет с хрустом это пережевывать.
 

Думаю понятно, что в такой организации скорость работы главного цикла является критичной. В том плане что всякие хардверные тупые задержки вроде delay_ms(****) в ней КРАЙНЕ НЕЖЕЛАТЕЛЬНЫ. Т.к. они тормозят весь конвейер. Все должно быть сделано через службу таймеров. Если нужны очень короткие задержки, то можно завести второй аппаратный таймер с такой же байдой, но тикающий чаще. Но тут надо думать и смотреть.
 

Еще одним недостатком такой организации является жесткий порядок выполнения операций. Т.е. пока мы не пробежим по всей цепочке if(flag.***==1) мы не сможем выполнить какую либо операцию заново (хотя если цепь быстрая, то редко когда критично).
Это частично решается переходом на конструкцию на базе SWITCH-CASE конструкции с выходом в корень после каждой операции. В этом случае у нас возникает другое западло — часто вызываемые задачи могут заблокировать выполнение более медленных.
 

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

Но об этом в следующем посте, а то Война и Мир получается. Оставайтесь на линии!
 

139 thoughts on “AVR. Учебный Курс. Архитектура Программ”

  1. >> ока мы не пробежим по всей цепочке if(flag.***==1) мы не сможем выполнить какую либо операцию заново

    Обычно решаю эту проблему тщательно подбирая последовательность if`ов в главном цикле и втыкая continue где надо. switch неудобный вариант, потому что в ряде случаев надо проверить несколько условий для выполнения задачи («если флаг А или флаг В»).

    В целом хорошая статья, спасибо =)

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

      Есть альтернатива — явное выделение состояний (автоматное программирование).
      И тут как раз switch рулит — if-ветвление при многоальтернативности запутывает очень нехило. Есть даже целое направление, называемое «Switch-технология», причём появляются инструменты, генерящие код из UML-диаграммы состояний.

      Я в комментариях ниже накидал ссылок.

  2. Для простого главного цикла с флагами, какие обычно использую я, программный таймер излишне усложнен. Такой больше подходит для RTOS. Нет смысла в достаточно простых программах с фиксированным количеством задач делать динамическое распределение таймеров, что тратит время и усложняет понимание программы. Я просто каждой задаче, если ей нужно, выделяю свой программный таймер. Причем, он может быть и, например, секундным. Когда задаче нужно, она просто кидает в него нужное значение задержки и при следующих проходах цикла проверяет его на 0. Прерывание 1мс просто просматривает цепочку таймеров и декрементирует те, что не 0. Это быстрее, чем предварительно проверять флаги их состояний, да и таймеров редко бывает больше десятка. Кроме того, часто закладываю еще один счетчик на 1000, по обнулению которого проверяется и декрементируется цепочка секундных программных таймеров. Туда же иногда цеплял программные часы и календарь (сейчас проще DS1307 использовать).
    Чтобы ускорить вращение главного цикла, у каждой задачи есть свои флаги состояния (в зависимости от сложности задач), и крупные (длительные) задачи выполняю не сразу целиком, а по частям, чтобы не тормозить другие, более мелкие. Проверку флагов делаю в таком порядке, чтобы в первую очередь проверялись более вероятные, а пропуски задач делались максимально быстро. Пока все работало…

    1. А зачем вообще в этом случае делать кучу разных таймеров? Заводишь глобальное время, какую нибудь зверскую 6ти байтную переменную и тикаешь ей глобальное время. А все задержки отщелкиваешь по смещению между текущим значением и нынешним. А чтобы не шерстить все байты вычисляешь только интересюущие тебя разряды +/- один разряд.

      1. «Заводишь глобальное время, какую нибудь зверскую 6ти байтную переменную и тикаешь ей глобальное время. А все задержки отщелкиваешь по смещению между текущим значением и нынешним.»
        ———————————-
        Так это же вообще изврат какой-то… Ради проверки, кончился ли заданный интервал, городить кучу многобайтных вычислений, контроллер только этим и будет заниматься… Это даже еще хуже, чем динамическое распределение таймеров. Мне бы такое даже в кошмарном сне не приснилось.
        У меня — проще некуда. Кинул значение в нужный программный таймер, и потом просто смотришь, не «0» ли там. Для задачи свой программный таймер — просто переменная, загружаемая значением задержки, когда надо, и проверяемая на 0. А в прерывании просто делаешь всем декремент, если не 0 (в 8048, например, это вообще одна команда была). Проверка на 0, декремент, если не 0, иначе пропуск. Только и делов. К некоторым можно в прерывании при обнулении добавить установку флажка, который в главном цикле запустит неактивную по умолчанию задачу (если этот таймер запускает другая задача). Другие задачи проверяют свои счетчики сами.
        А при динамическом распределении таймеров ты к ним обращаешься косвенно, предварительно вычисляя адрес, отыскиваешь свободный, освобождаешь неиспользуемые, выставляешь и проверяешь флаги таймеров, в том числе и в прерывании. Нафига столько лишней работы? Для операционки — поневоле. А в простой флаговой системе — то нафига?

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

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

          «А при динамическом распределении таймеров ты к ним обращаешься косвенно……»
          А на AVR разницы нет. Косвенная адресация там развита очень мощно, а вот прямая адерсация в память только через LOAD/STORE поэтому что так что эдак грузить переменную из памяти разница не велика. На Пике может это и существенно было бы.

          1. «Правила Хольцмана для надежного ПО», применяемые в НАСА, гласят:

            — не допускается динамическое выделение памяти, за исключением команд инициализации нового объекта;
            — использование указателей максимально ограничивается. В любом случае допускается не более одного уровня ссылок (не разрешены ссылки на ссылки). Указатели на функции запрещены;

      2. Действительно, динамическое распределение таймеров востребовано только при заранее неизвестном составе крутящихся задач. То есть на полноценной RTOS.
        В случаях с фиксированным набором задач всё же оптимальнее фиксированный набор таймеров.
        Например, такой вариант у Татарчевского описан:
        http://kit-e.ru/articles/circuit/2007_3_180.php

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

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

      У DI_HALT’а таймеры являются частью диспетчера задач (планировщика) и могут служить для распределения времени ЦП между задачами (автоматами). Ведь автоматы, составляющие приложение, в общем случае должны работать с разными тактовыми частотами, т.е. интервалы между вызовами таких автоматов должны быть разными. Можно сделать планировщик с жестким распределением этого ресурса и для отдельных приложений такая схема будет оптимальной, но схема с использованием таймеров является более гибкой и универсальной. Например, автомат, управляя, таймером, может на ходу менять свою тактовую частоту в зависимости от каких-либо условий. Мне только кажется, что динамическое распределение таймеров может оказаться избыточным.

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

      1. Чем мне не нравится концепция SWG так это тем, что ожидающий таймера код продолжает крутиться в главном цикле. Хавая процессорное время и затягивая реакцию на событие. При этом у нас также тикает и таймер. Получается двойная работа. Вместо того, чтобы проверить и выполнить один раз (при обнулении таймера) мы это делаем в каждой итерации главного цикла. Да, при этом мы имеем экономию памяти в один указатель/флаг. Но стоит ли это того? Нарастить быстродействие сложней чем заиметь больше памяти.

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

        2. «ожидающий таймера код продолжает крутиться в главном цикле. Хавая процессорное время и затягивая реакцию на событие.»
          —————-
          Так и диспетчер тоже постоянно вертится с теми же потерями. Кроме того, часто у задачи есть другая работа, кроме ожидания отработки таймера, а если делать больше нечего — переход у другим задачам происходит максимально быстро.

          » При этом у нас также тикает и таймер.»
          —————
          Так он и должен тикать, если его загрузили, иначе он просто пропускается. А при динамическом распределении надо в прерывании еще и определять, какие таймеры распределены, какие нет, так что в любом случае получается дольше. У меня же в прерывании на каждый таймер всего 5 команд асма, независимо от количества таймеров в программе. А с динамическим распределением чем больше таймеров, тем больше накладные расходы на их распределение, проверку, индексацию, и прочее…

          «Вместо того, чтобы проверить и выполнить один раз (при обнулении таймера) мы это делаем в каждой итерации главного цикла.»
          —————
          Ну-ну… С динамическими таймерами надо проверять, какие сейчас задействованы, декрементировать их, проверять, обнулился ли он, по обнулении каким — то образом сообщать это задаче, (просто так запустить задачу нельзя, поскольку она может использовать таймер неоднократно и при разном своем текущем состоянии). Набегают десятки, если не сотни команд ассемблера, даже если на Си это десяток строчек. Где же здесь выигрыш? Куда проще задаче самой проверять таймер (являющийся для нее всего лишь байтовым флажком) на 0, занимаясь в то же время другими делами. Накладные расходы минимальны, ничего лишнего.
          Диспетчер же крутится постоянно, проверяя те же флажки, и также занимая ресурсы.

          Например, у меня задача выдачи информации в радиоканал, включив несушую передатчика, и запустив таймер для стабилизации АРУ в приемнике, в это время занимается подготовкой в буфере пакета данных для передачи, по окончании задержки таймера начинает выдавать байты в канал, и в это же время главный цикл продолжает вертеться, другие задачи также отрабатывают свои кусочки, и если надо, также следят за своими таймерами. Все вертится предельно быстро, время прогона цикла — доли миллисекунды. За время передачи или приема байта с канала главный цикл пробегает многократно. Крупные задачи разбиваю на отдельные этапы выполнения таким образом, чтобы в любом случае все кольцо успело провернуться нескодько раз между тиками таймера, а если уж шибко надо, для критических по времени событий использую и прерывания. И все просчитано по тактам, все предсказуемо. За 1 мс на частоте 16МГц PIC выполняет 4000 одноцикловых команд. Команд с 2 циклами (команды с переходами) обычно не так много, реже больше 10% от общего количества. Так что вполне хватает, даже для программ на Паскале. Например, у меня сейчас программа ходового контроллера менее 1,5 килобайт. Правда, я пока выкинул некоторые куски (например, обмен с контроллером бамперов переделываю на I2C), но она у меня пока еще больше 1800 байт ни разу не была.

  3. В SetTimer бажина затесалась: если в очереди таймеров есть пустой таймер ДО таймера с Number == NewNumber, то будет использован пустой таймер, а не переписан нужный. :)

    По поводу «Особые извращенцы под каждый флаг заюзывают по байту» я бы добавил, что иногда так приходится делать, если при реализации конкретной задачи RAM’а оказывается много, а flash’а мало.

        1. Попридераюсь чуть-чуть :) Индекс пустого таймера можно искать-запоминать в первом цикле, что поможет избежать второго.

      1. супецикл есть везде одно дело его писать самому а другое использовать готовый
        а таймеры и прерывания я бы выделил в одну группу — хардварная событийная модель.

    1. Не всегда, не все контроллеры работают в зацикленном состоянии. Как глупый вариант: таймер для взрывного устройства. А суперцикл мы считаем само-собой разумеющимся, т.к. все наши устройства базируются на этом понятии. И DI HALT правильно сделал, что вывел его в самое начало для описания структуры будущих разработок. Но можно было в основу положить линейное исполнение с последующим выключением.

        1. Я вообще не предлагаю, что-либо писать. Просто поясняю, что кроме зацикливания есть линейные задачи. А раз вы решили, что описание «глупого варианта» — предложение к написанию, то вы кто? Тот кто решил, что его назвали глупцом, но ведь я не писал, что вы глупы…

    2. В самом начале статьи автор пишет, что это чисто его терминология и нефиг за это пинать. А если уж подходить формально, то практически любой планировщик задач основан на суперцикле. Что ж теперь не использовать этот термин? Предложите другие варианты названия такого суперцикла, в котором нет никаких зачатков планировщика.

    1. Скажем так. Там цикл связывает воедино лишь компонеты самой ОС. А вот реальные задачи гоняются уже не по жесткому циклу, а как их диспетчер растасует.

      В концепции же чистого суперцикла у нас

      while(1)
      {
      action1();
      delay();
      action2();
      action3();
      }
      и все в таком духе.

  4. Скажем так. Ты сам так пишешь?
    Нет, не пишешь.
    Пиши людей писать правильно, используя аппаратные таймеры, прерывания, флаги.
    Или они всю жизнь будут писать DELAY();
    По поводу операционки.
    Программист встроенных систем должен полностью контроллировать все ресурсы. Не важно одна задача или многозадачная ОС. Кто будет писать нижний уровень для ОС, дядя?
    Суперцикл есть всегда, поверь

    1. Да не говорю я что его нет. Повторяю еще раз — он есть.

      Вопрос лишь в том чем он занимается. Непосредственной прогонкой рабочего кода или циклическим пинанием диспетчера который уже сам все растасует как надо.

      Разница в том на чем акцентирует внимание программист когда пишет функционал.

      ОС или диспетчер или автомат пишется один раз, а потом уже поверх него наращивается мясо функционала, используая средства диспетчера/ОС/автомата. Не сильно заморачивась о том в какой последовательности оно пойдет в реальной жизни. Главное чтобы потоки заданий шли в нужном порядке и все. Тут каждую цепочку заданий или каждую задачу рассматриваешь как отдельный поток, выполняемый псеводпараллельно друг другу.

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

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

        1. Проблему неоптимального кода можно решить на примере Microsoft+Intel. GHz-ы(CPU)->GByte-ы(RAM)->TByte-ы(ПЗУ) — все быстро и наглядно (время — деньги!). А колличество ног МК определяется не объемом RAM или ПЗУ, а разрядностью МК и возможностями для подключения периферии — яркий тому пример связка МК Freescale MC68HC05 (8 bit) и ColdFire v1 (32 bit).

        2. А тут ситуация у нас такая.
          Легкая задача, не требовательная к скорости/времени. Да можно решить на флажках с простотой, но на ОС подобной среде быстрей. А память все равно будет пустовать.

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

          Оверхед диспетчера тоже палка о двух концах. У меня он занимает около 400-500байт (на ассемблере около 250байт). Но это навсегда и больше расти он не будет. А множественные же флажки, структуры, конструкций логические продолжают возрастать линейно с увеличением сложности. И может возникнуть такая ситуация, что потери на управление тут будут даже выше чем на диспетчере.

          1. » А множественные же флажки, структуры, конструкций логические продолжают возрастать линейно с увеличением сложности. И может возникнуть такая ситуация, что потери на управление тут будут даже выше чем на диспетчере.»
            ———————————————
            С увеличением количества и сложности задач диспетчер тоже надо будет усложнять, чтобы ресурсы распределялись оптимально. Для фиксированного и заранее определенного набора задач проще один раз просчитать порядок и время их выполнения, разбить на части, чем обучать этому диспетчера, причем под каждый набор задач тоже заново. Обычно разработчик с опытом уже интуитивно, на уровне подсознания, чувствует узкие места в программе и способы их преодоления. А вот обучить этому диспетчера…
            Кстати, посмотрел вчера, как компилятор МикроПаскаля обрабатывает мои программные таймеры в прерываниях. Вот один из них («pauza») на Паскале, всего 1 строка, как и для любого из них:

            if pauza > 0 then Dec(pauza);

            и то что компилятор сделал на ассемблере:

            ;ROBO_SWG_1P.mpas,117 :: if pauza > 0 then Dec(pauza);
            0x024A 0x0856 MOVF _pauza, 0
            0x024B 0x3C00 SUBLW 0
            0x024C 0x1803 BTFSC STATUS, 0
            0x024D 0x2A4F GOTO L_ROBO_SWG_1P_Int_Timer05
            0x024E 0x03D6 DECF _pauza, 1
            L_ROBO_SWG_1P_Int_Timer05:

            Итого: 5 команд, 6 циклов, итого на частоте 16МГц = 1,5мкс.
            Куда уж проще. Загрузка и проверка его в программе — обычная загрузка и проверка байтовой переменной, безо всяких наворотов типа распределения таймеров, вычислений, а какой же в данный момент используется задачей, и прочее:

            pauza := 100; // начальная пауза
            …….
            if ((Sost_Prd = 0) and (pauza = 0)) then //- Если начальная пауза окончена!
            begin

            Кстати, в байте слова состояния передатчика (Sost_Prd) — 8 флажков для поэтапной обработки передачи. В исходном состоянии они все в 0, поэтому тут проверяется байт целиком. Хотя и для работы с битами у PIC тоже просто.
            Например: Sost_Prd.6 := 1; // Передача начата

  5. Да, мне гораздо проще — я пришел из программинга :) точнее я им и занимаюсь — а мк — хобби :) даже не знаю чего меня потащило в электронику, но мне понравилось….. наверное хотелось создать что-то, что можно пощупать, подержать в руках….. а программный код в руках не подержишь :)

      1. угу, я когда занялся, мне сразу вспомнились пропущеные пары по ТОЭ и электронике в универе. Теперь приходится самому перечитывать учебники :)

  6. Немножко по коду (псевдокоду) придерусь, можно?
    if(flag==0) и if(flag==1) — вариант индусский
    if(!flag) и if(flag) — наш выбор
    с масками — то же самое, в С/С++ любое не нулевое значение конвертируется в TRUE и нулевое в FALSE

    по инкрементам/декрементам — если возвращаемое значение игнорируется, лучше всегда использовать префиксный вариант т.е.
    ++i вместо i++, и —i вместо i—
    это связано как раз с возвращаемым значением, префиксный вариант сначала инкрементирует/декрементирует, потом возвращает значение, постфиксный вариант сначала возвращает значение, потом инкрементирует/декрементирует, но даже если значение не используется — память под него резервируется и в нее записывается начальное значение.

    1. Ну это же псевдокод, я специально так пишу чтобы понятней было. А индусский вариант читабельней не для посвященных.

      А по поводу i++/++i может быть. Но Надо посмотреть что там компилятор делает. У меня есть бооольшое подозрение что ему пофигу.

      1. Ну это оптимизация компилятора, а компиляторы разные. Но привычка дело правильное — даже вон гугл об этом говорит: http://bit.ly/5Ai4EP
        Кстати, давно-давно проверял (правда на х86) — действительно имелся косяк

    2. >> по инкрементам/декрементам — если возвращаемое значение игнорируется, лучше всегда >> использовать префиксный вариант т.е.
      >> ++i вместо i++, и –i вместо i–

      Не смущайте людей. Это из плюсов, когда итераторы там не целые переменные, а классы.
      В данном случае в циклах можно с чистой совестью писать i++.
      Код гарантировано будет ОДИНАКОВЫМ. Это гарантирует нам Бьёрн Страуструп и gcc в частности.
      С целыми итераторами имеет значение только в выражениях, и то надо постараться
      чтобы компилятор таки выделил лишнюю память под префиксный инкремент.

    3. >>if(flag==0) и if(flag==1) — вариант индусский
      >>if(!flag) и if(flag) — наш выбор

      не гоните пургу pls :). Вариант c if(!flag) абсолютно нечитаем при быстром просмотре кода, а посему провоцирует ошибки. Плавали, знаем.

      Помните, что умный программист тупым кодом пишет гениальные вещи, а не наоборот (c).

      1. Не гоню, вариант if(!flag) очень читаем и понятен, вот, чесн — за всю свою практику, а это уже лет под 10 ни разу не слышал, что !var не читаем. Хороший программист владеет языком на котором пишет. Тупой код порождает баги и тормоза.
        Вот кстати, косяков больше порождает вариант if(flag=0) — и при беглом чтении не всегда его видно.

        1. 1. Про тупой код. Между «тупым кодом» и «тупым алгоритмом» разница не только семантическая. Пояснять надо?

          2. Использование абсолютно всех конструкций языка не является показателем какой-то крутости автора. Если то же самое можно написать более «понятным языком», автор будет нещадно бит на code review и отправится переписывать. Речь, естественно, не идет о каких-то учебных проектах или когда автор один, и никому его творчество больше не интересно. В последнем случае автор может писать как угодно для удовлетворения собственных амбиций.

          3. !var менее читаем хотя бы потому, что var == 0 занимает больше символов на экране, следовательно, выше вероятность того, что ошибка не будет пропущена на code review. Еще раз повторюсь, речь тут не идет о том, что кто-то не понимает, что означает «!». Это всего ли вопрос стиля, который позволяет писать более надежные программы при тех же трудозатратах.

        2. на последнем месте работы был принят стиль if( true == var ). Поначалу непривычно, но когда пару раз случайно напишешь присваивание вместо равенства и компилятор на это ругнется, понимаешь что «Так хорошо».

    4. >> if(flag==0) и if(flag==1) — вариант индусский

      бывает частенько ошибочным
      лучше if (0 == flag) при ошибке компилятор матюгнется на присвоение константе

      вообще неплохой стиль использовать такие схемы

      >> if(!flag) и if(flag) — наш выбор
      не самый лучший выбор
      if ( !flag ) — пробелы дают лучшее представление о том что внутри
      не мельчите… тяжеко потом читать ))

      пишу как кодер с++ с 17 летним стажем ))

      но и это не супер

      if ( not flag ) — четко видно

  7. Спасибо автору, открыта важная тема общей методологии построения систем на микроконтроллерах, а то часто на практике получается так, что «за деревьями не видно леса». Сейчас читаю книгу «Embedded multitasking with small microcontrollers,» by Keith Curtis, в которой как раз рассматриваются эти вопросы, от составления ТЗ до тестирования готовых изделий, причем основной идеей является многозадачность за счет использования диспетчера и набора конечных автоматов, т.е. без вытесняющих и кооперативных RTOS («третий» путь). Насколько я понимаю, это практически то же самое, что SWITCH-технология программирования (www.kit-e.ru/articles/circuit/2006_11_164.php).

    С интересом буду наблюдать за развитием темы.

  8. Кстати кроме планирования задач есть ещё одна важная тема по структуре — как писать код, так чтобы он был понятен и его можно было легко изменить. К этому относится, например, разбиение на функции(вместо copy-paste), вынесение констант, разбиение программы на части с абстрактными интерфесом между ними, комментарии(если программу пишут несколько человек), разбиение на несколько файлов, и т.п. Без этого большую программу написать довольно проблематично, так как рано или поздно оказывается что, для того чтобы сделать ещё что-то легче переписать программу заново чем исправлять существующую.
    Так что вот ещё одна тема для статьи.

    1. Тут все это может вылезти боком. Т.к. ресурсы МК ограничены сильно. Так что эффективность кода превыше читабельности и расширяемости. Но задел на это брать нужно и искать золотую середину.

      1. Сейчас скажу банальность:
        Зависит от задачи, то есть насколько она одноразовая.
        Если предвидится поддержка и модификация, то лучше подобрать железку с ресурсами побольше.

      2. При напрсании одноразовой программы на контроллеры с 2-4КБ памяти да, можно писать как угодно, а вот, например, на Mega16 уже надо писать как следует иначе достаточно быстро окажется, что память уже кончилась(а чтобы расчистить надо переписать заново пол программы) а нужно сделать ещё больше половины. А потом ещё потребуется добавить что-нибудь, что так же закончится переписыванием с нуля приличной части программы.

        1. И кстати отделение планировщика задач от самих задач тоже частный случай разбиения программы на части — планировщик становится отдельной частью, которая не зависит от того, какие именно задачи выполняются.
          Так же к таким вещам относится выделение кода, работающего с переферийными устройствами(UARD, жк-индикатор и т.д.) в отдельные функции, а затем — в модули со своим интерфейсом.

  9. блин, хоть кто-то понял важность обьяснения архитектуры программ.

    Ди, ты читал книгу Э. Таненбаум «Операционные системы. Разработка и реализация»? Если нет, то почитай, там много интересного именно про организацию управления задачами на железе.

    1. А еще хорошо посмотреть Керниган, Пайк «Практика программирования». Очень рекомендую. Художественная литература по программированию.

      1. Насчёт «художественно о проблемах программирования» — мне нравится «Психбольница в руках пациентов» Алана Купера.

  10. > Самое интересное, что правильной организации программы учат программистов в ВУЗах

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

  11. Можно я тоже свои 5 копеек вставлю?
    Идея красивая, реализация не очень.

    Допустим у нас N таймеров.
    1. В прерывании мы обходим все N таймеров, т.е. сложность прерывания у нас O(N)
    2. В SetTimer() в худшем случае тоже обходим N раз, тоже O(N)
    А теперь вопрос: нафига в SetTimer искать свободное место под таймер, если его номер
    уже известен? Более того можно завести enumи всё будет вообще зашибись.

    Код (компилятора под рукой нет, просьба не придираться):

    enum Actions{aLedOn,aLedOff,Max_Numbers_of_Timer};

    volatile static int SoftTimer[Max_Numbers_of_Timer];

    ISR(Timer1)
    {
    for(int i=0;i<Max_Numbers_of_Timer;i++)
    {
    if(!SoftTimer[i])continue;
    —SoftTimer[i];
    if(!SoftTimer[i])
    flags|=1<<i;
    }

    void SetTimer(Actions timer,char timeout)
    {
    InterruptDisable(); // Çàïðåùàåì ïðåðûâàíèÿ. Ïîìíèì îá àòîìàðíîì äîñòóïå!
    SoftTimer[timer]=timeout;
    InterruptRestore(); // Âîññòàíàâëèâàåì ïðåðûâàíèÿ êàê áûëî.
    }

    void LED_ON(void)
    {
    SET_BIT_LED(); // Ñîáñòâåííî, çàæãëè ÷òî òî òàì
    SetTimer(aLedOff,1000); // Ïîñòàâèòü ôëàã íà ïîãàøåíèå ÷åðåç 1ñ
    }

    void LED_OFF(void)
    {
    CLR_BIT_LED(); // Ñîáñòâåííî, ïîãàñèëè ÷òî òî òàì
    SetTimer(aLedOn,1000); // Ïîñòàâèòü ôëàã íà çàææåíèå ÷åðåç 1ñ
    }

    void main()
    {
    while(1)
    {
    for(int i=0;i<flags_count;i++)
    {
    if(!(flags&(1<<i)))continue;
    flags&=~(1<<i);
    switch(i)
    {
    case aLedOn:
    LED_ON();
    break;
    case aLedOff:
    LED_OFF();
    break;
    }
    }
    }
    }

    Что мы получили?
    SetTimer() за O(1)
    Прерывание по прежнему O(N) но код стал гораздо проще.
    O(N) тоже жирновато, но избавится от неё не так просто увы.

    А вообще идея прикольная.
    Жду след. статью.

    1. «А теперь вопрос: нафига в SetTimer искать свободное место под таймер, если его номер
      уже известен?»

      А с какой радости его номер уже известен? Он ведь динмически может плавать по массиву. Допустим у нас первым стал таймер на 10, вторым таймер на 20. Первый таймер выщелкал и перед 20ым возникла пустота которую надо найти и заполнить.

        1. Тогда можно выделять меньше памяти на большее число таймеров. Главное следить за тем, чтобы все таймеры одновременно не лезли. Я обычно прикидываю циклограмму распределения таймерного времени и ставлю очередь таймеров такой, чтобы хватало какраз. В итоге имею, допустим, 10 программных таймеров, а места выделено у меня для 6. Реальная экономия памяти коей очень уж мало.

          1. Тогда можно и прикинуть, какие таймеры никогда не пересекаются (взаимоисключающи), и тупо назначать им одинаковый фиксированный индекс.

            Хотя в динамике теперь вижу резон, ага. При вероятностном подходе. Я-то за детерминированный ратую.

          2. «Тогда можно выделять меньше памяти на большее число таймеров.»
            ————————
            Весьма спорное утверждение. Один таймер занимант у меня обычно 1 байт (хотя бывают и 2х байтовые, но реже). Система их динамического распределения скушает как минимум десятки байт. Это сколько же нужно задействовать в программе таймеров, чтобы получить экономию? Да еще и по времени обработки в прерывании проигрываем, и при их проверке.

          3. Прежде чем следовать за автором, задайте себе следущие вопросы.

            Вопрос 1: У вас полноценна RTOS, у которой количество запущенных процессов может меняться или у вас просто сложный автомат?

            Допустим у вас RTOS, и вам заранее неизвестно какое количество таймеров понадобиться одновременно. В какой-то задаче вы запускаете очередной таймер.

            Вопрос 2: Что будем делать (что будет делать прога) в случае отсутствия свободного слота для установки этого таймера?

            void SetTimer(NewNumber,NewTime)
            {

            // тут можно сделать return c кодом ошибки — нет свободных таймеров
            }

            Автору идеи — будьте последовательны, идите до конца в объяснении своей теории. Множество белых пятен играют против Вас.

            Вопрос 3: Может-ли в принципе возникнуть ситуация и есть-ли 100% уверенность в том, что никогда не понадобится одновременно запустить _все таймеры_? Чем это гарантируется?

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

            Автору спасибо за стать. Статья проблемная (в смысле поднимает вопросы насущного программописания), загружает коллективный разум работой.

            1. Забыл добавить, что у сложного автомата — тем более не имеет никакого смысла динамически распределять таймеры по слотам.

              Это тот случай, когда чем проще алгоритм, тем надежнее работа проги.

              И еще. Мы сейчас рассматриваем не решения какие-то уникальных задач, а общие задачи, Задачи повседневного программирования. Таким образом, проги, которые используют с 10 таймеров, обычно реализуются в МК с несколькими килобайтами флеша и оперативы от килограмма. Т.е. ресурсов более чем достаточно. Да, вы израсходуете больше флеша на крутых алгоритмах, а оперативу — на стеках вызовов и локальных переменных!

              Хотя, чисто с теоретической точки зрения размять мозги — весьма и весьма приятно :)
              Практически же ценности я не вижу. Извините :)

      1. Во, точно. Думал что я забыл добавить в перечень возможных архитекутр. Конечные автоматы! Тема интересная, но слишком уж сильно приходится перестраивать мышление. Хотя мне, после PLC, думаю будет не сложно =) Надо покурить. Спасибо за ссылочку — пару месяцев назад искал инфу по этим автоматам и находил только какие то обрывки, а тут прям все и сразу.

        1. PLC очень близко, ага.
          И вообще, Ladder-схемы куда больший выверт мозгов.

          А табличный КА даже без микроконтроллера делается — ПЗУ плюс регистр-защёлка (ну и тактовый генератор нужен).

          1. Ladder? да там же все просто! Это же реле! =))))))

            Обожаю эту логику :) Особенно полюбил после того как просек основной прицнип — идти от противного.

            Т.е. не «сначала а, потом б, потом с, а потом в сочетании абс мы включаем двигатель». А «включаем двигтаель! А потом обвешиваешь его ограничивающими условиямии если А и если С и если В. Получается сверх компактно, с минимальным числом промежуточных «реле». Правда за такое программирование меня препод называл империалистическим вредителем, т.к. мой код работал безупречно, но как он работает он понять так и не смог.

            1. Ну, реализовать комбинаторную схему на релейке — действительно просто.
              Кстати, для оптимизации рулят карты Карно.

              А вот когда выходы зависят не только от текущего состояния входов, но и от предистории — вот тогда становится весело…
              Statechart сильно проще, чесслово.

              1. Ну не только. У нас были программы управления всякими автоматами. Вроде как «вращающийся по положениям датчиков стол, дрель с тремя видами сверел и три вида отверстий разной глубины» и вот это мы кодили. Т.е. надо было выбрать инструмент, просверлить, поменять инструмент, просверлить, повернуть стол, определить тип детали, на основании этого вычислить каким инструментом ее сверлить. В Общем, мозг там убить было нефиг делать :)

                1. Ну а я про что? Убивание мозга методом его выворачивания :)

                  То бишь, после этого тебе автоматы семечками будут.

                  Вот где можно вообще «офигительно расширить сознание» — так это Пролог, только я не представляю его применение в МК. И ведь он, в отличие от BrainFuck’а и фунгиноидов, таки где-то применяется и изучается в ВУЗах!

        2. Могу и еще ссылок подкинуть.
          http://authorit.ru/HTML/dd_prog/dd_prog.htm Автоматное программирование для начинающих
          http://rsdn.ru/article/patterns/Protocols.xml Реализация систем, управляемых событиями. Использование конечных автоматов
          http://matlab.exponenta.ru/stateflow/book1/3.php Stateflow 5. Руководство пользователя
          http://itc.ua/node/19921/

          Ну и банальные
          http://ru.wikipedia.org/wiki/Конечный_автомат
          http://ru.wikipedia.org/wiki/Автоматное_программирование
          http://ru.wikipedia.org/wiki/Switch-технология
          — с них можно по ссылкам побегать.

          Также можно гуглить «Switch-технология» и «КА-технология», а также «исполняемый UML», «явное выделение состояний»

          Есть инструменты, позволяющие программировать, рисуя диаграммы состояний — например, IAR visualSTATE http://www.iar.se/website1/1.0.1.0/371/1/index.php? (я его не пробовал)

          1. О биг сенкс!!!

            Сейчас мельком нарвался из этого списка на несколько очень доходчивых статей. Потом повнимательней изучу.

            Как я понял, при автоматном подходе наша задача разбивается на стадии. Т.е, например, стадия ожидания кнопки старт, стадия ожидания сработки датчика, стадия ожидания сработки другого датчика. Стадия ожидания еще чего то там. В результате, вся программа разваливается на десяток программок поменьше. Все это пихается в кейсы свитча и по выполнении каждой из подрограммок она, по результатам своего анализа, перекидывает всю конструкцию на другую стадию (подпрограммку)? Так? А весь свитч крутится непрерывно и просто каждый раз, при каждой итерации, мы по его ключу вваливаемся в нужный участо кода (стадию) и не выпадем оттуда до тех пор пока ее условие не позволит изменить стадию.

            Вроде все просто :)

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

    1. Флаговый автомат — это не подобие конечного автомата, а его частный случай. Тот же граф состояний присутствует, просто выглядит немного иначе.

      1. Я это и имел в виду. Случай-то не то что частный, а вообще ж практически вырожденный.
        Хотел вот только выразиться похудожественнее — ну не вышло.

        1. Да хоть горшком назови… Одни обзывают, другие делают. Я еще в 80х годах на контроллерах с весьма ограниченными ресурсами (580ВМ80 или 1816ВЕ35) делал многозадачную обработку в реальном времени и с весьма жесткими допусками на времена, порядка десятков микросекунд на канал при одновременной дуплексной работе 5 каналов, с измерениями, выдачей статистики по часам и календарю, управлением с консоли(телетайп). Программы занимали 2-3 килобайта (ПЗУ ставил максимум на 4Кб, ОЗУ до 2кб, а то и без него). На этом самом флаговом автомате с программными таймерами. Это вам не «Hello World» на ЖКИ на Си вывести, и получить код в несколько килобайт.

          1. Эмн. Это к чему?
            Я-то имел в виду, что список неполон без упоминания конечных автоматов, частным случаем которого является флаговый автомат.

            И холиварить «флаги vs состояния» не стоит так же, как и «asm vs C». Всему своё место. Выигрываем в эффективности и ресурсоёмкости — проигрываем в сопровождаемости, а зачастую и в надёжности.
            Да, в 80-е проблема ресурсов стояла в полный рост. Сейчас это менее важно.
            Задача нехилая, уважаю. Немного напоминает известную «Историю одного байта». Но это уже искусство, а не производственный процесс.

            Что же касается меряния — у меня стаж, конечно, существенно меньше — в конце 80-х я еще, так сказать, пешком под стол ходил — с БЗ-33 игрался и Бейсик с Фокалом изучал. Зато сейчас могу похвастаться, что мои автоматные программы в половине случаев начинают работать сразу, без отладки, а в другой половине — отладка требуется минимальная, никаких «подземных стуков».

  12. В последнем решении резануло глаз отсутствие «чистоты» процедур. ИМХО лучше все подобные функции называть особым образом (event_blah, например) и возвращать ими битовую маску, которую уже ксорить с текущим состоянием флагов. Плюс в том, что функции становятся «сферическими в вакууме», т.е. никак не влияют на окружающий их код и их смело можно копировать из другого проекта. Сами битовые маски можно брать препроцессором из конфига на стадии компиляции, а то и просто раздавать на стадии компиляции.

  13. я начинающий покоритель МК… и пока только вижу, что в первом примере супер цикла мигание диода будет незаметно на глаз… если только KeyScan() не будет тратить 1000 мсек. на обработку…

  14. Ещё о таймерах, я пока сильно не копал, но подозреваю, что есть способ избавится от O(N) и в прерывании и при добавлении таймера. Сам я использовал уже использовал другую схему, в прерывании O(1) а при добавлении O(N) в худшем случае, но может быть даже O(1) или O(2). Как это делается? массив таймеров хранится во всегда отсортированном виде, а оставшееся до срабатывания время хранится разностно, то есть относительно предыдущего (это который сработает позже) таймера. Что надо делать в прерывании, уменьшаем значение верхнего (первого) таймера в очереди и если он достиг 0 дергаем соответствующий таймер, и удаляем первый элемент из очереди. Теперь в начале очереди находится другой таймер, и его значение — это время оставшееся до его срабатывания. С добавлением сложнее, надо от начала очереди просматривать все записи при этом корректируя значение добавляемого, добавить его после того как значение добавляемого стало меньше просматриваемого, и после ещё убавить значение просматриваемого на значение добавляемого. Мутно описал, но думаю самим додумать интереснее будет. Поделитесь лучшим вариантом если знаете.

    1. «Что надо делать в прерывании, уменьшаем значение верхнего (первого) таймера в очереди и если он достиг 0 дергаем соответствующий таймер, и удаляем первый элемент из очереди. Теперь в начале очереди находится другой таймер, и его значение — это время оставшееся до его срабатывания. С добавлением сложнее, надо от начала очереди просматривать все записи при этом корректируя значение добавляемого, добавить его после того как значение добавляемого стало меньше просматриваемого, и после ещё убавить значение просматриваемого на значение добавляемого. »
      ——————————————
      И во сколько десятков, а скорее — сотен команд процессора это выльется? Да еще и прерывании, где каждая микросекунда на вес золота…

      1. Это делается только при добавлении таймера. В прерывании же инкрементируется лишь один из таймеров — старший. Остальные за ним гуськом.

      2. В прерывании все как раз хорошо, в большинстве случаем будет только декремент одной переменной и проверка Z флага, при срабатывании таймера, переставить один указатель на новый элемент, это тоже операция не долгая. У добавления в эту очередь есть ещё полезная особенность, часто вызываемые таймеры будут добавляться в начало и поэтому быстрее. И хотя добавление таймера тут очень тяжелая операция, я думаю это можно улучшить.

      3. В прерывании все как раз хорошо, в большинстве случаев будет только декремент одной переменной и проверка Z флага, при срабатывании таймера, переставить один указатель на новый элемент, это тоже операция не долгая. У добавления в эту очередь есть ещё полезная особенность, часто вызываемые таймеры будут добавляться в начало и поэтому быстрее. И хотя добавление таймера тут очень тяжелая операция, я думаю это можно улучшить.

    2. O(2) = О(1) = О(С). Важен порядок величины, а не константа.
      Где используется двусвязность списка? По-моему можно обойтись и односвязным.
      Не факт, что так будет быстрее на коротких списках. Тут надо смотреть асмовый код (и/или дождаться DIHALT’а), насколько длиннее будет джамп по адресу в списке (плюс весь сопутствующий код для модификации), чем просто увеличение указателя на 1.
      А вообще решение классное, спасибо, запомню.

      1. Да действительно, 1 2 это лишь частные случаи, некорректно написал. Про список, пересмотрел свой код, не используется, путаю с другим местом в этом же коде.

    3. Если отсортированный список хранить в виде массива, то добавление и удаление будет очень долгим и это полностью ликвидирует выигрыш в скорости. Если же связным списком(например внутри массива), то нужно хранить адрес следующего таймера, а выигрыша в размере переменной для хранения времени выигрыша не будет(да и выигрыш в скорости для десятка таймеров небольшой), так что теряем в объёме памяти.
      Хотя в случае варианта с большим количеством таймеров такая структура возможно имеет приемущество, но на контроллерах с небольшим количеством памяти очень много бывает редко, а на мощных уже используется нормальная ОС вплоть до Linux.

      И в случае небольшого количества объектов опрелелить что-нибудь O(N) и т.п. нельзя — нужно учитывать коэффициент и младшие порядки величин. Например, что лучше алгоритм с O(2^N) или O(1), полученный из предыдущего добавлением задержки, которая становтся меньше при увеличении N?

      Кроме того если вспомнить математику, то становится понятно, что O(1) и O(2) — одно и то же(то есть при больших N сложность постоянна).

      1. При небольшом N будет выигрывать более простой способ, это понятно.

        Так, а у DI HALT что то я посмотрел SetTimer тоже не особо быстро реализован, можно же это сделать за время не зависящее от N, в прерывании при этом останется проход по всем N.

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

        И ещё, в обоих случаях надо следить за тем, что может произойти если прерывание произойдет в момент выполнения SetTimer.

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

  16. если мне необходимо реализовать некое подобие очереди (задания из очереди отсылают короткие пакеты данных (до 20байт) в USART).
    Обработчик очереди лучше оставлять в суперцикле или наоборот обработку сделать по таймеру?

    задания в очередь добавляються по ходу работы основной программы…

  17. О примере с програмным таймером:
    SetTimer(OFF_LED_FLAG,1000); // Поставить флаг на погашение через 1с
    Установка таймера на 1000 приведет к его срабатыванию примерно через 1001 мс., и к ошибке в -1 мигание за 42 минуты работы устройства :)
    Установка таймера на 999 даст более точный результат.

  18. функция «SetTimer»
    1) Комментарий к первому «for» надо подправить на: «Ищем не завершенный таймер с тем же номером».
    2) Во втором «for» вместо всех «MainTimer» наверное надо «SoftTimer».
    3) В каждом «for» перед «return» надо поставить «InterruptRestore()».

      1. функция “SetTimer”, комментарий к первому “for”
        вместо: “//Прочесываем очередь таймеров. Ищем пустой”
        надо что-то типа: “… Ищем запущенный”

    1. А что тут: GM_RTOS1.inc

      taim_TC0: outi tcnt0, 0x00;
      . . .

      end_program_C: RETI;

      В прерывании нет сохранения флагов и используемых регистров в стеке. Проге снесет крышу очень быстро. Причем я бы не рекомендовал использовать тут макросы Load/Save что у тебя там есть — слишком громоздко для прерывания.

      Почему у тебя в Flag:
      разрешение прерываний идет только в конце? По идее запрещать прервания вообще нельзя. А тут их блочит на все время выполнения активной фазы. А там очень много чего может быть. Так что события ты прохлопаешь ушами, а это fail. Вообще там еще бы не помешал WDR и задача Idle куда потом можно впихать sleep mode например.

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

  19. GM_RTOS1.inc тут просто макросы (половину из них надо переделать, а другую удалить, я их просто таскаю из проекта в проект и все руки ни как не дойдут, а мусор накапливается и накапливается ….), вставил потому что была поздняя ночь и сил бы мне не хватило бы выдергивать используемые макросы и defы.

    taim_TC0: outi tcnt0, 0×00 — это сброс счетчика что бы «правильно считал», равномерно.

    end_program_C: RETI — а это типа островка, т.к. во первых косанда BRNE переносит только на 64 команды РС вверх вниз, а во вторых кампилятор ругался на «BRNE RETI» т.к. ему надо было «BRNE МЕТКА», поэтому метка у меня и стоит.

    Load/Save у меня вообще не используется, да я и не помню зачем этот макрос мне был нужен, это мусор который я никак не удалю. В проге использовалось не более 5 макросов, да и вставил я что бы людям было понятно что такое ldi_OZY например.

    Действительно SEI стоит у меня в конце, задачей этой программы было мигать с разной частотой диодиками на пинбоорде с разной частотой одновременно, (что бы на осциле было видно практически одновременное выполнение), передач по SPI и прочее не задумывалось, тем более я не бил гейтс и кросплатформенность тоже отношения к проге не имеет.

    До sleep mode в вашем курсе я не добрался. Курс я прохожу время от времени т.к. много другой работы а Ваш курс это хобби типа увлечения.
    Что касается Си все никак до него не доберусь, хотелось бы закончить сперва с асмом, что бы полностью весь курс пройти на асме (в последнее время он стал на столько близким и родным) что вроде как и привык к нему, но я прекрасно понимаю что Си тоже нужен.

    Прога была сделана для того что бы помигать диодиками с разной частотой (практически одновременно), а сюда выложил для того что бы может быть для кого то была понятна общая концепция флагового автомата на асме. (Сам я писал прогу не из Си, а из соходя из смысла статьи уважаемого автора). (Т.е. все что было написано на Си в статье, для меня была как китайская клинопись, разве что на коменты, что то поясняли), ну и написал прогу как понял по общему смыслу, пробовал вчера ночью работает. Указатель стека да и сам стек под контроль не ставил, написал для пробы, так по быстрому (заняло где то от 45 до 60 минут). А так если по нормальному делать, то я бы сделал уже с адресацией по типу icall, т.к. перебирать флаги мне кажется долго будет тем более если их еще там накопиться много. Да и проще мне будет по принципу icall.

    А что такое WDR? Idle?

    А так на сегодняшнее утро созрела мысль сделать прогу которая переходила на задачи по косвенному переходу, где данные приходили бы с «кольцевого буфера» (я не помню где, но где то я слышал комбинацию слов «кольцевой буфер», но честно сказать я не знаю что это значит), так вот с «кольцевого буфера» буду приходить адреса переходов на задачи, где минимальной задачей должен быть холостой ход (NOP), + к этому кольцевому буферу состоящему из ограниченного количества адресов переходов (ячеек) должен быть сортировщик по приоритетам (от 1 до 5 уровней приоритета) и времени на выполнение (что то типа программного счетчика с сортировкой). Ну и другую кучу примочек собираюсь пихнуть, а все это оформить типа «контейнеров» (библиотек), которые при необходимости можно будет подключать/отключать в зависимости от задач решаемых прогой. Не исключено, что к сегодняшнему вечеру я передумаю, и решу что надо делать иначе. Для меня программирование стало заменой шахматам (больше вариантов, т.к. шахматы мне стали больше напоминать крестики нолики, где после второго хода понятно какой будет результат игры), а здесь (программировании) можно творить, что бы было все симметрично, что бы не было ничего лишнего, так что бы не возникало желания, что либо добавить или убавить и т.д. и т.п.
    P/S: пока такой «идеальной программы» мне не удавалось не разу создать, даже близко пока еще не подобрался. Но все еще я так думаю впереди.

  20. Всем привет. А можно рабочий пример флагового автомата + софтварный таймер? Просто на пути к диспетчеру хотелось бы покапать что-то попроще. В общем,принцип (ф. Автомата) понятен,но с реализацией у меня полная шняга получается .

    1. А я там в конце ссылки пришпилил на статьи Шалыто, Вот там и таймер и еще одна реализация более сложная.

  21. Как лучше организовать программный таймер, если нужны отсчеты в мс и мкс — использовать два аппаратных таймера или один? Для МК не слишком затруднительно будет прочесывать очередь и выполнять декремент каждую мкс? Или можно это делать как-то только для тех таймеров, которым нужна такая точность?

  22. Пишу программы для AVR и STM32.
    DI HALT, подскажи пожайлуста, в каких книгах можно обо всем этом (организации программ) почитать (суперцикл, флаговый автомат и т.д.) . То есть интерисуют первоисточники.

    1. Да нет какого либо одного первоисточника. Личный опыт да общая инфосфера.

      Рекомендую вам почитать труды Шалыто по конечным автоматам. Таненнбаума про операционные системы. Документацию про разные rtos особенно хороша у сальвы.

  23. Извините, у меня такой вопрос начинающего, я перерыл некоторое количество И-нета, но ответа не нашёл. Помогите, пожалуйста.
    Создаю битовое поле типа:
    struct { unsigned a1 : 1;
    unsigned a2 : 1;
    unsigned a3 : 1;
    unsigned a4 : 1; } Flag;
    Но в обработчике прерываний эта строка не работает:
    Flag |= 1<<Number ; // Дощелкали до нуля? Взводим флаг в флаговом байте
    AVRStudio пишет — invalid operands to binary | (have ‘struct ‘ and ‘int’)

    1. Добрый день!
      Я решил эту проблему вот так:
      unsigned char *uk_Flag = &Flag;
      . . .
      *uk_Flag |= 1<<i;
      (объявил указатель на область памяти, где располагается структура)

      Только компилятор выдаёт предупреждение. Как это реализовать без предупреждений я не знаю… Может быть, у кого-то найдутся другие варианты?

  24. Привет, коллеги.
    Балуюсь светодиодами. Планируется что-то типа бегущей строки. Пишу на ассемблере. Естественно применяется динамическая индикация.

    Данные в памяти идут друг за другом. Каждый «кадр», занимает 8 байт, дальше следующий кадр. При динамической индикации, последовательно читаются 8 адресов, и указатель ZH и ZL устанавливается на начало кадра. И вот в одном месте затупил: как установить указатели на n-кадр. Видится такая структура
    LDI ZL,low(mult*2+8*clock) ; установить начало кадра
    Как применить такую структуру? Вместо clock необходимо постоянно менять значения. Обычные регистры эта команда не пропускает.
    Спасибо

    1. Конечно не получится. Ассемблер же это не язык программирования а тупо комманды и ты не можешь менять Clock как тебе вздумается, т.к. препроцессор это не сможет превратить больше чем в одну команду.

      Как делаем:
      Берем клок, кладем в регистр, умножаем на 8 двойным сдвигом влево.

      Берем кладем в Z твой mult*2.

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

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

      LDI ZL,low(mult*2+8*1)

      вывод

      LDI ZL,low(mult*2+8*2)

      вывод


      LDI ZL,low(mult*2+8*8)

      Будет проще и возможно компактней.

      1. Привет DI. Все получилось. Готов поделится для всеобщего просмотра своим вариантом «автомата световых эффектов» :). Пока только проект в протеусе.

  25. Очень полезная статья! Уважаемый DI HALT могли бы вы рассказать о заголовочных файлах и составления программы из разных отдельно компилируемых модулях?

    1. Да и без меня понаписано. Читайте мануалы по языку Си, там это все очень подробно расписано.

      1. По моему Суперцикл+прерывания — это вторая стадия микроконтроллерной шизофрении. Пытаешься пальцами переместить значение из стека по нужному адресу в главном цикле.
        Хорошая статья =) А это прям моя стадия

    2. А что Вас конкретно интересует ?

      в заголовочный файл выносятся именования ф-ций и некие глобальные параметры

      модуль — это объектный файл, компилирующийся из исходного *.с файла

      далее ликовщик может это собрать в исполняемый файл

      ну и все это счастье компилируется под Вашу архитектуру

      Это крайне коротко

  26. вопрос по структурам:

    SetTimer(OFF_LED_FLAG,1000); // Поставить флаг на погашение через 1с

    как объявлять OFF_LED_FLAG, в соответствии с положением флага в структуре?

    Т.е. создаем битовое поле:

    struct { unsigned debug : 1; //0
    unsigned test : 1; //1
    unsigned led_on : 1; //2
    unsigned led_off : 1; //3
    } flags;

    И после этого отдельным дефайном прописываем:

    #define OFF_LED_FLAG 2

    а после этого уже вызываем

    SetTimer(OFF_LED_FLAG,1000);

    так?

    1. Чет ты куда то не туда полез. Если касается речь моей таймерной службы, то вот так SetTimer(OFF_LED_FLAG,1000) у тебя нихуя не выйдет. Т.к. OFF_LED_FLAG должен быть адресом функции. А вот уже в ней делай что хочешь.

      1. > у тебя нихуя не выйдет
        уже понял

        > OFF_LED_FLAG должен быть адресом функции.
        курить в сторону указателей?

        > Если касается речь моей таймерной службы
        речь касается последних двух блоков кода в статье — приведи пример инициализации OFF_LED_FLAG и ON_LED_FLAG, плз…

        1. Нет, ещё проще. Ты пишешь функцию типа
          Void offledflag (void)
          {
          Тут гасим лед
          }

          И Ее имя указываешь в set timer. И когда придёт время он эту функцию выполнит.

          1. Эт ты уже про диспетчер вроде рассказываешь) Потому что иначе какая-то херня получается (или я не выспался…):

            Void offledflag (void)
            {
            Тут гасим лед
            }

            дальше из функции
            void SetTimer(NewNumber,NewTime)
            ——-

            if (SoftTimer[i].Number == 255)
            {
            SoftTimer[i].Number = NewNumber; // Заполняем поле флага

            ————

            не уверен, что в NewNumber (если это адрес функции) будет содержаться число от 0 до 254.

            1. Да. Это я прогнал. Я же не вижу к какому тексту коммент в админке. ДУмал речь о диспетчере… Короче, смотри. У таймера есть время и задержка. Ну и флаг соответственно.

              Т.е. мы, например, говорим, что led_on это 1 таймер. led_off это 2 таймер. Просто задефайнили имена.

              Дальше, SetTimer(OFF_LED_FLAG,1000); просто через 1000тиков поднимет флаг в спец регистре. И все, а главный цикл у тебя будет состоять тупо из такого кода

              while(1)
              {
              if(flag.timer_off) offled();
              if(flag.timer_on) onled();
              if(flag.something) something();
              }

              Ну и где то прописаны функции все эти каждая делает что то свое. В результате эта шняга гонится прогонами по кругу, а если флаг стоит — выполняется.

              1. Спасибо, но это как раз понятно :) моя не понял, как работает такая строка:
                —-
                flags |= 1<<Number
                —-

                ведь flags — это структура, как понимаю? и при такой операции компилятор ругается, мол "чего ты тут структуре присваиваешь, обращайся к ней как положено, блеат!" )

                1. Там же написано, псевдокод :)))) Специально везде это пишу ,чтобы не приябывались к синтаксису. Главное тут взвод флага. А как он делается совершенно насрать. Можно объявить битовое поле и обращаться к битикам, а ставит в этот же байт по маске вот таким способом. Придумай что-нибудь свое :)

Добавить комментарий

Ваш e-mail не будет опубликован.

Перед отправкой формы:
Human test by Not Captcha