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 » writing strings to the lcd using fprintf_P

May 27, 2009
by lxowle
lxowle's Avatar

Hi there,

I'm learning everything as I go, but I would appreciate a couple of pointers here. I want to write a string to the LCD using fprintf_P (as per the clock demo).

Currently I can write a number to the LCD screen using:

 fprintf_P(&lcd_stream, PSTR("%16.2f sec"), (double) the_time / 100.0);

However, I want to define a variable string (which can change its value) and write this to the LCD as well.

I've tried the following:

 char sName[10] = "John";
 fprintf_P(&lcd_stream, PSTR("%s"), sName);

But that doesn't work. I think I'm on the right track, but it's not quite right. Could anyone give me any advice?

Thanks, lxowle.

May 27, 2009
by DonNYC
DonNYC's Avatar

I have only been doing C a few months but it looks like you created a character array sName[10] but when you call it in fprintf_P you don't use array brackets.

Don't you have to use a for loop to pull each character one at a time because C doesn't have any string variables?

May 27, 2009
by wayward
wayward's Avatar

lxowle,

I've been tinkering with this a lot myself, and I suspect there's either a bug or some sort of contrived "feature" in avr-gcc's string initialization. If you do instead

char name[] = { 'J', 'o', 'h', 'n', '\0' };

your code will work. The difference in produced assembly for "abc" can be seen below:

char test1[] = "abc";
   b00: 80 91 00 01     lds r24, 0x0100
   b04: 90 91 01 01     lds r25, 0x0101
   b08: a0 91 02 01     lds r26, 0x0102
   b0c: b0 91 03 01     lds r27, 0x0103
   b10: 89 83           std Y+1, r24    ; 0x01
   b12: 9a 83           std Y+2, r25    ; 0x02
   b14: ab 83           std Y+3, r26    ; 0x03
   b16: bc 83           std Y+4, r27    ; 0x04
char test2[] = { 'a', 'b', 'c', '\0' };
   b18: 81 e6           ldi r24, 0x61   ; 97
   b1a: 8d 83           std Y+5, r24    ; 0x05
   b1c: 82 e6           ldi r24, 0x62   ; 98
   b1e: 8e 83           std Y+6, r24    ; 0x06
   b20: 83 e6           ldi r24, 0x63   ; 99
   b22: 8f 83           std Y+7, r24    ; 0x07
   b24: 18 86           std Y+8, r1     ; 0x08

I'm thinking about asking on the avr-gcc-list about this, but I'll hold off just a little longer, so I don't look stupid like I already did once :)

May 27, 2009
by lxowle
lxowle's Avatar

Thanks very much to you both - both your solutions worked. DonNYC, I found if I used a loop and inserted each letter one by one it was fine:

    for (x=0;x<4;x++) {
        fprintf_P(&lcd_stream, PSTR("%c"), name[x]);
    }

Equally, creating the name array as wayward suggested produced the same result:

 char name[] = { 'J', 'o', 'h', 'n', '\0' };
 fprintf_P(&lcd_stream, PSTR("%s"), name);

Not that I understand assembly, but wayward, how did you read the assembly code? Anyway, I too have only just started C, and I've got a C book now as a reference, so I'll plough on. Thanks again for taking the time to help me,

lxowle.

May 27, 2009
by wayward
wayward's Avatar

Take a look at the Makefile. There should be a target called clock.ass or similar. Just run

make clock.ass

and you'll end up with a disassembly file in the same directory as the rest of the files. Alternatively, just run

avr-objdump -S some_file.o > some_other_file

which will produce "some_other_file".

May 27, 2009
by wayward
wayward's Avatar

lxowle,

edit your Makefile and change the line starting with

avr-objcopy -j .text -O ihex ...

to

avr-objcopy -j .text -j .data -O ihex ...

After that, your "..." string initializers will work.

Humberto, Mike, can we make this the default for NerdKits makefiles? I think that the importance of ensuring the code behaves As Expected™ far outweighs a slight gain in upload time.

May 28, 2009
by wayward
wayward's Avatar

