Automatisk kalibrering av AD-omvandlare i AVRmicrocontroller

Planering och tankar kring eventuella framtida projekt.
Användarvisningsbild
jesse
Inlägg: 9240
Blev medlem: 10 september 2007, 12:03:55
Ort: Alingsås

Automatisk kalibrering av AD-omvandlare i AVRmicrocontroller

Inlägg av jesse »

Hej på er.

Vill bara berätta lite om vad jag sysslar med just nu. Jag håller på att rita på en prototyp till en produkt som ska användas i elbilar. Den ska logga ett antal DC-spänningar.

Den AVR microcontroller jag ska använda har en 12-bitars AD-omvandlare med intern referens på 2.56V. Jag kan alltså mäta exakta spänningar mellan 0 och 2.55 volt. För att kunna mäta större spänningar behövs en spänningsdelare på varje AD-ingång. Det handlar om många AD ingångar och det är inte roligt att hålla på att kalibrera dessa med en trimpot på varje ingång.

:idea: Då kom jag på idén om automatisk kalibrering. Kanske värdefullt för fler att ta del av tänkte jag, eftersom det väsentligt förenklar proceduren att kalibrera. Efter att ha funderat ett tag gjorde jag så här:

Jag använder vanliga motstånd med 5% tolerans som spänningsdelare och håller mig inom marginalen så att jag kommer under 2.56 volt in vid max U-in. Vid kalibrering ansluter jag alla ingångar till en känd stabil spänning, t.ex. 12.00 Volt. Jag låter processorn läsa av värdena och dividera det ideala värdet 12.00 V med dessa. kvoten (ett 16-bitars tal) sparas sedan i processorns EEPROM.

När jag sedan ska avläsa ett spänningsvärde multiplicerar jag det avlästa värdet med kvoten - och vips - så har jag fått fram det exakta värdet!


värdet 2.56 V motsvaras internt av heltalet 65536. Antag att spänningsdelaren är 1/16 - t.ex 150KΩ + 10KΩ ...

då är ideala 12.00 volt in = 0.75 volt på AD-ingången --> avläst värde 19200.

antag att resistorena avviker en del och spänningen in blir 0.6965 volt istället. Då kommer AD-omvandlaren läsa värdet 17808...

För att kompensera det inlätsta värdet måste jag alltså ta det och multiplicera med 19200/17808 = 1.0781671

Jag konstruerade en enkel algoritm (med bara 17 AVR-RISK instruktioner!) som skapar ett 16-bitars binaltal där MSB (bit 15) har värdet 1 och bitarna efter har värdena 0.5 - 0.25 - 0.125 - 0.0625 - osv... dvs negativa potenser på 2. Med detta tal kan jag representera allt från 0 till 1.99997 (eller 0.000 0000 0000 0000 till 1.111 1111 1111 1111 binärt) vilket duger bra i det här läget.

Division handlar ju om att subtrahera för att kolla om resultatet är positivt och i så fall skriva dit en siffra på rätt position i resultatet, och sedan ta resten och göra om subtraktionen tills du har tillräckligt många decimaler (minns divisionen från mellanstadiet)

Algoritm:

Bild

EDIT: Ovanstående flödesschema förutsätter att båda talen är väldigt höga - annars förlorar man i noggrannhet. Därför har jag lagt till en loop i början (se kod nedan) som multiplicerar upp talen till högsta möjliga 16-bitars värde innan divisionen startar. Kanske ett klumpigt sätt att ta hand om problemet på, men det var det enklaste jag kunde komma på just nu utan att göra om algoritmen helt och hållet.

Kod för AtTiny25:

Kod: Markera allt


; standardsetting
.INCLUDE "tn25def.inc" ; Attiny25

; ------------------EXEMPELTAL--------------------
; Testprogram för att testa subrutinerna binaldiv
; och binalmult
;
.EQU TALJ  = 27955	; A     : Exempel på värden som
.EQU NAEMN = 31578	; B     : ska divideras.
; resultatet ska bli ca  27955/31578=0,88527
; resultatet blir i verkligheten 0,88531494140625 D
; eller 0.111 0001 0101 0010 Binärt
;-----------------------------------------------------

; definition av variabler
; Aritmetiska register
.DEF Tmp = r16
.DEF Bit= r17
.DEF  Al= r18
.DEF  Ah= r19
.DEF  Bl= r20
.DEF  Bh= r21
.DEF  Cl= r22
.DEF  Ch= r23
.DEF  Dl= r24
.DEF  Dh= r25


.CSEG 
.ORG 0000
	rjmp Reset

