Перейти к содержанию
  • запись
    21
  • комментариев
    136
  • просмотров
    3 089

Минималистическая RTOS - продолжение


ARV

1 178 просмотров

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

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

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-миллисекундынми интервалами.

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

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

2 Комментария


Рекомендуемые комментарии

Что первым бросилось в глаза - это слишком тяжёлый обработчик таймеров. Перебор, да ещё и работа через указатели, ...
Роман, почитай доку на OSA. Там автор хорошо описывает обработку одного из типов своих таймеров. Там неважно их кол-во. Они обрабатываются по времени как один единственный.

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

Сейчас лень искать, но на память, вроде, Qtimer'ы их величать.

PS: А, у тебя переустановка идёт в прерывании, не заметил сразу. Тогда отбой ... :)

Изменено пользователем Alex
Ссылка на комментарий

Чудес не бывает, @Alex ... За все придется платить... Я сосредоточил усилия на простоте эксплуатации таймеров, и, возможно, потерял в эффективности их внутреннего обслуживания. Кому-то важнее эффективность обслуживания - но тогда возрастают проблемы эксплуатации... С точки зрения производительности перебор массива указателей ничем не хуже перебора свзного списка указателей. А мелочные нюансы можно и пожертвовать. В моем случае жертвовать приходится тем, что если в текущий момент нет активных таймеров, все равно происходит перебор массива структур вхолостую. Ну да и пускай - все равно в любй RTOS есть Idle-задача, которая молотит хрен знает зачем...

Изменено пользователем ARV
Ссылка на комментарий

Присоединяйтесь к обсуждению

Вы публикуете как гость. Если у вас есть аккаунт, авторизуйтесь, чтобы опубликовать от имени своего аккаунта.
Примечание: Ваш пост будет проверен модератором, прежде чем станет видимым.

Гость
Unfortunately, your content contains terms that we do not allow. Please edit your content to remove the highlighted words below.
Добавить комментарий...

×   Вставлено с форматированием.   Восстановить форматирование

  Разрешено использовать не более 75 эмодзи.

×   Ваша ссылка была автоматически встроена.   Отображать как обычную ссылку

×   Ваш предыдущий контент был восстановлен.   Очистить редактор

×   Вы не можете вставлять изображения напрямую. Загружайте или вставляйте изображения по ссылке.

Загрузка...
×
×
  • Создать...