An explanation for the technically inclined (aren't we all) :)

From our C code to the code in the microcontroller we go in three phases:

program in C -> [ELF object file] -> [Intel 8-bit hex format] -> avrdude upload

avr-gcc, the compiler, produces the original object file. This is a binary file containing all the symbols and functions from your program, plus some automagically generated code (such as what avr-gcc adds to interrupt handlers to prevent them from interrupting each other, etc.)

This object file is then fed to avr-objcopy which translates it from ELF format to Intel 8-bit hex format (-O ihex option), which is suitable for upload to the MCU.

Each file in the ELF format (and other formats as well) contains several blocks, or sections, organized by purpose. This is very visible in our beloved AVRs: you cannot even access those different sections using the same set of instructions; hence printf() vs. printf_P(), PSTR() macro, PROGMEM, et cætera.

The purpose of sections being to delineate functionally different bits and pieces of a program, it is very common to have one section dedicated to the program code itself (let's call it .text); one section holding initialized static variables (global ones and those declared using the keyword 'static' in C — we'll call that section .data); one section storing a list of string literals that appear through out the code, and so on.

Our Makefile has a line similar to this one:

avr-objcopy -j .text -O ihex program.o program.hex

This converts the object file from ELF to a different format we can upload to the MCU, but with one important thing: it copies only the .text section. What does this mean for us?

When we write the following:

char[] my_string = "Hello, World!";

compiler does a number of things:

  • it stores the character literal "Hello, World!\0" (note the auto-appended null character!) in the resulting object file's string table;
  • it allocates an array of 14 bytes in the .data section and initializes it with the character literal above;
  • if my_string is static, it stores its value (address of "Hello, World!\0" copy residing in .data, NOT the one from the string table) as yet another value inside the .data section;
  • if my_string isn't static, it allocates space for it inside the current function's stack frame (for which, I think, there is a special section as well) and fills it with the address of "Hello, World!\0" from the .data section. (If you're using optimizations, which you likely do — look for -O2 switch in the Makefile — it may instead simply load that address into the microcontroller's register(s)).

That's quite a lot of work! But the end result is that our compiled function has access to a pointer to the .data section, pointing to a copy of "Hello, World!\0", which we are free to modify at will. (The original string literal in the string table remains unmolested at all times.) But wait... What about:

avr-objcopy -j .text -O ihex program.o program.hex

Didn't we just say that this won't copy .data from the original object file? Indeed it won't. This line generates a file to be uploaded to the MCU without our preinitialized .data segment, carrying our strings, now lost in digital translation. All references to "Hello, World!\0" are lost and there will be only blank memory when our code tries to access them. Even worse, it may even go looking for it Bob-knows-where, if my_string was static and was as such initialized in the .data section. You get the point. :)

Please note that dropping .data from your end-product object file makes a lot of sense if you solemnly declare that you will never, ever mutate those strings initialized using string literals, and you don't mind doing this:

char[] my_string_in_program_space PROGMEM = "Hello, World! (from ROM)";

or just something like:

printf_P(PSTR("Hello, World! (from another cozy spot in the ROM)");

(PROGMEM and PSTR() are avr-libc's macros that ensure that a string will be placed in the code area, not in .data or elsewhere.)

If you change your mind and do wish to mutate such a string, you'll have to make an array for it (which'll be given room in the RAM) and copy bytes from Flash using pgm_get_byte() or something similar. But, since we're not retaining the original .data section, this:

char my_string[] = ":(";

will never, ever work. It has to go inside program memory, or not at all.

Easy way out? Simply change your Makefile and add that .data section to the resulting object file:

avr-objcopy -j .text -j .data -O ihex program.o program.hex

This will eat up your RAM by the total size of all preinitialized static variables, strings included, but if you're planning on changing those pesky strings, it may well be worth it.

Hope this helps,

Zoran

P.S. Further reading, down the slippery slope past the tip of the iceberg:

avr-libc documentation

ELF format on Wikipedia

A nice reference PDF on ELF

May 28, 2009
by wayward
wayward's Avatar

Disclaimer: above is for Linux, and very likely BSD and Darwin too. ELF has been a domestic file format on Linux for a number of years now. I don't know which format avr-gcc compiles to on other platforms, and I'd like to. I'm pretty sure that ELF isn't supported on Windows, but perhaps avr-gcc still uses it as an intermediary format since we're not building a Windows binary anyway.

May 28, 2009
by wayward
wayward's Avatar

Upon further review, I noticed an error I made while typing this out last night.

"if my_string isn't static, it allocates space for it inside the current function's stack frame (for which, I think, there is a special section as well)..." — there is no stack section in the resulting object file. There is a part of process memory that serves as stack when the program is running, but there's no need for it inside the object file, since the stack is dynamically created at runtime.

There are probably lots more so feel free to amend.

May 30, 2009
by lxowle
lxowle's Avatar

That's an amazing post wayward - thanks very much for it. I'm going to be away for a week, but I'll have a play with it when I return. I've been reading it, and trying to get my head around it, and it's been really helpful, even though I can't follow all of it yet :)

May 31, 2009
by luisgarciaalanis
luisgarciaalanis's Avatar

No!!!

char sName[10] = "John";

This is a NO! NO!

I am even suprised it compiles.

char sName[10] = { 0 }; sName[0] = 'J'; sName[1] = 'o'; .... should work

or sName[0] = { 0 }; something like strcpy(sName, "John");

May 31, 2009
by luisgarciaalanis
luisgarciaalanis's Avatar

When we write the following:

char[] my_string = "Hello, World!"; compiler does a number of things:

•it stores the character literal "Hello, World!0 " (note the auto-appended null character!) in the resulting object file's string table; •it allocates an array of 14 bytes in the .data section and initializes it with the character literal above;

if my_string is declared on a function shouldn't that be compiled into the function itself and be on the heap when the program is running?

May 31, 2009
by wayward
wayward's Avatar

Actually, the C99 standard is pretty clear on this one:

"If there are fewer initializers in a brace-enclosed list than there are elements or members
of an aggregate, or fewer characters in a string literal used to initialize an array of known
size than there are elements in the array, the remainder of the aggregate shall be
initialized implicitly the same as objects that have static storage duration."

(ISO/IEC 9899:TC3, §6.7.8 Initialization, ¶21, pp.127)

whereas

"All objects with static storage duration shall be initialized (set to their
initial values) before program startup. The manner and timing of such initialization are
otherwise unspecified."

