在C语言中转换为ASCII

7
使用微控制器(PIC18F4580),我需要收集数据并将其发送到SD卡进行后续分析。它收集的数据将具有0到1023之间的值,即0x0和0x3FF。
所以我需要做的是将1023转换为基于10进制的ASCII值字符串(0x31、0x30、0x32、0x33等)。
我的问题是,我想到的唯一将数字拆分的方法需要大量除法。
char temp[4];
temp[0] = 1023 % 10;
temp[1] = (1023 % 100) / 10;
temp[2] = (1023 % 1000) / 100;
temp[3] = (1023 % 10000) / 1000;

使用这种方法,查找n位十进制数的ASCII值需要进行2n-1次除法。是否有一种更快的方法?
最终目标是得到一个.csv文件,存储在SD卡上,可以快速地插入任何笔记本电脑中,在Excel中查看数据的图表。

@strager: 只有在对数据进行字符串操作时才需要。将4个字节写入SD卡不需要终止符。 - Clifford
@Merlyn,链接中给出的itoa实现方法与我上面所做的大致相同,只是它可以适用于任何大小的int,但需要额外进行一次除法。 - John Moffitt
@Merlyn,如果sprintf可以将int转换为以空字符结尾的“字符串”,那就太完美了,只要它背后的魔法不比一堆除法更糟糕。 - John Moffitt
@Merlyn:Microchip的MPLAB C18库支持itoa()函数:http://ww1.microchip.com/downloads/en/devicedoc/mplab_c18_libraries_51297f.pdf,但你可以打赌它仍然执行%和/操作。尽管如此,它应该被考虑。另一方面,sprintf()很可能有一个大的堆栈开销。itoa()将更有效,因为它更专业化。 - Clifford
1
你想要将 temp 数组反转吗?个位数放在 temp[0],十位数放在 temp[1],以此类推。例如:42 ==> temp[0] = '2'; temp[1] = '4'; temp[2] = '0'; temp[3] = '0'; - pmg
显示剩余4条评论
9个回答

15

明显的解决方案并不是将数据转换为ASCII格式,而是以二进制格式存储。这样你只需要担心数据的字节序问题。如果执行后续分析的系统比嵌入式目标更强大,那么让该系统处理转换和字节序的问题是有道理的。

另一方面,除法和取模的执行时间可能与将数据传输到SD卡所需的时间相比微不足道;因此确保你正在优化正确的事情。


我考虑过这个问题,但最好的方法是能够简单地将SD卡拔出并插入笔记本电脑中,在Excel中查看它。您可能对与传输速度到SD卡相关的分区的微不足道感到正确,但是微控制器将同时从多达11个传感器中提取数据,并且还需要转换时间值,因此每个传感器轮询大约需要200个分区。之后,我需要尽可能快地轮询所有传感器。 - John Moffitt
1
@John:30行C或C++代码编译成一个Windows可执行文件,存储在SD卡上并将数据转换为Excel友好格式。 :) - Nicholas Knight
@Nick,我一直很专注于使这个程序便携且易用,却忘记了所有的操作都将在SD卡上进行。这是一个解决方案。 - John Moffitt
1
一个相关的选项是将它们以十六进制形式输入,此时转换需要除以16 -- 这在编译时只编译为一个位移操作,而不是一个“真正”的除法。然后可以使用Excel的HEX2DEC()转换函数进行转换。 - Brooks Moses
鉴于其他列出的限制条件,这是正确的解决方案。 - Joshua
显示剩余2条评论

4

有一种更快的方法:预先计算1024个字符串的数组。然后您只需要进行边界检查,然后对数组进行索引。

从您的问题中无法确定您的代码是否正在微控制器上运行。如果是这种情况,则可能没有足够的内存来使用此方法。


