понеділок, 28 березня 2016 р.

Таймери STM32 -- одноімпульсний режим/CMSIS

Останні теми, перш ніж повернемося до далекоміра -- вже із новими засобами. Взагалі, якщо чесно, вся ця серія з'явилася через те, що я дуже розсердився, витративши, щоб розібратися із One Pulse Mode (одноімпульсний режим, OPM) кілька повних днів. Тому текст нижче -- спроба розказати про нього максимально детально.

Що це за режим оглядово розповідалося в загальному пості про таймери:
На додачу до всього описаного вище, таймер має спеціальний біт, який вказує -- продовжувати відлік чи зупинятися після події оновлення (забороняючи таймер). З допомогою цієї можливості реалізовано спеціальний, одноімпульсний, режим. По зовнішньому чи внутрішньому сигналу (запустити таймер може зовнішня подія або інший таймер) відбувається затримка (визначається вже згаданим TIMx_CCRx), після чого -- імпульс, тривалістю TIMx_ARRx-TIMx_CCRx. Що таке імпульс визначається тими режимами, якими володіє кожен із каналів -- встановити у активний, пасивний, змінити на протилежний, ШІМ1, ШІМ2, не змінювати нічого, примусовий активний чи пасивний, що значить активний/пасивний визначається полярністю.

Режим одного імпульсу.
(c) STMicroelectronics
Розглянемо два види подій, що запускатимуть OPM -- внутрішня, від іншого таймера та зовнішня -- від кнопки.

Почнемо із простішого і більш корисного -- реакція на зовнішню подію із заданою затримкою.

Запуск OPM кнопкою 

Якщо не робити remapping, кнопка, яка на нашій платі під'єднана до піну PA0, відповідає каналу CH1 таймера TIM2. Тому світлодіод, свічення якого і буде нашою реакцією на подію, під'єднано до PA1 -- CH2, другого каналу, того ж таймера.

Світлодіоди на платі, зелений та синій, використаємо для індикації стану таймера:
  • Коли підлеглий таймер почав відлік, але триває затримка -- світиться зелений 
  • Коли розпочався імпульс, світлодіоди на платі гаснуть -- світиться зовнішній, той що на PA1
  • Коли імпульс завершився, таймер зупинився, засвічується блакитний -- очікування на подію
Це можна зобразити якось так:

(У прикладах нижче використано TI1 замість TI2)

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

void TIM2_IRQHandler(void)
{
 // Перевіряємо джерело переривання
 // Зауважте, бувають події, які встановлюють зразу декілька
 // прапорців переривань -- тоді порядок перевірки може бути важливим!
 // Це -- не говорячи про pending-переривання від більш ранніх подій
 // чи від інших таймерів
 if( TIM2->SR  & TIM_SR_CC2IF ) // Capture-compare каналу 2 відбулося
 {
  TIM2->SR &= ~TIM_SR_CC2IF; //Очищаємо прапорець переривання
  GPIOC->ODR &=  ~(GPIO_Pin_9);  // Вимикаємо зелений, коли підлеглий таймер
            // дорахував до значення в CCR2
     // буде світитися світлодіод, керований таймером -- на PA1
 }

 if( TIM2->SR  & TIM_SR_UIF ) // Оновлення підлеглого таймера, після якого він зупиниться
         // завдяки OPM біту
 {
  TIM2->SR &= ~TIM_SR_UIF; //Очищаємо прапорець переривання
  GPIOC->ODR &= ~(GPIO_Pin_9); // Вимикаємо зелений коли закінчили імпульс
  GPIOC->ODR |=  (GPIO_Pin_8); // При тому вмикаємо синій -- свідчить про
          // очікування на наступну подію
 }

 if( TIM2->SR  & TIM_SR_TIF ) // Таймер запущено
 {
  TIM2->SR &= ~TIM_SR_TIF; //Очищаємо прапорець переривання

  GPIOC->ODR &= ~(GPIO_Pin_8); // Гасимо синій та
  GPIOC->ODR |= (GPIO_Pin_9);  // вмикаємо зелений
          // коли підлеглий таймер почав відлік
 }
}

Якихось хитрощів у ній немає, тому переходимо до main().

Спочатку, традиційно, дозволяємо тактування та ініціалізуємо піни:

 // PC8 -- blue, PC9 -- green

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

 RCC->APB1ENR  |= RCC_APB1Periph_TIM2; // Дозволяємо тактування TIM3


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

 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->PSC = 24000-1;
 TIM2->ARR = 7000;         // Відлік -- 7000, 7 с
 TIM2->CCR2 = 2000;        // З них, завдяки PWM2 і Polarity-HIGH,
         // 2 секунда -- затримка перед ввімкненням світлодіода на PA1

