Contents

Lineær aktuator med ESP32

Oppgaven

Jeg har fått i oppgave å styre en lineær aktuator med en klemsikring.

For å gi oppgaven en ekstra utfordring, valgte jeg å bruke transistorer for å kontrollere motoren. For å oppnå muligheten til å styre motoren i begge retninger, måtte jeg lage en H-bro.

/aktuator/hbridge.png
H-bridge ( kilde )

Prinsippet er veldig simpelt: Jeg bruker fire transistorer, og ved å slå på to av dem samtidig, styrer jeg retningen strømmen beveger seg. I praksis viste dette seg til å være noe mer utfordrende, da ikke alle transistorer er like. Jeg måtte velge en transistor som kunne håndtere strømmene som ville passert gjennom kretsen, samtidig som den lot seg styre via mikroprosessoren min.

Min valgte mikroprosessor var en ESP32 jeg hadde tilgjengelig. Denne kan programmeres på samme måte som en vanlig Arduino via Arduino IDE, men den har også innebygd støtte for WiFi og Bluetooth. Den mest betydningsfulle forskjellen fra en Arduino er imidlertid at den opererer med 3,3V I/O i stedet for 5V. Dette påvirket valgene mine av komponenter for å sikre kompabilitet i systemet mitt.

Teori

For å kunne måle strømmen i kretsen min, spiller Kirchoffs lover en avgjørende rolle. Kirchoffs strømlov fastslår at summen av strømmene inn til et punkt er lik summen av strømmene ut, og Kirchhoffs spenningslov sier at summen av potensialforskjellene rundt en lukket krets er null. Dette betyr at jeg kan få en indirekte indikasjon på strømmen gjennom kretsen min ved å måle spenningsfallet over en strategisk plassert motstand med kjent verdi.

/aktuator/spenningsfall.jpg
Måler spenningsfall over motstand R1

Dersom jeg får et spenningsfall på 1V over motstanden på 0,47Ω flyter det i følge Ohms lov 2,1A gjennom kretsen

Valg av komponenter

Jeg har valgt å bruke en ESP32 i stedet for en Arduino for å gjennomføre prosjektet mitt. Hovedgrunnen til dette er at ESP32 har WiFi og Bluetooth, noe som gir meg muligheten til å utfordre meg selv ved å implementere fjernstyring via nettet. Planen min inkluderer å utvikle en enkel web-visualisering for å kontrollere aktuatoren og vise statusen.

Den største forskjellen mellom ESP32 og Arduino Uno er at ESP32 opererer med 3.3V I/O. Dette innebærer at de analoge inngangene jeg benytter, bør ha en maksimal spenningsinngang på 3.3V. ESP32-en har en 12-bit ADC, noe som gir 4096 mulige verdier. Dette betyr at mikrokontrolleren har en teoretisk oppløsning på 3300mV / 4096 = 0,8mV.

Ved valg av shuntmotstand stilte jeg følgende kriterier:

  • Den må tåle strømmen som passerer gjennom den.
  • Under normal drift av motoren må jeg oppnå et spenningsfall innenfor måleområdet til mikroprosessoren.
  • Spenningsfallet må ikke være så høyt at det påvirker motorens funksjon.

Selv om den lineære aktuatoren maksimalt trekker 1.2A under drift, har jeg satt dimensjonerende maksimalstrøm til 2A som en sikkerhetsmargin.

Jeg valgte shuntmotstanden jeg hadde tilgjengelig, en på 0.47 Ohm som tåler 5 Watt. Ved 2A gjennom motstanden blir det avgitt 4.25 Watt, noe som er innenfor motstandens kapasitet. Jeg tar hensyn til at startstrømmen til motoren kan være høyere, men dette vil være i korte tidsperioder slik at motstanden ikke rekker å varme seg opp til skadelige temperaturer.

/aktuator/kurve.jpg
Døsone ( kilde )

Analog-til-digital omformeren til mikroprosessoren er ikke-lineær, noe som skaper en dødsone rundt nedre målegrense (0V) og (3.3V). Dette påvirker nøyaktigheten til de avleste verdiene.

For å løse dette kunne jeg ha valgt en høyere motstand for å øke spenningsfallet over den ved lavere strømtrekk. Jeg valgte imidlertid å ikke gjøre dette for å unngå lavere spenningsfall over motoren. I stedet benytter jeg en signalforsterker med gain-faktor på 2 for å forsterke signalet fra shuntmotstanden. Dette beskytter mikroprosessoren fra spenningsvariasjoner, da forsterkeren er koblet til 3.3V-forsyningen til mikroprosessoren, og spenningen ut av forsterkeren aldri kan overstige 3.3V. Ulempene med denne løsningen er at den vil begrense målenøyaktigheten, og at forsterkeren også vil forsterke støy.

