четвер, 20 жовтня 2016 р.

Зовсім просто про далекомір HC-SR04 із GPIO/HAL

Взято тут.
Пост: "Далекомір HC-SR04 -- використовуючи GPIO/HAL/STM32CubeMX" написано виходячи із того, що попередні тексти, зокрема: "Далекомір HC-SR04 -- використовуючи GPIO/CMSIS" читачу відомі, виклад зосереджено лише на нюансах роботи з Cube/CoIDE/HAL. 

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

Подробиці генерації проекту та роботи із середовищем тут не описуватимемо, за ними див., посилання вище та "GPIO мікроконтролерів STM32F303 з використанням HAL". Нам достатньо знати, що один пін, в режимі GPIO-Output, виділено під Trig, один -- в режимі GPIO-Input, під Echo:
Звертаюся самі-знаєте-до-кого -- бездумно "перемальовувати" схему із скріншота -- погана ідея. Для запобігання, тут, за можливості, наводжу лише важливі фрагменти.
Підключаємо їх до відповідних пінів далекоміра.  Увага -- він нормально спрацьовує від логічної одинички 3.3В-мікроконтролера, але для свого живлення потребує 5 вольт!

Принципи роботи із даним пристроєм описані тут: "Далекомір HC-SR04 -- огляд", коротко нагадаємо їх:


  1. На вхід TRIG слід подати логічну одиничку на 16 мікросекунд.
  2. Через якийсь час, далекомір встановить пін ECHO в одиницю на час, рівний часу польоту звуку туди і назад.
  3. Вимірявши тривалість імпульсу на ECHO та поділивши отриману величину на дві швидкості звуку, отримуємо віддаль до перешкоди.
  4. Якщо відбитий сигнал не було отримано, з тих чи інших причин, імпульс триватиме 38 мілісекунд.


Примітивний варіант -- HAL/GPIO

Тривалість типового імпульсу буде складати від одиниць до десятків мілісекунд. За 1 мс звук пролітає ~33 см, тобто, імпульс тривалістю 1мс відповідатиме віддалі 33/2 = ~15.5 см. Тому, в грубому наближенні, для заміру часу можна скористатися функцією HAL_GetTick().

Трішки складнішою є проблема затримки на мікросекунди. Детальніше див. "Мікросекундні затримки та відлік мікросекунд для STM32". Вважатимемо, що ми задачу таких затримок вирішили, згідно описаного там, і у нас є функція udelay_asm(), або аналогічна, яка здійснює затримку. Тоді робота із далекоміром може виглядати так:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
  /* USER CODE BEGIN WHILE */
  while (1)
  {
   HAL_GPIO_WritePin(TRIG_GPIO_Port, TRIG_Pin, GPIO_PIN_SET);
   udelay_asm(16);
   HAL_GPIO_WritePin(TRIG_GPIO_Port, TRIG_Pin, GPIO_PIN_RESET);

   while(HAL_GPIO_ReadPin(ECHO_GPIO_Port, ECHO_Pin) == GPIO_PIN_RESET );
     {}
   uint32_t before = HAL_GetTick();
   while(HAL_GPIO_ReadPin(ECHO_GPIO_Port, ECHO_Pin) == GPIO_PIN_SET );
     {}
   uint32_t pulse_time = HAL_GetTick()-before;
   //! Увага, не забудьте додати:
   // monitor arm semihosting enable
   // До  Debug Configurations -> Startup Tab:
   printf("Time: %lu ms, distance: %lu cm\n",
     pulse_time,
     pulse_time*343/20
     );

  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */

  }
  /* USER CODE END 3 */

}

Ніби, все просто. Однак,  такий варіант надміру грубий. Крок вимірювання віддалі складатиме півтора десятки сантиметрів. На практиці було отримано 17 см для (швидкості звуку за) кімнатної температури:


Важливе запитання: куди ж виводить printf()? Коротка відповідь -- текст передається дебагеру за допомогою технології, відомої під назвою SemiHosting. Детально подробиці розглядаються тут: "Стандартна бібліотека C та SemiHosting (на прикладі STM32 і CoIDE)". Для зовсім лінивих внизу буде коротка інструкція.
Таки потрібно часову роздільну здатність, помітно меншу за мілісекунди, в ідеалі -- мікросекунди. Досягнути її, поки частота мікроконтролера не паде нижче декількох мегагерц, в принципі, просто -- див. вже згадану нотатку: "Мікросекундні затримки та відлік мікросекунд для STM32".

Примітивний варіант 2 -- HAL/GPIO/мікросекунди