Після події -- затримка, тривалістю 2 секунди (2000 кроків таймера із подільником 24000 та тактовою частотою 24МГц) і реакція на неї у вигляді імпульсу тривалістю 5 секунд (7-2).

Обираємо джерело сигналу -- TI1, який, для TIM2, під'єднаний до PA0:

 // TS[2:0]: Trigger selection, 101: Filtered Timer Input 1 (TI1FP1)
 TIM2->SMCR &= ~( TIM_SMCR_TS_1 );
 TIM2->SMCR |=  ( TIM_SMCR_TS_2 | TIM_SMCR_TS_0 );

Вказуємо починати відлік у відповідь на тригер:

 // 110: Trigger Mode - The counter starts at a rising edge of the trigger TRGI
 // (but it is not reset). Only the start of the counter is controlled.
 TIM2->SMCR &= ~ TIM_SMCR_SMS_0;
 TIM2->SMCR |=   TIM_SMCR_SMS_2 | TIM_SMCR_SMS_1;

Тепер налаштовуємо пов'язану ланку -- реакцією на тригер, що приходить з CH1, буде запуск Capture/Compare на каналі 2, CH2 (див. відповідний пост за подробицями цього режиму). Так як нам потрібна пауза, після якої імпульс заданої тривалості, слід обрати режим PWM2 із полярністю HIGH:
Справа в тому, що як обрати, за допомогою бітів OC2M, "Set channel 2 to active level on match", канал ж залишить на високому рівні після зупинки таймера. В прикладах найчастіше використовують режим toggle -- перемикання на протилежний стан, однак, при всій простоті написання прикладу, він не демонструє важливих нюансів використання цього режиму -- звідси й виник цей пост.

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

 TIM2->CCMR1|=(TIM_CCMR1_OC2M_0| TIM_CCMR1_OC2M_1 | TIM_CCMR1_OC2M_2); // PWM2 -- OCxM=0b111

 TIM2->CCMR1&= ~TIM_CCMR1_CC2S; // Зануляємо -- на вивід. (Default)

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

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

Вмикаємо "одноімпульсний" режим -- зупинку по події оновлення:

TIM2->CR1  |= TIM_CR1_OPM;  // One pulse mode

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

 // Дозволяємо переривання
 TIM2->DIER |= TIM_DIER_UIE;
 TIM2->DIER |= TIM_DIER_TIE;
 TIM2->DIER |= TIM_DIER_CC2IE;

 NVIC_EnableIRQ(TIM2_IRQn);

Запускаємо таймер та входимо в безмежний цикл:

 TIM2->CR1 |= TIM_CR1_CEN;

    while(1)
    {
    }


Запуск OPM іншим таймером

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

Запуск TIM2 здійснюватимемо за допомогою TIM1, вивід здійснюватиметься тими ж пінами -- хіба що PA0, на якому кнопка, не треба ініціалізувати. Тобто, вся та ж ініціалізація, що і вище, яку не повторюватиму, плюс дозволяємо тактування TIM1:

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

Пам'ятаємо, що він на APB2, на відміну від TIM2, котрий на APB1.

Від TIM1 нам потрібно не багато: вести відлік, генеруючи події оновлення, які перенаправляти на вихід тригера (TRGO):

 TIM1->PSC = 24000-1;
 TIM1->ARR = 10000;  // Час між імпульсами -- 10 с.

 // MMS: Master mode selection -->
 // 010: Update - The update event is selected as a trigger output (TRGO).
 TIM1->CR2 &= ~( TIM_CR2_MMS_0 | TIM_CR2_MMS_2 );
 TIM1->CR2 |=  ( TIM_CR2_MMS_1 );

Таймер генеруватиме тригер кожних 10 секунд.

Часові інтервали для TIM2 залишимо тими ж:

 TIM2->PSC = 24000-1;
 TIM2->ARR = 7000;         // Відлік -- 7000, 7 с
 TIM2->CCR2 = 2000;        // З них, завдяки PWM2 і Polarity-HIGH,
         // 2 секунда -- затримка перед ввімкненням світлодіода на PA1

