Как стать автором
Обновить
460.04
YADRO
Тут про железо и инженерную культуру

Работа с RISC-V контроллерами на примере GD32VF103 и CH32V303. Часть 6. Дробные числа

Время на прочтение14 мин
Количество просмотров3.1K

Макетная плата GD32VF103


Одно из основных предназначений микроконтроллера — это получение информации извне, ее обработка и выдача реакции. Причем зачастую эта информация представлена не в цифрах, а в терминах реального мира: 3 сантиметра, 101 килопаскаль, 3.6 вольта. Мало того, что информацию надо получить, ее зачастую надо потом отобразить человеку. Вот только подобные аналоговые величины плохо ложатся на целочисленные переменные, с которыми так хорошо работает контроллер. О том, как дробные числа можно закодировать и какие при этом встречаются подводные камни, сегодня и поговорим.


Часть 1. Введение


Часть 2. Память и UART


Часть 3. Прерывания


Часть 4. Си и таймеры


Часть 5. DMA


10.1 IEEE754


Начнем с классического для «компьютерного» программирования решения — тип данных float (а также double и подобные). Наиболее распространенный сегодня формат представления дробных чисел — это IEEE754. Согласно ему, число представляется как мантисса (значащие цифры), порядок и знак. То есть это классическая экспоненциальная форма записи числа. Например, в десятичном числе 1.23·10⁸ мантисса это 1.23, а порядок — 8. Точно так же это выглядит и с двоичными числами: у 1.001011·2¹⁰¹⁰ число 1.001011 — это мантисса, а 1010 — порядок. В экспоненциальной форме мантисса всегда записывается как одна значащая (ненулевая) цифра перед точкой и некоторое количество цифр после. И при использовании двоичной системы счисления это оказывается крайне удобно, ведь там единственная цифра, кроме нуля — единица.


Отсюда возникает первая особенность IEEE754: раз первая цифра мантиссы всегда равна 1, ее можно не хранить. Вторая особенность: для представления отрицательных чисел используется не дополнительный код мантиссы, а отдельный знаковый бит. Вероятно, это связано с тем, что сложение и вычитание всегда требуют выравнивания порядков, то есть сдвигов мантиссы влево-вправо. Да еще подразумевается единица до точки. Все выгоды от дополнительного кода пропадают.


От общих соображений углубимся немного в конкретику. IEEE754 регламентирует размер каждого поля, причем в нескольких вариантах. Первый вариант используется для 32-битного представления: под мантиссу отводится 23 бита (с 0 по 22), под порядок 8 бит (23–30), под знак — один (31-й). Для 64-битного представления размеры побольше: 52 под мантиссу, 11 под порядок, 1 под знак. Есть и 128-битный формат, но его мы рассматривать не будем. Как, впрочем, и 64-битный.


Ну и третья особенность данного формата: хранение порядка увеличенным на 127. То есть если в поле порядка хранится число 200, то сам порядок равен (200 – 127). Вероятно, это сделано, чтобы, если записать во все биты числа нули, порядок получился минимально возможным, –127. Причем само число при этом оказывается даже не 1.0·2⁻¹²⁷, как можно было подумать, а еще меньше. И это четвертая особенность. Если порядок равен –127 (в поле порядка записан ноль), число считается денормализованным. То есть вместо неявной единицы, про которую мы говорили у мантиссы, там предполагается неявный ноль.


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


Наконец, пятая особенность: не все битовые комбинации, которые можно записать в число, являются корректными числами. Некоторые из них обозначают специальные значения — бесконечности, не-числа, ошибки. Эти значения могут возникать, скажем, при делении на ноль, извлечении корней и т.п. Для дополнительной информации рекомендую ознакомиться с соответствующими лекциями на uneex (2022, 2024).


Некоторую сложность в ручных операциях с дробными числами может представлять то, что переводить из двоичной системы целые числа умеют почти все, а вот к дробным мы не привыкли. И даже большинство калькуляторов не привыкло. Принцип там, разумеется, тот же, что и в любой другой позиционной системе. Рассмотрим число 1010.1101₂:


2⁰ 2⁻¹ 2⁻² 2⁻³ 2⁻⁴
1 0 1 0 . 1 1 0 1

Можно, конечно, умножать побитово, но оперировать отрицательными степенями двойки опять же неудобно. Поэтому сначала умножим все число на 2⁴, чтобы оно стало целым, а потом поделим на 2⁴ обратно:


2⁷ 2⁶ 2⁵ 2⁴ 2⁰
1 0 1 0 1 1 0 1

Перевести двоичное число 10101101₂ в десятичное сумеет любой калькулятор: 173. Множитель 2⁴ также вычисляется легко: 16. Вот и получается, что наше исходное дробное число равно 173 / 16 = 10.8125.


По этому принципу мы и будем переводить в десятичный формат мантиссу. В ней один бит (равный 1) до точки и 23 бита после. Поэтому записываем биты, как будто это целое число, переводим в десятичный формат и делим на 2²³.


В качестве примера рассмотрим вот такое число: 11000011100111010001010001100011.


S [  E   ] [         M           ]
1 10000111 00111010001010001100011

S - sign, знак
E - exponent, порядок
M - mantissa, мантисса

Знак равен 1, то есть число отрицательное.


Порядок равен 10000111₂ = 135₁₀. Вычитаем 127, получаем 8.


Мантисса равна (1.)00111010001010001100011, или, в десятичном формате, 100111010001010001100011₂ / 2²³ = 1.22718465328. Умножаем на порядок (2⁸ = 256), не забываем добавить знак и получаем -314.15927124. Осталось проверить правильно ли проведен расчет:


int main(){
  union{
    uint32_t u;
    float f;
  }val;
  val.u = 0b11000011100111010001010001100011;
  printf("%f\n", val.f);
}

$ gcc main.c
$ ./a.out 
-314.159271

Все верно!


10.2 Аппаратный модуль FPU


В контроллере CH32V303 работа с дробными числами одинарной точности (32 бита) реализована аппаратно. Об этом говорит буква f в списке расширений imafc. Напоминаю, что другой наш контроллер, GD32VF103, имеет список расширений imac, то есть аппаратно дробных чисел не поддерживает. Вообще, работа с FPU в RISC-V реализована несколько странно, добавлением практически автономного блока (сопроцессора) с собственными регистрами.


Зачем это сделано и чем не устроило использование обычных регистров, я достоверно сказать не могу. Возможно, ради совместимости с D, Q и подобными расширениями (64-, 128-битные дробные числа). Это ведь только 32-битные float-ы помещаются в один регистр, а 64-битные уже нет. Впрочем, существуют и экзотические расширения Zfinx (float in X), Zdinx (double in X), Zhinx (half in X), в которых дробные числа хранятся как раз в обычных целочисленных регистрах. Двойная точность там обеспечивается регистровой парой. Но это уже сильный расход регистров, да и вообще не поддерживается нашим контроллером.


В нашем же случае вместе с модулем FPU добавляется 32 специальных регистра f0–f31. Как и обычные, они разделены на временные (ft0–ft11), сохраняемые (fs0–fs11) и аргументы функций (fa0–fa7). Конвенции по сохранению при использовании в функциях такие же, как для обычных регистров. Но надо помнить, что с ними умеет работать только сопроцессор, а не основное ядро. Поэтому все операции с f-регистрами пройдут только через специальные FPU-инструкции.


Любой расчет на FPU начинается с загрузки в f-регистр значения либо из обычного регистра (команда вроде fcvt.s.wu fa5, a5), либо из памяти (например flw fa0, 12(s3)), проведения с ним каких-то операций и выгрузки обратно (fcvt.w.s a0, fa5 / fsw fa0, 12(s3)). Обратите внимание на суффиксы у команды fcvt. Она универсальна и умеет преобразовывать f32, f64,… в u32, i32, u64,… и обратно. Собственно .w, .s, .l, .d отвечают именно за это. В нашем случае, когда поддерживаются только 32-битные целые (.w / .wu) и только 32-битные дробные (.s), набор суффиксов оказывается небольшим. Еще fcvt умеет округлять значение вверх (к +∞), вниз (к –∞), к нулю и от нуля. За это отвечает третий, опциональный, аргумент. Например, fcvt.w.s a0, fa5, rtz говорит «взять float значение из fa5, округлить до ближайшего целого в сторону нуля (round towards zero) и сохранить в int32_t регистр a0». Впрочем, слабо представляю для чего выбор округления может пригодиться в повседневном программировании. Но если вдруг понадобится — вот он. Кстати, округление можно настроить не только для каждой команды индивидуально, но и для всех сразу, за это отвечает CSR-регистр fcsr.


