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

Изначально требовалось, чтобы следующий код не выдавал ошибок, а в итоге получили гораздо более мощный инструмент, чем предполагали.

const char *pStr = PSTR("Hello");	// В этом месте ошибка.
	// error: statement-expressions are not allowed outside functions nor in template-argument lists

int main() {…}

Те, кто не в курсе проблемы работы с памятью в микроконтроллерах серии AVR могут посмотреть спойлер
В контроллерах AVR используется два независимых адресных пространства:

  • для кода,
  • для оперативной памяти и регистров.

Компилятор 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), обязательно будет случай, когда мы захотим взять символ за пределами строки, что приведет к ошибке компиляции.

Первым рабочим вариантом был следующий способ:

  1. Добавляем к исходной строке строку, состоящую из символа '\0' и имеющую длину N символов. Добавление осуществлялось так: #define ADD_STR(STR) STR "\0\0\…\0".
  2. Проводим операцию 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};

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

Комментарии (8)


  1. Eivind
    06.10.2016 10:58
    +3

    Для GCC вы можете делать как-то так:

    #include <iostream>
    
    #define PROGMEM
    
    template <char... String>
    struct ProgmemString {
        static const char PROGMEM v[sizeof...( String ) + 1];
    };
    
    template <char... String>
    const char ProgmemString<String...>::v[sizeof...( String ) + 1] = {String..., '\0'};
    
    template <typename CharT, CharT... String>
    constexpr auto operator"" _progmem()
    {
        return ProgmemString<String...>::v;
    }
    
    int main()
    {
        std::cout << "progmem string"_progmem << std::endl;
    }
    


    1. ko1un
      06.10.2016 12:20

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


  1. LampTester
    06.10.2016 12:20
    +1

    Гм. Может быть я не проник в суть статьи или отстал от жизни, но я все-таки не совсем понял, чем не устроил вариант

    extern const char PROGMEM caption1[];
    const char caption1[] = "Hello";
    const char *pStr = caption1;
    


    Собственно, я всегда примерно так и делал. Более того, это рекомендуемый в документации на avr-libc способ решения подобных проблем. Собственно все еще проще:

    const char foo[] PROGMEM = "Hello, world!";
    
    ...
    
    strcpy_P(dest,foo);
    


    и, собственно, все.


    1. ko1un
      06.10.2016 12:32
      +1

      Согласен, этот способ работает, но требуется написать две строки вместо одной.

      strcpy_P(dest,SPS("Hello world!"));
      


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

      Короче, лень — двигатель прогресса.

      Кстати, рекомендуемый в документации способ, не работал одно время. Компилятор ругался, что __attribute__((__progmem__)) не может быть использован при создании переменной, а только в объявлении. Это было давно, но именно оттуда у меня привычка писать отдельно объявление, а потом инициализацию.


      1. LampTester
        06.10.2016 13:10

        Понял.

        Плюс сама строка располагается непосредственно в месте ее использования.


        В сущности, разделение объявления строки и места ее использования можно считать фичей. Такой подход позволяет вынести сообщения в отдельный файл, и при необходимости локализации править только его. С именами особой проблемы тоже нет:

        const char init_error_message[] PROGMEM = "Initialization failed.";
        const char init_OK_message[] PROGMEM = "Hardware initialized.";
        
        ...
        


        Но, конечно, тут на вкус и цвет.


  1. x893
    06.10.2016 12:49
    +1

    Взял библиотеку уже готовую — например adafruit fona.
    Там повсеместно используется конструкция

    имя_функции(F("какой-то текст"));
    


    «какой-то текст» располагается во флэш.
    Реализацию не сложно посмотреть.

    Хотя может и не понял глубины мысли в этом посте.


    1. LynXzp
      06.10.2016 13:42

      Тоже не понял глубину мысли. Все строки у меня PROGMEM или PSTR, т.е. в flash памяти кода. И все две функции работы со строками немного изменены и это «понимают». Ну нельзя просто так взять конкретный символ и что? pgm_read_byte( (char*)(&(str))+i ) — вот макрос для взятия символа. Ну разве что макросы — зло, а двухстраничные темплейты — добро.


    1. ko1un
      06.10.2016 21:00

      Макрос F() — это аналог PSTR.
      Весь смысл в том, что эти макросы не работают вне функций. А нам нужно было инициализировать глобальные переменные в кодовой памяти указателями на строку.

      static const char *pStr = PSTR("Hello");
      // error: statement-expressions are not allowed outside functions nor in template-argument lists
      


      Естественно, мы делали так же, как указал LampTester, но нам было лень постоянно так делать и мы нашли такой способ.