Pull to refresh

Используем template + constexpr для создания масок регистров периферии микроконтроллера на этапе компиляции (C++14)

Reading time 15 min
Views 10K

Введение


Эта небольшая заметка содержит не очевидное решение поставленной ниже задачи, до которого мне пришлось доходить несколько бессонных ночей.

Задача: на основе заданных пользователем данных о том, как должен работать периферийный блок микроконтроллера, получить на этапе компиляции массив масок конфигурации его регистров, которые можно было бы использовать в реальном времени. При этом требуется на этапе компиляции проверить, что все параметры заданные верно (периферия микроконтроллера будет сконфигурирована верна).



Заинтересовавшихся в том, как это можно сделать, прошу под кат.

Оглавление:


  1. Основные проблемы и ограничения C++14.
  2. Решение, основывающиеся на том, что пользователь никогда не ошибается.
  3. Учет пользовательских ошибок.
  4. Расширим возможности класса.
  5. О сокращенных записях.
  6. Итог.

Основные проблемы и ограничения C++14


Как только я столкнулся с задачей, описанной во введении, я сразу же вспомнил про спецификатор языка C++ — constexpr. Поскольку необходимость решения данной задачи была выявлена еще на этапе проектирования библиотеки, то были учтены все ограничения стандарта C++11, на котором изначально планировалось построить библиотеку (невозможность использовать внутри constexpr методов циклы и т.д.), и в качестве языка библиотеки был выбран стандарт C++14, в котором убраны многие ограничения C++11 (C++17 не был выбран намеренно, т.к. он, с точки зрения поставленных перед библиотекой задач, не слишком отличался от C++14, при этом его поддержка у GCC, так же выбранным в качестве основного компилятора библиотеки, на момент написания статьи, не слишком стабильна).

Но несмотря на то, что constexpr функции в C++14 стали практически такими же гибкими, как и функции реального времени, у них остался один жирный минус, перечеркивающий большинство достоинств — отсутствие какой-либо отладки. Об этом писалось на Хабре тут. Из данной статьи я почерпнул основную мысль:
Но на этом проблемы не заканчиваются. Когда пишешь какую-то constexpr-функцию, которую потом будут часто использовать, хорошо бы возвращать читабельную ошибку. Тут можно ошибочно предположить, что static_assert как раз для этого подходит. Но static_assert использовать не получится, так как параметры функций не могут быть constexpr, из-за чего значения параметров не гарантированно будут известны на этапе компиляции.

Как же выводить ошибки? Единственный более-менее нормальный способ, который я нашел, заключается в выбрасывании исключения:
А так как исключения не поддерживаются в constexpr методах, то мы просто будем получать ошибку о том, что использование throw в constexpr невозможно. В случае, если мы обрабатывали что-то в цикле, то мы никогда не сможем узнать, на каком именно элементе мы упали (при проверке какого элемента мы вызвали исключение).

И того, ситуация следующая: static_assert нельзя, throw нельзя, printf и прерывание компиляции — нельзя.

Решение, основывающиеся на том, что пользователь никогда не ошибается


Как же быть со всеми этими ограничениями и невозможностью отладки? Для начала предположим, что ПОЛЬЗОВАТЕЛЬ НИКОГДА НЕ ОШИБАЕТСЯ (да, футуристично, но пока примем это за аксиому). Тогда constexpr, выходит, в принципе не нуждается в отладке и выбрасыванию исключений о том, что параметры входных данных неверны.

Для примера рассмотрим класс объекта управления выводом порта микроконтроллера, изначально кем-то включенным (подан тактовый сигнал на блок периферии) и с настроенным на выход выводом с нужной скоростью (самое то, чтобы мигать светодиодом). По сути, объект нашего класса должен иметь методы для переключения состояния вывода из 1 в 0 и обратно. Большего от нашего объекта не требуется.

