0
votes

My problem explained:

On my microcontroller (Atmel AT90CAN128) i have about 2500 bytes of RAM left. In those 2500 bytes i need to store 5 times 100 data sets (size could change in the future). The data sets have a predefined but varying length between 1 and 9 bytes. The total bytes that the pure data sets occupy is about 2000 bytes. I now need to be able to access the data sets in an array like fashion by passing a uint8 to a function and get a pointer to the data set in return. But i only have about 500 bytes left, so an array with pointers to each data set (calculated at start of run time) is simply not possible.

My attempt:

i use one big uint8 array[2000] (in RAM) and the length of the data sets is stored in flash as const uint8[] = {1, 5, 9, ...};.

The position of the data set in the big array is the accumulated length of the sets before it. So i would have to iterate through the length array and add the values up and then use it as an offset to the pointer of the big data array.

At runtime this gives me bad performance. The position of the data sets within the big array IS KNOWN at compile time, I just dont know how to put this information into an array that the compiler can store into flash.

As the amount of data sets could change, i need a solution that automatically calculates the positions.

Goal:

something like that

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

Is this even possible, as the compiler is a 1 pass comiler ?

(I am using Codevision, not avr gcc)

My solution

The pure C solution/answer is technically the right answer for my question but it just seems overly complicated (from my perspective). The idea with the build script seemed better but codevision is not very practical in that way. So i ended up with a bit of a mix.

I wrote a javascript that writes the C code/definition of the variables for me. The raw-definitions are easy to edit and i just copy paste the whole thing into a html text file and open it in a browser and copy paste the content back into my C file.

In the beginning i was missing a crucial element and that is the position of the 'flash' keyword in the definition. The following is a simplified output of my javascript that compiles just the way i like it.

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, ...};

The ugly part (lots of manual labor without script) is adding up the length for each pointer as the compiler will only compile if the pointer is increased by a constant and not a value stored in a constant array.

The raw definitions that are fed to the javascript then look like this

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

Within the javascript it is an array of strings, this way i could copy my old definitions into it and just add some quotes. I only need to define the ones that i want to use, index 2 is not defined and the script uses length 0 for it but does include it. The macro would have needed an entry with 0 i guess, which is bad for overview in my case.

It is not a one click solution but it is very readable and tidy which makes up for the copy-paste.

4
Describe your problem from the beginning and sequentially. It's impossible to understand your description.Eugene Sh.
i simplified itNikkyD
this is about code running on a microcontroller, there is no operating systemNikkyD
@NikkyD You should clarify your question, first what is the microchip reference you use... secondly, post an minimal reproducible example of your logic with your two actual array, I'm sure you are doing something strange.Stargateur
If it's an AVR, use progmemuser2371524

4 Answers

2
votes

One common method of packing variable-length data sets to a single continuous array is using one element to describe the length of the next data sequence, followed by that many data items, with a zero length terminating the array.

In other words, if you have data "strings" 1, 2 3, 4 5 6, and 7 8 9 10, you can pack them into an array of 1+1+1+2+1+3+1+4+1 = 15 bytes as 1 1 2 2 3 3 4 5 6 4 7 8 9 10 0.

The functions to access said sequences are quite simple, too. In OP's case, each data item is an uint8:

uint8  dataset[] = { ..., 0 };

To loop over each set, you use two variables: one for the offset of current set, and another for the length:

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

To find a specific dataset, you do need to find it using a loop. For example:

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++;
    }
}

The above function will return a pointer to the length item of the index'th set (0 referring to the first set, 1 to the second set, and so on), or NULL if there is no such set.

It is not difficult to write functions to remove, append, prepend, and insert new sets. (When prepending and inserting, you do need to copy the rest of the elements in the dataset array forward (to higher indexes), by 1+length elements, first; this means that you cannot access the array in an interrupt context or from a second core, while the array is being modified.)


If the data is immutable (for example, generated whenever a new firmware is uploaded to the microcontroller), and you have sufficient flash/rom available, you can use a separate array for each set, an array of pointers to each set, and an array of sizes of each set:

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,
};

When this data is generated at firmware compile time, it is common to put this into a separate header file, and simply include it from the main firmware .c source file (or, if the firmware is very complicated, from the specific .c source file that accesses the data sets). If the above is dataset.h, then the source file typically contains 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;
}

i.e., it includes the dataset, and then defines the functions that access the data. (Note that I deliberately made the data itself static, so they are only visible in the current compilation unit; but the dataset_length() and dataset_pointer(), the safe accessor functions, are accessible from other compilation units (C source files), too.)

When the build is controlled via a Makefile, this is trivial. Let's say the generated header file is dataset.h, and you have a shell script, say generate-dataset.sh, that generates the contents for that header. Then, the Makefile recipe is simply

dataset.h: generate-dataset.sh
    @$(RM) $@
    $(SHELL) -c "$^ > $@"

