вівторок, 15 листопада 2016 р.

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

Традиційно, вирішимо задачу із попереднього посту, користуючись тим же підходом, з використанням HAL та SMT32CubeMX.

Апаратна конфігурація та ж:
  • TRIG -- PA10  (TIM1_CH3)
  • ECHO -- PA6 (TIM3_CH1, CH2 indirect)
  • Світлодіод, яскравість якого показує віддаль до перешкоди -- PA0 (TIM2_CH1)
  • Світлодіоди плати, як завжди, синій на піні PB8 і зелений на PB9
  • TIM1 генеруватиме імпульс заданої довжини ~ 10-15 мкс, в ролі TRIG, на PA10
  • TIM3 вимірюватиме тривалість імпульсу на лінії ECHO, PA6
  • TIM2 -- PWM (ШІМ) на PA0
 Візуально це виглядатиме так:
Клікабельно!


Де кожен таймер сконфігуровано відповідним чином:
TIM1: тактування внутрішнє, на CH3 хочемо PWM (ШІМ), зупинятися після події переповнення (нагадую, одноімульсним режимом називають трішки хитрішу річ, хоч і побудовану навколо цього біта).

TIM2: Ще простіше, він тільки генерує ШІМ.

TIM3: насправді, найскладніша конфігурація. Але ми її вже розглядали, говорячи про захоплення ШІМ. Таймер перевантажується за подією на першому каналі, ловлячи початок імпульсу, його завершення ловиться на другому каналі.
Пам'ятаємо, що крім таймерів, слід налаштувати RCC. За подробицями див, наприклад: "Далекомір HC-SR04 -- використовуючи GPIO/HAL/STM32CubeMX".

Переходимо до налаштувань на вкладці Configuration.

Налаштування TIM1. Подробиці в тексті.

  1. FREQ_DIVIDER = 23 -- константа, задана на вкладці User Constants, тобто частота ядра, 24МГц, ділитиметься на FREQ_DIVIDER + 1 = 24, і таймер рахуватиме раз на мікросекунду. Там же задано TOO_FAR_TIMEOUT = 38000 -- максимальна можлива довжина імпульсу, 38 мс, яку далекомір повертає, коли не дочекався луни.
  2. Період таймера.
  3. Як було детально описано в попередньому пості, обрано режим PWM2.
  4. "Імпульс" -- 2, тобто, завдяки PWM2, 2 мкс немає сигналу, потім 15-2 = 13, він є, потім знову немає.
  5. Активним вважається високий рівень, +3.3В.
  6. В неактивному стані на піні -- 0.
Зауважте блок налаштувань, пов'язаних із перериванням -- Break, який ми не чіпаємо. Cube, однак, потурбується, щоб біт MOE було встановлено і сигнал з PWM каналу таки потрапив на пін.
Для тих, у кого таймери TIM2 і т.д. чудово працювали, TIM1 може підсунути сюрприз. Зацитую попередній пост: Важливе зауваження. TIM1 є просунутим таймером, і підтримує функцію break, "аварійну зупинку". Тому поки не встановлено біт MOE -- Master Output Enabled, із регістра BDTR -- Break and Dead-Time Register, нічого подавати на свої виводи він не буде. Лічильник рахуватиме, канали перехоплюватимуть (CC --- Capture Compare), переривання прилітатимуть. І тільки на виводах буде 0. Іншими словами, якщо взяти той самий код для TIM2 чи TIM3, все працюватиме, а для TIM1 -- ні, якщо не додати рядка "TIM1->BDTR |= TIM_BDTR_MOE".

