суботу, 26 березня 2016 р.

Таймери STM32 -- захоплення ШІМ/CMSIS

За допомогою захоплення вводу, розглянутого недавно, можна організувати потрібні для роботи із далекоміром вимірювання тривалості імпульсів. Реалізувати це можна як програмно (налаштувавши захоплення по спаду і по зростанню сигналу та програмно відстежувати поточний стан) так і апаратно (з'єднавши фізично два канали таймера і на одному захоплювати фронт, на іншому -- спад). Думка, що такі задачі постають, прийшла й розробникам STM32, тому у таймерів є спеціальний режим -- захоплення ШІМ. Зацитую описане в огляді таймерів:
Хитра конфігурація режиму захоплення сигналу -- коли два канали "слухають" той самий вхід, один ловить фронт, другий спад. При тому, початок відліку каналу по фронту очищає лічильник таймера. Тоді подія по спаданню збереже у TIMx_CCRx відповідного каналу тривалість імпульсу, а наступний фронт дасть у TIMx_CCRx каналу, що їх ловить, період. Значно краще все це зрозуміти можна на рисунку із даташіту:


Режим захоплення ШІМ.
(c) STMicroelectronics
Насправді, це всього лиш комбінація вже розглянутого режиму захоплення, різних видів тригерів і реакції на них та під'єднання двох каналів до одного піну.



Скористаємося цим режимом, щоб заміряти час натискання кнопки. Для простоти -- без всіляких кільцевих буферів, програма просто виводить тривалість попереднього натискання, та час між натисканнями.
Написав спочатку більш загальний варіант, але суть губилася поміж технічних подробиць, мається на увазі -- губилася більше, ніж в типових моїх текстах :-), то вирішив таки переробити. Абсолютний час теж можна було б виводити, але для цього потрібен ще один таймер або якісь додаткові програмні хитрощі -- під час захоплення ШІМ лічильник таймер регулярно зануляється.
Виводитимемо результат за допомогою Semihosting та Newlib-printf.

Апаратна конфігурація -- та ж, що й в попередньому пості, зокрема кнопка на платі під'єднана до піна PA0. Тому ініціалізуємо як і раніше:

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

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


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

Задаємо базові властивості таймера:

 TIM2->PSC = 24000 - 1;
 TIM2->CNT = 0;
 TIM2->ARR = reload_val;

де reload_val -- константа, рівна 10000, яка використовується і в обробці переривань -- на неї множиться лічильник переповнень, див. далі.

Тепер працюватимуть два канали, CH1 i CH2. Вимкнемо подільник і налаштуємо фільтри для них:

 TIM2->CCMR1 &= ~ TIM_CCMR1_IC1PSC; // Prescaler disabled
 TIM2->CCMR1 &= ~ TIM_CCMR1_IC2PSC; // Prescaler disabled

 TIM2->CCMR1 |=   TIM_CCMR1_IC1F; // Filter -- по максимуму
 TIM2->CCMR1 |=   TIM_CCMR1_IC2F; // Filter -- по максимуму

Як і раніше, повідомляємо, що перший канал служитиме TI1, спрацювання по зростанню:

 TIM2->CCMR1 &= ~ TIM_CCMR1_CC1S_1; // CCR1 <- TI1 (01)
 TIM2->CCMR1 |=   TIM_CCMR1_CC1S_0;

 TIM2->CCER  &= ~ TIM_CCER_CC1P; // Rising edge

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

 TIM2->CCMR1 |=   TIM_CCMR1_CC2S_1; // CCR2 <- TI1 (10)
 TIM2->CCMR1 &= ~ TIM_CCMR1_CC2S_0;

 TIM2->CCER  |=   TIM_CCER_CC2P; // Falling edge

Вказуємо таймеру використовувати в ролі тригера TI1 і по тригеру перевантажуватися:

 // Trigger input -- TI1FP1, TS=101b
 TIM2->SMCR  &= ~ TIM_SMCR_TS_1;
 TIM2->SMCR  |= ( TIM_SMCR_TS_2 | TIM_SMCR_TS_0 );

 // SMS -- 100, reset mode
 TIM2->SMCR  &= ~ ( TIM_SMCR_SMS_1 | TIM_SMCR_SMS_0 );
 TIM2->SMCR  |=   TIM_SMCR_SMS_2;

Тобто, після того, як канал CH1 захопить (збереже в регістр CCR1), регістр-лічильник, CNT, занулиться -- відлік почнеться з нуля.

Все, дозволяємо захоплення на каналах 1 і 2, дозволяємо переривання, вмикаємо таймер:

 TIM2->CCER |= ( TIM_CCER_CC1E | TIM_CCER_CC2E ); // Enable capture

 TIM2->DIER |= ( TIM_DIER_CC1IE | TIM_DIER_CC2IE ); // Enable interrupt on capture
 TIM2->DIER |= TIM_DIER_UIE; // Enable interrupt on update -- for long counter;

 TIM2->CR1  |= TIM_CR1_CEN;

