К основному контенту

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

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

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

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

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

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

Комментарии

Популярные сообщения из этого блога

Все ниже, и ниже, и ниже... стремим CLK AVR...

Как ни посмотришь, так все всегда в гонке... Выше, больше, быстрее, потом еще больше, еще выше, еще быстрее... Мегагерцы, Гигагерцы... А потом нервные срывы и - милости просим к нам в гнездо, в комнату с белым потолком, с правом на надежду! И это еще хорошо, если так повезет... А кому это надо? Мне, например, не надо. Свой последний проект на микроконтроллере AVR я сделал на тактовой частоте в 32768 Гц. Ни больше, ни меньше, а 32 килогерца. Само собой, это вышло не специально... Просто решил делать часы на микроконтроллере, в котором нет аппаратного таймера специально под организацию часов реального времени... Ну и самым простым оказалось перевести весь проект на тактирование от часового кварца.  А чего такого? Это самая низкая из доступных "по умолчанию" частот (даже тактирование от генератора WDT и то на большей частоте получается - порядка 100 кГц), при том стабильная, ибо кварц.  И вышло так, что практически никаких ограничений в процессе написания прошивки я не испытывал

Музей древностей

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

О братьях наших меньших

  Когда наступишь ты в говно, Знай: невиновное оно! Ведь, безусловно, младший брат Реально в этом виноват.