Строки в кодовой памяти AVR

в 7:23, , рубрики: avr, c++, c++11, PROGMEM, программирование микроконтроллеров

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

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

Автор: ko1un

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js