[Home] [Donate!] [Контакты]

SysTick: системный таймер

Процессоры семейства Cortex-M имеют стандартный таймер, называемый SysTick (системный таймер). Существует несколько вариантов применения этого таймера. Если мы используем RTOS (операционную систему реального времени), то таймер будет задействован для нужд RTOS. Если нам не нужна RTOS, SysTick можем использовать на своё усмотрение. Например, как просто таймер с достаточно высокочастотным тактовым сигналом (используется тактовый сигнал процессора); для периодического выполнения определённых действий (внутри обработчика прерывания) с возможностью настраивать интервал времени между вызовами обработчика в широком диапазоне значений с высокой точностью; как счётчик тактов для точного измерения времени выполнения некоторого фрагмента кода и т.д...

Оглавление
SysTick: системный таймер
Введение
Регистры системного таймера
CMSIS для работы с SysTick
Функция SysTick_Config
Часы на основе системного таймера
Пример программы
Реализация задержек в программе
Формирование задержки с высокой точностью

Введение

Профиль ARMv7-M предусматривает наличие в процессоре с этой архитектурой системного таймера. Системный таймер содержит 24-битовый вычитающий счётчик с автоматической загрузкой начального значения при достижении счётчиком 0. В качестве тактового сигнала для счётчика может быть выбран тактовый сигнал ядра или "внешний" тактовый сигнал. Источник внешнего тактового сигнала определяется реализацией микроконтроллера, для микроконтроллеров серии STM32F100xx это сигнал с частотой в 8 раз меньше частоты ядра. Если таймер включён, каждый раз, когда счётчик достигает нулевого значения, генерируется прерывание SysTick. Для обработки прерывания в программе нужно определить обработчик с именем SysTick_Handler:

// Обработчик прерывания от системного таймера,
// функция-обработчик на C++ должна иметь спецификатор extern "C"
extern "C" void SysTick_Handler()
{
    // Здесь может быть какой-нибудь код...
    // А может и не быть - никаких обязательных действий
    // от обработчика не требуется.
}

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

Пример: подсчёт времени (глобальная переменная cnt увеличивается на 1 каждую миллисекунду).

// Счётчик количества исключений от системного таймера.
volatile uint32_t cnt=0;

// Обработчик исключения от системного таймера.
extern "C" void SysTick_Handler()
{
    cnt++;
}

int main()
{
    // Конфигурируем и запускаем системный таймер
    // для генерации исключения каждую 1 мс.
    if(SysTick_Config(SystemCoreClock/1000))
    {
        // Обработка ошибок.
    }

    while(true){}
}

Регистры системного таймера

Для управления системным таймером используются всего 4 регистра.

Табл. 1. Регистры SysTick
Имя Тип Значение после сброса Описание
SYST_CSR RW 0x0000000x Регистр управления и состояния.
SYST_RVR RW UNKNOWN Перезагружаемое в счётчик значение при достижении счётчиком 0.
SYST_CVR RW UNKNOWN Текущее значение счётчика системного таймера.
SYST_CALIB RO Зависит от реализации Калибровочное значение.
Табл. 2. Используемые биты регистра SYST_CSR
Биты Тип доступа Имя Функция
[16] RO COUNTFLAG Индикатор достижения счётчиком нулевого значения с момента последнего чтения этого регистра:
0 - счётчик не достигал значения 0;
1 - счётчик достигал значения 0;
бит устанавливается в 1 в момент перехода счётчика от значения 1 к значению 0 и сбрасывается в 0 при чтении этого регистра (SYST_CSR) или любой записи в регистр текущего значения счётчика (SYST_CVR).
[2] R/W CLKSOURCE Источник тактового сигнала для системного таймера.
0 - используется определяемый реализацией внешний источник тактового сигнала;
1 - используется тактовый сигнал процессора.
Если использование внешнего тактового сигнала не поддерживается, то при чтении бита возвращается 1, запись игнорируется.
[1] R/W TICKINT Бит разрешения генерации исключения SysTick.
0 - исключения не генерируются;
1 - когда счётчик достигает нулевого значения, состояние исключения SysTick изменяется на "отложено" (и будет обработано, когда это будет возможно с учётом приоритета).
Сброс счётчика в 0 путём записи в регистр SYST_CVR не приводит к откладыванию исключения.
[0] R/W ENABLE Включение счётчика системного таймера.
0 - счётчик выключен;
1 - счётчик работает.