(ISO/IEC 9899:TC3, §5.1.2 Execution environments, ¶1, pp.11)

While the manner of initialization is implementation-dependent, this is certainly allowed by the standard. We can safely assume that the remainder of the string is filled with null characters.

May 31, 2009
by wayward
wayward's Avatar

luisgarciaalanis, character literals aren't stored in the code block unless explicitly placed there (via some compiler-specific mechanism, such as whatever the PROGMEM macro uses on gcc). Local variables usually end up on the stack of the compiled function, not on the heap, which is always empty when the program starts. Heap is only for dynamically allocated memory.

May 31, 2009
by hevans
(NerdKits Staff)

hevans's Avatar

This is a very interesting discussion. In my time using microcontrollers the need to have a "dynamic" string that was not taken care of with PSTR("Foo %s") has actually not come up. Especially because program memory is so much larger than RAM on these little chips (16K vs 1K) it is usually a good thing to put strings in program memory and leave our RAM open for our crazy processing.

Although I was aware that doing

char[] foo = "bar";

would not work, I had attributed to a bug in avr-gcc, not the way we were ignoring the .data section. Thanks for pointing that out wayward.

In some ways I like the fact that we are currently forced to put constant strings in program memory, and copy them out if we need to play with them. It sort of forces you to think about the fact that you are in a limited memory environment, that constant strings belong in a separate part of memory and you should only pull out to your memory arrays the things that are absolutely necessary. There is at least one project in the works here that will likely involve heavy string manipulation and parsing, perhaps once we undertake that I will have a better answer for what I think the "right" way to go about it is. Until then, keep posting your solutions, and preferred way of doing things. Our forums are quickly becoming a great resource for everybody who is interesting knowing about microcontrollers!

Humberto

June 03, 2009
by wayward
wayward's Avatar

One more thing,

malloc() will fail if you omit .data (and perhaps .bss, not quite sure yet). Perhaps they depend on some preinitialized extern or static stuff. In any event, malloc() keeps returning 0 if I only upload the .text section.

Cheers

June 07, 2009
by wayward
wayward's Avatar

This probably goes in here:

Probably the best place to start would be avr-glibc's user manual, it's chock-full of gotchas and useful tips.

August 17, 2009
by jbremnant
jbremnant's Avatar

geez, I should've read this post earlier. Took me a while to figure out why calloc and malloc's were constantly giving me NULL return values and I couldn't allocate memory dynamically during runtime. It's one of those dangerous assumptions you make when you start writing C code for MCU's - that the memory is available to you when you ask for it.

I came to accept the fact that dynamic mem allocation in embedded programming is not a good practice, and one should always initialize the arrays you want to use up-front.

Thanks for the excellent explanations wayward.

January 04, 2010
by JKITSON
JKITSON's Avatar

I have the following array, i defined it as follows, and gave each element a value.

 char *a[10];
    a[0]="0000";  
    a[1]="0001";
    a[2]="0010";

    a[9]="1001";

I cannot get any element of array "a" to print to the led screen.

When i use the same program in "C" on windows and run on the pc screen it works perfect.

I looks like the printf_P and the PSTR functions do not like string's. Any idea how to make them work...

Thanks Jim

January 04, 2010
by pbfy0
pbfy0's Avatar

I'd do it like this:

int a[10];
a[0] = 0;
a[1] = 1;
a[2] = 10;

a[9] = 1001;

I don't think you can have an array of strings in C (AVR C anyway). You need the array as an int[10] because 1001 is bigger than 255 (2^8 - 1, max for char).

January 05, 2010
by wayward
wayward's Avatar

JKITSON,

did you get a chance to read the post I wrote on the 28th of May, in this thread? It explains why you can't output things like char[] arrays that live in data space. Keep in mind that AVR microcontrollers are a modified Harvard architecture—quite a different breed than what you're used to when dealing with commercial home computers. The bulk of your PC's memory is a single address space containing both program code and program data. By contrast, Harvard architecture uses separate address spaces (separate memories) for code and data. When you write

