Преобразование в ASCII в C - PullRequest
       44

Преобразование в ASCII в C

7 голосов
/ 12 сентября 2010

Используя микроконтроллер (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;

Используя этот метод, найдите значения ASCIIдесятичного числа из n цифр требуется 2n-1 деление.Есть ли способ, который был бы быстрее?

Конечная цель этого - получить файл .csv на SD-карте, который можно быстро подключить к любому ноутбуку, чтобы увидеть график данных в Excel.

Ответы [ 9 ]

15 голосов
/ 12 сентября 2010

Очевидным решением является , а не для преобразования данных в ASCII вообще, но для сохранения их в двоичном формате.Таким образом, все, о чем вам нужно беспокоиться, это порядковый номер данных.Если система, выполняющая более поздний анализ, является гораздо более мощной, чем ваша встроенная цель, то имеет смысл разрешить этой операции преобразование и порядок байтов.

С другой стороны, возможно, что выполнениевремя / и% незначительно по сравнению со временем, которое требуется для передачи данных на SD-карту;поэтому убедитесь, что вы оптимизируете правильную вещь.

4 голосов
/ 12 сентября 2010

Конечно, есть гораздо более быстрый способ: иметь массив из 1024 предварительно вычисленных строк. Затем вы можете просто выполнить проверку границ, а затем индекс в массиве.

Из вашего вопроса неясно, работает ли ваш код на микроконтроллере. Если это так, у вас может не хватить памяти для этого подхода.

3 голосов
/ 13 сентября 2010

Я согласен с тем, что сказал Клиффорд, что вам не нужно беспокоиться об оптимизации, если вам это не нужно, и что вы можете перенести очистку журнала на свою платформу анализа, а не беспокоиться о форматировании во встроенном приложении.

Как говорится, вот статья, которая может быть вам полезна. Он использует цикл, сдвиги, добавления и ответвления, с линейной / постоянной сложностью: http://www.johnloomis.org/ece314/notes/devices/binary_to_BCD/bin_to_bcd.html

Кроме того, я подумал, что было бы интересно создать некоторый код, который не выполняет деления, умножения или ответвления, но все же дает правильный ответ [0 - 1024). Не обещает, что это будет быстрее, чем другие варианты. Этот код просто вариант для изучения.

Хотелось бы посмотреть, могут ли кто-нибудь придумать какие-нибудь хитрости, чтобы сделать код меньше, потребовать меньше памяти или меньше операций, при этом оставляя равными остальные значения или уменьшая их:)

Статистика:

  • 224 байта в константах (без представления о размере кода)
  • 5 бит-прав смещения
  • 3 вычитания
  • 5 поразрядно
  • 4 бит-ор
  • 1 больше, чем сравнение

Перф:

Используя сравнения perf и процедуры itoa в ответе Джонатана Леффлера, вот статистика, которую я получил:

  • Деление 2,15
  • Вычитание 4.87
  • Мое решение 1.56
  • Поиск грубой силы 0.36

Я увеличил количество итераций до 200000, чтобы у меня не было проблем с разрешением синхронизации, и мне пришлось добавить volatile к сигнатурам функций, чтобы компилятор не оптимизировал цикл. Я использовал VS2010 express с ванильными настройками «выпуска» на двухъядерной 64-битной Windows 7-машине с тактовой частотой 3 ГГц (хотя она была скомпилирована в 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);
  }
}
2 голосов
/ 14 сентября 2010

Если значения правильно находятся в диапазоне (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 ГГц (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% и стоит того).

2 голосов
/ 12 сентября 2010

Есть способ сделать это, используя вычитания, но я не уверен, что это быстрее, чем использование вычитаний и модулей на «нормальном» процессоре (может отличаться во встроенной среде).

Примерно так:

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[5] = '\0';

  return tmp;
}

Затем вызов makestring() должен привести к (статической, поэтому скопируйте перед перезаписью) строке с преобразованным числом (с нулевым префиксом, шириной 4 символа, поскольку исходное предположениезначение в диапазоне 0-1023).

2 голосов
/ 12 сентября 2010

С некоторой тщательностью при поиске подходящего числа (й) вы можете умножить на обратную базы, а не делить на базу. Код Терье предназначен для x86, но перенос общей идеи на PIC не должен быть чрезвычайно сложным.

1 голос
/ 16 сентября 2010

Требуется ли вам использовать ASCII-строку представления десятичное ?Было бы намного проще хранить его в шестнадцатеричном формате.Деление не требуется, только (относительно дешевые) сменные операции.Excel должен быть в состоянии прочитать его, если вы добавите «0x» к каждому числу.

1 голос
/ 13 сентября 2010

Я заменил свой предыдущий ответ на лучший.Этот код создает 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. Я обошел эту проблему, умножив значение на 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));
}
1 голос
/ 13 сентября 2010

Есть ли какая-то причина, по которой вас это особенно беспокоит?

Если ваш компилятор и библиотека C предоставляют функцию itoa(), используйте ее, а затем беспокойтесь о написании этого кода (и связанных тестов ии так далее, чтобы убедиться, что вы поняли это правильно!) если по какой-то причине это оказывается слишком медленным или не помещается в ОЗУ или что-то в этом роде.

...