С++, благодаря своей строгой типизации, может помочь программисту на этапе компиляции. На хабре уже довольно много статьей, описывающих как, используя типы, добиться этого, и это прекрасно. Но во всех, что я читал, есть один изъян. Сравним с++ подход и си подход с использованием CMSIS, привычный в мире программирования микроконтроллеров:


some_stream.set (Direction::to_periph)    SOME_STREAM->CR |= DMA_SxCR_DIR_0
   .inc_memory()                                          |  DMA_SxCR_MINC_Msk
   .size_memory (DataSize::word16)                        |  DMA_SxCR_MSIZE_0
   .size_periph (DataSize::word16)                        |  DMA_SxCR_PSIZE_0
   .enable_transfer_complete_interrupt();                 |  DMA_SxCR_TCIE_Msk;

Сразу видно, что с++ подход более читаем, и, поскольку каждая функция принимает конкретный тип, нельзя ошибиться. Си подход не проверяет валидность данных, это ложится на плечи программиста. Как правило, об ошибке узнают только при отладке. Но с++ подход не бесплатен. Фактически, каждая функция имеет своё обращение к регистру, в то время как на си сначала собирается маска из всех параметров на этапе компиляции, так как это всё константы, и записывается в регистр разом. Далее я расскажу, как попытался совместить типобезопасность с++ с минимизацией обращений к регистру. Вы увидите, это значительно проще, чем кажется.


Сначала приведу пример, как бы я хотел, чтобы это выглядело. Желательно, чтобы это не сильно отличалось от уже привычного с++ подхода.


some_stream.set(
     dma_stream::direction::to_periph
   , dma_stream::inc_memory
   , dma_stream::memory_size::byte16
   , dma_stream::periph_size::byte16
   , dma_stream::transfer_complete_interrupt::enable
);

Каждый параметр в методе set — отдельный тип, по которому можно понять, в какой регистр надо записать значение, а значит во время компиляции можно оптимизировать обращение к регистрам. Метод вариадический, поэтому аргументов может быть любое количество, но при этом должна присутствовать проверка, что все аргументы относятся к данной периферии.


Ранее эта задача казалась мне довольно сложной, пока я не наткнулся на это видео о value-based метапрограммировании. Данный подход к метапрограммированию позволяет писать обобщённые алгоритмы, как будто это обычный плюсовый код. В данной статье я приведу только самое необходимое из видео для решения поставленной задачи, там куда больше обобщённых алгоритмов.


Решать задачу буду абстрактно, не для конкретной периферии. Итак, есть несколько полей регистров, условно запишу их в качестве перечислений.


enum struct Enum1 { _0, _1, _2, _3 };
enum struct Enum2 { _0, _1, _2, _3 };
enum struct Enum3 { _0, _1, _2, _3, _4 };
enum struct Enum4 { _0, _1, _2, _3 };

Первые три будут относиться к одной периферии, четвертое к другой. Таким образом, если вписать значение четвёртого перечисления в метод первой периферии, должна быть ошибка компиляции, желательно понятная. Так же первые 2 перечисления будут относиться к одному регистру, третье к другому.


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


struct Enum1_traits { static constexpr std::size_t mask = 0b00111; };
struct Enum2_traits { static constexpr std::size_t mask = 0b11000; };
struct Enum3_traits { static constexpr std::size_t mask = 0b00111; };
struct Enum4_traits { static constexpr std::size_t mask = 0b00111; };

Осталось связать эти 2 типа. Тут пригодится фишка уже 20 стандарта, но она довольно тривиальна и можно реализовать её самому.


template <class T> struct type_identity { using type = T; };

// получить значение типа
constexpr auto some_type = type_identity<Some_type>{};

// достать тип из значения типа
using some_type_t = typename decltype(some_type)::type;
#define TYPE(type_identity) typename decltype(type_identity)::type

Суть в том, что можно из любого типа сделать значение и передать его в функцию в качестве аргумента. Это главный кирпичик value-based подхода в метапрограммировании, в котором надо стараться передавать информацию о типе через значения, а не в качестве параметра шаблона. Тут я определил макрос, но являюсь противником их в с++. Но он позволяет далее писать меньше. Далее приведу связывающую перечисление и его свойства функцию и ещё один макрос, позволяющий уменьшить количество копипасты.


constexpr auto traits(type_identity<Enum1>) {
    return type_identity<Enum1_traits>{};
}

#define MAKE_TRAITS_WITH_MASK(enum, mask_) struct enum##_traits {     static constexpr std::size_t mask = mask_; }; constexpr auto traits(type_identity<enum>) {     return type_identity<enum##_traits>{}; }

Необходимо поля связать с соответствующими регистрами. Я выбрал связь через наследование, поскольку в стандарте уже есть метафункция std::is_base_of, которая позволит определять связи между полями и регистрами уже в обобщенном виде. Наследоваться от перечислений нельзя, поэтому наследуемся от их свойств.


struct Register1 : Enum1_traits, Enum2_traits {
   static constexpr std::size_t offset = 0x0;
};

Адрес, где находится регистр, хранится в виде смещения относительно начала периферии.


Перед тем, как описывать периферию, необходимо рассказать о списке типов в value-based метапрограммировании. Это довольно простая структура, которая позволяет сохранить несколько типов и передать их по значению. Немного напоминает type_identity, но для нескольких типов.


template <class...Ts> struct type_pack{};

using empty_pack = type_pack<>;

Для этого списка можно реализовать множество constexpr функций. Их реализация значительно проще в понимании, чем знаменитые списки типов Александреску (библиотека Loki). Далее будут примеры.


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


template<class Address>
struct Periph1 {
   Periph1(Address) {}

   static constexpr auto registers = type_pack<Register1, Register2>{};

   template<class...Ts>
   static constexpr void set(Ts...args) {
       ::set(registers, Address::value, args...);
   }
};

Всё, что делает метод set — вызывает свободную функцию, передавая в неё всю необходимую для обобщенного алгоритма информацию.


Приведу примеры типов, предоставляющих адрес на периферию.


// статический адрес микроконтроллера
struct Address { static constexpr std::size_t value = SOME_PERIPH_BASE; };

// динамический адрес для тестов, передается через конструктор
struct Address {
   static inline std::size_t value;
   template<class Pointer>
   Address(Pointer address) { value = reinterpret_cast<std::size_t>(address); }
};

Вся информацию для обобщенного алгоритма подготовлена, осталось его реализовать. Приведу текст этой функции.