char*a[10];

this array will be allocated in the data space. fprintf_P is made so it treats the address you pass it as referencing the memory in program space rather than data space, so it looks for your characters in the wrong place.

For a quick workaround, if you're only going to output characters, try using lcd_write_data(c) from lcd.h.

Zoran

January 05, 2010
by JKITSON
JKITSON's Avatar

Thanks for the info..

I need the the 4 bits of data to use for my binary coded decimal data bus.

I had read all the posts and even tried the .data -0 change to the makefile. Did not work.....

Will try using the lcd.h.

I am trying to make a 7 inch high display to show the speed and distance traveled from my Tractor Pull Sled. The display will be in front of the bleachers and will use the wireless radio link for data.

Thanks again, the HELP FROM THIS FORUM is fantastic..

Jim

July 18, 2012
by RogerFL
RogerFL's Avatar

Sorry to drag up an old thread, but I recently found it helpful and I wanted to add to this discussion.

I was not satisfied with the option that is in the nerdkits makefiles. That is, if you write code that uses initialized globals like this:

int my_global = 22;

your program will fail without an error or warning. You can use my_global, but it will not have been initialized as you programmed it to be.

The option that is offered above is to add -j .data to your avr-objcopy command and then the data section will be included in your .hex file (which is loaded on the MCU) and initialized globals will work again. Make it look something like this:

avr-objcopy -j .text -j .data -O ihex $(MCU).out $(MCU).hex

However, it was also mentioned that you should avoid initialized globals when you can use PROGMEM instead because you'll waste precious RAM. So, here's a way to stick with the "do not use initialized globals" convention, but also get an error if you inadvertently do use it (you would never violate your own convention, of course, but maybe you're re-using someone else's code :) ).

  1. Find the default linker script your build is using now by adding --Wl,--verbose to the LINKFLAGS and force a rebuild. Mine is /usr/local/CrossPack-AVR/avr/lib/ldscripts/avr5.x

  2. Copy this file to your build directory and name it atmega328p.ld (or whatever fits your configuration).

  3. Make these changes to it:

Line 7 was:

data   (rw!x) : ORIGIN = 0x800060, LENGTH = 0xffa0

Change to:

data   (rw!x) : ORIGIN = 0x800100, LENGTH = 0x000e
bss    (rw!x) : ORIGIN = 0x80010e, LENGTH = 0xff92

Line 168 was:

  }  > data

Change to:

  }  > bss
  1. Add -T atmega328p.ld to your LINKFLAGS (and remove the verbose option you added in #1).

Now rebuild. If you accidentally add new initialized data you'll get this error message:

ld: address 0x800110 of atmega328p.out section `.data' is not within region `data'

Now, we did just add 14 bytes (see LENGTH = 0x000e above) of flash to our program because that's how much initialized data the C library was using (in my environment) and I had to allow enough memory for that since I have no control over it. But that's a good thing anyway. We probably made some library functions start working that did not before, because some data they depended on was never initialized (malloc for example).

If your C library uses more or less initialized data than mine, you need to change that LENGTH = 0x000e to something else. You can find the something else by looking at the .map file of your build; just look for the address of .bss - that's the length of your C library's .data.

You can generate the .map file in your builds by adding this to the avg-gcc command line for your linking step: -Map,$(MCU).map

The linker detects your new initialized data as an error because we allocate just enough memory for what initialized data the C library uses. Use one more byte and the memory region is no longer big enough to hold it. Link fail.

Honestly, I'd recommend just adding the -j .data option to your avr-objcopy and just try to follow good RAM conserving practices. But if you don't choose that option, I'd recommend doing all this stuff above so you don't spend much time debugging code that is failing only because your developer tool chain is violating standard C.

Roger

July 18, 2012
by Ralphxyz
Ralphxyz's Avatar

Sorry to drag up an old thread,

Bringing up old threads is expected.

That is why they keep them so that people can search and bring them up again.

Personally I think forums that discourage bringing up old threads are stupid, they get the same things repeated over and over. They expect people to search the archives but NOT to make comment, that is dumb.

At least when you reference an old thread all of the responders do not have to repeat themselves, so if they have nothig to add besides complaining about someone bring up a old thread they can just ignore it, but they would rather b*tch..

Besides bringing up an old thread shows that you are searching the Forum and not everyone might have seen the thread so it gets more exposure and more people's questions answered.

Your additions are most valuable, thank you.

Of course there needs to a writeup such as this added to the Nerdkit Community Library so that there is a ready reference to these type modifications beyond the basic beginings of the Nerdkits User Guide.

Ralph

Post a Reply

Please log in to post a reply.

Did you know that an electroluminescent backlight for an LCD panel requires hundreds of volts AC to run? Learn more...