首页 > 解决方案 > 我可以在编译时将指向 RAM 的指针存储在闪存中吗

问题描述

我的问题解释:

在我的微控制器(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 的条目,这对我来说不利于概览。

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

标签: cembeddedatmel

解决方案


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

换句话说,如果你有数据“字符串”、、、1和,2 3你可以将它们打包成一个 1+1+1+2+1+3+1+4+1 = 15 个字节的数组。4 5 67 8 9 101 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'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()and dataset_pointer(),安全访问器函数,也可以从其他编译单元(C 源文件)访问。)

当通过 a 控制构建时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 中的缩进始终使用Tabs,但本论坛不会在代码片段中重现它们。(不过,您始终可以运行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,因为它是不可变的并且进入闪存/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_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_lenand subset_ptrasstatic inline会向编译器表明您希望它们内联,这会稍微增加代码大小,但每次调用可能会减少几个周期。

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


推荐阅读