Pull to refresh

Comments 40

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

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


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

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

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

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

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

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

Это регистры не процессора а периферии. К тому же в ARM нет команд IN OUT как в x86 или AVR. Поэтому работаем с портами как с памятью.

Я, честно говоря, бегло посмотрел, но макрос 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
TYPE() можно заменить на
template <auto Value>
using type_t = typename decltype(Value)::type;

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

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

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>;  
};

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

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


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

Это проблема си с классами/жава подхода. Да даже проблема не их, а просто неправильного 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> Тогда все эти магические типы пропадут.

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 гаписать свою простенькую функцию. Просто показал, что можно так.


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

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

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


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

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


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

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


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

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


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


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

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


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

Можно.


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

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


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

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


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

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


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


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

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

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

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


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

Я показал как, тот же 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, но он уже есть. Я вижу какую-то непопонятную логику с записью в два регистра сразу. Которой управлять нельзя.


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

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

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

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"а. Но я считаю это не фичёй, а багом. Вы не прокомментировали это.

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


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


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

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

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


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

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


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


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


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

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


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


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

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

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


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


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

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


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

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


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

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

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

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


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

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

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

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

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

UFO just landed and posted this here

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

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


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

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

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

Вообще, не преуменьшая значения статьи… Именно для этой конкретной задачи — установка полей в регистрах — что не так с подходом через битовые поля типа 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 компилирует без предупреждения.)

О, вы знаете __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, и он даже помог. Только вот нашлось место, где он проигнорировал и сгенерировал байтовый доступ. В общем работа с битовыми полями — дело глючное.


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

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

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


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

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


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

Вот эта часть, которая глючила:
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

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


Гуру меня поправят, если что, но насколько я понимаю… Во-первых, -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. В общем, да, это тёмные уголки стандарта и его взаимодействия с железом, но в принципе и там можно нащупать правила игры.

Вдогонку, теперь можно проверить и то, как работает конфликт между стандартом и 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)));

  ifcr->CGIF3 = 1;                                                                                                                                                                                                                                                                                                                                                                              14:   e5c23001        strb    r3, [r2, #1] 

Теперь GCC не может тронуть первый байт (согласно стандарту) и возвращается к байтовому доступу.


А вот если заменить последний байт:
struct IFCR {
   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.
   */
   uint8_t dummy;
}__attribute__((packed, aligned(4)));

  ifcr->CGIF3 = 1;                                                                                                                                                                                                                                                                                                                                                                              14:   e1c230b0        strh    r3, [r2]  

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


И, наконец, с заменой первого байта, но на 8-битное поле:
struct IFCR {
   uint8_t dummy : 8;
   /*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)));

  ifcr->CGIF3 = 1;                                                                                                                                                                                                                                                                                                                                                                              14:   e5823000        str     r3, [r2]

Всё снова хорошо. dummy — всё ещё обычный байт, но теперь это поле, и согласно модели памяти C++11 его снова можно трогать при доступе к другим битовым полям.


В общем жить можно с битовыми полями. Нюансы надо держать в уме, это да.

Sign up to leave a comment.

Articles