Может быть, странная проблема, с которой я столкнулся, связана со сравнением чисел с плавающей точкой?

У меня есть прерывание таймера, которое управляет шаговым двигателем на плате 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?

, 👍2

Обсуждение

объявлен ли motorSpeed изменчивым?, @Juraj

Да, все переменные в обработчике прерываний являются изменчивыми., @stib

Для этого есть библиотека: https://www.airspayce.com/mikem/arduino/AccelStepper/, @Jot

Я попробовал это сделать, но возникли некоторые проблемы с библиотекой LCD, которую я использую. По какой-то причине они не хотели сосуществовать., @stib

Они не используют тот же таймер, он должен работать. Возможно, они используют слишком много памяти. Какая это библиотека LCD и работает ли она сейчас?, @Jot

это liquidCrystal, я провел некоторое начальное тестирование, используя другую библиотеку для меню и accelStepper. Но затем я закончил разработку своего собственного класса меню и изучил как прерывания, так и классы C++, так что я доволен., @stib


3 ответа


Лучший ответ:

2

Самая большая проблема здесь в том, что числа с плавающей точкой не очень хорошо поддаются сравнению. Проблема в том, что число с плавающей точкой не является точным числом. Это, в значительной степени, приближение.

Некоторые числа не могут быть представлены числами с плавающей точкой, поэтому ваше «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

Никогда не следует сравнивать два числа с плавающей точкой с помощью == или != напрямую. Причина в том, что числа с плавающей точкой неточны, поэтому, возможно, 1.0 может быть 0.99999999999, а другое значение 1.0 может быть 1.00000000001 (упрощение).

Вместо этого используйте «диапазон», например:

if (fabs(motorSpeed - targetSpeed) < 0.001)
{
   // Равный
}

Это означает, что разница должна быть меньше 0,001. fabs означает абсолютное значение числа с плавающей точкой (математический символ |x| для абсолютного значения).

Если вы делаете больше сравнений таким образом, сделайте 0.001 #define или const.

,

3

Вы уже знаете из предыдущих ответов, почему сравнение не дает вы результат, который вы ожидаете: вы предположили, что любое число, кратное 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