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

Hello! I am currently testing Bluetooth communication between two arduinos for my final project. My project is a remote-controlled robotic arm and I have seen codes from other people that did the same but what I don't get is why they use the function Serial.write() to send the values via bluetooth instead of Serial.print().

From what I've learned so far is that the Serial.write function converts the value to the ASCII value. Imagine I do Serial.write(100) I would be sending the letter "d" instead of the value 100.
If I want to control a servo I would have to use a number on the Servo.write() function which is why I can't understand the usage of the Serial.write() instead of Serial.print().

From what I've learned so far is that the Serial.write function converts the value to the ASCII value.

No, Serial.write() just sends raw bytes of data.

Serial.print() converts and displays data in human readable forms (ASCII).

1 Like

Read these posts:
https://forum.arduino.cc/index.php?topic=42603.0

1. Serial Monitor of the Arduino Platform is an ASCII (Text) type display unit. It means that if we want to see a character of the English Language Alphabet on this display unit, we must send/transmit ASCII code (Fig-1) of that character to the Serial Monitor. As a result, the character will appear on the OutputBox (Fig-2) of the Serial Monitor.


Figure-1: ASCII code Table

SerialMonitor.png
Figure-2: Serial Monitor

2. Now, there are two methods to deal with transferring ASCII code to the Serial Monitor. The methods are:

Serial.print();   //indirect method
Serial.write();  //direct method

3. Examples:
(1) To see A on the Serial Monitor, which method should we use?
Obviously, we will execute the following code:

Serial.write(0x41);   //0x41 is the ASCII code of the character A (Fig-1)

Serial.write() method always transmits the 8-bit (1-byte) number that we have entered in the argument field. If the entered number matches with an ASCII code, the corresponding character will appear on the Serial Monitor.

If we don't want to memorize the ASCII codes of the charcaters, then we can enter the 'to be printed character' in this style 'A' (within opening and closing single quotes) in the argument field of the .write() method. The compiler will place the corresponding ASCII code in place of 'A'. Thus, the following two codes are equivalent.

Serial.write(0x41);   //will show A
Serial.write('A');                //will show A

(2) Can we execute the following code that uses .print() method to see A on Serial Monitor?

Serial.print(0x41);       //shows: 65

No!

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. As a result, we see 65 on the Serial Monitor.

Serial.write(0x36);    //transmits ASCII code of 6; as a result, 6 appears on Serial Monitor
Serial.write(0x35);  //transmits ASCII code of 5; as a result, 5 appears on Serial Monitor

