LPCXpresso Урок 4. Systick. Использование таймера для отсчета времени.

Продолжаем осваивать LPC13xx в среде от CodeRed в рамках курса. Знакомимся с системным таймером. Учимся мигать светодиодом по часам.


Отсчёт задержки

Естественно тупить в цикле со счётчиком самый простой и быстрый вариант. Его вполне можно применять не «первых парах», потому как он не требует каких-либо глубоких познаний. Но я уже говорил, что мне он не нравится. Давайте сделаем функцию задержки чуть более точной, что ли.
Как вы, вероятно, помните из первого урока, для отсчета времени там использовался 16-ти битный таймер. Я же пойду несколько другим путём. Я возьму за основу системный таймер, aka SYSTICK.

Системный таймер

Обычно системный таймер позиционируют как служебный для операционных систем. По нему происходит переключение контекста в многопоточных системах, по нему же обновляются часы. Вообще, по нему выполняются всевозможные служебные функции.
В даташите на LPC13xx на блок-схеме данный таймер не отмечем, а в тексте про него написано поразительно много информации:
7.15 System tick timer
The ARM Cortex-M3 includes a system tick timer (SYSTICK) that is intended to generate
a dedicated SYSTICK exception at a fixed time interval, normally set to 10 ms.
Тут сказано, что SYSTICK позволяет генерировать прерывания каждые 10мс. Спросите: «что за фуфло ты нам подсовываешь?».
А все дело в том, что таймер этот на самом деле присутствует даже на блок-схеме. И не где-нибудь, а в элементе ARM Cortex-M3. Дело в том, что системный таймер является «стандартом» для ядра Cortex. Не может быть АРМ-контроллера с ядром Cortex, но без системного таймера. Так что можете быть уверены, это фуфло будет в каждом контроллере.
А если открыть User Manual по LPC13xx, то можно обнаружить целый раздел посвященный данному таймеру. А если конкретнее, то UM10375 Chapter 16. Собственно там написано что это: 24 битный таймер обратного счёта, с автозагрузкой, тактируется либо от системного генератора (CPU clock), либо от собственного. Для системного таймера выделен отдельный вектор прерывания.
Таймер по факту не обязательно выдает 10 миллисекундные интервалы, его можно настроить на другой интервал (чем мы и воспользуемся).

Средства для работы с системным таймером

Библиотека CMSIS представляет нам средства для работы с данным таймером.
В самой библиотеке, в файле core_cm3.h имеется следующее определение:
static __INLINE uint32_t SysTick_Config(uint32_t ticks)
{ 
  if (ticks > SysTick_LOAD_RELOAD_Msk)  return (1);            /* Reload value impossible */
                                                               
  SysTick->LOAD  = (ticks & SysTick_LOAD_RELOAD_Msk) - 1;      /* set reload register */
  NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 1);  /* set Priority for Cortex-M0 System Interrupts */
  SysTick->VAL   = 0;                                          /* Load the SysTick Counter Value */
  SysTick->CTRL  = SysTick_CTRL_CLKSOURCE_Msk | 
                   SysTick_CTRL_TICKINT_Msk   | 
                   SysTick_CTRL_ENABLE_Msk;                    /* Enable SysTick IRQ and SysTick Timer */
  return (0);                                                  /* Function successful */
}

Суть его проста. Для начала проверяется допустимость использования запрошенного интервала времени, и если он не допустим, то возвращаем признак ошибки. В регистр загрузки заносится значение задержки. Затем устанавливается самый низкий приоритет прерывания для котроллера прерываний. Сбрасываем счетчик, и разрешаем работу таймера от системного генератора.
Настраиваемый контроллер векторов прерываний NVIC заслуживает отдельного повествования, не станем сейчас этим заниматься. Просто считайте, что это средство, позволяющее определить, какое из возникших прерываний должно быть обслужено немедленно, а какое может быть отложено до завершения обработки других прерываний.
Что такое прерывание? Это ваш друг Вася, позвавший вас пить пиво, пока вы были заняты изучением данного курса. Вы находились в основном цикле выполнения программы – изучение урока. Но Вася «потребовал» от вас прерваться от этого занятия и пойти с ним выпить пива – перейти к обработке прерывания. По завершении попойки вы вернётесь к изучению материала с того места, где остановились до прихода Васи – вернулись к выполнению основной программы. Да, немного не реалистичный пример, но близко).
В рамках предложенной модели NVIC может выступать в роли вашего сознания, если вдруг кроме Васи к вам прейдет ещё и Вика. В соответствии с расставленными приоритетами вы пойдете либо пить пиво с Васей, либо отправитесь с Викой.
В проекте же, в файле cr_startup_lpc13.c присутствуют следующие строки
WEAK void SysTick_Handler(void);