Однако для того, чтобы не плодить сущности, предположим, что у нас есть некоторая структура, описывающая конфигурацию одного вывода целиком. Эту структуру использует не только класс нашего объекта, но и другие (например те, которые заранее для нас инициализировали модуль и сконфигурировали вывод в нужное состояние). Структура эта будет выглядеть так:

/*
 * Структура конфигурации вывода.
 */
struct __attribute__( ( packed ) ) pin_config_t {
    EC_PORT_NAME                port;             // Имя порта 
                                                  // ( пример: EC_PORT_NAME::A ).
    EC_PORT_PIN_NAME            pin_name;         // Номер вывода 
                                                  // ( пример: EC_PORT_PIN_NAME::PIN_0 ).
    EC_PIN_MODE                 mode;             // Режим вывода 
                                                  // ( пример: EC_PIN_MODE::OUTPUT ).
    EC_PIN_OUTPUT_CFG           output_config;    // Режим выхода 
                                                  // ( пример: EC_PIN_OUTPUT_CFG::NOT_USE ).
    EC_PIN_SPEED                speed;            // Скорость вывода
                                                  // ( пример: EC_PIN_SPEED::MEDIUM ).
    EC_PIN_PULL                 pull;             // Подтяжка вывода 
                                                  // ( пример: EC_PIN_PULL::NO ).
    EC_PIN_AF                   af;               // Альтернативная функция вывода 
                                                  // ( пример: EC_PIN_AF::NOT_USE ).
    EC_LOCKED                   locked;           // Заблокировать ли настройку данного
                                                  // вывода во время инициализации 
                                                  // global_port объекта 
                                                  // ( пример EC_LOCKED::NOT_LOCKED ).
    EC_PIN_STATE_AFTER_INIT     state_after_init; // Состояние на выходе после инициализации
                                                  // ( в случае, если вывод настроен как выход ).
                                                  // (пример EC_PIN_STATE_AFTER_INIT::NO_USE).
};

Структура использует следующие enum class-ы
/**********************************************************************
 * Область enum class-ов.
 **********************************************************************/

/*
 * Перечень выводов каждого порта.
 */
enum class EC_PORT_PIN_NAME {
    PIN_0   = 0,
    PIN_1   = 1,
    PIN_2   = 2,
    PIN_3   = 3,
    PIN_4   = 4,
    PIN_5   = 5,
    PIN_6   = 6,
    PIN_7   = 7,
    PIN_8   = 8,
    PIN_9   = 9,
    PIN_10  = 10,
    PIN_11  = 11,
    PIN_12  = 12,
    PIN_13  = 13,
    PIN_14  = 14,
    PIN_15  = 15
};

/*
 * Режим вывода.
 */
enum class EC_PIN_MODE {
    INPUT   = 0,    // Вход.
    OUTPUT  = 1,    // Выход.
    AF      = 2,    // Альтернативная функция.
    ANALOG  = 3     // Аналоговый режим.
};

/*
 * Режим выхода.
 */
enum class EC_PIN_OUTPUT_CFG {
    NO_USE      = 0,    // Вывод не используется как вывод.
    PUSH_PULL   = 0,    // "Тянуть-толкать".
    OPEN_DRAIN  = 1     // "Открытый сток".
};

/*
 * Скорость выхода.
 */
enum class EC_PIN_SPEED {
    LOW         = 0,    // Низкая.
    MEDIUM      = 1,    // Средняя.
    FAST        = 2,    // Быстрая.
    HIGH        = 3     // Очень быстрая
};

/*
 * Выбор подтяжки
 */
enum class EC_PIN_PULL {
    NO_USE  = 0,    // Без подтяжки.
    UP      = 1,    // Подтяжка к питанию.
    DOWN    = 2     // Подтяжка к земле.
};

/*
 * Выбираем альтернативную функцию, если используется.
 */
enum class EC_PIN_AF {
    AF_0        = 0,
    NO_USE      = AF_0,
    SYS         = AF_0,

