середу, 24 лютого 2016 р.

Далекомір HC-SR04 -- використовуючи GPIO/HAL/STM32CubeMX

Як вже говорилося, CMSIS -- занадто низькорівнева, SPL від неї не далеко втекла. Після років роздумів та роботи, інженери STMicroelecronics випустили чергову спробу --- пакет STM32Cube. Він складається із двох частин: 
  • Коду для мікроконтролерів: бібліотеки, призначеної замінити SPL -- HAL, (Hardware Abstraction Layer) та всіляких middleware ("проміжних")  компонент -- бібліотеку підтримки FAT, бібліотеку для роботи із USB, TCP/IP  стек, RTOS, тощо.
  • STM32CubeMX -- програми для комп'ютера, яка дозволяє графічно конфігурувати периферію, генеруючи код мовою С, який реалізує "намальоване". 
Після  кількох років дозрівання проект став дуже потужним. Особисті враження: як мало треба для щастя -- лише не виставляти ті сотні бітиків вручну, медитуючи над даташітами і аппнотами (appnotes). 

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

Інсталяція 


Скачати STM32CubeMX можна тут. Інсталяцію детально не розглядатиму, вона менш більш типова. Зауважу тільки, що проект модульний -- складається із самої програми та бібліотек для конкретних сімейств мікроконтролерів: STM32F1, STM32F3, STM32F4, STM32L1, STM32F0, і т.д. (повний список див. за посиланням):
Пункт меню із керуванням оновленнями та вікно із списком бібліотек STM32CubeMX.
Відповідно, заінсталювавши сам "Cube", слід встановити бібліотеки для тих сімейств, із якими планується працювати. Як видно із рисунка вище, встановлення бібліотек, а також оновлень, є достатньо тривіальним. Доступна навіть можливість встановлення старіших версій бібліотек.

Всі зображення клікабельні! На жаль, вони або замалі, щоб щось можна було прочитати, або не вміщаються в ширину колонки.

Створення проекту

Початок стандартний -- "New Project". При тому програма дозволяє вибрати мікроконтролер або плату, із списку підтримуваних. Пошук спрощується великою колекцією фільтрів:
Вікно вибору мікроконтролера в STM32CubeMX. Зверніть увагу на фільтри за серією (STM32F0, STM32F1, і т.д. ), лінійкою (STM32F0x0, STM32F0x1, і т.д.), корпусом та (в More Filters) -- розміром EEPROM, Flash, RAM, кількістю I/O-пінів. Ліворуч можна задати необхідну периферію. По середині -- список мікроконтролерів, що задовільняють фільтру. Клікабельно!
Вибір демоплат. Виробник -- лише сама STM. Можна включити опис плат, можна дати запит ініціалізувати всю периферію на платі (корисна річ, особливо для початківців). На жаль, STM32VLDiscovery, (за давністю і слабкістю?) немає в списку. Але і STM32F3Discovery i STM32F4Discovery --- є, а з їх зоопарком периферії, це важливіше. Клікабельно!
Так як нашої плати, STM32VlDiscovery, там немає, обираємо контролер вручну:
Приклад вибору контролера. Для демонстрації, ввімкнено всі додаткові фільтри. Клікабельно!
Отримаємо таку картинку:
Головне вікно проекту. Клікабельно!
Є чотири вкладки. Перша, та що відкрита на рисунку вище, дозволяє вибрати налаштування периферії, яку слід ініціалізувати.

Для цього проекту STM32CubeMX  -- як з гармати по горобцях, але для демонстрації зійде. Отож, нам потрібні:
  • SysTick 
  • PB1 -- Trig, вивід
  • PB2 -- Echo, ввід,
  • PC8 -- вивід, синій світлодіод
  • PC9 -- вивід, зелений світлодіод 
  • Крім того, варто сконфігурувати тактування від зовнішнього кварцу (підсистема RCC).
