Как инициализировать массивы с помощью форм сигналов и других файловых данных. Советы и рекомендации по разработке прошивок

Добавлено 29 сентября 2019 в 09:32

В данной статье показано, как инициализировать массивы в программе на языке C значениями из текстовых файлов.

Предполагается, что данные не хранятся в исходных файлах. Файлы читаются во время компиляции программы. Рассматриваются одномерные и многомерные массивы. Примеры также показывают, как управлять размещением массивов в ОЗУ или энергонезависимой памяти и выбирать, какие файлы данных использовать для инициализации.

Для примеров используется компилятор GCC для ARM с 32-разрядным микроконтроллером в качестве цели. Все примеры используют стандарт C и работают с этим компилятором.

Основы инициализации массивов

Массив может быть инициализирован значениями, когда он «объявляется». Типовое объявление показано ниже. Значения в фигурных скобках называются «инициализаторами».

int16_t testArray[6] = {1, 2, 3, 4, 5, 6};

Если размер массива не указан в скобках, размер будет равен количеству инициализаторов. Если инициализаторов меньше, чем размер массива, дополнительные элементы устанавливаются в значение 0. Большее количество инициализаторов, по сравнению с размером массива, вызовет ошибку.

Пробелы

Инициализаторы должны быть разделены запятыми. Добавление «пробела» – это нормально. В этом случае под пробелом подразумеваются символы «пустого пространства» или пробельные символы. Набор пробельных символов включает в себя пробел, табуляцию, перевод строки, возврат каретки, вертикальную табуляцию и перевод страницы. Символы новой строки и возврата каретки используются для обозначения конца строки в исходном коде C. Я знаю символ перевода страницы, но что такое вертикальная табуляция?

Как правило, C не заботится о том, содержит ли оператор пробелы или продолжается ли он в следующей строке. Оператор здесь эквивалентен коду, приведенному выше. В случае больших массивов обычно можно увидеть много-много строк инициализаторов. Возможно, даже страницы. В какой-то момент мы можем спросить: «А есть ли способ лучше?»

int16_t testArray[6] = {1, 2, 3, 
                        4, 5, 6};

Инициализация массива из файла

Исходный код C перед компиляцией проходит через препроцессор. Обычно используемая функция препроцессоров C – это «включение файла». Вот цитата из известной книги Кернигана и Ричи «Язык программирования Си».

Включение файлов позволяет легко обрабатывать коллекции #defines и объявлений (среди прочего).

Я подчеркнул «среди прочего». Хотя мы обычно включаем файлы «.c» и «.h», препроцессору не важно расширение имени файла. Подойдет любой текстовый файл. Итак, следующий синтаксис работает для инициализации массива.

int16_t testArray[6] = {
#include "array_fill.txt"
};

Файл не должен содержать никаких специальных символов, которые иногда скрыты для форматирования документа. Не усложняйте. Никакого форматирования текста. Никаких заголовков столбцов. Только цифры, запятые и пробелы. Вот файл, созданный с помощью Windows Notepad.

Содержимое текстового файла
Содержимое текстового файла

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

Значения массива в памяти
Значения массива в памяти

Хранение массива в энергонезависимой памяти и выбор файла данных

В приведенном выше примере массив является глобальной переменной, и ничто не указывает, куда поместить массив. Компилятор и линкер предполагают, что программа может изменить массив, и помещают его в оперативную память. Начальные значения находятся в энергонезависимой памяти («NVM», «non-volatile memory», обычно флэш-память), и массив в RAM памяти инициализируется этими данными кодом, который выполняется перед основной программой. Эти данные в NVM не доступны для программы. Если массив не будет изменяться (будет «константой»), он помещается только в NVM и доступен программе напрямую. Это экономит оперативную память, которой часто не хватает. Сообщение компилятору и линкеру о том, что массив не будет изменяться, и что его нужно поместить в NVM, обычно делается с помощью спецификатора «const». Ниже приведены пример и его результат. Столбец Location указывает на расположение массива в начальных адресах памяти, которые для данного микроконтроллера являются флэш-памятью.

const int16_t testArray[6] = {
#include "array_fill.txt"
};
Новое расположение массива в памяти
Новое расположение массива в памяти

Операторы препроцессора #define и #if могут использоваться для предоставления выбора для определения местоположения массива и выбора файлов данных, которые используются для инициализации. Вот пример, который дает выбор размещения массива в RAM или NVM.

#define USE_NVM 1
// USE_NVM необходимо определить до момента использования

#if USE_NVM
  const
#endif
int16_t testArray[6] = {
  #include "array_fill.txt"
};

Конструкция #if является примером «условного включения». В этом случае она управляет тем, используется ли спецификатор «const» при объявлении массива. Это работает, потому что объявление может быть описано в более чем одной строке или, иначе говоря, пробельные символы – это хорошо.

Вот пример использования условного включения для выбора файла для инициализации.

#define USE_NVM 1
#define USE_NOISE
// USE_NVM и USE_NOISE необходимо определить до момента использования

#if USE_NVM
  const
#endif
int16_t haversineArray[101] = {
  #ifdef USE_NOISE
    #include "haversine_with_noise.txt"
  #else
     #include "haversine_pure.txt"
  #endif
};

Тестирование с большим массивом

У меня был большой файл случайных данных, описывающих форму шума, и я использовал его для проверки инициализации большого массива в NVM. Вот график данных и объявление массива.

