четвер, 3 березня 2016 р.

Таймери STM32 -- відлік часу/CMSIS

Почнемо вивчення роботи таймерів із найпростіших операцій. Найелементарніша така операція, яку вміє навіть SysTick -- відлік часу, який досягається генерацією переривань по проходженню певної кількості тіків таймера. 

Нагадаю, працюватимемо на прикладі STM32F100RB, яким обладнана STM32VLDiscovery. Для інших мікроконтролерів сімейства, скажімо, STM32F303, зміни будуть мінімальними, якщо взагалі будуть, навіть якщо користуватися виключно CMSIS.

Переривання таймерів


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

Природу події можна вияснити, подивившись на біти регістра статусу, TIMx_SR. Подія оновлення (тобто, таймер дорахував до TIMx_ARR чи нуля, в залежності від напрямку рахунку -- вверх чи вниз), встановлюється біт UIF цього регістра. 

Список таких "комунальних" квартир переривань таймерів для STM32F100RB:
  • TIM1_BRK_TIM15_IRQn, TIM1_UP_TIM16_IRQn, TIM1_TRG_COM_TIM17_IRQn,  TIM1_CC_IRQn -- як бачимо, три із чотирьох переривань єдиного просунутого таймера цього контролера, TIM1, використовуються спільно із іншими таймерами (TIM15/16/17).
  • TIM2_IRQn
  • TIM3_IRQn
  • TIM4_IRQn
  • TIM6_DAC_IRQn -- разом із перериванням від ЦАП (DAC)
  • TIM7_IRQn
Зауважте, що таймери TIM12/13/14 у варіанта мікроконтролера STM32F100RB (у всіх Low i Medium density, якщо точніше) -- відсутні, є лише в High-density STM32F100.

Для STM32F303VC, у якого таймерів навіть менше, (деякі із STM32F100RB відсутні), але вони більш потужні (+ TIM8 просунутий + TIM2 32-бітний загального використання - TIM5 16-бітний загального використання - TIM12/13/14 з обмеженою кількістю каналів; так виглядає, кількість каналів та ж):
  • TIM1_BRK_TIM15_IRQn, TIM1_UP_TIM16_IRQn, TIM1_TRG_COM_TIM17_IRQn,  TIM1_CC_IRQn, як і для STM32F100RB
  • TIM2_IRQn
  • TIM3_IRQn
  • TIM4_IRQn
  • TIM8_BRK_IRQn, TIM8_UP_IRQn, TIM8_TRG_COM_IRQn, TIM8_CC_IRQn -- переривання другого просунутого таймера повністю незалежні.
  • TIM6_DAC_IRQn
  • TIM7_IRQn

Реалізовуємо відлік

Почнемо із банального -- реалізуємо мигання світлодіодами, користуючись TIM1 (просунутий) та TIM6 (базовий). Нехай TIM6 керує миганням зеленого світлодіоду плати, під'єднаного до PC9, TIM1 -- миганням синього, на PC8. (Свідомо не кажу "керує світлодіодом" -- безпосереднє керування пінами розглянемо в майбутньому.)

Тактування периферії

Спочатку, як завжди, слід ініціалізувати всю периферію. Вмикаємо тактування:

RCC->APB2ENR  |= RCC_APB2ENR_IOPCEN; // Дозволяємо тактування порту C

RCC->APB1ENR  |= RCC_APB1Periph_TIM6; // Дозволяємо тактування TIM6
RCC->APB2ENR  |= RCC_APB2Periph_TIM1; // Дозволяємо тактування TIM1

Для цього встановлюємо біти регістрів APBxENR, APB1 i APB2 -- шини периферії, повільна та швидка, відповідно. Як видно, PortC та TIM1 належать шині APB2, а TIM1 -- APB1.

Конфігурація пінів

Далі, згідно таблички із даташіта, конфігуруємо ці піни на вивід в режимі push-pull, в швидкісному режимі -- 50МГц (детальніше див., наприклад, тут):
(c) STMicroelectronics

GPIOC->CRH &= ~GPIO_CRH_CNF8;
GPIOC->CRH  |= GPIO_CRH_MODE8;
GPIOC->CRH &= ~GPIO_CRH_CNF9;
GPIOC->CRH  |= GPIO_CRH_MODE9;

В регістр CRH, який відповідає за піни з номерами 8-15 (для пінів 0-7 це був би CRL, H[igh] i L[ow] в назві натякають), біти CNF встановлюються в 0 (GPIO Push-Pull), MODE -- в 1, 50МГц.

Налаштування таймерів

Тепер можна перейти до таймерів. Почнемо по старшинству, з TIM1:

TIM1->PSC = 24000 - 1; // Подільник -- 24000, за частоти 24 МГц -- 1 тік в мілісекунду
TIM1->ARR = 300 ;     // Переривання -- раз на 300 мс = 0.3 с
TIM1->DIER |= TIM_DIER_UIE; // Дозволяємо переривання
TIM1->CR1 |= TIM_CR1_CEN;   // Дозволити таймеру працювати
NVIC_EnableIRQ(TIM1_UP_TIM16_IRQn); // Дозволити відповідне переривання, TIM6_DAC_IRQn, в NVIC

Подільник отримується додаванням до вмісту PSC одиниці. Контролер працює на частоті 24МГц, тому для відліку раз у мілісекунду встановлюємо його в 24000, записуючи в PSC 24000 - 1. Щоб отримувати переривання раз на 0.3 секунди, кажемо генерувати його кожних 300 відліків, за допомогою ARR.

Дозвіл на генерацію переривань здійснюється в два етапи:
  1. Спершу слід дозволити генерацію переривання конкретною периферією, таймером в даному випадку. Для цього слід в регістрі DIER (DMA/interrupt enable register) слід встановити біт UIE, дозволяючи переривання по оновленню лічильника. 
  2. Потім слід дозволити це переривання і в контролері переривань, NVIC, для якого воно має назву  TIM1_UP_TIM16_IRQn. 
  3. Звичайно, сама подія, що викликає переривання, теж має бути дозволеною. Наприклад, біт UDIS (Update disable) регістра CR1 забороняє виникнення події оновлення, тому навіть дозволені переривання не виникатимуть.
Тепер можна дозволити таймеру працювати, встановивши біт CEN (Counter enable) в CR1.

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

TIM6->PSC = 24000 - 1; // Подільник -- 24000, за частоти 24 МГц -- 1 тік в мілісекунду
TIM6->ARR = 1000 ;     // Переривання -- раз на 1000 мс = 1 с
TIM6->DIER |= TIM_DIER_UIE; // Дозволяємо переривання
TIM6->CR1 |= TIM_CR1_CEN;   // Дозволити таймеру працювати
NVIC_EnableIRQ(TIM6_DAC_IRQn); // Дозволити відповідне переривання, TIM6_DAC_IRQn, в NVIC

Залишається найцікавіше -- реакція на переривання. Як вже обговорювалося (тут, розділ "Ініціалізація та написання обробників переривань"), в файлі ініціалізації мікроконтролера (startup_stm32f10x_md*.*, асемблерному чи написаному на С) оголошуються слабкі (weak) символи -- обробники по замовчуванню, які можна заміщати. У нашому випадку вони називаються TIM1_UP_TIM16_IRQHandler та TIM6_DAC_IRQHandler.

void TIM1_UP_TIM16_IRQHandler(void)
{
 if( TIM1->SR & TIM_SR_UIF ) // Перевіряємо джерело
 {
  TIM1->SR &= ~TIM_SR_UIF; //Очищаємо прапорець переривання
  GPIOC->ODR^=(GPIO_Pin_8); //Змінюємо стан світлодіода
 }
}

Переривання по даній лінії може прийти із різних джерел, тому перевіряємо -- чи справді біт UIF у SR для TIM1 встановлено. Якщо так -- очищаємо його (встановлюється він апаратно, але скидати слід програмно) та змінюємо стан світлодіода, змінивши на протилежний відповідний біт регістра ODR, котрий (в обраному режимі піна) вказує, у якому стані має бути пін.

В нашому випадку, звичайно, TIM16 заборонений, переривання від нього прилетіти не може -- можна було б і не перевіряти, однак, в загальному випадку, краще це зробити -- будь-яка реальна програма буде  рости, залучати нову периферію і рано чи пізно можна отримати сюрприз, забувши де і коли "зрізали дорогу".


Аналогічно, для TIM6:

void TIM6_DAC_IRQHandler(void)
{
 if( TIM6->SR & TIM_SR_UIF ) // Перевіряємо джерело
 {
  TIM6->SR &= ~TIM_SR_UIF; //Очищаємо прапорець переривання
  GPIOC->ODR^=(GPIO_Pin_9); //Змінюємо стан світлодіода
 }
}

Пам'ятайте, що ці функції повинні бути оголошені як extern "C", якщо програма компілюється C++ компілятором! Інакше воно компілюватиметься та лінкуватиметься успішно, однак, через заплутування імен, ця функція фізично називатиметься не так, як слабкий символ-обробник по замовчуванню і не заміщатиме його.

