用String或sprintf( )与dtostrf( )做类似printf( )格式化输出


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);
  1. 说明: 只要第一个是 String(""), 之后不论整数 int, long, 实数 float 等都会自动转为字符串, 用 + 串接在一起 !
  2. 缺点: 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( )

  1. http://www.atmel.com/webdoc/AVRLibcReferenceManual/group__avr__stdio_1ga6017094d9fd800fa02600d35399f2a2a.html
  2. http://www.atmel.com/webdoc/AVRLibcReferenceManual/group__avr__stdio_1gaa3b98c0d17b35642c0f3e4649092b9f1.html
  3. http://www.atmel.com/webdoc/AVRLibcReferenceManual/group__avr__stdio_1ga2b829d696b17dedbf181cd5dc4d7a31d.html
  4. 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 可以再参考:

  1. https://github.com/vancegroup-mirrors/avr-libc/blob/master/avr-libc/libc/stdlib/dtostrf.c

  2. 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;
}

猜你喜欢

转载自blog.csdn.net/acktomas/article/details/88365120