четвер, 20 жовтня 2016 р.

Мікросекундні затримки та відлік мікросекунд для STM32

Взято тут.
Затримки тривалістю в мікросекунди, які, наприклад, потрібні для роботи із нашим ультразвуковим далекоміром, не підтримуються HAL. При тому, HAL використовує SysTick, конфігуруючи його так, щоб той лічив мілісекунди, а переконфігуровувати його не завжди можна і не завжди хочеться.
Про те, як переконфігурувати, якщо є таке бажання, див. "Далекомір 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" із великої та, в цілому, хорошої колекції бібліотек, однак занадто відмінний, щоб вважати його прямою похідною.
Ось тут згадується, що лічильник може бути не 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. Дозволяємо:

На вкладці Configuration налаштовуємо його параметри:
Клікабельно!
Подільник, рівний Prescaler + 1, встановлюємо в 64 (Prescaler == 63), так як із плати STM32F3Discovery сходу вищу частоту добути важко. Однак, все рівно, потім ми в коді його оновимо згідно реальної частоти, користуючись SystemCoreClock.

Період оновлення встановлюємо в 10000 -- кожних десять тисяч відліків таймер починатиме рахувати з нуля. Крім того, дозволимо переривання від таймера: 
Вектор переривання для TIM6 i ЦАП (DAC) співпадають, для нас це не проблема -- ЦАПом ми не користуємося. Але якби й користувалися -- джерело конкретного переривання завжди можна вияснити безпосередньо в обробнику, та й HAL про це турбується.

Тоді можна буде за кожного оновлення таймера збільшувати лічильник, щоб відраховувати часи, більші за 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 мкс.

Післямова

Код вище не дуже акуратний, зокрема -- нехтує занадто великим інтервалами, не перевіряє вхідних аргументів і т.д. Взагалі, не очікуйте від нього забагато. Але він простий та, у багатьох випадках, цілком достатній.

На жаль, із не-технічних міркувань готові проекти не викладатиму. Однак, створити їх самостійно -- не проблема.

А на разі --
Дякую за увагу!

3 коментарі:

  1. "1: subs r0, #1 \n\t" Компилятор ругается на эту строку в Atollic TS. Вероятно, в ассемблере нет функции subs. Можно ли написать sub?

    ВідповістиВидалити
  2. Какой предложите вариант организации микросекундных задержек для M0" ?

    ВідповістиВидалити