Ну вот, санитары отпустили, и теперь можно вспомнить, что еще не совсем забыто и сделать, что еще не сделано. Например, рассказать, чего это такое я хотел рассказать ранее, да не успел.
Собственно вот что я сделал.
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-миллисекундынми интервалами.
Надеюсь, эта идея вам понравится, как и мне. Кстати, на таком же принципе очень удобно делать всякие парсеры строк, ну то есть когда вам надо в зависимости от того или иного текста в строке выполнять ту или иную функцию. Если делать все в одном файле - он будет дико объёмным, кто не верит - посмотрите в исходники какого-либо бейсика. Разобраться в таком файле сложно... А применив описанный принцип, т.е. совместив на уровне скрипта определенные в разных модулях элементы общего массива, можно получить очень компактный и понятный код.
До встречи на процедурах!
Комментарии
Отправить комментарий