“Simply PID loop in Arduino does not work when I send a value through serial”

huangapple go评论60阅读模式
英文:

Simply PID loop in Arduino does not work when I send a value through serial

问题

我有一个在Arduino Nano上运行的PID环,计划通过USB连接到Raspberry Pi。我正在在我的笔记本电脑上进行一些测试,试图确保它正常工作,但它没有。下面是代码。您可以看到,我在下面的循环中注释掉了target = sin的部分。当target = rpm被注释掉,而target = sin未被注释时,程序运行完美。然而,当我注释掉target = sin并取消注释target = rpm时,它不起作用。我已经向Nano发送了值到串行监视器窗口,我知道它已经收到了值,因为我在监视器中看到了"Transmission received"。当target = sin处于活动状态时,目标值也在串行监视器中更新,但当我发送值给它时,串行监视器中的目标值为零。有人能解释一下我做错了什么吗?

英文:

I have a PID loop running on an Arduino Nano that I plan to connect to a Raspberry Pi via USB. I'm doing some testing on my laptop and I am trying to make sure it works, but it doesn't. The code is posted below. You can see that I commented out the target = sin function in the loop below. When the target = rpm is commented out, and the target = sin is not. The program runs perfectly. However, when I comment out the target = sin and uncomment the target = rpm, it does not work. I have sent values to the nano in the serial monitor window, and I know that it has received the value because I see "Transmission received" in the monitor. The target value is also updated in the serial monitor when the target = sin is active, but the target value says zero in the serial monitor when I send a value to it. Could someone please explain what I am doing wrong?



#include <CytronMotorDriver.h> 


  // Configure the motor driver.
CytronMD motorR(PWM_DIR, 3, 4);  // PWM 1 = Pin D3, DIR 1 = Pin D4.

// Configure encoder pins
#define EncoderA 6 // yellow wire 
#define EncoderB 7 // green wire

volatile int posi = 0;      // position of motor
long previousT = 0;
float eprevious = 0;
float eintegral = 0;

int rpm;
int rpm_correction_factor = 1;
float wheel_circumference = 0.090;

// The setup routine runs once when you press reset.
void setup() {
  Serial.begin(9600);
  pinMode(EncoderA, INPUT);
  pinMode(EncoderB, INPUT);
  attachInterrupt(digitalPinToInterrupt(EncoderA), readEncoder, RISING); //whenever encoder phase A is HIGH, readEncoder function is called
  Serial.println("target position");
  }


// setSpeed can be any number for -255 to 255 (negative means reverse)
void loop() {
   //check if arduino has received some data. if data has arrived, use readStringUntil to get the next line. All bytes received until /n are converted to useable communication data
    if (Serial.available() > 0) {
        String received_speed = Serial.readStringUntil('\n');
        int rpm = round(rpm_correction_factor * (received_speed.toInt())/(wheel_circumference * 3.14 * 60));
        Serial.print("transmission received");
    }
  
  
  //int target = 250*sin(previousT/1e6); //test target position for PID control loop
  int target = rpm; //target position from serial port 

  //PID constants (these values may have to be adjusted)
  float kp = 1;
  float kd = 0;
  float ki = 0;

  //finding the time difference
  long currentT = micros();
  float deltaT = ((float)(currentT-previousT))/1.0e6;
  previousT = currentT;  

  int motor_position = 0;
  noInterrupts();
  motor_position = posi;
  interrupts();


  //error calculation
  int e = motor_position - target; //position and target may have to be switched depending on how the motor leads are wired

  //derivative
  float dedt = (e - eprevious)/(deltaT);

  //integral
  eintegral = eintegral + e*deltaT;

  //control signal
  float u = kp*e + kd*dedt + ki*eintegral;

  //set motor power and limit it to between -255 and +255
  //float power = fabs(u);
  float power = u;
  if(power>255){
    power = 255;
  }
  if(power<-255){
    power = -255;
  }

  motorR.setSpeed(power);
  //store previous error
  eprevious = e;

  //print target and position to serial plotter
  Serial.print(target);
  Serial.print(",");
  Serial.println(motor_position);
  //Serial.println();

 
}

void readEncoder(){
  // called when EncoderA is high; If the motor is rotating counterclockwise, EncoderB should already be high, so we would add one to the position 
  int b = digitalRead(EncoderB);
  if(b>0) {
    posi ++;
    }
  else{
    posi --;
    }
  }


答案1

得分: 2

看起来问题是程序员称之为"遮蔽"的情况。你有一个在顶部定义的变量rpm,使它成为了一个全局变量:

int rpm;
int rpm_correction_factor = 1;
float wheel_circumference = 0.090;

然而,当你从Serial中读取时,实际上将该变量重新定义为if块的"作用域"内的局部变量:

    if (Serial.available() > 0) {
        String received_speed = Serial.readStringUntil('\n');
        int rpm = round(rpm_correction_factor * (received_speed.toInt())/(wheel_circumference * 3.14 * 60));
        Serial.print("transmission received");
    }

这个rpm变量不是与你的全局rpm相同的变量。它只在if块内部可见。一旦离开if块,该变量就不复存在。实际上,你从未设置你在这里引用的rpm变量:

int target = rpm; //从串口获得的目标位置

