Как Вы знаете в STM32 много 16 битных таймеров. При этом их разрядности порой мало для получения тайм штампов. При этом 32-битных таймеров всего два: TIM2 и TIM5.

Таймер

Разрядность

Системная шина

Каналов компараторов

TIM1

16

APB2

4

TIM2

32

APB1

4

TIM3

16

APB1

4

TIM4

16

APB1

4

TIM5

32

APB1

4

TIM6

16

APB1

0

TIM7

16

APB1

0

TIM8

16

APB2

4

TIM9

16

APB2

2

TIM10

16

APB2

2

TIM11

16

APB2

2

TIM12

16

APB1

2

TIM13

16

APB1

2

TIM14

16

APB1

2

Определения

up-time - время с момента включения электропитания на электронную плату
тайм штамп - временная отметка. По сути это и есть up_time. Нужна для того, чтобы подписывать строчку в UART-Log-е загрузки прошивки.

В чем проблема?
Проблема в том, что в STM32 большинство таймеров обладают очень низкой разрядностью. Двенадцать таймеров 16 битные. Есть только два 32 битных таймера и они обычно чем-то заняты: моторы, счетчики импульсов и пр.

При этом большинство настоящих прошивок требуют возможности получать микросекундные тайм штампы. Это нужно для выдерживания пауз менее 1 ms, для импровизированных планировщиков на основе limiter-ов, для подписывания логов в UARTе и прочего. То есть нужен таймер, который увеличивается на +1 каждую одну микросекунду и не переполняется в обозримой перспективе.

Проблема с том что в случае 16 битного таймера такой таймер будет переполняться каждые 65 ms. Это ни о чем. Человек моргает и то продолжительнее. Переполнение за 65 ms - это плохо.

разрядность

Тактирование, MHz

Макс число

Переполнение,s

Переполнение,min

16

1

65536

0.065536

0.00109

32

1

4294967296

4294.96

71

64

1

18446744073709551616

18446744073709

307445734561

Постановка задачи.
Из двух 16 бит таймеров собрать один 32 битный таймер.

Таймер

Биты

Режим работы

PSC

Разрядность

TIM3

0...15

master

47

16

TIM9

16....31

slave

0

16

Реализация

Хорошая новость заключается в том, что микроконтроллеры STM32 позволяют собирать каскадом составные аппаратные таймеры. Схемотехнически это выглядит так.

Надо отметить, что в STM32 не всяким таймером можно тактировать конкретный аппаратный таймер (TIM9). Например Timer 9 может тактировать одним из 4х таймеров на выбор. Выбор осуществляется трёхбитовым битовым полем Trigger selection (TS) в регистре TIMx_SMCR

Ведомый таймер (TIM9)

Trigger selection

Trigger selection

Ведущий таймер \/

dec

bin

TIM2

0

0b00

TIM3

1

0b01

TIM10

2

0b10

TIM11

3

0b11

Для каждого аппаратного таймера есть отдельная таблица с реестром возможных ведущих таймеров.

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

Настройка ведущего таймера TIM3

--Подать тактирование на Timer3. В регистре RCC_APB1ENR установить бит 1.

--Активировать тактирование от внутренней шины
--В регистре TIMx_CR1 поле Clock division установить в ноль CKD=0. Пред делитель для тактовых импульсов.
--Переключить таймер3 в режим мастера. В регистре TIMx_CR2 в поле MMS ( Master mode selection ) прописать значение 2=0b10. Это для генерирования события Update. При переполнении таймера произойдет тактирование ведомого таймера. При переполнении ведущего таймера ведомый таймер увеличится на единицу.
--Прописать пред делитель, чтобы получилось инкрементирование каждый 1us. Значение пред делителя зависит от частоты тактирования таймера.
--Активировать таймер 3. Прописать в поле CEN=1 (Counter enable). После этого таймер начнет считать.
Сырые значения регистров выглядят так

