← Kursens material Kalibrering · område 4 Område 4

Område 4 · lektion 20

Kalibrering

Ett råvärde är ett tal mellan 0 och 1023. Det är inte en temperatur, inte en ljusstyrka och inte ett avstånd – det är bara ett tal. Den här guiden handlar om steget från talet till en storhet med enhet, och om vad som kan gå fel på vägen. Tre nya saker i språket kommer in här, och alla tre för att mätningen kräver dem: loopen, arrayen och din första egna funktion.

Mål

Genomgång

Två punkter räcker

En kalibrering är ett svar på frågan: vilken storhet svarar det här råvärdet mot? Svaret får du genom att ställa din givare bredvid något du litar på – en referens – läsa av båda samtidigt, och skriva ned paret. Gör det två gånger vid två olika värden, så har du två punkter. Genom två punkter går exakt en rät linje.

Linjen genom punkterna (x1, y1) och (x2, y2) har lutningen

k = (y2 − y1) / (x2 − x1)

och skrivs

y = y1 + k · (x − x1)

Här är x råvärdet och y storheten. Ingen matematik utöver det behövs.

Räknat exempel. En NTC i en delare, avläst mot en referenstermometer på två temperaturer:

PunktReferensen visadeRåvärdet var
120,0 °C569
230,0 °C456

Lutningen blir

k = (30,0 − 20,0) / (456 − 569) = 10,0 / (−113) = −0,0885 °C per steg

Minustecknet är väntat: NTC:ns resistans faller när det blir varmare, så råvärdet faller också. Linjen blir

T = 20,0 + (−0,0885) · (rå − 569)

Kontrollera den mot punkt 2 innan du går vidare:

T = 20,0 − 0,0885 · (456 − 569) = 20,0 − 0,0885 · (−113) = 20,0 + 10,0 = 30,0 °C

Och prova ett värde mitt emellan, rå = 512:

T = 20,0 − 0,0885 · (512 − 569) = 20,0 + 5,04 = 25,0 °C

Hur långt utanför punkterna linjen håller

NTC:ns verkliga samband är ingen rät linje – det såg du i förra guiden. Linjen är alltså en approximation, och frågan är hur bra. Tabellen nedan jämför tvåpunktslinjen med de råvärden en NTC på 10 kΩ i en 10 kΩ-delare faktiskt ger, räknade ur databladet.

Verklig temperaturRåvärdeLinjen sägerFel
0 °C7880,6 °C+0,6
10 °C6849,8 °C−0,2
15 °C62714,9 °C−0,1
20 °C56920,0 °Ckalibreringspunkt
25 °C51225,0 °C0,0
30 °C45630,0 °Ckalibreringspunkt
35 °C40334,7 °C−0,3
40 °C35439,0 °C−1,0
50 °C27046,5 °C−3,5

Läs tabellen nedifrån och upp. Tjugo grader utanför kalibreringsområdet är linjen 3,5 grader fel. Tio grader utanför är den en grad fel. Inuti området, mellan 20 och 30, är felet mindre än en tjugondels grad.

Regeln som följer: lägg dina två punkter i vardera änden av det område du faktiskt ska mäta i, inte där det är bekvämt att mäta. Ska instrumentet visa rumstemperatur räcker kranvatten och kroppsvärme. Ska det visa ugnstemperatur duger de inte alls.

Ett diagram med råvärde på den vågräta axeln, från 250 till vänster till 800 till höger, och temperatur på den lodräta axeln, från 50 grader upptill till 0 grader nedtill. En heldragen, svagt böjd kurva går från övre vänstra hörnet ned mot nedre högra. En streckad rät linje ligger nästan ovanpå kurvan i mitten och viker av från den mot båda ändarna. Två ifyllda punkter markerar där kurvan och den streckade linjen skär varandra. 50 0 °C 250 800 rå

Din första egna funktion

Omvandlingen från råvärde till grader behövs på mer än ett ställe. Redan i nästa guide ska samma tal både skrivas ut på seriemonitorn och styra ett servo, och att skriva samma formel två gånger är att skapa två ställen att rätta när kalibreringen ändras. Det är precis det en funktion löser.