template<class...Registers, class...Args>
constexpr void set(type_pack<Registers...> registers, std::size_t address, Args...args) {
   // из аргументов достаем их свойства и упаковываем, используя value based подход
   constexpr auto args_traits = make_type_pack(traits(type_identity<Args>{})...);

   // и теперь можно проверить все ли свойства аргументов являются базовыми для данной периферии
   static_assert(all_of(args_traits, [](auto arg){
       return (std::is_base_of_v<TYPE(arg), Registers> || ...);
   }), "one of arguments in set method don`t belong to periph type");

   // определяем список регистров, в которые надо записывать данные
   constexpr auto registers_for_write = filter(registers, [](auto reg){
       return any_of(args_traits, [](auto arg){
           // как без захвата в эту лямбду пoпало значение reg?
           return std::is_base_of_v<TYPE(arg), TYPE(reg)>;
       });
   });

   // определяем значения в каждом регистре и пишем по его адресу
   foreach(registers_for_write, [=](auto reg){
       auto value = register_value(reg, args...);
       auto offset = decltype(reg)::type::offset;
       write(address + offset, value);
   });
};

Реализация функции, которая преобразует аргументы (конкретные поля регистров) в type_pack, довольно тривиальна. Напомню, что многоточие у шаблонного списка типов раскрывает список типов через запятую.


template <class...Ts>
constexpr auto make_type_pack(type_identity<Ts>...) {
   return type_pack<Ts...>{};
}

Для проверки, что все аргументы относятся к переданным регистрам, а значит конкретной периферии, необходимо реализовать алгоритм all_of. По аналогии со стандартной библиотекой алгоритм принимает на вход список типов и функцию-предикат. В качестве функции используем лямбду.


template <class F, class...Ts>
constexpr auto all_of(type_pack<Ts...>, F f) {
   return (f(type_identity<Ts>{}) and ...);
}

Тут впервые применено выражение развёртки 17 стандарта. Именно это нововведение сильно упростило жизнь тем, кто увлекается метапрограммированием. В данном примере применяется функция f для каждого из типов в списке Ts, преобразуя его к type_identity, а результат каждого вызова собирается по И.


Внутри static_assert применён этот алгоритм. В лямбду по очереди передается args_traits, обернутый в type_identity. Внутри лямбды используется стандартная метафункция std::is_base_of, но поскольку регистров может быть не один, используется выражение развёртки, чтобы выполнить ее для каждого из регистров по логике ИЛИ. В результате, если найдётся хоть один аргумент, свойства которого не являются базовым хотя бы для одного регистра, сработает static assert и выведет понятное сообщение об ошибке. По нему легко понять в каком именно месте ошибка (передан не тот аргумент в метод set) и исправить её.


Очень похожа и реализация алгоритма any_of, которая понадобится далее:


template <class F, class...Ts>
constexpr auto any_of(type_pack<Ts...>, F f) {
   return (f(type_identity<Ts>{}) or ...);
}

Следующая задача обобщенного алгоритма — определить, в какие регистры надо будет произвести запись. Для этого надо исходный список регистров отфильтровать и оставить только те, к которым есть аргументы в нашей функции. Нужен алгоритм filter, который возьмёт исходный type_pack, применит функцию предикат для каждого типа из списка, и добавит его в новый список, если предикат вернет true.


template <class F, class...Ts>
constexpr auto filter(type_pack<Ts...>, F f) {
   auto filter_one = [](auto v, auto f) {
       using T = typename decltype(v)::type;
       if constexpr (f(v))
           return type_pack<T>{};
       else
           return empty_pack{};
   };
   return (empty_pack{} + ... + filter_one(type_identity<Ts>{}, f));
}

Вначале описана лямбда, которая выполняет функцию предикат над одним типом и возвращает type_pack с ним, если предикат вернул true, или пустой type_pack, если предикат вернул false. Тут помогает еще одна новая фишка последних плюсов — constexpr if. Её суть в том, что в результирующем коде остается только одна ветка if, вторая выбрасывается. А поскольку в разных ветках возвращаются разные типы, без constexpr была бы ошибка компиляции. Результат выполнения этой лямбды для каждого типа из списка конкатенируется в один результирующий type_pack, опять благодаря выражению развертки. Не хватает перегрузки оператора сложения для type_pack. Его реализация также довольно проста:


template <class...Ts, class...Us>
constexpr auto operator+ (type_pack<Ts...>, type_pack<Us...>) {
   return type_pack<Ts..., Us...>{};
}

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


Следующий алгоритм, который понадобится это foreach. Он просто применяет функцию к каждому типу из списка, оборачивая его в type_identity. Тут в выражении развертки применяется оператор запятая, которая выполняет все действия, описанные через запятую и возвращает результат последнего действия.


template <class F, class...Ts>
constexpr void foreach(type_pack<Ts...>, F f) {
   (f(type_identity<Ts>{}), ...);
}

Функция позволяет добраться до каждого из регистров, куда надо писать. В лямбде вычисляется значение для записи в регистр, определяется адрес, куда необходимо записать, и непосредственно запись в регистр.


Для того, чтобы вычислить значение одного регистра, вычисляется значение для каждого аргумента, к которому относится этот регистр, и объединяется результат по ИЛИ.


template<class Register, class...Args>
constexpr std::size_t register_value(type_identity<Register> reg, Args...args) {
   return (arg_value(reg, args) | ...);
}

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


template<class Register, class Arg>
constexpr std::size_t arg_value(type_identity<Register>, Arg arg) {
    constexpr auto arg_traits = traits(type_identity<Arg>{});
    // значение вычисляется только, если аргумент соотвествует регистру
    if constexpr (not std::is_base_of_v<TYPE(arg_traits), Register>)
        return 0;

    constexpr auto mask = decltype(arg_traits)::type::mask;
    constexpr auto arg_shift = shift(mask);
    return static_cast<std::size_t>(arg) << arg_shift;
}

Алгоритм, определяющий смещение по маске, можно написать самому, но я воспользовался уже существующей builtin функцией.


constexpr auto shift(std::size_t mask) {
   return __builtin_ffs(mask) - 1;
}

Осталась последняя функция, которая пишет значение по конкретному адресу.


inline void write(std::size_t address, std::size_t v) {
   *reinterpret_cast<std::size_t*>(address) |= v;
}

Для проверки выполнения задачи написан небольшой тест:


// место, где будет периферия
volatile std::size_t arr[3];

int main() {
    // необходимо передать адрес динамически (для тестов)
    // поскольку адрес динамический, то эту часть можно выполнить не на микроконтроллере
    auto address = Address{arr};
    auto mock_periph = Periph1{address};
    // значение 1 в первый регистр без смещения
    // значение 3 в первый регистр со смещением на 3
    // значение 4 во второй регистр без смещения
    // итого в первом регистре 0b00011001 (25)
    //      во втором регистре 0b00000100 (4)
    mock_periph.set(Enum1::_1, Enum2::_3, Enum3::_4); // all ok
    // mock_periph.set(Enum4::_0);                       // must be compilation error
}

