В этом тексте я покажу, что можно сделать, если у вас закончились все аппаратные таймеры в микроконтроллере. Также Вы узнаете как из 32-битного таймера сделать 64-битный таймер.

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

В ARM Cortex-M3 процессорах и старше помимо SysTick есть еще один 32 битный таймер по имени DWT ( Data Watchpoint and Trace unit). Этот таймер увеличивается на +1 каждый тик ядра. Если вы работаете на частоте 144MHz, то инкрементация будет происходить каждые 6,94 ns. Как же воспользоваться этим таймером?

Физический адрес регистров таймера начинается с 0xE0001000. Первым делом надо проинициализировать DWT. Как видите, я специально сделал возможность определять несколько экземпляров таймера, так как существуют еще и трёх ядерные микроконтроллеры.

/*If not()  */
#define ifn(CONDITION)    if(!(CONDITION))

/*   ISO-26262 require verify configuration data  */
bool DwtIsValidConfig(const DwtConfig_t* const Config) {
    bool res = false;
    if(Config) {
        res = true;
        ifn(Config->DWTx) {
            LOG_ERROR(LG_DWT, "DWT%u,DWTx,Err", Config->num);
            res = false;
        }
    }
    return res;
}

bool dwt_init_common(const DwtConfig_t* const Config, DwtHandle_t* const Node) {
    bool res = false;
    if(Config) {
        if(Node) {
            Node->name = Config->name;
            Node->DWTx = Config->DWTx;
            Node->num = Config->num;
            Node->counter_freq = Config->counter_freq;
            Node->valid = true;
            res = true;
        }
    }
    return res;
}

/* Table C1-24 DWT_CTRL (0xE0001000) */
typedef union{
    uint32_t dword;
    struct{
        uint32_t CYCCNTENA:1;      /*[0]  Enable CYCCNT */
        uint32_t POSTPRESET:4; /*[4:1]  Preset (reload) value for POSTCNT */
        uint32_t POSTCNT:4; /*[8:5]  xxxxxxxxxxxxxx */
        uint32_t CYCTAP:1; /*[9]  Selects a tap on the DWT_CYCCNT register */
        uint32_t SYNCTAP:2; /*[11:10]  Selects a synchronization packet rate. */
        uint32_t PCSAMPLENA:1; /*[12]  See CYCEVTENA */
        uint32_t RES1:3;// 13 14 15
        uint32_t EXCTRCENA:1; /*[16]  Enables exception trace. */
        uint32_t CPIEVTENA:1; /* [17] Enables CPI count event. */
        uint32_t EXCEVTENA:1; /*[18]  Enables Exception Overhead event */
        uint32_t SLEEPEVTENA:1; /*[19]  Enables Sleep count event. */
        uint32_t LSUEVTENA:1; /*[20]  Enables LSU count event. */
        uint32_t FOLDEVTENA:1; /*[21]  Enables Folded-instruction count event */
        uint32_t CYCEVTENA:1; /*[22]  Used with PCSAMPLENA to control CYCCNT or PC sample event generation. */
        uint32_t RES2:1; /*[23 ]  xxxxxxxxxxxxxx */
        uint32_t NOPRFCNT:1; /*[24]  When set, DWT_FOLDCNT, DWT_LSUCNT, DWT_SLEEPCNT, DWT_EXCCNT, and DWT_CPICNT are not supported */
        uint32_t NOCYCCNTd:1; /*[25]  When set, DWT_CYCCNT is not supported */
        uint32_t NOEXTTRIGc:1; /*[26]  When set, no CMPMATCH[N] support */
        uint32_t NOTRCPKTb:1; /*[27]  When set, trace sampling and exception tracing are not supported */
        uint32_t NUMCOMP:4; /*[31:28]  Number of comparators available. */
    };
}ARM_DWT_CTRL_t;

