繁体   English   中英

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

[英]Can i store a pointer to RAM in flash at compile time

我的问题解释:

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

我的尝试:

我使用一个大的uint8 array[2000] (在RAM中),并且数据集的长度以const uint8[] = {1, 5, 9, ...};形式存储在闪存中const uint8[] = {1, 5, 9, ...};

数据集在大数组中的位置是数据集在其之前的累积长度。 因此,我将不得不遍历length数组并将其值相加,然后将其用作大数据数组指针的偏移量。

在运行时,这会给我带来不好的性能。 数据集在大数组中的位置在编译时是已知的,我只是不知道如何将这些信息放入一个数组,编译器可以将该数组存储到闪存中。

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

目标:

这样的东西

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

由于编译器是1 pass编译器,这是否有可能?

(我使用的是Codevision,而不是AVR GCC)

我的解决方案

从技术上讲,纯C解决方案/答案是我问题的正确答案,但从我看来,它似乎过于复杂。 使用构建脚本的想法似乎更好,但是codevision并不是很实用。 所以我最后有点混了。

我写了一个JavaScript,为我写了C代码/变量的定义。 raw-definitions易于编辑,我只需将整个内容复制粘贴到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项,这对我而言是不利的。

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

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

换句话说,如果有数据“串” 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++;
    }
}

上面的函数将返回一个指向第th个index集的长度项的指针(0代表第一个index集,1指向第二个index集,依此类推),如果没有这样的index集,则返回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 ,则源文件通常包含

#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中的缩进始终使用Tab ,但是此论坛不会在代码段中复制它们。 (不过,您始终可以运行sed -e 's|^ *|\\t|g' -i Makefile - 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位指针只能到达闪存的一半,因此,将数据集dataset_len和数据集dataset_ptr数组“靠近”在较低的64k中非常重要。 不过,您的编译器应注意这一点。

为了生成正确的代码以从闪存(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 subset_ptr向编译器表明您希望它们内联,这会稍微增加代码大小,但可能会减少每次调用的几个周期。

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

首先,应该使用PROGMEM将预定义长度数组放入闪存中(如果尚未安装)。

您可以使用预定义的长度数组作为输入来编写脚本,以生成一个包含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支持,则可以将脚本作为Pre-build事件运行。 否则,您将必须记住每次更新偏移量时都要运行脚本。

您提到数据集长度是预定义的,但不是如何定义的-因此,我将假设如何将长度写入代码。

如果按偏移量而不是长度来定义闪存阵列,则应立即获得运行时好处。

随着闪光的长度,我希望你有这样的事情:

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]);
}

由于flash中具有偏移量,因此const数组已从uint8_t变为uint16_t ,这使Flash使用量增加了一倍,外加一个附加元素以加快计算最后一个元素的长度。

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宏可以扩展到的示例。 简短的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 

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

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

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

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

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

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

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

考虑到长度,您应该能够在设计时计算出这些偏移量。 您自己说,只需累加长度即可获得补偿。

通过预先计算的偏移量,代码将不会具有在运行时累加的不良性能。 您只需将数据集的偏移量添加到big array的地址中,即可在运行时找到任何数据集的地址。 array的地址不需要链接时间就可以确定。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM