пʼятницю, 26 лютого 2016 р.

Далекомір HC-SR04 -- GPIO/C++

Раз вже цей приклад представлено в стількох варіантах, продемонструю, як C++ може бути корисним під час роботи з контролерами. Людина із ніком fede.tft запропонувала цікавий підхід спрощення роботи з STM32 і подібними пристроями, з використанням шаблонів та інших сучасних фішок С++: "STM32 GPIOs and Template Metaprogramming". Замість або плутаного, або багатослівного, підходів, описаних в попередніх постах, без втрати продуктивності (!), можна робити так:

typedef Gpio<GPIOB_BASE,1>  trig;
trig::mode(Mode::OUTPUT);
trig::high();
trig::low();

typedef Gpio<GPIOB_BASE,2>  echo;
echo::mode(Mode::INPUT);
if( echo::value() )
{
...
}


Розглянемо проблему використання засобів С++ із MCU.
  1. При всій спокусливості, "динамічне" ООП витрачає пам'ять. Не вживатиму слова -- неефективно, але для контролерів обійтися без цих витрат може бути критичним. Навіть створення об'єкта порожнього класу потребуватиме мінімум один байт. Віртуальні функції, крім додаткових витрат пам'яті (зазвичай, незначних, але, скажімо, GCC для AVR8 мав погану звичку таблицю віртуальних функцій в RAM поміщати), привносять ще й опосередковані виклики. Знову ж таки, якщо подібний функціонал потрібен -- навряд чи "на коліні" його вдасться реалізувати краще, ніж скориставшись відпрацьованим механізмом -- віртуальними функціями. Однак, по можливості, хотілося б обійтися.  
  2. Навіть без класів, виклик функцій вимагає часу.
  3. Структура регістрів мікроконтролерів далеко не завжди однорідна чи регулярна. Як зовсім простий приклад, для керування першими вісьмома пінами порту STM32 слід використовувати регістр CRL, (Low), а другими вісьмома  -- CRH, (High). Вручну це написати просто, але при спробі створити єдину функцію для всіх пінів, починаються певні труднощі -- конструкція if(pin<8){....} else {} може сповільнювати програму (може і не сповільнювати, якщо оптимізатор справився, але...).
Звичайно, ці проблеми, в більшості  випадків не критичні -- он, всі бібліотеки Arduino так реалізовані, що не завадило платформі бути успішною. Але швидкодія багатьох операцій з використанням засобів Ардуїно менша на порядок, ніж при безпосередній роботі з регістрами. (Див. статті про I2C, наприклад, там ця тема піднімалася на практиці).

 С++ надає засоби для вирішення всіх цих проблем, відповідно: 
  1. Групування функцій в клас має сенс -- навіть чисто з точки зору впорядкування, а щоб обійтися без створення об'єктів, можна скористатися статичними функціями. При тому, ми втрачаємо можливість пов'язати з кожним, скажімо, портом, якийсь стан. Але зазвичай це не проблема -- стан доступний із регістрів. Варіант -- скористатися placement-new та розташовувати об'єкти прямо поверх регістрів -- зазвичай їх блоки неперервні в пам'яті. 
  2. inline-функції якраз вирішують цю проблему.
  3. Метапрограмування з допомогою шаблонів дозволяє робити вибір під час компіляції.

Розглянемо готову реалізацію від fede.tft, яку  можна скачати тут: gpio.h (або тут).

Перерахування enum Mode_  містить всі можливі комбінації режимів (С++11 не використовувалося, за його відсутністю в той час):