; ------------------------------------------------------------------
; Här startar huvudprogrammet ( testprogram)
;
; testprogrammet utför först en division ( D = A / B )
; och sedan en miltiplikation B * D för att återfå värdet
; som fanns i A för att jämföra med ursprungliga värdet i A. 
;-------------------------------------------------------------------

.ORG 0x10
Reset:
	ldi Tmp , LOW(RAMEND)	; Initiera stacken
	out SPL, Tmp

;----------MAIN-------------
	ldi Al,LOW(TALJ)
	ldi Ah,HIGH(TALJ)
	ldi Bl,LOW(NAEMN)
	ldi Bh,HIGH(NAEMN)

	push Al ; spara A
	push Ah

	push Bl ; spara B
	push Bh

	rcall Binaldiv           ; beräkna D = A / B

	; nu ska B*D bli A om det stämmer...
	; testar det här nedan

	pop Ah  ; ta fram gamla sparade B till A
	pop Al
	mov Bl,Dl  ; flytta resultatet D till B
	mov Bh,Dh
	
	rcall binalmult        ; beräkna nu C = A * B

	pop Ah  ; återkalla värdet som var i A från början.
	pop Al  ; för att kunna jämföra med resultatet

	; resultatet C ska nu likna
	; innehållet i A
Slut:	
	rjmp slut
;---------------------SLUT PÅ MAIN-----------------------------

;-------------SUBRUTIN BINAL DIVISION ----------------------
;   D = A / B      ( D = Dl:Dh  A = Al:Ah,  B =  Bl:Bh,  C = Cl: Ch )
;
; division av två sextonbitars tal med ett 16-bitars decimaltal
; som resultat från 0.000000000000000 till 
; 1.111111111111111 (0-1.99998)
;
;-------------------------------------------------------------------


Binaldiv:
	; D behöver ej nollställas i förväg
	ldi Bit, 16          ; kör 16 varv
	;högsta upplösning börjar vid bit 15
shifta:
	cpi Ah,128 ; kolla bit 7
	brcc loop_bdiv
	cpi Bh,128 ; kolla bit 7
	brcc loop_bdiv
	;shifta båda talen vänster
	lsl Al
	rol Ah
	lsl Bl
	rol Bh
	rjmp shifta
loop_bdiv:
	lsl Dl          ; flytta vänster tidigare bitar i resultatregistret D
	rol Dh
	;  kolla om B=0... KANSKE ONÖDIGT?
	;cpi Al,0
	;brne ej_noll
	;cpi Ah,0
	;breq negativ
	;ej_noll:
	mov Cl,Al          ; C = A  (vissa processorer har instruktionen movw som är 16-bitars)
	mov Ch,Ah
	sub Cl,Bl          ; C = C - B
	sbc Ch,Bh
	brcs negativ          ; hoppa om resultatet är negativt
	
	; positiv
	inc Dl          ; sätt etta I Dl's bit 0 (denna shiftas sedan åt vänster)
	mov Al,Cl          ; A = C (spara resten i A)
	mov Ah,Ch
	
negativ:
	lsr Bh          ; shifta höger summan i B för att jämföra med resten i A i nästa varv.
	ror Bl          ; (lsr flyttar LSB till Carry. ror shiftar in carry till MSB )
	dec Bit          ; räkna ner antal bitar kvar
	brne loop_bdiv          ; loopa
	ret          ; återvänd från subrutin	
	
;----------SUBRUTIN BINAL MULTIPLIKATION ----------------
; här kommer multiplikationsprogrammet som behövs för att
; kunna skapa ett korrekt läsvärde av en inläst spänning.
;------------------------------------------------------------------
binalmult:
                ;-----------------------------------------------------
	; multiplicering av 16-bitars heltalet A med
	; binalen B med formatet x.xxx xxxx xxxx xxxx xxxxB
	; resultatet i hamnar i C.   Bit används som räknare.
                ; -----------------------------------------------------

	clr Cl ; nollställ först C
	clr Ch
	ldi Bit,16 ;antal bitar multiplikation
binmul_loop:
	lsl Bl	; plocka ut MSB i B till carry-
	rol Bh  ; flaggan.
	brcc bmul_nolla ; biten var en nolla

	; en etta - addera!
	add Cl,Al
	adc Ch,Ah
	brcs bimul_overflow

bmul_nolla:
	lsr Ah ; dela A med 2
	ror Al
	dec Bit
	brne binmul_loop
	ret


bimul_overflow:
	; vid overflow sätts resultatet till max
	; och carry = 1
	ldi Ch,0xFF
	ldi Cl,0xFF
	ret

