понеділок, 21 листопада 2016 р.

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

Повторимо зроблене в попередньому пості, із використанням HAL/SMT32CubeMX. Такі, трішки нестандартні, режими роботи, HAL безпосередньо не підтримує, але і не заважає сильно -- все ж, абстракція, надана цією бібліотекою, зовсім тоненька.

Нагадаємо апаратну конфігурацію:
  • TRIG -- PA6 (TIM3_CH1)
  • ECHO -- PB0 (TIM3_CH3)
  • PC8 -- синій світлодіод
  • PC9 -- зелений світлодіод 
За потреби, Cube вміє автоматично робити ремапінг. Тут в ньому потреби немає:


До конфігурації таймера слід підходити обережно:

Всі елементи цієї конфігурації вже розглядалися. Звичайно, CH1/CH2 i CH3 працюватимуть розділено, але так ми переконуємо Cube згенерувати необхідну ініціалізацію.

Налаштування TIM3 на вкладці конфігурування теж очевидні:

Стрілками показано найважливіші (а загальний принцип той же, що і в попередньому пості, зокрема, див. туди за тим, що таке FREQ_DIVIDER  і чому ARR = 65535):
  • Підлеглий режим -- перезапуск таймера (reset). Нагадаємо, він використовуватиметься для захоплення початку імпульсу
  • CH1, 1 канал, захоплює початок імпульсу
  • CH2, 2 канал -- кінець імпульсу
  • CH3 генерує сигнал заданої довжини на TRIG
Також пам'ятаємо дозволити переривання від TIM3 на вкладці NVIC Settings.

Переходимо до коду.

main.c

Логіка роботи буде тією ж, що і раніше. Тому стан програми задається так само:

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;

Допоміжні функції із CMSIS не потрібні -- HAL містить достатньо точні їх аналоги, а функції обробки помилок такі ж, тільки з використанням вже розглянутих і/або очевидних макросів HAL:

void resetTimer()
{
 TIM_CCxChannelCmd(htim3.Instance, TIM_CHANNEL_1, TIM_CCx_DISABLE);
 TIM_CCxChannelCmd(htim3.Instance, TIM_CHANNEL_2, TIM_CCx_DISABLE);
 TIM_CCxChannelCmd(htim3.Instance, TIM_CHANNEL_3, TIM_CCx_DISABLE);
 __HAL_TIM_DISABLE(&htim3);
 __HAL_TIM_SET_COUNTER(&htim3, 0);
 __HAL_TIM_CLEAR_IT(&htim3,  TIM_IT_UPDATE | TIM_IT_CC1 | TIM_IT_CC2 | TIM_IT_CC3 );

 HAL_NVIC_ClearPendingIRQ(TIM3_IRQn);
}

void on_error(const char* text, bool hang)
{
 HAL_GPIO_WritePin(GPIOC, GPIO_PIN_8, GPIO_PIN_SET);
 puts(text);
 if(hang)
 {
  while(1);
 }
 else {
  resetTimer();
  state = IDLE_S;

 }
}


Тоді тіло main() починається звично:


int main(void) {

 /* USER CODE BEGIN 1 */

 /* USER CODE END 1 */

 /* MCU Configuration----------------------------------------------------------*/

 /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
 HAL_Init();

 /* Configure the system clock */
 SystemClock_Config();

 /* Initialize all configured peripherals */
 MX_GPIO_Init();
 MX_TIM3_Init();

 /* USER CODE BEGIN 2 */

 /* USER CODE END 2 */

 /* Infinite loop */
 /* USER CODE BEGIN WHILE */
 puts("Starting");
 __HAL_TIM_ENABLE_IT(&htim3, TIM_IT_CC1);
 __HAL_TIM_ENABLE_IT(&htim3, TIM_IT_CC2);
 __HAL_TIM_ENABLE_IT(&htim3, TIM_IT_UPDATE);

Перед початком головного циклу, дозволяємо переривання, які нас цікавлять -- від першого та другого каналів та у відповідь на переповнення. (Переривання від 3-го каналу дозволятимемо в циклі).

Тоді, після матеріалу попередніх постів, головний цикл мав би бути повністю зрозумілим.

while (1) {
  state = IDLE_S;
  HAL_TIM_PWM_Start_IT(&htim3, TIM_CHANNEL_3); // Розбити на частини?
  state = TRIGGERING_S;

  //! 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( HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == GPIO_PIN_SET )
  {
   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;
     }

     TIM_CCxChannelCmd(htim3.Instance, TIM_CHANNEL_1, TIM_CCx_ENABLE);
     TIM_CCxChannelCmd(htim3.Instance, TIM_CHANNEL_2, TIM_CCx_ENABLE);
     __HAL_TIM_ENABLE(&htim3);

  /* USER CODE END WHILE */
  /* USER CODE BEGIN 3 */
     while( state == WAITING_FOR_ECHO_START_S ||
         state == WAITING_FOR_ECHO_STOP_S
       );

     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;
     HAL_Delay(200);

 }
 /* USER CODE END 3 */

Робота третього каналу запускається викликом HAL_TIM_PWM_Start_IT() (див. "Таймери STM32 -- ШІМ/HAL"). Коли він відпрацював, дозволяються канали CH1 i CH2, таймер запускається знову, якщо отримали результат -- виводимо.

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


Коли приходить Capture-compare переривання від CH3, зупиняємо таймер та переводимо програму у стан WAITING_FOR_ECHO_START_S:

void HAL_TIM_OC_DelayElapsedCallback(TIM_HandleTypeDef *htim)
{
 if( htim->Instance == TIM3  )
 {
  if( htim->Channel == HAL_TIM_ACTIVE_CHANNEL_3 )
  {
       TIM_CCxChannelCmd(htim3.Instance, TIM_CHANNEL_3, TIM_CCx_DISABLE);
       // В main() може бути запізно канал забороняти
   __HAL_TIM_DISABLE(htim); // <== Працює лише коли
   // "The counter of a timer instance is disabled only if all the CCx and CCxN
   //  channels have been disabled"
   // Інакше слід вручну робити
   //htim->Instance->CR1 &= ~(TIM_CR1_CEN);
   // Хоч це і суперечить ідеї бібліотеки
   __HAL_TIM_DISABLE_IT(&htim3, TIM_IT_CC3); // А то при оптимізації прилітає
   state = WAITING_FOR_ECHO_START_S;
  }
 }
}

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

Зауваження: номер каналу в полі htim->Channel  -- HAL_TIM_ACTIVE_CHANNEL_3 і число (точніше, бітова маска), яку хочуть більшість функцій -- TIM_CHANNEL_3, це різні величини! Не плутайте!
Дрібне зауваження: принаймні в поточній реалізації HAL, обробник HAL_TIM_IRQHandler() викликає HAL_TIM_OC_DelayElapsedCallback() і HAL_TIM_PWM_PulseFinishedCallback() підряд, (що природно, фізична подіє їм відповідає єдина), тому вибір HAL_TIM_OC_DelayElapsedCallback() -- чисто з логічних міркувань, вона ідейно ближча задачі.
Обробник переповнення вирішує, "як жити далі":

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
 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: ;
 }
}

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

void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
 if( htim->Instance == TIM3  )
 {
  __HAL_TIM_CLEAR_FLAG(htim, TIM_IT_CC3); // А то воно ж натікує! :=)
  //! Порядок обробки каналів у HAL_TIM_IRQHandler () рятує від
  //! додаткових перевірок в HAL_TIM_OC_DelayElapsedCallback()
  if( htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1 )
  {
   __HAL_TIM_CLEAR_FLAG(htim, TIM_FLAG_UPDATE);
   state = WAITING_FOR_ECHO_STOP_S;
   __HAL_TIM_ENABLE(htim); //! Потрібно лише для OPM -- бо тут ми зразу і зупинилися
   return;
  }
  if( htim->Channel == HAL_TIM_ACTIVE_CHANNEL_2 )
  {
   measured_time = HAL_TIM_ReadCapturedValue( htim, TIM_CHANNEL_2);
   resetTimer();
   state = READING_DATA_S;
  }
 }
}

У ній явно очищаємо події від 3-го каналу, щоб HAL_TIM_IRQHandler() ще й її не "опрацював". Далі, якщо подія від першого каналу -- імпульс розпочався, очищаємо подію оновлення, змінюємо стан програми, запускаємо таймер знову -- через біт OPM він тут зупинився, (детальніше про логіку див. попередній пост). Якщо ж від другого каналу -- читаємо дані.
Зауваження: в принципі, задача і без біту OPM, який зупиняє таймер у відповідь на оновлення, успішно вирішується. Але код виходить трішки складнішим, то я вирішив ним таки скористатися.

Ось і все. Як бачимо, нічого складного, а HAL+Cube ще й конфігурування сильно спрощує. Щоправда, тільки якщо в ньому орієнтуватися...

Наступного разу подивимося, як користуватися зовнішніми перериваннями для визначення тривалості імпульсу, а поки --

Дякую за увагу!


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

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