пʼятницю, 18 листопада 2016 р.

Далекомір HC-SR04 -- трасування, додаток

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

Один із способів пошуку помилок у таких програмах -- зберегти послідовність виконання програми в самому мікроконтролері, а потім передати її на комп'ютер -- так зване трасування. Воно використовувалося для створення програм, які працюють із далекоміром з використанням таймерів (чесно кажучи, однотаймерний варіант я б без трасування не зміг би до ладу довести, принаймні, це забрало б на порядок більше часу). Розглянемо використаний там підхід.
Увага: дана реалізація дуже примітивна і має багато недоліків! Створювалася нашвидкоруч і без якихось подальших перспектив. На С++ можна зробити багато краще та надійніше. Та й на чистому С є простір для покращення.
Крім того, навіть ідеально реалізований, цей метод придатний не всюди -- він не підійде, наприклад, для дуже чутливої до часів реакції апаратури, або коли пам'яті не вистачає. Однак, на загал, інструмент зручний.
Отож, поїхали.

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

Можливість трасування потрібна далеко не завжди, тому вона контролюється умовною компіляцією та макросом USE_TRACE -- коли його не оголошену, всі макроси журналювання замінюються на порожні вирази.

Відповідні змінні -- глобальні.

typedef enum trace_t{IN_TIM3_IRQ=1, IN_CC1IF, IN_CC3IF, IN_CC3IF_SKIP,
  IN_UIF, IN_CC2IF, OUT_TIM3_IRQ,
  MAIN_IDLE, MAIN_TRIG_EN, MAIN_TRIG_DONE, MAIN_TRIG_OK,
  MAIN_ECHO_EN, MAIN_ECHO_DONE, MAIN_ECHO_OK
} trace_t;

#define TRACE_SIZE  100
volatile trace_t trace[TRACE_SIZE]={0};
volatile uint32_t trace_data[TRACE_SIZE]={0};
volatile int trace_idx = 0;

Розмір буфера -- 100 подій, тому він займає 800 байт -- помітну частину із 8Кб, якими обладнаний мікроконтролер плати STM32VLDiscovery. Для нашого прикладу маленький розмір буфера не є проблемою -- можна очищати його для кожного циклу вимірів.

Змінна trace_idx містить індекс комірки, в яку буде записано наступну подію.

Для запису подій використовується ряд макросів:
#define PUT_TO_TRACE(stg) do{ trace[trace_idx++] = (stg); }while(0)
#define PUT_TO_TRACE_WITH_DATA(stg, data)do{ \
             trace_data[trace_idx] = data; \
             trace[trace_idx++] = (stg); }while(0)
#define PUT_TO_TRACE_NO_IRQ(stg) do{ \
             NVIC_DisableIRQ(TIM3_IRQn); \
             trace[trace_idx++] = (stg); \
             NVIC_EnableIRQ(TIM3_IRQn); }while(0)
#define PUT_TO_TRACE_WITH_DATA_NO_IRQ(stg, data) do{ \
             NVIC_DisableIRQ(TIM3_IRQn); \
             trace_data[trace_idx] = data; \
             trace[trace_idx++] = (stg); \
             NVIC_EnableIRQ(TIM3_IRQn); }while(0)

Кожен із них, зберігши поточну подію -- одну із перерахованих в trace_t, збільшує trace_idx на одиницю -- переходить до наступної комірки. Макроси, котрі отримують дані, зразу зберігають їх в trace_data[]. 

За звертання до буфера може бути конкуренція -- між головною програмою і обробниками переривань. У нас всі переривання мають один і той же пріоритет, тому обробник переривання не може бути перерваним примусово, і в ньому використовується зовсім прості макроси логування, PUT_TO_TRACE() та PUT_TO_TRACE_WITH_DATA(), які безпосередньо пишуть у буфер. Головна програма не може розраховувати, що її не буде перервано обробником переривань, том у ній використовуються трохи складніший і повільніший підхід -- перш ніж щось писати, переривання забороняються. Реалізовано його макросами PUT_TO_TRACE_NO_IRQ() і PUT_TO_TRACE_WITH_DATA_NO_IRQ().

Для очищення буферу трасування використовується відповідна функція: 

void clear_trace()
{
 int i;
 for(i=0; i<TRACE_SIZE; ++i)
 {
  trace[i] = 0;
  trace_data[i] = 0;
 }
 trace_idx = 0;
}

Занулення буфера не є обов'язковим, можна було б обмежитися рядком trace_idx = 0, але як всі коди операцій більші за 0, можна легко відрізнити ще незаповнені комірки буфера -- іноді це корисно.

Для читабельного виводу стану таймера використовується така функція:

void print_status(uint32_t status)
{
 printf("SR=%"PRIu32": ", status);
 if(status & TIM_SR_UIF)
  printf("UIF, ");
 if(status & TIM_SR_CC1IF)
  printf("CC1IF, ");
 if(status & TIM_SR_CC2IF)
  printf("CC2IF, ");
 if(status & TIM_SR_CC3IF)
  printf("CC3IF, ");
 if(status & TIM_SR_CC4IF)
  printf("CC4IF, ");
 if(status & TIM_SR_TIF)
  printf("TIF, ");
 if(status & TIM_SR_CC1OF)
  printf("CC1OF, ");
 if(status & TIM_SR_CC2OF)
  printf("CC2OF, ");
 if(status & TIM_SR_CC3OF)
  printf("CC3OF, ");
 if(status & TIM_SR_CC4OF)
  printf("CC4OF, ");

 puts("");
}

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