SysTick, по замовчуванню, керується самим HAL і використовується для його потреб. Налаштування RCC -- див. наступну картинку. Там же -- конфігурування пінів. Зайняті (в результаті попередніх налаштувань) піни підсвічуються зеленим. Периферія, яка конфліктує із вже ініціалізованою (тобто, її не вдасться включити в поточній конфігурації), помічається червоним, периферія, частина функціональності якої тепер недоступна -- жовтим. Піни, необхідні периферії, помічаються самі, для ручного вибору слід клікнути по піну. При цьому отримуємо список його можливих функцій.
Вибір периферії та функції пінів. Вибрано тактування HSE (High-Speed External) від зовнішнього кварцу. Для PC9 видно вікно вибору можливих функцій. Видно, що ADC1 частково недоступний. Якщо розкрити відповідний список, можна побачити, що його IN9 мав використовувати пін PB1. Щоб отримати інформацію про природу конфлікту, можна навести курсор на відповідний елемент (див. наступне зображення). Клікабельно!
Приклад конфлікту по пінах, разом із описом. Він не є проблемою -- ми тут ADC не користуємося. 
Друга вкладка -- Clock Configuration. Тут варто налаштувати тактування, щоб добитися максимальної частоти, 24МГц:
Тактування мікроконтролера. Все зроблено тривіально -- зовнішнє високошвидкісне джерело (HSI), від кварцу 8МГц, через помножувач (PLL -- Phase-locked loop, або система фазового автопідстроювання  частоти), вибравши в ролі системного тактового сигналу -- PLL, до SYSCLK і далі, до високошвидкісної шини AHB. За потреби можна налаштовувати все, що налаштовується. Клікабельно!

Зауважте, яка велика кількість всіляких подільників та множників частот. :-)

Наступний етап -- конфігурування обраних компонент, вкладка Configuration:
Налаштування периферії. Клікабельно!
В принципі, через простоту проекту, тут все вже готове, але глянути може бути цікаво.

Налаштування NVIC та GPIO:

Переривання від SysTick дозволені. Видно можливість налаштування пріоритетів та груп пріоритетів переривань. Клікабельно!
Налаштування режиму окремих пінів GPIO, (вкладка RCC зараз нецікава -- там налаштування пінів, до яких під'єднано кварц). Можна також давати пінам назви. Клікабельно!
Остання вкладка займається розрахунком витрат електроенергії, на ній поки не зупинятимемося.

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

Коли графічне налаштування завершено, можна згенерувати проект, за допомогою пункту Project->Generate Code. Вікно має дві вкладки. Перша -- Project:

Вкладка Project діалогу створення проекту.
На ній можна вибрати назву проекту (це буде ім'я директорії, тому без пробілів, кирилиці чи спецсимволів!, туди ж збереже сам файл проекту "Cube" -- .ioc.), шлях до нього та середовище, для якого генерувати проект. Донедавна тут були лише комерційні оболонки. Зараз з'явилася безкоштовна, на базі Eclipse -- System Workbench for STM32 (SW4STM32). Його і обираємо -- хоча б з тієї точки зору, що схрестити з CoIDE простіше буде. Хоч це і збочення, anyway. :-)

Друга вкладка, Code Generator:
Налаштування генерації коду.
В порівнянні із варіантом по замовчуванню, вибрано "Enable Full Assert" -- це може бути корисно, особливо на початках, та "Backup previously generated files when re-generating" -- не зважаючи на обіцянки, іноді має звичку прибити користувацький код. 

CoIDE

Звичайно, розумнішим було б скористатися згаданим  System Workbench for STM32 для подальшої роботи. Це й перегенерацію проекту спростить -- якщо треба буде щось в "Cube" виправити. Але про нього напишу окремо, а зараз, для одноманітності викладу, скористаємося CoIDE.

Дерево проекту виглядає так:

Нам знадобиться:
  • Вмісти директорії Drivers/CMSIS/Device/ST/STM32F1xx/Include
  • Вмісти директорії Drivers/CMSIS/Inlcude
  • Файл Drivers/CMSIS/Device/ST/STM32F1xx/Source/Templates/system_stm32f1xx.c
  • Вміст Drivers/STM32F1xx_HAL_Driver/Inc, крім  вмісту Legacy (просто не потрібне), 
  • Вміст Drivers/STM32F1xx_HAL_Driver/Src, крім stm32f1xx_hal_msp_template.c. Цей файл -- просто шаблон.
  • Вміст Inc та Src -- власне, коду, згенерованого STM32CubeMX  для нашого проекту (все вище -- CMSIS та HAL). Зокрема, тут знаходиться stm32f1xx_hal_msp.c, згенерований із шаблону, згаданого вище -- якщо шаблон, stm32f1xx_hal_msp_template.c, залишити -- будуть проблеми лінкування.
Зрозуміло, додаючи до проекту, краще всі ці файли структурувати -- створити відповідні групи. З одного боку, в такі моменти видно, що CoIDE зовсім простеньке, місцями -- надміру обмежене (починаючи від відсутності рекурсивного додавання файлів, але далеко не обмежуючись тим). З іншого -- розібратися із структурою проекту корисно, а без примусу хто б тим займався? ;-)