-->tdrr 3
Base:0x40000400,Cnt:20
+-----+--------------------+-------+------+------------+------------+
|  N  |        Name        |offset | size |    Addr    |   ValHex   |
+-----+--------------------+-------+------+------------+------------+
|   1 |           TIMx_CR1 | 0x000 |    0 | 0x40000400 | 0x00000001 |
|   2 |           TIMx_CR2 | 0x004 |    4 | 0x40000404 | 0x00000020 |
|   3 |          TIMx_SMCR | 0x008 |    4 | 0x40000408 | 0x00000000 |
|   4 |          TIMx_DIER | 0x00c |    4 | 0x4000040c | 0x00000000 |
|   5 |            TIMx_SR | 0x010 |    4 | 0x40000410 | 0x0000001f |
|   6 |           TIMx_EGR | 0x014 |    4 | 0x40000414 | 0x00000000 |
|   7 |         TIMx_CCMR1 | 0x018 |    4 | 0x40000418 | 0x00000000 |
|   8 |         TIMx_CCMR2 | 0x01c |    4 | 0x4000041c | 0x00000000 |
|   9 |          TIMx_CCER | 0x020 |    4 | 0x40000420 | 0x00000000 |
|  10 |           TIMx_CNT | 0x024 |    4 | 0x40000424 | 0x00009731 |
|  11 |           TIMx_PSC | 0x028 |    4 | 0x40000428 | 0x0000002f |
|  12 |           TIMx_ARR | 0x02c |    4 | 0x4000042c | 0x0000ffff |
|  13 |           TIMx_RCR | 0x030 |    4 | 0x40000430 | 0x00000000 |
|  14 |          TIMx_CCR1 | 0x034 |    4 | 0x40000434 | 0x00000000 |
|  15 |          TIMx_CCR2 | 0x038 |    4 | 0x40000438 | 0x00000000 |
|  16 |          TIMx_CCR3 | 0x03c |    4 | 0x4000043c | 0x00000000 |
|  17 |          TIMx_CCR4 | 0x040 |    4 | 0x40000440 | 0x00000000 |
|  18 |          TIMx_BDTR | 0x044 |    4 | 0x40000444 | 0x00000000 |
|  19 |           TIMx_DCR | 0x048 |    4 | 0x40000448 | 0x00000000 |
|  20 |          TIMx_DMAR | 0x04c |    4 | 0x4000044c | 0x00000001 |
+-----+--------------------+-------+------+------------+------------+

Настройка ведомого таймера TIM9

--Подать тактирование на Timer9. В регистре RCC_APB2ENR установить в 1 бит 16 (RCC_APB2ENR_TIM9EN). Это нужно чтобы появилась возможность прописывать регистры аппаратного таймера. Без активации тактирования настройки не применятся.

-- В регистре TIMx_CR1 поле Clock division установить в ноль CKD=0. Пред делитель для тактовых импульсов.

TIMx_CR1
TIMx_CR1

--В регистре TIMx_SMCR (slave mode control register) поле Slave mode selection установить в SMS=7 ( External Clock mode 1 - Rising edges of the selected trigger (TRGI) clock the counter. )
--В регистре TIMx_SMCR поле Trigger selection установить в значение TS=1-(Internal Trigger 1 (ITR1)) . После инициализации в регистре TIMx_SMCR должно оказаться значение 0x17 = 0b1_0111.

TIMx_SMCR
TIMx_SMCR

--Активировать таймер 9. Прописать в поле CEN=1 (Counter enable). После этого таймер начнет считать.

-->tdrr 9
Base:0x40014000,Cnt:20
+-----+--------------------+-------+------+------------+------------+
|  N  |        Name        |offset | size |    Addr    |   ValHex   | 
+-----+--------------------+-------+------+------------+------------+
|   1 |           TIMx_CR1 | 0x000 |    0 | 0x40014000 | 0x00000001 |
|   2 |           TIMx_CR2 | 0x004 |    4 | 0x40014004 | 0x00000000 |
|   3 |          TIMx_SMCR | 0x008 |    4 | 0x40014008 | 0x00000017 |
|   4 |          TIMx_DIER | 0x00c |    4 | 0x4001400c | 0x00000000 |
|   5 |            TIMx_SR | 0x010 |    4 | 0x40014010 | 0x00000040 |
|   6 |           TIMx_EGR | 0x014 |    4 | 0x40014014 | 0x00000000 |
|   7 |         TIMx_CCMR1 | 0x018 |    4 | 0x40014018 | 0x00000000 |
|   8 |         TIMx_CCMR2 | 0x01c |    4 | 0x4001401c | 0x00000000 |
|   9 |          TIMx_CCER | 0x020 |    4 | 0x40014020 | 0x00000000 |
|  10 |           TIMx_CNT | 0x024 |    4 | 0x40014024 | 0x00003eab |
|  11 |           TIMx_PSC | 0x028 |    4 | 0x40014028 | 0x00000000 |
|  12 |           TIMx_ARR | 0x02c |    4 | 0x4001402c | 0x0000ffff |
|  13 |           TIMx_RCR | 0x030 |    4 | 0x40014030 | 0x00000000 |
|  14 |          TIMx_CCR1 | 0x034 |    4 | 0x40014034 | 0x00000000 |
|  15 |          TIMx_CCR2 | 0x038 |    4 | 0x40014038 | 0x00000000 |
|  16 |          TIMx_CCR3 | 0x03c |    4 | 0x4001403c | 0x00000000 |
|  17 |          TIMx_CCR4 | 0x040 |    4 | 0x40014040 | 0x00000000 |
|  18 |          TIMx_BDTR | 0x044 |    4 | 0x40014044 | 0x00000000 |
|  19 |           TIMx_DCR | 0x048 |    4 | 0x40014048 | 0x00000000 |
|  20 |          TIMx_DMAR | 0x04c |    4 | 0x4001404c | 0x00000000 |
+-----+--------------------+-------+------+------------+------------+

Теперь каждый раз, когда надо получить тайм штамп можно просто прочитать два 16 бит регистра и скомпоновать одно 32битное значение. Однако один момент. TIM9->CNT это volatile переменная. Мы можем попасть на момент обновления регистра CNT. Поэтому надо выполнять как минимум два чтения. Убедиться, что ведомый таймер TIM9->CNT за стабилизировался и только после этого уже компоновать финальный тайм штамп.


uint32_t time_stamp_get_us( void ) {
    uint32_t tim_word_lo = 0x0000; 
    uint32_t tim_word_hi = 0x0000;
    do {
        tim_word_lo = TIM3->CNT;
        tim_word_hi = TIM9->CNT;
    } while( tim_word_hi != TIM9->CNT );
    uint32_t timestamp_dword = (tim_word_hi<<16) | tim_word_lo;
    return timestamp_dword;
}

Достоинства

1) Не происходят частые прерывания по таймеру и, как следствие, не тормозится основная прошивка.
2) Бесхозные 16ти битные таймеры внезапно начинают приносить реальную пользу.
3) Появляется еще один 32 битный таймер буквально из неоткуда.

Недостатки

1) После 72ой минуты 32 таймер переполнится и начнет считать заново. Можно генерировать прерывания и по slave таймеру, умножать счетчик прерываний на 0xFFFFFFFF и прибавлять cnt32. В результате получится уже тайм штамп типа uint64_t.

Итог
Удалось собрать 32 битный таймер буквально из подручных материалов. Каскадирование таймеров может быть хорошим подспорьем в решении прикладных задач прошивки. Это открывает дорогу для генерирования полноценных тайм штампов в прошивках на STM32.

Вопросы