I Python skrev du def ravarde_till_grader(ravarde): och lät tolken lista ut resten. C++ vill veta två saker till: vad funktionen lämnar tillbaka och vad varje parameter är för sorts värde.

// Kalibreringen, uppmätt mot en referenstermometer. Byt mot dina egna tal.
const int   RA_VID_20 = 569;    // ravardet da referensen visade 20,0 grader
const int   RA_VID_30 = 456;    // ravardet da referensen visade 30,0 grader
const float TEMP_20   = 20.0;
const float TEMP_30   = 30.0;

float ravardeTillGrader(int ravarde) {
  float k = (TEMP_30 - TEMP_20) / (RA_VID_30 - RA_VID_20);
  return TEMP_20 + k * (ravarde - RA_VID_20);
}

Rad för rad:

Anropet ser ut som vilket annat anrop som helst:

int   ravarde = analogRead(A0);
float grader  = ravardeTillGrader(ravarde);

Kontrollera typerna en gång, för det är där nybörjarfelen sitter. TEMP_30 - TEMP_20 är 10.0, ett flyttal. RA_VID_30 - RA_VID_20 är -113, ett heltal. När ett flyttal delas med ett heltal räknar C++ i flyttal, så k blir -0.0885 och inte 0. Hade båda talen varit heltal hade svaret blivit 0, och funktionen hade lämnat 20,0 vad du än matade in. Det är områdets vanligaste tysta fel.

Var funktionen ska stå Skriv dina egna funktioner ovanför setup(). Arduino-miljön är visserligen snäll och klarar oftast att du skriver dem längst ned, men i vanlig C++ måste en funktion vara känd innan den används. Att alltid lägga dem överst är en vana som fungerar i båda världarna och kostar ingenting.

map() och vad den gör med decimalerna

map() är Arduinos färdiga omskalning. Den räknar precis det här:

ut = (x − in_min) · (ut_max − ut_min) / (in_max − in_min) + ut_min

Alltså exakt samma räta linje som ovan, uttryckt med två punkter i varje ände i stället för med lutning. Bekvämt – med en enda hake, och den är stor.

map() räknar i heltal. Alla fem talen är heltal och divisionen är en heltalsdivision, som kapar decimalerna i stället för att avrunda dem. Räkna igenom det klassiska exemplet:

map(512, 0, 1023, 0, 5)

= (512 − 0) · (5 − 0) / (1023 − 0) + 0 = 512 · 5 / 1023 = 2560 / 1023 = 2,50…

och heltalsdivisionen kapar det till

2

Inte 2,5. Utsignalen kan bara bli 0, 1, 2, 3, 4 eller 5 – sex möjliga värden ur 1024 möjliga insignaler. Nästan hela din upplösning kastades bort på en rad som såg rimlig ut.

Ett annat kort kan svara 3 På ditt Uno-kort blir svaret 2, varje gång – Arduino-kärnan för AVR trunkerar. Kör raden själv och se det, det står som första steg i Prova själv nedan. Byter du senare till ett ESP32-kort blir samma rad 3: den kärnan lägger till halva nämnaren före divisionen och avrundar därmed i stället för att kapa. Poängen är densamma i båda fallen: 2,5 finns inte bland de möjliga svaren, och ett svar som inte går jämnt ut kan skilja ett steg mellan två kärnor. Räkningarna längre ned i guiden är valda så att de går jämnt ut och därför ger samma svar på båda.

Två utvägar finns, och båda används. Den första är att skala till en finare enhet i stället för till en grov:

map(512, 0, 1023, 0, 5000) = 512 · 5000 / 1023 = 2 560 000 / 1023 = 2502

2502 millivolt, alltså 2,502 V. Nu ryms hela upplösningen, eftersom utområdet har fler steg än inområdet. Regeln: map() är bra så länge utområdet är minst lika stort som inområdet.

Den andra utvägen är att inte använda map() alls när svaret ska ha decimaler, utan räkna med float som i funktionen ovan. För en temperatur i grader är det nästan alltid rätt val. För en servovinkel i hela grader är map() rätt val, eftersom svaret ändå ska bli ett heltal.

map() begränsar ingenting

Den andra hälften av fällan är att map() inte bryr sig om sina egna gränser. Ligger insignalen utanför in_min … in_max räknar formeln glatt vidare och lämnar ett svar utanför ut_min … ut_max.

