index
https://www.arduino.cn/thread-14644-1-1.html
关于类似 printf( ); 的输出方式, 虽然本站站长奈何大神有写了一篇关于格式化输出:
http://www.arduino.cn/thread-8366-1-2.html
但是我觉得那篇写得不好, 因为虽然那篇让你多知道一点秘密, 但是不好用 !
1. 简单用法, 不注重格式, 只要有印出就好
要格式化输出,最简单的方法就是直接用 C++ 的 String( ) 串接功能即可:
int height=168;
float weight=72.5;
Serial.println(String("")+"Your Height="+height + ", and Weight=" + weight);
- 说明: 只要第一个是 String(""), 之后不论整数 int, long, 实数 float 等都会自动转为字符串, 用 + 串接在一起 !
- 缺点: float 小数点后会印出几位无法控制 ,( 注意 UNO 如果用 double 其实会被偷改为 float )
2. 那如果是要印到 LCD 或 SoftwareSerial 软串口甚至 SPI 呢?
简单, 先放到 String 字符串即可, 之后爱怎样就怎样 :
int height=168;
float weight=72.5;
String gy = String("")+"Your Height="+height + ", and Weight=" + weight;
Serial.println(gy); // 印到串口
LCD.print(gy); // 假设你已经有 LCD. 可以用
3. 如果实数float要印出小数点后两位呢?
只是看起来有点麻烦而已(其实你用 Serial.print(float) 它也是偷偷类似这样做 **注!):
int height=168;
float weight=72.5;
String gy = String("")+"Your Height="+height;
gy += ", and Weight=";
// 开始处理 float weight 的值
long tmp = weight; // 整数部分
long yytmp = (weight -tmp)*100+0.5; // 小数部分; 从小数点后第三位做四舍五入(round)到第二位
/// if(yytmp >= 100) yytmp=99; // 改用以下方法处理 ..
if(yytmp >= 100) { // 防错
yytmp=0;
++tmp; // 0.99xyz.. +0.005 ===> 1.0pqr...
} // it is 0.99xyz...
gy += tmp; // 整数部分
gy += "."; // 小数点, 废话
if(yytmp < 10) gy += "0";
gy += yytmp;
Serial.println(gy); // 印到串口
LCD.print(gy); // 假设你已经有 LCD. 可以用
**注: 严格说来 Serial.print(float); 就是 Serial.print(float, 2);
这时它是用以下(5.)说的用AVR的 dtostrf( ) 函数把 float 转换为包括小数点后两位的字符串!
4. 使用 sprintf( ) 印到 C 的字符串 (注意, 不是 C++ 的 String 喔!)
4.1用法
但是, 这招虽然在标准 C 没问题, 可是在 Arduino 上不可以用在 float, double, 以及 long long 都不行!
所以, 其实用这招除了可以 %5d 或 %8d 等这样格式之外, 并没有比前面用 String( ) 方法好用 !
int height=168;
float weight=72.5;
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. 可以用
4.2 注意 float 与 double 无效, 但印整数时格式仍可能有类似 %8.5d 喔!
自己把上面(4.1)的 sprintf( ) 改为如下看看:
sprintf(cgy, "Your Height=%8.5d, and Weight=%ld", height, wtmp);
// %8.5d 表示此 int 会用 8 格位置, 但至少印五位, 左边会补 0, 如 00168
// 注意, 如果 long 对应的格式写成 %d 则只会印出其右边 2 bytes 的值!!
5. 我就想要类似 %6.3f 印实数 float / double 可不可以 ?
** 如果你只是要印一个实数印到小数点后第三位, 那这样就可以了:
Serial.print(float, 3); // 把 float 打印到串口, 只到小数点后第三位!
**在 UNO 上其实 double 会被偷改为 float, 所以其实没有 double 可用 !
那也不难, 偷用 AVR Library 内的 dtostrf( ) 先把 float 转为 C 的字符串即可:
int height = 168;
float weight = 72.12666; // 故意
char cgy[66];
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
// 也可用来 print
Serial.println(cgy); // 印到串口
LCD.print(cgy); // 假设你已经有 LCD. 可以用
就这样, 够简单吧 !?
参考:
5.1 vfprintf()
关于 format 格式字符串: http://www.atmel.com/webdoc/AVRLibcReferenceManual/group__avr__stdio_1gaa3b98c0d17b35642c0f3e4649092b9f1.html
5.2 其他 *printf( )
- http://www.atmel.com/webdoc/AVRLibcReferenceManual/group__avr__stdio_1ga6017094d9fd800fa02600d35399f2a2a.html
- http://www.atmel.com/webdoc/AVRLibcReferenceManual/group__avr__stdio_1gaa3b98c0d17b35642c0f3e4649092b9f1.html
- http://www.atmel.com/webdoc/AVRLibcReferenceManual/group__avr__stdio_1ga2b829d696b17dedbf181cd5dc4d7a31d.html
- http://www.atmel.com/webdoc/AVRLibcReferenceManual/group__avr__stdlib_1ga060c998e77fb5fc0d3168b3ce8771d42.html
5.3 Prototype of the function dtostrf( )
char * dtostrf(
double __val,
signed char __width,
unsigned char __prec,
char * __s);
5.4 Prototype of the function sprintf( )
int sprintf(
char * __s,
const char * __fmt,
... );
5.5 Function sprintf_P()
http://www.atmel.com/webdoc/AVRLibcReferenceManual/group__avr__stdio_1ga2b829d696b17dedbf181cd5dc4d7a31d.html
6.实数float要印到小数点后第四位怎办 ?
ㄟ, 阿我前面已经有(3)印到两位和(5)印到三位的范例,
要印到四位参考着写应该就会啦 !
不过我还是来写些比较深入的让一些比较好奇的 Arduino 爱好宝宝满足好奇心 :-)
6.1最简单方法是先把 float 稍微加工处理后用 String 串接, 如下:
float weight=72.12666; // 故意
long prec = 10000; // 四位
float aw = ((long)(weight*prec+0.5))/1.0/prec; //注意
String ans = String("Weight=") + aw;
Serial.println(ans);
LCD.print(ans);
优点:
简单, 要小数点后两位就 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; 可是 float 的有效精准度只有 7 位左右, 就是说 从最左边不是 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”)
6.2 偷用 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);
/// 注意 .substring( )用法 !
/// http://arduino.cc/en/Reference/StringSubstring
Serial.println(ans);
LCD.print(ans);
优点:也还简单, 要小数点后两位就 int precision = 2; 其他以此类推!
缺点:
与前面说的一样, 仍是有float只有七位左右的准确有效位数的问题,
且没有对没印出的第一位做四舍五入(round) !
6.3 使用 AVR (Arduino 的底层)Library dtostrf( )
这在(5)我们已经用过了, 要注意参数习惯不相同就是了!
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, 实际只会用真正长度!
/// 你可以故意再串接一个等号看看:
ans += "="; // 故意再串接一个等号
Serial.println(ans);
LCD.print(ans);
6.4 把刚刚©的后半段改用如(6)的方法用 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 虽然整数部分只有一位, 但不够用会子几长大 :-)
char cgy[66];
sprintf(cgy, "Your Weight=%s=", ctmp); // 最后也故意有个=等号
Serial.println(cgy);
LCD.print(cgy);
8. 何时该使用 sprintf_P( ) 呢 ?
如果你已经会使用 sprintf( ) 先把一个甚至多个信息格式法到字符串, 那可能很想知道 sprintf( ) 与 sprintf_P( ) 有何差别 ? !
其实两者几乎一样, 但 sprintf_P( ) 是让你节省 RAM 来的,
因为大部分 Arduino 的 Flash (ROM) 是 32KB, 扣掉 BootLoader 还有 31.5KB;
可是 RAM 只有 2KB, 通常变数都放 RAM, 硬件串口的缓存区, 软串口的缓存区等都是用 RAM,
String 字符串更是须用到不少的 RAM, 所以, RAM 应该省着用 : -)
当你发现 RAM 好像不太够用(程序会莫名其妙死机 !),
但程序码空间(Flash / ROM)还不少的时候,
要尽量把不会变的信息放在 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); // 最后也故意有个=等号
Serial.println(cgy);
LCD.print(cgy);
9. 为何 Arduino 的 printf/sprintf 不支援 float / double / long long ?
9.1 前面说过 Arduino 的 double 根本是骗人的,
因为 Arduino 的 CPU 是 8 bit CPU, 意思是大部分指令都只处理 8 bit, float 用 32 bit 已经很辛苦, double 用 64 bit岂不更辛苦 !? 参考用百度查询 “IEEE 754” 看看就知道了!
9.2 在标准 C 的程序库大约有一百多个函数(不算入 C++ 的喔),
其中很多函数都是一行两行就做完了,
但是, 标准 C 的 printf( ) 却多达两千多行(包括相关的sprintf/vsprintf等) !
想一想, 如果 Arduino 也让你真的可以像在 PC 或大型计算机上
使用 printf( )/sprintf( ) 的所有功能,
那你的 32KB 程序码空间可能就去掉六分之一啰!
9.3 其实就算是现在精简版的 printf/sprintf 也占用约1.5KB,
只要你的程序码用了一行 sprintf( ) 或 printf( ),
你编译出的机器码就会多大约 1.5KB,
当然多用几行并不会再增加太多(只是多了参数传递与函数调用)!
9.4 前面用到的 dtostrf( ) 本身也占用大约 1.5KB;
所以你可以故意用一行 dtostrf( ) 再重新编译看看它占多少空间 !?
9.5 啥? 你说反正 Flash/ROM 程序码空间有 31.5KB 不怕喔 !?
(以 Arduino IDE 1.0.6 为例, 有 32256 Bytes = 32768 - 512 Byte BootLoader)
如果简单程序码当然没问题啦 !
要注意, 使用C++的 String 字符串也是要多用大约 1.5KB的空间!
当你随着传感器或GSM / Wifi / Ethernet 一直加上去,
你会发现很快的, 31.5KB 好像不太够用了!
这时你就要想办法尽量节省着用啰,
能不用的当然就不用 !
例如, 用了 String 了(这好像比较好用吧),
那就尽量不要再用 dtostrf( ) 以及 sprintf( );
如果空间真的不够用, 且你须要更快的执行效能,
则可能也不要用 String 类别, 因为 String 类别不但多用了 1.5KB,
而且它比传统 C 的字符串(就是 char array[ ])处理慢数倍! (这我以前有写过 !)
只是传统 C 的 strcat( ), strcpy( ), strncpy( ) 使用要很小心,
且对大多数 Arduino 的入门者也不好用 :
所以空间足够时当然先用 C++ 的 String 来处理方便多了 !
如果你要研究 dtostrf( ) 以及其用到的 dtoa_prf ( );
9.6 可以再参考:
-
https://github.com/vancegroup-mirrors/avr-libc/blob/master/avr-libc/libc/stdlib/dtostrf.c
-
https://android.googlesource.com/toolchain/avr-libc/+/edcf5bc1c8da8cc4c8b560865d2a54b73c1b51d3/avr-libc-1.7.1/libc/stdlib/dtoa_prf.c
// 为了方便想研究的查看, 复制到以下…
char *
dtostrf (double val, signed char width, unsigned char prec, char *sout)
{
unsigned char flags;
/* DTOA_UPPER: for compatibility with avr-libc <= 1.4 with NaNs */
flags = width < 0 ? DTOA_LEFT | DTOA_UPPER : DTOA_UPPER;
dtoa_prf (val, sout, abs(width), prec, flags);
return sout;
}
// If precision is < 0, the string is left adjusted with leading spaces.
// If precision is > 0, the string is right adjusted with trailing spaces.
// 以下是 int dtoa_prf ( )
#include "ftoa_engine.h"
#include "dtoa_conv.h"
#include "sectionname.h"
int
dtoa_prf (double val, char *s, unsigned char width, unsigned char prec,
unsigned char flags)
{
int exp;
int n;
unsigned char vtype;
unsigned char sign;
unsigned char ndigs;
unsigned char buf[9];
ndigs = prec < 60 ? prec + 1 : 60;
exp = __ftoa_engine (val, (char *)buf, 7, ndigs);
vtype = buf[0];
sign = 0;
if ((vtype & (FTOA_MINUS | FTOA_NAN)) == FTOA_MINUS)
sign = '-';
else if (flags & DTOA_PLUS)
sign = '+';
else if (flags & DTOA_SPACE)
sign = ' ';
if (vtype & FTOA_NAN) {
ndigs = sign ? 4 : 3;
width = (width > ndigs) ? width - ndigs : 0;
if (!(flags & DTOA_LEFT)) {
while (width) {
*s++ = ' ';
width--;
}
}
if (sign) *s++ = sign;
if (flags & DTOA_UPPER) {
*s++ = 'N'; *s++ = 'A'; *s++ = 'N';
} else {
*s++ = 'n'; *s++ = 'a'; *s++ = 'n';
}
while (width) {
*s++ = ' ';
width--;
}
*s = 0;
return DTOA_NONFINITE;
}
if (vtype & FTOA_INF) {
ndigs = sign ? 4 : 3;
width = (width > ndigs) ? width - ndigs : 0;
if (!(flags & DTOA_LEFT)) {
while (width) {
*s++ = ' ';
width--;
}
}
if (sign) *s++ = sign;
if (flags & DTOA_UPPER) {
*s++ = 'I'; *s++ = 'N'; *s++ = 'F';
} else {
*s++ = 'i'; *s++ = 'n'; *s++ = 'f';
}
while (width) {
*s++ = ' ';
width--;
}
*s = 0;
return DTOA_NONFINITE;
}
n = (sign ? 1 : 0) + (exp > 0 ? exp + 1 : 1) + (prec ? prec + 1 : 0);
width = width > n ? width - n : 0;
if (!(flags & DTOA_LEFT) && !(flags & DTOA_ZFILL)) {
while (width) {
*s++ = ' ';
width--;
}
}
if (sign) *s++ = sign;
if (!(flags & DTOA_LEFT)) {
while (width) {
*s++ = '0';
width--;
}
}
ndigs += exp; /* exp is resticted approx. -40 .. +40 */
sign = buf[1];
if ((vtype & FTOA_CARRY) && sign == '1')
ndigs -= 1;
if ((signed char)ndigs < 1)
ndigs = 1;
else if (ndigs > 8)
ndigs = 8;
n = exp > 0 ? exp : 0;
do {
if (n == -1)
*s++ = '.';
flags = (n <= exp && n > exp - ndigs) ? buf[exp - n + 1] : '0';
if (--n < -prec)
break;
*s++ = flags;
} while (1);
if ( n == exp && (sign > '5' || (sign == '5' && !(vtype & FTOA_CARRY))) )
flags = '1';
*s++ = flags;
while (width) {
*s++ = ' ';
width--;
}
*s++ = 0;
return 0;
}