середу, 9 березня 2016 р.

Таймери STM32 -- зовнішнє тактування/CMSIS

Зовнішній годинник, а що? :-)
(c) Wiki
Як вже говорилося раніше, таймери можуть тактуватися не тільки від RCC, але і від інших зовнішніх чи внутрішніх джерел:
  • External clock mode1 -- тактування від входів TI1 або TI2,
  • External clock mode2 -- від піна ETR,
  • Internal trigger clock (ITRx) -- від внутрішніх джерел.
Про внутрішні джерела тактування таймерів поговоримо окремо, а зараз познайомимося із зовнішнім тактуванням на простому прикладі -- підрахунку кількості натискання кнопки. 

На платі STM32VLDiscovery користувацька (user) кнопка під'єднана до піна PA0:

(c) STMicroelectronics
Коли кнопка не натиснута, пін підтягнутий до землі через резистор R21, після натискання -- підтягується до живлення. 

PA0 може служити як входом каналу CH1 таймера TIM2, так і ETR входом цього ж таймера. 
Знайти опис, які піни відповідають яким каналам таймерів іноді складно. На жаль. Детальніше див. у "Таймери STM32 -- огляд", після слів "Щодо документації, то знайти відповідну інформацію -- задача нетривіальна." -- на жаль, якорі мені blogspot постійно калічить, тому на конкретне місце в постах посилаюся так.
Переказуючи написане там, в документації на STM32F100 ця інформація знаходиться в розділі 7.3, "Alternate function I/O and debug configuration (AFIO)", зокрема, таблиця 36 стосується TIM2. Для STM32F303VC -- загальний, детальний, даташіт ("RM0316 Reference manual") відправляє до документації на конкретний пристрій: "Refer to the alternate function mapping in the device datasheets", у відповідному даташіті (DM00058181.pdf, "STM32F303xB STM32F303xC") таких  таблиць, як для STM32F100 немає, але є таблиці з 14 по 19. "Alternate functions for port A"/"Port B/.../Port F". Як на мене, шукати тут щось помітно важче, ніж в документації на STM32F100.
Там же можна знайти інформацію про варіанти remapping, наприклад, що замість PA0 можна використовувати PA15. Дуже корисна річ під час розведення плат.
Спробуємо обидва режими, mode1 -- від TI1 і mode2 -- від ETR. Мене не залишає відчуття, що я щось впустив, але склалося враження, що основна функціональна відмінність між ними -- ETR підтримує подільник, дозволяючи рахувати кожен 2, 4 чи 8 імпульс, але під'єднаний до таймера безпосередньо, хоч і має свій фільтр, тоді як TIx може проходити через інші блоки (див. також тут, в розділі "Зауваження про піни таймера").

External clock mode1 -- тактування від пінів TI1 або TI2

Починається все традиційно -- слід дозволити тактування таймера та відповідного порту: 

 RCC->APB2ENR  |= RCC_APB2ENR_IOPAEN; // Дозволяємо тактування порту A
 RCC->APB1ENR  |= RCC_APB1Periph_TIM2; // Дозволяємо тактування TIM2

Далі, налаштуємо пін PA0 так, як того хоче таймер -- Input floating:

 GPIOA->CRL |=  GPIO_CRL_CNF0_0;// 01 -- Input floating
 GPIOA->CRL &= ~GPIO_CRL_CNF0_1;
 GPIOA->CRL  &= ~GPIO_CRL_MODE0; // Має бути 00 -- Input

З портами і пінами все просто. Переходимо до налаштування таймера.

Для акуратності, ініціалізуємо лічильник та регістр перевантаження:

 TIM2->CNT = 0;
 TIM2->ARR = 10000;

Накажемо використовувати перший канал, CH1, в ролі входу та сказати використовувати його в ролі TI1 -- для отримання тактування:

 // 01: CC1 channel is configured as input, IC1 is mapped on TI1
 TIM2->CCMR1 |=  TIM_CCMR1_CC1S_0;
 TIM2->CCMR1 &= ~TIM_CCMR1_CC1S_1;

Подільник при цьому ігнорується, встановлювати біти IC1PSC немає сенсу.

