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.
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.
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.
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.
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.
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
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() {
}