void SysTick_Handler(void) {
	while (1) {
	}
}

Ну и запись в массиве векторов прерываний, aka g_pfnVectors. Суть так же проста: вектору прерывания системного таймера ставится функция SysTick_Handler в качестве обработчика, которая на данный момент просто загонит контроллер в бесконечный цикл. Бесконечный цикл позволит вам определить возникновение необработанного вами прерывания по зависанию контроллера (удобно это или нет, не будем сейчас рассматривать).
Но. Следует отметить наличие в определении функции слова WEAK. Это «слабое связывание». По сути, оно означает, что эта функция может быть «строго» переопределена где-либо в другом месте проекта, и именно переопределённую, более строгую, функцию следует использовать взамен «слабой». При этом «строгим» будет простое определение функции, без каких либо дополнительных ключевых слов.
Чем это полезно нам? А нам просто не надо искать номер вектора прерывания, искать в таблице векторов, куда вставить свой адрес. Нам достаточно просто описать свою функцию обработки прерывания, и вызвать функцию настройки. И всё, мы получаем рабочее средство.

Набираем код

Итак, в файле main.c заменяем функцию delay_ms на следующий код:
// --- Средства работы со временем - Системный таймер ---
static volatile uint32_t msTicks = 0;		// counts 1ms timeTicks

void SysTick_Handler(void)
{
	msTicks++;		// инкремент счётчика времени
}

void delay_ms(uint32_t ms)
{
	uint32_t startTicks;
	startTicks = msTicks;
	while((msTicks - startTicks) < ms);	// Ждем завершения периода
}

Тут мы определили счётчик миллисекунд. Эта переменная будет просто инкрементироваться в обработчики события системного таймера. Функция delay_ms в цикле ждет, пока данный счетчик изменится на величину ms переданную в параметре. Следует отметить наличие ключевого слова volatile в определение счётчика времени, необходимого, что бы оптимизатор ни переделал цикл ожидания в бесконечный.
В функцию main после настройки порта светодиода, добавляем код настройки таймера:
SysTick_Config(SystemCoreClock / 1000);	// настройка таймера на период 1мс

Здесь, SystemCoreClock переменная, определённая в CMSIS и содержащая частоту системного генератора. Поделив её на 1000, мы вычислили, сколько тактов надо для задержки в 1мс. Вот собственно и всё, таймер настроен и запущен.
Отмечу, что данный способ реализации задержки так же не является точным. В целом задержка может получиться и на 1 миллисекунду меньше запрошенной, если вызов функции был на границе миллисекундного интервала. Если в последнем цикле < заменить на <= то задержка будет гарантированно не меньше запрошенной, но может быть и на 1 миллисекунду больше. Обычно используется именно последний вариант.

Собираем проект

Тут всё как обычно. Собрали, запустили на отладку, и любуемся достижениями. Хоть по секундомеру замеряй.

Точки останова

Хочу рассказать вам о таком средстве отладчика как точка останова — Breakpoint.
В некоторых случаях (да в большинстве случаев) пошаговая отладка не удобна, ведь что бы зайти в нужную нам функцию может потребоваться много тактов процессора. А если вход выполняется и «не зависимо от нас», то всё становится ещё печальнее.
В этих случаях на помощь нам и приходят breakpointы. Устанавливается и снимается он кликом мышки слева (в области номера строки) от интересующей нас строки. Отображается там же, синим кружком (если кружок не синий, то что-то не так):

Когда контроллер в режиме отладки дойдет но данной строки, он остановится перед её выполнением, и среда соответствующим образом изменит свой вид, установив курсор на данную строку:

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

Просмотр значения переменных

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

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

Статистика

