понеділок, 22 лютого 2016 р.

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

Ієрархія засобів програмування, орієнтована на SPL.
Клікабельно. (c) STMicroelectronics
CMSIS, про яку йшла мова минулого разу, штука потужна, але дуже вже низькорівнева. (Кожен раз себе так відчуваю, ніби попав у кабіну авіалайнера -- ті сотні бітиків, які треба правильно сконфігурувати, інакше нічого не працюватиме.) В STMicroelectronics теж розуміли, що користуватися нею, в більшості випадків, не дуже зручно. Першою спробою надати більш високий рівень абстракції для роботи із їхніми мікроконтролерами, була SPL -- Standard Peripherals Library. (Нагадую, основа CMSIS універсальна, від розробника ядра ARM Cortex-M, ARM Inc., виробники конкретних мікроконтролерів її хіба розширяють, а ось SPL -- специфічна для STM32). 
Зауваження -- не плутайте SPL із STL з С++ -- стандартною бібліотекою шаблонів. ;-)
Повторимо те ж, що і в попередньому пості, засобами  Standard Peripherals Library, SPL. Апаратна конфігурація та ж:
  • PB1 -- Trig
  • PB2 -- Echo
  • PC8 -- синій світлодіод
  • PC9 -- зелений світлодіод

STM32 SPL


Всі необхідні компоненти вже підключено раніше -- на жаль, репозиторій CoIDE трохи змішує частини CMSIS i SPL. Нам будуть потрібні дві підсистеми, робота із якими входить до SPL: RCC -- Reset and clock control для керування тактуванням, GPIO для роботи з пінами. В головну програму, main.c, слід додати:

#include <stm32f10x_gpio.h>
#include <stm32f10x_rcc.h>

Керування тактуванням здійснюється спеціальною функцією:

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

якій передається назва периферії, тактування якої слів ввімкнути (ENABLE) чи вимкнути (DISABLE), в даному випадку: RCC_APB2Periph_GPIOB -- тактування порту B, під'єднаного до шини APB2.

Ініціалізація SysTick та ж, SysTick_Config з CMSIS -- цей таймер належить універсальному ядру "кортексів".

Для ініціалізації портів використовується спеціальна структура, GPIO_InitTypeDef, яку слід наповнити налаштуваннями та передати GPIO_Init:

GPIO_InitTypeDef TrigPin_InitStruct; //PB1

TrigPin_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
TrigPin_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; //Out - Push-Pull
TrigPin_InitStruct.GPIO_Pin = GPIO_Pin_1;
GPIO_Init(GPIOB, &TrigPin_InitStruct);

GPIO_InitTypeDef EchoPin_InitStruct; //PB2
    
EchoPin_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
EchoPin_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
EchoPin_InitStruct.GPIO_Pin = GPIO_Pin_2;
GPIO_Init(GPIOB, &EchoPin_InitStruct);
  • Ця структура має поля із доволі очевидними назвами -- GPIO_Speed, GPIO_Mode, GPIO_Pin. 
  • Швидкості задаються символами типу, GPIO_Speed_50MHz.
  • Режим -- GPIO_Mode_Out_PP для виводу Push-Pull, GPIO_Mode_IN_FLOATING для входу, що плаває (не є притягнутим до 0 чи 1). 
  • Піни, які слід сконфігурувати, задаються біт-масками типу GPIO_Pin_2, їх можна об'єднувати за допомогою побітового OR (АБО), наприклад: GPIO_Pin_8 | GPIO_Pin_9. 
  • GPIO_Init(), крім вказівника на структуру з конфігураційною інформацією отримує також порт, якому належать піни для конфігурування, наприклад GPIOB.
Зауваження 1: структури для конфігурування різної периферії бажано спочатку ініціалізувати відповідними функціями, які для всіх полів вкажуть розумні значення за замовчуванням. Є така функція і для GPIO_InitTypeDef:
void GPIO_StructInit(GPIO_InitTypeDef* GPIO_InitStruct);

В прикладі вище її не застосовував, так як, все рівно, всі поля задавалися вручну, однак краще не опускати їх виклики -- позбавляє багатьох проблем.
Зауваження 2: існує обернена до GPIO_Init функція:
void GPIO_DeInit(GPIO_TypeDef* GPIOx);

