Может быть, странная проблема, с которой я столкнулся, связана со сравнением чисел с плавающей точкой?
У меня есть прерывание таймера, которое управляет шаговым двигателем на плате UNO. В обработчике прерываний он проверяет скорость двигателя и сравнивает ее с целевой скоростью. Это позволяет двигателю разгоняться до целевой скорости неблокирующим образом.
Это соответствующая часть обработчика прерываний:
ISR(TIMER1_COMPA_vect){
if (targetSpeed > 0 || motorSpeed > 0){
// передний край шагового цикла
if (leadingEdge){
digitalWrite(STEP_PIN, HIGH);
stepNumber++;
// проверьте скорость и отрегулируйте при необходимости
if (motorSpeed < targetSpeed){
motorSpeed += speedIncrement;
}
else if (motorSpeed > targetSpeed){
motorSpeed -= speedIncrement;
}
leadingEdge = false;
}
// задняя кромка
else{
digitalWrite(STEP_PIN, LOW);
leadingEdge = true;
}
}
OCR1A = minInteruptsPerStep + (minInteruptsPerStep * (1 - motorSpeed));
}
В loop()
у меня есть функция, которая проверяет состояние motorSpeed
для управления другими функциями
if (motorSpeed == targetSpeed) {
motorState = RUNNING;
} else if (motorSpeed < targetSpeed) {
motorState = ACCELERATING;
} else if (motorSpeed > targetSpeed) {
motorState = DECELERATING;
}
motorSpeed
и targetSpeed
— числа с плавающей точкой в диапазоне от 0 до 1 с шагом 0,1. speedIncrement
равен 0,001. Насколько я понимаю, это должно увеличивать скорость до тех пор, пока motorSpeed == targetSpeed
с шагом 100. И в основном это происходит. Скорость достигает targetSpeed
, а motorState переключается с ACCELERATING
на RUNNING
, как и ожидалось... в основном. Проблема в том, что он иногда начинает переключаться между тремя состояниями, по непонятной мне причине — нет ничего, что могло бы изменить значение motorSpeed
за пределами обработчика прерываний, поэтому после прекращения ускорения он должен быть стабильным.
Это потому, что я использую и сравниваю числа с плавающей точкой? Мне следует увеличить масштаб переменных скорости и использовать int или даже byte?
@stib, 👍2
Обсуждение3 ответа
Лучший ответ:
Самая большая проблема здесь в том, что числа с плавающей точкой не очень хорошо поддаются сравнению. Проблема в том, что число с плавающей точкой не является точным числом. Это, в значительной степени, приближение.
Некоторые числа не могут быть представлены числами с плавающей точкой, поэтому ваше «100» на самом деле может быть «100,00001» или «99,999999».
Лучше использовать целые числа, когда это возможно. Выполняйте вычисления с использованием больших чисел, а затем уменьшайте результаты, когда вам нужно их использовать.
Если вы работаете с шагом 0,1 между 0 и 100, то на самом деле вам следует работать с шагом 1 между 0 и 1000, а затем делить на 10 позже, когда вам понадобится использовать это значение (если вам вообще это нужно).
Вот небольшой пример, который я набросал на Linux:
#include <stdio.h>
void main() {
float f = 0;
while (f < 100) {
f += 0.1;
}
printf("%.6f\n",f);
}
Вы ожидали бы, что ответ будет "100.000000". Но этого не происходит. Вместо этого вы получаете 100.099045. Это потому, что самое близкое к 100 значение, которое он получает, на самом деле — это предыдущая итерация: 99.999046.
Если вы хотите сравнить числа с плавающей точкой, то лучшим способом будет использовать операцию «в пределах X». Вот небольшой макрос:
#define WITHIN(A,B,DIFF) (fabs((A) - (B)) <= (DIFF))
Тогда вы можете:
if (WITHIN(motorSpeed, targetSpeed, 0.1)) {
...
}
Это должно дать вам значение TRUE, если motorSpeed и targetSpeed находятся в пределах 0,1 друг от друга.
Спасибо. Я раньше не занимался низкоуровневым программированием, поэтому не знал о проблеме с float. Возможно, попробую сменить тип данных на более подходящий., @stib
Никогда не следует сравнивать два числа с плавающей точкой с помощью == или != напрямую. Причина в том, что числа с плавающей точкой неточны, поэтому, возможно, 1.0 может быть 0.99999999999, а другое значение 1.0 может быть 1.00000000001 (упрощение).
Вместо этого используйте «диапазон», например:
if (fabs(motorSpeed - targetSpeed) < 0.001)
{
// Равный
}
Это означает, что разница должна быть меньше 0,001. fabs означает абсолютное значение числа с плавающей точкой (математический символ |x| для абсолютного значения).
Если вы делаете больше сравнений таким образом, сделайте 0.001 #define или const.
Вы уже знаете из предыдущих ответов, почему сравнение не дает вы результат, который вы ожидаете: вы предположили, что любое число, кратное 0,1, также кратно 0,001, что верно для действительных чисел, но не для чисел с плавающей точкой.
Чтобы решить эту проблему, вам было предложено разрешить некоторые суетливость в сравнении. Я бы предложил другой подход: сохранить ваши сравнения такими, какие они есть, и вместо этого измените способ измерения скорости обновлено в ISR:
if (motorSpeed < targetSpeed) {
motorSpeed = min(motorSpeed + speedIncrement, targetSpeed);
}
else if (motorSpeed > targetSpeed) {
motorSpeed = max(motorSpeed - speedIncrement, targetSpeed);
}
С этой логикой motorSpeed
всегда будет точно равен
на targetSpeed
, независимо от того, является ли изменение скорости кратным
прироста или нет.
Распространенное заблуждение о числах с плавающей точкой состоит в том, что они не точно. По правде говоря, большинство вычислений с плавающей точкой включают округление ошибки, но сами числа с плавающей точкой являются точными числами, если не те цифры, которые вы ожидаете¹. Тогда, когда вы выполните задание
motorSpeed = targetSpeed;
что код выше в конечном итоге делает в какой-то момент, вы можете проверить
if (motorSpeed == targetSpeed) ...
и это будет правдой.
¹ Когда вы пишете «0.1» в исходном коде, вы получаете float
, который
наиболее близкое к 0,1, что равно 0,100000001490116119384765625
именно.
Хорошая идея, но она все еще упускает проблему сравнения в основном цикле (), который проверяет состояние двигателя. Я читал о числах с плавающей точкой и лучше понимаю проблему, но меня все еще озадачивает то, что она не выглядит детерминированной. Поскольку значение motorSpeed и значение targetSpeed являются постоянными, как только двигатель достигает стабильного состояния, а в моих тестах он сообщает, что motorSpeed == targetSpeed
, почему тогда после сообщения об этом как об истинном для сотен шагов оно внезапно переключается на ложное, хотя в коде нет ничего, что изменяло бы значение любой из переменных?, @stib
Обратите внимание, что motorSpeed — это *не* измерение, это скорость, с которой Arduino сообщает двигателю работать, так что это не механическая проблема., @stib
Я использовал эту технику с целыми числами, что решило проблему того, что speedIncrement не обязательно кратен targetSpeed, а также проблему основного цикла loop(), @stib
@stib: Вычисления с плавающими числами _являются_ детерминированными. Если вы станете свидетелем недетерминированного поведения, вам придется поискать его где-то в другом месте кода., @Edgar Bonet
- Как преобразовать четыре uint16_t в двойное число с плавающей запятой IEEE754?
- Float печатается только 2 десятичных знака после запятой
- Как получить тип данных переменной?
- Отправка и получение различных типов данных через I2C в Arduino
- Преобразование в Unix Timestamp и обратно
- Использование millis() и micros() внутри процедуры прерывания
- Подсчет импульсов с прерыванием
- Как перевести float в четыре байта?
объявлен ли motorSpeed изменчивым?, @Juraj
Да, все переменные в обработчике прерываний являются изменчивыми., @stib
Для этого есть библиотека: https://www.airspayce.com/mikem/arduino/AccelStepper/, @Jot
Я попробовал это сделать, но возникли некоторые проблемы с библиотекой LCD, которую я использую. По какой-то причине они не хотели сосуществовать., @stib
Они не используют тот же таймер, он должен работать. Возможно, они используют слишком много памяти. Какая это библиотека LCD и работает ли она сейчас?, @Jot
это liquidCrystal, я провел некоторое начальное тестирование, используя другую библиотеку для меню и accelStepper. Но затем я закончил разработку своего собственного класса меню и изучил как прерывания, так и классы C++, так что я доволен., @stib