• Announcements

    • admin

      Размещайте материалы своей компании БЕСПЛАТНО!   04/18/18

      Редакционная политика портала позволяет размещать на бесплатной основе различные типы материалов: интересную информацию, наработки, технические решения, аналитические статьи и т.д. Пример такого блога. Взамен мы рекламируем ваш блог в наших группах в соц. сетях, ну и плюс естественная самореклама от пользователей форума и блогов, которые будут читать ваш блог. К примеру охват одного поста только в нашей группе VK составляет более 10 тыс. человек. Т.е. мы предлагаем бартер - вы ведете у нас блог и публикуете какую-то полезную и интересную информацию связанную с вашим производством, а мы рекламируем ваш блог в наших соц. сетях. Блоги можно полностью кастомизировать: поставить изображение шапки, сделать меню или оглавление, также в своем блоге вы будете модератором - сможете удалять комментарии и т.д. Ведение своего блога требует времени и навыков, но рекламный эффект колоссальный, т.к. это живое общение и отклик. Посты не должны быть рекламой, а также должны соответствовать правилам форума. Для тех компаний, которые будут публиковать интересный контент, права в дальнейшем будут расширяться - сможете публиковать больше ссылок, пресс-релизы, новости компании, анонсы и т.д. Ну а если вы хотите размещать платную рекламу: условия и прайс размещения на сайте и форуме, коммерческая тема на форуме, реклама в группе VK.
Sign in to follow this  
  • entries
    6
  • comments
    8
  • views
    290

About this blog

Собственно, мотив создания этого блога лучше всего отражается вот этой классической цитатой: а чем я хуже?

Entries in this blog

ARV

Есть ли жизнь на Марсе, нет ли её там - науке это не известно. Наука пока не в курсе дела.

Есть ли жизнь в экосистеме AVR? Или эти мамонты уже вымерли, уступив более теплокровным ARM? 

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

На видео - небольшая (как кредитка) игрушечка, реализованная на attiny85... Напомню: всего 6 ног, 8К flash и 512 байт RAM. Вот так-то...

ARV

Продолжая свой полет, неожиданно сделал давно задуманную, да почему-то постоянно откладываемую на потом, штуку... А именно: параллельный опрос нескольких термодатчиков семейства DS18x20.

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

Последовательный опрос несколких датчиков, хоть по адресу хоть без увеличивет общее время опроса пропорционально количеству датчиков. В частности, в моих личных играх опрос 8 датчиков последовательно затягивался почти на 100 мс. С учетом того, что примерно половина этого времени будет требовать запрета прерываний (кусочками по 65 мкс примерно), такая длительность выглядит не очень хорошо.

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

Нет, если вы делаете для себя, то вы, конечно, можете адреса этих датчиков и вручную прописать в программу, и красочкой их пометить... А если это "на сторону" устройство? А если датчик в процессе эксплуатации выйдет из строя и потребует замены? Вот зачем пользователю все эти проблемы с сопоставлением адреса датчика и функцией устройства? С моей точки зрения - оно ему не нужно.

Альтернатива - повесить на один порт один датчик, а портов задействовать столько, сколько надо. Минус, конечно, есть, и не маленький - расход пинов микроконтроллера. Зато плюсов существенно больше. 

Во-первых, нет гемора с адресацией: любой датчик используется в режиме SKIP_ROM, потому как он единственный на своей линии. Достаточно к линии порта, ответственной за температуру на улице, подключить уличный датчик, и "соответствие" автоматически обеспечено.

Во-вторых, считывать информацию с датчиков, подключенных к одному порту, можно одновременно, т.е. по сравнению с "последовательным опросом" многократно быстрее!

Идея проста, как колумбово яйцо (правое): управляя не отдельным битом порта, а сразу всеми битами, формируются тайм-слоты чтения (при записи тоже ничто не препятствует, но это менее интересно, т.к. одновременная запись в "обычную" цепочку 1-wire датчиков возможна и так), и считывание битов из 8 линий происходит одновременно. Т.е. вместо 72 битов из одного датчика мы получаем 8х72 бита из 8-и датчиков. Накопив в отдельный массив эти 72 байта за время опроса ОДНОГО датчика, мы затем можем пройтись по этому массиву и выделить информацию каждого из восьми... Ну понятно же, что в 0-ом бите всех байтов массива будут биты из датчика с линии 0, в 1-ом бите - из датчика с линии 1 и т.д.

