Есть ли что-то не то с этим кодом
Привет, я довольно новичок в cpp. Может ли кто-нибудь просмотреть мой код и объяснить какие-либо подводные камни или проблемы? Ниже приведен код и пример для считывания с расходомера с датчиком Холла. Заранее спасибо. Грег
/** example
#include "flow.h"
Flow f(3);
Flow f2(4);
volatile unsigned int counter;
void flow_counter() // ISR
{
// Приращение счетчика импульсов
counter ++;
}
void setup()
{
f.start(); //использование ISR по умолчанию
f2.assign_isr(&flow_counter);
f2.assign_counter(&counter);
f2.start();
}
void loop()
{
Serial.printf("flow rate on 3 = %f \n",f.rate());
Serial.printf("flow total = %dml %f%% of 500ml \n",f.total_ml(),f.percent(500));
Serial.printf("flow rate on 4 = %f \n",f2.rate());
delay(1000);
}
*/
#include <math.h>
#include "Arduino.h"
volatile unsigned int flowPulseCounter;
void flowCounter() // ISR
{
// Приращение счетчика импульсов
flowPulseCounter ++;
}
class Flow{
public:
Flow(uint8_t pin)
{
if(flowPin==254)
flowPin = pin;
}
void assign_isr ( void (*isr)()){ this->flowISR = isr; };
void assign_counter (volatile unsigned int * counter ){ this->counter = counter; };
void assign_pin(uint8_t pin){ flowPin = pin; };
void set_calibration_factor( float factor ) { this->calibrationFactor = factor; };
float get_calibration_factor( ) { return this->calibrationFactor; };
uint8_t retrieve_pin(){ return flowPin; };
void start()
{
detachInterrupt(digitalPinToInterrupt(this->flowPin));
*this->counter = 0; // counter-это указатель, поэтому * присваивает значение по этому адресу 0
this->flowRate = 0;
this->flowMillilitres = 0;
this->flowMilliseconds = millis();
pinMode(this->flowPin, INPUT_PULLUP);
this->running = true;
attachInterrupt(digitalPinToInterrupt(this->flowPin), this->flowISR, FALLING);
}
void calculate()
{
if(this->running)
{
this->flowRate = ((1000.0 / (millis() - this->flowMilliseconds)) * ( *this->counter )) / this->calibrationFactor;
this->flowMilliseconds = millis();
// перевести в миллилитры.
this->flowMillilitres = (this->flowRate / 60) * 1000;
// Добавьте миллилитры к совокупной сумме
this->totalMillilitres += this->flowMillilitres;
}
}
void stop()
{
detachInterrupt(digitalPinToInterrupt(flowPin));
this->calculate();
this->running = false;
pinMode(this->flowPin, INPUT_PULLUP);
}
bool is_max( uint32_t millilitres )
{
this->calculate();
return (this->totalMillilitres >= millilitres) ? true : false;
}
float percent( uint32_t millilitres )
{
this->calculate();
return (float)((double) this->totalMillilitres / (double) millilitres) * 100.0;
}
float rate ()
{
this->calculate();
return this->flowRate;
}
uint32_t total_ml()
{
this->calculate();
return this->totalMillilitres;
}
private:
uint8_t flowPin=0;
uint32_t flowMilliseconds = 0;
float flowRate=0;
uint32_t flowMillilitres = 0;
uint32_t totalMillilitres = 0;
float calibrationFactor = 4.5;
bool running = false;
void (*flowISR)() = &flowCounter;
volatile unsigned int * counter = &flowPulseCounter;
};
@Yardie, 👍1
Обсуждение4 ответа
Лучший ответ:
Единственная проблема, которую я вижу, заключается в использовании вами переменной counter. Это изменчивая переменная, которая увеличивается в подпрограмме прерывания и считывается из основного контекста цикла. Переменная является "неатомной" в том смысле, что она состоит из нескольких меньших байтов (по крайней мере, на 8-битных ардуино), что означает, что для ее чтения из памяти требуется несколько последовательных инструкций.
В любой момент этой последовательности прерывание может вызвать и изменить значение, повредив ваши данные.
Вы всегда должны использовать критический раздел вокруг любых обращений к переменным, которые изменяются внутри подпрограмм прерывания, таких как:
noInterrupts(); // Отключить прерывания
uint32_t currentCount = this->counter; // Захватить текущий счетчик
interrupts(); // Повторно включить прерывания
// Делайте что-то с CurrentCount вместо этого - >счетчик
re:Вы всегда должны использовать критический раздел вокруг любых обращений к переменным, которые изменяются внутри подпрограмм прерывания, таких как: Поскольку переменная является изменчивой и, следовательно, явно читается каждый раз, почему это необходимо. Убийство прерываний ломает так много других вещей., @Yardie
Потому что, как я уже сказал, **это не атомно**. Это означает, что переменная может изменяться *во время чтения, потому что это не одна операция, а последовательность более мелких операций*., @Majenko
И вы всегда держите критическую секцию как можно короче, чтобы уменьшить влияние временного отключения прерываний, которое в данном случае должно быть нулевым., @Majenko
Извините, @Majenko, я спешил на работу и не прочитал ваш комментарий должным образом., @Yardie
Вот что я предлагаю в качестве решения. Хотя я рад, что ошибаюсь.` uint32_t this_count = count;
uint32_t count_check = количество;
if((this_count>>3)!=(count_check>>3)
return -0.0; // Мне нравится этот возврат, так как он также позволяет возвращать допустимые значения 0
// допустимое чтение ie. все, кроме последних 3 бит, совпадают, что позволяет некоторое продвижение в счетчике
`, @Yardie
Хотя я понимаю ваше мышление, я не совсем согласен с ним. Да, это может быть правильно для 8-битной однопоточной среды, как и для многих AVR, но это не всегда так. Мне не нравится идея иметь раздел отключения прерывания, скрытый внутри класса. Работа с esp32 (т. е. несколько ядер и очень простая многопоточная система) вероятность взлома многих вещей возрастает. Кроме того, если я запущу несколько экземпляров класса, они могут прерывать счетчики друг друга., @Yardie
@Yardie: 1. Если вы публикуете библиотеку, которая работает только на ESP32, вы должны задокументировать это ограничение и иметь условную директиву "#error " в заголовке, которая запрещает кому-либо использовать ее на другой платформе. 2. Этот критический участок настолько мал, что не будет иметь никакого влияния. Если импульсы приходят так быстро, что это действительно оказывает влияние, ваша программа все равно не сможет их обработать., @Edgar Bonet
@Yardie Это не "мое" мышление: критические разделы существуют с самого начала вычислительной техники. Ничто в вашем вопросе не говорит о том, что вы хотите использовать ESP32, и поскольку это сайт *Arduino*, мы должны предположить, если вы не скажете нам иначе, что вы используете Arduino. А поскольку большинство плат Arduino имеют 8 бит, можно с уверенностью предположить, что потребуется критическая секция. Если вы действительно хотите использовать какую-то другую плату, то вы должны предпринять действия, соответствующие *этой* плате. Для 32-битной платы вполне может оказаться, что операция является атомарной и не имеет никаких действий ..., @Majenko
... требуется. Но если вы хотите опубликовать свою библиотеку для других, вы не можете предположить, что все они захотят использовать ESP32. Вам нужно будет предпринять соответствующие шаги, чтобы создать подходящую критическую секцию для тех плат, которые требуют их (конечно, все не 32-битные платы и, возможно, некоторые 32-битные). Для этого будет полезно определить ваши собственные макросы, которые изменяются в зависимости от того, для чего они компилируются. Кроме того, в зависимости от целевой архитектуры вы можете использовать более мелкозернистый критический раздел, в котором вы отключаете только то прерывание, с которым работаете ..., @Majenko
.. это отключает их глобально. Но в целом, чтобы развеять ваши опасения по поводу критических разделов, когда прерывания отключены, любые прерывания, которые могут возникнуть, обычно откладываются до тех пор, пока прерывания не будут снова включены, после чего они будут обработаны. И если ваша критическая секция имеет длину всего в несколько тактов, а прерывания происходят только с тысячами тактов между ними, то влияние критической секции будет несуществующим., @Majenko
Хорошо, спасибо, ребята., @Yardie
Да, вы правы, однако код для Arduino теперь должен учитывать ESP. Я работаю и с тем, и с другим и пишу код в первую очередь для себя. Если бы я опубликовал код, этот пост-единственное место, где я бы его опубликовал. Я многое приобрел от свободного программного обеспечения и люблю возвращать его обратно, когда могу. Вот и все. Я люблю свои ATTinies и хочу написать что-то, что работает для обоих. Я просто пытался рассмотреть обе возможности. Остановка всей системы прерываний для чтения переменной всегда беспокоила меня. Однако читая ваш последний комментарий Majenko это действительно развеивает это и проясняет мое заблуждение, @Yardie
Я исследовал достаточно низкоуровневый код, чтобы увидеть некоторые довольно неприятные предположения и оплошности. Я хотел избежать этого в своем коде, даже если это только для меня., @Yardie
В любом случае, спасибо огромное всем, ваша помощь была фантастической и очень поучительной. Извините, если я кого-то чем-то обидел. Это никогда не было намеренно. Никто не родился с этим знанием, и все мы идем разными путями. Спасибо, что поделились своими знаниями, @Yardie
Эдгар, я все равно никогда не смогу опубликовать этот код. В любом случае, теперь это в основном ваш код. :) Спасибо. Теперь у меня есть простая библиотека, чтобы легко добавить расходомер к attiny или esp32. Это все, чего я хотел. Да, и более глубокое понимание связанных с этим проблем тоже., @Yardie
Трудно ответить без какого-либо описания реальной проблемы.
На мой взгляд, с таким большим количеством кода я бы подумал о том, чтобы начать с создания отдельного объявления класса и написать свои определения членов снаружи. Это сделает код более управляемым и облегчит устранение неполадок.
https://www.learncpp.com/cpp-tutorial/class-code-and-header-files/
Кроме того, у вас есть несколько операторов include, по крайней мере, в двух разных местах. Они обычно идут первыми в шеренге вместе с охранниками. Если ваш код состоит из отдельных файлов, пожалуйста, разъясните это читателям.
Re: the includes - сначала я тоже так думал, пока не заметил, что первые includes находятся внутри большого комментария., @Majenko
Теперь я это вижу. Кстати, как вы делаете шикарное цветовое форматирование?, @Erik
Вы заверните его во что - то, что я не могу напечатать здесь;)-backtick-backtick-backtick-c++ и backtick-backtick-backtick, @Majenko
Я добавил спецификатор языка после трех тиков в вопросе. Похоже, он уже достаточно сбил с толку людей. Иногда без него система не выделяет синтаксис или выбирает неправильный тип языка. Вы можете видеть разницу в истории [править / править код](https://arduino.stackexchange.com/posts/84578/revisions)., @timemage
\\
\c++ \[code\] \
\\
, @Dave Newton
Отлично, это именно те ответы, которые мне нужны, спасибо. Проблема статического ISR была особенно неприятна мне., @Yardie
@Erik извините, я думал, что примерная часть /** и ее комментирование сделают это достаточно ясным, @Yardie
Спасибо @timemage за добавление спецификатора языка. Это, безусловно, помогает прояснить вопрос, @Yardie
Вот несколько случайных комментариев от просмотра кода.
Первый вопрос, который больше касается примера, чем самой библиотеки:
в стандартных ядрах Arduino нет Serial.printf ()
. Да, он
обеспечивается некоторыми альтернативными ядрами, и жаль, что он недоступен
в Arduino, но так оно и есть.
В конструкторе есть if(flowPin==254)
. Почему такое состояние?
flowPin
инициализируется нулем, поэтому, казалось бы, условие
всегда должно быть ложным.
С точки зрения стиля, вы можете удалить все this->
из методов.
В любом случае это неявно.
Ради согласованности имен я бы переименовал retrieve_pin()
в
get_pin ()
и весь метод assign_*()
как set_ * ()
.
Это не полезно, чтобы detachInterrupt()
прямо перед
attachInterrupt()
, поскольку последнее подразумевает первое.
В calculate ()
избегайте вызова millis()
дважды. У вас нет гарантии
, что оба вызова вернут одно и то же значение, а если это не так,
то вычисления времени будут отключены. Кроме того, эта функция потерпит неудачу
(деление на ноль), если будет вызвана дважды в течение одной и той же миллисекунды.
Нет смысла переводить расход в миллилитры. Вы имеете в виду миллилитры в минуту? В секунду? В любом случае, вы здесь конвертируете скорость в другую единицу, но это все равно скорость потока.
Вычисление Totalmillitres
неверно: оно увеличивается на
скорость потока. Вместо этого он должен быть увеличен на коэффициент продукта × время. Или
еще лучше: вычислите его по количеству импульсов, не проходя через
частоту сердечных сокращений. В противном случае вы вычисляете численную производную (чтобы получить
скорость) только для того, чтобы интегрировать ее позже (чтобы получить общий объем). Эти дополнительные
вычисления не служат никакой другой цели, кроме накопления ошибок округления.
Edit: Размышляя дальше об этой программе, я нашел еще несколько вещей , которые, как мне кажется, можно было бы улучшить. Пара незначительных моментов и один не такой уж незначительный.
Одним из второстепенных моментов является то, что begin()
было бы лучшим именем для
start ()
, для согласованности с большинством других библиотек Arduino.
Другое дело, что есть несколько методов, которые не имеют никакого отношения
к классу: is_max()
и percent()
. Это тривиальные
вычисления, которые никоим образом не связаны с работой с расходомером. Как
пользователь библиотеки, я бы предпочел написать
if (flow.total_ml() >= threshold_volume) ...
чем
if (flow.is_max(threshold_volume)) ...
поскольку первое гораздо яснее и проще для понимания.
Более серьезный момент заключается в обращении с несколькими расходомерами.
Первый полностью обрабатывается библиотекой, и это хорошо.
Однако для второго пользователь должен предоставить кирпичи самого низкого уровня
. Пользователь библиотеки не должен сам справляться с такими
низкоуровневые детали. Таким образом, я думаю, что и переменная counter, и функция ISR
должны обрабатываться библиотекой как члены потока
класс.
Я знаю, что это создает некоторые трудности. Для того чтобы быть присоединенным к прерыванию, ISR должен быть статическим методом класса. Для того чтобы этот метод мог получить доступ к счетчику, этот счетчик должен быть статическим членом. Для того чтобы каждый расходомер имел свой собственный счетчик, мы должны иметь отдельный класс для каждого из них.
Я нашел решение, которое считаю достаточно элегантным: пусть Flow
будет шаблоном
класса, параметризованным выводом. Это создает
отдельный класс для каждого расходомера. Чтобы избежать дублирования кода,
эти классы могут быть почти пустыми оболочками, которые делегируют как
можно больше обработчику, который является обычным обычным классом.
Вот как может выглядеть этот класс “обработчик”. В нем есть все , кроме ISR:
class FlowMeterHandler {
// Все здесь закрыто, доступно только классу Flow.
template<uint8_t pin> friend class Flow;
private:
void begin(uint8_t pin, void (*isr)()) {
attachInterrupt(digitalPinToInterrupt(pin), isr, FALLING);
}
float flow_rate() {
uint32_t now = millis();
if (now - last_millis >= 10) {
uint32_t this_count;
noInterrupts();
this_count = count;
interrupts();
last_flow = calibration * (this_count - last_count)
/ (now - last_millis);
last_millis = now;
last_count = this_count;
}
return last_flow;
}
// + другие методы...
volatile uint32_t count = 0;
uint32_t last_count = 0;
uint32_t last_millis = 0;
float last_flow = 0;
float calibration = 1e3 / 4.5;
};
Я написал методы inline, но вы также можете поместить реализации в отдельный файл .cpp.
А вот и класс шаблона. За исключением ISR, он пересылает все обработчику.
template<uint8_t pin> class Flow {
public:
static void begin() { handler.begin(pin, isr); }
static float flow_rate() { return handler.flow_rate(); }
// другие методы также пересылаются обработчику...
private:
static FlowMeterHandler handler;
static void isr() { handler.count++; };
};
template<uint8_t pin> FlowMeterHandler Flow<pin>::handler;
Обратите внимание, что класс не имеет данных экземпляра: все в нем статично
.
Вот как это будет использоваться:
Flow<3> f1;
Flow<4> f2;
void setup()
{
f1.begin();
f2.begin();
}
void loop()
{
Serial.print(F("flow rate on 3 = "));
Serial.println(f1.flow_rate());
Serial.print(F("flow rate on 4 = "));
Serial.println(f2.flow_rate());
delay(1000);
}
Спасибо, сейчас мне нужно идти на работу, но потом я все проверю как следует. Спасибо Эдгар, @Yardie
Эдгар, это великолепно, очень элегантно. Мне все еще не нравится этот
`c++
noInterrupts();
this_count = count;
прерывания();
`, @Yardie
if(flowPin==254) был оставлен после более ранней попытки в нескольких экземплярах, которые я не закончил конкретизировать, извините, это была отвлекающая маневр, @Yardie
Мне также нравится ваше мышление сif (flow.total_ml() >= threshold_volume)
делает все очень просто., @Yardie
@Yardie: Re “_I все еще не нравится noInterrupts(); ...
_”: avr-gcc имеет макрос [ATOMIC_BLOCK ()
] (https://www.nongnu.org/avr-libc/user-manual/group__util__atomic.html), который инкапсулирует эту маскировку прерывания, возможно, семантически более ясным способом. Однако он специфичен для AVR и поэтому не подходит в более широком контексте Arduino. AFAIK, ядро Arduino не предоставляет ничего лучше, чем noInterrupts ()
/ "interrupts ()" для атомарного доступа к переменным., @Edgar Bonet
Итак, если кому-то интересно, вот что я получил
/*
это еще предстоит изменить, но происходит из следующих ссылок
*
https://www.bc-robotics.com/tutorials/using-a-flow-sensor-with-arduino/
"Они не способны контролировать расход менее 1 литра в минуту или более 30 литров в минуту.
Датчик рассчитан максимум на 2,0 МПа (290 фунтов на квадратный дюйм) ".
и
каждый поворот означает, что датчик прошел 2,25 мл жидкости
другой-это
https://diyhacking.com/projects/FlowMeterDIY.ino
"// Датчик расхода с эффектом Холла выдает приблизительно 4,5 импульса в секунду на
// литр/минута расхода.
"
затем, наконец,
https://arduino.stackexchange.com/questions/84578/is-there-anything-wrong-with-this-code
Я не предъявляю никаких личных претензий ни к одному из положений этого кодекса. Это просто моя версия версии Эдгара.
Спасибо Эдгар
*
*
*
* Я использовал start и stop, поскольку они означают начало и остановку процесса.
* класс начинается, когда он начинается и не имеет конца isr остается связанным с классом в течение всего времени
* и start просто присоединяет прерывание к isr
* stop просто вычисляет общую громкость и отсоединяет прерывание.
*
* как скорость потока, связанная со временем
* просто установите свое собственное время начала
* прочитайте total_ml
* затем после вашего собственного предопределенного времени
* прочитайте total_ml еще раз.
* и рассчитайте скорость потока исходя из этого.
*/
class FlowMeterHandler {
// Everything here is private, only accessible to the Flow class.
template<uint8_t pin> friend class Flow;
private:
void start(uint8_t pin, void (*isr)())
{
noInterrupts();
count = 0;
interrupts();
attachInterrupt(digitalPinToInterrupt(pin), isr, FALLING);
}
void stop(uint8_t pin)
{
detachInterrupt(digitalPinToInterrupt(pin));
}
unsigned int count_is()
{
unsigned int this_count;
noInterrupts();
this_count = count;
interrupts();
return this_count;
}
volatile unsigned int count = 0;
unsigned int ul_per_tick = 2250;
};
template<uint8_t pin> class Flow {
public:
static void start() { handler.start(pin, isr); }
static void stop() { handler.stop(pin); }
static unsigned int total_ul() { return handler.count_is() * handler.ul_per_tick; }
static unsigned int total_ml() { return ( handler.count_is() * handler.ul_per_tick ) / 1000 ; }
static void set_ul_per_rotation( unsigned int ul) { handler.ul_per_tick = ul; }
private:
static FlowMeterHandler handler;
static void isr() { handler.count++; };
};
template<uint8_t pin> FlowMeterHandler Flow<pin>::handler;
пример ino выглядит так
#include "flow.h"
Flow<3> f1;
Flow<4> f2;
void setup()
{
f1.start();
f2.start();
}
void loop()
{
Serial.print(F("So far volume on 3 = "));
Serial.println(f1.total_ml());
Serial.print(F("and the volume on 4 = "));
Serial.println(f2.total_ml());
delay(1000);
}
Компилируется чисто на всех моих платах. Бежит ??? Любые другие предложения будут с благодарностью приняты.
Спасибо. Грег
Обратите внимание, что хранить total_ml
внутри FlowMeterHandler
бесполезно, так как он используется только сразу после установки. Вместо этого это может быть локальная переменная ml ()
или даже быть полностью удалена: float ml() { return count_is() * ml_per_tick; }
Кроме того, нецелесообразно вызывать ml()
из stop()
., @Edgar Bonet
Эдгар спасибо, да, вы совершенно правы. Это были незаконченные/ненужные идеи. Мне тоже не нравится избыточный код. Наверное, я так долго занимался неаккуратной самостоятельной отладкой. Это то, что я обычно нахожу месяцы/годы спустя. Какого черта я это сделал? Я спрашивал себя об этом много раз. Приятно, когда кто-то для разнообразия заглядывает мне через плечо. Я сменил код на костюм., @Yardie
вероятно, можно было бы просто сделать `return handler.count_is() * handler.ml_per_tick;
` в шаблоне., @Yardie
избавление от поплавков могло бы немного ускорить процесс. Использование микролитров вместо мл., @Yardie
- Почему необходимо использовать ключевое слово volatile для глобальных переменных при обработке прерываний в ардуино?
- Серийное прерывание
- Ошибка 'Serial' was not declared in this scope
- Прерывание ардуино при смене контакта
- Влияет ли `millis()` на длинные ISR?
- Как прервать функцию цикла и перезапустить ее?
- Задержка Arduino внутри прерывания
- Аппаратное прерывание срабатывает случайным образом
Почему весь первый раздел закомментирован?, @chrisl
@chrisl: Это библиотека. Блок комментариев-это пример того, как его использовать., @Edgar Bonet