NerdKits - electronics education for a digital generation

You are not logged in. [log in]

NEW: Learning electronics? Ask your questions on the new Electronics Questions & Answers site hosted by CircuitLab.

Microcontroller Programming » Putting Data in Program Space, and using it...

December 28, 2011
by JimFrederickson
JimFrederickson's Avatar


I am not a 'c programmer' so there may very well be some problems with some of this.

All of this here I have had, or do have, working in some application. (Not the same names though...)

But since this is 'not a second language for me' I may have 'trascribed some things incorrectly', or I may have 'done some things that aren't considered entirely correct'.

Mostly, I attempt to keep the access to the 'Program Space' as similar in my eyes, to what I would do in 'RAM'.

Feel free to 'correct', 'off other solutions', or even 'poke-fun' of what is here.

None-the-less I think this could give a few valid hints, and some helpful information to some.



You have this 'New Microcontroller' that you are programming and since you want to save some 'RAM Space' you decide to put as much as you can in 'Program Space'...

Sounds Simple!

If turns out it really isn't all that simple...

So what's the deal!

Okay, that's not entirely true. It is 'pretty simple', but you have find the proper information and weed out or trudge through the stuff that is just wrong. (Well, at the very least, the things that do not work well for me!)

So this is only about what I found, which may or may not be useful to people here. It may also be a 'rehash' of what many people here already know as well.

The truth of the matter is though, I wasn't able to find any single place that had all of this information in a form usable to me, so there may others in that boat as well.

This, is for... those others...

For me there are really only two important things to be able to use programming information.

1 - How to define the necessary data. 2 - How to write the code to access the data.

Additionally, I will keep the code as close to 'simple c' as possible. (Okay my terms...)
I will only use 'minimal ptr references and ptr math'. (Albeit keeping in mind that much of what is done does translate to 'ptr references'. That is, ultimately, the only way to access data.)

In any c source from which you intend to store Data in Program Space you will need to use the following statement:

#include <avr/pgmspace.h>

This statement simply provides the Macros, definitions, and asorted code necessary for manipulating Data into the Program Space as well as retreiving it.

After that it is just a matter and 'Defining and Coding'...

Pretty much everyone understands how to 'define and code' the most common, in Nerdkit Code, use of Program Space for Data Storage.

Simply:

lcd_write_string(PSTR("ADC: "));

This use is pretty much 'all encompassing'.

The Data to be put into Program Space, ("ADC: "), is Defined and it's use Coded all at once. (Do note as well that 'PSTR' is a Macro that formats the string appropriately for Program Space and returns the proper reference. Additionally 'lcd_write_string' is expecting a reference to Program Space as well.

Putting other, hopefully more usable stuff, into the Program Space can be a little more difficult.

Basic Program Space References; (mostly this stuff is pretty easy to find)

Data Definitions:

uint8_t     xfoo PROGMEM =      1;
uint16_t    yfoo PROGMEM =      1200;

uint16_t    resultfoo;

char stringfoo[] PROGMEM = "TEST";

Code:
resultfoo = 2 * pgm_read_byte(&(xfoo));
resultfoo = resultfoo + pgm_read_word(&(yfoo));

Intermediate Program Space References:

Data:
uint8_t     itemp;
uint16_t    itemp16;

typedef struct {
    uint8_t     x;
    uint16_t    y;
    char        strname[20];
    } STRUCT_TESTFOO;

STRUCT_TESTFOO testfoo[] PROGMEM = {
    {0,     12,     "test1"},
    {1,     1200,   "test2"},
    {2,     12000,  "test3"},
    {255,   0,      "end"}
    };

Code:
itemp = pgm_read_byte(&(testfoo[0].x));

itemp16 = pgm_read_word(&(testfoo[2].y));

itemp = pgm_read_byte(&(testfoo[0].strname[1]));

mylcd_writestring(&(testfoo[0].strname[1]));

Not that works out well, but there is something that is really not as efficient as you may want.!

Line 1 has a 'string' that is 5 bytes long, with a 0 terminater that makes 6 bytes.

Since we have defined 20 bytes fo the 'char array' we are WASTING 14 bytes for each of our declarations.

For 'simple things' I don't usually care. I like the 'ease of use.

Sometimes though, for a variety of reasons, I do care about the 'wasted space'.

So we have to change our format a little.

We can still use our 'structure', we need to create a 'list of pointers to each string we want to incorporate into the structure'.

Not a 'horrible issue', but still something I try to avoid if i can.

Data:

typedef struct {
    uint8_t     x;
    uint16_t    y;
    PGM_P       strname;
    } STRUCT_TESTFOO2;