Отож, нехай у нас є три функції для роботи із мікросекундами, із тим чи іншим способом функціонування:
  • void init_timing() -- ініціалізує необхідну периферію.
  • uint32_t get_us() -- повертає кількість мікросекунд, що минули від якогось моменту в минулому (наприклад, від моменту ініціалізації).
  • void udelay(uint32_t useconds) -- функція затримки на вказану кількість мікросекунд. Може бути не ідеально точною -- давати трохи більші затримки, ніж передано їй.
Тоді робота із далекоміром виглядатиме майже тотожно до наведеного вище:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
  /* USER CODE BEGIN 2 */
  init_timing();
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {

   HAL_GPIO_WritePin(TRIG_GPIO_Port, TRIG_Pin, GPIO_PIN_SET);
   udelay(16);
   HAL_GPIO_WritePin(TRIG_GPIO_Port, TRIG_Pin, GPIO_PIN_RESET);

   while(HAL_GPIO_ReadPin(ECHO_GPIO_Port, ECHO_Pin) == GPIO_PIN_RESET );
     {}
   uint32_t before = get_us();
   while(HAL_GPIO_ReadPin(ECHO_GPIO_Port, ECHO_Pin) == GPIO_PIN_SET );
     {}
   uint32_t pulse_time = get_us()-before;
   //! Увага, не забудьте додати:
   // monitor arm semihosting enable
   // До  Debug Configurations -> Startup Tab:
   printf("Time: %lu us, distance: %lu cm\n",
     pulse_time,
     pulse_time/58);
  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */

  }
  /* USER CODE END 3 */

}


Але результати будуть якісно кращими:


Код, готовий до неприємностей

Вище ми виходили із того, що все чудово працює. Але так буває далеко не завжди. Спробуємо розглянути можливі аварійні ситуації із далекоміром та способи реакції коду на них.
  1. Перш ніж ми подамо TRIG, на цій лінії має бути нуль. (От покази на ECHO -- не гарантуються, лише частина далекомірів цього сімейства дає нульовий сигнал в пасивному стані. Можливо, причина в китайській якості... Не досліджував ретельно.)
  2. Після подання TRIG -- на відповідній лінії має бути 1. 
  3. Зразу після завершення імпульсу на TRIG, ECHO має бути в нулі.
  4. Через якийсь час вона має перейти в стан 1. Будемо вважати, що півсекунди їй більш ніж достатньо.
  5. Ще через якийсь час вона має повернутися до нуля. Будемо вважати, що півсекунди їй також більш ніж достатньо -- максимальна тривалість імпульсу, згідно документації, 38 мс.
  6. Крім того, віддалі, суттєво більші за 4-5м -- гарантовано невірні. 
 Реалізуємо це в коді:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
  /* USER CODE BEGIN WHILE */
  HAL_GPIO_WritePin(TRIG_GPIO_Port, TRIG_Pin, GPIO_PIN_RESET);
  while (1)
  {
   //int e_state = HAL_GPIO_ReadPin(ECHO_GPIO_Port, ECHO_Pin); // Could be 1 here!
   int t_state = HAL_GPIO_ReadPin(TRIG_GPIO_Port, TRIG_Pin);
   while(  t_state == GPIO_PIN_SET )
   {
    printf("Wrong state before triggering, Trig is high\n");
    HAL_Delay(300);
   }
   HAL_GPIO_WritePin(TRIG_GPIO_Port, TRIG_Pin, GPIO_PIN_SET);
   if ( HAL_GPIO_ReadPin(TRIG_GPIO_Port, TRIG_Pin) != GPIO_PIN_SET )
   {
    printf("Line Trig do not went high while triggering.\n");
    HAL_GPIO_WritePin(TRIG_GPIO_Port, TRIG_Pin, GPIO_PIN_RESET);
    HAL_Delay(300);
    continue;
   }
   udelay(16);
   HAL_GPIO_WritePin(TRIG_GPIO_Port, TRIG_Pin, GPIO_PIN_RESET);
   if ( HAL_GPIO_ReadPin(TRIG_GPIO_Port, TRIG_Pin) != GPIO_PIN_RESET )
   {
    printf("Line Trig do not went low after triggering.\n");
    HAL_GPIO_WritePin(TRIG_GPIO_Port, TRIG_Pin, GPIO_PIN_RESET);
    HAL_Delay(300);
    continue;
   }
   if ( HAL_GPIO_ReadPin(ECHO_GPIO_Port, ECHO_Pin) == GPIO_PIN_SET )
   {
    printf("Line ECHO is high too early.\n");
    HAL_Delay(300);
    continue;
   }

   uint32_t watchdog_begin = get_us();
   int didnt_had_1_at_echo = 0;
   while(HAL_GPIO_ReadPin(ECHO_GPIO_Port, ECHO_Pin) == GPIO_PIN_RESET )
   {
    if( get_us() - watchdog_begin > 500000 )
    {
     didnt_had_1_at_echo = 1;
     break;
    }
   }
   if(didnt_had_1_at_echo)
   {
    printf("Line ECHO didn't go high for a long time.\n");
    HAL_Delay(300);
    continue;
   }

   uint32_t before = get_us();
   int didnt_had_0_at_echo = 0;
   while(HAL_GPIO_ReadPin(ECHO_GPIO_Port, ECHO_Pin) == GPIO_PIN_SET )
   {
    if( get_us() - watchdog_begin > 500000 )
    {
     didnt_had_0_at_echo = 1;
     break;
    }
   }
   if(didnt_had_0_at_echo)
   {
    printf("Line ECHO didn't go low after echoing pulse stared for a long time.\n");
    HAL_Delay(300);
    continue;
   }


   uint32_t pulse_time = get_us()-before;
   uint32_t distance = pulse_time/58;
   //! Увага, не забудьте додати:
   // monitor arm semihosting enable
   // До  Debug Configurations -> Startup Tab:
   printf("Time: %lu us, distance: %lu cm\n",
     pulse_time,
     distance);
   if( distance > 500 )
   {
    printf("\tToo far -- possibly no echo at all.");
   }
  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */

  }
  /* USER CODE END 3 */