bool dwt_init_one(uint8_t num) {
    bool res = false;
    const DwtConfig_t* Config = DwtGetConfig(num);
    if(Config) {
        res = DwtIsValidConfig(Config);
        if(res) {
            LOG_WARNING(LG_DWT, "%s", DwtConfigToStr(Config));
            DwtHandle_t* Node = DwtGetNode(num);
            if(Node) {
                res = dwt_init_common(Config, Node);
                Node->DWTx->CYCCNT = 0;
                Node->counter_freq =  clock_core_freq_get();
                LOG_INFO(LG_DWT, "CPUFreq:%u Hz",  Node->counter_freq );
                Node->divider_1us = Node->counter_freq/1000000UL;
                Node->divider_1ms = Node->counter_freq/1000UL;
                ARM_DWT_CTRL_t DWT_CTRL;
                DWT_CTRL.dword = Node->DWTx->CTRL;
                DWT_CTRL.CYCCNTENA = 1;
                Node->DWTx->CTRL = DWT_CTRL.dword; /* enable the counter */
                Node->valid = true;
                Node->init = true;
            } 
        } 
    } 
    return res;
}

Главный вопрос: как быть с тем, что DWT таймер переполняется? На частоте 168MHz переполнение будет происходить каждые 25.56 с. Согласитесь, мало пользы от таймера, который считает только до 25 с. При этом на этом таймере у нас нет прерывания, которое срабатывает при переполнении, как в случае с аппаратными таймерами или SysTick. Ответ прост. Придется вручную следить за переполнением программным образом. Придется периодически опрашивать DWT таймер с периодом меньшим, чем период переполнения, например каждые 5 сек. То есть вызывать функцию dwt_get_run_time_counter_u64. Благо у меня уже есть кооперативный планировщик, который позволяет вызывать функции с заданным в конфиге периодом.

bool dwt_proc_one(uint8_t num) {
    bool res = false;
    DwtHandle_t* Node = DwtGetNode(num);
    if(Node) {
        Node->spin++;
        dwt_get_run_time_counter_u64(num);
        LOG_PARN(LG_DWT, "DWT_%u,Spin:%u,Proc", num, Node->spin);
    }
    return res;
}

Функция dwt_get_run_time_counter_u64 не просто читает значение счетчика из регистров, она выявляет факт переполнения этого счетчика, сравнивая текущие показания с предыдущим значением. Если нарушена монотонность, значит с момент прошлого измерения upTime случилось переполнение. В случае переполнения в финальную переменную up_time надо прибавить константу (0xFFFFFFFF+1). Вот так.

uint64_t dwt_get_run_time_counter_u64(uint8_t num) {
    uint64_t up_time_u64 = 0;
    DwtHandle_t* Node = DwtGetNode(num);
    if(Node) {
        enter_critical();
        Node->up_time_u32 = Node->DWTx->CYCCNT;
        if(Node->up_time_u32 < Node->up_time_u32_prev) {
            Node->wrap_counter += DWT_U32_OVERFLOW_VALUE;
        }
        Node->up_time_u32_prev = Node->up_time_u32;
        exit_critical();
        Node->up_time_u64 = Node->wrap_counter | Node->up_time_u32;
        up_time_u64 = Node->up_time_u64;
    }
    return up_time_u64;
}

Теперь, благодаря работе с 64-битным счетчиком можно считать вплоть до 1,09e11 секунд. Это, к слову, 3454 лет. Теперь можно вообще не беспокоиться за переполнение таймера.

Так как работа с типом uint64_t не является атомарной операцией, то придется на время склеивания универсального upTime значения временной войти в критическую секцию.

bool isFromInterrupt(void) {
    bool res = false;
    res = ((SCB->ICSR & SCB_ICSR_VECTACTIVE_Msk) != 0);
    return res;
}

void enter_critical(void) {
    if(!isFromInterrupt()) {
        if(critical_nesting_level == 0) {
            disable_interrupt();
        }
        critical_nesting_level++;
    }
}

void exit_critical(void) {
    if(!isFromInterrupt()) {
        if(critical_nesting_level) {
            critical_nesting_level--;
            if(critical_nesting_level == 0) {
                enable_interrupt();
            }
        }
    }
}

Осталось только преобразовать тики таймера в удобные человеко-читаемый параметр: секунды. Интерес представляют обычно миллисекунды (для логов) и микросекунды (для планировщика). Пред-делители лучше заранее вычислить в функции dwt_init, так как функция get_time_stamp скорее всего будет вызыватьcя очень и очень часто (hi-load процедура).