SYST_RVR
Содержит значение, загружаемое в счётчик системного таймера при достижении им нуля. Если счётчик включён, значение счётчика уменьшается на 1 с каждым импульсом тактового сигнала. Когда счётчик достигает нулевого значения, по фронту следующего импульса, в счётчик загружается значение из регистра SYST_RVR. Затем значение счётчика снова начинает уменьшаться с каждым последующим импульсом тактового сигнала. Счётчик является 24-битовым, соответственно в регистре SYST_RVR также используется 24 бита (младших), биты [23:0] регистра.

Если записать в регистр 0, то после достижения счётчиком нулевого значения, он остановится.

SYST_CVR
Текущее значение счётчика системного таймера. При чтении возвращает текущее значение счётчика как 32-битовое число (в неиспользуемых старших битах всегда возвращается 0). Любая запись сбрасывает регистр в 0, а также сбрасывает бит COUNTFLAG в регистре SYST_CSR.

Табл. 3. Используемые биты регистра SYST_CALIB
Биты Имя Функция
[31] NOREF Индикатор наличия определяемой реализацией доступности внешнего тактового сигнала.
0 - возможность использования внешнего тактового сигнала не реализована;
1 - реализована.
[30] SKEW Индикатор точности калибровочного значения для 10 мс интервала:
0 - калибровочное значение точное; 1 - калибровочное значение неточное из-за значения частоты тактового сигнала.
[23:0] TENMS Опциональное поле, если реализовано, значение определяется разработчиком. Если загрузить это значение в регистр SYST_RVR, то счётчик системного таймера будет достигать нулевого значения каждые 10 мс (100 Гц). Если поле содержит нулевое значение, калибровочное значение неизвестно.

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

Для доступа к регистрам системного таймера в CMSIS определена структура SysTick_Type

typedef struct
{
    __IO uint32_t CTRL;  /*!< Offset: 0x00 SysTick Control and Status Register */
    __IO uint32_t LOAD;  /*!< Offset: 0x04 SysTick Reload Value Register       */
    __IO uint32_t VAL;   /*!< Offset: 0x08 SysTick Current Value Register      */
    __I  uint32_t CALIB; /*!< Offset: 0x0C SysTick Calibration Register        */
} SysTick_Type;

и указатель на структуру SysTick

#define SysTick ((SysTick_Type *)SysTick_BASE)
Имя регистра в ARM ARM* Доступ к регистру в CMSIS
SYST_CSR SysTick->CTRL
SYST_RVR SysTick->LOAD
SYST_CVR SysTick->VAL
SYST_CALIB SysTick->CALIB

* ARM ARM - ARMxxx Architecture Reference Manual (руководство по архитектуре ARM).

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

#define SysTick_CTRL_CLKSOURCE_Pos 2 /*!< SysTick CTRL: CLKSOURCE Position */
#define SysTick_CTRL_CLKSOURCE_Msk (1ul << SysTick_CTRL_CLKSOURCE_Pos) /*!< SysTick CTRL: CLKSOURCE Mask */

Надеюсь, принцип именования констант понятен и нет необходимости приводить их полный перечень.

Функция SysTick_Config для конфигурирования системного таймера

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

uint32_t SysTick_Config(uint32_t ticks);

Одного вызова этой функции достаточно, чтобы полностью настроить системный таймер! SysTick_Config настраивает таймер на генерацию исключений каждые ticks тактов, устанавливает минимально возможный приоритет для исключения SysTick, сбрасывает счётчик. После этого, записью в SysTick->CTRL соответствующего значения: SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_TICKINT_Msk | SysTick_CTRL_ENABLE_Msk;, функция разрешает генерацию исключений и запускает счётчик с тактированием от тактового сигнала процессора.

Функция возвращает 0 в случае успеха и 1 в случае ошибки. Ошибка возникает в случае выхода аргумента за допустимые пределы.

Если нам требуется, чтобы исключение от системного таймера генерировалось с определённой частотой f (т.е. f раз в секунду), то функцию SysTick_Config вызывают следующим образом:

    SysTick_Config(SystemCoreClock/f);

Объяснить такую форму вызова можно следующим образом. Здесь SystemCoreClock - определённая в файле system_<device.c> переменная, в которую во время выполнения стартового кода инициализации помещается значение частоты тактового сигнала ядра процессора. Кстати, если в дальнейшем мы изменим частоту, изменяя настройки системы тактирования микроконтроллера, то следует вызвать функцию void SystemCoreClockUpdate (void), которая определена в этом же файле. Функция обновляет значение переменной SystemCoreClock с учётом реальной частоты тактового сигнала ядра в данный момент.