Як і раніше, перш ніж перейти до головного циклу, глянемо на процедуру обробки переривання від таймера TIM2:

#define  reload_val UINT16_MAX

volatile int timer_overflows = 0;

volatile bool is_overflow = false;
volatile bool have_pulse = false;
volatile bool have_period = false; // we _have_ period :-)
volatile bool has_started = false; // it _has_ started :-)
volatile int pulse_width;
volatile int pulse_timer_overflows;
volatile int period_width;
volatile int period_timer_overflows;
volatile bool have_data = false;

void TIM2_IRQHandler(void)
{
 // Джерело перевіряти не треба -- цей обробник лише для TIM2.
 // А от походження переривання -- треба
 // Для спрощення, можливість overcapture ігноруємо
 if( TIM2->SR & TIM_SR_CC1IF )
 {
  TIM2->SR &= ~TIM_SR_UIF; // Оновлює лічильник! А ми не хочемо, щоб псувався timer_overflows
  if(has_started)
  {
   if(have_period)
    is_overflow = true;
   have_period = true;
  }
  // Не потрібно -- очищається при читанні CCR1 // TIM2->SR &= ~TIM_SR_CC1IF;
  period_width = TIM2->CCR1;
  period_timer_overflows = timer_overflows;
  timer_overflows = 0;
 }

 if( TIM2->SR & TIM_SR_CC2IF )
 {
  has_started = true;
  if(have_pulse)
   is_overflow = true;
  have_pulse = true;
  pulse_width = TIM2->CCR2;
  pulse_timer_overflows = timer_overflows;
 }

 if( TIM2->SR & TIM_SR_UIF )
 {
  TIM2->SR &= ~TIM_SR_UIF;
  ++timer_overflows;
 }
}

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

Скільки раз таймер оновлювався на момент події захоплення, зберігається разом із вмістом CCRx відповідного каналу -- в змінних period_timer_overflows та pulse_timer_overflows.

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

while(1)
    {
     NVIC_DisableIRQ(TIM2_IRQn); 
     if( !(have_pulse && have_period) )
     {
          //! Ми ще не маємо цілого імпульсу
          NVIC_EnableIRQ(TIM2_IRQn);
          continue;
     } 
     have_pulse_copy = have_pulse;
     have_period_copy = have_period;
     is_overflow_copy = is_overflow;
     if( have_pulse )
     {
      pulse_width_copy = pulse_width;
      pulse_timer_overflows_copy = pulse_timer_overflows;
     }
     if( have_period )
     {
      period_width_copy = period_width;
      period_timer_overflows_copy = period_timer_overflows;
     }
     have_pulse = false;
     have_period = false;
     is_overflow = false;
     NVIC_EnableIRQ(TIM2_IRQn);

     if(is_overflow_copy)
     {
      puts("Overflow -- data streaming too fast!");
     }
     if(have_pulse_copy)
     {
      printf("\tPulse: %i, raw pulse: %i, ows: %i\n",
        pulse_timer_overflows*reload_val + pulse_width_copy ,
        pulse_width_copy, pulse_timer_overflows_copy);
     }
     if(have_period_copy)
     {
      printf("\tPeriod: %i, raw period: %i, ows: %i\n",
        period_timer_overflows*reload_val + period_width_copy ,
        period_width_copy, period_timer_overflows_copy);
      puts("----------");
     }
}

Якось так. В результаті можна побачити таке: 

    Pulse: 208, raw pulse: 208, ows: 0
    Period: 2141, raw period: 2141, ows: 0
----------
    Pulse: 1783, raw pulse: 1783, ows: 0
    Period: 3504, raw period: 3504, ows: 0
----------
    Pulse: 93, raw pulse: 93, ows: 0
    Period: 78827, raw period: 13292, ows: 1
----------
    Pulse: 126, raw pulse: 126, ows: 0
    Period: 3298, raw period: 3298, ows: 0

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

Звичайно, це не зовсім штатне використання режиму, але воно дозволяє подивитися в подробицях особливості його функціонування.



main.c


#include <stdint.h>
#include <stdio.h>
#include <stdbool.h>

#include <stm32f10x.h>
#include <semihosting.h>
#include <inttypes.h>