/aktuator/signalamp.png
Signalforsterker

Mikroprosessoren forsynes av en 24V-5V DC-DC-omformer, med en bryter for å koble fra strømforsyningen ved behov. Ved tilkobling via USB bør denne strømforsyningen være inaktiv.

Programmering

Jeg har valgt å benytte Arduino Cloud for å programmere mikrokontrolleren. Dette gir meg fordelen av å bruke den velkjente Arduino IDE-en samtidig som det tilbyr ekstra funksjoner, inkludert muligheten til å lage en webvisualisering for programmet mitt.

En begrensning jeg støter på under programmeringen er at jeg ikke kan benytte delay-funksjonen med lange forsinkelser. Dette skyldes at WiFi-funksjonaliteten ville bli ustabil på grunn av lengre syklustider i programmet mitt. Denne begrensningen har ført til at programmet mitt har blitt betydelig mer avansert enn nødvendig. Dette skyldes at alle steder som krever tidsforsinkelser nå involverer spesielle timere som teller tiden fra Arduinoen startet kjøringen av programmet sitt. Eksempler på steder der dette er relevant inkluderer tidsforsinkelsen for når strømovervåkingen blir aktivert og funksjonen som kjører aktuatoren inn etter at den har stoppet på grunn av sikkerhetsfunksjonen.

Jeg har plukket ut den viktigste logikken fra programmet mitt.

Følgende snippet kaller funksjonen getAverageReadings som henter inn gjennomsnittsverdier fra ADC. Den bruker så Ohms lov til å beregne strømtrekket til motoren.

// Strøm motor:

getAverageReadings(); // Gjennomsnitt av "rå" verdier fra ADC. 0 - 4095
vDrop = average / (4095 / 3.3); // Beregner spenningsfall over motstand. 1240.9 verdier per volt.
motor_current = (vDrop / resistor) / ampGain; // Ohms Lov

Følgende funksjon kalles når aktuatoren skal kjøres. Første variabel styrer retning, andre variabel styrer hastighet.

void moveActuator(bool dir, int speed) { // dir 0 = inn, dir 1 = ut. Speed: 0-255

  timeSinceStart = curTime;     // Setter tid siden start til gjeldende tid

  if (dir == 1) {               // Dersom retning er UT....
    analogWrite(sw2, 0);        // Slå av sw2
    digitalWrite(sw4, LOW);     // Slå av sw4
    delay(10);                  // Ventetid for å lade ut transistorer
    analogWrite(sw1, speed);    // Skriv PWM signal med hastighet "speed" til sw1
    digitalWrite(sw3, HIGH);    // Slå på sw3
    movingTo = 2;               // Sett status "kjører ut"
    delay(1);
  }
  else {
    analogWrite(sw1, 0);
    digitalWrite(sw3, LOW);
    delay(10);
    analogWrite(sw2, speed);
    digitalWrite(sw4, HIGH);
    movingTo = 1;
    delay(1);
  }
}

Klemsikring aktiveres når motorstrømmen er høyere enn strømgrensen. Funksjonen stopper aktuatoren og setter boolean variabelen overloaded til true.

// Stopp aktuator ved høy strøm
if ((motor_current >= curLimit) && !overloaded) {
      timeSinceOverload = curTime;  // Setter tid når overlast skjedde til gjeldende tid
      overloaded = 1;               // Setter overload til TRUE
      position = 0;                 // Setter posisjon til ukjent
      moveActuator(0, 255);         // Kjør aktuatoren ut
    }
    
    // Stopp aktuatoren når den har kjørt ut litt etter en overlast skjedde.
    if ((curTime - timeSinceOverload >= 500) && overloaded && !trigger) {
      haltActuator();               // Stopp aktuator
    }

Knapp på HMI styrer når aktuatoren kjøres inn og ut. Programmet holder styr på hvor aktuatoren er, så retning er bestemt av posisjonen til aktuatoren. Når trigger knappen aktiveres når klemsikring er blitt aktivert (variabelen overloaded) kjøres aktuatoren ut med sakte fart.

void onTriggerChange()  {
  if (trigger && !overloaded) {

    if (position <= 1) {
      // Aktuator er inne eller status er ukjent.
      moveActuator(1, 255);
    }
    else {
      // aktuator er ute. Kjør aktuator inn.
      moveActuator(0, 255);
    }
  }
  else if (trigger && overloaded) {
    // Tving aktuator ut med redusert hastighet dersom trigger holdes inne
    moveActuator(1, slowSpeed);
  }
}

Hele programmet, inklusiv hjelpefunksjoner, er tilgjengelig i slutten av artikkelen.

