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

Используя микроcontroller (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.

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

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

 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) ,

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

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

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

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

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

Это, как говорится, вот статья, которая может быть вам полезна. Он использует цикл, сдвиги, дополнения и ветви с линейной / постоянной сложностью: 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 w / vanilla «release», на трехъядерном двухъядерном 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); } } 

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

Если значения правильны в диапазоне (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)

Эта платформа, вероятно, не является представителем вашего микроcontrollerа, но тест показывает, что на этой платформе вычитание происходит значительно медленнее, чем деление.

 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  #include  #include  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 секунды с вычитанием

Ясно, что на этой машине использование вычитания не является выигрышным предложением. Вы должны будете измерить на своей машине, чтобы принять решение. И удаление операции modulo 10000 будет только искажать результаты в пользу деления (он сбрасывает примерно 0,02 секунды с момента деления при замене на сравнение, это 15% экономии и стоит иметь).

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

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

Я заменил свой предыдущий ответ на лучший. Этот код создает 4-значную строку в правильном порядке, наиболее значимую цифру на выходе [0] – наименее значимую на выходе [3] с нулевым терминатором на выходе [4]. Я ничего не знаю о вашем controllerе 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 десятичного представления? Было бы намного проще сохранить его в шестнадцатеричном формате. Не требуется никакого разделения, только (относительно дешевые) операции смены. Excel должен иметь возможность читать его, если вы добавили «0x» к каждому номеру.