Один из распространенных способов упаковки наборов данных переменной длины в один непрерывный массив состоит в использовании одного элемента для описания длины следующей последовательности данных, за которой следует такое количество элементов данных с нулевой длиной, оканчивающей массив.
Другими словами, если у вас есть «строки» данных 1
, 2 3
, 4 5 6
и 7 8 9 10
, вы можете упаковать их в массив 1 + 1 + 1 + 2 + 1 + 3 + 1 + 4 + 1 = 15 байтов как 1
1
2
2 3
3
4 5 6
4
7 8 9 10
0
.
Функции доступа к указанным последовательностям также довольно просты. В случае OP каждый элемент данных представляет собой uint8
:
uint8 dataset[] = { ..., 0 };
Чтобы зациклить каждый набор, вы используете две переменные: одну для смещения текущего набора, а другую для длины:
uint16 offset = 0;
while (1) {
const uint8 length = dataset[offset];
if (!length) {
offset = 0;
break;
} else
++offset;
/* You have 'length' uint8's at dataset+offset. */
/* Skip to next set. */
offset += length;
}
Чтобы найти определенный набор данных, вам нужно найти его с помощью цикла. Например:
uint8 *find_dataset(const uint16 index)
{
uint16 offset = 0;
uint16 count = 0;
while (1) {
const uint8 length = dataset[offset];
if (length == 0)
return NULL;
else
if (count == index)
return dataset + offset;
offset += 1 + length;
count++;
}
}
Вышеприведенная функция вернет указатель на элемент длины index
-го набора (0 относится к первому набору, 1 - ко второму набору и т. Д.) Или NULL, если такого набора нет.
Нетрудно написать функции для удаления, добавления, добавления и добавления новых наборов. (При добавлении и вставке сначала необходимо скопировать остальные элементы в массиве dataset
вперед (к более высоким индексам), сначала на элементы 1+ длины; это означает, что вы не можете получить доступ к массиву в контексте прерывания или из второго ядра, в то время как массив модифицируется.)
Если данные являются неизменяемыми (например, генерируются всякий раз, когда новая микропрограмма загружается в микроконтроллер), и у вас достаточно флэш-памяти, можно использовать отдельный массив для каждого набора, массив указателей на каждый набор и массив размеров каждого набора:
static const uint8 dataset_0[] PROGMEM = { 1 };
static const uint8 dataset_1[] PROGMEM = { 2, 3 };
static const uint8 dataset_2[] PROGMEM = { 4, 5, 6 };
static const uint8 dataset_3[] PROGMEM = { 7, 8, 9, 10 };
#define DATASETS 4
static const uint8 *dataset_ptr[DATASETS] PROGMEM = {
dataset_0,
dataset_1,
dataset_2,
dataset_3,
};
static const uint8 dataset_len[DATASETS] PROGMEM = {
sizeof dataset_0,
sizeof dataset_1,
sizeof dataset_2,
sizeof dataset_3,
};
Когда эти данные генерируются во время компиляции встроенного программного обеспечения, обычно их помещают в отдельный заголовочный файл и просто включают его из основного исходного файла встроенного программного обеспечения .c (или, если встроенное программное обеспечение очень сложно, из конкретного .c исходный файл, который обращается к наборам данных). Если выше dataset.h
, то исходный файл обычно содержит, скажем,
#include "dataset.h"
const uint8 dataset_length(const uint16 index)
{
return (index < DATASETS) ? dataset_len[index] : 0;
}
const uint8 *dataset_pointer_P(const uint16 index)
{
return (index < DATASETS) ? dataset_ptr[index] : NULL;
}
Т.е. он включает в себя набор данных, а затем определяет функции, которые обращаются к данным. (Обратите внимание, что я специально сделал сами данные static
, чтобы они были видны только в текущем модуле компиляции; но функции безопасного доступа dataset_length()
и dataset_pointer()
доступны из других модулей компиляции (исходные файлы C) тоже.)
Когда сборка контролируется с помощью Makefile
, это тривиально. Допустим, сгенерированный файл заголовка dataset.h
, и у вас есть сценарий оболочки, скажем generate-dataset.sh
, который генерирует содержимое для этого заголовка. Тогда рецепт Makefile будет просто
dataset.h: generate-dataset.sh
@$(RM) $@
$(SHELL) -c "$^ > $@"
с рецептами для компиляции исходных файлов C, которые нуждаются в нем, и содержат его в качестве предварительного условия:
main.o: main.c dataset.h
$(CC) $(CFLAGS) -c main.c
Обратите внимание, что для отступа в Makefiles всегда используются Tab s, но этот форум не воспроизводит их в фрагментах кода. (Тем не менее, вы всегда можете запустить sed -e 's|^ *|\t|g' -i Makefile
, чтобы исправить скопированные Makefiles.)
OP упомянул, что они используют Codevision, которая не использует Makefiles (но система конфигурирования на основе меню). Если Codevision не предоставляет ловушку перед сборкой (для запуска исполняемого файла или сценария перед компиляцией исходных файлов), тогда OP может написать сценарий или программу, запущенную на хост-компьютере, возможно, с именем pre-build
, которая восстанавливает все созданные файлы заголовков. и запускайте его вручную перед каждой сборкой.
В гибридном случае, когда вы знаете длину каждого набора данных во время компиляции, и он неизменен (постоянен), но сами наборы меняются во время выполнения, вам нужно использовать вспомогательный скрипт для генерации довольно большого C заголовочный (или исходный) файл. (В нем будет 1500 или более строк, и никто не должен поддерживать это вручную.)
Идея состоит в том, что вы сначала объявляете каждый набор данных, но не инициализируете их. Это заставляет компилятор С резервировать ОЗУ для каждого:
static uint8 dataset_0_0[3];
static uint8 dataset_0_1[2];
static uint8 dataset_0_2[9];
static uint8 dataset_0_3[4];
/* : : */
static uint8 dataset_0_97[1];
static uint8 dataset_0_98[5];
static uint8 dataset_0_99[7];
static uint8 dataset_1_0[6];
static uint8 dataset_1_1[8];
/* : : */
static uint8 dataset_1_98[2];
static uint8 dataset_1_99[3];
static uint8 dataset_2_0[5];
/* : : : */
static uint8 dataset_4_99[9];
Далее объявляем массив, который задает длину каждого набора. Установите эту константу и PROGMEM
, поскольку она неизменна и переходит во flash / rom:
static const uint8 dataset_len[5][100] PROGMEM = {
sizeof dataset_0_0, sizeof dataset_0_1, sizeof dataset_0_2,
/* ... */
sizeof dataset_4_97, sizeof dataset_4_98, sizeof dataset_4_99
};
Вместо операторов sizeof
вы также можете сделать так, чтобы ваш скрипт выводил длины каждого набора в виде десятичного значения.
Наконец, создайте массив указателей на наборы данных. Сам этот массив будет неизменным (const и PROGMEM
), но цели, наборы данных, которые были определены выше, являются изменяемыми:
static uint8 *const dataset_ptr[5][100] PROGMEM = {
dataset_0_0, dataset_0_1, dataset_0_2, dataset_0_3,
/* ... */
dataset_4_96, dataset_4_97, dataset_4_98, dataset_4_99
};
В AT90CAN128 флэш-память находится по адресам 0x0 .. 0x1FFFF (всего 131072 байта). Внутренняя SRAM находится по адресам 0x0100 .. 0x10FF (всего 4096 байт). Как и другие AVR, он использует архитектуру Гарварда, где код находится в отдельном адресном пространстве - во Flash. Он имеет отдельные инструкции для чтения байтов с флэш-памяти (LPM
, ELPM
).
Поскольку 16-разрядный указатель может достигать только половины флэш-памяти, очень важно, чтобы массивы dataset_len
и dataset_ptr
находились "рядом", в нижних 64 КБ. Ваш компилятор должен позаботиться об этом, хотя.
Чтобы сгенерировать правильный код для доступа к массивам из flash (progmem), как минимум, AVR-GCC требуется некоторый вспомогательный код:
#include <avr/pgmspace.h>
uint8 subset_len(const uint8 group, const uint8 set)
{
return pgm_read_byte_near(&(dataset_len[group][set]));
}
uint8 *subset_ptr(const uint8 group, const uint8 set)
{
return (uint8 *)pgm_read_word_near(&(dataset_ptr[group][set]));
}
Код сборки, помеченный счетчиками циклов, который генерирует avr-gcc-4.9.2 для at90can128 и выше, равен
subset_len:
ldi r25, 0 ; 1 cycle
movw r30, r24 ; 1 cycle
lsl r30 ; 1 cycle
rol r31 ; 1 cycle
add r30, r24 ; 1 cycle
adc r31, r25 ; 1 cycle
add r30, r22 ; 1 cycle
adc r31, __zero_reg__ ; 1 cycle
subi r30, lo8(-(dataset_len)) ; 1 cycle
sbci r31, hi8(-(dataset_len)) ; 1 cycle
lpm r24, Z ; 3 cycles
ret
subset_ptr:
ldi r25, 0 ; 1 cycle
movw r30, r24 ; 1 cycle
lsl r30 ; 1 cycle
rol r31 ; 1 cycle
add r30, r24 ; 1 cycle
adc r31, r25 ; 1 cycle
add r30, r22 ; 1 cycle
adc r31, __zero_reg__ ; 1 cycle
lsl r30 ; 1 cycle
rol r31 ; 1 cycle
subi r30, lo8(-(dataset_ptr)) ; 1 cycle
sbci r31, hi8(-(dataset_ptr)) ; 1 cycle
lpm r24, Z+ ; 3 cycles
lpm r25, Z ; 3 cycles
ret
Конечно, объявление subset_len
и subset_ptr
как static inline
будет указывать компилятору, что вы хотите, чтобы они были встроены, что немного увеличивает размер кода, но может сократить пару циклов за вызов.
Обратите внимание, что я проверил вышеупомянутое (кроме использования unsigned char
вместо uint8
) для at90can128 с использованием avr-gcc 4.9.2.