Як видно, код намагається продовжити виконання після нештатних ситуацій.
Увага! Код писався та тестувався нашвидкуруч, тому надміру прямолінійний. Крім того, він може містити помилки, пропущені критичні ситуації, які міг врахувати і т.д. Але для демонстрації принципів його має бути досить. От свої змінні для кожного випадку залишені свідомо -- для наочності.
Тепер запустимо його на виконання і всілякими способами знущатимемося із далекоміра -- від'єднуватимемо контакти, встановлюватимемо на них примусові логічні рівні (обережно, не закоротіть TRIG -- підключайте його до землі чи живлення лише через резистор, не менше 200Ом, а краще 10кОм+, та й з ECHO краще робити так само).

Результат:

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


Робота на перериваннях

Очікувати, поки лінія прийде в стан 1, а потім перейде назад, в нуль -- дещо вульгарно. Краще скористатися перериванням від зовнішніх подій, EXTIx.
Детальний пост про них в найближчих планах -- трішки застряг, разом із іншими, ще з весни.
Почнемо із простого, "оптимістичного" підходу. Конфігуруємо ECHO:

Дозволяємо обробку відповідного переривання:
Лінії переривання 10-15 та 5-9 обслуговуються єдиним обробником, тому дозволяються теж в одному місці. Детальніше про ці всі нюанси колись іншим разом.
Налаштовуємо спрацювання і по спаду і по фронту сигналу -- щоб ловити обидва краї імпульсу, перейменовуємо пін в ECHOI:

Зауважте, що робиться це в діалозі налаштувань GPIO.

Перегенеровуємо код. (Нюанси перегенерування за використання Semihosting -- див. нижче, а щодо багу в макросі -- див. "Мікросекундні затримки та відлік мікросекунд для STM32").

Обробник переривань, HAL_GPIO_EXTI_Callback(), який заміщає відповідний "слабкий" символ, буде викликатися за кожного разу, коли сигнал на лінії ECHO змінюється. При тому, обробник сам по собі викликається асинхронно по відношенню до головної програми, і не знає, на якому вона етапі в цей момент. Для вирішення таких задач добре підходять скінчені автомати. (Про методику їх застосування ми поговоримо детальніше в наступних постах).

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


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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;


Назви станів говорять самі за себе. Хоча, зараз використовуватимемо далеко не всі.

Тоді обробник переривань може поводитися наступним чином:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
volatile uint32_t echo_start;
volatile uint32_t echo_finish;
volatile uint32_t measured_time;

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
 if (GPIO_Pin == ECHOI_Pin )
 {
  switch (state) {
  case WAITING_FOR_ECHO_START_S: {
   echo_start =  get_us();
   state = WAITING_FOR_ECHO_STOP_S;
   break;
  }
  case WAITING_FOR_ECHO_STOP_S: {
   echo_finish = get_us();
   measured_time = echo_finish - echo_start;
   state = READING_DATA_S;
   break;
  }
  default:
   state = ERROR_S;
  }
 }
}

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

В інших випадках переходить в стан помилки. 