Налаштування TIM2, після попередніх постів, тривіальні:
Налаштування TIM2. Окремих коментарів не потребують.
TIM3 -- трохи складніше налаштований, але, насправді, і про цей режим ми вже достатньо говорили:
Налаштування TIM3, подробиці в тексті.
  1. Подільник той же, що й раніше, тому відлік відбувається раз в мікросекунду.
  2. В ARR -- максимальне можливе значення. Максимальна тривалість імпульсу буде 38000 мс, додамо час, поки пристрій відгукнеться, часу повинно вистачити, тому подію оновлення цього таймера можна буде використовувати, як ознаку, що не дочекалися ECHO. 
  3. Канал 1 (CH1) перезапускатиме таймер, зануляючи CNT.
  4. Канал 1 ловить фронт, початок сигналу.
  5. Робить він це безпосередньо із свого піна.
  6. Фільтр каналу 1 -- на максимум.
  7. Канал 2 (CH2) ловить спадання сигналу -- кінець імпульсу.
  8. Цей канал під'єднаний до того ж піна, що і CH1, тому -- indirect.
Фільтр для другого каналу окремо налаштувати конфігуратор Cube не дає, але це не мало б бути проблемою. (А може він взагалі користується фільтром каналу 1? Не впевнений, може десь в документації про це і пише...)

Також, на вкладці NVIC settings (див. попередній рисунок) слід дозволити переривання від цього таймера:
Дозволяємо переривання від TIM3.
Можна генерувати код, (за потреби, наслідуючи збочений спосіб, описаний раніше, готуємо проект для CoIDE, або зразу користуємося SW4STM32). Після цього можемо починати писати нашу програму.


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 = IDLE_S;
volatile uint32_t measured_time;

Із допоміжних функцій залишилося дві, аналоги решти надає HAL:

inline void set_LED_lum(uint16_t arg)
{
 // TIM2->CCR1 = TIM2->ARR - arg;
 int lm = -6*arg + TIM2->ARR;
 __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, (lm>=0 ? lm : 0) );
}

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;
 }
}

set_LED_lum() використовує макрос __HAL_TIM_SET_COMPARE, який не зовсім очевидний за вмістом, але, в принципі, простий:

#define __HAL_TIM_SET_COMPARE(__HANDLE__, __CHANNEL__, __COMPARE__) \
(*(__IO uint32_t *)(&((__HANDLE__)->Instance->CCR1) + ((__CHANNEL__) >> 2)) = (__COMPARE__))

TIM_CHANNEL_x задані наступним чином:

#define TIM_CHANNEL_1                      ((uint32_t)0x0000)
#define TIM_CHANNEL_2                      ((uint32_t)0x0004)
#define TIM_CHANNEL_3                      ((uint32_t)0x0008)
#define TIM_CHANNEL_4                      ((uint32_t)0x000C)

Регістри CCRx ідуть в пам'яті один за одним. Увага, хоч вони і 16-бітні, під кожен зарезервовано 32 біти. Тобто, фактично, макрос розраховує адресу регістра, починаючи з CCR1, додаючи до нього 0, 1, 2 чи 3, розіменовує отриманий таким чином вказівник, як  uint32_t та записує туди нове значення. (В структурі TIM_TypeDef, яка "лягає" поверх регістрів таймера у пам'яті, CCRx оголошені як поля типу uint32_t).

Для обробки переривань використовуються відповідні функції зворотного виклику, HAL_TIM_PeriodElapsedCallback() -- для події оновлення та HAL_TIM_IC_CaptureCallback() -- для події захоплення.

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
 if( htim->Instance == TIM3 )
 {
  if(state == WAITING_FOR_ECHO_START_S)
  {
   state = ECHO_TIMEOUT_S;
  }
  if(state == WAITING_FOR_ECHO_STOP_S)
  {
   state = ECHO_NOT_WENT_LOW_S;
  }
 }
}

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

void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
 if( htim->Instance == TIM3  )
 {
  if( htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1 )
  {
   __HAL_TIM_CLEAR_FLAG(htim, TIM_FLAG_UPDATE);
   if(state == WAITING_FOR_ECHO_START_S)
    state = WAITING_FOR_ECHO_STOP_S;
  }
  if( htim->Channel == HAL_TIM_ACTIVE_CHANNEL_2 )
  {
   if( state == WAITING_FOR_ECHO_STOP_S )
   {
    measured_time = HAL_TIM_ReadCapturedValue( htim, TIM_CHANNEL_2);
    //measured_time = TIM3->CCR2;
    state = READING_DATA_S;
   }
  }
 }
}

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