Anta att du kalibrerat ditt instrument på råvärdena 200 till 800 och mappar dem till en servovinkel 0 till 180 grader. En dag blir det varmare än du någonsin mätte, och råvärdet blir 900:

map(900, 200, 800, 0, 180) = (900 − 200) · 180 / (800 − 200) = 700 · 180 / 600 = 126 000 / 600 = 210

Tvåhundratio grader. Åt andra hållet, med råvärdet 100:

map(100, 200, 800, 0, 180) = (100 − 200) · 180 / 600 = −18 000 / 600 = −30

Minus trettio grader. Vad som händer sedan beror på vad du matar talet till, och inget av alternativen är bra:

constrain() är motmedlet. Den tar ett värde och två gränser och lämnar värdet klippt till intervallet:

const int RA_MIN = 200;      // ravardet vid kalibreringens ena ande
const int RA_MAX = 800;      // ravardet vid den andra

int ravarde = analogRead(A0);
int begransat = constrain(ravarde, RA_MIN, RA_MAX);
int vinkel = map(begransat, RA_MIN, RA_MAX, 0, 180);

constrain(900, 200, 800) ger 800, och vinkeln blir då 180 – samma slutresultat som servobiblioteket ändå hade gett, men nu med ditt beslut i din kod i stället för i ett bibliotek vars beteende du hade fått slå upp.

Bättre än att bara klippa är att klippa och säga till. Riktiga instrument gör det: en multimeter visar OL, en våg blinkar. Ditt bygge kan göra samma sak med en rad:

if (ravarde > RA_MAX || ravarde < RA_MIN) {
  Serial.println("utanfor omradet");
}

Brus: en enda avläsning darrar

Läs samma stillastående givare sexton gånger i rad och du får sällan sexton lika tal. Typiskt kan det se ut så här:

511 513 512 510 514 512 511 513

Det är brus: små slumpmässiga störningar från omvandlaren själv, från matningen och från allt elektriskt i rummet. Det går inte att koppla bort. Men det går att räkna bort, eftersom bruset lika gärna drar uppåt som nedåt: lägger du ihop många avläsningar tar avvikelserna delvis ut varandra.

Regeln är att medelvärdet av N prov har ett slumpmässigt fel som är √N gånger mindre än en enskild avläsnings. Med sexton prov:

√16 = 4, alltså en fjärdedel av bruset

Att komma ned ytterligare en fjärdedel kräver sexton gånger fler prov:

√256 = 16, och 256 · 104 µs = 26 600 µs = 27 ms

Sexton prov kostar däremot bara

16 · 104 µs = 1 664 µs = 1,7 ms

Det är därför sexton eller trettiotvå är de tal du ser överallt: de tar bort merparten av bruset för nästan ingen tid alls, och steget därefter kostar mycket och ger lite.

Att läsa sexton gånger kräver en loop. Så här ser en for-loop ut i C++:

const int GIVARE_PIN = A0;
const int ANTAL_PROV = 16;

int medelvarde() {
  long summa = 0;
  for (int i = 0; i < ANTAL_PROV; i++) {
    summa = summa + analogRead(GIVARE_PIN);
  }
  return summa / ANTAL_PROV;
}

Parentesen efter for innehåller tre delar åtskilda av semikolon: startvärde (int i = 0), villkor att fortsätta (i < ANTAL_PROV) och vad som sker efter varje varv (i++, som betyder i = i + 1). Loopen körs alltså med i lika med 0, 1, 2 … 15 – sexton varv. Att räkna från 0 och använda < i stället för <= är standard, och det passar ihop med hur arrayer numreras, vilket är nästa stycke.

summa är en long och inte en int, och det är inte pedanteri. Sexton avläsningar på högst 1023 ger

16 · 1023 = 16 368

vilket ryms i en int, som slutar vid 32 767. Men sextiofyra avläsningar ger

64 · 1023 = 65 472

och då har talet vikt runt till ett negativt värde utan ett ord om saken. Skriv long från början så är frågan avklarad oavsett hur många prov du väljer.

Arrayen: att spara proven och mäta sitt eget brus