яка повертає всі регістри, що керують портом, до значень по замовчуванню. За рештою цікавих функцій та констант, що стосуються GPIO -- див. stm32f10x_gpio.h.

Щоб встановити пін в 1, чи 0, використовується, відповідно:

GPIO_SetBits(GPIOB, GPIO_Pin_1);
GPIO_ResetBits(GPIOB, GPIO_Pin_1);

Як на мене, такий код очевидніший, ніж аналогічний, з CMSIS:
 
GPIOB->BSRR = GPIO_BSRR_BS1;
GPIOB->BSRR = GPIO_BSRR_BR1;

(А про зрозумілість CMSIS- та STL- варіантів коду ініціалізації і мова не йде, хоча останній трохи багатослівніший.)

Прочитати значення на "ніжці":

GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_2)

Повертає вона 0 або 1 (точніше, рекомендується використовувати символи Bit_SET і Bit_RESET, які якраз рівні 0 і 1 відповідно).

У всьому решта програма абсолютно тотожна попередній:

#include <stdint.h>
#include <stdio.h>
#include <stdbool.h>

#include <stm32f10x.h>
#include <stm32f10x_gpio.h>
#include <stm32f10x_rcc.h>
#include <semihosting.h>


volatile uint32_t curTicks;

#ifdef __cplusplus
extern "C"{
#endif
void SysTick_Handler(void) {
 curTicks++;       /* Зроблено ще один тік */
}
#ifdef __cplusplus
}
#endif

/*! В чистому С90 ініціалізатори глобальних змінних самі мають бути
 * "зовсім-зовсім" константами -- заданими define, а не const type
 * Для простоти використовую define.
 * Див. також про ініціалізацію статичних змінних в С++ тут:
 * http://indrekis2.blogspot.com/2015/11/c-arm-gcc-stm32-coide.html
 */
// const uint32_t microseconds_granularity=2;
#define MICROSECONDS_GRANULARITY 2
// Потрібна частота спрацювань таймера SysTick в герцах
#define FREQ ((1000000)/(MICROSECONDS_GRANULARITY))
// const uint32_t freq=1000000/microseconds_granularity;
#define TICKS ((SystemCoreClock)/(FREQ))
//const uint32_t ticks=SystemCoreClock/freq;

/*! Затримка в мікросекундах, грануляції -- microseconds_granularity мікросекунд,
 * одну не витягує сам контролер. 24МГц -- один такт це 42нс=0.042мкс */
inline static void delay_some_us(uint32_t mks)
{
 uint32_t ticks=mks/MICROSECONDS_GRANULARITY;
 int stop_ticks=ticks+curTicks;
 while (curTicks < stop_ticks) {};
}

//! Затримка в мікросекундах
inline static void delay_some_us1 (uint32_t dlyTicks) {
 // Взято з leaflabs Maple
 dlyTicks *= 6; //STM32_DELAY_US_MULT;

 /* Невелика і не дуже точна поправка на час виклику фунції */
 dlyTicks--;
 asm volatile("   mov r0, %[dlyTicks]          \n\t"
              "1: subs r0, #1            \n\t"
              "   bhi 1b                 \n\t"
              :
              : [dlyTicks] "r" (dlyTicks)
              : "r0");
}
// Підрахунок тактів
// http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.ddi0337g/BABBCJII.html
// mov - 2 (не в циклі!)
// subs - 1
// bhi - 1+2 якщо відбувається перехід
// всього в циклі -- 4
// 1 такт = 1/24 мкс = 41.6нс
// 4 такти -- 0.167 мкс
// коеф. = 1/0.167 = 6
// 1 мкс - 24 такти
// Див. також https://my.st.com/public/STe2ecommunities/mcu/Lists/STM32Discovery/DispForm.aspx?ID=3100&RootFolder=%2Fpublic%2FSTe2ecommunities%2Fmcu%2FLists%2FSTM32Discovery%2FSTM32F0Discovery%20timing%20issue
// Апаратний лічильник: http://electronix.ru/forum/lofiversion/index.php/t99506.html
//  однак в STM32F100RB відсутній