    AF_1        = 1,
    TIM1        = AF_1,
    TIM2        = AF_1,

    AF_2        = 2,
    TIM3        = AF_2,
    TIM4        = AF_2,
    TIM5        = AF_2,

    AF_3        = 3,
    TIM8        = AF_3,
    TIM9        = AF_3,
    TIM10       = AF_3,
    TIM11       = AF_3,

    AF_4        = 4,
    I2C1        = AF_4,
    I2C2        = AF_4,
    I2C3        = AF_4,

    AF_5        = 5,
    SPI1        = AF_5,
    SPI2        = AF_5,
    I2S2        = AF_5,


    AF_6        = 6,
    SPI3        = AF_6,
    I2S3        = AF_6,

    AF_7        = 7,
    USART1      = AF_7,
    USART2      = AF_7,
    USART3      = AF_7,

    AF_8        = 8,
    UART4       = AF_8,
    UART5       = AF_8,
    USART6      = AF_8,

    AF_9        = 9,
    CAN1        = AF_9,
    CAN2        = AF_9,
    TIM12       = AF_9,
    TIM13       = AF_9,
    TIM14       = AF_9,

    AF_10       = 10,
    OTG_FS      = AF_10,

    AF_11       = 11,
    ETH         = AF_11,

    AF_12       = 12,
    FSMC        = AF_12,
    SDIO        = AF_12,

    AF_13       = 13,
    DCMI        = AF_13,

    AF_14       = 14,

    AF_15       = 15,
    EVENTOUT    = AF_15
};

/*
 * Разрешено ли блокировать конфигурацию вывода методами set_locked_key_port и
 * set_locked_keys_all_port объекту класса global_port.
 * Важно! Блокировка применяется только один раз объектом global_port. Во время последующей
 * работы заблокировать иные выводы или же отключить блокировку текущих - невозможно.
 * Единственный способ снять блокировку - перезагрузка чипа.
 */
enum class EC_LOCKED {
    NOT_LOCKED  = 0,    // Не блокировать вывод.
    LOCKED      = 1     // Заблокировать вывод.
};

/*
 * Состояние на выходе после инициализации
 * (в случае, если вывод настроен как выход).
 */
enum class EC_PIN_STATE_AFTER_INIT {
    NO_USE  = 0,
    RESET   = 0,
    SET     = 1
};


Как говорилось ранее, объект нашего класса должен только менять состояние на выходе вывода (ножки). Так как библиотека пишется под stm32f2 (и только), то разумным будет использовать для этих целей имеющийся в физическом блоке GPIO каждого порта регистр BSR, который позволяет записью единицы (1) в биты 0-15 устанавливать соответствующий бит (запись единицы (1) в 0-й бит выставит состояние вывода порта 0 в 1), а записью единицы (1) в 16-31 сбрасывать соответствующий бит — 16 (запись единицы (1) в 31-й бит сбросит 31-16 = 15-й вывод порта в 0).

Как видно, задача установки нужного вывода в 1 сводится к записи в BSR регистр 1 << номер_вывода, а сброса в записи 1 << номер_вывода + 16.

Для этих целей нам достаточно взять из полученной от пользователя структуры поля port и pin_name. Все остальные поля нам не нужны.

Обозначим общий вид класса нашего объекта:

class pin {
public:
    constexpr pin ( const pin_config_t* const pin_cfg_array );

    void    set     ( void ) const;
    void    reset   ( void ) const;
    void    set     ( uint8_t state ) const;
    void    set     ( bool state ) const;
    void    set     ( int state ) const;

private:
    constexpr uint32_t  p_bsr_get                ( const pin_config_t* const pin_cfg_array );
    constexpr uint32_t  set_msk_get              ( const pin_config_t* const pin_cfg_array );
    constexpr uint32_t  reset_msk_get            ( const pin_config_t* const pin_cfg_array );