uint32_t dwt_get_time_ms32(uint8_t num){
    uint32_t time_ms32=0;
    DwtHandle_t* Node = DwtGetNode(num);
    if(Node) {
        uint64_t counter_u64 = dwt_get_run_time_counter_u64(num);
        time_ms32 =(uint32_t) (counter_u64 /Node->divider_1ms);
    }
    return time_ms32;
}

uint64_t dwt_get_time_us64(uint8_t num){
    uint64_t time_us64=0;
    DwtHandle_t* Node = DwtGetNode(num);
    if(Node) {
        uint64_t counter_u64 = dwt_get_run_time_counter_u64(num);
        time_us64 = (counter_u64 /Node->divider_1us);
    }
    return time_us64;
}

Отладка

Вот я собрал прошивку и запустил DWT таймер на частоте ядра 144 MHz. На момет 26 мин 30 сек с момента старта DWT насчитал как раз 1590490 ms = 26.5 min. Значит таймер работает.

В микроконтроллерах есть огромный калейдоскоп всяческих источников для чтения набежавшего времени. Это SysTick, модуль внутреннего RTC, внешний RTC на I2C шине, дюжина аппаратных таймеров, каскадные таймеры и DWT. При корректной работе все они должны показывать приблизительно одинаковую циферку, как тут upTime=43 мин 5 с.

Достоинства DWT

1++Переносимость кода. Тот же самый код драйвера DWT таймера вы сможете пере использовать на ARM Cortex-M процессоре любого вендора: STM32, Eliot, MDR32, NRF53, YunTu, Artery, CC26x6 и прочее.
2++ Простота настройки. Надо сконфигурировать только один регистр. По факту, прописать требуется только один бит. После чего таймер побежит. Это намного проще, чем копаться в дюжине регистров аппаратных таймеров и вкуривать спеку.
3++ Высочайшее разрешение. DWT можно смело называть бешеным таймером. На частоте ядра 168 MHz Вы можете измерять временные интервалы вплоть до 5,9 ns. За это время свет проходит всего 1.7 метра. Порой невозможно тактировать аппаратные таймеры на такой высокой частоте, a DWT - можно.

Недостатки DWT

1--В DWT нет регистра пред делителя, как у аппаратных таймеров или SysTick. В связи с этим для конвертации в микросекунды каждый раз надо делать программное деление на константу. Часто оптимальнее настроить аппаратный таймер и можно уже спокойно атомарно считывать из регистра TIM2->CNT чистые микросекунды.
2--Низкая разрядность 32 бит. Всё таки хотелось бы получить 64 битный UpTime регистр, как в RISC-V процессорах.

Итог

Удалось научиться пользоваться DWT таймером. Удалось научиться измерять upTime с точностью до микросекунд, при этом не беспокоясь за переполнение.

Как видите, в ARM Cortex-M процессорах всегда можно воспользоваться отдельным независимым аппаратным счетчиком DWT.

Словарь

Сокращение

Расшифровка

DWT

Data Watchpoint and Trace unit

ARM

Advanced RISC Machine

RISC

reduced instruction set computer

Ссылки

Название

URL

Счётчик DWT

https://habr.com/ru/articles/476582/?ysclid=mmi5ru5yly716855972

Cycle Counting on ARM Cortex-M with DWT

https://mcuoneclipse.com/2017/01/30/cycle-counting-on-arm-cortex-m-with-dwt/

Диспетчер Задач для Микроконтроллера

https://habr.com/ru/articles/757000/

DWT - Data Watchpoint and Trace unit

https://arm-stm.blogspot.com/2014/05/dwt-data-watchpoint-and-trace-unit.html

Вопросы

1--Как из 32-битного таймера сделать 64-битный таймер?
2--Что делать, если все аппаратные таймеры на микроконтроллере уже закончились?

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Вы запускали DWT Таймер?
56.52%Да13
43.48%нет10
Проголосовали 23 пользователя. Воздержались 2 пользователя.