将可变长度数据集打包到单个连续数组的一种常用方法是使用一个元素来描述下一个数据序列的长度,然后是那么多数据项,以零长度终止数组。
换句话说,如果您有数据“字符串”1、2 3、4 5 6 和 7 8 9 10,则可以将它们打包成 1+1+1+2+1+3 的数组+1+4+1 = 15 个字节为1122 334 5 647 8 9 100。
访问所述序列的函数也非常简单。在 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'th 集合的长度项的指针(0 表示第一个集合,1 表示第二个集合,依此类推),如果没有这样的集合,则返回 NULL。
编写删除、追加、前置和插入新集合的函数并不难。 (在添加和插入时,您确实需要先将 dataset 数组中的其余元素向前复制 1+length 个元素(到更高的索引);这意味着您无法在中断上下文中访问数组或来自第二个核心,同时正在修改阵列。)
如果数据是不可变的(例如,每当将新固件上传到微控制器时都会生成),并且您有足够的可用闪存/ROM,则可以为每组使用单独的数组,即指向每组的指针数组,以及每个集合的大小数组:
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,那么源文件一般包含say
#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,并且您有一个shell 脚本,比如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 中的缩进总是使用 Tabs,但本论坛不会在代码 sn-ps 中重现它们。 (不过,您始终可以运行 sed -e 's|^ *|\t|g' -i Makefile 来修复复制粘贴的 Makefile。)
OP 提到他们正在使用 Codevision,它不使用 Makefiles(而是一个菜单驱动的配置系统)。如果 Codevision 不提供预构建挂钩(在编译源文件之前运行可执行文件或脚本),则 OP 可以编写在主机上运行的脚本或程序,可能命名为 pre-build,重新生成所有生成的头文件,并在每次构建之前手动运行它。
在混合情况下,您在编译时知道每个数据集的长度,并且它是不可变的(恒定的),但集合本身在运行时会发生变化,您需要使用帮助脚本生成一个相当大的C 头文件(或源文件)。 (它将有 1500 行或更多行,没有人应该手动维护。)
这个想法是你首先声明每个数据集,但不要初始化它们。这使得 C 编译器为每个保留 RAM:
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 数组在较低的 64k 中“接近”是相当重要的。不过,您的编译器应该处理好这一点。
要生成正确的代码以从闪存(程序)访问数组,至少 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 会向编译器表明您希望它们内联,这会稍微增加代码大小,但每次调用可能会减少几个周期。
请注意,我已经使用 avr-gcc 4.9.2 对 at90can128 进行了上述验证(除了使用 unsigned char 而不是 uint8)。