是的,代码将在微控制器上运行,但它只有32KB的闪存。这样一个数组会使用4KB,对吗? - John Moffitt
@John:如果嵌入终止零,那么它的大小将为5kb。如果您编写一个例程以附加终止空值,或者在使用时不需要终止空值,那么可以缩小到4kb。 - Merlyn Morgan-Graham
@John:我认为Clifford的回答是最好的,但你能不能把查找表存储在SD卡上呢?(我假设你有足够的RAM来加载它。) - Nicholas Knight
@Nick,我只有1536B的RAM可用:P也许我可以通过WinRAR运行它。 - John Moffitt
3
四位数的数组不需要以零结尾;你知道没有超过四个字符的数字。此外,如果计算前导1的存在并使用查找表处理剩下的三位数,可以节省一千字节的内存。 - Brooks Moses
@Brooks:我刚添加了一些代码来“计算答案中前导1的存在”。 - Merlyn Morgan-Graham

3
我同意 Clifford 的观点,如果不需要的话,你不必担心优化,而且你可以将日志清理推到分析平台上,而不必担心在嵌入式应用程序中格式化。
话虽如此,这里有一篇文章可能对你有用。它使用了循环、移位、加法和分支,具有线性/常数复杂度:http://www.johnloomis.org/ece314/notes/devices/binary_to_BCD/bin_to_bcd.html 另外,我认为制作一些不执行除法、乘法或分支操作,但仍然给出正确答案(0-1024)的代码会很有趣。不能保证这比其他选项更快。这种代码只是一个探索的选择。
我很想看看是否有人能提供一些技巧,使代码更小、需要更少的内存或更少的操作,同时保持其余计数相等或缩小它们:)
统计数据:
- 224字节的常量(不知道代码大小) - 5位移位右 - 3个减法 - 5个按位与 - 4个按位或 - 1个大于比较
性能:
使用 Jonathan Leffler 的答案中的 perf 比较和 itoa 例程,这里是我得到的统计数据:
- 除法 2.15 - 减法 4.87 - 我的解决方案 1.56 - 暴力查找 0.36
我将迭代次数增加到 200000,以确保没有时间分辨率问题,并且必须在函数签名中添加 volatile,以使编译器不优化循环。我使用 VS2010 express w/ vanilla "release" 设置,在一个 3ghz 双核 64 位 Windows 7 机器上运行(尽管它编译成 32 位)。
代码:
#include "stdlib.h"
#include "stdio.h"
#include "assert.h"

void itoa_ten_bits(int n, char s[])
{
  static const short thousands_digit_subtract_map[2] =
  {
    0, 1000,
  };

  static const char hundreds_digit_map[128] =
  {
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
    3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
    4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
    5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
    6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
    7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
    8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
    9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
    0, 0, 0,
  };

  static const short hundreds_digit_subtract_map[10] =
  {
    0, 100, 200, 300, 400, 500, 600, 700, 800, 900,
  };

  static const char tens_digit_map[12] =
  {
    0, 1, 2, 3, 3, 4, 5, 6, 7, 7, 8, 9,
  };

  static const char ones_digit_map[44] =
  {
    0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
    0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
    0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
    0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
    0, 1, 2, 3
  };

  /* Compiler should optimize out appX constants, % operations, and + operations */
  /* If not, use this:
    static const char ones_digit_append_map[16] =
    {
      0, 6, 2, 8, 4, 10, 6, 12, 8, 14, 10, 16, 12, 18, 14, 20,
    };
  */
  static const char a1 = 0x10 % 10, a2 = 0x20 % 10, a3 = 0x40 % 10, a4 = 0x80 % 10;
  static const char ones_digit_append_map[16] =
  {
    0, a1, a2, a1 + a2,
    a3, a1 + a3, a2 + a3, a1 + a2 + a3,
    a4, a1 + a4, a2 + a4, a1 + a2 + a4,
    a3 + a4, a1 + a3 + a4, a2 + a3 + a4, a1 + a2 + a3 + a4,
  };

  char thousands_digit, hundreds_digit, tens_digit, ones_digit;

  assert(n >= 0 && n < 1024 && "n must be between [0, 1024)");
  /* n &= 0x3ff; can use this instead of the assert */

  thousands_digit = (n >> 3 & 0x7f) > 0x7c;
  n -= thousands_digit_subtract_map[thousands_digit];

  ones_digit = ones_digit_map[
    (n & 0xf)
      + ones_digit_append_map[n >> 4 & 0xf]
      + ones_digit_append_map[n >> 8 & 0x3]
    ];
  n -= ones_digit;

  hundreds_digit = hundreds_digit_map[n >> 3 & 0x7f];
  n -= hundreds_digit_subtract_map[hundreds_digit];

  tens_digit = tens_digit_map[n >> 3];

  s[0] = '0' | thousands_digit;
  s[1] = '0' | hundreds_digit;
  s[2] = '0' | tens_digit;
  s[3] = '0' | ones_digit;
  s[4] = '\0';
}

