【问题标题】:Can i store a pointer to RAM in flash at compile time我可以在编译时将指向 RAM 的指针存储在闪存中吗
【发布时间】:2018-05-04 14:53:43
【问题描述】:

我的问题解释:

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

我的尝试:

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

数据集在大数组中的位置是它之前的集合的累加长度。所以我必须遍历长度数组并将值相加,然后将其用作大数据数组指针的偏移量。

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

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

目标:

类似的东西

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

这是否可能,因为编译器是 1 pass 编译器?

(我使用的是 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 的条目,在我的情况下这不利于概览。

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

【问题讨论】:

  • 从头到尾描述您的问题。无法理解您的描述。
  • 我把它简化了
  • 这是关于在微控制器上运行的代码,没有操作系统
  • @NikkyD 你应该澄清你的问题,首先你使用的微芯片参考是什么......其次,用你的两个实际数组发布你的逻辑的minimal reproducible example,我确定你在做奇怪的东西。
  • 如果是AVR,请使用progmem

标签: c embedded atmel


【解决方案1】:

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

换句话说,如果您有数据“字符串”12 34 5 67 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 中。它具有从闪存读取字节的单独指令(LPMELPM)。

因为 16 位指针只能到达闪存的一半,所以 dataset_lendataset_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_lensubset_ptr 声明为 static inline 会向编译器表明您希望它们内联,这会稍微增加代码大小,但每次调用可能会减少几个周期。

请注意,我已经使用 avr-gcc 4.9.2 对 at90can128 进行了上述验证(除了使用 unsigned char 而不是 uint8)。

【讨论】:

  • OP 声明代码在 RUN 时生成数据集中的值,因此不能用于预编程到头文件等我的建议,将闪存编程为每个数据集计算并使所有数据集长度相同,因此可以视为表格
  • @user3629249:请注意,OP 已经在 cmets 中声明了 “数据是可变的”“数据是恒定的”问题。我的回答试图回答两者。至于使用闪存作为持久内存,那将是第三种方法。如果仅在数据更改时执行(因此闪存中会有某种生成计数器或校验和),而不是每次开机,我相信这也是一种非常可行的方法。不过,我个人需要更多地了解 OP 的硬件,才能提出任何实用的建议。
  • “数据集”在运行时会发生很多变化,这就是为什么我希望快速访问它们而无需迭代。最好是一个指针数组,但这个数组应该在闪存中,我不知道如何实现。
  • @NikkyD:我已将混合案例添加到答案中。如果使用 avr-gcc 编译它可以正常工作(我使用较小的集合进行了测试,只有 3 组 3 组),但我不确定它是否需要更改才能使用 CodeVision 进行编译。如果我知道您在编译时如何知道各个集合的长度,我可以帮助您编写代码生成器(使用awkbash)。如果长度形成一个整数序列,或者是可计算的,那么执行该运行时间可能会更快。我只是不知道,因为你没有告诉我们。 (考虑在答案中添加数字细节。)
【解决方案2】:

首先,您应该使用 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 支持,您可以将脚本作为预构建事件运行。否则,您必须记住每次更新偏移量时都运行脚本。

【讨论】:

  • 不确定 codevision 是否可以以这种方式运行脚本
  • 我对codevision不熟悉。 Atmel Studio 7 具有构建前和构建后事件。
【解决方案3】:

您提到数据集的长度是预定义的,但不是它们是如何定义的 - 所以我将假设如何将长度写入代码中......

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

对于 Flash 中的长度,我希望你有这样的东西:

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 

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

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

【讨论】:

  • 我有 100k 的闪存,我不想要在运行时进行许多计算。我想要预定义的地址,因为所有信息都在运行时可用。
  • 我相信你的意思是'...在编译时可用'。我的答案的 X-Macro 部分就是这样做的。第一个扩展定义了一个结构体,该结构体具有多个字节数组成员,其大小为已定义(示例中为 1,5,9)。这个结构没有被实例化,它只是作为一种类型存在。第二个扩展使用第一个扩展定义的结构创建一个数组,其中包含每个数据集的偏移量。第三个扩展创建一个具有长度的数组。我将编辑我的答案以对此进行扩展。
  • @NikkyD 我刚刚赶上了您在其他问题中添加的一些 cmets。将偏移量更改为指针也应该是微不足道的
【解决方案4】:

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

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

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

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

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

鉴于您已经知道长度,您应该能够在设计时计算这些偏移量。你自己说的,只是累加长度来得到偏移量。

使用预先计算的偏移量,代码不会在运行时出现累积的不良性能。你可以在运行时找到任何数据集的地址,只需将数据集的偏移量添加到大array的地址即可。并且大array的地址直到链接时间才需要结算。

【讨论】:

  • 存储偏移量将是B计划的解决方案,但我这里的问题是如何实现A计划,这是一个指针数组。该信息在编译时可用,问题是我如何将其输入编译器。
  • @NikkyD 编译时怎么知道array的地址?
  • 我不知道,但编译器必须知道。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2023-04-09
  • 1970-01-01
  • 2016-05-28
  • 1970-01-01
  • 2014-02-19
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多