#ifdef __cplusplus
extern "C"{
#endif

#define  reload_val UINT16_MAX

int timer_overflows = 0;

bool is_overflow = false;
bool have_pulse = false;
bool have_period = false; // we _have_ period :-)
bool has_started = false; // it _has_ started :-)
int pulse_width;
int pulse_timer_overflows;
int period_width;
int period_timer_overflows;
bool have_data = false;

void TIM2_IRQHandler(void)
{
 // Джерело перевіряти не треба -- цей обробник лише для TIM2.
 // А от походження переривання -- треба
 // Для спрощення, можливість overcapture ігноруємо
 if( TIM2->SR & TIM_SR_CC1IF )
 {
  TIM2->SR &= ~TIM_SR_UIF; // Оновлює лічильник! А ми не хочемо, щоб псувався timer_overflows
  if(has_started)
  {
   if(have_period)
    is_overflow = true;
   have_period = true;
  }
  // Не потрібно -- очищається при читанні CCR1 // TIM2->SR &= ~TIM_SR_CC1IF;
  period_width = TIM2->CCR1;
  period_timer_overflows = timer_overflows;
  timer_overflows = 0;
 }

 if( TIM2->SR & TIM_SR_CC2IF )
 {
  has_started = true;
  if(have_pulse)
   is_overflow = true;
  have_pulse = true;
  pulse_width = TIM2->CCR2;
  pulse_timer_overflows = timer_overflows;
 }

 if( TIM2->SR & TIM_SR_UIF )
 {
  TIM2->SR &= ~TIM_SR_UIF;
  ++timer_overflows;
 }
}

#ifdef __cplusplus
}
#endif


int main(void)
{
 RCC->APB2ENR  |= RCC_APB2ENR_IOPAEN; // Дозволяємо тактування порту A

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


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

 TIM2->PSC = 24000-1;
 TIM2->CNT = 0;
 TIM2->ARR = reload_val;

 TIM2->CCMR1 &= ~ TIM_CCMR1_IC1PSC; // Prescaler disabled
 TIM2->CCMR1 &= ~ TIM_CCMR1_IC2PSC; // Prescaler disabled

 TIM2->CCMR1 |=   TIM_CCMR1_IC1F; // Filter -- по максимуму
 TIM2->CCMR1 |=   TIM_CCMR1_IC2F; // Filter -- по максимуму

 TIM2->CCMR1 &= ~ TIM_CCMR1_CC1S_1; // CCR1 <- TI1 (01)
 TIM2->CCMR1 |=   TIM_CCMR1_CC1S_0;

 TIM2->CCER  &= ~ TIM_CCER_CC1P; // Rising edge

 TIM2->CCMR1 |=   TIM_CCMR1_CC2S_1; // CCR2 <- TI1 (10)
 TIM2->CCMR1 &= ~ TIM_CCMR1_CC2S_0;

 TIM2->CCER  |=   TIM_CCER_CC2P; // Falling edge

 // Trigger input -- TI1FP1, TS=101b
 TIM2->SMCR  &= ~ TIM_SMCR_TS_1;
 TIM2->SMCR  |= ( TIM_SMCR_TS_2 | TIM_SMCR_TS_0 );

 // SMS -- 100, reset mode
 TIM2->SMCR  &= ~ ( TIM_SMCR_SMS_1 | TIM_SMCR_SMS_0 );
 TIM2->SMCR  |=   TIM_SMCR_SMS_2;

 TIM2->CCER |= ( TIM_CCER_CC1E | TIM_CCER_CC2E ); // Enable capture

 TIM2->DIER |= ( TIM_DIER_CC1IE | TIM_DIER_CC2IE ); // Enable interrupt on capture
 TIM2->DIER |= TIM_DIER_UIE; // Enable interrupt on update -- for long counter;

 TIM2->SR &= ~TIM_SR_UIF; // Прилітає звідкись...
 TIM2->CR1  |= TIM_CR1_CEN;


 NVIC_EnableIRQ(TIM2_IRQn);

 bool is_overflow_copy = false;
 bool have_pulse_copy = false;
 bool have_period_copy = false;
 int pulse_width_copy;
 int pulse_timer_overflows_copy;
 int period_width_copy;
 int period_timer_overflows_copy;
 while(1)
 {
     NVIC_DisableIRQ(TIM2_IRQn);
     if( !(have_pulse && have_period) )
     {
          //! Ми ще не маємо цілого імпульсу
          NVIC_EnableIRQ(TIM2_IRQn);
          continue;
     } 
     have_pulse_copy = have_pulse;
     have_period_copy = have_period;
     is_overflow_copy = is_overflow;
     if( have_pulse )
     {
      pulse_width_copy = pulse_width;
      pulse_timer_overflows_copy = pulse_timer_overflows;
     }
     if( have_period )
     {
      period_width_copy = period_width;
      period_timer_overflows_copy = period_timer_overflows;
     }
     have_pulse = false;
     have_period = false;
     is_overflow = false;
     NVIC_EnableIRQ(TIM2_IRQn);

     if(is_overflow_copy)
     {
      puts("Overflow -- data streaming too fast!");
     }
     if(have_pulse_copy)
     {
      printf("\tPulse: %i, raw pulse: %i, ows: %i\n",
        pulse_timer_overflows*reload_val + pulse_width_copy ,
        pulse_width_copy, pulse_timer_overflows_copy);
     }
     if(have_period_copy)
     {
      printf("\tPeriod: %i, raw period: %i, ows: %i\n",
        period_timer_overflows*reload_val + period_width_copy ,
        period_width_copy, period_timer_overflows_copy);
      puts("----------");
     }

 }
}



Проект можна скачати тут.

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

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