Если нам требуется, чтобы исключение системного таймера генерировалось с частотой f, или что то же самое, с периодом T=1/f, мы должны настроить системный таймер на генерацию исключения каждые ticks тактов, где ticks=SystemCoreClock*T или ticks=SystemCoreClock/f. Второй вариант предпочтительнее - здесь мы можем использовать целочисленное деление (если, конечно, частота f может быть достаточно точно выражена целым), тогда как в первом случае потребуется умножение чисел с плавающей точкой и округление до целого - намного более "тяжёлые" операции.

Например, если требуется генерация исключения каждую миллисекунду (T=1 мс, f=1000 Гц), то вызов функции будет иметь вид:

    SysTick_Config(SystemCoreClock/1000);

Минимальная и максимальная частоты генерации исключений системного таймера зависят от тактовой частоты. Например, по умолчанию для STM32F100xx частота ядра составляет 24 МГц. С учётом того, что счётчик системного таймера имеет 24 разряда, максимальный период между исключениями (1/Fclk) * 2**24, что примерно составляет 0.7 с (1.43 Гц).

Минимальный период (максимальная частота) должна выбираться с учётом того, чтобы процессор успевал выполнить код обработчика между двумя исключениями, и чтобы оставалось достаточно ресурсов на выполнение другого кода.

Часы на основе системного таймера

Трудно не поддаться искушению использовать системный таймер для отсчёта времени. Особенно если микроконтроллер синхронизирован внешним кварцевым резонатором, то SysTick может использоваться как достаточно точные часы с очень высоким разрешением (счётчик таймера подсчитывает каждый такт, а тактовая частота может составлять десятки МГц). Но такое применение будет сопряжено с рядом трудностей.

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

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

И, в-третьих, естественно, при отключённом питании системный таймер не работает.

Так что в качестве часов лучше использовать часы реального времени, если они в данном микроконтроллере присутствуют. Эти часы синхронизируются от отдельного "часового" кварца на 32768 Гц, обеспечивают высокую точность хода, не останавливаются в режимах низкого потребления и продолжают работать при отключённом питании от батарейки - в микроконтроллерах с часами реального времени предусмотрен отдельный вывод для питания часов от батарейки.

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

Пример программы

При программировании для компьютеров, традиционно образцом простейшей программы является программа, выводящая сообщение вроде "Hello, World!". В случае микроконтроллеров обычно в качестве простейшего примера приводится программа, заставляющая микроконтроллер мигать светодиодом.

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

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

//(!) VECT_TAB_SRAM - определить символ для отладки в RAM (!)

#include <stm32f10x.h>

// Время горения светодиода, мс.
const uint32_t LED_ON_TM=1000;

// Время паузы, мс.
const uint32_t LED_OFF_TM=2000;

// Счётчик миллисекунд.
volatile uint32_t msTicks=0;

// Обработчик прерывания от системного таймера
// подсчитывает прошедшее время в мс.
extern "C" void SysTick_Handler()
{
    msTicks++;
}

int main()
{
    // Включаем тактовый сигнал порта GPIOC.
    RCC->APB2ENR|=RCC_APB2ENR_IOPCEN;

    // Конфигурируем PC9 как двухтактный выход,
    // основная функция, макс. частота 2МГц.
    // CNF: 00; MODE: 10
    GPIOC->CRH=GPIO_CRH_MODE9_1;

    // При желании можно настроить режим работы всех
    // портов в соответствии с правилами хорошего тона.

    // Включить LED.
    GPIOC->BSRR=GPIO_BSRR_BS9;

    // Конфигурируем и запускаем системный таймер
    // для генерации исключения каждую 1 мс.
    if(SysTick_Config(SystemCoreClock/1000))
    {
        // Здесь могла бы быть обработка ошибок
        // в случае неудачного вызова SysTick_Config.
    }

    // Начальная точка отсчёта времени (начало периода
    // цикла переключения светодиода).
    uint32_t t0=msTicks;

    // В бесконечном цикле управляем светодиодом.
    while(true)
    {
        // Если период переключения завершился, то зажигаем светодиод
    	// (так как новый период начинается с зажигания) и
    	// смещаем точку отсчёта для формирования нового периода.
        if(msTicks-t0>=LED_ON_TM+LED_OFF_TM)
        {
            GPIOC->BSRR=GPIO_BSRR_BS9;
            t0=msTicks;
        }
        else if(msTicks-t0>LED_ON_TM&&      // Если время горения истекло
                (GPIOC->ODR&GPIO_ODR_ODR9)) // и светодиод ещё горит, то
            GPIOC->BRR=GPIO_BRR_BR9;        // гасим светодиод.
    }
}