Біт CC1P з CCER визначає полярність захоплення/реакції на сигнал -- по зростанню чи по спаданню, CC1NP робить те ж для компланарного каналу, якщо такий є:

       // rising edge polarity -- HIGH
       TIM2->CCER &= ~( TIM_CCER_CC1P | TIM_CCER_CC1NP); 

Налаштовуємо тепер підлеглий (slave) режим за допомогою регістра SMCR. Вибираємо external clock mode1 бітами SMS:

 // 111: External Clock Mode 1 - Rising edges of the selected 
 // trigger (TRGI) clock the counter.
 TIM2->SMCR |= TIM_SMCR_SMS;

Можливі варіанти SMS для STM32F100:
  • 000 -- підлеглий режим заборонено, таймер тактується безпосередньо від RCC,
  • 001, 010, 011 -- читання з енкодерів,
  • 100 -- перевантаження таймера та подія оновлення по зовнішньому тригеру,
  • 101 --  початок відліку по зовнішньому тригеру, відлік призупиняється, коли зовнішній сигнал пропадає,
  • 110 -- початок відліку по зовнішньому тригеру, продовжується незалежно від зовнішнього сигналу,
  • 111 -- тактовим імпульсом служить зростання рівня на піні, власне використаний вище.
В STM32F303 поле SMS 4-бітове, присутні всі режими вище (старший, 4-й, біт при тому рівний нулю), та ще один додатковий:
  • 1000 -- комбінований reset + trigger режим -- подія оновлює лічильник та запускає таймер.
Дуже корисні можливості, із деякими з них ми ще зустрінемося.

Вибираємо, хто буде тригером:

 // 101: Filtered Timer Input 1 (TI1FP1)
 TIM2->SMCR |=  (TIM_SMCR_TS_0 | TIM_SMCR_TS_2);
 TIM2->SMCR &= ~(TIM_SMCR_TS_1);

Назва цього режиму -- Filtered Timer Input, натякає ще на одну важливу річ. Натискання кнопки супроводжується такою неприємною річчю, як дрижання контактів. Після натискання рівень сигналу може змінюватися декілька раз, перш ніж стабілізується. Щоб боротися із цим явищем, канали таймера, працюючи на вхід, підтримують фільтрування -- можна вказати, скільки вибірок сигналу мають мати нове значення, щоб вважати, що подія відбулася. Керується фільтр бітами IC1F (для першого каналу) регістра CCMR1, які задають частоту вибірки та необхідну кількість подій.
Частота може бути рівна тактовій частоті мікроконтролера, її половині, четвертині, аж до 1/32, кількість вибірок -- до 8. Детальніше див. документацію. На частоті 24МГц, якщо взяти мінімальну частоту вибірки, 24/32 = 0.75 МГц і максимальну кількість "перевірок", 8, (для цього всі біти IC1F мають бути одиничними), отримаємо максимальну можливу тривалість такого очікування на кінець дрижання -- приблизно 10 мс. Не дуже багато, тому корисність цієї можливості деякі автори піддають сумніву. Для перевірки нижче буде описано невеликий експеримент.
Для початку, фільтруванням не користуватимемося:

       TIM2->CCMR1 &= ~TIM_CCMR1_IC1F; // Filter 

Можна запускати таймер:

       TIM2->CR1  |= TIM_CR1_CEN;

Щоб бачити результат, скористаємося Semihosting:

    int prev_TIM2_CNT = TIM2->CNT;
    while(1)
    {
     GPIOC->ODR^=(GPIO_Pin_9); // Зміною стану LED відмічаємо цикл
     delay_some_ms(5000000);

     int new_TIM2_CNT = TIM2->CNT;
     printf("Counted: %i, change = %i\n", new_TIM2_CNT, new_TIM2_CNT-prev_TIM2_CNT);
     prev_TIM2_CNT = new_TIM2_CNT;
    }

