我可以在编译时在闪存中存储指向RAM的指针

时间:2018-05-04 14:53:44

标签: c embedded atmel

我的问题解释了:

在我的微控制器(Atmel AT90CAN128)上,我还剩下大约2500字节的RAM。 在那2500个字节中,我需要存储5次100个数据集(大小可能会在未来发生变化)。数据集具有1到9个字节之间的预定义但变化的长度。纯数据集占用的总字节数约为2000字节。我现在需要能够通过将uint8传递给函数并获取指向数据集的指针来访问数组中的数据集。 但是我只剩下大约500个字节,所以一个指向每个数据集的指针(在运行时开始时计算)根本不可能。

我的尝试:

我使用一个大的uint8 array[2000](在RAM中),数据集的长度作为const uint8[] = {1, 5, 9, ...};存储在flash中。

大数组中数据集的位置是它之前的集合的累积长度。所以我必须迭代长度数组并将值加起来然后用它作为大数据数组指针的偏移量。

在运行时,这给我带来了糟糕的表现。大数组中数据集的位置在编译时是已知的,我只是不知道如何将这些信息放入编译器可以存储到闪存中的数组中。

由于数据集的数量可能会发生变化,我需要一个自动计算位置的解决方案。

目标:

类似的东西

uint8 index = 57; uint8 *pointer_to_data = pointer_array[57];

这是否可行,因为编译器是1通道编译器?

(我使用的是Codevision,而不是avr gcc)

我的解决方案

纯C解决方案/答案在技术上是我的问题的正确答案,但它似乎过于复杂(从我的角度来看)。使用构建脚本的想法看起来更好,但是codevision在这方面不太实用。 所以我最终得到了一些混合。

我写了一个javascript,为我编写变量的C代码/定义。原始定义很容易编辑,我只需将整个内容粘贴到一个html文本文件中,然后在浏览器中打开它并将内容复制粘贴回我的C文件中。

一开始我错过了一个关键元素,那就是定义中'flash'关键字的位置。以下是我的javascript的简化输出,它按照我喜欢的方式编译。

flash uint8 len[150] = {4, 4, 0, 2, ...};

uint8 data1[241] = {0}; //accumulated from above

uint8 * flash pointers_1[150] = {data1 +0, data1 +4, data1 +0, data1 +8, ...};

丑陋的部分(许多没有脚本的手工劳动)正在累加每个指针的长度,因为只有当指针增加一个常量而不是存储在常量数组中的值时,编译器才会编译。

提供给javascript的原始定义如下所示