Подробно рассматривать команды работы с данным модулем смысла не вижу. Если кому-нибудь все же интересно, их можно найти в документации на ядро RISC-V или в тех же лекциях на uneex. Дело в том, что если уж программа достаточно сложна, чтобы потребовалась работа с дробными числами, писать ее, скорее всего, будут не на ассемблере, а как минимум на Си.


Особенности и ограничения придется знать в любом случае. Самое банальное: компилятор будет вынужден сохранить все f-регистры, если вы используете дробные числа в прерывании. Или если из прерывания вызывается другая функция (компилятор ведь может не знать, вдруг дробные числа используются где-то в ней). Сохранение 32 лишних регистров никак не прибавляет скорости обработки. А вот со второй особенностью будет лучше ознакомиться на примере кода:


  uint32_t t_prev = systick_read32();

  volatile float x = 1.1;
  volatile float res = 0;
  for(int i=0; i<9; i++)res += x;

  uint32_t t_cur = systick_read32();

  UART_puts(USART, "Float:");
  uart_fpi32(res*100000000, 8);
  UART_puts(USART, "\r\nt=");
  uart_fpi32( t_cur - t_prev, 0 );
  UART_puts(USART, "\r\n");

Здесь uart_fpi32 — всего лишь функция вывода на UART числа с фиксированной точкой. Что это такое — чуть ниже.


Что иллюстрирует пример? Первое — время выполнения кода, 81 такт. И второе — результат сложения, не 9.9 ровно, а 9.90000064. Это обусловлено тем, что числа-то мы задаем в десятичной системе, а хранятся они в двоичной, причем для хранения отведено всего 23 бита (ну хорошо, 24), что соответствует приблизительно 7 десятичным разрядам. Причем стоит помнить, что эти 7 разрядов достижимы разве что для идеальных условий. При выполнении математических операций точность будет каждый раз снижаться, так что в реальности доверять более чем 3–5 разрядам уже нельзя. Причем уточню: речь идет не о знаках после точки, а именно о 3–5 значимых цифрах. Также из этого следует, что проверять дробные числа на строгое равенство нельзя почти никогда. То есть следующий код будет работать некорректно:


  for(float x = 0; x != 10; x+=0.1){...}

Ближайшими значениями являются не 9.9 и 10.0, а 9.90000128 и 10.00000192.


10.3 Программная реализация


А что же делать с GD32VF103, в котором модуля FPU нет? Использовать программную реализацию. К счастью, тип float входит в стандарт языка Си, то есть будет поддерживаться компилятором в любом случае. Но не все так просто.


Если мы только изменим в makefile тип ядра на imac, компилятор нас обругает. Дело в том, что реализация работы с дробными числами компилятора gcc находится в отдельной библиотеке libgcc.a, причем отдельно для каждого подтипа ядер (по крайней мере, в risc-v gcc в Debian так). И что еще веселее, хотя этот подтип мы явно указываем, компилятор не желает его учитывать. Но если ему подсказать «ищи в /usr/lib/gcc/riscv64-unknown-elf/12.2.0/rv32imac/ilp32/ библиотеку gcc», он ее подставит. Вот только писать точную версию 12.2.0 прямо в makefile как-то неприлично. Вдруг выйдет новая. Поэтому для себя на Debian пришлось написать вот такой костыль:


GCCVER=`$(CC) --version | sed -n "1s/.* \([0-9]*[.][0-9]*[.][0-9]*\).*/\1/p"`
GCCPATH = -L/usr/lib/gcc/riscv64-unknown-elf/$(GCCVER)/$(ARCH_$(MCU))/ilp32/
...
LDFLAGS += $(GCCPATH) -lgcc

Тот же самый код на том же самом контроллере CH32V303, но с настройками imac (как будто FPU у нас нет) выдает в качестве результата суммирования те же 9.90000064 (что хорошо: поведение программной и аппаратной реализаций совпадают), но вот время выполнения возрастает аж до 789 тактов — почти в 10 раз!


В некоторых дистрибутивах поддержку 32-разрядных float-ов не завезли. Правильным решением было бы пинать мейнтейнеров, чтобы поправили, но можно взять библиотеку из дистрибутива, в котором поддержка есть. Вот, например, версии из моего Debian: для ядер rv32imac и rv32imafc. И разумеется, никто не запрещает переписать соответствующие функции самостоятельно — это замечательная практика по внутреннему устройству float-ов. А еще после такой практики надолго отпадет желание использовать их где попало.