    const uint32_t  p_bsr;
    const uint32_t  bsr_set_msk, bsr_reset_msk;
};

Как видно, класс имеет следующие методы:

  • set без параметров — устанавливает состояние на выходе вывода в 1;
  • reset — сбрасывает состояние на выходе вывода в 0;
  • set c параметрами разных типов — на деле представляет из себя одну функцию (о чем будет далее), которая устанавливает заданное состояние на выходе используя функции выше.

Все эти методы используются пользователем в реальном времени. Рассмотрим их.

/*
 * Метод устанавливает вывод порта в <<1>>,
 * если вывод настроен как выход.
 */
void pin::set ( void ) const {
    *M_U32_TO_P(this->p_bsr) = this->bsr_set_msk;
}

/*
 * Метод устанавливает вывод порта в <<0>>,
 * если вывод настроен как выход.
 */
void pin::reset ( void ) const {
    *M_U32_TO_P(this->p_bsr) = this->bsr_reset_msk;
}

/*
 * Метод выставляет на выход заданное состояние,
 * если вывод настроен как выход.
 */
void pin::set ( uint8_t state ) const {
    if ( state ) {
        this->set();
    } else {
        this->reset();
    }
}

void pin::set ( bool state ) const {
    this->set( static_cast< uint8_t >( state ) );
}

void pin::set ( int state ) const {
    this->set( static_cast< uint8_t >( state ) );
}

Методы set и reset используют приведенный ниже define для явного преобразования значения в uint32_t переменной в указатель на uint32_t переменную.

// Преобразует число в uint32_t переменной в указатель на uint32_t.
// Данные по указателю можно изменять.
#define M_U32_TO_P(point)				((uint32_t *)(point))

На данный момент мы разобрались с тем, как методы объекта работают с готовыми масками, осталось самое главное (то, ради чего и писалась данная статья) подготовить их.

Класс имеет три метода:

  1. set_msk_get — возвращает значение uint32_t переменной, являющееся маской регистра BSR для установки заданного пользователем вывода в <<1>>.
  2. reset_msk_get — возвращает значение uint32_t переменной, являющееся маской регистра BSR для сброса заданного пользователем вывода в <<0>>.
  3. p_bsr_get — возвращает значение uint32_t переменной, содержащее в себе адрес регистра BSR на физической карте памяти микроконтроллера.

Зная, что пользователь точно не ошибся при указании параметров структуры, можем написать следующий код:

/**********************************************************************
 * Область constexpr функций.
 **********************************************************************/

/*
 * Метод возвращает маску установки выхода в "1" через регистр BSR.
 */
constexpr uint32_t pin::set_msk_get ( const pin_config_t* const pin_cfg_array ) {
    return 1 << M_EC_TO_U8(pin_cfg_array->pin_name);
}

/*
 * Метод возвращает маску установки выхода в "0" через регистр BSR.
 */
constexpr uint32_t pin::reset_msk_get ( const pin_config_t* const pin_cfg_array ) {
    return 1 << M_EC_TO_U8( pin_cfg_array->pin_name ) + 16;
}

/*
 * Метод возвращает указатель на регистр BSR, к которому относится вывод.
 */
constexpr uint32_t pin::p_bsr_get( const pin_config_t* const pin_cfg_array ) {
    uint32_t p_port = p_base_port_address_get( pin_cfg_array->port );
    return p_port + 0x18;
}

Эти функции используют define для преобразования значения enum class-а в uint8_t переменную.

// Преобразует enum class в uint8_t.
#define M_EC_TO_U8(ENUM_VALUE)			((uint8_t)ENUM_VALUE)

Так же метод p_bsr_get используют не принадлежащий никакому конкретному классу метод p_base_port_address_get, который принимая значение enum class-а EC_PORT_NAME (имя порта) возвращает физический адрес начала расположения регистров этого порта на физической карте микроконтроллера. Выглядит он следующим образом:

Общий метод p_base_port_address_get
/*
 * Возвращает указатель на базовый адрес выбранного порта ввода-вывода
 * на карте памяти в соответствии с выбранным контроллером.
 */
constexpr uint32_t p_base_port_address_get( EC_PORT_NAME port_name ) {
    switch( port_name ) {
#ifdef PORTA
    case EC_PORT_NAME::A:   return 0x40020000;
#endif
#ifdef PORTB
    case EC_PORT_NAME::B:   return 0x40020400;
#endif
#ifdef PORTC
    case EC_PORT_NAME::C:   return 0x40020800;
#endif
#ifdef PORTD
    case EC_PORT_NAME::D:   return 0x40020C00;
#endif
#ifdef PORTE
    case EC_PORT_NAME::E:   return 0x40021000;
#endif
#ifdef PORTF
    case EC_PORT_NAME::F:   return 0x40021400;
#endif
#ifdef PORTG
    case EC_PORT_NAME::G:   return 0x40021800;
#endif
#ifdef PORTH
    case EC_PORT_NAME::H:   return 0x40021C00;
#endif
#ifdef PORTI
    case EC_PORT_NAME::I:   return 0x40022000;
#endif
    }
}
Конструктор класса, заполняющий константы масок сброса/установки и адреса регистра выглядит следующим образом.

/**********************************************************************
 * Область constexpr конструкторов.
 **********************************************************************/

constexpr pin::pin ( const pin_config_t* const pin_cfg_array ):
    p_bsr               ( this->p_bsr_get( pin_cfg_array ) ),
    bsr_set_msk         ( this->set_msk_get( pin_cfg_array ) ),
    bsr_reset_msk       ( this->reset_msk_get( pin_cfg_array ) ) {};

Технически, уже сейчас можно пользоваться данным классом, но мы ведь помним, что пользователь далеко не всегда может ввести все параметры структуры правильно…

Учет пользовательских ошибок.


Теперь, когда у нас имеется рабочий и отлаженный класс, осталось только доработать проверку входной структуры и можно спокойно использовать объекты нашего класса. Но, как говорилось ранее, сделать это в constexpr комфортно — невозможно. Но решение есть — template. Так как все объекты в коде пользователя должны задаваться глобально (это основное условие использования библиотеки, о котором можно будет почитать в документе ее (библиотеки) описания, ссылку на которую дам в конце статьи), то использование template-ов кажется наиболее разумным. Дело в том, что в template-ах разрешен static_assert. Так же в них можно использовать разного рода код для проведения более сложных проверок. И, что самое главное:

Если создать template class, унаследованный от структуры, провести в конструкторе этого класса все необходимые проверки, а затем объявить объект данного template класса глобально в коде пользователя, то компилятор разрешит применить неявное преобразование типов к той самой структуре, от которой класс был унаследован.

Такую возможность просто нельзя не использовать!

/**********************************************************************
 * Область template оболочек.
 **********************************************************************/
template < EC_PORT_NAME              PORT,
           EC_PORT_PIN_NAME          PIN_NAME,
           EC_PIN_MODE               MODE,
           EC_PIN_OUTPUT_CFG         OUTPUT_CONFIG,
           EC_PIN_SPEED              SPEED,
           EC_PIN_PULL               PULL,
           EC_PIN_AF                 AF,
           EC_LOCKED                 LOCKED,
           EC_PIN_STATE_AFTER_INIT   STATE_AFTER_INIT >