#define PRN_CASE(txt) case txt: puts(#txt); break;
void print_trace()
{
 int i;
 for(i = 0; i<trace_idx; ++i)
 {
  switch(trace[i])
  {
  //PRN_CASE(IN_TIM3_IRQ);
  case IN_TIM3_IRQ: printf("IN_TIM3_IRQ."); print_status(trace_data[i]); break;
  //PRN_CASE(IN_CC1IF);
  case IN_CC1IF: printf("IN_CC1IF, ticks: %"PRIu32"\n", trace_data[i]); break;
  //PRN_CASE(IN_CC3IF);
  case IN_CC3IF: printf("IN_CC3IF, state: %"PRIu32"\n", trace_data[i]); break;
  PRN_CASE(IN_CC3IF_SKIP);
  //PRN_CASE(IN_UIF);
  case IN_UIF: printf("IN_UIF, state: %"PRIu32"\n", trace_data[i]); break;
  //PRN_CASE(IN_CC2IF);
  case IN_CC2IF: printf("IN_CC2IF, ticks: %"PRIu32"\n", trace_data[i]); break;
  PRN_CASE(OUT_TIM3_IRQ);
  PRN_CASE(MAIN_IDLE);
  PRN_CASE(MAIN_TRIG_EN);
  PRN_CASE(MAIN_TRIG_DONE);
  PRN_CASE(MAIN_TRIG_OK);
  PRN_CASE(MAIN_ECHO_EN);
  PRN_CASE(MAIN_ECHO_DONE);
  PRN_CASE(MAIN_ECHO_OK);
  }
 }
 puts("---------------------------------\n");
}

Власне, на цьому все. Щодо використання, давайте глянемо на повний текст обробника переривань та функції main() попереднього, одно-таймерного, проекту:


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

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

 }

 if( TIM3->SR & TIM_SR_UIF ) // Перевіряємо джерело
 {
  PUT_TO_TRACE_WITH_DATA(IN_UIF, state);
  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 )
 {
  PUT_TO_TRACE_WITH_DATA(IN_CC2IF, curTicks);
  // TIM3->SR &= ~TIM_SR_CC2IF; // Очищається при читанні CCR2
  measured_time = TIM3->CCR2;
  TIM3_stop(); // Зупиняємо таймер -- інакше потім ще Update прилетить
  // Зловили сигнал
  state = READING_DATA_S;
 }
 PUT_TO_TRACE(OUT_TIM3_IRQ);
}

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");
    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;
     PUT_TO_TRACE(MAIN_IDLE);
     TIM3_CC3_enable();
     state = TRIGGERING_S;
     TIM3_start();

     PUT_TO_TRACE_NO_IRQ(MAIN_TRIG_EN);

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

     PUT_TO_TRACE_NO_IRQ(MAIN_TRIG_DONE);

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

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

     TIM3_CC3_disable();

     impulse_capture_enable();

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

     PUT_TO_TRACE_NO_IRQ(MAIN_ECHO_EN);

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


     impulse_capture_disable();

     PUT_TO_TRACE_NO_IRQ(MAIN_ECHO_DONE);

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

     PUT_TO_TRACE_NO_IRQ( MAIN_ECHO_OK );
     print_trace();
     clear_trace();

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


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

MAIN_IDLE
MAIN_TRIG_EN
IN_TIM3_IRQ.SR=8: CC3IF,
IN_CC3IF, state: 1
OUT_TIM3_IRQ
MAIN_TRIG_DONE
MAIN_TRIG_OK
MAIN_ECHO_EN
IN_TIM3_IRQ.SR=83: UIF, CC1IF, CC4IF, TIF,
IN_CC1IF, ticks: 43
OUT_TIM3_IRQ
IN_TIM3_IRQ.SR=88: CC3IF, CC4IF, TIF,
IN_CC3IF, state: 3
IN_CC3IF_SKIP
OUT_TIM3_IRQ
IN_TIM3_IRQ.SR=84: CC2IF, CC4IF, TIF,
IN_CC2IF, ticks: 105
OUT_TIM3_IRQ
MAIN_ECHO_DONE
MAIN_ECHO_OK
---------------------------------

Distance: 1066 mm, measured time: 6186 us


Якщо щось пішло не так, тоді отримаємо:

Error -- echo do not started.
MAIN_IDLE
MAIN_TRIG_EN
IN_TIM3_IRQ.SR=8: CC3IF,
IN_CC3IF, state: 1
OUT_TIM3_IRQ
MAIN_TRIG_DONE
MAIN_TRIG_OK
MAIN_ECHO_EN
IN_TIM3_IRQ.SR=17: UIF, CC4IF,
IN_UIF, state: 2
OUT_TIM3_IRQ
MAIN_ECHO_DONE
---------------------------------


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

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

Щодо решти -- якраз маємо послідовність подій, описану в попередньому пості.

Можемо переходити до "однотаймерного" варіанту із використанням HAL.



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

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