Comments 48
--Почему в STM32 нет таймеров TIM6, TIM7, TIM10 и TIM11? При этом есть TIM8, TIM9 и TIM12.
Правда ? Наверное, стоит хотя бы мельком обозначить серию контроллеров, о которой идет речь ?

А зачем так сложно? Нельзя просто взять, да хоть uint64_t переменную счетчик и инкрементировать её в обработчике прерываний по переполнению тех же 16 битных таймеров? А зависимый функционал будет плясать от значения такого счетчика.
Вот так, например:
volatile uint64_t tim3Tick64 = 0;
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if (htim->Instance == TIM3){
tim3Tick64 ++;
}
}в обработчике прерываний по переполнению тех же 16 битных таймеров? А зависимый функционал будет плясать от значения такого счетчика.
Прерывания каждую микросекунду?
Приложение будет тормозить.
Прерывания каждую микросекунду?
Прерывания каждые 64 миллисекунды.
А я предлагаю каскадный 32бит таймер, который вообще прерывания не производит.
Такой способ требует определенного подхода для получения корректных значений на стыке переполнения.
Допустим, мы решили прочитать значение составного программно-аппаратного таймера.
Запрещаем срабатывание прерывания таймера, а лучше вообще все прерывания.
Читаем аппаратную часть
Читаем флаг переполнения аппаратного таймера
Если флаг сброшен - читаем программную часть, разрешаем прерывание и выходим
Снова читаем аппаратную часть. Читаем программную, прибавляем единицу к программной части (именно к результату чтения), разрешаем прерывание и выходим.
Такой алгоритм позволит устранить коллизии на стыке переполнения аппаратного таймера
Не получится использовать systick timer для этого, у него нет флага.
И да, можно хоть 128 битный таймер сделать на таком принципе. Прерывание таймера должно быть самым высокоприоритетным (в котором только инкремент программного таймера), чтобы можно было в других прерываниях корректно читать время
для SysTick таймера можно использовать немгого другой скелет алгоритма:
прочитать значения счетчика прерываний в ячейке памяти
прочитать занчение из регистра таймера
убедиться, что значени в ячейке к этому моменту не изменилось
если изменилось, то, в простейшем случае, повторить с пункта 1.
Такой вариант требует обязательного наличия работающего неперекрытого прерывания, иначе конфликты могут быть, если прерывание не сработало между п. 1 и п. 3.
Так на единицы микросекунд вам 16битного и даже 8битного таймера за глаза должно хватить. А 1000мкс это уже миллисекунды, где здесь 32 бита? Просто настройте таймер правильно. А на миллисекундах, если прерываться, то ничего тормозить не должно. Или я чего-то не догоняю?
P.s. а ну или Вы ставите задачу добывать значение счетчика только таким способом
tim_word_hi = TIM9->CNT;
и не прерываться от таймера вообще? (это я из Вашего ответа на коммент выше понял)
Делаю нечто похожее с системным таймером на одну (или сколько надо для системного тика) миллисекунд и переменной. (таймстампы не требуют под себя отдельный таймер) Но каскадирование таймеров может быть хорошим подспорьем в решении прикладных задач прошивки. Так что техника, на мой взгляд, полезная.
Проблемы навскидку данного примера кода:
слишком частые прерывания будут в системе;
нарушение атомарности
контроллер 32 бита, переменная 64 -> запись за один такт не возможна. нужно либо ставить критическую секцию, что чревато блокировкой чего-то важного достаточно часто.
и да, на ARM Cortex M критическая секция не гарантирует что инструкции внутри нее будут выполнены в правильном порядке, поэтому нужны ещё барьерные инструкции.
tim3Tick64 ++;
вот этот инкремент так же может быть выполнен уже после выхода из прерывания, если конвейер ядра решит что так быстрее. то есть сначала выйдет из IRQ, затем инкремент.
это не только в документации на ядро описано, но и реальный баг который я неделю отлавливал у себя.
Я взял самый простой пример навскидку (uint64_t просто сферически большое значение счетчика в вакууме) чтобы повысить свою эрудицию и увидеть картину шире. А так весь HAL дико неатомарен, это само собой.
Разве при выходе из прерывания в Cortex-M не выполняется обязательный неявный __DSB()?
По дизассемблеру - нет. Компилятор использовал от segger, который явно сделан на основе arm clang.
На том же stackoverflow есть несколько тем посвященных dsb и dmb.
А ещё книжка хорошая есть по ядру М3 и М4 от (имя забыл, Янг кажется). Плюс спеку на ядро полезно посмотреть. Там как раз описывается зачем эти инструкции нужны и область их применения.
Просто именно на M3/M4 я с этим проблем обычно не имел (в Keil и CubeIDE с GCC), а вот при порте на M7 уже пришлось ручками барьеры вешать, но там все работой кеша объясняется. Надо посмотреть по дизассемблеру, мне казалось для всех исключений (т.е. не только прерываний) на выходе автоматом встаёт барьер.
А переменную объявляли как volatile? Это может быть не проблемой ядра cortex-m а перестановкой вычислений компилятором. Барьеры памяти тоже решают проблему
Конечно. И статик и volatile вешал.
Я делал свой таймер, с коррекцией типа под мастера. Спасение было в расстановке барьеров.
Вот тут нюанс описывается
В указанном примере хоть 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
Dmb и dsb полезны только при использовании Кеша в многоядерной архитектуре. Если нет ни того ни другого, то использование их не имеет преимущества. Лучше пустой барьер (фактически, на него не тратятся инструкции выполнения, это чисто руководство для компилятора, что все что до и после этого барьера - не перетасовываем)
Возможно, но тогда какой смысл их вводить и описывать для простых ядер типа М3 и М4? Я согласен с тем что надо понимать когда они действительно работают, а когда нет.
В моем понимании, если у переменной (64 бита например) обращается кто-то из кода и в этот же момент приходит прерывание в котором эта же переменная обновляется, то очень с большой долей вероятности можно получить некорректное значение переменной. Отсюда и возникает необходимость в DMB, которая заставит конвейер выполнить все операции с памятью до этой инструкции:
Команда DМВ используется в качестве барьера памяти данных. Она rарантирует, что все обращения к памяти) явно указанные до вызова команды DMB будут выполнены до Toro, как начнут выполняться явные обращения к памяти) появляющиеся после команды DМВ. Команда DМВ не влияет на порядок или выполнение
Причем в тестах, DMB именно что следовало располагать в прерывании таймера. (Я делал составной таймер, правда на nrf52840, но в данном случае речь по то же самое ядро М4. Сложность была в том, что мастер периодически делал коррекцию тика через алгоритм синхронизации. Поэтому в прерывании нужно было считать как целые, так и доли секунд. Проблема была именно в атомарном доступе к переменной, которая средствами языка не решилась. Неделя ушла на то, чтобы научиться получать эту Ситуацию стабильно и примерно месяц тестов чтобы убедится в полноценном решении.
Dsb в этом плане для аппаратных регистров подходит. Она нужна чтобы предыдущая команда была исполнена сразу. В применении к таймера - когда нужно забрать значение и тут же обнулить таймер. Проблемная ситуация: кто-то влезает между этими действиями. Вендор рекомендует после записи делать "volatile void" чтение. Документация на ядро - DSB.
Поэтому польза не только для многоядерных, но и для одиночных контроллер есть.
Edit: да, если параноить дальше, то ещё REX (исклюзианый доступ) надо пользовать. Эти инструкции позволят оценить что при обращении к ячейке памяти никто больше туда не влезал. Но это уже отдельная тема, которую я совсем поверхностно смотрел
В моем понимании, если у переменной (64 бита например) обращается кто-то из кода и в этот же момент приходит прерывание в котором эта же переменная обновляется, то очень с большой долей вероятности можно получить некорректное значение переменной. Отсюда и возникает необходимость в DMB, которая заставит конвейер выполнить все операции с памятью до этой инструкции:
Прерывание не прерывает выполнение ассемблерной инструкции посередине. Потому что инструкция атомарна. Если 64-х битная операция "а++" неатомарна, то никакая dmb dsb не поможет, если возникнет прерывание в процессе вычисления "а".
Когда срабатывает прерывание, то в процессоре вообще много чего происходит:
Во-первых, дожидаемся выполнения текущей инструкции, после чего сбрасывается конвеер команд, во-вторых сохраняется в стек (читай, в память) весь контекст (все регистры). Потом читается адрес PC из таблицы векторов (тоже не мало обращений к памяти), потом снова загрузка конвейера. Так что dmb dsb тут и рядом не стоит. Все операции с памятью, которые должны были выполниться перед прерыванием выполнятся ДО начала процесса прерывания в любом случае.
По поводу того, зачем dmb dsb в одноядерных приложениях: ну кеш никто не отменял, особенно в купе с использованием с DMA (DMA не в курсе про кеш, он работает с внешней памятью только). Ну и второе: из песни слов не выкинешь. Так и из ядра, разработанного на все случаи жизни
up-time - время с момента включения питания на плату
тайм штамп - временная отметка. По сути это и есть up_time
А чем вам не нравится модуль RTC с субсекундным разрешением, который встроен во все STM32?
Ну попробуйте его заставить микросекунды считать. В статье как раз этот кейс описывается
Ну попробуйте его заставить микросекунды считать
Зачем его заставлять? Он сам их считает.
В статье как раз этот кейс описывается
В статье нет ни слова про RTC.
Ну попробуйте его на мегагерце хотя бы запустить. Далеко не на каждом МК его можно сконфигурировать на тактирование от высокоскоростных источников: из того что я видел - только от HSE, и (у большинства моделей, с которыми имел дело) с предделителем на 32 или 128, при том что максимальная частота HSE меньше.
В статье описывается кейс микросекундных таймстампов, а не RTC, как я и писал выше.
Ну попробуйте его на мегагерце хотя бы запустить.
Выберите себе обидное прозвище, которым я буду вас называть, если я попробую и у меня получится запустить его на мегагерце. Тогда продолжим дискуссию :))
Если мы с Вами не в 1 комнате, то можете меня даже бить. Давайте, запустите RTC на 1МГц на, например, STM32F103C8. Только без читов недокументированных возможностей, нам повторяемость в промышленных масштабах нужна. Вот Вам ещё для затравки схема тактирования из рефмануала на микроконтроллер. Её должно хватить, чтобы остудить Ваш пыл.