char testfoo2_strname1[] PROGMEM = "testend-1";
char testfoo2_strname2[] PROGMEM = "test-2";
char testfoo2_strname3[] PROGMEM = "end-3";

STRUCT_TESTFOO2 testfoo2[] PROGMEM = {
    {1,     100,    &testfoo2_strname1[0]},
    {2,     1200,   &testfoo2_strname2[0]},
    {255,   13000,  &testfoo2_strname3[0]}
    };

So now you have to access the 'pointers' to get the 'entry for the string' then you are okay.

    itemp = pgm_read_byte(&(testfoo2[0].x));

    itemp16 = pgm_read_word(&(testfoo2[2].y));

//  read the pointer to the string
    itemp16 = pgm_read_word(&(testfoo2[1].strname));

//  now we can use that pointer to write a string
    mylcd_writestring((PGM_P) itemp16);

    Data:
typedef struct {
    uint8_t     x;
    uint16_t    y;
    char        str[10];
    void        (*codeptr) (void); //   code pointer
    } STRUCT_TESTFOO3;

STRUCT_TESTFOO3 testfoo[] PROGMEM = {
    {0,     12,     "test1",    usertask_test1},
    {1,     1200,   "test2",    usertask_test2},
    {2,     12000,  "test3",    usertask_test3}
    };

Code:
void (*icodeptr)();

icodeptr = (void (*) ()) pgm_read_word(&(testfoo[3].codeptr));
(icodeptr) ();

That works to call a function.

But now let's say you want to interpret the pointers.

(Maybe you read the return address from the stack to see where a call came from, or maybe you have a dmacro you put in front of each function so that you can set a switch and put the last function executed into a var...

Anyway there will be 'reasons' you may want to interpret a pointer to code.)

So let's say you are reviewing your pointers to code in the structure and you find...

The following:

usertask_blinky 1c34 usertask_tmps_display 1e26 usertask_log_write 0d16

You think all is good. I have a structure with pointers to code, and I can call functions.

Print/display a pointer to code and look it up in the symbols in the assember listing

usertask_blinky 0e1a in the actual call? usertask_tmps_display of13 in the actual call? usertask_log_write o6b8 in the actual call?

So now you are REALLY CONFUSED!

Symbol Table:

00001c34 g     F .text  00000030 usertask_blinky
00001e26 g     F .text  00000086 usertask_tmps_display
00000d16 g     F .text  00000032 usertask_log_write

Assembler:

void usertask_blinky () {
    static char iblinkychar = ' ';

    if (mytasks_flags & TASKFLAGS_STARTING) {
    1c34:   80 91 64 01     lds r24, 0x0164

void usertask_tmps_display(uint8_t iadcselected) {
    1e26:   ef 92           push    r14
    1e28:   ff 92           push    r15
    1e2a:   0f 93           push    r16

void usertask_log_write(uint16_t idata) {
    mylog.buffer[mylog.indexnext] = idata;
     d16:   20 91 71 01     lds r18, 0x0171
     d1a:   e2 2f           mov r30, r18
     d1c:   f0 e0           ldi r31, 0x00   ; 0

Everything seems to work, but you can't cross reference what seems to be running with the assembler listing.

Problem!

Note really. You are running into an 'interpretation problem'. (Or I suppose it could be considered an 'implementation problem' as well...)

usertask_blinky 1c34 DOESN'T MATCH 0e1a in the actual call? usertask_tmps_display 1e26 DOESN'T MATCH of13 in the actual call? usertask_log_write 0d16 DOESN'T MATCH o6b8 in the actual call?

If you put those pointers into an array of 'code pointers' it no longer makes sense...

Let's look at them as decimal since I do decimal match easier... (Well unless it's dividing by some multiple of 16)

7220 doesn't match 3610 7718 doesn't match 3859 3350 doesn't match 1675

But wait... Bigger to smaller, even to odd or even? (That is, at least, a hint...)

They are really the same...

3610 x 2 = 7220 3859 x 2 = 7718 1675 x 2 = 3350!!!!

See they match!

WHY!

The compiler/linker views the 'pointers to code in program space' as a specific series of 'bytes' so that is what is seen in the symbol table.

When you look at the pointer values used for calls the value doesn't seem to match because calls on the AVR CPU views the 'same program space' as a 'series of words for the destinations for pointers to code'.

So the pointer is actually 1/2 of the value shown in the symbol table.

So for me, if I am debugging errors messages that I use the show the functions program pointer I multiply by 2 so that I can search for the symbols easier.

A practical use of of a the 'Program Space' array...

The following function show two different approaches to a specific function.

Yes, some things could be optimized, but the 2 approaches are quite similar in what they do and that was a bit of a goal.

Converting a 16-bit unsigned integer to a hex value.

1 - the first uses math to 'convert the nibbles' into 'characters to be output. 2 = the second uses a 'lookup table' to convert the 'nibbles' into 'characters' to be output.

Compiled the first approach uses 56 instruction words, vs the 26 instruction words, of the second approach.

NOTE: The second approach needs to add in 9 words that are necessary to hold the 'char array'. So 35 instruction words.

The second is MUCH EASIER to read as well.

So putting data for lookups in 'Program Space' can make things smaller and more readable.

26 Instructions (words)...
35 Instructions including the array (wordds)...

56 Instructions (words)...

void mylcd_writehex16(uint16_t ihex) {
    uint8_t itemp;

    itemp = ihex / 256;

    if ((itemp / 16) > 9) {
        mylcd_writechar((itemp / 16) + 'A' - 10);
        }
    else {
        mylcd_writechar((itemp / 16) + '0');
        }

    if ((itemp & 15) > 9) {
        mylcd_writechar((itemp & 15) + 'A' - 10);
        }
    else {
        mylcd_writechar((itemp & 15) + '0');
        }

    itemp = ihex & 255;

    if ((itemp / 16) > 9) {
        mylcd_writechar((itemp / 16) + 'A' - 10);
        }
    else {
        mylcd_writechar((itemp / 16) + '0');
        }

    if ((itemp & 15) > 9) {
        mylcd_writechar((itemp & 15) + 'A' - 10);
        }
    else {
        mylcd_writechar((itemp & 15) + '0');
        }
    }

char strtohex[] PROGMEM = "0123456789ABCDEF";

void mylcd_writehex16(uint16_t ihex) {
    uint8_t itemp;

    itemp = ihex / 256;

    mylcd_writechar(pgm_read_byte(&strtohex[itemp / 16]));
    mylcd_writechar(pgm_read_byte(&strtohex[itemp & 15]));

    itemp = ihex & 255;

    mylcd_writechar(pgm_read_byte(&strtohex[itemp / 16]));
    mylcd_writechar(pgm_read_byte(&strtohex[itemp & 15]));
    }

Lookup tables, similar to the above, are great for getting results easily as well as for avoiding complex reoccurring math.

April 29, 2012
by pcbolt
pcbolt's Avatar

Jim -

In the "better late than never" department, I wanted to thank you for the above post. I'm working with a Real Time Clock module and wanted a sleek way to translate month numbers into text (same with the days of the week). I took your lead here and came up with the following...

char weekdays[] PROGMEM = "Sun\0Mon\0Tue\0Wed\0Thu\0Fri\0Sat\0";
char months[] PROGMEM = "Jan\0Feb\0Mar\0Apr\0May\0Jun\0Jul\0Sep\0Oct\0Nov\0Dec\0";

You'll notice I added the zero termination character ("\0") between text entries so I can streamline sending pointers directly to the NK LCD library functions. Writing out the text is as easy as...

lcd_write_string(&(months[(month_index - 1) * 4]));

I plan to implement the same strategy for a custom menu screen. Thanks again.

April 29, 2012
by Ralphxyz
Ralphxyz's Avatar

re:

The truth of the matter is though, I wasn't able to find any single place that had all of this information in a form usable to me,

This should be added to the Nerdkit Community Library. It seems a bit jumpy to me but I understood some (most) of it.

Posting it here in the forum, thank you very much, it will drift into oblivion unless someone knows it is here or chances upon it in a search.

The Library makes it a ready reference!!

Ralph

April 29, 2012
by JimFrederickson
JimFrederickson's Avatar

PCBolt,

Thanks.

I am happy that you found that helpful.

I try, whenever possible, to avoid doing pointer math. (Just because I find it can get convoluted as to what is happening, at least for me.)

Getting things out of the AVR RAM, and being able to access tables and constants in the Program Space and freeing up Data Space is quite helpful. (In fact, otherwise you can get a "double whammy"! Initialization Data may get stored in Program Space along with Initialization Code, and then Data Space is used up too!)

Menu Prompts/Structures, Text Strings for any messages, tables for various conversions/lookups, if find are all great uses for constants.

If you find any errors in what I wrote post that information here as well. (I tried to be careful and edit it accurately, but that doesn't always work... :) All of the examples I used I had compiled as well.)

Post a Reply

Please log in to post a reply.

Did you know that any circuit of voltage sources and resistors can be simplified to a "Thevenin" equivalent circuit? Learn more...