10.4 Числа с фиксированной точкой


Понятно, что использование чисел с плавающей точкой в контроллерах без FPU достаточно накладно. Но ведь и работают контроллеры не в сферическом вакууме, а с реальными значениями из реального мира. И диапазон этих значений вполне предсказуем. Например, температура для бытовых условий может меняться где-то от –50 до +150 градусов. Ну хорошо, у нас, знакомых с паяльником, аж до +350–400, причем точность выше одной десятой нужна крайне редко. Тут нет нужды использовать разделение на мантиссу и порядок, достаточно просто считать не в единицах градусов, а в десятых долях. Или в сотых, или в тысячных. А при выводе на дисплей просто поставить в нужном месте десятичный разделитель. То есть температура 36.6 градуса может храниться как 366 дециградусов или 36600 миллиградусов. А это уже целые числа, работа с которыми нам хорошо знакома и не представляет никаких сложностей. Такое представление называется числами с фиксированной точкой. Давайте перепишем наш предыдущий код под работу с ними:


  t_prev = systick_read32();

  volatile uint32_t y = 110000000; //1.1 * 10⁸
  volatile uint32_t ires = 0;
  for(int i=0; i<9; i++)ires += y;

  t_cur = systick_read32();

  UART_puts(USART, "Fixed-point:");
  uart_fpi32(ires, 8);
  UART_puts(USART, "\r\nt=");
  uart_fpi32( t_cur - t_prev, 0 );
  UART_puts(USART, "\r\n");

Результат расчета — 9.90000000, время 73 такта. Мы выиграли и по точности, и по быстродействию. Причем не только у программной реализации FPU, но и у аппаратной! Но, разумеется, не все так радужно. Диапазон целых чисел все-таки ограничен, для 32-битных он составляет всего ±2·10⁹. Сравните с float, где диапазон 10³⁸. То есть сверхмалые и сверхбольшие числа таким способом не обработать. Но, повторяю, в микроконтроллерах диапазон чисел почти всегда известен заранее.


И вот теперь, когда мы рассмотрели, что такое числа с фиксированной точкой, можно чуть подробнее описать принцип работы uart_fpi32(val, dot). По сути, это всего лишь преобразование целого числа в строку, размещение после dot символов (считая справа) десятичной точки и вывод полученной строки в UART. Ее код настолько прост, что несколько раз мне было лень искать предыдущую реализацию и я писал ее с нуля. Самое сложное в ней (в том смысле, что все остальное еще проще) — добавить нули между концом числа и точкой, если выводится число вроде 0.00123.


Исходный код примера доступен на github. Внимание: для сборки с аппаратной поддержкой дробных чисел используется makefile_hw.mk, а с программной — makefile_sw.mk


Из любопытства я проверил и другие операции: выполнил каждую 10000 раз в цикле и вычислил среднее количество тактов на одну операцию.


Операция fixed-point Hardware FPU Software float SW/HW
Сложение 1.70 2.65 63.58 24.0
Умножение 1.70 2.65 80.86 30.5
Деление 10.70 10.65 84.89 8.0
sqrtf - 11.65 264.08 22.7
sinf - 1518.75 17876.18 11.7

10.5 Не только внутри контроллеров


Как ни странно, числа с фиксированной точкой применяются не только в микроконтроллерах с их ограниченными ресурсами. Так, в линуксе есть вот такой файл с интересным содержимым:


$ cat /sys/class/thermal/thermal_zone0/temp 
44000

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


Размер дробной части не обязательно задавать в десятичном формате. Рассмотрим такую распространенную микросхему, как DS18B20. Это цифровой термометр с диапазоном до 125 градусов и разрешением до 12 бит. А примечателен он в данном случае тем, что значения выдает именно в формате с фиксированной точкой: 8 старших бит — целая часть, 4 младших — дробная, слева дополняется знаковым битом до двухбайтной величины. Отрицательные значения представлены в дополнительном коде. То есть в десятичном формате достаточно разделить двоичный результат на 2⁴. Рассмотрим пару примеров декодирования из его документации:


0x00A2 -> 162₁₀ / 2⁴ = 10.125 градуса
0xFE6F -> -401₁₀ / 2⁴ = -25.0625 градуса