class pin_config_check_param : public pin_config_t {
public:
    constexpr pin_config_check_param(): pin_config_t( {
        .port               = PORT,
        .pin_name           = PIN_NAME,
        .mode               = MODE,
        .output_config      = OUTPUT_CONFIG,
        .speed              = SPEED,
        .pull               = PULL,
        .af                 = AF,
        .locked             = LOCKED,
        .state_after_init   = STATE_AFTER_INIT
    } ) {
/*
 * Проверяем введенные пользователем данные в структуру инициализации.
 */
#if defined(STM32F205RB)|defined(STM32F205RC)|defined(STM32F205RE) \
    |defined(STM32F205RF)|defined(STM32F205RG)
            static_assert( PORT >= EC_PORT_NAME::A && 
                    PORT <= EC_PORT_NAME::H, 
                    "Invalid port name. The port name must be A..H." );
#endif
            
            static_assert( PIN_NAME >= EC_PORT_PIN_NAME::PIN_0 &&
                    PIN_NAME <= EC_PORT_PIN_NAME::PIN_15,
                    "Invalid output name. An output with this name does not"
                    "exist in any port. The output can have a name PIN_0..PIN_15." );

            static_assert( MODE >= EC_PIN_MODE::INPUT &&
                    MODE <= EC_PIN_MODE::ANALOG,
                    "The selected mode does not exist. "
                    "The output can be set to mode: INPUT, OUTPUT, AF or ANALOG." );

            static_assert( OUTPUT_CONFIG == EC_PIN_OUTPUT_CFG::PUSH_PULL ||
                    OUTPUT_CONFIG == EC_PIN_OUTPUT_CFG::OPEN_DRAIN,
                    "A non-existent output mode is selected. "
                    "The output can be in the mode: PUSH_PULL, OPEN_DRAIN." );

            static_assert( SPEED >= EC_PIN_SPEED::LOW &&
                    SPEED <= EC_PIN_SPEED::HIGH,
                    "A non-existent mode of port speed is selected. "
                    "Possible modes: LOW, MEDIUM, FAST or HIGH." );

            static_assert( PULL >= EC_PIN_PULL::NO_USE &&
                    PULL <= EC_PIN_PULL::DOWN,
                    "A non-existent brace mode is selected."
                    "The options are: NO_USE, UP or DOWN." );

            static_assert( AF >= EC_PIN_AF::AF_0 &&
                    AF <= EC_PIN_AF::AF_15,
                    "A non-existent mode of the alternative port function is selected." );

            static_assert( LOCKED == EC_LOCKED::NOT_LOCKED ||
                    LOCKED == EC_LOCKED::LOCKED,
                    "Invalid port lock mode selected." );

            static_assert( STATE_AFTER_INIT == EC_PIN_STATE_AFTER_INIT::NO_USE ||
                    STATE_AFTER_INIT == EC_PIN_STATE_AFTER_INIT::SET,
                    "The wrong state of the output is selected."
                    "The status can be: NO_USE, UP or DOWN." );
    };
};

Теперь мы можем объявить в коде пользователя объект данного класса:

const pin_config_check_param< EC_PORT_NAME::C,        EC_PORT_PIN_NAME::PIN_4,  
                              EC_PIN_MODE::OUTPUT,    EC_PIN_OUTPUT_CFG::PUSH_PULL,   
                              EC_PIN_SPEED::MEDIUM,   EC_PIN_PULL::NO_USE,    
                              EC_PIN_AF::NO_USE,      EC_LOCKED::LOCKED,      
                              EC_PIN_STATE_AFTER_INIT::SET > lcd_res;


После чего, при создании объекта класса pin сослаться не него, как на обычную структуру:

const constexpr pin pin_lcd_res( &lcd_res );

После чего, в коде пользователя можно пользоваться методами этого объекта:

void port_test ( void ) {
    pin_lcd_res.reset();
    pin_lcd_res.set();
}

Расширим возможности класса


Мы получили класс, который вполне годиться для управления ножкой в режиме выхода. Однако, не создавать же отдельный класс для входа? Немного доработаем класс, чтобы им можно было пользоваться как для выводов, сконфигурированных на выход, так и на вход. Так же добавим еще и метод инвертирования состояния на выходе.

