Why Serial.write() instead of Serial.print()

GolamMostafa:
The Serial.print() method works this way: when the entered argument is a number, a base (default 10) comes into picture, and the method takes over the following form --

Serial.print(0x41, 10);  //or Serial.print(0x41, DEC);

Now, the compiler transforms 0x41 into its decimal equivalent which is 65 (because of 10 base) and then executes the following two codes one after another.

@GolamMostafa - this is quite some BS... I don't know why you are making this up...There is no base 10 transformation by the compiler or whatever black magic...

what comes to play is that the compiler will select the method from the Print Class with the right signature.

Those are the ones defined in Print.h

    size_t print(const __FlashStringHelper *);
    size_t print(const String &);
    size_t print(const char[]);
    size_t print(char);
    size_t print(unsigned char, int = DEC);
    size_t print(int, int = DEC);
    size_t print(unsigned int, int = DEC);
    size_t print(long, int = DEC);
    size_t print(unsigned long, int = DEC);
    size_t print(double, int = 2);
    size_t print(const Printable&);

So if you do

Serial.print(0x41)

what happens is the following: to simplify, in C or C++ integer literal are promoted into the smallest type starting from int that fits. (in practice - the type of the integer literal is the first type in which the value can fit, from the list of types which depends on which numeric base and which integer-suffix was used)

Here 0x41 totally fits into an int and so the compiler/linker sees you want to call print() with an int as parameter. So this parameter will actually be set on the calling stack as 2 bytes 0x0041.

So at link time, what is called is found by looking in the list above for what could work and finds

size_t print(int, int = DEC);

. Indeed although we are looking for a method with only one parameter, the function signatures states that if the second parameter is optional and if not set, then use DEC.

So the binary function that gets executed is this one:

size_t Print::print(int n, int base)
{
  return print((long) n, base);
}

which actually casts the 0x0041 into a long and calls another print function with a long first parameter and the code really being executed is this:

size_t Print::print(long n, int base)
{
  if (base == 0) {
    return write(n);
  } else if (base == 10) {
    if (n < 0) {
      int t = print('-');
      n = -n;
      return printNumber(n, 10) + t;
    }
    return printNumber(n, 10);
  } else {
    return printNumber(n, base);
  }
}

here the base is DEC (which is defined as 10) and so the code first tests if the number is negative, in which case it prints a minus sign and then print the number without the sign by calling printNumber().

size_t Print::printNumber(unsigned long n, uint8_t base)
{
  char buf[8 * sizeof(long) + 1]; // Assumes 8-bit chars plus zero byte.
  char *str = &buf[sizeof(buf) - 1];

  *str = '\0';

  // prevent crash if called with base == 1
  if (base < 2) base = 10;

  do {
    char c = n % base;
    n /= base;

    *--str = c < 10 ? c + '0' : c + 'A' - 10;
  } while(n);

  return write(str);
}

So long story short the compiler does not transform 0x41 into 65, it's actually after a lot of functions calls that the developers have created a small algorithm transforming an unsigned long number into a char buffer which is the ASCII representation of the number and only then proper writes are issued.

Same applies when you say this:

(3) If we execute this code: Serial.print('A');, then A appears on the Serial Monitor. In this case, the base does not come into picture;

why would there be a base coming in the picture? How can someone make sense of this?

What happens is that the compiler now sees that the type of the parameter of the print method is a char. So it looks for a method with a compatible signature and finds

size_t print(char);

which code from the library is just

size_t Print::print(char c)
{
  return write(c);
}

and so that's why write(0x41) is actually called

It's all based on method signature and what parameters looks like.

Here is a quick example to better illustrate this. I create a class with 3 printValue() methods, each with a different signature and then from the setup() I call printValue() with different types. You'll see in the console which ones gets executed

here is the code:

class testSignature
{
  public:
    void printValue(uint8_t v)
    {
      Serial.print(F("This is an uint8_t -> ")); Serial.println(v);
    }

    void printValue(int v)
    {
      Serial.print(F("This is an int -> ")); Serial.println(v);
    }

    void printValue(char v)
    {
      Serial.print(F("This is a char -> ")); Serial.println(v);
    }
};

testSignature testObject;

void setup()
{
  Serial.begin(115200);
  testObject.printValue((uint8_t) 0x41); // we cast the parameter explicitly into a byte
  testObject.printValue(0x41); // here the default C rules will apply and 0x41 is seen as an int
  testObject.printValue('A'); // here the parameter is clearly of type char since it's in simple quotes
}

void loop() {}

Here is what the Serial console (at 115200 bauds) would show:

[color=purple]
This is an uint8_t -> 65
This is an int -> 65
This is a char -> A
[/color]

hope this helps