Угу, в HAL иногда не только атомарностью не пахнет, а вообще хоть сколько-нибудь вменяемым временем выполнения для базовых вещей. В таймер новое значение закинуть там такая портянка тянется... вот потому автор и пользует регистры напрямую. Да, приходилось таймеры сцепливать. После AVRок возможности STMок читались в документации с восторгом - зачем городить какие-то костыли, если контроллер позволяет аппаратно прокладывать целые линии, цепляя один модуль к другому - таймеры и ШИМ на GPIO, АЦП, DMA, UART. Только таблицы приходится смотреть что с чем можно цеплять, а что нет. Кстати, чёт действительно мало видел случаев, чтоб RTC заводили!
Идея здравая, плюсую. Но хотелось бы узнать, в каких случаях 20 прерываний в секунду (каждые 50 мс), могут оказать критическое влияние на работу устройства?
А что будет, если при чтении попадëм на обновление слейв-таймера?
tim_word_hi = TIM9->CNT;
tim_word_lo = TIM3->CNT;
Это же не атомарное чтение одного 32-битного регистра, а всë-таки двух независимых 16-битных. Как защитить целостность данных?
Обновление tim_word_hi происходит раз в 65 ms.
Я бы предпочел ничего не делать.
На следующей итерации прочитается уже корректное значение.
Но как-то ведь надо отличить некорректное значение от корректного. А раз прошивка работает достаточно долго, такой кейс рано или поздно случится.
А что будет, если при чтении попадëм на обновление слейв-таймера?
Я видел такое решение
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;
}Однако почему так сделано - затрудняюсь пояснить.
Сделано чисто для отлова ситуации, когда старший таймер обработает переполнение младшего, иначе получите метку времени "из прошлого": младший уже сбросится, а данные считанные со старшего уже не актуальны. Можно сделать проще и проверять, что новое значение младшего таймера больше предыдущего, но на длинных интервалах времени это не исключает ошибку полностью, поэтому действительно корректнее после записи повторно сверяться со старшим таймером и при необходимости повторить операцию заново.
Читаем старшие 16 бит.
Читаем младшие 16 бит.
Снова читаем старшие 16 бит.
Если п.1<>п.3 и п.2<32768 используем значение из п.1. Во всех остальных случаях - из п.3.
Может сработать некорректно, если между п.1 и п.3 прошло более 32 мс, что уж очень маловероятно.
В стм в таком режиме как раз 16 бит старшего отображается в старших битах младшего таймера. Как-то так. Получается одно 32х битное число. Если конечно мне память не изменяет. Т.е. там все схвачено
Еще есть вариант, что при вычитке младшего таймера значение старшего буферизируется ( так сделано в AVR по-моему). То есть нужно просто прочитать в правильной последовательности слачало значение ведомого, потом ведущего таймера
Это где вы нашли про отображение старшего в младшем? Такого нет ни в STM32, и не было в AVR.
Вы, наверное, имеете в виду теневой регистр для чтения длинного значения 8-разрядным мк. Внутри одного таймера - видел такое. Между разными таймерами - не видел.
Более того, таймеры в статье находятся в разных тактовых доменах. И появляется задержка на синхронизацию между ними в несколько тактов.
Если я все правильно понимаю, в итоге двумя таймерами мы можем посчитать 2^32 микросекунд, т.е. примерно 4300 секунд, что немногим более часа. А если устройство ну хотя бы месяцами работает без перерывов, то для чего такой timestamp?
Да. Это классическая "ошибка 71ой минуты".
>>> ((10**(-6))*(2**32))/60
71.58278826666667Что делать - не знаю.
Переходить на RISC-V.
Там 64битный systick встроен прямо в ядро!
После 72ой минуты 32 таймер переполнится и начнет считать заново. Можно генерировать прерывания и по slave таймеру, умножать счетчик прерываний на 0xFFFFFFFF и прибавлять cnt32. В результате получится уже тайм штамп типа uint64_t.
Насколько я понял, описываемое решение применяется в случаях, когда 16 или более прерываний в секунду (раз в 64 мс) - это слишком часто. Я с такими случаями не сталкивался, но исключить их возможность не могу.
Прерывания раз в час (или раз в 71 минуту) уж точно навредить не могут, поэтому просто старшие 32 бита счетчика храним в памяти и оперируем 64-битным значением, которое за время существования нашего МК точно не переполнится.
Если же прерывания по переполнению таймера раз в 64 мс допустимы, то можно использовать 16-битный таймер и тоже 32 бита счетчика в памяти. 48 бит хватит лет на шесть, так что раз в 5-6 лет надо будет перезагружаться, что выглядит некритичным ограничением.
Каскадный Таймер на STM32 (или Таймер с Прицепом)