Форма шума
Форма шума
const int16_t bigFloatArray[10000] = {
  #include "random10000.csv"
};

Вот начало файла.

Начало файла данных
Начало файла данных

Исходный файл csv не имел запятой после значений. Они были легко добавлены с помощью редактора, который мог использовать регулярные выражения в операциях поиска/замены. В этом случае я использовал выражение для разделителя строк "\r". Искалось «\r», и результат заменялся на ", \r". Одна операция поиска/замены добавила все запятые для 10000 значений.

Все отлично работало и очень быстро компилировалось! Вот начало массива в памяти. Отладчик красиво разбил отображение на группы по 100 элементов в каждой.

Расположение массива в памяти
Расположение массива в памяти

Многомерные массивы

Что если данные организованы в двух или более измерениях? Давайте посмотрим на двумерный массив, объявленный как uint16_t test[2][3]. В C правый индекс (3) представляет собой одномерный массив с элементами, смежными в памяти. Левый индекс (2) означает, что имеется два таких трехэлементных массива. Ниже показано распределение памяти для этих шести элементов:

[0,0] [0,1] [0,2] [1,0] [1,1] [1,2]

Порядок в памяти важен, потому что доступ к последовательным элементам в памяти путем увеличения правого индекса происходит быстрее, чем доступ к элементам путем увеличения левого индекса, который требует «прыжков» через память. Если массив содержит два вектора из 1000 элементов, для самого быстрого доступа он должен быть организован следующим образом test[2][1000].

Ниже показан пример инициализации двумерного массива. Обратите внимание, что инициализаторы сгруппированы дополнительными фигурными скобками, группирующими инициализаторы для одномерных массивов правого индекса.

uint16_t test2DArray[2][3] = {{1,2,3},{4,5,6}};

Этот формат создает проблему для файла данных, который может содержать только цифры, запятые и пробелы. Что произойдет, если дополнительные фигурные скобки опущены?

uint16_t test2DArray[2][3] = {1,2,3,4,5,6};

Компилятор заполняет массив, проходя через инициализаторы слева направо с заполнением сперва правого индекса. Компилятор, который я использую, выдает предупреждение: «отсутствуют скобки вокруг инициализатора». Если количество инициализаторов точно совпадает с количеством элементов в массиве, это не вызывает проблем. Однако, если они не равны, не ясно, как заполнить массив, если нет фигурных скобок, действующих в качестве направляющих.

Массив может быть заполнен из нескольких файлов несколькими операторами #include. Вот пример, где инициализация полностью заключена в пары фигурных скобок. Я опускаю детали, показанные в предыдущих примерах.

uint16_t test2DArray[2][3] = {{
  #include "array_fill_v0.txt"
},{
  #include "array_fill_v1.txt"
}};

Инициализация массивов в объединениях union

Объединение (union) – это переменная, которая может содержать объекты разных типов, которые совместно используют одну и ту же память, а компилятор отслеживает объекты, как если бы они были разными вещами. Такое расположение может быть полезно для встроенного приложения, испытывающего недостаток памяти. Вот пример с vector[6] с одним измерением и matrix[2][3] с двумя измерениями. Это два массива, которые занимают одинаковые места в памяти.

union uray_tag {
  uint16_t vector[6];
  uint16_t matrix[2][3];
} unionArray = {{
  #include "twoDarray_fill.txt"
}};

Правило инициализации объединения – первый объект в объединении (vector[6]) заполняется инициализаторами. Если бы порядок массивов был обратным, компилятор выдал бы предупреждение, потому что инициализаторы не полностью заключены в фигурные скобки. Обратите внимание, что фигурные скобки вокруг #include двойные. Я думаю, что внешний набор включает в себя любые инициализаторы для объединения, а внутренний набор для типа массива.

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

Содержимое файла с данными для инициализации
Содержимое файла с данными для инициализации

Ниже показан массив в памяти. Обратите внимание, что начальное положение vector[] и matrix[][] совпадают.

Расположение массива в памяти
Расположение массива в памяти

Существуют ли другие способы инициализации многомерных массивов из одного файла, состоящего только из цифр, запятых и пробелов? Напишите об этом в комментариях.

Бонусный совет: строки

А как насчет строк? Вот пример инициализации строки.

char testString[] = {"String"};

#include в кавычках не работает. Мой редактор, который знает синтаксис C, дает мне много вопросительных знаков и подчеркиваний. Символы новых строк и сам #include являются инициализаторами! Бедный редактор в замешательстве. Этот бардак компилируется, но строка заполняется символами, которые мы видим в исходном файле, а не из текстового файла данных.

Неправильная инициализация строки из текстового файла
Неправильная инициализация строки из текстового файла

Решение заключается в том, чтобы поместить кавычки в файл.

Содержимое текстового файла для инициализации строки
Содержимое текстового файла для инициализации строки

Затем используем следующий оператор.

char testString[] = {
  #include "string_fill.txt"
};

Обратите внимание, что кавычки вокруг имени файла являются частью синтаксиса #include и не управляют инициализаторами. Ниже показан результат в оперативной памяти.

Результат инициализации строки из файла
Результат инициализации строки из файла

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

Теги

ARMFirmware / прошивкаMCUМассивы в CМикроконтроллерПО встраиваемых системПрограммирование на CРазработка ПО для встраиваемых системФорма сигнала

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.