(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; the compiler executes this code: Serial.write(0x41).

(4) If we execute this code: Serial.print(0x41, HEX);, what will appear on the Serial Moniotr? (Ans: 41)

(5) If we execute this code: Serial.print("0x41");. what will appear on Serial Moniotr? (Ans: 0x41). The base will not come into picture as the argument is not a number. The argument is a string of 4 characters (0 x 4 1). The following codes will be executed one after another; as a result, we will see 0x41 on Serial Monitor.

Serial.write(0x30); //ASCII code of 0
Serial.write(0x78);  //ASCII code of x
Serial.write(0x34);  //ASCII code of 4
Serial.write(0x31); //ASCII code of 1

SerialMonitor.png

jremington:
No, Serial.write() just sends raw bytes of data.

Serial.print() converts and displays data in human readable forms (ASCII).

Ok I get that, but why does the person use Serial.write() to send a value previously mapped instead of Serial.print()? Doesn't that convert the byte to the corresponding ASCII character?

This is the code he used:

thumb = map(thumb ,ClosedThumb ,OpenedThumb ,0,180); // The analog read has to be readapted in values between 0 and 180 to be used by the servomotors.
index = map(index ,ClosedIndex ,OpenedIndex ,0,180); // The minimum and maximum values from the calibrations are used to correctly set the analog reads.
middle = map(middle ,ClosedMiddle ,OpenedMiddle ,0,180);
annular = map(annular,ClosedAnnular,OpenedAnnular,0,180);
pinky = map(pinky ,ClosedPinky ,OpenedPinky ,0,180);

Serial.write("<"); // This character represent the beginning of the package of the five values
Serial.write(thumb); // The values are sent via the Tx pin (the digital pin 1)
Serial.write(index);
Serial.write(middle);
Serial.write(annular);
Serial.write(pinky);

TEAC:
Serial.write("<"); // This character represent the beginning of the package of the five values

The above code is not exactly correct though it works. It should be (in my opinion) like this:

Serial.write('<');

But how does the slave arduino read the values from the Serial exactly as a decimal value? Given the fact that it is Serial.write() and not 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

GolamMostafa:
The above code is not exactly correct though it works. It should be (in my opinion) like this:

Serial.write('<');

Again - this is showing misunderstanding of function signatures... The Print class defines 3 possible signatures for the write() method

size_t write(uint8_t);
size_t write(const char *str);
size_t write(const uint8_t *buffer, size_t size);

so if you call write(">") the compiler will take the second method (because in C or C++ the double quotes denote a char* type) whereas if you call write('>') then the compiler takes the first method (because the simple quotes denote a char type). Both are legit, the second one with simple quotes is of course more efficient.

What ends up being executed depends on the method signature, thus the type of the parameters.

@J-M-L
This could be reworked for the ‘Tutorial forum’ .

@J-M-L

By no means, I am in a position to debate with you on this issue. Based on my understanding that it is this code: Serial.write() which is executed by the MCU to place data byte into the actual transmitter of the UART Port and not this code: Serial.print(), I have made my Post#3 to correlate (at conceptual level) the results that I observe on the Serial Monitor in response to Serial.print() and Serial.write() commands.

UART Port transfers data 1-byte at a time; therefore, the Serial.print("1234"); command must execute the following codes (again at conceptual level) one after another so that the corresponding ASCII codes appear on the Serial Monitor and I observe the image 1234.

Serial.write(0x31);
Serial.write(0x32);
Serial.write(0x33);
Serial.write(0x34);

The Serial.print(1234) command also shows 1234 on the Serial Monitor. Are Serial.print("1234") and Serial.print(1234) are subjected to the same/identical interpretation because they show the same image (1234) on the Serial Monitor?

Why/how does Serial.print(1234) show 1234 on the Serial Monitor? If I am asked this question by my co-workers, I will explain it by bringing the concept of base 10 as the following codes are compiled well and got executed.

Serial.print("1234");    //shows: 1234
Serial.print(1234);                //shows: 1234
Serial.print(1234, DEC);       //shows: 1234
Serial.print("1234", DEC);      //compilation error

The source codes that you have presented explain the Mechanics and not the Mechanism of the working principles (at conceptual level) of the .print() and .write() methods.

I am always at liberty to explain things in a way that satisfies my queries and does not contradict with the visible results of the users. It is up to the freedom of the users to support/accept it or not; however, it crosses the boundary of politeness when the works are declared as BS. Look at the early 7-8th Century translations of the Greek Works by the Arabians -- Syrian speaking Arabians translate the works into Syrian first and then into Arabic and then into Latin and then into European Languages. The early Greek-Syrian-Arabic translations contained many mistakes (discovered later when re-translations took place as the Language advanced) and the historians attributed it to the limitations of the Language itself and they never showered BS upon the translators. The translators did to the best they could do.

"Absolute Rules are for the monkeys; Intelligent agents use both rules and judgement."

and to make the explanation to a newbie harder
Serial.write('A')
does the same as
Serial.print('A')
and
Serial.write("ABC")
does the same as
Serial.print("ABC")

but there are multiple print functions with different parameter types including number types
so
Serial.write(65)
will print A in Serial Monitor, because 65 is ASCII code of 'A'
Serial.print(65)
will print 65 in Serial Monitor, because print for a number type takes over

it is your decision if you want to transfer numeric data between two MCU as human readable or as binary.
with human readable you can test with Serial Monitor, send data or read data in place of one of the MCUs.
but binary needs less bytes for transfer. numbers until 255 fit into one byte, but string "255" is 3 bytes

and we must mention one for binary transfer useful write function here. one which takes an array of bytes which is not a zero terminated string, so the second parameter is the length
const byte buff[] = {65, 66, 67};
Serial.write(buff, 3);
will print ABC in Serial Monitor

the write and print functions are defined in base class Print and used in many derived classes, not only for hardware or software Serials but too in good LCD libraries and networking libraries clients

TEAC:
But how does the slave arduino read the values from the Serial exactly as a decimal value? Given the fact that it is Serial.write() and not Serial.print() ?

Practice the following sketches to see the mechanism of extracting decimal number at the receiver side after collecting the data bytes transferred by the sender using .write() methods.

UNO-1 (Sender Codes)

#include<SoftwareSerial.h>
SoftwareSerial SUART(2, 3); //SRX/STX pin of UNO

void setup() 
{
  Serial.begin(9600);
  SUART.begin(9600);
}

void loop() 
{
  SUART.write('<'); //Start Mark of Message
  SUART.write(0x31);    //sends 1
  SUART.write(0x32);    //sends 2
  SUART.write(0x33);    //sends 3
  SUART.write(0x34);    //send 4
  SUART.write('>'); //End Mark of Message
  delay(1000);

}

UNO-2 (Receiver Codes)

#include<SoftwareSerial.h>
SoftwareSerial SUART(2, 3); //SRX/STX pin of NANO
byte myData[10];
bool flag1 = false;
int i = 0;
int x1, x2, x;

void setup()
{
  Serial.begin(9600);
  SUART.begin(9600);
}

void loop()
{
  byte n = SUART.available(); //checking if a data byte has arrived
  {
    if (n != 0)
    {
      if (flag1 == false)   //Start Mark has not arrived
      {
        byte x = SUART.read();
        if (x == '<')
        {
          flag1 = true;   //Start Mark has arrived
        }
      }
      else    //Start Mark has already arrived
      {
        byte y = SUART.read();
       // Serial.println(y, HEX);
        if (y != '>')
        {
          myData[i] = y;
          i++;
        }
        else
        {
          x = extractDecimal();    //extract decimal number for myData array
          Serial.println(x, HEX);    //shows received decimal number: 1234
          flag1 = false;
          i = 0; //reset array
        }
      }
    }
  }
}

int extractDecimal()
{
  x1 = (int)((myData[0] - 0x30)<< 4)| (int)(myData[1] - 0x30); //12
  x2 = (int)((myData[2] - 0x30) << 4) | (int)(myData[3] - 0x30);  //34
  x = (x1 << 8) | x2;
  return x;
}

BTW: Give a try to re-write the receiver codes using the following method:

SUART.readBytesUntil('>', myArray, 10); //what would be the type of myArray -- byte or char?

@GolamMostafa what if I want to send #3C
it is not good to use ascii separators with binary data
for binary data it is simpler to send the length as first byte or two bytes and then the data. (and maybe a checksum at the end)

Juraj:
@GolamMostafa what if I want to send #3C
it is not good to use ascii separators with binary data
for binary data it is simpler to send the length as first byte or two bytes and then the data. (and maybe a checksum at the end)

In that case, I would go on transferring 'Intel-Hex' formatted frame/file which is what you have mentioned in your above quote. In Intel-Hex format, usually the colon (: = 0x3A) is transmitted first as the beginning of a frame.

To send #3C, I will form the following Intel-Hex formatted frame for onward transmission to the receiver:
: 03 1000 00 #3C 54 (appearance on ASCII Monitor)
(a) (b) (c) (d) (e)--> (f)

(a) -- beginning mark of frame
(b) -- number of ASCII coded information bytes in field (e)
(c) -- buffer storage location
(d) -- indicates not the end-of-file
(e) -- actual ASCII coded information bytes (3 bytes)
(f) -- Checksum : add all ASCII coded bytes of fields (b) to (e), discard carry; take 2's complement and send as last byte (probably, different approach is to adopted to compute CHKSUM due to the presence of the symbol # as data?)

==> 3A 3033 31303030 3030 233343 3534 (actual transmission over TX Line as ASCII codes for the characters of the Intel-Hex frame; spaces are shown for clarity.)

Implementations:
Sender Codes: (UNO-1)

Receiver Codes: (UNO-2)

Screenshot: (UNO-2)

@Golam

The source codes that you have presented explain the Mechanics and not the Mechanism of the working principles (at conceptual level) of the .print() and .write() methods.

I do have explained the fundamental mechanism - what the compiler does to find which function to call. It’s signature dependent. I attached to source to show that the compiler dies not perform the base 10 conversion but that it is actually the library code which does the right work.

At method level description, print transforms its input in ASCII representation if needed (write(“hello”) is the same output as print(“hello”)) and write does not pre-process the input.

I feel You are totally free to explain things the way you want and i feel I’m totally free to call this BS when you make statements such as the compiler transforms the number in base 10...

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. As a result, we see 65 on the Serial Monitor.

Serial.write(0x36);    //transmits ASCII code of 6; as a result, 6 appears on Serial Monitor

Serial.write(0x35);  //transmits ASCII code of 5; as a result, 5 appears on Serial Monitor

This is plain wrong. Note that I’m offering my opinion on the quality of the explanation and it’s not a value judgement on your person. And BS is not as bad as “alternate truth” or “lie”...

Yes a write happens at the end because this is the way the Print class works with write being the only virtual function you need to implement to get printing working. But the bytes values that are sent are not black magic work done by the compiler but the result of carefully crafted algorithms written by a developer with clear intent

Explaining how the compiler picks a code to execute when multiple methods have the same name but different signature is in my opinion what the right answer to the question.

Now you are totally free to think differently and call also my explanation BS

PS/

Serial.write() which is executed by the MCU to place data byte into the actual transmitter of the UART Port and not this code: Serial.print(),

if you want to be technically accurate, you should state that the write method does directly transfer the byte in the register ONLY if the serial buffer is empty (for efficiency reason) otherwise it’s added to the end of the circular buffer (possibly after blocking if buffer is full) until an interrupt later will picks up the right byte and that’s where t is actually being put in the register. That’s why you don’t need to worry about a risk of writing too fast in the register and can issue multiple write next too each other

GolamMostafa:
In that case, I would go on transferring 'Intel-Hex' formatted frame/file which is what you have mentioned in your above quote. In Intel-Hex format, usually the colon (: = 0x3A) is transmitted first as the beginning of a frame.

Intel HEX format is "human readable", not binary. I commented the unpractical binary protocol in your previous comment.

J-M-L:
Note that I’m offering my opinion on the quality of the explanation and it’s not a value judgement on your person. And BS is not as bad as “alternate truth” or “lie”...

I acknowledge and appreciate your sincere efforts so engaged for the elimination of misconceptions that I might have conceived owing to my very nature of self-learning through experiments and logical matching without really looking into the actual implementations of the developers.

GolamMostafa:
I acknowledge and appreciate your sincere efforts so engaged for the elimination of misconceptions that I might have conceived owing to my very nature of self-learning through experiments and logical matching without really looking into the actual implementations of the developers.

I think the main misconception is failure to understand how the compiler picks the right method based on signature, hence leading to different code being executed.

Portraying this as magic done by the compiler transforming parameters automagically is very far from the technical truth and not leading learners towards the right path.

But again, that’s just my view, your poetical interpretation of the compiler looking after you and understanding your intent is refreshing. May be when AI gets better ?

all print functions (same set of println exists) 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&);
  • __FlashStringHelper is for the F() macro
  • Printable& is for classes implementing Printable (only example is IPAddress)

and all write functions

    virtual size_t write(uint8_t) = 0;
    size_t write(const char *str) {
      if (str == NULL) return 0;
      return write((const uint8_t *)str, strlen(str));
    }
    virtual size_t write(const uint8_t *buffer, size_t size);
    size_t write(const char *buffer, size_t size) {
      return write((const uint8_t *)buffer, size);
   }

pure virtual write(uint8_t) must be implemented in the derived class

and to be complete, implementation of write(buffer, size) in Print.cpp:

size_t Print::write(const uint8_t *buffer, size_t size)
{
  size_t n = 0;
  while (size--) {
    if (write(*buffer++)) n++;
    else break;
  }
  return n;
}

so simple