Объединяем (конкатенируем) значения двух и более макросов в чистом С
“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 команды. В даташите они выглядят так:
Т.е. к битам 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