Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Кармин Новиелло - Освоение STM32.pdf
Скачиваний:
2741
Добавлен:
23.09.2021
Размер:
47.68 Mб
Скачать
HAL_NVIC_SetPriority()
HAL_SYSTICK_Config()

Таймеры

357

Макрос __HAL_DBGMCU_UNFREEZE_TIMx() восстанавливает поведение по умолчанию (то есть таймер не останавливается во время точки останова).

Обратите внимание, что перед вызовом макроса __HAL_DBGMCU_FREEZE_TIMx()

компонент отладки микроконтроллера (MCU debug component, DBGMCU) дол-

жен быть включен вызовом макроса __HAL_RCC_DBGMCU_CLK_ENABLE().

11.4. Системный таймер SysTick

SysTick – это специальный таймер, встроенный в ядро Cortex-M, который предоставляется всеми микроконтроллерами STM32. Он в основном используется в качестве генератора временного отсчета для CubeHAL и операционной системы реального времени (если используется). Самое важное в таймере SysTick заключается в том, что, если он используется в качестве генератора временного отсчета для HAL, он должен быть сконфигурирован на генерацию исключения каждые 1 мс: обработчик исключений будет увеличивать счетчик системных тиков (глобальная, размером 32 бит и статическая переменная), к которому можно обратиться, вызвав процедуру HAL_GetTick().

SysTick – это 24-разрядный таймер нисходящего отсчета, тайктируемый шиной AHB (то есть он имеет ту же частоту, что и HCLK). Его тактовая частота может быть в конечном итоге разделена на 8 с помощью функции:

void HAL_SYSTICK_CLKSourceConfig(uint32_t CLKSource);

которая принимает параметры SYSTICK_CLKSOURCE_HCLK и SYSTICK_CLKSOURCE_HCLK_DIV8.

Частота обновления SysTick определяется начальным значением счетчика SysTick, который конфигурируется с помощью функции:

uint32_t HAL_SYSTICK_Config(uint32_t TicksNumb);

Чтобы сконфигурировать таймер SysTick таким образом, чтобы он генерировал событие обновления каждые 1 мс, и предполагая, что он тактируется с той же частотой, что и шина AHB, достаточно вызвать следующим образом:

HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000);

Процедура HAL_SYSTICK_Config() также отвечает за включение таймера и разрешение его исключения SysTick_IRQn40. Приоритет исключения может быть сконфигурирован во время компиляции установкой символьной константы TICK_INT_PRIORITY в файле include/stm32XXxx_hal_conf.h или вызовом для исключения

SysTick_IRQn, как было показано в Главе 7.

40 Помните, что SysTick_IRQn является исключением, а не прерыванием, несмотря на то что принято называть его прерыванием. Это означает, что мы не можем использовать функцию HAL_NVIC_EnableIRQ(), чтобы разрешить его.

Таймеры

358

Когда таймер SysTick достигает нуля, возникает исключение SysTick_IRQn и вызывается соответствующий обработчик. CubeMX уже предоставляет нам правильное тело функции, которое определяется следующим образом:

void SysTick_Handler(void) { HAL_IncTick(); HAL_SYSTICK_IRQHandler();

}

HAL_IncTick() автоматически увеличивает глобальный счетчик SysTick, в то время как HAL_SYSTICK_IRQHandler() содержит не что иное, как вызов процедуры HAL_SYSTICK_Callback(), являющейся необязательным обратным вызовом, который мы можем реализовать по желанию, чтобы получить уведомление об опустошении счетчика таймера.

Прочитайте внимательно

Избегайте использования медленного кода внутри процедуры HAL_SYSTICK_Callback(), иначе это может повлиять на генерацию временного отсчета. Это может привести к непредсказуемому поведению некоторых модулей HAL, которые зависят от точной генерации временного отсчета в 1 мс.

Кроме того, необходимо соблюдать осторожность при использовании HAL_Delay(). Эта функция обеспечивает точную задержку (в миллисекундах) на основе счетчика SysTick. Это подразумевает, что если HAL_Delay() вызывается из обслуживаемой периферийной ISR, то прерывание SysTick должно иметь более высокий приоритет (численно ниже), чем периферийное прерывание. В противном случае обработка вызванной ISR будет заблокирована (поскольку глобальный счетчик тиков никогда не увеличится).