Тоді головний цикл програми виглядатиме так:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
  /* USER CODE BEGIN WHILE */
  while (1)
  {

   HAL_GPIO_WritePin(TRIG_GPIO_Port, TRIG_Pin, GPIO_PIN_SET);
   udelay(16);
   HAL_GPIO_WritePin(TRIG_GPIO_Port, TRIG_Pin, GPIO_PIN_RESET);

   state = WAITING_FOR_ECHO_START_S;

   while( state == WAITING_FOR_ECHO_START_S && state != ERROR_S )
   {}
   if ( state == ERROR_S )
   {
    printf("Unexpected error while waiting for ECHO to start.\n");
    continue;
   }
   while( state == WAITING_FOR_ECHO_STOP_S && state != ERROR_S )
   {}
   if ( state == ERROR_S )
   {
    printf("Unexpected error while waiting for ECHO to finish.\n");
    continue;
   }

   uint32_t distance = measured_time/58;
   //! Увага, не забудьте додати:
   // monitor arm semihosting enable
   // До  Debug Configurations -> Startup Tab:
   printf("Time: %lu us, distance: %lu cm\n",
     measured_time,
     distance);
  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */

  }
  /* USER CODE END 3 */

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

УВАГА! Код вище легковажно відноситься до того, що переривання можуть виникати поки відбувається звертання до виразів! Конкретно тут воно особливих проблем не створює, але будьте уважні! На момент читання/запису state чи measured_time слід забороняти переривання! Звертання навіть до int не обов'язково є атомарним, а між state == WAITING_FOR_ECHO_START_S та state != ERROR_S переривання може вклинитися тільки так. Однак, тут це б зробило код більш заплутаним, а критичним не є, то я дещо злегковажив. Можливо -- даремно. Для ілюстрації, як це робити, див., наприклад "Далекомір HC-SR04 -- два таймери/HAL", змінну state_copy.

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


Додаток: SemiHosting i libc для лінивих


УВАГА!  Тест нижче залишаю для "зворотної сумісності". Для підключення SemiHosting див. виправлений і доповнений варіант із окремого поста: "Зовсім просто про semihosting". (Update: 10-2017)

Що таке Semihosting, як його підключити і використовувати із "старим та добрим" printf(), детально описано тут: "Стандартна бібліотека C та SemiHosting (на прикладі STM32 і CoIDE)", хоча, як видно з назви, для іншого середовища розробки.

Тут розглянемо процедуру для лінивих.
  • Качаємо ось цей архів: semihosting_n_libc_support.zip
  • Його вміст копіюємо в директорію проекту, так щоб файли заголовків (піддиректорія Inc) та текстів -- *.c і асемблерний sh_cmd.s (Src) опинилися у відповідних його піддиректоріях.
  • Оновлюємо проект (в SW4STM32 тиснемо F5).
  • Update 10-2017: З поточним вмістом архіву для SW4STM32 цей крок не потрібен. (Тепер ручне втручання буде потрібно робити тим, хто користується CoIDE).  До скрипта лінкера (STM32F303VCTx_FLASH.ld) додаємо рядок із оголошенням __StackLimit, на яке покладається доданий вище код:
1
2
3
4
5
6
/* Highest address of the user mode stack */
_estack = 0x2000A000;    /* end of RAM */
/* Generate a link error if heap and stack don't fit into RAM */
_Min_Heap_Size = 0x200;      /* required amount of heap  */
_Min_Stack_Size = 0x400; /* required amount of stack */
__StackLimit = _estack - _Min_Stack_Size; /* <========= */

  • Увага! На жаль, кожна перегенерація коду в STM32CubeMX затиратиме скрипт лінкера і цей рядок доведеться додавати знову.
  • Увага! По замовчуванню, SW4STM32 використовує Newlib-nano без підтримки floating-point чисел при вводі-виводі:
    Щоб її додати, після  "-specs=nano.specs" слід вписати: "-u _printf_float" для підтримки виводу double i "-u _scanf_float" для підтримки їх вводу:
    Кожна із цих можливостей потребує доволі багато ресурсів, тому підключати їх варто лише за потреби.
  • Щоб дебагер бачив вивід Semihosting, слід ввімкнути його підтримку. Робиться це, як постійно наголошують приклади вище, у вікні Properties (проекту чи глобальному). Шукаємо там "Run/Debug Settings" -> <активна конфігурація> -> "Edit...". У діалозі, що відкрився, переходимо до вкладки "Startup" і додаємо команду "monitor arm semihosting enable":
Клікабельно!
Після всіх цих маніпуляцій, вивід printf() потраплятиме до дебагера. Як це виглядає, показано на чисельних прикладах вище:



Післямова

Вище коротко розглянуто різні способи роботи із далекоміром, користуючись HAL та конфігураціями, згенерованими STM32CubeMX. Як і в попередньому пості цієї серії, виклад відносно поверхневий, орієнтований на практичне використання.

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

А на разі --
Дякую за увагу!


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

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