Как инициализировать массивы с помощью форм сигналов и других файловых данных. Советы и рекомендации по разработке прошивок
В данной статье показано, как инициализировать массивы в программе на языке 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
и не управляют инициализаторами. Ниже показан результат в оперативной памяти.
Важно отметить, что примеры теоретически должны работать с любым компилятором. Однако некоторые примеры могут быть необычными и могут вызывать проблемы с некоторыми компиляторами. Если обнаружите проблему, напишите об этом в комментариях.