class Mode
{
public:
    /**
     * GPIO mode (INPUT, OUTPUT, ...)
     * \example pin.mode(Mode::INPUT);
     */
    enum Mode_
    {
        INPUT              = 0x4, ///Floating Input             (CNF=01 MODE=00)
        INPUT_PULL_UP_DOWN = 0x8, ///Pullup/Pulldown Input      (CNF=10 MODE=00)
        INPUT_ANALOG       = 0x0, ///Analog Input               (CNF=00 MODE=00)
        OUTPUT             = 0x3, ///Push Pull  50MHz Output    (CNF=00 MODE=11)
        OUTPUT_10MHz       = 0x1, ///Push Pull  10MHz Output    (CNF=00 MODE=01)
        OUTPUT_2MHz        = 0x2, ///Push Pull   2MHz Output    (CNF=00 MODE=10)
        OPEN_DRAIN         = 0x7, ///Open Drain 50MHz Output    (CNF=01 MODE=11)
        OPEN_DRAIN_10MHz   = 0x5, ///Open Drain 10MHz Output    (CNF=01 MODE=01)
        OPEN_DRAIN_2MHz    = 0x6, ///Open Drain  2MHz Output    (CNF=01 MODE=10)
        ALTERNATE          = 0xb, ///Alternate function 50MHz   (CNF=10 MODE=11)
        ALTERNATE_10MHz    = 0x9, ///Alternate function 10MHz   (CNF=10 MODE=01)
        ALTERNATE_2MHz     = 0xa, ///Alternate function  2MHz   (CNF=10 MODE=10)
        ALTERNATE_OD       = 0xf, ///Alternate Open Drain 50MHz (CNF=11 MODE=11)
        ALTERNATE_OD_10MHz = 0xd, ///Alternate Open Drain 10MHz (CNF=11 MODE=01)
        ALTERNATE_OD_2MHz  = 0xe  ///Alternate Open Drain  2MHz (CNF=11 MODE=10)
    };
private:
    Mode(); //Just a wrapper class, disallow creating instances
};

Для вибору CRL чи CRH якраз використано шаблонну магію:
  • Є "головна" реалізація, яка отримує, в ролі параметра, вказівник на структуру регістра (у вигляді unsigned int,  через технічні особливості шаблонів), номер піна та булівський параметр, який має значення по замовчуванню, рівне (pin>=8). Ця реалізація вважає, що порт -- "верхній", використовує CRH.
  • А що ж робити із pin<8? Дуже просто -- робиться спеціалізація для випадку, коли третій аргумент шаблону є false.

template<unsigned int P, unsigned char N, bool = N >= 8>
struct GpioMode
{
    inline static void mode(Mode::Mode_ m)
    {
        reinterpret_cast<GPIO_TypeDef*>(P)->CRH &= ~(0xf<<((N-8)*4));
        reinterpret_cast<GPIO_TypeDef*>(P)->CRH |= m<<((N-8)*4);
    }
};

template<unsigned int P, unsigned char N>
struct GpioMode<P, N, false>
{
    inline static void mode(Mode::Mode_ m)
    {
        reinterpret_cast<GPIO_TypeDef*>(P)->CRL &= ~(0xf<<(N*4));
        reinterpret_cast<GPIO_TypeDef*>(P)->CRL |= m<<(N*4);
    }
};

reinterpret_cast потрібен, бо вказівник отримується як unsigned int, решта мало б бути зрозумілим (якщо ні -- див. про функціонування GPIO, наприклад, тут).

Оголошується спеціальний клас Gpio, якому в ролі параметрів шаблона теж передаються назва порту та номер піна. Для керування створено набір простих статичних функцій. Більшість коментарів опущено, див. оригінальну статтю та файл gpio.h, але суть мала б бути зрозумілою:

template<unsigned int P, unsigned char N>
class Gpio
{
public:
    static void mode(Mode::Mode_ m)
    {
        GpioMode<P, N>::mode(m);
    }

    static void high()
    {
        reinterpret_cast<GPIO_TypeDef*>(P)->BSRR= 1<<N;
    }

    static void low()
    {
        reinterpret_cast<GPIO_TypeDef*>(P)->BRR= 1<<N;
    }

    static int value()
    {
        return ((reinterpret_cast<GPIO_TypeDef*>(P)->IDR & 1<<N)? 1 : 0);
    }

    static void pullup()
    {
        high();//When in input pullup/pulldown mode ODR=choose pullup/pulldown
    }