Реализация задержек в программе

Простейшая функция для реализации задержки на заданное количество миллисекунд может быть такой:

// (!) VECT_TAB_SRAM - определить символ для отладки в RAM (!)

#include <stm32f10x.h>

// Счётчик миллисекунд.
volatile uint32_t msTicks=0;

// Обработчик прерывания от системного таймера
// подсчитывает прошедшее время в мс.
extern "C" void SysTick_Handler()
{
    msTicks++;
}

// Задержка на ms миллисекунд.
// Перед первым вызовом нужно настроить системный
// таймер на генерацию исключения каждую миллисекунду.
void delay(uint32_t ms)
{
    uint32_t t=msTicks+ms;
    while(t>msTicks);  // Ждём намеченного времени.
}

int main()
{
    if(SysTick_Config(SystemCoreClock/1000))
    {
        // Обработка ошибок.
    }
    // Остальной код...
}

Использование функции задержки позволяет существенно упростить код программы для "мигания" светодиодом.

// Цикл управления светодиодом.
while(true)
{
    // Зажигаем.
    GPIOC->BSRR=GPIO_BSRR_BS9;
    // Ждём.
    delay(LED_ON_TM);
    // Гасим.
    GPIOC->BRR=GPIO_BRR_BR9;
    // Ждём.
    delay(LED_OFF_TM);
}

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

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

Возможностей функции в этом варианте достаточно для переключения светодиода, но, например, для программного формирования сигнала на выходе (скажем, для реализации нестандартного интерфейса) функция непригодна.

Формирование задержки с высокой точностью

Далее приведён другой вариант функции задержки, в котором исключены названные выше недостатки функции delay. Функция делает задержку на заданное количество тактов процессора, что позволяет использовать её для формирования интервалов микросекундного диапазона при тактовой частоте микроконтроллера, начиная с единиц МГц.

//(!) VECT_TAB_SRAM - определить символ для отладки в RAM (!)

#include "stm32f10x.h"

extern "C" void SysTick_Handler()
{
}

// Калибровочная константа для функции задержки.
uint32_t UCALIBR=0;

// Задержка на n тактов процессора.
void ndelay(uint32_t n)
{
    // Начальное значение счётчика SysTick.
    uint32_t n0=SysTick->VAL;

    // Предыдущее значение счётчика.
    uint32_t np=n0;

    // Корректировка.
    if(n>UCALIBR)
        n-=UCALIBR;
    else
        return;

    // Текущее значение счётчика SysTick.
    int32_t nc;

    // Выполняем цикл до тех пор, пока не пройдёт
    // заданное количество тактов процессора.
    do{
        nc=SysTick->VAL;

        // Проверка на переполнение, корректировка
        // на модуль пересчёта в случае переполнения.
        if(nc>=np)
            n0+=SysTick->LOAD+1;
        np=nc;
    }while(n0-nc<n);
}

// Калибровка функции задержки.
// Необязательна, можно не вызывать и её код исключить.
void ncalibr()
{
    // Сохраняем значение регистра SysTick->LOAD.
    uint32_t load=SysTick->LOAD;
    // Устанавливаем достаточно большой модуль
    // пересчёта для счётчика системного таймера.
    SysTick->LOAD=0x00FFFFFF;
    // Сбрасываем счётчик системного таймера записью
    // в его регистр любого числа.
    SysTick->VAL=0;

    // Читаем регистр запрета исключений.
    bool primask=__get_PRIMASK();
    // Запрещаем исключения.
    __disable_irq();

    // Собственно калибровка функции ndelay.
    const uint32_t SOME_TICKS=500;
    uint32_t n0=SysTick->VAL;
    ndelay(SOME_TICKS);
    UCALIBR=n0-SysTick->VAL-SOME_TICKS;

    /*
       // Здесь можем добавить код для тестирования
       // точности функции, например, под отладчиком.
       n0=SysTick->VAL;
       ndelay(100);
       uint32_t tmp1=n0-SysTick->VAL;
       
       n0=SysTick->VAL;
       ndelay(1000);
       uint32_t tmp2=n0-SysTick->VAL;
       
       // После этого фрагмента можно поставить точку
       // остановки и посмотреть значения tmp1, tmp2, ...
       // Они должны быть близки к значениям аргументов
       // функции ndelay в соответствующих вызовах.
    */

    // Восстанавливаем конфигурацию системного
    // таймера и сбрасываем его счётчик.
    SysTick->LOAD=load;
    SysTick->VAL=0;
    // Восстанавливаем предыдущее состояние
    // регистра запрета исключений.
    __set_PRIMASK(primask);
}