Всё тут написанное объединил вместе и скомпилировал в godbolt. Там любой может поэксперементировать с подходом. Видно, что поставленная цель выполнена: нет лишних обращений к памяти. Значение, которое необходимо записать в регистры, вычисляется на этапе компиляции:


main:
  mov QWORD PTR Address::value[rip], OFFSET FLAT:arr
  or QWORD PTR arr[rip], 25
  or QWORD PTR arr[rip+8], 4
  mov eax, 0
  ret



P.S.:
Спасибо всем за комментарии, благодаря им, я немного доработал подход. Новый вариант можно посмотреть тут


  • убрал типы помошники *_traits, маску можно сохранить прямо в перечислении.
    enum struct Enum1 { _0, _1, _2, _3, mask = 0b00111 };
  • связь регистров с аргументами теперь сделал не через наследование, теперь это статическое поле регистра
    static constexpr auto params = type_pack<Enum1, Enum2>{};
  • поскольку связь теперь не через наследование пришлось написать функцию contains:
    template <class T, class...Ts>
    constexpr auto contains(type_pack<Ts...>, type_identity<T> v) { 
        return ((type_identity<Ts>{} == v) or ...); 
    }
  • без лишних типов пропали все макросы
  • аргументы в метод передаю через параметры шаблона, чтобы использовать их в constexpr контексте
  • теперь в методе set четко разделена constexpr логика и логика непосредственно записи
    template<auto...args>
    static void set() {
        constexpr auto values_for_write = extract(registers, args...);
        for (auto [value, offset] : values_for_write) {
            write(Address::value + offset, value);
        }
    }
  • функция extract выделяет в constexpr массив значения для записи в регистры. Её реализация очень похожа на прошлую функцию set, за исключением того, что в ней непосредственно нет записи в регистр.
  • пришлось добавить ещё одну метафункцию, преобразующую type_pack в массив согласно лямбда функции.
    template <class F, class...Ts>
    constexpr auto to_array(type_pack<Ts...> pack, F f) {
        return std::array{f(type_identity<Ts>{})...};
    }

