cembeddedprintfkeilc51

printf() results in gibberish


I have this code:

unsigned char *command = "0000";
unsigned char foo = (hex_char_to_int(command[0]) << 4) | hex_char_to_int(command[1]);
unsigned char bar = (hex_char_to_int(command[2]) << 4) | hex_char_to_int(command[3]);
printf("foo: %02x, bar: %02x\r\n", foo, bar);

It uses this function:

unsigned char hex_char_to_int(unsigned char ch) {
    switch (ch){
        case '0': return 0;
        case '1': return 1;
        case '2': return 2;
        case '3': return 3;
        case '4': return 4;
        case '5': return 5;
        case '6': return 6;
        case '7': return 7;
        case '8': return 8;
        case '9': return 9;
        case 'A': return 0xA;
        case 'B': return 0xB;
        case 'C': return 0xC;
        case 'D': return 0xD;
        case 'E': return 0xE;
        case 'F': return 0xF;
        case 'a': return 0xA;
        case 'b': return 0xB;
        case 'c': return 0xC;
        case 'd': return 0xD;
        case 'e': return 0xE;
        case 'f': return 0xF;
        default: return 0;
    }
}

This is the result:

"JW\xd6\x96'$$LK\x90\xbbar: 3030\r\r\n"

This is on the Keil C51 compiler, on an AT89C55WD, with printf() going over a serial port.

What is going on?

EDIT

I change the printf line to

printf("%02x%02x\r\n", (unsigned int)foo, (unsigned int)bar);

So it looks like a bug in printf. Please, programmers, never make a debugging tool that lies. I beg you.


Solution

  • As far as I can tell, that code should work under any conforming C compiler.

    I haven't used Keil C51, but I've seen some indications that it doesn't entirely follow the requirements of the C standard, for example in promoting narrow types.

    (This answer previously included a number of possible suggestions, most of which didn't pan out. If you're curious, see the edit history.)

    Apparently an unsigned char argument passed to printf is not promoted to int or unsigned int, as the c standard requires.

    To work around this while keeping your code reasonably portable, add casts to explicitly convert the values of foo and bar to unsigned int:

    printf("foo: %02x, bar: %02x\r\n", (unsigned int)foo, (unsigned int)bar);
    

    (The \r normally wouldn't be necessary, since \n is automatically converted to the system's line ending sequence for text streams, but perhaps Keil C51 works differently.)

    Again, it should work either way, but this change might work around a bug feature of Keil 51.

    UPDATE :

    I just checked the online documentation for Keil C51. The documentation for printf shows some non-standard features, including b and B to specify char types, just as l specifies long types.

    b and B are not necessary in standard C, since it's not possible to pass a char (or unsigned char, or signed char) argument to printf; any such argument will be promoted to int, or possibly unsigned int. I infer from this, and from the error you've run into, that Keil C51 doesn't promote narrow arguments to variadic functions, and in particular that an unsigned char argument is not promoted either to int or to unsigned int.

    That explains why

    printf("%02x", foo);
    

    didn't work, and why

    printf("%02x", (unsigned int)foo);
    

    did.

    This compiler targets a small 8-bit microprocessor. It makes sense that you wouldn't want to implicitly widen single-byte arguments. The authors apparently chose performance over conformance -- which is a perfectly valid decision. (It would be nice if the documentation were more explicit about this, or maybe I've missed something.)

    Probably the recommended way to print unsigned char values in hex would be:

    printf("foo: %02bx, bar: %02bx\r\n", foo, bar);
    

    Note that this is specific to Keil C51, and makes your code non-portable to other platforms. But then again, code written to run on such a small system isn't likely to be portable anyway.

    Casting to unsigned int, as I suggested previously, should also work, but using "%02bx" might be more efficient in time and code size, since the arguments can be passed as single bytes.