Крім того, Drivers/CMSIS/Device/ST/STM32F1xx/Source/Templates/gcc містить startup-файли для різних контролерів. Нам підійшов би startup_stm32f100xb.s.  Однак, порівнявши його із своїм, про який мова йшла тут, вирішив залишити свій. Але різниця між ними невелика. Файл із бібліотеки CUBE викликає додаткову ініціалізацію:

/* Call static constructors */
    bl __libc_init_array

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

В моєму, старішому за походженням, проекті, адреса початкового стеку називається _eram, в новішому  _estack, але це ім'я задається скриптом лінкера (який у нас також свій). Кілька імен обробників переривань відрізняються: було RTCAlarm_IRQHandler, стало RTC_Alarm_IRQHandler, і двічі щось, що видається мені помилкою. В старішому:

  .weak  I2C1_ER_IRQHandler
  .thumb_set I2C1_ER_IRQHandler,Default_Handler

  .weak  I2C2_EV_IRQHandler
  .thumb_set I2C2_EV_IRQHandler,Default_Handler

  .weak  I2C2_ER_IRQHandler
  .thumb_set I2C2_ER_IRQHandler,Default_Handler

в новішому:

  .weak  I2C1_ER_IRQHandler
  .thumb_set I2C1_ER_IRQHandler,Default_Handler
 
  .weak  I2C2_EV_IRQHandler
  .thumb_set I2C1_EV_IRQHandler,Default_Handler

  .weak  I2C2_ER_IRQHandler
  .thumb_set I2C1_ER_IRQHandler,Default_Handler


аналогічно для SPI2.

Отож, за мотивами попереднього абзацу, із проекту, описаного в попередніх постах про С чи С++, беремо startup-файл (startup_stm32f10x_md_vl.s), за потреби -- скрипт лінкера (), syscalls.c та runtime_config.h  для підтримки стандартної бібліотеки С. Крім того, зручно залишити компоненту SemiHosting -- реалізація від CoIDE, при всій своїй простоті, достатньо зручна.

Також, слід оголосити (Configuration->Compile->Defined symbols) макрос STM32F100xB -- він використовується новішими версіями CMSIS для ідентифікації нашого мікроконтролера (на відміну від макросу STM32F100RBT6B, що використано в CMSIS, який йде із CoIDE).

Ще одним корисним макросом є USE_FULL_ASSERT -- він вкаже компілятору включати в прошивку код, який, за проблем із переданими аргументами чи некоректним використанням функцій HAL, виводитиме (користуючись перенаправлябельним -- retagretable, printf) відповідні повідомлення. Щоправда, він доволі часто спрацьовує і для коректних, максиму -- дещо нетипових, параметрів... Сподіваюся, діагностику покращать в наступних версіях.

Загальний вигляд коду


Згенерований main.c виглядає так (частину коду викинуто, для зменшення об'єму посту):

/* Includes ------------------------------------------------------------------*/
#include "stm32f1xx_hal.h"

/* USER CODE BEGIN Includes */

/* USER CODE END Includes */

/* Private variables ---------------------------------------------------------*/

/* USER CODE BEGIN PV */
/* Private variables ---------------------------------------------------------*/

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);

/* USER CODE BEGIN PFP */
/* Private function prototypes -----------------------------------------------*/