int main(void)
{
    // HC-SR04 підключено до PB1 (Trig) i PB2 (Echo)
 // Дозволяємо тактування порту B
 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
 // Дозволяємо тактування порту C, на ньому -- світлодіоди плати STM32VLDiscovery
 // PC8 -- blue, PC9 -- green
 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);

 //! Створювати кілька екземплярів цієї структури немає потреби.
 //! Можна модифікувати одну і ту ж, але так наочніше.
 GPIO_InitTypeDef TrigPin_InitStruct; //PB1
 GPIO_InitTypeDef EchoPin_InitStruct; //PB2
 GPIO_InitTypeDef LEDs_InitStruct; // PC8/PC9

 TrigPin_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
 TrigPin_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; //Out - Push-Pull
 TrigPin_InitStruct.GPIO_Pin = GPIO_Pin_1;
 GPIO_Init(GPIOB, &TrigPin_InitStruct);

 EchoPin_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
 EchoPin_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
 EchoPin_InitStruct.GPIO_Pin = GPIO_Pin_2;
 GPIO_Init(GPIOB, &EchoPin_InitStruct);

 LEDs_InitStruct.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9;
 LEDs_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
 LEDs_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
 GPIO_Init(GPIOC, &LEDs_InitStruct);


 // Перевіримо, чи лінія Echo в нулі поки ми ще не подали імпульс
 if( GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_2)!=0 )
 {
  // Помилка -- імпульсу не було, а на Echo вже одиниця
  GPIO_SetBits(GPIOC, GPIO_Pin_8); // Синім світлодіодом позначатимемо помилку
  while (1);
 }

 // Ініціалізуємо таймер SysTick
 if (SysTick_Config (TICKS))
 {
  // Помилка -- таймер не ініціалізувався
  GPIO_SetBits(GPIOC, GPIO_Pin_8); // Синім позначатимемо помилку
  printf("Error -- SysTick failed to start.\n");
  while(1); // Зависаємо
 }


 uint32_t starting_ticks;
 uint32_t timeout_ticks;
 uint32_t distance_mm;

 bool are_echoing=false;

    while(1)
    {
     //! Якщо не подавали сигнал - вперед, подаємо
  if(!are_echoing)
  {
   // Пауза між вимірами, двічі по 1/4 секунди
   delay_some_us(500000/2);
   // Гасимо світлодіоди
   GPIO_ResetBits(GPIOC, GPIO_Pin_8);
   GPIO_ResetBits(GPIOC, GPIO_Pin_9);
   delay_some_us(500000/2);
   are_echoing=true;
   // Посилаємо імпульс
    GPIO_SetBits(GPIOB, GPIO_Pin_1);
    delay_some_us(12); // 12 ms
    GPIO_ResetBits(GPIOB, GPIO_Pin_1);
   // Починаємо заміряти час
   timeout_ticks=curTicks;
  }else{
   // Якщо ж подали -- чекаємо на імпульс і заміряємо його тривалість
   bool measuring=false;
   uint32_t measured_time;
   // Скільки чекати, поки не вирішити, що імпульсу вже не буде
   uint32_t usoniq_timeout = 100000;
   while( (curTicks - timeout_ticks) < usoniq_timeout )
   {
    // Перевіряємо логічний рівень на Echo
    //printf("B->IDR %X\n",GPIOB->IDR);
    if( GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_2)!=0)
    {
     // Якщо раніше сигналу не було, починаємо заміряти його тривалість
     if(!measuring)
     {
      starting_ticks=curTicks;
      measuring=true;
     }
    }else{
     // Якщо сигнал на Echo був і зник -- заміряємо його тривалість
     if(measuring)
     {
      // Результат буде в міліметрах
      measured_time = (curTicks - starting_ticks)*10*MICROSECONDS_GRANULARITY;
      distance_mm = measured_time/58;
      // Повідомляємо про успішний вимір зеленим світлодіодом
      GPIO_SetBits(GPIOC, GPIO_Pin_9);
      break;
     }
    }
   }
   if(!measuring)
   {
    // Не отримали сигналу, повідомляємо про помилку і пробуємо ще
    GPIO_SetBits(GPIOC, GPIO_Pin_8);
    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;

  }

    }
}


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

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

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

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