Go Down

Topic: Why Serial.write() instead of Serial.print() (Read 2050 times) previous topic - next topic

TEAC

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().

jremington

#1
Jun 15, 2019, 08:28 pm Last Edit: Jun 15, 2019, 08:31 pm by jremington
Quote
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).

larryd

No technical PMs.
If you are asked a question, please respond with an answer.
If you are asked for more information, please supply it.
If you need clarification, ask for help.

GolamMostafa

#3
Jun 15, 2019, 10:21 pm Last Edit: Jun 15, 2019, 10:29 pm by GolamMostafa
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


Figure-2: Serial Monitor

2.  Now, there are two methods to deal with transferring ASCII code to the Serial Monitor. The methods are:
Code: [Select]
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:
Code: [Select]
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.
Code: [Select]
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?
Code: [Select]
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 --
Code: [Select]
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.
Code: [Select]
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.
Code: [Select]
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

TEAC

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);
 

GolamMostafa

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:
Code: [Select]
Serial.write('<');

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() ?

J-M-L

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 --
Code: [Select]
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
Code: [Select]

    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
Code: [Select]
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
Code: [Select]
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:
Code: [Select]
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:
Code: [Select]
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().
Code: [Select]
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:
Quote
(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
Code: [Select]
size_t print(char);
which code from the library is just
Code: [Select]
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:
Code: [Select]
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:

This is an uint8_t -> 65
This is an int -> 65
This is a char -> A


hope this helps
Hello - Please do not PM me for help,  others will benefit as well if you post your question publicly on the forums.
Bonjour Pas de messages privés SVP, postez dans le forum directement pour que ça profite à tous

J-M-L

#8
Jun 15, 2019, 11:25 pm Last Edit: Jun 15, 2019, 11:31 pm by J-M-L
The above code is not exactly correct though it works. It should be (in my opinion) like this:
Code: [Select]
Serial.write('<');
Again - this is showing misunderstanding of function signatures... The Print class defines 3 possible signatures for the write() method
Code: [Select]
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.

Hello - Please do not PM me for help,  others will benefit as well if you post your question publicly on the forums.
Bonjour Pas de messages privés SVP, postez dans le forum directement pour que ça profite à tous

larryd

#9
Jun 15, 2019, 11:50 pm Last Edit: Jun 15, 2019, 11:51 pm by larryd
@J-M-L
This could be reworked for the 'Tutorial forum' .



No technical PMs.
If you are asked a question, please respond with an answer.
If you are asked for more information, please supply it.
If you need clarification, ask for help.

GolamMostafa

#10
Jun 16, 2019, 04:56 am Last Edit: Jun 16, 2019, 08:07 am by GolamMostafa
@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.
Code: [Select]
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.
Code: [Select]
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."

Juraj

#11
Jun 16, 2019, 07:13 am Last Edit: Jun 16, 2019, 07:40 am by Juraj
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

GolamMostafa

#12
Jun 16, 2019, 07:58 am Last Edit: Jun 16, 2019, 08:48 am by GolamMostafa
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)
Code: [Select]
#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)
Code: [Select]
#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:
Code: [Select]
SUART.readBytesUntil('>', myArray, 10); //what would be the type of myArray -- byte or char?

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)

GolamMostafa

#14
Jun 16, 2019, 08:41 am Last Edit: Jun 16, 2019, 10:23 am by GolamMostafa
@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)

Go Up