/* USER CODE END PFP */

/* USER CODE BEGIN 0 */

/* USER CODE END 0 */

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

  /* USER CODE BEGIN 2 */

  /* USER CODE END 2 */

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

  /* USER CODE BEGIN 3 */

  }
  /* USER CODE END 3 */

}

/** System Clock Configuration
*/
void SystemClock_Config(void)
{
// Тут купа коду, викинув заради лаконічності, див. нижче та файли проекту.
}

/** Configure pins as 
        * Analog 
        * Input 
        * Output
        * EVENT_OUT
        * EXTI
*/
void MX_GPIO_Init(void)
{
// Тут купа коду, викинув заради лаконічності, див. нижче та файли проекту.
}

/* USER CODE BEGIN 4 */

/* USER CODE END 4 */

#ifdef USE_FULL_ASSERT

/**
   * @brief Reports the name of the source file and the source line number
   * where the assert_param error has occurred.
   * @param file: pointer to the source file name
   * @param line: assert_param error line source number
   * @retval None
   */
void assert_failed(uint8_t* file, uint32_t line)
{
  /* USER CODE BEGIN 6 */
  /* User can add his own implementation to report the file name and line number,
    ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
  /* USER CODE END 6 */

}

#endif

/**
  * @}
  */ 

/**
  * @}
*/ 

/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/

Видно купу секцій USER CODE BEGIN / USER CODE END, відмічених коментарями. Думаю, слід дослухатися до їх рекомендацій -- CUBE, оновлюючи проект, їх поважає, залишаючи код у цих секціях без змін (резервні копії краще мати, звичайно).

Послідовно викликаються функції HAL_Init(), SystemClock_Config(), MX_GPIO_Init(), які ініціалізують периферію, налаштовану графічним інструментом. Про них поговоримо трішки пізніше. Далі йде while(1) -- класичний нескінчений цикл, (хитро розбитий секціями USER CODE на дві частини). В кінці -- реалізація функції assert_failed(), в якій можна організувати вивід інформації про проблеми зручним для програміста способом (скажімо, за допомогою UART чи SemiHosting).

Функція HAL_Init() знаходиться у файлі stm32f1xx_hal.c, її призначення: налаштувати вибірку із випередженням з флеш-пам'яті (Flash prefetch), якщо така є, конфігурування системного таймера та задати початкові налаштування NVIC. Зокрема, HAL використовує SysTick для відліку часу та затримок, тому, по замовчуванню, включається генерація його переривань кожну мілісекунду.

SystemClock_Config() знаходиться в main() і здійснює специфічне для проекту налаштування:

void SystemClock_Config(void)
{

  RCC_OscInitTypeDef RCC_OscInitStruct;
  RCC_ClkInitTypeDef RCC_ClkInitStruct;

  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL3;
  HAL_RCC_OscConfig(&RCC_OscInitStruct);

  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_SYSCLK;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
  HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_0);

  HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000);

  HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);

  /* SysTick_IRQn interrupt configuration */
  HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);
}

В нашому випадку -- встановлює режим тактування, який описано вище, "підтверджує" налаштування SysTick, встановлює пріоритети переривань (зараз -- тільки для SysTick).

MX_GPIO_Init() налаштовує піни згідно обраної конфігурації:

void MX_GPIO_Init(void)
{

  GPIO_InitTypeDef GPIO_InitStruct;

  /* GPIO Ports Clock Enable */
  __GPIOD_CLK_ENABLE();
  __GPIOB_CLK_ENABLE();
  __GPIOC_CLK_ENABLE();

  /*Configure GPIO pin : HC_SR04_Trig_Pin */
  GPIO_InitStruct.Pin = HC_SR04_Trig_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Speed = GPIO_SPEED_LOW;
  HAL_GPIO_Init(HC_SR04_Trig_GPIO_Port, &GPIO_InitStruct);

  /*Configure GPIO pin : HC_SR04_Echo_Pin */
  GPIO_InitStruct.Pin = HC_SR04_Echo_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  HAL_GPIO_Init(HC_SR04_Echo_GPIO_Port, &GPIO_InitStruct);

  /*Configure GPIO pins : Blue_LED_Pin Red_LED_Pin */
  GPIO_InitStruct.Pin = Blue_LED_Pin|Red_LED_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Speed = GPIO_SPEED_LOW;
  HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);

}

