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

используя микроконтроллер (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 деление. Есть ли способ, который был бы быстрее?

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

9 ответов


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

что-то вроде этого:

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] = '';

  return tmp;
}

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


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

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


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

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


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

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

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

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

статистика:

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

Perf:

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

  • отдела 2.15
  • вычитание 4.87
  • мое решение 1.56
  • Поиск грубой силы 0.36

я увеличил количество итераций до 200000, чтобы убедиться, что у меня не было проблем с временным разрешением, и мне пришлось добавить volatile сигнатурам функций, чтобы компилятор не оптимизировал цикл. Я использовал настройки VS2010 express w/ vanilla "release", на 3GHz dual core 64 бит Windows 7 машина (tho она скомпилирована до 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] = '';
}

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);
  }
}

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


если корректно в диапазоне (0..1023), тогда ваше последнее преобразование излишне расточительно для делений; последняя строка может быть заменена на:

temp[3] = 1023 / 1000;

или еще:

temp[3] = 1023 >= 1000;

поскольку деление-это повторное вычитание, но у вас есть очень особый случай (а не общий случай) деления, я бы соблазнился сравнить тайминги для следующего кода с версией деления. Я замечаю, что вы помещаете цифры в строку в ' reverse order' - наименьшая значимая цифра идет в 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% и стоит иметь).


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

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


Я заменил свой предыдущий ответ на лучший. Этот код создает 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));
}

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