Для отображения температуры человеку выдачу с фиксированной точкой в двоичной системе стоит перевести в формат фиксированной точки в десятичной: умножить на 10 в нужной степени и поделить на 2 в степени, соответствующей исходному формату (в нашем случае 4). Допустим, нам достаточно одного знака после точки: 0x00A2 * 10¹ / 2⁴ = 162 * 10 / 16 = 101.25. Дробная часть отбрасывается, после первого разряда выставляется точка, и получается искомое 10.1. Или 0xFE6F * 10² / 2⁴ = –2506.25 -> –25.06.


10.6 Хранение


При работе с величинами из реального мира стоит обсудить и вопрос длительного хранения. Я имею в виду уже не внутреннее представление, а то, в котором оно передается во внешний мир и показывается пользователю. И это различие существенно! Потому что очень велик соблазн выдавать сразу сырые данные, скажем, с АЦП или датчика, или что-то в числах с фиксированной точкой. Так делать не надо. Через какое-то время вы попросту забудете, за что эти величины отвечают и как их перевести во что-то осмысленное. Поэтому для любого общения с внешним миром лучше всего использовать числа в стандартной системе Си. Если уж масса, то в килограммах (даже если получится 9.1093837·10⁻³¹), если напряжение, то в вольтах, если температура, то в градусах, если расстояние то в метрах. Чтобы лет через пять не вспоминать, что где-то величина выводилась в десятках миллиметров, а где-то — в килоомах. Если помимо чисел можно вывести подсказку, это совсем замечательно: можно указать там формат вывода (в каком столбце какая величина) и единицы измерения.


Но, как обычно, реальный мир все-таки отличается от идеального, и возможности вывода (или даже расчета) дробных чисел может физически не быть, или их использование неоправданно усложнит устройство. Тогда действительно приходится выводить как получится, а подробности описывать в документации. С этим мы уже познакомились на примере датчика температуры и будем сталкиваться во множестве другой периферии. Впрочем, это лишь рекомендация по человекочитаемому выводу.


10.7 Табличные функции


Нередко возникают задачи, связанные с вычислением математически тяжелых функций. Возьмем хотя бы синус. Лобовое решение float x = sin(y); слишком часто оказывается неэффективным (да вы видели, почти 18 тысяч тактов на один вызов!). Вместо этого можно воспользоваться тем, что у нас довольно много флеш-памяти, и разместить в ней заранее рассчитанную таблицу значений. Причем значения не обязательно должны быть float-ами. Тот же синус удобнее считать не в радианах, а в долях от 8-битного числа. То есть 0 это 0 радиан, 128 — это π, а 256 — 2π. И значения синуса пусть меняются не от –1 до +1, а от 0 до 255 или от –127 до +127.


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


10.8 Цифровой синтез сигналов, DDS


Поговорим о генерации синусоид. Допустим, мы хотим синтезировать звуковой сигнал при помощи ШИМ. Максимальная частота таймера равна тактовой частоте контроллера, по умолчанию 8 МГц. При 8-битном ШИМ его частота составит 31250 Гц. Но ведь нам нужен не меандр, а синусоида. То есть надо последовательно вывести все 256 значений из нашей таблицы. Максимальная частота составит уже 122 Гц. Как-то маловато...


Но ведь никто нас не заставляет непременно использовать все отсчеты. Скажем, если нам нужна частота 244 Гц, можно выводить каждое второе значение из таблицы, если 488 — каждое четвертое и так далее. Если желаемая частота не делится на наши 122 Гц нацело, код становится несколько более сложным. Интереса ради я набросал, как он может выглядеть:


volatile uint32_t dpos = (1LLU<<32) * 1000 / (144000000 / 256);
//                           │          │        │         └─── разрядность ШИМ
//                           │          │        └───────────── Частота тактирования таймера
//                           │          └────────────────────── Выходная частота
//                           └───────────────────────────────── Размер переменной счетчика (32 бита)