Код достатньо очевидний. GPIO_InitStruct знайома ще по SPL, код функції HAL_GPIO_Init знаходиться в Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_hal_gpio.c, він завеликий, щоб я його тут наводив, але дуже рекомендую подивитися!

GPIO


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

Для читання використовується функція HAL_GPIO_ReadPin(). Для даного мікроконтролера (і у цій версії HAL) вона виглядає так:

/**
  * @brief  Reads the specified input port pin.
  * @param  GPIOx: where x can be (A..G depending on device used) to select the GPIO peripheral
  * @param  GPIO_Pin: specifies the port bit to read.
  *         This parameter can be GPIO_PIN_x where x can be (0..15).
  * @retval The input port pin value.
  */
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
  GPIO_PinState bitstatus;

  /* Check the parameters */
  assert_param(IS_GPIO_PIN(GPIO_Pin));

  if ((GPIOx->IDR & GPIO_Pin) != (uint32_t)GPIO_PIN_RESET)
  {
    bitstatus = GPIO_PIN_SET;
  }
  else
  {
    bitstatus = GPIO_PIN_RESET;
  }
  return bitstatus;
}

GPIO_PinState -- перерахування, яке має два можливих значення, GPIO_PIN_SET та GPIO_PIN_RESET. Як видно, ніякої особливої магії, в порівнянні із SPL, немає -- все те ж звертання до регістру IDR відповідного порту із використанням переданої маски. Вартує уваги хіба перевірка, за допомогою assert_param(), чи справді передані номери можуть кодують піни (перевірка зовсім проста, але, все ж, частину помилок здатна відловити).

Користуватися цією функцією можна так:

if( HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_2) == GPIO_PIN_SET)

Так як GPIO_PIN_RESET==0, GPIO_PIN_SET==1, можна скористатися лінивим (хоч і не рекомендованим STM!) способом:

if( HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_2) )

Аналогічно, для зміни стану піна можна використати HAL_GPIO_WritePin():

HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_SET);

Ось з відліком часу складніше. HAL налаштовує таймер для генерації тіків кожної мілісекунди, надаючи доступ до поточного значення лічильника функцією HAL_GetTick(), (а також, функцію HAL_Delay(), яка створює затримку на передану їй кількість мілісекунд, вважаючи, що SysTick сконфігурований на спрацювання раз в мілісекунду).

Однак, для нас це завеликий інтервал -- потрібен доступ до замірів інтервалів часу, співмірних із мікросекундами. Зробити це можна різними способами, найбільш акуратний, напевне,  скористатися іншим таймером. Однак, в цьому проекті, для простоти, я зробив дещо негарно -- просто змінив частоту спрацювання SysTick() -- в функції  SystemClock_Config(), замість HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000) вставив вже звичне по іншим варіантам цього проекту:

HAL_SYSTICK_Config( TICKS );

Пам'ятайте, робити так -- потенційно небезпечно! Код HAL покладається на те, що таймер спрацьовує раз в мілісекунду. В даному, простому, випадку, можна легко перевірити, що для нашого простого проекту нічого не псується, але будьте обережні.

Як би там не було, в порівнянні із іншими проектами, особливо, якщо компілювати без оптимізації, розмір коду зріс і контролер не встигає тепер опрацьовувати переривання кожних дві мікросекунди. Хіба -- кожних три-чотири, тому MICROSECONDS_GRANULARITY встановлено 4. На кінцевий результат це не дуже сильно впливає.