Дебаг версия получилась 3376 байт.
Релиз версия заняла 2560 байт.
Да, в ATtiny2113 такой код не поместится, но кого это волнует, у нас 32кБайта кода, на тактовой частоте почти в 4 раза выше и цене всего лишь в 2 раза. Да и потом, разве мы только миганием светодиодов собираемся заниматься?

Замечания по проекту из архива

У меня присутствует так же «бонусная» функция getUptime.
__INLINE uint32_t getUptime(void)
{
	return msTicks;
}

Она просто возвращает количество миллисекунд, прошедших с запуска системы (или последнего сброса). Мы её не используем сейчас ни где, но пусть валяется.
Код настройки таймера выглядит несколько сложнее:
if (SysTick_Config(SystemCoreClock / 1000)) {	// настройка таймера на период 1мс
	GPIOSetValue(LED_PORT, LED_BIT, LED_ON);
	while(1);
}
NVIC_SetPriority(SysTick_IRQn, 1);		// задаем почти максимальный приоритет
if ( !(SysTick->CTRL & SysTick_CTRL_CLKSOURCE_Msk) ) {
	LPC_SYSCON->SYSTICKCLKDIV = 0x08;		// из 16.6.1 замечание к внешнему таймеру (по примеру)
}

Если функция SysTick_Config вернёт нам ошибку, то мы зажигаем светодиод и уходим в бесконечный цикл, ведь мы не можем работать без таймера. Тут можно было сделать что угодно, например, задействовать вместо системного таймера обычный, или заменить задержку исходной функцией тупления. Да и просто можно было ничего не делать: ошибка возвращается только если величина задержки больше максимально допустимой (24 битное значение), но мы, то знаем, что 72000 помещается в них.
Далее я добавил вызов NVIC_SetPriority. Его можно и выкинуть. Это переназначение прерыванию системного таймера самого высокого приоритета. Просто у меня был случай, когда отсчет времени должен был продолжаться во время обработки других прерываний. А в библиотеке, как было отмечено раннее, прерыванию системного таймера ставился самый низкий приоритет, что не позволяло ему вытеснять другие обработчики прерываний. Оставил данную строку я, что бы продемонстрировать возможность такого решения.
Ну и последняя ветка условия в нашем примере так же может быть выброшена. Строка нужна при тактировании от встроенного генератора. Зачем и почему, можете почитать в User Manual’е. А в библиотеке, как мы помним, тактирование шло от системного таймера.
Так же функция delay_ms у меня выглядит как:
void delay_ms(uint32_t ms)
{
	uint32_t startTicks;
	startTicks = msTicks;
	if(startTicks > 0xFFFFFFFF - ms) {	// для случая "вблизи" верхней границы времени
		while(msTicks >= startTicks);
	}
	while((msTicks - startTicks) < ms);	// Ждем завершения периода
}

Якобы для случая переполнения счётчика, которое произойдет примерно через 49 дней и 17 часов. Отличие не критичное, но для его удаления из проекта надо больше времени, а оно того не стоит, уж извиняйте.
  • 0
  • 09 сентября 2011, 09:35
  • angel5a
  • 1
Файлы в топике: blinky_systick.zip

Комментарии (11)

RSS свернуть / развернуть
void delay_ms(uint32_t ms)
{
        uint32_t startTicks;
        startTicks = msTicks;
        if(startTicks > 0xFFFFFFFF - ms) {      // для случая "вблизи" верхней границы времени
                while(msTicks >= startTicks);
        }
        while((msTicks - startTicks) < ms);     // Ждем завершения периода
}
Никакой «случай вблизи верхней границы» обрабатывать не нужно.

Так что вот чуть более годный код:
delay_ms(unsigned ms)
{
    unsigned offset = msTicks;
    while(msTicks - offset < ms);
}
Объясняю. Допустим, мы вызываем delay_ms(5), а msTicks равен 0xFFFFFFFD. По мере увеличения счётчика, msTicks — offset будет изменяться так:
0xFFFFFFFD - 0xFFFFFFFD = 0,
0xFFFFFFFE - 0xFFFFFFFD = 1,
0xFFFFFFFF - 0xFFFFFFFD = 2,
0x00000000 - 0xFFFFFFFD = 3,
0x00000001 - 0xFFFFFFFD = 4,
0x00000002 - 0xFFFFFFFD = 5
вышли из цикла.

