views:

603

answers:

11
char byte_to_ascii(char value_to_convert, volatile char *converted_value) {

 if (value_to_convert < 10) {
  return (value_to_convert + 48);
 } else {
  char a = value_to_convert / 10;
  double x = fmod((double)value_to_convert, 10.0);
  char b = (char)x;
  a = a + 48;
  b = b + 48;
  *converted_value = a;
  *(converted_value+1) = b;
  return 0;
 }
}

The purpose of this function is to take an unsigned char value of 0 through 99 and return either it's ascii equivalent in the case it is 0-9 or manipulate a small global character array that can be referenced from the calling code following function completion.

I ask this question because two compilers from the same vendor interpret this code in different ways.

This code was written as a way to parse address bytes sent via RS485 into strings that can easily be passed to a send-lcd-string function.

This code is written for the PIC18 architecture (8 bit uC).

The problem is that the free/evaluation version of a particular compiler generates perfect assembly code that works while suffering a performance hit, but the paid and supposedly superior compiler generates code more efficiently at the expense of being able reference the addresses of all my byte arrays used to drive the graphics on my lcd display.

I know I'm putting lots of mud in the water by using a proprietary compiler for a less than typical architecture, but I hope someone out there has some suggestions.

Thanks.

A: 

It is typical for optimizers to do unwanted thingies from time to time if you poke around in the internals.

Is your converted_value a global value or otherwise assigned in such a fashion that the compiler knows not to touch it?

Paul Nathan
Yeah, the array is declared volatile, which assures in terms of this compiler that it is more likely to be accessible from anywhere in the generated assembly. Global declaration alone isn't enough, the volatile qualifier helps a great deal.
Nate
I mean, is the memory actually allocated? Just making sure. :)
Paul Nathan
To be honest, no, I'm not calling malloc to return my variables, but I am using 15% of my available flash memory for program space. So I haven't been compelled to use malloc, much to the chagrin of the hardcore C devs. I will try that tomrrow and see if I see any problems. Thanks for the suggestion.
Nate
you could statically allocate it such that it persists in the scope of the byte_to_ascii caller, e.g., char c[4];
Paul Nathan
Paul, that is exactly what I am doing currently.
Nate
I wouldn't recommend using malloc on a lower-end microcontroller like this. It will slow things down, and take up more code space and RAM then just allocating everything at compile time.
Steve Melnikoff
@Steve: Agreed. @Nate: Just making sure!
Paul Nathan
+3  A: 

Is it poor form to convert to floating, call fmod, and convert to integer, instead of just using the % operator? I would say yes. There are more readable ways to slow down a program to meet some timing requirement, for example sleeping in a for loop. No matter what compiler or what tweaking of assembly code or whatever else, this is a highly obfuscated way to control the execution speed of your program, and I call it poor form.

If perfect assembly code means that it works right but it's even slower than the conversions to floating point and back, then use integers and sleep in a for loop.

As for the imperfect assembly code, what's the problem? "at the expense of being able reference the addresses of all my byte arrays"? It looks like type char* is working in your code, so it seems that you can address all your byte arrays the way the C standard says you can. What's the problem?

Windows programmer
I ask because I'm new to C, not because I'm being a rhetorical pain in the neck. Thanks for the suggestions, I could do without the looking-down perspective though.
Nate
The questions that you asked were a bit different from asking if you should use C. If you're new to C then this isn't the best way for you to start out. If you need to program your machine with a timing loop and the functionality that you're doing here, maybe you should do it in assembly. If you need C code then ask a colleague to write a C function and explain it to you. This will have nothing to do with choosing compilers though.
Windows programmer
That being said, this function has been sanitized prior to being called that all inputs will be 0 through 99. I care not about casting losses as I am dealing with whole numbers at all times. I want to convert a byte (unsigned char) value of 55 to *ptr = '5' and *(ptr+1) = '5' - that's all. It works on all standard compilers, I'm asking why it fails. Pedantic snobbery does not help me.
Nate
Let me quality 'new' - I've been writing C for 8 mos coming from a primarily C-derived set of languages (C#, Java, etc.) I really don't want to start some sort of holy war. Just asking if those more experienced have some advice. I am not good, but I am passable by most standards.
Nate
Well, with this kind of code, and then adding that you're new to C, it looked like you were coming from an assembly language background not like you were coming from a C# and Java background. If you could read the assembly language output of your compilers then ... well, your C# and Java background are irrelevant, you understand how the machine works, and I can't really figure out why you're having trouble.
Windows programmer
A: 

PIC's don't like doing pointer arithmetic.

As Windows programmer points out, use the mod operator (see below.)

char byte_to_ascii(char value_to_convert, volatile char *converted_value) {

 if (value_to_convert < 10) {
  return (value_to_convert + 48);
 } else {
  char a = value_to_convert / 10;  
  char b = value_TO_convert%10;
  a = a + 48;
  b = b + 48;
  *converted_value = a;
  *(converted_value+1) = b;
  return 0;
 }
}
Tim Williscroft
now you went and made me curious. What do you mean by Pics not wanting to do pointer arithmetic? I've never done PIC work before.
Paul Nathan
I'm gonna test this approach first thing tomorrow, I'll post my results.
Nate
As mentioned elsewhere, division and mod are not supported in hardware on smaller microcontrollers, so if speed is important, use of these operators should be kept to a minimum.
Steve Melnikoff
switching to integer mod from floating point mod will probably yield all the performance required. Given that it's a C compiler, it has to know how to compile div and mod efficiently. The compiler library writer has an opportunity here, and we should exploit it unless they admit a terrible implementation. (I'm looking at you MSVC math library guys)
Tim Williscroft
+1  A: 

The code seems to be doing two very different things, depending on whether it's given a number in the range 0-9 or 10-99. For that reason, I would say that this function is written in poor form: I would split your function into two functions.

Chip Uni
Yes, it is designed to do that exactly. I set a temporary variable, call the function, and if the temporary variable returns != 0, I use the return value, if not, I reference the global it manipulated.
Nate
@Nate, and what happens if value_to_convert is 0?
quinmars
+1, I agree, a function should not work such different, except maybe in the case of errors.
quinmars
Given that a value < 10 is a subset of a value < 99, I'd be inclined (unless speed is more important than space) to get rid of the first case altogether, and the return value, and just return the string in all cases.
Steve Melnikoff
Steve -- I agree wholeheartedly with you.
Chip Uni
+2  A: 

Frankly, I would say yes..

If you wanted b to be the remainder, either use MOD or roll-your-own:

char a = value_to_convert / 10;
char b = value_to_convert - (10 * a);

Conversion to/from floats is never the way to do things, unless your values really are floats.

Furthermore, I would strongly recommend to stick to the convention of explicitly referring to your datatypes as 'signed' or 'unsigned', and leave the bare 'char' for when it actually is a character (part of a string). You are passing in raw data, which I feel should be an unsigned char (assuming of course, that the source is unsigned!). It is easy to forget if something should be signed/unsigned, and with a bare char, you'll get all sorts of roll-over errors.

Most 8-bit micros take forever for a multiply (and more than forever for a divide), so try and minimise these.

Hope this helps..

CapsicumDreams
Thanks for the suggestions. I have written things as chars and unsigned chars, to the same result. I will take heed to the idea of 8-bit uCs taking longer to compute floats, thanks for the feedback.
Nate
+4  A: 

I'd probably write that as:

char byte_to_ascii(char value_to_convert, volatile char *converted_value)
{
 if (value_to_convert < 10) {
  return value_to_convert + '0';
 } else {
  converted_value[0] = (value_to_convert / 10) + '0';
  converted_value[1] = (value_to_convert % 10) + '0';
  return 0;
 }
}
caf
You're missing the point of my function. I want to pass 0x0F as the value to convert and have the *converted_value consist of '1'
Nate
Okay, help me understand. It does exactly what I did before but instead of using integer values it uses the char code, which is the same. The code evaluates to an identical result. '0' = 48 - thus + '0' = + 48
Nate
The problem in your original code is likely in the floating-point usage - floating-point is often buggy or not fully supported on embedded platforms like you're using. This version uses only integer ops - as for the rest, like I wrote - it's how *I'd* write it (actually, I'd probably put the return value in `converted_value` regardless of whether it was one digit or two...).
caf
Okay, valid point, I will explore it tomorrow. Thanks for all the suggestions, really.
Nate
+4  A: 

modulo and integer division can be very very expensive. I have do not know about your particular architecture, but my guess it is expensive there as well.

If you need both, division and modulo, do one of them and get the other one by multiplication/difference.

q =p/10;
r = p - q*10;
aaa
I'll try that out tomorrow and let you know how it works out. For whatever reason calling any function prototyped through math.h has been causing problems. I'll try a more efficient local approach and see if I get better results.
Nate
This is by far the easiest and least expensive method to accomplish what I need. Thanks for actually reading my post and suggesting a refreshingly simple solution. Cheers!
Nate
Do you have any sources to your claim that modulo and integer division are expensive?
tstenner
If you're going to do the division anyway, you get the remainder for "free" in most architectures. You probably don't even have to use inline assembly, as any C compiler with half a brain for optimization will automatically use it.
sskuce
+6  A: 

I would definitely avoid using floating point anything on a PIC. And I would -try not to- use any divisions. How many times do you see sending a non-ascii char to the LCD? Can you save it to the LCD's memory and then call it by it's memory position?

Here's what a divide by 10 looks like in my code, note the 17 cycles it needs to complete. Think about how long that will take, and make sure there is nothing else waiting on this.

61:                         q = d2 / 10;
 01520  90482E     mov.b [0x001c+10],0x0000
 01522  FB8000     ze 0x0000,0x0000
 01524  2000A2     mov.w #0xa,0x0004
 01526  090011     repeat #17
 01528  D88002     div.uw 0x0000,0x0004
 0152A  984F00     mov.b 0x0000,[0x001c+8]

If you do a floating point anything in your code, look in the program memory after you've compiled it, on the Symbolic tab (so you can actually read it) and look for the floating point code that will need to be included. You'll find it up near the top (depending on your code), soon(ish) after the _reset label.

Mine starts at line number 223 and memory address of 001BC with _ floatsisf, continues through several additional labels (_fpack, _divsf3, etc) and ends in _funpack, last line at 535 and memory address 0042C. If you can handle (42C-1BC = 0x270 =) 624 bytes of lost program space, great, but some chips have just 2k of space and that's not an option.

Instead of floating point, if it's possible, try to use fixed point arithmetic, in base 2.

As far as not being able to reference all the byte arrays in your LCD, have you checked to make sure that you're not trying to send a null (which is a fine address) but it get's stopped by code checking for the end of an ascii string? (it's happened to me before).

ArielP
Wow, that's a lot of really good insight. I did check and my floating point code was indeed chewing up a ton of space. Good call on checking for the null, but unfortunately that doesn't seem to be the problem. I was able to easily abandon my earlier code and use the:q =p/10;r = p - q*10;as suggested by someone else. I was really making it much more difficult than was necessary. I'm somewhat green when it comes to embedded development and having come from a windows dev background I often forget to be as memory conscious as I should. I'm sure this will change as my programs grow in size.
Nate
+1  A: 

Since we're discussing divisions by 10 here..

This is my take. It only simple operations and does not even need wide registers.

unsigned char divide_by_10 (unsigned char value)
{
  unsigned char q;
  q = (value>>1) + (value>>2);
  q += (q>>4);
  q >>= 3;
  value -= (q<<3)+q+q;
  return q+((value+6)>>4);
}

Cheers, Nils

Nils Pipenbrinck
You forgot to declare and initialize n, unless I've gone blind.
sskuce
yep - I fixed that.
Nils Pipenbrinck
May I ask how you came up with such an algorithm for a division?
Gauthier
Yep. you start with the binary pattern for 1/10. multiply with it (bit for bit) and remove all multiplications with zero. Once done you combine the common therms to get the equation down to it's minimum.. I did half of the work using the derive-tool (TI) and the other half with brute-force search.. (keep in mind: The max. value is 255, so you can brute-force millions of instruction sequences within an hour)..
Nils Pipenbrinck
Is that really easier than (value/10)?
mjh2007
A: 
Thomas Matthews
A: 

Just to be a nitwitt, but multiple return statements from the same function can be considered bad form (MISRA).

Also, some of the discussions above are on the limit of permature optimizations. Some tasks must be left to the compiler. However, in such a minimalistic embedded environment, these tricks may be valid still.

e8johan