int main(int argc, char* argv)
{
  int i;
  for(i = 0; i < 1024; ++i)
  {
    char blah[5];
    itoa_ten_bits(i, blah);
    if(atoi(blah) != i)
      printf("failed %d %s\n", i, blah);
  }
}

thousands_digit_low_maskthousands_digit_high_maskthousands_digit_subtract_map可以分别简化为一个>比较和两个==比较。hundreds_digit_subtract_map可以选择简化为乘法。此外,在处理ASCII字符时,最好使用+而不是|。这个解决方案相当疯狂,不得不说! - strager
@strager:疯狂就是关键 :) 至于那些表格,我(也许没有理由)试图避免分支,但当时我没有想到仅仅获取这些比较的结果可能不会导致分支。谢谢你的提示!我还(也许错误地)假设加法比按位或更昂贵。 - Merlyn Morgan-Graham

2
通过仔细寻找正确的数字,您可以使用基数的倒数进行乘法运算,而不是除以基数。Terje的代码是针对x86的,但将这个通用想法移植到PIC应该不会非常困难。

如果我能够解密这背后的逻辑,它可能会很有前途。我会把它发给我的一个朋友看看他是否能够理解它。 - John Moffitt

2

有一种方法可以使用减法来实现,但我不确定它比在“普通”CPU上使用减法和模数更快(在嵌入式环境中可能会有所不同)。

类似于这样:

char makedigit (int *number, int base)
{
  static char map[] = "0123456789";
  int ix;

  for (ix=0; *number >= base; ix++) { *number -= base; }

  return map[ix];
}


char *makestring (int number)
{
  static char tmp[5];

  tmp[0] = makedigit(&number, 1000);
  tmp[1] = makedigit(&number, 100);
  tmp[2] = makedigit(&number, 10);
  tmp[3] = makedigit(&number, 1);
  tmp[4] = '\0';

  return tmp;
}

接下来,调用makestring()函数应该返回一个转换后的字符串(静态字符串,所以在覆盖之前需要复制),该字符串为零填充、4个字符宽度的数字(因为原始假设是0-1023范围内的值)。


这并不是在“嵌入式环境”中普遍存在的问题,而是由于讨厌的PIC18指令集没有DIV指令,因此这些操作需要大量周期。我不确定这个解决方案通常是否更快,但它取决于数字的大小;这可能在某些应用程序中是个问题。 - Clifford
这大概是O(log n),对于0-1023的范围,你最多只需要进行27次减法和28次比较(如果我算对了的话)。 - Vatine
我们最终实现了这个解决方案。 - John Moffitt
为什么使用昂贵的 map[ix] 而不是 '0'+ix - R.. GitHub STOP HELPING ICE
@a.. (a) 不想受ASCII的限制(所发布的方法在EDCDIC以及ASCII、PETSCII和任何其他字符集中都适用)(b) 我觉得它稍微可读一些(c) 如果这是瓶颈,那就进行优化。 - Vatine
@Vatine:这并不是假设ASCII。C语言要求数字'0''9'的值是连续的,当然所有现有的遗留编码都满足这个要求。 - R.. GitHub STOP HELPING ICE

