Я примерно про то и говорю. Я просто думаю в контексте Rust, где подобная библиотека генерируется автоматически по SVD файлам. И когда я ещё пытался делать свою прошивку на C/C++, мне именно такая библиотека и была нужна, а не вот этот HAL от STM, который пытается быть чуть более высокоуровневым, но у него это получается не особо.
Последний пункт, правда, не понял, про сложные операции. Код на Rust должен компилироваться в прямой доступ к регистрам.
Тот кусочек кода не раскрывает, чем он принципиально лучше.
То есть я согласен, строгая типизация — это важно, я про это писал выше. По этому параметру он, конечно же, лучше.
Но меня это в меньшей степени беспокоит, и вот почему.
Например, я перенёс свой датчик Холла с TIM1 на TIM2. А порт взял такой, который на второй канал попадает. И может быть, это альтернативный порт для этого канала, и нужно переназначение каналов таймера настраивать. И поставил триггер Шмитта, то есть вход стал инвертированный.
В обоих вариантах код будет сильно зависеть от конкретного железа. Соответственно, его всё равно придётся изолировать от остального кода. В обоих вариантах мне придётся существенно переписывать этот код (особенно, настройку). И вникать в техническую спецификацию, чтобы понять, какие регистры и как мне надо настроить (например, переназначение каналов таймера — это же совсем ни разу не очевидным образом настраивается).
И если мне и так приходится а) делать свой слой абстракции б) вникать, фактически, до уровня регистров, то в чём существенный плюс (кроме типизации, за которую я «за» обеими руками, о чём я уже писал)?
Делаешь, например, настройку имя порта + пины для вывдов DATA, EN, R/S LCD экрана и всё хорошо. А потом оказывается, что EN и R/S на другом порту, а ты это не предусмотрел. Придётся вводить ещё один параметр.
А потом что? Правильно, DATA пины (4 штуки) оказываются разбиты между двумя портами. И опять вся вот эта идея «меняем в одном месте» летим к чертям.
А потом ещё и не забыть нужные подсистемы включить (и, насколько я помню, у разных чипов раскладка периферии по подсистемам разная и HAL это не абстрагирует).
Я эти грабли все на своём проекте собирал, пока метался между F0 и F103, разными платами, языками (C -> C++ -> Rust), библиотеками (StdPeriph -> HAL -> Rust), подходами (C++ с шаблонами <-> C++ с классами).
Самая жизнеспособная в данном случае абстракция — это «интерфейс» с тремя методами «выставить EN», «выставить R/S», «выставить DATA». А уже что оно там внутри будет делать — пофиг, хоть HAL, хоть не HAL, там кода будет — кот наплакал.
P.S. А правильные библиотеки — согласен, как я уже говорил, я переехал на Rust и весьма счастлив. :)
Проблема HAL, на мой взгляд, что он предлагает уровень абстракции, который идёт под углом к тому, что мне нужно.
HAL строит абстракции вокруг конкретной переферии. «Порты»/«Таймеры»/и.т.д.
Это может нормально работает для случаев, когда подсистема сильно отделена от всего остального. Не знаю, USB какой-нибудь, что-ли (хотя всегда есть DMA который на себя всё подряд замыкает). Я с такими крупными подсистемами не работал — не знаю. С FLASH работал, там HAL более-менее был удобен.
А вот когда у тебя подсистема — это хитрым образом сконфигурированный таймер и разные пины на разных портах, HAL тебе никак не поможет. Всё равно придётся писать свой слой абстракции, который будет предлагать API специфичный для подсистемы (например «выставить задержку на следующий цикл PWM»/«установить следующий цикл PWM последним»). И если ты такой слой пишешь, то по моему опыту, настроить регистрами проще и быстрее, чем пытаться найти комбинацию вызовов HAL, которая приведёт к нужному результату.
И которая, в 90% случаев всё равно не будет работать на другом устройстве, или из-за разницы в чипах, или банально из-за существенной разницы в раскладке на переферию/пины (тут выход PWM был основной пин, там — альтернативный, и.т.д).
В примере выше — это специфичный «HAL на коленке» под задачу. А HAL от STM тебе выдаст вон тот самый HAL_GPIO_WritePin, к которому ты ещё и номер пина будешь таскать.
И вот как раз использование такого макроса понятнее — «включить LED»/«выключить LED», всё ровно в терминах высокоуровневой задачи. И сам макрос тупее некуда.
И зафиксированные значения (по уму) будут локализованы в отдельном файле, где будет привязка к железу.
Не понимаю, как тут поможет HAL? Ну то есть можно прям в самом макросе HAL вызвать, но это не сильно поможет. Весь остальной код и так уже написан в терминах Work_Led_ON/Work_Led_OFF.
Да. У меня сильно больше времени ушло на попытку «правильно» использовать HAL, чем я то же самое потом повторил просто на регистрах.
Простые вещи, типа GPIO, нормально переносятся (хотя я и там словил грабли, т.к не было привычки инициализировать структуры нулями), а вот сложная настройка PWM, master-slave у таймеров, да ещё чтоб правильные выходы были включены и прерывания — вот это ни разу не просто делается. Даже банальный энкодер у меня не завёлся чисто через HAL, какие-то не те параметры они выставляют.
Не знаю, то ли я совсем балбес, то ли что, но когда мне потребовалось настроить таймер не самым тривиальным образом (PWM, но с тонкостями типа правильно настроенного preload и прерываний), то я потратил кучу времени на анализ кода HAL, чтобы правильно настроить регистры. Причём я так и не нашёл магическую комбинацию вызовов, которая бы сделала ровно то, что мне нужно.
В итоге, на тот момент остался совершенно нечитаемый вариант, где половина через HAL, а половина через регистры. Понять, на какое поведение рассчитывает код, исходя из кода инициализации, было решительно невозможно.
Переносимость? Я какое-то время метался между F0 и F103 — довольно сильные различия, HAL не сильно помогает. Даже банальные GPIO отличаются.
На мой взгляд, важнее иметь сильно типизированный API к регистрам периферии. Учитывая, что производитель публикует SVD файлы, которые (в идеале, конечно) описывают все нужные детали, этот типизированный API можно просто генерировать из этих SVD. При этом за счёт сильной типизации уже на этом уровне будут ловиться ошибки типа неправильных битовых масок, и.т.д, т.к все разрешённые значения будут иметь имена и привязку к конкретным позициям в конкретных регистрах.
Вот, например, как выглядит использование подобного API на Rust. Да, этот код ни разу не переносим, но для одноразового переноса на другой чип мне быстрее будет прочитать документацию по новому чипу и переписать его (и компилятор мне в этом поможет), а для разных целевых платформ я просто сделаю свой HAL, на том уровне абстракции, на котором мне нужно. Как пример, HAL для LCD экрана.
CLion хорош. Я когда делал свой мини-проект для STM32 почти было его купил.
Но потом я попробовал Rust и переписал всё на него. Рекомендую попробовать.
Есть плагин для IntellJ, но он довольно сыроват. В частности, вывод типов работает не идеально, из-за чего местами затруднительно использовать сгенерированную под контроллер библиотеку (генерируется много весьма специфичных типов, под конкретную периферию).
Согласен. Но для тех, кто в такие темы погружается набегами, в качестве хобби, любая информация полезна. Я просто недавно искал замену C++ в для своего маленького проекта на STM32 и без этого блога я бы самостоятельно Rust на голом STM32 не осилил бы.
Фишка DCEVM в возможности делать произвольные изменения классов (*).
[*] На самом деле, есть несколько ограничений. Можно добавлять интерфейсы к классу, но нельзя менять базовый класс и удалять интерфейсы. Можно добавлять/удалять поля класса, но статические поля не будут должным образом проинициализированны. Сборщик мусора поддерживается только один (хотя для отладки это не столь важно). Есть некоторые проблемы со стабильностью.
Сама большая проблема DCEVM — проект никуда особо не движется и какие у него перспективы (особенно с учётом выхода Java 9) — непонятно.
Нет, Java (компилятор) не создает новый класс для каждого места использования лямбды (если речь о 1.8 -> 1.8), а использует LambdaMetafactory/invokedynamic. Это легко проверяется компиляцией вот такого кода:
import java.util.function.Supplier;
public class Test {
public static void main(String... args) {
String hello = "hello";
print(() -> hello + ", world!");
}
public static <T> void print(Supplier<T> s) {
System.out.println(s.get());
}
}
И последующей декомпиляцией через «javap -verbose -p Test»
P.S. В рантайме да, создается класс, насколько я помню.
Я с деталями реализации Kotlin не знаком, а почему они не пошли по пути лямбд Java, где класс-реализация генерируется автоматически в райнтайме через LambdaMetafactory#metafactory?
Разработчики сохраняют классы в отдельные файлы а, при использовании inline, пользуются возможностью появившейся в Java, которая позволяет разместить в одном файле несколько классов.
Так это ж три этапа развития разработчика (на правах полу-шутки):
1) Копипастишь.
2) Не копипастишь, а переиспользуешь.
3) Понимаешь, когда копипастить можно, а когда не нужно.
Много разработчиков остаются на втором уровне понимания (отчасти, я думаю, из-за перфекционизма, типа модули, переиспользование кода, красота же).
Пусть делают небюджетные дизели, а бюджетными будут бензиновые двигатели, у них с этим попроще. А как иначе? Ослабление экологических норм — это какое-то странное решение, мой сосед, значит, экономит, а дышать мне.
Последний пункт, правда, не понял, про сложные операции. Код на Rust должен компилироваться в прямой доступ к регистрам.
То есть я согласен, строгая типизация — это важно, я про это писал выше. По этому параметру он, конечно же, лучше.
Но меня это в меньшей степени беспокоит, и вот почему.
Например, я перенёс свой датчик Холла с TIM1 на TIM2. А порт взял такой, который на второй канал попадает. И может быть, это альтернативный порт для этого канала, и нужно переназначение каналов таймера настраивать. И поставил триггер Шмитта, то есть вход стал инвертированный.
В обоих вариантах код будет сильно зависеть от конкретного железа. Соответственно, его всё равно придётся изолировать от остального кода. В обоих вариантах мне придётся существенно переписывать этот код (особенно, настройку). И вникать в техническую спецификацию, чтобы понять, какие регистры и как мне надо настроить (например, переназначение каналов таймера — это же совсем ни разу не очевидным образом настраивается).
И если мне и так приходится а) делать свой слой абстракции б) вникать, фактически, до уровня регистров, то в чём существенный плюс (кроме типизации, за которую я «за» обеими руками, о чём я уже писал)?
Вот что будет с HAL.
Делаешь, например, настройку имя порта + пины для вывдов DATA, EN, R/S LCD экрана и всё хорошо. А потом оказывается, что EN и R/S на другом порту, а ты это не предусмотрел. Придётся вводить ещё один параметр.
А потом что? Правильно, DATA пины (4 штуки) оказываются разбиты между двумя портами. И опять вся вот эта идея «меняем в одном месте» летим к чертям.
А потом ещё и не забыть нужные подсистемы включить (и, насколько я помню, у разных чипов раскладка периферии по подсистемам разная и HAL это не абстрагирует).
Я эти грабли все на своём проекте собирал, пока метался между F0 и F103, разными платами, языками (C -> C++ -> Rust), библиотеками (StdPeriph -> HAL -> Rust), подходами (C++ с шаблонами <-> C++ с классами).
Самая жизнеспособная в данном случае абстракция — это «интерфейс» с тремя методами «выставить EN», «выставить R/S», «выставить DATA». А уже что оно там внутри будет делать — пофиг, хоть HAL, хоть не HAL, там кода будет — кот наплакал.
P.S. А правильные библиотеки — согласен, как я уже говорил, я переехал на Rust и весьма счастлив. :)
P.S. Плюс код инициализации, но это отдельная песня.
HAL строит абстракции вокруг конкретной переферии. «Порты»/«Таймеры»/и.т.д.
Это может нормально работает для случаев, когда подсистема сильно отделена от всего остального. Не знаю, USB какой-нибудь, что-ли (хотя всегда есть DMA который на себя всё подряд замыкает). Я с такими крупными подсистемами не работал — не знаю. С FLASH работал, там HAL более-менее был удобен.
А вот когда у тебя подсистема — это хитрым образом сконфигурированный таймер и разные пины на разных портах, HAL тебе никак не поможет. Всё равно придётся писать свой слой абстракции, который будет предлагать API специфичный для подсистемы (например «выставить задержку на следующий цикл PWM»/«установить следующий цикл PWM последним»). И если ты такой слой пишешь, то по моему опыту, настроить регистрами проще и быстрее, чем пытаться найти комбинацию вызовов HAL, которая приведёт к нужному результату.
И которая, в 90% случаев всё равно не будет работать на другом устройстве, или из-за разницы в чипах, или банально из-за существенной разницы в раскладке на переферию/пины (тут выход PWM был основной пин, там — альтернативный, и.т.д).
В примере выше — это специфичный «HAL на коленке» под задачу. А HAL от STM тебе выдаст вон тот самый HAL_GPIO_WritePin, к которому ты ещё и номер пина будешь таскать.
И вот как раз использование такого макроса понятнее — «включить LED»/«выключить LED», всё ровно в терминах высокоуровневой задачи. И сам макрос тупее некуда.
И зафиксированные значения (по уму) будут локализованы в отдельном файле, где будет привязка к железу.
Не понимаю, как тут поможет HAL? Ну то есть можно прям в самом макросе HAL вызвать, но это не сильно поможет. Весь остальной код и так уже написан в терминах Work_Led_ON/Work_Led_OFF.
Простые вещи, типа GPIO, нормально переносятся (хотя я и там словил грабли, т.к не было привычки инициализировать структуры нулями), а вот сложная настройка PWM, master-slave у таймеров, да ещё чтоб правильные выходы были включены и прерывания — вот это ни разу не просто делается. Даже банальный энкодер у меня не завёлся чисто через HAL, какие-то не те параметры они выставляют.
В итоге, на тот момент остался совершенно нечитаемый вариант, где половина через HAL, а половина через регистры. Понять, на какое поведение рассчитывает код, исходя из кода инициализации, было решительно невозможно.
Переносимость? Я какое-то время метался между F0 и F103 — довольно сильные различия, HAL не сильно помогает. Даже банальные GPIO отличаются.
На мой взгляд, важнее иметь сильно типизированный API к регистрам периферии. Учитывая, что производитель публикует SVD файлы, которые (в идеале, конечно) описывают все нужные детали, этот типизированный API можно просто генерировать из этих SVD. При этом за счёт сильной типизации уже на этом уровне будут ловиться ошибки типа неправильных битовых масок, и.т.д, т.к все разрешённые значения будут иметь имена и привязку к конкретным позициям в конкретных регистрах.
Вот, например, как выглядит использование подобного API на Rust. Да, этот код ни разу не переносим, но для одноразового переноса на другой чип мне быстрее будет прочитать документацию по новому чипу и переписать его (и компилятор мне в этом поможет), а для разных целевых платформ я просто сделаю свой HAL, на том уровне абстракции, на котором мне нужно. Как пример, HAL для LCD экрана.
Но потом я попробовал Rust и переписал всё на него. Рекомендую попробовать.
Есть плагин для IntellJ, но он довольно сыроват. В частности, вывод типов работает не идеально, из-за чего местами затруднительно использовать сгенерированную под контроллер библиотеку (генерируется много весьма специфичных типов, под конкретную периферию).
Фишка DCEVM в возможности делать произвольные изменения классов (*).
[*] На самом деле, есть несколько ограничений. Можно добавлять интерфейсы к классу, но нельзя менять базовый класс и удалять интерфейсы. Можно добавлять/удалять поля класса, но статические поля не будут должным образом проинициализированны. Сборщик мусора поддерживается только один (хотя для отладки это не столь важно). Есть некоторые проблемы со стабильностью.
Сама большая проблема DCEVM — проект никуда особо не движется и какие у него перспективы (особенно с учётом выхода Java 9) — непонятно.
И последующей декомпиляцией через «javap -verbose -p Test»
P.S. В рантайме да, создается класс, насколько я помню.
А это что за возможность такая?
Смотрим host -t на twitter.com, github.com и reddit.com
Видно, где серьёзный бизнес, а кто так, постоять рядом.
1) Копипастишь.
2) Не копипастишь, а переиспользуешь.
3) Понимаешь, когда копипастить можно, а когда не нужно.
Много разработчиков остаются на втором уровне понимания (отчасти, я думаю, из-за перфекционизма, типа модули, переиспользование кода, красота же).