Testing

Aktuatoren styres via webgrensesnittet. Trigger knappen kjører aktuatoren inn eller ut avhengig av posisjon.

/aktuator/HMI1.jpg
Visualisering viser posisjon, strømtrekk og en trigger knapp
/aktuator/HMI2.png
Strømtrekk vises på skjermen når aktuatoren er i bevegelse

Elsikkerhet

Maskinsikkerheten er ivaretatt ved at jeg har implementert en klemsikringsfunksjon som får motoren til å reversere litt når den oppdager overbelastning. Elsikkerheten er ikke bekymringsfull siden jeg bruker en 24VDC SELV-krets til å forsyne systemet, via en variabel strømforsyning med strømbegrensningsfunksjonen satt til 1 ampere.

I en reell, industriell situasjon ville vi ikke ha brukt en Arduino-mikrokontroller til å håndtere slike oppgaver, i hvert fall ikke sikkerhetsfunksjoner som klemsikringer. Vi ville ha brukt en sikkerhets-PLS med SIL-klassifisering, og for eksempel et lysgitter til å overvåke arbeidsområdet til aktuatoren, slik at hovedstrømmen brytes hvis noen går inn på området. Å implementere en klemsikringsfunksjon ved å måle strømtrekk på større motorer vil være nesten umulig å løse på en trygg og pålitelig måte. Vi måtte også ha vurdert bruken av en sikkerhetsbryter ved motoren, slik at vedlikehold kan utføres på en trygg måte.

Dokumentasjon

Functional Acceptance Test

Test Resultat Kommentar
Motor kjører ut OK
Motor kjører inn OK
HMI viser status OK
HMI viser strømtrekk OK
Motor stopper ved overlast OK
Motor kjører litt tilbake etter overlast OK
Motor kan hastighetsreguleres OK
Wifi funksjonalitet OK

IO-Liste

Type Datatype Adresse Betegnelse Funksjon
AI INT 35 -R1 Analoge verdier fra shuntmotstand
DO INT 18 -Q3 Utgang til transistor -Q3
DO INT 19 -Q4 Utgang til transistor -Q4
DO INT 20 -Q2 Utgang til transistor -Q2
DO INT 21 -Q1 Utgang til transistor -Q1

Koblingsskjema

/aktuator/skjema.png
Koblingsskjema

Kildekode

// Koen: Kildekode må oppdateres med nyeste rev.

/*
Laget av Koen Van Eck - 2023
*/

#include "thingProperties.h"

// IO
const int currentPin = 35;      // Inngang som leser av strøm
const int sw1 = 18;             // NPN 1 (TIP 120)
const int sw2 = 19;             // NPN 2 (TIP 120)
const int sw3 = 23;             // PNP 2 (TIP 125)
const int sw4 = 22;             // PNP 1 (TIP 125)

// Setup
const float resistor = 0.47;    // Motstand verdi i ohm
const int slowSpeed = 120;      // Hastighet etter aktuator stopper på overlast. 0-255
const int smoothReadings = 10;  // Antall målinger som for å beregne gjennomsnitt
const long interval = 500;      // Ventetid før overlastbeskyttelse aktiveres i ms

// Variabler
int currentRaw = 0;             // 0 - 4095
float vDrop = 0;                // 0 - 3,3
int position = 0;               // 0 = Ukjent, 1 = inne, 2 = ute
int movingTo = 0;               // 0 = ingen bevegelse, 1 = inn, 2 = ut.
bool overloaded = 0;            // 0 = vanlig drift, 1 = slowSpeed
int ampGain = 2;                // Forsterkning av spenningsfall over motstand via op-amp

// Smoothing
int readings[smoothReadings];   // Array med størrelse på "smoothReadings (10)"
int readIndex = 0;              // Index
int total = 0;
int average = 0;

// Timer
unsigned long curTime = 0;              // Gjeldende tid

unsigned long timeSinceStartMoving = 0; // Tid siden aktuator begynte å bevege
unsigned long timeSinceOverload = 0;    // Tid siden overbelastning


void setup() {
  pinMode(sw1, OUTPUT);
  pinMode(sw2, OUTPUT);
  pinMode(sw3, OUTPUT);
  pinMode(sw4, OUTPUT);

  // Åpne serial port
  Serial.begin(9600);
  delay(1500);

  // Defined in thingProperties.h
  initProperties();

  // Connect to Arduino IoT Cloud
  ArduinoCloud.begin(ArduinoIoTPreferredConnection);
  
  setDebugMessageLevel(2);
  ArduinoCloud.printDebugInfo();
  
  // Reset alle verdier for gjennomsnittsmåling
  for (int thisReading = 0; thisReading < smoothReadings; thisReading ++) {
    readings[thisReading] = 0;
  }

}