Детальніше про ці функції зворотного виклику, див. відповідний пост про обробку переривань таймерів у HAL, а про логіку програми -- в попередньому пості.

Головна програма починається звично, із ініціалізації:

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_TIM1_Init();
 MX_TIM2_Init();
 MX_TIM3_Init();

 /* USER CODE BEGIN 2 */
 // Перевіримо, чи лінія Echo в нулі поки ми ще не подали імпульс
 if ( HAL_GPIO_ReadPin(ECHO_PA6_TIM3_CH1_GPIO_Port, ECHO_PA6_TIM3_CH1_Pin) == GPIO_PIN_SET ) {
  on_error("Error -- Echo line is high, though no impulse was given.",
    true);
 }
 puts("Starting");
 /* USER CODE END 2 */

Детальніше функції ініціалізації зараз не розглядатимемо -- всі їх елементи ми вже обговорювали раніше. Див. також файлі tim.c у проекті.

Запускаємо таймери TIM3 i TIM2:

 HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_1);
 HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_2);
 __HAL_TIM_ENABLE_IT(&htim3, TIM_IT_UPDATE);

 HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);

Так як використання трішки нестандартне, канали та сам TIM3, запускаємо безпосереднім викликом відповідних макросів.

Головний цикл, із поправкою на заміну засобів, залишився таким же:

 while (1) {
  HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_3); // Біт OPM встановлено, але запускаємо як PWM
  HAL_NVIC_DisableIRQ(TIM3_IRQn);
  state = WAITING_FOR_ECHO_START_S;
  HAL_NVIC_EnableIRQ(TIM3_IRQn);

  while(TIM1->CR1 & TIM_CR1_CEN ); // Звичайно, щоб напряму не звертатися до
       // регістрів, тут можна звертатися до прапорця,
       // який виставлятиметься в перериванні,
       // але навіщо так складно і неефективно? :-)

  if( HAL_GPIO_ReadPin(TRIG_PA10_TIM1_CH3_GPIO_Port, TRIG_PA10_TIM1_CH3_Pin) == GPIO_PIN_SET )
  {
   puts("Trigger does not went low!");
   HAL_NVIC_DisableIRQ(TIM3_IRQn);
   state = IDLE_S;
   HAL_NVIC_EnableIRQ(TIM3_IRQn);
   continue;
  }

  while(state != READING_DATA_S &&
     state != ECHO_TIMEOUT_S &&
     state != ECHO_NOT_WENT_LOW_S
     );
  HAL_NVIC_DisableIRQ(TIM3_IRQn);
  state_t state_copy = state;
  state = IDLE_S;
  HAL_NVIC_EnableIRQ(TIM3_IRQn);
  if( HAL_GPIO_ReadPin(ECHO_PA6_TIM3_CH1_GPIO_Port, ECHO_PA6_TIM3_CH1_Pin) == GPIO_PIN_SET )
  {
   puts("Echo line does not went low!");
   if( state_copy == ECHO_NOT_WENT_LOW_S)
    puts("\tConfirmed from interrupt");
  }
  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */
  if( state_copy == ECHO_TIMEOUT_S )
  {
   printf("Echo timeout!\n");
   set_LED_lum(TOO_FAR_TIMEOUT);
  }
  else
  {
   set_LED_lum(measured_time);
   if( measured_time>TOO_FAR_TIMEOUT )
   {
    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);
   }
  }
  HAL_Delay(50);
 }

В одному місці допущено "низькорівневий" код -- біт CEN для TIM3 перевіряємо безпосередньо -- у HAL відповідного макросу чи функції не знайшов, а дозволяти переривання від нього, щоб встановити прапорець-змінну "таймер зупинено", лише щоб позбутися рядка "while(TIM1->CR1 & TIM_CR1_CEN );" мені якось не хочеться.

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

На цьому все. Однак, таймери -- цінний ресурс! Витрачати цілих два (TIM2 не рахуємо, у нього чисто декоративна задача, із пристроєм він безпосередньо не працює) на один далекомір не хотілося б. В наступних постах спробуємо обійтися одним.


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

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