Взято тут. |
Про те, як переконфігурувати, якщо є таке бажання, див. "Далекомір HC-SR04 -- використовуючи GPIO/HAL/STM32CubeMX", після слів "Ось з відліком часу складніше".
Аналогічно, HAL_GetTick() повертає, по замовчуванню, кількість мілісекунд. Це доволі велика одиниця...
В цьому пості розглянемо альтернативи:
- Активне очікування -- затримку можна зробити, скориставшись так званим busy loop (холостим циклом).
- Лічильник тактів -- його використання дозволяє вирішити обидві задачі, і створення затримок і відлік часу із мікросекундною точністю. (Старші моделі STM32 обладнані лічильником, схожим на той, який на x86 читає команда RDTSC -- DWT->CYCCNT).
- Найбільш загальний варіант -- використання повноцінних таймерів.
Активне очікування
Найпростіший спосіб затримки -- акуратний busy loop, бажано -- написаний на асемблері.
Такий код вже розглядався раніше, в пості "Відлік часу без таймерів". Так як час використаних там інструкцій для ядер ARM Cortex M3 (на ньому базується STM32F1) та M4 (STM32F3) є однаковим, цим кодом можна скористатися і тут. З однією поправкою -- час його виконання залежить від тактової частоти.
Щодо часу виконання інструкцій, див. "Cortex-M series processors -> Cortex-M3 -> Revision: <...> -> ARM Cortex‑M3 Processor Technical Reference Manual -> Instruction set summary -> Processor instructions" та аналогічну таблицю для Cortex M4. Наводжу "шлях" у документації, так як посилання регулярно псуються.
- HAL зберігає поточну тактову частоту в глобальній змінній SystemCoreClock. (Звичайно, якщо режим тактування змінювати вручну, в обхід HAL, її значення може стати невірним).
- Одна ітерація циклу із "Відлік часу без таймерів" займає 4 такти.
- Значить, частота його виконання рівна SystemCoreClock/4 герц, або SystemCoreClock/4000000 мегагерц.
- Навіщо нам мегагерци? Тому що, якщо їх помножити на кількість мікросекунд, отримаємо, скільки треба ітерацій:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // Частота виконання циклу (4 такти і одиниця -- мегагерци, щоб зручніше на // мікросекунди множити): #define LOOP_FREQ (SystemCoreClock/4000000) // LOOP_FREQ == 6 для 24МГц // LOOP_FREQ == 16 для 64МГц // LOOP_FREQ == 18 для 72МГц //! Затримка в мікросекундах. //! Увага! Не працюватиме за частот, менших за 4МГц, через значення LOOP_FREQ //! ЗАтримка буде трішки більшою за задану, але на лічені такти. inline void udelay_asm (uint32_t useconds) { useconds *= LOOP_FREQ; asm volatile(" mov r0, %[useconds] \n\t" "1: subs r0, #1 \n\t" " bhi 1b \n\t" : : [useconds] "r" (useconds) : "r0"); } |
На жаль, вимірювати час так не вдасться.
Використання лічильника тактів
Мікроконтролери сімейств ARM Cortex M3 i M4 мають ряд потужних засобів відлагодження. Поміж них -- DWT, Data Watchpoint and Trace, підсистема, яка включає лічильник тактів CYCCNT, котрий автоматично збільшується кожного такту процесора. Для передачі цієї інформації використовується ITM, Instrumentation Trace Macrocell.
Подробиці можна почитати, наприклад, в книзі "The Definitive Guide to the ARM Cortex-M3" за авторством Joseph Yiu, (перше чи друге видання, її доволі легко знайти в Інтернеті) -- глави "Chapter 15 – Debug Architecture" і "Chapter 16 – Debugging Components". Детальніше особливості підсистеми відлагодження ARM-ів тут не розглядатимемо -- вони доволі рідко потрібні прикладним програмістам.
На жаль, не всі STM32 підтримують цю можливість. Скажімо, STM32F100 -- ні. Але STM32F303 -- підтримує.
Використаємо її для виміру часу та створення затримок.
Спочатку слід ініціалізувати DWT: дозволити ITM, дозволити відлік тактів, перевірити чи вдалося -- чи лічильник справді змінюється. Покладемо цей код у функцію:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | int DWT_Init(void) { /* Enable TRC */ CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; // 0x01000000; /* Enable counter */ DWT->CTRL |= SysTick_CTRL_ENABLE_Msk; /* Reset counter */ DWT->CYCCNT = 0; // Для STM32F3 -- 32-бітний /* Check if DWT has started */ uint32_t before = DWT->CYCCNT; __NOP(); __NOP(); /* Return difference, if result is zero, DWT has not started */ return (DWT->CYCCNT - before); } |
Код вище писався за мотивами бібліотеки "DELAY - For delay functions using Systick and DWT counter" із великої та, в цілому, хорошої колекції бібліотек, однак занадто відмінний, щоб вважати його прямою похідною.
а виміряти час, з моменту перевантаження CYCCNT, наприклад, так:
Ще одна корисна функція:
Ось тут згадується, що лічильник може бути не 32-бітним. Але для STM32F3 він таки 32-бітний беззнаковий, може нарахувати до чотирьох із хвостиком мільярдів (\(2^{32}-1\) ) тактів.Тоді затримку можна організувати так:
1 2 3 4 5 6 7 8 9 10 | #define SystemCoreClockInMHz (SystemCoreClock/1000000) //! Увага! Не працюватиме за частот, менших за 1МГц, через значення SystemCoreClockInMHz inline void udelay_DWT (uint32_t useconds) { // DWT->CYCCNT = 0; // Максимізуємо можливий інтервал // Але тоді udelay_DWT i get_DWT_us не можна буде змішувати. useconds *= SystemCoreClockInMHz; while( DWT->CYCCNT < useconds){} } |
а виміряти час, з моменту перевантаження CYCCNT, наприклад, так:
1 2 3 4 5 6 7 8 9 | inline uint32_t get_DWT_cycles() { return DWT->CYCCNT; } inline uint32_t get_DWT_us() { return get_DWT_cycles()/SystemCoreClockInMHz; } |
Ще одна корисна функція:
1 2 3 4 | inline void reset_DWT_cycles() { DWT->CYCCNT = 0; } |
Таймери
Штатним засобом відліку часу є, власне, таймери. Про них в цьому блозі написано багато. Тому сильно повторюватися не будемо.
Як описано в "Таймери STM32 -- огляд", вони сильно відрізняються за можливостями. Таймери -- завжди цінний ресурс, тому для "банального" відліку часу візьмемо один із двох найпростіших, базових (TIM6, TIM7).
Конфігуруємо TIM6. Дозволяємо:
Конфігуруємо TIM6. Дозволяємо:
На вкладці Configuration налаштовуємо його параметри:
Клікабельно! |
Подільник, рівний Prescaler + 1, встановлюємо в 64 (Prescaler == 63), так як із плати STM32F3Discovery сходу вищу частоту добути важко. Однак, все рівно, потім ми в коді його оновимо згідно реальної частоти, користуючись SystemCoreClock.
Період оновлення встановлюємо в 10000 -- кожних десять тисяч відліків таймер починатиме рахувати з нуля. Крім того, дозволимо переривання від таймера:
Тоді можна буде за кожного оновлення таймера збільшувати лічильник, щоб відраховувати часи, більші за 10000мкс = 10 мс.
Детальніше про обробку переривань від таймерів див. тут: "Таймери STM32 -- відлік часу/CMSIS", розділ "Переривання таймерів" та тут: "Таймери STM32 -- відлік часу/HAL", розділ: "Обробка переривань від таймерів". Ці ж пости безпосередньо стосуються обговорюваної тут теми.
1 2 3 4 5 6 7 8 9 | volatile uint32_t tim6_overflows = 0; void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if( htim->Instance == TIM6 ) { ++tim6_overflows; } } |
Для встановлення подільника, що відповідає поточній тактовій частоті та запуску таймера створимо функцію (сама ініціалізація таймера здійснюється HAL, подробиці див. згадані вище пости):
1 2 3 4 5 6 7 8 | inline void TIM6_reinit() { HAL_TIM_Base_Stop(&htim6); __HAL_TIM_SET_PRESCALER( &htim6, (SystemCoreClockInMHz-1) ); __HAL_TIM_SET_COUNTER( &htim6, 0 ); tim6_overflows = 0; HAL_TIM_Base_Start_IT(&htim6); } |
Тоді функція отримання поточного часу виглядатиме так:
1 2 3 4 5 6 7 8 9 | inline uint32_t get_tim6_us() { __HAL_TIM_DISABLE_IT(&htim6, TIM_IT_UPDATE); //! Дуже важливо! //__disable_irq(); uint32_t res = tim6_overflows * 10000 + __HAL_TIM_GET_COUNTER(&htim6); //__enable_irq(); __HAL_TIM_ENABLE_IT(&htim6, TIM_IT_UPDATE); return res; } |
Зверніть увагу на заборону переривань під час розрахунків! Без неї регулярно, хоч і не часто, виникатиме ситуація, коли tim6_overflows встигатиме оновитися в процесі, даючи на виході безглуздий результат.
А функція затримки покладатиметься на неї:
1 2 3 4 | inline void udelay_TIM6(uint32_t useconds) { uint32_t before = get_tim6_us(); while( get_tim6_us() < before+useconds){} } |
Увага! Як мінімум, версія CMSIS, що йде із бібліотекою ST32CubeF3 версії 1.6.0, має грубу помилку в макросі __HAL_TIM_SET_PRESCALER -- зайвий пробіл після його імені та перед круглими дужками із аргументами. (Інші версії не перевіряв)
Варіантів виправлення є два. Перший -- прибрати його:
1 2 3 - #define __HAL_TIM_SET_PRESCALER (__HANDLE__, __PRESC__) ((__HANDLE__)->Instance->PSC = (__PRESC__)) + #define __HAL_TIM_SET_PRESCALER (__HANDLE__, __PRESC__)((__HANDLE__)->Instance->PSC = (__PRESC__))
Недолік цього варіанту -- за кожної перегенерації проекту в Cube, виправлення доведеться повторювати.
Другий -- не користуватися цим макросом, безпосередньо звертаючись до відповідного регістра (В дусі макросу, (&htim6)->Instance->PSC, чи й зразу TIM6->PSC). Недолік такого підходу -- потенційне зниження портабельності коду. (Яка, правда, і так не дуже висока).
Збираємо все до купи
Щоб мати можливість легко переключатися між розглянутими варіантами, можна зробити так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | //#define USE_HAL_DELAY_AND_ASM 1 //#define USE_DWT_TIMING 1 #define USE_TIM6_TIMING 1 void init_timing() { #ifdef USE_HAL_DELAY_AND_ASM return; #elif defined USE_DWT_TIMING DWT_Init(); #elif defined USE_TIM6_TIMING TIM6_reinit(); #else #error "Unknown timing method." #endif } inline uint32_t get_us() { #ifdef USE_HAL_DELAY_AND_ASM return 1000*HAL_GetTick();// ДУже грубо, а що зробиш? #elif defined USE_DWT_TIMING return get_DWT_us(); #elif defined USE_TIM6_TIMING return get_tim6_us(); #else #error "Unknown timing method." #endif } inline void udelay(uint32_t useconds) { #ifdef USE_HAL_DELAY_AND_ASM udelay_asm(useconds); #elif defined USE_DWT_TIMING udelay_DWT(useconds); #elif defined USE_TIM6_TIMING udelay_TIM6(useconds); #else #error "Unknown timing method." #endif } |
Для першого варіанту, щоб зберегти одноманітність інтерфейсу, в get_us() множимо результат HAL_GetTick() на 1000, отримуючи результат в мікросекундах, хоч і з кроком в 1000 мкс.
Післямова
Код вище не дуже акуратний, зокрема -- нехтує занадто великим інтервалами, не перевіряє вхідних аргументів і т.д. Взагалі, не очікуйте від нього забагато. Але він простий та, у багатьох випадках, цілком достатній.
На жаль, із не-технічних міркувань готові проекти не викладатиму. Однак, створити їх самостійно -- не проблема.
А на разі --
На жаль, із не-технічних міркувань готові проекти не викладатиму. Однак, створити їх самостійно -- не проблема.
А на разі --
Дякую за увагу!
Автор видалив цей коментар.
ВідповістиВидалити"1: subs r0, #1 \n\t" Компилятор ругается на эту строку в Atollic TS. Вероятно, в ассемблере нет функции subs. Можно ли написать sub?
ВідповістиВидалитиКакой предложите вариант организации микросекундных задержек для M0" ?
ВідповістиВидалити