这个rpm变量在你的程序中从未被修改,因此它保持为0。所以这里的解决方案是在rpm之前删除int

    if (Serial.available() > 0) {
        String received_speed = Serial.readStringUntil('\n');
        rpm = round(rpm_correction_factor * (received_speed.toInt())/(wheel_circumference * 3.14 * 60));
        Serial.print("transmission received");
    }

这样你只能赋值给全局版本。我不确定为什么这没有引发某种警告。我不是一个Arduino专家,但确保启用了所有警告。这应该至少会触发一个"-Wall"的"未使用变量"警告。如果这是gcc或clang,可以使用"-Wshadow"启用更具体的警告。

英文:

it looks like the issue is something us programmers call "shadowing." You have a variable rpm that is defined at the top making it a global variable:

int rpm;
int rpm_correction_factor = 1;
float wheel_circumference = 0.090;

However, when you read from Serial, you actually redefine that variable as local to the "scope" of the if block:

    if (Serial.available() > 0) {
        String received_speed = Serial.readStringUntil('\n');
        int rpm = round(rpm_correction_factor * (received_speed.toInt())/(wheel_circumference * 3.14 * 60));
        Serial.print("transmission received");
    }

This rpm variable is not the same as your global rpm. It is only visible inside the if block. Once you leave the if block, that variable no longer exists. In essence, you never set rpm variable that you are referencing here:

int target = rpm; //target position from serial port

This rpm variable is never touched in your program, therefore, it remains 0. So the solution here is to remove the int before rpm:

    if (Serial.available() > 0) {
        String received_speed = Serial.readStringUntil('\n');
        rpm = round(rpm_correction_factor * (received_speed.toInt())/(wheel_circumference * 3.14 * 60));
        Serial.print("transmission received");
    }

This way you can only assign to the global version. I'm not sure why this didn't throw some kind of warning. I'm not an arduino guy, but make sure all warnings are enabled. This should have at least triggered an "unused variable" with -Wall. If this were gcc or clang, a more specific warning could be enabled with -Wshadow.

答案2

得分: 0

  1. 使用3.14而不是(例如)M_PI
  2. 将值发送为ASCII字符(如Serial.readStringUntil('\n');所示)会增加问题(最好发送固定大小的二进制数据值)。
  3. 尽管通过某个调试端口(例如UART)发送调试信息是好的,但发送/接收这些信息的时间会干扰电机控制所需的实际时序。

在为商业机器人电机控制系统(使用Linux和ROS的控制系统)完成此操作后,向Arduino(使用FreeRTOS的等效系统)发送位置值,控制变得“松散”。

Arduino必须在精确的时间间隔内提供PWM值。它必须能够处理由RPi发送的丢失/损坏的值。

我们发现控制系统应该[周期性地]发送“轨迹/速度”值,而不是位置值。也就是说,发送(例如)旋转速度值(例如弧度/秒)。或者,一个“路径”,例如机器人在旅程的某个段落的最终地板位置。

当发送位置值时,您需要进行大量脆弱的计算,以根据端口速度、RPi的出发时间戳和Arduino上的到达时间戳来计算到达值的延迟。

相反,让Arduino采用速度值并自行计算PWM值。

然后,Arduino可以以高速计算PWM位置值,只有在RPi确定速度/轨迹变化时才需要来自RPi的控制。

考虑这个类比... 两个人在一辆车上。一个是司机[Arduino],另一个在看地图[RPi]。如果乘客说在拐角处右转,乘客不应该告诉司机如何在每时每刻实施转弯。例如,乘客不应该发送命令,如:右转方向盘到30度,施加刹车力,保持1.3秒,将方向盘恢复到直立位置等。

英文:

A few issues ...

  1. Using 3.14 instead of (e.g.) M_PI
  2. Sending the values in ASCII (as evidenced by the Serial.readStringUntil('\n'); further adds to the issue (better to send fixed size binary data values).
  3. Although it is good to send debug information back via some debug port (e.g. the UART), the time to send/receive these perturbates the real timing that is needed for the motor control.

Having done this for a commercial robotics motor control system (control system using linux (and ROS)), sending position values to Arduino (equivalent system using FreeRTOS), the control is "sloppy/loose".

The Arduino must provide the PWM values at precise intervals. It must be able to handle missed/corrupted values sent by the RPi.

We found that the control system should [periodically] send "trajectory/velocity" values instead of position values. That is, send (e.g.) rotational velocity values (e.g. radians / second). Or, a "path", such as final floor position of the robot for the given leg of the journey.

When you send position values, you have a lot of fragile calculations to calculate the latency of the arrived values, based on port speed, departure timestamps (from the RPi) and arrival timestamps on the arduino.

Instead, have the arduino take the velocity values and calculate the PWM values itself.

Then, the arduino can calculate the PWM position values at a high rate, and only need control from the RPi if the RPi determines that the velocity/trajectory changes.

Consider this analogy ... Two people are in a car. One is the driver [Arduino] and the other is reading a map [RPi]. If the passenger says turn right at the corner, the passenger should not tell the driver how to implement the turn on a moment-by-moment basis. (e.g.) The passenger should not be sending commands such as: Turn steering wheel right to 30 degrees, apply braking force, hold for 1.3 seconds, return steering wheel to upright position, etc.

huangapple
  • 本文由 发表于 2023年4月10日 22:51:16
  • 转载请务必保留本文链接:https://go.coder-hub.com/75978132.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定