    static void pulldown()
    {
        low();//When in input pullup/pulldown mode ODR=choose pullup/pulldown
    }

private:
    Gpio();//Only static member functions, disallow creating instances
};

Що важливо, за ввімкненої оптимізації ефективність цього коду буде така ж, як і за ручної маніпуляції регістрами. За подробицями див. згадану статтю

Із використанням такого класу, програма виглядатиме наступним чином. (За базу взято CMSIS-приклад, все ж ініціалізацію пристроїв слід робити окремо).

#include <stdint.h>
#include <cstdio>
extern "C"
{
#include <stm32f10x.h>
#include <semihosting.h>
}
#include "gpio.h"

volatile uint32_t curTicks;

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

const uint32_t microseconds_granularity=2;
// Потрібна частота спрацювань таймера SysTick в герцах
const uint32_t freq=1000000/microseconds_granularity;
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) {};
}


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

    // На  Trig подаватимемо сигнал
    typedef Gpio<GPIOB_BASE,1>  trig;
    trig::mode(Mode::OUTPUT);

    // З Echo його зніматимемо
    typedef Gpio<GPIOB_BASE,2>  echo;
    echo::mode(Mode::INPUT);

    //! Піни світлодіодів - теж на вивід.
    typedef Gpio<GPIOC_BASE,8>  error_led;
    typedef Gpio<GPIOC_BASE,9>  OK_led;
    error_led::mode(Mode::OUTPUT);
    OK_led::mode(Mode::OUTPUT);


    // Перевіримо, чи лінія Echo в нулі поки ми ще не подали імпульс
    if( echo::value() )
    {
        // Помилка -- імпульсу не було, а на Echo вже одиниця
        error_led::high();
        printf("Error -- Echo line is high, though no impuls was given\n");
        while(1); // Зависаємо
    }

    // Ініціалізуємо таймер SysTick
    if (SysTick_Config (ticks))
    {
        // Помилка -- таймер не ініціалізувався
        error_led::high();
        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);
            // Гасимо світлодіоди
            error_led::low();
            OK_led::low();
            delay_some_us(500000/2);
            are_echoing=true;
            // Посилаємо імпульс
            trig::high();
            delay_some_us(12);
            trig::low();
            // Починаємо заміряти час
            timeout_ticks=curTicks;
        }else{
            // Якщо ж подали -- чекаємо на імпульс і заміряємо його тривалість
            bool measuring=false;
            uint32_t measured_time;
            // Скільки чекати, поки не вирішити, що імпульсу вже не буде
            uint32_t usoniq_timeout = 100000;
            while( (curTicks - timeout_ticks) < usoniq_timeout )
            {
                // Перевіряємо логічний рівень на Echo
                if( echo::value() )
                {
                    // Якщо раніше сигналу не було, починаємо заміряти його тривалість
                    if(not measuring)
                    {
                        starting_ticks=curTicks;
                        measuring=true;
                    }
                }else{
                    // Якщо сигнал на Echo був і зник -- заміряємо його тривалість
                    if(measuring)
                    {
                        // Результат буде в міліметрах
                        measured_time = (curTicks - starting_ticks)*10*microseconds_granularity;
                        distance_mm = measured_time/58;
                        // Повідомляємо про успішний вимір зеленим світлодіодом
                        OK_led::high();
                        break;
                    }
                }
            }
            if(not measuring)
            {
                // Не отримали сигналу, повідомляємо про помилку і пробуємо ще
                error_led::high();
                printf("Echo signal not arrived in time\n");
            }else{
                printf("Distance: %u, measured time: %u\n",distance_mm, measured_time/10);
            }
            are_echoing=false;

        }

    }
}

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

Повторюся, апаратна конфігурація та ж, що й в інших GPIO-прикладах:
  • PB1 -- Trig
  • PB2 -- Echo
  • PC8 -- синій світлодіод
  • PC9 -- зелений світлодіод 

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

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