Medelvärdet ovan behövde ingen array – summan räckte. Men medelvärdet berättar inte hur mycket givaren darrar, och det är ett tal du vill ha: det är gränsen för vad ditt instrument kan visa, och det är ett mätvärde som hör hemma i dokumentationen. För att räkna på proven i efterhand måste de sparas, och sexton tal som hör ihop sparas i en array.

const int GIVARE_PIN = A0;
const int ANTAL_PROV = 16;

int prov[ANTAL_PROV];        // sexton platser for int, numrerade 0 till 15

void fyllProv() {
  for (int i = 0; i < ANTAL_PROV; i++) {
    prov[i] = analogRead(GIVARE_PIN);
  }
}

int spridning() {
  int minsta  = prov[0];
  int storsta = prov[0];
  for (int i = 1; i < ANTAL_PROV; i++) {
    if (prov[i] < minsta)  { minsta  = prov[i]; }
    if (prov[i] > storsta) { storsta = prov[i]; }
  }
  return storsta - minsta;
}

Tre saker skiljer en C++-array från en Python-lista, och alla tre går att läsa ut ur raden int prov[ANTAL_PROV];:

Med de två funktionerna på plats kan bygget rapportera både sitt mätvärde och sin egen osäkerhet:

fyllProv();

long summa = 0;
for (int i = 0; i < ANTAL_PROV; i++) {
  summa = summa + prov[i];
}
int medel = summa / ANTAL_PROV;

Serial.print(medel);
Serial.print("  spridning ");
Serial.println(spridning());

Är spridningen sex steg är det, med kalibreringen från början av guiden,

6 · 0,0885 = 0,53 °C

mellan största och minsta enskilda avläsning. Efter medelvärdesbildning över sexton prov är den slumpmässiga delen en fjärdedel av det:

0,53 / 4 = 0,13 °C

Det talet är svaret på frågan "hur många decimaler får jag skriva ut?" Två decimaler vore att påstå mer än instrumentet vet. En decimal är ärligt.

Medelvärde tar bort brus, inte ett systematiskt fel

Det här är områdets viktigaste begreppsskillnad, och den som blandas ihop oftast.

Exempel på ett systematiskt fel som kalibreringen fångar. Ditt fasta motstånd är märkt 10 kΩ men är i verkligheten 10,4 kΩ, vilket ryms väl inom en femprocentig tolerans. Vid 25 °C, med en NTC på 10,0 kΩ, blir spänningen

Uut = 5 · 10 000 / (10 400 + 10 000) = 50 000 / 20 400 = 2,451 V

i stället för 2,500 V, alltså ett råvärde på

rå = 2,451 · 1023 / 5 = 501, i stället för 512

Elva steg fel, varje gång, åt samma håll. Översatt till temperatur:

11 · 0,0885 = 0,97 ≈ 1,0 °C

En hel grad. Och tvåpunktskalibreringen tar bort hela felet, eftersom båda punkterna mättes upp med samma motstånd på plats. Det är hela skälet att man kalibrerar den färdiga kedjan i stället för att räkna på delarna var för sig.

Exempel på ett systematiskt fel som kalibreringen inte fångar. Referenstermometern du kalibrerade mot visar en halv grad för lite. Då blir båda dina punkter en halv grad för låga, linjen genom dem likaså, och varje avläsning ditt bygge gör är en halv grad för låg. Ingen mängd prov, ingen bättre kod och ingen tredje kalibreringspunkt mot samma termometer hittar det. Bara en bättre referens gör det. Det är därför industrins kalibreringar går tillbaka till en normal som i sin tur är kalibrerad, och det är därför du ska skriva i dokumentationen vilken referens du använde.

En trevlig egenskap: matningen räknar bort sig själv Omvandlarens referens är kortets 5 V, och din spänningsdelare matas från samma 5 V. Sjunker matningen till 4,8 V sjunker både spänningen på pinnen och det som ett steg är värt, i samma proportion – och råvärdet blir oförändrat. Kalibrerar du direkt från råvärde till grader, som i den här guiden, försvinner alltså matningens fel av sig självt. Går du omvägen över volt och räknar med 5,00 V som om det vore sanning, får du tillbaka felet som ett systematiskt fel på några procent. Den kortare vägen är den bättre.

Felkällorna, samlade