var strings = [
"len[0] = 4;",
"len[1] = 4;",
"len[3] = 2;",
...

在javascript中它是一个字符串数组,这样我就可以将旧的定义复制到其中并添加一些引号。我只需要定义我想要使用的那些,索引2没有定义,脚本使用长度0,但确实包含它。这个宏需要一个带有0的输入我猜,这在我的情况下对于概述是不好的。

它不是一键式解决方案,但它非常易读且整洁,可以弥补复制粘贴。

4 个答案:

答案 0 :(得分:2)

将可变长度数据集打包到单个连续数组的一种常用方法是使用一个元素来描述下一个数据序列的长度,然后是许多数据项,其长度为零,终止数组。

换句话说,如果你有数据"字符串" 12 34 5 67 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第n组长度项的指针(0表示第一组,1表示第二组,依此类推),如果有,则返回NULL没有这样的设定。

编写要移除,追加,添加和插入新集的函数并不困难。 (在预先插入和插入时,您需要先将dataset数组中的其余元素向前(更高的索引)复制1 +长度元素,这意味着您无法在中断中访问数组当数组被修改时,上下文或来自第二个核心。)

如果数据是不可变的(例如,每当新固件上传到微控制器时都会生成),并且您有足够的闪存/ 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

请注意,Makefile中的缩进始终使用 Tab ,但此论坛不会在代码段中重现它们。 (但是,您始终可以运行sed -e 's|^ *|\t|g' -i Makefile来修复复制粘贴的Makefile。)

OP提到他们使用的是Codevision,它不使用Makefile(而是使用菜单驱动的配置系统)。如果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中。它有单独的指令用于从闪存中读取字节(LPMELPM)。

因为16位指针只能达到闪存的一半,所以{64}中dataset_lendataset_ptr数组在#34;附近是非常重要的。但是,您的编译器应该处理这个问题。

要生成从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_lensubset_ptr声明为static inline会向编译器指示您希望它们内联,这会稍微增加代码大小,但可能会减少几个周期每次调用。

请注意,我已使用avr-gcc 4.9.2对at90can128验证了上述内容(使用unsigned char代替uint8除外)。

答案 1 :(得分:0)

首先,你应该使用PROGMEM将预定义的长度数组放在flash中,如果你还没有。

您可以使用预定义的长度数组作为输入编写脚本,以生成包含PROGMEM数组定义的.c(或cpp)文件。这是python中的一个例子:

# Assume the array that defines the data length is in a file named DataLengthArray.c
# and the array is of the format
# const uint16 dataLengthArray[] PROGMEM = {
#      2, 4, 5, 1, 2, 
#      4 ... };

START_OF_ARRAY = "const uint16 dataLengthArray[] PROGMEM = {"
outFile = open('PointerArray.c', 'w')
with open("DataLengthArray.c") as f:
    fc = f.read().replace('\n', '')
    dataLengthArray=fc[fc.find(START_OF_ARRAY)+len(START_OF_ARRAY):]
    dataLengthArray=dataLengthArray[:dataLengthArray.find("}")]
    offsets = [int(s) for s in dataLengthArray.split(",")]
    outFile.write("extern uint8 array[2000];\n")
    outFile.write("uint8* pointer_array[] PROGMEM = {\n")
    sum = 0
    for offset in offsets:
        outFile.write("array + {}, ".format(sum))
        sum=sum+offset
    outFile.write("};")

哪个会输出PointerArray.c:

extern uint8 array[2000];
uint8* pointer_array[] = {
array + 0, array + 2, array + 6, array + 11, array + 12, array + 14, };

如果您的IDE支持,您可以将脚本作为预构建事件运行。否则,每次更新偏移量时都必须记住运行脚本。

答案 2 :(得分:0)

你提到数据集的长度是预先定义的,但不是它们的定义方式 - 所以我要假设如何将长度写入代码中来获取...

如果您根据偏移量而不是长度来定义闪存阵列,则应立即获得运行时优势。

闪光灯的长度,我希望你有这样的东西:

const uint8_t lengths[] = {1, 5, 9, ...};

uint8_t get_data_set_length(uint16_t index)
{
    return lengths[index];
}
uint8_t * get_data_set_pointer(uint16_t index)
{
    uint16_t offset = 0;
    uint16_t i = 0;
    for ( i = 0; i < index; ++i )
    {
        offset += lengths[index];
    }
    return &(array[offset]);
}

对于闪存中的偏移量,const数组已从uint8_t变为uint16_t,这使闪存的使用量加倍,另外还有一个元素可以加速计算最后一个元素的长度。

const uint16_t offsets[] = {0, 1, 6, 15, ..., /* last offset + last length */ };

uint8_t get_data_set_length(uint16_t index)
{
    return offsets[index+1] - offsets[index];
}
uint8_t * get_data_set_pointer(uint16_t index)
{
    uint16_t offset = offsets[index];
    return &(array[offset]);
}

如果你无法承受额外的闪存,你也可以通过为所有元素的长度和一小部分索引的偏移量组合两者,例如下面例子中的每16个元素,交易运行 - 时间成本与闪存成本。

uint8_t get_data_set_length(uint16_t index)
{
    return lengths[index];
}
uint8_t * get_data_set_pointer(uint16_t index)
{
    uint16_t i;
    uint16_t offset = offsets[index / 16];
    for ( i = index & 0xFFF0u; i < index; ++i )
    {
        offset += lengths[index];
    }
    return &(array[offset]);
}

为简化编码,您可以考虑使用x-macros,例如

#define DATA_SET_X_MACRO(data_set_expansion) \
  data_set_expansion( A, 1 ) \
  data_set_expansion( B, 5 ) \
  data_set_expansion( C, 9 )

uint8_t array[2000];
#define count_struct(tag,len) uint8_t tag;
#define offset_struct(tag,len) uint8_t tag[len];
#define offset_array(tag,len) (uint16_t)(offsetof(data_set_offset_struct,tag)),
#define length_array(tag,len) len,
#define pointer_array(tag,len) (&(array[offsetof(data_set_offset_struct,tag)])),

typedef struct
{
    DATA_SET_X_MACRO(count_struct)
}   data_set_count_struct;

typedef struct
{
    DATA_SET_X_MACRO(offset_struct)
}   data_set_offset_struct;

const uint16_t offsets[] = 
{
    DATA_SET_X_MACRO(offset_array)
};

const uint16_t lengths[] = 
{
    DATA_SET_X_MACRO(length_array)
};

uint8_t * const pointers[] = 
{
    DATA_SET_X_MACRO(pointer_array)
};

预处理器将其转换为:

typedef struct
{
    uint8_t A;
    uint8_t B;
    uint8_t C;
}   data_set_count_struct;

typedef struct
{
    uint8_t A[1];
    uint8_t B[5];
    uint8_t C[9];
}   data_set_offset_struct;

typedef struct
{
    uint8_t A[1];
    uint8_t B[5];
    uint8_t C[9];
}   data_set_offset_struct;

const uint16_t offsets[] = { 0,1,6, };

const uint16_t lengths[] = { 1,5,9, };

uint8_t * const pointers[] = 
{
    array+0,
    array+1,
    array+6,
};

这只是展示了x-macro可以扩展到的示例。简短main()可以显示这些内容:

int main()
{
    printf("There are %d individual data sets\n", (int)sizeof(data_set_count_struct) );
    printf("The total size of the data sets is %d\n", (int)sizeof(data_set_offset_struct) );
    printf("The data array base address is  %x\n", array );
    int i;
    for ( i = 0; i < sizeof(data_set_count_struct); ++i )
    {
        printf( "elem %d: %d bytes at offset %d, or address %x\n", i, lengths[i], offsets[i], pointers[i]);
    }

    return 0;
}

使用样本输出

There are 3 individual data sets
The total size of the data sets is 15
The data array base address is  601060
elem 0: 1 bytes at offset 0, or address 601060
elem 1: 5 bytes at offset 1, or address 601061
elem 2: 9 bytes at offset 6, or address 601066 

以上要求您提供一个标签&#39; - 每个数据集的有效C标识符,但如果您有500个这样的标识符,那么将每个长度与描述符配对可能并不是件坏事。有了这么多数据,我还建议使用x-macro的包含文件,而不是#define,特别是如果数据集定义可以导出到其他地方。

这种方法的好处是您可以在一个地方定义数据集,并且所有内容都是从这个定义生成的。如果重新排序定义或添加定义,则将在编译时生成数组。它也纯粹使用编译器工具链,特别是预处理器,但是不需要编写外部脚本或在预构建脚本中挂钩。

答案 3 :(得分:-1)

您说要存储每个数据集的地址,但如果存储每个数据集的偏移量,似乎会更简单。存储偏移而不是地址意味着您不需要在编译时知道大array的地址。

现在你有一个包含每个数据集的长度的常量数组。

const uint8_t data_set_lengths[] = { 1, 5, 9...};

只需将其更改为包含大数组中每个数据集的偏移量的常量数组。

const uint8_t data_set_offsets[] = { 0, 1, 6, 15, ...};

如果您已经知道长度,您应该能够在设计时计算这些偏移量。你说自己,只是累积长度以获得补偿。

在预先计算偏移量的情况下,代码不会在运行时累积不良性能。您只需将数据集的偏移量添加到大array的地址即可在运行时找到任何数据集的地址。并且在链接时间之前不需要解决大array的地址。