Комментарии (39)


  1. Punk_Joker
    09.01.2020 23:20
    +1

    Ссылка на видео не валидна. Или это у меня не открывается.


    1. slonegd Автор
      10.01.2020 09:43

      Извиняюсь, не прокликал должным образом, поправил


  1. lamerok
    10.01.2020 00:04

    Спасибо за статью, очень познавательно, тоже этой темой озадачивался.
    Пока вижу только одну проблемку — скорее всего без оптимизации кода будет много из-за этого:


    Address(Pointer address) { value = reinterpret_cast<std::size_t>(address); }

    В результате Address::value не может быть constexpr, а значит и в set() передаваться будет рантайм значение, поэтому без оптимизации точно будет куча кода. Думаю, что с оптимизацией компилятор кончено все сам высчитает.
    А так идея хорошая… возьму на вооружение. Спасибо.


    1. slonegd Автор
      10.01.2020 09:50

      На самом деле дело ещё хуже, непосредственно запись тоже внутри set с reinterpret_cast сделана, поэтому он и в случае статического адреса без оптимизаций сделает много лишнего кода. Возможно, стоит непосредственно запись вынести в отдельную функцию, а расчет адресов и значений в constexpr. Хотя, чтобы результат расчёта constexpr сделать, придётся параметры передавать в качестве аргументов шаблона, а мне это не очень нравится. К тому же в 20 стандарте появится consteval и можно будет передавать через аргументы функции.
      В выходные попробую переделать так, чтоб запись отдельно, и расчёт отдельно в constexpr, выложу результат в PS.


      1. Overlordff
        10.01.2020 11:41

        К сожалению (или к счастью) параметры consteval функции не являются constant expression. Вот такой вот парадокс. Так что передавать constexpr значения в функцию можно пока только через шаблонные параметры, либо передавать в функцию объекты заглушки, а сами значения получать через decltype(paramN)::value.


  1. VaalKIA
    10.01.2020 05:00

    Прошу прощения, но я не понял, здесь обсуждается запись в порт отображённый на память или запись в регистры специального назначения в процессоре?


    1. slonegd Автор
      10.01.2020 09:52

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


      1. VaalKIA
        11.01.2020 06:39

        Да я уже почитал литературу. Просто не сталкивался с микроконтроллерами и не знал, что регистры могут быть отображаемыми на память. Обычно регистр это то, к чему нужен быстрый доступ отдельной командой, а если не нужен, то всё можно повесить на порты, ну а порт уже отмапить.


  1. sergegers
    10.01.2020 06:35

    Я, честно говоря, бегло посмотрел, но макрос TYPE() можно заменить на

    template <auto Value>
    using type_t = typename decltype(Value)::type;
    

    а макрос MAKE_TRAITS_WITH_MASK() на
    template <typename Enum>
    constexpr std::size_t mask = [](auto a) -> std::size_t
    { // здесь ошибка компиляции, но можно поставить дефолтное значение, если есть хорошее
        static_assert(!std::is_same_v<decltype(a), decltype(a)>);
        return 0;
    }(Enum{})
    ;
    
    template <typename Enum>
    struct enum_traits
    {
        static constexpr std::size_t mask = ::mask<Enum>;    
    };
    
    template <typename Enum>
    constexpr enum_traits<Enum> enum_traits_value{};
    
    enum class xx {};
    
    template <> constexpr std::size_t mask<xx> = 0b110;
    
    

    https://godbolt.org/z/ddRCxc


    1. slonegd Автор
      10.01.2020 14:20

      TYPE() можно заменить на
      template <auto Value>
      using type_t = typename decltype(Value)::type;

      Это первое, что я попытался сделать, но компилятор меня сразу осадил. Value не может быть аргументом шаблона, так как пришла сюда в качестве аргумента функции.


      1. sergegers
        11.01.2020 00:48

        Тогда запакованный тип — уже параметр шаблона

        template <typename T>
        constexpr void foor(T value)
        {
            using type = std::type_identity_t<T>;  
        }
        
        constexpr auto foo = []<typename T>(T value) // C++ 20
        {
            using type = std::type_identity_t<T>;  
        };
        
        


  1. selrorener
    10.01.2020 12:08

    Сразу видно, что с++ подход более читаем, и, поскольку каждая функция принимает конкретный тип, нельзя ошибиться.

    Для меня это не очевидно, да и это не С++-подход. Это скорее си с классами подход.


    Но с++ подход не бесплатен. Фактически, каждая функция имеет своё обращение к регистру, в то время как на си сначала собирается маска из всех параметров на этапе компиляции, так как это всё константы, и записывается в регистр разом.

    Это проблема си с классами/жава подхода. Да даже проблема не их, а просто неправильного api.


    Желательно, чтобы это не сильно отличалось от уже привычного с++ подхода.

    А здесь всё теряется. Удобство использования. ide асист не даёт, отдельно сеттеры не валидируются и прочее. Ошибку плюёт сразу в вариадик-лапшу, а не в отдельные функции. Никакие флагсеты не запишешь. Нужно костылить дополнительно то, что ниже.


    Поэтому правильными подходами будет два подхода.


    some_stream.set = inc_memory | size_memory(DataSize::word16) | size_periph(DataSize::word16) |enable_transfer_complete_interrupt;
    
    //Семантика будет так же
    
    //либо перенести билдер туда же:
    
    some_stream.set = set_builder().inc_memory().size_memory(DataSize::word16).size_periph(DataSize::word16).enable_transfer_complete_interrupt();
    

    https://godbolt.org/z/hs9BwL — второй пример элементарно делается. Правда там должны быть типы, а не рантайм-фигня.


    Второй лучше тем, что лучше асист. Если там много всяких флагов — с первым запутаешься. Но первый лучше тем, что как и обычный | записывать наборы флагов в переменные.


    Хотя и билдер это позволяет сделать, но менее универсально. Хотя можно реализовать какой-то метод .extend(other)/+/| вставлять даже в середину/конец, а не только начало.


    Правило простое. Все изменения производит на уровне типов, тогда получаем constexpr-контекст везде. Рантайм-параметры таскаем как значения.


    Про:


    struct {} constexpr not_set;
    
    template<typename> constexpr auto enum_traits = not_set;
    
    template<> constexpr auto enum_traits<Enum1> = 0b00111;
    template<> constexpr auto enum_traits<Enum2> = 0b11000;
    template<> constexpr auto enum_traits<Enum3> = 0b00111;
    template<> constexpr auto enum_traits<Enum4> = 0b00111;

    Уже сказали.


    Наследовать так же лучше как-то так: : enum_list<Enum1, Enum3> Тогда все эти магические типы пропадут.


    1. slonegd Автор
      10.01.2020 14:43

      some_stream.set = inc_memory | size_memory(DataSize::word16) | size_periph(DataSize::word16) |enable_transfer_complete_interrupt;

      Тут или нет валидации параметров, или нужно будет переопределять оператор | для каждого параметра и сразу навскидку я не соображу как это провернуть.


      some_stream.set = set_builder().inc_memory().size_memory(DataSize::word16).size_periph(DataSize::word16).enable_transfer_complete_interrupt();

      Забавно, что мой подход назвали из джавистким, и сами же приводите пример ооп-паттерна, которые в джаве на уровне языка есть. Опять же билдер придётся писать для каждой периферии для валидации. Хотя может и можно обобщённый алгоритм тоже написать. Можно подумать.
      Плюс моего подхода в том, что алгоритм написан один раз, протестирован и далее просто нужно выполнить концепт работы с перечислениями и периферией.


      Наследовать так же лучше как-то так:: enum_list<Enum1, Enum3>

      Может и лучше. Моё мнение, что лучше вообще агрегировать как это сделано с регистрами в периферии, чтобы было единообразно, а вместо std::enable_if гаписать свою простенькую функцию. Просто показал, что можно так.


      Суть поста была в том, чтобы показать, что со списками типов стало работать куда проще и можно решать задачи, которые раньше были не по силам среднестатистическому разработчику. Детали реализации не так важны.


      1. selrorener
        10.01.2020 18:10

        Тут или нет валидации параметров

        Как нет, если есть.


        или нужно будет переопределять оператор | для каждого параметра и сразу навскидку я не соображу как это провернуть.

        Очевидно, что всё нужно переопределять.


        Забавно, что мой подход назвали из джавистким

        Я объяснил почему и что.


        и сами же приводите пример ооп-паттерна, которые в джаве на уровне языка есть.

        Я привожу пример доступный. К тому же здесь нету никакого нового паттерна — это паттерн уже был изначально. Я просто разделил конфигурацию его.


        А из жавы там не паттерн, а подход.


        Опять же билдер придётся писать для каждой периферии для валидации.

        Опять же нет. Он такой же, как и базовый. Если набор методов зависит от some_stream — это так же не проблема. Его/его тип можно использовать для создания билдера.


        Хотя может и можно обобщённый алгоритм тоже написать. Можно подумать.

        Можно.


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

        Там так же всё написано один раз. В к тому же потерять всё и игнорировать этот факт — это плохой подход.


        Может и лучше.

        Именно лучше. У первого варианта нету никаких преимуществ — одни недостатки.


        Суть поста была в том, чтобы показать, что со списками типов стало работать куда проще и можно решать задачи, которые раньше были не по силам среднестатистическому разработчику. Детали реализации не так важны.

        Ну дак в этом проблема. Ведь смысл то не в списке типов, а в разделение состояния на constexpr в шаблонах(в типе) и на рантайм(в данных).


        А задача безопасности — быть незаметной. Страдать за веру — это не подход С++.


        Ах да, код не работает в шланге из-за "как попало без захвата". [=]


        1. slonegd Автор
          10.01.2020 18:16

          Ну попробуйте реализовать без списка типов обобщённо. Я с удовольствием почитаю, а то только можно да можно, без конкретики. Ту ссылку, что вы привели не имеет никакой валидации, а чтобы её прикрутить, на скорый взгляд, придётся делать всё тоже самое. Как минимум надо будет хранить в constexpr контексте информацию о регистрах и как они связаны с перечислениями, при этом количество этих регистров и перечислений в обобщённом алгоритме неизвестно. А там посмотрим, какой подход лаконичнее и понятнее.


          1. selrorener
            10.01.2020 19:35

            Ну попробуйте реализовать без списка типов обобщённо.

            Причём тут список типов?


            Я с удовольствием почитаю, а то только можно да можно, без конкретики.

            Я показал как, тот же ranges показал как — там 1в1 первый вариант.


            Ту ссылку, что вы привели не имеет никакой валидации

            Очевидно, что она есть.


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

            Причём тут тоже самое? Речь ни о реализации, а об интерфейсе для пользователя. Это какие-то непонятные мне манёвры.


            Как минимум надо будет хранить в constexpr контексте информацию о регистрах и как они связаны с перечислениями

            Вы не понимаете как это работает. У вас там никакого constexpr-контекста нет. То, что вы натыкали туда constexpr — это ничего не значит. Смысл подхода, который вы повторили именно в том, что-бы иметь контекст вне контекста.


            https://godbolt.org/z/dTmqgt — это было предсказуемо. Реально там ничего не constexpr, для чего не определяется constexpr-контекст явно. А он там определяется только в if constexpr и всё, может где-то ещё.


            Потому как аргумент лямбды не constexpr, даже если op() выполняется в constexpr-контексте.


            при этом количество этих регистров и перечислений в обобщённом алгоритме неизвестно.

            С чего вдруг оно неизвестно? Оно не может быть неизвестно. Это, опять же, непонятные мне манёвры и попытка придумать новую задачу, что-бы оправдать неудачный подход.


            К тому же, если он неизвестный, что что делает какая-то лапша в Register1, в static constexpr void set(Ts...args) и остальных местах? Т.е. это уже попытка реализовать ту самую "известно", причём руками. Причём лапшой.


            Здесь просто реализована функция set, которая непонятно что, как и куда записывает. Это не реализация интерфейса показанного выше. Это просто форчи + фильтр.


            А там посмотрим, какой подход лаконичнее и понятнее.

            Дак у вас нет никакого подхода. Вы не реализовали то, что показывали выше. Вы реализовали именно сишный вариант.


            Он тайпчекается, в enum class — не записать не те значения. Этой фишка есть по умолчанию.


            У вас осталось только одно — это диспатч по типу значения в регистр. Это просто побочный эффект и я не вижу его выше. Да и не факт, что он вообще адекватен задаче.


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


            В общем, вы как-то более подробно опишите то, что вам нужно. Я вижу только аналог си-интерфейса. Я вижу попытку сделать типобезопасный enum, но он уже есть. Я вижу какую-то непопонятную логику с записью в два регистра сразу. Которой управлять нельзя.


            Опишите — я напишу реализацию. Пока что я ничего не понимаю.


            1. slonegd Автор
              10.01.2020 19:55

              А что конкретно непонятно в поставленной задаче? Записать данные в регистры периферии из некого списка параметров (хоть как у меня, хоть через оператор или билдер), при этом чтобы обращения к регистрам были минимизированы. Если параметры относятся к одному регистру, то они собираются по или и пишутся за одно обращение к регистру. При этом сделать так, чтобы было невозможно ненароком записать параметр, который не относится к данной периферии. Но всё это написано в шапке статьи, я просто повторил.


              1. selrorener
                10.01.2020 21:28

                А что конкретно непонятно в поставленной задаче?

                some_stream.set (Direction::to_periph)    SOME_STREAM->CR |= DMA_SxCR_DIR_0
                   .inc_memory()                                          |  DMA_SxCR_MINC_Msk
                   .size_memory (DataSize::word16)                        |  DMA_SxCR_MSIZE_0
                   .size_periph (DataSize::word16)                        |  DMA_SxCR_PSIZE_0
                   .enable_transfer_complete_interrupt();                 |  DMA_SxCR_TCIE_Msk;

                Вот ваша задача.


                Сразу видно, что с++ подход более читаем, и, поскольку каждая функция принимает конкретный тип, нельзя ошибиться. Си подход не проверяет валидность данных, это ложится на плечи программиста.

                Во-первых здесь ошибка. Если CR и все флаги — enum class cr, то никакой проблемы нет. Проблема здесь только для обычного енума. Таком образом на С++ левый вариант так же проверяется.


                enum class cr: size_t {
                  DMA_SxCR_DIR_0,
                  DMA_SxCR_MINC_Msk,
                  DMA_SxCR_MSIZE_0,
                  DMA_SxCR_PSIZE_0,
                  DMA_SxCR_TCIE_Msk
                };
                
                constexpr cr operator|(cr a, cr b) {
                  return cr{(size_t)a | (size_t)b};
                }
                
                cr cr = cr::DMA_SxCR_DIR_0 | cr::DMA_SxCR_MINC_Msk | cr::DMA_SxCR_PSIZE_0;

                Всё, и никаких проблем. Причём ide при написании | сама подсказывает нужные флаги.


                Далее вы так и не ответили на вопрос — зачем записывать сразу в несколько регистров. Я вижу тут только дыру в реализации. Я не особо разбираюсь в мк"ашках и видел их последний раз много лет назад. Да и потолком моего знакомства было ваяние генератора сигналов.


                Теперь вернёмся к примеру на С++. Вы утверждали, что там всё проверяется. Но когда я вам показал точно такой же интерфейс, только с РЕАЛЬНЫМ компилтайм-вычислением флагом — вы мне сказали "там ничего не проверяется". Вы это так же не прокомментировали.


                Непонятно, что вам там непонятно. Флаги заменяются на вызов методов. Сам флаг передаётся через тип. Ничего невозможно никуда передать, точно так же как это сделано в примере выше — у вас попросту нет будет нужных методов.


                Если не стоит задача сделать реальный constexpr — достаточно просто смены интерфейса. Нам нужно вначале составлять значение, а потом уже писать. Делается точно так же как в с маской — я показал.


                Если хочется и аргументы передавать как constexpr, то это так же без проблем делается. Вариантов много. Передавать через nttp.


                Так:


                template<enum cr arg> auto f() {}

                Можно использовать уже value-подход.


                template<auto value> struct enum_t: std::integral_constant<decltype(value), value> {};
                template<auto value> auto enum_v = enum_t<value>{};
                
                template<enum cr value> auto f(enum_t<value>) {}
                
                f(enum_v<cr::DMA_SxCR_DIR_0>);
                

                Можно так, если нужно принимать не только cr.


                auto f(auto flag) {
                  static_assert(std::is_same_v<decltype(flag()), enum cr>);
                }
                
                template<auto flag> auto f(enum_t<flag>) {
                  static_assert(std::is_same_v<decltype(flag), enum cr>);
                }
                

                Первое здесь правильный value-подход. Вытаскивание типа через nttp — это такой больше костыль.


                Можно вообще отказаться от enum. И сразу определять флаги в виде привычном для value-подхода.


                Повторяю ещё раз. Я не увидел какой-то валидации сверх enum class и его достаточно. Так же здесь нет constexpr, о чём я уже сообщал выше. Проблему показанного С++-интерфейса можно решить просто отделением билдера.


                Единственный функционал какой здесь — это диспатч по регистрам в зависимости от типа enum"а. Но я считаю это не фичёй, а багом. Вы не прокомментировали это.


                1. slonegd Автор
                  11.01.2020 05:03

                  Очень много пишите сразу, потому легко пропустить на что ответить.


                  DMA_SxCR_DIR_0 не перечисление, это просто дефайн в CMSIS. Допустим, мы из них делаем перечисление и тогда валидацию на уровне enum можно делать, как вы предлагаете. Но это единый enum на регистр, что мешает сделать названия понятнее, по типу direction::to_periph или memory_size::byte16. Мелочь, а неприятно.


                  Зачем писать в несколько регистров? Для унификации интерфейса. Я, как пользователь периферии, не очень то хочу помнить в какой конкретно регистр устанавливается определенное значение. Я знаю, что у периферии есть свойство, для dma то же самое направление и размер данных, я его и задаю, а в какой регистр это делается в CR или CRL или даже CR[1] не важно. У разных версий микроконтроллера по разному. А поскольку мне важно задать свойство, я могу задать параметры, которые записываются в разные регистры, и это отработает. Хороший же интерфейс как раз, позволяет меньше задумываться о низкоуровневой реализации.


                  1. selrorener
                    11.01.2020 05:39

                    DMA_SxCR_DIR_0 не перечисление, это просто дефайн в CMSIS. Допустим, мы из них делаем перечисление и тогда валидацию на уровне enum можно делать, как вы предлагаете. Но это единый enum на регистр, что мешает сделать названия понятнее, по типу direction::to_periph или memory_size::byte16. Мелочь, а неприятно.

                    Ну дак и в вашем решении всё тоже самое. У вам такие же енумы.


                    Зачем писать в несколько регистров? Для унификации интерфейса. Я, как пользователь периферии, не очень то хочу помнить в какой конкретно регистр устанавливается определенное значение.

                    Не в этом дело. Дело в пересечении. У вас один енум на двух регистрах. Зачем одно и тоже значение писать сразу в два регистра? Это какая-то общая конфигурация? Как оно должно работать.


                    Тоже самое непонятно каким образом смешивать разные енумы? Зачем в разных енумах то, что потом смешивается? За счёт чего достигается безопасность?


                    Насколько я могу могу понять — разделение на разные енумы — это типа костыль для "понятных названий"? Опять же, зачем здесь енумы?


                    Хороший же интерфейс как раз, позволяет меньше задумываться о низкоуровневой реализации.

                    Это всё крайне сомнительно. Вы как-то изначально всё неправильно делаете. Зачем использовать данные в качестве флага?


                    Интерфейс — это интерфейс. В нём не должны торчать какие-то данные, кишки, енумы и прочее. Особенно если вы хотите constexpr. И какой-то реальной безопасности.


                    В общем я так и не понял что вам нужно. Дайте ссылку ну C++-api — хоть посмотрю.


                    1. slonegd Автор
                      11.01.2020 07:39

                      У вас один енум на двух регистрах.

                      Это где? Вся суть в том, что каждый параметр — отдельный тип, не обязательно enum, кстати, просто в примере только enum. По типу в constexpr контексте и определяется куда этот параметр записать. А что записать, уже не constexpr, хотя, как правило, тоже известно на этапе компиляции, но не всегда.


                      Под безопасностью я имел ввиду, что значения запишутся туда куда нужно просто по факту своего существования. В си очень легко перепутать параметр и записать не то.


                      Вот пример регистров периферии. Там есть как флаги, так и перечисления, так и просто данные. А апи мы все тут сами и изобретаем, кому что в голову придёт. Нет некого стандарта. Я предложил лишь один из вариантов.


  1. eao197
    10.01.2020 16:47

    А почему нельзя было mask поместить непосредственно в enum? Типа такого:


    enum class Enum1 { _1, _2, _3, mask = 0b00111 };


    1. slonegd Автор
      10.01.2020 18:03

      Это связано со спецификой некоторых регистров. Условно может быть так:


      struct Register {
          enum1 _0 : 2;
          enum1 _1 : 2;
          enum1 _2 : 2;
          ...
      };

      То есть одно и тоже перечисление с разными масками. Если маску хранить в traits, то можно от базового перечисления наследоваться, и новому типу делать любую другую маску.


      1. eao197
        10.01.2020 19:05

        То есть одно и тоже перечисление с разными масками.

        И как вы тогда вот это вот разруливаете:


        constexpr auto traits(type_identity<Enum1>) {
            return type_identity<Enum1_traits>{};
        }

        Ведь если у вас есть Enum1, то вы не сможете написать для него перегрузки traits так, чтобы они возвращали type_identity для других EnumX_traits.


        1. slonegd Автор
          11.01.2020 12:09
          +1

          Да, тут я немного перемудрил, действительно эти трейты и не нужны вовсе. Думал про фичу из другого языка. В PS немного переписал, используя вашу идею хранения маски в самом перечислении.


  1. mctMaks
    10.01.2020 17:02

    судя по последним нескольким статьям предлагаю лозунг «даешь С++ на микроконтроллеры»
    активно читаю данные статьи, не все понятно, но я пытаюсь. И вроде все хорошо, но хотелось бы чуть больше менее абстрактных примеров. Ну запись в регистры, ну много разных методов. Разнообразие это хорошо, но меня пока и мой подход устаивает.
    А будет расмотрено что более сложное, чем запись в регистры? Например описание реализации класса по работе, скажем с I2C акселерометром. На чистом Си я знаю как это будет выглядеть, на С++ примерно. Там и наследование, и желание избежать лишенго кода, ибо на шине может быть и кто-то другой, да и разделенный доступ к ресурсу будет. Вот какие преимущества от использования С++ будут там?
    Сам опыта в С++ имею крайне мало, да и то, из-за наличия желания повозится с GUI на QT


  1. realimba
    10.01.2020 17:14

    Было бы интересно узнать насколько изменилось время сборки и размер бинаря особенно с debug info. Все эти мета штучки конечно занятные, но каждый раз с бубном плясать когда компилятор не осилил или раздул код в 100 раз не особо весело. А уж креши дебажить… ну короче удачи. На примере g2o + eigen от адептов александреску билд с -g раздувался почти до гигабайта!


    1. slonegd Автор
      10.01.2020 17:30

      Александреску крут, но его подход я и сам не поддерживаю по причине того, что найти разработчика, который поймёт что написано, нереально. Все эти новые фишечки добавляются комитетом как раз для того, чтобы убрать боли, связанные с метапрограммированием. Подход с constexpr функциями ускоряет компиляцию в разы по сравнению со списками Александреску. Посмотрите доклад по ссылке в начале, там чуть подробнее.
      В микроконтроллер debug info никогда не пишется. Отладчик берёт её из других файлов. Уж не знаю как можно скомпилировать прогу под микро, чтобы она была гиг. Попробуйте в годболте измините оптимизацию на без, увидите, что он там накомпилировал, только вот зачем компилировать без оптимизации?
      Ну и, если вам понадобилась отладка, значит вы делаете что-то не так. Не пишите тесты, например.


  1. realimba
    10.01.2020 17:54

    Ну и, если вам понадобилась отладка, значит вы делаете что-то не так

    на хелло ворлд согласен, а на большом проекте? вот так просто поставить крест на отладчике и дебажить по printf? или по времянкам? :D


    1. slonegd Автор
      10.01.2020 18:10

      Да нет же, printf ещё хуже. Всё должно быть покрыто тестами, тем более для большого проекта. Вы при отладке никогда не проверите все возможные варианты событий.
      Сам залезаю в отладчик иногда, но только когда совсем не могу понять в какой части проблема, когда выясняю, изолирую проблему и пишу тесты, которые сначала падают, исправляю ошибку, чтобы тесты не падали. Отладчик просто необходим, если вы пишите на CMSIS, в упор не видите, где ошиблись с маской из дефайна, так как они все на одно лицо, и только в отладке смотрите на перефирию, чтобы увидеть, что нужный бит где-то не выставлен. Строгая типизация с++ позволяет не заботится о том, что вы ошиблись с передачей не того аргумента в функцию, а значит и отладчик для проверки регистров периферии не нужен.


      1. Falstaff
        10.01.2020 18:39

        Под тестами вы понимаете юнит-тесты или некие интеграционные тесты которые вы пишете для всей прошивки в целом и которые выполняются на вашем целевом железе? Последние часто довольно-таки трудно сделать, а первые вам не скажут что, к примеру (от балды пример) где-то в другом модуле есть какой-нибудь off by one и ваша переменная внезапно была перезаписана DMA контроллером, которому сказали записать за пределами буфера.


        Отладка и тесты — они всё-таки ортогональные штуки, друг друга не совсем исключают.


        1. slonegd Автор
          11.01.2020 05:25

          Не исключают, но приведенный от балды пример как раз и показывает, что связанность вышла через чур, из-за чего юнит тестирование уже не поможет. Разбивайте задачи на слабосвязанные, тестируете через юнит, и дебаг вам не понадобится. Полагаться на интеграционные тесты — такая себе практика.
          Наткнулись вы дебагером на вот такую функцию set, как в статье. Зачем вам заходить внутрь неё, если она со всех сторон протестирована и делает ровно то, что написано? step over и дальше смотрим.


          1. Falstaff
            11.01.2020 10:02

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


  1. Falstaff
    10.01.2020 22:15
    +1

    Вообще, не преуменьшая значения статьи… Именно для этой конкретной задачи — установка полей в регистрах — что не так с подходом через битовые поля типа enum class? Я, может, чего-то не усматриваю, но вот так городить приходится меньше:


    enum class MemorySize : uint8_t {
      kWord16 = 0,
      kWord32 = 1,
      ...
    };
    
    enum class Direction : uint8_t {
      kToPeripheral = 0,
      ...
    };
    
    struct StreamControlReg {
      Direction direction : 2;  // Не даст определить поле, в которое не влезет Direction.
      MemorySize msize : 2;
    } __attribute__((packed));
    
    ...
    
    Stream->CR = StreamControlReg{
      .direction = Direction::kToPeripheral,  // Даст присвоить только Direction.
      .msize = MemorySize::kWord16
    };

    Так проверяется и тип регистра, и типы полей. (И, да, gcc выдаст warning, мол, поле слишком узкое для enum, но это уже пофиксили и патч ушёл в trunk месяц или два назад. Clang компилирует без предупреждения.)


    1. slonegd Автор
      11.01.2020 05:20

      О, вы знаете __attribute__((packed)), круто. Как раз так и сделано в моей последней библиотеке, потому что поставленная в статье задача была слишком сложной для меня.


      Есть ещё одна проблема проблема с битовыми полями, на которую можно наткнуться. Некоторые регистры не позволяют побайтовое обращение, а компилятор с битовым полем может сделать так:


         0x0800023a <systemTimerInit()+10>:   19 78   ldrb    r1, [r3, #0]
         0x0800023c <systemTimerInit()+12>:   04 32   adds    r2, #4
         0x0800023e <systemTimerInit()+14>:   0a 43   orrs    r2, r1
      => 0x08000240 <systemTimerInit()+16>:   1a 70   strb    r2, [r3, #0]

      Вот в месте где стрелочка, stm32 уходит в hardfault. Нашел флаг компиляции, который запрещает байтовый доступ: -fno-strict-volatile-bitfields, и он даже помог. Только вот нашлось место, где он проигнорировал и сгенерировал байтовый доступ. В общем работа с битовыми полями — дело глючное.


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


  1. Falstaff
    11.01.2020 09:50

    Только вот нашлось место, где он проигнорировал и сгенерировал байтовый доступ. В общем работа с битовыми полями — дело глючное.

    Не видя кода, сложно сказать, но я бы начал со взгляда на структуру и есть ли там обычные поля. Там есть один глюк, который не совсем глюк, но нюанс (с) и, возможно, это он нагадил. Начиная с C++11, если я правильно помню суть вопроса, компилятору запрещено при изменении битового поля трогать поля структуры, которые не являются битовыми полями, поэтому если структура смешивает битовые поля и обычные члены, то компилятору приходится выбирать между тем, чтобы соблюсти стандарт и сгенерировать байтовый доступ (чтобы не трогать те биты, которые не входят в битовые поля), или соблюсти ARM ABI и нарушить стандарт C++. GCC вроде бы решает в пользу стандарта. Так что можно попробовать сделать все остальные члены структуры битовыми полями, может помочь.


    … подход из статьи позволяет не думать о регистрах при использовании, абстрагироваться от конкретики. На самом деле, какая разница в какой конкретно регистр записывается определенное свойство периферии, главное что оно пишется.

    В целом дело благое, и идея value-based metaprogramming здравая — это я к тому, что за статью спасибо. Но преследует чувство, что именно в этом конкретном приложении абстракция не там проведена: всё равно где-то будут наружу вылезать потроха, потому что на реализацию они плохо ложатся — где-то хочется установить выход компаратора, но у двух компараторов это не отдельные регистры с теми же полями (и по тому же смещению от базового), а два поля в одном регистре с разными смещениями; где-то два контроллера DMA имеют свои наборы периферии, где-то для одной версии контроллера бит есть, а для другой нет (и к тому же только для одной копии периферии — как SW1 только для одного компаратора и только для Cat.4).


    В общем, такое чувство, что API получился странный именно в плане идеи "спроецируем эту кучу enum'ов на всю периферию, код будет знать как их по регистрам раскидать", больше своих нюансов добавит чем под собой спрячет.


    1. slonegd Автор
      11.01.2020 11:13

      Вот эта часть, которая глючила:
      https://github.com/slonegd/mculib3/blob/b5a9142dfac4f2b24b4746dfe198b33d3df35ac9/src/periph/dma_f1.h#L40-L51
      Вот описание структуры:
      https://github.com/slonegd/mculib3/blob/develop/src/bits/bits_dma_f1.h
      Обычное битовое поле, а не работает.


      Я не развивал особо мысль как это всё на разные периферии делать. Первое, что в мысль приходит, описывать их в отдельных неймспесах, а при работе просто выбрать нужный.


      using namespace stm32f4;

      А те поля, которые отсутствуют в конкретном микроконтроллере — просто делать функции пустышки. В целом опыт подсказывает, что всё можно обобщить и даже проверять, поддерживает конкретный экземпляр периферии определённый функционал или нет на этапе компиляции. Делал такое для uarta, если в конструктор передали пины, которые не поддерживают uart, срабатывал static_assert, да ещё и с описанием: какие пины подходят.
      Вот пример:
      https://github.com/slonegd/mculib3/blob/b5a9142dfac4f2b24b4746dfe198b33d3df35ac9/src/periph/usart_f1_f4.h#L225


      1. Falstaff
        11.01.2020 12:52

        Ага. Я посмотрел на структуру и попробовал с оригинальным вариантом установки битовых полей, тот который сейчас закомментирован.


        Гуру меня поправят, если что, но насколько я понимаю… Во-первых, -fno-strict-volatile-bitfields не запрещает байтовый допуск, строго говоря. Она разрешает компилятору использовать самую эффективную инструкцию для типа данных битового поля, на своё усмотрение. Это может быть и 32-разрядный доступ, так что в части случаев это будет работать. Суть в том что packed отменяет выравнивание, поэтому, если компилятор не может быть уверен, что структура располагается в памяти по адресу кратному 4, то он не будет читать её целиком.


        ...
           bool CHTIF7 :1;
           bool CTEIF7 :1;
           uint32_t    :4; // Bits 31:28 Reserved, must be kept at reset value.
        }__attribute__((packed));
        
        ...
        
        int main(int argc, char** argv) {
          ifcr->CGIF3 = 1;
        ...

        // arm-none-eabi-g++ -O3 -g -fno-strict-volatile-bitfields
        
          ifcr->CGIF3 = 1;                                                                                                                                                                                                                                                                                                                                                                              14:   e5c23001        strb    r3, [r2, #1]  

        Если мы уверены, что структура выравнена в памяти (а регистры таки должны быть), то можно компилятору об этом сказать:


        ...
           bool CHTIF7 :1;
           bool CTEIF7 :1;
           uint32_t    :4; // Bits 31:28 Reserved, must be kept at reset value.
        }__attribute__((packed, aligned(4)));
        
        ...
        
        int main(int argc, char** argv) {
          ifcr->CGIF3 = 1;
        ...

        // arm-none-eabi-g++ -O3 -g -fno-strict-volatile-bitfields
        
          ifcr->CGIF3 = 1;                                                                                                                                                                                                                                                                                                                                                                              14:   e5823000        str     r3, [r2] 

        Однако -fno-strict-volatile-bitfields всё ещё даёт компилятору слишком много свободы выбора. Можно попробовать наоборот, с -fstrict-volatile-bitfields которая предписывает should use a single access of the width of the field’s type, aligned to a natural alignment if possible. Предыдущий пример, даже с aligned(4), даёт байтовый доступ, но если вдобавок сменить типы битовых полей на 32-разрядные чтобы удовлетворить формулировке...


           uint32_t CHTIF7 :1;
           uint32_t CTEIF7 :1;
           uint32_t    :4; // Bits 31:28 Reserved, must be kept at reset value.
        }__attribute__((packed, aligned(4)));

        // arm-none-eabi-g++ -O3 -g -fstrict-volatile-bitfields
        
          ifcr->CGIF3 = 1;                                                                                                                                                                                                                                                                                                                                                                              14:   e5823000        str     r3, [r2]

        То вроде бы опять всё хорошо, и без выдачи компилятору карт-бланша использованием -fno-strict-volatile-bitfields. В общем, да, это тёмные уголки стандарта и его взаимодействия с железом, но в принципе и там можно нащупать правила игры.


        1. Falstaff
          11.01.2020 13:23

          Вдогонку, теперь можно проверить и то, как работает конфликт между стандартом и ABI, взяв последний результат за основу.


          Вот если заменить первые восемь бит на обычное, не битовое поле:
          struct IFCR {
             uint8_t dummy;
             /*uint32_t CGIF1  :1; // Bits 24, 20, 16, 12, 8, 4, 0 CGIFx: Channel x global interrupt clear (x = 1 ..7)
             uint32_t CTCIF1 :1; // Bits 25, 21, 17, 13, 9, 5, 1 CTCIFx: Channel x transfer complete clear (x = 1 ..7)
             uint32_t CHTIF1 :1; // Bits 26, 22, 18, 14, 10, 6, 2 CHTIFx: Channel x half transfer clear (x = 1 ..7)
             uint32_t CTEIF1 :1; // Bits 27, 23, 19, 15, 11, 7, 3 CTEIFx: Channel x transfer error clear (x = 1 ..7)
             uint32_t CGIF2  :1;
             uint32_t CTCIF2 :1;
             uint32_t CHTIF2 :1;
             uint32_t CTEIF2 :1;*/
             uint32_t CGIF3  :1;
             uint32_t CTCIF3 :1;
             uint32_t CHTIF3 :1;
             uint32_t CTEIF3 :1;
             uint32_t CGIF4  :1;
             uint32_t CTCIF4 :1;
             uint32_t CHTIF4 :1;
             uint32_t CTEIF4 :1;
             uint32_t CGIF5  :1;
             uint32_t CTCIF5 :1;
             uint32_t CHTIF5 :1;
             uint32_t CTEIF5 :1;
             uint32_t CGIF6  :1;
             uint32_t CTCIF6 :1;
             uint32_t CHTIF6 :1;
             uint32_t CTEIF6 :1;
             uint32_t CGIF7  :1;
             uint32_t CTCIF7 :1;
             uint32_t CHTIF7 :1;
             uint32_t CTEIF7 :1;
             uint32_t    :4; // Bits 31:28 Reserved, must be kept at reset value.
          }__attribute__((packed, aligned(4)));