FelkällaSlagSå syns den i mätvärdetBotemedel
Brus i omvandlare, matning och ledningarslumpmässigttalet darrar några steg fram och tillbakamedelvärde av N prov, felet delas med √N
Glapp i kopplingsdäcketslumpmässigt, hoppandeenstaka helt orimliga tal mellan normalatryck ned benen, byt hål, sålla bort ytterlägen
Tolerans i det fasta motståndetsystematisktsyns inte – alla värden lika mycket feltvåpunktskalibrering av hela kedjan
Referensen visar felsystematisktsyns inte allsbättre referens. Inget annat hjälper
Mätning utanför kalibreringsområdetsystematiskt, växandefelet växer ju längre bort du kommerkalibrera i det område du mäter i, och constrain
Givarens tröghetdynamisktvärdet släpar efter vid snabba ändringarvänta in värdet, och redovisa svarstiden
Egenuppvärmning i givarensystematisktnågot för hög temperatur, stadigtstörre motstånd i delaren, alltså mindre ström

I industrin och i vardagen

I industrin: en trycktransmitter i en processanläggning kalibreras i exakt två punkter, och de har egna namn: nollpunkt och spann. Nollpunkten ställs med givaren öppen mot atmosfären, spannet mot en referens som lämnar ett känt tryck. Båda talen skrivs in i ett kalibreringsprotokoll med datum, instrumentets serienummer och referensens identitet – och referensen är i sin tur kalibrerad mot en normal, som är kalibrerad mot en nationell normal. Kedjan kallas spårbarhet, och den finns för att lösa exakt det problem din referenstermometer hade.

Storleksordningen på vad som står på spel: en transmitter för 0–10 bar vars nollpunkt glidit 0,1 bar ger ett fel på

0,1 / 10 = 1 % av mätområdet, i varje avläsning

Det låter lite. I en dosering som blandar två komponenter efter tryck betyder en procent åt fel håll att varje sats blir fel, och det syns inte i mätvärdet – det syns i produkten, veckor senare. Därför är kalibreringsintervall inskrivna i underhållsplanen, inte något man gör när man misstänker något.

I vardagen: badrumsvågen kalibrerar sin nollpunkt varje gång du använder den. Den nollställer sig själv medan den står tom – det är kalibreringspunkt ett, och den sköts åt dig. Men den har ingen andra punkt, och därför kan dess spann glida utan att någonting ser konstigt ut. Vågen visar fortfarande noll när den är tom och ett rimligt tal när du står på den.

Det går att testa på två minuter. Väg dig. Ta upp en hantel eller en vattendunk du vet väger 10,0 kg och väg dig igen. Skillnaden ska bli 10,0 kg. Visar den 9,4 kg är spannet

(10,0 − 9,4) / 10,0 = 0,06 = 6 % fel

och på en person som väger 70 kg betyder det

70 · 0,06 = 4,2 kg

Fyra kilo, systematiskt, i varje vägning – utan att någonting darrar eller ser trasigt ut. Det är samma sorts fel som trycktransmitterns spann, mätt med samma sorts tvåpunktstanke, och det är därför en våg som används i handeln måste kontrolleras med kända vikter och inte bara nollställas.

Prova själv

  1. Kör raden Serial.println(map(512, 0, 1023, 0, 5)); i setup(). Ditt Uno-kort ska svara 2, och att se det med egna ögon är hela poängen: trunkeringen är inget påstående i en guide utan något du just mätt. Kör sedan Serial.println(map(512, 0, 1023, 0, 5000)); och jämför. Skriv en mening om varför den andra raden är användbar och den första inte är det.
  2. Skriv medelvarde() ur guiden och skriv ut både en enskild analogRead och medelvärdet på samma rad, fyra gånger i sekunden. Titta i en halv minut på en stillastående givare. Vilken av de två kolumnerna darrar mest?
  3. Lägg till arrayen och spridning(). Anteckna spridningen i två lägen: med kortet på USB från datorn, och med kortet på en powerbank eller ett batteri. Skiljer det?
  4. Kalibrera din NTC i två punkter. Punkt ett: ett glas kranvatten med en vanlig termometer i. Punkt två: samma glas efter att du hällt i varmt vatten. Vänta en minut vid varje punkt så att givaren hinner med, och anteckna par av referens och råvärde.
  5. Skriv ravardeTillGrader() med dina egna två punkter och låt bygget skriva ut grader med en decimal. Kontrollera mot termometern vid en tredje temperatur som ligger mellan dina två punkter. Skriv ned skillnaden – det är din kalibrerings verkliga fel.
  6. Ändra TEMP_20 och TEMP_30 från const float till const int, och skriv 20 och 30 utan decimal. Ladda upp och se vad funktionen svarar nu, vid tre olika råvärden. Räkna ut varför innan du ändrar tillbaka.
  7. Mät en fjärde punkt långt utanför dina två, till exempel iskallt vatten. Jämför vad linjen säger med vad termometern säger, och jämför avvikelsen med tabellen i guiden.