// Способ пересчёта интервала в единицах времени в
// количество тактов процессора.
// Вариант для пересчёта во время компиляции (если 
// us задано константой).
#define CURRENT_CORE_CLOCK 24000000
#define USTON(us) (uint32_t)((us)/1.0e6*CURRENT_CORE_CLOCK)

int main()
{
    // Для использования функции ndelay требуется
    // включить системный таймер, частота генерации
    // исключений от таймера непринципиальна.
    if(SysTick_Config(SystemCoreClock/123))
    {
        // Обработка ошибок.
    }

    // Для увеличения точности задаваемых интервалов,
    // можно выполнить калибровку (но необязательно).
    ncalibr();

    // Для увеличения точности, во время формирования
    // задержки обработку исключений следует запрещать.
    __disable_irq();
    // Задержка на 1 секунду.
    ndelay(USTON(1000000));
    __enable_irq();

    // ...............
}

Функция ndelay не требует какой-то особой настройки системного таймера, достаточно просто его включить. Также она не требует никакого кода внутри обработчика исключения от системного таймера для своей работы. Может работать, когда запрещена обработка исключений и это даже предпочтительный вариант, если требуется высокая точность. При тактовой частоте микроконтроллера 24 МГц легко обеспечивает точность порядка 1 мкс. При некотором усовершенствовании кода функции, её точность можно довести до долей микросекунды.

Трудности в использовании функции ndelay вызывает необходимость задавать интервалы времени в тактах процессора. Если частота ядра и длительность интервала в данной точке программы известны на этапе компиляции, то особых проблем нет, например, можно использовать макрос вроде предложенного в примере USTON. Хуже, если хотя бы один из параметров является переменной. Тогда без вычислений во время выполнения не обойтись.

// Вариант 1.
// uint32_t us=...
uint32_t n=SystemCoreClock*us/1000000;

Этот вариант плох тем, что на частоте 24 МГц уже при us>178 мкс возникает переполнение целого во время умножения. Для того, чтобы переполнение целых не возникало, должно выполняться условие us<(2**32)/Fclk, где Fclk - тактовая частота процессора.

// Вариант 2.
// uint32_t us=...
uint32_t n=SystemCoreClock/1000000*us;

Этот вариант возможен только в том случае, когда гарантируется, что тактовая частота процессора выражается целым числом МГц. Иначе, при целочисленном делении будет допущена большая погрешность из-за отбрасывания дробной части. Величина us может достигать значения около 178000000 (178 с) при частоте ядра 24 МГц или us<(2**32)*1000000/Fclk, где Fclk - тактовая частота процессора.

// Вариант 3.
// uint32_t us=...
uint32_t n=SystemCoreClock/1e6*us;

Вариант 3 - наиболее универсальный. Не требуется знать частоту ядра и длительность интервала времени в момент компиляции, нет особых ограничений на эти величины (кроме того, что результат вычислений n должен помещаться в 32-битовое целое). Здесь для того чтобы избежать переполнения или потери точности из-за округления, происходит переход к действиям над числами с плавающей точкой. Такой вариант можно использовать только в крайнем случае, так он сильно увеличивает объём кода. Кроме того, действия над вещественными числами требуют много процессорного времени и если их проводить совместно с формированием временных интервалов, они будут вносить большую погрешность.

В приведённом примере использования ndelay применяется макрос USTON для перевода единиц времени в такты процессора. Там используются действия над вещественными числами, чтобы обеспечить максимальную общность применения. Если использовать макрос для констант, преобразование будет выполнено во время компиляции и код получится весьма эффективным - в коде останется просто вызов ndelay с аргументом-константой. В случае, когда длительность интервала константой не является, применять USTON следует осторожно - его раскрытие приведёт к вычислениям во время выполнения программы, аналогичным третьему варианту.

author: hamper; date: 2016-01-09; modified: 2016-02-26
  Рейтинг@Mail.ru