Тригер тепер інший:

 // TS[2:0]: Trigger selection, 000: Internal Trigger 0 (ITR0) -- TIM1
 TIM2->SMCR &= ~( TIM_SMCR_TS_2 | TIM_SMCR_TS_1 | TIM_SMCR_TS_0 );

Але все решта залишається тим же! Тому навіть не копіюватиму.

В кінці залишається запустити обидва таймери:

 TIM1->CR1 |= TIM_CR1_CEN;
 TIM2->CR1 |= TIM_CR1_CEN;

    while(1)
    {
    }

Тепер наша програма кожних 10 секунд: чекає 2 секунди, (про це повідомляє зелений світлодіод), запалює світлодіод, під'єднаний до PA1, через 7-2=5 секунд гасить його, вмикаючи синій та чекає до завершення десяти секунд, щоб почати наново.

Повний текст main() для обох випадків нижче -- копіювати процедуру обробки переривань ще раз немає сенсу.



int main() -- запуск кнопкою


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

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

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

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


 GPIOA->CRL &= ~GPIO_CRL_CNF1_0; // 10 -- AF PP (Alternative function -- push-pull)
 GPIOA->CRL |=  GPIO_CRL_CNF1_1;
 GPIOA->CRL  |= GPIO_CRL_MODE1; //Встановити обидва біти 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;

 //GPIOC->BSRR = GPIO_BSRR_BS9;
 //GPIOC->BSRR = GPIO_BSRR_BR8;


 TIM1->PSC = 24000-1;
 TIM1->ARR = 10000;  // Час між імпульсами -- 10 с.

 // MMS: Master mode selection -->
 // 010: Update - The update event is selected as a trigger output (TRGO).
 TIM1->CR2 &= ~( TIM_CR2_MMS_0 | TIM_CR2_MMS_2 );
 TIM1->CR2 |=  ( TIM_CR2_MMS_1 );

 //  TIM1->SR &= ~TIM_SR_UIF;
 // TIM1->DIER |= TIM_DIER_UIE; // Дозволяємо переривання

 // NVIC_EnableIRQ(TIM1_UP_TIM16_IRQn); // Дозволити відповідне переривання

 TIM2->PSC = 24000-1;
 TIM2->ARR = 5000;         // Відлік -- 5000
 TIM2->CCR2 = 1000;        // З них, завдяки PWM2 і Polarity-HIGH,
         // 1 секунда -- затримка перед ввімкненням світлодіода на PA1

 // TS[2:0]: Trigger selection, 000: Internal Trigger 0 (ITR0) -- TIM1
 TIM2->SMCR &= ~( TIM_SMCR_TS_2 | TIM_SMCR_TS_1 | TIM_SMCR_TS_0 );

 // 110: Trigger Mode - The counter starts at a rising edge of the trigger TRGI
 // (but it is not reset). Only the start of the counter is controlled.
 TIM2->SMCR &= ~ TIM_SMCR_SMS_0;
 TIM2->SMCR |=   TIM_SMCR_SMS_2 | TIM_SMCR_SMS_1;

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

 TIM2->CCMR1|=(TIM_CCMR1_OC2M_0| TIM_CCMR1_OC2M_1 | TIM_CCMR1_OC2M_2); // PWM2 -- OCxM=0b111

 TIM2->CCMR1&= ~TIM_CCMR1_CC2S; // Зануляємо -- на вивід. (Default)

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

 // Дозволяємо переривання
 TIM2->DIER |= TIM_DIER_UIE;
 TIM2->DIER |= TIM_DIER_TIE;
 TIM2->DIER |= TIM_DIER_CC2IE;

 TIM2->CR1  |= TIM_CR1_OPM;  // One pulse mode

 NVIC_EnableIRQ(TIM2_IRQn);

  //! Перестраховуємося -- очищаємо всі події, перш ніж почати
  TIM1->SR &= ~TIM_SR_UIF;
  TIM2->SR &= ~TIM_SR_UIF;

 TIM1->CR1 |= TIM_CR1_CEN;
 TIM2->CR1 |= TIM_CR1_CEN;

    while(1)
    {
    }
}

