【问题标题】:Converting to ASCII in C在 C 中转换为 ASCII
【发布时间】:2010-09-12 07:35:45
【问题描述】:

使用微控制器 (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 除法。有没有更快的方法?

这样做的最终目标是在 SD 卡上创建一个 .csv 文件,该文件可以快速插入任何笔记本电脑以在 Excel 中查看数据图表。

【问题讨论】:

  • 您可能需要添加一个空终止符,以防万一。 (否则,您可能依赖于未定义的行为。)
  • @strager:仅当他要对数据执行字符串操作时。向 SD 卡写入 4 个字节不需要终止符。
  • @Clifford,因此我写了“可能”。 @John Moffitt,你绝对需要 base 10 吗?你能以 16 基数或 256 基数定居吗?
  • @all,在本例中,我只需将 0x30 添加到数组中的每个单元格并将其发送到 SD 卡。
  • 你想要temp“反转”吗?在 temp[0] 处的个位,在 temp[1] 处的十位,...? 42 ==> 温度 [0] = '2';温度 [1] = '4';温度[2] = '0';温度[3] = '0';

标签: c embedded ascii microcontroller


【解决方案1】:

明显的解决方案是将数据完全转换为ASCII,而是以二进制格式存储。这样,您只需要担心数据的字节顺序。如果执行后期分析的系统比您的嵌入式目标强大得多,那么让它处理转换和字节顺序是有意义的。

另一方面,与将数据传输到 SD 卡所用的时间相比,/ 和 % 的执行时间可能微不足道;所以请确保您正在优化正确的内容。

【讨论】:

  • 这个。在文件的开头贴一个字节顺序标记,然后开心。
  • 我考虑过这一点,但最好能够简单地将 SD 卡拉出并将其插入笔记本电脑以在 Excel 中查看。您可能是正确的关于与 SD 卡的传输速度相关的部门的微不足道,但是微控制器一次将从多达 11 个传感器中提取数据,并且它还需要将时间值转换为好吧,给我大约 200 个设备,只是为了轮询每个传感器一次。之后,我需要尽快轮询所有传感器。
  • @John:将 30 行 C 或 C++ 编译为 Windows 可执行文件,该可执行文件位于 SD 卡上并转换为 Excel 友好格式。 :)
  • @Nick,我一心想让这个便携和简单,以至于我忘记了所有这些都将发生在 SD 卡上。这是一种解决方案
  • 一个相关的选项是将它们以十六进制形式输入,其中转换需要除以 16——编译为位移位,而不是“真正的”除法。然后对它们使用 Excel 的 HEX2DEC() 转换函数。
【解决方案2】:

当然有一个更快的方法:有一个由 1024 个预先计算的字符串组成的数组。然后你可以做边界检查,然后是数组的索引。

您的问题不清楚您的代码是否在微控制器上运行。如果是这种情况,您可能没有足够的内存来使用这种方法。

【讨论】:

  • 是的,代码将在微控制器上运行,但它有 32KB 的闪存。这样的数组会使用 4KB,对吗?
  • @John:嵌入终止零后,它将是 5kb。如果您将它包装在附加终止 null 的例程中,或者以不需要终止 null 的方式使用它,那么您可以逃脱 4kb。
  • @John:我认为 Clifford's 是最好的答案,但您不能将查找表存储在 SD 卡上吗? (我在这里假设您确实有足够的 RAM 来加载它。)
  • @Nick,我只有 1536B 的 RAM 可以使用:P 也许我可以通过 winrar 运行它。
  • 四位数的数组不需要以零结尾;你知道没有什么比四个字符长。此外,如果您计算前导 1 的存在,然后使用查找表查找剩余的三位数字,则可以节省一千字节的内存。
【解决方案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 添加到函数签名中,以便编译器不会优化循环。我在一台 3ghz 双核 64 位 Windows 7 机器上使用了带有香草“发布”设置的 VS2010 express(但它编译为 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 可以分别简化为一个 &gt; 比较和两个 == 比较。 hundreds_digit_subtract_map 可以选择减少为乘法。此外,在处理 ASCII 字符时,使用 + 而不是 | 可能会更好。至少可以说,这个解决方案非常疯狂!
  • @strager: 疯狂是重点 :) 至于那些表,我(也许没有理由)试图避免分支,但当时我并没有想到只是得到这些比较的结果可能不会导致分支。谢谢你的提示!我还(可能是错误地)假设添加会比按位或更昂贵。
