Pull to refresh

Comments 54

можно ли полноценно программировать при помощи директивы #define в языке C?

может сначала нужно было ответить на вопрос: "А нужно ли?". Вроде как Дефайны не раз были объявлены воплощением зла в программировании? Хотя, без них и в самом модном С++100500+, все также никуда, кажется.

Хотя, может я что-то пропустил ... ? какой-то сбой в матрице?

Но вот для конструкций с битами в портах в embedded, лучше #define-ов не придумать, это точно. Я правда подзабыл как это было, никто не напомнит?

может сначала нужно было ответить на вопрос: "А нужно ли?". 

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

Вы серьёзно задаёте этот вопрос по тексту, опубликованному в хабе "ненормальное программирование"?

так я же на него, вроде как, сам и ответил:

Хотя, без них и в самом модном С++100500+, все также никуда, кажется.

Там есть другой вопрос, он намного интересней, мне кажется.

Часто при программировании под AVR использовал конструкции типа ON(PORT, N) и OFF(PORT, N). Сами конструкции были типа (PORT|=(1<<N)) и (PORT&=~(1<<N)).

вроде не так, там что-то хитрое было, была задача и было решение. Что-то про PortA.3

PortB.5, DDRD.7, ...

Придется искать - найду, напишу позже.

Такое, как минимум CVAVR умеет. Прикол АВРок в этом плане в том, что там есть специальные команды битового доступа до некоторой периферии. То есть в идеале мы должны получить одну машинную инструкцию на выходе, а не чтение-модификация-запись. Впрочем, и в СТМ32 есть похожий механизм. Если таки найдёте как грамотно внедрить такие конструкции, то буду признателен.

как-то так было:

// Макросы
#define set_out2(port, bit) do { DDR ## port  |= (1<<bit) } while(0)
#define set_in2(port, bit) do { DDR ## port  &= ~(1<<bit) } while(0)
#define on2(port, bit) do { PORT ## port  |= (1<<bit) } while(0)

#define set_out(...) set_out2(__VA_ARGS__)
#define set_in(...) set_in2(__VA_ARGS__)
#define on(...) on2(__VA_ARGS__)
// определяем порт (обратите внимание - задана буква порта, PORT/PIN/DDR приписываем к нему в макросах через ##)
#define W1 C, 5
 
// настраиваем порт
set_out(W1);
// используем
on(W1);

вот здесь вот нашел что-то похожее на то что я имел ввиду. Но у меня не было __VA_ARGS__, это что-то и для меня уже новенькое.

Я сам уже с портами несколько лет ничего не писал, своих проектов не найду ни как.

А! вот! по той же ссылке, найдите:

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

...

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

Эх, а я уж губу раскатал на прямой битовый доступ.

Вместо подобных "функций" с передачей порта и бита себе сразу пишу конструкции типа

#define LedON() GPIOB->BSRR |= GPIO_BSRR_BS_1
#define LedOFF() GPIOB->BSRR |= GPIO_BSRR_BR_1

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

Тоже получается чисто запись, а не чтение-модификация-запись.

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

Типа так:

вместо LedON()

TurnOn(LedX)

соответственно можно для какого-нибудь ControlY

TurnOn(ControlY)

То есть не надо для каждой ноги свой Дефайн писать!

И функции в одном месте по ногам выписаны в виде порт:пин - вот это меня особенно порадовало, я помню, когда у меня такой список в коде появился!

Так ведь та же история и выходит. Что LedX определить, что LedON() сделать. Всё в main.h какой-нибудь вынести, вот в одном месте и получается. Не вижу явных плюсов или минусов.

Что LedX определить, что LedON() сделать.

ну так плюсы появляются когда у вас много пинов с разными функциями как пины используются. Когда у вас Led1 .. Led9 и

например, Key1 .. Key 5

и фиг знает какие еще пины, в общем если есть штук десять пинов в работе, и если их просто начинаешь проверять-искать по всему коду или

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

то чуствуешь разницу с таким подходом.

начинаешь проверять-искать по всему коду

У нас явно возникло какое-то досадное недопонимание. Зачем по всему? Один раз дефайном объявляется LedON() на светодиод, или наоборот, всякие IsEnable() для опроса состояния ноги-входа и всё, дальше уже используются. При изменении порта все правки по-прежнему происходят в одном месте. Хоть одна нога, хоть десять, всё равно все правки ровно однократные и в одном месте. Точно так же, как однократно объявить TurnOn() и пучок LedX.

Один раз дефайном объявляется LedON() на светодиод

а если у нас три светодиода?

Но все это конечно не чудо, вопрос только в том есть ли желание попробовать другую технику. Вы решили что вам не к чему. Я уважаю ваше решение.


/*******************************************************************************
  port
*******************************************************************************/
// port 
#define A0_PORT                 GPIOA
#define A1_PORT                 GPIOA
...
..

/*******************************************************************************
  pin
*******************************************************************************/
// port A
#define A0_PIN                  GPIO_PIN_0
#define A1_PIN                  GPIO_PIN_1
...
..

/*******************************************************************************
  macro
*******************************************************************************/

#define GPIO_PORT_(x)           x##_PORT
#define GPIO_PIN_(x)            x##_PIN
#define GPIO_PORT_NUM_(x)       x##_PORT_NUM
#define GPIO_PIN_NUM_(x)        x##_PIN_NUM

#define GPIO_PORT(x)            GPIO_PORT_(x)
#define GPIO_PIN(x)             GPIO_PIN_(x)
#define GPIO_PORT_NUM(x)        GPIO_PORT_NUM_(x)
#define GPIO_PIN_NUM(x)         GPIO_PIN_NUM_(x)

//pin config
#define GPIO_CONFIG_IN_M(pin)           (GPIO_PORT_(pin)->MODER &= ~(3U<<(2*GPIO_PIN_NUM_(pin))))
#define GPIO_CONFIG_OUT_M(pin)          (GPIO_PORT_(pin)->MODER |= 1<<(2*GPIO_PIN_NUM_(pin)))

// pin set
#define GPIO_READ_PIN_M(pin)            (GPIO_PORT_(pin)->IDR & GPIO_PIN_(pin))
#define GPIO_SET_PIN_M(pin)             (GPIO_PORT_(pin)->BSRR = GPIO_PIN_(pin))
#define GPIO_RESET_PIN_M(pin)           (GPIO_PORT_(pin)->BRR = GPIO_PIN_(pin))

// port set
#define GPIO_SET_PORT_M(port, value)    (port->BSRR = value)
#define GPIO_RESET_PORT_M(port, value)  (port->BRR = value)
#define GPIO_ASSIGN_PORT_M(port, value) (port->ODR = value)

//работает так
#define LED_RED    A1  
GPIO_SET_PIN_M(LED_RED)

ага, вот оно. А я только версию для АВР-ов раньше видел значит, эта еще интереснее.

Это называется Bit-Banding. Грамотно внедрить его можете сами, достаточно немного почитать документацию. А еще есть такие полезные регистры в GPIO как BSRR и BRR, которые позволяют менять состояния пинов без RMW.

В СТМах без проблем, там, как вы и пишете, отдельные регистры для таких манёвров. А вот в АВРах не регистр, а отдельная команда. Это было бы интереснее из С.

AVR вообще значительно проще устроен. Я не только про ядро, я про вообще. Такие штуки зависят от реализации конкретной периферии в конкретном микроконтроллере, ну или в серии.

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

Для данного хаба - самое то!

Я в далеком 2005 и сам занимался подобными извращениями.

Всё, описанное в статье, невероятно круто.

Я тут подумал… Интересно, а есть ли альтернативные встроенному препроцессоры, заточенные под C/++, как LESS заточен под CSS? И что с их помощью можно было бы сделать, если бы они были? (Да, вот такая упячная постановка вопроса в духе «Есть ли жизнь на Марсе и как с ней бороться?» ;).