Дополним класс двумя (2) константами:

  1. p_bb_odr_read — здесь будет находиться указатель на бит вывода объекта в регистре ODR (выставленное пользователем положение на выходе вывода, если вывод используется на выход). Используется bit-banding область.
  2. p_bb_idr_read — здесь будет находиться указатель на бит вывода объекта в регистре IDR (в данном регистре содержится реальное состояние на входах вывода, в не зависимости от того, как настроен вывод). Используется bit-banding область.

Напишем методы, которые возвращают значения этих констант для конкретного вывода.

/*
 * Метод возвращает указатель на bit_banding
 * область памяти, в которой находится бит состояния входа.
 */
constexpr uint32_t pin::bb_p_idr_read_get ( const pin_config_t* const pin_cfg_array ) {
    uint32_t p_port = p_base_port_address_get( pin_cfg_array->port );
    uint32_t p_idr  = p_port + 0x10;
    return M_GET_BB_P_PER(p_idr, M_EC_TO_U8(pin_cfg_array->pin_name));
}
/*
 * Метод возвращает указатель на bit banding область памяти,
 * с выставленным пользователем состоянием на выходе вывода.
 */
constexpr uint32_t pin::odr_bit_read_bb_p_get ( const pin_config_t* const pin_cfg_array ) {
    uint32_t p_port     = p_base_port_address_get( pin_cfg_array->port );
    uint32_t p_reg_odr  = p_port + 0x14;
    return M_GET_BB_P_PER(p_reg_odr, M_EC_TO_U8(pin_cfg_array->pin_name));
}

Здесь используется define M_GET_BB_P_PER который по uint32_t значению адреса регистра в области периферии и uint32_t номера бита порта возвращает bit-banding адрес этого бита.

define M_GET_BB_P_PER
//*********************************************************************
// Определения, не касающиеся основных модулей.
//*********************************************************************
#define BIT_BAND_SRAM_REF   0x20000000
#define BIT_BAND_SRAM_BASE  0x22000000

//Получаем адрес бита RAM в Bit Banding области.
#define MACRO_GET_BB_P_SRAM(reg, bit) \
  ((BIT_BAND_SRAM_BASE + (reg - BIT_BAND_SRAM_REF)*32 + (bit * 4)))

#define BIT_BAND_PER_REF   ((uint32_t)0x40000000)
#define BIT_BAND_PER_BASE  ((uint32_t)0x42000000)

// Получаем адрес бита периферии в Bit Banding области.
#define M_GET_BB_P_PER(ADDRESS,BIT) \
    ((BIT_BAND_PER_BASE + (ADDRESS - BIT_BAND_PER_REF)*32 + (BIT * 4)))


Допишем в конструктор инициализацию этих команд.

/**********************************************************************
 * Область constexpr конструкторов.
 **********************************************************************/

constexpr pin::pin ( const pin_config_t* const pin_cfg_array ):
    p_bsr               ( this->p_bsr_get( pin_cfg_array ) ),
    p_bb_odr_read       ( this->odr_bit_read_bb_p_get( pin_cfg_array ) ),
    bsr_set_msk         ( this->set_msk_get( pin_cfg_array ) ),
    bsr_reset_msk       ( this->reset_msk_get( pin_cfg_array ) ),
    p_bb_idr_read       ( this->bb_p_idr_read_get( pin_cfg_array ) ) {};

Ну и допишем функции реального времени, которые будут работать с этими константами:

/*
 * Метод инвертирует состояние на выходе вывода,
 * если вывод настроен как выход.
 */
void pin::invert( void ) const {
    if (*M_U32_TO_P_CONST(p_bb_odr_read)) {			// Если был 1, то выставляем 0.
        this->reset();
    } else {
        this->set();
    }
}

/*
 * Метод возвращает состояние на входе вывода.
 */
int pin::read() const {
    return *M_U32_TO_P_CONST(p_bb_idr_read);
}

Здесь используется еще 1 define (M_U32_TO_P_CONST), который преобразует значение, хранящееся в uint32_t переменной в указатель на uint32_t переменную, защищенную от записи.