Програма чекає певний час, потім виводить покази лічильника, та наскільки вони змінилися з попереднього разу. (Пам'ятаємо, що на 10 000 відбудеться його занулення, якщо комусь вистачить терпіння стільки натискати).

Якщо не натискати кнопку, результати не змінюватимуться, якщо натискати -- ростимуть. Виглядатиме це якось так:

До результатів ще повернемося, а поки -- те ж, користуючись mode2, ETR.

 External clock mode2 -- від піна ETR

Даний режим керується тим же SMCR -- slave mode control register, але є одна приємна хитрість. Якщо встановити біт ECE -- External clock enable, це буде еквівалентно SMS=111 і TS=111, яке відповідає тактуванню від ETRF (про SMS -- див. попередній розділ), тому вручну ці біти змінювати потреби немає. Крім того, подільник (prescaler) для цього режиму враховується -- можна наказати рахувати кожен 1, 2, 4 або 8 такт на ETR. Фільтрація керується бітами ETF цього ж регістру (для TIx -- бітами фільтра відповідного каналу), але налаштовується та поводиться так само.

Підсумовуючи, конфігурування буде зовсім простим (перших дві та остання команди -- ті ж, що і раніше):

 TIM2->CNT = 0;
 TIM2->ARR = 10000;

 TIM2->SMCR &= ~(TIM_SMCR_ETF); // Filter ETF[3:0] -- no

 TIM2->SMCR &= ~(TIM_SMCR_ETPS); // Prescaler OFF (off, 2, 4, 8)
 TIM2->SMCR &= ~(TIM_SMCR_ETP); // External trigger polarity -- Risign edge/HIGH
 TIM2->SMCR |=  (TIM_SMCR_ECE); // Enable external clock mode 2

 TIM2->CR1  |= TIM_CR1_CEN;

Для експериментів можна використати той же безмежний цикл, що і вище.

Експерименти з фільтрами

Спробуємо подивитися, наскільки дрижання контактів є критичним для даної задачі та наскільки змінюють ситуацію фільтри. Використовуватимемо режим 1 (тактування від TIx), хоча конкретний вибір режиму не повинен впливати на результат.
Експеримент проводитиму наступним чином. Програма чекає вказаний проміжок часу (див. цикл вище), протягом якого (орієнтуючись на світлодіод, котрий засвічується-гаситься на початку циклу чи на консоль дебаггера) кнопка натискається один раз. Це повторюється декілька десятків разів. Потім повторюється із натисканням кнопки двічі за інтервал -- попередні експерименти показали, що при цьому ситуація чомусь погіршується. Порівнюється очікувана кількість натискань із зафіксованою таймером -- якщо втручається дрижання, зафіксованих подій буде більше, ніж реальних натискань. Потім експеримент повторюється із включеним фільтром із максимальною тривалістю вибірки.

Без фільтрів, натискаємо один раз:

00: Counted: 0, change = 0
01: Counted: 1, change = 1
02: Counted: 2, change = 1
03: Counted: 3, change = 1
04: Counted: 4, change = 1
05: Counted: 6, change = 2
06: Counted: 7, change = 1
07: Counted: 8, change = 1
08: Counted: 9, change = 1
09: Counted: 10, change = 1
10: Counted: 11, change = 1
11: Counted: 12, change = 1
12: Counted: 13, change = 1
13: Counted: 14, change = 1
14: Counted: 15, change = 1
15: Counted: 16, change = 1
16: Counted: 17, change = 1
17: Counted: 18, change = 1
18: Counted: 19, change = 1
19: Counted: 20, change = 1
20: Counted: 21, change = 1
21: Counted: 22, change = 1
22: Counted: 23, change = 1
23: Counted: 24, change = 1
24: Counted: 27, change = 3
25: Counted: 28, change = 1
26: Counted: 29, change = 1
27: Counted: 30, change = 1
28: Counted: 32, change = 2
29: Counted: 33, change = 1
30: Counted: 34, change = 1
31: Counted: 35, change = 1
32: Counted: 36, change = 1
33: Counted: 37, change = 1
34: Counted: 38, change = 1
35: Counted: 39, change = 1
36: Counted: 40, change = 1
37: Counted: 41, change = 1
38: Counted: 42, change = 1
39: Counted: 43, change = 1
40: Counted: 44, change = 1

41: Counted: 45, change = 1

Як видно, на 40 натискань (під час нульового інтервалу не натискалося нічого) припало 3 випадки фальшивих спрацювань, один із яких -- подвійний. Не претендуючи на якусь точність -- це вимагало б значно довшого експерименту, можна оцінити частоту таких випадків як 10%.

Без фільтрів, натискаємо два рази:

00: Counted: 0, change = 0
01: Counted: 3, change = 3
02: Counted: 5, change = 2
03: Counted: 7, change = 2
04: Counted: 10, change = 3
05: Counted: 14, change = 4
06: Counted: 17, change = 3
07: Counted: 20, change = 3
08: Counted: 22, change = 2
09: Counted: 24, change = 2
10: Counted: 26, change = 2
11: Counted: 28, change = 2
12: Counted: 30, change = 2
13: Counted: 32, change = 2
14: Counted: 34, change = 2
15: Counted: 37, change = 3
16: Counted: 40, change = 3
17: Counted: 43, change = 3
18: Counted: 46, change = 3
19: Counted: 48, change = 2
20: Counted: 50, change = 2
21: Counted: 53, change = 3
22: Counted: 55, change = 2
23: Counted: 59, change = 4
24: Counted: 61, change = 2
25: Counted: 63, change = 2
26: Counted: 65, change = 2
27: Counted: 67, change = 2
28: Counted: 71, change = 4
29: Counted: 73, change = 2
30: Counted: 75, change = 2
31: Counted: 77, change = 2
32: Counted: 79, change = 2
33: Counted: 81, change = 2
34: Counted: 83, change = 2
35: Counted: 85, change = 2
36: Counted: 87, change = 2
37: Counted: 89, change = 2
38: Counted: 91, change = 2
39: Counted: 93, change = 2
40: Counted: 96, change = 3
41: Counted: 99, change = 3


На 40*2 натискань прийшлося 99 подій відліку, тобто 99-80=19 фальшивих спрацювань, 25%!
Зауважу, кнопка під'єднана акуратно, дрижання проявляє себе значно слабше, ніж для простої зовнішньої кнопки. Чому зростає при повторних натисканнях -- не знаю. Натискання артикулював чітко -- з повним відпусканням та паузою перед наступним натисканням. Можливо, якісь механічні ефекти у кнопці або електричні у конденсаторах схеми -- частота натискань, все ж, зросла...
Вмикаємо фільтр "на максимум": із частотою  вибірки 1/32 тактової, та вісьмома вибірками, повторюємо. Одне натискання за інтервал:

00: Counted: 0, change = 0
01: Counted: 1, change = 1
02: Counted: 2, change = 1
03: Counted: 3, change = 1
04: Counted: 5, change = 2
05: Counted: 6, change = 1
06: Counted: 7, change = 1
07: Counted: 8, change = 1
08: Counted: 9, change = 1
09: Counted: 10, change = 1
10: Counted: 11, change = 1
11: Counted: 12, change = 1
12: Counted: 13, change = 1
13: Counted: 15, change = 2
14: Counted: 16, change = 1
15: Counted: 17, change = 1
16: Counted: 18, change = 1
17: Counted: 19, change = 1
18: Counted: 20, change = 1
19: Counted: 21, change = 1
20: Counted: 22, change = 1
21: Counted: 23, change = 1
22: Counted: 24, change = 1
23: Counted: 25, change = 1
24: Counted: 26, change = 1
25: Counted: 27, change = 1
26: Counted: 28, change = 1
27: Counted: 29, change = 1
28: Counted: 30, change = 1
29: Counted: 31, change = 1
30: Counted: 32, change = 1
31: Counted: 33, change = 1
32: Counted: 34, change = 1
33: Counted: 35, change = 1
34: Counted: 36, change = 1
35: Counted: 37, change = 1
36: Counted: 38, change = 1
37: Counted: 39, change = 1
38: Counted: 40, change = 1
39: Counted: 41, change = 1
40: Counted: 42, change = 1
41: Counted: 43, change = 1


Маємо дві фальшивих події на 40, 5% (хоч вибірка і замала для оцінки статистичної значущості відмінності!)

Два натискання за інтервал:

00: Counted: 0, change = 0
01: Counted: 2, change = 2
02: Counted: 4, change = 2
03: Counted: 6, change = 2
04: Counted: 8, change = 2
05: Counted: 11, change = 3
06: Counted: 13, change = 2
07: Counted: 15, change = 2
08: Counted: 17, change = 2
09: Counted: 19, change = 2
10: Counted: 21, change = 2
11: Counted: 23, change = 2
12: Counted: 25, change = 2
13: Counted: 27, change = 2
14: Counted: 29, change = 2
15: Counted: 31, change = 2
16: Counted: 33, change = 2
17: Counted: 35, change = 2
18: Counted: 37, change = 2
19: Counted: 39, change = 2
20: Counted: 41, change = 2
21: Counted: 43, change = 2
22: Counted: 45, change = 2
23: Counted: 47, change = 2
24: Counted: 49, change = 2
25: Counted: 52, change = 3
26: Counted: 55, change = 3
27: Counted: 57, change = 2
28: Counted: 59, change = 2
29: Counted: 61, change = 2
30: Counted: 63, change = 2
31: Counted: 65, change = 2
32: Counted: 67, change = 2
33: Counted: 69, change = 2
34: Counted: 71, change = 2
35: Counted: 73, change = 2
36: Counted: 75, change = 2
37: Counted: 77, change = 2
38: Counted: 79, change = 2
39: Counted: 82, change = 3
40: Counted: 84, change = 2
41: Counted: 86, change = 2


Маємо 6 фальшивих подій на 80 натискань -- 7.5% замість 25%. Тут різниця більш виражена!

Отож, підсумовуючи:
  • Дрижання контактів таки є проблемою.
  • Вбудований фільтр (через свою короткочасність?) не є панацеєю від неї -- все рівно доведеться приймати додаткові міри боротьби.
  • Однак, так виглядає, він помітно покращує ситуацію, в рази зменшуючи кількість фальшивих спрацювань. (Щоб сказати впевнено, слід провести статистичний аналіз -- перевірити значущість відмінності, а мені поки лінь).


main.c

#include <stdio.h>
#include <stm32f10x.h>

volatile uint32_t curTicks;

#ifdef __cplusplus
extern "C"{
#endif
void SysTick_Handler(void) {
 curTicks++;       /* Зроблено ще один тік */
}

#ifdef __cplusplus
}
#endif

#define MICROSECONDS_GRANULARITY 10
// Потрібна частота спрацювань таймера SysTick в герцах
#define FREQ ((1000000)/(MICROSECONDS_GRANULARITY))
#define TICKS ((SystemCoreClock)/(FREQ))

/*! Затримка в мікросекундах, грануляція -- microseconds_granularity мікросекунд,
 * одну не витягує сам контролер. 24МГц -- один такт це 42нс=0.042мкс */
inline static void delay_some_us(uint32_t mks)
{
 uint32_t ticks=mks/MICROSECONDS_GRANULARITY;
 int stop_ticks=ticks+curTicks;
 while (curTicks < stop_ticks) {};
}


int main(void)
{
 RCC->APB2ENR  |= RCC_APB2ENR_IOPCEN; // Дозволяємо тактування порту C із
       // світлодіодами плати STM32VLDiscovery
       // PC8 -- blue, PC9 -- green

 // TIM2 -- в пості "Таймери STM32 -- зовнішнє тактування/CMSIS"
 RCC->APB2ENR  |= RCC_APB2ENR_IOPAEN; // Дозволяємо тактування порту A, із кнопкою
 RCC->APB1ENR  |= RCC_APB1Periph_TIM2; // Дозволяємо тактування TIM2

 // PA0 -- кнопка.
 GPIOA->CRL |=  GPIO_CRL_CNF0_0;// 01 -- Input floating
 GPIOA->CRL &= ~GPIO_CRL_CNF0_1;
 GPIOA->CRL  &= ~GPIO_CRL_MODE0; // Має бути 00 -- Input

 // Порти світлодіодів - теж на вивід. Звертаємося до старших пінів, тому CRH -- High
 GPIOC->CRH &= ~GPIO_CRH_CNF8;
 GPIOC->CRH  |= GPIO_CRH_MODE8;
 GPIOC->CRH &= ~GPIO_CRH_CNF9;
 GPIOC->CRH  |= GPIO_CRH_MODE9;

 //-------------------------------------------------------------------------
 // TIM2 рахує натискання кнопки
 TIM2->CNT = 0;
 TIM2->ARR = 10000;

 // Якщо наступний макрос оголошено -- користуючись TI1, інакше -- ETF
#define USE_TIM2_TI1_SYNC
#ifdef USE_TIM2_TI1_SYNC
 // 01: CC1 channel is configured as input, IC1 is mapped on TI1
 TIM2->CCMR1 |=  TIM_CCMR1_CC1S_0;
 TIM2->CCMR1 &= ~TIM_CCMR1_CC1S_1;

 // Подільник все рівно ігнорується //TIM2->CCMR1 &= ~TIM_CCMR1_IC1PSC;

 TIM2->CCMR1 &= ~TIM_CCMR1_IC1F; // Filter -- no
 // TIM2->CCMR1 |= TIM_CCMR1_IC1F; // Filter -- maximal time length

 TIM2->CCER &= ~( TIM_CCER_CC1P | TIM_CCER_CC1NP); // rising edge polarity -- HIGH

 // 111: External Clock Mode 1 - Rising edges of the selected trigger (TRGI) clock the counter.
 TIM2->SMCR |= TIM_SMCR_SMS;

 // 101: Filtered Timer Input 1 (TI1FP1)
 TIM2->SMCR |=  (TIM_SMCR_TS_0 | TIM_SMCR_TS_2);
 TIM2->SMCR &= ~(TIM_SMCR_TS_1);
#else // Use ETR
 //TIM2->SMCR &= ~(TIM_SMCR_ETF); // Filter ETF[3:0] -- no
 CTIM2->SMCR |=  (TIM_SMCR_ETF); // Filter ETF[3:0] -- maximal

 TIM2->SMCR &= ~(TIM_SMCR_ETPS); // Prescaler OFF (off, 2, 4, 8)
 TIM2->SMCR &= ~(TIM_SMCR_ETP); // External trigger polarity -- Risign edge/HIGH
 TIM2->SMCR |=  (TIM_SMCR_ECE); // Enable external clock mode 2
           // Еквівалентне SMS=111 and TS=111
#endif

 TIM2->CR1  |= TIM_CR1_CEN;

 // Ініціалізуємо таймер SysTick
 if (SysTick_Config (TICKS))
 {
  // Помилка -- таймер не ініціалізувався
  TIM1->CR1 &= ~TIM_CR1_CEN;
  GPIOC->BSRR = GPIO_BSRR_BS8; // Синім позначатимемо помилку
  while(1); // Зависаємо
 }


 int prev_TIM2_CNT = TIM2->CNT;
 int idx = 0;
    while(1)
    {
     GPIOC->ODR^=(GPIO_Pin_9); // Зміною стану LED відмічаємо цикл
     delay_some_us(5000000/3);

     int new_TIM2_CNT = TIM2->CNT;
     printf("%02i: Counted: %i, change = %i\n", idx, new_TIM2_CNT, new_TIM2_CNT-prev_TIM2_CNT);
     prev_TIM2_CNT = new_TIM2_CNT;
     ++idx;
    }
}


Зауваження, в коді вибір тактування від TI1 чи ETR, здійснюється макросом USE_TIM2_TI1_SYNC -- якщо його оголошено, тактується із TI1, інакше з ETR. Нагадаю, кнопка на платі під'єднана до піна, котрий може служити і тим і тим.




Завершальне слово


Скачати проект із кодом для цього та двох наступних пості -- "Таймери STM32 -- внутрішні тригери/CMSIS" і "Таймери STM32 -- Автоматична зупинка/CMSIS", можна тут.

Код, який стосується лише цього посту, можна знайти у файлі main_external_trig.c,  в директорії app/ проекту (не включений у проект CoIDE -- щоб не конфліктувати із int main(), котрий містить все зразу).


Немає коментарів:

Дописати коментар