Поскольку обработка массива может вестись на предельной частоте микроконтроллера, длиться она будет крайне незначительное время, не смотря на кажущуюся громоздскость. В частности, в своих играх я получаю информацию с 8-и параллельно подключенных к одному порту датчиков за 12 мс (примерно) - ощутите разницу! Ровно (на самом деле нет) в 8 раз быстрее, чем традиционным способом.

Так что если интересуетесь многодатчиковыми системами контроля температуры - рекомендую.

ARV

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

Собственно вот что я сделал.

typedef uint16_t timer_sz_t;

/// тип функции таймера. если возвращает не ноль, то таймер продолжает работать.
/// в качестве параметра получает указатель на структуру timer_struct_t, т.е. на тот самый
/// экземпляр таймера, к которому привязана функция.
/// вызывается в "безопасном" режиме, т.е. при запрещенных прерываниях
/// (атомарно), поэтому из функции можно модифицировать значения полей таймера напрямую,
/// хотя для поля \b counter это делать не имеет смысла, т.к. это поле все равно может измениться после
/// завершения функции.
typedef bool (*timer_callback)(void *t);

/// тип структуры, описывающей таймер
typedef struct{
	timer_sz_t	counter;	//!< счетчик
	timer_sz_t	period;		//!< заданный период
	timer_callback	shot;	//!< таймерная функция
} timer_struct_t;

/// тип для создания экземпляра таймера
typedef volatile timer_struct_t timer_t;

/// внешняя ссылка, помечающая начало обалсти таймеров в ОЗУ
extern timer_t __timer_start;
/// внешняя ссылка, помечающая конец области таймеров в ОЗУ
extern timer_t __timer_end;

/// макрос определения таймера с заданным именем
/// @param t идентификатор экземпляра таймера
/// @param d период в мс (если не равен 0, то таймер немедленно стартует)
/// @param f функция (NULL или 0, если функция не требуется)
#define TIMER(t,d,f)	static timer_t __attribute__((used, section(".timer_sec"))) t = {.period = d, .counter = d, .shot = f}

/**
 * запуск/перезапуск таймера
 * @param tmr указатель на экземпляр таймера
 * @param duration период таймера в мс
 * @param callback указатель на функцию таймера
 * \note значение duration=0 фактически останавливает таймер
 */
void timer_start(timer_t *tmr, timer_sz_t duration, timer_callback callback);

/**
 * проверка таймаута
 * @param tmr указатель на экземпляр таймера
 * @return 1, если указанный таймер истек
 * @return 0, если таймер еще продолжает счет
 */
bool timeout(timer_t *tmr);

/**
 * остановка таймера
 * @param tmr указатель на экземпляр таймера
 */
void timer_stop(timer_t *tmr);

Это было содержимое заголовочного файла с описанием программных таймеров. Вроде ничего необычного... А вот так выглядит сам исходник этого модуля таймеров:

/*
 * эта функция вызывается каждый системный тик, т.е. каждую милисекунду
 */
static void Timer_Tick(void){
	// обрабатываем программные таймеры
	for(timer_t *tmr = &__timer_start; tmr != &__timer_end; tmr++){
		if((tmr->period) && (tmr->counter)){						// если задан период и счетчик не равен нулю
			tmr->counter--;								// уменьшаем счетчик
			if(!tmr->counter && (tmr->shot != NULL)){				// как только счетчик обнуляется,
				if(tmr->shot((void*)tmr)) tmr->counter = tmr->period;		// то если указана функция - вызываем её
			}									// и, если она вернула true, переустанавливаем счетчик заново
		}
	}
}

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wreturn-type"

bool timeout(timer_t *tmr){
	ATOMIC_BLOCK(ATOMIC_RESTORESTATE){
		return tmr->counter == 0;
	}
}

void timer_start(timer_t *tmr, timer_sz_t duration, timer_callback callback){
	ATOMIC_BLOCK(ATOMIC_RESTORESTATE){
		tmr->counter = duration;
		tmr->period = duration;
		tmr->shot = callback;
	}
}