Все. Після прошивки плата підморгує світлододами, із частотою 1/2Гц та 0.3/2Гц. Чому ділимо на два?  Бо світлодіоди світяться один період таймера, потім не світяться ще один, відповідно, частота вдвічі менша.

Скачати демонстраційний проект можна тут.



main.c -- повний текст

#include <stm32f10x.h>

#ifdef __cplusplus
extern "C"{
#endif

// Обробник переривання TIM6_DAC --- може бути від двох джерел!
void TIM6_DAC_IRQHandler(void)
{
 if( TIM6->SR & TIM_SR_UIF ) // Перевіряємо джерело
 {
  TIM6->SR &= ~TIM_SR_UIF; //Очищаємо прапорець переривання
  GPIOC->ODR^=(GPIO_Pin_9); //Змінюємо стан світлодіода
 }
}

//! Обробник переривань, викликається у відповідь на подію оновлення
//! TIM1 або будь-яку подію TIM16
void TIM1_UP_TIM16_IRQHandler(void)
{
 if( TIM1->SR & TIM_SR_UIF ) // Перевіряємо джерело
 {
  TIM1->SR &= ~TIM_SR_UIF; //Очищаємо прапорець переривання
  GPIOC->ODR^=(GPIO_Pin_8); //Змінюємо стан світлодіода
 }
}

#ifdef __cplusplus
}
#endif


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

 RCC->APB1ENR  |= RCC_APB1Periph_TIM6; // Дозволяємо тактування TIM6
 RCC->APB2ENR  |= RCC_APB2Periph_TIM1; // Дозволяємо тактування TIM1


 GPIOA->CRL &= ~GPIO_CRL_CNF7_0; // 10 -- AF PP (Alternative function -- push-pull)
 GPIOA->CRL |=  GPIO_CRL_CNF7_1;
 GPIOA->CRL  |= GPIO_CRL_MODE7; //Встановити обидва біти MODE для піна 1 -- швидкість 50MHz

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

 TIM1->PSC = 24000 - 1; // Подільник -- 24000, за частоти 24 МГц -- 1 тік в мілісекунду
 TIM1->ARR = 300 ;     // Переривання -- раз на 300 мс = 0.3 с
 TIM1->DIER |= TIM_DIER_UIE; // Дозволяємо переривання
 TIM1->CR1 |= TIM_CR1_CEN;   // Дозволити таймеру працювати
 NVIC_EnableIRQ(TIM1_UP_TIM16_IRQn); // Дозволити відповідне переривання, TIM6_DAC_IRQn, в NVIC

 TIM6->PSC = 24000 - 1; // Подільник -- 24000, за частоти 24 МГц -- 1 тік в мілісекунду
 TIM6->ARR = 1000 ;     // Переривання -- раз на 1000 мс = 1 с
 TIM6->DIER |= TIM_DIER_UIE; // Дозволяємо переривання
 TIM6->CR1 |= TIM_CR1_CEN;   // Дозволити таймеру працювати
 NVIC_EnableIRQ(TIM6_DAC_IRQn); // Дозволити відповідне переривання, TIM6_DAC_IRQn, в NVIC


 TIM3->PSC = 0;       // Подільник частоти - 1
 TIM3->ARR = 1000;              // Оновлюємо кожних 1000 тіків

 TIM3->CCR2 = 10;      // На початку -- майже не світимо

 TIM3->CCER |= (TIM_CCER_CC2E); // Capture/Compare 2 output enable -- PA7
 TIM3->CCER &= ~TIM_CCER_CC2P;   // Полярність -- active high (Default)

 TIM3->CCMR1|=(TIM_CCMR1_OC2M_1 | TIM_CCMR1_OC2M_2); // PWM1 -- OCxM=0b110
 TIM3->CCMR1&=~TIM_CCMR1_OC2M_0;
 // TIM3->CCMR1|=(TIM_CCMR1_OC2M_0| TIM_CCMR1_OC2M_1 | TIM_CCMR1_OC2M_2); // PWM2 -- OCxM=0b111

 TIM3->CCMR1&= ~TIM_CCMR1_CC2S; // Зануляємо -- на вивід. (Default)
 TIM3->CCMR1|= TIM_CCMR1_OC2PE; // Preload для CCR2, вимагається Datasheet для PWM

 TIM3->CR1  |= TIM_CR1_ARPE;    // Preload для ARR,  вимагається Datasheet для PWM
 TIM3->CR1  &= ~TIM_CR1_DIR;    // Відлік -- вверх. (Default)

 TIM3->CR1  |= TIM_CR1_CEN;

    while(1)
    {
    }
}
Зауважте -- головний цикл програми порожній. За потреби вона може займатися своїми справами, не відволікаючись на світлодіоди.

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

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