Я вчера немножко наспех и левой ногой написал этот комментарий, и хочу дополнить.

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

А спрашивая «Для чего?», я имел в виду следующее. Я знаю, для чего он пригодился бы мне. Но мне интересно послушать, как бы его использовали другие.

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

void adns__querysend_tcp(adns_query qu, struct timeval now) {
  ...
  if (!(errno == EAGAIN || EWOULDBLOCK || 
        errno == EINTR || errno == ENOSPC ||
        errno == ENOBUFS || errno == ENOMEM)) {
  ...
}

Чтобы не допускать ошибок, предлагалось правильно форматировать код. С моей точки зрения, если эта ошибка — рак поджелудочной железы, то форматирование — это свекольный сок. А химиотерапией был бы is in set. Только вот как его сделать? Единственное адекватное решение, которое мне предложили, когда я задал этот вопрос, включало в себя шаблоны и вызов функции. Шаблонам в нешаблонном коде делать нечего, это семантически неправильно и эстетически отвратительно, а оптимизация вызова всё ещё зависит от выбора правильного компилятора (и это не студия). И, конечно же, ни в какой стандартной библиотеке этой жизненно необходимой для избежания ошибок штуки нет.

Предложенный вариант на шаблонах
// © vopl
#include <errno.h>

template <auto... set>
bool isin(auto val)
{
    return (false || ... || (set == val));
}

int main() 
{
    // 
    if(isin<EAGAIN,
            EWOULDBLOCK,
            EINTR,
            ENOSPC,
            ENOBUFS,
            ENOMEM>(errno))
    {
        return 0;
    }

    return 1;
}

Правда, таскать дополнительный препроцессор это тот ещё геморрой, но если правильно его встроить в IDE, можно было бы видеть результирующий код только при отладке.

Что такое LESS и CSS, я думаю, все знают и так, и почему я про них вспомнил — тоже.

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

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

Ну вроде m4 помощнее будет (хотя руками я его не щупал).

Спасибо, не слышал про такой. Он, кстати, через три года будет праздновать юбилей — 50 лет ))

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

Можно без шаблонов (если не считать таковым сам initializer_list) и читабельнее:

bool in(const int test_value, const std::initializer_list<int> values) {
  for (const auto & value : values)
  {
    if (test_value == value)
    {
      return true;
    }
  }

  return false;
}
// И где-то ниже
if (!in(errno, {EAGAIN, EWOULDBLOCK, EINTR, ENOSPC, ENOBUFS, ENOMEM}))

Хотя ещё лучше сделать in шаблоном и вместо int использовать T.
Но в любом случае это не шаблоны на списке типов, где каждый новый список - это новая сгенерированная функция.
А вообще в c++ появляется всё больше разного для замены макросов. И в шаблонах, и, например, constexpr/consteval для вычисления в compile time.

Конкретно в этом случае (если там условно только return false) как раз switch-case с fallthrough подходит? Но если развивать идею, то куски кода побольше уже в нужный case не засунешь. А выпригивать из switch в опрятный код ничем не лучше.

Честно, конструкция понравилась. Странно, что её нет в языках.

switch не подходит по многим причинам.

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

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

В-третьих, и вообще, выразительность хромает.

Честно, конструкция понравилась. Странно, что её нет в языках.

Именно потому мне и не хватает нормальных макросов.

я добавлял рекурсию в макросы в ллвм, но как-то всё остановилось, т.к. все боятся препроцессора как огня (в коммитете)

https://github.com/llvm/llvm-project/pull/65851

Упоминали М4, но он ужасен. Общий принцип: если хотите сложную кодогенерацию, лучше взять условный питон и засунуть его в prebuild шаг сборки. Отладка макросов это очень больно.

В принципе, можно сломать мозг и попробовать осмыслить препроцессор как РЕФАЛ-машину с её подстановками и без присваивания, но практический смысл околонулевой.

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

