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

Объединяем (конкатенируем) значения двух и более макросов в чистом С

чистый си

“C” by duncan is licensed under CC BY-NC 2.0

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

Я все же для макроопределений буду использовать слово “макросы”. Если кто не до конца понял, речь о конструкции, типа:

#define BUFFER_SIZE 1024

где ‘#define’ - директива, определяющая макрос с именем ‘BUFFER_SIZE’, которому соответствует значение ‘1024’.

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

У меня же очень много директив #define, и мне нужно два из них объединять в одну прямо в самом коде. Сейчас объясню, как так получилось.

Задача: нужно написать прошивку для моего любимого микроконтроллера, который будет взаимодействовать с такой микросхемой, как nRF24L01. Написать с нуля без готовых библиотек (привет ардуино), опираясь лишь на даташит.

В даташите наборы битов, которые нужно отправлять микросхеме. Дабы не запутаться в этих битовых последовательностях я и использую такую директиву, как #define.

Выглядит это следующим образом:

#include <avr/io.h>
#include <avr/interrupt.h>

// КОМАНДЫ
// прочитать данные из регистра:
#define R_REGISTER_3 000
// записать данные в регистр:
#define W_REGISTER_3 001
// прочитать данные из буфера:
#define R_RX_PAYLOAD 0b01100001
// записать данные в буфер для послед. отправки:
#define W_TX_PAYLOAD 0b10100000
// очистка буфера передатчика:
#define FLUSH_TX 0b11100001
// очистка буфера приемника:
#define FLUSH_RX 0b11100010
// использовать повторно посл. переданный пакет:
#define REUSE_TX_PL 0b11100011

// РЕГИСТРЫ
#define CONFIG_5 00000
#define EN_AA_5 00001
#define EN_RXADDR_5 00010
#define SETUP_AW_5 00011
#define SETUP_RETR_5 00100
#define RF_CH_5 00101
#define RF_SETUP_5 00110
#define STATUS_5 00111
#define OBSERVE_TX_5 01000
#define CD_5 01001
#define RX_ADDR_P0_5 01010
#define RX_ADDR_P1_5 01011
#define RX_ADDR_P2_5 01100
#define RX_ADDR_P3_5 01101
// ...
#define FIFO_STATUS_5 10111
// и т.д.

Удобочитаемость кода наше все!

Интересуют первые 2 команды. В даташите они выглядят так:

регистры R_REGISTER и W_REGISTER в nRF24L01

Т.е. к битам 000 или 001 добавляется еще и номер регистра ААААА. И чтобы не писать кучу #define отдельно для команды R_REGISTER и отдельно для W_REGISTER нужно заставить препроцессор объединять два значения макросов.

Для примера попробуем присвоить переменной х значение, состоящее из объединения значений W_REGISTER_3 с, ну допустим, с SETUP_AW_5.

Если мы определим R_REGISTER_3 с 0b

#define W_REGISTER_3_ 0b001

которому остается только дописать номер регистра из пяти последующих битов, то написав банальное

uint8_t x = W_REGISTER_3_SETUP_AW_5;

компилятор пошлет вас нахрен, спросив, “А что такое этот ваш W_REGISTER_3_SETUP_AW_5?”.

Особенно если мы попытаемся сделать так:

uint8_t x = 0bW_REGISTER_3;

мол “Ты ахренел подставлять мне вместо единиц и ноликов буковки?”. Это при том, что uint8_t x = 0b001; компилятором вполне себе спокойно проглатывается.

Препроцессор не умеет подставлять значения, когда рядышком другие циферки и буковки. Но вот для подобного рода объединения придумали такую штуку, как “##”. Работает примерно так:

// ...
#define SPLIT(a, b) a##b##_z
// ...
uint8_t xy_z = 8;
uint8_t x = SPLIT(x, y);
// ... и т.д.

Результат работы препроцессора можно посмотреть в папке проекта командой:

avr-gcc -E main.c

А результат этого куска кода следующий:

uint8_t xy_z = 8;
uint8_t x = xy_z;

Ну т.е. вместо a и b подставили x и y и конкатенировали не только друг с другом, но и с _z.

Можно ли также сделать со значениями макросов? Давайте попробуем! Создадим макрос с именем RW_REG (read-write register).

//Чтение-запись регистра
#define RW_REG(cmd, reg) 0b##cmd##reg

И подставим в него имена макросов.

uint8_t x = RW_REG(W_REGISTER_3, SETUP_AW_5);

Думайте конструкция работает? А вот х(нет)! Компилятор ругается, мол “А что такое этот ваш bW_REGISTER_3SETUP_AW_5?”. Ибо результат работы препроцессора в этом куске кода такой:

uint8_t x = 0bW_REGISTER_3SETUP_AW_5;

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

Как?
Почему?
Как с этим быть?

Варианты решений искал достаточно долго, но все же нашел!

Речь о так называемой “двойной развязке”. Оказывается, чтобы подставились именно значения макросов, нужно сначала сделать макрос с объединением через “##”, потом создать еще один макрос, который содержит в себе предыдущий макрос.

Сам бы я до такого не додумался. В общем в нашем случае это делается так:

#define _RW_REG(cmd, reg) 0b##cmd##reg
#define RW_REG(cmd, reg) _RW_REG(cmd, reg)

В итоге наш кусочек кода

uint8_t x = RW_REG(W_REGISTER_3, SETUP_AW_5);

после работы препроцессора имеет вид

uint8_t x = 0b00100011;

То, что нам и нужно. В общем целом с этим теперь очень удобно работать. По крайней мере и сам механизм работы препроцессора становится несколько понятен. К тому же, как я уже протестировал, значений макросов в такой конструкции может быть и больше двух.

Надеюсь, эта статья кому-нибудь, да помогла.

 17.10.2021