int main() -- запуск іншим таймером


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

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

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

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


 GPIOA->CRL &= ~GPIO_CRL_CNF1_0; // 10 -- AF PP (Alternative function -- push-pull)
 GPIOA->CRL |=  GPIO_CRL_CNF1_1;
 GPIOA->CRL  |= GPIO_CRL_MODE1; //Встановити обидва біти 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;

 //GPIOC->BSRR = GPIO_BSRR_BS9;
 //GPIOC->BSRR = GPIO_BSRR_BR8;


 TIM1->PSC = 24000-1;
 TIM1->ARR = 10000;  // Час між імпульсами -- 10 с.

 // MMS: Master mode selection -->
 // 010: Update - The update event is selected as a trigger output (TRGO).
 TIM1->CR2 &= ~( TIM_CR2_MMS_0 | TIM_CR2_MMS_2 );
 TIM1->CR2 |=  ( TIM_CR2_MMS_1 );

 //  TIM1->SR &= ~TIM_SR_UIF;
 // TIM1->DIER |= TIM_DIER_UIE; // Дозволяємо переривання

 // NVIC_EnableIRQ(TIM1_UP_TIM16_IRQn); // Дозволити відповідне переривання

 TIM2->PSC = 24000-1;
 TIM2->ARR = 5000;         // Відлік -- 5000
 TIM2->CCR2 = 1000;        // З них, завдяки PWM2 і Polarity-HIGH,
         // 1 секунда -- затримка перед ввімкненням світлодіода на PA1

 // TS[2:0]: Trigger selection, 000: Internal Trigger 0 (ITR0) -- TIM1
 TIM2->SMCR &= ~( TIM_SMCR_TS_2 | TIM_SMCR_TS_1 | TIM_SMCR_TS_0 );

 // 110: Trigger Mode - The counter starts at a rising edge of the trigger TRGI
 // (but it is not reset). Only the start of the counter is controlled.
 TIM2->SMCR &= ~ TIM_SMCR_SMS_0;
 TIM2->SMCR |=   TIM_SMCR_SMS_2 | TIM_SMCR_SMS_1;

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

 TIM2->CCMR1|=(TIM_CCMR1_OC2M_0| TIM_CCMR1_OC2M_1 | TIM_CCMR1_OC2M_2); // PWM2 -- OCxM=0b111

 TIM2->CCMR1&= ~TIM_CCMR1_CC2S; // Зануляємо -- на вивід. (Default)

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

 // Дозволяємо переривання
 TIM2->DIER |= TIM_DIER_UIE;
 TIM2->DIER |= TIM_DIER_TIE;
 TIM2->DIER |= TIM_DIER_CC2IE;

 TIM2->CR1  |= TIM_CR1_OPM;  // One pulse mode

 NVIC_EnableIRQ(TIM2_IRQn);

  //! Перестраховуємося -- очищаємо всі події, перш ніж почати
  TIM1->SR &= ~TIM_SR_UIF;
  TIM2->SR &= ~TIM_SR_UIF;

 TIM1->CR1 |= TIM_CR1_CEN;
 TIM2->CR1 |= TIM_CR1_CEN;

    while(1)
    {
    }
}




Cкачати готові проекти можна тут та тут.

7 коментарів:

  1. Дуже цікава статейка і особливо приємно що Ви так само як і я починали з STM32+CMSIS. Хотів спитати чи тут дійсно треба гасити зелений?

    if( TIM2->SR & TIM_SR_UIF ) // Оновлення підлеглого таймера, після якого він зупиниться
    // завдяки OPM біту
    {
    TIM2->SR &= ~TIM_SR_UIF; //Очищаємо прапорець переривання
    GPIOC->ODR &= ~(GPIO_Pin_9); // Вимикаємо зелений коли закінчили імпульс
    GPIOC->ODR |= (GPIO_Pin_8); // При тому вмикаємо синій -- свідчить про
    // очікування на наступну подію
    }

    Адже UIF це переповнення, а значить червоний має гаснути, а не зелений. Зелений був вимкнений коли таймер дорахував до значення CCR2, тоді ж ми викнули зелений і перейшли на червоний. Значить по UIF наступним кроком ми вимикаємо червоний і вмикаємо синій. Ну або я щось просто не зрозумів:) Дякую за гарний пост. Олег. UR7WKH

    ВідповістиВидалити
    Відповіді
    1. Щойно побачив Ваш коментар -- щось зламалося в нотифікації...

      Чесно кажучи, вже не пам'ятаю, але мене точно щось змусило було його додати. Звичайно, може помилка в іншому місці (яку міг виправити потім).

      Видалити
  2. Все добре, але публікації треба продовжувати.Все заглохло.

    ВідповістиВидалити
  3. Відповіді
    1. Тут це нічого не змінювало, код еволюціонував після того, як було зроблено малюнок, але за давністю не пам'ятаю подробиць.

      Видалити
  4. ВО блін написав в соді, а треба в коді

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