#include <Arduino.h>
#include <WiFi.h>
#include <WiFiUdp.h>
#define TELEPLOT_URL "teleplot.fr"
#define TELEPLOT_PORT 36948
WiFiUDP udp;
#define WIFI_SSID "Wokwi-GUEST"
#define WIFI_PASSWORD ""
// Defining the WiFi channel speeds up the connection:
#define WIFI_CHANNEL 6
volatile int motorPos = 0;
#define ppr 100 // pulses per revolution
#define PWM_FREQ 5000
#define PIN_MOTOR_P 25
#define PIN_MOTOR_N 26
#define PWM_CHANNEL_0 0
#define PWM_CHANNEL_1 1
#define PIN_ENCODER_A 34
#define PIN_ENCODER_B 35
#define MIN(a,b) (((a)<(b)) ? (a) : (b))
#define MAX(a,b) (((a)>(b)) ? (a) : (b))
void IRAM_ATTR pinChangeInterrupt();
void setSpeed(int speed);
float mse_best = 0.0;
float kp_test = 1.5, ti_test = 9.0, td_test = 0.1;
float kp_best = 1.5, ti_best = 9.0, td_best = 0.1;
float mse = 0.0;
unsigned int mse_cnt = 0;
typedef struct {
float kp;
float ti;
float td;
float integral;
float prev_err;
unsigned long prev_ms;
} controller_t;
void pid_init(controller_t* pid, float kp, float ti, float td) {
pid->kp = kp;
pid->ti = ti;
pid->td = td;
pid->integral = 0.0;
pid->prev_err = 0.0;
pid->prev_ms = 0;
}
float pid_update(controller_t* pid, float setpos, float motorPos) {
unsigned long now_ms = millis();
unsigned long dt_ms = now_ms - pid->prev_ms;
float dt = dt_ms / 1000.0;
if (dt == 0) return 0; // Prevent division by zero
float err = setpos - motorPos;
pid->integral += err * dt;
pid->integral = fmax(-1000, fmin(1000, pid->integral)); // Anti-windup
float derivative = (err - pid->prev_err) / dt;
float cmd = pid->kp * (err + pid->integral / pid->ti + pid->td * derivative);
cmd = fmax(-255, fmin(255, cmd));
pid->prev_err = err;
pid->prev_ms = now_ms;
return cmd;
}
// PID instance
controller_t servo_pid;
void setup() {
Serial.begin(115200);
Serial.printf("\n\n\n\n\n");
WiFi.begin(WIFI_SSID, WIFI_PASSWORD, WIFI_CHANNEL);
while (WiFi.status() != WL_CONNECTED) {
delay(100); Serial.print(".");
}
Serial.printf("\nConnected as %s\n", WiFi.localIP().toString().c_str());
/* motor outputs */
pinMode(PIN_MOTOR_P, OUTPUT);
pinMode(PIN_MOTOR_N, OUTPUT);
/* 5 kHz PWM, 8-bit resolution */
/*
ledcSetup(PWM_CHANNEL_0, PWM_FREQ, 8);
ledcSetup(PWM_CHANNEL_1, PWM_FREQ, 8);
ledcAttachPin(PIN_MOTOR_P, PWM_CHANNEL_0);
ledcAttachPin(PIN_MOTOR_N, PWM_CHANNEL_1);
*/
ledcAttach(PIN_MOTOR_P, 10000, 8);
ledcAttach(PIN_MOTOR_N, 10000, 8);
/* quadrature encoder inputs */
pinMode(PIN_ENCODER_A, INPUT);
pinMode(PIN_ENCODER_B, INPUT);
motorPos = -100;
attachInterrupt(PIN_ENCODER_A, pinChangeInterrupt, CHANGE);
Serial.printf("setPos, motorPos, err, der, int, cmd\n");
pid_init(&servo_pid, kp_best, ti_best, td_best);
}
/**
* @brief Set motor speed
* @param speed (-255: full speed CCW ... 0: stop ... +255: full speed CW)
*/
void setSpeed(int speed) {
speed = MAX(-255, MIN(255, speed));
if (speed > 0) {
ledcWrite(PIN_MOTOR_P, 255);
ledcWrite(PIN_MOTOR_N, 255 - speed);
} else {
ledcWrite(PIN_MOTOR_P, 255 + speed);
ledcWrite(PIN_MOTOR_N, 255);
}
}
void loop() {
/* Read user input */
unsigned long now_ms = millis();
/* Generate square signal for setpos */
int setpos = ((now_ms % 4000) < 2000) ? 20 : 0;
/* PID regulation */
float cmd = pid_update(&servo_pid, setpos, motorPos);
/* Actuate motor */
setSpeed((int)cmd);
/* Print out data for display (use the Serial plotter to graph out data) */
Serial.printf("%9d, %9d, %9d\n", setpos, motorPos, (int)cmd);
/* PID auto-tuning */
float err = setpos - motorPos;
mse += err * err;
mse_cnt ++;
static unsigned long last_tune_ms = 0;
const unsigned long tune_interval_ms = 12000;
if (now_ms - last_tune_ms > tune_interval_ms) {
mse = mse / mse_cnt;
setSpeed(0);
Serial.printf("Params %3.1f, %3.1f, %3.1f:",
kp_test, ti_test, td_test);
Serial.printf("MSE current %9.3f, best %9.3f\n", mse, mse_best);
sendTelemetry("mse", mse);
sendTelemetry("mse_best", mse_best);
sendTelemetry("kp_test", kp_test);
sendTelemetry("ti_test", ti_test);
sendTelemetry("td_test", td_test);
if (now_ms > 2 * tune_interval_ms) {
if ((mse < mse_best) || (mse_best == 0.0)) {
// use new parameter set
mse_best = mse;
kp_best = kp_test;
ti_best = ti_test;
td_best = td_test;
Serial.printf("Updated set %3.1f, %3.1f, %3.1, %3.1f\n", kp_best, ti_best, td_best, mse_best);
} else {
// roll back
kp_test = kp_best;
ti_test = ti_best;
td_test = td_best;
}
// randomly update one parameter
int param = rand() % 3;
float factor = (rand() % 2 == 0) ? 1.1 : 0.9;
switch(param) {
case 0: kp_test *= factor; break;
case 1: ti_test *= factor; break;
default: td_test *= factor; break;
}
}
// reset PID regulation parameters
pid_init(&servo_pid, kp_test, ti_test, td_test);
mse = 0.0;
mse_cnt = 0;
last_tune_ms = now_ms;
}
delay(100);
}
/**
* @brief Read quadrature encoder signals, and update shaft position
*/
void IRAM_ATTR pinChangeInterrupt() {
int a = digitalRead(PIN_ENCODER_A);
int b = digitalRead(PIN_ENCODER_B);
motorPos += (a ^ b) ? 1 : -1;
}
void sendTelemetry(const char *signal, float value)
{
static char buffer[256];
uint32_t now_ms = millis();
snprintf(buffer, sizeof(buffer), "%s:%3.3f",
signal, value);
udp.beginPacket(TELEPLOT_URL, TELEPLOT_PORT);
udp.write((const uint8_t*)buffer, strlen(buffer));
udp.endPacket();
}
void sendTelemetry(const char *signal, int value)
{
static char buffer[256];
uint32_t now_ms = millis();
snprintf(buffer, sizeof(buffer), "%s:%d",
signal, value);
udp.beginPacket(TELEPLOT_URL, TELEPLOT_PORT);
udp.write((const uint8_t*)buffer, strlen(buffer));
udp.endPacket();
}