Передача переменных для PID в прерывании

interrupt loop pid

Мне нужно передать переменные из функции timerIsr() в функцию цикла(). У меня мало опыта работы с языком Arduino, я в основном знаком с Python, поэтому для меня это было довольно сложно.

По сути, я создаю ПИД-регулятор для поддержания частоты вращения двигателя вентилятора, если к нему применяется трение. Я реализую ПИД-регулятор в функции цикла(), но мне нужна информация о переменной вращения из моей функции timerIsr(). Я попытался вернуть информацию о переменной, но она не соответствует фактической скорости вращения вентилятора. Я думаю, что это как-то связано с разным временем задержки/таймера, действующим в разных случаях, но я не уверен.

Вот что у меня есть на данный момент

#include "TimerOne.h"
#include <PID_v1.h>

int counter=0;

const int IN1 = 11;
const int IN2 = 10;
volatile float pot=0;
const int POT = 0;
const int ENA = 6;

double Setpoint, Input, Output;
double Kp=2, Ki=5, Kd=1;

PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT);


void docount()  // считает от датчика скорости
{
  counter++;  // увеличиваем +1 значение счетчика
} 

void timerIsr()
{
  Timer1.detachInterrupt();  //остановим таймер
  Serial.print("Motor Speed: "); 
  int rotation = (counter / 30);  // делим на количество отверстий на диске
  Serial.print(rotation,DEC);  
  Serial.println(" Rotation per second"); 
  counter=0;  // обнуляем счетчик
  Timer1.attachInterrupt( timerIsr );  //включаем таймер
  Serial.print("pot = ");
  Serial.print(pot);
  Serial.print("\n");

  //возврат вращения;
}

void setup() 
{
  Serial.begin(9600);

  pinMode(IN1, OUTPUT);
  pinMode(IN2, OUTPUT); 

  Timer1.initialize(1000000); // устанавливаем таймер на 1 секунду
  attachInterrupt(0, docount, FALLING);  // увеличиваем счетчик, когда на выводе датчика скорости устанавливается высокий уровень
  Timer1.attachInterrupt( timerIsr ); // включаем таймер


  myPID.SetMode(AUTOMATIC);
} 

void loop()
{
  //Ввод = timerIsr();
  pot=analogRead(POT)/4.01569;
  //Уставка = sp;

  //Serial.print(Вход);

  analogWrite(ENA, pot);
  digitalWrite(10, HIGH);  // устанавливаем вращение двигателя по часовой стрелке
  digitalWrite(11, LOW);

  delay (250);
}

Как видите, я прокомментировал кое-что, связанное с библиотекой PID. Прямо сейчас я просто пытаюсь передать переменную вращения в функцию цикла().

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

, 👍0


1 ответ


2

Краткий ответ: Да, можно обновлять глобальные переменные из подпрограмм обработки прерываний, но это также сопряжено с проблемами.

Развернутый ответ:

Сначала вам нужно понять, как работают прерывания. Вы используете прерывание по таймеру и внешнее (выводное) прерывание. Скомпилированная программа лежит во флэше чипа. В начале есть несколько специальных адресов для различных прерываний. Если оборудование затем зарегистрирует условие прерывания, оно остановит выполнение основного кода (кода, который был выполнен при возникновении условия прерывания), сохранив текущее состояние и перейдя на соответствующий адрес, куда он будет отправлен. к ISR (программе обслуживания прерываний), которую вы определили (в вашем случае функция docount()). После выполнения этой программы микроконтроллер вернется к выполнению основного кода (восстановив ранее сохраненное состояние и перейдя туда). ISR может иметь только тип возвращаемого значения void (без возвращаемого значения), поскольку при его возврате не существует специального кода для получения и использования этого значения. Прерывание может каждый раз происходить в разное время.

