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 |
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:
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:
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 |
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 |
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
your code will work. The difference in produced assembly for "abc" can be seen below:
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 |
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:
Equally, creating the name array as wayward suggested produced the same result:
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 |
Take a look at the Makefile. There should be a target called clock.ass or similar. Just run
and you'll end up with a disassembly file in the same directory as the rest of the files. Alternatively, just run
which will produce "some_other_file". |
May 27, 2009 by wayward |
lxowle, edit your Makefile and change the line starting with
to
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 |
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:
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:
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:
compiler does a number of things:
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:
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:
or just something like:
(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:
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:
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: |
May 28, 2009 by wayward |
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 |
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 |
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 |
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 |
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 |
Actually, the C99 standard is pretty clear on this one:
(ISO/IEC 9899:TC3, §6.7.8 Initialization, ¶21, pp.127) whereas
(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 |
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) |
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
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 |
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 |
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 |
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 |
I have the following array, i defined it as follows, and gave each element a value.
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 |
I'd do it like this:
I don't think you can have an array of strings in C (AVR C anyway). You need the array as an |
January 05, 2010 by wayward |
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
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 |
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 |
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:
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:
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 :) ).
Line 7 was:
Change to:
Line 168 was:
Change to:
Now rebuild. If you accidentally add new initialized data you'll get this error message:
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 |
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 |
Please log in to post a reply.
Did you know that NerdKits has a TV commercial, seen on MythBusters and the Science Channel? Learn more...
|