void timer_stop(timer_t *tmr){
	ATOMIC_BLOCK(ATOMIC_RESTORESTATE){
		tmr->period = 0;
	}
}

#pragma GCC diagnostic pop

На что я тут хочу обратить внимание... На пару моментов.

1. Обратите внимание на директивы #pragma: начиная с какой-то версии avr-gcc появилась возможность временно запрещать компилятору выводить варнинги на некоторые особенности кода, которые на самом деле никакой проблемы не составляют. В частности, в этом коде формируется warning о том, что из функции может быть выход с неопределенным значением - это из-за return изнутри ATOMIC_BLOCK. Но в конкретном случае из ATOMIC_BLOCK не может быть иного варианта выхода, т.е. беспокоиться не о чем. И директива #pragma GCC diagnostic ignored "-Wreturn-type" отключает беспокойство компилятора... Удобно. При помощи этой директивы можно отключить многие warning-и (не любые, но многие) только для той части кода, где вы на 101% уверены в безопасности содеянного.

2. Обратите внимание на функцию Timer_Tick. В ней четко просматривается цикл перебора записей типа timer_t (или timer_struct_t, что почти то же самое), но в коде модуля нет никакого упоминания какого-либо массива этих структур! Что же перебирается в этом цикле? И именно в этом весь цимес!

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

Предположим, Вам надо, чтобы все время мигал светодиод на PORTB. В любом месте вашего исходника, где подключен заголовочный файл таймеров, вы пишите что-то типа такого:

// пусть светодиод будет на 1-ой линии порта
#define LED (1 << PB1)

// функция переключения состония светодиода
bool led_blink(void *t){
   RORTB ^= LED;
   return true;
}

TIMER(T_LED, 500, led_blink);

Всё! Светоидод замигал. Теперь вам приспичило, чтобы ожидание приема байта по USART было не бесконечным, а длилось, предположим, не больше 1 секунды. Делаете так:

TIMER(T_USART, 0, 0);

char get_usart_byte(void){
   timer_start(T_USART, 1000, 0);
   while(!timeout(T_USART) && bit_is_clear(UCSRB, RXC));
   return timeout(T_USART) ? 0 : UDR;
}

Я не пишу комментариев, т.к. мне представляется, что код полностью очевиден.

Но что же происходит на самом деле? Макросом TIMER мы определили структуры, описывающие тот или иной таймер, но как они попали в то место, которое обрабатывает Timer_Tick?! Ведь никакого массива (еще раз повторяю) нет! Что же перебирает цикл? Как вообще оказывается возможным "межмодульное" пополнение какого-то неявного списка-массива?!

А вы не задумывались, как компилятор собирает таблицу векторов? Ведь формально таблица векторов - это массив в памяти программ, но мы все привыкли, что содержимое таких массивов всегда указывается явно и в одном месте целиком!

// примерно так мы задаем массивы во FLASH
const __flash char str[] = "СТРОКА"; // текстовая строка во FLASH
const __flash int buf[5] = {1,2,3,4,5}; // массив из 5-и int-ов во FLASH

// в древних версиях avr-gcc (WinAVR) это было так
PROGMEM char str[] = "СТРОКА";
PROGMEM int buf[5] = {1,2,3,4,5};

И никгда и никак нельзя было сделать так, чтобы в одном модуле мы определили массив, а в 5-и разных модулях затем определили по одному из его 5 элементов! Но ведь таблица векторов прерываний как-то заполняется компилятором именно так, т.е. в любо модуле при помощи макроса ISR мы легко можем задать значение одного из элементов этой таблицы! Как компилятор это делает?!

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

А никак он это не делает.

Это делает линкер, а не компилятор. Компилятору мы лишь указываем, что тот или иной элемент должен быть помещен в определенную секцию памяти, а уж линкер затем все эти элементы помещает в эту самую секцию. Будь у вас хоть 1000 файлов в проекте, из всех этих файлов элементы одной и той же секции будут размещены рядышком последовательно - чем не массив?! Да ничем! Это и есть массив: упорядоченное последовательное размещение в памяти однотипных элементов данных.