Вместо этого принято записывать глобальные переменные в ISR. Для этого вам необходимо учитывать некоторые моменты:

  1. Глобальные переменные, которые могут быть изменены во время ISR, должны быть объявлены (дополнительно к типу) как Летучие. Это означает, что компилятор не может оптимизировать эту переменную (например, в локальную переменную) и должен учитывать, что она может измениться в любое время, поэтому он не может использовать кэшированное значение.
  2. Использование переменной в основном коде должно быть атомарным или защищенным от изменения переменной. Если вы используете переменную в расчете, это может занять много циклов (особенно, если у вас большой тип данных или некоторые дорогостоящие вычисления, как в случае с float). Если прерывание произойдет в это время, значение может быть изменено в середине расчета, что сделает результат бесполезным. Если вычисление занимает только один цикл операторов, его нельзя прервать и он называется атомарным. Поскольку это редко возможно, вы можете включить соответствующий код в защитный код. Для платформы Arduino вы можете использовать функции noInterrupts() и interrupts(), чтобы отключить все прерывания, выполнить расчет и снова включить прерывания. Обязательно отключайте прерывания только на короткое время, чтобы не пропустить прерывания. Часто это используется только для копирования значения переменной в локальную переменную (локальную рабочую копию), которую затем можно безопасно использовать для вычислений, в то время как глобальная переменная все равно может измениться.

Кроме того, вам также нужно подумать о том, какой код вы пишете в ISR. ISR не может быть прерван другим прерыванием. Таким образом, функции, работа которых зависит от прерываний, не будут вести себя должным образом. Например, функция delay() просто заблокирует выполнение всего кода, поскольку результат возврата функции millis() не изменится (это зависит от прерывания timer0). В вашем ISR Timer1 вы используете Serial.print(). Эту функцию можно использовать, но только до тех пор, пока не будет заполнен буфер библиотеки Serial. Фактическая связь произойдет после возврата ISR, поскольку она также зависит от прерываний. Когда вы попытаетесь отправить больше, чем помещается в буфере (64 байта), вы потеряете данные. В настоящее время вы должны быть ниже этого уровня, но вы должны иметь это в виду при кодировании.


Я не думаю, что вам действительно нужно прерывание по таймеру для расчета и вывода данных каждую секунду. Здесь я бы использовал неблокирующий стиль кодирования в функции loop() , как вы можете видеть в примере BlinkWithoutDelay, который поставляется с Arduino IDE. Это лучше, чем использовать delay(), который просто занят ожиданием. Вы сохраняете временную метку с помощью функции millis() и проверяете разницу во времени. Это сохранит Timer1 для других целей, а также приведет к рефакторингу вашего основного кода, чтобы сделать его расширяемым и неблокирующим, если вы захотите добавить больше функций.


Как я прочитал из этого примера из PID библиотеке вам нужно время от времени вызывать myPID.Compute(), чтобы выполнять вычисления PID с новыми значениями. Поскольку они меняются только каждую секунду, вам следует вызывать эту функцию сразу после вычисления Input.


В целом вам нужно что-то вроде этого (я не включил весь код, только важную часть, чтобы вы могли понять принцип):

unsigned long timestamp;
#define INTERVAL 1000

void loop(){
    if(millis() - timestamp >= INTERVAL){
        timestamp += INTERVAL;
        noInterrupts();
        int counter_copy = counter;
        interrupts();
        // Выполняем вычисления с помощью counter_copy и получаем тот же результат, что и для `Input`
        // Вы можете поместить сюда код вашего timerisr (без кода Timer1)
        myPID.Compute();
    }
}

Ваш код pot также должен быть заключен в такой оператор if, чтобы предотвратить очень быстрый перезапуск ШИМ снова и снова. Если время реакции в 1 с вас устраивает, вы можете поместить этот код в оператор if выше. Если вам нужно более быстрое время реакции, вы можете добавить второй оператор if, подобный этому, в функцию loop() (включая его собственную глобальную переменную метки времени и значение интервала).

Вызовы digitalWrite(), указывающие направление, можно включить в настройку, поскольку они никогда не изменяются в вашем коде.

,

Вы имеете в виду if (millis() - временная метка >= INTERVAL)., @Edgar Bonet

О да, вы правы. Я это исправил. Спасибо., @chrisl

Итак, вы хотите сказать, что я могу в значительной степени свести все мои прерывания к этому циклу, просто используя временные метки? Я пытаюсь переварить всю эту информацию и применить ее, извините., @igomez

Нет, не все. У вас все еще есть внешнее прерывание, которое вы присоединяете с помощью AttachInterrupt() и функции docount(). Это имеет смысл, потому что вы не хотите пропустить ни одного импульса. Я исключил это из своего кода, поскольку оно точно такое же, как и в вашем коде. Я отказался только от прерывания Timer1. Я думаю, точность интервала в 1 с не так уж и важна, для этого действительно нужен собственный аппаратный таймер. Если вы используете неблокирующий код, цикл() выполняется очень быстро, так что этого должно быть достаточно., @chrisl