const int8_t sin256[256] = {0,3,6,9,12,15,18,21,24,27,30,33,36,39,42,45,48,51,54,57,59,62,65,67,70,73,75,78,80,82,85,87,89,91,94,96,98,100,102,103,105,107,108,110,112,113,114,116,117,118,119,120,121,122,123,123,124,125,125,126,126,126,126,126,127,126,126,126,126,126,125,125,124,123,123,122,121,120,119,118,117,116,114,113,112,110,108,107,105,103,102,100,98,96,94,91,89,87,85,82,80,78,75,73,70,67,65,62,59,57,54,51,48,45,42,39,36,33,30,27,24,21,18,15,12,9,6,3,0,-3,-6,-9,-12,-15,-18,-21,-24,-27,-30,-33,-36,-39,-42,-45,-48,-51,-54,-57,-59,-62,-65,-67,-70,-73,-75,-78,-80,-82,-85,-87,-89,-91,-94,-96,-98,-100,-102,-103,-105,-107,-108,-110,-112,-113,-114,-116,-117,-118,-119,-120,-121,-122,-123,-123,-124,-125,-125,-126,-126,-126,-126,-126,-127,-126,-126,-126,-126,-126,-125,-125,-124,-123,-123,-122,-121,-120,-119,-118,-117,-116,-114,-113,-112,-110,-108,-107,-105,-103,-102,-100,-98,-96,-94,-91,-89,-87,-85,-82,-80,-78,-75,-73,-70,-67,-65,-62,-59,-57,-54,-51,-48,-45,-42,-39,-36,-33,-30,-27,-24,-21,-18,-15,-12,-9,-6,-3};

//Прерывание по переполнению Timer4 (TIM_UIE)
__attribute__((interrupt)) void TIM4_IRQHandler(void){
  static uint32_t pos = 0;
  pos += dpos;

  TIM4->CH1CVR = 127 + sin256[(pos>>24)]; //Для адресации используются только 8 старших бит, остальные — аккумулятор, в них хранится ошибка, накопившаяся к текущему времени

  static uint32_t ppos = 0;
  if(pos < ppos)GPO_T(GLED); //при переполнении переменной-счетчика мигаем светодиодом, так проще измерить частоту
  ppos = pos;

  TIM4->INTFR = 0;
}

Код работает и даже рисует на экране осциллографа красивую синусоиду. Правда, при частотах выше 5 кГц она становится несколько треугольной — но чего вы хотели, 6 точек на период.


В реальном применении, разумеется, вместо прерывания от таймера логично использовать DMA. Как минимум оно будет значительно реже отвлекать ядро от его задач. А если чуть-чуть пожертвовать точностью, туда можно записать вообще всю таблицу значений и больше про него не думать. Но здесь ради наглядности я решил обойтись таймером.


Не менее очевидно, что вместо синуса можно использовать и любую другую функцию. Скажем, при работе с бесколлекторными двигателями или частотными преобразователями иногда используют вот такой «покореженный синус», а то и еще более извращенные вариации.


покореженные синусы


Такая странная форма сигналов позволяет достичь максимальной амплитуды межфазного напряжения, сохраняя его форму синусоидальной. Межфазное напряжение (между фиолетовой и зеленой фазами) нарисовано на графиках желтой линией. Достаточно подробно об этом рассказал в своем ролике TDM Lab.


10.9 Внезапный фейл с дробным числом


Это не относится напрямую к теме, просто история из жизни. На работе у меня установлен источник питания с управлением по COM-порту путем посылки обычных текстовых строк. В частности, для установки выходного напряжения, скажем, в 1.23 В нужно послать строку "VSET0 1.23\r\n". Эту строку я формировал обычным sprintf(buff, "VSET0 %.3lg\r\n", volt);. И однажды оказалось, что при установке напряжений около нуля прибор зависает и перестает менять напряжение. Проблема оказалась в формате %lg, который в обычных условиях заставляет sprintf автоматически выбирать способ записи — алгебраический (123.456) или экспоненциальный (1.23456e+2). Для обычного вывода на экран или в файл это очень удобно, но вот прибору экспоненциальная форма категорически не понравилась.


Не знаю, какая у этой истории мораль. Разве что при разработке своих приборов надо не лениться и реализовывать разные форматы ввода. В частности, не забывать, что десятичным разделителем может быть не только точка, но и запятая.


Дополнительная информация


Оригинал на github pages


Видеоверсия на Ютубе (только по gd32)


CC BY 4.0

Теги:
Хабы:
Всего голосов 22: ↑22 и ↓0+30
Комментарии5

Публикации

Информация

Сайт
yadro.com
Дата регистрации
Дата основания
Численность
5 001–10 000 человек
Местоположение
Россия
Представитель
Ульяна Соловьева