Чтобы приостановить генерацию системного временного отсчета, можно использовать процедуру HAL_SuspendTick(), а для ее возобновления – процедуру HAL_ResumeTick().

11.4.1.Использование другого таймера в качестве источника системного временного отсчета

Таймер SysTick имеет только одно подходящее применение: в качестве генератора временного отсчета для HAL или необязательной ОСРВ. Поскольку тактовый сигнал SysTick нельзя легко предварительно масштабировать до более гибких частот отсчета, он не подходит для использования в качестве традиционного таймера. Также у него есть соответствующее ограничение, которое мы лучше проанализируем в Главе 23: он не подходит для использования в бестиковых режимах (tickless modes), предлагаемых некоторыми ОСРВ для приложений с пониженным энергопотреблением. По этой причине иногда важно использовать другой таймер (возможно, LPTIM) в качестве генератора системного временного отсчета. Наконец, как мы увидим в Главе 23, при использовании ОСРВ удобно разделять источник временного отсчета для HAL и для ОСРВ.

Последние выпуски программного обеспечения CubeMX позволяют легко использовать другой таймер вместо SysTick. Для этого перейдите в представление Pinout, затем

Таймеры

359

откройте пункт RCC в IP tree pane и выберите источник временного отсчета Timebase source, как показано на рисунке 35.

Рисунок 35: Как выбрать другой таймер в качестве источника системного временного отсчета

CubeMX сгенерирует дополнительный файл с именем stm32XXxx_hal_timebase_TIM.c, содержащий определение HAL_InitTick() (которая содержит весь необходимый код для инициализации таймера, так чтобы он переполнялся каждые 1 мс), HAL_Sus-

pendTick() и HAL_ResumeTick(), а также определение HAL_TIM_PeriodElapsedCallback(), ко-

торая содержит вызов процедуры HAL_IncTick(). Такое «переопределение» процедур HAL возможно благодаря тому, что эти функции определены как __weak внутри файлов с исходным кодом HAL.

11.5.Пример из практики: как точно измерить микросекунды с помощью

микроконтроллеров STM32

Иногда, особенно когда речь идет о протоколах обмена данными, не реализованных в аппаратном обеспечении периферийным устройством, нам необходимо точно измерять задержки в диапазоне от 1 до нескольких микросекунд. Это приводит к еще одному более общему вопросу: как точно измерять микросекунды в микроконтроллерах STM32?

Существует несколько способов сделать это, но некоторые методы более точны, а другие более универсальны среди различных микроконтроллеров и конфигураций тактового сигнала.

Давайте рассмотрим одного члена семейства STM32F4: STM32F401RE. Данный микроконтроллер может работать на частоте до 84 МГц, используя внутренний RC-генератор. Это означает, что каждую 1 мкс происходит 84 тактовых цикла. Таким образом, нам нужен способ подсчета 84 тактовых циклов, чтобы утверждать, что истекла 1 мкс (я предполагаю, что вы можете допустить 1% точность внутреннего тактового RC-генератора).

Довольно часто встречаются процедуры задержки, подобные следующей:

void delay1US() {

 

#define

CLOCK_CYCLES_PER_INSTRUCTION

X

#define

CLOCK_FREQ

Y // в МГц (например, 16 для 16 МГц)

volatile int cycleCount = CLOCK_FREQ / CLOCK_CYCLE_PER_INSTRUCTION;

while (cycleCount--);

}

Таймеры

360

Но как установить, сколько тактовых циклов требуется для вычисления одного шага инструкции while(cycleCount--)? К сожалению, ответ дать не просто. Давайте предположим, что cycleCount равен 1. Выполняя некоторые тесты (я объясню позже, как я их сделал), с отключенной оптимизацией компилятора (опция -O0 для GCC), мы сможем увидеть, что в этом случае для выполнения всей инструкции Си требуется 24 тактовых цикла. Как это возможно? Если мы дизассемблируем бинарный файл микропрограммы, то мы выясним, что наш оператор Си разворачивается в несколько ассемблерных инструкций:

...

 

 

 

while(counter--);

 

 

800183e:

f89d 3003

ldrb.w r3, [sp, #3]

8001842:

b2db

uxtb

r3, r3

8001844:

1e5a

subs

r2, r3, #1

8001846:

b2d2

uxtb

r2, r2

8001848:

f88d 2003

strb.w r2, [sp, #3]

800184c:

2b00

cmp

r3, #0

800184e:

d1f6

bne.n 800183e <delay1US+0x3e>

Кроме того, еще один источник задержки связан с извлечением инструкций из внутренней Flash-памяти микроконтроллера (которая сильно отличается у «недорогих» микроконтроллеров STM32 и более мощных, таких как STM32F4 и STM32F7 с ускорителем ART Accelerator, который рассчитан на нулевую задержку доступа к Flash-памяти). Таким образом, эта инструкция имеет «базовые затраты» в 24 тактовых цикла. Сколько потребуется тактовых циклов, если cycleCount равен 2? В этом случае микроконтроллеру потребуется 33 тактовых цикла, то есть 9 дополнительных тактовых циклов. Это означает, что если мы хотим простаивать в течение 84 тактовых циклов, cycleCount должен быть равен (84-24)/9, что составляет около 7. Таким образом, мы можем написать нашу функцию задержки в более общем виде:

void delayUS(uint32_t us) {

volatile uint32_t counter = 7*us; while(counter--);

}

Тестирование данной функции в этом коде:

while(1) { delayUS(1);

GPIOA->ODR = 0x0; delayUS(1); GPIOA->ODR = 0x20;

}

с помощью осциллографа, подключенного к выводу PA5, мы можем проверить, получаем ли мы задержку, которую ищем:

Таймеры

361

Является ли этот способ задержки 1 мкс состоятельным? К сожалению, ответ – нет. Прежде всего, он работает хорошо только тогда, когда этот конкретный микроконтроллер (STM32F401RE) работает на полной скорости (84 МГц). Если мы решим использовать другую тактовую частоту, нам нужно изменить ее, выполнив тесты. Во-вторых, он подвергается оптимизации компилятора, как мы скоро увидим, и внутреннему кэшированию ЦПУ на D-Bus и I-Bus, доступному в некоторых микроконтроллерах STM32 (эти кэшпамяти можно в конечном итоге отключить, установив PREFETCH_ENABLE, INSTRUC-

TION_CACHE_ENABLE, DATA_CACHE_ENABLE в файле include/stm32XXxx_hal_conf.h).

Давайте включим оптимизацию GCC по размеру (-Os). Какие результаты мы получаем? В этом случае мы получаем, что функция delayUS() затрачивает всего 72 тактовых цикла ЦПУ, то есть ~850 нс. Осциллограф подтверждает это:

А что будет, если мы включим максимальную оптимизацию по скорости (-O3)? В этом случае у нас всего 64 тактовых цикла ЦПУ, то есть наша delayUS() длится всего ~750 нс. Тем не менее, эта проблема может быть решена с помощью специальных директив pragma компилятора GCC:

#pragma GCC push_options #pragma GCC optimize ("O0") void delayUS(uint32_t us) {

volatile uint32_t counter = 7*us; while(counter--);

}

#pragma GCC pop_options

Однако, если мы хотим использовать более низкую частоту ЦПУ или хотим перенести наш код на другой микроконтроллер STM32, нам все равно нужно снова повторить тесты и определить количество тактовых циклов эмпирически.

Таймеры

362

Однако следует учитывать, что чем ниже частота ЦПУ, тем сложнее выполнить точную задержку в 1 мкс, поскольку количество тактовых циклов фиксировано для данной инструкции, но количество тактовых циклов на одну и ту же единицу времени меньше.

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

Один ответ может быть представлен установкой таймера, который переполняется каждые 1 мкс (просто установив его Period на частоту периферийной шины в МГц – например, для STM32F401RE нам нужно установить Period в (84 – 1)), и мы можем увеличивать глобальную переменную, которая отслеживает прошедшие микросекунды. Это то же самое, что и делает таймер SysTick для генерации временного отчета в HAL.

Однако такой подход нецелесообразен, особенно для низкоскоростных микроконтроллеров STM32. Генерация прерывания каждые 1 мкс (что в микроконтроллере STM32F0, работающем на полной скорости, будет означать каждые 48 тактовых циклов ЦПУ) приведет к перегруженности микроконтроллера, уменьшая общую степень многозадачности. Более того, обработка прерываний имеет отнюдь не ничтожные затраты (от 12 до 16 тактовых циклов), что повлияет на генерацию временного отсчета в 1 мкс.

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

Более надежное решение приходит из предыдущих тестов. Как я измерил такты процессора? Процессоры Cortex-M3/4/7 могут иметь необязательный модуль отладки, назы-

ваемый модулем трассировки и поддержки контрольных точек данных (Data Watchpoint and Tracing, DWT), который предоставляет точки наблюдения, трассировку данных и профилирование системы для процессора. Одним из регистров этого устройства является CYCCNT, который подсчитывает количество тактовых циклов, выполненных ЦПУ. Таким образом, мы можем использовать этот специальный модуль, доступный для подсчета количества тактовых циклов, выполненных микроконтроллером во время выполнения команды.

uint32_t cycles = 0;

/* Структура DWT определена в файле core_cm4.h */ DWT->CTRL |= 1 ; // включение счетчика

DWT->CYCCNT = 0; // сброс счетчика delayUS(1); cycles = DWT->CYCCNT;

cycles--; /* Вычитаем тактовый цикл, используемый для переноса содержимого CYCCNT в переменную тактовых циклов */

Используя DWT, мы можем построить более общую процедуру delayUS() следующим образом:

Таймеры

363

#pragma GCC push_options #pragma GCC optimize ("O3") void delayUS_DWT(uint32_t us) {

volatile uint32_t cycles = (SystemCoreClock/1000000L)*us; volatile uint32_t start = DWT->CYCCNT;

do {

} while(DWT->CYCCNT - start < cycles);

}