// Преобразует число в uint32_t переменной в указатель на uint32_t.
// Причем запрещает переписывать то, что по указателю (только чтение).
#define M_U32_TO_P_CONST(point)		((const uint32_t *const)(point))

В конечном итоге, наш класс приобрел следующий вид:

class pin {
public:
    constexpr pin ( const pin_config_t* const pin_cfg_array );

    void    set     ( void ) const;
    void    reset   ( void ) const;
    void    set     ( uint8_t state ) const;
    void    set     ( bool state ) const;
    void    set     ( int state ) const;
    void    invert  ( void ) const;
    int     read    ( void ) const;

private:
    constexpr uint32_t  p_bsr_get                ( const pin_config_t* const pin_cfg_array );
    constexpr uint32_t  set_msk_get              ( const pin_config_t* const pin_cfg_array );
    constexpr uint32_t  reset_msk_get            ( const pin_config_t* const pin_cfg_array );
    constexpr uint32_t  odr_bit_read_bb_p_get    ( const pin_config_t* const pin_cfg_array );
    constexpr uint32_t  bb_p_idr_read_get        ( const pin_config_t* const pin_cfg_array );


    const uint32_t  p_bsr;
    const uint32_t  bsr_set_msk, bsr_reset_msk;
    const uint32_t  p_bb_odr_read, p_bb_idr_read;
};

О сокращенных записях.


Зачастую бывает, что нужно создать структуру конфигурации объекта под определенную задачу (например, под вход ADC). Если таких выводов много, то писать каждый раз все параметры утомительно. Для этого можно использовать template class, который будет использовать наш template class. Для ADC это будет выглядеть следующем образом:


template < EC_PORT_NAME              PORT,
           EC_PORT_PIN_NAME          PIN_NAME >
class pin_config_adc_check_param : public pin_config_check_param< PORT, PIN_NAME,
                                                                  EC_PIN_MODE::INPUT,
                                                                  EC_PIN_OUTPUT_CFG::NO_USE,
                                                                  EC_PIN_SPEED::LOW,
                                                                  EC_PIN_PULL::UP,
                                                                  EC_PIN_AF::NO_USE,
                                                                  EC_LOCKED::LOCKED,
                                                                  EC_PIN_STATE_AFTER_INIT::NO_USE > {
public:
    constexpr pin_config_adc_check_param() {};
};

Объявление в коде займет многократно меньше места:

const pin_config_adc_check_param< EC_PORT_NAME::B, EC_PORT_PIN_NAME::PIN_1 >      adc_left;

Итог


Таким образом, на выходе, мы получили возможность создавать объекты на этапе компиляции, которые никак не используют ОЗУ и не требуют вызова конструктора в реальном времени. При этом мы будем точно уверены в том, что инициализированы они верно.

В данном конкретном случае ошибиться сложно, согласен. Может быть, проверки тут и излишни, но например, когда речь идет о создании настроек конфигурации PLL, тут уже сложнее. Можно что-то не учесть. Например, что какой-то делитель не может быть выставлен в какое-то значение, хотя поле для его ввода позволяет его принять, или же выставленный делитель получает частоту, которая превышает, или наоборот, не дотягивает до границ рекомендуемых значений. В таких случаях возможность проверки на этапе компиляции очень сильно помогают.

Так же стоит заметить, что созданная глобальная структура инициализации объекта класса pin не пойдет в файл основной прошивки. Она будет отброшена компоновщиком как не используемая. В flash пойдут только uint32_t переменные, заполненные конструктором и методы, реально вызванные в программе пользователя.

Приведенный в статье код — часть этой библиотеки. Библиотека еще в начальной стадии разработки. Как будет альфа версия — будет отдельная статья на эту тему.

Отдельное спасибо madcomaker за ответ на Тостере, натолкнувший на идею.
Tags:
Hubs:
+17
Comments 9
Comments Comments 9

Articles