В нашей компании мы пишем программы для контроллеров серии AVR. В этой статье я опишу как мы в нашей компании создаем строки, расположенные в кодовой памяти.
Нам требовалось, чтобы следующий код не выдавал ошибок, а в итоге мы получили гораздо более мощный инструмент, чем предполагали.
const char *pStr = PSTR("Hello"); // В этом месте ошибка.
// error: statement-expressions are not allowed outside functions nor in template-argument lists
int main() {…}
- для кода,
- для оперативной памяти и регистров.
Компилятор GCC использует двухбайтовый указатель, что предоставляет нам доступ к первых 64К кодовой памяти (остальная может быть использована только для инструкций) или ко всей ОЗУ.
Но узнать по указателю в какой памяти располагается переменная нет возможности. Из-за этого в библиотеке для avr-gcc появились отдельные функции для работы с кодовой памятью и строками, расположенными в кодовой памяти. Они маркируются суффиксом “_P” в конце имени функции. Например strcpy_P – аналог функции strcpy, принимающий указатель на строку, расположенную в кодовой памяти.
К сожалению, компилятор сам не может проверить где будет использоваться переменная и разместить ее там, а следовательно программисту требуется позаботиться об этом самому. Соответственно, появилась необходимость маркировки переменных, расположенных в кодовой памяти ключевым словом PROGMEM, чтобы явно указать что мы намерены работать с кодом.
Это, однако, не отменяет необходимости программисту следить за правильностью пользования переменных.
Больше всего неудобств нам доставляли строки. Строковые литералы являются объектами, расположенными в оперативной памяти, а значит занимают и оперативную память, и кодовую (надо же откуда-то брать значения для инициализации). Опять же они не подходят для работы с функциями, работающими с кодовой памятью. Например:
int main() {
char dest[20];
strcpy_P(dest, "Hello world!");
}
Этот код приведет к неопределенным последствиям, так как будет брать данные из кодовой памяти, расположенной по тому же адресу, что и строка “Hello world!” в оперативной памяти.
Для этих случаев в библиотеке avr был предусмотрен макрос PSTR(текст), возвращающий указатель на строку, расположенную в кодовой памяти.
int main() {
char dest[20];
strcpy_P(dest, PSTR("Hello world!"));
}
Теперь этот код работает и даже не занимает оперативную память. Но стоит вынести этот макрос за пределы какой-либо функции, он перестает работать.
const char *pStr = PSTR("Hello"); // В этом месте ошибка.
// error: statement-expressions are not allowed outside functions nor in template-argument lists
int main() {…}
Приходилось писать примерно такой код:
extern const char PROGMEM caption1[];
const char caption1[] = "Hello";
const char *pStr = caption1;
Это надуманный пример, но представим, что вместо pStr у нас инициализируется какая-то пользовательская структура, ожидающая указатель на строку.
В первую очередь это было необходимо для инициализации структуры меню. Вся инициализация должна была проводиться статически, на этапе компиляции.
Поэтому мы стали искать надежный способ для получения указателя на стоку в кодовой памяти. В этом нам помогли шаблонные классы. Для шаблонного класса можно создать статическую переменную, располагающуюся в кодовой памяти и получить указатель на нее.
template <char value>
struct ProgmemChar {
static const char PROGMEM v;
};
template <char value>
const char ProgmemChar<value>::v = value;
const char *pChar = &(ProgmemChar<'a'>::v);
Но строку не передашь параметром в шаблон. Поэтому мы решили разбить строку на символы. Как мы разбиваем строку на символы я покажу дальше, а пока покажу простой пример строки в кодовой памяти:
template <char ch1, char ch2, char ch3, char ch4, char ch5>
struct ProgmemString {
static const char PROGMEM v[5];
};
template <char ch1, char ch2, char ch3, char ch4, char ch5>
const char ProgmemString<ch1, ch2, ch3, ch4, ch5>::v[5] = {ch1, ch2, ch3, ch4, ch5};
const char *pStr = ProgmemString<'a', 'b', 'c', 'd', 0>::v;
Данный пример работает для строк, имеющих размер ровно 4 символа и завершающий 0 в конце. Причем строка ProgmemString<'a', 0, 0, 0, 0> тоже будет занимать 5 байт.
Для решения этой проблемы мы использовали частичную специализацию шаблонного класса, добавив в шаблон еще размер строки. Вот базовый шаблонный класс:
template<size_t S, char... L>struct _Pstr;
Теперь вернемся к проблеме разбиения строки на символы. Если честно, то для нас это до сих пор проблема, так как мы не смогли придумать пока ничего лучше, чем написать макрос, который N раз возьмет i-ый (от 0 до N-1) символ из исходной строки.
#define SPLIT_TO_CHAR_4(STR) STR[0], STR[1], STR[2], STR[3]
Этот макрос разбивает строку, в которой должно быть не меньше четырех символов, на символы. В данном случае N = 4.
Если подглядеть на код после препроцессора, то мы бы увидели следующий код:
"Hello world!"[0], "Hello world!"[1], "Hello world!"[2], "Hello world!"[3]
Согласен, что это длинный текст, но мы с этим смирились. Тем более, что сам компилятор уже выдает только четыре символа.
Более важной проблемой было взятие символа с большим индексом. Для большого N (а мы хотим чтобы все наши строки были короче N), обязательно будет случай, когда мы захотим взять символ за пределами строки, что приведет к ошибке компиляции.
Первым рабочим вариантом был следующий способ:
- Добавляем к исходной строке строку, состоящую из символа '' и имеющую длину N символов. Добавление осуществлялось так: #define ADD_STR(STR) STR "…".
- Проводим операцию SPLIT_TO_CHAR над получившейся строкой.
Этот способ работает, но гарантированно увеличивает код после препроцессора на N*N символов. В итоге мы быстро получаем предел компилятора.
К счастью с приходом с++11 и constexpr функций у нас получилось избавиться от лишних символов, используя класс селектор символов. Для краткости он называется _CS (Char Selector).
struct _CS {
template<size_t n>
constexpr _CS(const char (&s)[n]) :s(s), l(n){}
constexpr char operator [](size_t i){return i < l ?s[i] :0;}
const char *s = 0;
const size_t l = 0;
};
Код этого класса я давненько подсмотрел на Хабре, но не могу сейчас найти где именно (спасибо тебе автор).
Код макроса разделения на символы стал проще:
#define SPLIT_TO_CHAR(STR) _CS(STR)[0], _CS(STR)[1], …, _CS(STR)[N-1]
Теперь осталось собрать все вместе:
// Базовый шаблон строки
template<size_t S, char... L>struct _PStr;
// Вспомогательные макросы, раскрывающие последовательность пронумерованных элементов. В примере я ограничился 10 элементами
#define ARGS01(P, S) P##00 S
#define ARGS02(P, S) ARGS01(P, S),P##01 S
#define ARGS03(P, S) ARGS02(P, S),P##02 S
#define ARGS04(P, S) ARGS03(P, S),P##03 S
#define ARGS05(P, S) ARGS04(P, S),P##04 S
#define ARGS06(P, S) ARGS05(P, S),P##05 S
#define ARGS07(P, S) ARGS06(P, S),P##06 S
#define ARGS08(P, S) ARGS07(P, S),P##07 S
#define ARGS09(P, S) ARGS08(P, S),P##08 S
#define ARGS0A(P, S) ARGS09(P, S),P##09 S
// Специализации класса для определенной длины строки (от 0 до 10 символов). Строка гарантированно будет завершена 0.
template<char... L>struct _PStr<0x00, L...>{static const char PROGMEM v[];};
template<char... L>const char _PStr<0x00, L...>::v[] = {0};
template<ARGS01(char _,), char... L>struct _PStr<0x01, ARGS01(_,), L...>{static const char PROGMEM v[];};
template<ARGS01(char _,), char... L>const char _PStr<0x01, ARGS01(_,), L...>::v[] = {ARGS01(_,), 0};
template<ARGS02(char _,), char... L>struct _PStr<0x02, ARGS02(_,), L...>{static const char PROGMEM v[];};
template<ARGS02(char _,), char... L>const char _PStr<0x02, ARGS02(_,), L...>::v[] = {ARGS02(_,), 0};
template<ARGS03(char _,), char... L>struct _PStr<0x03, ARGS03(_,), L...>{static const char PROGMEM v[];};
template<ARGS03(char _,), char... L>const char _PStr<0x03, ARGS03(_,), L...>::v[] = {ARGS03(_,), 0};
template<ARGS04(char _,), char... L>struct _PStr<0x04, ARGS04(_,), L...>{static const char PROGMEM v[];};
template<ARGS04(char _,), char... L>const char _PStr<0x04, ARGS04(_,), L...>::v[] = {ARGS04(_,), 0};
template<ARGS05(char _,), char... L>struct _PStr<0x05, ARGS05(_,), L...>{static const char PROGMEM v[];};
template<ARGS05(char _,), char... L>const char _PStr<0x05, ARGS05(_,), L...>::v[] = {ARGS05(_,), 0};
template<ARGS06(char _,), char... L>struct _PStr<0x06, ARGS06(_,), L...>{static const char PROGMEM v[];};
template<ARGS06(char _,), char... L>const char _PStr<0x06, ARGS06(_,), L...>::v[] = {ARGS06(_,), 0};
template<ARGS07(char _,), char... L>struct _PStr<0x07, ARGS07(_,), L...>{static const char PROGMEM v[];};
template<ARGS07(char _,), char... L>const char _PStr<0x07, ARGS07(_,), L...>::v[] = {ARGS07(_,), 0};
template<ARGS08(char _,), char... L>struct _PStr<0x08, ARGS08(_,), L...>{static const char PROGMEM v[];};
template<ARGS08(char _,), char... L>const char _PStr<0x08, ARGS08(_,), L...>::v[] = {ARGS08(_,), 0};
template<ARGS09(char _,), char... L>struct _PStr<0x09, ARGS09(_,), L...>{static const char PROGMEM v[];};
template<ARGS09(char _,), char... L>const char _PStr<0x09, ARGS09(_,), L...>::v[] = {ARGS09(_,), 0};
template<ARGS0A(char _,), char... L>struct _PStr<0x0A, ARGS0A(_,), L...>{static const char PROGMEM v[];};
template<ARGS0A(char _,), char... L>const char _PStr<0x0A, ARGS0A(_,), L...>::v[] = {ARGS0A(_,), 0};
// Селектор символа
struct _CS {
template<size_t n>
constexpr _CS(const char (&s)[n]) :s(s), l(n){}
constexpr char operator [](size_t i){return i < l ?s[i] :0;}
const char *s = 0;
const size_t l = 0;
};
// Вспомогательный макрос для экранирования запятых
#define STR_UNION(...) __VA_ARGS__
// Главный макрос, возвращающий указатель на строку, расположенную в кодовой памяти. SPS = StaticProgramString.
#define SPS(T) STR_UNION(_PStr<_CS(T).l - 1, ARGS0A(_CS(T)[0x, ])>::v)
Разберем по элементам главнй макрос:
- _Pstr<size_t S, char… L>::v – указатель на строку длиной S и содержащую символы L,
- _CS(T).l – 1 — размер исходной строки без нуля в конце,
- ARGS0A(_CS(T)[0x, ]) — макрос, забирающий первые 10 символов из исходной строки.
Для каждой строки будет выбрана своя специализация шаблона, подходящая по длине строки.
Подводя итоги я хотел бы сказать, что с помощью этого макроса нам удалось реализовать не только получение указателя на строку в коде, независимо от того где этот макрос применяется, но и еще два явных преимущества перед PSTR:
- Для каждой уникальной строки, созданной с помощью SPS будет создан только один экземпляр строки, ведь статическое поля шаблона создается только один раз для всего проекта. Конечно, современные компиляторы могут оптимизировать использование строк, но только в рамках компиляции одного файла cpp.
- Строка создается с глобальным доступным именем, что необходимо для использования в качестве параметра шаблона.
template <class T, const char *name>
struct NamedType {
T value;
static const char *getName() {
return name;
}
};
NamedType<int, SPS("Параметр")> var1 = {3};
Эти шаблонные классы позволили нам собирать метаданные о переменных в проекте, что позволило нам упростить разработку на порядок, одновременно улучшив пользовательский интерфейс и гибкость настройки. Но это уже другая история.
Автор: ko1un