[Arduino]用String或sprintf( )與dtostrf( )做類似printf( )格式化輸出
關於類似 printf( ); 的輸出方式, 雖然本站站長奈何大神有寫了一篇關於格式化輸出:
http://www.arduino.cn/thread-8366-1-2.html
但是我覺得那篇寫得不好, 因為雖然那篇讓你多知道一點秘密, 但是不好用 !
最簡單的方法就是直接用 C++ 的 String( ) 串接功能即可:
Serial.println(String("")+"Your Height="+height + ", and Weight=" + weight);
說明: 只要第一個是 String(""), 之後不論整數 int, long, 實數 float 等都會自動轉為字符串, 用 + 串接在一起 !
缺點: float 小數點後會印出幾位無法控制 :-(
(注意 UNO 如果用 double 其實會被偷改為 float )
(二)那如果是要印到 LCD 或 SoftwareSerial 軟串口甚至 SPI 呢?
簡單, 先放到 String 字符串即可, 之後愛怎樣就怎樣 :
String gy = String("")+"Your Height="+height +
", and Weight=" + weight;
Serial.println(gy); // 印到串口
LCD.print(gy); // 假設你已經有 LCD. 可以用
也很簡單, 只是看起來有點麻煩而已(其實你用 Serial.print(float) 它也是偷偷類似這樣做 **註!):
String gy = String("")+"Your Height="+height;
long tmp = weight; // 整數部分
long yytmp = (weight -tmp)*100+0.5; // 小數部分; 從小數點後第三位做四捨五入(round)到第二位
/// if(yytmp >= 100) yytmp=99; // 改用以下方法處理 ..
++tmp; // 0.99xyz.. +0.005 ===> 1.0pqr...
if(yytmp < 10) gy += "0";
Serial.println(gy); // 印到串口
LCD.print(gy); // 假設你已經有 LCD. 可以用
**註: 嚴格說來 Serial.print(float); 就是 Serial.print(float, 2);
這時它是用以下(六)說的用AVR的 dtostrf( ) 函數把 float 轉換為包括小數點後兩位的字符串!
(四)使用 sprintf( ) 印到 C 的字符串 (注意, 不是 C++ 的 String 喔!)
可是在 Arduino 上不可以用在 float, double, 以及 long long 都不行!
所以, 其實用這招除了可以 %5d 或 %8d 等這樣格式之外, 並沒有比前面用 String( ) 方法好用 !
char cgy[66]; // C 字符串; 自己要注意是否 66 bytes 夠用, 別忘了 C 字符串須要多一 char 放 '\0' (就是整數 0)表示結束!!!
long wtmp = weight + 0.5; // 因為Arduino 的 sprintf( ) 不可用 float/double; 只好似捨五入為整數 long
sprintf(cgy, "Your Height=%d, and Weight=%ld", height, wtmp);
/// 注意 %d 是給 int 用(2 bytes); long 要用 %ld 才對喔 !
Serial.println(cgy); // 印到串口
LCD.print(cgy); // 假設你已經有 LCD. 可以用
(五)注意 float 與 double 無效, 但印整數時格式仍可能有類似 %8.5d 喔!
自己把上面(四)的 sprintf( ) 改為如下看看:
sprintf(cgy, "Your Height=%8.5d, and Weight=%ld", height, wtmp);
// %8.5d 表示此 int 會用 8 格位置, 但至少印五位, 左邊會補 0, 如 00168
// 注意, 如果 long 對應的格式寫成 %d 則只會印出其右邊 2 bytes 的值!!
(六)我就想要類似 %6.3f 印實數 float / double 可不可以 ?
** 如果你只是要印一個實數印到小數點後第三位, 那這樣就可以了:
Serial.print(float, 3); // 把 float 打印到串口, 只到小數點後第三位!
**在 UNO 上其實 double 會被偷改為 float, 所以其實沒有 double 可用 !
那也不難, 偷用 AVR Library 內的 dtostrf( ) 先把 float 轉為 C 的字符串即可:
float weight=72.12666; // 故意
char www[22]; // 放 weight 體重, 故意用 22 bytes, 注意位置一定要夠用 !
dtostrf(weight, 6, 3, www); // 相當於 %6.3f
String gy = String("")+"Your Height="+height +
", and Weight=" + www); // 注意 www 是 weight 的字串格式
Serial.println(gy); // 印到串口
LCD.print(gy); // 假設你已經有 LCD. 可以用
sprintf(cgy, "Your Height=%d, and Weight=%s", height, www);
// 注意 www 是用 %s "印" 入 cgy 字符串內部!
// 這樣 cgy 也是字符串, 是 C 的字符串, 不是 C++ 的 String
Serial.println(cgy); // 印到串口
LCD.print(cgy); // 假設你已經有 LCD. 可以用
http://www.atmel.com/webdoc/AVRL ... f3e4649092b9f1.html
http://www.atmel.com/webdoc/AVRL ... 600d35399f2a2a.html
http://www.atmel.com/webdoc/AVRL ... f3e4649092b9f1.html
http://www.atmel.com/webdoc/AVRL ... 81cd5dc4d7a31d.html
http://www.atmel.com/webdoc/AVRL ... 168b3ce8771d42.html
這是 Prototype of the function dtostrf( )
/////////////////////////////////////
這是 Prototype of the function sprintf( )
http://www.atmel.com/webdoc/AVRLibcReferenceManual/group__avr__stdio_1ga2b829d696b17dedbf181cd5dc4d7a31d.html
//////////////////////////////////////////////////////////////////////////////////////////////
ㄟ, 阿我前面已經有(三)印到兩位和(六)印到三位的範例,
不過我還是來寫些比較深入的讓一些比較好奇的 Arduino 愛好寶寶滿足好奇心 :-)
(A)最簡單方法是先把 float 稍微加工處理後用 String 串接, 如下:
float weight=72.12666; // 故意
float aw = ((long)(weight*prec+0.5))/1.0/prec; //注意
String ans = String("Weight=") + aw;
簡單, 要小數點後兩位就 prec = 100; 要三位就 prec = 1000;
但請注意, 上面那 /1.0/prec 是必要的, 是讓它先變為 float;
因為 /1.0 左邊已經是 long 整數, 如果不先 /1.0 就直接
做 /prec 則變 long /long 將不對, 因為 38/10 是 3 不是 3.8喔 !
因為 Arduino 考慮 MCU 能力有限, 無法用 double,
即使你寫 double, 也會被偷改為 float;
就是說 從最左邊不是 0 開始只有七位到八位是可以信任的,
也就是 float x = 123.45678999; 與 float x = 123.45678922;
其實幾乎是一樣的, 這原因是因為 float 只有用 32 bits表示,
其中 23 bits 加上一個隱藏的 bit 共 24 bits 存有效值 significand;
所以把 float 轉換為 binary 之後只能保留左邊 24 bits,
這等於 binary 準確 24位, 相當於十進制的 24*0.3010=7.2位!
http://zh.wikipedia.org/wiki/IEEE_754
(如看不到, 請用百度查詢 "IEEE 754")
(B)偷用 String 的 indexOf( ) 和 substring( )
float weight=72.12666; // 故意
int precision = 4; // 小數點後四位
String stmp = String("") + weight; // 轉為字符串
int dtw = stmp.indexOf("."); // 小數點的位置
if(dtw == -1) stmp += ".000000000"; // 沒找到小數點
dtw += precision; // 小數點位置再往右邊數 precision 位置
String ans = String("Weight=") + stmp.substring(0, dtw+1);
http://arduino.cc/en/Reference/StringSubstring
優點:也還簡單, 要小數點後兩位就 int precision = 2; 其他以此類推!
與前面說的一樣, 仍是有float只有七位左右的準確有效位數的問題,
且沒有對沒印出的第一位做四捨五入(round) !
(C)使用 AVR (Arduino 的底層)Library dtostrf( )
這在(六)我們已經用過了, 要注意參數習慣不相同就是了!
float weight=72.12666; // 故意
int precision = 4; // 小數點後四位
char ctmp[22]; // 不可能多達 21 位吧 ?! 21+1 = 22
dtostrf(weight, 6, 4, ctmp); // 相當於 %6.4f
// 注意 %6.4f 雖然整數部分只有一位, 但不夠用會子幾長大 :-)
String ans = String("Weight=") + ctmp; // 這樣也 OK
/// 注意雖然 ctmp[22] 表面看有 22 char, 實際只會用真正長度!
(D)把剛剛(C)的後半段改用如(六)的方法用 sprintf( )
就是用 dtostrf( ) 把 weight 轉為須要的 C 字符串 ctmp[]後,
繼續用純 C 的方法, 注意 String( ) 是 C++ 的方法 !
float weight=72.12666; // 故意
int precision = 4; // 小數點後四位
char ctmp[22]; // 不可能多達 21 位吧 ?! 21+1 = 22
dtostrf(weight, 6, 4, ctmp); // 相當於 %6.4f
// 注意 %6.4f 雖然整數部分只有一位, 但不夠用會子幾長大 :-)
sprintf(cgy, "Your Weight=%s=", ctmp); // 最後也故意有個=等號
(八)何時該使用 sprintf_P( ) 呢 ?
如果你已經會使用 sprintf( ) 先把一個甚至多個資訊格式法到字符串,
那可能很想知道 sprintf( ) 與 sprintf_P( ) 有何差別 ?!
但 sprintf_P( ) 是讓你節省 RAM 來的,
因為大部分 Arduino 的 Flash (ROM) 是 32KB,
扣掉 BootLoader 還有 31.5KB; 可是 RAM 只有 2KB,
通常變數都放 RAM, 硬件串口的緩存區, 軟串口的緩存區等都是用 RAM,
當你發現 RAM 好像不太夠用(程序會莫名其妙死機 !),
要儘量把不會變的資訊放在 Flash/ROM 的程序碼空間!
方法很簡單, 例如: (注意, 變數不可以喔 !)
const long haha PROGMEM = 1234567;
const PROGMEM unsigned int charSet[] = { 65000,
32796, 16843, 10, 11234};
http://arduino.cc/en/Reference/PROGMEM
好了, 現在回到 sprintf_P() 這函數,
這函數所要用的 format 格式資訊是放在 Flash/ROM,
const char fmt[ ] PROGMEM = "Your Weight=%s=";
/// 以下是延續前面(七)的(D)最後那 sprintf( ) 換為 sprintf_P( )
sprintf_P(cgy, fmt, ctmp); // 最後也故意有個=等號
(九)為何 Arduino 的 printf/sprintf 不支援 float / double / long long ?
(A)前面說過 Arduino 的 double 根本是騙人的,
因為 Arduino 的 CPU 是 8 bit CPU, 意思是大部分指令都只處理 8 bit,
float 用 32 bit 已經很辛苦, double 用 64 bit豈不更辛苦 !?
參考用百度查詢 "IEEE 754" 看看就知道了!
(B)在標準 C 的程序庫大約有一百多個函數(不算入 C++ 的喔),
但是, 標準 C 的 printf( ) 卻多達兩千多行(包括相關的sprintf/vsprintf等) !
想一想, 如果 Arduino 也讓你真的可以像在 PC 或大型電腦上
使用 printf( )/sprintf( ) 的所有功能,
那你的 32KB 程序碼空間可能就去掉六分之一囉!
(C)其實就算是現在精簡版的 printf/sprintf 也占用約1.5KB,
只要你的程序碼用了一行 sprintf( ) 或 printf( ),
當然多用幾行並不會再增加太多(只是多了參數傳遞與函數調用)!
(D)前面用到的 dtostrf( ) 本身也占用大約 1.5KB;
所以你可以故意用一行 dtostrf( ) 再重新編譯看看它佔多少空間 !?
(E)啥? 你說反正 Flash/ROM 程序碼空間有 31.5KB 不怕喔 !?
(以 Arduino IDE 1.0.6 為例, 有 32256 Bytes = 32768 - 512 Byte BootLoader)
要注意, 使用C++的 String 字符串也是要多用大約 1.5KB的空間!
當你隨著傳感器或GSM/Wifi/Ethernet 一直加上去,
例如, 用了 String 了(這好像比較好用吧),
那就儘量不要再用 dtostrf( ) 以及 sprintf( );
則可能也不要用 String 類別, 因為 String 類別不但多用了 1.5KB,
而且它比傳統 C 的字符串(就是 char array[ ])處理慢數倍! (這我以前有寫過 !)
只是傳統 C 的 strcat( ), strcpy( ), strncpy( ) 使用要很小心,
且對大多數 Arduino 的入門者也不好用 :-(
所以空間足夠時當然先用 C++ 的 String 來處理方便多了 !