void loop() {
  ArduinoCloud.update();

  // timer
  curTime = millis();

  // Strøm motor
  getAverageReadings();                         // Leser "rå" verdier fra ADC. 0 - 4095 og lager gjennomsnittverdi
  vDrop = average / (4095 / 3.3);               // Beregner spenningsfall over motstand. 1240.9 verdier per volt.
  motor_current = (vDrop / resistor) / ampGain; // Bruker ohms lov til å beregne strøm gjennom kretsen
  Serial.println(analogRead(currentPin));       // For debugging
  delay(1);

  // Ventetid slik at strømmen får stabilisere seg
  if (curTime - timeSinceStartMoving >= interval) {
    // Stopp aktuator ved høy strøm
    if ((motor_current >= curLimit) && !overloaded) {
      timeSinceOverload = curTime;  // Setter tid når overlast skjedde til gjeldende tid
      overloaded = 1;               // Setter overload til TRUE
      position = 0;                 // Setter posisjon til ukjent
      moveActuator(0, 255);         // Kjør aktuatoren ut
    }
    
    // Stopp aktuatoren når den har kjørt ut litt etter en overlast skjedde.
    if ((curTime - timeSinceOverload >= 500) && overloaded && !trigger) {
      haltActuator();               // Stopp aktuator
    }

    // Oppdater posisjon
    if (movingTo == 1 && motor_current <= 0.01) {
      haltActuator();
      position = 1;
    }
    else if (movingTo == 2 && motor_current <= 0.01) {
      haltActuator();
      position = 2;
      overloaded = 0;
    }
  }
  
  // Oppdater status tekst
  if (overloaded) {
    status = "Overbelastet!";
  }
  else if (movingTo == 1) {
    status = "Kjører inn";
  }
  else if (movingTo == 2) {
    status = "Kjører ut";
  }
  else if (position == 0) {
    status = "Posisjon ukjent";
  }
  else if (position == 1) {
    status = "Aktuator inne";
  }
  else if (position == 2) {
    status = "Aktuator ute";
  }
  else {
    status = "Ukjent";
  }

}

// Funksjon for å ta gjennomsnittet av analoge målinger
void getAverageReadings() {
  total = total - readings[readIndex];          // Fjern forrige måling med samme index fra totalen   

  readings[readIndex] = analogRead(currentPin); // Skriv gjeldende analoge verdi til reading arrayen

  total = total + readings[readIndex];          // Legg måleverdien til totalen

  readIndex = readIndex + 1;                    // Øk index

  if (readIndex >= smoothReadings) {            // Dersom indexen er blir høyere enn antall målinger, sett index til 0.
    readIndex = 0;
  }

  average = total / smoothReadings;             // Rekn ut gjennomsnitt
}

void moveActuator(bool dir, int speed) { // dir 0 = inn, dir 1 = ut. Speed: 0-255

  timeSinceStartMoving = curTime;     // Setter tid siden start til gjeldende tid

  if (dir == 1) {               // Dersom retning er UT....
    analogWrite(sw2, 0);        // Slå av sw2
    digitalWrite(sw4, LOW);     // Slå av sw4
    delay(10);                  // Ventetid for å lade ut transistorer
    analogWrite(sw1, speed);    // Skriv PWM signal med hastighet "speed" til sw1
    digitalWrite(sw3, HIGH);    // Slå på sw3
    movingTo = 2;               // Sett status "kjører ut"
    delay(1);
  }
  else {
    analogWrite(sw1, 0);
    digitalWrite(sw3, LOW);
    delay(10);
    analogWrite(sw2, speed);
    digitalWrite(sw4, HIGH);
    movingTo = 1;               // Sett status "kjører inn"
    delay(1);
  }
}

// Funksjon for å stoppe aktuator
void haltActuator() {
  analogWrite(sw1, 0);
  analogWrite(sw2, 0);
  digitalWrite(sw3, LOW);
  digitalWrite(sw4, LOW);
  movingTo = 0;                 // Sett status "posisjon ukjent"
}

// Funksjon som kjøres når knapp på HMI trykkes på
void onTriggerChange()  {
  if (trigger && !overloaded) {

    if (position <= 1) {
      // Aktuator er inne eller status er ukjent.
      moveActuator(1, 255);
    }
    else {
      // aktuator er ute. Kjør aktuator inn.
      moveActuator(0, 255);
    }
  }
  else if (trigger && overloaded) {
    // Tving aktuator ut med redusert hastighet dersom trigger holdes inne
    moveActuator(1, slowSpeed);
  }

}

void onCurLimitChange() {
  
}