середу, 16 листопада 2016 р.

Далекомір HC-SR04 -- один таймер/CMSIS

Приклад скінченного автомата,
який визначає чи двійкове число
має непарну кількість 0.
(c) Wiki
Чим простіший код, тим краще. Код, де пінами ECHO та TRIG керують окремі таймери відносно простий. Однак, з одного боку, таймери -- ресурс цінний та обмежений. На STM32F100, котрим обладнана наша плата, таймерів, здатних генерувати PWM -- десяток, але вони сильно відрізняються за можливостями. Для реальних пристроїв це не так вже і багато! З іншого боку, частина таймерів мають багато каналів. (Для цього контролера п'ять -- по 4 канали, три -- по 2, і три -- єдиний (комплементарні не рахуючи). 

Захоплення ШІМ, на яке ми покладаємося, потребує двох каналів, генерація TRIG -- ще одного. Отож, одного таймера для керування далекоміром має вистачити. 
Заради простоти я не став вішати на четвертий канал ШІМ для візуального відображення віддалі світлодіодом. Залишимо це на домашнє завдання. Видається, це можливо, хоч гарантії, не зробивши, не дам.
Користуватимемося таймером TIM3, його CH1 i CH2 займатимуться захопленням ECHO, а CH3 -- TRIG. Тому, без використання ремапінгу, апаратна конфігурація така:
  • TRIG -- PA6 (TIM3_CH1)
  • ECHO -- PB0 (TIM3_CH3)
  • PC8 -- синій світлодіод
  • PC9 -- зелений світлодіод 
 Думаю, можна зробити краще, вважатимемо приклад нижче лише технологічною демонстрацією.

Логіка роботи програми



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

Далі:
  1.  Дозволяємо третій канал -- TRIG. 
  2. Переходимо у стан TRIGGERING_S, запускаємо таймер. 
  3. Чекаємо, поки стан залишається TRIGGERING_S.
  4. Якщо прийшло переривання capture-compare від третього каналу (встановлено прапорець CC3IF в SR), переходимо до WAITING_FOR_ECHO_START_S, зупиняємо таймер.
  5. Якщо ж першим прийшло переповнення, значить, з якихось причин, генерація імпульсу не завершилася успішно (зазвичай -- логічна помилка в програмі). Переходимо в стан TRIG_NOT_WENT_LOW_S, хоча тут назва доволі умовна, можливих причин багато.
  6. Так чи по іншому, після переривання, стан вже не TRIGGERING_S, головна програма продовжує. Перевіряє, чи не маємо аварійного стану (TRIG_NOT_WENT_LOW_S в змінній state, або фізична присутність одинички на TRIG -- на цей момент її вже не має бути). Якщо аварійний -- повертаємося до п. 1. Інакше продовжуємо. В цей момент режим -- WAITING_FOR_ECHO_START_S.
  7. Забороняємо третій канал. Переходимо в режим захоплення -- дозволяємо канали 1 і 2, підлеглий режим -- reset. Запускаємо таймер. (Далекомір дає достатньо часу на ці всі операції).
  8. Чекаємо, поки імпульс прийде і закінчиться.
  9. Якщо маємо переривання захоплення від першого каналу -- імпульс почався. Переходимо в стан WAITING_FOR_ECHO_STOP_S. Так як таймеру встановлено біт OPM, а захоплення по першому каналу зразу "ресетить" його, викликаючи подію оновлення, котра, у свою чергу, його зупиняє, зразу запускаємо знову. Прапорець події оновлення очищаємо. Головна програма чекає далі.
  10. Якщо маємо переривання від другого каналу, імпульс закінчився, читаємо його довжину з CCR2, переходимо в режим READING_DATA_S. Таймер зупиняємо.
  11. Якщо ж першим прилетіло оновлення -- то імпульс або не почався (були на цей момент в TRIGGERING_S), або не закінчився (були в WAITING_FOR_ECHO_STOP_S) -- відповідно змінюємо стан на ECHO_TIMEOUT_S або ECHO_NOT_WENT_LOW_S.
  12. Так чи по іншому, стан змінюється і головна програма продовжує. Канали 1 і 2 забороняємо, підлеглий режим забороняємо. Якщо була помилка, стан не є  READING_DATA_S -- повідомляємо про це, переходимо до п. 1.
  13. Інакше все добре, є дані для виводу. Виводимо, чекаємо і переходимо до п. 1 -- починаємо наново.
 У дещо спрощеному вигляді це можна зобразити так:
Схема функціонування програми. Чотири комірки над і під прямокутниками позначають, чи дозволено канали таймера (CH1/CH2/CH3) та чи таймер запущено (EN), верхні -- до операції, описаної в блоці, нижні -- після. Підпис біля стрілок -- подія, що викликає перехід. Якщо підпису немає -- перехід відбувається в рамках потоку виконання програми.

Стан програми задається звичним способом:

typedef enum state_t{IDLE_S, TRIGGERING_S, WAITING_FOR_ECHO_START_S, 
                     WAITING_FOR_ECHO_STOP_S, TRIG_NOT_WENT_LOW_S, 
                     ECHO_TIMEOUT_S, ECHO_NOT_WENT_LOW_S, READING_DATA_S, 
                     ERROR_S} state_t;

volatile state_t state;
volatile uint32_t measured_time;

Більшість допоміжних функцій схожі або точно такі ж, як і раніше:

inline void TIM3_CC3_enable()
{
 TIM3->CCER |= (TIM_CCER_CC3E); // Capture/Compare 3 output enable -- PB0
}

inline void TIM3_CC3_disable()
{
 TIM3->CCER &= ~(TIM_CCER_CC3E); // Capture/Compare 3 output disable
}

inline void TIM3_start()
{
 TIM3->CR1  |= TIM_CR1_CEN;
}

inline void TIM3_stop()
{
 TIM3->CR1  &= ~TIM_CR1_CEN;
}

inline void impulse_capture_enable()
{
 TIM3->CCER |= ( TIM_CCER_CC1E | TIM_CCER_CC2E );
 // SMS 100: reset mode
 TIM3->SMCR  &= ~ ( TIM_SMCR_SMS_1 | TIM_SMCR_SMS_0 );
 TIM3->SMCR  |=   TIM_SMCR_SMS_2;
}

inline void impulse_capture_disable()
{
 // SMS 000: Slave mode disabled
 TIM3->SMCR  &= ~( TIM_SMCR_SMS );
 TIM3->CCER  &= ~( TIM_CCER_CC1E | TIM_CCER_CC2E );
 //! Перестраховуюся:
 TIM3->CNT = 0;
 TIM3->SR = 0;
}

Зауважте, що функція  impulse_capture_enable(), крім того що дозволяє канали 1 і 2, які використовуються в режимі захоплення ШІМ, переводить таймер у підлеглий (slave) режим із перезапуском (reset) у відповідь на подію, а impulse_capture_disable() не тільки забороняє ці канали, але й вимикає підлеглий режим.

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

void resetTimer()
{
 TIM3_stop(); // Зупиняємо таймер
 impulse_capture_disable(); // Очищає CNT i SR
 TIM3_CC3_disable(); // Capture/Compare 3 output disable
 NVIC_ClearPendingIRQ(TIM3_IRQn);
} 
 
void on_error(const char* text, bool hang)
{
 GPIOC->BSRR = GPIO_BSRR_BS8; // Синім позначатимемо помилку
 puts(text);
 if(hang)
 {
  while(1);
 }
 else {
  resetTimer();
  state = IDLE_S;
 }
}

Ініціалізацію детальніше розглянуто нижче, а звертання до неї в main() вона виглядає так:

int main(void)
{
 enableClock();
 initGPIO();

 initTimer();

 // Перевіримо, чи лінія Echo в нулі поки ми ще не подали імпульс
 if(GPIOB->IDR & GPIO_IDR_IDR0 )
 {
  on_error("Error -- Echo line is high, though no impulse was given.", true);
 }

 // Ініціалізуємо таймер SysTick
 if (SysTick_Config (TICKS))
 {
  on_error("Error -- SysTick failed to start.", true);
 }

 puts("Starting");

де код функції enableClock(), initGPIO(), initTimer(), буде далі, але що вони роблять, має бути зрозумілим і з попереднього поста.

Головний цикл виглядає наступним чином:

while(1)
    {
     //! Idle if here
     if( state != IDLE_S ){
      on_error("Error -- unexpected mode. Should be IDLE_S", false);
      state = IDLE_S;
      continue;
     }
     GPIOC->BSRR = GPIO_BSRR_BS9;
     TIM3_CC3_enable();
     state = TRIGGERING_S;
     TIM3_start();

     //! Echoing -- waiting to finish
     while( state == TRIGGERING_S );

     if( state == TRIG_NOT_WENT_LOW_S ){
      on_error("Trig do not went low.", false);
      continue;
     }

 if( GPIOB->IDR & GPIO_IDR_IDR0 ){
  puts("Echo line does not went low -- physically!");
  continue;
 }


     if( state != WAITING_FOR_ECHO_START_S  ){
      on_error("Error -- unexpected mode. Should be WAITING_FOR_ECHO_START_S", false);
      continue;
     }

     //! Переключаємося на очікування луни

     TIM3_CC3_disable();
     impulse_capture_enable();
     TIM3_start(); // Чекаємо на імпульс

     while( state == WAITING_FOR_ECHO_START_S ||
         state == WAITING_FOR_ECHO_STOP_S
       );

     impulse_capture_disable();

     switch(state)
     {
     case ECHO_NOT_WENT_LOW_S:
      on_error("Error -- echo do not went low.", false);
      continue;
      break;
     case ECHO_TIMEOUT_S:
      on_error("Error -- echo do not started.", false);
      continue;
      break;
     default: ;
     }
     if( state != READING_DATA_S ){
      on_error("Error -- unexpected mode. Should be READING_DATA_S", false);
     }

     if( measured_time>38000 ){
      printf("Tooo far -- no echo received, wating for %"PRIu32" mks\n", measured_time);
     }else{
      uint32_t distance_mm = (measured_time*10)/58;
      printf("Distance: %"PRIu32" mm, measured time: %"PRIu32" us\n",distance_mm, measured_time);
     }

     state = IDLE_S;
     GPIOC->BSRR = GPIO_BSRR_BR9;
     delay_some_us(500000*1);
     GPIOC->BSRR = GPIO_BSRR_BR8;
    }

В принципі, її логіка детально писана вище, тому сенсу коментувати кожен рядок, немає.

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

void TIM3_IRQHandler(void)
{
 if( TIM3->SR & TIM_SR_CC1IF )
 {
  TIM3->SR &= ~TIM_SR_CC1IF; // Ми CCR1 тут не читаємо, тому слід очистити
  TIM3->SR &= ~TIM_SR_UIF;   // Переривання оновлення нам тут не потрібно!
           // Лічильник оновився, але нам досить знати, що
           // імпульс почався
        // Тому цей if перед наступним
  state = WAITING_FOR_ECHO_STOP_S;
  // Через OPM таймер тут зупиниться -- запускаємо знову
  // Інакше capture відбудеться, але в CCR2 Буде 0
  TIM3_start();
  return;
 }

 if( TIM3->SR & TIM_SR_CC3IF )
 {
  TIM3->SR &= ~TIM_SR_CC3IF; // Очищаємо
  // Переривання чи так чи так прилітатиме
  // Можна звичайно його (CC3IF) забороняти, потім дозволяти...
  // але простіше просто перевірити, коли ми сюди потрапили
  if( state == TRIGGERING_S )
  {
   TIM3->SR  &= ~TIM_SR_UIF; // Переривання оновлення нам тут не потрібно!
   TIM3_stop();              // Зупиняємо таймер
   state = WAITING_FOR_ECHO_START_S;
   return;
  }
 }

 if( TIM3->SR & TIM_SR_UIF ) // Перевіряємо джерело
 {
  TIM3->SR &= ~TIM_SR_UIF; //Очищаємо прапорець переривання
  switch(state)
  {
  case IDLE_S: 
   state = ERROR_S;
   break;
  case TRIGGERING_S:
   state = TRIG_NOT_WENT_LOW_S;
   //! Таймер дорахував і зупинився,
   break;
  case WAITING_FOR_ECHO_START_S:
   state = ECHO_TIMEOUT_S; // Ехо не почалося
   break;
  case WAITING_FOR_ECHO_STOP_S:
   state = ECHO_NOT_WENT_LOW_S; // Ехо почалося, але не закінчилося
   break;
  default: ;
  }
 }
 if( TIM3->SR & TIM_SR_CC2IF )
 {
  // TIM3->SR &= ~TIM_SR_CC2IF; // Очищається при читанні CCR2
  measured_time = TIM3->CCR2;
  TIM3_stop(); // Зупиняємо таймер -- інакше потім ще Update прилетить
  // Зловили сигнал
  state = READING_DATA_S;
 }
}

Важливе зауваження: якщо обробка переповнення, UIF, буде першою, логіка роботи програми поламається! Адже, в нашій конфігурації, CC1IF викликає перезапуск таймера і UIF. Особливо важливо за цим пильнувати у високорівневих середовищах (типу HAL), де немає такого повного контролю.

Якось так. Плюс -- економія таймерів. Мінус -- ускладнення логіки коду та збільшення його об'єму.

Ініціалізація

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

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

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

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


void initGPIO()
{
 //! PB0 -- TIM3_CH3, TRIG
 GPIOB->CRL &= ~GPIO_CRL_CNF0_0; // 10 -- AF PP (Alternative function -- push-pull)
 GPIOB->CRL |=  GPIO_CRL_CNF0_1;
 GPIOB->CRL  |= GPIO_CRL_MODE0; //Встановити обидва біти MODE для піна 1 -- швидкість 50MHz

 //! PA6 -- TIM3_CH1, ECHO
 GPIOA->CRL |=  GPIO_CRL_CNF6_0;// 01 -- Input floating
 GPIOA->CRL &= ~GPIO_CRL_CNF6_1;
 GPIOA->CRL  &= ~GPIO_CRL_MODE6; // Має бути 00 -- Input

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


void baseTimerInit()
{
 TIM3->PSC = SystemCoreClock/1000000 - 1; // Подільник частоти -- 1 тік на мікросекунду
 TIM3->ARR = UINT16_MAX;     // Максимальна тривалість імпульса від таймера -- 38 000 мкс
           // Тому, як відбулося переповнення -- ми точно не дочекалися,
           // бо  пройшло ~65 000 мкс

 TIM3->CCR3 = 12;   // TRIG -- 12 мкс

 TIM3->CR1  &= ~TIM_CR1_DIR;    // Відлік -- вверх. (Default)

 TIM3->CR1  |= TIM_CR1_OPM;
}



void trigChannelInit()
{
 // Канал TRIG -- CH3
 // TIM3->CCER |= (TIM_CCER_CC3E); // Capture/Compare 3 output enable -- PB0
 TIM3->CCER &= ~TIM_CCER_CC3P;   // Полярність -- active high (Default)

 TIM3->CCMR2 |= (TIM_CCMR2_OC3M_1 | TIM_CCMR2_OC3M_2); // PWM1 -- OCxM=0b110
 TIM3->CCMR2 &= ~TIM_CCMR2_OC3M_0;
 //TIM3->CCMR2|=(TIM_CCMR2_OC3M_0| TIM_CCMR2_OC3M_1 | TIM_CCMR2_OC3M_2); // PWM2 -- OCxM=0b111

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

 TIM3->CR1  |= TIM_CR1_ARPE;    // Preload для ARR,  вимагається Datasheet для PWM

 TIM3->DIER |= ( TIM_DIER_CC3IE );
}


void echoChannelsInit()
{
 // Канали ECHO -- PWM capture, PA6
 TIM3->CCMR1 &= ~ TIM_CCMR1_IC1PSC; // Prescaler disabled
 TIM3->CCMR1 &= ~ TIM_CCMR1_IC2PSC; // Prescaler disabled

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

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

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

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

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

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

 // SMS 000: Slave mode disabled
 TIM3->SMCR  &= ~ ( TIM_SMCR_SMS );
 // SMS 100: reset mode
 // TIM3->SMCR  &= ~ ( TIM_SMCR_SMS_1 | TIM_SMCR_SMS_0 );
 // TIM3->SMCR  |=   TIM_SMCR_SMS_2;

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

 TIM3->DIER |= ( TIM_DIER_CC1IE | TIM_DIER_CC2IE ); // Enable interrupt on capture
}


void initTimer()
{
 baseTimerInit();
 trigChannelInit();
 echoChannelsInit();

 TIM3->DIER |= TIM_DIER_UIE;
 // При запуску любить зразу переривання прилетіти...
 TIM3->SR = 0;
 NVIC_ClearPendingIRQ(TIM3_IRQn);

 NVIC_EnableIRQ(TIM3_IRQn);
}

Ніяких особливостей, не розглянутих в постах про захоплення ШІМ, генерацію ШІМ, OPM та роботу із далекоміром, користуючись двома таймерами, тут немає, тому від детальних коментарів утримаюся. Варто зауважити хіба, що, оскільки канал CH3 таймера і так забороняється після імпульсу, то використано PWM1 -- проблема із високим рівнем на піні після кожного періоду, розглянута в попередньому пості, не виникатиме.


Завершуючи

Скачати проект можна тут.

Повний текст main.c, не наводжу через громіздкість, та й він детально розібраний вище.

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


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

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