2
如果值在正确的范围内(0..1023),那么你最后一次转换中的除法是不必要的浪费;最后一行可以替换为:
temp[3] = 1023 / 1000;

甚至更多:
temp[3] = 1023 >= 1000;

由于除法是重复减法,但你有一个非常特殊的情况(不是一般情况)需要处理,我建议比较以下代码与除法版本的时间。我注意到你将数字按“反向顺序”放入字符串中 - 最低有效位放在temp [0]中,最高有效位放在temp [4]中。此外,考虑到存储方式,没有机会使字符串以空字符结尾。这段代码使用了8个字节的静态数据表,比许多其他解决方案少得多。

void convert_to_ascii(int value, char *temp)
{
    static const short subtractors[] = { 1000, 100, 10, 1 };
    int i;
    for (i = 0; i < 4; i++)
    {
        int n = 0;
        while (value >= subtractors[i])
        {
            n++;
            value -= subtractors[i];
        }
        temp[3-i] = n + '0';
    }
}

性能测试 - 英特尔x86_64 Core 2 Duo 3.06 GHz(MacOS X 10.6.4)

这个平台可能不代表您的微控制器,但测试显示,在此平台上,减法比除法慢得多。

void convert_by_division(int value, char *temp)
{
    temp[0] = (value %    10)        + '0';
    temp[1] = (value %   100) /   10 + '0';
    temp[2] = (value %  1000) /  100 + '0';
    temp[3] = (value % 10000) / 1000 + '0';
}

void convert_by_subtraction(int value, char *temp)
{
    static const short subtractors[] = { 1000, 100, 10, 1 };
    int i;
    for (i = 0; i < 4; i++)
    {
        int n = 0;
        while (value >= subtractors[i])
        {
            n++;
            value -= subtractors[i];
        }
        temp[3-i] = n + '0';
    }
}

#include <stdio.h>
#include <timer.h>
#include <string.h>

static void time_convertor(const char *tag, void (*function)(void))
{
    int r;
    Clock ck;
    char buffer[32];

    clk_init(&ck);
    clk_start(&ck);
    for (r = 0; r < 10000; r++)
        (*function)();
    clk_stop(&ck);
    printf("%s: %12s\n", tag, clk_elapsed_us(&ck, buffer, sizeof(buffer)));
}

static void using_subtraction(void)
{
    int i;
    for (i = 0; i < 1024; i++)
    {
        char temp1[4];
        convert_by_subtraction(i, temp1);
    }
}

static void using_division(void)
{
    int i;
    for (i = 0; i < 1024; i++)
    {
        char temp1[4];
        convert_by_division(i, temp1);
    }
}

int main()
{
    int i;

    for (i = 0; i < 1024; i++)
    {
        char temp1[4];
        char temp2[4];
        convert_by_subtraction(i, temp1);
        convert_by_division(i, temp2);
        if (memcmp(temp1, temp2, 4) != 0)
            printf("!!DIFFERENCE!! ");
        printf("%4d: %.4s %.4s\n", i, temp1, temp2);
    }

    time_convertor("Using division   ", using_division);
    time_convertor("Using subtraction", using_subtraction);

    time_convertor("Using division   ", using_division);
    time_convertor("Using subtraction", using_subtraction);

    time_convertor("Using division   ", using_division);
    time_convertor("Using subtraction", using_subtraction);

    time_convertor("Using division   ", using_division);
    time_convertor("Using subtraction", using_subtraction);

    return 0;
}

使用GCC 4.5.1编译,并在32位下运行,平均计时如下(优化'-O'):

  • 使用除法:0.13
  • 使用减法:0.65