with the recipes for the compilation of the C source files that need it, containing it as a prerequisite:

main.o: main.c dataset.h
    $(CC) $(CFLAGS) -c main.c

Do note that the indentation in Makefiles always uses Tabs, but this forum does not reproduce them in code snippets. (You can always run sed -e 's|^ *|\t|g' -i Makefile to fix copy-pasted Makefiles, though.)

OP mentioned that they are using Codevision, that does not use Makefiles (but a menu-driven configuration system). If Codevision does not provide a pre-build hook (to run an executable or script before compiling the source files), then OP can write a script or program run on the host machine, perhaps named pre-build, that regenerates all generated header files, and run it by hand before every build.


In the hybrid case, where you know the length of each data set at compile time, and it is immutable (constant), but the sets themselves vary at run time, you need to use a helper script to generate a rather large C header (or source) file. (It will have 1500 lines or more, and nobody should have to maintain that by hand.)

The idea is that you first declare each data set, but do not initialize them. This makes the C compiler reserve RAM for each:

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

Next, declare an array that specifies the length of each set. Make this constant and PROGMEM, since it is immutable and goes into 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
};

Instead of the sizeof statements, you can also have your script output the lengths of each set as a decimal value.

Finally, create an array of pointers to the datasets. This array itself will be immutable (const and PROGMEM), but the targets, the datasets defined first above, are mutable:

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

On AT90CAN128, the flash memory is at addresses 0x0 .. 0x1FFFF (131072 bytes total). Internal SRAM is at addresses 0x0100 .. 0x10FF (4096 bytes total). Like other AVRs, it uses Harvard architecture, where code resides in a separate address space -- in Flash. It has separate instructions for reading bytes from flash (LPM, ELPM).

Because a 16-bit pointer can only reach half the flash, it is rather important that the dataset_len and dataset_ptr arrays are "near", in the lower 64k. Your compiler should take care of this, though.

To generate correct code for accessing the arrays from flash (progmem), at least AVR-GCC needs some helper code:

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

The assembly code, annotated with the cycle counts, avr-gcc-4.9.2 generates for at90can128 from above, is

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

Of course, declaring subset_len and subset_ptr as static inline would indicate to the compiler you want them inlined, which increases the code size a bit, but might shave off a couple of cycles per invocation.

Note that I have verified the above (except using unsigned char instead of uint8) for at90can128 using avr-gcc 4.9.2.

0
votes

First, you should put the predefined length array in flash using PROGMEM, if you haven't already.

You could write a script, using the predefined length array as input, to generate a .c (or cpp) file that contains the PROGMEM array definition. Here is an example in 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("};")

Which would output PointerArray.c:

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

You could run the script as a Pre-build event, if your IDE supports it. Otherwise you will have to remember to run the script every time you update the offsets.

0
votes

You mention that the data set lengths are pre-defined, but not how they are defined - so I'm going to make the assumption of how the lengths are written into code is up for grabs..

If you define your flash array in terms of offsets instead of lengths, you should immediately get a run-time benefit.

With lengths in flash, I expect you have something like this:

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

With offsets in flash, the const array has gone from uint8_t to uint16_t, which doubles the flash usage, plus an additional element to be speed up calculating the length of the last element.

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

If you can't afford that extra flash memory, ou could also combine the two by having the lengths for all elements and offsets for a fraction of the indices, e.g every 16 element in the example below, trading off run-time cost vs flash memory cost.

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

To simplify the encoding, you can consider using x-macros, e.g.

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

The preprocessor turns that into:

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,
};

This just shows an example of what the x-macro can expand to. A short main() can show these in action:

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

With sample output

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 

The above require you to give a 'tag' - a valid C identifier for each data set, but if you have 500 of these, pairing each length with a descriptor is probably not a bad thing. With that amount of data, I would also recommend using an include file for the x-macro, rather than a #define, in particular if the data set definitions can be exported somewhere else.

The benefit of this approach is that you have the data sets defined in one place, and everything is generated from this one definition. If you re-order the definition, or add to it, the arrays will be generated at compile-time. It is also purely using the compiler toolchain, in particular the pre-processor, but there's no need for writing external scripts or hooking in pre-build scripts.

-1
votes

You said that you want to store the address of each data set but it seems like it would be much simpler if you store the offset of each data set. Storing the offsets instead of the addresses means that you don't need to know the address of big array at compile time.

Right now you have an array of constants containing the length of each data set.

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

Just change that to be an array of constants containing the offset of each data set in the big array.

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

You should be able to calculate these offsets at design time given that you already know the lengths. You said yourself, just accumulate the lengths to get the offsets.

With the offsets precalculated the code won't have the bad performance of accumulating at run time. And you can find the address of any data set at run time simply by adding the data set's offset to the address of the big array. And the address of big array doesn't need to be settled until link time.