
Иногда в работе инженера кажется, что текущая задача является уникальной, что с такой проблемой никто ни разу не сталкивался. Соответственно, решение должно быть таким же уникальным.
Однако при более пристальном взгляде зачастую оказывается, что задача не просто не уникальна, она сама по себе является представителем некоего класса задач, для которого выработано общее типовое решение.
Например, вам может потребоваться измерять наработку различных подвижных узлов оборудования и для этого нужно фиксировать время начала и конца движения каждого из узлов.
Или при работе операторов с оборудованием применяется некоторая ролевая модель и необходимо логировать действия пользователя, записывая когда, кто и что сделал.
Или оборудование производит измерения, по большей части с идентичными значениями. А чтобы не заполнять базу данных бесконечной чередой одинаковых чисел решено производить замеры через неравные интервалы времени только при изменениях текущих показаний свыше определённого порога.
Во всех этих задачах требуется получать временные метки — таймстампы. Если вы используете микроконтроллер STM32, сделать это очень просто.
Как правильно получать таймстампы на STM32?
Устанавливаете и запускаете CubeMX. Выбираете «File→New Project…»
Вво́дите в строке поиска название вашего микроконтроллера, выбираете его из списка и нажимаете «Start Project». Я работаю с демоплатой Nucleo‑U031R8, соответственно у меня будет микроконтроллер STM32U031R8T6.

На левой панели в разделе «System Core» заходите в пункт RCC.
На панели «RCC Mode and Configuration» устанавливаете параметр «Low Speed Clock (LSE)» в режим «BYPASS Clock Source».
Также я поставил галку в пункте «Master Clock Output 2». Не слишком академический приём, но для демонстрации подойдёт: я в дальнейшем получу тактовый сигнал 1 МГц на выходе MCO2 и подключу его на вход LSE для тактирования часов реального времени.

Далее следует выбрать на левой панели пункт RTC в разделе «Timers».
И на панели «RCC Mode and Configuration» проставить галки в пунктах «Activate Clock Source», «Activate Calendar», «Timestamp», а также прописать число 99 в поле «Asynchronous Predivider» и число 9999 в поле «Synchronous Predivider».
При появлении восходящего фронта на входе RTC_TS (PC13) будет происходить автоматическое копирование текущих значений часов реального времени в буферные регистры, а также вызываться прерывание. Причём на плате Nucleo‑U031R8 вывод PC13 соединён с пользовательской кнопкой, что особенно удобно в тестовом примере.

Далее нужно перейти на вкладку «NVIC Settings» и поставить единственную галку в колонке «Enabled».

Для демонстрации ещё потребуется задействовать UART. Поэтому в разделе «Connectivity» на правой панели нужно выбрать пункт USART1 и на вкладке «Parameter Settings» прописать число 9600 в поле «Baud Rate».

Для завершения настройки осталось перейти во вкладку «Clock Configuration», прописать число 1000 в поле «Input Frequency» на входе LSE, выбрать в селекторе «RTC Clock Mux» позицию LSE, выбрать в селекторе «MCO2 Source Mux» позицию HSI16, установить делитель после селектора в значение 16 и нажать кнопку «Generate Code».