;--------------------------------KOD SLUT-----------------------

[/color]

Processorn kan programmeras enligt följande:

1) vid reset läses en byte av i EEPROM som är satt till ett visst värde om kretsen redan är kalibrerad. Om den är det går man vidare till huvudprogrammet omedelbart.
2) om inte, skall kretsen kalibreras. Indikera t.ex. på display eller med lysdios (t.ex. blinkar) att kretsen väntar på kalibrering.
3) anslut en exakt spänningskälla på ingången (före spänningsdelaren förstås)
4) ge signal utifrån till kretsen (t.ex en knapp till en digital ingång) när spänningen är påsatt.
5) kresten läser in det analoga värdet och gör en grovkoll att värdet är ungefär rätt (annars ges ett felmeddelande)
6) kretsen dividerar sedan det "rätta" (ideala) värdet med det inlästa med ovan skrivna algoritm.
7) det 16-bitars decimaltalet lagras i EEPROM som kalibreringsvärde. Har man flera AD-ingångar går man igenom processen för varje ingång och lagrar data i EEPROM.
8) signalera att kalibrering lyckats och gå till SLEEP. (måste startas om för att fungera igen)

i huvudprogrammet läser man sedan in ett värde på ingången och multiplicerar med decimaltalet i EEPROMET. Det gör du enlkelt genom att shifta höger läsvärdet och addera (eller inte addera beroende på etta eller nolla) för varje bit man betar av på decimaltalet. Resultatet blir ett exakt kalibrerat läsvärde.



(ett binärt decimaltal kallas egentligen för binaltal, men jag har skrivit decimaltal för att man ska fatta - deci= 1/10 bi = 1/2 )
Pedagogiskt exempel på omvandling av ett decimaltal till binaltal:

Uttryck talet π med ett binärt tal med 12 binaler. π ≈3.1416

3 är heltalsdelen = 11B.
resten = 0.1416 ska delas upp i binaler:
.1416-0.5 (2^-1) (neg=0)
.1416-0.25 (2^-2) (neg=0)
.1416-0.125 (2^-3)(pos=1) = 0.0166 rest
.0166-.0625 (2^-4)(neg=0)
.0166-.03125 (2^-5)(neg=0)
.0166- 0.015625 (2^-6) (pos=1) = 0.000975 rest
.000975 - 0.0078125 (2^-7) (neg=0)
.000975 - 0.00390625 (2^-8)(neg=0) ehh.. den gubben var inte meningen 2^-8
.000975 - 0.001953125 (2^-9) (neg=0)
.000975 - 0.0009765625 (2^-10) (neg=0)
.000975 - 0.000488281250 (2^-11) (pos=1) = 0.00048671875 rest
.000486718750 - .000244140625 (2^-12) (pos=1) = 0.000242578125 rest


Svar π ≈ 11.001001000011 (avvikelse max + 2^-12 dvs. +.00025)
Senast redigerad av jesse 16 februari 2008, 03:20:03, redigerad totalt 7 gånger.
Användarvisningsbild
jesse
Inlägg: 9240
Blev medlem: 10 september 2007, 12:03:55
Ort: Alingsås

Inlägg av jesse »

En liten kommentar till koden:

koden stämmer inte helt med flödesschemat även om principen är densamma. Jag gör inte som på schemat att jag börjar med att sätta Bit 15 på D, sedan bit14, 13 , 12 osv... till 0 eftersom det programmeringsmässigt är krångligt att sätta bitar på olika ställen i register (särskilt som det är 16-bitar = två olika register att hålla reda på) så jag har gjort på ett traditionellt sätt : jag sätter alltid bit 0 med etta eller nolla (beroende på resultatet) och shiftar sedan hela registret ett steg åt gången 16 gånger tills bit 0 har hamnat på position 15. Då är programmet klart.

kommentar 2: Jag har inte hunnit provköra koden än. (lite sent på natten, ehmm) men är 99% säker på att den ska fungera (med reservation för stavfel o dyl). Återkommer när jag testat.

EDIT: hoppsan - en hel del syntaxfel o.dyl.. samt att jag glömt att vissa förutsättningar måste råda för att få 16 bitars upplösning på svaret. Kommer att arbeta om koden och provköra den för att sedan ändra här...

EDIT: koden fungerar nu, men noggrannheten är lägre än 16 bitar - de tre sista bitarna kan avvika... har inte kollat orsaken till detta. Det är irrelevant i sammanhanget då AD-omvandlaren är 12-bitars, så resultatet kan aldrig bli noggrannare än 12 bitar ändå, så programmet funkar ändå i praktiken.
Skriv svar