--Можно ли на STM32 собрать из четырех 16 битных таймеров собрать один 64 битный таймер? Может ли таймер работать одновременно в режиме master и slave?

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


  1. Am0k-HABR
    07.01.2026 17:42

    --Почему в STM32 нет таймеров TIM6, TIM7, TIM10 и TIM11? При этом есть TIM8, TIM9 и TIM12.

    Правда ? Наверное, стоит хотя бы мельком обозначить серию контроллеров, о которой идет речь ?

    А тут они есть.
    А тут они есть.


  1. Albert2009Zi
    07.01.2026 17:42

    А зачем так сложно? Нельзя просто взять, да хоть uint64_t переменную счетчик и инкрементировать её в обработчике прерываний по переполнению тех же 16 битных таймеров? А зависимый функционал будет плясать от значения такого счетчика.

    Вот так, например:

    volatile uint64_t tim3Tick64 = 0;
    
    void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
                if (htim->Instance == TIM3){
                       tim3Tick64 ++;
                }
      }


    1. danil_12345 Автор
      07.01.2026 17:42

      в обработчике прерываний по переполнению тех же 16 битных таймеров? А зависимый функционал будет плясать от значения такого счетчика.

      Прерывания каждую микросекунду?
      Приложение будет тормозить.


      1. Flammmable
        07.01.2026 17:42

        Прерывания каждую микросекунду?

        Прерывания каждые 64 миллисекунды.


        1. danil_12345 Автор
          07.01.2026 17:42

          А я предлагаю каскадный 32бит таймер, который вообще прерывания не производит.


        1. kajdarov
          07.01.2026 17:42

          Такой способ требует определенного подхода для получения корректных значений на стыке переполнения.

          Допустим, мы решили прочитать значение составного программно-аппаратного таймера.

          1. Запрещаем срабатывание прерывания таймера, а лучше вообще все прерывания.

          2. Читаем аппаратную часть

          3. Читаем флаг переполнения аппаратного таймера

          4. Если флаг сброшен - читаем программную часть, разрешаем прерывание и выходим

          5. Снова читаем аппаратную часть. Читаем программную, прибавляем единицу к программной части (именно к результату чтения), разрешаем прерывание и выходим.

          Такой алгоритм позволит устранить коллизии на стыке переполнения аппаратного таймера

          Не получится использовать systick timer для этого, у него нет флага.


          1. kajdarov
            07.01.2026 17:42

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


          1. misha_ruchk0
            07.01.2026 17:42

            для SysTick таймера можно использовать немгого другой скелет алгоритма:

            1. прочитать значения счетчика прерываний в ячейке памяти

            2. прочитать занчение из регистра таймера

            3. убедиться, что значени в ячейке к этому моменту не изменилось

            4. если изменилось, то, в простейшем случае, повторить с пункта 1.


            1. kajdarov
              07.01.2026 17:42

              Такой вариант требует обязательного наличия работающего неперекрытого прерывания, иначе конфликты могут быть, если прерывание не сработало между п. 1 и п. 3.


              1. misha_ruchk0
                07.01.2026 17:42

                Если прерывание не сработало между пунктом 1 и 3 у Вас в руках уже количество тиков системного таймера от начала времен и текущее значение регистра таймера считающего периоды установленной Вами частоты заполнения системного (SysTick) таймера.


      1. Albert2009Zi
        07.01.2026 17:42

        Так на единицы микросекунд вам 16битного и даже 8битного таймера за глаза должно хватить. А 1000мкс это уже миллисекунды, где здесь 32 бита? Просто настройте таймер правильно. А на миллисекундах, если прерываться, то ничего тормозить не должно. Или я чего-то не догоняю?

        P.s. а ну или Вы ставите задачу добывать значение счетчика только таким способом

        tim_word_hi = TIM9->CNT;

        и не прерываться от таймера вообще? (это я из Вашего ответа на коммент выше понял)


    1. misha_ruchk0
      07.01.2026 17:42

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


      1. Albert2009Zi
        07.01.2026 17:42

        Несомненно полезная техника. Я автору, спасибо за статью забыл сказать. Исправляюсь.


    1. mctMaks
      07.01.2026 17:42

      Проблемы навскидку данного примера кода:

      1. слишком частые прерывания будут в системе;

      2. нарушение атомарности

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

      и да, на ARM Cortex M критическая секция не гарантирует что инструкции внутри нее будут выполнены в правильном порядке, поэтому нужны ещё барьерные инструкции.

      tim3Tick64 ++;

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

      это не только в документации на ядро описано, но и реальный баг который я неделю отлавливал у себя.


      1. Albert2009Zi
        07.01.2026 17:42

        Я взял самый простой пример навскидку (uint64_t просто сферически большое значение счетчика в вакууме) чтобы повысить свою эрудицию и увидеть картину шире. А так весь HAL дико неатомарен, это само собой.


      1. d_nine
        07.01.2026 17:42

        Разве при выходе из прерывания в Cortex-M не выполняется обязательный неявный __DSB()?


        1. mctMaks
          07.01.2026 17:42

          По дизассемблеру - нет. Компилятор использовал от segger, который явно сделан на основе arm clang.

          На том же stackoverflow есть несколько тем посвященных dsb и dmb.

          А ещё книжка хорошая есть по ядру М3 и М4 от (имя забыл, Янг кажется). Плюс спеку на ядро полезно посмотреть. Там как раз описывается зачем эти инструкции нужны и область их применения.


          1. d_nine
            07.01.2026 17:42

            Просто именно на M3/M4 я с этим проблем обычно не имел (в Keil и CubeIDE с GCC), а вот при порте на M7 уже пришлось ручками барьеры вешать, но там все работой кеша объясняется. Надо посмотреть по дизассемблеру, мне казалось для всех исключений (т.е. не только прерываний) на выходе автоматом встаёт барьер.


      1. kajdarov
        07.01.2026 17:42

        А переменную объявляли как volatile? Это может быть не проблемой ядра cortex-m а перестановкой вычислений компилятором. Барьеры памяти тоже решают проблему


        1. mctMaks
          07.01.2026 17:42

          Конечно. И статик и volatile вешал.

          Я делал свой таймер, с коррекцией типа под мастера. Спасение было в расстановке барьеров.

          Вот тут нюанс описывается


          1. kajdarov
            07.01.2026 17:42

            В указанном примере хоть flag использует volatile, но buffer просто char. И именно в этом проблема, что компилятор не видит причины сперва поставить флаг а потом записать в буфер. Только две переменные volatile не меняются между собой. Так что данный пример некорректный и имеет массу косяков. Правильно использовать барьер между записью в буфер и установкой флагов. Тогда не потребуется и запрет прерываний делать

            volatile int flag=0; 
            char buffer[10]; 
            void foo(char c)
            {
                buffer[0]=c;
                __ASM volatile ("" : : : "memory");
                flag=1;
            }

            Вот такой код без dmb и без запрета прерываний с использованием пустого барьера памяти будет работать не хуже

            Еще можно использовать

            __sync_synchronize()

            Вроде тоже как dmb. Поддерживается компиляторами clang и gcc


            1. kajdarov
              07.01.2026 17:42

              Dmb и dsb полезны только при использовании Кеша в многоядерной архитектуре. Если нет ни того ни другого, то использование их не имеет преимущества. Лучше пустой барьер (фактически, на него не тратятся инструкции выполнения, это чисто руководство для компилятора, что все что до и после этого барьера - не перетасовываем)


              1. mctMaks
                07.01.2026 17:42

                Возможно, но тогда какой смысл их вводить и описывать для простых ядер типа М3 и М4? Я согласен с тем что надо понимать когда они действительно работают, а когда нет.

                В моем понимании, если у переменной (64 бита например) обращается кто-то из кода и в этот же момент приходит прерывание в котором эта же переменная обновляется, то очень с большой долей вероятности можно получить некорректное значение переменной. Отсюда и возникает необходимость в DMB, которая заставит конвейер выполнить все операции с памятью до этой инструкции:

                Команда DМВ используется в качестве барьера памяти данных. Она rарантирует, что все обращения к памяти) явно указанные до вызова команды DMB будут выполнены до Toro, как начнут выполняться явные обращения к памяти) появляющиеся после команды DМВ. Команда DМВ не влияет на порядок или выполнение

                Причем в тестах, DMB именно что следовало располагать в прерывании таймера. (Я делал составной таймер, правда на nrf52840, но в данном случае речь по то же самое ядро М4. Сложность была в том, что мастер периодически делал коррекцию тика через алгоритм синхронизации. Поэтому в прерывании нужно было считать как целые, так и доли секунд. Проблема была именно в атомарном доступе к переменной, которая средствами языка не решилась. Неделя ушла на то, чтобы научиться получать эту Ситуацию стабильно и примерно месяц тестов чтобы убедится в полноценном решении.

                Dsb в этом плане для аппаратных регистров подходит. Она нужна чтобы предыдущая команда была исполнена сразу. В применении к таймера - когда нужно забрать значение и тут же обнулить таймер. Проблемная ситуация: кто-то влезает между этими действиями. Вендор рекомендует после записи делать "volatile void" чтение. Документация на ядро - DSB.

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

                Edit: да, если параноить дальше, то ещё REX (исклюзианый доступ) надо пользовать. Эти инструкции позволят оценить что при обращении к ячейке памяти никто больше туда не влезал. Но это уже отдельная тема, которую я совсем поверхностно смотрел


                1. kajdarov
                  07.01.2026 17:42

                  В моем понимании, если у переменной (64 бита например) обращается кто-то из кода и в этот же момент приходит прерывание в котором эта же переменная обновляется, то очень с большой долей вероятности можно получить некорректное значение переменной. Отсюда и возникает необходимость в DMB, которая заставит конвейер выполнить все операции с памятью до этой инструкции:

                  Прерывание не прерывает выполнение ассемблерной инструкции посередине. Потому что инструкция атомарна. Если 64-х битная операция "а++" неатомарна, то никакая dmb dsb не поможет, если возникнет прерывание в процессе вычисления "а".

                  Когда срабатывает прерывание, то в процессоре вообще много чего происходит:

                  Во-первых, дожидаемся выполнения текущей инструкции, после чего сбрасывается конвеер команд, во-вторых сохраняется в стек (читай, в память) весь контекст (все регистры). Потом читается адрес PC из таблицы векторов (тоже не мало обращений к памяти), потом снова загрузка конвейера. Так что dmb dsb тут и рядом не стоит. Все операции с памятью, которые должны были выполниться перед прерыванием выполнятся ДО начала процесса прерывания в любом случае.

                  По поводу того, зачем dmb dsb в одноядерных приложениях: ну кеш никто не отменял, особенно в купе с использованием с DMA (DMA не в курсе про кеш, он работает с внешней памятью только). Ну и второе: из песни слов не выкинешь. Так и из ядра, разработанного на все случаи жизни


  1. Flammmable
    07.01.2026 17:42

    up-time - время с момента включения питания на плату
    тайм штамп - временная отметка. По сути это и есть up_time

    А чем вам не нравится модуль RTC с субсекундным разрешением, который встроен во все STM32?


    1. Antares1991
      07.01.2026 17:42

      Ну попробуйте его заставить микросекунды считать. В статье как раз этот кейс описывается


      1. Flammmable
        07.01.2026 17:42

        Ну попробуйте его заставить микросекунды считать

        Зачем его заставлять? Он сам их считает.

        В статье как раз этот кейс описывается

        В статье нет ни слова про RTC.


        1. Antares1991
          07.01.2026 17:42

          Ну попробуйте его на мегагерце хотя бы запустить. Далеко не на каждом МК его можно сконфигурировать на тактирование от высокоскоростных источников: из того что я видел - только от HSE, и (у большинства моделей, с которыми имел дело) с предделителем на 32 или 128, при том что максимальная частота HSE меньше.
          В статье описывается кейс микросекундных таймстампов, а не RTC, как я и писал выше.


          1. Flammmable
            07.01.2026 17:42

            Ну попробуйте его на мегагерце хотя бы запустить.

            Выберите себе обидное прозвище, которым я буду вас называть, если я попробую и у меня получится запустить его на мегагерце. Тогда продолжим дискуссию :))


            1. Antares1991
              07.01.2026 17:42

              Если мы с Вами не в 1 комнате, то можете меня даже бить. Давайте, запустите RTC на 1МГц на, например, STM32F103C8. Только без читов недокументированных возможностей, нам повторяемость в промышленных масштабах нужна. Вот Вам ещё для затравки схема тактирования из рефмануала на микроконтроллер. Её должно хватить, чтобы остудить Ваш пыл.


  1. goldexer
    07.01.2026 17:42

    Угу, в HAL иногда не только атомарностью не пахнет, а вообще хоть сколько-нибудь вменяемым временем выполнения для базовых вещей. В таймер новое значение закинуть там такая портянка тянется... вот потому автор и пользует регистры напрямую. Да, приходилось таймеры сцепливать. После AVRок возможности STMок читались в документации с восторгом - зачем городить какие-то костыли, если контроллер позволяет аппаратно прокладывать целые линии, цепляя один модуль к другому - таймеры и ШИМ на GPIO, АЦП, DMA, UART. Только таблицы приходится смотреть что с чем можно цеплять, а что нет. Кстати, чёт действительно мало видел случаев, чтоб RTC заводили!


    1. yappari
      07.01.2026 17:42

      Так и у автора атомарностью не особо пахнет, несмотря на регистры. В зависимости от юзкейсов, могут прилетать интересные спецэффекты при использовании таких "сцепленных" конструкций. Опыт работы с AVR мог помочь, но, видимо, оно обошло автора стороной.


  1. ptr128
    07.01.2026 17:42

    Идея здравая, плюсую. Но хотелось бы узнать, в каких случаях 20 прерываний в секунду (каждые 50 мс), могут оказать критическое влияние на работу устройства?


  1. alcotel
    07.01.2026 17:42

    А что будет, если при чтении попадëм на обновление слейв-таймера?

    tim_word_hi = TIM9->CNT;

    tim_word_lo = TIM3->CNT;

    Это же не атомарное чтение одного 32-битного регистра, а всë-таки двух независимых 16-битных. Как защитить целостность данных?


    1. danil_12345 Автор
      07.01.2026 17:42

      Обновление tim_word_hi происходит раз в 65 ms.
      Я бы предпочел ничего не делать.
      На следующей итерации прочитается уже корректное значение.


      1. alcotel
        07.01.2026 17:42

        Но как-то ведь надо отличить некорректное значение от корректного. А раз прошивка работает достаточно долго, такой кейс рано или поздно случится.


        1. danil_12345 Автор
          07.01.2026 17:42

          А что будет, если при чтении попадëм на обновление слейв-таймера?

          Я видел такое решение

          uint32_t time_stamp_get_us( void ) {
              uint32_t tim_word_lo = 0x0000; 
              uint32_t tim_word_hi = 0x0000;
              do {
                  tim_word_lo = TIM3->CNT;
                  tim_word_hi = TIM9->CNT;
              } while( tim_word_hi != TIM9->CNT );
              uint32_t timestamp_dword=(tim_word_hi<<16)|tim_word_lo;
              return timestamp_dword;
          }

          Однако почему так сделано - затрудняюсь пояснить.


    1. ptr128
      07.01.2026 17:42

      1. Читаем старшие 16 бит.

      2. Читаем младшие 16 бит.

      3. Снова читаем старшие 16 бит.

      4. Если п.1<>п.3 и п.2<32768 используем значение из п.1. Во всех остальных случаях - из п.3.

      Может сработать некорректно, если между п.1 и п.3 прошло более 32 мс, что уж очень маловероятно.


      1. danil_12345 Автор
        07.01.2026 17:42

        Если п.1<>п.3

        Что значит оператор "<>" ?


        1. ptr128
          07.01.2026 17:42

          Не равно. Для C это !=

          P.S. Извиняюсь, что не уточнил. Последнее время у меня очень много кода, где <> встречается чаще, чем !=


    1. kajdarov
      07.01.2026 17:42

      В стм в таком режиме как раз 16 бит старшего отображается в старших битах младшего таймера. Как-то так. Получается одно 32х битное число. Если конечно мне память не изменяет. Т.е. там все схвачено

      Еще есть вариант, что при вычитке младшего таймера значение старшего буферизируется ( так сделано в AVR по-моему). То есть нужно просто прочитать в правильной последовательности слачало значение ведомого, потом ведущего таймера


      1. yappari
        07.01.2026 17:42

        Это где вы нашли про отображение старшего в младшем? Такого нет ни в STM32, и не было в AVR.


      1. alcotel
        07.01.2026 17:42

        Вы, наверное, имеете в виду теневой регистр для чтения длинного значения 8-разрядным мк. Внутри одного таймера - видел такое. Между разными таймерами - не видел.

        Более того, таймеры в статье находятся в разных тактовых доменах. И появляется задержка на синхронизацию между ними в несколько тактов.


  1. Aheir
    07.01.2026 17:42

    Если я все правильно понимаю, в итоге двумя таймерами мы можем посчитать 2^32 микросекунд, т.е. примерно 4300 секунд, что немногим более часа. А если устройство ну хотя бы месяцами работает без перерывов, то для чего такой timestamp?


    1. danil_12345 Автор
      07.01.2026 17:42

      Да. Это классическая "ошибка 71ой минуты".

      >>> ((10**(-6))*(2**32))/60
      71.58278826666667

      Что делать - не знаю.
      Переходить на RISC-V.
      Там 64битный systick встроен прямо в ядро!


    1. danil_12345 Автор
      07.01.2026 17:42

      После 72ой минуты 32 таймер переполнится и начнет считать заново. Можно генерировать прерывания и по slave таймеру, умножать счетчик прерываний на 0xFFFFFFFF и прибавлять cnt32. В результате получится уже тайм штамп типа uint64_t.


    1. ptr128
      07.01.2026 17:42

      Насколько я понял, описываемое решение применяется в случаях, когда 16 или более прерываний в секунду (раз в 64 мс) - это слишком часто. Я с такими случаями не сталкивался, но исключить их возможность не могу.

      Прерывания раз в час (или раз в 71 минуту) уж точно навредить не могут, поэтому просто старшие 32 бита счетчика храним в памяти и оперируем 64-битным значением, которое за время существования нашего МК точно не переполнится.

      Если же прерывания по переполнению таймера раз в 64 мс допустимы, то можно использовать 16-битный таймер и тоже 32 бита счетчика в памяти. 48 бит хватит лет на шесть, так что раз в 5-6 лет надо будет перезагружаться, что выглядит некритичным ограничением.