После генерации кода появится предложение открыть проект в среде разработки, с которым следует согласиться.
В проекте нужно создать буфер FIFO, так как события, требующие получения таймстампов могут происходить быстрее, чем осуществляется отправка данных через UART. Для этого в файле main.c сразу после объявления глобальных переменных (хэндлеров) hrtc и huart следует написать так:
volatile uint16_t tsFifoHead = 0; volatile uint16_t tsFifoTail = 0; typedef struct { RTC_TimeTypeDef sTime; RTC_DateTypeDef sDate; } TimeStampTypeDef; #define TSFIFO_SIZE 32 volatile TimeStampTypeDef tsFifo[TSFIFO_SIZE];
Затем там же, в файле main.c, потребуется добавить функцию (точнее, переписать коллбэк), вызываемую при прерывании, которая бы копировала зафиксированные значения времени в буфер FIFO:
void HAL_RTCEx_TimeStampEventCallback(RTC_HandleTypeDef *hrtc) { uint16_t nextHead = tsFifoHead + 1; if(nextHead >= TSFIFO_SIZE) nextHead = 0; HAL_RTCEx_GetTimeStamp(hrtc, &(tsFifo[nextHead].sTime), &(tsFifo[nextHead].sDate), RTC_FORMAT_BIN); tsFifoHead = nextHead; }
Наконец, можно в функции main, прямо в главном цикле, реализовать отправку данных из буфера FIFO по UART:
while (1) { #define TXBUFF_SIZE 50 if(tsFifoTail != tsFifoHead) { tsFifoTail = tsFifoTail + 1; if(tsFifoTail >= TSFIFO_SIZE) tsFifoTail = 0; uint8_t tx_buff[TXBUFF_SIZE] = {" "}; snprintf(tx_buff, TXBUFF_SIZE, "timestamp: %02d:%02d:%02d.%04d \n", tsFifo[tsFifoTail].sTime.Hours, tsFifo[tsFifoTail].sTime.Minutes, tsFifo[tsFifoTail].sTime.Seconds, (10000-1)-tsFifo[tsFifoTail].sTime.SubSeconds); HAL_UART_Transmit(&huart1, tx_buff, TXBUFF_SIZE, 1000); } }
Теперь соединяем на плате выход микроконтроллера MCO2 со входом RCC_OSC32_IN, выход микрокотроллера USART1_TX со входом Rx моста USB‑UART, собираем проект, загружаем прошивку, запускаем терминал, жмём на кнопку и получаем таймстампы!
И это всё?
Нет, не всё.
Можно оставить в коллбэке прерывания только копирование в FIFO сырых данных из регистров, перенеся разделение на секунды, минуты, часы и дни (а также перевод из двоично‑десятичного кода) в отправку по UART. Или даже перенести эту логику на сервер.
Можно «закольцевать» счётчики начала и конца буфера не по условию превышения максимального количества элементов массива, а наложением маски.
Что для этого потребуется
Объявление FIFO будет выглядеть так:
volatile uint16_t tsFifoHead = 0; volatile uint16_t tsFifoTail = 0; typedef struct { RTC_TimeTypeDef sTime; RTC_DateTypeDef sDate; } TimeStampTypeDef; #define TSFIFO_WIDTH 5 typedef struct { uint32_t rawTime; uint16_t rawSubS; uint16_t rawDate; } RawTimeStampTypeDef; RawTimeStampTypeDef tsFifo[1<<(TSFIFO_WIDTH+1)];
Коллбэк прерывания будет выглядеть так:
void HAL_RTCEx_TimeStampEventCallback(RTC_HandleTypeDef *hrtc) { static uint16_t nextHead = 0; nextHead = (tsFifoHead + 1)&((uint16_t)((1<<(TSFIFO_WIDTH+1))-1)); tsFifo[nextHead].rawTime = ((RTC_TypeDef*)RTC)->TSTR; tsFifo[nextHead].rawSubS = ((RTC_TypeDef*)RTC)->TSSSR; tsFifo[nextHead].rawDate = ((RTC_TypeDef*)RTC)->TSDR; ((RTC_TypeDef*)RTC)->SCR = RTC_SCR_CITSF | RTC_SCR_CTSF; tsFifoHead = nextHead; }
Главный цикл будет выглядеть так:
while (1) { #define TXBUFF_SIZE 50 if(tsFifoTail != tsFifoHead) { uint8_t tx_buff[TXBUFF_SIZE] = {" "}; tsFifoTail = (tsFifoTail + 1)&((uint16_t)((1<<(TSFIFO_WIDTH+1))-1)); TimeStampTypeDef tsSend; tsSend.sTime.SubSeconds = READ_BIT(tsFifo[tsFifoTail].rawSubS, RTC_TSSSR_SS); tsSend.sTime.Hours = (uint8_t)((tsFifo[tsFifoTail].rawTime & (RTC_TSTR_HT | RTC_TSTR_HU)) >> RTC_TSTR_HU_Pos); tsSend.sTime.Minutes = (uint8_t)((tsFifo[tsFifoTail].rawTime & (RTC_TSTR_MNT | RTC_TSTR_MNU)) >> RTC_TSTR_MNU_Pos); tsSend.sTime.Seconds = (uint8_t)((tsFifo[tsFifoTail].rawTime & (RTC_TSTR_ST | RTC_TSTR_SU)) >> RTC_TSTR_SU_Pos); tsSend.sDate.Month = (uint8_t)((tsFifo[tsFifoTail].rawDate & (RTC_TSDR_MT | RTC_TSDR_MU)) >> RTC_TSDR_MU_Pos); tsSend.sDate.Date = (uint8_t)(tsFifo[tsFifoTail].rawDate & (RTC_TSDR_DT | RTC_TSDR_DU)); tsSend.sDate.WeekDay = (uint8_t)((tsFifo[tsFifoTail].rawDate & (RTC_TSDR_WDU)) >> RTC_TSDR_WDU_Pos); tsSend.sTime.Hours = (uint8_t)RTC_Bcd2ToByte( tsSend.sTime.Hours); tsSend.sTime.Minutes = (uint8_t)RTC_Bcd2ToByte(tsSend.sTime.Minutes); tsSend.sTime.Seconds = (uint8_t)RTC_Bcd2ToByte(tsSend.sTime.Seconds); tsSend.sDate.Month = (uint8_t)RTC_Bcd2ToByte( tsSend.sDate.Month); tsSend.sDate.Date = (uint8_t)RTC_Bcd2ToByte( tsSend.sDate.Date); tsSend.sDate.WeekDay = (uint8_t)RTC_Bcd2ToByte(tsSend.sDate.WeekDay); snprintf(tx_buff, TXBUFF_SIZE, "timestamp: %02d:%02d:%02d.%04d \n", tsSend.sTime.Hours, tsSend.sTime.Minutes, tsSend.sTime.Seconds, (10000-1)-tsSend.sTime.SubSeconds); HAL_UART_Transmit(&huart1, tx_buff, TXBUFF_SIZE, 1000); } }
Это сократит время выполнения коллбэка примерно в 6 раз.
Можно добавить в логику отправки данных (или, опять же, в логику на сервере) перевод времени в формат UNIX Time — время в секундах, отсчитываемое от 1 января 1970 года.
Можно поменять делитель «Asynchronous Predivider» с 99 на 0, а затем при отправке или на сервере делить получившееся время на 100. Это позволит достичь точности таймстампов в 1 мкс (в вышеприведённых примерах кода эта точность составляет 100 мкс).
Можно полученный функционал дополнить проверкой переполнения буфера FIFO, а также проверкой ситуации, когда два события происходят с задержкой менее 1 мкс.
Но тут возникает вопрос:
Зачем вы всё это рассказываете?
Зачем вы это всё рассказываете сейчас,
- когда есть AN3371 от самого STMicroelectronics, где описывается работ с часами реального времени (в том числе и функционалом таймстампов);
- когда есть ChatGPT, который вполне сносно напишет этот базовый функционал;
- когда есть 219 уроков по STM32, детально описывающих все аспекты работы с этими микроконтроллерами?
К тому же серии STM32 в следующем году исполняется 20 лет. Как сказал в схожей ситуации @Khort:
Лет 15 это все там есть как минимум, отлажено настолько что никто уже давно в этот код и не лезет внутрь. Нет, можно конечно и самому все написать, но я бы клеил шилдик "РЕТРО" к такому материалу.
Во‑первых следует заметить, что в фундаментальном труде «219 уроков по STM32» часы реального времени упоминаются лишь раз и в контексте внешнего подключения часов в виде отдельной микросхемы DS3231. Вообще, по запросу в поисковике «пример RTC_TSTR» (Real‑Time Clock, Time Stamp Time Register) на русском языке найдутся лишь упоминания даташитов, но не проекты или туториалы. Да и на английском языке в профильных сообществах можно увидеть вопросы вроде What is the STM32 "timestamp" feature designed for?
Во‑вторых, на Хабре в 2026 году вполне появляются, например, статьи как «склеить» таймеры, чтобы генерировать таймстампы.
Причём эти статьи до сих пор находят аудиторию и даже своих зилотов, готовых доказывать что без «велосипедов» (взамен штатно предусмотренного механизма) никак не обойтись.
Буквально. Человек в дискуссии доказывал, что RTC в STM32 не способны инкрементироваться с периодом в 1 мкс, так как на картинке древа тактирования в reference manual нарисовано «32.768 kHz»:

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

...такое ограничение в 1000 кГц вполне отражено на древе тактирования в кодогенераторе CubeMX:

В дискуссиях с подобного рода техническими зилотами, ими нередко применяется рационализация (не в смысле «улучшение», а в смысле «оправдание»). Например, можно столкнуться с тезисом: «ну хорошо, микросекундный таймстамп можно получить на основе RTC. А вдруг потребуется бо́льшая точность?»
Надо понимать, что
во‑первых, точность таймстампов будет всегда ограничена максимальной частотой тактирования;
во‑вторых, при фиксированном размере регистра счёта тактовых импульсов, увеличение точности будет приводить к уменьшению максимального периода измерения времени — и наоборот;
в‑третьих, при фиксированной ширине шины памяти, расширение регистра счёта будет увеличивать минимальное время реакции на два соседних события.
Если взять STM32U031R с максимальной частотой тактирования ядра в 56 МГц и попытаться охватить периодом измерения 1 год, обеспечив при этом максимально возможную точность, то потребуется регистр счёта длиной в 51 бит. Это потребует «склеивания» четырёх 16‑битных регистров. Причём «склеивание» включает в себя решение проблемы атомарности копирования, когда скопировав младший регистр в окрестности переполнения, вы не можете сразу точно сказать, успел ли поменяться старший регистр.
Копирование четырёх регистров, а также проверки на переполнение приведут к тому, что реакция на два соседних события будет происходить с задержкой в 2...3 мкс, если не больше.
В рамках рационализации вероятнее всего будет конструироваться такая задача, для которой
- точность таймстампов должна быть строго выше 1 мкс;
- но не превышать 1/56×10⁶с;
- максимальный период должен превышать размер 16‑битного регистра;
- но не слишком сильно;
- при этом формулировка задачи должна гарантировать мягкие требования к реакции на соседние события.
Что‑то похожее на анекдот про «Ты за меня или за медведя?»
Феномен имеет зонтичный характер и сродни эффекту Манделлы.
Есть немалое количество инженеров полагающих, что JTAG это интерфейс для программирования микроконтроллеров, а не система изначально созданная для тестирования межсоединений на печатных платах. Некоторые из таких инженеров даже выдумывают свою систему тестирования межсоединений на базе UART!
Есть немалое количество инженеров полагающих, что наиболее органичным способом вывести отладочный printf(...) из микроконтроллера это передать его содержимое через мост USB‑UART в терминал, а не через SWV отладчика в консоль среды разработки. Некоторые из таких инженеров даже разрабатывают весьма продвинутую систему отладочного текстового ввода‑вывода через UART с ролевой моделью!
Возможно об этом всё же стоит говорить, это стоит обсуждать, если нет желания однажды оказаться в среде специалистов с полностью чуждыми приёмами работы, при этом считающими эти приёмы безальтернативно правильными.
Ведь в ходе обсуждения может быть поднят экзистенциальный вопрос…
Что такое «правильно»?
Вернёмся к таймстампам.
На первый взгляд концепция аппаратных таймстампов выглядит вполне правильно, профессионально и даже быть может респектабельно.
Не важно, замеряете ли вы ими очень малый или очень большой промежуток времени, все данные из RTC скопируются в промежуточный буфер одновременно. Вам не нужно будет решать проблему «склеенных» счётчиков.
Максимальный период измерения времени значителен и составляет 99 лет.
Да и точность в одну микросекунду, а также возможность автоматической фиксации таймстампа по внешнему сигналу кажутся вполне заманчивыми, чтобы использовать аппаратные возможности микроконтроллера.
На практике, если вы попытаетесь в точности повторить последовательность действий из начала статьи, у вас ничего не получится.
Во‑первых по умолчанию современный CubeMX генерирует код для среды разработки EWARM. Чтобы код сгененрировался для STM32CubeIDE необходимо перейти на вкладку «Project Manager» и выбрать в поле «Toolchain/IDE» нужную среду.

Во‑вторых, тактирование RTC возможно осуществить
- от внешнего низкоскоростного источника (LSE, Low Speed External) с частотой до 1 МГц;
- от внутреннего низкоскоростного источника (LSI) с фиксированной частотой в 32 кГц;
- от внешнего высокоскоростного источника (HSE), прошедшего через делитель;
Иными словами, обеспечить получение таймстампов с точностью в 1 мкс без внешних источников тактирования и/или соединения MCO→ RCC_OSC32_IN невозможно.
Но и тактирование RTC от HSE тоже не всегда способно обеспечить 1 мкс. Да, в серии STM32F4 делитель HSE можно выбрать из длинного списка вариантов:

Но в серии STM32U0 делитель фиксированный — 32 раза. При этом, частота самого HSE должна находиться в диапазоне 4...48 МГц, то есть поставив 32‑мегагерцовый генератор, всё ещё есть возможность затактировать RTC через внутреннее соединение.
А, например, в серии STM32F1 фиксированный делитель равен 128, при максимальной частоте HSE в 24 МГц.
То есть рационализация рационализацией, но представить себе целый ряд ситуаций, когда микросекундная точность таймстампов в принципе не будет достижима штатными средствами не так уж и трудно.
Во‑третьих, генератор кода вставляет в инициализацию выхода MCO2...
GPIO_InitStruct.Alternate = GPIO_AF0_TRACE;
...однако в файле stm32u0xx_hal_gpio_ex.h среди констант отсутствует «GPIO_AF0_TRACE», соответственно компиляция свежесгенерированного кода закончится ошибкой.
В этом случае следует вручную поправить код на...
GPIO_InitStruct.Alternate = GPIO_AF0_MCO2;
В‑четвёртых, на демоплате Nucleo‑U031R8 пользовательская кнопка по умолчанию не подтянута к питанию. Чтобы она начала функционировать, нужно вывод 23 гребёнки CN7 соединить через резистор с питанием 3,3 В.
А линия RCC_OSC32_IN не подключена к выходному пину. Да, формально пин PC14 (RCC_OSC32_IN) имеет выход на гребёнку. Но на электрической схеме видно, что контактные площадки перемычки SB22 по умолчанию не замкнуты (DNF).

Потому, чтобы всё заработало нужно сделать как‑то так:

Наконец, самое главное.
У STM32U031R субсекундный регистр RTC, непосредственно считающий такты, является 16‑битным. Следовательно он может посчитать 10 тыс. тактов, что даст точность в 100 мкс.
Ну хорошо, 25 тыс. тактов, что даст точность в 40 мкс.
А для 1 мкс нужен 1 миллион.
Возможен хак, когда RTC будет считать время в 100 раз быстрее, а получившиеся значения затем программно будут поделены на 100.
Казалось бы, при наличие календаря, считающего до 99 лет вполне рабочий вариант.
Но.
Вот так выглядит регистр даты часов реального времени RTC_DR:

А вот так выглядит буферный регистр RTC_TSDR, куда по прерыванию копируются данные регистра RTC_DR:

Да, уважаемые читатели, в отличие от 32‑битного RTC_DR, регистр RTC_TSDR является 16‑битным. Поэтому при копировании по прерыванию годы отрезаются! Соответственно в распоряжении остаётся лишь интервал времени длинною в 1 год, который будучи поделён на 100 (для обеспечения микросекундной точности) даст примерно 3,5 суток максимального периода измерений.
И тут дело не только в таймстампах.
Взять дилемму — вывести printf() через UART или через SWV? Такой выбор есть для STM32F4. Для STM32U0 такого выбора нет. Потому, что ядром STM32U0 является Cortex‑M0+, для которого SWV не предусмотрен, как и ряд другого отладочного функционала, доступного в Cortex‑M3.
Или взять автоматический анализ качества монтажа и целостности дорожек при помощи интерфейса JTAG. Не существует опенсорсного или хотя бы триального ПО, способного по net‑листу из EDA сгенерировать последовательность тестовых векторов, а затем прогнать их через FT2232. Всё многообразие компаний, предлагающих решения для JTAG‑тестирования основывают их на собственных отладчиках.
Но даже если вы купите софт, отладчик и прочитаете в документации на микросхему:
The Boundary Scan Descriptive Language (BSDL) file for this device is available by contacting your local XXX sales representative.
...есть ненулевая вероятность, что ваш local sales representative вернёт вам такой ответ производителя:
Sorry that took so long. We could not find the files for the <IC1_NAME>. The IC design team found a related device the <IC2_NAME> with a similar BSDL file. There are differences in the addressing and /BW cells since the file is for a smaller device (128K vs 512K addresses). Perhaps you could modify this file. Our apps team does not have the capability to rewrite and check a modified file (we no longer have the necessary software).
По сути, выбор «правильного» технического решения периодически похож на задачу: как забить гвоздь, если вам выдали набор инструментов состоящий из эргономичного утюга и молотка с «ограниченным» функционалом, таким, что его рукоятка обрезана на длину 3 сантиметра.
Можете взяться двумя пальцами за 3‑сантиметровую рукоятку, можете обхватить оголовье молотка ладонью, можете бить по гвоздю утюгом. И обосновать правильность любого выбора.
Причём от того, что конкретный инженер знает, что такое RTC, SWV или EDA (или не знает и потому использует «велосипедные» решения), «ручка молотка» не удлинится. Иногда незнание — действительно сила.
Ни тебя ни меня нет на самом деле
Представим себе ситуацию, что однажды функционал таймстампов будет коренным образом переработан. Вместо надстройки над RTC, с их двоично‑десятичным кодом, рассчитанным буквально на часы с табло, будет сделан отдельный 64...96‑битный регистр с тактированием от чего угодно и механизмом прямого доступа к памяти по внешнему событию.
Тогда всем тем, кто привык использовать аппаратные таймстампы в STM32 так, как это описано в начале статьи, можно будет говорить: «Дед, прими таблетки! Так уже никто не работает!»
Тут, однако, вот какое дело.
В 2013 году Intel решила создать «убийцу» Raspberry Pi и заодно Arduino — одноплатный компьютер Intel Galileo. В пресс‑релизах рассказывалось о Windows 8 за $0 на данном одноплатнике.
Затем последовал Intel Galileo Gen.2, затем Intel Edison. Для данных компьютеров была выпущена линейка процессоров Intel Quark, включая Quark D1000 и Quark D2000 — на ядре x86 и в корпусе QFN‑48!
А затем в 2017 году Intel махнула шашкой и закончила производство Intel Edison. Процессоры Intel Quark выпускались до 2019 года и тоже сошли на нет.
При этом, почти одновременно с «новейшим и революционным» семейством Intel Quark, в 2014 году компания Rochester Electronics осуществила перезапуск производства микроконтроллеров Intel 80C196K.
Микроконтроллеры этой линейки выпускались компанией Intel с 1982 по 2007 год. Но в середине 2010‑х они оказались весьма востребованы, а их складские запасы истощены. По состоянию на 2026 год на сайте Rochester микроконтроллеры EN80C196KC20 находятся в состоянии «Manufacturer Life Cycle: Rochester Active».
Зачастую, мы представляем историю как прогресс, как поступательное движение от простого к сложному. Но кто сказал, что это единственное направление? Почему появившись в очередной серии микроконтролеров, гипотетический функционал аппаратных таймстампов не может внезапно уйти в прошлое из‑за прекращения выпуска этой новой серии? А методы работы, характерные для предыдущих поколений устройств — вновь стать актуальными?
Если это так, то правильность решения оказывается в зависимости не столько от физических законов или технических ограничений. Правильность технических решений оказывается социальным конструктом (кстати, так считаю не только я), зависящим от экономической ситуации, традиций среды, особенностей менеджмента, системы образования, отраслевой мифологии и так далее. Правильность становится подвержена влиянию контенста, рефреймингу, как и любые социальные явления.
Есть в этом что‑то глубоко ироничное, что инженеры, гордящиеся (а иногда — кичащиеся) логикой в обосновании тех или решений не так уж сильно отличаются, например, от специалистов гуманитарных дисциплин :)