ИМХО, в этом вопросе главное — синтаксис и стоящие за ним идеи (у меня их нет), а не то, на чём и как это писать.

Отладка макросов это очень больно.

Опять же ИМХО, но это касается только встроенного препроцессора. (Вроде бы, студию вообще нельзя заставить выдать код после обработки). Если сделать внешний препроцессор в виде плагина к IDE, чтобы он при отладке переключался на сгенерированный код, откуда возьмётся боль?

про питон это я в целях удобства и понимаемости, чтобы писать "шаблоны" в той же парадигме, что и остальной код. Касательно отладки макросов: пока все просто, понятно, во что развернется макрос, а вот если попробуете добавить в код полиформизм с помощью макросов или еще что хитрое, там начинается конкатенация токенов, в итоге вы ловите ошибку компиляции из-за криво развернувшегося макроса, а почему он криво развернулся - только руками, ide поможет слабо, максимум - пошаговым разворачиванием.

Теперь я понял.

Нуууууууу… это как с шаблонными классами и алгоритмами, ИМХО. В том смысле, что нормальный (в математическом смысле) прикладник ими только пользуется. Конечно, это не C++ way, но лично я был бы счастлив.

чтобы писать "шаблоны" в той же парадигме, что и остальной код

C/C++ с препроцессингом на питоне - это скорее адская смесь, чем общая парадигма. Тут хороший пример - Лисп, где макросы пишутся на том же языке, что основной код, и синтаксис просто мапится на удобные для обработки структуры данных - но не всем такой синтаксис нравится )

Лисп, конечно, идеал, но питон на С похож больше, чем собственный сёвый препроцессор)

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

В слёзы :-D

Пару секунд прикидывал, сколько раз оно само с собой сложится, и тут заметил, что там ноль :-D

Это не просто программистская шутка, это классический анекдот с классической концовкой-переворотом, и несмотря на язык, на котором этот анекдот написан — я ржал как конь идиот конь-идиот :-D

Будет 2^63 - 1 сложений. Нуля :)

Да, классическая задача с зернышками на шахматной доске (там же неспроста 64 макроса?), только на первой клетке... 0 зернышек!

Сишный код вывел "3 2 1 0", а версия на макросах "3 2 1".

Да, действительно, в макросах у меня нет принта при d==0. Сейчас поправлю. Спасибо!

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

Sonic the Hedgehog
Sonic the Hedgehog

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

Хитрые приёмы, зато как классно работают) Можно почитать здесь на счёт архетектуры и приёмов в Genesis, если интересно: https://habr.com/ru/articles/471914/

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

Привет, как с продолжением о троичном процессоре?

на PC подобные эффекты делали с помощью изменения палитры. На приставках - без понятия...

Ну пример со сложением 2^64 количества нулей недалеко ушёл :)

по алгоритму огня - ощущение постоянной скорости изменения ("горения"), что выглядит не совсем натурально

Тут вопрос компромисса: нужно максимизировать визуальный результат при минимизации количества и сложности строк кода. Присылайте пулл реквест :)

Кстати, спойлер: следующая моя демка будет с гидродинамикой сглаженных частиц.

Помню, работал в одной из контор... STM32, память только набортная, динамической аллокации нет. Зато есть не SQL БД и вебморда правления миром. Нужно было как-то объявлять переменные на стеке такого размера, чтобы они были размером с поле в БД, и чтобы другие разрабы не косячили с памятью, ошибаясь в размерах.

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

Прошёл годик, я решил вспомнить как же оно работает... И не вспомнил / не понял... Ну, работает и ладно :)

Будьте осторожны в ваших макросах :)

Я, честно говоря, немного прифигел. Не думал, что отрисовка огня - настолько простая задача

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

Если вы любите такого рода магию, посмотрите на библиотеку P99 за авторством Jens Gustedt. За использование на проде меня один раз побили.

Sign up to leave a comment.

Articles