Однако всё равно это говноцикл задержки ничуть не лучше for(i=0; i<1000000; ++i);. А вот человеческий способ:
int main()
{
    int lastBlink = msTicks;

    // главный цикл.
    while(1) {
        if(msTicks - lastBlink >= 1000) {
            // переключаем светодиодик
            lastBlink = msTicks;
        }

        // делаем другие полезные вещи, вместо того,
        // чтобы тупить в говноциклах задержек
    }

}
0
Функция getUptime для тебя ;)
На оптимальность кода не претендую.
С int lastBlink = msTicks; рекомендую поаккуратнее :)
0
На оптимальность кода не претендую.
А зря.

С int lastBlink = msTicks; рекомендую поаккуратнее :)
Ага. Но это чисто эстетический косяк. Ошибки нету.
0
Ну может мне сюда ещё и всевозможных финтов по оптимизации воткнуть, типа подсчета битов, карты памяти, оптимизированное деление 64 битных чисел? :) Курс для новичков.

Да, в функции задержки «перебздел» и безнаковость и проверка. А косяк на самом деле не эстетический, а скрытый, возникает при «определённой фазе луны». Проблема будет из-за сравнения знаковых и беззнаковых чисел, и если со сравнением оаздать, то можно зависнуть очень на долго.

Вообщем пахнет холливаром. Попозже статью думаю подправлю, но чисто внешне (без архива и статистики).
0
Проблема будет из-за сравнения знаковых и беззнаковых чисел
Не будет.
0
Да, в функции задержки «перебздел» и безнаковость и проверка.
Будет отлично работать и со знаковыми числами.
0
Да, разработчик был укурен, что-то стормозил. Надо обновить знания по языку.
Остается вопрос с чего это меня так сглючило? Ведь код из проекта.
0
Спасибо за помощь.
0
допустим мы настроили системный таймер функцией SysTick_Config, использовали пару раз задержки, а дальше у нас выполняется код без использования задержек. но ПРЕРЫВАНИЯ от SysTick будут происходить?
0
  • avatar
  • del
  • 04 июля 2013, 15:14
Да. По сути таймер для целей постоянного формирования прерываний с заданным периодом и предназначался (нужно для различных ОС). Но вам ни кто не запрещает его отключить.

Если Вам надо только 1-2 раза в определенных местах сделать задержку, то можете непосредственно в тех местах использовать инициализацию таймера и установку периода в период требуемой задержки, а после использования запрещать работу.
Можете даже от прерываний отказаться, а просто в цикле ожидать установки соответствующего флага.
По сути использовать SysTick как и прочие таймеры (коим, только урезанным по функционалу, он впринципе и является).
0
чтоб не задалбывать мк прерываниями, написал вот такое:
void Init_SysTick(void){
  SysTick->CTRL &= ~SysTick_CTRL_TICKINT; //Выключить прерывание от SysTick 
  SysTick->CTRL |= SysTick_CTRL_CLKSOURCE; //Тактировать с частотой ядра
}


delay_us(uint32_t us){
  
  SysTick->LOAD  = (SYSCLK_FREQ_24MHz / 1000000);  //Заносим значение для перегрузки
  SysTick->VAL   = 0; //Обнуляем счетный регистр 
  SysTick->CTRL |= SysTick_CTRL_ENABLE; //Включить SysTick
  
  while(us > 0){
    while (!(SysTick->CTRL & SysTick_CTRL_COUNTFLAG));      
    us--;
  }
  SysTick->CTRL &= ~SysTick_CTRL_ENABLE; //Выключить SysTick  
}


delay_ms(uint32_t ms){
  
  SysTick->LOAD  = (SYSCLK_FREQ_24MHz / 1000);  //Заносим значение для перегрузки
  SysTick->VAL   = 0; //Обнуляем счетный регистр 
  SysTick->CTRL |= SysTick_CTRL_ENABLE; //Включить SysTick
  
  while(ms > 0){
    while (!(SysTick->CTRL & SysTick_CTRL_COUNTFLAG));     
    ms--;
  }
  SysTick->CTRL &= ~SysTick_CTRL_ENABLE; //Выключить SysTick  
}
+1
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.