使用GCC 4.5.1编译,并在64位下运行,平均计时如下:

  • 使用除法:0.13
  • 使用减法:0.48

显然,在这台机器上,使用减法不是一个胜利的选择。您需要在自己的机器上进行测试以做出决策。如果删除模10000操作,结果将只会偏向除法(当用比较替换时,它可以节省约0.02秒的时间;这相当于15%的节省,值得一试)。


你的例程似乎以相反的顺序输出和打印值。此外,我在我的答案中添加了性能数据,与你的例程进行比较。感谢提供基准代码 :) - Merlyn Morgan-Graham
@Merlyn: 是的,我输出的字节顺序与你在问题中的示例代码相同-将最低有效位放在temp [0]中,最高有效位放在temp [3]中。这有点奇怪,但是顺序与您的示例代码匹配,我所做的只是添加“0”以转换为可打印数字。微控制器是否具有内置除法?如果是这样,除法可能比重复减法快。 - Jonathan Leffler
@Merlyn:我已经下载了PIC18F4580的数据表,似乎它有一个8x8位乘法器,但没有提到除法。据我所知,它基本上是一个8位芯片 - 我没有看到任何直接16位加减的东西,但有一个10位DAC。因此,即使处理范围在0..1023之间的数字也需要多个操作。或者我错过了什么?无论如何,通用软件除法的速度不会比建议的减法快多少 - 反而可能更慢。如果您使用更多的数据空间,可以加快速度 - 以空间换时间! - Jonathan Leffler
@Jonathan:我只是提供了一个答案,而不是原始问题,尽管我现在看到你的字节顺序与问题相匹配。 - Merlyn Morgan-Graham
@Merlyn:抱歉 - 没有仔细看。 - Jonathan Leffler
显示剩余2条评论

1

你是否必须使用ASCII字符串的十进制表示?使用十六进制格式存储会更容易。不需要除法,只需要(相对便宜的)移位操作。如果在每个数字前加上“0x”,Excel应该能够读取它。


1

你特别关心这个的原因是什么?

如果你的编译器和C库提供了itoa()函数,那就使用它,然后如果出现某些情况导致它太慢或者无法适应RAM等问题,再考虑编写这段代码(以及相关测试等)。


1
我已经用更好的答案替换了以前的答案。这段代码按正确的顺序创建了一个4个字符的字符串,最重要的数字位于output[0]中,最不重要的数字位于output[3]中,并在output[4]中加入了零终止符。我不知道您的PIC控制器或C编译器有什么特殊要求,但这段代码只需要16位整数、加法/减法和移位即可。
int x;
char output[5];
output[4] = 0;
x = 1023;
output[3] = '0' + DivideByTenReturnRemainder(&x);
output[2] = '0' + DivideByTenReturnRemainder(&x);
output[1] = '0' + DivideByTenReturnRemainder(&x);
output[0] = '0' + x;

关键在于神奇的函数DivideByTenReturnRemainder。即使不明确使用除法,仍然可以通过右移来除以2的幂;问题是10不是2的幂。我通过将值乘以25.625而不是256来避开了这个问题,让整数截断向下舍入到正确的值。为什么是25.625?因为它很容易用2的幂表示。25.625 = 16 + 8 + 1 + 1/2 + 1/8。同样,乘以1/2相当于向右移动一位,乘以1/8相当于向右移动3位。要获得余数,将结果乘以10(8+2),并从原始值中减去它。
int DivideByTenReturnRemainder(int * p)
{
    /* This code has been tested for an input range of 0 to 1023. */
    int x;
    x = *p;
    *p = ((x << 4) + (x << 3) + x + (x >> 1) + (x >> 3)) >> 8;
    return x - ((*p << 3) + (*p << 1));
}

看起来这个方法平均应该比Vatine的解决方案更快。我已经快完成这部分的代码了,但是一旦我验证它是否有效,我会尝试使用这种方法。 - John Moffitt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接