Рядка в кодовій пам'яті AVR

У нашій компанії ми пишемо програми для контролерів серії AVR. У цій статті я опишу як ми в нашій компанії створюємо рядки, розташовані в кодовій пам'яті.

Нам потрібно, щоб наступний код не видавав помилок, а в результаті ми отримали набагато більш потужний інструмент, ніж припускали.

const char *pStr = PSTR("Привіт"); // У цьому місці помилка.
// 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("Привіт"); // У цьому місці помилка.
// error: statement-expressions are not allowed outside functions nor in template-argument lists

int main() {...}

Доводилося писати приблизно такий код:

extern const char PROGMEM caption1[];
const char caption1[] = "Привіт";
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 ch 2, char ch 3, char ch 4, char ch5>
struct ProgmemString {
static const char PROGMEM v[5];
};
template <char ch1, char ch 2, char ch 3, char ch 4, 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##S 00
#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};

Ці шаблонні класи дозволили нам збирати метадані про змінних у проекті, що дозволило нам спростити розробку на порядок, одночасно поліпшивши користувальницький інтерфейс та гнучкість налаштування. Але це вже інша історія.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.