#pragma GCC pop_options

Насколько точна данная функция? Если вас интересует наилучшая разрешающая способность при 1 мкс, эта функция вам не поможет, как показано на рисунке.

Наилучшая производительность достигается при более высоком уровне оптимизации компилятора. Как видите, для требуемой задержки в 1 мкс функция дает задержку около 1,22 мкс (на 22% медленнее). Однако, если нам нужно простаивать в течение 10 мкс, мы получаем реальную задержку в 10,5 мкс (на 5% медленнее), что ближе к тому, что мы хотим.

Начиная с задержки 100 мкс погрешность совсем незначительна.

Почему данная функция не так точна? Чтобы понять, почему эта функция менее точна по сравнению с другой, вам необходимо выяснить, что мы используем последовательность инструкций для проверки того, сколько тактовых циклов истекло с момента запуска функции (условия while). Эти инструкции затрачивают тактовые циклы ЦПУ как для обновления внутренних регистров ЦПУ содержимым регистра CYCCNT, так и для сравнения и ветвления. Однако преимущество этой функции заключается в том, что она автоматически определяет скорость процессора и работает «из коробки», особенно если мы работаем на более быстрых процессорах.

Таймеры

364

Если вам нужен полный контроль над оптимизацией компилятора, наилучшая задержка в 1 мкс может быть достигнута с помощью этого макроса, полностью написанного на ассемблере:

#define delayUS_ASM(us) do {

\

asm volatile ("MOV R0,%[loops]\n

\

1: \n

\

SUB R0, #1\n

\

CMP R0, #0\n

\

BNE 1b \t"

\

: : [loops] "r" (16*us) : "memory" \

);

\

} while(0)

 

Это наиболее оптимизированный способ написания функции while(counter--). Выполняя тесты с осциллографом, я обнаружил, что задержка в 1 мкс может быть получена, когда микроконтроллер выполняет данный цикл 16 раз при 84 МГц. Однако данный макрос необходимо перестраивать, если скорость вашего процессора ниже, и имейте в виду, что, будучи макросом, он «расширяется» каждый раз, когда вы его используете, вызывая увеличение размера микропрограммы.