Секции же могут быть стандартными и пользовательскими. О том, что такое стандартные секции, читайте в документации на avr-gcc, благо, она есть на всех популярных в нашей стране языках. Наполнение стандартных секций линкер делает при помощи стандартного скрипта. А вот чтобы линкер правильно обработал пользовательские секции (в частности, нашу секцию .timer_sec (см. первую врезку кода), надо вручную подправить этот скрипт.

Стандартные скрипты для avr-gcc находятся в папке avr\lib\ldscripts в папке тулчейна. Можно взять подходящий, поместить его в папку своего проекта, подкорректировать, как надо (ниже покажу, как), и в опциях линкера в makefile дописать директиву закгрузки этого скрипта. Например, наш скрипт называется avr5.x (подходит для "больших" атмег - с памятью 128К, например). Подправленная версия, лежащая в папке нашего проекта, будет иметь название avr5_mod.x, тогда в makefile надо дописать строку LDFLAGS += -T c:\My_prj\avr5_mod.x, и все.

Ну, а тепрь главное, что же писать в скрипте? А вот что. Сначала найдите место, с которого описывается стандартная секция .data (статические переменные в ОЗУ), а затем допишите в эту секцию команды для добавления нашей секции .timer_sec, а так же заодно определение двух символов __timer_start и __timer_end:

  .data          :
  {
     PROVIDE (__data_start = .) ;
    *(.data)
     *(.data*)
     /* ++++++++++++++++++++++++++++ */
     PROVIDE (__timer_start = .) ;
     *(.timer_sec)
     *(.timer_sec*)
     KEEP (*(.timer_sec*))
     PROVIDE (__timer_end = .) ;
     /* ++++++++++++++++++++++++++++ */
    *(.rodata)  /* We need to include .rodata here if gcc is used */

Вот так должна выглядеть у вас эта часть файла - остальное не трогайте! Только добавьте то, что между плюсиками.

Теперь при сборке вашего проекта линкер поместит все структуры timer_t, где бы они ни были определены при помощи макроса TIMER, в одно место, поместит адрес начала этой области (т.е. адрес первой структуры) в символ __timer_start, а адрес следующего за полсденей структурой байта - в __timer_end (а эти два символа, как видно по второй врезке кода, и используются в цикле перебора программных таймеров). Понятно?

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

Ах, да! Главное: Timer_Tick вы должны вызывать из обработчика прерывания настоящего аппаратного таймера. В моих примерах подразумевается, что прерывание это возникает каждую миллисекунду, но для многих применений это слишком часто. На практике вполне можно удовлетвориться и 10-миллисекундынми интервалами.

Надеюсь, эта идея вам понравится, как и мне. Кстати, на таком же принципе очень удобно делать всякие парсеры строк, ну то есть когда вам надо в зависимости от того или иного текста в строке выполнть ту или иную функцию. Если делать все в одном файле - он будет дико объёмным, кто не верит - посмотрите в исходники какого-либо бейсика. Разобраться в таком файле сложно... А применив описанный принцип, т.е. совместив на уровне скрипта определенные в разных модулях элементы общего массива, можно получить очень компактный и понятный код.

До встречи на процедурах!

ARV

В моём гнезде прибавление.

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

Преамбула.

Что мы понимаем под понятием "таймер"? Ну, не в смысле задатчика времени варки яиц всмятку, а в программировании? Это некая функция, которая "сама по себе" выполняется через заданные интервалы времени. Или же чуть иначе: функция выполнится через заданный интервал времени однократно. Наконец, и третья интерпретация тоже имеет место быть: таймер - это некий счетчик, который сам по себе считает, а мы можем время от времени поглядывать на его значение и принимать какие-то решения.

Амбула.

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

#define TIMER_CNT	5
static volatile uint8_t timer[TIMER_CNT];

// обработчик прерывания от таймера, вызывается каждую миллисекунду
ISR(TIMER_vect){
  for(uint8_t i=0; i<TIMER_CNT; i++){
    if(timer[i]) timer[i]--;
  }
}

// вот так можно ограничить длительность цикла интервалом времени в 100 мс
timer[0] = 100;
while(timer[0]){
  // что-то длительное
  if(какое-то-условие-неизвестно-когда-возникающее) break;
}
if(!timer[0]){
  // из цикла вышли по таймауту
} else {
  // из цикла вышли по условию
}

Вроде бы, все просто и понятно. И даже удобно. Я сам 100 раз так делал!

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

Путем нехитрых манипуляций можно заметно улучшить ситуацию. Хотя и несколько усложнив код:

#include <util/atomic.h>
  
#define TIMER_CNT	5

typedef uint8_t (*tmr_func)(void);  
typedef struct{
  uint16_t	counter;
  uint16_t	duration;
  tmr_func	func;
} timer_t;

static volatile timer_t timer[TIMER_CNT];

ISR(TIMER_vect){
  for(uint8_t i=0; i<TIMER_CNT; i++){
    if(timer[i].counter){
      timer[i].counter--;
      if((timer[i].counter == 0) && (timer[i].func != NULL))
        if(timer[i].func()) timer[i].counter = timer[i].duration;
    }
  }
}

void timer_start(uint8_t t, uint16_t duration, tmr_func f){
  ATOMIC_BLOCK(ATOMIC_RESTORESTATE){
    timer[t].counter = duration;
    timer[t].duration = duration;
    timer[t].func = f;
  }
}

uint8_t timer_out(uint8_t t){
  ATOMIC_BLOCK(ATOMIC_RESTORESTATE){
    return timer[t].counter == 0;
  }
}

// вот так можно заставить светодиоды мигать с разной частотой
static uint8_t blink_led1(void){
  PORTB ^= 1<<0; // светодиод на нулевой линии порта
  return 1; // для перезапуска функции
}

static uint8_t blink_led2(void){
  PORTB ^= 1<<1; // светодиод на первой линии порта
  return 1; // для перезапуска функции
}

timer_start(0, 500, blink_led1);
timer_start(0, 300, blink_led2);

while(1){
  // тут что-то делаем, а светодиоды тем временем мигают каждый по-своему
}

Разумеется, здесь уже и атомарность доступа к значению счетчика реализована (ценой вызова отдельной функции), и все вариаты из преамбулы тоже. Надеюсь, очевидно, что если переданная в таймер функция вернет 0, она больше не будет вызываться после того, как таймер истечет? Чем вам не RTOS в минимальном виде? Главное условие в применимости такого подхода - предельно быстрое исполнение таймерной функции. Но при использовании автоматов состояний этим способом можно решать большой спектр практических задач.

Но проблема с "учетом" таймеров осталась. Да и если вы вдруг станете нуждаться в бОльшем количестве таймеров, чем TIMER_CNT, вам придется эту константу менять. И в случае, если вы модифицируете старый проект, и старое количество таймеров вам не нужно, то тоже надо это вручную менять. Мелочь, а неприятно.

Хорошо было бы, если бы в любом месте кода описал свой отдельный static (т.е. невидимый другим) таймер, и пользуешься им. Не нужен -удалил его описание, и не пользуешься. А "система" сама заботится о том, чтобы таймер "тикал" или не "тикал", если не нужен.

И обычно для этого используют возможности RTOS.

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

ARV

Нельзя полюбить RTOS и избавиться от волнений. Любовь - это штука, волнующая кровь по определению, так что...

Стихли первые эмоции на основе эйфории, появилась тревога. 

Отладка RTOS - та еще песня! Никогда не знаешь точно, что и как происходит не так, если оно не так. Когда одна задача посылает команды по USART в устройство, другая задача принимает от него ответы, а третья занимается управлением, понять, почему третья задача работает не правильно, очень не просто. Непросто потому, что запросы и ответы разделены во времени и в пространстве, и если управляющая задача ждет готовности, надо выснить, из-за неотправленного запроса или же из-за не полученного ответа. А если на все это накладывается еще и не совсем разумное поведение устройства, то вообще все становится загадочно и сташно.

Пытаюсь совместить модуль плейера MP3-файлов с RTOS. Написал две функции-задачи для приема ответов и отправки команд. Вроде бы все правильно, все корректно. 

/**
* задача приема сообщений от других задач и выдачи команд в плейер
* @param p не используется
*/
static void control_task(void *p){
	static int16_t ver;
	static player_msg_t *msg;

	ver = get_current_mbox_version(&player_mailbox);
	while(1){
		wait_for_increment_of(&tick, 10);
		// обработка сообщений
		if((msg = read_mbox_min_version(&player_mailbox, &ver)) != NULL){
			// новое в ящике
			// разбор сообщения
			switch(msg->cmd){
			case PCMD_RESET: // сброс
				status = P_NOT_READY;
				mp3_cmd(MP3_CMD_RESET,0,0);
				break;
			case PCMD_SET_VOL: // громкость
				mp3_cmd(MP3_CMD_SET_VOL, 0, msg->bparam > MP3_MAX_VOL ? MP3_MAX_VOL : msg->bparam);
				break;
			case PCMD_STOP: // остановка воспроизведения
				if(status != P_READY){
					mp3_cmd(MP3_CMD_STOP, 0, 0);
					status = P_READY;
				}
				break;
			case PCMD_S_MSG_QUEUE: // воспроизвести сообщение с ожиданием
				while((status != P_READY)) yield(); //continue;
			case PCMD_S_MSG_NOW: // немедленно воспроизвести сообщение
				// если папка не указана - ищем трек в корне
				if(msg->bparam)
					mp3_cmd(MP3_CMD_PLAY_FOLDER, msg->bparam, msg->wparam);
				else
					mp3_cmd(MP3_CMD_PLAY, msg->wparam >> 8, msg->wparam & 0xFF);
				status = P_PLAY;
				break;
			case PCMD_L_MSG_QUEUE: // воспроизвести трек из "большой" папки с ожиданием
				while(status != P_READY) yield(); //continue;
			case PCMD_L_MSG_NOW: // воспроизвести  из "большой" папки немедленно
				status = P_PLAY;
				mp3_cmd(MP3_CMD_PLAY_3000, ((msg->bparam & 0x0F) << 4) | ((msg->wparam>>8) & 0x0F),msg->wparam & 0xFF);
				break;
			case PCMD_USER: // любая иная команда
				mp3_cmd(msg->bparam, msg->wparam>>8, msg->wparam & 0xFF);
				break;
			}
		}
		release_mbox_read();
		ver++;
	}
}

#include <util/delay.h>

/**
 * Задача приема сообщений от модуля плейера. Осуществляет управление статусом
 * плейера в зависимости от принятых команд.
 * @param p не используется
 */
static void reseive_task(void *p){
	static mp3_buf_t packet;
	static uint8_t old;
	static uint8_t d;

	while(1){
		// обработка ответов модуля
		d = 0;
		// ждем время, достаточное для приема пакета (10 мс)
		wait_for_increment_of(&tick,10);
		// ищем стартовый байт
		if(data_reseived()) d = data_get();
		if(d != MP3_START_BYTE) continue;
		// считываем пакет
		for(uint8_t i=0; i < (MP3_PACKET_SZ-1); i++){
			packet.bytes[i] = d;
			while(!data_reseived()) to_os();
			d = data_get();
		}
		// обрабатываем пакет
		switch(packet.command){
		case MP3_ERROR: // ошибка
			mprintf("\nError %02X st=%d", packet.param_lo, status);
			if(status == P_PLAY) status = P_READY;
			break;
		case MP3_STAY_USB:// конец воспроизведения
		case MP3_STAY_SD:
				// STAY приходит дважды!!!, один раз надо игнорировать
				//dbg_packet(&packet);
				if(old != packet.param_lo)
					status = P_READY;
				old = packet.param_lo;
			break;
		case MP3_DEV_STATUS: // инициализация закончена
			if((status == P_NOT_READY) && (packet.param_lo == DEV_SD))
				status = P_READY;
			break;
		case MP3_PLUG_IN: // подключение источника
			status = P_READY;
			break;
		case MP3_PULL_OUT: // отключение источника
			status = P_NOT_READY;
			break;
		default: // все прочие пакеты
			break;
		}
	}
}

Проверяю функционирование при помощи простой функции, "говорящей время":

void say_time(uint8_t h, uint8_t m){
	player_send_msg(PCMD_S_MSG_QUEUE, FOLDER_MSG, SAY_TIME);
	if(h==0) h=24;
	if(m==0) m=60;
	player_send_msg(PCMD_S_MSG_QUEUE, FOLDER_HOUR, h);
	player_send_msg(PCMD_S_MSG_QUEUE, FOLDER_MIN, m);
}

Вызываю эту функцию каждые 5 секунд, имитируя минуты, в отдельной задаче. В итоге система говорит время некоторое количество раз, после чего состояние модуля становится P_PLAY и не исчезает. Если посмотреть на код функций управления и приема ответов, то можно понять, что такая ситуация возможна, если модуль не ответил о том, что файл проигран до конца. НО ОН ОТВЕЧАЕТ! И отвечает 2 раза на каждый файл, о чем в документации нет ни слова!

Что происходит, как выяснить? Самое удивительное, что если снять ремарку с отладочного вывода содержимого принятого ответа, то все начинает работать! И в терминале я вижу, что на каждый файл приходит подтверждение окончания воспроизведения... И, значит, состояние P_PLAY обязано сбрасываться в P_READY! Но если отладочный вывод в терминал заремарить - рано или поздно все виснет.

А я-то думал, волноваться больше не придется...

ARV

Вот вы говорите: AVR слишком убоги, чтобы применять на них RTOS... А я рискнул...

Сначала попытался рассмотреть имеющиеся варианты, чтобы сделать предварительные выводы. Поиск вываливает примерно с десяток готовых разработок RTOS разной степени крутости, из которых FreeRTOS, естественно, в лидерах. Однако, я оценил свои силы и решил, что вхождение в эту ОС для меня обернется большими сложностями, в основном, из-за большого количества возможностей API, и англоязычным их описанием. Ну не принимает душа русская языка аглицкого, даже со словарем и гуглопереводчиком в больших количествах. А из осей на великом и могучем нашлось только две: кооперативная OSA и присиплюсплюснатая ScmRTOS.

Опять-таки из-за собственной ограниченности более современная и продвинутая ScmRTOS мне показалась недоступной - С++ пока что понимаю и принимаю исключительно в качестве наказания. Ну, собственно, и вышло, что начать и закончить поиск осей для AVR можно на OSA.

Попробовал - получается. Не без скрипа, но работает. И даже увлекло меня это. Но вот что мне не понравилось в этом варианте.

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

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

Вот представьте себе ситуацию: задачи формируют текстовые сообщения и выводят их в USART. Кажется логичным сделать функцию, которая занимается отправкой в USART строки посимвольно и использовать эту функцию во всех задачах - а нельзя! Более того, не смотря на то, что все задачи ПООЧЕРЕДНО формируют строки (ОС ведь кооперативная), каждая из задач должна иметь собственный промежуточный static-буфер для формирования своей строки - это ведь явно лишний расход памяти! При обычном подходе мы бы работали с локальным буфером в каждой функции, а локальный буфер, как известно, исчезал бы при выходе из функции... 

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

В общем, знакомство с OSA было увлекательным, недолгим, интересным, но разочаровывающим.

Другие же ОС, найденные мной, были не кооперативными, а вытесняющими. Вытесняющие ОС имеют много преимуществ перед кооперативными, но один их недостаток сильно ограничивает применение на AVR: они весьма требовательны к объемам ОЗУ. Именно отсюда растут ноги у паникерских мнений, что AVR и "нормальная" RTOS - понятия несовместимые. И это на самом деле так, если мы говорим о микроконтроллерах младше (т.е. слабее) atmega32. Для справки: OSA вполне себе способна быть полезной не только на atmega8, но даже и на attiny2313! 

Но, к счастью для меня, не одной atmega32 ограничен мир AVR, и, кроме прочего, не ограничен и я сам. У меня в загашнике есть и at90can128, и даже atmega2560! И, спросил я себя, почему я должен переживать по поводу вытесняющей ОС при таких-то ресурсах? В at90can128 целых 4К ОЗУ, а уж flash-памяти по 8-битным меркам просто немеряно - 128К, а у монстра atmega2560 вдвое больше всего! Правда, если первый МК паять вполне комфортно (TQFP64), то второй без микроскопа уже сложно (TQFP100 c шагом выводов 0,5 мм). А тут еще у меня завалялась отладочная платка DVK90CAN1... Ну, вы поняли...

Итак, решающим теперь для меня стал поиск максимально простой операционки - чтобы мне по силам.

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

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

Плюсы этой RTOS перед OSA неоспоримы: не надо предпринимать практически никаких усилий по оформлению кода - пишется точно так же, как всегда, с локальными переменными, с вложенными вызовами функций и т.д. Разумеется, надо следить за общими ресурсами и блокировать к ним доступ, если необходимо - но это вообще всегда необходимо в многозадачных системах, и даже в ОSA частично так. Минусы, правда, тоже заметны: минимальное приложение, тупо мигающее двумя светодиодами (каждый в своей задаче) занимает почти 2К flash и порядка 400 байт ОЗУ. На просторах выбранного мной МК это даже и не заметно, но для atmega8 может быть близким к техническому пределу.

YAVRTOS написана на 99,9% на Си (только сохранение/восстановление контекста реализовано в виде ассемблерной вставки из трех десятков push-pop), всего два файла (task.c и task.h) - все это явный плюс в плане изучения и модификации под себя, если надо (и если хватает ума). Косвенным плюсом (или минусом, если продолжать переживать о ресурсах) является массовое применение malloc в ядре ОС, а значит, и в пользовательском приложении уже вполне оправдано динамическое распределение памяти. 

И мой энтузиазм просто на взлете от первого опыта! Например, вот как выглядит код задачи и вспомогательных функций для извлечения точного времени из GPS-приемника, подключенному к USART1, и вывода этих показаний на стандартный вывод (stdout, связанный с USART0):

const __flash char gps_msg[] = "RMC,";
#define GPS_MSG_SZ (sizeof(gps_msg)-1)

// поллинг 1 символа от GPS
static uint8_t get_char(void){
	while(bit_is_clear(UCSR1A, RXC)) wait_for_increment_of(&tick, 1);
	return UDR1;
}

// получение 1 цифры из символа
static uint8_t get_dig(void){
	return (get_char() - '0');
}

// собственно сама задача
void p2p_usart(void *p){
	uint8_t i;
	uint8_t h,m,s;
	while(1){
		i = 0;
      // ждем прихода сообщения с точным временем
		while((i < GPS_MSG_SZ) && (get_char() == gps_msg[i])) i++;
		if(i == GPS_MSG_SZ){
          // разбираем сообщение по символам
			h = get_dig()*10 + get_dig() + 3; // +3 - это часовой пояс
			h %= 24;
			m = get_dig()*10 + get_dig();
			s = get_dig()*10 + get_dig();
          // пропускаем сотые доли секунды
			get_char(); // '.'
			get_char(); // 's'
			get_char(); // 's'
			get_char(); // ','
          // проверка корректности времени и его вывод
			if(get_char() == 'A'){
				printf_P(PSTR("GPS Time %02d:%02d.%02d\r"),h,m,s);
			} else {
				printf_P(PSTR("No GPS, wait...  \r"));
			}
		}
	}
}

Как видите, код крайне "тупой", то есть прямолинейный, как лом: сплошные ожидания и никакой заботы о том, что параллельно должно что-то еще работать. В моем случае просто мигают 2 светодиода - один с длительностью импульса/паузы в 500 тиков, а второй в 501 (кстати, 1 тик = 1 мс, тактовая частота МК = 8 МГц). Но вместо светодиодов может быть еще две (или сколько надо) аналогично прямолинейно написанных задач, и можно быть уверенным, что все будет работать! Приведу данные по итогам компиляции проекта, чтобы продемонстрировать израсходованные ресурсы:

Цитата

AVR Memory Usage
----------------
Device: at90can128

Program:    5544 bytes (4.2% Full)
(.text + .data + .bootloader)

Data:         47 bytes (1.1% Full)
(.data + .bss + .noinit)

Не так уж и плохо, учитывая свободное применение printf. В активном режиме используется дополнительно 380 байт ОЗУ под стеки задач и ОС, т.е. примерно 10% всего объема - еще много остается.

Есть, кроме YAVRTOS, и другие альтернативы, например, FemtoOS, которая поддерживает даже (!!!) attiny25, и при этом тоже является вытесняющей операционкой. Но она существенно "богаче" в плане API, и разобраться с нею будет посложнее, т.к. документирована она явно менее детально. Возможно, я и её попробую на вкус...

И, скорее всего, теперь это станет для меня основным способом написания программ. RTOS позволяет сильно упростить себе жизнь. Имхо.

Sign in to follow this