Pull to refresh
9
0
Денис Кирюшин @slonegd

Программист

Send message

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

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

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

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

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


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


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

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

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


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

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


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


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

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

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

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

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


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

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

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

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


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

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

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

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

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

Вместо возни с флагами оптимизации, проще просто переменную t объявить volatile, тогда будет работать при любых флагах, так как это прямое указание компилятору, что переменную нельзя оптимизировать.
А где брать svd файл? У меня всё работает, но в зоне cortex peripherial написано no svd file loaded. Списка поддерживаемых контроллеров тоже не нашел.
arm-none-eabi-gcc поддерживает последний 17 стандарт в полной мере.

Information

Rating
Does not participate
Location
Вологда, Вологодская обл., Россия
Date of birth
Registered
Activity