Тепер наш код виглядає так:

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

  /* USER CODE BEGIN 2 */
 // Перевіримо, чи лінія Echo в нулі поки ми ще не подали імпульс
 if( HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_2) )
 {
  // Помилка -- імпульсу не було, а на Echo вже одиниця
  HAL_GPIO_WritePin(GPIOC, GPIO_PIN_8, GPIO_PIN_SET); // Синім позначатимемо помилку
  printf("Error -- Echo line is high, though no impuls was given\n");
  while(1); // Зависаємо
 }

 uint32_t starting_ticks;
 uint32_t timeout_ticks;
 uint32_t distance_mm;

 bool are_echoing=false; // For C needs <stdbool.h>! Some violation of strict C standard here.

  /* USER CODE END 2 */

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

  /* USER CODE BEGIN 3 */
   //! Якщо не подавали сигнал - вперед, подаємо
  if(!are_echoing)
  {
   // Пауза між вимірами, двічі по 1/4 секунди
   delay_some_us(500000/2);
   // Гасимо світлодіоди
   HAL_GPIO_WritePin(GPIOC, GPIO_PIN_8, GPIO_PIN_RESET);
   HAL_GPIO_WritePin(GPIOC, GPIO_PIN_9, GPIO_PIN_RESET);
   delay_some_us(500000/2);
   are_echoing=true;
   // Посилаємо імпульс
   HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_SET);
   delay_some_us(12);
   HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_RESET);
   // Починаємо заміряти час
   timeout_ticks=HAL_GetTick();
  }else{
   // Якщо ж подали -- чекаємо на імпульс і заміряємо його тривалість
   bool measuring=false;
   uint32_t measured_time;
   // Скільки чекати, поки не вирішити, що імпульсу вже не буде
   uint32_t usoniq_timeout = 100000;
   while( (HAL_GetTick() - timeout_ticks) < usoniq_timeout )
   {
    // Перевіряємо логічний рівень на Echo
    //printf("B->IDR %X\n",GPIOB->IDR);
    if( HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_2) )
    {
     // Якщо раніше сигналу не було, починаємо заміряти його тривалість
     if( !measuring )
     {
      starting_ticks=HAL_GetTick();
      measuring=true;
     }
    }else{
     // Якщо сигнал на Echo був і зник -- заміряємо його тривалість
     if(measuring)
     {
      // Результат буде в міліметрах
      measured_time = (HAL_GetTick() - starting_ticks)*10*MICROSECONDS_GRANULARITY;
      distance_mm = measured_time/58;
      // Повідомляємо про успішний вимір зеленим світлодіодом
      HAL_GPIO_WritePin(GPIOC, GPIO_PIN_9, GPIO_PIN_SET);
      break;
     }
    }
   }
   if(!measuring)
   {
    // Не отримали сигналу, повідомляємо про помилку і пробуємо ще
    HAL_GPIO_WritePin(GPIOC, GPIO_PIN_8, GPIO_PIN_SET);
    printf("Echo signal not arrived in time\n");
   }else{
    printf("Distance: %u mm, measured time: %lu us\n",distance_mm, measured_time/10);
   }
   are_echoing=false;

  }
  }
  /* USER CODE END 3 */

}

Скачати відповідний демонстраційний проект можна тут (містить купу зайвого з HAL, зокрема -- шаблони).


3 коментарі:

  1. Hello, I do not understand MICROSECONDS_GRANULARITY. Can you please explain. You have done a good job here. I am working with my STM32F4 Discovery board.

    ВідповістиВидалити
    Відповіді
    1. Hello!

      Reason for existance of this constant is the following. MCU(*) works at 24MHz, so it just cannot handle interrupts once a microsecond (with frequency 1MHz) --- just entering and exiting ISR takes 24 tacts. So I let it count every MICROSECONDS_GRANULARITY microseconds -- 2, 4 or more, lowering time resolution but leaving time for other work.

      I must admit, that described here method of measuring time intervals, is inferior anyway. Using TIMx timers (subject of several other my posts) is much better. It's only advantage is simplicity. Most simple relevant post: http://indrekis2.blogspot.com/2016/10/stm32_20.html (using TIM6). More advanced: http://indrekis2.blogspot.com/2016/03/stm32-hal.html and http://indrekis2.blogspot.com/2016/03/stm32-cmsis.html. (I do understand language problem, but cannot do anything about it right now :-) But do not hesitate to ask.)

      (*) Board STM32F1Discovery with STM32F100RB MCU was used for this posts.

      Thank you for your feedback and praise.

      Видалити