Kontrollfrågor

Du mäter rå = 800 vid 5,0 °C och rå = 400 vid 45,0 °C. Vad blir lutningen, och vad visar rå = 600?

k = (45,0 − 5,0) / (400 − 800) = 40,0 / (−400) = −0,100 °C per steg. Linjen blir T = 5,0 − 0,100 · (rå − 800), och vid rå = 600: T = 5,0 − 0,100 · (−200) = 5,0 + 20,0 = 25,0 °C. Mitt emellan, som sig bör när punkten ligger mitt emellan.

Vad blir map(1023, 0, 1023, 0, 180), och vad blir map(1100, 0, 1023, 0, 180)?

Den första blir 1023 · 180 / 1023 = 180, alltså precis toppen. Den andra blir 1100 · 180 / 1023 = 198 000 / 1023 = 193,5…, alltså 193 – utanför det utområde du själv angav. map() kontrollerar ingenting; det gör constrain().

Din givare darrar sex steg. Du går från 16 till 64 prov. Hur mycket bättre blir det, och vad kostar det?

√64 / √16 = 8 / 4 = 2, alltså hälften så stort slumpmässigt fel. Kostnaden är 64 · 104 µs = 6,7 ms i stället för 1,7 ms. Dubbelt så bra för fyra gånger tiden – och nästa halvering kostar fyra gånger till. Det är därför man sällan går över några tiotal prov.

Ditt bygge visar konsekvent 2 grader för mycket. Hjälper det att läsa 256 prov i stället för 16?

Nej, inte det minsta. Ett fel som är lika stort och åt samma håll varje gång är systematiskt, och ett medelvärde av felaktiga tal är lika felaktigt som talen. Kalibrera om, och kontrollera först att referensen du kalibrerar mot är den du tror.

Varför står det long summa och inte int summa?

Därför att summan växer med antalet prov. 16 · 1023 = 16 368 ryms i en int, men 64 · 1023 = 65 472 gör det inte – en int tar slut vid 32 767 och viker runt till ett negativt tal utan varning. Symtomet är ett medelvärde som plötsligt blir negativt när du ökade antalet prov.

Vad händer om du skriver prov[16] i en array som deklarerats int prov[16];?

Platserna heter 0 till 15, så prov[16] ligger utanför. C++ säger ingenting, varken vid kompilering eller vid körning – du läser eller skriver i minnet bredvid arrayen. Symtomet är att en helt annan variabel ändrar sig av sig själv, vilket är ett av de svåraste felen att hitta. Låt loopens villkor vara i < ANTAL_PROV och problemet uppstår aldrig.

Varför ska omvandlingen ligga i en funktion i stället för att skrivas där den behövs?

Därför att den behövs på mer än ett ställe – i nästa guide både på seriemonitorn och på servot – och två kopior av samma formel betyder två ställen att rätta när kalibreringen ändras. Det är också självdokumenterande: ravardeTillGrader(medel) säger vad som händer, medan samma formel utskriven mitt i loop() bara ser ut som räkning.

Vanliga fel

Vidare

Tre kapitel i språkreferensen öppnar med den här guiden: Loopar, Funktioner och Arrayer. De är guidens uppslagsdel – här står de i den ordning mätproblemet krävde dem, där står de i sin egen ordning med alla detaljer samlade.

Nästa guide är Datablad. Där byter vi ut de givare du kan mäta upp själv med en multimeter mot sådana där talen måste hämtas ur tillverkarens dokument – och där ett bibliotek för första gången är rätt svar på en givare.