【解决方案4】:

有一种使用减法的方法,但我不相信它比在“普通”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指令,因此这些操作需要大量周期。我不确定这个解决方案通常会更快,但它会根据数字的大小而有所不同;这在某些应用程序中可能是一个问题。
  • 这是(大约)O(log n),在 0-1023 范围内,你看到的减法不超过 27 次,比较不超过 28 次(如果我已经完成了数字对)。
  • 我们最终实施了这个解决方案。
  • 为什么要昂贵的map[ix] 而不是'0'+ix
  • @R.. (a) 不想受 ASCII 的约束(发布的方法在 EDCDIC 以及 ASCII、PETSCII 和任何其他字符集中都有效)(b)我觉得它有点可读( c) 如果这是一个瓶颈,请优化。
【解决方案5】:

在找到要使用的正确数字时要小心,您可以multiply by the reciprocal 的底数,而不是除以底数。 Terje 的代码是针对 x86 的,但将总体思路移植到 PIC 上应该不会非常困难。

【讨论】:

  • 如果我能破译这背后的逻辑,它可能是有希望的。我会把它发给我的一个朋友,看看他能不能把它包起来。
【解决方案6】:

如果值在范围 (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';
    }
}

性能测试 - Intel 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秒使用减法

在 64 位下编译和工作,平均时间为:

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

显然,在这台机器上,使用减法并不是一个成功的命题。您必须在您的机器上进行测量才能做出决定。并且删除模 10000 运算只会使结果偏斜,有利于除法(当用比较替换时,除法时间减少了大约 0.02 秒;节省了 15%,值得拥有)。

【讨论】:

  • 您的例程似乎以相反的显示顺序输出和打印值。此外,我为您的例程添加了性能数据,而不是我在回答中的解决方案。感谢基准代码:)
  • @Merlyn:是的,我以与问题中的示例代码相同的倒序输出字节 - 将最低有效数字放入 temp[0] 并将最高有效数字放入 temp[3 ]。这有点奇怪,但顺序与您的示例代码中的内容相匹配 - 我所做的只是添加“0”以转换为可打印的数字。微控制器是否有内置分频器?如果是这样,除法可能比重复减法更快。
  • @Merlyn:我已经下载了 PIC18F4580 的数据表,它似乎有一个 8x8 位乘法,但没有提到除法。 AFAICS,它基本上是一个 8 位芯片——我没有看到任何直接 16 位加法或减法的方式,但是有一个 10 位 DAC。因此,即使处理 0..1023 范围内的数字似乎也涉及多个操作。还是我错过了什么?无论如何,通用软件部门不会比建议的减法快多少——而且可能会更慢。如果您使用更多数据空间,您可能会加快速度 - 空间换时间!
  • @Jonathan:我只提供了一个答案,而不是原始问题,尽管我现在看到您的字节顺序与问题匹配。
  • @Merlyn:道歉 - 看的不够仔细。
【解决方案7】:

您有什么理由特别关注这个吗?

如果您的编译器和 C 库提供了一个 itoa() 函数,请使用它,然后担心编写此代码(以及相关的测试等以确保您做对了!)如果由于某种原因导致太慢或不适合 RAM 或其他东西。

【讨论】:

    【解决方案8】:

    我已经用一个更好的答案替换了我之前的答案。此代码以正确的顺序创建一个 4 字符字符串,输出 [0] 中的最高有效数字到输出 [3] 中的最低有效数字,输出 [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 的幂。我通过在除以 256 之前将值乘以 25.625 来回避这个问题,让整数截断向下舍入到正确的值。为什么是 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 的解决方案平均更快。我几乎完成了这部分的代码,但是一旦我完成并且我验证它正在工作,我会尝试这个方法。
    【解决方案9】:

    您是否需要使用 十进制 表示的 ASCII 字符串?以十六进制格式存储它会容易得多。不需要分工,只需(相对便宜的)轮班操作。如果您在每个数字前加上“0x”,Excel 应该能够读取它。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2022-12-13
      • 2011-10-03
      • 1970-01-01
      • 2014-03-24
      • 2018-06-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多